这是我参与8月更文挑战的第7天,活动详情查看: 8月更文挑战
load和initialize方法的调用原则和顺序?
load方法
load方法是在应用程序加载过程中,在main函数之前,也就是dyld加载过程中完成调用的;- 在底层
load_images方法被调用时,维护了两张表格,一张用来存放类的load方法,另一张用来存放分类的load方法; - 优先调用
类的load方法; - 在对类的
load方法处理时,进行递归调用来确保父类的方法能被优先处理; load方法调用顺序为:父类、子类、分类;- 分类中
load方法的调用顺序有变异顺序决定;
initialize方法
initialize在第一次消息发送时进行调用,所以load方法比initialize方法先调用;- 分类的
initialize方法是会插在最前面,以确保分类的initialize优先调用;
C++构造函数的调用
- 在
dyld流程分析的时候,我们知道其调用顺序为:load、C++构造函数、main; - 如果是在源码工程
objc中的C++构造函数,在objc_init()的时候,会通过static_init方法优先调用C++构造函数;
runtime是什么?
runtime是由C和C++汇编实现的一套Api,为OC语言加入了面向对象和运行时的功能;- 运行时是指将数据类型的确定由编译时推迟到了运行时;比如
extension和category的区别; - 平时编写的
OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,Runtime是Object-C的幕后工作者;
方法的本质是什么?sel是什么?imp是什么?它们之前有什么关系?
方法的本质是发送消息,流程如下:
- 1.快速查找(
objc_msgSend)~cache_t缓存消息; - 慢速查找~递归自己|父类~
lookUpImpOrForward; - 查找不到消息:动态方法解析~
resolveInstanceMethod; - 消息快速转发~
forwardingTargetForSelector; - 消息慢速转发~
methodSignatureForSelector&forwardInvoation;
sel是方法编号,在read_images期间就编译进入内存;
imp就是函数实现的指针,找imp就是找函数的过程;
举个例子:sel相当于书本的目录,imp就是书本的页码;查找具体的函数就是想看这本书里边具体篇章的内容;
1.我们想看什么?sel
2.根据目录找到页码;imp
3.翻到内容所在页;
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
- 不能向编译后得到的类中增加实例变量
- 只要类没有被注册到内存中就可以添加实例变量
原因:编译好的实例变量存储在
ro中,它是只读的,一旦编译完成,内存结构就完全确定,无法修改;可以增加属性和方法;
[self class]和[super class]的区别及原理分析?
我们创建一个Person类,及其子类Teacher;然后在Teacher的init方法中作如下打印:
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"%@==%@",[self class], [super class]);
}
return self;
}
查看打印结果:
按照我们正常的理解,应该打印一个Teacher和一个Person才对,但是为什么打印了两个Teacher呢?
接下来,我们来看一下class方法的实现:
最终调用的是objc_getClass(self),此处的self是从隐藏参数中(id self, sel _cmd)中传递过来的,也就是消息发送objc_msgSend(id receiver, ...)中的receiver,所以当调用[self class]时objc_getClass(self)中的self指的是Teacher;
接下来看objc_getClass的实现:
其实现是返回类的isa,因为Teacher的isa是Teacher,所以返回[self class]打印的是:Teacher;
那么为什么[super class]打印的也是Teacher呢?我们通过clang看一下Teacher.m的cpp文件:
我们发现起底层调用的是objc_msgSendSuper方法,我们在源码工程中查看objc_msgSendSuper方法:
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
起第一个参数是个结构体类型的objc_super,我们查看其结构:
其实简化之后,就是id receiver和Class super_class两个成员,根据注释super_class is the first class to search可以知道super_class表示第一个要查找的类,而不是父类;那么整个调用逻辑就清晰了:
在Teacher中调用[super class]其底层会调用objc_msgSendSuper方法,传入参数receiver是Teacher,super_class是第一个要查找的类,也就是Teacher类的父类Person;因为[super class]的receiver是Teacher,所以最终打印的是Teacher;
其调用
objc_msgSendSuper时,在底层最终调用的是objc_msgSendSuper2
内存平移
我们先来看一段代码的执行结果:
两种方式都能调用saySomething方法,这是为什么呢?
第一种我们正常调用可以理解,那么为什么第二种方式也能调用呢?
要想解决这个疑问,首先需要明白,为什么person能调用saySomething方法?
通过我们之前的知识积累可以明白person之所以能调用saySomething方法,是因为能够通过person对象的isa找到对应的类,在类中进行地址平移找到cache_t,然后进行方法的查找;
此处kc指针指向LGPerson类的首地址,那么就依然能够通过内存平移找到方法;
接下来,我们修改一下saySomething方法的打印:
- (void)saySomething{
NSLog(@"%s - %@",__func__, self.kc_name);
}
运行,查看打印结果:
因为没有给kc_name属性进行赋值,所以[person saySomething]打印个null;但是为什么调用[(__bridge id)kc saySomething]的时候,打印的是person对象呢?虽然都是调用了saySomething,但是结果不同,说明person和kc是有区别的。
person是一个对象,它开辟了内存空间,里边存放了isa和成员变量,而kc只是一个指针地址,没有内存空间。如下:
当person访问kc_name的时候,是通过内存平移的方式访问的,也就是self+kc_name的偏移量offset来找到kc_name的地址,然后获取值;也就是person的收地址向下偏移8获取到kc_name得值;
那么我们修改代码验证一下:
那我们看一下kc的地址偏移8之后是什么?
所以打印的是person对象;
接下来,我们修改代码如下:
在kc_name属性上方,再加一个属性kc_hobby,然后运行打印:
只有kc_name一个属性的时候,person偏移8,现在在kc_name之前加了个kc_hobby,就需要偏移16也就是0x10才能够访问到kc_name的值,代码验证:
那么,kc偏移0x10之后怎么找到了ViewController的对象呢?
要解决这个问题,首先我们需要分析一下压栈逻辑,根据前文中的结论,我们知道[super viewDidLoad]此处也是一个结构体,那么结构体是如何亚栈的呢?接下来我们来自定义结构体验证一下;
自定义一个结构体:
struct structT{
NSNumber *num1;
NSNumber *num2;
} structT;
修改代码如下:
打印内存地址,可以分析出栈的内存如下:
在看一下结构体数据的存放地址:
说明num1在低地址,num2在高地址;
那么回到我们最初的代码:
kc偏移0x10之后其实是找到了[super viewDidLoad]对应的结构体中的class,而class指的就是ViewController,其receiver就是ViewController对象;
{num1, num2}结构体压栈,num1在低地址,num2在高地址
那么参数是怎么压栈的呢?
(id p1, id p2)参数压栈,p1在高地址,p2在低地址
接下来,通过一段测试代码验证一下我们的结论: