美文网首页iOS 功能类
iOS app秒开H5优化总结

iOS app秒开H5优化总结

作者: lumic000 | 来源:发表于2019-08-19 13:51 被阅读0次

    为了快速迭代,更新,大部分公司都用了h5去实现公司部分模块功能,而公司使用h5实现的模块的性能和原生还是有很大的差距,就衍生了如何优化h5的加载速度,和体验问题。

    首先对wkwebview初始化优化

    创建缓存池,CustomWebViewPool ,减少wkwebview 创建花销的时间
    .h文件

    @interface CustomWebViewPool : NSObject
    
    + (instancetype)sharedInstance;
    
    /**
     预初始化若干WKWebView
     @param count 个数
     */
    - (void)prepareWithCount:(NSUInteger)count;
    
    /**
     从池中获取一个WKWebView
     
     @return WKWebView
     */
    
    - (CustomWebView *)getWKWebViewFromPool;
    

    .m文件

    @interface CustomWebViewPool()
    @property (nonatomic) NSUInteger initialViewsMaxCount;  //最多初始化的个数
    @property (nonatomic) NSMutableArray *preloadedViews;
    
    @end
    
    @implementation CustomWebViewPool
    
    + (instancetype)sharedInstance {
        static dispatch_once_t onceToken;
        static CustomWebViewPool *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 = 20;
            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
     */
    - (CustomWebView *)getWKWebViewFromPool {
        if (!self.preloadedViews.count) {
            NSLog(@"不够啦!");
            return [self createPreloadedView];
        } else {
            id preloadedView = self.preloadedViews.firstObject;
            [self.preloadedViews removeObject:preloadedView];
            return preloadedView;
        }
    }
    
    /**
     创建一个WKWebView
     @return WKWebView
     */
    - (CustomWebView *)createPreloadedView {
        
        WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
        WKUserContentController *wkUController = [[WKUserContentController alloc] init];
        wkWebConfig.userContentController = wkUController;
        
        CustomWebView *wkWebView = [[CustomWebView alloc]initWithFrame:CGRectZero configuration:wkWebConfig];
        //根据自己的业务需求初始化WKWebView
        wkWebView.opaque = NO;
        wkWebView.scrollView.scrollEnabled = YES;
        wkWebView.scrollView.showsVerticalScrollIndicator = YES;
        wkWebView.scrollView.scrollsToTop = YES;
        wkWebView.scrollView.userInteractionEnabled = YES;
        if (@available(iOS 11.0,*)) {
            wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        }
        wkWebView.scrollView.bounces = NO;
        wkWebView.backgroundColor = [UIColor clearColor];
        
        return wkWebView;
    }
    
    @end
    

    创建CustomWebView

        CustomWebViewPool *webViewPool = [CustomWebViewPool sharedInstance];
        [webViewPool prepareWithCount:20];
        self.webView = [webViewPool getWKWebViewFromPool];
    

    通过修改初始化缓存策略,从而实现初始化的优化(公司有人修改了缓存策略不使用本地缓存,哎)

    NSMutableURLRequest *requst=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
    [requst setCachePolicy: NSURLRequestUseProtocolCachePolicy];
    

    注释

    1. NSURLRequestUseProtocolCachePolicy(常用)
      这个是默认缓存策略也是一个比较有用的缓存策略,它会根据HTTP头中的信息进行缓存处理。
      此处都说一句,缓存会将获取到的数据缓存的disk。具体验证和详细解析可以看NSURLCache Uses a Disk Cache as of iOS 5
      服务器可以在HTTP头中加入Expires和Cache-Control等来告诉客户端应该施行的缓存策略。在后面会详细介绍。
      1> NSURLRequestUseProtocolCachePolicy = 0, 默认的缓存策略, 如果缓存不存在,直接从服务端获取。如果缓存存在,会根据response中的Cache-Control字段判断下一步操作,如: Cache-Control字段为must-revalidata, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端.

    2. NSURLRequestReloadIgnoringCacheData(偶尔使用)
      顾名思义 忽略本地缓存。使用场景就是要忽略本地缓存的情况下使用。3.NSURLRequestReturnCacheDataElseLoad(不用)
      这个策略比较有趣,它会一直偿试读取缓存数据,直到无法没有缓存数据的时候,才会去请求网络。这个策略有一个重大的缺陷导致它根本无法被使用,即它根本没有对缓存的刷新时机进行控制,如果你要去使用它,那么需要额外的进行对缓存过期进行控制。

    3. NSURLRequestReturnCacheDataDontLoad(不用)
      这个选项只读缓存,无论何时都不会进行网络请求

    使用默认的话,wkwebview 能做到自动做相应的缓存,并在恰当的时间清除缓存
    如果用 NSURLRequestReturnCacheDataElseLoad ,需要写相应的缓存策略
    在AppDelegate的application: didFinishLaunchingWithOptions: 方法中写

    NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024
                                                             diskCapacity:50 * 1024 * 1024
                                                                 diskPath:nil];
    [NSURLCache setSharedURLCache:URLCache];
    

    腾讯bugly发表的一篇文章《移动端本地 H5 秒开方案探索与实现》中分析,H5体验糟糕,是因为它做了很多事:

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


    9a2f8beb.png 屏幕快照 2019-08-20 下午8.45.16.png

    一般页面在 dom 渲染后才能展示,可以发现,H5 首屏渲染白屏问题的原因关键在于,如何优化减少从请求下载页面到渲染之间这段时间的耗时。 所以,减少网络请求,采用加载离线资源加载方案来做优化。

    核心原理:

    在app打开时,从服务端下载h5源文件zip包,下载到本地,通过url地址做本地拦截,判断该地址本地是否有源文件,有的话,直接加载本地资源文件,减少从请求下载页面到渲染之间这段时间的耗时,如果没有的话,在请求网址

    将h5源文件打包成zip包下载到本地

    为此可以把所有资源文件(js/css/html等)整合成zip包,一次性下载至本地,使用SSZipArchive解压到指定位置,更新version即可。 此外,下载时机在app启动和前后台切换都做一次检查更新,效果更好。

    注释:
    zip包内容:css,js,html,通用的图片等
    下载时机:在app启动的时候,开启线程下载资源,注意不要影响app的启动。
    存放位置:选用沙盒中的Library/Caches。
    因为资源会不定时更新,而/Library/Documents更适合存放一些重要的且不经常更新的数据。
    更新逻辑:把所有资源文件(js/css/html等)整合成zip包,一次性下载至本地,使用SSZipArchive解压到指定位置,更新version即可

    //获取沙盒中的Library/Caches/路径
     NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
     NSString *dirPath = [docPath componentsSeparatedByString:@"loadH5.zip"];
     NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDownloadTask *downLoadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
      if (!location) {
          return ;
      }
      //下载成功,移除旧资源
      [fileManager removeFileAtPath:dirPath fileExtesion:nil];
      
      //脚本临时存放路径
      NSString *downloadTmpPath = [NSString stringWithFormat:@"%@pkgfile_%@.zip", NSTemporaryDirectory(), version];
      // 文件移动到指定目录中
      NSError *saveError;
      [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:downloadTmpPath] error:&saveError];
      //解压zip
      BOOL success = [SSZipArchive unzipFileAtPath:downloadTmpPath toDestination:dirPath];
      if (!success) {
          LogError(@"pkgfile: unzip file error");
          [fileManager removeItemAtPath:downloadTmpPath error:nil];
          [fileManager removeFileAtPath:dirPath fileExtesion:nil];
          return;
      }
      //更新版本号
      [[NSUserDefaults standardUserDefaults] setValue:version forKey:pkgfileVisionKey];
      [[NSUserDefaults standardUserDefaults] synchronize];
      //清除临时文件和目录
      [fileManager removeItemAtPath:downloadTmpPath error:nil];
    }];
    [downLoadTask resume];
    [session finishTasksAndInvalidate];
    

    WKURLSchemeHandler

    iOS 11上, WebKit 团队终于开放了WKWebView加载自定义资源的API:WKURLSchemeHandler。

    根据 Apple 官方统计结果,目前iOS 11及以上的用户占比达95%。又结合自己公司的业务特性和面向的用户,决定使用WKURLSchemeHandler来实现拦截,而iOS 11以前的不做处理。

    着手前,要与前端统一URL-Scheme,如:customScheme,资源定义成customScheme://xxx/path/xxxx.css。native端使用时,先注册customScheme,WKWebView请求加载网页,遇到customScheme的资源,就会被hock住,然后使用本地已下载好的资源进行加载。

    客户端使用直接上代码:

    注册

    @implementation ViewController
    - (void)viewDidLoad {    
        [super viewDidLoad];    
        WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
        //设置URLSchemeHandler来处理特定URLScheme的请求,URLSchemeHandler需要实现WKURLSchemeHandler协议
        //本例中WKWebView将把URLScheme为customScheme的请求交由CustomURLSchemeHandler类的实例处理    
        [configuration setURLSchemeHandler:[CustomURLSchemeHandler new] forURLScheme: @"customScheme"];    
        WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];    
        self.view = webView;    
        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"customScheme://www.test.com"]]];
    }
    @end
    
    复制代码
    

    注意:

    1. setURLSchemeHandler注册时机只能在WKWebView创建WKWebViewConfiguration时注册。
    2. WKWebView 只允许开发者拦截自定义 Scheme 的请求,不允许拦截 “http”、“https”、“ftp”、“file” 等的请求,否则会crash。
    3. 【补充】WKWebView加载网页前,要在user-agent添加个标志,H5遇到这个标识就使用customScheme,否则就是用原来的http或https。

    拦截

    #import "ViewController.h"
    #import <WebKit/WebKit.h>
    
    @interface CustomURLSchemeHandler : NSObject<WKURLSchemeHandler>
    @end
    
    @implementation CustomURLSchemeHandler
    //当 WKWebView 开始加载自定义scheme的资源时,会调用
    - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
    API_AVAILABLE(ios(11.0)){
    
        //加载本地资源
        NSString *fileName = [urlSchemeTask.request.URL.absoluteString componentsSeparatedByString:@"/"].lastObject;
        fileName = [fileName componentsSeparatedByString:@"?"].firstObject;
        NSString *dirPath = [kPathCache stringByAppendingPathComponent:kCssFiles];
        NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
    
        //文件不存在
        if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
            NSString *replacedStr = @"";
            NSString *schemeUrl = urlSchemeTask.request.URL.absoluteString;
            if ([schemeUrl hasPrefix:kUrlScheme]) {
                replacedStr = [schemeUrl stringByReplacingOccurrencesOfString:kUrlScheme withString:@"http"];
            }
    
            NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:replacedStr]];
            NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
            NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
    
            NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
                [urlSchemeTask didReceiveResponse:response];
                [urlSchemeTask didReceiveData:data];
                if (error) {
                    [urlSchemeTask didFailWithError:error];
                } else {
                    [urlSchemeTask didFinish];
                }
            }];
            [dataTask resume];
        } else {
            NSData *data = [NSData dataWithContentsOfFile:filePath];
    
            NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL
                                                                MIMEType:[self getMimeTypeWithFilePath:filePath]
                                                   expectedContentLength:data.length
                                                        textEncodingName:nil];
            [urlSchemeTask didReceiveResponse:response];
            [urlSchemeTask didReceiveData:data];
            [urlSchemeTask didFinish];
        }
    }
    
    - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id)urlSchemeTask {
    }
    
    //根据路径获取MIMEType
    - (NSString *)getMimeTypeWithFilePath:(NSString *)filePath {
        CFStringRef pathExtension = (__bridge_retained CFStringRef)[filePath pathExtension];
        CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, NULL);
        CFRelease(pathExtension);
    
        //The UTI can be converted to a mime type:
        NSString *mimeType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType);
        if (type != NULL)
            CFRelease(type);
    
        return mimeType;
    }
    
    @end
    复制代码
    

    分析,这里拦截到URLScheme为customScheme的请求后,读取本地资源,并返回给WKWebView显示;若找不到本地资源,要将自定义 Scheme 的请求转换成 http 或 https 请求用NSURLSession重新发出,收到回包后再将数据返回给WKWebView。

    综上总结:此方案需要服务端,h5端,iOS端共同合作讨论完成,此方案具有一定风险性,最后在做这种方式的是有有个开关处理,防止线上因为这种改动导致大规模网页加载不了的问题。
    服务端:
    需要把所涉及到的h5页面的源文件上传到服务端,并为客户端提供接口,供前端下载
    h5:
    1.需要做iOS和安卓的来源判断,可以通过user-agent,加个特殊字段做,也可以通过js直接写个本地方法
    2.需要前端统一URL-Scheme,且注意跨域问题

    腾讯UIWebView秒开框架
    轻量级高性能Hybrid框架VasSonic秒开实现解析
    Github: Tencent/VasSonic

    相关文章

      网友评论

        本文标题:iOS app秒开H5优化总结

        本文链接:https://www.haomeiwen.com/subject/qveksctx.html