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

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

注釋

  1. 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ù),若已更新,則請求服務端.

  2. NSURLRequestReloadIgnoringCacheData(偶爾使用)
    顧名思義 忽略本地緩存。使用場景就是要忽略本地緩存的情況下使用。3.NSURLRequestReturnCacheDataElseLoad(不用)
    這個策略比較有趣,它會一直償試讀取緩存數(shù)據(jù),直到無法沒有緩存數(shù)據(jù)的時候,才會去請求網絡。這個策略有一個重大的缺陷導致它根本無法被使用,即它根本沒有對緩存的刷新時機進行控制,如果你要去使用它,那么需要額外的進行對緩存過期進行控制。

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


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

一般頁面在 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

復制代碼

注意:

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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