在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的延遲加載加載進行綜述。(待續)