iOS底层原理-Category

Category内部实现

//分类的结构:
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_t这个结构体下,相当于编写出一个分类,就生成了一个对应的结构体对象,例如有5个分类,到时就会生成对应5个结构体对象,每个结构体变量的变量名和内部存储的值是不同的,此时并没有合并到类对象对应的方法中

  • 误区:由于一个类只存在一个类对象,所以不存在说创建一个分类就对应有一个分类的类对象这么一个情况,而是分类内部对应的对象方法和类方法都会合并到该类对应的类对象和原类对象中
  • 分类中的方法合并:通过runtime动态将分类的方法合并到类对象、元类对象中

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

  • 由源码可以得出,最后面编译的分类,在方法列表中位置最靠前
  • 在xcode中Build Phases - Compile Sources中可以手动设置编译的顺序

category(分类)与extension(类扩展)的区别:

  • 类扩展内容一编译就已经合并到类中了,分类中是在运行时通过runtime合并到类信息中

category的实现原理:

  • category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime会将category的数据,合并到类信息中(类对象、元类对象中)

通过查看源码,发现底层将category插入数组时,调用两个函数memmovememcpy,二者的区别是:
memmove:会先判断是往左挪还是往右挪(会保证完整性)
memcpy:会一个一个拷贝(从小地址开始)

例如存在下面4块内存空间 1 2 3 4
现在要将1和2,移动到2和3的位置,通过memmove可以实现3 1 2 4
通过memcpy实现即为:1 1 1 4

+load()方法

  • 会在runtime加载类、分类时调用
  • 每个类、分类的+load,在程序运行过程中只调用一次

从源码分析,会先调用类的load方法,再调用分类的load方法
之所以运行时,类调用load方法和分类调用load方法不会像其他方法一样被覆盖,是因为内部实现中,会直接取出load方法的函数地址直接进行调用,而不是通过消息转发(objc_msgSend)

如果是通过消息机制调用方法,流程都是通过isa指针,找到对应的类方法/元类方法,在方法列表中按顺序查找
而load方法是直接通过load方法在内存中的地址值直接调用的

  • 调用顺序:
    1.先调用类的load方法
    2.按照编译的先后顺序调用(先编译,先调用)
    3.调用子类的load方法之前会先调用父类的load方法
    4.调用分类的load方法
    5.按照编译的先后顺序调用(先编译,先调用)

一道面试题:

Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?

  • 有load方法
  • load方法在runtime加载类、分类的时候调用
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是系统自动调用

+initialize()方法

  • 会在类第一次接收到消息时调用

  • 调用顺序:
    1.先调用父类的+initialize,再调用子类的+initialize(前提是父类之前没有被初始化过,)
    2.先初始化父类,再初始化子类,每个类只会初始化一次

之所以调用子类的方法,会调用父类的initialize方法,是因为内部主动发送了2条消息(一条是给父类发送initialize消息,一条是给子类发送initialize消息)
注:initialize方法最终是通过objc_msgSend方法调用的

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

  • 如果子类没有实现+initialize,会调用父类的+initialize方法(所以父类的+initialize方法可能会被调用多次)
  • 如果分类实现了+initialize,就覆盖了类本身的+initialize调用

+load()方法和+initialize()方法总结

1.调用方式

  • load是根据函数地址直接调用
  • initialize是通过objc_msgSend调用

2.调用时刻

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

3.调用顺序

1)load:

  • 先调用类的load 1)先编译的类优先调用load 2)调用子类的load之前,会先调用父类的load
  • 再调用分类的load 1)先编译的分类,优先调用load

2)initialize:

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

关联对象

@property (nonatomic, assign)int age;

在类中声明属性,系统会做3件事:
1.生成带下划线成员变量
2.setter和getter声明
3.setter和getter的实现

分类中添加属性,则只会生成getter和setter的声明

要想与类一样,实现一个属性的读写,可供解决方案有:
1.通过全局变量当中间值赋值,问题是没次创建一个属性都是共用一个全局变量,会导致数据错乱
2.使用可变字典作为全局变量,问题:1.存在线程安全的问题2.每次添加属性比较麻烦

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中,但可以通过关联对象来间接实现

//添加关联对象
void objc_setAssociatedObject(id  _Nonnull object, const void * _Nonnull key,
                              id  _Nullable value, objc_AssociationPolicy policy);

//获得关联对象
id objc_getAssociatedObject(id  _Nonnull object, const void * _Nonnull key)

//移除所有关联对象
void objc_removeAssociatedObjects(id  _Nonnull object)
  • 关联策略policy


    Snip20180703_24.png
  • 关键字key
    关联属性方法中的key获取方式:
    1.可以直接通过const void *name = &name方式,即传入name获取
    2.static const char name.传入&name获取
    3.key处传入@selector(name);
    4.仅限于getter,直接在参数处传入_cmd
objc_getAssociatedObject(self, _cmd)

注:
 1)往全局变量添加一个static,令该全局变量作用域仅当前文件
 2)直接将字符串写入,由于字符串位于常量区,故所有的值都是相同的
 3)因为每个方法其实都默认传递2个参数,第一个是self,第二个是_cmd,故也可以使用一下方式传递key值,setter方法不行,因为两者的@selector是不同的
 4)关联对象中存入的值既不是存放类对象中,也不是存放在实例对象中

实现关联对象技术的核心对象有:

  • AssociationsManager
  • AssociationHashMap
  • ObjectAssociationMap
  • ObjcAssociation
Snip20180703_30.png

其中的disguised_ptr_t是经过一个DISGUISE函数(其内部就是获取object地址进行相应的位运算)

若查找时发现value已经销毁或不存在,则系统会报坏内存访问,因为ObjcAssociation内部对value是强引用的

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

推荐阅读更多精彩内容