读书笔记之AfinalBitmap源码分析

前言

在项目中会用到Afinal框架,感觉很方便也挺稳定的。他有四部分功能:FinalActivity、FinalBitmap、FinalDb、FinalHttp。本篇分析FinalBitmap原理,如有错误欢迎指出!

源码分析

1.FinalBitmap的用法

先是新建一个对象,然后做一些配置。

mFinalBitmap = FinalBitmap.create(RadioApplication.mContext);
mFinalBitmap.configLoadingImage(R.drawable.ic_launcher);
mFinalBitmap.configLoadfailImage(R.drawable.ic_launcher);

然后就是一行代码来显示图片

mFinalBitmap.display(imageview, url);

2. 构造函数

private static FinalBitmap mFinalBitmap;
public static synchronized FinalBitmap create(Context ctx) {
    if (mFinalBitmap == null) {
        mFinalBitmap = new FinalBitmap(ctx.getApplicationContext());
    }
    return mFinalBitmap;
}

private FinalBitmap(Context context) {
        mContext = context;
        mConfig = new FinalBitmapConfig(context);
        configDiskCachePath(Utils.getDiskCacheDir(context, "afinalCache").getAbsolutePath());//配置缓存路径
        configDisplayer(new SimpleDisplayer());//配置显示器
        configDownlader(new SimpleDownloader());//配置下载器
    }

单例模式创建一个FinalBitmap对象。如果还没创建的话,那么就会调用FinalBitmap的构造函数。

在构造函数中有如下四个步骤,他们也是FinalBitmap必不可少的基本配置:

  1. 会创建一个FinalBitmapConfig对象,mConfig中包含一个默认对象。
  2. 配置路径用来保存DiskCache路径。
  3. 配置一个SimpleDisplayer显示对象,在下载成功后用来显示图片。
  4. 配置一个SimpleDownloader下载对象用来下载图片。

既然提到了FinalBitmapConfig,下面看下的构造函数。

private class FinalBitmapConfig {
    public String cachePath;
    public Displayer displayer;
    public Downloader downloader;
    public BitmapDisplayConfig defaultDisplayConfig;
    public float memCacheSizePercent;//缓存百分比,内存缓存占android系统分配给每个apk内存的比例
    public int memCacheSize;//内存缓存百分比
    public int diskCacheSize;//磁盘百分比
    public int poolSize = 3;//默认的线程池线程并发数量
    public boolean recycleImmediately = true;//是否立即回收内存

    public FinalBitmapConfig(Context context) {
        defaultDisplayConfig = new BitmapDisplayConfig();

        defaultDisplayConfig.setAnimation(null);
        defaultDisplayConfig.setAnimationType(BitmapDisplayConfig.AnimationType.fadeIn);

        //设置图片的显示最大尺寸(为屏幕的大小,默认为屏幕宽度的1/2)
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        int defaultWidth = (int) Math.floor(displayMetrics.widthPixels / 2);
        defaultDisplayConfig.setBitmapHeight(defaultWidth);
        defaultDisplayConfig.setBitmapWidth(defaultWidth);

    }
}

其中包含了一个默认的defaultDisplayConfig以及其他一些配置。注释已经写的很清楚功能,这边就不再提了。

3. 主入口display方法

加载显示图片的话就调用了display方法,把控件和路径传进来。下面看下这个方法的源码

public void display(View imageView, String uri) {
    doDisplay(imageView, uri, null);
}

 private void doDisplay(View imageView, String uri, BitmapDisplayConfig displayConfig) {
        //初始化工作
        if (!mInit) {
            init();
        }

        //如果传进来的uri或者为空那么就不做任何操作
        if (TextUtils.isEmpty(uri) || imageView == null) {
            return;
        }

        //如果传进来的配置为NULL,那么采用默认的配置
        if (displayConfig == null)
            displayConfig = mConfig.defaultDisplayConfig;

        Bitmap bitmap = null;

        //如果内存缓存不为空,那么从内存缓存中取出图片
        if (mImageCache != null) {
            bitmap = mImageCache.getBitmapFromMemoryCache(uri);
        }

        //如果内存缓存中已经存在的话直接显示该图片
        if (bitmap != null) {
            if (imageView instanceof ImageView) {
                ((ImageView) imageView).setImageBitmap(bitmap);
            } else {
                imageView.setBackgroundDrawable(new BitmapDrawable(bitmap));
            }
        }
        //检测imageView中是否已经有线程在运行。如果没有的话返回true,并开始创建线程下载图片。
        else if (checkImageTask(uri, imageView)) {
            //根据配置创建图片加载以及显示的线程,图片加载显示的主要内容就是在该线程中进行的。
            final BitmapLoadAndDisplayTask task = new BitmapLoadAndDisplayTask(imageView, displayConfig);
            //将图片与该线程进行绑定
            final AsyncDrawable asyncDrawable = new AsyncDrawable(mContext.getResources(), displayConfig.getLoadingBitmap(), task);

            //显示默认的图片,也就是刚开始显示的图片。
            if (imageView instanceof ImageView) {
                ((ImageView) imageView).setImageDrawable(asyncDrawable);
            } else {
                imageView.setBackgroundDrawable(asyncDrawable);
            }
            //新创建的任务开始执行。
            task.executeOnExecutor(bitmapLoadAndDisplayExecutor, uri);
        }
    }

该方法的流程如下图所示:

在这里插入图片描述

从图中看出有四个重要的过程。分别是初始化、从内存缓存中获取图片、直接显示图片、下载显示操作。

其中直接显示图片的操作就不说了,接下来主要介绍其他三个重要的过程:

3.1 init初始化

在所有其他操作之前会进行初始化操作。比如说读取配置信息以及建立线程池等。

private FinalBitmap init() {
    if (!mInit) {
	BitmapCache.ImageCacheParams imageCacheParams = new BitmapCache.ImageCacheParams(mConfig.cachePath);
        //mConfig.memCacheSizePercent的比例介于0.05与0.8之间
        if (mConfig.memCacheSizePercent > 0.05 && mConfig.memCacheSizePercent < 0.8) {
            imageCacheParams.setMemCacheSizePercent(mContext, mConfig.memCacheSizePercent);
        } else {
            //mConfig.memCacheSize如果大于2M
            if (mConfig.memCacheSize > 1024 * 1024 * 2) {
                imageCacheParams.setMemCacheSize(mConfig.memCacheSize);
            } else {
                //设置默认的内存缓存大小
                imageCacheParams.setMemCacheSizePercent(mContext, 0.3f);
            }
        }

        if (mConfig.diskCacheSize > 1024 * 1024 * 5)
            imageCacheParams.setDiskCacheSize(mConfig.diskCacheSize);

        //是否立即回收内存
        imageCacheParams.setRecycleImmediately(mConfig.recycleImmediately);
        //init Cache
        mImageCache = new BitmapCache(imageCacheParams);

        //初始化线程池
        bitmapLoadAndDisplayExecutor = Executors.newFixedThreadPool(mConfig.poolSize, r -> {
            Thread t = new Thread(r);
            // 设置线程的优先级别,让线程先后顺序执行(级别越高,抢到cpu执行的时间越多)
            t.setPriority(Thread.NORM_PRIORITY - 1);
            return t;
        });

        //init BitmapProcess
        mBitmapProcess = new BitmapProcess(mConfig.downloader, mImageCache);

        mInit = true;
    }

    return this;
}
  1. 内存缓存的配置。内存缓存会判断两次,memCacheSizePercent是否符合条件,memCacheSize是否符合条件。如果都不符合条件那么就采用默认的0.3。
  2. 如果diskCacheSize大于5M,则配置imageCacheParams的最大DiskCache大小为diskCacheSize
  3. 配置是否立即回收内存,这会影响到后续MemoryCache的创建。

以上三点配置好了之后就可以创建BitmapCache对象了,该对象用于管理对缓存的操作。

接下来就是就是创建线程池,如果没设置的话默认poolSize是3。该线程池用于执行图片加载任务。

最后就是创建BitmapProcess对象了。他用于管理图片的下载、加入缓存等针对图片的操作。

3.2 从内存缓存中获取图片

这边要先介绍下BitmapCache的初始化过程了。上代码:

public BitmapCache(ImageCacheParams cacheParams) {
     init(cacheParams);
}

private void init(ImageCacheParams cacheParams) {
     mCacheParams = cacheParams;
     // 是否启用内存缓存
     if (mCacheParams.memoryCacheEnabled) {
       //是否立即回收内存
       if(mCacheParams.recycleImmediately) 
          mMemoryCache = new SoftMemoryCacheImpl(mCacheParams.memCacheSize);
       else
          mMemoryCache = new BaseMemoryCacheImpl(mCacheParams.memCacheSize);
     }

     ...
 }

首先是内存缓存的初始化。先判断memoryCacheEnabled = false的话mMemoryCache会一直为null。如果memoryCacheEnabled = true的话,那就判断下是否立即回收内存。根据判断结果就会创建不同MemoryCache对象。一个是SoftMemoryCacheImpl(软应用),另外一个是BaseMemoryCacheImpl(强引用)。而BitmapCache对象的构造函数在FinalBitmap类中初始化的时候会调用。

@FinalBitmap类中的doDisplay方法
if (mImageCache != null) {
            bitmap = mImageCache.getBitmapFromMemoryCache(uri);
}

@ImageCachepublic Bitmap getBitmapFromMemoryCache(String data) {
    if (mMemoryCache != null)
       return mMemoryCache.get(data);
    return null;
}

所以内存缓存中获取图片最终是调用到了mMemoryCache的get方法。

从前面的介绍可以看到mMemoryCache对象是初始化时根据recycleImmediately值由不同的类创建的,他们具有着不同的属性。

SoftMemoryCacheImpl类里面是也是对HashMap进行操作。

private final HashMap<String, SoftReference<Bitmap>> mMemoryCache;

public SoftMemoryCacheImpl(int size) {
   mMemoryCache = new HashMap<String, SoftReference<Bitmap>>();
}

只不过HashMap用了SoftReference软引用对Bitmap封装,get与set操作通过该HashMap进行。

BaseMemoryCacheImpl类中的构造函数

private final LruMemoryCache<String, Bitmap> mMemoryCache;

public BaseMemoryCacheImpl(int size) {
   mMemoryCache = new LruMemoryCache<String, Bitmap>(size) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return Utils.getBitmapSize(bitmap); 
            }
        }; 
}

他是new出一个LruMemoryCache对象,看下LruMemoryCache构造函数:

public LruMemoryCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}

那么本质上他也是对LinkedHashMap对象进行操作。

3.3 启动线程下载图片

为了方便阅读,将前面doDisplay方法该部分的代码搬下来:

else if (checkImageTask(uri, imageView)) {
            //根据配置创建图片加载以及显示的线程,图片加载显示的主要内容就是在该线程中进行的。
            final BitmapLoadAndDisplayTask task = new BitmapLoadAndDisplayTask(imageView, displayConfig);
            //将图片与该线程进行绑定
            final AsyncDrawable asyncDrawable = new AsyncDrawable(mContext.getResources(), displayConfig.getLoadingBitmap(), task);

            //显示默认的图片,也就是刚开始显示的图片。
            if (imageView instanceof ImageView) {
                ((ImageView) imageView).setImageDrawable(asyncDrawable);
            } else {
                imageView.setBackgroundDrawable(asyncDrawable);
            }
            //新创建的任务开始执行。
            task.executeOnExecutor(bitmapLoadAndDisplayExecutor, uri);
}

有一点需要注意:用final修饰的局部变量修饰task之后。之后会同样拷贝一份引用给bitmapLoadAndDisplayExecutor线程池所分配的线程中,所以在该线程结束之后对应的BitmapLoadAndDisplayTask对象也被回收了。当该方法执行完之后task引用被回收,但是分配的线程中仍持有该引用。至于为什么也不是很清楚,先这样子理解。

先看下checkImageTask(uri, imageView),这个是用来判断当前这个加载任务是不是有线程在执行了。如果已经有线程在下载,那么就不用重复操作了。

在介绍具体逻辑之前先看下AsyncDrawable类。

private static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapLoadAndDisplayTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap, BitmapLoadAndDisplayTask bitmapWorkerTask) {
        //创建一个Drawable对象利用Resources对象以及Bitmap对象。
        super(res, bitmap);
        //创建一个bitmapWorkerTask对象的弱引用
        bitmapWorkerTaskReference = new WeakReference<BitmapLoadAndDisplayTask>(
                bitmapWorkerTask);
    }

    //返回bitmapWorkerTask对象
    public BitmapLoadAndDisplayTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

AsyncDrawable继承了BitmapDrawable类,所以他可以用来作为背景使用。

注意到BitmapLoadAndDisplayTaskBitmapLoadAndDisplayTask的弱引用,他不阻止所封装的BitmapLoadAndDisplayTask对象呗回收。前面讲到给他分配的线程持有BitmapLoadAndDisplayTask的引用,也就是说线程结束的时候bitmapWorkerTask对应的内存也会被回收了。此时对应的bitmapWorkerTaskReference.get()就是null了。

OK!回头看下checkImageTask方法:

public static boolean checkImageTask(Object data, View imageView) {
    final BitmapLoadAndDisplayTask bitmapWorkerTask = getBitmapTaskFromImageView(imageView);
	....
}

首先是通过getBitmapTaskFromImageView方法获取BitmapLoadAndDisplayTask对象。

getBitmapTaskFromImageView怎么操作的呢?跟进去看下:

private static BitmapLoadAndDisplayTask getBitmapTaskFromImageView(View imageView) {
    if (imageView != null) {
        Drawable drawable = null;
        if (imageView instanceof ImageView) {
            drawable = ((ImageView) imageView).getDrawable();
        } else {
            drawable = imageView.getBackground();
        }

        if (drawable instanceof AsyncDrawable) {
            final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
            return asyncDrawable.getBitmapWorkerTask();
        }
    }
    return null;
}

重点看asyncDrawable.getBitmapWorkerTask()这一句,从前面的AsyncDrawable可以知道他返回的是AsyncDrawable对象的BitmapWorkerTask

直到了这些,重新看下checkImageTask方法:

public static boolean checkImageTask(Object data, View imageView) {
    final BitmapLoadAndDisplayTask bitmapWorkerTask = getBitmapTaskFromImageView(imageView);

    if (bitmapWorkerTask != null) {
        final Object bitmapData = bitmapWorkerTask.data;
        if (bitmapData == null || !bitmapData.equals(data)) {
            bitmapWorkerTask.cancel(true);
        } else {
            return false;
        }
    }
    return true;
}

getBitmapTaskFromImageView(imageView)方法的返回值为空的话,说明对应的线程以及结束了。

getBitmapTaskFromImageView(imageView)不为空的话,那么就判断下bitmapData(图片地址)与传进来的data(图片地址)值是否一致。一致的话,说明有线程在下载了。不一致的话,说明该imageView控件要显示新的图片了。那么cancel掉当前的进程并且返回true重新请求图片。

将前面代码再次搬下来:

else if (checkImageTask(uri, imageView)) {
            //根据配置创建图片加载以及显示的线程,图片加载显示的主要内容就是在该线程中进行的。
            final BitmapLoadAndDisplayTask task = new BitmapLoadAndDisplayTask(imageView, displayConfig);
            //将图片与该线程进行绑定
            final AsyncDrawable asyncDrawable = new AsyncDrawable(mContext.getResources(), displayConfig.getLoadingBitmap(), task);

            //显示默认的图片,也就是刚开始显示的图片。
            if (imageView instanceof ImageView) {
                ((ImageView) imageView).setImageDrawable(asyncDrawable);
            } else {
                imageView.setBackgroundDrawable(asyncDrawable);
            }
            //新创建的任务开始执行。
            task.executeOnExecutor(bitmapLoadAndDisplayExecutor, uri);
        }

如果判断当前没有线程在做同样的事情,那接下来就开始要下载操作了。

显示创建了BitmapLoadAndDisplayTask对象,再创建AsyncDrawable对象。然后先把AsyncDrawable对象显示出来,注意这边默认是显示加载图片后续加载完成会进行更新显示。最后调用线程池开始加载图片。

executeOnExecutor是怎么执行的呢,进AsyncTaskexecuteOnExecutor方法看一下:

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
           Params... params) {
       ...
       mWorker.mParams = params;
       //利用传进来的线程池开始执行任务。任务的具体业务在mFuture中操作。
       exec.execute(mFuture);     
       return this;
   }

也就是利用线程池执行mFuture,而uri参数传给了mWorker.mParams。但这两个有什么联系呢?看下AsyncTask构造函数他们是怎么定义的:

public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
           ...
        }
    };

    mFuture = new FutureTask<Result>(mWorker) {
        @Override
        protected void done() {
            ...
        }
    };
}

在构造的时候就定义了mWorker以及mFuturemWorker又作为参数传给了mFuture(包括uri),mFuture拿着参数就在线程池中开始执行了。

到此为止负责下载图片的线程就开始了!

7 下载并显示图片

下载并显示图片分为两个部分介绍。一个是线程池如何管理回调方法,一个是各个具体业务(比如说下载,显示等)

7.1 线程池执行BitmapLoadAndDisplayTask任务

首先介绍下BitmapLoadAndDisplayTask类。他属于FinalBitmap的内部类,承载着下载与显示的具体实现。看下他实现了那些业务:

private final Object mPauseWorkLock = new Object();

//暂停操作
public void pauseWork(boolean pauseWork) {
        synchronized (mPauseWorkLock) {
            mPauseWork = pauseWork;
            if (!mPauseWork) {
                mPauseWorkLock.notifyAll();
            }
        }
    }

private class BitmapLoadAndDisplayTask extends AsyncTask<Object, Void, Bitmap> {
    private Object data;
    private final WeakReference<View> imageViewReference;
    private final BitmapDisplayConfig displayConfig;

    public BitmapLoadAndDisplayTask(View imageView, BitmapDisplayConfig config) {
        imageViewReference = new WeakReference<View>(imageView);
        displayConfig = config;
    }

    @Override
    protected Bitmap doInBackground(Object... params) {
        synchronized (mPauseWorkLock) {
                while (mPauseWork && !isCancelled()) {
                    try {
                        mPauseWorkLock.wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
        if (bitmap == null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) {
                bitmap = processBitmap(dataString, displayConfig);
            }
        ...
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        ...
    }

    @Override
    protected void onCancelled(Bitmap bitmap) {
        super.onCancelled(bitmap);
            synchronized (mPauseWorkLock) {
                mPauseWorkLock.notifyAll();
            }
    }
}

这边先讲下同步问题,mPauseWorkLock用来实现同步操作。有两种情况,一种是暂停,一种是cancel取消。

暂停的时候会调用pauseWork方法。pauseWork的参数为true的话会暂停线程,为false的话会继续。他会赋值给mPauseWork并通知mPauseWorkLock的notify方法。

cancel的时候也就是回调方法的时候。isCancelled()已经变成true了,同时他也会调用mPauseWorkLock的notify方法。

那么在doInBackground方法中,有三种情况

  1. 如果是暂停(mPauseWork = true)而且isCancelled() = true,那么wait()就不会去执行。而下面processBitmap也不会执,直接返回null。
  2. 如果是暂停(mPauseWork = true)而且isCancelled() = false的话,那么就会调用wait()方法一直在那边等待。当暂停或者canel的时候都会打断wait()让while循环重新执行一遍。
  3. 如果mPauseWork = false,那么就都不会跑进wait()方法。

总结下:如果只是暂停的话,就会在doInBackground刚开始就进入wait状态。而cancel以及暂停动作会让while循环重新判断一次。

下面介绍下BitmapLoadAndDisplayTask类重写的三个方法的作用:

  1. doInBackground:线程池的线程里面执行的逻辑,主要是一些耗时操作。
  2. onPostExecute:界面的显示操作等。
  3. onCancelled:线程被cancel的时候会回调,用于通知doInBackground中的锁对象。

介绍完功能,回头看下线程池分配的线程是怎么回调的。

前面介绍了executeOnExecutor方法会执行exec.execute(mFuture)。那么先看一下AsyncTask的构造方法:

private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
            //如果有人调用则将Invoked调用标志设置为true
            mTaskInvoked.set(true);

            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            //先执行doInBackground在线程池中执行。执行完毕后返回Bitmap图片对象
            //再利用postResult将Bitmap图片用handler交给主线程处理
            return postResult(doInBackground(mParams));
        }
    };

    mFuture = new FutureTask<Result>(mWorker) {
        @Override
        protected void done() {
            try {
                //异步任务执行完成,获取结果。get()阻塞在执行mFuture的线程。
                //在call()方法没人调用的情况下也要返回结果
                postResultIfNotInvoked(get());
            } catch (InterruptedException e) {
                android.util.Log.w(LOG_TAG, e);
            } catch (ExecutionException e) {
                throw new RuntimeException("An error occured while executing doInBackground()",
                        e.getCause());
            } catch (CancellationException e) {
                postResultIfNotInvoked(null);
            }
        }
    };
}

定义了一个mWorker对象,并将该对象与mFuture绑定在一起。线程池执行mFuture之后会调用mWorker的call方法,执行完毕后会执行FutureTask的done()方法。

看下回调done()的postResultIfNotInvoked方法:

private void postResultIfNotInvoked(Result result) {
    final boolean wasTaskInvoked = mTaskInvoked.get();
    if (!wasTaskInvoked) {
        postResult(result);
    }
}

他是先判断mTaskInvoked.get()是否为ture,默认是flase。那哪里赋值的呢,call()刚开始就设置为true。

第一个疑问:那这个方法的意思就是在没调用call()之前就调用了done(),所以就直接get()返回结果了。为什么会这样子呢?

回到前面的call()方法,他会执行mTaskInvoked.set(true)操作。而mTaskInvoked默认是false,那什么时候不会去调call()呢?Task被cancel的时候!前面提到checkImageTask方法中有可能会跑去执行bitmapWorkerTask.cancel(true)方法,可能还没调用call()就被cancel了直接回调done()方法。

跟进cancel方法看看:

public boolean cancel(boolean mayInterruptIfRunning) {
        ...
        try {    // in case call to interrupt throws exception
            ...
        } finally {
            finishCompletion();
        }
        return true;
    }

private void finishCompletion() {
        ..
        done();
}

注意到他最后会调用到done()方法!

第二个疑问:AtomicBoolean mTaskInvoked,为什么要用AtomicBoolean 修饰呢

因为bitmapWorkerTask.cancel(true)方法是在主线程被调用,而call()方法是在线程池分配的线程中执行。用AtomicBoolean来保证线程安全。

解决完这两个问题,done()也分析完了。接下来看下call()方法,注意到比较重要的一行postResult(doInBackground(mParams))。他是先执行doInBackground方法,再执行postResult方法。

doInBackground方法是在BitmapLoadAndDisplayTask子类中具体实现,他是在线程池分配的线程中执行。

下面看下postResult方法:

private static final InternalHandler sHandler = new InternalHandler();
private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

看下这个MESSAGE_POST_RESULT消息做了哪些处理,他的处理在InternalHandler里面。将当前对象和result封装成AsyncTaskResult对象传给sHandler处理。

private static class InternalHandler extends Handler {
      @SuppressWarnings("unchecked")
	@Override
      public void handleMessage(Message msg) {
          @SuppressWarnings("rawtypes")
   AsyncTaskResult result = (AsyncTaskResult) msg.obj;
          switch (msg.what) {
              case MESSAGE_POST_RESULT:
                  result.mTask.finish(result.mData[0]);
                  break;
          }
      }
  }

result.mTask就是当前Async对象,reusult就是doInBackground返回的结果。接下来看下finish怎么处理的:

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

如果isCancelled()为true的话回调onCancelled方法,如果为false的话回调onPostExecute方法。

OK!到这边BitmapLoadAndDisplayTask三个重写的方法都找到了调用的地方。

接下来重点关注下doInBackground以及onPostExecute这两个方法:

7.2 doInBackground方法获取图片

贴上doInBackground的代码:

@Override
protected Bitmap doInBackground(Object... params) {
    data = params[0];
    final String dataString = String.valueOf(data);
    Bitmap bitmap = null;

    ...

    //真正的获取Bitmap在processBitmap方法中操作
    if (bitmap == null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) {
        bitmap = processBitmap(dataString, displayConfig);
    }

    //当bitmap加载成功后,将该图片添加到内存缓存中
    if (bitmap != null) {
        mImageCache.addToMemoryCache(dataString, bitmap);
    }

    return bitmap;
}

重点看下getAttachedImageView()这个方法:

/**
 * 获取线程匹配的imageView,防止出现闪动的现象
 * @return
 */
private View getAttachedImageView() {
    final View imageView = imageViewReference.get();
    final BitmapLoadAndDisplayTask bitmapWorkerTask = getBitmapTaskFromImageView(imageView);

    if (this == bitmapWorkerTask) {
        return imageView;
    }

    return null;
}

前面讲checkImageTask判断后的代码块里,BitmapLoadAndDisplayTask是作为弱引用存在于AsyncDrawable里。而ImageView又设置了AsyncDrawable对象作为背景。最后BitmapLoadAndDisplayTask里又分装了一个iamgeview的弱引用。

结构如下图所示:

在这里插入图片描述

分析下getAttachedImageView()里的代码:

先是从BitmapLoadAndDisplayTask里获取imageview,再从imageview里获取AsyncDrawable,再从AsyncDrawable里获取BitmapLoadAndDisplayTask。然后和自己(BitmapLoadAndDisplayTask对象)做判断。

为什么绕一大圈后又和自己判断,难道不是一样?

以下是我的推测。

流程如下图所示:

在这里插入图片描述

持有弱引用那两个是不会变的,构造函数定义了就不会变。重点就在于imageView!如果在分配线程之后,imageView控件的背景可能会被修改。就比如说ListView的控件就是重复使用的,并不是和背景一一对应关系。

那如果不使用getAttachedImageView()方法会造成什么问题呢?

就是AsyncDrawableAsyncDrawable_1有可能交叉显示。其实前面checkImageTask()就是用来拦截这个问题的,但是可能有漏网之鱼。这边就在下载和显示两个回调方法里再次拦截。

7.2.1 processBitmap方法获取图片

接下来看下processBitmap(dataString, displayConfig)方法:

private Bitmap processBitmap(String uri, BitmapDisplayConfig config) {
    if (mBitmapProcess != null) {
        return mBitmapProcess.getBitmap(uri, config);
    }
    return null;
}

public Bitmap getBitmap(String url, BitmapDisplayConfig config) {

		//尝试从磁盘读取图片
		Bitmap bitmap = getFromDisk(url,config);

		//如果获取的Bitmap为null,则通过url下载图片
		if(bitmap == null){
			byte[] data = mDownloader.download(url);
			if(data != null && data.length > 0){
                	//什么情况下配置为空?
				if(config !=null)
					//如果配置不为空,那么按照配置设定的长宽解析数据。
					bitmap =  BitmapDecoder.decodeSampledBitmapFromByteArray(data,0,data.length,config.getBitmapWidth(),config.getBitmapHeight());
				else
					//如果配置为空,那么按默认状态配置数据。
					return BitmapFactory.decodeByteArray(data,0,data.length);
				//将url于data加入DiskCache缓存中
				mCache.addToDiskCache(url, data);
			}
		}
		
		return bitmap;
	}

这个流程也是比较清晰的,如下图所示:

在这里插入图片描述

可以看到主要有两个部分:1、读取与存入Disk缓存。2、下载图片并解析数据。

7.2.1.2 DiskCache缓存操作

先看下操作读取缓存,从processBitmap方法入手:

public Bitmap getFromDisk(String key,BitmapDisplayConfig config) {
		//从BytesBuffer池中获取一个BytesBuffer对象
        BytesBuffer buffer = sMicroThumbBufferPool.get();
        Bitmap b = null;
        try {

        	boolean found = mCache.getImageData(key, buffer);
            if ( found && buffer.length - buffer.offset > 0) {
    	        if( config != null){
    	            b = BitmapDecoder.decodeSampledBitmapFromByteArray(buffer.data,buffer.offset, buffer.length ,config.getBitmapWidth(),config.getBitmapHeight());
    	        }else{
    	        	b = BitmapFactory.decodeByteArray(buffer.data, buffer.offset, buffer.length);
    	        }
            }
        } finally {
			//将BytesBuffer对象清空后重新放入BytesBuffer池。
        	sMicroThumbBufferPool.recycle(buffer);
        }
        return b;
    }

首先就是一个BytesBuffer池操作,这边就不多说了。用完之后BytesBuffer池就把BytesBuffer回收掉,避免重复创建消耗资源。关键是mCache.getImageData(key, buffer)这个方法,进去看下:

public boolean getImageData(String url, BytesBuffer buffer){
    	...
    	byte[] key = Utils.makeKey(url);
        long cacheKey = Utils.crc64Long(key);
        LookupRequest request = new LookupRequest();
            request.key = cacheKey;
            request.buffer = buffer.data;
            synchronized (mDiskCache) {
                if (!mDiskCache.lookup(request)) 
                	return false;
        }
    	...
        //校验后将数据赋值给buffer后返回。
        if (Utils.isSameKey(key, request.buffer)) {
            buffer.data = request.buffer;
            buffer.offset = key.length;
            buffer.length = request.length - buffer.offset;
            return true;
        }
    }

首先是利用url经过一系列的计算之后得到cacheKey。将cacheKey以及buffer.data封装到request并调用mDiskCache.lookup方法。执行完该方法后,接下来会将得到的数据重新填入buffer中返回。

重点看下lookup方法:

public boolean lookup(LookupRequest req) throws IOException {
        // Look up in the active region first.
        if (lookupInternal(req.key, mActiveHashStart)) {
            if (getBlob(mActiveDataFile, mFileOffset, req)) {
                return true;
            }
        }
		...
    }

关键的地方就是getBlob方法,他会从mActiveDataFile文件读取数据到req中。

先看下mActiveDataFile是什么东西,在DiskCache文件中:

private void setActiveVariables() throws IOException {
    mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
    mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1;
    ...
}

有两个变量mActiveDataFile以及mInactiveDataFile,他们分别可能通过判断mActiveRegionmDataFile0mDataFile1对应。

mActiveRegion、mDataFile0、mDataFile1又分别是什么呢?

先看下mActiveRegion,跟踪下哪里给他赋值了。

public void insert(long key, byte[] data) throws IOException {
        ...

        if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes
                || mActiveEntries * 2 >= mMaxEntries) {
            flipRegion();
        }private void flipRegion() throws IOException {
    mActiveRegion = 1 - mActiveRegion;
    ...
}

mMaxBytesmMaxEntries是配置的时候传进来的最大值,如果没设置默认情况下是:

//默认的磁盘缓存大小
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 50; // 50MB
private static final int DEFAULT_DISK_CACHE_COUNT = 1000 * 10 ; // 缓存的图片数量

那就是说如数据超过50MB或者图片数量大于10000张的时候就会做执行mActiveRegion = 1 - mActiveRegion,对应的mActiveDataFile以及mInactiveDataFile对应的文件做了一下切换。

mDataFile0mDataFile1又是什么呢?

public DiskCache(String path, int maxEntries, int maxBytes, boolean reset, int version) throws IOException {
   		...
      mDataFile0 = new RandomAccessFile(path + ".0", "rw");
      mDataFile1 = new RandomAccessFile(path + ".1", "rw");
  }

构造的时候定义了mDataFile0以及mDataFile1对象。也就是会生成.0和.1结尾的文件,他们就是用来存放缓存的。

总结下:mActiveDataFile就是当前被启用的缓存文件,有可能对应着.0或者.1的文件。如果存放的数据满了的话。那就会对应的切换被启用的缓存文件。

重新再看一下lookup方法。

如果从刚才启用的缓存文件里找不到数据会怎么处理呢?

public boolean lookup(LookupRequest req) throws IOException {
        // Look up in the active region first.
        if (lookupInternal(req.key, mActiveHashStart)) {
            if (getBlob(mActiveDataFile, mFileOffset, req)) {
                return true;
            }
        }
		...
        if (lookupInternal(req.key, mInactiveHashStart)) {
            if (getBlob(mInactiveDataFile, mFileOffset, req)) {
                // If we don't have enough space to insert this blob into
                // the active file, just return it.
                if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes
                        || mActiveEntries * 2 >= mMaxEntries) {
                    return true;
                }
                // Otherwise copy it over.
                mSlotOffset = insertOffset;
                try {
                    insertInternal(req.key, req.buffer, req.length);
                    mActiveEntries++;
                    writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
                    updateIndexHeader();
                } catch (Throwable t) {
                    Log.e(TAG, "cannot copy over");
                }
                return true;
            }
        }
    }

可以看到,他会选择从mInactiveHashStart那个没被启用的缓存文件里读取。接下来就是判断如果这个被启用的缓存文件还没满的话,就把该数据从没被启用的缓存文件拷贝到被启用的缓存文件。

到这边getImageData获取缓存数据就说完了。

下面说下如何存储数据,调用了BitmapCatch.addToDiskCache方法。看下代码:

public void addToDiskCache(String url, byte[] data) {
    if (mDiskCache == null || url == null || data == null) {
        return;
    }
    byte[] key = Utils.makeKey(url);
    long cacheKey = Utils.crc64Long(key);
    //将key和data写入buffer
    ByteBuffer buffer = ByteBuffer.allocate(key.length + data.length);
    buffer.put(key);
    buffer.put(data);
    synchronized (mDiskCache) {
        try {
           mDiskCache.insert(cacheKey, buffer.array());
        } catch (IOException ex) {
            // ignore.
        }
    }

}

重点是mDiskCache.insert方法,他负责把数据写入文件当中:

public void insert(long key, byte[] data) throws IOException {
    ...
    if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes
            || mActiveEntries * 2 >= mMaxEntries) {
        flipRegion();
    }
    ...
    insertInternal(key, data, data.length);
}

private void insertInternal(long key, byte[] data, int length)
            throws IOException {
        ...
        //把头部数据写入当前缓存文件
        mActiveDataFile.write(header);
        //把真正的数据写入当前缓存文件
        mActiveDataFile.write(data, 0, length);
    }

insert中有两个要关注的地方。第一步就是如果数据满了之后,调用flipRegion()切换缓存文件,就是刚才说的会对启用的缓存文件进行切换。第二步就是insertInternal方法,他来实现写入文件。再看下insertInternal方法,利用mActiveDataFile.write操作进行写文件。写文件分析完毕。

7.2.2 Downloader下载图片

默认调用的是SimpleDownLoader下载。提供了网络下载也可以从本地或者,没什么好讲的贴上代码。

public byte[] download (String urlString){
   if (urlString == null)
      return null;
   if (urlString.trim().toLowerCase().startsWith("http")) {
      return getFromHttp(urlString);
   }
   else if(urlString.trim().toLowerCase().startsWith("file:")){
      try {
         File f = new File(new URI(urlString));
         if (f.exists() && f.canRead()) {
            return getFromFile(f);
         }
      } catch (URISyntaxException e) {
         Log.e(TAG, "Error in read from file - " + urlString + " : " + e);
      }
   }
   else{
      File f = new File(urlString);
      if (f.exists() && f.canRead()) {
         return getFromFile(f);
      }
   }
   
   return null;
}

private byte[] getFromHttp(String urlString) {
		HttpURLConnection urlConnection = null;
		BufferedOutputStream out = null;
		FlushedInputStream in = null;

		try {
			final URL url = new URL(urlString);
			urlConnection = (HttpURLConnection) url.openConnection();
			in = new FlushedInputStream(new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE));
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			int b;
			while ((b = in.read()) != -1) {
				baos.write(b);
			}
			return baos.toByteArray();
		} catch (final IOException e) {
			Log.e(TAG, "Error in downloadBitmap - " + urlString + " : " + e);
		} finally {
			if (urlConnection != null) {
				urlConnection.disconnect();
			}
			try {
				if (out != null) {
					out.close();
				}
				if (in != null) {
					in.close();
				}
			} catch (final IOException e) {
			}
		}
		return null;
	}
7.3 onPostExecute方法显示图片

前面doInBackground执行完之后,就开始要回调onPostExecute方法显示图片了。贴上onPostExecute方法代码:

@Override
protected void onPostExecute(Bitmap bitmap) {
    ...

    final View imageView = getAttachedImageView();
    if (bitmap != null && imageView != null) {
        mConfig.displayer.loadCompletedisplay(imageView, bitmap, displayConfig);
    } else if (bitmap == null && imageView != null) {
        mConfig.displayer.loadFailDisplay(imageView, displayConfig.getLoadfailBitmap());
    }
}

doInBackground执行完的结果会通过参数传进来。

如果为null的话,那么就直接显示加载失败的图片。如果不为null的话,这边会用动画来显示下载成功的图片。

注意这边设置完的图片都是Bitmap对象而不是AsyncDrawable对象。

先看下loadFailDisplay方法:

public void loadFailDisplay(View imageView,Bitmap bitmap){
   if(imageView instanceof ImageView){
      ((ImageView)imageView).setImageBitmap(bitmap);
   }else{
      imageView.setBackgroundDrawable(new BitmapDrawable(bitmap));
   }
}

他只是简单的把图片设置进去。

接下来看下loadCompletedisplay方法:

public void loadCompletedisplay(View imageView,Bitmap bitmap,BitmapDisplayConfig config){
   switch (config.getAnimationType()) {
   case BitmapDisplayConfig.AnimationType.fadeIn:
      fadeInDisplay(imageView,bitmap);
      break;
   case BitmapDisplayConfig.AnimationType.userDefined:
      animationDisplay(imageView,bitmap,config.getAnimation());
      break;
   default:
      break;
   }
}

有两种动画,一种是默认的淡入方式,另一种是用户自定义动画。

首先看一下fadeInDisplay方法:

private void fadeInDisplay(View imageView,Bitmap bitmap){
   final TransitionDrawable td =
               new TransitionDrawable(new Drawable[] {
                       new ColorDrawable(android.R.color.transparent),
                       new BitmapDrawable(imageView.getResources(), bitmap)
               });
       if(imageView instanceof ImageView){
      ((ImageView)imageView).setImageDrawable(td);
   }else{
      imageView.setBackgroundDrawable(td);
   }
       td.startTransition(300);
}

300毫秒的延时时间,从透明渐变为图片显示。

再看一下animationDisplay方法:

private void animationDisplay(View imageView,Bitmap bitmap,Animation animation){
   animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());      
   if(imageView instanceof ImageView){
      ((ImageView)imageView).setImageBitmap(bitmap);
   }else{
      imageView.setBackgroundDrawable(new BitmapDrawable(bitmap));
   }
   imageView.startAnimation(animation);
}

就是根据用户设置的animation开始执行动画。

结语

主要涉及了四个方面的知识:多线程同步、数据缓存、图片下载、图片显示。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值