Android 性能篇 - 内存优化二(精华篇)

本篇文章已授权微信公众号 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 指令,可以查询内存的划分:
procrank
  • VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
  • RSS- Resident Set Size 实际使用物理内存(包含所有共享库占用的内存)
  • PSS- Proportional Set Size 实际使用的物理内存(按比例分配共享库占用的内存)
  • USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

那么最值得关注的是 PSSUSS,我们可以用 dumpsys meminfo 来查询(无需 root 权限)

2、dumpsys meminfo 查询 pss 划分
dumpsys meminfo

重点字段解读:

  • 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 heapdirty+ clean) + art heapdirty + clean

通过上面图片可得 launcher app 占用的内存是 250M,大部分内存在 Native Heapcodegraphics,那如何分析和解决,我们下面讲。

3、android studio profile 是 ide 提供出来的分类:
android studio profile
  • Total - 整个应用占据的总内存
  • Java - java 堆占据的内存
  • Native - Native 层调用 malloc/newC/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 内存泄漏的出现:

泄漏检查

通过上图可以知道 SearchActivityHistorySource.mContext 持有,HistorySource 是一个单例,然后最顶层的 Thread.contextClassLoader 就是 GC root(注意:静态变量不是 GC root),Thread.contextClassLoaderPathClassLoader 类,只要把 SearchActivitycontext 换成 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 是分析 Hprofjava 库)

小结:

那么LeakCanary 只能解决界面上的泄漏,其他内存上的优化是做不到的,譬如:线程池的泄漏,内存的抖动,大对象的滥用.. 那么就需要更为强大的工具 MAT

2、内存检测工具 MAT

MAT 是分析内存文件 hprof 的工具。(MAT 工具地址)

2.1 、抓取步骤

跑几分钟 monkey 后,退回应用主界面,手动多次点击GC 按钮,把可回收的回收掉,为了剔除脏数据。通过 Android StudioProfile 把 内存文件 hprofdump 下来。

抓取步骤
  • 进入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 、分析内存:

完成以上步骤之后的结果图

hprof
  • 直接点击左上角 Histogram 查看内存分布
  • objects - 对象数目
  • shallow heap - 对象自身实际占用的堆大小
  • retained heap - 对象被回收后能释放多少内存
  • Inspector - 可以看到对象的 GC Root 是谁
1566982559(1).png
  • with outgoing references - 表示的是 当前对象,引用了内部哪些成员对象
  • with incoming references - 表示的是 当前对象,被外部哪些对象应用了(重点操作)
1566988199(1).png
  • merge shortest paths to gc roots - 从GC roots 到一个或一组对象的公共路径
  • exclude all phantom/weak/soft etc. references - 排除一些类型的引用(如软引用、弱引用、虚引用),留下强引用
1566988599(1).jpg
  • 为了避免查看太多并不是强相关的对象,直接从本应用的java 类入手,MAT 也提供正则式过滤,直接 输入.*com.vd.*(本应用 ``````packageName```) 去过滤,结果就非常明显,整个应用自己写的对象占用的内存都在这里。从大的对象下手,是否这个对象有存在的意义,是否需要占这么大的一个内存。是否可以对其做相应的处理。
1566989713(1).jpg
  • 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 是一个可以跑在AdnroidC 库 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"'

1567132646(1).jpg
  • 关掉自身应用,再打开,monkey 跑起来
  • 通过adb shell dumpsys meminfo com.all.videodownloader.videodownload 查到 pid 为 2968
image.png
  • 通过adb shell am dumpheap -n /data/local/tmp/heap.txt 把文件抓取出来到 /data/local/tmp/heap.txt
image.png
  • native 内存文件 拷贝出来,等下分析
image.png
2、使用 python 分析
2.1、搭建环境
  • 下载 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):函数中的部分代码,把下面代码:

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

    image.png

  • def main(): 函数插入部分代码在函数第一行插入和最后一行插入以下代码,目的是直接把结果 log 输出到 test.txt 可以直接查看。

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

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


    image.png
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-> BA方法调用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 这个函数。那么上面这个占用了 10MallocateHeapBitmap,究竟是 java 层哪个类调用下来的,这个目前是无解(包括最近华为的方舟环境平台 DevEco 也不行),只能在 java 层去全盘查询了,哪些图片使用了较多的内存。

3.3、内存信息分析二
image.png
  • WebViewGoogle.apk占用了 10M 的内存,WebViewGoogle.apk就是应用使用的WebViewandroid 5.0 之前作为模块存在于 frameworks/base目录下,并提供接口。android 5.0 之后变成了编译为一个独立的apk ,包名是 com.android.webview。检查所有的WebView 使用情况,譬如:如果场景允许,使用完毕是否有 调用 WebView.clearCache()
  • boot-framework.oat 占了5MAndroid 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 传递大对象,或者 ArrayListIntent 的上限是 505K
    解决方案:
  1. 一般通过 static 持有需要传递的对象解决。
  2. 把跳转的页面写成 fragment ,数据可以不需要传递也可获取
  3. 通过EventBus RxBus(原理都是通过全局单例来传递)
  4. 通过 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() 会在 循环内生成 100shake 局部变量 + 100 个局部变量的引用,
memoryShake1()会在 循环内生成 100shake 局部变量 + 1 个局部变量的引用,一个对象引用在 64bit 的环境是 8byte100*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 的内存占用比例都是比较小,可不必理会。

你可能感兴趣的:(Android 性能篇 - 内存优化二(精华篇))