本文較長,閱讀大約5分鐘
App進行到最終的測試的時候,往往會出現一些性能上,以及內存上的問題,需要優化,這也是一個Android高級工程師所需要了解並且掌握的知識點,內存這個小妮子比較調皮,每個月總有那麼幾次洩漏或者溢出(OOM),這篇文章所講的是內存溢出,這裡要注意,內存溢出和內存洩漏是兩個概念,這點大家要清楚,當然,內存洩漏過多會導致內存洩漏,至於什麼是內存洩漏呢,大家都知道我們的內存回收機制是GC,所以用一句話來概括:GC回收機制所無法回收的垃圾對象。
如果把垃圾回收機制比喻你在用餐,而服務員會來收盤的話,那麼理想中的狀態便是你吃完飯一走,服務員就把盤子收走了,即對象用完GC自動回收,但是這裡卻只是理想中的樣子,實際上,對於Android的內存管理機制和回收機制,Android系統的一個內存管理機制,被稱為Low Memorry Killer的一種管理機制,其實就是根據優先級去kill掉一些優先級較低的程序,而回收機制就比較佛系了,叫做GC,採用的也是懶人機制,你不需要用的變量,對象等,你放那裡就好,系統會在Heap剩餘空間不夠的時候去回收,並且有一個隱患,即GC觸發後,所有的線程都會被暫停。
內存
要了解內存洩漏,我們首先了解內存,我們都知道Android系統的底層是Linux,並且他運行是一個沙箱機制,即每個App對應獨立運行在一個虛擬機中,並且有一個進程,這也延伸出了多任務機制,並且每個App都是獨立的,即使你崩潰了也不會對系統造成影響,如果想看進程,可以使用ps命令:
並且每個進程都有一個pid,按照順序分配的,可以發現,init進程就是第一個,這個我們不做深究,你知道有這麼一回事兒就行了
我們可以再次輸入一個命令:dumpsys meminfo packagename
這裡我們就可以看到更多的內存信息了,統計了一些物理內存,虛擬內存使用情況以及統計,裡面有三個參數,Heap Size , Heap Alloc ,Heap Free ,指分配了多少內存,使用了多少內存,剩餘多少內存,一般 Heap Size = Heap Alloc + Heap Free (1985 = 1374 + 611),這裡單位是K。
講完系統,我們再化大為小,說一下App,其實App在內存中安裝後,系統會預分配一個最大內存,這跟沙箱機制有一定關係,每家的系統都是不一樣的,我們可以通過代碼去讀取出來:
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
//最大內存
int mc = am.getMemoryClass();
//Large最大內存
int lm = am.getLargeMemoryClass();
這裡有一個Large,實際上我們之前就用過,也就是開啟硬體加速後的最大內存,如何開啟硬體加速,則需要在清單文件的Application根節點添加android:largeHeap="true"
不過大部分廠商會禁止此功能
我們把應用跑在模擬器上可以得到如下的數據:mc = 96 lm = 256
也得到了一個結論,即我這套模擬器給我的這個Demo App分配的最大內存為96,開啟硬體加速後為256,這裡的單位是M,但是大部分真機,這兩個數值都是一樣的,這裡不曾考究,自行探索下。
其實這裡要提一下,現在大部分的App其實所考慮的什麼所謂的內存優化,都是因為圖片過多,所以我們真正考慮的,還是如何有效率的優化圖片給App帶來的負荷,圖片吃內存比較大。
我們繼續來說一下Low Memorry Killer內存管理機制,這裡涉及要當你的App切入後臺後的管理方式,實際上可以用四個字概括:先進先出,當你的應用進入後臺並且開始啟動kill機制或者內存不夠的時候,會優先清理任務棧最底層的應用,也就是最先開啟的應用,而近期應用則相當於保護起來。這種機制叫做:LRU Cache (緩存淘汰)算法。
並且當系統內存存在變化的時候,可以通過Application的onTrimMemory方法監聽
@Override
public void onTrimMemory(int level) {
// level 等級
super.onTrimMemory(level);
}
這裡的內存等級是這樣劃分的:
int TRIM_MEMORY_BACKGROUND = 40;
int TRIM_MEMORY_COMPLETE = 80;
int TRIM_MEMORY_MODERATE = 60;
int TRIM_MEMORY_RUNNING_CRITICAL = 15;
int TRIM_MEMORY_RUNNING_LOW = 10;
int TRIM_MEMORY_RUNNING_MODERATE = 5;
int TRIM_MEMORY_UI_HIDDEN = 20;
我們有好幾種方式可以監聽到內存的使用情況和波動,這裡我一一道來,首先,我們知道,當我們打開手機的設置 - 應用 - 對應的某一個App的時候就可以看到這個App的使用情況,實際上我們可以通過代碼的方式來獲取:
float totalMemory = Runtime.getRuntime().totalMemory() * 1.0f / (1024 * 1024);
float freeMemory = Runtime.getRuntime().freeMemory() * 1.0f / (1024 * 1024);
float maxMemory = Runtime.getRuntime().maxMemory() * 1.0f / (1024 * 1024);
這樣獲取到運行時的內存情況了,我們可以看下數據:
當然,你也可以通過Android Profile 查看
也可以通過Android Monitor查看
其實在面試中也經常有人會被問到內存優化的方法,只能說內存控制方面有很多的小技巧,但是歸根結底還是要你自己有一個良好的代碼習慣,當然,如果真發生了錯誤,比如內存洩漏或者溢出,那麼你也應該知道如何去解決這些問題。
內存洩漏
如果想解決內存洩露,那麼我們應該如何找到問題的根源尼?如果你只是一味的看內存增長是找不到問題所在的,應該內存洩漏如果不嚴重是察覺不到的,這裡我們可以來寫一段這樣的代碼:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
這段代碼我只需要啟動後點擊退出按鈕,再啟動,再點擊,那麼Activity就會每次都finish掉,但是子線程卻一在運行,Runnable是持有Activity對象的,這樣我們就可以看到如下的Memory走勢圖
我反反覆覆的啟動後finish,最終的結果將原本15.7MB的內存變成25.2MB,並且還會無限增加,最終導致內存溢出
那好,如果項目龐大的話,光這樣看是定位不到的,我們可以這樣來:先點擊Profile app
然後在下面的Profiler中點擊Record,然後開始使用App,當看到波形變動的時候再點擊Stop
這樣就會出現如下的文件列表,這裡可以選擇按照包名分類
可以看到,這裡的MainActivity出現了3個實例,這肯定是有問題的,也就定位到了發生溢出的界面為MainActivity。但是到這裡還只是定位到了Activity,我們還可以更加精確一點,我們點擊Record按鈕旁邊的下載按鈕,然後點擊保存hprof文件
有了這個文件之後我們就可以進一步使用MAT工具來分析了
MAT
mat工具是Eclipse的,沒有的話可以到這裡去下載:
MAT工具下載:http://www.eclipse.org/mat/downloads.php
下載後如圖:
打開之後就是我i們久違的Eclipse風格了
但是這裡還不能直接導入,因為Android Studio導出的hprof文件並不是MAT標準的文件,所以我們需要用到SDK目錄下的platform-tools下hprof-conv.exe工具,在此目錄下進入cmd,通過命令:hprof-conv old.hprof new.hprof 來轉換文件:
現在我們可以回到MAT點擊菜單欄的File - Open Heap Dump 導入new.hpfof
只需要點擊Create a historam from an arbitray set of objects 也就是這個小圖標,即可生成分析表
然後我們在這裡輸入過濾:
到這裡就很明朗了,我們繼續縮小範圍
右鍵選擇 List object - with outgoing references ,這個的意思是查看外部所引用的對象
然後繼續過濾一下,並且右鍵 選擇 Merge Shortest Paths to GC Roots - exclude all phantom/weak/soft etc .references 這個的意思是排查所有的弱引用,虛引用
到這裡你是否有一種恍然大悟的感覺,我們過濾之後只剩下一條Thread的錯誤,而所指向的對象為this$0,也就是他本身,意思是 子線程中所持有本類對象,那聯想到內存溢出是我們退出後所引起的,所以最終得到的結論:Activity已經退出,但是子線程仍然持有本類對象所導致內存洩漏。
LeakCanary
當然,上述的方法我更多的傾向於你所了解這個一個追述的過程,畢竟有些繁瑣,所以這裡再教大家使用一款工具 —— LeakCanary
Github:https://github.com/square/leakcanary
我們在app/build.gradle下配置:
implementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
最新的v2.0-alpha-1貌似有些問題,所以我還是使用穩定版本,在Application中增加
public class BaseApp extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
}
}
這樣我們就可以正常的運行了,當發生內存洩漏的時候就會通知欄提示:
到這裡,基本上本章內容也講完了,當然,這也只是一些皮毛而已,當你的項目足夠大的時候,做這項優化工作還是比較繁瑣的,所以最好還是儘量保持良好的編碼習慣才是最重要的。
最新的精品文章我都會第一時間發表在我的星球中,文章內容比較紮實,歡迎加入!
我會繼續保持文章的更新,也會繼續給大家帶來高質量的精品,謝謝大家!