Android 5.1 WebView內存泄漏分析

MalloryHarr 8年前發布 | 19K 次閱讀 安卓開發 Android開發 移動開發

背景

 Android 5.1 系統上,在項目中遇到一個WebView引起的問題,每打開一個帶webview的界面,退出后,這個activity都不會被釋放,activity的實例會被持有,由于我們項目中經常會用到瀏覽web頁面的地方,可能引起內存積壓,導致內存溢出的現象,所以這個問題還是比較嚴重的。

問題分析

使用Android Studio的內存monitor,得到了以下的內存分析,我打開了三個BookDetailActivity界面(都有webview),檢查結果顯示有3個activity泄漏,如下圖所示:

2016-08-19-memory-leak-1.png

這個問題還是比較嚴重的,那么進一步看詳細的信息,找出到底是哪里引起的內存泄漏,詳情的reference tree如下圖所示:

2016-08-19-memory-leak-2.png

從上圖中可以看出,在第1層中的 TBReaderApplication 中的 mComponentCallbacks 成員變量,它是一個array list,它里面會持有住activity,引導關系是 mComponentCallbacks->AwContents->BaseWebView->BookDetailActivity , 代碼在 Application 類里面,代碼如下所示:

public void registerComponentCallbacks(ComponentCallbacks callback) {
        synchronized (mComponentCallbacks) {
            mComponentCallbacks.add(callback);
        }
    }

public void unregisterComponentCallbacks(ComponentCallbacks callback) {
    synchronized (mComponentCallbacks) {
        mComponentCallbacks.remove(callback);
    }
}</code></pre> 

上面兩個方法,會在 Context 基類中被調用,代碼如下:

/**
     * 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);
    }

從第二張圖我們已經知道,是webview引起的內存泄漏,而且能看到是在 org.chromium.android_webview.AwContents 類中,難道是這個類注冊了component callbacks,但是未反注冊?一般按系統設計,都會反注冊的,最有可能的原因就是某些情況下導致不能正常反注冊,不多說,read the fucking source。基于這個思路,我把chromium的源碼下載下來,代碼在這里 chromium_org

然后找到 org.chromium.android_webview.AwContents 類,看看這兩個方法 onAttachedToWindow 和 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();
    }

系統會在attach處detach進行注冊和反注冊component callback,注意到 onDetachedFromWindow() 方法的第一行, if (isDestroyed()) return; , 如果 isDestroyed() 返回 true 的話,那么后續的邏輯就不能正常走到,所以就不會執行unregister的操作,通過看代碼,可以得到,調用主動調用 destroy() 方法,會導致 isDestroyed() 返回 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();
            }
        });
    }

一般情況下,我們的activity退出的時候,都會主動調用 WebView.destroy() 方法,經過分析,destroy()的執行時間在onDetachedFromWindow之前,所以就會導致不能正常進行unregister()。

解決方案

找到了原因后,解決方案也比較簡單,核心思路就是讓onDetachedFromWindow先走,那么在主動調用之前destroy(),把webview從它的parent上面移除掉。

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

    mWebView.destroy();

完整的代碼如下:

public void destroy() {
        if (mWebView != null) {
            // 如果先調用destroy()方法,則會命中if (isDestroyed()) return;這一行代碼,需要先onDetachedFromWindow(),再
            // destory()
            ViewParent parent = mWebView.getParent();
            if (parent != null) {
                ((ViewGroup) parent).removeView(mWebView);
            }

            mWebView.stopLoading();
            // 退出時調用此方法,移除綁定的服務,否則某些特定系統會報錯
            mWebView.getSettings().setJavaScriptEnabled(false);
            mWebView.clearHistory();
            mWebView.clearView();
            mWebView.removeAllViews();

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

            }
        }
    }

Android 5.1之前的代碼

對比了5.1之前的代碼,它是不會存在這樣的問題的,以下是kitkat的代碼,它少了一行 if (isDestroyed()) return; ,有點不明白,為什么google在高版本把這一行代碼加上。

/**
     * @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;
        }
    }

結束語

在開發過程中,還發現一個支付寶SDK的內存問題,也是因為這個原因,具體的類是 com.alipay.sdk.app.H5PayActivity ,我們沒辦法,也想了一個不是辦法的辦法,在每個activity destroy時,去主動把 H5PayActivity 中的webview從它的parent中移除,但這個問題限制太多,不是特別好,但的確也能解決問題,方案如下:

/**
     * 解決支付寶的 com.alipay.sdk.app.H5PayActivity 類引起的內存泄漏。
     *
     * <p>
     *     說明:<br>
     *         這個方法是通過監聽H5PayActivity生命周期,獲得實例后,通過反射將webview拿出來,從
     *         它的parent中移除。如果后續支付寶SDK官方修復了該問題,則我們不需要再做什么了,不管怎么
     *         說,這個方案都是非常惡心的解決方案,非常不推薦。同時,如果更新了支付寶SDK后,那么內部被混淆
     *         的字段名可能更改,所以該方案也無效了。
     * </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);
                }
            }
        }
    }

以上是對發現的WebView內存泄漏的一個簡單分析,權且記錄一下。

 

 

來自:http://www.jianshu.com/p/eada9b652d99

 

 本文由用戶 MalloryHarr 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!