內(nèi)存泄露的一些坑

Activity內(nèi)部類(lèi)泄漏

  • Activity如果存在內(nèi)部類(lèi),無(wú)論是匿名內(nèi)部類(lèi),或者是聲明的內(nèi)部類(lèi),都有可能造成Activity內(nèi)存泄漏,因?yàn)閮?nèi)部類(lèi)默認(rèn)是直接持有這個(gè)activity的引用,如果內(nèi)部類(lèi)的生命周期比activity的生命周期要長(zhǎng),那么在activity銷(xiāo)毀的時(shí)候內(nèi)部類(lèi)仍然存在并且持有activity的引用,那么activity自然無(wú)法被gc,造成內(nèi)存泄漏

Activity內(nèi)部Handler

class MyHandler extends Handler {
        
        MyHandler() {
            
        }

        @Override
        public void handleMessage(Message msg) {
            // to do your job
        }
    }
MyHandler myHandler = new MyHandler();

如上,在Activity內(nèi)部如果聲明一個(gè)這樣的Handler,那么myHandler就默認(rèn)持有Activity引用,假設(shè)Activity退出了,但是可能這時(shí)候才有myHandler的任務(wù)post,那么Activity是無(wú)法被回收的,可以采用以下方式解決:

static class MyHandler extends Handler {
        WeakReference<Activity> mActivityReference;

        MyHandler(Activity activity) {
            mActivityReference = new WeakReference<Activity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            final Activity activity = mActivityReference.get();
            if (activity != null) {
                if (msg.what == 1 && isJumpToHomePage) {
                    Intent intent = new Intent(activity, HomePageActivity.class);
//                    intent.putExtra("themeType", themeType);
//                    LogUtil.d("themeType == " + themeType);
                    activity.startActivity(intent);
                    activity.finish();
                }
            }
        }
    }

這里面是把MyHandler是一個(gè)內(nèi)部靜態(tài)類(lèi),靜態(tài)類(lèi)在java虛擬機(jī)加載的時(shí)候就是獨(dú)立加載到內(nèi)存中的,不會(huì)依賴(lài)于任何其他類(lèi),而且這里面是把a(bǔ)ctivity以弱引用的方式傳到MyHandler中,即便是靜態(tài)MyHandler類(lèi)對(duì)象一直存在,但是由于它持有的是activity弱引用,在gc回收的時(shí)候activity對(duì)象是可以被回收的,另外注意一點(diǎn),對(duì)于Handler的使用如果有sendEmptyMessageDelayed()來(lái)延遲任務(wù)執(zhí)行的話最好在Activity的onDestroy里面把Handler的任務(wù)都移除(removeCallbacks(null)),activity在退出后,就是應(yīng)該在onDestroy方法里面把一些任務(wù)取消掉,做一些清理的操作

Activity內(nèi)部線程

  • 在Activity里面有時(shí)候?yàn)榱藢?shí)現(xiàn)異步操作會(huì)單獨(dú)開(kāi)一個(gè)線程來(lái)執(zhí)行任務(wù),或者是異步的網(wǎng)絡(luò)請(qǐng)求也是單獨(dú)開(kāi)線程來(lái)執(zhí)行的,那么就會(huì)存在一個(gè)問(wèn)題,如果內(nèi)部線程的生命周期比Activity的生命周期要長(zhǎng),那么內(nèi)部線程任然默認(rèn)持有Activity的引用,導(dǎo)致Activity對(duì)象無(wú)法被回收,但是當(dāng)這個(gè)線程執(zhí)行完了之后,Activity對(duì)象就能被成功的回收了,這會(huì)造成一個(gè)崩潰風(fēng)險(xiǎn),可能在線程里面有調(diào)用到一些Activity的內(nèi)部對(duì)象,但是在Activity退出后這些對(duì)象有可能有些已經(jīng)被回收了,就變成null了,這時(shí)候要是不進(jìn)行null的判斷就會(huì)報(bào)空指針異常,如果這個(gè)線程是一直跑的,那就會(huì)造成Activity對(duì)象一直不會(huì)被回收了,因此,在activity退出后一定要做相關(guān)的清理操作,中斷線程,取消網(wǎng)絡(luò)請(qǐng)求等等

Activity內(nèi)部類(lèi)回調(diào)監(jiān)聽(tīng)

  • 在編碼中常常會(huì)定義各種接口回調(diào),類(lèi)似有點(diǎn)擊時(shí)間監(jiān)聽(tīng)OnClickListener,這些回調(diào)監(jiān)聽(tīng)有時(shí)候就定義在Activity內(nèi)部,或者直接用Activity對(duì)象去實(shí)現(xiàn)這個(gè)接口,到時(shí)候設(shè)置監(jiān)聽(tīng)的時(shí)候直接調(diào)用setListener(innerListener)或者setListener(this),innerListener是Activity內(nèi)部定義的,this就是Activity對(duì)象,那么問(wèn)題來(lái)了,回調(diào)監(jiān)聽(tīng)并不一定馬上返回,只有在觸發(fā)條件滿(mǎn)足的時(shí)候才會(huì)回調(diào),這個(gè)時(shí)間是無(wú)法確定的,因此在Activity退出的時(shí)候應(yīng)該顯示的把回調(diào)監(jiān)聽(tīng)都移除掉setListener(null),既釋放了回調(diào)監(jiān)聽(tīng)對(duì)象占用的內(nèi)存,也避免回調(diào)監(jiān)聽(tīng)繼續(xù)持有activity引用;對(duì)與內(nèi)部類(lèi)還有一種解決方式,和內(nèi)部Handler相似,定義成static內(nèi)部類(lèi),然后把Activity對(duì)象的弱引用傳遞進(jìn)去,這樣也就萬(wàn)無(wú)一失,舉個(gè)項(xiàng)目中遇到的實(shí)際場(chǎng)景:
private static class RecorderTimeListener implements TimeCallback {

        WeakReference<ChatActivity> target;

        RecorderTimeListener(ChatActivity activity) {
            target = new WeakReference<>(activity);
        }

        @Override
        public void onCountDown(final int time) {
            if (target == null || target.get() == null) {
                return;
            }
            final ChatActivity activity = target.get();
            activity.runOnUiThread(new Runnable() {

                @Override
                public void run() {
                    activity.volumeView.setResetTime(time);
                }
            });
        }

        @Override
        public void onMaxTime() {
            if (target == null || target.get() == null) {
                return;
            }
            final ChatActivity activity = target.get();
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    activity.isMaxTime = true;
                    activity.stopRecord();
                }
            });
        }
    }

private class StartRecorderListener implements StartCallback {


        @Override
        public boolean onWait() {
            cancelRecord();
            return true;
        }

        @Override
        public void onStarted() {
            if (playerManager.isPlaying()) {
                playerManager.stop();
            }
            recordWaveView.setVisibility(View.VISIBLE);
            animation = (AnimationDrawable) recordWaveView.getBackground();
            animation.start();

            volumeView.showMoveCancelView();
            volumeDialog.show();

            viewHandler.postDelayed(volumeRunnable, 100);
        }

        @Override
        public void onFailed(int errorCode) {
            if (errorCode == RecorderManager.ERROR_START_FAIL) {
                showHintDialog(R.string.chat_permission_dialog_title, R.string.chat_permission_dialog_message);
            }
        }
    }

private void startRecord() {
        SystemDateUtil.init(this);
        LogUtil.i(ChatKey.TAG_INFO, "--------------------------錄音開(kāi)始--------------------------");
        final long startSendTime = SystemDateUtil.getCurrentDate().getTime();
        sliceSender = dialogMsgService.createSliceSender(
                AccountUtil.getCurrentFamilyChatDialogId(),
                AccountUtil.getCurrentImAccountId(), new DialogMsgService.OnSendVoiceMsgListener() {
                    @Override
                    public void onSuccess() {
                        LogUtil.d(TAG, "錄音上傳成功");
                        sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_SUCCESS);
                    }

                    @Override
                    public void onFailure() {
                        sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_FAILURE);
                        LogUtil.d(TAG, "錄音上傳失敗");
                    }
                });
        RecorderManager.getInstance(this).startRecorder(sliceSender, new StartRecorderListener(), new RecorderTimeListener(this));
        LogUtil.i(ChatKey.TAG_INFO, "groupId:" + sliceSender.getGroupId());
    }

如上StartRecorderListener是內(nèi)部類(lèi),RecorderTimeListener是靜態(tài)內(nèi)部類(lèi)并傳入Activity弱引用,如果把StartRecorderListener的實(shí)現(xiàn)改成RecorderTimeListener的實(shí)現(xiàn),那么Activity內(nèi)存泄漏就不存在了

動(dòng)畫(huà)導(dǎo)致內(nèi)存泄漏

  • 進(jìn)入Activity界面后如果有一些和控件綁定在一起的屬性動(dòng)畫(huà)在運(yùn)行,退出的時(shí)候要記得cancel掉這些動(dòng)畫(huà)
自定義控件ImageButton中:
public void start(float startAngle, float endAngle) {
        setStop(false);

        final AnimatorSet as = new AnimatorSet();
        final ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress",
                startAngle, endAngle);
        oa.setDuration(duration);
        oa.setInterpolator(new DecelerateInterpolator(1.1f));
        oa.setRepeatCount(count);
//      oa.setRepeatMode(ObjectAnimator.INFINITE);
        oa.addUpdateListener(new AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                if (stop && as.isRunning()) {
                    as.cancel();
//                    oa.removeAllListeners();
                } else {
                    float p = (float) animator.getAnimatedValue();
                    setProgress(p);
                }
            }
        });
        as.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
            }
        });
        as.play(oa);
        as.start();
    }
    
    public void cancel() {
        setStop(true);
    }

    public void setStop(boolean stop) {
        this.stop = stop;
        if (stop) {
            setProgress(0.0f);
        }
    }

如上如果不cancel掉屬性動(dòng)畫(huà)就會(huì)一直運(yùn)行并且一直去執(zhí)行控件的onDraw方法,那么ImageButton持有了Activity對(duì)象,而屬性動(dòng)畫(huà)ObjectAnimator持有了ImageButton,ObjectAnimator一直在運(yùn)行,那么Activity對(duì)象也就不能被釋放了

  • 屬性動(dòng)畫(huà)的對(duì)象盡量不要用static修飾,static修飾和,這個(gè)對(duì)象一旦被創(chuàng)建那么就一直存在了,屬性動(dòng)畫(huà)一旦start之后,那么就一直運(yùn)行,這時(shí)候就算退出activity的時(shí)候cancel掉動(dòng)畫(huà)也仍然會(huì)持有activity引用,就像下面這個(gè)例子:
private static ValueAnimator valueAnimator;

private void startValueAnimator() {
        int displayTime2Show = displayTime - 1;
        if (displayTime2Show > 1) {
            valueAnimator = ValueAnimator.ofInt(displayTime2Show, 1);
            valueAnimator.setDuration(displayTime2Show * 1000);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    tvStartPageTime.setText(animation.getAnimatedValue().toString());
                }
            });
            valueAnimator.start();
        }

    }
protected void onPause() {
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        super.onPause();
    }

即便是在activity退出后cancel掉動(dòng)畫(huà),activity依然無(wú)法被釋放,為什么?因?yàn)関alueAnimator是靜態(tài)的,而且添加了動(dòng)畫(huà)屬性改變的監(jiān)聽(tīng)addUpdateListener,在監(jiān)聽(tīng)回調(diào)里面有tvStartPageTime(TextView)控件,默認(rèn)持有Activity對(duì)象,因此即便Activity退出,動(dòng)畫(huà)cancel掉也無(wú)法釋放持有的引用,修改方法有兩種,一種是把valueAnimator的static修飾去掉,另一中國(guó)是:

protected void onPause() {
valueAnimator.removeAllUpdateListeners();
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        super.onPause();
    }

加一句監(jiān)聽(tīng)器的移除代碼removeAllUpdateListeners()

傳Context參數(shù)的時(shí)候使用Activity對(duì)象造成內(nèi)存泄漏

  • 在android中常常會(huì)用到Context環(huán)境變量,Activity繼承了Context,所以在傳入Context的時(shí)候常常直接在Activity中傳入this即Activity本對(duì)象,這是比較不好的習(xí)慣,在沒(méi)有規(guī)定一定要傳Activity對(duì)象的時(shí)候盡量采用全局的Context對(duì)象,即ApplicationContext來(lái)作為參數(shù)傳遞進(jìn)去,因?yàn)锳pplicationContext只要app在運(yùn)行那么它就一直存在,因此即便有一個(gè)對(duì)象長(zhǎng)期引用它,生命周期也不會(huì)比ApplicationContext長(zhǎng),所以不會(huì)造成ApplicationContext的內(nèi)存泄漏,因?yàn)锳pplicationContext只要App在運(yùn)行就不允許被回收
  • 在Android程序中要慎用單例,如果單例需要傳Context對(duì)象,那么就需要謹(jǐn)慎了因?yàn)樵趩卫腥绻袰ontext保存起來(lái),那么這個(gè)單例一旦被創(chuàng)建,就一直存在了,如果傳入的是Activity對(duì)象,那將一直持有Activity對(duì)象引用導(dǎo)致內(nèi)存泄漏,解決版本是傳入ApplicationContext對(duì)象,或者在Activity退出的時(shí)候銷(xiāo)毀這個(gè)單例對(duì)象,單例在什么時(shí)候時(shí)候使用,如果一個(gè)對(duì)象并不會(huì)被頻繁的調(diào)用,那就沒(méi)必要用單例,對(duì)于可能會(huì)被頻繁調(diào)用的對(duì)象方法可以采用單例,這樣做可以避免反復(fù)創(chuàng)建對(duì)象和gc對(duì)象造成的內(nèi)存抖動(dòng);對(duì)于需要保存的全局變量也可以用單例封裝起來(lái);單例只要?jiǎng)?chuàng)建了就一直有存在引用,所以是不會(huì)被gc的
  • 使用靜態(tài)變量來(lái)保存Activity對(duì)象,這是一個(gè)非常不好的編碼習(xí)慣,static修飾的代碼片段,變量或者類(lèi)是在app加載的時(shí)候就已經(jīng)加載到內(nèi)存中了,所以和單例有點(diǎn)相似,static變量也會(huì)一直持有Activity對(duì)象直到APP被殺死或者顯示的把static變量置空

在Android5.0以上的WebView泄漏

  • 如果Activity引用了WebView控件來(lái)加載一個(gè)網(wǎng)頁(yè)或者加載一個(gè)本地的網(wǎng)頁(yè),在退出activity之后即便你調(diào)用了webView.destroy()方法,也無(wú)法釋放webview對(duì)于activity持有的引用,原因和解決方案可參考Android5.1的WebView內(nèi)存泄漏,如這篇文章所分析的解決方案確實(shí)有效,親測(cè)可用!

子線程中不當(dāng)?shù)氖褂肔ooper.prepare()和Looper.loop()方法造成內(nèi)存泄漏

  • Looper.loop()是一個(gè)無(wú)限循環(huán)的方法,它是反復(fù)的去MessageQueue里面去取出Message并分發(fā)給對(duì)應(yīng)的Handler去執(zhí)行,如果在子線程中調(diào)用了Looper.prepare()和Looper.loop()方法,Looper.loop()會(huì)導(dǎo)致這個(gè)線程一直不死,一直堵在這里,因此線程就無(wú)法結(jié)束運(yùn)行,在Looper.prepare()和Looper.loop()之間的所有對(duì)象都沒(méi)辦法被釋放,解決方案就是在不用的時(shí)候及時(shí)的把Looper給quit掉

EditText使用setTransformationMethod導(dǎo)致的內(nèi)存泄漏

  • 這個(gè)問(wèn)題只有在4.0的android系統(tǒng)上才會(huì)存在,在5.0以上的系統(tǒng)已經(jīng)不存在了,應(yīng)該是屬于Android的一個(gè)缺陷


    這里寫(xiě)圖片描述
    這里寫(xiě)圖片描述

    問(wèn)題的根源應(yīng)該就是這:

loginPasswdEt.setTransformationMethod(PasswordTransformationMethod.getInstance());
loginPasswdEt.setTransformationMethod(HideReturnsTransformationMethod.getInstance());

而PasswordTransformationMethod和HideReturnsTransformationMethod分別都是一個(gè)單例:

private static PasswordTransformationMethod sInstance;

private static HideReturnsTransformationMethod sInstance;
PasswordTransformationMethod

public CharSequence getTransformation(CharSequence source, View view) {
        if (source instanceof Spannable) {
            Spannable sp = (Spannable) source;

            /*
             * Remove any references to other views that may still be
             * attached.  This will happen when you flip the screen
             * while a password field is showing; there will still
             * be references to the old EditText in the text.
             */
            ViewReference[] vr = sp.getSpans(0, sp.length(),
                                             ViewReference.class);
            for (int i = 0; i < vr.length; i++) {
                sp.removeSpan(vr[i]);
            }

            removeVisibleSpans(sp);

            sp.setSpan(new ViewReference(view), 0, 0,
                       Spannable.SPAN_POINT_POINT);
        }

        return new PasswordCharSequence(source);
    }
    
private static class ViewReference extends WeakReference<View>
            implements NoCopySpan {
        public ViewReference(View v) {
            super(v);
        }
    }

上面是5.0系統(tǒng)的源碼,里面已經(jīng)用ViewReference來(lái)包裝view設(shè)置到Spannable中了,所以是把view的弱引用傳進(jìn)去了,因此可以被gc回收,而在4.0android系統(tǒng)上,很可能就不是這么做的,所以4.0系統(tǒng)上面就是View對(duì)象被PasswordTransformationMethod和HideReturnsTransformationMethod單例長(zhǎng)期持有,而View又持有Activity對(duì)象,所以針對(duì)4.0系統(tǒng)我們只需要釋放這兩個(gè)單例對(duì)象即可:

private void releaseMemoryLeak() {
        int sdk = Build.VERSION.SDK_INT;
        if (sdk >= Build.VERSION_CODES.LOLLIPOP) {
            return;
        }
        try {
            Field field1 = PasswordTransformationMethod.class.getDeclaredField("sInstance");
            if (field1 != null) {
                field1.setAccessible(true);
                field1.set(PasswordTransformationMethod.class, null);
            }
            Field field2 = HideReturnsTransformationMethod.class.getDeclaredField("sInstance");
            if (field2 != null) {
                field2.setAccessible(true);
                field2.set(HideReturnsTransformationMethod.class, null);
            }
        } catch (NoSuchFieldException e) {
            SyncLogUtil.e(e);
        } catch (IllegalAccessException e) {
            SyncLogUtil.e(e);
        }
    }

加上上述代碼后驗(yàn)證發(fā)現(xiàn)內(nèi)存不再泄漏,搞定。

控件的BackGround導(dǎo)致的內(nèi)存泄漏(4.0android系統(tǒng)已經(jīng)解決)

  • 有時(shí)候?yàn)榱吮苊鈭D片反復(fù)的加載,就把第一次加載后的Bitmap或者Drawable用靜態(tài)變量保存起來(lái),但是要是把這種靜態(tài)修飾的圖片對(duì)象設(shè)置成控件的背景,那就呵呵了
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);

  TextView label = new TextView(this);
  label.setText("Leaks are bad");

  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);

  setContentView(label);
}

因?yàn)樵赩iew的setBackgroundDrawable方法里面有一句:

public void setBackgroundDrawable(Drawable background) {
......省略很多代碼
background.setCallback(this);
mBackground = background;
}

Drawable對(duì)象把View對(duì)象作為回調(diào)保存起來(lái)了,不過(guò)在4.0系統(tǒng)以后引入回調(diào)來(lái)保存View對(duì)象了,所以已經(jīng)不會(huì)造成內(nèi)存泄漏問(wèn)題了:

public final void setCallback(Callback cb) {
        mCallback = new WeakReference<Callback>(cb);
    }

這里依然要舉例子出來(lái)是想說(shuō)明不恰當(dāng)?shù)氖褂胹tatic來(lái)修飾變量很有可能導(dǎo)致對(duì)象無(wú)法被回收

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容