OC底层原理探索—经典面试题原理
2021-07-26 本文已影响0人
十年开发初学者
1.load
和initialize
方法的调用原则和调用顺序?
load
-
load
方法在应用程序加载过程中(dyld
)完成调用,在main
之前 - 在底层进行
load_images
处理时,维护了两个load
的加载表,一个是本类的表
,另一个是分类的表
,所以说有先对本类的load
发起调用 - 在对类
load
方法进行处理时,进行递归处理,以确保父类优先被处理 - 在
load
方法的调用顺序是父类、子类、分类
- 在分类中
load
调用顺序,是根据编译的顺序为准
initialize
-
initialize
是在第一次消息发送的时候进行调用,load
先于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方法之前调用的情况
方法的本质
- 方法的本质:发送消息
消息发送的流程
- 首先进入快速查找也就是通过
objc_msgSend
去缓存(cache_t
)中查找 - 慢速查找:通过
递归自己或者父类
查找,也就是lookupImporForward
方法 - 动态方法解析:
resolveInstanceMethod
- 消息快速转发:
forwardingTargetForSelector
- 消息慢速转发:
methodSignatureForSelector
和forwardInvocation
能否向编译后的得到的类中增加实例变量?能否向运⾏时创建的类中添加实例变量?
1.不能向编译后得到的类增加实例变量
- 首先编译好的实例变量存储在
ro
中,一旦完成编译,内存结构确定 - 可以通过分类以
关联对象形式
向类中添加分类和属性
- 可以向运⾏时创建的类中添加实例变量
- 可以通过
objc_allocateClassPair
运行时创建类,并添加属性、实例变量、方法等
` const char *className = "SHObject";
Class objc_class = objc_getClass(className);
if (!objc_class) {
Class superClass = [NSObject class];
objc_class = objc_allocateClassPair(superClass, className, 0);
}
class_addIvar(objc_class, "name", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *));
class_addMethod(objc_class, @selector(addName:), (IMP)addName, "V@:");
[self class]和[super class]区别
来看下下面案例,LGTeacher类继承自LGPerson,在LGTeacher的init初始化方法中,调用了[self class]和[super class],结果会是什么
// LGPerson
@interface LGPerson : NSObject
@end
@implementation LGPerson
@end
// LGTeacher
@interface LGTeacher : LGPerson
@end
@implementation LGTeacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@", [self class], [super class]);
}
return self;
}
@end
首先这两个类中都没有实现class
方法,那么根据继承关系,他们最终会调用到NSObject
中的class
方法
- (Class)class {
return object_getClass(self);
}
由上图可知这两个方法返回的self
对应的类。关于这个self
是谁,这里涉及到消息发送objc_msgSend
,有两个隐形参数,分别是id self 和 SEL sel
,这里SEL sel
没啥好说的,主要来说下id self
-
[self class]
输出LGTeacher
,这里消息的发送者是LGTeacher
对象,通过调用NSObject 的 class
,但是消息的接受者没有发生变化,所以是LGTeacher
- [super class]这里的输出仍然是
LGteacher
,至于为什么,我们来clang
一下,查看下cpp文件
image.png
通过上图我们看到[super class]
的低层实现时objc_msgSendSuper
方法,同时存在存在id self 和 SEL sel
两个隐形参数
/// 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 */
};
我们来查看下objc_super
结构体
由上图知:id receiver
和Class super_class
两个参数,其中super_class
表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class],其内部会调用objc_msgSendSuper方法,并且会传入参数objc_super,其中receiver是LGTeacher对象,super_class是LGTeacher的父类,也就是要第一个查找的类。
内存偏移
案例1
创建一个person类
@interface Person : NSObject
@property (nonatomic,copy)NSString *name;
- (void)say1;
@end
@implementation Person
- (void)say1{
NSLog(@"%s",__func__);
}
@end
在viewDidload
中添加以下代码
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Person *person = [[Person alloc] init];
[person say1];
Class cls = [Person class];
void *sh =&cls;
[(__bridge id)sh say1];
}
查看下打印
image.png
分析:
- 这两处调用的本质
objc_msgSend
的调用,在汇编源码
中进行方法的快速查找
-
[person say1];
通过person
对象的isa
指针找到对应的类,在类中进行地址平移,首先在cache_t
中快速查找,如果找不到,则在类或者父类的方法列表遍历查找
-
[(__bridge id)sh say1]
,这里能够调用成功的原因是,Class cls = [Person class];
,cls
是一个指针,指向一个objc_class
指针,这里是指向Person
类,将cls
地址赋给sh
,sh
为cls
的地址,也是指向类
由上图知,
sh
是指向Person类的
image.png
总结:
-
person对象
里面的isa
指向Person
类,通过内存平移的方式找到say1
方法 -
sh指向cls
而cls指向Person类
,同样通过首地址平移找到say1
案例2
接着上面的案例,我们新增一个属性打印
@implementation Person
- (void)say1{
NSLog(@"%s,%@",__func__,self.name);
}
@end
查看打印
image.png
首先我们先要了解,取出属性的值
,其实是要先计算出偏移大小,在通过内存平移获取值。其实是Person类内部存储着成员变量
,每次偏移8字节
进行存取
至于sh
打印的self.name
的值是Person;0X600...
,是因为cls
只有Person类的内存首地址
,但是没有person对象的内存结构
,所以sh
只能在栈里面进行内存平移。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Class cls = [Person class] ;
void *sh =&cls;
[(__bridge id)sh say1];
Person *person = [[Person alloc] init];
[person say1];
// 下面代码为打印栈结构
void *sp = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
for (long i = 0; i < count; i++) {
void *address = sp - 0x8 * I;
if (i == 1) {
NSLog(@"%p : %s",address, *(char **)address);
} else {
NSLog(@"%p : %@",address, *(void **)address);
}
}
}
看下栈结构打印
image.png
-
0x16f35ffb8 : <ViewController: 0x136906d30>
是viewDidload
第一个隐形参数id self
-
0x16f35ffb0 : viewDidLoad``是
viewDidload第二个隐形参数
SEL _cmd` -
0x16f35ffa8 : ViewController
,这个是结构体class压栈 -
0x16f35ffa0 : <ViewController: 0x136906d30>
为结构体receiver
压栈 -
Person
是sh压栈
-
0x16f35ff90 : <Person: 0x16f35ff98>
为person
压栈
什么可以压栈
-
方法的参数
,viewDidLoad的(id self, SEL _cmd)。
-
结构体参数
,objc_super
,相当于下边这块代码创建了一个sh_objc_super的临时变量,所以也可以压栈。
struct objc_super sh_objc_super;
sh_objc_super.super_class = class;
sh_objc_super.receiver = receiver;
- 临时变量即:
sh、person