您的位置:首页 > 其它

高效使用Bitmaps(二) 后台加载Bitmap

2013-12-12 10:12 253 查看
转自:/article/3466694.html

为什么要在后台加载Bitmap?

在Android中,使用BitmapFactory.decodeResource(),BitmapFactory.decodeStream() 等方法可以把图片加载到Bitmap中。但由于这些方法是耗时的,所以多数情况下,这些方法应该放在非UI线程中,否则将有可能导致界面的卡顿,甚至是触 发ANR。

一般情况下,网络图片的加载必须放在后台线程中;而本地图片就可以根据实际情况自行决定了,如果图片不多不大的话,也可以在UI线程中操作来图个方便。至于谷歌官方的说法,是只要是从硬盘或者从网络加载Bitmap,统统不应该在主线程中进行。

基础操作:使用AsyncTask

01
class
BitmapWorkerTask
extends
AsyncTask<Integer,Void,Bitmap> {
02
private
final
WeakReference<ImageView> imageViewReference;
03
private
int
data =
0
;
04
05
public
BitmapWorkerTask(ImageView imageView) {
06
// Use a WeakReference to ensure the ImageView can be garbage collected
07
imageViewReference =
new
WeakReference<ImageView>(imageView);
08
}
09
10
// Decode image in background.
11
@Override
12
protected
Bitmap doInBackground(Integer... params) {
13
data =params[
0
];
14
return
decodeSampledBitmapFromResource(getResources(),data,
100
,
100
));
15
}
16
17
// Once complete,see if ImageView is still around and set bitmap.
18
@Override
19
protected
void
onPostExecute(Bitmap bitmap) {
20
if
(imageViewReference !=
null
&& bitmap !=
null
) {
21
final
ImageView imageView = imageViewReference.get();
22
if
(imageView !=
null
) {
23
imageView.setImageBitmap(bitmap);
24
}
25
}
26
}
27
}
以上代码摘自Android官方文档,是一个后台加载Bitmap并在加载完成后自动将Bitmap设置到ImageView的AsyncTask的实现。有了这个AsyncTask之后,异步加载Bitmap只需要下面的简单代码:

1
public
void
loadBitmap(
int
resId,ImageView imageView) {
2
BitmapWorkerTasktask = 
new
BitmapWorkerTask(imageView);
3
task.execute(resId);
4
}
然后,一句loadBitmap(R.id.my_image,mImageView) 就能实现本地图片的异步加载了。

并发操作:在ListView和GridView中进行后台加载

在实际中,影响性能的往往是ListView和GridView这种包含大量图片的控件。在滑动过程中,大量的新图片在短时间内一起被加载,对于没有进行任何优化的程序,卡顿现象必然会随之而来。通过使用后台加载Bitmap的方式,这种问题将被有效解决。具体怎么做,我们来看看谷歌推荐的方法。

首先创建一个实现了Drawable接口的类,用来存储AsyncTask的引用。在本例中,选择了继承BitmapDrawable,用来给ImageView设置一个预留的占位图,这个占位图用于在AsyncTask执行完毕之前的显示。

01
static
class
AsyncDrawable
extends
BitmapDrawable {
02
private
final
WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
03
04
public
AsyncDrawable(Resources res,Bitmap bitmap,
05
BitmapWorkerTaskbitmapWorkerTask) {
06
super
(res,bitmap);
07
bitmapWorkerTaskReference =
08
new
WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
09
}
10
11
public
BitmapWorkerTaskgetBitmapWorkerTask() {
12
return
bitmapWorkerTaskReference.get();
13
}
14
}
接下来,和上面类似,依然是使用一个loadBitmap()方法来实现对图片的异步加载。不同的是,要在启动AsyncTask之前,把AsyncTask传给AsyncDrawable,并且使用AsyncDrawable为ImageView设置占位图:

1
public
void
loadBitmap(
int
resId,ImageView imageView) {
2
if
(cancelPotentialWork(resId,imageView)) {
3
final
BitmapWorkerTasktask = 
new
BitmapWorkerTask(imageView);
4
final
AsyncDrawableasyncDrawable =
5
new
AsyncDrawable(getResources(),mPlaceHolderBitmap,task);
6
imageView.setImageDrawable(asyncDrawable);
7
task.execute(resId);
8
}
9
}
然后在Adapter的getView()方法中调用loadBitmap()方法,就可以为每个Item中的ImageView进行图片的动态加载了。

loadBitmap()方法的代码中有两个地方需要注意:第一,cancelPotentialWork()这个方法,它的作用是进行两项检查:首先检查当前是否已经有一个AsyncTask正在为这个ImageView加载图片,如果没有就直接返回true。如果有,再检查这个Task正在加载的资源是否与自己正要进行加载的资源相同,如果相同,那就没有必要再进行多一次的加载了,直接返回false;而如果不同(为什么会不同?文章最后会有解释),就取消掉这个正在进行的任务,并返回true。第二个需要注意的是,本例中的 BitmapWorkerTask实际上和上例是有所不同的。这两点我们分开说,首先我们看cancelPotentialWork()方法的代码:

01
public
static
boolean
cancelPotentialWork(
int
data,ImageView imageView) {
02
final
BitmapWorkerTaskbitmapWorkerTask = getBitmapWorkerTask(imageView);
03
04
if
(bitmapWorkerTask !=
null
) {
05
final
int
bitmapData = bitmapWorkerTask.data;
06
if
(bitmapData != data) {
07
// 取消之前的任务
08
bitmapWorkerTask.cancel(
true
);
09
}
else
{
10
// 相同任务已经存在,直接返回false,不再进行重复的加载
11
return
false
;
12
}
13
}
14
// 没有Task和ImageView进行绑定,或者Task由于加载资源不同而被取消,返回true
15
return
true
;
16
}
在cancelPotentialWork()的代码中,首先使用getBitmapWorkerTask()方法获取到与ImageView相关联的Task,然后进行上面所说的判断。好,我们接着来看这个getBitmapWorkerTask()是怎么写的:

01
private
static
BitmapWorkerTaskgetBitmapWorkerTask(ImageView imageView) {
02
if
(imageView !=
null
) {
03
final
Drawable drawable = imageView.getDrawable();
04
if
(drawable
instanceof
AsyncDrawable) {
05
final
AsyncDrawableasyncDrawable = (AsyncDrawable) drawable;
06
return
asyncDrawable.getBitmapWorkerTask();
07
}
08
}
09
return
null
;
10
}
从代码中可以看出,该方法通过imageView获取到与它内部的 Drawable对象,如果获取到了并且该对象为AsyncDrawable的实例,就调用这个AsyncDrawable的 getBitmapWorkerTask()方法来获取到它对应的Task,也就是通过一个 ImageView->Drawable->AsyncTask的链来获取到ImageView所对应的AsyncTask。

好的,cancelPotentialWork()方法分析完了,我们回到刚才提到的第二个点:BitmapWorkerTask类的不同。这个类的改动在于onPostExecute()方法,具体请看下面代码:

01
class
BitmapWorkerTask
extends
AsyncTask<Integer,Void,Bitmap> {
02
...
03
04
@Override
05
protected
void
onPostExecute(Bitmap bitmap) {
06
if
(isCancelled()) {
07
bitmap =
null
;
08
}
09
10
if
(imageViewReference !=
null
&& bitmap !=
null
) {
11
final
ImageView imageView = imageViewReference.get();
12
final
BitmapWorkerTaskbitmapWorkerTask =
13
getBitmapWorkerTask(imageView);
14
if
(
this
== bitmapWorkerTask && imageView !=
null
) {
15
imageView.setImageBitmap(bitmap);
16
}
17
}
18
}
19
}
从代码中可以看出,在后台加载完Bitmap之后,它 并不是直接把Bitmap设置给ImageView,而是先判断这个ImageView对应的Task是不是自己(为什么会不同?文章最后会有解释)。如果是自己,才会执行ImageView的setImageBitmap()方法。到此,一个并发的异步加载ListView(或GridView)中图片的实现全部完成。

延伸:[b]文中两个“[b]为什么会不同”的解答[/b][/b]

首先,简单说一下ListView中Item和Item对应的View的关系 (GridView中同理)。假设一个ListView含有100项,那么它的100个Item应该分别对应一个View用于显示,这样一共是100个 View。但Android实际上并没有这样做。出于内存考虑,Android只会为屏幕上可见的每个Item分配一个View。用户滑动 ListView,当第一个Item移动到在可视范围外后,他所对应的View将会被系统分配给下一个即将出现的Item。

回到问题。

我们不妨假设屏幕上显示了一个ListView,并且它最多能显示10个 Item,而用户在最顶部的Item(不妨称他为第1个Item)使用Task加载Bitmap的时候进行了滑动,并且直到第1个Item消失而第11个 Item已经在屏幕底部出现的时候,这个Task还没有加载完成。那么此时,原先与第1个Item绑定的ImageView已经被重新绑定到了第11个 Item上,并且第11个Item触发了getItem()方法。在getItem()方法中,ImageView第二次使用Task为自己加载Bitmap,但这时它需要加载的图片资源已经变了(由第1个Item对应的资源变成了第11个Item对应的资源),因此在cancelPotentialWork()方法执行时会判断两个资源不一致。这就是为什么相同ImageView却对应了不同的资源。

同理,一个Task持有了一个ImageView,但由于这个Task有可能已经过时,因此这个ImageView所对应的Task未必就是这个Task本身,也有可能是另一个更年轻的Task。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: