Android WebView独立进程解决方案

App中大量Web页面的使用容易导致App内存占用巨大,存在内存泄露,崩溃率高等问题,WebView独立进程的使用是解决Android WebView相关问题的一个合理的方案。

为什么要采用WebView独立进程

Android WebView的问题

  1. WebView导致的OOM问题
  2. Android版本不同,采用了不同的内核,兼容性Crash
  3. WebView代码质量,WebView和Native版本不一致,导致Crash

Android App进程的内存使用是有限制的,通过以下两个方法可以查看App可用内存的大小:

ActivityManager.getMemoryClass()获得正常情况下可用内存的大小
ActivityManager.getLargeMemoryClass()可以获得开启largeHeap最大的内存大小

以Google Nexus 6P为例,正常情况下可用内存是192M,最大可用内存是512M。

Android WebView内存占用很大,在低配置手机上,常有这样的场景发生:连续开启多个WebView页面,此时栈底的Activity被销毁了,返回时Activity需要重新创建;或者连续开启多个Webview页面,App中的某些单例对象被系统回收,此时如果未做特殊处理,就容易报数据空指针错误。这些都是WebView导致的OOM的表现。WebView独立进程可以避免对主进程内存的占用。

问题2 3也是Webview容易发生的,国产手机各家的内核都不太一样,Web页面兼容没有做到位容易导致Crash;随着App版本和App-Web版本发布相互独立,web页面对native的依赖会变得复杂,版本兼容性没有做好,也会导致webview与native进行交互时发生crash。

微信经验介绍

启动微信时进程列表


打开微信公众号时进程列表


image.png

打开微信小程序时进程列表


image.png

以上是微信的进程list,简单分析一下各个进程的功能如下:
com.tencent.mm :微信主进程,会话和朋友圈相关
com.tencent.mm:push :微信push, 保活
com.tencent.mm:tools 和 com.tencent.mm:support : 功能支持,比如微信中打开一个独立网页是在tools进程中
com.tencent.mm:exdevice :估计是监控手机相关的
com.tencent.mm:sandbox :公众号进程,公众号打开的页面都是在该进程中运行
com.tencent.mm:appbrand :适用于小程序,测试发现微信每启动一个小程序,都会建立一个独立的进程 appbrand[0], 最多开5个进程

微信通过这样的进程分离,将网页、公众号、小程序分别分离到独立进程中,有效的增加了微信的内存使用,避免了WebView对主进程内存的占用导致的主进程服务异常;同时也通过这种独立进程方案避免了质量参差不齐的公众号网页和小程序Crash影响主进程的运行。由此可见,WebView独立进程方案是可行的,也是必要的。

如何实现WebView独立进程

WebView独立进程的实现

WebView独立进程的实现比较简单,只需要在AndroidManifest中找到对应的WebViewActivity,对其配置"android: process"属性即可。如下:

<activity
    android:name=".remote.RemoteCommonWebActivity"
    android:configChanges="orientation|keyboardHidden|screenSize"
    android:process=":remoteWeb"/>

WebView进程与主进程间的数据通信

首先我们了解下为何两个进程间不能直接通信


image.png

Android多进程的通讯方式有很多种,主要的方式有以下几种:

  1. AIDL
  2. Messenger
  3. ContentProvider
  4. 共享文件
  5. 组件间Bundle传递
  6. Socket传输

考虑到WebView主要的通讯方式就是方法调用,所以采用AIDL方式。AIDL本质采用的是Binder机制,这里贴一张网上的Binder机制原理图,具体的AIDL的使用方式这里不赘述, 以下是几个核心AIDL文件

image.png

IBinderPool: Webview进程和主进程的通讯可能涉及到多个AIDL Binder,从功能上来讲,我们也会把不同功能的接口写成不同的AIDL Binder,所以IBinderPool用于满足调用方根据不同类型获取不同的Binder。

interface IBinderPool {
    IBinder queryBinder(int binderCode);  //查找特定Binder的方法
}

IWebAidlInterface: 最核心的AIDL Binder,这里把WebView进程对主进程的每一个调用看做一次action, 每个action都会有唯一的actionName, 主进程会提前注册好这些action,action 也有级别level,每次调用结束通过IWebAidlCallback返回结果

interface IWebAidlInterface {
    
    /**
     * actionName: 不同的action, jsonParams: 需要根据不同的action从map中读取并依次转成其他
     */
    void handleWebAction(int level, String actionName, String jsonParams, in IWebAidlCallback callback);

 }

IWebAidlCallback: 结果回调

interface IWebAidlCallback {
    void onResult(int responseCode, String actionName, String response);
}

为了维护独立进程和主进程之间的连接,避免每次aidl调用时都去重新进行binder连接和获取,需要专门提供一个类去维护连接,并根据条件返回Binder. 这个类就叫做 RemoteWebBinderPool

public class RemoteWebBinderPool {

    public static final int BINDER_WEB_AIDL = 1;

    private Context mContext;
    private IBinderPool mBinderPool;
    private static volatile RemoteWebBinderPool sInstance;
    private CountDownLatch mConnectBinderPoolCountDownLatch;

    private RemoteWebBinderPool(Context context) {
        mContext = context.getApplicationContext();
        connectBinderPoolService();
    }

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

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

    public IBinder queryBinder(int binderCode) {
        IBinder binder = null;
        try {
            if (mBinderPool != null) {
                binder = mBinderPool.queryBinder(binderCode);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return binder;
    }

    private ServiceConnection mBinderPoolConnection = new ServiceConnection() {   // 5

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinderPool = IBinderPool.Stub.asInterface(service);
            try {
                mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            mConnectBinderPoolCountDownLatch.countDown();
        }
    };

    private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() {    // 6
        @Override
        public void binderDied() {
            mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0);
            mBinderPool = null;
            connectBinderPoolService();
        }
    };

    public static class BinderPoolImpl extends IBinderPool.Stub {

        private Context context;

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

        @Override
        public IBinder queryBinder(int binderCode) throws RemoteException {
            IBinder binder = null;
            switch (binderCode) {
                case BINDER_WEB_AIDL: {
                    binder = new MainProAidlInterface(context);
                    break;
                }
                default:
                    break;
            }
            return binder;
        }
    }

}

从代码中可以看到这个连接池连接的是主进程 MainProHandleRemoteService.

public class MainProHandleRemoteService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Binder mBinderPool = new RemoteWebBinderPool.BinderPoolImpl(context);
        return mBinderPool;
    }
}

Native-Web交互和接口管理

一次完整的Web页面和Native交互过程是这样的:

  1. Native打开页面时注册接口:“webView.addJavascriptInterface(jsInterface, "webview");” 其中jsInterface是JsRemoteInterface类的实例:
public final class JsRemoteInterface {
    @JavascriptInterface
    public void post(String cmd, String param) {
        ...
    }
  1. Web页面通过“window.webview.post(cmd,JSON.stringify(para))”调用native;
  2. Native(即Webview进程)收到调用之后,通过IWebAidlInterface实例传递给主进程执行;
  3. 主进程收到action请求之后,根据actionname分发处理,执行结束之后通过IWebAidlCallback完成进程间回调。

其中,通用的Action结构如下:

public interface Command {

    String name();

    void exec(Context context, Map params, ResultBack resultBack);
}

根据不同的Level将所有的command提前注册好, 以BaseLevelCommand为例:

public class BaseLevelCommands {

    private HashMap<String, Command> commands;
    private Context mContext;

    public BaseLevelCommands(Context context) {
        this.mContext = context;
        registerCommands();
    }

    private void registerCommands() {
        commands = new HashMap<>();
        registerCommand(playVideoByNativeCommand);
    }

    private Command playVideoByNativeCommand = new Command() {
        @Override
        public String name() {
            return "videoPlay";
        }

        @Override
        public void exec(Context context, Map params, ResultBack resultBack) {
            if (params != null) {
                String videoUrl = (String) params.get("url");
                if (!TextUtils.isEmpty(videoUrl)) {
                    String suffix = videoUrl.substring(videoUrl.lastIndexOf(".") + 1);
                    DJFullScreenActivity.startActivityWithLanscape(context, videoUrl, DJFullScreenActivity.getVideoType(suffix), DJVideoPlayer.class, " ");
                }
            }
        }
    };
}

CommandsManager 负责分发action,结构如下:

public class CommandsManager {

    private static CommandsManager instance;

    public static final int LEVEL_BASE = 1; // 基础level
    public static final int LEVEL_ACCOUNT = 2; // 涉及到账号相关的level

    private Context context;
    private BaseLevelCommands baseLevelCommands;
    private AccountLevelCommands accountLevelCommands;

    private CommandsManager(Context context) {
        this.context = context;
        baseLevelCommands = new BaseLevelCommands(context);
        accountLevelCommands = new AccountLevelCommands(context);
    }

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

    public void findAndExec(int level, String action, Map params, ResultBack resultBack) {
        boolean methodFlag = false;
        switch (level) {
            case LEVEL_BASE: {
                if (baseLevelCommands.getCommands().get(action) != null) {
                    methodFlag = true;
                    baseLevelCommands.getCommands().get(action).exec(context, params, resultBack);
                }
                if (accountLevelCommands.getCommands().get(action) != null) {
                    AidlError aidlError = new AidlError(RemoteActionConstants.ERRORCODE.NO_AUTH, RemoteActionConstants.ERRORMESSAGE.NO_AUTH);
                    resultBack.onResult(RemoteActionConstants.FAILED, action, aidlError);
                }
                break;
            }
            case LEVEL_ACCOUNT: {
                if (baseLevelCommands.getCommands().get(action) != null) {
                    methodFlag = true;
                    baseLevelCommands.getCommands().get(action).exec(context, params, resultBack);
                }
                if (accountLevelCommands.getCommands().get(action) != null) {
                    methodFlag = true;
                    accountLevelCommands.getCommands().get(action).exec(context, params, resultBack);
                }
                break;
            }
        }
        if (!methodFlag) {
            AidlError aidlError = new AidlError(RemoteActionConstants.ERRORCODE.NO_METHOD, RemoteActionConstants.ERRORMESSAGE.NO_METHOD);
            resultBack.onResult(RemoteActionConstants.FAILED, action, aidlError);
        }
    }

}

描述可能有些不清楚的地方,更详细的源码请转到 github webprogress: Android WebView独立进程解决方案,欢迎大家star.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335