讲一讲 Android 内存优化的小秘密
生活随笔
收集整理的這篇文章主要介紹了
讲一讲 Android 内存优化的小秘密
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
/? ?今日科技快訊? ?/
昨日,美國商務部產業安全局在其官網上宣布,將28家中國組織和企業列入“實體清單”,其中包括海康威視、科大訊飛、曠世科技、大華科技、廈門美亞柏科信息有限公司、依圖科技、頤信科技有限公司共8家人工智能公司。
/? ?作者簡介? ?/
本篇文章來自Overried的投稿,分享了Android開發中內存優化的相關知識,希望對大家有所幫助!同時也感謝作者貢獻的精彩文章。
Overried的博客地址:
https://www.jianshu.com/u/75711cf32043
/? ?內存的劃分? ?/
| 分類的標準 | procrank | dumpsys meminfo | android studio profile | JMM |
劃分區域 | VSS/RSS/PSS/USS | Native/Dalvik/Cursor/Ashmem.. | java/native/graphics/stack/code/othe | 方法區/java堆/java棧/native棧/程序計數器 |
procrank是一個adb的root指令,可以查詢內存的劃分:
- VSS - Virtual Set Size虛擬耗用內存(包含共享庫占用的內存)
- RSS-?Resident Set Size實際使用物理內存(包含所有共享庫占用的內存)
- PSS-?Proportional Set Size實際使用的物理內存(按比例分配共享庫占用的內存)
- USS-?Unique Set Size進程獨自占用的物理內存(不包含共享庫占用的內存)
dumpsys meminfo查詢pss劃分
重點字段解讀:
- 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)
android studio profile是ide提供出來的分類:
- Total?- 整個應用占據的總內存
- Java?-?java堆占據的內存
- Native?-?Native層調用malloc/new(C/C++)占據的內存
- Graphics?- 圖形緩沖區隊列向屏幕顯示像素,(如果沒有用到OpenGL或者不是游戲,可以直接忽略)
- Stack?- 線程棧占據的內存
- Code?-?dex+so庫占據的內存
- Others?- 未解之謎
JMM 分類
- 方法區 - 存放常量和靜態變量區域
- java堆 -?new出來的對象內存都放在這里面
- java棧 - 方法中執行的基本類型變量 和 變量引用都在棧中。(類中的內部成員屬性的引用在堆中)
- native棧 - 同java棧,指針引用都在native棧中
- 程序計數器 - 作用是多線程切換記錄上一個線程執行到的點。譬如:A線程 切換到B線程。程序計數器要記錄A線程 已經執行到哪一行代碼。接著cpu切換到B線程,再切換回來A線程的時候,cpu才知道從A線程哪一行代碼繼續執行
注意:
- java棧、native棧、程序計數器是線程私有
- java堆、方法區是線程共有的
| Java內存優化 | 內存泄漏 | 內存抖動 | 大內存對象使用 |
| 發生的場景 | 單例、匿名內部類、接口忘記釋放 ... | String拼接、循環內重復生成對象 ... | HashMap、ArrayList ... |
Java檢查泄漏 - LeakCanary使用
LeakCanary結果分析
通過上圖可以知道SearchActivity被HistorySource.mContext持有,HistorySource是一個單例,然后最頂層的Thread.contextClassLoader就是GC root(注意:靜態變量不是GC root),Thread.contextClassLoader是PathClassLoader類,只要把 SearchActivity的context換成Application那就解決了。
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
分析內存
完成以上步驟之后的結果圖。
- 直接點擊左上角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)去過濾,結果就非常明顯,整個應用自己寫的對象占用的內存都在這里。從大的對象下手,是否這個對象有存在的意義,是否需要占這么大的一個內存。是否可以對其做相應的處理。
MAT提供了更加方便的OQL查詢,可以找到指定一個名字的對象,包括可以根據本身java對象的成員屬性來做條件語句。譬如上圖我找長寬都大于100px的圖片都有哪些。可以把大圖片揪出來。
小結
可先用LeakCanary跑出明顯的內存泄漏,再用MAT檢查整個應用的內存分布狀況,去優化該優化的Java堆內存。
| 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/lib64、/vendor/lib64/整個 文件夾pull 下來到電腦上,譬如這里是pull到C:/Users/chaojiong.zhang/Documents/AndroidStudio/DeviceExplorer/xiaomi-mi_8-4b429b4。
在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?- 占用的內存所屬哪一行
內存信息分析一
可以看得出來 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被調用的流程:
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內存優化? ?/
/? ?stack內存優化? ?/
解決棧溢出
死循環問題
JDK 1.8之前的HaskMap,避免使用多線程造成死循環問題。
遞歸問題
避免深層次的遞歸問題,較深層次的遞歸可采用尾遞歸的方法。遞歸的退出,最好用標識位退出。或者通過線程interrupt(),isInterrupted()去退出遞歸,確保遞歸正確退出。遞歸中如果有Thread.sleep ,要注意中斷被消費問題。
Intenet問題
對于Intent傳遞大對象,或者ArrayList<Info>,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使用問題
/? ?code內存優化? ?/
以上三種文件都是要加載到運行內存才能被解析運行,所以它們的體積要算進自身的應用內存中。
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提取使用的字體。
歡迎關注我的公眾號 學習技術或投稿
總結
以上是生活随笔為你收集整理的讲一讲 Android 内存优化的小秘密的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BPG边界网关协议知识(二)
- 下一篇: ios苹果免越狱群控电脑鼠标操作手机同步