目前而言,手机硬件已经到达了较高的水平。一般而言,都是性能过剩的。但是内存优化还是有着一定的用处的,提高启动速度,减少内存,在系统回收时,同一优先级的APP,内存越少,回收优先级越低。
同时进行内存优化的过程中,也能够帮忙我们发现必要处理的内存泄露等问题。
模块分布
进行问题分析时,我们要清楚的知道,问题有那几个方面,对于内存优化也是一样,我们要知道Android内存被大致划分为几个部分。
计算方式大致如下:
Java Heap: Dalvik Heap dirty + (.art mmap).dirty + (.art mmap).clean
Native Heap: nativePrivateDirtyCode: (.so mmap).dirty + (.so mmap).clean + (.jar mmap).dirty + (.jar mmap).clean + (.apk mmap).dirty + (.apk mmap).clean + (.ttf mmap).dirty + (.ttf mmap).clean + (.dex mmap).dirty + (.dex mmap).clean + (.oat mmap).clean + (.oat mmap).dirty
Stack: Stack.clean + Stack.dirty
Graphics: (GL mtrack).clean + (GL mtrack).dirty + (EGL mtrack).clean + (EGL mtrack).dirty + (Gfx dev).clean + (Gfx dev).dirty
Private other: Dalvik Heap clean + Dalvik Other dirty + Dalvik Other clean + Ashmem dirty + Ashmem clean + Other dev dirty + Other dev clean + Other mmap dirty + Other mmap clean + Unknown dirty + Unknown clean
其可以大致分为如下几个模块:
|
模块
|
代表部分
|
|---|---|
| Java Heap | 主要是Java代码分配的对象,从而造成的内存占用。 |
| Native Heap | Native(C)代码分配的内存,虚拟机和Android框架本身也会分配。 |
| Code | 主要是dex和so代码占用的内存空间。 |
| Graphics | 图像资源,一般可以通过回到桌面进行回收。 |
| Private Other | 主要是类的数据结构和索引。 |
| System | 系统共享资源,包括一些共享图片和系统字体。 |
模块内存优化工具
俗话说:工欲善其事,必先利其器。本模块主要针对Java Heap和Native Heap模块介绍一些方法与工具。
Profiler
Profiler是当前Android studio集成的性能分析工具,可以帮助我们了解APP的CPU、内存、网络等相关使用情况。具体流程可以查阅官方文档。
我们可以使用Profiler来进行简单的Java和Native内存分析。
1、利用Profiler进行Java分析.
- 在进行Profiler监听时,选择Memory一行,可以点击时间线获取当前时间的对象分布情况。如下图:
这里我们可以根据当前的类大小占用,估计其是否符合预期。太大了是否有无用的大变量没有释放。
- 有时候我们往往总是需要进行前后对比,如果人工对比比较麻烦还不够精准。这时我们可以使用MAT工具,该工具依赖Profiler生成的的hprof文件。
生成hprof文件流程如下:
在项目运行时,进入memory界面,点击下载按钮。会在左侧生成当前的Java Heap内存快照文件,点击保存即可,一般我们需要保存两份(功能运行前与功能运行后)进行对比。
对获取到的hprof文件,使用hprof-conv进行转化(指令为:hprof-conv 源文件名 目标文件名)
使用MAT打开两者文件,得到如下:
选择直方图,进行对比,这里可以清晰的看到对象的增量,从而进一步分析哪里存在问题。
2、利用Profiler进行Native分析
Profiler也可以进行简单的native内存分析,但是遗憾的是其只能看到Java层的类相关对象,同时其精度也存在疑虑。所以简单介绍和使用。使用Profiler dump Java的文件,可以直接在Android Profiler打开查看,其中包含Native Size一列。
如图显示,其右上角为当前实例,右下角为引用关系。可以根据引用关系排查如何释放该部分的内存。
DDMS (Dalvik Debug Monitor Service)
DDMS是 Android 开发环境中的Dalvik虚拟机调试监控服务。我们可以通过DDMS看到运行进程的堆信息。但是在高版本的Android Studio中已经移除了该工具,本次将会简单进行介绍。
其需要以下几个准备:
-
安装DDMS,去搜索Android Studio 3.1及其以下版本的android-sdk/tools工具。其目录下有monitor工具,打开即是。
-
一台root的手机,或者Android 6版本的虚拟机(该版本下的虚拟机默认支持userdebug模式)
- 判断是否支持的指令:adb shell getprop ro.build.type,返回userdebug模式表示可以支持。
-
配置DDMS,这样才能展示Native Heap的选项
- 在ddms.cfg的配置文件中加上一行native=true
步骤较为简单,链接手机后在操作前后分别记录其快照信息,通过对比快照前后的so变化确认具体的增长情况。(该方法还需手动计算so的方法与位移,从而发现分配内存的地方,不太方便,弃用了)
AM dumpheap
DDMS的原理是我们获取到当前进程的so信息,然后根据复杂的方式算出其对应的内存分配调用栈。相对而言还是太过于复杂和麻烦了。因此进一步我们可以使用AM dumpheap进行Native的内存分析。
预先准备:
- 一台root的手机,或者Android 6版本的虚拟机
步骤:
-
设置属性:
setprop libc.debug.malloc 1 setprop libc.debug.malloc.options backtrace setprop libc.debug.malloc.program app_process (设置启动进程,不然可能会比较卡,虚拟机可以不配置该项) stop start
-
使用am 抓取操作前后dumpheap 差异
am dumpheap -n PID /data/local/tmp/before.txtam dumpheap -n PID /data/local/tmp/after.txt
-
解析dumpheap文件
- 这里需要选择现有的python脚本工具,下载地址。(PS:注意修改脚本中的SDK相关包配置路径,objdump和addr2line地址)
- python native_heapdump_viewer.py before.txt > before-deal.txt 和 python native_heapdump_viewer.py after.txt > after-deal.txt (会依赖手机上的一些so库,可以根据提示adb pull到PC上)
-
对比前后的dumpheap文件,可以使用sublime,范例如下
Perfetto(强烈推荐)
am dumpheap的确可以帮助我们查看到native的内存消耗情况,但是其要求较高,步骤复杂。同时获取的diff文件还比较难看。
开发者文档上建议了功能更加强大的Perfetto工具。
使用该工具查看Native内存需要以下要求:
- 手机操作系统在 Android 10+
- 手机开启了系统追踪功能(华为好像不支持,我使用的三星测试)
- 使用的调试APP支持debuggable
使用步骤如下:
1、打开APP,获取到当前APP的PID
2、使用Perfetto官网,设置配置信息
主要设置PID,以及监控时间即可。
3、得到其config在命令行执行,获取其trace文件;或者直接使用该网站执行监控。
4、使用该网站打开trace文件,分析数据,其数据大致如下,可以看到具体的类方法对native内存的占用。
leakcanary
leakcanary是内存监控的工具,网上介绍的文档很多,就不详细介绍了。这个工具可以帮助我们发现内存泄露的情况,对于我们线下详细分析内存占用情况不是很有帮助。
内存消耗建议
从分析中,我们可以看到一些比较通用的内存消耗问题。
线程
线程是占用内存的大头,一个线程往往可能占用几百K的内存,最少都会占用64K。所以建议:
- 线程池统一创建,创建时不使用系统提供的Executors提供的线程池,尽量使用ThreadPoolExecutor进行创建线程池,可以更好的思考自己需要什么样的线程池。能少用核心线程就少用核心线程。
- 谨慎使用HandlerThread和AsyncTask,因为其内部都自己维护了一个线程池,包含核心线程,会一直存在占用内存。
图片bitmap
图片bitmap所分配的内存是native内存,而且其占用的内存也较大。如果不手动释放的话会一直存在。所以需要注意释放。
对于本地图片的加载过程,也是存在一定策略。
- 设置inSampleSize参数,缩小比例参数。
- 设置inDensity参数,资源目录参数。
- 设置inPreferredConfig参数,颜色模式,一般为我们都会使用ARGB_8888,但是如果不需要透明度的时候,推荐使用RGB_565,其只是少了透明通道。而前者一个像素4个字节,后者一个像素2个字节。
硬件加速
开启硬件加速时,会存在RenderNode相关线程绘制,其不受开发者控制。如果不需要使用硬件加速时可以进行关闭,可以减少几百K的内存。
动画
动画和图片bitmap一样,都会比较占用空间,所以在进行动画后,都记住要回收动画资源。
网络
目前Android的网络请求默认是集成了okHttp实现的。在开发过程中我们影响不到其内部的配置。会发现最后会在native内存残留很大一部分的okhttp相关占用。这里暂时不进行理会。
单例/引用,资源释放
单例一般是不会释放的,所以会一直占用内存,同时单例如果引用其他类时,会持有其他的类引用导致其不能释放,所以对于单例要做好以下几点:
- 该类是否有必要设置为单例。
- 类中的资源要及时释放,比如说list中的广告信息是否有clear等。
枚举
枚举会比一般的静态变量更占用内存,所以使用枚举前简单考虑下是否有必要,也不必因噎废食。当然也可以尝试使用@IntDef和@StringDef注解。
原来使用枚举而不是常量的,是因为枚举可以限制输入值,使用@IntDef和@StringDef也是一样的效果。
谨慎引入第三方库
因为第三方库对我们而言是黑盒,不受控制的。有时候其占用空间还很大,在引入第三方库的时候要做好评估。
class重排序
这里要知道,Android加载是按页加载的,一个类在A页中,会把该页中全部的类都加载到内存中,为4K。所以如果一个功能各个运行的类比较分散,会造成加载的页较多。从这个理论上来说,我们要尽可能的将一起运行的类放到一起。
这里有现成的工具,facebook提供的redex,可以根据运行时类对apk进行重排序从而减少包大小和加载内存。网上的教程也比较多,就不详细介绍了,其原理是通过am dumpheap获取的加载类对apk中的类进行重排序,使使用到的类聚居在一起,从而减少加载页。