app開發中的異步處理(三)

本文是系列文章《Android和iOS開發中的異步處理》的第三篇。在本篇文章中,我們主要討論在執行多個異步任務的時候可能碰到的相關問題。

通常我們都需要執行多個異步任務,使它們相互協作來完成需求。本文結合典型的應用場景,講解異步任務的三種協作關係:

  • 先後接續執行
  • 併發執行,結果合併
  • 併發執行,一方優先

以上三種協作關係,本文分別以三種應用場景爲例展開討論。這三種應用場景分別是:

  • 多級緩存
  • 併發網絡請求
  • 頁面緩存

最後,本文還會嘗試給出一個使用RxJava這樣的框架來實現“併發網絡請求”的案例,並進行相關的探討。

注:本系列文章中出現的代碼已經整理到GitHub上(持續更新),代碼庫地址爲:

其中,當前這篇文章中出現的Java代碼,位於com.zhangtielei.demos.async.programming.multitask這個package中。


多個異步任務先後接續執行

“先後接續執行”指的是一個異步任務先啓動執行,待執行完成結果回調發生後,再啓動下一個異步任務。這是多個異步任務最簡單的一種協作方式。

一個典型的例子是靜態資源的多級緩存,其中最爲大家所喜聞樂見的例子就是靜態圖片的多級緩存。通常在客戶端加載一個靜態圖片,都會至少有兩級緩存:第一級Memory Cache和第二級Disk Cache。整個加載流程如下:

  1. 先查找Memory Cache,如果命中,則直接返回;否則,執行下一步
  2. 再查找Disk Cache,如果命中,則直接返回;否則,執行下一步
  3. 發起網絡請求,下載和解碼圖片文件。

通常,第1步查找Memory Cache是一個同步任務。而第2步和第3步都是異步任務,對於同一個圖片加載任務來說,這兩步之間便是“先後接續執行”的關係:“查找Disk Cache”的異步任務完成後(發生結果回調),根據緩存命中的結果再決定要不要啓動“發起網絡請求” 的異步任務。

下面我們就用代碼展示一下“查找Disk Cache”和“發起網絡請求”這兩個異步任務的啓動和執行情況。

首先,我們需要先定義好“Disk Cache”和“網絡請求”這兩個異步任務的接口。

public interface ImageDiskCache {
    /**
     * 異步獲取緩存的Bitmap對象.
     * @param key
     * @param callback 用於返回緩存的Bitmap對象
     */
    void getImage(String key, AsyncCallback<Bitmap> callback);
    /**
     * 保存Bitmap對象到緩存中.
     * @param key
     * @param bitmap 要保存的Bitmap對象
     * @param callback 用於返回當前保存操作的結果是成功還是失敗.
     */
    void putImage(String key, Bitmap bitmap, AsyncCallback<Boolean> callback);
}

ImageDiskCache接口用於存取圖片的Disk Cache,其中參數中的AsyncCallback,是一個通用的異步回調接口的定義。其定義代碼如下(本文後面還會用到):

/**
 * 一個通用的回調接口定義. 用於返回一個參數.
 * @param <D> 異步接口返回的參數數據類型.
 */
public interface AsyncCallback <D> {
    void onResult(D data);
}

而發起網絡請求下載圖片文件,我們直接調用上一篇文章《Android和iOS開發中的異步處理(二)——異步任務的回調》中介紹的Downloader接口(注:採用最後帶有contextData參數的那一版本的Dowanloder接口)。

這樣,“查找Disk Cache”和“發起網絡下載請求”的代碼示例如下:

    //檢查二級緩存: disk cache
    imageDiskCache.getImage(url, new AsyncCallback<Bitmap>() {
        @Override
        public void onResult(Bitmap bitmap) {
            if (bitmap != null) {
                //disk cache命中, 加載任務提前結束.
                imageMemCache.putImage(url, bitmap);
                successCallback(url, bitmap, contextData);
            }
            else {
                //兩級緩存都沒有命中, 調用下載器去下載
                downloader.startDownload(url, getLocalPath(url), contextData);
            }
        }
    });

Downloader的成功結果回調的實現代碼示例如下:

    @Override
    public void downloadSuccess(final String url, final String localPath, final Object contextData) {
        //解碼圖片, 是個耗時操作, 異步來做
        imageDecodingExecutor.execute(new Runnable() {
            @Override
            public void run() {
                final Bitmap bitmap = decodeBitmap(new File(localPath));
                //重新調度回主線程
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (bitmap != null) {
                            imageMemCache.putImage(url, bitmap);
                            imageDiskCache.putImage(url, bitmap, null);
                            successCallback(url, bitmap, contextData);
                        }
                        else {
                            //解碼失敗
                            failureCallback(url, ImageLoaderListener.BITMAP_DECODE_FAILED, contextData);
                        }
                    }
                });
            }
        });
    }

多個異步任務併發執行,結果合併

“併發執行,結果合併”,指的是同時啓動多個異步任務,它們同時併發地執行,等到它們全部執行完成的時候,再合併所有執行結果一起做後續處理。

一個典型的例子是,同時發起多個網絡請求(即遠程API接口),等獲得所有請求的返回數據之後,再將數據一併處理,更新UI。這樣的做法通過併發網絡請求縮短了總的請求時間。

我們根據最簡單的兩個併發網絡請求的情況來給出示例代碼。

首先,還是要先定義好需要的異步接口,即遠程API接口的定義。

/**
 * Http服務請求接口.
 */
public interface HttpService {
    /**
     * 發起HTTP請求.
     * @param apiUrl 請求URL
     * @param request 請求參數(用Java Bean表示)
     * @param listener 回調監聽器
     * @param contextData 透傳參數
     * @param <T> 請求Model類型
     * @param <R> 響應Model類型
     */
    <T, R> void doRequest(String apiUrl, T request, HttpListener<? super T, R> listener, Object contextData);
}

/**
 * 監聽Http服務的監聽器接口.
 *
 * @param <T> 請求Model類型
 * @param <R> 響應Model類型
 */
public interface HttpListener <T, R> {
    /**
     * 產生請求結果(成功或失敗)時的回調接口.
     * @param apiUrl 請求URL
     * @param request 請求Model
     * @param result 請求結果(包括響應或者錯誤原因)
     * @param contextData 透傳參數
     */
    void onResult(String apiUrl, T request, HttpResult<R> result, Object contextData);
}

需要注意的是: 在HttpService這個接口定義中,請求參數request使用Generic類型T來定義。如果這個接口有一個實現,那麼在實現代碼中應該會根據實際傳入的request的類型(它可以是任意Java Bean),利用反射機制將其變換成Http請求參數。當然,我們在這裏只討論接口,具體實現不是這裏要討論的重點。

而返回結果參數result,是HttpResult類型,這是爲了讓它既能表達成功的響應結果,也能表達失敗的響應結果。HttpResult的定義代碼如下:

/**
 * HttpResult封裝Http請求的結果.
 *
 * 當服務器成功響應的時候, errorCode = SUCCESS, 且服務器的響應轉換成response;
 * 當服務器未能成功響應的時候, errorCode != SUCCESS, 且response的值無效.
 *
 * @param <R> 響應Model類型
 */
public class HttpResult <R> {
    /**
     * 錯誤碼定義
     */
    public static final int SUCCESS = 0;//成功
    public static final int REQUEST_ENCODING_ERROR = 1;//對請求進行編碼發生錯誤
    public static final int RESPONSE_DECODING_ERROR = 2;//對響應進行解碼發生錯誤
    public static final int NETWORK_UNAVAILABLE = 3;//網絡不可用
    public static final int UNKNOWN_HOST = 4;//域名解析失敗
    public static final int CONNECT_TIMEOUT = 5;//連接超時
    public static final int HTTP_STATUS_NOT_OK = 6;//下載請求返回非200
    public static final int UNKNOWN_FAILED = 7;//其它未知錯誤

    private int errorCode;
    private String errorMessage;
    /**
     * response是服務器返回的響應.
     * 只有當errorCode = SUCCESS, response的值纔有效.
     */
    private R response;

    public int getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(int errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public R getResponse() {
        return response;
    }

    public void setResponse(R response) {
        this.response = response;
    }
}

HttpResult也包含一個Generic類型R,它就是請求成功時返回的響應參數類型。同樣,在HttpService可能的實現中,應該會再次利用反射機制將請求返回的響應內容(可能是個Json串)變換成類型R(它可以是任意Java Bean)。

好了,現在有了HttpService接口,我們便能演示如何同時發送兩個網絡請求了。

public class MultiRequestsDemoActivity extends AppCompatActivity {
    private HttpService httpService = new MockHttpService();
    /**
     * 緩存各個請求結果的Map
     */
    private Map<String, Object> httpResults = new HashMap<String, Object>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multi_requests_demo);

        //同時發起兩個異步請求
        httpService.doRequest("http://...", new HttpRequest1(),
                new HttpListener<HttpRequest1, HttpResponse1>() {
                    @Override
                    public void onResult(String apiUrl,
                                         HttpRequest1 request,
                                         HttpResult<HttpResponse1> result,
                                         Object contextData) {
                        //將請求結果緩存下來
                        httpResults.put("request-1", result);
                        if (checkAllHttpResultsReady()) {
                            //兩個請求都已經結束
                            HttpResult<HttpResponse1> result1 = result;
                            HttpResult<HttpResponse2> result2 = (HttpResult<HttpResponse2>) httpResults.get("request-2");
                            if (checkAllHttpResultsSuccess()) {
                                //兩個請求都成功了
                                processData(result1.getResponse(), result2.getResponse());
                            }
                            else {
                                //兩個請求並未完全成功, 按失敗處理
                                processError(result1.getErrorCode(), result2.getErrorCode());
                            }
                        }
                    }
                },
                null);
        httpService.doRequest("http://...", new HttpRequest2(),
                new HttpListener<HttpRequest2, HttpResponse2>() {
                    @Override
                    public void onResult(String apiUrl,
                                         HttpRequest2 request,
                                         HttpResult<HttpResponse2> result,
                                         Object contextData) {
                        //將請求結果緩存下來
                        httpResults.put("request-2", result);
                        if (checkAllHttpResultsReady()) {
                            //兩個請求都已經結束
                            HttpResult<HttpResponse1> result1 = (HttpResult<HttpResponse1>) httpResults.get("request-1");
                            HttpResult<HttpResponse2> result2 = result;
                            if (checkAllHttpResultsSuccess()) {
                                //兩個請求都成功了
                                processData(result1.getResponse(), result2.getResponse());
                            }
                            else {
                                //兩個請求並未完全成功, 按失敗處理
                                processError(result1.getErrorCode(), result2.getErrorCode());
                            }
                        }
                    }
                },
                null);
    }

    /**
     * 檢查是否所有請求都有結果了
     * @return
     */
    private boolean checkAllHttpResultsReady() {
        int requestsCount = 2;
        for (int i = 1; i <= requestsCount; i++) {
            if (httpResults.get("request-" + i) == null) {
                return false;
            }
        }
        return true;
    }

    /**
     * 檢查是否所有請求都成功了
     * @return
     */
    private boolean checkAllHttpResultsSuccess() {
        int requestsCount = 2;
        for (int i = 1; i <= requestsCount; i++) {
            HttpResult<?> result = (HttpResult<?>) httpResults.get("request-" + i);
            if (result == null || result.getErrorCode() != HttpResult.SUCCESS) {
                return false;
            }
        }
        return true;
    }

    private void processData(HttpResponse1 data1, HttpResponse2 data2) {
        //TODO: 更新UI, 展示請求結果. 省略此處代碼
    }

    private void processError(int errorCode1, int errorCode2) {
        //TODO: 更新UI,展示錯誤. 省略此處代碼
    }
}

我們首先要等兩個請求全部都完成了,才能將它們的結果進行合併。而爲了判斷兩個異步請求是否全部完成了,我們需要在任一個請求回調時都去判斷所有請求是否已經返回。這裏需要注意的是,之所以我們能採取這樣的判斷方法,有一個很重要的前提:HttpService的onResult已經調度到主線程執行。我們在上一篇文章《Android和iOS開發中的異步處理(二)——異步任務的回調》中“回調的線程模型”一節,對回調發生的線程環境已經進行過討論。在onResult已經調度到主線程執行的前提下,兩個請求的onResult回調順序只能有兩種情況:先執行第一個請求的onResult再執行第二個請求的onResult;或者先執行第二個請求的onResult再執行第一個請求的onResult。不管是哪種順序,上面代碼中onResult內部的判斷都是有效的。

然而,如果HttpService的onResult在不同的線程上執行,那麼兩個請求的onResult回調就可能交叉執行,那麼裏面的各種判斷也會有同步問題。

相比前面講過的“先後接續執行”,這裏的併發執行顯然帶來了不小的複雜度。如果不是對併發帶來的性能提升有特別強烈的需求,也許我們更願意選擇“先後接續執行”的協作關係,讓代碼邏輯保持簡單易懂。

多個異步任務併發執行,一方優先

“併發執行,一方優先”,指的是同時啓動多個異步任務,它們同時併發地執行,但不同的任務卻有不同的優先級,任務執行結束時,優先採用高優先級的任務返回的結果。如果高優先級的任務先執行結束了,那麼後執行完的低優先級任務就被忽略;如果低優先級的任務先執行結束了,那麼後執行完的高優先級任務的返回結果就覆蓋之前低優先級任務的返回結果。

一個典型的例子是頁面緩存。比如,一個頁面要顯示一份動態的列表數據。如果每次頁面打開時都是隻從服務器取列表數據,那麼碰到沒有網絡或者網絡比較慢的情況,頁面會長時間空白。這時通常顯示一份舊的數據,比什麼都不顯示要好。因此,我們可能會考慮給這份列表數據增加一個本地持久化的緩存。

本地緩存也是一個異步任務,接口代碼定義如下:

public interface LocalDataCache {
    /**
     * 異步獲取本地緩存的HttpResponse對象.
     * @param key
     * @param callback 用於返回緩存對象
     */
    void getCachingData(String key, AsyncCallback<HttpResponse> callback);

    /**
     * 保存HttpResponse對象到緩存中.
     * @param key
     * @param data 要保存的HttpResponse對象
     * @param callback 用於返回當前保存操作的結果是成功還是失敗.
     */
    void putCachingData(String key, HttpResponse data, AsyncCallback<Boolean> callback);
}

這個本地緩存所緩存的數據對象,就是之前從服務器取到的一個HttpResponse對象。異步回調接口AsyncCallback,我們在前面已經講過。

這樣,當頁面打開時,我們可以同時啓動本地緩存讀取任務和遠程API請求的任務。其中後者比前者的優先級高。

public class PageCachingDemoActivity extends AppCompatActivity {
    private HttpService httpService = new MockHttpService();
    private LocalDataCache localDataCache = new MockLocalDataCache();
    /**
     * 從Http請求到的數據是否已經返回
     */
    private boolean dataFromHttpReady;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_page_caching_demo);

        //同時發起本地數據請求和遠程Http請求
        final String userId = "xxx";
        localDataCache.getCachingData(userId, new AsyncCallback<HttpResponse>() {
            @Override
            public void onResult(HttpResponse data) {
                if (data != null && !dataFromHttpReady) {
                    //緩存有舊數據 & 遠程Http請求還沒返回,先顯示舊數據
                    processData(data);
                }
            }
        });
        httpService.doRequest("http://...", new HttpRequest(),
                new HttpListener<HttpRequest, HttpResponse>() {
                    @Override
                    public void onResult(String apiUrl,
                                         HttpRequest request,
                                         HttpResult<HttpResponse> result,
                                         Object contextData) {
                        if (result.getErrorCode() == HttpResult.SUCCESS) {
                            dataFromHttpReady = true;
                            processData(result.getResponse());
                            //從Http拉到最新數據, 更新本地緩存
                            localDataCache.putCachingData(userId, result.getResponse(), null);
                        }
                        else {
                            processError(result.getErrorCode());
                        }
                    }
                },
                null);
    }


    private void processData(HttpResponse data) {
        //TODO: 更新UI, 展示數據. 省略此處代碼
    }

    private void processError(int errorCode) {
        //TODO: 更新UI,展示錯誤. 省略此處代碼
    }
}

雖然讀取本地緩存數據通常來說比從網絡獲取數據要快得多,但既然都是異步接口,就存在一種邏輯上的可能性:網絡獲取數據先於本地緩存數據發生回調。而且,我們在上一篇文章《Android和iOS開發中的異步處理(二)——異步任務的回調》中“回調順序”一節提到的“提前的失敗結果回調”和“提前的成功結果回調”,爲這種情況的發生提供了更爲現實的依據。

在上面的代碼中,如果網絡獲取數據先於本地緩存數據回調了,那麼我們會記錄一個布爾型的標記dataFromHttpReady。等到獲取本地緩存數據的任務完成時,我們判斷這個標記,從而忽略緩存數據。

單獨對於頁面緩存這個例子,由於通常來說讀取本地緩存數據和從網絡獲取數據所需要的執行時間相差懸殊,所以這裏的“併發執行,一方優先”的做法對性能提升並不明顯。這意味着,如果我們把頁面緩存的這個例子改爲“先後接續執行”的實現方式,可能會在沒有損失太多性能的前提下,獲得代碼邏輯的簡單易懂。

當然,如果你決意要採用本節的“併發執行,一方優先”的異步任務協作關係,那麼一定要記得考慮到異步任務回調的所有可能的執行順序。

使用RxJava zip來實現併發網絡請求

到目前爲止,爲了對付多個異步任務在執行時的各種協作關係,我們沒有采用任何工具,可以說是屬於“徒手搏鬥”的情形。本節接下來就要引入一個“重型武器”——RxJava,看一看它在Android上能否會讓異步問題的複雜度有所改觀。

我們以前面講的第二種場景“併發網絡請求”爲例。

在RxJava中,有一個建立在lift操作之上的zip操作,它可以把多個Observable的數據合併在一起,成爲一個新的Observable。這正是“併發網絡請求”這一場景所需要的特性。

我們可以把兩個併發的網絡請求看成兩個Observable,然後使用zip操作將它們的結果進行合併。這看起來簡化了很多。不過,這裏我們首先要解決另一個問題:把HttpService代表的異步網絡請求接口封裝成Observable。

通常來說,把一個同步任務封裝成Observable比較簡單,而把一個現成的異步任務封裝成Observable就不是那麼直觀了,我們需要用到AsyncOnSubscribe。

public class MultiRequestsDemoActivity extends AppCompatActivity {
    private HttpService httpService = new MockHttpService();

    private TextView apiResultDisplayTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multi_requests_demo);

        apiResultDisplayTextView = (TextView) findViewById(R.id.api_result_display);

        /**
         * 先根據AsyncOnSubscribe機制將兩次請求封裝成兩個Observable
         */

        Observable<HttpResponse1> request1 = Observable.create(new AsyncOnSubscribe<Integer, HttpResponse1>() {
            @Override
            protected Integer generateState() {
                return 0;
            }

            @Override
            protected Integer next(Integer state, long requested, Observer<Observable<? extends HttpResponse1>> observer) {
                final Observable<HttpResponse1> asyncObservable = Observable.create(new Observable.OnSubscribe<HttpResponse1>() {
                    @Override
                    public void call(final Subscriber<? super HttpResponse1> subscriber) {
                        //啓動第一個異步請求
                        httpService.doRequest("http://...", new HttpRequest1(),
                                new HttpListener<HttpRequest1, HttpResponse1>() {
                                    @Override
                                    public void onResult(String apiUrl, HttpRequest1 request, HttpResult<HttpResponse1> result, Object contextData) {
                                        //第一個異步請求結束, 向asyncObservable中發送結果
                                        if (result.getErrorCode() == HttpResult.SUCCESS) {
                                            subscriber.onNext(result.getResponse());
                                            subscriber.onCompleted();
                                        }
                                        else {
                                            subscriber.onError(new Exception("request1 failed"));
                                        }
                                    }
                                },
                                null);
                    }
                });
                observer.onNext(asyncObservable);
                observer.onCompleted();
                return 1;
            }
        });

        Observable<HttpResponse2> request2 = Observable.create(new AsyncOnSubscribe<Integer, HttpResponse2>() {
            @Override
            protected Integer generateState() {
                return 0;
            }

            @Override
            protected Integer next(Integer state, long requested, Observer<Observable<? extends HttpResponse2>> observer) {
                final Observable<HttpResponse2> asyncObservable = Observable.create(new Observable.OnSubscribe<HttpResponse2>() {
                    @Override
                    public void call(final Subscriber<? super HttpResponse2> subscriber) {
                        //啓動第二個異步請求
                        httpService.doRequest("http://...", new HttpRequest2(),
                                new HttpListener<HttpRequest2, HttpResponse2>() {
                                    @Override
                                    public void onResult(String apiUrl, HttpRequest2 request, HttpResult<HttpResponse2> result, Object contextData) {
                                        //第二個異步請求結束, 向asyncObservable中發送結果
                                        if (result.getErrorCode() == HttpResult.SUCCESS) {
                                            subscriber.onNext(result.getResponse());
                                            subscriber.onCompleted();
                                        }
                                        else {
                                            subscriber.onError(new Exception("reques2 failed"));
                                        }
                                    }
                                },
                                null);
                    }
                });
                observer.onNext(asyncObservable);
                observer.onCompleted();
                return 1;
            }
        });

        //對於兩個Observable表示的request,用zip合併它們的結果
        Observable.zip(request1, request2, new Func2<HttpResponse1, HttpResponse2, List<Object>>() {
            @Override
            public List<Object> call(HttpResponse1 response1, HttpResponse2 response2) {
                List<Object> responses = new ArrayList<Object>(2);
                responses.add(response1);
                responses.add(response2);
                return responses;
            }
        }).subscribe(new Subscriber<List<Object>>() {
            private HttpResponse1 response1;
            private HttpResponse2 response2;

            @Override
            public void onNext(List<Object> responses) {
                response1 = (HttpResponse1) responses.get(0);
                response2 = (HttpResponse2) responses.get(1);
            }

            @Override
            public void onCompleted() {
                processData(response1, response2);
            }

            @Override
            public void onError(Throwable e) {
                processError(e);
            }

        });
    }

    private void processData(HttpResponse1 data1, HttpResponse2 data2) {
        //TODO: 更新UI, 展示數據. 省略此處代碼
    }

    private void processError(Throwable e) {
        //TODO: 更新UI,展示錯誤. 省略此處代碼
    }

通過引入RxJava,我們簡化了異步任務執行結束時的判斷邏輯,但把大部分精力花在了“將HttpService封裝成Observable”上面了。我們說過,RxJava是一件“重型武器”,它所能完成的事情遠遠大於這裏所需要的。把RxJava用在這裏,不免給人“殺雞用牛刀”的感覺。

對於另外兩種異步任務的協作關係:“先後接續執行”和“併發執行,一方優先”,如果想應用RxJava來解決,那麼同樣首先需要先成爲RxJava的專家,這樣纔有可能很好地完成這件事。

而對於“先後接續執行”的情況,它本身已經足夠簡單了,不引入別的框架反而更簡單。有時候,我們也許更希望處理邏輯簡單,那麼把多個異步任務的執行,都按照“先後接續執行”的方式來處理,也是一種解決思路。雖然這會損害一些性能。


本文先後討論了三種多異步任務的協作關係,最後並不想得到這樣一個結論:把多個異步任務的執行都改成“先後接續執行”以簡化處理邏輯。取捨仍然在於開發者自己。

而且,一個不容忽視的問題是,在很多情況下,選擇權不在我們手裏,我們拿到的代碼架構也許已經造成了各種各樣的異步任務協作關係。我們需要做的,就是在這種情況出現時,能夠總是保持頭腦的冷靜,從紛繁複雜的代碼邏輯中識別和認清當前所處的局面到底屬於哪一種。

(完)

轉自:http://zhangtielei.com/posts/blog-series-async-task-3.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章