1. 程式人生 > 實用技巧 >01-03 category 原理概述

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會呼叫父類的