iOS底層面試總結

SunshineBrother發表於2018-12-06

前言:這篇文章是我看李明傑老師的iOS底層原理班(下)/OC物件/關聯物件/多執行緒/記憶體管理/效能優化總結所得,斷斷續續歷時3個月左右,把課堂聽的東西給做了一下筆記。

總結不易,耗時耗力,您的一顆小星星✨是我無限的動力。原文地址

iOS底層原理.png


我們經常會看一些面試題,但是好多面試題我們都是知其然不知其所以然,你如果認真的看了我上面總結的幾十篇文章,那麼你也會知其所以然。

OC物件本質

1、一個NSObject物件佔用多少記憶體?

系統分配了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物件

具體實現請參考: 1、一個NSObject物件佔用多少記憶體 2、OC物件的分類

KVO

1、iOS用什麼方式實現對一個物件的KVO?(KVO的本質是什麼?)

  • 利用RuntimeAPI動態生成一個子類,並且讓instance物件的isa指向這個全新的子類
  • 當修改instance物件的屬性時,會呼叫Foundation的_NSSetXXXValueAndNotify函式
    • 1、呼叫willChangeValueForKey方法
    • 2、呼叫setAge方法
    • 3、呼叫didChangeValueForKey方法
    • 4、didChangeValueForKey方法內部呼叫oberser的observeValueForKeyPath:ofObject:change:context:方法

2、如何手動觸發KVO?

手動呼叫willChangeValueForKey:和didChangeValueForKey:

3、直接修改成員變數會觸發KVO麼?

不會觸發KVO

具體實現請參考:3、KVO實現原理

KVC

1、通過KVC修改屬性會觸發KVO麼?

會觸發KVO,因為KVC是呼叫set方法,KVO就是監聽set方法

2、KVC的賦值和取值過程是怎樣的?原理是什麼?

KVO的setValue:forKey原理

KVC2.png

  • 1、按照setKey,_setKey的順序查詢成員方法,如果找到方法,傳遞引數,呼叫方法
  • 2、如果沒有找到,檢視accessInstanceVariablesDirectly的返回值(accessInstanceVariablesDirectly的返回值預設是YES),
    • 返回值為YES,按照_Key,_isKey,Key,isKey的順序查詢成員變數, 如果找到,直接賦值,如果沒有找到,呼叫setValue:forUndefinedKey:,丟擲異常
    • 返回NO,直接呼叫setValue:forUndefinedKey:,丟擲異常

KVO的ValueforKey原理

KVC3.png

  • 1、按照getKey,key,isKey,_key的順序查詢成員方法,如果找到直接呼叫取值
  • 2、如果沒有找到,檢視accessInstanceVariablesDirectly的返回值
    • 返回值為YES,按照_Key,_isKey,Key,isKey的順序查詢成員變數,如果找到,直接取值,如果沒有找到,呼叫setValue:forUndefinedKey:,丟擲異常
    • 返回NO,直接呼叫setValue:forUndefinedKey:,丟擲異常

具體實現請參考:4、KVC實現原理

Category

1、Category的實現原理

  • Category編譯之後的底層結構是struct category_t,裡面儲存著分類的物件方法、類方法、屬性、協議資訊
  • 在程式執行的時候,runtime會將Category的資料,合併到類資訊中(類物件、元類物件中)

2、Category和Class Extension的區別是什麼?

  • Class Extension在編譯的時候,它的資料就已經包含在類資訊中
  • Category是在執行時,才會將資料合併到類資訊中

3、load、initialize方法的區別什麼?

  • 1.呼叫方式

    • 1> load是根據函式地址直接呼叫
    • 2> initialize是通過objc_msgSend呼叫
  • 2.呼叫時刻

    • 1> load是runtime載入類、分類的時候呼叫(只會呼叫1次 )
    • 2> initialize是類第一次接收到訊息的時候呼叫,每一個類只會initialize一次(父類的initialize方法可能會被呼叫多次)

4、load、initialize的呼叫順序

1.load

  • 1> 先呼叫類的load
    • a) 先編譯的類,優先呼叫load
    • b) 呼叫子類的load之前,會先呼叫父類的load
  • 2> 再呼叫分類的load
    • a) 先編譯的分類,優先呼叫load

2.initialize

  • 1> 先初始化父類
  • 2> 再初始化子類(可能最終呼叫的是父類的initialize方法)

5、如何實現給分類“新增成員變數”?

預設情況下,因為分類底層結構的限制,不能新增成員變數到分類中。但可以通過關聯物件來間接實現

關聯物件提供了以下API
新增關聯物件
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

獲得關聯物件
id objc_getAssociatedObject(id object, const void * key)

移除所有的關聯物件
void objc_removeAssociatedObjects(id object)
複製程式碼

具體實現請參考: 5.1、分類的實現原理 5.2、Load和Initialize實現原理

Block

1、block的原理是怎樣的?本質是什麼?

  • block本質上也是一個OC物件,它內部也有個isa指標
  • block是封裝了函式呼叫以及函式呼叫環境的OC物件

block的底層.png

2、block的(capture)

變數捕獲.png

為了保證block內部能夠正常訪問外部的變數,block有個變數捕獲機制

3、Block型別有哪幾種 block有3種型別,可以通過呼叫class方法或者isa指標檢視具體型別,最終都是繼承自NSBlock型別

  • 1、NSGlobalBlock ( _NSConcreteGlobalBlock
  • 2、NSStackBlock ( _NSConcreteStackBlock )
  • 3、NSMallocBlock ( _NSConcreteMallocBlock )

Block型別.png

4、block的copy

在ARC環境下,編譯器會根據情況自動將棧上的block複製到堆上,比如以下情況

  • 1、block作為函式返回值時
  • 2、將block賦值給__strong指標時
  • 3、block作為Cocoa API中方法名含有usingBlock的方法引數時
  • 4、block作為GCD API的方法引數時
MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);

ARC下block屬性的建議寫法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

複製程式碼

5、__block修飾符

  • __block可以用於解決block內部無法修改auto變數值的問題

  • __block不能修飾全域性變數、靜態變數(static)

  • 編譯器會將__block變數包裝成一個物件

  • 當__block變數在棧上時,不會對指向的物件產生強引用

  • 當__block變數被copy到堆時

    • 會呼叫__block變數內部的copy函式
    • copy函式內部會呼叫_Block_object_assign函式
    • _Block_object_assign函式會根據所指向物件的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用(注意:這裡僅限於ARC時會retain,MRC時不會retain)
  • 如果__block變數從堆上移除

    • 會呼叫__block變數內部的dispose函式
    • dispose函式內部會呼叫_Block_object_dispose函式
    • _Block_object_dispose函式會自動釋放指向的物件(release)

6、迴圈引用

  • 用__weak、__unsafe_unretained解決
__unsafe_unretained typeof(self) weakSelf = self;
self.block = ^{
 print(@"%p", weakSelf);
}
複製程式碼
__weak typeof(self) weakSelf = self;
self.block = ^{
 print(@"%p", weakSelf);
}
複製程式碼
  • 用__block解決(必須要呼叫block)
__block id weakSelf = self;
self.block = ^{
weakSelf = nil;
}
self.block();
複製程式碼

具體實現請參考:6、Block底層解密

RunTime

1、講一下 OC 的訊息機制

  • OC中的方法呼叫其實都是轉成了objc_msgSend函式的呼叫,給receiver(方法呼叫者)傳送了一條訊息(selector方法名)
  • objc_msgSend底層有3大階段:訊息傳送(當前類、父類中查詢)、動態方法解析、訊息轉發

2、訊息轉發機制流程

  • 1、訊息傳送
  • 2、動態方法解析
  • 3、訊息轉發

訊息傳送階段

訊息傳送流程是我們平時最經常使用的流程,其他的像動態方法解析和訊息轉發其實是補救措施。具體流程如下

訊息傳送1.png

  • 1、首先判斷訊息接受者receiver是否為nil,如果為nil直接退出訊息傳送
  • 2、如果存在訊息接受者receiverClass,首先在訊息接受者receiverClass的cache中查詢方法,如果找到方法,直接呼叫。如果找不到,往下進行
  • 3、沒有在訊息接受者receiverClass的cache中找到方法,則從receiverClass的class_rw_t中查詢方法,如果找到方法,執行方法,並把該方法快取到receiverClass的cache中;如果沒有找到,往下進行
  • 4、沒有在receiverClass中找到方法,則通過superClass指標找到superClass,也是現在快取中查詢,如果找到,執行方法,並把該方法快取到receiverClass的cache中;如果沒有找到,往下進行
  • 5、沒有在訊息接受者superClass的cache中找到方法,則從superClass的class_rw_t中查詢方法,如果找到方法,執行方法,並把該方法快取到receiverClass的cache中;如果沒有找到,重複4、5步驟。如果找不到了superClass了,往下進行
  • 6、如果在最底層的superClass也找不到該方法,則要轉到動態方法解析

動態方法解析

訊息傳送2.png

開發者可以實現以下方法,來動態新增方法實現

  • +resolveInstanceMethod:
  • +resolveClassMethod: 動態解析過後,會重新走“訊息傳送”的流程,從receiverClass的cache中查詢方法這一步開始執行

訊息轉發

如果方法一個方法在訊息傳送階段沒有找到相關方法,也沒有進行動態方法解析,這個時候就會走到訊息轉發階段了。

訊息傳送6.png

  • 呼叫forwardingTargetForSelector,返回值不為nil時,會呼叫objc_msgSend(返回值, SEL)
  • 呼叫methodSignatureForSelector,返回值不為nil,呼叫forwardInvocation:方法;返回值為nil時,呼叫doesNotRecognizeSelector:方法
  • 開發者可以在forwardInvocation:方法中自定義任何邏輯
  • 以上方法都有物件方法、類方法2個版本(前面可以是加號+,也可以是減號-)

3、什麼是Runtime?平時專案中有用過麼?

  • OC是一門動態性比較強的程式語言,允許很多操作推遲到程式執行時再進行
  • OC的動態性就是由Runtime來支撐和實現的,Runtime是一套C語言的API,封裝了很多動態性相關的函式
  • 平時編寫的OC程式碼,底層都是轉換成了Runtime API進行呼叫

具體應用

  • 利用關聯物件(AssociatedObject)給分類新增屬性
  • 遍歷類的所有成員變數(修改textfield的佔位文字顏色、字典轉模型、自動歸檔解檔)
  • 交換方法實現(交換系統的方法)
  • 利用訊息轉發機制解決方法找不到的異常問題

4、super的本質

  • super呼叫,底層會轉換為objc_msgSendSuper2函式的呼叫,接收2個引數
    • struct objc_super2
    • SEL
  • receiver是訊息接收者
  • current_class是receiver的Class物件

具體實現請參考:

RunLoop

1、講講 RunLoop,專案中有用到嗎? 1、定時器切換的時候,為了保證定時器的準確性,需要新增runLoop 2、在聊天介面,我們需要持續的把聊天資訊存到資料庫中,這個時候需要開啟一個保活執行緒,在這個執行緒中處理

2、runloop內部實現邏輯

每次執行RunLoop,執行緒的RunLoop會自動處理之前未處理的訊息,並通知相關的觀察者。具體順序

  • 1、通知觀察者(observers)RunLoop即將啟動
  • 2、通知觀察者(observers)任何即將要開始的定時器
  • 3、通知觀察者(observers)即將處理source0事件
  • 4、處理source0
  • 5、如果有source1,跳到第9步
  • 6、通知觀察者(observers)執行緒即將進入休眠
  • 7、將執行緒置於休眠知道任一下面的事件發生
    • 1、source0事件觸發
    • 2、定時器啟動
    • 3、外部手動喚醒
  • 8、通知觀察者(observers)執行緒即將喚醒
  • 9、處理喚醒時收到的時間,之後跳回2
    • 1、如果使用者定義的定時器啟動,處理定時器事件
    • 2、如果source0啟動,傳遞相應的訊息
  • 10、通知觀察者RunLoop結束

RunLoop7.png

3、RunLoop與執行緒

  • 每條執行緒都有唯一的一個與之對應的RunLoop物件
  • RunLoop儲存在一個全域性的Dictionary裡,執行緒作為key,RunLoop作為value
  • 執行緒剛建立時並沒有RunLoop物件,RunLoop會在第一次獲取它時建立
  • RunLoop會線上程結束時銷燬
  • 主執行緒的RunLoop已經自動獲取(建立),子執行緒預設沒有開啟RunLoop

4、timer 與 runloop 的關係?

  • 一個RunLoop包含若干個Mode,每個Mode又包含若干個Source0/Source1/Timer/Observer
  • RunLoop啟動時只能選擇其中一個Mode,作為currentMode
  • 如果需要切換Mode,只能退出當前Loop,再重新選擇一個Mode進入
  • 不同組的Source0/Source1/Timer/Observer能分隔開來,互不影響
  • 如果Mode裡沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出

解決定時器在滾動檢視上面失效問題NSTimer新增到兩種RunLoop中

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
複製程式碼
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製程式碼

5、RunLoop有幾種狀態

kCFRunLoopEntry = (1UL << 0), // 即將進入RunLoop 
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer 
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source 
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠 
kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
 kCFRunLoopExit = (1UL << 7),// 即將退出RunLoop
複製程式碼

**6、RunLoop的mode的作用 **

RunLoop的mode的作用 系統註冊了5中mode

kCFRunLoopDefaultMode //App的預設Mode,通常主執行緒是在這個Mode下執行
UITrackingRunLoopMode //介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode // 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
GSEventReceiveRunLoopMode // 接受系統事件的內部 Mode,通常用不到
kCFRunLoopCommonModes //這是一個佔位用的Mode,不是一種真正的Mode
複製程式碼

但是我們只能使用兩種mode

kCFRunLoopDefaultMode //App的預設Mode,通常主執行緒是在這個Mode下執行
UITrackingRunLoopMode //介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
複製程式碼

具體實現請參考:7、RunLoop實現原理

多執行緒

1、你理解的多執行緒? 2、iOS的多執行緒方案有哪幾種?你更傾向於哪一種? 3、你在專案中用過 GCD 嗎? 4、GCD 的佇列型別 5、說一下 OperationQueue 和 GCD 的區別,以及各自的優勢 6、執行緒安全的處理手段有哪些? 使用執行緒鎖

  • 1、OSSpinLock
  • 2、os_unfair_lock
  • 3、pthread_mutex
  • 4、dispatch_semaphore
  • 5、dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • 6、NSLock
  • 7、NSRecursiveLock
  • 8、NSCondition
  • 9、NSConditionLock
  • 10、@synchronized
  • 11、pthread_rwlock
  • 12、dispatch_barrier_async
  • 13、atomic

7、執行緒通訊 執行緒間通訊的體現

  • 1、一個執行緒傳遞資料給另一個執行緒
  • 2、在一個執行緒中執行完特定任務後,轉到另一個執行緒繼續執行任務

1、NSThread 可以先將自己的當前執行緒物件註冊到某個全域性的物件中去,這樣相互之間就可以獲取對方的執行緒物件,然後就可以使用下面的方法進行執行緒間的通訊了,由於主執行緒比較特殊,所以框架直接提供了在主執行緒執行的方法

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
 
複製程式碼

2、GCD

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      
 });
複製程式碼

記憶體管理

1、使用CADisplayLink、NSTimer有什麼注意點? CADisplayLink、NSTimer會對target產生強引用,如果target又對它們產生強引用,那麼就會引發迴圈引用

2、介紹下記憶體的幾大區域

  • 程式碼段:編譯之後的程式碼
  • 資料段
    • 字串常量:比如NSString *str = @"123"
    • 已初始化資料:已初始化的全域性變數、靜態變數等
    • 未初始化資料:未初始化的全域性變數、靜態變數等
  • 棧:函式呼叫開銷,比如區域性變數。分配的記憶體空間地址越來越小
  • 堆:通過alloc、malloc、calloc等動態分配的空間,分配的記憶體空間地址越來越大

3、講一下你對 iOS 記憶體管理的理解 在iOS中,使用引用計數來管理OC物件的記憶體

  • 一個新建立的OC物件引用計數預設是1,當引用計數減為0,OC物件就會銷燬,釋放其佔用的記憶體空間
  • 呼叫retain會讓OC物件的引用計數+1,呼叫release會讓OC物件的引用計數-1

記憶體管理的經驗總結

  • 當呼叫alloc、new、copy、mutableCopy方法返回了一個物件,在不需要這個物件時,要呼叫release或者autorelease來釋放它
  • 想擁有某個物件,就讓它的引用計數+1;不想再擁有某個物件,就讓它的引用計數-1

可以通過以下私有函式來檢視自動釋放池的情況 extern void _objc_autoreleasePoolPrint(void);

4、ARC 都幫我們做了什麼 LLVM + Runtime

  • LVVM生成release程式碼
  • RunTime負責執行

5、weak指標的實現原理 runtime維護了一個weak表,用於儲存指向某個物件的所有weak指標。weak表其實是一個hash(雜湊)表,key是所指物件的地址,Value是weak指標的地址(這個地址的值是所指物件指標的地址)陣列

  • 1、初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址
  • 2、新增引用時:objc_initWeak函式會呼叫 storeWeak() 函式, storeWeak() 的作用是更新指標指向,建立對應的弱引用表
  • 3、釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄

6、autorelease物件在什麼時機會被呼叫release

  • 1、iOS在主執行緒的Runloop中註冊了2個Observer
  • 2、第1個Observer監聽了kCFRunLoopEntry事件,會呼叫objc_autoreleasePoolPush()
  • 3、第2個Observer監聽了kCFRunLoopBeforeWaiting事件,會呼叫objc_autoreleasePoolPop()、objc_autoreleasePoolPush() 監聽了kCFRunLoopBeforeExit事件,會呼叫objc_autoreleasePoolPop() autoreleased 物件是在 runloop 的即將進入休眠時進行釋放的

7、方法裡有區域性物件, 出了方法後會立即釋放嗎 在ARC情況下會立即釋放 在MRC情況下,物件是在 runloop 的即將進入休眠時進行釋放的

文章中可以提煉出來的題目太多了,我這裡也就簡單的總結幾道題,想要了解具體實現請到我的github中找到相關文章進行閱讀。歡迎點贊哦,如果裡面有什麼我理解的不太正確,歡迎提出,我們相互印證

相關文章