天天看點

關于Android中的消息機制和異步

Android中的異步

android中的應用開發,不像是寫控制台程式,他是一種和UI相關的程式。幾乎所有的UI應用程式都會有這樣的要求:不能在主線程(即UI線程)中做耗時的操作。因為一般情況下,主線程負責處理消息和更新界面。其實更新界面也是基于消息驅動的。

在android裝置上, 我們做的每個操作,比如按下菜單鍵或傳回鍵,或者點選了界面上的一個按鈕,這些事件 都會被封裝成一個消息,發送到主線程的消息隊列中。而主線程監聽在他的消息隊列上, 如果消息隊列中進入了一個消息,那麼主線程便取出這個消息,調用這個消息上的回調方法,如果主線程的消息隊列中沒有消息,那麼主線程便會阻塞在隊列上,直到一個消息的到來。這種消息機制可以用下面的一張圖來解釋(該圖檔來自百度):

關于Android中的消息機制和異步

從這張圖中可以看到android消息機制的幾個角色:

  • MessageQueue:消息隊列。和線程綁定,用于存儲目前線程的消息
  • Looper:循環器。和線程綁定,用于控制消息循環。例如在消息隊列為空時阻塞目前線程。
  • Message:消息實體。
  • Handler:句柄。和線程綁定,用于發送消息,并且負責消息的回調處理。

其實主線程中的所有代碼都是由這種消息機制驅動的。比如我們熟悉的onCreate等回調方法,是架構向該應用程式的主線程的消息隊列中發送了一個消息,然後由主線程基于這個消息,調用onrCreate等回調方法。

如果在主線程中做耗時的操作,比如IO和網絡,那麼主線程就會被長時間的占用,他的消息隊列中還有其他消息就不能被即使處理,導緻應用程式崩潰,這就是著名的ANR(application no response)錯誤。舉個例子,主線程正在從資料庫中讀取大量的資料,這時你點選了界面上的一個按鈕,這個事件被封裝成消息發送到主線程的消息隊列,等待主線程處理,由于主線程正在讀資料,是以這個消息得不到及時的處理。

是以,在安卓應用開發中, 為了避免主線程被阻塞,将耗時的操作放到子線程中是非常重要的。最主要的處理方式是:

  1. 主線程建立一個Handler對象,這個Handler對象在建立完成後就和主線程綁定在一起,他将消息發送到主線程的消息隊列中,并且負責這個消息的處理。
  2. 将耗時的操作放到一個新開的子線程中執行,并且傳入主線程的Handler,在子線程執行完畢時,使用這個Handler發送一個消息到主線程的消息隊列
  3. 主線程的Looper(主線程建立時建立)控制主線程讀取到這個消息
  4. 主線程執行這個消息上的回調方法(一般情況下會回調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中的線程并發庫中可以很友善的實作這種線程等待的邏輯。

  1. 首先建立線程池ExecutorService
  2. 調用ExecutorService的submit方法,傳入一個任務對象Callable,傳回一個結果Future
  3. 在目前線程中調用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請求,并且資料量很少,可以使用這種方法。如果是上傳下載下傳檔案這類的操作,就不能使用這種方式了。