OC底层原理(16)底层面试题

217 阅读11分钟

【1】load 和 initialize顺序

load 方法调用

在类的加载中已经探究了load方法的调用顺序,现在做一个总结,在探究load方法注意点 类的load方法和分类的load方法

  • load方法调用是是通过load_images调用。过程中底层有两张表,一张是类的load方法表,一张是分类的load方法。类的load方法先于分类的load方法
  • 如果类之间不存在继承关系,那么优先加载的类,其load方法先调用。即按照编译顺序进行进行调用
  • 如果类之间存在继承关系,那么在load方法调用时,底层会先去查找父类的load方法的递归,然后将其添加到类的load方法表中。即父类的load方法先于类中的load方法调用
  • 分类之间的load方法调用是按照编译的顺序进行调用
  • load方法的调用顺序:父类 > > 分类 这个顺序是大体的排序,细节可以按照上面的顺序继续细分

initialize方法调用

initialize方法是第一次消息发送的时候调用,也是系统底层主动调用,在消息慢速查找是时进行调用

  • 显而易见initialize是在load方法之后调用的
  • initialize其实和普通的方法一样,如果主类和分类中的有相同的普通方法只会调用分类的方法
  • 如果类之间存在继承关系,在initialize方法调用底层处理中也是先递归调用父类的initialize方法
  • 如果父类实现了initialize方法,类没有实现,那么回去调用父类的initialize方法
  • initialize调用顺序:父类 > 分类 或者 父类 >

【2】Runtime是什么

  • runtime是由CC++汇编实现的一套API,为OC添加了面前对象,以及运行时的功能
  • 运行时是将数据类型的确定由编译时推迟到了运行时,比如分类
  • 平时编写的OC代码,在程序运行时的过程中,其实最终会转换成runtimeC语言代码,runtimeObject—C的幕后工作者

【3】方法的本质,sel是什么?IMP是什么?两者之间的关系是什么?

  1. 方法的本质:发送消息,消息会有以下几个流程

    • 快速查找流程(objc_msgSend) ~ cache_t缓存查找
    • 慢速查找递归自己和父类 ~ lookUpImpOrForward
    • 查到不到消息:动态方法决议~ resolveInstanceMethod
    • 消息快速转发 ~ forwardingTargetForSelector
    • 消息慢速转发 ~ methodSignatureForSelector & forwardInvocation
  2. sel是方法编号在read_images期间就编译进入了内存,sel也是一个结构体指针类型

  3. imp就是函数实现指针,找imp过程就是找函数的过程

  4. sel相当于书本的目录,imp就是书本的页码

  5. 查找具体的函数就是想看这本书⾥⾯具体篇章的内容

    • 我们⾸先知道想看什么 ~ tittle (sel)
    • 根据⽬录对应的⻚码 (imp
    • 通过页码定位具体的内容,方法实现

【4】能否向编译后的内存中添加实例变量?能否向运行时创建的类中添加实例变量

  • 不能向编译后的类中添加实例变量
  • 类没有注册到内存还是可以添加属性和方法 原因:编译好的实例变量存储在ro,一旦编译完成,内存结构就完全确定,无法修改

动态创建类和添加变量和方法

void printMyName(id self, SEL _cmd) {
    id name = [self valueForKey:@"name"];
    NSLog(@"name = %@", name);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 定义一个 YJTeacher 类,继承自 NSObject
        Class YJTeacher = objc_allocateClassPair(NSObject.class, "YJTeacher", 0);
        // 添加实例变量
        BOOL isSuccess = class_addIvar(YJTeacher, "name", sizeof(NSString *), 0, "@");
        isSuccess ? NSLog(@"添加变量成功") : NSLog(@"添加变量失败");
        // 添加方法
        class_addMethod(YJTeacher, @selector(printMyName), (IMP)printMyName, "v@:");
        // 注册 YJTeacher
        objc_registerClassPair(YJTeacher);

        // 实例化
        id t = [[YJTeacher alloc] init];
        [t setValue:@"张三" forKey:@"name"];
        [t performSelector:@selector(printMyName)];
    }
    return 0;
}

运行,可正常输出:

Xnip2022-07-21_17-33-31.png

【5】[self class][super class]的区别以及原理分析

案例分析测试:创建YJPerson类继承NSObject代码如下

@interface YJPerson : NSObject
@end
@implementation YJPerson
- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"-- %@ -- %@ --",[self class], [super class]);
    }
    return self;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 调用init方法
        YJPerson * person = [[YJPerson alloc] init];
    }
    return 0;
}

运行,输出:

2022-07-21 17:46:12.985317+0800 Runtime01[6659:156163] -- YJPerson -- YJPerson --

打印结果两个都是YJPerson[self class]的结果是YJPerson没有任何问题,[super class]为什么返回的也是YJPerson。很奇怪,下面来探究下class方法的源码

- (Class)class {
    return object_getClass(self);
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

object_getClass(self)中的self- (Class)class 方法的隐藏参数中的一个,这两个隐藏参数是id selfSEL _cmd。那么这个隐藏参数的self是哪里传过来的呢?方法的本质是消息发送通过objc_msgSend,在objc_msgSend方法中有两个参数:一个是消息的接收者self,一个是方法名sel,现在很清楚了[self class]的消息接收者是self也就是YJPerson类的实例化对象所以object_getClass(self)返回的就是YJPerson

现在只要搞清楚[super class]这个消息的接收者是谁那么这个问题就迎刃而解了。通过clang把YJPerson.m文件生成YJPerson.cpp文件

Xnip2022-07-21_18-00-18.png

[super class]方法是通过objc_msgSendSuper进行消息发,参数的类型是__rw_objc_super类型和还有一个就是SEL。其实这里可以简单理解super的作用其实就是向父类发送消息。那么super可以说是一个关键字也可以说是标识符。在cpp文件中全局搜索__rw_objc_super

Xnip2022-07-21_18-01-48.png

__rw_objc_super是个结构体类型,里面有两个变量objectsuperClass,此时在结合objc_msgSendSuper方法的参数,在objc源码中全局搜索objc_msgSendSuper

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

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 */
};

objc_super结构体类型对应cpp文件中的__rw_objc_super其实消息的接收者还是receiver就是self所以[super class]返回的就是YJPerson类,objc_msgSendSuper的作用就是去父类开始查找方法仅此而已,并不是返回父类。这就是为什么重写init方法,需要[super init]而不是[self init][self init]就会死递归 有点人好奇可以不写[super init]答案是可以,但是如果不写就不能继承父类的属性和方法,父类的一些自带的就不能用所以最好写[super init]

【6】内存平移案例分析

上代码:

@interface YJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation** YJPerson
- (void)say1 {
    NSLog(@"%s, name = %@", __func__ , self.name);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YJPerson *p1 = [[YJPerson alloc] init];
        p1.name = @"张三";
        [p1 say1];

        Class cls = YJPerson.class;
        void *p1= &cls;
        [(__bridge id)p1 say1];
    }
    return 0;
}

[p1 say1] 这是很常规的操作嘛,没啥疑问。 void *p2 = &cls; [(__bridge id)p2 say1]; 这是什么骚操作?能运行么?来,咱试试:

Xnip2022-07-21_21-32-54.png

这什么👻,怎么输出个 name = <YJPerson: 0x108f23eb0>,不要捉急,接下来咱好好分析分析

[(__bridge id)p2 say1] 调用

[(__bridge id)p2 say1] 为啥它就能调用了?首先我们要知道方法调流程:对象通过 isa 找到所属类,然后去类的 methodsList 里 。。。[p1 say]没有疑问,因为 p1 就是 YJPerson 对象嘛。到此p2为啥能调用,要么它也是YJPerson 对象,要么它本身就是指向 YJPerson类的指针。下面进行源码调试下

Xnip2022-07-21_22-00-50.png

消息查找图:

未标题-1.png

图中可以看出实例对象是通过isa去找到,然后在类中进行方法查找,而p2也是一个指针,这个指针指向了cls,而cls里面存放的是YJPerson类的地址,到最后都找打了YJPerson

[(__bridge id)p2 say1] 打印结果

Xnip2022-07-21_21-32-54.png

图中显示[p1 say1]的调用结果就是name的真实值,而[(__bridge id)p2 say1]的调用结果是一个指针地址,我们知道实例对象中的成员变量都是存储在实力对象的内存中正常情况下是通过内存偏移来获取对应的值

Xnip2022-07-21_23-55-01.png

在栈空间中地址是高地址到低地址即先进后出,先进入栈中的地址是高于后进入栈中的地址,所以最后打印的是指针p1。其实现在回头看内存平移很简单,只要弄清楚原理,一切都不是问题

结构体成员/函数参数入栈顺序

入栈顺序就是看谁的地址大,地址大的就是先入栈的

Xnip2022-07-22_00-33-44.png

lldb调试很明显可以看出

  • 先入栈的地址大于后入栈的地址
  • 函数参数:按参数顺序入栈,谁在前谁先入栈
  • 结构体:按成员倒序入栈,谁在后谁先入栈
    • &p1&yj_stt地址相差16个字节,因为yj_stt有两个指针类型成员,占16字节。&num1地址小于&num2,说明num2先入栈

【7】 Method Swizzling方法交换

方法交换的原理

每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是由编译器编译时为类创建的。在这个结构体中又包括了,指向其父类的指针以及Dispatch tableDispatch table是一张SELIMP的对应表。

也就是说方法编号SEL最后还是要通过Dispatch table表寻找到对应的IMPIMP就是一个函数指针,然后执行这个方法。

  • 方法编号SEL方法实现IMP的对应关系

    image.png

  • 方法交换后对应关系

    image.png

    • oriSEL的方法实现变成了swiIMP
    • swiSEL的方法实现变成了oriIMP
  • 方法交换的方式

// 类中获取oriSEL对应的方法实现
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
// 获取swiSEL对应的方法实现
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 将两个方法实现进行交换,
method_exchangeImplementations(oriMethod, swiMethod);

方法交换案例分析

是否会递归调用?

创建一个YJStudent类,类中有两个实例方法,studentSayyj_studentSay,在load方法中对两个方法进行交换,同时,yj_studentSay的实现中再次调用yj_studentSay方法

// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    YJStudent *s = [YJStudent new] ;
    [s studentSay];
}
@end

// YJStudent.m
@implementation YJStudent
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 类中获取oriSEL对应的方法实现
        Method oriMethod = class_getInstanceMethod(self, @selector(studentSay));
        // 获取swiSEL对应的方法实现
        Method swiMethod = class_getInstanceMethod(self, @selector(yj_studentSay));
        // 将两个方法实现进行交换,
        method_exchangeImplementations(oriMethod, swiMethod);
    });
}

- (void)studentSay {
    NSLog(@"调用了 -- %s", __func__);
}
- (void)yj_studentSay {
    [self yj_studentSay];
    NSLog(@"调用了 -- %s", __func__);
}
@end

yj_studentSay中再次调用该方法,是否会引起递归调用呢?运行项目试试。。。

Xnip2022-07-22_14-05-15.png

并没有引起递归,因为进行了方法交换,所以调用对象方法studentSay会找到yj_studentSay的方法实现。而调用yj_studentSay会去找studentSay的方法实现,如图

Xnip2022-07-22_14-16-36.png

交换父类的方法

YJStudent类 继承 YJPerson类,YJPerson 中有实例方法personSay,在YJStudent类的load方法中进行方法交换,将studentSay方法交换成父类中的personSay方法。

// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    YJStudent *s = [YJStudent new] ;
    [s personSay];
}
@end

// YJPerson.m
@implementation YJPerson
- (void)personSay {
    NSLog(@"调用了 -- %s", __func__);
}
@end

// YJStudent.m
@implementation YJStudent
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 类中获取oriSEL对应的方法实现
        Method oriMethod = class_getInstanceMethod(self, @selector(personSay));
        // 获取swiSEL对应的方法实现
        Method swiMethod = class_getInstanceMethod(self, @selector(yj_studentSay));
        // 将两个方法实现进行交换,
        method_exchangeImplementations(oriMethod, swiMethod);
    });
}
- (void)yj_studentSay {
    [self yj_studentSay];
    NSLog(@"调用了 -- %s", __func__);
}
@end

YJStudent对象是否能够成功调用personSay方法? 运行程序看看结果,见下图:

Xnip2022-07-22_14-31-53.png

成功调用了,因为子类对象调用父类方法personSay,我们在学习消息发送的原理时已经知道,其会进行慢速方法查找找到父类方法。但是此时父类方法对应的方法实现已经被交换成了,子类的yj_studentSay方法,所有会执行子类的yj_studentSay方法实现。于此同时子类中调用yj_studentSay方法,最终的方法实现是父类的personSay方法。

这样看着貌似没啥问题,初始化YJPerson实例,来调用 personSay 试试:

Xnip2022-07-22_14-37-20.png

报错,why? 首先父类调换用 personSay 方法会执行子类中的yj_studentSay实现,但是但是此时又调用yj_studentSay方法,而此时的调用者是YJPerson对象,父类中并没有yj_studentSay方法的实现。所以方法找不到,进而报错。

方法交换设计思路

通过上面的案例,为避免上面案例的问题,总结以下实现思路:

+ (void)yj_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时,添加oriSEL的方法,实现为swiMethod
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        // 替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));
    }
    
    
    // 如果`cls`本身有`oriSEL`方法(继承父类的不算),则添加失败
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    // 添加成功,
    if (didAddMethod) {
        // 走到这儿说明`cls`本身没有`oriSEL`,但通过`class_addMethod`已添加上了,并且`oriSEL`的实现是 `swiMethod`
        // 将`swizzledSEL`替换为父类的实现
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }
    // 添加失败
    else {
        // 走到这儿,说明`cls`本身就有 `oriSEL` 和 `swizzledSEL` 直接交换即可
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}
  • 先判断 cls 本身是否有要交换的方法 oriSEL, 通过 didAddMethod 的成功与否来判断有没有

  • didAddMethod 成功,说明 cls 本身没有,但同时也给 cls 添加了 oriSEL 并且实现指向了 swiMethod。然后使用 class_replaceMethod接口,来替换 swizzledSEL 的实现为 oriMethod,注意这里是 替换,不印象父类的 ori

  • didAddMethod 失败,说明 cls 本身有,是自己的直接交换,不会影响兄弟姐妹、子、父类的 的使用