淺談Android中的任務封裝

1 概述

1.1 定義

  • 一次UI更新
  • 一次數據庫中讀寫操做
  • 上傳或下載一張圖片
  • 從網絡接口獲取數據 等等

抽象而言,任何代碼塊執行的業務邏輯均可稱之爲一個任務。最多見的是封裝在RunableCallableThread子類的run方法中的業務邏輯。數據庫

1.2 任務在哪裏執行

  • 當前線程直接方法調用
  • 新建子線程執行
  • 提交到線程池執行
  • 放在handler隊列依次調用

即將一個任務分派(委託)到一個線程中執行(也可爲當前線程)網絡

1.3 理想特性

設想一下,咱們最但願任務具備什麼特性異步

(1)可取消性

Android開發不少是基於組件生命週期回調的,如Activity,Fragment,Service等都有提供了一系列on***回調方法。如當前界面關閉,相關的任務都需取消。如:ide

  • 取消跟界面相關的全部網絡請求
  • handler待處理的Message和Callback清空

如不及時取消,有可能出什麼情況:oop

  • 浪費流量和系統資源
  • 若網絡請求響應較慢,致使Activity不能及時銷燬,甚者內存泄漏
  • 執行回調,則容易出現BadTokenExceptionNullPointException異常(TODO:代碼驗證)
(2)同異步兼備

如有以下需求:獲取用戶信息,先從本地數據庫讀取,有則直接返回,沒有則從網絡獲取。post

同步版本測試

UserInfo getUserInfoFromDb(long uid){
	...
}

UserInfo getUserInfoFromNet(long uid){
	...
}

UserInfo getUserInfo(long uid){
	UserInfo userInfo = getUserInfoFromDb(uid);
	if(userInfo!=null){
		return userInfo;
	}else{
		return getUserInfoFromNet(uid);
	}
}

異步版ui

public static interface Callback {
	public void onCallback(UserInfo userInfo);
}

void getUserInfoFromDb(long uid, Callback callback) {
	...
}

void getUserInfoFromNet(long uid, Callback callback) {
	...
}

void getUserInfo(long uid, final Callback callback) {
	getUserInfoFromDb(uid, new Callback() {
		public void onCallback(UserInfo userInfo) {
			if (userInfo != null) {
				if (callback != null) {
					callback.onCallback(userInfo);
				}
			} else {
				getUserInfoFromNet(uid, new Callback() {
					public void onCallback(UserInfo userInfo) {
						if (callback != null) {
							callback.onCallback(userInfo);
						}
					}
				});
			}
		}
	});
}

很明顯組織同步代碼塊比異步代碼塊簡易許多,可閱讀性高,且測試方便,魯棒性強。而異步代碼,不阻塞當前線程,最典型的異步行爲是:非UI操做分派到子線程去運行,執行結果可經過handler或其餘事件通知機制通知主線程作UI刷新。一個任務若能同時具有同步和異步性,能由調用者選擇,則是最佳的。url

(3)可組合性

一個UI界面的展現,可能從多個接口獲取數據。如我的資料頁由基本用戶信息和最近動態信息組成。若能將兩個數據接口合併請求,且合併響應,上層處理邏輯將很簡易。若一類任務都能自由組合,勢必快哉。線程

資料頁 = 用戶資料接口+我的動態接口(最近一條)
我的動態頁 = 我的動態接口(多屏分頁)

2 實現

2.1 取消任務

(1) handler

handler簡潔易用,維持一個任務(消息)隊列,由一個工做線程負責調度。post*(*)send*((*)實現添加任務或發送消息,對應的removeCallbacks(*)removeMessages(*)實現移除任務或消息。使用不當就會致使內存泄漏:

public class SampleActivity extends Activity {
  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);
    // Go back to the previous Activity.
    finish();
  }
}

因爲非靜態內部類帶有外部類的引用,故mLeakyHandler和匿名Runnable都帶有SmapleActivity的引用。任務隊列中維持着者兩個內部類的引用,並計劃在10分鐘後執行,故該Activity最快也是在10分鐘後才能被GC掉。必須明確一點,若任務延遲執行的時間改成1ms,那麼就不太算內存泄漏。或者在onDestory及時remove該任務,也不會內存泄漏。我的認爲的最佳實踐是:

  • 定義全局的handler,一個UI相關,一個非UI相關
public class TaskExecutor {
	private static Handler uiHandler = new Handler(Looper.getMainLooper());
	private static Handler workHandler = null;
	private static Looper wordLooper = null;

	static {
		HandlerThread workThread = new HandlerThread("workThread");
		workThred.start();
		workLooper = workThread.getLooper();
		workHandler = new Handler(workLooper);
	}

	public static Handler uiHandler() {
		return uiHandler;
	}

	public static Handler workHandler() {
		return workHandler;
	}
}
  • 跟當前組件(Activity或Fragment)生命週期相關的任務,及時去除。
(2) 普通子線程

如何取消(終止)一個運行中的線程?調用目標線程的stop()方法,這種太直接,可能目標線程還沒作好中止前準備,丟失數據,目前該方法已經廢棄。中斷時實現取消的最合適的方式。以下典型的例子:

public class WorkThread extends Thread {
	public void run() {
		try {
			while (!isInterrupted()) {
				// 繼續工做
			}
			// 退出工做
		} catch (InterruptedException e) {
			// 退出工做
		}
	}

	public void cancel() {
		interrupt();
	}
}

調用cancle()時,如當線程爲阻塞狀態(wait,sleep,join或io等待),將馬上拋出InterruptedException;當線程在非阻塞態,要麼在while判斷中不符合條件而退出,要麼遇到下一個阻塞狀態,拋出InterruptedException

(3) 線程池

ExecutorService.submit將返回一個Future來描述任務。Future有一個cancel方法。

public class TaskExecutor {
	// .... 加上上面部分實現

	private static ExecutorService executor = Executors.newCachedThreadPool();

	public static ExecutorService executor() {
		return executor;
	}

	public static Future<?> runInPoolThread(final Runnable task) {
		return executor.submit(new Runnable() {
			@Override
			public void run() {
				try {
					task.run();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	public static <V> Future<V> runInPoolThread(final Callable<V> task) {
		return executor.submit(new Callable<V>() {
			@Override
			public V call() {
				try {
					return task.call();
				} catch (Exception e) {
					e.printStackTrace();
					return null;
				}
			}
		});
	}
}

進行取消操做

Future<String> sayHiFuture = TaskExecutor.runInPoolThread(new Callback<String>(){
	public String call(){
		return "hello word!";
	}
});

sayHiFuture.cancle(true); //進行取消 cancel(boolean mayInterruptIfRunning)

一個任務的狀態有等待執行中已完成已取消,其中已完成包括正常完成的和取消完成的。線程池維持一個任務隊列,按必定的策略進行調度。調用cancle後,若在對應任務還在等待中,則順利取消任務,如是在執行中,則更加參數mayInterruptIfRunning決定是否進行發起中斷請求。那重點來了,如何處理該中斷請求,要看具體的任務,或許被任務直接忽略,繼續執行完任務。

結合上面的小結,不難發現:中斷是一種協商機制。當前線程發起中斷請求,目標線程須要在特定條件(阻塞)或主動去判斷中斷狀態。是否取消,最終決定權在目標線程。

題外話:不要直接new一個線程執行,最佳方案是由線程池來調度

2.2 同異步變化

(1) 同步變異步

基本思路是,讓任務在非當前線程執行,看須要是否有必要將執行結果進行回調。如

//同步的
UserInfo getUserInfo(long uid){
	.... 
}
//改成異步
Future<UserInfo> getUserInfo(final long uid, final Callback<UserInfo> callback){
	return TaskExecutor.runInPoolThread(new Callback<UserInfo>(){
		public String call(){
			UserInfo userInfo = getUserInfo(uid);
			if(callback!=null){
				callback.onCallback(userInfo);
			}
			return userInfo;
		}
	});
}
(2) 異步變同步

基本思路是,調用異步的線程一直等待(或規定時間),直到有有回調結果。以下面典型的例子:

//異步的
void getUserInfo (long uid, Callback<UserInfo> callback) {
}

//改成同步的
UserInfo getUserInfo (long uid) {
	final CountDownLatch latch = new CountDownLatch(1);
	final AtomicReference<UserInfo> resultRef = new AtomicReference<UserInfo>();
	getUserInfo (uid, new Callback<UserInfo>() {
		public void onCallback(UserInfo userInfo){
			try {
				resultRef.set(userInfo);
			} finally {
				latch.countDown();
			}
		}
	});
	try {
		latch.await(10, TimeUnit.SECONDS); //最多等待10秒
	} catch(){
		//等待中斷
	}
	return resultRef.get();
}

是否設置等待時間或設置多少決定具體業務需求。我的建議都設置等待時間,不然若回調沒有調用,將永遠阻塞。上面只是異步代碼同步化的一種實現方案,或許你有更好的方案。

(3) 同異步兼備

利用線程池的Future.get() 借用 同步變異步的例子

// ... 同步變異步的代碼
	// 異步使用方式
	getUserInof(1, new Callback<UserInfo>() {
		public void onCallback(UserInfo userInfo){
			// 處理你的業務邏輯
		}
	});	
	
	// 同步使用方式
	Future<UserInfo> future = getUserInfo(1, null);
	UserInfo userInfo = future.get();

future.get()會一直阻塞,直達關聯的任務執行完(多是被取消的)。咱們如今來看OkHttp的封裝方式

OkHttpClient client = new OkHttpClient();

//同步版
String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();
      
  Call call = client.newCall(request)
  Response response = call.execute();
  return response.body().string();
}

// 異步版
void run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();
      
  Call call = client.newCall(request)
  call.enqueue(new Callback() {
	    @Override
	    public void onFailure(Request request, IOException e) {
	
	    }
	
	    @Override
	    public void onResponse(Response response) throws IOException {
			String res = response.body().string()
			// 處理業務邏輯
	    }
	});
	
	// call.cancle();//取消任務
}

咱們不細究OkHttp的實現細節,重點是借鑑其封裝任務的方式。調用某個任務,不直接執行,而是返回一箇中間對象Call(相似Future),便於對任務進行控制。如同步執行,異步執行,取消,執行狀態判斷等等。

2.3 組合任務

將大的任務,拆分紅粒度小的有依賴關係或獨立的子任務。最近比較火的RxJava就是解決任務串的利器。這裏不細說,從此會分享多包協議設計就是這種思想的實踐。

總結

但願咱們封裝的任務是可取消的,可組合的,同時也兼備同異步。或許很難具有全部特性,可是可取消性是最基本要求。可取消可取消可取消。。。。

相關文章
相關標籤/搜索