Category的本质
Category的底层结构
1.我们先给Person
增加一个Person+Eat
的分类
@interface Person (Eat) <NSCopying, NSCoding>
- (void)eat;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
@end
@implementation Person (Eat)
- (void)eat
{
NSLog(@"eat");
}
- (void)eat1
{
NSLog(@"eat1");
}
+ (void)eat2
{
}
+ (void)eat3
{
}
@end
2.然后通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Eat.m
转换成Person+Eat.cpp文件
,发现内部会生成一个_category_t
类型的结构体
struct _category_t {
const char *name; // 类名
struct _class_t *cls;
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; // 属性列表
};
3.我们还发现会生成一个_category_t
结构体类型的变量,这个变量对应着该分类文件是Person+Eat
,并且里面记录着所有的分类信息
// 变量名对应着分类文件名
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person", // 类名
0, // cls
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat, // 对象方法
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat, // 类方法
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat, // 协议列表
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat, // 属性列表
};
4._OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat
这个变量里面记录着分类的对象方法eat
和eat1
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Eat_eat},
{(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_Person_Eat_eat1}}
};
5._OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat
这个变量里面记录着分类的类方法eat2
和eat3
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat2", "v16@0:8", (void *)_C_Person_Eat_eat2},
{(struct objc_selector *)"eat3", "v16@0:8", (void *)_C_Person_Eat_eat3}}
};
6. _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat
这个变量里面记录着NSCopying
和NSCoding
两个协议
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};
7._OBJC_$_PROP_LIST_Person_$_Eat
这个变量里面记录着属性weight
和height
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"weight","Ti,N"},
{"height","Td,N"}}
// Ti,N和Td,N对应着int和double两个类型
};
Category的加载处理过程
1.通过分析查找到objc-rumtime-new.mm
文件里的attachCategories
函数,将分类文件里的数据信息都附加到对应的类对象或者元类对象里,详细代码如下
// 附加上分类的核心操作
// cls:类对象或者元类对象,cats_list:分类列表
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)" : "");
}
// 先分配固定内存空间来存放方法列表、属性列表和协议列表
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();
for (uint32_t i = 0; i < cats_count; i++) {
// 取出某个分类
auto& entry = cats_list[i];
// entry.cat就是category_t *cat
// 根据isMeta属性取出每一个分类的类方法列表或者对象方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
// 如果有方法则添加mlist数组到mlists这个大的方法数组中
// mlists是一个二维数组:[[method_t, method_t, ....], [method_t, method_t, ....]]
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
// 将分类列表里先取出来的分类方法列表放到大数组mlists的最后面(ATTACH_BUFSIZ - ++mcount),所以最后编译的分类方法列表会放在整个方法列表大数组的最前面
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 同上面一样取出的是分类中的属性列表proplist加到大数组proplists中
// proplists是一个二维数组:[[property_t, property_t, ....], [property_t, property_t, ....]]
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;
}
// 同上面一样取出的是分类中的协议列表protolist加到大数组protolists中
// protolists是一个二维数组:[[protocol_ref_t, protocol_ref_t, ....], [protocol_ref_t, protocol_ref_t, ....]]
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){
return !c->cache.isConstantOptimizedCache();
});
}
}
// 将分类的所有属性附加到类对象的属性列表中
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
// 将分类的所有协议附加到类对象的协议列表中
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
2.上述的每步操作都会调用attachLists
方法来进行元素分配,详细代码如下
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// 获取原本的个数
uint32_t oldCount = array()->count;
// 最新的个数 = 原本的个数 + 新添加的个数
uint32_t newCount = oldCount + addedCount;
// 重新分配内存
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
// 将新数组的个数改为最新的个数
newArray->count = newCount;
// 将旧数组的个数改为最新的个数
array()->count = newCount;
// 递减遍历,将旧数组里的元素从后往前的依次放到新数组里
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
// 将新增加的元素从前往后的依次放到新数组里
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
// 释放旧数组数据
free(array());
// 赋值新数组数据
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else { .... }
}
总结
- 编译时
- 每一个
Category
都会生成一个_category_t
结构体对象,记录着所有的属性、方法和协议信息
- 每一个
- 运行时
- 通过
Runtime
加载某个类的所有Category
数据 - 把所有
Category
的对象方法、类方法、属性、协议数据,分别合并到一个二维数组中,并且后面参与编译的Category
数据,会在数组的前面 - 将合并后的
Category
数据(方法、属性、协议),插入到类原来数据的前面
- 通过
面试题
1.如果几个分类中都有同样的方法,会调用哪个,调用顺序是什么
- 有分类会先调用分类的方法,如果多个分类都有相同的方法,那么会根据编译顺序来决定执行哪个分类的方法,后参与编译的分类方法会放到整个方法列表数组的最前面,到时调用会遍历所有的方法列表数组,先找到的分类方法列表先执行。
- 在Xcode中查看编译顺序:
Build Phases->Compile Sources
- 分类里面相同的方法会覆盖类原本的方法这种说法是错误的,根本就没有覆盖,只是最先遍历找到哪个就执行哪个,不存在覆盖的概念
2. Class Extension和Category的实现是一样的吗
- 不一样。
- 类扩展只是将.h文件中的声明放到.m中作为私有来使用,编译时就已经合并到该类中了。
- 分类中的声明都是公开的,而且是利用运行时机制在程序运行时将分类里的数据合并到类中
3.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- 有
load方法
-
load方法
在Runtime
加载类、分类的时候调用,而且只调用一次 -
load方法
可以继承,但是一般情况下不会主动去调用load方法
,都是让系统自动调用
4.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
load方法
-
load方法
会在Runtime
加载类、分类时调用- 每个类、分类的load
,在程序运行过程中只调用一次
源码分析
1.在objc-runtime-new.mm
中load_images
方法可以发现准备处理load方法和调用load方法的函数
void load_images(const char *path __unused, const struct mach_header *mh) {
....
{
mutex_locker_t lock2(runtimeLock);
// 准备load方法
prepare_load_methods((const headerType *)mh);
}
// 调用load方法
call_load_methods();
}
2.在prepare_load_methods
中发现,调用load方法
前的类的处理和分类的处理
void prepare_load_methods(const headerType *mhdr) {
size_t count, i;
runtimeLock.assertLocked();
// 按编译顺序拿到所有的类的list
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
// 按添加进数组的顺序遍历处理类的列表
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
// 按编译顺序拿到所有的分类列表
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
....
// 按顺序直接添加
add_category_to_loadable_list(cat);
}
}
3.递归调用schedule_class_load
方法来优先添加父类放到列表中,然后再添加当前类,所以执行调用时肯定先执行父类的load方法
static void schedule_class_load(Class cls) {
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// 递归调用,找到传进来的类的父类添加到列表中
schedule_class_load(cls->getSuperclass());
// 然后再调用当前传进来的类添加到列表中,所以父类肯定是在前面
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
4.在objc-loadmethod.mm
中call_load_methods
方法可以发现,程序运行时会先调用类的load方法
,然后调用分类的load方法
,详细代码如下
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *po ol = objc_autoreleasePoolPush();
do {
// 1. 优先调用类的load方法
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. 只调用一次分类的load方法
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
5.找到每个类的load方法
的内存地址,然后直接调用
static void call_class_loads(void) {
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 取出类里面的load方法
// load_method_t:指向函数地址的指针
// 这里的method对应的结构体为loadable_class
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 直接调用load方法
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
// 找到的method对应的结构体
struct loadable_class {
Class cls; // may be nil
IMP method; // 这个就是load方法
};
6.找到每个分类的load方法
的内存地址,然后直接调用
static bool call_category_loads(void) {
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
// 取出每一个分类里的load方法
// 这里的method对应的结构体为loadable_category
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
// 直接调用load方法
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
....
}
// 找到的method对应的结构体
struct loadable_category {
Category cat; // may be nil
IMP method; // 这个方法就是load方法
};
调用顺序
- 先调用类的
load
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的
load
之前会先调用父类的load
- 再调用分类的
load
- 按照编译先后顺序调用(先编译,先调用)
load方法系统调用和主动调用的区别
- 系统调用
load方法
调用是直接找到类和分类中的方法的内存地址直接调用 - 主动调用
load方法
是通过消息机制来发送消息的,会在对应的消息列表里按顺序遍历一层层查找,找到就调用
initialize方法
initialize方法
会在类第一次接收到消息时调用
源码分析
1.由于在调用到这个类的时候才会执行initialize方法
,那么说明是在发消息过程中来执行的,我们在objc-runtime-new.mm
中调用class_getInstanceMethod
或者class_getClassMethod
方法,详细代码如下
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
return _class_getMethod(cls, sel);
}
2.一步步调用最后找到initializeNonMetaClass
函数,先递归找到父类调用initialize
方法,然后当前类调用initialize
方法。可以在callInitialize
里发现本质都是通过Runtime的消息机制进行的发送
void initializeNonMetaClass(Class cls) {
ASSERT(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// 如果存在父类,并且没有初始化父类,就去初始化父类
supercls = cls->getSuperclass();
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
// 未初始化的类通过setInitializing做了标记,下次就不会再调用了
cls->setInitializing();
reallyInitialize = YES;
// Grab a copy of the will-initialize funcs with the lock held.
localWillInitializeFuncs.initFrom(willInitializeFuncs);
}
}
....
{
// 调用初始化
callInitialize(cls);
....
}
....
}
// 调用initialize的函数
void callInitialize(Class cls) {
// 通过runtime消息机制发送initialize消息
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
调用顺序
- 先调用父类的
initialize
,再调用子类的initialize
- (先初始化父类,再初始化子类,每个类只会初始化1次)
initialize和load的区别
-
initialize
是通过objc_msgSend
进行调用的,而load
是找到函数地址直接调用的 - 如果子类没有实现
initialize
,会调用父类的initialize
(所以父类的initialize
可能会被调用多次) - 如果分类实现了
initialize
,就覆盖类本身的initialize
调用
5.Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category
添加成员变量,但是可以间接实现Category
有成员变量的效果
实现方案
通过objc_setAssociatedObject
设置关联对象来实现
@interface Person (Test)
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) int weight;
@end
@implementation Person (Test)
- (void)setName:(NSString *)name
{
/*
* object:关联的对象
* value:关联的值
* objc_AssociationPolicy:关联策略
*/
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
// 隐式参数
// _cmd == @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
- (void)setWeight:(int)weight
{
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight
{
// _cmd == @selector(weight)
return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
实现源码分析
1.在objc-references.mm
中我们可以看到objc_setAssociatedObject
的实现代码如下
void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) {
if (!object && !value) return;
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// 通过传进来的object生成一个key
DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
// 取出AssociationsManager里的AssociationsHashMap这个属性
AssociationsHashMap &associations(manager.get());
// 如果value有值
if (value) {
// 根据传进来的object key的值disguised取出ObjectAssociationMap
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it‘s the first association we make */
isFirstAssociation = true;
}
// 根据refs_result的key存放进association
// association就是ObjcAssociation
// 总之就是对应的每一个key一层层的赋值
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else { // 如果value为空
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
// 也会找到associations进行擦除
associations.erase(refs_it);
}
}
}
}
}
....
}
2.关联对象的取值函数objc_getAssociatedObject
的实现代码如下
void _object_get_associative_reference(id object, const void *key) {
ObjcAssociation association{};
{
AssociationsManager manager;
// associations是manager里面所有的AssociationsHashMap的list
AssociationsHashMap &associations(manager.get());
// 根据object在list里找到对应的那个AssociationsHashMap类型的i
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
// 再取出所有的ObjectAssociationMap的list
ObjectAssociationMap &refs = i->second;
// 根据key在list里找到对应的ObjectAssociationMap类型的j
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
// 取出ObjectAssociation类型的值association
association = j->second;
// 取出association里的value和策略
association.retainReturnedValue();
}
}
}
return association.autoreleaseReturnedValue();
}
3.关联对象的几个类的实现关系可以用下图表示
总结:
- 关联对象并不是存储在被关联对象本身内存中
- 关联对象存储在全局的统一的一个
AssociationsManager
中 - 设置关联对象为
nil
,就相当于是移除关联对象 - 当
object
对象被释放,关联对象的值也会对应的从内存中移除(内存管理自动做了处理)