Hybrid App開發模式中, IOS/Android 和 JavaScript相互調用方式


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也執行在主線程。



 

 

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM