目錄
前言
Sorry,忘記了我的臉已經過了五四青年節了。。。。。。小小幽默一下,接下來說點正事吧!
本篇是《性能優化》專題的第二篇,上一篇中介紹了Android的啓動優化,有興趣的朋友可以去翻看一下:
今天來說一下Android的內存優化,關於內存優化基本上可以說是面試必問的一個知識點,可見掌握Android的內存優化是何等的重要,接下來我會針對Android內存優化工具、內存管理機制、內存抖動、內存泄露、線上內存監控等幾個方面做全方位的介紹。
一、內存優化介紹
1.1、爲什麼要做內存優化?
內存優化一直是一個很重要但卻缺乏關注的點,我們所寫的每一行代碼其實都涉及到了內存的申請以及回收等過程,對於內存問題它的表現形式相對隱蔽,又由於安卓使用Java語言開發,大家都知道Java的內存回收機制是自動的,所以大多數情況下開發者都是不重視的。於是乎當你在某些平臺上看到應用內存的一些堆棧信息,比如出現了OutOfMemory,在你認真跟蹤下來可能會發現內存出現問題的地方僅僅只是一個表現的地方,並非深層次的原因,因爲內存問題相對比較複雜,它是一個逐漸擠壓的過程,正好在你出現問題的代碼那裏爆了,所以針對應用的內存問題開發者必須多加關注。
1.2、內存問題表現形式
- 內存抖動:鋸齒狀、GC頻繁導致卡頓
- 內存泄露:可用內存逐漸減少、頻繁GC
- 內存溢出:OOM、程序異常
二、內存優化工具
2.1、Memory Profiler
2.1.1、Profiler簡介(Android Studio自帶的工具)
- 實時圖表展示應用內存使用量(非常直觀)
- 識別內存泄露(這裏的識別只是一個簡單判斷)、抖動(這個相對比較簡單、鋸齒狀圖形)等
- 提供捕獲堆轉儲、強制GC以及跟蹤內存分配的能力
2.1.2、Profiler用法介紹
首先找到Profiler面板,如下圖中所示:View——>Tool Windows——>Profiler即可調出該面板。
在Profiler面板的左上角,點擊➕加號,可以選擇想要跟蹤的哪臺設備的哪個進程:
然後在面板的中間大的區域一共分爲了四塊:CPU、MEMORY、NETWORK、ENERGY,這裏我們關注的是MEMORY這個條目,你可以直接點擊MEMORY這一行,然後面板中展示的就是內存的使用情況了:
接着來看一下它的具體使用情況,這裏用一張圖來介紹一下相關的工具按鈕的使用說明:
2.1.3、Dump Java heap
先說明一點,因爲我的電腦屏幕較小,爲了展示整體的效果,圖片裏面有些英文字母出現了省略號,大家根據解釋說明結合自己的工具將對應的單詞對號入座就OK了!
點擊向下的那個箭頭,然後它就會開始Dump內存信息,然後以文件的形式展現出來:
- app heap:這一列展示了內存中所存在的這些類
- Allocations:是分配了多少對象
- Native Size:主要反映Bitmap所使用的像素內存
- Shallow Size:該實例的大小
- Retained Size:該實例所支配的內存大小
然後隨便選取一個條目點擊查看,這裏我選了一個Bitmap的條目,然後右側會展示出這個Bitmap所創建的對象,右鍵可以跟到具體的源碼位置,上方同樣的有Shallow Size等數據的展示,其中Depth的意思是:從任何GC根到所選實例的最短跳數:
2.1.4、Allocation Tracking
對於8.0之前其實是有一個start record和stop record的按鈕用來統計一段時間內的內存分配情況的,8.0之後可以直接到Memory面板區域用鼠標直接拖拽一段距離,然後就可以生成對象內存分配的統計結果了:
左側是剛剛操作app所分配的內存情況,點擊頭部Class Name可以按字母排序,然後隨便點擊一個條目選中了StringBuilder,然後右側Instance View面板中就出現了它的一些實例,點擊某個實例可以看到調用棧信息和調用的過程,下方的Allocation Call Stack中可以看到這個對象是在哪裏創建的,比如這裏的FeedAdapter類中的onBindViewHolder方法的75行,右鍵jump to source就可以跳到對應的源碼位置。
2.1.5、Memory Profiler總結
- 方便直觀:整個內存使用情況可以通過圖表的方式直觀展示出來,同時也可以知道內存的分配情況,並且還可以知道分配某個對象具體的堆棧信息,方便跟蹤
- 線下平時使用:Android Studio自帶的工具,開發過程使用較爲方便
2.2、Memory Analyzer(MAT)
2.2.1、MAT簡介
- 強大的Java Heap分析工具,查找內存泄漏及內存佔用
- 生成整體報告、分析問題等
- 線下深入使用
官網下載地址:http://www.eclipse.org/mat/downloads.php ,這個地址是不是有你熟悉的單詞,嗯,沒錯啦,MAT是Eclipse中的一個插件,因爲現在開發過程中很多人都使用了IDEA或者Android Studio,所以你不想下載Eclipse的話呢,你可以去下載MAT的獨立版,解壓之後裏面有一個MemoryAnalyzer.exe的可執行文件,直接點擊就可以使用了。
福利來啦:MAT版下載地址(無需積分哦):https://download.csdn.net/download/JArchie520/12488709
這個工具很多時候我們需要結合Android Studio的堆轉儲能力配合使用,但是需要注意,AS3.0之後生成的hprof文件不是標準的hprof文件了,需要使用命令轉換一下:hprof-conv 原文件路徑 轉換後文件路徑
2.2.2、MAT用法簡介
①、Overview:概覽信息
Top Consumers
- 通過圖表展示出佔用內存比較多的對象,此欄在做降低內存佔用時比較有幫助
- Biggest Objects:相對詳細的信息
Leak Suspects
- 快速查看內存泄露的可疑點
②、 Histogram:直方圖
- Class Name:具體檢索某一個類
- Objects:某一個具體的Class有多少實例
- Shallow Heap:某單一實例自己佔了多少內存
- Retained Heap:在這個引用鏈之上這些對象總共佔了多少內存
Group by packge:將類對象以包名形式展示
List objects
- with outgoing references:自身引用了哪些類
- with incoming references:自身被哪些類引用
③、dominator_tree
- 每個對象的支配樹
- percentage:佔所有對象的百分比
在條目上右鍵它也有List objects,它和Histogram之間有啥區別呢?主要區別就是下面兩點:
- Histogram:基於類的角度分析
- dominator_tree:基於實例的角度分析
④、OQL:對象查詢語言,類似於從數據庫中檢索內容
⑤、thread_overview:詳細的展示線程信息,可以查看出當前內存中存在多少線程
2.3、LeakCanary
2.3.1、LeakCanary簡介
- 自動內存泄漏檢測
- https://github.com/square/leakcanary
- 線下集成(帶到線上可能會對內存造成更嚴重的影響)
2.3.2、LeakCanary使用
首先在build.gradle文件中添加依賴:
//LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
然後在你自己的Application類的onCreate()方法中初始化:
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
然後當你的App中出現內存泄露時,手機桌面會出現一個Leaks的圖標,點擊進去可以看到產生泄露的記錄列表,點擊列表條目可以展開具體的泄露信息:
2.3.3、LeakCanary原理
- 監控生命週期,onDestroy添加RefWatcher檢測(其實就是設置懷疑點)
- 二次確認斷定發生內存泄露
- 分析泄露,找引用鏈
- 監控組件+分析組件
三、Android內存管理機制
由於Android開發很多還是用的Java語言,所以先來說一下Java的內存管理機制,然後再來說Android的內存管理機制。關於Java的內存管理機制這一部分,我在之前的一篇文章中有圖文的詳細介紹,建議大家去看看,這裏就只簡單的介紹一下了:
文章詳情見《帶你認識JVM》:https://blog.csdn.net/JArchie520/article/details/103734810
3.1、Java內存分配
- 方法區:存儲的是Java的類信息、常量和靜態變量等,這塊區域是所有線程都共享的
- 虛擬機棧:存儲的是局部變量表和操作數棧等
- 本地方法棧:對於它的理解可以結合虛擬機棧進行對比,虛擬機棧是爲Java方法服務的,而本地方法棧是爲Native方法服務的
- 堆:最大的一塊區域,是所有線程共享的,每個對象的實際內存分配都是在堆中進行的,虛擬機棧中分配的只是引用,這些引用會指向堆中真正創建的對象,同時它是GC主要作用的一塊區域,我們平時說的內存泄漏也是發生在這塊區域的
- 程序計數器:知道有這個概念就OK,用來存儲當前線程執行的方法執行到的具體位置
3.2、Java內存回收算法
(一)、標記-清除算法
- 標記出所有需要回收的對象
- 統一回收所有被標記的對象
總結:
- 標記和清除效率不高
- 產生大量不連續的內存碎片
(二)、複製算法
- 將內存劃分爲大小相等的兩塊
- 一塊內存用完之後複製存活對象到另一塊
- 清理另一塊內存
總結:
- 實現簡單,運行高效(相較於第一種來說)
- 浪費一半空間,代價大
(三)、標記-整理算法
- 標記過程與“標記-清除”算法一樣
- 存活對象往一端進行移動
- 清理其餘內存
總結:
- 避免標記-清除導致的內存碎片
- 避免複製算法的空間浪費
(四)、分代收集算法
- 結合多種收集算法優勢將它們應用於不同的生命週期
- 新生代對象存活率低,複製(複製比例可以調整)
- 老年代對象存活率高,標記-整理
3.3、Android內存管理機制
(一)、Android內存管理機制特點
- 內存彈性分配,分配值與最大值受具體設備影響(高端機和低端機單個app可以使用的內存是不同的)
- OOM場景:內存真正不足、可用內存不足
(二)、Dalvik與Art區別
- Dalvik僅固定一種回收算法(手機出廠之前燒ROM的時候就已經確定好了,運行期間無法改變)
- Art回收算法可運行期間選擇(不同的情況下選擇合適的回收算法)
- Art具備內存整理能力,減少內存空洞
(三)、Low Memory Killer
這套機制是針對所有進程來說的,如果手機內存不足的情況下,這套機制會針對所有進程進行回收。
- 進程分類(前臺、可見、服務、後臺、空進程五大類,優先級由高到低排列,優先回收低優先級的)
- 回收收益(具體回收的大小)
四、實戰內存抖動解決
4.1、內存抖動簡介
- 定義:內存頻繁分配和回收導致內存不穩定
- 表現:頻繁GC、內存曲線呈鋸齒狀
- 危害:導致卡頓、嚴重時會導致OOM
4.2、內存抖動導致OOM
- 頻繁創建對象,導致內存不足及碎片(不連續)
- 不連續的內存片無法被分配,導致OOM
4.3、實戰分析
這一部分我會模擬一次內存抖動,並通過Profiler分析內存情況,定位到具體內存抖動的代碼。
首先先來創建一個佈局文件activity_memory.xml,裏面就一個按鈕,用來觸發模擬內存抖動的那部分代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_memory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="模擬內存抖動"/>
</LinearLayout>
然後定義一個MemoryShakeActivity頁面,加載剛纔的佈局,並且在頁面中定義一個Handler,當點擊模擬內存抖動的按鈕時,我們定時執行handleMessage中的模擬抖動的代碼,整個代碼都是很容易能看懂的那種:
/**
* 作者:created by Jarchie
* 時間:2020/05/31 09:22:17
* 郵箱:[email protected]
* 說明:模擬內存抖動頁面
*/
public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {
@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//模擬內存抖動的場景,每隔10毫秒執行一次,循環執行100次,每次通過new分配大內存
for (int i=0;i<100;i++){
String[] obj = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0,10);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory);
findViewById(R.id.btn_memory).setOnClickListener(this);
}
@Override
public void onClick(View view) {
if (view.getId() == R.id.btn_memory){
mHandler.sendEmptyMessage(0);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
然後將我們的“愛僕”跑起來,我截了兩張圖給大家看一下,第一張是沒有執行模擬抖動的代碼之前的,第二張是執行之後的:
從上面兩張圖中可以清晰的看到第一張內存比較平穩,第二張內存圖有鋸齒狀出現,突然出現了頻繁的GC,看到下面好多小垃圾桶了沒,這個時候可以初步判定應該是出現了內存抖動現象,因爲比較符合它的特徵,然後在面板上拖動一段距離它就會將這段時間內的內存分配情況給我們展示出來:
首先雙擊Allocations,然後將這一列按照從大到小的順序排列好,然後你會發現String數組居然有這麼多,它佔用的內存大小也是最高的(值得關注的點我都用矩形標出了),此時我們就應該鎖定這個目標,爲什麼String類型的數組會有這麼多,這裏很有可能是有問題的。然後排查究竟是哪裏導致的這個問題,很簡單點擊String[]這一行,在右側Instance View面板中隨便點擊一行,下方Allocation Call Stack面板中就出現了對應的堆棧信息,上面也列出了具體哪個類的哪一行,右鍵jupm to source就可以跳轉到指定的源碼位置,這樣就找到了內存抖動出現的位置,然後我們分析代碼作相應的修改即可。
流程總結:①、使用Memory Profiler初步排查;②、使用Memory Profiler或CPU Profiler結合代碼排查
內存抖動解決技巧:找循環或者頻繁調用的地方
五、實戰內存泄露解決
5.1、內存泄露簡介
定義:內存中存在已經沒有用的對象
表現:內存抖動、可用內存逐漸變少
危害:內存不足、GC頻繁、OOM
5.2、實戰分析
這裏還是通過代碼來真實的模擬一次內存泄露的場景,對於一般的APP程序來說,最大的問題往往都是在Bitmap上,因爲它消耗的內存比較多,拿它來模擬會看的比較明顯。好首先來看佈局文件activity_memoryleak.xml,裏面就一個ImageView控件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_memoryleak"
android:layout_width="50dp"
android:layout_height="50dp" />
</LinearLayout>
然後定義了一個模擬處理某些業務的Callback回調接口,和一個統一管理這些回調接口的Manager類:
//模擬回調處理某些業務場景
public interface CallBack {
void dpOperate();
}
//統一管理Callback
public class CallBackManager {
public static ArrayList<CallBack> sCallBacks = new ArrayList<>();
public static void addCallBack(CallBack callBack) {
sCallBacks.add(callBack);
}
public static void removeCallBack(CallBack callBack) {
sCallBacks.remove(callBack);
}
}
然後在我們的模擬內存泄露的頁面上設置Bitmap,並設置回調監聽:
/**
* 作者:created by Jarchie
* 時間:2020/6/2 10:48:19
* 郵箱:[email protected]
* 說明:模擬內存泄露頁面
*/
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.big_bg);
imageView.setImageBitmap(bitmap);
CallBackManager.addCallBack(this);
}
@Override
public void dpOperate() {
}
}
OK,我們的代碼就寫完了,現在來實際運行一下,然後將這個頁面連續打開關閉多次,看看這段代碼會不會造成內存泄露呢?
這是我用Profiler截取的內存圖片,可以看到整個內存在經過了我的反覆開關頁面之後雖然有的地方出現了一個小抖動,但是整體是呈階梯狀上升的,可用內存在逐漸減少,此時基本上可以斷定這個界面出現了內存泄露。Profiler工具雖然可以初步幫我們斷定出現了內存泄露,但是它卻無法斷定具體是哪裏出現了內存泄露,意思就是我們還是不知道該修改哪裏的代碼,所以此時需要用到強大的Java Heap工具了,來有請MAT出場。
首先需要在Profiler中點擊Dump Java Heap按鈕,使用堆轉儲功能轉換成一個文件,然後點擊保存按鈕將文件保存到本地目錄下,比如我這裏保存爲H盤中的memoryleak.hprof文件,然後使用hprof-conv命令將其轉換爲標準的hprof文件,我這裏是轉換後的名稱是:memoryleak_transed.hprof,如下所示:
然後打開MAT工具,導入剛剛生成的轉換後的文件:
點擊Histogram查看內存中所有存活的對象,然後我們在Class Name中可以輸入內容搜索想要查找的對象:
然後可以看到該對象的具體信息,以及數量和所佔用的內存大小,我這裏發現內存中居然存在6個MemoryLeakActivity對象:
然後右鍵List objects---->with incoming references找到所有引向它的強引用:
然後右鍵Path To GC Roots----->with all references,將所有引用都計算在內然後算出來這個對象和GCRoot之間的路徑:
來看結果,最後是到了sCallBacks這裏,而且它左下角有個小圓圈,這就是我們真正要找的位置,也就是說MemoryLeakActivity是被CallBackManager這個類的sCallBacks這個對象引用了:
根據上面找的結果到代碼中去找CallBackManager的sCallBacks看看這裏究竟是做了什麼引發的?
public static ArrayList<CallBack> sCallBacks = new ArrayList<>();
MemoryLeakActivity是被sCallBacks這個靜態變量引用着,由於被static關鍵字修飾的變量的生命週期是和App的整個生命週期一樣長的,所以當MemoryLeakActivity這個頁面關閉時,我們應該將變量的引用關係給釋放掉,否則就出現了上面的內存泄露的問題。所以解決這個問題也很簡單了,添加如下幾行代碼:
@Override
protected void onDestroy() {
super.onDestroy();
CallBackManager.removeCallBack(this);
}
流程總結:①、使用Memory Profiler初步觀察(可用內存逐漸減少);②、通過Memory Analyzer結合代碼確認
六、線上內存監控方案
線上內存問題最大的就是內存泄露,對於內存抖動和內存溢出它們一般都和內存泄露導致的內存無法釋放相關,如果能夠解決內存泄露,則線上內存問題就會減少很多。線上內存監控其實還是比較困難的,因爲我們無法使用線下的這些工具來直觀的發現分析問題。
6.1、常規方案
①、設定場景線上Dump
比如你的App已經佔用到單個App最大可用內存的較高百分比,比如80%,通過:Debug.dumpHprofData();這行代碼可以實現將當前內存信息轉化爲本地文件。
整個流程如下:超過內存80%——>內存Dump——>回傳文件(注意文件可能很大,保持在wifi狀態回傳)——>MAT手動分析
總結:
- Dump文件太大,和對象數正相關,可裁剪
- 上傳失敗率高、分析困難
②、LeakCanary線上使用
- LeakCanary帶到線上
- 預設泄露懷疑點
- 發現泄露回傳
總結:
- 不適合所有情況,必須預設懷疑點,限制了全面性
- 分析比較耗時,也容易OOM(實踐發現LeakCanary分析過程較慢,很有可能自己在分析的過程中自身發生OOM)
6.2、LeakCanary定製
- 預設懷疑點——》自動找懷疑點(誰的內存佔用大就懷疑誰,大內存對象出現問題的概率更大)
- 分析泄露鏈路慢(分析預設對象的每一個對象)——》分析Retain Size大的對象(減少它的分析工作量,提高分析速度)
- 分析OOM(將內存堆棧生成的所有文件全部映射到內存中,比較佔用內存)——》對象裁剪,不全部加載到內存
6.3、線上監控完整方案
- 待機內存、重點模塊內存、OOM率
- 整體及重點模塊GC次數、GC時間
- 增強的LeakCanary自動化內存泄露分析
七、內存優化技巧
優化大方向:
- 內存泄露
- 內存抖動
- Bitmap
優化細節:
- LargeHeap屬性(雖然有點耍流氓,但還是應該向系統申請)
- onTrimMemory、onLowMemory(系統給的低內存的回調,可以根據不同的回調等級去處理一些邏輯)
- 使用優化過的集合:SparseArray
- 謹慎使用SharedPreference(一次性load到內存中)
- 謹慎使用外部庫(儘量選擇經過大規模驗證的外部庫)
- 業務架構設計合理(加載的數據是你能用到的,不浪費內存加載無用數據)
今天的內容又有點多了,先寫到這裏吧,留着慢慢消化啦,各位,下期再會!