為了快速迭代,更新,大部分公司都用了h5去實(shí)現(xiàn)公司部分模塊功能,而公司使用h5實(shí)現(xiàn)的模塊的性能和原生還是有很大的差距,就衍生了如何優(yōu)化h5的加載速度,和體驗(yàn)問題。
首先對(duì)wkwebview初始化優(yōu)化
創(chuàng)建緩存池,CustomWebViewPool ,減少wkwebview 創(chuàng)建花銷的時(shí)間
.h文件
@interface CustomWebViewPool : NSObject
+ (instancetype)sharedInstance;
/**
預(yù)初始化若干WKWebView
@param count 個(gè)數(shù)
*/
- (void)prepareWithCount:(NSUInteger)count;
/**
從池中獲取一個(gè)WKWebView
@return WKWebView
*/
- (CustomWebView *)getWKWebViewFromPool;
.m文件
@interface CustomWebViewPool()
@property (nonatomic) NSUInteger initialViewsMaxCount; //最多初始化的個(gè)數(shù)
@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;
}
/**
預(yù)初始化若干WKWebView
@param count 個(gè)數(shù)
*/
- (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(@"=======初始化耗時(shí):%f", delta);
}
/**
從池中獲取一個(gè)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;
}
}
/**
創(chuàng)建一個(gè)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];
//根據(jù)自己的業(yè)務(wù)需求初始化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
創(chuàng)建CustomWebView
CustomWebViewPool *webViewPool = [CustomWebViewPool sharedInstance];
[webViewPool prepareWithCount:20];
self.webView = [webViewPool getWKWebViewFromPool];
通過修改初始化緩存策略,從而實(shí)現(xiàn)初始化的優(yōu)化(公司有人修改了緩存策略不使用本地緩存,哎)
NSMutableURLRequest *requst=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
[requst setCachePolicy: NSURLRequestUseProtocolCachePolicy];
注釋
NSURLRequestUseProtocolCachePolicy(常用)
這個(gè)是默認(rèn)緩存策略也是一個(gè)比較有用的緩存策略,它會(huì)根據(jù)HTTP頭中的信息進(jìn)行緩存處理。
此處都說一句,緩存會(huì)將獲取到的數(shù)據(jù)緩存的disk。具體驗(yàn)證和詳細(xì)解析可以看NSURLCache Uses a Disk Cache as of iOS 5
服務(wù)器可以在HTTP頭中加入Expires和Cache-Control等來告訴客戶端應(yīng)該施行的緩存策略。在后面會(huì)詳細(xì)介紹。
1> NSURLRequestUseProtocolCachePolicy = 0, 默認(rèn)的緩存策略, 如果緩存不存在,直接從服務(wù)端獲取。如果緩存存在,會(huì)根據(jù)response中的Cache-Control字段判斷下一步操作,如: Cache-Control字段為must-revalidata, 則詢問服務(wù)端該數(shù)據(jù)是否有更新,無更新的話直接返回給用戶緩存數(shù)據(jù),若已更新,則請(qǐng)求服務(wù)端.NSURLRequestReloadIgnoringCacheData(偶爾使用)
顧名思義 忽略本地緩存。使用場景就是要忽略本地緩存的情況下使用。3.NSURLRequestReturnCacheDataElseLoad(不用)
這個(gè)策略比較有趣,它會(huì)一直償試讀取緩存數(shù)據(jù),直到無法沒有緩存數(shù)據(jù)的時(shí)候,才會(huì)去請(qǐng)求網(wǎng)絡(luò)。這個(gè)策略有一個(gè)重大的缺陷導(dǎo)致它根本無法被使用,即它根本沒有對(duì)緩存的刷新時(shí)機(jī)進(jìn)行控制,如果你要去使用它,那么需要額外的進(jìn)行對(duì)緩存過期進(jìn)行控制。NSURLRequestReturnCacheDataDontLoad(不用)
這個(gè)選項(xiàng)只讀緩存,無論何時(shí)都不會(huì)進(jìn)行網(wǎng)絡(luò)請(qǐng)求
使用默認(rèn)的話,wkwebview 能做到自動(dòng)做相應(yīng)的緩存,并在恰當(dāng)?shù)臅r(shí)間清除緩存
如果用 NSURLRequestReturnCacheDataElseLoad ,需要寫相應(yīng)的緩存策略
在AppDelegate的application: didFinishLaunchingWithOptions: 方法中寫
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024
diskCapacity:50 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
騰訊bugly發(fā)表的一篇文章《移動(dòng)端本地 H5 秒開方案探索與實(shí)現(xiàn)》中分析,H5體驗(yàn)糟糕,是因?yàn)樗隽撕芏嗍拢?/p>
初始化 webview -> 請(qǐng)求頁面 -> 下載數(shù)據(jù) -> 解析HTML -> 請(qǐng)求 js/css 資源 -> dom 渲染 -> 解析 JS 執(zhí)行 -> JS 請(qǐng)求數(shù)據(jù) -> 解析渲染 -> 下載渲染圖片


一般頁面在 dom 渲染后才能展示,可以發(fā)現(xiàn),H5 首屏渲染白屏問題的原因關(guān)鍵在于,如何優(yōu)化減少從請(qǐng)求下載頁面到渲染之間這段時(shí)間的耗時(shí)。 所以,減少網(wǎng)絡(luò)請(qǐng)求,采用加載離線資源加載方案來做優(yōu)化。
核心原理:
在app打開時(shí),從服務(wù)端下載h5源文件zip包,下載到本地,通過url地址做本地?cái)r截,判斷該地址本地是否有源文件,有的話,直接加載本地資源文件,減少從請(qǐng)求下載頁面到渲染之間這段時(shí)間的耗時(shí),如果沒有的話,在請(qǐng)求網(wǎng)址
將h5源文件打包成zip包下載到本地
為此可以把所有資源文件(js/css/html等)整合成zip包,一次性下載至本地,使用SSZipArchive解壓到指定位置,更新version即可。 此外,下載時(shí)機(jī)在app啟動(dòng)和前后臺(tái)切換都做一次檢查更新,效果更好。
注釋:
zip包內(nèi)容:css,js,html,通用的圖片等
下載時(shí)機(jī):在app啟動(dòng)的時(shí)候,開啟線程下載資源,注意不要影響app的啟動(dòng)。
存放位置:選用沙盒中的Library/Caches。
因?yàn)橘Y源會(huì)不定時(shí)更新,而/Library/Documents更適合存放一些重要的且不經(jīng)常更新的數(shù)據(jù)。
更新邏輯:把所有資源文件(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];
//腳本臨時(shí)存放路徑
NSString *downloadTmpPath = [NSString stringWithFormat:@"%@pkgfile_%@.zip", NSTemporaryDirectory(), version];
// 文件移動(dòng)到指定目錄中
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;
}
//更新版本號(hào)
[[NSUserDefaults standardUserDefaults] setValue:version forKey:pkgfileVisionKey];
[[NSUserDefaults standardUserDefaults] synchronize];
//清除臨時(shí)文件和目錄
[fileManager removeItemAtPath:downloadTmpPath error:nil];
}];
[downLoadTask resume];
[session finishTasksAndInvalidate];
WKURLSchemeHandler
iOS 11上, WebKit 團(tuán)隊(duì)終于開放了WKWebView加載自定義資源的API:WKURLSchemeHandler。
根據(jù) Apple 官方統(tǒng)計(jì)結(jié)果,目前iOS 11及以上的用戶占比達(dá)95%。又結(jié)合自己公司的業(yè)務(wù)特性和面向的用戶,決定使用WKURLSchemeHandler來實(shí)現(xiàn)攔截,而iOS 11以前的不做處理。
著手前,要與前端統(tǒng)一URL-Scheme,如:customScheme,資源定義成customScheme://xxx/path/xxxx.css。native端使用時(shí),先注冊customScheme,WKWebView請(qǐng)求加載網(wǎng)頁,遇到customScheme的資源,就會(huì)被hock住,然后使用本地已下載好的資源進(jìn)行加載。
客戶端使用直接上代碼:
注冊
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
//設(shè)置URLSchemeHandler來處理特定URLScheme的請(qǐng)求,URLSchemeHandler需要實(shí)現(xiàn)WKURLSchemeHandler協(xié)議
//本例中WKWebView將把URLScheme為customScheme的請(qǐng)求交由CustomURLSchemeHandler類的實(shí)例處理
[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
復(fù)制代碼
注意:
- setURLSchemeHandler注冊時(shí)機(jī)只能在WKWebView創(chuàng)建WKWebViewConfiguration時(shí)注冊。
- WKWebView 只允許開發(fā)者攔截自定義 Scheme 的請(qǐng)求,不允許攔截 “http”、“https”、“ftp”、“file” 等的請(qǐng)求,否則會(huì)crash。
- 【補(bǔ)充】WKWebView加載網(wǎng)頁前,要在user-agent添加個(gè)標(biāo)志,H5遇到這個(gè)標(biāo)識(shí)就使用customScheme,否則就是用原來的http或https。
攔截
#import "ViewController.h"
#import <WebKit/WebKit.h>
@interface CustomURLSchemeHandler : NSObject<WKURLSchemeHandler>
@end
@implementation CustomURLSchemeHandler
//當(dāng) WKWebView 開始加載自定義scheme的資源時(shí),會(huì)調(diào)用
- (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 {
}
//根據(jù)路徑獲取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
復(fù)制代碼
分析,這里攔截到URLScheme為customScheme的請(qǐng)求后,讀取本地資源,并返回給WKWebView顯示;若找不到本地資源,要將自定義 Scheme 的請(qǐng)求轉(zhuǎn)換成 http 或 https 請(qǐng)求用NSURLSession重新發(fā)出,收到回包后再將數(shù)據(jù)返回給WKWebView。
綜上總結(jié):此方案需要服務(wù)端,h5端,iOS端共同合作討論完成,此方案具有一定風(fēng)險(xiǎn)性,最后在做這種方式的是有有個(gè)開關(guān)處理,防止線上因?yàn)檫@種改動(dòng)導(dǎo)致大規(guī)模網(wǎng)頁加載不了的問題。
服務(wù)端:
需要把所涉及到的h5頁面的源文件上傳到服務(wù)端,并為客戶端提供接口,供前端下載
h5:
1.需要做iOS和安卓的來源判斷,可以通過user-agent,加個(gè)特殊字段做,也可以通過js直接寫個(gè)本地方法
2.需要前端統(tǒng)一URL-Scheme,且注意跨域問題
騰訊UIWebView秒開框架
輕量級(jí)高性能Hybrid框架VasSonic秒開實(shí)現(xiàn)解析
Github: Tencent/VasSonic