詳解ListView載入網路圖片的優化,讓你輕鬆掌握!
最近身邊很多的人在問ListView載入網路圖片該如何防止OOM,對於初學者來說ListView雖然平常用的比較多,但大多不知道該如何進行優化。同時,在面試的過程中ListView的優化問題也是最常會被問到的,以前面試中要是你能說出優化ListView的幾個方法,那基本上面試官可能就會認可你的能力了。
我們來了解一些ListView在載入大量網路圖片的時候存在的常見問題:
1.效能問題,ListView的滑動有卡頓,不流暢,造成非常糟糕的使用者體驗。
2.圖片的錯位問題。
3.圖片太大,載入Bitmap時造成的OOM(Out of memory),也就是棧記憶體溢位。
4.非同步執行緒丟失的問題。
針對所存在的問題我們逐個擊破,徹底的掌握ListView的優化問題,有利於我們的學習和工作。
(一) 效能問題:
在這個問題上我們可以在Adapter介面卡中中複用convertView 和寫一個內部ViewHolder類來解決。但是如果每個Adapter中都寫一個ViewHolder類會顯得非常的麻煩,下面我給大家一個萬能的ViewHolder類,方便在任何Adapter中呼叫。
public class BaseViewHolder { @SuppressWarnings("unchecked") public static <T extends View> T get(View view, int id) { SparseArray<View> viewHolder = (SparseArray<View>) view.getTag(); if (viewHolder == null) { viewHolder = new SparseArray<View>(); view.setTag(viewHolder); } View childView = viewHolder.get(id); if (childView == null) { childView = view.findViewById(id); viewHolder.put(id, childView); } return (T) childView; } }
呼叫BaseViewHolder類的示例程式碼:
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(
R.layout.personplans_item, parent, false);
}
TextView tv_product_type1 = BaseViewHolder.get(convertView,
R.id.tv_product_type1);
注意:在BaseViewHolder類中我們看到SparseArray是Android提供的一個工具類 ,用意是用來取代HashMap工具類的。如下圖:
SparseArray是android裡為<Interger,Object>這樣的Hashmap而專門寫的類,目的是提高效率。具體如何提高效率可以去Android文件查詢一下,這裡就不贅述了。
(二)圖片錯位問題
這個問題導致的原因是因為複用ConvertView導致的,在載入大量的Item時,常見的錯位問題。這種問題的解決思路通常是以圖片的Url做為唯一的key,然後setTag中,然後獲取時根據圖片的URL來獲得圖片。
(三)防止OOM,以及非同步載入。
關於非同步載入圖片的思路是:
1.第一次進入時,是沒有圖片的,這時候我們會啟動一個執行緒池,非同步的從網上獲得圖片資料,為了防止圖片過大導致OOM,可以呼叫BitmapFactory中的Options類對圖片進行適當的縮放,最後再顯示主執行緒的ImageView上。
2.把載入好的圖片以圖片的Url做為唯一的key存入記憶體快取當中,並嚴格的控制好這個快取的大小,防止OOM的發生。
3.把圖片快取在SD當中,如果沒有SD卡就放在系統的快取目錄cache中,以保證在APP退出後,下次進來能看到快取中的圖片,這樣就可以讓使你的APP不會給客戶呈現一片空白的景象。
4.使用者第二次進來的時候,載入圖片的流程則是倒序的,首先從內容中看是否存在快取圖片,如果沒有就從SD卡當中尋找,再沒有然後才是從網路中獲取圖片資料。這樣做的既可以提高載入圖片的效率,同時也節約了使用者的流量。
說完了理論性的東西,我們來開始動手實戰一下吧,下面介紹一個GitHub上一個很輕巧的開源框架LazyListGitHub地址,然後基於它做一些我們想要的效果,關於開源的東西,我們不止要學會用,還要從中能學到東西。眾所周知的Android-Universal-Image-Loader其實就是基於LazyList的一個拓展 ,增加了更多的配置。但是從學習的角度我們還是希望能從原理學起,太多的功能封裝,難免會讓我們暈乎,簡單的功能實現就夠了。
1.先來看一下執行效果圖:
2.來看一下LazyList專案的結構:
構非常的簡單,整個專案的體積才10多k,適合加入我們自己的專案當中去,研究起來也不會覺得難,因為都是最為核心的東西。
3.呼叫Lazylist的入口:
adapter = new LazyAdapter(this, mStrings);
list.setAdapter(adapter);
傳入一個裝滿圖片Url地址的字串陣列進去,然後在LazyAdapter中對ListView中的進行顯示。
4.具體LazyAdapter中呼叫的程式碼是:
public class LazyAdapter extends BaseAdapter {
private Activity activity;
private String[] data;
private static LayoutInflater inflater=null;
public ImageLoader imageLoader;
public LazyAdapter(Activity a, String[] d) {
activity = a;
data=d;
inflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
imageLoader=new ImageLoader(activity.getApplicationContext());
}
public int getCount() {
return data.length;
}
public Object getItem(int position) {
return position;
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
View vi=convertView;
if(convertView==null)
vi = inflater.inflate(R.layout.item, null);
ImageView image=BaseViewHolder.get(vi, R.id.image);
ImageView image2=BaseViewHolder.get(vi, R.id.image2);
imageLoader.DisplayImage(data[position], image);
imageLoader.DisplayImage(data[position], image2);
return vi;
}
}
5.從上面我們可以看出來其實最重要的封裝顯示圖片的方法就在ImageLoader這個類中。
public class ImageLoader {
MemoryCache memoryCache=new MemoryCache();
FileCache fileCache;
private Map<ImageView, String> imageViews=Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
ExecutorService executorService;
Handler handler=new Handler();//handler to display images in UI thread
public ImageLoader(Context context){
fileCache=new FileCache(context);
executorService=Executors.newFixedThreadPool(5);
}
// 當進入listview時預設的圖片,可換成你自己的預設圖片
final int stub_id=R.drawable.stub;
public void DisplayImage(String url, ImageView imageView)
{
imageViews.put(imageView, url);
// 先從記憶體快取中查詢
Bitmap bitmap=memoryCache.get(url);
if(bitmap!=null)
imageView.setImageBitmap(bitmap);
else
{
// 若沒有的話則開啟新執行緒載入圖片
queuePhoto(url, imageView);
imageView.setImageResource(stub_id);
}
}
private void queuePhoto(String url, ImageView imageView)
{
PhotoToLoad p=new PhotoToLoad(url, imageView);
executorService.submit(new PhotosLoader(p));
}
private Bitmap getBitmap(String url)
{
File f=fileCache.getFile(url);
/**
* 先從檔案快取中查詢是否有
*/
//from SD cache
Bitmap b = decodeFile(f);
if(b!=null)
return b;
/**
* 最後從指定的url中下載圖片
*/
//from web
try {
Bitmap bitmap=null;
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection)imageUrl.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
InputStream is=conn.getInputStream();
OutputStream os = new FileOutputStream(f);
Utils.CopyStream(is, os);
os.close();
conn.disconnect();
bitmap = decodeFile(f);
return bitmap;
} catch (Throwable ex){
ex.printStackTrace();
if(ex instanceof OutOfMemoryError)
memoryCache.clear();
return null;
}
}
/**
* decode這個圖片並且按比例縮放以減少記憶體消耗,虛擬機器對每張圖片的快取大小也是有限制的
*/
//decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f){
try {
//decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
FileInputStream stream1=new FileInputStream(f);
BitmapFactory.decodeStream(stream1,null,o);
stream1.close();
//Find the correct scale value. It should be the power of 2.
final int REQUIRED_SIZE=70;
int width_tmp=o.outWidth, height_tmp=o.outHeight;
int scale=1;
while(true){
if(width_tmp/2<REQUIRED_SIZE || height_tmp/2<REQUIRED_SIZE)
break;
width_tmp/=2;
height_tmp/=2;
scale*=2;
}
//decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize=scale;
FileInputStream stream2=new FileInputStream(f);
Bitmap bitmap=BitmapFactory.decodeStream(stream2, null, o2);
stream2.close();
return bitmap;
} catch (FileNotFoundException e) {
}
catch (IOException e) {
e.printStackTrace();
}
return null;
}
//Task for the queue
private class PhotoToLoad
{
public String url;
public ImageView imageView;
public PhotoToLoad(String u, ImageView i){
url=u;
imageView=i;
}
}
class PhotosLoader implements Runnable {
PhotoToLoad photoToLoad;
PhotosLoader(PhotoToLoad photoToLoad){
this.photoToLoad=photoToLoad;
}
@Override
public void run() {
try{
if(imageViewReused(photoToLoad))
return;
Bitmap bmp=getBitmap(photoToLoad.url);
memoryCache.put(photoToLoad.url, bmp);
if(imageViewReused(photoToLoad))
return;
BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad);
handler.post(bd);
}catch(Throwable th){
th.printStackTrace();
}
}
}
/**
* 防止圖片錯位
* @param photoToLoad
* @return
*/
boolean imageViewReused(PhotoToLoad photoToLoad){
String tag=imageViews.get(photoToLoad.imageView);
if(tag==null || !tag.equals(photoToLoad.url))
return true;
return false;
}
/**
* 用於在UI執行緒中更新介面
*
*/
//Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable
{
Bitmap bitmap;
PhotoToLoad photoToLoad;
public BitmapDisplayer(Bitmap b, PhotoToLoad p){
bitmap=b;
photoToLoad=p;
}
public void run()
{
if(imageViewReused(photoToLoad))
return;
if(bitmap!=null)
photoToLoad.imageView.setImageBitmap(bitmap);
else
photoToLoad.imageView.setImageResource(stub_id);
}
}
public void clearCache() {
memoryCache.clear();
fileCache.clear();
}
}
由於篇幅的原因,FileCache和MenoryCache這兩個快取類,我就不貼大量程式碼了,全部都在我提供的demo示例程式碼中,你可以仔細的去研究一下,並有詳細的註釋,大家下載就可以用在自己的專案中了。