iOS 运行时的那些事儿

第一部分:基础篇

什么是运行时?

编程语言有静态和动态之分。

静态语言:如 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 指针
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, ...)

消息发送会经过以下几个步骤:

  1. 检测 SELtarget 是否为空,有一个为空时,会被忽略
  2. 查找类的 IMP 实现,检测 cache,找到则执行对应函数
  3. cache 找不到,查找 methodLists ,找到则执行对应函数,并添加至 cache
  4. methodLists 找不到,就沿着父类链向上查找,直到 NSObject 类。
  5. 仍然找不到,则会进入 动态方法解析 -> 接收者重定向 -> 消息重定向

动态方法解析:进入 -resolveClassMethod: ,如果可以正确解析到函数,返回 YES,否则返回 NO,进入下一个阶段。
接收者重定向:进入 -forwardingTargetForSelector:,如果查找到能处理该消息的接收者,返回接收者,否则返回 nil,进入下一个阶段。
消息重定向:进入 -forwardInvocation: ,结果就两种,消息成功处理或者抛出异常。

具体看一下。

动态方法解析

当类和其父类都无法找到接收者和响应函数,那么运行时就会进入动态添加方法,我们可以在此方法中,对消息作出反应。

方法有两种,一种是针对实例消息,一种是针对类消息。

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

示例:定义一个类,分别调用它的两个只有方法声明但是没有实现的方法。

这里分别是 sayHellorun。不出意外的话,肯定会出错,接下来我们演示针对这两个方法动态的添加上实现。

// 实例的动态方法解析
+(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 中的分类通常只是为了增加方法,如果你添加了属性,那么只能生成 settergetter 方法,无法生成成员变量,变相的无法完成属性的添加。但是有了运行时 ,你可以通过其关联对象特性来完成 settergetter 方法,相关的值则被关联到某一个对象上。

核心代码

/**
 关联属性
 @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 更高的版本中该对象能够拥有更强大的能力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容