Runtime

588 阅读16分钟

runtime 用来做什么?

  • 在程序运行过程中,动态的创建类,动态添加、修改这个类的属性和方法;
  • 遍历一个类中所有的成员变量、属性、以及所有方法
  • 消息传递、转发

案例

  • 给系统分类添加属性、方法
  • 方法交换
  • 获取对象的属性、私有属性
  • 字典转换模型
  • KVC、KVO
  • 归档(编码、解码)
  • NSClassFromString class<->字符串
  • block
  • 类的自我检测

Rumtime是Objective-C语言动态的核心,Objective-C的对象一般都是基于Runtime的类结构,达到很多在编译时确定方法推迟到了运行时,从而达到动态修改、确定、交换...属性及方法

isa指针

isa:

  • 是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类
  • Class(类对象)里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。

同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。

在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息 从64bit开始,isa需要进行一次位运算,才能计算出真实地址(通过 isa & ISA_MSAK可以查看isa指向类信息)

实例对象、类、元类关系图.png

isa 指针有两种类型

  • 纯指针,指向内存地址
  • NON_POINTER_ISA,除了内存地址,还存有一些其他信息

isa_t

initIsa 方法中,我们可以看到 isa = isa_t((uintptr_t)cls);isa 的数据结构其实为 isa_t,然后我们再进入 isa_t 看一下。

isa_t源码

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
# if __arm64__
    struct {
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
#endif
};

# elif __x86_64__
    struct {                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
}

参照arm64架构下 ,ISA_BITFIELD我们来看看每个字段都存储了什么内容 , 以便更深刻的理解对象的本质。

成员含义
nonpointer1bit表示是否对 isa 指针开启指针优化。 0:纯 isa 指针;1:不止是类对象地址。isa 中包含了类信息、对象的引用计数等
has_assoc1bit标志位: 表明对象是否有关联对象。0:没有;1:存在。没有关联对象的对象释放的更快
has_cxx_dtor1bit标志位: 表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快
shiftcls33bit存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
magic6bit用于调试器判断当前对象是真的对象还是没有初始化的空间 , 固定为 0x1a
weakly_referenced1bit标志位:用于表示该对象是否被别ARC对象弱引用或者引用过。没有被弱引用的对象释放的更快
deallocating1bit标志位: 用于表示该对象是否正在被释放
has_sidetable_rc1bit标志位: 用于标识是否当前的引用计数过大 ( 大于 10 ) ,无法在 isa 中存储,则需要借用sidetable来存储,标志是否有外挂的散列表
extra_rc19bit实际上是对象的引用计数减 1 . 比如,一个 object 对象的引用计数为7,则此时 extra_rc 的值为 6

KVC

键值编码是一种机制,该机制用于间接访问对象的属性,而不是通过调用访问器方法或通过实例变量直接访问它们,而是使用字符串来标识属性。

当调用 setValue:forKey: 设置属性 value 时,其底层的执行流程为

  1. 首先查找是否有这三种 setter 方法,按照查找顺序为 set<Key>: -> _set<Key> -> setIs<Key>

    • 如果有其中任意一个 setter 方法,则直接设置属性的 value(主注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)

    • 如果都没有,则进入 2

  2. 如果没有第一步中的三个简单的 setter 方法,则查找 accessInstanceVariablesDirectly 是否返回 YES

    • 如果返回 YES,则查找间接访问的实例变量进行赋值,查找顺序为:_<key> -> _is<Key> -> <key> -> is<Key>

      • 如果找到其中任意一个实例变量,则赋值

      • 如果都没有,则进入 3

    • 如果返回NO,则进入 3

  3. 如果 setter 方法 或者 实例变量都没有找到,系统会执行该对象的 setValue:forUndefinedKey: 方法,默认抛出 NSUndefinedKeyException 类型的异常

KVO

KVO 的实现过程实际上是利用了 OCruntime 机制,当一个实例对象(比如上面的 self.person)添加观察者时,底层根据该实例对象所属的类动态添加了一个类(动态添加的类名就是在原来类的类名前加上NSKVONotifying_前缀),这个类是继承自原来的类的。上面实例的底层实现过程如下:

  1. 实例对象 isa指针 的指向在注册 KVO 观察者之后,由原有类更改为指向中间类

  2. 中间类重写了观察属性的 setter 方法、classdealloc_isKVOA 方法

    • 中间类重写了观察属性的 setter 方法是调用的 setName: 方法,前面说了 setName: 方法 被重写了,所以实际上调用的是 _NSSetObjectValueAndNotify 这个方法。这个方法实现苹果是没有开源的,无法得知其具体实现,不过可以猜出其实现流程大致如下: 首先调用 [self willChangeValueForKey:@"name"]; 这个方法。 然后调用原先的 setter 方法的实现(比如 _name = name; ); 再调用 [self didChangeValueForKey:@"name"]; 这个方法。 最后在 didChangeValueForKey: 这个方法中调用观察者的 observeValueForKeyPath: ofObject: change: context: 方法来通知观察者属性值发生了变化。
  3. dealloc 方法中,移除 KVO 观察者之后,实例对象 isa指针 指向由中间类更改为原有类

  4. 中间类从创建后,就一直存在内存中,不会被销毁

iOS中isKindOfClass和isMemberOfClass的区别

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

打印结果:

2020-09-15 22:38:50.139130+0800 KCObjc[23825:541164]  
 re1 :1
 re2 :0
 re3 :0
 re4 :0
2020-09-15 22:38:50.139506+0800 KCObjc[23825:541164]  
 re5 :1
 re6 :1
 re7 :1
 re8 :1

- (BOOL)isKindOfClass 对象方法

第一次是获取对象类 与 传入类对比,如果不相等,后续对比是继续获取上次 类的父类 与传入类进行对比

  • re5: NSObject对象的类 - NSObject 类NSObject 类 相等,返回 1
  • re7: LGPerson 对象的类 - LGPerson 类LGPerson 类 相等,返回 1
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isKindOfClass 类方法

第一次比较是 获取类的元类 与 传入类对比,再次之后的对比是获取上次结果的父类 与 传入 类进行对比

  • re1: NSObject 类NSObject 元类 不相等,然后再比较 NSObject 类NSObject 元类的父类,相等,返回 1
  • re3: LGPerson 类LGPerson 元类 不相等,然后再比较 LGPerson 类LGPerson 元类的父类 - LGPerson 根元类 不相等,然后再比较 LGPerson 类LGPerson 根元类 的父类 - NSObject 类 不相等,然后再比较 LGPerson 类NSObject 类 的父类 - nil 不相等,返回 0
+ (BOOL)isKindOfClass:(Class)cls {
    // 类 vs 元类
    // 根元类 vs NSObject
    // NSObject vs NSObject
    // LGPerson vs 元类 (根元类) (NSObject)
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isMemberOfClass 对象方法

获取对象的类,与 传入类对比

  • re6: NSObject 对象的类 - NSObject 类NSObject 类 相等,返回 1
  • re8: LGPerson 对象的类 - LGPerson 类LGPerson 类 相等,返回 1
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isMemberOfClass 类方法

获取类的元类,与 传入类对比

  • re2: NSObject 类NSObject 元类 不相等,返回 0
  • re4: LGPerson 类LGPerson 元类 不相等,返回 0
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

Runtime 的方法缓存?存储的形式、数据结构以及查找的过程?

cache_t 增量扩展的哈希表结构。哈希表内部存储的 bucket_tbucket_t中存储的是 SELIMP 的键值对。

  • 如果是有序方法列表,采用二分查找
  • 如果是无序方法列表,直接遍历查找 类(Class)的本质是一个结构体 ,结构体内部结构如下 :
typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_class : objc_object {
    // Class ISA;
    Class superclass; // 父类指针
    cache_t cache;             //  方法缓存存储数据结构
    class_data_bits_t bits;    // bit 中存储了属性,方法等类的源数据
    class_rw_t *data() const {
        return bits.data();
    }
    ...
}

cache_t 结构体

struct cache_t {
    struct bucket_t * _buckets; // 缓存数组,即哈希桶,是 bucket_t 结构体的数组,bucket_t 是用来存放方法编号 SEL 和函数指针 IMP 的。
    mask_t _mask; // 缓存数组的容量临界值,实际上是为了 capacity 服务
    uint16_t _flags; // 位置标记
    uint16_t _occupied; // 缓存数组中已缓存方法数量
    ...省略
}

消息查找流程(objc_msgSend流程)

  1. 快速查找:cache -> 首地址偏移16字节获取cache,高16位存mask,低48位存buckets -> 通过SEL &mask得到方法下标index,从buckets里面取对应index的bucket,判断sel和bucket(imp,sel)的sel是否相同,不同循环查找(从后往前)
  2. 慢速查找:lookUpImpOrForward -> 二分查找 -> 本类找不到找父类 ,递归查找,一直找到NSObject,NSObject->superCls = nil,停止递归循环 -> 动态方法决议
  3. 动态方法决议:resolveInstanceMethod、resolveClassMethod 询问当前类能够通过动态添加方法处理这个未知的selector
  4. 快速消息转发:forwardingTargetForSelector 查看是否存在其他对象能处理这条消息,称为备援接收者,能处理则交给备援接收者处理,处理不了,则进入完整消息转发流程
  5. 完整消息转发:methodSignatureForSelector、forwardInvocation runtime 系统会把和消息有关的所有信息都放进 NSInvocation 对象中,再给接收者一次机会,处理未知的 selector

objc 中在向一个 nil 对象发送消息时将会发生了什么?

如果向一个 nil 也不会崩溃。对象发送消息,首先在寻找对象的 isa 指针时就是 0 地址返回了,所以不会出现任何错误。也不会崩溃。

objc 在向一个对象发送消息时,发生了什么?

objc 在向一个对象发送消息时,runtime 会根据对象的 isa 指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到,就去执行它的实现 IMP

使用 runtime Associate 方法关联的对象,需要在主对象 dealloc 的时候释放么?

无论在 MRC 下还是 ARC 下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在 被 NSObject -dealloc 调用的 object_dispose() 方法中释放。

  1. 调用 -release :引用计数变为零 对象正在被销毁,生命周期即将结束

不能再有新的 _weak 弱引用,否则将指向 nil.

调用[self dealloc]

  1. 父类调用 -dealloc

继承关系中最直接继承的父类再调用 -dealloc

如果是 MRC 代码则会手动释放实例变量们(iVars

继承关系中每一层的父类都再调用 -dealloc

  1. NSObject-dealloc 只做一件事:调用 Objective一C runtimeobject_dispose() 方法

  2. 调用 object_dispose()C++ 的实例变量们(iVars)调用 destructors

ARC 状志下的实例变量们(iVars)调用 -release

解除所有使用 runtime Associate 方法关联的对象

清空引用计数表并清除弱引用表,将 weak 指针置为 nil

调用 free()

Category

Category 的实现原理?

Category 在刚刚编译完成的时候, 和原来的类是分开的,只有在程序运行起来的时候, 通过 runtime 合并在一起。

被添加在了 class_rw_t 的对应结构里。

Category 实际上是 Category_t 的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的 Category,添加了同一个方法,执行的实际上是最后一个。

Category 在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 Runtime , Category 和原来的类才会合并到一起。

mememove,memcpy: 这俩方法是位移、复制,简单理解就是原有的方法移动到最后,根据新开辟的控件, 把前面的位置留给分类,然后分类中的方法,按照倒序依次插入,可以得出的结论就就是,越晚参与编译的分类,里面的方法才是生效的那个。

Category 在编译过后,是在什么时机与原有的类合并到一起的?

  1. 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init

  2. 然后会 map_images。(dyldimage 加载进内存时 , 会触发该函数。)

  3. 接下来调用 map_images_nolock

  4. 再然后就是 read_images,这个方法会读取所有的类的相关信息。

  5. 最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。

  6. reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入 ClassCategory ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体中。

Category 有哪些用途?

  • 给系统类添加方法、属性(需要关联对象)。
  • 对某个类大量的方法,可以实现按照不同的名称归类。

如何给 Category 添加属性?关联对象以什么形式进行存储?

关联对象 以哈希表的格式,存储在一个全局的单例中。

@interface NSObject (Extension)

@property (nonatomic,copy ) NSString name;

@end
@implementation NSObject (Extension)

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
    return objc_getAssociatedobject(self,@selector(name));
}
@end

class_ro_t 是只读的,存放的是 编译期间就确定 的字段信息;而class_rw_t 是在 runtime 时才创建的,它会先将 class_ro_t 的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去,之所以要这么设计是因为 Objective-C 是动态语言,你可以在运行时更改它们方法,属性等,并且分类可以在不改变类设计的前提下,将新方法添加到类中。

使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

  • 不需要,被关联的对象的生命周期内要比对象本身释放晚很多, 它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。

[self class] 与 [super class]

下面的代码输出什么?

@implementation Son : Father
- (id)init
{
    self = [super init);
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@",  NSStringFromClass([super class]));
    }
    return self;
}
@end

NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son

详解: 这个题目主要是考察关于 Objective一C 中对 selfsuper 的理解。

  • self 是类的隐藏参数,指向当前调用方法的这个类的实例;
  • super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者。

不同点在于: super 会告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

在调用 [super class] 的时候,runtime 会去调用 objc_msgSendSuper 方法,而不是objc_msgSend

objc_msgSendSuper 方法中,第一个参数是一个 objc_super 的结构体,这个结构体里面有两个变量,一个是接收消息的 receiver,一个是当前类的父类 super_class.

objc_msgSendSuper 的工作原理应该是这样的: 从 objc_super 结构体指向的 superClass 父类的方法列表开始查找 selector,找到后以objc->receiver 去调用父类的这个 selector。注意,最后的调用者是 objc->receiver,而不是 super_class! 那么 objc_msgSendSuper 最后就转变成:

// 注意这里是从父类开始msgSend,而不是从本类开始
objc msgSend(objc_super->receiver, @selectorclass))
/// Specifies an instance of a class. 这是类的一个实例
    _unsafe_unretained id receiver;
由于是实的调用,所以是减号方法
- (Class)class {
    return object_getClass(self);
}

由于找到了父类 NSObject 里面的 class 方法的 IMP,又因为传入的人参 objc_super->receiver = selfself 就是 son,调用 class,所以父类的方法 class 执行 IMP 之后,输出还是 son,最后输出两个都一样,都是输出 son.

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量? 为什么?

  • Clean Memory:加载后不会发生更改的内存块,class_ro_t 属于 Clean Memory,因为它是只读的(方法列表)、 (属性列表)、 (协议列表) 以及 实例变量、类的名称、大小等 编译期确定 的信息。

  • Dirty Memoryclass_rw_t 属于 Dirty Memory,运行时会发生更改的内存块,类结构一旦被加载,就会变成 Dirty Memory (方法列表、协议列表、属性列表)

  • 不能再编译后得到的类中增加实例变量。因为编译后的类已经注册在 runtime 中, 类结构体中objc_ivar_list 实例变量的链表和 objc_ivar_list 实例变量的内存大小已经确定,所以不能向存在的类中添加实例变量

  • 能在运行时创建的类中添加实力变量。调用 class_addIvar 函数

runtime 如何实现 weak 变量的自动置 nil?知道 SideTable 吗?

runtime 对注册的类会进行布局,对于 weak 修饰的对象会放入一个 hash表 中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为 0 的时候会 dealloc,假如 weak 指向的对象内存地址是 a,那么就 会以 a 为键, 在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil

weak引用指向的对象被释放时,又是如何去处理 weak 指针的呢?当释放对象时,其基本流程如下:

  1. 调用 objc_release

  2. 因为对象的引用计数为 0,所以执行 dealloc

  3. dealloc 中,调用了 _objc_rootDealloc 函数

  4. _objc_rootDealloc 中,调用了 object_dispose 函数

  5. 调用 objc_destructInstance

  6. 最后调用 objc_clear_deallocating

对象被释放时调用的 objc_clear_deallocating函数:

  1. weak 表中获取废弃对象的地址为键值的记录

  2. 将包含在记录中的所有附有 weak 修饰符变量的地址,赋值为 nil

  3. weak 表中该记录删除

  4. 从引用计数表中删除废弃对象的地址为键值的记录

总结: 其实 weak 表是一个 hash(哈希)表Keyweak 所指对象的地址,valueweak 指针的地址(这个地址的值是所指对象指针的地址)数组。