Android 性能篇 - 內(nèi)存優(yōu)化二(精華篇)

本篇文章已授權(quán)微信公眾號 guolin_blog(郭霖)獨家發(fā)布

一、內(nèi)存的劃分

二、java 內(nèi)存優(yōu)化

三、native 內(nèi)存優(yōu)化

四、graphics 內(nèi)存優(yōu)化

五、stack 內(nèi)存優(yōu)化

六、code 內(nèi)存優(yōu)化

七、other 內(nèi)存優(yōu)化

一、內(nèi)存的劃分

分類的標(biāo)準(zhǔn) procrank dumpsys meminfo android studio profile JMM
劃分區(qū)域 VSS/RSS/PSS/USS Native/Dalvik/Cursor/Ashmem.. java/native/graphics/stack/code/other 方法區(qū)/java堆/java棧/native棧/程序計數(shù)器
1、procrank 是一個 adb 的 root 指令,可以查詢內(nèi)存的劃分:
procrank
  • VSS - Virtual Set Size 虛擬耗用內(nèi)存(包含共享庫占用的內(nèi)存)
  • RSS- Resident Set Size 實際使用物理內(nèi)存(包含所有共享庫占用的內(nèi)存)
  • PSS- Proportional Set Size 實際使用的物理內(nèi)存(按比例分配共享庫占用的內(nèi)存)
  • USS- Unique Set Size 進程獨自占用的物理內(nèi)存(不包含共享庫占用的內(nèi)存)

那么最值得關(guān)注的是 PSSUSS,我們可以用 dumpsys meminfo 來查詢(無需 root 權(quán)限)

2、dumpsys meminfo 查詢 pss 劃分
dumpsys meminfo

重點字段解讀:

  • Native Heap - Native 堆內(nèi)存
  • Dalvik Heap - Dalvik虛擬機內(nèi)存,Dalvik虛擬機代碼在 libdvm.so 主要負責(zé)運行時dex解析成機器碼(android 5.0+ ART 中已經(jīng)取消 Dalvik 虛擬機,這里任然出現(xiàn) Dalvik 目測是沒改過來)
  • .art mmap - Android RunTime 映射內(nèi)存,art 代碼在 android_runtime.so, mmap(是linux C 的一個函數(shù)接口,用來做內(nèi)存映射)
  • Private Dirty - 進程獨占的內(nèi)存,內(nèi)存已經(jīng)被本進程修改過,只能被自己進程使用
  • Private clean - 進程獨占的內(nèi)存,內(nèi)存是映射過來的,沒有做修改,可以置換給到其他進程使用
  • java heap - Dalvik heapdirty+ clean) + art heapdirty + clean

通過上面圖片可得 launcher app 占用的內(nèi)存是 250M,大部分內(nèi)存在 Native Heap、codegraphics,那如何分析和解決,我們下面講。

3、android studio profile 是 ide 提供出來的分類:
android studio profile
  • Total - 整個應(yīng)用占據(jù)的總內(nèi)存
  • Java - java 堆占據(jù)的內(nèi)存
  • Native - Native 層調(diào)用 malloc/newC/C++)占據(jù)的內(nèi)存
  • Graphics - 圖形緩沖區(qū)隊列向屏幕顯示像素,(如果沒有用到 OpenGL 或者不是游戲,可以直接忽略)
  • Stack - 線程棧占據(jù)的內(nèi)存
  • Code - dex + so 庫占據(jù)的內(nèi)存
  • Others - 未解之謎
4、JMM 分類
  • 方法區(qū) - 存放常量和靜態(tài)變量區(qū)域
  • java堆 - new出來的對象內(nèi)存都放在這里面
  • java棧 - 方法中執(zhí)行的基本類型變量 和 變量引用都在棧中。(類中的內(nèi)部成員屬性的引用在堆中)
  • native棧 - 同 java棧 ,指針引用都在 native棧 中
  • 程序計數(shù)器 - 作用是多線程切換記錄上一個線程執(zhí)行到的點。譬如:A 線程 切換到 B 線程。程序計數(shù)器要記錄 A 線程 已經(jīng)執(zhí)行到哪一行代碼。接著 cpu 切換到 B 線程,再切換回來 A 線程的時候,cpu 才知道 從 A 線程哪一行代碼繼續(xù)執(zhí)行。

java 棧、native 棧、程序計數(shù)器是線程私有
java 堆、方法區(qū)是線程共有的。

二、java 內(nèi)存優(yōu)化

Java 內(nèi)存優(yōu)化 內(nèi)存泄漏 內(nèi)存抖動 大內(nèi)存對象使用
發(fā)生的場景 單例、匿名內(nèi)部類、接口忘記釋放 ... String拼接、循環(huán)內(nèi)重復(fù)生成對象 ... HashMap、ArrayList ...

詳細的理論可參考這篇文章

1、Java 檢查泄漏 - LeakCanary 使用
1.1、 LeakCanary 結(jié)果分析

LeakCanary 可以檢查 Activity Fragment View 界面的泄漏問題。通過接入 LeakCanary
接入庫地址) 跑上 monkey 接著靜等 java 內(nèi)存泄漏的出現(xiàn):

泄漏檢查

通過上圖可以知道 SearchActivityHistorySource.mContext 持有,HistorySource 是一個單例,然后最頂層的 Thread.contextClassLoader 就是 GC root(注意:靜態(tài)變量不是 GC root),Thread.contextClassLoaderPathClassLoader 類,只要把 SearchActivitycontext 換成 Application 那就解決了。

1.2、Android 中 GC root 有哪些:
  • System ClassLoader 加載過的類,繼而生成的對象,譬如 rt.jar 中的類
  • PathClassLoader、DexClassLoader
  • 活著的線程 Thread
  • 函數(shù)方法中的局部變量(跑在線程中的)
  • JNI 中的全局變量和局部變量

GC root 更多詳情

1.3、LeakCanary 的核心原理:
  • 通過 registerActivityLifecycleCallbacks() 監(jiān)聽 各個 Activity 的退出
  • Activity 退出后 ,拿到 Activity 的對象封裝成 KeyedWeakReference 弱引用對象。
  • 通過手動 Runtime.getRuntime().gc(); 垃圾回收
  • 通過 removeWeaklyReachableReferences() 手動移除已經(jīng)被回收的對象
  • 通過 gone() 函數(shù)判斷是否被移除,如果移除了,說明Activity 已經(jīng)沒有其他強引用 在引用它,沒有泄露
  • 如果沒有移除,通過 android 原生接口 Debug.dumpHprofData(),把 Hprof 文件搞下來,通過 haha 這個第三方庫去解析是否有指定 Activity 的殘留。(haha 是分析 Hprofjava 庫)

小結(jié):

那么LeakCanary 只能解決界面上的泄漏,其他內(nèi)存上的優(yōu)化是做不到的,譬如:線程池的泄漏,內(nèi)存的抖動,大對象的濫用.. 那么就需要更為強大的工具 MAT

2、內(nèi)存檢測工具 MAT

MAT 是分析內(nèi)存文件 hprof 的工具。(MAT 工具地址

2.1 、抓取步驟

跑幾分鐘 monkey 后,退回應(yīng)用主界面,手動多次點擊GC 按鈕,把可回收的回收掉,為了剔除臟數(shù)據(jù)。通過 Android StudioProfile 把 內(nèi)存文件 hprofdump 下來。

抓取步驟
  • 進入Android SDK 目錄:G:\AndroidSDK\platform-tools
  • dump 下載的文件 memory-20190828T162317.hprof 拖進 platform-tools 文件夾
  • 敲入cmd 命令 hprof-conv memory-20190828T162317.hprof 1.hprof轉(zhuǎn)成可被 MAT 識別的 1.hprof 文件
  • 使用 MAT 打開 1.hprof
2.2 、分析內(nèi)存:

完成以上步驟之后的結(jié)果圖

hprof
  • 直接點擊左上角 Histogram 查看內(nèi)存分布
  • objects - 對象數(shù)目
  • shallow heap - 對象自身實際占用的堆大小
  • retained heap - 對象被回收后能釋放多少內(nèi)存
  • Inspector - 可以看到對象的 GC Root 是誰
1566982559(1).png
  • with outgoing references - 表示的是 當(dāng)前對象,引用了內(nèi)部哪些成員對象
  • with incoming references - 表示的是 當(dāng)前對象,被外部哪些對象應(yīng)用了(重點操作)
1566988199(1).png
  • merge shortest paths to gc roots - 從GC roots 到一個或一組對象的公共路徑
  • exclude all phantom/weak/soft etc. references - 排除一些類型的引用(如軟引用、弱引用、虛引用),留下強引用
1566988599(1).jpg
  • 為了避免查看太多并不是強相關(guān)的對象,直接從本應(yīng)用的java 類入手,MAT 也提供正則式過濾,直接 輸入.*com.vd.*(本應(yīng)用 ``````packageName```) 去過濾,結(jié)果就非常明顯,整個應(yīng)用自己寫的對象占用的內(nèi)存都在這里。從大的對象下手,是否這個對象有存在的意義,是否需要占這么大的一個內(nèi)存。是否可以對其做相應(yīng)的處理。
1566989713(1).jpg
  • MAT 提供了更加方便的 OQL 查詢,可以找到指定一個名字的對象,包括可以根據(jù)本身 java 對象的成員屬性來做條件語句。譬如上圖我找長寬都大于 100px 的圖片都有哪些。可以把大圖片揪出來。

MAT 官方使用指導(dǎo)


小結(jié)

可先用LeakCanary 跑出明顯的內(nèi)存泄漏,再用MAT 檢查整個應(yīng)用的內(nèi)存分布狀況,去優(yōu)化該優(yōu)化的 Java 堆內(nèi)存。

三、native 內(nèi)存優(yōu)化

native 內(nèi)存優(yōu)化 malloc_debug heapsnap DDMS
root權(quán)限 需要 需要 不需要
環(huán)境 python jni 需要使用sdk18 的 tools/ddms.bat(sdk 18之后就被剔除了)
  • malloc_debug是官方推薦的一種方法,目前效果還不錯
  • heapsnap 是一個可以跑在AdnroidC github 開源庫 ,目前只能查詢內(nèi)存泄漏。而且編譯不過,原因是缺少了一些庫。在它基礎(chǔ)上我整合了一份編譯成功,有興趣點擊這里
  • DDMS 目前被遺棄,在 android 9.0 沒整成功,放棄。
1、malloc_debug 步驟
  • 開啟 malloc debug 模式,打開 cmd 窗口輸入
//查詢所有內(nèi)存
adb shell setprop wrap.packagename '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'

//查詢內(nèi)存泄漏
adb shell setprop wrap.packagename '"LIBC_DEBUG_MALLOC_OPTIONS=leak_track logwrapper"'

1567132646(1).jpg
  • 關(guān)掉自身應(yīng)用,再打開,monkey 跑起來
  • 通過adb shell dumpsys meminfo com.all.videodownloader.videodownload 查到 pid 為 2968
image.png
  • 通過adb shell am dumpheap -n <PID_TO_DUMP> /data/local/tmp/heap.txt 把文件抓取出來到 /data/local/tmp/heap.txt
image.png
  • native 內(nèi)存文件 拷貝出來,等下分析
image.png
2、使用 python 分析
2.1、搭建環(huán)境
  • 下載 native_heapdump_viewer.py
  • python編譯器我選擇了 PyCharm
  • 新建項目,把native_heapdump_viewer.pyheap.txt,放到同一個目錄,如下圖
    image.png
2.2、修改 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):函數(shù)中的部分代碼,把下面代碼:

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 里面的內(nèi)存地址都需要 通過so 庫來查找對應(yīng)的是哪一個函數(shù)。而 so 存放的父路徑地址就是 self.symboldir,那么也就是說需要把 手機上的 /system/lib64、/vendor/lib64/整個 文件夾pull 下來到電腦上,譬如這里是pull
    C:/Users/chaojiong.zhang/Documents/AndroidStudio/DeviceExplorer/xiaomi-mi_8-4b429b4

    image.png

  • def main(): 函數(shù)插入部分代碼在函數(shù)第一行插入和最后一行插入以下代碼,目的是直接把結(jié)果 log 輸出到 test.txt 可以直接查看。

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

 //...
sys.stdout.close()
  • 跑起來


    image.png
3、malloc_debug 內(nèi)存文件分析
3.1、字段解讀
  • BYTES- 占用的內(nèi)存大小單位byte
  • %TOTAL - 占總 native 內(nèi)存百分比
  • %PARENT - 占父幀內(nèi)存百分比
  • COUNT - 調(diào)用了多少次
  • ADDR- 內(nèi)存地址
  • LIBRARY - 占用的內(nèi)存所屬哪一個 so
  • FUNCTION- 占用的內(nèi)存所屬哪一個方法
  • LOCATION - 占用的內(nèi)存所屬哪一行
3.2、內(nèi)存信息分析一
10285756  58.29%  99.95%       49     eac0b276 /system/lib/libhwui.so android::Bitmap::allocateHeapBitmap(SkBitmap*)

可以看得出來 allocateHeapBitmap方法占用了,10M 左右的內(nèi)存,占總 native 內(nèi)存 58.29%,占父幀 99.95% (意思是:A-> B ,A方法調(diào)用B方法,A方法總共占用了 10M,其中9.9M 是在B方法中申請的,那么 %PARENT 就是 99%),調(diào)用了49 次,動作發(fā)生在 libhwui.so 中的 android::Bitmap::allocateHeapBitmap方法。下面是 allocateHeapBitmap 被調(diào)用的流程:

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 創(chuàng)建都會跑到 allocateHeapBitmap 這個函數(shù)。那么上面這個占用了 10MallocateHeapBitmap,究竟是 java 層哪個類調(diào)用下來的,這個目前是無解(包括最近華為的方舟環(huán)境平臺 DevEco 也不行),只能在 java 層去全盤查詢了,哪些圖片使用了較多的內(nèi)存。

3.3、內(nèi)存信息分析二
image.png
  • WebViewGoogle.apk占用了 10M 的內(nèi)存,WebViewGoogle.apk就是應(yīng)用使用的WebView,android 5.0 之前作為模塊存在于 frameworks/base目錄下,并提供接口。android 5.0 之后變成了編譯為一個獨立的apk ,包名是 com.android.webview。檢查所有的WebView 使用情況,譬如:如果場景允許,使用完畢是否有 調(diào)用 WebView.clearCache()
  • boot-framework.oat 占了5M ,Android framework 代碼通過dex2oat轉(zhuǎn)成的 oat 二進制文件(機器碼),無需優(yōu)化
  • libandroid_runtime.so 占了 5M ,虛擬機內(nèi)存,屬于按比例劃分共享庫,無需優(yōu)化

小結(jié)

native 內(nèi)存目前無法很清晰的定位到對應(yīng)的java 層代碼,無解。只能看個大概,然后有目的性去排查某個類,或者某個模塊。

四、graphics 內(nèi)存優(yōu)化

若應(yīng)用沒有自己接入 OpenGL/ GL surfaces/ GL textures開源庫,來繪制圖形,可不必理會。畢竟已經(jīng)超出 android應(yīng)用工程師的范圍了。

五、stack 內(nèi)存優(yōu)化

1、解決棧溢出
1.1、死循環(huán)問題
  • JDK 1.8 之前的 HaskMap,避免使用多線程造成死循環(huán)問題。
1.2、遞歸問題
  • 避免深層次的遞歸問題,較深層次的遞歸可采用尾遞歸的方法。
  • 遞歸的退出,最好用標(biāo)識位退出?;蛘咄ㄟ^線程 interrupt(),isInterrupted() 去退出遞歸,確保遞歸正確退出。遞歸中如果有 Thread.sleep ,要注意中斷被消費問題。
1.3、Intenet 問題
  • 對于 Intent 傳遞大對象,或者 ArrayList<Info>,Intent 的上限是 505K 。
    解決方案:
  1. 一般通過 static 持有需要傳遞的對象解決。
  2. 把跳轉(zhuǎn)的頁面寫成 fragment ,數(shù)據(jù)可以不需要傳遞也可獲取
  3. 通過EventBus RxBus(原理都是通過全局單例來傳遞)
  4. 通過 ObjectCache 把對象轉(zhuǎn)成json 串,保存到本地,獲取時候序列化為對象。
2、解決重復(fù)生成局部變量
2.1、避免在循環(huán)內(nèi)重復(fù)生成局部變量:
    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() 會在 循環(huán)內(nèi)生成 100shake 局部變量 + 100 個局部變量的引用,
memoryShake1()會在 循環(huán)內(nèi)生成 100shake 局部變量 + 1 個局部變量的引用,一個對象引用在 64bit 的環(huán)境是 8byte 。100*8 = 800 byte = 0.8KB

2.2、String 使用問題

循環(huán)內(nèi)字符的拼接不要使用 + 符號,(使用 + 符號,編譯成字節(jié)碼后,循環(huán)內(nèi)會生成StringBuilder 對象去拼接)。

正確應(yīng)該使用StringBuffer (線程安全)或者 StringBuilder(線程不安全)。

六、code 內(nèi)存優(yōu)化

code 內(nèi)存消耗主要是: so 庫,dex ,ttf。
以上三種文件都是要加載到運行內(nèi)存才能被解析運行,所以它們的體積要算進自身的應(yīng)用內(nèi)存中。

  • so 庫,可以通過 STRIP 去掉一些符號表 和調(diào)試信息,在Android.mk 加入 LOCAL_STRIP_MODULE:= true,即可。

  • dex,是 java 代碼編譯成的字節(jié)碼,沒混淆的 apk 中的 dex 會大很多,混淆后的dex 會小很多,所以 debug 包的內(nèi)存占用會大于 release 包。Android Studio 3.3帶了了一個新特新 R8 壓縮,可以在gradle.properties 加入 android.enableR8=true ,減小 dex 包的體積(完美兼容現(xiàn)有混淆)。當(dāng)然還要剔除自身應(yīng)用的無用代碼,可使用
    Android Studio Menu > Refactor > Remove Unused Resources 進行排查,這里不再詳細展開。

  • ttf - 如果應(yīng)用中只用到部分字體,可通過 FontZip 提取使用的字體。

七、other 內(nèi)存優(yōu)化

目前不清楚這部分是哪部分內(nèi)存,無從下手,不過一般 other 的內(nèi)存占用比例都是比較小,可不必理會。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容