想象一下,你正在一个超流畅的App里滑动屏幕,突然,画面一卡,整个应用直接罢工,弹出一个“应用未响应”的提示……是不是想砸手机?这就是触犯了Android世界的第一天条:绝对不要在UI线程(主线程)执行耗时操作!
UI线程,顾名思义,就是负责处理和更新用户界面的“老大”。它是个急性子,要求所有界面操作都必须快速完成。如果你让它在主线程里执行网络请求、读取大文件,或者像咱们今天要做的——让广告牌的文字无限循环轮播,它就会被“阻塞”。它一不爽,整个界面就“冻住”了,用户怎么点都没反应,最终结果就是ANR崩溃。
那怎么办呢?“惹不起,咱躲得起”——开个新线程去干这些脏活累活呗!
但问题又来了:Android规定,只有UI线程才能触摸和更新界面。这就好比,后台小弟(新线程)辛辛苦苦把货搬来了,但不能直接放进前台(UI)的橱窗里,他得找个“传话员”去告诉前台经理该换展示品了。
这个伟大的“传话员”,就是咱们今天的主角——Handler。
我们的目标:打造一个如图所示的电子广告牌,文字自动、平滑地轮播,同时主界面操作依然流畅得飞起。
别被“机制”俩字吓到,咱们把它想象成一个公司里的快递系统,秒懂!
Looper(循环器/仓库管理员):每个线程想收快递(消息),都得先雇一个Looper。它的工作就是死循环,不停地检查自己的“仓库”——MessageQueue里有没有新快递。UI线程天生就自带一个Looper,所以它能直接使用Handler。而我们开的新线程,如果想收消息,就得手动调用
Looper.prepare() 和
Looper.loop() 来请这位管理员。MessageQueue(消息队列/仓库):一个先进先出的队列,专门用来存放Message。Looper就是从这个仓库里一件一件地取快递。Message(消息/快递包裹):这就是传递信息的基本单位。一个Message里可以装很多东西:
what(标识码,像快递单号)、
arg1、
arg2(整型数据)、
obj(一个Object对象,比如字符串),等等。Handler(处理者/快递小哥+业务员):这是咱们程序员直接打交道的对象。它有两项绝活:
送快递(Send Message):可以在任何线程(比如后台线程)调用
handler.sendMessage(msg),把消息扔到它关联的那个线程的MessageQueue里排队。处理快递(Handle Message):当Looper从队列里取出这条消息时,会回调Handler的
handleMessage(Message msg) 方法。关键来了:这个回调方法,是运行在Handler所关联的线程上的! 如果Handler关联的是UI线程,那么
handleMessage里就可以安全地更新UI了。
整个工作流程,就像一部精彩的职场剧:
后台线程(搬砖小弟)想更新UI(换橱窗展示),但它没权限。于是它把新内容打包成一个Message(快递包裹),交给一个关联了UI线程的Handler(专属快递小哥)。Handler小哥把这个包裹投递到UI线程的MessageQueue(前台仓库)里。UI线程的Looper(前台仓库管理员)一直在摸鱼…啊不,在循环检查,一看到有新包裹,立马拿出来,叫来Handler小哥:“你的快递,你自己拆!” Handler小哥就在UI线程的地盘上拆开包裹(执行
handleMessage),并根据里面的指示,安全地更新了界面。
理解了这套“宫心计”,代码写起来就豁然开朗了!
接下来,就是见证奇迹的时刻!我们将创建一个Activity,里面包含一个TextView作为我们的广告牌显示屏,一个Button用来开始和停止轮播。
1. 布局文件(activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tv_billboard"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#f0f0f0"
android:gravity="center"
android:text="欢迎光临!"
android:textColor="#333"
android:textSize="24sp" />
<Button
android:id="@+id/btn_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始轮播" />
</LinearLayout>
2. Java代码(MainActivity.java)—— 含详细注释
public class MainActivity extends AppCompatActivity {
private TextView tvBillboard;
private Button btnControl;
private Handler mHandler; // 我们的主心骨——Handler
private Thread mWorkThread; // 在后台默默搬砖的线程
private volatile boolean isRunning = false; // 控制轮播的开关,volatile保证线程可见性
// 广告语素材库
private final String[] advertisements = {
"全场大促,买一送一!",
"新用户立享100元红包!",
"今晚8点,直播间抽奖送手机!",
"最后的清仓,错过等一年!",
"品质保证,假一赔十!"
};
private int currentIndex = 0; // 当前显示的广告索引
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initHandler();
setClickListener();
}
private void initView() {
tvBillboard = findViewById(R.id.tv_billboard);
btnControl = findViewById(R.id.btn_control);
}
private void initHandler() {
// 创建Handler对象,并绑定到主线程(UI线程)的Looper
// 因此,它的handleMessage方法将在UI线程执行,可以安全更新UI
mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
// 在这里处理从后台线程发来的消息
switch (msg.what) {
case 1: // 用what来区分消息类型,这里1代表更新广告牌
String newText = (String) msg.obj; // 从消息中取出字符串
tvBillboard.setText(newText); // 安全更新UI!
break;
// 还可以有 case 2, case 3... 处理其他类型的消息
}
}
};
}
private void setClickListener() {
btnControl.setOnClickListener(v -> {
if (!isRunning) {
// 如果没在运行,就启动轮播
startBillboard();
btnControl.setText("停止轮播");
} else {
// 如果在运行,就停止
stopBillboard();
btnControl.setText("开始轮播");
}
});
}
private void startBillboard() {
isRunning = true;
// 创建并启动后台线程
mWorkThread = new Thread(() -> {
// 这是一个典型的Looper线程工作模式
try {
while (isRunning) {
// 1. 从素材库获取下一条广告
String adText = advertisements[currentIndex];
currentIndex = (currentIndex + 1) % advertisements.length; // 循环索引
// 2. 创建一个Message对象,并设置它的内容
Message message = mHandler.obtainMessage(); // 推荐这样获取,效率高
message.what = 1; // 设置消息标识
message.obj = adText; // 设置要传递的数据
// 3. 通过Handler发送消息到主线程的消息队列
mHandler.sendMessage(message);
// 4. 让线程休眠2秒,模拟轮播间隔
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
// 如果休眠被中断,说明可能被要求停止了,安静退出即可
}
});
mWorkThread.start(); // 线程开始搬砖!
}
private void stopBillboard() {
isRunning = false; // 关闭开关
if (mWorkThread != null) {
mWorkThread.interrupt(); // 礼貌地请求中断线程
mWorkThread = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 当Activity销毁时,务必停止后台线程,避免内存泄漏
stopBillboard();
// 移除Handler所有未处理的消息,这也是好习惯
mHandler.removeCallbacksAndMessages(null);
}
}
mHandler的创建:我们传入
Looper.getMainLooper(),明确指定它关联主线程。这样,所有通过它发送的消息,最终都会在主线程被处理。
obtainMessage():为什么不直接
new Message()?因为Android内部维护了一个消息池,
obtainMessage()是从池里取,用完后会回收,减少了对象创建销毁的开销,更高效。
volatile关键字:
isRunning这个标志位会被两个线程(UI线程和我们的工作线程)访问。
volatile确保了当一个线程修改了它的值,另一个线程能立刻看到最新值,避免了因线程缓存导致的“停不下来”的bug。异常处理:在
Thread.sleep() 时,我们捕获了
InterruptedException。这是为了当调用
thread.interrupt() 试图停止线程时,能让线程从睡眠中优雅地醒来并退出。资源清理:
onDestroy里做的两件事(停止线程、移除Handler消息)是防止内存泄漏的好习惯,务必记住!
我们这个例子只是Handler的冰山一角。它还能这么玩:
post(Runnable r):如果你不需要传递复杂数据,只是想在主线程执行一段代码,可以直接
handler.post(() -> { // 更新UI的代码 });,更简洁。延迟消息:
handler.sendMessageDelayed(msg, 3000) 可以让消息延迟3秒再处理。实现“3秒后跳转页面”等功能轻而易举。定时任务:结合
sendMessageDelayed 在
handleMessage 里再给自己发一条延迟消息,就能实现一个简单的定时器,比用
TimerTask 更贴近Android机制。
恭喜你!到这里,你已经不仅学会了一个“电子广告牌”的写法,更是亲手打通了Android多线程编程的“任督二脉”。Handler作为Android异步通信的基石,从简单的UI更新,到复杂的线程间协作,无处不在。
记住这个核心思想:UI线程是皇上,只负责最终决策(更新界面);后台线程是臣子,负责干活(处理数据);Handler就是那个上奏折和传圣旨的太监总管(传递消息)。