ホームページ >Java >&#&チュートリアル >Java で手ぶれ補正とスロットルを実装する方法
イベントが継続的にトリガーされ、一定時間内に再度イベントがトリガーされなかった場合、イベント処理関数は 1 回実行されます。設定時間前にトリガーされなかった場合、イベントが再度トリガーされ、遅延が再び開始されます。
アンチシェイク、つまり、短期間に多数の同じイベントがトリガーされた場合、タイマーはリセットされます。この機能は、時間が経過するまで実行されません。イベントはトリガーされなくなり、指定されたイベントを待ちます。そして、このプロセス全体がサーバーに対して同様の機能をトリガーします。原則: タイマーを設定し、指定された時間が経過した後にイベント処理をトリガーします。タイマーは、イベントがトリガーされるたびにリセットされます。
例: 非常に単純な例は、友達のサークルに狂ったようにいいねをした後、いいねをキャンセルすると、このプロセスによってタイマーがクリアされ、飽きたらクリックしなくなります。関数がトリガーされ、最終結果がサーバーに送信されるまで 0.5 秒待ちます。
質問 1: そうだとしたら、手ぶれ補正をフロントエンドにやらせたほうがよいのではありませんか?答えは「はい」ですが、ユーザーエクスペリエンスは失われます。もともと、一部のユーザーは単にプレイするだけで気に入っていましたが、現在はフロントエンドが「操作が速すぎます~休憩してください」というメッセージを直接表示します。ユーザーが楽しみを失ったかどうか、この点はQQスペースのいいねも参考にする必要があります。手ぶれ補正を使用しているかどうかはわかりませんが、アニメーションいいねとキャンセルがあり、ユーザーが操作するたびに実行されるアニメーションがポップアップ表示され、ユーザー エクスペリエンスが大幅に向上します。
質問 2: そこで質問ですが、一定時間内にクリックし続けるとタイマーがリセットされます。では、一昼夜クリックした場合、実行をやめるでしょうか?理論的にはそうなのですが、人は疲れてしまいます。戦い続けることはできないですよね?人間にはできないので、機械とスクリプトで処理するしかありません。それはその通りです。手ぶれ補正は、一部のスクリプト攻撃をブロックするためにも使用できます。
イベントが継続的にトリガーされる場合、イベント処理関数は一定期間内に 1 回だけ呼び出されることが保証されます。ユーザーがこの関数をトリガーし続け、トリガーが設定値未満になるたびに、関数スロットルが 1 回呼び出されると仮定します。
このように考えると、多くの人が機能を思い浮かべるでしょう。そう、それは重複送信を防止する機能です。ただし、このビジネス時間は制御が難しいため、ステートレスな操作を行うために使用することをお勧めします。たとえば、ランキングを更新するとき、フロントエンドは常にクリックしているように見えますが、実際には、インターフェイスのクラッシュを防ぐために、バックエンドは 1 秒ごとにのみ実際の更新を実行します。
アンチシェイクとは、指定された時間内にトリガーされた後、複数の実行を 1 つの実行に変更することです。
スロットリングとは、複数の実行を指定した時間に変更することです。何度トリガーされても、時間切れになると 1 回だけ実行されます。
java で手ぶれ補正とスロットリングを実現 鍵となるのは Timer クラスと Runnable インターフェイスです。
その中で、Timer の重要なメソッド cancel() は手ぶれ補正を実装し、schedule() はスロットリングを実装します。以下に、これら 2 つの方法について簡単に紹介します。
Timer##cancel(): Timer.cancel() は、タイマー スレッド全体が呼び出された後に終了します。
これはわかりやすいのですが、手ぶれ補正なので指定時間内にトリガーしさえすれば直接cancel()、つまりキャンセルして実行させません。
Timer##schedule(): ユーザーは、schedule() メソッドを呼び出した後、run() メソッドを初めて実行するまでに N 秒待つ必要があります。
この N は、ビジネスに基づいて評価された時間であり、パラメーターとして渡されます。
package com.example.test01.zhangch; import java.util.Timer; import java.util.TimerTask; /** * @Author zhangch * @Description java 防抖 * @Date 2022/8/4 18:18 * @Version 1.0 */ @SuppressWarnings("all") public class DebounceTask { /** * 防抖实现关键类 */ private Timer timer; /** * 防抖时间:根据业务评估 */ private Long delay; /** * 开启线程执行任务 */ private Runnable runnable; public DebounceTask(Runnable runnable, Long delay) { this.runnable = runnable; this.delay = delay; } /** * * @param runnable 要执行的任务 * @param delay 执行时间 * @return 初始化 DebounceTask 对象 */ public static DebounceTask build(Runnable runnable, Long delay){ return new DebounceTask(runnable, delay); } //Timer类执行:cancel()-->取消操作;schedule()-->执行操作 public void timerRun(){ //如果有任务,则取消不执行(防抖实现的关键) if(timer!=null){ timer.cancel(); } timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { //把 timer 设置为空,这样下次判断它就不会执行了 timer=null; //执行 runnable 中的 run()方法 runnable.run(); } }, delay); } }
ご覧のとおり、テスト中は 1 ミリ秒に 1 回リクエストしました。 1秒以内に連続性があり、要求された手ぶれ補正動作は実行されません。
public static void main(String[] args){ //构建对象,1000L: 1秒执行-->1秒内没有请求,在执行防抖操作 DebounceTask task = DebounceTask.build(new Runnable() { @Override public void run() { System.out.println("防抖操作执行了:do task: "+System.currentTimeMillis()); } },1000L); long delay = 100; while (true){ System.out.println("请求执行:call task: "+System.currentTimeMillis()); task.timerRun(); try { //休眠1毫秒在请求 Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } } }
結果は予想どおりです:
ターゲット VM に接続されました、アドレス: '127.0.0.1:5437'、トランスポート: 'ソケット'
リクエストの実行:呼び出しタスク: 1659609433021
リクエストの実行: 呼び出しタスク: 1659609433138
リクエストの実行: 呼び出しタスク: 1659609433243
リクエストの実行: 呼び出しタスク: 1659609433350
リクエストの実行: 呼び出しタスク: 1659609433462
リクエストの実行:呼び出しタスク: 1659609433572
リクエストの実行: 呼び出しタスク: 1659609433681
リクエストの実行: 呼び出しタスク: 1659609433787
リクエストの実行: 呼び出しタスク: 1659609433893
リクエストの実行: 呼び出しタスク: 1659609433999
リクエストの実行:呼び出しタスク: 1659609434106
リクエストの実行: 呼び出しタスク: 1659609434215
リクエストの実行: 呼び出しタスク: 1659609434321
リクエストの実行: 呼び出しタスク: 1659609434425
リクエストの実行: 呼び出しタスク: 1659609434534
テスト 2 では、2 秒間のリクエストの後、メインスレッドを 2 秒間休ませますが、このとき、手ぶれ補正は 1 秒以内に再度トリガーされません。手ぶれ補正動作。
public static void main(String[] args){ //构建对象,1000L:1秒执行 DebounceTask task = DebounceTask.build(new Runnable() { @Override public void run() { System.out.println("防抖操作执行了:do task: "+System.currentTimeMillis()); } },1000L); long delay = 100; long douDelay = 0; while (true){ System.out.println("请求执行:call task: "+System.currentTimeMillis()); task.timerRun(); douDelay = douDelay+100; try { //如果请求执行了两秒,我们让他先休息两秒,在接着请求 if (douDelay == 2000){ Thread.sleep(douDelay); } Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } } }
結果は期待どおりです。手ぶれ補正タスクは 1 回トリガーされます。スリープが完了すると、要求は 1 ミリ秒以内に継続的にトリガーされ、手ぶれ補正は再度トリガーされません。
请求执行:call task: 1659609961816
请求执行:call task: 1659609961924
请求执行:call task: 1659609962031
请求执行:call task: 1659609962138
请求执行:call task: 1659609962245
请求执行:call task: 1659609962353
防抖操作执行了:do task: 1659609963355
请求执行:call task: 1659609964464
请求执行:call task: 1659609964569
请求执行:call task: 1659609964678
请求执行:call task: 1659609964784
简易版:根据新手写代码习惯,对代码写法做了调整,但是不影响整体功能。这种写法更加符合我这种新手小白的写法。
public static void main(String[] args){ //要执行的任务,因为 Runnable 是接口,所以 new 对象的时候要实现它的 run方法 Runnable runnable = new Runnable() { @Override public void run() { //执行打印,真实开发中,是这些我们的业务代码。 System.out.println("防抖操作执行了:do task: "+System.currentTimeMillis()); } }; //runnable:要执行的任务,通过参数传递进去。1000L:1秒执行内没有请求,就执行一次防抖操作 DebounceTask task = DebounceTask.build(runnable,1000L); //请求持续时间 long delay = 100; //休眠时间,为了让防抖任务执行 long douDelay = 0; //while 死循环,请求一直执行 while (true){ System.out.println("请求执行:call task: "+System.currentTimeMillis()); //调用 DebounceTask 防抖类中的 timerRun() 方法, 执行防抖任务 task.timerRun(); douDelay = douDelay+100; try { //如果请求执行了两秒,我们让他先休息两秒,在接着请求 if (douDelay == 2000){ Thread.sleep(douDelay); } Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } } }
package com.example.test01.zhangch; import java.util.Timer; import java.util.TimerTask; /** * @Author zhangch * @Description 节流 * @Date 2022/8/6 15:41 * @Version 1.0 */ public class ThrottleTask { /** * 节流实现关键类 */ private Timer timer; private Long delay; private Runnable runnable; private boolean needWait=false; /** * 有参构造函数 * @param runnable 要启动的定时任务 * @param delay 延迟时间 */ public ThrottleTask(Runnable runnable, Long delay) { this.runnable = runnable; this.delay = delay; this.timer = new Timer(); } /** * build 创建对象,相当于 ThrottleTask task = new ThrottleTask(); * @param runnable 要执行的节流任务 * @param delay 延迟时间 * @return ThrottleTask 对象 */ public static ThrottleTask build(Runnable runnable, Long delay){ return new ThrottleTask(runnable, delay); } public void taskRun(){ //如果 needWait 为 false,结果取反,表达式为 true。执行 if 语句 if(!needWait){ //设置为 true,这样下次就不会再执行 needWait=true; //执行节流方法 timer.schedule(new TimerTask() { @Override public void run() { //执行完成,设置为 false,让下次操作再进入 if 语句中 needWait=false; //开启多线程执行 run() 方法 runnable.run(); } }, delay); } } }
节流测试,每 2ms 请求一次,节流任务是每 1s 执行一次。真实效果应该是 1s 内前端发起了五次请求,但是后端只执行了一次操作
public static void main(String[] args){ //创建节流要执行的对象,并把要执行的任务传入进去 ThrottleTask task = ThrottleTask.build(new Runnable() { @Override public void run() { System.out.println("节流任务执行:do task: "+System.currentTimeMillis()); } },1000L); //while一直执行,模拟前端用户一直请求后端 while (true){ System.out.println("前端请求后端:call task: "+System.currentTimeMillis()); task.taskRun(); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果如我们所料
前端请求后端:call task: 1659772459363
前端请求后端:call task: 1659772459574
前端请求后端:call task: 1659772459780
前端请求后端:call task: 1659772459995
前端请求后端:call task: 1659772460205
节流任务执行:do task: 1659772460377
前端请求后端:call task: 1659772460409
前端请求后端:call task: 1659772460610
前端请求后端:call task: 1659772460812
前端请求后端:call task: 1659772461027
前端请求后端:call task: 1659772461230
节流任务执行:do task: 1659772461417
idea 爆红线了,强迫症的我受不了,肯定要解决它
脑子第一时间冒出来的是 @SuppressWarnings("all") 注解,跟所有的警告说拜拜~瞬间就清爽了
算了,压制警告总感觉是不负责任。总不能这样草草了事,那就来直面这个爆红。既然让我用 ScheduledExecutorService ,那简单,直接替换
public class ThrottleTask { /** * 节流实现关键类: */ private ScheduledExecutorService timer; private Long delay; private Runnable runnable; private boolean needWait=false; /** * 有参构造函数 * @param runnable 要启动的定时任务 * @param delay 延迟时间 */ public ThrottleTask(Runnable runnable, Long delay) { this.runnable = runnable; this.delay = delay; this.timer = Executors.newSingleThreadScheduledExecutor(); } /** * build 创建对象,相当于 ThrottleTask task = new ThrottleTask(); * @param runnable 要执行的节流任务 * @param delay 延迟时间 * @return ThrottleTask 对象 */ public static ThrottleTask build(Runnable runnable, Long delay){ return new ThrottleTask(runnable, delay); } public void taskRun(){ //如果 needWait 为 false,结果取反,表达式为 true。执行 if 语句 if(!needWait){ //设置为 true,这样下次就不会再执行 needWait=true; //执行节流方法 timer.schedule(new TimerTask() { @Override public void run() { //执行完成,设置为 false,让下次操作再进入 if 语句中 needWait=false; //开启多线程执行 run() 方法 runnable.run(); } }, delay,TimeUnit.MILLISECONDS); } } }
那么定时器 Timer 和 ScheduledThreadPoolExecutor 解决方案之间的主要区别是什么,我总结了三点...
定时器对系统时钟的变化敏感;ScheduledThreadPoolExecutor并不会。
定时器只有一个执行线程;ScheduledThreadPoolExecutor可以配置任意数量的线程。
TimerTask中抛出的运行时异常会杀死线程,因此后续的计划任务不会继续运行;使用ScheduledThreadExecutor–当前任务将被取消,但其余任务将继续运行。
以上がJava で手ぶれ補正とスロットルを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。