WebView 性能和用户体验优化

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

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

推荐阅读更多精彩内容