Android性能:内存篇之内存优化

Android的内存问题是Android开发领域永恒的话题,作为Android老大难问题,内存所带来的困扰远远大于读写的性能问题,近乎所有的问题最后都会变成内存问题,而内存问题,就包括且不局限于“内存溢出”、“内存泄漏”、“内存抖动”等等,那就得学会合理地进行内存管理或进行内存优化了。

尽管Android的Dalvik虚拟机扮演了常规的垃圾回收的角色,但这并不意味着你可以忽视app的内存分配与释放的时机与地点,平时开发应尽量避免因不正确使用内存 & 缺乏管理,从而出现 内存泄露(ML)、内存溢出(OOM)、内存空间占用过大等问题,最终导致应用程序崩溃(Crash)。

一、概念

  • 内存泄漏(Memory Leak):当一个对象不在使用了,本应该被垃圾回收器(JVM)回收。但是这个对象由于被其他正在使用的对象所持有,造成无法被回收的结果。内存泄漏最终会导致内存溢出。
  • 内存溢出(Out of Memory):系统会给每个APP分配内存也就是Heap Size值。当APP占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存时就会抛出的Out Of Memory异常。
  • 内存抖动:内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象,从而使UI线程被频繁阻塞,导致画面卡顿。

它们三者的重要等级分别:内存溢出 > 内存泄露 > 内存抖动。
内存溢出对我们的App来说,影响是非常大的。有可能导致程序闪退,无响应等现象,因此,我们一定要优先解决OOM的问题。
内存泄漏是造成应用程序OOM的主要原因之一。由于Android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。

二、优化方案

1. 内存泄漏

常见引发内存泄露原因主要有:集合类Static关键字修饰的成员变量非静态内部类 / 匿名类资源对象使用后未关闭

  • 集合类
    成因:集合类(Set、List、Map、Iterator等)添加元素后,仍引用着集合元素对象,导致该集合元素对象不可被回收,从而导致内存泄漏。
    解决方案:集合类添加集合元素对象后,在使用后必须从集合中删除。

  • Static关键字修饰的成员变量
    成因:被 Static 关键字修饰的成员变量的生命周期 = 应用程序的生命周期,若使被 Static 关键字修饰的成员变量引用耗费资源过多的实例(如Context),则容易出现该成员变量的生命周期 > 引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露。
    解决方案:尽量避免 Static 成员变量引用资源耗费过多的实例(如 Context),若需引用Context,则尽量使用Applicaiton的Context;某些场景下使用弱引用(WeakReference) 代替强引用持有实例。

  • 单例模式
    成因:单例模式由于其静态特性,其生命周期的长度 = 应用程序的生命周期,若1个对象已不需再使用 而单例对象还持有该对象的引用,那么该对象将不能被正常回收,从而导致内存泄漏。
    解决方案:慎重使用单例模式,单例模式尽量避免引用资源耗费过多的实例(如Context),若需引用Context,则尽量使用Applicaiton的Context。

  • 非静态内部类 / 匿名类
    成因:非静态内部类 / 匿名类 默认持有外部类的引用,若非静态内部类所创建的实例生命周期 = 应用的生命周期,会因非静态内部类默认持有外部类的引用而导致外部类无法释放,最终造成内存泄露;如handler(主线程的Looper对象的生命周期 = 该应用程序的生命周期)、多线程:AsyncTask、实现Runnable接口、继承Thread类(主线程介绍了,子线程没有结束)。
    解决方案:1. 将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)+弱引用;2. 将内部类抽取出来封装成一个单例;3. 尽量避免非静态内部类所创建的实例生命周期 = 静态。4. 子线程实例不引用外部类;5. 外部类销毁前销毁内部类实例。6.当外部类结束生命周期时,清空Handler内消息队列。

  • 资源对象使用后未关闭
    成因:对于资源的使用(如WebView广播BraodcastReceiver、文件流File、数据库游标Cursor、图片资源Bitmap等),若在Activity销毁时无及时关闭 / 注销这些资源,则这些资源将不会被回收,从而造成内存泄漏。
    解决方案:在Activity销毁时及时关闭或注销资源。

  • Service没有停止
    成因:通过service在后台去执行任务,除非当它将要去执行一个任务的时候,否则不应该一直在后台驻留。在完成任务后记得将service停掉,如果Service占用了无用资源可能会导致了内存泄露。
    解决方案:尽量避免使用持久化的service,因为它持有占有了可用内存。建议使用其它替代方案,比如JobScheduler。如果必须使用service,最好是使用 IntentService来限制service的生命周期,一旦处理完启动它的Intent,该IntentService就会将自己停掉

2. 内存溢出

常见引发内存泄露原因主要有:static保存大资源使用数量多的Bitmap加载过多大文件未及时清理内存泄漏最终导致内存溢出

  • static保存大资源
    成因:静态变量时属于类而不是对象,对象可及时被GC清理,但是静态对象不会,所以使用static保存大资源容易占用内存导致最终内存溢出。
    解决方案:尽量不要使用static 变量保存大资源(比如Context等),大资源使用软引用,若需引用Context,则尽量使用Applicaiton的Context。

  • 使用数量多的Bitmap
    成因:bitmap占用大量内存。
    解决方案:1. 及时销毁Bitmap,bitmap不在使用的时候及时Recycle;2. 设定采样率,使用BitmapFactory处理Bitmap再使用;3. 使用软应用,如果使用一个Bitmap却没有保留他的引用那么则没有办法调用recycle方法,使用软应用可以使bitmap在内存不足的时候得到释放。

  • 一次性加载过大文件(如图片、音频、视频)未及时清理
    成因:一次性加载大文件,切割后的文件全放在内存里。
    解决方案:1. 面向文件的开发应该尽量全过程采用流式操作的思想,把所有切割后的文件全放在内存里是不合适的,应该设置缓存区,然后边读边写;2. 使用软应用,使用过的文件流,及时清理;3. 使用JNI来进行处理(JNI层占用native内存,不占用head内存,详见《Android性能:内存篇之进程内存管理》);4. 使用9path代替大图;5. 使用第三方优质文件处理库。具体情况具体分析

  • 内存泄漏最终导致内存溢出
    成因:对象由于被其他正在使用的对象所持有,造成无法被回收的结果,内存泄漏最终会导致内存溢出。
    解决方案:根据以上分析的内存泄漏的各种原因,解决了内存泄漏即可解决此问题。

  • 进行隐式装箱
    成因:自动装箱是Java 5 引入的一个特性,即自动将原始类型的数据转换成对应的引用类型,比如将int转为Integer等。这种特性,极大的减少了编码时的琐碎工作,但是稍有不注意就可能创建了不必要的对象了。当将原始数据类型的值加入集合中时,也会发生自动装箱。
    解决方案:正确地声明变量类型(如基础类型),避免因为自动装箱引起的性能问题,如有需要避免这种情况,可以选择SparseArray,SparseBooleanArray,SparseLongArray等容器。

  • 谨慎选用容器
    成因:Java和Android提供了很多编辑的容器集合来组织对象。比如ArrayList,ContentValues,HashMap等。然而,这样容器虽然使用起来方便,但也存在一些问题,就是他们会自动扩容,这其中不是创建新的对象,而是创建一个更大的容器对象。这就意味这将占用更大的内存空间。
    解决方案:使用SparseArray,ArrayMap等容器替换。

  • 使用注解替代枚举
    成因:枚举是我们经常使用的一种用作值限定的手段,使用枚举比单纯的常量约定要靠谱。然后枚举的实质还是创建对象,占用内存。
    解决方案:好在Android提供了相关的注解,使得值限定在编译时进行,进而减少了运行时的压力。相关的注解为IntDef和StringDef。

  • 使用注解替代枚举
    成因:枚举是我们经常使用的一种用作值限定的手段,使用枚举比单纯的常量约定要靠谱。然后枚举的实质还是创建对象,占用内存。
    解决方案:好在Android提供了相关的注解,使得值限定在编译时进行,进而减少了运行时的压力。相关的注解为IntDef和StringDef。

3. 内存抖动

常见引发内存抖动原因主要有:在循环体内创建对象、自定义View的onDraw()创建对象、大量初始化Bitmap、频繁创建大内存对象。

  • 原因、解决方案复用(解决内存抖动的方法也是复用
    成因:内存抖动是由于短时间内有大量对象进出Young Generiation区导致的,它伴随着频繁的GC。
    解决方案:1. 尽量避免在循环体内创建对象,应该把对象创建移到循环体外;2. 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象;3. 当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用; 4. 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

本章主要从分析原因和构思解决方案进行讨论,但是判断应用内存使用情况,需要借助代码和一些工具,在下一章将会细细详聊。

你可能感兴趣的:(Android,内存回收)