前言
對於大多開發者來說,蘋果WWDC2018大會關注比較多的是iOS 12、新的ARKit、新的CoreML,其實還有一個更改在session上沒有具體提及,但對於開發者來說影響挺大,如下圖:
雖然只是加個小小的“Deprecated”標籤,但可以看出蘋果已經放棄對UIWebView這個元件的維護,希望開發者全量地切換到WKWebView這個元件上面。雖然WKWebView已經在iOS8已經推出,相比UIWebView擁有更低的記憶體消耗和更快的JavaScript引擎,但是我們的蘋果大佬給我們開發者埋了太多坑了,按道理來說WKWebView應該更快更6,可是開發者用起來苦不堪言,所以市面上還是很多開發者還是沒有放棄UIWebView,因為它用起來比較穩定,想了解具體有哪些大坑可以看騰訊bugly寫的這篇文章《WKWebView 那些坑》,既然蘋果爸爸要放棄UIWebView,開發者只能在適配的時候共勉之。對於專案的影響
有些專案適配起來相對比較簡單,因為不涉及複雜的互動邏輯,只需要替換底層的Web容器,改下呼叫的API就行了,但是在適配一個較為複雜的跨端互動專案時就比較gg了,因為WKWebView不使用JavaScriptCore相關的API,而是使用自己的一套機制進行JS與原生橋接,而且JS呼叫方式與JavaScriptCore完全不同,如果原生只是簡單地替換下容器和改下互動的API,那我們的h5同學又得加班加點了,上層JS API在新的容器是無法呼叫。
進化吧,XDMicroJSBridge
為了h5同學完美過渡,無需改動任何橋接程式碼,只需關注自己的業務迭代,我們的橋接框架需要來一次大升級,程式碼“WK”。因為不能使用JavaScriptCore,所以XDMicroJSBridge底層橋接框架得重新設計,這次我們使用WKWebView的WKScriptMessageHandler進行橋接層的設計。
New API ?
我們先看下新的框架介面層的設計:
看起來API層是不是沒什麼改變,除了要保證我們的h5能完美過渡,我們也要保證我們的原生端接入新框架的時候也能完美過渡,這樣原生這邊也不用改橋接程式碼。容器配置
唯一的改變就是容器這一塊的API互動,以前是使用方傳進容器,現在是我們裡面提供配置好的容器給外部,降低原生同學使用的複雜度,確實WKWebView這個容器相比UIWebView這個容器更加靈活,各種各樣的配置項。
- (WKWebView *)getBridgeWebView {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 設定偏好設定
config.preferences = [[WKPreferences alloc] init];
// 預設為0
config.preferences.minimumFontSize = 10;
// 預設認為YES
config.preferences.javaScriptEnabled = YES;
// 在iOS上預設為NO,表示不能自動通過視窗開啟
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
config.processPool = [[WKProcessPool alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
//解決self 迴圈引用問題
XDWKWeakScriptMessageDelegate *weakSelf = [[XDWKWeakScriptMessageDelegate alloc] initWithDelegate:self];
[config.userContentController addScriptMessageHandler:weakSelf name:@"XDWKJB"];
WKUserScript *injectScript = [[WKUserScript alloc] initWithSource:injectJS injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[config.userContentController addUserScript:injectScript];
_webview = [[WKWebView alloc] initWithFrame:CGRectNull configuration:config];
return _webview;
}
複製程式碼
這裡有個注意點就是WKWebView在註冊JS的訊息回撥這一塊需要弄個額外的物件進行回撥註冊,如果直接用self會造成迴圈引用導致記憶體洩露,經過實驗寫成weakSelf也不管用。
WKWebView有個人性化的設計就是能夠原生化的進行JS預載入,而且能夠指定載入時機和位置,相比之下,以前在UIWebView進行JS預載入一點都不pure。
橋接層設計
WKWebView提供WKScriptMessageHandler進行JS層的訊息捕獲,例如註冊一個名為XDWKJB的橋接物件,JS層那邊通過window.webkit.messageHandlers.XDWKJB.postMessage(e)
這個方法將訊息e傳送到原生這邊,原生這邊在WKScriptMessageHandler的回撥方法- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message
進行訊息的捕獲和解析。
這次的設計需要引入一定JS程式碼,因為WKWebView提供的JS互動只支援簡單的資料傳輸,當存在callback的時候,當前的互動並不能將js的callback完美轉化。所以需要設計一個js的callback管理器用於儲存從js的callback,在js層先賦予callback一個id並對映起來,因為id可以設計為簡單的資料格式,如字串,所以原生這邊很容易捕獲和解析,當原生回撥資料時通過callback管理器獲取對應id的callback,執行對應callback的js程式碼就可以實現橋接回撥,程式碼例項如下:
var XDMCBridge = {};var xd_jscallback_center={_callbackbuf:{},addCallback:function(a,c){\"function\"==typeof c&&(this._callbackbuf[a]=c)},fireCallback:function(a,c){if(\"string\"==typeof a){var f=this._callbackbuf[a];\"function\"==typeof f&&(void 0===c?f():f(c))}}};
複製程式碼
h5完美過渡方案
因為上個版本的XDMicroJSBridge提供的JS API是類微信web API風格,我們這次繼續沿用這種程式碼風格,為了適配這種風格,所以這次的框架需要提供一些模板JS程式碼來輔助JS方法註冊和注入,模板例項如下:
%@.%@=function(){var a=arguments.length,e={methodName:\"%@\"},l=Array.from(arguments);a>0&&(\"function\"==typeof l[a-1]?(e.callbackId=\"%@\",e.params=a-1>0?l.slice(0,a-1):[]):e.params=l),null!=e.callbackId&&xd_jscallback_center.addCallback(e.callbackId,l[a-1]),window.webkit.messageHandlers.XDWKJB.postMessage(e)};
複製程式碼
%@是佔位符,用於替換註冊的JS介面的名稱空間名和方法名
最終效果
XDMicroJSBridge擴充出XDMicroJSBridge_WK,用於適配WKWebView,同樣支援名稱空間,原生專注原生程式碼,web專注JavaScript
初始化Bridge
#import "XDMicroJSBridge_WK.h"
@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic, strong) XDMicroJSBridge_WK *bridge;
@property (nonatomic, copy) XDMCJSBCallback callback;
self.bridge = [[XDMicroJSBridge_WK alloc] init];
複製程式碼
註冊JS方法
跟上個版本註冊方式一模一樣
__weak typeof(self) weakself = self;
[_bridge registerAction:@"camerapicker" handler:^(NSArray *params, XDMCJSBCallback callback) {
dispatch_async(dispatch_get_main_queue(), ^{
//if your javaScript method has callback, you should register this call like this.
if (callback) {
weakself.callback = callback;
}
UIImagePickerController *cameraVC = [[UIImagePickerController alloc] init];
cameraVC.delegate = weakself;
cameraVC.sourceType = UIImagePickerControllerSourceTypeCamera;
[weakself presentViewController:cameraVC animated:YES completion:nil];
});
}];
複製程式碼
h5呼叫原生註冊的JS方法
也跟上個版本呼叫一模一樣,h5同學會不會很開心?,不用改程式碼
<script>
function clickcamera() {
XDMCBridge.camerapicker(function (response) {
var photos = response['photos'];
var insert = document.getElementById('insert');
for(var i = 0; i < photos.length; i++) {
var img = new Image(100,100);
img.src = photos[i];
insert.appendChild(img);
}
});
}
</script>
複製程式碼