首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >EPOLL原理详解

EPOLL原理详解

原创
作者头像
大发明家
发布于 2021-12-17 14:32:28
发布于 2021-12-17 14:32:28
2.3K0
举报
文章被收录于专栏:技术博客文章技术博客文章

一、内核接收数据流程

1.网卡发现 MAC 地址符合,就将包收进来;发现 IP 地址符合,根据 IP 头中协议项,知道上一层是 TCP 协议;

2.DMA把TCP数据包copy到内核缓冲区;

3.触发CPU中断,中断程序摘除TCP头通过socket五要素(源IP/PORT、目的IP/PORT、协议)找到对应的socket文件,并把原始二进制数据报copy到socket接收缓冲区;

4.中断程序唤醒被阻塞的内核线程;

5.内核线程切换到用户线程把数据从socket接口缓冲区copy到应用内存;

二、中断处理流程

中断处理.png

I/O发出的信号的异常代码,拿到异常代码之后,CPU就会触发异常处理的流程。计算机在内存里会保存中断向量表,用来存放不同的异常代码对应的异常处理程序所在的地址。

CPU在拿到了异常码之后,会先把当前的程序执行的现场,保存到程序栈里面,然后根据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。

异常处理程序结束之后返回到原来指令执行的位置继续执行;

三、阻塞不占用 cpu

网卡何时接收到数据是依赖发送方和传输路径的,这个延迟通常都很高,是毫秒(ms)级别的。而应用程序处理数据是纳秒(ns)级别的。也就是说整个过程中,内核态等待数据,处理协议栈是个相对很慢的过程。这么长的时间里,用户态的进程是无事可做的,因此用到了“阻塞(挂起)”。

阻塞是进程调度的关键一环,指的是进程在等待某事件发生之前的等待状态。请看下表,在 Linux 中,进程状态大致有 7 种(在

include/linux/sched.h 中有更多状态):

从说明中可以发现,“可运行状态”会占用 CPU 资源,另外创建和销毁进程也需要占用 CPU 资源(内核)。重点是,当进程被"阻塞/挂起"时,是不会占用

CPU 资源的。

为了支持多任务,Linux 实现了进程调度的功能(CPU

时间片的调度)。而这个时间片的切换,只会在“可运行状态”的进程间进行。因此“阻塞/挂起”的进程是不占用 CPU 资源的。

四、工作队列和等待队列

工作队列和等待队列.png

工作队列:为了方便时间片的调度,所有“可运行状态”状态的进程组成的队列;

fd文件列表:内核打开的文件句柄,Linux一切皆文件,用户线程执行创建Socket时内核就会创建一个由文件系统管理的sock对象;

sock:socket内核中的数据结构,主要包含发送缓冲区、接收缓冲区、等待队列;

代码语言:txt
AI代码解释
复制
struct sock {
代码语言:txt
AI代码解释
复制
    __u32   daddr;  /* 外部IP地址   */
代码语言:txt
AI代码解释
复制
    __u32   rcv_saddr; /* 绑定的本地IP地址  */
代码语言:txt
AI代码解释
复制
    __u16   dport;  /* 目标端口   */
代码语言:txt
AI代码解释
复制
    __u16   sport;  /* 源端口    */
代码语言:txt
AI代码解释
复制
    unsigned short  family;  /* 地址簇   */
代码语言:txt
AI代码解释
复制
    int   rcvbuf;  /* 接受缓冲区长度(单位:字节) */
代码语言:txt
AI代码解释
复制
    struct sk_buff_head receive_queue; /* 接收包队列   */
代码语言:txt
AI代码解释
复制
    int   sndbuf;  /* 发送缓冲区长度(单位:字节)  */
代码语言:txt
AI代码解释
复制
    struct sk_buff_head write_queue; /* 包发送队列   */
代码语言:txt
AI代码解释
复制
    wait_queue_head_t *sleep;  /* 等待队列,通常指向socket的wait域 */
代码语言:txt
AI代码解释
复制
    ......
代码语言:txt
AI代码解释
复制
}

等待队列:等待当前socket的线程;

工作队列中线程执行到阻塞操作等待socket时,会从工作队列中移除,移动到该socket的等待队列中;当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。

五、BIO

代码语言:txt
AI代码解释
复制
public static void main(String[] args) throws IOException {
代码语言:txt
AI代码解释
复制
    ServerSocket serverSocket = new ServerSocket(9000);
代码语言:txt
AI代码解释
复制
    while (true) {
代码语言:txt
AI代码解释
复制
        // 没有连接-阻塞
代码语言:txt
AI代码解释
复制
        Socket socket = serverSocket.accept();
代码语言:txt
AI代码解释
复制
        byte[] bytes = new byte[1024];
代码语言:txt
AI代码解释
复制
        InputStream inputStream = socket.getInputStream();
代码语言:txt
AI代码解释
复制
        while (true) {
代码语言:txt
AI代码解释
复制
            // 没有数据-阻塞
代码语言:txt
AI代码解释
复制
            int read = inputStream.read(bytes);
代码语言:txt
AI代码解释
复制
            if (read != -1) {
代码语言:txt
AI代码解释
复制
                System.out.println(new String(bytes, 0, read));
代码语言:txt
AI代码解释
复制
            } else {
代码语言:txt
AI代码解释
复制
                break;
代码语言:txt
AI代码解释
复制
            }
代码语言:txt
AI代码解释
复制
        }
代码语言:txt
AI代码解释
复制
        socket.close();
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
}

BIO模式存在两个阻塞点,一个时accept阻塞等待客户端连接,一个是阻塞等待socket请求数据;简单跟以下源码就会发现

new ServerSocket(9000)最终通过

  • int newfd = socket0(stream, false /*v6 Only*/);调用Linux int socket(int domain, int type, int protocol);创建服务端socket;
  • bind0(nativefd, address, port, exclusiveBind);调用Linux int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);绑定端口;
  • listen0(nativefd, backlog);调用Linux int listen(int sockfd, int backlog);设置监听;

接下来serverSocket.accept()最终通过

  • newfd = accept0(nativefd, isaa);调用Linux int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);阻塞等待客户端连接,会创建一个socket用于跟该客户端进行通信,返回socket文件描述符fd;

最后inputStream.read(bytes);调用Linux `ssize_t recv(int sockfd, void *buf,

size_t len, int flags);`阻塞从socket读缓冲区读取客户端请求数据;

可以通过 man 命令查看Linux 系统调用方法具体描述;

通过传统BIO的操作方式可以看出一个请求必须要创建一个内核线程进行处理,recv只能监视单个socket,当并发比较高时就会消耗大量系统资源,也就是所谓的C10K问题;那么如何解决这个问题呢?后面出现的多路复用

select / poll / epoll思路都是使用一个线程来处理若干个连接(监视若干个socket),类似餐厅服务员的角色。

六、select

select 方案是用一个 fd_set 结构体来告诉内核同时监控多个socket,当其中有socket的状态发生变化或超时,则调用返回。之后应用使用

FD_ISSET 来逐个查看是哪个socket的状态发生了变化。

6.1 select原理

以下面select伪代码为例:

代码语言:txt
AI代码解释
复制
int s = socket(AF_INET, SOCK_STREAM, 0);  
代码语言:txt
AI代码解释
复制
bind(s, ...)
代码语言:txt
AI代码解释
复制
listen(s, ...)
代码语言:txt
AI代码解释
复制
//接受客户端连接
代码语言:txt
AI代码解释
复制
int c = accept(s, ...)
代码语言:txt
AI代码解释
复制
int readSet[] =  存放需要监听的socket文件描述符
代码语言:txt
AI代码解释
复制
while(1){
代码语言:txt
AI代码解释
复制
    int n = select(..., readSet, ...)
代码语言:txt
AI代码解释
复制
    for(int i=0; i < readSet.count; i++){
代码语言:txt
AI代码解释
复制
        if(FD_ISSET(readSet[i], ...)){
代码语言:txt
AI代码解释
复制
            //fds[i]的数据处理
代码语言:txt
AI代码解释
复制
        }
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
}
代码语言:txt
AI代码解释
复制
select 系统调用函数
代码语言:txt
AI代码解释
复制
# maxfd:表示的是待测试的描述符基数,它的值是待测试的最大描述符加 1
代码语言:txt
AI代码解释
复制
# readset:读描述符集合
代码语言:txt
AI代码解释
复制
# writeset:写描述符集合
代码语言:txt
AI代码解释
复制
# exceptset:错误描述符集合
代码语言:txt
AI代码解释
复制
# timeout:超时时间
代码语言:txt
AI代码解释
复制
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

上面示例代码中,先准备一个数组 readSet 存放着所有需要监视读事件的socket。然后调用select,如果 readSet

中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒线程。用户可以遍历

readSet,通过FD_ISSET (对这个数组进行检测,判断出对应socket的元素 readSetfd是 0 还是 1)判断具体哪个 socket

收到数据,然后做出处理。

6.2 select流程

select-1.png

线程 A 调用 select readSet 数组元素为sock1、sock2、sock3 ,此时3个socket 都没有数据可读,就把线程A

从工作队列中移除,并分别添加到3个sock 的等待队列中;

select-2.png

当3个sock中任意一个有数据可读时,中断程序都会把线程A

从所有sock等待队列中移除并重新加入工作队列,等待cpu时间片继续执行(即从select中返回)。但是此时线程A

并不知道哪个socket有数据,于是还要遍历readSet使用 FD_ISSET 来逐个查看是哪个socket的有数据可读。

6.3 select的缺点

1、每次调用select都需要将线程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fd_set列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。

2、由于只有一个字段记录关注和发生事件,每次调用之前要重新初始化 fd_set 结构体。

3、线程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。

七、poll

总结以下 select 的缺点就是句柄上限+重复初始化+逐个排查所有文件句柄状态效率不高。poll 主要解决 select

的前两个问题:通过改变跟内核交互的数据结构突破了文件描述符的限制,同时使用不同字段分别标注关注事件和发生事件,来避免重复初始化。

代码语言:txt
AI代码解释
复制
int poll(struct pollfd *fds, unsigned long nfds, int timeout); 
代码语言:txt
AI代码解释
复制
# fd:描述符 fd
代码语言:txt
AI代码解释
复制
# events:描述符上待检测的事件类型events,注意这里的 events 可以表示多个不同的事件,
代码语言:txt
AI代码解释
复制
#         具体的实现可以通过使用二进制掩码位操作来完成,例如,POLLIN 和 POLLOUT 可以表示读和写事件
代码语言:txt
AI代码解释
复制
# revents:
代码语言:txt
AI代码解释
复制
struct pollfd {
代码语言:txt
AI代码解释
复制
    int    fd;       /* file descriptor */    
代码语言:txt
AI代码解释
复制
    short  events;   /* events to look for */    
代码语言:txt
AI代码解释
复制
    short  revents;  /* events returned */ 
代码语言:txt
AI代码解释
复制
};

和 select 非常不同的地方在于:

  • poll 每次检测之后的结果不会修改原来的传入值,而是将结果保留在 revents 字段中,这样就不需要每次检测完都得重置待检测的描述字和感兴趣的事件。
  • 在 select 里面,文件描述符的个数已经随着 fd_set 的实现而固定,没有办法对此进行配置;而在 poll 函数里,可以控制 pollfd 结构的数组大小,这意味着可以突破原来 select 函数最大描述符的限制,在这种情况下,应用程序调用者需要分配 pollfd 数组并通知 poll 函数该数组的大小。

八、epoll

epoll 使用 eventpoll 作为中间层,线程不用加入在被监视的每一个 socket 阻塞队列中,也不用再遍历 socket

列表查看哪些有事件发生。

epoll提供了三个函数:

代码语言:txt
AI代码解释
复制
# 创建了一个 epoll 实例 epollevent
代码语言:txt
AI代码解释
复制
int epoll_create(int size);
代码语言:txt
AI代码解释
复制
# 往这个 epoll 实例增加或删除监控的事件
代码语言:txt
AI代码解释
复制
# epfd:epoll_create 创建的 epoll 实例句柄
代码语言:txt
AI代码解释
复制
# op:增加还是删除一个监控事件
代码语言:txt
AI代码解释
复制
# fd:注册的事件的文件描述符
代码语言:txt
AI代码解释
复制
# event:注册的事件类型,并且可以在这个结构体里设置用户需要的数据
代码语言:txt
AI代码解释
复制
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
代码语言:txt
AI代码解释
复制
# 类似之前的 poll 和 select 函数,调用者进程被挂起,在等待内核 I/O 事件的分发
代码语言:txt
AI代码解释
复制
# epfd:实例描述字,也就是 epoll 句柄
代码语言:txt
AI代码解释
复制
# events:用户空间需要处理的 I/O 事件,这是一个数组,数组的大小由 epoll_wait 的返回值决定,这个数组的每个元素都是一个需要待处理的 I/O 事件,其中 events 表示具体的事件类型,事件类型取值和 epoll_ctl 可设置的值一样,这个 epoll_event 结构体里的 data 值就是在 epoll_ctl 那里设置的 data,也就是用户空间和内核空间调用时需要的数据
代码语言:txt
AI代码解释
复制
# maxevents:大于 0 的整数,表示 epoll_wait 可以返回的最大事件值
代码语言:txt
AI代码解释
复制
# timeout:阻塞调用的超时值,如果这个值设置为 -1,表示不超时;如果设置为 0 则立即返回,即使没有任何 I/O 事件发生
代码语言:txt
AI代码解释
复制
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
8.1 代码示例
代码语言:txt
AI代码解释
复制
public class ServerConnect {
代码语言:txt
AI代码解释
复制
    private static final int BUF_SIZE = 1024;
代码语言:txt
AI代码解释
复制
    private static final int PORT = 8080;
代码语言:txt
AI代码解释
复制
    private static final int TIMEOUT = 3000;
代码语言:txt
AI代码解释
复制
    public static void main(String[] args) {
代码语言:txt
AI代码解释
复制
        selector();
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    public static void selector() {
代码语言:txt
AI代码解释
复制
        Selector selector = null;
代码语言:txt
AI代码解释
复制
        ServerSocketChannel ssc = null;
代码语言:txt
AI代码解释
复制
        try {
代码语言:txt
AI代码解释
复制
            // 打开一个Channel
代码语言:txt
AI代码解释
复制
            ssc = ServerSocketChannel.open();
代码语言:txt
AI代码解释
复制
            // 将Channel绑定端口
代码语言:txt
AI代码解释
复制
            ssc.socket().bind(new InetSocketAddress(PORT));
代码语言:txt
AI代码解释
复制
            // 设置Channel为非阻塞,如果设置为阻塞,其实和BIO差不多了。
代码语言:txt
AI代码解释
复制
            ssc.configureBlocking(false);
代码语言:txt
AI代码解释
复制
            // 打开一个Slectore
代码语言:txt
AI代码解释
复制
            selector = Selector.open();
代码语言:txt
AI代码解释
复制
            // 向selector中注册Channel和感兴趣的事件
代码语言:txt
AI代码解释
复制
            ssc.register(selector, SelectionKey.OP_ACCEPT);
代码语言:txt
AI代码解释
复制
            while (true) {
代码语言:txt
AI代码解释
复制
                // selector监听事件,select会被阻塞,直到selector监听的channel中有事件发生或者超时,会返回一个事件数量
代码语言:txt
AI代码解释
复制
                //TIMEOUT就是超时时间,selector初始化的时候会添加一个用于主动唤醒的pipe,待会源码分析会说
代码语言:txt
AI代码解释
复制
                if (selector.select(TIMEOUT) == 0) {
代码语言:txt
AI代码解释
复制
                    System.out.println("==");
代码语言:txt
AI代码解释
复制
                    continue;
代码语言:txt
AI代码解释
复制
                }
代码语言:txt
AI代码解释
复制
                /**
代码语言:txt
AI代码解释
复制
                 * SelectionKey的组成是selector和Channel
                 * 有事件发生的channel会被包装成selectionKey添加到selector的publicSelectedKeys属性中
                 * publicSelectedKeys是SelectionKey的Set集合
                 *下面这一部分遍历,就是遍历有事件的channel
                 */
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    }
                    if (key.isReadable()) {
                        handleRead(key);
                    }
                    if (key.isWritable() && key.isValid()) {
                        handleWrite(key);
                    }
                    if (key.isConnectable()) {
                        System.out.println("isConnectable = true");
                    }
                    //每次使用完,必须将该SelectionKey移除,否则会一直存储在publicSelectedKeys中
                    //下一次遍历又会重复处理
                    iter.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (selector != null) {
                    selector.close();
                }
                if (ssc != null) {
                    ssc.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
        SocketChannel sc = ssChannel.accept();
        sc.configureBlocking(false);
        sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUF_SIZE));
    }
    public static void handleRead(SelectionKey key) throws IOException {
        SocketChannel sc = (SocketChannel) key.channel();
        ByteBuffer buf = (ByteBuffer) key.attachment();
        long bytesRead = sc.read(buf);
        while (bytesRead > 0) {
            buf.flip();
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if (bytesRead == -1) {
            sc.close();
        }
    }
    public static void handleWrite(SelectionKey key) throws IOException {
        ByteBuffer buf = (ByteBuffer) key.attachment();
        buf.flip();
        SocketChannel sc = (SocketChannel) key.channel();
        while (buf.hasRemaining()) {
            sc.write(buf);
        }
        buf.compact();
    }
}

调用到内核的流程如下:

1、ssc = ServerSocketChannel.open() -> EPollSelectorProvider -> int

socket(int domain, int type, int protocol);

2、ssc.socket().bind(newInetSocketAddress(PORT)); -> int bind(int sockfd,

const struct sockaddr _addr, socklen_t addrlen);

3、getImpl().listen(backlog); -> int listen(int sockfd, int backlog);

4、ssc.configureBlocking(false); -> int fcntl(int fd, int cmd, ... /_ arg

*/ );

5、selector=Selector.open(); -> EPollSelectorImpl epollCreate() -> int

epoll_create(int size);

6、ssc.register(selector,SelectionKey.OP_ACCEPT); -> int epoll_ctl(int

epfd, int op, int fd, struct epoll_event *event);

while

7、selector.select(TIMEOUT) -> int epoll_wait(int epfd, struct epoll_event

*events, int maxevents, int timeout);

8.2 epoll流程

epoll.png

如图,eventpoll 主要包含3个结构

  • 监视队列:epoll_ctl添加或删除所要监听的socket。例如:图中通过epoll_ctl添加sock1、sock2的监视,内核会将eventpoll添加到这socket的等待队列中(图中虚线)。为了方便的添加和移除,还要便于搜索,以避免重复添加,使用红黑树结构进行存储(搜索、插入和删除时间复杂度都是O(log(N)),效率较好。
  • 等待队列:eventpoll也是一个文件句柄,因此也有等待队列,记录阻塞的线程,例如:图中线程A 执行了 epoll_wait 操作,被从工作队列中移除,然后被eventpoll 的等待队列引用;
  • 就绪队列:当某个socket有事件发生时,中断处理程序就会把该socket加入到就绪队列,同时唤醒eventpoll 阻塞队列中的线程,此时线程只需要遍历就绪队列就可以知道哪个socket有事件发生,例如:图中sock2有数据到来时,中断处理程序先把sock2 放入就绪队列中,然后唤醒等待队列中的线程A,这时线程A 被重新加入工作队列中,等到CPU时间片轮询到线程A时,遍历就绪队列中的socket进行处理。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
暂无评论
推荐阅读
iOS KVC和KVO
无论是在我们的今后的工作当中还是面试找工作当中,这两个知识点是十分重要的,有些同学们对这方面的知识还是不是很了解,概念模糊,这里我整理下相关的内容知识分享给大家。
conanma
2021/10/28
8960
kvo深入浅出举例
一,概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。 二,使用方法 系统框架已经支持KVO,所以程序员在使用的时候非常简单。 1. 注册,指定被观察者的属性    2. 实现回调方法 谁是观察者 这个回调方法就写在哪 3. 移除观察  最好在dealloc中写 三,实例: 假设一个场景,股票的价格显示在当前屏幕上,当股票价格更改的时候,实时显示更新其
用户1219438
2018/02/01
6380
面试驱动技术 - KVO && KVC
what?怎么跑出来一个NSKVONotifying_MNPerson?person的class 不是MNPerson 吗?
小蠢驴打代码
2019/03/15
1.2K0
KVC & KVO
        KVC和KVO看上去又是两个挺牛的单词简写,KVC是Key-Value Coding的简写,是键值编码的意思。KVO是Key-Value  Observing的简写,是键值观察的意思。那么我们能拿KVC和KVO干些什么事呢?这两个缩写单词不能否认听起来挺高端的样子。这两个方法都是runtime方法,我们先来介绍KVC。 1.KVC(Key-Value Coding)键值编码             为了测试我们建立两个测试类                   测试类一: 1 2
lizelu
2018/01/11
9030
KVC/KVO 本质
2. 若没有找到Set方法,会调用对象的类方法+ (BOOL)accessInstanceVariablesDirectly;此方法返回YES时(默认返回YES),会按照_key,_iskey,key,iskey的顺序搜索成员,然后赋值。
用户1941540
2019/02/15
6430
KVC/KVO 本质
程序员面试闪充 -- KVC&KVO
一、键值编码KVC kvc&kvo视频讲解 1、介绍 由于oc的语言特性,使得开发者根本不必进行任何操作就可以进行属性的动态读写,这种方式就是Key Value Coding(简称KVC)。 KVC的操作方法由NSKeyValueCoding协议提供,而NSObject就实现了这个协议,也就是说OC中几乎所有的对象都支持KVC操作,常用的KVC操作方法如下: 动态设置:setValue:属性值 forKey:属性名用于简单路径;setValue:属性值 forKeyPath:属性路径用于复合路径,例如Pe
谦谦君子修罗刀
2018/05/02
7840
[Objective-C] KVC 和 KVO
KVC是一种用间接方式访问类的属性的机制。比如你要给一个类中的属性赋值或者取值,可以直接通过类和点运算符实现,当然也可以使用KVC。不过对于私有属性,点运算符就不起作用,因为私有属性不暴露给调用者,不过使用KVC却依然可以实现对私有属性的读写。
wOw
2018/09/18
7450
ios KVO及实现原理
概述 KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
赵哥窟
2018/09/13
7010
iOS中KVC与KVO的应用解析 原
KVC键值编码是Object-C为我们提供的一种对成员变量赋值的方法。在探讨其方法之前,我们先来看一个小例子:
珲少
2018/08/15
3700
编码篇-精析OC史诗级技术之KVC
不得不承认KVC在开发过程中是神器一般的存在。如果正确灵活使用kvc,会使得整个开发过程轻松很多。简单而强大。
進无尽
2018/09/12
1.4K0
编码篇-精析OC史诗级技术之KVC
KVC 使用方法详解及底层实现你要知道的KVC、KVO、Delegate、Notification都在这里
你要知道的KVC、KVO、Delegate、Notification都在这里 转载请注明出处 https://cloud.tencent.com/developer/user/1605429 本系列文章主要通过讲解KVC、KVO、Delegate、Notification的使用方法,来探讨KVO、Delegate、Notification的区别以及相关使用场景,本系列文章将分一下几篇文章进行讲解,读者可按需查阅。 KVC 使用方法详解及底层实现 KVO 正确使用姿势进阶及底层实现 Protocol与Dele
WWWWDotPNG
2018/04/10
1.3K0
KVC 使用方法详解及底层实现你要知道的KVC、KVO、Delegate、Notification都在这里
iOS开发·KVO用法,原理与底层实现: runtime模拟实现KVO监听机制(Blcok及Delgate方式)
KVO 是 Objective-C 对 观察者模式(Observer Pattern)的实现。当被观察对象的某个属性发生更改时,观察者对象会获得通知。有意思的是,你不需要给被观察的对象添加任何额外代码,就能使用 KVO 。这是怎么做到的?
陈满iOS
2018/09/10
2.2K0
iOS开发·KVO用法,原理与底层实现: runtime模拟实现KVO监听机制(Blcok及Delgate方式)
# iOS中的KVO底层实现
KVO是Key-Value-Observer的缩写,使用的是观察者模式。底层实现机制都是isa-swizzing,就是在底层调用object_setClass函数,将对象的isa指向的Class偷偷换掉。
Haley_Wong
2019/03/29
1.3K0
iOS KVO实现原理及使用
KVO(key-value observe)是在KVC的基础上实现的一种用于监听属性变化的设计模式;如果对某个类的某个属性设置了KVO,那么当这个属性发生变化时,就会触发监听方法,从而知道属性变化了。如果本类一个属性的改变会影响到其他多个属性的变化,我们也会经常自己重写这个属性的set方法,用来监听他的变化,但是如果不是本类的属性,我们就没办法重写其set方法了,这个时候KVO就可以上场了,其实KVO本质上也是重写set方法,而整个过程依赖于runtime才能实现。
iOSSir
2023/03/19
5530
iOS KVO实现原理及使用
iOS进阶_KVC(&KVC赋值取值过程分析&KVC自定义&异常处理)
在WTPerson.m中我们让accessInstanceVariablesDirectly返回NO,则程序直接崩溃。
编程怪才-凌雨画
2020/09/18
8920
OC底层探索20-KVO中的isa-swizzling分析OC底层探索20-KVO中的isa-swizzling分析
猜测NSKVONotifying_LGPerson这个类是系统动态进行添加,所以需要分析它的进行关系。获取LGPerson的子类
用户8893176
2021/08/09
6680
OC底层探索20-KVO中的isa-swizzling分析OC底层探索20-KVO中的isa-swizzling分析
iOS kvc
今天,他们遇到了kvc第二次去学习它,在网上看了很多博客,这似乎不符合我的口味,为了提取一些以下的。总结自己的。
全栈程序员站长
2022/07/06
2590
iOS 知识点回顾(一)
温故而知新 目录 一个NSObject对象占用多少内存? 对象的isa指针指向哪里? OC的类信息存放在哪里? iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) KVC Category +load方法和+initialize方法 Block _ _weak 1. 一个NSObject对象占用多少内存? 系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInst
且行且珍惜_iOS
2019/12/30
6540
iOS - 关于 KVC 的一些总结
我们可以使用setter方法为currentBalance属性赋值,这是直接的,但缺乏灵活性。
师大小海腾
2020/04/16
2K0
iOS_KVC:Key-Value Coding-1(使用)
以上2个方法如果Key值不对(即该属性不存在),则会触发valueForUndefinedKey:方法,默认会抛出NSUndefinedKeyException异常,导致crash。
mikimo
2022/07/20
4190
iOS_KVC:Key-Value Coding-1(使用)
相关推荐
iOS KVC和KVO
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 一、内核接收数据流程
  • 二、中断处理流程
  • 三、阻塞不占用 cpu
  • 四、工作队列和等待队列
  • 五、BIO
  • 六、select
  • 七、poll
  • 八、epoll
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档