iOS app秒开H5优化总结

为了快速迭代,更新,大部分公司都用了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 请求数据 -> 解析渲染 -> 下载渲染图片


iOS app秒开H5优化总结_第1张图片
9a2f8beb.png
iOS app秒开H5优化总结_第2张图片
屏幕快照 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 

@interface CustomURLSchemeHandler : NSObject
@end

@implementation CustomURLSchemeHandler
//当 WKWebView 开始加载自定义scheme的资源时,会调用
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id )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优化总结)