Objective-C Runtime 从应用到原理

·  阅读 1180

面试大纲

是什么:OC 动态语言的运行时机制

objc_msgSend 消息过程:

  • 消息发送&查找
  • 动态方法解析
  • 消息转发 应用:
  • 关联对象
  • 方法混淆
  • 消息转发

1. 应用

1.1 关联对象 Associated Objects

应用场景:通过分类给已有的类添加成员变量

在分类中虽然可以写 @property,但不会自动生成私有属性和 set/get 方法的实现,只会生成 set/get 的声明,需要自己实现。

#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface NSObject (YHName)
@property (nonatomic, strong) NSString *name;
@end
@implementation NSObject (YHName)
- (NSString *)name {
    return objc_getAssociatedObject(self, @"name_key");
}
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @"name_key", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
复制代码
  • 关联对象实现原理: 一个实例对象对应一个 ObjectAssociationMap,Map 中存储着多个此实例对象的关联对象的 key 以及 ObjcAssociation,ObjcAssociation 中存储着关联对象的 value 和 policy 策略。
    关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的 map 用来存放每一个对象及其对应关联属性表格。
    如果设置关联对象为 nil,就相当于是移除关联对象。

1.2 方法混淆 Method Swizzling

(1) 方法添加

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
//cls:添加方法的类
//name:添加的方法名,@selector(methodName)
//imp:方法实现,函数入口,函数必须至少两个参数 self 和 _cmd
//types:参数以及返回值类型的字符串,需要用特定符号
复制代码

这样动态给某个类添加方法,相当于方法的懒加载机制,类中许多方法暂时用不到,那么就先不加载,等用到的时候再去加载方法。

(2) 方法替换

#import <objc/message.h>
@implementation UIImage (YHImage)
//load把类加载进内存的时候调用,只会调用一次
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
        Method yh_imageNamedMethod = class_getClassMethod(self, @selector(yh_imageNamed:));
        method_exchangeImplementations(imageNamedMethod, yh_imageNamedMethod);
    });
}

+ (UIImage *)yh_imageNamed:(NSString *)name {
	  //注意调用yh_imageNamed才不会死循环
    UIImage *image = [UIImage yh_imageNamed: name];
    //可加一些自定义处理
    return image;
}
@end
复制代码

1.3 方法调用 performSelector

performSelector: withObject: 可以向一个对象传递任何消息,且不需要在编译的时候声明这些方法(所以也有崩溃风险,避免崩溃可和responseToSelector 配合使用)。 本质就是 objc_msgSend

  • 带有定时器的 performSelector 方法:- (void)performSelector:(SEL)aSelector withObject:(id)arg; afterDelay:(NSTimeInterval)delay;
    • afterDelay 增加了一个在 NSDefaultRunLoopMode 模式下的定时器,所以在没有 runloop 的子线程无法执行;
    • aSelector 被添加到了队列的最后面,要等当前调用此方法的方法执行完毕后,selector 方法才会执行;
  • 应用:防止按钮被多次点击
//点击后设为不可被点击的状态,1秒后恢复
-(void)buttonClicked:(id)sender{
    self.button.enabled = NO;
    [selfperformSelector:@selector(changeButtonStatus)withObject:nilafterDelay:1.0f];//防止重复点击
}
-(void)changeButtonStatus{
    self.button.enabled =YES;
}
复制代码

1.4 遍历类的所有成员变量(私有API、字典转模型、自动归档解档)

两种字典转 model 实现的区别
KVC:遍历字典所有 key,在 model 中找对应属性名,以字典 key 为准;
Runtime:遍历 model 中所有属性名,去字典查找对应 key,以 model 属性为准;Runtime 方式代码如下:

+ (instancetype)modelWithDict:(NSDictionary *)dict {
    id objc = [[self alloc] init]; // 1.创建对应类的对象
    unsigned int count = 0; // count:成员属性总数
    // 获得成员属性列表和成员属性数量
    Ivar *ivarList = class_copyIvarList(self, &count);
    for (int i = 0 ; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSString *key = [propertyName substringFromIndex:1];
        id value = dict[key];
        // 获取成员属性类型
        NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
            // value是字典且value的成员属性不是系统类时才需要进行二级转换
            NSRange range = [propertyType rangeOfString:@"\""]; // @\"Mode\"去掉前面的@\"
            propertyType = [propertyType substringFromIndex:range.location + range.length];
            range = [propertyType rangeOfString:@"\""]; // Mode\"去掉后面的\"
            propertyType = [propertyType substringToIndex:range.location];
            // 获取需要转换类的类对象,将字符串转化为类名
            Class modelClass =  NSClassFromString(propertyType);
            if (modelClass) {
                value =  [modelClass modelWithDict:value]; // 返回二级模型赋值给value
            }
	}
        if (value) {
            [objc setValue:value forKey:key];
	}
    }
    return objc;
}
复制代码

1.5 使用消息转发解决NSTimer的循环引用问题

和 Runtime 相关的解决循环引用的方法:
将 target 分离出来独立成一个 WeakProxy 代理对象, NSTimer 的 target 设置为 WeakProxy 代理对象,WeakProxy 是传给timer对象的代理对象,所有发送到 WeakProxy 的消息都会被转发到传给timer的对象,可以达到 NSTimer 不直接持有对象的目的。

2. 原理:Object 对象

2.1 底层结构

每个对象本质都是一个存储指针的结构体,指针指向对象的类。

struct objc_object {
    struct objc_class *isa;
};
复制代码

[面试题] 一个 NSObject 对象占用多少内存?

一个 NSObjec 对象只有一个 isa 指针成员,指向 objc_class 结构体,指针所占用的内存是8个字节(64位)/4个字节(32位)

[面试题] 继承关系下占用内存情况?(64位)

@interface Person : NSObject {
    int _age;
}
@end
@implementation Person
@end

@interface Student : Person {
    int _no;
}
@end
@implementation Student
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%zd  %zd", class_getInstanceSize([Person class]), class_getInstanceSize([Student class]));
    }
    return 0;
}
复制代码

因内存对齐原因,都是16字节

  • Person:isa 8字节 + age 4字节 + 内存对齐 = 16字节
  • Student:isa 8字节 + age 4字节 + no 4字节 (已对齐)= 16字节

编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。
这个最宽的基本数据类型的大小作为对齐模数。 为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

  • 内存对齐总结:
  1. 前面的地址必须是后面的地址正数倍,不是就补齐
  2. 整个Struct的地址必须是最大字节的整数倍

3. 原理:Class 类

类对象 class 和元类对象 mete-class 类型都是 Class,底层都是指向 objc_class 结构体的指针

3.1 底层结构

struct objc_class {
    struct objc_class *isa;        //isa指针
    struct objc_class *superclass; //指向父类的指针
    cache_t cache;                 //方法缓存
    class_data_bits_t bits;        //通过掩码可以获得更多类信息
};
复制代码

objc_class 是继承 objc_object的,isa实际上是父类 objc_object提供的

  • class_rw_t 类(通过objc_objec的bits+掩码获得)
struct class_rw_t {
    const class_ro_t *ro;             //只读的类信息
    method_list_t *methods;           //方法列表
    property_list_t *properties;      //属性列表
    const protocol_list_t *protocols; //协议列表
	  ...//省略其他非关键成员
};
复制代码

methods、properties、protocols 都是可读写的二维数组,包括类和分类的内容。

  • class_ro_t类:只读,存储着成员变量信息和「非动态添加的」方法、属性、协议。
struct class_rw_t {
    const char *name;                     //类名
    const ivar_list_t *ivars              //成员变量列表
    method_list_t *baseMethodList;        //方法列表
    property_list_t *baseProperties;      //属性列表
    const protocol_list_t *baseProtocols; //协议列表
复制代码

一开始类的方法,属性,成员变量属性协议等等都是存放在class_ro_t中的,程序运行时需要将分类中的列表跟类初始的列表合并在一起的时,就将class_ro_t中的列表和分类中的列表合并起来存放在class_rw_t中。

3.2 Class 中的 isa 指针、superClass 指针指向

类继承关系:NyanCat : Cat : NSObject objctree.png 实例对象 Instance:存储isa指针、成员变量的值

  • isa指针指向类对象

成员变量的值是存储在实例对象中的,因为只有创建实例对象时才为成员变量赋值。
但成员变量的名字和类型只需要有一份就可以了,所以存储在class对象中。
类对象 Class:存储isa指针、superClass指针、属性&成员变量信息、对象方法&协议方法信息

  • isa指针指向元类对象
  • 子类的superClass指针指向父类,基类的superClass指针指向nil 元类对象 meta class:存储isa指针、superClass指针、类方法信息
  • 所有元类对象的isa指针都指向基类的元类对象(基类的元类对象的isa指针也指向自己)
  • 子类的元类对象的superClass指针指向父类的元类对象,基类的元类对象的superClass指针指向基类的类对象;

[面试题] isKindOfClass & isMemberClass

//继承关系:YHPerson是NSObject的子类
BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [[YHPerson class] isKindOfClass:[YHPerson class]];
BOOL res4 = [[YHPerson class] isMemberOfClass:[YHPerson class]];
复制代码

res1-4 分别是:YES ; NO ; NO ; NO;
原因:A 是类对象,B 是元类对象时才能相等,因此 res2/3/4 都是 NO
但res1,基类 NSObject 的元类对象的 superClass 指针指向基类对象NSObject,所以是 YES

  • 结合 isKindOfClass & isMemberClass 源码理解:
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
复制代码

知识点1:两个方法的逻辑区别

  • A isKindOfClass B :对A取类对象或isa指针,判断和其superClass,是否是B
  • A isMemberOfClass B :对A取类对象或isa指针,判断是否是B 知识点2:对调用者操作不同
  • 对于实例对象A,取的是类对象:person -> [Person class]
  • 对于类对象A,取的是isa指针指向的元类:[Person class] -> (通过isa指向) object_getClass([Person class]) 知识点3:基类的元类对象的superClass指针指向基类对象

[面试题] 调用Super时的输出类

//继承关系:YHPerson是NSObject的子类
//在YHPerson(子类)内调用如下方法
NSLog(@"%@", [self class]);
NSLog(@"%@", [super class]);
NSLog(@"%@", [self superclass]);
NSLog(@"%@", [super superclass]);
复制代码

输出:YHPerson, YHPerson, NSObject NSObject
原因:
当利用 super 调用方法时,编译器看到super这个标志会让当前对象去调用父类方法,本质还是当前对象在调用,只是去父类找实现。
super 仅仅是一个编译指示器。但是消息的接收者 receiver 依然是self,superclass 仅仅是用来告知消息查找从哪一个类开始(从父类的类对象开始去查找),最终在 NSObject 获取 isa 指针的时候,获取到的依旧是 self 的 isa。

3.3 Class 中的 method_t 结构体

struct method_t {
    SEL name;  //函数名(选择器)
    const char *types;  //编码字符串(返回值类型,参数类型)
    IMP imp; // 函数的具体实现,指向函数的指针(存储函数地址)
};
复制代码

不同类中相同的方法名的SEL是全局唯一的。

3.4 Class 中的 cache 方法缓存

用来缓存曾经调用过的方法,以提高方法的查找速度

struct cache_t {
    struct bucket_t *_buckets; // 哈希表,有多组key和value
    mask_t _mask; // 哈希表长度 -1
    mask_t _occupied; // 已经缓存的方法数量
};
复制代码

bucket_t是以数组的方式存储方法列表的:

struct bucket_t {
private:
    cache_key_t _key; // SEL作为Key
    IMP _imp; // 函数的内存地址作为value
};
复制代码

4. 原理:分类 Category

将 category 中的方法,属性,协议数据放在 category_t 结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
分类结构体中是不存在成员变量的,在分类中虽然可以写 @property,但不会自动生成私有属性和 set/get 方法的实现,只会生成 set/get 的声明,需要自己实现。
编译时,分类的方法,属性,协议列表被放在了类对象中原本存储的方法、属性、协议列表的前面,以保证分类方法优先调用。
所以本质上并不是覆盖本类方法,本类方法仍然存在。

[面试题] 分类与扩展的区别

  1. 扩展可以理解为匿名分类,一般直接写在主类文件内
  2. 扩展是在编译时加载的,分类是运行时加载的。

[面试题] 如果分类方法与原类方法重名时不想调用分类的实现,如何解决

更改方法查找的顺序,先查找原类对象的方法。

5. 原理:KVO

5.1 监听原理

对A1这个实例对象的变量a进行 addObserver 后,会修改A1实例对象的isa指针,从指向类对象A改为指向一个A的子类NSKVONotifying_A(通过Runtime动态创建的),子类拥有自己的set方法实现,会依次调用: willChangeValueForKey -> 原本的set实现 -> didChangeValueForKey
然后didChangeValueForKey会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法

动态生成的 NSKVONotifying_A 结构体:有isa、superClass、setAge方法、class方法

  • setAge 方法:对观察的变量重写的set方法
  • class 方法:重写,以隐藏自己的存在(返回再上一层),使 A 的实例调用 class 时仍返回 A 而非真实的 class NSKVONotifying_A

5.2 如何不改变值的情况下手动触发KVO

自己调用willChangeValueForKeydidChangeValueForKey,缺一不可。

[p1 willChangeValueForKey:@“age”];
[p1 didChangeValueForKey:@“age”];
复制代码

6. 原理:方法调用过程 objc_msgSend

调用方法的过程,在底层实际是 objc_msgSend 的方法调用,给方法调用者发送消息:objc_msgSend(对象, sel_registerName("方法名"))
其中传入的sel_registerName("方法名")实际就是日常使用的 @selector(方法名),都是一个 sel。

6.1 objc_msgSend 三个阶段总结

  1. 消息发送与查找
  2. 动态方法解析
  3. 消息转发 三个阶段都没找到方法 -> “unrecognized selector sent to instance” 报错崩溃

每个OC方法最终都是一个C函数,默认任何一个方法都有两个参数:
self : 方法调用者
_cmd : 调用方法编号

在源码内看 objc_msgSend 实现:objc-msg-arm64.s ENTRY -> END_ENTRY 通过汇编实现

6.2 阶段1:消息发送与查找

查找接收者的 class(由取 isa 指针得到)的方法缓存 list,若命中,直接调用方法;
若未命中,到接收者 class_rw_t 中的 methods 列表二分查找/遍历查找(取决于 methods 列表是否有序),若找到,填充到方法缓存并调用;
若未找到,依次一层层找各个父类(receiverClass通过superClass指针找到super class)的方法缓存和方法列表,如果找到父类缓存/方法,也填充到接收者 class 的缓存并调用;
若仍未找到,进入到下一环节动态方法解析。

6.3 阶段2:动态方法解析 Method Resolver

动态方法解析只会进行一次,如果已经解析过了,直接跳过进入消息转发

  • 若是元类对象:调用resolveClassMethod
  • 若是类对象:调用resolveInstanceMethod 无论开发者是否重写实现动态方法解析,都会 retry 回到消息发送的流程

应用:在resolveInstanceMethod (实例方法) / resolveClassMethod (类方法) 中动态添加方法

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) { //test:需要动态添加实现的方法
        Method method = class_getInstanceMethod(self, @selector(other));//other:想代替实现的其他方法; Method实际就是指向method_t结构体的指针
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEdcoding(method)); //self是类对象,不是元类对象
        //如果在resolveClassMethod内实现,第一个参数需要是元类对象 object_getClass(self)
        return YES; //返回YES代表有动态添加方法,不返回也不影响,内部没有用它判断
    }
    return [super resolveInstanceMethod:sel];
}
复制代码

6.4 阶段3:消息转发 Method Forward

  1. 调用 forwardingTargetForSelector 方法,可以重写这个方法转发给别的类处理,若 return 对象,则给该对象发送消息 objc_msgSend(对象, sel_registerName("方法名"));若这个方法返回空,进入下一步。
  2. 调用 methodSignatureForSelector 方法,获取一个方法签名(返回值类型&参数类型信息),可以在这里重写修改处理对象。
  3. 若 methodSignatureForSelector 返回不为空,调用 forwardInvocation 方法转发 Invocation。(可以重写修改 Invocation 参数,同样也是修改处理对象)

NSInvocation封装了一个方法调用,包括方法调用者、方法、方法参数
对所有方法做处理时消息转发比方法混淆更方便
ff64784c811744378aef147199204b81~tplv-k3u1fbpfcp-zoom-1.image.png


7. WWDC 2020 Runtime 优化

7.1 类数据结构优化:将 class_rw_t 其中动态的部分再拆分

class_rw_t中动态的部分拆分为class_rw_ext_t,节约了内存

7.2 方法列表存储的地址优化为相对地址

节约了内存

针对方法混淆这种需要用绝对地址的地方,使用一个全局映射表,维护混淆方法的绝对地址。

7.3 Tagged Pointer 格式变化

Tagged Pointer:通过在其最后一个 bit 位设置为特殊标记位,并且把数据直接保存在指针本身中。
优化:在 ARM64中从最低位标记改成最高位标记,这是一个对objc_msgSend 的优化,用最高位可以仅通过一次比较检查。

关于Runtime可供复习的精选文章

Objective-C 中的类和对象 | Garan no dou Objective-C 中的消息与消息转发 | Garan no dou

分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改