前言:這篇文章是我看李明傑老師的iOS底層原理班(下)/OC物件/關聯物件/多執行緒/記憶體管理/效能優化總結所得,斷斷續續歷時3個月左右,把課堂聽的東西給做了一下筆記。
- 1、一個NSObject物件佔用多少記憶體
- 2、OC物件的分類
- 3、KVO實現原理
- 4、KVC實現原理
- 5、分類
- 6、Block底層解密
- 7、RunLoop實現原理
- 8、RunTime實現原理
- 9、多執行緒
- 10、記憶體管理
總結不易,耗時耗力,您的一顆小星星✨是我無限的動力。原文地址
我們經常會看一些面試題,但是好多面試題我們都是知其然不知其所以然,你如果認真的看了我上面總結的幾十篇文章,那麼你也會知其所以然。
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:
方法
- 1、呼叫
2、如何手動觸發KVO?
手動呼叫willChangeValueForKey:和didChangeValueForKey:
3、直接修改成員變數會觸發KVO麼?
不會觸發KVO
具體實現請參考:3、KVO實現原理
KVC
1、通過KVC修改屬性會觸發KVO麼?
會觸發KVO,因為KVC是呼叫set
方法,KVO就是監聽set
方法
2、KVC的賦值和取值過程是怎樣的?原理是什麼?
KVO的setValue:forKey原理
- 1、按照setKey,_setKey的順序查詢成員方法,如果找到方法,傳遞引數,呼叫方法
- 2、如果沒有找到,檢視accessInstanceVariablesDirectly的返回值(accessInstanceVariablesDirectly的返回值預設是YES),
- 返回值為YES,按照_Key,_isKey,Key,isKey的順序查詢成員變數, 如果找到,直接賦值,如果沒有找到,呼叫setValue:forUndefinedKey:,丟擲異常
- 返回NO,直接呼叫setValue:forUndefinedKey:,丟擲異常
KVO的ValueforKey原理
- 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物件
2、block的(capture)
為了保證block內部能夠正常訪問外部的變數,block有個變數捕獲機制
3、Block型別有哪幾種 block有3種型別,可以通過呼叫class方法或者isa指標檢視具體型別,最終都是繼承自NSBlock型別
- 1、NSGlobalBlock ( _NSConcreteGlobalBlock
- 2、NSStackBlock ( _NSConcreteStackBlock )
- 3、NSMallocBlock ( _NSConcreteMallocBlock )
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、首先判斷訊息接受者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也找不到該方法,則要轉到動態方法解析
動態方法解析
開發者可以實現以下方法,來動態新增方法實現
+resolveInstanceMethod:
+resolveClassMethod:
動態解析過後,會重新走“訊息傳送”的流程,從receiverClass的cache中查詢方法這一步開始執行
訊息轉發
如果方法一個方法在訊息傳送階段沒有找到相關方法,也沒有進行動態方法解析,這個時候就會走到訊息轉發階段了。
- 呼叫
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物件
具體實現請參考:
- 8.1、isa解析
- 8.2、方法快取
- 8.3、objc_msgSend執行流程
- 8.4、@dynamic關鍵字
- 8.5、Class和SuperClass區別
- 8.6、isKindOfClass和isMemberOfClass區別
- 8.7、RunTime的相關API
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結束
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中找到相關文章進行閱讀。歡迎點贊哦,如果裡面有什麼我理解的不太正確,歡迎提出,我們相互印證