手把手講解--性能優化案例(3)內存抖動和洩漏的優化

前言

手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。

文章可能過分詳細,但是這是為了幫助到儘量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.

這個系列的文章:

1、用通俗易懂的講解方式,講解一門技術的實用價值

2、詳細書寫源碼的追蹤,源碼截圖,繪製類的結構圖,儘量詳細地解釋原理的探索過程

3、提供Github 的 可運行的Demo工程,但是我所提供代碼,更多是提供思路,拋磚引玉,請酌情cv

4、集合整理原理探索過程中的一些坑,或者demo的運行過程中的注意事項

5、用gif圖,最直觀地展示demo運行效果

如果覺得細節太細,直接跳過看結論即可。

本人能力有限,如若發現描述不當之處,歡迎留言批評指正。

學到老活到老,路漫漫其修遠兮。與眾君共勉 !

正文大綱

  • jvm內存管理常識
  • 檢測以及處理內存抖動
  • 檢測以及處理內存洩漏

正文

vm內存管理常識

  • LMK (LowMemoryKill)機制
  • android底層會在系統內存告急的時候,按照一定規則殺死一些進程來滿足其他進程的內存需要。其中 消耗內存的高低就是其中一項指標,所以,優化app的內存佔用,能夠有效降低app被系統殺死的概率。
  • GC STW機制
  • GC,垃圾回收進程,在GC線程執行任務的時候,會存在一個 STW (stop the world) 機制,他就會把其他所有線程都掛起。如果GC非常頻繁地調用,那就會導致主線程不流暢,給用戶的感覺就是卡頓。
  • 內存抖動頻繁引起OOM
  • 內存抖動太頻繁,導致大量對象頻繁創建和銷燬,會產生大量不連續的內存空間,如果此時有一個大對象需要申請內存,就有可能申請失敗,導致OOM內存溢出。
  • 一句話解釋 內存洩漏
  • 長生命週期的對象持有短生命週期對象的強引用,在短生命週期對象需要回收的時候發現不能被回收,視為洩漏
  • GC回收 可達性分析
  • GC線程判定 一個對象是不是可以回收,是根據可達性分析算法,計算GcRoot,從GcRoot向下搜索,把GcRoot沒有直接關聯的對象全部作為垃圾來回收。
  • 強軟弱虛四大引用
  • 強和虛自不必說。強 最常見,沒有特殊處理的都是強引用(包括,匿名內部類會持有外部類的強引用)。虛引用沒什麼用,不予討論。
  • 軟引用,用來定義一些還有用,但是不是必須的對象,使用SoftRefrence修飾,在內存緊張的時候,GC回收之後,使用SoftRefrence修飾,如果系統還有足夠的內存可用,那麼軟引用關聯的對象就不會被回收。如果不足,則回收軟引用關聯的對象。
  • 弱引用(WeakRefrence),比軟引用更弱一些,只要GC觸發,弱引用關聯的對象就會被回收。
  • 注意,使用軟和弱引用,要判定關聯對象是否為空。

檢測以及處理內存抖動

我們使用s開發,平時我們運行app,一般會點 RunApp,但是還有另一個選擇, 那就是 profileApp, 運行app起來之後,會在as下方看到profile 窗口

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

點擊之後,as下方會出現profile,圖中會顯示網絡,內存和cpu使用情況

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

如果內存的圖中抖動得非常明顯,比如像這樣的心電圖一樣:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

那就說明非常明顯存在內存抖動,急需處理:

點擊內存圖形區域之後,就能看到詳細的內存變化情況,以及內存分配情況:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

這裡有個坑

如果你從圖形中觀察到,內存走勢平穩,並沒有出現上滿模擬抖動的圖中那麼誇張,是不是就不存在內存抖動呢?並不是。因為我們的gc,是在內存不可用的情況下才會去回收內存,如果app佔用內存一直比較少,沒有觸及gc的臨界值,那麼就不會出現斷崖式下跌. 那麼這樣就觀察不出內存抖動了,怎麼辦呢?

解決方法

在8.0以下的安卓手機上,在下方的位置上會出現一個Record按鈕(如果是8.0以上,你可以直接用拖拽的方式來截取一段內存record):

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

點擊它,一段時間之後,再點一下:你就能在下方發現一張表格:[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XZh3ddJV-1571377551222)

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

這張表格代表的是,你Record這段時間之內創建的對象,點擊一下第二列Allocations,對創建的數量進行排序,找出創建次數最多的對象:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

然後,點擊排行第一的String之後,會在右方看到:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

然後點擊其中的一個,又會看到一個新的窗口:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

到此為止,就找到了創建對象的元兇,以這個為線索,找到你們自己包名下的類和方法,確定是我們自己的代碼在不合理地創建對象.

再往後,就是根據各自的業務代碼去做優化了,記住一個宗旨:不要讓代碼幹多餘的事。如果是我們調用了系統的api導致了不合理地大量對象的創建,那麼就要考慮這個系統API為什麼會這樣創建對象,有沒有其他方法避免嗎,從業務代碼層來合理使用這個api,實在不行再考慮自定義api或者換個系統api。

在我們做了一次優化之後,再profile運行一次app,再重複上面的過程。以此類推,直到內存抖動達到理想狀態。

總結

優化內存抖動,核心就是防止頻繁創建對象。常見的反面教材就是:循環中創建對象,大量調用的api中創建對象。而優化的主要手段,就是對象複用,常見的手段是:對象池,像是 Handler的Message 單鏈表池,Glide的bitmap池等。

檢測以及處理內存洩漏

經典案例:處理handler異步任務導致的內存洩漏方法

  • 在Activity的onDestroy中移除所有的任務
 @Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);//移除所有任務
}
  • 使用靜態內部類 + Activity弱引用的方式
 MyHandler handler = new MyHandler(this);
private static class MyHandler extends Handler {
WeakReference<activity> activityWeakReference;
MyHandler(Activity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
//在執行任務的時候,判斷弱引用所關聯的對象是否為空,能在對象已經被回收的情況下,不去執行不必要的任務
switch (msg.what) {
case 1:
if (activityWeakReference.get() != null) {
//TODO
}
}
}
}
/<activity>

工具的使用

依然是profileApp,先用profile看出內存的變化情況。

  • 問:如何判斷內存洩漏?
  • 答:內存洩漏是精細功夫,不能全盤觀察,只能憑藉profile的內存變化來推測
  • 比如,打開app之後內存一路飆升,直到超出app能夠使用的最大內存,app崩潰,,這是最明顯的。
  • 又比如,你反覆打開關閉某一個界面,發現內存的穩定線(內存穩定之後,內存佔用值)隨著每一次的打開關閉,都在提高,這說明,這一個界面上存在洩漏,有對象無法被回收。

上一章節使用profile 最多是瞭解到 哪些對象的創建和回收引起了內存抖動,但是,涉及到洩漏,只通過profile尚且不能知道是哪個類持有了希望被回收的對象的強引用.

這裡就要藉助另外一款工具,他的名字叫做 Eclipse Mat (自行百度)

先回到剛才的profile,

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

點一下,然後再點一下,界面會自動跳轉:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

點擊上面的保存按鈕,將文件存到本地;

然後:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

但是這個文件是無法直接在mat打開的。

找到SDK目錄下的要hprof-conv.exe:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

使用cmd命令,對文件進行轉換,命令為:hprof-conv [源文件名] [目標文件名]

如 hprof-conv 1.hprof 2.hprof 回車

將得到的2.hprof利用剛才下載的Mat工具打開:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

這裡有很多指標,但是檢查內存洩漏,我們只需要關注這個直方圖按鈕即可:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

這個圖中會列出你dump的這一段內存中的所有對象,包括framework層的,也包括我們自己代碼創建的對象。

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

案例模擬

我模擬了一個經典案例,也就是前面提到的Handler延時任務導致Activity不能被釋放,核心代碼如下:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

我就用一個非常普通的方式創建了一個handler對象,並且用它來執行一段延時任務,只不過,延時任務的延時時間是Integer的最大值,也就是說,任務要很久以後才會執行。之後,我反覆進出這一個Activity,然後按照上面的方式dump了一段 hprof,經過 hprof-conv 轉化,然後用Mat打開:

結果如下:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

我填寫過濾信息:SecondActivity 回車:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

在我們最終退出SecondActivity之後,內存中依然保留了18個無用的對象。

那麼是不是我們這18個都是洩漏的呢?

不一定。

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

前文講過,只有不合理的強引用,才會導致內存洩漏,所以我們要按照上面的方式排除軟弱虛引用。

之後我們能看到下面的界面,把能展開的信息盡數展開:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

瞭解Handler源碼的同志們應該一眼就看明白了,handler引起了內存洩漏,是因為存在不合理地強引用鏈,

上圖中可以看出,最終是callback對象持有了 SecondActivity對象。

callback 任務的延時時間太長了,還沒有執行完,所以強引用不會給你釋放掉,而callback持有了Activity,導致Activity不能被釋放。

如何優化內存洩漏

我們剛才已經看到了Handler的不合理使用導致了內存洩漏,那麼如果在onDestroy中移除所有的任務:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

執行同樣的任務,dump下來的hprof 在mat:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

觸發了GC之後,SecondActivity數量變為了0,內存洩漏解決。

當然還有另一種做法,靜態內部類+弱引用。

ps:靜態內部類是為了防止內部類持有外部類的引用,弱引用是為了在GC觸發之時,回收掉WeakRefrence中的對象。

public class SecondActivity extends AppCompatActivity {
Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
handler.postDelayed(runnable, Integer.MAX_VALUE);//依然是那個延時很久的任務
}
Runnable runnable = new MyRunnable(this);
private static class MyRunnable implements Runnable {// 靜態內部類
WeakReference<activity> activityWeakReference;//弱引用
MyRunnable(Activity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void run() {
}
}
}
/<activity>
手把手講解--性能優化案例(3)內存抖動和洩漏的優化

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

但是排除之後,一個都沒有了。

小技巧

上面的步驟雖然可行,但是如果有很多頁面都需要排查洩漏,那麼我們一個一個頁面去點開關閉,整個過程將會非常冗長難受。其實有辦法解決。

回到之前的直方圖:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

使用方法為:如果你想進行一個操作,你操作前後各dump一個hprof,命名為 before和after, 然後用hprof-conv轉換一下,變為 before_ 和 after_ ,用eclipse mat同時打開這兩個文件,然後切換到after_.hprof ,點擊上圖中的按鈕:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

它會讓你選擇想要對比的文件,點擊before_,然後過濾SecondActivity:

手把手講解--性能優化案例(3)內存抖動和洩漏的優化

這種方式可以在處理洩漏之前,事先排查可能洩露的代碼區域。簡化我們的優化工作。

結語

內存抖動和洩漏優化涉及到Jvm很多知識點,除了我之前列出的幾點之外,還有很多細枝末節。要做好 內存優化,需要紮實的JVM知識基礎。

如果你的技術提升遇到瓶頸了,或者缺高級Android進階視頻學習提升自己,這有大量大廠面試題為你面試做準備!可以私聊我領取喔~

你的贊和關注是我繼續創作的動力~


分享到:


相關文章: