目录

iOS源码阅读笔记

Runtime

AutoreleasePoolPageSideTableMapAssociationsManager 是在map_images->map_images_nolock->arr_init() 函数中初始化的;

一个类最多能添加64个分类;

dyld加载流程

引自飘云的dyld详解

  • 第一步:设置运行环境
  • 第二步:加载共享缓存
  • 第三步:实例化主程序
  • 第四步:加载插入的动态库
  • 第五步:链接主程序
  • 第六步:链接插入的动态库
  • 第七步:执行弱符号绑定
  • 第八步:执行初始化方法(runtime和类的加载是在这一步)
  • 第九步:查找入口点并返回
叙述一下:
  1. 配置环境变量,开启共享缓存,接下来把主程序的可执行文件(macho)加载进内存,检测macho内的magiccpu等属性以及兼容性,如果通过则创建一个imageLoader,然后插入动态库,链接主程序(加载进所有依赖库,然后rebase修正偏移、rebind绑定符号地址)和插入的动态库,然后弱符号绑定,接下来就是进入真正的初始化方法,如下图,当dyld加载到开始链接主程序的时候,递归调用recursiveInitialization函数;

/images/opensource/ios/Runtime_objc_init.png
_objc_init
/images/opensource/ios/Runtime_recursiveInitialization.png
recursiveInitialization

  1. 这个函数第一次执行,会走到 doInitialization -> doModInitFunctions -> libSystemInitialized,进行libsystem的初始化;
  2. libsystem 的初始化,它会调用起 libdispatch_initlibdispatchinit 会调用 _os_object_init,这个函数里面调用了 _objc_init
  3. _objc_init 中注册并保存了map_imagesload_imagesunmap_image函数地址;
  4. 注册完毕继续回到 recursiveInitialization 递归下一次调用,例如 libobjc,当 libobjc 来到 recursiveInitialization 调用时会触发libsystem,调用到_objc_init里注册好的回调函数进行调用,就来到了libobjc,调用load_images

类的加载

  1. read_image: 初始化gdb_objc_realized_classes表,容量是类总数量的 4/3 倍,这个表用来存放不在共享缓存中并且非懒加载类。接下来从mach-o中读取非懒加载类,把从mach-o中读取的类指针转换成类名,变成可识别的类(地址 -> 类名),然后加入前面创建的gdb_objc_realized_classes map表中(这个表的作用是在后面处理类有没有被正确的处理),同时也会加入 allocatedClasses 这个set表中(这个表是在 _objc_init 中的 runtime_init 函数中初始化的,存储的是已经初始化过的类);总的来说,这一步就是把类读取到内存;

  2. 接下来就是重点方法realizeClassWithoutSwift:创建rw,把ro放到rw中(方法列表是在后面放的),递归执行realizeClassWithoutSwift,即递归设置rw

  3. 设置类、父类和元类,其实就是生成一个双向链表;

  4. methodizeClass : 获取classro中的baseMethods,通过prepareMethodLists函数进行方法升序排序,然后 unttachedCategories::attachToClass ,这里是重头戏, attachToClass中会执行attachCategories,在这里面会初始化rwe,在这过程中会把ro中的方法列表、属性列表、协议列表都复制到rwe中,然后接着会把category中的方法列表、属性列表、协议列表也会吸纳进去,此时类才算加载完成;

  5. 方法排序的这里有几个细节:在methodizeClass中首次从class_ro_w中拿到basemethods后就立即做了升序排序处理,而分类中的方法排序发生在attachCategory方法中,也就是说他们是分开各自独立排序的(疑问:没有整合到一起后再排序,那怎么用的二分查找?)。

    解答:

    只有类和分类都实现load方法,才会存在load_image阶段分类方法整合到所属类的方法列表中的操作,也就是说只有类或者分类中实现load方法的情况,类的方法列表和分类方法都是直接在编译期存放在class_ro_t中的baseMethods中的;那这种情况怎么能保证分类方法在原始类方法前面的?这应该是编译器自己在编译期做的处理,让分类方法地址比原始类的方法地址要低。而对于二分查找,其实整合后的方法列表其实是个二维数组,内部存的是排好序的一维方法列表(methodizeClass阶段preparemethod进行方法排序),方法查找先是顺序遍历二维数组,再在有序的一维方法列表中进行二分查找。

  6. 方法添加顺序:新建个数组,先把类中class_ro_w basemethods放到数组的后面,然后把分类方法放到数组前面

为什么category会覆盖原来的方法?

map_images方法的 attachCategories -> attachLists 分类附加到原来的类的方法列表时,会先重新开辟一个新的数组,把原来的方法列表倒序遍历添加到新数组的后面,接着再正序遍历,把分类的方法添加到新数组的前面(方法列表的顺序与原来的顺序一致);

/images/opensource/ios/Runtime_AttackMethodLists.png
methodLists

类和category实现load对加载的影响

只有类和分类都实现load方法,才会发生在load_image阶段分类方法整合到所属类的方法列表中的操作; 只有类或者分类中实现load的时候,类的方法和分类方法都是直接在编译期存放到class_ro_t中的baseMethods中的。那这种情况怎么能保证分类方法在原始类方法前面的?这应该是编译器自己在编译期做的处理,让分类方法地址比原始类的方法地址要低(方法排序用的是升序排序)。

而对于类和分类都实现load的场景,即在load_image阶段把分类方法整合到类的方法列表中的情况是如何进行二分查找的呢?其实整合后的方法列表是个二维数组,内部存的是排好序的一维方法列表(methodizeClass阶段preparemethod进行的方法升序排序),方法查找时先是顺序遍历二维数组,再在有序的一维方法列表中进行二分查找。

综上所述,不要在类和分类中同时实现load方法也是提升启动速度的一个点,当然,不用load最好了。

为什么会有CleanMemory和DirtyMemory呢?

  • iOS运行过程中会涉及对内存进行增删改查,为了防止对原始数据的修改,所以把原来的CleanMemory copy一份到rw中。
  • 有了rw为什么还要rwe(脏内存)?因为不是所有的类加载进内存时需要进行动态的插入、删除,当我们添加一个属性、一个方法会对内存改动很大,对内存的消耗有一定影响,所以只要我们对类进行动态处理了(比如把category的方法、属性、协议合并到类中),就会生成一个rwe

为什么执行load方法时没有触发initialize

一定明确initialize是在首次发消息时才会触发,而load的执行是通过函数指针的方式调用的,没有走消息发送机制,所以不会触发initialize

为什么在对象释放过程中通过weak变量获取不到这个对象?

在关联的场景中,比如A关联BB弱持有AA释放时会释放其关联的B,导致Bdealloc执行,然后我们在Bdealloc方法中通过weak变量读取A,却发现获取到的是nil(根据释放流程此时A还没有free掉),这是为什么?

分析如下:

读取weak变量时执行的是objc_loadWeak函数,内部执行大概流程为:objc_loadWeak -> objc_loadWeakRetained -> obj->rootTryRetain() -> rootRetain(true, RRVariant::Fast) ,在rootRetain中如果当前对象正在处于释放流程中,则返回nil。具体代码如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
id
objc_loadWeakRetained(id *location)
{
    id obj;
    id result;
    Class cls;

    SideTable *table;
    
 retry:
    obj = *location;
    if (_objc_isTaggedPointerOrNil(obj)) return obj;
    
    table = &SideTables()[obj];
    
    table->lock();
    if (*location != obj) {
        table->unlock();
        goto retry;
    }
    
    result = obj;

    cls = obj->ISA();
    if (! cls->hasCustomRR()) {
        // 执行此逻辑
        if (! obj->rootTryRetain()) {
            result = nil;
        }
    }
    else {
        // 执行不到的逻辑,删掉
    }
        
    table->unlock();
    return result;
}

ALWAYS_INLINE bool 
objc_object::rootTryRetain()
{
    return rootRetain(true, RRVariant::Fast) ? true : false;
}

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa().bits);

    // ...

    do {
        transcribeToSideTable = false;
        newisa = oldisa;

        // 关键逻辑:
        // 如果正在释放中,并且tryRetain=true,则返回nil
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa().bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa().bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa().bits, &oldisa.bits, newisa.bits)));

    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }

        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}

既然没释放,那我们怎么拿到这个对象呢?通过unsafe_unretained或者assign标记就可以获取到了。

对象释放流程

调用release -> rootRelease,引用计数-1,当引用计数变为0时,就会通过objc_msgSend调用Objective-C对象的dealloc方法,然后进入到objc_object::rootDealloc()函数,函数内部会读取当前对象的isa中存储的信息,包括是否是非指针、有没有弱引用、成员变量、关联对象、has_sidetable_rc,如果都没有会直接释放(free),否则会执行objc_destructInstance(obj),这个函数的逻辑为先释放成员变量,接着移除关联对象,再移除弱引用,把弱引用指针置为nil,最后再从SideTableRefcountMap refcnts成员变量中 把存储当前对象引用计数的记录(key-value)从引用计数表中移除,类似于从字典中把这条key-value都删除(疑问:此时引用计数已经是0了,那最后这个引用计数表的处理是不是多余的,什么情况下会执行进来???)。

dealloc方法中如果有对self的引用,比如- (void)dealloc { id obj = self; },是不会发生引用计数+1的,runtime处理如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 是否正在释放
bool isDeallocating() {
    return extra_rc == 0 && has_sidetable_rc == 0;
}

// retain最终执行的函数
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    // 省略代码
    ... 

    // 在dealloc中这里的执行结果是true
    if (slowpath(newisa.isDeallocating())) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) {
            ASSERT(variant == RRVariant::Full);
            sidetable_unlock();
        }
        if (slowpath(tryRetain)) {
            return nil;
        } else {
            return (id)this;
        }
    }

    // 省略代码
    ...
}

Weak

weak_table_t 是全局保存弱引用的哈希表,它是通过对object地址做hash计算,然后从8SideTable数组中取出其中一张,然后再从SideTable中读取到weak_tableweak_table_t 是以 object 地址为 key,以 weak_entry_tvalue

weak_entry_t 是用来存储所有指向某个对象的弱引用变量的地址的,里面有个weak_referrer_t数组,它存储的其实是弱引用的指针,即指针的指针,这么做的目的是可以把弱引用置为nil

weak_entry_t 中有2种结构,当存储的弱引用数量<= 4个的时候用的其实是个定长数组,> 4的时候才会转为哈希数组。(这里使用哈希数组的原因应该是为了处理B弱引用A,然后B先释放了,这时那个弱引用可能也要置为nil,用hash数组的话查询速度会比较快)。往weak_entry_t 中添加弱引用变量时,即更新weak_referrer_t采用的是定向寻址法;

weak_table 中插入weak_entry_t时,先是对object地址取hash作为它的index,如果这个index下的位置不为空,则通过一个算法(index = (index+1) & weak_table->mask)重新计算生成一个新的index再读取对应的位置,直到找到一个空位置,然后把weak_entry_t放进去,同时更新元素数量。这种插入方式其实也是定向寻址法。

hash 函数,与 mask 做与操作,防止 index 越界;

1
size_t begin = hash_pointer(referent) & weak_table->mask;

/images/opensource/ios/Runtime_WeakHashInsert.png
weak_hash_insert

weak_table_t 还有一个扩容和缩容的处理,当前使用容量占到总容量(mask + 1)的 3/4 的时候会进行扩容处理,扩大到现有总容量的2倍。 当总容量超过1024,而实际使用的空间低于总空间的 1/16 时则会进行容量压缩,缩到现有总容量的1/8 (为什么是八分之一?是为了保证总容量是现有使用容量的2`倍)。

@synchronized原理

  1. 先从当前线程的TLS中尝试获取SyncData(本身是个单向链表),如果存在并且SyncData中的object与传进来的object相同,则说明找到对应的SyncData了。更新锁数量(lockCount),并返回SyncData

    (注意:一条线程的TLS中只能存唯一一个SyncData,假如已经存在了但是object并不与自己传进来的一致,则创建新的SyncData后并不会更新到TLS中,而是保存到 pthread_data 中,有点先入为主的意思)

  2. pthread_data中获取SyncCache(里面存着一个SyncCacheItem数组,SyncCacheItem存的是SyncData),如果存在则遍历SyncCacheItem数组,如果cacheItem中的syncData中的object与传进来的object相同,则更新 item->lockCount ,然后返回SyncData

  3. 走到这里就说明没有从thread cache中找到合适的SyncData。这时就会从全局StripMap<SyncList> sDataLists 表中读取,先通过对象objecthash值取出一个SyncList,接着拿到SyncList中的SyncData链表,然后遍历整个链表。

    a. 如果发现与object匹配的SyncData则更新SyncData中的threadCount数量,然后把找到的这个SyncData保存到TLS或者pthread_data中的SyncCache里面;

    b. 如果遍历到最后也没发现匹配的,则找到链表中第一个未使用(SyncData中的threadCount = 0)的SyncData,进行复用。这个SyncData也会和上面一样进行缓存;

    c. 如果没找到匹配的,也没找到未使用的,则创建一个新的SyncData。这个新的SyncData会先保存到SyncList中,然后也会和上面一样保存到TLS或者pthread_data中一份,即新创建的有2份缓存。

    /images/opensource/ios/Runtime_Synchronized.png
    synchronized

Associate 原理

所有的关联对象都是由AssociationsManager管理的,AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象。这相当于把所有对象的关联对象都存在一个全局hashMap里面,hashMapkey是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个hashMapvalue又是一个ObjectAssociationsMap,里面保存了关联对象的key和对应的value值。runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有会调用_object_remove_assocations做关联对象的清理工作。

setget时,即对内部的map进行操作时都会用manager中的spinlock(底层其实还是unfair_lock),所以setget时一般情况下是线程安全的。但是可能是为了追求性能,set时把旧对象的释放放到了锁外,atomic get时为了保证线程安全,会retain一下访问对象,在锁外又autorelease了一下,如果不执行retain操作可能会出现数据竞争。可以参考下这篇文章: AssociatedObject 源码分析:如何实现线程安全?

/images/opensource/ios/Runtime_Associate.png
Associate


GCD

可创建的最大线程数是 255

1
thread_pool_size = DISPATCH_WORKQ_MAX_PTHREAD_COUNT     255 
  1. 自定义串行队列是overcommit的,并行队列不是overcommit

  2. 自定义队列的目标队列在初始化时传参为NULL,然后会为其从_dispatch_root_queues 中获取一个根目标队列;当 tqNULL,即入参目标队列为 DISPATCH_TARGET_QUEUE_DEFAULT(值是 NULL) 时, 根据 qosovercommit_dispatch_root_queues 全局的根队列数组中获取一个根队列作为新队列的目标队列

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    if (!tq) {
        tq = _dispatch_get_root_queue(
                qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos,
                overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq;
    
        if (unlikely(!tq)) {
            // 如果未取得目标队列则 crash
            DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
        }
    }
    

dispatch_sync

a. 首先将任务加入队列

b. 执行任务block

c. 将任务移出队列

d. sync里面的处理最终执行的是barrier的内部函数

e. 会死锁的原因:执行时会检查当前线程的状态(是否正在等待),然后与当前的线程的ID_dispatch_tid_self())做比较,相等的话则判定为死锁。(相关处理在 __DISPATCH_WAIT_FOR_QUEUE__ 函数中)


dispatch_async

a. 将异步任务(dispatch_queue 、 block)封装为 dispatch_continuation_t 类型

b. 然后执行 _dispatch_continuation_async -> dx_push递归重定向到根队列,然后执行_dispatch_root_queue_poke进行出队操作,通过创建线程执行 dx_invoke 执行block回调;


dispatch_barrier_async

a. 和dispatch_async 流程一样,只是里面有一个while循环,等队列中的barrier前面的任务执行完,才执行后面的;

b. 这里有个优化是:封装成 dispatch_continuation_s 结构时,会先从当前线程的TLS中获取一下,获取不到再从堆上创建新的


dispatch_group

a. dispatch_group内部维护着一个数值,初始值为0enter时减4leave时加4 https://juejin.cn/post/6902346229868019719#heading-4

b. 等待用的是while循环,而不是信号量


dispatch_semaphore_t

a. dispatch_semaphore_wait 时里面其实是起了一个do-while 循环,不断的去查询原子变量的值,不满足条件时会一直循环,借此阻塞流程的进行。有点像dispatch_once


dispatch_group_async

内部其实是对dispatch_asyncdispatch_group_enter / dispatch_group_leave 的封装


线程池复用原理

线程创建后从队列里取出任务执行,任务执行后使用信号量使其等待5秒钟,如果在这期间再有GCD任务过来,会先尝试唤醒线程,让它继续工作,否则等待超时后线程会自动结束,被系统销毁。(不是tableview中的复用池机制)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void *
_dispatch_worker_thread(void *context)
{
    //////////////////////////////////
    // 删减了部分代码
    //////////////////////////////////

	dispatch_queue_global_t dq = context;
	dispatch_pthread_root_queue_context_t pqc = dq->do_ctxt;

    int pending = os_atomic_dec2o(dq, dgq_pending, relaxed);

    // 线程存活5秒钟
	const int64_t timeout = 5ull * NSEC_PER_SEC;
	pthread_priority_t pp = _dispatch_get_priority();
	dispatch_priority_t pri = dq->dq_priority;

    // 从队列中取出任务执行,执行完后等待5秒钟
    // 如果5秒后没有被唤醒则进入超时逻辑,队列释放,线程退出
	do {
        // 任务执行
		_dispatch_root_queue_drain(dq, pri, DISPATCH_INVOKE_REDIRECTING_DRAIN);
		_dispatch_reset_priority_and_voucher(pp, NULL);
	} while (dispatch_semaphore_wait(&pqc->dpq_thread_mediator,
			dispatch_time(0, timeout)) == 0);

#if DISPATCH_USE_INTERNAL_WORKQUEUE
	if (monitored) _dispatch_workq_worker_unregister(dq);
#endif
	(void)os_atomic_inc2o(dq, dgq_thread_pool_size, release);
	_dispatch_root_queue_poke(dq, 1, 0);
	_dispatch_release(dq); // retained in _dispatch_root_queue_poke_slow
	return NULL;
}

dispatch_once

dispatch_once函数中的token (dispatch_once_t) 会被强转为dispatch_once_gate_t类型,而dispatch_once_gate_t里面是个union联合体类型,其中dgo_once用来记录当前block的执行状态,执行完后状态会被标记为DLOCK_ONCE_DONE

1
2
3
4
5
6
typedef struct dispatch_once_gate_s {
	union {
		dispatch_gate_s dgo_gate;
		uintptr_t dgo_once;
	};
} dispatch_once_gate_s, *dispatch_once_gate_t;

我们首先获取dgo_once变量的值,如果是DLOCK_ONCE_DONE,则表示已经执行过了,直接return掉; 如果是DLOCK_ONCE_UNLOCKED状态,则表示首次执行,然后会把当前的线程id存到dgo_once变量中,然后开始执行block任务,结束后会把dgo_once置为DLOCK_ONCE_DONE; 如果有其他线程执行过来,根据dgo_once判断,发现正在执行中,则会进入等待流程,等待其实是启了个for (;;)无限循环,在循环中不断地通过原子操作查询dgo_once的状态,等发现变为DLOCK_ONCE_DONE后则退出循环。


dispatch_source_merge_data

对应的结构定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 定义在 libdispatch 仓库中的 init.c 文件中
DISPATCH_VTABLE_INSTANCE(source,
	.do_type        = DISPATCH_SOURCE_KEVENT_TYPE,
	.do_dispose     = _dispatch_source_dispose,
	.do_debug       = _dispatch_source_debug,
	.do_invoke      = _dispatch_source_invoke,

	.dq_activate    = _dispatch_source_activate,
	.dq_wakeup      = _dispatch_source_wakeup,
	.dq_push        = _dispatch_lane_push,
);

把任务包装成dispatch_continuation_t对象,每次dispatch_source_merge_data时对内部变量进行原子性的ADD、OR、REPLACE等操作,并执行dx_wakeup函数,dx_wakeup是个宏定义,其实调用的是_dispatch_source_wakeup,wakeup这个函数其实是一个入队操作,但并不是每次都会进行入队(此处还未完全看明白 o(╯□╰)o ),接着会执行_dispatch_main_queue_drain -> _dispatch_continuation_pop_inline出队操作,流程基本和dispatch_async一致。


NSTimer

timer添加到runloop的过程:

如果是commonMode ,会被添加到runloop持有的一个_commonModeItems 集合中, 然后调用 __CFRunLoopAddItemToCommonModes 函数,把timer添加到runloopMode对象持有的_timers数组中 ,同时也会把modeName添加到runloopTimer_rlModes 中,记录runloopTimer都能在哪种runloop mode下执行;

如果是普通mode,则先获取这个runloopMode对象,把runloopModename添加到runloopTimer持有的 _rlModes集合中,然后调用 __CFRepositionTimerInMode 函数,把runloopTimer插入runloopMode持有的 _timers 数组中(如果数组中已经存在了,则先做移除操作);

上面添加完成后,会调用 __CFRepositionTimerInMode 函数,然后调用 __CFArmNextTimerInMode,再调用 mk_timer_arm 函数把 CFRunLoopModeRef_timerPort 和一个时间点注册到系统中,等待着 mach_msg 发消息唤醒休眠中的 runloop 起来执行到达时间的计时器。(macOS 和 iOS 下都是使用 mk_timer 来唤醒 runloop);

每次计时器都会调用 __CFArmNextTimerInMode 函数注册计时器的下次执行时间(这个时间是基于本次执行的理论时间叠加得到的,而非当前的真实时间,也就是说假如本次执行滞后了,不会影响下次理论上要执行的时间点),休眠中的runloop 通过当前runloop mode_timerPort 端口唤醒,然后在本次runloop循环中在 _CFRunloopDoTimers 函数中循环调用 __CFRunLoopDoTimer 函数,执行达到触发时间的timer_callout 函数。 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info); 是执行计时器的 _callout 函数。

NSTimer 不准时问题

通过上面的 NSTimer 执行流程可看到计时器的触发回调完全依赖 runloop 的运行(macOS 和 iOS 下都是使用 mk_timer 来唤醒 runloop),使用 NSTimer 之前必须注册到 run loop,但是 run loop 为了节省资源并不会在非常准确的时间点调用计时器,如果一个任务执行时间较长(例如本次 run loop 循环中 source0 事件执行时间过长或者计时器自身回调执行时间过长,都会导致计时器下次正常时间点的回调被延后或者延后时间过长的话则直接忽略这次回调(计时器回调执行之前会判断当前的执行状态 !__CFRunLoopTimerIsFiring(rlt),如果是计时器自身回调执行时间过长导致下次回调被忽略的情况大概与此标识有关 )),那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer 提供了一个 tolerance 属性用于设置宽容度,即当前时间点已经过了计时器的本次触发点,但是超过的时间长度小于 tolerance 的话,那么本次计时器回调还可以正常执行,不过是不准时的延后执行。 tolerance 的值默认是 0,最大值的话是计时器间隔时间_interval 的一半,可以根据自身的情况酌情设置 tolerance 的值。  (NSTimer 不是一种实时机制,以 main run loop 来说它负责了所有的主线程事件,例如 UI 界面的操作,负责的运算使当前 run loop 持续的时间超过了计时器的间隔时间,那么计时器下一次回调就被延后,这样就造成 timer 的不准时,计时器有个属性叫做 tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。如果延后时间过长的话会直接导致计时器本次回调被忽略。)

推荐文章