[Android Performance Optimization: Memory] – WebView memory leak management

Background: When optimizing memory leaks in a company project, the author found that the memory leak problem related to WebView is very classic. The WebView used by a Fragment page has multiple leak paths, so I recorded them.

Fragment and Activity are not released when using WebView

A Fragment in the project uses Webview, but it is not released when the Fragment is onDestroyView. It is not easy to release the WebView, so the author added the following code to the Fragment’s onDestroyView:

if (webView != null) {
  ViewGroup parent = (ViewGroup) webView.getParent();
  if (parent != null) {
    parent.removeView(webView);
  }
  webView.destroy();
  webview = null;
}

However, this actually results in incomplete release, and other leakage paths are still captured.

As shown in the figure, GC reference chain: AwContents->WebVIew->View.LinsenerInfo->WebViewFragment

The reason is that when using WebView, OnFocusChangeListener is registered

webView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
  @Override
  public void onFocusChange(View v, boolean hasFocus) {
    //Omit
  }
});

Therefore, when releasing WebView, you also need to release some registered Listeners.

WebView is not fully released

The above describes an example of incomplete release when releasing WebView resources. So how can we fully release the used WebView resources?

The author encapsulated an interface as follows:

public void destroyWebView(WebView webView) {
  try {
    if (webView != null) {
      ViewGroup parent = (ViewGroup) webView.getParent();
      if (parent != null) {
        parent.removeView(webView);
      }
      webView.setOnTouchListener(null);
      webView.setOnKeyListener(null);
      webView.setOnFocusChangeListener(null);
      webView.setWebChromeClient(null);
      webView.setWebViewClient(null);
      webView.loadUrl("about:blank");
      webView.onPause();
      webView.removeAllViews();
      webView.destroyDrawingCache();
      webView.destroy();
      webView = null;
    }
  } catch (Throwable e) {
    e.printStackTrace();
  }
}

Is this kind of release really a complete release? If the WebView you are using also registers other Listeners, remember to release them too

On the Internet, there is also a saying that you need to call

webView.pauseTimers();
webView.clearHistory();

Use the above interfaces with caution, because they take effect globally, not just the current WebView!

After following the above two steps to solve the problem, the author thought that there would be no more leaks, but who knew that I still found a third leak path! !

GC reference chain: AwContents->BannerView->Banner->CardView->Container->AdView->Anonymous inner class AdListener->WebViewFragment

Anonymous inner classes cause WebView to leak

According to the reference chain described above, the anonymous inner class implicitly holds a reference to the outer class Fragment, and this anonymous inner class AdShowListener happens to be held by AdView, which is essentially a WebView.

The solution is very conventional: change the anonymous inner class to a static inner class, then change the Fragment used in the static inner class to a weak reference, and when the Fragment is destroyed, the AdShowListener is left empty.

At this point, the author thought that memory leaks would not happen again, but unexpectedly, I still caught it. This time I caught the Activity that wrapped the Fragment as the Context and was held by the webview.

Is it unexpected or surprising?

GC reference chain: AwContents->WebView->WebViewActivity, WebViewActivity is held by WebView as Conext

Because Fragment uses getActivity() when initializing WebView, the context is always held by the WebView core. I guess some systems will have this problem. Is this problem unsolvable? There is no way out of the mountains and rivers, and there is a bright future. The author accidentally discovered that there is a class MutableContextWrapper that can be used.

MutableContextWrapper switches Context

Use AppContext when initializing WebView, switch to Activity when using Webview, and finally switch back to AppContext before destroying WebView.

Why switch to Activity when Activity is using WebView? Because some scenes in WebView may depend on Activity, such as pop-up Dialog, and the Context is AppContext, a crash will occur.

private WebView webview;
//Initialize Webview
MutableContextWrapper contextWrapper = new MutableContextWrapper(getAppContext());
webview = new WebView(contextWrapper);

//Use in Activity
private WebView acquireWebView(Activity activity) {
    //webview in cache
    MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
    contextWrapper.setBaseContext(activity);
   return webView;
}

//Before destroying
  public void recycleWebView(WebView webView) {
    if (webView == null) {
      return;
    }
    MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
    contextWrapper.setBaseContext(getAppContext());
    destroyWebView(webview);
  }

//Destroy the webview interface
public void destroyWebView(WebView webView) {
  try {
    if (webView != null) {
      ViewGroup parent = (ViewGroup) webView.getParent();
      if (parent != null) {
        parent.removeView(webView);
      }
      webView.setOnTouchListener(null);
      webView.setOnKeyListener(null);
      webView.setOnFocusChangeListener(null);
      webView.setWebChromeClient(null);
      webView.setWebViewClient(null);
      webView.loadUrl("about:blank");
      webView.onPause();
      webView.removeAllViews();
      webView.destroyDrawingCache();
      webView.destroy();
      webView = null;
    }
  } catch (Throwable e) {
    e.printStackTrace();
  }
}

So far, no leakage path has been found.

Summary

This article lists the methods to manage WebView memory leaks in the project:

1) Release WebView when Fragment and Activity are destroyed.

2) Releasing WebView needs to be released completely, and all the listeners registered by WebView need to be released.

3) At the same time, consider whether Fragment and Activity use anonymous inner classes. If so, you need to change them to static inner classes. If you want static inner classes to use Fragment and Activity, you must use weak references.

4) Use AppContext when initializing WebView, switch to Activity when Activity uses Webview, and finally switch back to AppContext before destroying WebView.