iOS面試題總結(二)

Crazy巴旦木發表於2018-03-19

面試總結一傳送門:面試總結一
發現自己不僅僅是不會說的問題,有些問題的答案是有聯絡的,理論性的知識也要學會從理解的角度按照自己的方式說出來,還是太菜了啊!!!!

在有了自動合成屬性例項變數之後,@synthesize還有哪些使用場景?

什麼時候不會自動合成?

1.同時重寫了setter/getter方法。
2.重寫了只讀的getter方法。
3.使用了@dynamic
4.在@protocol定義的屬性。
5.在category定義的屬性。
6.過載的屬性。

當在子類中過載了父類的屬性,必須使用@synthesize手動合成ivar。除了後三條,可以總結一個規律:當你想手動管理@property的所有內容時,你就會嘗試通過實現@property的所有“存取方法”或者使用@dynamic來達到這個目的,這時編譯器就會認為你打算手動管理@property,所以編譯器就禁用了@synthesize。還有就是上面說的自定義例項變數的名稱,一般都會自動生成的,看自己的需求。

objc中向一個nil物件傳送訊息將會發生什麼?

可以向nil傳送訊息,只是在執行時不會有任何作用。如果一個方法的返回值是一個物件,那麼傳送給nil的訊息將返回0(nil)。

1.如果方法返回值為指標型別,其指標大小為小於或等於sizeof(void*),float,double,long double或者long long 的整形標量,傳送給nil的訊息將返回0。
2.如果方法返回值為結構體,傳送給nil的訊息將返回0.結構體中各個欄位的值將都是0。
3.如果方法的返回值不是上面的情況,那麼返回值將是未定義的。

原因:
Objc是動態語言,每個方法在執行時會被動態轉為訊息傳送,即:objc_msgSend(receiver,selector)。objc在向一個物件傳送訊息時,runtime庫會根據物件的isa指標找到該物件實際所屬類,然後再該類中的方法列表以及其父類方法列表中尋找方法執行,然後傳送訊息時,objc_msgSend方法不會返回值,所謂的返回內容都是具體呼叫時執行的。如果向一個nil物件傳送訊息,首先在尋找物件的isa指標就是0地址返回了,所以不會有錯誤的。

objc中向一個物件傳送訊息[obj foo]和objc_msgSend()函式之間有什麼關係?

[objc foo]在動態編譯時,會被轉意為objc_msgSend(obj,@selector(foo))。

什麼時候會報unrecognized selector的異常?

當物件上的某個方法,並沒有實現這個方法時,可以通過“訊息轉發”解決。 objc是動態語言,每個方法在執行時會被動態轉為訊息傳送,即:objc_msgSend(receiver,selector)。
objc在向一個物件傳送訊息時,runtime庫會根據物件的isa指標找到該類物件所屬的類,然後在該類中的方法列表以及其父類方法列表中尋找方法的執行,如果,在最頂層的父類中依然找不到相應的方法時,程式在執行時會掛掉並丟擲異常“unrecognized selector sent to xxx”。但是在這之前可以通過三種方法來完成訊息的傳送:

1.Method resolution
objc執行時會呼叫+resolveInstanceMethod或者+resolveClassMethod,讓你有機會提供一個函式實現。如果你新增了函式並返回YES,那麼執行時系統就會重新啟動一次訊息轉發的過程,如果resolve方法返回NO,執行時就會移到下一步,訊息轉發。
2.Fast forwarding
如果目標物件實現了-forwardingTargetForSelector,Runtime這時就會呼叫這個方法,給你把這個訊息轉發給其他物件的機會。只要這個方法返回的不是nil和self,整個訊息傳送的過程就會被重啟,當然傳送的物件會變成你返回的那個物件,否則就會繼續Normal Fowarding。這裡叫Fast,是為了區別下一步的轉發機制,因為這一步不會建立任何新物件,但是下一步轉發會建立一個NSInvocation物件,所以相對更快一點。
3.Normal forwarding
首先會傳送-methodSignatureForSelector訊息獲得函式的引數和返回值型別。如果-methodSignatureForSelector返回nil,Runtime則會發出-doseNotRecognizeSelector訊息,程式也就會掛掉了。如果返回了一個函式簽名,Runtime就會建立一個NSInvocation物件併傳送-forwardInvocation訊息給目標物件。

一個objc物件如何進行記憶體佈局?(考慮有父類的情況)

所有父類的成員變數和自己的成員變數都會存放在該物件所對應的儲存空間裡。每個物件內部都有一個isa指標,指向他的類物件,類物件中存放著本物件的物件方法列表(物件能夠接收的訊息列表,儲存在它所對應的類物件中),成員變數的列表,屬性列表。它內部也有一個isa指標值指向元物件(mate class),元物件內部存放的是類方法列表,類物件內部還有個superclass指標,指向它的父類物件。根物件就是NSObject,它的superclass指標指向nil。 類物件既然成為物件,那麼它也是一個例項。類物件中也有一個isa指標指向它的元類,即類物件是元類的例項。元類內部存放的是類方法列表,根元類的isa指標指向自己,superclass指標指向NSObject類。 借用一下圖:

image

一個objc物件的isa的指標指向什麼?有什麼作用?

isa指向它的類物件,類物件中存放的是物件方法列表,成員變數列表,屬性列表。有什麼用,獲取到之後可以比如可以category新增屬性,或者更換方法名稱和實現。

runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和例項方法)

每個類物件都有一個方法列表,方法列表中記錄著方法的名稱,方法實現,以及引數型別,其實selector本質就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應的方法實現。

使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?

無論ARC和MRC下都不需要。被關聯物件在生命週期中要比物件本身釋放的晚很多,它們會在被NSObject -dealloc呼叫的object_dispose()方法中釋放。
物件的記憶體銷燬時間表,分四個步驟:

1.呼叫-release:引用計數變為0
物件正在被銷燬,生命週期即將結束。
不能再有新的_weak弱引用,否則將指向nil。
呼叫[self dealloc]
2.父類呼叫-dealloc
繼承關係中最底層的父類在呼叫-dealloc
如果是MRC程式碼則會手動釋放例項變數們(ivars)
繼承關係中每一層的父類 都在呼叫 -dealloc
3.NSObject調-dealloc
只做一件事:呼叫Objective-C runtime中的object_dispose()方法。
4.呼叫object_dispose()
為C++的例項變數們呼叫destructors
為ARC狀態下的例項變數們呼叫-release
解除所有使用runtime Associate方法的關聯
解除所有__weak引用
呼叫free()

objc中的類方法和例項方法有什麼本質區別和聯絡?

類方法:

  • 類方法是屬於類物件的
  • 類方法只能通過類物件呼叫
  • 類方法中的self是類物件
  • 類方法可以呼叫其他的類方法
  • 類方法中不能訪問成員變數
  • 類方法不定直接呼叫物件方法

例項方法:

  • 例項方法是屬於例項物件的
  • 例項方法只能通過例項物件呼叫
  • 例項方法中的self是例項物件
  • 例項方法中可以訪問成員變數
  • 例項方法中直接呼叫例項方法
  • 例項方法中也可以呼叫類方法(通過類名)

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

_objc_msgForward是IMP型別,用於訊息轉發的。當向一個物件傳送一條訊息時,但它並沒有實現的時候,_objc_msgForward會嘗試做訊息轉發。objc_msgSend的動作:首先在類中的快取查詢IMP(沒有快取則初始化快取),如果沒找到,則向父類的類查詢。如果一直查詢到根類仍然沒有實現,則用_objc_msgForward的函式指標代替IMP,最後執行這個IMP。
我們可以看一下具體過程,開啟Debug模式列印出所有執行時傳送的訊息,可以在程式碼中執行下面的方法:

1.(void)instrumentObjcMessageSends(YES);
或者斷點暫停程式執行,並在控制檯輸入:
2.call (void)instrumentObjcMessageSends(YES)

以第二種方法為例,可以看到:

iOS面試題總結(二)
在控制檯輸入,然後進入/ tmp/msgSend-XXX的找到最新的就是我們剛剛生成的。開啟看到如下過程:

iOS面試題總結(二)

1.呼叫resolveInstanceMethod:(或者resoveClassMethod:)。允許使用者為該類動態新增實現。如果有實現,則呼叫並返回YES,那麼重新開始objc_msgSend流程,這一次物件會響應這個選擇器,一般是因為它已經呼叫過class_addMethod。如果沒實現,則繼續下面的動作。
2.呼叫forwardingTargetForSelector,嘗試找到一個能響應該訊息的物件。如果獲取到,則轉發給它,返回非0物件。否則返回nil,繼續下面的動作。注意,不要返回self,否則造成死迴圈。
3.呼叫方法signatureForSelector,嘗試獲取一個方法簽名、如果獲取不到,則直接呼叫doesNotRecognizeSelector丟擲異常。如果能獲取,則返回非0,建立一個NSInvocation並傳給forwardInvocation。
4.呼叫forwardInvocation將第三步獲取到的方法簽名包裝成Invocation傳入,如何處理就在裡面了,並返回非nil。
5.doNotRecognizeSelector,預設的實現是丟擲異常。如果第三步沒有方法簽名,則直接執行此步驟。
上面前4個方法均是模板方法,可以重寫,有執行時呼叫。最常見的就是:重寫3和4。
直接呼叫msgForward會不會有問題?
可以直接呼叫,但是非常危險,用不好會崩潰。 當向物件傳送訊息時,並沒有實現的時候,msgForward會嘗試做訊息轉發。直接呼叫msgForward,將跳過查詢IMP過程,直接出發訊息轉發,就是自己告訴系統沒有找到方法實現。哪裡用到直接呼叫_objc_msgForward常見場景?是想要獲取某方法對應的NSInvocation的物件,例如JSPatch就是直接呼叫_objc_msgForward來實現其核心功能的。具體可看:JSPatch實現原理詳解

執行時如何實現弱變數的自動置零?

runtime對註冊的類,會進行佈局,對於weak物件會放入一個hash表中。用weak指向的物件記憶體地址作為key,當此物件的引用計數為0的時候會dealloc,假如weak指向的物件記憶體地址是a,那麼以a為鍵,在這個weak表中搜尋,找到所有以a為鍵的weak物件,從而置為nil。
objc_storeWeak(&a,b)
b是賦值物件的記憶體地址作為key,將第一個引數-weak修飾的屬性變數(a)的記憶體地址(&a)作為value,註冊到weak表中。如果第二個引數b為0,那麼變數a的記憶體地址&a從weak表刪除。可以理解為objc_store(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傳送訊息極易崩潰。

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

不能,可以。
因為編譯後的類已經註冊在runtime中,類結構體中的objc_var_list例項變數的連結串列和instance_size例項變數的記憶體大小已經確定,同時runtime會呼叫class_setIvarLayout或class_setWeakIvarLayout來處理strong weak引用,所以不能向存在的類中新增例項變數。
執行時建立的類是可以新增例項變數,呼叫class_addIvar函式。但是得在呼叫objc_allocateClassPair之後,objc_registerClassPair之前,原因同上。

runloop和執行緒有什麼關係?

runLoop和執行緒是緊密聯絡的,可以說runloop為了執行緒而生。Runloops是執行緒的基礎架構部分,Cocoa和CoreFundation都提供了runloop物件方便配置和管理執行緒的runloop。每個執行緒,包括程式的主執行緒都有與之相對應的runloop物件。
主執行緒的runloop預設是啟動的。
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
這個函式的作用就是應用無人操作時休息,需要幹活時能立馬響應。
其他執行緒runloop是預設沒啟動的,需要手動配置和啟動。
currentRunloop來獲取當前執行緒的runloop。

runloop的mode作用是什麼?

主要有四個mode:
預設mode:NSDefaultRunLoopMode
滑動mode:UITrackingRunLoopMode:
程式啟動mode:UIInitializationRunLoopMode
mode集合:NSRunLoopCommonModes
公開的mode是前兩個。這裡可以解釋一下tableview中NSTimer為什麼要加到NSRunLoopCommonModes中。

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

RunLoop只能執行在一種mode下,如果要換mode,當前loop也需要停下重新啟動新的loop。利用這個機會,預設mode會切換到滑動mode中。如果把NSTimer物件以預設mode新增到主執行緒迴圈,在滑動時會因為切換mode,導致NSTimer不再被排程。
可以新增到到CommonModes解決。兩種方式,第一種把NSTimer分別加入預設和滑動的runloop中,另一種就是加入modes集合中(加入modes中的會自動新增到所有標記為Common的mode中)。

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

一般一個執行緒一次只能執行一個任務,完成後執行緒就退出。如果需要一個機制,讓執行緒能夠隨時處理事件但並不退出。 參考深入理解RunLoop

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

每次runloop時,都會檢查物件的引用計數,如果retainCount為0,說明物件沒有地方需要使用了,可以釋放掉了。

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

編碼時根據程式碼上下文,插入保留/釋放。

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

分為手動干預釋放時機和系統自動去釋放。
1.手動干預:當作用域大括號結束時釋放。
2.自動釋放:不手動指定autorelease,Autorelease物件會在當前的runloop迭代結束時釋放。如果一個vc的viewDidLoad中建立一個Autorelease物件,那麼該物件會在viewDidAppear方法執行前就被銷燬了。
參考:黑幕背後的Autorelease

BAD_ACCESS在什麼情況下出現?

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

蘋果是如何實現autoreleasepool的?

autoreleasepool以一個佇列陣列的形式實現,主要有三個函式。

1.objc_autoreleasepoolPush
2.objc_autoreleasepoolPop
3.objc_autorelease

對自動釋放分別執行推,彈出,釋放操作。

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

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

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

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

預設情況下,在塊中訪問的外部變數是複製過去的,即:寫操作不對原變數生效但是可以加上__block讓其寫操作生效。

__block int a = 0; void(^ foo)(void)= ^ { a = 1; } foo(); 這裡a就成了1。

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

UIView的塊寫動畫時不需要考慮迴圈引用。但有些api需要考慮: 所謂“引用迴圈”是指雙向的強引用,所以那些“單向的強引用”(block強引用self)沒有問題,比如:

[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,則需要考慮迴圈引用:

__weak __typeof __(self)weakSelf = self;
dispatch_group_async(_operationsGroup,_operationsQueue,^
{
    __typeof __(self)strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doSomethingElse];
});

複製程式碼

類似:

__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顯然這也是一個迴圈引用。

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

1.序列佇列Serial Dispatch Queue。
2.並行佇列併發排程佇列。

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

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

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(),^ {
        //合併圖片
});
複製程式碼

dispatch_barrier_async的作用是什麼?

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

蘋果為什麼要廢棄dispatch_get_current_queue?

dispatch_get_current_queue容易造成死鎖。

如何手動觸發一個value的KVO

自動觸發是指:在註冊KVO之前設定一個初始值,註冊之後,設定一個不一樣的值,就可以觸發了。 手動觸發的原理是:
鍵值觀察者通知依賴於兩個方法:willChangeValueForKey:和 didChangeValueForKey:。在一個被觀察屬性發生改變之前,willChangeValueForKey:一定會被呼叫,這就會記錄舊的值。而當改變發生後,didChangeValueForKey:會被呼叫,繼而observeValueForKey:ofObject:change:context:也會被呼叫。如果可以手動實現這些呼叫,就可以實現“手動觸發”了。
那麼手動觸發的使用場景是什麼?一般我們只希望能控制“回撥的呼叫時機”時才會這麼做。比如手動觸發self.now:
[self willChangeValueForKey:@"now"]; “手動觸發self.now的KVO”,必寫。 [self didChangeValueForKey:@"now"]; “手動觸發self.now的KVO”,必寫。

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

_foo,因為自動生成帶下劃線的屬性變數,但如果自己帶下劃線了,則不會生成對應的,也不會去掉下劃線。 都可以。哈哈,這裡不懂原理。

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

  • 必須用在集合物件上或普通物件的集合屬性上。
  • 簡單集合運算子有@avg,@count,@max,@min,@sum。
  • 格式@“@sum.age”或者@“集合屬性.@max.age”

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

KVO支援例項變數。

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

參考如何自己動手實現KVO

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

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

iOS面試題總結(二)
具體參考 KVO黑魔法

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

參考:IBOutlet連出來的檢視屬性為什麼可以被設定成weak? 文章說:因為既然有外鏈那麼檢視在xib或者storyboard中肯定存在,檢視已經對它有一個強引用了。
還有一點很重要,使用storyboard(xib不行)建立的vc,會有一個叫_topLevelObjectsToKeepAliveFromStoryboard的私有陣列強引用所有top level的物件,所以這時即便outlet宣告成weak也沒關係。

IB中User Defined Runtime Attributes如何使用?

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

如何除錯BAD_ACCESS錯誤

  • 重寫object的respondsToSelector方法,顯示出現EXEC_BAD_ACCESS前訪問的最後一個object
  • 通過Zombie ,開啟Scheme,勾選Enable Zombie Objects
  • 設定全域性斷點快速定位問題程式碼所在行。
  • Xcode7 整合了BAD_ACCESS捕獲功能:Address Sanitizer。在Scheme中勾選Enable Address Sanitizer

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

寫到這裡先,有的理論不會,大腦一片空白,有的甚至沒聽過,學到了一些東西。試著自己練習說出來,把上面的知識都消化調,並且延伸學習。

相關文章