Android语言基础教程(223)Android Handler消息传递机制范例之开启新线程实现电子广告牌:Android Handler防坑指南:别让UI线程“卡成狗”,手把手教你开新线程

  • 时间:2025-11-20 20:36 作者: 来源: 阅读:0
  • 扫一扫,手机访问
摘要:深度分析Android Handler消息传递机制 一、 引子:UI线程的“小暴脾气”与广告牌的诞生 想象一下,你正在一个超流畅的App里滑动屏幕,突然,画面一卡,整个应用直接罢工,弹出一个“应用未响应”的提示……是不是想砸手机?这就是触犯了Android世界的第一天条:绝对不要在UI线程(主线程)执行耗时操作! UI线程,顾名思义,就是负责处理和更新用户界面的“老大”。它是个急性子,要求所

深度分析Android Handler消息传递机制

一、 引子:UI线程的“小暴脾气”与广告牌的诞生

想象一下,你正在一个超流畅的App里滑动屏幕,突然,画面一卡,整个应用直接罢工,弹出一个“应用未响应”的提示……是不是想砸手机?这就是触犯了Android世界的第一天条:绝对不要在UI线程(主线程)执行耗时操作!

UI线程,顾名思义,就是负责处理和更新用户界面的“老大”。它是个急性子,要求所有界面操作都必须快速完成。如果你让它在主线程里执行网络请求、读取大文件,或者像咱们今天要做的——让广告牌的文字无限循环轮播,它就会被“阻塞”。它一不爽,整个界面就“冻住”了,用户怎么点都没反应,最终结果就是ANR崩溃。

那怎么办呢?“惹不起,咱躲得起”——开个新线程去干这些脏活累活呗!

但问题又来了:Android规定,只有UI线程才能触摸和更新界面。这就好比,后台小弟(新线程)辛辛苦苦把货搬来了,但不能直接放进前台(UI)的橱窗里,他得找个“传话员”去告诉前台经理该换展示品了。

这个伟大的“传话员”,就是咱们今天的主角——Handler

我们的目标:打造一个如图所示的电子广告牌,文字自动、平滑地轮播,同时主界面操作依然流畅得飞起。

二、 Handler核心机制:解剖Android的“消息快递系统”

别被“机制”俩字吓到,咱们把它想象成一个公司里的快递系统,秒懂!

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的七十二变

我们这个例子只是Handler的冰山一角。它还能这么玩:

post(Runnable r):如果你不需要传递复杂数据,只是想在主线程执行一段代码,可以直接 handler.post(() -> { // 更新UI的代码 });,更简洁。延迟消息 handler.sendMessageDelayed(msg, 3000) 可以让消息延迟3秒再处理。实现“3秒后跳转页面”等功能轻而易举。定时任务:结合 sendMessageDelayed handleMessage 里再给自己发一条延迟消息,就能实现一个简单的定时器,比用 TimerTask 更贴近Android机制。
六、 结语:从广告牌到高手

恭喜你!到这里,你已经不仅学会了一个“电子广告牌”的写法,更是亲手打通了Android多线程编程的“任督二脉”。Handler作为Android异步通信的基石,从简单的UI更新,到复杂的线程间协作,无处不在。

记住这个核心思想:UI线程是皇上,只负责最终决策(更新界面);后台线程是臣子,负责干活(处理数据);Handler就是那个上奏折和传圣旨的太监总管(传递消息)。

  • 全部评论(0)
手机二维码手机访问领取大礼包
返回顶部