iOS Native與JavaScript交互
說到 Native 與 JS 做交互,就不得不提一嘴 HyBird Mobile App。
Hybird 的翻譯結果并不是很文明(擦汗,不知道為啥很多翻譯軟件會譯為“雜種”,但我更喜歡將它翻譯為“混合、混血”),Hybird App 我對它的理解為通過 Web 網絡技術(如 HTML,CSS 和 JavaScript)與 Native 相結合的混合移動應用程序。
那么我們來看一下 Hybird 對比 Native 有哪些優劣:
因為 Hybird 的靈活性(更改 Web 頁面內的 JS 就可以直接生效不必重新發版)以及通用性(一份 H5 玩遍公眾號、Android、iOS)再加上門檻低(前端玩家過來可以無痛上手開擼)的優勢,所以在非核心功能模塊使用 Web 通過 Hybird 的方式實現可能從各方面都會稍微好一些。而 Native 則可以在核心功能和設備硬件的調用上為 JS 提供強有力的支持。
雖然現在很多技術如 RN 和 Weex 等通過 JS 直接寫 Native 的技術出現,對 Hybird App 沖擊很大。但是由于 Hybird 技術門檻低(幾乎無學習成本)等原因,國內很多公司包括大廠依舊沒有完全拋棄它,畢竟合適的才是***的!
Hybird 的發展史
H5 發布
Html5 是在 2014 年 9 月份正式發布的,這一次的發布做了一個***的改變就是“從以前的 XML 子集升級成為一個獨立集合”。
H5 滲入 Mobile App 開發
Native APP 開發中有一個 webview 的組件(Android 中是 webview,iOS 有 UIWebview和 WKWebview),這個組件可以加載 Html 文件。
在 H5 大行其道之前,webview 加載的 web 頁面單調(因為只能加載一些靜態資源)??勺源?H5 火了之后,在很多仿生框架的幫助下,前端玩家們開發的 H5 頁面在 webview 中的體驗不俗使得 H5 開發慢慢滲透到了 Mobile App 開發中來。
Hybird 的現狀
雖然目前已經出現了 RN 和 Weex 這些 JS 寫 Native 的技術,但是 Hybird 依舊沒有被淘汰。市面上絕大多說應用都不同程度的引用了 Web 頁面,Web 頁面中的 JS 與 Native 如何交互依然是每個 iOS 猿必須掌握的技能。
JavaScriptCore
不好意思,在進入正題之前,容我再 BB 一下。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 的轉換表
iOS 與 JS 交互
對于 iOS Native 與 JS 做交互我們先從調用方向上分為兩種情況來看:
- JS 調用 iOS Native
- iOS Native 調用 JS
JS 調用 iOS 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。我們需要設置并實現它的 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>
Call Native!
其實這里有個實際的栗子來的,我之前寫過一篇文章,由于適配原因采取了假 Request 的方式也做到了解決問題。文章鏈接貼在這里 各位想看的可以移步去看一下哈。
JavaScriptCore 方法
iOS 7 有了 JavaScriptCore 專門用來做 iOS 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 )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>)
搞定收工!
總結
- 這篇文章簡單的介紹了一下 Hybird Mobile App(其中還包括 Hybird 的發展簡史)。
- 介紹了 JavaScriptCore 的組成,并且把很多文章沒有講清楚的 JSVirtualMachine 與 JSContext 和 JSValue 之間的關系用圖片的形式表述出來。(JSVirtualMachine 包含 JSContext 包含 JSValue,都是 1 對 n 的關系,且由于同一個 JSVirtualMachine 下的代碼會相互阻塞,所以如果想異步執行交互需要在不同的線程聲明 JSVirtualMachine 并發執行)
- 從調用方向的角度把 JS 與 iOS Native 相互調用的方式方法分別用代碼講解了一遍。
- ***補充了 WKWebView 與 JS 交互特有的方法:WKUIDelegate 和 MessageHandler。
如果您覺得我的文章表述錯誤請您予以指正,您的指正將會讓我避免誤導很多人。