加载图片内存溢出的问题和解决办法
以下我从四个个方面来讨论:
- 为什么加载图片会内存溢出?
- 如何通过网络加载图片?
- 如何解决内存溢出?
- 日常开发如何做?
安卓的内存机制(为什么会产生内存溢出?)
安卓内存机制:
安卓中应用程序在运行时,系统会给该应用分配一定的内存,系统不同大小可能存在出入,一般为16M,所以如果加载大量图片,特别是高清图片,内存就有点吃紧。而我们知道java虚拟机的内存回收机制,不会去回收强引用的对象,况且垃圾回收机制本身就不及时。(下文会谈到java的内存回收机制)。图片所占内存较大,容易导致OOM(内存溢出)
- java的内存回收机制:我们都知道java虚拟机中有堆内存和栈内存:
- 栈中一般存放成员变量,对象的引用等。
- 堆内存一般存放对象,类,集合等。
- 默认栈中的引用对堆中的对象是强引用关系,不会被回收
概念:引用
- 默认强引用, 垃圾回收器不会回收
- 软引用, 垃圾回收器会考虑回收 SoftReference
- 弱引用, 垃圾回收器更会考虑回收 WeakReference
- 虚引用, 垃圾回收器最优先回收 PhantomReference
通过网络加载图片有哪些方法
- 1.通过URLConnection获取请求连接,将输入流转为Bitmap对象在ImageView中呈现,这种是最简单粗暴的做法,没有进行任何优化。**
public Bitmap download(String url) {
HttpURLConnection conn;
try {
URL murl = new URL(url);
conn = (HttpURLConnection) murl.openConnection();
conn.setRequestMethod("GET");// 请求方式,必须大写
conn.setConnectTimeout(5000);// 连接超时时间
conn.setReadTimeout(5000);// 读取超时时间
conn.connect();// 开始连接
int code = conn.getResponseCode();// 获取返回码
if (code == 200) {
// 成功后获取流,进行处理
InputStream is = conn.getInputStream();
// 根据流来获取对应的数据,这里是图片,所以直接根据流得到bitmap对象
Bitmap bitmap = BitmapFactory.decodeStream(is);
return bitmap;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
- 2.通过开源框架xUtils中的BitmapUtils,调用display(ImageView iv,String url)方法,将对应url的图片展示在ImageView中,这是开发中常用的解决方案,并且解决了内存溢出的问题,并且提供了三级缓存的机制**
//代码说明:这是一个ListView的适配器,每一个item中展示的图片都来自网络,每次加载图片时,调用了BitmapUtils 的display方法,就会下载对应的图片并显示在ImageView中
class PicListAdapter extends BaseAdapter {
public BitmapUtils bitmapUtils; //xUtils的图片加载工具类
// public MyBitmapUtils bitmapUtils;
public PicListAdapter() {
bitmapUtils = new BitmapUtils(mActivity);
//设置默认加载的图片,当网络请求失败,默认显示的图片
bitmapUtils.configDefaultLoadFailedImage(R.drawable.pic_item_list_default);
bitmapUtils = new MyBitmapUtils();
}
@Override
public int getCount() {
return photosDataList.size();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder ;
if(convertView==null){
holder = new ViewHolder();
convertView = View.inflate(mActivity, R.layout.item_list_pic, null);
holder.ivNews = (ImageView) convertView.findViewById(R.id.iv_pic);
holder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
PhotosData item = photosDataList.get(position);
bitmapUtils.display(holder.ivNews, item.listimage);
// bitmapUtils.display(holder.ivNews, item.listimage, R.drawable.pic_item_list_default);
holder.tvTitle.setText(item.title);
return convertView;
}
@Override
public PhotosData getItem(int position) {
return photosDataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
}
- 3.自己写一个利用三级缓存加载图片的方案,并且讨论解决内存溢出的问题(我们自己写是为了更深入了解安卓中内存优化的机制,图片加载的细节等等,实际上xUtils已经解决的很完美了)**
MyBitmapUtils工具类(自己实现的加载图片的工具类)使用时,只需要调用display方法,传入相应的ImageView 图片的url和默认显示的图片资源ID, 其中用到的工具类代码附在后面。
package com.cattsoft.zhhxa.utils;
import android.graphics.Bitmap;
import android.widget.ImageView;
/**
* 利用三级缓存加载图片的工具类
*
* @author ZXJM
* @date 2016年8月23日
*
*/
public class MyBitmapUtils {
private NetCacheUtils mNetCacheUtil;//网络缓存工具类
private LocalCacheUtils mLocalCacheUtil;//本地缓存工具类
private MemoryCacheUtils mMemoryCacheUtil;//内存缓存工具类
public MyBitmapUtils() {
mMemoryCacheUtil = new MemoryCacheUtils();
mLocalCacheUtil = new LocalCacheUtils();
mNetCacheUtil = new NetCacheUtils(mLocalCacheUtil, mMemoryCacheUtil);
}
/**
*
* @param imageView
* 要展示加载图片的ImageView
* @param url
* 加载图片的链接
* @param resId
* 默认图片的资源id
*/
public void display(ImageView imageView, String url, int resId) {
// 设置默认图片
imageView.setImageResource(resId);
Bitmap bitmap = null;
// 0.先从内存加载,如果内存中有值
bitmap = mMemoryCacheUtil.getMemoryCache(url);
if (bitmap != null) {
System.out.println("从内存中加载");
imageView.setImageBitmap(bitmap);
return;
}
// 1.先从本地加载,判断是否有本地缓存
bitmap = mLocalCacheUtil.getLocalCache(url);
if (bitmap != null) {
System.out.println("从本地加载");
imageView.setImageBitmap(bitmap);
//写内存缓存
mMemoryCacheUtil.setMemoryCache(url, bitmap);
return;
}
// 2.从网络加载
mNetCacheUtil.getBitmapFromNet(imageView, url);
System.out.println("从网络加载");
}
}
NetCacheUtils 网络缓存工具类
package com.cattsoft.zhhxa.utils;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;
/**
* 网络缓存工具
*
* @author ZXJM
* @date 2016年8月23日
*
*/
public class NetCacheUtils {
private LocalCacheUtils mLocalCacheUtil;
private MemoryCacheUtils mMemoryCacheUtil;
public NetCacheUtils(LocalCacheUtils localCacheUtil,
MemoryCacheUtils memoryCacheUtil) {
super();
this.mLocalCacheUtil = localCacheUtil;
this.mMemoryCacheUtil = memoryCacheUtil;
}
// 从网络加载图片
public void getBitmapFromNet(ImageView imageView, String url) {
// imageView,url 两个参数会封装为数组传给doInBackground
new BitmapTask().execute(imageView, url);
}
/**
* AsyncTask的用法
*
* @author ZXJM
* @date 2016年8月23日 三个泛型的含义:参1:doInBackground的参数类型 参2:onProgressUpdate的参数类型
* 参3:doInBackground 加载完的返回类型,和onPostExecute 加载结束后处理的入参类型
*/
class BitmapTask extends AsyncTask<Object, Integer, Bitmap> {
private ImageView imageView;
private String url;
/**
* 1.预加载 ,运行在主线程
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
// System.out.println("预加载");
}
/**
* 2.正在加载(核心方法),运行在子线程
*/
@Override
protected Bitmap doInBackground(Object... params) {
// System.out.println("正在加载");
imageView = (ImageView) params[0];
url = (String) params[1];
imageView.setTag(url);// 打标记
Bitmap bitmap = download(url);
return bitmap;
}
/**
* 3.进度更新(如果下载文件),运行在主线程
*/
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
/**
* 4.加载结束,运行在主线程
*/
@Override
protected void onPostExecute(Bitmap result) {
// System.out.println("加载结束");
// 由于listview的重用机制导致imageview对象可能被多个item共用,
// 从而可能将错误的图片设置给了imageView对象
// 所以需要在此处校验, 判断是否是正确的图片
if (result != null) {
String url = (String) imageView.getTag();
if (url != null && url.equals(this.url)) {
// 从网络加载图片
imageView.setImageBitmap(result);
System.out.println("从网络加载图片啦.....");
// 写本地缓存
mLocalCacheUtil.setLocalCache(url, result);
//写内存缓存
mMemoryCacheUtil.setMemoryCache(url, result);
}
}
}
}
/**
* 根据url下载图片
*
* @param url
* @return
*/
public Bitmap download(String url) {
HttpURLConnection conn;
try {
URL murl = new URL(url);
conn = (HttpURLConnection) murl.openConnection();
conn.setRequestMethod("GET");// 请求方式,必须大写
conn.setConnectTimeout(5000);// 连接超时时间
conn.setReadTimeout(5000);// 读取超时时间
conn.connect();// 开始连接
int code = conn.getResponseCode();// 获取返回码
if (code == 200) {
// 成功后获取流,进行处理
InputStream is = conn.getInputStream();
// 根据流来获取对应的数据,这里是图片,所以直接根据流得到bitmap对象
Bitmap bitmap = BitmapFactory.decodeStream(is);
return bitmap;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
LocalCacheUtils 本地缓存工具类
package com.cattsoft.zhhxa.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;
/**
* 本地缓存工具类
*
* @author ZXJM
* @date 2016年8月23日
*
*/
public class LocalCacheUtils {
private static final String LOCAL_CACHE_PATH = Environment
.getExternalStorageDirectory().getAbsolutePath()+"/zhxa_cache";
//写本地缓存
public void setLocalCache(String url,Bitmap bitmap) {
File dir = new File(LOCAL_CACHE_PATH);
if(!dir.exists() || !dir.isDirectory()){
dir.mkdirs();//创建文件夹
}
try {
String fileName = MD5Encoder.encode(url);//采用MD5加密文件名
File cacheFile = new File(dir, fileName);
// 参1:图片格式;参2:压缩比例0-100; 参3:输出流
bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(cacheFile));
} catch (Exception e) {
e.printStackTrace();
}
}
//读本地缓存
public Bitmap getLocalCache(String url) {
try {
File cacheFile = new File(LOCAL_CACHE_PATH,MD5Encoder.encode(url));
if(cacheFile.exists()){
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(cacheFile));
return bitmap;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
MemoryCacheUtils 内存缓存工具类
package com.cattsoft.zhhxa.utils;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import android.graphics.Bitmap;
/**
* 内存缓存工具类
* 1.第一次优化,利用软引用解决可能的OOM异常
*
* @author ZXJM
* @date 2016年8月23日
*
*/
public class MemoryCacheUtils {
// private HashMap<String, Bitmap> hash;
private HashMap<String, SoftReference<Bitmap>> hash;
// 写内存缓存
public void setMemoryCache(String url, Bitmap bitmap) {
// if (hash == null) {
// hash = new HashMap<String, Bitmap>();
// }
// hash.put(url, bitmap);
if(hash == null){
hash = new HashMap<String, SoftReference<Bitmap>>();
}
//使用软引用把Bitmap包装起来
SoftReference<Bitmap> soft = new SoftReference<Bitmap>(bitmap);
hash.put(url, soft);
}
// 读内存缓存
public Bitmap getMemoryCache(String url) {
// if (hash != null && hash.containsKey(url)) {
// Bitmap bitmap = hash.get(url);
// return bitmap;
// }
if(hash!=null && hash.containsKey(url)){
SoftReference<Bitmap> soft = hash.get(url);
Bitmap bitmap = soft.get();
return bitmap;
}
return null;
}
}
如何处理内存溢出?
上面我们给出了一套自己实现的利用三级缓存加载图片的方案,这很好解决了加载速度,节省用户流量等体验问题,但随着图片数量的增多,内存缓存这一环节就会暴露内存溢出的问题。
优化1.利用软引用解决内存缓存时可能出现的内存溢出
- 上文我们提到安卓中的几种引用,我们在内存缓存时,直接将Bitmap对象缓存在集合中,默认是强引用,随着图片数量的增多,应用所分配到的内存就越用越少,最后OOM,但如果我们把缓存的Bitmap对象封装成软引用,那么在内存将要不够时,系统就会回收软引用对应的内存。上面代码注释部分有。
优化2.利用LruCache解决内存缓存
- 上面我们用软引用做了优化,但是在Android2.3之后,软引用更倾向于被系统垃圾回收机制回收,也就是说,这时候软引用变的不可靠,刚一创建,还没使用,就被回收,这当然不是我们想要的。因此LruCache这个类很好的解决了内存缓存的问题,可以替代软引用。见上面代码。
小结(重要)
以上分析,我们知道,在通过网络加载图片时,为了更好的体验,我们使用了三级缓存的一个机制,并且在内存缓存中利用Lrucache进行处理,防止OOM异常。但是,我们上面自己实现的方案最多算做一个Demo,可以加深我们对网络加载图片内存溢出机制有更深入的理解。开发中,我们还是使用xUtils,因为xUtils已经对这些问题做了很好的规避,并且非常稳定,出现异常的概率远比我们自己写的Demo低。