0x00 Category 实现原理
编译之后每个 Category 文件都会生成一个 struct _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;
};
看下面源码可以知道( while i-- )
, Category 是按照编译顺序倒序
加入 method_list
内
// Category 主要部分源码
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
所以
- 通过 Runtime 加载某各类的所有 Category 数据
- 把所有内容(属性方法协议等)放入一个大的数组中(倒序排列)
- 合并后的数据插入到原始数据的前面, 所以并不是 Category 内实现的方法覆盖了原方法
0x01 Category 与 Extension 区别
- Extension 是在编译的时候数据就和类的信息放在一起
- Category 是在运行时才将数据合并在一起
0x02 Category 中的 +load 方法
- Category 的 load 方法什么时候调用?
- load 方法是在运行时加载类和分类的时候调用, 父类会优先调用
- 调用顺序 (看下面的源码)
- 先调用类的 load 方法, 先按照编译顺序, 然后按照先父类, 后子类
- 再调用Category 的 load 方法, 按照编译顺序
- 为什么 load 方法会被全部调用, 而其他的方法只会调用最前面的?
通过源码可以知道因为 load 方法是系统通过函数地址调用, 所有都调了一遍, 而其他方法是手动通过消息发送机制调用, 故只调用了最前面的, 手动 load 也是消息机制也只会调用最前面的 load
/* prepare_load_methods 这里会准备好执行 load 方法的类的顺序 */
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
/* 递归将类和父类添加入数组, 优先加入的是父类, 而选择类的顺序为编译顺序(即调整编译顺序 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;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
/* while (loadable_classes_used > 0) { call_class_loads(); }
* 上面的 while 就是说存在可用的类时就会调用类的 load 方法 (call_class_loads), 没有可用的类的时候就会调用类别的方法(call_category_loads) */
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 *pool = objc_autoreleasePoolPush();
do {
// 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();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
0x03 About Initialize
- 类第一次接收到消息的时候调用
- 调用子类的, 会先调用父类的
- 如果子类没有实现, 就会调用父类的, 所以会出现多次调用
- 如果 Category 实现了, 就会调用 Category 的
// 递归去做父类的初始化
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
// 实际就是消息发送
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
0x04 为 Category 添加属性
Category 不能添加成员变量, 但是可以间接实现添加属性
主要是用 Runtime 的对象关联手段, Runtime 中有个 AssociationsManager 类做管理, 其中有全局的 hashmap 负责收藏属性的值
这个 hashmap 是两层的就比如下面的例子在实际的 hashmap 中是类似这样的{ &per : { name 的 key : name 的 value } }
0x05 我的测试代码
使用 Command Line Tool 创建测试就好
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
void _aboutCategory(void);
void _about_load(void);
void _about_initialize(void);
void _about_instance_ivar(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// _aboutCategory();
// _about_load();
// _about_initialize();
_about_instance_ivar();
}
return 0;
}
void printMethodNamesOfClass(Class cls) {
// 定义
unsigned int outCount = 0;
Method *methodList = NULL;
Method tempMethod = NULL;
NSMutableString *strResult = [NSMutableString string];
NSString *strTemp = [NSString string];
// copy 出方法列表
methodList = class_copyMethodList(cls, &outCount);
// 遍历
for (int i = 0; i < outCount; ++i) {
tempMethod = methodList[i];
strTemp = NSStringFromSelector(method_getName(tempMethod));
[strResult appendFormat:@"\n%@", strTemp];
}
NSLog(@"%@", strResult);
}
@interface Person : NSObject
- (void)walk;
@end
@implementation Person
+ (void)initialize {
NSLog(@"+initialize Person");
}
+ (void)load {
NSLog(@"+load Person");
}
- (void)walk {
NSLog(@"any person can walk");
}
@end
@interface Person (Run)
- (void)run;
@end
@implementation Person (Run)
- (void)run {
NSLog(@"any person can run");
}
@end
@interface Person (Eat)
- (void)eat;
@end
@implementation Person (Eat)
- (void)eat {
NSLog(@"any person can eat");
}
@end
/**
* Category 与 Extension 区别在于:
* Extension 是在编译的时候数据就和类的信息放在一起
* Category 是在运行时才将数据合并在一起
*/
void _aboutCategory() {
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;
};
/**
* 编译之后每个 Category 文件都会生成一个 struct _category_t 的结构体
* 看下面源码可以知道( while i-- ), Category 是按照编译顺序倒序加入 method_list 内
* 实现原理如下:
* 1. 通过 Runtime 加载某各类的所有 Category 数据
* 2. 把所有内容(属性方法协议等)放入一个大的数组中(倒序排列)
* 3. 合并后的数据插入到原始数据的前面, 所以并不是 Category 内实现的方法覆盖了原方法
*/
Person *person = Person.alloc.init;
[person walk];
[person eat];
[person run];
printMethodNamesOfClass(person.class);
}
// Category 主要部分源码
// Count backwards through cats to get newest categories first
//int mcount = 0;
//int propcount = 0;
//int protocount = 0;
//int i = cats->count;
//bool fromBundle = NO;
//while (i--) {
// auto& entry = cats->list[i];
//
// method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
// if (mlist) {
// mlists[mcount++] = mlist;
// fromBundle |= entry.hi->isBundle();
// }
//
// property_list_t *proplist =
// entry.cat->propertiesForMeta(isMeta, entry.hi);
// if (proplist) {
// proplists[propcount++] = proplist;
// }
//
// protocol_list_t *protolist = entry.cat->protocols;
// if (protolist) {
// protolists[protocount++] = protolist;
// }
//}
@interface Student : Person
+ (void)test;
@end
@implementation Student
//+ (void)initialize {
// NSLog(@"+initialize Student");
//}
+ (void)load {
NSLog(@"+load Student");
}
+ (void)test {
NSLog(@"+test");
}
@end
@interface Student (Run)
@end
@implementation Student (Run)
//+ (void)initialize {
// NSLog(@"+initialize Run");
//}
+ (void)load {
NSLog(@"+load Run");
}
+ (void)test {
NSLog(@"+test Run");
}
@end
@interface Student (Eat)
@end
@implementation Student (Eat)
//+ (void)initialize {
// NSLog(@"+initialize Eat");
//}
+ (void)load {
NSLog(@"+load Eat");
}
+ (void)test {
NSLog(@"+test Eat");
}
@end
/** 下面注释中的是相关部分源码
* 1. Category 的 load 方法什么时候调用?
* load 方法是在运行时加载类和分类的时候调用, 父类会优先调用
*
* 2. 调用顺序 (看下面的源码)
* 先调用类的 load 方法, 先按照编译顺序, 然后按照先父类, 后子类
* 再调用Category 的 load 方法, 按照编译顺序
*
* 3. 为什么 load 方法会被全部调用, 而其他的方法只会调用最前面的?
* 通过源码可以知道因为 load 方法是系统通过函数地址调用, 所有都调了一遍, 而其他方法是手动通过消息发送机制调用, 故只调用了最前面的, 手动 load 也是消息机制也只会调用最前面的 load
*/
void _about_load() {
[Student test];
Student *std = Student.alloc.init;
printMethodNamesOfClass(object_getClass(std.class));
}
/* prepare_load_methods 这里会准备好执行 load 方法的类的顺序
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
*/
/* 递归将类和父类添加入数组, 优先加入的是父类, 而选择类的顺序为编译顺序(即调整编译顺序 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;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
*/
/* while (loadable_classes_used > 0) { call_class_loads(); }
* 上面的 while 就是说存在可用的类时就会调用类的 load 方法 (call_class_loads), 没有可用的类的时候就会调用类别的方法(call_category_loads )
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 *pool = objc_autoreleasePoolPush();
do {
// 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();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
*/
/** initialize
* 1. 类第一次接收到消息的时候调用
* 2. 调用子类的, 会先调用父类的
* 3. 如果子类没有实现, 就会调用父类的, 所以会出现多次调用
* 4. 如果 Category 实现了, 就会调用 Category 的
*/
void _about_initialize(void) {
// [Student alloc] 不调用就不执行
[Student alloc];
}
@interface Person (Name)
//{ NSString * _name } 成员变量无法添加的
@property (nonatomic, copy) NSString *name;
@end
@implementation Person (Name)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, _cmd);
}
@end
/**
* Category 不能添加成员变量, 但是可以间接实现添加属性
* 主要是用 Runtime 的对象关联手段, Runtime 中有个 AssociationsManager 类做管理, 其中有全局的 hashmap 负责收藏属性的值
* 这个 hashmap 是两层的就比如下面的例子在实际的 hashmap 中是类似这样的 { &per : { name 的 key : name 的 value } }
*/
void _about_instance_ivar(void) {
Person *per = Person.alloc.init;
per.name = @"lily";
NSLog(@"%@", per.name);
per.name = @"lily_2";
NSLog(@"%@", per.name);
}
网友评论