服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - 编程技术 - WKWebview 秒开的实践及踩坑之路

WKWebview 秒开的实践及踩坑之路

2021-07-28 23:46网罗开发伤心的Easyman 编程技术

一般情况下,只要对照这个列表,对比差异就基本能搞定绝大部分前端性能问题了。不过我们在里面仔细再分析下,对首屏启动速度影响最大的就是网络请求,包括请求 HTML、css、image 等静态资源和展示数据的请求。

WKWebview 秒开的实践及踩坑之路

优化背景

  • 众所周知,H5 的部分优势(开发快,迭代快,热更新)是很明显的,公司客户端的部分业务都是由 H5 来实现的,网络好的情况下体验也是很不错的
  • 但是其实 H5 的体验是比原生差的,这就需要想办法如何提高 H5 加载速度,优化体验,首屏的加载速度还是很影响体验的

加载速度

关于加载速度慢有很多文章都已经详细解释了,h5在加载工作中做了很多事

初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片

WKWebview 秒开的实践及踩坑之路

一般页面在 dom 渲染后才能展示,可以发现,H5 首屏渲染白屏问题的原因关键在于,如何优化减少从请求下载页面到渲染之间这段时间的耗时。

前后端优化

这其中可做的优化特别多,前后端能够做的是:

  • 降低请求量:减少 HTTP 请求数, 合并资源,minify / gzip 压缩,webP,lazyLoad。
  • 因为手机浏览器同时响应请求是 4 个,4 个的请求数也许不是特别靠谱,没有查到出处,但是肯定是越少越好。
  • HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
  • 加快请求速度:预解析 DNS,减少域名数,并行加载,CDN 分发。
  • 渲染:JS/CSS 优化,加载顺序,服务端渲染模板直出。

一般情况下,只要对照这个列表,对比差异就基本能搞定绝大部分前端性能问题了。不过我们在里面仔细再分析下,对首屏启动速度影响最大的就是网络请求,包括请求 HTML、css、image 等静态资源和展示数据的请求。所以客户端内,优化最关键的其实就是如何缓存这些网络资源,也就是离线包缓存方案。

离线包方案的实践

方案选型是两种

  • 基于 LocalWebServer 实现 WKWebView 离线资源加载
  • 使用 WKURLSchemeHandler 实现 WKWebView 离线资源加载

LocalWebServer

基于 iOS 的 local web server,目前大致有以下几种较为完善的框架:

  • CocoaHttpServer (支持 iOS、macOS 及多种网络场景)
  • GCDWebServer (基于 iOS,不支持 https 及 webSocket)
  • Telegraph (Swift 实现,功能较上面两类更完善)

当时采用的是 GCDWebServer,在打开 APP 后直接启动Webserver,H5 的链接直接替换成本地 localhost + 端口号链接的地址。

本来的方案是本地服务器和远端h5服务器同步下载资源,下载后客户端请求本地服务器的路径,如未找到相应的资源再请求远端服务器的资源文件。

测试过程中碰到很多奇怪的问题(暂不一一举例),也有提到以下问题并且时间紧急所以并未做进一步的深入:

  • 资源访问权限安全问题
  • APP 前后台切换时,服务重启性能耗时问题
  • 服务运行时,电量及 CPU 占有率问题
  • 多线程及磁盘 IO 问题

WKURLSchemeHandler

关于离线包

前端项目的静态资源直接打包成 zip 包,APP 在启动时开始下载该包并解压到本地。WKWebview 通过 WKURLSchemeHandler 拦截并加载本地资源文件。关于离线包的分发,就是普通的 zip 离线包和一个版本控制的 json 文件,每次打离线包会修改 json 文件里的版本号,并附有离线包下载地址。此处可以优化的更好,但暂时并不需要太复杂。

离线包的下载和解压

只是简单的下载并解压到本地资源路径,关于版本比对的代码这里没有展示出来,自行注意,避免每次都全量更新。

  1. /* 创建网络下载对象 */ 
  2. AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; 
  3. /* 下载地址 */ 
  4. NSURL *url = [NSURL URLWithString:request.urlParameters.path]; 
  5. NSURLRequest *request = [NSURLRequest requestWithURL:url]; 
  6. /* 下载路径 */ 
  7. //获取Document文件 
  8. NSString * docsdir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 
  9. NSString * zipFilePath = [docsdir stringByAppendingPathComponent:@"zip"];//将需要创建的串拼接到后面 
  10. NSString * H5FilePath = [docsdir stringByAppendingPathComponent:@"H5"]; 
  11. NSFileManager *fileManager = [NSFileManager defaultManager]; 
  12. BOOL zipIsDir = NO
  13. BOOL H5IsDir = NO
  14. // fileExistsAtPath 判断一个文件或目录是否有效,isDirectory判断是否一个目录 
  15. BOOL zipexisted = [fileManager fileExistsAtPath:zipFilePath isDirectory:&zipIsDir]; 
  16. BOOL H5Existed = [fileManager fileExistsAtPath:H5FilePath isDirectory:&H5IsDir]; 
  17. if ( !(zipIsDir == YES && zipexisted == YES) ) {//如果文件夹不存在 
  18.     [fileManager createDirectoryAtPath:zipFilePath withIntermediateDirectories:YES attributes:nil error:nil]; 
  19. if (!(H5IsDir == YES && H5Existed == YES) ) { 
  20.     [fileManager createDirectoryAtPath:H5FilePath withIntermediateDirectories:YES attributes:nil error:nil]; 
  21. //删除 
  22. //        [[NSFileManager defaultManager] removeItemAtPath:zipFilePath error:nil]; 
  23. NSString *filePath = [zipFilePath stringByAppendingPathComponent:url.lastPathComponent]; 
  24. /* 开始请求下载 */ 
  25. NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) { 
  26.     NSLog(@"下载进度:%.0f%", downloadProgress.fractionCompleted * 100); 
  27. } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) { 
  28.     /* 设定下载到的位置 */ 
  29.     return [NSURL fileURLWithPath:filePath]; 
  30. } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) { 
  31.     NSTimeInterval delta = CACurrentMediaTime() - self->start; 
  32.      NSLog(@"下载完成,耗时:%f",delta); 
  33.     // filePath就是你下载文件的位置,你可以解压,也可以直接拿来使用 
  34.     NSString *imgFilePath = [filePath path];// 将NSURL转成NSString 
  35.     NSString *zipPath = imgFilePath; 
  36.     //删除  
  37. //            [[NSFileManager defaultManager] removeItemAtPath:H5FilePath error:nil]; 
  38.     [fileManager createDirectoryAtPath:H5FilePath withIntermediateDirectories:YES attributes:nil error:nil]; 
  39.     //解压 
  40.     [SSZipArchive unzipFileAtPath:zipPath toDestination:H5FilePath]; 
  41.     //清理缓存 
  42.     [DLCommenHelper clearWebCache]; 
  43. }]; 
  44. [downloadTask resume]; 

WKWebview 缓存池

美团有篇文章提到,在使用 iOS 10 的模拟器测试 WKWebView 的加载速度,首次初始化的时间耗时有 700 多毫秒。其实本人用 iOS 13 的真机,发现初始化的时间约在 200 毫秒左右甚至更短。虽然只占整个加载时间的特别小的一部分,但是本着能优则优的原则还是做了处理,也就是预加载 Webview。

  • 新建了一个单例类 SDIWKWebViewPool,默认缓存池里的数量是 10 个
  1. + (instancetype)sharedInstance { 
  2.     static dispatch_once_t onceToken; 
  3.     static SDIWKWebViewPool *instance = nil; 
  4.     dispatch_once(&onceToken,^{ 
  5.         instance = [[super allocWithZone:NULL] init]; 
  6.     }); 
  7.     return instance; 
  8.   
  9. + (id)allocWithZone:(struct _NSZone *)zone{ 
  10.     return [self sharedInstance]; 
  11.   
  12. - (instancetype)init 
  13.     self = [super init]; 
  14.     if (self) { 
  15.         self.initialViewsMaxCount = 10; 
  16.         self.preloadedViews = [NSMutableArray arrayWithCapacity:self.initialViewsMaxCount]; 
  17.     } 
  18.     return self; 
  • 在合适的地方提前调用 //预加载wkwebview [[SDIWKWebViewPool sharedInstance] prepareWithCount:10];,自行选择在 delegate 或主页面初始化的时候调用。
  1. /** 
  2.  预初始化若干WKWebView 
  3.   
  4.  @param count 个数 
  5.  */ 
  6. - (void)prepareWithCount:(NSUInteger)count { 
  7.     NSTimeInterval start = CACurrentMediaTime(); 
  8.     // Actually does nothing, only initialization must be called. 
  9.     while (self.preloadedViews.count < MIN(count,self.initialViewsMaxCount)) { 
  10.         id preloadedView = [self createPreloadedView]; 
  11.         if (preloadedView) { 
  12.             [self.preloadedViews addObject:preloadedView]; 
  13.         } else { 
  14.             break; 
  15.         } 
  16.     } 
  17.     NSTimeInterval delta = CACurrentMediaTime() - start; 
  18.     NSLog(@"=======初始化耗时:%f",  delta); 
  19.   
  20. /** 
  21.  从池中获取一个WKWebView 
  22.  @return WKWebView 
  23.  */ 
  24. - (WKWebView *)getWKWebViewFromPool { 
  25.     if (!self.preloadedViews.count) { 
  26.         NSLog(@"不够啦!"); 
  27.         return [self createPreloadedView]; 
  28.     } else { 
  29.         id preloadedView = self.preloadedViews.firstObject; 
  30.         [self.preloadedViews removeObject:preloadedView]; 
  31.         return preloadedView; 
  32.     } 
  • 创建 webview 的方法如下,需要注意的是 kWKWebViewReuseScheme,WKWebView 需要注册这个 scheme 才能实现拦截,这个是 WKWebview 拦截需要的准备工作。
  • SDICustomURLSchemeHandler 是我的自定义拦截类

关于这里的版本为什么设置成 iOS 12 以上,WKURLSchemeHandler 是苹果 iOS 11 就已推出,但是有发现某款机型在 iOS 11.2 上拦截失效,导致产生 Webview 白屏。所以这里一刀切,直接 12 以上才处理。其实 iOS 12 一下的用户量特别少,所以不需要太担心。

  1. //scheme定义 
  2. #define kWKWebViewReuseScheme    @"kwebview" 
  3.  
  4. /** 
  5.  创建一个WKWebView 
  6.  @return WKWebView 
  7.  */ 
  8. - (WKWebView *)createPreloadedView { 
  9.       WKUserContentController *userContentController = WKUserContentController.new; 
  10.      WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; 
  11.       NSString *cookieSource = [NSString stringWithFormat:@"document.cookie = 'API_SESSION=%@';",  [SAMKeychain usertoken]]; 
  12.       WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:cookieSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 
  13.       [userContentController addUserScript:cookieScript]; 
  14.      // 赋值userContentController 
  15.      configuration.userContentController = userContentController; 
  16.      configuration.preferences.javaScriptEnabled = YES; 
  17.      configuration.suppressesIncrementalRendering = YES; // 是否支持记忆读取 
  18.      [configuration.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];//支持跨域 
  19. //    WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init]; 
  20. //    WKUserContentController *wkUController = [[WKUserContentController alloc] init]; 
  21. //    wkWebConfig.userContentController = wkUController; 
  22.  
  23.     if (@available(iOS 12.0, *)) { 
  24.         [configuration setURLSchemeHandler:[[SDICustomURLSchemeHandler alloc] init] forURLScheme:kWKWebViewReuseScheme]; 
  25.     } else { 
  26.         // Fallback on earlier versions 
  27.     } 
  28.     WKWebView *wkWebView = [[WKWebView alloc]initWithFrame:CGRectZero configuration:configuration]; 
  29.     //根据自己的业务 
  30.     wkWebView.allowsBackForwardNavigationGestures = YES; 
  31.     return wkWebView; 

替换 url scheme

  1. if (@available(iOS 12.0, *)) { 
  2.     if([urlString hasPrefix:@"http"] && [urlString containsString:@"ui-h5"]){ 
  3.         urlString = [urlString stringByReplacingOccurrencesOfString:@"https" withString:kWKWebViewReuseScheme]; 
  4.     } 

这里是通过规则直接把 https 替换为 kWKWebViewReuseScheme,也就是替换 url scheme http(s) 为自定义协议,完成这一步后,拦截生效。

需要注意的有两点:

  • 前端这边加载 js 等资源都是用相对路径,前端的 ajax 请求,像 post 请求,scheme 使用 http(s) 不使用自定义协议,这样native 不会拦截,完全交给 H5 与服务器交互,就不会发生发送 post 请求,body 丢失的情况。
  • 在我的项目里,H5 对服务器的请求都是通过 native 端来转发的,所以也不存在拦截 post 请求,body 丢失的情况。所以上面这样的改动对 H5 端是无侵入式的,不需要修改业务代码。

最最重要的自定义 SDICustomURLSchemeHandler 类

  1. - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask 
  2. API_AVAILABLE(ios(12.0)){ 
  3.     dispatch_sync(self.serialQueue, ^{ 
  4.         [_taskVaildDic setValue:@(YES) forKey:urlSchemeTask.description]; 
  5.     }); 
  6.      
  7.     NSDictionary *headers = urlSchemeTask.request.allHTTPHeaderFields; 
  8.     NSString *accept = headers[@"Accept"]; 
  9.      
  10.     //当前的requestUrl的scheme都是customScheme 
  11.     NSString *requestUrl = urlSchemeTask.request.URL.absoluteString; 
  12.     NSString *fileName = [[requestUrl componentsSeparatedByString:@"?"].firstObject componentsSeparatedByString:@"ui-h5/"].lastObject; 
  13.     NSString *replacedStr = [requestUrl stringByReplacingOccurrencesOfString:kWKWebViewReuseScheme withString:@"https"]; 
  14.     self.replacedStr = replacedStr; 
  15.     //Intercept and load local resources. 
  16.     if ((accept.length >= @"text".length && [accept rangeOfString:@"text/html"].location != NSNotFound)) {//html 拦截 
  17.         [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask]; 
  18.     } else if ([self isMatchingRegularExpressionPattern:@"\\.(js|css)" text:requestUrl]) {//js、css 
  19.         [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask]; 
  20.     } else if (accept.length >= @"image".length && [accept rangeOfString:@"image"].location != NSNotFound) {//image 
  21.       NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:[NSURL URLWithString:replacedStr]]; 
  22.         [[SDWebImageManager sharedManager].imageCache queryImageForKey:key options:SDWebImageRetryFailed context:nil completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { 
  23.             if (image) { 
  24.                 NSData *imgData = UIImageJPEGRepresentation(image, 1); 
  25.                 NSString *mimeType = [self getMIMETypeWithCAPIAtFilePath:fileName] ?: @"image/jpeg"
  26.                 [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:mimeType requestData:imgData]; 
  27.             } else { 
  28.                 [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask]; 
  29.             } 
  30.         }]; 
  31.     } else { 
  32.         //return an empty json. 
  33.         NSData *data = [NSJSONSerialization dataWithJSONObject:@{ } options:NSJSONWritingPrettyPrinted error:nil]; 
  34.         [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:@"text/html" requestData:data]; 
  35.     } 
  36.  
  37. -(BOOL)isMatchingRegularExpressionPattern:(NSString *)pattern text:(NSString *)text{ 
  38.     NSError *error = NULL
  39.     NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error]; 
  40.     NSTextCheckingResult *result = [regex firstMatchInString:text options:0 range:NSMakeRange(0, [text length])]; 
  41.     return MHObjectIsNil(result)?NO:YES; 
  • 上面的代码是拦截资源请求后的处理代码。收到拦截请求后,先获取本地资源包对应的资源,转换成 data 回传给 webView 进行渲染处理;若本地没有,则 customScheme 替换成 https 的 url 重发请求通知 webview,这就是基本流程。
  • 以下就是加载本地资源和重发请求的代码
  1.   //Load local resources, eg: html、js、css... 
  2. - (void)loadLocalFile:(NSString *)fileName urlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0)){ 
  3.     if(![self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask || fileName.length == 0){ 
  4.                    return
  5.     } 
  6.      
  7.     NSString * docsdir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 
  8.     NSString * H5FilePath = [[docsdir stringByAppendingPathComponent:@"H5"] stringByAppendingPathComponent:@"h5"]; 
  9.     //If the resource do not exist, re-send request by replacing to http(s). 
  10.     NSString *filePath = [H5FilePath stringByAppendingPathComponent:fileName]; 
  11.      
  12.     if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 
  13.         NSLog(@"开始重新发送网络请求"); 
  14.         if ([self.replacedStr hasPrefix:kWKWebViewReuseScheme]) { 
  15.  
  16.             self.replacedStr =[self.replacedStr stringByReplacingOccurrencesOfString:kWKWebViewReuseScheme withString:@"https"]; 
  17.                      
  18.             NSLog(@"请求地址:%@",self.replacedStr); 
  19.              
  20.         } 
  21.      
  22.         self.replacedStr = [NSString stringWithFormat:@"%@?%@",self.replacedStr,[SAMKeychain h5Version]?:@""]; 
  23.         start = CACurrentMediaTime();//开始加载时间 
  24.         NSLog(@"web请求开始地址:%@",self.replacedStr); 
  25.          
  26.         @weakify(self) 
  27.         NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.replacedStr]]; 
  28.         NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; 
  29.         NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { 
  30.             @strongify(self) 
  31.             if([self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] == NO || !urlSchemeTask){ 
  32.                 return
  33.             } 
  34.             [urlSchemeTask didReceiveResponse:response]; 
  35.             [urlSchemeTask didReceiveData:data]; 
  36.             if (error) { 
  37.                 [urlSchemeTask didFailWithError:error]; 
  38.             } else { 
  39.                 NSTimeInterval delta = CACurrentMediaTime() - self->start; 
  40.                 NSLog(@"=======web请求结束地址%@:::%f", self.replacedStr, delta); 
  41.                 [urlSchemeTask didFinish]; 
  42.             } 
  43.         }]; 
  44.         [dataTask resume]; 
  45.         [session finishTasksAndInvalidate]; 
  46.     } else { 
  47.         NSLog(@"filePath:%@",filePath); 
  48.         if(![self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask || fileName.length == 0){ 
  49.             NSLog(@"return"); 
  50.             return
  51.         } 
  52.          
  53.         NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:nil]; 
  54.         [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:[self getMIMETypeWithCAPIAtFilePath:filePath] requestData:data]; 
  55.     } 
  56.  
  57. - (void)resendRequestWithUrlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask 
  58.                               mimeType:(NSString *)mimeType 
  59.                            requestData:(NSData *)requestData  API_AVAILABLE(ios(11.0)) { 
  60.     if(![self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask|| !urlSchemeTask.request || !urlSchemeTask.request.URL){ 
  61.         return
  62.     } 
  63.  
  64.     NSString *mimeType_local = mimeType ? mimeType : @"text/html"
  65.     NSData *data = requestData ? requestData : [NSData data]; 
  66.     NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL 
  67.                                                         MIMEType:mimeType_local 
  68.                                            expectedContentLength:data.length 
  69.                                                 textEncodingName:nil]; 
  70.     [urlSchemeTask didReceiveResponse:response]; 
  71.     [urlSchemeTask didReceiveData:data]; 
  72.     [urlSchemeTask didFinish]; 

整个过程中遇到的一些踩坑点

1. 'The task has already been stopped'崩溃问题

  • _taskVaildDic 是一个 NSMutableDictionary,它里面存的是以当前的 urlSchemeTask做 key,拦截开始时设置 YES,收到停止通知时设置 NO。这是由于在快速切换 webview 时,之前的 urlSchemeTask 已经停止但是后面再次调用了它的方法就会产生该崩溃。
  • 在实际使用过程中,用 bugly 监控到还是会有该崩溃发生,只不过次数特别少,一天约四五条左右。还在寻找问题的原因中。
  1. - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask 
  2. API_AVAILABLE(ios(12.0)){ 
  3.     dispatch_sync(self.serialQueue, ^{ 
  4.         [_taskVaildDic setValue:@(YES) forKey:urlSchemeTask.description]; 
  5.     }); 
  6.  
  7. - (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask  API_AVAILABLE(ios(12.0)){ 
  8.     NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL]; 
  9.     NSLog(@"weberror:%@",error); 
  10.     dispatch_sync(self.serialQueue, ^{ 
  11.          [self->_taskVaildDic setValue:@(NO) forKey:urlSchemeTask.description]; 
  12.     }); 

2. WKWebview 的默认缓存策略问题

之前未考虑到 WKWebview 的默认缓存策略(WKWebView 默认缓存策略完全遵循 HTTP 缓存协议)。

在 h5 打包上线并更新离线包后,H5 的资源文件修改是变更 md5 文件名的。由于缓存策略默认时间是一个小时,会导致缓存的 url 加载不到修改后的 js,css 等文件(无论是本地离线包和远端服务器都已经没有这个 md5 文件)。

简单的解决方案是通过资源链接加版本号后缀,每次更新资源的时候变更版本号,在上面的代码中有做这部分处理。既保证了实时的更新,又保证了加载速度。

3. uni-app 图片 CDN 问题

做完上述的离线包优化后,发现新下载 APP 的情况,会偶发加载很慢问题。iOS 出现,但是 android 并未出现。

H5 部分是用 uni-app 开发的,所以发现这个问题后由前端同事修复后恢复正常。

4. chunk-vendors.js 文件过大

这个问题也是抓包发现的,在未打开离线包缓存开关时,发现h5的加载速度过慢,发现加载的 chunk-vendors.js 文件过大约 1.7M。 stopURLSchemeTask 方法里会报 error 错误信息 Error Domain= 的错误信息。也由前端同事处理了这个问题。

最终效果

统计了 APP 在不开离线包方案时,webview 平均加载时长在 1.5-2 秒的范围内(这里是计算的 webview开始加载到导航完成的时间),在上述优化完成后,打开的时长在 0.25-0.3 秒之间。

所以效果还是很显著的,用户的直观感受就是接近于秒开的体验。

总结

上面的优化过程中踩了很多坑,但是也重新梳理了 Webview 的加载过程,默认缓存策略机制等内容。上面的方案肯定不是最优的,只是一个快速达到 WKWebview 接近秒开效果的一个方案。

有什么更好的解决方案或者上述文中有不对的地方,希望大家指出,欢迎共同讨论~

原文链接:https://mp.weixin.qq.com/s/NHlwR5zPqO8aEpeRmKJ_Xg

延伸 · 阅读

精彩推荐