Hybrid App 原理解析


一、現有混合方案

Hybrid App,俗稱混合應用,即混合了 Native技術 與 Web技術 進行開發的移動應用。現在比較流行的混合方案主要有三種,主要是在UI渲染機制上的不同:

  1. 基於 WebView UI 的基礎方案,市面上大部分主流 App 都有采用,例如微信JS-SDK,通過 JSBridge 完成 H5 與 Native 的雙向通訊,從而賦予H5一定程度的原生能力。
  2. 基於 Native UI 的方案,例如 React-Native、Weex。在賦予 H5 原生API能力的基礎上,進一步通過 JSBridge 將js解析成的虛擬節點樹(Virtual DOM)傳遞到 Native 並使用原生渲染。
  3. 另外還有近期比較流行的 小程序方案,也是通過更加定制化的 JSBridge,並使用雙 WebView 雙線程的模式隔離了JS邏輯與UI渲染,形成了特殊的開發模式,加強了 H5 與 Native 混合程度,提高了頁面性能及開發體驗。

以上的3種方案,其實同樣都是基於 JSBridge 完成的通訊層,第2、3種方案,其實可以看做是在方案1的基礎上,繼續通過不同的新技術進一步提高了應用的混合程度。因此,JSBridge 也是整個混合應用最關鍵的部分。

二、Hybrid技術原理

Hybrid App的本質,其實是在原生的 App 中,使用 WebView 作為容器直接承載 Web頁面。因此,最核心的點就是 Native端 與 H5端 之間的雙向通訊層,其實這里也可以理解為我們需要一套 跨語言通訊方案,來完成 Native(Java/Objective-c/...) 與 JavaScript 的通訊。實現的關鍵,便是作為容器的 WebView,一切的原理都是基於 WebView 的機制。

三、Native 通知 H5 (Native 調用 JS)

由於 webview 作為 H5 的宿主,Native 可以通過 webview 的 API直接執行 Js 代碼。

3.1 Android 調 H5

loadUrl

// 即當前webview對象     
mWebView = new WebView(this);       
mWebView.loadUrl("javascript:方法名('參數需要轉為字符串')"); 
// ui線程中運行
runOnUiThread(new Runnable() {  
    @Override  
    public void run() {  
        mWebView.loadUrl("javascript: 方法名('參數需要轉為字符串')");  
        Toast.makeText(Activity名.this, "調用方法...", Toast.LENGTH_SHORT).show();  
    }  
});

該方法沒有系統版本的限制,但是無法獲取函數的返回值。需要使用 prompt 方法進行兼容,讓 H5端 通過 prompt 進行數據的發送,客戶端進行攔截並獲取數據。

注意: mWebView.loadUrl("javascript: 方法名('參數需要轉為字符串')");
函數需在UI線程運行,因為mWebView為UI控件,會阻塞UI線程。

evaluateJavascript

// 異步執行JS代碼,並獲取返回值    
mWebView.evaluateJavascript("javascript: 方法名('參數需要轉為字符串')", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
        // 這里的value即為對應JS方法的返回值
    }
});

該方法的弊端是在 >= Android 4.4 版本才能使用。但是可以獲得 Js 函數執行的返回值。

3.2 iOS 調 H5

stringByEvaluatingJavaScriptFromString

可以取得JS函數執行的返回值,
但是方法必須是綁定在頂層頁面的window上對象,如:window.top.foo 。

// Swift
webview.stringByEvaluatingJavaScriptFromString("方法名(參數)")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"方法名(參數);"];

四、H5 通知 Native(JS 調用 Native)

基於 WebView 的機制和開放的API,實現 JS 調用 Native 有3種常見的方案:

  1. WebView URL Scheme 跳轉攔截
  2. WebView 中的prompt/console/alert攔截:通常使用prompt,因為這個方法在前端中使用頻率低,比較不會出現沖突
  3. WebView API注入:原理其實就是Native獲取JavaScript環境上下文,並直接在上面掛載對象或者方法,使 js 可以直接調用,Android 與 IOS 分別擁有對應的掛載方式

4.1 URL Scheme

第1、2種機制的原理是類似的,都是通過對WebView信息冒泡傳遞的攔截,從而達到通訊的,接下來我們主要從 “概述-定制協議-攔截協議-參數傳遞-回調機制” 5個方面詳細闡述下第1種方案 -- URL Scheme攔截方案。

4.1.1 概述

URL Scheme 是一種類似於url的鏈接, 是為了方便 App 之間互相調用設計的。可以用系統的 OpenURI 打開一個類似於 url 的鏈接(可拼入參數),
然后系統會進行判斷,如果是系統的 url scheme,則打開系統應用,
否則找看是否有 App 注冊這種 scheme,打開對應App。而本文中混合開發交互的 URL Scheme 則是仿照上述的形式的一種方式。

URL Scheme 適用於所有的系統設備(低版本 Android 和低版本 iOS 都適用)。這個是最廣為流傳的交互方式,因為在 hybrid 剛出來時,很多低版本都需要兼容,因此幾乎都采用這種方式。

URL Scheme 的原理就是在 WebView 中發出的網絡請求,客戶端都能進行監聽和捕獲。

H5 通過 iframe 或者 location 觸發一個 url scheme 
-> Native端捕獲到url
-> Native分析其參數屬於哪一個功能並執行 
-> Native調用H5提供的回調函數將執行結果回調給H5

但是 URL Scheme 畢竟是通過url攔截實現的,在大量數據傳輸以及效率上都有一些影響。

4.1.2 協議的定制

我們需要制定一套URL Scheme規則,通常我們的請求會帶有對應的協議開頭,例如常見的 https://xxx.com 或者 file://xxx.jpg,代表着不同的含義。我們這里可以將協議類型的請求定制為:

xx_protocal://xx_host/xx_path?params=xx

這里有幾個需要注意點的是:

(1) xx_protocal:// 只是一種規則,可以根據業務進行制定,使其具有含義,例如我們定義 xx_company:// 為公司所有App系通用,為通用工具協議:

xx_company://xx_host/getNetwork

而定義 xx_app:// 為每個App單獨的業務協議。

xx_app://xx_host/clipboard?params=xx

不同的協議頭代表着不同的含義,這樣便能清楚知道每個協議的適用范圍。

(2) 這里不要使用 location.href 發送,通過 location.href 有個問題,就是如果我們連續多次修改 window.location.href 的值,在 Native層 只能接收到最后一次請求,前面的請求都會被忽略掉。,而並發協議其實是非常常見的功能。我們會使用創建 iframe 發送請求的方式。

(3) 通常考慮到安全性,需要在客戶端中設置域名白名單或者限制,避免公司內部業務協議被第三方直接調用。

4.1.3 協議的攔截

客戶端可以通過 API 對 WebView 發出的請求進行攔截:

  • IOS:shouldStartLoadWithRequest
  • Android:shouldOverrideUrlLoading

當解析到請求 URL 頭為制定的協議時,便不發起對應的資源請求,而是解析參數,並進行相關功能或者方法的調用,完成協議功能的映射。

4.1.4 協議回調

由於協議的本質其實是發送請求,這屬於一個異步的過程,因此我們便需要處理對應的回調機制。

定義 window.xx 全局方法回調
// 例子:獲取當前網絡情況。

/** js code **/
function getNetwork() {
  return new Promise(resolve => {
    // 定義回調函數
    let cb = 'getNetwork';
    // 定義scheme格式和對應的參數
    let url = 'xx_protocal://xx_host/getNetwork&params={}&cb=${cb}';
    window[cb] = result => resolve(result);
    // 觸發 scheme
    window.location.href = url;
  })
}
getNetwork().then(result => { /* 具體業務代碼 */ })

/** Native code **/

network_result = getNetworkFromSystem();
cb = getFromSchemeParams();

// Android
mWebView.loadUrl("javascript: cb(network_result)");

// iOS
[webView stringByEvaluatingJavaScriptFromString:@"cb(network_result);"];
利用 JS 的事件訂閱/派發

這里我們會用到 window.addEventListenerwindow.dispatchEvent 這兩個方法。

  1. 發送協議時,通過 window.addEventListener 注冊自定義事件,將回調綁定到對應的事件上, 客戶端完成對應的功能后,調用 window.dispatchEvent,直接攜帶 data 觸發該協議的自定義事件
// 例子:獲取當前網絡情況。

/** js code **/
function getNetwork() {
  return new Promise(resolve => {
    // 自定義事件名
    let eventType = 'getNetwork';
    // 綁定自定義事件
    window.addEventListener(eventType, e => {
        resolve(e)
    })
    // 定義scheme格式和對應的參數
    let url = 'xx_protocal://xx_host/getNetwork&params={}&eventType=${eventType}';
    // 觸發 scheme
    window.location.href = url;
  })
}

getNetwork().then(result => { /* 具體業務代碼 */ })


/** Native code **/
network_result = getNetworkFromSystem();
eventType = getFromSchemeParams();

let event = new Event(eventType)
event.data = network_result

// Android
mWebView.loadUrl("javascript: dispatchEvent(event)");

// iOS
[webView stringByEvaluatingJavaScriptFromString:@"dispatchEvent(event);"];

這里有一點需要注意的是,應該避免事件的多次重復綁定,因此當唯一標識重置時,需要 removeEventListener對應的事件。

2.1.5 參數傳遞方式

由於 WebView 對 URL 會有長度的限制,因此常規的通過 search參數 進行傳遞的方式便具有一個問題,當需要傳遞的參數過長時,可能會導致被截斷,例如:傳遞base64或者傳遞大量數據時。

因此我們需要制定新的參數傳遞規則,我們使用的是函數調用的方式。這里的原理主要是基於Native 可以直接調用 JS 方法並直接獲取函數的返回值。

我們只需要對每條協議標記一個唯一標識,並把參數存入參數池中,到時客戶端再通過該唯一標識從參數池中獲取對應的參數即可。

4.2 WebView API 注入

在 iOS 中使用 JavaScriptCore, 其不支持 iOS7 以下。在 Android 中使用 addJavascriptInterface, 其在4.2以前有風險漏洞。隨着手機更新換代,這些低版本系統的手機占比逐漸變小, 所以造成的影響不大。

Android - addJavascriptInterface

首先,原生webview需要先注冊可供前端調用的JS函數

WebSettings webSettings = mWebView.getSettings();  
 // 設置webview允許JS腳本運行
webSettings.setJavaScriptEnabled(true);
// 設置橋接對象
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
// Android4.2版本及以上,方法要加上注解@JavascriptInterface,否則會找不到方法。
private Object getJSBridge(){  
    Object insertObj = new Object(){  
        @JavascriptInterface
        public String foo(){  
            return "foo";  
        }  

        @JavascriptInterface
        public String foo2(final String param){  
            return "foo2:" + param;  
        }  
    };  
    return insertObj;  
}

然后H5中即可調用原生中注冊的函數

// 調用方法1 
window.JSBridge.foo(); // 返回:'foo'
// 調用方法2 
window.JSBridge.foo2('test'); // 返回:'foo2:test'

iOS - JavaScriptCore

以OC為例,首先,需要引入JavaScriptCore庫。

#import <JavaScriptCore/JavaScriptCore.h>

然后原生需要注冊API

//webview加載完畢后設置一些js接口
-(void)webViewDidFinishLoad:(UIWebView *)webView{
    [self hideProgress];
    [self setJSInterface];
}

-(void)setJSInterface{

    JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    // 注冊名為foo的api方法
    context[@"foo"] = ^() {

        //獲取參數
        NSArray *args = [JSContext currentArguments];
        NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
    
        //返回一個值  'foo:'+title
        return [NSString stringWithFormat:@"foo:%@", title];
    };
}

之后 JS 就可以調用了

// 調用方法,用top是確保調用到最頂級,因為iframe要用top才能拿到頂級
window.top.foo('test'); // 返回:'foo:test'

注意:引入官方提供的JavaScriptCore庫 (需iOS7及以上),可以將api綁定到JSContext上
,JS 默認通過 window.top.*(iframe中時需加top)可調用。

iOS7之前,js無法直接調用Native,只能通過 url scheme 方式間接調用

五、探討 JSBridge 接入方式

接下來,我們來理下代碼上需要的資源。實現這套方案,可以分為兩個部分:

  • JS部分: 在JS環境中注入 bridge 的實現代碼,包含了協議的拼裝/發送/參數池/回調池等一些基礎功能。
  • Native部分: 在客戶端中 bridge 的功能映射代碼,實現了URL攔截與解析/環境信息的注入/通用功能映射等功能。

推薦的做法是,將這兩部分一起封裝成一個 Native SDK,由客戶端統一引入。客戶端在初始化一個 WebView 頁面時,如果頁面地址在白名單中,會直接在 HTML 的頭部注入對應的 bridge.js。這樣的做法有以下的好處:

  • 雙方的代碼統一維護,避免出現版本分裂的情況。有更新時,只要由客戶端更新SDK即可,不會出現版本兼容的問題;
  • App的接入十分方便,只需要按文檔接入最新版本的SDK,即可直接運行整套Hybrid方案,便於在多個App中快速的落地;
  • H5端無需關注,這樣有利於將 bridge 開放給第三方頁面使用。

這里有一點需要注意的是,H5的調用,一定是需要確保執行在bridge.js 成功注入后。由於客戶端的注入行為屬於一個附加的異步行為,從H5方很難去捕捉准確的完成時機,因此這里需要通過客戶端監聽頁面完成后,基於事件回調機制通知 H5端,頁面中即可通過 監聽橋接事件進行初始化。

window.addEventListener('bridgeReady', e => {})

本文主要解析了 Hybrid 的基礎原理,只有在了解其最本質的實現原理后,才能對這套方案進行實現以及進一步的優化。后續,我們將基於上面的理論,繼續探討真正可用的代碼實現方案。


免責聲明!

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



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