App 中大量 web 页面的使用容易导致 App 内存占用巨大,存在内存泄露,崩溃率高等问题,WebView 独立进程的使用是解决 Android WebView 相关问题的一个合理的方案。
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打开微信小程序时进程列表
以上是微信的进程 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 独立进程的实现比较简单,只需要在 AndroidManifest 中找到对应的 WebViewActivity,对其配置 "android: process" 属性即可。如下:
- <activity
- android:name=".remote.RemoteCommonWebActivity"
- android:configChanges="orientation|keyboardHidden|screenSize"
- android:process=":remoteWeb"/>
首先我们了解下为何两个进程间不能直接通信
image.pngAndroid 多进程的通讯方式有很多种,主要的方式有以下几种:
考虑到 WebView 主要的通讯方式就是方法调用,所以采用 AIDL 方式。AIDL 本质采用的是 Binder 机制,这里贴一张网上的 Binder 机制原理图,具体的 AIDL 的使用方式这里不赘述, 以下是几个核心 AIDL 文件
image.pngIBinderPool: 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, inIWebAidlCallback 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;
- }
- }
一次完整的 Web 页面和 Native 交互过程是这样的:
- public final class JsRemoteInterface {
- @JavascriptInterface
- public void post(String cmd, String param) {
- ...
- }
其中,通用的 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.
来源: https://juejin.im/entry/5a3513daf265da43305e89bf