前言
最近遇到了Android 内存溢出的问题,自己也研究了许久,想必这是大部分Android开发者所遇到的问题,参考了一些大神的博客,自己想把这一块的知识做个大总结,加深一下自己的理解,顺便做一个记录,方便自己以后查看。
Android 内存的意义
其实我们在用安卓手机的时候不用太在意剩余内存,android上的应用是java,当然需要虚拟机,而android上的应用是带有独立虚拟机的,也就是每开一个应用就会打开一个独立的虚拟机。其实和java的垃圾回收机制类似,系统有一个规则来回收内存。进行内存调度有个阀值,只有低于这个值系统才会按一个列表来关闭用户不需要的东西。当然这个值默认设置得很小,所以你会看到内存老在很少的数值徘徊。但事实上他并不影响速度。相反加快了下次启动应用的速度。这本来就是 android标榜的优势之一,如果人为去关闭进程,没有太大必要。特别是使用自动关进程的软件。为什么内存少的时候运行大型程序会慢呢,原因是:在内存剩余不多时打开大型程序时会触发系统自身的调进程调度策略,这是十分消耗系统资源的操作,特别是在一个程序频繁向系统申请内存的时候。这种情况下系统并不会关闭所有打开的进程,而是选择性关闭,频繁的调度自然会拖慢系统。
Android的内存机制
android应用层是由java开发的,android的davlik虚拟机与jvm也类似,只不过它是基于寄存器的。在java中,通过new为对象分配内存,所有对象在java堆内分配空间;而内存的释放是由垃圾收集器(GC)来回收的。 Java采用了有向图的原理。Java将引用关系考虑为图的有向边,有向边从引用者指向引用对象。线程对象可以作为有向图的起始顶点,该图就是从起始顶点(GC roots)开始的一棵树,根顶点可以到达的对象都是有效对象,GC不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
Android 内存溢出的原因
1、内存泄露导致
1、由于我们编码的过程的失误,长期保持某些资源(如Context)的引用,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成内存泄露。
2、还有我们android 中常见就是Activity被引用在调用finish之后却没有释放,第二次打开activity又重新创建,重复的创建,这样的内存泄露不断的发生,则会导致内存的溢出。
android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程.
2、占用内存比较大的对象
这就是我们我们经常遇到的加载占用内存很大的对象(如Bitmap)或加载很多的图片,造成内存超出了我们的限制。
常见的内存泄露和解决方法
1、引用对象没有释放造成内存泄露
1.1、注册没有取消关闭造成内存泄露
这种ndroid的内存泄露比纯Java的内存泄漏还要严重,因为其他一些Android程序可能引用系统的Android程序的对象(比如注册机制,服务广播等)。即使Android程序已经结束了,但是别的应用程序仍然还有对Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。
1.2、集合中对象没有清理造成内存泄露
当我把一些对象的引用放到集合中,但有时候我们不需要某个对象时,如果我们不把它清掉的话,这个集合就会越来越大,占用的内存也就会越来越大,造成内存泄露,如果是static 时,情况更加严重。
1.3、static的滥用导致内存泄露
static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例,当我们使用static去修占用存较大的对象时,会导致这个对象无法回收,造成内存泄露。
1.4、context的使用不当造成内存泄露
context表示上下文,很多时候我们在处理异步线程等,例如AsyncTask,Thread,第三方库初始化等等时,会引用context对象,但是这些异步第三库的生命周期是大于我们activity的生命周期的,所以当我们回收这个activity时,context被占用,回收不了,造成内存泄露。
所以Context尽量使用ApplicationContext,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。
还有些情景,只能用activity:比如,对话框,各种View,需要startActivity的等。
总之,尽可能使用Application。
1.5、线程(内部类的使用)使用不当造成内存泄露
线程产生内存泄露的主要原因在于线程生命周期的不可控。如果我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。如果非静态内部类的方法中,有生命周期大于其所在类的,那就有问题了。比如:AsyncTask、Handler,这两个类都是方便开发者执行异步任务的,但是,这两个都跳出了Activity/Fragment的生命周期。
线程使用不当解决办法
第一种:将线程的内部类,改为静态内部类。因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会hold其主体(引用)。也就使主体不能被释放,亦即内存泄露。当我们使用静态内部类时,静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。
第二种:如果非要使用activity的引用,就使用弱引用
2、资源对象没关闭造成的内存泄露
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待GC来处理。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。而且android数据库中对Cursor资源的是又限制个数的,如果不及时close掉,会导致别的地方无法获得。
3、一些不良代码成内存压力
有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存,对内存的回收和分配造成很大影响的,容易迫使虚拟机不得不给该应用进程分配更多的内存,造成不必要的内存开支。
3.1、 Bitmap没调用recycle()
Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才设置为null.
虽然recycle()从源码上看,调用它应该能立即释放Bitmap的主要内存,但是测试结果显示它并没能立即释放内存。但是我猜应该还是能大大的加速Bitmap的主要内存的释放。
3.2 、构造Adapter时,没有使用缓存的 convertView
以构造ListView的BaseAdapter为例,在BaseAdapter中提共了方法:
public View getView(int position, View convertView, ViewGroup parent)
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。
由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。
占用内存对象较大或对象过多造成内存溢出及其解决办法
1、占用内存较大的对象时
现在Android手机,高清的图片是越来越大了,随便一张图片都是几M甚至几十M,而Android分配给Bitmap的内存是有些的,所有很容易就造成内存溢出,我们要处理一下才行。
1.1、加载图片很大时造成内存溢出
解决办法:
1、等比例的缩小图片
有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一
尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,
因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。
因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,
decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,
无需再使用java层的createBitmap,从而节省了java层的空间。
2、对图片采用软引用,及时地进行recycle()操作
虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,我们应该要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”,这样就可以及时的释放内存了。
SoftReference<Bitmap> bitmap;
bitmap = new SoftReference<Bitmap>(pBitmap);
if(bitmap != null){
if(bitmap.get() != null && !bitmap.get().isRecycled()){
bitmap.get().recycle();
bitmap = null;
}
}
3、在页面切换时尽可能少地重复使用一些代码。比如:重复调用数据库,反复使用某些对象等等.....
4、Android堆内存也可以自己定义大小
自定义我们的应用需要多大的内存,这个好暴力哇,强行设置最小内存大小,代码如下:
private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;
//设置最小heap内存为6MB大小
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
5、优化Dalvik虚拟机的内存
优化Dalvik虚拟机的堆内存分配,听着很强大,来看下具体是怎么一回事
对于Android平台来说,其托管层使用的Dalvik JavaVM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。当然具体原理我们可以参考开源工程,这里我们仅说下使用方法: 代码如下:
private final static floatTARGET_HEAP_UTILIZATION = 0.75f;
在程序onCreate时就可以调用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
即可
2、对象(bitmap)过多时造成内存溢出
我们经常遇到加载多张网络图片,不做处理的话,会出现内存溢出,这个时候就要考虑缓存了,我们缓存的策略是:
我们会先从内存缓存中去查找是否有该图片,如果没有就去文件缓存中查找是否有该图片,如果还没有,我们就从网络下载图片。
使用内存缓存和文件缓存更高效,内存缓存的策略是:
先从强引用缓存中查找,如果没有再从软引用缓存中查找,如果在软引用缓存中找到了,就把它移入强引用缓存;如果强引用缓存满了,就会根据Lru算法把某些图片移入软引用缓存,如果软引用缓存也满了,最早的软引用就会被删除。
这里,有必要说明下几个概念:强引用、软引用、弱引用、Lru。
强引用:就是直接引用一个对象,一般的对象引用均是强引用
软引用:引用一个对象,当内存不足并且除了我们的引用之外没有其他地方引用此对象的情况 下,该对象会被gc回收
弱引用:引用一个对象,当除了我们的引用之外没有其他地方引用此对象的情况下,只要gc被调用,它就会被回收(请注意它和软引用的区别)
Lru:Least Recently Used 近期最少使用算法,是一种页面置换算法,其思想是在缓存的页面数目固定的情况下,那些最近使用次数最少的页面将被移出,对于我们的内存缓存来说,强引用缓存大小固定为4M,如果当缓存的图片大于4M的时候,有些图片就会被从强引用缓存中删除,哪些图片会被删除呢,就是那些近期使用次数最少的图片。
事例代码:
public class ImageMemoryCache {
/**
* 从内存读取数据速度是最快的,为了更大限度使用内存,这里使用了两层缓存。
* 强引用缓存不会轻易被回收,用来保存常用数据,不常用的转入软引用缓存。
*/
private static final String TAG = "ImageMemoryCache";
private static LruCache<String, Bitmap> mLruCache; // 强引用缓存
private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache; // 软引用缓存
private static final int LRU_CACHE_SIZE = 4 * 1024 * 1024; // 强引用缓存容量:4MB
private static final int SOFT_CACHE_NUM = 20; // 软引用缓存个数
// 在这里分别初始化强引用缓存和弱引用缓存
public ImageMemoryCache() {
mLruCache = new LruCache<String, Bitmap>(LRU_CACHE_SIZE) {
@Override
// sizeOf返回为单个hashmap value的大小
protected int sizeOf(String key, Bitmap value) {
if (value != null)
return value.getRowBytes() * value.getHeight();
else
return 0;
}
@Override
protected void entryRemoved(boolean evicted, String key,
Bitmap oldValue, Bitmap newValue) {
if (oldValue != null) {
// 强引用缓存容量满的时候,会根据LRU算法把最近没有被使用的图片转入此软引用缓存
Logger.d(TAG, "LruCache is full,move to SoftRefernceCache");
mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));
}
}
};
mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(
SOFT_CACHE_NUM, 0.75f, true) {
private static final long serialVersionUID = 1L;
/**
* 当软引用数量大于20的时候,最旧的软引用将会被从链式哈希表中移出
*/
@Override
protected boolean removeEldestEntry(
Entry<String, SoftReference<Bitmap>> eldest) {
if (size() > SOFT_CACHE_NUM) {
Logger.d(TAG, "should remove the eldest from SoftReference");
return true;
}
return false;
}
};
}
/**
* 从缓存中获取图片
*/
public Bitmap getBitmapFromMemory(String url) {
Bitmap bitmap;
// 先从强引用缓存中获取
synchronized (mLruCache) {
bitmap = mLruCache.get(url);
if (bitmap != null) {
// 如果找到的话,把元素移到LinkedHashMap的最前面,从而保证在LRU算法中是最后被删除
mLruCache.remove(url);
mLruCache.put(url, bitmap);
Logger.d(TAG, "get bmp from LruCache,url=" + url);
return bitmap;
}
}
// 如果强引用缓存中找不到,到软引用缓存中找,找到后就把它从软引用中移到强引用缓存中
synchronized (mSoftCache) {
SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);
if (bitmapReference != null) {
bitmap = bitmapReference.get();
if (bitmap != null) {
// 将图片移回LruCache
mLruCache.put(url, bitmap);
mSoftCache.remove(url);
Logger.d(TAG, "get bmp from SoftReferenceCache, url=" + url);
return bitmap;
} else {
mSoftCache.remove(url);
}
}
}
return null;
}
/**
* 添加图片到缓存
*/
public void addBitmapToMemory(String url, Bitmap bitmap) {
if (bitmap != null) {
synchronized (mLruCache) {
mLruCache.put(url, bitmap);
}
}
}
public void clearCache() {
mSoftCache.clear();
}
}
以上就是内存溢出的大总结,参考了一些大神的博文,其实防止内存溢出还是要我们养成良好的编码习惯,注意一些细节,我相信我们我们所撸的码应该就能告别内存问题了
![大笑](http://static.blog.csdn.net/xheditor/xheditor_emot/default/laugh.gif)
![大笑](http://static.blog.csdn.net/xheditor/xheditor_emot/default/laugh.gif)
![大笑](http://static.blog.csdn.net/xheditor/xheditor_emot/default/laugh.gif)
参考博文:
http://www.aiuxian.com/article/p-2536943.html
http://articles.e-works.net.cn/embedded/article103890.htm