天天看点

Android内存优化汇总

以下是工作中总结的一些内存优化的经验,点击每个条目的链接可以查看具体的代码描述。

1.使用保守的Service

  • 实现模块内具体功能时,尽量避免以应用内常驻后台的Service方式实现。如果应用需要使用 service在后台执行业务功能, 除非是一直在进行活动的工作,否则不要让它一直保持在后台运行。并且, 当你的 service 执行完成但是停止失败时要小心 service 导致的内存泄露问题。当启动 service 时, 系统总是优先保持服务的运行,这会导致内存应用效率非常低, 因为被该服务使用的内存不能做其它事情。也会减少系统一直保持的LRU缓存处理数目, 使不同的app切换效率降低。当前所有 service 的运行会导致内存不够维持正常系统的运行时, 系统会发生卡顿的现象,严重时能导致系统不断重启。最好的方式是使用 IntentService控制 service 的生命周期, 当使用 intent 开始任务后, 该 service 执行完所有的工作时会自动停止。在android应用中当不需要使用常驻 service 执行业务功能而去使用一个常驻 service 是最糟糕的内存管理方式之一,所以不要贪婪的使用 service 使你的应用一直运行状态。通过bindService绑定的Service,用完后要及时解除绑定,否则也会造成被绑定的Service一直无法被释放。

2.避免静态对象对Activity或Service的引用

  • 正常情况下,在AMS调度完Acitity的onDestroy之后,ActivityThread会删除对Activity的引用,之后不存在从GC roots到该Activity的引用路径,该Activity的java对象也会被回收。但是在Activity被静态对象引用后,即使前面的引用被删除后,依然存在到该Activity的引用,静态对象的生命周期和进程一致,进程不退出,静态对象不会释放,被其引用的Activity也不会释放。这种情况,也是Monkey测试中碰到最多的内存泄露情形。比较常见的情形,把Activity作为Context直接传给静态对象,把Activity的非静态内部类对象传给静态对象,把Activity的某个view传给静态对象,这些情形都会导致静态对象持有Activity的引用。第1种情况比较明显,后面2种情况中,非静态内部类会持有其外部类的引用,而view对象会通过其mContext持有Activity的引用。所以,对静态对象的赋值,除非是简单的java类型,否则一定要有相对应的清理操作。

3.函数需要传递Context参数时,避免直接传递Activity

  • 需要使用Context作为参数传递时,尽量使用ApplicationContext,使用Activity作为Context的参数传递时,可能会把Activty的引用传递给一个生命周期不同的对象,从而导致Activity在应该被释放时而没有被释放。在静态对象被构造时,如果有Context的参数,要主动转换成ApplicationContext,防止传入的参数是Activity。以上,对Service也是一样。
  • 试着使用关于application的context来替代和activity相关的context,这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免context相关的内存泄漏。最显著地一个是避免context逃出他自己的范围之外。使用Application context。这个context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一个context,记得使用application对象。你可以通过调用Context.getApplicationContext() or Activity.getApplication()来获得。更多的请看这篇文章如何避免Android内存泄漏。

4.谨慎使用SingleInstance模式

  • 如非必要,不要使用SingleInstance。SingleInstane的对象一旦被创建,在应用退出之前,其对象不会被释放。所以,这点要根据具体流程进行评估,仅在确定需要时使用SingleInstance。同样,对于静态对象,也是需要确保其生命周期和相应的逻辑流程一致,在不需要时进行清理。

5.Bitmap类的对象要及时主动释放

  • Bitmap是应用中内存消耗的大户,对于Bitmap类的对象要及时主动释放,不仅对于代码中主动构造的Bitmap对象,要有主动的释放操作,对于通过view. setBackgroundResource(@DrawableRes int resid)接口设置的Bitmap资源,以及通过xml布局文件配置的Bitmap资源,都要在不需要时进行主动释放。在Activity的onDestroy中,要主动释放通过各种方式加载的Bitmap对象。首先,通过View的getBackground()方法获取到BitmapDrawable对象,再通过BitmapDrawable得到Bitmap对象,最后调用Bitmap的recycle方法进行回收。另外有些图片用的是.9格式的png图片,这种图片生成的是NinePatchDrawable,也就是说,这种情况下,通过getBackground()方法获取到的是NinePatchDrawable对象。NinePatchDrawable中取得Bitmap的方法与BitmapDrawable不同,首先通过成员函数getConstantState()获取到内部的mNinePatchState对象,再通过NinePatchState的getBitmap()获取到Bitmap对象。

6.当界面变为隐藏状态后释放内存

  • 重写OnTrimMemory(int level)方法,在应用的所有界面不可见时,系统会回调该方法供应用释放部分内存,其中level的值为TRIM_MEMORY_UI_HIDDEN(如想了解level的其他取值及作用,请自行搜索,其他值可以暂不理会)。这时候可以释放掉Bitmap或其他不再使用的对象,在onRestart或其他合适的时间点,再重新恢复以上资源,保证应用在用户不可见的情况下,占用最少的内存。

7.注册的监听对象要及时进行注销

  • 通常情况下,Activity可能会实现了某些Listener接口,然后Activity会被作为Listener对象进行注册,如果保存Listener地方是全局的静态变量,或其他生命周期与应用生命周期一致的对象,在Activity生命周期结束后,没有进行取消注册的话,就会造成Activity的对象无法被释放。类似的,注册BraodcastReceiver、ContentObserver、EventBus等等,都需要在适当的时间取消注册。

    DialtactsActivity QuickContactActivity

  • 一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用registerReceiver后未调用unregisterReceiver。

    比如:假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

    但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。

    虽然有些系统程序,它本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。

8.使用的资源对象要及时关闭

  • 对于使用了File,Cursor,Stream,Bitmap等资源的应用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。集合中的对象,在不使用时要及时进行清理,特别是Static类型的集合对象,或者是被Static对象持有的集合对象。
  • 我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

9.使用优化过的数据集合

  • 这也是android官方推荐的行为。Android API当中提供了一些优化过后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用这些API可以让我们的程序更加高效。传统Java API中提供的HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。SparseArray适用于key为Integer 类型,类似HashMap<Integer,Obj> ,如果需要使用key为其他类型的情况,还是可以使用HashMap来处理。
  • 利用Android Framework里面优化过的容器类,例如SparseArray,SparseBooleanArray, 与 LongSparseArray。 通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。

10.避免在 bitmaps 中浪费内存

  • 对于xxhdpi的设备,在使用资源图片时,如果xxhdpi文件夹中存在相应的资源图片时,图片加载时,不会被进行缩放,如果xxhdpi不存在,xhdpi中存在,那么不管该图片的分辨率是多少,系统会对其进行放大。也就是说,对于同一张图片,仅仅放在xxhdpi或者xhdpi中时,对于xxhdpi的设备,获取到的Bitmap对象的大小是不一样的。对于一个1280×720的图片,大小可能是3.68M和8.29M,差别超过一倍,对于图片资源需要让设计部同事压缩到最小,如果图片不是颜色很丰富的可以考虑转为webp格式可以极大的压缩图片大小且失真很小,AS支持png转webp格式。

11.ListView复用

  • 初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象。因此getView里要尽量复用conertView,同时因为getView会频繁调用,要避免频繁地生成对象。

12.避免Handler导致的内存泄露

  • Handler一般以内部类的形式实现,从而Handler会持有外部类的引用。如果通过Handler发送一个延迟处理的消息到线程的消息队列,会存在一个消息队列-〉消息-〉Handler-〉外部类(Activity或Servie)的引用,如果外部类退出,这时候,因为存在从消息队列的引用,相应的Activity或Service就不会被释放。对于这种情况,可以考虑Handler不以内部类形式实现,或以静态内部类实现,或者在外部类退出时,从消息队列中删除相应的延迟消息。

13.避免线程导致内存泄露

  • 如果以内部类形式实现了一个子线程,线程开始运行并执行一个耗时的操作,在线程没有结束之前,外部类退出,此时,外部类不会被释放,因为作为内部类的子线程持有外部类的引用,解决方法和前一条类似。

14.避免动画引起的内存泄露

  • 如果在Activity中播放一个无限循环的动画,在退出Activity时没有在 onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看见动画效果了,并且这个时候 Activity的 View会被动画持有,而View又持有了Activty,最终Activity无法释放。解决方法是在Activity的onDestroy中调用animator.cancel()来停止动画。
  • 从 Android3.0开始,Google提供了属性动画,属性动画中有一类无限循环的动画,如果在Activity中播放此类动画且没有在 onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看见动画效果了,并且这个时候 Activity的 View会被动画持有,而View又持有了Activty,最终Activity无法释放。下面的动画是无限动画,会泄露当前的Activity,解决方法是在Activity的onDestroy中调用animator.cancel()来停止动画。

15.避免创建不必要的对象

  • 越少的对象意味着越少的GC操作,同时也意味着更好的性能和用户体验。下面是一些可以避免创建对象的场景:如果有一个需要拼接的字符串,那么优先考虑StringBuffer或StringBuilder来拼接;尽量使用基本的数据类型来代替封装数据类型,int比Integer要更加高效,其他数据类型也是一样;如果我们明确调用方会将这个返回的String再进行拼接的话,可以考虑返回一个StringBuffer对象来代替;基本数据类型要优于对象数据类型,类似地,基本数据类型数组也要优于对象数据类型的数组。

16.使用弱引用防止内存泄露

  • 某些情况下,如果需要长生命周期对象引用到Activity等对象时,可以考虑使用弱引用避免内存泄露,但是,弱引用应该是修复内存泄露的最后手段,在有清晰流程可以避免该内存泄露时,优先从流程上保证,而不是使用弱引用。

17.不要将back键流程处理为Home键的效果

  • 对于我们自己的系统应用,除非有明确的特殊需求,禁止在用户按返回键时,应用将自身放入后台,要保证返回键按下时,走正常的退出流程。这种问题在腾讯的应用中比较明显。

18.谨慎处理第三方库的内存占用

  • 根据之前的经验,很多第三方库在使用后,只要应用不退出,其内存不会主动释放,这种问题在Launcher和管家中都有碰到。针对这种情况,可以考虑将第三方库的相关流程放到单独的进程中处理,在流程执行完毕后,关闭该新进程,避免对主进程造成的额外内存占用。

继续阅读