1. 程式人生 > >Android非同步載入AsyncTask詳解

Android非同步載入AsyncTask詳解

曾看見有人說過,覺得很有道理,分享一下:

  技術分為術和道兩種:

  (1)具體做事的方法是術;

  (2)做事的原理和原則是道;

       最近專案發現個重大問題,結果打log跟蹤查是AsyncTask導致的。如果對AsyncTask瞭解的不夠深入透徹,那寫程式碼就是埋雷。以後不定在哪個時間爆炸。首先我們要了解,谷歌為什麼發明AsyncTask,AsyncTask到底是用來解決什麼問題的?Android有一個原則---單執行緒模型的原則:UI操作並不是執行緒安全的並且這些操作必須在UI執行緒中執行所以谷歌就製造AsyncTaskAsyncTask擴充套件Thread增強了與主執行緒的互動的能力。如果你的應用沒有與主執行緒互動,那麼就直接使用Thread就好了。

在單執行緒模型中始終要記住兩條法則:
1. 不要阻塞UI執行緒
2. 確保只在UI執行緒中訪問Android UI工具包

首先來說說AsyncTask重寫的4個方法:

(1)doInBackground()  //執行在後臺執行緒中

(2)onPreExecute()  //執行在UI執行緒中

(3)onProgressUpdate()  //執行在UI執行緒中

(4)onPostExecute()  //執行在UI執行緒中

 詳細的這幾個方法怎麼使用,具體不清楚的可以谷歌一下,好多同仁講的好詳細的;

為了正確的使用AsyncTask類,以下是幾條必須遵守的準則:
1) Task的例項必須在UI thread中建立
2) execute方法必須在UI thread中呼叫


3) 不要手動的呼叫onPreExecute(), onPostExecute(Result),doInBackground(Params…), onProgressUpdate(Progress…)這幾個方法
4) 該task只能被執行一次,否則多次呼叫時將會出現異常
doInBackground方法和onPostExecute的引數必須對應,這兩個引數在AsyncTask宣告的泛型引數列表中指定,第一個為doInBackground 接受的引數,第二個為顯示進度的引數,第第三個為doInBackground返回和onPostExecute傳入的引數。

     以上四點是我從網摘過來的,我覺得說的有道理,針對第4點,我有異議:即使多次呼叫,也不應該出現異常,因為AsyncTask類有對外公開的介面,cancel(true),isCancelled()。這兩個方法,這兩個方法配合使用就來控制AsyncTask可以手動退出。具體可以參照AsyncTask.java這個原始碼類中有例子的。

AsyncTask must be subclassed to be used. The subclass will override at least
 * one method ({@link #doInBackground}), and most often will override a
 * second one ({@link #onPostExecute}.)</p>
 *
 * <p>Here is an example of subclassing:</p>
 * <pre class="prettyprint">
 * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
 *     protected Long doInBackground(URL... urls) {
 *         int count = urls.length;
 *         long totalSize = 0;
 *         for (int i = 0; i < count; i++) {
 *             totalSize += Downloader.downloadFile(urls[i]);
 *             publishProgress((int) ((i / (float) count) * 100));
 *             // Escape early if cancel() is called
 *             if (isCancelled()) break;
 *         }
 *         return totalSize;
 *     }
 *
 *     protected void onProgressUpdate(Integer... progress) {
 *         setProgressPercent(progress[0]);
 *     }
 *
 *     protected void onPostExecute(Long result) {
 *         showDialog("Downloaded " + result + " bytes");
 *     }
 * }
這個是AsyncTask中給出的demo,看到
 if (isCancelled()) break;

這個就是用來判斷是否退出後臺執行緒,如果設定了cancel(true),就break跳出迴圈。在這個地方多說2點:

(1)終止執行緒最好不要用打斷執行緒來做,這樣的方式太粗暴了,而且不能保證程式碼的完整性,最好的處理方式就是在for迴圈,while迴圈中加入自己的判斷標誌位,就像AsyncTask這種方法來處理是最好的,這也是谷歌來指導我們怎麼來處理終止執行緒的辦法。

(2)也有同學感到疑惑,說我的程式碼就沒有迴圈,怎麼來加標誌位,其實這個一般來說後臺處理,大部分都是處理迴圈的邏輯,很少說一行程式碼或者十幾行程式碼很耗時的,(當然網路相關的另說了,還有下載相關的,這個有其他方法來解決的)。即使有的話,比如呼叫jni,so庫,返回就是慢。那就在幾個耗時的方法的後面都加上標誌位的判斷;

通過上述方法就可以做出完整的方案設計,就能設計,當下次再次執行AsyncTask,先判斷自己是否正在執行,如果在執行,就不執行或取消任務重新執行,這個要看具體的需求是什麼了;

舉個栗子:

private void stopAyncTaskRunning() {
		if (mContactsListLoader != null
				&& mContactsListLoader.getStatus() == AsyncTask.Status.RUNNING) {
			mContactsListLoader.cancel(true); //if task is still running, stop it;
		}

	}

private void getContactsList() {
		stopAyncTaskRunning();
		mContactsListLoader = new ContactsListLoader();
		mContactsListLoader.executeOnExecutor(AsyncTask.THEAD_POOL_EXECUTOR);
	}
當然,我的這個需求是下次進來的時候,就取消上次的任務,然後重新重新整理資料。另外也不要忘記在doInBackground()中的迴圈語句中加入
@Override
		protected Integer doInBackground(Object... arg0) {
			
			List<Contact> contacts = new ArrayList<Contact>();
			ContentResolver cr = mContext.getContentResolver();
			Cursor c = cr.query(ContactsContract.Contacts.CONTENT_URI,
					PROJECTION_CONTACT, null, null, null);
			if (c != null) {
				c.moveToPosition(-1);
				while (c.moveToNext()) {
					if (isCancelled()) {
						break;
					}
。。。 。。。
}
這樣,邏輯按照需求來寫,需求是什麼樣子的,邏輯就相應的怎麼處理;

    再來說說AsyncTask坑人的地方,就是在Android3.0以後的版本,AsyncTask的執行方法分為2個了:

    (1)execute()
    (2)executeOnExecutor()

    如果用AsyncTask呼叫(1)的時候,就表示序列執行執行緒,如果這個Activity中有4個fragment,而且每個fragment都有一個AsyncTask,這樣的話用(1)的話,就必須順序執行,等一個執行完,第二個才執行。如果用方法(2),則可以序列執行,這個UI效果就很好了。執行緒池可以用系統的,也可以用我們自定義的執行緒池;

另外對系統預設執行緒池中執行執行緒數的一些說明,如下:

下面的5代表corePoolSize,10代表阻塞佇列的長度,128代表maximumPoolSize
1:如果執行緒池的數量小於5,則建立新的執行緒並執行
2:如果執行緒數大於5且小於5+10(阻塞佇列大小),則將第6~15的執行緒加入阻塞佇列,待執行緒池中的5個正在執行的執行緒有某個結束後,取出阻塞佇列的執行緒執行。
3:如果執行緒數為16~128,則執行的執行緒數為num-10
4:如果執行緒數大於128,則捨棄。

   最後要說明的就是資料要載入一部分就重新整理UI,給使用者一個好的使用者體驗;舉個栗子,

private static final int DISPLAY_NUM = 10;
	private List<Contact> mContacts = new ArrayList<Contact>();
	
	private class ContactsListLoader extends
			AsyncTask<Object, Integer, Integer> {

		int count = 0;
		List<Contact> mTempContacts = new ArrayList<Contact>(DISPLAY_NUM);

		@Override
		protected void onPreExecute() {
			super.onPreExecute();
			mTempContacts.clear();
			mContacts.clear();
		}

		@Override
		protected Integer doInBackground(Object... arg0) {

			List<Contact> contacts = new ArrayList<Contact>();
			if (c != null) {
				c.moveToPosition(-1);
				while (c.moveToNext()) {
					if (isCancelled()) {
						break;
					}
					... ...
					
					contacts.add(contact);
					mTempContacts.add(contact);
					if (++count >= DISPLAY_NUM) {
						publishProgress();
						count = 0;
					}
				}
			}
			return contacts.size();
		}

		@Override
		protected void onProgressUpdate(Integer... values) {
			super.onProgressUpdate(values);
			mContacts.addAll(mTempContacts);
			mTempContacts.clear();
			mAdapter.notifyDataSetChanged();
		}

		@Override
		protected void onPostExecute(Integer size) {
			if (isCancelled())
				return;
			if (size > 0) {
				if (mTempContacts.size() > 0
						&& mTempContacts.size() != DISPLAY_NUM) {
					mContacts.addAll(mTempContacts);
				}
			} else {
				if (mTempContacts.size() > 0) {
					mContacts.addAll(mTempContacts);
				}
			}
			mAdapter.notifyDataSetChanged();
		}

	}
這個就是我的模型,大家看懂後,就可以套到自己的程式碼中去了。這是我用來動態載入重新整理UI的邏輯,刷新出10個數據就重新整理一次,這樣就可以避免次次重新整理影響效率,又能保證使用者不必等到資料都載入完才能看到資料。一舉兩得。

      要想了解更多AsyncTask,可以參考原始碼,如果有人瞭解的更深入,歡迎童鞋拍磚留言;