iOS多线程同时操作同一内存造成野指针
iOS多線程同時(shí)操作同一內(nèi)存造成野指針
iOS多線程同時(shí)操作同一內(nèi)存造成野指針,原因:崩潰線程崩中使用指針的真正創(chuàng)建與銷毀地方在另另外一個(gè)線程中,崩潰線程只是使用這個(gè)指針拷貝。
這兩個(gè)操作發(fā)送在兩個(gè)線程中。
問題總結(jié):
對(duì)于野指針問題,當(dāng)問題根源找到時(shí)覺得問題比較輕松,但未找到前的排查真正做起來很累,更多是考驗(yàn)定位人員的心理素質(zhì)和分析能力,總結(jié)的一些經(jīng)驗(yàn)如下:
1、需要從崩潰點(diǎn)上層各個(gè)調(diào)用對(duì)象作為中介從來源到去處引起共用指針的,要細(xì)心、耐心,把來源和去處一層層追根朔源才能發(fā)現(xiàn)問題。
2、要善于分析日志文件中提供的信息,當(dāng)?shù)谝淮伪罎⑹怯捎谌罩据敵龅燃?jí)低信息量不足并且不能定位與解決該問題時(shí)候需要將日志輸出等級(jí)調(diào)高并加入一些輔助定位的輸出信息,在下一次崩潰時(shí)候輸出的日志信息將提供很大幫助。
3、解決野指針問題通過閱讀代碼很重要,只有這樣才能對(duì)出問題時(shí)候程序線程運(yùn)行的數(shù)量、運(yùn)行功能和時(shí)序以及變量調(diào)用有清楚和全面的認(rèn)識(shí)。
4、編寫程序時(shí)候盡量少用指針拷貝,如果不得以使用,編寫代碼一定要具備要有多線程運(yùn)行意識(shí),從根源上杜絕野指針的出現(xiàn)。
————————————————
版權(quán)聲明:本文為CSDN博主「lezhiyong」的原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/lezhiyong/article/details/46697703
iOS多線程同時(shí)操作同一內(nèi)存造成野指針,一個(gè)解決方案。
什么是多線程的野指針問題
之前在《淺談多線程編程誤區(qū)》一文中,曾經(jīng)舉過如下這樣的多線程setter例子:
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.data = [[NSMutableData alloc] init];
});
}
如果這個(gè)self.data是個(gè)nonatomic的屬性的話,就會(huì)造成在多次釋放導(dǎo)致的野指針問題。(具體可以見《淺談多線程編程誤區(qū)》的原理解釋)。
從原理解釋中不難發(fā)現(xiàn),本質(zhì)上會(huì)產(chǎn)生野指針的場(chǎng)景是由于我們沒有對(duì)臨界區(qū)進(jìn)行保護(hù)。導(dǎo)致賦值替換的操作不是原子性的。
有些人會(huì)說,例子中你刻意構(gòu)建了一萬個(gè)線程才會(huì)導(dǎo)致Crash。而我們平時(shí)就用用幾個(gè)線程,不會(huì)有問題的。
理論上一萬個(gè)線程只不過是把兩個(gè)線程中可能出現(xiàn)問題的概率放大了而已。在一萬個(gè)線程中會(huì)出現(xiàn)的多線程野指針問題在兩個(gè)線程中一定也會(huì)發(fā)生。
傳統(tǒng)業(yè)界方案:賦值加鎖
既然原子性是導(dǎo)致野指針的罪魁禍?zhǔn)?#xff0c;那么我們只要在對(duì)應(yīng)可能產(chǎn)生沖突的臨界區(qū)內(nèi)加鎖就好了,比如:
[lock lock];
self.data = [[NSMutableData alloc] init];
[lock unlock]
按照這樣的做法,同一時(shí)間不管有多少線程試圖對(duì)self.data進(jìn)行賦值,最終都只有一個(gè)線程能夠搶到鎖對(duì)其賦值。
但是這樣的做法從安全性角度來說是解決了原子賦值的問題。但是這樣的做法卻對(duì)開發(fā)要求比較嚴(yán)格,因?yàn)槿我夥腔A(chǔ)類型的對(duì)象(Int, Bool)都有可能產(chǎn)生多線程賦值的野指針,所以開發(fā)需要牢記自身的屬性變量究竟有哪些會(huì)在多線程場(chǎng)景中被使用到。
而且,這樣的方案還有一個(gè)非常大的不確定性!
當(dāng)你開發(fā)了一個(gè)底層SDK,對(duì)外暴露了一些公共的readwrite的Property。別人對(duì)你的property賦值的時(shí)候,你怎么確定他們一定會(huì)做到線程安全?
我的方案:runtime追蹤對(duì)象初始化的GCD Queue
我們都知道,在Objective-C中,對(duì)于一個(gè)property的賦值最終都會(huì)轉(zhuǎn)化成對(duì)于ivar的setter方法。所以,如果我們能確保setter方法的線程安全性,就能確保多線程賦值不會(huì)產(chǎn)生野指針。
好,按照這個(gè)思路進(jìn)行操作的話,我們大致需要如下幾個(gè)步驟:
獲取第一次setter調(diào)用的時(shí)機(jī)及對(duì)應(yīng)的線程。
將這個(gè)線程記錄下來。
后續(xù)調(diào)用setter的時(shí)候,判斷當(dāng)前setter調(diào)用的線程是不是我們之前記錄的線程,如果是,直接賦值。如果不是,派發(fā)到對(duì)應(yīng)的線程進(jìn)行調(diào)用。
獲取所有的setter,重復(fù)實(shí)現(xiàn)上述步驟。
看起來思路很簡單,具體實(shí)現(xiàn)起來卻有一定的難度,容我由淺入深慢慢道來:
由于我們不能通過成員變量就記錄每個(gè)ivar對(duì)應(yīng)的setter的初始化線程(這樣setter的個(gè)數(shù)就無限增長了),因此本質(zhì)上我們只有通過局部靜態(tài)變量的方式來作為存儲(chǔ)。同時(shí)由于我們只需要在初次執(zhí)行時(shí)進(jìn)行記錄,所以很理所當(dāng)然就想到了dispatch_once。
具體代碼如下:
static dispatch_queue_t initQueue;
static void* initQueueKey;
static void* initQueueContext;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
從代碼中不難發(fā)現(xiàn),由于主隊(duì)列是全局共用的,所以如果這次setter的賦值是在主隊(duì)列進(jìn)行的,那么就直接復(fù)用主隊(duì)列即可;而如果當(dāng)前的隊(duì)列我們自身都不確定的話,那么就干脆開辟一個(gè)串行的隊(duì)列用語這個(gè)setter的后續(xù)賦值,并將其記錄下來。
細(xì)心的讀者可能會(huì)發(fā)現(xiàn),我們標(biāo)題里寫的是線程,但是在代碼中記錄的卻是GCD的隊(duì)列(Queue)。而且,我們判斷的是主隊(duì)列而不是主線程。這是為什么呢?
嘿嘿,容我賣個(gè)關(guān)子,文章最后會(huì)有詳細(xì)的闡述。
由于我們之前記錄的是隊(duì)列,所以我們是無法直接使用諸如如下代碼的方式進(jìn)行是否是同一個(gè)線程的判斷
[NSThread currentThread] == xxxThread
在iOS7之前,蘋果提供了dispatch_get_current_queue()用于獲取當(dāng)前正在執(zhí)行的隊(duì)列,如果有這個(gè)方法,我們就可以很容易判斷這個(gè)隊(duì)列和我們記錄的隊(duì)列是否是同一個(gè)了。但是很不幸的是,該方法已經(jīng)被從GCD的Public API中移除了,一時(shí)間研究陷入了僵局。
不過好在libdispatch是開源的,經(jīng)過一段時(shí)間的摸索,我發(fā)現(xiàn)了這個(gè)方法dispatch_get_specific,其自身實(shí)現(xiàn)如下:
DISPATCH_NOINLINE
void *
dispatch_get_specific(const void *key)
{
if (slowpath(!key)) {
return NULL;
}
void *ctxt = NULL;
// 1. 獲取當(dāng)前線程的執(zhí)行隊(duì)列
dispatch_queue_t dq = _dispatch_queue_get_current();
}
通過上述代碼不難理解,系統(tǒng)會(huì)自動(dòng)獲取當(dāng)前線程正在執(zhí)行的隊(duì)列的。如果進(jìn)行該隊(duì)列進(jìn)行過標(biāo)記,就根據(jù)我們傳入的key去獲取key對(duì)應(yīng)的value(ctxt)。如果查詢到了,就返回。否則按照目標(biāo)隊(duì)列層層上查,直至root_queue也沒找到為止。(關(guān)于libdispatch的具體原理,我下周還會(huì)專門寫篇細(xì)細(xì)分析的文章)。
通過這個(gè)方法,我們可以在直接記錄初始化隊(duì)列的時(shí)候?qū)ζ溥M(jìn)行特殊的標(biāo)定:
dispatch_queue_set_specific(initQueue, initQueueKey, initQueueContext, nil);
隨后在后續(xù)setter執(zhí)行的時(shí)候通過如下代碼進(jìn)行判斷并進(jìn)行相應(yīng)的直接賦值或者隊(duì)列重新派發(fā):
// 如果是當(dāng)前隊(duì)列
if (dispatch_get_specific(initQueueKey) == initQueueContext) {
_threadSafeArray = threadSafeArray;
} else {
// 不是當(dāng)前隊(duì)列
dispatch_sync(initQueue, ^{
_threadSafeArray = threadSafeArray;
});
}
3. 遍歷所有的setter,重復(fù)上述過程
由于我們的目的是減輕其他開發(fā)的負(fù)擔(dān),所以不得不借助了runtime的Method Swizzling技術(shù)。但是傳統(tǒng)的Method Swizzling技術(shù)是將函數(shù)實(shí)現(xiàn)兩兩交換。如果按照這個(gè)思路,我們就需要為每一個(gè)setter編寫一個(gè)對(duì)應(yīng)的hook_setter,這工作量無疑太巨大了。
所以,在這里我們需要的一個(gè)中心重定向的過程:即,將所有的setter都轉(zhuǎn)移到一個(gè)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);}
}
在這里有兩點(diǎn)需要注意的地方:
readonly的property是不具備setter功能的,所以將其過濾。
將每個(gè)setter,比如setThreadSafeArray都swizzle成了hook__setThreadSafeArray。即為每一個(gè)setter都定制了一個(gè)對(duì)應(yīng)的hook_setter。
哎,有人會(huì)問,你剛剛不才說為每一個(gè)setter編寫對(duì)應(yīng)的hook_setter是費(fèi)時(shí)費(fèi)力的嗎?怎么自己打自己臉啊?
別急,容我慢慢道來。
在Method Swizzling的時(shí)候,我們需要調(diào)用class_getInstanceMethod來進(jìn)行對(duì)應(yīng)方法名的函數(shù)查找。整個(gè)過程簡述如下:
method cache list -> method list -> 動(dòng)態(tài)方法決議 -> 方法轉(zhuǎn)交 (forward Invocation)
其中,在動(dòng)態(tài)方法決議這步,如果我們添加了之前的沒找到的方法,那么整個(gè)查找過程又會(huì)重新開始一遍。
由于那些hook_setter是壓根不會(huì)存在于method list中的,所以在查找這些函數(shù)的時(shí)候,一定會(huì)走到動(dòng)態(tài)決議這一步。
基于此,我實(shí)現(xiàn)了如下的動(dòng)態(tài)決議函數(shù):
-
(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];
}
從代碼中很容易發(fā)現(xiàn),如果是之前那么hook_setter的函數(shù)名,我就講這些方法的函數(shù)實(shí)現(xiàn)全部重定向到函數(shù)hook__proxy上。
在傳統(tǒng)的Method Swizzling技術(shù)中,由于我們是兩兩交換,因此我們不需要上下文這一個(gè)步驟,直接調(diào)用hook_setter就可以重新返回對(duì)應(yīng)的原setter方法。
可是在本文的實(shí)現(xiàn)中,由于我們將所有的setter都重定向到了hook__proxy中,所以我們需要在hook_proxy中尋找究竟是給哪個(gè)property賦值。
如果對(duì)Method Swizzling的理解只停留在表面,是很難想到后續(xù)步驟的。
Method Swizzling的原理是只是交換IMP,即函數(shù)實(shí)現(xiàn)。而我們?cè)贠bjective-C的函數(shù)調(diào)用統(tǒng)統(tǒng)是通過objc_msgSend結(jié)合函數(shù)的Selector(可以簡單理解為函數(shù)名)來找到真正的函數(shù)實(shí)現(xiàn)。
因此,swizzle后的Selector沒變,變的是IMP。
有了這個(gè)理解,我們就可以在hook_proxy使用__cmd這個(gè)隱藏變量,它會(huì)指引我們究竟是哪個(gè)Setter當(dāng)前正在被調(diào)用,具體代碼如下:
-
(void)hook_proxy:(NSObject *)proxyObject
{
// 只是實(shí)現(xiàn)被換了,但是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);
重復(fù)之前步驟即可。
}
本文中只是探索了下沒有重載setter的那些ivar,因此只需要簡單對(duì)ivar進(jìn)行賦值即可。
如果你碰到了大量自定義setter的ivar,那么也一樣很簡單,你只需要維護(hù)一個(gè)ivar 到對(duì)應(yīng)自定義的setter的imp映射,在hook_proxy將setValue:ForKey:替換成直接的IMP調(diào)用即可。
一些額外細(xì)節(jié)
線程和GCD Queue并不是一一對(duì)應(yīng)的關(guān)系。
前面提到了,我們要記錄的是隊(duì)列而不是線程。相信很多人可能一開始都不能理解,那么我用如下這樣的代碼進(jìn)行解釋:
if ([NSThread isMainThread]) {
[self doSomeThing];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomething];
});
}
上述代碼想必大家非常熟悉,就是全包在主線程執(zhí)行一些操作,比如UI操作等等。但是事實(shí)上,這里有個(gè)誤區(qū):
主隊(duì)列一定在主線程執(zhí)行,而主線程不一定只執(zhí)行主隊(duì)列。
換句話說:上述代碼的if 和 else是不等價(jià)的。
有時(shí)候,主線程有可能會(huì)被調(diào)度到執(zhí)行其他隊(duì)列(其他線程亦是如此),比如如下代碼:
// 在主線程創(chuàng)建
dispatch_queue_t dq = dispatch_queue_create(‘com.mingyi.dashuaibi’, NULL);
dispatch_sync(dq, ^{
NSLog(@“current thread is %@”, [NSThread currentThread]);
});
具體效果,大家可以自己嘗試下,看看Log輸出的結(jié)果是不是主線程。
為什么不能直接將所有的setter直接hook到hook_proxy,非要通過動(dòng)態(tài)決議來進(jìn)行。
我們舉個(gè)簡單的例子,假設(shè)我們有兩個(gè)property,分別叫A和B。那么在執(zhí)行下述代碼的時(shí)候:
for (int i = 0; i < 2; i++) {
SEL originSetter = NSSelectorFromString(setterName);
SEL newSetter = NSSelectorFromString(hook_proxy);
swizzleMethod([self class], originSetter, newSetter);
}
第一次交換的時(shí)候,Setter A的 IMP和 hook_proxy的 IMP進(jìn)行了交換,這一步?jīng)]問題。
第二次交換的時(shí)候,Setter B的 IMP和 hook_proxy的 IMP進(jìn)行了交換,而此時(shí)hook_proxy的IMP已經(jīng)指向了Setter A的IMP,因此導(dǎo)致的結(jié)果就是交換錯(cuò)亂了,調(diào)用setter B實(shí)質(zhì)上是調(diào)用了setter A。
總結(jié)
以上是生活随笔為你收集整理的iOS多线程同时操作同一内存造成野指针的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP资格证书查询系统源码 自动生成二维
- 下一篇: 问题解决:printf()函数无法打印