在Java中实现去抖动

时间:2011-01-20 00:10:10

标签: java algorithm

对于我正在编写的一些代码,我可以在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)
  • 某些Java标准库中是否存在(类似的东西)?
  • 你会如何实现?

9 个答案:

答案 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
}

}

输出

  • 搜索:H
  • 搜索:HE
  • 搜索:HEL
  • 等待用户完成输入
  • 搜索:地狱
  • 搜索:HELLO
  • 现在正在执行搜索:HELLO

答案 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());

    }
   

}