Android 進階——一次偶現的原生Settings 內存泄漏導致的系統重啓的定位和解決

引言

一次針對系統全應用跑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:lowmemorykillerZygote

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,最好在單獨的進程中運行

更多內存優化文章可參見性能優化系列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章