如何高效的显示Bitmap

前言

了解如何使用常用技术来处理和加载Bitmap,使我们的UI组件保持响应并避免超出应用程序内存限制。如果不小心,Bitmap可以快速消耗你的可用内存预算,导致应用程序崩溃,出现OOM.
为什么在Android应用程序中加载Bitmap是件很棘手的事情?
   ①移动设备资源受限.Android设备可以为当个应用程序提供少至16MB的内存,所有应当优化应用程序以在最小的内存限制下都可执行。

   ②Bitmap占用了大量的内存,特别是对于像照片这样的丰富图像,如果使用的位图配置时ARGB_8888(Android系统默认),以10801920像素的照片为例,则将此图像加载到内存中大约需要8M内存(10801920*4字节),这样会立即耗尽某些设备上的每个应用程序限制。

   ③Android应用UI通常需要一次就加载几个Bitmap.像ListView,GridView和ViewPager等组件通常就会同时在屏幕上显示几张Bitmap。


一.有效的加载大的Bitmap

不超出每个应用程序内存限制的情况下解码大的Bitmp,方法是在内存中加载较小的Bitmap。

1.1读取Bitmap的尺寸和类型

BitmapFactory类提供了几种解码方法(decodeByteArray(),decodeFile(),decodeResource()等),用于从各种源创建Bitmap。 根据图像数据源选择最合适的解码方法。 这些方法尝试为构造的位图分配内存,因此很容易导致OOM异常。 每种类型的解码方法都有其他签名,可通过BitmapFactory.Options类指定解码选项。 解码时将inJustDecodeBounds属性设置为true可避免内存分配,为位图对象返回null,但设置outWidth,outHeight和outMimeType.该方式允许在构造(和内存分配)位图之前读取图像数据的尺寸和类型。

1
2
3
4
5
6
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

要避免OOM,就需要在解码之前检查Bitmap的尺寸.

1.2将缩小的Bitmap加载到内存中

图像尺寸大小已知,它们就可决定是否将完整的图像加载到内存中,或者是够应加载小的Bitmap,以下时需要考虑的因素:
   ①估计在内存中加载完整映像的内存使用情况。
   ②在给定应用程序的任何其他内存要求的情况下,可以加载此映像的内存量。
   ③要加载图像的目标ImageView或UI组件的尺寸。
   ④屏幕尺寸和当前设备的密度。
要告诉解码器将较小的Bitmap加载到内存中,就需要将BitmapFactory.Options对象中的inSampleSize设置为true。以下是根据目标宽度和高度计算本大小值的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
//图像的原始高度和宽度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//计算最大的inSampleSize值,该值为2次幂并保持两者
//高度和宽度大于要求的高度和宽度。
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}

注意:计算两个幂的幂是因为解码器使用最终值通过舍入到最接近的2次幂
要使用此方法,首先使用inJustDecodeBounds设置为true进行解码,传递选项,然后使用新的inSampleSize值再次解码,并将inJustDecodeBounds设置为false

1
2
3
4
5
6
7
8
9
10
11
12
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 首先使用inJustDecodeBounds = true进行解码以检查尺寸
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用inSampleSize设置解码位图
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

此方法可以轻松地将任意大尺寸的位图加载到显示100x100像素缩略图的ImageView中,如以下示例代码所示:

1
2
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

二.关闭UI线程处理Bitmap

如果从磁盘或网络位置(或实际上除内存之外的任何其他源)读取源数据,则不应在主UI线程上执行BitmapFactory.decode *方法。 因为这些数据加载所需的时间是不可预测的,取决于各种因素(从磁盘或网络读取的速度,映像的大小,CPU的功率等)。如果其中一个任务阻止了UI线程,系统会将应用程序标记为无响应,并且用户可以选择关闭它。
下面主要是讲如何使用AsyncTask处理后台线程中的Bitmap。

2.1 使用AsyncTask

AsyncTask类提供了一种在后台线程中执行某些工作并将结果发布回UI线程的简单方法。要使用它,就需要创建一个子类并覆盖提供的方法。下面是使用AsyncTask和decodeSampledBitmapFromResource()将大图像加载到ImageView的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;

public BitmapWorkerTask(ImageView imageView) {
// 使用WeakReference确保可以对ImageView进行垃圾回收
imageViewReference = new WeakReference<ImageView>(imageView);
}

// 在后台解码图像。
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}

// 完成后,查看ImageView是否仍然存在并设置Bitmap。
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}

对ImageView的WeakReference确保AsyncTask不会阻止ImageView及其引用的任何内容被垃圾回收。 当任务完成时,无法保证ImageView仍然存在,因此还必须检查onPostExecute()中的引用。
要开始异步加载位图,只需创建一个新任务并执行它:

1
2
3
4
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}

2.2 处理并发

当与AsyncTask结合使用时,ListView和GridView等常见视图组件会引入另一个问题,如上一节所示。 为了提高内存的效率,这些组件会在用户滚动时回收子视图。 如果每个子视图都触发AsyncTask,则无法保证在完成时,关联的视图尚未被回收以在另一个子视图中使用。 此外,无法保证异步任务的启动顺序是它们完成的顺序。
创建一个专用的Drawable子类,以将引用存储回worker任务。 在这种情况下,使用BitmapDrawable,以便在任务完成时可以在ImageView中显示占位符图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}

public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}

在执行BitmapWorkerTask之前,需要创建一个AsyncDrawable并将其绑定到目标ImageView:

1
2
3
4
5
6
7
8
9
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}

上面的代码示例中引用的cancelPotentialWork方法检查另一个正在运行的任务是否已与ImageView关联。 如果是,则尝试通过调用cancel()取消先前的任务。 在少数情况下,新任务数据与现有任务匹配,不需要进一步发生任何事情。 这是cancelPotentialWork的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
//如果尚未设置bitmapData或它与新数据不同
if (bitmapData == 0 || bitmapData != data) {
// 取消上一个任务
bitmapWorkerTask.cancel(true);
} else {
// 同样的工作已在进行中
return false;
}
}
// 没有与ImageView关联的任务,或者已取消现有任务
return true;
}

上面使用了一个帮助方法getBitmapWorkerTask()来检索与特定ImageView关联的任务:

1
2
3
4
5
6
7
8
9
10
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}

最后一步是更新BitmapWorkerTask中的onPostExecute(),以便检查任务是否被取消以及当前任务是否与ImageView相关联的任务匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...

@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}

if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}

此实现现在适用于ListView和GridView组件以及回收其子视图的任何其他组件。 只需调用loadBitmap,我们通常会将图像设置为ImageView。 例如,在GridView实现中,这将在后备适配器的getView()方法中。

三.缓存位图

将单个Bitmap加载到用户界面(UI)非常简单,但是如果需要一次加载更多的图像,事情会变得更加复杂。 在许多情况下(例如使用ListView,GridView或ViewPager等组件),屏幕上的图像总数与可能很快滚动到屏幕上的图像基本上是无限的。
通过在子屏幕移动时回收子视图,可以使用这样的组件来降低内存使用率。 垃圾收集器还可以释放加载的Bitmap,假设在没有保留任何长期存在的引用。 这一切都很好,但为了保持流畅和快速加载的UI,我们希望避免每次它们回到屏幕时不断处理这些图像。 内存和磁盘缓存通常可以在这里提供帮助,允许组件快速重新加载已处理的映像。
下面将介绍使用内存和磁盘位图缓存来提高加载多个位图时UI的响应能力和流动性。

3.1 使用内存缓存

内存缓存以占用宝贵的应用程序内存为代价提供对位图的快速访问。 LruCache类(也可在支持库中使用,可用于API级别4)特别适合缓存位图,将最近引用的对象保存在强引用的LinkedHashMap中,并在缓存超过其之前驱逐最近最少使用的成员 指定大小。
以下是为Bitmap设置LruCache的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
...
//获取最大可用VM内存,超过此数量将抛出一个OOM异常。 以LruCache为单位存储为千字节
//在其构造函数中进行定义。
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

// 使用此内存缓存的1/8可用内存。
final int cacheSize = maxMemory / 8;

mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//缓存大小将以千字节为单位
return bitmap.getByteCount() / 1024;
}
};
...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}

public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}

将位图加载到ImageView时,首先检查LruCache。 如果找到条目,则立即使用它来更新ImageView,否则会生成后台线程来处理图像:

1
2
3
4
5
6
7
8
9
10
11
12
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);

final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}

还需要更新BitmapWorkerTask以将条目添加到内存缓存:

1
2
3
4
5
6
7
8
9
10
11
12
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}

3.2 使用磁盘缓存

内存缓存对于加快对最近查看的位图的访问非常有用,但是不能依赖此缓存中可用的图像。 具有较大数据集的GridView等组件可以轻松填充内存缓存。 应用程序可能会被其他任务(例如电话呼叫)中断,而在后台,它可能会被终止并且内存缓存会被破坏。 用户恢复后,应用程序必须再次处理每个图像。
在这些情况下,可以使用磁盘缓存来保留已处理的位图,并有助于减少内存缓存中不再提供映像的加载时间。 当然,从磁盘获取图像比从内存加载要慢,并且应该在后台线程中完成,因为磁盘读取时间可能是不可预测的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 初始化内存缓存
...
// 在后台线程上初始化磁盘缓存
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // 完成初始化
mDiskCacheLock.notifyAll(); // 唤醒所有等待的线程
}
return null;
}
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// 在后台解码图像。
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);

// 检查后台线程中的磁盘缓存
Bitmap bitmap = getBitmapFromDiskCache(imageKey);

if (bitmap == null) { // 在磁盘缓存中找不到
// 正常处理
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}

// 最终Bitmap添加到缓存
addBitmapToCache(imageKey, bitmap);

return bitmap;
}
...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
// 添加到内存缓存
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}

// 添加到磁盘缓存
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}

public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// 等待从后台线程启动磁盘缓存
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
}

//创建指定应用程序缓存目录的唯一子目录。 试图使用外部,但如果没有安装,则回退到内部存储。
public static File getDiskCacheDir(Context context, String uniqueName) {
检查是否已安装介质或内置存储,如果是,请尝试使用外部缓存目录
否则使用内部缓存目录
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();

return new File(cachePath + File.separator + uniqueName);
}

在UI线程中检查内存高速缓存时,将在后台线程中检查磁盘高速缓存。 磁盘操作绝不应该在UI线程上进行。 图像处理完成后,最终的Bitmap将添加到内存和磁盘缓存中以备将来使用。

3.3 处理配置更改

运行时配置更改(例如屏幕方向更改)会导致Android使用新配置销毁并重新启动运行活动。 避免再次处理所有图像,以便在发生配置更改时,用户可以获得流畅,快速的体验。
不过可以使用通过调用setRetainInstance(true)保留的Fragment将此高速缓存传递给新的Activity实例。 重新创建Activity后,将重新附加此保留的Fragment,可以访问现有的缓存对象,从而可以快速获取图像并将其重新填充到ImageView对象中。
下面介绍在配置更改中保留LruCache对象的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
... // 初始化缓存
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}

class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache<String, Bitmap> mRetainedCache;

public RetainFragment() {}

public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}

四.管理Bitmap内存

除了缓存位图中描述的步骤之外,还可以执行一些特定操作来促进垃圾收集和位图重用。

4.1 在Android 3.0及更高版本上管理内存

4.1.1 保存Bitmap供以后使用

以下代码段演示了如何存储现有位图,以便以后在示例应用程序中使用。 当应用程序在Android 3.0或更高版本上运行并且位图从LruCache中逐出时,对位图的软引用将放置在HashSet中,以便稍后可以在inBitmap中重用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;

//如果在Honeycomb或更新版本上运行,请创建一个synchronized HashSet对可重用位图的引用。
if (Utils.hasHoneycomb()) {
mReusableBitmaps =
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

// 通知已删除的不再缓存的条目。
@Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
//删除的条目是回收可绘制的,因此需要通知它已从内存缓存中删除。
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// 删除的条目是标准的BitmapDrawable。
if (Utils.hasHoneycomb()) {
//我们在Honeycomb或更高版本上运行,因此添加位图到一个SoftReference
//集合,以便以后可能与inBitmap一起使用。
mReusableBitmaps.add
(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}

4.1.2 使用现有Bitmap

在运行的应用程序中,解码器方法检查是否存在可以使用的现有Bitmap。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight, ImageCache cache) {

final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...

// If we're running on Honeycomb or newer, try to use inBitmap.
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}

下面代码显示了上述代码段中调用的addInBitmapOptions()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static void addInBitmapOptions(BitmapFactory.Options options,
ImageCache cache) {
//inBitmap只适用于可变位图,因此强制解码器返回可变位图。
options.inMutable = true;
if (cache != null) {
// 尝试找到用于inBitmap的位图。
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

if (inBitmap != null) {
//如果找到合适的位图,请将其设置为值inBitmap。
options.inBitmap = inBitmap;
}
}
}

//此方法遍历可重用的位图,查找一个用于inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;

if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
synchronized (mReusableBitmaps) {
final Iterator<SoftReference<Bitmap>> iterator
= mReusableBitmaps.iterator();
Bitmap item;

while (iterator.hasNext()) {
item = iterator.next().get();

if (null != item && item.isMutable()) {
// 检查以查看该项目可用于inBitmap。
if (canUseForInBitmap(item, options)) {
bitmap = item;

// 从可重复使用的设备中取出,因此无法再次使用。
iterator.remove();
break;
}
} else {
// 如果已清除引用,则从集合中删除。
iterator.remove();
}
}
}
}
return bitmap;
}

最后,此方法确定候选位图是否满足用于inBitmap的大小标准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//从Android 4.4开始,我们可以重新使用新Bitmap小于可重复使用的
//Bitmap候选分配字节数。
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}

// 在早期版本中,维度必须完全匹配,并且inSampleSize必须为1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}

/**
*一个辅助函数,用于根据位图的配置返回位图的每个像素的字节使用情况。
*/
static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
return 4;
} else if (config == Config.RGB_565) {
return 2;
} else if (config == Config.ARGB_4444) {
return 2;
} else if (config == Config.ALPHA_8) {
return 1;
}
return 1;
}

五 在UI中显示Bitmap

下面将展示如何使用后台线程和位图缓存将多个位图加载到ViewPager和GridView组件中,同时处理并发和配置更改。

5.1 将位图加载到ViewPager实现中

滑动视图图案是导航图库的详细视图的绝佳方式。 可以使用由PagerAdapter支持的ViewPager组件来实现此模式。 但是,更合适的后备适配器是子类FragmentStatePagerAdapter,它会在ViewPager中自动销毁和保存片段的状态,因为它们会在屏幕外消失,从而降低内存使用率。
以下使用ImageView子级的ViewPager的实现。 主要活动包含ViewPager和适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ImageDetailActivity extends FragmentActivity {
public static final String EXTRA_IMAGE = "extra_image";

private ImagePagerAdapter mAdapter;
private ViewPager mPager;

// 用于支持ViewPager适配器的静态数据集
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager); // 仅包含ViewPager

mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
}

public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;

public ImagePagerAdapter(FragmentManager fm, int size) {
super(fm);
mSize = size;
}

@Override
public int getCount() {
return mSize;
}

@Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
}
}

这是一个包含ImageView子元素的细节Fragment的实现,怎么可以改进?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private ImageView mImageView;

static ImageDetailFragment newInstance(int imageNum) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
f.setArguments(args);
return f;
}

// 空构造函数,根据Fragment docs所需
public ImageDetailFragment() {}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// image_detail_fragment.xml 只包含一个 ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
return v;
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final int resId = ImageDetailActivity.imageResIds[mImageNum];
mImageView.setImageResource(resId); // 将图像加载到ImageView中
}
}

以上会有一个问题:从UI线程上的资源中读取图像,这可能导致应用程序挂起并被强制关闭。 使用处理位图关闭UI线程课程中描述的AsyncTask,可以直接将图像加载和处理移动到后台线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ImageDetailActivity extends FragmentActivity {
...

public void loadBitmap(int resId, ImageView imageView) {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}

... // 包括BitmapWorkerTask类
}

public class ImageDetailFragment extends Fragment {
...

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (ImageDetailActivity.class.isInstance(getActivity())) {
final int resId = ImageDetailActivity.imageResIds[mImageNum];
// 调用ImageDetailActivity以在后台线程中加载Bitmap
((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
}
}
}

任何其他处理(例如调整大小或从网络获取图像)都可以在BitmapWorkerTask中进行,而不会影响主UI的响应性。 如果后台线程不仅仅是直接从磁盘加载映像,那么添加内存和/或磁盘缓存也是有益的, 以下是内存缓存的其他修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ImageDetailActivity extends FragmentActivity {
...
private LruCache<String, Bitmap> mMemoryCache;

@Override
public void onCreate(Bundle savedInstanceState) {
...
// 根据“使用内存缓存”部分初始化LruCache
}

public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);

final Bitmap bitmap = mMemoryCache.get(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}

... // 包括使用内存缓存部分更新的BitmapWorkerTask
}

将所有这些部分放在一起可以为您提供响应式ViewPager实现,同时将图像加载延迟降至最低,并且能够根据需要对图像进行尽可能多的背景处理。

5.2 将Bitmap加载到GridView中

网格列表构建块对于显示图像数据集很有用,并且可以使用GridView组件实现,其中许多图像可以在任何时间在屏幕上显示,并且如果用户向上或向下滚动,还需要准备好更多图像。 实现此类控件时,必须确保UI保持流畅,内存使用仍然受到控制,并且正确处理并发(由于GridView回收其子视图的方式)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private ImageAdapter mAdapter;

//用于支持GridView适配器的静态数据集
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};


public ImageGridFragment() {}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new ImageAdapter(getActivity());
}

@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this);
return v;
}

@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
startActivity(i);
}

private class ImageAdapter extends BaseAdapter {
private final Context mContext;

public ImageAdapter(Context context) {
super();
mContext = context;
}

@Override
public int getCount() {
return imageResIds.length;
}

@Override
public Object getItem(int position) {
return imageResIds[position];
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup container) {
ImageView imageView;
if (convertView == null) { // 如果它没有被回收,则初始化一些属性
imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} else {
imageView = (ImageView) convertView;
}
imageView.setImageResource(imageResIds[position]);
return imageView;
}
}
}

再一次,这个实现的问题是图像是在UI线程中设置的。 虽然这可能适用于小而简单的图像(由于系统资源加载和缓存),但如果需要进行任何其他处理,UI将停止运行。
可以在此处实现前一节中的相同异步处理和缓存方法,这是解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
...

private class ImageAdapter extends BaseAdapter {
...

@Override
public View getView(int position, View convertView, ViewGroup container) {
...
loadBitmap(imageResIds[position], imageView)
return imageView;
}
}

public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}

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

public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}

public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}

public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData != data) {
// 取消上一个任务
bitmapWorkerTask.cancel(true);
} else {
// 同样的工作已在进行中
return false;
}
}
// 没有与ImageView关联的任务,或者已取消现有任务
return true;
}

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}

注意:同样的代码也可以很容易地适用于ListView。

Newer Post

如何在ubuntu搭建Android开发环境

要进行程序的开发前提是:搭建开发环境下面我就介绍一下如何搭建Android开发环境. 一 安装jdkAndroid开发使用的语言是java,既然要使用java这就要牵扯到jdk了.步骤:1.1 下载jdk.官网地址:http://www.oracle.com/technetwork/java/jav …

继续阅读
Older Post

初识OpenGL ES2.0

概述要在Android应用中使用OpenGL ES绘制图形就必须为它们创建一个视图容器,使用该容器这边就需要引入GLSurfaceView和GLSurfaceView.Renderer.GLSurfaceView是使用OpenGL和GLSurfaceView.Renderer绘制的图形的视图容器,用 …

继续阅读