Lesson Learned form Developing Locadz SDK
Introduction
------------
市面上大部份講 Android 的書,多數都是在講 API 要怎麼樣使用,但是,許多的書都沒有提到一個重要的問題,就是什
麼是 UI Thread (有時被稱做Main Thread) ,為什麼要有 UI Thread 以及為什麼不可以在 UI Thread 中執行耗時間的運算。
在本文中,我講談一談我們在開發Locadz SDK所使用到的一些技巧。
UI Thread
---------
UI Thread是Android(or other GUI library)用來處理 Event 的 thread ,這些 Event 可能是使用者處發的,如
Touch Event,或者是其它程式或者是系統底層的事件,如 Intent 或 `Location Updates`_.
在 UI Thread 處理完一個事件後, UI Thread 會重畫整個 Application ,而前面這句話解釋了處理 ANR 的兩個原則
- UI事件的處理要越快越好,在處理完前,UI不會有反應
- UI的更新,一定要發生在UI Thread,要不然,要等到下次UI Event被處發時才會一併被處理
.. _Location Updates: http://developer.android.com/reference/android/location/LocationListener.html
Lesson I: Use WeakReference to Avoid Strong Reference and Memory Leak
----------------------------------------------------------------------
有許多的文章談到如何用 AsyncTask_ 或 ResultReceiver_ 來把需要大量運算的程式,放在另一個 Thread 來做處理。
然而,這些範例都有著一個常備忽略的問題,那就是,這些 AsyncTask or ResultReceiver 常會把前景的Activity包進來
,如果說你的程式只有一個 Activity 的話,這或許不是什麼問題,但是若是你的 App 有多個畫面的話,這些在背景執行的程
式可能就會讓你的程式有 memory leak 的問題;如某家廣告商的 SDK 就有此問題。
一個可能的發生狀況是,Activity A透過 AsyncTask 去遠端下載一個圖片來顯示在 *Activity A* 之上,但因為某些因素
(如忘了設 connection timeout),這個下載的程緒卡住了,既使 User 以從 *Activity A* 切換到 *Activity B* 之
上,這個 *Activity A* 還是不會被 GC 回收掉。
要避開因為有個 Strong Reference 造成 Activity 無法被回收的問題,我們在把有可能被回收的物件傳到另一個 Thread 中
被延後執行時,必需用 WeakReference_ 包住這個物件;如此一來,當Garbage Collector碰到一個已經不在前景的 Activity
時,Garbage Collector會把這物件處理掉,如此一來,就不會有 memory leak 的問題。
.. _AsyncTask: http://developer.android.com/reference/android/os/AsyncTask.html
.. _ResultReceiver: http://stackoverflow.com/questions/3197335/restful-api-service/3197456#3197456
.. _WeakReference: http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/ref/WeakReference.html
.. code-block:: java
/**
* AsyncTask to Load Image
*/
public class DownloadImagesTask extends AsyncTask {
WeakReference imageViewWeakReference = null;
public DownloadImagesTask(ImageView imageView) {
imageViewWeakReference = new WeakReference(imageView);
}
@Override
protected Bitmap doInBackground(Uri... uri) {
return downloadImage(uri);
}
@Override
protected void onPostExecute(Bitmap result) {
ImageView imageView = imageViewWeakReference.get();
if (imageView != null) {
imageView.setImageBitmap(result);
}
}
private Bitmap downloadImage(Uri url) {
...
}
}
Lesson II: Use IntentService to Run Business Logic
---------------------------------------------------
AsyncTask是 Android 最常被用來處理複雜運算時用的工具,透過AsyncTask,我們可以在背景處裡
一些複雜的運算,再把結果放回前景之上。
但據我的經驗,使用 AsyncTask 同時間來擔任 MVC 中的 View & Controller 的工作,最後往往是
把程式碼弄成一團麵線。因此,在開發 Locadz SDK 時,我們把一些跟 UI 無關的運算,都切出來
變成 IntentService 或者是非 Inner class 的 AsyncTask ,把所有的運算邏輯從 Activity 中切出
來,增加重用的可能。
然後運算的結果,再透過 `getHandler().post(...)`_ 更新到 UI 之上.
.. _getHandler().post(...): http://developer.android.com/reference/android/os/Handler.html#post(java.lang.Runnable)
.. code-block:: java
/** Service that retrieve the ad unit allocations from external source and cache locally in SharedPreference. */
public class AdUnitAllocationService extends IntentService {
private static final int CACHE_EXPIRATION_PERIOD = 30 * 60 * 1000; // 30 minutes.
private final static String PREFS_STRING_TIMESTAMP = "timestamp";
private final static String PREFS_STRING_CONFIG = "config";
// response code for possible result.
public static final int RESULT_OK = 1;
public AdUnitAllocationService() {
super(AdUnitAllocationService.class.getCanonicalName());
}
@Override
protected void onHandleIntent(Intent intent) {
AdUnitContext adUnitContext =
(AdUnitContext) intent.getParcelableExtra(IntentConstants.EXTRA_ADUNIT_CONTEXT);
AdUnitAllocation adUnitAllocation = getAdUnitAllocation(adUnitContext);
if (adUnitAllocation != null) {
Ration ration = getRandomRation(adUnitAllocation.getRations());
// send response through ResultReceiver.
ResultReceiver receiver = intent.getParcelableExtra(IntentConstants.EXTRA_RECEIVER);
Bundle resultData = new Bundle();
resultData.putString(IntentConstants.EXTRA_ADUNIT_ID, adUnitContext.getAdUnitId());
resultData.putSerializable(IntentConstants.EXTRA_RATION, ration);
resultData.putSerializable(IntentConstants.EXTRA_EXTRA,
adUnitAllocation.getExtra());
receiver.send(RESULT_OK, resultData);
}
}
/**
* Select a random ration form the provided rations.
* @param rations the candidates.
* @return a random ration from the candidates.
*/
private Ration getRandomRation(List rations) {
//...
}
/**
* Get the allocation configuration for the adunit.
* @param adUnitContext the context of the adunit.
* @return the allocation configuration for the adunit.
*/
AdUnitAllocation getAdUnitAllocation(AdUnitContext adUnitContext) {
//...
}
}
Lesson III: Use Disk Cache instead of (Main) Memory Cache
----------------------------------------------------------
底下的圖表,是Jeff Dean發表的,在談的是讀取資料的的效率,我們把這幾個數字先記起來,
再加一個代表UI設計時人體覺得是即時反應的反應時間上限 100 ms。然後我們再來談 Android UI 的設計。
.. image:: /images/2012-04-20/numbers-everyone-should-know.png
大家可以看到 Main Memory Reference(0.001ms) 與 Disk Seek(10ms) Disk Read(30ms) 的重大差
距,然而,後者的數字在 Mobile Phone 上就不是這樣了。在 Mobile Phone 上,傳統的硬碟扮演的角色,
被NAND Flash Memory, External SD Card所取代了。在存取效率上 NAND Flash Memory 雖然不
比 RAM 快,但是,也仍是 seek time ~1ms 的狠角色。
這 1ms 的負擔,雖比 0.001ms 的負擔高上百倍,以上,但是在 100ms 這UI 反應需求上,卻又變得很渺小了。
因此,在這邊,我會建議大家,若是有 cache 的需求時,直接往 `Internal Storage`_ 塞吧,不要放
在Main memory上,或用個SoftReferenceMap包著。
.. _Internal Storage: http://developer.android.com/guide/topics/data/data-storage.html#filesInternal
Lesson IV: Make All Public Method Async to Avoid UI Update Issue
-----------------------------------------------------------------
在上面第一個範例中有個錯誤,那就是DownloadImagesTask.onPostExecute()會在呼叫DownloadImagesTask.execute(...)的
那個 Thread 上執行,如果說,很不幸的,這個 DownloadImagesTask 並不是從 UI Thread 上來呼叫的話,那
麼,imageView.setImageBitmap(result)便有可能不會即時更新到UI之上。
如果你的開發環境會有這種問題,在包在層層呼叫後,無法確保 Method 是否是在 UI Thread 上執行;那麼我
會建議你把會更新 UI 的 Method ,標成 protected ,然後開放一個 public async method 出來,範例如下:
.. code-block:: java
/**
* Remove old ad views and push the new one.
*
* @param subView the adview to push.
*/
protected void pushSubView(ViewGroup subView) {
//....
}
/**
* submit a push view request to Android's handler. This will remove
* old ad view and push a new one to this layout asynchronously.
*
* @param subView the adview to push.
*/
public void submitPushSubViewRequest(ViewGroup subView) {
Log.d(LOG_TAG, String.format("Scheduled pushSubView(%s)", subView));
getHandler().post(new ViewAdRunnable(this, subView));
}
/**
* Runnable runs on the Main Thread that pushes an AdView to the layout.
*/
private static final class ViewAdRunnable implements Runnable {
private final WeakReference locadzLayoutWeakReference;
private ViewGroup subView;
public ViewAdRunnable(AdUnitLayout layout, ViewGroup subView) {
locadzLayoutWeakReference = new WeakReference(layout);
this.subView = subView;
}
@Override
public void run() {
AdUnitLayout locadzLayout = locadzLayoutWeakReference.get();
if (locadzLayout != null) {
locadzLayout.pushSubView(subView);
}
}
}