Hybrid模式多进程实践

作者: juexingzhe | 来源:发表于2018-09-11 20:05 被阅读8次

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

    • 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主要有三个步骤:

    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的方式基本有三种

    • 通过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。

    下车喽。

    相关文章

      网友评论

      • b868e930ddbd:还有个问题,将h5资料打包到本地,直接加载感觉还是需要3-4s,这种问题怎么破?
        juexingzhe:@进击的卡拉 耗时在请求数据,可以数据直出
      • b868e930ddbd:打开web进程的activity时,白屏过长。放到主进程就时间短很多,这个问题,请问您是怎么解决的
        juexingzhe:@进击的卡拉 可以提前加载

      本文标题:Hybrid模式多进程实践

      本文链接:https://www.haomeiwen.com/subject/gyxugftx.html