IOS:Objective-C 和 JavaScript 的相互調用
iOS7以前,iOS SDK 並沒有原生提供 js 調用 native 代碼的 API。但是 UIWebView 的一個 delegate 方法使我們可以做到讓 js 需要調用時,通知 native。在 native 執行完相應調用后,可以用stringByEvaluatingJavaScriptFromString 方法,將執行結果返回給 js。這樣,就實現了 js 與 native 代碼的相互調用。具體讓 js 通知 native 的方法是讓 js 發起一次特殊的網絡請求。使用加載一個隱藏的 iframe 來實現的,通過將 iframe 的 src 指定為一個特殊的 URL,在Objective-C中通過UIWebView的webView:shouldStartLoadWithRequest:navigationType:方法攔截這個跳轉,然后通過解析跳轉的url獲取js需要調用的方法名和參數。
Objective-C調用JavaScript
UIWebView有個方法是: - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script 可以
直接調用js。例如你想獲取頁面document的clientHeight屬性,這樣寫: NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.documentElement.clientHeight"]];
如果想調用頁面的一個叫xxx的函數,則只需要 [webview stringByEvaluatingJavaScriptFromString:@"xxx()"]
這種調用有一個限制條件: JS代碼占用的內存 < 10M。
Javascript調用Objective-C
iOS里面加載一個網頁用的是UIWebView,頁面加載是通過UIWebView的一個Delegate:UIWebViewDelegate來通知對應的webview的。而每次點擊頁面上的鏈接(或者是加載本頁面的地址時) 都會在加載前調用UIWebViewDelegate的一個方法: - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
如果這個方法的返回值是YES的話就繼續加載這個請求,如果是NO的話就不加載了。 Javascript調用Objective C代碼的秘訣就在這里面。
第一步. 匹配url格式
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType: (UIWebViewNavigationType)navigationType { if (request.URL.absoluteString match urlSchemePattern) { [self executeSomeObjectiveCCode]; return NO; } else { return YES; } }
request.URL.absoluteString match urlSchemePattern
這句的意思是: 如果頁面的url格式滿足某種特定格式, 就不加載那個請求,而是執行Objective-C代碼。
第2步 協商url格式以及參數傳遞方式
Javascript想要調用Objective-C代碼時,Javascript代碼就需要和Objective-C協商一個請求的協議,例如:凡是請求的url scheme 是"js-call://"
這樣格式開頭的就是Javascript需要調用Objective C的代碼,再具體點,比如"js-call://user/get" 就是要調用Objective-C 代碼中一個getUser的方法的。 如果Javascript需要傳遞參數給Objective-C, 最簡單的方法是像http的query string一樣傳參數, 例如:"js-call://user/set?uid=1&name=jpx",然后在分析url的時候將query string提取出來傳給Objective -C的方法即可。 代碼如下:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { if ([request.URL.absoluteString hasPrefix:@"js-call://user/set"]) { NSDictionary *parameters = [self parseQueryString:request.URL.absoluetString]; [self executeSomeObjectiveCCodeWithParameters:parameters]; return NO; } else if ([request.URL.absoluteString hasPrefix:@"js-call://user/get"]) { NSDictionary *parameters = [self parseQueryString:request.URL.absoluetString]; [self executeSomeObjectiveCCodeWithParameters:parameters]; return NO; } return YES; }
如果Javascript需要調用好幾個 Objective C的接口,那么在shouldStartLoadWithRequest的delegate方法里面就會有很多if ... else if分支代碼, 此外,解析query string的那部分代碼也是重復的,最好的辦法是將這一切封裝起來,可以定義一個JPXUIWebViewJSBridge方法。
self.bridge = [[JPXUIWebViewJSBridge alloc] initWithHandler:self];
self.bridge.routines = @[@[@"^js-call://user/set.*$", @"setUser"],
@[@"^js-call://user/get.*$", @"getUser"]
];
定義了這套規則之后,只需要比如說在ViewController里面實現一個叫setUser的方法即可: - (void)setUser:(NSDictionary *)parametersFromWeb
, 其中parametersFromWeb就是query string對應的字典!
然后在 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
只需要這樣寫就可以了:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSError *error; BOOL canHandleRequest = [self.bridge canHandleRequest:request error:&error]; if (canHandleRequest) { [self.bridge handleRequest:request error:&error]; NSLog(@"error1:%@", [error localizedDescription]); return NO; } else { NSLog(@"error2:%@", [error localizedDescription]); } return YES; }
Javascript調用Objective C時,很多人第一反應就是在a標簽里面的href寫url調用,例如: <a href="js-call://user/set?uid=1&name=jpx" >測試</a>
, 但是這樣的調用會如下的一些問題:
如果我們連續 2 個 js 調 native,連續 2 次改 <a href>
的話,在 native 的 delegate 方法中,只能截獲后面那次請求,前一次請求由於很快被替換掉,所以被忽略掉了。還有這種改url的方式也不太安全。
而合理的做法應該是通過加載一個iframe:
function execute(url)
{
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", url);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
從iOS7開始,我們可以使用JavaScriptCore框架來讓我們的Objective-C代碼和JavaScript進行深度交互,簡單的說我們可以在Objective-C代碼中訪問JavaScript中的變量或調用JavaScript的函數,也可以JavaScript中使用Objective-C的對象和方法
同步和異步
因為 iOS SDK 沒有天生支持 js 和 native 相互調用,大家的技術方案都是自己實現的一套調用機制,所以這里面有同步異步的問題。細心的同學就能發現,js 調用 native 是通過插入一個 iframe,這個 iframe 插入完了就完了,執行的結果需要 native 另外用 stringByEvaluatingJavaScriptFromString 方法通知 js,所以這是一個異步的調用。
而 stringByEvaluatingJavaScriptFromString 方法本身會直接返回一個 NSString 類型的執行結果,所以這顯然是一個同步調用。
所以 js call native 是異步,native call js 是同步。在處理一些邏輯的時候,不可避免需要考慮這個特點。
方法。
Android:Java 和 JavaScript 相互調用
在Android 4.2之前可以使用addJavascriptInterface
方式注入原生Java方法給JavaScript調用, 這種方案有一定的安全風險,在頁面中執行一些不可信的Javascript代碼即可能控制用戶的手機,
因此在Android 4.2之后Android提供了@JavascriptInterface
對象注入的方式建立Javascript對象和android原生對象的綁定,提供給javascript調用的函數必須帶有@JavascriptInterface
。本文以@JavascriptInterface為例,講解一下Android:Java和JavaScript之間相互調用的方法。
第一步: 加載本地html文件
有的時候我們在使用webview開發的時候會使用本地的html文件,在這里為了方便我們把html文件都放在assets
文件夾中,使用本地加載的方式,不需要server支持。
先定義一個html文件:
<!DOCTYPE html> <html> <body> <h1>this is html</h1> </body> </html>
使用file:///android_asset/index.html
加載到webview中:
private void initView() { webView = (WebView) findViewById(R.id.webView); webView.loadUrl("file:///android_asset/index.html"); }
Javascript調用Java方法
以Android的Toast的為例,從Javascript代碼中調用系統的Toast。
我們定義一個AndroidToast的Java類,它有一個show的方法用來顯示Toast:
public class AndroidToast { @JavascriptInterface public void show(String str) { Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show(); } }
需要對WebView設置一些參數,開啟JavaScipt,注冊JavascriptInterface:
private void initView() { webView = (WebView) findViewById(R.id.webView); WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setDefaultTextEncodingName("UTF-8"); webView.addJavascriptInterface(new AndroidToast(), "AndroidToast"); webView.loadUrl("file:///android_asset/index.html"); }
addJavascriptInterface
的作用是把AndroidToast類映射為Javascript中的AndroidToast對象。
在Javascript中調用Java代碼:
function toastClick(){ window.AndroidToast.show('from js'); }
通過window的屬性可以找到Java映射的對象AndroidToast,調用它的show方法。
注意這里傳輸的數據只能是基本數據類型和string,可以傳輸string意味着我們可以使用json
傳輸結構化數據。
Javascript調用有返回值Java函數
如果想從Javascript調的方法里面獲取到返回值,只需要定義一個帶返回值的@JavascriptInterface
方法:
public class AndroidMessage { @JavascriptInterface public String getMsg() { return "form java"; } }
添加Javascript的映射Webview:
webView.addJavascriptInterface(new AndroidMessage(), "AndroidMessage");
Javascript直接調用Java方法:
function showAlert(){ var str=window.AndroidMessage.getMsg(); console.log(str); }
Java調用Javascript方法
Java在調用js的時候,使用的是WebView.loadUrl()
方法,可以直接在HTML頁面里面執行JavaScript方法,首先定義一個Javascript方法給Java調用:
Java調用有參數無返回值的js函數
function callFromJava(str){ console.log(str); }
Java端調用Javascript方法:
public void javaCallJS(){ webView.loadUrl("javascript:callFromJava('call from java')"); // 可以在loadUrl中直接給Javascript方法直接傳值 }
調用js有參數有返回值的函數
Android在4.4之前並沒有提供直接調用js函數並獲取值的方法,所以在此之前,常用的思路是 java調用js方法,js方法執行完畢,再次調用java代碼將值返回。
1.Java調用js代碼
String call = "javascript:sumToJava(1,2)";
webView.loadUrl(call);
2.js函數處理,並將結果通過調用java方法返回
function sumToJava(number1, number2){ window.control.onSumResult(number1 + number2) }
3.Java在回調方法中獲取js函數返回值
@JavascriptInterface public void onSumResult(int result) { Log.i(LOGTAG, "onSumResult result=" + result); }
Android 4.4處理
Android 4.4之后使用evaluateJavascript即可。這里展示一個簡單的 具有返回值的js方法
function getGreetings() { return 1; }
java代碼時用evaluateJavascript方法調用
private void testEvaluateJavascript(WebView webView) { webView.evaluateJavascript("getGreetings()", new ValueCallback<String>() { @Override public void onReceiveValue(String value) { Log.i(LOGTAG, "onReceiveValue value=" + value); }}); }
注意事項:
- 上面限定了結果返回結果為String,對於簡單的類型會嘗試轉換成字符串返回,對於復雜的數據類型,建議以字符串形式的json返回。
- evaluateJavascript方法必須在UI線程(主線程)調用,因此onReceiveValue也執行在主線程。