Android页面卡顿,是时候做刷新控制了

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进行执行,问题迎刃而解!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容