WebView 性能和用户体验优化

2字数 1301阅读 12314

回顾系统 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

推荐阅读更多精彩内容