1. 程式人生 > >詳解ListView載入網路圖片的優化,讓你輕鬆掌握!

詳解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示例程式碼中,你可以仔細的去研究一下,並有詳細的註釋,大家下載就可以用在自己的專案中了。