iOS 底层原理:OC 底层面试(一)

513 阅读6分钟

一、load、C++构造函数、initialize 调用顺序

load 函数调用时机

  • load方法在dyld加载流程中被调用,函数调用流程是:doModInitFunctions -> libSystem_initializer -> _objc_init -> _dyld_objc_notify_register ->load_images ->。
  • 详细可阅读 iOS 底层原理:dyld 应用程序加载

initialize 函数调用时机

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方法之后。

调用顺序 总结

  1. objc源码中的C++构造函数。
  2. load方法。
  3. 普通的C++构造函数。
  4. initialize函数。

二、Runtime 是什么

  • runtime是由CC++汇编实现的一套API,为OC语言加入了面向对象,运行时的功能。
  • 运行时(Runtime)是指,将数据类型的确定由编译时推迟到了运行时,比如categoryrwe等。
  • 平时编写的OC代码,在程序运行过程中,其实最终会转换成RuntimeC语言代码,RuntimeObject-C 的幕后工作者。

三、方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?

  1. 方法的本质是发送消息,消息会有以下几个流程:
    1. 快速查找 ~ (objc_msgSend) ~ cache_t缓存消息。
    2. 慢速查找 ~ 递归自己活父类 ~ lookUpImpOrForward
    3. 查找不到消息 ~ 动态方法解析 ~ resolveInstanceMethod
    4. 消息快速转发 ~ forwardingTargetForSelector
    5. 消息慢速转发 ~ methodSignatureForSelector & forwardInvocation
  2. sel是方法编号,在read_images期间就编译进入了内存。
  3. imp就是我们函数实现指针,找imp就是找函数的过程。
  4. sel就相当于书本的目录tittle
  5. imp就是书本的⻚码。
  6. 查找具体的函数就是想看这本书里面具体篇章的内容
    1. 我们首先知道想看什么 ~ tittle(sel)。
    2. 根据目录对应的⻚码 ~ (imp)。
    3. 翻到具体的内容。

四、能否向编译后的得到的类中增加实例变量? 能否向运行时创建的类中添加实例变量?

  1. 不能向编译后的得到的类中增加实例变量。
    • 我们编译好的实例变量存储的位置在ro,一旦编译完成,内存结构就完全确定就无法修改。
    • 可以通过分类向类中添加方法和属性(关联对象)
  2. 只要没有注册到内存还是可以添加的,见下面代码:
    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这种方式是老版的调用方式,汇编跟源码查看真实的函数调用:

image.png

  • 真正调用的是objc_msgSendSuper2,它的结构跟objc_msgSendSuper是一样的:
    objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    

查看objc_msgSendSuper2的实现:

image.png

  • 可以看到调用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,后面的流程也就一样了。

图示:

image.png

内存偏移面试题 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调试验证:

image.png

图示:

image.png

内存偏移面试题 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>

结果分析:

  1. <ViewController: 0x7fc7b050a940>- (void)viewDidLoadid self压栈。
  2. viewDidLoad- (void)viewDidLoadSEL _cmd压栈。
  3. ViewController为结构体的class压栈。
  4. <ViewController: 0x7fc7b050a940>为结构体的receiver压栈。
  5. SSLPersonssl压栈。
  6. <SSLPerson: 0x7ffee876e1e8>person压栈。

什么东西才会压栈:

  1. 本方法的参数可以压栈,viewDidLoad(id self, SEL _cmd)
  2. 对于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原理图示:

image.png image.png

坑点一

上面的代码进行相关调用:

- (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__);
    }
    
  • 这时的selfSSLPerson实例,SSLPerson中并没有studentInstanceMethodSEL,找不到对应的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没有实现的话,运行程序,会发生死循环:

image.png

解决问题:

@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