Android WebView Development (3): WebView Performance Optimization

1. Android WebView development (1): basic application
2. Android WebView development (2): WebView and Native interaction
3. Android WebView development (3): WebView performance optimization
4. Android WebView development (4): WebView independent process solution
Five, Android WebView development (five): custom WebView toolbar


Attached GitHub source code: WebViewExplore


WebView performance optimization solution:

1. WebView pre-initialization:

In order to reduce the performance loss of WebView, we can create WebView in advance at the right time and store it in the cache pool. When the page needs to display content, we can directly obtain the created WebView from the cache pool. According to performance data, WebView pre-creation can reduce The first screen rendering time is 200ms+.

Image

Take the news landing page as an example. When the user enters the news list page, we will create the first WebView. When the user enters the news landing page, it will be taken from the cache pool to render the H5 page. In order not to affect the loading speed of the page, At the same time, we ensure that there are still available WebView components in the landing page cache pool next time. We will trigger the logic of pre-creating WebView every time the page is loaded (pageFinish) or back out of the landing page.

Since the initialization of WebView needs to be bound to the context, if you want to implement the pre-created logic, you need to ensure the consistency of the context. As a conventional approach, we consider using fragments to implement the container that carries the H5 page, so that the context can use the outer activity instance , but there are some problems with the smoothness of Fragment switching itself, and this limits the applicable scenarios for WebView pre-creation. To this end, we have found a more perfect alternative, namely: MutableContextWrapper [ a new context wrapper class that allows external modification of its baseContext, and all methods called by ContextWrapper will be proxied to baseContext for execution ]

Here is a piece of code that pre-creates the 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. Lazy loading of associated Native components:

After the WebView is initialized, you can loadUrl immediately without waiting for the framework onCreate or OnResume to end. In addition, between the initial completion of the WebView and the completion of the drawing of the homepage of the page, other operations on the UI thread should be minimized. A busy UI thread will slow down the speed of WebView.loadUrl.

Specific to our news landing page scenario, since our landing page consists of two parts, WebView+Native comment component , the normal process will start the initialization of the comment component and the acquisition of comment data after the initialization of the WebView is completed. Since the initialization of comments is still in the UI message processing of onCreate at this time, it will seriously delay the logic of the kernel loading the main document. Considering that the comment component is not visible to the user when the user enters the landing page, the initialization of the comment component is delayed until pageFinish of the page or when the rendering of the first screen is completed.

3. Offline cache:

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

Here we set the cache policy of WebView through the setCacheMode method. WebSettings.LOAD_DEFAULT is the default cache policy. It loads the cache when the cache is available and has not expired, otherwise it obtains resources through the network. In this way, the number of network requests for the page can be reduced, so how can we open the page offline? Here we can change the cache strategy of the webview without the network by judging the network status when loading the page .

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);//不使用网络,只加载缓存
}

In this way, we can make our hybrid application use part of the functions without the network, so that nothing can be displayed. Of course, if we make the cache better, when the network is good, for example, in In the WIFI state, go to the background to load some web pages and cache them. If this is done, even when some pages are opened for the first time without a network, the pages can be displayed.
Of course, there will be a problem after caching resources, that is, resources cannot be updated in time, and the cached version in the page in WebSettings.LOAD_DEFAULT does not seem to be very effective, so we may need to do a cache version control by ourselves . This cache version control can be placed in the APP version update.

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. Resource pre (asynchronous) loading:

1. Asynchronous loading via shouldInterceptRequest (pre) :

Sometimes a page has a lot of resources, pictures, CSS, and js, and it also refers to a behemoth like JQuery. It takes a long time from loading to page rendering. The solution is to follow a certain strategy and timing. Request part of the landing page html cache from the CDN in advance to the local, or you can preset html, css, js, image and other resources locally on the client, and negotiate with the server on the front-end version control and incremental update strategy, so that When you come to Webview, you can quickly load the local cache page resources first , and then return to WebView after the loading is complete. All that remains is to pull the incremental resources that need to be updated. This can improve loading speed and reduce server pressure. Rewrite the shouldInterceptRequest method in the WebClient class  [running in a child thread] , and then set this class to 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;
            }
        }

Taking the commonly used image loading as an example, this solution decouples the client and front-end codes while satisfying the image rendering speed. The client acts as a server to request and cache images, ensuring that the front-end and the client can share the image cache. .

2. H5 page pull optimization:

For these incremental resources that need to be pulled, webpack+gzip data compression and CDN acceleration can be performed on them to increase the pulling speed. And when establishing a network connection, the domain name requested by the front end can be consistent with the domain name of the client API interface to reduce DNS resolution time .
Finally, for H5 pages, the pull of image resources is the most time-consuming. A better solution is to load and display non-image content first, and delay the loading of these images to improve user experience. WebView has a setBlockNetworkImage(boolean) method, which is used to block the loading of pictures. You can use this method to implement delayed loading of images : block image loading when onPageStarted, and enable image loading when onPageFinished.

5. Memory leak:

First of all, for the problem of WebView memory leak, AS Profiler can be used to detect:

How does Android use profiler to check for memory leaks [One thing to note here is that when you click the garbage collection button, you need to click a few more times when you click the garbage collection button, or you may not be able to perform normal garbage collection].

In addition, I have tried to detect WebView memory leaks many times before, but "unfortunately" I found that no memory leaks will occur. Tried multiple phones and nothing happened. Later, I checked related articles [ Is there still a memory leak in Android-WebView? ], I learned that the WebView memory leak is actually just a vulnerability in the early kernel version, and it has already been fixed. Therefore, most devices including 5.0 and above will not have so-called memory leaks, and the Chromium kernel on low-version Android systems (Android 5 and above) is generally not too old, so the probability of memory leaks should be relatively small . If you still need to be compatible with this small part of the models, you can use the following two methods:

1. Avoid directly nesting the webview control in the xml layout file, but use the addview method to create a new webview and load it into the layout. The context variable uses applicationContext:

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

2. Destroy/release resources in time when the activity life cycle ends:

The memory leak caused by webview is mainly caused by registering component callbacks in the org.chromium.android_webview.AwContents class, but not deregistering normally.

There are two methods onAttachedToWindow and onDetachedFromWindow in the org.chromium.android_webview.AwContents class; the system will register and de-register component callbacks at attach and detach;
in the first line of the onDetachedFromWindow() method:

if (isDestroyed()) return;, 

If isDestroyed() returns true, then the subsequent logic cannot go normally, so the unregister operation will not be performed; when our activity exits, it will actively call the WebView.destroy() method, which will cause isDestroyed() Return true; the execution time of destroy() is before onDetachedFromWindow, so unregister() cannot be performed normally.
Then the solution is: let onDetachedFromWindow go first, and remove the webview from its parent before actively calling destroy().

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

mWebView.destroy();

The onDestroy() method of the complete activity: such as:

    @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 independent process solution:

Another solution to solve the memory leak and OOM of WebView is to realize it by opening up an independent process. For details, please refer to:

WebView independent process solution

reference:

WebView memory leak - summary of solutions - short book

Quality optimization of Toutiao - the practice of opening the graphic details page in seconds

WebView Performance, Experience Analysis and Optimization - Meituan Technical Team

Baidu APP-Android H5 First Screen Optimization Practice

Guess you like

Origin blog.csdn.net/u012440207/article/details/121781463