WebView 性能和用户体验优化

作者: Justson | 来源:发表于2017-08-13 12:49 被阅读2454次

    回顾系统 WebView 进化史

    • 从Android4.4系统开始,Chromium内核取代了Webkit内核。
    • 从Android5.0系统开始,WebView移植成了一个独立的apk,可以不依赖系统而独立存在和更新。
    • 从Android7.0 系统开始,如果用户手机里安装了 Chrome , 系统优先选择 Chrome 为应用提供 WebView 渲染。
    • 从Android8.0系统开始,默认开启WebView多进程模式,即WebView运行在独立的沙盒进程中。

    随着技术的发展 , Google 推出了 PWA Web 形态App ,微信推出小程序 ,Facebook 推出 React , 前端变得越来越广泛(复杂的前端环境) , 所以移动端的 Web 性能变得越来越重要 , 虽然随着 Google 不断的对 WebView 内核升级 , 性能也跟上了脚步 ,但是在移动端还是有很多方面值得我们去优化 。

    内核初始化

    第一次打开 Web 页面 , 使用 WebView 加载页面的时候特别慢 ,第二次打开就能明显的感觉到速度有提升 ,为什么 ? 是因为在你第一次加载页面的时候 WebView 内核并没有初始化 , 所以在第一次加载页面的时候需要耗时去初始化 WebView 内核 。提前初始化 WebView 内核 ,例如如下把它放到了 Application 里面去初始化 , 在页面里可以直接使用该 WebView

    public class App extends Application {
    
        private WebView mWebView ;
        @Override
        public void onCreate() {
            super.onCreate();
            mWebView = new WebView(new MutableContextWrapper(this));
            
        }
    
    }
    

    复用 WebView

    复用思想在移动端是一种很重要的思想 , 像 ListView ,RecyclerView 复用子View 一样 , 大大提高了性能和节俭内存 , 如果你大量使用 WebView 那么我建议你可以考虑一下复用 WebView , 如果你的应用只是在某些页面使用了 WebView 那么我建议你放弃复用 WebView , 因为复用 WebView 并不会给你带来多大的性能提升而且会带来一些问题 ,而且在内存吃紧移动端 ,内存显得特别珍贵 , 下面给出一些测试代码和数据。

    验证复用 WebView 和提前初始化 WebView 必要性

    private void testWebViewInitUsedTime(){
            long p = System.currentTimeMillis();
            WebView mWebView = new WebView(this);
            long n = System.currentTimeMillis();
            Log.i("Info", "testWebViewFirstInit use time:" + (n-p));
        }
    
     testWebViewInitUsedTime();
     testWebViewInitUsedTime();
    
    //测试环境 Android 7.0  三星S7
    testWebViewFirstInit use time:182
    testWebViewFirstInit use time:4
    

    上面是测试 WebView 初始耗时的一些代码 , 可以看出第一次提前初始化还是很有必要的 , 第二初始化只耗时 4 毫秒 , 也就是说一般情况创建一个 WebView 只需要4毫秒 ,如果单纯几个页面是复用 WebView 这种优化意义不大 , 因为稍微处理不妥当就会出现泄漏 。

    下面给出复用 WebView 的一些关键代码

    public class WebPools {
    
    
        private final Queue<WebView> mWebViews;
    
        private Object lock = new Object();
        private static WebPools mWebPools = null;
    
        private static final AtomicReference<WebPools> mAtomicReference = new AtomicReference<>();
        private static final String TAG=WebPools.class.getSimpleName();
    
        private WebPools() {
            mWebViews = new LinkedBlockingQueue<>();
        }
    
    
        public static WebPools getInstance() {
    
            for (; ; ) {
                if (mWebPools != null)
                    return mWebPools;
                if (mAtomicReference.compareAndSet(null, new WebPools()))
                    return mWebPools=mAtomicReference.get();
    
            }
        }
    
    
        public void recycle(WebView webView) {
            recycleInternal(webView);
        }
    
    
    
        public WebView acquireWebView(Activity activity) {
            return acquireWebViewInternal(activity);
        }
    
        private WebView acquireWebViewInternal(Activity activity) {
    
            WebView mWebView = mWebViews.poll();
    
            LogUtils.i(TAG,"acquireWebViewInternal  webview:"+mWebView);
            if (mWebView == null) {
                synchronized (lock) {
                    return new WebView(new MutableContextWrapper(activity));
                }
            } else {
                MutableContextWrapper mMutableContextWrapper = (MutableContextWrapper) mWebView.getContext();
                mMutableContextWrapper.setBaseContext(activity);
                return mWebView;
            }
        }
    
    
    
        private void recycleInternal(WebView webView) {
            try {
    
                if (webView.getContext() instanceof MutableContextWrapper) {
    
                    MutableContextWrapper mContext = (MutableContextWrapper) webView.getContext();
                    mContext.setBaseContext(mContext.getApplicationContext());
                    LogUtils.i(TAG,"enqueue  webview:"+webView);
                    mWebViews.offer(webView);
                }
                if(webView.getContext() instanceof  Activity){
    //            throw new RuntimeException("leaked");
                    LogUtils.i(TAG,"Abandon this webview  , It will cause leak if enqueue !");
                }
    
            }catch (Exception e){
                e.printStackTrace();
            }
    
    
        }
    
    }
    

    注意在 WebView 进入 WebPools 之前 , 需要重置 WebView ,包括清空注入 WebView 的注入对象 , 否则非常容易泄露。

    WebView 独立进程 , 进程预加载 。

    因为 WebView 内存泄露 , 以及多进程内存拓展 , 相信有一部分开发人员会把 WebView 放在一个独立的进程里面 , 那么第一次加载 WebView 页面 ,加上系统需要时间 Fork 出新进程 , 那么加载变得更慢了 , 因为进程的创建也是一件耗时的事情 , 所谓的预加载进程 , 就是提前把进程创建出来 , 提升加载速度 ,大致的做法如下

            <service
                android:name=".PreWebService"
                android:process=":web"/>
            <activity
                android:name=".WebActivity"
                android:process=":web"
                />
    
    

    其实不一定要 Service , 启动「web」 进程 Broadcast 广播也是可以的 , 提前在进入 WebView 页面之前 , 先启动 PreWebService 把 「web」 进程创建了 ,当系统在启动 WebActivity 的时候 , 系统发现了 「web」 进程已经创建存在了 , 系统就不需要耗费时间 Fork 出新的「web」进程了。

    提前显示进度条

    提前显示进度条不是提升性能 , 但是对用户体验来说也是很重要的一点 , WebView.loadUrl("url") 不会立马就回调 onPageStarted 或者 onProgressChanged 因为在这一时间段 , WebView 有可能在初始化内核 , 也有可能在与服务器建立连接 , 这个时间段容易出现白屏 , 白屏用户体验是很糟糕的 , 所以我建议

    private void go(String url) {
            this.mWebView.loadUrl(url); 
            this.mIndicator.show() //显示进度条
    }
    

    loadUrl 之后立马就把进度条显示出来 , 给用户一个明显视觉 。

    开启软硬件加速

    开启软硬件加速这个性能提升还是很明显的,但是会耗费更大的内存 。

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
     } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
                webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }
    

    总结

    上面提出这些性能优化都不是那么完美无缺的,基本都会带来一部分系统资源的消耗 , 比如在 Application 里面提前初始化WebView , 虽然提升了 WebView 页面的启动速度, 但是缺拖慢了 App 的冷启动速度 ,独立进程和开启软硬件加速也都会带来内存更大的开销 ,所以凡事都是存在利和弊,至于在项目中利与弊怎么权衡,都是需要根据用户需求和各种因素来量度的。

    最后

    留下一个基于 WebView 的强大库的传送门 GitHub

    相关文章

      网友评论

      • 浮华染流年:怎么加载本地Html 片段
      • FynnJason:怎么加载本地html?
      • 于是zb:请问一下,webview返回上一个html,怎么保留之前的位置
      • 李争献:回收webview实例之前,要不要调用它的destroy方法呢,调用后不能再进行loadUrl了,但是不调用的话,webview还会在后台运行是不是呢?
      • 我姓连:mAgentWeb.getAgentWebSettings().getWebSettings().setSupportZoom(true);
        mAgentWeb.getAgentWebSettings().getWebSettings().setBuiltInZoomControls(true);
        mAgentWeb.getAgentWebSettings().getWebSettings().setDisplayZoomControls(true);
        mAgentWeb.getAgentWebSettings().getWebSettings().setTextZoom(100);
        还是不支持缩放!!!
      • HOPE545:https://github.com/Justson/AgentWeb/issues/323
        麻烦看下这个问题,谢谢!
      • 347a15966ebe:楼主,我用这个怎么缩放没效果吗
        我姓连:是的!我也遇到这个问题
      • rivenlee:博主不就是AgentWeb的作者么, 这个库只要一查web, 肯定哪都有, 厉害! 应该是做的很厉害哟, 我真打算入手呢,
      • AWeiLoveAndroid:膜拜大神:+1:
        rivenlee:真是哪里都能看到你啊~我的天
      • edc81fe714ab:谢谢分享

      本文标题:WebView 性能和用户体验优化

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