独立进程 WebView 的思路


WebView 是个占内存大户,它和图片是OOM的两大元凶,图片一般使用三方库来加载,统一管理app中图片的内存,那么 WebView 呢,对于它又怎么处理?一般情况都是在 Activity 中使用到它时,动态的 new 一个webview,然后通过代码 addView() 方法添加到容器中,在 Activity 销毁时,把它从容器中 removeView() 掉,同时 load() 个空字符串,以减少内存的泄漏。

app可以有多个进程,一个主进程,其它的属于子进程,每个进程的内存上限是固定的,理论上进程越多,可以承受的内存就越大。但多进程也有坏处,由于进程之间的数据不能直接通用,此时单例等数据就会失效,进程间通信被称之为跨进程,跨进程有几种方案,之前的博客介绍过binder机制,通过aidl来实现跨进程通讯。多进程有利有弊,综合考量下来,把webview单独放到一个进程中,是很有必要的。还有个好处是,如果子进程崩溃了,主进程依然能继续运行,不会导致App崩溃。

把webview放到一个单独的进程中比较容易,我们可以用Fragment来封装一个WebView,然后用一个Activity来承载该Fragment,只需要在配置清单中,在此Activity的注册的节点中,重写 android:process 属性即可,我们可以自己定义名字,这样就跨进程了。


比如说主进程为A,子进程为B,之前讲解aidl时,是在主进程中注册Service服务,把Service定义为另外一个进程,通过定义aidl,绑定Binder来实现数据的通讯,在Activity中把数据传递给Service,然后获取到service返回的值,在Activity中进行下一步的操作;Service也可以是其他app的,也是通过aidl来进行通讯,常见的就是在自己的app中接入支付宝或微信支付,也是跨进程一通操作。回到WebView独立进程的话题中,此时主进程A和WebView进程B交互,也用binder机制,但是要注意一点,此时是在子进程B中来注册绑定Service与主进程A交互,Service是属于主进程的,这里和之前用aidl交互是颠倒的,这里想通了,后续操作就简单了。

 

    interface IWebInterface {
        String handleWeb(String name, String jsonParams);
    }


    
定义了上面的aidl,到此,算是完成了三分之一,看似子进程B和主进程A可以交互了,但这里存在些问题,如果交互是耗时的,是不是每次子进程给主进程传递数据并等待数据返回时,都要开启一个线程?这样的操作怪怪的,有没有其它方法?aidl 也是有接口回调概念的,跨进程可以传递的除了数据基本类型,还有序列化对象,甚至还有接口,那么,我们可以对上面的做个改进,定义个接口,然后把接口作为个参数传递进去,

    interface IWebCallback {
        void onResult(int code, String name, String response);
    }

    interface IWebInterface {
        void handleWeb(String name, String jsonParams, IWebCallback callback);
    }

在AS的 aidl 文件夹下,创建这两个aidl文件,AS会自动替我们创建好对应的类;通过Context来调用 bindService() 方法时,有些注意事项,如果绑定后服务断开了,我们需要重新绑定,IBinder 有个linkToDeath() 方法给我们监听,当binder断开了会触发此回调,我们可以在这个方法中主动断开然后重连;还有个要注意的事是阻塞问题,bindService() 方法是异步的,为了保证绑定成功后再执行其他的逻辑,我们可以借助并发工具类 CountDownLatch,先写个简单的绑定例子

/**
 * 这里是主进程
 */
public class MainProAidlInterface extends IWebInterface.Stub {

    private Context context;

    public MainProAidlInterface(Context context) {
        this.context = context;
    }


    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

    }

    @Override
    public void handleWeb(String name, String jsonParams, IWebCallback callback) throws RemoteException {
        try {
            handleRemoteAction(name, jsonParams, callback);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void handleRemoteAction(final String name, String params, final IWebCallback callback) throws Exception {
        if(!TextUtils.isEmpty(name) && name.length() > 1){
            if(callback != null){
                callback.onResult(1, name, name +"," + params);
            }
        }
    }


}

public class MainProHandleRemoteService extends Service {

    private Context context;

    @Override
    public void onCreate() {
        super.onCreate();
        context = this;
    }

    @Override
    public IBinder onBind(Intent intent) {
        int pid = android.os.Process.myPid();
        Binder binder = new MainProAidlInterface(context);
        return binder;
    }
}


public class RemoteWebBinder {


    private Context mContext;
    private IWebInterface mWebBinder;
    private static volatile RemoteWebBinder sInstance;
    private CountDownLatch mConnectBinderPoolCountDownLatch;

    public RemoteWebBinder(Context context) {
        this.mContext = context;
        connectBinderService();
    }

    public static RemoteWebBinder getInstance(Context context) {
        if (sInstance == null) {
            synchronized (RemoteWebBinder.class) {
                if (sInstance == null) {
                    sInstance = new RemoteWebBinder(context);
                }
            }
        }
        return sInstance;
    }

    private void connectBinderService() {
        mConnectBinderPoolCountDownLatch = new CountDownLatch(1);
        Intent service = new Intent(mContext, MainProHandleRemoteService.class);
        mContext.bindService(service, mBinderConnection, Context.BIND_AUTO_CREATE);
        try {
            mConnectBinderPoolCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


    private ServiceConnection mBinderConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mWebBinder = IWebInterface.Stub.asInterface(service);
            try {
                mWebBinder.asBinder().linkToDeath(mBinderDeathRecipient, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            mConnectBinderPoolCountDownLatch.countDown();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    private IBinder.DeathRecipient mBinderDeathRecipient = new IBinder.DeathRecipient() {    // 6
        @Override
        public void binderDied() {
            mWebBinder.asBinder().unlinkToDeath(this, 0);
            mWebBinder = null;
            connectBinderService();
        }
    };


    public IBinder getBinder(){
        return mWebBinder.asBinder();
    }

    public IWebInterface getIWebInterface(){
        return mWebBinder;
    }


}

这里显示的是绑定Binder,跨进程就是通过它传递数据的,那再看看webview,这里写个例子,自己找了个html,然后稍加修改,把它放到 assets 文件夹中,命名为 wxremote.html
 

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <script type="text/javascript">
    function actionFromNative(){
         document.getElementById("log_msg").innerHTML +=
             "<br\>Native调用了js函数";
    }

    function actionFromNativeWithParam(arg){
         document.getElementById("log_msg").innerHTML +=
             ("<br\>Native调用了js函数并传递参数:"+arg);
    }

    </script>
</head>
<body>
<p>WebView与Javascript交互</p>
<div>
    <button onClick="window.wx.post('1','come Js')">点击调用Native代码</button>
</div>
<br/>
<div>
    <button onClick="window.wx.post('11','come from Js')">点击调用Native代码并传递参数</button>
</div>
<br/>

<br/>
<div id="log_msg">调用打印信息</div>
</body>

然后定义一个 WebView,做简单的封装

public class DWebView extends WebView {
    public DWebView(Context context) {
        this(context, null);
    }

    public DWebView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    protected Context context;
    private DWebViewCallBack dWebViewCallBack;

    private void init(Context context){
        this.context = context;

        addJavascriptInterface(this, "wx");
        getSettings().setJavaScriptEnabled(true);


    }

    @JavascriptInterface
    public void post(final String cmd, final String param) {
        post(new Runnable() {
            @Override
            public void run() {
                if(dWebViewCallBack != null){
                    dWebViewCallBack.exec(context, cmd, param, DWebView.this);
                }
            }
        });
    }


    @JavascriptInterface
    public void post(final String name) {
        post(new Runnable() {
            @Override
            public void run() {
                if(dWebViewCallBack != null){
                    dWebViewCallBack.exec(context, name, "", DWebView.this);
                }
            }
        });
    }

    public void handleCallback(String response) {
        if (!TextUtils.isEmpty(response)) {
            String trigger = "javascript:" + "actionFromNativeWithParam" + "('" + response + "')";
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                evaluateJavascript(trigger, null);
            } else {
                loadUrl(trigger);
            }
        }
    }

    public void setWebViewCallBack(DWebViewCallBack wWebViewCallBack) {
        this.dWebViewCallBack = wWebViewCallBack;
    }
    
    public interface DWebViewCallBack {
        void exec(Context context, String name, String params, WebView webView);
    }

}


然后就是定义一个 Activity,这里先假设它是在主进程中,

public class WebRemoteActivity extends Activity implements DWebViewCallBack {

    DWebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_remote);

        webView = (DWebView) findViewById(R.id.webview);
        webView.setWebViewCallBack(this);
        loadUrl();
    }

    protected void loadUrl() {
        webView.loadUrl("file:///android_asset/wxremote.html");
    }

    @Override
    public void exec(Context context, String name, String params, WebView webView) {
        CommandDispatcher.getInstance().exec(context, name, params, webView);
    }
}

activity_web_remote:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="30dp"
    android:orientation="vertical">

    <remote.DWebView
        android:id="@+id/webview"
        android:layout_width="wrap_content"
        android:layout_height="300dp"
        />

</LinearLayout>


DWebView 和 WebRemoteActivity 通过定义的接口 DWebViewCallBack 产生了关联,webview中的按钮被点击后,会执行注入的 post() 方法,然后通过回调执行到Activity中的方法;如果此时我们在配置清单中 WebRemoteActivity 的节点中重写 android:process 属性,则可以把它变为子进程,此时,如何跨进程?这时候再写个中间类,做事件的分发,同时判断是否需要跨进程或直接在子进程中执行逻辑。


此时就需要在Binder注册好之后再执行 loadUrl() 方法,并且webview调用原生的 exec() 方法,要在这个里面进行判断,是否需要跨进程,如果需要,则通过IWebInterface来传输数据,然后我们在主进程中做相应的数据处理,或保存到内存或本地,或上报接口等操作,数据处理完了,就触发回调,把数据等相关信息重新传递给子进程,子进程接到回调后,继续执行自己的逻辑,通知webview做下一步操作。

public class CommandDispatcher {
    private final static String TAG = "CommandDispatcher";

    private static CommandDispatcher instance;
    private Handler mHandler = new Handler(Looper.getMainLooper());

    // 实现跨进程通信的接口
    protected IWebInterface webAidlInterface;

    private CommandDispatcher() {

    }

    public static CommandDispatcher getInstance() {
        if (instance == null) {
            synchronized (CommandDispatcher.class) {
                if (instance == null) {
                    instance = new CommandDispatcher();
                }
            }
        }
        return instance;
    }


    public void initAidlConnect(final Context context, final Action action) {
        if (webAidlInterface != null) {
            if (action != null) {
                action.call(null);
            }
            return;
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                RemoteWebBinder binderPool = RemoteWebBinder.getInstance(context);
                webAidlInterface = binderPool.getIWebInterface();
                if (action != null) {
                    action.call(null);
                }
            }
        }).start();
    }


    public void exec(Context context, String name, String params, WebView webView){
        try {
            execNonUI(context, name, params, webView);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void execNonUI(Context context, String name, String params, final WebView webView) throws Exception {

        if (inMainProcess(context, android.os.Process.myPid()) || name.length() < 2) {
            handleCallback(0, name, params, webView);
        } else {
            if (webAidlInterface != null) {
                webAidlInterface.handleWeb(name, params, new IWebCallback.Stub() {
                    @Override
                    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws
RemoteException {

                    }

                    @Override
                    public void onResult(int responseCode, String actionName, String response) throws RemoteException {
                        handleCallback(responseCode, actionName, response, webView);
                    }
                });
            }
        }
    }

    private void handleCallback(final int responseCode, final String name, final String response,
                                final WebView webView) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (webView instanceof DWebView) {
                    ((DWebView) webView).handleCallback(response);
                }
            }
        });
    }

 
    public static boolean inMainProcess(Context context, int pid) {
        String packageName = context.getPackageName();
        String processName = getProcessName(context,pid);
        return packageName.equals(processName);
    }

 
    public static String getProcessName(Context context, int pid) {

        // ActivityManager
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
        if (runningApps == null) {
            return null;
        }
        for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
            if (procInfo.pid == pid) {
                return procInfo.processName;
            }
        }
        return null;
    }

    public void runOnUiThread(Runnable runnable){
        mHandler.post(runnable);
    }
}


execNonUI() 方法中做了简单的逻辑区分,实际中比这里要复杂,可以定义好规则,比如以remote开头的方法名需要跨进程,这个还是根据业务需要定义的。以上就是个很简单粗糙的例子,实际使用中还是需要逐步完善的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值