结合源码拆解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.hqwc.cn/news/84031.html

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

相关文章

前端高频面试题 js中堆和栈的区别和浏览器的垃圾回收机制

一、 栈(stack)和 堆(heap) 栈(stack)&#xff1a;是栈内存的简称&#xff0c;栈是自动分配相对固定大小的内存空间&#xff0c;并由系统自动释放&#xff0c;栈数据结构遵循FILO&#xff08;first in last out&#xff09;先进后出的原则&#xff0c;较为经典的就是乒乓球盒结…

TouchGFX之移植

在前面篇幅的基础上移植TouchGFX 1.配置FreeRTOS 2.使用touchgfx需要打开CRC 3.配置TouchGFX 4.生成代码&#xff0c;工程目录下多了TouchGFX文件夹 5.打开TouchGFX Designer工程 6.放置box控件&#xff0c;将颜色设置为红色 7.运行模拟器&#xff0c;生成代码 8.打开keil工程&…

框架分析(3)-Vue.js

框架分析&#xff08;3&#xff09;-Vue.js 专栏介绍Vue.js核心特点响应式数据绑定组件化开发虚拟DOM模板语法插件系统 对比总结优缺点优点简单易学响应式数据绑定组件化开发虚拟DOM生态系统和社区支持 缺点生态系统相对较小文档和教程相对较少适用于中小型项目 专栏介绍 link…

Wireshark数据抓包分析之互联网控制报文协议_ICMP

一、实验目的: 通过使用wireshark抓取的ICMP数据包对这个ICMP控制报文进行分析 二、预备知识&#xff1a; 1.ICMP协议概述&#xff1a;ICMP是Internet Control Message Protocol的缩写&#xff0c;即互联网控制报文协议。它是TCP/IP协议族的一个子协议&#xff0c;用于IP主机、…

postgresql基于postgis常用空间函数

1、ST_AsGeoJSON 图元转geojson格式 select ST_AsGeoJSON(l.geom) from g_zd l limit 10 2、 ST_Transform 坐标转换 select st_transform(l.shape, 3857) from sde_wf_cyyq l limit 10select st_astext(st_transform(l.shape, 3857)) from sde_wf_cyyq l limit 103、st_aste…

wxpython:wx.html2 是好用的 WebView 组件

wxpython : wx.html2 是好用的 WebView 组件。 pip install wxpython4.2 wxPython-4.2.0-cp37-cp37m-win_amd64.whl (18.0 MB) Successfully installed wxpython-4.2.0 cd \Python37\Scripts wxdemo.exe 取得 wxPython-demo-4.2.0.tar.gz wxdocs.exe 取得 wxPython-docs-4.…

机器学习---LDA代码

1. 获取投影坐标 import numpy as npdef GetProjectivePoint_2D(point, line):a point[0]b point[1]k line[0]t line[1]if k 0: return [a, t]elif k np.inf: return [0, b]x (ak*b-k*t) / (k*k1)y k*x treturn [x, y] 该函数用于获取一个点到一条直线的投影点…

芯科科技宣布推出下一代暨第三代无线开发平台,打造更智能、更高效的物联网

第三代平台中的人工智能/机器学习引擎可将性能提升100倍以上 Simplicity Studio 6软件开发工具包通过新的开发环境将开发人员带向第三代平台 中国&#xff0c;北京 - 2023年8月22日 – 致力于以安全、智能无线连接技术&#xff0c;建立更互联世界的全球领导厂商Silicon Labs&…

C#开发WinForm之DataGridView开发

前言 DataGridView是开发Winform的一个列表展示&#xff0c;类似于表格。学会下面的基本特征用法&#xff0c;再辅以经验&#xff0c;基本功能开发没问题。 1.设置 DataGridView表格行首为序号索引, //设置 DataGridView表格行首为序号索引private void dataGridView1_RowPost…

AI教学赋能计划(大模型特辑)2023秋季学期启动申请!

AI教学赋能计划是由百度飞桨发起&#xff0c;面向高校提供产教融合人才培养方案&#xff0c;一站式助力高校复合型人才培养&#xff0c;截至目前已经培养5200名AI专业师资&#xff0c;助力484所高校开设人工智能与AIX特色课程。 2023年随着文心一言等预训练大语言模型发展&…

【Java 高阶】一文精通 Spring MVC - 转发重定向(四)

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

基于OpenCV实战(基础知识二)

目录 简介 1.ROI区域 2.边界填充 3.数值计算 4.图像融合 简介 OpenCV是一个流行的开源计算机视觉库&#xff0c;由英特尔公司发起发展。它提供了超过2500个优化算法和许多工具包&#xff0c;可用于灰度、彩色、深度、基于特征和运动跟踪等的图像处理和计算机视觉应用。Ope…