本文探索源碼 objc4
( git開源地址 )c++
Category
?
Category
是 Object-C 2.0 以後添加的語言特性,Category
的主要做用是爲已經存在的類添加方法.git
Extension
?
extension
一般咱們稱之爲擴展、延展、匿名分類。extension
看起來很像一個匿名的category
,可是extension
和category
幾乎徹底是兩個東西。和category
不一樣的是extension
不但能夠聲明方法,還能夠聲明屬性、成員變量。extension
通常用於聲明私有方法,私有屬性,私有成員變量。github
Category
和 Extension
有什麼區別 ?讓咱們從多個方面來回答這個問題。數組
Category
是一個 .h 和一個 .m.bash
Extension
是一個 .h . (固然,也能夠在一個類的 .m 中伴生, 寫法就是 @interface ***
/.../ @end
就很少說了.) 數據結構
Extension
是一個類的一部分, 它在 編譯期 和頭文件中@interface
, 實現文件中的@implement
一塊兒造成一個完整類 ,Extension
伴隨類的產生而產生 , 亦隨之一塊兒消亡.Extension
能夠添加實例變量.Extension
通常用來隱藏類的私有信息 , 它沒法直接爲系統類提供擴展 , 但能夠縣建立系統類的子類 , 而後添加擴展.
舉個🌰 app
p.objExtension = 28;
NSLog(@"%d",p.objExtension);
複製代碼
如上使用, 發生崩潰.ide
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[LBPerson setObjExtension:]: unrecognized selector sent to instance 0x6000008ed4e0'
複製代碼
將 NSObject
修改成 LBPerson
, 結果正常.函數
Category
是在運行時期決議. 這個時期對象的內存佈局已經肯定了 , 若是再添加實例變量會破壞類的內部佈局 , 這在編譯型語言是災難性的.佈局
Category
能夠給系統類添加分類.
Category
能夠添加屬性 , 可是並不會生成成員變量和對應的getter
以及setter
方法 .
一樣 , 咱們實驗一下 : 🌰
// LBPerson+Category.h
#import "LBPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LBPerson (Category)
@property (nonatomic,assign) int ageCategory;
@end
複製代碼
使用:
LBPerson * p = [[LBPerson alloc] init];
p.ageCategory = 25; //分類添加屬性錯誤
NSLog(@"%d",p.ageCategory);
複製代碼
打印結果: 閃退.
-[LBPerson setAgeCategory:]: unrecognized selector sent to instance 0x6000021a72a0
固然, 咱們能夠經過 runtime 設置關聯對象 來解決這個問題 , 下面會仔細闡述.
Category
有什麼用 ?Category
中framework
的私有方法公開 ( 在子類中經過引用 , 聲明父類類別後 , 便可調用其未公開的私有方法)Tips:
請不要亂來:蘋果官方會拒絕使用系統私有API的應用上架,所以即便學會了如何調用私有方法,在遇到調用其它類的私有方法時,要謹慎處理,儘可能用其它方法替代。
Category
底層原理解析說了這麼多 , 終於要開始看看它究竟是個啥了. 打開終端/iterm2 , 編譯轉換成 c++.
clang -rewrite-objc LBPerson+Category.m
複製代碼
找到編譯後的文件, 打開, 搜索 _category_t
, 找到結構體定義.
struct _category_t {
const char *name; // 類名稱
struct _class_t *cls; // 類對象, 在編譯時沒有值
const struct _method_list_t *instance_methods; //實例方法列表
const struct _method_list_t *class_methods; // 類方法列表
const struct _protocol_list_t *protocols; // 分類實現的協議列表
const struct _prop_list_t *properties; // 分類屬性列表
};
複製代碼
繼續搜索 , 便可找到編譯時 , 這個結構體存放的內容
static struct _category_t _OBJC_$_CATEGORY_LBPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"LBPerson",
0, // &OBJC_CLASS_$_LBPerson,
0,
0,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LBPerson_$_Category,
};
複製代碼
搜索一下 _PROP_LIST_LBPerson_
會發現咱們定義的屬性. 固然, 歸納一下: 定義的方法, 屬性, 等都會在編譯時存放在對應的字段中 , 編譯後經過 section
段區分標識存放到生成的 Mach-O
可執行文件中.
同時 , 再往下看
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_LBPerson_$_Category,
};
複製代碼
也就是說當在編譯時 , 工程中全部的分類會被存儲到 __DATA
數據段中的 __objc_catlist
這個 section
段中.
以上就是編譯時分類所作的事情 , 分類結構體, 分類的方法 , 以及聲明的屬性 , 和存放 已經完成.
想要了解分類在運行時是如何加載和處理的. 咱們須要先知道一個概念.
什麼是 dyld
?
dyld
是蘋果的動態加載器 , 用來加載image
( 注意: 這裏的image
不是指圖片 , 而是Mach-O
格式的二進制文件 )當程序啓動時 , 系統內核會首先加載
dyld
, 而dyld
會將咱們的 APP 所依賴的各類庫加載到內存中 , 其中就包括libobjc
( OC 和 runtime ) , 這些工做是在 APP 的main
函數執行以前完成的.
_objc_init
是Object-C
-runtime
庫的入口函數 , 在這裏主要是讀取Mach-O
文件 OC 對應的Segment section
, 並根據其中的數據代碼信息 , 完成爲 OC 的內存佈局 , 以及初始化runtime
相關的數據結構.
那麼也就是說 , 咱們要去 _objc_init
中一探究竟, 看看分類究竟是怎麼加載 , 如何讀取 , 又是如何釋放的呢 ?
步驟 1️⃣: 直接打開 objc4
源碼. 找到 _objc_init
入口函數 , 也能夠經過添加符號斷點找進來.
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
複製代碼
前面一些初始化操做就很少說了 . 來看看這個 , 順便提一句.
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
複製代碼
map_images
: dyld
將 image
加載進內存時 , 會觸發該函數.load_images
: dyld
初始化 image
會觸發該方法. ( 咱們所熟知的 load
方法也是在此處調用 )unmap_image
: dyld
將 image
移除時 , 會觸發該函數 .那麼咱們就去研究研究 map_images
他加載時 , 咱們的分類到底處理了什麼 .
步驟 2️⃣ : 點擊進去 , 中間過渡方法我就省略了. 直接來到 _read_images
的方法實現.
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertLocked();
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
}
複製代碼
因爲方法過長 , 我就把一些讀取其餘的數據刪除掉了, 只保留了讀取分類部分 , 感興趣的同窗能夠去研究下其餘數據的讀取邏輯.
點擊進去 _getObjc2ClassList
GETSECT(_getObjc2ClassList, classref_t, "__objc_classlist");
複製代碼
這個就是咱們剛剛提到的 , 在編譯時 , 分類被加載到這個 section
段中 , 咱們看到讀取是這麼讀的 . 也順便驗證了咱們編譯時的流程 .
那麼也就是說 遍歷全部的分類 , 而後一一添加設置 . 接下來 , 注意到在遍歷中有一個方法
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
複製代碼
看名字大概也猜獲得, 往原類中添加 ( 注意是原類, 不是元類. 也就是原先的類 ).
步驟 3️⃣: 直接進去查看這個方法.
static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader) {
runtimeLock.assertLocked();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
複製代碼
這裏主要是 NXMapInsert(cats, cls, list);
這個方法, 其實簡單說一下也就是把當前分類 , 和原類 創建起一個綁定關係 ( 其實就是經過數據結構映射起來 ). 爲下面的事情作準備.
步驟 4️⃣ : 回到遍歷方法中 , 創建起了綁定關係以後 , 下面還有一個方法 , 這個方法是遍歷中最後的方法了 , 那麼它必然是要將分類中的方法添加到原類中的 . 固然這是咱們的猜測 , 咱們帶着這個目的去分析代碼 .
remethodizeClass(cls->ISA());
複製代碼
prepareMethodLists
中 , 把分類等要添加要元類的數據放進去.
步驟 5️⃣ : 重點 attachCategories
方法前半段咱們已經簡單概述了一下 , 那麼接下來來到
rw->methods.attachLists(mlists, mcount);
複製代碼
點擊進去 :
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
/** 拷貝的操做 void *memmove(void *__dst, const void *__src, size_t __len); */
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
複製代碼
以上方法就是分類添加方法的核心邏輯 . 簡單來講就是經過 memmove
和 memcpy
這兩個函數 , 以及本來類的方法列表和新分類的方法列表合併 , 添加到原方法的方法列表中.
並且要注意的是:
分類的方法是添加在方法列表數組前面的位置的 .
Mach-O
可執行文件 , 加載其中的 image
資源時 ( 也就是 map_images
方法中的 _read_images
) , 去讀取編譯時所存儲到 __objc_catlist
的 section
段中的數據結構 , 並存儲到 category_t
類型的一個臨時變量中.catlist
和原先類 cls
進行映射remethodizeClass
修改 method_list
結構 , 將分類的內容添加到本來類中.至於屬性和協議, 跟方法的流程是同樣的, 只是存放在不一樣的
section
段中. 這裏就很少贅述了 , 參考如下 :
最後有一個疑問: ❓
首先咱們知道 OC 查找方法流程中 , 當查找類方法的方法列表時 , 是採用了一個二分查找的方式的 . 那麼咱們類方法的擴展方法是添加到了原類的方法列表中前面位置的 . 那麼它如何保證外部調用方法時 , 是必定會調用到類方法中的呢 ?
答:
看以下圖:
當方法遍歷二分查找時 , 後面的方法查找到 , 一樣會往前查找一遍看看有沒有同名 ( 方法編號 ) 方法 , 若是有 , 則返回的是前面的方法 . 以此來保證了其優先級順序 , 也就是說 方法列表中前面的方法會有高優先級執行權限 .
從而也就保證了分類實現的目的.
Category
關聯屬性衆所周知 , Category
中聲明屬性 , 但並不會在 method_list
中生成對應的 setter
和 getter
方法以及對應的實例變量 , 編譯時會有警告.
那麼解決方法你們也都知道 , 就是手動設置關聯屬性 , 能夠理解成手動補上 setter
和 getter
方法 . 具體寫法以下:
#import "LBPerson+Category.h"
#import <objc/runtime.h>
static NSString * ageCategoryKey = @"ageCategoryKey";
@implementation LBPerson (Category)
- (NSString *)ageCategory {
return objc_getAssociatedObject(self, &ageCategoryKey);
}
- (void)setAgeCategory:(NSString *)age {
objc_setAssociatedObject(self, &ageCategoryKey, age, OBJC_ASSOCIATION_COPY);
}
複製代碼
看了上面分類結構體的源碼以後 , 其實咱們就很清楚了 . 由於咱們並無看到像類的結構體中的實例變量列表 , 也就是咱們所說的 ivar_list
, 所以也就不會有編譯器像類中自動幫咱們作 @synthesize
生成實例變量 ivar
和自動生成setter
和 getter
方法了
那麼咱們就來深刻探討一下 , 關聯屬性究竟是如何實現屬性以及究竟是如何存儲 , 又是如何銷燬的呢 ?
注意 : 一樣是剛剛的代碼
objc4
步驟 1. 直接點擊進入 objc_setAssociatedObject
方法. 過渡方法跳過.
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
//進行內存管理!!!
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
//初始化 HashMap
AssociationsHashMap &associations(manager.associations());
//當前對象的地址按位取反(key)
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
//<fisrt : disguised_object , second : ObjectAssociationMap>
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
//<fisrt : 標識(自定義的) , second : ObjcAssociation>
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
//ObjcAssociation
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
複製代碼
具體總結一下這個方法: ( 其實咱們也能大概猜出來這個方法要作什麼 , 無非是和自動生成的 setter
方法相似的操做 )
先根據內存管理語義作對應的引用計數等其餘的操做. (
acquireValue(value, policy)
方法 )建立了一個管理者
AssociationsManager
, 如何建立了一個AssociationsHashMap
, 給這個map
對象賦值 : ( 將當前對象的地址 按位取反做爲 key , 建立ObjectAssociationMap
對象做爲 value)給
ObjectAssociationMap
賦值 : ( 將用戶指定 , 傳進來的key
做爲 key , 建立ObjcAssociation
對象做爲 value)
ObjcAssociation
對象中存放了用戶指定的值 以及內存管理策略語義 . ( ObjcAssociation(policy, new_value) )給當前原類的
isa
添加標識 , 以便銷燬時識別是否須要釋放關聯對象.(object->setHasAssociatedObjects()
)
步驟 2. 搜索 delloc
, 點擊依次進入, 找到:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
複製代碼
那咱們看到析構函數中根據 isa
判斷了當有關聯對象或者其餘標識時 , 會調用 object_dispose
方法. 再點擊 , 依次進入
void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製代碼
依次釋放關聯屬性等等. 那麼也就是說, 咱們的關聯屬性的生命週期 是跟隨原對象走的.
至此 , 關聯對象的原理咱們已經解析完畢 , 總結一下:
Category
關聯屬性總結:
關聯屬性經過本身定義一個新的數據結構
ObjcAssociation
容器來保存用戶設置的內容 以及讀取用戶設置的內容 . 以此達到屬性那種經過方法訪問實例變量的效果.分類關聯屬性的生命週期同原先類 . 經過在
isa
中標識是否有關聯對象來在dealloc
中實現銷燬操做.
下篇會繼續
load
方法的探索. 歡迎關注交流.