Category的本質
Category的底層結構
1.我們先給Person增加一個Person+Eat的分類
@interface Person (Eat) <NSCopying, NSCoding>
- (void)eat;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
@end
@implementation Person (Eat)
- (void)eat
{
NSLog(@"eat");
}
- (void)eat1
{
NSLog(@"eat1");
}
+ (void)eat2
{
}
+ (void)eat3
{
}
@end
2.然后通過xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Eat.m轉換成Person+Eat.cpp檔案,發現內部會生成一個_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; // 屬性串列
};
3.我們還發現會生成一個_category_t結構體型別的變數,這個變數對應著該分類檔案是Person+Eat,并且里面記錄著所有的分類資訊
// 變數名對應著分類檔案名
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person", // 類名
0, // cls
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat, // 物件方法
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat, // 類方法
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat, // 協議串列
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat, // 屬性串列
};
4._OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat這個變數里面記錄著分類的物件方法eat和eat1
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Eat_eat},
{(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_Person_Eat_eat1}}
};
5._OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat這個變數里面記錄著分類的類方法eat2和eat3
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat2", "v16@0:8", (void *)_C_Person_Eat_eat2},
{(struct objc_selector *)"eat3", "v16@0:8", (void *)_C_Person_Eat_eat3}}
};
6. _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat這個變數里面記錄著NSCopying和NSCoding兩個協議
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};
7._OBJC_$_PROP_LIST_Person_$_Eat這個變數里面記錄著屬性weight和height
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"weight","Ti,N"},
{"height","Td,N"}}
// Ti,N和Td,N對應著int和double兩個型別
};
Category的加載處理程序
1.通過分析查找到objc-rumtime-new.mm檔案里的attachCategories函式,將分類檔案里的資料資訊都附加到對應的類物件或者元類物件里,詳細代碼如下
// 附加上分類的核心操作
// cls:類物件或者元類物件,cats_list:分類串列
static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
// 先分配固定記憶體空間來存放方法串列、屬性串列和協議串列
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
// 判斷是否為元類
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();
for (uint32_t i = 0; i < cats_count; i++) {
// 取出某個分類
auto& entry = cats_list[i];
// entry.cat就是category_t *cat
// 根據isMeta屬性取出每一個分類的類方法串列或者物件方法串列
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
// 如果有方法則添加mlist陣列到mlists這個大的方法陣列中
// mlists是一個二維陣列:[[method_t, method_t, ....], [method_t, method_t, ....]]
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
// 將分類串列里先取出來的分類方法串列放到大陣列mlists的最后面(ATTACH_BUFSIZ - ++mcount),所以最后編譯的分類方法串列會放在整個方法串列大陣列的最前面
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 同上面一樣取出的是分類中的屬性串列proplist加到大陣列proplists中
// proplists是一個二維陣列:[[property_t, property_t, ....], [property_t, property_t, ....]]
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
// 同上面一樣取出的是分類中的協議串列protolist加到大陣列protolists中
// protolists是一個二維陣列:[[protocol_ref_t, protocol_ref_t, ....], [protocol_ref_t, protocol_ref_t, ....]]
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
// 將分類的所有物件方法或者類方法,都附加到類物件或者元類物件的方法串列中
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
return !c->cache.isConstantOptimizedCache();
});
}
}
// 將分類的所有屬性附加到類物件的屬性串列中
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
// 將分類的所有協議附加到類物件的協議串列中
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
2.上述的每步操作都會呼叫attachLists方法來進行元素分配,詳細代碼如下
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// 獲取原本的個數
uint32_t oldCount = array()->count;
// 最新的個數 = 原本的個數 + 新添加的個數
uint32_t newCount = oldCount + addedCount;
// 重新分配記憶體
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
// 將新陣列的個數改為最新的個數
newArray->count = newCount;
// 將舊陣列的個數改為最新的個數
array()->count = newCount;
// 遞減遍歷,將舊陣列里的元素從后往前的依次放到新陣列里
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
// 將新增加的元素從前往后的依次放到新陣列里
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
// 釋放舊陣列資料
free(array());
// 賦值新陣列資料
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else { .... }
}
總結
- 編譯時
- 每一個
Category都會生成一個_category_t結構體物件,記錄著所有的屬性、方法和協議資訊
- 每一個
- 運行時
- 通過
Runtime加載某個類的所有Category資料 - 把所有
Category的物件方法、類方法、屬性、協議資料,分別合并到一個二維陣列中,并且后面參與編譯的Category資料,會在陣列的前面 - 將合并后的
Category資料(方法、屬性、協議),插入到類原來資料的前面
- 通過
面試題
1.如果幾個分類中都有同樣的方法,會呼叫哪個,呼叫順序是什么
- 有分類會先呼叫分類的方法,如果多個分類都有相同的方法,那么會根據編譯順序來決定執行哪個分類的方法,后參與編譯的分類方法會放到整個方法串列陣列的最前面,到時呼叫會遍歷所有的方法串列陣列,先找到的分類方法串列先執行,
- 在Xcode中查看編譯順序:
Build Phases->Compile Sources - 分類里面相同的方法會覆寫類原本的方法這種說法是錯誤的,根本就沒有覆寫,只是最先遍歷找到哪個就執行哪個,不存在覆寫的概念
2. Class Extension和Category的實作是一樣的嗎
- 不一樣,
- 類擴展只是將.h檔案中的宣告放到.m中作為私有來使用,編譯時就已經合并到該類中了,
- 分類中的宣告都是公開的,而且是利用運行時機制在程式運行時將分類里的資料合并到類中
3.Category中有load方法嗎?load方法是什么時候呼叫的?load 方法能繼承嗎?
- 有
load方法 load方法在Runtime加載類、分類的時候呼叫,而且只呼叫一次load方法可以繼承,但是一般情況下不會主動去呼叫load方法,都是讓系統自動呼叫
4.load、initialize方法的區別什么?它們在category中的呼叫的順序?以及出現繼承時他們之間的呼叫程序?
load方法
load方法會在Runtime加載類、分類時呼叫- 每個類、分類的load,在程式運行程序中只呼叫一次
原始碼分析
1.在objc-runtime-new.mm中load_images方法可以發現準備處理load方法和呼叫load方法的函式
void load_images(const char *path __unused, const struct mach_header *mh) {
....
{
mutex_locker_t lock2(runtimeLock);
// 準備load方法
prepare_load_methods((const headerType *)mh);
}
// 呼叫load方法
call_load_methods();
}
2.在prepare_load_methods中發現,呼叫load方法前的類的處理和分類的處理
void prepare_load_methods(const headerType *mhdr) {
size_t count, i;
runtimeLock.assertLocked();
// 按編譯順序拿到所有的類的list
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
// 按添加進陣列的順序遍歷處理類的串列
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
// 按編譯順序拿到所有的分類串列
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
....
// 按順序直接添加
add_category_to_loadable_list(cat);
}
}
3.遞回呼叫schedule_class_load方法來優先添加父類放到串列中,然后再添加當前類,所以執行呼叫時肯定先執行父類的load方法
static void schedule_class_load(Class cls) {
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// 遞回呼叫,找到傳進來的類的父類添加到串列中
schedule_class_load(cls->getSuperclass());
// 然后再呼叫當前傳進來的類添加到串列中,所以父類肯定是在前面
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
4.在objc-loadmethod.mm中call_load_methods方法可以發現,程式運行時會先呼叫類的load方法,然后呼叫分類的load方法,詳細代碼如下
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *po ol = objc_autoreleasePoolPush();
do {
// 1. 優先呼叫類的load方法
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. 只呼叫一次分類的load方法
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
5.找到每個類的load方法的記憶體地址,然后直接呼叫
static void call_class_loads(void) {
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 取出類里面的load方法
// load_method_t:指向函式地址的指標
// 這里的method對應的結構體為loadable_class
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 直接呼叫load方法
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
// 找到的method對應的結構體
struct loadable_class {
Class cls; // may be nil
IMP method; // 這個就是load方法
};
6.找到每個分類的load方法的記憶體地址,然后直接呼叫
static bool call_category_loads(void) {
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
// 取出每一個分類里的load方法
// 這里的method對應的結構體為loadable_category
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
// 直接呼叫load方法
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
....
}
// 找到的method對應的結構體
struct loadable_category {
Category cat; // may be nil
IMP method; // 這個方法就是load方法
};
呼叫順序
- 先呼叫類的
load- 按照編譯先后順序呼叫(先編譯,先呼叫)
- 呼叫子類的
load之前會先呼叫父類的load
- 再呼叫分類的
load- 按照編譯先后順序呼叫(先編譯,先呼叫)
load方法系統呼叫和主動呼叫的區別
- 系統呼叫
load方法呼叫是直接找到類和分類中的方法的記憶體地址直接呼叫 - 主動呼叫
load方法是通過訊息機制來發送訊息的,會在對應的訊息串列里按順序遍歷一層層查找,找到就呼叫
initialize方法
initialize方法會在類第一次接收到訊息時呼叫
原始碼分析
1.由于在呼叫到這個類的時候才會執行initialize方法,那么說明是在發訊息程序中來執行的,我們在objc-runtime-new.mm中呼叫class_getInstanceMethod或者class_getClassMethod方法,詳細代碼如下
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
return _class_getMethod(cls, sel);
}
2.一步步呼叫最后找到initializeNonMetaClass函式,先遞回找到父類呼叫initialize方法,然后當前類呼叫initialize方法,可以在callInitialize里發現本質都是通過Runtime的訊息機制進行的發送
void initializeNonMetaClass(Class cls) {
ASSERT(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// 如果存在父類,并且沒有初始化父類,就去初始化父類
supercls = cls->getSuperclass();
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
// 未初始化的類通過setInitializing做了標記,下次就不會再呼叫了
cls->setInitializing();
reallyInitialize = YES;
// Grab a copy of the will-initialize funcs with the lock held.
localWillInitializeFuncs.initFrom(willInitializeFuncs);
}
}
....
{
// 呼叫初始化
callInitialize(cls);
....
}
....
}
// 呼叫initialize的函式
void callInitialize(Class cls) {
// 通過runtime訊息機制發送initialize訊息
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
呼叫順序
- 先呼叫父類的
initialize,再呼叫子類的initialize - (先初始化父類,再初始化子類,每個類只會初始化1次)
initialize和load的區別
initialize是通過objc_msgSend進行呼叫的,而load是找到函式地址直接呼叫的- 如果子類沒有實作
initialize,會呼叫父類的initialize- 所以父類的
initialize可能會被呼叫多次,第一次是系統通過訊息發送機制呼叫的父類initialize,后面多次的呼叫都是因為子類沒有實作initialize,而通過superclass找到父類再次呼叫的
- 所以父類的
- 如果分類實作了
initialize,就覆寫類本身的initialize呼叫
5.Category能否添加成員變數?如果可以,如何給Category添加成員變數?
不能直接給Category添加成員變數,但是可以間接實作Category有成員變數的效果
實作方案
通過objc_setAssociatedObject設定關聯物件來實作
@interface Person (Test)
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) int weight;
@end
@implementation Person (Test)
- (void)setName:(NSString *)name
{
/*
* object:關聯的物件
* value:關聯的值
* objc_AssociationPolicy:關聯策略
*/
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
// 隱式引數
// _cmd == @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
- (void)setWeight:(int)weight
{
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight
{
// _cmd == @selector(weight)
return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
實作原始碼分析
1.在objc-references.mm中我們可以看到objc_setAssociatedObject的實作代碼如下
void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) {
if (!object && !value) return;
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// 通過傳進來的object生成一個key
DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
// 取出AssociationsManager里的AssociationsHashMap這個屬性
AssociationsHashMap &associations(manager.get());
// 如果value有值
if (value) {
// 根據傳進來的object key的值disguised取出ObjectAssociationMap
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
isFirstAssociation = true;
}
// 根據refs_result的key存放進association
// association就是ObjcAssociation
// 總之就是對應的每一個key一層層的賦值
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else { // 如果value為空
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
// 也會找到associations進行擦除
associations.erase(refs_it);
}
}
}
}
}
....
}
2.關聯物件的取值函式objc_getAssociatedObject的實作代碼如下
void _object_get_associative_reference(id object, const void *key) {
ObjcAssociation association{};
{
AssociationsManager manager;
// associations是manager里面所有的AssociationsHashMap的list
AssociationsHashMap &associations(manager.get());
// 根據object在list里找到對應的那個AssociationsHashMap型別的i
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
// 再取出所有的ObjectAssociationMap的list
ObjectAssociationMap &refs = i->second;
// 根據key在list里找到對應的ObjectAssociationMap型別的j
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
// 取出ObjectAssociation型別的值association
association = j->second;
// 取出association里的value和策略
association.retainReturnedValue();
}
}
}
return association.autoreleaseReturnedValue();
}
3.關聯物件的幾個類的實作關系可以用下圖表示

總結:
- 關聯物件并不是存盤在被關聯物件本身記憶體中
- 關聯物件存盤在全域的統一的一個
AssociationsManager中 - 設定關聯物件為
nil,就相當于是移除關聯物件 - 當
object物件被釋放,關聯物件的值也會對應的從記憶體中移除(記憶體管理自動做了處理)
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/273193.html
標籤:iOS
下一篇:iOS底層原理(四)block
