Runtime消息传递:
一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:
首先,通过obj的isa指针找到它的class;
在class的method list找foo;
如果class中没到foo,继续往它的superclass中找 ;
一旦找到foo这个函数,就去执行它的实现IMP。、
但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。
缓存查找是Hash查找 排序的方法方法列表查找是二分查找 未排序的方法方法列表查找是遍历查找
对象(object),类(class),方法(method)这几个的结构体:
//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
消息传递用到的一些概念:
isa指针:
指针型isa :isa的值代表class的地址
非指针型isa:isa的值的部分代表class的地址
实例对象的 isa 指向类对象
类对象的 isa 指向元类对象
元类对象的 isa 指向元类的基类
类对象(objc_class)
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。
objc_class结构体的定义如下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
实例(objc_object)
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
元类(Meta Class)
就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass)
通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object结构体实例它的isa指针指向类对象,
类对象的isa指针指向了元类,super_class指针指向了父类的类对象,根类指向nil
而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了自己。
Method(objc_method)
runtime.h
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
Method和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码
SEL(objc_selector)
selector是SEL的一个实例。
IMP
类缓存(objc_cache) 局部性原理
Category(objc_category)
Runtime消息转发:
动态方法解析
首先,Objective-C运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}
备用接收者
如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,进入下一步转发
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
Runtime应用:
$\color{red}{关联对象(Objective-C Associated Objects)给分类增加属性}$
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
这里,第一个参数是主对象,第二个参数是键,第三个参数是关联的对象,第四个参数是存储策略:是枚举,定义了内存管理语义。
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
本质原理:
关联对象由AssociationsManage管理并在AssociationsHashMap中存储,所有对象的关联内容都存在一个全局的容器中。
方法魔法(Method Swizzling)方法添加和替换和KVO实现
@implementation ViewController
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(jkviewDidLoad);
Method originalMethod = class_getInstanceMethod(class,originalSelector);
Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
//judge the method named swizzledMethod is already existed.
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// if swizzledMethod is already existed.
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)jkviewDidLoad {
NSLog(@"替换的方法");
[self jkviewDidLoad];
}
- (void)viewDidLoad {
NSLog(@"自带的方法");
[super viewDidLoad];
}
@end
消息转发(热更新)解决Bug(JSPatch)
JSPatch 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。
关于消息转发,前面已经讲到过了,消息转发分为三级,我们可以在每级实现替换功能,实现消息转发,从而不会造成崩溃。JSPatch不仅能够实现消息转发,还可以实现方法添加、替换能一系列功能。
实现NSCoding的自动归档和自动解档
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
实现字典和模型的自动转换(MJExtension)
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
面试题:
1. class_copyIvarList & class_copyPropertyList区别?
交互两个方法的现实有什么风险?
第一种实现:
void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
Method originalMethod = class_getInstanceMethod(cls, origSelector);
Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
IMP previousIMP = class_replaceMethod(cls, origSelector, method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
class_replaceMethod(cls, newSelector, previousIMP,method_getTypeEncoding(originalMethod));
}
第一种方法,用class_replaceMethod()实现的。由于A中不存在hookPrint方法,class_replaceMethod会调用class_addMethod方法,而class_addMethod会把Base的hookPrint实现添加到当前类,print的实现最终会和hookPrint的实现交换。B中俩方法都不存在,也会添加俩方法,最终交换俩方法的实现。最终打印的时候,会调用Base的hookPrint方法。
如果调用[a hookPrint:@"hello_k"];那么最终实现的应该是A类中print的实现。结果是:A obj print say:hello_k。
第二种实现:
void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
Method originalMethod = class_getInstanceMethod(cls, origSelector);
Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
第二种方法,用method_exchangeImplementations实现。由于A没有实现hookPrint方法,在调用
[A twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的时候,将A的print实现与Base的hookPrint实现交换了。
接着调用 [B twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的时候,由于B类啥都没实现,它只能将Base的print实现与base的hookPrint交换了。最终,调用[b print:@"hello2"]的时候,调用的是代码中A类的print方法的实现。
俩中方法都用到了class_getInstanceMethod(cls, selector)方法,这个方法有个特点:如果这个类中没有实现selector这个方法,它返回的是它某父类的 Method 对象(沿着继承链找到为止)。
当我们写的类没有继承的关系的时候,俩种方法都没什么问题。当有继承关系又不确定方法实现没实现,最好用class_replaceMethod方法。当啥都不确定的时候,老老实实地用class_replaceMethod 吧,安全无痛苦
2. Aspects实现的核心原理是什么?
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;、
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
Aspects就是利用了消息转发机制,通过hook第三层的转发方法forwardInvocation:,然后根据切面的时机来动态调用block。接下来详细分析巧妙的设计
Aspects或者Runtime方法交换使用前提:
把"retain", "release", "autorelease", "forwardInvocation:这几个加入集合中,判断集合中是否包含传入的selector,如果包含返回NO,这也说明Aspects不能对这几个函数进行hook操作;
判断selector是不是dealloc方法,如果是切面时机必须是AspectPositionBefore,要不然就会报错并返回NO,dealloc之后对象就销毁,所以切片时机只能是在原方法调用之前调用
判断类和实例对象是否可以响应传入的selector,不能就返回NO
判断是不是元类,如果是元类,判断方法有没有被hook过,如果没有就保存数据,一个方法在一个类的层级里面只能hook一次
2.上报机制
在收集到埋点信息之后,会有一个上报到服务器进行统计分析的机制,比如实时发送、启动时发送、最小间隔时间发送等。服务器在接收到这些数据信息之后,按自己整理的计算统计规则得出最后的数据报表,提供给相关人员对项目进行分析使用。
面试题:
1. 动态绑定?
2.什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
3._objc_msgForward函数是做什么的,直接调用它将会发生什么?
_objc_msgForward是IMP类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
在“消息传递”过程中,objc_msgSend的动作比较清晰:首先在类中的缓存查找IMP(没缓存则初始化缓存),如果没找到,则向父类的类查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替IMP。最后,执行这个IMP。
直接调用_objc_msgForward是非常危险的事,如果用不好会直接导致程序崩溃,但是如果用得好,能做很多非常酷的事。
一旦调用_objc_msgForward,将跳过查找IMP的过程,直接触发“消息转发”,
如果调用了_objc_msgForward,即使这个对象确实已经实现了这个方法,你也会告诉objc_msgSend:
“我没有在这个对象里找到这个方法的实现”
有哪些场景需要直接调用_objc_msgForward?最常见的场景是:你想获取某方法所对应的NSInvocation对象。表示说明:
JSPatch就是直接调用_objc_msgForward来实现其核心功能的:
JSPatch以小巧的体积做到了让JS调用/替换任意OC方法,让iOS APP具有热更新的能力。
4. objc中向一个nil对象发送消息将会发生什么?
先说结论:OC中向nil发消息,程序是不会崩溃的
因为OC的函数都是通过objc_msgSend进行消息发送来实现的,相对于C和C++来说,对于空指针的操作会引起crash问题,而objc_msgSend会通过判断self来决定是否发送消息,如果self为nil,那么selector也会为空,直接返回,不会出现问题。视方法返回值,向nil发消息可能会返回nil(返回值为对象),0(返回值为一些基础数据)或0X0(返回值为id)等。但对于[NSNull null]对象发送消息时,是会crash的,因为NSNull类只有一个null方法
5. objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?
在OC中,给对象发送消息的语法是:id returnValue = [someObject messageName:parameter];
编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下
void objc_msgSend(id self, SEL cmd, ...)
因此,上述以OC形式展现出来的函数就会转化成如下函数:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
这个函数会在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就去实现代码,如果找不到就沿着继承体系继续向上查找。如果找到了就执行,如果最终还是找不到,就执行消息转发操作。
6. 什么时候会报unrecognized selector的异常?
当调用该对象上某个方法,而该对象上没有实现该方法的时候,可以通过“消息转发”进行解决。
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中查找方法运行,如果,在最上方的父类中依然找到相应的方法时,程序在运行时会挂掉并引发异常,但无法识别的选择器已发送到XXX。但是在这之前,objc的运行时会通过三次拯救程序崩溃的机会:
1. 方法解析
objc时运行会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移到下一步,消息转发
2. 快速转发
如果目标对象实现了-forwardingTargetForSelector:,运行时这时就会调用这个方法,给你把这个消息转发给其他对象的机会。只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。
3.正常转发
这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,运行时就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。
7. 一个objc对象的isa的指针指向什么?有什么作用?
对象的isa指针指向所属的类
类的isa指针指向了所属的元类
元类的isa指向了根元类,根元类指向了自己。
isa帮助一个对象找到它的方法。isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
8. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
1.不能向编译后得到的类增加实例变量
2.能向运行时创建的类中添加实例变量
解释:
1.编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,runtime会调用class_setvarlayout或class_setWeaklvarLayout来处理strong weak引用.所以不能向存在的类中添加实例变量
2.运行时创建的类是可以添加实例变量,调用class_addIvar函数.但是的在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上.
9.runtime如何实现weak变量的自动置nil?
runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
10.给类添加一个属性后,在类结构体里哪些元素会发生变化?
11.运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么?
12.有没有用过运行时,用它都能做什么?(交换方法,创建类,给新创建的类增加方法,改变isa指针)
13.runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)?
每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,实际上选择器实质上就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现。
参考NSObject上面的方法:
-(IMP)methodForSelector :( SEL)aSelector;+(IMP)instanceMethodForSelector :( SEL)aSelector;
14.使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在MRC下还是ARC下均不需要
15. 一个对象的isa指针指向什么?有什么作用?
16.objc_msgForward 函数是做什么的,直接调用会怎么样?
17.class_copyIvarList & class_copyPropertyList区别?
18.交互两个方法的现实有什么风险?
19.Aspects实现的核心原理是什么?哪些方法不能被hook
(1)不允许hookretain、release、autorelease、forwardInvocation:
(2)允许hookdealloc,但是只能在dealloc执行前,这都是为了程序的安全性设置的
(3)检查这个方法是否存在,不存在则不能hook
(4)Aspects对于hook的生效作用域做了区分:所有实例对象&某个具体实例对象。对于所有实例对象在整个继承链中,同一个方法只能被hook一次,这么做的目的是为了规避循环调用的问题
15.在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)?
20. 消息转发三种方法的防护优劣?
https://www.jianshu.com/p/9fac4ed527e3
可以利用消息转发机制来做文章。那么问题来了,在这三个步骤里面,选择哪一步去改造比较合适呢。
这里我们选择了第二步forwardingTargetForSelector来做文章。原因如下:
resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
选择了forwardingTargetForSelector之后,可以将NSObject的该方法重写,做以下几步的处理:
1.动态创建一个桩类
2.动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
3.将消息直接转发到这个桩类对象上。
注意如果对象的类本事如果重写了forwardInvocation方法的话,就不应该对forwardingTargetForSelector进行重写了,否则会影响到该类型的对象原本的消息转发流程。
通过重写NSObject的forwardingTargetForSelector方法,我们就可以将无法识别的方法进行拦截并且将消息转发到安全的桩类对象中,从而可以使app继续正常运行。
21. 无埋点实现的原理?