在给这篇博客起题目的时候让我很纠结,因为会涉及到下面的知识点:
- 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;
import com.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", "test@baidu.com");
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通信实现
其实这个可以参考我之间的一篇博客,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。
下车喽。
网友评论