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

孤舟蓑翁發表於2016-04-20

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



 

 

 
 

相關文章