Hybrid模式多程序實踐
在給這篇部落格起題目的時候讓我很糾結,因為會涉及到下面的知識點:
- Hybrid
- 跨程序通訊
- Java與JS通訊
最後考慮到都是Hybrid需要用到的知識點,所以就有了上面的題目。言歸正傳,先說說為什麼Hybrid需要用到跨程序的知識點。
作為一個Androider應該知道,虛擬機器分配給各個程序的執行記憶體是有限制的,不同機型不一致。如果App中有很多的圖片模組,雖然做了多級快取,也是會有OOM的風險,如果此時再用WebView載入網頁很有可能吃不消。市面上有用多程序去這樣操作的嗎?有,比如微信,開啟微信然後捉一下程序資訊:

wechat process.png
有8個程序,根據你開啟公眾號或者小程式會略有點不同,其中有一個tools程序是用來開啟webview和相簿用的。這樣如果網頁或者公眾號有問題也不會引起微信的崩潰。
今天要實踐的就是將Web單獨一個Web程序,Native一個程序,Web呼叫Native提供的方法時需要跨程序通訊。
看一下Demo,不知道在Mac上怎麼錄製Gif,將就著看圖片吧,Demo比較簡單:
第一張在主程序中,點選‘start web activity’會啟動Web程序

screenshot01.jpg
啟動web程序後會載入html檔案,很簡單就是兩個按鈕,點選會呼叫Native方法,並接受回撥

screenshot02.jpg

screenshot03.jpg
下面開始分解。先看看主結構,就是兩個Activity一個Service和Application
1.主結構
先看下Demo 的目錄結構,其中main就是主程序,web就是子程序的目錄,aidl就是定義跨程序的地方,assets目錄下是html檔案。

DemoStructure.png
再看下清單檔案,其中MainActivity和MainService是在主程序中,WebActivity在子程序,用一個屬性就能實現, android:process=":remote"
。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.juexingzhe.hybrid"> <uses-permission android:name="android.permission.INTERNET" /> <application android:name=".MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".main.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".web.WebActivity" android:enabled="true" android:exported="true" android:process=":remote" /> <service android:name=".main.MainServivce"/> </application> </manifest>
MainActivity
中放一個 TextView
,點選啟動子程序:
textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity.this, WebActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } });
MainService
中就是用來監聽子程序連線主程序用的,程式碼很簡單, BinderManager
就是用來管理Web程序和主程序之間Binder。
public class MainServivce extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return new BinderManager(this); } }
WebActivity
中就是一個簡單的WebView。
MyApplication
就是用來註冊給JS呼叫的Native方法,這個我為了簡單現在是弄成單例模式,工程化考慮可以通過 Annotation
在編譯時期做個掃描註冊。
public class MyApplication extends Application { private Context context; @Override public void onCreate() { super.onCreate(); WorkManager.getInstance().postTask(new Runnable() { @Override public void run() { JsBridge.getInstance().register(JsNativeInterface.class); } }); } @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); context = base; } }
Demo的大體結構就是這樣,下面看下跨程序的相關實現。
2.跨程序通訊實現
簡單總結下Android下的跨程序通訊方式:
- 四大元件間可以通過Bundle傳遞實現
- 共享檔案
- Messenger, 輕量級的跨程序通訊方案,和Handler使用有點類似,底層是通過AIDL實現
- AIDL,Android介面定義語言,用於定義跨程序通訊的介面
- ContentProvider, 常用於跨程序共享資料,比如系統的相簿,音樂等
- 使用Socket傳輸資料
今天主要用Android推薦的AIDL進行實現,AIDL主要有三個步驟:
- 客戶端使用bindService方法繫結服務端
- 服務端在onBind方法返回Binder物件
- 客戶端拿到服務端返回的Binder物件進行跨程序方法呼叫
在我們這個Demo中客戶端是 WebActivity
, 服務端就是 MainService
。
首先看下三個AIDL介面檔案, IBinderManager
是用來統一管理主程序提供給子程序 IBinder
的,
// IBinderManager.aidl package com.example.juexingzhe.hybrid; import android.os.IBinder; // Declare any non-default types here with import statements interface IBinderManager { IBinder queryBinder(int binderCode); }
在我們Demo裡其實只有一個 IBinder
,就是 IWebBinder
, 用來子程序具體呼叫主程序Native函式用的,
// IWebBinder.aidl package com.example.juexingzhe.hybrid; importcom.example.juexingzhe.hybrid.IWebBinderCallback; // Declare any non-default types here with import statements interface IWebBinder { void handleJsFunction(in String methodName, in String params, in IWebBinderCallback callback); }
呼叫後主程序的函式呼叫結果通過 IWebBinderCallback
返回給子程序,
// IWebBinderCallback.aidl package com.example.juexingzhe.hybrid; // Declare any non-default types here with import statements interface IWebBinderCallback { void onResult(in int msgType, in String message); }
接下來
先看看第一步的實現,到 WebActivity
中看程式碼, 在 onCreate
中,
final WebHelper webHelper = new WebHelper(this); webHelper.setWebView(webView); webHelper.setConnectCallback(new ServiceConnectCallback() { @Override public void onServiceConnected() { runOnUiThread(new Runnable() { @Override public void run() { webView.loadUrl("file:///android_asset/testjs.html"); } }); } });
可以看到主要會把工作交給 WebHelper
, 這一層主要和WebView和JS進行互動
@SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) public void setWebView(WebView webView) { this.webView = webView; this.webView.getSettings().setJavaScriptEnabled(true); webView.addJavascriptInterface(jsInterface, JS_INTERFACE_NAME); bindService(activity); }
其他程式碼先不看,看到最後一行 bindService(activity)
, 裡面通過執行緒池去做bindService的工作,很明顯可以看到工作還是代理給了這一行程式碼 webBinderHandler.bindMainService(activity)
,先按下後面說,接著往下看,繫結成功後通過 getWebBinder()
能獲取到IBinder,再轉化成 WebBinder
,這樣就可以和主程序通訊了。
protected void bindService(final Activity activity) { WorkManager.getInstance().postTask(new Runnable() { @Override public void run() { WebBinderHandler webBinderHandler = WebBinderHandler.getInstance(); webBinderHandler.bindMainService(activity); IBinder binder = webBinderHandler.getWebBinder(); webBinder = IWebBinder.Stub.asInterface(binder); if (connectCallback != null) { connectCallback.onServiceConnected(); } } }); }
接著看看上面提到的 webBinderHandler.bindMainService(activity)
,這裡會真正的, webBinderHandler
主要用來處理和主程序通訊的工作,這裡會進行真正的 bindService
/** * 繫結主程序服務 * * @param context */ public synchronized void bindMainService(Context context) { countDownLatch = new CountDownLatch(1); Intent intent = new Intent(context, MainServivce.class); if (serviceConnect == null) { serviceConnect = new ServiceConnectImpl(context); } context.bindService(intent, serviceConnect, Context.BIND_AUTO_CREATE); try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } }
然後在 ServiceConnection
中可以拿到主程序的Binder代理, 拿到的 IBinder
是IBinderManager的介面例項, IBinder
可以註冊一個死亡監聽,在 IBinder
死亡的時候可以通知到子程序。
private class ServiceConnectImpl implements ServiceConnection { private Context context; public ServiceConnectImpl(Context context) { this.context = context; } @Override public void onServiceConnected(ComponentName name, IBinder service) { binderManager = IBinderManager.Stub.asInterface(service); try { // Web程序監聽binder的死亡通知 binderManager.asBinder().linkToDeath(new IBinder.DeathRecipient() { @Override public void binderDied() { binderManager.asBinder().unlinkToDeath(this, 0); binderManager = null; // binder死了再次去啟動服務連線主程序 bindMainService(context); } }, 0); } catch (RemoteException e) { e.printStackTrace(); } countDownLatch.countDown(); } @Override public void onServiceDisconnected(ComponentName name) { } }
在拿到主程序 IBinderManager
就可以呼叫方法 queryBinder
拿到Web的 IBinder
了:
/** * 獲取與主程序通訊的Binder * * @return */ public IBinder getWebBinder() { IBinder binder = null; try { if (binderManager != null) { binder = binderManager.queryBinder(BinderManager.BINDER_WEB_AIDL_CODE); } } catch (RemoteException e) { e.printStackTrace(); } return binder; }
接下來就是主程序的 MainService
了,在這裡監聽子程序的連線,
public class MainServivce extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return new BinderManager(this); } }
程式碼很簡單,就是返回 BinderManager
, 再看看 BinderManager
,
/** * Web程序和主程序之間Binder的管理 */ public class BinderManager extends IBinderManager.Stub { public static final int BINDER_WEB_AIDL_CODE = 0x101; private Context context; public BinderManager(Context context) { this.context = context; } @Override public IBinder queryBinder(int binderCode) { IBinder binder = null; switch (binderCode) { case BINDER_WEB_AIDL_CODE: binder = new WebBinder(context); break; default: break; } return binder; } }
程式碼很簡單,就是返回 WebBinder
,
/** * 用於Web端向主程序通訊 */ public class WebBinder extends IWebBinder.Stub { private Context context; public WebBinder(Context context) { this.context = context; } @Override public void handleJsFunction(String methodName, String params, IWebBinderCallback callback) { JsBridge.getInstance().callJava(methodName, params, callback); } }
然後主程序就開始執行對應的函式,執行成功後跨程序回撥結果給子程序,
/** * 獲取使用者資訊 * * @param param * @param callback */ public static void getUserInfo(final JSONObject param, final IWebBinderCallback callback) { try { JSONObject jsonObject = new JSONObject(); jsonObject.put("account", "[email protected]"); jsonObject.put("password", "1234567"); // 回撥給子程序呼叫js String backToJS = String.format(CALL_TO_USER_INFO, jsonObject.toString()); if (callback != null) { callback.onResult(HybridConfig.MSG_TYPE_GET_USER_INFO, backToJS); } } catch (JSONException e) { e.printStackTrace(); } catch (RemoteException e) { e.printStackTrace(); } }
最後再看看子程序中的回撥 IWebBinderCallback
, 在 WebHelper
中
protected void handleJsFunction(String methodName, String params) { try { webBinder.handleJsFunction(methodName, params, new IWebBinderCallback.Stub() { @Override public void onResult(int msgType, String message) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(message, null); } else { webView.loadUrl(message); } } }); } catch (Exception e) { e.printStackTrace(); } }
其實就是把結果通過webView回撥給JS層,這個後面再說。跨程序通訊基本就是這樣實現了,再總結下:
- 首先通過繫結服務拿到主程序提供的
IBinderManager
,這樣就可以呼叫它的方法拿到IWebBinder
-
IWebBinder
就可以傳過去函式名和函式引數以及回撥,呼叫Native的函 數
3.回撥也是需要跨程序通訊的,所以也是一個aidl介面檔案
4.在結構上WebHelper
主要用於和WebView以及JS呼叫封裝處理,WebBinderHandler
主要用於和主程序進行通訊,各司其職。
接下來看看JS和Java層的通訊實現細節。
3.Java與JS通訊實現
其實這個可以參考我之間的一篇部落格, ofollow,noindex">Android與JS之JsBridge使用與原始碼分析 ,這裡再簡單總結下,
js呼叫Java的方式基本有三種
- 通過schema方式,使用shouldOverrideUrlLoading方法對url協議進行解析,js使用iframe來呼叫native程式碼
- 在webview頁面裡直接注入原生js程式碼方式,使用addJavascriptInterface方法來實現,Demo中有這種實現方式
- 使用prompt,console,log,alert方式,這三個方法對js裡是屬性原生的,在android webview這一層是可以重寫這三個方法的,一般使用pormpt,因為這個再js裡使用的不多,用來和native通訊副作用比較少。
Java呼叫JS基本只有一種方式就是loadUrl,
- 4.4之前Native通過loadUrl來呼叫JS方法,只能讓某個JS方法執行,但是無法獲取該方法的返回值
- 4.4及之後,通過evaluateJavascript非同步呼叫JS方法,並且能在onReceiveValue中拿到返回值
看下demo中的實現,首先就是JS呼叫Java程式碼,先看下第二種方式,首先到 WebHelper
中,註冊 RemoteJsInterface
,
public WebHelper(Activity activity) { this.activity = activity; jsInterface = new RemoteJsInterface(); jsInterface.setCallback(this); }
接著看 RemoteJsInterface
,
/** * Webview.addJavascriptInterface */ public final class RemoteJsInterface { private final Handler handler = new Handler(); private JsFunctionCallback callback; @JavascriptInterface public void callJavaFunction(final String methodName, final String params) { handler.post(new Runnable() { @Override public void run() { try { if (callback != null) { callback.execute(methodName, params); } } catch (Exception e) { e.printStackTrace(); } } }); } public void setCallback(JsFunctionCallback callback) { this.callback = callback; } public interface JsFunctionCallback { void execute(String methodName, String params); } }
最後通過回撥呼叫到 WebHelper
中,通過 WebBinder
呼叫到Native,
@Override public void handleJsFunction(String methodName, String params, IWebBinderCallback callback) { JsBridge.getInstance().callJava(methodName, params, callback); }
然後到 JsBridge
中, 在 Application
啟動的時候註冊方法,然後通過函式名反射呼叫。就不貼具體的程式碼了。
再看下 IWebBinderCallback
回撥的實現,其實小夥伴們應該猜到了就是通過 loadUrl
實現,在 WebHelper
中,
protected void handleJsFunction(String methodName, String params) { try { webBinder.handleJsFunction(methodName, params, new IWebBinderCallback.Stub() { @Override public void onResult(int msgType, String message) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(message, null); } else { webView.loadUrl(message); } } }); } catch (Exception e) { e.printStackTrace(); } }
再看下JS程式碼裡面的實現,回撥的時候指定好回撥的函式名 onUserInfoResult
就行,
function getUserInfo(){ window.jsInterface.callJavaFunction( 'getUserInfo', JSON.stringify({'info': 'I am JS, want to get UserInfo from Java'}) ) } function onUserInfoResult(repsonseData){ document.getElementById("show1").innerHTML = "repsonseData from java:\n\n\naccount = " + repsonseData.account + "\npassword = " + repsonseData.password document.getElementById("no1").style.display="none" }
最後看下第三種的呼叫方式,首先看下JS層的程式碼, prompt
是同步返回結果,主要同步的問題,
function getAddress(){ let result = prompt('getAddress', JSON.stringify({'info': 'I am JS, want to get Address from Java'})); onAddressResult(JSON.parse(result)) } function onAddressResult(repsonseData){ document.getElementById("show2").innerHTML = "repsonseData from java:\n\n\naddress = " + repsonseData.address document.getElementById("no2").style.display="none" }
然後在 WebView
中就能接受到,
webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onJsPrompt(WebView view, String url, final String message, final String defaultValue, JsPromptResult result) { if (!message.isEmpty()) { JSONObject jsonObject = new JSONObject(); try { jsonObject.put("address", "ShangHai"); } catch (JSONException e) { e.printStackTrace(); } result.confirm(jsonObject.toString()); } return true; } });
通訊方式基本就是上面這樣了,到這裡程式碼就基本都說完了。
4.總結
最後,稍微總結下,篇幅比較多,主要是涉及的內容會多點,有多程序通訊,Hybrid的開發模式還有JS和Java的通訊方式,其實每個點都可以單獨寫一篇部落格,Hybrid的開發模式不止這樣,還可以玩出很多花,比如快取,加快開啟速度等,在具體的工作中具體去解決實際的問題才是真理。
程式碼地址: Hybrid
歡迎Star。
下車嘍。