[转]runtime 详解
Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。Objective-C 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
- RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。
- 对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。
- OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。
- 只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
我们写的oc代码,它在运行的时候也是转换成了runtime方式运行的,更好的理解runtime,也能帮我们更深的掌握oc语言。
每一个oc的方法,底层必然有一个与之对应的runtime方法。
<ignore_js_op>
-
当我们用OC写下这样一段代码
[tableView cellForRowAtIndexPath:indexPath]; -
在编译时RunTime会将上述代码转化成[发送消息]
objc_msgSend(tableView, @selector(cellForRowAtIndexPath,indexPath);
-
获取属性列表
1
2
3
4
5
|
objc_property_t *propertyList = class_copyPropertyList([ self class ], &count); for (unsigned int i=0; i<count; i++) { const char *propertyName = property_getName(propertyList[i]);[/i] [i] NSLog ( @"property---->%@" , [ NSString stringWithUTF8String:propertyName]);[/i] [i] } |
[color=rgba(0, 0, 0, 0.6)]获取方法列表
1
2
3
4
5
|
ethod *methodList = class_copyMethodList([ self class ], &count); for (unsigned int i; i<count; i++) { Method method = methodList[i]; NSLog ( @"method---->%@" , NSStringFromSelector (method_getName(method))); } |
1
2
3
4
5
6
|
Ivar *ivarList = class_copyIvarList([ self class ], &count); for (unsigned int i; i<count; i++) { Ivar myIvar = ivarList[i]; const char *ivarName = ivar_getName(myIvar); NSLog ( @"Ivar---->%@" , [ NSString stringWithUTF8String:ivarName]); } |
1
2
3
4
5
6
|
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([ self class ], &count); for (unsigned int i; i<count; i++) { Protocol *myProtocal = protocolList[i]; const char *protocolName = protocol_getName(myProtocal); NSLog ( @"protocol---->%@" , [ NSString stringWithUTF8String:protocolName]); } |
1
2
3
|
Class PersonClass = object_getClass([Person class ]); SEL oriSEL = @selector (test1); Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL); |
1
2
3
|
Class PersonClass = object_getClass([xiaoming class ]); SEL oriSEL = @selector (test2); Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL); |
添加方法
1
|
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod)); |
替换原方法实现
1
|
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); |
交换两个方法
1
|
method_exchangeImplementations(oriMethod, cusMethod); |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
-( void )answer{ unsigned int count = 0; Ivar ivar = class_copyIvarList([ self .xiaoMing class ], &count); for ( int i = 0; i<count; i++) { Ivar var = ivar[i]; const char varName = ivar_getName(var); NSString *name = [ NSString stringWithUTF8String:varName]; if ([name isEqualToString:@”_age”]) { object_setIvar( self .xiaoMing, var, @”20”); break ; } } NSLog (@”XiaoMing’s age is %@”, self .xiaoMing.age); } |
2.动态添加方法
1
|
class_addMethod([ self .xiaoMing class ], @selector (guess), (IMP)guessAnswer, "v@: " ); |
1
2
3
|
(IMP)guessAnswer 意思是guessAnswer的地址指针; "v@:" 意思是,v代表无返回值 void ,如果是i则代表 int ;@代表 id sel; : 代表 SEL _cmd; “v@:@@” 意思是,两个参数的没有返回值。 |
2.调用guess方法响应事件:
[self.xiaoMing performSelectorselector(guess)];
3.编写guessAnswer的实现:
void guessAnswer(id self,SEL _cmd){
NSLog(@"i am from beijing");
}
这个有两个地方留意一下:
- void的前面没有+、-号,因为只是C的代码。
- 必须有两个指定参数(id self,SEL _cmd)
4.代码参考
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
-( void )[i]answer{[/i] class_addMethod([ self .xiaoMing class ], @selector (guess), (IMP)guessAnswer, "v@:" ); if ([ self .xiaoMing respondsToSelector: @selector (guess)]) { [ self .xiaoMing performSelector: @selector (guess)]; } else { NSLog ( @"Sorry,I don't know" ); } } void guessAnswer( id self , SEL _cmd){ NSLog ( @"i am from beijing" ); } |
3:动态交换两个方法的实现
在程序当中,假设XiaoMing的中有test1 和 test2这两个方法,后来被Runtime交换方法后,每次调动test1 的时候就会去执行test2,调动test2 的时候就会去执行test1, 。那么,Runtime是如何做到的呢?
- 获取这个类中的两个方法并交换
1
2
3
|
Method m1 = class_getInstanceMethod([ self .xiaoMing class ], @selector (test1)); Method m2 = class_getInstanceMethod([ self .xiaoMing class ], @selector (test2)); method_exchangeImplementations(m1, m2); |
4:拦截并替换方法
在程序当中,假设XiaoMing的中有test1这个方法,但是由于某种原因,我们要改变这个方法的实现,但是又不能去动它的源代码(正如一些开源库出现问题的时候),这个时候runtime就派上用场了。
我们先增加一个tool类,然后写一个我们自己实现的方法-change,
通过runtime把test1替换成change。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Class PersionClass = object_getClass([Person class ]); Class toolClass = object_getClass([tool class ]); ////源方法的SEL和Method SEL oriSEL = @selector (test1); Method oriMethod = class_getInstanceMethod(PersionClass, oriSEL); ////交换方法的SEL和Method SEL cusSEL = @selector (change); Method cusMethod = class_getInstanceMethod(toolClass, cusSEL); ////先尝试給源方法添加实现,这里是为了避免源方法没有实现的情况 BOOL addSucc = class_addMethod(PersionClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod)); if (addSucc) { // 添加成功:将源方法的实现替换到交换方法的实现 class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); } else { //添加失败:说明源方法已经有实现,直接将两个方法的实现交换即 method_exchangeImplementations(oriMethod, cusMethod); } |
5:在方法上增加额外功能
有这样一个场景,出于某些需求,我们需要跟踪记录APP中按钮的点击次数和频率等数据,怎么解决?当然通过继承按钮类或者通过类别实现是一个办法,但是带来其他问题比如别人不一定会去实例化你写的子类,或者其他类别也实现了点击方法导致不确定会调用哪一个,runtime可以这样解决:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@implementation UIButton (Hook) + ( void )load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class selfClass = [ self class ]; SEL oriSEL = @selector (sendAction:to:forEvent:); Method oriMethod = class_getInstanceMethod(selfClass, oriSEL); SEL cusSEL = @selector (mySendAction:to:forEvent:); Method cusMethod = class_getInstanceMethod(selfClass, cusSEL); BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod)); if (addSucc) { class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); } else { method_exchangeImplementations(oriMethod, cusMethod); } }); } - ( void )mySendAction:( SEL )action to:( id )target forEvent:(UIEvent *)event { [CountTool addClickCount]; [ self mySendAction:action to:target forEvent:event]; } @end |
6.实现NSCoding的自动归档和解档
如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
假设现在有一个Movie类,有3个属性,它的h文件这这样的
01
02
03
04
05
06
07
08
09
10
|
#import <Foundation/Foundation.h> //1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding @interface Movie : NSObject < NSCoding > @property ( nonatomic , copy ) NSString *movieId; @property ( nonatomic , copy ) NSString *movieName; @property ( nonatomic , copy ) NSString *pic_url; @end |
如果是正常写法, m文件应该是这样的:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
#import "Movie.h" @implementation Movie - ( void )encodeWithCoder:( NSCoder *)aCoder { [aCoder encodeObject:_movieId forKey: @"id" ]; [aCoder encodeObject:_movieName forKey: @"name" ]; [aCoder encodeObject:_pic_url forKey: @"url" ]; } - ( id )initWithCoder:( NSCoder *)aDecoder { if ( self = [ super init]) { self .movieId = [aDecoder decodeObjectForKey: @"id" ]; self .movieName = [aDecoder decodeObjectForKey: @"name" ]; self .pic_url = [aDecoder decodeObjectForKey: @"url" ]; } return self ; } @end |
如果这里有100个属性,那么我们也只能把100个属性都给写一遍。
不过你会使用runtime后,这里就有更简便的方法。
下面看看runtime的实现方式:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#import "Movie.h" #import <objc/runtime.h> @implementation Movie - ( void )encodeWithCoder:( NSCoder *)encoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList([Movie class ], &count); for ( int i = 0; i<count; i++) { // 取出i位置对应的成员变量 Ivar ivar = ivars; // 查看成员变量 const char *name = ivar_getName(ivar); // 归档 NSString *key = [ NSString stringWithUTF8String:name]; id value = [ self valueForKey:key]; [encoder encodeObject:value forKey:key]; } free(ivars); } - ( id )initWithCoder:( NSCoder *)decoder { if ( self = [ super init]) { unsigned int count = 0; Ivar *ivars = class_copyIvarList([Movie class ], &count); for ( int i = 0; i<count; i++) { // 取出i位置对应的成员变量 Ivar ivar = ivars; // 查看成员变量 const char *name = ivar_getName(ivar); // 归档 NSString *key = [ NSString stringWithUTF8String:name]; id value = [decoder decodeObjectForKey:key]; // 设置到成员变量身上 [ self setValue:value forKey:key]; } free(ivars); } return self ; } @end |
这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,还嫌麻烦,下面看看更加简便的方法:两句代码搞定。
我们把encodeWithCoder 和 initWithCoder这两个方法抽成宏
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#import "Movie.h" #import <objc/runtime.h> #define encodeRuntime(A) \ \ unsigned int count = 0;\ Ivar *ivars = class_copyIvarList([A class ], &count);\ for ( int i = 0; i<count; i++) {\ Ivar ivar = ivars;\ const char *name = ivar_getName(ivar);\ NSString *key = [ NSString stringWithUTF8String:name];\ id value = [ self valueForKey:key];\ [encoder encodeObject:value forKey:key];\ }\ free(ivars);\ \ #define initCoderRuntime(A) \ \ if ( self = [ super init]) {\ unsigned int count = 0;\ Ivar *ivars = class_copyIvarList([A class ], &count);\ for ( int i = 0; i<count; i++) {\ Ivar ivar = ivars;\ const char *name = ivar_getName(ivar);\ NSString *key = [ NSString stringWithUTF8String:name];\ id value = [decoder decodeObjectForKey:key];\ [ self setValue:value forKey:key];\ }\ free(ivars);\ }\ return self ;\ \ @implementation Movie - ( void )encodeWithCoder:( NSCoder *)encoder { encodeRuntime(Movie) } - ( id )initWithCoder:( NSCoder *)decoder { initCoderRuntime(Movie) } @end |
我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。
7.实现字典转模型的自动转换
字典转模型的应用可以说是每个app必然会使用的场景,虽然实现的方式略有不同,但是原理都是一致的:遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
像几个出名的开源库:JSONModel,MJExtension等都是通过这种方式实现的。
- 先实现最外层的属性转换
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// 创建对应模型对象 id objc = [[ self alloc] init]; unsigned int count = 0; // 1.获取成员属性数组 Ivar *ivarList = class_copyIvarList( self , &count); // 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值 for ( int i = 0; i < count; i++) { // 2.1 获取成员属性 Ivar ivar = ivarList; // 2.2 获取成员属性名 C -> OC 字符串 NSString *ivarName = [ NSString stringWithUTF8String:ivar_getName(ivar)]; // 2.3 _成员属性名 => 字典key NSString *key = [ivarName substringFromIndex:1]; // 2.4 去字典中取出对应value给模型属性赋值 id value = dict[key]; // 获取成员属性类型 NSString *ivarType = [ NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; } |
- 内层数组,字典的转换
[Objective-C] 查看源文件 复制代码0102030405060708091011121314151617181920212223242526272829303132333435363738394041424344if
([value isKindOfClass:[
NSDictionary
class
]] && ![ivarType containsString:
@"NS"
]) {
// 是字典对象,并且属性名对应类型是自定义类型
// 处理类型字符串 @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:
@"@"
withString:
@""
];
ivarType = [ivarType stringByReplacingOccurrencesOfString:
@"\""
withString:
@""
];
// 自定义对象,并且值是字典
// value:user字典 -> User模型
// 获取模型(user)类对象
Class modalClass =
NSClassFromString
(ivarType);
// 字典转模型
if
(modalClass) {
// 字典转模型 user
value = [modalClass objectWithDict:value];
}
}
if
([value isKindOfClass:[
NSArray
class
]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
if
([
self
respondsToSelector:
@selector
(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id
idSelf =
self
;
// 获取数组中字典对应的模型
NSString
*type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel =
NSClassFromString
(type);
NSMutableArray
*arrM = [
NSMutableArray
array];
// 遍历字典数组,生成模型数组
for
(
NSDictionary
*dict in value) {
// 字典转模型
id
model = [classModel objectWithDict:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
我自己觉得系统自带的KVC模式字典转模型就挺好的,假设movie是一个模型对象,dict 是一个需要转化的 [movie setValuesForKeysWithDictionary:dict]; 这个是系统自带的字典转模型方法,个人感觉也还是挺好用的,不过使用这个方法的时候需要在模型里面再实现一个方法才行,
- (void)setValue: (id)value forUndefinedKey: (NSString *)key 重写这个方法为了实现两个目的:1. 模型中的属性和字典中的key不一致的情况,比如字典中有个id,我们需要把它赋值给uid属性;2. 字典中属性比模型的属性还多的情况。
如果出现以上两种情况而没有实现这个方法的话,程序就会崩溃。
这个方法的实现:
1
2
3
4
5
6
|
- ( void )setValue:( id )value forUndefinedKey:( NSString *)key { if ([key isEqualToString: @"id" ]) { self .uid = value; } } |
六.几个参数概念
如果从头仔细看到尾,相信你基本的用法应该会了,虽然会用是主要的目的,有几个基本的参数概念还是要了解一下的。
1.objc_msgSend
1
2
3
4
5
6
7
8
9
|
/* Basic Messaging Primitives * * On some architectures, use objc_msgSend_stret for some struct return types. * On some architectures, use objc_msgSend_fpret for some float return types. * On some architectures, use objc_msgSend_fp2ret for some float return types. * * These functions must be cast to an appropriate function pointer type * before being called. */ |
这是官方的声明,从这个函数的注释可以看出来了,这是个最基本的用于发送消息的函数。另外,这个函数并不能发送所有类型的消息,只能发送基本的消息。比如,在一些处理器上,我们必须使用objc_msgSend_stret来发送返回值类型为结构体的消息,使用objc_msgSend_fpret来发送返回值类型为浮点类型的消息,而又在一些处理器上,还得使用objc_msgSend_fp2ret来发送返回值类型为浮点类型的消息。
最关键的一点:无论何时,要调用objc_msgSend函数,必须要将函数强制转换成合适的函数指针类型才能调用。
从objc_msgSend函数的声明来看,它应该是不带返回值的,但是我们在使用中却可以强制转换类型,以便接收返回值。另外,它的参数列表是可以任意多个的,前提也是要强制函数指针类型。
其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。
2.SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法),Cocoa 中有好多长长的方法哦。3.id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档.4.Class
之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:
typedef struct objc_class *Class;
objc_class里面的东西多着呢:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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; |
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class结构体中:ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是Category实现的原理.