
还记得你第一次做轮播图时的崩溃吗?图片死活不自动切换,或者切换时卡得亲妈都不认识?又或者,倒计时功能写着写着就内存泄漏了?
别问我是怎么知道的(抹泪)。今天咱们就来深扒Android里这个最常用,却又最容易被“想当然”使用的组件——计时器。
先别急着敲代码。咱们想想,计时器在App里到底在扮演什么角色?
轮播图:每3秒自动滑到下一张美女(哦不,是产品图)消息刷新:微信那个“正在输入…”的鬼畜效果游戏技能CD:亚瑟的一技能还有2秒就好!倒计时:双11秒杀,“还有01:23:45就要开始了!”看出来没?计时器本质上就是个“时间触发器”——到点了,就捅一下你的代码:“喂,该干活了!”
但在Android这个大江湖里,能干活的时间触发器可不止一个。接下来就请出我们的三位参赛选手!
这哥们是Android里的老前辈了,虽然年纪大,但地位稳如泰山。
它的工作流程是这样的:
// 1. 先雇个保姆(Handler)
private Handler mHandler = new Handler();
// 2. 让保姆定期叫你起床
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
// 这里写你要定期干的事
updateUI(); // 比如更新界面
// 重要!干完活记得再安排下一次
mHandler.postDelayed(this, 1000); // 1秒后再来
}
};
// 3. 开始计时
mHandler.postDelayed(mRunnable, 1000);
// 4. 想停止的时候
mHandler.removeCallbacks(mRunnable);
Handler的内心独白:“年轻人别老想着花里胡哨的,我这一套方法用了多少年,稳定!”
适用场景:简单的UI更新,比如进度条、动画效果。
坑点警告:
忘掉
removeCallbacks?恭喜你,内存泄漏大礼包一份!在子线程里直接操作UI?Crash正在赶来的路上!
这位是Java原生家族派来的代表,在很多Java项目里混得风生水起。
基本操作:
// 搭个班子
private Timer mTimer;
private TimerTask mTimerTask;
private void startTimer() {
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
// 注意:这里是在子线程!
doBackgroundWork(); // 适合后台计算
// 想更新UI?得找Handler帮忙
}
};
// 立即开始,每隔1秒执行一次
mTimer.schedule(mTimerTask, 0, 1000);
}
// 记得清理现场!
private void stopTimer() {
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
}
Timer的傲娇宣言:“我可是正儿八经的Java血统,精度高,功能强!”
但是(敲黑板):
默认不在主线程,更新UI得绕路
Timer.cancel()有时候会耍小性子,不是100%可靠异常处理不当?整个Timer直接罢工给你看
这是Google看大家用前两位用得实在太痛苦,于是亲手打造的“官方解决方案”。
看这优雅的写法:
// 倒计时60秒,每隔1秒回调一次
CountDownTimer countDownTimer = new CountDownTimer(60000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
// 每次倒计时时的回调
long seconds = millisUntilFinished / 1000;
mTextView.setText(seconds + "秒后世界毁灭");
}
@Override
public void onFinish() {
// 倒计时结束
mTextView.setText("嘭!世界毁灭了!");
}
};
// 开始倒计时
countDownTimer.start();
// 想提前结束?
countDownTimer.cancel();
CountDownTimer的优势:
专门为倒计时场景设计,API简单到哭自动在主线程回调,UI操作so easy生命周期管理相对省心局限性: 只能用于倒计时,想做周期性任务?还是回去找Handler吧。
光说不练假把式,来做个实际项目——健身App用的秒表!
需求分析:
开始/暂停/重置功能精确到0.1秒更新即使手机锁屏也要能继续计时(服务保活)上代码!
public class StopwatchActivity extends AppCompatActivity {
private TextView mTimeText;
private Button mStartBtn, mPauseBtn, mResetBtn;
private long mStartTime = 0;
private boolean mIsRunning = false;
// 选用我们的老将:Handler
private Handler mHandler = new Handler();
private Runnable mUpdateTimeRunnable = new Runnable() {
@Override
public void run() {
if (mIsRunning) {
long currentTime = System.currentTimeMillis() - mStartTime;
updateTimeDisplay(currentTime);
// 0.1秒后再次更新
mHandler.postDelayed(this, 100);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stopwatch);
initViews();
updateButtons();
}
private void initViews() {
mTimeText = findViewById(R.id.time_text);
mStartBtn = findViewById(R.id.start_btn);
mPauseBtn = findViewById(R.id.pause_btn);
mResetBtn = findViewById(R.id.reset_btn);
mStartBtn.setOnClickListener(v -> start());
mPauseBtn.setOnClickListener(v -> pause());
mResetBtn.setOnClickListener(v -> reset());
}
private void start() {
if (!mIsRunning) {
mStartTime = System.currentTimeMillis() - (mStartTime == 0 ? 0 :
(System.currentTimeMillis() - mStartTime));
mIsRunning = true;
mHandler.post(mUpdateTimeRunnable);
updateButtons();
}
}
private void pause() {
mIsRunning = false;
mHandler.removeCallbacks(mUpdateTimeRunnable);
updateButtons();
}
private void reset() {
mIsRunning = false;
mHandler.removeCallbacks(mUpdateTimeRunnable);
mStartTime = 0;
updateTimeDisplay(0);
updateButtons();
}
private void updateTimeDisplay(long millis) {
int totalSeconds = (int) (millis / 1000);
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
int tenths = (int) (millis % 1000) / 100;
String time = String.format("%02d:%02d.%d", minutes, seconds, tenths);
mTimeText.setText(time);
}
private void updateButtons() {
mStartBtn.setEnabled(!mIsRunning);
mPauseBtn.setEnabled(mIsRunning);
mResetBtn.setEnabled(!mIsRunning && mStartTime != 0);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 重要!避免内存泄漏
mHandler.removeCallbacks(mUpdateTimeRunnable);
}
}
这个实现的关键点:
用
System.currentTimeMillis()做时间基准,比依赖固定的时间间隔更准确每次点击暂停时记录当前时间,恢复时重新计算起始时间在
onDestroy里一定要清理Handler,这是程序员的自我修养
你的App可能会因为旋转屏幕而重启Activity,这时候计时器状态就丢了。怎么办?
解决方案: 用
ViewModel +
LiveData
public class StopwatchViewModel extends ViewModel {
private MutableLiveData<Long> mElapsedTime = new MutableLiveData<>();
private long mStartTime;
private boolean mIsRunning = false;
public void start() {
if (!mIsRunning) {
mStartTime = System.currentTimeMillis() -
(mElapsedTime.getValue() != null ? mElapsedTime.getValue() : 0);
mIsRunning = true;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (mIsRunning) {
mElapsedTime.setValue(System.currentTimeMillis() - mStartTime);
new Handler().postDelayed(this, 100);
}
}
}, 100);
}
}
// 省略pause、reset等方法...
@Override
protected void onCleared() {
super.onCleared();
mIsRunning = false;
}
}
用户切到后台,秒表还得继续跑啊!这就需要服务了:
public class TimerService extends Service {
// 前台服务保证不被杀
// 用Notification让用户知道你在运行
// 结合BroadcastReceiver更新界面
}
不过要小心:Android对后台服务的限制越来越严,得按规矩来!
postDelayed不保证精确时间,系统忙时会延迟 → 关键场景用
SystemClock.elapsedRealtime()线程混乱:在Timer里直接更新UI → 记住:只有主线程能动UI!生命周期失控:Activity都销毁了,计时器还在跑 → 结合生命周期回调管理电量杀手:无脑用AlarmManager做高频定时 → 考虑用WorkManager替代
记住,没有最好的计时器,只有最合适的场景。选对了,你的App丝般顺滑;选错了,就等着测试同事提着Bug来找你吧!
现在,去优雅地处理你的时间任务吧!毕竟,好的计时器就像好的伴侣——它在该出现的时候出现,不该出现的时候默默等待,而且永远不会让你卡住(希望如此)!
延伸思考:其实计时器的选择折射出Android开发的演进——从最初的Handler打天下,到后来各种架构组件的出现,再到如今对性能、电量的极致追求。你的代码风格,是不是也该跟着进化了呢?
(小声说:如果这篇文章帮到了你,请在心里默默点个赞~如果还有问题,欢迎在评论区“骚扰”我!)