iOS 你所了解的runtime

起来搬砖啦

我们说Objective-C这门语言是一门动态语言,哪个特性来体现的呢,就是runtime运行时系统来体现的。

下面通过几个方面来介绍runtime运行时:

  • runtime介绍
  • 消息发送
  • 消息转发
  • runtime使用场景

runtime介绍

先看看官方文档的介绍:

Objective-C语言从编译时间和链接时间到运行时推迟了尽可能多的决策。只要有可能,它就会动态地完成任务。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译代码。运行时系统充当Objective-C语言的一种操作系统;这就是语言运作的原因。

Objective-C runtime运行时有两个版本,lagecy(遗产)和modern(现代),modern版本运行时随Objective-C2.0推出,Objective-C2.02.0包含了许多新功能,其中一点是当更改类的实例变量布局时,不必重新编译。

运行时系统的核心是消息传递和转发,以及在消息传递和转发中动态的加载类并查找有关对象的信息。

不管你是显式的调用了runtime的API还是直接写OC有关的代码,编译器都会生成相应的数据结构和函数。

runtime运行时系统是一个动态共享库,可以使用 #<objc/runtime>来查看内部的API及各种结构体定义,且runtime是开源的,你可以在这里下载。

消息发送

曾经有这么一个面试题:

- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"class = %@",NSStringFromClass([self class]));
        NSLog(@"class = %@",NSStringFromClass([super class]));
    }
    return self;
}
打印:
class = Person
class = Person

自己先想想为啥...
接着看:
举个例子:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self test];
}

- (void)test {
    NSLog(@"%@",NSStringFromSelector(_cmd));
}

@end

我们经常说在viewDidLoad方法里通过self调用test方法,[self test],其实确切的应该说成给 self 发送 test 消息 当编译完后该代码转换成了如下代码:

objc_msgSend(self,sel_registerName("test"));

需要设置下xcode,不让xcode检查是否使用objc_msgSend发送消息,要不然会报错;

项目target--build Setting --搜索objc_msgSend ---设置为NO即可

objc_msgSend定义如下:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);

在想弄清楚runtime是怎么完成消息发送前,我们先来看下,OC对象和类在runtime中的定义:

/// 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;
};

//类结构体
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//指向元类

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;//指向父类(类也是一个对象)
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;//name
    long version                                             OBJC2_UNAVAILABLE;//版本
    long info                                                OBJC2_UNAVAILABLE;//info
    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;

看完上边的实例及类的定义,加上我们的开发经验,其实已经知道消息发送的一个流程了:

  1. 通过实例self的isa指针找到当前所属类
  2. 在当前类的objc_cache中查找,有没有test方法
  3. cache中没有找到test方法,再去methodList中去查找,找到就执行IMP,没有找到通过isa指针去父类中去找,直到找到,找不到就报unrecognized selector xxx

方法查找首先去objc_cache中查找是为了提高效率,调用过的方法会存放在缓存中,不能每次调用一个方法都要查找一遍methodList,显然是不行的。缓存的时候会把test的method_name作为key,方法的method_imp实现作为value来存储。

其他的一些结构体声明:

  • id
  • SEL
  • Method
  • Ivar
  • Category
  • objc_property_t
  • objc_cache
id

id类型,就是一个objc_object结构体指针,指向任意对象

/// A pointer to an instance of a class.
typedef struct objc_object *id;
SEL

方法选择器,可以理解为方法ID,可以联想到OC语言一个类中方法不能重载

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
Method

一个方法有方法名,有types,有方法实现;types举例:@"v@:" v代表返回值为void,@ 表示参数为参数为对象类型,: 表示selector方法选择器 具体请移步这里

struct objc_method {
    SEL _Nonnull method_name  //方法名                               OBJC2_UNAVAILABLE;
    char * _Nullable method_types  //types(@"v@:")                          OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp   //方法实现IMP                               OBJC2_UNAVAILABLE;
}                                                
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
} 
Category类别
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;//协议列表
} 
objc_property_t 属性
/// An opaque type that represents an Objective-C declared property.类声明的属性
typedef struct objc_property *objc_property_t;

//获取一个类中的属性列表
objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount);
objc_cache

缓存,刚才讲过了,寻找类的方法先是去缓存中找的,内部有个 buckets实例变量,意思很明白,一个方法桶子

typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};

以上简单列举了下,runtime运行时系统其他的结构体声明,理解了这些在加上内部的函数,就能更灵活的运用到自己的项目当中,内部更具体的函数、结构体声明请移步官方文档。

消息转发

我们都知道,假如你调用了一个没有实现的方法,会报unrecognised selector xxx,其实即便你没有实现这个方法,通过runtime的消息转发机制,你仍然还有机会让你的程序不崩溃。

NSObject类中有这么几个方法,以下几个方法就是运行时系统在给你最后机会来自己处理消息转发:

1、
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

2、- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

3、
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

挂
- (void)doesNotRecognizeSelector:(SEL)aSelector;

可以概括为三种方式:

  • 动态的添加方法实现,返回YES
  • 返回实现该方法的对象
  • 通过返回NSInvocation和MethodSignature完成一次完整转发

动态添加方法实现

举例:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    objc_msgSend(self,sel_registerName("test"));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == NSSelectorFromString(@"test")) {
        BOOL added = class_addMethod([self class], NSSelectorFromString(@"test"), (IMP)testImp, "v@:");
        if (added) {
            return YES;
        }
        return NO;
    }
    //其他系统访问返回super调用
    return [super resolveInstanceMethod:sel];
}


void testImp(id self,SEL _cmd) {
    NSLog(@"method name = %@",NSStringFromSelector(_cmd));
}
@end

打印:
method name = test

来看下官方文档的说明:

Dynamically provides an implementation for a given selector for an instance method.动态提供一个给定实例的方法实现,说的很明白了。

一个OC方法默认有两个参数(self 和 SEL),使用class_addMethod函数来动态添加一个函数实现,并返回YES

返回实现该方法的对象

如果你实现了forwardingTargetForSelector这个方法,系统就会调用它,会去你返回的那个对象实例中寻找方法,然后调用。

现在我创建了一个Person类,里边声明了test方法并实现。

@interface Person : NSObject
- (void)test;
@end

@implementation Person
- (void)test {
    NSLog(@"class_name = %@ , method_name = %@",[self class],NSStringFromSelector(_cmd));
}
@end

VC内部:
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    objc_msgSend(self,sel_registerName("test"));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == NSSelectorFromString(@"test")) {
        return NO;
    }
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == NSSelectorFromString(@"test")) {
        return [Person new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

打印:
class_name = Person , method_name = test

系统首先调用resolveInstanceMethod方法,发现没有找到方法实现,就会调用方法forwardingTargetForSelector,该方法内部返回Person对象实例,Person对象内部实现了test方法,然后调用了test方法。

看下系统给的解释:

如果对象实现(或继承)此方法,并返回非零(和非自身)结果,则返回的对象将用作新的接收器对象,并且消息调度将恢复到该新对象,如果这个方法返回self,会直接崩溃。

也就是系统会重新进行一次消息转发,消息的接收者变成了该方法返回的对象。

通过返回NSInvocation和MethodSignature完成一次完整转发

如果以上两个机会你都没有把握,系统就会开启一次完整的消息转发,步骤如下:

  1. 通过实现methodSignatureForSelector方法,实例化一个方法签名对象NSMethodSignature返回,这个方法签名对象是对方法method的一个描述
  2. runtime动态生成一个NSInvocation对象传递给forwardInvocation方法
  3. 通过实现forwardInvocation来指定一个对象,通过这个对象完成消息转发

看下面代码:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    objc_msgSend(self,sel_registerName("test"));
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return sig;
    }
    return [super methodSignatureForSelector:aSelector];
}

//这个官方有示例
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    Person *p = [[Person alloc] init];
    if ([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end

打印:
class_name = Person , method_name = test

有关NSMethodSignature和NSInvocation类,内部很简单,可以点进去看下。

以上就是消息转发的一个流程。

runtime使用场景

结合自己项目中runtime使用场景来列举下:

  1. 对象绑定
  2. 方法交换
  3. 自定义对象归档返归档
  4. dic-model
  5. KVO自定义实现

1. 对象绑定

直接看例子:

@interface Person : NSObject
- (void)bind;
@end


NSString * const personSource = @"personSource";

@implementation Person
- (NSString *)description {
    NSString *str = objc_getAssociatedObject(self, &personSource);
    return [NSString stringWithFormat:@"%@-刚出生就拥有:%@",self.name,str];
}

- (void)bind {
    objc_setAssociatedObject(self, &personSource, @"北京二环两套房+500万", OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void)dealloc {
    objc_removeAssociatedObjects(self);
}

//VC中
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p = [[Person alloc] init];
    p.name = @"小明";
    [p bind];
    NSLog(@"%@",p);
}

打印:
小明-刚出生就拥有:北京二环两套房+500万
@end

通过上边这个例子可以看出:现在是拼爹的时代,哈哈。

看下绑定对象API:

/** 
 * 通过给定key和绑定策略将value绑定到object上!
 * 
 * @param object 被绑定的对象
 * @param key 绑定value使用的key,注意:key-value一一对应
 * @param value 绑定的value,传空表示清空value
 * @param policy 绑定策略,用于绑定对象的内存管理
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
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);
    
  /** 
 * 根据key返回对象绑定的value值
 * @param object 被绑定的对象
 * @param key key
 * @return 绑定到对象上的value值
 * @see objc_setAssociatedObject
 */
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
 

//清除所有绑定到对象上的value,一般不用这个方法,使用objc_setAssociatedObject方法,value传递nil即可
void objc_removeAssociatedObjects(id _Nonnull object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

绑定策略:

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+原子性 
};

缓存策略用于绑定对象value内存管理用的,跟常见的属性修饰符管理完全相同。

2. 方法交换

方法交换,核心就是交换方法的实现(IMP),有图为证


屏幕快照 2019-01-10 18.33.39.png

简单说一下核心方法实现:

  1. 根据旧类class和SEL,获取要被替换的Method,即originMethod
  2. 根据新类class和SEL获取新加的Method,即cusMethod
  3. 如果没有originMethod,直接向旧类添加新方法,添加成功,就替换老方法的实现(IMP)
  4. 直接交换方法实现
void tt_swizzleMethodImplementation(Class originC,Class cusC ,SEL originSEL, SEL cusSEL) {
    Method originMethod = class_getInstanceMethod(originC, originSEL);
    Method cusMethod = class_getInstanceMethod(cusC, cusSEL);
    if (!originMethod) {
        BOOL added = class_addMethod(originC, cusSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (added) {
            class_replaceMethod(originC, originSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        }
    } else {
        method_exchangeImplementations(originMethod, cusMethod);
    }
}

这个我前一阵写过一个例子,就是通过方法交换来优化appDelegate内部的推送部分代码的,优化后你会发现,appDelegate推送部分代码非常简洁,里边写的比较详细,有需要的可以看下推送优化传送门,方法交换就此别过。

3. 自定义对象归档返归档

当存储自定义数据(model)时,需要用到归档反归档。
系统对归档的内部实现猜测是以key-value键值对存储的。通过实现系统的两个协议来告知系统你要存储的数据。需要实现NSCoding协议内的两个方法

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; 

一般如果model属性少的话,手写还是可以的,但是属性过多还要手写? 通过runtime来动态遍历出model的属性key,然后来赋值或取值。MJExtension内部有实现,而且一个宏搞定。

看下核心代码的实现:

- (void)tt_decode:(NSCoder *)decoder {
    unsigned int count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        objc_property_t property = properties[i];
        const char *name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        NSString *propertyValue = [decoder decodeObjectForKey:propertyName];
        if (nil != propertyValue) {
            [self setValue:propertyValue forKey:propertyName];
        }
    }
    free(properties);
}

- (void)tt_encode:(NSCoder *)encoder {
    unsigned int count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        objc_property_t property = properties[i];
        const char *name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:propertyName];
        if (nil != value) {
            [encoder encodeObject:value forKey:propertyName];
        }
    }
    free(properties);
}

这块不多说,需要注意的是最后需要释放objc_property_t指针,因为这是在OC环境,对C语言不自动管理内存。

dic-model(字典转模型)

字典转模型也不多说,看看常用的开源框架就行,核心是遍历出对象的属性列表然后把以属性名(可以自定义)为key从字典取出对应的value赋值给对象属性。

KVO自定义实现

当你了解了KVO的底层原理后,也可以尝试着使用runtime运行时来写一个自己的KVO,当然有可能会有bug,即便自己实现一个KVO也没必要用到项目中,毕竟系统的或RAC很成熟了,自己写的目的其实很简单,通过自己实现可以更熟悉底层的实现,从而给自己带来一些思考。前一阵自己实现过一个,也有demo,有需要的自己去看吧,这里不多说。

面试题

回过头来看看那个面试题,我们知道[self class] 等价于

objc_msgSend(self, @selector(class));

那么[supser class]等价于

    objc_msgSend(self, @selector(class));
    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClass, _cmd);

通过上面转化后的代码我们就清楚了,消息的接收者都是self,super只是一个标识符而已。

以上就是runtime运行时的一个概括汇总吧,只有当你真正理解了runtime,才会领悟这门语言动态的含义。

奋然前行!

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

推荐阅读更多精彩内容

  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,132评论 0 9
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 751评论 0 1
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 729评论 0 2
  • 什么时候不怎么有交集的他也成了梦里的故事?想来真是奇妙
    pumpkin安阅读 114评论 0 0