面试题引发的思考:
Q: Category中load
方法是什么时候调用的?load
方法能继承吗?
-
load
方法在runtime加载类、分类的时候调用; -
load
方法可以继承,但是一般情况下不会主动去调用load
方法,都是让系统自动调用。
Q: load
、initialize
方法的区别什么?它们在Category中的调用的顺序?以及出现继承时两者之间的调用过程?
- 答案见文章分析。
1. Category中load
方法探究:
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
+ (void)test;
@end
@implementation Person
+ (void)load {
NSLog(@"Person + load");
}
+ (void)test {
NSLog(@"Person + test");
}
@end
// TODO: ----------------- Person+Category1类 -----------------
@implementation Person (Category1)
+ (void)load {
NSLog(@"Person (Category1) + load");
}
+ (void)test {
NSLog(@"Person (Category1) + test");
}
@end
// TODO: ----------------- Person+Category2类 -----------------
@implementation Person (Category2)
+ (void)load {
NSLog(@"Person (Category2) + load");
}
+ (void)test {
NSLog(@"Person (Category2) + test");
}
@end
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// [Person test];
}
代码如上:
定义一个Person
类、定义两个Person
的分类,然后分别实现load
方法、test
方法,不做任何操作,直接运行程序打印结果:

发现类和分类的load
方法被调用,这是因为:
load
方法在runtime加载类、分类的时候调用。
接着放开[Person test]
语句,调用test
方法,运行程序打印结果:

查看编译顺序:

- 分类重写类的方法会优先调用,类的方法存在最后的内存中,所以调用顺序优先按照分类的编译顺序逆序排列。结论跟打印结果一致。
Q: 那么load
方法的是如何实现调用的呢?调用顺序又是怎样的呢?
(1) load
方法调用原理:
接下来根据OC源码进行分析,首先找到runtime初始化函数_objc_init
:

然后通过load_images
读取模块找到load_images
函数:

然后通过call_load_methods
读找到call_load_methods
函数:

源码显示:
优先调用类的load
方法,再调用分类的load
方法。
然后通过call_class_loads
找到call_class_loads
函数:

源码显示:
call_class_loads
函数直接取出类里面的方法,返回方法的地址,通过地址直接调用load
方法。
同理通过call_category_loads
找到call_category_loads
函数:

源码显示:
call_category_loads
函数直接取出分类里面的方法,返回方法的地址,通过地址直接调用load
方法;
分类中load
方法的调用顺序是按照编译顺序正序调用的。
Q: test
方法调用方式与load
方法调用方式有何不同?
load
方法调用方式是通过地址直接调用;test
方法调用方式是通过消息发送机制进行调用。
(2) load
方法调用顺序原理:
以上分析可知:
load_images
函数在执行call_load_methods();
语句之前,有一个prepare_load_methods
函数为load
函数处理:

然后通过prepare_load_methods
找到prepare_load_methods
函数:

发现优先对类进行处理,分类直接按顺序存储。
然后通过schedule_class_load
找到schedule_class_load
函数,看看需要对类做什么样的处理:

发现首先对schedule_class_load
函数进行递归操作,先获取父类存储,然后获取子类存储。
通过以上分析可知:
load
方法会在runtime加载类、分类时调用;- 每个类、分类的
load
方法在程序运行过程中只调用一次
load
方法的调用顺序:
- 先调用类的
load
方法
a> 按照编译的先后顺序调用
b> 调用子类的load
方法之前会先调用父类的load
方法- 再调用分类的
load
方法
a> 按照编译的先后顺序调用
接下来通过代码验证一下:
// TODO: ----------------- Person类 -----------------
@implementation Person
+ (void)load {
NSLog(@"Person + load");
}
@end
// TODO: ----------------- Person (Category1)类 -----------------
@implementation Person (Category1)
+ (void)load {
NSLog(@"Person (Category1) + load");
}
@end
// TODO: ----------------- Person (Category2)类 -----------------
@implementation Person (Category2)
+ (void)load {
NSLog(@"Person (Category2) + load");
}
@end
// TODO: ----------------- Student类,继承Person类 -----------------
@implementation Student
+ (void)load {
NSLog(@"Student + load");
}
@end
// TODO: ----------------- Student (Category1)类 -----------------
@implementation Student (Category1)
+ (void)load {
NSLog(@"Student (Category1) + load");
}
@end
// TODO: ----------------- Student (Category2)类 -----------------
@implementation Student (Category2)
+ (void)load {
NSLog(@"Student (Category2) + load");
}
@end
// TODO: ----------------- Animal类,独立的类 -----------------
@implementation Animal
+ (void)load {
NSLog(@"Animal + load");
}
@end

分析以上:
- 首先调用类的
load
方法,按照编译的先后顺序调用,则先调用的是Student
类,但是调用子类的load
方法之前会先调用父类的load
方法,所以先调用Person
类,再调用Student
类,最后调用Animal
类; - 再调用分类的
load
方法,按照编译的先后顺序调用,所以调用顺序为:Student (Category1)
、Student (Category2)
、Person (Category1)
、Person (Category2)
。
打印结果如下:

打印结果与分析结果一致,结论可证。
2. Category中initialize
方法探究:
// TODO: ----------------- Person类 -----------------
@implementation Person
+ (void)initialize {
NSLog(@"Person + initialize");
}
@end
// TODO: ----------------- Person+Category1类 -----------------
@implementation Person (Category1)
+ (void)initialize {
NSLog(@"Person (Category1) + initialize");
}
@end
// TODO: ----------------- Person+Category2类 -----------------
@implementation Person (Category2)
+ (void)initialize {
NSLog(@"Person (Category2) + initialize");
}
@end
// TODO: ----------------- Student类,继承Person类 -----------------
@implementation Student
+ (void)initialize {
NSLog(@"Student + initialize");
}
@end
// TODO: ----------------- Student (Category1)类 -----------------
@implementation Student (Category1)
+ (void)initialize {
NSLog(@"Student (Category1) + initialize");
}
@end
// TODO: ----------------- Student (Category2)类 -----------------
@implementation Student (Category2)
+ (void)initialize {
NSLog(@"Student (Category2) + initialize");
}
@end
// TODO: ----------------- Teacher类,继承Person类 -----------------
@implementation Teacher
@end
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[Teacher alloc];
// objc_msgSend([Person class], @selector(alloc));
// objc_msgSend([Teacher class], @selector(alloc));
[Student alloc];
// objc_msgSend([Person class], @selector(alloc));
// objc_msgSend([Student class], @selector(alloc));
[Person alloc];
// objc_msgSend([Person class], @selector(alloc));
}
代码如上:
定义一个Person
类、两个Person
的分类,再定义Person
的子类Student
、两个Student
的分类,然后分别实现initialize
方法,最后定义Person
的子类Teacher
;不做任何操作,直接运行程序则没有打印。
接着放开[Teacher alloc];
、[Student alloc];
、[Person alloc];
语句,运行程序打印结果:

查看编译顺序:

通过以上代码分析即编译顺序可知:
-
第一句打印的是
Person (Category2)
类的initialize
方法,代码执行的是[Teacher alloc];
语句。
说明[Teacher alloc];
语句会先调用父类Person
的initialize
方法; -
第二句打印的是
Person (Category2)
类的initialize
方法,代码执行的是[Student alloc];
语句。
说明[Student alloc];
语句会先调用父类Person
的initialize
方法; -
第三句打印的是
Student (Category2)
类的initialize
方法。
说明[Student alloc];
语句会先调用父类Person
的initialize
方法,再调用自己Student
的initialize
方法; -
第四句没有打印。
说明[Person alloc];
语句调用自己的initialize
方法时,前面已经初始化过了;说明每个类只会初始化一次。
通过以上分析可得:
initialize
方法在类第一次接收消息时调用;- 优先调用父类的
initialize
方法,再调用子类的initialize
方法;
(先初始化父类,再初始化子类,每个类只初始化一次)
如果分类没有实现initialize
方法,则会调用父类的initialize
方法
如果分类实现了initialize
方法,就会覆盖类本身的initialize
方法
(1) initialize
方法调用原理:
根据OC源码进行分析,首先找到runtime函数class_getClassMethod
:

然后通过class_getInstanceMethod
读找到class_getInstanceMethod
函数:

然后通过lookUpImpOrNil
读找到lookUpImpOrNil
函数:

然后通过lookUpImpOrForward
读找到lookUpImpOrForward
函数:

源码显示:
- 传入的类需要初始化且没有初始化,进行初始化操作,否则跳过;
- 只有在第一次接收消息的时候调用
initialize
方法。
然后通过_class_initialize
读找到_class_initialize
函数:

源码显示:
- 如果传入的类存在父类且父类没有初始化,那么先初始化父类;
- 然后再初始化自己,实现
initialize
方法。
3. Category中load
方法、initialize
方法比较:
Q:
load
方法、initialize
方法的区别是什么?以及出现继承时两者之间的调用过程?
- 调用方式:
load
是根据函数地址直接调用;initialize
是通过objc_msgSend
调用。
- 调用时刻:
load
是runtime加载类、分类的时候调用(只会调用一次);initialize
是类第一次接收到消息的时候调用,每个类只会调用initialize
一次(父类的initialize
可能调用多次)。
- 调用顺序:
load
- 先调用类的
load
方法;
a> 按照编译的先后顺序调用;
b> 调用子类的load
方法之前会先调用父类的load
方法;- 再调用分类的
load
方法。
a> 按照编译的先后顺序调用initialize
- 先初始化父类;
- 再初始化子类(可能最终调用的是父类的
initialize
方法)。
网友评论