1 概述
1.1 定義
- 一次UI更新
- 一次數(shù)據(jù)庫(kù)中讀寫(xiě)操作
- 上傳或下載一張圖片
- 從網(wǎng)絡(luò)接口獲取數(shù)據(jù) 等等
抽象而言,任何代碼塊執(zhí)行的業(yè)務(wù)邏輯都可稱(chēng)之為一個(gè)任務(wù)。最常見(jiàn)的是封裝在Runable或Callable或Thread子類(lèi)的run方法中的業(yè)務(wù)邏輯。
1.2 任務(wù)在哪里執(zhí)行
- 當(dāng)前線程直接方法調(diào)用
- 新建子線程執(zhí)行
- 提交到線程池執(zhí)行
- 放在handler隊(duì)列依次調(diào)用
即將一個(gè)任務(wù)分派(委托)到一個(gè)線程中執(zhí)行(也可為當(dāng)前線程)
1.3 理想特性
設(shè)想一下,我們最希望任務(wù)具有什么特性
(1)可取消性
Android開(kāi)發(fā)很多是基于組件生命周期回調(diào)的,如Activity,F(xiàn)ragment,Service等都有提供了一系列on***回調(diào)方法。如當(dāng)前界面關(guān)閉,相關(guān)的任務(wù)都需取消。如:
- 取消跟界面相關(guān)的所有網(wǎng)絡(luò)請(qǐng)求
- handler待處理的Message和Callback清空
如不及時(shí)取消,有可能出什么狀況:
- 浪費(fèi)流量和系統(tǒng)資源
- 若網(wǎng)絡(luò)請(qǐng)求響應(yīng)較慢,導(dǎo)致Activity不能及時(shí)銷(xiāo)毀,甚者內(nèi)存泄漏
- 執(zhí)行回調(diào),則容易出現(xiàn)
BadTokenException或NullPointException異常(TODO:代碼驗(yàn)證)
(2)同異步兼?zhèn)?/h5>
若有如下需求:獲取用戶(hù)信息,先從本地?cái)?shù)據(jù)庫(kù)讀取,有則直接返回,沒(méi)有則從網(wǎng)絡(luò)獲取。
同步版本
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);
}
}
異步版
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);
}
}
});
}
}
});
}
很明顯組織同步代碼塊比異步代碼塊簡(jiǎn)易許多,可閱讀性高,且測(cè)試方便,魯棒性強(qiáng)。而異步代碼,不阻塞當(dāng)前線程,最典型的異步行為是:非UI操作分派到子線程去運(yùn)行,執(zhí)行結(jié)果可通過(guò)handler或其他事件通知機(jī)制通知主線程做UI刷新。一個(gè)任務(wù)若能同時(shí)具備同步和異步性,能由調(diào)用者選擇,則是最佳的。
(3)可組合性
一個(gè)UI界面的展示,可能從多個(gè)接口獲取數(shù)據(jù)。如個(gè)人資料頁(yè)由基本用戶(hù)信息和最近動(dòng)態(tài)信息組成。若能將兩個(gè)數(shù)據(jù)接口合并請(qǐng)求,且合并響應(yīng),上層處理邏輯將很簡(jiǎn)易。若一類(lèi)任務(wù)都能自由組合,勢(shì)必快哉。
資料頁(yè) = 用戶(hù)資料接口+個(gè)人動(dòng)態(tài)接口(最近一條)
個(gè)人動(dòng)態(tài)頁(yè) = 個(gè)人動(dòng)態(tài)接口(多屏分頁(yè))
2 實(shí)現(xiàn)
2.1 取消任務(wù)
(1) handler
handler簡(jiǎn)潔易用,維持一個(gè)任務(wù)(消息)隊(duì)列,由一個(gè)工作線程負(fù)責(zé)調(diào)度。post*(*)、send*((*)實(shí)現(xiàn)添加任務(wù)或發(fā)送消息,對(duì)應(yīng)的removeCallbacks(*)、removeMessages(*)實(shí)現(xiàn)移除任務(wù)或消息。使用不當(dāng)就會(huì)導(dǎo)致內(nèi)存泄漏:
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();
}
}
由于非靜態(tài)內(nèi)部類(lèi)帶有外部類(lèi)的引用,故mLeakyHandler和匿名Runnable都帶有SmapleActivity的引用。任務(wù)隊(duì)列中維持著者兩個(gè)內(nèi)部類(lèi)的引用,并計(jì)劃在10分鐘后執(zhí)行,故該Activity最快也是在10分鐘后才能被GC掉。必須明確一點(diǎn),若任務(wù)延遲執(zhí)行的時(shí)間改為1ms,那么就不太算內(nèi)存泄漏?;蛘咴?code>onDestory及時(shí)remove該任務(wù),也不會(huì)內(nèi)存泄漏。個(gè)人認(rèn)為的最佳實(shí)踐是:
- 定義全局的handler,一個(gè)UI相關(guān),一個(gè)非UI相關(guān)
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;
}
}
- 跟當(dāng)前組件(Activity或Fragment)生命周期相關(guān)的任務(wù),及時(shí)去除。
(2) 普通子線程
如何取消(終止)一個(gè)運(yùn)行中的線程?調(diào)用目標(biāo)線程的stop()方法,這種太直接,可能目標(biāo)線程還沒(méi)做好停止前準(zhǔn)備,丟失數(shù)據(jù),目前該方法已經(jīng)廢棄。中斷時(shí)實(shí)現(xiàn)取消的最合適的方式。如下典型的例子:
public class WorkThread extends Thread {
public void run() {
try {
while (!isInterrupted()) {
// 繼續(xù)工作
}
// 退出工作
} catch (InterruptedException e) {
// 退出工作
}
}
public void cancel() {
interrupt();
}
}
調(diào)用cancle()時(shí),如當(dāng)線程為阻塞狀態(tài)(wait,sleep,join或io等待),將立刻拋出InterruptedException;當(dāng)線程在非阻塞態(tài),要么在while判斷中不符合條件而退出,要么遇到下一個(gè)阻塞狀態(tài),拋出InterruptedException。
(3) 線程池
ExecutorService.submit將返回一個(gè)Future來(lái)描述任務(wù)。Future有一個(gè)cancel方法。
public class TaskExecutor {
// .... 加上上面部分實(shí)現(xiàn)
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;
}
}
});
}
}
進(jìn)行取消操作
Future<String> sayHiFuture = TaskExecutor.runInPoolThread(new Callback<String>(){
public String call(){
return "hello word!";
}
});
sayHiFuture.cancle(true); //進(jìn)行取消 cancel(boolean mayInterruptIfRunning)
一個(gè)任務(wù)的狀態(tài)有等待,執(zhí)行中,已完成,已取消,其中已完成包括正常完成的和取消完成的。線程池維持一個(gè)任務(wù)隊(duì)列,按一定的策略進(jìn)行調(diào)度。調(diào)用cancle后,若在對(duì)應(yīng)任務(wù)還在等待中,則順利取消任務(wù),如是在執(zhí)行中,則更加參數(shù)mayInterruptIfRunning決定是否進(jìn)行發(fā)起中斷請(qǐng)求。那重點(diǎn)來(lái)了,如何處理該中斷請(qǐng)求,要看具體的任務(wù),或許被任務(wù)直接忽略,繼續(xù)執(zhí)行完任務(wù)。
結(jié)合上面的小結(jié),不難發(fā)現(xiàn):中斷是一種協(xié)商機(jī)制。當(dāng)前線程發(fā)起中斷請(qǐng)求,目標(biāo)線程需要在特定條件(阻塞)或主動(dòng)去判斷中斷狀態(tài)。是否取消,最終決定權(quán)在目標(biāo)線程。
題外話:不要直接new一個(gè)線程執(zhí)行,最佳方案是由線程池來(lái)調(diào)度
2.2 同異步變化
(1) 同步變異步
基本思路是,讓任務(wù)在非當(dāng)前線程執(zhí)行,看需要是否有必要將執(zhí)行結(jié)果進(jìn)行回調(diào)。如
//同步的
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) 異步變同步
基本思路是,調(diào)用異步的線程一直等待(或規(guī)定時(shí)間),直到有有回調(diào)結(jié)果。如下面典型的例子:
//異步的
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();
}
是否設(shè)置等待時(shí)間或設(shè)置多少?zèng)Q定具體業(yè)務(wù)需求。個(gè)人建議都設(shè)置等待時(shí)間,否則若回調(diào)沒(méi)有調(diào)用,將永遠(yuǎn)阻塞。上面只是異步代碼同步化的一種實(shí)現(xiàn)方案,或許你有更好的方案。
(3) 同異步兼?zhèn)?/h5>
利用線程池的Future.get() 借用 同步變異步的例子
// ... 同步變異步的代碼
// 異步使用方式
getUserInof(1, new Callback<UserInfo>() {
public void onCallback(UserInfo userInfo){
// 處理你的業(yè)務(wù)邏輯
}
});
// 同步使用方式
Future<UserInfo> future = getUserInfo(1, null);
UserInfo userInfo = future.get();
future.get()會(huì)一直阻塞,直達(dá)關(guān)聯(lián)的任務(wù)執(zhí)行完(可能是被取消的)。我們現(xiàn)在來(lái)看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()
// 處理業(yè)務(wù)邏輯
}
});
// call.cancle();//取消任務(wù)
}
我們不細(xì)究OkHttp的實(shí)現(xiàn)細(xì)節(jié),重點(diǎn)是借鑒其封裝任務(wù)的方式。調(diào)用某個(gè)任務(wù),不直接執(zhí)行,而是返回一個(gè)中間對(duì)象Call(類(lèi)似Future),便于對(duì)任務(wù)進(jìn)行控制。如同步執(zhí)行,異步執(zhí)行,取消,執(zhí)行狀態(tài)判斷等等。
2.3 組合任務(wù)
將大的任務(wù),拆分成粒度小的有依賴(lài)關(guān)系或獨(dú)立的子任務(wù)。最近比較火的RxJava就是解決任務(wù)串的利器。這里不細(xì)說(shuō),今后會(huì)分享多包協(xié)議設(shè)計(jì)就是這種思想的實(shí)踐。
總結(jié)
希望我們封裝的任務(wù)是可取消的,可組合的,同時(shí)也兼?zhèn)渫惒??;蛟S很難具備所有特性,但是可取消性是最基本要求。可取消、可取消、可取消。。。。