第一部分:基础篇
什么是运行时?
编程语言有静态和动态之分。
静态语言:如 Java、C,在编译阶段就确定了成员变量、函数的内存地址。
动态语言:如OC,在运行期间才确定成员变量和函数地址,即使没有实现部分同样能通过编译。
动态语言具有比较高的灵活性,但是正因为如此,动态语言即使在编译通过之后,依然会发生错误,程序有着当对的不确定性。Objective-C 就是一种动态语言,它为我们提供了 runtime
机制,是一套纯c语言的 api,OC 中代码最终都会便编译器转换成运行时的代码,通过消息机制决定使用那个函数。
消息机制
OC 代码最终都会转换成运行时的代码。OC 的方法调用一般为 [obj method]
,或者是其他衍生的调用方式,每次方法调用都是一个通过运行时发送消息的过程。[obj method]
在编译时期会被编译为 objc-msgSend(obj, method, org1, org2, ...)
。在运行时期会变得相对复杂,如果,obj
能都找到对应的 method
,则直接执行,如果找不到对应的 method
,消息被转发,或者指定其他接收者完成处理,否则会抛出异常发生崩溃。
OC 中最初的根类 NSObject 中的很多的方法就体现出了动态性,例如 -isKindOfClass:
检测对象是否属于某一类型、-respondsToSelector:
检测能否响应指定方法、-conformsToProtocol:
检测是否遵循指定协议等等,这些类似方法最终都通过 Runtime
进行转换实现。例如拿对象的初始化来讲,使用运行时同样能够实现类似的效果。
Class class = [UIView class];
// 等同于
Class viewClass = objc_getClass("UIView");
UIView *view = [UIView alloc];
// 等同于
UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc"));
Runtime 数据结构
接下来主要来看下 Class 在 Runtime 的结构,文件地址 objc/runtime.h
。
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
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;
参数解析:
- isa 指针
objc_object 结构中同样也有一个 isa 指针,指向该对象的定义,而类本身也可以看成一个对象,称之为类对象,也就是元类 meta,元类的 isa 指针指向最终的 Root class 类,Root class 的 isa 指向自己。
举个例子来说,Person 的实例就是图中的 Instance of Subclass
, 它的 isa 指针指向 Person 定义,而 Person 中的 isa 指针指向 Person.Class 元类。
- super_class
super_class 指向 objc_class 类所继承的类,如上图所示,如果当前类已经是最顶层的类,则 super_class 值为 nil。
- ivars
ivars 是类所存放成员变量的,它也是一个结构体。
- methodLists
和 ivars 类似,methodLists 则是存放方法的地方,例如成员变量的存取方法。
- cache
存放了 method 响应的记录,下次消息过来是,优先会在 cache 中查找,效率会提高。
- protocols
存放类所有的协议信息。
其他更多的运行时类型,如 objc_method 表示 方法,objc_ivar 表示 成员变量,等等。
除了这下定义,该文件中还有非常多的操作 objc_class 的函数,可以自行查看,后面的示例中也会介绍到相关函数的使用。
进一步认识消息机制
前面介绍到运行时的消息发送机制相对复杂,可能顺利执行也可能抛出异常,这一节中我们来详细了解一下完整的消息发送与转发机制。
OC 对象调用方法会被编译为下面的形式。
返回值 objc_msgSend(接收者, 方法, 参数1, 参数2, ...)
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
消息发送会经过以下几个步骤:
- 检测
SEL
和target
是否为空,有一个为空时,会被忽略 - 查找类的
IMP
实现,检测cache
,找到则执行对应函数 -
cache
找不到,查找methodLists
,找到则执行对应函数,并添加至cache
-
methodLists
找不到,就沿着父类链向上查找,直到 NSObject 类。 - 仍然找不到,则会进入 动态方法解析 -> 接收者重定向 -> 消息重定向。
动态方法解析:进入 -resolveClassMethod:
,如果可以正确解析到函数,返回 YES
,否则返回 NO
,进入下一个阶段。
接收者重定向:进入 -forwardingTargetForSelector:
,如果查找到能处理该消息的接收者,返回接收者,否则返回 nil
,进入下一个阶段。
消息重定向:进入 -forwardInvocation:
,结果就两种,消息成功处理或者抛出异常。
具体看一下。
动态方法解析
当类和其父类都无法找到接收者和响应函数,那么运行时就会进入动态添加方法,我们可以在此方法中,对消息作出反应。
方法有两种,一种是针对实例消息,一种是针对类消息。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
示例:定义一个类,分别调用它的两个只有方法声明但是没有实现的方法。
这里分别是 sayHello
和 run
。不出意外的话,肯定会出错,接下来我们演示针对这两个方法动态的添加上实现。
// 实例的动态方法解析
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(sayHello)) {
// 将实现部分指向 默认的处理
class_addMethod(self.class, sel, class_getMethodImplementation(self.class, @selector(defaultSayHello)), "v@:");
return YES;
}
// 继续查找父类是否能够处理
return [super resolveInstanceMethod:sel];
}
// 默认的实例处理方法
-(void)defaultSayHello{
NSLog(@"%s",__func__);
}
+(BOOL)resolveClassMethod:(SEL)sel{
if(sel == @selector(run)){
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(defaultRun)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+(void)defaultRun{
NSLog(@"%s",__func__);
}
其中
// 当前类、传递的消息、实现IMP、IMP对应的返回值和参数类型
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
更多的 types
类型,可以查询 官网资料。
接收者重定向
在动态方法解析无法处理该消息时,该消息就会进入到转发阶段。该阶段可以指定其他接收者,以保障程序的执行。
-(id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(sayHello)) {
//返回能够处理这个sel的实例对象
return [NSClassFromString(@"Student") new];
}
return [super forwardingTargetForSelector:aSelector];
}
如果是类方法,请使用 +
的类方法,以及返回 Class
类,而不是实例。
消息重定向
如果在经过动态方法解析、接收者重定向都无法处理此条消息时,那么就会进入最后的阶段,针对这条消息进行重定向。运行时会通过方法 -forwardInvocation:
来通知该对象,给予最后一次处理这条消息的机会。
Invocation
是一个消息对象,包含了调用者、方法选择器等信息。要想实现消息重定向,我们还需要重写 -methodSignatureForSelector:
为方法 -forwardInvocation:
的参数 anInvocation
提供一个 methodSignature
方法签名,这个 methodSignature
用来描述方法的返回值,参数类型,关于方法的描述可以看 官网资料。
-(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
// 判断某个对象是否可以响应消息,如果可以,该对象响应
if ([[NSClassFromString(@"Student") new] respondsToSelector:sel]) {
[anInvocation invokeWithTarget:[NSClassFromString(@"Student") new]];
}
/*检测其他对象*/
else {
// 如果依然没有可以响应消息,则爆找不到响应方法
[self doesNotRecognizeSelector:sel];
}
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature* methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return methodSignature;
}
以上就是完成的消息机制。三个阶段,范围一步一步的扩大:
动态方法解析范围依旧是在本类,你可以将采用动态添加的方式将消息转交给其他方法。
接收者重定向范围不限制在本类,你可以选择一个其他可以处理该消息的类。
消息重定向范围也不限制在本类,和接收者重定向比较,你会发现消息重定向允许你将消息传递给多个对象,搜索范围进一步扩大,你甚至可以在这里实现类似多重继承的操作,即当前类无法响应消息时,寻找其他多个响应者。
第二部分:应用篇
在简单认识了以下运行时关于消息传递知识后,我们来简单介绍以下运行时具体给可以帮我们做点什么。
经验总结。
- 方法交换:拦截方法,加入其他任务。如:AOP方式进行日志统计
- 属性关联:将某内存地址关联到属性。如:实现分类中的属性添加
- 解析未知对象:获取未知对象的成员列表、方法列表、系诶咦列表等信息
- 消息机制:动态添加方法,解决消息无法响应的问题
- 动态操作:动态的创建类、添加方法、添加属性,从无到有
方法交换
Method Swizzling
是运行时中的黑魔法,实现的原理是,通过运行时获取到类中的方法的实现 IMP
地址,将其指向另外一个 IMP
,进而能够动态的交换两方法,而你则可以无感知的进行额外操作。
下面是列举了两个应用示例。
- 日志统计
主要目的是为了监听控制的进出情况,以统计页面功能相关的指标,如收欢迎程度。
核心代码
//获取类方法
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//获取实例对象方法
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交换两个方法的实现
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
下面是在控制器的分类中进行的方法交换。
// 该方法会在编译时期被调用
+(void)load {
[self exchangeMethod:@selector(viewDidAppear:)];
[self exchangeMethod:@selector(viewDidDisappear:)];
}
// 交换两个方法
+ (void)exchangeMethod:(SEL)originalSelector{
SEL swizzledSelector = NSSelectorFromString([@"ll_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)ll_viewDidAppear:(BOOL)animated{
// 做一些统计操作
[self ll_viewDidAppear:animated]; //这里的方法指向的是系统的`-viewDidAppear:`
}
- (void)ll_viewDidDisappear:(BOOL)animated{
// 做一些统计操作
[self ll_viewDidDisappear:animated];
}
- 越界操作
这是针对一些危险操作的,如数组越界、字符截取等等。下面演示了关于不可变数组的操作拦截,对越界的行为进行了断点提示。
#define NSAssertTip(tip) NSAssert(NO, tip)
#import <objc/runtime.h>
@implementation NSObject (Swizzle)
// 通用的方法交换
+ (void)swizzleMethod:(SEL)srcSel tarClass:(NSString *)tarClassName tarSel:(SEL)tarSel{
if (!srcSel||!tarClassName||!tarSel) {
return;
}
Class srcClass = [self class];
Class tarClass = NSClassFromString(tarClassName);
Method srcMethod = class_getInstanceMethod(srcClass,srcSel);
Method tarMethod = class_getInstanceMethod(tarClass,tarSel);
method_exchangeImplementations(srcMethod, tarMethod);
}
@end
#pragma mark ----------------------- 不可变数组 -----------------------
@implementation NSArray (Safe)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleMethod:@selector(initWithObjects_safe:count:) tarClass:@"__NSPlaceholderArray" tarSel:@selector(initWithObjects:count:)];
[self swizzleMethod:@selector(objectAtIndex_safe:) tarClass:@"__NSArrayI" tarSel:@selector(objectAtIndex:)];
[self swizzleMethod:@selector(arrayByAddingObject_safe:) tarClass:@"__NSArrayI" tarSel:@selector(arrayByAddingObject:)];
});
}
- (instancetype)initWithObjects_safe:(id *)objects count:(NSUInteger)cnt{
NSUInteger newCnt=0;
for (NSUInteger i=0; i<cnt; i++) {
if (!objects[i]) {
NSAssertTip(@"数组初始化错误");
break;
}
newCnt++;
}
self = [self initWithObjects_safe:objects count:newCnt];
return self;
}
- (id)objectAtIndex_safe:(NSUInteger)index{
if (index>=self.count) {
NSAssertTip(@"数组越界");
return nil;
}
return [self objectAtIndex_safe:index];
}
- (NSArray *)arrayByAddingObject_safe:(id)anObject {
if (!anObject) {
NSAssertTip(@"新增的对象错误");
return self;
}
return [self arrayByAddingObject_safe:anObject];
}
@end
属性关联
OC 中的分类通常只是为了增加方法,如果你添加了属性,那么只能生成 setter
和 getter
方法,无法生成成员变量,变相的无法完成属性的添加。但是有了运行时 ,你可以通过其关联对象特性来完成 setter
和 getter
方法,相关的值则被关联到某一个对象上。
核心代码
/**
关联属性
@param object 需要设置关联属性的对象,即给哪个对象关联属性
@param key 关联属性对应的key,可通过key获取这个属性,
@param value 给关联属性设置的值
@param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
OBJC_ASSOCIATION_ASSIGN @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN @property(strong,atomic)。
OBJC_ASSOCIATION_COPY @property(copy, atomic)。
*/
void objc_setAssociatedObject(id _Nonnull object,
const void * _Nonnull key,
id _Nullable value,
objc_AssociationPolicy policy)
/**
获取关联的属性
@param object 从哪个对象中获取关联属性
@param key 关联属性对应的key
@return 返回关联属性的值
*/
id _Nullable objc_getAssociatedObject(id _Nonnull object,
const void * _Nonnull key)
/**
移除对象所关联的属性
@param object 移除某个对象的所有关联属性
*/
void objc_removeAssociatedObjects(id _Nonnull object)
下面列举了为 UIButton 对象添加了数据绑定。
@interface UIButton (PassValue)
@property (strong ,nonatomic) NSDictionary *paramDic;
@end
// 实现setter、getter方法
-(NSDictionary *)paramDic{
// _cmd 表示当前的方法,该参数也可以是唯一的全局对象,如字符串
return objc_getAssociatedObject(self, _cmd);
}
-(void)setParamDic:(NSDictionary *)paramDic{
objc_setAssociatedObject(self, @selector(paramDic), paramDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
解析未知对象
在 Runtime 数据结构中,我们看到了 objc_class
中存放着类的成员变量、方法列表等信息,运行时通用提供了方法,让能够进一步了解一个类。有了运行时,所有的类在你的面前都是透明的。
[self.textField setValue:UIColor.redColor forKey:@"_placeholderLabel.textColor"];
上面的代码是使用了私有成员变量来设置输入框的占位标签的颜色。那么如何知道 _placeholderLabel
这个私有属性的呢?是通过运行时。
unsigned int count;
Ivar *ivarList = class_copyIvarList([UITextField class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
上述代码输出了 UITextField
所有的成员变量,其中一个就是占位符标签 _placeholderLabel
。
你同样可以通过运行时获取到属性列表、方法列表等信息,对应的函数类似。
注:iOS 11以后,苹果限制非常多的通过私有api设值的方式,其中就有 _placeholderLabel
, 这个对象已经是 UITextFieldLabel
。因此,在开发过程中最好避免使用私有 api,一方面是很可能被拒绝,另一方面就是未来的某一时刻,这些私有api都不会做兼容的替换。
消息机制
这一节请参考上一部分的内容。
动态的加入类、方法、属性
更多的应用
- 数据和模型转换
参考这篇内容iOS 数据模型转换。
- 自动归档和解档
优化归档和解档同样是应用了通过运行时获取类成员变量,然后实现归档和解档操作。
// 对象解档
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if ([super init]) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:ivarName];
// 从解码器中获取到对象
id value = [aDecoder decodeObjectForKey:key];
// 通过KVC的方式设值
[self setValue:value forKey:key];
}
free(ivarList);
}
return self;
}
// 对象归档
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivarList);
}
总结
OC 运行时的功能十分的强大,上面仅仅介绍了冰上一角,熟练的掌握运行时,对我们开发有着非常大的帮助,也能帮助我们进一步了解 OC 底层原理。
和 OC 动态性相比,Swift 语言在对类型的定义上就非常的严厉了,你必须在你编译期明确类型,官方给出的解释是 Swift 基于安全稳定性考虑,让错误发生在编译期而非运行期,那么 app 整体会相对健壮。另一方面 Swift 砍掉了 OC 中的运行时,取而代之的是 Mirror
对象,但是这个对象目前而言功能仅限于查看对象的属性、方法等,和运行时相比简直就是云泥之别,期待在 Swift 更高的版本中该对象能够拥有更强大的能力。