iOS app秒開H5優(yōu)化總結(jié)

為了快速迭代,更新,大部分公司都用了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];

注釋

  1. 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ù)端.

  2. 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)行控制。

  3. 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ù) -> 解析渲染 -> 下載渲染圖片


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

一般頁面在 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ù)制代碼

注意:

  1. setURLSchemeHandler注冊時(shí)機(jī)只能在WKWebView創(chuàng)建WKWebViewConfiguration時(shí)注冊。
  2. WKWebView 只允許開發(fā)者攔截自定義 Scheme 的請(qǐng)求,不允許攔截 “http”、“https”、“ftp”、“file” 等的請(qǐng)求,否則會(huì)crash。
  3. 【補(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 注:本篇研究重點(diǎn)不在于某個(gè)離線方案的具體使用,而在于對(duì)方案的優(yōu)缺點(diǎn)分析、探究和選型,以及一些我個(gè)人的看法。 前言 ...
    LotLewis閱讀 10,557評(píng)論 7 16
  • iOS app秒開H5實(shí)戰(zhàn)總結(jié) 在《iOS app秒開H5優(yōu)化探索》一文中簡單介紹了優(yōu)化的方案以及一些知識(shí)點(diǎn),本文...
    叩首問路夢碼為生閱讀 2,818評(píng)論 2 8
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,656評(píng)論 1 32
  • 《好 喜歡你》 好喜歡你 初次相遇的時(shí)候 好喜歡你 睡前想你的時(shí)候 好喜歡你 當(dāng)你在生氣的時(shí)候 好喜歡你 當(dāng)我們聊...
    XIESHI閱讀 290評(píng)論 0 2
  • 士兵來報(bào):“大人,皇室建議城中開設(shè)聚賢館。” “聚賢館?已然開了,不過掛了別的名字而已?!背侵鬟呎f...
    兩晉閱讀 237評(píng)論 0 0

友情鏈接更多精彩內(nèi)容