iOS底层面试题

2021-09-10  本文已影响0人  冼同学

runtime的学习整理

对象

消息

应用程序加载、类、分类初始化

相关面试题

1. loadinitialize方法的调用原则和调用顺序?

具体的实现以及底层逻辑在类的加载(上)-- _objc_init&read_images
中。

补充c++构造函数

2.Runtime是什么?

3.⽅法的本质,sel是什么?IMP是什么?两者之间的关系⼜是什么?

4.能否向编译后的得到的类中增加实例变量?能否向运⾏时创建的类中添加实例变量?

// 使用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,并在LGTeacherinit初始化方法中,调用了[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

案例分析:
很清楚LGTeaherLGPerson都没有实现class方法,那么根据消息发送的原理,他们最终都会调用到NSObject的实例方法class,该方法实现如下:

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

调用方法的本质是发送消息objc_msgSend,并且有两个隐藏参数,分别是id selfSEL sel,这里的隐藏参数self就是我们要分析的类型。

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 receiverClass super_class,其中super_class表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class],其内部会调用objc_msgSendSuper方法,并且会传入参数objc_super,其中receiverLGTeacher对象,super_classLGTeacher类通过class_getsuperclass获取的父类,也就是要第一个查找的类。

通过下符号断点--objc_msgSendSuper2,查看寄存器,其中第一个地址为发放的第一个隐藏参数,也就是objc_super,通过类型强制,该结构体封装的recevierLGTeachersuper_classLGPerson,具体看下图:

查看寄存器
得出结论:[super class]的接收者依然是LGTeacher对象,去调用父类的方法。

最后查看运行结果:

查看运行结果
果然输出都是LGTeacher!!!

补充:objc_msgSendSuper为什么会调用到了objc_msgSendSuper2
通过 Objc-818.2源码查看的出:


全局搜索objc_msgSendSuper,进入汇编实现流程中,在汇编流程中,最终会调用objc_msgSendSuper2,见下图:
调用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:是否能够调用成功?

分析得出:两者调用的入口是一致的,从同一个地址开始进行方法查找流程,肯定是可以调用到的,person除了有地址,还有内存数据结构kc只有一个地址,是一个伪装的person对象。请看下图:

查找流程

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

查看kc指向class

最后运行代码:


运行代码

案例扩展
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调试,请继续走!

lldb调试
经过调试可以知道person进行地址平移获取属性kc_name,此数据结构是在中,而kc只是一个地址,获取kc数据结构只是输出了其在中的数据信息。

引申出压栈的概念
通过上面的案例分析,可以知道根本原因是栈中地址平移的问题,那么在程序运行过程中,压栈逻辑是怎样的呢?先入后出,这个比较清楚,那结构体是如何压栈的呢,函数调用中参数的压栈逻辑又是怎样的?

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


有上面可以得出:

补充:runtime面试题持续更新中哦。。。敬请期待!

上一篇 下一篇

猜你喜欢

热点阅读