Android的同步對話方塊(AlertDialog模態對話方塊返回值實現原理)
最近做畢業設計,在抽象層次上需要做一些統一的可複用介面互動方法,比如對話方塊。具體需求是通過呼叫一個方法,這個方法體中生成一個對話方塊與使用者互動,等與使用者互動完畢後將使用者輸入的資訊返回,用虛擬碼來體現,結構大致如下:
[mw_shl_code=java,true]public Object getXXXByDialog(){
Object result;
result = showDialog();//顯示一個對話方塊與使用者互動,並返回使用者輸入的資訊
return result;//返回使用者輸入的資訊
}[/mw_shl_code]
很容易理解的結果,但是實現起來很麻煩,因為遇到一個同步和非同步機制的問題。
在Android中,啟動一個activity,serivice,對話方塊等等這些元件都是採用非同步的機制(通過訊息迴圈和訊息佇列)。也就是在上面的程式碼中,執行showDialog方法顯示一個對話方塊後,不等對話方塊將使用者輸入的資訊返回,showDialog下一行的return就會馬上執行。所以在上面的虛擬碼結構中,return返回的結果永遠都是null。整個過程用圖形來表示大致如下,如圖:
當使用showDialog方法後,實際上就是向訊息佇列中傳送訊息,要求啟動對話方塊。訊息傳送完了之後就繼續執行showDialog後面的程式碼,對話方塊什麼時候出現取決於對話方塊的訊息處理的時候。因為處理的很快,所以就好像是showDialog呼叫後就直接顯示出了對話方塊,但實際上showDialog後面的程式碼已經執行了。
解決這個問題的思路就是想辦法讓對話方塊顯示,並且使用者輸入資訊後把對話方塊結束了再執行return方法,也就是讓它們同步。因此,先簡單瞭解一下執行緒和訊息迴圈。
在Android中,當啟動一個程式的時候,系統會先為這個程式啟動一個主執行緒並且為這個執行緒建立一個Looper,這個Looper就是管理主執行緒訊息迴圈訊息佇列的一個物件,它包含了一個訊息佇列。實際上,Activity、Serivice這些元件的啟動也是通過非同步機制來實現的,同樣是通過向主執行緒的這個訊息佇列中傳送訊息,等到訊息被處理程式處理解析後才會建立Activity、Service這些元件。非同步機制的好處大概就是可以提高程式的併發性和響應性等等。
與訊息佇列相關的還有怎麼向訊息佇列傳送訊息的問題。向訊息佇列中傳送訊息還涉及到另一個大名鼎鼎的類Handler,這個類可以看做是訊息佇列的工具類,用於向訊息佇列中新增訊息(sendMessage等方法),以及為訊息處理環節提供具體處理程式碼(重寫Handler類的handleMessage方法)。一個Handler例項會繫結到一個執行緒的Looper,繫結後就可以通過Handler向Looper的訊息佇列中傳送訊息和提供處理程式碼。使用預設的Handler構造方法構造的Handler例項會自動繫結到當前執行緒擁有的Looper(一般常說Handler要在主執行緒中建立的原因就是因為主執行緒擁有一個Looper,不是每個執行緒擁有Looper,在沒有Looper的執行緒中建立Handler將會丟擲異常)。
簡單瞭解了非同步的一些基礎東西后,回到真題。對話方塊是一個UI,所以執行在主執行緒上面,showDialog方法也就是向主執行緒的Looper訊息佇列中傳送訊息。Looper類提供了getMainLooper()靜態方法用於獲取主執行緒的Looper。另外Looper還提供了另一個靜態方法loop(),這個方法內是一個死迴圈,用於立即從訊息佇列中獲取訊息進行處理,當沒有訊息可以處理時,這個迴圈就是掛起,等到有新的訊息出現時再繼續迴圈。官方文件上說使用loop()方法後,loop方法後面的程式碼將不會執行,直到呼叫了Looper的另一個方法quit()才會結束死迴圈繼續後續程式碼。所以,根據這個資訊來編寫同步對話方塊。
先來看一下我根據這個思路抽象出來的一個同步對話方塊抽象類:
[mw_shl_code=java,true]import android.app.Dialog;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
public abstract class SynDialog extends Dialog {
private Handler mHandler;
protected Object result;
public SynDialog(Context context){
super(context);
onCreate();
}
public abstract void onCreate();
/**
* 結束對話方塊,將觸發返回result物件
*/
public void finishDialog(){
dismiss();
mHandler.sendEmptyMessage(0);
}
static class SynHandler extendsHandler{
@Override
public voidhandleMessage(Message msg) {
throw newRuntimeException();
}
}
/**
* 顯示同步對話方塊
* @return
*/
public Object showDialog() {
super.show();
try {
Looper.getMainLooper();
mHandler = newSynHandler();
Looper.loop();
} catch (Exception e) {
}
return result;
}
}
[/mw_shl_code]
這個抽象類繼承了Dialog,繼承這個抽象類實現自己具體的同步對話方塊類,然後構造出例項呼叫showDialog就可以顯示同步對話方塊(在對話方塊結束之前不會return result)。繼承的時候需要實現這個抽象類的onCreate方法,在onCreate方法中設定你自己的對話方塊介面。finishDialog方法用於結束這個同步對話方塊,也就是關閉結束對話方塊的時候一定要呼叫這個方法才會讓result返回。
大致解釋下原理:繼承後在onCreate中實現自定義對話方塊介面,顯示對話方塊要呼叫showDialog方法。進入showDialog方法,首先執行super.show()方法傳送訊息要求顯示對話方塊,此時還是因為非同步機制,訊息傳送出去後繼續執行下面的程式碼,進入try-catch模組。先用Looper.getMainLooper()方法獲取主執行緒的Looper物件,再建立Handler物件,也就是將建立的Handler物件和主執行緒的Looper繫結。再往下執行Looper.loop()方法,關鍵就是在這裡了,此時是主執行緒,loop方法後進入訊息死迴圈處理,後續程式碼暫停不再繼續執行下去,return自然沒有執行。等到使用者在對話方塊中輸入資訊後呼叫finishDialog方法,finishDialog先用dismiss關閉對話方塊,在用mHandler傳送一條空訊息進入訊息佇列。訊息佇列接受到訊息用,從mHandler的handleMessage方法中獲取處理程式碼。在這裡,處理程式碼僅僅是丟擲一個執行時異常。這個異常一丟擲就會被loop()所在的try-catch捕捉,然後進入catch,loop()方法也就退出了。異常catch之後,後續程式碼也就繼續執行下去,return也就被執行將result返回。這樣就實現了呼叫showDialog後,對話方塊沒有結束,就不會返回結果的同步假象。
至於為什麼要採用拋異常的方式,Looper有提供一個quit退出loop的方法,為什麼不直接呼叫這個quit方法?遺憾的是,主執行緒的Looper很特殊,不能quit,一quit也會丟擲異常。我試過另外建立一個執行緒並賦給一個新的Looper,但由於對話方塊執行在UI執行緒,實行起來也很麻煩,反而這種拋異常的方法最簡潔,雖然心裡上不太能接受這種奇怪的做法。
這裡有一個問題,在呼叫showDialog後,後續的程式碼沒有繼續執行,相當於是阻塞的樣子。按照android的機制,超時會丟擲ANR超時異常,但是在我的測試中並沒有丟擲ANR。找了很多ANR的資料,我自己猜測的結論是loop方法雖然是個死迴圈,但是它在訊息佇列中訊息空的時候會掛起睡眠,沒有一直佔用CPU等資源,也就沒丟擲ANR。
好了,同步對話方塊就介紹到這裡,折騰了一天,查了很多資料,結論多少有些個人的理解和猜想,如果有不對的地方還請指正。
主要參考了以下文章:
找到一個在Android上建立阻塞式模態對話方塊的方法http://blog.csdn.net/winux/article/details/6269687
Android應用程式訊息處理機制(Looper、Handler)分析http://blog.csdn.net/luoshengyang/article/details/6817933?reload
強烈跟大家推薦下面這個老羅的部落格,討論的東西深入到很多原始碼,值得膜拜的大神。