本讲义分为三部分,较为详细的介绍了Android平台下图片显示,加载等操作的处理原则与办法,以供大家共同学习探讨!
前提与解释:
安卓平台作为一款移动端的应用操作平台,其内存容量是十分有限的,内存资源是十分珍贵的,是无法与传统的桌面平台相比的,因此,在安卓平台下同样的图片操作与处理都要十分谨慎,否则你的程序可以迅速地消耗可用内存的预算,最终由于OutOfMemory导致程序崩溃掉。以下有三个原因说明了我们为什么要谨慎:
(1)安卓平台下对应用可使用的系统资源都做出了限制,标准安卓系统下,一个应用程序可用的最大内存为16M,一些第三方ROM
可能会上调这一限制,但是作为应用来说一定要控制自己的内存用量,这并不是可以无限制使用的。
(2)一张高分辨图片的内容耗用量是惊人的,例如,Galaxy Nexus的摄像头在拍摄2592X1936像素(5百万像素)。如果位图使用
的是配置ARGB_8888
(默认的Android 2.3开始),那么此图像加载到内存占用约19MB的内存(2592 * 1936 * 4字节),直接就耗
尽了在某些设备上的每个应用程序的内存上限。
(3)安卓应用程序的一些控件经常需要几个位图一起加载。例如ListView,GridView,ViewPager等控件,并且在使用中还要快速
的滑动,要及时对图片进行更新与回收,更加增加了图片处理的难度。
解决办法:
一、如何去加载与显示大图:
在安卓这样内存有限的平台上,是没有必要按照原始尺寸把一张大图完全加载进来的,只需要加载与我们显示控件相匹配的尺寸就行,多了只会浪费我们宝贵的内存。因此在加载图片时,我们按照我们需要显示的大小对原始图片再采样就OK了。同时我们也可以根据我们所能够使用的内存大小来对图片进行解码,按照我们能够承受的尺寸与分辨率来处理,保证图片所占用的内存在我们可支配的范围之内,也就避免了OOM的问题。
第一步:我们需要获取原始图片的相关尺寸,分辨率等数据
可以利用BitmapFactory的Options来达到这一目的,解码图片时可以先把inJustDecodeBounds的值设为true,这样并没有真正的去解码图片,不占用内存,但是我们却可以在这个过程中获取图片的宽,高以及类型,代码如下:
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;
第二步:获取原始的图片尺寸后,根据目标计算缩放比例系数
<span style="font-size:14px;">public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// Calculate ratios of height and width to requested height and width
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// Choose the smallest ratio as inSampleSize value, this will guarantee
// a final image with both dimensions larger than or equal to the
// requested height and width.
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}</span>
官方文档中说:inSampleSize这个属性最好是2的倍数,这样处理更快,效率更高。。。
第三步:开始对图片进行解码
<span style="font-size:14px;">public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}</span>
注意,真正解码时需要把inJustDecodeBounds属性重置为false,这样就可以把一张十分巨大的图轻松显示在一个100x100的ImageView中了
二、用ListView,GridView等集合显示控件来高效的显示图片
实际的使用环境中,如果图片来源是SD卡或者网络,那那么加载图片的过程一定不要放在UI线程中,这样会严重的阻塞UI线程,出现ANR,程序就废了。因此我们首先要实现异步加载。
第一步:利用AsyncTask实现图片的异步加载
将decodeSampledBitmapFromResource方法放入Task的doInBackground中后台执行。不熟悉AsyncTask的同学可以学习AsyncTask的相关知识,这里不再过多介绍。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(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必要时候的垃圾回收。否则可能会出现内存泄露,同时,我们一定要在Task执行完毕后对ImageView的存在性进行判断,因为不能保证Task执行完毕后,ImageView还会存在。
下来我们按照下面的代码就可以使用这个Task了:
ListView与GridView这种多子视图的控件会出现两个问题,
第一,一个ListView会有众多的ChildView,为了更高效的利用内存,控件会自动回收掉被用户滑动过去,不在当前有显示的ChildView,如果每一个ChildView都开启一个Task去加载图片,这样就不能保证开启Task的ChildView在Task执行完毕后没有被回收掉(很有可能用户滑动到其他地方去了)。
第二,因为每张图片的处理时间是不同的,因此同样不能保证加载完成的次序与开始的次序一致。
下来开始着手解决这些问题,要让ImageView与Task形成一种绑定的关系。
先来创建一个特殊的Drawable,这个Drawable有两个功能,一个是与Task形成一种绑定的关系,另外也充当了ImageView的临时占位图像,该Drawable的代码如下:
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();
}
}
在该Drawable中通过弱引用能与对应的Task形成一种一一对应的捆绑关系。
我们可以这样使用它,在执行Task之前,先创建一个对应的Drawable,并把它当成将要呈现实际图片的ImageView占位图片,同时也与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);
}
}
当然,我们需要判断下ImageView之前是否已经绑定了,如果之前绑定过但与本次的图片不同,那我们就要按最新的需要从新绑定下,如果之前与现在的一致,则保持原状,不再从新绑定,代码中的cancelPotentialWork就是做这个工作的,其代码如下:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (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;
}
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;
}
最后,我们在Task的 onPostExecute函数中,把加载的图片更新到视图中去,在更新前我们需要检查下Task是否被取消,并且当前的Task是否是那个与ImageView关联的Task,一致则我们把图片更新到ImageView上去,代码如下:
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适配器的getView函数中调用上面的loadBitmap函数就OK了
三、使用Memory Cache来实现加载之前已经被加载但是释放了的图片
用户在使用ListView或GridView时,控件会自动把用户滑过的已不在当前显示区域的ChildView回收掉,当然也会把该子视图上的bitmap回收掉以释放内存,因此,为了保证一个流畅,快速的操作体验,我们应当避免反复的对同一张图片进行加载,比如说用户在往下看图的过程中又向上滑回去看图,这时对于已经上面已经加载过的图片我们就没有必要让它再加载一遍了,应该能很快的把图片显示出来,这里我们要使用缓存来达到这一目的。
内存缓存速度快,同时为了更加适应实际应用的场景,我们使用LruCache来达到按使用频率缓存的目的,把最近使用的加入缓存,较长时间不用的则会剔除掉释放出空间。
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
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);
}
那么我们在loadBitmap的时候就可以先检查下缓存中保存的是否有该图片,有则直接取出使用,不再进行加载。
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);
}
}
当然,我们也要在加载图片时及时的维护缓存,把刚使用到的图片add进缓存中去。
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;
}
...
}
在使用内存做缓存的基础上,我们还可以使用Disk控件做为缓存,构成一种二级缓存的结构,设想这种情况,如果App在使用的过程被突然来电打断,那么此时有可能就会引起系统内存的回收,当用户再次切换到App时,App就要进行次很明显的图片再次加载的过程。这个时候,我们就需要用到Disk了,因为足够持久。
下面是是原来的基础上增加使用Disk Cache 的例子:
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) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
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; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(imageKey);
if (bitmap == null) { // Not found in disk cache
// Process as normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}