加入RUN!PC粉絲團
最近新增的精選文章
 
最多人點閱的精選文章
 
 
精選文章 - 開發技術
分享到Plurk
分享到FaceBook
 
Android 3.X SDK開發實務-使用載入器
文‧圖/何孟翰 2011/9/6 上午 10:50:07

在Android 3.0之前,如果使用者以一般Java的經驗來開發Android應用程式,常常會發生應用程式無回應(Application Not Responding),也就是ANR的情形。特別只要是超過0.5秒沒有回應使用者就會感受到不順暢, 而大約超過三秒一般人就會認為程式異常了。特別是當你在開一個檔案,讀取資料準備來更新使用者介面時,只要一執行這種I/O的同步呼叫函數,由於要等到這個函數return之後才能夠得到進行原先的程序,所以在這些I/O的呼叫時常常會讓使用者得不到適當的回應。因此在Android 3.0時就新增了一個loader的機制,來幫助你非同步的載入資料至Activity或者是Fragment的元件之中。

Lodaers的基礎特性
簡單來說,你可以把Loader想像成是一個獨立的元件並且有以下的幾個特性:
1. Loader原生就是非同步的讀取資料元件並且協助載入這些資料至程式中
2. Loader可以適用於各種的Activity和Fragment元件
3. Loader可以監控資料來源的改變,而當資料變動時就會顯示新的結果
4. Loader會自動重新連結到最後一次載入的cursor而在組態改變之後重新生成,因此他們不需要重新執行資料的查詢

所以我們可以把這個Loader想像是使用者元件層,如同一般的Activity或者是Fragment元件到資料層,也就是Content Provider的一種中間代理機制,而我們就可以運用Loader元件有系統的處理資料的呼叫。

Loaders的必要元件
一般說來最常用的 Cursor是CursorLoader,它可以從ContentProvider中捷取資料,並且傳回一個Cursor,正如同一般你使用ContentResolver的方式一樣。事實上你可以把它想像是一套由Android用AsyncTaskLoader所定義的標準方式,來執行背景查詢,並且它不會阻擋應用程式的UI,也就是說它不會造成使用者感受到ANR的情形。不過在了解如何使用CursorLoader之前,必須先了解一些重要的元件,才能方便你呼叫並且初始化這個CursorLoader。

首先如果你有開發過Android的經驗,一定很清楚Android中常常會用類別的manager(例如ServiceManager)來管理特定的類別與物件。舉例而言,管理Loader的就是LoaderManager,它可以用來初始化loader的類別實例。既然它是用來將資料裝填進Activity或者是Fragment的,你可以把它想像成是Activity或者是Fragment的一個協助的類別。因此對於每一個Activity或者是Fragment,都只有一個 LoaderManager,但是這一個LoaderManager可以有多個loaders。因此你可以想像,如果你在這個Activity或者是Fragment的生命周期中需要載入一次或多次大量的資料,你就可以都呼叫這個LoaderManager來幫助管理Loader的物件實例。

也因此,這個LoaderManager提供了一個介面叫作LoaderCallbacks,它可以讓你的這個Activity或者是Fragment來操作這些loader的物件,所以你可以呼叫這個界面的onCreateLoader()函數來取得一個loader的實例,並且當這個loader的載入完成之後這個介面的onLoadFinish(),或者是當這個loader的狀態被重置(reset)時,這個函數就會被呼叫,而在Activity或者是Fragment中就可以知道這個loader已經被無效化了。

由於LoaderManager的情境比較鎖碎, 所以讓我們一步一步在專案中加入這個功能。首先請你新增一個 Android 3.X的專案(截稿為止最新的是3.1),並且打開它最主要的Activity元件,將它所附屬的Loader啟動,並且試著從一個ContentProvider,例如從聯絡人中取出資料,並且使用非同步的方式將資料載入並顯現出來。

實作LoaderManager的介面初始化
如同前面所說,Loader物件的實例是由LoaderManager所控管的,所以我們必須要呼叫getLoaderManager()來取得這個LoaderManager的實例並且試著去初始化它。 一個好的地方來初始化就是在onCreate() 函數中初始化這個 LoaderManager,所以你可以試著在onCreate()的函數中加入如下的敘述:
getLoaderManager().initLoader(0, null, this);

然而當你一加入之後,你會看到這個函數需要該物件是有實作LoaderManager.LoaderCallbacks的介面,所以請你將這個Activity增加這個介面,並且使用一般的 Cursor當作參數:
(原先的宣告)implements LoaderManager.LoaderCallbacks

同時如果你使用的是Eclipse,你會看到這個介面需要實作三個函數,就是我們前面提到的三個loader的狀態對應的函數,onCreateLoader(), onLoadFinish()和onLoaderReset()。

處理非同步的資料載入
因此你可以想像,如果我們有一個Activity並且實作了LoaderManager的介面,則我們必須要提供一個在這個Activity中的資料結構,讓你的loader能夠在背景處理完之後可以更新這個資料結構。在此處讓我們使用一個簡單的SimpleCursorAdapter的類別來作為Activity的成員函數,並且用它來作為資料由loader中載入的容器。因此請你在這個類別中加入如下的敘述:
SimpleCursorAdapter m_adapter;
因此讓我們來想一下,當這個非同此的任務載入完成之後,你可以想像這組Adapter應該要被更新,而這組LoaderManager的介面中的onLoadFinish()也會被呼叫,同時Cursor會被傳入,因此我們就可以把這一組Cursor的實例傳入CursorAdapter之中如下:
m_adapter.swapCursor(arg1);
其中arg1就是傳入的第二個參數,也就是Cursor的物件。同理,當如果onLoaderReset,也就是說這個loader要被重置時,這組Cursor也就失效了,所以我們也就應該將m_adapter中的cursor設為null如下:
m_adapter.swapCursor(null);

當設定好了這組LoaderManager介面的兩個回應函數之後,我們就可以開始使用ContentResolver來擷取ContentProvider的資料了。如果你對 SQLite有初步的了解,你大概可以知道SQLite就是一種作為應用程式內部的嵌入式資料庫,因此你可以想像SQLite的scope是侷限於單一的應用程式。雖然說你或許也可以將你的應用程式的 SQLite資料庫包裝成其它的介面供別人使用(甚至就是封裝成一種Content Provider),但是主要SQLite的應用還是侷限於單一的應用程式中的資料來源。
至於ContentProvider你就可以把它想像成是整個Android的資料來源。也就是說,不是單一的應用程式,只要你的應用程式有被設定了適當的權限,它就可以讀取ContentProvider中的資訊。也因此,你可以想像在Android中只要是能夠讓所有的應用程式都能夠利用的資源,就可以被封裝成Content Provider,而能夠讓其它的應用程式透過一致的介面來存取這個資源的內容。舉例來說像是Android中的通訊錄,也就是Contacts就是一種內建的Content Provider,可以讓其它的應用程式來存取。因此讓我們以聯絡人的Content Provider來作練習,不過在這之前,先讓我們順便看看Android 3.X中聯絡人介面的更新。

聯絡人與Content Provider

當你將這個專案在模擬器底下執行時,你會看到呼叫出來的是一個Android 3.1的平板介面,至截稿之前,目前的Android3系列模擬器在執行時的效能依然都不是太好。就算是點擊了預設模擬器內建的應用程式都需要花一些時間才能等到系統的回應。此時請你先點擊了聯絡人的應用程式,並且稍等之後你就可以看到如圖1的畫面。


▲ 聯絡人的初始畫面


由於預設的Android模擬器中的聯絡人預設是空白的,所以你會看到如圖1的畫面,請你按下「建立新聯絡人」來新增一筆聯絡人,你可以填入一些適當的欄位之後按下新增,就會看到聯絡人中有一筆新的聯絡人如圖2。


▲ 新增了第一筆聯絡人的畫面


此時讓我們按下新增來新增第二個聯絡人,你可以看到在Android 3的畫面由於是平板,所以欄位的陳列更加的一目了然,此時請你新增第二個開發者Run!PC Developer如圖3。


▲ 新增第二個聯絡人


當你按下了完成之後,你可以看到這個聯絡人的畫面左方的Fragments中列有依照名稱的英文字母所分類的聯絡人,而當你點擊之後,在右側就會列出關於這個聯絡人詳細的資料如圖4。


▲ 當有多筆聯絡人時這些聯絡人會以英文字母來排列

當我們用聯絡人新增了幾筆資料之後,對應的Content Provider中的資料也隨之更新了。因此我們就可以開始繼續修改程式碼,使用Loader類別的實例將資料從Content Provider中載入至Activity的元件中。

新增List View來顯示loader的資訊
在新增好了數個使用者之後,讓我們試著將它利用Loader讀回來。首先讓我們新增一個 List元件,以便將資料能夠載入。一個很簡單的方式是請你在main.xml中加入一個ListView,並且將它的 ID設為ListView01。
在Activity的類別中,我們需要新增一個 ListView的成員變數lv1(list view instance1)來對應它。你可以使用findViewById()取得這個實例,並且使用setAdapter()的函數將它的adapter設定成m_adapter。

取出聯絡人的資料
再來我們必須要初始化這個SimpleCursorAdapter,你可以使用標準的SimpleCursorAdapter的建構子來初始化它。初始化之後,你就可以在onCreateLoader的函數中設法傳回這個Cursor,你可以參考下面的程式碼修改你的onCreate()函數,此處我們不去作任何的限制,將所有在通訊錄中的聯絡人,透過ContentProvider將資料傳回來。如果你需要作任何的限制,請參考Content Provider的文件,加上一些資料fetch的限制,讓它能夠傳谷你需要的資料列:
return new CursorLoader(this, baseUri,
CONTACTS_SUMMARY_PROJECTION, select, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");

當程式撰寫完成之後,請你注意由於是Content Provider的內容,所以你必須要在AndroidManifest.xml中新增適當的權限才行。由於是讀取的權限,請你新增一組READ_CONTACTS的權限於uses-permission之中。
當你設置好了之後,你就可以在模擬器底下準備執行它,如果沒有意外,你可以看到應用程式「先」執行了Hello World的這個訊息,再來你才會看到ListView的元件慢慢顯示出來。

小結
其實當你在實際了解了loader的運作實務之後,你可以發現其實LoaderManager的實作並不是阿拉丁神燈,如果你想要從ContentProvider中取出資料到使用者元件(例如ListView)之中,需要撰寫的程式碼如同 存取 Content Provider,或者是設定ListView的Adapter,甚至其它控制的函數都是不可偷懶的。只是藉著LoaderManager,你可以 (似乎)不需要自己控制多執行緒,就可以藉著實作這個LoaderManager的介面,就可以在背景藉著另外一個(你不會注意到的 )執行緒將資料下載,並且帶下載完之後自動替換掉Cursor的內容,這樣ListView或者你的元件就會一起更新了。因此或許你對多執行緒的Android程式設計不熟,或者是每次在開發多執行緒程式時會發生run on UI Thread的錯誤(就是說在背景的執行緒不能更新使用者元件的狀態)等等,那請你可以試著將使用者情境換成這種LoaderManager,就可以在一個 Android 3.X的環境底下完成這種非同步呼叫再更新使用者介面的方式。

結語

截至目前為止,Loader這個機制還是只有在Android 3.0,也就是給平板的系列的 SDK中才有提供。雖然說依照Google之前所公佈的藍圖,Android在2.4中會將一些平板電腦的機制也讓一般的手機使用者使用,但是就算在2.4正式的發佈並且廣泛被大家使用之前,你還是可以透過在非主執行緒的其它執行緒來作這種I/O的任務。此時請你注意Android有一個特性,就是如果是視覺元件的更新一定要透過主執行緒才能完成,至於會花時間的I/O任務,你就可以放在背景的其餘執行緒,這樣當你在執行這些會耗費時間的任務時,由於主執行緒並沒有耗時的任務,所以當使用者在操作時,就算這些I/O任務需要花時間完成,但是使用者仍然能夠得到作業系統的回應 (也就是說會回應使用者的點擊而作互動)。不過筆者建議,就算是在背景執行這些長時間的任務,也請你記得要適時的回應使用者目前的狀態,不然有時使用者還是會覺得這個程式似乎有些卡卡的,也就是說整體應用程式運行的狀態是異常的。