Android中的異步
android中的應用開發,不像是寫控制台程式,他是一種和UI相關的程式。幾乎所有的UI應用程式都會有這樣的要求:不能在主線程(即UI線程)中做耗時的操作。因為一般情況下,主線程負責處理消息和更新界面。其實更新界面也是基于消息驅動的。
在android裝置上, 我們做的每個操作,比如按下菜單鍵或傳回鍵,或者點選了界面上的一個按鈕,這些事件 都會被封裝成一個消息,發送到主線程的消息隊列中。而主線程監聽在他的消息隊列上, 如果消息隊列中進入了一個消息,那麼主線程便取出這個消息,調用這個消息上的回調方法,如果主線程的消息隊列中沒有消息,那麼主線程便會阻塞在隊列上,直到一個消息的到來。這種消息機制可以用下面的一張圖來解釋(該圖檔來自百度):
從這張圖中可以看到android消息機制的幾個角色:
- MessageQueue:消息隊列。和線程綁定,用于存儲目前線程的消息
- Looper:循環器。和線程綁定,用于控制消息循環。例如在消息隊列為空時阻塞目前線程。
- Message:消息實體。
- Handler:句柄。和線程綁定,用于發送消息,并且負責消息的回調處理。
其實主線程中的所有代碼都是由這種消息機制驅動的。比如我們熟悉的onCreate等回調方法,是架構向該應用程式的主線程的消息隊列中發送了一個消息,然後由主線程基于這個消息,調用onrCreate等回調方法。
如果在主線程中做耗時的操作,比如IO和網絡,那麼主線程就會被長時間的占用,他的消息隊列中還有其他消息就不能被即使處理,導緻應用程式崩潰,這就是著名的ANR(application no response)錯誤。舉個例子,主線程正在從資料庫中讀取大量的資料,這時你點選了界面上的一個按鈕,這個事件被封裝成消息發送到主線程的消息隊列,等待主線程處理,由于主線程正在讀資料,是以這個消息得不到及時的處理。
是以,在安卓應用開發中, 為了避免主線程被阻塞,将耗時的操作放到子線程中是非常重要的。最主要的處理方式是:
- 主線程建立一個Handler對象,這個Handler對象在建立完成後就和主線程綁定在一起,他将消息發送到主線程的消息隊列中,并且負責這個消息的處理。
- 将耗時的操作放到一個新開的子線程中執行,并且傳入主線程的Handler,在子線程執行完畢時,使用這個Handler發送一個消息到主線程的消息隊列
- 主線程的Looper(主線程建立時建立)控制主線程讀取到這個消息
- 主線程執行這個消息上的回調方法(一般情況下會回調Handler中的handleMessage方法)
代碼的形式如下:
Handler handlerMain = new Handler(){
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
// ...
break;
case 2:
// ...
break;
case 3:
// ...
break;
default:
break;
}
};
};
private void downloadFile(){
new Thread(new Runnable() {
@Override
public void run() {
// 在子線程下載下傳檔案
//...
//...
//...
//下載下傳完成,發送通知
Message msg = handlerMain.obtainMessage();
msg.what = 1;
//msg.sendToTarget();發送消息, 也可以這樣寫
handlerMain.sendMessage(msg);
}
}).start();
}
除此之外,為了友善于利用消息機制更新界面,Android特意創造了AsyncTask這個類。這個類的底層也是使用上述的消息機制,隻不過進行了一些封裝而已,此外AsyncTask中還使用了線程池技術。AsyncTask的使用方式如下:
AsyncTask<String, String, String> task = new AsyncTask<String, String, String>(){
@Override
protected String doInBackground(String... params) {
// 在子線程下載下傳檔案
//...
//...
//...
//下載下傳任務完成後, 會自動發送消息
return null;
}
protected void onPostExecute(String result) {
//主線程得到子線程發送的消息後,會回調到這個方法
//該方法在主線程中執行
//處理消息或更新界面
//...
//...
//...
};
};
private void downloadFileAndUpdateUI(){
task.execute(null);
}
較新的android版本中, 還引入了一些用于異步加載的API,這個異步加載的工具其實底層都是利用的Android的消息機制。
異步 or 同步
異步, 顧名思義就是不是同時執行的:這件事我幹不了, 交給你來幹, 你幹完了之後通知我一下, 我再做一些後續工作。這樣你來我往, 各自負責一部分事情, 就達到了異步的效果。 可是, 從上面的代碼可以看出,這種根據消息機制實作的異步, 在代碼上比較混亂, 閱讀時需要跳轉來閱讀, 有時候閱讀一個邏輯還需要跨越好幾個檔案, 也會在同一個檔案的不同地方跳來跳去。 是以, 我們可不可以實作這樣一種邏輯:這件事我幹不了, 交給你來幹,你快點幹,幹完之後也不用通知我了, 我等着你, 你幹完之後, 我再幹其他相關的事情。這是一種同步的機制,适用于執行時間不長的任務。 舉個例子, 在上一篇部落格 Android4.0網絡操作必須放在子線程中中,有一個登入驗證的網絡操作,在4.0中隻能放在子線程中執行,驗證通過後要跳轉到其他界面。要實作跳轉到其他界面, 必須依賴于驗證的結果,而驗證是在子線程中進行的,跳轉必須在主線程中進行,是以就必須使用異步, 再驗證完成之後發送消息到主線程,在主線程中跳轉。 其實這隻是一個非常簡單的http請求, 不會耗費很長時間,其實我們可以在主線程中等待這個操作完成後直接跳轉界面。 在jdk5中的線程并發庫中可以很友善的實作這種線程等待的邏輯。
- 首先建立線程池ExecutorService
- 調用ExecutorService的submit方法,傳入一個任務對象Callable,傳回一個結果Future
- 在目前線程中調用Future對象的get方法, 等待背景任務執行完成傳回結果
代碼如下:
/**
* 登陸驗證, 在主線程中直接調用, 主線程會等待背景線程驗證的結果傳回
* @param context
* @return 驗證成功傳回true, 反之傳回false
*/
public static boolean userLoginCheckWaited(final Context context){
//建立單個線程池, 将驗證的網絡操作放到子線程中
ExecutorService singleTheadPool = Executors.newSingleThreadExecutor();
//将驗證任務送出到線程池中
Future<Boolean> fu = singleTheadPool.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return ESDKUtils.userLoginCheck1(context);
}
});
try {
return fu.get(); //等待驗證結果的傳回
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 網絡操作放到背景線程中執行
*/
private static boolean userLoginCheck1( Context context){
//設定登入驗證的各項參數
List<BasicNameValuePair> params = new LinkedList<BasicNameValuePair>();
params.add(new BasicNameValuePair("yhid", userName));
params.add(new BasicNameValuePair("yhkl", passwd));
params.add(new BasicNameValuePair("sbid", DeviceTool.getDeviceId(context)));
params.add(new BasicNameValuePair("clientIp", IPTool.getPsdnIp()));
params.add(new BasicNameValuePair("ywxtbm", "BGPTNEW"));
params.add(new BasicNameValuePair("ywxtmc", "辦公平台更新"));
URL url = null;
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try{
//設定url位址
url = new URL(URLConstant.USER_LOGIN_CHEAK_ADDRESS);
String paramString = URLEncodedUtils.format(params, "GBK"); //請求參數編碼為GBK
byte[] dataToSend = paramString.getBytes(); //post請求中的實體資料
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
這樣的話, 可以直接在主線程中調用userLoginCheckWaited方法, 而不用再寫異步相關的代碼, 可以使代碼大大簡化。調用代碼如下:
//開始業務登陸驗證, 驗證使用者名和密碼的正确性,在主線程直接調用
if(ESDKUtils.userLoginCheckWaited(this)){
//跳轉到界面
}
我們還要明白一點, fu.get()方法是會阻塞的, 它等待背景任務的完成。是以要注意,在主線程中調用時, 它同樣也會阻塞主線程。是以這種方式隻适用于耗時很少的方法,比如驗證登陸隻是一個http請求,并且資料量很少,可以使用這種方法。如果是上傳下載下傳檔案這類的操作,就不能使用這種方式了。