講一講 Android 內存優化的小祕密

/   內存的劃分   /

procrank是一個adb的root指令,能夠查詢內存的劃分:

  • VSS - Virtual Set Size虛擬耗用內存(包含共享庫佔用的內存)
  • RSS- Resident Set Size實際使用物理內存(包含全部共享庫佔用的內存)
  • PSS- Proportional Set Size實際使用的物理內存(按比例分配共享庫佔用的內存)
  • USS- Unique Set Size進程獨自佔用的物理內存(不包含共享庫佔用的內存)

那麼最值得關注的是PSS和USS,咱們能夠用dumpsys meminfo來查詢(無需root權限)java

dumpsys meminfo查詢pss劃分

重點字段解讀:python

  • Native Heap - Native堆內存
  • Dalvik Heap - Dalvik虛擬機內存,Dalvik虛擬機代碼在 libdvm.so 主要負責運行時dex解析成機器碼(android 5.0+ ART 中已經取消 Dalvik 虛擬機,這裏任然出現 Dalvik 目測是沒改過來)
  • .art mmap - Android RunTime映射內存,art代碼在 android_runtime.so, mmap(是linux C的一個函數接口,用來作內存映射)
  • Private Dirty - 進程獨佔的內存,內存已經被本進程修改過,只能被本身進程使用
  • Private clean - 進程獨佔的內存,內存是映射過來的,沒有作修改,能夠置換給到其餘進程使用
  • java heap - Dalvik heap(dirty+ clean) + art heap(dirty + clean)

經過上面圖片可得launcher app佔用的內存是250M,大部份內存在Native Heap、code、graphics,那如何分析和解決,咱們下面講。linux

android studio profile是ide提供出來的分類:

  • Total - 整個應用佔據的總內存
  • Java - java堆佔據的內存
  • Native - Native層調用malloc/new(C/C++)佔據的內存
  • Graphics - 圖形緩衝區隊列向屏幕顯示像素,(若是沒有用到OpenGL或者不是遊戲,能夠直接忽略)
  • Stack - 線程棧佔據的內存
  • Code - dex+so庫佔據的內存
  • Others - 未解之謎

JMM 分類android

  • 方法區 - 存放常量和靜態變量區域
  • java堆 - new出來的對象內存都放在這裏面
  • java棧 - 方法中執行的基本類型變量 和 變量引用都在棧中。(類中的內部成員屬性的引用在堆中)
  • native棧 - 同java棧,指針引用都在native棧中
  • 程序計數器 - 做用是多線程切換記錄上一個線程執行到的點。譬如:A線程 切換到B線程。程序計數器要記錄A線程 已經執行到哪一行代碼。接着cpu切換到B線程,再切換回來A線程的時候,cpu才知道從A線程哪一行代碼繼續執行

注意:git

  • java棧、native棧、程序計數器是線程私有
  • java堆、方法區是線程共有的

/   Java內存優化   /

Java內存優化 內存泄漏 內存抖動 大內存對象使用
發生的場景 單例、匿名內部類、接口忘記釋放 ... String拼接、循環內重複生成對象 ... HashMap、ArrayList ...
Java檢查泄漏 - LeakCanary使用
LeakCanary結果分析

LeakCanary能夠檢查Activity Fragment View界面的泄漏問題。經過接入LeakCanary跑上monkey接着靜等java內存泄漏的出現:github

image

經過上圖能夠知道SearchActivity被HistorySource.mContext持有,HistorySource是一個單例,而後最頂層的Thread.contextClassLoader就是GC root(注意:靜態變量不是GC root),Thread.contextClassLoader是PathClassLoader類,只要把 SearchActivity的context換成Application那就解決了。web

Android中GC root有哪些:
  • 被System ClassLoader加載過的類,繼而生成的對象,譬如rt.jar中的類
  • PathClassLoader、DexClassLoader
  • 活着的線程Thread
  • 函數方法中的局部變量(跑在線程中的)
  • JNI中的全局變量和局部變量
LeakCanary的核心原理:
  • 經過registerActivityLifecycleCallbacks()監聽各個Activity的退出
  • Activity退出後,拿到Activity的對象封裝成KeyedWeakReference弱引用對象。
  • 經過手動Runtime.getRuntime().gc();垃圾回收
  • 經過removeWeaklyReachableReferences()手動移除已經被回收的對象
  • 經過gone()函數判斷是否被移除,若是移除了,說明Activity 已經沒有其餘強引用 在引用它,沒有泄露
  • 若是沒有移除,經過android原生接口Debug.dumpHprofData(),把Hprof文件搞下來,經過haha這個第三方庫去解析是否有指定Activity的殘留。(haha是分析Hprof的java庫)
小結
那麼LeakCanary只能解決界面上的泄漏,其餘內存上的優化是作不到的,譬如:線程池的泄漏,內存的抖動,大對象的濫用.. 那麼就須要更爲強大的工具MAT了。
內存檢測工具MAT
MAT是分析內存文件hprof的工具。
抓取步驟
跑幾分鐘monkey後,退回應用主界面,手動屢次點擊GC按鈕,把可回收的回收掉,爲了剔除髒數據。經過Android Studio的Profile把內存文件hprof給dump下來。

  • 進入Android SDK目錄:G:\AndroidSDK\platform-tools
  • 把dump下載的文件memory-20190828T162317.hprof拖進platform-tools文件夾
  • 敲入cmd命令hprof-conv memory-20190828T162317.hprof 1.hprof轉成可被MAT識別的1.hprof文件
  • 使用MAT打開1.hprof

分析內存完成以上步驟以後的結果圖。shell

image

  • 直接點擊左上角Histogram查看內存分佈
  • objects - 對象數目
  • shallow heap - 對象自身實際佔用的堆大小
  • retained heap - 對象被回收後能釋放多少內存
  • Inspector - 能夠看到對象的GC Root是誰

  • with outgoing references - 表示的是 當前對象,引用了內部哪些成員對象
  • with incoming references - 表示的是 當前對象,被外部哪些對象應用了(重點操做)

  • merge shortest paths to gc roots - 從GC roots到一個或一組對象的公共路徑
  • exclude all phantom/weak/soft etc. references - 排除一些類型的引用(如軟引用、弱引用、虛引用),留下強引用

爲了不查看太多並非強相關的對象,直接從本應用的java 類入手,MAT 也提供正則式過濾,直接輸入.com.vd.(本應用 packageName)去過濾,結果就很是明顯,整個應用本身寫的對象佔用的內存都在這裏。從大的對象下手,是否這個對象有存在的意義,是否須要佔這麼大的一個內存。是否能夠對其作相應的處理。 json

MAT提供了更加方便的OQL查詢,能夠找到指定一個名字的對象,包括能夠根據自己java對象的成員屬性來作條件語句。譬如上圖我找長寬都大於100px的圖片都有哪些。能夠把大圖片揪出來。windows

小結
可先用LeakCanary跑出明顯的內存泄漏,再用MAT檢查整個應用的內存分佈情況,去優化該優化的Java堆內存。

/   Native內存優化   /

native 內存優化 malloc_debug heapsnap DDMS
root權限 須要 須要 不須要
環境 python jni 須要使用sdk18 的 tools/ddms.bat(sdk 18以後就被剔除了)
  • malloc_debug是官方推薦的一種方法,目前效果還不錯

  • heapsnap 是一個能夠跑在Adnroid的C庫github開源庫 ,目前只能查詢內存泄漏。並且編譯不過,緣由是缺乏了一些庫。在它基礎上我整合了一份編譯成功,有興趣點擊這裏

  • DDMS目前被遺棄,在android 9.0沒整成功,放棄。

malloc_debug步驟開啓malloc debug模式,打開cmd窗口輸入。

//查詢全部內存
adb shell setprop wrap.packagename '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'

//查詢內存泄漏
adb shell setprop wrap.packagename '"LIBC_DEBUG_MALLOC_OPTIONS=leak_track logwrapper"'
複製代碼

  • 關掉自身應用,再打開,monkey跑起來
  • 經過adb shell dumpsys meminfo com.all.videodownloader.videodownload查到pid爲2968

經過adb shell am dumpheap -n <PID_TO_DUMP> /data/local/tmp/heap.txt把文件抓取出來到/data/local/tmp/heap.txt。

把native內存文件拷貝出來,等下分析。

使用python分析
搭建環境
  • 下載native_heapdump_viewer.py
  • python編譯器我選擇了PyCharm
  • 新建項目,把native_heapdump_viewer.py和 heap.txt,放到同一個目錄,以下圖

修改python代碼修改native_heapdump_viewer.py 代碼中NDK配置地方:

resByte = subprocess.check_output(["G:/AndroidNDK/android-ndk-r17/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-objdump""-w""-j"".text""-h", sofile])
複製代碼
p = subprocess.Popen(["G:/AndroidNDK/android-ndk-r17/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-addr2line""-C""-j"".text""-e", sofile, "-f"], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
複製代碼

替換def init(self):函數中的部分代碼,把下面代碼:

if len(extra_args) != 1:
      print(self._usage)
      sys.exit(1)
複製代碼

替換爲:

self.symboldir = "C:/Users/chaojiong.zhang/Documents/AndroidStudio/DeviceExplorer/xiaomi-mi_8-4b429b4"
extra_args.append("dump.txt")
複製代碼

self.symboldir - 就是dump.txt 裏面的內存地址都須要 經過so庫來查找對應的是哪個函數。而so存放的父路徑地址就是self.symboldir,那麼也就是說須要把 手機上的/system/lib6四、/vendor/lib64/整個 文件夾pull 下來到電腦上,譬如這裏是pull到C:/Users/chaojiong.zhang/Documents/AndroidStudio/DeviceExplorer/xiaomi-mi_8-4b429b4。

image

在def main():函數插入部分代碼在函數第一行插入和最後一行插入如下代碼,目的是直接把結果log輸出到test.txt能夠直接查看。

def main(): sys.stdout= open("test.txt""w")

 //...
sys.stdout.close()
複製代碼

跑起來看看。

malloc_debug內存文件分析
字段解讀
  • BYTES- 佔用的內存大小單位byte
  • %TOTAL - 佔總 native 內存百分比
  • %PARENT - 佔父幀內存百分比
  • COUNT - 調用了多少次
  • ADDR- 內存地址
  • LIBRARY - 佔用的內存所屬哪個 so 庫
  • FUNCTION- 佔用的內存所屬哪個方法
  • LOCATION - 佔用的內存所屬哪一行
內存信息分析一
10285756  58.29%  99.95%       49     eac0b276 /system/lib/libhwui.so android::Bitmap::allocateHeapBitmap(SkBitmap*)
複製代碼
能夠看得出來 allocateHeapBitmap方法佔用了,10M左右的內存,佔總native內存 58.29%,佔父幀 99.95% (意思是:A-> B ,A方法調用B方法,A方法總共佔用了10M,其中9.9M是在B方法中申請的,那麼%PARENT 就是99%),調用了49次,動做發生在libhwui.so中的 android::Bitmap::allocateHeapBitmap方法。下面是allocateHeapBitmap被調用的流程:
BitmapFactory.decodeResource -> BitmapFactory.nativeDecodeStream ->BitmapFactory.cpp 中 nativeDecodeStream() -> doDecode() -> SkBitmap.tryAllocPixels() -> ... -> android::Bitmap::allocateHeapBitmap()
複製代碼
Bitmap.createBitmap -> nativeCreate() -> Bitmap.cpp 中的 nativeCreate() -> GraphicsJNI.cpp zhong de allocateJavaPixelRef() -> ... -> android::Bitmap::allocateHeapBitmap() 
複製代碼

也就是說java層的bitmap 建立都會跑到allocateHeapBitmap這個函數。那麼上面這個佔用了10M的 allocateHeapBitmap,到底是java層哪一個類調用下來的,這個目前是無解(包括最近華爲的方舟環境平臺DevEco也不行),只能在java層去全盤查詢了,哪些圖片使用了較多的內存。內存信息分析二

  • WebViewGoogle.apk佔用了10M的內存,WebViewGoogle.apk就是應用使用的WebView,android 5.0以前做爲模塊存在於frameworks/base目錄下,並提供接口。android 5.0以後變成了編譯爲一個獨立的apk,包名是 com.android.webview。檢查全部的WebView使用狀況,譬如:若是場景容許,使用完畢是否有 調用WebView.clearCache()
  • boot-framework.oat 佔了5M ,Android framework代碼經過dex2oat轉成的oat二進制文件(機器碼),無需優化
  • libandroid_runtime.so 佔了 5M,虛擬機內存,屬於按比例劃分共享庫,無需優化
小結
native內存目前沒法很清晰的定位到對應的java層代碼,無解。只能看個大概,而後有目的性去排查某個類,或者某個模塊。

/   graphics內存優化   /

若應用沒有本身接入OpenGL/ GL surfaces/ GL textures開源庫,來繪製圖形,可沒必要理會。畢竟已經超出android應用工程師的範圍了。

/   stack內存優化   /

解決棧溢出
死循環問題
JDK 1.8以前的HaskMap,避免使用多線程形成死循環問題。
遞歸問題
避免深層次的遞歸問題,較深層次的遞歸可採用尾遞歸的方法。遞歸的退出,最好用標識位退出。或者經過線程interrupt(),isInterrupted()去退出遞歸,確保遞歸正確退出。遞歸中若是有Thread.sleep ,要注意中斷被消費問題。
Intenet問題
對於Intent傳遞大對象,或者ArrayList,Intent的上限是505K 。解決方案:
  • 通常經過 static 持有須要傳遞的對象解決。
  • 把跳轉的頁面寫成 fragment ,數據能夠不須要傳遞也可獲取
  • 經過EventBus RxBus(原理都是經過全局單例來傳遞)
  • 經過ObjectCache把對象轉成json串,保存到本地,獲取時候序列化爲對象。
解決重複生成局部變量
避免在循環內重複生成局部變量
 private void memoryShake() {
        ArrayList<Integer> shakes = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Integer shake = new Integer(i);
            shakes.add(shake);
        }
    }

    private void memoryShake1() {
        ArrayList<Integer> shakes = new ArrayList<>();
        Integer shake;
        for (int i = 0; i < 100; i++) {
            shake = new Integer(i);
            shakes.add(shake);
        }
    }
複製代碼

memoryShake()會在循環內生成100個shake局部變量+100個局部變量的引用,memoryShake1()會在循環內生成100個shake局部變量+1個局部變量的引用,一個對象引用在64bit的環境是8byte 。100*8 = 800 byte = 0.8KB。

String使用問題

循環內字符的拼接不要使用+符號,(使用+符號,編譯成字節碼後,循環內會生成StringBuilder對象去拼接)。正確應該使用StringBuffer(線程安全)或者StringBuilder(線程不安全)。

/   code內存優化   /

code內存消耗主要是:so庫,dex,ttf。

以上三種文件都是要加載到運行內存才能被解析運行,因此它們的體積要算進自身的應用內存中。so庫,能夠經過STRIP去掉一些符號表和調試信息,在Android.mk加入 LOCAL_STRIP_MODULE:= true,便可。

dex,是java代碼編譯成的字節碼,沒混淆的apk中的dex會大不少,混淆後的dex 會小不少,因此debug包的內存佔用會大於release包。Android Studio 3.3帶了了一個新特性R8壓縮,能夠在gradle.properties加入 android.enableR8=true ,減少dex包的體積(完美兼容現有混淆)。固然還要剔除自身應用的無用代碼,可以使用Android Studio Menu > Refactor > Remove Unused Resources進行排查,這裏再也不詳細展開。

ttf - 若是應用中只用到部分字體,可經過FontZip提取使用的字體。

相關文章
相關標籤/搜索