今年的Apple發(fā)布會(huì)也開完了,沒有什么太出彩的地方。不過(guò)廣受非議的iPhone 7依然大賣。群里、微信里都是各種討論外加各種炫,而我只能靜靜地看著,等著公司的測(cè)試機(jī)了。
每次都感嘆時(shí)間過(guò)得快,總是有各種事情,這一晃又三個(gè)星期了,哎。這期整理了之前的5個(gè)問(wèn)題,無(wú)規(guī)則無(wú)主題,大伙慢慢看:
- block未判空導(dǎo)致的EXC_BAD_ACCESS崩潰;
- 多Target開發(fā);
- dispatch_sync導(dǎo)致死鎖;
- makeObjectsPerformSelector:;
- NSSetUncaughtExceptionHandler
block未判空導(dǎo)致的EXC_BAD_ACCESS崩潰
我們?cè)谡{(diào)用block時(shí),如果這個(gè)block為nil,則程序會(huì)崩潰,報(bào)類似于EXC_BAD_ACCESS(code=1, address=0xc)異常[32位下的結(jié)果,如果是64位,則address=0x10]。如下圖所示,這個(gè)異常表示程序在試圖讀取內(nèi)存地址0xc的信息時(shí)出錯(cuò)。

在定義一個(gè)block時(shí),編譯器會(huì)在棧上創(chuàng)建一個(gè)結(jié)構(gòu)體,類似于圖2的結(jié)構(gòu)體。
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
}
block就是指向這個(gè)結(jié)構(gòu)體的指針。其中的invoke就是指向具體實(shí)現(xiàn)的函數(shù)指針。當(dāng)block被調(diào)用時(shí),程序最終會(huì)跳轉(zhuǎn)到這個(gè)函數(shù)指針指向的代碼區(qū)。而當(dāng)block為nil時(shí),程序就會(huì)試圖去讀取0xc地址的信息,而這個(gè)地址什么都不會(huì)有(duff address),于是拋出一個(gè)segmentation fault。在32位系統(tǒng)下,之所以是0xc,是因?yàn)閕nvoke前面的三個(gè)成員變量的大小正好是12。
所以我們?cè)谑褂胋lock時(shí),應(yīng)該首先去判斷block是否為空。一種比較優(yōu)雅的寫法是:
!block ?: block()
參考
多Target開發(fā)
在Xcode中,一個(gè)target表示工程中的一個(gè)product,target用于組織product所需要的源文件、資源文件、配置信息等。
在一些情況下,我們可以為一個(gè)工程設(shè)置多個(gè)target,如:同時(shí)開發(fā)Lite版和正式版;開發(fā)版本和發(fā)布版本需要不同配置;單工程構(gòu)建多個(gè)相似的App等等。如下圖所示。

這么做的好處是在共用一份代碼的情況下,可以為不同的target配置不同的資源、信息等,如不同的Info.plist, Build Setting, Build Phase配置等,最后得到不同的product。
參考
dispatch_sync導(dǎo)致死鎖
dispatch_sync函數(shù)用于將一個(gè)block提交到隊(duì)列中同步執(zhí)行,直到block執(zhí)行完后,這個(gè)函數(shù)才會(huì)返回。
這里有一個(gè)潛在的問(wèn)題,如果我們?cè)谀硞€(gè)串行隊(duì)列中調(diào)用dispatch_sync,并將其block提交到這個(gè)串行隊(duì)列中執(zhí)行,則會(huì)引發(fā)死鎖。如下代碼所示。
/ 死鎖
dispatch_queue_t queue = dispatch_queue_create("com.apple.test", NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"B");
});
NSLog(@"A");
});
其實(shí)還是很好理解,在com.apple.test這個(gè)串行隊(duì)列中,我們執(zhí)行一個(gè)task A,在這個(gè)task A中,我們又向隊(duì)列提交了一個(gè)同步的task B。由于是串行隊(duì)列,task A在task B之前,所以task B的執(zhí)行依賴于task A的完成,而task B又包含在task A中,task A的完成依賴于task B的完成。這樣就成了一個(gè)死鎖。
所以,千萬(wàn)不要在主隊(duì)列中這樣調(diào)用dispatch_sync,否則會(huì)導(dǎo)致主線程卡死。
當(dāng)然,如果在并行隊(duì)列中這樣使用是沒有問(wèn)題的,如下代碼所示,可以正常打印出B,A。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"B");
});
NSLog(@"A");
});
又或是dispatch_sync的目標(biāo)隊(duì)列不是當(dāng)前隊(duì)列,如下代碼所示,也可以正常打印出B,A。
dispatch_queue_t queue1 = dispatch_queue_create("com.apple.test1", NULL);
dispatch_queue_t queue2 = dispatch_queue_create("com.apple.test2", NULL);
dispatch_async(queue1, ^{
dispatch_sync(queue2, ^{
NSLog(@"B");
});
NSLog(@"A");
});
我們?cè)谑褂胐ispatch_sync提交task時(shí),可以看到大部分情況下task是在dispatch_sync所在的上下文線程中執(zhí)行,而不管dispatch_sync指定的隊(duì)列是什么【串行或并行】,如下代碼所示:
// 串行隊(duì)列
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100303310>{number = 1, name = main}
dispatch_queue_t queue = dispatch_queue_create("com.apple.test", NULL);
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100303310>{number = 1, name = main}
});
// 并行隊(duì)列
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100505ea0>{number = 2, name = (null)}
dispatch_queue_t queue = dispatch_queue_create("com.apple.test", NULL);
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100505ea0>{number = 2, name = (null)}
});
});
官方文檔給我們的解釋是這么做的目的是為了優(yōu)化性能:
As an optimization, this function invokes the block on the current thread when possible。
我們需要了解的是隊(duì)列和線程并不是一回事。我們將任務(wù)以block的形式提交到隊(duì)列,然后由GCD來(lái)決定將隊(duì)列中的block分發(fā)到系統(tǒng)管理的線程池中的某個(gè)線程中去執(zhí)行。
參考
makeObjectsPerformSelector:
遍歷一個(gè)數(shù)組的方法有幾種,for, forin, enumerateObjectsUsingBlock:方法。現(xiàn)在用得比較多的可能是enumerateObjectsUsingBlock:,它能很方便地讓我們獲取到數(shù)組中的元素及對(duì)應(yīng)的索引,然后根據(jù)這些信息做一些操作,如下代碼所示:
NSMutableArray *array = [[NSMutableArray alloc] init];
for (NSInteger index = 0; index < 10; index++) {
Test *t = [[Test alloc] init];
t.index = index;
[array addObject:t];
}
[array enumerateObjectsUsingBlock:^(Test * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj test];
}];
不過(guò),如果在循環(huán)中,只是想調(diào)用元素的某一個(gè)方法,則可以考慮使用makeObjectsPerformSelector:或者makeObjectsPerformSelector:withObject:,這兩個(gè)方法會(huì)按元素的順序向數(shù)組中的每個(gè)元素發(fā)送Selector指定的消息。如下代碼所示:
NSMutableArray *array = [[NSMutableArray alloc] init];
for (NSInteger index = 0; index < 10; index++) {
Test *t = [[Test alloc] init];
t.index = index;
[array addObject:t];
}
[array makeObjectsPerformSelector:@selector(test)];
[array makeObjectsPerformSelector:@selector(testWithNumber:) withObject:@10];
當(dāng)然,Selector不能是NULL,否則會(huì)拋NSInvalidArgumentException異常。大家如果熟悉runtime的話,應(yīng)該知道消息機(jī)制是如何處理調(diào)用不存在方法的。
NSSetUncaughtExceptionHandler
Foundation里面提供了一個(gè)NSSetUncaughtExceptionHandler函數(shù),可以設(shè)置一個(gè)頂層異常處理函數(shù),讓我們?cè)诔绦虬l(fā)生異常并終止前,有最后的機(jī)會(huì)來(lái)捕獲并輸出異常信息,如下代碼所示。
void UncaughtExceptionHandler(NSException *exception) {
NSArray *symbols = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
NSLog(@"reason = %@", reason);
NSLog(@"name = %@", name);
NSLog(@"symbols = %@", symbols);
}
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
return YES;
}
@end
這個(gè)函數(shù)的參數(shù)是一個(gè)函數(shù)指針,指向的函數(shù)其簽名是:void NSUncaughtExceptionHandler(NSException *exception)。可以看到這個(gè)函數(shù)有參數(shù)是一個(gè)NSException對(duì)象,通過(guò)這個(gè)對(duì)象我們就可以獲取到異常的信息。假定發(fā)生數(shù)組越界異常時(shí),會(huì)有如下輸出。
2016-09-20 11:55:36.719 Test111[5548:199035] reason = *** -[__NSSingleObjectArrayI objectAtIndex:]: index 10 beyond bounds [0 .. 0]
2016-09-20 11:55:36.720 Test111[5548:199035] name = NSRangeException
2016-09-20 11:55:36.720 Test111[5548:199035] symbols = (
0 CoreFoundation 0x0000000106cef34b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x000000010675021e objc_exception_throw + 48
2 CoreFoundation 0x0000000106d47bdf -[__NSSingleObjectArrayI objectAtIndex:] + 111
3 Test111 0x000000010617d87b -[AppDelegate application:didFinishLaunchingWithOptions:] + 235
4 UIKit 0x000000010710968e -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 290
5 UIKit 0x000000010710b013 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4236
6 UIKit 0x00000001071113b9 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1731
7 UIKit 0x000000010710e539 -[UIApplication workspaceDidEndTransaction:] + 188
8 FrontBoardServices 0x000000010a2ee76b __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24
9 FrontBoardServices 0x000000010a2ee5e4 -[FBSSerialQueue _performNext] + 189
10 FrontBoardServices 0x000000010a2ee96d -[FBSSerialQueue _performNextFromRunLoopSource] + 45
11 CoreFoundation 0x0000000106c94311 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
12 CoreFoundation 0x0000000106c7959c __CFRunLoopDoSources0 + 556
13 CoreFoundation 0x0000000106c78a86 __CFRunLoopRun + 918
14 CoreFoundation 0x0000000106c78494 CFRunLoopRunSpecific + 420
15 UIKit 0x000000010710cdb6 -[UIApplication _run] + 434
16 UIKit 0x0000000107112f34 UIApplicationMain + 159
17 Test111 0x000000010617db4f main + 111
18 libdyld.dylib 0x000000010928a68d start + 1
19 ??? 0x0000000000000001 0x0 + 1
)
不過(guò)這個(gè)函數(shù)有效范圍局限于異常,還有很多錯(cuò)誤是無(wú)法處理的,如EXC_BAD_ACCESS內(nèi)存訪問(wèn)錯(cuò)誤,這類錯(cuò)誤拋出的是Signal,需要專門做Signal處理。
小結(jié)
Crash始終是我們開發(fā)最大最頭疼的問(wèn)題,總會(huì)有各種各樣的Crash情況出現(xiàn)。看著Fabric里面長(zhǎng)長(zhǎng)的Crash列表,總是很傷感的。我們的成長(zhǎng)史也是一部和Bug戰(zhàn)斗的斗爭(zhēng)史,自己寫的Bug,熬夜也要把它們搞完。繼續(xù)戰(zhàn)斗吧,Bug君。
歡迎關(guān)注我的微信公眾號(hào):iOS知識(shí)小集,掃掃左邊站點(diǎn)概覽里的二維碼就OK了。對(duì)了,還有微博:@南峰子_老驢。