APP针对图片的OOM解决方式分析
在手机APP的开发中,RAM分配是开发的时候须要时时刻刻考虑的关键问题,近期做的project中发现载入的图片太多或图片过大时
常常出现OOM问题,找网上资料也提供了非常多方法,但自己感觉有点乱,特此,今天在不同型号的三款安卓手机上做了測试,由于有
效果也有结果,今天就做个具体的总结。以供朋友们共同交流学习,也供自己以后在解决OOM问题上有所提高,大家耐心看,肯定有
收获的,里面的非常多东西也是学习參考网络资料使用的,先来简单讲下下:
一般我们大家在遇到内存问题的时候经常使用的方式网上也有相关资料,大体例如以下几种:一:在内存引用上做些处理,经常使用的有软引用、强化引用、弱引用
二:在内存中载入图片时直接在内存中做处理,如:边界压缩
三:动态回收内存
四:优化Dalvik虚拟机的堆内存分配
五:自己定义堆内存大小
但是真的有这么简单吗,就用以上方式就能解决OOM了?不是的,继续来看...
一、软引用(SoftReference)、虚引用(PhantomRefrence)、弱引用(WeakReference),这三个类是对heap中java对象的应用,通过这个三个类能够和gc做简单的交互,除了这三个以外另一个是最经常使用的强引用
1.1:强引用。比如以下代码:Object o=new Object();上面代码中第一句是在heap堆中创建新的Object对象通过o引用这个对象,第二句是通过o建立o1到new Object()这个heap堆中的对象的引用,这两个引用都是强引用.仅仅要存在对heap中对象的引用。gc就不会收集该对象.假设通过例如以下代码:
Object o1=o;
o=null;
o1=null
heap中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。
应用的强弱顺序是强、软、弱、和虚。
对于对象是属于哪种可及的对象。由他的最强的引用决定。
例如以下:
String abc=new String("abc"); //1
SoftReference<String> abcSoftRef=new SoftReference<String>(abc); //2
WeakReference<String> abcWeakRef = new WeakReference<String>(abc); //3
abc=null; //4
abcSoftRef.clear();//5
上面的代码中:
第一行在heap对中创建内容为“abc”的对象。并建立abc到该对象的强引用,该对象是强可及的。
第二行和第三行分别建立对heap中对象的软引用和弱引用,此时heap中的对象仍是强可及的。第四行之后heap中对象不再是强可及的。变成软可及的。相同第五行运行之后变成弱可及的。
软引用是主要用于内存敏感的快速缓存。在jvm报告内存不足之前会清除全部的软引用,这样以来gc就有可能收集软可及的对象。可能解决内存吃紧问题。避免内存溢出。什么时候会被收集取决于gc的算法和gc执行时可用内存的大小。当gc决定要收集软引用是执行下面过程,以上面的abcSoftRef为例:
1 首先将abcSoftRef的referent设置为null。不再引用heap中的new String("abc")对象。
2 将heap中的new String("abc")对象设置为可结束的(finalizable)。
3 当heap中的new String("abc")对象的finalize()方法被执行并且该对象占用的内存被释放, abcSoftRef被加入到它的ReferenceQueue中。
注:对ReferenceQueue软引用和弱引用能够有可无,可是虚引用必须有,參见:
Reference(T paramT, ReferenceQueue<? super T>paramReferenceQueue)
被 Soft Reference 指到的对象,即使没有不论什么 Direct Reference。也不会被清除。一直要到 JVM 内存不足且 没有 Direct Reference 时才会清除。SoftReference 是用来设计 object-cache 之用的。如此一来 SoftReference 不但能够把对象 cache 起来,也不会造成内存不足的错误 (OutOfMemoryError)。我认为 Soft Reference 也适合拿来实作 pooling 的技巧。
A obj = new A();1.3:弱引用
Refenrence sr = new SoftReference(obj);
//引用时
if(sr!=null){
obj = sr.get();
}else{
obj = new A();
sr = new SoftReference(obj);
}
当gc碰到弱可及对象。并释放abcWeakRef的引用,收集该对象。
可是gc可能须要对此运用才干找到该弱可及对象。
通过例如以下代码能够了明了的看出它的作用:
gc收集弱可及对象的运行过程和软可及一样,仅仅是gc不会依据内存情况来决定是不是收集该对象。假设你希望能随时取得某对象的信息。但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference。String abc=new String("abc");WeakReference<String> abcWeakRef = new WeakReference<String>(abc);abc=null;System.out.println("before gc: "+abcWeakRef.get());System.gc();System.out.println("after gc: "+abcWeakRef.get());执行结果:before gc: abcafter gc: null
A obj = new A();
WeakReference wr = new WeakReference(obj);
obj = null;
//等待一段时间。obj对象就会被垃圾回收
...
if (wr.get()==null) {
System.out.println("obj 已经被清除了 ");
} else {
System.out.println("obj 尚未被清除。其信息是 "+obj.toString());
}
...
}
在此例中。透过 get() 能够取得此 Reference 的所指到的对象,假设返回值为 null 的话。代表此对象已经被清除。这类的技巧,在设计 Optimizer 或 Debugger 这类的程序时常会用到,由于这类程序须要取得某对象的信息,可是不能够 影响此对象的垃圾收集。
1.4:虚引用
就是没有的意思,建立虚引用之后通过get方法返回结果始终为null,通过源码你会发现,虚引用通向会把引用的对象写进referent,仅仅是get方法返回结果为null.先看一下和gc交互的过程在说一下他的作用.
1.4.1 不把referent设置为null, 直接把heap中的new String("abc")对象设置为可结束的(finalizable).
1.4.2 与软引用和弱引用不同, 先把PhantomRefrence对象加入到它的ReferenceQueue中.然后在释放虚可及的对象.
你会发如今收集heap中的new String("abc")对象之前,你就能够做一些其它的事情.通过下面代码能够了解他的作用.
import java.lang.ref.PhantomReference;import java.lang.ref.Reference;import java.lang.ref.ReferenceQueue;import java.lang.reflect.Field;public class Test {public static boolean isRun = true;public static void main(String[] args) throws Exception {String abc = new String("abc");System.out.println(abc.getClass() + "@" + abc.hashCode());final ReferenceQueue referenceQueue = new ReferenceQueue<String>();new Thread() {public void run() {while (isRun) {Object o = referenceQueue.poll();if (o != null) {try {Field rereferent = Reference.class.getDeclaredField("referent");rereferent.setAccessible(true);Object result = rereferent.get(o);System.out.println("gc will collect:"+ result.getClass() + "@"+ result.hashCode());} catch (Exception e) {e.printStackTrace();}}}}}.start();PhantomReference<String> abcWeakRef = new PhantomReference<String>(abc,referenceQueue);abc = null;Thread.currentThread().sleep(3000);System.gc();Thread.currentThread().sleep(3000);isRun = false;}}
结果为class java.lang.String@96354gc will collect:class java.lang.String@96354
总结:无论是SoftReference、Weak Reference其中的哪一个,在内存解决的问题上真的非常有帮助的,OOM通常是在连续decode的
时候就出现的假设是ListView、GridView等包括多个子控件的控件中使用时。肯定是要用到缓存的。不然肯定报错,典型的处理方
法就是在你第一次获取(decode的时候)到Bitmap的同一时候讲这个Bitmap以Map<Key,Value>的形式存入到SoftReference中,这个地方我
给朋友你贴上一个类。你看下大体的思路
package net.oschina.app.common;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import net.oschina.app.AppException;
import net.oschina.app.api.ApiClient;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Message;
import android.widget.ImageView;
/**
* 异步线程载入图片工具类
* 使用说明:
* BitmapManager bmpManager;
* bmpManager = new BitmapManager(BitmapFactory.decodeResource(context.getResources(), R.drawable.loading));
* bmpManager.loadBitmap(imageURL, imageView);
*
*/
public class BitmapManager {
private static HashMap<String, SoftReference<Bitmap>> cache;
private static ExecutorService pool;
private static Map<ImageView, String> imageViews;
private Bitmap defaultBmp;
static {
cache = new HashMap<String, SoftReference<Bitmap>>();
pool = Executors.newFixedThreadPool(5); //固定线程池
imageViews = Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
}
public BitmapManager(){}
public BitmapManager(Bitmap def) {
this.defaultBmp = def;
}
/**
* 设置默认图片
* @param bmp
*/
public void setDefaultBmp(Bitmap bmp) {
defaultBmp = bmp;
}
/**
* 载入图片
* @param url
* @param imageView
*/
public void loadBitmap(String url, ImageView imageView) {
loadBitmap(url, imageView, this.defaultBmp, 0, 0);
}
/**
* 载入图片-可设置载入失败后显示的默认图片
* @param url
* @param imageView
* @param defaultBmp
*/
public void loadBitmap(String url, ImageView imageView, Bitmap defaultBmp) {
loadBitmap(url, imageView, defaultBmp, 0, 0);
}
/**
* 载入图片-可指定显示图片的高宽
* @param url
* @param imageView
* @param width
* @param height
*/
public void loadBitmap(String url, ImageView imageView, Bitmap defaultBmp, int width, int height) {
imageViews.put(imageView, url);
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap != null) {
//显示缓存图片
imageView.setImageBitmap(bitmap);
} else {
//载入SD卡中的图片缓存
String filename = FileUtils.getFileName(url);
String filepath = imageView.getContext().getFilesDir() + File.separator + filename;
File file = new File(filepath);
if(file.exists()){
//显示SD卡中的图片缓存
Bitmap bmp = ImageUtils.getBitmap(imageView.getContext(), filename);
imageView.setImageBitmap(bmp);
}else{
//线程载入网络图片
imageView.setImageBitmap(defaultBmp);
queueJob(url, imageView, width, height);
}
}
}
/**
* 从缓存中获取图片
* @param url
*/
public Bitmap getBitmapFromCache(String url) {
Bitmap bitmap = null;
if (cache.containsKey(url)) {
bitmap = cache.get(url).get();
}
return bitmap;
}
/**
* 从网络中载入图片
* @param url
* @param imageView
* @param width
* @param height
*/
public void queueJob(final String url, final ImageView imageView, final int width, final int height) {
/* Create handler in UI thread. */
final Handler handler = new Handler() {
public void handleMessage(Message msg) {
String tag = imageViews.get(imageView);
if (tag != null && tag.equals(url)) {
if (msg.obj != null) {
imageView.setImageBitmap((Bitmap) msg.obj);
try {
//向SD卡中写入图片缓存
ImageUtils.saveImage(imageView.getContext(), FileUtils.getFileName(url), (Bitmap) msg.obj);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
};
pool.execute(new Runnable() {
public void run() {
Message message = Message.obtain();
message.obj = downloadBitmap(url, width, height);
handler.sendMessage(message);
}
});
}
/**
* 下载图片-可指定显示图片的高宽
* @param url
* @param width
* @param height
*/
private Bitmap downloadBitmap(String url, int width, int height) {
Bitmap bitmap = null;
try {
//http载入图片
bitmap = ApiClient.getNetBitmap(url);
if(width > 0 && height > 0) {
//指定显示图片的高宽
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
}
//放入缓存
cache.put(url, new SoftReference<Bitmap>(bitmap));
} catch (AppException e) {
e.printStackTrace();
}
return bitmap;
}
}
二、在内存中压缩做了下測试,对于少量不太大的图片这样的方式可行,但太多而又大的图片用个笨的方式就是,先在内存中压
缩。再用软引用避免OOM,两种方式代码例如以下。大家可參考下:
方式一代码例如以下:方式二代码例如以下:@SuppressWarnings("unused")private Bitmap copressImage(String imgPath){File picture = new File(imgPath);Options bitmapFactoryOptions = new BitmapFactory.Options();//以下这个设置是将图片边界不可调节变为可调节bitmapFactoryOptions.inJustDecodeBounds = true;bitmapFactoryOptions.inSampleSize = 2;int outWidth = bitmapFactoryOptions.outWidth;int outHeight = bitmapFactoryOptions.outHeight;bmap = BitmapFactory.decodeFile(picture.getAbsolutePath(),bitmapFactoryOptions);float imagew = 150;float imageh = 150;int yRatio = (int) Math.ceil(bitmapFactoryOptions.outHeight/ imageh);int xRatio = (int) Math.ceil(bitmapFactoryOptions.outWidth / imagew);if (yRatio > 1 || xRatio > 1) {if (yRatio > xRatio) {bitmapFactoryOptions.inSampleSize = yRatio;} else {bitmapFactoryOptions.inSampleSize = xRatio;}}bitmapFactoryOptions.inJustDecodeBounds = false;bmap = BitmapFactory.decodeFile(picture.getAbsolutePath(),bitmapFactoryOptions);if(bmap != null){//ivwCouponImage.setImageBitmap(bmap);return bmap;}return null;}
package com.lvguo.scanstreet.activity;import java.io.File;import java.lang.ref.SoftReference;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import android.app.Activity;import android.app.AlertDialog;import android.content.Context;import android.content.DialogInterface;import android.content.Intent;import android.content.res.TypedArray;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.BitmapFactory.Options;import android.os.Bundle;import android.view.View;import android.view.ViewGroup;import android.view.WindowManager;import android.widget.AdapterView;import android.widget.AdapterView.OnItemLongClickListener;import android.widget.BaseAdapter;import android.widget.Gallery;import android.widget.ImageView;import android.widget.Toast;import com.lvguo.scanstreet.R;import com.lvguo.scanstreet.data.ApplicationData;/*** @Title: PhotoScanActivity.java* @Description: 照片预览控制类* @author XiaoMa*/public class PhotoScanActivity extends Activity {private Gallery gallery ;private List<String> ImageList;private List<String> it ;private ImageAdapter adapter ;private String path ;private String shopType;private HashMap<String, SoftReference<Bitmap>> imageCache = null;private Bitmap bitmap = null;private SoftReference<Bitmap> srf = null;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);setContentView(R.layout.photoscan);Intent intent = this.getIntent();if(intent != null){if(intent.getBundleExtra("bundle") != null){Bundle bundle = intent.getBundleExtra("bundle");path = bundle.getString("path");shopType = bundle.getString("shopType");}}init();}private void init(){imageCache = new HashMap<String, SoftReference<Bitmap>>();gallery = (Gallery)findViewById(R.id.gallery);ImageList = getSD();if(ImageList.size() == 0){Toast.makeText(getApplicationContext(), "无照片,请返回拍照后再使用预览", Toast.LENGTH_SHORT).show();return ;}adapter = new ImageAdapter(this, ImageList);gallery.setAdapter(adapter);gallery.setOnItemLongClickListener(longlistener);}/*** Gallery长按事件操作实现*/private OnItemLongClickListener longlistener = new OnItemLongClickListener() {@Overridepublic boolean onItemLongClick(AdapterView<?> parent, View view,final int position, long id) {//此处加入长按事件删除照片实现AlertDialog.Builder dialog = new AlertDialog.Builder(PhotoScanActivity.this);dialog.setIcon(R.drawable.warn);dialog.setTitle("删除提示");dialog.setMessage("你确定要删除这张照片吗?");dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {File file = new File(it.get(position));boolean isSuccess;if(file.exists()){isSuccess = file.delete();if(isSuccess){ImageList.remove(position);adapter.notifyDataSetChanged();//gallery.setAdapter(adapter);if(ImageList.size() == 0){Toast.makeText(getApplicationContext(), getResources().getString(R.string.phoSizeZero), Toast.LENGTH_SHORT).show();}Toast.makeText(getApplicationContext(), getResources().getString(R.string.phoDelSuccess), Toast.LENGTH_SHORT).show();}}}});dialog.setNegativeButton("取消",new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {dialog.dismiss();}});dialog.create().show();return false;}};/*** 获取SD卡上的全部图片文件* @return*/private List<String> getSD() {/* 设定眼下所在路径 */File fileK ;it = new ArrayList<String>();if("newadd".equals(shopType)){//假设是从查看本人新增列表项或商户列表项进来时fileK = new File(ApplicationData.TEMP);}else{//此时为纯粹新增fileK = new File(path);}File[] files = fileK.listFiles();if(files != null && files.length>0){for(File f : files ){if(getImageFile(f.getName())){it.add(f.getPath());Options bitmapFactoryOptions = new BitmapFactory.Options();//以下这个设置是将图片边界不可调节变为可调节bitmapFactoryOptions.inJustDecodeBounds = true;bitmapFactoryOptions.inSampleSize = 5;int outWidth = bitmapFactoryOptions.outWidth;int outHeight = bitmapFactoryOptions.outHeight;float imagew = 150;float imageh = 150;int yRatio = (int) Math.ceil(bitmapFactoryOptions.outHeight/ imageh);int xRatio = (int) Math.ceil(bitmapFactoryOptions.outWidth / imagew);if (yRatio > 1 || xRatio > 1) {if (yRatio > xRatio) {bitmapFactoryOptions.inSampleSize = yRatio;} else {bitmapFactoryOptions.inSampleSize = xRatio;}}bitmapFactoryOptions.inJustDecodeBounds = false;bitmap = BitmapFactory.decodeFile(f.getPath(),bitmapFactoryOptions);//bitmap = BitmapFactory.decodeFile(f.getPath());srf = new SoftReference<Bitmap>(bitmap);imageCache.put(f.getName(), srf);}}}return it;}/*** 获取图片文件方法的详细实现* @param fName* @return*/private boolean getImageFile(String fName) {boolean re;/* 取得扩展名 */String end = fName.substring(fName.lastIndexOf(".") + 1, fName.length()).toLowerCase();/* 按扩展名的类型决定MimeType */if (end.equals("jpg") || end.equals("gif") || end.equals("png")|| end.equals("jpeg") || end.equals("bmp")) {re = true;} else {re = false;}return re;}public class ImageAdapter extends BaseAdapter{/* 声明变量 */int mGalleryItemBackground;private Context mContext;private List<String> lis;/* ImageAdapter的构造符 */public ImageAdapter(Context c, List<String> li) {mContext = c;lis = li;TypedArray a = obtainStyledAttributes(R.styleable.Gallery);mGalleryItemBackground = a.getResourceId(R.styleable.Gallery_android_galleryItemBackground, 0);a.recycle();}/* 几定要重写的方法getCount,传回图片数目 */public int getCount() {return lis.size();}/* 一定要重写的方法getItem,传回position */public Object getItem(int position) {return lis.get(position);}/* 一定要重写的方法getItemId,传并position */public long getItemId(int position) {return position;}/* 几定要重写的方法getView,传并几View对象 */public View getView(int position, View convertView, ViewGroup parent) {System.out.println("lis:"+lis);File file = new File(it.get(position));SoftReference<Bitmap> srf = imageCache.get(file.getName());Bitmap bit = srf.get();ImageView i = new ImageView(mContext);i.setImageBitmap(bit);i.setScaleType(ImageView.ScaleType.FIT_XY);i.setLayoutParams( new Gallery.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,WindowManager.LayoutParams.WRAP_CONTENT));return i;}}}
上面两种方式第一种直接使用边界压缩。另外一种在使用边界压缩的情况下间接的使用了软引用来避免OOM,但大家都知道,这些
函数在完毕decode后,终于都是通过java层的createBitmap来完毕的。须要消耗很多其它内存,假设图片多且大,这样的方式还是会引用
OOM异常的。不着急,有的是办法解决,继续看,下面方式也大有妙用的:
上面代码与以下代码大家可分开使用。也可有效缓解内存问题。1. InputStream is = this.getResources().openRawResource(R.drawable.pic1);BitmapFactory.Options options=new BitmapFactory.Options();options.inJustDecodeBounds = false;options.inSampleSize = 10; //width,hight设为原来的十分一Bitmap btp =BitmapFactory.decodeStream(is,null,options);2. if(!bmp.isRecycle() ){bmp.recycle() //回收图片所占的内存system.gc() //提醒系统及时回收}
/** 这个地方大家别搞混了,为了方便把两个贴一起了,使用的时候记得分开使用
* 以最省内存的方式读取本地资源的图片
*/
public static Bitmap readBitMap(Context context, int resId){
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable = true;
opt.inInputShareable = true;
//获取资源图片
InputStream is = context.getResources().openRawResource(resId);
return BitmapFactory.decodeStream(is,null,opt);}
if(bitmapObject.isRecycled()==false) //假设没有回收bitmapObject.recycle();
对于Android平台来说,其托管层使用的Dalvik JavaVM从眼下的表现来看还有非常多地方能够优化处理。比方我们在开发一些大
型游戏或耗资源的应用中可能考虑手动干涉GC处理。使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法能够
增强程序堆内存的处理效率。当然详细原理我们能够參考开源project,这里我们仅说下用法: 代码例如以下:
private final static floatTARGET_HEAP_UTILIZATION = 0.75f;在程序onCreate时就能够调用VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
五、自己定义我们的应用须要多大的内存,这个好暴力哇,强行设置最小内存大小。代码例如以下:
private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;//设置最小heap内存为6MB大小VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
通过上面的五种方式结合的解决APP中相应的OOM问题,特别是图片多并且大的那种APP尤其要值得注意。