Android內(nèi)存優(yōu)化二:內(nèi)存泄漏

Android內(nèi)存優(yōu)化一:java垃圾回收機制
Android內(nèi)存優(yōu)化二:內(nèi)存泄漏
Android內(nèi)存優(yōu)化三:內(nèi)存泄漏檢測與監(jiān)控
Android內(nèi)存優(yōu)化四:OOM
Android內(nèi)存優(yōu)化五:Bitmap優(yōu)化

內(nèi)存泄漏

內(nèi)存泄漏指的是不需要的對象無法被回收。

其本質(zhì)是對象生命周期的不一致性導(dǎo)致的,即生命周期較長的對象持有生命周期較短的對象的引用。

對象回收時機:

  1. 根據(jù)Java垃圾回收機制,Java 虛擬機使用可達(dá)性分析算法來判斷對象是否可被回收,以 GC Roots 為起始點進(jìn)行搜索,可達(dá)的對象都是存活的,不可達(dá)的對象可被回收。
  • 當(dāng)一個不需要的對象,仍然可達(dá)(從GC Roots到此對象具有完整的引用鏈),則無法被回收,導(dǎo)致此對象仍然占用著內(nèi)存,就發(fā)生了內(nèi)存泄漏
  1. 根據(jù)Java垃圾回收機制,判定對象是否可被回收與引用有關(guān)。
  • 強引用關(guān)聯(lián)的對象不會被回收。
  • 軟引用關(guān)聯(lián)的對象只有在內(nèi)存不夠的情況下才會被回收。
  • 弱引用關(guān)聯(lián)的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發(fā)生之前。
  • 虛引用關(guān)聯(lián)的對象,不會對其生存時間造成影響,也無法通過虛引用得到一個對象。

注:通過軟引用可以解決大部分內(nèi)存泄漏問題。只有當(dāng)對象只被軟引用關(guān)聯(lián)時,gc發(fā)生時,才會被回收;如果對象同時被強引用和弱引用關(guān)聯(lián),則無法被回收。

類型

內(nèi)部類

內(nèi)部類(包括匿名內(nèi)部類)會隱式持有外部類對象的引用

/**
 * 外部類
 */
public class Outer {

    private int count;

    private Inner inner = new Inner();

    public void doAction() {
        inner.doSomething();
    }

    /**
     * 內(nèi)部類
     */
    private class Inner {

        public void doSomething() {
            count++;
        }
    }

}

編譯后的字節(jié)碼

class Outer$Inner
{
  private Outer$Inner(Outer paramOuter) {}

  public void doSomething()
  {
    Outer.access$108(this.this$0);
  }
}
  • 可以看到,編譯器為內(nèi)部類單獨生成了一個.class文件,構(gòu)造函數(shù)被定義為私有的,且傳入了外部類的實例,所以它持有了外部類實例的強引用,也因此,內(nèi)部類可以調(diào)用到外部類的方法和屬性。
  • 當(dāng)內(nèi)部類生命周期長于外部類時,比如外部類為Activity,內(nèi)部類為Thread,當(dāng)Activity退出時,Thread仍未執(zhí)行完,導(dǎo)致Activity仍被Thread強引用著無法被回收而出現(xiàn)了內(nèi)存泄漏

解決方法

解決方法是使用靜態(tài)內(nèi)部類 + 弱引用,靜態(tài)內(nèi)部類編譯后的字節(jié)碼文件,編譯器并沒有為它添加額外的構(gòu)造函數(shù),所以它其實和我們的外部類沒有任何關(guān)系,這是寫在同一個.java源文件中而已。

  • 由于靜態(tài)內(nèi)部類未持有外部類引用,所以無法調(diào)用外部類的方法及屬性,解決方式是將外部類通過弱引用傳遞給靜態(tài)內(nèi)部類,當(dāng)外部類未被強引用時,則GC的時候,外部類會被回收,此時通過弱引用取出外部類的引用則為null
/**
 * 外部類
 */
public class Outer {

    private int count;

    private Inner inner = new Inner();

    public void doAction() {
        inner.doSomething();
    }

    /**
     * 靜態(tài)內(nèi)部類
     */
    private static class Inner {

            WeakReference<Outer> ref;

            Inner(Outer outer){
                ref = new WeakReference<Outer>(outer);
        }

        public void doSomething() {
            // 由于靜態(tài)內(nèi)部類未持有外部類引用,所以無法調(diào)用外部類的方法及屬性
            // 這里通過弱引用獲取到外部類的引用
            Outer outer = ref.get();
            // 當(dāng)外部類被回收時,取出的引用為null
            if (outer != null){
                 outer.count++;
            }
        }
    }

}

Handler

通過handler發(fā)送一個延遲消息,由于匿名內(nèi)部類Runnable持有了外部類的引用,且其生命周期大于外部類,所以造成了內(nèi)存泄漏。

public void post(){
    Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            // todo
        }
    },500);
}

通過handler發(fā)送非延遲消息,也會造成內(nèi)存泄漏。

這是因為消息會插入到消息隊列MessageQueue中,如果在這個消息之前有其他消息產(chǎn)生阻塞,或者存在同步屏障,會導(dǎo)致這個消息延遲執(zhí)行,所以其生命周期同樣會大于外部類。

public void post(){
    Handler handler = new Handler();
    handler.post(new Runnable() {
        @Override
        public void run() {
            // todo    
        }
    });
}

同樣,如下這種方式也會產(chǎn)生泄漏,這里的new Handler()創(chuàng)建的也是一個匿名內(nèi)部類

public void post(){
    Handler handler = new Handler(){

        @Override
        public void handleMessage(@NonNull Message msg) {
             super.handleMessage(msg);
             // todo
        }
    };
    Message msg = Message.obtain();
    msg.what = 1;
    handler.sendMessage(msg);
}

引用鏈

  1. 通過post調(diào)用,會將runnable存入創(chuàng)建的Message中,使Message持有runnable
// Handler
public final boolean post(@NonNull Runnable r) {
   return sendMessageDelayed(getPostMessage(r), 0);
}

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

當(dāng)發(fā)送一個Message,最終都會調(diào)用enqueueMessage將Message插入MessageQueue中

同時將當(dāng)前handler對象賦值給Message的target變量,使Message持有handler

// Handler
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
    // 這里的this為Handler
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
  1. MessageQueue是一個單向鏈表,在Looper初始化使創(chuàng)建
// Looper
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
  1. Looper用于為線程運行消息循環(huán)的類,一個線程只有一個Looper,主線程的Loop在ActivityThread中創(chuàng)建,并開啟循環(huán),作用于整個應(yīng)用的生命周期中。
// ActivityThread
public static void main(String[] args) {
        ....
        // 創(chuàng)建主線程的Looper
        Looper.prepareMainLooper();
    // 開啟循環(huán)
    Looper.loop();
    ....
}

引用鏈為:

Looper(****GC Roots****) -> MessageQueue -> Message -> Runnable(或Handler) -> Activity(外部類)

所以插入到MessageQueue中的Message如果還未得到執(zhí)行,其生命周期相當(dāng)于整個應(yīng)用的生命周期,且其間接持有了外部類的引用,所以造成了外部類的泄漏

解決方法

使用靜態(tài)內(nèi)部類+弱引用的方式 可以解決Handler造成外部類泄漏的問題,但是并沒有從根本上解決泄漏,因為MessageQueue仍然持有Message,仍會造成Message的泄漏。

可以如下方式移除當(dāng)前handler創(chuàng)建的Message

handler.removeCallbacksAndMessages(null);

靜態(tài)變量

靜態(tài)變量導(dǎo)致內(nèi)存泄漏的原因是因為短生命周期對象被聲明成為長生命周期對象,靜態(tài)變量是GC Roots的一種

  • 靜態(tài)變量是屬于類的不是屬于實例對象的,存儲在方法區(qū)中
  • 方法區(qū)主要存放永久代對象,而永久代對象的回收率比新生代低很多,所以在方法區(qū)上進(jìn)行回收性價比不高,主要是對常量池的回收和對類的卸載。

類的卸載條件很多,需要滿足以下三個條件,并且滿足了條件也不一定會被卸載:

  • 該類所有的實例都已經(jīng)被回收,此時堆中不存在該類的任何實例。

  • 加載該類的 ClassLoader 已經(jīng)被回收。

  • 該類對應(yīng)的 Class 對象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。

所以一般來說,靜態(tài)變量很少會被GC回收的

比如:

private static Context context;

當(dāng)將Activity賦值給靜態(tài)的context變量時,Activity成為了長生命周期對象GC Roots,而Activity 的生命周期可能很短,用戶一打開就關(guān)閉了。導(dǎo)致Activity無法被回收而產(chǎn)生泄漏

解決方法

如果需要靜態(tài) Context, 可以考慮使用 ApplicationContext,ApplicationContext的生命周期為整個應(yīng)用的生命周期,不會產(chǎn)生泄漏問題。

單例模式

單例模式產(chǎn)生泄漏的原因是因為長生命周期對象持有了短生命周期對象的引用

比如:

final class SingleTon{

    private static SingleTon instance;
    private Context context;

}

單例對象實際是一個靜態(tài)變量,持有了context的引用,當(dāng)context為Activity時,導(dǎo)致Activity無法被回收而產(chǎn)生泄漏

解決方法

如果需要單例需要引用 Context, 可以考慮使用 ApplicationContext,ApplicationContext的生命周期為整個應(yīng)用的生命周期,不會產(chǎn)生泄漏問題。

資源未釋放

  1. 忘了注銷 BroadcastReceiver

  2. 忘了關(guān)閉數(shù)據(jù)庫游標(biāo)(Cursor)

  3. 忘了關(guān)閉流

  4. 忘了調(diào)用 recycle() 方法回收創(chuàng)建的 Bitmap 使用的內(nèi)存

  5. 忘了在 Activity 退出時取消 RxJava 或協(xié)程所開啟異步任務(wù)

  6. Webview

不同的 Android 版本的 Webview 會有差異,加上不同廠商定制 ROM 的 Webview 的差異,導(dǎo)致 Webview 存在很大的兼容問題,一般情況下,在應(yīng)用中只要使用一次 Webview,它占用的內(nèi)存就不會被釋放,解決方案:WebView內(nèi)存泄漏--解決方法小結(jié) 。

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

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

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