目录
4.2.3 ListView/GridView的ConvertView复用
一、为什么要进行内存优化
1. 系统分配的内存是有限的(沙盒机制),运行内存限制,OOM导致APP崩溃
2. 高内存占用的应用会被kill
3. APP性能:流畅性、响应速度和用户体验。
二、如何进行内存优化
- 开源
- 使用多进程
- 对于webview,图库等,由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程。微信当前也会把它们放在单独的tools进程中
- 使用多进程
- 节流
- 数据结构优化
- StringBuilder
- 使用更加轻量的数据结构
- 避免在Android里面使用Enum
- 谨慎使用“抽象”编程
- 谨慎使用依赖注入框架
- 对象复用
- 复用系统中自带的资源
- ListView/GridView的ConvertView复用
- 避免在onDraw方法里面执行对象的创建(因为onDraw在界面,图像或者View一有变化的化就会重新调用,如果在里面执行对象的创建的话,就会影响绘制的时间)
- 避免内存泄漏
- Activity 泄露检测
- Native 内存泄漏检测
- 考虑使用Application Context而不是Activity Context
- 谨慎使用static对象
- 注意监听器的注销
- 注意缓存容器中的对象泄漏
- 注意WebView的泄漏
- 注意Cursor对象是否及时关闭
- 兜底回收内存
- Bitmap
- 加载Bitmap:缩放比例、解码格式、局部加载
- 使用更小的图片:无alfa通道的图片用jpk降低apk大小
- 注意临时Bitmap对象的及时回收
- 避免Bitmap的浪费
- Bitmap 分配及回收追踪
- 资源文件
- 资源文件需要选择合适的文件夹进行存放
- 优化布局层次,减少内存消耗
- 使用ProGuard来剔除不需要的代码
- Try catch某些大内存的分配的操作
- 兜底保护
- 数据结构优化
- - 其他
- 避免抖动等
- onTrimMemory中需要根据传递的参数类型进行判断,合理的选择释放自身的一些内 存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用
三、开源式方案
3.1 使用多进程
对于webview,图库等,由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程。微信当前也会把它们放在单独的tools进程中
四、节流式方案
4.1 数据结构优化
4.1.1 使用更加轻量的数据结构
例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构。
HashMap相比起Android专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。
通常的HashMap 的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效,在于他们避免了对key与 value的自动装箱(autoboxing),并且避免了装箱后的解箱。
4.1.2 避免在Android里面使用Enum
Android官方培训课程提到过“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具体原理请参考[《Android性能优化典范(三)》](https://www.csdn.net/article/2015-08-12/2825447-android-performance-patterns-season-3),所以请避免在Android里面使用到枚举。
4.1.3 谨慎使用“抽象”编程
很多时候,开发者会使用抽象类作为”好的编程实践”,因为抽象能够提升代码的灵活性与可维护性。然而,抽象会导致一个显著的额外内存开销:他们需要同等量的代码用于可执行,那些代码会被mapping到内存中,因此如果你的抽象没有显著的提升效率,应该尽量避免他们。
4.2 对象复用
大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码时显式地在程序里创建对象池,然后处理好复用的实现逻辑。要么就是利用系统框架既有的某些复用特性,减少对象的重复创建,从而降低内存的分配与回收
4.2.1 StringBuilder
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”
4.2.2 复用系统自带的资源
Android系统本身内置了很多的资源,比如字符串、颜色、图片、动画、样式以及简单布局等,这些资源都可以在 应用程序中直接引用。
这样做不仅能减少应用程序的自身负重,减小APK的大小,还可以在一定程度上减少内存的开销,复用性更好。
但是也有必要留意 Android系统的版本差异性,对那些不同系统版本上表现存在很大差异、不符合需求的情况,还是需要应用程序自身内置进去
4.2.3 ListView/GridView的ConvertView复用
注意在ListView/GridView等出现大量重复子组件的视图里对ConvertView的复用
4.2.4 避免在onDraw方法里面执行对象的创建
(因为onDraw在界面,图像或者View一有变化的化就会重新调用,如果在里面执行对象的创建的话,就会影响绘制的时间)
4.3 Bitmap
4.3.1 加载Bitmap:缩放比例、解码格式、局部加载
Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用可谓是重中之重,通常来说有以下2个措施:
1. inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
2. decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
4.3.2 使用更小的图片
在涉及给到资源图片时,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用更小的图片。
尽量使用更小的图片不 仅可以减少内存的使用,还能避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图时会 因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM
4.3.3 Bitmap对象的复用inBitmap高级属性
利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率(注:3.0以及4.4以后存在一些使用限制上的差异)。
使 用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张Bitmap在Heap中所 占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放Bitmap。
利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数 量的内存大小
4.3.4 注意临时Bitmap对象的及时回收
虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回 收的。例如临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原 始bitmap所占用的空间。
4.3.5 Bitmap 分配及回收追踪监控
Bitmap 一直以来都是 Android App 的内存消耗大户,很多 Java 甚至 native 内存问题的背后都是不当持有了大量大小很大的 Bitmap。
与此同时,Bitmap 有几个特点方便我们对它们进行监控:
- 创建场景较为单一。 Bitmap 通常通过在 Java 层调用 Bitmap.create 直接创建,或者通过 BitmapFactory 从文件或网络流解码。正好,我们有一层对 Bitmap 创建接口调用的封装,基本囊括微信内创建 Bitmap 的全部场景(包括调用外部库产生 Bitmap 也封装在这层接口内)。这层统一接口有利于我们在创建 Bitmap 时进行统一监控,而不需要进行插桩或 hook 等较为 hack 的方法。
- 创建频率较低。 Bitmap 创建的行为不如 malloc 等通用内存分配频繁,本身往往也伴随着耗时较长的解码或处理,因此在创建 Bitmap 时加入监控逻辑,其性能要求不会特别高。即使是获取完整的 Java 堆栈甚至做一些筛选,其耗时相比起解码或者其他图像处理也是微不足道,我们可以执行稍微复杂的逻辑。
- Java 对象的生命周期。 Bitmap 对象的生命周期和普通 Java 对象一样服从 JVM 的 GC,因此我们可以通过 WeakReference 等手段来跟踪 Bitmap 的销毁,而不用像创建一样对销毁也一并跟踪。
针对上述特点,我们加入了一个针对 Bitmap 的高性价比监控:
**在接口层中将所有被创建出来的 Bitmap 加入一个 WeakHashMap,同时记录创建 Bitmap 的时间、堆栈等信息,然后在适当的时候查看这个 WeakHashMap 看看哪些 Bitmap 仍然存活来判断是否出现 Bitmap 滥用或泄漏。**
4.4 资源文件
4.4.1 资源文件需要选择合适的文件夹进行存放
我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经 过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到 200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下
4.4.2 优化布局层次,减少内存消耗
越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。
1. 如果布局既可以通过RelativeLayout又可以通过LinearLayout实现的话,选择通过LinearLayout实现,因为RelativeLayout的功能相对于LinearLayout来说更加复杂,布局过程需要更多的CPU开销;如果某些布局能够通过RelativeLayout来实现,也可以通过嵌套LinearLayout来实现,这时候应该选择RelativeLayout来实现,因为嵌套相当于增加了布局的层级,带来的性能损失比布局RelativeLayout更大;
2. 使用<include>和<merge>标签来进行布局重用,减少布局的层级
3. 使用<ViewStub>标签,这个标签能够做到动态加载我们的布局,不同于我们通过GONE的方式来设置某个View是否可见,ViewStub是View的一种,但是它没有大小,没有绘制功能,也不参与布局,资源消耗非常低,这点是不同于我们通过GONE方式设置View不可见的,因为通过GONE方式设置View不可见了说到底还是会绘制View的,只不过你看不到而已
4.5 避免内存泄漏
4.5.1 兜底回收内存
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);
```
4.6 兜底保护
在用户无感知的情况下,在接近触发系统异常前,选择合适的场景杀死进程并将其重启,使得应用的内存占用回到正常情况,这不为是一种好的兜底方式。
- 微信是否在主界面退到后台 且 位于后台的时间超过 30 分钟
- 当前时间为凌晨 2~5 点
- 不存在前台服务(存在通知栏,音乐播放栏等情况)
- java heap 必须大于当前进程最大可分配的 85% || native 内存大于 800M || vmsize 超过了 4G(微信 32bit)的 85%
- 非大量的流量消耗(每分钟不超过 1M) && 进程无大量 CPU 调度情况
在满足以上几种条件下,杀死当前微信主进程并通过 push 进程重新拉起及初始化,来进行兜底保护。在用户角度,当用户将微信切回前台时,不会看到初始化界面,还是位于主界面中,所以也不会感到突兀
五、 其他方案
5.1 避免内存抖动
Memory Churn内存抖动,内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。
即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
最常见产生内存抖动的例子就是在 ListView 的 getView 方法中未复用 convertView 导致 View 的频繁创建和释放,针对这个问题的处理方式那当然就是复用 convertView;
或者是 String 拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的 log 的时候);
如果是其他的问题,就需要通过 Memory Monitor 去观察内存的实时分配释放情况,找到内存抖动的地方修复它,或者如果当出现下面这种情况下的 OOM 时,也是由于内存碎片导致无法分配内存:
5.2 onTrimMemory(int):
- Android 系统从4.0开始还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调
- 同时在这个回调里面 会传递以下的参数,代表不同的内存使用情况,收到onTrimMemory()回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内 存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用
六、工具
- [Android内存申请分析](https://mp.weixin.qq.com/s/b_lFfL1mDrNVKj_VAcA2ZA)
- [Android 性能优化 - 彻底解决内存泄漏](https://blog.csdn.net/wanghao200906/article/details/79305126)
- [Android性能优化篇之内存优化--内存优化分析工具](https://www.jianshu.com/p/b6684e5880d9)
- [Android Monitor工具详解大全](https://mp.weixin.qq.com/s/kcbEto2ljhhCSNknIWtbzA)
- [Android-App性能优化](https://mp.weixin.qq.com/s/M9_ggbxxGuvQj8inDSNTJw)
# 参考文献
- [内存优化,避免OOM总结](http://www.csdn.net/article/2015-09-18/2825737/3)
- [微信 Android 终端内存优化实践](https://mp.weixin.qq.com/s/KtGfi5th-4YHOZsEmTOsjg)
- [Android内存优化杂谈](https://mp.weixin.qq.com/s/Z7oMv0IgKWNkhLon_hFakg)
- [Android-APP内存优化](https://blog.csdn.net/xjh_shin/article/details/79842728)
- [Android内存优化建议](https://blog.csdn.net/hzw19920329/article/details/52318422)
- [Android 性能优化之内存泄漏检测以及内存优化(上)](https://blog.csdn.net/self_study/article/details/61919483)
- [Android 性能优化之内存泄漏检测以及内存优化(中)](https://blog.csdn.net/self_study/article/details/66969064)
- [Android 性能优化之内存泄漏检测以及内存优化(下)](https://blog.csdn.net/self_study/article/details/68946441)
- [《Android性能优化典范(三)》](https://www.csdn.net/article/2015-08-12/2825447-android-performance-patterns-season-3)