Android 性能优化之内存泄漏检测以及内存优化(下)

字数 5260阅读 341

上篇博客我们写到了 Android 中内存泄漏的检测以及相关案例,这篇我们继续来分析一下 Android 内存优化的相关内容。
  上篇:Android 性能优化之内存泄漏检测以及内存优化(上)
  中篇:Android 性能优化之内存泄漏检测以及内存优化(中)
  下篇:Android 性能优化之内存泄漏检测以及内存优化(下)
  转载请注明出处:http://blog.csdn.net/self_study/article/details/68946441
  对技术感兴趣的同鞋加群544645972一起交流。

QQ图片20170426100405.png

Android 内存优化

上篇博客描述了如何检测和处理内存泄漏,这种问题从某种意义上讲是由于代码的错误导致的,但是也有一些是代码没有错误,但是我们可以通过很多方式去降低内存的占用,使得应用的整体内存处于一个健康的水平,下面总结一下内存优化的几个点:

图片处理优化

由于图片在应用中使用的较为频繁,而且图片占用的内存通常来说也比较大,举个例子来说,现在正常的手机基本都在 1000W 像素左右的水平,较好的基本都在 1600W 像素,这时候拍出来的照片基本都在 3400*4600 这个水平,按照 ARGB_8888 的标准,一个像素 4 个字节,所以总共有 1600W*4=6400W 字节,总共 64M,也就是说会占用 64M 的内存,而实际出来的 .png 图片大小也就才 3M 左右,这是一个非常恐怖的数量,因为对于一个 2G 左右内存的手机来说,一个进程最大可用的内存可能也就在 100M+,一张图片就能够占用一半内存,这也就是为什么 decode 一个 bitmap 是发生 OOM 高频的地方,所以在实际开发过程中图片的处理和内存占用优化也是一个比较重要的地方。
  Android中图片有四种属性,分别是:<ul><li>ALPHA_8:每个像素占用1byte内存 </li><li>ARGB_4444:每个像素占用2byte内存 </li><li>ARGB_8888:每个像素占用4byte内存 (默认)</li><li>RGB_565:每个像素占用2byte内存</li></ul>

大图片优化

为了找出在运行过程中占用内存很大的图片,这个时候就可以借助上篇博客介绍到的 MAT 了,按照 Retained Heap 大小进行排序,找出占用内存比较大的几个对象,然后通过引用链找到持有它的地方,最后看能否有优化的地方。

图片分辨率相关

我们一般将不同分辨率的图片放置在不同的文件夹 hdpi/xhdpi/xxhdpi 下面进行适配,通过 android:background 来设置背景图片或者使用 BitmapFactory.decodeResource() 方法的时候,图片默认情况下会进行缩放,在 Java 层实际调用的是 BitmapFactory 里的 decodeResourceStream 方法:

/**
 * Decode a new Bitmap from an InputStream. This InputStream was obtained from
 * resources, which we pass to be able to scale the bitmap accordingly.
 */
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);
}

decodeResourceStream 在解析时会将 Bitmap 根据当前设备屏幕像素密度 densityDpi 的值进行缩放适配操作,使得解析出来的 Bitmap 与当前设备的分辨率匹配,达到一个最佳的显示效果,上面也提到过,解析过后 Bitmap 的大小将比原始的大不少,关于 Bitmap 的详细分析可以看一下这篇博客:Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
  关于 Density、分辨率和相关 res 目录的关系如下:

DensityDpi 分辨率 res Density
160dpi 320 x 533 mdpi 1
240dpi 460 x 800 hdpi 1.5
320dpi 720 x 1280 xhdpi 2
480dpi 1080 x 1920 xxhdpi 3
560dpi 1440 x 2560 xxxhdpi 3.5

举个例子来说一张 1920x1080 的图片来说,如果放在 xhdpi 下面,那么 xhdpi 设备将其转成 bitmap 之后的大小是 1920x1080,而 xxhdpi 设备获取的大小则是 2520x1418,大小约为前者的 1.7 倍,这些内存对于移动设备来说已经算是比较大的差距。有一点需要提到的是新版本 Android Studio 已经使用 mipmap 来代替了,比起 drawable 官方的解释是系统会在缩放上提供一定的性能优化:

Mipmapping for drawables

Using a mipmap as the source for your bitmap or drawable is a simple way to provide a quality image and various image scales, which can be particularly useful if you expect your image to be scaled during an animation.

Android 4.2 (API level 17) added support for mipmaps in the Bitmap class—Android swaps the mip images in your Bitmap when you've supplied a mipmap source and have enabled setHasMipMap(). Now in Android 4.3, you can enable mipmaps for a BitmapDrawable object as well, by providing a mipmap asset and setting the android:mipMap attribute in a bitmap resource file or by calling hasMipMap().

但是从用法来说和正常的 drawable 一样。
  系统也对图片展示进行了相应的优化,对于类似在 xml 里面直接通过 android:background 或者 android:src 设置的背景图片,以 ImageView 为例,最终会调用 ResourceImpl(低版本是 Resource) 类中的里的 loadDrawable 方法,在这个方法中我们可以很清楚的看到系统针对相同的图片使用享元模式构造了一个全局的缓存 DrawableCache 类的对象:

Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
        boolean useCache) throws NotFoundException {
    try {
        if (TRACE_FOR_PRELOAD) {
            // Log only framework resources
            if ((id >>> 24) == 0x1) {
                final String name = getResourceName(id);
                if (name != null) {
                    Log.d("PreloadDrawable", name);
                }
            }
        }

        final boolean isColorDrawable;
        final DrawableCache caches;
        final long key;
        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
            caches = mColorDrawableCache;
            key = value.data;
        } else {
            isColorDrawable = false;
            caches = mDrawableCache;
            key = (((long) value.assetCookie) << 32) | value.data;
        }

        // First, check whether we have a cached version of this drawable
        // that was inflated against the specified theme. Skip the cache if
        // we're currently preloading or we're not using the cache.
        if (!mPreloading && useCache) {
            final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
            if (cachedDrawable != null) {
                return cachedDrawable;
            }
        }
        .....
}

DrawableCache 类继承自 ThemedResourceCache 类,来看看这两个相关类:

/**
 * Class which can be used to cache Drawable resources against a theme.
 */
class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
    ......
}
/**
 * Data structure used for caching data against themes.
 *
 * @param <T> type of data to cache
 */
abstract class ThemedResourceCache<T> {
    private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
    private LongSparseArray<WeakReference<T>> mUnthemedEntries;
    private LongSparseArray<WeakReference<T>> mNullThemedEntries;
    .....
}

可以看到这个类使用一个 ArrayMap 来存储一个 Drawable 和这个 Drawable 对应的 Drawable.ConstantState 信息,相同的图片对应相同的 Drawable.ConstantState,所以这就可以保证在一些情况下相同的图片系统只需要保存一份,从而减少内存占用。我们从这里可以得到一些启示,如果我们在某些会重复使用图片的场景下,自己构造一个 Bitmap 缓存器,然后里面保存 Bitmap 的 WeakReference,当使用的时候先去缓存里面获取,获取不到再做解析的操作。

图片压缩

BitmapFactory 在 decode 图片的时候,可以带上一个 Options,这个很多人应该很熟悉,在 Options 中我们可以指定使用一些压缩的功能:<ul><li> inTargetDensity </li>表示要被画出来时的目标像素密度;<li> inSampleSize </li> 这个值是一个 int,当它小于 1 的时候,将会被当做 1 处理,如果大于 1,那么就会按照比例(1 / inSampleSize)缩小 bitmap 的宽和高、降低分辨率,大于 1 时这个值将会被处置为 2 的指数(3 会被处理为 4,5被处理为8)。例如 width=100,height=100,inSampleSize=2,那么就会将 bitmap 处理为,width=50,height=50,宽高降为 1/2,像素数降为 1/4;<li> inJustDecodeBounds </li>字面意思就可以理解就是只解析图片的边界,有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片;<li> inPreferredConfig </li>默认会使用 ARGB_8888,在这个模式下一个像素点将会占用 4 个字节,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用 RGB_565,这样一个像素只会占用 2 个字节,一下就可以省下 50% 内存了;<li>inPurgeable 和 inInputShareable</li>这两个需要一起使用,BitmapFactory 类的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个 Bitmap,有点类似软引用,但是实际在 5.0 以后这两个属性已经被忽略,因为系统认为回收后再解码实际反而可能会导致性能问题;<li> inBitmap </li>官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在 4.4 以前只有相同大小的图片内存区域可以复用,4.4 以后只要原有的图片比将要解码的图片大就可以实现复用了。</ul>  关于图片压缩和图片内存优化的例子可以参考我以前写的一个博客:android仿最新版本微信相册--附源码

巨型图片的处理

要加载一张巨型的图片,比如 20000*10000 分辨率的,这个时候全放进内存是完全不可能的,直接会占用 800M 内存,所以必须要用到上面说到的压缩比,将其分辨率降低到和屏幕匹配,匹配之后如果还要去支持用户的放大、缩小、左右滑动等操作,这时候就可以使用 BitmapRegionDecoder 这个类去处理图片了,具体的可以去看看这篇博客:Android 高清加载巨图方案 拒绝压缩图片,实现的原理就是分区域去加载,或者可以去参考这个开源库:WorldMap

图片缓冲池

现在默认的图片加载工具例如 Universal-ImageLoader 或者 Glide 都会使用一个 LruCache 来管理应用中的图片缓存,一般缓冲池的大小设置为应用可用内存的 1/8。

有效利用系统自带资源

Android 系统本身内置了大量的资源,比如一些通用的字符串、颜色定义、常用 icon 图片,还有些动画和页面样式以及简单布局,如果没有特别的要求,这些资源都可以在应用程序中直接引用。直接使用系统资源不仅可以在一定程度上减少内存的开销,还可以减少应用程序 APK 的体积:<ul><li>利用系统定义的 ID</li>比如我们有一个定义 ListView 的 xml 文件,一般的,我们会写类似下面的代码片段:

<ListView  
    android:id="@+id/mylist"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"/>  

这里我们定义了一个 ListView,定义它的 id 是 "@+id/mylist",实际上,如果没有特别的需求,就可以利用系统定义的 ID,类似下面的样子:

<ListView  
    android:id="@android:id/list"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"/>  

在 xml 文件中引用系统的 ID,只需要加上 “@android:” 前缀即可。如果是在Java代码中使用系统资源,和使用自己的资源基本上是一样的。不同的是,需要使用 android.R 类来使用系统的资源,而不是使用应用程序指定的 R 类。这里如果要获取 ListView 可以使用 android.R.id.list 来获取;
<li>利用系统的图片资源</li>这样做的好处,一个是美工不需要重复的做一份已有的图片了,可以节约不少工时,另一个是能保证我们的应用程序的风格与系统一致;<li>利用系统的字符串资源</li>如果使用系统的字符串,默认就已经支持多语言环境了,直接去使用 @android:string/yes 和 @android:string/no,在简体中文环境下会显示“确定”和“取消”,在英文环境下会显示 “OK” 和 “Cancel”;<li>利用系统的 Style</li>假设布局文件中有一个 TextView,用来显示窗口的标题,使用中等大小字体,可以使用下面的代码片段来定义 TextView 的 Style:

<TextView  
    android:id="@+id/title"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    android:textAppearance="?android:attr/textAppearanceMedium" />  

其中 android:textAppearance="?android:attr/textAppearanceMedium" 就是使用系统的 style,需要注意的是使用系统的 style 必须在想要使用的资源前面加 “?android:” 作为前缀,而不是 “@android:”;
<li>利用系统的颜色定义</li>除了上述的各种系统资源以外,还可以使用系统定义好的颜色,在项目中最常用的,就是透明色的使用 android:background ="@android:color/transparent" 。</ul>

内存抖动造成内存碎片优化

上篇博客说到过频繁的 GC 会造成内存的抖动,最终会导致内存当中存在很多内存碎片,虽然总体来说内存是可用的,但是当分配内存给一个大对象的时候,没有一块足够大的连续区域可以分配给这个对象就会造成 OOM,所以这个时候为了减少内存抖动,需要去观察 Memory Monitor,检查应用的正常使用过程中有没有因为频繁的内存分配和释放导致锯齿形状的内存图,如果有的话去检查相关代码,比较容易出现内存抖动的地方可能是 convertView 没有复用、频繁拼接小的 String 字符串、在 for 循环中创建对象等等,找到问题所在,解决内存抖动。

常用数据结构优化

ArrayMap 以及 SparseArray 是 Android 系统专门为移动设备而定制的数据结构,用于在一定情况下取代 HashMap 而达到节省内存的目的,对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代(一般 Lint 也会提示开发者将其换成 SparceArray),大概可以省30%的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10% 左右,但是数据量在 1000 以上时,查找速度可能会变慢,具体的可以看看这篇博客:HashMap,ArrayMap,SparseArray源码分析及性能对比

避免创建不必要的对象

最常见的例子就是当你要频繁操作一个字符串时,使用 StringBuilder 代替 String。对于所有基本类型的组合:int 数组比 Integer 数组好,这也概括了一个基本事实,两个平行的 int 数组比 (int,int) 对象数组性能要好很多。总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。

尽量避免使用枚举

Android 平台上枚举是比较争议的,在较早的 Android 版本,使用枚举会导致包过大,在某些情况下使用枚举甚至比直接使用 int 包的 size 大了 10 多倍。在 Stackoverflow 上也有很多的讨论,大致意思是随着虚拟机的优化,目前枚举变量在 Android 平台性能问题已经不大,而目前 Android 官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用 int 多使用 2 倍的内存,具体的可以看看这个讨论:Should I strictly avoid using enums on Android?

尽量使用系统类库

选择系统类库中的代码而非自己重写,第一个可以节省少部分内存,第二个考虑到系统空闲时会用汇编代码调用来替代系统类库中方法,这可能比 JIT 中生成的等价的最好的 Java 代码还要快:<ul><li>当你在处理字串的时候,不要吝惜使用 String.indexOf(),String.lastIndexOf() 等特殊实现的方法,这些方法都是使用 C/C++ 实现的,比起 Java 循环快 10 到 100 倍;</li><li>System.arraycopy 方法在有 JIT 的 Nexus One 上,自行编码的循环快 9 倍;</li><li>android.text.format 包下的 Formatter 类,提供了 IP 地址转换、文件大小转换等方法,DateFormat 类,提供了各种时间转换,都是非常高效的方法;</li><li>TextUtils 类,对于字符串处理 Android 为我们提供了一个简单实用的 TextUtils 类,如果处理比较简单的内容不用去思考正则表达式不妨试试这个 android.text.TextUtils 的类;</li><li>高性能 MemoryFile 类,很多人抱怨 Android 处理底层 I/O 性能不是很理想,如果不想使用 NDK 则可以通过 MemoryFile 类实现高性能的文件读写操作。MemoryFile 适用于哪些地方呢?对于 I/O 需要频繁操作的,主要是和外部存储相关的 I/O 操作,MemoryFile 通过将 NAND 或 SD 卡上的文件,分段映射到内存中进行修改处理,这样就用高速的 RAM 代替了 ROM 或 SD 卡,性能自然提高不少,对于 Android 手机而言同时还减少了电量消耗。该类实现的功能不是很多,直接从 Object 上继承,通过 JNI 的方式直接在 C 底层执行。</li></ul>

减少 View 的层级

虽然这或多或少有点渲染优化的味道,但是由于 View 也是会占用一定内存的,所以第一步是通过 Hierarchy Viewer 去去掉多余的 View 层级,第二步是通过使用 ViewStub 去对一些可以延迟加载的 View 做到使用时加载,一定程度上也可以降低内存使用。

数据相关

使用 Protocol Buffer 对数据进行压缩(关于 Protocol Buffer 和其他工具的对比,可以看看这篇文章:thrift-protobuf-compare),Protocol Buffer 相比于 xml 可以减少 30% 的内存使用量;慎用 SharedPreference,因为对于同一个 SP 有时候为了读取一个字段可能会将整个 xml 文件都加入内存,因此慎用 SP,或者可以将一个大的 SP 分散为几个小的 SP;数据库字段尽量精简,表设计合理,只读取所需要的字段而不是整个结构都加载到内存当中。

dex 优化,代码优化,谨慎使用外部库

有人觉得代码多少与内存没有关系,实际上会有那么点关系,现在稍微大一点的项目动辄就是百万行代码以上,多 dex 也是常态,不仅占用 Rom 空间,实际上运行时候需要加载的 dex 也是会占用内存的(几 M),有时候为了使用一些库里的某个功能函数就引入了整个庞大的库是不太合适的,此时可以考虑抽取必要部分;另外开启 proguard 优化代码,使用 Facebook redex 优化 dex(好像有不少坑)也是一种不错的方式。

对象池模式享元模式

对于对象的重复使用来说,对象池模式享元模式再合适不过了,具体的可以去看看我博客里面对于这两个模式的介绍和使用。

onLowMemory() 与 onTrimMemory()

我们都知道 Android 用户可以随意在不同的应用之间进行快速切换,系统为了让 Background 的应用能够迅速的切换到 Forground,每一个 Background 的应用都会占用一定的内存。Android 系统会根据当前的系统的内存使用情况,在一定情况下决定回收部分 Background 的应用内存,如果 Background 的应用从暂停状态直接被恢复到 Forground,能够获得较快的恢复体验,如果 Background 应用是从 Kill 状态进行恢复,相比之下就显得稍微有点慢:

这里写图片描述

<ul>
<li>onLowMemory()</li>Android 系统提供了一些回调来通知当前应用的内存使用情况,通常来说当所有的 Background 应用都被 kill 掉的时候,Forground 应用会收到 onLowMemory() 的回调,在这种情况下需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。
<li>onTrimMemory(int)</li>Android 系统从 4.0 开始还提供了 onTrimMemory() 的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递指定的参数,代表不同的内存使用情况,收到 onTrimMemory() 回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用,返回的参数:<ul><li> TRIM_MEMORY_BACKGROUND </li><li>TRIM_MEMORY_COMPLETE</li><li>TRIM_MEMORY_MODERATE</li><li>TRIM_MEMORY_RUNNING_CRITICAL</li><li>TRIM_MEMORY_RUNNING_LOW</li><li>TRIM_MEMORY_RUNNING_MODERATE</li><li>TRIM_MEMORY_UI_HIDDEN</li></ul></ul>因为 onTrimMemory() 的回调是在 API 14 才被加进来的,对于老的版本,你可以使用 onLowMemory 回调来进行兼容,onLowMemory 相当与 TRIM_MEMORY_COMPLETE。

谨慎使用多进程

使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得代码逻辑更加复杂,另外如果使用不当,它可能反而会导致显著的内存增加。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术,一个典型的例子是创建一个可以长时间后台播放的 Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些 UI 资源也没有办法得到释放,类似这样的应用可以切分成两个进程:一个用来操作 UI,另外一个给后台的 Service。

引用

http://blog.csdn.net/luoshengyang/article/details/42555483
http://blog.csdn.net/luoshengyang/article/details/41688319
http://blog.csdn.net/luoshengyang/article/details/42492621
http://blog.csdn.net/luoshengyang/article/details/41338251
http://blog.csdn.net/luoshengyang/article/details/41581063
http://blog.csdn.net/a396901990/article/details/38707007
https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
http://geek.csdn.net/news/detail/127226
http://www.jianshu.com/p/216b03c22bb8
https://zhuanlan.zhihu.com/p/25213586
https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
http://www.cnblogs.com/larack/p/6071209.html
https://source.android.com/devices/tech/dalvik/gc-debug.html
http://blog.csdn.net/high2011/article/details/53138202
http://gityuan.com/2015/10/03/Android-GC/
http://www.ayqy.net/blog/android-gc-log%E8%A7%A3%E8%AF%BB/
https://developer.android.com/studio/profile/investigate-ram.html
https://zhuanlan.zhihu.com/p/26043999
http://www.csdn.net/article/2015-09-18/2825737

推荐阅读更多精彩内容