android - 图片加载&缓存&管理(文档学习)

发表于:,更新于:,By Sally
大纲
  1. 1. 高效的加载大的位图
    1. 1.1. 读取位图的规模(大小)和类型 read bitmap dimensions and type
    2. 1.2. 将按比例缩小的版本加载到内存 load a scaled down version into memory
  2. 2. 在非ui线程处理位图 processing bitmaps off the ui thread
    1. 2.1. 使用异步任务 use an AsyncTask
    2. 2.2. 处理并发 handle concurrency
  3. 3. 缓存位图 caching bitmaps
    1. 3.1. 内存缓存 use a memory cache
    2. 3.2. 磁盘缓存 use a disk cache
  4. 4. 图片内存管理 managing bitmap memory
  5. 5. 在ui中展示位图 displaying bitmaps in your ui
    1. 5.1. 加载位图到viewpager的实现 load bitmaps into a ViewPager implementation
    2. 5.2. 加载图片到gridview的实现 load bitmaps into a GridView implementation

高效的加载大的位图

读取位图的规模(大小)和类型 read bitmap dimensions and type

decodeByteArray(), decodeFile(), decodeResources()

  • BitmapFactory类提供了一些解码方法(decodeByteArray(), decodeFile(), decodeResource(), etc.)用于创建各种来源的位图。

基于图片数据的来源选择最合适的解码方法。这些方法在构建位图时企图分配内存,因此很容易产生OutOfMemory异常。每种类型的解码方法

都有额外的特征,允许你通过BitmapFactory.Options类指定解码选项。解码时设置inJustDecodeBounds属性的值为true可以避免内存分配。

此时不会返回位图对象,但可以得到outWidthoutHeightoutMimeType。这个技术允许你在构建位图之前能读取到图片数据的尺寸和类型。

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

为了避免java.lang.OutOfMemory异常,在解码图片时首先检查图片的大小,除非你绝对的(absolutely)信任来源为你提供的可预见(predictably)大小的图片数据,舒适的(comfortably)适合在可用的内存。

将按比例缩小的版本加载到内存 load a scaled down version into memory

现在知道了图片的大小,它们可以用来决定是将完整的图片加载到内存还是将子样品版本加载到内存。有一些考虑的因素(factors)

  • 估算(estimated)加载完整图片到内存的内存使用

  • 基于你的应用中任何其他内存需求,加载这个图片你愿意提供多少内存

  • 图片要加载到的imageView或者其他ui组件的尺寸

  • 当前设备的屏幕尺寸和密度

例如:如果图片最终(eventually)在一个imageView中展示一个128x96像素的缩略图,那么将1024x768像素的图片加载到内存中的做法是不值得的。

  • BitmapFactory.Options对象中将inSampleSize设置为true,可以使解码器(decoder)对图片做二次抽样,将较小的版本加载到内存。

例如:将分辨率为2048x1536的图片,使用inSampleSize=4进行解码,生成的位图大约(approximately)是512x384。将它加载到内存需要0.75MB而不是完整图片的12MB。

这里有个方法计算样本尺寸的值,是基于目标宽高的2的幂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 官方文档的方法
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原始图片的宽和高
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;

if(height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}

return inSampleSize;
}

or

1
2
3
4
5
6
7
8
9
10
11
12
// 网上经常看到的方法
public static int calculateInSampleSize(BitmapFactory.Options options, int reqHeight, int reqWidth) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if(height > reqHeight || width > reqWidth) {
final int heightRatio = Math.round((float)height / (float)reqHeight);
final int widthRatio = Math.round((float)width / (float)reqWidth);
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}

使用这个方法,首先设置inJustDecodeBoundstrue进行解码,传递这个选项,然后在通过新的inSampleSize值并设置inJustDecodeBounds为false进行解码。

1
2
3
4
5
6
7
8
9
10
public static Bitmap decodeSampledBitmapFromResources(Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resid, options);

options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

这个方法使得加载一个任意大尺寸的位图到imageView并且显示为100x100像素的缩略图变的容易。

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

在非ui线程处理位图 processing bitmaps off the ui thread

  • BitmapFactory.decode*方法,如果资源数据来自磁盘或者网络时,这些方法不应该在主(ui)线程执行。这些数据需要加载的时间是不可预知(unpredictable)的,主要依赖的几个因素(读取磁盘or网络的速度,图片的大小,cpu的速度),如果其中一个任务阻塞了ui线程,系统将认为你的应用程序无响应,用户也会选择关闭该应用。

使用异步任务 use an AsyncTask

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
26
27
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;

public BitmapWorkerTask(ImageView imageView) {
// 使用虚引用确保iamgeview可以被GC(garbage collected)回收
imageViewReference = new WeakReference<ImageView>(imageView);
}

// decode image in background
@override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResources(getResources(), data, 100, 100);
}

// once complete, see if imageView is still around and set 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可以被GC回收。当任务结束时不确保(guarantee)ImageView依然存在,所以你必须在onPostExecute()方法中检查引用。ImageView也许不再存在,例如,用任务结束之前户已经导航离开活动or设备配置发生了变化(屏幕旋转等)。

开启一步加载位图,简单的创建任务并执行它即可:

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

处理并发 handle concurrency

普通的视图组件如ListViewGridView在使用上面演示的AsyncTask方法时会引入了另一个问题,为了更有效的使用内存,当用户滑动时,这些组件的子view会循环使用。如果每个子view都触发了AsyncTask,将不确保任务结束之前,与之相关联的view没有被回收并且用于别的子view。此外,也不能保证异步任务的开始顺序,就是其结束顺序。

Multithreading for Performance这个博客进一步的讨论了并发处理,并且提供了一个解决方案:imageView存储一个引用到最近的AsyncTask中,使得任务完成后更迟的被检出。下面的例子:使用类似的方法,将之前部分的AsyncTask改变成类似的模式。

解决方案:and offers a solution where the ImageVeiw stores a reference to the most recent AsyncTask which can later be checked when the task completes(这句不知道理解对了没)

创建一个Drawable子类来存储关联工作任务的引用。这样,当任务完成时,使用BitmapDrawable在imageView上显示一个占位图。

1
2
3
4
5
6
7
8
9
10
11
12
static class AsyncDrawable extends BitmapDrawable {
privite 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
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()方法取消之前的任务。在少数情况下,新任务的数据与已存在任务的数据匹配并且没有什么进一步的需求发生。

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尚未设置or与新的data不同 if bitmapData is not yet set or it differs from the new data
if(bitmapData == 0 || bitmapData != data) {
// 取消之前的任务 cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// 同样的工作已经在进行 the same work is already in progress
return false;
}
}
// no task associated with the imageView, or an existing task was cancelled
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)asyncDrawable;
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
class BitmapWorkTask 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);
}
}
}
}

这个实现使用与ListViewGridView以及其他会回收其子view的组件。通常在你需要为ImageView设置图片时简单的调用loadBitmap


缓存位图 caching bitmaps

加载一张图片到ui界面是比较简单的,如果你一次需要加载大量的图片那事情就变的很复杂了。在大多数情况下(像ListView,GridView,ViewPager这样的组件),快速滚动到屏幕上的图像的总和,结合形成屏幕上的图像本质上是无限的。

内存的使用是受组件控制的,如子view离开屏幕时就会将其回收。假如你没有保持任何持久的引用,GC(garbage collector)也会回收你加载的图片。这样是极好的,但是为了保持流畅性和快速的加载ui,你应该避免任何时候让它们重返屏幕这一频繁的过程。这通常要借助内存and磁盘缓存来处理,使得组件快速的重新加载处理过的图片。

内存缓存 use a memory cache

内存缓存是以提供宝贵的应用内存为代价来快速访问位图的。

LruCache类特别适合用来缓存图片,使用强引用LinkedHashMap保持最近的引用对象,在缓存超过指定的大小之前益处最近最少使用的成员。

注意:在过去,比较受欢迎的内存缓存实现是软引用(SoftReference)虚引用(WeakReference)图片缓存,但是不推荐这样做。从Android2.3开始,GC收集软/虚引用更积极,这使得软/虚引用缓存变的无效。另外,在Android3.0以前,支持图片数据存储在native内存,它使得数据以一种不可预见的方式释放。这可能导致应用程序短暂的超出内存限制乃至崩溃。

为了选择一个合适的LruCache大小,一下列出几点需要考虑的因素:

  • 你的活动/应用之外的内存密集型如何。how memory intensive is the rest of your activity and/or application?

  • 屏幕一次显示多少图片?需要准备多少图片显示在屏幕上?

  • 设备的屏幕尺寸和密度?比起Nexus S这样的设备,像Galaxy Nexus这样的高密度设备需要更大的缓存去加载相同数量的图片。

  • 每个位图的大小和配置,以及其需要多少内存?

  • 图片是否被频繁的访问?是否一些图片的访问频率高于另外一些?如果是,也许你应该保持一些常驻内存,或者为不同的图片组使用不同的LruCache对象,即维护多个LruCache对象。

  • 你能保证质量与数量之间的平衡吗?有时存储大量的低质量图片是比较好的,然后在后台任务默默的加载高质量的版本。

没有具体的大小or准则适合所有的应用,由你来分析你的使用并想出一个合适的解决方案。缓存太小不仅没有好处而且会导致额外的开销,缓存太大会再次导致OutOfMemory异常或给你应用的剩余部分留下极小的内存。

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
// 设置LruCache
private LruCache<String, Bitmap> mMemoryCache;

@override
protected void onCreate(Bundle savedInstanceState) {
...
// 得到最大的可用虚拟内存,超过这个值会抛出oom异常。
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);
}

当将bitmap加载到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) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
}

BitmapWorkerTask也需要添加一个保存到缓存的入口:

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

磁盘缓存 use a disk cache

内存缓存很实用,它可以加速访问最近浏览过的图片,然而你不能依赖于在缓存中可用的图片。像GridView这样的拥有大数据集的组件可以很容易的装满整个内存。你的应用可能因为打电话这样的任务二中断,当它退居后台的时候可能进程会被杀死,缓存会被销毁。一旦用户重新进入,你的应用会重新处理每一张图片。

在这种情况下,可以使用磁盘缓存来持久化处理过的位图,当内存缓存中的图片不再可用时帮助减少图片加载时间。当然,从磁盘拉取图片比从内存拉去要慢,因此应该在后台线程执行,同样的,磁盘的读取时间是不可预知的。

注意:如果图片的访问比较频繁的话,ContentProvider也许更合适。就像图像库应用,画廊应用。

示例代码使用DiskLruCache类实现,这里是源码

这里修改了示例代码,除了已经存在的内存缓存还添加了磁盘缓存。

1
// code

初始化磁盘高速缓存需要磁盘操作,因此不应该发生在主线程。既然如此,这就意味着有这样一种可能:缓存的访问在其初始化之前。为了解决此事,在上面的实现中,用一个锁对象确保了应用程序不能从磁盘缓存读取数据直到缓存被初始化。

内存缓存在ui线程被检出,磁盘缓存在后台线程中被检出。磁盘操作永远不应该发生在ui线程。当图片被处理完,最终图片被添加到内存缓存and磁盘缓存中以便将来使用。

图片内存管理 managing bitmap memory

除了Caching Bitmaps章节描述的步骤,还有一些做法可以促进垃圾回收和图片复用。这些推荐依赖于目标安卓版本。

为本节课奠定基础,这里展示了安卓图片内存管理是怎么进化的:

  • 在安卓2.2及以前,当发生垃圾回收时,你应用的线程就会停止。从而导致滞后影响性能。安卓2.3添加了并发垃圾回收机制,这意味着图片不再被引用后尽可能快的被回收使用。

  • 在安卓2.3.3及以前,支持将图片的像素数据存在native内存中,与位图本身分离,图片本身存储在虚拟机的堆内存中。存储在native内存中的像素数据不是以一种可预见的方法释放,这可能导致应用在短时间内超过内存限制并崩溃。在安卓3.0,图片的像素数据与图片关联,一起存在虚拟堆内存中。

  • TODO

在ui中展示位图 displaying bitmaps in your ui

加载位图到viewpager的实现 load bitmaps into a ViewPager implementation

  • ViewPagerPagerAdapterFragmentPagerAdapterFragmentStatePagerAdapter(能够自动的销毁或者保存viewpager中的fragments的状态,当它们离开屏幕的时候)

当图片数量不多,并且确信不会超过内存限制时,建议使用PagerAdapter or FragmentPagerAdapter

这里一定要看官方给出的示例代码,显示图片时,为了更好的提高性能,就不要在ui线程加载图片到imageview,在后台线程执行加载操作

加载图片到gridview的实现 load bitmaps into a GridView implementation

当显示图片集合时,在任一时刻显示多张图片到屏幕上,当用户滑动屏幕时需要准备多张图片显示时,使用gridview是很有用。当实现这种类型的控制时,你必须确保用户界面的流畅性,内存使用在控制之内,正确的处理并发(在gridview循环复用子view时)。

同样看官方示例,不要在ui中加载图片,在后台线程加载