HarmonyOS鸿蒙最全【Android Handler】消息机制核心代码_threadlocalworksource(1),HarmonyOS鸿蒙面试题2024中高级

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

        } else {  

needWake = mBlocked && p.target == null && msg.isAsynchronous();
//步骤3
Message prev;
for (;😉 {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}

if (needWake) {
nativeWake(mPtr);
}
}
//步骤4
return true;
}


首先说明MessageQueue使用一个单向链表维持着消息队列的,遵循先进先出的软解。 分析上面这端代码:


* 第一步:`mMessages`就是表头,首先取出链表头部。
* 第二步:一个判断语句,满足三种条件则直接将msg作为表头:


2. 若表头为空,说明队列内没有任何消息,msg直接作为链表头部;
3. ​`​when == 0​`​ 说明消息要立即执行(例如 ​`​sendMessageAtFrontOfQueue​`​方法,但一般的发送的消息除非特别指定都是发送时的时间加上延迟时间),msg插入作为链表头部;
4. ​`​when < p.when​`​,说明要插入的消息执行时间早于表头,msg插入作为链表头部。


* 第三步:通过循环不断的比对队列中消息的执行时间和插入消息的执行时间,遵循时间戳小的在前原则,将消息插入和合适的位置。
* 第四步:返回给调用者消息插入完成。


需要注意代码中的​`​needWake​`​和​`​nativeWake​`​,它们是用来唤醒当前线程的。因为在消息取出端,当前线程会根据消息队列的状态进入阻塞状态,在插入时也要根据情况判断是否需要唤醒。


接下来就是从消息队列中取出消息了


#### 从消息队列里取出消息


依旧是先看看准备准备工作


##### 准备工作


在非主线程中使用Handler,必须要做两件事


2. ​`​Looper.prepare()​`​:创建一个Loop
3. ​`​Looper.loop()​`​:开启循环


我们先不管它的创建,直接分段看啊循环开始的代码:首先是一些检查和判断工作,具体细节在代码中已注释



public static void loop() {
//获取loop对象
final Looper me = myLooper();
if (me == null) {
//若loop为空,则抛出异常终止操作
throw new RuntimeException(“No Looper; Looper.prepare() wasn’t called on this thread.”);
}
if (me.mInLoop) {
//loop循环重复开启
Slog.w(TAG, “Loop again would have the queued messages be executed”

  • " before this one completed.");
    }
    //标记当前loop已经开启
    me.mInLoop = true;
    //获取消息队列
    final MessageQueue queue = me.mQueue;
    //确保权限检查基于本地进程,
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    final int thresholdOverride =
    SystemProperties.getInt(“log.looper.”
  • Process.myUid() + “.”
  • Thread.currentThread().getName()
  • “.slow”, 0);
    boolean slowDeliveryDetected = false;
    //…省略下文代码
    }

##### loop中的操作


接下来就是循环的正式开启,精简关键代码:



public static void loop() {
//…省略上文代码
for (;😉 {
//步骤一
Message msg = queue.next();
if (msg == null) {
//步骤二
return;
}
//…省略非核心代码
try {
//步骤三
msg.target.dispatchMessage(msg);
//…
} catch (Exception exception) {
//…省略非核心代码
} finally {
//…省略非核心代码
}
//步骤四
msg.recycleUnchecked();
}
}


分步骤分析上述代码:


* 步骤一:从消息队列​`​MessageQueue​`​中取出消息(queue.next()可能会造成阻塞,下文会讲到)
* 步骤二:如果消息为null,则结束循环(消息队列中没有消息并不会返回null,而是在队列关闭才会返回null,下文会讲到)
* 步骤三:拿到消息后开始消息的分发
* 步骤四:回收已经分发了的消息,然后开始新一轮的循环取数据


###### MessageQueue的next方法


我们先只看第一步消息的取出,其他的在稍后小节再看,​`​queue.next()​`​代码较多,依旧分段来看



Message next() {
//步骤一
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
//步骤二
int pendingIdleHandlerCount = -1;
//步骤三
int nextPollTimeoutMillis = 0;
//…省略下文代码
}


* 第一步:如果消息循环已经退出并且已经disposed之后,直接返回null,对应上文中Loop通过​`​queue.next()​`​取消息拿到null后退出循环
* 第二部:初始化​`​IdleHandler​`​计数器
* 第三部:初始化native需要用的判断条件,初始值为0,大于0表示还有消息等待处理(延时消息未到执行时间),-1则表示没有消息了。


继续分析代码:



Message next() {
//…省略上文代码
for(;😉{
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
//…省略下文代码
}
}


这一段比较简单:


* 开启一个无限循环
* ​`​nextPollTimeoutMillis != 0​`​表示消息队列里没有消息或者所有消息都没到执行时间,调用native​`​Binder.flushPendingCommands()​`​方法,在进入阻塞之前跟内核线程发送消息,以便内核合理调度分配资源
* 再次调用native方法,根据​`​nextPollTimeoutMillis​`​判断,当为-1时,阻塞当前线程(在新消息入队时会重新进入可运行状态),当大于0时,说明有延时消息,​`​nextPollTimeoutMillis​`​会作为一个阻塞时间,也就是消息在多就后要执行。


继续看代码:



Message next() {
//…省略上文代码
for(;😉{
//…省略上文代码
//开启同步锁
synchronized (this) {
final long now = SystemClock.uptimeMillis();
//步骤一
Message prevMsg = null;
Message msg = mMessages;
//步骤二
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//步骤三
if (msg != null) {
//步骤四
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//步骤五
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
//步骤六
nextPollTimeoutMillis = -1;
}
}
//…省略下文IdleHandler相关代码
}
}


分析一下代码:


* 第一步:获取到队列头部
* 第二步:判断当前消息是否为同步消息(异步消息的target为null),开启循环直到发现同步消息为止
* 第三步:判断消息是否为null,不为空执行第四步,为空执行第六步;
* 第四步:判断消息执行的时间,如果大于当前时间,给前文提到的​`​nextPollTimeoutMillis​`​赋新值(当前时间和消息执行时间的时间差),在这一步基本完成了本次循环所有的取消息操作,如果当前消息没有到达执行时间,本次循环结束,新循环开始,就会使用上文中提到的​`​nativePollOnce(ptr, nextPollTimeoutMillis);​`​方法进入阻塞状态
* 第五步:从消息队列中取出需要立即执行的消息,结束整个循环并返回。
* 第六部:消息队列中没有消息,标记​`​nextPollTimeoutMillis​`​,以便下一循环进入阻塞状态


剩下的代码就基本上是IdleHandler的处理和执行了,在​`​IdleHandler​`​小节里进行讲解,这里就不展开说明了。


#### 消息的处理


还记得上文中​`​loop​`​方法中的​`​msg.target.dispatchMessage(msg);​`​吗? 消息就是通过​`​dispatchMessage​`​方法进行分发的。其中target是msg所持有的发送它的handler的引用,它在发送消息时被赋值。 ​`​dispatchMessage​`​的源码如下:



public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}


代码很简单,通过判断Message是否有​`​Runable​`​来决定是调用callback还是调用​`​handleMessage​`​方法,交给你定义的Handler去处理。需要注意的是,callback虽然是一个​`​Runable​`​,但是它并没有调用​`​run​`​方法,而是直接执行。这说明它并没有开启新的线程,就是作为一个方法使用(如果一开始Handler使用kotlin写的话,此处或许就是一个高阶函数了)。


#### 其他关键点


上面讲完了消息处理的主流程,接下来讲一下主流程之外的关键点源码


##### Loop的创建


还记得上文中的说到的**在非主线程中的要调用**​`​**Looper.prepare()**​`​**和** ​`**​Looper.loop()​**`​**方法**吗?这两个方法可以理解为初始化Loop和开启loop循环,而主线程中无需这么做是因为在app启动的main方法中,framework层已经帮我们做了。我们分别来看这两个方法:



static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException(“Only one Looper may be created per thread”);
}
sThreadLocal.set(new Looper(quitAllowed));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}


这里首先使用了一个静态的ThreadLocal确保Loop的唯一性,同时做到线程隔离,使得一个线程有且只有一个​`​Loop​`​实例。接着初始化​`​Loop​`​,同时创建​`​MessageQueue​`​ (quitAllowed设置是否允许退出)。在这一步实现了Loop和消息队列的关联。


需要注意的是,Loop的构造方式是私有的,我们只能通过​`​prepare​`​ 区创建,然后通过​`​myLooper​`​方法去获取。



public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}


ThreadLocal.get源码:



public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings(“unchecked”)
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}


可以看到,每个Thread都持有一个​`​ThreadLocalMap​`​ ,它和HashMap使用相同的数据结构,使用​`​ThreadLocal​`​作为key值,value就是Loop实例。不难发现:我们只能获取到当前线程的Loop实例。


Loop也提供了主线程中初始化的办法​`​prepareMainLooper​`​ ,但是这个方法明确说明不允许调用,只能由系统自己调用。 这基本上就是Loop创建的关键了,也是在这里完成了Loop和消息队列以及线成之间的关联。


##### Handler的创建


Handler的构造函数有以下几个:


2. public Handler()
3. public Handler(Callback callback)
4. public Handler(Looper looper)
5. public Handler(Looper looper, Callback callback)
6. public Handler(boolean async)
7. public Handler(Callback callback, boolean async)
8. public Handler(Looper looper, Callback callback, boolean async)


其中第一个和第二个已经被废弃了,实际上第1~5个构造方法都是通过调用​`​public Handler(Callback callback, boolean async)​`​或​`​public Handler(Looper looper, Callback callback, boolean async)​`​实现的,它们的源码如下:



public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}

mLooper = Looper.myLooper();
if (mLooper == null) {
//注意这个异常,loop不能为空的,首先要Looper.prepare();
throw new RuntimeException(
"Can’t create handler inside thread " + Thread.currentThread()

  • " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
    }

两个方法最大的区别就是一个使用传递过来的loop,一个直接使用当前线程的loop,然后就是相同的一些初始化操作了。这里就出现了一个关键点**handler处理消息所处的线程和创建它的线程无关,而是和创建它时loop的线程有关的**。


这也是Handler能实现线程切换的原因所在: ***handler的执行跟创建handler的线程无关,跟创建looper的线程相关,假如在子线程中创建一个Handler,但是Handler相关的Looper是主线程的,那么如果handler执行post一个runnable,或者sendMessage,最终的handle Message都是在主线程中执行的。***


##### Message的创建、回收和复用机制


我们可以直接使用new关键字去创建一个Message:


![](https://img-blog.csdnimg.cn/706700e714724b1085f8dc96796b8780.png#pic_center)


这些方法除了形参有些区别,用来给message不同的成员变量赋值之外,本质上都是通过 ​`​obtain()​`​来创建Message:



public static final Object sPoolSync = new Object();
Message next;
private static Message sPool;

public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize–;
return m;
}
}
return new Message();
}


这端代码很简单,​`​Message​`​内部维持了一个单线链表,使用sPool作为头部,用来存储​`​Message​`​实体。可以发现,每次调用者需要一个新的消息的时候,都会先从链表的头部去取,有消息就直接返回。没有消息才会创建一个新的消息。


那么这个链表是在何时插入消息的呢?接下来看​`​Message​`​的回收:



public static final Object sPoolSync = new Object();
private static final int MAX_POOL_SIZE = 50;

void recycleUnchecked() {
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;

synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}


该方法在每次消息从​`​MessageQueue​`​ 队列取出分发时都会被调用,就是在上文提到的​`​Loop.loop()​`​方法里。 代码也很简单,首先将​`​Message​`​的成员变量还原到初始状态,然后采用头插法将回收后的消息插入到链表之中(限制了最大容量为50)。而且插入和取出的操作,都是使用的同一把锁,保证了安全性。


注意插入和取出都是对链表的头部操作,这里和消息队列里就不太一样了。虽然都是使用单向链表,回收时使用头插和头取,先进后出,是个栈。而在​`​MessageQueue​`​里是个队列,遵循先进先出的原则,而且插入的时候是根据消息的状态确定位置,并没有固定的插入节点。


这是一个典型的享元模式,最大的特点就是复用对象,避免重复创建导致的内存浪费。这也是为什么android官方推荐使用这种方式创建消息的原因:就是为了提高效率减少性能开销。


##### IdleHandler


​`​IdleHandler​`​ 的定义很简单,就是一个定义在​`​MessageQueue​`​里的接口:



public static interface IdleHandler {
boolean queueIdle();
}


根据官方的解释,在 Looper循环的过程中,每当消息队列出现空闲:**没有消息或者没到任何消息的执行时间需要滞后执行**的时候,​`​queueIdle​`​ 方法就会被执行,而其返回的布尔值标识​`​IdleHandler​`​ 是永久的还是一次性的:


* ture:永久的,一旦空闲,就会执行
* false:一次性的,只有第一次空闲时会执行


它的使用方法如下:



Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
return true;
}
});


看一下​`​addIdleHandler​`​ 的实现





![img](https://img-blog.csdnimg.cn/img_convert/df05d28b3cd8fa958c676e5cd0fa839d.png)
![img](https://img-blog.csdnimg.cn/img_convert/4c2338e9769c788bb9c88bedca1ddc8b.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**





Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
return true;
}
});


看一下​`​addIdleHandler​`​ 的实现





[外链图片转存中...(img-JviVCzMv-1715823437835)]
[外链图片转存中...(img-UFkZJL9m-1715823437836)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值