1、概述
2、isa ,SEL,IMP, Method 关系
3、消息机制 以及消息转发机制
4、runtime的使用场景
5、参考文章
......
概述:
runtime
:Objective-C
是动态语言,它将很多静态语言在编译和链接时做的事放到了运行时,这个运行时系统就是runtime
。
runtime
其实就是一个库,它基本上是用C和汇编写的一套API,这个库使C语言有了面向对象的能力。
OC
是动态语言:函数真正调用的时机是在运行时,在运行的时候根据函数的名称找到对应的函数来调用。
isa ,SEL,IMP, Method 关系
OC中,类和类的实例在本质上没有区别,都是对象,任何对象都有isa
指针,它指向类或元类
SEL:SEL
(选择器)是方法的selector
的指针。方法的selector
表示运行时方法的名字。OC
在编译时,会依据每一个方法的名字、参数,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
。
IMP:IMP是一个函数指针,指向方法最终实现的首地址。SEL就是为了查找方法的最终实现IMP。
Method:用于表示类定义中的方法,它的结构体中包含一个SEL和IMP,相当于在SEL和IMP之间作了一个映射。
消息机制与消息转发机制
既然是运行时机制, 那么处理消息的时机就在运行时的时候处理。消息直到运行时才绑定到方法的实现上。编译器会将消息表达式[receiver message]
转化为一个消息函数,即objc_msgSend(receiver, selector)
。
isa
指针的作用:当我们向一个对象发送消息时,runtime
会根据这个对象的isa指针找到这个对象所属的类,在这个类的方法列表及父类的方法列表中,寻找与消息对应的selector
指向的方法,找到后就运行这个方法。
执行顺序是这样的:
1. 通过对象的`isa`指针获取类的结构体。
2. 在结构体的方法表里查找方法的`selector`。
3. 如果没有找到`selector`,则通过`objc_msgSend`结构体中指向父类的指针找到父类,并在父类的方法表里查找方法的`selector`。
4. 依次会一直找到`NSObject`。
5. 一旦找到`selector`,就会获取到方法实现`IMP`。
6. 传入相应的参数来执行方法的具体实现。
7. 如果最终没有定位到`selector`,就会走消息转发流程。
接上面最后一步,如果还是没有定位到selector
,那么怎么进行消息转发呢。
消息转发分为三个步骤:
1、动态方法解析,我们可以利用运行时绑定方法。当消息机制触发时,selector
为function
时那么就会利用运行时库动态添加一个方法,而这个方法的函数实现是我们用C
写的一个函数,这个函数的两个参数为self
,和_cmd
,因为OC
方法的本质就是至少包含两个参数的C函数这两个参数便是隐藏的self
,和_cmd
,前者是消息接受者,后者是一个SEL
指针
+(BOOL)resolveClassMethod:(SEL)sel{
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(function)) {
class_addMethod(self, sel,(IMP)function , "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
void function (id self,SEL _cmd)
{
NSLog(@"绑定方法");
}
2、习惯一般称为备用者。
-(id)forwardingTargetForSelector:(SEL)aSelector{
return [NewObject new]; //这个备用对象实现了需要的方法
}
3、如果上面两个方法都没有实现,没有进行动态绑定也没有指定备用对象处理,会执行这一步。首先进行签名,然后返回给NSInvocation
对象使用。
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector==@selector(function)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector =[anInvocation selector];
NewObject *new1=[NewObject new];
NewObject *new2=[NewObject new];
if ([RP1 respondsToSelector:selector]) {
[anInvocation invokeWithTarget:new1];
} if ([RP2 respondsToSelector:selector]){
[anInvocation invokeWithTarget:new2];
}
}
可以看到消息可以可以转发给多个对象进行处理.
runtime的使用场景
1、字典转模型等,可以参考MJExtension
源码 或者其他JsonModel
等。
2、方法交换(黑魔法)。
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{
Class class = [self class];
//原有方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//替换原有方法的新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
BOOL didAddMethod = class_addMethod(class,originalSelector,
method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
class_replaceMethod(class,swizzledSelector,
method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
method_exchangeImplementations(originalMethod, swizzledMethod); } }
@end
@implementation UIViewController (LXSwizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)];
});
}
- (void)sure_viewWillDisappear:(BOOL)animated {
[self sure_viewWillDisappear:animated];
[MBProgressHUD hideHUDForView:self.view animated:YES];
}
为什么方法交换调用在+load方法中?
在Objective-C runtime
会自动调用两个类方法,分别为+load
与+ initialize
。+load
方法是在类被加载的时候调用的,也就是一定会被调用。而+initialize
方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize
方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。此外+load
方法还有一个非常重要的特性,那就是子类、父类和分类中的+load
方法的实现是被区别对待的。换句话说在Objective-C runtime
自动调用+load
方法时,分类中的+load
方法并不会对主类中的+load
方法造成覆盖。综上所述,+load
方法是实现 Method Swizzling
逻辑的最佳“场所”。如需更深入理解
为什么方法交换要在dispatch_once
中执行?
方法交换应该要线程安全,而且保证在任何情况下(多线程环境,或者被其他人手动再次调用+load
方法)只交换一次,防止再次调用又将方法交换回来。除非只是临时交换使用,在使用完成后又交换回来。 最常用的解决方案是在+load
方法中使用dispatch_once
来保证交换是安全的。之前有读者反馈+load
方法本身即为线程安全,为什么仍需添加dispatch_once
,其原因就在于+load
方法本身无法保证其中代码只被执行一次。
3、给分类添加属性
使用 objc_getAssociatedObject 和 objc_setAssociatedObject 来做到存取方法。
#import "Person+AddAttribute.h"
#import
static NSString *ageKey = @"ageKey";
static NSString *ageKey2 = @"ageKey2";
@implementation Person (AddAttribute)
-(void)setAge:(NSString *)age{
objc_setAssociatedObject(self, &ageKey, age, OBJC_ASSOCIATION_RETAIN);
}
-(NSString *)age{
return objc_getAssociatedObject(self, &ageKey);
}
-(void)setAge2:(NSUInteger)age2{
objc_setAssociatedObject(self, &ageKey2, @(age2), OBJC_ASSOCIATION_ASSIGN);
}
-(NSUInteger)age2{
NSNumber *numVaue = objc_getAssociatedObject(self, &ageKey2);
return [numVaue integerValue];
}
4.关于强制横竖屏的时候有用到。
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
SEL selector = NSSelectorFromString(@"setOrientation:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevice currentDevice]];
int val = UIInterfaceOrientationPortrait;
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}
5、获得成员列表与属性列表
unsigned int count = 0; /** Ivar:表示成员变量类型 */
Ivar *ivars = class_copyIvarList([UITextField class], &count);
//获得一个指向该类成员变量的指针
for (int i =0; i < count; i ++) {
//获得Ivar
Ivar ivar = ivars[i];
//根据ivar获得其成员变量的名称--->C语言的字符串
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
NSLog(@"%d----%@",i,key);
}
NSLog(@"\n");
unsigned int count2 = 0;
//属性列表
objc_property_t *properties = class_copyPropertyList([UIButton class], &count2);
for (int i =0; i
6、埋点
iOS动态性(二)可复用而且高度解耦的用户统计埋点实现
参考文章
http://www.jianshu.com/p/d6a68575ce10
http://www.jianshu.com/p/91708b5b0501
Runtime Method Swizzling开发实例汇总
打个断点。后续补充