iOS多線程讀寫崩潰分析
最近再次遇到多線程讀寫導致的crash 問題,寫了一個測試demo,記錄分析過程。
for (int i = 0; i < 10000; i++)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.object = [TestObject new];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.object = [TestObject new];
});
}
上面是暴力重現多線程讀寫的崩潰,在debug環境下,開啓zombie ,窗口會輸出:
message sent to deallocated instance 0x170200c50
上面用了10000次碰撞才觸發崩潰,日常debug 環境下很難出現。但是到了線上環境,用戶量一大,問題就出現了。然後,我們只能通過崩潰日誌查找崩潰。
下面截取有用的崩潰日誌部分:
Incident Identifier: A22F5FFF-F98D-4F3B-95C3-45790E61F049
CrashReporter Key: 33c3939d695bcfab6c9a16efca18399fae8a83c3
Hardware Model: iPhone6,2
Process: Crash_mulThread [716]
Path: /private/var/containers/Bundle/Application/7CCB0B27-4B51-4D77-B571-A49153C8E8B7/Crash_mulThread.app/Crash_mulThread
Identifier: vedon.Crash-mulThread
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: vedon.Crash-mulThread [1266]
Date/Time: 2017-05-05 23:58:50.9184 +0800
Launch Time: 2017-05-05 23:58:50.6346 +0800
OS Version: iPhone OS 10.2.1 (14D27)
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00000003a42abec8
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 4
Filtered syslog:
None found
Thread 4 name: Dispatch queue: com.apple.root.default-qos
Thread 4 Crashed:
0 libobjc.A.dylib 0x0000000184f48894 objc_class::demangledName(bool) + 28
1 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
2 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
3 Crash_mulThread 0x00000001000e03f4 -[ViewController setObject:] (ViewController.m:13)
4 Crash_mulThread 0x00000001000e0148 __29-[ViewController viewDidLoad]_block_invoke (ViewController.m:29)
5 libdispatch.dylib 0x00000001853921fc _dispatch_call_block_and_release + 24
6 libdispatch.dylib 0x00000001853921bc _dispatch_client_callout + 16
7 libdispatch.dylib 0x00000001853a0a4c _dispatch_queue_override_invoke + 732
8 libdispatch.dylib 0x00000001853a234c _dispatch_root_queue_drain + 572
9 libdispatch.dylib 0x00000001853a20ac _dispatch_worker_thread3 + 124
10 libsystem_pthread.dylib 0x000000018559b2a0 _pthread_wqthread + 1288
11 libsystem_pthread.dylib 0x000000018559ad8c start_wqthread + 4
Thread 4 crashed with ARM Thread State (64-bit):
x0: 0x00000003a42abea8 x1: 0x0000000000000000 x2: 0x000000017401a1f0 x3: 0x000000017401a200
x4: 0x00000001700f0e00 x5: 0x0000000000000000 x6: 0x0000000000000000 x7: 0x0000000000000000
x8: 0xbaddf653a42abead x9: 0x000009a1000e5665 x10: 0xffffe9a1000e5665 x11: 0x000000330000007f
x12: 0x000000010101e110 x13: 0x000005a1000e554d x14: 0x00000001a597a340 x15: 0x0000000000397c01
x16: 0x0000000184f580f4 x17: 0x00000001000e03b4 x18: 0x0000000000000000 x19: 0x0000000170019be0
x20: 0x00000003a42abea8 x21: 0x0000000000000000 x22: 0x0000000000000000 x23: 0x00000001aa54d200
x24: 0x000000016e1bf0e0 x25: 0x00000001abc326c0 x26: 0x0000000000000014 x27: 0x0000000000000004
x28: 0xffffffffffffffff fp: 0x000000016e1bed10 lr: 0x0000000184f5bc04
sp: 0x000000016e1becd0 pc: 0x0000000184f48894 cpsr: 0x80000000
Binary Images:
0x1000d8000 - 0x1000e3fff Crash_mulThread arm64 <e2e3d2adf95930e19b6da09621898c31> /var/containers/Bundle/Application/7CCB0B27-4B51-4D77-B571-A49153C8E8B7/Crash_mulThread.app/Crash_mulThread
0x1001dc000 - 0x10020bfff dyld arm64 <f54ed85a94253887886a8028e20ed8ba> /usr/lib/dyld
0x184ebc000 - 0x184ebdfff libSystem.B.dylib arm64 <1b4d75209f4a37969a9575de48d48668> /usr/lib/libSystem.B.dylib
0x184ebe000 - 0x184f13fff libc++.1.dylib arm64 <b2db8b1d09283b7bafe1b2933adc5dfd> /usr/lib/libc++.1.dylib
0x184f14000 - 0x184f34fff libc++abi.dylib arm64 <e3419bbaface31b5970c6c8d430be26d> /usr/lib/libc++abi.dylib
0x184f38000 - 0x185311fff libobjc.A.dylib arm64 <538f809dcd7c35ceb59d99802248f045> /usr/lib/libobjc.A.dylib
FYI
SIGSEGV 訪問了非法的地址(地址還沒有從系統映射到當前進程的內存空間), 一般是野指針導致, 而野指針一般由於多線程操作對象導致.
SIGABRT 一般是Exception或者其他的代碼主動退出的問題.
SIGTRAP 代碼裏面觸發了調試指令, 該指令可能由編譯器提供的trap方法觸發, 如'__builtin_trap()'
SIGBUS 一般由於地址對齊問題導致, 單純的OC代碼挺難觸發的, 主要是系統庫方法或者其他c實現的方法導致
SIGILL 表示執行了非法的cpu指令, 但是一般是由於死循環導致
通過崩潰日誌,定位到崩潰的點在:
0 libobjc.A.dylib 0x0000000184f48894 objc_class::demangledName(bool) + 28
1 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
2 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
3 Crash_mulThread 0x00000001000e03f4 -[ViewController setObject:] (ViewController.m:13)
每條崩潰堆棧的記錄稱爲frame ,每個frame 都有一個編號,它是當前frame 在整個調用棧的索引。看到frame 3 是demo代碼調用的地方,當前pc 地址** 0x0000000184f48894** 對應frame 0 調用地址,而其他的frame 都是歷史記錄,不會保存當前frame所有寄存器的值,只存了lr 寄存器的內容(FYI: lr 是方法調用完之後,要返回的地址)。
從frame 2 就可以知道,對象被over release 了。實際情況一般是:丟失重要的堆棧信息。下面純粹是在只有frame 3 的堆棧下,怎麼定位問題。
frame 3 ,只有一個 setObject:也就是: self.object = [TestObject new]; 咋一看,不怎麼可能崩潰。下面來分析一下:
可以看到堆棧地址是: 0x00000001000e03f4,程序加載到內存的地址在0x1000d8000 - 0x1000e3fff 之間。
通過計算 0x00000001000e03f4 - 0x1000d8000 = 0x83F4。
0x83F4 爲相對偏移,這時候使用hopper 看看在0x83F4 究竟是什麼。
Screen Shot 2017-05-06 at 12.40.14 AM.png
frame 3 的lr 寄存器保存了調用方法的下一個指令地址,那麼可以確定崩潰發生在:imp___stubs__objc_storeStrong,下面分析一下這段彙編做了什麼。
00000001000083b4 sub sp, sp, #0x30 ; Objective C Implementation defined at 0x10000c478 (instance method), DATA XREF=0x10000c478
00000001000083b8 stp x29, x30, [sp, #0x20]
00000001000083bc add x29, sp, #0x20
// 保存方法調用的現場
00000001000083c0 adrp x8, #0x10000d000
00000001000083c4 add x8, x8, #0x538 ; _OBJC_IVAR_$_ViewController._object
// 動態定位獲取ViewController._object的描述地址, 放入x8
00000001000083c8 stur x0, [x29, #-0x8]
00000001000083cc str x1, [sp, #0x10]
00000001000083d0 str x2, [sp, #0x8]
// 把參數self/selector/傳進來的TestObject對象, 存到棧裏
00000001000083d4 ldr x0, [sp, #0x8]
00000001000083d8 ldur x1, [x29, #-0x8]
00000001000083dc ldrsw x8, x8
00000001000083e0 add x8, x1, x8
// 從x8裏把_object的在ViewController對象的偏移量取出來, 並與x1相加, 也就是`self指針+偏移量`, 結果存在x8 裏面
00000001000083e4 str x0, sp
// 把傳進來的對象存入棧
00000001000083e8 mov x0, x8
// 把`self指針+偏移量`指針放入x0
00000001000083ec ldr x1, sp
// 把傳進來的對象從棧裏取出來放到x1
00000001000083f0 bl imp___stubs__objc_storeStrong
// 把x1裏傳進來的對象賦值給x0, 然後強引用一次
00000001000083f4 ldp x29, x30, [sp, #0x20]
00000001000083f8 add sp, sp, #0x30
// 恢復最前面保存的現場
00000001000083fc ret
// 返回
上面其實就是一段setter 的代碼,崩潰發生在imp___stubs__objc_storeStrong,通過查看蘋果開源代碼:objc_storeStrong
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
objc_storeStrong 並不是原子性操作,當線程A可能執行到*location = obj 時,另外一個線程B執行 prev = *location; 。那麼當線程A繼續執行到objc_release(prev); 線程B 繼續執行 ,跑到objc_release(prev), 此刻,prev已經被釋放過了。Crash ~~~
========================================
iOS多線程同時操作同一內存造成野指針,原因:崩潰線程崩中使用指針的真正創建與銷燬地方在另另外一個線程中,崩潰線程只是使用這個指針拷貝。
這兩個操作發送在兩個線程中。
問題總結:
對於野指針問題,當問題根源找到時覺得問題比較輕鬆,但未找到前的排查真正做起來很累,更多是考驗定位人員的心理素質和分析能力,總結的一些經驗如下:
1、需要從崩潰點上層各個調用對象作爲中介從來源到去處引起共用指針的,要細心、耐心,把來源和去處一層層追根朔源才能發現問題。
2、要善於分析日誌文件中提供的信息,當第一次崩潰是由於日誌輸出等級低信息量不足並且不能定位與解決該問題時候需要將日誌輸出等級調高並加入一些輔助定位的輸出信息,在下一次崩潰時候輸出的日誌信息將提供很大幫助。
3、解決野指針問題通過閱讀代碼很重要,只有這樣才能對出問題時候程序線程運行的數量、運行功能和時序以及變量調用有清楚和全面的認識。
4、編寫程序時候儘量少用指針拷貝,如果不得以使用,編寫代碼一定要具備要有多線程運行意識,從根源上杜絕野指針的出現。
iOS多線程同時操作同一內存造成野指針,一個解決方案。
什麼是多線程的野指針問題
之前在《淺談多線程編程誤區》一文中,曾經舉過如下這樣的多線程setter例子:
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.data = [[NSMutableData alloc] init];
});
}
如果這個self.data是個nonatomic的屬性的話,就會造成在多次釋放導致的野指針問題。(具體可以見《淺談多線程編程誤區》的原理解釋)。
從原理解釋中不難發現,本質上會產生野指針的場景是由於我們沒有對臨界區進行保護。導致賦值替換的操作不是原子性的。
有些人會說,例子中你刻意構建了一萬個線程纔會導致Crash。而我們平時就用用幾個線程,不會有問題的。
理論上一萬個線程只不過是把兩個線程中可能出現問題的概率放大了而已。在一萬個線程中會出現的多線程野指針問題在兩個線程中一定也會發生。
傳統業界方案:賦值加鎖
既然原子性是導致野指針的罪魁禍首,那麼我們只要在對應可能產生衝突的臨界區內加鎖就好了,比如:
[lock lock];
self.data = [[NSMutableData alloc] init];
[lock unlock]
按照這樣的做法,同一時間不管有多少線程試圖對self.data進行賦值,最終都只有一個線程能夠搶到鎖對其賦值。
但是這樣的做法從安全性角度來說是解決了原子賦值的問題。但是這樣的做法卻對開發要求比較嚴格,因爲任意非基礎類型的對象(Int, Bool)都有可能產生多線程賦值的野指針,所以開發需要牢記自身的屬性變量究竟有哪些會在多線程場景中被使用到。
而且,這樣的方案還有一個非常大的不確定性!
當你開發了一個底層SDK,對外暴露了一些公共的readwrite的Property。別人對你的property賦值的時候,你怎麼確定他們一定會做到線程安全?
我的方案:runtime追蹤對象初始化的GCD Queue
我們都知道,在Objective-C中,對於一個property的賦值最終都會轉化成對於ivar的setter方法。所以,如果我們能確保setter方法的線程安全性,就能確保多線程賦值不會產生野指針。
好,按照這個思路進行操作的話,我們大致需要如下幾個步驟:
獲取第一次setter調用的時機及對應的線程。
將這個線程記錄下來。
後續調用setter的時候,判斷當前setter調用的線程是不是我們之前記錄的線程,如果是,直接賦值。如果不是,派發到對應的線程進行調用。
獲取所有的setter,重複實現上述步驟。
看起來思路很簡單,具體實現起來卻有一定的難度,容我由淺入深慢慢道來:
獲取第一次賦值的線程並記錄
由於我們不能通過成員變量就記錄每個ivar對應的setter的初始化線程(這樣setter的個數就無限增長了),因此本質上我們只有通過局部靜態變量的方式來作爲存儲。同時由於我們只需要在初次執行時進行記錄,所以很理所當然就想到了dispatch_once。
具體代碼如下:
static dispatch_queue_t initQueue;
static void* initQueueKey;
static void* initQueueContext;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 1. 主隊列
if ([UIApplication isMainQueue]) {
initQueue = dispatch_get_main_queue();
initQueueKey = [UIApplication mainQueueKey];
initQueueContext = [UIApplication mainQueueContext];
} else {
// 2. 非主隊列
const char *label = [NSStringFromSelector(_cmd) UTF8String];
initQueueKey = &initQueueKey;
initQueueContext = &initQueueContext;
initQueue = dispatch_queue_create(label, nil);
dispatch_queue_set_specific(initQueue, initQueueKey, initQueueContext, nil);
}
});
從代碼中不難發現,由於主隊列是全局共用的,所以如果這次setter的賦值是在主隊列進行的,那麼就直接複用主隊列即可;而如果當前的隊列我們自身都不確定的話,那麼就乾脆開闢一個串行的隊列用語這個setter的後續賦值,並將其記錄下來。
細心的讀者可能會發現,我們標題裏寫的是線程,但是在代碼中記錄的卻是GCD的隊列(Queue)。而且,我們判斷的是主隊列而不是主線程。這是爲什麼呢?
嘿嘿,容我賣個關子,文章最後會有詳細的闡述。
判斷後續賦值是否是記錄的線程
由於我們之前記錄的是隊列,所以我們是無法直接使用諸如如下代碼的方式進行是否是同一個線程的判斷
[NSThread currentThread] == xxxThread
在iOS7之前,蘋果提供了dispatch_get_current_queue()用於獲取當前正在執行的隊列,如果有這個方法,我們就可以很容易判斷這個隊列和我們記錄的隊列是否是同一個了。但是很不幸的是,該方法已經被從GCD的Public API中移除了,一時間研究陷入了僵局。
不過好在libdispatch是開源的,經過一段時間的摸索,我發現了這個方法dispatch_get_specific,其自身實現如下:
DISPATCH_NOINLINE
void *
dispatch_get_specific(const void *key)
{
if (slowpath(!key)) {
return NULL;
}
void *ctxt = NULL;
// 1. 獲取當前線程的執行隊列
dispatch_queue_t dq = _dispatch_queue_get_current();
while (slowpath(dq)) {
// 2. 如果進行過標記
if (slowpath(dq->dq_specific_q)) {
ctxt = (void *)key;
dispatch_sync_f(dq->dq_specific_q, &ctxt,
_dispatch_queue_get_specific);
if (ctxt) break;
}
// 3. 向上傳遞至target Queue
dq = dq->do_targetq;
}
return ctxt;
}
通過上述代碼不難理解,系統會自動獲取當前線程正在執行的隊列的。如果進行該隊列進行過標記,就根據我們傳入的key去獲取key對應的value(ctxt)。如果查詢到了,就返回。否則按照目標隊列層層上查,直至root_queue也沒找到爲止。(關於libdispatch的具體原理,我下週還會專門寫篇細細分析的文章)。
通過這個方法,我們可以在直接記錄初始化隊列的時候對其進行特殊的標定:
dispatch_queue_set_specific(initQueue, initQueueKey, initQueueContext, nil);
隨後在後續setter執行的時候通過如下代碼進行判斷並進行相應的直接賦值或者隊列重新派發:
// 如果是當前隊列
if (dispatch_get_specific(initQueueKey) == initQueueContext) {
_threadSafeArray = threadSafeArray;
} else {
// 不是當前隊列
dispatch_sync(initQueue, ^{
_threadSafeArray = threadSafeArray;
});
}
- 遍歷所有的setter,重複上述過程
由於我們的目的是減輕其他開發的負擔,所以不得不借助了runtime的Method Swizzling技術。但是傳統的Method Swizzling技術是將函數實現兩兩交換。如果按照這個思路,我們就需要爲每一個setter編寫一個對應的hook_setter,這工作量無疑太巨大了。
所以,在這裏我們需要的一箇中心重定向的過程:即,將所有的setter都轉移到一個hook_proxy中。代碼如下:
(void)hookAllPropertiesSetter
{
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
NSMutableArray *readWriteProperties = [[NSMutableArray alloc] initWithCapacity:outCount];
for (unsigned int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
unsigned int attrCount;
objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
// !!!!!!!!!!!!!!!!!!特別注意!!!!!!!!!!!!!!!!!!
// !!!!!!!!!!!!!!!!!!特別注意!!!!!!!!!!!!!!!!!!
BOOL isReadOnlyProperty = NO;
for (unsigned int j = 0; j < attrCount; j++) {
if (attrs[j].name[0] == 'R') {
isReadOnlyProperty = YES;
break;
}
}
free(attrs);
if (!isReadOnlyProperty) {
[readWriteProperties addObject:propertyName];
}
}
free(properties);
for (NSString *propertyName in readWriteProperties) {
NSString *setterName = [NSString stringWithFormat:@"set%@%@:", [propertyName substringToIndex:1].uppercaseString, [propertyName substringFromIndex:1]];
// !!!!!!!!!!!!!!!!!!特別注意!!!!!!!!!!!!!!!!!!
// !!!!!!!!!!!!!!!!!!特別注意!!!!!!!!!!!!!!!!!!
NSString *hookSetterName = [NSString stringWithFormat:@"hook_set%@:", propertyName];
SEL originSetter = NSSelectorFromString(setterName);
SEL newSetter = NSSelectorFromString(hookSetterName);
swizzleMethod([self class], originSetter, newSetter);
}
}
在這裏有兩點需要注意的地方:
readonly的property是不具備setter功能的,所以將其過濾。
將每個setter,比如setThreadSafeArray都swizzle成了hook__setThreadSafeArray。即爲每一個setter都定製了一個對應的hook_setter。
哎,有人會問,你剛剛不才說爲每一個setter編寫對應的hook_setter是費時費力的嗎?怎麼自己打自己臉啊?
別急,容我慢慢道來。
在Method Swizzling的時候,我們需要調用class_getInstanceMethod來進行對應方法名的函數查找。整個過程簡述如下:
method cache list -> method list -> 動態方法決議 -> 方法轉交 (forward Invocation)
其中,在動態方法決議這步,如果我們添加了之前的沒找到的方法,那麼整個查找過程又會重新開始一遍。
由於那些hook_setter是壓根不會存在於method list中的,所以在查找這些函數的時候,一定會走到動態決議這一步。
基於此,我實現瞭如下的動態決議函數:
(BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selName = NSStringFromSelector(sel);
if ([selName hasPrefix:@“hook_”]) {
Method proxyMethod = class_getInstanceMethod([self class], @selector(hook_proxy:));
class_addMethod([self class], sel, method_getImplementation(proxyMethod), method_getTypeEncoding(proxyMethod));
return YES;
}
return [super resolveInstanceMethod:sel];
}
從代碼中很容易發現,如果是之前那麼hook_setter的函數名,我就講這些方法的函數實現全部重定向到函數hook__proxy上。
尋找上下文
在傳統的Method Swizzling技術中,由於我們是兩兩交換,因此我們不需要上下文這一個步驟,直接調用hook_setter就可以重新返回對應的原setter方法。
可是在本文的實現中,由於我們將所有的setter都重定向到了hook__proxy中,所以我們需要在hook_proxy中尋找究竟是給哪個property賦值。
如果對Method Swizzling的理解只停留在表面,是很難想到後續步驟的。
Method Swizzling的原理是隻是交換IMP,即函數實現。而我們在Objective-C的函數調用統統是通過objc_msgSend結合函數的Selector(可以簡單理解爲函數名)來找到真正的函數實現。
因此,swizzle後的Selector沒變,變的是IMP。
有了這個理解,我們就可以在hook_proxy使用__cmd這個隱藏變量,它會指引我們究竟是哪個Setter當前正在被調用,具體代碼如下:
(void)hook_proxy:(NSObject *)proxyObject
{
// 只是實現被換了,但是selector還是沒變
NSString *originSelector = NSStringFromSelector(_cmd);
NSString *propertyName = [[originSelector stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@":"]] stringByReplacingOccurrencesOfString:@“set” withString:@""];
if (propertyName.length <= 0) return;
NSString *ivarName = [NSString stringWithFormat:@"_%@%@", [propertyName substringToIndex:1].lowercaseString, [propertyName substringFromIndex:1]];
//NSLog(@“hook_proxy is %@ for property %@”, proxyObject, propertyName);
重複之前步驟即可。
}
其他拓展
本文中只是探索了下沒有重載setter的那些ivar,因此只需要簡單對ivar進行賦值即可。
如果你碰到了大量自定義setter的ivar,那麼也一樣很簡單,你只需要維護一個ivar 到對應自定義的setter的imp映射,在hook_proxy將setValue:ForKey:替換成直接的IMP調用即可。
一些額外細節
線程和GCD Queue並不是一一對應的關係。
前面提到了,我們要記錄的是隊列而不是線程。相信很多人可能一開始都不能理解,那麼我用如下這樣的代碼進行解釋:
if ([NSThread isMainThread]) {
[self doSomeThing];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomething];
});
}
上述代碼想必大家非常熟悉,就是全包在主線程執行一些操作,比如UI操作等等。但是事實上,這裏有個誤區:
主隊列一定在主線程執行,而主線程不一定只執行主隊列。
換句話說:上述代碼的if 和 else是不等價的。
有時候,主線程有可能會被調度到執行其他隊列(其他線程亦是如此),比如如下代碼:
// 在主線程創建
dispatch_queue_t dq = dispatch_queue_create(‘com.mingyi.dashuaibi’, NULL);
dispatch_sync(dq, ^{
NSLog(@“current thread is %@”, [NSThread currentThread]);
});
具體效果,大家可以自己嘗試下,看看Log輸出的結果是不是主線程。
爲什麼不能直接將所有的setter直接hook到hook_proxy,非要通過動態決議來進行。
我們舉個簡單的例子,假設我們有兩個property,分別叫A和B。那麼在執行下述代碼的時候:
for (int i = 0; i < 2; i++) {
SEL originSetter = NSSelectorFromString(setterName);
SEL newSetter = NSSelectorFromString(hook_proxy);
swizzleMethod([self class], originSetter, newSetter);
}
第一次交換的時候,Setter A的 IMP和 hook_proxy的 IMP進行了交換,這一步沒問題。
第二次交換的時候,Setter B的 IMP和 hook_proxy的 IMP進行了交換,而此時hook_proxy的IMP已經指向了Setter A的IMP,因此導致的結果就是交換錯亂了,調用setter B實質上是調用了setter A。