1.load
和initialize
方法的调用原则和调用顺序?
load
方法
load
方法在应用程序加载过程中(dyld
)完成调用,在main
函数之前- 在底层进行
load_images
处理时,维护了两个load
加载表,一个类的表,另一个为分类的表,优先对类的load
方法发起调用 - 在对类
load
方法进行处理时,进行了递归处理,以确保父类优先被处理 - 所以
load
方法的调用顺序为父类、子类、分类 - 而分类中
load
方法的调用顺序根据编译顺序为准
initialize
方法
initialize
在第一次消息发送的时候调用,所以load
先于initialize
调用- 分类的⽅法是在类
realize
之后attach
进去的插在前⾯,所以如果分类中实现了initialize
方法,会优先调⽤分类的initialize
方法 initialize
内部实现原理是消息发送,所以如果子类没有实现initialize
会调用父类的initialize
方法,并且会调用两次- 因为内部同时使用了递归,所以如果子类和父类都实现了
initialize
方法,那么会优先调用父类的,在调用子类的
- 补充
c++
构造函数
- 在分析
dyld
之后,可以确定这样的一个调用顺序,load
->c++
->main
函数 - 但是如果
c++
写在objc
工程中,在objc_init()
调用时,会通过static_init()
方法优先调用c++
函数,而不需要等到_dyld_objc_notify_register
向dyld
注册load_images
之后再调用 - 同时,如果
objc_init()
自启的话也不需要dyld
进行启动,也可能会发生c++
函数在load
方法之前调用的情况
3.⽅法的本质,sel
是什么?IMP
是什么?两者之间的关系⼜是什么?
-
⽅法的本质:发送消息,消息会有以下⼏个流程:
- 快速查找 (
objc_msgSend
)~cache_t
缓存消息 - 慢速查找~ 递归⾃⼰或⽗类 ~
lookUpImpOrForward
- 查找不到消息: 动态⽅法解析 ~
resolveInstanceMethod
- 消息快速转发 ~
forwardingTargetForSelector
- 消息慢速转发 ~
methodSignatureForSelector
和forwardInvocation
- 快速查找 (
-
sel
是⽅法编号,在read_images
期间就编译进⼊了内存typedef struct objc_selector *SEL;
-
imp
就是我们函数实现指针,找imp
就是找函数的过程 -
sel
就相当于书本的⽬录tittle
-
imp
就是书本的⻚码 -
查找具体的函数就是想看这本书⾥⾯具体篇章的内容
- 我们⾸先知道想看什么 ~
tittle
(sel
) - 根据⽬录对应的⻚码 (
imp
) - 翻到具体的内容 方法实现
- 我们⾸先知道想看什么 ~
4.能否向编译后的得到的类中增加实例变量?能否向运⾏时创建的类中添加实例变量?
-
不能向编译后的得到的类中增加实例变量
- 我们编译好的实例变量存储的位置在
ro
,⼀旦编译完成,内存结构就完全确定; - 可以通过分类向类中添加方法和属性(关联对象)
- 我们编译好的实例变量存储的位置在
-
可以向运行时创建的类中添加实例变量,只要内没有注册到内存还是可以添加
可以通过objc_allocateClassPair
在运行时创建类,并向其中添加成员变量和属性,见下面代码:
// 使用objc_allocateClassPair创建一个类Class
const char * className = "SelClass";
Class SelfClass = objc_getClass(className);
if (!SelfClass){
Class superClass = [NSObject class];
SelfClass = objc_allocateClassPair(superClass, className, 0);
}
// 使用class_addIvar添加一个成员变量
BOOL isSuccess = class_addIvar(SelfClass, "name", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *));
class_addMethod(SelfClass, @selector(addMethodForMyClass:), (IMP)addMethodForMyClass, "V@:");
4.[self class]和[super class]区别和解析?
下面案例,JHSTeacher
类继承自JHSPerson
,在JHSTeacher
的init
初始化方法中,调用了[self class]
和[super class]
,运行打印结果会怎样呢?
// JHSPerson
@interface JHSPerson : NSObject
@end
@implementation JHSPerson
@end
//JHSTeacher
@interface JHSTeacher : JHSPerson
@end
@implementation JHSTeacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@", [self class], [super class]);
}
return self;
}
@end
分析思路:
首先确定,当前JHSPerson
和JHSTeacher
都没有实现class
方法,那么根据消息发送的原理,他们最终都会调用到NSObject
的实例方法class
,该方法的方法实现是:
- (Class)class {
return object_getClass(self);
}
也就是说这两个方法都会返回self
对应的类,那么self
是谁呢?我们在分析方法的本质时知道,调用方法的本质是发送消息objc_msgSend
,并且有两个隐藏参数,分别是id self
和SEL sel
,这里的隐藏参数self
就是我们要分析的类型。
[self class]
输出是JHSTeacher
,这个没有什么问题!因为消息的发送者是JHSTeacher
对象,通过消息发送机制,找到NSObejct
并调用class
方法,但是消息的接受者没有发生改变,依然是JHSTeacher
对象!
[super class]
输出的是呢?同样的方式clang
一下,查看cpp
中底层实现原理是怎样的?
super
关键字,在底层最终使用了objc_msgSendSuper
方法,同时其接受者是(id)self
,全局搜搜objc_msgSendSuper
的逻辑,见下图:
objc_super
结构体如下:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
也就是id receiver
和Class super_class
两个参数,其中super_class
表示第一个要去查找的类,至此我们可以得出结论,在JHSTeacher
中调用[super class]
,其内部会调用objc_msgSendSuper
方法,并且会传入参数objc_super
,其中receiver
是JHSTeacher
对象,super_class
是JHSTeacher
类通过class_getsuperclass
获取的父类,也就是要第一个查找的类。
下符号断点,objc_msgSendSuper2
,查看寄存器,其中第一个地址为发放的第一个隐藏参数,也就是objc_super
,通过类型强制,该结构体封装的recevier
是JHSTeacher
,super_class
是JHSPerson
。见下图:
就是说:[super class]
的接受者依然是JHSTeacher
对象,去调用父类的方法。
补充:
调用objc_msgSendSuper
,实际却调用了objc_msgSendSuper2
为什么呢?
全局搜索objc_msgSendSuper
,进入汇编实现流程中,在汇编流程中,最终会调用objc_msgSendSuper2
,见下图:
5.指针平移和消息发送原理案例?
有下面的一个案例,JHSPerson
类有一个实例方法saySomething
,在viewDidLoad
中通过两种方式调用该方法,一种是通过创建JHSPerson
对象调用,另一种是通过桥接调用,见下面代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test{
JHSTeacher *teacher = [JHSTeacher alloc];
[teacher saySomething];
Class cls = [JHSTeacher class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
问题是否能够调用成功? 分析思路
首先方法调用的本质是发送消息,通过对象的isa
找到类地址,进行地址平移,通过sel
找到对应的方法实现imp
。
-
[teacher saySomething];
此种方式肯定是可以的此流程的原理是什么?通过
teacher
对象的isa
指针找到对应的类,在类中进行地址平移,首先在cache_t
中快速查找,如果找不到,则在方法列表以及父类的方法列表中查找,总结一下就是:以类的地址作为入口,进行地址平移,最终找到对应的imp
。 -
[(__bridge id)kc saySomething];
是否可以呢?首先
Class cls = [JHSTeacher class];
,cls
是什么?cls
是一个指针,Class
的定义是一个指针,指向一个objc_class
的指针,这里就是指向JHSTeacher
类。将cls
的地址赋值给kc
,此时kc
为cls
的地址,也指向了类。
综上,两者调用的入口是一致的,从同一个地址开始进行方法查找流程,肯定是可以调用到的,teacher
除了有地址,还有内存数据结构;kc
只有一个地址,是一个伪装的teacher
对象,见下图:
通过lldb
调试可以发现,kc
指向类,见下图:
运行验证,两个都可以调用成功。见下图:
- 扩展案例
在上面案例的基础上进行修改,
saySomething
方法JHSTeacher
对象的第一个属性,中输出见下面代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test{
JHSTeacher *teacher = [JHSTeacher alloc];
teacher.jhs_name = @"name";
[teacher saySomething];
Class cls = [JHSTeacher class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
@interface JHSTeacher : NSObject
@property (nonatomic, copy) NSString *jhs_name;
- (void)saySomething;
@end
@implementation JHSTeacher
- (void)saySomething{
NSLog(@"%s ---- %@",__func__,self.jhs_name);
}
@end
此时运行结果又是怎样呢?
[teacher saySomething];
调用后输出结构没有什么疑问[(__bridge id)kc saySomething];
的输出结构是怎样的呢?见下图:
根据lldb
调试可以发现,teacher
进行地址平移获取属性jhs_name
,此数据结构是在堆中,而kc
只是一个地址,获取kc
数据结构只是输出了其在栈中的数据信息。见下图:
- 结构体验证逻辑
通过上面的案例分析,可以知道根本原因是栈中地址平移的问题,那么在程序运行过程中,压栈逻辑是怎样的呢?先入后出,这个比较清楚,那结构体是如何压栈的呢,函数调用中参数的压栈逻辑又是怎样的?
- 压栈,地址从大到小,新进去的地址大
- 添加结构体,查看栈中的地址
那么此时三个参数在栈中的存储顺序应该是下图:
上图再结合输出的地址,我们可以发现此时结构体占用16
个字节,那么结构体中元素的存储顺序是怎样的呢?见下图:
通过lldb输出结构体中两个属性的地址,发现,num1在num2的上面,所以在压栈过程中,按照下图中的方式进行的:
- 函数参数压栈顺序
通过下面的案例进行分析:
从上图中我们可以发现几个问题:
viewDidLoad
方法中person
指针的地址和kcFunction
中person
指针地址是不一样的,虽然他们都执行了同一片堆区- 根据指针的地址发现,参数在压栈时是根据
参数的顺序
进行的,第一个参数先入栈,然后依次压栈