RAM管理是开发者们从程序设计到上线运营都要头疼的一块,实际开发中,机佬们各取手段,不断变换设计和实现方式,而这些方式的技术要点总结起来主要为以下十五招。
1.Service的合理使用
很多developer都会使用Service与android内存管理员拉锯作战,这是邪教武功,不宜长久。一个service被启动后,系统在资源紧张释放内存时,会一直保留Service所在的进程,使进程运行的代价提高,减少了系统能存放到LRU缓存当中的进程数量,进而影响app之间的切换效率,用户就会发现,有几款app常驻后台,而现在手机已经很卡了,马上就会考虑卸载它们。
一个清爽的app中,service的使用方式是IntentService,它会在处理完交给它的intent后尽快结束自己。
2.UI隐藏时释放内存
当应用被切换并不可见后,释放UI占用的资源,可以很大程度的提高系统缓存进程的能力,用户体验就来了。
需要实现Activity类中的 onTrimMemory( ) 回调方法,在这个方法中,监听 TRIM_MEMORY_UI_HIDDEN级别的回调,通过这可以接收用户离开UI的通知。
与onStop( ) 中释放资源不一样,onStop( ) 会在你的同一个app内某一个Activity跳到另一个Activity时调用,并释放Activity里的资源,如网络连接、unregister广播接受者等。除非收到onTrimMemory( ) ,否则不应该释放UI资源,这可以确保用户从当前app内其它Activity切换回来时,Activity能迅速恢复。
3.内存紧张时释放部分内存
app运行时,在生命周期的任何阶段,都可以调用onTrimMemory( ) 获取整个设备内存资源状况。根据不同的状况,灵活使用手段,可以提高app存活率和体验效果。
TRIM_MEMORY_RUNNING_MODERATE
你的app正在运行并且不在死亡清单中。但是,系统处于低内存状态,开始触法杀死LRU Cache中的Process机制
TRIM_MEMORY_RUNNING_LOW
你的app正在运行并且不在死亡清单中。但是,系统处于更低内存状态,应该释放不用的资源以提高系统性能(但是会直接影响你的app的性能)。
TRIM_MEMORY_RUNNING_CRITICAL
你的app仍在运行,但是,系统已经杀死LRU Cache中的大多数进程,如果系统回收不到足够的RAM数量,将会清除所有LRU缓存中的进程,并且开始杀死之前判断不应该杀死的进程,如:包换正在运行状态的Service的进程。
此时应该释放所有非必须的资源,维持系统生态的和谐。
当你的app正在被cached时,可能收到以下几种状态值:
TRIM_MEMORY_BACKGROUND
系统处于低内存状态,你的app处于LRU缓存名单中最不容易杀掉的位置。
TRIM_MEMORY_MODERATE
系统处于低内存状态,你的app处于LRU缓存名单中部位置。应该释放不用的资源以提高系统性能(但是会直接影响你的app的性能)。
TRIM_MEMORY_COMPLETE
系统处于低内存状态,你的app处于LRU缓存名单中最容易杀掉的位置。此时应该释放掉任何不影响app恢复状态的资源以保全自己。
4.扩展heap空间
不同的设备为app提供的不同的heap限制,可以使用getMemoryClass( ) 来获取app的可用heap大小。如果你的app尝试使用更多的内存,就会出现OOM。
特殊的应用需要使用更大heap时,可以在manifest的application标签下添加 "largeHeap = true" 的属性来声明一个更大的空间,通过getLargeMemoryClass( ) 来获取一个更大的heap size。
这个方法如非必要,不可轻易使用,否则造成资源冗余,你的app的存在会影响整个系统的体验,每次GC运行时间更长,任务切换时系统性能也会降低,间接增加了app被卸载的危险。
5.bitmap合理使用
加载一个bitmap时,仅仅需要保留适配当前屏幕分辨率的数据即可。增加bitmap的尺寸会对内存呈现2次方的增加,因为XY都在增加。
图片加载功能的实现,建议使用ImageLoader框架,如Picasso、Glide等成熟的框架。
6.使用优化的数据容器
利用Android Framework里优化过的容器类,如SparseArray、SparseBooleanArray、LongSparseArray。
通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实力对象来记录Mapping操作。另外,sparseArray更加高效在于它们避免了对key、value的autobox,并避免了装箱后的解箱。
7.时刻注意各种内存开销
许多小细节都可能会导致大量的内存开销,因此必须对语言与库的开销有所了解,内存开销的控制应该贯穿始末。如:Enums的内存消耗通常是static constants的2倍,应该尽量避免在android中使用enums;Java中的每一个类(包括匿名内部类)都会使用大概 500 bytes;每一个类的实例产生的花销是12~16bytes;往HashMap中添加一个entry需要额外占用的32 bytes的entry对象等。
8.注意代码“抽象”
代码抽象会提升代码的灵活性和可维护性,但会导致一个显著的开销:通常它们需要同等量的代码用于可执行,那些代码会被map到内存中。因此,如果抽象没有显著提升效率,应该尽量避免它们。
9.为序列化的数据使用nano protobufs
通常的协议化操作会产生大量繁琐的代码,app就有很多隐藏的弊端:增加RAM使用量,APK太肥,执行速度缓慢,容易达到DEX字符限制等。
protocol buffers 是google为序列化结构数据而设计的,与语言、平台等无关,类似xml,却更轻量、快速、简单。如果数据需要实现序列化,那么代码中应该经常出现 nano protobufs。
10.避免使用依赖注入框架
现在有很多优秀的框架,能提升开发体验、减轻开发量、让代码更优雅,但是这些框架会通过扫描你的代码,执行很多初始化的操作,这回导致你的代码需要大量的RAM来map代码。但是,mapped pages会长时间的被保留在RAM中。
11.谨慎使用外部库
很多library的代码都不是为移动开发环境编写的,因此,运用到移动开发时会影响app的效率。即使针对android而设计的library,也会因为几个library所做的事不一样,从而影响到你的app。如一个lib使用的是nano protobufs,另一个lib使用micro protobufs,那么,你的app中就会有两种protobuf实现方式。这样的冲突可能会发生在输出日志、加载图片、缓存等模块上。
因此,如果不是需要大量使用这个库来达到开发效果,只为了一个两个功能而导入整个library,是非常不明智的,此时自己实现这个功能是最佳选择。
12.整体性能的优化
谷歌官方列出了许多优化整个App性能的文章,诸如 Best Practices for Performance。
还有些文章是讲解如何优化App 的CPU使用效率,有些是讲解如何优化App 的内存使用效率。
optimizing your UI是讲解如何为layout进行优化。
13.使用ProGuard剔除冗余代码
ProGuard可以通过剔除不需要的代码,重命名类、域与方法等对代码进行压缩、优化与混淆。可以使用更少的mapped代码所需要的RAM。
14.对最终的apk使用zipalign
zipalign可以对apk二次校准,减少app需要的RAM的。
15.使用多进程
特殊需要才可用这一招,因为大多数的app如果使用多进程,反而会增加内存的使用。当app需要在后台运行与前台一样的大量的任务时,可以考虑使用这个技术。如音乐播放的app,如果整个app运行在一个进程中,后台播放时,前台的UI无法释放,增加RAM负担。这样的app可以分成两个进程,一个操作前台UI,另一个用于后台Service。
可以在manifest文件中声明"android:process"属性来实现某个组件运行在另外一个进程的操作:
以上十五招需合理使用,开发过程中时刻考虑内存状况,各个阶段都做好内存管理,这样的app才会更健壮、更简洁、更高效。