Category原理解析(1)-load方法和initialize方法详解

1. Category的底层结构

通过runtime动态的将分类的方法合并到类对象或元类对象中,程序编译的时:category会生成,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;//属性
};
category的结构.png
  1. 从结构体可以知道,有属性列表,所以分类可以声明属性,但是分类只会生成该属性对应的get和set的声明,没有去实现该方法。
  2. 结构体没有成员变量列表,所以不能声明成员变量。
  3. 分类并不会改变原有类的内存分布的情况,它是在运行期间决定的,此时内存的分布已经确定,若此时再添加实例会改变内存的分布情况,这对编译性语言是灾难,是不允许的。

category的源码阅读轨迹:

  • objc-os.mm
  1. _objc_init
  2. map_images
  3. map_images_nolock
  • objc-runtime-new.mm
  1. _read_images
  2. remethodizeClass
  3. attachCategories
  4. attachLists
  5. realloc、memmove、 memcpy

category的源码分析
① readimges 是读取模块的意思,参数有totalClass是所有的类的意思.
②.remethodizeClass是重新组织类的方法的意思
③.attachCategories是将分类重新规整,参数有两个:一个类名,一个是分类的数组,在内部有一个方法数组的二维数组,一个属性数组的二维数组,一个协议的二维数组,把所有Category的方法、属性、协议数据,合并到一个大数组中.(attachCategories)
④.attachLists有两个参数一个所有category的方法列表或属性列表或协议列表的数组和第二个参数传入的数组的count,realloc重新计算方法列表分配的内存大小,memmove移动原来的方法数据到末尾,memcpy分类的数据插入到原来数据的前面.

顺序的总体的概括:

  1. 通过Runtime加载某个类的所有Category数据.
  2. 把所有Category的方法、属性、协议数据,合并到一个大数组中
    后面参与编译的Category数据,会在数组的前面.
  3. 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面.

category memmove到memcpy的过程流程图:

category-memmove-memcpy(1).png
category-memmove-memcpy(2).png

代码例子:

// 原来的类和分类看Demo,这里就不列举出来了
// 开始调用
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];
        [person run];
        [person test];
        [person eat];
        // 通过runtime动态将分类的方法合并到类对象、元类对象zhong
    }
    return 0;
}
log日志.png
Build Phases.png

通过运行结果可知,分类方法会覆盖原来类对象方法,并且最后参与编译的会在调用顺序最前面。(其实不是覆盖,只是寻找方法时的顺序.)

这里有2个关于category的问题
1.分类中能不能添加属性,为什么?
不能。类的内存布局在编译时期就已经确定了,category是运行时才加载的早已经确定了内存布局所以无法添加实例变量,如果添加实例变量就会破坏category的内部布局。
2.为什么说category是在运行时加载的?不能添加实例变量,那为什么能添加属性?
根据category_t的结构
①.category小括号里写的名字
②.要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
③.这个category所有的-方法
④.这个category所有的+方法
⑤.这个category实现的protocol,比较不常用在category里面实现协议,但是确实支持的
⑥.这个category所有的property,这也是category里面可以定义属性的原因,不过这个property不会@synthesize实例变量,一般有需求添加实例变量属性时会采用objc_setAssociatedObject和objc_getAssociatedObject方法绑定方法绑定,不过这种方法生成的与一个普通的实例变量完全是两码事。

2. +load方法和initialize方法

一. +load

+load方法会在runtime加载类、分类时调用,并且只调用一次.

  • load方法的调用顺序
  1. 先调用类的+load.
  2. 按照编译先后顺序调用(先编译,先调用).
  3. 调用子类的+load之前会先调用父类的+load.
  4. 再调用分类的+load.
  • 按照编译先后顺序调用(先编译,先调用)
  1. load方法为什么类和分类都调用,原因是load方法是根据方法地址直接调用.

代码佐证:


category的调用顺序.png
  • objc4源码解读过程:objc-os.mm
  1. _objc_init
  2. load_images
  3. prepare_load_methods
    schedule_class_load
    add_class_to_loadable_list
    add_category_to_loadable_list
  4. call_load_methods
    call_class_loads
    call_category_loads
    (*load_method)(cls, SEL_load)
  • +load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用。

二. +initialize方法

+initialize方法会在类第一次接收到消息时调用.(initialize应该是objc_msgSend实现的)

  • initialize调用顺序:
  1. 先调用父类的+initialize,再调用子类的+initialize.
  2. (先初始化父类,再初始化子类,每个类只会初始化1次).
  3. 如果分类实现了+initialize,就覆盖类本身的+initialize调用.
  • objc4源码解读过程
  1. objc-msg-arm64.s
    objc_msgSend
  2. objc-runtime-new.mm
    class_getInstanceMethod
    lookUpImpOrNil
    lookUpImpOrForward
    _class_initialize
    callInitialize
    objc_msgSend(cls, SEL_initialize)

调用的详情解析:
①.class_getInstanceMethod这个是找到对象方法的方法.
②.lookUpImporNil.
③.lookUpImporForward在查找方法的时候调用_class_initialize看类是否初始化,如果没有初始化就调用callInitialize初始化.
④._class_initialize 是一个递归来判断父类是否初始化.

调用顺序证明:

每个类都实现了+ initialize方法.png
并不是每一个类都实现了+ initialize方法.png

解析:
1.[Student alloc]会调用+initialize方法,因为他有父类Person,所以先调用Person的+initialize方法,又因为分类在前面,所以调用了Person(Test2)的+initialize方法。但是他自己本身没有实现+initialize方法,所以会去父类查找,然后分类方法在前面,所以调用了Person(Test2)的+initialize方法。
2.[Teacher alloc]会调用+initialize方法,因为他有父类Person,所以先调用Person的+initialize方法,但是前面已经初始化过了,所以跳过,调用自己的+initialize方法,但是因为他自己没有实现+initialize方法,所以调用父类的+initialize方法,又因为分类方法在前面,所以调用Person(Test) +initialize方法。
3.[Person alloc],因为前面已经初始化过了,所以不会再调+initialize方法,所以这里不打印。

+initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:

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