Android开发过程中总会在意想不到的的地方出现一些OOM的错误,类似于下面的代码:
1 2 3 4 5
Caused by: java.lang.OutOfMemoryError at java.io.ByteArrayOutputStream.expand(ByteArrayOutputStream.java:91) at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:201) at com.fanli.android.base.general.util.FileUtil.InputStreamToString(FileUtil.java:400) at com.fanli.android.base.general.util.FileUtil.readStringFromInputStream(FileUtil.java:210)
1. 一些简单的背景知识
进程的地址空间
在32位操作系统中,进程的地址空间为0到4GB,示意图如下:
这里主要说明一下Stack和Heap:
Stack空间:(进栈和出栈)由操作系统控制,其中主要存储 函数地址、函数参数、局部变量 等等。
所以Stack空间不需要很大,一般为几MB大小。
Heap空间:使用由程序员控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。
Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。
正是因为Heap空间由程序员管理,所以容易内存出现使用不当而导致严重问题。
Android的进程一般分为Native进程和Java进程.
native进程:采用C/C++实现,不包含dalvik实例的linux进程**,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。比如/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native进程。
java进程:实例化了dalvik虚拟机实例的linux进程,进程的入口main函数为java函数。 dalvik虚拟机实例的宿主进程是fork()系统调用创建的linux进程,所以每一个Android上的java进程实际上就是一个linux进程,只是进程中多了一个dalvik虚拟机实例。
进程中的对应的Heap空间内存(堆内存)也同样分为两种:
C/C++申请的内存空间在Native Heap中
Java申请的内存空间在dalvik heap中
其中Android系统针对每一个具体Java进程的dalvik heap都会设定一个具体的阀值,
当Java进程申请的空间超出这个阀值时就会抛出OOM异常
,也就是说
OOM仅与Java进程相关,且即使在RAM充足的情况下,也是可能发生OOM的
。
具体的dalvik heap内存阀值与Android机型ROM在编译时的设置有关,具体的大小可以执行下面的代码查看:
1
adb shell getprop | grep dalvik.vm.heap*
Android对应的GC操作
当虚拟机内存使用接近上限时,Android系统将自动触发Java的GC(Garbage Collection)操作,Android中GC的具体机制可以参考
这篇文章
。
Android不是用GC会自动回收资源么,为什么app的那些不用的资源不回收呢?
Android的GC是按照特定的算法回收程序不用的内存资源,避免app的内存申请越积越多,但GC一般回收的资源是那些无主的对象内存或者软饮用的资源,或者更软引用的引用资源。但是如果发生内存泄露,例如某个Activity一直被静态变量引用无法释放。当重新进入该Activity时又会new出一个新的实例。对应的内存实例越来越多,泄露的内存又无法通过gc进行释放,最后内存耗尽导致OOM。
2. 常用的避免内存泄露的套路
由于绝大多数发生OOM的情况都是由内存泄露引起的,针对内存泄露常用的一些方案,总结如下:
常见的内存泄露:
a. 非静态内部类内存泄露
在Activity中创建非静态内部类,非静态内部类会持有Activity的隐式引用,若内部类生命周期长于Activity,会导致Activity实例无法被回收。(屏幕旋转后会重新创建Activity实例,如果内部类持有引用,将会导致旋转前的实例无法被回收)。
解决方案:如果一定要使用内部类,就改用static内部类,在内部类中通过WeakReference的方式引用外界资源。
正确的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
static class ImageDownloadTask extends AsyncTask <String , Void , Bitmap > { private String url; private WeakReference<PhotoAdapter> photoAdapter; public ImageDownloadTask (PhotoAdapter photoAdapter) { this .photoAdapter = new WeakReference<PhotoAdapter>(photoAdapter); } @Override protected Bitmap doInBackground (String... params) { url = params[0 ]; Bitmap bitmap = photoAdapter.get().loadBitmap(url); if (bitmap != null ) { String key = MD5Tools.decodeString(url); photoAdapter.get().put(key, bitmap); } return bitmap; } @Override protected void onPostExecute (Bitmap bitmap) { super .onPostExecute(bitmap); ImageView mImageView = (ImageView) photoAdapter.get().mGridView.get().findViewWithTag(MD5Tools.decodeString(url)); if (mImageView != null && bitmap != null ) { mImageView.setImageBitmap(bitmap); photoAdapter.get().mDownloadTaskList.remove(this ); } } }
b. 匿名内部类内存泄漏
跟非静态内部类一样,匿名内部类也会持有外部类的隐式引用,比较常见的情况有,耗时Handler,耗时Thread,都会造成内存泄漏,解决方式也是static+WeakReference,下面给出正确写法。
Handler的正确写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
private static class MyHandler extends Handler { private final WeakReference<Context> context; private MyHandler (Context context) { this .context = new WeakReference<Context>(context); } @Override public void handleMessage (Message msg) { switch (msg.what) { } } } private final MyHandler mHandler = new MyHandler(this );private static final Runnable sRunnable = new Runnable() { @Override public void run () { } }; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_home); mHandler.postDelayed(sRunnable, 600000 ); }
Thread的正确写法:
1 2 3 4 5 6 7 8 9 10 11
private static class MyThread extends Thread { @Override public void run () { while (true ) { } } } new MyThread().start();
c. Context持有导致内存泄漏
Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
对于大部分非必须使用Activity Context的情况(创建Dialog的Context必须是Activity Context),应该使用Application Context。
d. 记得注销监听器和广播
注册监听器的时候会add Listener,不要忘记在不需要的时候remove掉Listener
e. 谨慎使用单例中不合理的持有
单例中的对象生命周期与应用一致,注意不要在单例中进行不必要的外界引用持有。如果一定要引用外部变量,需要在外部变量生命周期结束的时候接触引用(赋为null)
f. 一定要记得关闭无用连接
在onDestory中关闭Cursor,I/O,数据库,网络的连接用完记得关闭
g. 集合中的内存泄漏
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
3. 常用的内存优化套路
当APP使用的内存较大时为了避免因为达到使用上限的阀值,也可以进行这方面的一些优化:
使用轻量的数据结构
使用ArrayMap/SparseArray来代替HashMap,ArrayMap/SparseArray是专门为移动设备设计的高效的数据结构。
HashMap的缺点:
(1)就算没有数据,也需要分配默认16个元素的数组(2)一旦数据量达到Hashmap限定容量的75%,就将按两倍扩容
ArrayMap/SparseArray针对HashMap的主要优点是单个Entry占用内存低且每次增加元素是size++
ArrayMap/SparseArray缺陷:使用二分查找法的效率低于HashMap对应的效率
更加详细的原因可以参考
这篇文章
不要使用Enum
Android中使用枚举类型将会显著增加dex的体积,并且会产生额外的内存占用
更加详细的原因可以参考
这篇文章
不要使用String进行字符串拼接
严格的讲,String拼接只能归结到内存抖动中,因为产生的String副本能够被GC,不会造成内存泄露。
频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。
更加详细的介绍可以参考
这篇文章
资源文件需要选择合适的文件夹进行存放
hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下
谨慎使用static对象
static对象的生命周期过长,应该谨慎使用
恰当的使用Bitmap
及时的销毁
在用完Bitmap时,要及时的bitmap.recycle( )掉。
注意,recycle( )并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。
设置采样率
有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:
1 2 3 4 5 6 7
private ImageView preview; BitmapFactory.Options options = newBitmapFactory.Options(); options.inSampleSize = 2 ; Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null , options); preview.setImageBitmap(bitmap);
4. 内存溢出时一些可能的脑洞套路
虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
这里需要注意当栈的大小越大可分配的线程数就越少。
运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGen space
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
异常信息:java.lang.OutOfMemoryError:PermGen space
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。