Android开发高性能之 (内存优化)


在这里插入图片描述
内存问题: 异常(OOM 内存分配失败等), 卡顿
改进方向两方面的工作:

优化RAM

即降低运行时内存。防止程序发生OOM异常,以及降低程序由于内存过大被LMK机制杀死的概率。

优化ROM

即降低程序占ROM的体积。这里主要是为了降低程序占用的空间,防止由于ROM空间不足导致程序无法安装。

移动设备发展

设备性能分级:
移动设备发展Facebook 有一个叫 device-year-class 的开源库。

2008 140MB RAM ,2018 Mate 20 Pro 8GB RAM。
在这里插入图片描述

误区: Native 内存不用管

Native 内存多了,物理内存不足,lmk 开始杀进程。
eg: bitmap 手动分配到 Native heap 并释放。

// 步骤一:申请一张空的 Native Bitmap
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);

// 步骤二:申请一张普通的 Java Bitmap
Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);

// 步骤三:使用 Java Bitmap 将内容绘制到 Native Bitmap 中
mNativeCanvas.setBitmap(nativeBitmap);
mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);

// 步骤四:释放 Java Bitmap 内存
srcBitmap.recycle();
srcBitmap = null;

内存泄露的检测与修改

内存泄露

一. 内存泄露的监控方案

方法一: leakcanry

它通过弱引用方式侦查Activity或对象的生命周期,若发现内存泄露自动dump Hprof文件,通过HAHA库得到泄露的最短路径,最后通过notification展示。

  • dumphprof依然会造成应用明显的卡顿(SuspendAll Thread)。

通过对leakcanry做简单的定制,我们就可以实现以下一个内存泄露监控闭环。
在这里插入图片描述

方法二:DDMS

AndroidStudio 3.0中之后无法打开DDMS Google 放弃DDMS

  • 在SDK的 android-sdk/tools/ 路径下【就是和配置ADB命令一样的路径】有个monitor.bat 的批处理文件;
    在这里插入图片描述

1、点击update heap 在你需要调试的process

2、 右边点击道heap的使用的情况,点击cause gc, 可以实时观看内存的情况

方法三:命令行

adb shell dumpsys meminfo <package_name|pid> [-d]

adb shell ps 找到对应的 pid or 包名
在这里插入图片描述

方法四:Allocation Tracker

  • 信息过于分散
  • 跟 Traceview 一样,无法做到自动化分析,每次手动 开始 、结束。
  • Allocation Tracking 时,不会对手机本身造成过多性能影响,但是停止时直到 数据 dump 出来之前,经常手机卡死甚至 ANR .

方法五:android studio Profiler

二. 对系统内存泄露的Hack Fix

AndroidExcludedRefs列出了一些由于系统原因导致引用无法释放的例子,同时对于大多数的例子,都会提供建议如何通过hack的建议去修复。在微信中,,,

三. 通过兜底回收内存

Activity泄漏会导致该Activity引用到的Bitmap、DrawingCache等无法释放,对内存造成大的压力,兜底回收是指对于已泄漏Activity,尝试回收其持有的资源,泄漏的仅仅是一个Activity空壳,从而降低对内存的压力。

做法也非常简单,在Activity onDestory时候从view的rootview开始,递归释放所有子view涉及的图片,背景,DrawingCache,监听器等等资源,让Activity成为一个不占资源的空壳,泄露了也不会导致图片资源被持有。

  Drawable d = iv.getDrawable();
  if (d != null) {
      d.setCallback(null);
  }        
  iv.setImageDrawable(null);

总的来说,我们不是只懂得一些内存泄露解决方法就可以,更重要的是通过日常测试与监控,得到内存泄露检测与修改的一整套闭环体系。

降低运行时内存的一些方法

当我们能确保应用中不会出现内存泄露时,我们需要一些其他的方法来降低运行时的内存。更多的时候,我们其实只希望降低应用发生OOM的概率。

一. 减少bitmap占用的内存

见下一章

二. 自身内存监控

对于系统函数 onLowMemory 等函数是针对整个系统而已的,对于本进程来说,其dalvik内存距离OOM的差值并没有体现,也没有回调函数供我们及时释放内存。

  • 实时监控进程的堆内存使用率,达到设定值即关于通知相关模块进行内存释放:
Runtime.getRuntime().maxMemory();  
  Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
  • 操作方式
    定期(前台每隔3分钟)去得到这个值,当我们这个值达到危险值时(例如80%),我们应当主要去释放我们的各种cache 资源(bitmap的cache为大头).
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE)

三. 有节操的使用开进程

每个空进程也占用10MB 。
对于webview,图库等,由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程。微信当前也会把它们放在单独的tools进程中 。

四. 上报OOM详细信息

当系统发生OOM的crash时 ,内存信息。large heap、 inBitmap、SparseArray。。。

GC优化

测试GC性能

例如暂停挂起时间、总耗时、GC 吞吐量,我们可以通过发送SIGQUIT 信号获得 ANR 日志。

adb shell kill -S QUIT PID
adb pull /data/anr/traces.txt

它包含一些 ANR 转储信息以及 GC 的详细性能信息:
sticky concurrent mark sweep paused:	Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms     // GC 暂停时间

Total time spent in GC: 502.251ms     // GC 总耗时
Mean GC size throughput: 92MB/s       // GC 吞吐量
Mean GC object throughput: 1.54702e+06 objects/s 

一. GC的类型

1.GC_FOR_ALLOC
当堆内存不够的时候容易被触发,尤其是new一个对象的时候。可以提高dalvik.vm.heapstartsize的值,这样在启动过程中可以减少GC_FOR_ALLOC的次数。注意这个触发是以同步的方式进行的。

2.GC_EXPLICIT
这个gc是被可以调用的,比如system.gc, 一般gc线程的优先级比较低,所以这个垃圾回收的过程不一定会马上触发。

3.GC_CONCURRENT
当分配的对象大小超过384K时触发,注意这是以异步的方式进行回收的.如果发现大量反复的Concurrent GC出现,说明系统中可能一直有大于384K的对象被分配,而这些往往是一些临时对象,被反复触发了。给到我们的暗示是:对象的复用不够。

4.GC_EXTERNAL_ALLOC (在3.0系统之后被废了)

二. 内存抖动现象

短时间内大量的对象被创建又马上被释放。
这个操作有可能会影响到帧率,并使得用户感知到性能问题。

三. GC优化

1.字符串拼接优化
减少字符串使用加号拼接,改为使用StringBuilder。
减少StringBuilder.enlarge,初始化时设置capacity;

logging.println(">>>>> Dispatching to " + msg.target + " " +
                msg.callback + ": " + msg.what);

2.读文件优化 读文件使用ByteArrayPool,初始设置capacity,减少expand.

3.资源重用
建立全球缓存池,对频繁申请、释放的对象类型重用

4.减少不必要或不合理的对象
例如在ondraw、getview中应减少对象申请,尽量重用。更多是一些逻辑上的东西,例如循环中不断申请局部变量等

5.选用合理的数据格式 使用 SparseArray, SparseBooleanArray, and LongSparseArray来代替Hashmap

定好优化目标:
比如针对 512MB 的设备和针对 2GB 以上的设备,完全是两种不同的优化思路。
面向东南亚、非洲用户,那对内存优化的标准就要变得更苛刻一些。

内存优化 3 方面

设备分级、Bitmap 优化和内存泄漏这三个方面入手。

1、设备分级

类似 device-year-class ,低端机关闭复杂动画,或者某些功能,使用 565 格式图片,更小缓存等。

if (year >= 2013) {
    // Do advanced animation
} else if (year >= 2010) {
    // Do simple animation
} else {
    // Phone too slow, don't do any animations
}
  • 缓存管理
    统一缓存。当 系统内存不足 时,及时释放内存。
  • 进程模型
    一个 空进程也会占用 10MB 内存。有节操的增加进程。
  • 安装包大小
    代码、资源、图片以及 so 库的体积,跟它们占用的内存有很大的关系。

2、Bitmap 优化

Android bitmap 演进分析:

  • Android 2.x系统,当dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值时候就会发生OOM。其中bitmap是放于external中 。
    Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。如果不手动调用 recycle,Bitmap Native 内存的回收完全依赖 finalize 函数回调,熟悉 Java 的同学应该知道,这个时机不太可控。
    Android 2.x 系统 BitmapFactory.Options 里面隐藏的的inNativeAlloc反射打开后,申请的bitmap就不会算在external中。

  • Android 3.0~Android 7.0 将 Bitmap 对象和像素数据统一放到 Java 堆中。
    这样就算我们不调用 recycle,Bitmap 内存也会随着对象一起被回收。
    Java 堆限制也才到 512MB,可能我的物理内存还有 5GB,但是应用还是会因为 Java 堆内存不足导致 OOM.
    Android 4.x系统,废除了external的计数器,类似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= dalvik heap 最大值的时候就会发生OOM(art运行环境的统计规则还是和dalvik保持一致)
    可采用facebook的fresco库,即可把图片资源放于native中。

  • android 8.x 从Java heap 移到了native heap
    有没有一种实现,可以将 Bitmap 内存放到 Native中,也可以做到和对象一起快速释放,同时 GC 的时候也能考虑这些内存防止被滥用?NativeAllocationRegistry 可以一次满足你这三个要求,Android 8.0 正是使用这个辅助回 Native 内存的机制.
    Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率。

具体方法:

1.统一图片库

eg:低端机使用 565 格式。可以使用 Glide、Fresco 或者采取自研都可以。而且需要进一步将所 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢。

2.统一监控

在统一图片库后就非常容易监控 Bitmap 的使用情况了,这里主要有三点需要注意。

  • 大图片监控。 防止超宽。像素浪费
    即图片的大小不应该超过view的大小。对此,我们可以重载drawable与ImageView.
    图片存在像素浪费,合理利用.9图
  • 重复图片监控。
    作者回复:这个重复bitmap分析是在服务器后台做的,目前是对所有 bitmap 数组直接计算hash的方法匹对。
  • 图片总内存。
    在 OOM 崩溃的时候,也可以把图片占用的总内存、Top N 图片的内存都写到崩溃日志中,帮助我们排查问题。

一个好的imageLoader,可以将2.X、4.X或5.X对图片加载的处理对使用者隐藏,同时也可以将自适应大小、质量等放于框架中。

3、内存泄漏

内存泄漏简单来说就是没有回收不再使用的内存。

内存泄漏主要分两种:
同一个对象泄漏。
每次都会泄漏新的对象,可能会出现几百上千个无用的对象。

优秀的框架设计可以减少甚至避免程序员犯错。很多内存泄漏都是框架设计不合理所导致,各种各样的单例满天飞,MVC 中 Controller 的生命周期远远大于 View。

  • Java 内存泄漏
    建立类似 LeakCanary 自动化监测方案。
    在开发过程,我们希望出现泄漏时可以弹出对话框,让开发者更加容易去发现和解决问题。
  • OOM 监控
    美团有一个 Android 内存泄露自动化链路分析组件 Probe ,它在发生 OOM 的时候生成 Hprof 内存快照,然后通过单独进程对这个文件做进一步的分析。不过在线上使用这个文件做进一步的分析。不过在线上使用这个工具风险还是比较大,在崩溃的时候生成内存快照有可能会导致二次崩溃,而且部分手机生成 Hprof 快照可能会耗时几分钟,这对用户造成的体验影响比较大。
  • Native 内存泄漏监控
    上一期我讲到 Malloc 调试(Malloc Debug)和 Malloc 钩子(Malloc Hook)似乎还不是那么稳定。在 WeMobileDev 最近的一篇文章《微信 Android 终端内存优化实践》中,微信也做了一些其他方案上面的尝试。https://mp.weixin.qq.com/s/KtGfi5th-4YHOZsEmTOsjg?
    目前还不那么完善。

开发过程中内存泄漏排查可以使用 Androd Profiler 和 MAT 工具配合使用,而日常监控关键是成体系化,做到及时发现问题。

内存监控
内存泄漏的监控存在一些性能的问题,一般只会对内部人员和极少部分用户开启。
线上:需要通过其他更有效的方式去监控内存相关的问题。

采集方式
要按照用户抽样,而不是按次抽样。持续采集命中用户。
用户在前台的时候,可以每 5 分钟采集一次 PSS、Java 堆、图片总内存。

计算指标

内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
其中 PSS 的值可以通过 Debug.MemoryInfo 拿到 其中 PSS 的值可以通过 Debug.MemoryInfo 拿到。
触顶率:可以反映 Java 内存的使用情况,如果超过 85% 最大堆限制,GC 会变得更加频繁,容易造成 OOM 和卡顿。

内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV

long javaMax = runtime.maxMemory();
long javaTotal = runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
// Java 内存使用超过最大限制的 85%
float proportion = (float) javaUsed / javaMax;

一般客户端只上报数据,所有计算都在后台处理,这样可以做到灵活多变。

3.GC监控
在实验室或者内部试用环境,我们也可以通过 Debug.startAllocCounting 来监控 Java 内存分配和 GC 的情况,需要注意的是这个选项对性能有一定的影响,虽然目前还可以使用,但已经被 Android 标记为 deprecated。通过监控,我们可以拿到内存分配的次数和大小,以及 GC 发起次数等信息。

long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();

在 Android 6.0 之后系统可以拿到更加精准的 GC信息。

// 运行的 GC 次数
Debug.getRuntimeStat("art.gc.gc-count");
// GC 使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式 GC 的次数
Debug.getRuntimeStat("art.gc.blocking-gc-count");
// 阻塞式 GC 的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");

需要特别注意阻塞式 GC 的次数和耗时,因为它会暂停应用线程,可能导致应用发生卡顿。

发布了13 篇原创文章 · 获赞 2 · 访问量 7383

猜你喜欢

转载自blog.csdn.net/qq_37165429/article/details/104820032