iOS知識(shí)小集 第8期(2016.09.20)

今年的Apple發(fā)布會(huì)也開完了,沒有什么太出彩的地方。不過(guò)廣受非議的iPhone 7依然大賣。群里、微信里都是各種討論外加各種炫,而我只能靜靜地看著,等著公司的測(cè)試機(jī)了。

每次都感嘆時(shí)間過(guò)得快,總是有各種事情,這一晃又三個(gè)星期了,哎。這期整理了之前的5個(gè)問(wèn)題,無(wú)規(guī)則無(wú)主題,大伙慢慢看:

  1. block未判空導(dǎo)致的EXC_BAD_ACCESS崩潰;
  2. 多Target開發(fā);
  3. dispatch_sync導(dǎo)致死鎖;
  4. makeObjectsPerformSelector:;
  5. 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()

參考

  1. Why do nil / NULL blocks cause bus errors when run?

多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。

參考

  1. Xcode Target
  2. 猿題庫(kù)iOS客戶端的技術(shù)細(xì)節(jié)(一):使用多target來(lái)構(gòu)建大量相似App

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í)行。

參考

  1. dispatch_sync

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君。


南峰子的技術(shù)博客

歡迎關(guān)注我的微信公眾號(hào):iOS知識(shí)小集,掃掃左邊站點(diǎn)概覽里的二維碼就OK了。對(duì)了,還有微博:@南峰子_老驢

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容

  • 程序中同步和異步是什么意思?有什么區(qū)別? 解釋一:異步調(diào)用是通過(guò)使用單獨(dú)的線程執(zhí)行的。原始線程啟動(dòng)異步調(diào)用,異步調(diào)...
    風(fēng)繼續(xù)吹0閱讀 1,045評(píng)論 1 2
  • GCD (Grand Central Dispatch) :iOS4 開始引入,使用更加方便,程序員只需要將任務(wù)添...
    池鵬程閱讀 1,346評(píng)論 0 2
  • iOS中GCD的使用小結(jié) 作者dullgrass 2015.11.20 09:41*字?jǐn)?shù) 4996閱讀 20199...
    DanDanC閱讀 855評(píng)論 0 0
  • 一、GCD的API 1. Dispatch queue 在執(zhí)行處理時(shí)存在兩種Dispatch queue: 等待現(xiàn)...
    doudo閱讀 508評(píng)論 0 0
  • 又是一年將盡時(shí)。年底總要搞點(diǎn)大事情, 不 然 都 沒 法 安 心 過(guò) 年。 盤點(diǎn)16年,大事不斷,小事新鮮, 1月...
    酒莊惠小九閱讀 411評(píng)論 0 0