在日常开发中经常会使用到分类,对分类的了解也仅限于使用。但是对它的底层实现还不是那么了解,本文主要根据源码探索分类。
分类的介绍
Category是Objective-C 2.0之后添加的语言特性,分类、类别其实都是指的Category。Category的主要作用是为已经存在的类添加方法。
Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
分类的使用
- 声明私有方法
- 分解体积庞大的类文件
- 把Framework私有方法公开
- 模拟多继承(另外可以模拟多继承的还有protocol)
分类的特点
- 1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。
原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性
; - 2.分类中的可以写@property, 但不会生成
setter/getter
方法, 也不会生成实现以及私有的成员变量,会编译通过,但是引用变量会报错; - 3.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为
分类 > 本类 > 父类
; - 4.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。
- 5.运行时决议
- 6.同名分类方法生效取决于编译顺序
- 7.名字相同的分类会引起编译报错
分类的本质
使用clang 编译.m文件得到如下代码所示,可以得到.cpp的内容。
clang -rewrite-objc main.m -o main.cpp
编译后的.cpp部分内容如下所示。说明分类是一个_category_t
类型的结构体。
static struct /_method_list_t/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_good __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"studyMusic", "v16@0:8", (void *)_I_NSObject_good_studyMusic}}
};
static struct /_method_list_t/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_good __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"studyEnglish", "v16@0:8", (void*)_C_NSObject_good_studyEnglish}}
};
static struct /_prop_list_t/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_NSObject_$_good **__attribute__** ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"goodSize","T@\"NSString\",&,N"}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_NSObject;
static struct _category_t _OBJC_$_CATEGORY_NSObject_$_good __attribute__*((used, section ("__DATA,__objc_const"))) =
{
"NSObject",
0, // &OBJC_CLASS_$_NSObject,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_good,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_good,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_good,
};
在objc源码中查看分类:
struct category_t {
const char *name;//名称
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;//实例方法列表
WrappedPtr<method_list_t, PtrauthStrip> classMethods;//类方法列表
struct protocol_list_t *protocols;//协议方法
struct property_list_t *instanceProperties;//对象属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;//类属性
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
通过编译的中间层代码.cpp文件于objc4底层源码比较的到结论:
name
代表的是分类
名字。分类不区分类方法于实例方法
,本质类加载过程分类属于插入数据,实例方法插入到类中,类方法插入到元类中,所以自己并不会有分元类。- 分类的属性并
不会自动生成getter、setter方法
,这在.cpp文件中可以得知
分类的加载流程
分类的加载方法load_categories_nolock
根据注释可以看出实现分类的加载主要是由 attachCategories
和 attachToClass
实现
static void load_categories_nolock(header_info *hi) {
{
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
size_t count;
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, hi};
if (!cls) {
continue;
}
// Process this category.
if (cls->isStubClass()) {
// 类在初始化之前不知道它们的元类,
// 所以我们必须向类本身添加带有类方法或属性的类别。
// methodizeClass()将找到它们,并根据需要将它们添加到元类中。
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
//注册分类到类信息中。
//如果类已实现,则重建该类的方法列表
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};
processCatlist(hi->catlist(&count));
processCatlist(hi->catlist2(&count));
}
attachCategories
将方法列表、属性和协议从类别附加到类。 假设cats中的categories都加载了,并按加载顺序排序,最旧的类别在先。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/**
* 只有少数班级在发布时有超过 64 个类别。
* 这使用了一点堆栈,并避免了 malloc。
* 类别必须以正确的顺序添加,即从后到前。为了通过分块来做到这一点,我们迭代cats_list
* 从前到后,向后构建本地缓冲区,并在块上调用 attachLists。 attachLists 将列表放在前面,因此最终结果按预期顺序排列。
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
、
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "FFPerson") == 0)
{
if (!isMeta) {
printf("%s -FFPerson....\n",__func__);
}
}
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
//获取数据
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
//方法的排序
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
//倒叙插入
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
//插入属性
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
//插入协议
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
常见问题
Q:分类的对象方法
,类方法
都存在哪里?
一个类的所有分类的 对象方法
放在类对象中,所有分类的类方法
存放在元类中
Q:分类的方法是如何添加到类对象方法列表中的?
大概流程
-
1.通过Runtime加载某个类的所有Category数据
-
2.把所有Category的方法、属性、协议数据,合并到一个大数组中
-
3.后面参与编译的Category数据,会在数组的前面
-
4.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
Q:Category的实现原理
- Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
Q:Category和Class Extension的区别是什么?
- Class Extension在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中
Q:+load方法调用顺序?
1. 先调用类的+load方法
- 1.1按照编译先后顺序调用(先编译,先调用)
- 1.2先调用父类的+load再调用子类的+load
2. 再调用分类的+load方法
- 2.1按照编译先后顺序调用(先编译,先调用)
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
- 每个类、分类的+load,在程序运行过程中只调用一次,只有在加载类时候调用一次
- 不存在分类的+load方法覆盖类的+load方法
Q:load、initialize的调用顺序?
1.load
- 1> 先调用类的load
a) 先编译的类,优先调用load
b) 调用子类的load之前,会先调用父类的load - 2> 再调用分类的load
a) 先编译的分类,优先调用load
2.initialize
1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的initialize方法)