Runtime

概述

Runtime的特征主要是消息(方法)传递,如果消息在对象中找不到,就进行转发
它是OC面向对象和动态机制的基石
OC是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象,进行消息传递和转发

Runtime消息传递

一个对象的方法像这样[obj method],编译器会转成消息发送objc_msgSend(obj, method).

Runtime时执行的流程是这样的:
首先,通过objisa指针找到它的class;
classmethod listmethod;
如果class中没有method,继续往它的superclass中找;
一旦找到method这个函数,就去执行它的实现IMP.

但这种实现有一个问题,效率低.但一个class往往只有20%的函数会被经常调用,可能占到总调用次数的80%.每一个消息都需要遍历依次objc_method_list并不合理.如果把经常被调用的函数缓存起来,那可以大大提高函数查询的效率.这也就是objc_class中有另一个重要成员objc_cache做的事情–再找到method之后,把methodmethod_name作为key,method_imp作为value给存起来,当再次收到method消息的时候,可以直接在cache里找到,避免去遍历objc_method_list.obj_cache是存在objc_class结构体的.

objc_msgSend方法定义:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

对象(object)

struct objc_object {
	Class isa;
};

类(class):

struct objc_class {
	Class isa;
	Class super_class;
	const char *name;
	long verser;
	long info;
	long instance_size;
	struct objc_ivar_list *ivars;
	struct objc_method_list **methodLists;
	struct objc_cache *cache;
	struct objc_protocol_list *protocols
};

方法列表:

struct objc_method_list {
	struct objc_method_list *obsolete;
	int method_count;
	int space;
	struct objc_method method_list[1];
}

方法(method):

struct objc_method {
	SEL method_name;
	char *method_types;
	IMP method_imp;
}
  1. 系统首先找到消息的接收对象,然后通过对象的isa指针找到它的类.
  2. 在它的类中查找method_list,是否有selector方法.
  3. 没有则查找父类的method_list.
  4. 找到对应的method,执行它的IMP.
  5. 转发IMPreturn值.

一些概念

类对象(objc_class):

typedef struct objc_class *Class;

objc/runtime.h中:

struct objc_class {
	Class isa;
	Class super_class;
	const char *name;
	long verser;
	long info;
	long instance_size;
	struct objc_ivar_list *ivars;
	struct objc_method_list **methodLists;
	struct objc_cache *cache;
	struct objc_protocol_list *protocols
};

结构体里保存了指向父类的指针,类的名字,版本,实例大小,实例变量列表,方法列表,缓存,遵守的协议列表等.
objc_class 这个结构体存放的数据称为元数据(metadata).
该结构体的第一个成员变量也是isa指针,这就说明Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例.

实例(objc_object):

struct objc_object {
	Class isa;
};
typedef struct objc_object *id;

类对象中的元数据存储的都是如何创建一个实例的相关信息.
类对象和类方法就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass).
元类中保存了创建类对象以及类方法所需要的所有信息.

元类(Meta Class):
struct objc_object结构体实例的isa指针指向类对象
类对象的isa指针指向元类,super_class指针指向父类的类对象,
而元类的super_class指针指向了父类的元类,那元类的isa指针又指向自己.
元类是一个类对象的类.
所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法).
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体.这就引出了meta-class,元类中保存了创建类对象以及类方法所需要的所有消息
任何NSObject集成体系下的meta-class都是使用NSObjectmeta-class作为自己的所属类,而基类的meta-classisa指针指向它自己.

Method(objc_method):

typedef struct objc_method *Method;
struct objc_method {
	SEL method_name;
	char *method_types;
	IMP method_imp;
};

方法名, 方法类型, 方法实现

SEL(objc_selector):

typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL, 它是selectorOC中的表示类型.selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL:

@property SEL selector;

可以看到selectorSEL的一个实例.
其实selector就是映射到方法的c字符串,你可以用OC编译器命令@selector()或者Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器.

IMP:

typedef id (*IMP)(id, SEL, ...);

就是指向最终实现程序的内存地址的指针.
iosRuntime中,method通过selectorIMP两个属性,实现了快速查询方法及实现,相对提高了性能,有保持了灵活性.

类缓存(objc_cache):
OC运行时通过追踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象.然而,你可能只调用了他们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义.所以类实现一个缓存,每当你搜索一个类分派表,并且找到相应的选择器,它就把它放入它的缓存.所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存.
这是基于这个理论:如果你在类上调用了一个消息,你可能以后再次调用该消息,为了加速消息分发,系统会对方法和对应的地址进行缓存,就放下上述objc_cache.所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度.

Category(objc_category):
Category是表示一个指向分类的结构体的指针

struct category_t {
	const char *name;  //是指class_name,不是category_name
	classref_t cls;  //要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
	struct method_list_t *instanceMethods;  //category中所有给类添加的实例方法的列表
	struct method_list_t *classMethods;  //类方法的列表
	struct protocol_list_t *protocols;  //协议的列表
	struct protocol_list_t *instanceProperties;  //所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的.
};

从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量.

Runtime消息转发

进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索到继承树根部(通常为NSObject),如果找不到并且消息转发都失败了就会执行doesNotRecongnzieSelector:方法报unrecongnzied selector错.

  1. 动态方法解析
    首先,OC运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现,如果你添加了函数并返回YES,那么运行时系统会重新启动一次消息发送的过程.
    如果resolve方法返回NO,运行时就会转移到下一步:forwardingTargetForSelector.

  2. 备用接受者
    如果目标对象实现了-forwardingTargetSelector:,Runtime这是会调用这个方法,给你把这个消息转发给其他对象的机会.

  3. 完整消息转发
    如果上一步还不能处理未知消息,则唯一能做的就是启动完整的消息转发机制.首先它会发送-methodSignatureForSelector:消息获取函数的参数和返回值类型.如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecongnzieSelector:消息,程序会直接挂掉.如果返回了一个函数签名,Runtime就会创建一个NSInvication对象并发送-forwardInvocation:消息给目标对象.
    通过签名,Runtime生成一个对象anInvocation,发送给了forwardInvocation,我们可以在forwardInvocation方法里让新对象去执行方法,签名参数v@:.

Runtime应用

  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_removeAssociatedObject(id object)
    

    参数解释:
    id object: 被关联对象
    const void *key: 关联的key,要求唯一
    id value: 关联的对象
    objc_AssociationPolicy policy: 内存管理策略

    通过关联对象实现的属性的内存管理也是由ARC管理的,所以我们只需要给定适当的内存管理策略就行,不需要操心对象的释放

    实现:
    objc_setAssociatedObject调用栈:

    void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
    void objc_setAssociatedObject_non_gc(id object, void *key, id value, objc_AssociationPolicy policy)
    void _object_set_associative_refefence(id object, void *key, id value, uintptr_t policy)
    

    调用栈中的_object_set_associative_reference方法实际完成了设置关联对象的任务
    AssociationsManager维护了spinlock_tAssociationsHashMap的单例,初始化时会调用lock.lock()方法,在析构时会调用lock.unlock(),而associations方法用于取得一个全局的AssociationsHashMap单例
    也就是说AssociationsManager通过持有一个自旋锁splnlock_t保证对AssociationsHashMap的操作是线程安全的,即每次只会有一个线程对AssociationsHashMap进行操作.

    如何存储ObjcAssociation
    ObjcAssociation就是真正的关联对象的类,上面的所有数据结构只是更好的存储它.
    首先,AssociationsHashMap用于存储从对象的disguised_ptr_tobjectAssociationsHashMap映射
    ObjectAssociationsHashMap则保存了从key到关联对象ObjcAssociation的映射
    最关键的ObjcAssociation包含了policy以及value
    例子:
    关联对象ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello")在内存中是这样存储的:
    AssociationsManager
    static spinlock_t _lock
    static AssociationsHashMap *_map
    |
    AssociationsHashMap
    DISGUISE(obj) ObjectAssociationMap
    |
    ObjectAssociationMap
    @selector(hello) ObjcAssociation
    |
    ObjcAssociation
    OBJ_ASSOCIATION_RETAIN_NONATOMIC
    @“Hello”

    接下来我们可以重新回到对objc_setAssociatedObject方法的分析了.
    在这里会有两种情况:
    new_value != nil 设置/更新关联对象的值
    new_value == nil 删除一个关联对象

    new_value != nil是这样执行的:

    1. 使用old_association(0, nil)创建一个临时的ObjcAssociation对象(用于持有原有的关联对象,方便在方法调用的最后释放值)
    2. 调用acquireValuenew_value进行retain或者copy
    3. 初始化一个AssociationsManager,并获取唯一保存关联对象的哈希表AssociationsHashMap
    4. 先使用DISGUISE(object)作为key寻找对应的ObjectAssociationMap
    5. 如果没有找到,初始化一个ObjectAssociationMap,在实例化ObjectAssociation对象添加到Map中,调用setHasAssociatedObjects方法,表明当前对象含有关联对象
    6. 如果找到对应的ObjectAssociationMap,就会看key是否存在,由此决定是更新原关联对象,还是增加一个
    7. 最后的最后,如果原关联对象有值的话,会调用ReleaseValue()释放关联对象的值

    new_value == nil就说明我们要删除对应key的关联对象
    这种情况下方法的实现和前面的唯一区别就是,我们会调用erase方法,擦除ObjectAssociationMapkey对应的节点.

    objc_getAssociatedObject调用栈:

    id objc_getAssociatedObject(id object, const void *key)
    id objc_getAssociatedObject_non_gc(id object, const void *key)
    id _object_get_associative_reference(id object, void *key)
    
    1. 获取静态变量AssociationsHashMap
    2. DISGUISE(object)key查找ObjectAssociationMap
    3. void *keykey查找ObjcAssociation
    4. 根据policy调用相应的方法
    5. 返回关联对象ObjcAssociation的值
  2. 方法魔法(Method Swizzling)方法添加和替换和KVO实现
    方法添加

    class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
    

    cls 被添加的方法的类
    name 添加的方法的名称的SEL
    imp 方法的实现,该函数必须至少要有两个参数, self, _cmd
    v@: 类型编码

    方法替换
    swizzling应该只在+load中完成.在OC的运行时中,每个类有两个方法都会被自动调用.+load是在一个类被初始装载时调用,+initialize是在应用第一次调用该类的类方法或者实例方法前调用.两个方法都是可选的,并且只有在方法被实现的情况下才会被调用.
    swizzling应该只在dispatch_once中完成,由于swizzling改变了全局的状态,所以我们需要确保每一个预防措施在运行时都是可用的.原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次,GCDdispatch_once满足了所需要的需求,并且应该被当做使用swizzling中初始化单例方法的标准.
    KVO的实现.

  3. 消息转发(热更新)解决Bug(JSPatch)
    JSPatchios动态更新框架,只需要在项目中引入极小的引擎,就可以使用JavaScript调用任何OC原生接口,获得脚本语言的又是:为项目动态添加模块,或者替换项目原生代码动态修复bug.

  4. 实现NSCoding的自动归档和自动解档
    原理描述: 用Runtime提供的函数遍历model自身所有属性,并对属性进行encodedecode操作
    核心方法: 在Model的基类中重写方法

  5. 实现字典和模型的自动转换(MJExtension)
    原理描述: 用Runtime提供的函数遍历model自身所有属性,如果属性在json中有对应的值,则将其赋值
    核心方法: 在NSObject的分类中添加方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值