RunTime的了解与使用

runtime这个词对于iOS程序猿童鞋来说,都是一个“耳熟能详”的名词,因为runtime就像面试中的“诅咒”一样,每当遇到相关面试题,都是几家欢喜几家愁。那么runtime到底是什么呢?
runtime是 OC底层的一套C语言的API,编译器最终都会将OC代码转化为运行时代码。不过苹果已经将 ObjC runtime 代码开源了,我们可以下面的网址浏览源代码:
http://opensource.apple.com/source/objc4/objc4-493.9/runtime/
那么我们就先通过一些经典的面试题来了解一些runtime这个“诅咒”的威力:

1、下面代码输出结果是什么?

@implementation Son : Father
- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

答案:(1) Son / Son 因为super为编译器标示符,向super发送的消息被编译成objc_msgSendSuper,但仍以self作为receiver

2、下面代码输出结果是什么?

BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

答案:(2) YES / NO / NO / NO 协议有一套类方法的隐藏实现,所以编译运行正常;由于NSObject meta class的父类为NSObject class,所以只有第一句为YES
(3) 下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
    NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[NSObject foo];
[[NSObject new] foo];

答案(3) 编译运行正常,两行代码都执行-foo。 [NSObject foo]方法查找路线为 NSObject meta class –super-> NSObject class,和第二题知识点很相似。
(4) 下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end

@implementation ThirdViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
}
@end
RunTime的了解与使用_第1张图片
效果.png

答案编译运行正常,输出ThirdViewController中的self对象。 编译运行正常,调用了-speak方法,由于 id cls = [Sark class]; void *obj = &cls; obj已经满足了构成一个objc对象的全部要求(首地址指向ClassObject),所以能够正常走消息机制; 由于这个人造的对象在栈上,而取self.name的操作本质上是self指针在内存向高位地址偏移(32位下一个指针是4字节),按viewDidLoad执行时各个变量入栈顺序从高到底为(self, _cmd, self.class, self, obj)(前两个是方法隐含入参,随后两个为super调用的两个压栈参数),遂栈低地址的obj+4取到了self。
看到上面的几道面试题和答案,有些童鞋可能还是一头雾水,那下面就一点点的捋一捋runtime的功能:

一:RunTime中的一些名词概念


objc_msgSend函数定义如下:
id objc_msgSend(id self, SEL op, ...)

(1)什么是 SEL?


打开objc.h文件,看下SEL的定义如下:
typedef struct objc_selector *SEL
SEL是一个指向objc_selector结构体的指针。而objc_selector的定义并没有在runtime.h中给出定义。我们可以尝试运行如下代码:

SEL sel = @selector(foo);
NSLog(@"%s", (char *)sel);
NSLog(@"%p", sel);
const char *selName = [@"foo" UTF8String];
SEL sel2 = sel_registerName(selName);
NSLog(@"%s", (char *)sel2);
NSLog(@"%p", sel2);

输出如下:

2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114

Objective-C在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID。只要方法名称相同,那么它们的ID就是相同的。
所以两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么它的SEL就是一样的。每一个方法都对应着一个SEL。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。而SEL本质是一个字符串,所以直接比较它们的地址即可。
当然,不同的类可以拥有相同的selector。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP

(2)那么什么是IMP呢?继续看定义:


typedef id (*IMP)(id, SEL, ...);
IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。

(3)那么什么是Ivar呢?


ivar 在objc中被定义为:
typedef struct objc_ivar *Ivar;
它是一个指向objc_ivar结构体的指针,结构体有如下定义:

struct objc_ivar {
    char *ivar_name        OBJC2_UNAVAILABLE;
    char *ivar_type          OBJC2_UNAVAILABLE;
    int ivar_offset             OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                    OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

(4)@Property


类中的Property属性被编译器转换成了Ivar,并且自动添加了我们熟悉的Set和Get方法。

(5)isa


isa 是一个 objc_class 类型的指针,内存布局以一个 objc_class 指针为开始的所有东东都可以当做一个 object 来对待! 这就是说 objc_class 或者说类其实也可以当做一个 objc_object 对象来对待!这里要区分清楚两个名词:类对象(class object)实例对象(instance object)。ObjC还对类对象实例对象中的 isa 所指向的类结构作了不同的命名:
类对象中的 isa 指向类结构被称作 metaclass(元类),metaclass 存储类的static类成员变量与static类成员方法(+开头的方法);
实例对象中的 isa 指向类结构称作 class(普通的),class 结构存储类的普通成员变量与普通成员方法(-开头的方法)。

(6)super_class:


一看就明白,指向该类的父类呗!如果该类已经是最顶层的根类(如 NSObject 或 NSProxy),那么 super_class 就为 NULL。
好,先中断一下其他类结构成员的介绍,让我们厘清一下在继承层次中,子类,父类,根类(这些都是普通 class)以及其对应的 metaclass 的 isa 与 super_class 之间关系:
规则一:类的实例对象的 isa 指向该类;该类的 isa 指向该类的 metaclass; 规则二:类的 super_class 指向其父类,如果该类为根类则值为 NULL; 规则三:metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass 则指向自身; 规则四:metaclass 的 super_class 指向父 metaclass,如果该 metaclass 是根 metaclass 则指向该 metaclass 对应的类;

(7)那么 class 与 metaclass 的区别


class 是 instance object 的类类型。当我们向实例对象发送消息(实例方法)时,我们在该实例对象的 class 结构的 methodlists 中去查找响应的函数,如果没找到匹配的响应函数则在该 class 的父类中的 methodlists 去查找(查找链为上图的中间那一排)。如下面的代码中,向str 实例对象发送 lowercaseString 消息,会在 NSString 类结构的 methodlists 中去查找 lowercaseString 的响应函数。

NSString * str;
[str lowercaseString];

metaclass 是 class object 的类类型。当我们向类对象发送消息(类方法)时,我们在该类对象的 metaclass 结构的 methodlists 中去查找响应的函数,如果没有找到匹配的响应函数则在该 metaclass 的父类中的 methodlists 去查找。如下面的代码中,向 NSString 类对象发送 stringWithString 消息,会在 NSString 的 metaclass 类结构的 methodlists 中去查找 stringWithString 的响应函数。
[NSString stringWithString:@"str"];

二:Category添加属性

category在我们实际开发过程中,是一个非常实用的得力助手,因为我们可以在不改变原有类的情况下进行方法拓展,但是有些时候,我们也需要为这些category分类添加一些属性供我们使用,问题就在这里,我们都知道:category分类无法直接添加属性,但是我们开发中又有这样的需求,这就尴尬啦~~~
首先我们先看一下,为什么在category中无法直接添加属性呢,category是表示一个指向分类的结构体的指针,其定义如下:

struct objc_category {
    char *category_name  OBJC2_UNAVAILABLE;// 分类名
    char *class_name OBJC2_UNAVAILABLE;// 分类所属的类名
    struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;// 实例方法列表
    struct objc_method_list *class_methods OBJC2_UNAVAILABLE;// 类方法列表
    struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;// 分类所实现的协议列表
}

在这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
可发现,类别中没有ivar(ivar代表类中实例变量的类型)成员变量指针,也就意味着:类别中不能够添加实例变量和属性,struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;// 该类的成员变量链表简单的理解就是:在category中,系统无法自动为@property的属性生成 set、get方法
【有些人可能就有疑惑了:为什么官方API和一些第三方的框架工具都有为category添加属性的现象呢?比如:NSIndexPath (UITableView)、MJRefresh等等】所以这是我们就需要使用到runtime为category关联一些属性对象。

/** 
 *通过键值对关联对象
 * 
 * @param object  关联的对象源【通常为 self】
 * @param key 唯一键,通过这个键进行取值【const void *是用来起到声明作用】
 * @param value 值;“ key ”所对应的值
 * @param policy 内存管理策略,枚举:objc_AssociationPolicy
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

/** 
 * 通过 key 获取关联值.
 * 
 * @param object 关联的对象源【通常为 self】,在设置关联时所指定的与哪个对象关联的那个对象
 * @param key 唯一键,在设置关联时所指定的键
 * 
 * @return 返回唯一键对应的 value 值
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

/** 
 * 取消属性关联对象
 * 
 * @param object 要取消关联属性的对象
 * 
 * @see objc_setAssociatedObject
 * @see objc_getAssociatedObject
 */
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

其中的关联策略也是系统提供的一个枚举:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**表示弱引用关联,通常是基本数据类型 */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**表示强引用关联对象,是线程安全的; 如同:(nonatomic,strong) */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**表示关联对象copy,是线程安全的; 如同:(nonatomic,copy) */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**表示强引用关联对象,不是线程安全的;  如同:(atomic,strong)*/
    OBJC_ASSOCIATION_COPY = 01403          /**表示关联对象copy,不是线程安全的; 如同:(nonatomic,copy)  */
};

举个小栗子 ~

.h 中声明
///通过runtime关联数组属性
@property (nonatomic,strong)NSMutableArray * array;

.m 中实现set、get 方法
/// 设置属性关联
-(void)setArray:(NSMutableArray *)array{
    objc_setAssociatedObject(self, @selector(array),
                             array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSMutableArray *)array{
    return objc_getAssociatedObject(self, @selector(array));
}

RunTime的了解与使用_第2张图片
设置调用关联属性.png

RunTime的了解与使用_第3张图片
set方法被调用.png

RunTime的了解与使用_第4张图片
get方法被调用.png

当运行程序就可以发现:通过上面的runtime可以完美的为category关联我们需要的属性。

三:方法交换


说到方法交换,这个也是在实际开发中经常用到的一个技能,比如:对象默认调用的系统方法API,但是我们又需要对这个方法的调用及实现进行处理的情况下,就可以使用到 runtime的方法交换 method_exchangeImplementations进行实现这个需求。首先来看一些 runtime.h中提供的几个方法:

/** 
 * 返回一个指定类对象实现的实例方法。
 * 
 * @param cls 指定的类
 * @param name 需要检索的方法
 * 
 */
OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);

/** 
 * 返回一个指定类对象实现的 类方法。
 * 
 * @param cls 指定的类
 * @param name 需要检索的方法
 * 
 */
OBJC_EXPORT Method class_getClassMethod(Class cls, SEL name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
/** 
 * 交换两个方法的实现.
 * 
 * @param m1 方法与第二个方法交换。
 * @param m2 方法与第一个方法交换。
 * 
 */
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

举一个小栗子:

RunTime的了解与使用_第5张图片
类方法交换.png

RunTime的了解与使用_第6张图片
对象方法交换.png
从代码运行的效果可以看出:方法交换之后,再调用原来的 eat 方法,在实现过程中,就会默认进入到交换的 goToSchool 方法内。这就是关于 runtime方法交换的一个简单示例,在实际开发中可以根据自己的需求进行实际操作啦!

四:动态类型判断

即运行时再决定对象的类型。这类动态特性在日常应用中非常常见,简单说就是id类型。id类型即通用的对象类,任何对象都可以被id指针所指,而在实际使用中,往往使用introspection来确定该对象的实际所属类:
- (BOOL)isMemberOfClass:(Class)aClass是 NSObject 的方法,用以确定某个 NSObject 对象是否是某个类的成员。
- (BOOL)isKindOfClass:(Class)aClass是用以确定某个对象是不是某个类或其子类的成员;例如:

id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
    someClass *classSpecifiedInstance = (someClass *)obj;
    // Do Something to classSpecifiedInstance which now is an instance of someClass
    //...
}

五:动态绑定方法

首先先看一下系统提供的动态绑定相关的几个方法:

 /**
 当这个类被调用了一个没有实现的方法时,会调用到这里
 @param sel 未实现的类方法名
 */
+ (BOOL)resolveClassMethod:(SEL)sel  OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
 当这个类被调用了一个没有实现的对象
 @param sel 未实现的对象方法名
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
     动态添加
     class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP) myMethodIMP, "v@:");
     
     1、Class cls:类的类型
     2、name:方法标记 sel
     3、imp:方法的实现,是一个 函数指针
     4、type:返回值类型
     */
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp,  const char *types)   OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

/**
上述添加方法中的 IMP imp 所对应的函数格式如下:
*/
void myMethodIMP(id self, SEL _cmd)
{
    // implementation ....
}

动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。在Cocoa层,我们一般向一个NSObject对象发送-respondsToSelector:或者-instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类的+resolveClassMethod:和+resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。举个小栗子:

RunTime的了解与使用_第7张图片
动态添加方法.png
从运行结果上可以看出:当对象调用一个该类没有的方法时,系统会自动调用 resolveInstanceMethod :方法,然后我们可以使用 class_addMethod方法进行动态绑定,从而达到预期效果。
其中需要注意的是: performSelector: withObject: 方法,因为调用这个方法时,系统在编译时是不会进行校验方法是否存在,只有在运行时才会进行查询方法。而直接调用方法时,在编译过程中就会进行校验方法是否存在。

六:获取对象属性


最典型的用法就是一个对象在 归档 encodeWithCoder解档initWithCoder:方法中需要该对象所有的属性进行 encodeObject:decodeObjectForKey:,通过runtime我们声明中无论写多少个属性,都不需要再修改实现中的代码了。

获得某个类的所有成员变量(outCount 会返回成员变量的总数)
参数:
/**
1、哪个类
2、放一个接收值的地址,用来存放属性的个数
3、返回值:存放所有获取到的属性,通过下面两个方法可以调出名字和类型
*/
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
//获得成员变量的名字
const char *ivar_getName(Ivar v)
//获得成员变量的类型(除了基本数据类型)
const char *ivar_getTypeEndcoding(Ivar v)

举个小栗子:

// C语言内  但凡看到 copy creat new 需要释放
// ARC
// 告诉系统归档哪些东西
- (void)encodeWithCoder:(NSCoder *)coder
{
    unsigned int count = 0;
    Ivar * ivars = class_copyIvarList([Person class], &count);
    for (int i = 0; i < count; i ++) {
        //取出对应的成员Ivar
        Ivar ivar = ivars[i];
        const char * name = ivar_getName(ivar);
        const char *type = ivar_getTypeEncoding(ivar);
        NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
        
        NSString * key = [NSString stringWithUTF8String:name];
        //归档
        [coder encodeObject:[self valueForKey:key] forKey:key];
    }
    free(ivars);
}
//解档
- (instancetype)initWithCoder:(NSCoder *)coder
{
    if (self =[super init]) {
        unsigned int count = 0;
        Ivar * ivars = class_copyIvarList([Person class], &count);
        for (int i = 0; i < count; i ++) {
            //取出对应的成员Ivar
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString * key = [NSString stringWithUTF8String:name];
            //解档
            id value = [coder decodeObjectForKey:key];
            //设置到属性身上
            [self setValue:value forKey:key];
        }
        free(ivars);
        
    }
    return self;
}

这就是使用runtime实现的归档解档方法。

七:字典转模型


字典转模型这个在实际开发中也是最常用的,因为当我们网络请求到数据后,我们就需要将数据转化为对应的对象模型。不过现在有比较成熟的字典转模型框架,比如:YYModelMJExtension等,那我们如何利用runtime实现自己的字典转模型框架呢?举个小栗子:

NSObject+Runtime_Model.h 代码逻辑:

@interface NSObject (Runtime_Model)
/** 字典转模型
 使用该方法进行字典转模型时,如果使用了设置属性名字与返回的 key 不同时,
 需要实现 - (void)setValue:(id)value forUndefinedKey:(NSString *)key 方法,进行转化
 */
+ (instancetype)objectWithDict:(NSDictionary *)dict;
@end

NSObject+Runtime_Model.m 代码逻辑:

#import "NSObject+Runtime_Model.h"
#import 

@implementation NSObject (Runtime_Model)
// 字典转模型
+ (instancetype)objectWithDict:(NSDictionary *)dict{
    // 创建对应模型对象
    id objc = [[self alloc] init];
    
    // 判断字典中的 key 是否为成员变量,以便为成员变量进行替换赋值
    for (NSString * key in dict.allKeys) {
        
        id value = dict[key];
        
        /*判断当前属性是不是Model*/
        objc_property_t property = class_getProperty([self class], key.UTF8String);
        unsigned int outCount = 0;
        objc_property_attribute_t * attributeList = property_copyAttributeList(property, &outCount);
        
        if (!attributeList) {// 属于模型的属性
            
            if ([objc respondsToSelector:@selector(setValue:forUndefinedKey:)]) {
                [objc setValue:value forUndefinedKey:key];
            }
        }
    }
    unsigned int count = 0;
    
    // 1.获取成员属性数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
    for (int i = 0; i < count; i++) {
        
        // 2.1 获取成员属性
        Ivar ivar = ivarList[i];
        
        // 2.2 获取成员属性名 C -> OC 字符串
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 2.3 _成员属性名 => 字典key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 2.4 去字典中取出对应value给模型属性赋值
        id value = dict[key];
        
        // 属性对应的类名
        const char *type = ivar_getTypeEncoding(ivar);
        
        // 获取成员属性类型
        NSString *ivarType = [NSString stringWithUTF8String:type];
        
        // 二级转换,字典中还有字典,也需要把对应字典转换成模型
        //
        // 判断下value,是不是字典
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { //  是字典对象,并且属性名对应类型是自定义类型
            // user User
            
            // 处理类型字符串 @\"User\" -> User
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            // 自定义对象,并且值是字典
            // value:user字典 -> User模型
            // 获取模型(user)类对象
            Class modalClass = NSClassFromString(ivarType);
            
            // 字典转模型
            if (modalClass) {
                // 字典转模型 user
                value = [modalClass objectWithDict:value];
            }
            
            // 字典,user
            //            NSLog(@"%@",key);
        }
        
        // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            
            // 转换成id类型,就能调用任何对象的方法
            id idSelf = self;
            
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            
            // 生成模型
            Class classModel = NSClassFromString(ivarType);
            NSMutableArray *arrM = [NSMutableArray array];
            // 遍历字典数组,生成模型数组
            for (NSDictionary *dict in value) {
                // 字典转模型
                id model =  [classModel objectWithDict:dict];
                [arrM addObject:model];
            }
            // 把模型数组赋值给value
            value = arrM;
            
        }
        // 2.5 KVC字典转模型
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    // 返回对象
    return objc;
    
}
@end

当然这也只是RunTime的一部分功能,但却是在实际开发中经常用到的知识点。所以如果想要继续拓展RunTime技能深度的话,可以翻看苹果开源的RunTime源码http://opensource.apple.com/source/objc4/objc4-493.9/runtime/
参考 :http://devclub.cc/article?articleId=Vn/22T2qcRg1HT4927IyyA==

你可能感兴趣的:(RunTime的了解与使用)