iOS底层学习——Runtime学习整理

1,502 阅读9分钟

Runtime学习整理

对象

消息

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

相关面试题

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

  1. load方法

    • load方法在应用程序加载过程中(dyld)完成调用,在main函数之前
    • 在底层进行load_images处理时,维护了两个load加载表,一个类的表,另一个为分类的表,优先对类的load方法发起调用
    • 在对类load方法进行处理时,进行了递归处理,以确保父类优先被处理
    • 所以load方法的调用顺序为父类、子类、分类
    • 而分类中load方法的调用顺序根据编译顺序为准
  2. initialize方法

    • initialize在第一次消息发送的时候调用,所以load先于initialize调用
    • 分类的⽅法是在类realize之后attach进去的插在前⾯,所以如果分类中实现了initialize方法,会优先调⽤分类的initialize方法
    • initialize内部实现原理是消息发送,所以如果子类没有实现initialize会调用父类的initialize方法,并且会调用两次
    • 因为内部同时使用了递归,所以如果子类和父类都实现了initialize方法,那么会优先调用父类的,在调用子类的

具体底层实现原理见load和initialize分析

  1. 补充c++构造函数

    • 在分析dyld之后,可以确定这样的一个调用顺序,load->c++->main函数

    • 但是如果c++写在objc工程中,在objc_init()调用时,会通过static_init()方法优先调用c++函数,而不需要等到_dyld_objc_notify_registerdyld注册load_images之后再调用

    • 同时,如果objc_init()自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用的情况

2.Runtime是什么?

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

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

  1. ⽅法的本质:发送消息,消息会有以下⼏个流程:
    1. 快速查找 (objc_msgSend)~ cache_t 缓存消息
    2. 慢速查找~ 递归⾃⼰或⽗类 ~ lookUpImpOrForward
    3. 查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod
    4. 消息快速转发 ~ forwardingTargetForSelector
    5. 消息慢速转发 ~ methodSignatureForSelectorforwardInvocation
  2. sel是⽅法编号,在read_images期间就编译进⼊了内存
    • typedef struct objc_selector *SEL;
  3. imp就是我们函数实现指针,找imp就是找函数的过程
  4. sel就相当于书本的⽬录tittle
  5. imp就是书本的⻚码
  6. 查找具体的函数就是想看这本书⾥⾯具体篇章的内容
    1. 我们⾸先知道想看什么 ~ tittle (sel)
    2. 根据⽬录对应的⻚码 (imp
    3. 翻到具体的内容 方法实现

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

  1. 不能向编译后的得到的类中增加实例变量

    • 我们编译好的实例变量存储的位置在ro,⼀旦编译完成,内存结构就完全确定;
    • 可以通过分类向类中添加方法和属性(关联对象)
  2. 可以向运行时创建的类中添加实例变量,只要内没有注册到内存还是可以添加

    可以通过objc_allocateClassPair在运行时创建类,并向其中添加成员变量和属性,见下面代码:

    // 使用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@:");
            
    

4.[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

分析思路

首先确定,当前LGPersonLGTeacher都没有实现class方法,那么根据消息发送的原理,他们最终都会调用到NSObject的实例方法class,该方法的方法实现是:

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

也就是说这两个方法都会返回self对应的类,那么self是谁呢?我们在分析方法的本质时知道,调用方法的本质是发送消息objc_msgSend,并且有两个隐藏参数,分别是id selfSEL sel,这里的隐藏参数self就是我们要分析的类型。

  • [self class]输出是LGTeacher,这个没有什么问题!因为消息的发送者是LGTeacher对象,通过消息发送机制,找到NSObejct并调用class方法,但是消息的接受者没有发生改变,依然是LGTeacher对象!

  • [super class]输出的是呢?同样的方式clang一下,查看cpp中底层实现原理是怎样的?

    image.png

    super关键字,在底层最终使用了objc_msgSendSuper方法,同时其接受者是(id)self,全局搜搜objc_msgSendSuper的逻辑,见下图:

    image.png

    objc_super结构体如下:

    /// Specifies the superclass of an instance. 
    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。见下图:

    image.png

    就是说:[super class]的接受者依然是LGTeacher对象,去调用父类的方法。

查看运行结果:

image.png

补充:

调用objc_msgSendSuper,实际却调用了objc_msgSendSuper2为什么呢?

image.png

全局搜索objc_msgSendSuper,进入汇编实现流程中,在汇编流程中,最终会调用objc_msgSendSuper2,见下图:

image.png

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. 分析思路

    首先方法调用的本质是发送消息,通过对象的isa找到类地址,进行地址平移,通过sel找到对应的方法实现imp

    • [person saySomething];此种方式肯定是可以的

      此流程的原理是什么?通过person对象的isa指针找到对应的类,在类中进行地址平移,首先在cache_t中快速查找,如果找不到,则在方法列表以及父类的方法列表中查找,总结一下就是:以类的地址作为入口,进行地址平移,最终找到对应的imp

    • [(__bridge id)kc saySomething];是否可以呢?

      首先Class cls = [LGPerson class];cls是什么?cls是一个指针,Class的定义是一个指针,指向一个objc_class的指针,这里就是指向LGPerson类。将cls的地址赋值给kc,此时kccls的地址,也指向了类。

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

    image.png

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

    image.png

    运行验证,两个都可以调用成功。见下图:

    image.png

  2. 扩展案例

    在上面案例的基础上进行修改,saySomething方法LGPerson对象的第一个属性,中输出见下面代码:

    - (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
    

    此时运行结果又是怎样呢?

    • [person saySomething];调用后输出结构没有什么疑问
    • [(__bridge id)kc saySomething];的输出结构是怎样的呢?见下图:

    根据lldb调试可以发现,person进行地址平移获取属性kc_name,此数据结构是在堆中,而kc只是一个地址,获取kc数据结构只是输出了其在栈中的数据信息。见下图:

    image.png

  3. 结构体验证逻辑

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

    • 压栈,地址从大到小,新进去的地址大

    image.png

    • 添加结构体,查看栈中的地址

    image.png

    那么此时三个参数在栈中的存储顺序应该是下图:

    image.png

    上图再结合输出的地址,我们可以发现此时结构体占用16个字节,那么结构体中元素的存储顺序是怎样的呢?见下图:

    image.png

    通过lldb输出结构体中两个属性的地址,发现,num1在num2的上面,所以在压栈过程中,按照下图中的方式进行的:

    image.png

  4. 函数参数压栈顺序

    通过下面的案例进行分析:

    image.png

    从上图中我们可以发现几个问题:

    • viewDidLoad方法中person指针的地址和kcFunctionperson指针地址是不一样的,虽然他们都执行了同一片堆区
    • 根据指针的地址发现,参数在压栈时是根据参数的顺序进行的,第一个参数先入栈,然后依次压栈

Runtime面试题,持续更新……