从线上环境考虑,app 面临的内存问题,往往表现为卡顿和 oom,在测试和发布过程,能通过性能测试可以发现一些内存问题,例如
- 内存泄露
- 内存暴涨
- oom 问题
但是实际在线上环境,一般只会出现卡顿,或者 oom,无法直接能够观察或者反馈到崩溃系统,内存出现问题了,所以实际上需要一种内存监控的机制。
测试开发阶段-内存优化策略
内存泄露检测
1.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,
- 读取 hprof 文件
- 解析 hrpof 文件
- 分析 gc roots
- 找出泄露引用
- 找出泄露的最短路径
- 分析泄露的引用链
- 找出泄露的必经对象
- 计算泄露大小
简单说下,leakCanary 在检测到内存泄露的时候,会开始 dump 内存,代码如下:
//AndroidHeapDumper.java 该方法均运行在 LeakCanary 的线程池的子线程里
public File dumpHeap() {
File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
if (heapDumpFile == RETRY_LATER) {
return RETRY_LATER;
}
FutureResult<Toast> 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 ),需要裁剪,裁剪方案如下:
- 裁剪基本类型数组
- 重复对象,只检测一次
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 问题一般分为
- java 内存溢出
- 无连续可用空间
- FD 数量超出限制
- 线程数量超出限制
- 虚拟内存不足
关于每种 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 剩余。
堆内存分配失败,这里也会分为两种类型
- 为对象分配内存时达到进程的内存上限。由Runtime.getRuntime.MaxMemory()可以得到Android中每个进程被系统分配的内存上限,当进程占用内存达到这个上限时就会发生OOM,这也是Android中最常见的OOM类型。
- 没有足够大小的连续地址空间。这种情况一般是进程中存在大量的内存碎片导致的,其堆栈信息会比第一种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.<init>(ImageView.java:151)
at android.widget.ImageView.<init>(ImageView.java:140)
at android.widget.ImageView.<init>(ImageView.java:136)
at com.yy.base.memoryrecycle.views.YYImageView.<init>(YYImageView.java:0)
at com.yy.base.image.RecycleImageView.<init>(RecycleImageView.java:0)
... 27 more
图片 OOM 的问题,也是比较复杂的,可能不是单单一个图片的问题,但是按照实际的开发经验来说,一般这个图片肯定消耗了不少内存,例如我上面发的这个堆栈,就是,一个很小的 ImageView 上加载了一个分辨率挺大的图片,导致内存溢出,而且这个控件是悬浮控件,是常驻的,导致内存更难释放,所以后面专门改了下这个图片的分辨率。
StringBuilder OOM
堆栈如下:
java.lang.OutOfMemoryError
at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:82)
at java.lang.StringBuilder.<init>(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 出来;那么这里引发几个思考
- 如何确定这个阈值
- 如何获取当前使用内存数目
- 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 统计该进程消耗了多少物理内存。
例子如下:
在网上查了一下,也可以通过 procrank 查看并且排序各个进程的内存使用情况,但是我这边小米真机貌似执行不了这个命令。
内存阈值的确定
面对不同的机型,如何去确定这个内存阈值,从而在达到阈值的时候,去 dump hprof 文件,做出一点分析,根据经验,我们会获上面的
Runtime.getRuntime().maxMemory()
根据这个返回数据,去设置一个系数,比如达到 maxMemory * 0.8 的时候,启动内存检查,当然按照实际经验,这个系数可以是根据机型下发的,效果会更好一点。
常见内存优化手段
先来说下优化点吧,无非就这几种
- 内存泄露,上面已经说到过很多了
- 内存抖动
- 大对象的监控和使用
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 优化手段有
-
防止 bitmap 加载 oom
在图片框架层,统一去处理这个 oom 问题,在发现 oom 的时候,做两个兜底处理
a.清除图片框架的缓存 b.尝试降低图片的 inSampleSize
-
图片按需加载,这里的按需是说 图片解析出的 Bitmap 大小和 ImageView 的大小
这一点,应该是交给图片框架去做了,类似 glide 的,都有这样的功能
监控 ImageView 设置的 Bitmap 大小,可以重载 BitmapDrawable 和 ImageView,对 Bitmap 进行监控,超过阈值,则上报
其它可以参考这个文章
对象缓存
部分情况可以使用对象缓存,同时尽量不要再 for 循环创建临时对象,不要在 onDraw()等方法,频繁创建对象
字符串拼接
避免字符串拼接,大量字符串拼接会导致内存抖动
参考资料:**