Android WebView开发:WebView性能优化

Posted 红日666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android WebView开发:WebView性能优化相关的知识,希望对你有一定的参考价值。

一、Android WebView开发(一):基础应用
二、Android WebView开发(二):WebView与Native交互
三、Android WebView开发(三):WebView性能优化
四、Android WebView开发(四):WebView独立进程解决方案
五、Android WebView开发(五):自定义WebView工具栏


附GitHub源码:WebViewExplore


WebView性能优化方案:

1、WebView预初始化:

为了减少WebView的性能损耗,我们可以在合适时机提前创建好WebView,并存入缓存池,当页面需要显示内容时,直接从缓存池获取创建好的WebView,根据性能数据显示,WebView预创建可以减少首屏渲染时间200ms+。

以新闻落地页为例,当用户进入新闻列表页时,我们会创建第一个WebView,当用户进入新闻落地页后,会从缓存池中取出来渲染H5页面,为了不影响页面的加载速度,同时保证下次进入落地页缓存池中仍然有可用的WebView组件,我们会在每次页面加载完成(pageFinish)或者back退出落地页的时机,去触发预创建WebView的逻辑。

由于WebView的初始化需要和context进行绑定,若想实现预创建的逻辑,需要保证context的一致性,常规做法我们考虑可以用fragment来实现承载H5页面的容器,这样context可以用外层的activity实例,但Fragment本身的切换流畅度存在一定问题,并且这样做限定了WebView预创建适用的场景。为此,我们找到了一种更加完美的替代方案,即:MutableContextWrapper新的context包装类,允许外部修改它的baseContext,并且所有ContextWrapper调用的方法都会代理到baseContext来执行

下面是一段预创建WebView的代码:


/**
     * 创建WebView实例
     * 用了applicationContext
     */
    @DebugTrace
    public void prepareNewWebView() 
        if (mCachedWebViewStack.size() < CACHED_WEBVIEW_MAX_NUM) 
            mCachedWebViewStack.push(new WebView(new MutableContextWrapper(getAppContext())));
        
    
    /**
     * 从缓存池中获取合适的WebView
     * 
     * @param context activity context
     * @return WebView
     */
    private WebView acquireWebViewInternal(Context context) 
        // 为空,直接返回新实例
        if (mCachedWebViewStack == null || mCachedWebViewStack.isEmpty()) 
            return new WebView(context);
        
        WebView webView = mCachedWebViewStack.pop();
        // webView不为空,则开始使用预创建的WebView,并且替换Context
        MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
        contextWrapper.setBaseContext(context);
        return webView;
    

2、关联的Native组件懒加载:

WebView初始化完成,可以立刻loadUrl,无需等待框架onCreate或者OnResume结束。另外WebView初始完成后到页面首页绘制完成之间,尽量减少UI线程的其他操作,繁忙的UI线程会拖慢WebView.loadUrl的速度。

具体到我们新闻落地页场景,由于我们的落地页包含两部分,WebView+Native评论组件,正常流程会在WebView初始化结束后,开始评论组件的初始化及评论数据的获取。由于此时评论的初始化仍处在onCreate的UI消息处理中,会严重延迟内核加载主文档的逻辑。考虑到用户进入落地页的时候,评论组件对用户来说并不可见,所以将评论组件的初始化延迟到页面的pageFinish时或首屏渲染完成时。

3、离线缓存:

WebSettings settings = webView.getSettings();
settings.setAppCacheEnabled(true);
settings.setDatabaseEnabled(true);
settings.setDomStorageEnabled(true);//开启DOM缓存,关闭的话H5自身的一些操作是无效的
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setjavascriptEnabled(true);

这边我们通过setCacheMode方法来设置WebView的缓存策略,WebSettings.LOAD_DEFAULT是默认的缓存策略,它在缓存可获取并且没有过期的情况下加载缓存,否则通过网络获取资源。这样的话可以减少页面的网络请求次数,那我们如何在离线的情况下也能打开页面呢,这里我们在加载页面的时候可以通过判断网络状态,在无网络的情况下更改webview的缓存策略

ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if(info.isAvailable())

    settings.setCacheMode(WebSettings.LOAD_DEFAULT);
else 

    settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);//不使用网络,只加载缓存

这样我们就可以使我们的混合应用在没有网络的情况下也能使用一部分的功能,不至于什么都显示不了了,当然如果我们将缓存做的更好一些,在网络好的时候,比如说在WIFI状态下,去后台加载一些网页缓存起来,这样处理的话,即使在无网络情况下第一次打开某些页面的时候,也能将该页面显示出来。
当然缓存资源后随之会带来一个问题,那就是资源无法及时更新,WebSettings.LOAD_DEFAULT中的页面中的缓存版本好像不是很起作用,所以我们这边可能需要自己做一个缓存版本控制。这个缓存版本控制可以放在APP版本更新中。

if (upgrade.cacheControl > cacheControl)

    webView.clearCache(true);//删除DOM缓存
    VersionUtils.clearCache(mContext.getCacheDir());//删除APP缓存
    try
    
        mContext.deleteDatabase("webview.db");//删除数据库缓存
        mContext.deleteDatabase("webviewCache.db");
    
    catch (Exception e)
    
    

4、资源预(异步)加载:

1、通过shouldInterceptRequest(预)异步加载:

有时候一个页面资源比较多,图片,CSS,js比较多,还引用了JQuery这种庞然巨兽,从加载到页面渲染完成需要比较长的时间,解决方案就是可以按照一定的策略和时机,提前从CDN中请求部分落地页html缓存到本地,或者可以把html,css,js,image等资源预置在客户端本地,并和服务端协商好前端的版本控制和增量更新策略,如此一来Webview就可以先快速加载本地缓存页面资源,加载完成后,返回给WebView。剩下的就只需要拉取那些需要更新的增量资源即可。这样可以提升加载速度也能减少服务器压力。重写WebClient类中的 shouldInterceptRequest 【运行在子线程】方法,再将这个类设置给WebView。

        /**
         * 【实现预加载】
         * 有时候一个页面资源比较多,图片,CSS,js比较多,还引用了JQuery这种庞然巨兽,
         * 从加载到页面渲染完成需要比较长的时间,有一个解决方案是将这些资源打包进APK里面,
         * 然后当页面加载这些资源的时候让它从本地获取,这样可以提升加载速度也能减少服务器压力。
         */
        @Nullable
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) 
            if (request == null) 
                return null;
            
            String url = request.getUrl().toString();
            Log.d(TAG, "shouldInterceptRequest---> " + url);
            return getWebResourceResponse(url);
        

        protected WebResourceResponse getWebResourceResponse(String url) 
            //此处[tag]等需要跟服务端协商好,再处理
            if (url.contains("[tag]")) 
                try 
                    String localPath = url.replaceFirst("^http.*[tag]\\\\]", "");
                    InputStream is = getContext().getAssets().open(localPath);
                    Log.d(TAG, "shouldInterceptRequest: localPath " + localPath);
                    String mimeType = "text/javascript";
                    if (localPath.endsWith("css")) 
                        mimeType = "text/css";
                    
                    return new WebResourceResponse(mimeType, "UTF-8", is);
                 catch (IOException e) 
                    e.printStackTrace();
                    return null;
                
             else 
                return null;
            
        

以常用的图片加载来说,此方案在满足图片渲染速度的同时,解耦了客户端和前端代码,客户端充当server角色,对图片进行请求和缓存控制,保证前端和客户端可以共用图片缓存。

2、H5页面拉取优化:

而针对这些需要拉取的增量资源,可以对它们进行webpack+gzip数据压缩和CDN加速处理,以提升拉取速度。并且在建立网络连接时,可以让前端请求的域名和客户端API接口域名一致,以减少DNS解析时间
最后,对于H5页面来说,图片资源的拉取是最为耗时的,一个比较好的解决方案就是先加载并展示非图片内容,延迟这些图片的加载,以提升用户体验。WebView有一个setBlockNetworkImage(boolean)方法,该方法的作用是是否屏蔽图片的加载。可以利用这个方法来实现图片的延迟加载:在onPageStarted时屏蔽图片加载,在onPageFinished时开启图片加载。

5、内存泄露:

首先针对WebView内存泄漏的问题,可用AS的Profiler来进行检测:

Android 如何用profiler检查内存泄漏【这里需要注意一点的是,点击垃圾回收按钮,手动gc时,需要多点几次,要不然不一定会进行正常的垃圾回收】。

另外之前自己曾多次尝试检测WebView内存泄漏,但是"遗憾"是发现并不会发生内存泄漏。试了下多台手机都没有发生。后来查了下相关文章【Android-WebView还会存在内存泄漏吗?】,了解到WebView内存泄漏其实只是早期内核版本的一个漏洞,现早已修复。所以包括5.0以上的大部分设备不会产生所谓的内存泄漏,低版本 android 系统(Android 5以上)上搭载的 Chromium 内核一般来说也不会太旧,所以出现内存泄漏的概率应该是比较小的。如果仍需要兼容这很小的一部分机型,可以采用如下的这种两种方式:

1、避免在xml布局文件中直接嵌套webview控件,而是采用addview的方式new一个webview并加载到布局中,上下文变量使用applicationContext:

webView = new WebView(getApplicationContext());
webView.getSettings().setJavaScriptEnabled(true);
framelayout.addView(webView);
webView.loadUrl(url);

2、当activity生命周期结束时及时销毁/释放资源:

webview引起的内存泄漏主要是因为org.chromium.android_webview.AwContents 类中注册了component callbacks,但是未正常反注册而导致的。

org.chromium.android_webview.AwContents 类中有这两个方法 onAttachedToWindow 和 onDetachedFromWindow;系统会在attach和detach处进行注册和反注册component callback;
在onDetachedFromWindow() 方法的第一行中:

if (isDestroyed()) return;, 

如果 isDestroyed() 返回 true 的话,那么后续的逻辑就不能正常走到,所以就不会执行unregister的操作;我们的activity退出的时候,都会主动调用 WebView.destroy() 方法,这会导致 isDestroyed() 返回 true;destroy()的执行时间又在onDetachedFromWindow之前,所以就会导致不能正常进行unregister()。
然后解决方法就是:让onDetachedFromWindow先走,在主动调用destroy()之前,把webview从它的parent上面移除掉。

ViewParent parent = mWebView.getParent();
if (parent != null) 
    ((ViewGroup) parent).removeView(mWebView);


mWebView.destroy();

完整的activity的onDestroy()方法:如:

    @Override
    protected void onDestroy() 
        if (mWebView != null) 
            mWebView.destroy();

            ViewParent parent = mWebView.getParent();
            if (parent != null) 
                ((ViewGroup) parent).removeView(mWebView);
            
            mWebView.stopLoading();
            // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
            mWebView.getSettings().setJavaScriptEnabled(false);
            mWebView.clearHistory();
            mWebView.clearView();
            mWebView.removeAllViews();
            mWebView.destroy();
        
        super.onDestroy();
    

3、WebView独立进程解决方案:

解决WebView的内存泄漏及OOM的另一种解决方案是通过开辟独立进程来实现,具体可参考:

WebView独立进程解决方案

参考:

WebView内存泄漏--解决方法小结 - 简书

今日头条品质优化 - 图文详情页秒开实践

WebView性能、体验分析与优化 - 美团技术团队

百度APP-Android H5首屏优化实践

以上是关于Android WebView开发:WebView性能优化的主要内容,如果未能解决你的问题,请参考以下文章

Android WebView开发:WebView性能优化

Android WebView开发:WebView性能优化

Android WebView开发:自定义WebView工具框

Android WebView开发:WebView与Native交互

Android WebView开发:WebView与Native交互

Android WebView开发:自定义WebView工具框