再谈 android 内存优化

从线上环境考虑,app 面临的内存问题,往往表现为卡顿和 oom,在测试和发布过程,能通过性能测试可以发现一些内存问题,例如

  1. 内存泄露
  2. 内存暴涨
  3. oom 问题

但是实际在线上环境,一般只会出现卡顿,或者 oom,无法直接能够观察或者反馈到崩溃系统,内存出现问题了,所以实际上需要一种内存监控的机制。

测试开发阶段-内存优化策略

内存泄露检测

1.leakcanary

这里不描述开发环境如何检测内存泄露,主要探讨下,线上环境如何使用

leakcanary 源码分析

hrpof 文件裁剪

Leakcanary 检测内存泄露过程:

READING_HEAP_DUMP_FILE,
PARSING_HEAP_DUMP,
DEDUPLICATING_GC_ROOTS,
FINDING_LEAKING_REF,
FINDING_SHORTEST_PATH,
BUILDING_LEAK_TRACE,
COMPUTING_DOMINATORS,
COMPUTING_BITMAP_SIZE,
  1. 读取 hprof 文件
  2. 解析 hrpof 文件
  3. 分析 gc roots
  4. 找出泄露引用
  5. 找出泄露的最短路径
  6. 分析泄露的引用链
  7. 找出泄露的必经对象
  8. 计算泄露大小

简单说下,leakCanary 在检测到内存泄露的时候,会开始 dump 内存,代码如下:

//AndroidHeapDumper.java  该方法均运行在 LeakCanary 的线程池的子线程里 
public File dumpHeap() {
    File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

    if (heapDumpFile == RETRY_LATER) {
      return RETRY_LATER;
    }

    FutureResult waitingForToast = new FutureResult<>();
    showToast(waitingForToast);

    if (!waitingForToast.wait(5, SECONDS)) {
      CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
      return RETRY_LATER;
    }
  //省略通知栏的代码
  .....
    Toast toast = waitingForToast.get();
    try {
      //这里是dump 的操作,使用 Debug.dumpHprofdData()接口,默认文件夹在 /sd卡/Download/包名下
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      cancelToast(toast);
      notificationManager.cancel(notificationId);
      return heapDumpFile;
    } catch (Exception e) {
      CanaryLog.d(e, "Could not dump heap");
      return RETRY_LATER;
    }
  }

实际上, hprof 文件,特别大(随便可到七八十M,甚至一两百M ),需要裁剪,裁剪方案如下:

  1. 裁剪基本类型数组
  2. 重复对象,只检测一次

2.使用 mat 分析内存泄露

首先要获取一个 hprof 文件,随便一个么有混淆的包(最好没有混淆,不然混淆之后,排查起来更加麻烦)

Hprof 是 jvm 里面的一个性能调优工具,用于发现内存和CPU的性能问题,官方文档如下

step 1

命令,adb shell am 开启 dump java 内存,然后执行你的操作

adb shell am dumpheap com.yy.hiyo /data/local/tmp/hago_channel_list1.hprof
step 2

命令,adb pull ,从手机拉取 hprof 文件到电脑

adb pull /data/local/tmp/hago_channel_list1.hprof ~/Documents
step 3

进入到 android sdk 目录下,在 android/sdk/platforms-tools 下,有个 hprof-conv.exe 可执行文件,cd 命令行进入到该文件夹,然后执行命令,转化为标注的 hprof 文件:

prof-conv target.hrpof des.hprof,例如:

hprof-conv F:\profile\1.hprof F:\profile\des.hprof
step 4

使用 mat 分析对应对象的引用路径修复问题

参考资料:

https://developer.android.google.cn/topic/performance/memory.html

线上检测 oom

我们可以设想的就是,设置一个内存阈值,在发

OOM问题

常见的 oom 问题一般分为

  1. java 内存溢出
  2. 无连续可用空间
  3. FD 数量超出限制
  4. 线程数量超出限制
  5. 虚拟内存不足

关于每种 OOM 这里选取简单的例子进行说明

创建线程 oom
java.lang.OutOfMemoryError: pthread_create (KB stack) failed: Try again
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:[num])
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:[num])
at java.util.concurrent.ThreadPoolExecutor.ensurePrestart(ThreadPoolExecutor.java:[num])
申请java 堆内存 oom

报错信息如下:

Caused by: java.lang.OutOfMemoryError: Failed to allocate a 640012 byte allocation with 489766 free bytes and 478KB until OOM

常见堆栈如下:

这里的意思是需要 640012 内存空间,但是只有489766 剩余。

堆内存分配失败,这里也会分为两种类型

  1. 为对象分配内存时达到进程的内存上限。由Runtime.getRuntime.MaxMemory()可以得到Android中每个进程被系统分配的内存上限,当进程占用内存达到这个上限时就会发生OOM,这也是Android中最常见的OOM类型。
  2. 没有足够大小的连续地址空间。这种情况一般是进程中存在大量的内存碎片导致的,其堆栈信息会比第一种OOM堆栈多出一段信息:failed due to fragmentation (required continguous free “<< required_bytes << “ bytes for a new buffer where largest contiguous free ” << largest_continuous_free_pages << “ bytes)”; 其详细代码在art/runtime/gc/allocator/rosalloc.cc中,这里不作详述
图片使用不当导致 OOM
Caused by: java.lang.OutOfMemoryError: Failed to allocate a 2332812 byte allocation with 1717794 free bytes and 1677KB until OOM
    at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
    at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
    at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:837)
    at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:656)
    at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:1037)
    at android.content.res.Resources.loadDrawableForCookie(Resources.java:4056)
    at android.content.res.Resources.loadDrawable(Resources.java:3929)
    at android.content.res.Resources.loadDrawable(Resources.java:3779)
    at android.content.res.TypedArray.getDrawable(TypedArray.java:776)
    at android.widget.ImageView.(ImageView.java:151)
    at android.widget.ImageView.(ImageView.java:140)
    at android.widget.ImageView.(ImageView.java:136)
    at com.yy.base.memoryrecycle.views.YYImageView.(YYImageView.java:0)
    at com.yy.base.image.RecycleImageView.(RecycleImageView.java:0)
    ... 27 more

图片 OOM 的问题,也是比较复杂的,可能不是单单一个图片的问题,但是按照实际的开发经验来说,一般这个图片肯定消耗了不少内存,例如我上面发的这个堆栈,就是,一个很小的 ImageView 上加载了一个分辨率挺大的图片,导致内存溢出,而且这个控件是悬浮控件,是常驻的,导致内存更难释放,所以后面专门改了下这个图片的分辨率。

StringBuilder OOM

堆栈如下:

java.lang.OutOfMemoryError
    at java.lang.AbstractStringBuilder.(AbstractStringBuilder.java:82)
    at java.lang.StringBuilder.(StringBuilder.java:67)
    at com.unity3d.services.core.webview.WebViewApp.invokeJavascriptMethod(WebViewApp.java:90)
    at com.unity3d.services.core.webview.WebViewApp.sendEvent(WebViewApp.java:118)
    at com.unity3d.services.core.api.Request$2.onComplete(Request.java:94)

原因:StringBuilder 之后,会去申请内存空间,其构造函数,会申请 char 数组,所以我们也可以避免去使用太长的字符串。

内存监控

一般性能监控会监控内存,在app 使用达到内存阈值的时候demp heap 出来;那么这里引发几个思考

  1. 如何确定这个阈值
  2. 如何获取当前使用内存数目
  3. native 层内存是否有监控手段

谈一下怎么获取当前内存信息

实际上,我们可以通过系统 api 来获取内存信息,或者通过读取一个 proc 文件,先来看下通过系统 api 获取,具体的代码在下面了,然后在做监控的过程需要关心一个点,就是该监控需要额外消耗多少资源,我们简单通过耗时看一下

//方法一:通过 Runtime 类,获取总内存,可用内存
MLog.info("memory", "Runtime get memory begin ")
val rt = Runtime.getRuntime()
MLog.info("memory", "get runtime end,app当前占用memory:${rt.totalMemory().toMB()} app可申请最大内存memory:${rt.maxMemory
() / MB}" + " free memory:${rt.freeMemory() / MB} ")

//方法二 通过ActivityManager 类,获取设备信息
MLog.info("memory", "ActivityManager get memory begin ")
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
MLog.info("memory", "ActivityManager get memory end. 设备总内存total:${memoryInfo.totalMem / MB} " +
    "设备可用内存availMem:${memoryInfo.availMem.toMB()} 内存阈值(达到阈值会开始清除后台服务)threshold:${memoryInfo.threshold.toMB()}"
    + "是否低内存:lowMemory:${memoryInfo.lowMemory} ")

//方法三 通过 Debug 类设置
MLog.info("memory", "Debug get memory begin ")
val dMemoryInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(dMemoryInfo)
MLog.info("memory", "Debug get memory end, pss:${dMemoryInfo.totalPss / 1024} dalvikPss:${dMemoryInfo.dalvikPss}" +
    " nativePss:${dMemoryInfo.nativePss / 1024} otherPss:${dMemoryInfo.otherPss / 1024} " +
    "dalvikDirty:${dMemoryInfo.dalvikSharedDirty / 1024}")

每个方法的耗时如下:

2020-01-21 14:32:36.666 13157-13470/xxx I/[MainThread]memory: Runtime get memory begin 
2020-01-21 14:32:36.666 13157-13470/xxx I/[MainThread]memory: get runtime end,total memory:93 max memory:512 free memory:10 
2020-01-21 14:32:36.666 13157-13470/xxxI/[MainThread]memory: ActivityManager get memory begin 
2020-01-21 14:32:36.667 13157-13470/xxx I/[MainThread]memory: ActivityManager get memory end. total:5734 availMem:2309 threshold:216 lowMemory:false 
2020-01-21 14:32:36.667 13157-13470/xxx I/[MainThread]memory: Debug get memory begin 
2020-01-21 14:32:36.819 13157-13470/xxx I/[MainThread]memory: Debug get memory end, pss:416 dalvikPss:86374 nativePss:58 otherPss:273 dalvikDirty:9

通过 Runtime 和 ActivityManager 获取的内存信息基本是无消耗的,但是通过 Debug 类获取的内存信息,是比较大消耗的,所以要注意了。

adb 获取内存信息

可以 adb shell dumpsys meminfo --package [packagename]查看某一进程下的内存信息,例如

adb shell dumpsys meminfo -- com.test.kotlon

这里先说下,一些概率

  • Vss:进程的全部使用内存(可能包含了只 malloc 的,而没用写入的)

  • Rss:进程在 Ram 中使用的真实内存

  • Pss :实际使用的内存,如果多个进程使用共享库,会按照比例统计

  • Uss:进程的私有内存

一般使用 pss 统计该进程消耗了多少物理内存。

例子如下:

adb_shell_dumpmemoryinfo.jpg

在网上查了一下,也可以通过 procrank 查看并且排序各个进程的内存使用情况,但是我这边小米真机貌似执行不了这个命令。

内存阈值的确定

面对不同的机型,如何去确定这个内存阈值,从而在达到阈值的时候,去 dump hprof 文件,做出一点分析,根据经验,我们会获上面的

Runtime.getRuntime().maxMemory()

根据这个返回数据,去设置一个系数,比如达到 maxMemory * 0.8 的时候,启动内存检查,当然按照实际经验,这个系数可以是根据机型下发的,效果会更好一点。

常见内存优化手段

先来说下优化点吧,无非就这几种

  1. 内存泄露,上面已经说到过很多了
  2. 内存抖动
  3. 大对象的监控和使用

bitmap 优化

在移动端,图片作为内存消耗的大户,非常值得我们关注,至于 bitmap 占用多少内存可以参考这个文章,这里简单引用一下公式:

一张图片对应 Bitmap 占用内存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);
Native 方法中,mBitmapWidth = mOriginalWidth * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize,
mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize

例如一张 360 * 640 像素的图片,放进 xhdpi 文件夹(320),手机分辨率 1080x1920 对应的 density 是 480 imSampleSize 是1,默认 ARGB_888,则该图片占用内存为:

360 * (480/320) * 640 (480/320) * 4 byte = 2073600 byte = 2025 kb = 1.977 MB

常见的 bitmap 优化手段有

  1. 防止 bitmap 加载 oom

    在图片框架层,统一去处理这个 oom 问题,在发现 oom 的时候,做两个兜底处理

    a.清除图片框架的缓存 b.尝试降低图片的 inSampleSize

  2. 图片按需加载,这里的按需是说 图片解析出的 Bitmap 大小和 ImageView 的大小

    这一点,应该是交给图片框架去做了,类似 glide 的,都有这样的功能

  3. 监控 ImageView 设置的 Bitmap 大小,可以重载 BitmapDrawable 和 ImageView,对 Bitmap 进行监控,超过阈值,则上报

  4. 其它可以参考这个文章

对象缓存

部分情况可以使用对象缓存,同时尽量不要再 for 循环创建临时对象,不要在 onDraw()等方法,频繁创建对象

字符串拼接

避免字符串拼接,大量字符串拼接会导致内存抖动

参考资料:**

微信团队原创分享:Android内存泄漏监控和优化技巧总结

内存管理概览

你可能感兴趣的:(再谈 android 内存优化)