Runtime笔记
Runtime
是iOS开发中最绕不开的一个话题,它可以说就是iOS的根本,之前关于Runtime
看过很多次,过一段时间就会忘记一些细节,这次做个笔记算是加深印象。
Objective-C Runtime
是一个将C语言转化为面向对象语言的扩展,C++和Objective-C
都是在C语言的基础上增加了面向对象的概念,但是二者实现的方式截然不同。C++
是静态语言,而Objective-C
则是一门动态语言。
C++
编写的程序是会在编译期将函数地址加入到可执行文件中的,而Objective-C
在编译的时候是不会把地址符号加入到可执行文件中去的,之前写过一篇关于符号绑定到地址上的文章,Objective-C
的编译器会生成可执行文件,也就是我们的函数和变量,但是他们与地址的绑定是链接器的工作。函数标识与函数内容之间的关联可以利用Runtime
动态修改。Runtime
是Objective-C
不可缺少的重要部分。
Objective-C Runtime实现的基础
要想彻底的了解Runtime
,那么我们就必须了解iOS中的类和对象
,又是老生常谈的问题,之前也有写过相关笔记《iOS中的class,SEL,IMP》,这里就再复习一遍,我们都知道在iOS中是由类生成对象,那么我们就来看看iOS中的“类
”与“对象
”。
打开objc.h
文件,我们可以看到如下代码:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
Class
是指向objc_class
的指针,id
是指向objc_object
的指针,这就是为什么Objective-C
中大部分类型都可以使用id
来修饰,objc_object
内有一个叫做isa
的结构体指针,它则指向了我们的objc_class
,可以这么理解isa=英文的is a XXX
,它告诉我们这个对象属于哪个类。
我们再来看看objc_class
结构体,进入runtime.h
文件,objc_class
的定义如下:
struct objc_class {
//metaclass
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
//父类
Class _Nullable super_class OBJC2_UNAVAILABLE;
//类名
const char * _Nonnull name OBJC2_UNAVAILABLE;
//版本号
long version OBJC2_UNAVAILABLE;
// 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long info OBJC2_UNAVAILABLE;
// 该类的实例变量大小(包括从父类继承下来的实例变量)
long instance_size OBJC2_UNAVAILABLE;
// 该类的成员变量地址列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 缓存最近使用的方法地址,用于提升效率;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 存储该类声明遵守的协议的列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
相比较objc_object
,objc_class
中多了很多特征成员,其实类也是对象,分别称作类对象(class object)和实例对象(instance object)。
在objc_object
中的isa
指向类对象,我们的实例对象通过isa
指针去访问类对象
中的成员变量
和实例方法
(“-”方法),而objc_class
中的isa
则指向metaclass
,通过它去访问类方法
(“+”方法)。
在objc_class
结构体上方还有四个重要的定义:
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
/// An opaque type that represents a category.
typedef struct objc_category *Category;
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
ivar
类对象中所成员变量ivar
,以下是ivar
的结构体:
struct objc_ivar {
// 变量名
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
// 变量类型
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
// 基地址偏移字节
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
// 占用空间
int space OBJC2_UNAVAILABLE;
#endif
}
在objc_class
结构体中的objc_ivar_list
中就是存储我们类对象中的成员变量。
Method
objc_class
这个的objc_method_lis
t里面所包含的内容是我们类里面存在的方法,也就是method
,如果本类是一个普通的类对象则存储实例方法,如果是metaclass则存储类方法。
那我们再来看看方法(method)
的结构体定义:
struct objc_method {
//方法名称
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
//方法类型
char * _Nullable method_types OBJC2_UNAVAILABLE;
//方法实现
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
这里就又引出两个重要的概念SEL
和IMP
SEL
是selector
在Objective-C
中的表示类型,selector
可以理解为区别方法的ID。
typedef struct objc_selector *SEL;
selector
的结构体定义:
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;// 名称
char *types; OBJC2_UNAVAILABLE;// 类型
};
IMP
在objc.h
中的定义如下:
typedef id (*IMP)(id, SEL, ...);
它是由编译器生成的一个函数指针。当你发起一个消息后(下文介绍),这个函数指针决定了最终执行哪段代码。
在了解了SEL和IMP
之后,我们就对方法的调用原理有了更深层次的认识,它通过SEL
找到相应的函数指针IMP
,然后实现函数的实现。
objc_property_t
这个则是我们所定义的属性,与之关联的还有一个objc_property_attribute_t
,它是属性的attribute
,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等,以下是属性的定义:
typedef struct {
const char *name; // 名称
const char *value; // 值(通常是空的)
} objc_property_attribute_t;
Category
这个就是我们所熟悉的类别,或者叫分类,刚刚总结过iOS中的分类。
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
iOS中的消息传递
在iOS
中的方法调用我们常常会说是发送消息,也就是iOS
中objc_msgSend
方法,比如我们创建了一个对象实例objc
,让它执行一个实例方法testMethod
,转换成我们的OC代码就是下面这样的:
[objc testMethod];
Runtime
会将其转成类似这样的代码:
objc_msgSend(objc,testMethod)
来看一下objc_msgSend
是如何进行工作的:
第一步:检查这个
selector
是否要忽略的。
第二步:检查这个对象是否为
nil
,nil
对象发送任何一个消息都会被忽略掉。
第三步:
(1)调用实例方法时,它会首先在自身isa
指针指向的类(class)methodLists
中查找该方法,如果找不到则会通过class
的super_class
指针找到父类的类对象结构体,然后从methodLists
中查找该方法,如果仍然找不到,则继续通过super_class
向上一级父类结构体中查找,直至根class
。
(2)当我们调用某个某个类方法时,它会首先通过自己的isa
指针找到metaclass
,并从其中methodLists
中查找该类方法,如果找不到则会通过metaclass
的super_class
指针找到父类的metaclass
对象结构体,然后从methodLists
中查找该方法,如果仍然找不到,则继续通过super_class
向上一级父类结构体中查找,直至根metaclass
。
第四步:以上方法都没有找到,则进入消息转发阶段(后面详细说明)。
iOS是一门动态语言
一直说iOS
是一门动态语言,那么它动态在哪里呢?在编译期过后我们又可以利用Runtime
做些什么呢?
我在看runtime.h
的源码中还发现了几个熟悉的函数方法
首先是第一个关于方法交换:
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
之前我们说过,方法的调用是通过SEL找到对应的IMP来进行函数的实现,那么runtime
可以做到在运行期将SEL
和IMP
的对应进行更改,从而进行方法的替换。
Method originalMethod = class_getInstanceMethod(aClass, originalSel);
Method swizzleMethod = class_getInstanceMethod(aClass,swizzleSel);
method_exchangeImplementations(originalMethod, swizzleMethod);
使用method_exchangeImplementations
便可以将我们的SEL原本所对应的函数指针进行替换。
其次就是我们的关联对象:
/**
* Sets an associated value for a given object using a given key and association policy.
*
* @param object The source object for the association.
* @param key The key for the association.
* @param value The value to associate with the key key for object. Pass nil to clear an existing association.
* @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
/**
* Returns the value associated with a given object for a given key.
*
* @param object The source object for the association.
* @param key The key for the association.
*
* @return The value associated with the key \e key for \e object.
*
* @see objc_setAssociatedObject
*/
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
使用关联对象我们可以动态的为一个类添加属性,用法如下:
-(void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
return objc_getAssociatedObject(self, @"name");
}
关联对象搭配category
使用就可以为一个类动态的增加属性,在SDWebImage
中就多次使用关联对象为类增加一个属性,用于管理请求的URL
。
消息转发
消息转发流程图当我们的对象想调用方法,始终无法找到就会进入消息转发过程,这是我们最后的补救的机会,看一下消息转发的流程:
第一步:通过
resolveInstanceMethod:
方法决定是否动态添加方法。如果返回Yes
则通过class_addMethod
动态添加方法,消息得到处理,结束;如果返回No
,则进入下一步;
第二步:这步会进入
forwardingTargetForSelector:
方法,用于指定备选对象响应这个selector
,不能指定为self
。如果返回某个对象则会调用对象的方法,结束。如果返回nil
,则进入第三步;
第三步:这步我们要通过
methodSignatureForSelector:
方法签名,如果返回nil
,则消息无法处理。如果返回methodSignature
,则进入下一步;
第四步:这步调用
forwardInvocation:
方法,我们可以通过anInvocation
对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector
方法,若我们没有实现这个方法,那么就会crash
。
用源码给大家解释一下,首先我定义一个类叫做BadStudent
,我让他去学习,可是他没有任何方法。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface BadStudent : NSObject
@end
NS_ASSUME_NONNULL_END
我让他去执行学习这个动作,结果不出意外找不到方法,而且程序闪退
BadStudent *bad = [[BadStudent alloc] init];
[bad performSelector:@selector(doStudy)];
//控制台输出
-[BadStudent doStudy]: unrecognized selector sent to instance 0x6000034ec290
这时我强制让他去学习,在消息转发阶段动态为其增加一个方法,在BadStudent.m
中增加如下代码:
@implementation BadStudent
+(BOOL)resolveInstanceMethod:(SEL)sel{
if ([NSStringFromSelector(sel) isEqualToString:@"doStudy"]) {
class_addMethod(self, sel, (IMP)otherStudy, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void otherStudy(id self, SEL cmd)
{
NSLog(@"我在学习!");
}
@end
这时就可以正常执行方法了
2020-01-15 16:35:34.308702+0800 MethodForwarding[53375:8110706] 我在学习!
接下来为大家演示一下完整的消息转发流程,我不强迫他学习,但是他找到了一个好学生替他完成学习,我又创立了一个GoodStudent
类,其中含有关于学习的方法:
@interface GoodStudent : NSObject
- (void)doStudy;
@end
@implementation GoodStudent
- (void)doStudy{
NSLog(@"帮BadStudent学习");
}
@end
我们再来看BadStudent
中做了什么来完成这一操作的:
@implementation BadStudent
//不添加方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
return NO;
}
//暂时没找到新的对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
return nil;
}
//方法签名找doStudy这个方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) isEqualToString:@"doStudy"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
//找到有人能完成学习这个动作,找到那个对象让他去学习
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
GoodStudent *good = [GoodStudent new];
if ([good respondsToSelector:sel]) {
[anInvocation invokeWithTarget:good];
}
else{
[self doesNotRecognizeSelector:sel];
}
}
@end
//控制台输出
2020-01-15 16:40:32.662954+0800 MethodForwarding[53451:8145439] 帮BadStudent学习
这就是完整的消息转发流程,热更新控件JSPatch
就是利用了消息转发来实现动态修复线上代码。