iOS底层-Runtime系列二
Class的结构
获取源码
-
查看源码
typedef struct objc_class *Class;
-
Class是objc_class结构体指针
struct objc_class : objc_object { .... }
-
objc_class继承自objc_object
struct objc_object { private: isa_t isa; ... }
-
将父类也整合到objc_class类中如下:(注意这里只考虑成员变量)
struct objc_class { // Class ISA; private: isa_t isa; public: Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags //这个成员函数是特殊。使用bits成员的data()函数可以获取class_rw_t* class_rw_t *data() { return bits.data(); } ....成员函数 }
-
class_rw_t
如下:struct class_rw_t { uint32_t flags; uint32_t version; const class_ro_t *ro; //其他信息 method_array_t methods;//方法列表 property_array_t properties; //属性列表 protocol_array_t protocols; //协议列表 Class firstSubclass; Class nextSiblingClass; char *demangledName; ...成员函数 }
-
class_ro_t
如下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; protocol_list_t * baseProtocols; const ivar_list_t * ivars;//成员变量列表 const uint8_t * weakIvarLayout; property_list_t *baseProperties; };
-
分析:
- class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
- class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容
method_t
-
method_t是对方法\函数的封装
//对方法函数的封装 struct method_t { SEL name;//函数名 const char *types;//编码(返回值类型、参数类型) MethodListIMP imp;//指向函数的指针(函数地址) };
-
IMP代表函数的具体实现:
/// A pointer to the function of a method implementation. #if !OBJC_OLD_DISPATCH_PROTOTYPES typedef void (*IMP)(void /* id, SEL, ... */ ); #else typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); #endif
- SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似(即相当于C的字符串)
- 可以通过
@selector()
和sel_registerName()
获得 - 可以通过
sel_getName()
和NSStringFromSelector()
转成字符串 - 不同类中相同名字的方法,所对应的方法选择器是相同的
/// An opaque type that represents a method selector. typedef struct objc_selector *SEL;
- 可以通过
- types包含了函数返回值、参数编码的字符串
- 这个C字符串组成拼接:返回值、参数1、参数2…
- Type Encoding
- iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码
- 比如:
@encode(id);
,这返回值是:@
-
举例使用
Method mthod = class_getInstanceMethod (Person.class, @selector(test1)); Method mthod2 = class_getInstanceMethod (Person.class, @selector(test2:andCan2:)); const char *type = method_getTypeEncoding(mthod); const char *type2 = method_getTypeEncoding(mthod2); NSLog(@"===%s",type);//v16@0:8 NSLog(@"===%s",type2);//v28@0:8i16d20
- oc中的每个方法都有2个默认参数:self、SEL,self为id类型
v28@0:8i16d20
分析- 去除数字:
v@:id
代表:void id SEL int double
- 28代表参数占用的总字节数:
- id:指针类型,8个字节
- SEL:指针类型,8个字节
- int:4个字节
- double:8个字节
- 0代表:@是从第0个字节开始的
- 8代表:
:
是从第8个字节开始的 - 16代表:i是从第16个字节开始的
- 20代表:d是从第20个字节开始的
- 去除数字:
方法缓存
-
Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度
struct cache_t { struct bucket_t *_buckets; //散列表 (数组指针) mask_t _mask;//散列表的长度 -1 mask_t _occupied;// 已经缓存的方法数量 }
-
散列表_buckets是一个数组指针,数组元素为:bucket_t类型
struct bucket_t { private: MethodCacheIMP _imp;//函数的内存地址 cache_key_t _key; //SEL作为key }
-
-
缓存方法查找:
-
通过cache_t的成员函数find查找
struct bucket_t * find(cache_key_t key, id receiver); /** 获取缓存方法 @param k 传入的@selector(方法名) @param receiver 方法的接收这 @return 返回方法bucket_t */ bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); //拿到该类所有的缓存方法列表数组--散列表 bucket_t *b = buckets(); //拿到该类散列表长度-1值,就是数组长度-1 mask_t m = mask(); //cache_hash是个内联函数,本质是:k&m mask_t begin = cache_hash(k, m); //将begin作为遍历的第一个值 mask_t i = begin; do { //拿到数组元素bucket_t中的key值,如果为0,或者等于k,那么就返回这个方法存储对象bucket_t指针 if (b[i].key() == 0 || b[i].key() == k) { return &b[i]; } //如果不成立,i-1,继续遍历,直到找到k==对应元素的key } while ((i = cache_next(i, m)) != begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); }
- 内部原理:
- 当对象第一次调用一个方法时,它会将
@selector(方法名) & mask
作为数组的索引值存入到buckets数组中 - 由于不同的方法名
&mask
时可能生成同一个索引,因为@selector(方法名)
地址是不同的而且很大(比如:0x 0000 0000 0000 0FFE),&一个很小的mask(比如:64),肯定会出现同一个值的可能,那么&之后去存储到数组buckets时,发现该索引已经有值,那么它就会将索引自动-1,然后在存储,直到没有值为止 - 因此上面的find方法也是如此,现根据
@selector(方法名) & mask
作为索引查找buckets数组中相应的元素,然后拿到元素的key值比较,看看是否相等,不相等则-1遍历,继续查找,直到直到为止。
- 当对象第一次调用一个方法时,它会将
- 散列表存储的特点
- 也是一个数组
- 存储方式不是一个一个按顺序存入
- 通过某种方式指定index指定存入
- 比如&数组的长度-1
- 注意:x&y 结果一定小于y
- 所以可以通过&数组长度-1来获取索引值存储
-
objc_msgSend 分析
- objc_msgSend执行流程
-
示例:
//OC方法 [person p_test]; //转化为C++编译 objc_msgSend(person, sel_registerName("p_test"));
- OC中的方法调用,其实都是转换为objc_msgSend函数的调用
- objc_msgSend的执行流程可以分为3大阶段
- 消息发送
- 动态方法解析
- 消息转发
-
objc_msgSend源码分析,在objc源码中的
objc-msg-arm64.s
文件,发现objc_msgSend是用汇编实现的.OC中有些调用频率很高的方法直接用汇编实现objc-msg-arm64.s ENTRY _objc_msgSend b.le LNilOrTagged CacheLookup NORMAL .macro CacheLookup .macro CheckMiss STATIC_ENTRY __objc_msgSend_uncached .macro MethodTableLookup __class_lookupMethodAndLoadCache3
-
__class_lookupMethodAndLoadCache3
汇编中没有,直接找对应的C函数_class_lookupMethodAndLoadCache3
objc-runtime-new.mm _class_lookupMethodAndLoadCache3 lookUpImpOrForward getMethodNoSuper_nolock、search_method_list、log_and_fill_cache cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache _class_resolveInstanceMethod _objc_msgForward_impcache
-
查找
_objc_msgForward_impcache
方法实现objc-msg-arm64.s STATIC_ENTRY __objc_msgForward_impcache ENTRY __objc_msgForward Core Foundation __forwarding__(不开源)
-
-
- 从上面的分析,消息查找的本质都在:lookUpImpOrForward这个方法中
- 这个方法中可以看出,把消息查找分为3个阶段
- 消息发送
- 动态方法解析
- 消息转发
- 而且这个方法的名字:
lookUpImpOrForward
,查找IMP或者Forward
- 这个方法中可以看出,把消息查找分为3个阶段
-
消息发送阶段流程图如下:
- 如果是从class_rw_t中查找方法
- 已经排序的,二分查找
- 没有排序的,遍历查找
- receiver通过isa指针找到receiverClass
- receiverClass通过superclass指针找到superClass
- 如果是从class_rw_t中查找方法
-
动态方法解析阶段流程图如下:
- 开发者可以实现以下方法,来动态添加方法实现
- 动态添加实例方法:
+resolveInstanceMethod:
- 动态添加类方法:
+resolveClassMethod:
- 动态添加实例方法:
- 动态解析过后,会重新走“消息发送”的流程
- “从receiverClass的cache中查找方法”这一步开始执行
-
代码示例:
#import "Person.h" #import <objc/runtime.h> @implementation Person //-(void)p_test{ // NSLog(@"==========="); //} //也可以直接用C语言来动态添加 //但是必须有2个默认参数id,SEL void test2(id self,SEL _cmd){ NSLog(@"=====C语言函数======"); } //调用p_test,会动态调用到这 -(void)test{ NSLog(@"==========="); } //对象方法动态解析 /** 消息发送阶段没找到对应实例方法,会来到这--动态方法解析 作用:动态的添加一个方法 本质:将正在寻找的sel指向动态添加的方法的IMP @param sel 正在寻找的sel @return 是否有动态添加方法 */ +(BOOL)resolveInstanceMethod:(SEL)sel{ // if (sel == @selector(p_test)) { // //获取一个方法的实现 // Method method = class_getInstanceMethod(self, @selector(test)); // const char *typeEncode = method_getTypeEncoding(method); // IMP imptest = method_getImplementation(method); // //动态添加方法:添加实例方法,实例方法添加到类对象 // class_addMethod(self, @selector(p_test), imptest, typeEncode); // return YES; // } //或者添加C函数 if (sel == @selector(p_test)) { //设置type编码 const char *typeEncode = "v16@0:8"; //C语言函数名就是函数地址 IMP imptest = (IMP)test2; //动态添加方法:添加实例方法,实例方法添加到类对象 class_addMethod(self, @selector(p_test), imptest, typeEncode); return YES; } // NSLog(@"==========="); return [super resolveInstanceMethod:sel]; } //类方法解析 /** 消息发送阶段没找到对应类方法,会来到这--动态方法解析 作用:动态的添加一个方法 本质:将正在寻找的sel指向动态添加的方法的IMP @param sel 正在寻找的sel @return 是否有动态添加方法 */ +(BOOL)resolveClassMethod:(SEL)sel{ NSLog(@"==========="); return [super resolveClassMethod:sel]; } @end //使用 Person *person = [[Person alloc] init]; [person p_test];
- 开发者可以实现以下方法,来动态添加方法实现
- 消息转发阶段
-
源码如下:
_objc_msgForward_impcache //实现在objc-msg-arm64.s中 STATIC_ENTRY __objc_msgForward_impcache ENTRY __objc_msgForward Core Foundation __forwarding__(不开源)
-
分析流程如下:
- 开发者可以在forwardInvocation:方法中自定义任何逻辑
- 以上方法都有对象方法、类方法2个版本(前面可以是加号+,也可以是减号-)
-
代码举例:
#import "Person.h" #import "Person2.h" @implementation Person /** 调用条件:当前类消息发送阶段、动态解析阶段都没有找到时 作用: 返回一个能够处理aSelector的对象id @param aSelector 要处理的方法 @return 要转发的对象 */ //也可以是类方法:+(id)forwardingTargetForSelector:(SEL)aSelector //但是返回值应该是类对象:[Person2 class];而不是对象; //-(id)forwardingTargetForSelector:(SEL)aSelector{ // if (aSelector == @selector(p_test)) { // // return [[Person2 alloc] init]; // } // return [super forwardingTargetForSelector:aSelector]; //} /** 调用条件:没有实现forwardingTargetForSelector,或者forwardingTargetForSelector返回值为nil 作用:返回一个方法的签名 @param aSelector 要处理的方法 @return 方法的签名: 返回值类型、参数类型 */ //也可以是类方法:+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if (aSelector == @selector(p_test)) { return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"]; } return [super methodSignatureForSelector:aSelector]; } /** 调用条件:methodSignatureForSelector返回了一个方法签名 作用: 指定调用对象,调用方法 注意: 走到这里就不会报错了,这里可以写任何东西,甚至可以什么都不写 anInvocation.target; //调用者 anInvocation.selector; //方法名 [anInvocation getArgument:NULL atIndex:2]; 获取参数 @param anInvocation 封装了一个函数调用,包括方法调用者、方法名、参数,根据上面的方法签名包装的 */ //也可以是类方法:+(void)forwardInvocation:(NSInvocation *)anInvocation -(void)forwardInvocation:(NSInvocation *)anInvocation{ //这里可以什么都不写 //也可以指定相应的对象调用 // anInvocation.target = [[Person2 alloc] init]; // [anInvocation invoke]; //等价 [anInvocation invokeWithTarget:[[Person2 alloc] init]]; } @end
-
消息转发的应用
- 情景:避免应用因方法找不到而崩溃,同时又想收集哪些方法找不到
- 上面分析知道,只要实现了消息转发的最后一个阶段的方法,即使方法找不到,也一定不会崩溃
-
那么就可以创建一个基类或者直接给NSObject写一个分类,实现下面方法即可
#import "BeseObject.h" @implementation BeseObject -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ //正常的方法 if ([self respondsToSelector:aSelector]) { [super methodSignatureForSelector:aSelector]; } //找不到的方法 return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } -(void)forwardInvocation:(NSInvocation *)anInvocation{ //这里可以同步到服务器。。。 NSLog(@"==找不到的方法====%@",NSStringFromSelector(anInvocation.selector)); } @end