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; } }