美文网首页
面试题分析

面试题分析

作者: Kates | 来源:发表于2021-08-03 15:50 被阅读0次

    load和initialize方法的调用原则和调用顺序?

    1. load方法的调用时在dyld加载程序的时候调用,在main函数之前,调用顺序:父类,子类,分类,如果有多个分类,看谁先编译,编译的顺序可以通过Build Phases -> Compile Sources设置顺序
    2. initialize 类第一次发送消息的时候调用,调用顺序先调用父类的,在调用子类的

    Runtime是什么?

    Runtime是由C和C++汇编实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能
    运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时,如类扩展和分类的区别
    平时编写的OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代码,Runtime 是 Object-C 的幕后⼯作者

    super相关面试题

    @interface HFPerson : NSObject
    @end
    @interface HFTeacher : HFPerson
    @end
    @implementation HFTeacher
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            NSLog(@"%@---%@", [self class], [super class]);
        }
        return self;
    }
    
    @end
    
    打印的结果为
     HFTeacher---HFTeacher
    

    是不是觉得很奇怪,[self class]打印HFTeacher能理解, [super class]就有点不能理解了
    我们通过汇编来看看[super class] 底层调用的方法,前面的[self class]调用objc_msgSend方法而[super class]调用的是objc_msgSendSuper2

    image.png
    汇编中我们可以看到[super class]底层是通过objc_msgSendSuper2来发送消息的\
    id _Nullable
    objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    
    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_msgSendSuper2的接收者在objc_super结构体里的receiver,那这个receiver会是self吗?带着这个疑问我们调试看看?

    image.png
    首先我们断点来到这边
    然后开始lldb打印寄存器x0地址
    image.png
    x0地址的首地址就是self,也就是说receiver = self,接收对象还是self,也就是通过self去调用父类的方法,然后我们再来看看父类的class方法
    我们已知这边的self是实例对象,所以调用的是父类的实例方法也就是
    - (Class)class {
        return object_getClass(self);
    }
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    

    这边的self是HFTeacher实例对象,所以返回的也就是实例对象的isa-> HFTeacher

    内存平移

    接下来我们在来看一道更有意思的面试题

    @interface HFPerson : NSObject
    
    @property (nonatomic, strong) NSString *hf_name;
    
    - (void)saySomething;
    
    @end
    
    @implementation HFPerson
    - (void)saySomething{
        NSLog(@"%s - %@",__func__,self.hf_name);
    }
    
    - (void)sayHello {
        NSLog(@"123123---%@", self);
    }
    
    @end
    首先定义一个类HFPerson
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        HFPerson *person = [[HFPerson alloc] init];
        [person sayHello];
        Class cls = [HFPerson class];
        void *p = &cls;
        [(__bridge id)p sayHello];
    }
    
    结果输出:
    2021-08-03 14:34:35.432347+0800 内存平移[3075:666318] 123123---<HFPerson: 0x2824f0920>
    2021-08-03 14:34:35.432440+0800 内存平移[3075:666318] 123123---<HFPerson: 0x16d9c1c70>
    

    发现都可以调用到sayHello,接下来我们来分析一下
    首先[person sayHello]是实例对象调用方法,能够输出是正常,但是[(__bridge id)p sayHello]为什么也能够调用呢,我们要清楚一点的是,方法是存放在哪里,对象的方法是存放在类里面的,对象能够找到方法是通过对象的首地址isa获取方法。而现在我们Class cls = [HFPerson class] void *p = &cls 这边的p指针指向的就是HFPerson的首地址isa,所以这边也能找到方法并执行调用。
    接下来我们变换一下:

    - (void)sayHello {
        NSLog(@"%@---%@", self, self.hf_name);
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        HFPerson *person = [[HFPerson alloc] init];
        person.hf_name = @"hf";
        [person sayHello];
        Class cls = [HFPerson class];
        void *p = &cls;
        [(__bridge id)p sayHello];
    }
    输出结果:
    2021-08-03 14:46:57.475086+0800 内存平移[3090:669951] <HFPerson: 0x2838a3880>---hf
    2021-08-03 14:46:57.475187+0800 内存平移[3090:669951] <HFPerson: 0x16f4f9c70>---<HFPerson: 0x2838a3880>
    

    对于第一条打印结果就不解释了,直接来看看第二条。
    首先我们知道p指针指向的是HFPerson对象的isa,所以他也可以通过isa来查找到方法并调用,而这边的hf_name是对象的属性,self.hf_name调用的是hf_nameget方法,get方法实现实际就是通过实例对象的指针偏移获取,目前我们只有一个成员变量,所以要获取到hf_name需要偏移8个字节(前面还有个isa指针),所以p指针也会偏移8个字节,但是他是往哪里偏移呢?我们来看一幅图

    image.png
    通过lldb我们可以清楚知道是往高地址偏移
    p指针如何往高地址偏移呢?也就是偏移后指向哪里?
    我们还是lldb查看一下
    image.png
    lldb看到,person地址就是p地址偏移8字节后的地址。其实也不难理解,personcls都是栈上的变量,而这边我们也得出栈地址是从高位开始分配。
    接下来我们在探究一下如果继续往上偏移打印的会是什么呢?
    @interface HFPerson : NSObject
    @property (nonatomic, strong) NSString *hf_data1;
    @property (nonatomic, strong) NSString *hf_name;
    @end
    输出:
    2021-08-03 15:12:11.497457+0800 内存平移[3119:677027] <HFPerson: 0x16da01c70>---<ViewController: 0x147006ca0>
    
    @interface HFPerson : NSObject
    @property (nonatomic, strong) NSString *hf_data1;
    @property (nonatomic, strong) NSString *hf_data2;
    @property (nonatomic, strong) NSString *hf_name;
    @end
    输出:
    2021-08-03 15:12:55.672174+0800 内存平移[3125:677782] <HFPerson: 0x16eec1c70>---ViewController
    

    通过添加两个成员属性和三个属性看到打印的结果都不一样,两个成员属性,需要偏移16字节,而这边打印的是<ViewController: 0x147006ca0>三个属性的时候偏移24个字节打印的是ViewController,<ViewController: 0x147006ca0>似乎是个对象,而ViewController似乎是个类名。首先我们要先知道目前栈里面到底有哪些对象。viewDidLoad函数隐含着两个参数:selfsel,而这两个参数都是会入栈的,[super viewDidLoad]; 这边我们知道实际调用的是objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...),所以栈里面会有struct objc_super * super这个参数.
    所以刚刚的<ViewController: 0x147006ca0>和ViewController应该是struct objc_super * super的值,这个我们可以通过lldb来验证一下

    image.png
    通过lldb我们就可以看到偏移16个字节的时候打印的<HFPerson: 0x16f065c70>---<ViewController: 0x101300b30>就是struct objc_super * super里面的receiver当我们添加三个成员属性的时候打印的是struct objc_super * super里面的super_class
    可能这时候又有一个疑问,ViewController的super_class不是UIViewController吗?
    image.png
    注意:这边评估注释:/* super_class is the first class to search */,super_class是第一个开始查找的类,并没有说是父类,而第一个开始查找的类是当前类。
    通过上面分析我们也可以得出,结构体成员变量入栈顺序是从后面往前,也就是越后面的成员越先入栈用一幅图来表示
    image.png

    相关文章

      网友评论

          本文标题:面试题分析

          本文链接:https://www.haomeiwen.com/subject/hinsvltx.html