Categroy : 分类,也叫类别(请注意,和拓展是2个东西)
-
Category 的作用
-
苹果推荐:
1、为已存在的类添加方法、属性
2、可以把类的实现分开在几个不同的文件里面。好处:
a)可以减少单个文件的体积
b)可以把不同的功能组织到不同的Category里
c)可以由多个开发者共同完成一个类
d)可以按需加载想要的Category等等。
3、声明私有方法 -
脑洞大的开发者
4、 模拟多继承
5、 把Framework的私有方法公开
1、Category的结构
1.1 正确的结构
在源码中能找到这样的信息: 在objc-runtime-new.h
中
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;
// Fields below this point are not always present on disk.
.........
};
在分类的结构体中,能看到有类方法、实例方法、协议、属性,说明结构体是可以添加属性的
1.2 错误的结构
注意和另一个结构体区分 。后面的备注已经写了 objc2 不可用,也就是废弃
了
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
从另外一个角度来看,在源码void _objc_init(void)中,加载分类的时候有一段代码是这样的:
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
}
.......
}
1.3 结构中的name、cls
定义一个 JEPerson+JECate.h
的分类
通过clang命令转换成底层
clang -rewrite-objc JEPerson+JECate.m
在原有文件夹中找到 JEPerson+JECate.cpp
并打开,在靠近底部能看到这样的一个结构
static struct _category_t _OBJC_$_CATEGORY_JEPerson_$_JECate __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"JEPerson", ///< name 属性
0, // &OBJC_CLASS_$_JEPerson,
0,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_JEPerson_$_JECate,
0,
0,
};
从这里能看到,编译阶段 name 是 JEPerson
在 _read_images
中 有一个加载分类的流程
以
objc4-779
版本为例
// Discover categories.
for (EACH_HEADER) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
auto processCatlist = [&](category_t * const *catlist) {
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 添加这一行代码 来打印分类
printf("categories classes: %s %s\n",cat->name,cls->mangledName());
const char *cname = cat->name;
const char *oname = "JECate";
if (strcmp(cname, oname) == 0) {
printf("来了 老弟\n");
}
.......
};
processCatlist(_getObjc2CategoryList(hi, &count));
processCatlist(_getObjc2CategoryList2(hi, &count));
}
ts.log("IMAGE TIMES: discover categories");
打印结果:
.......
categories classes: XCTErrors NSError
categories classes: JECate JEPerson
对于分类中的name,我的理解为
编译时:是JEPerson
运行时:是JECate
⚠️
在objc4-750
版本 运行结果不一样:
cat->name 打印结果是JEPerson
,
mac 10.15之后 objc4-750没有运行起来,所以cls
没看到打印结果
2、分类的加载流程
在非懒加载类有简单说到在启动时,非懒加载类的加载流程(懒加载类在启动的时候并没有被加载),那么对于分类的加载流程时怎样的?
因为分类是与主类相关联,所以需要分多种情况来看
2.1 、主类懒加载
2.1.1、 分类懒加载
主类和分类都是懒加载的情况下,在编译期自行处理自己的相关信息,然后在类第一次发送消息(比如初始化一个实例对象)的时候,分类进行关联
2个重要的方法
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
const char *cname = cls->mangledName();
const char *oname = "JEPerson";
if (strcmp(cname, oname) == 0) {
printf("来了 老弟\n"); //正确的拿到我们需要研究的类 注意:进来的cls 是元类
}
.....
if (slowpath(!cls->isRealized())) { // 因为都是懒加载类,所以第一次发送消息时,类并没有实现
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock); // 元类的实现
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock); //在这里 进行元类和类的绑定,并实现类
}
}
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
.....
// Attach categories
methodizeClass(cls, previously); // 将分类的相关信息与主类相关联
return cls;
}
简单整理一下流程(初始化流程)
lookUpImpOrForward() -> realizeClassMaybeSwiftAndLeaveLocked()
-》
realizeClassWithoutSwift()
-》
lookUpImpOrForward() -> initializeAndLeaveLocked()
-》
getMaybeUnrealizedNonMetaClass(cls, inst) // 元类与类进行关联
-> realizeClassMaybeSwiftAndUnlock(); //实现类
-》
realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift()
-》
methodizeClass()分类的相关信息在这里面处理
2.1.2 、 分类非懒加载
在read_image()
中,因为主类是懒加载类,所以在这里主类是不加载的,但是分类会被加载(代码里的// Discover categories
部分),
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
因为主类是懒加载类,最后走的是 unattachedCategories
方法,也就是将分类先存储到哈希表里。
看addForClass()
中的 try_emplace()
解释
// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
那具体主类是什么时候被实现的呢? 在load_image
的时候 (所有版本类都是这个流程)
void _objc_init(void)
{
.....
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
// 就是这里面的 map_image 完毕之后 执行的 load_images 里面
}
load_images
-》
prepare_load_methods
load_images(const char *path __unused, const struct mach_header *mh)
{
.....
prepare_load_methods((const headerType *)mh);
call_load_methods();
}
void prepare_load_methods(const headerType *mhdr)
{
....
if (!cls) continue;
realizeClassWithoutSwift(cls, nil); // 这里进行主类的实现 并与分类进行关联,
}
主类懒加载+分类分懒加载的情况
,read_images
里面 主类并不处实现,将分类的信息先加入哈希表里
read_images
完毕之后,进入load_images
时,进行了prepare_load_methods
的操作,在这里,如果主类没有实现,就去实现主类,然后对分类进行attach操作
2.2 主类非懒加载
2.2.1、分类懒加载
主类非懒加载,所以在read_images
里面会 调用realizeClassWithoutSwift
对主类进行实现. 我们在这个方法里面去断点打印
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
ro = (const class_ro_t *)cls->data(); // 从cls中拿到data信息,进过处理后 赋值给 rw的ro
if (ro->flags & RO_FUTURE) {
....
} else {
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
}
⬅️ //在这个位置断点
....
// Attach categories
methodizeClass(cls, previously);
return cls;
}
通过lldb
打印ro信息
(lldb) p *ro
(const class_ro_t) $15 = {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000
name = 0x0000000100000ed1 "JEPerson"
baseMethodList = 0x0000000100002028
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $15.baseMethodList
(method_list_t *const) $16 = 0x0000000100002028
(lldb) p *$16
(method_list_t) $17 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 2 // 有2个方法
first = { // 第一个方法信息
name = "personCateMethod" // 这是在分类里定义实例方法
types = 0x0000000100000f2f "v16@0:8"
imp = 0x0000000100000d20 (KCObjcTest`-[JEPerson(JECate) personCateMethod] at JEPerson+JECate.m:17)
}
}
}
(lldb) p $17.getOrEnd(1) // 获取第二个方法信息 下标为1
(method_t) $18 = {
name = "method1" // 这是在主类里定义的实例方法
types = 0x0000000100000f2f "v16@0:8"
imp = 0x0000000100000e50 (KCObjcTest`-[JEPerson method1])
}
从以上打印信息中可以看出,
分类的信息 在编译时就已经添加到了data信息里面
2.2.2、分类非懒加载
在read_images
里面 先加载分类,主类还没有实现,所以通过objc::unattachedCategories.addForClass(lc, cls);
存在哈希表里
再加载主类,对主类进行实现realizeClassWithoutSwift()
,在这用2.2.1打印ro的步骤,打印出ro的相关信息
(lldb) p *$1
(method_list_t) $2 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 1 //方法只有一个
first = {
name = "method1"
types = 0x0000000100000f1f "v16@0:8"
imp = 0x0000000100000e40 (KCObjcTest`-[JEPerson method1])
}
}
}
从打印结果看,方法只有一个,是在主类里定义的方法,分类里的方法并没有。其通过methodizeClass() -> objc::unattachedCategories.attachToClass() -> attachCategories
进行信息绑定
3、 分类属性
3.1、 为分类添加属性
⚠️:属性和实例对象不是同一个东西
正常情况,我们为类添加属性的写法:
@property (nonatomic, assign) NSInteger age;
由于系统会自动生成get、set方法,我们可以直接通过get、set方法进行访问,但是对于分类(类别),如果直接用get、set方法访问,比如:
//Student 的分类
@interface Student (JE)
@property (nonatomic, assign) NSInteger age;
@end
//Student中调用
- (instancetype)init
{
if (self = [super init]) {
self.age = 10;
}
return self;
}
//就会出现这个错误:
[Student setAge:]: unrecognized selector sent to instance 0x600003ea8250
经典的错误: unrecognized selector 找不到方法,这是因为系统并没有为我们生成其get、set方法,如何正确的添加属性?----通过关联对象的方式
:
// static const char je_age_cate;
@property (nonatomic, assign) NSInteger age;
- (void)setAge:(NSInteger)age
{
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 因为写入的值是@(age) NSNumber类型,所以需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC缓存策略
// objc_setAssociatedObject(self, &je_age_cate, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSInteger)age
{
return [objc_getAssociatedObject(self, @selector(age)) integerValue];
// return [objc_getAssociatedObject(self, &je_age_cate) integerValue];
}
3.2、 分类属性结构
- 分类的属性存储在哪?
我们知道,在类加载的时候,系统就会为类对象分配其所需的大小,后续并不能添加,那么用分类的方式为类添加属性之后,这个属性在哪里?
答案是在:AssociationHashMap 。 其由AssociationsManager管理。所有的分类属性都在这里。
//以:objc_setAssociatedObject()为入口
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
.....
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
.....
}
....
}
//查看AssociationsManager结构
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
public:
......
};
- (void)setAge:(NSInteger)age
{
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
以这个结构为例,可以解析为:

AssociationPolicy和value封装成ObjcAssociation的结构,然后和key建立的映射关系构成ObjcAssociationMap,再对应由object的地址通过DISGUISE函数返回值生成的键存储在AssociationHashMap中

3.3、分类属性释放
正常的类的属性是在dealloc中,向其发送release信号,当引用值为0的时候,其被置空、释放,那么对于关联对象的释放逻辑是怎么样的?还是从dealloc的源码中来看:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this); // <--- 这里是对关联对象进行释放
}
}
void *objc_destructInstance(id obj)
{
if (obj) {
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj); // <----- 如果是关联对象,进行remove操作
obj->clearDeallocating();
}
return obj;
}
简单来说:在dealloc的时候,先进行release操作,如果有关联对象,进行_object_remove_assocations操作
3.4、分类的方法
分类的方法存储在哪? ------------在类对象/元类对象中。
从源码中来看:
_objc_init () -> map_images -> map_images_nolock() -> _read_images()
在 _read_images() 里有一段
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
....
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA()); //< 再进这个方法
}
}
remethodizeClass() -> attachCategories()
//将cls和category连接起来
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
....
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
....
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
// 该类的方法列表个数
uint32_t oldCount = array()->count;
// 新数组的个数
uint32_t newCount = oldCount + addedCount;
// 为新数组扩容
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// 将原来的元素移动到扩容后数组的尾部
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// 把addedLists中的成员放在array的前面
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
....
}
由此可见:
1、分类的方法会整合到类对象的方法列表中。
2、如果categorty和类本身有相同的方法,那么在类对象的方法列表中 有2个相同的方法名,而不是把原本的方法替换掉。
3、新方法会加在旧方法的前面,这样在调用的时候,从前往后找,找到第一个,就不再找了
用另外一种方法来验证:------ 直接打印方法列表的方法名
//Student.h
- (void)categoryMethod
{
NSLog(@"student categoryMethod %p",__FUNCTION__);
}
// Student+JE.h
- (void)categoryMethod
{
NSLog(@"categoryMethod %p",__FUNCTION__);
}
//ViewController.m
Student *s = [Student new];
[self methodInfo:s];
- (void)methodInfo:(id)obj
{
Class class = [obj class];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(class, &methodCount);
for (NSInteger i = 0; i < methodCount; i ++) {
Method method = methodList[i];
SEL methodName = method_getName(method);
NSLog(@"方法名:%@", NSStringFromSelector(methodName));
}
}
// 打印结果
方法名:categoryMethod
方法名:categoryMethod
从打印结果看,同一个方法名,打印了2次,说明分类的方法并没有覆盖类对象本身的方法
用同样的方法,可以验证,分类的方法是在类对象方法的前面的
(方法列表中,分类的方法下标小)。
关联对象key的4种写法

4、分类(category)和拓展类(extension)
拓展类也叫匿名分类
4.1、拓展的形式
这2个东西其实很好区分,之所以拿出来说一下,是因为我在深入学习分类之前,一直对这2个名字搞混。
- 分类:
其形式为Class+Category.h
、Class+Category.m
2个文件组成
//比如Student的分类 Student+JE
//.h文件
@interface Student (JE)
@end
//.m文件
@implementation Student (JE)
@end
- 拓展类
其形式为Class+ Extension.h
,只有 1个文件
// 比如Student的拓展类 Student+Extent
@interface Student ()
@end
但是在一般开发中,单独定义给一个拓展类是比较少的,而是再接在类中写
//比如Student类 其继承Person
#import "Person.h"
@interface Student () //这2行就是为Student添加了拓展
@end
@interface Student : Person
@end
这种写法就很熟悉了,在开发中经常会这样写
4.2、 拓展的加载
拓展类的信息作用在编译时
、在编译时信息就被编译到了data的ro里面。
这也侧面说明了 拓展类的变量存储在类的结构里,编译之后,ro数据不能再操作(只读属性),所以分类无法添加成员变量,之前也说过另外一个原因,就是类编译之后,系统在 alloc的时候,就为类分类了固定的大小,无法在动态去改变。
4.3、 分类与拓展类的区别
-
分类和扩展有什么区别?
1、分类多用于扩展方法实现,类扩展多用于申明私有变量和方法。
2、类扩展作用在编译期,直接和原类在一起,而分类作用在运行时,加载类的时候动态添加到原类中。
3、类扩展可以定义属性,系统自动生成get、set、实例变量。分类中定义的属性需要手动添加get、set进行关联对象。 -
分类有哪些局限性?
1.分类只能给现有的类加方法或协议,不能添加实例变量(ivar)。
2.分类添加的方法如果与现有的重名,会覆盖原有方法的实现(⚠️假覆盖,事实上2个方法都在,并没有消失)。如果多个分类方法都重名,则根据编译顺序执行最后一个。
网友评论