与你分享一份面试题关于iOS底层原理
写在前面: iOS底层原理探索的阶段总结是对于底层探索的总结内容,
篇章不会太多,主要是对于探索中的细节总结。 希望对大家能有帮助。
总结来自以下专栏内容
load 方法在什么时候调用
load_images 分析
在 load_images
的时候 , 在 add_classs_to_loadable_list
中进行单个类的收集在loadable_classes
表中,在 add_category_to_loadable_list
中进行分类的收集在 loadable_categories
表中 ,然后统一的 ( call_class_loads
call_category_loads
) 进行递归将 load
方法进行调用。
load
方法的调用顺序为父类、子类、分类。
在 load_images
方法中 会先 发现 load
方法 通过 prepare_load_methods((const headerType *)mh);
, 之后是对load方法进行调用 call_load_methods();
;
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// 如果这里没有+load方法,则返回而不获取锁。
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// 发现 load 方法
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
// 递归自己到父类一直到nil到load方法
// 加到表里 add_class_to_loadable_list(cls); 读取类的方法 匹配是 load 后添加 ( cls 和 imp )key-value的形式
// 做一个标记 cls->setInfo(RW_LOADED);
// schedule_class_load(remapClass(classlist[i]));
}
// 调用 +load 方法
call_load_methods();
// do{} while() 循环 来调用
}
此时,又有一个问题 , load 方法和 initialize 方法谁先调用?
load
方法
initialize
方法是在第一次消息的时候调用,也就是 lookUpImpOrForward
的时候调用。
- 分类的⽅法是在类
realize
之后attach
进去的插在前⾯,所以如果分类中实现了initialize
方法,会优先调⽤分类的initialize
方法。 initialize
内部实现原理是消息发送,所以如果子类没有实现initialize
会调用父类的initialize
方法,并且会调用两次- 因为内部同时使用了递归,所以如果子类和父类都实现了
initialize
方法,那么会优先调用父类的,在调用子类的
此处 load 方法, initialize 方法 都可以自起,还有一个方法可以自起 —— c++ 的构造函数 方法。
那么,这些方法的一个先后顺序是怎样的呢?
- 在分析
dyld
之后,可以确定这样的一个调用顺序,load
->c++
->main
函数 - 写在objc工程中的c++方法 在
objc_init()
调用时,会通过static_init()
方法优先调用c++
函数,而不需要等到_dyld_objc_notify_register
向dyld
注册load_images
之后再调用 - 同时,如果
objc_init()
自启的话也不需要dyld
进行启动,也可能会发生c++
函数在load
方法之前调用的情况
补充
项目中 类和分类的 加载 顺序 是什么?
通过这里来调整编译顺序
Runtime是什么
-
Runtime
是由C
和C++
汇编实现的⼀套API
,为OC
语⾔加⼊了⾯向对象,运⾏时的功能 -
运⾏时(
Runtime
)是指将数据类型的确定由编译时推迟到了运⾏时,如类扩展和分类的区别 -
平时编写的
OC
代码,在程序运⾏过程中,其实最终会转换成Runtime
的C
语⾔代码,Runtime
是Object-C
的幕后⼯作者
⽅法的本质,sel
是什么?IMP
是什么?两者之间的关系⼜是什么?
-
⽅法的本质:发送消息,消息会有以下⼏个流程:
- 快速查找 (
objc_msgSend
)~cache_t
缓存消息 - 慢速查找~ 递归⾃⼰或⽗类 ~
lookUpImpOrForward
- 查找不到消息: 动态⽅法解析 ~
resolveInstanceMethod
- 消息快速转发 ~
forwardingTargetForSelector
- 消息慢速转发 ~
methodSignatureForSelector
和forwardInvocation
- 快速查找 (
-
sel
是⽅法编号,在read_images
期间就编译进⼊了内存 -
imp
就是我们函数实现指针,找imp
就是找函数的过程 -
sel
就相当于书本的⽬录tittle
-
imp
就是书本的⻚码 -
查找具体的函数就是想看这本书⾥⾯具体篇章的内容
- 我们⾸先知道想看什么 ~
tittle
(sel
) - 根据⽬录对应的⻚码 (
imp
) - 翻到具体的内容 方法实现
- 我们⾸先知道想看什么 ~
能否向编译后的得到的类中增加实例变量? 能否想运行时创建的类中添加实例变量
1:不能向编译后的得到的类中增加实例变量
- 我们编译好的实例变量存储的位置在 ro,一旦编译完成,内存结构就完全确定 就无法修改
- 可以通过分类向类中添加方法和属性(关联对象)
2:只要内没有注册到内存还是可以添加
- 可以添加属性 + 方法
可以通过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@:");
[self class]
和[super class]
的区别以及原理分析
我们新建一个继承自 SMPerson 的 SMTeacher 类
@implementation SMTeacher
- (instancetype)init {
self = [super init];
if (self) {
//SMTeacher - SMPerson
NSLog(@"%@ - %@", [self class], [super class] );
}
return self;
}
@end
运行项目后打印
SMTeacher - SMTeacher
和我们的猜想在super这一部分的打印是不一样的, 接下来我们就需要分析下原因了:
首先 class
实现如下:
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
方法中有两个隐藏参数 : - (Class)class( id self, SEL _cmd ) { }
,此方法在OC底层都是通过 objc_msgSend(id self, SEL _cmd)
方法来发送消息。
[self class]
所以第一个参数是方法的调用者 SMTeacher 实例
,class
方法 通过isa
找到实例变量的类也就是 SMTeacher 类
。
[super class]
super 和 self 不一样的点首先是 super 是一个编译期的关键字,并不是参数名。
通过断点调试信息可以看到,实际上是调用了 objc_msgSendSuper2
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
super 底层是一个结构体
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */
};
可以看到 receiver 依然是 SMTeacher 实例, 只不过 会先去调用SMTeacher的父类中的方法。
所以,其本质是 objc_msgSendSuper,消息的接受者还是 self ,方法编号: class。 只是 objc_msgSenderSuper 会更快,直接跳过self的查找。
补充
通过clang
SMTeacher.h 文件 可以看到 super 是调用的 objc_msgSendSuper
查看objc_msgSendSuper
内部实现,最终调用到了objc_msgSendSuper2
内存平移
准备
案例代码:
Person1 *p = [Person1 alloc];
[p sleep];
Class cls = [Person1 class];
void *sm = &cls;
[(__bridge id)sm sleep];
- (void)sleep {
NSLog(@"person - %s", __func__);
}
打印结果:
person - -[Person sleep]
person - -[Person sleep]
显然 对于 实例p
执行 sleep
方法 打印内容,我们都能理解,那么,指针sm
为什么也可以 执行 sleep
方法 打印内容呢?
这样就要看一下,实例对象执行方法的一个过程:
- 实例对象的 isa
- 类的首地址
- 通过内存平移找到类的 bits
- rw 指向 ro
- 找到 methods 这样就可以在方法列表中找到方法的地址,然后去执行。
这样的一个流程下来,对于 指针 *sm
来说,它也是指向 Person类
的 , 所以按照上面的流程 同样可以找到方法,然后去执行。
- 现在对 sleep 稍作修改:
- (void)sleep {
NSLog(@"person - %s -%@", __func__ , self.name);
}
运行后打印内容:
我们对比一下 实例p 和 指针sm,他们很明显是不一样的,
我们先 xcrun
一下 看看底层源码:
_I_Person1_name
OBJC_IVAR_$_Person1$_name
- 所以在方法中打印
name
, 我们能获取到是通过 内存的偏移 来获取到的。
内存地址偏移 0x8 在viewDidLoad
的栈帧中打印内容,接下来我们探索下 viewDidLoad
这个方法的栈帧( 到底哪些内容压栈了 ):
viewDidLoad
中哪些内容压栈了
- 栈内存地址: 高 -> 低
- 堆内存地址: 低 -> 高
首先在 Person1
中 再添加一个属性:
@interface Person1 : NSObject
@property (nonatomic, retain) NSString *like;
@property (nonatomic, retain) NSString *name;
- (void)sleep;
@end
在此运行项目,查看打印的内容:
person - -[Person1 sleep] -SuperMan
person - -[Person1 sleep] -<ViewController: 0x7ff0b1504a60>
这次很奇怪 是打印了 ViewController
。
这次因为添加了个属性,所以这次的 Person1 打印name的时候需要偏移0x10
;
- 参数压栈
- 结构体压栈
验证:
- 自定义需要参数的方法
参数从前往后压栈
- 自定义一个结构体
struct SMPer {
NSNumber *num1;
NSNumber *num2;
};
打印内容
person - -[Person1 sleep] -SuperMan
person - -[Person1 sleep] -50
明显看到了我们定义的结构体的地址是在p和cls之间,(结构体是倒着压栈的)
结构体是从后往前压栈的
最后,我们看下整个 viewDidLoad
方法的压栈情况:
还记得最开始我们 xcrun 了 ViewController.m 我们就在这个文件中找到 viewDidLoad
的方法,来分析:
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
...
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){
(id)self,
(id)class_getSuperclass(objc_getClass("ViewController"))
}
, sel_registerName("viewDidLoad"));
Person1 *p = ((Person1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person1"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sleep"));
Class cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person1"), sel_registerName("class"));
void *sm = &cls;
((void (*)(id, SEL))(void *)objc_msgSend)((id)sm, sel_registerName("sleep"));
}
xcrun还原底层代码后,看起来就舒服多了,分总结如下:
转载自:https://juejin.cn/post/6992745514743627806