本篇文章已授权微信公众号 guolin_blog(郭霖)独家发布
一、内存的划分
二、java 内存优化
三、native 内存优化
四、graphics 内存优化
五、stack 内存优化
六、code 内存优化
七、other 内存优化
一、内存的划分
分类的标准 | procrank | dumpsys meminfo | android studio profile | JMM |
---|---|---|---|---|
划分区域 | VSS/RSS/PSS/USS | Native/Dalvik/Cursor/Ashmem.. | java/native/graphics/stack/code/other | 方法区/java堆/java栈/native栈/程序计数器 |
1、procrank 是一个 adb 的 root 指令,可以查询内存的划分:
-
VSS
-Virtual Set Size
虚拟耗用内存(包含共享库占用的内存) -
RSS
-Resident Set Size
实际使用物理内存(包含所有共享库占用的内存) -
PSS
-Proportional Set Size
实际使用的物理内存(按比例分配共享库占用的内存) -
USS
-Unique Set Size
进程独自占用的物理内存(不包含共享库占用的内存)
那么最值得关注的是
PSS
和USS
,我们可以用dumpsys meminfo
来查询(无需root
权限)
2、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
)
通过上面图片可得
launcher app
占用的内存是 250M,大部分内存在Native Heap
、code
、graphics
,那如何分析和解决,我们下面讲。
3、android studio profile 是 ide 提供出来的分类:
-
Total
- 整个应用占据的总内存 -
Java
-java
堆占据的内存 -
Native
-Native
层调用malloc/new
(C/C++
)占据的内存 -
Graphics
- 图形缓冲区队列向屏幕显示像素,(如果没有用到OpenGL
或者不是游戏,可以直接忽略) -
Stack
- 线程栈占据的内存 -
Code
-dex
+so
库占据的内存 -
Others
- 未解之谜
4、JMM 分类
- 方法区 - 存放常量和静态变量区域
-
java
堆 -new
出来的对象内存都放在这里面 -
java
栈 - 方法中执行的基本类型变量 和 变量引用都在栈中。(类中的内部成员属性的引用在堆中) -
native
栈 - 同java
栈 ,指针引用都在native
栈 中 - 程序计数器 - 作用是多线程切换记录上一个线程执行到的点。譬如:
A
线程 切换到B
线程。程序计数器要记录A
线程 已经执行到哪一行代码。接着cpu
切换到B
线程,再切换回来A
线程的时候,cpu
才知道 从A
线程哪一行代码继续执行。
java
栈、native
栈、程序计数器是线程私有
java
堆、方法区是线程共有的。
二、java 内存优化
Java 内存优化 | 内存泄漏 | 内存抖动 | 大内存对象使用 |
---|---|---|---|
发生的场景 | 单例、匿名内部类、接口忘记释放 ... | String拼接、循环内重复生成对象 ... | HashMap、ArrayList ... |
详细的理论可参考这篇文章
1、Java 检查泄漏 - LeakCanary 使用
1.1、 LeakCanary 结果分析
LeakCanary
可以检查 Activity Fragment View
界面的泄漏问题。通过接入 LeakCanary
(接入库地址) 跑上 monkey
接着静等 java
内存泄漏的出现:
通过上图可以知道
SearchActivity
被HistorySource.mContext
持有,HistorySource
是一个单例,然后最顶层的Thread.contextClassLoader
就是GC root
(注意:静态变量不是GC root
),Thread.contextClassLoader
是PathClassLoader
类,只要把SearchActivity
的context
换成Application
那就解决了。
1.2、Android 中 GC root 有哪些:
- 被
System ClassLoader
加载过的类,继而生成的对象,譬如rt.jar
中的类 PathClassLoader、DexClassLoader
- 活着的线程
Thread
- 函数方法中的局部变量(跑在线程中的)
-
JNI
中的全局变量和局部变量
GC root 更多详情
1.3、LeakCanary 的核心原理:
- 通过
registerActivityLifecycleCallbacks()
监听 各个Activity
的退出 -
Activity
退出后 ,拿到Activity
的对象封装成KeyedWeakReference
弱引用对象。 - 通过手动
Runtime.getRuntime().gc();
垃圾回收 - 通过
removeWeaklyReachableReferences()
手动移除已经被回收的对象 - 通过
gone()
函数判断是否被移除,如果移除了,说明Activity
已经没有其他强引用 在引用它,没有泄露 - 如果没有移除,通过
android
原生接口Debug.dumpHprofData()
,把Hprof
文件搞下来,通过haha
这个第三方库去解析是否有指定Activity
的残留。(haha
是分析Hprof
的java
库)
小结:
那么LeakCanary
只能解决界面上的泄漏,其他内存上的优化是做不到的,譬如:线程池的泄漏,内存的抖动,大对象的滥用.. 那么就需要更为强大的工具 MAT
了
2、内存检测工具 MAT
MAT
是分析内存文件 hprof
的工具。(MAT 工具地址)
2.1 、抓取步骤
跑几分钟 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
2.2 、分析内存:
完成以上步骤之后的结果图
- 直接点击左上角
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
的图片都有哪些。可以把大图片揪出来。
MAT 官方使用指导
小结
可先用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
没整成功,放弃。
1、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
把文件抓取出来到/data/local/tmp/heap.txt /data/local/tmp/heap.txt
- 把
native
内存文件 拷贝出来,等下分析
2、使用 python 分析
2.1、搭建环境
- 下载 native_heapdump_viewer.py
-
python
编译器我选择了 PyCharm - 新建项目,把
native_heapdump_viewer.py
和heap.txt
,放到同一个目录,如下图
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):
函数中的部分代码,把下面代码:
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()
-
跑起来
3、malloc_debug 内存文件分析
3.1、字段解读
-
BYTES
- 占用的内存大小单位byte
-
%TOTAL
- 占总native
内存百分比 -
%PARENT
- 占父帧内存百分比 -
COUNT
- 调用了多少次 -
ADDR
- 内存地址 -
LIBRARY
- 占用的内存所属哪一个so
库 -
FUNCTION
- 占用的内存所属哪一个方法 -
LOCATION
- 占用的内存所属哪一行
3.2、内存信息分析一
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
层去全盘查询了,哪些图片使用了较多的内存。
3.3、内存信息分析二
-
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 内存优化
1、解决栈溢出
1.1、死循环问题
-
JDK 1.8
之前的HaskMap
,避免使用多线程造成死循环问题。
1.2、递归问题
- 避免深层次的递归问题,较深层次的递归可采用尾递归的方法。
- 递归的退出,最好用标识位退出。或者通过线程
interrupt()
,isInterrupted()
去退出递归,确保递归正确退出。递归中如果有Thread.sleep
,要注意中断被消费问题。
1.3、Intenet 问题
- 对于
Intent
传递大对象,或者ArrayList
,Intent
的上限是505K
。
解决方案:
- 一般通过
static
持有需要传递的对象解决。 - 把跳转的页面写成
fragment
,数据可以不需要传递也可获取 - 通过
EventBus RxBus
(原理都是通过全局单例来传递) - 通过 ObjectCache 把对象转成
json
串,保存到本地,获取时候序列化为对象。
2、解决重复生成局部变量
2.1、避免在循环内重复生成局部变量:
private void memoryShake() {
ArrayList shakes = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Integer shake = new Integer(i);
shakes.add(shake);
}
}
private void memoryShake1() {
ArrayList 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
2.2、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 提取使用的字体。
七、other 内存优化
目前不清楚这部分是哪部分内存,无从下手,不过一般 other
的内存占用比例都是比较小,可不必理会。