当前位置: 首页 > news >正文

再谈Android重要组件——Handler(Native篇)

前言

最近工作比较忙,没怎么记录东西了。Android的Handler重要性不必赘述,之前也写过几篇关于hanlder的文章了:

  • Handler有多深?连环二十七问
  • Android多线程:深入分析 Handler机制源码(二)

Android单个进程其实就是个死循环,里面接收handler发来的事件处理,所谓的事件驱动系统。本篇文章我们将深入 Native 层,一起来探究 Looper#loop() 为什么不会卡死主线程背后的原因。

从 Android 2.3 开始,Google 把 Handler 的阻塞/唤醒方案从 Object#wait() / notify(),改成了用 Linux epoll 来实现。

原因是 Native 层也引入了一套消息管理机制,用于提供给 C/C++ 开发者使用,而现有的阻塞/唤醒方案是为 Java 层准备的,只支持 Java。

Native 希望能够像 Java 一样:main 线程在没有消息时进入阻塞状态,有到期消息需要执行时,main 线程能及时醒过来处理。怎么办?有两种选择:

  • 要么,继续使用 Object#wait() / notify( ),Native 向消息队列添加新消息时,通知 Java 层自己需要什么时候被唤醒
  • 要么,在 Native 层重新实现一套阻塞/唤醒方案,弃用 Object#wait() / notify() ,Java 通过 jni 调用 Native 进入阻塞态

结局我们都知道了,Google 选择了后者。

其实如果只是将 Java 层的阻塞/唤醒移植到 Native 层,倒也不用祭出 epoll 这个大杀器 ,Native 调用 pthread_cond_wait 也能达到相同的效果。

选择 epoll 的另一个原因是, Native 层支持监听 自定义 Fd (比如 Input 事件就是通过 epoll 监听 socketfd 来实现将事件转发到 APP 进程的),而一旦有监听多个流事件的需求,那就只能使用 Linux I/O 多路复用技术

理解 I/O多路复用之epoll

说了这么多,那到底什么是 epoll ?

epoll 全称 eventpoll,是 Linux I/O 多路复用的其中一个实现,除了 epoll 外,还有 select 和 poll ,我们这只讨论 epoll。

要理解 epoll  ,我们首先需要理解什么是 "流"?

在 Linux 中,任何可以进行 I/O 操作的对象都可以看做是流,一个 文件, socket, pipe,我们都可以把他们看作流。

接着我们来讨论流的 I/O 操作,通过调用 read() ,我们可以从流中读出数据;通过 write() ,我们可以往流 写入数据。

现在假定一个情形,我们需要从流中读数据,但是流中还没有数据:

int socketfd = socket();
connect(socketfd,serverAddr);
int n = send(socketfd,'在吗');
n = recv(socketfd); //等待接受服务器端 发过来的信息
...//处理服务器返回的数据

一个典型的例子为,客户端要从 socket 中读数据,但是服务器还没有把数据传回来,这时候该怎么办?

  • 阻塞: 线程阻塞到 recv() 方法,直到读到数据后再继续向下执行;
  • 非阻塞: recv() 方法没读到数据立刻返回 -1 ,用户线程按照固定间隔轮询 recv() 方法,直到有数据返回;

好,现在我们有了阻塞和非阻塞两种解决方案,接着我们同时发起100个网络请求,看看这两种方案各自会怎么处理:

先说阻塞模式,在阻塞模式下,一个线程一次只能处理一个流的 I/O 事件,想要同时处理多个流,只能使用 多线程 + 阻塞 I/O 的方案。但是,每个 socket 对应一个线程会造成很大的资源占用,尤其是对于长连接来说,线程资源一直不会释放,如果后面陆续有很多连接的话,很快就会把机器的内存跑完。

在非阻塞模式下,我们发现 单线程可以同时处理多个流了。只要不停的把所有流从头到尾的访问一遍,就可以得知哪些流有数据(返回值大于-1),但这样的做法效率也不高,因为如果所有的流都没有数据,那么只会白白浪费 CPU。

发现问题了吗?只有阻塞和非阻塞这两种方案时,一旦有监听多个流事件的需求,用户程序只能选择,要么浪费线程资源(阻塞型 I/O),要么浪费 CPU 资源(非阻塞型 I/O),没有其他更高效的方案。

并且,这个问题在用户程序端是无解的,必须让内核创建某种机制,把这些流的监听事件接管过去,因为任何事件都必须通过内核读取转发,内核总是能在第一时间知晓事件发生。

这种能够让用户程序拥有 “同时监听多个流读写事件” 的机制,就被称为 I/O 多路复用

然后我们来看 epoll 提供的三个函数:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epoll_create() 用于创建一个 epoll 池;
  • epoll_ctl() 用来执行 fd 的 “增删改” 操作,最后一个参数 event 是告诉内核 需要监听什么事件。还是以网络请求举例, socketfd  监听的就是 可读事件,一旦接收到服务器返回的数据,监听 socketfd 的对象将会收到 回调通知,表示 socket 中有数据可以读了;
  • epoll_wait() 是 使用户线程阻塞 的方法,它的第二个参数 events 接受的是一个 集合对象,如果有多个事件同时发生,events 对象可以从内核得到发生的事件的集合;

理解 Linux eventfd

理解了 epoll 我们再来看 Linux eventfd ,eventfd 是专门用来传递事件的 fd ,它提供的功能也非常简单:累计计数。

int efd = eventfd();
write(efd, 1);//写入数字1
write(efd, 2);//再写入数字2
int res = read(efd);
printf(res);//输出值为 3

通过 write() 函数,我们可以向 eventfd 中写入一个 int 类型的值,并且,只要没有发生 读 操作,eventfd 中保存的值将会一直累加。

通过 read() 函数可以将 eventfd 保存的值读了出来,并且,在没有新的值加入之前,再次调用 read() 方法会发生阻塞,直到有人重新向 eventfd 写入值。

eventfd 实现的是计数的功能,只要 eventfd 计数不为 0 ,那么表示 fd 是可读的。再结合 epoll 的特性,我们可以非常轻松的创建出 生产者/消费者模型

epoll + eventfd 作为消费者大部分时候处于阻塞休眠状态,而一旦有请求入队(eventfd 被写入值),消费者就立刻唤醒处理,Handler 机制的底层逻辑就是利用 epoll + eventfd。

好,有了 epoll 、 eventfd 基础,接下来我们开始正式进入 Handler 机制的 Native 世界。

进入Native Handler

绝大多数 Android 工程师都或多或少的了解过 Handler 机制,所以关于 Handler 的基本使用和实现的原理我们就不过多赘述了,直奔主题。

我们来重点关注 MessageQueue 类中的几个 jni 方法:nativeInit()、nativePollOnce() 和 nativeWake();

它们分别对应了 Native 消息队列中的 初始化消息队列、 消息的循环与阻塞 以及 消息的分送与唤醒 这三大环节。

/frameworks/base/core/java/android/os/MessageQueue.java
class MessageQueue {private native static long nativeInit();private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/private native static void nativeWake(long ptr);}

消息队列的初始化

先来看第一步,消息队列的初始化流程:

Java MessageQueue 构造函数中会调用 nativeInit() 方法,同步在 Native 层也会创建一个消息队列 NativeMessageQueue 对象,用于保存 Native 开发者发送的消息。

/frameworks/base/core/java/android/os/MessageQueue.java
MessageQueue(boolean quitAllowed) {mQuitAllowed = quitAllowed;mPtr = nativeInit();
}

看代码,在 NativeMessageQueue 的构造函数中,触发创建 Looper 对象(Native 层的)

/frameworks/base/core/jni/android_os_MessageQueue.cpp
class android_os_MessageQueue {void android_os_MessageQueue_nativeInit() {NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();}NativeMessageQueue() {mLooper = Looper::getForThread();if (mLooper == NULL) {mLooper = new Looper(false);Looper::setForThread(mLooper);}}
}

Native 创建 Looper 对象的处理逻辑和 Java 一样:先去 线程局部存储区 获取 Looper 对象,如果为空,创建一个新的 Looper 对象并保存到 线程局部存储区。

我们继续,接着来看 Native Looper 初始化流程:

/system/core/libutils/Looper.cpp
class looper {Looper::Looper() {int mWakeEventFd = eventfd();rebuildEpollLocked();}void rebuildEpollLocked(){int mEpollFd = epoll_create();//哎,这儿非常重要,在 Looper 初始化时创建了 epoll 对象epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);//把用于唤醒消息队列的eventfd 添加到 epoll 池}}

关键的地方来了!!!

Looper 的构造函数首先创建了 eventfd 对象 :mWakeEventFd,它的作用就是用来监听 MessageQueue 是否有新消息加入,这个对象非常重要,一定要记住它!

随后调用的 rebuildEpollLocked() 方法中,又创建了 epoll 对象:mEpollFd,并将刚刚申请的 mWakeEventFd 注册到 epoll 池;

到这一步,Handler 机制最依赖的两大核心对象 mEpollFd 和 mWakeEventFd ,全部都初始化成功!

我们来梳理一下 消息队列的初始化 步骤:

  1. Java 层初始化消息队列时,同步调用 nativeInit() 方法,在 native 层创建了一个 NativeMessageQueue 对象;
  2. Native 层的消息队列被创建的同时,也会创建一个 Native Looper 对象;
  3. 在 Native Looper 构造函数中,调用 eventfd() 生成 mWakeEventFd,它是后续用于唤醒消息队列的核心;
  4. 最后调用 rebuildEpollLocked() 方法,初始化了一个 epoll 实例 mEpollFd ,然后将 mWakeEventFd 注册到 epoll 池;

至此,Native 层的消息队列初始化完成,Looper 对象持有 mEpollFd 和 mWakeEventFd 两大金刚。

消息的循环与阻塞

Java 和 Native 的消息队列都创建完以后,整个线程就会阻塞到 Looper#loop() 方法中,在 Java 层的的调用链大致是这样的:

Looper#loop()-> MessageQueue#next()-> MessageQueue#nativePollOnce()
}

MessageQueue 最后一步调用的 nativePollOnce() 是一个 jni 方法,具体实现在 Native 层。

我们接着往下跟,看看 Native 中做了些什么:

/frameworks/base/core/jni/android_os_MessageQueue.cpp
class android_os_MessageQueue {//jni方法,转到 NativeMessageQueue#pollOnce()void android_os_MessageQueue_nativePollOnce(){nativeMessageQueue->pollOnce(env, obj, timeoutMillis);}class NativeMessageQueue : MessageQueue {/转到 Looper#pollOnce() 方法void pollOnce(){mLooper->pollOnce(timeoutMillis);}}
}

nativePollOnce() 接受到请求后,随手转发到 NativeMessageQueue 的 pollOnce() 方法。

而 NativeMessageQueue#pollOnce() 中什么都没做,只是又把请求转发给了 Looper#pollOnce()。

看来主要的逻辑都在 Looper 中,我们接着往下看:

//system/core/libutils/Looper.cpp
class looper {int pollOnce(int timeoutMillis){int result = 0;for (;;) {if (result != 0) {return result;}result = pollInner(timeoutMillis);//超时}}int pollInner(int timeoutMillis){int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);//调用 epoll_wait() 等待事件的产生}
}

看到了吗?线程阻塞和唤醒的执行逻辑都在这!

pollOnce() 会不停的轮询 pollInner() 方法,检查它的的返回值 result

这里的 result 类型是在 Looper.h 文件中声明的枚举类,一共有4种结果:

  • -1 表示在 “超时时间到期” 之前使用 wake() 唤醒了轮询,通常是有需要立刻执行的新消息加入了队列;
  • -2 表示多个事件同时发生,有可能是新消息加入,也有可能是监听的 自定义 fd 发生了 I/O 事件;
  • -3 表示设定的超时时间到期了;
  • -4 表示错误,不知道哪里会用到;

消息队列中没消息,或者 设定的超时时间没到期,再或者 自定义 fd 没有事件发生,都会导致线程阻塞到 pollInner() 方法调用。

pollInner() 中,则是使用了 epoll_wait() 系统调用等待事件的产生

本小节标题是 消息的循环与阻塞 ,现在线程已经阻塞到 pollInner() ,我们可以来梳理下发生阻塞的前后逻辑:

消息队列在初始化成功以后,Java 层的 Looper#loop() 会开始无限轮询,不停的获取下一条消息。如果消息队列为空,调用 epoll_wait 使线程进入到阻塞态,让出 CPU 调度

从 Java 到 Native 整个调用流程大致是这样的:

Looper#loop()-> MessageQueue#next()-> MessageQueue#nativePollOnce()-> NativeMessageQueue#pollOnce() //注意,进入 Native 层-> Looper#pollOnce()-> Looper#pollInner()-> epoll_wait()

消息的发送/唤醒机制

好,现在的消息队列里面是空的,并且经过上一小节的分析后,我们发现用户线程阻塞到了 native 层的 Looper#pollInner() 方法,我们来向消息队列发送一条消息唤醒它。

前面我们说了,Java 和 Native 都各自维护了一套消息队列,所以他们发送消息的入口也不一样
Java 开发使用 Handler#sendMessage() / post(),C/C++ 开发使用 Looper#sendMessage()。

我们先来看 Java:

/frameworks/base/core/java/android/os/Handler.java
class Handler {boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this;return queue.enqueueMessage(msg, uptimeMillis);}
}/frameworks/base/core/java/android/os/MessageQueue.java
class MessageQueue {boolean enqueueMessage(Message msg, long when) {//...按照到期时间将消息插入消息队列if (needWake) {nativeWake(mPtr);}}}

在使用 Handler 发送消息时,不管调用的是 sendMessage 还是 post,最后都是调用到 MessageQueue#enqueueMessage() 方法将消息入列,入列的顺序是按照执行时间先后排序。

如果我们发送的消息需要马上被执行,那么将 needWake 变量置为 true,接着使用 nativeWake() 唤醒线程。

注:nativeWake() 方法也是 jni 调用,经过层层转发最终调用到 Native Looper 中的 wake() 方法,整个转发过程的调用链清晰而且非常简单,这里就不展开分析了。

Java 发送消息的方式聊完了,然后我们看 Native 层如何发送消息:

/system/core/libutils/Looper.cpp
class looper {void Looper::sendMessageAtTime(uptime, handler,message) {int i = 0;int messageCount = mMessageEnvelopes.size();while (i < messageCount && uptime >= mMessageEnvelopes.itemAt(i).uptime) {i += 1;}mMessageEnvelopes.insertAt(messageEnvelope(uptime, handler, message), i, 1);// Wake the poll loop only when we enqueue a new message at the head.if (i == 0) {wake();}}
}

看上面的代码,Native 层通过 sendMessageAtTime() 方法向消息队列发送消息,添加消息的处理逻辑和 Java 处理逻辑类似:

按照时间的先后顺序添加到 mMessageEnvelopes 集合中,执行时间离得最近的消息被放在前面,如果发现需要唤醒线程,则调用 wake() 方法。

好,Java 和 Native 发送消息的方式都介绍完了。

我们发现,虽然它俩 发消息的方式 、消息类型 、 送达的消息队列 都不相同,但是,当需要唤醒线程时,Java 和 Native 都会执行到 Looper#wake() 方法。

之前我们说 “Handler 机制的底层是 epoll + eventfd”,读者朋友不妨大胆猜一下,这里的线程是怎么被唤醒的?

/system/core/libutils/Looper.cpp
class looper {void Looper::wake() {int inc = 1;write(mWakeEventFd, &inc);}
}

答案非常简单,write() 一行方法调用,向 mWakeEventFd 写入了一个 1(小提示:mWakeEventFd 的类型是 eventfd )。

为什么 mWakeEventFd 写入了一个 1,线程就可以被唤醒呢???

mWakeEventFd 被写入值后,状态会从 不可读 变成 可读,内核监听到 fd 的可读写状态发生变化,会将事件从内核返回给 epoll_wait() 方法调用;

而 epoll_wait() 方法一旦返回,阻塞态将会被取消,线程继续向下执行。

好,我们来总结一下 消息的发送与唤醒 中几个关键的步骤:

  1. Java 层发送消息,调用 MessageQueue#enqueueMessage() 方法,如果消息需要马上执行,那么调用 nativeWake() 执行唤醒;
  2. Native 层发送消息,调用 Looper#sentMessageAtTime() 方法,处理逻辑与 Java 类似,如果需要唤醒线程,调用 Looper#wake();
  3. Looper#wake() 唤醒方法很简单,向 mWakeEventFd 写入 1;
  4. 初始化队列 时为 mWakeEventFd 注册了 epoll 监听,所以一旦有来自于 mWakeEventFd 的新内容, epoll_wait() 阻塞调用就会返回,这里就已经起到了唤醒队列的作用;

呼~ 到这里 消息的发送与唤醒 的流程基本上结束了,接下来是 Handler 的重头戏:线程唤醒后的消息分发处理:

唤醒后消息的分发处理

线程在没有消息需要处理时会阻塞在 Looper 中的 pollInner() 方法调用,线程唤醒以后同样也是在 pollInner() 方法中继续执行。

线程醒来以后,先判断自己为什么醒过来,再根据唤醒类型执行不同的逻辑。

pollInner() 方法稍微有点长,大致可以分为5步来看,我们一点点来捋:

/system/core/libutils/Looper.cpp
class looper {int pollInner(int timeoutMillis){int result = POLL_WAKE;// step 1,epoll_wait 方法返回int eventCount = epoll_wait(mEpollFd, eventItems, timeoutMillis); if (eventCount == 0) { // 事件数量为0表示,达到设定的超时时间result = POLL_TIMEOUT;}for (int i = 0; i < eventCount; i++) {if (eventItems[i] == mWakeEventFd) {// step 2 ,清空 eventfd,使之重新变为可读监听的 fdawoken();} else {// step 3 ,保存自定义fd触发的事件集合mResponses.push(eventItems[i]);}}// step 4 ,执行 native 消息分发while (mMessageEnvelopes.size() != 0) {if (messageEnvelope.uptime <= now) { // 检查消息是否到期messageEnvelope.handler->handleMessage(message);}}// step 5 ,执行 自定义 fd 回调for (size_t i = 0; i < mResponses.size(); i++) {response.request.callback->handleEvent(fd, events, data);}return result;}void awoken() {read(mWakeEventFd) ;// 重新变成可读事件}}

step 1 : epoll_wait 方法返回说明有事件发生,返回值 eventCount 是发生事件的数量。如果为0,表示达到设定的超时时间,下面的判断逻辑都不会走,不为0,那么我们开始遍历内核返回的事件集合 eventItems,根据类型执行不同的逻辑。

step 2 : 如果事件类型是消息队列的 eventfd ,说明有人向消息队列提交了需要马上执行的消息,我们只需把消息队列的 eventfd 数据读出来,使他重新变成可以触发 可读事件 的 fd,然后等待方法结束就行了。

step 3 : 事件不是消息队列的 eventfd ,说明有其他地方注册了监听 fd,那么,我们将发生的事件保存到 mResponses 集合中,待会需要对这个事件做出响应,通知注册对象。

step 4 : 遍历 Native 的消息集合 mMessageEnvelopes,检查每个消息的到期时间,如果消息到期了,交给 handler 执行分发,分发逻辑参考 Java Handler。

step 5 : 遍历 mResponses 集合,把其他地方注册的 自定义 fd 消费掉,响应它们的回调方法。

唤醒后执行的逻辑还是非常复杂的,我们总结一下:

用户线程被唤醒后,优先分发 Native 层的消息,紧接着,通知 自定义 fd 发生的事件(如果有的话),最后 pollInner() 方法结束,返回到 Java 层 Looper#loop() 方法执行到 Java 层的消息分发。只有当 Java Handler 执行完消息分发,一次 loop() 循环才算是完成。

再之后,因为 Looper#loop() 是死循环,所以会马上再一次进入循环,继续调用 next() 方法获取消息、阻塞到 pollInner() 、从 pollInner() 唤醒执行分发,执行结束接着进入下一次循环,无尽的轮回。

main 线程的一生都将重复这一流程,直到 APP 进程结束运行...

总结

以上就是 Handler Native 篇的全部内容,主要介绍了 Java MessageQueue 中几个关键的 jni 方法在底层是如何实现的。

将全部的代码逻辑分析完以后,我们会发现 Native Handler 的实现不算复杂,关键的阻塞与唤醒部分是借助了 Linux 系统 epoll 机制来实现的。

所以,我们只要理解了 epoll 机制,再对照源码看看 Looper#pollInner() 中的内部逻辑,就能明白整个 Handler 机制是怎么一回事了。

希望对大家有帮助。

相关文章:

再谈Android重要组件——Handler(Native篇)

前言 最近工作比较忙&#xff0c;没怎么记录东西了。Android的Handler重要性不必赘述&#xff0c;之前也写过几篇关于hanlder的文章了&#xff1a; Handler有多深&#xff1f;连环二十七问Android多线程&#xff1a;深入分析 Handler机制源码&#xff08;二&#xff09; And…...

Javaweb之javascript的详细解析

JavaScript html完成了架子&#xff0c;css做了美化&#xff0c;但是网页是死的&#xff0c;我们需要给他注入灵魂&#xff0c;所以接下来我们需要学习JavaScript&#xff0c;这门语言会让我们的页面能够和用户进行交互。 1.1 介绍 通过代码/js效果演示提供资料进行效果演示&…...

Linux常用命令——cd命令

在线Linux命令查询工具 cd 切换用户当前工作目录 补充说明 cd命令用来切换工作目录至dirname。 其中dirName表示法可为绝对路径或相对路径。若目录名称省略&#xff0c;则变换至使用者的home directory(也就是刚login时所在的目录)。另外&#xff0c;~也表示为home directo…...

VHDL基础知识笔记(1)

1.实体&#xff1a;其电路意义相当于器件&#xff0c;它相当于电路原理图上的元器件符号。它给出了器件的输入输出引脚。实体又被称为模块。 2.结构体&#xff1a;这个部分会给出实体&#xff08;或者说模块&#xff09;的具体实现&#xff0c;指定输入和输出的行为。结构体的…...

volatile-日常使用场景

6.4 如何正确使用volatile 单一赋值可以&#xff0c;但是含复合运算赋值不可以&#xff08;i之类的&#xff09; volatile int a 10; volatile boolean flag true; 状态标志&#xff0c;判断业务是否结束 作为一个布尔状态标志&#xff0c;用于指示发生了一个重要的一次…...

策略模式在数据接收和发送场景的应用

在本篇文章中&#xff0c;我们介绍了策略模式&#xff0c;并在数据接收和发送场景中使用了策略模式。 背景 在最近项目中&#xff0c;需要与外部系统进行数据交互&#xff0c;刚开始交互的系统较为单一&#xff0c;刚开始设计方案时打算使用了if else 进行判断&#xff1a; if(…...

学习LevelDB架构的检索技术

目录 一、LevelDB介绍 二、LevelDB优化检索系统关键点分析 三、读写分离设计和内存数据管理 &#xff08;一&#xff09;内存数据管理 跳表代替B树 内存数据分为两块&#xff1a;MemTable&#xff08;可读可写&#xff09; Immutable MemTable&#xff08;只读&#xff0…...

Docker Swarm实现容器的复制均衡及动态管理:详细过程版

Swarm简介 Swarm是一套较为简单的工具&#xff0c;用以管理Docker集群&#xff0c;使得Docker集群暴露给用户时相当于一个虚拟的整体。Swarm使用标准的Docker API接口作为其前端访问入口&#xff0c;换言之&#xff0c;各种形式的Docker Client(dockerclient in go, docker_py…...

Proteus仿真--1602LCD显示仿手机键盘按键字符(仿真文件+程序)

本文主要介绍基于51单片机的1602LCD显示仿手机键盘按键字符&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 其中左下角12个按键模拟仿真手机键盘&#xff0c;使用方法同手机键一样&#xff0c;长按自动跳动切换键值&#xff0c;松手后确认选择&#xff…...

Rust语言和curl库编写程序

这是一个使用Rust语言和curl库编写的爬虫程序&#xff0c;用于爬取视频。 use std::env; use std::net::TcpStream; use std::io::{BufReader, BufWriter}; ​ fn main() {// 获取命令行参数let args: Vec<String> env::args().collect();let proxy_host args[1].clon…...

FSDiffReg:心脏图像的特征和分数扩散引导无监督形变图像配准

论文标题&#xff1a; FSDiffReg: Feature-wise and Score-wise Diffusion-guided Unsupervised Deformable Image Registration for Cardiac Images 翻译&#xff1a; FSDiffReg&#xff1a;心脏图像的特征和分数扩散引导无监督形变图像配准 摘要 无监督可变形图像配准是医学…...

音视频技术开发周刊 | 318

每周一期&#xff0c;纵览音视频技术领域的干货。 新闻投稿&#xff1a;contributelivevideostack.com。 日程揭晓&#xff01;速览深圳站大会专题议程详解 LiveVideoStackCon 2023 音视频技术大会深圳站&#xff0c;保持着往届强大的讲师阵容以及高水准的演讲质量。两天的参会…...

asp.net docker-compose添加sql server

打开docker-compose.yml 添加 sqldata:image: mysql:8.1.0 打开docker-compose.override.yml 添加 sqldata:environment:- MYSQL_ROOT_PASSWORDPasswordports:- "8080:8080"volumes:- killsb-one-sqldata:/etc/mysql/conf.d 在docker里面就有了sql server容器镜像…...

uniapp 微信小程序 uni-file-picker上传图片报错 chooseAndUploadFile

这个问题真的很搞&#xff0c; 原因是微信开发者工具更新了&#xff0c;导致图片上传问题。 解决方法&#xff1a; 将微信开发者工具的基础库改为2.33.0一下即可。 在微信开发者工具详情 - 本地设置中&#xff08;记得点击‘推送’按钮&#xff09;&#xff1a;...

《向量数据库指南》——用 Milvus Cloud和 NVIDIA Merlin 搭建高效推荐系统结论

如何搭建一个高效的推荐系统? 简单来说,现代推荐系统由训练/推理流水线(pipeline)组成,涉及数据获取、数据预处理、模型训练和调整检索、过滤、排名和评分相关的超参数等多个阶段。走遍这些流程之后,推荐系统能够给出高度个性化的推荐结果,从而提升产品的用户体验。 为…...

致:CSGO游戏搬砖人的一封信

最近大家还在坚持操作CSGO游戏搬砖项目不&#xff1f; 这个项目虽是稳赚项目&#xff0c;但也有行情好和行情不好的时候&#xff0c;平台的大中小各种活动的举办&#xff0c;都会对我们的项目造成一定影响。行情的上下波动势必然会影响卡价的波动&#xff0c;影响选品的快慢&a…...

MuLogin浏览器如何在一台设备上安全登录和管理多个LinkedIn账户?

一、LinkedIn多个账户的用处 LinkedIn作为世界上最大的专业人士社交平台&#xff0c;具有许多有用的功能&#xff0c;对于个人和企业来说都非常重要。以下是多个LinkedIn账户的一些典型用途&#xff1a; 1. 分行业账户&#xff1a;如果您在不同的行业从事职业活动&#xff0c…...

STM32_project:led_beep

代码&#xff1a; 主要部分&#xff1a; #include "stm32f10x.h" // Device header #include "delay.h"// 给蜂鸣器IO口输出低电平&#xff0c;响&#xff0c;高&#xff0c;不向。 //int main (void) //{ // // 开启时钟 // RC…...

[go 反射] 入门

[go 反射] 入门 首先认识go 反射的两大概念&#xff0c;反射之路少不了他们 reflect.Type(接口)获取类型&#xff0c;和列名就找它reflect.Value(结构体)获取值&#xff0c;设置值找它 [tips] 通常是用这两者手底下的方法&#xff0c;reflect.Value结构体中有什么自行查看 …...

【计算机网络】数据链路层-MAC和ARP协议

文章目录 1. 认识以太网2. MAC协议MAC帧的格式MAC地址和IP地址的区别MTU 3. 局域网通信原理碰撞检测和避免 4. ARP协议ARP数据报的格式ARP缓存 1. 认识以太网 网络层解决的是跨网络点到点传输的问题&#xff0c;数据链路层解决的是同一网络中的通信。 数据链路层负责在同一局域…...

本周三商店更新:多款套装下线,四款升级武器带异色皮肤返厂

本周三将迎来26.2版本更新与11商店大更新&#xff0c;版本更新可点击26.2版本更新公告进行查看&#xff0c;这里不一一赘述了&#xff0c;下面大概罗列一下商店更新&#xff0c;有皮肤下架&#xff0c;大家还能趁最后时间入手&#xff0c;最重要的是四款升级武器返厂咯。 危险玩…...

WindowsServer2019-搭建FTP服务器

这里写自定义目录标题 一、基础配置IP地址安装FTP服务检查连通性Windows10连接FTP服务 二、了解和使用FTP具体模块及其配置1、FTP IP地址和域限制2、FTP SSL设置3、FTP当前会话4、FTP防火墙5、FTP目录浏览6、FTP请求筛选7、FTP日志8、FTP身份验证9、FTP授权规则10、FTP消息11、…...

国际阿里云服务器买哪种好用点?

在当时数字化年代&#xff0c;云核算已经成为了企业进行事务运营和数据存储的重要东西。而阿里云作为我国最大的云核算服务供给商&#xff0c;其服务器产品线也适当丰厚。那么&#xff0c;对于用户来说&#xff0c;阿里云服务器买哪种好用点呢&#xff1f;这需求依据个人和企业…...

2023NOIP A层联测25 总结

T1 让你构造 40 40 40\times40 4040 的只含 r,y,x 的矩阵&#xff0c;含有 r y x ryx ryx 的个数恰好为 n n n&#xff0c; n ≤ 2222 n\le2222 n≤2222。看完题后就开始想构造&#xff0c;一开始想构造 3 ∗ 3 3*3 3∗3, 5 ∗ 5 5*5 5∗5 的单位矩阵的&#xff0c;但是始…...

Thread类的基本操作(JAVA多线程)

线程是操作系统中的概念&#xff0c;操作系统内核实现了线程这样的机制&#xff0c;并提供了一些API供外部使用。 JAVA中 Thread类 将系统提供的API又近一步进行了抽象和封装&#xff0c;所以如果想要使用多线程就离不开 Thread 这个类。 线程的创建(Thread类) 在JAVA中 创建…...

Redis 的三种部署模式

提前叠个 buff&#xff1a;这个文章不涉及图&#xff08;画起来比较麻烦&#xff09;&#xff0c;只是记录我的胡思乱想。 redis 从单点 -> 集群总共有三个部署模式&#xff1a;单机模式&#xff0c;主从模式&#xff0c;哨兵模式&#xff0c;集群模式 单机模式 新手入门模…...

【ArcGIS Pro二次开发】(73):使用NPOI库操作Excel

NPOI是一个开源的C#读写Excel、WORD等微软OLE2组件文档的项目。 NPOI可以在没有安装Office的情况下对Word或Excel文档进行读写操作。 相较于之前使用的Microsoft.Office.Interop.Excel&#xff0c;已经感觉到的优势&#xff0c;一是读写速度较快&#xff0c;虽然小数据量的读…...

python获取电脑所连接的wifi密码

电脑连接wifi后&#xff0c;很难直观地看到当前连接wifi的密码&#xff0c;需要借助命令行公管局才可以查看到相关信息。 CMD命令 查看所有已保存的wifi配置信息 netsh wlan show profiles查看某一个wifi的详细信息&#xff0c;需要输入wifi名称来查询 netsh wlan show pro…...

动态壁纸软件Live Wallpaper HD mac中文版功能特色

Live Wallpaper HD mac提供了一系列美丽的主题场景&#xff0c;将为您的桌面增添活力。从城市景观、日落到遥远的星系&#xff0c;每个屏幕都有特别的触感&#xff0c;可以定制您的天气小部件和时钟样式&#xff0c;并使用您喜爱的图片创建您自己的个性化壁纸。 Living Wallpap…...

Spring Boot 配置主从数据库实现读写分离

一、前言 现在的 Web 应用大都是读多写少。除了缓存以外还可以通过数据库 “主从复制” 架构&#xff0c;把读请求路由到从数据库节点上&#xff0c;实现读写分离&#xff0c;从而大大提高应用的吞吐量。 通常&#xff0c;我们在 Spring Boot 中只会用到一个数据源&#xff0…...