iOS-WKWebView

本文參考資料:
1.https://mp.weixin.qq.com/s/rhYKLIbXOsUJC_n6dt9UfA
2.https://xibhe.com/2018/02/03/WKWebView-disabuse/index.html
3.http://www.reibang.com/p/9a08aa2e6825

WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 組件膏孟,用以替代 UIKit 中笨重難用、內(nèi)存泄漏的 UIWebView祟印。WKWebView 擁有60fps滾動(dòng)刷新率简软、和 safari 相同的 JavaScript 引擎等優(yōu)勢(shì)谜悟。

一. WKWebView的優(yōu)點(diǎn)與缺點(diǎn)

1. WKWebView的優(yōu)點(diǎn)

(1). 多進(jìn)程饥瓷,在app的主進(jìn)程之外執(zhí)行

WKWebView為多進(jìn)程組件舷丹,也意味著會(huì)從App內(nèi)存中分離內(nèi)存到單獨(dú)的進(jìn)程(Network Process and Rendring Process)中氓奈。當(dāng)內(nèi)存超過了系統(tǒng)分配給WKWebView的內(nèi)存時(shí)候翘魄,會(huì)導(dǎo)致WKWebView瀏覽器崩潰白屏,但是App不會(huì)Crash舀奶。(app會(huì)收到系統(tǒng)通知暑竟,并且嘗試去重新加載頁面)
相反的,UIWebView是和app同一個(gè)進(jìn)程育勺,UIWebView加載頁面占用的內(nèi)存被計(jì)算為app內(nèi)存占用的一部分光羞,當(dāng)app超過了系統(tǒng)分配的內(nèi)存绩鸣,則會(huì)被操作系統(tǒng)crash。在整個(gè)過程中纱兑,會(huì)經(jīng)常收到iOS系統(tǒng)的通知用來防止app被系統(tǒng)kill呀闻,但是在某些時(shí)候,這些通知不夠及時(shí)潜慎,或者根本沒有返回通知捡多。

(2). 使用更快的Nitro JavaScript引擎

WKWebView使用和手機(jī)Safari瀏覽器一樣的Nitro JavaScript引擎,相比于UIWebView的JavaScript引擎有了非常重要的性能提升铐炫。

(3). 異步執(zhí)行處理JavaScript

WKWebView是異步處理app原生代碼與JavaScript之間的通信垒手,因此普遍上執(zhí)行速度會(huì)更快。
在實(shí)踐操作過程中倒信,JavaScript API調(diào)用原生(native)中方法不會(huì)阻塞線程科贬,等待回調(diào)函數(shù)的執(zhí)行。(在JavaScript代碼會(huì)繼續(xù)向下執(zhí)行鳖悠,而回調(diào)函數(shù)會(huì)由native端異步去回調(diào))榜掌。

(4). 消除觸摸延遲

UIWebView和WKWebView瀏覽器組件會(huì)將觸摸事件解釋后發(fā)送給app,因此乘综,我們無法提高觸摸事件的靈敏度或速度憎账。
在UIWebView上的任何觸摸事件會(huì)被延遲300ms,用以判斷用戶是單擊還是雙擊卡辰。這個(gè)機(jī)制也是那些基于HTML的web app一直不被用戶接受的重要原因琴许。
在WKWebView中罐孝,測(cè)試顯示,只有在點(diǎn)擊很快(<~125ms)的時(shí)候才會(huì)添加300ms的延遲,iOS將其解釋為更可能是雙擊“點(diǎn)擊縮放”手勢(shì)的一部分蛔六,而不是慢點(diǎn)擊(>?125 ms)后擎鸠。更多細(xì)節(jié)在這里
為了消除所有觸摸事件(包括快速點(diǎn)擊)的觸摸延遲睦焕,您可以添加FastClick或另一個(gè)消除此延遲的庫到您的內(nèi)容中迟几。

(5). 支持服務(wù)端的身份校驗(yàn)

與不支持服務(wù)器認(rèn)證校驗(yàn)的UIWebView不同,WKWebView支持服務(wù)端校驗(yàn)嚷兔。實(shí)際上森渐,這意味著在使用WKWebView時(shí),可以輸入密碼保護(hù)網(wǎng)站冒晰。

(6). 支持對(duì)錯(cuò)誤的自簽名安全證書和證書進(jìn)行身份驗(yàn)證

通過“繼續(xù)”/“取消”彈出窗口同衣,WKWebView允許您繞過安全證書中的錯(cuò)誤(例如,使用自簽名證書或過期證書時(shí))壶运。

2. WKWebView的缺點(diǎn)

(1). 需要iOS9或更高版本

我們的WKWebView集成僅適用于運(yùn)行iOS 9或更高版本的設(shè)備耐齐。雖然WKWebView是在iOS 8中引入的,但在這些版本中存在重大限制,包括無法訪問本地存儲(chǔ)的文件埠况,我們無法解決此問題耸携,因此此功能不兼容。

(2). 不支持AJAX請(qǐng)求到本地存儲(chǔ)的文件

WKWebView不允許XHR請(qǐng)求file:// URI辕翰,因?yàn)檫@些URI違反了瀏覽器引擎的跨源資源共享規(guī)則夺衍。使用這種類型的請(qǐng)求的項(xiàng)目應(yīng)該遠(yuǎn)程托管在服務(wù)器上,或使用現(xiàn)有的UIWebView瀏覽引擎喜命。

(3). 不支持"Accept Cookies"的設(shè)置

雖然WKWebView確實(shí)支持使用cookies沟沙,但并沒有公開選擇哪些cookies被源代碼接受的能力。這意味著在使用WKWebView瀏覽引擎時(shí)不會(huì)應(yīng)用“接受Cookie”設(shè)置壁榕。
WKWebView只允許我們?cè)L問cookie的名稱矛紫,而不是附加信息,如創(chuàng)建/過期日期或路徑牌里,這使得更難以解決Cookie出現(xiàn)的問題颊咬。

(4). 不支持"Advanced Cache Settings"(高級(jí)緩存設(shè)置)

使用WKWebView瀏覽引擎時(shí),不會(huì)應(yīng)用“緩存源”和“僅通知服務(wù)器重定向事件的瀏覽器”牡辽。

(5). App退出會(huì)清除HTML5的本地存儲(chǔ)的數(shù)據(jù)

當(dāng)應(yīng)用退出并重新啟動(dòng)時(shí)喳篇,HTML5本地存儲(chǔ)將被清除。

(6).不支持記錄WebKit的請(qǐng)求

WKWebView發(fā)出請(qǐng)求并呈現(xiàn)內(nèi)容催享,無法直接訪問此類請(qǐng)求杭隙,并且無法記錄這些請(qǐng)求哟绊。

(7).不能進(jìn)行截屏操作

盡管我們?cè)跍y(cè)試中沒有看到使用Kiosk Pro的JavaScript API進(jìn)行屏幕捕獲的任何問題因妙,但其他iOS開發(fā)人員報(bào)告說屏幕捕獲在WKWebView上隨機(jī)失敗。如果截屏的API是app中的關(guān)鍵操作票髓,建議使用現(xiàn)有的UIWebView瀏覽引擎攀涵。

二. WKWebView的常見問題

1. WKWebView 白屏問題

WKWebView 自詡擁有更快的加載速度,更低的內(nèi)存占用洽沟,但實(shí)際上 WKWebView 是一個(gè)多進(jìn)程組件以故,Network Loading 以及 UI Rendering 在其它進(jìn)程中執(zhí)行。初次適配 WKWebView 的時(shí)候裆操,我們也驚訝于打開 WKWebView 后怒详,App 進(jìn)程內(nèi)存消耗反而大幅下降,但是仔細(xì)觀察會(huì)發(fā)現(xiàn)踪区,Other Process 的內(nèi)存占用會(huì)增加昆烁。在一些用 webGL 渲染的復(fù)雜頁面,使用 WKWebView 總體的內(nèi)存占用(App Process Memory + Other Process Memory)不見得比 UIWebView 少很多缎岗。
在 UIWebView 上當(dāng)內(nèi)存占用太大的時(shí)候静尼,App Process 會(huì) crash;而在 WKWebView 上當(dāng)總體的內(nèi)存占用比較大的時(shí)候,WebContent Process 會(huì) crash鼠渺,從而出現(xiàn)白屏現(xiàn)象鸭巴。在 WKWebView 中加載下面的測(cè)試鏈接可以穩(wěn)定重現(xiàn)白屏現(xiàn)象:

http://people.mozilla.org/~rnewman/fennec/mem.html
這個(gè)時(shí)候 WKWebView.URL 會(huì)變?yōu)?nil, 簡(jiǎn)單的 reload 刷新操作已經(jīng)失效,對(duì)于一些長(zhǎng)駐的H5頁面影響比較大拦盹。
解決方案是:

(1). 借助 WKNavigtionDelegate

iOS 9以后 WKNavigtionDelegate 新增了一個(gè)回調(diào)函數(shù):

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

當(dāng) WKWebView 總體內(nèi)存占用過大鹃祖,頁面即將白屏的時(shí)候,系統(tǒng)會(huì)調(diào)用上面的回調(diào)函數(shù)普舆,我們?cè)谠摵瘮?shù)里執(zhí)行[webView reload](這個(gè)時(shí)候 webView.URL 取值尚不為 nil)解決白屏問題惯豆。在一些高內(nèi)存消耗的頁面可能會(huì)頻繁刷新當(dāng)前頁面,H5側(cè)也要做相應(yīng)的適配操作奔害。

(2).檢測(cè) webView.title 是否為空

并不是所有H5頁面白屏的時(shí)候都會(huì)調(diào)用上面的回調(diào)函數(shù)楷兽,比如,最近遇到在一個(gè)高內(nèi)存消耗的H5頁面上 present 系統(tǒng)相機(jī)华临,拍照完畢后返回原來頁面的時(shí)候出現(xiàn)白屏現(xiàn)象(拍照過程消耗了大量?jī)?nèi)存芯杀,導(dǎo)致內(nèi)存緊張,WebContent Process 被系統(tǒng)掛起)雅潭,但上面的回調(diào)函數(shù)并沒有被調(diào)用揭厚。在WKWebView白屏的時(shí)候,另一種現(xiàn)象是 webView.titile 會(huì)被置空, 因此扶供,可以在 viewWillAppear 的時(shí)候檢測(cè) webView.title 是否為空來 reload 頁面筛圆。
綜合以上兩種方法可以解決絕大多數(shù)的白屏問題。

2. WKWebView Cookie 問題

(1). WKWebView Cookie存儲(chǔ)

業(yè)界普遍認(rèn)為 WKWebView 擁有自己的私有存儲(chǔ)椿浓,不會(huì)將 Cookie 存入到標(biāo)準(zhǔn)的 Cookie 容器 NSHTTPCookieStorage 中太援。
實(shí)踐發(fā)現(xiàn) WKWebView 實(shí)例其實(shí)也會(huì)將 Cookie 存儲(chǔ)于 NSHTTPCookieStorage 中,但存儲(chǔ)時(shí)機(jī)有延遲扳碍,在iOS 8上提岔,當(dāng)頁面跳轉(zhuǎn)的時(shí)候,當(dāng)前頁面的 Cookie 會(huì)寫入 NSHTTPCookieStorage 中笋敞,而在 iOS 10 上碱蒙,JS 執(zhí)行 document.cookie 或服務(wù)器 set-cookie 注入的 Cookie 會(huì)很快同步到 NSHTTPCookieStorage 中,F(xiàn)ireFox 工程師曾建議通過 reset WKProcessPool 來觸發(fā) Cookie 同步到 NSHTTPCookieStorage 中夯巷,實(shí)踐發(fā)現(xiàn)不起作用赛惩,并可能會(huì)引發(fā)當(dāng)前頁面 session cookie 丟失等問題。
WKWebView Cookie 問題在于 WKWebView 發(fā)起的請(qǐng)求不會(huì)自動(dòng)帶上存儲(chǔ)于 NSHTTPCookieStorage 容器中的 Cookie趁餐。
比如喷兼,NSHTTPCookieStorage 中存儲(chǔ)了一個(gè) Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;

通過 UIWebView 發(fā)起請(qǐng)求http://y.qq.com澎怒, 則請(qǐng)求頭會(huì)自動(dòng)帶上 cookie: Nicholas=test褒搔;
而通過 WKWebView發(fā)起請(qǐng)求http://y.qq.com阶牍, 請(qǐng)求頭不會(huì)自動(dòng)帶上 cookie: Nicholas=test。

(2). WKProcessPool

A. Workaround

由于許多 H5 業(yè)務(wù)都依賴于 Cookie 作登錄態(tài)校驗(yàn)星瘾,而 WKWebView 上請(qǐng)求不會(huì)自動(dòng)攜帶 Cookie, 目前的主要解決方案是:

a. WKWebView loadRequest 前走孽,在 request header 中設(shè)置 Cookie, 解決首個(gè)請(qǐng)求 Cookie 帶不上的問題;
WKWebView * webView = [WKWebView new]; 
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]]; 

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
[webView loadRequest:request];
b. 通過 document.cookie 設(shè)置 Cookie 解決后續(xù)頁面(同域)Ajax琳状、iframe 請(qǐng)求的 Cookie 問題磕瓷;

注意:document.cookie()無法跨域設(shè)置 cookie

WKUserContentController* userContentController = [WKUserContentController new]; 
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 

[userContentController addUserScript:cookieScript];

這種方案無法解決302請(qǐng)求的 Cookie 問題,比如念逞,第一個(gè)請(qǐng)求是 www.a.com困食,我們通過在 request header 里帶上 Cookie 解決該請(qǐng)求的 Cookie 問題,接著頁面302跳轉(zhuǎn)到 www.b.com翎承,這個(gè)時(shí)候 www.b.com 這個(gè)請(qǐng)求就可能因?yàn)闆]有攜帶 cookie 而無法訪問硕盹。當(dāng)然,由于每一次頁面跳轉(zhuǎn)前都會(huì)調(diào)用回調(diào)函數(shù):

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

可以在該回調(diào)函數(shù)里攔截302請(qǐng)求叨咖,copy request瘩例,在 request header 中帶上 cookie 并重新 loadRequest。不過這種方法依然解決不了頁面 iframe 跨域請(qǐng)求的 Cookie 問題甸各,畢竟-[WKWebView loadRequest:]只適合加載 mainFrame 請(qǐng)求

c. 通過 WKProcessPool 實(shí)現(xiàn)多個(gè) WKWebView 之間共享 Cookie

①. 新建一個(gè)名為 LZWKWebKitSupport 的類垛贤,用于生成一個(gè)統(tǒng)一的,全局使用同一個(gè) WKProcessPool 的 WKWebView 對(duì)象趣倾。

// LZWKWebKitSupport.h
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
@interface LZWKWebKitSupport : NSObject
@property (nonatomic, strong,readonly) WKProcessPool *processPool;
+ (instancetype)sharedSupport;
+ (WKWebView *)createSharableWKWebView:(BOOL)isFullScreen isShowNav:(BOOL)showNav;
@end
// LZWKWebKitSupport.m
#import "LZWKWebKitSupport.h"
@interface LZWKWebKitSupport()
@end
@implementation LZWKWebKitSupport
+ (instancetype)sharedSupport {
    static LZWKWebKitSupport *_instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [LZWKWebKitSupport new];
    });
    return  _instance;
}
- (instancetype)init {
    if (self = [super init]) {
        self.processPool = [WKProcessPool new];
    }
    return self;
}
+ (WKWebView *)createSharableWKWebView:(BOOL)isFullScreen isShowNav:(BOOL)showNav
{
    WKUserContentController* userContentController = [WKUserContentController new];
    NSMutableString *cookies = [NSMutableString string];
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[cookies copy]                                                        injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    [userContentController addUserScript:cookieScript];    
    WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
    // 一下兩個(gè)屬性是允許H5視頻自動(dòng)播放,并且全屏,可忽略
    configuration.allowsInlineMediaPlayback = YES;
    configuration.mediaPlaybackRequiresUserAction = NO;
    // 全局使用同一個(gè)processPool
    configuration.processPool = [[LZWKWebKitSupport sharedSupport] processPool];
    configuration.userContentController = userContentController;
    // 考慮到左側(cè)菜單欄聘惦,需要設(shè)置webView的不同frame
    WKWebView *wk_webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, y, width, height) configuration:configuration];
    return wk_webView;
}
@end

②. 在加載H5的地方初始化 LZWKWebKitSupport,并在 WKNavigationDelegate 中獲取 cookie儒恋,并設(shè)置到本地善绎。

// 初始化LZWKWebKitSupport
- (void)viewDidLoad{
_wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:YES];
[self.view addSubview:_wkWebView];
}
#pragma mark - wkwebviewDelegate
//接收到服務(wù)器響應(yīng) 后決定是否允許跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
    decisionHandler(WKNavigationResponsePolicyAllow);
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
    // 讀取cookie,并設(shè)置到本地
    NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
    for (NSHTTPCookie *cookie in cookies) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }
}

③. 在從第一個(gè)H5頁面跳轉(zhuǎn)至第二個(gè)H5頁面時(shí)碧浊,在發(fā)起請(qǐng)求時(shí)注入Cookie涂邀。
這里以跳轉(zhuǎn)到 LZDetailViewController 頁面為例瘟仿,先是通過LZWKWebKitSupport 初始化一個(gè) WKWebView

// LZDetailViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化視圖
    [self setUpSubViews];   
}
- (void)setUpSubViews{
    _wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:NO];
    _wkWebView.UIDelegate = self;
    [self.view addSubview:_wkWebView];
    _jsBridge = [WKWebViewJavascriptBridge bridgeForWebView:_wkWebView];
    [_jsBridge setWebViewDelegate:self];
}

然后在加載請(qǐng)求時(shí)箱锐,注入之前設(shè)置的 Cookie

- (void)loadUrl{
    if (!_urlStr) {
        return;
    }
    NSURL *url = [NSURL URLWithString:_urlStr];
    NSMutableString *cookies = [NSMutableString string];
    NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];
    // 一般都只需要同步BJSESSIONID,可視不同需求自己做更改
    NSString * BJSESSIONID;
    // 獲取本地所有的Cookie
    NSArray *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    for (NSHTTPCookie * cookie in tmp) {
        if ([cookie.name isEqualToString:@"BJSESSIONID"]) {
            BJSESSIONID = cookie.value;
            break;
        }
    }
    if (BJSESSIONID.length) {
        // 格式化Cookie
        [cookies appendFormat:@"BJSESSIONID=%@;",BJSESSIONID];
    }
    // 注入Cookie
    [requestObj setValue:cookies forHTTPHeaderField:@"Cookie"];
    // 加載請(qǐng)求
    [self.wkWebView loadRequest:requestObj];
}

通過以上三步就可以達(dá)到同步 Cookie 的目的,現(xiàn)在看來之前通過 JS腳本 注入 Cookie 失敗劳较,可能是由于后臺(tái)需要同步 BJSESSIONID驹止,而BJSESSIONID 是 HtppOnly,不允許通過js腳本修改观蜗。

最后臊恋,需要特別注意的一點(diǎn)是:考慮在加載H5頁前,是否需要清除某些H5頁面的 Cookie ?

這里對(duì)于我們的項(xiàng)目而言墓捻,加載的需要驗(yàn)證用戶身份信息的H5頁面抖仅,是需要清除 Cookie 的,因?yàn)橛脩舻臋?quán)限不同,所看到的界面就不同撤卢,在同一臺(tái)設(shè)備下切換不同的用戶時(shí)环凿,如果不清除之前的 Cookie,所展示的就是上一個(gè)用戶的信息放吩。

- (void)deleteWKCookies
{
    // 清除WKWebView緩存的cookie(根據(jù)ip)
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0){
        NSString *iPPort = [LZUserDefaults objectForKey:PreferenceKey_SystemInit_ZyIPPort];
        NSArray *iPPortArray = [iPPort componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]];
        NSString *recordIP;
        if ([iPPortArray count] > 2) {
            recordIP = partnerIPPortArray[2];
        }
        WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore];
        [dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
            for (WKWebsiteDataRecord *record  in records)
            {
                // 以www.baidu.com為例智听,是否包含baidu.com
                if ([recordIP containsString:record.displayName])
                {
                    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{
                        NSLog(@"Cookies for %@ deleted successfully",record.displayName);
                    }];
                }
            }
        }];
    }
}

3. WKWebView NSURLProtocol問題

WKWebView 在獨(dú)立于 app 進(jìn)程之外的進(jìn)程中執(zhí)行網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求數(shù)據(jù)不經(jīng)過主進(jìn)程渡紫,因此到推,在 WKWebView 上直接使用 NSURLProtocol 無法攔截請(qǐng)求。蘋果開源的 webKit2 源碼暴露了私有API:

+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

通過注冊(cè) http(s) scheme 后 WKWebView 將可以使用 NSURLProtocol 攔截 http(s) 請(qǐng)求:

Class cls = NSClassFromString(@"WKBrowsingContextController”); 
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); 
if ([(id)cls respondsToSelector:sel]) { 
           // 注冊(cè)http(s) scheme, 把 http和https請(qǐng)求交給 NSURLProtocol處理 
           [(id)cls performSelector:sel withObject:@"http"]; 
           [(id)cls performSelector:sel withObject:@"https"]; 
}

但是這種方案目前存在兩個(gè)嚴(yán)重缺陷:

(1). post 請(qǐng)求 body 數(shù)據(jù)被清空

由于 WKWebView 在獨(dú)立進(jìn)程里執(zhí)行網(wǎng)絡(luò)請(qǐng)求惕澎。一旦注冊(cè) http(s) scheme 后莉测,網(wǎng)絡(luò)請(qǐng)求將從 Network Process 發(fā)送到 App Process,這樣 NSURLProtocol 才能攔截網(wǎng)絡(luò)請(qǐng)求唧喉。在 webkit2 的設(shè)計(jì)里使用 MessageQueue 進(jìn)行進(jìn)程之間的通信悔雹,Network Process 會(huì)將請(qǐng)求 encode 成一個(gè) Message,然后通過 IPC 發(fā)送給 App Process。出于性能的原因欣喧,encode 的時(shí)候 HTTPBody 和 HTTPBodyStream 這兩個(gè)字段被丟棄掉了

參考蘋果源碼:

https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88 (復(fù)制鏈接到瀏覽器中打開)

及bug report:

https://bugs.webkit.org/show_bug.cgi?id=138169 (復(fù)制鏈接到瀏覽器中打開)

因此,如果通過 registerSchemeForCustomProtocol 注冊(cè)了 http(s) scheme, 那么由 WKWebView 發(fā)起的所有 http(s)請(qǐng)求都會(huì)通過 IPC 傳給主進(jìn)程 NSURLProtocol 處理腌零,導(dǎo)致 post 請(qǐng)求 body 被清空;

(2).對(duì)ATS支持不足

測(cè)試發(fā)現(xiàn)一旦打開ATS開關(guān):Allow Arbitrary Loads 選項(xiàng)設(shè)置為NO唆阿,同時(shí)通過 registerSchemeForCustomProtocol 注冊(cè)了 http(s) scheme益涧,WKWebView 發(fā)起的所有 http 網(wǎng)絡(luò)請(qǐng)求將被阻塞(即便將Allow Arbitrary Loads in Web Content 選項(xiàng)設(shè)置為YES);

WKWebView 可以注冊(cè) customScheme, 比如 dynamic://, 因此希望使用離線功能又不使用 post 方式的請(qǐng)求可以通過 customScheme 發(fā)起請(qǐng)求驯鳖,比如 dynamic://www.dynamicalbumlocalimage.com/闲询,然后在 app 進(jìn)程 NSURLProtocol 攔截這個(gè)請(qǐng)求并加載離線數(shù)據(jù)。不足:使用 post 方式的請(qǐng)求該方案依然不適用浅辙,同時(shí)需要 H5 側(cè)修改請(qǐng)求 scheme 以及 CSP 規(guī)則扭弧;

4. WKWebView loadRequest 問題

在 WKWebView 上通過 loadRequest 發(fā)起的 post 請(qǐng)求 body 數(shù)據(jù)會(huì)丟失:

//同樣是由于進(jìn)程間通信性能問題,HTTPBody字段被丟棄[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];

workaround:
假如想通過-[WKWebView loadRequest:]加載 post 請(qǐng)求 request1: http://h5.qzone.qq.com/mqzone/index,可以通過以下步驟實(shí)現(xiàn):

  1. 替換請(qǐng)求 scheme记舆,生成新的 post 請(qǐng)求 request2: post://h5.qzone.qq.com/mqzone/index, 同時(shí)將 request1 的 body 字段復(fù)制到 request2 的 header 中(WebKit 不會(huì)丟棄 header 字段);
  2. 通過-[WKWebView loadRequest:]加載新的 post 請(qǐng)求 request2;
  3. 通過 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注冊(cè) scheme: post://;
  4. 注冊(cè) NSURLProtocol 攔截請(qǐng)求post://h5.qzone.qq.com/mqzone/index ,替換請(qǐng)求 scheme, 生成新的請(qǐng)求 request3: http://h5.qzone.qq.com/mqzone/index鸽捻,將 request2 header的body 字段復(fù)制到 request3 的 body 中,并使用 NSURLConnection 加載 request3泽腮,最后通過 NSURLProtocolClient 將加載結(jié)果返回 WKWebView;

5. WKWebView 頁面樣式問題

在 WKWebView 適配過程中御蒲,我們發(fā)現(xiàn)部分H5頁面元素位置向下偏移被拉伸變形,追蹤后發(fā)現(xiàn)主要是H5頁面高度值異常導(dǎo)致:

(1)

空間H5頁面有透明導(dǎo)航诊赊、透明導(dǎo)航下拉刷新厚满、全屏等需求,因此之前 webView 整個(gè)是從(0, 0)開始布局碧磅,通過調(diào)整webView.scrollView.contentInset 來適配特殊導(dǎo)航欄需求碘箍。而在 WKWebView 上對(duì) contentInset 的調(diào)整會(huì)反饋到webView.scrollView.contentSize.height的變化上遵馆,比如設(shè)置 webView.scrollView.contentInset.top = a,那么contentSize.height的值會(huì)增加a,導(dǎo)致H5頁面長(zhǎng)度增加丰榴,頁面元素位置向下偏移团搞;

解決方案是:調(diào)整WKWebView布局方式,避免調(diào)整webView.scrollView.contentInset多艇。實(shí)際上逻恐,即便在 UIWebView 上也不建議直接調(diào)整webView.scrollView.contentInset的值,這確實(shí)會(huì)帶來一些奇怪的問題峻黍。如果某些特殊情況下非得調(diào)整 contentInset 不可的話复隆,可以通過下面方式讓H5頁面恢復(fù)正常顯示:

/**設(shè)置contentInset值后通過調(diào)整webView.frame讓頁面恢復(fù)正常顯示 
 *參考:http://km.oa.com/articles/show/277372
 */ webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); 
webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);

(2)

在接入 now 直播的時(shí)候,我們發(fā)現(xiàn)在 iOS 9 上 WKWebView 會(huì)出現(xiàn)頁面被拉伸變形的情況姆涩,最后發(fā)現(xiàn)是window.innerHeight值不準(zhǔn)確導(dǎo)致(在WKWebView上返回了一個(gè)非常大的值)挽拂,而H5同學(xué)通過獲取window.innerHeight來設(shè)置頁面高度,導(dǎo)致頁面整體被拉伸骨饿。通過查閱相關(guān)資料發(fā)現(xiàn)亏栈,這個(gè)bug只在 iOS 9 的幾個(gè)系統(tǒng)版本上出現(xiàn),蘋果后來fix了這個(gè)bug宏赘。我們最后的解決方案是:延遲調(diào)用window.innerHeight

setTimeout(function(){height = window.innerHeight},0);

or

Use shrink-to-fit meta-tag 
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">

6. WKWebView 截屏問題

空間玩吧H5小游戲有截屏分享的功能绒北,WKWebView 下通過 -[CALayer renderInContext:]實(shí)現(xiàn)截屏的方式失效,需要通過以下方式實(shí)現(xiàn)截屏功能:

@implementation UIView (ImageSnapshot) 
- (UIImage*)imageSnapshot { 
    UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor); 
    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES]; 
    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext(); 
    UIGraphicsEndImageContext(); 
    return newImage; 
} 
@end

然而這種方式依然解決不了 webGL 頁面的截屏問題察署,筆者已經(jīng)翻遍蘋果文檔闷游,研究過 webKit2 源碼里的截屏私有API,依然沒有找到合適的解決方案贴汪,同時(shí)發(fā)現(xiàn) Safari 以及 Chrome 這兩個(gè)全量切換到 WKWebView 的瀏覽器也存在同樣的問題:對(duì)webGL 頁面的截屏結(jié)果不是空白就是純黑圖片脐往。無奈之下,我們只能約定一個(gè)JS接口扳埂,讓游戲開發(fā)商實(shí)現(xiàn)該接口业簿,具體是通過 canvas getImageData()方法取得圖片數(shù)據(jù)后返回 base64 格式的數(shù)據(jù),客戶端在需要截圖的時(shí)候阳懂,調(diào)用這個(gè)JS接口獲取 base64 String 并轉(zhuǎn)換成 UIImage梅尤。

7. WKWebView crash問題

WKWebView 放量后,外網(wǎng)新增了一些 crash, 其中一類 crash 的主要堆棧如下:

... 
28 UIKit 0x0000000190513360 UIApplicationMain + 208 29 Qzone 0x0000000101380570 main (main.m:181) 
30 libdyld.dylib 0x00000001895205b8 _dyld_process_info_notify_release + 36 Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called

主要是JS調(diào)用window.alert()函數(shù)引起的希太,從 crash 堆椏巳模可以看出是 WKWebView 回調(diào)函數(shù):

+ (void) presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnull void (^)())completionHandler;

completionHandler 沒有被調(diào)用導(dǎo)致的。在適配 WKWebView 的時(shí)候誊辉,我們需要自己實(shí)現(xiàn)該回調(diào)函數(shù),window.alert()才能調(diào)起 alert 框亡脑,我們最初的實(shí)現(xiàn)是這樣的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
{ 
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
    [alertController addAction:[UIAlertAction actionWithTitle:@"確認(rèn)" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
    [self presentViewController:alertController animated:YES completion:^{}]; 
}

如果 WKWebView 退出的時(shí)候堕澄,JS剛好執(zhí)行了window.alert(), alert 框可能彈不出來邀跃,completionHandler 最后沒有被執(zhí)行,導(dǎo)致 crash蛙紫;另一種情況是在 WKWebView 一打開拍屑,JS就執(zhí)行window.alert(),這個(gè)時(shí)候由于 WKWebView 所在的 UIViewController 出現(xiàn)(push或present)的動(dòng)畫尚未結(jié)束坑傅,alert 框可能彈不出來僵驰,completionHandler 最后沒有被執(zhí)行,導(dǎo)致 crash唁毒。我們最終的實(shí)現(xiàn)大致是這樣的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
{ 
    if (/*UIViewController of WKWebView has finish push or present animation*/) { 
        completionHandler(); 
        return; 
    } 
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
    [alertController addAction:[UIAlertAction actionWithTitle:@"確認(rèn)" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
    if (/*UIViewController of WKWebView is visible*/) 
        [self presentViewController:alertController animated:YES completion:^{}]; 
    else 
        completionHandler(); 
}

確保上面兩種情況下 completionHandler 都能被執(zhí)行蒜茴,消除了 WKWebView 下彈 alert 框的 crash,WKWebView 下彈 confirm 框的 crash 的原因與解決方式與 alert 類似浆西。

另一個(gè) crash 發(fā)生在 WKWebView 退出前調(diào)用:

 -[WKWebView evaluateJavaScript: completionHandler:]

執(zhí)行JS代碼的情況下粉私。WKWebView 退出并被釋放后導(dǎo)致completionHandler變成野指針,而此時(shí) javaScript Core 還在執(zhí)行JS代碼近零,待 javaScript Core 執(zhí)行完畢后會(huì)調(diào)用completionHandler()诺核,導(dǎo)致 crash。這個(gè) crash 只發(fā)生在 iOS 8 系統(tǒng)上久信,參考Apple Open Source窖杀,在iOS9及以后系統(tǒng)蘋果已經(jīng)修復(fù)了這個(gè)bug,主要是對(duì)completionHandler block做了copy(refer: https://trac.webkit.org/changeset/179160)裙士;對(duì)于iOS 8系統(tǒng)陈瘦,可以通過在 completionHandler 里 retain WKWebView 防止 completionHandler 被過早釋放。我們最后用 methodSwizzle hook 了這個(gè)系統(tǒng)方法:

+ (void) load 
{ 
     [self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil]; 
} 
/* 
 * fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation 
 */ - (void)altEvaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler 
{ 
    id strongSelf = self; 
    [self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e) { 
        [strongSelf title]; 
        if (completionHandler) { 
            completionHandler(r, e); 
        } 
    }]; 
}

8. 其它問題

(1). 視頻自動(dòng)播放

WKWebView 需要通過WKWebViewConfiguration.mediaPlaybackRequiresUserAction設(shè)置是否允許自動(dòng)播放潮售,但一定要在 WKWebView 初始化之前設(shè)置痊项,在 WKWebView 初始化之后設(shè)置無效。

(2). goBack API問題

WKWebView 上調(diào)用 -[WKWebView goBack], 回退到上一個(gè)頁面后不會(huì)觸發(fā)window.onload()函數(shù)酥诽、不會(huì)執(zhí)行JS鞍泉。

(3). 頁面滾動(dòng)速率

WKWebView 需要通過scrollView delegate調(diào)整滾動(dòng)速率:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
     scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肮帐,隨后出現(xiàn)的幾起案子咖驮,更是在濱河造成了極大的恐慌,老刑警劉巖训枢,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件托修,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡恒界,警方通過查閱死者的電腦和手機(jī)睦刃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來十酣,“玉大人涩拙,你說我怎么就攤上這事际长。” “怎么了兴泥?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵工育,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我搓彻,道長(zhǎng)如绸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任旭贬,我火速辦了婚禮怔接,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘骑篙。我一直安慰自己蜕提,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布靶端。 她就那樣靜靜地躺著谎势,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杨名。 梳的紋絲不亂的頭發(fā)上脏榆,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音台谍,去河邊找鬼须喂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛趁蕊,可吹牛的內(nèi)容都是我干的坞生。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼掷伙,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼是己!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起任柜,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤卒废,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后宙地,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摔认,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年宅粥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了参袱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蓖柔,靈堂內(nèi)的尸體忽然破棺而出辰企,到底是詐尸還是另有隱情风纠,我是刑警寧澤况鸣,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站竹观,受9級(jí)特大地震影響镐捧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜臭增,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一懂酱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧誊抛,春花似錦列牺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至随夸,卻和暖如春九默,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宾毒。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工驼修, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诈铛。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓乙各,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親幢竹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耳峦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348