[Framework] Android Handler 工作原理
Android 中的 Handler
都被人说烂了,但是还是想多说一次,因为在 Android 的系统中它真的非常重要而且它的机制并没有很复杂,无论是新手和老手都可以好好学习下,这对理解 Android 系统很重要,所以说学习的性价比非常高。
Android 中 IPC
跨进程通信主要依靠 Binder
, 而同一个进程的通信就主要依靠 Handler
。
这里先简单描述下 Handler
的工作流程:
首先在 Handler
处理任务的线程需要调用 Looper#prepare()
方法,来为当前线程创建一个 Looper
实例,这个实例通过 ThreadLocal
存放,也就是一个线程只能有一个这个实例;Looper
构造函数中会初始化一个 MessageQueue
对象,MessageQueue
对象就是用来封装任务的队列,它是用的链表实现,而任务队列的 Item 就是 Message
对象,也就是单个的任务;任务处理线程调用 Looper#prepare()
方法完成后,会调用 Looper#loop()
方法,这个时候任务线程就会阻塞去处理对应 Looper
中的 MessageQueue
中的任务,处理的方式就是一个死循环从 MessageQueue
中去获取最新的 Message
获取到后,然后调用对应的 Handler#handleMessage()
方法去执行它,执行完成后又去获取下一个任务,如果没有新的任务就会陷入阻塞,等有新的任务来的时候会唤醒这个阻塞(这个唤醒机制是用的 Linux 中的 epoll
机制),继续执行新的任务。
上面大致描述了 Looper
和 MessageQueue
如何从队列中获取新的任务和执行任务,这里再简单描述下怎么插入任务,首先要自定一个 Handler
对象,构造函数中需要传入上面所创建的 Looper
对象,其中自定义的 Handler#handleMessage
方法就是用来执行任务的方法,其他的线程需要向对应的 Looper
线程添加任务时,就调用上面 Handler
实例的 sendMessage
方法来添加任务,也就是向 MessageQueue
队列中添加任务,最终会在 Looper
所对应的线程执行,这样就完成了一次跨线程的通信。
下面是一个简单的 Handler
使用的代码:
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)var handlerLooper: Looper? = nullval handlerThread = object : Thread() {override fun run() {super.run()Looper.prepare()handlerLooper = Looper.myLooper()Looper.loop()}}handlerThread.start()while (handlerLooper == null) {// 等待 HandlerThread 初始化完成}val handler = object : Handler(handlerLooper!!) {override fun handleMessage(msg: Message) {super.handleMessage(msg)if (msg.what == 0) {// TODO: 处理任务,该方法最终工作在 HandlerThread 线程}}}val msg = handler.obtainMessage(0)// 发送的代码工作在主线程handler.sendMessage(msg)
}
Android 中有现成的 HandlerThread
,不用自定义,我这里是为了展示这个过程。
Tips: 我后续的源码分析都是基于 andorid 31
Looper
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();
}
// ...
初始化的过程非常简单,如果当前线程已经有 Looper
对象就直接报错,如果没有新建一个 Looper
对象放在 ThreadLocal
中,在 Looper
的构造函数中,创建了 MessageQueue
实例和保存了当前的 Thread
对象。
Looper 任务处理
// ...
public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}if (me.mInLoop) {Slog.w(TAG, "Loop again would have the queued messages be executed"+ " before this one completed.");}me.mInLoop = true;// ..me.mSlowDeliveryDetected = false;for (;;) {if (!loopOnce(me, ident, thresholdOverride)) {return;}}
}
// ...
Looper#loop()
方法也非常简单,修改了当前的 Looper
的一些状态,然后在死循环中无限调用 loopOnce()
方法去执行任务,如果该方法返回 false
就表示该 Looper
已经退出,就跳出循环。
private static boolean loopOnce(final Looper me,final long ident, final int thresholdOverride) {Message msg = me.mQueue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return false;}// This must be in a local variable, in case a UI event sets the loggerfinal Printer logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " "+ msg.callback + ": " + msg.what);}// ...Object token = null;if (observer != null) {token = observer.messageDispatchStarting();}long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);try {msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} catch (Exception exception) {if (observer != null) {observer.dispatchingThrewException(token, msg, exception);}throw exception;} finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) {Trace.traceEnd(traceTag);}}if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}// ...msg.recycleUnchecked();return true;
}
直接调用 MessageQueue#next()
方法去获取 Message
对象,如果返回空就表示 Looper
已经退出了。这里有一个 Printer
对象,在 Message
执行前后都会打印对应的日志,可以通过调用 Looper#setMessageLogging()
方法来自定义这个对象,根据打印的日志内容我们就可以分辨是开始前还是开始后,在 APM 中经常通过这个方法来判断主线程的任务执行是否超时。执行任务会调用的是 handler.target#dispatchMessage()
方法,其实这个 target 对象就是 Handler
,后续再分析这个方法。
这次看代码还有新发现,这里还多了一个 Observer
对象,我记得以前的旧版本是没有的,可以通过 Looper#setObserver()
方法来设置,它的接口如下:
public interface Observer {/*** Called right before a message is dispatched.** <p> The token type is not specified to allow the implementation to specify its own type.** @return a token used for collecting telemetry when dispatching a single message.* The token token must be passed back exactly once to either* {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}* and must not be reused again.**/Object messageDispatchStarting();/*** Called when a message was processed by a Handler.** @param token Token obtained by previously calling* {@link Observer#messageDispatchStarting} on the same Observer instance.* @param msg The message that was dispatched.*/void messageDispatched(Object token, Message msg);/*** Called when an exception was thrown while processing a message.** @param token Token obtained by previously calling* {@link Observer#messageDispatchStarting} on the same Observer instance.* @param msg The message that was dispatched and caused an exception.* @param exception The exception that was thrown.*/void dispatchingThrewException(Object token, Message msg, Exception exception);
}
Observer
可以监听任务开始,结束和异常,看上去它可以替换 Printer
,不过它是对应用层隐藏的,需要骚操作。
Handler
Handler 初始化
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {mLooper = looper;mQueue = looper.mQueue;mCallback = callback;mAsynchronous = async;
}
其中 Looper
和 MessageQueue
就不用多说了;其中 mCallback
对象就是可以优先于 Handler#handleMessage
方法处理任务,也可以拦截任务不让 Handler#handleMessage
执行;async
是表示发送的消息是否是异步消息,这个和消息屏障关系密切,后续会再分析。
Handler 发送消息
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {msg.target = this;msg.workSourceUid = ThreadLocalWorkSource.getUid();if (mAsynchronous) {msg.setAsynchronous(true);}return queue.enqueueMessage(msg, uptimeMillis);
}
无论是调用 Handler
的哪个方法发送消息,最终都会到 enqueueMessage()
方法里面去,在 Looper
中我们说到 target
就是 Handler
对象,在这里就得到验证了,如果 Handler
的构造函数中设置为异步,Message
也会被设置为异步,然后直接将处理好的 Message
通过 MessageQueue#enqueueMessage()
方法添加到队列中。
Handler 处理消息
在 Looper
下发消息时说到最后会调用 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
中有设置 Callback
就直接在对应的 Callback
中处理,不继续下发; 如果 Message
中没有设置 Callback
,会先检查 Handler
是否有设置 mCallback
,如果有设置,调用 mCallback
处理,mCallback
的 handleMessage
返回 true
就表示要拦截该消息,如果不拦截就交由 Handler
的 handleMessage()
方法处理。
MessageQueue
MessageQueue 插入任务
在讲 Handler
的时候说到插入消息会直接调用 MessageQueue#enqueueMessage()
方法:
boolean enqueueMessage(Message msg, long when) {if (msg.target == null) {throw new IllegalArgumentException("Message must have a target.");}synchronized (this) {if (msg.isInUse()) {throw new IllegalStateException(msg + " This message is already in use.");}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;}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;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {nativeWake(mPtr);}}return true;
}
如果 Message
没有设置 Handler
直接抛出异常;如果 Message
已经是 inUse
状态,直接抛出异常;如果 MessageQueue
已经退出,就直接回收 Message
;按照任务的触发时间由小到大,插入到 mMessage
链表中,最后如果需要唤醒 next()
方法中的 nativePollOnce()
方法,就会调用 nativeWake
方法,这是一个 C++ 实现的方法(内部实现其实是向一个管道 fd
中写入了一个数字 1,通过 epoll
机制,会监听到管道 fd
有数据写入,然后会唤醒 nativePollOnce()
方法,然后去读取新的 Message
)。
MessageQueue 读取任务
在讲 Looper
时讲到最后获取任务是通过 MessageQueue#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();}// 等待 epoll 唤醒,唤醒是等待 nativeWake() 方法调用或者超时(超时时间为 nextPollTimeoutMillis)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;// target 为空,就表示这是一个屏障的消息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) {// Next message is not ready. Set a timeout to wake up when it is ready.// 当前消息的时间大于当前的时间,表示还没有到达执行的时间,计算间隔,等待重新进入 nativePollOnce 方法nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {// 获取消息成功,把它从队列中移除,然后返回方法// Got a message.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 {// No more messages.nextPollTimeoutMillis = -1;}// Process the quit message now that all pending messages have been handled.if (mQuitting) {dispose();return null;}// 后续就是在没有消息时回调 IdleHandler// 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.// 如果 pendingIdleHandlerCount 小于 0 就表示当前的这一次 next() 方法调用,还没有执行过 IdleHandler,就需要去读取 IdleHandler 的数量,在一次 next() 方法调用时最多执行一次 IdleHandler。if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();}if (pendingIdleHandlerCount <= 0) {// 这里就表示 IdleHandler 为空,或者已经执行过了。// No idle handlers to run. Loop and wait some more.mBlocked = true;continue;}if (mPendingIdleHandlers == null) {mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];}mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);}// Run the idle handlers.// We only ever reach this code block during the first iteration.for (int i = 0; i < pendingIdleHandlerCount; i++) {final IdleHandler idler = mPendingIdleHandlers[i];mPendingIdleHandlers[i] = null; // release the reference to the handlerboolean keep = false;try {// 执行 idleHandlerkeep = idler.queueIdle();} catch (Throwable t) {Log.wtf(TAG, "IdleHandler threw exception", t);}// 如果不需要保留就把它移出if (!keep) {synchronized (this) {mIdleHandlers.remove(idler);}}}// Reset the idle handler count to 0 so we do not run them again.// 这里设置为 0 后,IdleHandler 就不会再次执行,需要等待下次的 next() 方法调用pendingIdleHandlerCount = 0;// While calling an idle handler, a new message could have been delivered// so go back and look again for a pending message without waiting.nextPollTimeoutMillis = 0;}
}
通过 nativePollOnce()
方法等待唤醒(通过调用 nativeWake()
方法)或者超时(超时时间是 nextPollTimeoutMillis
),唤醒后直接获取最新的一条 Message
(先跳过同步消息逻辑),然后判断任务执行的时间,如果大于当前时间就继续等待,直到可以执行,如果小于当前时间直接返回,同时把这条消息从队列中移除(先跳过 IdleHandler 逻辑)。
同步消息和消息屏障
如果 target
对象为空就表示该消息是一个同步屏障,如果是同步屏障的话他就会重新再去取该同步屏障消息后面的最新一个异步消息去执行,如果没有异步消息就会去等待。
通过以下方法添加屏障消息:
private int postSyncBarrier(long when) {// Enqueue a new sync barrier token.// We don't need to wake the queue because the purpose of a barrier is to stall it.synchronized (this) {final int token = mNextBarrierToken++;final Message msg = Message.obtain();msg.markInUse();msg.when = when;msg.arg1 = token;Message prev = null;Message p = mMessages;if (when != 0) {while (p != null && p.when <= when) {prev = p;p = p.next;}}if (prev != null) { // invariant: p == prev.nextmsg.next = p;prev.next = msg;} else {msg.next = p;mMessages = msg;}return token;}
}
其实就是在消息队列中添加了一个 target
为空的消息,同时在 Message
的 arg1
对象中添加 token
对象,然后这个 token
也会返回给调用方,这个 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) {Message prev = null;Message p = mMessages;while (p != null && (p.target != null || p.arg1 != token)) {prev = p;p = p.next;}if (p == null) {throw new IllegalStateException("The specified message queue synchronization "+ " barrier token has not been posted or has already been removed.");}final boolean needWake;if (prev != null) {prev.next = p.next;needWake = false;} else {mMessages = p.next;needWake = mMessages == null || mMessages.target != null;}p.recycleUnchecked();// If the loop is quitting then it is already awake.// We can assume mPtr != 0 when mQuitting is false.if (needWake && !mQuitting) {nativeWake(mPtr);}}
}
我之前在 [Framework] Android Choreographer 工作原理有简单介绍过,在绘制的时候就会用到同步消息和消息屏障来提高绘制相关的 Message
的优先级。
请求绘制最终会到 ViewRootImpl#scheduleTraversals()
方法中,在这个方法中就会添加一个屏障:
void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);notifyRendererOfFramePending();pokeDrawLockIfNeeded();}
}
在 Choregorapher
收到 Vsync
信号后会发送一个 Handler
的异步 Message
:
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,VsyncEventData vsyncEventData) {// ..Message msg = Message.obtain(mHandler, this);msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);// ..
}
在绘制完成后会调用 ViewRootImpl#unscheduleTraversals()
方法移除这个屏障:
void unscheduleTraversals() {if (mTraversalScheduled) {mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}}
IdleHandler
IdleHandler
就是在 next()
方法获取不到消息时会执行一次,next()
方法调用一次最多只能执行一次 IdleHandler
,执行 IdleHandler
的时候就表示当前的 Looper
比较空闲,IdleHandler
的数量最多为 4 个 (在 Activity
的 onDestroy()
回调就是在 IdleHandler
中执行的,可能写这部分代码的人,认为 onDestroy()
相对其他的生命周期没有那么重要,但是由于 IdleHandler
的特性也会引发一些问题,后续有时间会分析这个问题) 。
可以通过以下方法添加 IdleHandler
:
public void addIdleHandler(@NonNull IdleHandler handler) {if (handler == null) {throw new NullPointerException("Can't add a null IdleHandler");}synchronized (this) {mIdleHandlers.add(handler);}}
epoll 在 Handler 中的工作方式
在 MessageQueue
的构造函数中会通过 Native 方法 nativeInit()
初始化 epoll
,初始化流程会先通过 epoll_create()
方法创建一个 EpollFd
,然后构建一个管道的 fd
用于 epoll
来监听它的读写状态,这个 fd
被命名为 WakeFd
,然后通过 epoll_ctl()
方法将 WakeFd
添加到 EpollFd
中,以供后续通过 EpollFd
能够监听 WakeFd
的状态。
在 MessageQueue
的 next()
方法中会通过 Native 方法 nativePollOnce()
方法去监听上面所提到 WakeFd
的状态,Native 层是通过 epoll_wait()
方法去监听状态,这个方法会等到超时或者 WakeFd
有数据写入就会返回。
在 MessageQueue
的 enqueueMessage()
方法中插入 Message
后,会判断是否需要唤醒 next()
中的 nativePollOnce()
方法,如果需要唤醒就会调用 Native 方法 nativeWake()
方法,Native 层就会向 WakeFd
中写入一个数字 1,然后 nativePollOnce()
间听到这个 WakeFd
写状态后就会返回,也就解除 next()
方法的阻塞去读取新的 Message
。
最后聊一聊主线程
在 Android Framework 中有两个主要的主线程 Handler
,一个是 ActivityThread
中的 H
,它主要来处理四大组建的各种生命周期;还有一个是 Choreographer
中的 FrameHandler
,它主要负责绘制,动画,输入等操作。这两个 Handler
负责的工作和用户体验都极为密切,主线程也可以说非常忙。
在项目中我经常发现很多的代码都会在主线程执行,明明有的操作不需要主线程执行。比如说在一个网络请求的通用方法中,在网络完成的回调中就会主动切换到主线程,明明很多地方的代码不需要用到主线程,我个人的观点是一定需要切换主线程时再切换,在一定程度上能够缓解应用卡顿的问题。
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap
相关文章:
[Framework] Android Handler 工作原理
Android 中的 Handler 都被人说烂了,但是还是想多说一次,因为在 Android 的系统中它真的非常重要而且它的机制并没有很复杂,无论是新手和老手都可以好好学习下,这对理解 Android 系统很重要,所以说学习的性价比非常高。…...

KITTI数据集中的二进制激光雷达数据(.bin文件)转换为点云数据(.pcd文件)(C++代码)
目录 main.cpp CMakeLists.txt main.cpp #include <pcl/io/pcd_io.h> #include <pcl/point_types.h> #include <fstream> #include <iostream> #include <vector>int main() {// Define file pathsstd::string input_filename "/home/f…...
全球AI人工智能领袖:Anthropic联合创始人丹妮拉·阿莫迪!
在创业界中,有许多杰出的女性企业家,而丹妮拉阿莫迪则是其中的佼佼者。作为Anthropic联合创始人,丹妮拉以她的智慧、勇气和远见闻名于世。 她的故事既是启迪,又是励志,让我们一起来看看她的独特之处。 丹妮拉阿莫迪毕…...
CoT 的方式使用 LLM 设计测试用例实践
前期准备 import SparkApi import os from dotenv import load_dotenv, find_dotenv#以下密钥信息从控制台获取_=load_dotenv(find_dotenv()) appid = os.getenv("SPARK_APP_ID") api_secret=os.getenv("SPARK_APP_SECRET") api_key=os.getenv("SPAR…...

神秘的锦衣卫
在看明朝电视剧经常听到的一句台词:锦衣卫办案,闲杂人等速速离开。锦衣卫是明朝特务机构,直接听命于皇帝,是亲军卫之一,也是最重要的一卫。 1、卫所制 卫所制是明代最主要的军事制度,其目标是寓兵于农、屯…...
Springboot中使用Redis
Redis 是一个基于内存的key-value的结构数据库适合存储热点数据 Macos安装Redis https://redis.io/docs/getting-started/installation/install-redis-on-mac-os/安装redis brew install redis查看安装信息: brew info redis前台启动redis: redis-server后台启…...
超声波波形生成电路设计
摘要 随着我国微型电子技术和嵌入式系统的发展,目前行业内相对比较传统的超声波技术无法满足客户的需求。为了改进传统超声波技术在被测设备上的短板问题,在本次毕业设计中,将使用相对先进、快捷、智能的控制机制。该超声波生成控制系统的控制…...

C#和JS交互之Microsoft.ClearScript.V8(V8引擎)
之前测试了很多JS引擎,都只支持es5语法,不支持执行es6,测试了下微软的V8反正能跑通,应该是支持的。还得是微软呀。 如图:安装相关包: 这是参考的官方V8代码 using Microsoft.ClearScript.JavaScript; us…...

9月活动回顾(免费领取PPT)|火山引擎DataLeap、ByteHouse多位专家带来DataOps、实时计算等前沿技术分享!
更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群 在上月举行的火山引擎开发者社区 Meetup 第12期暨超话数据专场《数智化转型背景下的火山引擎大数据技术揭秘》上,来自火山引擎DataLeap、EMR、DataSail、…...
salesforce的按钮执行js代码如何链接到apex代码
在Salesforce中,你可以通过自定义JavaScript按钮或链接来触发Apex代码的执行。这可以通过使用JavaScript Remoting或Visualforce页面来实现。以下是一些步骤来将JavaScript按钮与Apex代码链接起来: 使用JavaScript Remoting链接JavaScript按钮到Apex代码…...

C语言 —— 操作符
1. 操作符的分类 算术操作符: - * / % 移位操作符: << >> 位操作符: & | ^ 赋值操作符: - 单目操作符 关系操作符 逻辑操作符 条件操作符 逗号表达式 下标引用、函数调用和结构成员 2. 算术操作符 - * / % 注意 /操作符: 对于整型的除法运算结果依然是整数…...

物联网AI MicroPython传感器学习 之 CCS811空气质量检测传感器
学物联网,来万物简单IoT物联网!! 一、产品简介 通过CCS811传感器模块可以测量环境中TVOC(总挥发性有机物质)浓度和eCO2(二氧化碳)浓度,作为衡量空气质量(IAQ)的指标。 引脚定义 VCC:3.3VGND&…...

TCP/IP(十五)拥塞控制
一 拥塞控制 ① 拥塞控制必要性 思考: 为什么要有拥塞控制呀,不是有流量控制了吗? ② 拥赛窗口 cwnd 什么是拥塞窗口? 和发送窗口有什么关系呢?明白: cwnd、swnd、rwnd 缩写 含义 ③ 如何知道当前网络是否出现了拥塞呢?…...
vue3 404解决方法
在 Vue 3 应用中解决 404 错误通常涉及到 Vue Router,因为 404 错误通常与路由无法匹配到的路径有关. 1. **检查路由配置**:首先确保你的路由配置正确。确保每个路由路径都与你的组件正确匹配,并且在需要时添加必要的路由守卫。 2. **通配符…...
Unity中使用Xlua调用lua相关
//引用命名空间 using XLua; public class L1 : MonoBehaviour {// Start is called before the first frame updatevoid Start(){//Lua解析器 让我们在Unity中使用luaLuaEnv env new LuaEnv();//执行env.DoString("print(OK)");//执行一个Lua脚本 在resource文件夹…...
基于http的protobuf服务实现
本文介绍在http协议中,使用protobuf格式进行二进制数据通信。双方需设置http的header中ContentType为application/x-protobuf。 1、springboot下实现protobuf: 1)pom.xml <dependency><groupId>org.springframework.boot</g…...

基于uniapp的商城外卖小程序
博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容:毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…...
【CSS】Tailwind CSS
直接参考 Tailwind CSS 安装提供了四种方式。因为我常用构建工具搭建项目,所以选择 Using PostCSS 。 其中 tailwind.config.js 的配置可以改为: module.exports {content: [./index.html, ./src/**/*.{vue,js,ts,jsx,tsx}],theme: {extend: {},},pl…...

leetcode-电话号码组合(C CODE)
1. 题目 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 示例 1: 输入:digits “23” 输出&#…...

Leetcode92. 反转链表 II
Every day a Leetcode 题目来源:92. 反转链表 II 解法1:模拟 注意 STL 的 reverse() 是左闭右开的。 代码: class Solution { public:ListNode *reverseBetween(ListNode *head, int left, int right){vector<int> nums getNums(…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...