Web 頁面中的 JS 與 iOS Native 如何交互是每一個 iOS 猿必須掌握的技能。而說到 Native 與 JS 交互,就不得不提一嘴 Hybrid。前端
Hybrid 的翻譯結果並非很文明(擦汗,不知道爲啥不少翻譯軟件會譯爲「雜種」,但我更喜歡將它翻譯爲「混合、混血」),Hybrid Mobile App 我對它的理解爲經過 Web 網絡技術(如 HTML,CSS 和 JavaScript)與 Native 相結合的混合移動應用程序。java
那麼咱們來看一下 Hybrid 對比 Native 有哪些優劣:ios
由於 Hybrid 的靈活性(更改 Web 頁面沒必要從新發版)以及通用性(一份 H5 玩遍全部平臺)再加上門檻低(前端猿能夠無痛上手開擼)的優點,因此在非核心功能模塊使用 Web 經過 Hybrid 的方式來實現可能從各方面都會優於 Native。而 Native 則能夠在覈心功能和設備硬件的調用上爲 JS 提供強有力的支持。git
下面簡述一下 Hybrid 的發展史:github
Html5 是在 2014 年 9 月份正式發佈的,這一次的發佈作了一個最大的改變就是「從之前的 XML 子集升級成爲一個獨立集合」。web
Native APP 開發中有一個 webview 的組件(Android 中是 webview,iOS 有 UIWebview和 WKWebview),這個組件能夠加載 Html 文件。數組
在 H5 大行其道以前,webview 加載的 web 頁面很單調(由於只能加載一些靜態資源),自從 H5 火了以後,前端猿們開發的 H5 頁面在 webview 中的表現不俗使得 H5 開發慢慢滲透到了 Mobile App 開發中來。安全
雖然目前已經出現了 RN 和 Weex 這些使用 JS 寫 Native App 的技術,可是 Hybrid 仍然沒有被淘汰,市面上大多數應用都不一樣程度的引入了 Web 頁面。bash
JavaScriptCore 這個庫是 Apple 在 iOS 7 以後加入到標準庫的,它對 iOS Native 與 JS 作交互調用產生了劃時代的影響。微信
JavaScriptCore 大致是由 4 個類以及 1 個協議組成的:
還有 JSExport 協議:
實現將 Objective-C 類及其實例方法,類方法和屬性導出爲 JavaScript 代碼的協議。
這裏的 JSContext,JSValue,JSManagedValue 相對比較好理解,下面咱們把 JSVirtualMachine 單拎出來講明一下:
官方文檔的介紹:
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 實例。
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 交互咱們先從調用方向上分爲兩種狀況來看:
其實 JS 調用 iOS Native 也分爲兩種實現方式:
原理:其實這種方式就是利用了 webview 的代理方法,在 webview 開始請求的時候截獲請求,判斷請求是否爲約定好的假請求。若是是假請求則表示是 JS 想要按照約定調用咱們的 Native 方法,按照約定去執行咱們的 Native 代碼就好。
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 有兩個代理,一個是 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>
複製代碼
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 的實現方法也被 JavaScriptCore 劃分開來:
在 iOS 平臺,webview 有注入並執行 JS 的 API。
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.
不一樣於 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 庫提供的 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 與 UIWebView 的區別就不在本文加以詳細說明了,更多信息還請自行查閱。這裏要講的是 WKWebView 在與 JS 的交互時特有的方法:
對於 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 是繼 Native 截獲 JS 假請求後另外一種 JS 調用 Native 的方法,該方法利用了 WKWebView 的新特性實現。對比截獲假 Request 的方法來講,MessageHandler 傳參數更加簡單方便。
WKUserContentController 類有一個方法:
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
複製代碼
該方法用來添加一個腳本處理器,能夠在處理器內對 JS 腳本調用的方法作出處理,從而達到 JS 調用 Native 的目的。
那麼 WKUserContentController 類和 WKWebView 有毛關係呢?
在 WKWebView 的初始化函數中有一個入參 configuration,它的類型是 WKWebViewConfiguration。WKWebViewConfiguration 中包含一個屬性 userContentController,這個 userContentController 就是 WKUserContentController 類型的實例,咱們能夠用這個 userContentController 來添加不一樣名稱的腳本處理器。
那麼回到 - (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 是腳本信息處理器協議,若是想讓一個對象具備腳本信息處理能力(好比上文中 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>) 複製代碼
搞定收工!
徒手擼了一個 Demo,實現了 JS 與 Native 代碼的交互,達到用 JS 在 webview 內調用 iOS 設備攝像頭的功能。Demo 內含權限申請,用戶拒絕受權等細節(技術上就是 JS 和 Native 相互傳值調用),還請各位大佬指教。
向各位基佬低頭,獻上個人膝蓋~(Demo 地址)
完結撒花,但願個人文章能夠爲你帶來價值!
補充~ 我建了一個技術交流微信羣,想在裏面認識更多的朋友!若是各位同窗對文章有什麼疑問或者工做之中遇到一些小問題均可以在羣裏找到我或者其餘羣友交流討論,期待你的加入喲~
Emmmmm..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的二維碼關注公衆號進羣。