Android性能调优之内存篇

Ram(运行时内存)在任何软件开发过程中都是一个需要优化使用的资源,尽管Android的Dalvik虚拟机会做垃圾回收工作,但是这并不意味着我们在app开放过程中可以随意的分配或释放内存。为了能让垃圾回收器正常的回收内存,我们需要在合适的时机释放对对象的引用,并尽量避免内存泄漏(最常见的内存泄漏原因是在全局变量中,例如static变量,长期持有一个对象,例如activity实例)下面我们从了解Android如何管理一个app进程以及内存分配开始,学习如何在实际开发中优化内存的使用。

Android如何管理内存


Android没有交换分区(swap space)但是有用到分页(paging)和memory-mapping技术,这就意味着对内存的任何修改,都会导致此部分内存不会被paged out,所以唯一的释放内存的方式就是释放对对象的引用,让垃圾回收器能够回收这部分内存,这也有一个例外:mmapped in的部分,如果没有经过修改,例如code,这部分内存可以paged out。

共享内存

Android 允许不同进程之前共享内存:

  • 每个app进程都是由一个Zygote的进程fork出来的,Zygote进程是在系统启动后自动运行起来的在此进程中加载framework的代码、系统资源并运行,而启动一个app的时候,系统会fork Zygote进程,然后在此新进程中运行app代码,这一机制就允许framework层分配的内存与任何app进程共享。
  • 静态代码数据(例如app中dex转换成odex后的文件,app中的资源文件,以及so库代码文件)不仅可以在进程之间共享,而且可以在系统需要的时候被paged out。
  • Android有一块可以共享的动态内存区域,例如window的surfaces会用这块区域允许各个app进行读取,cursor的buffer缓冲区会利用这个区域允许content provider和client之间共享数据。

正是由于有共享内存区域,所以确定一个app进程到底使用了多少内存才变得如此复杂。

分配和回收App内存有如下场景:

  • Java层的Dalvik堆对每个app进程允许分配的内存会限制在一个虚拟的内存区间内,这个区间叫做逻辑堆(logical heap),它的大小会变化,但是不会超过系统允许每个app使用内存的最大值
  • 所有app进程占用的逻辑堆大小之和,并不等于这些堆占用的物理内存之和,当用工具查看app进程占用的堆大小时,Android会计算出一个PSSProportional Set Size)值来表明一个app进程占用物理内存的大小
  • Android会把逻辑堆中没有用的内存重新回收,当垃圾回收器回收垃圾的时候,会把这些内存还给kernal从而导致物理内存的真正释放

受限制的内存

Android是一个多任务系统,它对每个app可以使用的dalvik堆有一个最大值的限制,这个最大值会根据不同的设备而不相同,如果一个app使用的内存已经达到最大值,那么如果它继续申请内存,系统会收到OutOfMemoryError提示。有些时候开发者可能想知道还有多少可用内存空间,那么可以通过调用系统的getMemoryClass()这个方法,它会返回一个整数来标明剩余的可用内存大小,下面在(了解还有多少内存可以用部分)会有详细讨论。

 

切换app场景:

Android会用一个LRU(least-recently used)数据结构来指示每个app的进程的活跃度,每个前台进程会排到这个LRU队列中的最后面,表示最活跃,也就意味被系统杀死的优先级最低,例如一个app被用户使用时它的进程是前台进程,之后用户切换使用另外一个app,那么前面这个app的进程转到后台但不会马上quit,而是保存在LRU队列里面。在LUR队列最前面的进程意味着用户很久没有使用,那么当系统内存吃紧的时候,系统会优先杀死它,但选择杀死哪个进程的时候系统有一个算法,会计算哪个进程占用的内存比较多,杀死后释放的比较多,因此,为了使app的进程尽量长时间的存活,需要在合适的时机尽量释放一些对象引用。



开发者应该如何管理app内存


尽量不要使Service长期在后台运行

start一个service的时候,系统会尽量保持service所在进程始终在running状态,而且由于service所在进程占用的内存资源不能被paged out,因此在service进程其实是非常“昂贵”的,过多的无用service进程,会严重影响app切换效率。在开发过程中为了尽量减少service的运行时间,最好使用IntentService,因为它当完成后台任务时(handling intent),会结束自己的进程。总而言之,保持一个没有执行实际任务的service进程始终在后台运行是一种严重浪费内存资源的错误,而且用户也可能会发现这种非正常的行为而去卸载整个app。

 

当app界面对用户不可见时释放部分内存

当app界面对用户不可见时,最好释放UI资源,这样可以大大的提高系统可以缓存的进程数量。

开发者可以重写Activity中的onTrimMemory(intlevel)方法,如果系统回调此方法,且判断level是TRIM_MEMORY_UI_HIDDEN时,意味着app的view对用户已经不可见,而且现在内存紧张,最好在此方法中执行一些释放UI资源的操作。

需要注意的是Activity的onTrimMemory()方法的回调和onStop()方法回调还是不一样的,onTrimMemory()的回调是在app的所有UI都不可见的时候才可能发生,而onStop的回调是在Activity本身不可见的时候回调,例如用户从app的一个activity跳转到另外一个activity的时候,所以在onStop回调中,开发者需要释放网络连接,反注册广播监听者等待操作,但是不能释放UI资源。

 

当系统内存资源紧张时释放部分内存

其实还是在onTrimMemory()中,上面提到的是当收到TRIM_MEMORY_UI_HIDDEN事件时候,释放UI资源,下面罗列了系统回调时候onTrimMemory(int level)时不同的level区别:

·      TRIM_MEMORY_RUNNING_MODERATE

app运行良好,不需要被kill,但是系统内存开始吃紧,系统马上要开始kill LRU中不活跃的进程

·      TRIM_MEMORY_RUNNING_LOW

app运行良好,不需要被kill,但是系统内存开始严重不够用,建议app收到此事件开始释放一些无用的资源,增加系统以及app本事的性能

·      TRIM_MEMORY_RUNNING_CRITICAL

app任然在运行,但是系统已经kill掉了许多LRU队列中不活跃的进程,建议app此事释放所有不是关键的资源和对象,如果系统任然不能获取足够的内存释放,那么LRU中的所有进程都会被kill。

以下是当app不在运行,而是它本身就在LRU队列中的情况:

·      TRIM_MEMORY_BACKGROUND

系统内存开始吃紧,并且开始从LRU队列头部kill进程,app所在进程在LRU队列的尾部,建议app此事释放一些容易恢复、重建的资源,已确保app所在进程尽量远离LRU队列的头部(怎么突然感觉有点悲壮。。。)。

·      TRIM_MEMORY_MODERATE

系统内存吃紧,并且开始从LRU队列头部kill进程,app所在进程已经在LRU队列的中部,如果系统继续kill进程,那么很快就会到此app进程。

·      TRIM_MEMORY_COMPLETE

系统内存吃紧,并且开始从LRU队列头部kill进程,app所在进程已经在LRU队列的头部,如果系统继续kill进程,马上被kill掉,建议app此时释放不是关键部分的所有对象。

onTrimMemory()方法是在api14中才增加的,在老版本中可以使用onLowMemory()方法,这个方法调用时机和TRIM_MEMORY_COMPLETE时间的调用时机类似。

了解还有多少内存可以用

开发者可以用getMemoryClass()来了解对于自己的app还有多少可用逻辑内存(返回Mbytes)在一些特殊的场景下,可以在AndroidManifest文件中对<application>增加largeHeap=ture属性,允许app使用更大的内存空间,此时需要用getLargeMemoryClass()来估计可用内存。需要注意的是,不要因为app经常出现OutOfMemory就使用这个属性,因为此属性会导致垃圾回收时间变长,app切换时间变长,甚至影响系统性能。

例如;断当前app剩余内存,决定Bitmap的LRUCache大小:


避免在bitmap上浪费内存资源

尽量在载入bitmap时用合适的分辨率参数,在2.3.x以及以下的系统上(api10),bitmap的像素数据时存在native的堆上(c++层),这样调试起来非常麻烦,从3.0 api11开始,bitmap的像素数据存在dalvik堆上(java层),所以调试bitmap的时候找一台3.0以上系统的设备。

 

小心使用多进程

 使用不慎,多进程可以轻松的增加app的内存占用,使用process tag可以标记一个component运行在另外一个进程:

<serviceandroid:name=".PlaybackService"
         android:process=":background" />

冒号可以确保线程为私有的。

一个不做任何事情的空进程,需要占用至少1.4M的内存资源,运行一个最简单的activity的进程至少需要4M,多进程开发原则:

1. 只有一个进程负责UI交互,如果你的Provider进程持有一些UI,那么任何使用provider的进程都会导致UI进程始终得不到释放。

2. 尽量保持代码的简洁,因为如果一个类在多进程中都被使用,那么它其实是有多份实例(有几个进程使用,就有几个实例)

 

记住以下几点

l  不要总是使用HashMap,Hashmap比较消耗内存,如果key是整数、可以用SparseArray,SparseBooleanArray,LongSparseArray

l  避免使用枚举类型(枚举类型内存占用为static常量的两倍),如果可以的话用static类型代替。

l  每个class(包括匿名内部类)会至少占用500 bytes

l  每个class的实例会至少占用12-16byte

l  如果不是必须,避免抽象方法,抽象方法内存占用和调用效率很低

l  使用google的nano protobufs替代xmls保存格式化数据,更高效,更少字节

l  避免使用Avoid dependencyinjection 框架,例如Guice或者RoboGuice,虽然很流行但是这些框架会导致过多的进程初始化操作,而且需要大量不需要的代码预先mapped in到内存中。

l  记得利用proguard优化代码,可以删除不需要的代码,并且混淆有用的代码。

l  用zipalign优化最终的apk(google play上是不接受没有zipalign优化过的apk的)

 

 

 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值