图片高效加载(二) 图片的异步加载
图片的异步加载是利用AsynTask类对图像进行后台加载完成后再给ImageView,先转载一篇前人的较好的总结后面再添加一些自己的见解和贴上完整的实现demo。
前面的转自:https://my.oschina.net/rengwuxian/blog/183802
摘要: 有没有过这种体验:你在Android手机上打开了一个带有含图片的ListView的页面,用手猛地一划,就见那ListView嘎嘎地卡,仿佛每一个新的Item都是顶着阻力蹦出来的一样?看完这篇文章,你将学会怎样避免这种情况的发生。
为什么要在后台加载Bitmap?
有没有过这种体验:你在Android手机上打开了一个带有含图片的ListView的页面,用手猛地一划,就见那ListView嘎嘎地卡,仿佛每一个新的Item都是顶着阻力蹦出来的一样?看完这篇文章,你将学会怎样避免这种情况的发生。
在Android中,使用BitmapFactory.decodeResource(), BitmapFactory.decodeStream() 等方法可以把图片加载到Bitmap中。但由于这些方法是耗时的,所以多数情况下,这些方法应该放在非UI线程中,否则将有可能导致界面的卡顿,甚至是触发ANR。
一般情况下,网络图片的加载必须放在后台线程中;而本地图片就可以根据实际情况自行决定了,如果图片不多不大的话,也可以在UI线程中操作来图个方便。至于谷歌官方的说法,是只要是从硬盘或者从网络加载Bitmap,统统不应该在主线程中进行。
基础操作:使用AsyncTask,其中decodeSampledBitmapFromResource函数是上一篇中讲到的将图像缩小后加载到控件节约内存和提高加载效率:http://www.cnblogs.com/bokeofzp/p/6064767.html
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); } } } }
以上代码摘自Android官方文档,是一个后台加载Bitmap并在加载完成后自动将Bitmap设置到ImageView的AsyncTask的实现。有了这个AsyncTask之后,异步加载Bitmap只需要下面的简单代码:
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); }
然后,一句loadBitmap(R.id.my_image, mImageView) 就能实现本地图片的异步加载了。
并发操作:在ListView和GridView中进行后台加载
在实际中,影响性能的往往是ListView和GridView这种包含大量图片的控件。在滑动过程中,大量的新图片在短时间内一起被加载,对于没有进行任何优化的程序,卡顿现象必然会随之而来。通过使用后台加载Bitmap的方式,这种问题将被有效解决。具体怎么做,我们来看看谷歌推荐的方法。
首先创建一个实现了Drawable接口的类,用来存储AsyncTask的引用。在本例中,选择了继承BitmapDrawable,用来给ImageView设置一个预留的占位图,这个占位图用于在AsyncTask执行完毕之前的显示。
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(); } }
接下来,和上面类似,依然是使用一个loadBitmap()方法来实现对图片的异步加载。不同的是,要在启动AsyncTask之前,把AsyncTask传给AsyncDrawable,并且使用AsyncDrawable为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); } }
然后在Adapter的getView()方法中调用loadBitmap()方法,就可以为每个Item中的ImageView进行图片的动态加载了。
loadBitmap()方法的代码中有两个地方需要注意:第一,cancelPotentialWork()这个方法,它的作用是进行两项检查:首先检查当前是否已经有一个AsyncTask正在为这个ImageView加载图片,如果没有就直接返回true。如果有,再检查这个Task正在加载的资源是否与自己正要进行加载的资源相同,如果相同,那就没有必要再进行多一次的加载了,直接返回false;而如果不同(为什么会不同?文章最后会有解释),就取消掉这个正在进行的任务,并返回true。第二个需要注意的是,本例中的 BitmapWorkerTask 实际上和上例是有所不同的。这两点我们分开说,首先我们看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) { // 取消之前的任务 bitmapWorkerTask.cancel(true); } else { // 相同任务已经存在,直接返回false,不再进行重复的加载 return false; } } // 没有Task和ImageView进行绑定,或者Task由于加载资源不同而被取消,返回true return true; }
在cancelPotentialWork()的代码中,首先使用getBitmapWorkerTask()方法获取到与ImageView相关联的Task,然后进行上面所说的判断。好,我们接着来看这个getBitmapWorkerTask()是怎么写的:
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; }
好的,cancelPotentialWork()方法分析完了,我们回到刚才提到的第二个点:BitmapWorkerTask类的不同。这个类的改动在于onPostExecute()方法,具体请看下面代码:
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中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。
好了以上是大神写的一篇文章,将如何异步加载图片写的非常透彻,但有些地方与我的理解有些偏差:
1、listView中的view为了节约资源是只创建了屏幕显示的个数的view,然而并非在一个view消失时候并非调用getItem,Imageview第二次为自己使用Task加载Bitmap的,通过查看源码发现,listView是AdapterView的子类,在AdapterView中Adapter接口是一个成员,用户利用Adapter的接口方法来完成AdapterView的一些函数功能的,如调用:
listView.getItemAtPosition(int position)
返回的就是getItem()返回的值,而若返回空并不影响listView的显示功能,getItem()只是AdapterView的成员Adapter抛出的方法,而AdapterView利用这些未实现的方法来实现自己预先的功能。具体listView如何将getView返回的view笔者看源码牵扯太多没看下去。
2、就是最后一块代码里,若cancel(true)了 onPostExecute(Bitmap bitmap)这个函数是不会调用的。可以将其改进:一些解释看注视吧。
protected void onPostExecute(Bitmap bitmap) { //如果cancel了就不会执行此函数了,所以觉得没必要这一步判断 // if(isCancelled()) // bitmap = null; final ImageView imageView = weakReference.get(); if(imageView!=null&&bitmap!=null)//将imageView!=null提当上面来应该可以加快判断效率,实验weakReference不为空时ImageView可能为空 { final ImageTask bitmapWorkerTask = getBitmapWorkerTask(imageView);//如快速滑动listview,View的图片未加载完成就会让其加载另外一张,此时imageview为同一个而任务AsynTask不同,且此时ImageView的drawable的绑定已经换了,asynTask也换了 //这里通过getBitmapWorkerTask(imageView)永远获取的是最新的asynTask if (this == bitmapWorkerTask) { imageView.setImageBitmap(bitmap); } } super.onPostExecute(bitmap); }
好了指出了一些瑕疵,但仍是值得我们学习的好文章。下面给出我实现出来代码,其中将drawable类写成了asyntask的静态内部类了,当我们需要异步加载图片的时候只需要调用这个类就可以完成啦。
package com.example.user.imagecashdemo; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.widget.ImageView; import java.lang.ref.WeakReference; /** * Created by user on 2016/11/15. */ public class ImageTask extends AsyncTask <Integer , Void , Bitmap>{ private WeakReference <ImageView> weakReference; private Context context; private Bitmap defaultImage; public int data; public ImageTask(ImageView view,Context context ,Bitmap defaultImage) { this.defaultImage = defaultImage; this.weakReference = new WeakReference(view); this.context = context; } @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; Bitmap bitmap = BitmapFactory.decodeResource(context.getResources() , data); return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { //如果cancel了就不会执行此函数了,所以觉得没必要这一步判断 // if(isCancelled()) // bitmap = null; final ImageView imageView = weakReference.get(); if(imageView!=null&&bitmap!=null)//将imageView!=null提当上面来应该可以加快判断效率,实验weakReference不为空时ImageView可能为空 { final ImageTask bitmapWorkerTask = getBitmapWorkerTask(imageView);//如快速滑动listview,View的图片未加载完成就会让其加载另外一张,此时imageview为同一个而任务AsynTask不同,且此时ImageView的drawable的绑定已经换了,asynTask也换了 //这里通过getBitmapWorkerTask(imageView)永远获取的是最新的asynTask if (this == bitmapWorkerTask) { imageView.setImageBitmap(bitmap); } } super.onPostExecute(bitmap); } static class AsyncDrawable extends BitmapDrawable { private final WeakReference<ImageTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, ImageTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<ImageTask>(bitmapWorkerTask); } public ImageTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } } public static boolean cancelPotentialWork(int data, ImageView imageView) { final ImageTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; if (bitmapData != data) { // 取消之前的任务 bitmapWorkerTask.cancel(true); } else { // 相同任务已经存在,直接返回false,不再进行重复的加载 return false; } } // 没有Task和ImageView进行绑定,或者Task由于加载资源不同而被取消,返回true return true; } private static ImageTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { //通过imageView的drawable反过来获取与之对应的AsynTask final Drawable drawable = imageView.getDrawable(); if (drawable instanceof ImageTask.AsyncDrawable) { final ImageTask.AsyncDrawable asyncDrawable = (ImageTask.AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; } public void loadImage(int data ,ImageView imageView) { if(cancelPotentialWork(data ,imageView)) { ImageTask.AsyncDrawable asyncDrawable= new ImageTask.AsyncDrawable(context.getResources() , defaultImage ,this);//defaultBitmap默认的一张图所以只需要加载一次到内存即可 imageView.setImageDrawable(asyncDrawable); this.execute(data); } } }
下面外面只需要在Adapter的getView里创建ImageTask对象,然后loadImage方法就可以了
package com.example.user.imagecashdemo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; public class MainActivity extends AppCompatActivity { private ListView list; private Bitmap defaultBitmap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); list = (ListView) findViewById(R.id.list); list.setAdapter(new MyAdapter()); defaultBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fy_default); } class MyAdapter extends BaseAdapter { int number = 30; @Override public int getCount() { return number; } @Override public Object getItem(int position) { return R.drawable.fy; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ImageView view = new ImageView(MainActivity.this); ImageTask imageTask = new ImageTask(view , MainActivity.this , defaultBitmap); imageTask.loadImage(R.drawable.fy ,view); return view; } } }