Android开发中,我们有时候会遇到变更频繁、特别复杂的页面。比如拿微信首页的聊天会话列表来说,每一条消息的到来,都会影响到聊天顺序,至少需要触发一次页面刷新。假如我的群非常多,隔一段时间不用微信的话,可能进入App后一直在收到消息,页面一直在刷新,那么刷新过于频繁的情况下,就会出现卡顿。微信现在当然不会这么明显,那如果是你来开发,会有微信这样的效果吗?
实际开发中,我就遇到过很多次这样的问题。各个模块、各种业务场景导致的数据变化,都有可能来触发同一个页面的刷新,我们一句简单的notifyDataChange调用就能实现需求,但久而久之,页面卡顿越来越明显。
至于卡顿的原理,大家可以自行去了解。现在我们需要做的,就是如何限制对UI的刷新?因为频繁的刷新,对于用户来说,眼睛是感知不到的,反而会导致掉帧和卡顿。每次刷新,我们也可以认为是对UI的一次请求。
大型App的多业务模块可能对服务端触发同一个请求,浪费流量甚至影响服务端性能,能否做到某些高并发情况下减轻对服务端的压力,同时又不影响客户端数据的最终一致性?我们完全可以对同一类请求进行限制,提高CS间请求有效交互率。
在一些常见的CS模式开发过程中,客户端(Client)和服务端(Server)之间依赖特定协议的请求进行数据同步和交互。经常会发生如下场景:因客户端的逻辑不健壮或漏洞,可能在特殊情况下爆发对服务端的频繁请求,从而导致服务端性能急剧下降甚至是宕机。
从客户端角度看,不同业务场景下可能触发的都是同一个请求,必要性非常低。客户端很多对服务端的请求用于同步数据,目的仅在于尽可能保证两端数据一致性。
那么,我们该采用什么样的策略来减少或者限制同类请求呢?
搞个定时器,每个请求延迟一分钟再触发,在当前请求触发之前的相同请求直接丢弃,不就OK了吗?
不错,既然认为是同类请求,那么丢弃其中一部分似乎合情合理。但这个丢弃方式是否太过粗糙?可能出现类似如下场景的问题。
假设某个买菜App中,用户再订单列表页点击确认收货后,进入订单详情后却一直显示“未确认”,等到一分钟后,才变成“已确认”。
对于上一个请求尚未响应时,同时由触发的同一请求,可以进行请求合并,从而降低对服务器并发。
不错,对同时并发的请求如果能够进行合并,的确会降低对服务器的并发,但是实际效果可能并不明显。
假设一个请求的响应时间需要100 ms,那么对于同一客户端,如果发送同一请求的平均间隔大于100 ms,那么这样的合并就意义不大了,但是如果我们服务端认为100 ms发一次本身就已经很频繁了,该怎么降低呢?假设服务端只能承受200 ms一次的访问呢?
总结以上两种想法,我们需要寻找一种方案,既能实现闲时及时请求,又能实现忙时大批量减少请求。至于刚提到的请求合并,我们后续文章再探讨一下。
在Android开发时,我们经常接触到Handler,作为一个常驻线程的句柄,可以通过post方法对其所属线程提交Runnable任务,期望它“立刻”执行,通过postDelayed方法,可以延迟一段时间执行。因此我们可以封装实现一个限制执行请求/任务的LimitRequester。
package com.ya.helper.util;
import android.os.Handler;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 包装Handler,实现请求控制
* 机制:前一次请求尚未执行完成,则仅保留最新一次请求
* 注意:同一LimitRequester对象提交的所有Runnable视为同一类可合并请求
*
* @author miss
*/
public class LimitRequester {
private static final String TAG = "LimitRequester";
private Handler mHandler;
private AutoTimeRunnable aRun;
private int mTime;
/**
* 创建限制刷新的Handler
* 上个请求尚未执行完,则仅保留最新一次请求
*
* @param handler 实际使用的线程句柄 Handler
*/
public LimitRequester(Handler handler) {
mHandler = handler;
aRun = new AutoTimeRunnable();
}
/**
* 创建间隔刷新的Handler
* 两次请求提交间隔不能超过interval,否则丢弃仅执行最新一次
*
* @param handler
* @param interval 毫秒间隔
*/
public LimitRequester(Handler handler, int interval) {
if (interval <= 0) {
throw new IllegalArgumentException("LimitRequester interval must > 0");
}
mHandler = handler;
mTime = interval;
aRun = new AutoTimeRunnable();
}
/**
* 提交请求,默认为同一类别
* 若Handler处于非空闲状态,则仅执行最新一次提交
*
* @param runnable
*/
public void postLimit(Runnable runnable) {
if (null == runnable) {
return;
}
aRun.post(runnable);
}
private class AutoTimeRunnable implements Runnable {
private Runnable toRun;
//标识是否空闲,默认是空闲状态
private final AtomicBoolean isIdle = new AtomicBoolean(true);
private long lastExecuteTime = 0L;
public void post(Runnable toRun) {
//若空闲,则执行,不空闲,则仅保存最新一次Runnable
this.toRun = toRun;
if (isIdle.compareAndSet(true, false)) {
execute();
}
}
/**
* 执行最新保存的Runnable
* 执行完成后,若发现有新请求,则执行
* 若没有新请求,则置为空闲状态
*/
@Override
public void run() {
Runnable r = toRun;
r.run();
if (r != toRun) {
execute();
} else {
isIdle.set(true);
}
}
/**
* 具体执行逻辑
* 若无间隔,则立刻执行
* 若有间隔,两次执行间隔超过要求间隔,立刻执行
* 两次执行间隔不及要求间隔,则延迟间隔时间后执行
*/
private void execute() {
if (mTime > 0) {
long now = System.currentTimeMillis();
long interval = now - lastExecuteTime;
if (interval >= mTime) {
lastExecuteTime = now;
mHandler.post(this);
} else {
lastExecuteTime = now + mTime;
mHandler.postDelayed(this, mTime);
}
} else {
mHandler.post(this);
}
}
}
}
需要限制请求访问的地方,即使用LimitRequester,传入执行线程句柄,通过postLimit进行提交即可。
需要注意的地方是:
1、同一个LimitRequester所处理的请求必须是可进行合并的Runnable,Runnable是不是同一个对象引用无所谓
2、不同请求的限制处理,需要构造不同的LimitRequester对象,实际可以是同一线程执行
我们通过一段测试代码进行验证:
假设我们要执行1000次打印,每200ms打印一次,如果使用LimitRequester,限制1s执行一次,则最终输出结果应该是200次打印。
new Thread(new Runnable() {
@Override
public void run() {
LimitRequester requester = new LimitRequester(new Handler(Looper.getMainLooper()), 1000);
final int[] count = {1};
for (int i = 0; i < 1000; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
requester.postLimit(new Runnable() {
@Override
public void run() {
Log.i(TAG, "执行第" + count[0]++ + "次");
}
});
}
}
}).start();
输出结果为:
.....
2020-05-23 15:58:55.292 7123-7123/com.ya.helper.androidlab I/TEST: 执行第195次
2020-05-23 15:58:56.302 7123-7123/com.ya.helper.androidlab I/TEST: 执行第196次
2020-05-23 15:58:57.315 7123-7123/com.ya.helper.androidlab I/TEST: 执行第197次
2020-05-23 15:58:58.326 7123-7123/com.ya.helper.androidlab I/TEST: 执行第198次
2020-05-23 15:58:59.334 7123-7123/com.ya.helper.androidlab I/TEST: 执行第199次
2020-05-23 15:59:00.339 7123-7123/com.ya.helper.androidlab I/TEST: 执行第200次
2020-05-23 15:59:01.345 7123-7123/com.ya.helper.androidlab I/TEST: 执行第201次
为啥是201次呢?因为第一次postLimit是立刻执行的,之后的才会进行合并。可以看到每次时间差不会是那么精确的1s,因为Handler的postDelay不可能做到极其精确的定时。
如果你正在开发的页面,也存在刷新过频的问题,不妨用这个方法一试,无论何处触发刷新,无论场景多么复杂,只要最后使用同一个LimitRequester进行执行,问题迎刃而解!