面试题引发的思考:
Q: Category的作用是什么?
- 在不修改原来类的基础上,为一个类扩展方法。
Q: Category的实现原理?
- Category编译之后的底层结构是
struct category_t
,里面存储着分类的对象方法、类方法、属性、协议信息; - 在程序运行的时候,Runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。
1. Category介绍
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)study;
+ (void)exam;
@end
@implementation Person
- (void)study {
NSLog(@"Person - study");
}
+ (void)exam {
NSLog(@"Person - exam");
}
@end
// TODO: ----------------- Person (Student)类 -----------------
@interface Person (Student) <NSCopying, NSCoding>
- (void)study;
+ (void)exam;
- (void)study1;
+ (void)exam1;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
@implementation Person (Student)
- (void)study {
NSLog(@"Person (Student) - study");
}
+ (void)exam {
NSLog(@"Person (Student) - exam");
}
- (void)study1 {
NSLog(@"Person (Student) - study1");
}
+ (void)exam1 {
NSLog(@"Person (Student) - exam1");
}
@end
// TODO: ----------------- Person (Teacher)类 -----------------
@interface Person (Teacher)
- (void)study;
@end
@implementation Person (Teacher)
- (void)study {
NSLog(@"Person (Teacher) - study");
}
@end
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person = [[Person alloc] init];
[person study];
[person study1];
[Person exam];
[Person exam1];
}
根据iOS底层原理 - OC对象的本质(二)可知:
当Person
的instance对象调用对象方法study
时,通过instance对象的isa
找到class对象,最后找到对象方法的实现进行调用。
Q: 那又是怎样调用Person
分类的对象方法study1
呢?
是通过instance对象的isa
找到一个category-class对象,然后找到对象方法的实现进行调用吗?
实际上是:
一个instance对象,只有一个class对象。
class对象会把本身以及分类的对象方法放到class对象中,即
study
方法、study1
方法都在Person
的class对象中。
即当instance调用对象方法study
、study1
时,通过instance的isa
找到class,最后找到对象方法的实现进行调用。同理meta-class对象会把本身以及分类的类方法放到meta-class对象中。即
exam
方法、exam1
方法都在Person
的meta-class对象中。
即当Person
调用类方法exam
、exam1
时,通过class的isa
找到meta-class,最后找到类方法的实现进行调用。
2. Category底层结构:
从OC源码中objc-runtime-new.h
文件可知:
由OC源码可知:
- Category本质是
category_t
结构体;category_t
结构体存储着对象方法、类方法、协议、属性;category_t
结构体没有成员变量的定义,说明分类是不允许添加成员变量;- 分类中添加的属性不会生成成员变量,只会生成
get
方法、set
方法的声明,需要我们自己去实现。
(1) 对以上结论进行实例分析:
通过命令行语句xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Preson+Student.m
将获得C++文件,文件中可以找到category_t
结构体:
category_t
结构体与OC源码中一样。
通过category_t
可以找到_OBJC_$_CATEGORY_Person_$_Student
:
_OBJC_$_CATEGORY_Person_$_Student
结构体中的参数与category_t
结构体中的参数一一对应,分别对应类名、_class_t
结构体、对象方法、类方法、协议、属性。
接着通过_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Student
找到instance_methods
实现:
里面有两个方法study
和study1
,这是我们在分类中定义的对象方法。
接着通过_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Student
找到class_methods
实现:
里面有两个方法exam
和exam1
,这是我们在分类中定义的类方法。
接着通过_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Student
找到protocols
实现:
里面有两个协议NSCopying
和NSCoding
,这是我们在分类中遵守的协议。
接着通过_OBJC_$_PROP_LIST_Person_$_Student
找到properties
实现:
里面有两个参数name
和age
,这是我们在分类中定义的属性。
由以上分析可知:
分类源码中将我们定义的对象方法、类方法、协议、属性等都存放在
catagory_t
结构体中。
(2) catagory_t
存储方式
现在通过Runtime源码查看catagory_t
存储的对象方法、类方法、属性、协议等是如何存储在类对象中的。
首先找到Runtime初始化函数_objc_init
:
然后通过&map_images
读取模块找到map_images
函数:
然后通过map_images_nolock
找到_read_images
函数:
然后通过_read_images
找到Discover categories
分类相关代码:
由Discover categories
分类相关代码可知:
搜索分类信息,也就是说这段代码的功能是用来查找是否有分类。
通过_getObjc2CategoryList
函数获取到分类列表以后,再进行遍历,获得其中的方法、协议、属性等;
然后通过remethodizeClass
函数重新组织类方法,我们进入remethodizeClass
函数内部:
由remethodizeClass
函数相关代码可知:
attachCategories
函数传入了对象cls
和分类数组cats
,对两者进行相关操作。我们进入attachCategories
函数内部:
由前文分析可知:
分类信息存储在
category_t
结构体中,而一个类可以有多个分类,则多个分类就保存在categroy_list
中。
根据以上源码对对象方法的操作进行分析:
- 首先通过
malloc
分配内存空间来存储方法数组;- 然后通过遍历取出分类中对象的方法列表;
- 得到
class_rw_t
结构体rw
,并通过attachList
函数将所有分类的对象方法附加到类对象的方法列表中。由以上代码可知:
attachCategories
函数对类方法、属性、协议的处理与对象方法同理。
接下来进入attachList
函数内部:
其中有两个函数:
memmove
内存移动:
void *memmove(void *__dst, const void *__src, size_t __len);
将__src
的内存移动__len
块内存到__dst
中
memcpy
内存拷贝:
void *memcpy(void *__dst, const void *__src, size_t __n);
将__src
的内存拷贝__n
块内存到__dst
中
根据以上代码分析:
addedLists
是所有分类的方法列表、属性列表、协议列表,addedCount
是分类的个数;array()->lists
是类的方法列表、属性列表、协议列表,array()->count
是原来的个数;memmove
函数:将array()->lists
的内存移动oldCount * sizeof(array()->lists[0])
块内存到array()->lists + addedCount
中;memcpy
函数:将addedLists
的内存复制addedCount * sizeof(array()->lists[0])
块内存到array()->lists
中。
(3) 根据最初的代码分析:
代码包括:Person
类,分类Person+Teacher
和分类Person+Student
,
array()->lists
是类的方法列表,array()->count
为1
,结构显示如下:
通过realloc
函数重新分配内存,addedLists
包括分类Person+Teacher
和分类Person+Student
的方法列表,addedCount
为2
,结构显示如下:
通过
memmove
函数将array()->lists
的内存移动oldCount * sizeof(array()->lists[0])
块内存到array()->lists + addedCount
中
即将类的方法列表的内存移动1
块内存到原地址+2
的内存中。通过
memcpy
函数将addedLists
的内存复制addedCount * sizeof(array()->lists[0])
块内存到array()->lists
中
即将两个分类的方法列表的内存移动2
块内存到原地址的内存中。
结构显示如下:
由以上分析可知:
- 分类的方法列表追加到类的方法列表前面,保证了分类方法的优先调用;
- 分类重写类的方法,不是覆盖,而是优先调用,类的方法存在最后的内存中;
- 多个分类和类有同样的方法时,调用方法是按照分类的编译顺序逆序排列,示例如下:
同理可知:
分类的属性、协议的处理与分类的方法同理。