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

Category底层原理

Category可以把一个类的功能拆解成很多模块

创建一个类,并创建两个分类


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;    //  协议列表
    const struct _prop_list_t *properties;   //  属性列表
};

每一个分类对应一个结构体对象

如:

#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"}}
};

分类里的属性、方法、协议等最后也是通过runtime动态合并到类对象,元类对象中
具体步骤:


Snip20200706_19.png

源码下载地址 https://opensource.apple.com/tarballs/objc4

如方法的合并:
runtime 会将Person的所有分类的方法列表先合并到一个列表里面,然后再通过内存移动插入到原来Person方法列表的前面


Snip20200706_21.png

至于Test1 和 Test2 谁在前 谁在后,根据编译循序来的因为 合并分类列表的代码为

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

通过源码可知,后编译的在前面,至于编译循序可以在下面调整


Snip20200706_23.png

所以如果在Person 和 两个分类中都有一个相同的方法 如 run,以上图的编译循序 是执行 Test1中的方法。这个并不是方法覆盖,而是,方法查找的时候会先查到Test1中的方法

和类扩展的区别 Extension(OC)

类扩展是在编译的时候就将方法 属性等加入到原先的方法、属性列表中了

分类添加属性相关

1、当我们给一个类添加属性的时候如

@property (nonatomic, assign) int age;
// 给一个类添加age 属性

会默认实现下面3个步骤

// 1、声明一个成员变量
{
   int _age;
}
//2、 声明set 和 get 方法
- (void)setAge:(int)age;
- (int)age;

//3、实现get 和set 方法
- (void)setAge:(int)age {
    _age = age;
}

- (int)age {
    return _age;
}

2、当我们给分类添加属性时,默认只有方法的声明

@property (assign, nonatomic) int weight;
//默认声明
- (void)setWeight:(int)weight;
- (int)weight;

但是没有实现,所以可以调用,但是会报错找不到方法

  Person *person = [Person new];
        person.weight = 10;
        NSLog(@"%d",person.weight);
//-[Person weight]: unrecognized selector sent to instance 0x10067f080'

我们如果手动加上成员变量 实现set 和get 方法
编译时就会报错,分类中不能添加成员变量


Snip20200706_31.png

从上面的分类编译成的底层代码也可以发现,根本没有成员变量列表,
下图是一个普通类的底层结构。有个成员变量列表


Snip20200706_32.png

3、给分类添加关联对象实现类似成员变量的功能

实现属性的set 和 get 方法

// 地址值
static const void *PersonNameKey = &PersonNameKey;
/**
  加上static 防止外面访问,否则别的地方
  通过 extern const void *PersonNameKey; 可以访问到
 */


- (void)setName:(NSString *)name {
//objc_AssociationPolicy 关联策略 类似于用什么修饰 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 对应的修饰符

Snip20200707_2.png

关联对象的原理

void objc_setAssociatedObject(id object, const void * key,
                              id value, 
                 objc_AssociationPolicy policy)

实现关联对象技术的核心对象有
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
runtime 管理着一个全局的AssociationsManager,内部管理一个map(AssociationsHashMap),这个map的key是根据object生成的,value对应的是另一个map(ObjectAssociationMap),objectMap的key就是上面方法传进来的key,value对应ObjcAssociation对象,ObjcAssociation内部包含policy和真正的value


Snip20200707_4.png

+load方法

一、+load 方法会在runtime加载类和分类时调用
二调用顺序
1.先调用类的+load
按照编译顺序调用(先编译,先调用)
调用子类的+load方法之前,如果父类的+load没调用过就先调用父类的+load方法
2、所有类的+load方法调用完,再调用分类的+load
按照编译顺序调用(先编译,先调用)(没有父类,子类之分)

3、底层代码
先调用父类的+load 方法的原因 准备调用
prepare_load_methods会调用下面的代码

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);//递归调用

    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(); // 调用类的
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads(); // 调用分类的

        // 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 调用分类的+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); // 直接通过函数指针调用
}

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方法是直接找到,然后通过函数指针调用的,不像上面的run方法 通过objc_msgSend调用,通过isa指针找方法列表

+initialize方法

调用顺序

+initialize方法会在类第一次接收到消息时调用
1、先调用父类的initialize, 再调用子类的+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)

源码

每次调用objc_msgSend 会调用

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的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点
如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
如果分类实现了+initialize,就覆盖类本身的+initialize调用

+load 和 initialize 总结

load、initialize方法的区别什么?
1.调用方式
1> load是根据函数地址直接调用
2> initialize是通过objc_msgSend调用

2.调用时刻
1> load是runtime加载类、分类的时候调用(只会调用1次)
2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

load、initialize的调用顺序?
1.load
1> 先调用类的load
a) 先编译的类,优先调用load
b) 调用子类的load之前,会先调用父类的load

2> 再调用分类的load
a) 先编译的分类,优先调用load

2.initialize
1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的initialize方法)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266