iOS底层原理 - Category实现原理(一)
通过探索Category底层原理回答以下问题
- Category是否可以添加方法、属性、成员变量?Category是否可以遵守Protocol?
- Category的本质是什么,在底层是怎么存储的?
- Category的实现原理是什么,Catagory中的方法是如何调用到的?
- Category中是否有Load方法,load方法是什么时候调用的?
- load、initialize的区别
Category可以直接添加 属性、成员变量吗?
首先我们创建一个类MGCPerson,为该类添加一个分类MGCPerson (Sport),并尝试为该分类添加实例方法、类方法、属性、成员变量,观察是否报错
创建MGCPerson
// MGCPerson.h
@interface MGCPerson : NSObject
@property (nonatomic, assign) NSInteger height;
@property (nonatomic, assign) NSInteger weight;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *brithday;
- (void)life;
+ (void)life;
@end
// MGCPerson.m
@implementation MGCPerson
- (void)life
{
NSLog(@"MGCPerson : - (void)life");
}
+ (void)life
{
NSLog(@"MGCPerson : + (void)life");
}
@end
添加分类MGCPerson (Sport),并尝试为该分类添加实例方法、类方法、属性、成员变量
通过操作我们发现,可以在Category中直接添加实例方法、类方法、属性以及准守协议,但是当尝试在Category中添加成员变量时会报错
Instance variables may not be placed in categories
,即不能直接在分类中添加成员变量。接下来我们继续探讨Category的本质,分析为什么不能在Category中直接添加成员变量
Category的底层数据结构
首先我们为MGCPerson创建两个分类MGCPerson (Sport)
、 MGCPerson (Eat)
MGCPerson (Sport)
// MGCPerson+Sport.h
@interface MGCPerson (Sport) <NSCopying, NSCoding>
@property (nonatomic, assign) NSInteger stepCount;
@property (nonatomic, copy) NSDate *sportStartTime;
@property (nonatomic, copy) NSDate *sportEndTime;
- (void)sport;
+ (void)sport;
- (void)run;
+ (void)run;
@end
// MGCPerson+Sport.m
@implementation MGCPerson (Sport)
- (void)sport
{
NSLog(@"MGCPerson (Sport) : - (void)sport");
}
+ (void)sport
{
NSLog(@"MGCPerson (Sport) : + (void)sport");
}
- (void)run
{
NSLog(@"MGCPerson (Sport) : - (void)run");
}
+ (void)run
{
NSLog(@"MGCPerson (Sport) : + (void)run");
}
- (void)life
{
NSLog(@"MGCPerson (Sport) : - (void)life");
}
+ (void)life
{
NSLog(@"MGCPerson (Sport) : + (void)life");
}
@end
MGCPerson (Eat)
// MGCPerson+Eat.h
@interface MGCPerson (Eat)
+ (void)eat;
- (void)eat;
@end
// MGCPerson+Eat.m
@implementation MGCPerson (Eat)
- (void)eat
{
NSLog(@"MGCPerson (Eat) : - (void)eat");
}
+ (void)eat
{
NSLog(@"MGCPerson (Eat) : + (void)eat");
}
- (void)life
{
NSLog(@"MGCPerson (Eat) : - (void)life");
}
+ (void)life
{
NSLog(@"MGCPerson (Eat) : + (void)life");
}
@end
我们知道OC是基于C/C++实现的,接下来我们将通过命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件名
将分类MGCPerson (Sport)
、 MGCPerson (Eat)
转化成.cpp
文件,窥探Category的底层实现
分析.cpp
文件思路
第一步: 因为我们分析的是.m
转化称的.cpp
文件,所以我们首先尝试在.cpp
文件中查找关键字@implementation MGCPerson (Sport)
,通过检索,我们找到了对应方法的实现
// @implementation MGCPerson (Sport)
static void _I_MGCPerson_Sport_sport(MGCPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_0);
}
static void _C_MGCPerson_Sport_sport(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_1);
}
static void _I_MGCPerson_Sport_run(MGCPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_2);
}
static void _C_MGCPerson_Sport_run(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_3);
}
static void _I_MGCPerson_Sport_life(MGCPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_4);
}
static void _C_MGCPerson_Sport_life(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_5);
}
// @end
对函数名进行简单分析,得出命名规则
Category-MethodNameRule.png
第二步: 检索函数名_I_MGCPerson_Sport_sport
、_C_MGCPerson_Sport_sport
进一步分析
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
3,
{{(struct objc_selector *)"sport", "v16@0:8", (void *)_I_MGCPerson_Sport_sport},
{(struct objc_selector *)"run", "v16@0:8", (void *)_I_MGCPerson_Sport_run},
{(struct objc_selector *)"life", "v16@0:8", (void *)_I_MGCPerson_Sport_life}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_CLASS_METHODS_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
3,
{{(struct objc_selector *)"sport", "v16@0:8", (void *)_C_MGCPerson_Sport_sport},
{(struct objc_selector *)"run", "v16@0:8", (void *)_C_MGCPerson_Sport_run},
{(struct objc_selector *)"life", "v16@0:8", (void *)_C_MGCPerson_Sport_life}}
};
分析可知:上述代码创建了_method_list_t
类型的结构体变量_OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport
和_OBJC_$_CATEGORY_CLASS_METHODS_MGCPerson_$_Sport
分别用于存储实例方法列表和类方法列表。
struct _method_list_t {
unsigned int entsize; // sizeof(struct _objc_method) 方法所占内存大小
unsigned int method_count; // 方法个数
struct _objc_method method_list[3]; // 方法列表
};
struct _objc_method {
struct objc_selector * _cmd; // SEL,可以理解为函数名字符串
const char *method_type; // 函数类型(包括返回值类型、参数类型)
void *_imp; // 指向函数实现
};
这里简单说下_objc_method
中的method_type
,详细介绍后续在探究runtime消息机制时再继续扒。method_type
其实可以看做用字符缩写来表达的函数类型字符串,比如v@:i
就是返回类型为void
,第一个参数为id类型
,第二个参数为指针类型
,第三个参数为int类型
的函数,如- (void)addStepCount:(int)count
(我们知道iOS中方法调用会默认传入隐式参数 方法调用者:self 和 方法名:_cmd。这也是为什么我们可以在方法内部访问self
、cmd
的原因)
第三步:继续检索_OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport
,继续分析
static struct _category_t _OBJC_$_CATEGORY_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MGCPerson",
0, // &OBJC_CLASS_$_MGCPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MGCPerson_$_Sport,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MGCPerson_$_Sport,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MGCPerson_$_Sport,
};
分析可知:上述代码创建了一个_category_t
类型的结构体变量 _OBJC_$_CATEGORY_MGCPerson_$_Sport
,并且传入了类方法列表、实例方法列表、协议类别、属性列表,具备了Category的所有信息,没错Category在底层就是_category_t
类型,下边我们检索结构体类型_category_t
,看下_category_t
的定义
struct _category_t {
const char *name; // 类名(MGCPerson)
struct _class_t *cls; // 类对象指针(指向MGCPerson类对象)
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在底层的存储结构,并且可以看到底层并没有存储成员变量,这也就是为什么直接添加成员变量会报错的原因。
上边我们已经分析了_category_t
及_method_list_t
,下边我们再看下_protocol_list_t
和_prop_list_t
先看_protocol_list_t
,同样的在.cpp
文件中检索_protocol_list_t
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit 协议个数
struct _protocol_t *super_protocols[2]; // 协议数组(2是数组长度)
} _OBJC_CATEGORY_PROTOCOLS_$_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying, // 协议变量地址
&_OBJC_PROTOCOL_NSCoding // 协议变量地址
};
分析可知:上述代码定义了_protocol_list_t
类型的结构体变量_OBJC_CATEGORY_PROTOCOLS_$_MGCPerson_$_Sport
用于存储协议列表
struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
0,
"NSCopying",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t _OBJC_PROTOCOL_NSCoding __attribute__ ((used)) = {
0,
"NSCoding",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCoding
};
分析可知:上述代码定义了两个_protocol_t
类型的协议变量_OBJC_PROTOCOL_NSCopying
和_OBJC_PROTOCOL_NSCoding
struct _protocol_t {
void * isa; // NULL
const char *protocol_name; // 协议名称
const struct _protocol_list_t * protocol_list; // super protocols 父协议
const struct method_list_t *instance_methods; // 协议中实例方法列表
const struct method_list_t *class_methods; // 协议中类方法列表
const struct method_list_t *optionalInstanceMethods;// 协议中可选实现的实例方法列表
const struct method_list_t *optionalClassMethods;// 协议中可选实现的类方法列表
const struct _prop_list_t * properties; // 协议中的属性列表
const unsigned int size; // sizeof(struct _protocol_t) // 协议所占内存大小
const unsigned int flags; // = 0
const char ** extendedMethodTypes;
};
再来看下_prop_list_t
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t) 属性数组所占内存大小
unsigned int count_of_properties; // 属性个数
struct _prop_t prop_list[3]; // 属性数组
} _OBJC_$_PROP_LIST_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
3,
{{"stepCount","Tq,N"},
{"sportStartTime","T@\"NSDate\",C,N"}, // 属性名、 类型名、copy、nonatomic
{"sportEndTime","T@\"NSDate\",C,N"}} // 属性名、 类型名、copy、nonatomic
};
分析可知:上述代码创建了一个_prop_t
类型的结构体变量_OBJC_$_PROP_LIST_MGCPerson_$_Sport
用于存放属性列表
struct _prop_t {
const char *name; // 属性名
const char *attributes; // 属性描述,就是@property后边的一些关键字
};
到此我们已经了解了Category的底层数据结构,以及可以在Category中添方法、协议、属性,但是不能添加成员变量,那么Category中的属性和类中的属性是否一样的呢?如果不一样,怎么实现和类中添加属性一样的效果呢?接下来我们将探讨这个问题
Category中的属性
我们已经为MGCPerson
添加了height
、weight
、name
、brithday
属性,为MGCPerson+Sport
添加了stepCount
、sportStartTime
、sportEndTime
属性
并且我们知道在类中添加一个属性,系统为我们做了三件事
@property (nonatomic, copy) NSString *name;
- 创建了一个成员变量
_name
- 生成了
setter
、getter
方法的声明 - 生成了
setter
、getter
方法的实现
那么在Category添加属性系统是否也会为我们做这三件事呢?
-
Category添加属性系统不会自动添加成员变量
根据上边的学习我们知道,_category_t 中并没有存储成员变量 -
Category添加属性系统会生成
setter
、getter
方法的声明
尝试在MGCPerson+Sport.h
中直接敲对应的setter
、getter
,发现是有提示的,说明会生成setter
、getter
方法的声明 -
Category添加属性系统不会生成
setter
、getter
方法的实现
下边我们比下MGCPerson.cpp
和MGCPerson+Sport.cppp
文件
// @implementation MGCPerson
static void _I_MGCPerson_life(MGCPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_d3d3d6_mi_0);
}
static void _C_MGCPerson_life(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_d3d3d6_mi_1);
}
static NSInteger _I_MGCPerson_height(MGCPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_height)); }
static void _I_MGCPerson_setHeight_(MGCPerson * self, SEL _cmd, NSInteger height) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_height)) = height; }
static NSInteger _I_MGCPerson_weight(MGCPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_weight)); }
static void _I_MGCPerson_setWeight_(MGCPerson * self, SEL _cmd, NSInteger weight) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_weight)) = weight; }
static NSString * _Nonnull _I_MGCPerson_name(MGCPerson * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_MGCPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_MGCPerson_setName_(MGCPerson * self, SEL _cmd, NSString * _Nonnull name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct MGCPerson, _name), (id)name, 0, 1); }
static NSDate * _Nonnull _I_MGCPerson_brithday(MGCPerson * self, SEL _cmd) { return (*(NSDate * _Nonnull *)((char *)self + OBJC_IVAR_$_MGCPerson$_brithday)); }
static void _I_MGCPerson_setBrithday_(MGCPerson * self, SEL _cmd, NSDate * _Nonnull brithday) { (*(NSDate * _Nonnull *)((char *)self + OBJC_IVAR_$_MGCPerson$_brithday)) = brithday; }
// @end
// @implementation MGCPerson (Sport)
static void _I_MGCPerson_Sport_sport(MGCPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_0);
}
static void _C_MGCPerson_Sport_sport(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_1);
}
static void _I_MGCPerson_Sport_run(MGCPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_2);
}
static void _C_MGCPerson_Sport_run(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_3);
}
static void _I_MGCPerson_Sport_life(MGCPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_4);
}
static void _C_MGCPerson_Sport_life(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_5);
}
// @end
可以看到,MGCPerson
自动生成了对应属性的setter
、getter
的实现方法,但是MGCPerson+Sport
却没有生成对应属性的setter
、getter
的实现方法,综上所述在Category添加属性系统仅仅只生成了setter
、getter
方法的声明
如何使Category中的属性与类中的属性具备同样的效果(关联对象)
通过上边的学习,我们知道想达到与类添加属性一样的效果,我们需要做两件事
- 添加一个存储属性值的变量
- 实现
setter
、getter
方法
可以通过runtime中的关联对象方法来实现
/ **
*使用给定的键和关联策略为给定的对象设置关联值。
*
* @param object 关联的源对象。
* @param key 关联的键。
* @param value 与对象的键相关联的值。 传递nil清除现有关联。
* @param policy 关联的策略
*
* /
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
// 关联策略,和@property后的关键字对应
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
};
- (void)setSportEndTime:(NSDate *)sportEndTime
{
objc_setAssociatedObject(self, @selector(sportEndTime), sportEndTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDate *)sportEndTime
{
return objc_getAssociatedObject(self, _cmd);
}
系统是如何管理关联对象的
去官网下载runtime源码,搜索objc_setAssociatedObject
方法,这里不做过多分析,简单说下结论,后续会单开一篇扒关联对象的实现原理。
runtime用四个类来管理关联对象,AssociationsManager
、AssociationsHashMap
、AssociationsMap
、ObjectAssociation
,四个类之间的关系如下图
- 对象并不是存储在被关联对象本身内存中
- 关联对象存储在全局的统一的一个AssociationsManager中
- 设置关联对象为nil,就相当于是移除关联对象
小尾巴
到此我们已经解决了开篇中的第1、2个问题,后续问题见iOS底层原理 - Category实现原理(二)