码迷,mamicode.com
首页 > 移动开发 > 详细

管理App的内存

时间:2016-07-11 17:12:41      阅读:301      评论:0      收藏:0      [点我收藏+]

标签:

https://developer.android.com/training/articles/memory.html#Android

对于任何软件来说RAM都是一个非常重要的资源,但是由于物理内存总是有限的,所以内存对于手机操作系统来说也更加重要。尽管Android的Dalvik虚拟机会执行GC,但是仍然不允许忽略应该在什么时候,什么地方分配和释放内存

为了垃圾回收器能够回收app的内存,需要避免内存泄露(通常是由全局变量持有对象引用引起)和在合适的时候释放引用的对象(如下面会说到的生命周期的回调)对于大部分的app,当应用活动线程相应的对象离开了作用域时,Dalvik垃圾回收器会回收分配的内存

这篇文章解释了Android是怎么管理app进程和内存分配的,和Android开发时应该主动的减少内存的使用,用Java编程时更多关于清理内存的一般实践可以参考其它书本或者在线文档关于管理资源引用的说明,如果你已经创建了一个工程并且正在寻找应该怎样分析你应用的内存,可以参考 [调查应用内存情况](https://developer.android.com/studio/profile/investigate-ram.html)

Android是怎么管理内存的?

Android不提供内存的互换,但是它可以用分页内存映射来管理内存,这意味着你修改的任何内存,不论是分配新对象还是映射页,都会常驻内存而不会被移除,因此从app完全释放内存的唯一方式就是释放你可能持有的对象引用,使得垃圾回收器可以正常回收。这样就导致了一种潜在的异常:当系统内存吃紧,任何被映射但是没有被修改的文件,如代码,会被移除内存

共享内存

为了适配Android对内存的需求,Android通过进程共享内存页,它可以通过如下方式:

  • 每一个app进程都是从已经存在的Zygote进程forked出来,Zygote进程会在系统启动和加载通用的framework代码和资源(如activity的主题)时启动,所以为了开启一个app进程,系统会fork Zygote进程然后在新的进程加载运行app的代码。这样就需要分配给framework代码和资源大部分内存需要被所有的进程共享

  • 大部分静态数据会被映射到进程中,这样不仅可以使得相同的数据在进程间共享同时当必要的时候被移除,比如静态数据包括:Dalvik代码(用于直接映射的预链接的.odex文件),app资源文件(通过设计一个可以被直接映射的资源表或对齐APK的zip entries)和传统的工程元素如.so的本地文件

  • 很多时候,Android通过显示的内存分配(ashmem或gralloc)实现在进程间共享动态内存,比如,系统surface在应用和screen compositor间共享内存,cursor buffers在content provider和客户端间共享内存

由于共享内存的大量使用,需要格外注意应用内存的使用,合理的决定应用内存使用的讨论在 调查应用内存情况

申请和回收内存

下面是一些关于Android是怎么分配和回收应用的实例

  • 每个进程的Dalvik堆栈被限制在一个虚拟内存范围内,它定义了逻辑的堆栈大小,它可以随着它的需要自增长(但是只能增长到系统分配给每个app的上限)

  • 逻辑堆栈大小不同于堆栈使用的物理内存大小,当检查应用堆栈的时候,Android会计算一个叫做Proportional Set Size(PSS)的值,这个值包含被其他进程共享的脏和干净的页,但是它也是按有多少apps共享内存按比例分配的。总的PSS大小才是系统认为的你的app物理内存大小,更多PSS的说明,参考调查应用内存情况

限制应用的内存

为了保证多任务运行环境,Android设置了每一个app的堆大小,准确的堆大小由于每个设备可用内存不一样所以也各有差别,一旦应用达到了堆栈上限,并且试图申请更多的内存,会收到系统OutOfMemoryError错误

一些情况下,你可能需要查询系统来确定你在设备上可用内存的准确数值,比如,为了确定应该缓存多少数据是安全的,可以调用getMemoryClass查询系统得到这个数据,它会返回app可用内存的一个以兆为单位的整型数值,下面将会讨论,检查应该用多少内存

切换apps

当用户切换app的时候,Android不是切换内存空间,而是把没有在forground的进程app切换到一个LRU的缓存中,比如,当用户第一次启动app时,会给它创建一个进程,但是当用户离开app时,进程并没有停止,而是系统缓存了这个进程,所以当用户稍候返回这个app时,进程可以快速被重用

如果app有一个缓存的进程,并且它持有了现在不需要的内存,即使用户没有在用,它也会影响系统的整体性能,所以,当系统内存吃紧时,它会杀死LRU缓存队列中最近最少使用的进程,但是也会考虑杀死内存占用最多的进程,为了保证进程在后台被保存的尽量长,听从下面的建议,关于应该什么时候释放引用

更多关于当app没有处于forground进程是怎么被缓存和Android是怎么决定杀死哪个进程的参考 进程和线程

app应该怎么管理内存

在开发的所有阶段都要考虑RAM的限制,包括在开发之前app的设计,有很多方式可以让你设计和编写代码得到更高效率的结果,尽管应用在聚集越来越多相同的技术

请设计和实现app的时候尽量的遵循下面建议使得内存更加高效

慎用services

如果app需要service在后台执行任务,不要让它一直运行除非它确实在执行任务,小心处理当它工作结束时由于没有关闭导致的service的泄露

当你开启一个service时,系统会尽量保持service的进程正常运行,这就导致了这个进程代价非常昂贵,因为这个service的RAM不可以被别的任何东西使用和替换,也导致了系统保存在LRU cache队列的进程数量会减少,使得app切换效率降低,甚至当内存紧张时会导致系统抖动,并且系统可能无法保持足够的进程来承载当前正在运行所有services

为了限制service的生命最好的方式是使用IntentService,它会结束自己当完成了开启它的intent时,更多信息可以参考 运行一个后台服务

android app容易犯的最糟糕的内存管理的错误就是让一个已经不需要的service一直运行,所以不要为了你app的持续运行通过保持一个持续运行的service实现,由于内存的限制这样不仅会增加你app低效率运行的风险,还会导致用户发现了这样的不好的行为时卸载app

当你的用户界面已经不在前台时释放内存

当用户切换到其它app,你的UI已经不再可见时,应用释放仅仅你的UI占用的所有资源,释放UI资源可以增加系统缓存进程的能力,并且可以很直接的影响用户体验质量

为了通知什么时候用户离开了你的UI,实现Activity的onTrimMomory()回调,用这个方法监听TRIM_MEMORY_UI_HIDDEN,它表明了你的UI是隐藏的,你应该释放只是你的UI占用的资源

需要注意的是,app接收TRIM_MEMORY_UI_HIDDENonTrimMemory()仅在当你的进程对于用户也是隐藏时,它不同于onStop()回调,onStop()回调是在Activity实例隐藏时也就是用户切换到app的另外一个activity时. 因此尽管你可以实现onStop()实例释放activity的资源就像网络连接或者取消注册broadcast receivers,但是你不应该释放你的UI资源除非接收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN),这样就确保了如果用户从另外一个activity返回时,你的UI资源仍旧可用从而使得activity可以快速可见

内存紧张时释放内存

对应app生命周期的各个阶段,onTrimMemory()回调告诉了我们什么时候设备处于低内存的状态,我们应该在收到onTrimMemory()时进一步的释放内存资源

  • TRIM_MEMORY_RUNNING_MODERATE

app正在运行并且系统没有考虑杀死它,但是设备运行内存吃紧,系统正在杀死LRU cache的进程

  • TRIM_MEMORY_RUNNING_LOW

应用正在运行并且系统没有考虑杀死它,但是设备运行内存吃紧,所以应该释放没有用的资源来提高系统运行效率(因为直接影响了app的效率)

  • TRIM_MEMORY_RUNNING_CRITICAL

应用正在运行,但是系统已经杀死了LRU cache的大部分进程,所以应该释放所有没有在临界的资源。如果系统不能回收足够的内存,它会清理所有的LRU cache的进程和一些系统倾向保持的进程,比如那些保持service的进程

同样的,当app进程当前正在被缓存的时候,你可能会收到onTrimMemory()的下面的这些回调

  • TRIM_MEMORY_BACKGROUND

系统内存吃紧并且你的进程处于LRU列表的前面,尽管你的app进程没有被系统杀死那么高的风险,但是系统仍然可能正在杀死LRU cache的进程,你应该释放一些容易恢复的资源以便于你的进程可以保持在LRU列表中,并且当用户返回app的时候你可以快速恢复

  • TRIM_MEMORY_MODERATE

系统运行内存吃紧并且你的进程处于LRU列表的中部,系统开始进一步的释放内存,你的进程有几率被杀死

  • TRIM_MEMORY_COMPLETE

系统运行内存吃紧并且如果系统没有恢复内存的话你的进程是第一个被杀死的,你应该释放所有不严重影响你app恢复的资源

由于onTrimMemory()回调是在API 14之后添加的,在低版本可以用onLowMemory()回调,低版本的回调相当于TRIM_MEMORY_COMPLETE事件

注意:当系统开始在LRU列表杀进程时,尽管它是自上而下的工作,它同样会考虑是哪个进程消耗了更多的内存并且如果杀死哪个会提供给系统更多的内存,因此在LRU列表你消耗越少的内存你越有更多的机会保持在列表中,并且更容易被用户快速恢复

检查应该用多少内存

就像上面提到的,每一个Android设备对系统来说有不同的RAM大小,这也导致了对于每个app不同的堆栈大小,可以调用getMemoryClass()来获得app以兆为单位的可用堆大小。如果app试图申请多于可用的内存大小,系统会报OutOfMemoryError错误

在特殊的情况下,可以在manifest的标签设置largeHeap属性为true申请一个较大的堆大小,如果这么做的话,可以调用getLargeMemoryClass()来获得大致的large堆大小值。

然而,申请大堆的能力仅用于一些可以证明需要更多RAM的app(如一个大图编辑app).不要仅仅是因为你把内存耗尽了所以去申请更大的堆内存,仅仅应该在你确切的知道你的内存被分配在什么时候在哪儿并且为什么它必须被保持。然而即使你可以证明你的app是正当的使用large heap,你应该避免任何时候需要扩展的时候都去申请它,使用扩展的内存会损害用户整体的体验,因为垃圾回收器会花费更长的时间,系统执行也会变慢如任务切换或者其他一些通用的执行

此外,large heap在不同的设备上也不一样,当运行在一个内存吃紧的设备时,large heap可能跟正常的heap大小一样,所以即使你申请了large heap,你应该调用getMemoryClass()检查正常的heap大小并且尽量的低于那个限制

避免bitmap的内存浪费

当你加载一个bitmap的时候,只需要保持当前屏幕分辨率的在内存中,如果bitmap是一个更高分辨率时去缩放它,要知道的是bitmap分辨率的增长代表着内存的增长,因为X和Y的尺寸都在增加

注意:在Android2.3.x(api level 10)以下,bitmap对象总是在app堆出现相同的大小忽略图片分辨率(实际的像素存储在本地内存中)。这导致调试bitmap的内存分配非常的困难,因为大部分的堆分析工具无法看到本地内存分配。然而,Android 3.0(api level 11)之后,bitmap的像素数据是被app的Davlik堆分配的,提高了垃圾回收和调试效率。因此如果你的app使用bitmaps并且你无法发现为什么你的app在一些老设备上正在使用一些内存,可以切换到Android3.0以上的设备debug调试

更多的一些关于bitmap处理的 参考管理bitmap内存

使用优化过的data容器

利用Android framework优化过的容易,如SpareArray,SpareBooleanArray,LongSparseArray.通常HashMap实现是非常消耗内存的,因为它需要为每一个映射创建entry对象,但是SparseArray类却非常高效,因为他们避免系统对key和一些value的自动装箱(它会创建了一个新的对象或两个entry)并且不用担心当它是有意义的数据时转换为原始的arrays

注意内存开销

了解你用的语言和链接库的开销,当设计app时从开始到结束都要记着这件事,通常,表面的一些看起来无害的事可能实际上会开销非常大比如:

  • Enums通常需要比静态变量超过两倍的内存,在Android中应该严格避免使用enums

  • 每一个Java类(包括抽象内部类)使用大约500 byte的代码

  • 每一个类实例花费12-16bytes的内存

  • 把一个单个entry放到HashMap中需要另外分配一个32bytes的entry对象(详细可以看 优化数据容器)

A few bytes here and there quickly add up—app designs that are class- or object-heavy will suffer from this overhead。这会导致你处于一种尴尬的位置:在堆分析器里看到很多小对象占用着你的内存

注意抽象代码的使用

通常情况下,开发者会把抽象认为是良好的代码实践,因为抽象可以提高程序的灵活性和可维护性,然而,抽象却有很大的成本:通常它们需要更多执行的代码,需要更多时间和内存使得代码映射到内存。因此如果抽象没有带来显著的好处的话应该避免使用它

为序列化得数据提供nano protobufs

protocol buffers 是google设计的一种语言无关,平台无关,可扩展的序列化得结构语言-如XML,但是更小、更快、更简单。如果你决定用protocol buffer的数据,应该在客户端代码中中使用nano protobufs。普通的protobufs会生成特别冗长的代码,而这些会导致app各种各样的问题:增加内存占用、apk大小增长、执行速度变慢和快速达到dex限制

更多信息,请查看 protobuf readme

避免依赖注入框架

使用依赖注入框架如Guice或者RoboGuice可能非常吸引人,因为它们可以使得你们写的代码简单并且提供自适应的环境用于测试或者其它配置的变化。然而,这些框架通过扫描你的代码的注释会生成大量的处理流,这需要将大量的代码映射到内存中。尽管你并不需要。这些映射页会被分配到干净的内存以便Android可以回收它们,但是直到它在内存中存在很长一段时间之后才会被回收。

谨慎使用第三方库

很多第三方库不是为移动设备环境写的,所以如果我们把它们用于我们的客户端就会非常的低效。至少当你决定用一个第三方库的时候应该要考虑到你将对这些库有重要的移植和维护的负担。在决定用之前提前计划并且分析这些库的代码大小和内存占用

即使是专门为Android设计的库也存在潜在的风险,因为每一个库在代码编写上可能完全不同,比如一个库可能使用nano protobufs但是另外一个库却用的是micro protobufs,那么现在在你的app中就会有两种不同的protobuf实现。同样会出现的比如对log、分析、图片加载、缓存和其它你想不到的不同的实现。ProGuard也解救不了你,因为这些都是依赖底层库需要的功能。当你使用一个库的Activity的子类的时候这个问题会变的更严重(意味着会有很多的依赖),当依赖库有反射的话(这是很常见的,意味着你将会花费大量的时间调整ProGuard使得库能正常使用)等等

要小心不要落入只使用一个共享库的一两个功能但是其它功能都没用的陷阱。因为你不想要引入你甚至都没用的大量的代码和内存开销。最后,如果没有一个跟你的需求完全匹配的现存的实现的话,最好的办法是自己实现

使用ProGuard去除无用的代码

ProGuard的工具通过移除无用的代码和用语意模糊的名字重命名类名、字段名、方法名实现压缩、优化、混淆代码的目的。使用ProGuard可以使代码更紧凑,使用更少的内存映射页

对最终的APK使用zipalign

如果你需要对系统编译生成的apk做任何处理的话(包括使用你的生产证书签名),必须要做的就是对apk进行zipalign对齐,如果不执行zipalign的话你的app会需要更多的内存,因为资源文件没办法从APK中被映射

注意: google play store不接受没有zipaligned的apk

分析内存占用

一旦你的app达到了一个相对稳定的状态,开始分析你的app在整个生命周期内存占用了多少内存。关于怎么分析app相关信息,请阅读调查应用内存情况

使用多进程

如果对于你的app适用的话,一个先进的技术可以帮助你管理你app的内存,将app的组件划分为不同的进程。这种技术通常来说非常有用,但是大部分的apps不应该多进程运行,因为一旦操作不当它可以很容易的使得app内存增加而不是减少。对于app来说在前后台运行重要的工作并且把这些操作区分开来是非常相当有用的

有一个例子是适合多进程操作的,比如一个音乐播放器,需要在一个service播放音乐很长一段时间。如果整个app在一个进程运行的话,那么为activity UI分配很多资源必须和播放音乐保持的时间一样长,尽管用户已经切换到了另外一个app但是service仍旧在控制播放。这样的app最好用两个进程,一个用于UI,另外一个用于后台service的持续运行

你可以在manifest文件为组件设置android:process属性来设定一个单独的进程。比如,可以设置service应该单独运行一个进程而不是在主进程可以声明一个新的进程比如’background‘(这个名字可以随设置为自己喜欢的随便什么名字)

<service android:name=".PlayService"
         android:progress=":background">

进程名字应该以“:”开头保证这个进程是app的私有进程

在创建一个进程之前,需要了解对内存的影响。为了说明每一个进程的影响,需要知道的是一个空进程什么都不做需要消耗1.4MB的内存,像下面展示的信息

技术分享

注意:更多关于这些数据应该怎么读取的参考调查应用内存情况.这里边关键的数据是Private Dirty和Private Clean Memory,它展示了这个进程消耗了大约1.4M为non-pageable内存(分配了Dalvik堆,native分配,库保存和加载)和150K用于要执行的代码的映射

对于一个空进程来说这些内存占用是非常重要的,因为当你要在那个进程做一些事的时候它可以快速反应。比如,这是一个仅用于显示一个包含一些文本的activity的进程内存占用

技术分享

这个进程大约占用了三倍的大小4M,仅仅是在UI上展示了一些文本。这就导出了一个重要的结论:如果你想把你的app设计为多个进程,应该只有一个进程用于UI,其它进程应该避免使用任何UI,因为进程这将会导致内存的快速增长(尤其是当你开始加载bitmap资源和其它资源的时候)。当UI绘制的时候减少内存占用会变得非常困难

此外,当运行多个进程时,保持代码整洁需要比平时更加重要,因为任何通用实现的没必要的内存开销都会在每一个进程中复制一份。比如,如果你用enums(尽管不应该使用enums).所有的内存需要创建和初始化这些常量,在每一个进程中复制,并且任何适配器和临时的其它抽象开销也同样会被复制

多进程还需要考虑的是它们之间存在的依赖关系。比如,如果你的app在默认进程运行着content provider同时承载着UI,然后使用那个content provider的代码运行在一个后台进程,它需要你的UI进程保存在内存中。如果你的目标是有一个后台进程可以独立运行于一个重量级的UI进程。它就不能依赖于UI进程执行的content provider和service

管理App的内存

标签:

原文地址:http://blog.csdn.net/nuannuandetaiyang/article/details/51879652

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!