趁著開年的空閒時間,找了一些面試題寫寫,算是回顧總結一下iOS開發方面的知識, 水平渣,答案僅作參考! 歡迎指導討論,寫的不對或不妥的地方望及時提出! 原題在這裡
iOS常見面試題基礎篇(附參考答案)看這裡
-
Block
-
block的實質是什麼?一共有幾種block?都是什麼情況下生成的?
-
簡單的來講,block在OC中的表現可以看作為帶有自動變數(區域性變數)的匿名函式。
C語言函式定義的時候,可以將函式的地址賦值給函式指標型別的變數,如下:
int func(int count){ return count + 1; } // 賦值 int (*funcprt)(int) = &func; int result = (*funcprt)(10);複製程式碼
同理,我們也可以將block語法賦值給宣告為block型別的變數中。如下:
// 宣告一個block型別的變數,其與函式指標型別的變數不同之處只有'*'改為'^' int (^blk)(int); // 賦值 int (^blk)(int) = ^(int count){return count + 1;}; int result = blk(10);複製程式碼
還可以通過typedef來宣告
blk_t
型別變數,如下:typedef int (^blk_t)(int); blk_t blk = ^(int count){return count + 1;}; int result = blk(10)複製程式碼
以上解釋了匿名函式,現在來解釋一下帶有自動變數
int value = 10; void(^blk)() = ^{ NSLog(@"value === %d", value); }; blk(); value = 2; NSLog(@"%@", value);複製程式碼
value結果為 10 。在block中,block表示式截獲所使用的自動變數的值,即儲存該變數的瞬間值,所以在執行了block後,改變block外自動變數的值,並不會影響block執行時自動變數的值。
-
block的本質
關於block的本質, ibireme大神objc 中的 block這篇部落格裡,有詳細的分析。
struct Block_descriptor_1 { uintptr_t reserved; uintptr_t size; }; struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 *descriptor; // imported variables };複製程式碼
根據block的資料結構可以發現,它是含有
*isa
指標的,在OC中根據物件的定義,凡是首地址是*isa的結構體指標,都可以認為是物件(id)。這樣在objc中,block實際上就算是物件。 -
既然OC處理Block是按照物件來處理的。在iOS中,常見的就是
_NSConcreteStackBlock
,_NSConcreteMallocBlock
,_NSConcreteGlobalBlock
這3種,還有另外幾種,暫不做討論。 -
ARC下:
void(^blockA)() = ^{ NSLog(@"just a block"); }; NSLog(@"%@", blockA); int value = 10; void(^blockB)() = ^{ NSLog(@"just a block === %d", value); }; NSLog(@"%@", blockB);複製程式碼
ARC下列印結果如下:
<__NSGlobalBlock__: 0x10c81c308> <__NSMallocBlock__: 0x60400025d160>
MRC下列印結果如下:
<__NSGlobalBlock__: 0x1056bf308> <__NSStackBlock__: 0x7ffeea540a48>
我們對棧上的block做copy操作:
NSLog(@"%@", [blockB copy])
結果為:
<__NSMallocBlock__: 0x600000444b60>
由此我們可以得出以下結果:
-
當block沒有引用外部區域性變數的時候,block為
全域性block
-
當block引用外部區域性變數的時候,ARC下為
堆block
,MRC下為棧block
,此時對MRC下的棧block進行copy,棧block
就變為堆block
。在ARC下,編譯器會把block從棧拷貝到堆。 -
經驗證,當block只引用靜態變數,全域性變數的時候,block均為
全域性block
-
-
-
為什麼在預設情況下無法修改被block捕獲的變數? __block都做了什麼?
- 預設情況下,在block中是無法修改被block捕獲的自動變數,因為block捕獲自動變數時,僅僅捕獲到該自動變數的值,並非是記憶體地址,因此早block內部無法改變自動變數的值。
- __block的實現原理詳見深入研究 Block 捕獲外部變數和 __block 實現原理。大致意思是說: 帶有
__block
的自動變數,經過編譯後會變成一個結構體,通過結構體中的__forwarding
指標可以訪問到變數,自然就可以修改變數了。
-
模擬一下迴圈引用的一個情況?block實現介面反向傳值如何實現?
-
迴圈引用場景
// 當block作為屬性時: @property(nonatomic, copy) void(^block)(); self.block = ^{ NSLog(@"%@",self); } 複製程式碼
此時會出現警告:
這時,可以用__weak修飾self,避免迴圈引用: -
介面反向傳值
//SecondViewController.h #import < UIKit/UIKit.h> typedef void(^CallBackBlock) (NSString *string); @interface SecondViewController : UIViewController @property (nonatomic,strong)UItextField *textField; @property (nonatomic,copy)CallBackBlcok callBackBlock; @end // 在implementation中新增一個點選事件: - (IBAction)click:(id)sender { self.callBackBlock(_textField.text); [self.navigationController popToRootViewControllerAnimated:YES]; } 複製程式碼
在FirstViewController中:
// FirstViewController.m - (IBAction)push:(id)sender { SecondViewController *secondVC = [[SecondViewController alloc]init] secondVC.callBackBlock = ^(NSString *string){ NSLog(@"string is %@",string); self.label.text = string; }; [self.navigationController pushViewController:secondVC animated:YES]; }複製程式碼
-
-
思考一下這個問題: ARC下會發生什麼? MRC呢?若
blockA
並沒有引用自動變數val
的話情況又是什麼樣?@property(nonatomic, weak) void(^block)(); - (void)viewDidLoad { [superviewDidLoad]; int val = 10; void(^blockA)() = ^{ NSLog(@"val:%d", val); }; NSLog(@"%@", blockA); _block = blockA; } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"%@", _block); }複製程式碼
-
Runtime
-
objc在向一個物件傳送訊息時,發生了什麼?
-
眾所周知,在objc中方法呼叫的本質是發訊息,例如:
[obj message]; // 執行時會轉化為: objc_msgSend(obj, selector) // 當有引數時: [obj message:(id)arg...]; // 執行時會轉化為: objc_msgSend(obj, selector, arg1, arg2, ...)複製程式碼
訊息在執行時才會與方法繫結
當向一個物件傳送訊息時:
1.首先檢測這個 selector 是不是要忽略。比如 Mac OS X 開發,有了垃圾回收就不理會 retain,release 這些函式。
2.檢測這個 selector 的 target 是不是 nil,Objc 允許我們對一個 nil 物件執行任何方法不會 Crash,因為執行時會被忽略掉。
3.如果上面兩步都通過了,那麼就開始查詢這個類的實現 IMP,先從 cache 裡查詢,如果找到了就執行對應的函式去執行相應的程式碼。
4.如果 cache 找不到就找類的方法列表中是否有對應的方法。 如果類的方法列表中找不到就到父類的方法列表中查詢,一直找到 NSObject 類為止。
5.如果還找不到,就要開始進入訊息轉發流程
如下圖所示:
-
-
什麼時候會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?
-
當呼叫某物件上某個方法,而該物件並沒有實現這個方法的時候, 可以通過訊息轉發進行解決。
-
訊息轉發步驟如下:
-
objc執行時會呼叫
+resolveInstanceMethod:
或者+resolveClassMethod:
,讓我們有機會提供一個函式實現。如果你新增了函式,那執行時系統就會重新啟動一次訊息傳送的過程,否則,執行時就會移到下一步,訊息轉發(Message Forwarding)。 -
呼叫
forwardingTargetForSelector:
方法,嘗試找到一個能響應該訊息的物件。如果獲取到,則直接轉發給它。如果返回了nil,繼續下面的動作。 -
呼叫
methodSignatureForSelector:
方法,嘗試獲得一個方法簽名。如果獲取不到,則直接呼叫doesNotRecognizeSelector丟擲異常。如果返回了一個方法簽名,Runtime就會建立一個NSInvocation物件,然後繼續進行第四步 -
呼叫
forwardInvocation:
方法,將地3步獲取到的方法簽名包裝成Invocation
傳入,如何處理就在這裡面了。
如圖所示:
-
-
-
能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?
- 不能向編譯後得到的類中增加例項變數
- 能向執行時建立的類中新增例項變數
- 因為編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 例項變數的連結串列 和 instance_size 例項變數的記憶體大小已經確定,同時runtime 會呼叫 class_setIvarLayout 或 class_setWeakIvarLayout 來處理 strong weak 引用。所以不能向存在的類中新增例項變數。執行時建立的類是可以新增例項變數,呼叫 class_addIvar 函式。但是得在呼叫 objc_allocateClassPair 之後,objc_registerClassPair 之前,原因同上。
-
runtime如何實現weak變數的自動置nil?
- weak修飾符表示該屬性是非擁有關係,執行期系統會將每一個類的weak變數放入相應的一個hash表中,在這個表中以weak變數所指向的物件的記憶體地址為key,當weak指向的物件引用計數為0執行dealloc方法,物件被銷燬,執行期系統通過key去hash表中找到相應的weak物件將他們設定成nil。
-
給類新增一個屬性後,在類結構體裡哪些元素會發生變化?
- 類的結構體如下:
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class *` */複製程式碼
- 當我們給類新增屬性後,例項物件的記憶體大小:
instance_size
和屬性列表:objc_ivar_list *ivars
會發生改變。
- 類的結構體如下:
RunLoop
-
runloop是來做什麼的?runloop和執行緒有什麼關係?主執行緒預設開啟了runloop麼?子執行緒呢?
-
runloop字面的意思就是跑圈,實際上App能一直不停的執行下去,runloop功不可沒!我們來分析一下main函式:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }複製程式碼
如果沒有runloop,執行緒執行完之後就會退出,就不能再執行任務了。這時我們就需要採用一種方式來讓執行緒能夠處理任務,並不退出。所以,我們就有了RunLoop,其中
UIApplicationMain
函式內部幫我們開啟了主執行緒的RunLoop,UIApplicationMain
內部擁有一個無線迴圈的程式碼。 -
runloop與執行緒對應關係如下:
- 一條執行緒對應一個RunLoop物件,每條執行緒都有唯一一個與之對應的RunLoop物件。
- 我們只能在當前執行緒中操作當前執行緒的RunLoop,而不能去操作其他執行緒的RunLoop。
- RunLoop物件在第一次獲取RunLoop時建立,銷燬則是線上程結束的時候。
- 主執行緒的RunLoop物件系統自動幫助我們建立好了(原理如下),而子執行緒的RunLoop物件需要我們主動建立。
-
-
runloop的mode是用來做什麼的?有幾種mode?
-
在ibireme大神的深入理解 Runloop一文中,詳細的介紹了一個Runloop包含了哪些東西,如下圖所示:
Mode代表RunLoop的執行模式,一個RunLoop可以包含若干個Mode,每個Mode又包含若干個Source/Timer/Observer。每次呼叫RunLoop的主函式時,只能指定其中一個Mode,這個Mode被稱作CurrentMode。如果需要切換Mode,只能退出Loop,再重新指定一個Mode進入。這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響。 -
常見的Mode如下:
1.
kCFRunLoopDefaultMode
:App的預設執行模式,通常主執行緒是在這個執行模式下執行 2.UITrackingRunLoopMode
:跟蹤使用者互動事件(用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他Mode影響) 3.UIInitializationRunLoopMode
:在剛啟動App時第進入的第一個 Mode,啟動完成後就不再使用 4.GSEventReceiveRunLoopMode
:接受系統內部事件,通常用不到 5.kCFRunLoopCommonModes
:偽模式,不是一種真正的執行模式
-
-
為什麼把NSTimer物件以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)新增到主執行迴圈以後,滑動scrollview的時候NSTimer卻不動了?
- 1.當我們不做任何操作的時候,RunLoop處於NSDefaultRunLoopMode下。
- 2.而當我們拖動scrollview的時候,RunLoop就結束NSDefaultRunLoopMode,切換到了UITrackingRunLoopMode模式下,這個模式下沒有新增NSTimer,所以我們的NSTimer就不工作了。
- 3.但當我們鬆開滑鼠的時候,RunLoop就結束UITrackingRunLoopMode模式,又切換回NSDefaultRunLoopMode模式,所以NSTimer就又開始正常工作了
- 4.我們可以把NSTimer也新增到UITrackingRunLoopMode模式下,或者開啟新的執行緒,把NSTimer新增到子執行緒的Runloop中,這樣就可以解決滑動時NSTimer失效的問題
-
蘋果是如何實現Autorelease Pool的?
-
autorelease
一個被autorelease修飾的物件會被加到最近的
autoreleasePool
中,當這個autoreleasePool
自身drain的時候,其中的autoreleased物件會被release
-
autoreleasePool是怎麼實現的?
1.
AutoreleasePool
並沒有單獨的結構,而是由若干個AutoreleasePoolPage
以雙向連結串列的形式組合而成2.
AutoreleasePoolPage
物件會記錄autorelease
物件地址3.AutoreleasePool的操作時通過以下這幾個函式實現的:
objc_autoreleasepoolPush
,objc_autoreleasepoolPop
,objc_autorelease
4.
Autorelease
物件是在當前的runloop
迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop
迭代中都加入了autorelease
的Push和Pop
推薦大家閱讀這篇黑幕背後的Autorelease
-
類結構
-
isa指標?(物件的isa,類物件的isa,元類的isa都要說)
- 上面題目中寫了類經過編譯後的結構體,其中包含有
isa
指標,在OC中類也是一種物件,它屬於元類metaClasss
,物件的isa
指標指向類,類的isa
指標指向元類,元類的isa
指標指向父類的元類,一直到根元類,最後根元類的isa
指標指向了自身,如圖:
- 上面題目中寫了類經過編譯後的結構體,其中包含有
-
類方法和例項方法有什麼區別?
- 呼叫方式不同,類方法由類名直接呼叫,例項方法由該類生成的物件呼叫。原因是類方法是在元類結構體的
methodLists
裡面,而例項方法位於類結構體的methodLists
中。 - 類方法不能使用該類的屬性,例項方法可以使用屬性
- 類方法中不能呼叫例項方法,而例項方法中可以呼叫類方法
- 類方法中self代表類本身,例項方法中self代表例項物件本身
- 呼叫方式不同,類方法由類名直接呼叫,例項方法由該類生成的物件呼叫。原因是類方法是在元類結構體的
-
介紹一下分類,能用分類做什麼?內部是如何實現的?它為什麼會覆蓋掉原來的方法?
-
- 擴充套件已有的類
- 分散原類的實現
- 宣告私有方法
- 模擬多繼承
- 公開framework的部分私有方法
-
分類經過編譯後也會成為一個結構體:
struct category_t { const char *name; // 類名 classref_t cls; // 分類所屬的類 struct method_list_t *instanceMethods; // 例項方法列表 struct method_list_t *classMethods; // 類方法列表 struct protocol_list_t *protocols; // 遵循的協議列表 struct property_list_t *instanceProperties; // 屬性列表 };複製程式碼
在執行時,
category
會被附加到類上面,包括把category
的例項方法、協議以及屬性新增到類上和category
的類方法和協議新增到類的metaclass
上。 -
category
的方法沒有完全替換掉原來類已經有的方法,也就是說如果category
和原來類都有methodA,那麼category
附加完成之後,類的方法列表裡會有兩個methodA,category
的方法被放到了新方法列表的前面,而原來類的方法被放到了新方法列表的後面,這也就是我們平常所說的category
的方法會“覆蓋”掉原來類的同名方法,這是因為執行時在查詢方法的時候是順著方法列表的順序查詢的,它只要一找到對應名字的方法,就會罷休^_^,殊不知後面可能還有一樣名字的方法。推薦大家閱讀美團出品的深入理解Objective-C:Category,我是知識的搬運工~ 也歡迎大家給我推薦高質量的文章!獨樂樂不如眾樂樂~
-
-
執行時能增加成員變數麼?能增加屬性麼?如果能,如何增加?如果不能,為什麼?
class_addIvar
給指定的類新增成員變數,但是不能為已經生成的類新增,執行時規定,只能在objc_allocateClassPair
與objc_registerClassPair
兩個函式之間為類新增變數。原因在上面的題目中有過解釋。class_addProperty
給指定的類新增屬性,可以成功新增了屬性但是不能用點呼叫法呼叫,可以利用KVC/關聯方式來該表這個屬性的值runtime
的objc_setAssociatedObject
和objc_getAssociatedObject
方法來實現關聯,給分類新增屬性就是利用這個方法實現的。
-
objc中向一個nil物件傳送訊息將會發生什麼?(返回值是物件,是標量,結構體)
- 向 nil 傳送訊息並不會引起程式crash,只是在執行時不會有任何作用。但是對
[NSNull null]
物件傳送訊息時,是會crash的。 - 當方法返回值為物件的時候, 給nil發訊息返回nil
- 當方法返回值為結構體的時候,給nil發訊息返回0,結構體中的各個引數也是0
- 當方法返回值為指標型別的時候, 給nil發訊息返回0
- 向 nil 傳送訊息並不會引起程式crash,只是在執行時不會有任何作用。但是對
參考部落格: