iOS 與 JS 交互開發(fā)知識(shí)總結(jié)

前言

Web 頁面中的 JS 與 iOS Native 如何交互是每個(gè) iOS 猿必須掌握的技能。而說到 Native 與 JS 交互,就不得不提一嘴 Hybrid。

Hybrid 的翻譯結(jié)果并不是很文明(擦汗,不知道為啥很多翻譯軟件會(huì)譯為“雜種”,但我更喜歡將它翻譯為“混合、混血”),Hybrid Mobile App 我對(duì)它的理解為通過 Web 網(wǎng)絡(luò)技術(shù)(如 HTML,CSS 和 JavaScript)與 Native 相結(jié)合的混合移動(dòng)應(yīng)用程序。

那么我們來看一下 Hybrid 對(duì)比 Native 有哪些優(yōu)劣:

hybrid_vs_native.jpg

因?yàn)?Hybrid 的靈活性(更改 Web 頁面不必重新發(fā)版)以及通用性(一份 H5 玩遍所有平臺(tái))再加上門檻低(前端猿可以無痛上手開擼)的優(yōu)勢(shì),所以在非核心功能模塊使用 Web 通過Hybrid的方式來實(shí)現(xiàn)可能從各方面都會(huì)優(yōu)于 Native。而 Native 則可以在核心功能和設(shè)備硬件的調(diào)用上為 JS 提供強(qiáng)有力的支持。

索引

Hybrid的發(fā)展簡(jiǎn)史

JavaScriptCore 簡(jiǎn)介

iOS Native 與 JS 交互的方法

WKWebView 與 JS 交互的特有方法

JS 通過 Native 調(diào)用 iOS 設(shè)備攝像頭的 Demo

總結(jié)

Hybrid 的發(fā)展簡(jiǎn)史

下面簡(jiǎn)述一下 Hybrid 的發(fā)展史:

1.H5 發(fā)布

html5.png

Html5 是在 2014 年 9 月份正式發(fā)布的,這一次的發(fā)布做了一個(gè)最大的改變就是“從以前的 XML 子集升級(jí)成為一個(gè)獨(dú)立集合”。

2.H5 滲入 Mobile App 開發(fā)

Native APP 開發(fā)中有一個(gè) webview 的組件(Android 中是 webview,iOS 有 UIWebview和 WKWebview),這個(gè)組件可以加載 Html 文件。

在 H5 大行其道之前,webview 加載的 web 頁面很單調(diào)(因?yàn)橹荒芗虞d一些靜態(tài)資源),自從 H5 火了之后,前端猿們開發(fā)的 H5 頁面在 webview 中的表現(xiàn)不俗使得 H5 開發(fā)慢慢滲透到了 Mobile App 開發(fā)中來。

3.Hybrid 現(xiàn)狀

雖然目前已經(jīng)出現(xiàn)了 RN 和 Weex 這些使用 JS 寫 Native App 的技術(shù),但是Hybrid仍然沒有被淘汰,市面上大多數(shù)應(yīng)用都不同程度的引入了 Web 頁面。

JavaScriptCore

JavaScriptCore 這個(gè)庫是 Apple 在 iOS 7 之后加入到標(biāo)準(zhǔn)庫的,它對(duì) iOS Native 與 JS 做交互調(diào)用產(chǎn)生了劃時(shí)代的影響。

JavaScriptCore 大體是由 4 個(gè)類以及 1 個(gè)協(xié)議組成的:

javascriptcore_framework.jpg

JSContext 是 JS 執(zhí)行上下文,你可以把它理解為 JS 運(yùn)行的環(huán)境。

JSValue 是對(duì) JavaScript 值的引用,任何 JS 中的值都可以被包裝為一個(gè) JSValue。

JSManagedValue 是對(duì) JSValue 的包裝,加入了“conditional retain”。

JSVirtualMachine 表示 JavaScript 執(zhí)行的獨(dú)立環(huán)境。

還有 JSExport 協(xié)議:

實(shí)現(xiàn)將 Objective-C 類及其實(shí)例方法,類方法和屬性導(dǎo)出為 JavaScript 代碼的協(xié)議。

這里的 JSContext,JSValue,JSManagedValue 相對(duì)比較好理解,下面我們把 JSVirtualMachine 單拎出來說明一下:

JSVirtualMachine 的用法和其與 JSContext 的關(guān)系

jsvirtualmachine.jpg

官方文檔的介紹:

JSVirtualMachine 實(shí)例表示用于 JavaScript 執(zhí)行的獨(dú)立環(huán)境。 您使用此類有兩個(gè)主要目的:支持并發(fā) JavaScript 執(zhí)行,并管理 JavaScript 和 Objective-C 或 Swift 之間橋接的對(duì)象的內(nèi)存。

關(guān)于 JSVirtualMachine 的使用,一般情況下我們不用手動(dòng)去創(chuàng)建 JSVirtualMachine。因?yàn)楫?dāng)我們獲取 JSContext 時(shí),獲取到的 JSContext 從屬于一個(gè) JSVirtualMachine。

每個(gè) JavaScript 上下文(JSContext 對(duì)象)都屬于一個(gè) JSVirtualMachine。 每個(gè) JSVirtualMachine 可以包含多個(gè)上下文,允許在上下文之間傳遞值(JSValue 對(duì)象)。 但是,每個(gè) JSVirtualMachine 是不同的,即我們不能將一個(gè) JSVirtualMachine 中創(chuàng)建的值傳遞到另一個(gè) JSVirtualMachine 中的上下文。

JavaScriptCore API 是線程安全的 —— 例如,我們可以從任何線程創(chuàng)建 JSValue 對(duì)象或運(yùn)行 JS 腳本 - 但是,嘗試使用相同 JSVirtualMachine 的所有其他線程將被阻塞。 要在多個(gè)線程上同時(shí)(并發(fā))運(yùn)行 JavaScript 腳本,請(qǐng)為每個(gè)線程使用單獨(dú)的 JSVirtualMachine 實(shí)例。

JSValue 與 JavaScript 的轉(zhuǎn)換表

iOS Native 與 JS 交互

對(duì)于 iOS Native 與 JS 交互我們先從調(diào)用方向上分為兩種情況來看:

JS 調(diào)用 Native

Native 調(diào)用 JS

call-eachother.jpg

JS 調(diào)用 Native

其實(shí) JS 調(diào)用 iOS Native 也分為兩種實(shí)現(xiàn)方式:

假 Request 方法

JavaScriptCore 方法

假 Request 方法

原理:其實(shí)這種方式就是利用了 webview 的代理方法,在 webview 開始請(qǐng)求的時(shí)候截獲請(qǐng)求,判斷請(qǐng)求是否為約定好的假請(qǐng)求。如果是假請(qǐng)求則表示是 JS 想要按照約定調(diào)用我們的 Native 方法,按照約定去執(zhí)行我們的 Native 代碼就好。

UIWebView

UIWebView 代理有用于截獲請(qǐng)求的函數(shù),在里面做判斷就好:

1

2

3

4

5

6

7-?(BOOL)webView:(UIWebView?*)webView?shouldStartLoadWithRequest:(NSURLRequest?*)request?navigationType:(UIWebViewNavigationType)navigationType?{

NSURL?*url?=?request.URL;

//?與約定好的函數(shù)名作比較

if([[url?scheme]?isEqualToString:@"your_func_name"])?{

//?just?do?it

}

}

WKWebView

WKWebView 有兩個(gè)代理,一個(gè)是 WKNavigationDelegate,另一個(gè)是 WKUIDelegate。WKUIDelegate 我們?cè)谙旅娴恼鹿?jié)會(huì)講到,這里我們需要設(shè)置并實(shí)現(xiàn)它的 WKNavigationDelegate 方法:

1

2

3

4

5

6

7

8

9

10

11-?(void)webView:(WKWebView?*)webView?decidePolicyForNavigationAction:(WKNavigationAction?*)navigationAction?decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler?{

NSURL?*url?=?navigationAction.request.URL;

//?與約定好的函數(shù)名作比較

if([[url?scheme]?isEqualToString:@"your_func_name"])?{

//?just?do?it

decisionHandler(WKNavigationActionPolicyCancel);

return;

}

decisionHandler(WKNavigationActionPolicyAllow);

}

Note: decisionHandler 是當(dāng)你的應(yīng)用程序決定是允許還是取消導(dǎo)航時(shí),要調(diào)用的代碼塊。 該代碼塊使用單個(gè)參數(shù),它必須是枚舉類型 WKNavigationActionPolicy 的常量之一。如果不調(diào)用 decisionHandler 會(huì)引起 crash。

這里補(bǔ)充一下 JS 代碼:

1

2

3functioncallNative()?{

loadURL("your_func_name://xxx");

}

然后拿個(gè) button 標(biāo)簽用一下就好了:

1

Call?Native!

JavaScriptCore 方法

iOS 7 有了 JavaScriptCore 專門用來做 Native 與 JS 的交互。我們可以在 webview 完成加載之后獲取 JSContext,然后利用 JSContext 將 JS 中的對(duì)象引用過來用 Native 代碼對(duì)其作出解釋或響應(yīng):

1

2

3

4

5

6

7

8

9

10

11

12

13

14//?首先引入?JavaScriptCore?庫

#import//?然后再?UIWebView?的完成加載的代理方法中

-?(void)webViewDidFinishLoad:(UIWebView?*)webView?{

//?獲取?JS?上下文

jsContext?=?[webView?valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

//?做引用,將?JS?內(nèi)的元素引用過來解釋,比如方法可以解釋成?Block,對(duì)象也可以指向?OC?的?Native?對(duì)象哦

jsContext[@"iosDelegate"]?=?self;

jsContext[@"yourFuncName"]?=?^(id?parameter){

//?注意這里的線程默認(rèn)是?web?處理的線程,如果涉及主線程操作需要手動(dòng)轉(zhuǎn)到主線程

dispatch_async(dispatch_get_main_queue(),?^{

//?your?code

});

}

}

而 JS 這邊代碼更簡(jiǎn)單了,干脆聲明一個(gè)不解釋的函數(shù)(約定好名字的),用于給 Native 做引用:

1

2varparameter?=?xxx;

yourFuncName(parameter);

iOS Native 調(diào)用 JS

iOS Native 調(diào)用 JS 的實(shí)現(xiàn)方法也被 JavaScriptCore 劃分開來:

webview 直接注入 JS 并執(zhí)行

JavaScriptCore 方法

webview 直接注入 JS 并執(zhí)行

在 iOS 平臺(tái),webview 有注入并執(zhí)行 JS 的 API。

UIWebView

UIWebView 有直接注入 JS 的方法:

1

2NSString?*jsStr?=?[NSString?stringWithFormat:@"showAlert('%@')",?@"alert?msg"];

[_webView?stringByEvaluatingJavaScriptFromString:jsStr];

Note: 這個(gè)方法會(huì)返回運(yùn)行 JS 的結(jié)果(nullable NSString *),它是一個(gè)同步方法,會(huì)阻塞當(dāng)前線程!盡管此方法不被棄用,但最佳做法是使用 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 注入并執(zhí)行 JS 的方法不會(huì)阻塞當(dāng)前線程。因?yàn)榭紤]到 webview 加載的 web content 內(nèi) JS 代碼不一定經(jīng)過驗(yàn)證,如果阻塞線程可能會(huì)掛起 App。

1

2

3

4NSString?*jsStr?=?[NSString?stringWithFormat:@"setLocation('%@')",?@"北京市東城區(qū)南鑼鼓巷納福胡同xx號(hào)"];

[_webview?evaluateJavaScript:jsStr?completionHandler:^(id?_Nullable?result,?NSError?*?_Nullable?error)?{

NSLog(@"%@----%@",?result,?error);

}];

Note: 方法不會(huì)阻塞線程,而且它的回調(diào)代碼塊總是在主線程中運(yùn)行。

官方文檔:

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 方法

上面簡(jiǎn)單提到過 JavaScriptCore 庫提供的 JSValue 類,這里再提供一下官方文檔對(duì) JSValue 的介紹翻譯:

JSValue 實(shí)例是對(duì) JavaScript 值的引用。 您可以使用 JSValue 類來轉(zhuǎn)換 JavaScript 和 Objective-C 或 Swift 之間的基本值(如數(shù)字和字符串),以便在本機(jī)代碼和 JavaScript 代碼之間傳遞數(shù)據(jù)。

不過你也看到了我貼在上面的 OC 和 JS 數(shù)據(jù)類型轉(zhuǎn)換表,那里面根本沒有限定為官方文檔所說的基本值。如果你不熟悉 JS 的話,我這里解釋一下為什么 JSValue 也可以指向 JS 中的對(duì)象和函數(shù),因?yàn)?JS 語言不區(qū)分基本值和對(duì)象以及函數(shù),在 JS 中“萬物皆為對(duì)象”。

好了下面直接 show code:

1

2

3

4

5

6

7

8//?首先引入?JavaScriptCore?庫

#import//?先獲取?JS?上下文

self.jsContext?=?[webView?valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

//?如果涉及?UI?操作,切回主線程調(diào)用?JS?代碼中的?YourFuncName,通過數(shù)組@[parameter]?入?yún)?/p>

dispatch_async(dispatch_get_main_queue(),?^{

JSValue?*jsValue?=?self.jsContext[@"YourFuncName"];

[jsValue?callWithArguments:@[parameter]];

});

上面的代碼調(diào)用了 JS 代碼中 YourFuncName 函數(shù),并且給函數(shù)加了 @[parameter] 作為入?yún)ⅰ榱朔奖汩喿x理解,這里再貼一下 JS 代碼:

1

2

3

4functionYourFuncName(arguments){

varresult?=?arguments;

//?do?what?u?want?to?do

}

WKWebView 與 JS 交互的特有方法

wkwebview.jpg

關(guān)于 WKWebView 與 UIWebView 的區(qū)別就不在本文加以詳細(xì)說明了,更多信息還請(qǐng)自行查閱。這里要講的是 WKWebView 在與 JS 的交互時(shí)特有的方法:

WKUIDelegate 方法

MessageHandler 方法

WKUIDelegate 方法

對(duì)于 WKWebView 上文提到過,除了 WKNavigationDelegate,它還有一個(gè) WKUIDelegate,這個(gè) WKUIDelegate 是做什么用的呢?

WKUIDelegate 協(xié)議包含一些函數(shù)用來監(jiān)聽 web JS 想要顯示 alert 或 confirm 時(shí)觸發(fā)。我們?nèi)绻?WKWebView 中加載一個(gè) web 并且想要 web JS 的 alert 或 confirm 正常彈出,就需要實(shí)現(xiàn)對(duì)應(yīng)的代理方法。

Note: 如果沒有實(shí)現(xiàn)對(duì)應(yīng)的代理方法,則 webview 將會(huì)按照默認(rèn)操作去做出行為。

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 監(jiān)聽 web 要顯示 alert 的代理方法中用 Native UIAlertController 替代 JS 中的 alert 顯示的栗子 :

1

2

3

4

5

6

7

8

9

10-?(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)?{

//?函數(shù)內(nèi)必須調(diào)用?completionHandler

completionHandler();

}]];

[self?presentViewController:alert?animated:YES?completion:nil];

}

MessageHandler 方法

MessageHandler 是繼 Native 截獲 JS 假請(qǐng)求后另一種 JS 調(diào)用 Native 的方法,該方法利用了 WKWebView 的新特性實(shí)現(xiàn)。對(duì)比截獲假 Request 的方法來說,MessageHandler 傳參數(shù)更加簡(jiǎn)單方便。

MessageHandler 指什么?

WKUserContentController 類有一個(gè)方法:

1

-?(void)addScriptMessageHandler:(id?)scriptMessageHandler?name:(NSString?*)name;

該方法用來添加一個(gè)腳本處理器,可以在處理器內(nèi)對(duì) JS 腳本調(diào)用的方法做出處理,從而達(dá)到 JS 調(diào)用 Native 的目的。

那么 WKUserContentController 類和 WKWebView 有毛關(guān)系呢?

在 WKWebView 的初始化函數(shù)中有一個(gè)入?yún)?configuration,它的類型是 WKWebViewConfiguration。WKWebViewConfiguration 中包含一個(gè)屬性 userContentController,這個(gè) userContentController 就是 WKUserContentController 類型的實(shí)例,我們可以用這個(gè) userContentController 來添加不同名稱的腳本處理器。

wkusercontentcontroller.jpg

MessageHandler 的坑

那么回到 - (void)addScriptMessageHandler:name: 方法上面,該方法添加一個(gè)腳本消息處理器(第一個(gè)入?yún)?scriptMessageHandler),并且給這個(gè)處理器起一個(gè)名字(第二個(gè)入?yún)?name)。不過這個(gè)函數(shù)在使用的時(shí)候有個(gè)坑:scriptMessageHandler 入?yún)?huì)被強(qiáng)引用,那么如果你把當(dāng)前 WKWebView 所在的 UIViewController 作為第一個(gè)入?yún)ⅲ@個(gè) viewController 被他自己所持有的 webview.configuration. userContentController 所持有,就會(huì)造成循環(huán)引用。

retaincycle.jpg

我們可以通過 - (void)removeScriptMessageHandlerForName: 方法刪掉 userContentController 對(duì) viewController 的強(qiáng)引用。所以一般情況下我們的代碼會(huì)在 viewWillAppear 和 viewWillDisappear 成對(duì)兒的添加和刪除 MessageHandler:

1

2

3

4

5

6

7

8

9-?(void)viewWillAppear:(BOOL)animated?{

[superviewWillAppear:animated];

[self.webview.configuration.userContentController?addScriptMessageHandler:self?name:@"YourFuncName"];

}

-?(void)viewWillDisappear:(BOOL)animated?{

[superviewWillDisappear:animated];

[self.webview.configuration.userContentController?removeScriptMessageHandlerForName:@"YourFuncName"];

}

WKScriptMessageHandler 協(xié)議

WKScriptMessageHandler 是腳本信息處理器協(xié)議,如果想讓一個(gè)對(duì)象具有腳本信息處理能力(比如上文中 webview 的所屬 viewController 也就是上面代碼的 self)就必須使其遵循該協(xié)議。

WKScriptMessageHandler 協(xié)議內(nèi)部非常簡(jiǎn)單,只有一個(gè)方法,我們必須要實(shí)現(xiàn)該方法(@required):

1

2

3

4

5

6

7

8

9//?WKScriptMessageHandler?協(xié)議方法,在接收到腳本信息時(shí)觸發(fā)

-?(void)userContentController:(WKUserContentController?*)userContentController?didReceiveScriptMessage:(WKScriptMessage?*)message?{

//?message?有兩個(gè)屬性:name?和?body

//?message.name?可以用于區(qū)別要做的處理

if([message.name?isEqualToString:@"YourFuncName"])?{

//?message.body?相當(dāng)于?JS?傳遞過來的參數(shù)

NSLog(@"JS?call?native?success?%@",?message.body);

}

}

補(bǔ)充 JS 的代碼:

1

2//??換?YourFuncName,?換你要的入?yún)⒓纯?/p>

window.webkit.messageHandlers..postMessage()

搞定收工!

JS 通過 Native 調(diào)用 iOS 設(shè)備攝像頭的 Demo

徒手?jǐn)]了一個(gè) Demo,實(shí)現(xiàn)了 JS 與 Native 代碼的交互,達(dá)到用 JS 在 webview 內(nèi)調(diào)用 iOS 設(shè)備攝像頭的功能。Demo 內(nèi)含權(quán)限申請(qǐng),用戶拒絕授權(quán)等細(xì)節(jié)(技術(shù)上就是 JS 和 Native 相互傳值調(diào)用),還請(qǐng)各位大佬指教。

向各位基佬低頭,獻(xiàn)上我的膝蓋~(Demo 地址

總結(jié)

這篇文章簡(jiǎn)單的介紹了一下HybridMobile App(其中還包括Hybrid的發(fā)展簡(jiǎn)史)。

介紹了 JavaScriptCore 的組成,并且把 JSVirtualMachine 與 JSContext 和 JSValue 之間的關(guān)系用圖片的形式表述出來(JSVirtualMachine 包含 JSContext 包含 JSValue,都是 1 對(duì) n 的關(guān)系,且由于同一個(gè) JSVirtualMachine 下的代碼會(huì)相互阻塞,所以如果想異步執(zhí)行交互需要在不同的線程聲明 JSVirtualMachine 并發(fā)執(zhí)行)。

從調(diào)用方向的角度把 JS 與 iOS Native 相互調(diào)用的方式方法分別用代碼示例講解了一遍。

介紹了 WKWebView 與 JS 交互特有的方法:WKUIDelegate 和 MessageHandler。

提供了一個(gè) JS 通過 Native 調(diào)用 iOS 設(shè)備攝像頭的 Demo。

完結(jié)撒花,希望我的文章可以為你帶來價(jià)值!如果覺得文章還過得去,請(qǐng)幫我分享一下哈~阿里嘎多!

作者:Lision_狗蛋

鏈接:http://www.lxweimin.com/p/5329170be7b3

來源:簡(jiǎn)書

著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 隨著H5技術(shù)的興起,在iOS開發(fā)過程中,難免會(huì)遇到原生應(yīng)用需要和H5頁面交互的問題。其中會(huì)涉及方法調(diào)用及參數(shù)傳值等...
    Chris_js閱讀 3,114評(píng)論 1 8
  • 前言 在 iOS 開發(fā)中,JS 與 Native 的交互分為兩種,第一種是 Native 調(diào) JS,即通過在 Na...
    CoCodeDZ閱讀 12,702評(píng)論 5 51
  • 寧靜到只能聽見鄉(xiāng)村的聲響,有蟲有鳥有貓咪;簡(jiǎn)潔到臺(tái)詞鮮有對(duì)話,大部分都是獨(dú)白;干凈到只有無限的田園風(fēng)光,把人都淹沒...
    愛麗絲念閱讀 1,676評(píng)論 5 19
  • 請(qǐng)你珍惜你身邊愛你的人。因?yàn)槭澜缣螅恢朗遣皇且晦D(zhuǎn)身你身邊的人就被淹沒在人海,等你想去珍惜,可是已經(jīng)找不到。 ...
    杯酒盡余歡閱讀 237評(píng)論 1 1
  • 今天,媽媽給我看地圖我覺的很意思。
    埃菲爾鐵塔603閱讀 294評(píng)論 0 0