本章节讨论的是安卓图片加载的基础部分,主要依据谷歌官方培训教程中的代码和方法,使用比较广泛的图片加载框架,很多都是要依据安卓的基础方法,在此基础上进行封装,如果直接看封装过得框架,以来难度会有点大,二来在基础方法不是太了解的情况下看大的完整框架会有些耗费时间,所以还是让我们来一起分析一下如何用安卓官方给出的基础方法来封装一个简单图像加载框架。
因为谷歌在中国建立了针对开发者的开发文档服务器,各位安卓开发人员想要学习谷歌官方的安卓文档已经不需要再翻墙了。虽然部分文档还是英文版的,但是最起码我们可以快速方便的打开了,地址:谷歌官方安卓开发文档
以下相关源代码和理论分析主要来自于:安卓图像之高效显示Bitmap
一、高效加载大图:首先,在应用开发中在显示图片时,往往很多原图是比较大的,而手机设备是不需要在内存中加载一个原图大小的大图的,这就需要加载一个符合手机需要大小的图片,以此来减少内存加载图片时过多的内存消耗。接下来用一段代码进入分析:
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;
BitmapFactory提供了一些解码(decode)的方法(decodeByteArray(), decodeFile(), decodeResource()等),用来从不同的资源中创建一个Bitmap。这些方法在构造位图的时候会尝试分配内存,因此OutOfMemory的异常往往在此时发生。
该段代码使用了BitmapFactory.Options来获取图片的宽高和类型,而当options.inJustDecodeBounds设置为true时,内存是不会加载该图片的,即返回一个为null的bitmap引用,但是会获取到图片的尺寸和类型,该数据可供后续的缩放使用。
接下来需要根据原图的大小和控件需要显示的大小来设置内存中应加载的图片的尺寸大小,当计算完毕以后再将图片加载进手机内存中,即可减少加载图片时在内存中的资源占用,下面是计算控件所需图片大小的代码:
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;
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
该方法为设置BitmapFactory.Options 中inSampleSize 的值,如图片的原大小为2048x1536,当inSampleSize 为4时,将会得到一个约512x384大小的Bitmap。加载这张缩小的图片仅仅使用大概0.75MB的内存,如果是加载完整尺寸的图片,那么大概需要花费12MB(前提都是Bitmap的配置是 ARGB_8888)。
设置inSampleSize为2的幂是因为解码器最终还是会对非2的幂的数进行向下处理,获取到最靠近2的幂的数。详情参考inSampleSize的文档。下面为加载任意大小图片并设置为说需的大小的代码:
public static Bitmap decodeSampledBitmapFromResource(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);
}
首先需要设置 inJustDecodeBounds 为 true, 把options的值传递过来,但不加载图片,不消耗内存,然后设置 inSampleSize 的值。此时设置 inJustDecodeBounds 为 false,之后重新调用相关的解码方法,指定大小的图片便加载进入了内存当中。如:
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
到这里基本的加载相关已经完成了,但依然有很多问题,比如图片加载是不应该在主线程完成的。
二、非UI线程处理Bitmap: 图片的加载是耗时操作,尤其是网络加载,需要在非UI线程中处理,此处使用AsyncTask演示在后台线程中处理Bitmap以及处理并发(concurrency)的问题。官网介绍简单情况比较青睐于用AsyncTask,复杂情况就不建议使用了。
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
imageViewReference = new WeakReference(imageView);}
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);} } } }
以上为对异步加载的代码封装,让加载操作在后台运行,对以上代码的调用方法如下:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId); }
异步加载的简单封装与其他情况下使用AsyncTask并无太大差别,在这里不再进行过多的介绍。下面将分析图像的并发处理。
此处的问题为在ListView、GridView、RecyclerView中我们为每一个item都添加了一个下载的task,在用户滑动控件的时候,会有一些item被回收,然而,task并没有消失也并不知道哪个Item被回收了哪个是要先加载的。接下来的这个类就是用来解决此问题的,AsyncDrawable类也是理解此小节的一个关键类。
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,BitmapWorkerTask bitmapWorkerTask)
{super(res, bitmap);
bitmapWorkerTaskReference =new WeakReference(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
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);
}
}
结合loadBitmap来理解代码意图:在loadBitmap中创建了AsyncDrawable实例, AsyncDrawable(getResources(), mPlaceHolderBitmap, task);第一个参数忽略,第二个参数为默认bitmap图片,第三个为需要异步加载的图片如来自网络,第一第二个参数是用来生成AsyncDrawable的默认图片的,它将会和目标imageView(item中的图片)进行绑定并显示一个默认的图片,同时执行task.execute(resId)代码,后台将加载需要加载的图片,在图片加载完成后将真正需要显示的图片显示在imageView上。整个过程中AsyncDrawable起到了关键的绑定作用,在loadBitmap中使用到了cancelPotentialWork方法,该方法是检查是否有另一个正在执行的任务与该ImageView关联了起来,如果的确是这样,它通过执行cancel()方法来取消另一个任务,以此来保证执行的是最新最近的task,而不是某一个已经过时的task。
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData == 0 || bitmapData != data) {
bitmapWorkerTask.cancel(true);
} else {
return false;
}
}
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;
}
getBitmapWorkerTask()被用作检索AsyncTask是否已经被分配到指定的ImageView
最后是更新BitmapWorkerTask的onPostExecute() 方法
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);
}
}
}
在onPostExecute中再次检查imageview是否被回收,task是否被取消,task是否一致如果一致且imageview没有被回收,然后再显示图像。以上就是并发加载的解决方案了,主要解决listview等空间在执行循环加载和滚动时的并发图像加载问题。
三、缓存Bitmap:解决完了加载问题,接下来要解决缓存问题,因为不可能所有的图片无论是否可重复利用均每次显示都重新加载。使用内存缓存与磁盘缓存可以提高响应速度与UI流畅度。
内存缓存以花费宝贵的程序内存为前提来快速访问位图。LruCache类(在API Level 4的Support Library中也可以找到)特别适合用来缓存Bitmaps,它使用一个强引用(strong referenced)的LinkedHashMap保存最近引用的对象,并且在缓存超出设置大小的时候剔除(evict)最近最少使用到的对象。当加载Bitmap显示到ImageView 之前,会先从LruCache 中检查是否存在这个Bitmap。如果确实存在,它会立即被用来显示到ImageView上,如果没有找到,会触发一个后台线程去处理显示该Bitmap任务。
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 需要在doInBackground中把解析好的Bitmap添加到内存缓存中
内存缓存能够提高访问最近用过的Bitmap的速度,但是我们无法保证最近访问过的Bitmap都能够保存在缓存中。像类似GridView等需要大量数据填充的控件很容易就会用尽整个内存缓存。另外,我们的应用可能会被类似打电话等行为而暂停并退到后台,因为后台应用可能会被杀死,那么内存缓存就会被销毁,里面的Bitmap也就不存在了。一旦用户恢复应用的状态,那么应用就需要重新处理那些图片。
磁盘缓存可以用来保存那些已经处理过的Bitmap,它还可以减少那些不再内存缓存中的Bitmap的加载次数。当然从磁盘读取图片会比从内存要慢,而且由于磁盘读取操作时间是不可预期的,读取操作需要在后台线程中处理。内存缓存的检查是可以在UI线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在UI线程中发生。当图片处理完成后,Bitmap需要添加到内存缓存与磁盘缓存中,方便之后的使用。该模块可结合网上所介绍的Bitmap三级缓存的一些知识来具体的了解图片的缓存机制。
以上只是在一些很基础的android层面进行了图片加载的分析,图片加载框架很多都会在此基础上进行封装,理解基础的图片加载对理解封装了更多方法的框架将会有不小的帮助。