在Android应用程序的开发中,从网络或者服务器上取得图片,往往需要花费一定的时间,占用一定的用户带宽,当页面有大量的图片时,如果不采取延迟加载的方法,则客户端需要等到所有的图片都获取之后,才可以呈现完整界面,这就可能导致界面反应不流畅,影响用户体验。
图片延迟加载的原理其实非常简单,有两种思路:
第一种思路是后台启动Thread下载图片,下载完成后,通过Message Handle的方式把控制权转让给UI Thread并绘制图片。此方法的优点是比较简单,缺点是不容易封装。
第二种思路是启动异步任务AsyncTask,通过doInBackground()方法获得图片资源之后,再通过onPostExecute()方法在UI Thread中绘制取好的图片。
以上两种方式都可以很好地处理图片的延迟加载。本文通过第一种方式来处理,对图片的延迟加载进行封装,并对性能进行如下优化:
1) 图片加载的线程优先级比UI低一级,这样可以优先处理UI任务。
2) 在内存、磁盘缓存下载的应用程序图片。读取图片的顺序为 内存 -> 磁盘 -> 网络,这样可以避免下载重复的图片,节省网络流量,并且提高响应速度和性能。
3) 本地缓存的图片可能由于某种原因过期,而与服务端不一致,比如服务端更新了图片资源,这个时候本地并不知道,从而导致了图片的不一致性,这也是采取缓存技术提高性能而导致的副作用。为了解决这个问题,可以引入时间戳的概念,当时间戳发生变化之后,重新从网络上获取图片并缓存。
以下将做简要的说明。
首先建立LazyImage对象,此对象包括了图片image,图片的网络资源链接url,以及图片所对应的时间戳(从服务端获得,如果没有时间戳的固定图片,也可以不用设置)。
package com.whyonly.core.bean;
import android.graphics.Bitmap;
public abstract class LazyImage {
private Bitmap image;
private String image_url;
private long timestamp;
public boolean hasLoadPhoto(){
return image==null ? false : true;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
this.hashCode();
}
public Bitmap getImage() {
return image;
}
public void setImage(Bitmap image) {
this.image = image;
}
public String getImage_url() {
return image_url;
}
public void setImage_url(String image_url) {
this.image_url = image_url;
}
public String toFileName(){ //convert the url + timestamp to file name to instore to local disk
String fileName = "";
if(image_url!=null && image_url.indexOf("/")!=-1 ){
fileName=image_url.substring(image_url.lastIndexOf("/")+1,image_url.length());
}
return fileName+"_"+timestamp;
}
}
建立一个下载任务
//Task for the queue
private class PhotoToLoad
{
public LazyImage lazyImage;
public ImageView imageView;
public boolean saveDisk;
public PhotoToLoad(LazyImage l, ImageView i, boolean s){
lazyImage=l;
imageView=i;
saveDisk = s;
}
}
以及下载队列
//stores list of photos to download
class PhotosQueue
{
private Stack<PhotoToLoad> photosToLoadStack=new Stack<PhotoToLoad>();
//removes all instances of this ImageView
public void clean(ImageView image)
{
for(int j=0 ;j<photosToLoadStack.size();){
if(photosToLoadStack.get(j).imageView==image)
photosToLoadStack.remove(j);
else
++j;
}
}
}
接着建立内存缓冲类:
class MemoryCache {
private HashMap<String, SoftReference<Bitmap>> cache=new HashMap<String, SoftReference<Bitmap>>();
public Bitmap get(String id){
if(!cache.containsKey(id))
return null;
SoftReference<Bitmap> ref=cache.get(id);
return ref.get();
}
public void put(String id, Bitmap bitmap){
cache.put(id, new SoftReference<Bitmap>(bitmap));
}
public void clear() {
cache.clear();
}
}
和本地磁盘缓冲类
class FileCache {
private File cacheDir;
public FileCache(Context context){
//Find the dir to save cached images
// if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"whyonly/cache");
// else
// cacheDir=context.getCacheDir();
Log.d("ImageLoader","cacheDir:"+cacheDir);
if(!cacheDir.exists())
cacheDir.mkdirs();
}
public File getFile(LazyImage lazyImage){
String filename= lazyImage.toFileName();
File f = new File(cacheDir, filename);
return f;
}
public void clear(){
File[] files=cacheDir.listFiles();
for(File f:files)
f.delete();
}
}
把图片资源显示到ImageView
//Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable
{
Bitmap bitmap;
ImageView imageView;
public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
public void run()
{
if(bitmap!=null)
imageView.setImageBitmap(bitmap);
else
imageView.setImageResource(defaultImageResId);
}
}
最后是通过线程来控制图片的下载和显示过程
class PhotosLoaderThread extends Thread {
public void run() {
try {
while(true)
{
//thread waits until there are any images to load in the queue
if(photosQueue.photosToLoadStack.size()==0)
synchronized(photosQueue.photosToLoadStack){
photosQueue.photosToLoadStack.wait();
}
if(photosQueue.photosToLoadStack.size()!=0)
{
PhotoToLoad photoToLoad;
synchronized(photosQueue.photosToLoadStack){
photoToLoad=photosQueue.photosToLoadStack.pop();
}
Bitmap bmp=getBitmap(photoToLoad.lazyImage,photoToLoad.saveDisk);
memoryCache.put(photoToLoad.lazyImage.toFileName(), bmp);
String tag=imageViews.get(photoToLoad.imageView);
if(tag!=null && tag.equals(photoToLoad.lazyImage.toFileName())){
BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
Activity a=(Activity)photoToLoad.imageView.getContext();
a.runOnUiThread(bd);
}
}
if(Thread.interrupted())
break;
}
} catch (InterruptedException e) {
//allow thread to exit
}
}
}
完整的图片加载类如下:
package com.whyonly.core.wrapper;
import java.io.File;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.WeakHashMap;
import com.whyonly.core.bean.LazyImage;
import com.whyonly.core.util.ImageUtil;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.widget.ImageView;
public class ImageLoader {
private static final String TAG = "ImageLoader";
private MemoryCache memoryCache;
private FileCache fileCache;
private Map<ImageView, String> imageViews;
private PhotosLoaderThread photoLoaderThread;
private PhotosQueue photosQueue;
private int defaultImageResId;
//private Context context;
public ImageLoader(Context context,int defaultImageResId){
//Make the background thead low priority. So it will not affect the UI performance
photoLoaderThread=new PhotosLoaderThread();
photoLoaderThread.setPriority(Thread.NORM_PRIORITY-1);
memoryCache=new MemoryCache();
fileCache=new FileCache(context);
imageViews=Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
photosQueue=new PhotosQueue();
//this.context = context;
this.defaultImageResId = defaultImageResId;
}
public void displayImage(LazyImage lazyImage, ImageView imageView){
displayImage(lazyImage,imageView,true);
}
public void displayImage(LazyImage lazyImage, ImageView imageView,boolean saveDisk)
{
imageViews.put(imageView, lazyImage.toFileName());
if(lazyImage.getImage()!=null){
imageView.setImageBitmap(lazyImage.getImage());//get from lazy image
//Log.d(TAG,"----LazyImage cache:"+lazyImage.toFileName());
}else{
Bitmap bitmap=memoryCache.get(lazyImage.toFileName());//get from memory cache
if(bitmap!=null){
lazyImage.setImage(bitmap);
imageView.setImageBitmap(bitmap);
//Log.d(TAG,"----MEMORY cache:"+lazyImage.toFileName());
}else
{
if(defaultImageResId>0)
imageView.setImageResource(defaultImageResId);
else
imageView.setImageBitmap(null);
if(lazyImage.getImage_url() != null)
queuePhoto(lazyImage, imageView,saveDisk);//get from SD card or web
}
}
}
private void queuePhoto(LazyImage lazyImage, ImageView imageView,boolean saveDisk)
{
//This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them.
photosQueue.clean(imageView);
PhotoToLoad photosToLoad=new PhotoToLoad(lazyImage, imageView, saveDisk);
synchronized(photosQueue.photosToLoadStack){
photosQueue.photosToLoadStack.push(photosToLoad);
photosQueue.photosToLoadStack.notifyAll();
}
//start thread if it's not started yet
if(photoLoaderThread.getState()==Thread.State.NEW)
photoLoaderThread.start();
}
private Bitmap getBitmap(LazyImage lazyImage,boolean saveDisk)
{
if(!saveDisk){
return ImageUtil.returnBitMap(lazyImage.getImage_url());
}
File f=fileCache.getFile(lazyImage);
//from SD cache
Bitmap b = ImageUtil.file2Bitmap(f);
if(b!=null){
lazyImage.setImage(b);
//Log.d(TAG,"----FILE cache:"+lazyImage.toFileName());
return b;
}
//from web
try {
URL imageUrl = new URL(lazyImage.getImage_url());
HttpURLConnection conn = (HttpURLConnection)imageUrl.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
InputStream is=conn.getInputStream();
ImageUtil.inputStream2File(is, f);
lazyImage.setImage(ImageUtil.file2Bitmap(f));
//Log.d(TAG,"----WEB URL:"+lazyImage.toFileName());
return lazyImage.getImage();
} catch (Exception ex){
//ex.printStackTrace();
return null;
}
}
//Task for the queue
private class PhotoToLoad
{
public LazyImage lazyImage;
public ImageView imageView;
public boolean saveDisk;
public PhotoToLoad(LazyImage l, ImageView i, boolean s){
lazyImage=l;
imageView=i;
saveDisk = s;
}
}
public void stopThread()
{
photoLoaderThread.interrupt();
}
//stores list of photos to download
class PhotosQueue
{
private Stack<PhotoToLoad> photosToLoadStack=new Stack<PhotoToLoad>();
//removes all instances of this ImageView
public void clean(ImageView image)
{
for(int j=0 ;j<photosToLoadStack.size();){
if(photosToLoadStack.get(j).imageView==image)
photosToLoadStack.remove(j);
else
++j;
}
}
}
class PhotosLoaderThread extends Thread {
public void run() {
try {
while(true)
{
//thread waits until there are any images to load in the queue
if(photosQueue.photosToLoadStack.size()==0)
synchronized(photosQueue.photosToLoadStack){
photosQueue.photosToLoadStack.wait();
}
if(photosQueue.photosToLoadStack.size()!=0)
{
PhotoToLoad photoToLoad;
synchronized(photosQueue.photosToLoadStack){
photoToLoad=photosQueue.photosToLoadStack.pop();
}
Bitmap bmp=getBitmap(photoToLoad.lazyImage,photoToLoad.saveDisk);
memoryCache.put(photoToLoad.lazyImage.toFileName(), bmp);
String tag=imageViews.get(photoToLoad.imageView);
if(tag!=null && tag.equals(photoToLoad.lazyImage.toFileName())){
BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
Activity a=(Activity)photoToLoad.imageView.getContext();
a.runOnUiThread(bd);
}
}
if(Thread.interrupted())
break;
}
} catch (InterruptedException e) {
//allow thread to exit
}
}
}
//Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable
{
Bitmap bitmap;
ImageView imageView;
public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
public void run()
{
if(bitmap!=null)
imageView.setImageBitmap(bitmap);
else
imageView.setImageResource(defaultImageResId);
}
}
public void clearCache() {
memoryCache.clear();
fileCache.clear();
}
public void clearMemoryCache() {
memoryCache.clear();
}
}
class MemoryCache {
private HashMap<String, SoftReference<Bitmap>> cache=new HashMap<String, SoftReference<Bitmap>>();
public Bitmap get(String id){
if(!cache.containsKey(id))
return null;
SoftReference<Bitmap> ref=cache.get(id);
return ref.get();
}
public void put(String id, Bitmap bitmap){
cache.put(id, new SoftReference<Bitmap>(bitmap));
}
public void clear() {
cache.clear();
}
}
class FileCache {
private File cacheDir;
public FileCache(Context context){
//Find the dir to save cached images
// if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"whyonly/cache");
// else
// cacheDir=context.getCacheDir();
Log.d("ImageLoader","cacheDir:"+cacheDir);
if(!cacheDir.exists())
cacheDir.mkdirs();
}
public File getFile(LazyImage lazyImage){
String filename= lazyImage.toFileName();
File f = new File(cacheDir, filename);
return f;
}
public void clear(){
File[] files=cacheDir.listFiles();
for(File f:files)
f.delete();
}
}
通过 ImageLoader的包装,应用起来应该非常简单,示例代码如下:
private ImageLoader imageLoader = new ImageLoader(this,R.drawable.nohead);
imageLoader.displayImage(bean, imageView);
以上给出来图片延迟加载的基本概念,以及通过一个示例,说明了如何封装,以便应用程序可以简单地调用。下一篇将通过一个完整的工程例子,对图片的延迟加载,以及ListView的延迟加载加载进行综述。(待续)