iOS H5 ページの数秒での読み込みに関する事前調査

バックグラウンド

ネイティブ アーキテクチャ + H5 ページの組み合わせは、非常に一般的なプロジェクト開発モデルです。H5 の利点は、クロスプラットフォーム、高速開発、高速イテレーション、ホット アップデートです。多くの主要メーカーのアプリのビジネス コードのほとんどは、H5 によって実装されています。 H5 は元のページよりも操作性が悪く、特にネットワーク環境が悪い場合は、最初の画面が H5 だと渋くてカッコいいのは誰でも知っています。

白い画面の警告

白い画面は主にページ リソースのダウンロードの失敗または遅さが原因で発生します。以下に H5 の読み込みプロセスを示します。

H5ローディングプロセス

この部分の内容は実際にインターネット上にたくさんあります。

WebViewの初期化 -> ページのリクエスト -> データのダウンロード -> HTMLの解析 -> js/cssリソースのリクエスト -> domレンダリング -> JSの実行の解析 -> JSリクエストデータ -> レンダリングの解析 -> レンダリングされた画像のダウンロード

最適化する主な点は、ダウンロードからレンダリングまでの期間です。最も簡単な解決策は、Web パッケージ全体をアプリにローカルに配置し、ローカル パス経由でロードすることです。このステップの後、H5 ページと連携して、読み込み中のプレースホルダー画像、基本的にインターネット上の白い画面がなくなり、速度も大幅に向上しました

以下のテストデータ(ネットワークアドレスロードとローカルパスロード)を参照できます。

WiFi ネットワーク上で H5 ページを 100 回開くのに時間がかかります: (ネットワークが良好な場合を表します)

ローカルロードの平均実行時間: 0.28 秒

ネットワーク読み込みの平均実行時間: 0.58 秒

4G/5G モバイル ネットワークで H5 ページを 100 回開くと時間がかかる:(ネットワークが良好な場合を表す)

ローカルロードの平均実行時間: 0.43 秒

ネットワーク読み込みの平均実行時間: 2.09 秒

3G 携帯電話ネットワークで H5 ページを 100 回開くには時間がかかります: (ネットワークが平均的または貧弱な場合を表します)

ローカルロードの平均実行時間: 1.48 秒

ネットワーク読み込みの平均実行時間: 19.09 秒

OK、おめでとうございます。H5 のオフライン 2 番目の読み込み機能が最適化されました。とてもシンプルですね?

IMG_4941.JPG

タイトルに入る

上記の実装によりローカルロードの速度は大幅に速くなりましたが、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 "YXWKURLSchemeHandler.h"
#import "YXNetworkManager.h"
#import <SDWebImage/SDWebImageManager.h>
#import <SDWebImage/SDImageCache.h>

@interface YXWKURLSchemeHandler ()

@property (nonatomic, strong) NSMutableSet * schemeTaskSet;

@end

@implementation YXWKURLSchemeHandler

- (void) webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    // 防止对已经stoped的task发送消息 会报错:"This task has already been stopped"
    [self.schemeTaskSet addObject: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];
            if ([self.schemeTaskSet containsObject:urlSchemeTask]) {
                [urlSchemeTask didReceiveResponse:[[NSURLResponse alloc] initWithURL:request.URL MIMEType:[self createMIMETypeForExtension:[urlStr pathExtension]] expectedContentLength:-1 textEncodingName:nil]];
                [urlSchemeTask didReceiveData:imgData];
                [urlSchemeTask didFinish];
                [self.schemeTaskSet removeObject:urlSchemeTask];
                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];
        if ([self.schemeTaskSet containsObject:urlSchemeTask]) {
            [urlSchemeTask didReceiveResponse:response];
            [urlSchemeTask didReceiveData:cachedData];
            [urlSchemeTask didFinish];
            [self.schemeTaskSet removeObject:urlSchemeTask];
            return;
        }
    }
    
    // 无缓存时发起网络请求(实际应用时需根据实际情况判断是否每个接口都要缓存)
    NSURLSessionDataTask * dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                           if ([self.schemeTaskSet containsObject:urlSchemeTask]) {
                                               if (error) {
                                                   [urlSchemeTask didFailWithError:error];
                                               } else {
                                                   [YXNetworkCache setHttpCache:data URL:urlStr parameters:bodyDict];
                                                   [urlSchemeTask didReceiveResponse:response];
                                                   [urlSchemeTask didReceiveData:data];
                                                   [urlSchemeTask didFinish];
                                               }
                                               [self.schemeTaskSet removeObject:urlSchemeTask];
                                           }
                                       }];
    [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);
    [self.schemeTaskSet removeObject:urlSchemeTask];
}

#pragma mark - lazy
- (NSMutableSet *) schemeTaskSet {
    if (!_schemeTaskSet) {
        _schemeTaskSet = [NSMutableSet set];
    }
    return _schemeTaskSet;
}

@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次耗时:(代表网络一般或者较差时)

Figure_1.png

拦截后加载平均执行耗时:0.24秒

不拦截加载平均执行耗时:1.09秒

可以发现通过自定义WKURLSchemeHandler拦截后,加载速度非常平稳根本不受网络的影响,不拦截的话虽然整体加载速度并不算太慢,但是随网络波动比较明显。

不足

这套方案也还是有一些缺点的,比如

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

总结

综上就是本次H5页面秒加载全部的预研过程了,总的来说,基本上可以满足秒加载的需求。没有完美的解决方案,只有合适的方案,有舍有得,根据公司项目情况来。

おすすめ

転載: juejin.im/post/7236281103221096505