对于我正在编写的一些代码,我可以在Java中使用debounce
的一个很好的通用实现。
public interface Callback {
public void call(Object arg);
}
class Debouncer implements Callback {
public Debouncer(Callback c, int interval) { ... }
public void call(Object arg) {
// should forward calls with the same arguments to the callback c
// but batch multiple calls inside `interval` to a single one
}
}
使用相同的参数在call()
毫秒内多次调用interval
时,应该只调用一次回调函数。
可视化:
Debouncer#call xxx x xxxxxxx xxxxxxxxxxxxxxx
Callback#call x x x (interval is 2)
答案 0 :(得分:26)
请考虑以下线程安全解决方案。请注意,锁粒度在密钥级别上,因此只有相同密钥的调用才会相互阻塞。它还处理在调用call(K)时发生的密钥K到期的情况。
public class Debouncer <T> {
private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();
private final Callback<T> callback;
private final int interval;
public Debouncer(Callback<T> c, int interval) {
this.callback = c;
this.interval = interval;
}
public void call(T key) {
TimerTask task = new TimerTask(key);
TimerTask prev;
do {
prev = delayedMap.putIfAbsent(key, task);
if (prev == null)
sched.schedule(task, interval, TimeUnit.MILLISECONDS);
} while (prev != null && !prev.extend()); // Exit only if new task was added to map, or existing task was extended successfully
}
public void terminate() {
sched.shutdownNow();
}
// The task that wakes up when the wait time elapses
private class TimerTask implements Runnable {
private final T key;
private long dueTime;
private final Object lock = new Object();
public TimerTask(T key) {
this.key = key;
extend();
}
public boolean extend() {
synchronized (lock) {
if (dueTime < 0) // Task has been shutdown
return false;
dueTime = System.currentTimeMillis() + interval;
return true;
}
}
public void run() {
synchronized (lock) {
long remaining = dueTime - System.currentTimeMillis();
if (remaining > 0) { // Re-schedule task
sched.schedule(this, remaining, TimeUnit.MILLISECONDS);
} else { // Mark as terminated and invoke callback
dueTime = -1;
try {
callback.call(key);
} finally {
delayedMap.remove(key);
}
}
}
}
}
答案 1 :(得分:8)
这是我的实施:
function routes($routeProvider){
$routeProvider
.when('/list',
{
templateUrl: 'sections/list/list.tpl.html',
controller: 'listController',
controllerAs: 'listCtrl'
})
.when('/overview',
{
templateUrl: 'sections/overview/overview.tpl.html',
controller: 'overviewController',
controllerAs: 'overviewCtrl'
})
...
使用示例:
public class Debouncer {
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentHashMap<Object, Future<?>> delayedMap = new ConcurrentHashMap<>();
/**
* Debounces {@code callable} by {@code delay}, i.e., schedules it to be executed after {@code delay},
* or cancels its execution if the method is called with the same key within the {@code delay} again.
*/
public void debounce(final Object key, final Runnable runnable, long delay, TimeUnit unit) {
final Future<?> prev = delayedMap.put(key, scheduler.schedule(new Runnable() {
@Override
public void run() {
try {
runnable.run();
} finally {
delayedMap.remove(key);
}
}
}, delay, unit));
if (prev != null) {
prev.cancel(true);
}
}
public void shutdown() {
scheduler.shutdownNow();
}
}
答案 2 :(得分:3)
我不知道它是否存在,但实施起来应该很简单。
class Debouncer implements Callback {
private CallBack c;
private volatile long lastCalled;
private int interval;
public Debouncer(Callback c, int interval) {
//init fields
}
public void call(Object arg) {
if( lastCalled + interval < System.currentTimeMillis() ) {
lastCalled = System.currentTimeMillis();
c.call( arg );
}
}
}
当然这个例子过于简单了,但这或多或少都是你需要的。如果您想为不同的参数保留单独的超时,则需要Map<Object,long>
而不是long
来跟踪上次执行时间。
答案 3 :(得分:1)
以下实现适用于基于Handler的线程(例如主UI线程或IntentService)。它只希望从创建它的线程中调用,并且它还将在该线程上运行它的操作。
public class Debouncer
{
private CountDownTimer debounceTimer;
private Runnable pendingRunnable;
public Debouncer() {
}
public void debounce(Runnable runnable, long delayMs) {
pendingRunnable = runnable;
cancelTimer();
startTimer(delayMs);
}
public void cancel() {
cancelTimer();
pendingRunnable = null;
}
private void startTimer(final long updateIntervalMs) {
if (updateIntervalMs > 0) {
// Debounce timer
debounceTimer = new CountDownTimer(updateIntervalMs, updateIntervalMs) {
@Override
public void onTick(long millisUntilFinished) {
// Do nothing
}
@Override
public void onFinish() {
execute();
}
};
debounceTimer.start();
}
else {
// Do immediately
execute();
}
}
private void cancelTimer() {
if (debounceTimer != null) {
debounceTimer.cancel();
debounceTimer = null;
}
}
private void execute() {
if (pendingRunnable != null) {
pendingRunnable.run();
pendingRunnable = null;
}
}
}
答案 4 :(得分:0)
这看起来可行:
class Debouncer implements Callback {
private Callback callback;
private Map<Integer, Timer> scheduled = new HashMap<Integer, Timer>();
private int delay;
public Debouncer(Callback c, int delay) {
this.callback = c;
this.delay = delay;
}
public void call(final Object arg) {
final int h = arg.hashCode();
Timer task = scheduled.remove(h);
if (task != null) { task.cancel(); }
task = new Timer();
scheduled.put(h, task);
task.schedule(new TimerTask() {
@Override
public void run() {
callback.call(arg);
scheduled.remove(h);
}
}, this.delay);
}
}
答案 5 :(得分:0)
我的实现非常易于使用,有2个util方法用于去抖动和节流,将您的可运行变量传递给它,以获取可运行的去抖动/油门
package basic.thread.utils;
public class ThreadUtils {
/** Make a runnable become debounce
*
* usage: to reduce the real processing for some task
*
* example: the stock price sometimes probably changes 1000 times in 1 second,
* but you just want redraw the candlestick of k-line chart after last change+"delay ms"
*
* @param realRunner Runnable that has something real to do
* @param delay milliseconds that realRunner should wait since last call
* @return
*/
public static Runnable debounce (Runnable realRunner, long delay) {
Runnable debounceRunner = new Runnable() {
// whether is waiting to run
private boolean _isWaiting = false;
// target time to run realRunner
private long _timeToRun;
// specified delay time to wait
private long _delay = delay;
// Runnable that has the real task to run
private Runnable _realRunner = realRunner;
@Override
public void run() {
// current time
long now;
synchronized (this) {
now = System.currentTimeMillis();
// update time to run each time
_timeToRun = now+_delay;
// another thread is waiting, skip
if (_isWaiting) return;
// set waiting status
_isWaiting = true;
}
try {
// wait until target time
while (now < _timeToRun) {
Thread.sleep(_timeToRun-now);
now = System.currentTimeMillis();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// clear waiting status before run
_isWaiting = false;
// do the real task
_realRunner.run();
}
}};
return debounceRunner;
}
/** Make a runnable become throttle
*
* usage: to smoothly reduce running times of some task
*
* example: assume the price of a stock often updated 1000 times per second
* but you want to redraw the candlestick of k-line at most once per 300ms
*
* @param realRunner
* @param delay
* @return
*/
public static Runnable throttle (Runnable realRunner, long delay) {
Runnable throttleRunner = new Runnable() {
// whether is waiting to run
private boolean _isWaiting = false;
// target time to run realRunner
private long _timeToRun;
// specified delay time to wait
private long _delay = delay;
// Runnable that has the real task to run
private Runnable _realRunner = realRunner;
@Override
public void run() {
// current time
long now;
synchronized (this) {
// another thread is waiting, skip
if (_isWaiting) return;
now = System.currentTimeMillis();
// update time to run
// do not update it each time since
// you do not want to postpone it unlimited
_timeToRun = now+_delay;
// set waiting status
_isWaiting = true;
}
try {
Thread.sleep(_timeToRun-now);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// clear waiting status before run
_isWaiting = false;
// do the real task
_realRunner.run();
}
}};
return throttleRunner;
}
}
答案 6 :(得分:0)
这是我正在执行的实现:
执行回调:
public interface cbDebounce {
void execute();
}
去抖动器:
public class Debouncer {
private Timer timer;
private ConcurrentHashMap<String, TimerTask> delayedTaskMap;
public Debouncer() {
this.timer = new Timer(true); //run as daemon
this.delayedTaskMap = new ConcurrentHashMap<>();
}
public void debounce(final String key, final cbDebounce debounceCallback, final long delay) {
if (key == null || key.isEmpty() || key.trim().length() < 1 || delay < 0) return;
cancelPreviousTasks(); //if any
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
debounceCallback.execute();
cancelPreviousTasks();
delayedTaskMap.clear();
if (timer != null) timer.cancel();
}
};
scheduleNewTask(key, timerTask, delay);
}
private void cancelPreviousTasks() {
if (delayedTaskMap == null) return;
if (!delayedTaskMap.isEmpty()) delayedTaskMap
.forEachEntry(1000, entry -> entry.getValue().cancel());
delayedTaskMap.clear();
}
private void scheduleNewTask(String key, TimerTask timerTask, long delay) {
if (key == null || key.isEmpty() || key.trim().length() < 1 || timerTask == null || delay < 0) return;
if (delayedTaskMap.containsKey(key)) return;
timer.schedule(timerTask, delay);
delayedTaskMap.put(key, timerTask);
}
}
主要(要测试)
public class Main {
private static Debouncer debouncer;
public static void main(String[] args) throws IOException, InterruptedException {
debouncer = new Debouncer();
search("H");
search("HE");
search("HEL");
System.out.println("Waiting for user to finish typing");
Thread.sleep(2000);
search("HELL");
search("HELLO");
}
private static void search(String searchPhrase) {
System.out.println("Search for: " + searchPhrase);
cbDebounce debounceCallback = () -> System.out.println("Now Executing search for: "+searchPhrase);
debouncer.debounce(searchPhrase, debounceCallback, 4000); //wait 4 seconds after user's last keystroke
}
}
输出
答案 7 :(得分:0)
我已经更新了@Eyal的答案,以便能够在每个呼叫中配置防弹跳时间,并使用可运行的代码块代替回调:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Debouncer<T> {
private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();
public Debouncer() {
}
public void call(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
TimerTask task = new TimerTask(key, runnable, interval, timeUnit);
TimerTask prev;
do {
prev = delayedMap.putIfAbsent(key, task);
if (prev == null)
sched.schedule(task, interval, timeUnit);
} while (prev != null && !prev.extend());
}
public void terminate() {
sched.shutdownNow();
}
private class TimerTask implements Runnable {
private final T key;
private final Runnable runnable;
private final int interval;
private final TimeUnit timeUnit;
private long dueTime;
private final Object lock = new Object();
public TimerTask(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
this.key = key;
this.runnable = runnable;
this.interval = interval;
this.timeUnit = timeUnit;
extend();
}
public boolean extend() {
synchronized (lock) {
if (dueTime < 0)
return false;
dueTime = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(interval, timeUnit);
return true;
}
}
public void run() {
synchronized (lock) {
long remaining = dueTime - System.currentTimeMillis();
if (remaining > 0) { // Re-schedule task
sched.schedule(this, remaining, TimeUnit.MILLISECONDS);
} else { // Mark as terminated and invoke callback
dueTime = -1;
try {
runnable.run();
} finally {
delayedMap.remove(key);
}
}
}
}
}
}
答案 8 :(得分:0)
这是我的实现,Java:
<块引用>SimpleDebounce.java
import android.os.Handler;
public class SimpleDebounce {
protected Handler handler;
protected IAfterDelay iAfterDelay;
protected long last_time_invoke = 0;
protected long delay;
public SimpleDebounce() {
this.handler = new Handler();
}
public SimpleDebounce(long delay, IAfterDelay iAfterDelay) {
this();
this.delay = delay;
this.iAfterDelay = iAfterDelay;
}
public void after(long delay, IAfterDelay iAfterDelay) {
this.delay = delay;
this.iAfterDelay = iAfterDelay;
this.iAfterDelay.loading(true);
this.handler.removeCallbacks(execute);
this.last_time_invoke = System.currentTimeMillis();
this.handler.postDelayed(execute, delay);
}
public void cancelDebounce() {
if (handler != null && iAfterDelay != null) {
handler.removeCallbacks(execute);
iAfterDelay.loading(false);
}
}
public interface IAfterDelay {
void fire();
void loading(boolean state);
}
protected Runnable execute = () -> {
if (System.currentTimeMillis() > (last_time_invoke + delay - 500)) {
if (iAfterDelay != null) {
iAfterDelay.loading(false);
iAfterDelay.fire();
}
}
};
}
<块引用>
MainActivity.java
public class MainActivity extends AppCompatActivity {
private SimpleDebounce simpleDebounce;
private long waitForMS = 5000;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_stocks);
simpleDebounce = new SimpleDebounce();
// You can click this button as many time as you want
// It will reset the time and fire after ${waitForMS} milisecons
// and in case you pressed this in the middle again, it will reset the time
someButtonWhichStartsThis.setOnClickListener(e -> {
simpleDebounce.after(waitForMS, new SimpleDebounce.IAfterDelay() {
@Override
public void fire() {
// Your job...
}
@Override
public void loading(boolean state) {
if (state) turnOnProgress();
else turnOffProgress();
}
});
});
// stop the future fire in the middle, if you want to
someButtonWhichStopsThis.setOnClickListener(e -> simpleDebounce.cancelDebounce());
}
}