在 iOS 開(kāi)發(fā)常見(jiàn)的幾種鎖 介紹了常見(jiàn)的幾種鎖的使用場(chǎng)景以及使用方法,它的底層是如何實(shí)現(xiàn)的呢拇涤?下面我們帶著疑問(wèn)一起去探索下 @synchronized
的底層原理吧
@synchronized
開(kāi)發(fā)中,在多個(gè)線程訪問(wèn)同一塊資源的時(shí)候,我們會(huì)添加以下代碼來(lái)避免引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全的問(wèn)題
@synchronized (self) {
//添加執(zhí)行的代碼
}
那我們?nèi)绾稳ヌ剿魉牡讓訉?shí)現(xiàn)呢?首先我們需要它在底層會(huì)調(diào)用什么方法,其次我們要知道方法所在的源碼庫(kù)穴翩,這樣我們才能清晰的知道它的底層是如何實(shí)現(xiàn)的。
底層方法調(diào)用探究
通過(guò)開(kāi)啟匯編調(diào)試锦积,我們看到如下 @synchronized
在執(zhí)行過(guò)程中芒帕,會(huì)走底層的objc_sync_enter
和 objc_sync_exit
方法
此時(shí)我們對(duì) objc_sync_enter
方法下一個(gè)符號(hào)斷點(diǎn),發(fā)現(xiàn)底層實(shí)現(xiàn)所在的源碼庫(kù)是 libobjc.A.dylib
objc_sync_enter & objc_sync_exit
打開(kāi) objc-781
源碼工程丰介,查看 objc_sync_enter
的源碼實(shí)現(xiàn)如下:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 執(zhí)行 ACQUIRE 操作背蟆,返回 data 數(shù)據(jù)
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
// 加鎖
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
再查看 objc_sync_exit
的源碼,實(shí)現(xiàn)如下:
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 執(zhí)行 RELEASE 操作哮幢,返回 data 數(shù)據(jù)
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;
}
從源碼中我們可以看到带膀,如果傳入的 obj
為 nil
,則什么都不做橙垢;如果傳入的 obj
不為 nil
垛叨,則會(huì)獲取相應(yīng)的 SyncData
對(duì)它進(jìn)行一系列的操作。那么這個(gè) SyncData
是什么柜某?它的結(jié)構(gòu)是什么樣的嗽元?SyncData
的定義如下:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
從 SyncData
的定義可以看到,它是一個(gè)結(jié)構(gòu)體喂击。第一個(gè)成員變量指向下一個(gè) SyncData
还棱,是一個(gè)鏈表結(jié)構(gòu);第四個(gè)成員屬性代表遞歸屬性惭等。從 SyncData
結(jié)構(gòu)就可以看出 @synchronized
是一個(gè)遞歸互斥鎖珍手。
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;
這里順便查看 SyncCache
的結(jié)構(gòu),后續(xù)會(huì)調(diào)用到辞做。SyncCache
是一個(gè)結(jié)構(gòu)體對(duì)象琳要,用于存儲(chǔ)線程。其中 list[0]
表示當(dāng)前線程的鏈表 data
秤茅,主要用于存儲(chǔ) SyncData
和 lockCount
id2data 源碼分析
這里源碼很長(zhǎng)稚补,先總體看下流程,后面詳細(xì)分析
整體分為四大步
- 快速緩存
如果支持快速緩存框喳,就從快速緩存中讀取線程和任務(wù)课幕,再進(jìn)行相應(yīng)操作
- 線程緩存
快速緩存沒(méi)找到,就從線程緩存中讀取線程和任務(wù)五垮,再進(jìn)行相應(yīng)操作
上述代碼中 fetch_cache
函數(shù)進(jìn)行緩存查詢(xún)和開(kāi)辟
- 循環(huán)遍歷
所有的緩存都找不到乍惊,循環(huán)遍歷每個(gè)線程和任務(wù),再進(jìn)行相應(yīng)操作
- Done
如果有錯(cuò)誤放仗,則拋出異常润绎;如果正常,則存入快速緩存(前提是支持快速緩存)和線程緩存中,便于下次快速查找
每個(gè)被鎖的
object
對(duì)象可擁有一個(gè)或多個(gè)線程
拓展
以下代碼莉撇,運(yùn)行后會(huì)發(fā)生什么呢蛤?
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.testArray) {
self.testArray = [NSMutableArray array];
}
});
}
我們運(yùn)行項(xiàng)目,看看會(huì)發(fā)生什么
項(xiàng)目運(yùn)行就崩潰了棍郎,原因在與 self.testArray
會(huì)觸發(fā) set
方法其障,而 set
方法本質(zhì)是新值 retain
,舊值 release
涂佃,而在異步調(diào)用時(shí)励翼,可能會(huì)造成多次調(diào)用 release
(上一次的 release
還沒(méi)結(jié)束,下一次的 release
已經(jīng)來(lái)了)巡李,導(dǎo)致野指針抚笔,從而 crash
扶认。
- 驗(yàn)證
我們打開(kāi)Xcode 工程 -> Edit Scheme... ->Run -> diagnostics -> 勾選 Zombie Objects侨拦,再次運(yùn)行項(xiàng)目
調(diào)用 [__NSArrayM release]
時(shí),是發(fā)送給了 deallocated
(已析構(gòu)釋放)的對(duì)象辐宾。
僵尸對(duì)象是一種用來(lái)檢測(cè)內(nèi)存錯(cuò)誤(EXC_BAD_ACCESS)的狱从,給僵尸對(duì)象發(fā)送消息時(shí),那么將在運(yùn)行期間崩潰和輸出錯(cuò)誤日志叠纹。通過(guò)日志可以定位到野指針對(duì)象調(diào)用的方法和類(lèi)名季研。
- 添加 @synchronized
既然我們知道了原因,那我們使用 @synchronized
加鎖試一下吧(這是 @synchronized 錯(cuò)誤示范??)
NSLog(@"123");
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.testArray) {
self.testArray = [NSMutableArray array];
}
});
}
再次運(yùn)行項(xiàng)目誉察,還是 crash
了与涡,報(bào)錯(cuò)信息和上面的一致,這是為什么呢持偏?在上面的源碼分析中可以看到驼卖,因?yàn)殒i的對(duì)象是 self.testArray
,它會(huì) release
鸿秆,它釋放了酌畜,等于鎖也就沒(méi)用了。
正確的使用方法
NSLog(@"123");
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) {
self.testArray = [NSMutableArray array];
}
});
}
@synchronized
鎖的對(duì)象卿叽,需要確保鎖內(nèi)代碼的生命周期桥胞。所以將鎖對(duì)象改為self。就解決問(wèn)題了考婴。當(dāng)然也可以用其他鎖來(lái)解決問(wèn)題