2022年已过1/4,时间过的真是快。近些年大Android的发展也很是迅速,尤其是遵循MVVM或者MVI架构下,使用Jetpack + Kotlin + Corroutine + Flow的组合,大大提升了Android应用的开发效率。然而,类似的效率的提升往往是通过层层封装,隐藏底层原理,简化调用,从而达到降低开发的上手门槛目的的。作为一个有品位的开发者,又怎能满足于只了解上层的API调用。本文我们就来通过实例来试着聊一聊Android经典线程通信框架Handler的基本原理。
通过实现一个简单的Handler框架,试图解释其底层的工作原理。
线程间通信
即不同线程之间交换信息,Java中常见的有:
Android的Handler工作原理
简言之,重复执行“发送消息-分发消息-处理消息”。
更准确的问题描述应该是:Android的基于Handler + Message + MessageQueue + Looper的消息机制是怎么工作的?
Handler借助
handler#sendMessage(message)方法,把消息Message对象插入消息队列MessageQueue对象中,同时Looper对象循环往复地从MessageQueue对象中取下一个消息Message对象分发给Looper对象所在线程去处理,如此循环往复。
注意: 同一个线程中,Looper和MessageQueue有且只有一个实例,能且只能在该线程内部实例化。Handler可有多个实例,可在任意线程内实例化,但前需保证实例化动作所在线程的Looper已被初始化。另外,也可以通过调用带Looper参数的Handler构造器实例化,以达到关联指定线程(Looper所在)的目的。
从上述描述中,我们可以得出几个关键的点:
消息对象:保存消息的数据结构(会保存发送者的Handler对象引用)
发送消息:调用者通过Handler#sendMessage(message)发送消息(发到哪里?缓存MessageQueue)
缓存消息:MessageQueue保存由外部发送来的消息(怎么保存?类似单向链表结构)
读取消息:Looper#loop()内调用MessageQueue.next()读取消息(没消息了怎么办?阻塞)
分发消息:取到消息后,执行消息对象持有的Handler对象的Handler#dispatchMessage(message)方法。(运行在哪个线程?Looper对象所在的线程)
处理消息:Handler#dispatchMessage(message)内再调用Handler#handleMessage(message)方法,后者便正是调用者常重写的方法。
循环往复:Looper#loop()内部是一个for(;;){...},一直执行,提供循环往复的驱动力。
线程内Looper对象唯一性:依靠ThreadLocal保证每个线程内部Looper唯一,即只被实例化一次。
线程内MessageQueue对象唯一性:被Looper实例化并持有,间接保证了唯一性。
应用启动时,
ActivityThread.main(String[])内部会提前初始化Looper。故在主线程内部,可直接使用Handler的无参数构造器实例化。
如上所述,子线程使用Handler需要先确保Looper已被初始化,但子线程默认是没有初始化Looper的,故需在子线程执行的一开始主动调用
Looper.prepare()和Looper.loop()。此后则可如在主线程一般任意实例化Handler。
即封装了Handler的Thread子类。
HandlerThread#start()被调用后,该线程内部会初始化Looper实例。其他线程可从该HandlerThread对象中取出Looper实例,并用它构造出新的Handler对象。此后其他线程可借助该Handler对象,调用Handler#sendMessage(message)给HandlerThread的线程发送消息。
在了解了上述的Android的Handler工作原理后,自己实现一个简单的消息框架就有方向了,无非就是解决上述中提到的“消息对象”,“发送消息”,“缓存消息”……关键点即可。
线程内Looper对象唯一性,线程内MessageQueue对象唯一性和读取消息是需要着重考虑的。
Message
package com.custom.handler
data class Message(
val what: Int = 0,
val args: Any? = null,
val block: (() -> Unit)? = null,
) {
companion object {
val HEAD_MESSAGE: Message = Message()
}
var target: Handler? = null
var next: Message? = null
}
MessageQueue
package com.custom.handler
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock
class MessageQueue {
private val queueLock = ReentrantLock()
private val queueCondition = queueLock.newCondition()
private val messageLock = ReentrantReadWriteLock()
private val messageReadLock = messageLock.readLock()
private val messageWriteLock = messageLock.writeLock()
private var currentMessage: Message = Message.HEAD_MESSAGE
get() = messageReadLock.withLock { field }
set(value) = messageWriteLock.withLock { field = value }
fun next(): Message {
while (true) {
queueLock.withLock {
if (currentMessage.next == null) {
println("[${Thread.currentThread().name}] Message queue is empty, then it is going to await...")
queueCondition.await()
println("[${Thread.currentThread().name}] Message queue wakes up.")
}
}
val nextMessage = currentMessage.next!!
nextMessage.next = null
currentMessage = nextMessage
return nextMessage
}
}
fun enqueue(msg: Message) {
messageWriteLock.withLock {
msg.next = null
currentMessage.next = msg
queueLock.withLock { queueCondition.signalAll() }
}
}
}
Looper
package com.custom.handler
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock
class Looper private constructor(thread: Thread) {
internal val mMessageQueue = MessageQueue()
private val mThread = thread
private val quitFlagReentrantReadWriteLock = ReentrantReadWriteLock()
private val quitFlagReadLock = quitFlagReentrantReadWriteLock.readLock()
private val quitFlagWriteLock = quitFlagReentrantReadWriteLock.writeLock()
private var quitFlag: Boolean? = null
get() = quitFlagReadLock.withLock { field }
set(value) = quitFlagWriteLock.withLock { field = value }
companion object {
private val looperMapReentrantReadWriteLock = ReentrantReadWriteLock()
private val looperMapReadLock = looperMapReentrantReadWriteLock.readLock()
private val looperMapWriteLock = looperMapReentrantReadWriteLock.writeLock()
private val looperMap: MutableMap<Thread, Looper> = mutableMapOf()
val looper: Looper?
get() = looperMapReadLock.withLock { looperMap[Thread.currentThread()] }
fun prepare(): Looper {
require(looper == null) { "Looper.prepare() must be called once in each thread." }
val thread = Thread.currentThread()
val threadLooper = Looper(thread)
looperMapWriteLock.withLock { looperMap[thread] = threadLooper }
return threadLooper
}
fun loop() {
requireNotNull(looper) { "Looper.prepare() must be called before Looper.loop()" }
looper?.quitFlag = false
while (looper?.quitFlag == false) {
val me = looper!!.mMessageQueue
val msg = me.next()
kotlin.runCatching {
println("[${Thread.currentThread().name}] Dispatching message is started.")
msg.target?.dispatchMessage(msg)
println("[${Thread.currentThread().name}] Dispatching message is stopped. ")
}.onFailure {
it.printStackTrace()
}
}
}
}
fun quit() {
quitFlag = true
looperMap.remove(Thread.currentThread())
}
}
Handler
package com.custom.handler
open class Handler {
private var interceptor: Interceptor? = null
private lateinit var mLooper: Looper
constructor() {
val looper = requireNotNull(Looper.looper) { "No looper found in current thread." }
Handler(looper)
}
constructor(looper: Looper) {
mLooper = looper
}
interface Interceptor {
fun handleMessage(message: Message?): Boolean
}
open fun dispatchMessage(message: Message?) {
println("[${Thread.currentThread().name}] onDispatchMessage: message=$message")
message ?: return
if (message.block == null) {
if (interceptor?.handleMessage(message) != true){
handleMessage(message)
}
return
}
message.block.invoke()
}
open fun handleMessage(message: Message?) {
println("[${Thread.currentThread().name}] onReceiveMessage: message=$message")
}
fun sendMessage(message: Message?) {
println("[${Thread.currentThread().name}] onSendMessage: message=$message")
message?.target = this
mLooper.mMessageQueue.enqueue(message ?: return)
}
fun quit(){
mLooper.quit()
}
}
[扩展] HandlerThread
import com.custom.handler.Looper
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class HandlerThread(name:String):Thread(name) {
private val lock = ReentrantLock()
private val condition = lock.newCondition()
private var mLooper: Looper? = null
val looper: Looper
get() {
if (mLooper == null) {
lock.withLock { condition.await() }
}
return requireNotNull(mLooper)
}
override fun run() {
super.run()
mLooper = Looper.prepare()
lock.withLock { condition.signalAll() }
Looper.loop()
}
}
[测试] Main
由于HandlerThread已经处理了子线程初始化Looper的操作,因此,使用该类直接测试即可。
import com.custom.handler.Handler
import com.custom.handler.Message
fun main(args: Array<String>) {
testHandler()
}
private fun testHandler(){
val handlerThread = HandlerThread("Thread-1").apply { start() }
val handler = object : Handler(handlerThread.looper) {
override fun handleMessage(message: Message?) {
super.handleMessage(message)
}
}
for (i in 0..3){
if (i == 3) {
handler.quit()
return
}
Thread.sleep(3000)
println("\n")
handler.sendMessage(Message(100, "message from Main-Thread"))
}
}
// console output
[Thread-1] Message queue is empty, then it is going to await...
[main] onSendMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Message queue wakes up.
[Thread-1] Dispatching message is started.
[Thread-1] onDispatchMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] onReceiveMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Dispatching message is stopped.
[Thread-1] Message queue is empty, then it is going to await...
[main] onSendMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Message queue wakes up.
[Thread-1] Dispatching message is started.
[Thread-1] onDispatchMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] onReceiveMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Dispatching message is stopped.
[Thread-1] Message queue is empty, then it is going to await...
[main] onSendMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Message queue wakes up.
[Thread-1] Dispatching message is started.
[Thread-1] onDispatchMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] onReceiveMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Dispatching message is stopped.
Process finished with exit code 0
发送消息时,调用
Handler#sendMessage(message),该方法运行在发生调用动作所在的线程(简称,调用线程);处理消息时,会先后调用Handler#dispatchMessage(messge)、Handler#handleMessage(message),此两方法运行在Looper.loop()的无限循环中,即Looper的实例对象所属的线程(简称,处理线程)。而发送消息的本质是:调用线程向处理线程的Looper内的消息队列MessageQueue中插入一个消息对象;处理消息的本质是:处理线程取出消息再调用消息实例对象持有的Handler实例的Handler#handleMessage(message),参数即为当前取出的消息实例对象。
android.os.MessageQueue#nativePollOnce(ptr, timeoutMillis)方法。若当前没有消息,则nativePollOnce(ptr, timeoutMillis)会从native层使用epoll的epoll_wait机制阻塞当前线程,至此MessageQueue.next()也就被阻塞。Message next () {
...
for (;;) {
...
android.os.MessageQueue#nativePollOnce
...
return nextMsg
}
}
MessageQueue#enqueueMessage(msg, when)会被调用,消息插入完成后,其内部android.os.MessageQueue#nativeWake(ptr)会被调用,此时原本被android.os.MessageQueue#nativePollOnce(ptr, timeoutMillis)阻塞的线程就会唤醒,并开始执行取消息和处理消息的流程。boolean enqueueMessage (message, when) {
...
//insert message
...
nativeWake(mPtr)
}
文中提到的ThreadLocal,简言之,就是JDK提供的一种存储“线程内部”变量的手段。Android的基于Handler的消息机制,也正是借助这一点来保存各个线程内部Looper的。当然,若在不考虑性能优化等各种因素条件下,不使用ThreadLocal也是可以实现类似功能的,如本文所实现的消息框架则是使用了Map的数据结构来实现的。 当然,本文的简单实现也旨在解释清楚Android消息机制的原理。
源码地址: https://github.com/heychinje/Simple-Handler
关于Android中的epoll:https://stackoverflow.com/questions/38818642/android-what-is-message-queue-native-poll-once-in-android
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的rubyyaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir