[安卓自定义类库]写一个自己的轻量级ImageLoader——LightImageLoader

来源:互联网 发布:异星工厂端口 编辑:程序博客网 时间:2024/05/16 07:39

说明

最近在学习Google traning的性能优化和图像与动画部分,这里面涉及了许多对于Bitmap,网络通信,缓存,多线程等方面的知识。感觉汇总起来写一个轻量级的ImageLoader刚好都能用上这些知识,于是就开了这么个github仓库开始这个项目。参考资料如下:

  • Google traning-图像和动画(中文版),英文版
  • Google traning-性能优化(中文版),英文版

实现要点

  • 以单例模式实现LightImageLoader以使得全局共享一个LightImageLoader的资源。
  • 通过ThreadPoolExecutor实现线程池以支持多个线程同时异步加载图片。
  • 通过HttpURLConnection实现网络连接和图片下载。
  • 通过给ImageView在异步图片加载任务提交前设置携带有任务信息的占位Drawable以支持并发场景下多个任务同时针对同一ImageView(如ListView有可能复用同一个ImageView,且对一个ImageView异步提交多个加载任务)时可以用于判断并取消不必要的任务。
  • 通过Handler和Message机制实现图片在非UI线程中加载完成后传递到UI线程中进行UI元素更新。
  • 通过LruCache实现内存缓存。
  • 通过DiskLruCache(非google官方库但是被官方承认)实现磁盘缓存。

单例模式实现LightImageLoader

  • 基于“懒汉式”实现单例模式,也即在第一次调用的时候才创建实例。此外,考虑到可能可能存在并发的初始化,需要在初始化时加锁。(其中两次判断==null的第一次判断是为了已经初始化之后减少加锁的开销,参加《Effective Java》71条)
  • 此外,注意需要让构造函数为私有以保证获取实例只能通过提供的静态方法获得。
  • 对外提供的公有方法都实现为实例方法,这样保证外部必须通过getInstance这个唯一的访问入口点来获取实例,继而调用实例方法,从而可以保证实例方法调用时单例已经创建完。
public class LightImageLoader {private LightImageLoader(){    //...}private static LightImageLoader mLightImageLoader;static public LightImageLoader getInstance(){        if(mLightImageLoader==null){            synchronized (LightImageLoader.class){                if(mLightImageLoader==null){                    mLightImageLoader=new LightImageLoader();                }            }        }        return  mLightImageLoader;    }    }public void loadImage(ImageView imageView,String uri){//wait for implement}

通过ThreadPoolExecutor实现线程池

  • ThreadPoolExecutor可以用于实现提供诸多灵活可控制项的线程池。
  • ThreadPoolExecutor的最大线程数一般应设置为设备CPU的核数。LightImageLoader中同时也让一开始就创建好和最大线程数一样数目的线程。此外,以1秒为一个线程空闲等待直至被杀掉的期限。
  • ThreadPoolExecutor的构造函数同时需要传入一个实现BlockingQueue接口的对象,可以使用LinkedBlockingQueue这个实现。具体的,项目中使用自定义的Runnable的实现类DownLoadImageRunnable。
  • 后面提交任务时,只要调用ThreadPoolExecutor对象的execute方法,传入DownLoadImageRunnable对象即可,当线程池存在空闲的线程时,ThreadPoolExecutor对象就会自动按FIFO顺序执行已提交的任务队列中的DownLoadImageRunnable对象。
public class LightImageLoader {    private ThreadPoolExecutor mThreadExecutor;    private LinkedBlockingQueue<Runnable> mTaskQueue;    private static int NUMBER_OF_CORES =            Runtime.getRuntime().availableProcessors();    // Sets the amount of time an idle thread waits before terminating    private static final int KEEP_ALIVE_TIME = 1;    // Sets the Time Unit to seconds    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;    private LightImageLoader(){        mTaskQueue=new LinkedBlockingQueue<>();        mThreadExecutor=new ThreadPoolExecutor(NUMBER_OF_CORES,                NUMBER_OF_CORES,KEEP_ALIVE_TIME,KEEP_ALIVE_TIME_UNIT,                mTaskQueue);        //...    }}

通过HttpURLConnection实现网络连接和图片下载和支持标记要取消的任务

  • 这部分同时也讲解实现了Runnable接口的DownLoadImageRunnable,因为网络连接就是在该类的run方法中发生的。
  • 在DownLoadImageRunnable对象中需要持有包含任务信息的ImageLoadTask对象的引用,该对象中会携带目标ImageView的弱引用,图片的uri等信息。注意这里应该使用弱引用以使得当当这个ImageView需要被垃圾回收时(比如用户在图片加载完成前退出了当前页面)不会因为存在一个ImageLoadTask持有它的强引用而无法被回收。参见Java:对象的强、软、弱和虚引用 。
  • 同时,ImageLoadTask支持设置和获取任务是否被取消的信息,注意其中的canceled域被标记为volatile,这保证在读取该域时读取的一定是某个线程修改后的结果,这对于读操作已经足够了,无需加锁(见《Effective Java》66条),但对写操作需要同步。
  • 此外,为了避免内部非静态成员类持有外部类的引用可能导致的内存泄漏问题,且由于ImageLoadTask和DownLoadImageRunnable都不需要直接访问LightImageLoader的实例方法,实现为静态成员类(见《Effective Java》22条)。
  • 在DownLoadImageRunnable的run方法中,先检查任务是否已被取消(见下一节的说明),再在这里判断disk缓存中是否有命中的缓存(memory缓存判断则无需异步做,会在前面还处于UI线程中时就进行),有则直接返回缓存。否则发起网络连接获取图片。注意设置GET这个method并通过BitmapFactory从得到的InputStream中解码出Bitmap。获取到图片后设置缓存,并通过Handler和Message机制将存储好Bitmap的ImageLoadTask对象转送回UI线程进行ImageView的更新。
  • 在DownLoadImageRunnable的run方法中直接使用LightImageLoader的静态实例mLightImageLoader是可行的,要到达这里的执行路径中必然已经初始化了该实例(单例实现使得外部调用必须经由getInstance方法)。直接通过域访问性能上会快一些。
static class ImageLoadTask{        WeakReference<ImageView> imageView;        Bitmap bitmap;        String uri;        volatile boolean canceled;        //when for read, volatile is enoght        // instead of a lock        public ImageLoadTask(ImageView imageView,String uri){            this.imageView=new WeakReference<>(imageView);            this.uri=uri;            canceled=false;        }        public void setBitmap(Bitmap bitmap){            this.bitmap=bitmap;        }        public synchronized void cancel(boolean canceled){            this.canceled=canceled;        }        boolean isCancel(){            return canceled;        }    }static class DownLoadImageRunnable implements Runnable{        private ImageLoadTask mImageLoadTask;        public DownLoadImageRunnable(ImageLoadTask imageLoadTask){            mImageLoadTask=imageLoadTask;        }        @Override        public void run(){            if(mImageLoadTask.isCancel()){                return;            }            Bitmap bitmap = mLightImageLoader.getBitmapFromDiskCache(mImageLoadTask.uri);            if(bitmap==null) {                try {                    URL url = new URL(mImageLoadTask.uri);                    HttpURLConnection connection = (HttpURLConnection) url                            .openConnection();                    connection.setRequestMethod("GET");                    InputStream inputStream = connection.getInputStream();                    bitmap = BitmapFactory.decodeStream(inputStream);                    inputStream.close();                } catch (Exception e) {                }            }            if(bitmap!=null){                mLightImageLoader.addBitmapToCache(mImageLoadTask.uri,bitmap);                mImageLoadTask.setBitmap(bitmap);                mLightImageLoader.deliverMessage(mImageLoadTask,                        LightImageLoader.TASK_COMPLETE);            }        }    }

通过占位Drawable支持并发场景下无效任务的取消

  • 这种需求主要针对并发情形下对ImageView存在复用的情况。比如ListView和GridView等控件的子Item视图会在用户滑动屏幕时被循环使用,如果每一个子视图都触发一个异步任务,那么就无法确保原先与该异步任务关联的视图在结束任务时是否已经被回收以供重用。而且,也无法确保所有的异步任务的完成顺序和他们本身的启动顺序保持一致,这可能会导致A任务后完成导致覆盖了A+1任务加载的图片,而实际上按照时序,最终应该显示A+1任务加载的图片。
  • 一种解决思路就是在一个任务提交前先使用一个持有任务信息引用的Drawable类(或其子类)对象作为目标ImageView的占位Drawable。当后续A+1任务提交时,检查这个Drawable中的任务信息,如果要加载的图片不一样(假设根据图片uri进行判断),则取消前面的任务;如果一样,则取消这一新的任务;如果Drawable是个普通的没有任务信息的Drawable,则正常提交当前任务。
 /**     * A drawable bound to the ImageView which containing the reference to the ImageLoadTask.     * In concurrent scenario, we need to make sure the target ImageView is     * still need the bitmap loaded by the task.     */    static class AsyncDrawable extends BitmapDrawable {        private final WeakReference<ImageLoadTask> imageLoadTaskReference;        public AsyncDrawable(ImageLoadTask bitmapWorkerTask){            super();            imageLoadTaskReference=new WeakReference<>(bitmapWorkerTask);        }        public AsyncDrawable(ImageLoadTask bitmapWorkerTask,Bitmap bitmap){            super(bitmap);            imageLoadTaskReference=new WeakReference<>(bitmapWorkerTask);        }        public ImageLoadTask getImageLoadTask() {            return imageLoadTaskReference.get();        }    }public void loadImage(ImageView imageView,String uri){if (cancelPotentialTask(imageView, uri)) {final ImageLoadTask imageLoadTask = new ImageLoadTask(imageView,                        uri);                AsyncDrawable asyncDrawable = new AsyncDrawable(imageLoadTask);                imageView.setImageDrawable(asyncDrawable);                final DownLoadImageRunnable downLoadImageRunnable = new DownLoadImageRunnable(imageLoadTask);                mThreadExecutor.execute(downLoadImageRunnable);    }}private static boolean cancelPotentialTask(ImageView imageView,String uri){        final ImageLoadTask imageLoadTask=getImageLoadTask(imageView);        if(imageLoadTask!=null){            final String oldUri=imageLoadTask.uri;            if(oldUri!=null&&oldUri.equals(uri)){                //The same with the uri newer, the new task shouldn't be                // executed.                return false;            }            else {                //Different to the uri newer, should be canceled.                imageLoadTask.cancel(true);            }        }        return true;    }    private static ImageLoadTask getImageLoadTask(ImageView imageView){        if(imageView!=null) {            Drawable drawable = imageView.getDrawable();            if (drawable instanceof AsyncDrawable) {                final AsyncDrawable asyncDrawable=(AsyncDrawable) drawable;                return asyncDrawable.getImageLoadTask();            }        }        return null;    }

通过Handler和Message机制转移到UI线程中进行UI元素更新

  • 在安卓中,只有UI线程(也即主线程)可以对UI元素进行更新。也即虽然我们在DownLoadImageRunnable的run方法中通过网络连接或是读取磁盘缓存已经拿到了Bitmap,且同时目标ImageView的弱引用也在DownLoadImageRunnable的mImageLoadTask对象里,但仍然不能在run方法中直接进行setImageBitmap操作,因为这里处于线程池中的非UI线程。
  • 处理这种将非UI线程中的数据转移到UI线程中进行操作的问题可以使用安卓提供的Handler和Message消息传递机制。
  • 首先在LightImageLoader构造函数中初始化一个依附在UI线程上的Handler,并覆盖实现其handleMessage(Message inputMessage)方法,在该方法中完成取出ImageView和Bitmap并进行setImageBitmap的操作。(这里为了未来可能的扩展增加了任务状态的switch判断机制)
  • 取消任务机制仍然在这里适用,结合前面run方法中的部分,也即本文所实现的取消或者在未发出网络请求或进行磁盘缓存确认前生效(这使得如果取消得早可以避免网络请求的消耗),或者在最后要更新UI前生效。具体何时生效取决于新任务并发环境下取消旧任务的发生时间点。在handleMessage中使用锁是为了防止可能出现旧任务覆盖新任务对ImageView的更新的问题。因为存在这种可能,在旧任务判断任务没有取消到实际更新ImageView之间,可能发生线程切换到新线程,如果没有锁,新线程将顺利设置canceled域且会以为它已经取消了旧任务,甚至顺利地走完整个流程把ImageView更新完了,然后此时再切换回旧任务的线程继续更新ImageView,这样就发生了覆盖。而如果存在锁,即使发生线程切换,由于锁的存在,新任务在设置canceld域时会阻塞(注意设置canceled域的cancel方法是在方法声明中加的sychronized关键字,这种方法要获得的锁就是针对该实例的同一把锁)直到handleMessage完成同步代码块内的步骤,这样就不会存在覆盖问题。
public class LightImageLoader {    private Handler mUiHandler;    private LightImageLoader(){        mUiHandler=new Handler(Looper.getMainLooper()){            @Override            public void handleMessage(Message inputMessage) {                ImageLoadTask task = (ImageLoadTask) inputMessage.obj;                switch (inputMessage.what){                    case TASK_COMPLETE:synchronized (task) {                            if (!task.isCancel()) {                                ImageView imageView = task.imageView.get();                                if (imageView != null) {                                    imageView.setImageBitmap(task.bitmap);                                }                            }                        }                        break;                }            }        };    }}
  • 随后我们来看怎么把数据作为Message发送给Handler。注意到前面run方法中最后调用了mLightImageLoader.deliverMessage方法。该方法实现如下:
private void deliverMessage(ImageLoadTask imageLoadTask,int state){        switch (state){            case TASK_COMPLETE:                Message message=mUiHandler.obtainMessage(state,imageLoadTask);                message.sendToTarget();                break;        }    }
  • 也即通过Handler的obtainMessage方法获取到Message后调用其sendToTarget方法即可。

通过LruCache实现内存缓存

  • 内存缓存主要是利用内存较快的读取速度以避免图片抖动,也即保证只要一个图片已经被加载过一次,那么再次显示它应该不会有闪烁的视觉感觉。同时,使用全局内存缓存还可以实现多个页面先后加载同一张图片时能够复用已经缓存的图片。
  • LruCache是安卓提供的一个LRU规则(保存最近引用的对象,并且在缓存超出设置大小的时候剔除最近最少使用到的对象。)的缓存实现,其内部是基于一个LinkedHashMap实现的。存储的是被缓存对象的强引用。Java中其实比较流行的缓存实现是通过弱引用(因为弱引用允许引用的对象在内存不足时被垃圾回收)。但这种做法在安卓中不是很适用,因为从Android 2.3 (API Level 9)开始,垃圾回收机制变得更加频繁,这使得释放软(弱)引用的频率也随之增高,会最终导致使用软(弱)引用的效率降低很多。
  • 因此,本文最终使用LruCache作为内存缓存,并使用1/8应用可用内存作为最大缓存大小。设计中Key使用存储图片Uri的String。
public class LightImageLoader {    private LruCache<String,Bitmap> mMemoryCache;    private LightImageLoader(){        mMemoryCache =new LruCache<String, Bitmap>(CACHE_SIZE){            @Override            protected int sizeOf(String key, Bitmap bitmap) {                // The cache size will be measured in kilobytes rather than                // number of items.                return bitmap.getByteCount() / 1024;            }        };}private void addBitmapToCache(String key, Bitmap bitmap) {    //The LruCache is thread safe therefore no need for synchronizing.        if (getBitmapFromMemCache(key) == null) {            mMemoryCache.put(key, bitmap);        }}private Bitmap getBitmapFromMemCache(String key) {        Bitmap bitmap=mMemoryCache.get(key);        if(bitmap!=null)            Log.e(TAG,"mem hit");        return bitmap;    }

通过DiskLruCache实现磁盘缓存。

  • 参考这篇文章,DiskLruCache是Google提供的(非Google官方编写,但获得官方认证)一个硬盘缓存类库。下载链接1,下载链接2。
  • 使用过程中需要注意的点主要是磁盘缓存需要在一开始调用DiskLruCache.open进行初始化,而这一步因为涉及磁盘操作应该通过异步任务来进行。而涉及到异步任务,就需要保证后续对磁盘缓存的读写需要等待这一步初始化的完成,因此需要加锁来进行同步。
  • 此外是磁盘缓存的key会最终成为缓存文件名的一部分,这意味着不能直接使用带有特殊符号的图片uri作为key。因此一种替代方式是计算uri的MD5摘要作为key(任意信息之间具有相同MD5码(128位)的可能性非常之低,通常被认为是不可能的)。
public class LightImageLoader {private DiskLruCache mDiskLruCache;private boolean mDiskCacheStarting = true;    private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB    private static final String DISK_CACHE_SUBDIR = "testDiskCache";    private LightImageLoader(){    File disCacheDir=new File(Environment.getExternalStorageDirectory(),DISK_CACHE_SUBDIR);        new AsyncTask<File,Void,Void>(){            @Override            protected Void doInBackground(File... params) {                synchronized (mDiskCacheLock) {                    File cacheDir = params[0];                    try {                        mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1,                                DISK_CACHE_SIZE);                        mDiskCacheStarting = false; // Finished initialization                    }                    catch (IOException e){                    }                    finally {                        mDiskCacheLock.notifyAll(); // Wake any waiting threads                    }                }                return null;            }        }.execute(disCacheDir);    }private void addBitmapToCache(String key, Bitmap bitmap) {        //The LruCache is thread safe therefore no need for synchronizing.        if (getBitmapFromMemCache(key) == null) {            mMemoryCache.put(key, bitmap);        }        // Also add to disk cache        synchronized (mDiskCacheLock) {            if (mDiskLruCache != null && getBitmapFromDiskCache(key) == null) {                String diskKey=hashKeyForDisk(key);                try {                    DiskLruCache.Editor editor = mDiskLruCache.edit(diskKey);                    if (editor != null) {                        OutputStream outputStream = editor.newOutputStream(0);                        ByteArrayOutputStream output = new ByteArrayOutputStream();                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, output);                        byte[] data=output.toByteArray();                        outputStream.write(data);                        outputStream.flush();                        editor.commit();                    }                    mDiskLruCache.flush();                } catch (IOException e) {                    e.printStackTrace();                }            }        }    }private Bitmap getBitmapFromDiskCache(String key){        synchronized (mDiskCacheLock) {            // Wait while disk cache is started from background thread            while (mDiskCacheStarting) {                try {                    mDiskCacheLock.wait();                } catch (InterruptedException e) {}            }            if (mDiskLruCache != null) {                try{                    String diskKey=hashKeyForDisk(key);                    DiskLruCache.Snapshot snapShot=mDiskLruCache.get(diskKey);                    if (snapShot != null) {                        InputStream is = snapShot.getInputStream(0);                        Bitmap bitmap = BitmapFactory.decodeStream(is);                        Log.e(TAG,"disk hit");                        return bitmap;                    }                }                catch (IOException e){                }            }        }        return null;    }static private String hashKeyForDisk(String key) {        String cacheKey;        try {            final MessageDigest mDigest = MessageDigest.getInstance("MD5");            mDigest.update(key.getBytes());            cacheKey = bytesToHexString(mDigest.digest());        } catch (NoSuchAlgorithmException e) {            cacheKey = String.valueOf(key.hashCode());        }        return cacheKey;    }    static private String bytesToHexString(byte[] bytes) {        StringBuilder sb = new StringBuilder();        for (int i = 0; i < bytes.length; i++) {            String hex = Integer.toHexString(0xFF & bytes[i]);            if (hex.length() == 1) {                sb.append('0');            }            sb.append(hex);        }        return sb.toString();    }  }

至此,我们完成了一步步编写一个轻量级ImageLoader的过程。希望本文所介绍的内容能对大家有益,也欢迎进一步的探讨和交流~本文的全部源码见github仓库。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 幼儿园小朋友很调皮怎么办 幼儿园小朋友上课调皮怎么办 孩子误冲游戏怎么办 遇到别的熊孩子怎么办 幼儿园遇到熊孩子怎么办 高铁上遇到熊孩子怎么办 幼儿爱打人家长怎么办 妈妈爱打孩子怎么办 35儿童爱打人怎么办? 一岁半宝宝太调皮怎么办 儿子高一不听话怎么办 小孩说了不听话怎么办 我的妈妈文盲怎么办 电脑键盘反拼音怎么办 小孩学习态度差怎么办 孩子不好好上学怎么办 小孩读书态度不好怎么办 幼儿园孩子不认识数字怎么办 一年级孩子拼音很差怎么办 孩子的拼音不好怎么办 小孩不会拼拼音怎么办 小孩拼音学不会怎么办 儿童l发音不准怎么办 小孩发音不标准怎么办 拼音l发音不准怎么办 孩子拼音声调分不清怎么办 小孩gk读成dt怎么办 拼音音调学不会怎么办 会拼音不会打字怎么办 大人拼音学不会怎么办 志愿服务经历少怎么办 医保报销发票丢失怎么办 费用发票丢失了怎么办 小孩乱拿东西怎么办 在家突然生了怎么办 二胎在家生的怎么办? 奶有一边没有怎么办 孩子应用题很弱怎么办 做不到不嫉妒怎么办 小孩自律太差怎么办 小学四年级数学差怎么办