转载请注明出处: http://blog.csdn.net/brucehurrican/article/details/73793288
如何优雅地处理多个请求并在请求结束后统一处理
前不久我接到一个需求,首页更新的数据是从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。
参考资料:
Java CountDownLatch应用
Java 并发专题 :闭锁 CountDownLatch 之一家人一起吃个饭
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore