查找方法的本质都是消息发送
,objc_msgSend
是由汇编
代码实现的,目的是更快更高效。之后的慢速查找函数lookUpImpOrForward
是C
实现的。
消息查找流程总结
-
objc_msgSend
之方法快速查找流程(CacheLoopUp
查找cache_t
)是查找本类的缓存 ,此行为在汇编
里,涉及到内存操作的都是汇编,更高效
。 - 缓存里没找的话,就会来到
objc_msgSend
的后半段慢速查找流程(lookUpImpOrForward)
是查找当前cls或其元类继承链的class_data_bits_t
里方法列表里使用二分查找``findMethodInSortedMethodList
看有没有,这里还会查找父类的缓存。如果这里没有找到,则imp
就会换成imp_forward
。lookUpImpOrForward
的后段会进行动态方法决议
。//这里二分查找方法列表的时候,是已经排好序
的(map_images
流程里初始化rwe时attachLists
,这里分类方法是放在方法数组最前面的,应该是运用了LRU
的算法思维,即分类方法使用概率要高于本类,优先调用,这也是分类存在的意义
),分类方法在前,类方法在后,但查找是从后向前
,并且找到第一个之后不立马返回,而是执行减减
操作,即找到列表中最靠前的符合的分类方法
才会返回。因为类和分类如果有同名方法,会优先执行分类的方法,所以这里不是所谓的网上说的先查找到分类方法就返回(如我理解有误,欢迎指出)。 -
动态方法决议
(LOOKUP_RESOLVE
)bool resolved = msg(cls, resolve_sel, sel)
; 如果没有实现动态决议的方法,resolveInstance/Class
方法会执行两次
后崩溃,因为第一次是系统先查找(查找执行的方法名是lookUpImpOrForward
)是否实现了resolveInstance
/Class
方法(容错),如果实现了,第二次则是再去下发sel
到resolveInstance/Class
方法,否则就没有第二次下发resolve
方法。注意这里指的是动态决议的方法,而不是未实现的自定义的方法
。接下来通过反汇编会知道要进入消息转发流程
. -
消息转发
之快速转发 forwardingTargetForSelector
,这里只需要返回任意一个实现了要查找的方法的类的对象即可,可以利用runtime
动态创建类并添加要查找的方法来防止崩溃。注意消息转发为什么会调用这个方法及下面的慢速转发
呢,可以通过反汇编CoreFoundation
查看。 - 慢速转发
methodSignatureForSelector
、forwardInvocation
(更灵活),如果methodSignatureForSelector
返回nil就会崩溃报错unrecognized selector sent to instance
。返回方法签名就会执行到forwardInvocation
,这里不处理也不会崩溃,自由度很高。
全局避免unrecognized selector sent to instance
可以在NSObject分类
中的resolveInstanceMethod
里处理类方法
和实例
方法。因为类方法的查找,在其继承链,查找的也是实例方法。根本原因还是类方法在元类中的实例方法,而元类方法查找最终会找到根NSObject
。
快速查找流程是CacheLoopUp
,即objc_msgSend
的时候在汇编这里通过mask等查找还原出类的cache_t
中buckets
里存储的sel
和imp
的过程,也就是查找缓存。这也是消息发送的第一步。如果缓存没找到,则进入lookUpImpOrForward
慢速查找流程。
cache_t
缓存,类的首地址移动16位(isa+superclass
)得到的是cache_t结构体,里面存放的是调用过的方法的缓存(SEL和IMP
),对象的init
方法也会缓存到cache_t
. 那么在进行消息发送的时候也会优先来cache_t
去查找方法缓存,这一过程成为快速查找,即CacheLoopUp
。
移动32位 得到的是class_data_bits_t
的类信息结构体
从源码的分析中,我们知道sel-imp是在cache_t的_buckets属性中(目前处于macOS环境),而在cache_t结构体中提供了获取_buckets属性的方法buckets()
获取了_buckets
属性,就可以获取sel-imp
了,这两个的获取在bucket_t
结构体中同样提供了相应的获取方法sel()
以及 imp(pClass)
。
因为在cache
初始化时,分配的空间是4
个,随着方法调用的增多,当存储的sel-imp
个数,即newOccupied + CACHE_END_MARKER
(等于1)的和 超过 总容量的3/4,例如有4个时,当occupied
等于2时,就需要对cache
的内存进行两倍
扩容,扩容时,是将原有的内存全部清除了,再重新申请了内存
.所以_occupied
的值总是0,1,2
。
从数据结构角度及使用方法来看 cache_t
的话,它是一个 SEL
作为 Key ,SEL + IMP(bucket_t)
作为 Value 的散列表
。执行了一次方法调用,cache
中就有了一个缓存,即调用一次方法就会缓存一次方法
.
NSCoding的手动与自动
//手动 一旦属性躲起来 麻烦
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.name forKey:@"name"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
//自动
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}
- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
}
return self;
}
伪多继承
-
在OC程序中可以借用
消息转发机制
来实现多继承的功能。通过forwardingTargetForSelector:
方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟
多继承的效果。 -
即使我们利用转发消息来实现了“假”继承,但是NSObject类还是会将两者区分开。像respondsToSelector:和 isKindOfClass:这类方法
只会考虑继承体系,不会考虑转发链
。如果非要制造假象,就得重写转发链,制造假象。
method-swizzling注意事项
-
+load方法系统只会调用一次,并且主类优先,分类次之。但可以
主动触发多次
,且用户主动触发时分类覆盖主类。还有一种是在继承关系中,比如同时对NSArray和NSMutableArray
中的objectAtIndex:方法都进行了Swizzling,这样可能会导致NSArray中的Swizzling失效的。所以mehod-swizzling
写在load方法中,防止方法的重复交换
,可以使用单例
设计,使方法只交换一次。 -
+initialize
方法是以懒加载
的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize方法是永远不会被调用的。所以Swizzling要是写在+initialize
方法中,是有可能永远都不被执行
。
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[RuntimeManager methodSwizzlingWithClass:self oriSEL:@selector(viewDidLoad) swizzledSEL:@selector(my_viewDidLoad)];
});
}
- 父类实现了方法A,但子类只是继承,
没有实现方法A
。然后在子类的分类里mehod-swizzling
了方法A
,并指向分类的方法category_A
。这时,如果父类调用方法A,就会崩溃。因为父类找不到方法category_A
。 解决:mehod-swizzling
前先尝试class_addMethod
。
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod)
{
class_replaceMethod(class, swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
else
{ //已有此方法,直接交换
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- 如果父类子类都没有实现方法,则需要在
class_addMethod
前执行
if (!oriMethod) {
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ //空实现 }));
}
本文是本人笔记整理而成,可能不太面向小白,是一个总结性质的,也可能有点乱。若有不对的,欢迎指出哦。
网友评论