OC底层面试题(一)附解题思路
问题一:load方法在什么时候调用?
答:在load_images
内部调用load方法。
- 收集类的load方法 到
loadable_classes
表; - 收集分类的load方法添加到loadable_categories表,
- 调用call_load_methods,内部调用两张load表里的load方法。
问题二:load_images在什么时候调用?
答:在dyly检测到新的镜像文件的时候,dyly会调用load_images。
问题三:load方法和initalize方法哪个先调用?
答:load方法更早。
-
load方法是在dyly链接镜像文件的时候就会去调用load_images,继而load_images会调用所有类和分类的load方法。
-
initalize是在第一次给对象发送消息的时候进行初始化调用。
问题四:load、initalize、C++这三个自起(自动调用)方法哪个先调用?
答:看C++定义在哪里。
-
C++方法如果
写在objc工程
里,那么执行顺序:C++方法 -> load方法 -> initalize方法 -
c++方法如果
写在自己工程
里,那么执行顺序:load方法 -> C++方法 -> initalize方法
补充:关于objc工程里的c++函数调用,在objc源码搜索_objc_init
,可以看到内部调用了static_init
函数。static_init
函数是对C++函数静态函数的初始化
。
问题五:runtime是什么?它是底层吗?
答:runtime它是一种运行时机制,是由C或C++汇编写成的一套API,为OC面向对象运行时的一种功能,它不是底层。场景的运用场景:
-
Category分类属性、方法的加载;
-
addMethod(动态添加方法);
runtime的存在,让类的属性、方法在编译之后,还能够动态的添加绑定到类上。
问题六:能否向编译后的得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
答:不能向编译后的得到的类中增加实例变量;只要没注册到内存的类就还可以添加。
- 编译好的实例变量存在于ro中,一旦编译完成,内存结构就完全确定,无法更改。
- 关于向运行时创建的类中添加实例变量,这边有个案例和推导可以看下: 我们运行时创建类,代码是这样的:
Class Person = objc_allocateClassPair(NSObject.class, "Person", 0);
class_addIvar(Person, "_name", sizeof(NSString *), log2(sizeof(NSString *)), "@");
objc_registerClassPair(Person);
主要用到了3个函数:
- objc_allocateClassPair(
获取成员变量Ivar
) - class_addIvar(
动态添加成员变量
) - objc_registerClassPair(
注册class
)
而且「class_addIvar」的执行是在objc_allocateClassPair之后
,在objc_registerClassPair之前
。至于为什么是这样,我们可以通过源码来分析一下:
首先看下objc_allocateClassPair干了什么?
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes)
{
...
objc_initializeClassPair_internal(superclass, name, cls, meta);
...
}
进入objc_initializeClassPair_internal
:
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
...
cls_rw_w->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
meta_rw_w->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING | RW_META;
...
}
我们可以看到,objc_allocateClassPair对flag标记为RW_CONSTRUCTING。
看下class_addIvar干了什么?
BOOL
class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *type)
{
if (!cls) return NO; // 非空判断
...
if (cls->isMetaClass()) {// 是元类就不能添加
return NO;
}
// 如果类已经分配但未注册,就跳过判断
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
return NO;
}
// 如果ro里面已经存在这个tIvar,就跳过判断
if ((name && getIvar(cls, name)) || size > UINT32_MAX) {
return NO;
}
...
}
/** 以上几个判断,说明满足以下几个条件才可以添加实例变量:
**
* 1、cls有值
* 2、cls不是元类
* 3、类已经分配但未注册(重点)
* 4、ro里面不存在这个tIvar
*/
我们要关注的点在这里:cls->data()->flags
什么时候发生变化?
看下objc_registerClassPair干了什么?
void objc_registerClassPair(Class cls)
{
...
// cls已分配且已注册 或 cls的ISA指向已分配且已注册,就返回
if ((cls->data()->flags & RW_CONSTRUCTED) ||
(cls->ISA()->data()->flags & RW_CONSTRUCTED))
{
_objc_inform("objc_registerClassPair: class '%s' was already "
"registered!", cls->data()->ro()->getName());
return;
}
//(非)cls已分配但未注册 或 (非)cls的ISA指向已分配但未注册,就返回
if (!(cls->data()->flags & RW_CONSTRUCTING) ||
!(cls->ISA()->data()->flags & RW_CONSTRUCTING))
{
_objc_inform("objc_registerClassPair: class '%s' was not "
"allocated with objc_allocateClassPair!",
cls->data()->ro()->getName());
return;
}
/// 将状态改为RW_CONSTRUCTED,且清除之前的标记
cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
...
}
所以,要执行objc_registerClassPair,需要满足已分配但未注册
。
objc_registerClassPair
函数一旦执行,flag
标记就变成了RW_CONSTRUCTED
,自然就不能再执行class_addIvar。
总结:
- objc_allocateClassPair(flag设置为
RW_CONSTRUCTING
) - class_addIvar(执行的条件之一是flag为
RW_CONSTRUCTING
) - objc_registerClassPair(flag设置为
RW_CONSTRUCTED
)
问题七: [self class] 和 [super class]的区别及原理是什么?
答:首先,我们写个测试案例打印一下:
/// DirectionChild.m内部代码:
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"查看[self class] 和 [super class]区别---------%@->%@",[self class],[super class]);
}
return self;
}
/// main函数
int main(int argc, const char * argv[]) {
DirectionChild *childClass = [[DirectionChild alloc] init];
}
我们发现两个打印的结果一样,这是为什么呢??
回到objc源码工程,command+单机进入class方法:
查看object_getClass
:
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
所以第一个self打印DirectionChild就不言而喻了。
接下来看[super class]
,
super不知道来自哪里,我们通过clang看一下编译后的源码是什么样的。指令如下:
- cd到指定文件的上层文件夹目录
- $
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc DirectionChild.m
打开得到的DirectionChild.mm
,为了快速定位,搜索「DirectionChild」:
提取[super class]
部分代码:
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("DirectionChild"))}, sel_registerName("class"));
我们发现[super class]
实际调用的是objc_msgSendSuper,
接下来,我们通过Debug查看下运行时的实际情况:
当前运行的是mac工程,那么真机运行下,是否也是这样呢? 这里,我们新建了一个测试工程,运行看效果:
看到这里是不是有点懵逼,clang静态编译
的时候调用objc_msgSendSuper
,动态运行
的时候调用的居然是objc_msgSendSuper2
。
为了探索这个变化,我们
objc源
码工程全局搜索objc_msgSendSuper
:
当前文件搜索L_objc_msgSendSuper2_body
,发现它会进入_objc_msgSendSuper2里面:
回到前面的问题,[super class]打印:
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self,
(id)class_getSuperclass(objc_getClass("DirectionChild"))},
sel_registerName("class"));
简化一下代码:
((void *)objc_msgSendSuper)((__rw_objc_super){
(id)self,
(id)class_getSuperclass(objc_getClass("DirectionChild"))
},sel_registerName("class"));
第一个参数是self
,我们前面分析过,class最终返回的是class函数第一个参数的isa指向
,self的isa指向是DirectionChild类,所以[self class]
和[super class]
都打印DirectionChild
。
问题七: 通常我们在给一个类写init初始化方法的时候,都会写self=[super init],这是为什么?
答:这样写是为了更好的继承父类定义的一些公共属性、方法、协议等等。
问题八:请问下面main函数运行会报错吗?
// Direction.h
#import <Foundation/Foundation.h>
@interface Direction : NSObject
@property (nonatomic , strong) NSString *hobby;
@end
// Direction.m
@implementation Direction
- (void)run{
NSLog(@"run faster.");
}
@end
// main.m
int main(int argc, const char * argv[]) {
Direction *dt = [Direction alloc];
[dt run];
Class cls = [Direction class];
void *ssj = &cls;
[(id)ssj run];
return 0;
}
运行代码:
事实证明,可以正常运行,控制台也打印出来了东西。那么,为什么可以不通过实例化对象就调用类的实例方法呢?
- 首先,我们要知道,
方法的调用
本质上就是
消息的发送objc_msgSend
。 - objc_msgSend第一个参数是
recever
消息接收者,通过它找到Class类
。 - 然后再从Class类的data里,
通过
objc_msgSend的第二个参数SEL
去找到对应的IMP
并调用。 dt
作为一个Direction实例对象,通过它的isa可以找到Direction类
。ssj
指针指向的就是Direction类地址,可以找到Direction类
。- 所以对于objc_msgSend来说,只要能找到Class类,第一个参数是dt或ssj都可以。
下面是「dt」和「ssj」指向Direction类
:
拓展:对run
方法进行改造,查看打印的数据。
// Direction.m
@implementation Direction
- (void)run{
NSLog(@"run faster.->%@",self.hobby);
}
@end
运行:
-
第一个打印null没什么疑问,因为没有对hobby赋值;
-
第二个为什么打印的是
Direction
类呢?
答:dt作为一个实例对象
,alloc开辟了空间,它有指针地址、也有内存
。而ssj
就是一个纯粹指向Direction类地址的指针,系统并没有为它开辟内存
。因此hobby
指向ssj指针地址的下一个地址(栈帧里的如栈顺序,也就是dt)。
我们来通过控制台打印,看下数据的变化:
然后在Direction
的run
方法下断点:
这时候,我们有个猜想
:
- 通过类地址指针ssj访问类的属性,得到的是数据可能跟ssj指针所在栈帧有关系。
为了验证这个猜想,我们对代码进行了添加:
// Direction.h
#import <Foundation/Foundation.h>
@interface Direction : NSObject
@property (nonatomic , strong) NSString *firendNames;
@property (nonatomic , strong) NSString *firendNames2;
@property (nonatomic , strong) NSString *hobby;
@end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
Direction *dt = [Direction alloc];
[dt run];
/// 添加3个实例变量zo1、zo2、zo3
Zoon *zo1 = [Zoon new];
Zoon *zo2 = [Zoon new];
Zoon *zo3 = [Zoon new];
Class cls = [Direction class];
void *ssj = &cls;
[(__bridge id)ssj run];
}
@end
继续重复上面的控制台打印:
在Direction
的run
方法下断点:
打印结果验证了上面的猜想:
通过类地址指针ssj访问类的属性,得到的是数据跟ssj指针所在栈帧有关系,按照入栈顺序打印。
问题九:请问以下代码的压栈顺序是怎么样的?
结构体压栈
- (void)viewDidLoad {
[super viewDidLoad];
/// 测试插入顺序
Direction *dt1 = [Direction alloc];
struct SSJStruct sSJStruct = {@1,@22};
Direction *dt2 = [Direction alloc];
NSLog(@"测试结束");
}
答:
-
dt1、sSJStruct、dt2,以此由高地址向低地址压栈。
-
sSJStruct内部,低地址向高地址压栈。
运行代码:
参数压栈
// 自定义函数
int ssj_fun(id obj1,id obj2){
return 0;
}
- (void)viewDidLoad {
[super viewDidLoad];
/// 测试参数压栈
Direction *dt1 = [Direction alloc];
Direction *dt2 = [Direction alloc];
ssj_fun(dt1, dt2);
NSLog(@"测试结束");
}
下断点,运行代码:
分别打印dt1、dt2、obj1、obj2,其中dt1、dt2是指针地址,obj1、obj2是值的地址。 根据栈的压栈从高到低顺序,压栈顺序先obj1再obj2。
问题十:[super viewDidLoad] 里的super传的是当前Controller还是当前Controller的父类?
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad]
// 实际运行会调用函数 objc_msgSendSuper2
// objc_msgSendSuper2定义如下
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
}
那么,这里的super传的是ViewController还是UIViewController? 带着这个疑问,我们准备实际操作打印一下:
下符号断点objc_msgSendSuper2
输入register read
查看寄存器
控制台,打印x0,即第一个参数的数据:
代码:
链接: pan.baidu.com/s/1uPGdFij0… 密码: il7h --来自百度网盘超级会员V2的分享
转载自:https://juejin.cn/post/7000620015045050375