什么是RunTime
OC是一门动态性比较强的编程语言 跟C,C++等静态语言有很大的不同。
静态语言:如C语言 编译阶段就要决定调用哪个函数 如果函数未实现就会报错。
动态语言:编译阶段并不能决定真正调用哪个函数 只要函数声明过 没有实现也不会报错。
OC之所以被称为动态语言 就是因为它把一些决定性的工作从编译阶段推迟到运行阶段。OC代码的运行不仅需要编译器,还需要运行时系统(Runtime Sytem)来执行编译后的代码。
RunTime 是一套底层纯C语言的API。OC代码最终都会被编译器编译为运行时代码。然后通过消息机制决定函数调用的方式。这也是OC作为动态语言使用的基础。
isa详解
要想学习RunTime 首先要了解它底层的一些常用的数据结构 比如isa指针。
之前我们总是认为OC中的每个对象都包含着一个isa指针,实例对象的isa指针 指向类对象 类对象的isa指针指向元类对象 元类对象的isa指向基类。
那么isa中只有这些信息吗,其实我们可以再深入的探究一下的。在arm64的架构中isa指针并不是直接指向类对象 而是要进行一次位运算 本身的isa指针地址 &ISA_MASK 才能得到类对象或者元类对象。在arm64之前isa就是一个普通的指针,存储着Class meta-Class对象的内存地址。arm64架构开始,对isa指针进行了优化,变成了一个共用体结构,还使用位域来存储更多的信息。所以在arm64架构中,我们拿到isa指针地址后 还要进行&ISA_MASK才能得到Class meta-Class对象的地址。
如果你看RunTime的源码 你会发现objc_object中的isa指针已经变成isa_t这种共用体结构了。里面通过位域技术存储了更多的信息。
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; //存放所有的数据 一共64位 下面struct结构体中的属性 写在前面的在低地址位置 也就是位数的最右边 #if SUPPORT_PACKED_ISA # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL struct { uintptr_t nonpointer : 1; //占1位 0代表普通指针 代表着isa只存储着Class meta-Class对象的内存地址 1 代表优化过 使用位域存储着更多的信息 uintptr_t has_assoc : 1;//是否设置过关联对象 没有释放更快 uintptr_t has_cxx_dtor : 1;//是否有C++的析构函数 没有释放的更快 uintptr_t shiftcls : 33;//存储着Class meta-Class对象的内存地址信息 uintptr_t magic : 6;//用于在调试时分辨对象是否未完成初始化 uintptr_t weakly_referenced : 1;//是否被弱指针指向过 uintptr_t deallocating : 1;//对象是否正在释放 uintptr_t has_sidetable_rc : 1;//引用计数是否过大 无法存储在isa中 如果为1 那么引用计数会存储在一个叫SideTable的类的属性中 uintptr_t extra_rc : 19;//存储的值是引用计数减1 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; }
很显然位域技术可以用更小的内存 存储更多的信息 比如BOOL值 一般存储一个BOOL值需要一个字节 但是如果使用位域技术 一个自己 0000 0000 用每一位代表一个二进制的信息 一个字节就可以存储8个BOOL值的信息了。
Class的结构
struct objc_class : objc_object { // Class ISA; objc_class; Class superclass; cache_t cache; // 方法缓存 class_data_bits_t bits; // 用于获取具体的类信息 &FAST_DATA_MASK 得到 class_rw_t } struct class_rw_t { //可读可写 // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; //指向了另一张表 ro_t 只读表 method_array_t methods; //方法列表 二维数组 method_array_t 装着 method_list_t 里面装着method_t property_array_t properties; //属性信息 二维数组 protocol_array_t protocols; //协议信息 二维数组 Class firstSubclass; Class nextSiblingClass; char *demangledName; } struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; //方法信息 一维数组 method_list_t 装着method_t protocol_list_t * baseProtocols; const ivar_list_t * ivars; //属性信息 const uint8_t * weakIvarLayout; property_list_t *baseProperties; method_list_t *baseMethods() const { return baseMethodList; } };
上面就是objc_class的结构了 可以清晰的看到 存储了isa指针 属性信息 方法信息 协议信息 成员变量等重要信息。
class_rw_t 里面的methods properties protocols 是二维数组 是可读可写的 包含了类的初始内容(初始化时候已有的属性 协议 方法) 分类的内容。
class_ro_t 里面的baseMethodList baseProtocols ivars baseProperties是一维数组 是只读的 包含了类的初始内容
为什么class_rw_t 和 class_ro_t 都存储了类的初始信息呢 是不是感觉有点浪费呢?还记得我们OC中的分类吗,一个类初始的方法 协议 成员变量 属性等信息其实是存储在class_ro_t中的,但是在运行阶段RunTime系统会把class_ro_t存储的方法 属性 协议等信息和分类中的方法 协议 属性等信息 一并合并到class_rw_t中,并且分类的方法靠前。所以class_rw_t存储的原始信息是这样来的。class_rw_t一开始是不存在的 在运行的时候合并的时候 创建出来的。一开始bits是指向class_ro_t的 我们设置了class_rw_t后 才指向class_rw_t的。
method_t
struct method_t { SEL name; //函数名 SEL代表方法或者函数名字 一般叫做选择器 const char *types; //编码(返回值类型 参数类型) 是个字符串 根据encode指令编写的 比如 v代表void @代表id类型 :代表SEL类型 IMP imp; // 指向函数的指针 IMP代表函数的具体实现 struct SortBySELAddress : public std::binary_function<const method_t&, const method_t&, bool> { bool operator() (const method_t& lhs, const method_t& rhs) { return lhs.name < rhs.name; } }; };
不同类中如果有相同的方法名 他们的选择器是相同的 即SEL相同 可通过@selector() 或者 sel_registerName()获取
cache_t 方法缓存
cache 用散列表来缓存曾经调用过的方法 可以提高方法的查找速度.
我们都知道当向对象发送一个消息的时候,对象会通过自己的isa指针找到类对象或者元类对象存储的方法列表中找到并实现,这个需要遍历方法列表寻找,如果在类对象或者元类对象的方法列表中找不到该方法,类对象和元类对象还会通过自己的superClass指针到自己的父类对像或者父类元类对象的方法列表中遍历寻找,直到找到该方法的实现。如果我们常用的方法每次都这样寻找会很麻烦。所以苹果对我们对象每次调用多的方法 都缓存到类对象或者元类对象的cache中。这样每次调用方法,会先通过isa指针找到类对象或者元类对象的cache列表中查找,如果找到直接调用。找不到,在走以上过程,找到了就缓存到cache列表中。极大的提高了效率
缓存cache_t的底层结构
struct cache_t { struct bucket_t *_buckets; //散列表 mask_t _mask; //散列表的长度-1 mask_t _occupied;//已经缓存的方法数量 } struct bucket_t { private: cache_key_t _key; //SEL 作为key IMP _imp; //函数的内存地址 }
散列表为什么比较快呢,散列表可以避免遍历直接找到目标。
原理是这样的。存储的时候 通过一定的规则 得到一个索引 那么我们就将存储的内容放到这个索引对应的位置。取出的时候可以按照规则直接得到索引,迅速找到。
方法缓存的索引规则其实是通过@selector('方法名') & _mask = 数组中的索引,得到这个索引后直接将该方法封装成bucket_t存储到该索引位置。取出时根据相同的规则得到索引,直接取值。
如果@selector('方法名') & _mask 得到索引值已经存储了东西 那么会存储到@selector('方法名') & (_mask -1)的位置。取出的时候也是如果发现key和调用的方法名不对,那么会@selector('方法名') & (_mask -1)得到一个新的索引值重新取。以此类推
如果索引之间有间距 直接填充NULL 所以是空间换时间 散列表就是哈希表
有了Cache_t这个结构体那么消息转发机制 就变成了这样先通过isa找到类对象或者元类对象 然后在起cache的方法列表中查找方法,如果找不到,再遍历其存储的方法列表查找,如果找到,调用并缓存起来,找不到通过superClass找到父类对象或者父元类对象 先在其cache的方法列表中查询,找到调用并缓存到自己的类或者元类。找不到在从其存储的方法列表中查询 直到找到调用并缓存到自己类或者元类的cache列表中。
objc_msgSend() 消息机制
MJPerson *person = [[MJPerson alloc] init]; [person personTest]; // objc_msgSend(person, @selector(personTest)); // 消息接收者(receiver):person // 消息名称:personTest
OC方法的调用 消息机制 给方法调用者 发送消息
objc_msgSend的执行流程可以分为3大阶段
1.消息发送 2.动态方法解析 3 消息转发
消息发送阶段就是我们上面讲的消息寻找流程 如果能找到就调用 如果不能就进入第二个阶段 允许我们动态的创建方法 如果这个阶段我们没做任何事情 那么就进入到第三个阶段 消息转发 可能会找其他对象去调用者饿高方法 如果这三个流程都走了 还是没找到方法 那就报找不到方法的错误了。
消息发送阶段我们已经讲的很清楚了。下面我们来讲一下动态解析阶段
1.先判断是否曾经有过动态解析 如果有 直接进入消息转发阶段
2.如果没有 调用+resolveInstanceMethod或者+resolveClassMethod方法 我们可以动态的添加实现 标记为已经动态解析 然后再次进入消息发送阶段
#import "Person.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person *p = [[Person alloc] init]; [p test]; [Person classMethod]; } @interface Person : NSObject - (void)test; +(void) classMethod; @end #import "Person.h" #import <objc/runtime.h> @implementation Person //如果消息发送阶段没有找到方法 就会走到动态解析阶段 会调用这个方法 //我们有机会在这个方法里 动态添加方法的实现 //如果我们已经动态添加了方法 又回回到第一阶段 消息发送阶段 +(BOOL)resolveInstanceMethod:(SEL)sel { //方案一 如果方法没被实现 回调用我们写的other方法 if (sel == @selector((test))) { Method otherMethod = class_getInstanceMethod(self, @selector(other)); //相当于放到class_rw_t即类存储的方法列表里面了 所以再次回到消息发送阶段会从类存储的方法列表里找到 class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod)); //代表已经实现了动态解析 return YES; } return [super resolveInstanceMethod:sel]; } //如果调用的类方法没被实现 可在这个方法里面动态实现 + (BOOL)resolveClassMethod:(SEL)sel { if (sel == @selector((classMethod))) { Method classMethod = class_getClassMethod(self, @selector(classOtherMethod)); //注意传参事元类对象 class_addMethod(object_getClass(self), sel, method_getImplementation(classMethod), method_getTypeEncoding(classMethod)); return YES; } return [super resolveClassMethod:sel]; } - (void) other { NSLog(@"%s",__func__); } + (void) classOtherMethod { NSLog(@"%s",__func__); } @end
如果第二阶段 我们也没做什么,那么就会进入消息转发阶段。将消息转发给别人。就是自己没能力处理 看看别人是否有能力处理。
调用forwradingTargetForSelector:返回一个对象 让这个对象接收这个消息 走这个对象的消息发送阶段
#import "Person.h" #import "Student.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person *p = [[Person alloc] init]; [p test]; [Person classMethod]; Student *s = [[Student alloc] init]; [s test]; } #import "Student.h" #import "Person.h" @implementation Student //消息转发阶段 - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(test)) { //给Person对象 发送test消息 return [[Person alloc] init]; } return [super forwardingTargetForSelector:aSelector]; } @end
如果消息转发阶段我们没有实现forwardingTargetForSelector:方法 或者该方法返回nil 系统还是给我们提供了一个流程来处理这个问题
会调用 methodSignatureForSelector:返回这个方法的签名 这些信息会包装到NSInvocation对象中 然后调用 forwardInvocation:方法 我们可以修改NSInvocation的调用对象 让另外一个对象调用这个方法。 达到和实现forwardingTargetForSelector:方法一样的效果
#import "Student.h" #import "Person.h" @implementation Student //消息转发阶段 //- (id)forwardingTargetForSelector:(SEL)aSelector { // if (aSelector == @selector(test)) { // //给Person对象 发送test消息 // return [[Person alloc] init]; // } // return [super forwardingTargetForSelector:aSelector]; //} - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(test)) { return nil; } return [super forwardingTargetForSelector:aSelector]; } //如果没有实现forwardingTargetForSelector 或者forwardingTargetForSelector 返回nil 会调用这个方法 获取方法签名 然后调用forwardInvocation:方法 //方法签名 返回值类型 参数类型 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(test)) { // v void 返回值为空 16:所有参数的大小 @0 id类型的参数从0开始 :SEL类型的参数 从第8位开始 返回nil 不会调用forwardingInvocation:方法 就报错了 return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"]; } return [super methodSignatureForSelector:aSelector]; } //NSInvocation封装了一个方法调用 包括 方法的调用者 方法 方法参数 //anInvocation.target; 方法调用者 //anInvocation.selector;//方法 //anInvocation getArgument: atIndex: 参数 - (void)forwardInvocation:(NSInvocation *)anInvocation { // anInvocation.target = [[Person alloc] init]; // //调用函数 // [anInvocation invoke]; [anInvocation invokeWithTarget:[[Person alloc] init]]; } @end
如果我们实现methodSignatureForSelector:和forwardInvocation:方法 仅仅能达到和forwardingTargetForSelector:一样的效果,那么下面的也太麻烦了。是不是下面的方法还能实现一些不同的效果呢。我们可以看一下 事实是,只要我们进入到forwardingInvocation:方法 我们可以做任何事情 我们甚至可以不给出转发者 只打印一下也是可以的。
- (void)forwardInvocation:(NSInvocation *)anInvocation { // anInvocation.target = [[Person alloc] init]; // //调用函数 // [anInvocation invoke]; // [anInvocation invokeWithTarget:[[Person alloc] init]]; //不给出转发者 只打印一下 NSLog(@"哈哈哈"); }
相当于我们调用test 方法 实现的是forwardInvocation:里面的内容。类方法也有消息转发机制 只要把消息转发机制的方法变成类方法就行了 意思就是-变为+号。因为消息转发机制的三个方法 都是用消息接收着直接调用的。如果你传的是实例对象 那就是实例方法 你传的是个类对象 那就是类方法。
super 关键字
struct objc_super { __unsafe_unretained _Nonnull id receiver; // 消息接收者 __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类 };
在arm64中 objc_super 的构成是一个消息接收者 和 一个消息接收者的父类
- (void)run { // super调用的receiver仍然是MJStudent对象 // 但是调用的方法 先从父类的cache找 然后从父类的method_list中找 [super run]; // struct objc_super arg = {self, [MJPerson class]}; // objc_msgSendSuper(arg, @selector(run)); // NSLog(@"MJStudet......."); }
可以看到虽然调用父类的run方法,但是从objc_msgSendSuper(arg, @selector(run));可以看到消息接收者仍然是子类 只不过执行消息发送的时候是从父类开始的。
[super message]的底层实现
1.消息接收者仍然是子类对象
2.从父类开始查找方法的实现
- (instancetype)init { if (self = [super init]) { NSLog(@"[self class] = %@", [self class]); // MJStudent NSLog(@"[self superclass] = %@", [self superclass]); // MJPerson NSLog(@"--------------------------------"); // objc_msgSendSuper({self, [MJPerson class]}, @selector(class)); NSLog(@"[super class] = %@", [super class]); // MJStudent NSLog(@"[super superclass] = %@", [super superclass]); // MJPerson } return self; }
结果为什么是这样呢?其实我们都知道 class方法,是在基类(NSObject)实现的。所以上面的代码调用的其实都是一个方法。接收者都是self 而class 和 superClass的实现是这样的
- (Class)class { return object_getClass(self); } - (Class)superclass { return class_getSuperclass(object_getClass(self)); }
所以上面的结局也都是可以理解的了。
RunTime相关的API
#import "ViewController.h" #import <objc/runtime.h> #import "MJPerson.h" #import "MJCar.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //动态创建一个类 Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0); //添加成员变量 第一个参数 为谁添加 第二个 成员变量的名字 第三个 成员变量的大小 第四个对齐方式 一般传1 第五个成员变量的类型 class_addIvar(newClass, "_age", 4, 1, @encode(int)); class_addIvar(newClass, "_weight", 4, 1, @encode(int)); //注册类 如果要添加成员变量和方法 要在这个方法之前调用 因为类管理的成员变量信息是只读的class_ro_t中 注册之后就不能更改了 //也就是说 已有的类不能再动态的添加成员变量了 但是方法可以 方法是存储在类中class_rw_t中的可读可写 objc_registerClassPair(newClass); //这个dog 就属于MJDog这个类了 id dog = [[newClass alloc] init]; [dog setValue:@10 forKey:@"_age"]; [dog setValue:@20 forKey:@"_weight"]; NSLog(@"%@",[dog class]); //MJDog NSLog(@"%zd and %@",class_getInstanceSize(newClass),[dog valueForKey:@"_age"]); //16 isa 8 _age 4 _weight 4 } - (void)test { MJPerson *person = [[MJPerson alloc] init]; //获取类对象 NSLog(@"%p and %p",object_getClass(person),[person class]); //获取元类对象 NSLog(@"%p",object_getClass([person class])); [person run]; //设置类对象指向的isa object_setClass(person, [MJCar class]); [person run]; //判断一个OC对象是否为calss object_isClass(person); NSLog(@"%d and %d and %d",object_isClass(person),object_isClass([MJPerson class]),object_isClass(object_getClass([MJPerson class]))); //是否为一个元类 class_isMetaClass(object_getClass([MJPerson class])); } @end
获取和设置成员变量
//获取成员变量 - (void) getIvar { Ivar ageIvar = class_getInstanceVariable([MJCar class], "_age"); NSLog(@"%s %s",ivar_getName(ageIvar),ivar_getTypeEncoding(ageIvar)); //设置或者获取成员变量的值 MJCar *car = [[MJCar alloc] init]; Ivar name = class_getInstanceVariable([MJCar class], "_name"); object_setIvar(car, name, @"123"); NSLog(@"%@",object_getIvar(car, name)); }
获取成员变量列表 和 获取属性列表
- (void)getIvarList { unsigned int count; Ivar *ivars = class_copyIvarList([MJCar class], &count); for (NSInteger i = 0; i < count ; i ++ ) { Ivar ivar = ivars[i]; const char *cname = ivar_getName(ivar); NSString *name = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding]; NSLog(@"%@",name); } free(ivars); //获取属性列表 unsigned int number; objc_property_t *propertys = class_copyPropertyList([MJCar class], &number); for (NSInteger i = 0; i < number; i ++) { objc_property_t property = propertys[i]; const char *cname = property_getName(property); NSString *name = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding]; NSLog(@"%@",name); //属性的特性 unsigned int attrCount = 0; objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount); for (unsigned int j = 0; j < attrCount; j ++) { objc_property_attribute_t attr = attrs[j]; const char * name = attr.name; const char * value = attr.value; NSLog(@"属性的描述:%s 值:%s", name, value); } } free(propertys); }
讲一下OC的消息机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给接收者(方法调用者)发送了一条消息(selector)
objc_msgSend底层有三大阶段 消息发送 动态解析 消息转发阶段
消息发送:
1.判断接收者是否为nil 如果为nil 直接返回
2.实例对象调用方法 先通过isa找到类对象 然后在类对象的缓存方法列表中查询 如果找到直接调用 找不到 再从类对象存储的方法列表中遍历查找 找到调用 并且缓存到类对象的cache列表中。找不到 通过superClass指针,找到父类对象 重复上述过程。类对象调用方法,先通过isa找到元类对象 然后在元类对象的缓存方法列表中查询 如果找到直接调用 找不到 再从元类对象存储的方法列表中遍历查找 找到调用 并且缓存到元类对象的cache列表中。如果还找不到 通过superClass指针 找到找到父元类对象 重复上述过程。
动态解析
如果上面的消息发送阶段到了基类仍没有找到方法实现 就会到达动态解析阶段 这个阶段会调用两个方法resolveInstanceMethod:(实例对象调用) 或者 resolveClassMethod:(类对象调用) 在这两个方法中 系统允许我们动态的添加一些方法的实现(存储到class_rw_t中)。如果实现了该方法 会标记为已经动态实现过 会再走一遍消息发送流程。事实上只要你重写了这两个方法 都会被标记为已经实现了动态解析。
消息转发
如果动态解析阶段 我们还是没做什么事情 那么就会进入到消息转发阶段 这个阶段我们可以指定一个其他的对象 来接收这个消息。我们可以实现forwardingTargetForSelector:来指定对象实现 如果没有指定对象 那么系统会调用methodSignatureForSelector:来获取一个方法签名(如果返回nil 就报错找不到方法) 如果methodSigntureForSelector:没有返回nil 那么就调用 forwardInvocation:方法 这个方法我们可以做任何处理
什么是RunTime?平时项目中有用过吗?
OC 是一门动态语言,相比C或者C++编译完成后就已经确定代码的结果,OC可以在运行的时候动态的改变类的实现 添加属性 修改方法的实现。这一切都是基于运行时的机制。
RunTIme就是C语言封装的一套底层的API 封装了很多动态性相关的函数。
平时我们写的一些代码 底层都是转换成了RunTImeAPI进行调用。
1.给分类添加属性 关联对象
2.遍历类的所有成员变量,然后访问私有变量 或者实现字典转模型 归档接档
3.交换方法的实现(一般是交换系统的方法实现,可以实现在调用系统方法的同时,实现自己的一些逻辑)
4.利用消息转发机制 解决一些方法找不到的问题
@dynamic 告诉编译器 不用生成getter和setter方法 也不会自动生成成员变量 等到运行时再添加方法的实现
@synthesize 关键字 可以自动生成getter和setter方法 并且生成一个_xxx的成员变量