Blanche
Engineer
Engineer
  • UID619
  • Fans2
  • Follows2
  • Posts59
Reads:1053Replies:0

Android 5.1 WebView memory leakage analysis

Created#
More Posted time:Nov 7, 2016 9:45 AM
Background
In Android 5.1 systems, I encountered a problem caused by WebView. Every time I opened an interface that contains WebView and exited, the activity would not be released. The instance of the activity would be held. Since we often need to browse the web pages in our projects, it may cause memory backlog and lead to out-of-memory exceptions. So this problem is comparatively serious.
Problem analysis
With the memory monitor feature of Android Studio, we get the following memory analysis. I opened three BookDetailActivity interfaces (all containing webview). The check results show that there are three leaked activities, as shown in the figure below:


This problem is comparatively serious. Let’s take a further look at the detailed information and try to find what has caused the memory leakage. The detailed reference tree is shown as below:


From the figure above, we can see that in the first layer, the mComponentCallbacks member variable in TBReaderApplication is an array list. It holds the activity. The lead relationship is mComponentCallbacks->AwContents->BaseWebView->BookDetailActivity. The code is in the Application class, as shown below:
public void registerComponentCallbacks(ComponentCallbacks callback) {
        synchronized (mComponentCallbacks) {
            mComponentCallbacks.add(callback);
        }
    }

    public void unregisterComponentCallbacks(ComponentCallbacks callback) {
        synchronized (mComponentCallbacks) {
            mComponentCallbacks.remove(callback);
        }
    }


The two methods above will be called in the Context base class and the code is as below:
/**
     * Add a new {@link ComponentCallbacks} to the base application of the
     * Context, which will be called at the same times as the ComponentCallbacks
     * methods of activities and other components are called.  Note that you
     * <em>must</em> be sure to use {@link #unregisterComponentCallbacks} when
     * appropriate in the future; this will not be removed for you.
     *
     * @param callback The interface to call.  This can be either a
     * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface.
     */
    public void registerComponentCallbacks(ComponentCallbacks callback) {
        getApplicationContext().registerComponentCallbacks(callback);
    }

    /**
     * Remove a {@link ComponentCallbacks} object that was previously registered
     * with {@link #registerComponentCallbacks(ComponentCallbacks)}.
     */
    public void unregisterComponentCallbacks(ComponentCallbacks callback) {
        getApplicationContext().unregisterComponentCallbacks(callback);
    }


From the second figure, we know that WebView caused the memory leakage, and we can also see that the leakage happens in the org.chromium.android_webview.AwContents class. Is it because this class registered component callbacks, but didn't unregister them? Generally, according to the system design, component callbacks will be unregistered. The most possible cause is that the unregistration is not available under some circumstances. No more talking. Let's just read the source. Based on this idea, I downloaded the chromium source code.
Find the org.chromium.android_webview.AwContents class and see the two methods of onAttachedToWindow and onDetachedFromWindow:
@Override
    public void onAttachedToWindow() {
        if (isDestroyed()) return;
        if (mIsAttachedToWindow) {
            Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
            return;
        }
        mIsAttachedToWindow = true;

        mContentViewCore.onAttachedToWindow();
        nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),
                mContainerView.getHeight());
        updateHardwareAcceleratedFeaturesToggle();

        if (mComponentCallbacks != null) return;
        mComponentCallbacks = new AwComponentCallbacks();
        mContext.registerComponentCallbacks(mComponentCallbacks);
    }

    @Override
    public void onDetachedFromWindow() {
        if (isDestroyed()) return;
        if (!mIsAttachedToWindow) {
            Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
            return;
        }
        mIsAttachedToWindow = false;
        hideAutofillPopup();
        nativeOnDetachedFromWindow(mNativeAwContents);

        mContentViewCore.onDetachedFromWindow();
        updateHardwareAcceleratedFeaturesToggle();

        if (mComponentCallbacks != null) {
            mContext.unregisterComponentCallbacks(mComponentCallbacks);
            mComponentCallbacks = null;
        }

        mScrollAccessibilityHelper.removePostedCallbacks();
    }


The system will register and unregister component callbacks by calling the detach method in the attach method. Pay attention to the first line of the onDetachedFromWindow() method: if (isDestroyed()) return;. If isDestroyed() returns true, the subsequent logic will not be normally processed, and so the unregister operation will not be executed. From the code, we can see that active calling of the destroy() method will cause isDestroyed() to return true.
/**
     * Destroys this object and deletes its native counterpart.
     */
    public void destroy() {
        if (isDestroyed()) return;
        // If we are attached, we have to call native detach to clean up
        // hardware resources.
        if (mIsAttachedToWindow) {
            nativeOnDetachedFromWindow(mNativeAwContents);
        }
        mIsDestroyed = true;
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                destroyNatives();
            }
        });
    }


In general, when our activity exits, it will actively call the WebView.destroy() method. According to the analysis, the execution of destroy() method goes before onDetachedFromWindow, so the unregister() function cannot be normally executed.
Solution
The solution is comparatively easy after we locate the cause. The core idea is to let the onDetachedFromWindow go first. So we can call the destroy() method before the active call and remove WebView from its parent.
ViewParent parent = mWebView.getParent();
    if (parent != null) {
        ((ViewGroup) parent).removeView(mWebView);
    }

    mWebView.destroy();


The complete code is as follows:
public void destroy() {
        if (mWebView != null) {
            // If you call the destroy() method first, you will hit the code line of if (isDestroyed()) return;. You need to call onDetachedFromWindow() first, and then
            // destory()
            ViewParent parent = mWebView.getParent();
            if (parent != null) {
                ((ViewGroup) parent).removeView(mWebView);
            }

            mWebView.stopLoading();
            // This method is called at exit to remove the bound services. Otherwise some specific systems will prompt errors.
            mWebView.getSettings().setJavaScriptEnabled(false);
            mWebView.clearHistory();
            mWebView.clearView();
            mWebView.removeAllViews();

            try {
                mWebView.destroy();
            } catch (Throwable ex) {

            }
        }
    }


Code before Android 5.1
After comparison with the code before Android 5.1, we know that the old code won’t have such a problem. Below is the KitKat code and the line of if (isDestroyed()) return; is missing in it. I don't quite understand why Google added this line of code in a higher version.
/**
     * @see android.view.View#onDetachedFromWindow()
     */
    public void onDetachedFromWindow() {
        mIsAttachedToWindow = false;
        hideAutofillPopup();
        if (mNativeAwContents != 0) {
            nativeOnDetachedFromWindow(mNativeAwContents);
        }

        mContentViewCore.onDetachedFromWindow();

        if (mComponentCallbacks != null) {
          mContainerView.getContext().unregisterComponentCallbacks(mComponentCallbacks);
          mComponentCallbacks = null;
        }

        if (mPendingDetachCleanupReferences != null) {
            for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) {
                mPendingDetachCleanupReferences.get(i).cleanupNow();
            }
            mPendingDetachCleanupReferences = null;
        }
    }


Ending
During development, I also find a memory problem in Alipay SDK caused by the same reason. The specific class is com.alipay.sdk.app.H5PayActivity. We had no solutions for it, but we managed to find a way to deal with it: at every activity destroy, we can actively remove WebView from its parent in H5PayActivity. There are many restrictions for this problem and the solution is not perfect. But it does solve the problem. The solution is as follows:
/**
     * Solve the memory leakage caused by the com.alipay.sdk.app.H5PayActivity class of Alipay.
     *
     * <p>
     *     Instruction:

     *         This method obtains the instance by listening to the lifecycle of H5PayActivity, and retrieves WebView via reflection,
     *         and removes it from its parent. If Alipay SDK has an official solution to this problem in the future, we don’t need to do anything anymore. Anyway,
     *         this solution is very ugly and not recommended. At the same time, if Alipay SDK is updated, the mixed internal
     *         field names may be changed, so this solution will also be ineffective.
     * </p>
     *
     * @param activity
     */
    public static void resolveMemoryLeak(Activity activity) {
        if (activity == null) {
            return;
        }

        String className = activity.getClass().getCanonicalName();
        if (TextUtils.equals(className, "com.alipay.sdk.app.H5PayActivity")) {
            Object object = Reflect.on(activity).get("a");

            if (DEBUG) {
                LogUtils.e(TAG, "AlipayMemoryLeak.resolveMemoryLeak activity = " + className
                    + ",  field = " + object);
            }

            if (object instanceof WebView) {
                WebView webView = (WebView) object;
                ViewParent parent = webView.getParent();
                if (parent instanceof ViewGroup) {
                    ((ViewGroup) parent).removeView(webView);
                }
            }
        }
    }


Above is a simple analysis on the WebView memory leakage I found and can be kept as a record.
Guest