Investigación previa sobre la carga de la página iOS H5 en segundos

fondo

La combinación de arquitectura nativa + página H5 es un modelo de desarrollo de proyectos muy común. Las ventajas de H5 son multiplataforma, desarrollo rápido, iteración rápida y actualización en caliente. La mayoría de los códigos comerciales de aplicaciones de muchos de los principales fabricantes están implementados por H5. Todos sabemos que H5 La experiencia de la página es peor que la original, especialmente cuando el entorno de red es deficiente. Si la primera página de la pantalla es H5, será agrio y genial. Cualquiera que lo haya conocido lo entenderá.

advertencia de pantalla blanca

La pantalla blanca se debe principalmente a la falla o la lentitud en la descarga de los recursos de la página. Puede ver el proceso de carga de H5 a continuación.

Proceso de carga H5

Esta parte del contenido es en realidad mucho en Internet, de la siguiente manera

Inicializar vista web -> página de solicitud -> descargar datos -> analizar HTML -> solicitar recurso js/css -> representación dom -> analizar ejecución JS -> datos de solicitud JS -> analizar representación -> descargar imagen renderizada

Lo principal para optimizar es el período desde la descarga hasta el renderizado. La solución más simple es colocar el paquete web completo localmente en la aplicación y cargarlo a través de la ruta local. Después de este paso, y luego cooperar con la página H5 para hacer un imagen de marcador de posición durante la carga, básicamente No habrá pantalla blanca en Internet, y la velocidad también se ha mejorado significativamente

Puede consultar los siguientes datos de prueba (carga de dirección de red y carga de ruta local)

Se necesita tiempo para abrir la página H5 100 veces en la red WiFi: (representa cuando la red es excelente)

El tiempo medio de ejecución de la carga local: 0,28 segundos

El tiempo medio de ejecución de la carga de la red: 0,58 segundos

Consume mucho tiempo abrir la página H5 100 veces en una red móvil 4G/5G: (representa cuando la red es buena)

El tiempo medio de ejecución de la carga local: 0,43 segundos

El tiempo medio de ejecución de la carga de la red: 2,09 segundos

Lleva tiempo abrir la página H5 100 veces en una red de telefonía móvil 3G: (representa cuando la red es regular o mala)

El tiempo medio de ejecución de la carga local: 1,48 segundos

El tiempo medio de ejecución de la carga de la red: 19,09 segundos

ok, felicitaciones, la segunda función de carga fuera de línea H5 ha sido optimizada, ¿así de simple?

IMG_4941.JPG

en el titulo

Aunque la velocidad de carga local es mucho más rápida a través de la implementación anterior, la visualización de datos de la página H5 aún debe solicitarse a través de la interfaz. Si el entorno de red no es bueno, será muy lento. ¿Cómo resolver este problema?

下面开始上绝活,这里引入一个概念 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页面秒加载全部的预研过程了,总的来说,基本上可以满足秒加载的需求。没有完美的解决方案,只有合适的方案,有舍有得,根据公司项目情况来。

Supongo que te gusta

Origin juejin.im/post/7236281103221096505
Recomendado
Clasificación