GridView中多Runnable+Handler定时器运行数分钟后出现跳秒问题
Hey there, let's break down why your GridView timers are skipping seconds after running for 15 minutes, and walk through how to fix these issues properly.
核心问题分析
Let's start with the key flaws in your current implementation:
1. Runnable管理完全混乱
- Every time
getViewis called (like when the GridView scrolls or you callnotifyDataSetChanged), you create a newRunnableand add it toupdateTimerThread. Over time, this list gets stuffed with useless, duplicate Runnables—meaning multiple threads might update the same item at once, causing time display chaos. - You're using
updateTimerThread.get(position + 1)to trigger the timer, which is a clear index mistake. Position starts at 0, so this will either grab the wrong Runnable or throw an out-of-bounds error entirely.
2. Time calculation relies on imprecise postDelayed
- Using
postDelayed(this, 1000)to update every second isn't perfectly accurate. If the main thread is busy with other UI tasks or layout calculations, the actual delay will be longer than 1 second. Your current logic accumulates this error over time, which is why you see noticeable skips after 15 minutes. - The way you reset
startTimewhen starting the timer also isn't properly calibrated for paused/resumed states, adding to the inaccuracy.
3. GridView reuse causes state glitches
- GridView recycles
convertViewto save memory, but you never clean up old Runnable callbacks when reusing a ViewHolder. This means old timers keep running in the background and clash with new ones, making the time jump around.
4. State switch logic is sloppy
- When pausing or stopping a timer, you try to remove callbacks but fail because of the index error or duplicate Runnables. This leaves timers running silently in the background, so when you restart them, the time calculation is way off.
修复方案与优化指导
Here's how to fix these issues step by step:
1. Bind Runnables directly to your data model
Instead of a global list, store each timer's Runnable directly in the Orders object—this ensures one timer per data item, no duplicates. First, add these fields to your Orders class:
private Runnable timerRunnable; private boolean isTimerRunning;
Then rewrite the getView method to manage timers properly:
@Override public View getView(final int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { holder = new ViewHolder(); convertView = lf.inflate(R.layout.list_item3, parent, false); holder.status = convertView.findViewById(R.id.status); holder.tvProduct = convertView.findViewById(R.id.tvProduct); holder.tvRunningTime = convertView.findViewById(R.id.tvTimeRemaining); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } final Orders order = getItem(position); holder.tvProduct.setText("Token : " + order.getTableno()); // Clean up old timer first to avoid duplicates if (order.getTimerRunnable() != null) { MeterActivity.mHandler.removeCallbacks(order.getTimerRunnable()); } // Use WeakReference to avoid memory leaks final WeakReference<ViewHolder> holderRef = new WeakReference<>(holder); Runnable timerRunnable = new Runnable() { @Override public void run() { ViewHolder currentHolder = holderRef.get(); if (currentHolder == null || !order.isTimerRunning()) { return; } // Calculate time accurately using absolute system time long currentUptime = SystemClock.uptimeMillis(); long elapsedTime = currentUptime - order.getStartTime() + order.getTimeSwapBuff(); int seconds = (int) (elapsedTime / 1000); int minutes = seconds / 60; seconds = seconds % 60; currentHolder.tvRunningTime.setText(String.format("%d:%02d", minutes, seconds)); order.setRate((long) (minutes * order.getIntervaltime())); MeterActivity.mHandler.postDelayed(this, 1000); } }; order.setTimerRunnable(timerRunnable); // Handle each state cleanly switch (order.getStatus()) { case 0: // Not Started holder.status.setText("Status : Not Started"); order.setTimerRunning(false); holder.tvRunningTime.setText("0:00"); break; case 1: // Ready to Start holder.status.setText("Status : Ready to Start"); order.setTimerRunning(false); holder.tvRunningTime.setText("0:00"); break; case 2: // Started holder.status.setText("Status : Started"); order.setTimerRunning(true); // Calibrate start time for first run or resume if (order.getTimeSwapBuff() == 0) { order.setStartTime(SystemClock.uptimeMillis()); } else { order.setStartTime(SystemClock.uptimeMillis() - order.getTimeSwapBuff()); } MeterActivity.mHandler.post(timerRunnable); break; case 3: // Paused holder.status.setText("Status : Paused"); order.setTimerRunning(false); // Save elapsed time to resume later long elapsedPause = SystemClock.uptimeMillis() - order.getStartTime() + order.getTimeSwapBuff(); order.setTimeSwapBuff(elapsedPause); MeterActivity.mHandler.removeCallbacks(timerRunnable); break; case 4: // Stopped holder.status.setText("Status : Stopped"); order.setTimerRunning(false); order.setTimeSwapBuff(0); order.setStartTime(0); MeterActivity.mHandler.removeCallbacks(timerRunnable); holder.tvRunningTime.setText("0:00"); break; } return convertView; }
2. Calculate time using absolute system time
Instead of accumulating time based on postDelayed intervals, we calculate elapsed time directly using SystemClock.uptimeMillis() every time the Runnable runs. This eliminates cumulative errors—even if the delay is off, the displayed time will always be accurate.
3. Clean up timers when the Activity is destroyed
To avoid memory leaks, add this to your MeterActivity's onDestroy method:
@Override protected void onDestroy() { super.onDestroy(); for (Orders order : yourOrderList) { if (order.getTimerRunnable() != null) { mHandler.removeCallbacks(order.getTimerRunnable()); } } }
4. Simplify BroadcastReceiver handling
When your BroadcastReceiver triggers a state change (start/pause/stop), just update the corresponding Orders object's status and call notifyDataSetChanged(). The Adapter will handle starting/stopping the timer automatically in getView.
内容的提问来源于stack exchange,提问作者user3329075

