為了快速迭代,更新,大部分公司都用了h5去實現(xiàn)公司部分模塊功能,而公司使用h5實現(xiàn)的模塊的性能和原生還是有很大的差距,就衍生了如何優(yōu)化h5的加載速度,和體驗問題。
首先對wkwebview初始化優(yōu)化
創(chuàng)建緩存池,CustomWebViewPool ,減少wkwebview 創(chuàng)建花銷的時間
.h文件
@interface CustomWebViewPool : NSObject
+ (instancetype)sharedInstance;
/**
預初始化若干WKWebView
@param count 個數(shù)
*/
- (void)prepareWithCount:(NSUInteger)count;
/**
從池中獲取一個WKWebView
@return WKWebView
*/
- (CustomWebView *)getWKWebViewFromPool;
.m文件
@interface CustomWebViewPool()
@property (nonatomic) NSUInteger initialViewsMaxCount; //最多初始化的個數(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;
}
/**
預初始化若干WKWebView
@param count 個數(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(@"=======初始化耗時:%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;
}
}
/**
創(chuàng)建一個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è)務需求初始化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];
通過修改初始化緩存策略,從而實現(xiàn)初始化的優(yōu)化(公司有人修改了緩存策略不使用本地緩存,哎)
NSMutableURLRequest *requst=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
[requst setCachePolicy: NSURLRequestUseProtocolCachePolicy];
注釋
NSURLRequestUseProtocolCachePolicy(常用)
這個是默認緩存策略也是一個比較有用的緩存策略,它會根據(jù)HTTP頭中的信息進行緩存處理。
此處都說一句,緩存會將獲取到的數(shù)據(jù)緩存的disk。具體驗證和詳細解析可以看NSURLCache Uses a Disk Cache as of iOS 5
服務器可以在HTTP頭中加入Expires和Cache-Control等來告訴客戶端應該施行的緩存策略。在后面會詳細介紹。
1> NSURLRequestUseProtocolCachePolicy = 0, 默認的緩存策略, 如果緩存不存在,直接從服務端獲取。如果緩存存在,會根據(jù)response中的Cache-Control字段判斷下一步操作,如: Cache-Control字段為must-revalidata, 則詢問服務端該數(shù)據(jù)是否有更新,無更新的話直接返回給用戶緩存數(shù)據(jù),若已更新,則請求服務端.NSURLRequestReloadIgnoringCacheData(偶爾使用)
顧名思義 忽略本地緩存。使用場景就是要忽略本地緩存的情況下使用。3.NSURLRequestReturnCacheDataElseLoad(不用)
這個策略比較有趣,它會一直償試讀取緩存數(shù)據(jù),直到無法沒有緩存數(shù)據(jù)的時候,才會去請求網絡。這個策略有一個重大的缺陷導致它根本無法被使用,即它根本沒有對緩存的刷新時機進行控制,如果你要去使用它,那么需要額外的進行對緩存過期進行控制。NSURLRequestReturnCacheDataDontLoad(不用)
這個選項只讀緩存,無論何時都不會進行網絡請求
使用默認的話,wkwebview 能做到自動做相應的緩存,并在恰當?shù)臅r間清除緩存
如果用 NSURLRequestReturnCacheDataElseLoad ,需要寫相應的緩存策略
在AppDelegate的application: didFinishLaunchingWithOptions: 方法中寫
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024
diskCapacity:50 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
騰訊bugly發(fā)表的一篇文章《移動端本地 H5 秒開方案探索與實現(xiàn)》中分析,H5體驗糟糕,是因為它做了很多事:
初始化 webview -> 請求頁面 -> 下載數(shù)據(jù) -> 解析HTML -> 請求 js/css 資源 -> dom 渲染 -> 解析 JS 執(zhí)行 -> JS 請求數(shù)據(jù) -> 解析渲染 -> 下載渲染圖片
一般頁面在 dom 渲染后才能展示,可以發(fā)現(xiàn),H5 首屏渲染白屏問題的原因關鍵在于,如何優(yōu)化減少從請求下載頁面到渲染之間這段時間的耗時。 所以,減少網絡請求,采用加載離線資源加載方案來做優(yōu)化。
核心原理:
在app打開時,從服務端下載h5源文件zip包,下載到本地,通過url地址做本地攔截,判斷該地址本地是否有源文件,有的話,直接加載本地資源文件,減少從請求下載頁面到渲染之間這段時間的耗時,如果沒有的話,在請求網址
將h5源文件打包成zip包下載到本地
為此可以把所有資源文件(js/css/html等)整合成zip包,一次性下載至本地,使用SSZipArchive解壓到指定位置,更新version即可。 此外,下載時機在app啟動和前后臺切換都做一次檢查更新,效果更好。
注釋:
zip包內容:css,js,html,通用的圖片等
下載時機:在app啟動的時候,開啟線程下載資源,注意不要影響app的啟動。
存放位置:選用沙盒中的Library/Caches。
因為資源會不定時更新,而/Library/Documents更適合存放一些重要的且不經常更新的數(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];
//腳本臨時存放路徑
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。
根據(jù) Apple 官方統(tǒng)計結果,目前iOS 11及以上的用戶占比達95%。又結合自己公司的業(yè)務特性和面向的用戶,決定使用WKURLSchemeHandler來實現(xiàn)攔截,而iOS 11以前的不做處理。
著手前,要與前端統(tǒng)一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需要實現(xiàn)WKURLSchemeHandler協(xié)議
//本例中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創(chuàng)建WKWebViewConfiguration時注冊。
- WKWebView 只允許開發(fā)者攔截自定義 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 {
}
//根據(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
復制代碼
分析,這里攔截到URLScheme為customScheme的請求后,讀取本地資源,并返回給WKWebView顯示;若找不到本地資源,要將自定義 Scheme 的請求轉換成 http 或 https 請求用NSURLSession重新發(fā)出,收到回包后再將數(shù)據(jù)返回給WKWebView。
綜上總結:此方案需要服務端,h5端,iOS端共同合作討論完成,此方案具有一定風險性,最后在做這種方式的是有有個開關處理,防止線上因為這種改動導致大規(guī)模網頁加載不了的問題。
服務端:
需要把所涉及到的h5頁面的源文件上傳到服務端,并為客戶端提供接口,供前端下載
h5:
1.需要做iOS和安卓的來源判斷,可以通過user-agent,加個特殊字段做,也可以通過js直接寫個本地方法
2.需要前端統(tǒng)一URL-Scheme,且注意跨域問題
騰訊UIWebView秒開框架
輕量級高性能Hybrid框架VasSonic秒開實現(xiàn)解析
Github: Tencent/VasSonic