有不少朋友都问过我,怎样才能写出高性能的应用程序,如何避免程序出现OOM,或者当程序内存占用过高的时候该怎么样去排查。确实,一个优秀的应用程序,不仅仅要功能完成得好,性能问题也应该处理得恰到好处。为此,我也是阅读了不少Android官方给出的高性能编程建议,那么从本篇文章开始,我就准备开始写一个全新系列的博文,来把这些建议进行整理和分析,帮助大家能够写出更加出色的应用程序。
注意本系列文章的内容基本源于Android Doc,如果想要阅读更加详细的关于性能方面的资料,可以直接去阅读Android官方文档。
内存(RAM)对于任何一个软件开发环境都是种非常珍贵的资源,而对于移动操作系统来讲的话,则会显得更加珍贵,因为手机的硬件条件相对于PC毕竟是比较落后的。尽管Android系统的虚拟机拥有自动回收垃圾的机制,但这并不代表我们就可以忽视应该在什么时候分配和释放内存。
为了使垃圾回收器可以正常释放程序所占用的内存,在编写代码的时候就一定要注意尽量避免出现内存泄漏的情况(通常都是由于全局成员变量持有对象引用所导致的),并且在适当的时候去释放对象引用。对于大多数的应用程序而言,后面其它的事情就可以都交给垃圾回收器去完成了,如果一个对象的引用不再被其它对象所持有,那么系统就会将这个对象所分配的内存进行回收。
我们在开发软件的时候应当自始至终都把内存的问题充分考虑进去,这样的话才能开发出更加高性能的软件。而内存问题也并不是无规律可行的,Android系统给我们提出了很多内存优化的建议技巧,只要按照这些技巧来编写程序,就可以让我们的程序在内存性能发面表现得相当不错,下面我们就来一一学习一下这些技巧。
如果应用程序当中需要使用Service来执行后台任务的话,请一定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完之后去停止Service的时候,要小心Service停止失败导致内存泄漏的情况。
当我们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会导致这个进程变得非常消耗内存。并且,系统可以在LRU cache当中缓存的进程数量也会减少,导致切换应用程序的时候耗费更多性能。严重的话,甚至有可能会导致崩溃,因为系统在内存非常吃紧的时候可能已无法维护所有正在运行的Service所依赖的进程了。
为了能够控制Service的生命周期,Android官方推荐的最佳解决方案就是使用IntentService,这种Service的最大特点就是当后台任务执行结束后会自动停止,从而极大程度上避免了Service内存泄漏的可能性。关于IntentService更加详细的用法讲解,可以参考《第一行代码——Android》的9.5.2节。
让一个Service在后台一直保持运行,即使它并不执行任何工作,这是编写Android程序时最糟糕的做法之一。所以Android官方极度建议开发人员们不要过于贪婪,让Service在后台一直运行,这不仅可能会导致手机和程序的性能非常低下,而且被用户发现了之后也有可能直接导致我们的软件被卸载(我个人就会这么做)。
当用户打开了另外一个程序,我们的程序界面已经不再可见的时候,我们应当将所有和界面相关的资源进行释放。在这种场景下释放资源可以让系统缓存后台进程的能力显著增加,因此也会让用户体验变得更好。
那么我们如何才能知道程序界面是不是已经不可见了呢?其实很简单,只需要在Activity中重写onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发了之后就说明用户已经离开了我们的程序,那么此时就可以进行资源释放操作了,如下所示:
除了刚才讲的TRIM_MEMORY_UI_HIDDEN这个回调,onTrimMemory()方法还有很多种其它类型的回调,可以在手机内存降低的时候及时通知我们。我们应该根据回调中传入的级别来去决定如何释放应用程序的资源:
以上是当我们的应用程序正在运行时的回调,那么如果我们的程序目前是被缓存的,则会收到以下几种类型的回调:
当我们读取一个Bitmap图片的时候,有一点一定要注意,就是千万不要去加载不需要的分辨率。在一个很小的ImageView上显示一张高分辨率的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存。需要仅记的一点是,将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,可能一张图片只有100k你觉得它并不大,但是读取到内存当中是按照像素点来算的,比如这张图片是1500*1000像素,使用的ARGB_8888颜色类型,那么每个像素点就会占用4个字节,总内存就是1500*1000*4字节,也就是5.7M,这个数据看起来就比较恐怖了。
至于如何去压缩图片,以及更多在图片方面节省内存的技术,大家可以去参考我之前写的一篇博客 Android高效加载大图、多图解决方案,有效避免程序OOM 。
Android API当中提供了一些优化过后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用这些API可以让我们的程序更加高效。传统Java API中提供的HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。
我们还应当清楚我们所使用语言的内存开支和消耗情况,并且在整个软件的设计和开发当中都应该将这些信息考虑在内。可能有一些看起来无关痛痒的写法,结果却会导致很大一部分的内存开支,例如:
许多程序员都喜欢各种使用抽象来编程,认为这是一种很好的编程习惯。当然,这一点不可否认,因为的抽象的编程方法更加面向对象,而且在代码的维护和可扩展性方面都会有所提高。但是,在Android上使用抽象会带来额外的内存开支,因为抽象的编程方法需要编写额外的代码,虽然这些代码根本执行不到,但是却也要映射到内存当中,不仅占用了更多的内存,在执行效率方面也会有所降低。当然这里我并不是提倡大家完全不使用抽象编程,而是谨慎使用抽象编程,不要认为这是一种很酷的编程方式而去肆意使用它,只在你认为有必要的情况下才去使用。
现在有很多人都喜欢在Android工程当中使用依赖注入框架,比如说像Guice或者RoboGuice等,因为它们可以简化一些复杂的编码操作,比如可以将下面的一段代码:
ProGuard相信大家都不会陌生,很多人都会使用这个工具来混淆代码,但是除了混淆之外,它还具有压缩和优化代码的功能。ProGuard会对我们的代码进行检索,删除一些无用的代码,并且会对类、字段、方法等进行重命名,重命名之后的类、字段和方法名都会比原来简短很多,这样的话也就对内存的占用变得更少了。
这个技巧其实并不是非常建议使用,但它确实是一种可以帮助我们节省和管理内存的高级技巧。如果你要使用它的话一定要谨慎使用,因为绝大多数的应用程序都不应该在多个进程当中运行的,一旦使用不当,它甚至会增加额外的内存而不是帮我们节省内存。这个技巧比较适用于那些需要在后台去完成一项独立的任务,和前台的功能是可以完全区分开的场景。
这里举一个比较适合去使用多进程技巧的场景,比如说我们正在做一个音乐播放器软件,其中播放音乐的功能应该是一个独立的功能,它不需要和UI方面有任何关系,即使软件已经关闭了也应该可以正常播放音乐。如果此时我们只使用一个进程,那么即使用户关闭了软件,已经完全由Service来控制音乐播放了,系统仍然会将许多UI方面的内存进行保留。在这种场景下就非常适合使用两个进程,一个用于UI展示,另一个则用于在后台持续地播放音乐。
想要实现多进程的功能也非常简单,只需要在AndroidManifest文件的应用程序组件中声明一个android:process属性就可以了,比如说我们希望播放音乐的Service可以运行在一个单独的进程当中,就可以这样写:
这里指定的进程名是background,你也可以将它改成任意你喜欢的名字。需要注意的是,进程名的前面都应该加上一个冒号,表示该进程是一个当前应用程序的私有进程。