RunLoop

作者: 叫我小黑 | 来源:发表于2019-06-28 11:59 被阅读0次

    什么是RunLoop

    顾名思义,运行循环,在程序运行过程中循环做一些事情。如果没有RunLoop,程序执行完毕就会立即退出,如果有了RunLoop,程序并不会马上退出,而是保持运行状态。

    RunLoop基本作用:

    • 保持程序持续运行
    • 处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
    • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
    • ......

    RunLoop在哪里开启

    UIApplicationMain函数内启动了Runloop,程序不会马上退出,而是保持运行状态。因此每一个应用必须要有一个runloop,
    我们知道主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    

    进入UIApplicationMain

    UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
    

    发现它返回的是一个int数,那么我们对main函数做一些修改

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSLog(@"开始");
            int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
            NSLog(@"结束");
            return re;
        }
    }
    

    运行程序,发现只会打印开始,并不会打印结束,这说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会立即返回,一直在运行中,也就保证了程序的持续运行。

    我们来看到RunLoop的源码

    void CFRunLoopRun(void) {   /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    

    我们发现RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。

    RunLoop对象

    • iOS中有2套API来访问和使用RunLoop
      • Foundation:NSRunLoop
      • Core Foundation:CFRunLoopRef
    • NSRunLoop和CFRunLoopRef都代表着RunLoop对象

    RunLoop与线程

    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
    • RunLoop会在线程结束时销毁
    • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

    下面通过看 CFRunloopRef 源码对前面三条结论进行证明
    CFRunLoopGetMain

    CFRunLoopRef CFRunLoopGetMain(void) {
        CHECK_FOR_FORK();
        static CFRunLoopRef __main = NULL; // no retain needed
        if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
        return __main;
    }
    

    CFRunLoopGetCurrent

    CFRunLoopRef CFRunLoopGetCurrent(void) {
        CHECK_FOR_FORK();
        CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
        if (rl) return rl;
        return _CFRunLoopGet0(pthread_self());
    }
    

    可以看到 CFRunLoopGetMain 和 CFRunLoopGetCurrent 调用了 _CFRunLoopGet0 函数,并把主线程或当前线程作为参数传递进去。

    _CFRunLoopGet0

    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
        }
        __CFLock(&loopsLock);
        if (!__CFRunLoops) {
            __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
            __CFLock(&loopsLock);
        }
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        __CFUnlock(&loopsLock);
        if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
            // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
            __CFUnlock(&loopsLock);
        CFRelease(newLoop);
        }
        if (pthread_equal(t, pthread_self())) {
            _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
            if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
            }
        }
        return loop;
    }
    

    通过源码可以看到,首先先将传入的线程参数跟kNilPthreadT 进行比较

    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    

    在 CFRunLoop 源码可以找到 kNilPthreadT 的定义

    #if DEPLOYMENT_TARGET_WINDOWS
    static pthread_t kNilPthreadT = { nil, nil };
    #else
    static pthread_t kNilPthreadT = (pthread_t)0;
    #endif
    

    可以看到 kNilPthreadT 表示线程 0,即主线程,所以上面源码通过将传入线程跟主线程进行比较,看是不是主线程,如果是,将主线程(pthread_main_thread_np())赋值给传入的线程

    接着判断 __CFRunLoops 是否存在,如果不存在,则先创建用于存储 Runloop 的全局Dictionary,再创建主线程对应的 mainLoop ,并以用 pthreadPointer 对主线程做一些处理得到的值作为 key,将mainLoop 存储到全局Dictionary 中

     __CFLock(&loopsLock);
    if (!__CFRunLoops) {
            __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
            __CFLock(&loopsLock);
        }
    

    接着将 __CFRunLoops 和 pthreadPointer 对线程 t 做一些处理得到的值 作为参数传入 CFDictionaryGetValue 函数中获取线程 t 对应的 loop

    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        __CFUnlock(&loopsLock);
        if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
            // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
            __CFUnlock(&loopsLock);
        CFRelease(newLoop);
        }
    

    如果 loop 不存在,创建一个newLoop, 然后再一次调用CFDictionaryGetValue 看一下现在是否已经存在对应的 loop 了,如果依旧不存在,将 newLoop 存入全局Dictionary,并将要返回的 loop 值赋值为newLoop,最后将 loop 返回

    写一下代码熟悉加深理解

    // MJPermenantThread.h 
    #import <Foundation/Foundation.h>
    
    typedef void (^MJPermenantThreadTask)(void);
    
    @interface MJPermenantThread : NSObject
    /**
     开启线程
     */
    //- (void)run;
    /**
     在当前子线程执行一个任务
     */
    - (void)executeTask:(MJPermenantThreadTask)task;
    /**
     结束线程
     */
    - (void)stop;
    @end
    
    // MJPermenantThread.m
    #import "MJPermenantThread.h"
    
    /** MJThread **/
    @interface MJThread : NSThread
    @end
    @implementation MJThread
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
    }
    @end
    
    /** MJPermenantThread **/
    @interface MJPermenantThread()
    @property (strong, nonatomic) MJThread *innerThread;
    @end
    
    @implementation MJPermenantThread
    #pragma mark - public methods
    - (instancetype)init
    {
        if (self = [super init]) {
            self.innerThread = [[MJThread alloc] initWithBlock:^{
                NSLog(@"begin----");
                
                // 创建上下文(要初始化一下结构体)
                CFRunLoopSourceContext context = {0};
                
                // 创建source
                CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
                
                // 往Runloop中添加source
                CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
                
                // 销毁source
                CFRelease(source);
                
                // 启动
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
                
    //            while (weakSelf && !weakSelf.isStopped) {
    //                // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
    //                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
    //            }
                
                NSLog(@"end----");
            }];
            
            [self.innerThread start];
        }
        return self;
    }
    
    //- (void)run
    //{
    //    if (!self.innerThread) return;
    //
    //    [self.innerThread start];
    //}
    
    - (void)executeTask:(MJPermenantThreadTask)task
    {
        if (!self.innerThread || !task) return;
        [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
    }
    
    - (void)stop
    {
        if (!self.innerThread) return;
        [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
        [self stop];
    }
    
    #pragma mark - private methods
    - (void)__stop
    {
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.innerThread = nil;
    }
    
    - (void)__executeTask:(MJPermenantThreadTask)task
    {
        task();
    }
    
    @end
    
    // ViewController.m
    #import "ViewController.h"
    #import "MJPermenantThread.h"
    
    @interface ViewController ()
    @property (strong, nonatomic) MJPermenantThread *thread;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];  
        self.thread = [[MJPermenantThread alloc] init];
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self.thread executeTask:^{
            NSLog(@"执行任务 - %@", [NSThread currentThread]);
        }];
    }
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
    }
    @end
    

    代码实现了线程保活的逻辑,创建了 source 并将其添加到当前线程的 runloop 的 kCFRunLoopDefaultMode 中,然后启动 runloop

     /* 启动
    * CFRunLoopMode mode :runloop 运行在哪个 mode 下,这里是 kCFRunLoopDefaultMode 
    * CFTimeInterval seconds:runloop 保持运行的时间,这里传入了一个很大的数 1.0e10
    * Boolean returnAfterSourceHandled:如果设置为true,代表执行完source后就会退出当前loop
    */
     CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
    

    启动之后线程会在 1.0e10 s 的时间内保活,如果想结束 runloop ,可以调用以下代码结束,从而使得线程结束。

    CFRunLoopStop(CFRunLoopGetCurrent());
    

    获取RunLoop对象

    Foundation

    // 获得当前线程的RunLoop对象
    [NSRunLoop currentRunLoop]; 
    // 获得主线程的RunLoop对象
    [NSRunLoop mainRunLoop]; 
    

    Core Foundation

    // 获得当前线程的RunLoop对象
    CFRunLoopGetCurrent(); 
    // 获得主线程的RunLoop对象
    CFRunLoopGetMain(); 
    

    RunLoop相关的类

    Core Foundation中关于RunLoop的5个类

    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopObserverRef
    
    typedef struct __CFRunLoop *CFRunLoopRef;
    struct __CFRunLoop {
        pthread_t _pthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
    };
    
    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    struct __CFRunLoopMode {
        CFStringRef _name;
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timer;
    };
    
    #### CFRunLoopModeRef
    
    image.png
    • CFRunLoopModeRef代表RunLoop的运行模式
    • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
    • RunLoop启动时只能选择其中一个Mode,作为currentMode
    • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
    • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

    RunLoop 有五种运行模式,其中常见的是前两种

    kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
    GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

    CFRunLoopSourceRef

    Source分为两种

    Source0:非基于Port的 用于用户主动触发的事件(点击button 或点击屏幕)
    Source1:基于Port的 通过内核和其他线程相互发送消息(与内核相关)

    CFRunLoopTimerRef

    • NSTimer
    • performSelector:withObject:afterDelay:

    CFRunLoopObserverRef

    • 用于监听RunLoop的状态
    • UI刷新(BeforeWaiting)
    • Autorelease pool(BeforeWaiting)
    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),                      // 即将进入 Loop
        kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2),     // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5),      // 即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒
        kCFRunLoopExit = (1UL << 7),                       // 即将推出 Loop
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    

    添加Observer监听RunLoop的所有状态

    // 创建Observer
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"kCFRunLoopEntry");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"kCFRunLoopBeforeTimers");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"kCFRunLoopBeforeSources");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"kCFRunLoopBeforeWaiting");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"kCFRunLoopAfterWaiting");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"kCFRunLoopExit");
                    break;
                default:
                    break;
            }
        });
        // 添加Observer到RunLoop中
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
        // 释放
        CFRelease(observer);
    

    RunLoop的运行逻辑

    RunLoop的运行逻辑
    源码分析
    // 供外部调用的公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
    void CFRunLoopRun(void) {   /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    
    // 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
    SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    
        // 通知Observers : 进入Loop
        // __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
    函数
        if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        
        // 核心的Loop逻辑
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        
        // 通知Observers : 退出Loop
        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
        return result;
    }
    
    // 精简后的 __CFRunLoopRun函数,保留了主要代码
    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
        int32_t retVal = 0;
        do {
            // 通知Observers:即将处理Timers
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 
            
            // 通知Observers:即将处理Sources
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
            
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 处理Sources0
            if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
                // 处理Blocks
                __CFRunLoopDoBlocks(rl, rlm);
            }
            
            // 如果有Sources1,就跳转到handle_msg标记处
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
            
            // 通知Observers:即将休眠
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            
            // 进入休眠,等待其他消息唤醒
            __CFRunLoopSetSleeping(rl);
            __CFPortSetInsert(dispatchPort, waitSet);
            do {
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            } while (1);
            
            // 醒来
            __CFPortSetRemove(dispatchPort, waitSet);
            __CFRunLoopUnsetSleeping(rl);
            
            // 通知Observers:已经唤醒
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
            
        handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
            if (被Timer唤醒的) {
                // 处理Timer
                __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
            }
            else if (被GCD唤醒的) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else { // 被Sources1唤醒的
                __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
            }
            
            // 执行Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
            if (sourceHandledThisLoop && stopAfterHandle) {
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(rl)) {
                __CFRunLoopUnsetStopped(rl);
                retVal = kCFRunLoopRunStopped;
            } else if (rlm->_stopped) {
                rlm->_stopped = false;
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                retVal = kCFRunLoopRunFinished;
            }
            
        } while (0 == retVal);
        
        return retVal;
    }
    

    上述源代码中,相应处理事件函数内部还会调用更底层的函数,内部调用才是真正处理事件的函数,通过上面bt打印全部堆栈信息也可以得到验证。

    __CFRunLoopDoObservers 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

    __CFRunLoopDoBlocks 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

    __CFRunLoopDoSources0 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

    __CFRunLoopDoTimers 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

    GCD 调用 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

    __CFRunLoopDoSource1 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

    RunLoop处理逻辑流程图

    RunLoop处理逻辑流程图
    RunLoop休眠的实现原理
    RunLoop休眠的实现原理
    RunLoop在实际开中的应用

    控制线程生命周期(线程保活)
    解决NSTimer在滑动时停止工作的问题
    监控应用卡顿
    性能优化

    相关文章

      网友评论

          本文标题:RunLoop

          本文链接:https://www.haomeiwen.com/subject/bwddcctx.html