Android Studio:用handler实现计数
一、哪里需要Handler?
1. UI 更新只能在主线程(UI 线程)中进行
在 Android 中,UI 操作必须在主线程中进行,不能直接在子线程中更新 UI。比如,修改 TextView
的内容、更新按钮的状态等操作都只能在主线程中执行。如果你尝试在子线程中直接更新 UI,程序会抛出 android.view.ViewRootImpl$CalledFromWrongThreadException
异常。
例如,假设你在子线程做了一个耗时操作,想要更新界面上的信息:
new Thread(new Runnable() {
@Override
public void run() {
// 做一些耗时操作(例如网络请求)
tvMessage.setText("任务完成!"); // 错误:子线程不能直接更新 UI
}
}).start();
上面代码会崩溃,因为 setText()
方法在 主线程 上调用,而这段代码在 子线程 中执行。
2. 如何解决这个问题?
我们需要将子线程中的任务发送到主线程来执行。这时,Handler
就可以帮上忙。你可以使用 Handler
将需要更新 UI 的操作传递到主线程,避免直接在子线程操作 UI。
Handler
解决了哪些问题?
1. 跨线程通信:
Handler
使得不同线程之间的通信变得简单。比如你在子线程执行一个耗时操作,完成后想更新主线程的 UI,Handler
可以帮助你将任务传递到主线程。
2. 延迟执行任务:
有时候你需要让某个任务在一段时间后再执行,或者周期性地执行。Handler
提供了 postDelayed()
和 post()
方法,允许你延迟执行任务或定时执行任务。
二、线程方法回顾
Java中简单的线程创建方法:
方式 1:继承 Thread
类
步骤:
- 创建一个类并继承
Thread
类。 - 重写
run()
方法,在其中编写要执行的任务。 - 创建
Thread
对象并调用start()
方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":正在执行 " + i);
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start(); // 启动线程
thread2.start(); // 启动线程
}
}
输出示例(结果顺序不确定,因为线程是并发执行的):
Thread-0:正在执行 0
Thread-1:正在执行 0
Thread-0:正在执行 1
Thread-1:正在执行 1
...
方式 2:实现 Runnable
接口(推荐)
步骤:
- 创建一个类并实现
Runnable
接口。 - 实现
run()
方法,编写任务逻辑。 - 创建
Thread
对象时,将Runnable
实现类传入构造方法。 - 调用
start()
启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行任务 " + i);
}
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
Thread thread2 = new Thread(myRunnable);
thread1.start();
thread2.start();
}
}
Runnable
方式避免 Java 单继承的限制,适用于多个线程共享同一个任务逻辑。
三、用handler实现计数器
准备一个简单的页面
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/btn_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="开始计数"
android:textColor="@color/black"
android:textSize="17sp" />
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
页面效果:
要实现的功能很简单,点击开始计数,下面的文本框内开始从0开始计数。这就意味着每过一秒就需要更新UI。问题在于,如何控制每次计数的中间间隔正好是1秒。
handler就有这样的机制,每隔固定时间启动一次任务类:
主页面完整代码
@SuppressLint("SetTextI18n")
public class HandlerPostActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_count; // 声明一个按钮对象
private TextView tv_result; // 声明一个文本视图对象
private boolean isStarted = false; // 是否开始计数
private Handler mHandler = new Handler(Looper.myLooper()); // 声明一个处理器对象
private int mCount = 0; // 计数值
// 定义一个计数任务
private Runnable mCounter = new Runnable() {
@Override
public void run() {
mCount++;
tv_result.setText("当前计数值为:" + mCount);
mHandler.postDelayed(this, 1000); // 延迟一秒后重复计数任务
}
};
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_post);
btn_count = findViewById(R.id.btn_count);
tv_result = findViewById(R.id.tv_result);
btn_count.setOnClickListener(this); // 设置按钮的点击监听器
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_count) {
if (!isStarted) { // 不在计数,则开始计数
btn_count.setText("停止计数");
mHandler.post(mCounter); // 立即启动计数任务
} else { // 已在计数,则停止计数
btn_count.setText("开始计数");
mHandler.removeCallbacks(mCounter); // 立即取消计数任务
}
isStarted = !isStarted;
}
}
}
private Handler mHandler = new Handler(Looper.myLooper());
什么是 Handler
?
Handler
是 Android 中用来处理线程任务的工具,它的作用是把任务(通常是一个 Runnable
)发送到线程中执行,或者延迟执行某个任务。我们通常用它来在子线程中做一些耗时操作,然后更新 UI(UI 更新必须在主线程进行)。
什么是 Looper
?
Looper
是 Android 中用来管理消息循环的机制。每个线程都可以有一个 Looper
,负责处理线程中的消息。主线程(UI 线程)默认有一个 Looper
,而子线程则需要手动创建一个 Looper
。
- 在主线程中,
Looper.myLooper()
返回的是主线程的Looper
。 - 在子线程中,你需要先调用
Looper.prepare()
创建一个Looper
,然后再创建Handler
。
Looper.myLooper()
获取当前线程的 Looper
对象。
- 如果你在 主线程 中执行这行代码,
myLooper()
会返回主线程的Looper
。 - 如果你在 子线程 中执行这行代码,
myLooper()
会返回当前子线程的Looper
。
Handler
的一个重要用途是在子线程中执行一些操作后,将任务切换到主线程执行。比如,假设你在子线程中做一些耗时操作(比如网络请求),完成后需要更新 UI(这只能在主线程进行),这时你就需要使用 Handler
来将任务切换到主线程执行。
2.2 定义计时任务
private Runnable mCounter = new Runnable() {
@Override
public void run() {
mCount++;
tv_result.setText("当前计数值为:" + mCount);
mHandler.postDelayed(this, 1000); // 延迟一秒后重复计数任务
}
};
效果: