Android内存优化

前言


手机极大的方便了和丰富了我们的生活,随着乔布斯改变世界的iOS操作系统的发展和android系统的扶摇直上,越来越多的人在手机上花费越来越多的时间,去做越来越多的事情。这就是移动互联网给我们的生活带来的改变。手机,已经成为让我们又爱又恨的特别的贴身工具,它可以是书本,是地图,可以是游戏机,随身听,可以是摄像机,钱包,报纸,健康秘书,画布,交流通讯,打车工具,订菜。。。 可以说,你想要的,它基本都能提供。这也就造就了一个丰富多彩的手机应用市场,只要有需求,我们程序员就能给你造就一个最快捷和便利的app,你需要做的,就是拿起手机,打开app,点一点,趣味横生,便利万分,效率超出你的想象。正因为不同的app有那么多丰富多采的功能,一部手机,代替了传统世界的很多物品,直接颠覆。

用户喜爱手机,依赖手机。自然而然,也会被资本市场所选择,繁盛的移动互联网领域,会挖掘用户所有可能在手机上实现并大大提升效率的需求,一一实现,一旦成功,便是端上了移动互联网的金饭碗。app如雨后春笋般,繁盛的增长着。可以看下下图的数据,ios和android 的app数量,早已经上百万级别!

Android内存优化_第1张图片
app数量


app这么多,当然用户可以根据根据自己的需求,选择自己有需要的app去安装和使用,这是一场声势浩大的app之战。所有的公司都希望自己的app能够跻身于用户的手机桌面上,希望能够得到主人多一点点的关注。

可是,手机毕竟体量小啊,当人的兴趣不断的挖掘,生活需求不断增长,app的数量不断增加,手机还装得下吗?手机的cpu够用吗?内存够用吗?手机能够完成自己的使命,保证稳定运行,满足用户所有的需求吗?

这就是我们身为开发人员要非常注意的问题,因为手机性能的限制,我们需要格外地关注自己的app的性能,本文着重讲一讲app内存的故事。

android内存限制


事实上,为了保证手机的流畅运行,android会公平地为每一个app分配一个可用的内存,不管现在手机的空闲内存是否多,就算整个手机只有你一个app,也不会允许你肆意地把所有内存都占用掉,你也不能太霸道是吧,不然,用户再想装一个app咋办。

有几个重要的指标:堆分配的初始大小(heapstartsize),单个应用程序最大内存限制(heapgrowthlimit),单个java虚拟机最大的内存限制(heapsize)

这些指标的大小跟机型有关系,比如我的OPPO R8207手机参数如下:

heapstartsize:12M 

heapgrowthlimit:192M

heapsize: 512M

adb查询方法:

查看单个应用程序最大内存限制: adb shell getprop|grep heapgrowthlimit

应用启动后分配的初始内存:adb shell getprop|grep dalvik.vm.heapstartsize

单个java虚拟机最大的内存限制:adb shell getprop|grep dalvik.vm.heapsize

这几个指标会如何影响app的内存分配呢?

请看的下图:

Android内存优化_第2张图片
app内存增长曲线

从上图可以看到,从app的启动到使用过程中,它占用的内存是从初始值开始,缓缓增长的。

增长的过程,也不会爆发式地增长,增长的过程会受到heap的最小空闲值,最大空闲值,目标利用率等等的限制。如果app使用过程中需要给对象分配内存,则先会去看,现在是否有足够的内存,如果内存不够,先触发内存回收GC(Garbage Collection)操作,释放的无用的对象内存之后,如果内存还不够,系统才会给app增加内存分配,那么,增加多少内存,这个值要受到目标内存利用率的影响,绝不会给你一大片内存,但你只使用了其中的1%,这样防止了内存的浪费。

那么app的内存可以无限制增长吗?当然不可能。app的内存最大值,默认情况下,就是前面所说的单个应用程序最大内存限制(heapgrowthlimit),但是系统给了一定的扩展空间,开发者可以在manifest文件中增加 android:largeHeap="true" 这个语句,就可扩展这个最大值。如果app使用过程中占用的内存超过这个最大值,就会发生OOM,导致app闪退。

这里提到了一个概念,内存回收(GC),这在android里面非常重要,也是我们进行内存优化需要考虑的首要问题。下面会做一个简单介绍。

内存回收(GC)


java中做了内存的自动回收,因此开发者不需要自己去考虑回收自己的对象,这无疑是非常方便的。但同事也有潜在的问题,因为开发者不需要自己回收内存,系统什么时候回收,能不能成功回收,对于我们来说都是透明的,如果我们不了解系统的内存回收机制,就很有可能因为系统的回收造成性能问题。如内存泄漏,内存问题引起卡顿,等等。严重影响用户体验。

那么,android系统回收哪些对象?什么时候会回收?为什么会有内存泄漏?为什么会造成卡顿?

容我一一道来。

回收哪些对象?

android系统中,有一个GC Root的概念,每一次GC发生的时候,系统会从这个根节点开始去遍历,能够从GC Root开始遍历到的对象,即为活跃对象,如下图中黄色的对象。而如果是所有根节点都无法触达的对象,就是垃圾对象(蓝色)这些对象,就是可以被回收的对象。

Android内存优化_第3张图片

android中的跟节点有哪些呢?直接截图来啦,主要有一些系统的class,没有被终止的thread,没有被终止的文件读写类,等等,如下:

A garbage collection root is an object that is accessible from outside the heap. The following reasons make an object a GC root:

System Class

Class loaded by bootstrap/system class loader. For example, everything from the rt.jar likejava.util.*.

JNI Local

Local variable in native code, such as user defined JNI code or JVM internal code.

JNI Global

Global variable in native code, such as user defined JNI code or JVM internal code.

Thread Block

Object referred to from a currently active thread block.

Thread

A started, but not stopped, thread.

Busy Monitor

Everything that has calledwait()ornotify()or that is synchronized. For example, by callingsynchronized(Object)or by entering a synchronized method. Static method means class, non-static method means object.

Java Local

Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

Native Stack

In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.

Finalizable

An object which is in a queue awaiting its finalizer to be run.

Unfinalized

An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.

Unreachable

An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.

Java Stack Frame

A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.

Unknown

An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.

什么时候回收?

前面说过,一般是你的程序需要新建新的对象,这些对象需要分配内存,但是当前的空闲内存又不够的情况下,就会触发内存回收操作。这是触发最多的时机。但事实上,内存回收的时机远不止这一种:

Dalvik 系统 的GC时机(Android 5.0以下)

GC Reason

What triggered the GC and what kind of collection it is. Reasons that may appear include:

GC_CONCURRENT

A concurrent GC that frees up memory as your heap begins to fill up.

GC_FOR_MALLOC

A GC caused because your app attempted to allocate memory when your heap was already full, so the system had to stop your app and reclaim memory.

GC_HPROF_DUMP_HEAP

A GC that occurs when you request to create an HPROF file to analyze your heap.

GC_EXPLICIT

An explicit GC, such as when you callgc()(which you should avoid calling and instead trust the GC to run when needed).

GC_EXTERNAL_ALLOC

This happens only on API level 10 and lower (newer versions allocate everything in the Dalvik heap). A GC for externally allocated memory (such as the pixel data stored in native memory or NIO byte buffers).

ART 系统的GC时机(android 5.0以上,以及4.+的少数加入测试的机器)

Concurrent

A concurrent GC which does not suspend app threads. This GC runs in a background thread and does not prevent allocations.

Alloc

The GC was initiated because your app attempted to allocate memory when your heap was already full. In this case, the garbage collection occurred in the allocating thread.

Explicit

The garbage collection was explicitly requested by an app, for instance, by callinggc()orgc(). As with Dalvik, in ART it is recommended that you trust the GC and avoid requesting explicit GCs if possible. Explicit GCs are discouraged since they block the allocating thread and unnecessarily was CPU cycles. Explicit GCs could also cause jank if they cause other threads to get preempted.

NativeAlloc

The collection was caused by native memory pressure from native allocations such as Bitmaps or RenderScript allocation objects.

CollectorTransition

The collection was caused by a heap transition; this is caused by switching the GC at run time. Collector transitions consist of copying all the objects from a free-list backed space to a bump pointer space (or visa versa). Currently collector transitions only occur when an app changes process states from a pause perceptible state to a non pause perceptible state (or visa versa) on low RAM devices.

HomogeneousSpaceCompact

Homogeneous space compaction is free-list space to free-list space compaction which usually occurs when an app is moved to a pause imperceptible process state. The main reasons for doing this are reducing RAM usage and defragmenting the heap.

DisableMovingGc

This is not a real GC reason, but a note that collection was blocked due to use of GetPrimitiveArrayCritical. while concurrent heap compaction is occuring. In general, the use of GetPrimitiveArrayCritical is strongly discouraged due to its restrictions on moving collectors.

HeapTrim

This is not a GC reason, but a note that collection was blocked until a heap trim finished.

以上信息来自google官方文档,具体每一项代表什么意思,大家可自行去查阅。

内存泄漏&卡顿


为什么会有内存泄漏?

内存泄漏,其实就是对象在内存中的存活时间超出了它本身的生命周期。比如最典型的,一个activity已经被destroy了,这时候内存中肯定是不应该再保存这个activity了呀,可是因为某种原因,如果这时候还有其他的对象引用了这个activity ,就可能导致GC Root遍历的时候,能够追溯到这个activity,那么GC发生的时候,就会认为这个对象还是有用的,便无法回收。

本该被回收的内存,就这样被无用的对象占用着,其他真正有用的对象无法使用这一片内存,不仅造成了内存的浪费,也会导致程序的卡顿。

为什么会造成卡顿?

内存使用不当,很容易造成app的卡顿,为什么呢?这就需要了解以下GC的过程了。不管在Dalvik系统还是在ART中,GC 发生 的时候都会有 Stop the World(终止所有线程的操作,包括UI先成功)的过程,尽管ART中对GC过程有很多的优化,终止所有线程的时间会变短,但是也有短时间的终止动作。因此,发生GC的过程中,程序中的代码都没有运行,UI渲染和数据计算都被中断,不卡顿才怪。。

内存优化法则


那么,我们应该怎样避免内存泄漏和卡顿的发生呢?这个话题在网上有很多文章有写。我们可以从以上的GC Root 的介绍出发,避免内存泄漏 ,最本质的,就是要确保对象的生命周期结束的时候,没有其他长期存在的对象一直引用着它。有以下几个简单的法则:

-注意对象的及时释放

-可用local的,不用field

-避免频繁创建很多生命周期很短的对象(如onDrawfor循环里面等)

-避免爆发式的使用大对象

及时释放对象,避免了GC 跟节点对无用对象的持有

local对象的生命周期更短,而且存储在stack空间中;而成员变量field是存储在heap中,访问的效率和释放空间的效率都比local局部变量要低

频繁创建新对象,很有可能频繁的出发GC,前面说过,GC会终止其他所有的线程,如果GC很频繁,cpu都去处理GC操作了,根本没有时间处理app真正需要处理和渲染的数据,就会造成卡顿

因为内存并不会爆发式地增长,爆发式地使用大对象,很容易引起内存不够;android内存分为native和dalvik内存,如果这个大对象使得某一种内存(如native)快速增长到一定的值。之后就算这一片内存被释放了,也无法被利用为dalvik内存,会导致内存分配非常不均,导致总内存足够的情况下,某一种内存不够用的现象。

最后,简单总结了前辈们一般说的避免内存泄漏的关键点,列出来以提醒大家:

1. Application Context代替Activity Context

2. 非静态内部类的静态实例容易造成内存泄漏,如静态的view或者单例模式

3. 警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程,引起此类问题很常见的就有timer,handler,在使用定时器的时候一定要在destroy中调用timer.cancel(),handler一定要注意remove掉所有的message队列,并置空对象

4. 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等

5. 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等

6. 不要在执行频率很高的方法或者循环中创建对象,可以使用对象池

7. 尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用style等资源达到节约内存

8. 对于有缓存等存在的应用尽量实现onLowMemory()和onTrimMemory()方法

9. 不要加载过大的Bitmap对象;譬如对于类似图片加载我们要通过BitmapFactory.Options设置图片的一些采样比率和复用等

10. 对批量加载等操作进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等

有更多需要注意的,还请大家多多指教~~

再来个预告:下一期将介绍一种最简单的方式,如何排查内存泄漏

参考文章


Investigating Your RAM Usage

Dalvik虚拟机垃圾收集(GC)过程分析

Android内存优化之OOM

Android GC那点事

Android Handler Memory Leaks

Android性能优化之内存篇

Manage Your App's Memory

Android性能优化之常见的内存泄漏

Android内存泄漏总结

PerformancePatterns系列视频

你可能感兴趣的:(Android内存优化)