核桃干貨 | Android常見內(nèi)存泄漏與優(yōu)化

Android虛擬機(jī):Dalvik和ART
?
1.1 JVM與Dalvik區(qū)別

Java字節(jié)碼以單字節(jié)(1 byte)為單元,JVM使用的指令只占1個(gè)單元;Dalvik字節(jié)碼以雙字節(jié)(2 byte)為單元,Dalvik虛擬機(jī)使用的指令占1個(gè)單元或2個(gè)單元。因此,在上面的代碼中JVM字節(jié)碼占11個(gè)單元=11字節(jié),Dalvik字節(jié)碼占6個(gè)單元=12字節(jié)(其中,mul-int/lit8指令占2單元)。
(2) 執(zhí)行的字節(jié)碼文件不同
?
JVM運(yùn)行的.class文件,Dalvik運(yùn)行的是.dex(即Dalvik Executable)文件。在Java程序中,Java類會(huì)編譯成一個(gè)或多個(gè).class文件,然后打包到.jar文件中,.jar文件中的每個(gè).class文件里面包含了該類的常量池、類信息、屬性等。
當(dāng)JVM加載該.jar文件時(shí),會(huì)加載里面的所有的.class文件,JVM的這種加載方式很慢,對(duì)于內(nèi)存有限的移動(dòng)設(shè)備并不合適;.dex文件是在.class文件的基礎(chǔ)上,經(jīng)過DEX工具壓縮和優(yōu)化后形成的,通常每一個(gè).apk文件中只包含了一個(gè).dex,這個(gè).dex文件將所有的.class里面所包含的信息全部整合在一起了,這樣做的好處就是減少了整體的文件尺寸(去除了.class文件中相同的冗余信息),同時(shí)減少了I/O操作,加快了類的查找速度。下圖展示了.jar和.dex的對(duì)比差異:

(3) 在內(nèi)存中的表現(xiàn)形式差異
Dalvik經(jīng)過優(yōu)化,允許在有限的內(nèi)存中同時(shí)運(yùn)行多個(gè)進(jìn)程,或說同時(shí)運(yùn)行多個(gè)Dalvik虛擬機(jī)的實(shí)例。
在Android中每一個(gè)應(yīng)用都運(yùn)行在一個(gè)Dalvik虛擬機(jī)實(shí)例中,每一個(gè)Dalvik虛擬機(jī)實(shí)例都運(yùn)行在一個(gè)獨(dú)立的進(jìn)程空間中,因此都對(duì)應(yīng)著一個(gè)獨(dú)立的進(jìn)程,獨(dú)立的進(jìn)程可以防止在虛擬機(jī)崩潰時(shí)所有程序都被關(guān)閉。而對(duì)于JVM來說,在其宿主OS的內(nèi)存中只運(yùn)行著一個(gè)JVM的實(shí)例,這個(gè)JVM實(shí)例中可以運(yùn)行多個(gè)Java應(yīng)用程序(進(jìn)程),但是一旦JVM異常崩潰,就會(huì)導(dǎo)致運(yùn)行在其中的所有程序被關(guān)閉。
(4) Dalvik擁有Zygote進(jìn)程與共享機(jī)制
?
在Android系統(tǒng)中有個(gè)一特殊的虛擬機(jī)進(jìn)程--Zygote,它是虛擬機(jī)實(shí)例的孵化器。它在Android系統(tǒng)啟動(dòng)的時(shí)候就會(huì)產(chǎn)生,完成虛擬機(jī)的初始化、庫的加載、預(yù)制類庫和初始化操作。
如果系統(tǒng)需要一個(gè)新的虛擬機(jī)實(shí)例,他會(huì)迅速復(fù)制自身,以最快的速度提供給系統(tǒng)。對(duì)于一些只讀的系統(tǒng)庫,所有的虛擬機(jī)實(shí)例都和Zygote共享一塊區(qū)域。Dalvik虛擬機(jī)擁有預(yù)加載-共享的機(jī)制,使得不同的應(yīng)用之間在運(yùn)行時(shí)可以共享相同的類,因此擁有更高的效率。而JVM則不存在這個(gè)共享機(jī)制,不同的程序被打包后都是彼此獨(dú)立的,即便它們?cè)诎锸褂昧讼嗤念?,運(yùn)行時(shí)的都是單獨(dú)加載和運(yùn)行,無法進(jìn)行共享。

1.2 Dalvik與ART區(qū)別
ART虛擬機(jī)被引入于Android 4.4,用來替換Dalvik虛擬機(jī),以緩解Dalvik虛擬機(jī)的運(yùn)行機(jī)制導(dǎo)致Android應(yīng)用運(yùn)行變慢的問題。在Android 4.4中,可以選擇使用Dalvik還是ART,而從Android 5.0開始,Dalvik被完全刪除,Android系統(tǒng)默認(rèn)采用ART。Dalvik與ART的主要區(qū)別如下:
(1) ART運(yùn)行機(jī)制優(yōu)于Dalvik
?
對(duì)于運(yùn)行在Dalvik虛擬機(jī)實(shí)例中的應(yīng)用程序而言,在每一次重新運(yùn)行的時(shí)候,都需要將字節(jié)碼通過JIT(Just-In-Time)編譯器編譯成機(jī)器碼,這會(huì)使用應(yīng)用程序的運(yùn)行效率降低,雖然Dalvik虛擬機(jī)已經(jīng)被做過很多優(yōu)化(.dex文件->.odex文件),但由于這種先翻譯再執(zhí)行的機(jī)制仍然無法有效解決Dalvik拖慢Android應(yīng)用運(yùn)行的事實(shí)。
而在ART中,系統(tǒng)在安裝應(yīng)用程序時(shí)會(huì)進(jìn)行一次AOT(Ahead Of Time compilication,預(yù)編譯),即將字節(jié)碼預(yù)先編譯成機(jī)器碼并存儲(chǔ)在本地,這樣應(yīng)用程序每次運(yùn)行時(shí)就不需要執(zhí)行編譯了,運(yùn)行效率會(huì)大大提高。

(2) 支持的CPU架構(gòu)不同
?
Dalvik是為32位CPU設(shè)計(jì)的,而ART支持64位并兼容32位的CPU。
(3) 運(yùn)行時(shí)堆劃分不同
?
Dalvik虛擬機(jī)的運(yùn)行時(shí)堆使用標(biāo)記--清除(Mark--Sweep)算法進(jìn)行GC,它由兩個(gè)Space以及多個(gè)輔助數(shù)據(jù)結(jié)構(gòu)組成,兩個(gè)Space分別是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。
Zygote Space用來管理Zygote進(jìn)程在啟動(dòng)過程中預(yù)加載和創(chuàng)建的各種對(duì)象,Zygote Space中不會(huì)觸發(fā)GC,應(yīng)用進(jìn)程和Zygote進(jìn)程之間會(huì)共享Zygote Space。
Zygote進(jìn)程在fork第一個(gè)子進(jìn)程之前,會(huì)把Zygote Space分為兩個(gè)部分,原來被Zygote進(jìn)程使用的部分仍然叫Zygote Space,而剩余未被使用的部分被稱為Allocation Space,以后fork的子進(jìn)程相關(guān)的所有的對(duì)象都會(huì)在Allocation Space上進(jìn)行分配和釋放。
需要注意的是,Allocation Space不是進(jìn)程共享的,在每個(gè)進(jìn)程中都獨(dú)立擁有一份。下圖展示了Dalvik虛擬機(jī)的運(yùn)行時(shí)堆結(jié)構(gòu):

安裝時(shí)間變長。應(yīng)用在安裝的時(shí)候需要預(yù)編譯,從而增大了安裝時(shí)間。 存儲(chǔ)空間變大。ART引入AOT技術(shù)后,需要更多的空間存儲(chǔ)預(yù)編譯后的機(jī)器碼。
1.3 Dalvik/ART的啟動(dòng)流程
//app_main.cpp$main函數(shù)int main(int argc, char* const argv[]){...// (1) 創(chuàng)建AppRuntime對(duì)象AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));...// (2) 解析執(zhí)行init.rc的啟動(dòng)服務(wù)的命令傳入的參數(shù)// 解析后:zygote = true// startSystemServer = true// niceName = zygote (當(dāng)前進(jìn)程名稱)bool zygote = false;bool startSystemServer = false;bool application = false;String8 niceName;String8 className;while (i < argc) {const char* arg = argv[i++];if (strcmp(arg, "--zygote") == 0) {zygote = true;niceName = ZYGOTE_NICE_NAME;} else if (strcmp(arg, "--start-system-server") == 0) {startSystemServer = true;} else if (strcmp(arg, "--application") == 0) {application = true;} else if (strncmp(arg, "--nice-name=", 12) == 0) {niceName.setTo(arg + 12);} else if (strncmp(arg, "--", 2) != 0) {className.setTo(arg);break;} else {--i;break;}}...// (3) 設(shè)置進(jìn)程名為Zygote,執(zhí)行ZygoteInit類// Zygote = trueif (!niceName.isEmpty()) {runtime.setArgv0(niceName.string());set_process_name(niceName.string());}if (zygote) {runtime.start("com.android.internal.os.ZygoteInit", args, zygote);} else if (className) {runtime.start("com.android.internal.os.RuntimeInit", args, zygote);} else {fprintf(stderr, "Error: no class name or --zygote supplied.\n");app_usage();LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");return 10;}}
創(chuàng)建AppRuntime實(shí)例。AppRuntime是在app_process.cpp中定義的類,繼承于系統(tǒng)的AndroidRuntime,主要用于創(chuàng)建和初始化虛擬機(jī)。AppRuntime類繼承關(guān)系如下:
class AppRuntime : public AndroidRuntime{};
解析執(zhí)行init.rc的啟動(dòng)服務(wù)的命令傳入的參數(shù)。/init.zygote64_32.rc文件中啟動(dòng)Zygote的內(nèi)容如下,在<Android源代碼目錄>/system/core/rootdir/ 目錄下可以看到init.zygote32.rc、init.zygote32_64.rc、init.zygote64.rc、init.zygote64_32.rc等文件,這是因?yàn)锳ndroid5.0開始支持64位的編譯,所以Zygote進(jìn)程本身也有32位和64位版本。啟動(dòng)Zygote進(jìn)程命令如下: /tasks service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygoteclass mainpriority -20socket zygote stream 660 root systemonrestart write /sys/android_power/request_state wakeonrestart write /sys/power/state ononrestart restart audioserveronrestart restart cameraserveronrestart restart mediaonrestart restart netdwritepid /dev/cpuset/foreground/tasks /dev/stune/foreground
執(zhí)行ZygoteInit類。由前面 解析命令傳入的參數(shù)可知,zygote=true說明當(dāng)前程序運(yùn)行的進(jìn)程是Zygote進(jìn)程,將調(diào)用AppRuntime的start函數(shù)執(zhí)行ZygoteInit類,從類名可以看出執(zhí)行該類將進(jìn)入Zygote的初始化流程。
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
該函數(shù)主要完成三個(gè)方面的工作:(a) 初始化JNI環(huán)境,啟動(dòng)虛擬機(jī);(b) 為虛擬機(jī)注冊(cè)JNI方法;(c)從傳入的com.android.internal.os.ZygoteInit 類中找到main函數(shù),即調(diào)用ZygoteInit.java類中的main方法。AndroidRuntime$start源碼如下:
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote){...// (1) 初始化JNI環(huán)境、啟動(dòng)虛擬機(jī)JniInvocation jni_invocation;jni_invocation.Init(NULL);JNIEnv* env;if (startVm(&mJavaVM, &env, zygote) != 0) {return;}onVmCreated(env);// (2) 為虛擬機(jī)注冊(cè)JNI方法if (startReg(env) < 0) {ALOGE("Unable to register all android natives\n");return;}...// (3) 從傳入的com.android.internal.os.ZygoteInit 類中找到main函數(shù),即調(diào)用// ZygoteInit.java類中的main方法。AndroidRuntime及之前的方法都是native的方法,而此刻// 調(diào)用的ZygoteInit.main方法是java的方法,到這里我們就進(jìn)入了java的世界char* slashClassName = toSlashClassName(className);jclass startClass = env->FindClass(slashClassName);if (startClass == NULL) {ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);/* keep going */} else {jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");if (startMeth == NULL) {ALOGE("JavaVM unable to find main() in '%s'\n", className);/* keep going */} else {env->CallStaticVoidMethod(startClass, startMeth, strArray);if (env->ExceptionCheck())threadExitUncaughtException(env);}}...}
# __ANDROID__##// JniInvocation::Initbool JniInvocation::Init(const char* library) {// Android平臺(tái)標(biāo)志char buffer[PROP_VALUE_MAX];char* buffer = NULL;// 獲取“l(fā)ibart.so”或“l(fā)ibdvm.so”library = GetLibrary(library, buffer);const int kDlopenFlags = RTLD_NOW | RTLD_NODELETE;// 加載“l(fā)ibart.so”或“l(fā)ibdvm.so”handle_ = dlopen(library, kDlopenFlags);if (handle_ == NULL) {if (strcmp(library, kLibraryFallback) == 0) {return false;}library = kLibraryFallback;handle_ = dlopen(library, kDlopenFlags);if (handle_ == NULL) {ALOGE("Failed to dlopen %s: %s", library, dlerror());return false;}}...return true;}
從JniInvocation::Init函數(shù)源碼可知,它首先會(huì)調(diào)用JniInvocation::GetLibrary函數(shù)來獲取要指定的虛擬機(jī)庫名稱–“l(fā)ibart.so”或“l(fā)ibdvm.so”,然后調(diào)用JniInvocation::dlopen函數(shù)加載這個(gè)虛擬機(jī)庫。
通過查閱JniInvocation::GetLibrary函數(shù)源碼可知,如果當(dāng)前不是Debug模式構(gòu)建的,是不允許動(dòng)態(tài)更改虛擬機(jī)動(dòng)態(tài)庫,即默認(rèn)為"libart.so";如果當(dāng)前是Debug模式構(gòu)建且傳入的buffer不為NULL時(shí),就需要通過讀取"persist.sys.dalvik.vm.lib.2"這個(gè)系統(tǒng)屬性來設(shè)置返回的library。JniInvocation::GetLibrary函數(shù)源碼如下:
static const char* kLibraryFallback = "libart.so";const char* JniInvocation::GetLibrary(const char* library, char* buffer) {return GetLibrary(library, buffer, &IsDebuggable, &GetLibrarySystemProperty);}const char* JniInvocation::GetLibrary(const char* library,char* buffer,bool (*is_debuggable)(),int (*get_library_system_property)(char* buffer)) {# __ANDROID__const char* default_library;// 如果不是debug構(gòu)建,不允許更改虛擬機(jī)動(dòng)態(tài)庫// library = default_library = kLibraryFallback = "libart.so"if (!is_debuggable()) {library = kLibraryFallback;default_library = kLibraryFallback;} else {// 如果是debug構(gòu)建,需要判斷傳入的buffer參數(shù)是否為空// 如果不為空,default_library賦值為bufferif (buffer != NULL) {if (get_library_system_property(buffer) > 0) {default_library = buffer;} else {default_library = kLibraryFallback;}} else {default_library = kLibraryFallback;}}#UNUSED(buffer);UNUSED(is_debuggable);UNUSED(get_library_system_property);const char* default_library = kLibraryFallback;#if (library == NULL) {library = default_library;}return library;}// "persist.sys.dalvik.vm.lib.2"是系統(tǒng)屬性// 它的取值可以為libdvm.so或libart.soint GetLibrarySystemProperty(char* buffer) {# __ANDROID__return __system_property_get("persist.sys.dalvik.vm.lib.2", buffer);#UNUSED(buffer);return 0;#}
常見內(nèi)存分析工具
2.1 Android Profiler

標(biāo)注(1~6)說明: 1:用于強(qiáng)制執(zhí)行垃圾回收事件的按鈕;
2:用于捕獲堆轉(zhuǎn)儲(chǔ)的按鈕,即Dump the Java heap;
3:用于放大、縮小、復(fù)位時(shí)間軸的按鈕;
4 :用于實(shí)時(shí)播放內(nèi)存分配情況的按鈕;
5:發(fā)生一些事件的記錄(如Activity的跳轉(zhuǎn),事件的輸入,屏幕的旋轉(zhuǎn));
6:內(nèi)存使用量事件軸,它包括以下內(nèi)容:一個(gè)堆疊圖表。顯示每個(gè)內(nèi)存類別當(dāng)前使用多少內(nèi)存,如左側(cè)的y軸和頂部的彩色健所示。
Java:從Java或Kotlin代碼分配的對(duì)象的內(nèi)存(重點(diǎn)關(guān)注); Native:從C或C++代碼分配的對(duì)象的內(nèi)存(重點(diǎn)關(guān)注); Graphics:圖像緩存等,包括GL surfaces, GL textures等; Stack:棧內(nèi)存(包括java和c/c++); Code:用于處理代碼和資源(如 dex 字節(jié)碼.so 庫和字體)分配的內(nèi)存; Other:系統(tǒng)都不知道是什么類型的內(nèi)存,放在這里; Allocated:從Java或Kotlin代碼分配的對(duì)象數(shù)。 一個(gè)堆疊圖表。顯示每個(gè)內(nèi)存類別當(dāng)前使用多少內(nèi)存,如左側(cè)的y軸和頂部的彩色健所示。 一條虛線。虛線表示分配的對(duì)象數(shù)量,如右側(cè)的y軸所示(5000/15000)。
每個(gè)垃圾回收時(shí)間的圖標(biāo)。
2.1.1 Allocation Tracker
分配了哪些類型的對(duì)象,分配了多大的空間; 對(duì)象分配的棧調(diào)用,是在哪個(gè)線程中調(diào)用的; 對(duì)象的釋放時(shí)間(只針對(duì)8.0+);
接下來,我們就以上一篇文章中所提及的單例模式引起的內(nèi)存泄漏為例,來檢查內(nèi)存分配的記錄,排查可能存在內(nèi)存泄漏的對(duì)象。具體的步驟如下:
快速查找某個(gè)類,比如SingleInstanceActivity,當(dāng)然我們還可以使用正則表達(dá)式Regex和大小寫匹配Match Case。紅色方框中其他選項(xiàng)意義:Allocations:堆中動(dòng)態(tài)分配對(duì)象個(gè)數(shù); Deallocations:解除分配的對(duì)象個(gè)數(shù); Total Counts:目前存在的對(duì)象總數(shù); Shallow Size:堆中所有對(duì)象的總大小(以字節(jié)為單位),不包含其引用的對(duì)象;
(3) 如果我們希望確定(2)中無法被GC的對(duì)象被誰持有,可以點(diǎn)擊該對(duì)象,此時(shí)在Instance View窗口的下方就會(huì)出現(xiàn)Allocation Call Stack標(biāo)簽,如上圖藍(lán)色方框所示,該標(biāo)簽中顯示了該對(duì)象被分配到何處以及哪里線程中,此外,我們還可以在標(biāo)簽中右鍵點(diǎn)擊任意行并選擇Jump to Source,以在編輯器中打開該代碼。2.1.2 Heap Dump
該時(shí)刻應(yīng)用分配了哪些類型的對(duì)象,每種對(duì)象有多少; 每個(gè)對(duì)象當(dāng)前時(shí)刻使用了多少內(nèi)存; 對(duì)象所分配到的調(diào)用堆棧(Android 7.1以下會(huì)有所區(qū)別);
實(shí)現(xiàn),獲得某一時(shí)刻的Heap Dump如下圖:

下面我們解釋下上圖顏色方框中相關(guān)標(biāo)簽名表示的意義。
Allocations: 堆中分配對(duì)象的個(gè)數(shù); Native Size:此對(duì)象類型使用的native內(nèi)存總量。此列僅適用于Android 7.0及更高版本。您將在這里看到一些用Java分配內(nèi)存的對(duì)象,因?yàn)锳ndroid使用native內(nèi)存來處理某些框架類,例如Bitmap。 Shallow Size: 此對(duì)象類型使用的Java內(nèi)存總量; Retained Size: 因此類的所有實(shí)例而保留的內(nèi)存總大??;
Depth:從任意 GC root 到所選實(shí)例的最短 hop 數(shù)。 Native Size: native內(nèi)存中此實(shí)例的大小。此列僅適用于Android 7.0及更高版本。 Shallow Size:此實(shí)例Java內(nèi)存的大小。 Retained Size:此實(shí)例支配[dominator]的內(nèi)存大?。ǜ鶕?jù) [支配樹]
2.2 MAT
生成,輸出的文件格式為hprof,分析工具使用的是MAT。由于Memory Profiler生成的hprof文件不是標(biāo)準(zhǔn)的hprof文件,需要使用SDK自帶的hprof-conv進(jìn)行轉(zhuǎn)換,它的路徑在sdk/platform-tools中,執(zhí)行命令:hprof-conv E:\1.hprof E:\standar.hprof。
MAT,全稱"Memory Analysis Tool",是對(duì)內(nèi)存進(jìn)行詳細(xì)分析的工具,它是eclipse的一個(gè)插件,對(duì)于AS開發(fā)來說,需要單獨(dú)下載MAT(當(dāng)前最新版本為1.9.1)。

在上述圖中,我們主要關(guān)注兩個(gè)部分:餅狀圖和Actions,其中,餅狀圖主要用來顯示內(nèi)存的消耗,它的彩色部分表示被分配的內(nèi)存,灰色部分則是空閑區(qū)域,單擊每個(gè)彩色區(qū)域可以看到這塊區(qū)域的詳細(xì)信息;Actons一欄列出了4種Action,其作用與區(qū)別如下。
Historgram:列出每個(gè)類的所有對(duì)象。從類的角度進(jìn)行分析,注重量的分析; Dominator Tree:列出大對(duì)象和它們的引用關(guān)系。從對(duì)象的角度分析,注重引用關(guān)系分析; Top Consumers:獲取開銷最大的對(duì)象,可通過類或包形式分組; Duplicate Classes:檢測(cè)出被多個(gè)類加載器加載的類;

從上圖可以看到,在Dominator Tree列出了很多SingleInstanceActivity的實(shí)例,而一般SingleInstanceActivity是不該有這么多實(shí)例的,因此,基本可以斷定發(fā)生了內(nèi)存泄漏,至于內(nèi)存泄漏的具體原因,就需要查看GC引用鏈。但在查看之前,我們需要理解下紅色方框幾個(gè)標(biāo)簽的意義。
Shallow Heap
Retained Heap

?
對(duì)象C直接支配對(duì)象D、E、H,故C是D、E、H的父節(jié)點(diǎn);
對(duì)象D直接支配對(duì)象F,故D是F的父節(jié)點(diǎn);
對(duì)象E直接支配對(duì)象G,故E是G的父節(jié)點(diǎn);

