首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Android 内存优化实践与总结(2)

Android 内存优化实践与总结(2)

二、Android 常见内存问题和对应检测,解决方式1. 内存泄露不止 Android 程序员,内存泄露应该是大部分程序员都遇到过的问题,可以说大部分的内存问题都是内存泄露导致的,Android 里也有一些很常见的内存泄露问题[6],这里简单罗列下:
  • 单例(主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放)
  • 静态变量(同样也是因为生命周期比较长)
  • Handler 内存泄露[7]
  • 匿名内部类(匿名内部类会引用外部类,导致无法释放,比如各种回调)
  • 资源使用完未关闭(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)
对 Android 内存泄露业界已经有很多优秀的组件其中 LeakCanary 最为知名(Square 出品,Square 可谓 Android 开源界中的业界良心,开源的项目包括 okhttp, retrofit,otto, picasso, Android 开发大神 Jake Wharton 就在 Square),其原理是监控每个 activity,在 activity ondestory 后,在后台线程检测引用,然后过一段时间进行 gc,gc 后如果引用还在,那么 dump 出内存堆栈,并解析进行可视化显示。使用 LeakCanary 可以快速地检测出 Android 中的内存泄露。
正常情况下,解决大部分内存泄露问题后,App 稳定性应该会有很大提升,但是有时候 App 本身就是有一些比较耗内存的功能,比如直播,视频播放,音乐播放,那么我们还有什么能做的可以降低内存使用,减少 OOM 呢?
2. 图片分辨率相关分辨率适配问题。很多情况下图片所占的内存在整个 App 内存占用中会占大部分。我们知道可以通过将图片放到 hdpi/xhdpi/xxhdpi 等不同文件夹进行适配,通过 xml android:background 设置背景图片,或者通过 BitmapFactory.decodeResource()方法,图片实际上默认情况下是会进行缩放的。在 Java 层实际调用的函数都是或者通过 BitmapFactory 里的 decodeResourceStream 函数
public static Bitmap decodeResourceStream(Resources res, TypedValue value,        InputStream is, Rect pad, Options opts) {    if (opts == null) {        opts = new Options();    }    if (opts.inDensity == 0 && value != null) {        final int density = value.density;        if (density == TypedValue.DENSITY_DEFAULT) {            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;        } else if (density != TypedValue.DENSITY_NONE) {            opts.inDensity = density;        }    }    if (opts.inTargetDensity == 0 && res != null) {        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;    }    return decodeStream(is, pad, opts);}decodeResource 在解析时会对 Bitmap 根据当前设备屏幕像素密度 densityDpi 的值进行缩放适配操作,使得解析出来的 Bitmap 与当前设备的分辨率匹配,达到一个最佳的显示效果,并且 Bitmap 的大小将比原始的大,可以参考下腾讯 Bugly 的详细分析 Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?。
关于 Density、分辨率、-hdpi 等 res 目录之间的关系:

举个例子,对于一张 1280×720 的图片,如果放在 xhdpi,那么 xhdpi 的设备拿到的大小还是 1280×720 而 xxhpi 的设备拿到的可能是 1920×1080,这两种情况在内存里的大小分别为:3.68M 和 8.29M,相差 4.61M,在移动设备来说这几 M 的差距还是很大的。
尽管现在已经有比较先进的图片加载组件类似 Glide,Facebook Freso, 或者老牌 Universal-Image-Loader,但是有时就是需要手动拿到一个 bitmap 或者 drawable,特别是在一些可能会频繁调用的场景(比如 ListView 的 getView),怎样尽可能对 bitmap 进行复用呢?这里首先需要明确的是对同样的图片,要 尽可能复用,我们可以简单自己用 WeakReference 做一个 bitmap 缓存池,也可以用类似图片加载库写一个通用的 bitmap 缓存池,可以参考 GlideBitmapPool[8]的实现。
我们也来看看系统是怎么做的,对于类似在 xml 里面直接通过 android:background 或者 android:src 设置的背景图片,以 ImageView 为例,最终会调用 Resource.java 里的 loadDrawable:
际上系统也是有一份全局的缓存,sPreloadedDrawables, 对于不同的 drawable,如果图片时一样的,那么最终只会有一份 bitmap(享元模式),存放于 BitmapState 中,获取 drawable 时,系统会从缓存中取出这个 bitmap 然后构造 drawable。而通过 BitmapFactory.decodeResource()则每次都会重新解码返回 bitmap。所以其实我们可以通过 context.getResources().getDrawable 再从 drawable 里获取 bitmap,从而复用 bitmap,然而这里也有一些坑,比如我们获取到的这份 bitmap,假如我们执行了 recycle 之类的操作,但是假如在其他地方再使用它是那么就会有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”异常。3. 图片压缩BitmapFactory 在解码图片时,可以带一个 Options,有一些比较有用的功能,比如:
  • inTargetDensity 表示要被画出来时的目标像素密度
  • inSampleSize 这个值是一个 int,当它小于 1 的时候,将会被当做 1 处理,如果大于 1,那么就会按照比例(1 / inSampleSize)缩小 bitmap 的宽和高、降低分辨率,大于 1 时这个值将会被处置为 2 的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将 bitmap 处理为,width=50,height=50,宽高降为 1 / 2,像素数降为 1 / 4
  • inJustDecodeBounds 字面意思就可以理解就是只解析图片的边界,有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片。
  • inPreferredConfig 默认会使用 ARGB_8888,在这个模式下一个像素点将会占用 4 个 byte,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用 RGB_565,一个像素只会占用 2 个 byte,一下可以省下 50%内存。
  • inPurgeable和inInputShareable 这两个需要一起使用,BitmapFactory.java 的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个 bitmap,有点类似软引用,但是实际在 5.0 以后这两个属性已经被忽略,因为系统认为回收后再解码实际会反而可能导致性能问题
  • inBitmap 官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在 4.4 以前只有相同大小的图片内存区域可以复用,4.4 以后只要原有的图片比将要解码的图片大既可以复用了。
4. 缓存池大小现在很多图片加载组件都不仅仅是使用软引用或者弱引用了,实际上类似 Glide 默认使用的事 LruCache,因为软引用 弱引用都比较难以控制,使用 LruCache 可以实现比较精细的控制,而默认缓存池设置太大了会导致浪费内存,设置小了又会导致图片经常被回收,所以需要根据每个 App 的情况,以及设备的分辨率,内存计算出一个比较合理的初始值,可以参考 Glide 的做法。
5. 内存抖动什么是内存抖动呢?Android 里内存抖动是指内存频繁地分配和回收,而频繁的 gc 会导致卡顿,严重时还会导致 OOM。

一个很经典的案例是 string 拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的 log 的时候), 见 Android 优化之 String 篇[9]。
而内存抖动为什么会引起 OOM 呢?
主要原因还是有因为大量小的对象频繁创建,导致内存碎片,从而当需要分配内存时,虽然总体上还是有剩余内存可分配,而由于这些内存不连续,导致无法分配,系统直接就返回 OOM 了。
比如我们坐地铁的时候,假设你没带公交卡去坐地铁,地铁的售票机就只支持 5 元,10 元,而哪怕你这个时候身上有 1 万张 1 块的都没用(是不是觉得很反人类..)。当然你可以去兑换 5 元,10 元,而在 Android 系统里就没那么幸运了,系统会直接拒绝为你分配内存,并扔一个 OOM 给你(有人说 Android 系统并不会对 Heap 中空闲内存区域做碎片整理,待验证)。
其他常用数据结构优化,ArrayMap 及 SparseArray 是 android 的系统 API,是专门为移动设备而定制的。用于在一定情况下取代 HashMap 而达到节省内存的目的,具体性能见 HashMap,ArrayMap,SparseArray 源码分析及性能对比[10],对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代,大概可以省 30%的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10%左右,但是数据量在 1000 以上时,查找速度可能会变慢。
枚举,Android 平台上枚举是比较争议的,在较早的 Android 版本,使用枚举会导致包过大,在个例子里面,使用枚举甚至比直接使用 int 包的 size 大了 10 多倍 在 stackoverflow 上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在 Android 平台性能问题已经不大,而目前 Android 官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用 int 多使用 2 倍的内存。
ListView 复用,这个大家都知道,getView 里尽量复用 conertView,同时因为 getView 会频繁调用,要避免频繁地生成对象
谨慎使用多进程,现在很多 App 都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M 左右),对于使用完的进程,服务都要及时进行回收。
尽量使用系统资源,系统组件,图片甚至控件的 id
减少 view 的层级,对于可以 延迟初始化的页面,使用 viewstub
数据相关:序列化数据使用 protobuf 可以比 xml 省 30%内存,慎用 shareprefercnce,因为对于同一个 sp,会将整个 xml 文件载入内存,有时候为了读一个配置,就会将几百 k 的数据读进内存,数据库字段尽量精简,只读取所需字段。
dex 优化,代码优化,谨慎使用外部库, 有人觉得代码多少于内存没有关系,实际会有那么点关系,现在稍微大一点的项目动辄就是百万行代码以上,多 dex 也是常态,不仅占用 rom 空间,实际上运行的时候需要加载 dex 也是会占用内存的(几 M),有时候为了使用一些库里的某个功能函数就引入了整个庞大的库,此时可以考虑抽取必要部分,开启 proguard 优化代码,使用 Facebook redex 使用优化 dex(好像有不少坑)。
返回列表