Android应用内存管理

jopen 10年前

      内存在任何软件开发环境中都是非常宝贵的资源,尤其是在手机操作系统中。尽管Dalvik虚拟机会通过gc来自动回收资源,但是这并不意味这你可以忽略应用内存的分配和释放,一些被引用的无用对象是不会被gc释放的。 
         Android没有为内存提供交换空间,但是它使用内存分页和内存映射来管理内存。这意味这任何你修改的内存,不论是分配新对象或者修改映射页,都会保 留在内存中。所以唯一的释放app内存的方法就是释放对象的引用,是该对象可以被gc回收。但是有个例外,任何被映射进内存的没有修改的文件,比如代码, 如果系统其他地方需要其所占的内存则可以被调度。

  • 共享内存
    由于本身的需求,Android尝试在不同的进程间共享内存,一般通过下面几种方式:
    1.每一个app进程都是由Zygote进程复制出来的,Zygote进程是在系统启动和加载framework代码和资源时起来的。当新启动一个进程 时,系统会复制Zygote进程,然后在新的进程中执行app的代码。这就允许framework代码和资源占用的大部分内存被所有的app进程共享。
    2.许多静态数据被映射到一个进程中,这不仅可以在需要同样数据的进程之间共享(在Android中,每个应用程序中储存的数据文件都会被多个进程访问: 安装程序会读取应用程序的manifest文件来处理与之相关的权限问题;Home应用程序会读取资源文件来获取应用程序的名和图标;系统服务会因为很多 种原因读取资源),当需要的话其所占的内存也可以被调度给其他地方使用。例如:Dalvik代码(存在于可以被直接映射的预编译后的odex文件 中),app资源(通过结构化设计资源表和对齐apk中的文件),普通的工程元素比如.so中的本地代码。
    3.在许多地方,Android通过共享内存区域实现进程间数据共享(ashmem或者gralloc)。比如,window surface在app和screen compositor之间使用共享内存,cursor buffers在content provider和client之间使用共享内存。
在开发App时可以使用以下技术使App内存使用更加有效率
  • 谨慎使用services
如果你的app需要一个service在后台完成工作,不要一直保持service在后台运行除非它确实在执行工作,要确保工作完成后成功停止service。
当你运行一个service时,系统会选择一直保持service所在的进程运行,这就使这个进程非常耗费资源,因为service使用的内存不能被其他任何地方使用和调度,这也会减少系统在LRU cache中缓存的进程数,使app之间的切换效率变低。
最后的方式来限制service的生命周期是使用IntentService,它会在处理完intent之后关闭自己。
留下一个不需要的service一直在运行是在Android中最糟糕的内存管理错误之一,
    所以不要妄想使用一个service来保持app一直在运行,这样做不仅会增加app因为内存不足而出问题的风险,而且用户还会发现这些表现不好的app并卸载它们。
        
  • 当用户界面隐藏时释放内存
        当用户切换到另一个app你的UI不再可见时,你应该释放只被您的UI使用的任何资源。在这种情况下释放UI资源可以明显地增加系统缓存进程的能力,从而提高用户体验。
        当用户离开您的UI时,Activity的onTrimMemory()方法会执行,你应该使用这个方法去监听 TRIM_MEMORY_UI_HIDDEN,它表示你的UI隐藏了,这时应该释放UI使用的资源。需要注意的是你的app只有在所有的UI组件都不可见 的情况在才会执行onTrimMemory() TRIM_MEMORY_UI_HIDDEN回调,这与Activity的onStop()是不同的,onStop()会在切换到另一个activity 时也调用,所以你可以在onStop回调中释放资源比如网络连接或者注销广播,但一般不应该释放UI资源,这保证了当你通过back键返回时,你的上一个 activity的UI资源是有效的,可以迅速的进行切换。


  • 在内存不足的情况下释放内存
在app的生命周期中,onTrimMemory()回调在设备的内存不足的情况下也会被调用,你应该根据下面的等级来进行相应的资源释放
TRIM_MEMORY_RUNNING_MODERATE
你的app在运行,而且没有被考虑杀掉,但是设备在低内存的情况下运行,系统已经开始杀掉LRU cache中的进程
TRIM_MEMORY_RUNNING_LOW
你的app在运行,而且没有被考虑杀掉,但是设备在更低内存的情况下运行,所以你应该释放不使用的资源来提高系统的表现(它会直接影响你的app的表现)
     TRIM_MEMORY_RUNNING_CRITICAL
        你的app在运行,但是系统已经杀掉了LRU cache中的大部分进程,所以你应该马上释放所有不重要的资源。如果系统不能回收足够的内存,它将会情况所有LRU cache中的进程,并且开始杀掉那些系统应该保持存活的进程,比如那些有正在运行的service的进程。
        同样,如果你的app进程目前正在被缓存,你可以从onTrimMemory()方法中收到下列级别的通知:
     TRIM_MEMORY_BACKGROUND
        系统在低内存的环境下运行,你的进程接近LRU列表的开头。尽管你的进程被杀掉的风险不是很高,但是系统可能已经准备杀掉LRU cache中的进程。你应该释放一些容易恢复的资源,保证你的进程仍然存在于这个列表中,用户可以很快的返回你的app。
    TRIM_MEMORY_MODERATE
        系统在低内存的环境下运行,你的进程解决LRU列表的中间位置,如果系统进一步内存吃紧的话,你的进程可能会被杀掉。
    TRIM_MEMORY_COMPLETE
        系统在低内存的环境下运行,你的进程在系统的首选kill列表中,你应该释放所有对于返回app时不重要的东西。
    
  • 避免Bitmaps耗费内存
     当加载一个bitmap,在内存中只保存对当前设备屏幕所需要的尺寸,如果原图比所需的大,就进行缩放。要记住bitmap尺寸的增加会导致相应的内存增加。
  • 使用优化过的数据容器
     使用经过优化过的数据容器,比如SparseArray,SparseBooleanArray,LongSparseArray。通常的 HashMap实现内存使用是相当低效的,因为它对每一个映射都使用entry对象,另外,SparseArray类更加有效,因为它避免了自动对key 和value的auto-boxing(将原始类型封装为对象类型,比如把int类型封装成Integer类型)


  • 注意内存开销
    了解你所使用语言与库的内存消耗,当设计应用的时候要时刻明确这些信息,表明看起来正常的可能会有巨大的开销,例如下面的几种情况:
     1.枚举的内存消耗是静态常量的两倍以上,你应该严格控制在Android上使用枚举。
     2.每一个java类包括匿名内部类需要大概500字节
     3.每一个类对象消耗12到16字节
     4.在HashMap中存放一个简单对象需要一个额外消耗32字节左右的entry对象
   可能最后app的内存消耗主要是各个地方的小内存累加起来的。
  • 小心代码的抽象
    开发者们经常会使用抽象只是因为它是一个良好的编程实践,因为抽象可以提高代码的灵活性和可维护性。但是抽象可能带来明显的消耗:通常它们需要相当数量的 代码来执行,需要更多的时间和更多的内存来映射代码进内存。所以如果你的抽象没有明显的好处,你应该避免使用它们。

  • 使用nano protobufs序列化数据
     Protocol buffers是一个由google设计的语言中立,平台中立,可扩展机制,用来序列化结构数据.像xml,但更小,更快,更简单.如果你决定使用 protobufs,你应该在客户端代码使用nano protobufs。普通的protobufs生成非常冗长的代码,可能会给app带来各自问题:增加内存占用,apk体积增长,执行慢,并且很快会达到 dex文件的限制。


  • 避免依赖注入框架
    使用依赖注入框架比如Guice或者RoboGuice可能是非常吸引人的,因为它们能简化代码,并且提供自适应的测试和配置环境,但是,这些框架需要执 行许多进程的初始化工作(通过扫描代码中的注解),这会导致明显的被映射进内存的代码增加,尽管有些你并不需要。这些映射的页被分配到clean memory,所以Android可以回收它们,但是这些pages可能会在内存中存放很长时间才会被回收。

  • 小心使用外部库
    外部库通常不是为移动环境写的,在移动客户端上使用可能会导致低效。至少,当你决定要用一个外部库时,你应该承担起移植维护并为移动优化的工作。在决定使用前,为这些工作作计划,分析库大小,内存占用。
    即使为Android设计的库,也可能会带来风险,因为每个库做不同的事情。比如,一个库使用nano protobufs另一个库使用micro protobufs。现在你有两种不同的protobuf实现。这些是不可预料的。ProGuard救不了你,因为这些都是你需要的库的特性需要的低级别 的依赖。当你从库中使用一个Activity的子类(会有大片的依赖)或使用反射或其他,更容易出问题。
    也要小心不要掉入为几十个特性中的一两个特性使用一个共享库的陷阱。你不需要增加一大堆你不会用的代码开销。找了很久也没找到一个与你的需求非常符合的现有实现,自己创建实现也许是最好的。


  • 使用ProGuard剔除不需要的代码
    ProGuard工具通过移除没有用到的代码,重命名类,方法,变量为语义模糊的名称来缩减,优化和混淆你的代码。使用Proguard会使你的代码更加紧凑,使用更少的内存来映射。
在最终的Apk上面使用zipalign
    如果你编译系统生成的apk做处理(包括签名),你必须使用zipalign工具来让apk重新对齐。不这样做的话会使你的app消耗更多的内存,因为资源文件可能不需要被映射。Google Play不接受没有被zipaligned的apk

  • 使用多个进程
        如果合适,把app的组件分到不同的进程中可能会对内存有帮助。这个技术必须小心使用,大部分的app不应该运行在多个进程中,因为如果使用不当的话很容 易就会增加而不是减少app的内存,它对于后台工作像前台工作一样多的app是有用的,可以对后台和前台的工作分开管理。
        一个多进程的例子就是当因为播放器在后台使用service长时间播放音乐时,如果所有的app都在一个进程中,那么给activity UI分配的内存就必须在音乐播放期间被保持,即使用户目前在其他的app,services在后台控制播放。像这样的app可以被分为两个进程:一个UI 进程,一个后台service进程。
        当你决定要创建一个新的进程,你应该理解新进程占用的内存。一个没有做任何事情的空进程占的内存大概为2.4m,当你开始在进程中工作的时候内存数据会明 显增长,比如当显示一个显示一些文字activity的时候,内存会增长到5m左右。当你把应用分成多个进程,只有一个进程应该和UI有关,其他进程应该 避免UI显示,因为这会显著增加进程的内存,当UI绘制出来后就很难减少内存的使用。
        另外,当使用多个进程时,你应该保证代码的简洁,因为任何公共实现不必要的内存消耗,都会在每个进程中都复制一份。例如如果你使用枚举,每一个进程会重复创建和初始化这些常量,耗费的内存也会加倍。
        另外需要注意的问题是进程之间的依赖,举个例子,如果在UI进程中有一个contentProvider,同时在后台进程中使用这个 contentProvider也会保持UI进程持续存在内存中。如果你的目标是有一个后台进程可以独立与重量级的UI进程,后台进程不能依赖运行在UI 进程的content provider或者service。