一.概述
我们知道安卓中的每个应用程序都会有最大的内存开销,如果在我们的程序中处理不当很容易就会导致OOM内存溢出而导致程序崩溃。最常见的就是图片的加载,现在的图片像素都是非常大的,每个像素点有4个字节,一张图片动不动就是几兆甚至十几兆,所以我们需要对图片进行一定的处理。通过本篇文章的学习,你知道如何按一定的采样率来加载缩小后的图片,图片的三级缓存以及如何在列表中加载的时候进行优化处理。
二.加载压缩后的图片
1.加载图片
BitmapFactory类提供了四个类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,然后我们可以调用ImageView的setImageBitmap方法将图片设置给控件完成图片的加载:
Bitmap bitmap= BitmapFactory.decodeByteArray(bytes,0,bytes.length);//方法一:通过字节数组得到Bitmap对象
Bitmap bitmap2= BitmapFactory.decodeFile("");//方法二:通过本地文件得到Bitmap对象
Bitmap bitmap3= BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);//方法三:通过资源文件得到Bitmap对象
Bitmap bitmap4= BitmapFactory.decodeStream(new InputStream() {//方法四:通过输入流得到Bitmap对象
@Override
public int read() throws IOException {
return 0;
}
});
iv.setImageBitmap(bitmap);
如果我们就这样给控件设置图片,那有的情况会出现bitmap的像素很高的情况比如10801920,它所占的内存:10801920*4/1024/1024=7.9M,如果同事有个两三张图片,那就很容易发生OOM了,那么我们如何进行压缩呢?
2.图片的压缩
其实在上面四中获取bitmap的方法中都有另外的一个重载方法含有一个BitmapFactory.Options参数,通过BitmapFactory.Options就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高Bitmap加载时的性能。
通过BitmapFactory.Options来缩放图片,主要用到了它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小;当inSampleSize大于1时,比如2,那么采样后的图片宽高均为原图大小的1/2,像素数为原图的1/4,其占有的内存大小也为原图的1/4,并且采样率同时作用于宽和高。我们可以根据我们的期望计算采样率进行压缩得到bitmap。
/**
* 得到压缩后的bitmap
* @param res
* @param resId
* @param reqWidth 期望图片宽(像素)
* @param reqHeight 期望图片高(像素)
* @return
*/
public static Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
final BitmapFactory.Options options= new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(res,resId,options);
//计算采样率
options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
options.inJustDecodeBounds=false;
return BitmapFactory.decodeResource(res,resId,options);
}
/**
* 获取采样率
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//获取图片的宽和高
int width = options.outWidth;
int height=options.outHeight;
int inSampleSize=1;
if (height>reqHeight || width>reqWidth){
final int halfHeight=height/2;
final int halfWidth=width/2;
//计算最大的采样率,采样率为2的指数
while ((halfHeight/inSampleSize)>=reqHeight && (halfHeight/inSampleSize)>=reqWidth){
inSampleSize *=2;
}
}
return inSampleSize;
}
这样我们就把一张像素很高的图片通过压缩加载到我们的控件上保证不出现oom。
三.图片的三级缓存
当我们在一个滑动列表上加载网络图片的时候,为了保证内存的使用始终维持在某一个合理的范围值之间,我们的想法是去处理那些移出屏幕外的图片,如果能保证屏幕外图片的内存销毁那就搞定了,可是有一个问题是移出屏幕外的图片如果用户再移进来,难道我们又要重新去加载吗?不是的,我们需要必免去加载已经加载过的图片,因此我们需要理解内存的缓存原理。
内存缓存技术最核心的类是LruCache,它的主要算法原理是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除,然后再添加新的缓存对象。图片的三级缓存原理:
去展示一张图片,我们先到内存中去访问是否加载过url的图片,有就直接展示,没有则去访问本地是否有加载过url的图片,有则保存到内存中并展示,没有则开启线程去下载到本地并保存到内存中并展示。可见,从内存中获取是最快的方式,其次是本地,最后是网络加载。图片三级缓存的工具类:
public class ImageUtils {
private static LruCache mCaches;
/**
* 定义上下文对象
*/
private Context mContext;
private static Handler mHandler;
//声明线程池,全局只有一个线程池,所有访问网络图片,只有这个池子去访问。
private static ExecutorService mPool;
//解决错位问题,定义一个存标记的集合
private Map mTags = new LinkedHashMap();
public ImageUtils(Context context) {
this.mContext = context;
if (mCaches == null) {
//申请内存空间
int maxSize = (int) (Runtime.getRuntime().freeMemory() / 4);
//实例化LruCache
mCaches = new LruCache(maxSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
//判断添加进入的value的占用内存的大小
//这里默认sizeOf是返回1,不占用,内存会不够用,所以要给它一个具体占用内存的大小
// return super.sizeOf(key, value);
//获取Bitmap的大小
return value.getRowBytes() * value.getHeight();
}
};
}
if (mHandler == null) {
//实例化Handler
mHandler = new Handler();
}
if (mPool == null) {
//创建固定大小的线程池
mPool = Executors.newFixedThreadPool(3);
//创建一个缓存的线程池,生产者和消费者,一个线程生产,必须得消费完成后再生产
/*Executors.newCachedThreadPool();
Executors.newSingleThreadExecutor();//创建一个单线程池
Executors.newScheduledThreadPool();//创建一个计划的任务池*/
}
}
/**
* 给imageView加载url对应的图片
*
* @param iv
* @param url
*/
public void display(ImageView iv, String url) {
//1.从内存中获取
Bitmap bitmap = mCaches.get(url);
if (bitmap != null) {
//内存中有,显示图片
iv.setImageBitmap(bitmap);
return;
}
//2.内存中没有,从本地获取
bitmap = loadFromLocal(url);
if (bitmap != null) {
//本地有,显示
iv.setImageBitmap(bitmap);
return;
}
//从网络中获取
loadFromNet(iv, url);
}
private void loadFromNet(ImageView iv, String url) {
mTags.put(iv, url);//url是ImageView最新的地址
//耗时操作
// new Thread(new LoadImageTask(iv, url)).start();
//用线程池去管理
mPool.execute(new LoadImageTask(iv, url));
// Future> submit = mPool.submit(new LoadImageTask(iv, url));
//取消的操作(有机率取消),而使用execute没有办法取消
// submit.cancel(true);
}
private class LoadImageTask implements Runnable {
private ImageView iv;
private String url;
public LoadImageTask(ImageView iv, String url) {
this.iv = iv;
this.url = url;
}
@Override
public void run() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
//连接服务器超时时间
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
//连接服务器(可写可不写)
conn.connect();
//获取流
InputStream is = conn.getInputStream();
//将流变成bitmap
Bitmap bitmap = BitmapFactory.decodeStream(is);
//存储到本地
save2Local(bitmap, url);
//存储到内存
mCaches.put(url, bitmap);
//在显示UI之前,拿到最新的url地址
String recentlyUrl = mTags.get(iv);
//把这个url和最新的url地址做一个比对,如果相同,就显示ui
if (url.equals(recentlyUrl)) {
//显示到UI,当前是子线程,需要使用Handler。其中post方法是执行在主线程的
mHandler.post(new Runnable() {
@Override
public void run() {
display(iv, url);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 存储到本地
*
* @param bitmap
* @param url
*/
public void save2Local(Bitmap bitmap, String url) throws FileNotFoundException {
File file = getCacheFile(url);
FileOutputStream fos = new FileOutputStream(file);
/**
* 用来压缩图片大小
* Bitmap.CompressFormat format 图像的压缩格式;
* int quality 图像压缩率,0-100。 0 压缩100%,100意味着不压缩;
* OutputStream stream 写入压缩数据的输出流;
* 返回值:如果成功地把压缩数据写入输出流,则返回true。
*/
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
}
/**
* 从本地获取图片
*
* @param url
* @return bitmap
*/
private Bitmap loadFromLocal(String url) {
//本地需要存储路径
File file = getCacheFile(url);
if (file.exists()) {
//本地有
//把文件解析成Bitmap
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
//存储到内存
mCaches.put(url, bitmap);
return bitmap;
}
return null;
}
/**
* 获取缓存文件路径(缓存目录)
*
* @return 缓存的文件
*/
private File getCacheFile(String url) {
//把url进行md5加密
String name = MD5Utils.encode(url);
//获取当前的状态,Environment是环境变量
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
//挂载状态,sd卡存在
File dir = new File(Environment.getExternalStorageDirectory(),
"/Android/data/" + mContext.getPackageName() + "/icon");
if (!dir.exists()) {
//文件不存在,就创建
dir.mkdirs();
}
//此处的url可能会很长,一般会使用md5加密
return new File(dir, name);
} else {
File dir = new File(mContext.getCacheDir(), "/icon");
if (!dir.exists()) {
//文件不存在,就创建
dir.mkdirs();
}
return new File(dir, name);
}
}
}
从上面我们可以看到如何从内存中获取bitmap然后去做判断,其实就是在LruCache
注意:在上面的工具类中有两个很重要的地方:
1.线程池的管理:
我们不可能有多少个图片就开多少个线程去进行请求,这样因为线程实例化大量对象也很容易oom,因此采用线程池管理线程。使用new FixedThreadPool()方法创建一个固定大小的线程池,表示固定只能同时运行设定值的数量的线程,其中某一个执行完成后再去执行下一个请求。
//实例化线程池
ExecutorService mPool=Executors.newFixedThreadPool(3);
//用线程池去管理请求
mPool.execute(new LoadImageTask(iv, url));
2.图片和url错位的问题:
工具类中有一个地方是显示UI之前,拿到最新的url地址recentlyUrl和url进行比对,一致的情况下才将图片设置到当前的控件。其实这里最主要涉及到像Listview和Recyclerview控件的复用原理,Listview和Recyclerview之所以能滑动无限的数量最主要原因就是在于他们巧妙的复用性。
比如在recyclerview中大量的图片加载到图片控件上,ImageView控件的个数其实就比一屏能显示的图片数量稍微多一点而已,移出屏幕的ImageView控件会进入到RecycleBin当中,而新进入屏幕的元素则会从RecycleBin中获取ImageView控件。当一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而网络请求是耗时操作,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了图片乱序的情况。
四.列表加载的优化
1.类似ListView尤其本身的优化,contentView的复用,开启异步任务加载图片,设置tag防止图片错位,加载大量数据时候分页加载。
2.我们可以控制异步任务的执行频率。比如用户滑动我们就要加载很多url的图片,大量的异步任务也会导致线程池的拥堵并同时如果回调成功会存在大量的UI更新导致卡顿,因此我们可以在用户滑动的时候停止加载图片,而在用户滑动停止的时候进行图片的加载,这样给用户的体验就会提升。
3.本篇文章主要让我们了解图片加载原理机制,在日常开发中我们可以用像picasso,glide等图片加载框架都已经解决了上面的这些问题,用起来就是几句代码搞定,爽歪歪。后期再出文章讲解这些图片加载框架原理和用法。
五.总结
以上就是关于Android图片加载机制及其优化的相关知识点,如有不足或者错误的地方请在下方指正。六一儿童节快乐!!!!哇哈哈!!!!