iOS多线程同时操作同一内存造成野指针
iOS多線程同時操作同一內存造成野指針
iOS多線程同時操作同一內存造成野指針,原因:崩潰線程崩中使用指針的真正創建與銷毀地方在另另外一個線程中,崩潰線程只是使用這個指針拷貝。
這兩個操作發送在兩個線程中。
問題總結:
對于野指針問題,當問題根源找到時覺得問題比較輕松,但未找到前的排查真正做起來很累,更多是考驗定位人員的心理素質和分析能力,總結的一些經驗如下:
1、需要從崩潰點上層各個調用對象作為中介從來源到去處引起共用指針的,要細心、耐心,把來源和去處一層層追根朔源才能發現問題。
2、要善于分析日志文件中提供的信息,當第一次崩潰是由于日志輸出等級低信息量不足并且不能定位與解決該問題時候需要將日志輸出等級調高并加入一些輔助定位的輸出信息,在下一次崩潰時候輸出的日志信息將提供很大幫助。
3、解決野指針問題通過閱讀代碼很重要,只有這樣才能對出問題時候程序線程運行的數量、運行功能和時序以及變量調用有清楚和全面的認識。
4、編寫程序時候盡量少用指針拷貝,如果不得以使用,編寫代碼一定要具備要有多線程運行意識,從根源上杜絕野指針的出現。
————————————————
版權聲明:本文為CSDN博主「lezhiyong」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/lezhiyong/article/details/46697703
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, ^{
});
從代碼中不難發現,由于主隊列是全局共用的,所以如果這次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();
}
通過上述代碼不難理解,系統會自動獲取當前線程正在執行的隊列的。如果進行該隊列進行過標記,就根據我們傳入的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;
});
}
3. 遍歷所有的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];
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];}
for (unsigned int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];}
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。
總結
以上是生活随笔為你收集整理的iOS多线程同时操作同一内存造成野指针的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP资格证书查询系统源码 自动生成二维
- 下一篇: 问题解决:printf()函数无法打印