Android最佳实践性能(一)管理您的应用程序的内存

管理您的应用程序的内存

随机存取存储器(RAM)是在任何软件开发环境中的宝贵资源,但它更有价值的移动操作系统的物理内存往往是有限的。虽然Android的Dalvik虚拟机执行日常垃圾收集,这并不能让我们忽略何时何地你的应用程序分配和释放内存。

为了使垃圾收集器从您的应用程序回收内存,你需要避免引入内存泄漏(通常由持有到全球成员对象引用导致的),并释放所有 Reference 对象在适当的时候(通过生命周期回调定义下面进一步讨论)。对于大多数应用程序,在Dalvik垃圾收集器会自动完成剩余的工作:系统回收的内存分配时,相应的对象留下您的应用程序的活动线程的范围。

本文档介绍了Android的如何管理应用程序进程和内存分配,以及如何主动减少内存使用量,而开发的机器人。有关Java编程的时候来清理你的资源,一般惯例 ​​的更多信息,请参考其他书籍或有关管理资源参考联机文档。如果你正在寻找有关如何分析你的应用程序的内存中,一旦你已经建立了信息,阅读调查你的内存使用情况

如何Android的内存管理


Android不会为内存提供交换空间,但它使用分页内存映射 (mmapping)来管理内存。这意味着,任何内存修改,是否通过分配新的对象,或触摸mmap函数映射页-仍驻留在内存中,不能调出。所以,从你的应用程序完全释放内存的唯一途径是释放对象引用您可捧,使内存可用于垃圾收集器。这是有一个例外:mmap函数在无需修改任何文件,如代码,可分页的内存,如果系统要在其他地方使用该内存。

共享内存

为了适应它需要在RAM中的一切,Android的尝试共享跨进程内存的页面。它可以通过以下方式这样做的:

  • 每个应用程序的过程是由称为受精卵现有进程分叉。Zygote的过程开始时,在系统启动并加载通用的框架代码和资源(如活动主题)。要开始一个新的应用程序时,系统叉那么受精卵进程加载并在新的进程中运行的应用程序的代码。这使得大部分分配给框架代码的内存页面和资源,以跨所有应用程序共享。
  • 大多数静态数据函数映射到进程中。这不仅让同样的数据在进程间共享,但也允许在需要的时候它被调出。例如静态数据包括:Dalvik的代码(通过将其放置在预先连接的ODEX。直接mmapping文件),应用程序的资源(通过设计资源表是可以函数映射的结构和对准APK的拉链条目)和传统的项目元素,如在本机代码。这样的文件。
  • 在许多地方,Android的共享跨进程相同的动态RAM使用显式分配的共享内存区域(无论是与ashmem或gralloc)。例如,窗户表面使用共享内存的应用程序和屏幕之间的排字和光标缓冲区使用的内容提供者和客户端之间共享内存。

由于大量使用共享内存,确定您的应用程序使用了多少内存,需要照顾。技术,以正确确定您的应用程序的内存使用都在讨论调查你的内存使用情况

分配和回收内存应用程序

下面是关于Android如何分配然后从您的应用程序回收内存的一些事实:

  • 在Dalvik堆为每个进程被限制在一个单一的虚拟内存范围。这个定义的逻辑堆大小,它可以增长,因为它需要(但仅限于该系统定义了每个应用程序的限制)。
  • 堆的逻辑大小是不一样的物理内存所使用的内存量。当检查你的应用程序的堆,Android的计算一个叫做比例设定大小(PSS),占既脏又干净的页面与其他进程,但只有其量是成正比的多少应用程序共享内存共享的价值。这(PSS)的总系统认为什么是你的物理内存占用。有关PSS的更多信息,请参阅你的调查RAM使用指南。
  • 在Dalvik堆不压缩堆的逻辑大小,这意味着Android不会整理堆收涨空间。安卓只能收缩逻辑堆大小时,有未使用的空间在堆的末尾。但是,这并不意味着使用堆的物理内存不能收缩。垃圾收集后,Dalvik的走堆,发现未使用的页面,然后使用和madvise返回这些页面到内核中。因此,成对分配和大块解除分配应导致回收所有(或几乎所有)使用的物理内存。然而,从较小的分配回收内存可以是效率低得多,因为用于小分配的页仍然可以用别的东西,尚未被释放共享。

限制应用程序内存

为了保持功能的多任务环境中,Android的设置上堆大小的硬性限制为每个应用程序。基于设备有多少RAM可用整体设备之间的确切的堆大小限制而变化。如果你的应用程序已经达到了堆能力,并试图分配更多的内存,它会收到一个内存不足错误

在某些情况下,您可能想要查询系统以确定到底你有多少堆空间都可以在当前设备-例如,以确定有多少数据是安全的,保持在缓存中。你可以通过调用查询系统这个数字getMemoryClass() 。这将返回一个整数,指示可用于您的应用程序的堆的兆字节数。这在下面进一步讨论,根据 检查你应该使用多少内存

切换应用程序

如果不使用交换空间,当用户在应用程序之间切换的,Android的保持未托管前景(“用户可见”)在最近最少使用(LRU)高速缓存的应用程序组件的进程。例如,当用户第一次启动一个应用程序,一个过程是为它创建的,但是在用户离开该应用程序时,该程序并没有结束。系统保持过程的缓存,所以如果用户稍后返回到应用程序,该过程重复用于更快的应用程序的切换。

如果您的应用程序有一个缓存的过程,它保留内存,它目前并不需要,那么你的应用程序 - 甚至在用户没有使用它,是制约系统的整体性能。因此,作为系统内存不足时,它可能会杀死进程在LRU缓存中最近最少使用的过程中开始的,但也给一些考虑对哪些进程是最占用大量内存。为了保持尽可能长的过程缓存,照做,在什么时候释放你参考下面的章节。

关于流程如何,而不是在前台运行,以及如何安卓决定哪些可以被杀死缓存的更多信息可在进程和线程的指南。

如何使你的应用程序应该管理存储器


你应该考虑内存的限制在整个开发的所有阶段,包括应用程序的设计过程中(在开始开发之前)。有很多方法可以设计和编写带来更有效的结果代码,通过应用一遍又一遍相同的技术集合。

你应该应用以下技术而设计和实现您的应用程序,使之更高效的内存。

使用尽量少的服务

如果你的应用需要一个服务 在后台进行工作,不要继续运行,除非它主动执行工作。另外要小心因未能阻止它时,它的工作是为了永不泄漏您服务。

当您启动服务时,系统先始终保持过程的服务运行。这使得该方法非常昂贵,因为所使用的服务的RAM不能使用任何其他或调出。这减少缓存过程,该系统可以保持在LRU缓存,使得应用程序交换效率较低的数目。它甚至可以导致颠簸在系统中,当内存紧张,系统不能维持足够的进程当前正在运行的托管的所有服务。

限制你的服务寿命的最佳方法是使用IntentService,它一旦它的完成处理启动它的意图完成本身。欲了解更多信息,请阅读 运行在后台服务 。

离开时,它并不需要运行的服务是最糟糕的内存管理错误之一的Android应用程序可以做到的。所以,不要保持一个服务,为您的应用程序运行的贪婪。它不仅会增加你的应用程序进行尽职不佳到RAM限制的风险,但是用户会发现这种行为不端的应用程序,并卸载它们。

释放内存时,用户界面​​被隐藏

当用户导航到不同的应用程序和你的用户界面不再可见,你应该释放所使用的只是你的用户界面的任何资源。在这个时候释放UI资源可以显著提高系统的容量缓存的过程,这对用户体验的质量有直接的影响。

当用户退出你的UI通知,落实onTrimMemory()在您的回调活动类。您应该使用此方法来侦听TRIM_MEMORY_UI_HIDDEN水平,这表明你的UI,现在从视图中隐藏,你应该释放,只有你的UI使用的资源。

请注意,您的应用程序接收onTrimMemory()回调TRIM_MEMORY_UI_HIDDEN 只有当所有的UI组件的应用程序的过程中被隐藏的用户。这是从独特的onStop()回调,当它被调用的活动实例变为隐藏的,这甚至发生在当你的应用程序到另一个活动的用户移动。所以,虽然你应该实现的onStop()来释放活性资源,如网络连接或注销广播接收器,你通常应该不会,直到您收到释放你的UI资源onTrimMemory(TRIM_MEMORY_UI_HIDDEN) 。这样可以确保在用户导航 ​​从你的应用程序的另一个活动,你的UI资源仍然可用来快速恢复活动。

释放内存作为内存变紧

在你的应用程序生命周期的任何阶段,onTrimMemory()回调也告诉你,当整个设备存储空间不足。你应该作出反应,进一步释放的基础上通过交付以下内存资源水平onTrimMemory() 

  • TRIM_MEMORY_RUNNING_MODERATE

    您的应用程序正在运行,并且不被视为可杀,但该设备运行时内存不足,系统正在积极杀死进程在LRU缓存。

  • TRIM_MEMORY_RUNNING_LOW

    您的应用程序正在运行,并且不被视为可杀,但该设备的内存中运行要低得多,因此您应该释放未使用的资源,以提高系统性能(这直接影响您的应用程序的性能)。

  • TRIM_MEMORY_RUNNING_CRITICAL

    您的应用程序仍在运行,但系统已经杀害了大多数在LRU缓存的过程,所以你现在应该释放所有非关键资源。如果系统无法回收足够数量的内存,它会清除所有的LRU缓存,并开始杀死进程,该系统更愿意留条活命,如举办一个正在运行的服务。

此外,当您的应用程序进程当前缓存,您可能会收到以下级别之一onTrimMemory() 

  • TRIM_MEMORY_BACKGROUND

    该系统运行时内存不足和你的过程是接近LRU列表的开头。虽然你的应用程序是不是在被杀害的高风险,系统可能已经在LRU缓存杀死进程。你应该释放,很容易恢复,以便您的程序会保留在列表中,恢复迅速,当用户返回到您的应用程序资源。

  • TRIM_MEMORY_MODERATE

    该系统运行时内存不足和你的过程是接近LRU列表的中间。如果系统变得进一步限定的内存,有一个机会,你的进程会被杀死。

  • TRIM_MEMORY_COMPLETE

    该系统运行时内存不足和你的过程是先如果系统没有现在恢复内存被杀死之一。你应该放开一切,这并不是恢复您的应用程序状态的关键。

因为onTrimMemory()回调中添加了API级别14,可以使用onLowMemory() 回调作为备用的旧版本,这是大致相当于TRIM_MEMORY_COMPLETE事件。

注意:当系统开始杀死进程在LRU缓存,尽管它的主要工作自下而上,它确实给一些考虑哪些进程正在消耗更多的内存,并因此提供更多的系统存储器的增益,如果被杀。所以,你消耗更少的内存,而在LRU列表的整体,更好的机会都留在列表中,并能够迅速恢复。

请检查您应该使用多少内存

如前面提到的,每个机器人供电设备具有不同量的系统可用的RAM,从而提供一个不同的堆限制为每个应用程序。您可以致电getMemoryClass() ,让您的应用程序的可用堆的估计值以MB为单位。如果您的应用程序尝试分配更多的内存比这里是可用的,它会收到一个 内存不足错误

在非常特殊的情况下,可以通过设置要求较大的堆大小largeHeap 属性为“true”在清单中的<application> 标签。如果你这样做,你可以调用getLargeMemoryClass()来获得较大的堆大小的估计。

但是,要求一个大的堆的能力仅用于一小部分,可以证明,需要消耗更多的内存(如大型照片编辑应用程序)的应用程序。从来没有要求一大堆,只是因为你已经用完了内存,你需要一个快速解决,你应该使用它,只有当你确切地知道你所有的内存被分配,以及为什么它必须被保留。然而,即使当你确信你的应用可以辩解一大堆,你应该避免要求其将一切尽可能。使用额外的内存会越来越多地以整体的用户体验造成损害,因为垃圾收集将需要更长的时间和系统性能可能会比较慢,当任务切换或执行其他常见的操作。

此外,大的堆的大小是不一样的上的所有设备,并于那些内存有限的设备上运行时,大的堆大小可能是完全一样的常规堆大小。所以,即使你做要求大型堆的大小,你应该叫getMemoryClass()来检查的常规堆大小,力求始终保持低于该限制。

避免与位图的内存浪费

当你载入一个位图,只在你需要为当前设备的屏幕分辨率保持在RAM中,缩放下来,如果原来的位图是一个更高的分辨率。请记住,增加位图的分辨率结果在相应的(增加2)在需要的内存,因为无论是X和Y尺寸增加。

注意:在Android上一个2.3.x(API级别10)及以下,位图对象总是不顾形象分辨率显示为相同大小在你的应用程序堆(实际像素数据分别存储在本机内存)。这使得它更难以调试的位图的内存分配,因为大多数堆分析工具看不到本地的分配。然而,在开始的Android 3.0(API级别11),位图像素数据被分配在你的应用程序的Dalvik的堆,改善垃圾收集和调试性。所以,如果你的应用程序使用的位图和你遇到麻烦发现为什么你的应用程序使用的是旧的设备上一些内存,切换到运行Android 3.0或更高版本进行调试的设备。

如需使用位图的详细提示,请阅读常务位图内存

使用优化的数据容器

您可以在Android框架优化的容器,如优势SparseArraySparseBooleanArrayLongSparseArray。通用HashMap的 实现可以是相当低效的内存,因为它需要一个单独的条目对象为每一个映射。此外,SparseArray类是更有效的,因为它们避免了系统的需要autobox 键,有时值(每条目创建另一个对象或2)。并且不要害怕在下降至原始数组时有意义的。

需要注意的内存开销

是熟悉的语言和库您所使用的费用和开销,并保持这些信息牢记,当你设计你的应用程序,从开始到结束。通常情况下,看起来无害的表面上的东西,实际上可能有大量的开销。实例包括:

  • 枚举往往需要超过两倍的内存作为静态常量。你应该严格避免使用枚举Android上。
  • 在Java中每个类(包括匿名内部类)使用大约500字节的代码。
  • 每一个类的实例有12-16字节的RAM开销。
  • 把一个单一的进入一个HashMap中需要额外的条目对象,需要32个字节(见约上一节的分配优化的数据容器)。

这里有几个字节迅速增加,应用程序设计,是类或对象重会患上这种开销。这可以让你在看一个堆分析,实现您的问题的困境是很多使用你的内存小的物体。

请小心使用代码抽象

通常情况下,开发人员使用抽象仅仅作为一个“良好的编程习惯,”因为抽象可以提高代码的灵活性和维护。然而,抽象付出了显著的成本:通常它们需要相当数量更多的代码需要被执行的,因此需要更多的时间和更多的内存用于代码映射到内存中。所以,如果你是抽象,没有提供显著的好处,你应该避免他们。

利用纳米protobufs序列化日期

协议缓冲区是一个语言中立,平台中立的,可扩展的由谷歌设计的序列化结构化数据,认为XML的机制,但体积更小,更快,更简单。如果您决定使用protobufs你的数据,你应该总是使用纳米protobufs在你的客户端代码。定期protobufs产生极其冗长的代码,这会导致多种问题,在您的应用程序:增加内存的使用,显著APK尺寸的增加,执行速度较慢,赶紧打了地塞米松符号极限。

欲了解更多信息,请参阅“纳米版本”部分protobuf的自述

避免依赖注入框架

使用依赖注入框架如吉斯或 RoboGuice可能是有吸引力的,因为它们可以简化你写的代码,并提供了一个自适应的环境,是用于测试和其他配置更改有用。然而,这些框架往往通过扫描你的代码的注释,这可能需要显著数量的代码被映射到RAM中,即使你并不需要它来执行很多进程初始化的。这些映射的页面被分配到干净的内存中,因此机器人可以删除它们,但是这不会发生,直到页面已经被保留在内存中进行的很长一段时间。

请小心使用外部库

外部库的代码往往是没有移动环境下编写并用于移动客户端上工作的时候可能是低效的。最起码,当你决定使用一个外部库,你应该假设你正在服用的显著移植和维护的负担,优化图书馆的移动。规划这项工作的前期和决定使用它之前的所有分析的代码大小和RAM占用方面的库。

甚至据说专为Android上使用的库是潜在的危险,因为每一个库可以做不同的事情。例如,一个库可以使用纳米protobufs而另一个采用微protobufs。现在,你在你的应用程序的两个不同的protobuf的实现。这也将发生与日志记录的不同实现,分析,图像的加载框架,缓存,以及各种你不要指望其他的东西。ProGuard的也救不了你这里,因为这些都将是较低级别的依赖关系是按您想要的库功能所需。这当您使用变得特别有问题的活动 从库的子类(其中往往有依赖关系的大片宽),当库使用反射(这是常见的,意味着你需要手动花了很多时间调整的ProGuard得到它工作),等等。

另外要注意不要陷入使用共享库的一个或两个功能了几十它做其他的东西的陷阱; 你不想拉了大量的代码和开销,你甚至不使用。在一天结束的时候,如果没有一个现有的实现,它是一个强大的对手对于你需要做的,它可能是最好的,如果你创建自己的实现。

优化整体性能

各种有关优化你的应用程序的整体性能信息可以在其他的文件中列出的最佳实践表现。许多这些文件包括对CPU性能优化技巧,但很多这些技巧也有助于优化您的应用程序的内存使用,如通过减少你的用户界面所需的布局对象的数量。

你也应该阅读有关优化你的用户界面的布局调试工具,并采取由所提供的优化建议利用皮棉工具

使用ProGuard的剥离任何不必要的代码

ProGuard的工具收缩,优化,并通过删除未使用的代码和重命名类,字段,以及与语义模糊的名称的方法混淆你的代码。使用ProGuard的可以使你的代码更紧凑,需要较少的内存页面映射。

使用zipalign在最终的APK

如果你做任何后期处理由编译系统(包括与您的最终生产证书签名吧)生成APK,那么你必须运行zipalign上它有它重新对齐。如果不这样做可能会导致您的应用程序需要显著更多的RAM,因为事情像资源不再能够从APK mmap函数。

注:谷歌播放商店不接受未zipaligned的APK文件。

分析你的内存使用

一旦你达到一个相对稳定的构建,开始分析你的应用程序是多少RAM使用整个生命周期的所有阶段。有关如何分析你的应用程序的信息,请阅读调查你的内存使用情况

使用多个进程

如果它是适合您的应用程序,先进的技术,可以帮助您管理您的应用程序的内存被划分你的应用程序的组件集成到多个进程。这种技术必须始终小心使用,大多数应用程序不应该运行多个进程,因为它可以很容易地增加,而不是减少,你的内存占用,如果操作不当。它主要是有用的,可能在后台和前台运行显著的工作,可以分别管理这些业务应用程序。

当多个进程可能是适当的例子是建立一个音乐播放器,可播放的音乐从服务的时间较长时期。如果整个应用程序在一个进程中运行,那么许多对于其活性的UI进行的分配必须保持周围,只要它在播放音乐,即使用户当前在另一个应用程序和服务控制重放。这样的应用程序可以被分成两个过程:一个是它的用户界面,另一个为继续在后台运行的服务的工作。

您可以通过声明指定每个应用程序组件的单独进程的过程:机器人在manifest文件的每个组件的属性。例如,您可以指定您的服务应该在一个进程通过声明名为“背景”的新工艺从您的应用程序的主进程分开(但你可以命名的过程中任何你喜欢的)运行:

<服务 机器人:名称= “。PlaybackService” 
         机器人:工艺= “:背景”  />

你的进程名称应以冒号(“:”),以确保过程的私密性,以您的应用程序。

在您决定创建一个新的进程,你需要了解内存的影响。为了说明每个进程所带来的后果,认为一个空的过程中做的基本上没什么拥有约1.4MB额外的内存占用,所表现出的内存信息转储下文。

adb shell dumpsys meminfo com.example.android.apis:empty

** MEMINFO in pid 10172 [com.example.android.apis:empty] **
                Pss     Pss  Shared Private  Shared Private    Heap    Heap    Heap
              Total   Clean   Dirty   Dirty   Clean   Clean    Size   Alloc    Free
             ------  ------  ------  ------  ------  ------  ------  ------  ------
  Native Heap     0       0       0       0       0       0    1864    1800      63
  Dalvik Heap   764       0    5228     316       0       0    5584    5499      85
 Dalvik Other   619       0    3784     448       0       0
        Stack    28       0       8      28       0       0
    Other dev     4       0      12       0       0       4
     .so mmap   287       0    2840     212     972       0
    .apk mmap    54       0       0       0     136       0
    .dex mmap   250     148       0       0    3704     148
   Other mmap     8       0       8       8      20       0
      Unknown   403       0     600     380       0       0
        TOTAL  2417     148   12480    1392    4832     152    7448    7299     148

注:有关如何读取这个输出的更多信息在提供调查你的内存使用情况。这里的关键数据是私人脏专用清洁内存,这表明该进程正在使用几乎1.4MB的非分页内存(分布在Dalvik的堆,本地分配,簿记和库加载),和另一150K的RAM已映射到执行代码。

这对于一个空进程的内存占用相当显著,它可以迅速成长为你开始做工作,在这个过程中。例如,下面是创建只显示一个活动,在一些文字处理程序的内存使用:

** MEMINFO in pid 10226 [com.example.android.helloactivity] **
                Pss     Pss  Shared Private  Shared Private    Heap    Heap    Heap
              Total   Clean   Dirty   Dirty   Clean   Clean    Size   Alloc    Free
             ------  ------  ------  ------  ------  ------  ------  ------  ------
  Native Heap     0       0       0       0       0       0    3000    2951      48
  Dalvik Heap  1074       0    4928     776       0       0    5744    5658      86
 Dalvik Other   802       0    3612     664       0       0
        Stack    28       0       8      28       0       0
       Ashmem     6       0      16       0       0       0
    Other dev   108       0      24     104       0       4
     .so mmap  2166       0    2824    1828    3756       0
    .apk mmap    48       0       0       0     632       0
    .ttf mmap     3       0       0       0      24       0
    .dex mmap   292       4       0       0    5672       4
   Other mmap    10       0       8       8      68       0
      Unknown   632       0     412     624       0       0
        TOTAL  5169       4   11832    4032   10152       8    8744    8609     134

这个过程现在已经几乎增加了两倍的大小,为4MB,只需通过显示在用户界面中的一些文字。这就导致了一个重要的结论:如果你要你的应用程序分割成多个进程,只有一个进程应当负责用户界面。其他进程应避免任何UI,因为这将迅速提高的过程所需的内存(特别是一旦你开始加载位图资产及其他资源)。然后它可能很难或根本不可能减小的内存使用情况,一旦UI绘制。

此外,运行多个进程时,它是你保持你的代码精简越好,因为任何不必要的内存开销,常见的实现,现在复制到每个进程比以往任何时候都更加重要。例如,如果您使用的是枚举(虽然你不应该使用枚举),所有的RAM的创建和初始化这些常数被复制在每个过程需要的,并且您有任何抽象与适配器和临时使用或其他开销将同样被复制。

与多进程的另一个值得关注的是它们之间存在的依赖关系。例如,如果你的应用程序有您在默认的进程都运行该地还拥有你的UI内容提供商,那么在使用该内容提供商也需要您的UI进程保持在内存中后台进程的代码。如果你的目标是有一个可以独立重量级的UI程序的运行后台进程,它不能对内容提供商或用户界面的过程,执行的服务依存关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值