今年的Apple發(fā)布會也開完了避矢,沒有什么太出彩的地方帽驯。不過廣受非議的iPhone 7依然大賣龟再。群里、微信里都是各種討論外加各種炫尼变,而我只能靜靜地看著利凑,等著公司的測試機(jī)了。
每次都感嘆時間過得快,總是有各種事情哀澈,這一晃又三個星期了牌借,哎。這期整理了之前的5個問題日丹,無規(guī)則無主題走哺,大伙慢慢看:
- block未判空導(dǎo)致的EXC_BAD_ACCESS崩潰;
- 多Target開發(fā);
- dispatch_sync導(dǎo)致死鎖;
- makeObjectsPerformSelector:;
- NSSetUncaughtExceptionHandler
block未判空導(dǎo)致的EXC_BAD_ACCESS崩潰
我們在調(diào)用block時,如果這個block為nil哲虾,則程序會崩潰,報類似于EXC_BAD_ACCESS(code=1, address=0xc)異常[32位下的結(jié)果择示,如果是64位束凑,則address=0x10]。如下圖所示栅盲,這個異常表示程序在試圖讀取內(nèi)存地址0xc的信息時出錯汪诉。
在定義一個block時,編譯器會在棧上創(chuàng)建一個結(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就是指向這個結(jié)構(gòu)體的指針。其中的invoke就是指向具體實現(xiàn)的函數(shù)指針拟烫。當(dāng)block被調(diào)用時该编,程序最終會跳轉(zhuǎn)到這個函數(shù)指針指向的代碼區(qū)。而當(dāng)block為nil時硕淑,程序就會試圖去讀取0xc地址的信息课竣,而這個地址什么都不會有(duff address),于是拋出一個segmentation fault置媳。在32位系統(tǒng)下于樟,之所以是0xc,是因為invoke前面的三個成員變量的大小正好是12拇囊。
所以我們在使用block時迂曲,應(yīng)該首先去判斷block是否為空。一種比較優(yōu)雅的寫法是:
!block ?: block()
參考
多Target開發(fā)
在Xcode中寥袭,一個target表示工程中的一個product路捧,target用于組織product所需要的源文件、資源文件纠永、配置信息等鬓长。
在一些情況下,我們可以為一個工程設(shè)置多個target尝江,如:同時開發(fā)Lite版和正式版涉波;開發(fā)版本和發(fā)布版本需要不同配置;單工程構(gòu)建多個相似的App等等。如下圖所示啤覆。
這么做的好處是在共用一份代碼的情況下苍日,可以為不同的target配置不同的資源、信息等窗声,如不同的Info.plist, Build Setting, Build Phase配置等相恃,最后得到不同的product。
參考
dispatch_sync導(dǎo)致死鎖
dispatch_sync函數(shù)用于將一個block提交到隊列中同步執(zhí)行笨觅,直到block執(zhí)行完后拦耐,這個函數(shù)才會返回。
這里有一個潛在的問題见剩,如果我們在某個串行隊列中調(diào)用dispatch_sync杀糯,并將其block提交到這個串行隊列中執(zhí)行,則會引發(fā)死鎖苍苞。如下代碼所示固翰。
/ 死鎖
dispatch_queue_t queue = dispatch_queue_create("com.apple.test", NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"B");
});
NSLog(@"A");
});
其實還是很好理解,在com.apple.test這個串行隊列中羹呵,我們執(zhí)行一個task A骂际,在這個task A中,我們又向隊列提交了一個同步的task B冈欢。由于是串行隊列歉铝,task A在task B之前,所以task B的執(zhí)行依賴于task A的完成涛癌,而task B又包含在task A中犯戏,task A的完成依賴于task B的完成。這樣就成了一個死鎖拳话。
所以先匪,千萬不要在主隊列中這樣調(diào)用dispatch_sync,否則會導(dǎo)致主線程卡死弃衍。
當(dāng)然呀非,如果在并行隊列中這樣使用是沒有問題的,如下代碼所示镜盯,可以正常打印出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)隊列不是當(dāng)前隊列,如下代碼所示速缆,也可以正常打印出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");
});
我們在使用dispatch_sync提交task時,可以看到大部分情況下task是在dispatch_sync所在的上下文線程中執(zhí)行艺糜,而不管dispatch_sync指定的隊列是什么【串行或并行】剧董,如下代碼所示:
// 串行隊列
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}
});
// 并行隊列
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幢尚。
我們需要了解的是隊列和線程并不是一回事。我們將任務(wù)以block的形式提交到隊列翅楼,然后由GCD來決定將隊列中的block分發(fā)到系統(tǒng)管理的線程池中的某個線程中去執(zhí)行尉剩。
參考
makeObjectsPerformSelector:
遍歷一個數(shù)組的方法有幾種,for, forin, enumerateObjectsUsingBlock:方法∫汶現(xiàn)在用得比較多的可能是enumerateObjectsUsingBlock:理茎,它能很方便地讓我們獲取到數(shù)組中的元素及對應(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];
}];
不過皂林,如果在循環(huán)中,只是想調(diào)用元素的某一個方法蚯撩,則可以考慮使用makeObjectsPerformSelector:或者makeObjectsPerformSelector:withObject:式撼,這兩個方法會按元素的順序向數(shù)組中的每個元素發(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,否則會拋NSInvalidArgumentException異常扰楼。大家如果熟悉runtime的話呀癣,應(yīng)該知道消息機(jī)制是如何處理調(diào)用不存在方法的。
NSSetUncaughtExceptionHandler
Foundation里面提供了一個NSSetUncaughtExceptionHandler函數(shù)弦赖,可以設(shè)置一個頂層異常處理函數(shù)项栏,讓我們在程序發(fā)生異常并終止前,有最后的機(jī)會來捕獲并輸出異常信息蹬竖,如下代碼所示沼沈。
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
這個函數(shù)的參數(shù)是一個函數(shù)指針,指向的函數(shù)其簽名是:void NSUncaughtExceptionHandler(NSException *exception)币厕×辛恚可以看到這個函數(shù)有參數(shù)是一個NSException對象,通過這個對象我們就可以獲取到異常的信息旦装。假定發(fā)生數(shù)組越界異常時页衙,會有如下輸出。
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
)
不過這個函數(shù)有效范圍局限于異常阴绢,還有很多錯誤是無法處理的店乐,如EXC_BAD_ACCESS內(nèi)存訪問錯誤,這類錯誤拋出的是Signal呻袭,需要專門做Signal處理眨八。
小結(jié)
Crash始終是我們開發(fā)最大最頭疼的問題,總會有各種各樣的Crash情況出現(xiàn)左电×啵看著Fabric里面長長的Crash列表页响,總是很傷感的。我們的成長史也是一部和Bug戰(zhàn)斗的斗爭史伏穆,自己寫的Bug拘泞,熬夜也要把它們搞完。繼續(xù)戰(zhàn)斗吧枕扫,Bug君陪腌。
歡迎關(guān)注我的微信公眾號:iOS知識小集,掃掃左邊站點概覽里的二維碼就OK了。對了伞辛,還有微博:@南峰子_老驢弓坞。