总结一下微信的本地图片加载有以下几个特点,也是提高用户体验的关键点
1、缩略图挨个加载,一个一个加载完毕,直到屏幕所有缩略图都加载完成
2、不等当前屏的所有缩略图加载完,迅速向下滑,滑动停止时立即加载停止页面的图片
3、已经加载成功的缩略图,不管滑出去多远,滑回来的时候不需要重新加载
4、在相册以外的环境中,需要让imageView的宽高比例随图片的宽高比例自动伸缩,而且要在图片加载完毕之前就要预留占位空间
为了满足上面几个要求,主要采用以下几个方法:
0、为了防止图片加载出来OOM,需要对分辨率和颜色的位数进行缩小到合适范围,同时采用LRU缓存
1、采用一个定长线程池,线程池的大小等于CPU的数量+1,把所有缩略图加载任务都交给线程池执行,以获得最快的加载效率。
2、在用户快速滑动的时候,没有加载完毕的划走了的图片立即停止加载,将所占线程让出来,让新的加载任务执行。
3、已经加载成功的缩略图,保存到sd卡中,下次再滑动回来的时候,直接从sd卡加载以前保存好的小图,不经过线程池。
4、对于三星这样的手机,其图片全都是宽度大于高度,方向用exif进行记录,图片加载器要读出exif的方向信息,然后通过矩阵进行旋转
效果展示:
github项目地址:https://github.com/AlexZhuo/AlxImageLoader
下面就是加载器主要部分的注释和代码
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.demo.task.AlxMultiTask;
/**
* Created by Administrator on 2016/4/8.
*/
public class AlxImageLoader {
private Context mcontext;
private HashMap> imageCache = new HashMap>();
private ConcurrentHashMap currentUrls = new ConcurrentHashMap<>();//记录一个imageView应该显示哪个Url,用于中断子线程
private Bitmap defbitmap;
public AlxImageLoader(Context context) {
this.mcontext = context;
defbitmap = BitmapFactory.decodeResource(mcontext.getResources(), R.drawable.upload_photo4x);//没加载到图片的默认显示
}
/**
* 从本地加载一张图片并使用imageView进行显示,可以设置是否根据图片的大小动态修改imageView的高度,宽度必须传入来控制显示图片的清晰度防止oom
* @param uri
* @param imageView
* @param imageViewWidth
* @param resizeImageView
* @param autoRotate
* @param imageCallback
* @return
*/
private Bitmap loadBitmapFromSD(final String uri, final ImageView imageView, final int imageViewWidth, final boolean resizeImageView, final boolean autoRotate,final boolean storeThumbnail ,final ImageCallback imageCallback) {
if (imageCache.containsKey(uri)) {//如果之前已经加载过这个图片,那么就从LRU缓存里加载
SoftReference SoftReference = imageCache.get(uri);
Bitmap bitmap = SoftReference.get();
if (bitmap != null) {
Log.i("Alex","现在是从LRU中拿出来的bitmap");
return bitmap;//从系统内存里直接拿出来
}
}
final int[] imageSize = {0,0};
if(uri ==null)return null;
if(storeThumbnail) {
File file = new File(imageView.getContext().getCacheDir().getAbsolutePath().concat("/" + new File(uri).getName()));
if (file.exists() && file.length()>1000) {
//因为从file中获取图片的宽高存在IO操作,所以把每个图片的宽高缓存起来
Log.i("Alex", "现在是从cache目录中拿出来的缩略图");
return BitmapFactory.decodeFile(file.getAbsolutePath());
}
}
//如果没有缓存options,那么就先获取options
new AlxMultiTask(){
@Override
protected BitmapFactory.Options doInBackground(Void... params) {//这一块主要是用来拿宽高,确定要加载图片的大小的
//线程开启之后,由于滚动太快,已经过了一段时间,可能imageView要显示的图片已经换了,就没有必要执行下面的东西了
String targetUrl = currentUrls.get(imageView);//滑动的非常快的时候会在此处中断
if(!uri.equals(targetUrl)) {
Log.i("Alex","这个图片已经过时了0");
return null;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 不读取像素数组到内存中,仅读取图片的信息,非常重要
BitmapFactory.decodeFile(uri, options);//读取文件信息,存放到Options对象中
String targetUrl1 = currentUrls.get(imageView);//滑动的非常快的时候会在此处中断
if(!uri.equals(targetUrl1)) {
Log.i("Alex","这个图片已经过时了1");
return null;
}
// 从Options中获取图片的分辨率
imageSize[0] = options.outWidth;
imageSize[1] = options.outHeight;
Log.i("Alex","原图的分辨率是"+imageSize[0]+" "+imageSize[1]);
Log.i("Alex","目标宽度是"+imageViewWidth);
if(imageSize[0]<1)return null;
int destWidth = imageViewWidth;
if (imageViewWidth > 200) destWidth /= 2;//如果imageView太大的话,不需要加载那么大的图片,就缩小一下
float compressRatio = imageSize[0] / (float) destWidth;//使用图片源宽度除以目标imageView的宽度计算出一个压缩比
int compressRatioInt = Math.round(compressRatio);//四舍五入
if (compressRatioInt % 2 != 0 && compressRatioInt != 1)
compressRatioInt++;//如果是奇数的话,就给弄成偶数
Log.i("Alex", "长宽压缩比是" + compressRatio + " 偶数化后" + compressRatioInt);
options.inSampleSize = compressRatioInt;
options.inPurgeable = true;
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565;
return options;
}
@Override
protected void onPostExecute(final BitmapFactory.Options options) {
super.onPostExecute(options);
//在线程终止回调的时候会产生巨大的延迟
String targetUrl = currentUrls.get(imageView);
if(!uri.equals(targetUrl)) {
Log.i("Alex","这个图片已经过时了haha");
return;
}
if (options == null) return;
asynGetBitmap(options,uri,imageView,imageViewWidth,resizeImageView,autoRotate,storeThumbnail,imageCallback);
}
}.executeDependSDK();
return defbitmap;//在子线程执行结束之前先用默认bitmap顶着
}
/**
* 一个在子线程里从文件获取bitmap,并存到LRU缓存的方法
* @param catchedOptions
* @param uri
* @param imageView
* @param autoRotate
* @param imageCallback
*/
private void asynGetBitmap(final BitmapFactory.Options catchedOptions, final String uri, final ImageView imageView, final int imageViewWidth, final boolean resizeImageView, final boolean autoRotate, final boolean storeThumbnail, final ImageCallback imageCallback){
//如果不需要重置imageView的大小,那么底下这部分先不执行
if (resizeImageView && imageViewWidth > 0) {//如果给出了imageView的宽度,就修改imageView的宽高以自适应图片的宽高
int imageViewHeight;
int degree = readPictureDegree(uri);
if (autoRotate && (degree == 90 || degree == 270)) {//如果原来是竖着的,且需要自动摆正那么宽和高要互换
imageViewHeight = catchedOptions.outWidth * imageViewWidth / catchedOptions.outHeight;
} else {
imageViewHeight = catchedOptions.outHeight * imageViewWidth / catchedOptions.outWidth;
}
JLogUtils.i("Alex", "准备重设高度" + imageViewHeight);
ViewGroup.LayoutParams params = imageView.getLayoutParams();
if (params != null) {//如果是旋转90度的图片,那么宽和高应该互换
params.height = imageViewHeight;
imageView.setLayoutParams(params);
}
}
new AlxMultiTask() {
@Override
protected Bitmap doInBackground(Void... params) {
String targetUrl = currentUrls.get(imageView);
if(!uri.equals(targetUrl)) {
Log.i("Alex","这个图片已经过时了2");//滑动的比较快的时候会在此处中断
return null;
}
Bitmap bitmap = null;
//首先获取完整的bitmap存到内存里,此处有可能oom
bitmap = getBitmapFromFile(uri, catchedOptions);
// String targetUrl3 = currentUrls.get(imageView);
// if(!uri.equals(targetUrl3)) {
// Log.i("Alex","这个图片已经过时了3");//在此处经常会中断
// return null;
// }
//获取完bitmap之后,因为已经过了一段时间,可能imageView要显示的图片已经换了,就没有必要执行下面的东西了
if (autoRotate) {//如果需要自动旋转
int degree = readPictureDegree(uri);
//获取完角度之后,因为已经过了一段时间,可能imageView要显示的图片已经换了,就没有必要执行下面的东西了
if (degree != 0) bitmap = rotateBitmap(bitmap, degree, true);
}
if (bitmap == null) bitmap = BitmapFactory.decodeResource(mcontext.getResources(), R.drawable.upload_photo4x);//如果出现异常,就用默认的bitmap
return bitmap;
}
@Override
protected void onPostExecute(final Bitmap bitmap) {
super.onPostExecute(bitmap);
String targetUrl = currentUrls.get(imageView);
if(!uri.equals(targetUrl)) {
Log.i("Alex","这个图片已经过时了5");//在此处经常会中断
return;
}
if (bitmap == null) return;
imageCallback.imageLoaded(bitmap, imageView, uri);
//显示完图片之后将缩略图缓存到本地
final Context context = imageView.getContext();
if(!storeThumbnail)return;
new AlxMultiTask(){
@Override
protected Void doInBackground(Void... params) {
imageCache.put(uri, new SoftReference(bitmap));//将bitmap存到LRU缓存里
storeThumbnail(context,new File(uri).getName(),bitmap);
return null;
}
}.executeDependSDK();
}
}.executeDependSDK();
}
private interface ImageCallback {
void imageLoaded(Bitmap imageBitmap, ImageView imageView, String uri);
}
/**
* 从本地根据相应的options获取完整的bitmap存到内存里,有可能会出现oom异常
* @param uri
* @param options
* @return
*/
private static Bitmap getBitmapFromFile(String uri, BitmapFactory.Options options) {
if(uri==null || uri.length()<4 || options==null)return null;
try{
if(!new File(uri).isFile())return null;//如果文件不存在
Bitmap bitmap = BitmapFactory.decodeFile(uri, options);// 这里还是会出现oom??
return bitmap;
}catch (Exception e){
Log.i("Alex","从图片中获取bitmap出现异常",e);
}catch (OutOfMemoryError e) {
Log.i("Alex","从文件中获取图片 OOM了",e);
}
return null;
}
/**
* 读取一个jpg文件的exif中的旋转信息
* @param path
* @return
*/
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
Log.i("Alex","获取图片旋转角度出现异常",e);
return degree;
}
Log.i("Alex","本张图片的旋转角度是"+path+" 角度是"+degree);
return degree;
}
/**
* 旋转一个bitmap,注意这个操作会销毁传入的bitmap,并且会占用源bitmap两倍的内存,所以要把一个已经压缩好的bitmap放进去,如果没有转换成功就返回原来的bitmap
* @param bitmap
* @param degrees
* @return
*/
public static Bitmap rotateBitmap(Bitmap bitmap, int degrees,boolean destroySource) {
if (degrees == 0) return bitmap;
Log.i("Alex","准备旋转bitmap,内存占用是"+AlxBitmapUtils.getSize(bitmap)+" 宽度是"+bitmap.getHeight()+" 高度是"+bitmap.getHeight()+" 角度是"+degrees);
try {
Matrix matrix = new Matrix();
matrix.setRotate(degrees, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
Bitmap bmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
if (null != bitmap && destroySource) bitmap.recycle();
return bmp;
} catch (Exception e) {
e.printStackTrace();
Log.i("Alex","旋转bitmap出现异常",e);
return bitmap;
} catch (OutOfMemoryError e) {
e.printStackTrace();
Log.i("Alex","旋转bitmap出现oom异常",e);
return bitmap;
}
}
/**
* 异步加载本地图片的暴露方法
* @param uri
* @param imageView
* @param imageViewWidth 如果想要imageView大小随图片文件自适应全显示的话,需要给一个imageView的目标宽度
*/
public void setAsyncBitmapFromSD(String uri, ImageView imageView,int imageViewWidth,boolean resizeImageView,boolean autoRotate,boolean storeThumbnail) {
//从LRU缓存里获取bitmap
if(uri!=null) currentUrls.put(imageView,uri);//把url绑定在imageView上,用来防止显示缓存错误
else currentUrls.put(imageView,"");
Bitmap cacheBitmap = loadBitmapFromSD(uri, imageView,imageViewWidth,resizeImageView,autoRotate,storeThumbnail,
new ImageCallback() {
public void imageLoaded(Bitmap imageBitmap, ImageView imageView, String imageUrl) {
Log.i("Alex","加载成功的bitmap宽高是"+imageBitmap.getWidth()+" x "+imageBitmap.getHeight());
imageView.setImageBitmap(imageBitmap);
}
});
if(cacheBitmap!=null) {
if(uri!=null)imageView.setImageBitmap(cacheBitmap);
Log.i("Alex","缓存的bitmap是"+cacheBitmap.getWidth()+" ::"+cacheBitmap.getHeight());
ViewGroup.LayoutParams params = imageView.getLayoutParams();
if(resizeImageView && params!=null && imageViewWidth>0 && cacheBitmap!=defbitmap) {//只有当现在缓存里的的bitmap不是默认bitmap的时候才重新修改大小,因为根据默认bitmap重设大小是没有意义的
int height = cacheBitmap.getHeight()* imageViewWidth / cacheBitmap.getWidth() ;
Log.i("Alex","准备重设高度haha"+height);
params.height = height;
imageView.setLayoutParams(params);
}
}else {
Log.i("Alex","缓存的bitmap为空");
}
}
/**
* 保存一个缩略图到sd卡,这样在selectPhoto的时候,第二次加载同一张图片就会变快
* @param bitmap
* @return
*/
public static boolean storeThumbnail(Context context, String fileName, Bitmap bitmap){
if(bitmap==null)return false;
File file = new File(context.getCacheDir().getAbsolutePath().concat("/"+fileName));
if(!file.exists()) try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
return false;
}
OutputStream out = null;
try {
out = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
return true;
} catch (FileNotFoundException e) {
e.printStackTrace();
return false;
}finally {
if(out!=null) try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
import android.os.AsyncTask;
import android.os.Build;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by Alex on 2016/4/19.
* 用于替换系统自带的AsynTask,使用自己的多线程池,执行一些比较复杂的工作,比如select photos,这里用的是缓存线程池,也可以用和cpu数相等的定长线程池以提高性能
*/
public abstract class AlxMultiTask extends AsyncTask {
private static ExecutorService photosThreadPool;//用于加载大图和评论的线程池
private final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private final int CORE_POOL_SIZE = CPU_COUNT + 1;
public void executeDependSDK(Params...params){
if(photosThreadPool==null)photosThreadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
if(Build.VERSION.SDK_INT<11) super.execute(params);
else super.executeOnExecutor(photosThreadPool,params);
}
}
调用方法
第一个参数是文件url路径,第一个是imageView对象,第二个是加载imageView的宽度,第三个参数是是否让imageView的大小随图片的宽高比例自动伸缩,第四个参数是是否让图片的方向随exif记录的角度旋转,最后一个参数是是否在sd卡保存加载成功缩略图
this.alxImageLoader = new AlxImageLoader(activity);
alxImageLoader.setAsyncBitmapFromSD(filePath,viewHolder.iv_photo,getScreenWidth()/3,false,true,true);