1. 程式人生 > >Android 性能優化之內存泄漏檢測以及內存優化(中)

Android 性能優化之內存泄漏檢測以及內存優化(中)

finall 針對 context cal 拼接 靜態內部類 操作 bit 等等

https://blog.csdn.net/self_study/article/details/66969064

上篇博客我們寫到了 Java/Android 內存的分配以及相關 GC 的詳細分析,這篇博客我們會繼續分析 Android 中內存泄漏的檢測以及相關案例,和 Android 的內存優化相關內容。
  上篇:Android 性能優化之內存泄漏檢測以及內存優化(上)。
  中篇:Android 性能優化之內存泄漏檢測以及內存優化(中)。
  下篇:Android 性能優化之內存泄漏檢測以及內存優化(下)。
  轉載請註明出處:http://blog.csdn.net/self_study/article/details/66969064
  對技術感興趣的同鞋加群544645972一起交流。

Android 內存泄漏檢測

  通過上篇博客我們了解了 Android JVM/ART 內存的相關知識和泄漏的原因,再來歸類一下內存泄漏的源頭,這裏我們簡單將其歸為一下三類:

自身編碼引起
由項目開發人員自身的編碼造成;
第三方代碼引起
這裏的第三方代碼包含兩類,第三方非開源的 SDK 和開源的第三方框架;
系統原因
由 Android 系統自身造成的泄漏,如像 WebView、InputMethodManager 等引起的問題,還有某些第三方 ROM 存在的問題。
Android 內存泄漏的定位,檢測與修復

  內存泄漏不像閃退的 BUG,排查起來相對要困難一些,比較極端的情況是當你的應用 OOM 才發現存在內存泄漏問題,到了這種情況才去排查處理問題的話,對用戶的影響就太大了,為此我們應該在編碼階段盡早地發現問題,而不是拖到上線之後去影響用戶體驗,下面總結一下常用內存泄漏的定位和檢測工具:

Lint

  Lint 是 Android studio 自帶的靜態代碼分析工具,使用起來也很方便,選中需要掃描的 module,然後點擊頂部菜單欄 Analyze -> Inspect Code ,選擇需要掃描的地方即可:
      

最後在 Performance 裏面有一項是 Handler reference leaks,裏面列出來了可能由於內部 Handler 對象持有外部 Activity 引用導致內存泄漏的地方,這些地方都可以根據實際的使用場景去排查一下,因為畢竟不是每個內部 Handler 對象都會導致內存泄漏。Lint 還可以自定義掃描規則,使用姿勢很多很強大,感興趣的可以去了解一下,除了 Lint 之外,還有像 FindBugs、Checkstyle 等靜態代碼分析工具也是很不錯的。

StrictMode

  StrictMode 是 Android 系統提供的 API,在開發環境下引入可以更早的暴露發現問題給開發者,於開發階段解決它,StrictMode 最常被使用來檢測在主線程中進行讀寫磁盤或者網絡操作等耗時任務,把這些耗時任務放置於主線程會造成主線程阻塞卡頓甚至可能出現 ANR ,官方例子:

public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
把上面這段代碼放在早期初始化的 Application、Activity 或者其他應用組件的 onCreate 函數裏面來啟用 StrictMode 功能,一般 StrictMode 只是在測試環境下啟用,到了線上環境就不要開啟這個功能。啟用 StrictMode 之後,在 logcat 過濾日誌的地方加上 StrictMode 的過濾 tag,如果發現一堆紅色告警的 log,說明可能就出現了內存泄漏或者其他的相關問題了:

比如上面這個就是因為調用 registerReceiver 之後忘記調用 unRegisterReceiver 導致的 activity 泄漏,根據錯誤信息便可以定位和修復問題。

LeakCanary

   LeakCanary 是一個 Android 內存泄漏檢測的神器,正確使用可以大大減少內存泄漏和 OOM 問題,地址:

https://github.com/square/leakcanary
1
集成 LeakCanary 也很簡單,在 build.gradle 文件中加入:

dependencies {
debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.5‘
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5‘
testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5‘
}
1
2
3
4
5
然後在 Application 類中添加下面代碼:

public class ExampleApplication extends Application {

@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
上面兩步做完之後就算是集成了 LeakCanary 了,非常簡單方便,如果程序出現了內存泄漏會彈出 notification,點擊這個 notification 就會進入到下面這個界面,或者集成 LeakCanary 之後在桌面會有一個 LeakCanary 的圖標,點擊進去是所有的內存泄漏列表,點擊其中一項同樣是進入到下面界面:

這個界面就會詳細展示引用持有鏈,一目了然,對於問題的解決方便了很多,堪稱神器,更多實用姿勢可以看看 LeakCanary FAQ。
  還有一點需要提到的是,LeakCanary 在檢測內存泄漏的時候會阻塞主界面,這是一點體驗有點不爽的地方,但是這時候阻塞肯定是必要的,因為此時必須要掛起線程來獲取當前堆的狀態。然後也並不是每個 LeakCanary 提示的地方都有內存泄漏,這時候可能需要借助 MAT 等工具去具體分析。不過 LeakCanary 有一點非常好的地方是因為 Android 系統也會有一些內存泄漏,而 LeakCanary 對此則提供了一個 AndroidExcludedRefs 類來幫助我們排除這些問題。

Android Memory Monitor

  Memory Monitor 是 Android Studio 自帶的一個監控內存使用狀態的工具,入口如下所示:

在 Android Monitor 點開之後 logcat 的右側就是 Monitor 工具,其中可以檢測內存、CPU、網絡等內容,我們這裏只用到了 Memory Monitor 功能,點擊紅色箭頭所指的區域,就會 dump 此時此刻的 Memory 信息,並且生成一個 .hprof 文件,dump 完成之後會自動打開這個文件的顯示界面,如果沒有打開,可以通過點擊最左側的 Capture 界面或者 Tool Window 裏面的 Capture 進入 dump 的 .hprof 文件列表:

  接著我們來分析一下這個生成的 .hprof 文件所展示的信息:

首先左上角的下拉框,可以選擇 App Heap、Image Heap 和 Zygote Heap,對應的就是上篇博客講到的 Allocation Space,Image Space 和 Zygote Space,我們這裏選擇 Allocation Space,然後第二個選擇 PackageTreeView 這一項,展開之後就能看見一個樹形結構了,然後繼續展開我們應用包名的對應對象,就可以很清晰的看到有多少個 Activity 對象了,上面那兩欄展示的信息按照從左到右的順序,定義如下所示:

Column Description
Class Name 占有這塊內存的類名
Total Count 未被處理的數量
Heap Count 在上面選擇的指定 heap 中的數量
Sizeof 這個對象的大小,如果在變化中,就顯示 0
Shallow Size 在當前這個 heap 中的所有該對象的總數
Retained Size 這個類的所有對象占有的總內存大小
Instance 這個類的指定對象
Reference Tree 指向這個選中對象的引用,還有指向這個引用的引用
Depth 從 GC Root 到該對象的引用鏈路的最短步數
Shallow Size 這個引用的大小
Dominating Size 這個引用占有的內存大小
然後可以點擊展開右側的 Analyzer Tasks 項,勾選上需要檢測的任務,然後系統就會給你分析出結果:

從分析的結果可以看到泄漏的 Activity 有兩個,非常直觀,然後點開其中一個,觀察下面的 ReferenceTree 選項:

可以看到 Thread 對象持有了 SecondActivity 對象的引用,也就是 GC Root 持有了該 Activity 的引用,導致這個 Activity 無法回收,問題的根源我們就發現了,接下來去處理它就好了。
  關於更多 Android Memory Monitor 的使用可以去看看這個官方文檔:HPROF Viewer and Analyzer。

MAT

  MAT(Memory Analyzer Tools)是一個 Eclipse 插件,它是一個快速、功能豐富的 JAVA heap 分析工具,它可以幫助我們查找內存泄漏和減少內存消耗,MAT 插件的下載地址:Eclipse Memory Analyzer Open Source Project,上面通過 Android studio 生成的 .hprof 文件因為格式稍有不同,所以需要經過一個簡單的轉換,然後就可以通過 MAT 去打開了:

通過 MAT 去打開轉換之後的這個文件:

用的最多的就是 Histogram 功能,點擊 Actions 下的 Histogram 項就可以得到 Histogram 結果:

我們可以在左上角寫入一個正則表達式,然後就可以對所有的 Class Name 進行篩選了,很方便,頂欄展示的信息 “Objects” 代表該類名對象的數量,剩下的 “Shallow Heap” 和 “Retained Heap” 則和 Android Memory Monitor 類似。咱們接著點擊 SecondActivity,然後右鍵:

在彈出來的菜單中選擇 List objects->with incoming references 將該類的實例全部列出來:

通過這個列表我們可以看到 SecondActivity@0x12faa900 這個對象被一個 this$00x12c65140 的匿名內部類對象持有,然後展開這一項,發現這個對象是一個 handler 對象:

快速定位找到這個對象沒有被釋放的原因,可以右鍵 Path to GC Roots->exclude all phantom/weak/soft etc. references 來顯示出這個對象到 GC Root 的引用鏈,因為強引用才會導致對象無法釋放,所以這裏我們要排除其他三種引用:

這麽處理之後的結果就很明顯了:

一個非常明顯的強引用持有鏈,GC Root 我們前面的博客中說到包含了線程,所以這裏的 Thread 對象 GC Root 持有了 SecondActivity 的引用,導致該 Activity 無法被釋放。
  MAT 還有一個功能就是能夠對比兩個 .hprof 文件,將兩個文件都添加到 Compare Basket 裏面:

添加進去之後點擊右上角的 ! 按鈕,然後就會生成兩個文件的對比:

同樣適用正則表達式將需要的類篩選出來:

結果也很明顯,退出 Activity 之後該 Activity 對象未被回收,仍然在內存中,或者可以調整對比選項讓對比結果更加明顯:

也可以對比兩個對象集合,方法與此類似,都是將兩個 Dump 結果中的對象集合添加到 Compare Basket 中去對比,找出差異後用 Histogram 查詢的方法找出 GC Root,定位到具體的某個對象上。

adb shell && Memory Usage

  可以通過命令 adb shell dumpsys meminfo [package name] 來將指定 package name 的內存信息打印出來,這種模式可以非常直觀地看到 Activity 未釋放導致的內存泄漏:

或者也可以通過 Android studio 的 Memory Usage 功能進行查看,最後的結果是一樣的:


Allocation Tracker

  Android studio 還自帶一個 Allocation Tracker 工具,功能和 DDMS 中的基本差不多,這個工具可以監控一段時間之內的內存分配:

在內存圖中點擊途中標紅的部分,啟動追蹤,再次點擊就是停止追蹤,隨後自動生成一個 .alloc 文件,這個文件就記錄了這次追蹤到的所有數據,然後會在右上角打開一個數據面板:

這個工具詳細的介紹可以看看這個博客:Android性能專項測試之Allocation Tracker(Android Studio)。

常見的內存泄漏案例

  我們來看看常見的導致內存泄漏的案例:

靜態變量造成的內存泄漏

  由於靜態變量的生命周期和應用一樣長,所以如果靜態變量持有 Activity 或者 Activity 中 View 對象的應用,就會導致該靜態變量一直直接或者間接持有 Activity 的引用,導致該 Activity 無法釋放,從而引發內存泄漏,不過需要註意的是在大多數這種情況下由於靜態變量只是持有了一個 Activity 的引用,所以導致的結果只是一個 Activity 對象未能在退出之後釋放,這種問題一般不會導致 OOM 問題,只能通過上面介紹過的幾種工具在開發中去觀察發現。
  這種問題的解決思路很簡單,就是不讓靜態變量直接或者間接持有 Activity 的強引用,可以將其修改為 soft reference 或者 weak reference 等等之類的,或者如果可以的話將 Activity Context 更換為 Application Context,這樣就能保證生命周期一致不會導致內存泄漏的問題了。

內部類持有外部類引用

  我們上面的 demo 中模擬的就是內部類對象持有外部類對象的引用導致外部類對象無法釋放的問題,在 Java 中非靜態內部類和匿名內部類會持有他們所屬外部類對象的引用,如果這個非靜態內部類對象或者匿名內部類對象被一個耗時的線程(或者其他 GC Root)直接或者間接的引用,甚至這些內部類對象本身就在做一些耗時操作,這樣就會導致這個內部類對象直接或者間接無法釋放,內部類對象無法釋放,外部類的對象也就無法釋放造成內存泄漏,而且如果無法釋放的對象積累起來就會造成 OOM,示例代碼如下所示:

public class SecondActivity extends AppCompatActivity{
private Handler handler;
private Bitmap bitmap;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一個大圖來模擬內存無法釋放導致的崩潰
findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});

handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);

}
};
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
}).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  這個問題的解決方法可以根據實際情況進行選擇:

將非靜態內部類或者匿名內部類修改為靜態內部類,比如 Handler 修改為靜態內部類,然後讓 Handler 持有外部 Activity 的一個 Weak Reference 或者 Soft Reference;
在 Activity 頁面銷毀的時候將耗時任務停止,這樣就能保證 GC Root 不會間接持有 Activity 的引用,也就不會導致內存泄漏;
錯誤使用 Activity Context

  這個很好理解,在一個錯誤的地方使用 Activity Context,造成 Activity Context 被靜態變量長時間引用導致無法釋放而引發的內存泄漏,這個問題的處理方式也很簡單,如果可以的話修改為 Application Context 或者將強引用變成其他引用。

資源對象沒關閉造成的內存泄漏

  資源性對象比如(Cursor,File 文件等)往往都用了一些緩沖,我們在不使用的時候應該及時關閉它們,以便它們的緩沖對象被及時回收,這些緩沖不僅存在於 java 虛擬機內,還存在於 java 虛擬機外,如果我們僅僅是把它的引用設置為 null 而不關閉它們,往往會造成內存泄漏。但是有些資源性對象,比如 SQLiteCursor(在析構函數 finalize(),如果我們沒有關閉它,它自己會調 close() 關閉),如果我們沒有關閉它系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性對象在不使用的時候,應該調用它的 close() 函數,將其關閉掉,然後再置為 null,在我們的程序退出時一定要確保我們的資源性對象已經關閉。
  程序中經常會進行查詢數據庫的操作,但是經常會有使用完畢 Cursor 後沒有關閉的情況,如果我們的查詢結果集比較小,對內存的消耗不容易被發現,只有在常時間大量操作的情況下才會出現內存問題,這樣就會給以後的測試和問題排查帶來困難和風險,示例代碼:

Cursor cursor = getContentResolver().query(uri...);
if (cursor.moveToNext()) {
... ...
}
1
2
3
4
更正代碼:

Cursor cursor = null;
try {
cursor = getContentResolver().query(uri...);
if (cursor != null && cursor.moveToNext()) {
... ...
}
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
//ignore this
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
集合中對象沒清理造成的內存泄漏

  在實際開發過程中難免會有把對象添加到集合容器(比如 ArrayList)中的需求,如果在一個對象使用結束之後未將該對象從該容器中移除掉,就會造成該對象不能被正確回收,從而造成內存泄漏,解決辦法當然就是在使用完之後將該對象從容器中移除。

WebView造成的內存泄露

  具體的可以看看我的這篇博客:android WebView詳解,常見漏洞詳解和安全源碼(下)。

未取消註冊導致的內存泄漏

  一些 Android 程序可能引用我們的 Android 程序的對象(比如註冊機制),即使我們的 Android 程序已經結束了,但是別的應用程序仍然還持有對我們 Android 程序某個對象的引用,這樣也會造成內存不能被回收,比如調用 registerReceiver 後未調用unregisterReceiver。假設我們希望在鎖屏界面(LockScreen)中,監聽系統中的電話服務以獲取一些信息,則可以在 LockScreen 中定義一個 PhoneStateListener 的對象,同時將它註冊到 TelephonyManager 服務中,對於 LockScreen 對象,當需要顯示鎖屏界面的時候就會創建一個 LockScreen 對象,而當鎖屏界面消失的時候 LockScreen 對象就會被釋放掉,但是如果在釋放 LockScreen 對象的時候忘記取消我們之前註冊的 PhoneStateListener 對象,則會間接導致 LockScreen 無法被回收,如果不斷的使鎖屏界面顯示和消失,則最終會由於大量的 LockScreen 對象沒有辦法被回收而引起 OOM,雖然有些系統程序本身好像是可以自動取消註冊的(當然不及時),但是我們還是應該在程序結束時明確的取消註冊。

因為內存碎片導致分配內存不足

  還有一種情況是因為頻繁的內存分配和釋放,導致內存區域裏面存在很多碎片,當這些碎片足夠多,new 一個大對象的時候,所有的碎片中沒有一個碎片足夠大以分配給這個對象,但是所有的碎片空間加起來又是足夠的時候,就會出現 OOM,而且這種 OOM 從某種意義上講,是完全能夠避免的。
  由於產生內存碎片的場景很多,從 Memory Monitor 來看,下面場景的內存抖動是很容易產生內存碎片的:

最常見產生內存抖動的例子就是在 ListView 的 getView 方法中未復用 convertView 導致 View 的頻繁創建和釋放,針對這個問題的處理方式那當然就是復用 convertView;或者是 String 拼接創建大量小的對象(比如在一些頻繁調用的地方打字符串拼接的 log 的時候);如果是其他的問題,就需要通過 Memory Monitor 去觀察內存的實時分配釋放情況,找到內存抖動的地方修復它,或者如果當出現下面這種情況下的 OOM 時,也是由於內存碎片導致無法分配內存:

出現上面這種類型的 Crash 時就要去分析應用裏面是不是存在大量分配釋放對象的地方了。

Android 內存優化

  內存優化請看下篇:Android 性能優化之內存泄漏檢測以及內存優化(下)。

引用

http://blog.csdn.net/luoshengyang/article/details/42555483
http://blog.csdn.net/luoshengyang/article/details/41688319
http://blog.csdn.net/luoshengyang/article/details/42492621
http://blog.csdn.net/luoshengyang/article/details/41338251
http://blog.csdn.net/luoshengyang/article/details/41581063
https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
http://geek.csdn.net/news/detail/127226
http://www.jianshu.com/p/216b03c22bb8
https://zhuanlan.zhihu.com/p/25213586
https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
http://www.cnblogs.com/larack/p/6071209.html
https://source.android.com/devices/tech/dalvik/gc-debug.html
http://blog.csdn.net/high2011/article/details/53138202
http://gityuan.com/2015/10/03/Android-GC/
http://www.ayqy.net/blog/android-gc-log%E8%A7%A3%E8%AF%BB/
https://developer.android.com/studio/profile/investigate-ram.html
https://zhuanlan.zhihu.com/p/26043999

Android 性能優化之內存泄漏檢測以及內存優化(中)