iOS爛大街的面試題自答

又到了跳槽季,這幾天公司web的兄弟在準備招人,準備了一大堆的問題等著虐來面試的人,我說這些問題換成你在提前不做準備的情況下也夠嗆能回答得很準確。web兄弟也很認同。但是面試不就是這樣嗎,不問點有料的東西怎么能表現出面試官很有水準呢?

所以趕緊理了理iOS的基礎問題,幫助自己復習一遍平時開發中不常關注的點,如果同時能幫到別人的話更好。另外博主水平有限,如果下面的內容中有錯誤還請提點。

以下問題不分主次,內容會慢慢更新。

關于block

1、block的類型

在block runtime中定義了6種類。

_NSConcreteStackBlock 棧上創建的block
_NSConcreteMallocBlock 堆上創建的block
_NSConcreteGlobalBlock 作為全局變量的block
_NSConcreteWeakBlockVariable
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock

其中只有前三種能夠用得到。在ARC下只有_NSConcreteMallocBlock和_NSConcreteGlobalBlock,因為在ARC開啟時,ARC會自動處理block的內存管理操作。(所以ARC下block屬性聲明成copy 或 strong都可以,而MRC下必須是copy)。

2、block的變量捕獲

block可以修改全局變量,方法內statice修飾的變量和有__block關鍵字的棧變量和傳入block的參數。對于普通的棧變量只可訪問不可修改。
這里需要注意的是下面這個例子:

NSInteger i = 1;
void (^aBlock)() = ^(void){
    NSLog(@“i = %ld", i);
};
i = 2;
aBlock();

這時的輸出結果為i = 1
而這個例子中

__block NSInteger i = 1;
void (^aBlock)() = ^(void){
    NSLog(@“i = %ld", i);
};
i = 2;
aBlock();

輸出結果為i = 2。

二者的區別是在變量i聲明時增加了block關鍵字。導致輸出結果不同的原因是在block訪問外部變量時,默認會將變量copy到他自己的數據結構中(說人話就是把外部的變量復制一份給自己,對應到例子中就是把i=1拷貝一份自己存起來,i=2就不管了)。所以調用block時它輸出的還是自己記住的i=1。而在聲明了block關鍵字之后block就不會copy變量,而是copy變量的內存地址,所以在程序執行了i=2后再調用block,block會從相同的地址拿到數據,這時這里面的數據已經是2。

block的數據結構可以參考下面連接中巧神的講解。(面個試連block的數據結構都要問一遍,哥哥你面的是BAT嗎- -)。

block中的循環引用

要知道block本身也是一個對象,明白這一點就比較好理解為什么block會發生循環引用了。
比如經常會出現self引用了block,而block里面又引用了self的情況,上升到對象層面就是對象A引用了對象B,對象B又引用了對象A,這樣就形成了一個標準的保留環。
解決方法當然是破壞掉這個環中的某一節,即用weak來避免雙向的強引用。

參考:
談Objective-C block的實現
objc 中的 block

關于runtime

OC的運行時機制由runtime庫實現,runtime庫做了兩件事
1、封裝:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數來實現,另外再加上了一些額外的特性。這些結構體和函數被runtime函數封裝后,我們就可以在程序運行時創建,檢查,修改類、對象和它們的方法了。
2、找出方法的最終執行代碼:當程序執行[object doSomething]時,會向消息接收者(object)發送一條消息(doSomething),runtime會根據消息接收者是否能響應該消息而做出不同的反應。

針對第一點,可以展開為類與對象的組成。首先要知道類的本質也是對象。因為class類型實際是一個指向objc_class結構體的指針。而在這個結構體中存在一個isa指針,凡是首地址是*isa的結構體指針,都可以被認為是objc中的對象(isa指針的作用:運行時可以通過isa指針,查找到該對象是屬于什么類(Class))。
既然類也是對象,那么它是屬于什么類的呢?答案是類的isa指針指向其元類(meta class)。元類的isa指向根元類(NSObject的元類),根元類的isa指向它自己。也就是下面這張被看爛了的關系圖。

objctree.png

圖片來源

當我們向一個對象發送消息時,runtime會在這個對象所屬類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。比如[[objA alloc] init]這個行代碼,alloc方法會保存在meta-class方法列表中,init方法會保存在class方法列表中。

關于第二點,要涉及到OC中的消息機制。我們通常所說的調用方法,在OC中準確的叫法應該是“消息傳遞”。在OC中,如果向對象發送消息,就會使用動態綁定機制來決定需要調用的方法。在底層,所有的方法都是C函數,對象收到消息后究竟要調用哪個方法完全在運行期決定,甚至可以在程序運行時改變,這些特性使得OC成為一門動態語言。

[objA message:parameter]這行代碼中,objA是接收者,message是選擇子(selector),選擇子和參數的組合叫做消息。當編譯器看到這條消息,會把它轉換成標準的c函數調用(objc_msgSend)。objc_msgSend函數會依據接收者和選擇子的類型來調用適當的方法。

具體的操作是會在接收者所屬的類對象中搜索其方法列表,如果能找到與選擇子名稱相符的方法,就跳至其實現代碼,如果沒找到就沿著該類的繼承體系繼續尋找,直到找到對應的實現。如果沒找到的話就會執行消息轉發操作(一會說)。

在每個類中都有一塊緩存,用來存儲“快速映射表”,快速映射表的存在是為了解決每次執行消息時都要走一次上面所述的繁瑣流程。它將曾經被調用過的方法保存在表中。在objc_msgSend執行的時候會優先在“快速映射表”里面查找對應的方法,找到了就會立即返回,從而避免了再次逐個類搜索方法實現的繁瑣流程。

關于消息轉發,這是一個比較模板化的東西,這里只描述消息轉發流程,不做具體使用講解(寫下來又是一大堆)。首先對象在收到未知的消息時會執行其所在類的+resolveInstanceMethod:方法(對應的還有+resolveClassMethod:方法,一個是在收到未知對象消息時,一個是收到未知類消息時)。使用這個方法的前提是解決此未知消息的相關方法實現已經寫好,只需要在執行+resolveClassMethod:/+resolveInstanceMethod:方法時利用runtime動態插入到類里面就可以。

如果上一步沒有成功解決問題,還有第二次機會,這次嘗試把該未知消息傳遞給其他接收者來處理。對應方法為- (id)forwardingTargetForSelector:(SEL)aSelector,如果該方法返回了一個非nil對象,則該未知消息會被發送給此對象。需要注意在這一步不可以對消息做任何操作。

如果在第二次機會仍然沒有完成對消息的處理,則會觸發完整的消息轉發機制。首先創建NSInvocation對象,將未處理的消息的全部細節封裝其中,此步驟會調用forwardInvocation:方法來轉發消息。

參考:
Objective-C 中的類和對象
《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》

load和initialize的區別

1、運行時間的區別
當類文件被加載時load方法就會被調用,也就是說無論這個類在實際應用時是否被你調用到,load方法都會被執行。
而initialize是在第一次向該類發送消息時才會被調用,比如[ClassA alloc],當向ClassA類發送alloc消息時,initialize方法才會被執行。
2、關于父類的調用
首先兩個方法都不需要手動調用父類方法([super load]/[super initialize]),區別是當子類沒有實現load方法時,不會調用父類的load,而initialize則會。
3、關于線程安全
兩個方法都是線程安全的,所以盡量避免執行阻塞現成的操作。
4、用途
load方法多用于Method swizzle操作,initialize多用于初始化全局變量或靜態變量(博主水平有限,表示initialize從來沒用過。)

參考:
細說OC中的load和initialize方法

關于內存管理

引用計數

OC語言使用引用計數來管理內存,在iOS系統上從來也沒有垃圾回收機制。在引用技術架構下,每個對象都擁有一個計數器,用來表示當前有多少個事物想另此對象繼續存活下去。即為引用計數。NSObject協議中有三個方法來操作引用計數。retain,release,autorelease。對象創建后retainCount為1,若有對象想讓其繼續存活,則引用計數+1(調用retain方法),若不想讓其繼續存活則引用計數-1(調用release方法)。當引用計數為0時,系統會將該對象所在的內存標記為“可重用”,即其他對象想要使用內存時可以占用這塊地址。所有指向該地址的引用也將無效。

這里有一個問題,就是當某個對象的引用計數為0時再次訪問此對象依然有效的問題。關于這個問題的解釋是當引用計數為0后,該對象所占的內存只是被放回了可用內存區域,如果這時候這塊內存尚未被覆寫,那么該對象依然有效。這種訪問方式極其的危險(只在MRC時代比較常見,ARC下很少會發生這種情況)。可以通過將指針置nil來避免誤操作。

ARC

在ARC下,引用計數還是會執行的,只不過所有的操作由ARC代為完成。在ARC下,retain,release,autorelease和dealloc方法是不允許被手動調用的。

在使用ARC時必須遵循方法命名規則,若方法名以alloc,new,copy,mutableCopy開頭,則返回的對象歸調用者所有。人話版就是這些詞語開頭的方法返回的對象需要調用者自己release,否則不需要,因為以其他單詞開頭的方法返回對象時會被自動加上autorelease。

其實這些操作全部都由ARC去處理,但是原理是這樣要清楚。

ARC下的所有權修飾符
ARC有效時,id類型和對象類型同C語言其他類型不同,其類型上必須附加所有權修飾符。包括以下四種:
strong
weak
unsafe_unretained
autoreleasing

strong是默認的所有權修飾符,表示對對象的強引用。也就是說當我們在ARC中寫下這樣的代碼時:

id *obj =[ [NSObject alloc] init];

實際上是這樣的

id __strong *obj =[ [NSObject alloc] init];

weak的出現是為了解決在引用計數中必然會出現的問題,就是循環引用問題。當兩個對象互相都使用strong修飾符互相持有時,就發生了循環引用。weak提供了與strong相反的弱引用。弱引用不持有對象,也就是當對象不再被任何強引用持有時,即使存在弱引用也仍然會釋放對象。這個弱引用會自動失效并置nil。

unsafe_unretained提供與weak類似的功能,不同點在于引用失效后不會自動設置為nil。它的存在是因為在iOS4以前并沒有weak修飾符,只有unsafe_unretained。

autoreleasing修飾符替換了MRC下的autorelease方法。雖然在ARC下NSAutoreleasePool和autorelease方法不能被使用。但是ARC有效時autorelease還是起作用的。下面兩端代碼是等效的:

//MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id *obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];

//ARC
@autoreleasepool{
    id __autoreleasing *obj = [[NSObject alloc] init];//__autoreleasing不必要顯示的寫出來。
}

參考:
《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》
《Objective-C高級編程 iOS與OS X多線程和內存管理》

tableView的優化

1、避免出現透明的控件,也就是給控件一個backgroundColor。
2、避免發生過多的離屏渲染。
3、針對不定高度的cell,提前緩存高度。
4、異步加載圖片并解碼(這里有一篇我寫的關于圖片加載的文章)。

copy

copy方法實現

想讓自定義的類實現copy操作,需要實現NSCopying協議。
- (id)copyWithZone:(NSZone *)zone
協議中只有一個方法,關于NSZone是一個歷史問題,以前的程序開發會根據這個參數把內存分區,即zone,創建的對象會在區里面。現在只有一個默認分區。所以不用管它。比如要將一個model類實現copy操作:
- (id)copyWithZone:(NSZone *)zone{
NELMineModel *model = [[[self class] allocWithZone:zone] init];
return model;
}
如果類中還有其他的屬性需要一并拷貝,那就要都寫上,比如這樣:

  • (id)copyWithZone:(NSZone *)zone{
    NELMineModel *model = [[[self class] allocWithZone:zone] init];
    model.datas = [self.datas mutableCopy];
    return model;
    }

對應的如果要實現mutableCopy,就要實現NSMutableCopying協議。

集合的copy

Foundation框架中的所有集合類默認都執行淺拷貝,也就是之拷貝容器本身,對容器內的存儲對象不做拷貝(因為對象未必都支持拷貝操作)。也就是說拷貝的集合與原集合內的對象共用同一塊內存空間,修改拷貝集合中的對象(注意是修改集合中的對象,不是對這個集合做操作)同時也會對原集合中的對象產生影響。
如果要使集合進行深拷貝,需要使用下面這個方法:
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
如果copyItems參數為YES,則該方法會向array里面的每個元素發送copy消息,用拷貝好的元素創建新的集合返回給調用者。
修改上面的代碼:
- (id)copyWithZone:(NSZone *)zone{
NELMineModel *model = [[[self class] allocWithZone:zone] init];
model.datas = [[NSMutableArray alloc] initWithArray:self.datas copyItems:YES];
return model;
}

重寫一個帶copy關鍵字屬性的setter

@property (nonatomic, copy) NSString *title;
- (void)setTitle:(NSString *)title{
  _title = [title copy];
}

為什么這么寫,和strong有什么區別?用_代替self避免遞歸調用。使用copy后無論傳入的是可變(NSString)還是不可變類型(NSMutableString)使用copy后都會變為不可變類型,方式數據被無意間變動。而使用strong則會直接將指針指向原地址,如果傳入的NSString沒問題,如果是可變類型一旦數據發生改動將會被一并改動。

如何手動通知KVO

首先要在被觀察對象的類里面重寫+ (BOOL)automaticallyNotifiesObserversForKey:方法。然后在需要調用KVO的地方增加willChangeValueForKey:和didChangeValueForKey:兩個方法。具體看這里

SEL和IMP的區別

SEL(選擇器),是表示一個方法的selector的指針,每一個方法都有一個對應的SEL。SEL只是一個指向方法的指針,用來查找對應的IMP。

IMP實際上是一個函數指針,指向方法實現的首地址,定義如下:

id (*IMP)(id, SEL, ...)

第一個參數是指向self的指針(如果是實例方法,則是類實例的內存地址,如果是類方法,則是指向元類的指針),第二個參數就是用來查找到對應IMP的SEL。

Autorelease對象什么時候釋放

在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop。

Autoreleasepool的使用場景

當需要在循環中大量創建臨時變量時,為了避免內存峰值過高,需要手動寫上@autoreleasepool。還有其他的使用場景求告知。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,217評論 30 472
  • 把網上的一些結合自己面試時遇到的面試題總結了一下,以后有新的還會再加進來。 1. OC 的理解與特性 OC 作為一...
    AlaricMurray閱讀 2,616評論 0 20
  • 基礎 1. 為什么說Objective-C是一門動態的語言? 2. 講一下MVC和MVVM,MVP? 3. 為...
    波妞和醬豆子閱讀 3,361評論 0 46
  • 偶然 翻開那塵封以久的相冊 發現那張發黃的相片 熟悉而陌生的身影 想不起她的名字 憶不起她的聲音 唯一留在腦海中的...
    瘦高的蒸菜閱讀 153評論 0 1
  • 這幾天有點委屈我的胃了,都是隨便吃的,今天決定,好好照顧自己!有點想念媽媽在家給我做的各種釀,雖然我只有一個電飯煲...
    f964145b03d3閱讀 695評論 2 1