runtime的学习整理
对象
- alloc底层原理探索
- 结构体内存对齐分析
- iOS对象的本质
- isa详解
类
- 类的探究分析
- 类的内存结构优化
- cahce底层分析
消息
- IOS底层原理之Runimte 运行时&方法的本质
- _lookUpImpOrForward慢速方法查找
- Objective-C 动态方法决议
- Objective-C 消息转发
应用程序加载、类、分类初始化
- dyld应用程序加载
- 类的加载(上)-- _objc_init&read_images
- 类的加载(中)
- 类的加载(下)-- 分类的加载
- iOS 类扩展&关联对象
相关面试题
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方法,那么会优先调用父类的,再调用子类的
-
具体的实现以及底层逻辑在类的加载(上)-- _objc_init&read_images
中。
补充c++构造函数
- 在分析
dyld之后,可以确定这样的一个调用顺序,load->c++->main函数 - 但是如果
c++写在objc工程中,在objc_init()调用时,会通过static_init()方法优先调用c++函数,而不需要等到_dyld_objc_notify_register向dyld注册load_images之后再调用 - 同时,如果
objc_init()自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用的情况
2.Runtime是什么?
-
Runtime是由C和C++汇编实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能 - 运⾏时
(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时,如类扩展和分类的区别 - 平时编写的
OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代码,Runtime是Object-C的幕后⼯作者
3.⽅法的本质,sel是什么?IMP是什么?两者之间的关系⼜是什么?
- ⽅法的本质:
发送消息,消息会有以下⼏个流程:-
快速查找(objc_msgSend)~cache_t缓存消息 -
慢速查找~ 递归⾃⼰或⽗类 ~lookUpImpOrForward -
查找不到消息:动态⽅法解析~resolveInstanceMethod -
消息快速转发~forwardingTargetForSelector -
消息慢速转发~methodSignatureForSelector和forwardInvocation
-
-
sel是⽅法编号,在read_images期间就编译进⼊了内存-
sel的内存结构:typedef struct objc_selector *SEL;
-
-
imp就是我们函数实现指针,找imp就是找函数的过程 -
sel就相当于书本的⽬录tittle,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@:");
5.[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
案例分析:
很清楚LGTeaher与LGPerson都没有实现class方法,那么根据消息发送的原理,他们最终都会调用到NSObject的实例方法class,该方法实现如下:
- (Class)class {
return object_getClass(self);
}
调用方法的本质是发送消息objc_msgSend,并且有两个隐藏参数,分别是id self和SEL sel,这里的隐藏参数self就是我们要分析的类型。
-
[self class]输出是LGTeacher,这个没有什么问题!因为消息的发送者是LGTeacher对象,通过消息发送机制,找到NSObejct并调用class方法,但是消息的接受者没有发生改变,依然是LGTeacher对象! -
[super class]就不一样了,同过xcrun查看main.cpp文件,查看底层源码得出以下:

super关键字,在底层最终使用了objc_msgSendSuper方法,同时其接受者是(id)self,全局搜搜objc_msgSendSuper的逻辑,见下图:

根据 Objc-818.2源码查看objc_super如下:
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表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class],其内部会调用objc_msgSendSuper方法,并且会传入参数objc_super,其中receiver是LGTeacher对象,super_class是LGTeacher类通过class_getsuperclass获取的父类,也就是要第一个查找的类。
通过下符号断点--objc_msgSendSuper2,查看寄存器,其中第一个地址为发放的第一个隐藏参数,也就是objc_super,通过类型强制,该结构体封装的recevier是LGTeacher,super_class是LGPerson,具体看下图:

得出结论:
[super class]的接收者依然是LGTeacher对象,去调用父类的方法。
最后查看运行结果:

果然输出都是
LGTeacher!!!
补充:objc_msgSendSuper为什么会调用到了objc_msgSendSuper2?
通过 Objc-818.2源码查看的出:

全局搜索
objc_msgSendSuper,进入汇编实现流程中,在汇编流程中,最终会调用objc_msgSendSuper2,见下图:
注意:这题还不够明白的话建议参考以上的消息相关文章,写得比较详细哦。
5.指针平移和消息发送原理案例分析
LGPerson类有一个实例方法saySomething,在viewDidLoad中通过两种方式调用该方法,一种是通过创建LGPerson对象调用,另一种是通过桥接调用,见下面代码:
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [LGPerson alloc];
[person saySomething];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__);
}
@end
问题1:是否能够调用成功?
- 方法调用的本质是
发送消息,通过对象的isa找到类地址,进行地址平移,通过sel找到对应的方法实现imp毋庸置疑,
person saySomething];此种方式肯定是没问题的
通过person对象的isa指针找到对应的类,在类中进行地址平移,首先在
cache_t中快速查找,如果找不到,则在方法列表以及父类的方法列表中查找,总结一下就是:以类的地址作为入口,进行地址平移,最终找到对应的imp。[(__bridge id)kc saySomething];是否可以呢?
首先Class cls = [LGPerson class];,cls是什么?cls是一个指针,Class的定义是一个指针,指向一个objc_class的指针,这里就是指向LGPerson类。将cls的地址赋值给kc,此时kc为cls的地址,也指向了类。
分析得出:两者调用的入口是一致的,从同一个地址开始进行方法查找流程,肯定是可以调用到的,person除了有地址,还有内存数据结构;kc只有一个地址,是一个伪装的person对象。请看下图:

通过lldb调试可以发现,kc指向类,见下图:

最后运行代码:

案例扩展
在LGPerson中添加一个属性kc_name,实现代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [LGPerson alloc];
person.kc_name = @"name123";
[person saySomething];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
- (void)saySomething;
@end
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s - %@", __func__, self.kc_name);
}
@end
那么这样子的输出结果优势怎么样子呢?是不是跟上面的一样都能输出呢?哈哈,以下继续进行lldb调试,请继续走!

经过调试可以知道
person进行地址平移获取属性kc_name,此数据结构是在堆中,而kc只是一个地址,获取kc数据结构只是输出了其在栈中的数据信息。
引申出压栈的概念
通过上面的案例分析,可以知道根本原因是栈中地址平移的问题,那么在程序运行过程中,压栈逻辑是怎样的呢?先入后出,这个比较清楚,那结构体是如何压栈的呢,函数调用中参数的压栈逻辑又是怎样的?
-
压栈,地址从大到小,先进去的地址大(栈开辟由高地址到低地址)

- 添加
结构体,查看栈中的地址

添加完结构体后,通过lldb的出下图:

明显看出结构体占用了16字节,那么结构体内容在栈中的位置是怎么样子的呢?继续进行lldb调试:

通过lldb输出结构体中两个属性的地址,发现,num1在num2的上面,所以在压栈过程中,按照下图中的方式进行的:

函数参数压栈顺序
通过下面的案例代码进行进一步探索:

有上面可以得出:
-
viewDidLoad方法中person指针的地址和kcFunction中person指针地址是不一样的,虽然他们都执行了同一片堆区 - 根据指针的地址发现,参数在压栈时是根据
参数的顺序进行的,第一个参数先入栈,然后依次压栈
补充:runtime面试题持续更新中哦。。。敬请期待!










