Android 5.1 WebView內(nèi)存泄漏分析

背景

Android 5.1 系統(tǒng)上捞镰,在項(xiàng)目中遇到一個WebView引起的問題束昵,每打開一個帶webview的界面奏窑,退出后灭必,這個activity都不會被釋放特占,activity的實(shí)例會被持有理肺,由于我們項(xiàng)目中經(jīng)常會用到瀏覽web頁面的地方摄闸,可能引起內(nèi)存積壓,導(dǎo)致內(nèi)存溢出的現(xiàn)象妹萨,所以這個問題還是比較嚴(yán)重的年枕。

問題分析

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

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


這個問題還是比較嚴(yán)重的树姨,那么進(jìn)一步看詳細(xì)的信息摩桶,找出到底是哪里引起的內(nèi)存泄漏,詳情的reference tree如下圖所示:

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


從上圖中可以看出帽揪,在第1層中的 TBReaderApplication 中的 mComponentCallbacks 成員變量硝清,它是一個array list,它里面會持有住activity转晰,引導(dǎo)關(guān)系是 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);
        }
    }

上面兩個方法查邢,會在 Context 基類中被調(diào)用蔗崎,代碼如下:

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

從第二張圖我們已經(jīng)知道,是webview引起的內(nèi)存泄漏扰藕,而且能看到是在 org.chromium.android_webview.AwContents 類中缓苛,難道是這個類注冊了component callbacks,但是未反注冊邓深?一般按系統(tǒng)設(shè)計(jì)未桥,都會反注冊的番官,最有可能的原因就是某些情況下導(dǎo)致不能正常反注冊,不多說钢属,read the fucking source徘熔。基于這個思路淆党,我把chromium的源碼下載下來酷师,代碼在這里 chromium_org

然后找到 org.chromium.android_webview.AwContents 類,看看這兩個方法 onAttachedToWindowonDetachedFromWindow:

    @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();
    }

系統(tǒng)會在attach處detach進(jìn)行注冊和反注冊component callback染乌,注意到 onDetachedFromWindow() 方法的第一行山孔,if (isDestroyed()) return;, 如果 isDestroyed() 返回 true 的話荷憋,那么后續(xù)的邏輯就不能正常走到台颠,所以就不會執(zhí)行unregister的操作,通過看代碼勒庄,可以得到串前,調(diào)用主動調(diào)用 destroy()方法侦香,會導(dǎo)致 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退出的時候刀疙,都會主動調(diào)用 WebView.destroy() 方法,經(jīng)過分析碳想,destroy()的執(zhí)行時間在onDetachedFromWindow之前寇损,所以就會導(dǎo)致不能正常進(jìn)行unregister()彤委。

解決方案

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

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

    mWebView.destroy();

完整的代碼如下:

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

            mWebView.stopLoading();
            // 退出時調(diào)用此方法劳景,移除綁定的服務(wù)誉简,否則某些特定系統(tǒng)會報錯
            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;,有點(diǎn)不明白筋量,為什么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;
        }
    }

結(jié)束語

在開發(fā)過程中碉熄,還發(fā)現(xiàn)一個支付寶SDK的內(nèi)存問題,也是因?yàn)檫@個原因肋拔,具體的類是 com.alipay.sdk.app.H5PayActivity锈津,我們沒辦法,也想了一個不是辦法的辦法凉蜂,在每個activity destroy時琼梆,去主動把 H5PayActivity 中的webview從它的parent中移除,但這個問題限制太多窿吩,不是特別好茎杂,但的確也能解決問題,方案如下:

    /**
     * 解決支付寶的 com.alipay.sdk.app.H5PayActivity 類引起的內(nèi)存泄漏纫雁。
     *
     * <p>
     *     說明:<br>
     *         這個方法是通過監(jiān)聽H5PayActivity生命周期煌往,獲得實(shí)例后,通過反射將webview拿出來轧邪,從
     *         它的parent中移除刽脖。如果后續(xù)支付寶SDK官方修復(fù)了該問題,則我們不需要再做什么了忌愚,不管怎么
     *         說曾棕,這個方案都是非常惡心的解決方案,非常不推薦菜循。同時翘地,如果更新了支付寶SDK后,那么內(nèi)部被混淆
     *         的字段名可能更改癌幕,所以該方案也無效了衙耕。
     * </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);
                }
            }
        }
    }

以上是對發(fā)現(xiàn)的WebView內(nèi)存泄漏的一個簡單分析,權(quán)且記錄一下勺远。

(完)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末橙喘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子胶逢,更是在濱河造成了極大的恐慌厅瞎,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件初坠,死亡現(xiàn)場離奇詭異和簸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)碟刺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門锁保,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事爽柒∥獠ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵浩村,是天一觀的道長做葵。 經(jīng)常有香客問我,道長心墅,這世上最難降的妖魔是什么蜂挪? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮嗓化,結(jié)果婚禮上棠涮,老公的妹妹穿的比我還像新娘。我一直安慰自己刺覆,他們只是感情好严肪,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谦屑,像睡著了一般驳糯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氢橙,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天酝枢,我揣著相機(jī)與錄音,去河邊找鬼悍手。 笑死帘睦,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坦康。 我是一名探鬼主播竣付,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼滞欠!你這毒婦竟也來了古胆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤筛璧,失蹤者是張志新(化名)和其女友劉穎逸绎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夭谤,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡棺牧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了沮翔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陨帆。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖采蚀,靈堂內(nèi)的尸體忽然破棺而出疲牵,到底是詐尸還是另有隱情,我是刑警寧澤榆鼠,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布纲爸,位于F島的核電站,受9級特大地震影響妆够,放射性物質(zhì)發(fā)生泄漏识啦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一神妹、第九天 我趴在偏房一處隱蔽的房頂上張望颓哮。 院中可真熱鬧,春花似錦鸵荠、人聲如沸冕茅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽姨伤。三九已至,卻和暖如春庸疾,著一層夾襖步出監(jiān)牢的瞬間乍楚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工届慈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留徒溪,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓金顿,卻偏偏與公主長得像词渤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子串绩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容