本文转载自微信公众号「网罗开发」,作者伤心的Easyman 。转载本文请联系网罗开发公众号。

关于加载速度慢有很多文章都已经详细解释了,h5在加载工作中做了很多事
初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片
一般页面在 dom 渲染后才能展示,可以发现,H5 首屏渲染白屏问题的原因关键在于,如何优化减少从请求下载页面到渲染之间这段时间的耗时。
这其中可做的优化特别多,前后端能够做的是:
一般情况下,只要对照这个列表,对比差异就基本能搞定绝大部分前端性能问题了。不过我们在里面仔细再分析下,对首屏启动速度影响最大的就是网络请求,包括请求 HTML、css、image 等静态资源和展示数据的请求。所以客户端内,优化最关键的其实就是如何缓存这些网络资源,也就是离线包缓存方案。
基于 iOS 的 local web server,目前大致有以下几种较为完善的框架:
当时采用的是 GCDWebServer,在打开 APP 后直接启动Webserver,H5 的链接直接替换成本地 localhost + 端口号链接的地址。
本来的方案是本地服务器和远端h5服务器同步下载资源,下载后客户端请求本地服务器的路径,如未找到相应的资源再请求远端服务器的资源文件。
测试过程中碰到很多奇怪的问题(暂不一一举例),也有提到以下问题并且时间紧急所以并未做进一步的深入:
关于离线包
前端项目的静态资源直接打包成 zip 包,APP 在启动时开始下载该包并解压到本地。WKWebview 通过 WKURLSchemeHandler 拦截并加载本地资源文件。关于离线包的分发,就是普通的 zip 离线包和一个版本控制的 json 文件,每次打离线包会修改 json 文件里的版本号,并附有离线包下载地址。此处可以优化的更好,但暂时并不需要太复杂。
离线包的下载和解压
只是简单的下载并解压到本地资源路径,关于版本比对的代码这里没有展示出来,自行注意,避免每次都全量更新。
- /* 创建网络下载对象 */
 - AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
 - /* 下载地址 */
 - NSURL *url = [NSURL URLWithString:request.urlParameters.path];
 - NSURLRequest *request = [NSURLRequest requestWithURL:url];
 - /* 下载路径 */
 - //获取Document文件
 - NSString * docsdir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
 - NSString * zipFilePath = [docsdir stringByAppendingPathComponent:@"zip"];//将需要创建的串拼接到后面
 - NSString * H5FilePath = [docsdir stringByAppendingPathComponent:@"H5"];
 - NSFileManager *fileManager = [NSFileManager defaultManager];
 - BOOL zipIsDir = NO;
 - BOOL H5IsDir = NO;
 - // fileExistsAtPath 判断一个文件或目录是否有效,isDirectory判断是否一个目录
 - BOOL zipexisted = [fileManager fileExistsAtPath:zipFilePath isDirectory:&zipIsDir];
 - BOOL H5Existed = [fileManager fileExistsAtPath:H5FilePath isDirectory:&H5IsDir];
 - if ( !(zipIsDir == YES && zipexisted == YES) ) {//如果文件夹不存在
 - [fileManager createDirectoryAtPath:zipFilePath withIntermediateDirectories:YES attributes:nil error:nil];
 - }
 - if (!(H5IsDir == YES && H5Existed == YES) ) {
 - [fileManager createDirectoryAtPath:H5FilePath withIntermediateDirectories:YES attributes:nil error:nil];
 - }
 - //删除
 - // [[NSFileManager defaultManager] removeItemAtPath:zipFilePath error:nil];
 - NSString *filePath = [zipFilePath stringByAppendingPathComponent:url.lastPathComponent];
 - /* 开始请求下载 */
 - NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
 - NSLog(@"下载进度:%.0f%", downloadProgress.fractionCompleted * 100);
 - } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
 - /* 设定下载到的位置 */
 - return [NSURL fileURLWithPath:filePath];
 - } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
 - NSTimeInterval delta = CACurrentMediaTime() - self->start;
 - NSLog(@"下载完成,耗时:%f",delta);
 - // filePath就是你下载文件的位置,你可以解压,也可以直接拿来使用
 - NSString *imgFilePath = [filePath path];// 将NSURL转成NSString
 - NSString *zipPath = imgFilePath;
 - //删除
 - // [[NSFileManager defaultManager] removeItemAtPath:H5FilePath error:nil];
 - [fileManager createDirectoryAtPath:H5FilePath withIntermediateDirectories:YES attributes:nil error:nil];
 - //解压
 - [SSZipArchive unzipFileAtPath:zipPath toDestination:H5FilePath];
 - //清理缓存
 - [DLCommenHelper clearWebCache];
 - }];
 - [downloadTask resume];
 
美团有篇文章提到,在使用 iOS 10 的模拟器测试 WKWebView 的加载速度,首次初始化的时间耗时有 700 多毫秒。其实本人用 iOS 13 的真机,发现初始化的时间约在 200 毫秒左右甚至更短。虽然只占整个加载时间的特别小的一部分,但是本着能优则优的原则还是做了处理,也就是预加载 Webview。
- + (instancetype)sharedInstance {
 - static dispatch_once_t onceToken;
 - static SDIWKWebViewPool *instance = nil;
 - dispatch_once(&onceToken,^{
 - instance = [[super allocWithZone:NULL] init];
 - });
 - return instance;
 - }
 - + (id)allocWithZone:(struct _NSZone *)zone{
 - return [self sharedInstance];
 - }
 - - (instancetype)init
 - {
 - self = [super init];
 - if (self) {
 - self.initialViewsMaxCount = 10;
 - self.preloadedViews = [NSMutableArray arrayWithCapacity:self.initialViewsMaxCount];
 - }
 - return self;
 - }
 
- /**
 - 预初始化若干WKWebView
 - @param count 个数
 - */
 - - (void)prepareWithCount:(NSUInteger)count {
 - NSTimeInterval start = CACurrentMediaTime();
 - // Actually does nothing, only initialization must be called.
 - while (self.preloadedViews.count < MIN(count,self.initialViewsMaxCount)) {
 - id preloadedView = [self createPreloadedView];
 - if (preloadedView) {
 - [self.preloadedViews addObject:preloadedView];
 - } else {
 - break;
 - }
 - }
 - NSTimeInterval delta = CACurrentMediaTime() - start;
 - NSLog(@"=======初始化耗时:%f", delta);
 - }
 - /**
 - 从池中获取一个WKWebView
 - @return WKWebView
 - */
 - - (WKWebView *)getWKWebViewFromPool {
 - if (!self.preloadedViews.count) {
 - NSLog(@"不够啦!");
 - return [self createPreloadedView];
 - } else {
 - id preloadedView = self.preloadedViews.firstObject;
 - [self.preloadedViews removeObject:preloadedView];
 - return preloadedView;
 - }
 - }
 
关于这里的版本为什么设置成 iOS 12 以上,WKURLSchemeHandler 是苹果 iOS 11 就已推出,但是有发现某款机型在 iOS 11.2 上拦截失效,导致产生 Webview 白屏。所以这里一刀切,直接 12 以上才处理。其实 iOS 12 一下的用户量特别少,所以不需要太担心。
- //scheme定义
 - #define kWKWebViewReuseScheme @"kwebview"
 - /**
 - 创建一个WKWebView
 - @return WKWebView
 - */
 - - (WKWebView *)createPreloadedView {
 - WKUserContentController *userContentController = WKUserContentController.new;
 - WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
 - NSString *cookieSource = [NSString stringWithFormat:@"document.cookie = 'API_SESSION=%@';", [SAMKeychain usertoken]];
 - WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:cookieSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
 - [userContentController addUserScript:cookieScript];
 - // 赋值userContentController
 - configuration.userContentController = userContentController;
 - configuration.preferences.javaScriptEnabled = YES;
 - configuration.suppressesIncrementalRendering = YES; // 是否支持记忆读取
 - [configuration.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];//支持跨域
 - // WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
 - // WKUserContentController *wkUController = [[WKUserContentController alloc] init];
 - // wkWebConfig.userContentController = wkUController;
 - if (@available(iOS 12.0, *)) {
 - [configuration setURLSchemeHandler:[[SDICustomURLSchemeHandler alloc] init] forURLScheme:kWKWebViewReuseScheme];
 - } else {
 - // Fallback on earlier versions
 - }
 - WKWebView *wkWebView = [[WKWebView alloc]initWithFrame:CGRectZero configuration:configuration];
 - //根据自己的业务
 - wkWebView.allowsBackForwardNavigationGestures = YES;
 - return wkWebView;
 - }
 
- if (@available(iOS 12.0, *)) {
 - if([urlString hasPrefix:@"http"] && [urlString containsString:@"ui-h5"]){
 - urlString = [urlString stringByReplacingOccurrencesOfString:@"https" withString:kWKWebViewReuseScheme];
 - }
 - }
 
这里是通过规则直接把 https 替换为 kWKWebViewReuseScheme,也就是替换 url scheme http(s) 为自定义协议,完成这一步后,拦截生效。
需要注意的有两点:
最最重要的自定义 SDICustomURLSchemeHandler 类
- - (void)webView:(WKWebView *)webView startURLSchemeTask:(id
 )urlSchemeTask - API_AVAILABLE(ios(12.0)){
 - dispatch_sync(self.serialQueue, ^{
 - [_taskVaildDic setValue:@(YES) forKey:urlSchemeTask.description];
 - });
 - NSDictionary *headers = urlSchemeTask.request.allHTTPHeaderFields;
 - NSString *accept = headers[@"Accept"];
 - //当前的requestUrl的scheme都是customScheme
 - NSString *requestUrl = urlSchemeTask.request.URL.absoluteString;
 - NSString *fileName = [[requestUrl componentsSeparatedByString:@"?"].firstObject componentsSeparatedByString:@"ui-h5/"].lastObject;
 - NSString *replacedStr = [requestUrl stringByReplacingOccurrencesOfString:kWKWebViewReuseScheme withString:@"https"];
 - self.replacedStr = replacedStr;
 - //Intercept and load local resources.
 - if ((accept.length >= @"text".length && [accept rangeOfString:@"text/html"].location != NSNotFound)) {//html 拦截
 - [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
 - } else if ([self isMatchingRegularExpressionPattern:@"\\.(js|css)" text:requestUrl]) {//js、css
 - [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
 - } else if (accept.length >= @"image".length && [accept rangeOfString:@"image"].location != NSNotFound) {//image
 - NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:[NSURL URLWithString:replacedStr]];
 - [[SDWebImageManager sharedManager].imageCache queryImageForKey:key options:SDWebImageRetryFailed context:nil completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
 - if (image) {
 - NSData *imgData = UIImageJPEGRepresentation(image, 1);
 - NSString *mimeType = [self getMIMETypeWithCAPIAtFilePath:fileName] ?: @"image/jpeg";
 - [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:mimeType requestData:imgData];
 - } else {
 - [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
 - }
 - }];
 - } else {
 - //return an empty json.
 - NSData *data = [NSJSONSerialization dataWithJSONObject:@{ } options:NSJSONWritingPrettyPrinted error:nil];
 - [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:@"text/html" requestData:data];
 - }
 - }
 - -(BOOL)isMatchingRegularExpressionPattern:(NSString *)pattern text:(NSString *)text{
 - NSError *error = NULL;
 - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
 - NSTextCheckingResult *result = [regex firstMatchInString:text options:0 range:NSMakeRange(0, [text length])];
 - return MHObjectIsNil(result)?NO:YES;
 - }
 
- //Load local resources, eg: html、js、css...
 - - (void)loadLocalFile:(NSString *)fileName urlSchemeTask:(id
 )urlSchemeTask API_AVAILABLE(ios(11.0)){ - if(![self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask || fileName.length == 0){
 - return;
 - }
 - NSString * docsdir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
 - NSString * H5FilePath = [[docsdir stringByAppendingPathComponent:@"H5"] stringByAppendingPathComponent:@"h5"];
 - //If the resource do not exist, re-send request by replacing to http(s).
 - NSString *filePath = [H5FilePath stringByAppendingPathComponent:fileName];
 - if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
 - NSLog(@"开始重新发送网络请求");
 - if ([self.replacedStr hasPrefix:kWKWebViewReuseScheme]) {
 - self.replacedStr =[self.replacedStr stringByReplacingOccurrencesOfString:kWKWebViewReuseScheme withString:@"https"];
 - NSLog(@"请求地址:%@",self.replacedStr);
 - }
 - self.replacedStr = [NSString stringWithFormat:@"%@?%@",self.replacedStr,[SAMKeychain h5Version]?:@""];
 - start = CACurrentMediaTime();//开始加载时间
 - NSLog(@"web请求开始地址:%@",self.replacedStr);
 - @weakify(self)
 - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.replacedStr]];
 - NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
 - NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
 - @strongify(self)
 - if([self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] == NO || !urlSchemeTask){
 - return;
 - }
 - [urlSchemeTask didReceiveResponse:response];
 - [urlSchemeTask didReceiveData:data];
 - if (error) {
 - [urlSchemeTask didFailWithError:error];
 - } else {
 - NSTimeInterval delta = CACurrentMediaTime() - self->start;
 - NSLog(@"=======web请求结束地址%@:::%f", self.replacedStr, delta);
 - [urlSchemeTask didFinish];
 - }
 - }];
 - [dataTask resume];
 - [session finishTasksAndInvalidate];
 - } else {
 - NSLog(@"filePath:%@",filePath);
 - if(![self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask || fileName.length == 0){
 - NSLog(@"return");
 - return;
 - }
 - NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:nil];
 - [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:[self getMIMETypeWithCAPIAtFilePath:filePath] requestData:data];
 - }
 - }
 - - (void)resendRequestWithUrlSchemeTask:(id
 )urlSchemeTask - mimeType:(NSString *)mimeType
 - requestData:(NSData *)requestData API_AVAILABLE(ios(11.0)) {
 - if(![self->_taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask|| !urlSchemeTask.request || !urlSchemeTask.request.URL){
 - return;
 - }
 - NSString *mimeType_local = mimeType ? mimeType : @"text/html";
 - NSData *data = requestData ? requestData : [NSData data];
 - NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL
 - MIMEType:mimeType_local
 - expectedContentLength:data.length
 - textEncodingName:nil];
 - [urlSchemeTask didReceiveResponse:response];
 - [urlSchemeTask didReceiveData:data];
 - [urlSchemeTask didFinish];
 - }
 
1. 'The task has already been stopped'崩溃问题
- - (void)webView:(WKWebView *)webView startURLSchemeTask:(id
 )urlSchemeTask - API_AVAILABLE(ios(12.0)){
 - dispatch_sync(self.serialQueue, ^{
 - [_taskVaildDic setValue:@(YES) forKey:urlSchemeTask.description];
 - });
 - }
 - - (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id
 )urlSchemeTask API_AVAILABLE(ios(12.0)){ - NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL];
 - NSLog(@"weberror:%@",error);
 - dispatch_sync(self.serialQueue, ^{
 - [self->_taskVaildDic setValue:@(NO) forKey:urlSchemeTask.description];
 - });
 - }
 
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 接近秒开效果的一个方案。
有什么更好的解决方案或者上述文中有不对的地方,希望大家指出,欢迎共同讨论~
                网站标题:WKWebview秒开的实践及踩坑之路
                
                文章出自:http://www.csdahua.cn/qtweb/news26/417926.html
            
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网