Android 内存笔记

Avoiding memory leaks

Android applications are, at least on the T-Mobile G1, limited to 16 MB of heap. It's both a lot of memory for a phone and yet very little for what some developers want to achieve. Even if you do not plan on using all of this memory, you should use as little as possible to let other applications run without getting them killed. The more applications Android can keep in memory, the faster it will be for the user to switch between his apps. As part of my job, I ran into memory leaks issues in Android applications and they are most of the time due to the same mistake: keeping a long-lived reference to a Context.

On Android, a Context is used for many operations but mostly to load and access resources. This is why all the widgets receive a Context parameter in their constructor. In a regular Android application, you usually have two kinds of Context, Activity and Application. It's usually the first one that the developer passes to classes and methods that need a Context:

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  
  TextView label = new TextView(this);
  label.setText("Leaks are bad");
  
  setContentView(label);
}

This means that views have a reference to the entire activity and therefore to anything your activity is holding onto; usually the entire View hierarchy and all its resources. Therefore, if you leak the Context ("leak" meaning you keep a reference to it thus preventing the GC from collecting it), you leak a lot of memory. Leaking an entire activity can be really easy if you're not careful.

When the screen orientation changes the system will, by default, destroy the current activity and create a new one while preserving its state. In doing so, Android will reload the application's UI from the resources. Now imagine you wrote an application with a large bitmap that you don't want to load on every rotation. The easiest way to keep it around and not having to reload it on every rotation is to keep in a static field:

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  
  TextView label = new TextView(this);
  label.setText("Leaks are bad");
  
  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);
  
  setContentView(label);
}

This code is very fast and also very wrong; it leaks the first activity created upon the first screen orientation change. When a Drawable is attached to a view, the view is set as a callback on the drawable. In the code snippet above, this means the drawable has a reference to the TextView which itself has a reference to the activity (the Context) which in turns has references to pretty much anything (depending on your code.)

This example is one of the simplest cases of leaking the Context and you can see how we worked around it in the Home screen's source code (look for the unbindDrawables() method) by setting the stored drawables' callbacks to null when the activity is destroyed. Interestingly enough, there are cases where you can create a chain of leaked contexts, and they are bad. They make you run out of memory rather quickly.

There are two easy ways to avoid context-related memory leaks. The most obvious one is to avoid escaping the context outside of its own scope. The example above showed the case of a static reference but inner classes and their implicit reference to the outer class can be equally dangerous. The second solution is to use the Application context. This context will live as long as your application is alive and does not depend on the activities life cycle. If you plan on keeping long-lived objects that need a context, remember the application object. You can obtain it easily by calling Context.getApplicationContext() or Activity.getApplication().

In summary, to avoid context-related memory leaks, remember the following:

  • Do not keep long-lived references to a context-activity (a reference to an activity should have the same life cycle as the activity itself)
  • Try using the context-application instead of a context-activity
  • Avoid non-static inner classes in an activity if you don't control their life cycle, use a static inner class and make a weak reference to the activity inside. The solution to this issue is to use a static inner class with a WeakReference to the outer class, as done in ViewRoot and its W inner class for instance
  • A garbage collector is not an insurance against memory leaks

Note: This article was originally posted on my personal blog.




Memory management for Android Apps

Android apps have more memory available than ever before, but are you sure you're using it wisely? Even though Android has got a garbage collector, do I have to worry about memory management?

In this post I will discuss about:

  • how heap size affects applications
  • how GC (garbage collector) works and memory leak
  • how to handle bitmap efficiently

Memory Heap Size

Android is a full multitasking system so it’s possible tu run multiple programs at the same time and obviously each one can’t use all of your device memory.
For this reason there is a hard limit on your Android application’s heap size: if your application needs to allocate more memory and you have gone up to that heap size already, you are basically going to get a “out of memory error”.
Heap size limit is device dependent and it has changed a lot over the years, the first Android phone (the G1) has 16MB heap size limit, now we have tablets like Motorola Xoom that have 48MB heap size limit.
The basic reason is that new tablets and new phones have an high monitor resolution so they need more memory in order to draw more pixels.
However this limit is also too tight for a new kind of application that you can image for a tablet. For example, think about a photo editor that is really memory intensive, probably 48MB of heap space isn’t enough for that.
For that kind of application Honeycomb introduced an option in the Android Manifest: “android:largeHeap=true” that give you a big heap space limit.”
This doesn’t mean that if you get an “out of memory error” you can fix it simply setting this option!
You have to use this option only if you are going to build an application that is really memory intensive and you really need it.
When you develop an application you have to think that your application will run on a very large set of different devices with different capability and different heap size limit, so you need to worry about that and a simple thing you can do it’s provide your bitmap resources in all the different resolution ldpi, mdpi, hdpi, xhdpi.

Garbage Collector

The goal of the garbage collector is to keep the memory clean from unreferenced object.
Each object in memory must be connected directly or indirectly to the GC Roots, otherwise they can be removed from garbage collector.
All the objects in memory are represented by a tree and the garbage collector start traversing the tree from the GC roots to find unreferenced objects that need to be cleaned.
In the follow picture, red dots aren’t connected to the the GC roots so they can’t be cleaned.

Garbage collector make a good work but doesn’t prevent memory leaks. A leak is a reference to an unused object that prevent the GC to collect it.
A common mistake that usually generates a leak it’s a long reference to an Activity, a View or a Drawable.
In the follow example I will show you a circumstance that crates a leak.
Android 内存笔记_第1张图片

The code used to generate this situation is very simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends Activity {
     
     private static Leaky mLeak;
     
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         if (mLeak == null ) {
             mLeak = new Leak();
         }
     }
     
     class Leaky {
         public void hello() {
             System.out.println( "hello" );
         }
     }
}

The problem in the side code is that the MainActivity has a static field that refers to an object of a non-static inner class. Non-static Inner class keeps a reference to enclosing object and
static fields are related to the class not to the instance of an object. So the mLeak field will live until MainActivity.class is freed and keep a reference to the MainActivity that instantiate the mLeak field.

Handle bitmap efficiently

If you're not careful, bitmaps can quickly consume your available memory budget leading to an application crash due to the dreaded exception: “java.lang.OutofMemoryError: bitmap size exceeds VM budget”.

Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface (UI).
Given that you are working with limited memory, the best thing that you can do is to load a lower resolution version into the memory. The lower resolution version should match the size of the UI component that displays it. An image with an higher resolution does not provide any visible benefit, but still takes up precious memory and incurs additional performance overhead due to additional on the fly scaling.
The BitmapFactory class provides several decoding methods (decodeByteArray(), decodeFile(),decodeResource(), etc.) for creating a Bitmap from various sources.
Each type of decode method has additional signatures that let you specify the decoding options via BitmapFactory.Options class.
Setting the inJustDecodeBounds property to true while decoding avoids memory allocation, returning null for the bitmap object but setting outWidth, outHeight and outMimeType. This technique allows you to read the dimensions and type of the image data before the construction (and memory allocation) of the bitmap.
Now that the image dimensions are known, they can be used to decide if we should load the full image or a subsampled version, into the memory. To tell the decoder to subsample the image, loading a smaller version into memory, set inSampleSize to the scale factor in your BitmapFactory.Options object.
The code below shows how to load an Image of arbitrarily large size to a bitmap of the reqWidth and reqHeight.
The calcualteInSampleSize just determinates the scale factor from the original size and the requested size.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
         int reqWidth, int reqHeight) {
     // First decode with inJustDecodeBounds=true to check dimensions
     final BitmapFactory.Options options = new BitmapFactory.Options();
     options.inJustDecodeBounds = true ;
     BitmapFactory.decodeResource(res, resId, options);
 
     // Calculate inSampleSize
     options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
 
     // Decode bitmap with inSampleSize set
     options.inJustDecodeBounds = false ;
     return BitmapFactory.decodeResource(res, resId, options);
}




尊重原创作者,转载请注明出处:

http://blog.csdn.net/gemmem/article/details/8920039

最近在网上看了不少Android内存管理方面的博文,但是文章大多都是就单个方面去介绍内存管理,没有能全局把握,缺乏系统性阐述,而且有些观点有误,仅仅知道这些,还是无法从整体上理解内存管理,对培养系统优化和系统稳定性分析方面的能力是不够的。

    我结合自己的一些思考和理解,从宏观层面上,对内存管理做一个全局性的介绍,在此与大家交流分享。

首先,回顾一下基础知识,基础知识是理解系统机制的前提和关键:

1、  进程的地址空间

在32位操作系统中,进程的地址空间为0到4GB,

示意图如下:

  Android 内存笔记_第2张图片

图1

这里主要说明一下Stack和Heap:

Stack空间(进栈和出栈)由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等,所以Stack空间不需要很大,一般为几MB大小。

Heap空间由程序控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。正是因为Heap空间由程序员管理,所以容易出现使用不当导致严重问题。

2、 进程内存空间和RAM之间的关系

进程的内存空间只是虚拟内存(或者叫作逻辑内存),而程序的运行需要的是实实在在的内存,即物理内存(RAM)。在必要时,操作系统会将程序运行中申请的内存(虚拟内存)映射到RAM,让进程能够使用物理内存。

示意图如下:

Android 内存笔记_第3张图片

图2

基础知识介绍到这里,如果读者理解以上知识有障碍,请好好恶补一下基础知识,基础理论知识至关重要。 

3、  Android中的进程

(1)   native进程:采用C/C++实现,不包含dalvik实例的进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。如图           3,/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native进程。

(2)   java进程:Android中运行于dalvik虚拟机之上的进程。dalvik虚拟机的宿主进程由fork()系统调用创建,所以每一个java进程都是存在于一个native进程中,因此,java进程的内存分配比native进程复杂,因为进程中存在一个虚拟机实例。如图3,Android系统中的应用程序基本都是java进程,如桌面、电话、联系人、状态栏等等。

Android 内存笔记_第4张图片

图3

4、  Android中进程的堆内存

RAM作为进程运行不可或缺的资源,对Android系统性能和稳定性有着决定性影响,RAM的一部分被操作系统留作他用,比如显存等等,当然这个程序员无法干预,我们也不必过多地关注它。图1和图4分别介绍了native process和javaprocess的结构,这个是我们程序员需要深刻理解的,进程空间中的heap空间是我们需要重点关注的。heap空间完全由程序员控制,我们使用的malloc、C++ new和java new所申请的空间都是heap空间, C/C++申请的内存空间在native heap中,而java申请的内存空间则在dalvik heap中。

Android 内存笔记_第5张图片

图4

5、  Android的 java程序为什么容易出现OOM

这个是因为Android系统对dalvik的vm heapsize作了硬性限制,当java进程申请的java空间超过阈值时,就会抛出OOM异常(这个阈值可以是48M、24M、16M等,视机型而定),可以通过adb shell getprop | grep dalvik.vm.heapsize查看此值。

也就是说,程序发生OMM并不表示RAM不足,而是因为程序申请的java heap对象超过了dalvik vm heapsize。也就是说,在RAM充足的情况下,也可能发生OOM。

这样的设计似乎有些不合理,但是Google为什么这样做呢?这样设计的目的是为了让Android系统能同时让比较多的进程常驻内存,这样程序启动时就不用每次都重新加载到内存,能够给用户更快的响应。迫使每个应用程序使用较小的内存,移动设备非常有限的RAM就能使比较多的app常驻其中。但是有一些大型应用程序是无法忍受vm heapsize的限制的,后面会介绍如何让自己的程序跳出vm heap size的限制。

6、  Android如何应对RAM不足

在第5点中提到:java程序发生OMM并不是表示RAM不足,如果RAM真的不足,会发生什么呢?这时Android的memory killer会起作用,当RAM所剩不多时,memory killer会杀死一些优先级比较低的进程来释放物理内存,让高优先级程序得到更多的内存。我们在分析log时,看到的进程被杀的log,如图5,往往就是属于这种情况。


图5

7、  如何查看RAM使用情况

可以使用adb shell cat /proc/meminfo查看RAM使用情况:

MemTotal:        396708 kB

MemFree:           4088 kB

Buffers:           5212 kB

Cached:          211164 kB

SwapCached:           0 kB

Active:          165984 kB

Inactive:        193084 kB

Active(anon):    145444 kB

Inactive(anon):     248 kB

Active(file):     20540 kB

Inactive(file):  192836 kB

Unevictable:       2716 kB

Mlocked:              0 kB

HighTotal:            0 kB

HighFree:             0 kB

LowTotal:        396708 kB

LowFree:           4088 kB

SwapTotal:            0 kB

SwapFree:             0 kB

Dirty:                0 kB

Writeback:            0 kB

AnonPages:       145424 kB

……

……

这里对其中的一些字段进行解释:

MemTotal:可以使用的RAM总和(小于实际RAM,操作系统预留了一部分)

MemFree:未使用的RAM

Cached:缓存(这个也是app可以申请到的内存)

HightTotal:RAM中地址高于860M的物理内存总和,只能被用户空间的程序使用。

HightFree:RAM中地址高于860M的未使用内存

LowTotal:RAM中内核和用户空间程序都可以使用的内存总和(对于512M的RAM: lowTotal= MemTotal)

LowFree: RAM中内核和用户空间程序未使用的内存(对于512M的RAM: lowFree = MemFree)

8、  如何查看进程的内存信息

(1)、使用adb shell dumpsys meminfo + packagename/pid:

从图6可以看出,com.example.demo作为java进程有2个heap,native heap和dalvik heap,

native heap size为159508KB,dalvik heap size为46147KB

  Android 内存笔记_第6张图片

图6 

(2)、使用adb shell procrank查看进程内存信息

        如图7:

Android 内存笔记_第7张图片

图7

解释一些字段的意思:

VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)

PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

注意:procrank可以查看native进程和java进程,而dumpsys meminfo只能查看java进程。

9、  应用程序如何绕过dalvikvm heapsize的限制

对于一些大型的应用程序(比如游戏),内存使用会比较多,很容易超超出vm heapsize的限制,这时怎么保证程序不会因为OOM而崩溃呢?

(1)、创建子进程

               创建一个新的进程,那么我们就可以把一些对象分配到新进程的heap上了,从而达到一个应用程序使用更多的内存的目的,当然,创建子进程会增加系统开销,而且并不是所有应用程序都适合这样做,视需求而定。

创建子进程的方法:使用android:process标签

(2)、使用jni在native heap上申请空间(推荐使用)

      nativeheap的增长并不受dalvik vm heapsize的限制,从图6可以看出这一点,它的native heap size已经远远超过了dalvik heap size的限制。

只要RAM有剩余空间,程序员可以一直在native heap上申请空间,当然如果 RAM快耗尽,memory killer会杀进程释放RAM。大家使用一些软件时,有时候会闪退,就可能是软件在native层申请了比较多的内存导致的。比如,我就碰到过UC web在浏览内容比较多的网页时闪退,原因就是其native heap增长到比较大的值,占用了大量的RAM,被memory killer杀掉了。

(3)、使用显存(操作系统预留RAM的一部分作为显存)

使用 OpenGL textures API texture memory 不受 dalvik vm heapsize 限制,这个我没有实践过。再比如 Android 中的 GraphicBufferAllocator 申请的内存就是显存。

 

10、Bitmap分配在native heap还是dalvik heap上?

一种流行的观点是这样的:

Bitmap是jni层创建的,所以它应该是分配到native heap上,并且为了解释bitmap容易导致OOM,提出了这样的观点:

              native heap size + dalvik heapsize <= dalvik vm heapsize

详情请看: http://devspirit.blog.163.com/blog/static/16425531520104199512427/

但是请大家看看图6,native heap size为159508KB,远远超过dalvik vm heapsize,所以,事实证明以上观点是不正确的。

正确的观点:

大家都知道,过多地创建bitmap会导致OOM异常,且native heapsize不受dalvik限制,所以可以得出结论:

Bitmap只能是分配在dalvik heap上的,因为只有这样才能解释bitmap容易导致OOM。

但是,有人可能会说,Bitmap确实是使用java native方法创建的啊,为什么会分配到dalvik heap中呢?为了解决这个疑问,我们还是分析一下源码:

涉及的文件:

framework/base/graphic/java/Android/graphics/BitmapFactory.java
framework/base/core/jni/Android/graphics/BitmapFactory.cpp
framework/base/core/jni/Android/graphics/Graphics.cpp

BitmapFactory.java里面有几个decode***方法用来创建bitmap,最终都会调用:

private staticnative Bitmap nativeDecodeStream(InputStream is, byte[] storage,Rect padding,Options opts);

而nativeDecodeStream()会调用到BitmapFactory.cpp中的deDecode方法,最终会调用到Graphics.cpp的createBitmap方法。

我们来看看createBitmap方法的实现:

jobjectGraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, jbyteArray buffer,
                                  boolisMutable, jbyteArray ninepatch, int density)
{
    SkASSERT(bitmap);
    SkASSERT(bitmap->pixelRef());
 
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
           static_cast(reinterpret_cast(bitmap)),
            buffer, isMutable, ninepatch,density);
    hasException(env); // For the side effectof logging.
    return obj;
}

从代码中可以看到bitmap对象是通过env->NewOject( )创建的,到这里疑惑就解开了,bitmap对象是虚拟机创建的,JNIEnv的NewOject方法返回的是java对象,并不是native对象,所以它会分配到dalvik heap中。

11、java程序如何才能创建native对象

必须使用jni,而且应该用C语言的malloc或者C++的new关键字。实例代码如下:

JNIEXPORT void JNICALLJava_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)
{
        
         void * p= malloc(1024*1024*50);
 
         SLOGD("allocate50M Bytes memory");
 
         if (p !=NULL)
         {       
                   //memorywill not used without calling memset()
                   memset(p,0, 1024*1024*50);
         }
         else
                   SLOGE("mallocfailure.");
   ….
   ….
free(p); //free memory
}

或者:

JNIEXPORT voidJNICALL Java_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)
{
        
         SLOGD("allocate 50M Bytesmemory");
         char *p = new char[1024 * 1024 * 50];
         if (p != NULL)
         {       
                   //memory will not usedwithout calling memset()
                   memset(p, 1, 1024*1024*50);
         }
         else
                  SLOGE("newobject failure.");
 ….
….
free(p); //free memory
}

这里对代码中的memset做一点说明:

       new或者malloc申请的内存是虚拟内存,申请之后不会立即映射到物理内存,即不会占用RAM,只有调用memset使用内存后,虚拟内存才会真正映射到RAM。




1 问题

谈到Bitmap就必须和Android虚拟机内存扯上关系。在Android开发中,内存使用一直是围绕Bitmap开发的难点。虽然Java有专门的垃圾回收机制(GC),但是在Bitmap使用中,常常会遇到过OOM(out of memory)异常,相信有Android多媒体开发经验的朋友体会尤为深刻。

首先来分析一下OOM的原因,当我们使用BitmapFactory来decode一张3.5M大小的图片(3456*2304),我们decode出来的Bitmap的大小是多少呢?

Bitmap大小计算公式: Size = image.width * image.height * Config

(Config 参数为图片的编码器,常见的图片编译器有 ALPHA_8、RGB_565、ARGB_4444(Deprecated)、ARGB_8888)

RGB_565解码器占用2 byte       ARGB_8888 解码器占用4byte      

当图片采用ARGB_8888编码器时,Config为4,那么decode后的Bitmap大小为:3456 * 2304 * 4,换算成M为30.375M,My God整整是原来图片的8.6785倍,因此当我们使用Bitmap时稍有不慎,造成OOM简直成了理所当然了。

2 分析

难道编写Android的大牛们没有考虑到这个问题?

答案肯定是考虑到了的。在阐述接下来的问题之前,要给大家洗洗脑。很多人都认为Android应用所占用的内存就是虚拟机(Dalvik)所占用的内存。其实Android应用所占用的内存由Dalvik虚拟机内存和Native Memory内存组成。因此,当我们在进行多媒体开发的时候遇到OOM时,Java层代码已经做到perfect了(代码优化到极致),还是存在OOM异常的时候。可以利用Native Memory(本地内存)来解决OOM异常。

利用Native Memory?

我没听错吧?Davik 虚拟机内存和 C/C++的Native Memory共享?真是什么高难度技术,研究了半天源码,还是没有看出大牛们是怎么实现。具体实现的我们先放一边,(如果大家对内存共享感兴趣的话,可以去研究一下,顺便告诉我以下,在此先谢过了)我们的目的是怎样使用Native Memory内存。换句话说我们需要怎样将Bitmap所占用的内存挪到Native Memory内存中,这才是我们的重点。

查看应用占用的内存(Dalvik 虚拟机和Native Memory)大小的方式如下所示:

[cpp] view plain copy
  1. # adb shell dumpsys meminfo com.jony.actionbar  

3 实现

首先我们来看看使用Dalvik虚拟机来的decode一张Bitmap所占用的内存大小。

使用图片信息: Storage: 3.5M图片(3456*2304)

网上有很多人说使用decodeStream()方法,不需要使用Java层decode Bitmap,因此可以节省Java层虚拟机的内存。我不知道这些人是凭空想象,还有有什么证据来说明他们的结论。根据我的实验以及源码的分析,BitmapFactory的decode方法都是调用的Java的Native方法。因此网上的这种说法正确性还有待考证。

方法一:

[java] view plain copy
  1. AssetManager am = getAssets();  
  2. InputStream is = am.open("high_pixel_img.jpg");  
  3. Bitmap bitmap = BitmapFactory.decodeStream(is);  

Dalvik虚拟机占用的内存如图3-1所示:


                                                                            图 3-1

(备注:其中红色标记的为当前Dalvik虚拟机所占内存)

使用#adb shell dumpsys meminfo com.jony.bitmaptest 命令查看Dalvik虚拟机内存和Native memory使用情况如图3-2所示:

                                                                           图 3-2

方法二:

[java] view plain copy
  1. try {  
  2.             BitmapFactory.Options options = new BitmapFactory.Options();  
  3.             options.inPreferredConfig = Config.ARGB_8888;  
  4.             options.inPurgeable = true;//允许可清除  
  5.             options.inInputShareable = true;// 以上options的两个属性必须联合使用才会有效果  
  6.             AssetManager am = getAssets();  
  7.             InputStream is = am.open("high_pixel_img.jpg");  
  8.             Bitmap bitmap = BitmapFactory.decodeStream(is,null,options);  
  9.             imageView.setImageBitmap(bitmap);  
  10.         } catch (IOException e) {  
  11.             e.printStackTrace();  
  12.         }  

Dalvik虚拟机占用的内存如图3-3所示:


                                                                                       图 3-3 

使用#adb shell dumpsys meminfo com.jony.bitmaptest 命令查看Dalvik虚拟机内存和Native memory使用情况如图所示:

                                                                                 图 3-4


结合以上信息可以分析出:采用方法一Decode一张图片的时候Bitmap占用的是Dalvik虚拟机的内存;采用方法二Decode一张图片的时候Bitmap占用的Native memory的内存空间。因此在使用Bitmap的时候,可以根据业务的需求采用不同的内存占用方式,完美的解决OOM问题。


你可能感兴趣的:(android)