Android AsyncTask 問題
很多時開發 app 需要經網絡拿取資料,android 的話最簡單是用 AsyncTask
。AsyncTask
提供一個方便清晰的方法,使用另一 thread 去執行費時的工作,然後更新介面,這能避免阻擋 UI Thread 的工作,導致 "Android Not Responding" 的出現。
這次我們來看看 AsyncTask
的用法和它潛在的問題。
AsyncTask 一般做法
因為需要更新 UI,所以 AsyncTask
一般會以 inner class 的形式加在 Activity
中。如 MainActivity
中要下載一檔案,一般會這樣寫:
public class MainActivity extends Activity {
TextView resultTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
//....initialize resultTextView
new DownloadFilesTask().execute(url1, url2, url3);
}
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 onPostExecute(Long result) {
resultTextView.setText("Downloaded " + result + " bytes");
}
}
}
這是最簡單的用法,但此做法其實有兩個問題:
不死的 Activity
問題
因為使用的是 nested class,DownloadFilesTask
會有一 implicit reference 指向 MainActivity
。結果是,DownloadFileTask
只要未完成工作,MainActivity
就算被 destroy,也不會被回收 (Garbage collected)。由於 Activity
佔記憶體較多,不能被適時清埋的話會做成 OS 效率降低。
覺得影響不大? 試想想一個有 AsyncTask
的 activity
,然後不斷旋轉 android 手機,由於 android 在 rotate screen 時會殺掉舊有的 activity
,再新建一個 activity
,但是舊有的 activity
被 AsyncTask
鎖住,未能及時釋放,你會發現程式所佔的記憶體會不斷上升,直至 OutOfMemoryException
的出現。
AsyncTasks should ideally be used for short operations (a few seconds at the most.)
雖然 Android document 提及過 AsyncTask
不適宜執行太費時的工作(只多只是數秒),但沒有人能預測網絡速度,下載同一 file 有時不用一秒,有時要數分數呀。難道要使用 Service
或自行寫 Thread
來做嗎? 這樣 AsyncTask
還有存在價值嗎?
臃腫
因為要使用 resultTextView
去進行更新,所以最方便的做法是如上面的 nested class。但這樣令原本跟 MainActivity
不相干的程式碼都要放在一起,令 MainActivity
檔案太長太臃腫。
改良之一:static class
最簡單的解決方法,是將 inner class 變為 static,這樣便沒有 implicit reference,解決了僵屍 activity
的問題。
public class MainActivity extends Activity {
TextView resultTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
//....initialize resultTextView
new DownloadFilesTask(this).execute(url1, url2, url3);
}
public void updateDownloadResult(long result){
resultTextView.setText("Downloaded " + result + " bytes");
}
static class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
private MainActivity mainActivity;
public DownloadFilesAsyncTask(MainActivity activity){
mainActivity = activity;
}
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 onPostExecute(Long result) {
mainActivity.updateDownloadResult(result);
}
}
}
不過還是使用 inner class,檔案太大太長,實在不好看。
改良之二: 獨立的 AsyncTask
檔案
比較好的做法當然是將 AsyncTask
抽出來成獨文的 DownloadFilesTask
,然後將 MainActivity
當成 parameter 在 AsyncTask
中使用:
MainActivity.java
:
public class MainActivity extends Activity {
TextView resultTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
//....initialize resultTextView
new DownloadFilesTask(this).execute(url1, url2, url3);
}
public void updateDownloadResult(long result){
resultTextView.setText("Downloaded " + result + " bytes");
}
}
DownloadFilesTask.java
:
public class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
private MainActivity mainActivity;
public DownloadFilesTask(MainActivity mainActivity){
this.mainActivity = MainActivity;
}
protected Long doInBackground(URL... urls) {
//... downloading
return totalSize;
}
protected void onPostExecute(Long result) {
mainActivity.updateDownloadResult("Downloaded " + result + " bytes");
}
}
這樣 MainActivity.java
便能減少不相關的 coding。但熟悉 OO 的你,一定覺得這樣太 tightly coupled: DownloadFilesTask
只能在 MainActivity
中使用,不能由其他 class
執行。
改良之三: 使用 interface
要解決這問題,我們可以用 interface
將 MainActivity
從 DownloadFilesTask
拆開:
新增 OnDownloadFinishedListener
:
public interface OnDownloadFinishedListener {
public void updateDownloadResult(long result);
}
將 MainActivity
implements OnDownloadFinishedListener
:
public class MainActivity extends Activity implements OnDownloadFinishedListener {
TextView resultTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
//....initialize resultTextView
new DownloadFilesTask(this).execute(url1, url2, url3);
}
@Override
public void updateDownloadResult(long result){
resultTextView.setText("Downloaded " + result + " bytes");
}
}
DownloadFilesTask
便可在完成工作後,用 OnDownloadFinishedListener
去更新 UI:
public class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
private OnDownloadFinishedListener listener;
public DownloadFilesTask(OnDownloadFinishedListener listener){
this.listener = listener;
}
protected Long doInBackground(URL... urls) {
//... downloading
return totalSize;
}
protected void onPostExecute(Long result) {
listener.updateDownloadResult("Downloaded " + result + " bytes");
}
}
這樣任何 implement OnDownloadFinishedListener
的 class
都能使用 DownloadFilesTask
了!
改良之四:使用 Local Broadcast
若 DownloadFilesTask
完成後需要通知幾個不同的 object
要怎辦?不會是在 OnDownloadFinihsedListener.updateDownloadResult()
裏 call 它們去更新吧?
這種情況,我們可使用 local broadcast。Android OS 跟不同程式的溝通便是使用廣播 intent
來進行,因為我們不需要通知其他 app,所以只用到 local broadcast。
新加 ResultBroadcastReceiver
以進行接收 broadcast 後的更新
public class ResultBroadcastReceiver extends BroadcastReceiver {
private OnDownloadFinishedListener listener;
public ResultBroadcastReceiver(OnDownloadFinishedListener listener){
this.listener = listener;
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(MainActivity.RESULT)) {
long result = intent.getLongExtra(MainActivity.RESULT_DATA, 0);
listener.updateDownloadResult(result);
}
}
}
MainActivity
加進 ResultBroadcastReceiver
去準備接收
public class MainActivity extends Activity implements OnDownloadFinishedListener {
public static final String RESULT_ACTION = "com.thirtysparks.blog.MainActivity.result";
public static final String RESULT_DATA = "result_data";
private ResultBroadcastReceiver receiver;
TextView resultTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
//....initialize resultTextView
receiver = new ResultBroadcastReceiver(this);
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, new IntentFilter(RESULT_ACTION));
new DownloadFilesTask().execute(url1, url2, url3);
}
@Override
public void onDestroy(){
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
}
@Override
public void updateDownloadResult(long result){
resultTextView.setText("Downloaded " + result + " bytes");
}
}
可看到加了 receiver
並在 onCreate()
中透過 LocalBroadcastManager
去註冊接收廣播,並不忘在 onDestory()
去取消註冊。
最後只要在 DownloadFilesTask
進行廣播即可:
public class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
protected Long doInBackground(URL... urls) {
//... downloading
return totalSize;
}
protected void onPostExecute(Long result) {
Intent in = new Intent(MainActivity.RESULT_ACTION);
in.putExtra(MainActivity.RESULT_DATA, result);
LocalBroadcastManager.getInstance(context).sendBroadcast(in);
}
}
透過 local broadcast,DownloadFilesTask
跟 MainActivity
之間再沒有直接關係。任何登記接受 RESULT_ACTION
的 class 也會收到
intent` 去做相關的更新工作。
Local Broadcast 的缺點
寫到這裏,程式由一開始的一個檔案,已增加到 4 個了。若要再添一個 AsyncTask
去執行其他工作,再加上相對應的 BroadcastReceiver
和 interface
,又會增加 3 個檔案。怪不得很多人說寫 java 實在太麻煩、太累贅了。
另外,使用 intent 傳送 int
或 long
這類的沒有問題,但若想在 intent
放自行編寫的 class
,一是必須 implements Serializable
(慢),一是 implments Parcelable
(煩),很是麻煩。
當然,以上缺點一早有人想到解決方法: EventBus
最終的改良: EventBus
EventBus is publish/subscribe event bus optimized for Android.
使用 EventBus
原理其實跟 local broadcast 差不多,不過麻煩的東西 EventBus
已經替你處理掉了。DownloadFilesTask
完成後發出 event
,EventBus
再通知需接知此 event
的 class
去執行相關行動。
加進 ResultEvent
用作 AysncTask
工作完成後要通知 MainActivity
的信差:
public class ResultEvent {
private long result;
public ResultEvent(long result){
this.result = result;
}
public long getResult() {
return result;
}
public void setResult(long result) {
this.result = result;
}
}
ResultEvent
只是 POJO,不需要碰麻煩的 Parcelable
。要加減 field 完全沒難度。
將 MainActivity
改寫成
public class MainActivity extends Activity{
TextView resultTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
//....initialize resultTextView
EventBus.getDefault().register(this);
new DownloadFilesTask().execute(url1, url2, url3);
}
@Override
public void onDestroy(){
super.onDestroy();
EventBus.getDefault().unregister(this);
}
public void onEvent(ResultEvent event){
updateDownloadResult(event.getResult());
}
public void updateDownloadResult(long result){
resultTextView.setText("Downloaded " + result + " bytes");
}
}
重點是 onCreate()
中登記 EventBus
,和新增了 onEvent(ResultEvent)
,onEvent(ResultEvent)
只是需要執行 updateDownloadResult()
而已。
DownloadFilesTask.onPostExecute()
只需發出 ResultEvent
即可:
protected void onPostExecute(Long result) {
EventBus.getDefault().post(new ResultEvent(result));
}
相比起使用 broadcast ,code 變得更簡潔易明。萬一 ResultEvent
需要更改,也不用處理惱人的 parcelable
。
可能你會問,為此新加一個 library,不是更麻煩嗎? 我可以答你,使用 EventBus
是為了將來的需要。若像最初的簡單程式,使用最基本的方法當然沒什麼問題,但當你的程式越來越多功能,越來越複雜,使用 EventBus
一定會更簡潔清晰,而更簡潔通常代表更少的 bugs。
總結
AsyncTask
雖然簡單方便,但其實地雷不少,將來總有踩中的一天。用 local broadcast 可以助你避過地雷,但會增加 boilterplate code 。要更方便和支援更大 project 的話,請使用 EventBus
(或 Otto
) 。
簡單來說,不想增加 dependency 又不介意重複寫類似 coding 的話用 local broadcast,想簡潔的話用 EventBus
。