为了快速迭代,更新,大部分公司都用了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];
注释
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, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端.NSURLRequestReloadIgnoringCacheData(偶尔使用)
顾名思义 忽略本地缓存。使用场景就是要忽略本地缓存的情况下使用。3.NSURLRequestReturnCacheDataElseLoad(不用)
这个策略比较有趣,它会一直偿试读取缓存数据,直到无法没有缓存数据的时候,才会去请求网络。这个策略有一个重大的缺陷导致它根本无法被使用,即它根本没有对缓存的刷新时机进行控制,如果你要去使用它,那么需要额外的进行对缓存过期进行控制。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 请求数据 -> 解析渲染 -> 下载渲染图片
一般页面在 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
复制代码
注意:
- setURLSchemeHandler注册时机只能在WKWebView创建WKWebViewConfiguration时注册。
- WKWebView 只允许开发者拦截自定义 Scheme 的请求,不允许拦截 “http”、“https”、“ftp”、“file” 等的请求,否则会crash。
- 【补充】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