结合源码拆解Handler机制

作者:Pingred

前言

当初在讲App启动流程的时候,它的整个流程涉及到的类可以汇总成下面这张图:

那时着重讲了AMS、PMS、Binder这些知识点,有一个是没有对它进行详细讲解的,那就是常见的Handler,它不仅在这个流程里作用在ApplicationThread和ActivityThread进行通信,它在整个安卓体系中也扮演着重要的角色,所以也是面试过程中经常被问到的知识点,所以接下来将对它的源码进行分析。

Handler发送消息过程分析

先来从它的发送消息方法跟踪:

public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {Message msg = Message.obtain();msg.what = what;return sendMessageDelayed(msg, delayMillis);}

继续看sendMessageDelayed()方法:

public final boolean sendMessageDelayed(Message msg, long delayMillis){if (delayMillis < 0) {delayMillis = 0;}return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);}

这里可以看到一个sendMessageAtTime()方法,把消息传进去之外,还传了消息时间(系统启动时间+用户定义的时长),它其实就是核心方法,而同样handler的sendMessage()方法跟踪下去,它最后也是调用这个sendMessageAtTime()方法,所以来看该方法的详情:

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {MessageQueue queue = mQueue;if (queue == null) {RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");Log.w("Looper", e.getMessage(), e);return false;}return enqueueMessage(queue, msg, uptimeMillis);}

可以看到,在这里创建了MessageQueue对象,也就是我们常说的Handler机制里的四大元素之一队列,然后把发送的消息和队列以及时间参数传到了enqueueMessage()方法,也就是入队列操作:

 private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this;if (mAsynchronous) {msg.setAsynchronous(true);}return queue.enqueueMessage(msg, uptimeMillis);}

继续来看queue的enqueueMessage()方法:

boolean enqueueMessage(Message msg, long when) {...synchronized (this) {if (mQuitting) {IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");Log.w(TAG, e.getMessage(), e);msg.recycle();return false;}...

在分析这个方法前,先来想一想队列这个概念,可能有部分人对它有点陌生,这里解释一下,MessageQueue是一个队列,它并不是数组或者集合那样,是定义一个有容量的“容器”,而是通过单链表Message这个结构:

public final class MessageQueue {private static final String TAG = "MessageQueue";private static final boolean DEBUG = false;// True if the message queue can be quit.private final boolean mQuitAllowed;@SuppressWarnings("unused")private long mPtr; // used by native codeMessage mMessages;//单链表Message...

作为MessageQueue持有的变量,Message里有一个指向下一个Message对象的引用,因此Message消息其实是一个单链表形式串联在一起的。它不同于线性表那样,比如一个数组是一块连续的内存空间,里面每一个对象按照顺序排在一起,因此查询的时候可以通过基址加偏移量(也就是直接a[index]的方式)来访问每一个元素对象,但它也有它的缺点,容易造成内存碎片,进而内存溢出:

每次构造都需要一大片连续内存空间才能构造出来,所以即使中间有些不是连续空出来的空间也只能浪费掉。

而单链表如果要访问第三个对象(比如此时有三个对象),则必须先要知道第二个对象的地址,而第二个对象又必须要靠第一个对象来获取,因此这就需要从头开始遍历才能最终访问到第三个对象。但单链表的好处就是,如果要在某个位置上插入或者删除某个节点对象,直接操作就可以(把它的指向下一个对象的引用的指向进行更改就可以)。

可以看到,创建链表不需要一大块连续内存空间,通过next引用的指向就能达到一个连一个,像一条链一样。了解单链表之后,回到enqueueMessage()方法来继续分析:

boolean enqueueMessage(Message msg, long when) {...msg.markInUse();msg.when = when;Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;} else {...}
...

这段代码其实很好理解,第一条消息进来时,P对象为空,mMessages对象也是为空,然后此时p==null成立,因此走if里面的代码块,然后让当前这条要发送的消息的下一个消息对象next引用指向p(也就是null),然后让mMessages等于当前消息对象。

那么当第二条消息进来时,又重新new了一个p对象,让它等于mMessage(也就是第一条消息对象),这次进来就需要判断第二条消息的时间是不是小于第一条消息的时间,也就是when < p.when是否成立,如果是小于的情况,那么就让当前消息对象msg(也就是第二条消息)的下一个指向next引用指向p(也就是第一条消息对象),而此时mMessages对象等于第二条消息对象,那现在这种情况也就是说如果我发送的第二条消息对象的时间是比第一条消息对象的时间还要小的话,那么就会让第二条消息排在第一条消息对象的前面,也就是先让第二条消息先发送:

第三条消息的时间也是比第二条消息要小,所以依然第三条消息的next引用指向第二条消息对象,那么当第四条消息对象的时间是比第三条消息对象的时间要大的话,则就走else块的代码,来看看会发生什么:

boolean enqueueMessage(Message msg, long when) {...msg.markInUse();msg.when = when;Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;} else {// Inserted within the middle of the queue.  Usually we don't have to wake// up the event queue unless there is a barrier at the head of the queue// and the message is the earliest asynchronous message in the queue.needWake = mBlocked && p.target == null && msg.isAsynchronous();Message prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p; // invariant: p == prev.nextprev.next = msg;}
...

可以看到,此时就是先创建一个prev对象,用来记录当前消息的上一个消息对象的,让它等于p(也就是第三条消息对象),然后p等于p(第三条消息对象)的下一个消息对象(此时是为第二条消息),所以现在p就为第二条消息对象,之后就判断当前对象(第四条消息对象)的时间when是否小于第二条消息对象的when,如果成立,则不用再循环,走msg.next = p;和prev.next = msg这两行代码,这两行代码的意思就是:当前消息对象msg(第四个消息对象)的下一个指向对象为p(此时为第二个消息),然后prev(此时为第三个消息对象)的下一个指向为msg当前消息对象,所以此时单链表的顺序是第三条消息对象指向第四条消息对象,第四条消息对象则指向第二条消息对象,因为第四条消息对象的时间是小于第二条的时间,大于第三条的时间:

那如果刚刚在for循环那里,此时第四条消息它的时间比第二条消息还要大,那循环就继续,prev等于第二条消息对象,而p等于 第二条消息对象的下一个对象即第一条消息对象,然后此时判断条件第四条消息对象时间比第一条消息对象时间要小,成立,则跳出循环,执行msg.next = p;和prev.next = msg这两行代码,所以此时第四条消息对象的下一个对象则指向第一条消息对象,而第二条消息对象则执行第四条消息对象,所以此时链的顺序是:第三条消息对象指向第二条消息对象,第二条消息对象则指向第四条消息对象,而第四条消息对象则指向第一条消息对象:

所以,handler的消息排序机制也就是靠这个时间when来决定谁先谁后,小的那个排在大的后面,借由单链表的插入特性来进行插入。因此,handler发送的消息其实是发送到MessageQueue里的链表Message里存储。

Handler请求在哪里被处理

那就要在ActivityThread里找到main()方法,也就是当应用启动时的方法,在里面可以看到:

public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");// CloseGuard defaults to true and can be quite spammy.  We// disable it here, but selectively enable it later (via// StrictMode) on debug builds, but using DropBox, not logs.CloseGuard.setEnabled(false);...Looper.prepareMainLooper();...if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}// End of event ActivityThreadMain.Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}

Looper.prepareMainLooper()方法就是创建Looper对象,而Looper.loop()方法则开启轮询消息,不断去取消息,然后进行分发,所以看看它是怎么处理消息的:

public static void loop() {final Looper me = myLooper();...for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;}...try {msg.target.dispatchMessage(msg);dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} finally {if (traceTag != 0) {Trace.traceEnd(traceTag);}}...}
}

可以看到是遍历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 iterationint 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 flagsPoolSize--;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 {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare();Handler handler = new Handler(){@Overridepublic 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<String> threadLocal = new ThreadLocal<String>(){@Nullable@Overrideprotected String initialValue() {//默认返回null,现重写方法返回HelloWorldreturn "HelloWorld";}};//从ThreadLocalMap中获取值,key是主线程Log.v("main","主线程的threadLocal:" + threadLocal.get());Thread thread = new Thread(new Runnable() {@Overridepublic 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);elsecreateMap(t, value);}

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

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

@Overridepublic 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 {@Overrideprotected 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(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);handler.sendEmptyMessageDelayed(123, 1000);}};@Overrideprotected 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的匹配架构

WMS:
1.WMS的诞生
2.WMS的重要成员和Window的添加过程
3.Window的删除过程

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/106688.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k8s之工作负载、Deployment、DaemonSet、StatefulSet、Job、CronJob及GC

文章目录 1、工作负载1.1、定义1.2、分类 2、Deployment2.1、定义2.2、Deployment创建2.3、Deployment 更新机制2.3.1、比例缩放&#xff08;Proportional Scaling&#xff09;2.3.2、HPA&#xff08;动态扩缩容&#xff09;2.3.2.1、需要先安装metrics-server2.3.2.2、配置hpa…

STM32--SPI通信与W25Q64(1)

文章目录 前言SPI通信硬件电路移位过程 SPI时序起始与终止条件交换一个字节 W25Q64硬件电路框图 FLASH操作注意事项软件SPI读写W25Q64 前言 USART串口链接入口 I2C通信链接入口 SPI通信 SPI&#xff08;Serial Peripheral Interface&#xff09;是一种高速的、全双工、同步的串…

实战:大数据Spark简介与docker-compose搭建独立集群

文章目录 前言技术积累Spark简介Spark核心功能及优势Spark运行架构 Spark独立集群搭建安装docker和docker-composedocker-compose编排docker-compose编排并运行容器 Spark集群官方案例测试写在最后 前言 很多同学都使用过经典的大数据分布式计算框架hadoop&#xff0c;其分布式…

c++11 标准模板(STL)(std::basic_istringstream)(五)

定义于头文件 <sstream> template< class CharT, class Traits std::char_traits<CharT> > class basic_istringstream;(C11 前)template< class CharT, class Traits std::char_traits<CharT>, class Allocator std::allo…

小程序中的全局配置以及常用的配置项(window,tabBar)

全局配置文件和常用的配置项 app.json: pages:是一个数组&#xff0c;用于记录当前小程序所有页面的存放路径&#xff0c;可以通过它来创建页面 window:全局设置小程序窗口的外观(导航栏&#xff0c;背景&#xff0c;页面的主体) tabBar:设置小程序底部的 tabBar效果 style:是否…

C#-集合小例子

目录 背景&#xff1a; 过程: 1.添加1-100数: 2.求和: 3.平均值: 4.代码:​ 总结: 背景&#xff1a; 往集合里面添加100个数&#xff0c;首先得有ArrayList导入命名空间&#xff0c;这个例子分为3步&#xff0c;1.添加1-100个数2.进行1-100之间的总和3.求总和的平均值&…

数据结构(5)

堆 堆可以看作一颗完全二叉树的数组对象。 特性&#xff1a; 1.堆是完全二叉树&#xff0c;除了树最后一层不需要满&#xff0c;其余层次都需要满&#xff0c;如果最后一层不是满的&#xff0c;那么要求左满右不满 2.通常使用数组实现&#xff0c;将二叉树结点依次放入数组中…

Redis 重写 AOF 日志期间,主进程可以正常处理命令吗?

重写 AOF 日志的过程是怎样的&#xff1f; Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的&#xff0c;这么做有以下两个好处。 子进程进行 AOF 重写期间&#xff0c;主进程可以继续处理命令请求&#xff0c;从而避免阻塞主进程子进程带有主进程的数据副本。这里…

远程控制:用了向日葵控控A2后,我买了BliKVM v4

远程控制电脑的场景很多&#xff0c;比如把办公室电脑的文件发到家里电脑上&#xff0c;但是办公室电脑旁边没人。比如当生产力用的电脑一般都比较重&#xff0c;不可能随时带在身边&#xff0c;偶尔远程操作一下也是很有必要的。比如你的设备在工况恶劣的环境中&#xff0c;你…

基础论文学习(2)——DETR

目标检测 DETR&#xff1a;End-to-End Detection with Transformer detr是facebook提出的引入transformer到目标检测领域的算法&#xff0c;效果很好&#xff0c;做法也很简单&#xff0c;相较于RCNN和YOLO系列算法&#xff0c;避免了Proposal/AnchorNMS的复杂流程。 1. detr…

jvm开启远程调试功能;idea远程debug

概述 有时候一些问题本地调试无法复现&#xff0c;这个时候可以开启jvm的远程调试功能 jar包启动 jdk8 java -agentlib:jdwptransportdt_socket,address8787,servery,suspendn -jar xxx.jarjdk11/17 java -agentlib:jdwptransportdt_socket,address*:8787,servery,suspe…

STM32F103 4G Cat.1模块EC200S使用

一、简介 EC200S-CN 是移远通信最近推出的 LTE Cat 1 无线通信模块&#xff0c;支持最大下行速率 10Mbps 和最大上行速率 5Mbps&#xff0c;具有超高的性价比&#xff1b;同时在封装上兼容移远通信多网络制式 LTE Standard EC2x&#xff08;EC25、EC21、EC20 R2.0、EC20 R2.1&a…

Linux--进程地址空间

1.线程地址空间 所谓进程地址空间&#xff08;process address space&#xff09;&#xff0c;就是从进程的视角看到的地址空间&#xff0c;是进程运行时所用到的虚拟地址的集合。 简单地说&#xff0c;进程就是内核数据结构和代码和本身的代码和数据&#xff0c;进程本身不能…

代码随想录第29天|491.递增子序列,46.全排列,47.全排列II

491.递增子序列 491. 递增子序列 这道题的特点是有序的子序列(不能对原数组排序)&#xff0c;最终结果集res不能有重复子集。所以这道题又是子集又是去重 回溯三部曲 1.递归函数参数 本题求子序列&#xff0c;很明显一个元素不能重复使用&#xff0c;所以需要startIndex&a…

【C++练习】普通方法+利用this 设置一个矩形类(Rectangle), 包含私有成员长(length)、 宽(width), 定义一下成员函数

题目 设置一个矩形类(Rectangle), 包含私有成员长(length)、 宽(width), 定义成员函数: void set_ len(int l); //设置长度 设置宽度void set_ wid(int w); 获取长度: int get len(); 获取宽度: int get _wid); 显示周长和面积: v…

汽车电子笔记之:AUTOSAR方法论及基础概念

目录 1、AUTOSAR方法论 2、AUTOSAR的BSW 2.1、MCAL 2.2、ECU抽象层 2.3、服务层 2.4、复杂驱动 3、AUTOSAR的RTE 4、AUTOSAR的应用层 4.1、SWC 4.2、AUTOSAR的通信 4.3、AUTOSAR软件接口 1、AUTOSAR方法论 AUTOSAR为汽车电子软件系统开发过程定义了一套通用的技术方法…

分布式事务篇-2.4 Spring-Boot整合Seata

文章目录 前言一、pom jar导入:二、项目配置&#xff1a;2.1 配置 说明&#xff1a;2.1 .1 seata server 端:2.1 .2 seata client 端: 2.2 开启seata 对于数据源的代理:2.3 seata-client 的注册中心&#xff1a;2.4 seata-client 的配置中心&#xff1a;2.5 去掉手写的数据源代…

二叉树链式结构的实现

文章目录 1.前置说明 2.二叉树的遍历 文章内容 1.前置说明 学习二叉树的基本操作前&#xff0c;需先要创建一棵二叉树&#xff0c;然后才能学习其相关的基本操作。由于现在我们对于二叉树的了解还处于初级阶段&#xff0c;所以我们手动创建一棵简单的二叉树&#xff0c;以便…

javeee eclipse项目导入idea中

步骤一 复制项目到idea工作空间 步骤二 在idea中导入项目 步骤三 配置classes目录 步骤四 配置lib目录 步骤五 添加tomcat依赖 步骤六 添加artifacts 步骤七 部署到tomcat

电商项目part06 微服务网关整合OAuth2.0授权中心

微服务网关整合 OAuth2.0 思路分析 网关整合 OAuth2.0 有两种思路&#xff0c;一种是授权服务器生成令牌, 所有请求统一在网关层验证&#xff0c;判断权 限等操作&#xff1b;另一种是由各资源服务处理&#xff0c;网关只做请求转发。 比较常用的是第一种&#xff0c;把API网关…