HarmonyOS鸿蒙最全结合源码拆解Handler机制(4),2024年最新2024春招面试时间

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!


img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

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

}

}
}

可以看到是遍历MessageQueue队列,然后去取它里面的消息对象Message,调用queue的next()方法:

Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;😉 {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

取之前,有个nativePollOnce()方法,是用来判断是否唤醒请求方法,使用了epoll机制,防止loop()方法一直轮询会卡死程序。然后接下来取消息的代码块里又加了synchronized同步锁,也就意味着多个消息不会出现并发的问题(即使多个handler同时发送消息):


for (;😉 {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

取出消息对象后,再次回到loop()方法中,最后调用这个方法来处理消息:

public static void loop() {


try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}

}

调用了msg消息对象的target的dispatchMessage()方法去分发消息,这里的target其实就是handler对象:

public final class Message implements Parcelable {
public int what;

/package/ int flags;

/package/ long when;

/package/ Bundle data;

/package/ Handler target;

/package/ Runnable callback;

// sometimes we store linked lists of these things
/package/ Message next;


}

Handler、Looper、MessageQueue和Message四者关系

首先Handler可以在任意Activity里创建的,而Looper则是在ActivityThread的main()方法里创建的,接下里回到main()方法里往下看prepareMainLooper()方法:

public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException(“The main Looper has already been prepared.”);
}
sMainLooper = myLooper();
}
}

点进去看prepare()方法:

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));
}

可以看到这里new了Looper对象,然后把它传到sThreadLocal的set()方法里,那么先来看看looper的构造方法:

private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

好,那到现在为止,我们可以知道Looper是在ActivityThread的main()方法里调用Looper.prepareMainLooper()方法创建的,而Looper调用构造方法的同时,它也把MessageQueue给创建出来,创建的方法有直接new的方法,也有obtain()方法:

Message message = new Message();
Message message2 = Message.obtain();

官方建议使用obtain()方法来创建Message对象,因为obtain()方法采用了缓存池方式,定义了一个缓存池来构造了消息对象,然后要创建消息对象,就直接从池里取就行(没有缓存对象才去重新new一个新的消息对象),这样能避免了每次都要构造与销毁对象造成的性能消耗以及可能引发的内存抖动现象。

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();
}

给它上锁,防止并发问题,然后定义m对象,让它等于sPool,也就是此时最后那个消息对象,然后sPool等于最后一个消息对象的下一个消息对象,那此时假设它为第一个消息对象,接着继续,此时m的下一个对象要指向null了,因为要把它取出来,然后容量要自减1,这段逻辑就是一个链表的常规取元素的逻辑,很容易理解。可以看到该池默认的容量是50:

不过这里有一点要补充的,就是这里的容量是50可能会有些歧义,让人觉得MessageQueue就真的只能缓存50个消息对象,但上面已经分析过Message结构是链表,理论上来说数量是没有限制的,而这里规定了50是处于性能的考虑以及方便我们去管理MessageQueue,如果实在是要突破这个容量去构造Message对象也是可以的,因为源码里也没有超出50之后抛异常的逻辑,所以当超出50之后,就重新new消息对象,直到本身内存不够报错。

为什么要用缓存池的方式来处理消息对象,因为除了我们用户之外,安卓底层其实很多地方都使用到了handler来发送和处理消息(比如ActivityThread和ApplicationThread通信),因此消息对象会频繁地被使用到,为了避免刚才说的频繁构造与销毁对象造成的性能消耗以及可能引发的内存抖动现象,就定义池来管理消息对象,一旦你要构造消息对象,就使用池里事先缓存好的消息对象,直到缓存池里的消息对象用完了,这时才去new新的消息对象。

ThreadLocal跟Handler的关联

在主线程中可以直接new我们的Handler对象,而在子线程中则要创建looper对象和开启looper轮询消息方法:

public class PingredActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
handler.sendEmptyMessage(1);
Looper.loop();
}
}).start();
}

}

刚刚在看源码的时候可以看到主线程在activityThread中这两步已经帮我们写好了,因此直接创建Handler对象就可以。

还有一点要注意的是,每个线程的Looper都是不一样的,主线程中的Looper是属于主线程的,而子线程中Looper则属于子线程的,在创建Looper对象时,别忘了,它最后是作为参数传给了threadLocal对象里:

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));
}

而ThreadLocal是一个用来存储每个线程自己独有的一些副本数据的对象,用线程自己的id作为key值可以从ThreadLocal对象里取它对应的线程里存的数据,比如Looper对象,所以为什么说一个线程里可以有多个handler对象,而looper对象以及messageQueue只能是一个,就是因为它们都被set()方法存进ThreadLocal里。

或许用一个示例(伪)代码你就更能明白了:

public void doThreadLocal(){
final ThreadLocal threadLocal = new ThreadLocal(){
@Nullable
@Override
protected String initialValue() {
//默认返回null,现重写方法返回HelloWorld
return “HelloWorld”;
}
};
//从ThreadLocalMap中获取值,key是主线程
Log.v(“main”,“主线程的threadLocal:” + threadLocal.get());

Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//还没set值之前
String before_value = threadLocal.get();
Log.v(“child”,“thread1:” + before_value);
//set值之后
threadLocal.set(“pingred”);
String after_value = threadLocal.get();
Log.v(“child”,“after_thread1:” + after_value);

}
});
thread.start();
}

现在在主线程里定义了一个ThreadLocal对象,它有一个重写方法initialValu()方法,该方法返回了我们定义的值,该方法是当没有给ThreadLocal设置值的时候,就返回这个初始化的值HelloWorld。而无论是用哪个线程去获取ThreadLocal里的值时,如果没有线程在当初自己线程里往ThreadLocal存值时,那么无论是哪个线程去获取ThreadLocal里的值时,就会返回这个初始值HelloWorld。

而现在子线程里给initialValu()方法里调用set()方法设置了一个值,因此当在子线程里通过get()方法去取ThreadLocal里的值时,就会获取到“pingred”,也就是当前子线程当初存的值,而此时如果主线程去获取,则获取到的值则是“HelloWorld”,也就是主线程自己设置的那个值,这也印证了我们上面说过的,每个线程通过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();
}

可以看到,通过获取到当前的线程去构造了线程对象t,然后通过它去获取到一个ThreadLocalmap集合,这样一来其实就是表示每个线程都有它自己的独有的这个map集合,用来存储它们自己独有的数据,最后返回获取到数据。如果map里没有数据,则就返回初始方法里的初始值。比如现在是在主线程里调用get()方法,那它现在得到的map肯定就不是子线程的map,那它自然就不可能得到子线程里存储的数据,因此返回的是setInitiaValue()方法里的值。

而同样如果此时是子线程里调用get()方法,那它也只能是根据自己的map去得到自己存的值,而不会得到主线程存的值,除非子线程没有存值,因此就会走setInitiaValue()方法,返回的是该方法里设置的值。set()方法也是遵循这个原则:

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

使用当前线程去操作,不同的线程设置自己的数据。所以ThreadLocal也有防止多线程并发问题的作用,这样一来Looper对象以及消息队列及其里面的消息就不用被其他线程访问到。

说到这个Thread.currentThread()方法,就不得不提到一个常见的问题,就是为什么子线程里不能更新UI,我们来看看requestLayout()方法的源码:

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

它里面有这么一个方法checkThread():

void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
“Only the original thread that created a view hierarchy can touch its views.”);
}
}

可以看到,当View进行测量、布局和绘制之前会先调用checkThread()方法去当前线程是不是同一个线程,如果不是就会抛出异常。其实这样也是避免了多线程都可以去控制UI的更新进而引发并发问题,而且每一次UI更新都是一次性能的损耗,需要测量和绘制等,而多线程操纵UI会导致多次调用测量和绘制。

epoll机制

为什么Looper一直循环都不会导致应用卡死,因为使用了epoll机制,epoll机制又是什么,它是linux的通知机制,最早的时候,linux的(请求)通知机制是请求方不停地去轮询发送请求,也就是每隔几秒就会发送请求,这种方式太损耗发送方的性能和资源了。

于是就改为第二种方式,也就是当请求方发送请求之后,就会阻塞:一直等待接收方,直到接收方把相应数据消息返回回来,发送方才关闭请求。但这样的话,有不好的地方,如果接收方一直没回应,发送方就一直阻塞在这里,还有,假如有100个请求发送给接收方,只有50条请求是有数据返回过去,剩下50条不用返回数据,这时服务器就会不断循环这100条然后进行判断哪条需要发送数据,哪条不用发送数据,这样也很消耗服务器资源和性能,因为的app这么多人使用,同一个时间内能发送几百万条请求也不是不可能的。

因此基于上面种种,就有了epoll机制,监听每个请求,并且用表记录每个请求的发送情况,当有数据发过来,就记录该请求是有事件发生的,然后epoll_wait就加1,表示这条请求连接它是有事件发生的,是需要返回数据的,那这样服务器就之后就不用遍历每条请求连接,因为只需去看这个表的记录情况,当检查到某条连接记录epoll_wait是发生变化的,则就表示需要返回数据,而没变化,则不用返回数据,继续让该请求连接保持休眠状态,然后下次再监听到该请求的表中的epoll_wait有变化,则就唤醒该连接,然后把数据返回给发送方,这样请求方也不用一直阻塞了。

可以理解epoll_create表示创建一个epoll对象,epoll_ctl记录epoll对象中添加请求socket,epoll_wait记录发生事件的请求连接。

nativeWake()方法就是如果当前请求需要休眠,则调用linux的epoll机制的唤醒/休眠方法去处理该Handler请求,所以Looper一直轮训发送/接收请求都不会卡死应用。

最后,Handler使用不当容易引起内存泄露问题,因为Handler对于Activity来说它是内部类,持有Activity的引用,因此当Activity销毁后,Handler消息此时刚好传回来的时候,它由于持有Activity的引用,导致Activity还有GCroot,而不会被回收,从而导致内存泄漏,要解决这个问题,可以把内部类修饰为static静态,这样就等于断开Activity与Handler的联系,Handler不再持有Activity。又或者可以在Activity的onDestroy方法里调用removeCallbacksAndMessages方法:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler.sendEmptyMessageDelayed(123, 1000);
// Message message = new Message();
// Message message2 = Message.obtain();
}

private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
handler.sendEmptyMessageDelayed(123, 1000);
}
};

@Override
protected void onDestroy() {
handler.removeCallbacksAndMessages(null);
handler.removeMessages(123);
super.onDestroy();
}
}

如果你还没有掌握 Handler,现在想要在最短的时间里吃透它,可以参考一下《Android Framework核心知识点》,里面内容包含了:Init、Zygote、SystemServer、Binder、Handler、AMS、PMS、Launcher……等知识点记录。

《Framework 核心知识点汇总手册》:https://qr18.cn/AQpN4J

Handler 机制实现原理部分:
1.宏观理论分析与Message源码分析
2.MessageQueue的源码分析
3.Looper的源码分析
4.handler的源码分析
5.总结

Binder 原理:
1.学习Binder前必须要了解的知识点
2.ServiceManager中的Binder机制
3.系统服务的注册过程
4.ServiceManager的启动过程
5.系统服务的获取过程
6.Java Binder的初始化
7.Java Binder中系统服务的注册过程

Zygote :

  1. Android系统的启动过程及Zygote的启动过程
  2. 应用进程的启动过程

AMS源码分析 :

  1. Activity生命周期管理
  2. onActivityResult执行过程
  3. AMS中Activity栈管理详解

深入PMS源码:

1.PMS的启动过程和执行流程
2.APK的安装和卸载源码分析
3.PMS中intent-filter的匹配架构

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!


img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

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

7301567eac2.webp?x-oss-process=image/format,png)

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!


[外链图片转存中…(img-IdRXgTOJ-1715300445608)]
[外链图片转存中…(img-2eMuxkNL-1715300445608)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值