Category底層原理
Category可以把一個類的功能拆解成很多模塊
創(chuàng)建一個類,并創(chuàng)建兩個分類

分類編譯時底層編譯成的代碼:
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; // 協(xié)議列表
const struct _prop_list_t *properties; // 屬性列表
};
每一個分類對應(yīng)一個結(jié)構(gòu)體對象
如:
#import "MJPerson+Test.h"
@implementation MJPerson (Test)
- (void)run
{
NSLog(@"MJPerson (Test) - run");
}
- (void)test
{
NSLog(@"test");
}
+ (void)test2
{
}
編譯成C++代碼
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MJPerson",
0, // &OBJC_CLASS_$_MJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Test,
};
// 對象方法
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_MJPerson_Test_test}}
};
// 類方法
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_MJPerson_Test_test2}}
};
// 屬性列表
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_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"weight","Ti,N"},
{"height","Td,N"}}
};
分類里的屬性、方法、協(xié)議等最后也是通過runtime動態(tài)合并到類對象,元類對象中
具體步驟:

源碼下載地址 https://opensource.apple.com/tarballs/objc4
如方法的合并:
runtime 會將Person的所有分類的方法列表先合并到一個列表里面,然后再通過內(nèi)存移動插入到原來Person方法列表的前面

至于Test1 和 Test2 誰在前 誰在后,根據(jù)編譯循序來的因為 合并分類列表的代碼為
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count; // 分類結(jié)構(gòu)數(shù)組
bool fromBundle = NO;
while (i--) { // 使先編譯的后調(diào)用
auto& entry = cats->list[I];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;// 合并
fromBundle |= entry.hi->isBundle();
}
通過源碼可知,后編譯的在前面,至于編譯循序可以在下面調(diào)整

所以如果在Person 和 兩個分類中都有一個相同的方法 如 run,以上圖的編譯循序 是執(zhí)行 Test1中的方法。這個并不是方法覆蓋,而是,方法查找的時候會先查到Test1中的方法
和類擴展的區(qū)別 Extension(OC)
類擴展是在編譯的時候就將方法 屬性等加入到原先的方法、屬性列表中了
分類添加屬性相關(guān)
1、當(dāng)我們給一個類添加屬性的時候如
@property (nonatomic, assign) int age;
// 給一個類添加age 屬性
會默認(rèn)實現(xiàn)下面3個步驟
// 1、聲明一個成員變量
{
int _age;
}
//2、 聲明set 和 get 方法
- (void)setAge:(int)age;
- (int)age;
//3、實現(xiàn)get 和set 方法
- (void)setAge:(int)age {
_age = age;
}
- (int)age {
return _age;
}
2、當(dāng)我們給分類添加屬性時,默認(rèn)只有方法的聲明
@property (assign, nonatomic) int weight;
//默認(rèn)聲明
- (void)setWeight:(int)weight;
- (int)weight;
但是沒有實現(xiàn),所以可以調(diào)用,但是會報錯找不到方法
Person *person = [Person new];
person.weight = 10;
NSLog(@"%d",person.weight);
//-[Person weight]: unrecognized selector sent to instance 0x10067f080'
我們?nèi)绻謩蛹由铣蓡T變量 實現(xiàn)set 和get 方法
編譯時就會報錯,分類中不能添加成員變量

從上面的分類編譯成的底層代碼也可以發(fā)現(xiàn),根本沒有成員變量列表,
下圖是一個普通類的底層結(jié)構(gòu)。有個成員變量列表

3、給分類添加關(guān)聯(lián)對象實現(xiàn)類似成員變量的功能
實現(xiàn)屬性的set 和 get 方法
// 地址值
static const void *PersonNameKey = &PersonNameKey;
/**
加上static 防止外面訪問,否則別的地方
通過 extern const void *PersonNameKey; 可以訪問到
*/
- (void)setName:(NSString *)name {
//objc_AssociationPolicy 關(guān)聯(lián)策略 類似于用什么修飾 copy assign strong 等
// objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
objc_setAssociatedObject(self, PersonNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, PersonNameKey);
}
其中const void * _Nonnull key 是一個地址值可以有很多種辦法生成
//static const char LQNameKey;
- (void)setName:(NSString *)name {
//1、 使用get方法的@selector
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
// 2、使用屬性名稱
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
// 3、使用一個字符
objc_setAssociatedObject(self, &LQNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
objc_AssociationPolicy 對應(yīng)的修飾符

關(guān)聯(lián)對象的原理
void objc_setAssociatedObject(id object, const void * key,
id value,
objc_AssociationPolicy policy)
實現(xiàn)關(guān)聯(lián)對象技術(shù)的核心對象有
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
runtime 管理著一個全局的AssociationsManager,內(nèi)部管理一個map(AssociationsHashMap),這個map的key是根據(jù)object生成的,value對應(yīng)的是另一個map(ObjectAssociationMap),objectMap的key就是上面方法傳進(jìn)來的key,value對應(yīng)ObjcAssociation對象,ObjcAssociation內(nèi)部包含policy和真正的value

+load方法
一、+load 方法會在runtime加載類和分類時調(diào)用
二調(diào)用順序
1.先調(diào)用類的+load
按照編譯順序調(diào)用(先編譯,先調(diào)用)
調(diào)用子類的+load方法之前,如果父類的+load沒調(diào)用過就先調(diào)用父類的+load方法
2、所有類的+load方法調(diào)用完,再調(diào)用分類的+load
按照編譯順序調(diào)用(先編譯,先調(diào)用)(沒有父類,子類之分)
3、底層代碼
先調(diào)用父類的+load 方法的原因 準(zhǔn)備調(diào)用
prepare_load_methods會調(diào)用下面的代碼
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);//遞歸調(diào)用
add_class_to_loadable_list(cls);// 加入類load列表中
cls->setInfo(RW_LOADED);
}
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads(); // 調(diào)用類的
}
// 2. Call category +loads ONCE
more_categories = call_category_loads(); // 調(diào)用分類的
// 3. Run more +loads if there are classes OR more untried categories
}
call_class_loads 簡化為
static void call_class_loads(void)
{
struct loadable_class *classes = loadable_classes;
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
(*load_method)(cls, SEL_load);
}
}
call_category_loads 調(diào)用分類的+load
static bool call_category_loads(void) {
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
(*load_method)(cls, SEL_load); // 直接通過函數(shù)指針調(diào)用
}
load_method_t
struct loadable_class {
Class cls; // may be nil
IMP method; // load方法
};
struct loadable_category {
Category cat; // may be nil
IMP method; //load方法
};
所以load方法是直接找到,然后通過函數(shù)指針調(diào)用的,不像上面的run方法 通過objc_msgSend調(diào)用,通過isa指針找方法列表
+initialize方法
調(diào)用順序
+initialize方法會在類第一次接收到消息時調(diào)用
1、先調(diào)用父類的initialize, 再調(diào)用子類的+initialize
2、(先初始化父類,再初始化子類,每個類只會初始化1次)
源碼解讀過程
objc4源碼解讀過程
objc-msg-arm64.s
objc_msgSend
objc-runtime-new.mm
class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)
源碼
每次調(diào)用objc_msgSend 會調(diào)用
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
if (initialize && !cls->isInitialized()) { // 需要初始化,沒有初始化
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
}
void _class_initialize(Class cls) // 初始化
{
assert(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) { // 如果父類沒有初始化,先遞歸初始化父類
_class_initialize(supercls);
}
callInitialize(cls);
}
// 最終初始化 也是通過objc_msgSend
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
注意
+initialize和+load的很大區(qū)別是,+initialize是通過objc_msgSend進(jìn)行調(diào)用的,所以有以下特點
如果子類沒有實現(xiàn)+initialize,會調(diào)用父類的+initialize(所以父類的+initialize可能會被調(diào)用多次)
如果分類實現(xiàn)了+initialize,就覆蓋類本身的+initialize調(diào)用
+load 和 initialize 總結(jié)
load、initialize方法的區(qū)別什么?
1.調(diào)用方式
1> load是根據(jù)函數(shù)地址直接調(diào)用
2> initialize是通過objc_msgSend調(diào)用
2.調(diào)用時刻
1> load是runtime加載類、分類的時候調(diào)用(只會調(diào)用1次)
2> initialize是類第一次接收到消息的時候調(diào)用,每一個類只會initialize一次(父類的initialize方法可能會被調(diào)用多次)
load、initialize的調(diào)用順序?
1.load
1> 先調(diào)用類的load
a) 先編譯的類,優(yōu)先調(diào)用load
b) 調(diào)用子類的load之前,會先調(diào)用父類的load
2> 再調(diào)用分類的load
a) 先編譯的分類,優(yōu)先調(diào)用load
2.initialize
1> 先初始化父類
2> 再初始化子類(可能最終調(diào)用的是父類的initialize方法)