專案經驗,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)
前言
記憶體洩露說簡單也簡單,說複雜也複雜。簡單是因為我們有很多工具,比如 Android Studio Profiler、MAT 等,對記憶體洩露進行定位。複雜是因為我們需要了解很多其它知識,比如 Android 虛擬機器(Dalvik 或者 ART)的自動垃圾回收機制、Android 平臺應用記憶體佔用分析、Android Context 原理等,以看懂這些工具提供的資料。否則,無法進行下一步的記憶體洩露分析,或者只知其然,而不知其所以然。
所以,強烈推薦先閱讀筆者的前三篇文章後,再閱讀本文:
如果你已經對上述內容非常熟悉或有所瞭解,也可以直接閱讀本文。
至於 Android Context 原理,筆者會就 Android 9.0(Pie,API28)原始碼進行簡要分析。
如有錯誤,歡迎指正。
記憶體洩露概念
我們知道在 C 語言中,記憶體是需要開發人員自己管理的,使用 malloc() 分配記憶體,free() 釋放記憶體。如果沒有呼叫 free() 進行釋放,將導致記憶體洩露。而 Java 虛擬機器實現了自動記憶體管理機制,將開發人員從手動管理中解放出來。那麼為什麼也會存在“記憶體洩露”呢?其實,這裡的“記憶體洩露”指的是變數的存活時間超過了其本身的生命週期,說的糙一點,就是“該死的時候沒死”。在實體記憶體有限的移動裝置上,每個 Java 程式都有一個記憶體使用閾值,超過這個閾值時,虛擬機器將會丟擲 OutOfMemoryError。而持續的記憶體洩露,很可能會導致 OutOfMemoryError,這也是我們為什麼要修復記憶體洩露的原因。
單個應用堆記憶體上限
檢視 Android 裝置上單個應用的 Java 堆記憶體使用上限:
java 程式碼:
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
Logger.debug("JavaHeap", "dalvik.vm.heapgrowthlimit: %dM", am.getMemoryClass());
Logger.debug("JavaHeap", "dalvik.vm.heapsize: %dM", am.getLargeMemoryClass());
複製程式碼
Logger
是筆者封裝的一個日誌列印工具類:Logger.java
shell 命令:
adb shell getprop dalvik.vm.heapgrowthlimit
adb shell getprop dalvik.vm.heapsize
複製程式碼
分別對應:
- 普通應用 Java 堆使用上限:對應 /system/build.prop 中 "dalvik.vm.heapgrowthlimit"
- 大應用 Java 堆使用上限:需要在 Manifest application 標籤中設定 android:largeHeap="true",對應 /system/build.prop 中 "dalvik.vm.heapsize"
示例(小米6,Android 8.0,MIUI 10 8.12.13):
2018-12-31 17:13:39.335 4142-4142/? D/WanAndroid: JavaHeap: dalvik.vm.heapgrowthlimit: 256M
2018-12-31 17:13:39.335 4142-4142/? D/WanAndroid: JavaHeap: dalvik.vm.heapsize: 512M
複製程式碼
檢視記憶體資訊
上一篇文章說過,我們可以使用:
adb shell dumpsys meminfo [-s|-d|-a] <package_name|pid>
複製程式碼
注:[]
表示命令選項,<>
表示命令引數。
選項 | 作用 |
---|---|
-s | 輸出概要記憶體資訊 |
-d | 輸出詳細記憶體資訊 |
-a | 輸出全部記憶體資訊 |
示例(小米6,Android 8.0,MIUI 10 8.12.13)
D:\Android\projects\wanandroid_java>adb shell dumpsys meminfo -s com.yuloran.wanandroid_java
Applications Memory Usage (in Kilobytes):
Uptime: 563701889 Realtime: 1391868153
** MEMINFO in pid 5897 [com.yuloran.wanandroid_java] **
App Summary
Pss(KB)
------
Java Heap: 9384
Native Heap: 15636
Code: 28680
Stack: 120
Graphics: 5688
Private Other: 8020
System: 3788
TOTAL: 71316 TOTAL SWAP PSS: 44
Objects
Views: 0 ViewRootImpl: 0
AppContexts: 2 Activities: 0
Assets: 18 AssetManagers: 2
Local Binders: 24 Proxy Binders: 21
Parcel memory: 8 Parcel count: 34
Death Recipients: 3 OpenSSL Sockets: 0
WebViews: 0
複製程式碼
以上顯示的是 PSS(不瞭解 PSS的,請閱讀筆者的上篇文章)資料,單位是 KB。應用來自筆者的 JetPack 實踐專案 wanandroid_java。
此處我們主要關注 Activities
,這是定位應用是否存在記憶體洩露的切入點。因為 Activity 作為應用與使用者的互動入口,絕大多數的記憶體洩露,最終都會反應到 Activity 上。正常情況下,開啟一個 Activity,Activities
計數+1,退出一個 Activity,Activities
計數-1,退出應用,Activities
計數=0。 如果不是,說明你的應用存在記憶體洩露。顯然,上圖所示的資料,Activities
為 0,不存在記憶體洩露。
至於 AppContexts
,表示的是程式中仍然存活的 Context 物件,相較於 Activities
重要性不高。我們知道 Application、Activity、Service 都是 Context 物件,所以這些物件的數量,都會納入 AppContexts
計數。而 ContentProvider、BroadcastReceiver 的 Context 是外部傳進去的,所以不會對 AppContexts
的計數結果產生影響。一般情況下,Application 的數量為 1,除非應用開了多個程式(一個 Java 程式對應一個虛擬機器,所以自然也對應一個新 Application),筆者的應用並沒有使用多程式,那麼為什麼 AppContexts
數量為 2 呢?使用 Android Studio Profiler 除錯一下:
Instance View 視窗可以檢視該物件的內部引用,Reference 視窗可以檢視該物件被哪些外部物件引用。我們分別檢視下這兩個 Context 物件正被哪些物件引用:
如上圖所示,該物件被 MyApplication 的成員變數 mBase 引用,沒有問題。
如上圖所示,該物件被 ActivityThread 的成員變數 mSystemContext 引用,也沒有問題。
Context 原始碼簡析
既然上面提到了 Context,此處簡單分析下其子類 Application、Activity、Service 的建立原始碼。由於 Application 一般在建立 Activity 時順帶建立,所以直接從 Activity 的建立原始碼切入,原始碼基於 SDK Android 9.0(Pie,API28)。
Activity 建立原始碼
-> ActivityThread::performLaunchActivity()
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// 建立一個適用於 Activity 的 ContextImpl 物件,此處變數名改為 activityContext 更合適
// ContextImpl extends Context,所以也是 Context 的子類
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
// 反射建立 Activity 物件
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
} catch (Exception e) {
}
try {
// 建立 Application 物件
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
// 將 activity 賦給 ContextImpl::mOuterContext 物件,可以看出 mOuterContext 代表的是實際元件物件,
// 比如具體的 Application、Activity、Service 物件,此處為 Activity 物件
appContext.setOuterContext(activity);
// 將 ContextImpl 型別的 appContext 物件賦給 ContextWrapper::mBase 物件,而大多數時候,實際幹活的
// 也正是這個物件
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
// 設定主題
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}
// 通過 Instrumentation 型別的 mInstrumentation 物件,間接呼叫 onCreate()
// 注意 mInstrumentation 物件,這是代理模式的一個應用,用於託管直接建立系統元件或呼叫
// 系統元件功能,用來監測和協助執行 Android 應用。
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
} catch (SuperNotCalledException e) {
} catch (Exception e) {
}
return activity;
}
複製程式碼
-> LoadedApk::makeApplication()
public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
Application app = null;
try {
java.lang.ClassLoader cl = getClassLoader();
// 建立一個適用於 Application 的 ContextImpl 物件
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
// 建立 Application 物件
app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);
// 將 app 賦給 ContextImpl::mOuterContext 物件,可以看出 mOuterContext 代表的是實際元件物件,
// 比如具體的 Application、Activity、Service 物件,此處為 Application 物件
appContext.setOuterContext(app);
} catch (Exception e) {
}
return app;
}
複製程式碼
-> Instrumentation::newApplication()
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
Application app = getFactory(context.getPackageName()).instantiateApplication(cl, className);
// 將 ContextImpl 型別的 context 物件賦給 ContextWrapper::mBase 物件,而大多數時候,實際幹活的
// 也正是這個物件
app.attach(context);
return app;
}
複製程式碼
Service 建立原始碼
-> ActivityThread::handleCreateService()
private void handleCreateService(CreateServiceData data) {
LoadedApk packageInfo = getPackageInfoNoCheck(
data.info.applicationInfo, data.compatInfo);
Service service = null;
try {
// 建立 Service 物件
java.lang.ClassLoader cl = packageInfo.getClassLoader();
service = packageInfo.getAppFactory().instantiateService(cl, data.info.name, data.intent);
} catch (Exception e) {
}
try {
// 建立一個適用於 Application 的 ContextImpl 物件
ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
// 將 service 賦給 ContextImpl::mOuterContext 物件,可以看出 mOuterContext 代表的是實際元件物件,
// 比如具體的 Application、Activity、Service 物件,此處為 Service 物件
context.setOuterContext(service);
// 檢查建立 Application 物件,只會建立一次
Application app = packageInfo.makeApplication(false, mInstrumentation);
// 將 ContextImpl 型別的 context 物件賦給 ContextWrapper::mBase 物件,而大多數時候,實際幹活的
// 也正是這個物件
service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService());
// 呼叫 onCreate()
service.onCreate();
} catch (Exception e) {
}
}
複製程式碼
以上程式碼已經過筆者精簡,僅保留了與本文主題相關部分。由以上原始碼可知,ContextWrapper::mBase 物件的具體型別為 ContextImpl,實際幹活的也正是它。至於 Context 怎麼理解,讀者可以理解為類似於環境變數或者機器貓的百變口袋,可以用來獲取各種系統資源。Android 中大多數的記憶體洩露也正是 Context 洩露,比如 View 和 Drawable 物件會保持對其源 Activity 的引用,因此保持 View 或 Drawable 物件,就可能導致 Activity 洩露。
記憶體洩露案例
本來想找個大廠 App 測試下有沒有記憶體洩露,因為筆者以前曾經整合過他們的 SDk,那個時候是存在記憶體洩露的。剛才試了一下,額,退出後連程式都沒有了?...不禁想起筆者讀大學時,那個時候 Android 應用上線,有的連混淆都不做,強如 QQ 都被改的面目全非,各種殺馬特皮膚。筆者也反編譯修改過 Miui 的設定中心,剔除了自啟管理和病毒掃描功能。而今天,各種混淆加密加固,再也無法胡作非為了?。而記憶體洩露這種低階問題,大廠的 App 自然也很少再出現了。
扯遠了,沒辦法,手寫一個記憶體洩露吧:
@Override
protected void onStart()
{
super.onStart();
Single.fromCallable(new Callable<SectionResp>()
{
@Override
public SectionResp call() throws Exception
{
Thread.sleep(TimeUnit.SECONDS.toMillis(20));
return new SectionResp();
}
}).subscribeOn(Schedulers.newThread()).subscribe(new Consumer<SectionResp>()
{
@Override
public void accept(SectionResp sectionResp) throws Exception
{
Logger.debug("RxThread", "thread stopped.");
}
});
}
複製程式碼
上述程式碼,使用 RxJava 模擬弱網請求響應過慢的場景,不是很嚴重的記憶體洩露,因為我們一般都會設定請求超時時間,屆時執行緒自會停止,洩露也隨之消失。不過這並不重要,因為無論什麼形式的洩露,定位方式是相同的。
首先,退出應用,然後 dumpsys meminfo,快速檢視是否存在 Activity 洩露:
D:\Android\projects\wanandroid_java>adb shell dumpsys meminfo -s com.yuloran.wanandroid_java
Applications Memory Usage (in Kilobytes):
Uptime: 578084002 Realtime: 1406250267
** MEMINFO in pid 30243 [com.yuloran.wanandroid_java] **
App Summary
Pss(KB)
------
Java Heap: 10692
Native Heap: 27740
Code: 31264
Stack: 120
Graphics: 5816
Private Other: 8792
System: 12896
TOTAL: 97320 TOTAL SWAP PSS: 28
Objects
Views: 227 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 18 AssetManagers: 3
Local Binders: 26 Proxy Binders: 27
Parcel memory: 10 Parcel count: 42
Death Recipients: 3 OpenSSL Sockets: 0
WebViews: 0
複製程式碼
顯然,存在 Activity 洩露,因為 Activity 內通常都會持有其它引用,進而導致大量資源無法及時釋放。
接下來,需要分析具體的記憶體洩露原因,有兩種方法:Android Studio Profiler 和 MAT。
使用 Android Studio Profiler 分析
顯然,onStart() 方法中的匿名內部類 new Callable(){} 導致了 MainActivity 洩露。
使用 MAT 分析
說實話,Android Studio 的效能調優工具發展到現在,從最初的 DeviceMonitor 到現在的 Profiler,確實是越來越好用了,想想也就是幾年的時間。在以前自帶工具不好用的時候,都是先把記憶體 dump 為 xxx.hprof,再轉為 MAT 可讀格式,使用 MAT 進行分析。
下載與安裝
下載 MAT 獨立版,無需安裝,解壓即用。
匯出為 *.hprof
將應用 Java 堆記憶體匯出為 *.hprof(heap profile):
也可以使用 adb shell am dumpheap
命令來匯出:
adb shell am dumpheap com.yuloran.wanandroid_java /data/local/tmp/temp.hprof
adb pull /data/local/tmp/temp.hprof temp.hprof
複製程式碼
格式轉換
使用 hprof-conv.exe
將上面匯出的檔案轉為 MAT 可讀的檔案,字尾名一樣:
hprof-conv.exe .\memory-20181231T225508.hprof .\memory-20181231T225508_mat.hprof
複製程式碼
hprof-conv.exe
位於 platform-tools
目錄下:
為方便使用,筆者將 platform-tools
新增到了系統環境變數中,所以在上面的 Windows Bat 命令中,可以直接使用此程式。
MAT 分析
開啟上面轉換後的檔案 memory-20181231T225508_mat.hprof
:
點選直方圖:
輸入 .*Activity.*
進行過濾:
右擊選擇 with outgoing references
:
右擊選擇“排除所有虛/弱/軟引用”:
檢視 MainActivity 洩露原因:
顯然,onStart() 方法中的匿名內部類 new Callable(){} 導致了 MainActivity 洩露。
結語
在公開平臺寫作與個人私下使用是兩種完全不同的感受,需要更為嚴謹的態度以及多方論證。所以筆者的文章大多來自官方文件或原始碼分析,然而畢竟水平有限,如有錯誤,還望不吝賜教?。
附
Google Android Developers 官網有關記憶體調優的文章:
- Overview of memory management:包括垃圾回收、共享記憶體、記憶體分配與回收、記憶體限制、快速切換機制
- Investigating Your RAM Usage:包括 GC 日誌分析、記憶體分配追蹤、hprof 分析、dumpsys meminfo 資料詳解、如何觸發記憶體洩露
- Manage your app's memory:記憶體優化建議