1. 程式人生 > >Android-ListView優化常見的三種方式

Android-ListView優化常見的三種方式

優化原理

使用ListView時儘可能的少去執行Layout的Inflate,只渲染和佈置那些在可視範圍內,或者即將出現在可視範圍內的Item

第一

Layout的Inflate是消耗資源巨大的程式碼。即使,Layout檔案已經被高效的解析程式轉換為了二進位制程式碼。Infalte操作依舊需要徹底包含整個XML程式碼樹,而且還要例項化相應的View。在Android 的原始碼中,ListView通過View回收機制解決了這個問題。這樣就可以非常簡單的通過可回收的View設定每個Item的內容。而不用,為每一個Item都Inflate Layout 。

第二

通過ListView的View回收機制。在可視範圍上面或者下面的View加入到回收池中。當在可視範圍內的View被移出可視範圍內時,其也會被新增到回收池中。以這種方式ListView只需佔用非常少的記憶體幾可以儲存可視範圍內的View和回收池中的View。ListView的View回收機制以兩種不同的方式提供可回收的View即從上往下提供,和從下往上提供。採取何種方式,取決於滑動的方式。下面這張圖展示了,當你下滑時ListView的View回收機制所做的工作。


當滑動ListView時,ListView動態的提供可回收的View。因此,如何使Adapter的getView()變得儘可能輕巧成為了關鍵所在。

方式一:使用可回收的View

每當ListView需要顯示一條新的Item的時候,它會回撥你的Adapter的getView()方法。getView()方法有三個引數:Item的位置,convertView,Item的上級容器。

引數convertView實際上就是一個之前提到的可回收的View。當ListView要回收這個View的時候,它的資料就會被清空。因此,當convertView不為null的時候,只需要將資料填充到裡面,而不用Inflate一個新的View。

Adapter中的getView()方法的程式碼應該像下面這樣設計:

public View getView(int position, View convertView, ViewGroup parent) {
		    if (convertView == null) {
		        convertView = mInflater.inflate(R.layout.your_layout, null);
		    }
		    TextView text = (TextView) convertView.findViewById(R.id.text);
		    text.setText("Position " + position);
		    return convertView;
		}

方式二:使用ViewHolder

在一個已經Inflate的Layout中尋找View是Android開發中非常普遍的操作。這通常通過View的findViewById方法來實現。這個方法會遞迴整個View樹,以尋找那個與ID匹配的View。在靜態的程式碼中使用findViewById()還是非常棒的,但是,如你看到的那樣。當滑動ListView的時候,ListView會非常頻繁的回撥Adapter的getView()方法。這就可能在不知不覺中影響ListView的滑動效能。尤其發生在你的Item的Layout非常的複雜的時候。

ViewHolder就是用來儲存那些在你的getView()方法中呼叫findViewById()方法得到的View。ViewHolder是一個非常輕巧的內部類。它儲存那些Item內部的View的直接引用。然後可以在Inflate結束之後,將ViewHolder物件儲存在Item的tag當中。以這種方式,你只需要在第一次建立Item的時候呼叫findViewById就可以了。

下面就是使用ViewHolder提高ListView的程式碼:

public View getView(int position, View convertView, ViewGroup parent) {
		    ViewHolder holder;

		    if (convertView == null) {
		        convertView = mInflater.inflate(R.layout.your_layout, null);

		        holder = new ViewHolder();
		        holder.text = (TextView) convertView.findViewById(R.id.text);

		        convertView.setTag(holder);
		    } else {
		        holder = convertView.getTag();
		    }

		    holder.text.setText("Position " + position);

		    return convertView;
		}

		private static class ViewHolder {
		    public TextView text;
		}

方式三:使用非同步載入

在Android的App中,ListView使用像圖片這樣耗費資源的Item是非常普遍的。在你的Adapter中使用drawable資源是非常棒的,因為Android記憶體的程式碼會快取這些資源。但是,你可能會想要使用更加靈活的內容。比如來自本地裝置或者來自網路的縮圖或者圖片之類的。在這種情況下,你可能不想要直接的在你的Adapter的getView()中載入這些資源。因為阻塞UI執行緒會造成ANR異常。同時這麼做也使得你的ListView滑動的更加平滑。

你所要做的事情就是讓那些需要IO操作或者耗費CPU資源的操作在一個額外的執行緒中執行。為了實現這個目的,你任然需要遵循ListView的View回收機制的規則。例如:當你的Adapter的getView()正在使用AsyncTask載入一張圖片。需要這張圖片的Item可能已經在圖片載入完成之前就已經被ListView的View回收機制回收了。因此你必須要知道,當你完成非同步載入的時候,對應的Item是否已經被回收了。

一個簡單的方法是,為你的AsyncTask提供一個識別資訊用以區別其對應的Item。這樣,當你的AsyncTask完成載入工作的時候,就可以判斷對應的Item還是不式最初的那個Item。事實上存在許多方式實現這個功能,下面的程式碼只是其中最為簡單的一種:
public View getView(int position, View convertView,
		        ViewGroup parent) {
		    ViewHolder holder;

		    ...

		    holder.position = position;

		    new ThumbnailTask(position, holder)
		            .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);

		    return convertView;
		}

		private static class ThumbnailTask extends AsyncTask {
		    private int mPosition;
		    private ViewHolder mHolder;

		    public ThumbnailTask(int position, ViewHolder holder) {
		        mPosition = position;
		        mHolder = holder;
		    }

		    @Override
		    protected Cursor doInBackground(Void... arg0) {
		        // Download bitmap here
		    }

		    @Override
		    protected void onPostExecute(Bitmap bitmap) {
		        if (mHolder.position == mPosition) {
		            mHolder.thumbnail.setImageBitmap(bitmap);
		        }
		    }
		}

		private static class ViewHolder {
		    public ImageView thumbnail;
		    public int position;
		}