一、load、C++构造函数、initialize 调用顺序
load 函数调用时机
load方法在dyld加载流程中被调用,函数调用流程是:doModInitFunctions->libSystem_initializer->_objc_init->_dyld_objc_notify_register->load_images->。- 详细可阅读 iOS 底层原理:dyld 应用程序加载。
initialize 函数调用时机
initialize是在类第一次被加载时调用的,通过lookUpImpOrForward函数调用。- 详细可阅读 OC 原理探索:方法的慢速查找流程。
C++构造函数 函数调用时机
- 如果
C++构造函数是在objc源码中,那么它是在static_init中被调用的,我们在 iOS 底层原理:类的加载原理上 分析过。- 它的函数调用顺序是
doModInitFunctions->libSystem_initializer->_objc_init->static_init->getLibobjcInitializers。 static_init在_dyld_objc_notify_register之前,所以在load方法前。
- 它的函数调用顺序是
- 普通的
C++构造函数也是在doModInitFunctions内被调用,不过是在libSystem_initializer之后,所以在load方法之后。
调用顺序 总结
objc源码中的C++构造函数。load方法。- 普通的
C++构造函数。 initialize函数。
二、Runtime 是什么
runtime是由C、C++、汇编实现的一套API,为OC语言加入了面向对象,运行时的功能。- 运行时(
Runtime)是指,将数据类型的确定由编译时推迟到了运行时,比如category、rwe等。 - 平时编写的
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)。 - 翻到具体的内容。
- 我们首先知道想看什么 ~
四、能否向编译后的得到的类中增加实例变量? 能否向运行时创建的类中添加实例变量?
- 不能向编译后的得到的类中增加实例变量。
- 我们编译好的实例变量存储的位置在
ro,一旦编译完成,内存结构就完全确定就无法修改。 - 可以通过分类向类中添加方法和属性(关联对象)
- 我们编译好的实例变量存储的位置在
- 只要没有注册到内存还是可以添加的,见下面代码:
void createClassAddIvar(void) { // 使用objc_allocateClassPair创建一个类Class const char * className = "MyClass"; Class SelfClass = objc_getClass(className); if (!SelfClass){ Class superClass = [NSObject class]; SelfClass = objc_allocateClassPair(superClass, className, 0); } // 使用class_addIvar添加三个成员变量 class_addIvar(SelfClass, "name", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *)); class_addIvar(SelfClass, "age", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *)); class_addIvar(SelfClass, "sex", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *)); objc_registerClassPair(SelfClass); } // 打印 MyClass 的成员变量 void logIvars(void) { const char * className = "MyClass"; Class SelfClass = objc_getClass(className); unsigned int outCount; Ivar * ivars = class_copyIvarList(SelfClass, &outCount); for (unsigned int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; NSLog(@"--------key:%@",key); } } // 打印结果 --------key:name --------key:age --------key:sex
五、[self class] 和 [super class] 的区别以及原理分析
创建SSLPerson类和SSLStudent类:
@interface SSLPerson : NSObject
@end
@implementation SSLPerson
@end
@interface SSLStudent : SSLPerson
@end
@implementation SSLStudent
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"%@ - %@",[self class],[super class]);
}
return self;
}
@end
SSLStudent继承自SSLPerson。SSLStudent中实现init方法,方法中打印[self class],[super class],它们的打印结果是什么呢?
结果分析
打印结果是SSLStudent - SSLStudent,[self class]打印SSLStudent我们都能理解,下面分析一下[super class]是如何打印SSLStudent的。
clang -rewrite-objc SSLStudent.m -o SSLStudent.cpp得到SSLStudent.cpp:
// [self class] 编译后的C++代码
objc_msgSend(self, sel_registerName("class"))
// [super class] 编译后的C++代码
objc_msgSendSuper({self,class_getSuperclass(objc_getClass("SSLStudent"))},sel_registerName("class"))
[super class]编程成C++以后是objc_msgSendSuper函数的调用,看一下它的结构:objc_msgSendSuper(struct objc_super *super, SEL op, ... ) struct objc_super { __unsafe_unretained _Nonnull id receiver; __unsafe_unretained _Nonnull Class super_class; };- 通过
class_getSuperclass函数获取到父类,作为参数传入。
其实objc_msgSendSuper这种方式是老版的调用方式,汇编跟源码查看真实的函数调用:
- 真正调用的是
objc_msgSendSuper2,它的结构跟objc_msgSendSuper是一样的:objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
查看objc_msgSendSuper2的实现:
- 可以看到调用
objc_msgSendSuper2函数时,传入的是当前类,通过汇编源码来获取父类,再调用CacheLookup函数,到这里就和 objc_msgSend 流程 一样了。 - 如果调用的是
objc_msgSendSuper函数,直接传入的就是获取好的父类,然后跳到L_objc_msgSendSuper2_body也调到了CacheLookup函数。
分析了这么多,其实[self class]和[super class]的区别,只是[super class]从父类开始查找方法而已,最终还是要看class的方法实现。
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
self是我们传入的隐藏参数也就是SSLStudent的实例对象,它的isa自然就是SSLStudent类。
五、内存偏移面试题
内存偏移面试题 1
创建SSLPerson类:
@interface SSLPerson : NSObject
- (void)say1;
@end
@implementation SSLPerson
- (void)say1
{
NSLog(@"%s",__func__);
}
@end
在viewDidLoad中添加下面代码,大家可以猜测一下打印结果:
- (void)viewDidLoad {
[super viewDidLoad];
SSLPerson *person = [[SSLPerson alloc] init];
[person say1];
Class cls = [SSLPerson class];
void *ssl = &cls;
[(__bridge id)ssl say1];
}
查看打印结果:
-[SSLPerson say1]
-[SSLPerson say1]
结果分析:
- 上下两处的本质都是
objc_msgSend的调用,objc_msgSend 流程 中我们分析过,在汇编源码中会先通过isa & ISA_MASK获取到class,再进行方法的查找。 [person say1]可以调用成功,我们都能理解不做过多解释。[(__bridge id)kc say1]也是可以调用成功的,因为ssl中存储这类的首地址,汇编源码中通过类地址 & ISA_MASK当然能获取到class,后面的流程也就一样了。
图示:
内存偏移面试题 2
修改SSLPerson类:
@interface SSLPerson : NSObject
@property (nonatomic, copy) NSString *ssl_name;
- (void)say1;
@end
@implementation SSLPerson
- (void)say1
{
NSLog(@"%s - %@",__func__,self.ssl_name);
}
@end
在viewDidLoad中添加代码:
- (void)viewDidLoad {
[super viewDidLoad];
SSLPerson *person = [[SSLPerson alloc] init];
person.ssl_name = @"SSL";
[person say1];
Class cls = [SSLPerson class];
void *ssl = &cls;
[(__bridge id)ssl say1];
}
查看打印结果:
-[SSLPerson say1] - SSL
-[SSLPerson say1] - <SSLPerson: 0x6000015e48d0>
结果分析:
clang -rewrite-objc SSLPerson.m -o SSLPerson.cpp得到SSLPerson.cpp:
// 计算 ssl_name 的偏移大小
OBJC_IVAR_$_SSLPerson$_ssl_name = __OFFSETOFIVAR__(struct SSLPerson, _ssl_name);
// 通过内存平移获取到 ssl_name
_I_SSLPerson_ssl_name(SSLPerson * self, SEL _cmd) {
return self + OBJC_IVAR_$_SSLPerson$_ssl_name;
}
- 可以看到
_ssl_name的取值,是先计算出它的偏移大小,再通过内存平移得到值。
lldb调试验证:
图示:
内存偏移面试题 3
看下面代码,栈结构打印:
- (void)viewDidLoad { // (id self, SEL _cmd)
[super viewDidLoad];// 结构体{receiver,class}
Class cls = [SSLPerson class];
void *ssl = &cls;
SSLPerson *person = [SSLPerson alloc];
person.ssl_name = @"SSL";
// 下面代码为打印栈结构
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);
}
}
}
查看打印结果:
0x7ffee876e208 : <ViewController: 0x7fc7b050a940>
0x7ffee876e200 : viewDidLoad
0x7ffee876e1f8 : ViewController
0x7ffee876e1f0 : <ViewController: 0x7fc7b050a940>
0x7ffee876e1e8 : SSLPerson
0x7ffee876e1e0 : <SSLPerson: 0x7ffee876e1e8>
结果分析:
<ViewController: 0x7fc7b050a940>为- (void)viewDidLoad的id self压栈。viewDidLoad为- (void)viewDidLoad的SEL _cmd压栈。ViewController为结构体的class压栈。<ViewController: 0x7fc7b050a940>为结构体的receiver压栈。SSLPerson为ssl压栈。<SSLPerson: 0x7ffee876e1e8>为person压栈。
什么东西才会压栈:
本方法的参数可以压栈,viewDidLoad的(id self, SEL _cmd)。- 对于
objc_super这种结构体参数,相当于下边这块代码创建了一个ssl_objc_super的临时变量,所以也可以压栈。struct objc_super ssl_objc_super; ssl_objc_super.super_class = class; ssl_objc_super.receiver = receiver;
六、Method Swizzling 面试题
基本原理
先看下Method Swizzling代码:
@interface SSLRuntimeTool : NSObject
/**
交换方法
@param cls 交换对象
@param oriSEL 原始方法编号
@param swizzledSEL 交换的方法编号
*/
+ (void)ssl_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
@end
@implementation SSLRuntimeTool
+ (void)ssl_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
@end
Method Swizzling使用:
@interface SSLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation SSLPerson
- (void)personInstanceMethod {
NSLog(@"%s",__func__);
}
@end
@interface SSLStudent : SSLPerson
@end
@implementation SSLStudent
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[SSLRuntimeTool ssl_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(studentInstanceMethod)];
});
}
- (void)studentInstanceMethod {
[self studentInstanceMethod];
NSLog(@"%s",__func__);
}
@end
- 防止重复交换,使用
单例更安全。
Method Swizzling原理图示:
坑点一
上面的代码进行相关调用:
- (void)viewDidLoad {
[super viewDidLoad];
SSLPerson *person = [[SSLPerson alloc] init];
[person personInstanceMethod];
}
发生错误:
'-[SSLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600002fbc400'
- 因为方法交换,我们实际调用是这个方法:
- (void)studentInstanceMethod { [self studentInstanceMethod]; NSLog(@"%s",__func__); } - 这时的
self是SSLPerson实例,SSLPerson中并没有studentInstanceMethod的SEL,找不到对应的SEL,所以就会CRASH。
解决问题:
@implementation SSLRuntimeTool
+ (void)ssl_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// personInstanceMethod(sel) - studentInstanceMethod(imp)
// 尝试添加你要交换的方法 - studentInstanceMethod
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) { // 添加成功说明自己没有
// studentInstanceMethod (SEL) - personInstanceMethod(imp)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { // 自己有 - 交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
@end
class_replaceMethod函数:- 如果该
Class不存在指定SEL,则class_replaceMethod的作用就和class_addMethod一样; - 如果该
Class存在指定的SEL,则class_replaceMethod的作用就和method_setImplementation一样。
- 如果该
坑点二
如果personInstanceMethod没有实现的话,运行程序,会发生死循环:
解决问题:
@implementation SSLRuntimeTool
+ (void)ssl_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
// 在 oriMethod 为 nil 时,替换后将swizzledSEL复制一个不做任何事的空实现
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}
// 尝试添加你要交换的方法 - studentInstanceMethod
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) { // 添加成功说明自己没有 - 替换 - 父类重写一个
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { // 自己有 - 交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
@end