布景

原生架构+H5页面的组合是很常见的项目开发形式了,H5的优势是跨渠道、开发快、迭代快、热更新,很多大厂的App大部分事务代码都是H5来完成的,众所周知H5页面的体会是比原生差的,特别是网络环境差的时候,假如首屏页面是H5的话,那酸爽遇见过的都懂

白屏警告

iOS H5页面秒加载预研

iOS H5页面秒加载预研

白屏首要便是下载页面资源失败或许慢导致的,下面能够看下H5加载进程

H5加载进程

这部分内容其实网上已经很多了,如下

初始化 webview -> 恳求页面 -> 下载数据 -> 解析HTML -> 恳求 js/css 资源 -> dom 烘托 -> 解析 JS 履行 -> JS 恳求数据 -> 解析烘托 -> 下载烘托图片

首要要优化的便是下载到烘托这段时刻,最简略的计划便是把整个web包放在App本地,经过本地途径去加载,做到这一步,再配合上H5页面做加载中占位图,基本上就不会有白屏的状况了,而且速度也有了很显着的提升

可参阅以下测试数据(网络地址加载和本地途径加载)

WiFi网络打开H5页面100次耗时:(代表网络优异时)

iOS H5页面秒加载预研

本地加载均匀履行耗时:0.28秒

网络加载均匀履行耗时:0.58秒

4G/5G手机网络打开H5页面100次耗时:(代表网络杰出时)

iOS H5页面秒加载预研

本地加载均匀履行耗时:0.43秒

网络加载均匀履行耗时:2.09秒

3G手机网络打开H5页面100次耗时:(代表网络一般或许较差时)

iOS H5页面秒加载预研

本地加载均匀履行耗时:1.48秒

网络加载均匀履行耗时:19.09秒

ok,恭喜你,H5离线秒加载功用优化完毕,这么简略?

iOS H5页面秒加载预研

进入正题

经过上述完成本地加载后速度虽然是快了不少,但H5页面的数据显示仍是需求走接口恳求的,假如网络环境欠好的话,也是会很慢的,这个问题怎么处理呢?

下面开始上绝活,这儿引入一个概念 WKURLSchemeHandler 在iOS11及以上系统中,能够经过WKURLSchemeHandler自定义阻拦恳求,有什么用?简略说便是能够利用原生的数据缓存去加载H5页面,能够无视网络环境的影响,在阻拦到H5页面的网络恳求后先判断本地是否有缓存,有缓存的话能够直接拼接一个成功的返回,没有的话直接铺开持续走网络恳求,成功后再缓存数据,对H5来说也是无侵入无感知的。

首先自定义一个SchemeHandler类,遵守WKURLSchemeHandler协议,完成协议办法

@protocol WKURLSchemeHandler <NSObject>
/*! @abstract Notifies your app to start loading the data for a particular resource 
 represented by the URL scheme handler task.
 @param webView The web view invoking the method.
 @param urlSchemeTask The task that your app should start loading data for.
 */
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;
/*! @abstract Notifies your app to stop handling a URL scheme handler task.
 @param webView The web view invoking the method.
 @param urlSchemeTask The task that your app should stop handling.
 @discussion After your app is told to stop loading data for a URL scheme handler task
 it must not perform any callbacks for that task.
 An exception will be thrown if any callbacks are made on the URL scheme handler task
 after your app has been told to stop loading for it.
 */
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;
@end

直接上代码

#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface YXWKURLSchemeHandler : NSObject<WKURLSchemeHandler>
@end
NS_ASSUME_NONNULL_END
#import "CMWKURLSchemeHandler.h"
#import "YXNetworkManager.h"
#import <SDWebImage/SDWebImageManager.h>
#import <SDWebImage/SDImageCache.h>
@implementation YXWKURLSchemeHandler
- (void) webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    NSURLRequest * request = urlSchemeTask.request;
    NSString * urlStr = request.URL.absoluteString;
    NSString * method = urlSchemeTask.request.HTTPMethod;
    NSData * bodyData = urlSchemeTask.request.HTTPBody;
    NSDictionary * bodyDict = nil;
    if (bodyData) {
        bodyDict = [NSJSONSerialization JSONObjectWithData:bodyData options:kNilOptions error:nil];
    }
    NSLog(@"阻拦urlStr=%@", urlStr);
    NSLog(@"阻拦method=%@", method);
    NSLog(@"阻拦bodyData=%@", bodyData);
    // 图片加载
    if ([urlStr hasSuffix:@".jpg"] || [urlStr hasSuffix:@".png"] || [urlStr hasSuffix:@".gif"]) {
        SDImageCache * imageCache = [SDImageCache sharedImageCache];
        NSString * cacheKey = [[SDWebImageManager sharedManager] cacheKeyForURL:request.URL];
        BOOL isExist = [imageCache diskImageDataExistsWithKey:cacheKey];
        if (isExist) {
            NSData * imgData = [[SDImageCache sharedImageCache] diskImageDataForKey:cacheKey];
            [urlSchemeTask didReceiveResponse:[[NSURLResponse alloc] initWithURL:request.URL MIMEType:[self createMIMETypeForExtension:[urlStr pathExtension]] expectedContentLength:-1 textEncodingName:nil]];
            [urlSchemeTask didReceiveData:imgData];
            [urlSchemeTask didFinish];
            return;
        }
        [[SDWebImageManager sharedManager] loadImageWithURL:request.URL options:SDWebImageRetryFailed progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {}];
    }
    // 网络恳求
    NSData * cachedData = (NSData *)[YXNetworkCache httpCacheForURL:urlStr parameters:bodyDict];
    if (cachedData) {
        NSHTTPURLResponse * response = [self createHTTPURLResponseForRequest:urlSchemeTask.request];
        [urlSchemeTask didReceiveResponse:response];
        [urlSchemeTask didReceiveData:cachedData];
        [urlSchemeTask didFinish];
    } else {
        NSURLSessionDataTask * dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                               if (error) {
                                                   [urlSchemeTask didFailWithError:error];
                                               } else {
                                                   [YXNetworkCache setHttpCache:data URL:urlStr parameters:bodyDict];
                                                   [urlSchemeTask didReceiveResponse:response];
                                                   [urlSchemeTask didReceiveData:data];
                                                   [urlSchemeTask didFinish];
                                               }
                                           }];
        [dataTask resume];
    }
} /* webView */
- (NSHTTPURLResponse *) createHTTPURLResponseForRequest:(NSURLRequest *)request {
    // Determine the content type based on the request
    NSString * contentType;
    if ([request.URL.pathExtension isEqualToString:@"css"]) {
        contentType = @"text/css";
    } else if ([[request valueForHTTPHeaderField:@"Accept"] isEqualToString:@"application/javascript"]) {
        contentType = @"application/javascript;charset=UTF-8";
    } else {
        contentType = @"text/html;charset=UTF-8"; // default content type
    }
    // Create the HTTP URL response with the dynamic content type
    NSHTTPURLResponse * response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type": contentType }];
    return response;
}
- (NSString *) createMIMETypeForExtension:(NSString *)extension {
    if (!extension || extension.length == 0) {
        return @"";
    }
    NSDictionary * MIMEDict = @{
            @"txt"  : @"text/plain",
            @"html" : @"text/html",
            @"htm"  : @"text/html",
            @"css"  : @"text/css",
            @"js"   : @"application/javascript",
            @"json" : @"application/json",
            @"xml"  : @"application/xml",
            @"swf"  : @"application/x-shockwave-flash",
            @"flv"  : @"video/x-flv",
            @"png"  : @"image/png",
            @"jpg"  : @"image/jpeg",
            @"jpeg" : @"image/jpeg",
            @"gif"  : @"image/gif",
            @"bmp"  : @"image/bmp",
            @"ico"  : @"image/vnd.microsoft.icon",
            @"woff" : @"application/x-font-woff",
            @"woff2": @"application/x-font-woff",
            @"ttf"  : @"application/x-font-ttf",
            @"otf"  : @"application/x-font-opentype"
    };
    NSString * MIMEType = MIMEDict[extension.lowercaseString];
    if (!MIMEType) {
        return @"";
    }
    return MIMEType;
} /* MIMETypeForExtension */
- (void) webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    NSLog(@"stop = %@", urlSchemeTask);
}
@end

重点来了,需求hook WKWebview 的 handlesURLScheme 办法来支撑 http 和 https 恳求的署理

直接上代码

#import "WKWebView+SchemeHandle.h"
#import <objc/runtime.h>
@implementation WKWebView (SchemeHandle)
+ (void) load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod1 = class_getClassMethod(self, @selector(handlesURLScheme:));
        Method swizzledMethod1 = class_getClassMethod(self, @selector(cmHandlesURLScheme:));
        method_exchangeImplementations(originalMethod1, swizzledMethod1);
    });
}
+ (BOOL) cmHandlesURLScheme:(NSString *)urlScheme {
    if ([urlScheme isEqualToString:@"http"] || [urlScheme isEqualToString:@"https"]) {
        return NO;
    } else {
        return [self handlesURLScheme:urlScheme];
    }
}
@end

这儿会有一个疑问了?为什么要这么用呢,这儿首要是为了满足对H5端无侵入、无感知的要求 假如不hook http和https的话,就需求在H5端修改代码了,把scheme修改成自定义的customScheme,全部都要改,而且对安卓还不适用,所以别这么搞,信我!!!

老老实实hook,一步到位

怎么运用

首先是WKWebView的初始化,直接上代码

- (WKWebView *) wkWebView {
    if (!_wkWebView) {
        WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
        // 允许跨域访问
        [config setValue:@(true) forKey:@"allowUniversalAccessFromFileURLs"];
        // 自定义HTTPS恳求阻拦
        YXWKURLSchemeHandler * handler = [YXWKURLSchemeHandler new];
        [config setURLSchemeHandler:handler forURLScheme:@"https"];
        _wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, kScreenHeight) configuration:config];
        _wkWebView.navigationDelegate = self;
    }
    return _wkWebView;
} /* wkWebView */
NSString * htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"H5"];
NSURL * fileURL = [NSURL fileURLWithPath:htmlPath];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
[self.wkWebView loadRequest:request];

ok,到了这一步基本上就能看到作用了,H5页面的接口恳求和图片加载都会阻拦到,直接运用原生的缓存数据,忽略网络环境的影响完成秒加载了

可参阅以下测试数据(自定义WKURLSchemeHandler阻拦和不阻拦)

3G手机网络打开H5页面100次耗时:(代表网络一般或许较差时)

iOS H5页面秒加载预研

阻拦后加载均匀履行耗时:0.24秒

不阻拦加载均匀履行耗时:1.09秒

能够发现经过自定义WKURLSchemeHandler阻拦后,加载速度非常平稳底子不受网络的影响,不阻拦的话虽然全体加载速度并不算太慢,可是随网络波动比较显着。

不足

这套计划也仍是有一些缺点的,比如

  1. App打包的时候需求预嵌入web模块的数据,会导致App包的巨细添加(需求尽量缩小web模块的包巨细,特别是资源文件的优化)
  2. App需求规划一套web模块包的更新机制,同时需求规划一个web包上传发布渠道,后续版本管理更新比之前直接上传服务器替换相对麻烦一些
  3. 需求更新时下载全量web模块包会略大,也能够考虑用BSDiff差分算法来做增量更新处理,可是会添加程序复杂度

总结

综上便是本次H5页面秒加载全部的预研进程了,总的来说,基本上能够满足秒加载的需求。没有完美的处理计划,只有适宜的计划,有舍有得,依据公司项目状况来。