深入理解 Handler(java 层 + native 层)

news2024/11/26 6:12:36

文章目录

    • 回顾
    • 线程消息队列时怎样实现的
    • 消息是怎么传递的?
    • Handle 的延迟消息是怎么处理的?
    • IdleHandler 的原理
    • 主线程进入了 Looper 循环为什么没有 ANR?
    • 消息屏障是什么?

回顾

之前学习过Handler相关的基础知识,今天再学习一下 Handler 更深层次的知识。

线程消息队列时怎样实现的

	- 可以在子线程创建 Handler 么?
	- 主线程的 Looper 和 子线程的 Looper 有什么区别?
	- Handler Looper 和 MessageQueue 有什么关系?
	- MessageQueue 是怎么创建的?
  • 可以在子线程创建 Handler 么?
    在子线程创建 Handler 时会报一个 RuntimeException 让你调用 Looper.prepare()
   public Handler(Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

Looper.myLooper() 是从 ThreadLocal 中读取 Looper

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
  • Looper.prepare()
    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));
    }

Looper.prepare() 呢,先从 sThreadLocal.get() 如果已经存在则抛出异常。如果没有 Looper 则创建一个 Looper 传入了 quitAllowed 参数,默认是 true ,quitAllowed 是什么意思的?quitAllowed 代表 Looper 是否可以退出,工作完成以后可以调用 Looper.quit() 退出,主线程创建的 Looper 可以看一下,创建的时候传入的是 false ,是不可以退出的,并且创建完会将 Looper 保存到一个静态变量里面,就可以随时获得主线程的 Looper。

  • 可以在子线程创建 Handler 么?
    • 可以,需要先创建 Looper
  • 主线程的 Looper 和 子线程的 Looper 有什么区别?
    • 上面讲的子线程创建 Looper quitAllowed 是true 主线程是 false;就是子线程的 Looper 可以退出,主线程不可以。
  • 那么 Looper 创建的时候做了哪些事
	// Looper 的构造函数
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

Looper 在创建的时候 new MessageQueue(quitAllowed); 创建了消息队列

  • new MessageQueue(quitAllowed);
    MessageQueue(boolean quitAllowed) {
    	// 记录了是否可以退出
        mQuitAllowed = quitAllowed;
        // 调用了  nativeInit() 在 native 层去初始化
        mPtr = nativeInit();
    }
  • Handler Looper 和 MessageQueue 有什么关系?
    • 线程在创建 Handler 时需要创建 Looper ,创建 Looper 时创建了 MessageQueue (Handler 创建可以传入 Looper)多个 Handler 可以往同一个 Looper 发送 msg ,MessageQueue 发送消息时会根据 target 往对应的 Handler 中回调数据。
  • 接下来看看 MessageQueue() 在 native 层做了哪些处理
    frameworks/base/core/jni/android_os_MessageQueue.cpp -> android_os_MessageQueue_nativeInit
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }
    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

其主要创建了 NativeMessageQueue() 对象

  • NativeMessageQueue
NativeMessageQueue::NativeMessageQueue() :
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    // 先从当前线程的缓存中获取 Looper 
    mLooper = Looper::getForThread();
    // 如果获取不到 则 new 一个 Looper
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        // 然后设置到局部缓存中
        Looper::setForThread(mLooper);
    }
}
  • Looper::getForThread(); 线程的局部缓存是什么呢?
sp<Looper> Looper::getForThread() {
    int result = pthread_once(& gTLSOnce, initTLSKey);
    LOG_ALWAYS_FATAL_IF(result != 0, "pthread_once failed");

    Looper* looper = (Looper*)pthread_getspecific(gTLSKey);
    return sp<Looper>::fromExisting(looper);
}

其实就是获取线程的 TLS ,叫 Thread Local Storage ,就是对线程内全部开放,其他线程无法访问。我记得之前好像学习 JVM 的内存分配的时候有涉及到。

  • native 的 Looper 的创建
Looper::Looper(bool allowNonCallbacks) :... {
    mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

    AutoMutex _l(mLock);
    rebuildEpollLocked();
}

首先根据 eventfd 创建了 mWakeEventFd ,这里是Android9 早起版本不是 mWakeEventFd 是管道,因为使用 mWakeEventFd 计数器比用管道的性能要好。管道需要写再读需要拷贝数据。(在android后期又对mWakeEventFd 的使用做了优化。)然后调用了 rebuildEpollLocked();

  • rebuildEpollLocked();
void Looper::rebuildEpollLocked() {
    // Allocate the new epoll instance and register the WakeEventFd.
    // epoll_create1 创建了 epoll
    mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
    // 创建了 createEpollEvent 事件设置了 mWakeEventFd  ,监听可读事件
    epoll_event wakeEvent = createEpollEvent(EPOLLIN, WAKE_EVENT_FD_SEQ);
    int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &wakeEvent);
    	//	...	
}

Looper 创建的时候会创建 mWakeEventFd ,并且去监听它的事件,那么数据是什么写到里面的呢?另外一个线程往当前线程消息队列中插入一条消息,然后会调用 wake() 函数 :调用完了 wake() 函数就会往 mWakeEventFd 里面写东西。

wake() 函数调用:可以看看java 层调用 Handler sendMessage 时,加入到 MessageQueue 后,会调用到下面代码,mPtr 是上面创建 MessageQueue时调用的 mPtr = nativeInit(); 返回的。

            if (needWake) {
                nativeWake(mPtr);
            }
private native static void nativeWake(long ptr);
  • 那么什么时候去读数据的呢?
    在 Looper.loop() 里会从 Message 中获取 next() 的 message ,在调用 next() 函数时调用了 nativePollOnce(ptr, nextPollTimeoutMillis); 函数。调用到了 native 层 Looper::pollOnce
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        //....
        result = pollInner(timeoutMillis);
    }
}
  • 调用了 pollInner() 函数
int Looper::pollInner(int timeoutMillis) {
	struct epoll_event eventItems[EPOLL_MAX_EVENTS];
	// epoll_wait 等待有没有事件触发
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
	
	//...
	// 如果有事件了则在 for 循环中处理事件
    for (int i = 0; i < eventCount; i++) {
        const SequenceNumber seq = eventItems[i].data.u64;
        uint32_t epollEvents = eventItems[i].events;
        if (seq == WAKE_EVENT_FD_SEQ) {
            if (epollEvents & EPOLLIN) {
            // 调用了 awoken(); 函数 awoken 就是把事件读出来 消化掉
                awoken();
            } 
        } else {
            //...
        }
    }
}
  • 架构图
    在这里插入图片描述
    Handler 架构分为 java 层和 native 层,java 层开始,一个线程创建了 Looper 对应的创建了一个 MessageQueue , java 层的 MessageQueue 创建时对应创建了一个 native 层的 NativeMessageQueue 并且创建了一个 Looper ,也就是说 java 层的 Looper 和 MessageQueue 在 native 层也对应的有 Looper 和 MessageQueue。
  • Handler Looper 和 MessageQueue 有什么关系?
    • 一一对应的关系
  • MessageQueue 是怎么创建的?
    • java 层 Looper 创建的时候创建的 MessageQueue ,java 层MessageQueue 创建的时候会创建一个 native 层的 NativeMessageQueue,NativeMessageQueue 创建的时候会创建 Naive 层的 Looper ,Native 层的 Looper 创建的时候会创建一个可读的 epoll 。

消息是怎么传递的?

上面一段讲了 Handler 在 java 层和 native 层的架构,这回梳理一下消息是怎么传递呢。

	- 消息循环过程是怎么样的?
	- 消息是怎么发送的?
	- 消息是怎么处理的?
  • 从 java 层的 Looper.loop() 循环开始
	public static void loop() {
		// 拿到 looper 
		final Looper me = myLooper();
		// 拿到 MessageQueue
		final MessageQueue queue = me.mQueue;
        for (;;) {
        	// 取下一条消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // 没有消息直接返回
                return;
            }
        // 调用消息的 target.dispatchMessage(msg);
		// target 就是对应的 Handler
		msg.target.dispatchMessage(msg);
		// 回收消息
		msg.recycleUnchecked();
	}

loop() 中重点是 Message msg = queue.next(); 如何获取下一个消息和 msg.target.dispatchMessage(msg); 如何分发消息。
在这里插入图片描述

  • msg.target.dispatchMessage(msg); 分发 msg 比较简单
    public void dispatchMessage(Message msg) {
    	// 先以 msg 中的 callback 优先回调回去
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
        	// 然后再检查全局 mCallback 
            if (mCallback != null) {
            // mCallback.handleMessage(msg) 返回 true 则不往下分发了。
            // 一些 hook 点就是通过反射 设置 mCallback 偷偷的更换 msg 然后返回 false 
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            // 最后才调用 handleMessage
            handleMessage(msg);
        }
    }
  • Message msg = queue.next(); 怎么取消息
Message next() {
	int nextPollTimeoutMillis = 0;	
    for (;;) {
    	// 目的是阻塞线程,当其他线程发送一些特殊消息的时候会唤起阻塞
    	// 第一次 nextPollTimeoutMillis = 0 所以第一次一定不会阻塞
    	// 如果第一次下去之后没有消息了 nextPollTimeoutMillis = -1 了就需要一直等待了
		nativePollOnce(ptr, nextPollTimeoutMillis);
		synchronized (this) {
			Message prevMsg = null;
			// 取一条消息
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                msg.next = null;
                // 标记成使用中
                msg.markInUse();
                // 然后返回消息
                return msg;
				// ... 

			// No more messages.
            nextPollTimeoutMillis = -1;
		}
	}
}

next() 方法这里主要看 nativePollOnce(ptr, nextPollTimeoutMillis); 方法,首次超时时间 nextPollTimeoutMillis = 0,所以一定不会阻塞,会去从队列中取消息,如果没有消息则把 nextPollTimeoutMillis 设置成 -1 ,下次 for() 循环会一直阻塞住。接下来看一下 nativePollOnce() 函数。

  • nativePollOnce()
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

android_os_MessageQueue_nativePollOnce() 调用了 NativeMessageQueue 的 pollOnce(env, obj, timeoutMillis);
在这里插入图片描述

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    mPollEnv = env;
    mPollObj = pollObj;
    mLooper->pollOnce(timeoutMillis);
    mPollObj = NULL;
    mPollEnv = NULL;
	// ...
}

NativeMessageQueue::pollOnce() 函数调用了 Looper 的 pollOnce() 函数,并且带了一个超时时间。

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
    //...
        if (result != 0) {    
            if (outFd != nullptr) *outFd = 0;
            if (outEvents != nullptr) *outEvents = 0;
            if (outData != nullptr) *outData = nullptr;
            return result;
        }
        result = pollInner(timeoutMillis);
    }
}

Looper::pollOnce() 首次的时候 result = 0 ,所以会调用 pollInner(timeoutMillis); 函数。

int Looper::pollInner(int timeoutMillis) {
	// ...
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    // 调用了 epoll_wait() 函数,这个函数是用来阻塞的,它返回只有几种情况
    // 第一种出错了,eventCount<0,第二种超时了 eventCount=0,第三种有事件传递进来 eventCount 就是事件个数
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
	// ...
	// 有事件返回以后则通过 for 循环处理事件
    for (int i = 0; i < eventCount; i++) {
        const SequenceNumber seq = eventItems[i].data.u64;
        uint32_t epollEvents = eventItems[i].events;
        if (seq == WAKE_EVENT_FD_SEQ) {
            if (epollEvents & EPOLLIN) {
            // 如果事件满足条件,则调用 awoken() 来消费事件
                awoken();
            }
        } else {
            // ...
        }
    }
    //...
    return result;
}

当 Looper::pollInner() 返回了,就可以继续执行最上面的 next() 函数了,一直循环拿到下一个msg,就是不停的调用 nativePollOnce() 一直监听其他线程是否有发送事件进来,如果有事件,nativePollOnce() 就可以顺利执行下去,就可以拿下一个信息了。

在这里插入图片描述

  • 那么怎么往消息队列里面发送消息呢?

一般使用的时候都是调用 Handler 的 sendMessage()

    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

最后都会走到下面这个方法

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    // Handler 设置给 target
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        // 调用 MessageQueue 的 enqueueMessage 并传入 uptimeMillis
        return queue.enqueueMessage(msg, uptimeMillis);
    }
  • queue.enqueueMessage(msg, uptimeMillis); 重点代码如下
    boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            // 先将消息插入到消息队列中
            //...
            if (needWake) {
            	// 然后调用 nativeWake(mPtr); 去将唤醒消息队列所在的线程
                nativeWake(mPtr);
            }
        }
        return true;
    }

在这里插入图片描述

enqueueMessage() 首先将消息插入到消息队列,然后调用 nativeWake(mPtr); 唤醒消息队列所在的线程,这里重点看是如何唤醒的。

  • nativeWake(mPtr);
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->wake();
}

android_os_MessageQueue_nativeWake() 函数调用到了 NativeMessageQueue 的 wake() 函数。

void NativeMessageQueue::wake() {
    mLooper->wake();
}

最后又调用到了 Looper 的 mLooper->wake();

void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
    ALOGD("%p ~ wake", this);
#endif
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
}

Looper::wake() 往 mWakeEventFd 里面的计数器写数,这样 epllo_wait() 的循环就可以收到可读事件了。

在这里插入图片描述

Handle 的延迟消息是怎么处理的?

  • 发送延时消息一般从 Handler 发送消息开始,传入延迟的毫秒数。
    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        // 调用 sendMessageAtTime() 用当前时间 + 延迟时间 就是发送的时间
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
  • 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);
    }

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

调用了 MessageQueue 的 enqueueMessage() 传入了 msg 和 时间

    boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            //...
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // 消息队列是空 或者 when = 0 (调用 sendMessageAtFrontOfQueue 的时候 when = 0)
                // 或者比第一个时间还早 
                // 满足上面几个条件之一则插入到第一个节点
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                // 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.next
                prev.next = msg;
            }
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

enqueueMessage() 就是按照时间为 Message 单链表做了个排序,所以延迟的意思就是先加入队列,到时间再处理消息。然后还是调用了 nativeWake(mPtr); 函数,上面代码将到了 调用完了 nativeWake(mPtr); 会写入事件,唤醒 native 层的 Looper 循环返回数据。先看一下 java 层的 loop

    public static void loop() {
        final Looper me = myLooper();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
       }

需要注意的是无消息或者消息未到时间的阻塞是在 queue.next(); 函数中 那么如果 msg == null 返回的 return 是出现异常了 loop() 停止了, 这是两个概念。 接下来看一下 queue.next();

Message next() {
	int nextPollTimeoutMillis = 0;	
    for (;;) {
    	// 目的是阻塞线程,当其他线程发送一些特殊消息的时候会唤起阻塞
    	// 第一次 nextPollTimeoutMillis = 0 所以第一次一定不会阻塞
    	// 如果第一次下去之后没有消息了 nextPollTimeoutMillis = -1 了就需要一直等待了
		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());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // 如果消息还没到时间,则 nextPollTimeoutMillis 等待时间设置成还差多少时间
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 如果时间到了则返回 msg
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                      
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
	}
}

上面讲的时候说过,没有消息时 nativePollOnce() 会阻塞住,当 nativeWake() 发送后,会使得 nativePollOnce() 通过,会走下面的代码。首先 nativePollOnce(ptr, nextPollTimeoutMillis); 的 nextPollTimeoutMillis 参数是指睡眠多长时间,如果是 -1 则一直睡眠等待 wake() ,上面拿到 message 后如果到了时间则直接返回 msg ,如果还未到则计算一下还剩下多少时间赋值给 nextPollTimeoutMillis ,然后调用 nativePollOnce(ptr, nextPollTimeoutMillis); 睡眠等待时间到达。 下一次唤醒会把 msg 返回回去。

  • 总结
    延迟操作就是首先按照时间顺序插入消息队列中,然后通过 epoll_wait() 进行延迟阻塞,到时间了再返回消息。只不过延迟精度不一定很精确。而且如果处理消息太耗时,可能会让下一个消息延迟了。

IdleHandler 的原理

	了解 IdleHandler 的作用以及调用方式
	了解 IdleHandler 有哪些使用场景
	熟悉 IdleHandler 的实现原理
    /**
     * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     */
    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }

从上面注释来看,boolean queueIdle(); 回调的时机第一种是消息队列中没有了消息。第二种可能是消息队列中有消息,但是时间还未到执行它的时候。

  • IdleHanlder 的用法
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
            // 如果 return true; 就可以一直收到回调,如果 return false;就只能收到一次回调
                return true;
            }
        });
  • MessageQueue 中 addIdleHandler() 函数
    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();

    public void addIdleHandler(@NonNull IdleHandler handler) {
        if (handler == null) {
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
            mIdleHandlers.add(handler);
        }
    }

addIdleHandler() 函数就是往 mIdleHandlers 数组中添加一个 handler ,那么 mIdleHandlers 的列表是什么时候调用的?

  • mIdleHandlers 的列表是什么时候调用的?

在 Looper 的 loop() 函数中,上面讲过 loop() 会从 MessageQueue 中获取Message,然后去执行分发,然乎回收消息。那么MessageQueue是如何返回消息的?

  • MessageQueue: next() 函数
    Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
			// 阻塞用 有消息 或者超时 或者异常了 会往下走
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // ... 获取 Message 逻辑省略
				// 如果没有获取到普通Message消息会往下获取 mIdleHandlers 中的数据
				
                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                // 如果没有 IdleHandler 则直接跳过此次循环
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                // 将 mIdleHandlers 转换为数组
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            // 从 数组中获取 IdleHandler 数据
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
				// 如果 queueIdle() 返回的 false 则执行完了从列表中删除,也就是只执行一次
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }

所以 nativePollOnce() 返回之后没有消息需要分发了,就开始处理 IdleHandler 中的数据了。

  • framework 中用到了 IdleHandler 的地方
    void scheduleGcIdler() {
        if (!mGcIdlerScheduled) {
            mGcIdlerScheduled = true;
            Looper.myQueue().addIdleHandler(mGcIdler);
        }
        mH.removeMessages(H.GC_WHEN_IDLE);
    }

    final class GcIdler implements MessageQueue.IdleHandler {
        @Override
        public final boolean queueIdle() {
            doGcIfNeeded();
            // 返回。false 只执行一次
            return false;
        }
    }

在 ActivityThread 中添加进去了一个 mGcIdler 执行的时候会调用GC操作。

需要注意的是,如果 MessageQueue 中没有消息了,addIdleHandler 之后并不会触发 Idle 事件的执行,有时候需要往 MessageQueue 中 send 一条普通消息才可以。下面那条例子也是其中之一

  • 之前的 Idle 都是异步的,下面这种情况是处理同步 Idle 的情况。

frameworks/base/core/java/android/app/Instrumentation.java

    public void waitForIdleSync() {
        validateNotAppThread();
        Idler idler = new Idler(null);
        mMessageQueue.addIdleHandler(idler);
        mThread.getHandler().post(new EmptyRunnable());
        idler.waitForIdle();
    }

waitForIdleSync() 等待 Idle 执行返回,最后调用了idler.waitForIdle(); 等待

        public void waitForIdle() {
            synchronized (this) {
                while (!mIdle) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
        }

调用了 wait(); 同步等待线程 mIdle 完成。直到 IdleHandler 的 queueIdle() 方法执行

        public final boolean queueIdle() {
            if (mCallback != null) {
                mCallback.run();
            }
            synchronized (this) {
                mIdle = true;
                notifyAll();
            }
            return false;
        }

将mIdle = true; 再调用 notifyAll(); ,这样上面 wait() 的代码就可以执行下去了。我们自己开发的时候也可以使用这种方式。

  • IdleHandler 适用场景
  • 之前研究性能优化中的启动优化时,一些不必要立刻启动的项目可以放到 IdleHandler 中执行,或者 Activity onCreate() 以后一些可以在 UI 绘制等以后执行的,可以放在 IdleHandler 执行。
  • 批量任务:任务密集,只关注最终结果(比如打开 App 收到一堆通知要刷新UI ,可以先汇总,等待UI绘制结束再统一刷新一次页面。)

主线程进入了 Looper 循环为什么没有 ANR?

	了解 ANR 触发原理
	了解应用大致启动流程
	了解消息循环机制
	了解系统和应用通信流程
  • ANR 是什么?

ANR 实际上是 AMS 在系统进程弹出来的一个 dialog

AMS 在发生 ANR 时会调用

            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mAppErrors.appNotResponding(proc, activity, parent, aboveSystem, annotation);
                }
            });
  • mAppErrors.appNotResponding(proc, activity, parent, aboveSystem, annotation);
            // Bring up the infamous App Not Responding dialog
            Message msg = Message.obtain();
            msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
            msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);

            mService.mUiHandler.sendMessage(msg);

上面发送 mUiHandler 不是在 SystemServer 的主线程,其实是在子线程。(所以 UI 不一定是在主线程刷新,之前讲 UI 线程的时候提到过)

在 frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java 中会接收到 handleMessage 消息

final class UiHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHOW_NOT_RESPONDING_UI_MSG: {
                mAppErrors.handleShowAnrUi(msg);
                ensureBootCompleted();
            } break;

在 mAppErrors.handleShowAnrUi(msg); 中会创建 diaog

                dialogToShow = new AppNotRespondingDialog(mService, mContext, data);
                proc.anrDialog = dialogToShow;
                 dialogToShow.show();
  • 发生 ANR 的场景有哪些

      Service Timeout
      BroadcastQueue Timeout
      ContentProvider Timeout
      InputDispatching Timeout (包括 Activity 输入等处理超时)
    
  • 那么 ANR 是怎么触发的呢?系统如何知道 ANR 了。
    下面以 Service 为例

之前的文章 Android 深入理解 Service 的启动和绑定 有讲到过启动 service 的过程要经过下面的方法。

  • ActiveService : realStartServiceLocked()
private final void realStartServiceLocked(ServiceRecord r,
            ProcessRecord app, boolean execInFg) throws RemoteException {

        bumpServiceExecutingLocked(r, execInFg, "create");
	
app.thread.scheduleCreateService(r, r.serviceInfo,
                    mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
                    app.repProcState);
}

在调用 app.thread.scheduleCreateService() 之前,先调用了 bumpServiceExecutingLocked()

    private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
        boolean timeoutNeeded = true;
        long now = SystemClock.uptimeMillis();
        if (r.executeNesting == 0) {
            r.executeFg = fg;
            ServiceState stracker = r.getTracker();
            if (stracker != null) {
                stracker.setExecuting(true, mAm.mProcessStats.getMemFactorLocked(), now);
            }
            if (r.app != null) {
                r.app.executingServices.add(r);
                r.app.execServicesFg |= fg;
                if (timeoutNeeded && r.app.executingServices.size() == 1) {
                    scheduleServiceTimeoutLocked(r.app);
                }
            }
        } else if (r.app != null && fg && !r.app.execServicesFg) {
            r.app.execServicesFg = true;
            if (timeoutNeeded) {
                scheduleServiceTimeoutLocked(r.app);
            }
        }
        //...
    }

其内部调用了 scheduleServiceTimeoutLocked(r.app);

	// static final int SERVICE_TIMEOUT_MSG = 12;
    // How long we wait for a service to finish executing.
    static final int SERVICE_TIMEOUT = 20*1000;

    // How long we wait for a service to finish executing.
    static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;

    void scheduleServiceTimeoutLocked(ProcessRecord proc) {
        if (proc.executingServices.size() == 0 || proc.thread == null) {
            return;
        }
        Message msg = mAm.mHandler.obtainMessage(
                ActivityManagerService.SERVICE_TIMEOUT_MSG);
        msg.obj = proc;
        mAm.mHandler.sendMessageDelayed(msg,
                proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
    }

其实内部就是为 ActivityManagerService 的 handler 发送了一个延迟消息延迟时间就是 service 超时时间。发送的what = static final int SERVICE_TIMEOUT_MSG = 12; 超时以后 AMS 接收到消息就会调用到 frameworks/base/services/core/java/com/android/server/am/ActiveServices.java ,再调用到了 mAm.mAnrHelper.appNotResponding(proc, anrMessage); 然后就弹出弹窗了。

  • 那么如果 Service 正常启动了以后为什么没有弹窗呢?
    之前文章讲过,Service 启动会回调到 ActivityThread 的 handleCreateService()

  • handleCreateService()

   private void handleCreateService(CreateServiceData data) {
        // 这里面就是说的 IdleHanlder 的用处之一
        unscheduleGcIdler();
        LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
        Service service = null;
        try {
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = packageInfo.getAppFactory()
                    .instantiateService(cl, data.info.name, data.intent);
        } 

        try {
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            context.setOuterContext(service);

            Application app = packageInfo.makeApplication(false, mInstrumentation);
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManager.getService());
            service.onCreate();
            mServices.put(data.token, service);
            try {
            	// 调用完了 service.onCreate(); 之后调用到了 AMS 的serviceDoneExecuting() 
                ActivityManager.getService().serviceDoneExecuting(
                        data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        } 
    }
  • ActivityManager.getService().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
    然后调用到了 serviceDoneExecutingLocked()

  • 重点:serviceDoneExecutingLocked 内部调用了下面方法

   static final int SERVICE_TIMEOUT_MSG = 12;
 mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);

将同一个 what 属性的 ActivityManagerService.SERVICE_TIMEOUT_MSG 从 Handler 移除掉,这样 ANR 的弹窗就不会弹出来了。

  • 主线程的消息循环
    public static void main(String[] args) {
        Looper.prepareMainLooper();
		// ... 
        Looper.loop();
    }

原理上面已经讲过,loop() 循环 从 MeesageQueue 中读取数据,等等…,那么有几种情况会发送消息到主线程的 Hanlder 呢

	1. 应用主线程发送消息
	2. 应用子线程发送消息
	3. binder 线程往主线程发送消息
		- 	比如启动 AMS Service 都是通过 binder 线程发送到主线程去处理的
  • 总结

      为什么没有ANR:ANR是没有在规定时间内没有完成AMS的任务,和 loop() 循环没有啥必然联系
      AMS 的请求都是丢到应用端的 binder 线程去处理,然后再丢到发送消息去唤醒主线程处理。
      ANR 不是因为 for(;;) 而是主线程有耗时任务导致的 AMS 任务延迟导致的。比如上面启动 Service 的情况
      是先走的  service.onCreate() 然后去移除的 Handler 消息,所以 service onCreate() 不能有太耗时的操作。
    

消息屏障是什么?

在这里插入图片描述

  • 正常的消息队列分为几种消息,平时大多只用了普通消息,还有两种 一种是屏障消息,一种是异步消息。
  • 屏障消息不是为了分发的,是为了阻塞普通消息的分发的
  • 异步消息和普通消息的本质区别就是有一个异步的标志位,导致会有不同的处理。
  • 如何发布一个屏障?
  • frameworks/base/core/java/android/os/MessageQueue.java 中有一个函数 postSyncBarrier()
    private int postSyncBarrier(long when) {
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;
			// 插入消息链表
            return token;
        }
    }

因为屏障消息不需要分发,所以不需要 target 也就是 Handler,后面会根据 target 是不是空来判断是不是屏障消息。并且它也会按照时间排序,不过它只会影响后面的消息。返回的 token 是用来后面撤销屏障用的。我们自己发送的消息 target 必须是有值的。

  • 移除屏障的方法,需要通过 token
   public void removeSyncBarrier(int token) {
        // Remove a sync barrier token from the queue.
        // If the queue is no longer stalled by a barrier then wake it.
        synchronized (this) {
           	// 移除消息 ... 
            // If the loop is quitting then it is already awake.
            // We can assume mPtr != 0 when mQuitting is false.
            if (needWake && !mQuitting) {
                nativeWake(mPtr);
            }
        }
    }

移除消息通过 token,移除后调用 nativeWake(mPtr); 函数唤醒 native_wait() 。唤醒以后会继续处理加入的普通消息。

  • 屏障用在哪里了?

loop 获取消息是从 MessageQueue 中的 next() 函数,屏障消息也是如此

    Message next() {
        final long ptr = mPtr;
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            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;
                // msg.target == null 就是屏障消息
                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());
                }
                if (msg != null) {
                    // 处理返回消息
                    return msg;
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

            }
            nextPollTimeoutMillis = 0;
        }
    }

next() 函数在获取 message 时,判断它是不是屏障消息,也就是 target == null ,如果是屏障消息则进行 do while() 循环,查找是否有异步消息要处理,如果有异步消息则返回异步消息,如果没异步消息,然后睡眠等待屏障的移除(需要其他线程唤醒,也就是上面的移除唤醒)。

  • 插入消息也可能会唤醒线程
boolean enqueueMessage(Message msg, long when) {
	
	// 消息插入到了队列的头 如果休眠状态,需要唤醒
	// 如果普通消息,并且在屏障后面,则没有必要唤醒
	// 如果插入了最早的一条异步消息则需要唤醒
            if (needWake) {
                nativeWake(mPtr);
            }
}
  • Android framework 哪里用到了屏障

主要是屏幕绘制的时候 ViewRootImpl 的 scheduleTraversals() 开始绘制之前发送了一个 postSyncBarrier()

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 插入屏障 这样普通消息就会 block住。
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 然后等待 mTraversalRunnable 执行(下一个 vsync 信号到来)
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
   void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            performTraversals();
        }
    }

doTraversal() 的时候移除消息,然后开始绘制了。目的是为了防止开始绘制因为普通消息延迟。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP-IDF + Vscode ESP32 开发环境搭建以及开发入门

ESP-IDF Vscode ESP32 开发环境搭建以及开发入门 文章目录ESP-IDF Vscode ESP32 开发环境搭建以及开发入门1. 前言2. 下载开发工具3. 配置工具4. 创建工程5. 解决vscode找不到头文件&#xff0c;波浪线警告6. 添加自己的组件6.1 组件说明6.2 添加项目组件6.3 添加扩展组件7. …

Python进阶篇(一)-- Django快速上手

1 Django概述 Web框架&#xff0c;就是用于开发Web服务器端应用的基础设施&#xff0c;说得通俗一点就是一系列封装好的模块和工具。事实上&#xff0c;即便没有Web框架&#xff0c;我们仍然可以通过socket或CGI来开发Web服务器端应用&#xff0c;但是这样做的成本和代价在商业…

Stable Diffusion 1 - 初始跑通 文字生成图片

文章目录关于 Stable DiffusionLexica代码实现安装依赖库登陆 huggingface查看 huggingface token下载模型计算生成设置宽高测试迭代次数生成多列图片关于 Stable Diffusion A latent text-to-image diffusion model Stable Diffusion 是一个文本到图像的潜在扩散模型&#xff…

撕开市场缺口,认养一头牛“犟心”能给谁?

随着疫情防控政策优化&#xff0c;2023年以来中国消费力和投资活动均迎来复苏。其中&#xff0c;乳制品赛道受益于国内消费者健康消费理念的加强&#xff0c;呈现出稳步增长的势头。一方面&#xff0c;乳制品消费需求旺盛&#xff0c;市场未来可期。据中商研究院预计&#xff0…

【Hello Linux】Linux环境下写的第一个程序 -- 进度条

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;写出Linux中的第一个小程序 进度条 进度条小程序行缓冲区概念\r 和 \n进度条代码和演示行缓冲区概念 我们首先用两段代码来感受下行缓…

结合ENVI和PIE Hyp讲述高光谱遥感信息处理技术,包括光谱恢复、光谱库建立、光谱特征提取、混合像元分解、图像分类及精度检验

大气温室气体浓度不断增加&#xff0c;导致气候变暖加剧&#xff0c;随之会引发一系列气象、生态和环境灾害。如何降低温室气体浓度和应对气候变化已成为全球关注的焦点。海洋是地球上最大的“碳库”,“蓝碳”即海洋活动以及海洋生物&#xff08;特别是红树林、盐沼和海草&…

05 OpenCV色彩空间处理

色彩空间&#xff08;Color Space&#xff09;是一种用于描述颜色的数学模型&#xff0c;它将颜色表示为多维向量或坐标&#xff0c;通常由三个或四个独立的分量来表示。不同的色彩空间在颜色的表示方式、可表达颜色的范围、计算速度和应用场景等方面存在差异&#xff0c;不同的…

ChatGPT写代码水平惊艳到我,很性感但有点危险

这几天属实是被ChatGPT刷屏了&#xff0c;十年寒窗无人问&#xff0c;一举成名天下知。不少人和ChatGPT对话后&#xff0c;都觉得自己像个傻逼。这位“最强懂哥”可以轻松应答各种问题&#xff0c;给出的答案不仅条理清晰&#xff0c;还会引用例子支撑观点。让它帮忙写程序&…

九、初识卷积

文章目录1、通过边缘检测认识卷积2、Padding3、Strid Convelution4、RGB图像的卷积THE END1、通过边缘检测认识卷积 \qquad在使用神经网络进行图像识别时&#xff0c;神经网络的前几层需要完成对图像的边缘检测任务&#xff0c;所谓的边缘检测就是让计算机识别出一张图片的垂直…

【智能计算数学】微积分

高数问题解决流程引例&#xff1a;回归回归引例&#xff1a;分类分类线性可分FLD线性不可分智能计算讨论范围下降法为什么要用下降法&#xff1f;- 解析解很难写出公式或很复杂难计算有哪些常用的下降法&#xff1f;- 梯度下降&高斯-牛顿法梯度下降&#xff08;Gradient De…

初步认识操作系统(Operator System)

操作系统一&#xff0c;冯诺依曼体系结构内存的重要作用二&#xff0c;操作系统的概念三&#xff0c;设计操作系统的目的三&#xff0c;操作系统在计算机体系中的定位四&#xff0c;操作系统是如何进行管理的一&#xff0c;冯诺依曼体系结构 在众多计算机相关的书籍中&#xff…

linux安装docker和Docker Compose

1、安装环境 此处在Centos7进行安装&#xff0c;可以使用以下命令查看CentOS版本 lsb_release -a 在 CentOS 7安装docker要求系统为64位、系统内核版本为 3.10 以上&#xff0c;可以使用以下命令查看 uname -r 2、用yum源安装 2.1 查看是否已安装docker列表 yum list inst…

Doom流量回放工具导致的测试环境服务接口无响应的排查过程

Doom流量回放工具导致的测试环境服务接口无响应的排查过程 现象描述&#xff1a; a)部分接口&#xff08;A组接口&#xff09;无响应 b)部分接口&#xff08;B组接口&#xff09;正常响应 c)还有一部分接口&#xff08;C组接口&#xff09;,场景1无响应&#xff0c;场景2正常响…

ChatGPT 桌面应用程序 for macOS, Linux, Windows v0.10

请访问原文链接&#xff1a;https://sysin.org/blog/chatgpt/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;www.sysin.org ChatGPT 是什么 ChatGPT 是 OpenAI 公司开发的一种基于对话优化的语言模型。用俗话说就是“人工智障”聊天机器…

三次握手-升级详解-注意问题

TCP建立连接的过程就是三次握手&#xff08;Three-way Handshake&#xff09;&#xff0c;在建立连接的过程实际上就是客户端和服务端之间总共发送三个数据包。进行三次握手主要是就是为了确认双方都能接收到数据包和发送数据包&#xff0c;而客户端和服务端都会指定自己的初始…

Tomcat构建

软件架构C/S:Client/Server.需要安装才能使用。B/S:Brower/Server。有浏览器就可以。资源分类动态资源&#xff1a;每个用户访问相同的资源后&#xff0c;得到的结果可能不一样&#xff0c;称为动态资源。动态资源被访问后&#xff0c;先转换为静态资源&#xff0c;再被浏览器解…

ESP-IDF: 基于计数型信号量的生产者和消费者模型代码

ESP-IDF: 基于计数型信号量的生产者和消费者模型代码 SemaphoreHandle_t freeBowl NULL;//初始状态有5个空碗 SemaphoreHandle_t Mantou NULL;//初始状态没有馒头&#xff0c;从零开始计数 int queue[5]; //用数组模拟空碗&#xff0c;对数组取余操作&#xff0c;模拟循环链…

如何利用 ESLint 规范 TypeScript 代码

ESLint 是一种 JavaScript linter&#xff0c;可以用来规范 JavaScript 或 TypeScript 代码&#xff0c;本文教你怎么在项目中快速把 ESLint 安排上。 前导 怎么写出优雅的代码是一个老生常谈的话题&#xff0c;这其中包含了太多内容可聊&#xff0c;但搞一套标准规范绝对是万…

opencv锁定鼠标定位

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人的博客_CSDN博客-微信小程序,前端,python领域博主lqj_本人擅长微信小程序,前端,python,等方面的知识https://blog.csdn.net/lbcyllqj?spm1011.2415.3001.5343哔哩哔哩欢迎关注…

【MySQL进阶】 存储引擎 索引

&#x1f60a;&#x1f60a;作者简介&#x1f60a;&#x1f60a; &#xff1a; 大家好&#xff0c;我是南瓜籽&#xff0c;一个在校大二学生&#xff0c;我将会持续分享Java相关知识。 &#x1f389;&#x1f389;个人主页&#x1f389;&#x1f389; &#xff1a; 南瓜籽的主页…