iOS底層面試總結

前言:這篇文章是我看李明傑老師的iOS底層原理班(下)/OC對象/關聯對象/多線程/內存管理/性能優化總結所得,斷斷續續歷時3個月左右,把課堂聽的東西給作了一下筆記。git

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

iOS底層原理.png


咱們常常會看一些面試題,可是好多面試題咱們都是知其然不知其因此然,你若是認真的看了我上面總結的幾十篇文章,那麼你也會知其因此然。面試

OC對象本質

一、一個NSObject對象佔用多少內存?數據庫

系統分配了16個字節給NSObject對象(經過malloc_size函數得到),但NSObject對象內部只使用了8個字節的空間(64bit環境下,能夠經過class_getInstanceSize函數得到)編程

二、對象的isa指針指向哪裏?數組

  • instance對象的isa指向class對象
  • class對象的isa指向meta-class對象
  • meta-class對象的isa指向基類的meta-class對象

三、OC的類信息存放在哪裏?緩存

  • 對象方法、屬性、成員變量、協議信息,存放在class對象中
  • 類方法,存放在meta-class對象中
  • 成員變量的具體值,存放在instance對象

具體實現請參考: 一、一個NSObject對象佔用多少內存 二、OC對象的分類安全

KVO

一、iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)性能優化

  • 利用RuntimeAPI動態生成一個子類,而且讓instance對象的isa指向這個全新的子類
  • 當修改instance對象的屬性時,會調用Foundation的_NSSetXXXValueAndNotify函數
    • 一、調用willChangeValueForKey方法
    • 二、調用setAge方法
    • 三、調用didChangeValueForKey方法
    • 四、didChangeValueForKey方法內部調用oberser的observeValueForKeyPath:ofObject:change:context:方法

二、如何手動觸發KVO?bash

手動調用willChangeValueForKey:和didChangeValueForKey:

三、直接修改爲員變量會觸發KVO麼?

不會觸發KVO

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

KVC

一、經過KVC修改屬性會觸發KVO麼?

會觸發KVO,由於KVC是調用set方法,KVO就是監聽set方法

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

KVO的setValue:forKey原理

KVC2.png

  • 一、按照setKey,_setKey的順序查找成員方法,若是找到方法,傳遞參數,調用方法
  • 二、若是沒有找到,查看accessInstanceVariablesDirectly的返回值(accessInstanceVariablesDirectly的返回值默認是YES),
    • 返回值爲YES,按照_Key,_isKey,Key,isKey的順序查找成員變量, 若是找到,直接賦值,若是沒有找到,調用setValue:forUndefinedKey:,拋出異常
    • 返回NO,直接調用setValue:forUndefinedKey:,拋出異常

KVO的ValueforKey原理

KVC3.png

  • 一、按照getKey,key,isKey,_key的順序查找成員方法,若是找到直接調用取值
  • 二、若是沒有找到,查看accessInstanceVariablesDirectly的返回值
    • 返回值爲YES,按照_Key,_isKey,Key,isKey的順序查找成員變量,若是找到,直接取值,若是沒有找到,調用setValue:forUndefinedKey:,拋出異常
    • 返回NO,直接調用setValue:forUndefinedKey:,拋出異常

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

Category

一、Category的實現原理

  • Category編譯以後的底層結構是struct category_t,裏面存儲着分類的對象方法、類方法、屬性、協議信息
  • 在程序運行的時候,runtime會將Category的數據,合併到類信息中(類對象、元類對象中)

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

  • Class Extension在編譯的時候,它的數據就已經包含在類信息中
  • Category是在運行時,纔會將數據合併到類信息中

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

  • 1.調用方式

    • 1> load是根據函數地址直接調用
    • 2> initialize是經過objc_msgSend調用
  • 2.調用時刻

    • 1> load是runtime加載類、分類的時候調用(只會調用1次 )
    • 2> initialize是類第一次接收到消息的時候調用,每個類只會initialize一次(父類的initialize方法可能會被調用屢次)

四、load、initialize的調用順序

1.load

  • 1> 先調用類的load
    • a) 先編譯的類,優先調用load
    • b) 調用子類的load以前,會先調用父類的load
  • 2> 再調用分類的load
    • a) 先編譯的分類,優先調用load

2.initialize

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

五、如何實現給分類「添加成員變量」?

默認狀況下,由於分類底層結構的限制,不能添加成員變量到分類中。但能夠經過關聯對象來間接實現

關聯對象提供瞭如下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.一、分類的實現原理 5.二、Load和Initialize實現原理

Block

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

  • block本質上也是一個OC對象,它內部也有個isa指針
  • block是封裝了函數調用以及函數調用環境的OC對象

block的底層.png

二、block的(capture)

變量捕獲.png

爲了保證block內部可以正常訪問外部的變量,block有個變量捕獲機制

三、Block類型有哪幾種 block有3種類型,能夠經過調用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型

  • 一、NSGlobalBlock ( _NSConcreteGlobalBlock
  • 二、NSStackBlock ( _NSConcreteStackBlock )
  • 三、NSMallocBlock ( _NSConcreteMallocBlock )

Block類型.png

四、block的copy

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

  • 一、block做爲函數返回值時
  • 二、將block賦值給__strong指針時
  • 三、block做爲Cocoa API中方法名含有usingBlock的方法參數時
  • 四、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);

複製代碼

五、__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)

六、循環引用

  • 用__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();
複製代碼

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

RunTime

一、講一下 OC 的消息機制

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

二、消息轉發機制流程

  • 一、消息發送
  • 二、動態方法解析
  • 三、消息轉發

消息發送階段

消息發送流程是咱們平時最常用的流程,其餘的像動態方法解析和消息轉發實際上是補救措施。具體流程以下

消息發送1.png

  • 一、首先判斷消息接受者receiver是否爲nil,若是爲nil直接退出消息發送
  • 二、若是存在消息接受者receiverClass,首先在消息接受者receiverClass的cache中查找方法,若是找到方法,直接調用。若是找不到,往下進行
  • 三、沒有在消息接受者receiverClass的cache中找到方法,則從receiverClass的class_rw_t中查找方法,若是找到方法,執行方法,並把該方法緩存到receiverClass的cache中;若是沒有找到,往下進行
  • 四、沒有在receiverClass中找到方法,則經過superClass指針找到superClass,也是如今緩存中查找,若是找到,執行方法,並把該方法緩存到receiverClass的cache中;若是沒有找到,往下進行
  • 五、沒有在消息接受者superClass的cache中找到方法,則從superClass的class_rw_t中查找方法,若是找到方法,執行方法,並把該方法緩存到receiverClass的cache中;若是沒有找到,重複四、5步驟。若是找不到了superClass了,往下進行
  • 六、若是在最底層的superClass也找不到該方法,則要轉到動態方法解析

動態方法解析

消息發送2.png

開發者能夠實現如下方法,來動態添加方法實現

  • +resolveInstanceMethod:
  • +resolveClassMethod: 動態解析事後,會從新走「消息發送」的流程,從receiverClass的cache中查找方法這一步開始執行

消息轉發

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

消息發送6.png

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

三、什麼是Runtime?平時項目中有用過麼?

  • OC是一門動態性比較強的編程語言,容許不少操做推遲到程序運行時再進行
  • OC的動態性就是由Runtime來支撐和實現的,Runtime是一套C語言的API,封裝了不少動態性相關的函數
  • 平時編寫的OC代碼,底層都是轉換成了Runtime API進行調用

具體應用

  • 利用關聯對象(AssociatedObject)給分類添加屬性
  • 遍歷類的全部成員變量(修改textfield的佔位文字顏色、字典轉模型、自動歸檔解檔)
  • 交換方法實現(交換系統的方法)
  • 利用消息轉發機制解決方法找不到的異常問題

四、super的本質

  • super調用,底層會轉換爲objc_msgSendSuper2函數的調用,接收2個參數
    • struct objc_super2
    • SEL
  • receiver是消息接收者
  • current_class是receiver的Class對象

具體實現請參考:

RunLoop

一、講講 RunLoop,項目中有用到嗎? 一、定時器切換的時候,爲了保證定時器的準確性,須要添加runLoop 二、在聊天界面,咱們須要持續的把聊天信息存到數據庫中,這個時候須要開啓一個保活線程,在這個線程中處理

二、runloop內部實現邏輯

每次運行RunLoop,線程的RunLoop會自動處理以前未處理的消息,並通知相關的觀察者。具體順序

  • 一、通知觀察者(observers)RunLoop即將啓動
  • 二、通知觀察者(observers)任何即將要開始的定時器
  • 三、通知觀察者(observers)即將處理source0事件
  • 四、處理source0
  • 五、若是有source1,跳到第9步
  • 六、通知觀察者(observers)線程即將進入休眠
  • 七、將線程置於休眠知道任一下面的事件發生
    • 一、source0事件觸發
    • 二、定時器啓動
    • 三、外部手動喚醒
  • 八、通知觀察者(observers)線程即將喚醒
  • 九、處理喚醒時收到的時間,以後跳回2
    • 一、若是用戶定義的定時器啓動,處理定時器事件
    • 二、若是source0啓動,傳遞相應的消息
  • 十、通知觀察者RunLoop結束

RunLoop7.png

三、RunLoop與線程

  • 每條線程都有惟一的一個與之對應的RunLoop對象
  • RunLoop保存在一個全局的Dictionary裏,線程做爲key,RunLoop做爲value
  • 線程剛建立時並無RunLoop對象,RunLoop會在第一次獲取它時建立
  • RunLoop會在線程結束時銷燬
  • 主線程的RunLoop已經自動獲取(建立),子線程默認沒有開啓RunLoop

四、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];
複製代碼

五、RunLoop有幾種狀態

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

**六、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 影響
複製代碼

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

多線程

一、你理解的多線程? 二、iOS的多線程方案有哪幾種?你更傾向於哪種? 三、你在項目中用過 GCD 嗎? 四、GCD 的隊列類型 五、說一下 OperationQueue 和 GCD 的區別,以及各自的優點 六、線程安全的處理手段有哪些? 使用線程鎖

  • 一、OSSpinLock
  • 二、os_unfair_lock
  • 三、pthread_mutex
  • 四、dispatch_semaphore
  • 五、dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • 六、NSLock
  • 七、NSRecursiveLock
  • 八、NSCondition
  • 九、NSConditionLock
  • 十、@synchronized
  • 十一、pthread_rwlock
  • 十二、dispatch_barrier_async
  • 1三、atomic

七、線程通信 線程間通訊的體現

  • 一、一個線程傳遞數據給另外一個線程
  • 二、在一個線程中執行完特定任務後,轉到另外一個線程繼續執行任務

一、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);
 
複製代碼

二、GCD

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

內存管理

一、使用CADisplayLink、NSTimer有什麼注意點? CADisplayLink、NSTimer會對target產生強引用,若是target又對它們產生強引用,那麼就會引起循環引用

二、介紹下內存的幾大區域

  • 代碼段:編譯以後的代碼
  • 數據段
    • 字符串常量:好比NSString *str = @"123"
    • 已初始化數據:已初始化的全局變量、靜態變量等
    • 未初始化數據:未初始化的全局變量、靜態變量等
  • 棧:函數調用開銷,好比局部變量。分配的內存空間地址愈來愈小
  • 堆:經過alloc、malloc、calloc等動態分配的空間,分配的內存空間地址愈來愈大

三、講一下你對 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);

四、ARC 都幫咱們作了什麼 LLVM + Runtime

  • LVVM生成release代碼
  • RunTime負責執行

五、weak指針的實現原理 runtime維護了一個weak表,用於存儲指向某個對象的全部weak指針。weak表實際上是一個hash(哈希)表,key是所指對象的地址,Value是weak指針的地址(這個地址的值是所指對象指針的地址)數組

  • 一、初始化時:runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址
  • 二、添加引用時:objc_initWeak函數會調用 storeWeak() 函數, storeWeak() 的做用是更新指針指向,建立對應的弱引用表
  • 三、釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取全部weak指針地址的數組,而後遍歷這個數組把其中的數據設爲nil,最後把這個entry從weak表中刪除,最後清理對象的記錄

六、autorelease對象在什麼時機會被調用release

  • 一、iOS在主線程的Runloop中註冊了2個Observer
  • 二、第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
  • 三、第2個Observer監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() 監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop() autoreleased 對象是在 runloop 的即將進入休眠時進行釋放的

七、方法裏有局部對象, 出了方法後會當即釋放嗎 在ARC狀況下會當即釋放 在MRC狀況下,對象是在 runloop 的即將進入休眠時進行釋放的

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

相關文章
相關標籤/搜索