1.典型应用场景:
- 分类增加属性(关联对象(Objective-C Associated Objects))
- 消息转发(热更新)解决Bug(JSPatch)
- 方法魔法(Method Swizzling)方法添加和替换和KVO实现
- 实现NSCoding的自动归档和自动解档
- 实现字典和模型的自动转换(MJExtension)
2.相关技术链接
整理runtime文档
苹果维护的开源代码
苹果和GNU各自维护一个开源的Runtime开源
3.基本介绍
Runtime 基本是用 C 和汇编写的,它使用来处理OC转换C的中间结构体。
高级编程语言 -> 汇编语言 - > 机器语言
OC(面向对象)-> 过渡runtime实现->C语言(面向过程)
[obj foo] 编译器转换objc_msgSend(obj, foo),runtime执行流程:
- 首先,通过obj的isa指针找到它的 class ;
- 在 class 的 method list 找 foo ;
- 如果 class 中没到 foo,继续往它的 superclass 中找 ;
- 一旦找到 foo 这个函数,就去执行它的实现IMP 。
- 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list
4.数据结构
//实例(objc_object)
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct object_class{
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
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;
}
// Method(objc_method)
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; 方法名
char *method_types OBJC2_UNAVAILABLE; 方法类型
IMP method_imp OBJC2_UNAVAILABLE; 方法实现
}
元类(Meta Class)
是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass), 元类中保存了创建类对象以及类方法
所需的所有信息。
objc_object结构体实例它的isa指针指向了类对象objc_class,它的isa指针指向了metaclass,元类isa指向了NSobject元类。
类缓存(objc_cache)
当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。
Category(objc_category)
Category是表示一个指向分类的结构体的指针,其定义如下:
struct objc_category{
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性
复制代码
5.方法与消息
SEL
又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
复制代码
通过下面三种方法可以获取SEL:
a、sel_registerName函数
SEL deallocSelector = sel_registerName("dealloc");
复制代码
b、Objective-C编译器提供的@selector()
SEL deallocSelector = @selector(dealloc);
复制代码
c、NSSelectorFromString()方法
SEL selector = NSSelectorFromString(@"setOrientation:");
复制代码
IMP
实际上是一个函数指针,指向方法实现的地址。 其定义如下:
id (*IMP)(id, SEL,...)
复制代码
第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
第二个参数:是方法选择器(selector)接下来的参数:方法的参数列表。
Method
用于表示类定义中的方法,则定义如下:
typedef struct objc_method *Method
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
复制代码
- 方法调用流程 消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。
objc_msgSend(receiver, selector)
复制代码
如果消息中还有其它参数,则该方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2,...)
复制代码
- 消息转发处理 消息转发机制基本上分为三个步骤: 1>、动态方法解析 2>、备用接收者 3>、完整转发 没有处理就程序崩溃
动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。 在这个方法中,我们有机会为该未知消息新增一个“处理方法”,通过运行时class_addMethod函数动态添加到类里面就可以了。 这种方案更多的是为了实现@dynamic属性。
// 测试消息转发
- (void)runtimeTestMethodForward
{
//执行foo函数
[self performSelector:@selector(foo:) withObject:@10 withObject:@20]; // 最多传递2个参数
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(foo:)) {
//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)newFoo, “@@:”); /// ”v@:”意思就是这已是一个void类型的方法,没有参数传入,, “i@:”就是说这是一个int类型的方法,没有参数传入,,”i@:@”就是说这是一个int类型的方法,又一个参数传入;@@:传递对象
return YES;
}
return NO; // 运行时就会移到下一步:forwardingTargetForSelector
}
void newFoo(id obj, SEL _cmd, id params)
{
NSLog(@"doing new foo");
}
复制代码
备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector 如果在上一步无法处理消息,则Runtime会继续调以下方法: 如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (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 {
return YES;//返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
复制代码
完整消息转发 如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 我们首先要通过,指定方法签名,若返回nil,则表示不处理。 如下代码:
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,进入下一步转发
}
// 完整消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo:"]) {
return [NSMethodSignature signatureWithObjCTypes:"@@:"];//签名,进入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];
}
}
void newFoo(id obj, SEL _cmd, id params)
{
NSLog(@"doing new foo %@", params);
}
复制代码
应用 1.关联对象(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)
参数说明:
id object:被关联的对象
const void *key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略
内存管理策略:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0, // 表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401, // 表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403 // 表示关联对象copy,不是线程安全的
};
@interface UIView (DefaultColor)
@property (nonatomic, strong) UIColor *defaultColor;
@end
#import <objc/runtime.h>
@implementation UIView (DefaultColor)
@dynamic defaultColor;
static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor *)defaultColor
{
objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIColor *)defaultColor
{
return objc_getAssociatedObject(self, &kDefaultColorKey);
}
@end
复制代码
通过关联对象实现的属性的内存管理也是有ARC管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。
2.方法魔法(Method Swizzling)方法添加和替换和KVO实现 方法添加
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
参数说明:
cls 被添加方法的类
name 添加的方法的名称的SEL
imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
类型编码
方法替换
// 方法替换
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(crviewDidLoad);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 校验交换的方法是否已经存在
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
}else
{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)crviewDidLoad
{
NSLog(@"自定义的方法");
}
复制代码
+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用. swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。
查找消息的唯一依据是selector的名字。所以,我们可以利用Objective-C的runtime机制,实现在运行时交换selector对应的方法实现以达到我们的目的。 即运行时交换方法名。
每一个SEL与一个IMP一一对应,正常情况下通过SEL可以查找到对应消息的IMP实现。 交换后
实现NSCoding的自动归档和自动解档 原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。 核心方法:在Model的基类中重写方法:
- (instancetype)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];
}
free(ivars);
}
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];
}
free(ivars);
}
复制代码
测试整形,数据没有都可以正常存储数据
实现字典和模型的自动转换(MJExtension)
- (instancetype)initWithDic:(NSDictionary *)dic
{
if (self = [super init]) {
unsigned int outCount;
//获取类的属性及属性对应的类型
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 * key = [NSString stringWithUTF8String:property_getName(property)];
NSString *type = [NSString stringWithUTF8String:property_getAttributes(property)];
NSLog(@"type %@, key %@", type, key);
if ([dic valueForKey:key] == nil) continue;
[self setValue:[dic valueForKey:key] forKey:key];
}
//立即释放properties指向的内存
free(properties);
}
return self;
}
复制代码
总结:ivar和property区别
ivar = instanc variable 实例变量,仅仅是一个变量;必须存在;带下划线的变量”_name”
property = objc声明的属性,操作变量函数setter和getter;是可选的;没有带下滑线的变量“name”
复制代码