在Android开发中,我们免不了需要去加载一些图片,这些图片可能是存在于本地,也可能是从网络所获取。然而app运行过程中所拥有的内存是有限的,图片又特别消耗内存,稍微不注意就可能造成OOM(Out of memory),而且在加载大量图片的时候,我们滑动界面时会发现特别不流畅,所以在加载过程中就要特别的注意。
基于这个原因网络上拥有许多图片加载的框架,Picasso、Glide、Fresco等。当我们在开发时使用这些框架的时候就可以很好的避免OOM,而且加载也十分的流畅,并且使用起来特别的简单,省去了很多的开发时间。比如说Glide框架
Glide.with(Context).load(URL).into(imageView);
上面这么简单的一句代码就能够加载网络或者本地的图片,更厉害的是,它也支持加载gif。大多时候对于我们来说一个工具会用就行了,但是有的时候我们也可以试着去了解一下它们的内部原理,如果大家想知道Glide的实现原理的话,http://blog.csdn.net/guolin_blog/article/details/53759439 我推荐大家可以去看看这系列的文章。
言归正传,我们如何去创建一个简易的图片加载器呢?
首先我们要确定一下这个图片加载器要解决的一些问题
1.支持异步加载,可以同时加载多张图片。
2.支持本地和内存缓存,防止不必要的多次加载,
3.支持图片压缩,一个小的Imageview没必要去加载一个超高清的图片。
4.使用简单,可以选择各种加载的方式。
那么基于上面的这些问题,我们就来试着创建一个简易的图片加载器。
首先确定这个图片加载器的使用方式,参照Glide的来做,
Klose.with(Context).add(Imageview).zip(true).fail(resource).size(h,w).load(URl)
上面就是这个加载框架的完整的加载方法
with(Context context):传递一个当前Activity的context,必须传递。
add(ImageView imageView):传递你需要加载图片的ImageView ,必须传递。
zip(boolean isZip):是否压缩图片,不必须,默认为true。
fail(int resource):加载失败时显示的图片,不必须。
size(int height,int width):加载图片的指定大小,传递之后,就按指定大小来压缩图片,不必须。
load(string url):网络图片的网络地址,使用在最后,必须传递。
现在我们就按这个结构来构建一个图片加载器。
首先我们创建一个Klose类,这是一个单列类,在里面初始化硬盘缓存和内存缓存。我们看源码来讲:
public class Klose
{
//全局单例Klose
private static Klose mKlose = null;
//用来获取全局的ApplicationContext()
Context mContext;
//用于硬盘缓存
private DiskLruCache diskLruCache;
//用于内存缓存
private LruCache mMemoryCache;
//线程池,用来多线程下载图片
ThreadPoolExecutor threadPoolExecutor;
private Klose(Context context)
{
//mContext赋值
mContext=context;
//初始化内存缓存
initCache();
//初始化硬盘缓存
initDiskCache();
//初始化线程池
threadPoolExecutor=new ThreadPoolExecutor(10, 10, 1, TimeUnit.SECONDS, new LinkedBlockingDeque(120));
}
//单例实现,初始化Klose,加锁防止多线程调用时,出错
public static synchronized Klose with(Context context)
{
if(mKlose == null){
synchronized (Klose.class){
if(mKlose==null){
context=context.getApplicationContext();
mKlose = new Klose(context);
}
}
}
return mKlose;
}
//添加ImageView,生成一个ImageLoader类,用于加载图片。
public ImageLoader add(ImageView imageView){
return new ImageLoader(mKlose,imageView);
}
/**
* 设置内存缓存大小
*/
private void initCache(){
//获取当前运行app的总内存
int maxMenroy=(int)Runtime.getRuntime().maxMemory();
//当前app运行内存的八分之一
int cachesize=maxMenroy/8;
//使用LruCache,开辟一个占app运行总内存八分之一大小的内存缓存,用于缓存图片。
mMemoryCache=new LruCache(cachesize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
/**
* 获取文件硬盘缓存路径
*/
private File getDiskCacheDir(String name){
String lj=null;
if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())||!Environment.isExternalStorageRemovable()){
lj=mContext.getExternalCacheDir().getPath();
}else {
lj=mContext.getCacheDir().getPath();
}
String filesd=lj+File.separator+name;
Log.e("file", filesd);
return new File(filesd);
}
/**
* 获取app版本号
*/
private int getAppVersion(Context context){
try {
PackageInfo info=mContext.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
/**
* 创建一个硬盘缓存,大小为6M
*/
private void initDiskCache(){
try {
//得到本地缓存的文件夹
File cacheDir=getDiskCacheDir("Klosebitmap");
if(!cacheDir.exists()){
cacheDir.mkdirs();
Log.e("dd","dd");
}
//创建或打开一个DiskLruCache,用于本地缓存,大小为6M。
diskLruCache=DiskLruCache.open(cacheDir,getAppVersion(mContext),1,6*1024*1024);
} catch (IOException e) {
e.printStackTrace();
}
}
public Context getContext()
{
return mContext;
}
public DiskLruCache getDiskLruCache()
{
return diskLruCache;
}
public LruCache getMemoryCache()
{
return mMemoryCache;
}
}
从上面的代码,我们首先通过with方法创建一个全局唯一的一个Klose对象,从上面可以看到,不管我们第一次传入的是什么context,我们都会将ApplicationContext这个全局的Context作为Klose对象里面的Context,这样可以减少不必要的内存泄漏。
在Klose的构造函数中,我们初始化了一个LruCache对象,LruCache是android提供的一个缓存工具类,它将最近使用的对象存储在缓存中,如果超出了设置的缓存大小,就会将最远使用的对象删除,它是以key-value形式的方法储存的,因为是在内存缓存中,所以它的存取速度特别快,但是能缓存的东西也比较少,一般设置它的大小为当前app运行内存的八分之一。
然后初始化一个DiskLruCache对象,DiskLruCache是一个本地缓存的工具类,它不是google官方开发的,但是google推荐使用它,所以sdk中没有这个类,你需要去githua上下载https://github.com/JakeWharton/DiskLruCache。在手机中,我们大多数的本地缓存都缓存在
/storage/emulated/0/Android/data/应用包名/cache/这个地址下面,所以我们通过获取到这个缓存地址,新建一个存缓存图片的文件夹,用于缓存图片,设置它的大小为6M。
最后初始化一个线程池,用来加载网络上的图片。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)
corePoolSize:核心线程数
maxPoolSize:最大线程数
keepAliveTime:线程最大空闲时间
unit:时间单位
workQueue :线程池所使用的缓冲队列
这里就简单说一下这几个参数的意思,想了解更多线程池的使用或原理,点击http://blog.csdn.net/siobhan/article/details/51282570
因为Klose对象是全局唯一的,所以下面所使用的Lrucache,DiskLrucache,ThreadPoolExecutor都是全局唯一的。
最后创建一个add方法,这个add中传入需要加载图片的ImageView,然后创建一个ImageLoader对象。
ImageLoader类:
public class ImageLoader
{
//是否压缩
private boolean mIsZip = true;
private Klose mKlose;
private ImageView mImageView;
//用来对文件进行32Key生成
private MD5Util mMD5Util;
//不指定压缩size的默认值
private int mHeight = -1;
private int mWidth = -1;
//加载失败图片
private int failLoadImage = R.drawable.error;
//加载失败图片
private int failLoadBitmap;
//加载中图片
private int LoadingImage = R.drawable.ing;
//加载中图片
private Bitmap LoadingBitmap;
//构造函数,获取唯一Klose对象,和需要加载图片的ImageView
public ImageLoader(Klose klose,ImageView imageView){
mKlose=klose;
mImageView=imageView;
//初始化mMD5Util对象,用于key生成。
mMD5Util=new MD5Util();
//正在加载图片初始化
LoadingBitmap = BitmapFactory.decodeResource(mKlose.getContext().getResources(),LoadingImage);
}
//fail方法,设置失败时的图片,返回当前ImageLoader对象
public ImageLoader fail(int resource){
failLoadImage = resource;
return this;
}
//size方法,设置指定加载的图片大小,返回当前ImageLoader对象
public ImageLoader size(int height,int width){
if (height<=0 || width <=0){
return this;
}
mHeight = height;
mWidth = width;
return this;
}
//zip方法,设置指定加载的图片是否压缩,返回当前ImageLoader对象
public ImageLoader zip(boolean isZip){
mIsZip = isZip;
return this;
}
//load方法,设置指定加载的图片,返回当前ImageLoader对象。
public ImageLoader load(String url){
//初始化正在加载
mImageView.setImageBitmap(LoadingBitmap);
//根据url得到唯一32位key
String key = mMD5Util.getMD5(url);
//根据key从内存缓存中取图片,如果有取出,如果没有,去本地缓存取
Bitmap bitmap=mKlose.getMemoryCache().get(key);
if(bitmap==null){
//初始化异步加载类
BitmapWorkerTask bitmapWorkerTask=new BitmapWorkerTask();
//传入线程池,将异步加载放入线程池中 bitmapWorkerTask.executeOnExecutor(mKlose.threadPoolExecutor,key,url);
}else {
mImageView.setImageBitmap(bitmap);
}
return this;
}
//异步线程,加载图片
class BitmapWorkerTask extends AsyncTask
{
String mUrl;
String mKey;
@Override
protected Bitmap doInBackground(String... params)
{
mKey = params[0];
mUrl = params[1];
FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
DiskLruCache.Snapshot snapshot = null;
try
{
//根据key判断本地缓存中有没有图片,有则取出放入内存缓存,没有则网络下载
snapshot = mKlose.getDiskLruCache().get(mKey);
if (snapshot == null)
{
//以写入方式打开缓存文件夹
DiskLruCache.Editor editor = mKlose.getDiskLruCache().edit(mKey);
if (editor != null)
{
OutputStream outputStream = editor.newOutputStream(0);
//去网络上下载图片
if (downloadUrlToStream(mUrl, outputStream))
{
Log.e("jjjjj", "kkk");
editor.commit();
}
else
{
editor.abort();
return getBitmap(failLoadImage);
}
}
//再去本地缓存中取
snapshot = mKlose.getDiskLruCache().get(mKey);
}
//不为空则取出图片,显示,并加入内存缓存
if (snapshot != null)
{
//读图
Log.e("jjjjj", "kkk1");
fileInputStream = (FileInputStream) snapshot.getInputStream(0);
fileDescriptor = fileInputStream.getFD();
}
Bitmap bitmap = null;
if (fileDescriptor != null)
{
Log.e("jjjjj", "kkk2");
//判断是否压缩
if (mIsZip){
bitmap = getBitmap(fileDescriptor);
}else {
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
}
if (bitmap != null)
{
Log.e("jjjjj", "kkk3");
//放入内存缓存
mKlose.getMemoryCache().put(mKey, bitmap);
}
return bitmap;
}
catch (IOException e)
{
e.printStackTrace();
}
//如果有错,显示失败图片
return getBitmap(failLoadImage);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
//显示
mImageView.setImageBitmap(bitmap);
Log.e("jjjjj","kkk4");
}
}
//下载图片
private boolean downloadUrlToStream(String url, OutputStream outputStream) {
//创建连接
URLConnection urlcon=null;
//创建输入输出流
BufferedOutputStream out=null;
BufferedInputStream in=null;
if(url!=null){
Log.e("sssssss",url);
try {
URL url1=new URL(url);
urlcon=(URLConnection)url1.openConnection();
in =new BufferedInputStream(urlcon.getInputStream(),8*1024);
out=new BufferedOutputStream(outputStream,8*1024);
int b;
//写入文件
while((b=in.read())!=-1){
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(out!=null) {
out.close();
}
if(in!=null){
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
/**
*对图片进行简单的压缩
*/
private Bitmap getBitmap(FileDescriptor fileDescriptor){
//获取options
BitmapFactory.Options options = new BitmapFactory.Options();
//设置加载时只获取图片的基本信息,
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
int imaHeight = options.outHeight;
int imaWidth = options.outWidth;
int viewHeight;
int viewWidth;
//做判断,看是否自定义了大小,没有的话根据控件大小缩放
if(mHeight>0 && mWidth>0){
viewHeight = mHeight;
viewWidth = mWidth;
}else{
viewHeight = mImageView.getHeight();
viewWidth = mImageView.getWidth();
}
//判断缩小系数
int inSampleSize = 1;
if (imaHeight > viewHeight || imaWidth > viewWidth) {
// 计算出实际宽高和目标宽高的比率
final int heightRatio = Math.round((float) imaHeight / (float) viewHeight);
final int widthRatio = Math.round((float) imaWidth / (float) viewWidth);
// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
// 一定都会大于等于目标的宽和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
Log.e("blll",imaHeight+" "+viewHeight+ " "+imaWidth+" "+viewWidth+" "+inSampleSize);
//设置缩小系数
options.inSampleSize = inSampleSize;
//设置加载图片
options.inJustDecodeBounds = false;
//返回压缩后图片
return BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
}
/**
*对图片进行简单的压缩,重载函数
*/
private Bitmap getBitmap(int failImage){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(mKlose.getContext().getResources(),failImage,options);
int imaHeight = options.outHeight;
int imaWidth = options.outWidth;
int viewHeight;
int viewWidth;
if(mHeight>0 && mWidth>0){
viewHeight = mHeight;
viewWidth = mWidth;
}else{
viewHeight = mImageView.getHeight();
viewWidth = mImageView.getWidth();
}
int inSampleSize = 1;
if (imaHeight > viewHeight || imaWidth > viewWidth) {
// 计算出实际宽高和目标宽高的比率
final int heightRatio = Math.round((float) imaHeight / (float) viewHeight);
final int widthRatio = Math.round((float) imaWidth / (float) viewWidth);
// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
// 一定都会大于等于目标的宽和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
Log.e("blll",imaHeight+" "+viewHeight+ " "+imaWidth+" "+viewWidth+" "+inSampleSize);
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(mKlose.getContext().getResources(),failImage,options);
}
}
从上面的代码可以看到,在构造函数中初始化几个变量。我们发现这里面的fail()、size()、zip()、load()这几个方法返回值都是当前ImageLoader本身。这几个方法就是用来便于使用者设置加载方式的几个方法,当然你也可以添加更多的方法来增加加载图片的方式。
在load方法中我们传入了一个url,这是网络图片的地址,我们给需要加载图片的Imageview初始化一个正在加载的图片。然后使用MD5对url进行加密,得到唯一key,根据key去Lrucache中获取图片,如果有就加载。
如果Lrucache中没有图片,就声明一个异步加载对象,并把它放入事先初始化好的线程池中运行。
BitmapWorkerTask类中,我们重新了doInBackground()方法,通过key去本地缓存中获取DiskLruCache.Snapshot对象,如果存在通过文件输出流得到图片。根据是否压缩,是否指定了指定大小来得到最终的图片,显示出来。
如果不存在DiskLruCache.Snapshot对象,通过downloadUrlToStream这个方法去下载图片,这个方法中就是运用了http的方法从网络上下载图片,存储在本地缓存中,成功则返回true,失败返回false。然后将图片送入内存缓存,进行显示。
这就是整个图片加载的流程,里面使用了Lrucache和DiskLrucahce来做双缓存,避免了图片的重复加载,使加载数据更流畅。使用提前获得图片合理的大小,来避免OOM。
使用:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder=null;
if(convertView==null){
convertView= LayoutInflater.from(getApplicationContext()).inflate(R.layout.music_list_xianshi, null);
holder=new ViewHolder();
holder.Iv_musicicon= (ImageView) convertView.findViewById(R.id.image);
convertView.setTag(holder);
}else{
holder=(ViewHolder) convertView.getTag();
}
Klose.with(getApplicationContext()).add(holder.Iv_musicicon).load(mList.get(position));
return convertView;
}
使用体验,加载120张网络图片:
不压缩:加载不太流畅,加载到后面会报OOM。
压缩:加载流程,使用占位图,图片不会错位,不会报OOM。
如果你想使用我这个简易的图片加载框架的话,欢迎大家使用。使用方式
在project gradle添加
allprojects {
repositories {
google()
jcenter()
//添加这一行
maven { url 'https://jitpack.io' }
}
}
在app gradle添加
dependencies {
//添加这一行
compile 'com.github.Klose-w:Klose:1.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}
添加完毕后,在你的项目中就能够使用了