• 10 netty服务端启动流程


    前置知识

    selector介绍

    多线程模式下selector的使用以及IO模型概念的区分

    IDEA断点调试说明

    1 服务端启动步骤概述

    netty的启动关键逻辑可以用以下代码表示

    package CodeAnalyze;
    
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    public class Example {
        public static void main(String[] args) {
            try {
                 // 下面三行都是原生的NIO的API
                 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 服务器监听socket
                 serverSocketChannel.configureBlocking(false);
                 Selector selector = Selector.open();
                 NioServerSocketChannel attachment = new NioServerSocketChannel();   // netty提供
    
                 SelectionKey sscKey = serverSocketChannel.register(selector,0,attachment);
    
                 // 绑定端口
                 serverSocketChannel.bind(new InetSocketAddress(8080));
                 // 关联事件
                 sscKey.interestOps(SelectionKey.OP_ACCEPT);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    上述代码中服务端启动可以划分为以下3个步骤

    • step1:创建ServerSocketChannel对象
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 服务器监听socket
    serverSocketChannel.configureBlocking(false);
    
    • step2:将ServerSocketChannel注册到selector选择器,从而方便后续监控channel的事件
    NioServerSocketChannel attachment = new NioServerSocketChannel();   // netty提供
    Selector selector = Selector.open();
    SelectionKey sscKey = serverSocketChannel.register(selector,0,attachment);
    
    • step3:绑定端口并关联accept事件
    // 绑定端口
    serverSocketChannel.bind(new InetSocketAddress(8080));
    // 关联事件
    sscKey.interestOps(SelectionKey.OP_ACCEPT);
    

    2 netty服务端启动代码

    package CodeAnalyze;
    
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import io.netty.handler.logging.LoggingHandler;
    
    public class TestSourceServer {
        public static void main(String[] args) {
            new ServerBootstrap()
                    .group(new NioEventLoopGroup())
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LoggingHandler());
                        }
                    }).bind(8080);      // 入口1
        }
    }
    

    bind源码追踪

    io.netty.bootstrap.AbstractBootstrap.bind(int inetPort) =>
    io.netty.bootstrap.AbstractBootstrap.doBind(final SocketAddress localAddress
    
        public ChannelFuture bind(int inetPort) {            // 入口1源码
            return bind(new InetSocketAddress(inetPort));    // 入口2
        }
    ---------------------------------------------------------------------------------------
        public ChannelFuture bind(SocketAddress localAddress) { //入口2源码
            validate();  // 检测group和channel的工厂类是否为空,为空则抛出异常
            return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress")); // 入口3
        }
    ---------------------------------------------------------------------------------------
        private ChannelFuture doBind(final SocketAddress localAddress) { // 入口3源码
            final ChannelFuture regFuture = initAndRegister();
            final Channel channel = regFuture.channel();
            if (regFuture.cause() != null) {
                return regFuture;
            }
    
            if (regFuture.isDone()) {
                // At this point we know that the registration was complete and successful.
                ChannelPromise promise = channel.newPromise();
                doBind0(regFuture, channel, localAddress, promise);
                return promise;
            } else {
                // Registration future is almost always fulfilled already, but just in case it's not.
                final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
                regFuture.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        Throwable cause = future.cause();
                        if (cause != null) {
                            // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                            // IllegalStateException once we try to access the EventLoop of the Channel.
                            promise.setFailure(cause);
                        } else {
                            // Registration was successful, so set the correct executor to use.
                            // See https://github.com/netty/netty/issues/2586
                            promise.registered();
    
                            doBind0(regFuture, channel, localAddress, promise);
                        }
                    }
                });
                return promise;
            }
        }
    
    
    

    总结:基于bind的源码追踪可以定位到doBind方法,该方法实现了服务端的3个启动步骤

    2-1 dobind()源码分析

    2-1-1 概述

        private ChannelFuture doBind(final SocketAddress localAddress) { 
            /* 
               创建并初始化NioServerSocketChannel,并且将该channel注册到一个EventLoop中,需要注意的是
               这里注册即绑定selector的操作是通过EventLoop提交线程任务完成,对于这种类型的异步操作,这里
               initAndRegister()会返回一个Future对象用于感知注册的结果
            */
            final ChannelFuture regFuture = initAndRegister();
            final Channel channel = regFuture.channel();
            if (regFuture.cause() != null) {
                return regFuture;
            }
            if (regFuture.isDone()) {   // channel注册已经完成(绝大部分情况),直接进行绑定
                ChannelPromise promise = channel.newPromise();
                doBind0(regFuture, channel, localAddress, promise);
                return promise;
            } else {                    // channel注册还未完成
                final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
                // 当前注册还么完成通过在future中添加回调函数确保
                // EventLoop中线程执行完任务后调用该回调函数完成绑定操作
                regFuture.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        Throwable cause = future.cause();
                        if (cause != null) {
                            promise.setFailure(cause);
                        } else {
                            promise.registered();
                            doBind0(regFuture, channel, localAddress, promise);
                        }
                    }
                });
                return promise;
            }
        }
    

    2-1-2 initAndRegister()

    initAndRegister()源码
        final ChannelFuture initAndRegister() {
            Channel channel = null;
            try {
                channel = channelFactory.newChannel();  //实例化NioServerSocketChannel
                init(channel);          // 初始化channel(在pipeline中添加handler)
            } catch (Throwable t) {
                if (channel != null) {
                    channel.unsafe().closeForcibly();
                    return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
                }
                return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
            }
            // 将创建的channel实例对象与EventLoopGroup中的一个EventLoop进行绑定
            ChannelFuture regFuture = config().group().register(channel);
            if (regFuture.cause() != null) {
                if (channel.isRegistered()) {
                    channel.close();
                } else {
                    channel.unsafe().closeForcibly();
                }
            }
            return regFuture;
        }
    

    initAndRegister方法包含以下三个核心功能

    // a)创建NioServerSocketChannel实例
    channel = channelFactory.newChannel();
    // b) 为channel的pipeline添加初始化handler,注意该handler只被调用一次,并且当前为调用
    init(channel);
    // c) 将NioServerSocketChanenl实例注册NioEventLoopGroup的selector中
    ChannelFuture regFuture = config().group().register(channel);
    

    a) 创建channel对象
    通过channelFactory创建NioServerSocketChannel的实例
    


    上面截图表明:方法initAndRegister()在执行过程中通过反射的方式利用NioServerSocketChannel的构造器实例化NioServerSocketChannel对象

    进一步分析netty的NioServerSocketChannel的构造源码

    public NioServerSocketChannel() {
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
    }
    ---------------------------------------------------------------------------
    private static ServerSocketChannel newSocket(SelectorProvider provider) {
        try {
            return provider.openServerSocketChannel();  // 
        } catch (IOException e) {
            throw new ChannelException(
                "Failed to open a server socket.", e);
        }
    }
    

    对比JDK中ServerSocketChannel的open方法源码

        public static ServerSocketChannel open() throws IOException {
            return SelectorProvider.provider().openServerSocketChannel();
        }
    

    总结:对比上述两个代码段可以判定在实例化netty的NioServerSocketChannel的过程中也实例化了JDK自带的ServerSocketChannel


    b) 初始化channel对象
    在NioServerSocketChannel的pipeline添加初始化handler
    

    init()方法源码

     @Override
        void init(Channel channel) throws Exception {
            final Map<ChannelOption<?>, Object> options = options0();
            synchronized (options) {
                setChannelOptions(channel, options, logger);
            }
    
            final Map<AttributeKey<?>, Object> attrs = attrs0();
            synchronized (attrs) {
                for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                    @SuppressWarnings("unchecked")
                    AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
                    channel.attr(key).set(e.getValue());
                }
            }
    
            ChannelPipeline p = channel.pipeline();
    
            final EventLoopGroup currentChildGroup = childGroup;
            final ChannelHandler currentChildHandler = childHandler;
            final Entry<ChannelOption<?>, Object>[] currentChildOptions;
            final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
            synchronized (childOptions) {
                currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0));
            }
            synchronized (childAttrs) {
                currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));
            }
    		// 为NioServerSocketChannel添加handler,该handler后续只会被调用一次
            // 用于初始化NioServerSocketChannel!!!!!!
            p.addLast(new ChannelInitializer<Channel>() {
                @Override
                public void initChannel(final Channel ch) throws Exception {
                    final ChannelPipeline pipeline = ch.pipeline();
                    ChannelHandler handler = config.handler();
                    if (handler != null) {
                        pipeline.addLast(handler);
                    }
    
                    ch.eventLoop().execute(new Runnable() {
                        @Override
                        public void run() {
                            pipeline.addLast(new ServerBootstrapAcceptor(
                                    ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                        }
                    });
                }
            });
        }
    

    c) 完成channel对象的注册

    ChannelFuture regFuture = config().group().register(channel)的调用链如下所示:

    io.netty.channel.MultithreadEventLoopGroup.register =>
    io.netty.channel.SingleThreadEventLoop.register(Channel channel) =>
    io.netty.channel.SingleThreadEventLoop.register(final ChannelPromise promise)=>
    io.netty.channel.AbstractChannel.register(EventLoop eventLoop, final ChannelPromise promise)=>
    io.netty.channel.AbstractChannel.register0=>
    io.netty.channel.nio.doRegister()
    

    ==============================================================================

    分析调用链可发现:注册的基本逻辑是先从NioEventLoopGroup中选择一个NioEventLoop,然后通过选择的NioEventLoop提交任务完成channel的注册(绑定当前eventLoop的selector)

    ==============================================================================

        @Override
        public ChannelFuture register(Channel channel) {
            return next().register(channel);
        }
        ------------------------------------------------------------------------
        @Override
        public ChannelFuture register(Channel channel) {
            return register(new DefaultChannelPromise(channel, this));
        }
    -------------------------------------------------------------------------------
            @Override
        public ChannelFuture register(final ChannelPromise promise) {
            ObjectUtil.checkNotNull(promise, "promise");
            promise.channel().unsafe().register(this, promise);
            return promise;
        }
    -------------------------------------------------------------------------------
         @Override
         public final void register(EventLoop eventLoop, final ChannelPromise promise) {
                if (eventLoop == null) {
                    throw new NullPointerException("eventLoop");
                }
                if (isRegistered()) {
                    promise.setFailure(new IllegalStateException("registered to an event loop already"));
                    return;
                }
                if (!isCompatible(eventLoop)) {
                    promise.setFailure(
                            new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
                    return;
                }
    
                AbstractChannel.this.eventLoop = eventLoop;
    			// 下面的if-else通过检查当前线程是否时NIO线程,确保register0方法在NIO线程中执行!!
                if (eventLoop.inEventLoop()) {
                    register0(promise);
                } else {
                    try {
                        eventLoop.execute(new Runnable() {
                            @Override
                            public void run() {
                                register0(promise);
                            }
                        });
                    } catch (Throwable t) {
                        logger.warn(
                                "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                                AbstractChannel.this, t);
                        closeForcibly();
                        closeFuture.setClosed();
                        safeSetFailure(promise, t);
                    }
                }
            }
    =====================================register0=================================================
    	private void register0(ChannelPromise promise) {
                try {
                    // check if the channel is still open as it could be closed in the mean time when the register
                    // call was outside of the eventLoop
                    if (!promise.setUncancellable() || !ensureOpen(promise)) {
                        return;
                    }
                    boolean firstRegistration = neverRegistered;
                    doRegister();
                    neverRegistered = false;
                    registered = true;
                    // 调用之前添加的初始化handler
                    pipeline.invokeHandlerAddedIfNeeded();
    				
                    // 设置promise容器中为success,之后会调用promise容器关联的回调函数进行bind
                    safeSetSuccess(promise);
                    // 触发channelRegister方法调用
                    pipeline.fireChannelRegistered();
                    if (isActive()) {
                        if (firstRegistration) {
                            pipeline.fireChannelActive();
                        } else if (config().isAutoRead()) {
                            beginRead();
                        }
                    }
                } catch (Throwable t) {
                    closeForcibly();
                    closeFuture.setClosed();
                    safeSetFailure(promise, t);
                }
            }
    ----------------------------doregister----------------------------------------
        @Override
        protected void doRegister() throws Exception {
            boolean selected = false;
            for (;;) {
                try {             
                    selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                    return;
                } catch (CancelledKeyException e) {
                    if (!selected) {
                        eventLoop().selectNow();
                        selected = true;
                    } else {
                        throw e;
                    }
                }
            }
        }
    

    总结:register过程中首先从EventLoopGroup中选择1个EventLoop,由EventLoop中的线程池提交任务去执行doRegister()方法完成注册,所谓注册就是让EventLoop中的selector关注ServerSocketChannel的事件

    // 主线程和NIO线程配合的关键代码
    if (eventLoop.inEventLoop())   
    // 源码中eventLoop通过判断当前线程是否是evenLoop中的线程对象来确定是否需要提交任务。
    // 不是,则执行eventLoop.execute(()->{ register0(promise);}),该代码让选择eventLoop提交任务。
    

    register0的工作:

    • doRegister()方法将ServerSocketChannel注册到当前eventLoop中
    selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
    // javaChannel()就是JDK原生的ServerSocketChannel,eventLoop()
    // unwrappedSelector()是当前EventLoop的selector
    // this指的就是已经创建好的NioServerSocketChannel(Netty提供)
    // doRegister()方法中的这行代码与(1 服务端启动步骤概述 step2)本质是相同的。
    
    • 执行handler初始化Channel
    pipeline.invokeHandlerAddedIfNeeded();    // 此处代码执行之前添加在pipeline中的初始化handler
    

    问题:为什么在“初始化channel对象阶段b”添加的handler到“channel对象注册阶段c“才进行执行?

    原因:在阶段b,channel对象还没有绑定eventLoop,但是所添加的handler中需要EventLoop提交任务(见下面代码片段),因此必须等到阶段c中完成EventLoop的绑定之后再调用handler中的代码

    			ch.eventLoop().execute(new Runnable() {
                        @Override
                        public void run() {
                            pipeline.addLast(new ServerBootstrapAcceptor(
                                    ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                        }
    /* 
    	ServerBootstrapAcceptor是1个handler用于ServerSocketChannel中accept事件
    	发生时进行连接建立的处理。
    */
    
    initAndRegister方法总结

    ​ 该方法总体的执行需要主线程和EventLoop中的线程配合完成

    * 主线程创建并初始化channel对象并从EventLoop中选出channel绑定的EventLoop
    * 实际注册工作是EventLoop中线程池提交任务完成的
    

    注册的结果通过future容器(regFuture)作为该方法的返回值返回

    注册成功设置位置如下:Nio线程执行register0()时设置promise容器中为success

     safeSetSuccess(promise);
    

    在dobind()源码中:regFuture就是上述代码的promise

     /* 
        通过regFuture(promise容器)感知NIO线程中register是否完成,dobind方法中如果发现注册还没有完成,此	 时会为future添加回调函数ChannelFutureListener()来确保执行doBind0()中完成端口绑定。
     */
     regFuture.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        Throwable cause = future.cause();
                        if (cause != null) {
                            promise.setFailure(cause);
                        } else {
                            promise.registered();
                            doBind0(regFuture, channel, localAddress, promise);
                        }
                    }
                });
    

    2-1-3 doBind0()

    调用链

    io.netty.bootstrapAbstractBootstrap.doBind0()
    =>
    io.netty.channel.AbstractChannel.bind(SocketAddress localAddress, ChannelPromise promise) =>
    io.netty.channel.DefaultChannelPipeline.bind(SocketAddress localAddress, ChannelPromise promise)
    =>
    io.netty.channel.AbstractChannelHandlerContext.bind(SocketAddress localAddress) 
    =>
    io.netty.channel.AbstractChannelHandlerContext.invokeBind(SocketAddress localAddress, ChannelPromise promise)
    =>
    io.netty.channel.DefaultChannelPipeline.bind(localAddress, promise);
    =>
    io.netty.channel.AbstractChannel.bind(final SocketAddress localAddress, final ChannelPromise promise)
    =>
        
    

    AbstractChannelHandlerContext中bind相关源码

    @Override
        public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
            if (localAddress == null) {
                throw new NullPointerException("localAddress");
            }
            if (isNotValidPromise(promise, false)) {
                // cancelled
                return promise;
            }
    
            final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND);
            EventExecutor executor = next.executor();
            /*
               下面的if-else确保后续的invokeBind()操作是由nio线程完成
               if 检测当前线程是否是EventLoop的线程
                   invokeBind
               else
               	   线程池提交任务及逆行invokeBind
               返回promise
            */
            if (executor.inEventLoop()) {      
                next.invokeBind(localAddress, promise);
            } else {
                safeExecute(executor, new Runnable() {
                    @Override
                    public void run() {
                        next.invokeBind(localAddress, promise);
                    }
                }, promise, null);
            }
            return promise;
        }
    
        private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
            if (invokeHandler()) {
                try {
                    ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
                } catch (Throwable t) {
                    notifyOutboundHandlerException(t, promise);
                }
            } else {
                bind(localAddress, promise);
            }
        }
        }
    

    注意:上述源码中涉及线程的切换,代码中保证invokeBind()方法调用是由EventLoop的线程执行的。

    AbstractChannel.bind源码如下

            @Override
            public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
                assertEventLoop();
    
                if (!promise.setUncancellable() || !ensureOpen(promise)) {
                    return;
                }
    
                // See: https://github.com/netty/netty/issues/576
                if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
                    localAddress instanceof InetSocketAddress &&
                    !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
                    !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
                    // Warn a user about the fact that a non-root user can't receive a
                    // broadcast packet on *nix if the socket is bound on non-wildcard address.
                    logger.warn(
                            "A non-root user can't receive a broadcast packet if the socket " +
                            "is not bound to a wildcard address; binding to a non-wildcard " +
                            "address (" + localAddress + ") anyway as requested.");
                }
    
                boolean wasActive = isActive();
                try {
                    doBind(localAddress);     // 1)完成底层ServerSocketChannel和端口的绑定
                } catch (Throwable t) {
                    safeSetFailure(promise, t);
                    closeIfClosed();
                    return;
                }
    			
                // 2)完成端口绑定后触发pipeline中handler的active事件
                if (!wasActive && isActive()) {
                    invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            pipeline.fireChannelActive(); 
                            // fire channel active(调用所有handler的channelActive方法
                        }
                    });
                }
    
                safeSetSuccess(promise);
            }
    

    上述源码中的注意点:

    1) doBind方法完成底层ServerSocketChannel和端口的绑定

     protected void doBind(SocketAddress localAddress) throws Exception {
            if (PlatformDependent.javaVersion() >= 7) {
                javaChannel().bind(localAddress, config.getBacklog());
            } else {
                javaChannel().socket().bind(localAddress, config.getBacklog());
            }
        }
    

    2) 完成所有工作后,会触发当前NioServerSocketChannel的pipeline中所有handler的active事件

    动机:当前的NioServerSocketChannel的pipeline结构是 head <= accept handler <= tail,

    其中head和tail是pipeline默认的handler。三个handler中,head handler的channelActive让key关注accept事件

    sscKey.interestOps(SelectionKey.OP_ACCEPT); // 默认的head handler的channelActive会完成该工作
    

    3 总结

    3-1 设计思想


    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false);
    Selector selector = Selector.open();
    SelectionKey sscKey = serverSocketChannel.register(selector,0,null);
    serverSocketChannel.bind(new InetSocketAddress(8080));
    sscKey.interestOps(SelectionKey.OP_ACCEPT);
    

    上述原始的服务端启动流程在netty中显得异常复杂度,主要原因在于:

    代码结构

    • netty采用EventLoop对原始的JDK的selector进行封装
      • 动机:充分利用多线程优势
    • 采用NioServerSocketChannel封装了ServerSocketChannel
      • 动机:netty基于责任链模式设计的channel让数据的处理更加方便直观

    执行逻辑:执行过程中,无论是注册还是绑定端口都是通过channel绑定的EventLoop取提交任务实现,线程间的

    协同使得代码不是特别直观。但框架的复杂性保证了使用者的简便性。


    3-2 线程间的同步

    Netty中线程间的写法

    promise = func1(参数,...,promise)
    
    // 情况1:通过promise判断func1的逻辑是否执行完整,如果已经执行完整,直接执行后续逻辑
    if(promise.isSuccess){     
    	func2()
    }else{
    // 情况2:通过promise判断func1的逻辑是否执行完整,如果未执行完整,添加回调函数,当执行完成后通过回调函数调用确保后续逻辑执行
    	promise.addListener(()->{func1})
    }
    

    说明:上述伪代码逻辑在netty中经常出现,首先是异步方法func1执行(涉及其他线程),我们可以通过方法参数中是否有promise类型对象来判断当前方法是否涉及异步调用。然后通过返回的promise内容的判断其他线程是否执行完成(其他线程会将结果放到promise中),确保func2的逻辑一定实在func1后面执行。

    • 上述逻辑在Netty启动源码中体现:端口的绑定必须在channel注册后才能执行,这种严格顺序关系就是通过上述伪代码的思路实现。本质上还是多线程思想中的保护性暂停模式。这里的promise是充当了线程间同步的协调者

    • 上述逻辑func2()由于线程执行的不确定性,可能是线程1执行,也可能是线程2执行,如果我们希望确保某个目标线程执行某些操作,那么可以判断当前线程是否是目标线程。netty源码中多次出现inEventLoop()方法调用就是确保某些操作必须由EventLoop中的线程完成

    参考资料

    Netty基础课程

  • 相关阅读:
    如何增加按钮的点击间隔时间
    如何增加button的点击范围
    定时器Timer的使用
    NSCache
    GCD和NSOperation的区别
    NSOperation实现线程间通信
    NSOperation添加操作依赖和监听
    自定义NSOperation
    NSOperation的多线程
    单例的实现(完整版代码)
  • 原文地址:https://www.cnblogs.com/kfcuj/p/16160339.html
Copyright © 2020-2023  润新知