2025-05-23

ListView是一種可以顯示一系列項目並能進行滾動顯示的View。在每行裡,既可以是簡單的文本,也可以是復雜的結構。一般情況下,你都需要保證ListView運行得很好(即:渲染更快,滾動流暢)。在接下來的內容裡,我將就ListView的使用,向大傢提供幾種解決不同性能問題的解決方案。
如果你想使用ListView,你就不得不使用ListAdapter來顯示內容。SDK中,已經有瞭幾種簡單實現的Adapter:
·ArrayAdapter<T> (顯示數組對象,使用toString()來顯示)
·SimpleAdapter (顯示Maps列表)
·SimpleCursorAdapter(顯示通過Cursor從DB中獲取的信息)
這些實現對於顯示簡單的列表來說,非常棒!一旦你的列表比較復雜,你就不得不書寫自己的ListAdapter實現。在多數情況下,直接從ArrayAdapter擴展就能很好地處理一組對象。此時,你需要處理的工作隻是告訴系統如何處理列表中的對象。通過重寫getView(int, View, ViewGroup)方法即可達到。
在這裡,舉一個你需要自定義ListAdapter的例子:顯示一組圖片,圖片的旁邊有文字挨著。
ListView例:以圖片文字方式顯示的Youtube搜索結果
圖片需要實時從internet上下載下來。讓我們先創建一個Class來代表列表中的項目:
public class ImageAndText {
    private String imageUrl;
    private String text;
 
    public ImageAndText(String imageUrl, String text) {
        this.imageUrl = imageUrl;
        this.text = text;
    }
    public String getImageUrl() {
        return imageUrl;
    }
    public String getText() {
        return text;
    }
}
現在,我們要實現一個ListAdapter,來顯示ImageAndText列表。
public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> {
 
    public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts) {
        super(activity, 0, imageAndTexts);
    }
 
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Activity activity = (Activity) getContext();
        LayoutInflater inflater = activity.getLayoutInflater();
 
        // Inflate the views from XML
        View rowView = inflater.inflate(R.layout.image_and_text_row, null);
        ImageAndText imageAndText = getItem(position);
 
        // Load the image and set it on the ImageView
        ImageView imageView = (ImageView) rowView.findViewById(R.id.image);
        imageView.setImageDrawable(loadImageFromUrl(imageAndText.getImageUrl()));
 
        // Set the text on the TextView
        TextView textView = (TextView) rowView.findViewById(R.id.text);
        textView.setText(imageAndText.getText());
 
        return rowView;
    }
 
    public static Drawable loadImageFromUrl(String url) {
        InputStream inputStream;
        try {
            inputStream = new URL(url).openStream();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return Drawable.createFromStream(inputStream, "src");
    }
}
這些View都是從“image_and_text_row.xml”XML文件中inflate的:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content">
 
        <ImageView android:id="@+id/image"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:src="@drawable/default_image"/>
 
        <TextView android:id="@+id/text"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"/>
 
</LinearLayout>
這個ListAdapter實現正如你所期望的那樣,能在ListView中加載ImageAndText。但是,它唯一可用的場合是那些擁有很少項目、無需滾動即可看到全部的列表。如果ImageAndText列表內容很多的時候,你會看到,滾動起來不是那麼的平滑(事實上,遠遠不是)。
性能改善
上面例子最大的瓶頸是圖片需要從internet上下載。因為我們的代碼都在UI線程中執行,所以,每當一張圖片從網絡上下載時,UI就會變得停滯。如果你用3G網絡代替WiFi的話,性能情況會變得更糟。
為瞭避免這種情況,我們想讓圖片的下載處於單獨的線程裡,這樣就不會過多地占用UI線程。為瞭達到這一目的,我們可能需要使用為這種情況特意設計的AsyncTask。實際情況中,你將註意到AsyncTask被限制在10個以內。這個數量是在Android SDK中硬編碼的,所以我們無法改變。這對我們來說是一個制限事項,因為常常有超過10個圖片同時在下載。
AsyncImageLoader
一個變通的做法是手動的為每個圖片創建一個線程。另外,我們還應該使用Handler來將下載的圖片invoke到UI線程。我們這樣做的原因是我們隻能在UI線程中修改UI。我創建瞭一個AsyncImageLoader類,利用線程和Handler來負責圖片的下載。此外,它還緩存瞭圖片,防止單個圖片被下載多次。
public class AsyncImageLoader {
    private HashMap<String, SoftReference<Drawable>> imageCache;
 
    public AsyncImageLoader() {
        imageCache = new HashMap<String, SoftReference<Drawable>>();
    }
 
    public Drawable loadDrawable(final String imageUrl, final ImageCallback imageCallback) {
        if (imageCache.containsKey(imageUrl)) {
            SoftReference<Drawable> softReference = imageCache.get(imageUrl);
            Drawable drawable = softReference.get();
            if (drawable != null) {
                return drawable;
            }
        }
        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message message) {
                imageCallback.imageLoaded((Drawable) message.obj, imageUrl);
            }
        };
        new Thread() {
            @Override
            public void run() {
                Drawable drawable = loadImageFromUrl(imageUrl);
                imageCache.put(imageUrl, new SoftReference<Drawable>(drawable));
                Message message = handler.obtainMessage(0, drawable);
                handler.sendMessage(message);
            }
        }.start();
        return null;
    }
 
    public static Drawable loadImageFromUrl(String url) {
        // …
    }
 
    public interface ImageCallback {
        public void imageLoaded(Drawable imageDrawable, String imageUrl);
    }
}
註意:我使用瞭SoftReference來緩存圖片,允許GC在需要的時候可以對緩存中的圖片進行清理。它這樣工作:
·調用loadDrawable(ImageUrl, imageCallback),傳入一個匿名實現的ImageCallback接口
·如果圖片在緩存中不存在的話,圖片將從單一的線程中下載並在下載結束時通過ImageCallback回調
·如果圖片確實存在於緩存中,就會馬上返回,不會回調ImageCallback
在你的程序中,隻能存在一個AsyncImageLoader實例,否則,緩存不能正常工作。在ImageAndTextListAdapter類中,我們可以這樣替換:
ImageView imageView = (ImageView) rowView.findViewById(R.id.image);
imageView.setImageDrawable(loadImageFromUrl(imageAndText.getImageUrl()));
換成
final ImageView imageView = (ImageView) rowView.findViewById(R.id.image);
Drawable cachedImage = asyncImageLoader.loadDrawable(imageAndText.getImageUrl(), new ImageCallback() {
    public void imageLoaded(Drawable imageDrawable, String imageUrl) {
        imageView.setImageDrawable(imageDrawable);
    }
});
imageView.setImageDrawable(cachedImage);
使用這個方法,ListView執行得很好瞭,並且感覺滑動更平滑瞭,因為UI線程再也不會被圖片加載所阻塞。
更好的性能改善 www.aiwalls.com
如果你嘗試瞭上面的解決方案,你將註意到ListView也不是100%的平滑,仍然會有些東西阻滯著它的平滑性。這裡,還有兩個地方可以進行改善:
·findViewById()的昂貴調用
·每次都inflate XML
因此,修改代碼如下:
public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> {
 
    private ListView listView;
    private AsyncImageLoader asyncImageLoader;
 
    public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts, ListView listView) {
        super(activity, 0, imageAndTexts);
        this.listView = listView;
        asyncImageLoader = new AsyncImageLoader();
    }
 
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Activity activity = (Activity) getContext();
 
        // Inflate the views from XML
        View rowView = convertView;
        ViewCache viewCache;
        if (rowView == null) {
            LayoutInflater inflater = activity.getLayoutInflater();
            rowView = inflater.inflate(R.layout.image_and_text_row, null);
            viewCache = new ViewCache(rowView);
            rowView.setTag(viewCache);
        } else {
            viewCache = (ViewCache) rowView.getTag();
        }
        ImageAndText imageAndText = getItem(position);
 
        // Load the image and set it on the ImageView
        String imageUrl = imageAndText.getImageUrl();
        ImageView imageView = viewCache.getImageView();
        imageView.setTag(imageUrl);
        Drawable cachedImage = asyncImageLoader.loadDrawable(imageUrl, new ImageCallback() {
            public void imageLoaded(Drawable imageDrawable, String imageUrl) {
                ImageView imageViewByTag = (ImageView) listView.findViewWithTag(imageUrl);
                if (imageViewByTag != null) {
                    imageViewByTag.setImageDrawable(imageDrawable);
                }
            }
        });
        imageView.setImageDrawable(cachedImage);
 
        // Set the text on the TextView
        TextView textView = viewCache.getTextView();
        textView.setText(imageAndText.getText());
 
        return rowView;
    }
}
這裡有兩點需要註意:第一點是drawable不再是加載完畢後直接設定到ImageView上。正確的ImageView是通過tag查找的,這是因為我們現在重用瞭View,並且圖片有可能出現在錯誤的行上。我們需要擁有一個ListView的引用來通過tag查找ImageView。
另外一點是,實現中我們使用瞭一個叫ViewCache的對象。它這樣定義:
public class ViewCache {
 
    private View baseView;
    private TextView textView;
    private ImageView imageView;
 
    public ViewCache(View baseView) {
        this.baseView = baseView;
    }
 
    public TextView getTextView() {
        if (textView == null) {
            textView = (TextView) baseView.findViewById(R.id.text);
        }
        return titleView;
    }
 
    public ImageView getImageView() {
        if (imageView == null) {
            imageView = (ImageView) baseView.findViewById(R.id.image);
        }
        return imageView;
    }
}
有瞭ViewCache對象,我們就不需要使用findViewById()來多次查詢View對象瞭。
總結
我已經向大傢演示瞭3種改進ListView性能的方法:
·在單一線程裡加載圖片
·重用列表中行
·緩存行中的View

摘自 與時俱進

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *