淺談Android中的任務(wù)封裝

1 概述

1.1 定義

  • 一次UI更新
  • 一次數(shù)據(jù)庫(kù)中讀寫(xiě)操作
  • 上傳或下載一張圖片
  • 從網(wǎng)絡(luò)接口獲取數(shù)據(jù) 等等

抽象而言,任何代碼塊執(zhí)行的業(yè)務(wù)邏輯都可稱(chēng)之為一個(gè)任務(wù)。最常見(jiàn)的是封裝在RunableCallableThread子類(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)BadTokenExceptionNullPointException異常(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很難具備所有特性,但是可取消性是最基本要求。可取消、可取消、可取消。。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容