一个简单的图片加载器android

在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。

一个简单的图片加载器android_第1张图片

如果你想使用我这个简易的图片加载框架的话,欢迎大家使用。使用方式

在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'
}

添加完毕后,在你的项目中就能够使用了

你可能感兴趣的:(Android)