引言
一次針對系統全應用跑Monkey 腳本測試時導致設備系統異常重啓…
adb shell monkey -v -v -v -s 100 --throttle 500 --pct-motion 30 --pct-touch 60 --pct-syskeys 10 --ignore-timeouts --ignore-crashes --ignore-security-exceptions --ignore-native-crashes 90000000 > d:\monkey_log_1106.txt
源碼基於 Android7.1.2,僅供參考。
一、分析定位
在分析了aplog、klog、tombstone、dropbox等一堆日誌之後,終於在aplog裏定位到關鍵信息:
首先是兩個重要的TAG:lowmemorykiller 和 Zygote。
1、Low Memory Killer機制
在Android中,當用戶退出應用程序之後,對應的進程也還是存在於系統中,這是爲了方便程序的再次啓動。隨着打開的程序數量的增加,系統的內存會變得不足,此時就需要殺掉一部分進程以釋放內存空間。而至於是否需要殺死一些進程和哪些進程需要被殺死,是通過Low Memory Killer機制來進行判定的,詳情請參見下篇文章。
2、Zygote 概述
Android應用的進程都是從一個叫做Zygote的進程fork出來的,Android 會對每個應用進行內存限制(通過ActivityManager實例的getMemoryClass()查看),也可以查看/system/build.prop中的對應字段來查看App的最大允許申請內存。
3、分析日誌
首先定位到Zygote 第一條日誌的打印處frameworks\base\core\jni\com_android_internal_os_Zygote.cpp
// This signal handler is for zygote mode, since the zygote must reap its children
static void SigChldHandler(int /*signal_number*/) {
pid_t pid;
int status;
// It's necessary to save and restore the errno during this function. Since errno is stored per thread, changing //it here modifies the errno on the thread on which this signal handler executes. If a signal occurs
// between a call and an errno check, it's possible to get the errno set here.
int saved_errno = errno;
//Zygote 監聽了所有進程的死亡
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {//進程正常結束
if (WEXITSTATUS(status)) {
ALOGI("Process %d exited cleanly (%d)", pid, WEXITSTATUS(status));
}
} else if (WIFSIGNALED(status)) {//進程異常死亡
if (WTERMSIG(status) != SIGKILL) {
ALOGI("Process %d exited due to signal (%d)", pid, WTERMSIG(status));
}
if (WCOREDUMP(status)) {
ALOGI("Process %d dumped core.", pid);
}
}
// If the just-crashed process is the system_server, bring down zygote
// so that it is restarted by init and system server will be restarted from there.
if (pid == gSystemServerPid) {//system_server 進程異常終止
ALOGE("Exit zygote because system server (%d) has terminated", pid);
kill(getpid(), SIGKILL);//通知內核殺掉Zygote進程
}
}
...
errno = saved_errno;
}
繼續分析lowmemorykiller 猜想可能是由於內存不足導致核心進程被殺,很可能是因爲內存泄漏(我們的設備系統可用內存爲2G),而從日誌中發現原生Setting 佔用了540M,極其不正常
初步結論:由於原生Settings 內存泄漏導致系統可用內存不足,進而通過lowmemorykiller 機制強殺system_server ,Zygote進程也隨之被殺最終導致系統重啓。
二、案情重現
爲了驗證結論和猜想,在Monkey 測試過程,實時檢測系統內存分配情況,引入阿里開源的性能測試開發工具mobileperf,並單獨對Settings 應用進行Monkey 測試,腳本如下:
adb shell monkey -p com.android.settings -v -v -v -s 100 --throttle 500 --pct-motion 30 --pct-touch 60 --pct-syskeys 10 --ignore-timeouts --ignore-crashes --ignore-security-exceptions --ignore-native-crashes 90000000 > d:\monkey_settings_log_1106.txt
mobileperf輸出的產物:
Settings 進程實時內存分配情況:
從上圖統計中來看,原生Settings 的內存一直在往上增加即使GC後也沒有下降,這就是典型的內存泄漏現象。
再結合MAT(Memory Analyzer Tool)分析.hprof文件:
結果如下:
這些原生對象的實例數目明顯異常,原來是因爲原生Settings 裏的子界面——應用詳情界面裏的菜單(形如通知、權限、存儲、流量使用情況等),這些二級界面跳回到應用詳情界面時會頻繁創建這些對象,卻沒有及時回收舊對象。分析到這基本石錘了原生Settings 存在內存泄漏的隱患。
三、優化修改
由於我們是定製性的系統,修改方案可能沒有共性,就不詳述具體細節,總之修改完畢之後的結果異常令人滿意。再次針對系統全應用執行Monkey 測試時,原生Settings 的內存分配情況
單獨針對Settings 執行Monkey測試時,Setting 的內存分配情況
四、小結
在Android 移動設備上尤其是低端的設備上,內存很容易出現不足的情況,一旦出現內存泄漏等問題,很可能導致不可預知的崩潰,所以在編碼時需要注意儘量避免內存泄漏的隱患,以下有幾條建議(不僅限於):
- 合理使用一些輕量級的數據結構,在Android中很多情況下,我們可以優先考慮使用SparseArray或者ArrayMap替代HashMap等傳統Java的數據結構,相比起這些Android特意開發的ArrayMap、SparseArray的容器,傳統容器消耗更多的資源和相對低效些。因爲HashMap自身需要一個額外的實例對象來記錄Mapping操作導致更加消耗內存,同時SparseArray避免了key和value的自動裝箱,從而避免了自動拆箱。
- 合理使用Bitmap,儘量減小BitMap的內存佔用。在把圖片加載到內存之前,可以根據需求對原始圖片進行縮放處理,避免加載過大的圖片,另外解碼時使用合適的編碼格式。
- 儘量複用Bitmap對象、ViewHolder等對象
- 在使用上下文的時候,儘量優先使用Appcation的Context,而非Activity的Context,當然局部的Dialog必須使用Activity的Context。
- 使用軟引用來避免Handler的泄漏
- 謹慎使用static和單例對象,因爲兩者的生命週期都是與App的生命週期一樣,如果不是需要全生命週期存在就儘量避免過度使用單例和static。
- 注意原始WebView的泄漏(Android系統中一個失敗的控件,不僅僅兼容性存在問題,泄漏也是一大敗筆),如果一定使用原始WebView,最好在單獨的進程中運行
更多內存優化文章可參見性能優化系列