iOS | 面試知識整理 - OC底層 (三)
前言:
最近公司專案不怎麼忙,閒暇時間把iOS 在面試中可能會遇到的問題整理了一番,一部分題目是自己面試遇到的,一部分題目則是網上收錄的,方便自己鞏固複習,也分享給大家! 知識點比較多,比較雜,這裡做了分類,下面是分類連結地址;
面試知識點整理 - 目錄:
iOS | 面試知識整理 - OC基礎 (一)
iOS | 面試知識整理 - OC基礎 (二)
iOS | 面試知識整理 - OC基礎 (三)
iOS | 面試知識整理 - UI 相 關 (四)
iOS | 面試知識整理 - 記憶體管理 (五)
iOS | 面試知識整理 - 多 線 程 (六)
iOS | 面試知識整理 - 網路相關 (七)
iOS | 面試知識整理 - 資料持久化 (八)
iOS | 面試知識整理 - Swift 基礎 (九)
iOS | 面試知識整理 - OC底層 (三)
1. 一個OC物件佔用多少記憶體
- 系統分配了16個位元組給NSObject物件(通過
malloc_size
函式獲得) - 但NSObject物件內部只使用了8個位元組的空間(64bit環境下,可以通過
class_getInstanceSize
函式獲得)
2. 物件的isa指標指向哪裡?
- instance物件的isa指向class物件
- class物件的isa指向meta-class物件
- meta-class物件的isa指向基類的meta-class物件
3.OC的類資訊存放在哪裡?
- 物件方法、屬性、成員變數、協議資訊,存放在class物件中
- 類方法,存放在meta-class物件中
- 成員變數的具體值,存放在instance物件
4.iOS用什麼方式實現對一個物件的KVO?(KVO的本質是什麼?)
- 利用RuntimeAPI動態生成一個子類,並且讓instance物件的isa指向這個全新的子類
- 當修改instance物件的屬性時,會呼叫Foundation的_NSSetXXXValueAndNotify函式
willChangeValueForKey:
父類原來的setter
didChangeValueForKey: - 內部會觸發監聽器(Oberser)的監聽方法(
observeValueForKeyPath:ofObject:change:context:
5.如何手動觸發KVO?
手動呼叫willChangeValueForKey:和didChangeValueForKey:
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];;
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[p willChangeValueForKey:@"name"];
[p didChangeValueForKey:@"name"];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"被觀測物件:%@,被觀測的屬性:%@,值的改變: %@\n,攜帶資訊:%@",object,keyPath,change,context);
}
複製程式碼
6.直接修改成員變數會觸發KVO麼?
- 不會觸發KVO
7.通過KVC修改屬性會觸發KVO麼?
- 會觸發KVO
- KVC在賦值時候,內部會觸發監聽器(Oberser)的監聽方法(observeValueForKeyPath:ofObject:change:context:) 傳送通知
8.KVC的賦值和取值過程是怎樣的?原理是什麼?
- KVC的全稱是Key-Value Coding,俗稱“鍵值編碼”,可以通過一個key來訪問某個屬性
- 呼叫 setValue:forKey:
setKey,_setKey ->找到了則進行賦值,未找到呼叫accessInstanceVarlableDirctly
是否允許修改value值,返回YES,呼叫_key,_isKey,key,isKey 進行賦值
9.Category的使用場合是什麼?
- 在不修改原有類程式碼的情況下,為類添物件方法或者類方法
- 或者為類關聯新的屬性
- 分解龐大的類檔案
使用場合:
- 新增例項方法
- 新增類方法
- 新增協議
- 新增屬性
- 關聯成員變數
10.Category的實現原理
- Category編譯之後的底層結構是
struct category_t
,裡面儲存著分類的物件方法、類方法、屬性、協議資訊 - 在程式執行的時候,runtime會將Category的資料,合併到類資訊中(類物件、元類物件中)
11.Category和Class Extension的區別是什麼?
- Class Extension在編譯的時候,它的資料就已經包含在類資訊中
- Category是在執行時,才會將資料合併到類資訊中
12.Category中有load方法嗎?load方法是什麼時候呼叫的?load 方法能繼承嗎?
- 有load方法
- load方法在runtime載入類、分類的時候呼叫
- load方法可以繼承,但是一般情況下不會主動去呼叫load方法,都是讓系統自動呼叫
13. initialize方法如何呼叫,以及呼叫時機
- 當類第一次收到訊息的時候會呼叫類的initialize方法
- 是通過 runtime 的訊息機制 objc_msgSend(obj,@selector()) 進行呼叫的
- 優先呼叫分類的 initialize,如果沒有分類會呼叫 子類的,如果子類未實現則呼叫 父類的
13. load、initialize方法的區別什麼?它們在category中的呼叫的順序?以及出現繼承時他們之間的呼叫過程?
- load 是類載入到記憶體時候呼叫,優先父類->子類->分類
- initialize 是類第一次收到訊息時候呼叫,優先分類->子類->父類
- 同級別和編譯順序有關係
- load 方法是在 main 函式之前呼叫的
14. Category能否新增成員變數?如果可以,如何給Category新增成員變數?
- 不能直接給Category新增成員變數,但是可以間接實現Category有成員變數的效果
- Category是發生在執行時,編譯完畢,類的記憶體佈局已經確定,無法新增成員變數(Category的底層資料結構也沒有成員變數的結構)
- 可以通過 runtime 動態的關聯屬性
15. block的原理是怎樣的?本質是什麼?
- block 本質其實是OC物件
- block 內部封裝了函式呼叫以及呼叫環境
16. __block的作用是什麼?有什麼使用注意點?
- 如果需要在 block 內部修改外部的 區域性變數的值,就需要使用__block 修飾(全域性變數和靜態變數不需要加__block 可以修改)
- __block 修飾以後,區域性變數的資料結構就會發生改變,底層會變成一個結構體的物件,結構內部會宣告 一個 __block修飾變數的成員,並且將 __block修飾變數的地址儲存到堆記憶體中. 後面如果修改 這個變數的值,可以通過 isa 指標找到這個結構體,進來修改 這個變數的值;
- 可以在 block 內部修改 變數的值
17. block的屬性修飾詞為什麼是copy?使用block有哪些使用注意?
- block 一旦沒有進行copy操作,就不會在堆上
- 使用注意:迴圈引用問題 (外部使用__weak 解決)
17. block在修改NSMutableArray,需不需要新增__block?
- 如果是操作 NSMutableArray 物件不需要,因為 block 內部拷貝了 NSMutableArray物件的記憶體地址,實際是通過記憶體地址操作的
- 如果 NSMutableArray 物件要重新賦值,就需要加__block
18. Block 內部為什麼不能修改區域性變數,需要加__block
- 通過檢視Block 原始碼,可以發現,block 內部如果單純使用 外部變數,會在 block 內部建立同樣的一個變數,並且將 外部變數的值引用過來..(只是將外部變數值拷貝到 block 內部),內部這個變數和外部 實際已經沒關係了
- 從另一方面分析,block 本質也是一個 函式指標,外部的變數也是一個區域性變數,很有可能 block 在使用這個變數時候,外部變數已經釋放了,會造成錯誤
- 加了__block 以後,會將外部變數的記憶體拷貝到堆中,記憶體由 block 去管理.
19.講一下 OC 的訊息機制
- OC中的方法呼叫其實都是轉成了objc_msgSend函式的呼叫,給receiver(方法呼叫者)傳送了一條訊息(selector方法名)
- objc_msgSend底層有3大階段
- 訊息傳送(當前類、父類中查詢)、
- 動態方法解析、
- 訊息轉發
20. 訊息傳送流程
- 當我們的一個 receiver(例項物件)收到訊息的時候,會通過 isa 指標找到 他的類物件,然後在類物件方法列表中查詢 對應的方法實現,如果 未找到,則會通過 superClass 指標找到其父類的類物件,找到則返回,未找打則會一級一級往上查到,最終到NSObject 物件,如果還是未找到就會進行動態方法解析
- 類方法呼叫同上,只不過 isa 指標找到元類物件;
21. 動態方法解析機制
當我們傳送訊息未找到方法實現,就會進入第二步,動態方法解析: 程式碼實現如下
// 動態方法繫結- 例項法法呼叫
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
Method method = class_getInstanceMethod(self,@selector(test));
class_addMethod(self,sel,method_getImplementation(method),method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 類方法呼叫
+(BOOL) resolveClassMethod:(SEL)sel....
複製程式碼
22.訊息轉發機制流程
未找到動態方法繫結,就會進行訊息轉發階段
// 快速訊息轉發- 指定訊息處理物件
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [Student new];
}
return [super forwardingTargetForSelector:aSelector];
}
// 標準訊息轉發-訊息簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(run))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//內部邏輯自己處理
}
複製程式碼
23. 什麼是Runtime?平時專案中有用過麼?
- Objective-C runtime是一個
執行時
庫,它為Objective-C語言的動態特性提供支援,我們所寫的OC程式碼在執行時都轉成了runtime相關的程式碼,類轉換成C語言對應的結構體,方法轉化為C語言對應的函式,發訊息轉成了C語言對應的函式呼叫。通過了解runtime以及原始碼,可以更加深入的瞭解OC其特性和原理 - OC是一門動態性比較強的程式語言,允許很多操作推遲到程式執行時再進行
- OC的動態性就是由Runtime來支撐和實現的,Runtime是一套C語言的API,封裝了很多動態性相關的函式
- 平時編寫的OC程式碼,底層都是轉換成了Runtime API進行呼叫
22.runtime具體應用
- 利用關聯物件(AssociatedObject)給分類新增屬性
- 遍歷類的所有成員變數(修改textfield的佔位文字顏色、字典轉模型、自動歸檔解檔)
- 交換方法實現(交換系統的方法)
- 利用訊息轉發機制解決方法找不到的異常問題
23.unrecognized selector sent to instance 錯誤
該錯誤是基於OC的訊息機制:
- 在方法列表中未找到方法實現
- 嘗試動態方法解析,也未繫結犯法
- 進行訊息轉發,也未處理
- 最後進行報錯
24.如果向一個nil物件發訊息不會crash的話,那麼message sent to deallocated instance的錯誤是怎麼回事?
- 這是因為這個物件已經被釋放了(引用計數為0了),那麼這個時候再去呼叫方法肯定是會Crash的,因為這個時候這個物件就是一個野指標(指向殭屍物件(物件的引用計數為0,指標指向的記憶體已經不可用)的指標)了,安全的做法是釋放後將物件重新置為nil,使它成為一個空指標
25. 向一個nill物件傳送訊息會發生什麼?
- OC中向nil發訊息,什麼都不會方式,程式是不會崩潰的。
- 因為OC的函式都是通過objc_msgSend進行訊息傳送來實現的,相對於C和C++來說,對於空指標的操作會引起crash問題,而objc_msgSend會通過判斷self來決定是否傳送訊息,如果self為nil,那麼selector也會為空,直接返回,不會出現問題。視方法返回值,向nil發訊息可能會返回nil(返回值為物件),0(返回值為一些基礎資料)或0X0(返回值為id)等。但對於[NSNull null]物件傳送訊息時,是會crash的,因為NSNull類只有一個null方法
26.程式碼列印結果:
@interface Person : NSObject
@end
@implementation Person
@end
@interface Student : Person
@end
@implementation Student
- (instancetype)init{
if (self= [super init]) {
NSLog(@"%@",[self class]);
NSLog(@"%@",[super class]);
NSLog(@"%@",[self superclass]);
NSLog(@"%@",[super superclass]);
}
}
[self class] 和 [super class] 都是給當前類返送訊息,spuer 表示在父類中查詢
[self superClass] 和 [super superclass] 也是也當前類發訊息,返回父類
第一個列印:
Student / Student/ Person / Person
複製程式碼
27.程式碼執行結果?
BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [[Person class] isKindOfClass:[Person class]];
BOOL res4 = [[Person class] isMemberOfClass:[Person class]];
NSLog(@"%d-%d-%d-%d",res1,res2,res3,res4);
複製程式碼
- isKindOfClass 表示物件是否為當前類或者子類的 型別
- isMemberOfClass 表示是否為當前類的的型別
- isMemberOfClass 分為- 物件方法 和+ 類方法2中
- (bool)isMemberOfClass; 比較的是類物件
+ (bool)isMemberOfClass; 比較的是元類
列印結果: 1,0
28.講講 RunLoop,專案中有用到嗎?
- runloop執行迴圈,保證程式一直執行,主執行緒預設開啟
- 用於處理執行緒上的各種事件,定時器等
- 可以提高程式效能,節約CPU資源,有事情做就做,沒事情做就讓執行緒休眠
- 應用範疇:
定時器,事件響應,手勢識別,介面重新整理,以及autoreleasePool 等等
29.runloop內部實現邏輯?
- 實際上 RunLoop 就是這樣一個函式,其內部是一個 do-while 迴圈。當你呼叫 CFRunLoopRun() 時,執行緒就會一直停留在這個迴圈裡;直到超時或被手動停止,該函式才會返回。
30.runloop和執行緒的關係?
- 每條執行緒都有唯一的一個與之對應的RunLoop物件
- RunLoop儲存在一個全域性的Dictionary裡,執行緒作為key,RunLoop作為value
- 執行緒剛建立時並沒有RunLoop物件,RunLoop會在第一次獲取它時建立
- RunLoop會線上程結束時銷燬
- 主執行緒的RunLoop已經自動獲取(建立),子執行緒預設沒有開啟RunLoop
31.timer 與 runloop 的關係?
- timer 定時器,是基於 runloop 來實現的,runloop 在執行迴圈當中,監聽到了定製器 就會執行;所以 timer 需要新增到 runloop 中去,注意子執行緒的 runloop 預設是不開啟的,如果在子執行緒執行 timer 需要手動開啟 runloop
32.程式中新增每3秒響應一次的NSTimer,當拖動tableview時timer可能無法響應要怎麼解決?
- 將 timer 物件新增到 runloop 中,並修改 runloop 的執行 mode
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:nil];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製程式碼
33. runloop的mode作用是什麼?
runloop 只能在一種 mode 下執行,做不同的事情,runloop 會切換到對應的 model 下來執行,預設是 kCFRunLoopDefaultMode 如果檢視滑動再回切換到 UITrackingRunLoopMode,如果需要在多種 mode 下執行則需要手動設定 kCFRunLoopCommonModes;
- kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行
- UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
- UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用,會切換到kCFRunLoopDefaultMode
- GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
- kCFRunLoopCommonModes: 這是一個佔位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並不是一種真正的Mode
34.使用method swizzling要注意什麼?
- 方式無限迴圈
- 進行版本迭代的時候需要進行一些檢驗,防止系統庫的函式發生了變化
35. 一個系統方法被 多次交換,會有什麼影響嗎?以及呼叫順序?原理
都會執行,後交換的會先呼叫.
第一次交換 viewwillAppAppear 和 test1 的指向的方法實現地址發生變化
第二次交換 viewwillAppAppear 和 test2 實際上等於是 test2 和 test1 進行了交換,因為 viewwillAppAppear 已經變為了 test1了.
呼叫 --> viewwillAppAppear
實際呼叫順序 -->test2--->test1-->viewwillAppAppear
形成一個閉環:viewwillAppAppear 也只會呼叫一次
複製程式碼
36.runloop 主執行緒監聽卡頓
- 使用者層面感知的卡頓都是來自處理所有UI的主執行緒上,包括在主執行緒上進行的大計算,大量的IO操作,或者比較重的繪製工作。
- 如何監控主執行緒呢,首先需要知道的是主執行緒和其它執行緒一樣都是靠NSRunLoop來驅動的。可以先看看CFRunLoopRun的大概的邏輯,不難發現NSRunLoop呼叫方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之後,也就是如果我們發現這兩個時間內耗時太長,那麼就可以判定出此時主執行緒卡頓.只需要另外再開啟一個執行緒,實時計算這兩個狀態區域之間的耗時是否到達某個閥值,便能揪出這些效能殺手.
- 用GCD裡的dispatch_semaphore_t開啟一個新執行緒,設定一個極限值和出現次數的值,然後獲取主執行緒上在kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting兩個狀態之間的超過了極限值和出現次數的場景,將堆疊dump下來,最後發到伺服器做收集,通過堆疊能夠找到對應出問題的那個方法。
- (void)start
{
if (observer)
return;
// // 建立訊號
semaphore = dispatch_semaphore_create(0);
// 註冊RunLoop狀態觀察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,&runLoopObserverCallBack,&context);
CFRunLoopAddObserver(CFRunLoopGetMain(),observer,kCFRunLoopCommonModes);
// 在子執行緒監控時長
dispatch_async(dispatch_get_global_queue(0,0),^{
while (YES)
{
// 假定連續5次超時50ms認為卡頓(當然也包含了單次超時250ms)
long st = dispatch_semaphore_wait(semaphore,dispatch_time(DISPATCH_TIME_NOW,50*NSEC_PER_MSEC));
// Returns zero on success,or non-zero if the timeout occurred.
if (st != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
// kCFRunLoopBeforeSources 即將處理source kCFRunLoopAfterWaiting 剛從睡眠中喚醒
// RunLoop會一直迴圈檢測,從執行緒start到執行緒end,檢測檢測到事件源(CFRunLoopSourceRef)執行處理函式,首先會產生通知,corefunction向執行緒新增runloopObservers來監聽事件,並控制NSRunLoop裡面執行緒的執行和休眠,在有事情做的時候使當前NSRunLoop控制的執行緒工作,沒有事情做讓當前NSRunLoop的控制的執行緒休眠。
if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 3)
continue;
NSLog(@"有點兒卡");
}
}
timeoutCount = 0;
}
});
}
複製程式碼
37. _objc_msgForward 函式是做什麼的?直接 呼叫它將會發生什麼?
- _objc_msgForward 是 IMP 型別,用於訊息轉發的:當向一個物件傳送一條訊息,但 它並沒有實現的時候,_objc_msgForward 會嘗試做訊息轉發
- 直接呼叫_objc_msgForward 是非常危險的事,這是把雙刃刀,如果用不好會直接 導致程式 Crash,但是如果用得好,能做很多非常酷的事
- JSPatch 就是直接呼叫_objc_msgForward 來實現其核心功能的
38. 如何列印一個類中的所有例項變數
OC的類實際上是一個objc_class型別的結構體,包含了例項變數列表: (objc_ivar_list),可以通過 runtime 函式來獲取這個列表:OBJC_EXPORT Ivar _Nonnull * _Nullable class_copyIvarList(Class _Nullable cls,unsigned int * _Nullable outCount)
例子:
Student *stu = [[Student alloc]init];
stu.stu_name = @"alex";
stu.stu_age = 10;
unsigned int count = 0;
Ivar *list = class_copyIvarList([stu class],&count);
NSMutableDictionary * dict = [NSMutableDictionary dictionary];
for (int i = 0; i< count; i++){
id iVarName = [NSString stringWithUTF8String:ivar_getName(list[i])];
dict[iVarName] = [stu valueForKey:iVarName];
}
NSLog(@"%@",dict);
複製程式碼
39. 如何使用 rumtime 動態新增一個類
runtime 很強大.可以動態的建立一個全新的類或物件
// 新增一個繼承NSObject的類 類名是MyClass
Class MyClass = objc_allocateClassPair([NSObject class],"MyClass",0);
// 增加例項變數
class_addIvar(MyClass,"_age",sizeof(NSString *),"@");
//註冊這個類到runtime系統中就可以使用他了
objc_registerClassPair(MyClass);
//生成了一個例項化物件
id myobj = [[MyClass alloc] init];
//給剛剛新增的變數賦值
[myobj setValue:@30 forKey:@"age"];
// 列印
NSLog(@"age= %@",[myobj valueForKey:@"age"]);
複製程式碼
下一篇入口:
其實呢作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是我的微信 大家有興趣可以新增 邀請小夥伴們進入微信群裡一起 交流(想要進入的可加小編微信17512010526)
作者:LEON_iOS
連結:www.jianshu.com/p/f0504db3a…