喜糖

移动开发工程师 。涉及 android、ios、jni

导航


Google Developing for Android 二 - Memory 最佳实践

  |   分类于 Android最佳实践

原文:Developing for Android, II
The Rules: Memory

在决定应用的行为,是否有好的用户体验以及整体的设备体验来说,内存的使用可能是独立因素中最重要的。内存因素包括应用的内存占用,以及内存搅动(导致的垃圾回收会对运行期间的性能有影响)。

避免在循环中分配内存

内存分配虽然不可避免,但是应尽可能的避免,特别是在平凡的调用的代码块中。比如在绘制代码中,因为每一帧的渲染都会执行该方法。

避免在自定义View的onDraw方法中分配内存,因为动画也许会调用它。使用缓存对象替换临时对象可以避免新的内存开销。典型的例子就是在onDraw方法中分配了一个新的Paint对象,因为Canvas需要一个Paint对象。对于这种自定义View的实例只分配一个独立的Paint对象而后在onDraw方法中临时使用更好。

尽可能的避免内存分配

避免常量,临时变量的内存分配。下面有一些可参考的策略,也许并不适用于传统的java编码,但是对于Android开发是推荐的。通常可以使用工具帮助我们去决定是否某一块代码需要优化。如果代码的某一部分很少执行(比如用户改变一些设置的操作),更简洁和传统的抽象层是不错的选择。但是如果分析表明某些代码频繁执行并导致了大量的内存搅动,考虑以下策略:

  • 对象缓存
    重用对象在一些常量内存的再分配中很有用,比如在内部循环中避免内存分配。比如,有些频繁调用的方法中可能需要一个Rect对象存储一些中间值,最好在把Rect作为类级别的常量,只分配一次内存,甚至是静态的,避免每次方法调用的时候都分配。关于单例的一些警示对于这种方式也是适用的,在Android上,静态的常见缺陷就是它们对于某一个进程是静态的,但是可能有多个活动在同一进程中。小心应用,这种技术在避免内存的再分配中是通用的

  • 对象池
    如果代码临时需要同一种类型的多个对象,考虑适用对象池而不是频繁的分配内存。但对象池可能不容易管理。如果对象有状态并且又是被任意线程访问的情况,要注意一些并发性的问题。在内存压力方面也有问题(可以使用LruCache策略),对象的增加存在着内存泄漏的风险,因此当你使用对象池的时候注意这些问题,考虑只在特定的情况下使用它。如果这种策略在那些内存配置低低老版本或者设备上非常有用,你应该通过API版本或者isLowRamDevice()方法来检测对象池的使用限制

  • Arrays
    ArrayList是一种很便利的集合也不会造成太多的分配。它会再分配,并且会复制当前的数组添加到列表后面,但是设置一个合理的初始化容量可以避免频繁的分配内存。如果你的集合不需要动态的变化大小,考虑使用Array

  • Andorid集合类
    除非你需要一个map去存储大量的数据,否则考虑使用ArrayMap或者是SimpleArayMap作为数据结构而不是HashMap。这些类是经过优化的,相对于HashMap会有更高的内存效率以及更少的GC压力,这样的数据结构在移动设备上能够更好的满足通用的使用情景(另外它们也支持实体的遍历而不使用Iterator)。当然,考虑设置一个恰当的初始容量去避免自动扩容

  • 需要修改对象的方法
    这种情况你不应该不要返回一个新的对象,考虑将该对象作为参数传入进来,去修改该对象:

 
Rect getRect(int w, int h)
 
 getRect(Rect ,  w,  h)
  • 当原始类型可以满足时避免使用对象类型 使用Integer,Float而不是int,float时,会导致内存分配,自动装箱以及更多对象自身内存的分配。比如,如果你的方法中一个Integer,然后代码调用附带了一个int,由于自动装箱,将会导致一个内存分配。带有传统类型的集合类和泛型可能不可避免的,但是当原始类型可以搞定的时候应该避免包装类型。Android中提供了一些像SparseIntArray和SparseLongArray的集合类,它们内部使用的就是原始类型而不是对象类型。
  • 避免对象数组 如果你有一组简单的数据对象,可以考虑将每一个字段存储到数组中。比如,假设你绘制的时候需要追踪一些之前的touch X/Y的point数据。你应该使用两个int[]来保存它们,一个用于存储X坐标值,另一个用于存储Y坐标值,而不是使用一个Point[]。这样不仅仅减少了原始对象的个数(节省了内存),也增加了数据的局部性,可以更好的利用宝贵的内存和CPU的缓存。

避免Interators

明确的(List.iterator())或者不明确的(for(Object o : myObjects))使用Interator会导致一个Interator对象的内存分配。单独一个内存分配不是什么大事,但是应该尽量避免在内部循环中分配内存。当然,直接的使用角标进行集合遍历可以不用分配任何的内存。

 
final  count = myList.();
( i = ; i < count; ++i) {
Object o = myList.get(i);


值得注意的是,Interator总会导致一个内存的分配,即使是空集合。因此,如果当你非要使用foreach的时候,应该在遍历集合的之前可以进行一次isEmpty的检验。

枚举通常可以用来代表常量,但是会比原始类型耗费更多,它涉及到代码量的大小和枚举对象内存的分配。

一个临时的枚举不会造成较大的内存消耗。但是Proguard会,在一些情况下他会进行一些静态的分析所有的代码,将枚举优化为int值。当枚举在整个应用中被被广泛的使用或者当一个library或某个API中的枚举被其它很多应用使用的时候时就是问题了,甚至会很糟糕。

使用AndioStudio1.3版本中的@IntDef注解能够保证你的代码在build时期是类型安全的(当lint error开启的时候)。因此使用int变量对于性能和代码量都会更好。

避免非移动应用的Frameworks和Libraries

有时会使用一些熟悉的java平台的一些框架,比如注解依赖的Guice。但是它并没有为移动应用进行优化,使用它们将会导致一些问题。

如果你只是使用了某个Library中的一小部分,你可以试着将那一部分抽取出来。即使Proguard在很多情况下可以跳过那些不用的代码,但是在大的library的依赖图可能会导致优化失败(也会大大增加Proguard的build时间)。

有一些libraries虽然被引入到Android应用中,但是你不应该随意使用,除非很熟悉它,知道它可能为应用带来的问题。

还有些问题就是使用那些非移动的框架和库可能会增加内存的开销。你可以通过监视内存的使用和垃圾回收器的行为来检测它们导致的问题程度。

避免静态的内存泄漏

对于避免临时的内存分配使用static对象很有用。但是应该注意使用静态变量去缓存对象时,它们实际上不应该一直存在整个进程的生命周期中。特别的,这些static的变量不应该和Activity的生命周期一致。比如,当屏幕方向改变的时候Activity会destroy并recreate,但是static变量持有Activity的引用,这样会导致内存泄漏。Activities是非常耗资源的,这种内存泄漏很快会导致你的应用和系统OOM。

避免Finalizers

因为和java语言的席位差别,finalizers需要的不是一个垃圾回收器,而是两个。这就意味着不仅资源会被finalizer会被冻住直到两个垃圾回收器都触发的时候,而且系统中同时运行两个垃圾回收器也会导致资源消耗和卡顿。有一种特殊情况需要finalization,当你的对象持有一个本地的指针时。如果没有这样的情况,就可以完全避免finalizers。

如果你确实需要finalizers,考虑实现AutoCloseable接口并且在你的代码域内通过close方法释放所有的资源。

避免过度的静态初始化

在你的应用中一些重要的时间内(比如启动的时候),过多的初始化可能导致性能的问题和较差的用户体验。可以在你需要它的时候再去加载代码。

通过命令释放缓存

从API 14,ComponentCallbacks2提供了onTrimMemory()回调方法允许你的app在较低内存压力的情况下释放内存。更多详情可以参考Google I/O 2012的Video Doing More with Less,展示了一个如何LruCache处理bitmap的例子。

使用 isLowRamDevice()

KitKat版本中出现的ActivityManager.isLowRamDevice()方法可以帮助你检测应用运行时的内存限制.当你的应用中有一些特性比较好内存的时候u,可以通过这种方式去检测内存是否可以满足你的特性,然后决定是否开启。

避免请求较大的Heap内存

应用可以通过在mainfest文件中设置application tag来开启请求较大heap内存的功能,但是你不应该这么做。请求一个大的Heap的行为可能着该应用只考虑到自己的需求,但是对于整个设备度体验来说是一个错误度决定。

请求大的Heap在很少的情况下可能是必要的,比如media内容的处理。但是应用使用该功能只是为了更好的管理内存和资源而不是导致整个设备的用户体验变得更差。应用请求较大的heap将会导致设备上其它的进程拥有更少的内存,用户在切换activity的时候就有可能导致其它应用被kill掉然后重启。

避免在必要情况外过长的运行service

每一个进程在系统中都有一个资源的限制。如果你不需要service在后台一致运行,就及时将它关闭。

Android提供了很多机制来确保组件只在特定的范围内运行:

  • 使用BroadcastReceiver去接收那些重要但是不频繁的事件,而不是使用一个大多数时间都无用的Service,比如network状态变化或者alarm。App可以在不需要但时候 关闭BroadcastReceivers,以至于系统只在需要的时候才被唤醒。
  • 使用IntentService实现一个service,这样的service会在任务栈为空的时候自动关闭

优化代码大小

瘦身的应用会运行更快。加载的代码量越少,用户下载你的app的时间就会越少,你的应用也会更快的启动和初始化。下面是一些建议:

  • 使用Proguard剔除无用的代码。使用Gradle也可以,而且它还会从你依赖的libraries中剔除那些无用的代码
  • 谨慎依赖 当你只是需要某一种指定的数据类型的时候不要使用那些拥有各种集合较大的library。
  • 确保你自己理解那些自动化生成代码的消耗
  • 越简单越好,直接解决问题,而不是创造出大量的结构和抽象去解决问题