如果優雅地處理多個請求並在請求結束後統一處理
如何優雅地處理多個請求並在請求結束後統一處理
前不久我接到一個需求,首頁更新的資料是從3個介面獲取的,三個介面獲取到的資料後再重新整理介面,大家可以腦補X東,X寶的app首頁,螢幕從上到下,上面是banner區,用來展示促銷商品之類的廣告,中間是幾個按鈕區,方便使用者分類進入相應的模組,如XX超市,XX家電,XX生鮮,話費充值之類的,下面是推薦商品展示區。
我司app也是這種大眾臉。開發時,後臺童鞋針對首頁資料提供了三個介面,分別對應banner區簡稱A區,按鈕區簡稱B區,展示區簡稱C區。從事android開發的都知道,這種通過網路獲取資料的耗時操作是不能放在UI執行緒中進行的,耗時的操作必需放在非UI執行緒中進行。
下面是示例程式碼
public class Demo {
public static final long TIMEOUT = 5L;
private String getBannerInfo() {
try {
System.out.println("開始獲取banner區資料");
TimeUnit.SECONDS.sleep(TIMEOUT);
System.out.println("獲取banner區資料請求結束");
return "一條廣告" ;
} catch (InterruptedException e) {
e.printStackTrace();
return "資料異常";
}
}
private int getButtonVisibleCount() {
try {
System.out.println("開始獲取按鈕區可顯示個數");
TimeUnit.SECONDS.sleep(TIMEOUT);
System.out.println("獲取按鈕區可顯示個數請求結束" );
return 8;
} catch (InterruptedException e) {
e.printStackTrace();
return -1;
}
}
private String getShowAreaImgUrl() {
try {
System.out.println("開始獲取展示區圖片地址");
TimeUnit.SECONDS.sleep(TIMEOUT);
System.out.println("獲取展示區圖片地址請求結束");
// 測試圖片
return "http://xxx.1112222.jpg";
} catch (InterruptedException e) {
e.printStackTrace();
return "";
}
}
private void updateUI(String bannerInfo, int buttonCount, String imgUrl) {
System.out.println(String.format("更新banner區: %s, 可顯示按鈕個數: %d, 展示區圖片地址: %s", bannerInfo, buttonCount, imgUrl));
}
public static void main(String[] args) {
final Demo demo = new Demo();
System.out.println("開始獲取伺服器資料-------");
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
String bannerInfo = demo.getBannerInfo();
int buttonCount = demo.getButtonVisibleCount();
String imgURL = demo.getShowAreaImgUrl();
System.out.println("banner區資訊: " + bannerInfo);
System.out.println("可以顯示的按鈕個數: " + buttonCount);
System.out.println("展示圖片URL: " + imgURL);
demo.updateUI(bannerInfo, buttonCount, imgURL);
}
});
}
}
結果如下:
開始獲取伺服器資料——-
開始獲取banner區資料
獲取banner區資料請求結束
開始獲取按鈕區可顯示個數
獲取按鈕區可顯示個數請求結束
開始獲取展示區圖片地址
獲取展示區圖片地址請求結束
banner區資訊: 一條廣告
可以顯示的按鈕個數: 8
展示圖片URL: http://xxx.1112222.jpg
更新banner區: 一條廣告, 可顯示按鈕個數: 8, 展示區圖片地址: http://xxx.1112222.jpg
這樣的請求看似沒有問題,實際上有個問題,當資料量小時,3個請求在一個執行緒裡順次執行,所有請求結束後,更新主介面UI。理想很完美,現實很殘酷。實際中,這些介面對應的資料表都是非常龐大的,如果3個網路請求的響應各是5s,那麼更新操作將在15s之後才能進行。或者,請求第一個介面時,伺服器那邊異常了,但是沒有資料返回,後面兩個介面是正常的,因為第一個請求沒有結束,後面的操作會一直阻塞,這會影響使用者體驗的。也許有人會說,可以將三個介面作一個介面提供給移動端同學呼叫,這樣移動端人員呼叫也方便。這種介面設計針對小資料量是沒有問題的,但是資料量多的話,請求響應資料就會很多,移動端解析時就會很慢。
針對3介面響應不能順次請求來重新整理UI,那麼有沒有辦法來解決呢,答案是肯定有的。
方法1:通過單個執行緒請求每1個介面,最後在更新UI時,判斷3個接個請求有沒有成功響應。為了達成這個目的,可以通過介面回撥+標誌flag的方式來完成,簡言之,執行緒1請求A區介面,執行緒2請求B區介面,執行緒3請求C區介面,每個介面成功響應後設置一個標誌flag,當更新UI時,判斷flag,如果3個請求的flag均為true時,則更新UI。
這樣做的方式有個缺點,極難維護,flag滿天飛,後期增加其他介面,必定導致flag數量增多,增加後期維護難度。
方法2:java在1.5時就已經提供了一個類幫助解決這種問題——CountDownLatch。該類接受一個int的引數,是一個計數器表示可以通過的執行緒數,當呼叫avait表示當前執行緒處於等待狀態,呼叫countDown遞減計數器,當計數器減為0時,執行avait後面的邏輯。
示例程式碼如下
public class Demo {
public static final long TIMEOUT = 5L;
private String getBannerInfo() {
try {
System.out.println("開始獲取banner區資料");
TimeUnit.SECONDS.sleep(TIMEOUT);
System.out.println("獲取banner區資料請求結束");
return "一條廣告";
} catch (InterruptedException e) {
e.printStackTrace();
return "資料異常";
}
}
private int getButtonVisibleCount() {
try {
System.out.println("開始獲取按鈕區可顯示個數");
TimeUnit.SECONDS.sleep(TIMEOUT);
System.out.println("獲取按鈕區可顯示個數請求結束");
return 8;
} catch (InterruptedException e) {
e.printStackTrace();
return -1;
}
}
private String getShowAreaImgUrl() {
try {
System.out.println("開始獲取展示區圖片地址");
TimeUnit.SECONDS.sleep(TIMEOUT);
System.out.println("獲取展示區圖片地址請求結束");
// 測試圖片
return "http://xxx.1112222.jpg";
} catch (InterruptedException e) {
e.printStackTrace();
return "";
}
}
private void updateUI(String bannerInfo, int buttonCount, String imgUrl) {
System.out.println(String.format("更新banner區: %s, 可顯示按鈕個數: %d, 展示區圖片地址: %s", bannerInfo, buttonCount, imgUrl));
}
String bannerInfo;
int buttonCount;
String imgURL;
public static void main(String[] args) {
final CountDownLatch countDownLatch = new CountDownLatch(3);
final Demo demo = new Demo();
System.out.println("開始獲取伺服器資料-------");
new Thread(new Runnable() {
@Override
public void run() {
demo.bannerInfo = demo.getBannerInfo();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo.buttonCount = demo.getButtonVisibleCount();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo.imgURL = demo.getShowAreaImgUrl();
countDownLatch.countDown();
}
}).start();
try {
countDownLatch.await();
demo.updateUI(demo.bannerInfo, demo.buttonCount, demo.imgURL);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Executors.newSingleThreadExecutor().execute(new Runnable() {
// @Override
// public void run() {
// String bannerInfo = demo.getBannerInfo();
// int buttonCount = demo.getButtonVisibleCount();
// String imgURL = demo.getShowAreaImgUrl();
// System.out.println("banner區資訊: " + bannerInfo);
// System.out.println("可以顯示的按鈕個數: " + buttonCount);
// System.out.println("展示圖片URL: " + imgURL);
// demo.updateUI(bannerInfo, buttonCount, imgURL);
// }
// });
}
}
執行後結果如下:
開始獲取伺服器資料——-
開始獲取banner區資料
開始獲取按鈕區可顯示個數
開始獲取展示區圖片地址
獲取按鈕區可顯示個數請求結束
獲取banner區資料請求結束
獲取展示區圖片地址請求結束
更新banner區: 一條廣告, 可顯示按鈕個數: 8, 展示區圖片地址: http://xxx.1112222.jpg
『獲取按鈕區可顯示個數請求結束
獲取banner區資料請求結束
獲取展示區圖片地址請求結束』這三條日誌會因不同的機器,不同的執行次數順序會不一樣
這樣就能保證了三個執行緒分別呼叫介面後再更新UI。這樣設計的好處是,未來新增其他的介面時,可以很方便的修改計數器即可,不需要維護N個flag。