Android开发经验谈JS与Android NativeAndroid开发

Hybrid模式多进程实践

2018-09-11  本文已影响8人  juexingzhe

在给这篇博客起题目的时候让我很纠结,因为会涉及到下面的知识点:

最后考虑到都是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下的跨进程通信方式:

今天主要用Android推荐的AIDL进行实现,AIDL主要有三个步骤:

  1. 客户端使用bindService方法绑定服务端
  2. 服务端在onBind方法返回Binder对象
  3. 客户端拿到服务端返回的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层,这个后面再说。跨进程通信基本就是这样实现了,再总结下:

  1. 首先通过绑定服务拿到主进程提供的IBinderManager,这样就可以调用它的方法拿到IWebBinder
  2. IWebBinder就可以传过去函数名和函数参数以及回调,调用Native的函 数
    3.回调也是需要跨进程通信的,所以也是一个aidl接口文件
    4.在结构上WebHelper主要用于和WebView以及JS调用封装处理,WebBinderHandler主要用于和主进程进行通信,各司其职。

接下来看看JS和Java层的通信实现细节。

3.Java与JS通信实现

其实这个可以参考我之间的一篇博客,Android与JS之JsBridge使用与源码分析,这里再简单总结下,

js调用Java的方式基本有三种

Java调用JS基本只有一种方式就是loadUrl,

看下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。

下车喽。

上一篇下一篇

猜你喜欢

热点阅读