[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 机制),继续执行新的任务。

上面大致描述了 LooperMessageQueue 如何从队列中获取新的任务和执行任务,这里再简单描述下怎么插入任务,首先要自定一个 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;
}

其中 LooperMessageQueue 就不用多说了;其中 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 下发消息时说到最后会调用 HandlerdispatchMessage() 方法:


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 处理,mCallbackhandleMessage 返回 true 就表示要拦截该消息,如果不拦截就交由 HandlerhandleMessage() 方法处理。

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 为空的消息,同时在 Messagearg1 对象中添加 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 个 (在 ActivityonDestroy() 回调就是在 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 的状态。
MessageQueuenext() 方法中会通过 Native 方法 nativePollOnce() 方法去监听上面所提到 WakeFd 的状态,Native 层是通过 epoll_wait() 方法去监听状态,这个方法会等到超时或者 WakeFd 有数据写入就会返回。
MessageQueueenqueueMessage() 方法中插入 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

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

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

相关文章

TCP/IP(五)TCP的连接管理(二)三次握手细节

一 ISN序列号探究 本文主要探究三次握手建立TCP连接的细节备注&#xff1a; 某些问题探究的比较深入,当前用不到,暂时通过链接引入进来吃水不忘挖井人&#xff1a; 小林 coding ① 初始序列号 ISN 是如何随机产生的 ISN: 初始化序列号 Initial Sequence Number 接收方和…

scsi READ CAPACITY (10)命令总结

READ CAPACITY (10)概述&#xff1a; READ CAPACITY(10)命令(参见表119)请求设备服务器将描述直接访问块设备的容量和介质格式的8字节参数数据传输到数据缓存中。这个命令可以被处理&#xff0c;就好像它有一个HEAD OF QUEUE任务属性。 如果逻辑单元支持保护信息&#xff0c;应…

渗透测试KAILI系统的安装环境(第八课)

KAILI系统的安装环境(第八课) Kaili是一款基于PHP7的高性能微服务框架&#xff0c;其核心思想是面向服务的架构&#xff08;SOA&#xff09;&#xff0c;支持http、websocket、tcp等多种通信协议&#xff0c;同时还提供了RPC、Service Mesh、OAuth2等功能。Kaili框架非常适合构…

gradle

Gradle Gradle 介绍 Gradle 官方文档是这样介绍的 Gradle 的&#xff1a; Gradle is an open-source build automationopen in new window tool flexible enough to build almost any type of software. Gradle makes few assumptions about what you’re trying to build or…

相机噪声评估

当拥有一个相机&#xff0c;并且写了一个降噪的算法&#xff0c;想要测试降噪的应用效果。 相机在光线不足的情况下产生噪点的原因主要与以下几个因素有关&#xff1a; 感光元件的工作原理&#xff1a;相机的图像传感器是由数百万甚至数千万的感光元件&#xff08;如CMOS或CC…

怎么把heic改成jpg?方法大全在这里

怎么把heic改成jpg&#xff1f;HEIC是一种现代的图像文件格式。它是由ISO制定的标准&#xff0c;并得到了苹果公司的支持和推广。与JPG等传统图像格式相比&#xff0c;HEIC格式可以提供更好的图像质量&#xff0c;并且占用更少的存储空间。这使得它在手机、平板电脑和其他移动设…

CANoe-如何实现27服务解锁

27服务解锁的工作原理可以在文章《诊断27服务介绍》查看,这里简单介绍下流程: Tester向ECU发送27 01诊断请求请求种子seed,ECU收到该请求后随机生成一个seed,通过67 01诊断响应发送给Tester。Tester收到该诊断响应后取出seed值,传入和ECU相同的算法后生成一个密钥keyT。然…

《理解深度学习》2023最新版本+习题答案册pdf

刚入门深度学习或者觉得学起来很困难的同学看过来了&#xff0c;今天分享的这本深度学习教科书绝对适合你。 就是这本已在外网获13.1万次下载的宝藏教科书《理解深度学习》。本书由巴斯大学计算机科学教授Simon J.D. Prince撰写&#xff0c;全书共541页&#xff0c;目前共有21…

Vscode 插件-代码敲出不同的特效

为了让写代码的时候增加一点趣味性&#xff0c;vscode有个插件&#xff0c;可以增加烟花特效&#xff0c;还挺好玩的。 一.在应用商店下载这个插件 二. 在设置里 添加配置文件 settings.json //是否开启"powermode.enabled": true,//效果样式 “水花-particles”,…

linux 查看当前正在运行的端口和监听的端口的工具及命令

在Linux系统中&#xff0c;你可以使用多种工具和命令来查看当前正在运行的端口和监听的端口。以下是一些常用的工具和命令&#xff1a; netstat命令&#xff1a; Netstat&#xff08;Network Statistics&#xff09;是一个用于查看网络连接、路由表和接口统计信息的命令行工具…

PostMan环境变量、全局变量、动态参数使用

一、环境准备 postmanmoco [{"description": "登录认证","request": {"uri": "/login","method": "post","forms": {"user": "admin","password": "a123…

CentOS 7 基于C 连接ZooKeeper 客户端

前提条件&#xff1a;CentOS 7 编译ZooKeeper 客户端&#xff0c;请参考&#xff1a;CentOS 7 编译ZooKeeper 客户端 1、Docker 安装ZooKeeper # docker 获取zookeeper 最新版本 docker pull zookeeper# docker 容器包含镜像查看 docker iamges# 准备zookeeper 镜像文件挂载对…

【C#】什么是并发,C#常规解决高并发的基本方法

给自己一个目标&#xff0c;然后坚持一段时间&#xff0c;总会有收获和感悟&#xff01; 在实际项目开发中&#xff0c;多少都会遇到高并发的情况&#xff0c;有可能是网络问题&#xff0c;连续点击鼠标无反应快速发起了N多次调用接口&#xff0c; 导致极短时间内重复调用了多次…

Tomcat 线程模型性能调优

Linux I/O模型详解 I/O要解决什么问题 I/O&#xff1a;在计算机内存与外部设备之间拷贝数据的过程。 程序通过CPU向外部设备发出读指令&#xff0c;数据从外部设备拷贝至内存需要一段时间&#xff0c;这段时间CPU就没事情做了&#xff0c;程序就会两种选择&#xff1a; 让出…

mysql建表语句转Oracle建表语句

mysql建表语句转Oracle建表语句 mysql CREATE TABLE XM_PSXGBM (id varchar(32) NOT NULL,bbh varchar(32) DEFAULT NULL COMMENT ,xmid varchar(100) DEFAULT NULL COMMENT ,bmid varchar(100) DEFAULT NULL COMMENT 部门编号,bmmc varchar(100) DEFAULT NULL COMMENT 部门名…

四款数字办公工具大比拼,在线办公无压力

在线办公软件使企业、员工实现办公场所、距离的自由&#xff0c;尤其是近几年&#xff0c;受“口罩”的影响&#xff0c;远程办公软件的使用者也越来越多&#xff0c;无论是财务、行政、还是设计师&#xff0c;都开始追求好用的在线办公软件&#xff0c;作为办公软件发烧友&…

10月10日上课内容 Docker--harbor私有仓库部署与管理

Docker--harbor私有仓库部署与管理 ------------------ 1、搭建本地私有仓库 ------------------------------ #首先下载 registry 镜像 docker pull registry #在 daemon.json 文件中添加私有镜像仓库地址 vim /etc/docker/daemon.json { "insecure-registries"…

C++11新特性(右值引用,万能转发)

这篇文章是C的重中之重&#xff0c;通过这篇文章你能体会到C/C大佬们对性能的极致追求&#xff0c;你能感受到独属C/C人的浪漫&#xff0c;对高消耗的零容忍&#xff0c;对高性能的不倦探索。右值引用是由Scott Meyers在他的著名书籍《Effective C》中提出的&#xff0c;因为其…

每日一练 | 网络工程师软考真题Day41

1、包过滤防火墙对通过防火墙的数据包进行检查&#xff0c;只有满足条件的数据包才能通过&#xff0c;对数据包的检查内容一般不包括 。 A&#xff0e;源地址 B&#xff0e;目的地址 C&#xff0e;协议 D&#xff0e;有效载荷 2、下面关于ARP木马的描述中&#xff0c;错误的…

java html转word、pdf(包含图片)

html转word maven依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.14</version> </dependency> <dependency><groupId>org.jsoup</groupId><artifactId>…