iOS Category底層原理 、+load 、+initialize

Category底層原理

Category可以把一個類的功能拆解成很多模塊

創(chuàng)建一個類,并創(chuàng)建兩個分類


Snip20200706_24.png

分類編譯時底層編譯成的代碼:

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)合并到類對象,元類對象中
具體步驟:


Snip20200706_19.png

源碼下載地址 https://opensource.apple.com/tarballs/objc4

如方法的合并:
runtime 會將Person的所有分類的方法列表先合并到一個列表里面,然后再通過內(nèi)存移動插入到原來Person方法列表的前面


Snip20200706_21.png

至于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)整


Snip20200706_23.png

所以如果在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 方法
編譯時就會報錯,分類中不能添加成員變量


Snip20200706_31.png

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


Snip20200706_32.png

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)的修飾符

Snip20200707_2.png

關(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


Snip20200707_4.png

+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方法)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容