前言
Web 頁面中的 JS 與 iOS Native 如何互動是每個 iOS 猿必須掌握的技能。而說到 Native 與 JS 互動,就不得不提一嘴 Hybrid。
Hybrid 的翻譯結果並不是很文明(擦汗,不知道為啥很多翻譯軟體會譯為“雜種”,但我更喜歡將它翻譯為“混合、混血”),Hybrid Mobile App 我對它的理解為通過 Web 網路技術(如 HTML,CSS 和 JavaScript)與 Native 相結合的混合移動應用程式。
那麼我們來看一下 Hybrid 對比 Native 有哪些優劣:
因為 Hybrid 的靈活性(更改 Web 頁面不必重新發版)以及通用性(一份 H5 玩遍所有平臺)再加上門檻低(前端猿可以無痛上手開擼)的優勢,所以在非核心功能模組使用 Web 通過 Hybrid 的方式來實現可能從各方面都會優於 Native。而 Native 則可以在核心功能和裝置硬體的呼叫上為 JS 提供強有力的支援。
索引
- Hybrid 的發展簡史
- JavaScriptCore 簡介
- iOS Native 與 JS 互動的方法
- WKWebView 與 JS 互動的特有方法
- JS 通過 Native 呼叫 iOS 裝置攝像頭的 Demo
- 總結
Hybrid 的發展簡史
下面簡述一下 Hybrid 的發展史:
1.H5 釋出
Html5 是在 2014 年 9 月份正式釋出的,這一次的釋出做了一個最大的改變就是“從以前的 XML 子集升級成為一個獨立集合”。
2.H5 滲入 Mobile App 開發
Native APP 開發中有一個 webview 的元件(Android 中是 webview,iOS 有 UIWebview和 WKWebview),這個元件可以載入 Html 檔案。
在 H5 大行其道之前,webview 載入的 web 頁面很單調(因為只能載入一些靜態資源),自從 H5 火了之後,前端猿們開發的 H5 頁面在 webview 中的表現不俗使得 H5 開發慢慢滲透到了 Mobile App 開發中來。
3.Hybrid 現狀
雖然目前已經出現了 RN 和 Weex 這些使用 JS 寫 Native App 的技術,但是 Hybrid 仍然沒有被淘汰,市面上大多數應用都不同程度的引入了 Web 頁面。
JavaScriptCore
JavaScriptCore 這個庫是 Apple 在 iOS 7 之後加入到標準庫的,它對 iOS Native 與 JS 做互動呼叫產生了劃時代的影響。
JavaScriptCore 大體是由 4 個類以及 1 個協議組成的:
- JSContext 是 JS 執行上下文,你可以把它理解為 JS 執行的環境。
- JSValue 是對 JavaScript 值的引用,任何 JS 中的值都可以被包裝為一個 JSValue。
- JSManagedValue 是對 JSValue 的包裝,加入了“conditional retain”。
- JSVirtualMachine 表示 JavaScript 執行的獨立環境。
還有 JSExport 協議:
實現將 Objective-C 類及其例項方法,類方法和屬性匯出為 JavaScript 程式碼的協議。
這裡的 JSContext,JSValue,JSManagedValue 相對比較好理解,下面我們把 JSVirtualMachine 單拎出來說明一下:
JSVirtualMachine 的用法和其與 JSContext 的關係
官方文件的介紹:
JSVirtualMachine 例項表示用於 JavaScript 執行的獨立環境。 您使用此類有兩個主要目的:支援併發 JavaScript 執行,並管理 JavaScript 和 Objective-C 或 Swift 之間橋接的物件的記憶體。
關於 JSVirtualMachine 的使用,一般情況下我們不用手動去建立 JSVirtualMachine。因為當我們獲取 JSContext 時,獲取到的 JSContext 從屬於一個 JSVirtualMachine。
每個 JavaScript 上下文(JSContext 物件)都屬於一個 JSVirtualMachine。 每個 JSVirtualMachine 可以包含多個上下文,允許在上下文之間傳遞值(JSValue 物件)。 但是,每個 JSVirtualMachine 是不同的,即我們不能將一個 JSVirtualMachine 中建立的值傳遞到另一個 JSVirtualMachine 中的上下文。
JavaScriptCore API 是執行緒安全的 —— 例如,我們可以從任何執行緒建立 JSValue 物件或執行 JS 指令碼 - 但是,嘗試使用相同 JSVirtualMachine 的所有其他執行緒將被阻塞。 要在多個執行緒上同時(併發)執行 JavaScript 指令碼,請為每個執行緒使用單獨的 JSVirtualMachine 例項。
JSValue 與 JavaScript 的轉換表
OBJECTIVE-C | JAVASCRIPT | JSVALUE CONVERT | JSVALUE CONSTRUCTOR |
---|---|---|---|
nil | undefined | valueWithUndefinedInContext | |
NSNull | null | valueWithNullInContext: | |
NSString | string | toString | |
NSNumber | number, boolean | toNumber toBool toDouble toInt32 toUInt32 |
valueWithBool:inContext: valueWithDouble:inContext: valueWithInt32:inContext: valueWithUInt32:inContext: |
NSDictionary | Object object | toDictionary | valueWithNewObjectInContext: |
NSArray | Array object | toArray | valueWithNewArrayInContext: |
NSDate | Date object | toDate | |
NSBlock | Function object | ||
id | Wrapper object | toObject toObjectOfClass: |
valueWithObject:inContext: |
Class | Constructor object |
iOS Native 與 JS 互動
對於 iOS Native 與 JS 互動我們先從呼叫方向上分為兩種情況來看:
- JS 呼叫 Native
- Native 呼叫 JS
JS 呼叫 Native
其實 JS 呼叫 iOS Native 也分為兩種實現方式:
- 假 Request 方法
- JavaScriptCore 方法
假 Request 方法
原理:其實這種方式就是利用了 webview 的代理方法,在 webview 開始請求的時候截獲請求,判斷請求是否為約定好的假請求。如果是假請求則表示是 JS 想要按照約定呼叫我們的 Native 方法,按照約定去執行我們的 Native 程式碼就好。
UIWebView
UIWebView 代理有用於截獲請求的函式,在裡面做判斷就好:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = request.URL;
// 與約定好的函式名作比較
if ([[url scheme] isEqualToString:@"your_func_name"]) {
// just do it
}
}
複製程式碼
WKWebView
WKWebView 有兩個代理,一個是 WKNavigationDelegate,另一個是 WKUIDelegate。WKUIDelegate 我們在下面的章節會講到,這裡我們需要設定並實現它的 WKNavigationDelegate 方法:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL;
// 與約定好的函式名作比較
if ([[url scheme] isEqualToString:@"your_func_name"]) {
// just do it
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
複製程式碼
Note:
decisionHandler
是當你的應用程式決定是允許還是取消導航時,要呼叫的程式碼塊。 該程式碼塊使用單個引數,它必須是列舉型別WKNavigationActionPolicy
的常量之一。如果不呼叫decisionHandler
會引起 crash。
這裡補充一下 JS 程式碼:
function callNative() {
loadURL("your_func_name://xxx");
}
複製程式碼
然後拿個 button 標籤用一下就好了:
<button type="button" onclick="callNative()">Call Native!</button>
複製程式碼
JavaScriptCore 方法
iOS 7 有了 JavaScriptCore 專門用來做 Native 與 JS 的互動。我們可以在 webview 完成載入之後獲取 JSContext,然後利用 JSContext 將 JS 中的物件引用過來用 Native 程式碼對其作出解釋或響應:
// 首先引入 JavaScriptCore 庫
#import <JavaScriptCore/JavaScriptCore.h>
// 然後再 UIWebView 的完成載入的代理方法中
- (void)webViewDidFinishLoad:(UIWebView *)webView {
// 獲取 JS 上下文
jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 做引用,將 JS 內的元素引用過來解釋,比如方法可以解釋成 Block,物件也可以指向 OC 的 Native 物件哦
jsContext[@"iosDelegate"] = self;
jsContext[@"yourFuncName"] = ^(id parameter){
// 注意這裡的執行緒預設是 web 處理的執行緒,如果涉及主執行緒操作需要手動轉到主執行緒
dispatch_async(dispatch_get_main_queue(), ^{
// your code
});
}
}
複製程式碼
而 JS 這邊程式碼更簡單了,乾脆宣告一個不解釋的函式(約定好名字的),用於給 Native 做引用:
var parameter = xxx;
yourFuncName(parameter);
複製程式碼
iOS Native 呼叫 JS
iOS Native 呼叫 JS 的實現方法也被 JavaScriptCore 劃分開來:
- webview 直接注入 JS 並執行
- JavaScriptCore 方法
webview 直接注入 JS 並執行
在 iOS 平臺,webview 有注入並執行 JS 的 API。
UIWebView
UIWebView 有直接注入 JS 的方法:
NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')", @"alert msg"];
[_webView stringByEvaluatingJavaScriptFromString:jsStr];
複製程式碼
Note: 這個方法會返回執行 JS 的結果(
nullable NSString *
),它是一個同步方法,會阻塞當前執行緒!儘管此方法不被棄用,但最佳做法是使用WKWebView
類的evaluateJavaScript:completionHandler:method
。官方文件: The stringByEvaluatingJavaScriptFromString: method waits synchronously for JavaScript evaluation to complete. If you load web content whose JavaScript code you have not vetted, invoking this method could hang your app. Best practice is to adopt the WKWebView class and use its evaluateJavaScript:completionHandler: method instead.
WKWebView
不同於 UIWebView,WKWebView 注入並執行 JS 的方法不會阻塞當前執行緒。因為考慮到 webview 載入的 web content 內 JS 程式碼不一定經過驗證,如果阻塞執行緒可能會掛起 App。
NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')", @"北京市東城區南鑼鼓巷納福衚衕xx號"];
[_webview evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@----%@", result, error);
}];
複製程式碼
Note: 方法不會阻塞執行緒,而且它的回撥程式碼塊總是在主執行緒中執行。
官方文件: Evaluates a JavaScript string. The method sends the result of the script evaluation (or an error) to the completion handler. The completion handler always runs on the main thread.
JavaScriptCore 方法
上面簡單提到過 JavaScriptCore 庫提供的 JSValue 類,這裡再提供一下官方文件對 JSValue 的介紹翻譯:
JSValue 例項是對 JavaScript 值的引用。 您可以使用 JSValue 類來轉換 JavaScript 和 Objective-C 或 Swift 之間的基本值(如數字和字串),以便在本機程式碼和 JavaScript 程式碼之間傳遞資料。
不過你也看到了我貼在上面的 OC 和 JS 資料型別轉換表,那裡面根本沒有限定為官方文件所說的基本值。如果你不熟悉 JS 的話,我這裡解釋一下為什麼 JSValue 也可以指向 JS 中的物件和函式,因為 JS 語言不區分基本值和物件以及函式,在 JS 中“萬物皆為物件”。
好了下面直接 show code:
// 首先引入 JavaScriptCore 庫
#import <JavaScriptCore/JavaScriptCore.h>
// 先獲取 JS 上下文
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 如果涉及 UI 操作,切回主執行緒呼叫 JS 程式碼中的 YourFuncName,通過陣列@[parameter] 入參
dispatch_async(dispatch_get_main_queue(), ^{
JSValue *jsValue = self.jsContext[@"YourFuncName"];
[jsValue callWithArguments:@[parameter]];
});
複製程式碼
上面的程式碼呼叫了 JS 程式碼中 YourFuncName 函式,並且給函式加了 @[parameter] 作為入參。為了方便閱讀理解,這裡再貼一下 JS 程式碼:
function YourFuncName(arguments){
var result = arguments;
// do what u want to do
}
複製程式碼
WKWebView 與 JS 互動的特有方法
關於 WKWebView 與 UIWebView 的區別就不在本文加以詳細說明了,更多資訊還請自行查閱。這裡要講的是 WKWebView 在與 JS 的互動時特有的方法:
- WKUIDelegate 方法
- MessageHandler 方法
WKUIDelegate 方法
對於 WKWebView 上文提到過,除了 WKNavigationDelegate,它還有一個 WKUIDelegate,這個 WKUIDelegate 是做什麼用的呢?
WKUIDelegate 協議包含一些函式用來監聽 web JS 想要顯示 alert 或 confirm 時觸發。我們如果在 WKWebView 中載入一個 web 並且想要 web JS 的 alert 或 confirm 正常彈出,就需要實現對應的代理方法。
Note: 如果沒有實現對應的代理方法,則 webview 將會按照預設操作去做出行為。
- Alert: If you do not implement this method, the web view will behave as if the user selected the OK button.
- Confirm: If you do not implement this method, the web view will behave as if the user selected the Cancel button.
我們這裡就拿 alert 舉例,相信各位讀者可以自己舉一反三。下面是在 WKUIDelegate 監聽 web 要顯示 alert 的代理方法中用 Native UIAlertController 替代 JS 中的 alert 顯示的栗子 :
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
// 用 Native 的 UIAlertController 彈窗顯示 JS 將要提示的資訊
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
// 函式內必須呼叫 completionHandler
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
複製程式碼
MessageHandler 方法
MessageHandler 是繼 Native 截獲 JS 假請求後另一種 JS 呼叫 Native 的方法,該方法利用了 WKWebView 的新特性實現。對比截獲假 Request 的方法來說,MessageHandler 傳引數更加簡單方便。
MessageHandler 指什麼?
WKUserContentController 類有一個方法:
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
複製程式碼
該方法用來新增一個指令碼處理器,可以在處理器內對 JS 指令碼呼叫的方法做出處理,從而達到 JS 呼叫 Native 的目的。
那麼 WKUserContentController 類和 WKWebView 有毛關係呢?
在 WKWebView 的初始化函式中有一個入參 configuration,它的型別是 WKWebViewConfiguration。WKWebViewConfiguration 中包含一個屬性 userContentController,這個 userContentController 就是 WKUserContentController 型別的例項,我們可以用這個 userContentController 來新增不同名稱的指令碼處理器。
MessageHandler 的坑
那麼回到 - (void)addScriptMessageHandler:name:
方法上面,該方法新增一個指令碼訊息處理器(第一個入參 scriptMessageHandler),並且給這個處理器起一個名字(第二個入參 name)。不過這個函式在使用的時候有個坑:scriptMessageHandler 入參會被強引用,那麼如果你把當前 WKWebView 所在的 UIViewController 作為第一個入參,這個 viewController 被他自己所持有的 webview.configuration. userContentController
所持有,就會造成迴圈引用。
我們可以通過 - (void)removeScriptMessageHandlerForName:
方法刪掉 userContentController 對 viewController 的強引用。所以一般情況下我們的程式碼會在 viewWillAppear
和 viewWillDisappear
成對兒的新增和刪除 MessageHandler:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.webview.configuration.userContentController addScriptMessageHandler:self name:@"YourFuncName"];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.webview.configuration.userContentController removeScriptMessageHandlerForName:@"YourFuncName"];
}
複製程式碼
WKScriptMessageHandler 協議
WKScriptMessageHandler 是指令碼資訊處理器協議,如果想讓一個物件具有指令碼資訊處理能力(比如上文中 webview 的所屬 viewController 也就是上面程式碼的 self)就必須使其遵循該協議。
WKScriptMessageHandler 協議內部非常簡單,只有一個方法,我們必須要實現該方法(@required):
// WKScriptMessageHandler 協議方法,在接收到指令碼資訊時觸發
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// message 有兩個屬性:name 和 body
// message.name 可以用於區別要做的處理
if ([message.name isEqualToString:@"YourFuncName"]) {
// message.body 相當於 JS 傳遞過來的引數
NSLog(@"JS call native success %@", message.body);
}
}
複製程式碼
補充 JS 的程式碼:
// <name> 換 YourFuncName,<messageBody> 換你要的入參即可
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
複製程式碼
搞定收工!
JS 通過 Native 呼叫 iOS 裝置攝像頭的 Demo
徒手擼了一個 Demo,實現了 JS 與 Native 程式碼的互動,達到用 JS 在 webview 內呼叫 iOS 裝置攝像頭的功能。Demo 內含許可權申請,使用者拒絕授權等細節(技術上就是 JS 和 Native 相互傳值呼叫),還請各位大佬指教。
向各位基佬低頭,獻上我的膝蓋~(Demo 地址)
總結
- 這篇文章簡單的介紹了一下 Hybrid Mobile App(其中還包括 Hybrid 的發展簡史)。
- 介紹了 JavaScriptCore 的組成,並且把 JSVirtualMachine 與 JSContext 和 JSValue 之間的關係用圖片的形式表述出來(JSVirtualMachine 包含 JSContext 包含 JSValue,都是 1 對 n 的關係,且由於同一個 JSVirtualMachine 下的程式碼會相互阻塞,所以如果想非同步執行互動需要在不同的執行緒宣告 JSVirtualMachine 併發執行)。
- 從呼叫方向的角度把 JS 與 iOS Native 相互呼叫的方式方法分別用程式碼示例講解了一遍。
- 介紹了 WKWebView 與 JS 互動特有的方法:WKUIDelegate 和 MessageHandler。
- 提供了一個 JS 通過 Native 呼叫 iOS 裝置攝像頭的 Demo。
完結撒花,希望我的文章可以為你帶來價值!
補充~ 我建了一個技術交流微信群,想在裡面認識更多的朋友!如果各位同學對文章有什麼疑問或者工作之中遇到一些小問題都可以在群裡找到我或者其他群友交流討論,期待你的加入喲~
Emmmmm..由於微信群人數過百導致不可以掃碼入群,所以請掃描上面的二維碼關注公眾號進群。