An overview of the

  • The blog link

OnDestroy: Remove the WebView from the View tree and then call webView. destroy: Remove the WebView from the View tree and then call webView. destroy.

override fun onDestroy(a) {
    valparent = webView? .parentif (parent isViewGroup) { parent.removeView(webView) } webView? .destroy()super.onDestroy()
}
Copy the code

So I write a simple Activity that contains a WebView, and then try to do nothing in activity. onDestroy and just call the WebView.destroy method, respectively. Then, leakcanary was integrated in the project to detect memory leak. After launching the App, it was found that Activity. OnDestroy was called normally in landscape and portrait screen repeatedly, but LeakCanary did not inform of memory leak. So I guess the higher version of WebView should fix this problem. I used Android 9 as a test machine, so I decided to switch to a lower version, so I ran an Android 6 phone and found that there was still no memory leak. I read some articles about WebView memory leak on the Internet, some from 2019. If you’re talking about WebView leaks in 2019, Android 6 shouldn’t be behaving properly. Grasp not to understand The principle of not to give up, encounter this kind of problem is easy to do, Read The Fucking Source Code is done.

What does the WebView do when it is destroyed

Since the online solution says to remove the WebView by calling removeView and then calling the webView.destroy method, If you’re thinking about memory leaks, you can look at onDetachedFromWindow(detach from Window) and Destroy (destroy from Window). Take a look at these two WebView methods:

public void destroy(a) {
    checkThread();
    mProvider.destroy();
}

protected void onDetachedFromWindowInternal(a) {
    mProvider.getViewDelegate().onDetachedFromWindow();
    super.onDetachedFromWindowInternal();
}
Copy the code

In general should destroy methods in the Activity. OnDestroy manual call, when the onDetachedFromWindowInternal method in the View of detach when will be back by the system. Note that onDestroy was called earlier than onDetachedFromWindow. The source code can be traced by referring to the View series articles in the Android Graphics System Review.

Each of these methods produces an object called mProvider. What is this object? A search for mProvider = in webview.java found only one assignment:

private WebViewProvider mProvider;

mProvider = getFactory().createWebView(this.new PrivateAccess());
Copy the code

It is an instance of the WebViewProvider type. To see how it is assigned, first take a look at the factory object returned by getFactory:

private static WebViewFactoryProvider getFactory(a) {
    return WebViewFactory.getProvider();
}

// WebViewFactory
static WebViewFactoryProvider getProvider(a) {
    if(sProviderInstance ! =null) return sProviderInstance;
    Class<WebViewFactoryProvider> providerClass = getProviderClass();
    // CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"
    staticFactory = providerClass.getMethod(CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class);
    sProviderInstance = (WebViewFactoryProvider) staticFactory.invoke(null.new WebViewDelegate());
    return sProviderInstance;
}
Copy the code

The above WebViewFactory. GetProvider () method looks through a call to the create method of providerClass got sProviderInstance instance, We need to see what type of class getProviderClass returns:

private static Class<WebViewFactoryProvider> getProviderClass(a) {
    // ...
    return getWebViewProviderClass(clazzLoader);
}

public static Class<WebViewFactoryProvider> getWebViewProviderClass(ClassLoader clazzLoader) throws ClassNotFoundException {
    return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true, clazzLoader);
}
Copy the code

View the source code, can be found CHROMIUM_WEBVIEW_FACTORY values for the com. Android. Webview. Chromium. WebViewChromiumFactoryProviderForP, My view is the Android source code version of P, so here is WebViewChromiumFactoryProviderForP, look at the other Android version of the source code, Found that there is a corresponding WebViewChromiumFactoryProviderForX values. The WebViewChromiumFactoryProviderForP class is not in the AOSP, that should go where looking for?

Refer to the Chrome Developer documentation: WebView for Android is based on Chromium Open Source Project. The new Webview shares the same rendering engine as the Chrome browser on Android, so rendering between Webview and Chrome should be more consistent. Starting with Android 5.0(Lollipop), the WebView is migrated to a separate APK — The Android System WebView, so it can be updated separately on the Android platform. This APP can be seen in the application management. I can understand why the memory leak was not tested on the Android 6 machine before. I guess the Android System WebView APP version has solved the memory leak problem. The version of the app is 86.0.4240.198(you can view the version of the Android System WebView app in the app manager, or open the url in the browser to display the version). So let’s test this guess.

The source code of the Chromium open source project can be viewed here: Chromium open Source Project Ref, where you can view the source code of the target version, I choose 86.0.4240.198 version of the source code for parsing. Then the above WebViewChromiumFactoryProviderForP begins:

public class WebViewChromiumFactoryProviderForP extends WebViewChromiumFactoryProvider {
    public static WebViewChromiumFactoryProvider create(android.webkit.WebViewDelegate delegate) {
        return new WebViewChromiumFactoryProviderForP(delegate);
    }

    protected WebViewChromiumFactoryProviderForP(android.webkit.WebViewDelegate delegate) {
        super(delegate); }}Copy the code

Can see returns a WebViewChromiumFactoryProviderForP instance, its createWebView method in the superclass WebViewChromiumFactoryProvider:

public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
    return new WebViewChromium(this, webView, privateAccess, mShouldDisableThreadChecking);
}
Copy the code

So the mProvider above is an instance of WebViewChromium. Take a look at its onDetachedFromWindow and Destroy methods:

public WebViewProvider.ViewDelegate getViewDelegate(a) {
    return this;
}

public void onDetachedFromWindow(a) {
    // ...
    mAwContents.onDetachedFromWindow();
}

public void destroy(a) {
    // ...
    mAwContents.destroy();
}
Copy the code

The two will be called to AwContents corresponding method, so the above WebView destruction, destroy and onDetachedFromWindowInternal method will call into the corresponding method in AwContents, This is where an earlier version of the memory leak occurred.

Memory leak in AwContents

Let’s first look at creating mAwContents:

mAwContents = newAwContents(mFactory.getBrowserContextOnUiThread(), mWebView, mContext, ...) ;Copy the code

86.0.4240.198 version

Let’s first look at some related methods in the AwContents class in version 86.0.4240.198:

public void destroy(a) {
    if (isDestroyed(NO_WARN)) return;
    // ...
    // Remove pending messages
    mContentsClient.getCallbackHelper().removeCallbacksAndMessages();
    if (mIsAttachedToWindow) {
        // If detach is not present, call onDetachedFromWindow first and then set mIsDestroyed to true
        Log.w(TAG, "WebView.destroy() called while WebView is still attached to window.");
        onDetachedFromWindow();
    }
    mIsDestroyed = true;
}

OnAttachedToWindow is called
public void onAttachedToWindow(a) {
    if (isDestroyed(NO_WARN)) return;
    if (mIsAttachedToWindow) {
        Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
        return;
    }
    mIsAttachedToWindow = true;
    // ...
    if(mComponentCallbacks ! =null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    / / register ComponentCallbacks
    mContext.registerComponentCallbacks(mComponentCallbacks);
}

// onDetachedFromWindow
public void onDetachedFromWindow(a) {
    if (isDestroyed(NO_WARN)) return;
    if(! mIsAttachedToWindow) { Log.w(TAG,"onDetachedFromWindow called when already detached. Ignoring");
        return;
    }
    mIsAttachedToWindow = false;
    // ...
    if(mComponentCallbacks ! =null) {
        // Unregister ComponentCallbacks
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null; }}Copy the code

The onAttachedToWindow method is called when the View attaches to the Window, and the onDetachedFromWindow method is called when the View detach is attached. These two methods are invoked in a registerComponentCallbacks and unregisterComponentCallbacks registered reconciliation, respectively, a Callback function, low version of the memory leak will happen this why!

So let’s look at the logic for ComponentCallbacks:

// Context
public void registerComponentCallbacks(ComponentCallbacks callback) {
    getApplicationContext().registerComponentCallbacks(callback);
}

// Application
public void registerComponentCallbacks(ComponentCallbacks callback) {
    synchronized(mComponentCallbacks) { mComponentCallbacks.add(callback); }}Copy the code

So I suppose in AwContents just call registerComponentCallbacks registration method without call unregisterComponentCallbacks method to the registration, then what happens? So if we look at the implementation of this AwComponentCallbacks class, it’s a non-static inner class in AwContents, so it’s going to hold a reference to an external instance of AwContents, AwContents, on the other hand, holds the Context of the WebView, which in the case of a WebView layout in XML is the Activity in which it’s located, So if at the end of the Activity lifecycle is not registered call unregisterComponentCallbacks method solution, then memory leaks may occur.

In 86.0.4240.198 version, if the Activity onDestroy method in what also not stem, so in the View of detach when registration will still call unregisterComponentCallbacks method solution; If the webView. destroy method is only called manually in the activity.onDestroy method, it will still be unregistered by calling onDetachedFromWindow first. If (isDestroyed(NO_WARN)) return; If it is false, the logic to unregister it will be executed normally before it is marked as destroyed.

54.0.2805.1 version

Then let’s look at AwContents in an old version 54.0.2805.1:

public void destroy(a) {
    if (isDestroyed(NO_WARN)) return;
    // Remove pending messages
    mContentsClient.getCallbackHelper().removeCallbacksAndMessages();
    // ...
    if (mIsAttachedToWindow) {
        Log.w(TAG, "WebView.destroy() called while WebView is still attached to window.");
        nativeOnDetachedFromWindow(mNativeAwContents);
    }
    mIsDestroyed = true;
}

public void onAttachedToWindow(a) {
    if (isDestroyed(NO_WARN)) return;
    // ...
    if(mComponentCallbacks ! =null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    mContext.registerComponentCallbacks(mComponentCallbacks);
}

public void onDetachedFromWindow(a) {
    if (isDestroyed(NO_WARN)) return;
    nativeOnDetachedFromWindow(mNativeAwContents);
    // ...
    if(mComponentCallbacks ! =null) {
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null; }}Copy the code

You can see that mIsDestroyed is set to true while onDetachedFromWindow is not called, if only webView. destroy is called in the activity.ondestroy method. When you detach, onDetachedFromWindow determines that isDestroyed is true and will not run the following unregistered logic, resulting in a memory leak.

If you don’t manually call WebView.destroy in activity.onDestroy, you can theoretically call onDetachedFromWindow to unregister the Callback when you detach the WebView. This memory leak should not occur, but without calling the webView. destroy method, other problems could occur. Say don’t call mContentsClient. GetCallbackHelper (.) removeCallbacksAndMessages () to remove the pending messages, perhaps a new memory leak or something…

To test the memory leak of low version Chromium, you can find a low version of Android phone, and then uninstall its Android System WebView application to the installed version, and then view the corresponding version of AwContents class source code, You can test if there is a possibility of a memory leak in the source code. In addition, if you have a Root phone, you can try to uninstall the latest version of The Android System WebView, and then download a lower version of the Android System WebView APK in the ApkMirror to install on the phone; Or directly compile a specific version of the Android System WebView application from the source code. The source code compilation time is limited and I have not tried, please refer to build-instructions.

conclusion

The memory leak in WebView is actually related to the Chromium kernel version, which has been fixed in the new version of the Chromium kernel. And from Android 5.0(Lollipop) version of Chromium WebView to a separate APP — Android System WebView, With the independent release of Android System WebView, the Chromium kernel on the low version of Android System (Android 5 above) is generally not too old, so the probability of memory leak should be relatively small. If you still need to be compatible with this small number of models, you can destroy the WebView by removing the WebView component first and making sure to call the onDetachedFromWindow method to unregister it. The rest of the destruction logic is then handled through the webView.destroy method.

If there is any error or omission in the relevant content of the article, welcome to point out, and make progress together! Feel good to leave a like to go again ha ~