01-03 category 原理概述
1、Category
1.1、原理
#import "FQPeople+Test.h"
@implementation FQPeople (Test)
+ (void)test{
}
- (void)test1{
}
@end
-
分類的物件方法test1存放在類物件FQPeople中
-
分類的類方法test存在FQPeople元類中
使用:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc FQPeople+Test.m
在編譯時生成c++程式碼
每一個分類都會產生一個結構體
XFPeople XFPeople+Category1 XFPeople+Category2
產生兩個結構體
struct _category_t {
const char *name;//類名FQPeople
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;
};
在方法編譯時 分類的方法,資料等都存在編譯之後的結構體中即_category_t中
在執行時通過runtime動態的將分類的方法合併到類物件、元類物件中 在程式啟動的時候就合併
而不是在使用該分類 或者編譯時呼叫的時候才合併
通過這個流程得知 依次插入
XFPeople XFPeople+Category1 XFPeople+Category2
著三個中都有-(void)test{}方法,會先從分類中找方法,如果分類中沒有再從物件中找方法 (陣列遍歷從索引0開始的)
原始碼:
至於是先執行 XFPeople+Category1 或者是 XFPeople+Category2中的方法,則存在不確定性,這個要看哪個先編譯,最後編譯的放在最前面
總結整體流程:
1、通過runtime載入某一個類的所有分類資料
2、把所有的category的方法屬性協議合併到一個數組中(後編譯的分類資料會放在陣列的前面)
3、將合併後的分類資料(方法,屬性,協議)插入到類原來資料的前面
最後貼上一下執行時原始碼
最後貼上一下執行時原始碼
/*
類物件,分類列表
cls XQPeople
cats = [category_t(Test),category_t(Eat)]
*/
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
///方法陣列
/**
[
[method-t,method-t],[method-t,method-t]
]
*/
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
///屬性陣列
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
///協議陣列
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
///取出某一個分類
auto& entry = cats->list[i];
///取出分類中的物件方法(或者元類方法) 最終把所有的分類方法列表放入mlists大陣列中
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
///獲取類物件中的資料
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
///將所有分類的方法放到類物件方法中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
///將所有分類的屬性方法放到類物件方法中
rw->properties.attachLists(proplists, propcount);
free(proplists);
///將所有分類的屬協議方法放到類物件方法中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
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;
//array()->lists 原來的方法列表
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
///addedLists 所有的分類方法列表
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
...
1.2、擴充套件(類擴充套件)
@interface FQPeople()
@end
@implementation FQPeople
@end
類擴充套件是在編譯的時候就把擴充套件的資訊合併到類裡面去,而分類是在執行時合併的
2、Category實現原理(m)
-
category 編譯之後的底層時顯示一個 struct 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 和class extension 的區別(m)
-
class extension是在編譯的時候就把擴充套件的資訊合併到類裡面去,
-
Category是在執行時將資料合併到類資訊中
4、load initialize
4.1、load的原理
- load方法在在runtime載入類、分類的時候呼叫
相當於執行時程式一旦啟動的時候進行呼叫,載入類的時候呼叫類的load 載入分類的時候呼叫分類的load,
- 每個類、分類的+load,在程式執行過程中只調用一次;
4.1.1、不存在繼承關係的load
證明
@implementation People (Test1)
+ (void)load{
NSLog(@"%s",__func__);
}
+ (void)test{
NSLog(@"%s",__func__);
}
@end
@implementation People (Test2)
+ (void)load{
NSLog(@"%s",__func__);
}
+ (void)test{
NSLog(@"%s",__func__);
}
@end
@implementation People
+ (void)load{
NSLog(@"%s",__func__);
}
+ (void)test{
NSLog(@"%s",__func__);
}
直接執行
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
+[People load]
+[People(Test2) load]
+[People(Test1) load]
可以看出 只要有load方法在程式啟動的時候就會呼叫
void printMethodNamesOfCalss(Class cls){
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodsNames = [NSMutableString string];
for (int i = 0; i<count; i++) {
Method method = methodList[i];
NSString *methodsName = NSStringFromSelector(method_getName(method));
[methodsNames appendString:methodsName];
[methodsNames appendString:@", "];
}
free(methodList);
NSLog(@"%@ %@",cls,methodsNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
printMethodNamesOfCalss(object_getClass([People class]));
[People test];
}
return 0;
}
程式啟動載入時(runtime)呼叫load
+[People load]
+[People(Test2) load]
+[People(Test1) load]
///打印出物件和分類物件的方法列表 可以看得有多個load 多個test
People load, test, load, test, load, test,
///[People test]; 只調用了Test2的test方法
+[People(Test2) test]
呼叫順序
- 類的load方法
疑問:同樣是類方法,且都在類方法列表中存在 為什麼load呼叫了三次 而test只調用了分類中的呢?
void call_load_methods(void){
do {
//先呼叫類的load方法
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE 再調動分類的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);
static void call_class_loads(void)
{
int i;
// Detach current loadable list. 所有有load的類的列表
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.遍歷load類列表
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 取出該load的load類方法指標
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, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
typedef void(*load_method_t)(id, SEL);
struct loadable_class {
Class cls; // may be nil
IMP method;//這個裡面存放的就是類load方法
};
struct loadable_category {
Category cat; // may be nil
IMP method;//這個裡面存放的就是分類load方法
};
答:為什麼load會執行三次
因為在執行時時期 會遍歷所有帶有load方法的類,直接拿到load方法的地址 直接呼叫
而+(void)test{}呢?
這個方法是通過傳送訊息
objc_msgSend(objc_getClass("People"), sel_registerName("test"));
4.1.2、存在繼承關係的load
-
先呼叫類load方法 1000個類的話,那先呼叫那個類呢?
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 遞迴呼叫 先呼叫s父類 schedule_class_load(cls->superclass); ///把父類加入到包含load類的列表中 列表中儲存類物件 add_class_to_loadable_list(cls); cls->setInfo(RW_LOADED); }
首先會先呼叫父類的load方法 在呼叫子類的load方法
如果不存在繼承關係呢?
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertWriting();
///按載入順序加入陣列
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
///載入子類之前先載入父類
schedule_class_load(remapClass(classlist[i]));
}
///按載入順序加入陣列
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
///直接把分類加入陣列最後面
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_
}
總結:
1、先呼叫類的load方法
- 按照編譯順序呼叫 (先編譯,先呼叫)
- 呼叫子類的+load方法之前,先呼叫父類的+load方法
2、再呼叫分類的load方法
- 按照編譯順序呼叫 (先編譯,先呼叫)
4.2、initialize
[ɪˈnɪʃəlaɪz]
-
+initialize方法會在類第一次接收到訊息的時候呼叫 且每個類只會初始化一次
比如:[Student alloc]; 訊息機制objc_msgSend方法 呼叫的時分類的方法
[Student alloc];
+[People(Test1) initialize]
+[Student(Test2) initialize]
- 會先呼叫父類的initilize(分類)在呼叫子類的initialize(先分類)的方法
如果父類initialize 沒有呼叫的話會呼叫,如果已經呼叫了那就不能在子類初始化的時候再呼叫
向子類傳送訊息 會先向父類傳送訊息
objc_msgSend(object_getClass([People alloc]), @selector(initialize))
objc_msgSend(object_getClass([Student alloc]), @selector(initialize))
證明:
Method class_getInstanceMethod(Class cls, SEL sel)
{
lookUpImpOrNil(cls, sel, nil,
NO/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
///這個方法是否需要初始化 &&如果沒有初始化
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;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
/// 如果存在父類,並且沒有初始化 m那就初始化父類
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
#if __OBJC2__
@try
#endif
{
///真真的初始化
callInitialize(cls);
}
}
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
5、initialize和load的區別
+initialize 是通過objc_msgSend 方法進行呼叫的 +load在程式載入時直接找到函式指標進行呼叫的
所以+initialize有以下特點
- 如果子類沒有實現+initialize方法會呼叫父類的initialize(所以父類的initialize會被呼叫多次)
- 如果分類實現了+initialize方法會覆蓋類本身的initialize呼叫
@implementation People
+ (void)initialize{
NSLog(@"%s",__func__);
}
@end
@implementation Student //(繼承People)
+ (void)initialize{
NSLog(@"%s",__func__);
}
@end
@implementation Animal //(繼承People)
+ (void)initialize{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Student alloc];
[Animal alloc];
}
return 0;
}
+[People initialize]
+[Student initialize]
+[Animal initialize]
為什麼會是這個結果: [Student alloc]; 是會先去判斷父類是否已初始化
沒有所以呼叫父類的
objc_msgSend(object_getClass([People alloc]), **@selector**(initialize));
在執行自己的
objc_msgSend(object_getClass([Student alloc]), **@selector**(initialize));
而[Animal alloc]; 時父類People已經初始化了所以只執行了自己的
objc_msgSend(object_getClass([Animal alloc]), **@selector**(initialize));
@implementation People
+ (void)initialize{
NSLog(@"%s",__func__);
}
@end
@implementation Student //(繼承People)
@end
@implementation Animal //(繼承People)
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Student alloc];
[Animal alloc];
}
return 0;
}
+[People initialize]
+[People initialize]
+[People initialize]
為什麼子類沒有initialize會執行多次呢?
[Student alloc];
if (Student是否需要初始化&&Student沒有初始化) {
if (People是否需要初始化&&People有沒有沒有初始化) {
objc_msgSend(object_getClass([People alloc]), @selector(initialize))
}
objc_msgSend(object_getClass([Student alloc]), @selector(initialize))
}
[Animal alloc];
if (Animal是否需要初始化&&Animal沒有初始化) {
if (People是否需要初始化&&People有沒有沒有初始化) {
objc_msgSend(object_getClass([People alloc]), @selector(initialize))
}
objc_msgSend(object_getClass([Animal alloc]), @selector(initialize))
}
也就是
objc_msgSend(object_getClass([People alloc]), @selector(initialize))
objc_msgSend(object_getClass([Student alloc]), @selector(initialize))
根本找不到Student 的initialize方法 去父類中找所以People的initialize被呼叫
objc_msgSend(object_getClass([Animal alloc]), @selector(initialize))
根本找不到Animal 的initialize方法 去父類中找所以People的initialize被呼叫
注意點 雖然[People initialize] 被執行了三次但並不代表父類被初始化了三次
他是第一次對父類,其他兩次是對子類進行的初始化
6、Category中有+load方法嗎?load方法是什麼時候呼叫的?load方法能繼承嗎?(m)
- 有
- 在runtime載入類、分類的時候呼叫這個方法
- 可以繼承 如果子類沒有load放會呼叫父類的load 傳送訊息的流程
[Student load]; Student中沒有 呼叫父類的 父類有分類呼叫分類的
嚴格來講不會主動呼叫load方法
7、initialize和load的區別(m)
-
呼叫方式的區別
- load是直接通過函式指標的方式去呼叫 initialize是使用的訊息機制objc_msgSend
-
呼叫時機不同
- load是在runtime載入類、分類的時候呼叫且之後呼叫一次
- initialize 是在類第一次接收訊息的時候呼叫,而且某一個類只會initialize一次,但是父類的initialize可能會被呼叫多次
8、initialize和load的呼叫順序(m)
-
load先呼叫類的load方法 且先編譯的類的先呼叫 且呼叫子類的load之前會先呼叫父類的load方法
-
在呼叫分類的load方法 且先編譯的分類的先呼叫
-
initialize 先初始化父類 在初始化子類 如果子類沒有initialize會呼叫父類的