《招聘一個靠譜的 iOS》—參考答案(下)

《招聘一個靠譜的 iOS》—參考答案(下)

《招聘一個靠譜的 iOS》—參考答案(下)

說明:面試題來源是微博@我就叫Sunny怎麼了的這篇博文:《招聘一個靠譜的 iOS》,其中共55題,除第一題為糾錯題外,其他54道均為簡答題。

出題者簡介: 孫源(sunnyxx),目前就職於百度,負責百度知道 iOS 客戶端的開發工作,對技術喜歡刨根問底和總結最佳實踐,熱愛分享和開源,維護一個叫 forkingdog 的開源小組。

答案為微博@iOS程式犭袁整理,未經出題者校對,如有紕漏,請向微博@iOS程式犭袁指正。


目錄

25. _objc_msgForward函式是做什麼的,直接呼叫它將會發生什麼?

_objc_msgForward是 IMP 型別,用於訊息轉發的:當向一個物件傳送一條訊息,但它並沒有實現的時候,_objc_msgForward會嘗試做訊息轉發。

我們可以這樣建立一個_objc_msgForward物件:

IMP msgForwardIMP = _objc_msgForward;

上篇中的《objc中向一個物件傳送訊息[obj foo]objc_msgSend()函式之間有什麼關係?》曾提到objc_msgSend在“訊息傳遞”中的作用。在“訊息傳遞”過程中,objc_msgSend的動作比較清晰:首先在 Class 中的快取查詢 IMP (沒快取則初始化快取),如果沒找到,則向父類的 Class 查詢。如果一直查詢到根類仍舊沒有實現,則用_objc_msgForward函式指標代替 IMP 。最後,執行這個 IMP 。

Objective-C執行時是開源的,所以我們可以看到它的實現。開啟 ***Apple Open Source 裡Mac程式碼裡的obj包*** 下載一個最新版本,找到 objc-runtime-new.mm,進入之後搜尋_objc_msgForward

enter image description here

裡面有對_objc_msgForward的功能解釋:

enter image description here

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* initialize==NO tries to avoid  initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/

objc-runtime-new.mm檔案裡與_objc_msgForward有關的三個函式使用虛擬碼展示下:

//  objc-runtime-new.mm 檔案裡與 _objc_msgForward 有關的三個函式使用虛擬碼展示
//  Created by https://github.com/ChenYilong
//  Copyright (c)  微博@iOS程式犭袁(http://weibo.com/luohanchenyilong/). All rights reserved.
//  同時,這也是 obj_msgSend 的實現過程
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); //呼叫這個函式,虛擬碼...
}
//查詢IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; //_objc_msgForward 用於訊息轉發
return imp;
}
IMP lookUpImpOrNil(Class cls, SEL sel) {
if (!cls->initialize()) {
_class_initialize(cls);
}
Class curClass = cls;
IMP imp = nil;
do { //先查快取,快取沒有時重建,仍舊沒有則向父類查詢
if (!curClass) break;
if (!curClass->cache) fill_cache(cls, curClass);
imp = cache_getImp(curClass, sel);
if (imp) break;
} while (curClass = curClass->superclass);
return imp;
}

雖然Apple沒有公開_objc_msgForward的實現原始碼,但是我們還是能得出結論:

_objc_msgForward是一個函式指標(和 IMP 的型別一樣),是用於訊息轉發的:當向一個物件傳送一條訊息,但它並沒有實現的時候,_objc_msgForward會嘗試做訊息轉發。

上篇中的《objc中向一個物件傳送訊息[obj foo]objc_msgSend()函式之間有什麼關係?》曾提到objc_msgSend在“訊息傳遞”中的作用。在“訊息傳遞”過程中,objc_msgSend的動作比較清晰:首先在 Class 中的快取查詢 IMP (沒快取則初始化快取),如果沒找到,則向父類的 Class 查詢。如果一直查詢到根類仍舊沒有實現,則用_objc_msgForward函式指標代替 IMP 。最後,執行這個 IMP 。

為了展示訊息轉發的具體動作,這裡嘗試向一個物件傳送一條錯誤的訊息,並檢視一下_objc_msgForward是如何進行轉發的。

首先開啟除錯模式、列印出所有執行時傳送的訊息:
可以在程式碼裡執行下面的方法:

(void)instrumentObjcMessageSends(YES);

或者斷點暫停程式執行,並在 gdb 中輸入下面的命令:

call (void)instrumentObjcMessageSends(YES)

以第二種為例,操作如下所示:

enter image description here

之後,執行時傳送的所有訊息都會列印到/tmp/msgSend-xxxx檔案裡了。

終端中輸入命令前往:

open /private/tmp

enter image description here

可能看到有多條,找到最新生成的,雙擊開啟

在模擬器上執行執行以下語句(這一套除錯方案僅適用於模擬器,真機不可用,關於該除錯方案的拓展連結: ***Can the messages sent to an object in Objective-C be monitored or printed out?*** ),向一個物件傳送一條錯誤的訊息:

//
//  main.m
//  CYLObjcMsgForwardTest
//
//  Created by http://weibo.com/luohanchenyilong/.
//  Copyright (c) 2015年 微博@iOS程式犭袁. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "CYLTest.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
CYLTest *test = [[CYLTest alloc] init];
[test performSelector:(@selector(iOS程式犭袁))];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

enter image description here

你可以在/tmp/msgSend-xxxx(我這一次是/tmp/msgSend-9805)檔案裡,看到列印出來:

enter image description here

  CYLTest NSObject initialize
CYLTest NSObject alloc
- CYLTest NSObject init
- CYLTest NSObject performSelector:
CYLTest NSObject resolveInstanceMethod:
CYLTest NSObject resolveInstanceMethod:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject class
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject class

結合《NSObject官方文件》,排除掉 NSObject 做的事,剩下的就是_objc_msgForward訊息轉發做的幾件事:

  1. 呼叫resolveInstanceMethod:方法 (或 resolveClassMethod:)。允許使用者在此時為該 Class 動態新增實現。如果有實現了,則呼叫並返回YES,那麼重新開始objc_msgSend流程。這一次物件會響應這個選擇器,一般是因為它已經呼叫過class_addMethod。如果仍沒實現,繼續下面的動作。

  2. 呼叫forwardingTargetForSelector:方法,嘗試找到一個能響應該訊息的物件。如果獲取到,則直接把訊息轉發給它,返回非 nil 物件。否則返回 nil ,繼續下面的動作。注意,這裡不要返回 self ,否則會形成死迴圈。

  3. 呼叫methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果獲取不到,則直接呼叫doesNotRecognizeSelector丟擲異常。如果能獲取,則返回非nil:建立一個 NSlnvocation 並傳給forwardInvocation:

  4. 呼叫forwardInvocation:方法,將第3步獲取到的方法簽名包裝成 Invocation 傳入,如何處理就在這裡面了,並返回非ni。

  5. 呼叫doesNotRecognizeSelector: ,預設的實現是丟擲異常。如果第3步沒能獲得一個方法簽名,執行該步驟。

上面前4個方法均是模板方法,開發者可以override,由 runtime 來呼叫。最常見的實現訊息轉發:就是重寫方法3和4,吞掉一個訊息或者代理給其他物件都是沒問題的

也就是說_objc_msgForward在進行訊息轉發的過程中會涉及以下這幾個方法:

  1. resolveInstanceMethod:方法 (或 resolveClassMethod:)。

  2. forwardingTargetForSelector:方法

  3. methodSignatureForSelector:方法

  4. forwardInvocation:方法

  5. doesNotRecognizeSelector: 方法

下面回答下第二個問題“直接_objc_msgForward呼叫它將會發生什麼?”

直接呼叫_objc_msgForward是非常危險的事,如果用不好會直接導致程式Crash,但是如果用得好,能做很多非常酷的事。

就好像跑酷,幹得好,叫“耍酷”,幹不好就叫“作死”。

正如前文所說:

_objc_msgForward是 IMP 型別,用於訊息轉發的:當向一個物件傳送一條訊息,但它並沒有實現的時候,_objc_msgForward會嘗試做訊息轉發。

如何呼叫_objc_msgForward
_objc_msgForward隸屬 C 語言,有三個引數 :

_objc_msgForward引數型別
1.所屬物件id型別
  1. |方法名 | SEL型別

  2. |可變引數 |可變引數型別

首先了解下如何呼叫 IMP 型別的方法,IMP型別是如下格式:

為了直觀,我們可以通過如下方式定義一個 IMP型別 :

typedef void (*voidIMP)(id, SEL, ...)

一旦呼叫_objc_msgForward,將跳過查詢 IMP 的過程,直接觸發“訊息轉發”,

如果呼叫了_objc_msgForward,即使這個物件確實已經實現了這個方法,你也會告訴objc_msgSend

“我沒有在這個物件裡找到這個方法的實現”

想象下objc_msgSend會怎麼做?通常情況下,下面這張圖就是你正常走objc_msgSend過程,和直接呼叫_objc_msgForward的前後差別:

enter image description here

有哪些場景需要直接呼叫_objc_msgForward?最常見的場景是:你想獲取某方法所對應的NSInvocation物件。舉例說明:

JSPatch (Github 連結)就是直接呼叫_objc_msgForward來實現其核心功能的:

JSPatch 以小巧的體積做到了讓JS呼叫/替換任意OC方法,讓iOS APP具備熱更新的能力。

作者的博文《JSPatch實現原理詳解》詳細記錄了實現原理,有興趣可以看下。

26. runtime如何實現weak變數的自動置nil?

runtime 對註冊的類, 會進行佈局,對於 weak 物件會放入一個 hash 表中。 用 weak 指向的物件記憶體地址作為 key,當此物件的引用計數為0的時候會 dealloc,假如 weak 指向的物件記憶體地址是a,那麼就會以a為鍵, 在這個 weak 表中搜尋,找到所有以a為鍵的 weak 物件,從而設定為 nil。

上篇中的《runtime 如何實現 weak 屬性》有論述。(注:在上篇的《使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?》裡給出的“物件的記憶體銷燬時間表”也提到__weak引用的解除時間。)

我們可以設計一個函式(虛擬碼)來表示上述機制:

objc_storeWeak(&a, b)函式:

objc_storeWeak函式把第二個引數–賦值物件(b)的記憶體地址作為鍵值key,將第一個引數–weak修飾的屬性變數(a)的記憶體地址(&a)作為value,註冊到 weak 表中。如果第二個引數(b)為0(nil),那麼把變數(a)的記憶體地址(&a)從weak表中刪除,

你可以把objc_storeWeak(&a, b)理解為:objc_storeWeak(value, key),並且當key變nil,將value置nil。

在b非nil時,a和b指向同一個記憶體地址,在b變nil時,a變nil。此時向a傳送訊息不會崩潰:在Objective-C中向nil傳送訊息是安全的。

而如果a是由assign修飾的,則:
在b非nil時,a和b指向同一個記憶體地址,在b變nil時,a還是指向該記憶體地址,變野指標。此時向a傳送訊息極易崩潰。

下面我們將基於objc_storeWeak(&a, b)函式,使用虛擬碼模擬“runtime如何實現weak屬性”:

// 使用虛擬碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計數變為0,變數作用域結束*/
objc_destroyWeak(&obj1);

下面對用到的兩個方法objc_initWeakobjc_destroyWeak做下解釋:

總體說來,作用是:
通過objc_initWeak函式初始化“附有weak修飾符的變數(obj1)”,在變數作用域結束時通過objc_destoryWeak函式釋放該變數(obj1)。

下面分別介紹下方法的內部實現:

objc_initWeak函式的實現是這樣的:在將“附有weak修飾符的變數(obj1)”初始化為0(nil)後,會將“賦值物件”(obj)作為引數,呼叫objc_storeWeak函式。

obj1 = 0;
obj_storeWeak(&obj1, obj);

也就是說:

weak 修飾的指標預設值是 nil (在Objective-C中向nil傳送訊息是安全的)

然後obj_destroyWeak函式將0(nil)作為引數,呼叫objc_storeWeak函式。

objc_storeWeak(&obj1, 0);

前面的原始碼與下列原始碼相同。

// 使用虛擬碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變為0,被置nil ... */
objc_storeWeak(&obj1, 0);

objc_storeWeak函式把第二個引數–賦值物件(obj)的記憶體地址作為鍵值,將第一個引數–weak修飾的屬性變數(obj1)的記憶體地址註冊到 weak 表中。如果第二個引數(obj)為0(nil),那麼把變數(obj1)的地址從weak表中刪除。

27. 能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?

  • 不能向編譯後得到的類中增加例項變數;

  • 能向執行時建立的類中新增例項變數;

解釋下:

  • 因為編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 例項變數的連結串列 和 instance_size 例項變數的記憶體大小已經確定,同時runtime 會呼叫 class_setIvarLayoutclass_setWeakIvarLayout 來處理 strong weak 引用。所以不能向存在的類中新增例項變數;

  • 執行時建立的類是可以新增例項變數,呼叫 class_addIvar 函式。但是得在呼叫 objc_allocateClassPair 之後,objc_registerClassPair 之前,原因同上。

28. runloop和執行緒有什麼關係?

總的說來,Run loop,正如其名,loop表示某種迴圈,和run放在一起就表示一直在執行著的迴圈。實際上,run loop和執行緒是緊密相連的,可以這樣說run loop是為了執行緒而生,沒有執行緒,它就沒有存在的必要。Run loops是執行緒的基礎架構部分, Cocoa 和 CoreFundation 都提供了 run loop 物件方便配置和管理執行緒的 run loop (以下都以 Cocoa 為例)。每個執行緒,包括程式的主執行緒( main thread )都有與之相應的 run loop 物件。

runloop 和執行緒的關係:

  1. 主執行緒的run loop預設是啟動的。

iOS的應用程式裡面,程式啟動後會有一個如下的main()函式

`Objective-C
int main(int argc, char * argv[]) {

@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}

}


重點是UIApplicationMain()函式,這個方法會為main thread設定一個NSRunLoop物件,這就解釋了:為什麼我們的應用可以在無人操作的時候休息,需要讓它幹活的時候又能立馬響應。
2. 對其它執行緒來說,run loop預設是沒有啟動的,如果你需要更多的執行緒互動則可以手動配置和啟動,如果執行緒只是去執行一個長時間的已確定的任務則不需要。
3. 在任何一個 Cocoa 程式的執行緒中,都可以通過以下程式碼來獲取到當前執行緒的 run loop 。
```Objective-C
NSRunLoop *runloop = [NSRunLoop currentRunLoop];

參考連結:《Objective-C之run loop詳解》

29. runloop的mode作用是什麼?

model 主要是用來指定事件在執行迴圈中的優先順序的,分為:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):預設,空閒狀態

  • UITrackingRunLoopMode:ScrollView滑動時

  • UIInitializationRunLoopMode:啟動時

  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

蘋果公開提供的 Mode 有兩個:

  1. NSDefaultRunLoopMode(kCFRunLoopDefaultMode)

  2. NSRunLoopCommonModes(kCFRunLoopCommonModes)

30. 以 scheduledTimerWithTimeInterval…的方式觸發的timer,在滑動頁面上的列表時,timer會暫定回撥,為什麼?如何解決?

RunLoop只能執行在一種mode下,如果要換mode,當前的loop也需要停下重啟成新的。利用這個機制,ScrollView滾動過程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode會切換到UITrackingRunLoopMode來保證ScrollView的流暢滑動:只能在NSDefaultRunLoopMode模式下處理的事件會影響scrllView的滑動。

如果我們把一個NSTimer物件以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)新增到主執行迴圈中的時候,
ScrollView滾動過程中會因為mode的切換,而導致NSTimer將不再被排程。

同時因為mode還是可定製的,所以:

Timer計時會被scrollView的滑動影響的問題可以通過將timer新增到NSRunLoopCommonModes(kCFRunLoopCommonModes)來解決。程式碼如下:

// 
// http://weibo.com/luohanchenyilong/ (微博@iOS程式犭袁)
// https://github.com/ChenYilong
//將timer新增到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
//然後再新增到NSRunLoopCommonModes裡
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

31. 猜想runloop內部是如何實現的?

一般來講,一個執行緒一次只能執行一個任務,執行完成後執行緒就會退出。如果我們需要一個機制,讓執行緒能隨時處理事件但並不退出,通常的程式碼邏輯

是這樣的:

function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

或使用虛擬碼來展示下:

// 
// http://weibo.com/luohanchenyilong/ (微博@iOS程式犭袁)
// https://github.com/ChenYilong
int main(int argc, char * argv[]) {
//程式一直執行狀態
while (AppIsRunning) {
//睡眠狀態,等待喚醒事件
id whoWakesMe = SleepForWakingUp();
//得到喚醒事件
id event = GetEvent(whoWakesMe);
//開始處理事件
HandleEvent(event);
}
return 0;
}

參考連結:

  1. 《深入理解RunLoop》

  2. 摘自博文***CFRunLoop***,原作者是微博@我就叫Sunny怎麼了

32. objc使用什麼機制管理物件記憶體?

通過 retainCount 的機制來決定物件是否需要釋放。
每次 runloop 的時候,都會檢查物件的 retainCount,如果retainCount 為 0,說明該物件沒有地方需要繼續使用了,可以釋放掉了。

33. ARC通過什麼方式幫助開發者管理記憶體?

編譯時根據程式碼上下文,插入 retain/release

34. 不手動指定autoreleasepool的前提下,一個autorealese物件在什麼時刻釋放?(比如在一個vc的viewDidLoad中建立)

分兩種情況:手動干預釋放時機、系統自動去釋放。

  1. 手動干預釋放時機–指定autoreleasepool
    就是所謂的:當前作用域大括號結束時釋放。

  2. 系統自動去釋放–不手動指定autoreleasepool

Autorelease物件會在當前的 runloop 迭代結束時釋放。

如果在一個vc的viewDidLoad中建立一個 Autorelease物件,那麼該物件會在 viewDidAppear 方法執行前就被銷燬了。

參考連結:《黑幕背後的Autorelease》

35. BAD_ACCESS在什麼情況下出現?

訪問了野指標,比如對一個已經釋放的物件執行了release、訪問已經釋放物件的成員變數或者發訊息。
死迴圈

36. 蘋果是如何實現autoreleasepool的?

autoreleasepool以一個佇列陣列的形式實現,主要通過下列三個函式完成.

  1. objc_autoreleasepoolPush

  2. objc_autoreleasepoolPop

  3. objc_aurorelease

看函式名就可以知道,對autorelease分別執行push,和pop操作。銷燬物件時執行release操作。

37. 使用block時什麼情況會發生引用迴圈,如何解決?

一個物件中強引用了block,在block中又使用了該物件,就會發射迴圈引用。
解決方法是將該物件使用weak或者block修飾符修飾之後再在block中使用。

  1. id weak weakSelf = self;
    或者 weak __typeof(&*self)weakSelf = self該方法可以設定巨集

  2. id __block weakSelf = self;

38. 在block內如何修改block外部變數?

預設情況下,在block中訪問的外部變數是複製過去的,即:寫操作不對原變數生效。但是你可以加上__block來讓其寫操作生效,示例程式碼如下:

__block int a = 0;
void  (^foo)(void) = ^{ 
a = 1; 
}
f00(); 
//這裡,a的值被修改為1

參考連結:微博@唐巧_boy的著作《iOS開發進階》中的第11.2.3章節

39. 使用系統的某些block api(如UIView的block版本寫動畫時),是否也考慮引用迴圈問題?

系統的某些block api中,UIView的block版本寫動畫時不需要考慮,但也有一些api 需要考慮:

所謂“引用迴圈”是指雙向的強引用,所以那些“單向的強引用”(block 強引用 self )沒有問題,比如這些:

`Objective-C
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }];
`

`Objective-C
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }];
`

`Objective-C
[[NSNotificationCenter defaultCenter] addObserverForName:@”someNotification”

                                              object:nil 
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * notification) {
self.someProperty = xyz; }]; 

`

這些情況不需要考慮“引用迴圈”。

但如果你使用一些引數中可能含有 ivar 的系統 api ,如 GCD 、NSNotificationCenter就要小心一點:比如GCD 內部如果引用了 self,而且 GCD 的其他引數是 ivar,則要考慮到迴圈引用:

`Objective-C
weak typeof__(self) weakSelf = self;
dispatchgroupasync(operationsGroup, operationsQueue, ^
{
typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doSomethingElse];
} );
`
類似的:

`Objective-C
weak typeof__(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@”testKey”

                                                            object:nil
queue:nil
usingBlock:^(NSNotification *note) {
__typeof__(self) strongSelf = weakSelf;
[strongSelf dismissModalViewControllerAnimated:YES];

}];
`
self –> _observer –> block –> self 顯然這也是一個迴圈引用。

40. GCD的佇列(dispatch_queue_t)分哪兩種型別?

  1. 序列佇列Serial Dispatch Queue

  2. 並行佇列Concurrent Dispatch Queue

41. 如何用GCD同步若干個非同步呼叫?(如根據若干個url非同步載入多張圖片,然後在都下載完成後合成一張整圖)

使用Dispatch Group追加block到Global Group Queue,這些block如果全部執行完畢,就會執行Main Dispatch Queue中的結束處理的block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*載入圖片1 */ });
dispatch_group_async(group, queue, ^{ /*載入圖片2 */ });
dispatch_group_async(group, queue, ^{ /*載入圖片3 */ }); 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合併圖片
});

42. dispatch_barrier_async的作用是什麼?

在並行佇列中,為了保持某些任務的順序,需要等待一些任務完成後才能繼續進行,使用 barrier 來等待之前任務完成,避免資料競爭等問題。
dispatch_barrier_async 函式會等待追加到Concurrent Dispatch Queue並行佇列中的操作全部執行完之後,然後再執行 dispatch_barrier_async 函式追加的處理,等 dispatch_barrier_async 追加的處理執行結束之後,Concurrent Dispatch Queue才恢復之前的動作繼續執行。

打個比方:比如你們公司週末跟團旅遊,高速休息站上,司機說:大家都去上廁所,速戰速決,上完廁所就上高速。超大的公共廁所,大家同時去,程式猿很快就結束了,但程式媛就可能會慢一些,即使你第一個回來,司機也不會出發,司機要等待所有人都回來後,才能出發。 dispatch_barrier_async 函式追加的內容就如同 “上完廁所就上高速”這個動作。

43. 蘋果為什麼要廢棄dispatch_get_current_queue

dispatch_get_current_queue容易造成死鎖

44. 以下程式碼執行結果如何?

  • (void)viewDidLoad
    {

    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2");
    });
    NSLog(@"3");

    }

只輸出:1 。發生主執行緒鎖死。

45. addObserver:forKeyPath:options:context:各個引數的作用分別是什麼,observer中需要實現哪個方法才能獲得KVO回撥?

// 新增鍵值觀察
/*
1 觀察者,負責處理監聽事件的物件
2 觀察的屬性
3 觀察的選項
4 上下文
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];

observer中需要實現一下方法:

// 所有的 kvo 監聽到事件,都會呼叫此方法
/*
1. 觀察的屬性
2. 觀察的物件
3. change 屬性變化字典(新/舊)
4. 上下文,與監聽的時候傳遞的一致
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

46. 如何手動觸發一個value的KVO

所謂的“手動觸發”是區別於“自動觸發”:

自動觸發是指類似這種場景:在註冊 KVO 之前設定一個初始值,註冊之後,設定一個不一樣的值,就可以觸發了。

想知道如何手動觸發,必須知道自動觸發 KVO 的原理:

鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey:didChangevlueForKey: 。在一個被觀察屬性發生改變之前, willChangeValueForKey: 一定會被呼叫,這就
會記錄舊的值。而當改變發生後, didChangeValueForKey: 會被呼叫,繼而 observeValueForKey:ofObject:change:context: 也會被呼叫。如果可以手動實現這些呼叫,就可以實現“手動觸發”了。

那麼“手動觸發”的使用場景是什麼?一般我們只在希望能控制“回撥的呼叫時機”時才會這麼做。

具體做法如下:

如果這個 value 是 表示時間的 self.now ,那麼程式碼如下:最後兩行程式碼缺一不可。

`Objective-C
// .m檔案
// Created by https://github.com/ChenYilong
// 微博@iOS程式犭袁(http://weibo.com/luohanchenyilong/).
// 手動觸發 value 的KVO,最後兩行程式碼缺一不可。

//@property (nonatomic, strong) NSDate *now;

  • (void)viewDidLoad
    {

    [super viewDidLoad];
    [self willChangeValueForKey:@"now"]; // “手動觸發self.now的KVO”,必寫。
    [self didChangeValueForKey:@"now"]; // “手動觸發self.now的KVO”,必寫。

    }
    `

但是平時我們一般不會這麼幹,我們都是等系統去“自動觸發”。“自動觸發”的實現原理:

> 比如呼叫 setNow: 時,系統還會以某種方式在中間插入 wilChangeValueForKey:didChangeValueForKey:observeValueForKeyPath:ofObject:change:context: 的呼叫。

大家可能以為這是因為 setNow: 是合成方法,有時候我們也能看到人們這麼寫程式碼:

`Objective-C

  • (void)setNow:(NSDate *)aDate {

    [self willChangeValueForKey:@"now"]; // 沒有必要
    _now = aDate;
    [self didChangeValueForKey:@"now"];// 沒有必要

    }
    `
    這是完全沒有必要的程式碼,不要這麼做,這樣的話,KVO程式碼會被呼叫兩次。KVO在呼叫存取方法之前總是呼叫 willChangeValueForKey: ,之後總是呼叫 didChangeValueForkey: 。怎麼做到的呢?答案是通過 isa 混寫(isa-swizzling)。下文《apple用什麼方式實現對一個物件的KVO?》會有詳述。

47. 若一個類有例項變數 NSString *_foo ,呼叫setValue:forKey:時,可以以foo還是 _foo 作為key?

都可以。

48. KVC的keyPath中的集合運算子如何使用?

  1. 必須用在集合物件上或普通物件的集合屬性上

  2. 簡單集合運算子有@avg, @count , @max , @min ,@sum,

  3. 格式 @”@sum.age”或 @”集合屬性[email protected]

49. KVC和KVO的keyPath一定是屬性麼?

KVO支援例項變數

50. 如何關閉預設的KVO的預設實現,並進入自定義的KVO實現?

請參考:《如何自己動手實現 KVO》

51. apple用什麼方式實現對一個物件的KVO?

Apple 的文件對 KVO 實現的描述:

> Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

Apple 的文件可以看出:Apple 並不希望過多暴露 KVO 的實現細節。不過,要是藉助 runtime 提供的方法去深入挖掘,所有被掩蓋的細節都會原形畢露:

> 當你觀察一個物件時,一個新的類會被動態建立。這個類繼承自該物件的原本的類,並重寫了被觀察屬性的 setter 方法。重寫的 setter 方法會負責在呼叫原 setter 方法之前和之後,通知所有觀察物件:值的更改。最後通過 isa 混寫(isa-swizzling) 把這個物件的 isa 指標 ( isa 指標告訴 Runtime 系統這個物件的類是什麼 ) 指向這個新建立的子類,物件就神奇的變成了新建立的子類的例項。我畫了一張示意圖,如下所示:

enter image description here

KVO 確實有點黑魔法:

> Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。

下面做下詳細解釋:

鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey:didChangevlueForKey: 。在一個被觀察屬性發生改變之前, willChangeValueForKey: 一定會被呼叫,這就
會記錄舊的值。而當改變發生後, didChangeValueForKey: 會被呼叫,繼而 observeValueForKey:ofObject:change:context: 也會被呼叫。可以手動實現這些呼叫,但很少有人這麼做。一般我們只在希望能控制回撥的呼叫時機時才會這麼做。大部分情況下,改變通知會自動呼叫。

比如呼叫 setNow: 時,系統還會以某種方式在中間插入 wilChangeValueForKey:didChangeValueForKey:observeValueForKeyPath:ofObject:change:context: 的呼叫。大家可能以為這是因為 setNow: 是合成方法,有時候我們也能看到人們這麼寫程式碼:

`Objective-C

  • (void)setNow:(NSDate *)aDate {

    [self willChangeValueForKey:@"now"]; // 沒有必要
    _now = aDate;
    [self didChangeValueForKey:@"now"];// 沒有必要

    }
    `
    這是完全沒有必要的程式碼,不要這麼做,這樣的話,KVO程式碼會被呼叫兩次。KVO在呼叫存取方法之前總是呼叫 willChangeValueForKey: ,之後總是呼叫 didChangeValueForkey: 。怎麼做到的呢?答案是通過 isa 混寫(isa-swizzling)。第一次對一個物件呼叫 addObserver:forKeyPath:options:context: 時,框架會建立這個類的新的 KVO 子類,並將被觀察物件轉換為新子類的物件。在這個 KVO 特殊子類中, Cocoa 建立觀察屬性的 setter ,大致工作原理如下:

`Objective-C

  • (void)setNow:(NSDate *)aDate {

    [self willChangeValueForKey:@"now"];
    [super setValue:aDate forKey:@"now"];
    [self didChangeValueForKey:@"now"];

    }
    `
    這種繼承和方法注入是在執行時而不是編譯時實現的。這就是正確命名如此重要的原因。只有在使用KVC命名約定時,KVO才能做到這一點。

KVO 在實現中通過 isa 混寫(isa-swizzling) 把這個物件的 isa 指標 ( isa 指標告訴 Runtime 系統這個物件的類是什麼 ) 指向這個新建立的子類,物件就神奇的變成了新建立的子類的例項。這在Apple 的文件可以得到印證:

> Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

然而 KVO 在實現中使用了 isa 混寫( isa-swizzling) ,這個的確不是很容易發現:Apple 還重寫、覆蓋了 -class 方法並返回原來的類。 企圖欺騙我們:這個類沒有變,就是原本那個類。。。

但是,假設“被監聽的物件”的類物件是 MYClass ,有時候我們能看到對 NSKVONotifying_MYClass 的引用而不是對 MYClass 的引用。藉此我們得以知道 Apple 使用了 isa 混寫(isa-swizzling)。具體探究過程可參考 這篇博文

52. IBOutlet連出來的檢視屬性為什麼可以被設定成weak?

參考連結: ***Should IBOutlets be strong or weak under ARC?***

文章告訴我們:

因為既然有外鏈那麼檢視在xib或者storyboard中肯定存在,檢視已經對它有一個強引用了。

不過這個回答漏了個重要知識,使用storyboard(xib不行)建立的vc,會有一個叫_topLevelObjectsToKeepAliveFromStoryboard的私有陣列強引用所有top level的物件,所以這時即便outlet宣告成weak也沒關係

53. IB中User Defined Runtime Attributes如何使用?

它能夠通過KVC的方式配置一些你在interface builder 中不能配置的屬性。當你希望在IB中作儘可能多得事情,這個特效能夠幫助你編寫更加輕量級的viewcontroller

54. 如何除錯BAD_ACCESS錯誤

  1. 重寫object的respondsToSelector方法,現實出現EXECBADACCESS前訪問的最後一個object

  2. 通過 Zombie
    enter image description here

  3. 設定全域性斷點快速定位問題程式碼所在行

  4. Xcode 7 已經整合了BAD_ACCESS捕獲功能:Address Sanitizer
    用法如下:在配置中勾選✅Enable Address Sanitizer
    enter image description here

55. lldb(gdb)常用的除錯命令?

  • breakpoint 設定斷點定位到某一個函式

  • n 斷點指標下一步

  • po列印物件

更多 lldb(gdb) 除錯命令可檢視

  1. ***The LLDB Debugger***

  2. 蘋果官方文件: ***iOS Debugging Magic***


Posted by 微博@iOS程式犭袁
原創文章,版權宣告:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0