【iOS】Runtime

商道如狼道  金牌会员 | 2024-6-24 05:19:51 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 543|帖子 543|积分 1629


前言

之前分part学习了Runtime的内容,但是没有体系的总结,这篇博客用来总结学过的全部Runtime知识
一、Runtime简介

Runtime又叫运行时,是一套底层的C语言API,是iOS体系的核心之一
在编码阶段中,当我们向一个对象发送消息时,编译阶段只是确定了我们需要向吸收者发送消息,但是吸收者怎样响应与处理这条消息是运行时决定的,我们来看一个例子
首先,让我们界说这些类:
  1. #import <Foundation/Foundation.h>
  2. // 基类 Animal
  3. @interface Animal : NSObject
  4. - (void)speak;
  5. @end
  6. @implementation Animal
  7. - (void)speak {
  8.     NSLog(@"Some generic animal sound");
  9. }
  10. @end
  11. // Dog 类继承自 Animal
  12. @interface Dog : Animal
  13. @end
  14. @implementation Dog
  15. - (void)speak {
  16.     NSLog(@"Woof!");
  17. }
  18. @end
  19. // Cat 类继承自 Animal
  20. @interface Cat : Animal
  21. @end
  22. @implementation Cat
  23. - (void)speak {
  24.     NSLog(@"Meow!");
  25. }
  26. @end
复制代码
现在,我们编写一个主函数来创建差别的动物对象,并对它们调用 speak 方法:
  1. int main(int argc, const char * argv[]) {
  2.     @autoreleasepool {
  3.         // 创建 Animal 类型的数组
  4.         NSArray *animals = @[[[Dog alloc] init], [[Cat alloc] init], [[Animal alloc] init]];
  5.         
  6.         // 遍历数组中的每一个动物,并调用 speak 方法
  7.         for (Animal *animal in animals) {
  8.             [animal speak];
  9.         }
  10.     }
  11.     return 0;
  12. }
复制代码
可以看到我们animal担当了speak这个方法,但是运行时会查找animal的实际类,并且动态地查找这个类或其父类中的 speak 方法实现。
   同时OC也是一门动态语言,这意味着它不光需要一个编译器,更需要一个运行时体系来动态得创建类和对象、进行消息转达和转发
  Objc 在三种层面上与 Runtime 体系进行交互:



  • 层面一:通过OC源代码
    我们只需要编写OC代码,Runtime体系会自动将我们写的代码在编译阶段转换为运行时代码
  • 层面二:通过Foudation框架的NSObject的类自界说方法
    在NSObject协议中有五种方法可以从Runtime中获取信息,并且让对象进行自我检查
  1. - (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
  2. - (BOOL)isKindOfClass:(Class)aClass;
  3. - (BOOL)isMemberOfClass:(Class)aClass;
  4. - (BOOL)conformsToProtocol:(Protocol *)aProtocol;
  5. - (BOOL)respondsToSelector:(SEL)aSelector;
复制代码
-class方法返回对象的类;
-isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中;
-respondsToSelector: 检查对象能否响应指定的消息;
-conformsToProtocol:检查对象是否实现了指定协议类的方法;
在NSObject类中另有一个方法会返回SEL的IMP
  1. - (IMP)methodForSelector:(SEL)aSelector;
复制代码


  • 层面三:通过对 Runtime 库函数的直接调用
  1. 1. Class and Metaclass Functions
  2.         •        objc_getClass(const char *name): 获取指定名称的类。
  3.         •        objc_getMetaClass(const char *name): 获取指定名称的元类。
  4.         •        objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes): 动态创建一个新的类。
  5.         •        objc_registerClassPair(Class cls): 注册一个动态创建的类。
  6. 2. Method Functions
  7.         •        class_addMethod(Class cls, SEL name, IMP imp, const char *types): 向类中添加一个方法。
  8.         •        class_replaceMethod(Class cls, SEL name, IMP imp, const char *types): 替换类中的一个方法。
  9.         •        class_getInstanceMethod(Class cls, SEL name): 获取实例方法。
  10.         •        class_getClassMethod(Class cls, SEL name): 获取类方法。
  11.         •        method_getName(Method m): 获取方法的选择器。
  12.         •        method_getImplementation(Method m): 获取方法的实现。
  13. 3. Property and Ivar Functions
  14.         •        class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types): 向类中添加一个实例变量。
  15.         •        class_getInstanceVariable(Class cls, const char *name): 获取类中的实例变量。
  16.         •        class_getProperty(Class cls, const char *name): 获取类中的属性。
  17.         •        class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount): 向类中添加属性。
  18. 4. Selector Functions
  19.         •        sel_registerName(const char *str): 注册一个选择器。
  20.         •        sel_getUid(const char *str): 获取一个选择器。
  21. 5. Protocol Functions
  22.         •        objc_getProtocol(const char *name): 获取指定名称的协议。
  23.         •        objc_allocateProtocol(const char *name): 动态创建一个新的协议。
  24.         •        objc_registerProtocol(Protocol *proto): 注册一个动态创建的协议。
  25.         •        protocol_addMethodDescription(Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod): 向协议中添加方法描述。
  26.         •        protocol_addProperty(Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty): 向协议中添加属性。
  27. 6. Object and Messaging Functions
  28.         •        objc_msgSend(id self, SEL op, ... ): 发送消息。
  29.         •        objc_msgSendSuper(struct objc_super *super, SEL op, ... ): 发送消息给父类。
  30.         •        object_getClass(id obj): 获取对象的类。
  31.         •        object_setClass(id obj, Class cls): 设置对象的类。
复制代码
二、NSObject库劈头

刚才说了我们有三种方式可以和Runtime进行交互,前两种方式都与NSObject有关,我们就从NSObject基类开始说起
我们通过源码可以得知NSObject的界说如下:
  1. typedef struct objc_class *Class;
  2. @interface NSObject <NSObject> {
  3.     Class isa  OBJC_ISA_AVAILABILITY;
  4. }
复制代码
其内部只包含了一个名为isa的Class指针,同时Class指针实际上就是一个objc_class布局体,怎样明白这个布局体呢,我们来看一下这个布局体的源码:
在Objc2.0之前,objc_class源码如下:
  1. struct objc_class {
  2.     Class isa  OBJC_ISA_AVAILABILITY;
  3.    
  4. #if !__OBJC2__
  5.     Class super_class                                        OBJC2_UNAVAILABLE;
  6.     const char *name                                         OBJC2_UNAVAILABLE;
  7.     long version                                             OBJC2_UNAVAILABLE;
  8.     long info                                                OBJC2_UNAVAILABLE;
  9.     long instance_size                                       OBJC2_UNAVAILABLE;
  10.     struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
  11.     struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
  12.     struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
  13.     struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
  14. #endif
  15.    
  16. } OBJC2_UNAVAILABLE;
复制代码
可以看到在一个类中,有超类的指针,类名,版本的信息,同时另有指向成员变量列表的指针,指向方法列表的指针
我们可以通过动态的修改方法列表来达到使用分类向类中添加方法
关于分类的文章之前写过,现在发现一篇更好的,各人可以读一下
深入明白Objective-C:Category
同时在先前说过Category的底层布局体中是有属性列表的,但是为什么不能添加属性呢,这是由于当我们使用@property声明属性时,会自动添加实例变量,但是Category的底层布局体中没有实例变量列表,因此无法实现,同时另有一个原因是编译器不会为分类自动合成set与get方法,但最最主要的原因是rw中没有成员变量列表,不允许修改成员变量
在objc2.0之后,objc_class的界说就变了:
  1. typedef struct objc_class *Class;
  2. typedef struct objc_object *id;
  3. @interface Object {
  4.     Class isa;
  5. }
  6. @interface NSObject <NSObject> {
  7.     Class isa  OBJC_ISA_AVAILABILITY;
  8. }
  9. struct objc_object {
  10. private:
  11.     isa_t isa;
  12. }
  13. struct objc_class : objc_object {
  14.     // Class ISA;
  15.     Class superclass;
  16.     cache_t cache;             // formerly cache pointer and vtable
  17.     class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
  18. }
  19. union isa_t
  20. {
  21.     isa_t() { }
  22.     isa_t(uintptr_t value) : bits(value) { }
  23.     Class cls;
  24.     uintptr_t bits;
  25. }
复制代码
将源码转换为类图就酿成了下面如许子:

在源码中我们可以看出来全部的对象都包含一个isa_t范例的布局体,这是怎样看出来的呢
  1. struct objc_object {
  2. private:
  3.     isa_t isa;
  4. }
  5. struct objc_class : objc_object {
  6.     // Class ISA;
  7.     Class superclass;
  8.     cache_t cache;             // formerly cache pointer and vtable
  9.     class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
  10. }
复制代码
从这两段代码我们可以看出来,objc_class 是objc_object的子类,我们明白一下这两个布局体名字:
objc_object的意思是对象,也就是在OC中全部对象都有一个isa_t变量,objc_class的意思是类,但是他却继承于对象,那么说明我们的类实际上也是一个对象,也就是类对象
这也就说明了上面的结论:全部的对象都会包含一个isa_t范例的布局体。
objc_object被源码typedef成了id范例,这也说明了为什么任何范例都可以用id来表示,这是由于id范例是全部对象的父类
我们一步步来分析这内里的成员变量,首先是object类和NSObject类内里分别都包含一个objc_class范例的isa
isa

首先我们通过学习消息流程可以知道,当一个对象的方法被调用时,首先会根据isa指针找到相应的类,然后在该类的class_data_bits_t中去查找方法。class_data_bits_t是指向了类对象的数据区域。在该数据区域内查找相应方法的对应实现
同时当调用类方法是也会通过isa查找方法,此时isa指向的是元类(Meta Class),这里有标题可以看先前的博客,不再赘述
同时元类与类对象是唯一的
isa_t布局体

isa_t 是现代Objective-C运行时中的一个重要优化,它通过位域布局封装了 isa 指针,使得它不光仅是一个指向类的指针,还携带了大量运行时所需的附加信息。通过这种计划,Objective-C运行时能够在保持高效内存使用的同时,提供丰富的对象管理功能。
总结就是isa_t比力抽象,笔者也讲不懂,但是内里用到了Tagged Pointer技术,各人可以去了解
深入明白 Tagged Pointer
cache_t的详细实现

cache_t出现objc_class中,我们来通过源码分析一下
  1. struct cache_t {
  2.     struct bucket_t *_buckets;
  3.     mask_t _mask;
  4.     mask_t _occupied;
  5. }
  6. typedef unsigned int uint32_t;
  7. typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
  8. typedef unsigned long  uintptr_t;
  9. typedef uintptr_t cache_key_t;
  10. struct bucket_t {
  11. private:
  12.     cache_key_t _key;
  13.     IMP _imp;
  14. }
复制代码

通过源码我们知道了cache_t中存储了一个bucket_t的布局体,和两个unsigned int的变量。


  • mask:分配用来缓存bucket的总数。
  • occupied:表明目前实际占用的缓存bucket的个数。
同时我们看一下bucket_t布局体,他内里只有两个元素,一个是key,一个是IMP
cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。
当我们使用方法后,编译器会自动将方法的SEL存为Key,其实现IMP存进bucket_t中的Key对应的IMP中,如许就优化了方法调用的性能,不消每次调用方法时都去方法列表中查找
   Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但如许查找方式效率太低,由于每每一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。
  class_data_bits_t的详细实现

  1. struct objc_class : objc_object {
  2.     // Class ISA;
  3.     Class superclass;
  4.     cache_t cache;             // formerly cache pointer and vtable
  5.     class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
  6. }
复制代码
在objc2.0之前我们的objc_class布局体中有十分多的元素,但是更新后就变得十分简洁,这些元素并没有消失,其实都存在了数据区域class_data_bits_t中
同样来看源码:
  1. struct class_data_bits_t {
  2.     // Values are the FAST_ flags above.
  3.     uintptr_t bits;
  4. }
  5. struct class_rw_t {
  6.     uint32_t flags;
  7.     uint32_t version;
  8.     const class_ro_t *ro;
  9.     method_array_t methods;
  10.     property_array_t properties;
  11.     protocol_array_t protocols;
  12.     Class firstSubclass;
  13.     Class nextSiblingClass;
  14.     char *demangledName;
  15. }
  16. struct class_ro_t {
  17.     uint32_t flags;
  18.     uint32_t instanceStart;
  19.     uint32_t instanceSize;
  20. #ifdef __LP64__
  21.     uint32_t reserved;
  22. #endif
  23.     const uint8_t * ivarLayout;
  24.    
  25.     const char * name;
  26.     method_list_t * baseMethodList;
  27.     protocol_list_t * baseProtocols;
  28.     const ivar_list_t * ivars;
  29.     const uint8_t * weakIvarLayout;
  30.     property_list_t *baseProperties;
  31.     method_list_t *baseMethods() const {
  32.         return baseMethodList;
  33.     }
  34. };
复制代码

在 objc_class布局体中的解释写到 :
class_data_bits_t相当于 class_rw_t指针加上 rr/alloc 的标志
也就是说先前的属性、方法以及遵照的协议在obj 2.0的版本之后都放在class_rw_t中,那么ro是用来干什么的呢?
我们知道OC作为一门动态语言运行阶段分为编译器与运行期,在编译期类的布局中的 class_data_bits_t *data指向的是一个 class_ro_t *指针:

在Objc运行时会调用realizeClass方法:

  • 从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针,这一步是为了class_rw_t的ro能被正确赋值
  • 初始化一个 class_rw_t 布局体
  • 设置布局体ro的值以及flag
  • 末了设置正确的data,也就是返回末了的rw布局体(由于原本data指向的是ro)
    我们来看一下更改后的图片

此时realizeClass方法运行后我们的rw布局体已经被初始化,同时ro已经被赋值,但是此时的方法,属性以及协议列表均为空,这时需要 realizeClass 调用 methodizeClass 方法来将类自己实现的方法(包罗分类)、属性和遵照的协议加载到 methods、 properties 和 protocols 列表中。
  1. struct method_t {
  2.     SEL name;
  3.     const char *types;
  4.     IMP imp;
  5.     struct SortBySELAddress :
  6.         public std::binary_function<const method_t&,
  7.                                     const method_t&, bool>
  8.     {
  9.         bool operator() (const method_t& lhs,
  10.                          const method_t& rhs)
  11.         { return lhs.name < rhs.name; }
  12.     };
  13. };
复制代码
同时我们可以再通过这里讲讲我们的消息查找,如果动态修改了方法会生成rw_e布局体,查找方法时会优先去rw_e中查找,否则去ro中查找
三、[self class] 与 [super class]

我们来看一道标题
下面代码输出什么?
  1. @implementation Son : Father
  2.     - (id)init
  3.     {
  4.         self = [super init];
  5.         if (self)
  6.         {
  7.             NSLog(@"%@", NSStringFromClass([self class]));
  8.             NSLog(@"%@", NSStringFromClass([super class]));
  9.         }
  10.     return self;
  11.     }
  12.     @end
复制代码
self和super的区别:
self是类一个隐藏参数,每个方法的实现的第一个参数为self
super则负责告诉编译器,调用方法时,去调用父类的方法,而不是本类中的方法
也就是说[super class]调用了objc_msgSendSuper方法,而不是objc_msgSend
  1. OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
  2. /// Specifies the superclass of an instance.
  3. struct objc_super {
  4.     /// Specifies an instance of a class.
  5.     __unsafe_unretained id receiver;
  6.     /// Specifies the particular superclass of the instance to message.
  7. #if !defined(__cplusplus)  &&  !__OBJC2__
  8.     /* For compatibility with old objc-runtime.h header */
  9.     __unsafe_unretained Class class;
  10. #else
  11.     __unsafe_unretained Class super_class;
  12. #endif
  13.     /* super_class is the first class to search */
  14. };
复制代码
在objc_msgSendSuper方法中,我们会从父类的方法列表开始查找selector,找到后以objc->receiver去调用父类的这个selector。注意,末了的调用者是objc->receiver,而不是super_class!
那么objc_msgSendSuper末了就转酿成
  1. // 注意这里是从父类开始msgSend,而不是从本类开始,谢谢@Josscii 和他同事共同指点出此处描述的不妥。
  2. objc_msgSend(objc_super->receiver, @selector(class))
  3. /// Specifies an instance of a class.  这是类的一个实例
  4.     __unsafe_unretained id receiver;   
  5. // 由于是实例调用,所以是减号方法
  6. - (Class)class {
  7.     return object_getClass(self);
  8. }
复制代码
由于找到了父类NSObject内里的class方法的IMP,又由于传入的入参objc_super->receiver = self。self就是son,调用class,所以父类的方法class执行IMP之后,输出照旧son,末了输出两个都一样,都是输出son。
四、消息发送与转发

这部门内容之前已经学的十分详细了,可以直接看之前写的博客
【iOS】消息流程分析
五、Runtime应用场景

同时我们讲完了Runtime,我们自然要知道怎样应用Runtime,我们来看一下Runtime的一些应用


  • (1) 实现多继承Multiple Inheritance
  • (2) Method Swizzling
  • (3) Aspect Oriented Programming
  • (4) Isa Swizzling
  • (5) Associated Object关联对象
  • (6) 动态的增加方法
  • (7) NSCoding的自动归档和自动解档
  • (8) 字典和模子相互转换
此中大多数应用之前博客都有讲各人可以自行查找,同时Isa Swizzling对应的应用是KVO的原理,至于字典模子相互转换之后在学习JsonModel源码中会讲
参考博客:
神经病院 Objective-C Runtime 入院第一天—— isa 和 Class神经病院 Objective-C Runtime 入院第一天—— isa 和 Class
深入解析 ObjC 中方法的布局
深入明白Objective-C:Category

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

商道如狼道

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表