Android 图片加载框架全解析

前言:

首先说明一下,这篇文章的代码是用Hongyang大神的代码来进行讲解的,需要源码的同学可以在Android 框架练成 教你打造高效的图片加载框架文章最下方下载的源码进行阅读,第一次引用别人的代码不知道合不合适,如果有侵权问题我会马上修改或删除

因为之前看到网上不少关于图片加载框架的文章,读着读着你会发现原来图片加载框架需要涉及到很多的知识点,缺少其中一点都会瞬间懵逼,要先补充不懂的知识点,再回来发现不懂得又再补充看很费力.文章面向的也是知识面比较广的人看,有不少地方一笔带过没有细说,感觉很难一篇文章能看懂.看得半懂不懂的最后其实什么都不懂.

这会是一篇很长的文章,几乎每一个知识点都有讲到.如果你还没有耐心观看的打算.建议你可以收藏文章,等你静下心来了再阅读 PS:最好配合源码一起阅读.
如果发现了文章有错误的地方希望能指出,好让我修改避免误导他人.


首先说下图片加载框架封装好后调用的步骤.

  1. 创建一个单例的图片加载类,这个加载类提供** 一个 **getInstance方法获取加载类的实例
    (Listview那么多item,每个time都需要显示图片如果每个item都新建一个实例太浪费资源了,共用一个就好了)
  2. 获取到实例后调用LoadImage加载图片.

假设现在在自定义Adpater的无参构造方法中获取图片加载框架的实例:

public  class MyAdpater extends BaseAdapter implements{
    private ImageLoader ImageLoader;
     .......
    //在Listview的适配器的构造方法中获取ImageLoader的实例.
    public MyAdpater(){
    ImageLoader = ImageLoader.getInstance(3, Type.LIFO);}
}

接着来看上面的代码在获取实例时,内部发生了什么.

class ImageLoader{

  private static ImageLoad mInstance;
  ........

  ImageLoader(int threadCount, Type type) {
        Init(threadCount, type);
    }

    //关键字: #懒汉式加载,双重检查模式.
    public static ImageLoader getInstance(int threadCount, Type type) {
        if (mInstance == null) {
            synchronized (ImageLoader.class) {
                if (mInstance == null) {
                    mInstance = new ImageLoader(threadCount, type); 
                }
            }
        }
        return mInstance;
    }

    //整个图片加载框架的核心方法
    private void Init(int threadCount, Type type) {
        initBackThread(); 
        int CacheMemory = (int) Runtime.getRuntime().maxMemory()/8;
        mLruCache = new LruCache(CacheMemory) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getHeight() * bitmap.getRowBytes();
            }
        };
        mThreadPool = Executors.newFixedThreadPool(threadCount);
        mTaskQueue = new LinkedList<>();
        mType = type;
        mSemaphoreThreadPool = new Semaphore(threadCount);
    }

    private void initBackThread() {
        mLooperThread = new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                //只在初次获取Imageloader实例时启动后台轮询
                mLooperThreadHandler = new Handler() {

                    @Override
                    public void handleMessage(Message msg) {

                        mThreadPool.execute(getTask());

                    }
                };
                mSemaphoreHandlerInit.release();
                Looper.loop();
            }
        });
        mLooperThread.start();
    }
}
  1. 先调用ImageLoader的静态方法getInstance(3,Type.LIFO) 接收的两个参数下面会说
  2. getInstance(3,Type.LIFO)方法就是获取单例的ImageLoader实例.可以看到我们一开始就覆写了ImageLoader的构造方法
  3. 视线转到ImageLoader的构造方法调用了Init(threadCount, type)方法
  4. 接着看Init(threadCount, type)方法内部发生了什么?

要知道发生了什么先看Init()方法的成员和方法都有什么,作用是什么.

成员变量:

  • CacheMemory
  • mLruCache
  • mThreadPool
  • mTaskQueue
  • mType
  • mSemaphoreThreadPool
    方法:
  • initBackThread

作用:

  • CacheMemory:获取的是当前进程(当前运行的应用)可用内存的1/8.
  • mLruCache:"近期最少使用缓存"
  • 可用看到LruCache(CacheMemory)是一个泛型类,看名字Cache可以知道他和缓存有关,是的这里我们用来缓存图片,LruCache
  • 接着看到需要传入的参数CacheMemory.这个参数用来做什么的呢?为缓存分配空间大小.也就是说我们分了当前应用可用内存的1/8的来缓存图片.
  • 那覆写的sizeOf有什么用?Sizeof方法的作用是当我们向缓存添加进一个数据后,当前缓存剩余空间就会根据覆写的sizeOf方法将当前加进来的数据以bitmap.getHeight() * bitmap.getRowBytes();进行计算算出缓存空间剩余空间的大小,便于统计当前使用了多少内存,如果已使用的大小超过CacheMemory就会进行清除动作
    来讲下计算方式: 假设现在LruCache分配到的内存是10MB,每张添加到缓存的图片都是1MB的大小.前面10张图片都缓存到mLruCache后要存入第11张图片怎么办?他内部会根据你调用get(url)统计你从缓存中取出的每张图片的次数,次数越多这个url对应的bitmap在缓存的优先级越高.10张图片优先级最低的那张会自动从缓存删掉,把第11张缓存进来.
    后面我们需要从缓存取图片需要传入对应key:
    Bitmap bitmap=mLruCache.get(图片url)
  • mThreadPool:"线程池"这里只讲newFixedThreadPool,另外几个可以自行Google.
  • 线程池的好处?线程的创建和结束都需要耗费一定的系统时间(特别是创建),不停创建和删除线程会浪费大量的时间。所以,在创建出一条线程并使其在执行完任务后不结束,而是使其进入休眠状态,在需要用时再唤醒,那么就可以节省一定的时间。
  • **newFixedThreadPool线程池的的特点? **newFixedThreadPool创建固定大小的线程池,每次提交一个任务就创建一条线程,直到线程达到线程池的最大大小,之后需要执行的线程要等待正在执行的线程其中一个执行完后才能让一个等待线程到池内开始执行,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
mThreadPool =new newFixedThreadPool(3)//创建3条固定线程
mThreadPool.execute(需要执行的runable)}//每次调用该方法就从池内分配一条线程执行传入的runable```
- LinkedList mTaskQueue:任务队列(它是一个List列表但是比ArrayList执行这种大量的添加删除操作速度更快,更合适.)每个出现在Listview可见的item都需要加载图片,我不断的滑动Listview,不断的有图片需要显示.我们的解决办法就是为每个item都新建一个runable任务,每新建一个任务就存入到mTaskQueue.等待合适的时候取出来把任务交给线程池来执行任务.
- mType:配合上面的mTaskQueue任务队列使用,很好理解:
 if (mType == Type.FILO) {
        return mTaskQueue.removeFirst();//最先存进来的任务先取出
    } else if (mType == Type.LIFO) {
        return mTaskQueue.removeLast();//最后存进来的任务最后取出
    }

Semaphore 信号量,配合线程池使用效果更佳.

方法:

  • mSemaphoreThreadPool.acquire(); //获取信号量
  • mSemaphoreThreadPool.release(); //释放信号量

作用:

  1. 信号量能跨线程来获取或释放信号量(当互斥锁用,是的跨线程的互斥锁)
    可以在A线程调用acquire()方法获取信号量,在B线程满足某个条件后再调用release()释放信号量
  • 控制线程池并行运行(同时执行)线程的数量.

习题: 建议之前没接触过的自己运行一下这个信号量的demo,把信号量改成3和把信号量去掉或者把输出语句的位置换一下再观察下结果

// 允许2个线程同时访问  
      
    final Semaphore semaphore = new Semaphore(2);  
        ExecutorService executorService = Executors.newCachedThreadPool();  //新建一个线程池
        for (int i = 0; i < 10; i++) {     //循环创建10个线程,并且循环在线程池中执行
            final int index = i;   
            executorService.execute(new Runnable() {  
                public void run() {  
                    try {  
                        semaphore.acquire();      //获得信号灯(在初始化semaphore的时候传入值是2,所以这里同时并发运行两条线程)
                        // 这里可能是业务代码  
                        System.out.println("线程:" + Thread.currentThread().getName() + "获得许可:" + index);  
                        TimeUnit.SECONDS.sleep(1);  
                        semaphore.release();  //释放信号灯,放其他的线程执行
                        System.out.println("允许TASK个数:" + semaphore.availablePermits());    
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            });  
        }  
        executorService.shutdown();  

最后还有个initBackThread方法没说

  • initBackThread();
    private void initBackThread() {
        mLooperThread = new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                //只在初次获取Imageloader实例时启动后台轮询
                mLooperThreadHandler = new Handler() {

                    @Override
                    public void handleMessage(Message msg) {

                        mThreadPool.execute(getTask());

                    }
                };
                mSemaphoreHandlerInit.release();
                Looper.loop();
            }
        });
        mLooperThread.start();
    }

Handler挺复杂的,这里只做快速的讲解.

  1. 什么是Handler?"每条线程都有一个Handler",他的作用是进行跨线程间的通信,可以发送也可以接收其他线程发来的消息.
  2. 上面的的代码就是在子线程中创建了一个mLooperThread线程的Handler,同时覆写了handleMessage方法.
  • 注意上面的代码Looper.prepare()和Looper.loop()方法刚好包围着Handler实例,为什么呢?
  • 上面已经讲了Handler的作用是实现跨线程通信,例如:线程B的Handler接收到了其他线程发来的信息,其实每次往线程B的Handler发送消息,在信息送达到线程B前都会存储到一个消息队列存储起来.
  • 为什么存储起来?假设线程A向B发送了东西,线程B的Handler还没处理完,线程C这时也发来了消息怎么办?队列的作用就在线程B的Handler从存储了消息的队列中取出等处理完一个消息再放其他的消息进来处理.
  • 那跟Looper.prepare()和Looper.loop()方法有什么关系?你把东西存到消息队列后,调用Looper.prepare()的作用是为当前线程创建消息队列. Looper.loop()就是开始不断的轮询(死循环)Handler的消息队列判断消息队列中是否有消息,如果有就取出来.
  • 那消息从哪里取出来?记得我们刚刚覆写的handlerMessage?
//存储的信息就在msg里面

     public void handleMessage(Message msg) {

       mThreadPool.execute(getTask()); 

     }

因为上面的代码不需要从Handler中取出msg,但是你需要确保你大概能明白整个Handler的执行的过程就够了.后面的代码会详细运用到msg.

接着再来看当需要加载图片时loadImage()方法的内部是怎么样运作的:

    public void loadImage(final String path, final ImageView imageView,
            final boolean isFromNet)
    {
        imageView.setTag(path);
        if (mUIHandler == null)
        {
            mUIHandler = new Handler()
            {
                public void handleMessage(Message msg)
                {
                    // 获取得到图片,为imageview回调设置图片
                    ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
                    Bitmap bm = holder.bitmap;
                    ImageView imageview = holder.imageView;
                    String path = holder.path;
                    // 将path与getTag存储路径进行比较
                    if (imageview.getTag().toString().equals(path))
                    {
                        imageview.setImageBitmap(bm);
                    }
                };
            };
        }

        // 根据path在缓存中获取bitmap
        Bitmap bm = getBitmapFromLruCache(path);

        if (bm != null)
        {
            refreashBitmap(path, imageView, bm);
        } else
        {
            addTask(buildTask(path, imageView, isFromNet));
        }

    }

- imageView.setTag(path)和imageview.getTag()是什么?setTag接收一个Object,我们可以往里面传入任性类型的参数.getTag又是什么?你往setTag传入什么getTag就能取出什么.举个栗子:

String a="我是a"
imageView.setTag(path)
String b= (String)imageView.getTag();
System.out.print(a);
System.out.print(b);
//输出结果当然是一样的都是输出:"我是a"
- loadImage是运行在主线程的,但是很奇怪为什么这里的Handler没有被Looper.prepare()和Looper.loop()方法包围?因为在主线程会自动调用这个两个方法.(想知道为什么好奇宝宝可以自己去查阅资料)
- 再来看这里UIHandler的handlerMessage()代码块可以看到这里调用了msg.obj,上面刚刚讲过了msg是其他线程发送过来的消息.但是这里为什么要强转成ImgBeanHolder ?我们把视线暂时转到还没讲到的代码部分:
private class ImgBeanHolder
{
    Bitmap bitmap;
    ImageView imageView;
    String path;
}
private void refreashBitmap(final String path, final ImageView imageView,
        Bitmap bm)
{
    Message message = Message.obtain();
    ImgBeanHolder holder = new ImgBeanHolder();
    holder.bitmap = bm;
    holder.path = path;
    holder.imageView = imageView;
    message.obj = holder;
    mUIHandler.sendMessage(message);
}

稍后我们就是这样往UIHandler发送消息的,上面有认真看教程的话应该就明白为什么在UIHandler中要强转了把.
UIHandler最下面的if()判断语句先暂时跳过,后面再讲.


现在来讲下整个加载框架的完整执行过程:

  1. 获取ImageLoader实例
  2. 调用LoadImage(imageview,path);//需要显示的图片的控件,图片的url
  3. 根据LoadImage传入的url,接着调用getBitmapFromLruCache(url);从缓存中查找是否有缓存过图片.(后面会详细讲)
  • ture:从缓存中获得图片的bitmap,接着调用上面刚刚提到的refreashBitmap()该方法向UIHandler发送消息,UIHandler取出后消息后刷新控件.
- false:调用buildTask()新建一个Runable任务去加载图片.这个方法相当重要,直接上手绘图:
Android 图片加载框架全解析_第1张图片
buildTask完整的过程

有图看起来整个逻辑就比较清晰了,就是多次的if else关系.

第一步根据url从内存查找内存缓存是否有图片这一步请回看loadimage的代码块.这里只讲内存缓存没有找到图片返回false的代码.

private Runnable buildTask(final String path, final ImageView imageView,
            final boolean isFromNet)
    {
        return new Runnable()
        {
            @Override
            public void run()
            {
                Bitmap bm = null;
                if (isFromNet)//是否允许从网络加载,在调用loadImage方法时传入的第三个参数.
                {
                    File file = getDiskCacheDir(imageView.getContext(),md5(path));//根据图片url从手机存储空间获取图片的绝对路径
                    if (file.exists())// 如果在缓存文件中发现
                    {
                        
                        //从本地获取图片的bitmap
                        bm = loadImageFromLocal(file.getAbsolutePath(),imageView);
                    } else
                    {
                        if (isDiskCacheEnable)// 检测是否开启硬盘缓存
                        {
                            //从网络下载图片并且保存到手机本地,如果下载成功返回ture
                            boolean downloadState = DownloadImgUtils
                                    .downloadImgByUrl(path, file);
                            if (downloadState)// 如果下载成功
                            {
                                //从本地获取下载成功并且保存到本地的图片bitmap
                                bm = loadImageFromLocal(file.getAbsolutePath(),
                                        imageView);
                            }
                        } else
                        
                        {
                            // 不允许本地缓存,直接从网络获取图片
                            bm = DownloadImgUtils.downloadImgByUrl(path,
                                    imageView);
                        }
                    }
                } else
                {
                    //不允许从网络加载图片,直接从手机本地缓存加载图片
                    bm = loadImageFromLocal(path, imageView);
                }
                // 3、把图片加入到缓存
                addBitmapToLruCache(path, bm);
                //通知UIHandler刷新控件    
                refreashBitmap(path, imageView, bm);
                //释放信号量,后面再讲.
                mSemaphoreThreadPool.release();
            }

            
        };
    }

可以对着上面的手绘图,重复多看几次BuilTask()方法中的代码,应该能更好的理解.
执行的逻辑都清楚了再来深究一下里面的代码是怎么样处理的.
buildTask()的代码从上到下讲一遍:

//根据图片url从手机存储空间获取图片的绝对路径
File file = getDiskCacheDir(imageView.getContext(),md5(path));```
getDiskCacheDir()方法的具体代码:
private File getDiskCacheDir(Context context, String path) {
    String CachePath;
    //如果挂载了sd卡,返回File  /Android/packge_name/cache/path
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
        CachePath = context.getExternalCacheDir().getPath();
    } else {
        //   否则返回  /data/data/packge_name/cache/path
        CachePath = context.getCacheDir().getPath();
    }
    //  File.separator=='/' 
    return new File(CachePath + File.separator + path);
}

loadImageFromLocal()方法的具体代码:

      getImageViewSize
    private Bitmap loadImageFromLocal(String path, ImageView imageView) {
        Bitmap bitmap;
        ImageSize imageSize = ImageSizeUtil.getImageViewSize(imageView);
        bitmap = decodeSampledBitmapFromPath(path, imageSize.width, imageSize.height);

        return bitmap;
    }

可以看到该方法调用了ImageSizeUtil类的静态方法 getImageViewSize(imageView);
来看看getImageViewSize的具体代码:

     public static ImageSize getImageViewSize(ImageView imageView) {
        ImageSize imageSize = new ImageSize();
        DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();
        ViewGroup.LayoutParams lp = imageView.getLayoutParams();

        // 通过各种办法获取到imageview的高和宽
        int width = imageView.getWidth();// 获取imageview的实际宽度
        if (width <= 0) {
            width = lp.width;
        }
        if (width <= 0) {
            width = imageView.getMaxWidth();
        }
        if (width <= 0) {
            width = displayMetrics.widthPixels;
        }
        int height = imageView.getHeight();// 获取imageview的实际高度
        if (height <= 0) {
            height = lp.height;// 获取imageview在layout中声明的宽度
        }
        if (height <= 0) {
            height = imageView.getMaxHeight();
        }
        if (height <= 0) {
            height = displayMetrics.heightPixels;
        }
        imageSize.width = width;
        imageSize.height = height;

        return imageSize;
    }


 public static class ImageSize {

        int width;
        int height;
    }



注意区分
ViewGroup.LayoutParams lp = imageView.getLayoutParams();
int width=lp.width;
int width=imageView.getWidth();
注意区分:两个width获取的含义. 前者是获取imageview控件的宽度,后者是获取imageview中bitmap的宽度.

  1. 把imageview中的bitmap宽度赋值给width
  • 如果没有获取bitmap的宽度说明width<=0,imageview控件的宽度赋值给width
  • 如果控件的宽度还是<=0,把imageview的maxwidth赋值给width.
  • 如果还是没有,那就用干脆直接获取屏幕的宽度赋值给width
  • 最后把宽和高封装成ImageSize返回.

    ImageSize imageSize = ImageSizeUtil.getImageViewSize(imageView);
    bitmap = decodeSampledBitmapFromPath(path, imageSize.width, imageSize.height);

接着调用了decodeSampledBitmapFromPath():

      protected Bitmap decodeSampledBitmapFromPath(String path, int width,
            int height)
    {
        // 获得图片的宽和高,并不把图片加载到内存中.
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);

        options.inSampleSize = ImageSizeUtil.caculateInSampleSize(options,
                width, height);

        // 使用获得到的InSampleSize再次解析图片
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(path, options);
        return bitmap;
    }

接着看ImageSizeUtil.caculateInSampleSize(options,width, height);

    public int caculateInSampleSize(BitmapFactory.Options options, int Viewwidth, int Viewheight) {
        int bmWidth = options.outWidth;
        int bmHeight = options.outHeight;
        int inSampleSize = 1;
    //把stream中获取到图片的大小和
        if (bmWidth > Viewwidth || bmHeight > Viewheight) {
            int widthRadio = Math.round((float) bmWidth / (float) Viewwidth);
            int heightRadio = Math.round((float) bmHeight / (float) Viewheight);
            inSampleSize = Math.max(widthRadio, heightRadio);
        }

        return inSampleSize;
    }

思路是:先获取到最适合image的大小,然后decodeFile中获取到的bitmap的大小进行比较,如果image比bitmap要大.把bitmap获取到图片之前压缩成最适合image的大小再获取,为什么这么做?因为我们在ImageSizeUtil.getImageViewSize中获取到了图片宽高的最佳值然后对获取bitmap时进行压缩好处就是能节省手机内存,图片加载速度更快.


    boolean downloadState = DownloadImgUtils.downloadImgByUrl(path, file);
    Bitmap bitmap = DownloadImgUtils.donwloadImagByUrl(path, imageView);

注意有两个downloadImgByUrl方法,上面的比较简单不讲了,讲下面获取bitmap的方法,先看代码:

    public static Bitmap downloadImgByUrl(String urlStr, ImageView imageview)
    {
        FileOutputStream fos = null;
        InputStream is = null;
        try
        {
            URL url = new URL(urlStr);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(conn.getInputStream());
            is.mark(is.available());
        
            Options opts = new Options();
            opts.inJustDecodeBounds = true;
            Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts);
            
            //获取imageview想要显示的宽和高
            ImageSize imageViewSize = ImageSizeUtil.getImageViewSize(imageview);
            opts.inSampleSize = ImageSizeUtil.caculateInSampleSize(opts,
                    imageViewSize.width, imageViewSize.height);
            
            opts.inJustDecodeBounds = false;
            is.reset();
            bitmap = BitmapFactory.decodeStream(is, null, opts);

            conn.disconnect();
            return bitmap;

        } catch (Exception e)
        {
            e.printStackTrace();
        } finally
        {
            try
            {
                if (is != null)
                    is.close();
            } catch (IOException e)
            {
            }

            try
            {
                if (fos != null)
                    fos.close();
            } catch (IOException e)
            {
            }
        }

        return null;

    }

重点讲下is.mark(),从url中获取一次InputStream,后,如果再次获取InputStream会报错.,这里 is.mark(is.available())的作用就是在当前流的位置做一个标记,mark方法有个参数,通过这个整型参数,你告诉系统,希望在读出这么多个字符之前,这个mark保持有效.而reset()就是恢复到标记的位置.


大家可能留意到了在图片从本地保存和获取时候传入的url都传入了一个md5()的方法中
那什么是md5?

从百科复制过来的:
MD5即Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一(又译摘要算法、哈希算法),主流编程语言普遍已有MD5实现。将数据(如汉字)运算为另一固定长度值,是杂凑算法的基础原理,MD5的前身有MD2、MD3和MD4。
MD5算法具有以下特点:
1、压缩性:任意长度的数据,算出的MD5值长度都是固定的。
2、容易计算:从原数据计算出MD5值很容易。
3、抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
4、强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。

md5还有一个好处:相信很早期用Android机的同学都遇到这样一个问题,系统莫名其妙多了很多广告图片,或者头像图片,删除了过不久又出现,原因就是一些应用直接把图片的名字缓存到了手机的存储空间例如:New_York_Buildings_Skyscrapers_2880x1800.jpg 安卓系统的相册会自动扫描用户的存储空间把.jpg .png的图片全部显示到相册,md5算法能把图片的名字进行算法,变成没有英文+数字的"乱码"例如:38b8c2c1093dd0fec383a9d9ac940515 这时候.jpg和.png的后缀就不见了,系统也扫描不了,当用户需要获取图片的时候再进行计算,把原来的名字解析出来,这就是md5

    public String md5(String str)
    {
        byte[] digest = null;
        try
        {
            MessageDigest md = MessageDigest.getInstance("md5");
            digest = md.digest(str.getBytes());
            return bytes2hex02(digest);

        } catch (NoSuchAlgorithmException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 方式二
     * 
     * @param bytes
     * @return
     */
    public String bytes2hex02(byte[] bytes)
    {
        StringBuilder sb = new StringBuilder();
        String tmp = null;
        for (byte b : bytes)
        {
            
            tmp = Integer.toHexString(0xFF & b);
            if (tmp.length() == 1)
            {
                tmp = "0" + tmp;
            }
            sb.append(tmp);
        }

        return sb.toString();

    }

再来说代码的事儿:

  1. MessageDigest md = MessageDigest.getInstance("md5");获取 消息摘要算法
  • digest = md.digest(str.getBytes());换取字符串转成字节数组的"密文".
  • 重点来了,md5算法返回的是byte数组,这是一组密文,我们要把密文转成String作为文件名,我们的做法是先把byte转成16进制的int,再把int转成String.
  • 视线移到bytes2hex02()方法.我们对传入的byte数组进行遍历,把每个byte和0xff进行与运算.为什么要进行与运算?
    在Android系统中 byte为8位占一个字节,int为32位占4个字节.
    Integer.toHexString()方法接收的是int类型的参数,8位的byte直接传进去的话会变成32为的int类型而负数因为需要补位所以会造成错误.举个栗子:
    假设(8位)byte=-1转换成int(32位):
8位的-1      二进制的原码: 10000001    要转成32位   需要进行补位:
8位的-1      二进制的反码: 11111110
8位的-1      二进制的补码: 11111111
负数前位前24位补1:  11111111 11111111 11111111 11111111
这时候再转成16进制变成了0xffffffff,但是-1的16进制是0xff才对这时候我们&0xff
0xff:    00000000 00000000 00000000 11111111
-1:      11111111 11111111 11111111 11111111     前24位清零最后变成了:
         00000000 00000000 00000000 11111111     这时候byte转int后的16进制就不会出错了

还有为什么要判断tmp.lenght==1

    if (tmp.length() == 1)
            {
                tmp = "0" + tmp;
            }

因为Integer.toHexString返回的是无符号的整形,所以我们在tmp长度为1的数前面补一个0.

最开始提到的判断语句

    public void loadImage(final String path, final ImageView imageView,final boolean isFromNet)
    {
        imageView.setTag(path);      //把需要显示的图片和控件进行绑定
        if (mUIHandler == null)
        {
            mUIHandler = new Handler()
            {
                public void handleMessage(Message msg)
                {
                   
                    ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
                    Bitmap bm = holder.bitmap;
                    ImageView imageview = holder.imageView;
                    String path = holder.path;
                   
                    if (imageview.getTag().toString().equals(path))     //将path与getTag绑定的path进行比较
                    {
                        imageview.setImageBitmap(bm);
                    }
                };
            };
        }
        Bitmap bm = getBitmapFromLruCache(path);

        if (bm != null)
        {
            refreashBitmap(path, imageView, bm);  
        } else
        {
            addTask(buildTask(path, imageView, isFromNet));
        }
    }

这里我们主要看 setTag和getTag,为什么LoadImage一开始要对传进来的Imageview控件和path进行绑定?在后面HandlerMessage处理消息的时候把getTag和path进行比较?
提到Listview和ViewHolder还有convertView相信不少人都有印象了.这是Listview控件的重用机制.下面是一张很经典的图.使用了Listview控件的重用机制后,当item1完全滑出屏幕看不见后,item8这时出现在屏幕上.实际上item8并没有新建一个新的item对控件进行重新的绑定,而是把item1直接进行重用.重点来了item1和item8中的控件其实是同一个,是同一个,同一个.
现在假设item1和item8调用了loadImage(final String path, final ImageView imageView)item1传入了path:123456,控件:A. item8传入了path:456789,控件A.
item1中调用了loadImage()方法中控件A.setTag(123456)进行了绑定,然后加入了消息队列等待handlermessage进行处理然.还没等handlermessage 进行处理.item1已经移出了屏幕,item8进来了,重用了item1的控件.调用了loadImage()方法中控件A.setTag(456789)进行了绑定(和item1传入的控件是同一个)
还没等item1的任务处理,item8的任务先执行完了.然后item1接着执行,因为和item8是同一个控件,在之前item8调用loadImage()方法时把A.setTag(456789)
这时候如果不进行if语句的判断.程序会以为item8的控件应该显示item1的path.
加了判断语句的话.A.getTag()得到的path是456789 很明显和传进来的path123456不是同一个.所以不进行imageview.setImageBitmap()就不会出现错位显示了

Android 图片加载框架全解析_第2张图片

  • 写了很长很啰嗦的一篇文章,能讲的差不多都讲了.建议能一边看着源码一边看着这篇文章来同步阅读.
  • 还有什么不懂的地方在下面留言我会尽量回答.
  • 如果文章有错误的地方希望能指出,避免误导他人.
  • 如果这篇文章对你有所帮助,不妨点个赞,让我有更多分享文章的动力哈.



如果对Android反射机制有兴趣的同学,可以去看看我的另一篇文章:Android反射机制

你可能感兴趣的:(Android 图片加载框架全解析)