Out of Memory(内存溢出) 几乎是每个Android程序员都会遇到的事。在网上也能找到一大堆的解决方案,之前写过一篇《Android 内存溢出管理与测试》的博文。但感觉写得不是很好,今天整理一下打算重新写一篇。
首先什么是OOM?为什么会出现OOM?
Out Of Memory,一般是由于程序编写者对内存使用不当,如对该释放的内存资源没有释放,导致其一直不能被再次使用而使计算机内存被耗尽的现象。重启计算机即可,但根本解决办法还是对代码进行优化。(摘自百度百科)
那么解决OOM的方法有哪些呢?或者说常见的导致OOM的错误有哪些?
1、动态回收内存。
2、为应用分配更多的内存。
3、自定义内存大小。
4、如果是因为图片引起的OOM,其实就可以从图片下手。(使图片体积大小变小)
5、加载图片时在内存中做处理。(图片的边界压缩)
6、Context泄漏。
7、使用eclipse DDMS中的heap查看内存。
8、构造Adapter时,没有使用缓存的convertView。
9、资源对象没关闭造成的内存泄漏。
10、注册没取消造成的内存泄漏。
11、集合中对象没清理造成的内存泄漏。
12、使用缓存技术。
1、动态回收内存算是最简单的解决方法吧,就是手动的调用System.gc();
例如:bit为Bitmap对象
if (bit != null && !bit.isRecycled()) { bit.recycle(); bit = null; }
System.gc();
bitmap.recycle()方法用于回收该bitmap所占用的内存,用System.gc()调用一下系统的垃圾回收器。
需要注意的是:回收内存要及时,比如说SurfaceView,就应该在onSurfaceDestroyed这个方法中回收。如果Activity使用了bitmap,就可以在onStop或者onDestroy方法中回收等等。
2、为应用分配更多的内存。
在清单文件中的< application >节点下,添加如下代码:android:largeHeap="true"。
android:largeHeap应用程序的进程是否会用较大的 Dalvik 堆来创建。 这将作用于所有为该应用程序创建的进程,但只对第一个被装入进程的应用程序生效。 如果通过共享用户 ID 的方式让多个应用程序公用一个进程,那么这些应用程序必须全部指定本选项,否则将会导致不可预知的后果。
大部分应用程序不需要用到本属性,而是应该关注如何减少内存消耗以提高性能。 使用本属性并不能确保一定会增加可用的内存,因为某些设备可用的内存本来就很有限。
要在运行时查询可用的内存大小,请使用 getMemoryClass()
或getLargeMemoryClass()
方法。
除上述方法外,还有一个方法。
使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。
具体的使用如下:
private final static floatTARGET_HEAP_UTILIZATION = 0.75f; //在程序onCreate时就可以调用 VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); //即可
3、一直感觉自定义的这种方法实在是太暴力了。
强制定义自己软件的对内存大小,我们使用Dalvik提供的 dalvik.system.VMRuntime类来设置最小堆内存为例:
private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ; VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //设置最小heap内存为6MB大小。当然对于内存吃紧来说还可以通过手动干涉GC去处理
4、这也分为两个方面:
1、分辨率不变,图片大小减小。 2、分辨率改变,图片减小。(用PS都很容易的)
需要注意的是:不要减小得太小而影响了人眼看上去的美感。
5、这里给出一个简单的操作和一个封装后的操作,可以对比看看。
简单的操作:
//压缩,用于节省BITMAP内存空间--解决BUG的关键步骤 BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 2;//这个的值压缩的倍数(2的整数倍),数值越小,压缩率越小,图片越清晰 //返回原图解码之后的bitmap对象 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.begin_background, opts);
这里的bitmap就是压缩后得到的图片。
封装后的操作:
private Bitmap imgUtis(Resources res, int img, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;// 让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。 BitmapFactory.decodeResource(getResources(), img, options); // 在加载图片之前就获取到图片的长宽值和MIME类型,并返回压缩的尺寸 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(getResources(), img, options); } /** * * @param options 操作对象 * @param reqWidth 目标宽 * @param reqHeight 目标高 * @return */ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源图片的高度和宽度 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // 计算出实际宽高和目标宽高的比率 final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 // 一定都会大于等于目标的宽和高。 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; }
这里得到的bitmap是按照规定的尺度比例来进行压缩的。
6.
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); }
7.Android 开发工具eclipse中的DDMS带有一个内存监测工具Heap,可以检测一个进程的内存变化,根据这个工具我们大致可以测试某个应用的内存变化。
具体的操作方法如下:
1、打开eclipse,切换到DDMS,并确认Devices视图、Heap视图都是打开的。
2、将手机通过USB链接至电脑,链接时,选择 “USB调试”模式。
3、链接成功后,在DDMS的Devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息。
4、在Devices 中,点击要监控的程序。
5、点击Devices视图界面中最上方一排图标中的“Update Heap”。
6、点击Heap视图。
7、点击Heap视图中的“Cause GC”按钮。
8、到此为止需检测的进程就可以被监视。
操作如图所示:
说明:
1、点击“Cause GC”按钮相当于向虚拟机请求了一次垃圾回收操作;
2、当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化;
3、内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。
Heap视图中有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会存在内存泄漏。判断方法如下:
进入某应用,不断的操作该应用,同时注意观察data object的Total Size值。
正常情况下Total Size值都会稳定在一个有限的范围内。
反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被kill掉。
8.例如:ListView的工作原理简而言之是针对List中每个item, adapter都会调用一个getView的方法获得布局视图。我们一般会Inflate一个新的View,填充数据并返回显示。当然如果我们的Item很多话(比如上万个),都会新建一个View吗?很明显这样内存是接受不了的。因此优化就开始了,我们在getView()方法中使用了convertView == null的判断,这是Android已经给我们提供了Recycler机制了,我们就应该利用此机制,而不是每次都去inflate一个View。除此之外,我们还是从getView中的每一个方法调用去查看,发现其实我们拿到convertView的时候,每次都会根据这个布局去findViewById。因此,应使用一个静态类,保存xml中的各个子View的引用关系,这样就不必要每次都去解析xml了,而这个静态类就是代码中的ViewText。
其示例代码:
public class ExamDataAdapter extends BaseAdapter { private List<Exam> exams = null; private LayoutInflater inflater; private int resource;// 绑定的条目界面 public ExamDataAdapter(List<Exam> exam, Context context, int id) { this.resource = id; this.exams = exam; this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } public int getCount() { return exams.size(); } public Object getItem(int position) { return exams.get(position); } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { TextView examCount = null; TextView examName = null; TextView examNumber = null; if (convertView == null) { convertView = inflater.inflate(resource, null); examName = (TextView) convertView.findViewById(R.id.test_list_name); examCount = (TextView) convertView.findViewById(R.id.test_list_count); examNumber = (TextView) convertView.findViewById(R.id.test_list_id); ViewText viewText = new ViewText(); viewText.examName = examName; viewText.examCount = examCount; viewText.examNumber = examNumber; convertView.setTag(viewText); } else { ViewText viewText = (ViewText) convertView.getTag(); examName = viewText.examName; examCount = viewText.examCount; examNumber = viewText.examNumber; } Exam exam = exams.get(position); examName.setText(exam.getExam_Name().trim()); examCount.setText(exam.getExam_Count() + "".trim()); examNumber.setText(exam.getExam_ID() + "".trim()); return convertView; } public final class ViewText { public TextView examCount; public TextView examName; public TextView examNumber; } }
这种优化方式在2009年 Google IO开发者大会中已做说明,属于ViewHolder类型,其优化结果如图所示。
9.资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:
Cursor cursor = getContentResolver().query(uri...); if (cursor.moveToNext()) { ... ... }
修正示例代码:
Cursor cursor = null; try { cursor = getContentResolver().query(uri...); if (cursor != null &&cursor.moveToNext()) { ... ... } } finally { if (cursor != null) { try { cursor.close(); } catch (Exception e) { //ignore this } } }
10.一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用registerReceiver后未调用unregisterReceiver。
比如:假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。
但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。
虽然有些系统程序,它本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。
11.我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
12.(此段内容摘自Android官网)
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
- 你的设备可以为每个应用程序分配多大的内存?
- 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
- 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
- 图片的尺寸和大小,还有每张图片会占据多少内存空间。
- 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
- 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。
下面是一个使用 LruCache 来缓存图片的例子:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用内存值的1/8作为缓存的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重写此方法来衡量每张图片的大小,默认返回图片数量。 return bitmap.getByteCount() / 1024; } }; } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。
public void loadBitmap(int resId, ImageView imageView) { final String imageKey = String.valueOf(resId); final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); } }
BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { // 在后台加载图片。 @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } }
大体过程就如上面的例子所示。下面来一个简单的完整的例子:
public class MainActivity extends Activity { private LruCache<String, Bitmap> mMemoryCache; private ImageView iv; public String str = "http://wenwen.sogou.com/p/20100623/20100623101110-601052657.jpg"; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.main); iv = (ImageView) findViewById(R.id.iv); // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用内存值的1/8作为缓存的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重写此方法来衡量每张图片的大小,默认返回图片数量。 return bitmap.getByteCount() / 1024; } }; loadBitmap(str, iv); } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); } public void loadBitmap(String url, ImageView imageView) { String imageKey = url; Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.empty_photo); BitmapWorkerTask1 task = new BitmapWorkerTask1(); task.execute(url); } } /** * 异步下载图片的任务。 * * @author guolin */ class BitmapWorkerTask1 extends AsyncTask<String, Void, Bitmap> { /** * 图片的URL地址 */ private String imageUrl; @Override protected Bitmap doInBackground(String... params) { imageUrl = params[0]; // 在后台开始下载图片 Bitmap bitmap = downloadBitmap(imageUrl); if (bitmap != null) { // 图片下载完成后缓存到LrcCache中 addBitmapToMemoryCache(imageUrl, bitmap); } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); iv.setImageBitmap(bitmap); } /** * 建立HTTP请求,并获取Bitmap对象。 * * @param imageUrl * 图片的URL地址 * @return 解析后的Bitmap对象 */ private Bitmap downloadBitmap(String imageUrl) { Bitmap bitmap = null; HttpURLConnection con = null; try { URL url = new URL(imageUrl); con = (HttpURLConnection) url.openConnection(); con.setConnectTimeout(5 * 1000); con.setReadTimeout(10 * 1000); if (con.getResponseCode() == 200) { bitmap = BitmapFactory.decodeStream(con.getInputStream()); } else { System.out.println("输入的路径不存在"); } } catch (Exception e) { e.printStackTrace(); } finally { if (con != null) { con.disconnect(); } } if (bitmap != null) { return bitmap; } return null; } } }
运行结果如下(来张吾女王的图片,嘿嘿):
PS:如果读者对第6段中提到的 强引用、软引用、弱引用、虚引用 不了解,可以查看相关博文: