OC 語法底層基礎
文章目錄
分類(實現機制,原理等)
分類都做了哪些事情?
- 聲明一些私有方法(對外不暴露)
- 分解體積龐大的類文件(根據不同功能組織到不同的分類中,方便多人共同開發一個類)
- 把Framework的私有方法公開
- 模擬多繼承(可以模擬多繼承的還有protocol、NSProxy)
特點
- 運行時決議 (只有在運行時纔會通過runtime添加到宿主類中)
- 可以爲系統類添加分類
- 分類可以訪問原來類的成員變量
- 分類如果和原來類有同名方法,優先調用分類的(分類(最後參與編譯)—>原來類->父類)
分類中都可以添加哪些內容?
- 實例方法
- 類方法
- 協議
- 屬性(實際上只聲明瞭getter和setter方法,並未在分類中添加實例變量(可以通過關聯對象添加實例變量))
Category的底層結構
typedef struct category_t *Category;
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; //實例屬性列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
分類加載調用棧:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-n8gyviLP-1584405995454)(media/15457312839940/15457936552352.jpg)]
images (指的是鏡像而不是圖片)
源碼解讀順序
-
objc-os.mm
- _objc_init
- map_images
- map_images_nolock
-
objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attchLists
- realloc、memmove 、memcpy
-
分類添加的方法可以"覆蓋(不是覆蓋只是分類的方法比原類的方法靠前)"原類方法
-
同名分類方法誰能生效取決於編譯順序
-
名字相同的分類會引起編譯報錯
實現原理
- Category 編譯之後的底層結構是 struct category_t,裏面存儲着分類的對象方法、類方法、屬性、協議等信息
- 在程序運行時,Runtime 會將 Category 的數據,合併到類信息中
Category 的加載處理過程
- 通過 Runtime 加載某個類的所有的 Category 數據
- 把所有 Category 的方法、屬性、協議數據,合併到一個大數組中,後面把參與編譯的Category 數據,會在數組的前面(會優先調用)
- 將合併後的分類數據(方法、屬性、協議),插入到類原來數據的前面(memmove、 memcpy)
- 分類最後參與編譯,分類的方法列表會追加到原來類的方法列表中,並且是在前面,所以調用類的方法時去方法列表中找的時候先找到前面分類的實現,類似被覆蓋的效果( memmove memcpy,先把原來的方法往後移動,再把分類的方法列表拷貝到原來的位置)
常見面試題
-
Category 和 Class Extension 的區別是什麼?
- Class Extension 在編譯的時候(編譯時決議),他的數據就已經包含在類信息中
- Category 是在運行時(運行時決議),纔會將數據合併到類信息中
- 擴展只以聲明的形式存在,多數情況下寄生於宿主類的.m文件中;
- 不能爲系統類添加擴展,分類可以爲系統類添加擴展
-
Category 中有 load 方法嗎? load 方法是什麼時候調用的? load 方法能繼承嗎?
- 有load 方法
- load 方法在 Runtime 加載類、分類的時候調用
- load 方法可以繼承,但是一般情況下不會主動去調用 load 方法,都是讓系統自動調用
-
laod initialize 方法的區別是什麼? 他們在 Category 中的調用順序? 以及出現繼承時他們之間的調用過程?
- 區別:
- 1、調用方式的區別:
- load 是根據函數地址直接調用
- initialize 是通過 objc_msgSend 調用
- 2、調用時刻的區別:
- load 是 runtime 加載類、分類的時候調用(只會調用1次)
- initialize 是類第一次接受到消息的時候調用,每一個類只會 initialize 一次 (父類的initialize 可能會被調用多次)
- 1、調用方式的區別:
- 調用順序:
- load
- 先調用類的 load
- 先編譯的類,優先調用load
- 調用子類的load 之前,會先調用父類的load
- 再調用分類的load
- 先調用的分類,優先調用load
- 先調用類的 load
- initialize
- 先初始化父類
- 再初始化子類 (可能最終調用的是父類的initialize 方法)
- load
- 區別:
-
Category 能否添加成員變量?如果可以,如何給Category 添加成員變量?
- 不能直接給 Category 添加成員變量,但是可以間接實現 Category 有成員變量的效果
load 方法
-
+load 方法會在 runtime 加載類 、分類 時調用
-
每個類、分類的 +load 在程序運行過程中只調用一次
-
調用順序
- 先調用類的 +load
- 按照編譯先後順序調用(先編譯,先調用)
- 調用子類的 +load 之前會先調用父類的 +load 方法
- 再調用分類的 +load
- 按照編譯先後順序調用(先編譯,先調用)
- 先調用類的 +load
-
objc4源碼解讀過程:objc-os.mm
_objc_init load_images prepare_load_methods schedule_class_load add_class_to_loadable_list add_category_to_loadable_list call_load_methods call_class_loads call_category_loads (*load_method)(cls, SEL_load)
+load方法是根據方法地址直接調用,並不是經過objc_msgSend函數調用
initialize 方法
-
+initialize 方法會在 類 第一次接收到消息時調用
-
調用順序:
- 先調用父類的 +initialize , 再調用子類的 +initialize(沒有用到子類(沒有繼承關係)就只調用類的 +initialize。分類調用 +initialize 與編譯順序有關係)
- 先初始化父類,再初始化子類,每個類只會初始化1次
-
+initialize 和 + load 區別是:
- +initialize 是通過 objc_msgSend 進行調用的
- 如果子類沒有實現+initialize ,會調用父類的 +initialize(所以父類的+initialize 可能會被調用多次)
- 如果分類實現了 +initialize 就會覆蓋本身類的 +initialize 調用
- +load方法是根據方法地址直接調用,並不是經過objc_msgSend函數調用
- +initialize 是通過 objc_msgSend 進行調用的
-
objc4源碼解讀過程
objc-msg-arm64.s objc_msgSend objc-runtime-new.mm class_getInstanceMethod lookUpImpOrNil lookUpImpOrForward _class_initialize callInitialize objc_msgSend(cls, SEL_initialize)
關聯對象
類和分類添加屬性的區別
類 添加屬性會自動生成成員變量,getter和setter 方法聲明 和 對應的實現
分類 添加屬性會自動生成成員變量,getter和setter 方法聲明,但是不會生成實現
{
int _age;
}
- (void)setAge:(int)age;
-(int)age;
//@property (nonatomic,assign) int age;
/*
以上代碼
1、會自動生成成員變量
2、生成 setter 和 getter 方法的聲明
3、生成 setter 和 getter 方法的實現
*/
- (void)setAge:(int)age {
_age = age;
}
-(int)age {
return _age;
}
分類添加“成員變量”
關聯對象並不是把成員變量添加到實例對象的屬性列表中,而是自己維護的一個 hashMap
//添加關聯對象
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)
//獲取關聯對象
id objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
//移除所有的關聯對象
void objc_removeAssociatedObjects(id _Nonnull object)
@implementation Person (Test)
/* key 的方式可以寫成多種形式
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
使用屬性名作爲key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
使用get方法的@selecor作爲key (常用的方式)
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
*/
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @"name");
}
//常用以下方式
- (void)setWeight:(int)weight {
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_ASSIGN);
}
- (int)weight {
//_cmd = @selector(weight)
return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
關聯對象的實現
- 實現關聯對象技術的核心對象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
源碼解讀:
class AssociationsManager {
static AssociationsHashMap *_map;
......
};
//key : disguised_ptr_t value : ObjectAssociationMap *
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator>
// key : ObjcAssociation value : ObjectPointerLess
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator>
class ObjcAssociation {
uintptr_t _policy;
id _value;
......
}
- 關聯對象的原理
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yhnJKWcU-1584405995456)(media/15457312839940/15638762809616.jpg)]
AssociationsManager 是一個全局的管理者,內部是一個字典(AssociationsHashMap) ,key 是傳進去的object對象,value 也是一字典(ObjectAssociationMap ),key 是第三個參數 key ,value 是一個 ObjcAssociation ,裏面存放的是 policy 和 value
擴展(Extension 和分類的區別)
一般用擴展做什麼?
- 聲明私有屬性
- 聲明私有方法(便於閱讀,沒有太大意義)
- 聲明私有成員變量
分類和擴展的區別
- 擴展是編譯時決議,而分類是運行時決議;
- 擴展只以聲明的形式存在,多數情況下寄生於宿主類的.m文件中;
- 不能爲系統類添加擴展,分類可以爲系統類添加擴展
代理
介紹代理
- 準確來說是一種設計模式(代理模式)
- iOS中以
@protocol
形式體現 - 傳遞方式
一對一
工作流程
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-iQpEWr0F-1584405995457)(media/15457312839940/15815242180051.jpg)]
協議中可以定義方法和屬性
使用注意
- 一般聲明爲 weak 以規避循環引用
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FnFKaUzo-1584405995460)(media/15457312839940/15458089417658.jpg)]
通知實現機制( NSNotification 原理)
特點
- 使用
觀察者模式
來實現的用於跨層傳遞信息的機制; - 傳遞方法是
一對多
; - 通知是松耦合的,通知方不需要知道被通知方的任何情況,而 delegate 不行
- 通知的效率比 delegate 略低
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HMsIe6FB-1584405995462)(media/15457312839940/15841812461183.jpg)]
如何實現通知機制?
NSNotificationCenter 內部會維護一個map表(字典) ,key 是 notificationName
value 是 一個數組列表
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uCCIVU3D-1584405995464)(media/15457312839940/15458092549569.jpg)]
KVO (實現機制)
什麼是KVO ?KVO 的實現機制是什麼?
-
KVO 是 OC 對
觀察者設計模式
的一種實現; -
使用了
isa混寫(isa-swizzling)
來實現的;(其實就是在調用了addObserver:之後系統動態創建一個派生類(NSKVONotifying_XXX),並把isa 指針指向了派生類)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gss2VyP1-1584405995465)(media/15457312839940/15458095894570.jpg)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WE1zJyGN-1584405995466)(media/15457312839940/15458099584582.jpg)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oD8tGHiD-1584405995468)(media/15457312839940/15458099689254.jpg)] -
通過KVC 設置的value 能否生效?爲什麼?
能夠生效;
考察的KVC的實現機制,KVC 調用的時候會調用setter方法,而 setter 方法被派生類重寫了,就會調用派生類的方法,這樣就會觸發KVO 。 -
通過成員變量直接賦值value 能否生效?(手動KVO)
不能夠生效;
通過添加willChangeValueForKey:
和didChangeValueForKey:
使其生效。
觸發KVO 的方式:
- 使用setter 方法改變值 KVO 纔會生效。
- 使用setValue:forKey: 改變值KVO 纔會生效
- 成員變量直接修改
需要手動添加
KVO 纔會生效
KVC
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GyWStZ7I-1584405995475)(media/15457312839940/15458120083176.jpg)]
KVC就是指iOS的開發中,可以允許開發者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不需要調用明確的存取方法。這樣就可以在運行時動態地訪問和修改對象的屬性。而不是在編譯時確定,這也是iOS開發中的黑魔法之一。很多高級的iOS開發技巧都是基於KVC實現的
- 通過
鍵值編碼
是否有違背面向對象的編程思想?
知道一個類或者實例內部私有成員名稱的情況下可以通過KVC 進行設置和取值的,是破壞了面向對象編程思想的。
KVC 原理
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uXEWHm3N-1584405995476)(media/15457312839940/15458122258873.jpg)]
訪問器方法是否存在流程:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hvcrrAcC-1584405995477)(media/15457312839940/15458122868788.jpg)]
實例變量說明:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WYZcfyk3-1584405995478)(media/15457312839940/15458123529484.jpg)]
setValue:forKey: 調用流程:
setKey _setKey
accessInstanceVariablesDirectly 返回YES
_key _isKey key isKey
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-U3e4MR4I-1584405995479)(media/15457312839940/15458124088385.jpg)]
- 程序優先調用set:屬性值方法,代碼通過setter方法完成設置。注意,這裏的是指成員變量名,首字母大小寫要符合KVC的命名規則,下同
- 如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那麼在這一步KVC會執行setValue:forUndefinedKey:方法,不過一般開發者不會這麼做。所以KVC機制會搜索該類裏面有沒有名爲的成員變量,無論該變量是在類接口處定義,還是在類實現處定義,也無論用了什麼樣的訪問修飾符,只在存在以命名的變量,KVC都可以對該成員變量賦值。
- 如果該類即沒有set:方法,也沒有_成員變量,KVC機制會搜索_is的成員變量。
- 和上面一樣,如果該類即沒有set:方法,也沒有_和_is成員變量,KVC機制再會繼續搜索和is的成員變量。再給它們賦值。
- 如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。
- 即如果沒有找到Set方法的話,會按照_key,_iskey,key,iskey的順序搜索成員並進行賦值操作。
- 如果開發者想讓這個類禁用KVC,那麼重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set:屬性名時,會直接用setValue:forUndefinedKey:方法。
valueForKey 調用流程:
getKey key isKey _key
accessInstanceVariablesDirectly 返回yes
_key _isKey key isKey
- 首先按get,,is的順序方法查找getter方法,找到的話會直接調用。如果是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。
- 如果上面的getter沒有找到,KVC則會查找countOf,objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外兩個方法中的一個被找到,那麼就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬於NSArray的方法,就會以countOf,objectInAtIndex或AtIndexes這幾個方法組合的形式調用。還有一個可選的get:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。
- 如果上面的方法沒有找到,那麼會同時查找countOf,enumeratorOf,memberOf格式的方法。如果這三個方法都找到,那麼就返回一個可以響應NSSet所的方法的代理集合,和上面一樣,給這個代理集合發NSSet的消息,就會以countOf,enumeratorOf,memberOf組合的形式調用。
- 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行爲),那麼和先前的設值一樣,會按_,_is,,is的順序搜索成員變量名,這裏不推薦這麼做,因爲這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那麼會直接調用valueForUndefinedKey:方法,默認是拋出異常。
屬性關鍵字 (weak/assign/copy/strong)
- 讀寫權限
- 原子性
- 引用計數
讀寫權限
- readonly
- readwrite(默認)
原子性
- atomic (默認)
賦值和獲取是線程安全的(成員屬性的直接賦值和獲取) 不代表操作和訪問,比如說對一個數組的賦值和獲取是線程安全的,但是對數組進行操作比如添加和移除是不保證線程安全的。 - nonatomic
引用計數
-
retain(MRC)/strong(ARC)
-
assign/unsafe_unretained(MRC ARC幾乎不行)
-
weak/copy
-
assgin 和 weak 區別:
assgin:
- 修飾基本數據類型,比如int Bool 等。
- 修飾對象類型時,不改變其引用計數。
- 會產生懸垂指針。(assgin 修飾的對象被釋放後,
assgin指針仍然指向原來對象的地址
,如果繼續訪問的話就會導致異常)
weak:
- 不改變被修飾對象的引用計數
- 所指
對象在被釋放之後會自動置爲nil
。
相同點:都不改變引用計數。
- 爲什麼weak 修飾的對象被釋放之後會自動置爲nil ?<內存管理章節>
-
copy(深拷貝淺拷貝 重點)
- 淺拷貝是對內存地址的複製,讓目標對象指針和源對象指向
同一片
內存空間。- 會增加引用計數
- 並未發生新的內存分配
- 讓目標對象指針和源對象指針指向
兩片
內容相同(不是同一塊內存)的內存空間。- 不會增加引用計數
- 產生了新的內存分配
- 區分
- 看是否有新的內存分配
- 看是否增加引用計數
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8BYPcfVa-1584405995481)(media/15457312839940/15840938160407.jpg)]
- 淺拷貝是對內存地址的複製,讓目標對象指針和源對象指向
可變對象copy 和 mutableCopy 都是深拷貝。
不可變對象的copy 是淺拷貝,mutableCopy 是深拷貝。
copy 方法返回的都是不可變對象
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gwD8QIuz-1584405995481)(media/15457312839940/15458149050100.jpg)]
MRC下如何重寫retain 修飾變量的setter 方法?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aIX2m0tn-1584405995483)(media/15457312839940/15458150305498.jpg)]