Android WebView独立进程解决方案

作者: 浪淘沙xud | 来源:发表于2017-12-15 16:57 被阅读2340次

    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.

    相关文章

      网友评论

      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/gz1kbl 欢迎点赞支持!
        欢迎订阅《徐卫东的独家号》https://toutiao.io/subjects/154145
      • 1琥珀川1:有几个问题
        1.action有必要跨进程交互吗
        2.js可以调用native,这方案native能调用js吗?
        浪淘沙xud:action是否要跨进程调用取决于action要做怎样的操作,如果action要获取或者修改主进程的数据,就需要跨进程,在我的源码中,是同时支持wevview进程和主进程交互的;native可以调用js,方法层面就是webView.evaluateJavascript 和 webView.loadUrl。
      • android_cyw:“Android WebView内存占用很大,在低配置手机上,常有这样的场景发生:连续开启多个WebView页面,此时栈底的Activity被销毁了,返回时Activity需要重新创建;” 系统一般不会销毁一个单独的Activity吧?
        浪淘沙xud:@heavyRain 存在这种可能的,系统在内存不足时通常采用以下两种策略来回收内存:1)杀掉处于Paused状态或Stoped状态的Activity,2)或者直接杀掉该Activity所在的Process. 经过实际测试和查验,在原生Android中采用的是第二种策略;在实际测试中发现oppo机型两个策略都会采用。
        heavyRain:系统回收是按进程来的吧,如果我没记错的话。
        浪淘沙xud:@android_cyw 会的 已经在国产机体验过了

      本文标题:Android WebView独立进程解决方案

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