Android—內存泄漏及LeakCanary源碼解析

內存泄露:程序在向系統申請分配內存空間後(new),在使用完畢後未釋放。結果導致一直佔據該內存單元,我們和程序都無法再使用該內存單元,直到程序結束,這是內存泄露。

內存溢出(OOM):程序向系統申請的內存空間超出了系統能給的。比如內存只能分配一個int類型,我卻要塞給他一個long類型,系統就出現oom。又比如一車最多能坐5個人,你卻非要塞下10個,車就擠爆了。

大量的內存泄露會導致內存溢出(oom)。

引起內存泄漏的情況

1.對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷。

  • 比如在Activity中register了一個BraodcastReceiver,但在Activity結束後沒有unregister該BraodcastReceiver。
  • 資源性對象比如Cursor,Stream、File文件等往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。它們的緩衝不僅存在於 java虛擬機內,還存在於java虛擬機外。如果我們僅僅是把它的引用設置爲null,而不關閉它們,往往會造成內存泄漏。
  • 對於資源性對象在不使用的時候,應該調用它的close()函數將其關閉掉,然後再設置爲null。在我們的程序退出時一定要確保我們的資源性對象已經關閉。
  • Bitmap對象不在使用時調用recycle()釋放內存。2.3以後的bitmap應該是不需要手動recycle了,內存已經在java層了。

2.內部類持有外部類引用,Handler,Runnable匿名內部類導致內存泄露,內部類天然持有外部類的實例引用,activity finish掉了,那麼消息隊列的消息依舊會由handler進行處理,若此時handler聲明爲內部類,那麼就會導致activity無法回收,進而導致activity泄露。​​

  • 在onDestroy方法中調用Handler的removeCallbacksAndMessages方法,清空消息。
  • 靜態內部類不持有外部類的引用,使用靜態的handler不會導致activity的泄露,還要用WeakReference 包裹外部類的對象。弱引用:private WeakReference<Activity類名> weakReference;  創建Handler時傳入activity對象。因爲我們需要使用外部類的成員,可以通過"activity. "獲取變量方法等,如果直接使用強引用,顯然會導致activity泄露。
  • 將Handler放到抽取出來放入一個單獨的頂層類文件中。
  • 不要讓生命週期長於Activity的對象持有到Activity的引用。

3.靜態內部類持有外部成員變量(或context):可以使用弱引用、使用ApplicationContext。

  • 用 Activity 或Service的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例對象所持有,其生命週期等於整個應用程序的生命週期,所以當前 Activity 退出時它的內存並不會被回收,這就造成泄漏了。
  • 用 Application 的 Context,因爲 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。
  • 弱引用:private WeakReference<類名> weakReference;  創建靜態內部類對象時傳入外部類。

4.集合中沒用的對象沒有及時remove。

  • 我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。
  • 在退出程序之前,將集合裏的東西clear,然後置爲null,再退出程序。

5.WebView

WebView單獨存放在一個activity中,onDestory()中調用android.os.Process.killProcess(android.os.Process.myPid())等方法殺死進程。

activity泄漏可以使用LeakCanary。

內存

JAVA是在JVM所虛擬出的內存環境中運行的,JVM的內存可分爲三個區:堆(heap)、棧(stack)和方法區(method)。

棧(stack):存放基本類型變量數據和對象的引用。(不是對象)。

堆(heap):堆內存用於存放由new創建的對象和數組。在堆中分配的內存,由java虛擬機自動垃圾回收器來管理。JVM只有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身。

常量池:存放字符串常量和基本類型常量(public static final)

方法區(method):又叫靜態區,跟堆一樣,被所有的線程共享。方法區包含所有的class和static變量。

內存泄露原因分析

在JAVA中JVM的棧記錄了方法的調用,每個線程擁有一個棧。在線程的運行過程當中,執行到一個新的方法調用,就在棧中增加一個內存單元,即幀(frame)。在frame中,保存有該方法調用的參數、局部變量和返回地址。然而JAVA中的局部變量只能是基本類型變量(int),或者對象的引用。所以在棧中只存放基本類型變量和對象的引用。引用的對象保存在堆中。

當某方法運行結束時,該方法對應的frame將會從棧中刪除,frame中所有局部變量和參數所佔有的空間也隨之釋放。線程回到原方法繼續執行,當所有的棧都清空的時候,程序也就隨之運行結束。

而對於堆內存,堆存放着普通變量。在JAVA中堆內存不會隨着方法的結束而清空,所以在方法中定義了局部變量,在方法結束後變量依然存活在堆中。

綜上所述,棧(stack)可以自行清除不用的內存空間。但是如果我們不停的創建新對象,堆(heap)的內存空間就會被消耗盡。所以JAVA引入了垃圾回收(garbage collection,簡稱GC)去處理堆內存的回收。

當一個對象不需要使用了,但是其他對象一直有該對象的引用會導致該對象無法被回收,造成內存的浪費。這就是造成內存泄露的原因。

垃圾回收機制

垃圾回收(garbage collection,簡稱GC)可以自動清空堆中不再使用的對象。在JAVA中對象是通過引用使用的。如果再沒有引用指向該對象,那麼該對象就無從處理或調用該對象,這樣的對象稱爲不可到達(unreachable)。垃圾回收用於釋放不可到達的對象所佔據的內存。

垃圾回收實現思想

引用計數法

簡單理解就是記錄一個對象被引用的次數,一個引用被使用引用計數器就+1,反之就-1,當引用次數爲0就說明是一個垃圾對象可以被回收了。

可達性分析法

根據是否被GC Root引用確認是否是垃圾對象要被GC回收。

引用類型

從JDK 1.2版本開始,把對象的引用分爲4種級別,從而使程序能更加靈活地控制對象的生命週期。這4種級別由高到低依次爲:強引用、軟引用、弱引用和虛引用。

1. 強引用(Strong reference)
實際編碼中最常見的一種引用類型。常見形式如:A a = new A();等。強引用本身存儲在棧內存中,其存儲指向對內存中對象的地址。如果一個對象具有強引用,則無論在什麼情況下,GC都不會回收被引用的對象。

2. 軟引用(Soft Reference)
軟引用的一般使用形式如下:

A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);

表示一個對象處在有用但非必須的狀態。如果一個對象具有軟引用,在內存空間充足時,GC就不會回收該對象;當內存空間不足時,GC會回收該對象的內存(回收發生在OutOfMemoryError之前)。

3. 弱引用(Weak Reference)
同樣的,軟引用的一般使用形式如下:

A a = new A();
WeakReference<A> wrA = new WeakReference<A>(a);

無論當前內存是否緊缺,GC都將回收被弱引用關聯的對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被GC回收了,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中,以便在恰當的時候將該弱引用回收。

4. 虛引用(Phantom Reference)

虛引等同於沒有引用,這意味着在任何時候都可能被GC回收,設置虛引用的目的是爲了被虛引用關聯的對象在被垃圾回收器回收時,能夠收到一個系統通知。

虛引用在使用時必須和引用隊列(ReferenceQueue)聯合使用。

ReferenceQueue queue=new ReferenceQueue();
PhantomReference pr=new PhantomReference(object.queue);

內存泄露原因

不再需要的對象依然被引用,導致對象被分配的內存無法被回收

其實在Android中會造成內存泄露的情景無外乎兩種:

  • 全局進程(process-global)的static變量。這個無視應用的狀態,持有Activity的強引用的怪物。
  • 活在Activity生命週期之外的線程。沒有清空對Activity的強引用。

LeakCanary

由Square開源的一款輕量第三方內存泄漏檢測工具,可以實時監測Activity,並提高內存泄漏信息。

原理:watch一個即將要銷燬的對象

使用方法:在Applicantion的子類裏。

 LeakCanary.install(this);
  public static @NonNull RefWatcher install(@NonNull Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

內部建立了一個refWatcher對象,通過refWatcher監聽activity回收情況。

  • DisplayService是發生內存泄漏時的通知服務

  • excludedRefs()是排除Android源碼出現的內存泄漏問題

最主要的是AndroidRefWatcherBuilder.buildAndInstall()

進入buildAndInstall()方法。

  public @NonNull RefWatcher buildAndInstall() {
    if (LeakCanaryInternals.installedRefWatcher != null) {
      throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
// 根據app包名生成LeakCanary關聯應用
      if (enableDisplayLeakActivity) {
        LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
      }
// 監聽Activity
      if (watchActivities) {
        ActivityRefWatcher.install(context, refWatcher);
      }
// 監聽Fragment
      if (watchFragments) {
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }

  ActivityRefWatcher.install(context, refWatcher);

  public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

// 通過在Application註冊監聽每個Activity的生命週期,然後轉發給RefWatcher
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          refWatcher.watch(activity);
        }
      };

  FragmentRefWatcher.Helper.install(context, refWatcher);

    public static void install(Context context, RefWatcher refWatcher) {
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
      .......
      Helper helper = new Helper(fragmentRefWatchers);
      Application application = (Application) context.getApplicationContext();

//給application註冊activityLifecycleCallbacks監聽
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }

//在onActivityCreated的時候遍歷fragmentRefWatchers隊列給每個watcher調用watchFragments(activity)方法
    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity); 
            }
          }
        };

// 到watchFragments(activity)方法中
    private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {

//在Fragment摧毀的時候調用watch方法
        @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
          View view = fragment.getView();
          if (view != null) {
            refWatcher.watch(view);
          }
        }

        @Override
        public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          refWatcher.watch(fragment);
        }
      };

  @Override public void watchFragments(Activity activity) {
    FragmentManager fragmentManager = activity.getFragmentManager();
//watchFragments方法裏面又註冊了一層監聽
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
  }

總結:

  • 通過 Application.registerActivityLifecycleCallbacks() 監聽Activity的生命週期,在onActivityDestroyed階段調用RefWatcher的watch(Object)方法。
  • 通過FragmentManager.registerFragmentLifecycleCallbacks() 監聽Fragment的生命週期並在onFragmentViewDestroyed和onFragmentDestroyed階段調用RefWatcher的watch(Object)方法。

我們得知監聽最後都是通過調用RefWatcher的watch(Object)方法,接下來到RefWatcher的watch(Object)方法裏面看看。

//watchedReference傳入的就是activity或fragment對象
  public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
//爲監聽生成唯一ID
    String key = UUID.randomUUID().toString();
//將key值存入一個隊列
    retainedKeys.add(key);

//生成reference對象,封裝了watchedReference和key等。
//queue是Reference隊列,對象被回收了,那麼對應的弱引用對象會在回收時被添加到queue中,
//重點:KeyedWeakReference內部繼承了弱引用類,reference是弱引用對象,調用GC時就會被回收。
    final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
//最重要的是這個方法,傳入了監聽對象
    ensureGoneAsync(watchStartNanoTime, reference);
  }

//watchExecutor.execute方法確保了主線程空閒,生命週期走完後纔會在子線程運行run方法
  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

接下來看看ensureGone方法的具體實現

  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

// 將queue隊列中對象的key從key隊列中移除,因爲在queue隊列的reference已經被回收
    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }

// 如果key隊列沒有該reference的key值,說明在某次GC已經回收該對象,沒有內存泄漏,不需要處理
    if (gone(reference)) {
      return DONE;
    }

// 執行一次GC
    gcTrigger.runGc();

// 再檢查
    removeWeaklyReachableReferences();

// key隊列還有key值,說明已經內存泄漏,dump
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }

  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

ensureGone()主要將已經GC掉的弱引用對象從隊列中移除,然後判斷是否已經沒有,如果沒有跳出,有則會執行GC後再檢查,如果有說明內存泄漏,創建dump文件,分析dump文件。

RefWatcher.watch(Object)總結:

RefWatcher.watch(Object)會爲每一個監聽引用對象(Activity或Fragment)提供唯一ID,RefWatcher類有兩個隊列分別存放reference對象和key,在主線程走完生命週期後通過 Retryable.run()在子線程檢測內存泄漏,監測是ensureGone()方法通過兩個隊列來判斷其是否reference對象是否GC回收。

LeakCanary總結:

LeakCanary調用install方法,內部創建RefWather對象,對象分別設置了對Activity和Fragment在Destroy階段的監聽,就是當Activity和Fragment在Destroy階段時調用watch方法傳入Activity或Fragment對象並生成相對應的唯一值key封裝成reference弱引用對象(注意是弱引用對象,因爲弱引用對象GC後會被傳入Reference隊列),最後調用sureGone方法在主線程生命週期走完後,在子線程中進行檢查,先將Reference隊列的對象的key從key隊列中移出,再判斷現reference對象的key在不在key隊列裏,不在則該對象已經被回收,否則再調用一次GC,再判斷一次,有內存泄漏便會進行 heap dump 的操作以獲取當前內存的 hprof。通過對hprof文件進行解析,計算出GC root的最短引用路徑,反饋異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章