RunTime

什么是RunTime

RunTime是一套比较底层的纯C语言API,属于一个C语言库,包含了很多底层C语言API.我们平时编写的OC代码,程序运行过程中,其实最终都是转成RunTime的C语言代码,RunTime算是OC的幕后工作者.
比如:
OC中的代码

[[Student alloc] init];

其底层实现实际是消息发送机制,使用runtime表示为:

objc_msgSend(objc_msgSend(“Student” , “alloc”), “init”)

RunTime在什么时候使用

使用RunTime实现OC无法实现的,不好实现的一些非常底层的操作:

  • 在程序运行过程中,动态的创建一个类(比如KVO的底层实现)
  • 在程序运行过程中,动态的为某个类添加属性或者方法,修改属性的值或者方法
  • 遍历一个类的所有成员变量(属性)或者方法(比如实现归档和反归档操作,有一个类就对其进行一次这样的操作比较麻烦,我们就可以给NSObject对象添加一个类目,这样就可以很方便的实现一键归档和反归档)

接下来我们用具体的代码来看一下怎样在项目中使用RunTime

1 使用RunTime实现归档和反归档

首先给NSObject写一个类目,遵守NSCoding协议,然后实现归档和反归档的两个方法

   #import "NSObject+Coding.h"
  // 需要导入RunTime的头文件
  #import <objc/objc-runtime.h>
  @implementation NSObject (Coding)
  // 遍历类中所有的实例变量,逐个进行归档,反归档
  -(void)encodeWithCoder:(NSCoder *)aCoder
  { /*class_copyIvarList函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。*/
      // 遍历实例变量
      unsigned int ivarCount = 0;
      Ivar *vars = class_copyIvarList(object_getClass(self), &ivarCount);
      for (int i = 0; i < ivarCount; i++) {
          // 获取实例变量名字
          Ivar var = vars[i];
          NSString *varName = [NSString stringWithUTF8String:ivar_getName(var)];
          // KVC
          id value = [self valueForKey:varName];
          // 进行归档
          [aCoder encodeObject:value forKey:varName];
      }
      // 释放指针
      free(vars);   
  }
  -(instancetype)initWithCoder:(NSCoder *)aDecoder
  {
      self = [self init];
      if (self) {
          // 遍历实例变量链表,逐个反归档
          unsigned int ivarCount = 0;
          Ivar *vars = class_copyIvarList(object_getClass(self), &ivarCount);
          for (int i = 0; i < ivarCount; i++) {
          Ivar var = vars[i];
          NSString *varName = [NSString stringWithUTF8String:ivar_getName(var)];
          // 反归档
          id value = [aDecoder decodeObjectForKey:varName];
          // KVC
          [self setValue:value forKey:varName];
        }
        free(vars);
    }
     return self;
  }
  @end

我们在使用的时候,创建一个Student类,在类中声明自己需要的属性,在要使用此类的控制器中实现归档和反归档操作,控制器中具体代码如下:

Student *stu1 = [[Student alloc] init];
stu1.name = @"zhangsan";
stu1.age = 23;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:stu1];
Student *stu2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSLog(@"%@", stu2.name);

2 使用RunTime实现多播委托

2.1 什么是多播委托

简单的说是指允许创建方法的调用列表或者链表的能力.当多播委托调用的时候,列表中的方法均自动执行.
在IOS中我就以我们平常用的最多的delagate为例,普通的delegate只能是一对一的回调,无法做到一对多的回调。而多播委托正式对delegate的一种扩展和延伸,多了一个注册和取消注册的过程,任何需要回调的对象都必须先注册。
如何在IOS中实现多播委托?老外早就已经写好了,而且相当的好用。我最初接触IOS多播委托是我在研究XMPPframework的时候,而多播委托可以说是XMPPframework架构的核心之一。具体的类名就是GCDMulticastDelegate,从名字就可以看出,这是一个支持多线程的多播委托。那为什么要支持多线程呢?我的理解是多个回调有可能不是在同一个线程的,比如我注册回调的时候是在后台线程,但是你回调的时候却在UI线程,那就有可能出问题了。因此必须保证你注册的时候在哪个线程上注册的,那么回调的时候必须还是在那个线程上回调的。

2.2 多播委托的本质,消息转发

如果一个对象收到一条无法处理的消息,运行时系统会在抛出错误前,给该对象发送一条forwardInvocation:消息,该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。

2.2 使用RunTim怎么实现多播委托

给NSObject添加一个类目,添加两个方法,一个增加代理,一个移除代理.

NSObject+MultiDelegate.h

#import <Foundation/Foundation.h>
@interface NSObject (MultiDelegate)
- (void)addDelegate:(id)delegate;
- (void)removeDelegate:(id)delegate;
@end

NSObject+MultiDelegate.m

#import "NSObject+MultiDelegate.h"
#import <objc/objc-runtime.h>

// 将数组关联对象时用的KEY
NSString *const kMultiDelegateKey = @"multiDatagateKey";
@implementation NSObject (MultiDelegate)

// 添加
- (void)addDelegate:(id)delegate
{
      /*我们可以把关联对象想象成一个Objective-C对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void指针(const void *)。我们还需要指定一个内存管理策略,以告诉Runtime如何管理这个对象的内存。*/
    // 设置代理数组,为对象关联对象
    NSMutableArray *delegateArray = objc_getAssociatedObject(self, (__bridge const void *)(kMultiDelegateKey));
    // 若当前对象没有关联的数组,创建并设置
    if (!delegateArray) {
        delegateArray = [NSMutableArray new];
        objc_setAssociatedObject(self, (__bridge const void *)(kMultiDelegateKey), delegateArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    // 添加到数组中
    [delegateArray addObject:delegate];

}

// 删除
- (void)removeDelegate:(id)delegate
{
    NSMutableArray *delegateArray = objc_getAssociatedObject(self, (__bridge const void *)(kMultiDelegateKey));
    if (!delegateArray) {
        @throw [NSException exceptionWithName:@"MultiDelegate error" reason:@"数组为空是不对的" userInfo:nil];
    }
    [delegateArray removeObject:delegate];
}

// 消息转发给代理数组中的元素
- (void)doNothing
{

}

// 消息转发
  /*消息转发机制使用从下面这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。*/
// 获取方法标识
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    // 获取代理的数组
    NSMutableArray *delegateArray = objc_getAssociatedObject(self, (__bridge const void *)(kMultiDelegateKey));
    // 变量数组
    for (id aDelegate in delegateArray) {
        // 取出每个元素对应aSelector的方法标识
        NSMethodSignature *sig = [aDelegate methodSignatureForSelector:aSelector];
        // 如果sig 不为空,返回
        if (sig) {
            return sig;
        }
      }
    return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)];
}
/*运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。*/
// 消息转发给其他对象
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSMutableArray *delegateArray = objc_getAssociatedObject(self, (__bridge const void *)(kMultiDelegateKey));
    for (id aDelegate in delegateArray) {
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 异步转发消息
            [anInvocation invokeWithTarget:aDelegate];  
        });
    }
}

2.4 使用

引入头文件 #import "NSObject+MulticastDelegate.h"

以Student类为例
// 创建student对象
Student *stu = [Student new];
// 添加代理为控制器
[stu addDelegate :self];
// 调用代理方法
[stu performSelector:NSSelectorFromString(@"doSomething") withObject:nil];

// 实现代理方法
- (void)doSomething {
NSLog(@"doNothing");
}

3 使用RunTime实现KVO

3.1 KVO的内部实现

当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

3.2 使用RunTime自己实现KVO

首先创建 NSObject 的 Category,并在头文件中添加两个 API:

 @interface NSObject (KVO)
  // 添加观察者
  - (void)addObserver:(id)observer forKey:(NSString *)key         withBlock:(void(^)(id, NSString *, id, id))block;
 // 移除观察者 
  - (void)removeObserver:(NSObject *)observer forKey:(NSString *)key;
 @end

接下来实现方法:
1、检查对象的类有没有相应的 setter 方法。如果没有抛出异常;
2、检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;
3、检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
4、添加这个观察者

@implementation NSObject (KVO)

- (void)addObserver:(id)observer forKey:(NSString *)key withBlock:(void(^)(id, NSString *, id, id))block
{
    // 获取setterName
    NSString *setName = setterName(key);
    SEL setSelector = NSSelectorFromString(setName);
    // 通过SEL获取方法
    Method setMethod = class_getInstanceMethod(object_getClass(self), setSelector);
    if (!setMethod) {
        @throw [NSException exceptionWithName:@"KVO Error" reason:@"若无setter方法,无法KVO" userInfo:nil];
    }
    // 获取当前类
    // 判断是否已经创建衍生类
    Class thisClass = object_getClass(self);
    NSString *thisClassName = NSStringFromClass(thisClass);
    if (![thisClassName hasPrefix:KVOClassPrefix]) {
        thisClass = [self makeKVOClassWithOriginalClassName:thisClassName];
        // 改变类的标识
        object_setClass(self, thisClass);
    }
    // 判断衍生类是否实现了setter方法
    if (![self hasSelector:setSelector]) {
        const char *setType = method_getTypeEncoding(setMethod);
        class_addMethod(object_getClass(self), setSelector, (IMP)setter, setType);
    }
    // 将observer 添加到观察者数组
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(KVOServerAssociatedKey));
    if (!observers) {
        observers = [NSMutableArray new];
        objc_setAssociatedObject(self, (__bridge const void *)(KVOServerAssociatedKey), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
   }

  // 创建观察者info类
  KVObServerInfo *info = [[KVObServerInfo alloc] initWithObserver:observer forKey:key withBlock:block];
  [observers addObject:info];
}

// 重写setter方法,新的setter在调用原来的setter方法后,通知每个观察者(调用之前传入的block)
- void setter(id objc_self, SEL cmd_p, id newValue)
{
    // setterName 转为name
    NSString *setName = NSStringFromSelector(cmd_p);
    NSString *key = nameWithSetterName(setName);
    // 通过kvc获取key对应的value
    id oldValue = [objc_self valueForKey:key];
    // 将setter消息转发给父类
    struct objc_super selfSuper = {
        .receiver = objc_self,
        .super_class = class_getSuperclass(object_getClass(objc_self))
    };
    objc_msgSendSuper(&selfSuper, cmd_p, newValue);
    // 调用block
    NSMutableArray *observers = objc_getAssociatedObject(objc_self, (__bridge const void *)(KVOServerAssociatedKey));
    for (KVObServerInfo *info in observers) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([info.key isEqualToString:key]) {
            info.block(objc_self, key, oldValue, newValue);
        }
    });
  }
}
// 从setterName转回name
NSString *nameWithSetterName(NSString *setName)
{
    if (setName.length <= 4 || ![setName hasPrefix:@"set"] || ![setName hasSuffix:@":"]) {
        @throw [NSException exceptionWithName:@"KVO Error" reason:@"set方法not available" userInfo:nil];
}
    NSString *Name = [setName substringWithRange:NSMakeRange(3, setName.length - 4)];
    NSString *firstCharacter = [Name substringToIndex:1];
    return [[firstCharacter lowercaseString]   stringByAppendingString:[Name substringFromIndex:1]];
}

// 在衍生类中判断set方法是否存在
- (BOOL)hasSelector:(SEL)aSelector
{
    unsigned int mCount = 0;
    Method *methods =     class_copyMethodList(object_getClass(self), &mCount);
    for (int i = 0; i < mCount; i++) {
        Method method = methods[i];
        SEL setSelector = method_getName(method);
        if (setSelector == aSelector) {
            free(methods);
            return YES;
        }
    }  
    free(methods);
    return NO;
}

// 通过runTime创建类
- (Class)makeKVOClassWithOriginalClassName:(NSString *)className
{
    NSString *kvoClassName = [KVOClassPrefix stringByAppendingString:className];
    Class KVOClass = NSClassFromString(kvoClassName);
    if (KVOClass) {
        return KVOClass;
    }
    // objc_allocateClassPair 创建类
    KVOClass = objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0);
    return KVOClass;
}

// 通过key获取对应的setterName
NSString *setterName(NSString *key) {
    if (key.length == 0) {
        @throw [NSException exceptionWithName:@"KVO Error" reason:@"没有对应的key" userInfo:nil];
    }
    NSString *firstCharacter = [key substringToIndex:1];
    NSString *Name = [[firstCharacter uppercaseString] stringByAppendingString:[key substringFromIndex:1]];
    return [NSString stringWithFormat:@"set%@:", Name];
}

最后一步,把这个观察的相关信息存在 associatedObject 里。观察的相关信息(观察者,被观察的 key, 和传入的 block )封装在 KVObServerInfo 类里。

// 创建一个用于存放观察者info的类
@interface KVObServerInfo: NSObject

// 观察者属性
@property (nonatomic, weak) id observer;
// key属性
@property (nonatomic, copy) NSString *key;
// 回调block
@property (nonatomic, copy) ObsverserBlock block;

@end

@implementation KVObServerInfo

// 初始化方法
- (instancetype)initWithObserver:(id)observer forKey:(NSString *)key withBlock:(ObsverserBlock)block {
    self = [super init];
    if (self) {
        _observer = observer;
        _key = key;
        _block = block;
    }
    return self;
 }
 @end

3.3 使用

在控制器中引入头文件 #import "NSObject+KVO.h"

创建Student的对象
Student *stu1 = [[Student alloc] init];
// 给对象添加观察者
[stu1 addObserver:self forKey:@"name" withBlock:^(id observerObject, NSString *key, id oldValue, id newValue) {
    NSLog(@"%@", oldValue);
    NSLog(@"%@", newValue);
}];
stu1.name = @"张三";
stu1.name = @"李四";

到此我们自己手动完成了KVO的实现。

想要学习更多关于runtime的知识,可以参考博客

Objective-C Runtime 运行时之一:类与对象
Objective-C Runtime 运行时之二:成员变量与属性
Objective-C Runtime 运行时之二:成员变量与属性
Objective-C Runtime 运行时之三:方法与消息
Objective-C Runtime 运行时之四:Method Swizzling
Objective-C Runtime 运行时之五:协议与分类
Objective-C Runtime 运行时之六:拾遗

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

推荐阅读更多精彩内容