源码解读

iOS底层原理 - Category实现原理(一)

2020-04-17  本文已影响0人  julieQY7

通过探索Category底层原理回答以下问题

  1. Category是否可以添加方法、属性、成员变量?Category是否可以遵守Protocol?
  2. Category的本质是什么,在底层是怎么存储的?
  3. Category的实现原理是什么,Catagory中的方法是如何调用到的?
  4. Category中是否有Load方法,load方法是什么时候调用的?
  5. 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-IVarError.png
通过操作我们发现,可以在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。这也是为什么我们可以在方法内部访问selfcmd的原因)

第三步:继续检索_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添加了heightweightnamebrithday属性,为MGCPerson+Sport添加了stepCountsportStartTimesportEndTime属性

并且我们知道在类中添加一个属性,系统为我们做了三件事
@property (nonatomic, copy) NSString *name;

那么在Category添加属性系统是否也会为我们做这三件事呢?

// @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自动生成了对应属性的settergetter的实现方法,但是MGCPerson+Sport却没有生成对应属性的settergetter的实现方法,综上所述在Category添加属性系统仅仅只生成了settergetter方法的声明

如何使Category中的属性与类中的属性具备同样的效果(关联对象)

通过上边的学习,我们知道想达到与类添加属性一样的效果,我们需要做两件事

/ **
  *使用给定的键和关联策略为给定的对象设置关联值。
  *
  * @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用四个类来管理关联对象,AssociationsManagerAssociationsHashMapAssociationsMapObjectAssociation,四个类之间的关系如下图

管理关联对象的类之间的关系.png

小尾巴

到此我们已经解决了开篇中的第1、2个问题,后续问题见iOS底层原理 - Category实现原理(二)

上一篇下一篇

猜你喜欢

热点阅读