本文是系列文章《Android和iOS開發中的異步處理》的第三篇。在本篇文章中,咱們主要討論在執行多個異步任務的時候可能碰到的相關問題。java
一般咱們都須要執行多個異步任務,使它們相互協做來完成需求。本文結合典型的應用場景,講解異步任務的三種協做關係:git
以上三種協做關係,本文分別以三種應用場景爲例展開討論。這三種應用場景分別是:程序員
最後,本文還會嘗試給出一個使用RxJava這樣的框架來實現「併發網絡請求」的案例,並進行相關的探討。github
注:本系列文章中出現的代碼已經整理到GitHub上(持續更新),代碼庫地址爲:api
其中,當前這篇文章中出現的Java代碼,位於com.zhangtielei.demos.async.programming.multitask這個package中。緩存
「前後接續執行」指的是一個異步任務先啓動執行,待執行完成結果回調發生後,再啓動下一個異步任務。這是多個異步任務最簡單的一種協做方式。服務器
一個典型的例子是靜態資源的多級緩存,其中最爲你們所喜聞樂見的例子就是靜態圖片的多級緩存。一般在客戶端加載一個靜態圖片,都會至少有兩級緩存:第一級Memory Cache和第二級Disk Cache。整個加載流程以下:網絡
一般,第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,看一看它在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的專家,這樣纔有可能很好地完成這件事。
而對於「前後接續執行」的狀況,它自己已經足夠簡單了,不引入別的框架反而更簡單。有時候,咱們也許更但願處理邏輯簡單,那麼把多個異步任務的執行,都按照「前後接續執行」的方式來處理,也是一種解決思路。雖然這會損害一些性能。
本文前後討論了三種多異步任務的協做關係,最後並不想獲得這樣一個結論:把多個異步任務的執行都改爲「前後接續執行」以簡化處理邏輯。取捨仍然在於開發者本身。
並且,一個不容忽視的問題是,在不少狀況下,選擇權不在咱們手裏,咱們拿到的代碼架構也許已經形成了各類各樣的異步任務協做關係。咱們須要作的,就是在這種狀況出現時,可以老是保持頭腦的冷靜,從紛繁複雜的代碼邏輯中識別和認清當前所處的局面到底屬於哪種。
(完)
其它精選文章: