「iOS」——RunLoop学习

底层学习


iOS--RunLoop学习

RunLoop的概念

一般来讲,一个线程只能执行一次任务,任务完成后按成就会退出。但如果我们需要一个机制,让线程能随时处理事件但不退出。就需要这样一个dowhilt循环。

objective-c 复制代码
do {
   //获取消息
   //处理消息
} while (消息 != 退出)

这种模型通常被称为Event Loop,在很多系统和框架都有实现。例如:Node.js 的事件处理,Windows 程序的消息循环,而在macOS、iOS中就是Run Loop。

实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

实际上,RunLoop就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面Event Loop的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

在iOS系统中,提供了两个EventLoop 的具体实现:

NSRunLoopCFRunLoopRef

CFRunLoopRef是在 CoreFoundation框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

RunLoop与线程的关系

RunLoop 是和线程一一对应的,app 启动之后,程序进入了主线程,苹果帮我们在主线程启动了一个 RunLoop。如果是我们开辟的线程,就需要自己手动开启 RunLoop,而且,如果你不主动去获取 RunLoop,那么子线程的 RunLoop 是不会开启的,它是懒加载的形式。

另外苹果不允许直接创建 RunLoop,只能通过 CFRunLoopGetMain()CFRunLoopGetCurrent() 去获取,其内部会创建一个 RunLoop 并返回给你(子线程),而它的销毁是在线程结束时。

这两个函数内部的逻辑大概是这样:

其中pthread_main_thread_np() 或 [NSThread mainThread] 获取主线程; pthread_self() 或 [NSThread currentThread] 获取当前线程。

objectivec 复制代码
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

从上面的代码中可以看出:线程和RunLoop之间是一一对应的,其关系保存在一个全局的Dictionary中。

线程刚创建时并没有RunLoop,主动获取后系统会为我们创建一个RunLoop。在线程结束时RunLoop会被销毁。

  • CFRunLoopGetCurrent 函数便是获取当前线程的 CFRunLoop 对象,如果不存在的话会则会创建一个。
  • CFRunLoopGetMain 则是获取主线程的 CFRunLoop 对象。

就有如下代码:

  • NSRunloop类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
  • CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全。
objectivec 复制代码
// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象


// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象

RunLoop的结构

RunLoop里面有五个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其结构图如下图所示:

Mode

Mode也就是模式,一个RunLoop当前只能处于某一种Mode中。Mode之间互不干扰,A Mode中发生的事情与B mode无关,尽管他们在一个RunLoop中。而苹果的滚动和默认状态分别对应两种不同的Mode,因此他们非常丝滑。

可以自定义 Mode,但是基本不会这样,苹果也为我们提供了几种 Mode:

  • kCFRunLoopDefaultMode :app默认Mode,通常主线程是在这个Mode下运行

  • UITrackingRunLoopMode :界面追踪Mode,比如ScrollView滚动时就处于这个Mode

  • UIInitializationRunLoopMode :刚启动app时进入的第一个Mode,启动完后不再使用

  • GSEventReceiveRunLoopMode :接受系统事件的内部Mode,通常用不到

  • kCFRunLoopCommonModes :不是一个真正意义上的Mode,但是如果你把事件丢到这里来,那么不管你当前处于什么Mode,都会触发你想要执行的事件。

  • NSModalPanelRunLoopMode:当应用程序显示一个模态对话框时使用此模式。在此模式下,RunLoop只处理与模态面板相关的事件,忽略其他非模态事件,确保用户只能与模态面板交互。

程序运行后,画面静止没有操作时,就处于 kCFRunLoopDefaultMode 状态,当滚动它时,就会处于 UITrackingRunLoopMode状态。如果你想要这两个状态能同时相应一件事情,要么同时添加到两种Mode里,要么把这件事情放到 kCFRunLoopCommonModes 中去执行

CFRunLoop对外暴露的管理Mode接口只有下面2个:

objectivec 复制代码
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

Mode暴露的管理mode item的接口有下面几个:

objective-c 复制代码
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
  • 只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。

  • 对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

Observer

CFRunLoopObserverRef是观察者,可以观察RunLoop的各种状态,每个Observer都包含一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接受这个变化。

苹果用一个枚举值来表示RunLoop的状态:

objectivec 复制代码
/* 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   // 所有的状态
};

这里通过RunLoop的执行流程,来了解Observer的工作,具体如下:

由上图也可知道,Timer和Source就是RunLoop要干的活。

Timer

从结构的那张图可以看到,Mode中有一个Timer的数组,一个Mode中可以有多个Timer。Timer其实就是定时器,它的工作原理是:生成一个 Timer,确定要执行的任务,和多久执行一次,将其注册到 RunLoop 中,RunLoop 就会根据你设定的时间点,当时间点到时,去执行这个任务,如果它正在休眠,那么就会先唤醒 RunLoop,再去执行。

但是,这个时间点并不是非常准确。因为RunLoop的执行是有一个顺序的,要处理的事情也有先后顺序。如果时间点到了,RunLoop会将Timer要执行的事情添加到待执行清单,但是也需要等待执行清单前面的事情执行完了才会执行到Timer的事情

对于NSTimer,当我们用scheduledTimerWithTimeInterval方法初始化定时器时

objectivec 复制代码
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以他就等同于下面代码:

objectivec 复制代码
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

Source

Source是另外一种RunLoop要干的活。RunLoop中定义了两种版本的Source,一个是Source0,一个是Source1

  • Source0:处理 App 内部事件,App 自己负责管理(触发),如 UIEventCFSocket

​ Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

  • Source1:由 RunLoop 内核管理,Mach port 驱动,如 CFMackPortCFMessagePort

这里Source1比较不好理解。

Mach Port 是 macOS/iOS 底层(基于 Mach 微内核 )的 进程间通信(IPC)机制 ,用于线程、进程或内核之间的安全消息传递。它是 XNU 内核(iOS/macOS 的核心)的核心组件之一,也是 Source1的底层驱动

所以简而言之,Source1其实就是进程间或者线程间通信的一种方式。比如在同一个RunLoop下,线程A想要给线程B发送C,这就需要通过port进行传输,然后系统将传输的东西包装成Source1,在线程A中监听port是否有东西传输过来,接收到后,唤醒RunLoop进行处理。

在源码上,他们不同的点在于:Source1的结构体中会带有mach_port_t,可以接受内核消息并触发回调。

objectivec 复制代码
typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

我们还可以添加Run Loop Source

在此之前,我们看一看Run Loop Mode提供给我们管理mode item的接口

cpp 复制代码
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

CFRunLoopAddSource的代码结构如下:

objectivec 复制代码
//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rls)) return;
    Boolean doVer0Callout = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在,则copy一个新的复制给set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems为空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把传入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果刚才set copy到的数组里有数据
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source
    } else {
        //根据modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在,则初始化_sources0,_sources0和_portToV1SourceMap
        if (NULL != rlm && NULL == rlm->_sources0) {
            rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
        }
        //如果_sources0和_sources1中都不包含传入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,则加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1,则加到_sources1
            } else if (1 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources1, rls);
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来
                    //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_runLoops中
            if (NULL == rls->_runLoops) {
                rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
            }
            CFBagAddValue(rls->_runLoops, rl);
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.schedule) {
                    doVer0Callout = true;
                }
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
    }
}

传入的三个参数分别是:

  • RunLoop对象
  • 添加的事件源即Source
  • RunLoop Mode的名称

通过添加source的这段代码可以得出如下结论:

  • 如果modeName传入kCFRunLoopCommonModes,则该source会被保存到RunLoop的_commonModeItems中

  • 如果modeName传入kCFRunLoopCommonModes,则该source会被添加到所有commonMode中

  • 如果modeName传入的不是kCFRunLoopCommonModes,则会先查找该Mode,如果没有,会创建一个

  • 同一个source在一个mode中只能被添加一次

RunLoop 执行流程

  1. 通知 Observer 已经进入 RunLoop

  2. 通知 Observer 即将处理 Timer

  3. 通知 Observer 即将处理 Source0

  4. 处理 Source0

  5. 如果有 Source1,跳到第 9 步(处理 Source1)

  6. 通知 Observer 即将休眠

  7. 将线程置于休眠状态,直到发生以下事件之一

    • 有 Source0

    • Timer 到时间执行

    • 外部手动唤醒

    • 为 RunLoop 设定的时间超时

  8. 通知 Observer 线程刚被唤醒

  9. 处理待处理事件

    • 如果是 Timer 事件,处理 Timer 并重新启动循环,跳到 2

    • 如果 Source1 触发,处理 Source1

    • 如果 RunLoop 被手动唤醒但尚未超时,重新启动循环,跳到 2

  10. 通知 Observer 即将退出 Loop

实际上 RunLoop 内部就是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,直到超时或手动停止,该函数才会返回。

但是默认时间是一个巨大的数,可以理解为无穷大即不会超时。

RunLoop 进入休眠所调用的函数是 mach_msg(),其内部会进行一个系统调用,然后内核会将线程置于等待状态,所以这是一个系统级别的休眠。因此RunLoop在休眠时不会占用CPU。

RunLoop 的应用

1.AutoreleasePool是什么时候释放的

自动释放池的释放时机和RunLoop有关。苹果在主线程的RunLoop中注册了两个Observer。

第一个 Observer,监听一个事件,就是 Entry,即将进入 Loop 的时候,创建一个自动释放池,并且给了一个最高的优先级,保证自动释放池的创建发生在其他回调之前,这是为了保证能管理所有的引用计数。

第二个 Observer,监听两个事件,一个 BeforeWaiting,一个 ExitBeforeWaiting 的时候,干两件事,一个释放旧的池,然后创建一个新的池,所以这个时候,自动释放池就会有一次释放的操作,是在 RunLoop 即将进入休眠的时候。Exit 的时候,也释放自动释放池,这里也有一次释放的操作。

即:

  1. 进入RunLoop,先创建一个自动释放池
  2. RunLoop休息**kCFRunLoopBeforeWaiting(即将休眠)**,释放的当前的自动释放池,创建新的自动释放池
  3. RunLoop退出**kCFRunLoopExit(退出 RunLoop)**,释放当前的自动释放池

2.触控事件的响应

苹果会提前在App内注册一个Source1来监听系统事件。

比如,当一个 触摸/锁屏/摇晃 之类的系统事情产生,系统会先包装,包装好了,通过 mach port 传输给需要的 App 进程,传输后,提前注册的 Source1 就会触发回调,然后由 App 内部再进行分发。该行为发生在kCFRunLoopAfterWaiting 阶段

  1. 注册一个 Source1 用于接收系统事件
  2. 硬件事件发生
  3. IOKit.framework 生成 IOHIDEvent 事件并由 SpringBoard 接收
  4. SpringBoard 用 mach port 转发给需要的 App
  5. 注册的 Source1 触发回调
  6. 回调中将 IOHIDEvent 包装成 UIEvent 进行处理或分发

3.刷新界面

我们都知道改变 UI 的参数后,它并不会立马刷新。而它的刷新,也是通过 RunLoop 来实现。

当 UI 需要更新,先标记一个 dirty,然后提交到一个全局容器中去,调用 setNeedsLayout/setNeedsDisplay 后,系统将视图标记为待更新。。然后,在休眠前(BeforeWaiting)或退出时(Exit),调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 触发界面渲染。

最终执行 CA::Transaction::commit() 提交所有 UI 变更。

4.线程保活

线程保活问题,从字面意思上就是保护线程的生命周期不结束.正常情况下,当线程执行完一次任务之后,需要进行资源回收,但是当有一个任务,随时都有可能去调用,如果在子线程去执行,并且让子线程一直存活着,为了避免来回多次创建毁线程的动作, 降低性能消耗。

通俗一点,我们可以创建一个常驻线程,其RunLoop始终运行,来实现这个功能。

例如第三方库AFNetworking

Runloop启动前必须要至少一个Timer/Observer/Source,所以AFNetworking在[runLoop run]

之前创建了NSMachPort添加进去了.通常情况下调用者需要持有这个NSMachPort并在外部线程通过这个port发送消息到loop内

objectivec 复制代码
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

小知识

mach Port

Mach Port 是 macOS/iOS 底层(基于 Mach 微内核 )的 进程间通信(IPC)机制 ,用于线程、进程或内核之间的安全消息传递。它是 XNU 内核(iOS/macOS 的核心)的核心组件之一,也是 RunLoop 事件源(Source1)的底层驱动

Toll-Free Bridging(对象桥接)详解

在 Apple 的框架中,Toll-Free Bridging 是一种允许 Core Foundation 对象(C 语言)和 Foundation 对象(Objective-C)之间无缝转换 的机制。

  • 特点 :某些 Core Foundation 和 Foundation 的类实际上是同一底层实现的两种接口,可以 直接强制转换 而无需额外内存操作。
  • 内存管理 :转换后的对象仍遵循原有的内存管理规则(ARC 或手动 CFRetain/CFRelease)。
CFRunLoopTimerNSTimer 的桥接
  • CFRunLoopTimerRef:Core Foundation 的 C 语言结构体,用于定时任务。
  • NSTimer:Foundation 的 Objective-C 类,封装了定时器功能。
  • 桥接关系
objectivec 复制代码
// Core Foundation → Foundation
CFRunLoopTimerRef cfTimer = ...;
NSTimer *nsTimer = (__bridge NSTimer *)cfTimer;

// Foundation → Core Foundation
NSTimer *nsTimer = ...;
CFRunLoopTimerRef cfTimer = (__bridge CFRunLoopTimerRef)nsTimer;
相关推荐
超浪的晨25 分钟前
Java 单元测试详解:从入门到实战,彻底掌握 JUnit 5 + Mockito + Spring Boot 测试技巧
java·开发语言·后端·学习·单元测试·个人开发
飞翔的佩奇2 小时前
OpenTelemetry学习笔记(十二):在APM系统中,属性的命名空间处理遵循规则
笔记·学习·springboot·sdk·apm·opentelemetry
序属秋秋秋3 小时前
《C++初阶之STL》【vector容器:详解 + 实现】
开发语言·c++·笔记·学习·stl
Brookty3 小时前
【Java学习】匿名内部类的向外访问机制
java·开发语言·后端·学习
快乐肚皮3 小时前
Zookeeper学习专栏(十):核心流程剖析之服务启动、请求处理与选举协议
linux·学习·zookeeper·源码
ATaylorSu4 小时前
Kafka入门指南:从零开始掌握分布式消息队列
笔记·分布式·学习·kafka
CarmenHu5 小时前
RNN学习笔记
笔记·rnn·学习
钊兵6 小时前
mysql时间处理函数和操作笔记
数据库·笔记·mysql
xnglan7 小时前
使用爬虫获取游戏的iframe地址
开发语言·爬虫·python·学习
songgeb9 小时前
Concurrency in Swift学习笔记-初识
ios·swift