Category--load与initialize

Category的概述以及实现原理
Category和Class Extension 类扩展的区别
Category中load方法是什么时候调用的?
使用关联对象为分类添加成员变量
load 与 initialize 的区别

作用

分类的主要作用就是在不改变原有类的前提下,动态地给这个类添加一些方法。具体在项目中大概有这么几个好处:
1、分类在架构设计上面可以达到解耦的效果。 开发过程中比较繁重啰嗦的业务代码对项目的可读性造成了压力,为追求架构清晰,降低维护成本低,可以通过分类进行梳理。典型的就是将项目中 AppDelegate 拆分。 AppDelegate 作为程序的入口,一般都会实现各种第三方 SDK 的初始化、写各种版本的容错代码、实现通知、支付逻辑等等功能,所以 AppDelegate 这个类很容易臃肿,这个时候可以通过实现 AppDelegate 分类来将不同的业务代码分离。

2、通过实现分类的 load 方法来实现 Method Swizzling

3、通过分类来为已知的类扩展方法和属性,Category 不会为我们的属性添加实例变量和存取方法,我们可以通过关联对象这个技术来实现对象绑定

注意事项:
1:如果category中有和原有类同名的方法,会优先调用分类中的方法,就是说会忽略原有类的方法。
2:如果多个分类中都有和原有类中同名的方法,那么调用该方法的时候执行谁由编译器决定,编译器会执行最后一个参与编译的分类中的方法。
3:过渡使用分类 也会导致APP项目 支离破碎感+性能降低
4:我们在分类中添加了属性之后,系统只是为我们生成了get方法和set方法的声明,并没有为我们生成成员变量和方法的实现
5: 名字相同的分类会引起编译报错;

Category 的实现原理

我们知道 Objective-C 通过 Runtime 运行时来实现动态语言这个特性,所有的类和对象,在 Runtime 中都是用结构体来表示的,Category 在 Runtime 中是用结构体 category_t 来表示的,下面是结构体 category_t 具体表示:

typedef 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;//添加的所有属性
} category_t;

通过结构体 category_t 可以知道,在 Category 中我们可以增加实例方法、类方法、协议、属性。我们这里简述下 Category 的实现原理:

1、在编译时期,会将分类中实现的方法生成一个结构体 method_list_t 、将声明的属性生成一个结构体 property_list_t ,然后通过这些结构体生成一个结构体 category_t 。
2、然后将结构体 category_t 保存下来
3、在运行时期,Runtime 会拿到编译时期我们保存下来的结构体 category_t。然后将结构体 category_t 中的实例方法列表、协议列表、属性列表添加到主类中。将结构体 category_t 中的类方法列表、协议列表添加到主类的 metaClass 中。

  • 在程序运行的时候,通过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

Category 为什么不能添加实例变量

通过结构体 category_t ,我们就可以知道,在 Category 中我们可以增加实例方法、类方法、协议、属性。这里没有 objc_ivar_list 结构体,代表我们不可以在分类中添加实例变量。

因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这个就是 Category 中不能添加实例变量的根本原因。

使用关联对象为分类添加成员变量

上面我们说到 在分类中添加了属性之后,系统只是为我们生成了get方法和set方法的声明,并没有为我们生成成员变量和方法的实现,下面我们来验证这一说法
我们为GSPerson对象添加了一个分类, 并且在分类中设置了一个height的属性,下面我们来使用一下该分类对象:

#import "GSPerson.h"
@interface GSPerson (Utility)
@property (nonatomic, assign) int height;
@end
image

我们为height赋值20 ,运行起来之后竟然奔溃了,并且提示我们

reason: '-[GSPerson setHeight:]: unrecognized selector sent to instance 0x1006259a0'

是不是印证了我们的说法?
那么我们该如何为Category添加一个成员变量呢?答案是关联对象:
其实主要涉及到关联对象的两个方法:objc_getAssociatedObjectobjc_setAssociatedObject

/*
 参数一:id object : 获取哪个对象里面的关联的属性。
 参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
 */
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
/*
参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self
参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过此key获得属性的值并返回。
参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
 */
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)

其中objc_AssociationPolicy policy策略是个枚举值

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   
};

具体的对应关系如下所示:


了解了这两个方法后,为关联对象添加属性就非常的简单了,比如为 GSPerson对象添加height属性具体如下:

#import "GSPerson+Utility.h"
#import <objc/runtime.h>
@implementation GSPerson (Utility)
-(void)setHeight:(int)height{
    //key值只要是一个指针即可,我们可以传入@selector(name)
    objc_setAssociatedObject(self, @selector(height), @(height), OBJC_ASSOCIATION_ASSIGN);
    
    //或者    
    //objc_setAssociatedObject(self, @"height", @(height), OBJC_ASSOCIATION_ASSIGN);
}
-(int)height{
   //_cmd == @selector(height)
   return  [objc_getAssociatedObject(self, _cmd) intValue];
   //或者
   //return  [objc_getAssociatedObject(self, @"height") intValue];
   
}
@end

那么问题来了,我们用关联对象为Category添加的成员变量具体是存储在了哪呢?
下面我们通过一幅图来了解下关联对象的本质:


具体到更直观的数据结构上,我们可以看下图

  • 关联对象是由AssociationManager全局类管理并存储在AssociationHashMap中。
  • 所有对象的关联内容都存在于一个全局容器中,和宿主类是无关的。

Extension 类扩展

一般用类扩展做什么工作?
  • 声明私有属性
  • 声明私有方法,一般没有多大用,只是为了阅读方便而已
  • 声明私有成员变量
类扩展有什么特点?
  • 编译时决议
  • 只以声明的形式存在,多数情况下存在于宿主类的.m文件中
  • 不能为系统类添加扩展
Category和Class Extension 类扩展的区别

有了上面的特点,我们就方便的将分类和扩展放到一起比较了。

  • 分类是在运行时才会将数据合并到类信息中。而类扩展在程序编译的时候,它的数据就已经包含在类信息中了
  • 分类中既可以有声明也可以有实现,而类扩展只以声明的形式存在,多数情况下存在于宿主类的.m文件中
  • 可以为系统类添加分类,但是不能添加扩展
  • 在分类中只能添加“方法”,不能增加成员变量。而扩展中既可以添加方法也可以添加成员变量。

Category中load方法是什么时候调用的?

image

load方法在程序启动加载类信息的时候就会调用。它是根据函数地址直接调用的,而不是通过消息机制的。调用顺序如下:

  1. 先调用类的+load

    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的+load之前会先调用父类的+load
  2. 再调用分类的+load

    • 按照编译的先后顺序调用(先编译,先调用)

load、initialize的区别

1. 调用顺序

以main为分界,load方法在main函数之前执行,initialize在main函数之后执行

2.相同点和不同点

2.1 相同点
  1. loadinitialize会被自动调用,不能手动调用它们。
  2. 子类实现了loadinitialize的话,会隐式调用父类的loadinitialize方法
  3. load和initialize方法内部使用了锁,因此它们是线程安全的。
2.2 不同点

子类中没有实现load方法的话,不会调用父类的load方法;而子类如果没有实现initialize方法的话,也会自动调用父类的initialize方法。
load方法是在类被装在进来的时候就会调用,initialize在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次,是懒加载模式,如果这个类一直没有使用,就不回调用到initialize方法。

3. load

在执行load方法之前,会调用load_images方法,用来扫描镜像中的+ load符号,将需要调用 load 方法的类添加到一个列表中loadable_classes,在这个列表中,会先把父类加入到待加载列表,这样保证父类在子类前调用load方法,而分类中的load方法会在类的load的方法后面加入另外一个待加载列表loadable_categories,这样保证了两个规则:

  1. 父类先于子类调用
  2. 类先于分类调用

在扫描完load方法加入到待加载方法后,会调用call_load_methods,先从loadable_classes调用类的load方法,call_class_loads;调用完loadable_classes后会调用loadable_categories中分类的load方法,call_category_loads

调用顺序如下:

  1. 父类load先于类添加到loadable_classes列表,通过call_class_loads,调用列表中的load方法,这样父类的load先于类的load执行
  2. loadable_classes为空的时候,查看loadable_classes是否为空,如果不为空则调用call_category_loads加载分类中的load方法,这样分类的load在类之后执行

4. initialize

initialize 只会在对应类的方法第一次被调用时,才会调用,initialize 方法是在 alloc 方法之前调用的,alloc 的调用导致了前者的执行。

initialize的调用栈中,直接调用其方法的其实是_class_initialize 这个C语言函数,在这个方法中,主要是向为初始化的类发送+initialize消息,不过会强制父类先发送。

与 load 不同,initialize 方法调用时,所有的类都已经加载到了内存中。

5. 使用场景

5.1 load

load一般是用来交换方法Method Swizzle,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在load方法中注册

5.2 initialize

initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作

总结:

一、调用方式

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

二、调用时刻

1、load是runtime加载类、分类的时候调用(只会调用一次)
2、initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次)
3、load比initialize先调用

三、使用场景

load 和 initialize 方法本质都是做初始化的,只不过级别或者说针对的过程不一样。load 只会调用一次,在 main 方法调用之前做初始化,比如方法交换。initialize 方法针对的是 main 方法之后,而且是懒加载(类第一次接收到消息的时候调用),使用到时才初始化。鉴于 objc_msgSend 的机制,存在多次调用的可能,但是可以使用代码进行判断。

四、load和initialize的调用顺序

load调用顺序
调用顺序,需要同时满足四种规则
先调用类的load, 在调用分类的load
先编译的类, 优先调用load
调用子类的load之前, 会先调用父类的load(父类 -> 子类)
没有父子关系的类及分类之间,按Build Phases ->Complie Source 中的编译顺序
特点:
load在父类,子类,分类之间的调用不存在覆盖,只存在先后执行顺序
initialize调用顺序
initialize调用的优先级为 (父类分类 > 父类) > (子类分类 > 子类)
initialize在父类,子类,父分类,子分类之间的调用存在覆盖【(父类与子类)(父分类与子分类)本类与分类,两两存在覆盖关系,两组相互之间只会调用优先级最高的一个】,有优先级
特点:
优先级:先分类后本类【分类覆盖本类】
顺序是:先父类后子类【存在先后执行,不存在覆盖】
通过消息机制调用, 当子类没有initialize方法时, 会调用(父类分类>父类)的initialize方法, 所以(父类分类>父类)的initialize方法会调用多次

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

推荐阅读更多精彩内容

  • 懒加载的介绍 swift中也有懒加载的方式(苹果的设计思想:希望所有的对象在使用时才真正加载到内存中) 和OC不同...
    猴子的救兵520阅读 979评论 0 1
  • 懒加载的2个好处:延迟加载属性(UI类型控件一般都会延迟加载)在后边的代码中,延迟加载的属性,不用再强制解包 懒加...
    Homer1ynn阅读 1,293评论 5 3
  • @(〓〓 iOS-Swift语法)[Swift 语法] 作者: Liwx 邮箱: 1032282633@qq.c...
    Liwx阅读 664评论 0 0
  • 懒加载的介绍 swift中也有懒加载的方式(苹果的设计思想:希望所有的对象在使用时才真正加载到内存中) 和OC不同...
    年轻岁月阅读 244评论 0 0
  • 懒加载就是延时加载的意思,比方说给某个类定义个对象属性,在用到这个属性的时候才初始化,而且重复使用只会初始化一次,...
    Super超人阅读 1,785评论 0 3