应用内存优化主要思路
应用内存优化主要思路
针对内存优化,从系统的角度看,希望应用能够在用户操作流程需要时启动,流程结束时停止,在相应的操作流程之外,不要再占用系统的内存;从应用的层面看,应用内的每一个对象,都是在相应的逻辑流程开始时创建,流程结束时销毁,保证对象的生命周期不超过相应的逻辑流程。根据上周对联系人模块调查,以及一些Monkey测试中的内存泄露案例,初步总结了一下目前比较常见的内存相关的问题点。
1. 首先还是需要强调一下,实现模块内具体功能时,尽量避免以应用内常驻后台的Service方式实现,android官方也认为这种方式过于贪婪。这个也是前段时间调查的重点,但是新增的功能中还是会有新的常驻后台Service出现,针对这类后台Service,请大家根据具体的业务流程,合理处理Service的生命周期,保证流程结束时,Service能够被正常停止。通过bindService绑定的Service,用完后要及时解除绑定,否则也会造成被绑定的Service一直无法被释放。
2. 对于我们自己的系统应用,除非有明确的特殊需求,禁止在用户按返回键时,应用将自身放入后台,要保证返回键按下时,走正常的退出流程。目前发现在短信界面,会有类似的问题,其他应用是否还存在类似的处理,请大家自查。
3. 在应用的内部流程上,保证对象的生命周期不会超过相应的逻辑流程。比如之前在联系人模块中发现一个问题,在DialerApplication的onCreate函数中,创建并持有了一个Bitmap对象,该Bitmap是用来在通话界面作为背景使用。Application的生命周期和整个进程一致,只要该进程还在内存中存在,Application对象就不会被销毁,其持有的这个Bitmap对象也不会被释放,会导致大约30M的内存一直被额外占用。而这种占用不会被工具认为是内存泄露,在Monkey等相关测试中也不会被发现。类似这种有意或无意中导致的对象生命周期与逻辑流程不一致的情况,需要大家对代码流程进行仔细的梳理,争取杜绝这种情况的发生。
4. 静态对象直接或间接引用到Activity,静态对象的生命周期和进程一致。正常情况下,在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类型,否则一定要有相对应的清理操作。
5. 需要使用Context作为参数传递时,尽量使用ApplicationContext,使用Activity作为Context的参数传递时,可能会把Activty的引用传递给一个生命周期不同的对象,从而导致Activity在应该被释放时而没有被释放。在静态对象被构造时,如果有Context的参数,要主动转换成ApplicationContext,防止传入的参数是Activity。以上,对Service也是一样。
6. 如非必要,不要使用SingleInstance。SingleInstane的对象一旦被创建,在应用退出之前,其对象不会被释放。所以,这点要根据具体流程进行评估,仅在确定需要时使用SingleInstance。同样,对于静态对象,也是需要确保其生命周期和相应的逻辑流程一致,在不需要时进行清理。
7. 对于Bitmap类的对象要及时主动释放,不仅对于代码中主动构造的Bitmap对象,要有主动的释放操作,对于通过view. setBackgroundResource(@DrawableRes int resid)接口设置的Bitmap资源,以及通过xml布局文件配置的Bitmap资源,都要在不需要时进行主动释放。我之前用eclipse向导生成了一个空的应用,只有一个简单的Activity,背景通过xml配置了一张非常大的图片。应用打开后,通过adb shell dumpsys meminfo + packagename 查看,占用内存150M。在不主动释放背景图片的情况下,按返回键退出,再次查看该应用内存占用为80M左右;在通过主动获取界面背景的Bitmap,并主动释放后,该应用在退出后,后台占用的内存仅有25M。在adb shell dumpsys meminfo显示的参数中,内存差别的部分主要集中在Native Heap和Graphics,前者应该是为图片数据分配的内存,后者应该是描画该图片占用的内存。因此,在Activity的onDestroy中,要主动释放通过各种方式加载的Bitmap对象。首先,通过View的getBackground()方法获取到BitmapDrawable对象,再通过BitmapDrawable得到Bitmap对象,最后调用Bitmap的recycle方法进行回收。
8. 重写OnTrimMemory(int level)方法,在应用的所有界面不可见时,系统会回调该方法供应用释放部分内存,其中level的值为TRIM_MEMORY_UI_HIDDEN(如想了解level的其他取值及作用,请自行搜索,其他值可以暂不理会)。这时候可以释放掉Bitmap或其他不再使用的对象,在onRestart或其他合适的时间点,再重新恢复以上资源,保证应用在用户不可见的情况下,占用最少的内存。也是上面的例子,在OnTrimMemory实现背景图片释放后,通过状态栏切换到其他应用时,该应用的内存占用有时是80M左右,有时是25M左右,2者区别在Native Heap中,也就是说,这种情况下,Native Heap有时会被释放,有时不会被释放,目前还不太清楚具体的原因。
9. 注册某个对象后,要及时进行取消。比如Activity实现了某些Listener接口,在被作为Listener对象注册后,在Activity生命周期结束后,没有进行取消注册的话,就会造成Activity的对象无法被释放。类似的,注册广播接收器、注册观察者、EventBus等等,都需要在适当的时间取消注册。
10. 使用的资源对象要及时关闭,比如File、Cursor等。集合中的对象,在不使用时要及时进行清理,特别是Static类型的集合对象,或者是被Static对象持有的集合对象。之前在Dialer模块中就有发现这种情况。
11. 使用优化过的数据集合,这也是android官方推荐的。Android API当中提供了一些优化过后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用这些API可以让我们的程序更加高效。传统Java API中提供的HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。
12. 图片分辨率相关,对于xxhdpi的设备,在使用资源图片时,如果xxhdpi文件夹中存在相应的资源图片时,图片加载时,不会被进行缩放,如果xxhdpi不存在,xhdpi中存在,那么不管该图片的分辨率是多少,系统会对其进行放大。也就是说,对于同一张图片,仅仅放在xxhdpi或者xhdpi中时,对于xxhdpi的设备,获取到的Bitmap对象的大小是不一样的。对于一个1280×720的图片,大小可能是3.68M和8.29M,差别超过一倍。
13. ListView复用,getView里尽量复用conertView,同时因为getView会频繁调用,要避免频繁地生成对象。
14. 针对ROM3.0主分支和只有2GB小内存的项目,相关的代码要保持兼容。比如,在3.0主分支中,比如MMS等部分重要应用通过开机启动后台Service的方式实现热启动,这点在2GB的小内存项目上是不允许的。目前我的想法是通过获取手机RAM的大小,来决定是否启动相应的后台Service。这部分主要集中在通短联模块,请姚明注意关注。
15. 针对游戏模式,之前要求取消后台Service。如果未找到合适的方法,可以考虑将后台Service放在单独的进程中,和界面相关的部分隔离开,因为目前后台Service占用的内存较小,只有几M,而界面相关部分被开启后,会有几十M,分开后可以保证界面部分能够及时退出。
16. 对于常驻内存的部分应用,其内存占用要重点处理。之前Launcher进行调查时,发现进行地图描画后,部分内存一直不会释放,高德的解释是这部分静态内存会在应用退出时释放,但是Launcher是不会退出的,因此类似这种内存占用还需要想办法争取进行释放。SystemUI中,通过adb shell dumpsys meminfo指令查看内存状态时,发现在内存占用达到160M时,View对象达到1600多个,比原生高出接近1000个view对象,这部分也需要重点调查下,确保不再需要对象不会被创建。
17. Handler导致的内存泄露,Handler一般以内部类的形式实现,从而Handler会持有外部类的引用。如果通过Handler发送一个延迟处理的消息到线程的消息队列,会存在一个消息队列-〉消息-〉Handler-〉外部类(Activity或Servie)的引用,如果外部类退出,这时候,因为存在从消息队列的引用,相应的Activity或Service就不会被释放。对于这种情况,可以考虑Handler不以内部类形式实现,或以静态内部类实现,或者在外部类退出时,从消息队列中删除相应的延迟消息。
18. 线程导致内存泄露,如果以内部类形式实现了一个子线程,线程开始运行并执行一个耗时的操作,在线程没有结束之前,外部类退出,此时,外部类不会被释放,因为作为内部类的子线程持有外部类的引用,解决方法和前一条类似。
19. 如果在Activity中播放一个无限循环的动画,在退出Activity时没有在 onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看见动画效果了,并且这个时候 Activity的 View会被动画持有,而View又持有了Activty,最终Activity无法释放。解决方法是在Activity的onDestroy中调用animator.cancel()来停止动画。