一、
RunTime简称运行时,就是系统在运行的时候的一些机制a.OC代码调用一个方法
1
|
[self.loginBt login];
|
b.在编译时RunTime会将上述代码转化成[发送消息]
1
|
objc_msgSend(self.loginB,@selector(login));
|
2、Class、对象
类名操作的函数主要有:
1
2
|
// 获取类的类名
const char * class_getName ( Class cls );
|
- 对于class_getName函数,如果传入的cls为Nil,则返回一个字字符串。
父类(super_class)和元类(meta-class)
父类和元类操作的函数主要有:
1
2
3
4
5
|
// 获取类的父类
Class class_getSuperclass ( Class cls );
// 判断给定的Class是否是一个元类
BOOL class_isMetaClass ( Class cls );
|
- class_getSuperclass函数,当cls为Nil或者cls为根类时,返回Nil。不过通常我们可以使用NSObject类的superclass方法来达到同样的目的。
- class_isMetaClass函数,如果是cls是元类,则返回YES;如果否或者传入的cls为Nil,则返回NO。
实例变量大小(instance_size)
实例变量大小操作的函数有:
1
2
|
// 获取实例大小
size_t class_getInstanceSize ( Class cls );
|
成员变量(ivars)及属性
在objc_class中,所有的成员变量、属性的信息是放在链表ivars中的。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针。runtime提供了丰富的函数来操作这一字段。大体上可以分为以下几类:
1.成员变量操作函数,主要包含以下函数:
1
2
3
4
5
6
7
8
9
10
11
|
// 获取类中指定名称实例成员变量的信息
Ivar class_getInstanceVariable ( Class cls, const char *name );
// 获取类成员变量的信息
Ivar class_getClassVariable ( Class cls, const char *name );
// 添加成员变量
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 获取整个成员变量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
|
- class_getInstanceVariable函数,它返回一个指向包含name指定的成员变量信息的objc_ivar结构体的指针(Ivar)。
- class_getClassVariable函数,目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。
- Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的提供的类,还是我们自定义的类,都无法动态添加成员变量。但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能在objc_allocateClassPair函数与objc_registerClassPair之间调用。另外,这个类也不能是元类。成员变量的按字节最小对齐量是1<<alignment。这取决于ivar的类型和机器的架构。如果变量的类型是指针类型,则传递log2(sizeof(pointer_type))。
- class_copyIvarList函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。
2.属性操作函数,主要包含以下函数:
1
2
3
4
5
6
7
8
9
10
11
|
// 获取指定的属性
objc_property_t class_getProperty ( Class cls, const char *name );
// 获取属性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
// 为类添加属性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 替换类的属性
void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
|
这一种方法也是针对ivars来操作,不过只操作那些是属性的值。我们在后面介绍属性时会再遇到这些函数。
二、常见的作用
1. 动态的添加对象的成员变量和方法
①动态获取类中的所有属性[当然包括私有]
1
|
Ivar *ivar = class_copyIvarList([self class], &count);
|
②遍历属性找到对应name字段
1
|
const char *varName = ivar_getName(
var
);
|
③修改对应的字段值
1
|
object_setIvar(self,
var
, @
"Test"
);
|
2. 动态交换两个方法的实现
①动态找到firstSay和secondSay方法
1
2
|
Method m1 = class_getInstanceMethod([self class], @selector(firstSay));
Method m2 = class_getInstanceMethod([self class], @selector(secondSay));
|
②交换两个方法
1
|
method_exchangeImplementations(m1, m2);
|
3. 动态添加方法
①动态给类中添加guess方法:
1
|
class_addMethod([self class], @selector(guess), (IMP)guessAnswer,
"v@:"
);
|
这里参数地方说明一下:
(IMP)guessAnswer 意思是guessAnswer的地址指针;
"v@:" 意思是,v代表无返回值void,如果是i则代表int;@代表 id sel; : 代表 SEL _cmd;
“v@:@@” 意思是,两个参数的没有返回值。
②调用guess方法响应事件:
1
|
[self.xiaoMing performSelector:@selector(guess)];
|
③编写guessAnswer的实现:
1
2
3
|
void guessAnswer(id self,SEL _cmd){
NSLog(@
"He is from GuangTong"
);
}
|
这个有两个地方留意一下:
a.void的前面没有+、-号,因为只是C的代码。
b.必须有两个指定参数(id self,SEL _cmd)
4.实现分类也可以添加属性
①申明chineseName属性
1
2
3
4
5
6
7
|
#import "XiaoMing.h"
@interface XiaoMing (MutipleName)
@property(nonatomic,copy) NSString *chineseName;
@end
|
②动态添加属性和实现方法
objc_setAssociatedObject 相当于iOS的属性的set方法
③使用chineseName属性
1
2
3
4
|
-(void)answer{
NSLog(@
"My Chinese name is %@"
,self.xiaoMing.chineseName);
self.chineseNameTf.text = self.xiaoMing.chineseName;
}
|
三、高级用法
1. Method Swizzling:Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。
2. 下载lua代码,动态地修改app中的OC代码,实现动态修复bug。
用法实例:
例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。
这种情况下,我们就可以使用Method Swizzling,如在代码所示:
在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。
使用method swizzling需要注意的一些问题:
Swizzling应该总是在+load中执行
在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。
Swizzling应该总是在dispatch_once中执行
与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。
选择器、方法与实现
在Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。
以下是Objective-C Runtime Reference中的对这几个术语一些描述:
- Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
- Method(typedef struct objc_method *Method):在类定义中表示方法的类型
- Implementation(typedef id (*IMP)(id, SEL, …)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。每一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。
理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。
为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。
调用_cmd
我们回过头来看看前面新的方法的实现代码:
1
2
3
4
|
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}
|
咋看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。
注意事项
Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:
- 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
- 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
- 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
- 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。
以下是run time提供的方法: