iOS面試知識點html
如今進入本篇的正題。本篇的面試題是我認爲比較好的iOS開發基礎知識點,但願你們看過這後在理解的基礎上掌握而不是死記硬背。死記硬背很快也會忘記的。ios
深拷貝同淺拷貝的區別:淺拷貝是指針拷貝,對一個對象進行淺拷貝,至關於對指向對象的指針進行復制,產生一個新的指向這個對象的指針,那麼就是有兩個指針指向同一個對象,這個對象銷燬後兩個指針都應該置空。深拷貝是對一個對象進行拷貝,至關於對對象進行復制,產生一個新的對象,那麼就有兩個指針分別指向兩個對象。當一個對象改變或者被銷燬後拷貝出來的新的對象不受影響。git
實現深拷貝須要實現NSCoying協議,實現- (id)copyWithZone:(NSZone *)zone 方法。當對一個property屬性含有copy修飾符的時候,在進行賦值操做的時候實際上就是調用這個方法。github
父類實現深拷貝以後,子類只要重寫copyWithZone方法,在方法內部調用父類的copyWithZone方法,以後實現本身的屬性的處理面試
父類沒有實現深拷貝,子類除了須要對本身的屬性進行處理,還要對父類的屬性進行處理。算法
NSNotification是通知,也是一對多的使用場景。在某些狀況下,KVO和NSNotification是同樣的,都是狀態變化以後告知對方。NSNotification的特色,就是須要被觀察者先主動發出通知,而後觀察者註冊監聽後再來進行響應,比KVO多了發送通知的一步,可是其優勢是監聽不侷限於屬性的變化,還能夠對多種多樣的狀態變化進行監聽,監聽範圍廣,使用也更靈活。編程
delegate 是代理,就是我不想作的事情交給別人作。好比狗須要吃飯,就經過delegate通知主人,主人就會給他作飯、盛飯、倒水,這些操做,這些狗都不須要關心,只須要調用delegate(代理人)就能夠了,由其餘類完成所須要的操做。因此delegate是一對一關係。swift
block是delegate的另外一種形式,是函數式編程的一種形式。使用場景跟delegate同樣,相比delegate更靈活,並且代理的實現更直觀。設計模式
KVO通常的使用場景是數據,需求是數據變化,好比股票價格變化,咱們通常使用KVO(觀察者模式)。delegate通常的使用場景是行爲,需求是須要別人幫我作一件事情,好比買賣股票,咱們通常使用delegate。
Notification通常是進行全局通知,好比利好消息一出,通知你們去買入。delegate是強關聯,就是委託和代理雙方互相知道,你委託別人買股票你就須要知道經紀人,經紀人也不要知道本身的顧客。Notification是弱關聯,利好消息發出,你不須要知道是誰發的也能夠作出相應的反應,同理發消息的人也不須要知道接收的人也能夠正常發出消息。api
dispatch_async(dispatch_get_main_queue(), ^{ //須要執行的方法 });
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主隊列 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ //須要執行的方法 }]; [mainQueue addOperation:operation];
[self performSelector:@selector(method) onThread:[NSThread mainThread] withObject:nil waitUntilDone:YES modes:nil]; [self performSelectorOnMainThread:@selector(method) withObject:nil waitUntilDone:YES]; [[NSThread mainThread] performSelector:@selector(method) withObject:nil];
[[NSRunLoop mainRunLoop] performSelector:@selector(method) withObject:nil];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES]; -(void)timerMethod { //調用類方法 [[self class] staticMethod]; } -(void)invalid { [timer invalid]; timer = nil; }
typedef struct objc_object *id
id能夠理解爲指向對象的指針。全部oc的對象 id均可以指向,編譯器不會作類型檢查,id調用任何存在的方法都不會在編譯階段報錯,固然若是這個id指向的對象沒有這個方法,該崩潰仍是會崩潰的。
NSObject *指向的必須是NSObject的子類,調用的也只能是NSObjec裏面的方法不然就要作強制類型轉換。
不是全部的OC對象都是NSObject的子類,還有一些繼承自NSProxy。NSObject *可指向的類型是id的子集。
封裝、繼承、多態
設計模式6個原則
設計一個類的功能,如何劃分粒度(單一職責)
接口隔離。
若是有一個鳥類,有飛的動做,一個鴕鳥繼承它是合適的嗎(里氏替換)
類之間的依賴如何依賴偶合度最小(依賴倒轉)
高層依賴低層,低層不能依賴高層。依賴接口,不能依賴具體的類。
若是A要調用C函數,但C是B的成員類,應該如何設計?(迪米特)
如何設計類,能作到只增長代碼,而不修改代碼,有哪些經驗(開放封閉)
經過設計模式解決。
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屬性」:
1
2
3
4
5
6
7
8
|
// 使用僞代碼模擬:runtime如何實現weak屬性
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計數變爲0,變量做用域結束*/
objc_destroyWeak(&obj1);
|
下面對用到的兩個方法objc_initWeak和objc_destroyWeak作下解釋:
整體說來,做用是: 經過objc_initWeak函數初始化「附有weak修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak函數釋放該變量(obj1)。
下面分別介紹下方法的內部實現:
objc_initWeak函數的實現是這樣的:在將「附有weak修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak函數。
1
2
|
obj1 = 0;
obj_storeWeak(&obj1, obj);
|
也就是說:
weak 修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的)
而後obj_destroyWeak函數將0(nil)做爲參數,調用objc_storeWeak函數。
1
|
objc_storeWeak(&obj1, 0);
|
前面的源代碼與下列源代碼相同。
1
2
3
4
5
6
7
8
9
|
// 使用僞代碼模擬:runtime如何實現weak屬性
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_setIvarLayout 或 class_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()函數
1
2
3
4
|
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 。
1
|
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
|
參考連接:《Objective-C之run loop詳解》。
29. runloop的mode做用是什麼?
model 主要是用來指定事件在運行循環中的優先級的,分爲:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默認,空閒狀態
UITrackingRunLoopMode:ScrollView滑動時
UIInitializationRunLoopMode:啓動時
NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
蘋果公開提供的 Mode 有兩個:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
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)來解決。代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
//將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內部是如何實現的?
通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要一個機制,讓線程能隨時處理事件但並不退出,一般的代碼邏輯 是這樣的:
1
2
3
4
5
6
7
|
function
loop() {
initialize();
do
{
var
message = get_next_message();
process_message(message);
}
while
(message != quit);
}
|
或使用僞代碼來展現下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
int main(int argc, char * argv[]) {
//程序一直運行狀態
while
(AppIsRunning) {
//睡眠狀態,等待喚醒事件
id whoWakesMe = SleepForWakingUp();
//獲得喚醒事件
id event = GetEvent(whoWakesMe);
//開始處理事件
HandleEvent(event);
}
return
0;
}
|
參考連接:
摘自博文CFRunLoop,原做者是微博@我就叫Sunny怎麼了
32. objc使用什麼機制管理對象內存?
經過 retainCount 的機制來決定對象是否須要釋放。 每次 runloop 的時候,都會檢查對象的 retainCount,若是retainCount 爲 0,說明該對象沒有地方須要繼續使用了,能夠釋放掉了。
33. ARC經過什麼方式幫助開發者管理內存?
編譯時根據代碼上下文,插入 retain/release
34. 不手動指定autoreleasepool的前提下,一個autorealese對象在什麼時刻釋放?(好比在一個vc的viewDidLoad中建立)
分兩種狀況:手動干預釋放時機、系統自動去釋放。
手動干預釋放時機--指定autoreleasepool 就是所謂的:當前做用域大括號結束時釋放。
系統自動去釋放--不手動指定autoreleasepool
Autorelease對象會在當前的 runloop 迭代結束時釋放。
若是在一個vc的viewDidLoad中建立一個 Autorelease對象,那麼該對象會在 viewDidAppear 方法執行前就被銷燬了。
參考連接:《黑幕背後的Autorelease》
35. BAD_ACCESS在什麼狀況下出現?
訪問了野指針,好比對一個已經釋放的對象執行了release、訪問已經釋放對象的成員變量或者發消息。 死循環
36. 蘋果是如何實現autoreleasepool的?
autoreleasepool以一個隊列數組的形式實現,主要經過下列三個函數完成.
objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_aurorelease
看函數名就能夠知道,對autorelease分別執行push,和pop操做。銷燬對象時執行release操做。
37. 使用block時什麼狀況會發生引用循環,如何解決?
一個對象中強引用了block,在block中又使用了該對象,就會發射循環引用。 解決方法是將該對象使用__weak或者__block修飾符修飾以後再在block中使用。
id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self該方法能夠設置宏
id __block weakSelf = self;
38. 在block內如何修改block外部變量?
默認狀況下,在block中訪問的外部變量是複製過去的,即:寫操做不對原變量生效。可是你能夠加上__block來讓其寫操做生效,示例代碼以下:
1
2
3
4
5
6
|
__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 )沒有問題,好比這些:
1
2
3
4
5
6
|
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }];
[[NSNotificationCenter defaultCenter] addObserverForName:@
"someNotification"
object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * notification) {
self.someProperty = xyz; }];
|
這些狀況不須要考慮「引用循環」。
但若是你使用一些參數中可能含有 ivar 的系統 api ,如 GCD 、NSNotificationCenter就要當心一點:好比GCD 內部若是引用了 self,並且 GCD 的其餘參數是 ivar,則要考慮到循環引用:
1
2
3
4
5
6
7
|
__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^
{
__typeof__(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doSomethingElse];
} );
|
相似的:
1
2
3
4
5
6
7
8
|
__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)分哪兩種類型?
串行隊列Serial Dispatch Queue
並行隊列Concurrent Dispatch Queue
41. 如何用GCD同步若干個異步調用?(如根據若干個url異步加載多張圖片,而後在都下載完成後合成一張整圖)
使用Dispatch Group追加block到Global Group Queue,這些block若是所有執行完畢,就會執行Main Dispatch Queue中的結束處理的block。
1
2
3
4
5
6
7
8
|
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. 如下代碼運行結果如何?
1
2
3
4
5
6
7
8
9
|
- (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
5
6
7
8
|
// 添加鍵值觀察
/*
1 觀察者,負責處理監聽事件的對象
2 觀察的屬性
3 觀察的選項
4 上下文
*/
[self.person addObserver:self forKeyPath:@
"name"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@
"Person Name"
];
|
observer中須要實現一下方法:
1
2
3
4
5
6
7
8
|
// 全部的 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 ,那麼代碼以下:最後兩行代碼缺一不可。
1
2
3
4
5
6
7
8
9
10
11
12
|
// .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: 是合成方法,有時候咱們也能看到人們這麼寫代碼:
1
2
3
4
5
|
- (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中的集合運算符如何使用?
必須用在集合對象上或普通對象的集合屬性上
簡單集合運算符有@avg, @count , @max , @min ,@sum,
格式 @"@sum.age"或 @"集合屬性.@max.age"
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 系統這個對象的類是什麼 ) 指向這個新建立的子類,對象就神奇的變成了新建立的子類的實例。我畫了一張示意圖,以下所示:
KVO 確實有點黑魔法:
Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。
下面作下詳細解釋:
鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一個被觀察屬性發生改變以前, willChangeValueForKey: 必定會被調用,這就 會記錄舊的值。而當改變發生後, didChangeValueForKey: 會被調用,繼而 observeValueForKey:ofObject:change:context: 也會被調用。能夠手動實現這些調用,但不多有人這麼作。通常咱們只在但願能控制回調的調用時機時纔會這麼作。大部分狀況下,改變通知會自動調用。
好比調用 setNow: 時,系統還會以某種方式在中間插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的調用。你們可能覺得這是由於 setNow: 是合成方法,有時候咱們也能看到人們這麼寫代碼:
1
2
3
4
5
|
- (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 ,大體工做原理以下:
1
2
3
4
5
|
- (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方法,現實出現EXEC_BAD_ACCESS前訪問的最後一個object
2. 經過 Zombie
3. 設置全局斷點快速定位問題代碼所在行
4. Xcode 7 已經集成了BAD_ACCESS捕獲功能:Address Sanitizer。 用法以下:在配置中勾選?Enable Address Sanitizer
55. lldb(gdb)經常使用的調試命令?
breakpoint 設置斷點定位到某一個函數
n 斷點指針下一步
po打印對象
更多 lldb(gdb) 調試命令可查看
蘋果官方文檔: iOS Debugging Magic 。