OC底层探索十八,iOS-OC底层

前言

锁,在我们的iOS开发中还是经常用到的,特别是在一些多线程的安全访问方面提供了提供了便捷的方案。锁,分为自旋锁,互斥锁,读写锁等类型。在iOS下,我们常见的锁包括:@synchronized,NSLock,dispatch_semphore,NSCondition,NSConditionLock,NSRecursiveLock等。本篇文章我们就重点分析下@synchronized的本质。

开始

首先从一个大家熟悉的卖票案例说起:

- (void)demo11{_totalCount = 20;[self startSaleTicket];}- (void)startSaleTicket{dispatch_async(dispatch_queue_create("queue_1", DISPATCH_QUEUE_CONCURRENT), ^{for (int i = 0; i < 30; i++) {[self saleTicket];}});dispatch_async(dispatch_queue_create("queue_2", DISPATCH_QUEUE_CONCURRENT), ^{for (int i = 0; i < 15; i++) {[self saleTicket];}});}- (void)saleTicket{if (_totalCount > 0) {_totalCount--;sleep(0.1);NSLog(@"当前余票还剩:%ld张",_totalCount);}else{NSLog(@"当前车票已售罄");}}

以上代码表示有两个并发队列同时执行异步的买票任务,这就意味着saleTicket在某个时刻或者多个时刻同时被多个任务访问,而导致数据的不同步,从而导致数据的不安全。输出结果:

2020-11-09 16:29:05.728246+0800 TestApp[13533:7920231] 当前余票还剩:18张2020-11-09 16:29:05.728246+0800 TestApp[13533:7920234] 当前余票还剩:19张2020-11-09 16:29:05.728464+0800 TestApp[13533:7920234] 当前余票还剩:17张2020-11-09 16:29:05.728464+0800 TestApp[13533:7920231] 当前余票还剩:17张2020-11-09 16:29:05.728604+0800 TestApp[13533:7920234] 当前余票还剩:16张2020-11-09 16:29:05.728624+0800 TestApp[13533:7920231] 当前余票还剩:15张2020-11-09 16:29:05.728750+0800 TestApp[13533:7920234] 当前余票还剩:13张2020-11-09 16:29:05.728752+0800 TestApp[13533:7920231] 当前余票还剩:13张2020-11-09 16:29:05.728925+0800 TestApp[13533:7920234] 当前余票还剩:12张2020-11-09 16:29:05.729969+0800 TestApp[13533:7920231] 当前余票还剩:11张2020-11-09 16:29:05.730211+0800 TestApp[13533:7920234] 当前余票还剩:10张...............省略一些打印数据...............2020-11-09 16:29:05.733704+0800 TestApp[13533:7920231] 当前余票还剩:1张2020-11-09 16:29:05.734041+0800 TestApp[13533:7920234] 当前余票还剩:0张2020-11-09 16:29:05.735104+0800 TestApp[13533:7920231] 当前车票已售罄2020-11-09 16:29:05.735527+0800 TestApp[13533:7920234] 当前车票已售罄2020-11-09 16:29:05.735923+0800 TestApp[13533:7920231] 当前车票已售罄

从输出的结果可以看到,整个数据一开始就发生了错误。接下来我们使用@synchronized给saleTicket加锁然后再执行:

- (void)saleTicket{@synchronized (self) {if (_totalCount > 0) {_totalCount--;sleep(0.1);NSLog(@"当前余票还剩:%ld张",_totalCount);}else{NSLog(@"当前车票已售罄");}}}2020-11-09 16:31:46.802605+0800 TestApp[13600:7921875] 当前余票还剩:19张2020-11-09 16:31:46.802823+0800 TestApp[13600:7921875] 当前余票还剩:18张2020-11-09 16:31:46.803009+0800 TestApp[13600:7921875] 当前余票还剩:17张2020-11-09 16:31:46.803180+0800 TestApp[13600:7921874] 当前余票还剩:16张2020-11-09 16:31:46.803327+0800 TestApp[13600:7921874] 当前余票还剩:15张2020-11-09 16:31:46.803493+0800 TestApp[13600:7921875] 当前余票还剩:14张................省略一些打印数据.................2020-11-09 16:31:46.809983+0800 TestApp[13600:7921874] 当前余票还剩:1张2020-11-09 16:31:46.810512+0800 TestApp[13600:7921874] 当前余票还剩:0张2020-11-09 16:31:46.811509+0800 TestApp[13600:7921874] 当前车票已售罄2020-11-09 16:31:46.812906+0800 TestApp[13600:7921874] 当前车票已售罄2020-11-09 16:31:46.813443+0800 TestApp[13600:7921874] 当前车票已售罄

此时的打印结果就不会出现异常的数据了。那@synchronized是怎么做到的呐?内部都做了些什么?我们首先通过断点并打开Always Show Disassembly来看下汇编的信息,是否有一些线索:

截屏2020-11-09 下午4.41.21.png截屏2020-11-09 下午4.42.45.png从汇编中,能看到的是在执行前后多了objc_sync_enter 和objc_sync_exit指令,可能有用先记录下。接下来我们通过clang来看下@synchronized最终转化为了什么:首先,我们在main.m中,添加几行代码:

int main(int argc, char * argv[]) {NSString * appDelegateClassName;@autoreleasepool {// Setup code that might create autoreleased objects goes here.appDelegateClassName = NSStringFromClass([AppDelegate class]);}@synchronized (appDelegateClassName) {NSLog(@"@synchronized");}return UIApplicationMain(argc, argv, nil, appDelegateClassName);}

通过clang编译后的代码:

int main(int argc, char * argv[]) {NSString * appDelegateClassName;/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));}{ id _rethrow = 0; id _sync_obj = (id)appDelegateClassName; objc_sync_enter(_sync_obj);try {struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}~_SYNC_EXIT() {objc_sync_exit(sync_exit);}id sync_exit;} _sync_exit(_sync_obj);NSLog((NSString *)&__NSConstantStringImpl__var_folders_yy_htpy_x9s09v1zf7ms0jgytwr0000gn_T_main_54c387_mi_0);} catch (id e) {_rethrow = e;}{ struct _FIN { _FIN(id reth) : rethrow(reth) {}~_FIN() { if (rethrow) objc_exception_throw(rethrow); }id rethrow;} _fin_force_rethow(_rethrow);}}return UIApplicationMain(argc, argv, __null, appDelegateClassName);}

比较凌乱,我们整理一下:

int main(int argc, char * argv[]) {NSString * appDelegateClassName;/* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool;appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));}/* @synchronized */{id _rethrow = 0;id _sync_obj = (id)appDelegateClassName;objc_sync_enter(_sync_obj);try {struct _SYNC_EXIT {_SYNC_EXIT(id arg) : sync_exit(arg) {}~_SYNC_EXIT() {objc_sync_exit(sync_exit);}id sync_exit;}_sync_exit(_sync_obj);NSLog((NSString *)&__NSConstantStringImpl__var_folders_yy_htpy_x9s09v1zf7ms0jgytwr0000gn_T_main_54c387_mi_0);} catch (id e) {_rethrow = e;}{struct _FIN {_FIN(id reth) : rethrow(reth) {}~_FIN() { if (rethrow) objc_exception_throw(rethrow);}id rethrow;}_fin_force_rethow(_rethrow);}}return UIApplicationMain(argc, argv, __null, appDelegateClassName);}

这里第一个作用域的内容是关于@autoreleasepool相关的,关于这部分内容可以直接看我的iOS-OC底层-@autoreleasepool分析 这篇文章。第二个作用域中的内容就是关系@synchronized了,还是挺复杂的💔。这里我们并不需要看太多:1.首先需要注意到的是objc_sync_enter(_sync_obj);这跟在汇编看到的是一致的2.然后是一个try{}包裹逻辑,内部有一个struct _SYNC_EXIT结构体,并且包换一个构造方法_SYNC_EXIT(id arg)和析构方法~_SYNC_EXIT(),3._sync_exit(_sync_obj);初始化结构体执行_SYNC_EXIT(id arg)4.NSLog(),这是需要加锁的代码块主体。5.当try{}执行完时,会触发结构体的~_SYNC_EXIT()的析构方法,即objc_sync_exit(sync_exit)方法。所以这里简单的理解就是:objc_sync_enter(_sync_obj) ->代码主体 -> objc_sync_exit(sync_exit)而catch部分我们就不做分析了,当发生异常后会执行objc_exception_throw。到此,我们需要探索的重点就锁定在了objc_sync_enter()和objc_sync_exit()的两个方法的具体实现中来,通过汇编跟踪下这两个方法在哪个库中(以objc开头,通常是在libobjc.dylib中):

截屏2020-11-09 下午5.17.58.png果不其然,确实就在libobjc中,查看源码:

// Begin synchronizing on 'obj'. // Allocates recursive mutex associated with 'obj' if needed.// Returns OBJC_SYNC_SUCCESS once lock is acquired.int objc_sync_enter(id obj){int result = OBJC_SYNC_SUCCESS;if (obj) {SyncData* data = id2data(obj, ACQUIRE);ASSERT(data);data->mutex.lock();} else {// @synchronized(nil) does nothingif (DebugNilSync) {_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");}objc_sync_nil();}return result;}

这里的参数id obj其实就是@synchronized时传入的参数;判断obj是否存在,如果存在,就通过id2data()方法读取一个SyncData* data,然后通过data->mutex.lock()来上锁。这里要说一下mutex:mutex:用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。这也就意味着,@synchronized实质上是一种(递归)互斥锁。而当obj == nil时,即@synchronized(nil),看else内部注释:@synchronized(nil) does nothing这里还是要看下objc_sync_nil()做了什么:

BREAKPOINT_FUNCTION(void objc_sync_nil(void));# define BREAKPOINT_FUNCTION(prototype) \OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \prototype { asm(""); }

这里是一个宏定义的实现,但内部也没有做什么实质性的处理。

// End synchronizing on 'obj'. // Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERRORint objc_sync_exit(id obj){int result = OBJC_SYNC_SUCCESS;if (obj) {SyncData* data = id2data(obj, RELEASE); if (!data) {result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;} else {bool okay = data->mutex.tryUnlock();if (!okay) {result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;}}} else {// @synchronized(nil) does nothing}return result;}

再看下int objc_sync_exit(id obj),当obj存在,同样会通过id2data(obj, RELEASE)去获取SyncData* data,如果存在,会通过data->mutex.tryUnlock();去解锁。当obj不存在,则dose nothing。从上面的源码中我们可以发现,两个方法内部都调用了同样的方法id2data(),只是第二个传参有所不同,一个是ACQUIRE,一个是RELEASE。我们重点看下id2data()方法:先预览下这部分代码:

截屏2020-11-09 下午5.49.18.png我们把代码大致分成如图的4个部分:

#if SUPPORT_DIRECT_THREAD_KEYS .. #endif部分SyncCache *cache = fetch_cache(NO);部分lockp->lock();到lockp->unlock(); 部分if(result)部分

这里我们要注意代码注释还是非常有价值的,首先是判断是否支持DIRECT_THREAD_KEYS的方式读取缓存,如果不支持就通过per-thread cache读取缓存;如果缓存中不存在则尝试通过in-use list查找,找到后插入缓存;如果都没有,即第一次进入,则通过posix_memalign()创建新的SyncData,最终写入缓存。

static SyncData* id2data(id object, enum usage why){spinlock_t *lockp = &LOCK_FOR_OBJ(object);SyncData **listp = &LIST_FOR_OBJ(object);SyncData* result = NULL;#if SUPPORT_DIRECT_THREAD_KEYS// Check per-thread single-entry fast cache for matching objectbool fastCacheOccupied = NO;SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);if (data) {fastCacheOccupied = YES;if (data->object == object) {// Found a match in fast cache.uintptr_t lockCount;result = data;lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);if (result->threadCount used; i++) {SyncCacheItem *item = &cache->list[i];if (item->data->object != object) continue;// Found a match.result = item->data;if (result->threadCount lockCount lockCount++;break;case RELEASE:item->lockCount--;if (item->lockCount == 0) {// remove from per-thread cachecache->list[i] = cache->list[--cache->used];// atomic because may collide with concurrent ACQUIREOSAtomicDecrement32Barrier(&result->threadCount);}break;case CHECK:// do nothingbreak;}return result;}}// Thread cache didn't find anything.// Walk in-use list looking for matching object// Spinlock prevents multiple threads from creating multiple // locks for the same new object.// We could keep the nodes in some hash table if we find that there are// more than 20 or so distinct locks active, but we don't do that now.lockp->lock();{SyncData* p;SyncData* firstUnused = NULL;for (p = *listp; p != NULL; p = p->nextData) {if ( p->object == object ) {result = p;// atomic because may collide with concurrent RELEASEOSAtomicIncrement32Barrier(&result->threadCount);goto done;}if ( (firstUnused == NULL) && (p->threadCount == 0) )firstUnused = p;}// no SyncData currently associated with objectif ( (why == RELEASE) || (why == CHECK) )goto done;// an unused one was found, use itif ( firstUnused != NULL ) {result = firstUnused;result->object = (objc_object *)object;result->threadCount = 1;goto done;}}// Allocate a new SyncData and add to list.// XXX allocating memory with a global lock held is bad practice,// might be worth releasing the lock, allocating, and searching again.// But since we never free these guys we won't be stuck in allocation very often.posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));result->object = (objc_object *)object;result->threadCount = 1;new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);result->nextData = *listp;*listp = result; done:lockp->unlock();if (result) {// Only new ACQUIRE should get here.// All RELEASE and CHECK and recursive ACQUIRE are // handled by the per-thread caches above.if (why == RELEASE) {// Probably some thread is incorrectly exiting // while the object is held by another thread.return nil;}if (why != ACQUIRE) _objc_fatal("id2data is buggy");if (result->object != object) _objc_fatal("id2data is buggy");#if SUPPORT_DIRECT_THREAD_KEYSif (!fastCacheOccupied) {// Save in fast thread cachetls_set_direct(SYNC_DATA_DIRECT_KEY, result);tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);} else #endif{// Save in thread cacheif (!cache) cache = fetch_cache(YES);cache->list[cache->used].data = result;cache->list[cache->used].lockCount = 1;cache->used++;}}return result;}

首先看第1部分:这里先扩展一下TLS相关的概念:线程局部存储(Thread Local Storage,TLS),是操作系统为线程单独提供的私有空间,通常只有有限的容量。通常通过pthread库中的:pthread_key_create(),pthread_getspecific(),pthread_setspecific(),pthread_key_delete()等API来存取一些数据或保存一些状态。简单的理解就是给线程也提供了一些KVC的存取操作的能力。这里主要存储就是两个值SYNC_DATA_DIRECT_KEY:SyncData *data和SYNC_COUNT_DIRECT_KEY:uintptr_t lockCount。这里先看下SyncData的数据结构:

typedef struct alignas(CacheLineSize) SyncData {struct SyncData* nextData;DisguisedPtr object;int32_t threadCount;// number of THREADS using this blockrecursive_mutex_t mutex;} SyncData;

SyncData内部实际是是一个单链表结构,每一个结构都会指向nextData;threadCount来记录当前线程数,object即传入的参数,mutex核心的锁结构。接下来,如果找到了data,将data赋值给result,同时将lockCount ++或者lockCount--,并通过tls_set_direct()更新到对应的key。当lockCount == 0时,会将线程缓存值SYNC_DATA_DIRECT_KEY置为NULL,同时threadCount --;第2部分:这里其实还是读取缓存,还是先看下SyncCache数据结构:

typedef struct {SyncData *data;unsigned int lockCount;// number of times THIS THREAD locked this block} SyncCacheItem;typedef struct SyncCache {unsigned int allocated;unsigned int used;SyncCacheItem list[0];} SyncCache;

只是这里查询的结构有所不同,其最终还是去查询SyncData和lockCount。然后其他的增减逻辑就一模一样了。第三部分:

//Thread cache didn't find anything.//Walk in-use list looking for matching object

第四部分:

// Allocate a new SyncData and add to list.首次执行,新创建 SyncData,// Save in fast thread cache// Save in thread cache

最后通过一个图标来总结下:

图1图2 内部关联逻辑

从图中可以看出:

锁的内部首先是一个SyncCache结构体,包含着一个list数组,存放着SyncCacheItem类型的数据,该元素表示某一个线程被锁定的任务;SyncCacheItem结构包含一个SyncData *data,和int lockCount,其中lockCount记录这这条线程上锁的总数,data记录着被锁定的任务;SyncData,内部包含了一个nextData,也是SyncData类型,表示下一个被锁定的任务,所以这里是一个单链表的结构,来记录整个任务链。使用中的注意事项

@synchronized,由于需要各种链表的查询,创建等操作,所以在整个iOS锁的性能排名上还是比较靠后的。但由于它使用起来非常的简单,而且不需要做额外的上锁解锁操作,所以在项目中使用频率还是挺高的。先写一个案例:

- (void)demo33{for (int i = 0; i < 100000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{self->_testArray = [NSMutableArray array];});}}

在执行demo33时,会发生EXC_ACCESS崩溃

截屏2020-11-10 下午4.32.49.png这是由于多个异步线程在给self.testArray赋值,即retain新值,release旧值,但在某一个瞬间,retain和release在多个异步线程的情况下,发生资源抢占,导致没有成对执行,而导致了release野指针的情况,如下:

2020-11-10 16:37:08.198551+0800 TestApp[45349:8517346] @synchronized2020-11-10 16:37:08.364108+0800 TestApp[45349:8517491] *** -[__NSArrayM release]: message sent to deallocated instance 0x6000032b0ae02020-11-10 16:37:08.364115+0800 TestApp[45349:8517494] *** -[__NSArrayM release]: message sent to deallocated instance 0x60000324cea0

接下来,我们给代码添加一个@synchronized锁

- (void)demo33{for (int i = 0; i < 100000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{@synchronized (self.testArray) {self.testArray = [NSMutableArray array];}});}}

此时还是发生了崩溃,

截屏2020-11-10 下午4.40.16.png同样是由于objc_release去释放了一个野指针导致。这里要注意的就是@synchronized()的参数了,self.testArray在多次的赋值过程中,在某个瞬间可能为nil,而@synchronized(nil)意味着加锁失败,就相当于没有锁,所以这里崩溃的情况就跟之前的一样了。再次修改代码,把@synchronized()传入的参数修改为self:

- (void)demo33{for (int i = 0; i < 100000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{@synchronized (self) {self.testArray = [NSMutableArray array];}});}}

此时执行代码,结果显示正常。所以这里要注意,当使用@synchronized锁时,要保证该锁传入的参数在使用期间不能为nil,否则锁将失去意义,比如这里使用生命周期更长的self,但这里不仅限于self,只要保证在使用期间不为nil就是可以的。再者就是基于@synchronized的性能表现不是很好,所以还是尽量选择其他锁来替代它。

总结

这篇文章有问题,写的不好!!!!摸不着头脑!!!!谨慎阅读!!!!

055与提康德罗加差距0213844上海哪的催收06Cr19Ni10不锈钢板06Cr25Ni20是什么材质EDD尽职调查表加强反洗钱客户尽职调查工作的几点建议校花的贴身高手第3057章断林逸的财路校花的贴身高手第7438章状告司马逸0852在线阅读0381是哪的区号拼多多拒收钱怎么退回拒收有什么后果属兔的今年多大属龙的今年多大文案句子网第420章采购域名后缀mobi是什么意思07级会计学专升本管理成本会计模拟试卷041奴隶拍卖场057迫不得已0289家庭责任调侃老板的语录顾汐龙禹辰免费阅读03明星模特套图073章一飞冲天