筆記 - Category

1、Category的基本使用

Category的使用場合是什麼?

- 將一個類拆成很多模塊(其實就是解耦,將相關的功能放到一起)

2、Category的實現原理?

- 通過runtime動態將分類的方法合併到類對象、元類對象中
- Category編譯之後的底層結構是 struct_category_t , 裏面存儲着分類的對象方法、類方法、屬性、協議信息
- 在程序運行的時候,runtime會將 Category 的數據,合併到類信息中(類對象、元類對象)
- 轉C++代碼
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Test.m

- 分類結構體
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;         // 屬性列表 (分類裏能實現屬性)
};


- Test分類
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,
    0,
    0,
    0,
};


- Eat分類
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MJPerson",
    0, // &OBJC_CLASS_$_MJPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
    0,
    0,
    0,
};

3、源碼分析1

objc4-723.tar.gz

源碼定義

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

Category的加載處理過程

- 通過Runtime加載某個類的所有Category數據
- 把所有Category的方法、屬性、協議數據,合併到一個大數組中,後面參與編譯的Category數據,會在數組的前面
- 將合併後的分類數據(方法、屬性、協議),插入到類原來數據的前面
源碼解讀順序

objc-os.mm
- _objc_init
- map_images
- map_images_nolock

objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attachLists
- realloc、memmove、memcpy
cls = [MJPerson class]
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();

    /*
     方法數組
     [
       [method_t, method_t],
       [method_t, method_t]
     ]
     */
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    
    /*
     屬性數組
     [
       [property_t, property_t],
       [property_t, property_t]
     ]
     */
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    
    /*
     協議數組
     [
       [protocol_t, protocol_t],
       [protocol_t, protocol_t]
     ]
     */
    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];

        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);
}

5、memmove、memcopy的區別

void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count); 

- 他們的作用是一樣的
- 唯一的區別是,當內存發生局部重疊的時候,memmove保證拷貝的結果是正確的,memcpy不保證拷貝的結果的正確

參考鏈接


6、load基本使用

Category中有load方法麼?load方法什麼時候調用的?load方法能繼承麼?

- 有load方法
- +load方法會在runtime加載類、分類時調用;
- 每個類、分類的+load,在程序運行過程中只調用一次

- 調用順序
1、先調用類的+load,(按照編譯先後順序,先編譯,先調用),調用子類的+load之前會調用父類的+load
2、再調用分類的+load按照編譯先後順序調用(先編譯,先調用)

7、load調用原理

-load方法調用

test方法和load方法的本質區別?(+load方法爲什麼不會被覆蓋)
- test方法是通過消息機制調用 objc_msgSend([MJPerson class], @selector(test))
- + load方法調用,直接找到內存中的地址,進行方法調用

8、load調用順序

- +load方法會在runtime加載類、分類時調用
- 每個類、分類的+load,在程序運行過程中只調用一次

調用順序
- 1、先調用類的+load
     按照編譯先後順序調用(先編譯,先調用)
     調用子類的+load之前會先調用父類的+load

- 2、再調用分類的+load
     按照編譯的順序調用(先編譯,先調用)
objc4源碼解讀過程:objc-os.mm

- _objc_init

- load_images

- prepare_load_methods
  schedule_class_methods
  add_class_to_loadable_list
  add_category_to_loadable_list

- call_load_methods
  call_class_loads
  call_category_loads
  (*load_method)(cls, SEL_load)

+ load方法時根據方法地址直接調用,並不是經過objc_msgSend函數調用

  • 先調用類方法,後調用分類方法
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 *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        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;
}

如果有100個類,會先調用哪個類的方法(按照數組的先後順序)

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_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

9、initialize的基本使用

initialize 方法(分類會覆蓋類方法,通過消息機制(objc_msgSend)調用)

- initialize方法會在類第一次接收到消息時調用
- 調用順序:
  先調用父類的+initialize,再調用子類的+initialize(如果父類已經調用過了,調用子類的時候,父類就不回再調用了)

爲什麼load三個方法都會調用?
- 直接通過函數指針調用,不是通過消息機制(objc_msgSend)調用

10、initialize 注意點

initialize和load的很大區別是,
- +initialize是通過objc_msgSend進行調用的
- +load方法是直接找到內存地址進行調用的

所以initialize有以下特點:
- 如果子類沒有實現+initialize,會調用父類的+initialize(所以父類的+initialize可能會被調用多次)
- 如果分類實現了+initialize,就覆蓋類本身的+initialize調用

// 僞代碼(爲什麼會打印三次的原因,不代表父類初始化了3次)
if (MJStudent沒有初始化) {
    if (MJPerson沒有初始化) {
        objc_msgSend([MJPerson class], @selector(initialize))
    }
    objc_msgSend([MJStudent class], @selector(initialize))
}

if (MJTeacher沒有初始化) {
    if (MJPerson沒有初始化) {
        objc_msgSend([MJPerson class], @selector(initialize))
    }
    objc_msgSend([MJTeacher class], @selector(initialize))
}

11、面試題

1、load、initialize方法的區別是什麼?它們在category中的調用順序?以及出現繼承時他們之間的調用過程?


區別:
- 調用方式
1、load是根據函數地址直接調用
2、initialize是榮光objc_msgSend調用

- 調用時刻
1、load是runtime加載類、分類的時候調用(只會調用1次)
2、initialize是類第一次接收消息時調用,每一個類只會initialize一次(父類的initialize方法可能會被調用多次)


load、initialize的調用順序
- load
1、先調用類的load
   先編譯的類,優先調用load
   調用子類的load之前,會先調用父類的load
2、再調用分類的load
   先編譯的分類,優先調用load

- initialize
  先初始化父類
  再初始化子類(可能最終調用的是父類的initialize方法)

2、不同Category中存在同一個方法,會執行哪個方法?如果是連個都執行,執行順序是什麼樣的?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *test = [[NSObject alloc] init];
        [test printTest];
    }
    return 0;
}


@implementation NSObject (Test1)

- (void)printTest {
    NSLog(@"-----test1");
}

@end


@implementation NSObject (Test2)

- (void)printTest {
    NSLog(@"-----test2");
}

@end

2019-07-04 08:56:36.870570+0800 test[911:13532] -----test2

3、Category和 Class Extension的區別?(分類和 類擴展(就是.m文件中的.h) 的區別)

- Class Extension 在編譯的時候,它的數據就已經包含在類信息中
- Category是在運行時,纔會將數據合併到類信息中

4、Category中有load方法麼?load方法什麼時候調用的?load方法能繼承麼?

- 有load方法
- +load方法會在runtime加載類、分類時調用;
- load方法可以繼承,但是一般情況下不回主動去調用load方法,都是讓系統自動去調用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章