Android - 初探 Handler

71 阅读5分钟

Android - 初探 Handler

Android Handler - 初探

  • 什么是Handler?
  • 它的作用是什么?
  • 什么场景下能用到它?
  • 深入探一探
    • 一个线程可以有几个Handler?
    • 线程间通信的原理是什么样的?
    • Looper是什么,作用是什么?

本篇文章你所能斩获的内容:

  • Handler基础定义、核心作用以及实际开发中的典型应用场景
  • 初步理解Handler机制的关键细节(线程与Handler数量关系、线程通信原理、Looper角色)
  • Handler使用过程中常见的问题 - 内存泄漏
  • 子线程中使用Handler的正确姿势

一. 什么是Handler,其作用是什么?

Handler是一套用于线程间通信、任务调度的核心组件,核心作用是解决同一个APP中不同线程间的数据传递和任务执行。

翻译一下这句话的意思是,在一个APP里面,通过Handler可以实现主线程和子线程之间的通信、子线程和子线程间的通信、子线程和主线程间的通信。

Handler的出现规避了什么问题呢?

  • 在Android开发中,更新UI只能在UI线程中进行更新,而不能通过其他线程进行UI的更新,但是如果在UI线程中处理耗时操作,一旦时间超过规定时间(5s)就会引发程序UI线程阻塞崩溃(ANR)异常。而Handler作为线程间的通信桥梁,我们就能实现在非UI线程中去处理耗时操作,在非UI线程中通过Handler向UI线程发送消息/任务,由UI线程在合适的时机处理,刷新UI。规避了UI线程阻塞引起的ANR异常。

二. Handler的使用场景是什么?

理解了Handler是个什么东西,它的出现是为了什么,能达到什么样的结果。再考虑它的使用场景,我想,你应该是手到擒来吧。

  • 在执行IO耗时操作、网络请求、文件读写、数据库操作等耗时操作时,会出现页面的卡顿现象,可能只是一瞬,也可能是肉眼可见的卡顿,原因就是因为在UI线程中执行了耗时的操作,导致UI线程的阻塞卡顿引起的页面卡顿。

    • 怎么解决呢?
      • 这就需要用到主角Handler,注意观察下面这段代码
    // 在主线程中创建Handler
    (注意:这儿是需要绑定主线程的Looper的,这样该任务执行时就是在UI线程中进行的,可以直接对相关UI进行操作不会引发异常)
    private val mainHandler = Handler(Looper.getMainLooper()) { msg ->
        when(msg.what) {
            1-> {
                // 获取子线程传递的实际数据
                val data = msg.obj as String
                // 更新UI    
                findViewById<TextView>(R.id.text).text = data
            }
        }
        true
    }
    
    // 在子线程(IO线程中)执行耗时的操作
    thread {
        // 模拟IO耗时操作:接口请求等耗时操作    
        Thread.sleep(2000)  
        val result = "接口返回的数据"
    
        // 通过Handler向主线程发送消息(携带返回的结果)
        val msg = Message.obtain(mainHandler,1,result)
        mainHandler.sendMessage(msg)
    }
    

    在这段代码中就很详细的展示了Handler的简单使用,也是我们最常见的使用场景

  • 现在再回想一下,你一定在某一款APP中见到过这样一个页面跳转。点击APP图标启动APP,就会出现欢迎页,三秒后就跳转到登录页这个过程。这个过程就属于延迟任务的执行,可以通过Handler的postDelayed()、sendEmptyMessageDelayed()等方法实现

    // 1. 延迟跳转的实现
    mainHandler.postDelayed({
        startActivity(Intent(this,MainActivity::))
        finish()
    },3000) // 延迟3秒执行

    // 2. 定时任务的实现    
    // 倒计时10秒的实现
    private var count = 10
    private val countHandler = Handler(Looper.getMainLooper())
    private val countRunnable = object:Runnable {
        override fun run() {
            tvCount.texat = "倒计时: $count"
            count--
            if(count >= 0) {
                // 一秒重复执行,形成定时循环    
                countHandler.postDelayed(this,1000)
            } else {
                tvCount.text = "倒计时结束"
            }
        }
    }

    // 启动倒计时
    countHandler.post(countRunnable)

为什么用Handler呢?

  • 相比Timer/TimerTask,Handler更轻量、无线程安全问题(任务执行是在绑定的线程中执行的,无需跨线程)
  • 若Handler绑定的是UI线程的Looper,可以直接进行UI的更新,无需进行线程切换

三. 一个线程可以有几个Handler?

说到线程的问题,很多小伙伴就会想到,那么一个线程里面可以有多少个Handler呢?答案是多个,一个线程可以创建多个Handler,这些Handler会绑定到该线程唯一的Looper上,所有的Handler发送的消息和任务都会进入Looper关联的同一个MessageQueue,Looper通过loop()方法取出Message,根据Message的target字段分发给对应的Handler处理,最终所有任务都在Looper所在的线程串行执行。 小小验证一波

class MainActivity:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // UI 线程创建第一个Handler
        val handler1 = Handler(Looper.getMainLooper()){msg->
            Log.d("HandlerTest", "handler1处理消息:${msg.what},线程:${Thread.currentThread().name}")
            true
        }

        // UI 线程创建第二个Handler    
        val handler2 = Handler(Looper.getMainLooper()) { msg ->
            Log.d("HandlerTest", "handler2处理消息:${msg.what},线程:${Thread.currentThread().name}")
            true
        }        

        // 验证两个Handler是否绑定同一个Looper,是否都为UI线程的MainLooper
        Log.d("HandlerTest", "handler1的Looper:${handler1.looper}")
        Log.d("HandlerTest", "handler2的Looper:${handler2.looper}")
        Log.d("HandlerTest", "主线程Looper:${Looper.getMainLooper()}")

        
        // 测试Handler发送消息
        handler1.sendEmptyMessage(1)
        handler2.sendEmptyMessage(2)
    }
}

五. 线程间通信的原理是什么?

作为标准的"程序猿",大家都大概率思考一个问题,不同线程间的通信方式是什么样的?在实际的开发中,我们经常会遇到这样的场景,一个线程处理的数据,需要在另一个线程中被使用或处理。

  • 线程间通信的核心原理 线程是进程的执行单元,同一进程内的所有线程共享该进程的地址空间,这是线程间能进行通信的物理基础。
  • 常用的线程通信方式
    1. 共享内存模式(线程安全的共享变量/集合),这是最基础的方式。通过线程安全的容器(如ConcurrentHashMap、CopyOnWriteArrayList)或加锁的共享变量,让线程间共享数据。
    fun main() {
        val testList = CopyOnWriteArrayList<String>()
    
        // 写数据的线程
        val writeThread = Thread {
            for(i in 1..5) {
                testList.add(i)
                Thread.sleep(2000) // 模拟耗时操作
            }
        }
    
        // 读数据的线程    
        val readerThread = Thread {
            // 循环读取,当集合中没有数据时循环等待
            while(testList.size < 5) {
                Thread.sleep(300)
            }
        }
    
        writeThread.start()
        readerThread.start()
    
        // 等待线程执行完成    
        writeThread.join()
        readerThread.join()
    }
    
    1. 生产者消费者,使用wait()和notify()实现生产者消费者模型
    fun main() {
        // 共享缓冲区    
        val buffer = LinkedList<String>()
        val maxSize = 3 // 缓冲区最大容量
    
        // 生产者线程
        val producer = Thread {
            for(i in 1..5) {
                synchronized(buffer) {
                    // 如果是缓冲区满了,生产者就该等待    
                    while(buffer.size == maxSize) {
                        buffer.wait()
                    }
                    // 生产数据    
                    val data = "生产的第$i个"
                    buffer.add(data)
                    // 唤醒消费者
                    buffer.notify()
                }
                // 模拟生产者的耗时
                Thread.sleep(500)
            }
        }
    
        // 消费者线程    
        val consumer = Thread {
            for(i in 1..5) {
                synchronized(buffer) {
                    // 缓冲区空了,消费者挂起等待    
                    while(buffer.isEmpty()) {
                        buffer.wait()
                    }
                    // 消费者拿数据消费    
                    val data = buffer.removeFirst()
                    // 消费者取出数据后,唤醒生产者继续生产
                    buffer.notify()
                }
                // 模拟消费者耗时
                Thread.sleep(800)
            }
        }
    
        producer.start()    
        consumer.start()
    
        producer.join()    
        consumer.join()
    }
    
  • 总结: 线程间通信的核心基础是同一个进程内的线程共享进程的地址空间 说白了就是同一个进程内的线程共享内存

六. Looper是什么?

Looper是Android为线程提供的消息循环机制,它的核心是为线程绑定一个MessageQueue,并通过无限循环将消息分发给对应的Handler。 通俗点说:Looper就像是循环传送带上的动力装置,负责让传送带(MessageQueue,消息队列)持续循环转动,传送带上的每一个东西就是Message,由不同的人(Handler)放到传送带上,而动力装置(Looper)会把传送带上的东西依次送到对应的接收人(Handler)的手中。

好的,后会有期~ 下一期将从源码入手,再探Handler