前言
在项目中会用到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
必不可少的基本配置:
- 会创建一个
FinalBitmapConfig
对象,mConfig
中包含一个默认对象。 - 配置路径用来保存
DiskCache
路径。 - 配置一个
SimpleDisplayer
显示对象,在下载成功后用来显示图片。 - 配置一个
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;
}
- 内存缓存的配置。内存缓存会判断两次,
memCacheSizePercent
是否符合条件,memCacheSize
是否符合条件。如果都不符合条件那么就采用默认的0.3。 - 如果
diskCacheSize
大于5M
,则配置imageCacheParams
的最大DiskCache
大小为diskCacheSize
。 - 配置是否立即回收内存,这会影响到后续
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);
}
@ImageCache类
public 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
类,所以他可以用来作为背景使用。
注意到BitmapLoadAndDisplayTask
是BitmapLoadAndDisplayTask
的弱引用,他不阻止所封装的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
是怎么执行的呢,进AsyncTask
的executeOnExecutor
方法看一下:
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
以及mFuture
。mWorker
又作为参数传给了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
方法中,有三种情况
- 如果是暂停
(mPauseWork = true)
而且isCancelled() = true
,那么wait()就不会去执行。而下面processBitmap
也不会执,直接返回null。 - 如果是暂停
(mPauseWork = true)
而且isCancelled() = false
的话,那么就会调用wait()方法一直在那边等待。当暂停或者canel
的时候都会打断wait()让while循环重新执行一遍。 - 如果mPauseWork = false,那么就都不会跑进wait()方法。
总结下:如果只是暂停的话,就会在doInBackground
刚开始就进入wait状态。而cancel以及暂停动作会让while循环重新判断一次。
下面介绍下BitmapLoadAndDisplayTask
类重写的三个方法的作用:
doInBackground
:线程池的线程里面执行的逻辑,主要是一些耗时操作。onPostExecute
:界面的显示操作等。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()
方法会造成什么问题呢?
就是AsyncDrawable
和AsyncDrawable_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
,他们分别可能通过判断mActiveRegion
与mDataFile0
与mDataFile1
对应。
那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;
...
}
mMaxBytes
与mMaxEntries
是配置的时候传进来的最大值,如果没设置默认情况下是:
//默认的磁盘缓存大小
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
对应的文件做了一下切换。
mDataFile0
和mDataFile1
又是什么呢?
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开始执行动画。
结语
主要涉及了四个方面的知识:多线程同步、数据缓存、图片下载、图片显示。