OC-runtime简述

Runtime是什么

在C语言中,将代码转换成为可执行文件,一般要经历三个步骤:
编译--->链接--->运行
在链接的时候,对象的类型、方法的实现已经确认好了.
而在OC中,却将一下在编译和链接所处理的工作放在了运行阶段,也就是说, 一个编译好的.ipa包,在程序没有运行的时候,谁也不知道调用一个方法会发生什么,这也为热修复提供了可能, 因此OC是一门动态语言.
这样的设计使Objective-C变得灵活,我们可以在程序运行的时候,动态的修改一个方法的实现,这个黑魔法就是runtime.
runtime就是iOS的一个库,这个库使我们在程序运行时,创建对象、检查对象、修改类和对象的方法.

Runtime是怎么工作的

首先要知道类和对象在OC 中是怎么定义的?

Class(类)的定义

在objc.h中,Class被定义为指向objc_class的指针定义如下:

typedef struct objc_class  *Class;

而objc_class是一个结构体,在runtime 中定义如下:

struct objc_class {
    Class isa;                                // 实现方法调用的关键
    Class super_class;                        // 父类
    const char * name;                        // 类名
    long version;                             // 类的版本信息,默认为0
    long info;                                // 类信息,供运行期使用的一些位标识
    long instance_size;                       // 该类的实例变量大小
    struct objc_ivar_list * ivars;            // 该类的成员变量链表
    struct objc_method_list ** methodLists;   // 方法定义的链表
    struct objc_cache * cache;                // 方法缓存
    struct objc_protocol_list * protocols;    // 协议链表
};

提示:在 Xcode 中,使用快捷键 command + shift + o,可以打开搜索窗口,输入 objc_class 即可看到头文件定义

可以看到一个类中保存了自身所有的:
成员变量(ivers)
所有方法(methodLists)
所有实现的协议(objc_protocol_list)
isa(指向指针)
cache(缓存)
...

OC中对象(objc)的定义

id 被定义为指向objc_object 的结构体指针,说明objc_object就是用到的对象的定义.

typedef struct objc_object *id

在看objc_object 的定义, objc_object其实是一个结构体, 只包含一个isa 指针, 而objc_object结构体中唯一保存的就是Class 的isa(地址)

struct objc_object {
       Class isa;
}

总结: 当一个对象调用方法时, 对象会通过自身的isa 寻找对应的objc_class, 然后在objc_class 结构体methodLists找到调用的方法;

再说cache ,因为调用方法的过程是个查找 methodLists 的过程,如果每次调用都去查找,效率会非常低。所以对于调用过的方法,会以 map 的方式保存在 cache 中,下次再调用就会快很多。

Meta Class 元类

调用一个对象的类方法的过程是怎么样的?还有 objc_class 中也有一个 isa 指针,它是干嘛用的?

其实观察 objc_class 和 objc_object 的定义,会发现两者其实本质相同(都包含 isa 指针),只是 objc_class 多了一些额外的字段,相应的,类也是一个对象,只是保存了一些字段.

既然说类也是对象,那么类的类型是什么呢?这里就引出了另外一个概念 —— Meta Class(元类).

在 Objective-C 中,每一个类都有对应的元类。而在元类的 methodLists 中,保存了类的方法链表,即所谓的「类方法」。并且类的 isa 指针指向对应的元类。因此上面的问题答案就呼之欲出,调用一个对象的类方法的过程如下:

  1. 通过对象的isa 指针找到对应的Class;
    2.通过类的isa 找到Class对应的 Meta Class;
  2. 在Meta Class 的methodLists, 找到对应的方法, 然后执行;

那么Meta Class(元类)也应该是一个对象, OC 的设计者让所有的元类的isa 指向基类的元类, 而基类的元类指向自己;

OC Method(方法调用)

Method 的定义:

// Method 被定义为一个 objc_method 指针
typedef struct objc_method *Method;

在看结构体obje_method 怎么定义的:

##### objc_method
//在 objc_method 结构体中,包含一个 SEL 和一个 IMP
struct objc_method {
       SEL method_name;
       char *method_types;
       IMP method_imp;
}
SEL定义

SEL 是一个指向 objc_selector 的指针,而 objc_selector 在头文件中找不到明确的定义

typedef struct objc_selector *SEL;

我们来测试以下代码:

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);          // 输出:viewDidLoad
SEL sel1 = @selector(viewDidLoad1);
NSLog(@"%s", sel1);         // 输出:viewDidLoad1

可以看到,SEL 不过是保存了方法名的一串字符,因此我们可以认为,SEL 就是一个保存方法名的字符串.

由于一个 Method 只保存了方法的方法名,并最终要根据方法名来查找方法的实现,因此在Objective-C 中不支持下面这种定义.

-(void)setWidth:(int)width;
-(void)setWidth:(double)width;
IMP定义
typedef id (*IMP)(id, SEL, ...);

再来说 IMP 。可以看到它是一个「函数指针.简单来说,「函数指针」就是用来找到函数地址,然后执行函数.
这里要注意, IMP 指向的函数的前两个参数是默认参数, id 和 SEL 。这里的 SEL 好理解,就是函数名,而 id ,对于实例方法来说, self 保存了当前对象的地址;对于类方法来说, self 保存了当前对应类对象的地址,后面的省略号即是参数列表。

敲黑板: Method 建立了 SEL 和 IMP 的关联,当对一个对象发送消息时,会通过给出的 SEL 去找到 IMP ,然后执行.

在 Objective-C 中,所有的方法调用,都会转化成向对象发送消息。发送消息主要是使用 objc_msgSend 函数; 看一下头文件定义:

id objc_msgSend(id self, SEL op, ...);
// 譬如: 
[self loadView]; 
// 调用时就会变成:
objc_msgSend(self, @selector(loadView));

当向一个对象发送消息时,会去这个类的 methodLists 中查找相应的 SEL ,如果查不到,则通过 super_class 指针找到父类,再去父类的 methodLists 中查找,层层递进。最后仍然找不到,才走抛异常流程;

当一个方法找不到的时候,会走拦截调用和消息转发流程。我们可以重写 +resolveClassMethod: 和 +resolveInstanceMethod: 方法,在程序崩溃前做一些处理。通常的做法是动态添加一个方法,并返回 YES 告诉程序已经成功处理消息。如果这两个方法返回 NO ,这个流程会继续往下走.

Category(分类)

我们来看一下 Category 在头文件中的定义:

typedef struct objc_category *Category;
struct objc_category {
    char * category_name;
    char * class_name;
    struct objc_method_list * instance_methods;
    struct objc_method_list * class_methods;
    struct objc_protocol_list * protocols;
}

Category 是一个指向 objc_category 结构体的指针,在 objc_category 中包含对象方法列表、类方法列表、协议列表。从这里我们也可以看出, Category 支持添加对象方法、类方法、协议,但不能保存成员变量。

注意:在 Category 中是可以添加属性的,但不会生成对应的成员变量、 getter 和 setter 。因此,调用 Category 中声明的属性时会报错。

我们可以通过「关联对象」的方式来添加可用的属性。具体操作如下:
1、在 UIViewController+Tag.h 文件中声明 property 。

@property (nonatomic, strong) NSString *tag;

2、在 UIViewController+Tag.m 中实现 getter 和 setter 。记得添加头文件 #import。主要是用到 objc_setAssociatedObject 和 objc_getAssociatedObject 这两个方法。

static void *tag = &tag;
@implementation UIViewController (Tag)
- (void)setTag:(NSString *)t {
    objc_setAssociatedObject(self, &tag, t, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tag {
    return objc_getAssociatedObject(self, &tag);
}
@end

3、在子类中调用。

// 子类 ViewController.m
- (void)testCategroy {
    self.tag = @"TAG";
    NSLog(@"%@", self.tag);   // 这里输出:TAG
}

注意:当一个对象被释放后, Runtime 回去查找这个对象是否有关联的对象,有的话,会将它们释放掉。因此不需要我们手动去释放。

Runtime 常规操作

1. Method Swizzling 方法交换

首先来介绍一下被称为「黑魔法」的 Method Swizzling,Method Swizzling 使我们有办法在程序运行的时候,去修改一个方法的实现。包括原生类(比如 UIKit 中的类)的方法。首先来看下通常的写法:

Method originalMethod = class_getInstanceMethod(class, (originalSelector));
Method swizzledMethod = class_getInstanceMethod(class, (swizzledSelector));
if (!class_addMethod((class),                                               
                     (originalSelector),                                 
                     method_getImplementation(swizzledMethod),  
                     method_getTypeEncoding(swizzledMethod))) {             
    method_exchangeImplementations(originalMethod, swizzledMethod);         
} else {                                                                    
    class_replaceMethod((class),                                            
                        (swizzledSelector),                                 
                        method_getImplementation(originalMethod),           
                        method_getTypeEncoding(originalMethod));            
}

简单描述一下:先获取 originalMethod 和 swizzledMethod 。将 originalMethod 加到想要交换方法的类中(注意此时的 IMP 是 swizzledMethod 的 IMP ),如果加入成功,就用 originalMethod 的 IMP 替换掉 swizzledMethod 的 IMP ;如果加入失败,则直接交换 originalMethod 和 swizzledMethod 的 IMP 。
那么问题来了,为什么不直接用 method_exchangeImplementations 来交换就好?
因为可能会影响父类中的方法。比如我们在一个子类中,去交换一个父类中的方法,而这个方法在子类中没有实现,这个时候父类的方法就指向了子类的实现,当这个方法被调用的时候就会出问题。所以先采取添加方法的方式,如果添加失败,证明子类已经实现了这个方法,直接用 method_exchangeImplementations 来交换;如果添加成功,则说明没有实现这个方法,采取先添加后替换的方式。这样就能保证不影响父类了。
如果每次交换都写这么多就太麻烦了,我们可以定义成一个宏,使用起来更方便。

#define SwizzleMethod(class, originalSelector, swizzledSelector) {              \
    Method originalMethod = class_getInstanceMethod(class, (originalSelector)); \
    Method swizzledMethod = class_getInstanceMethod(class, (swizzledSelector)); \
    if (!class_addMethod((class),                                               \
                         (originalSelector),                                    \
                         method_getImplementation(swizzledMethod),              \
                         method_getTypeEncoding(swizzledMethod))) {             \
        method_exchangeImplementations(originalMethod, swizzledMethod);         \
    } else {                                                                    \
        class_replaceMethod((class),                                            \
                            (swizzledSelector),                                 \
                            method_getImplementation(originalMethod),           \
                            method_getTypeEncoding(originalMethod));            \
    }                                                                           \
}

在 +load 中调用:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod([self class], @selector(viewWillAppear:), @selector(AA_viewWillAppear:));        
    });
}

注意:我们要保证方法只会被交换一次。因为 +load 方法原则上只会被调用一次,所以一般将 Method Swizzling 放在 +load 方法中执行。但 +load 方法也可能被其他类手动调用,这时候就有可能会被交换多次,所以这里用 dispatch_once_t 来保证只执行一次。
那么上面的交换操作是否万无一失了呢?还远远不够。
通常情况下上面的交换不会出什么问题,但考虑下面一种场景。(注: ViewController 继承自 UIViewController )
修改 UIViewController 中的 viewWillAppear: :

// UIViewController (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod([self class], @selector(viewWillAppear:), @selector(AA_viewWillAppear:));        
    });
}
- (void)AA_viewWillAppear:(BOOL)animated {
    NSLog(@"UIViewController");
    [self AA_viewWillAppear:animated];
}

修改 ViewController 中的 viewWillAppear: (注: ViewController 没有重写该方法):

// ViewController (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod([self class], @selector(viewWillAppear:), @selector(BB_viewWillAppear:));        
    });

}
- (void)BB_viewWillAppear:(BOOL)animated {
    NSLog(@"ViewController");
    [self BB_viewWillAppear:animated];
}

这里父类和子类同时对 viewWillAppear: 方法进行交换,每次交换都加入一句输出语句。则当 ViewController 调用 viewWillAppear: 时,我们期望输出下面结果:
ViewController
UIViewController
大部分情况的确是这样,但也有可能只输出:
ViewController
因为我们是在 +load 中做交换操作,而子类的 +load 却有可能先于父类执行。这样造成的结果是,子类先拷贝父类的 viewWillAppear: ,并进行交换,然后父类再进行交换。但这个时候父类的交换结果并不会影响子类,也无法将 NSLog(@"UIViewController") 写入子类的 viewWillAppear: 方法中,所以不会输出。
这里解决这个问题的思路是:在子类的 swizzledMethod 中,动态地去查找父类替换后方法的实现。每次调用都会去父类重新查找,而不是拷贝写死在子类的新方法中。这样子类 viewWillAppear: 方法的执行结果就和 +load 的加载顺序无关了。
至于怎么实现动态查找,这里推荐 RSSwizzle ,这个库不仅解决了上面提到的问题,还保证了 Method Swizzling 的线程安全,是一种更安全优雅的解决方案。简单使用举例:

RSSwizzleInstanceMethod([self class],
                        @selector(viewWillAppear:),
                        RSSWReturnType(void),
                        RSSWArguments(BOOL animated),
                        RSSWReplacement({
    NSLog(@"ViewController");
    RSSWCallOriginal(animated);
}), RSSwizzleModeAlways, NULL);
2.获取所有属性和方法

Runtime 中提供了一系列 API 来获取 Class 的成员变量( Ivar )、属性( Property )、方法( Method )、协议( Protocol )等.

// 测试 打印属性列表
- (void)testPrintPropertyList {
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"property----="">%@", [NSString stringWithUTF8String:propertyName]);
    }
    free(propertyList);
}
// 测试 打印方法列表
- (void)testPrintMethodList {
    unsigned int count;
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i=0; i
        Method method = methodList[i];
        NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
    }
    free(methodList);
}
// 测试 打印成员变量列表
- (void)testPrintIvarList {
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i=0; i
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"ivar----="">%@", [NSString stringWithUTF8String:ivarName]);
    }
    free(ivarList);
}
// 测试 打印协议列表
- (void)testPrintProtocolList {
    unsigned int count;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i=0; i
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolName]);
    }
    free(protocolList);
}
RunTime 的使用场景
1.面向切面编程(AOP)

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率.

画重点,对业务逻辑进行分离,降低耦合度。
假设现在有这样一个需求,我们要对应用中所有按钮的点击事件进行上报,统计每个按钮被点击的次数。
首先我们要明确,统计功能应该与业务无关,即统计代码不应该与业务代码耦合在一起。因此用上面「AOP」的思想来实现是合适的,而 Runtime 给我们提供了这样一条途径。因为当按钮点击时,会调用 sendAction:to:forEvent: 方法,所以我们可以使用 Method Swizzling 来修改该方法,在其中添加上报的逻辑.

// UIButton+Swizzling.m
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        RSSwizzleInstanceMethod([self class],
                                @selector(sendAction:to:forEvent:),
                                RSSWReturnType(void),
                                RSSWArguments(SEL action, id target, UIEvent *event),
                                RSSWReplacement({
            NSString *name = NSStringFromClass([self class]);
            NSLog(@"UIButton+Swizzling:%@ 按钮被点击--上报", name);
            RSSWCallOriginal(action, target, event);
        }), RSSwizzleModeAlways, NULL);
    });
}

注意:尽管上面的需求也可以用继承一个基类的方式来实现,但是如果此时已经有很多类继承自 UIButton ,则修改起来会很麻烦,其次我们也不能保证后续的所有按钮都继承这个基类。另外上面提到,统计逻辑不应该和业务逻辑耦合,如果为了统计的需求去修改业务代码,也是不可取的(除非迫不得已)。因此上面利用 Method Swizzling 的方式更为合适,也更为简洁。

2.dictionary 转model

我们可以用 KVC 来实现字典转模型,方法是调用 setValuesForKeysWithDictionary: 。但这种方法要求 Model 的属性和 NSDictionary 的 key 一一对应,否则就会报错。这里可以用 Runtime 配合 KVC ,来实现更灵活的字典转模型。

下面为 NSObject 添加一个分类,添加一个初始化方法,来看代码:

// NSObject+JSONExtension.h
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
    self = [self init];
    if (self) {
        unsigned int count;
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        for (unsigned int i=0; i
            // 获取属性列表
            const char *propertyName = property_getName(propertyList[i]);
            NSString *name = [NSString stringWithUTF8String:propertyName];
            id value = [dictionary objectForKey:name];
            if (value) {
                // 注意这里用到 KVC
                [self setValue:value forKey:name];
            }
        }
        free(propertyList);
    }
    return self;
}

尝试调用:

NSDictionary *info = @{@"title": @"标题", @"count": @(1), @"test": @"hello"};
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
NSLog(@"%@", objectA.title);     // 输出:标题
NSLog(@"%ld", (long)objectA.count);         // 输出:1

注意:在实际的应用中,会有更多复杂的情况需要考虑,比如字典中包含数组、对象等。这里只是做个简单示例.

3. 进行归解档

「归档」是将对象序列化存入沙盒文件的过程,会调用 encodeWithCoder: 来序列化。「解档」是将沙盒文件中的数据反序列化读入内存的过程,会调用 initWithCoder: 来反序列化。

通常来说,归解档需要对实例对象的各个属性依次进行归档和解档,十分繁琐且易出错。这里我们参照「字典转模型」的例子,通过获取类的所有属性,实现自动归解档。
触发对象归档可以调用 NSKeyedArchiver 的 + archiveRootObject:toFile: 方法;触发对象解档可以调用 NSKeyedUnarchiver 的 + unarchiveObjectWithFile: 方法。
注: xib 文件在载入的时候,也会触发 initWithCoder: 方法,可见读取 xib 文件也是一个解档的过程。
首先在 NSObject 的分类中添加两个方法:

// NSObject+JSONExtension.m
- (void)initAllPropertiesWithCoder:(NSCoder *)coder {
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i
        const char *propertyName = property_getName(propertyList[i]);
        NSString *name = [NSString stringWithUTF8String:propertyName];
        id value = [coder decodeObjectForKey:name];
        [self setValue:value forKey:name];
    }
    free(propertyList);
}
- (void)encodeAllPropertiesWithCoder:(NSCoder *)coder {
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i
        const char *propertyName = property_getName(propertyList[i]);
        NSString *name = [NSString stringWithUTF8String:propertyName];
        id value = [self valueForKey:name];
        [coder encodeObject:value forKey:name];
    }
    free(propertyList);
}

在 NSObject 的子类中实现归解档方法:

// ObjectA.m
- (id)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        [self initAllPropertiesWithCoder:aDecoder];
    }
    return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder{
    [self encodeAllPropertiesWithCoder:aCoder];
}

尝试调用:

NSDictionary *info = @{@"title": @"标题11", @"count": @(11)};
NSString *path = [NSString stringWithFormat:@"%@/objectA.plist", NSHomeDirectory()];
// 归档
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
[NSKeyedArchiver archiveRootObject:objectA toFile:path];
// 解档
ObjectA *objectB = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"%@", objectB.title);      // 输出:标题11
NSLog(@"%ld", (long)objectB.count);       // 输出:11

注:上面的代码逻辑并不完善,只是做简单示例用。

4. 逆向开发

在「逆向开发」中,会用到一个叫 class-dump 的工具。这个工具可以将已脱壳的 APP 的所有类的头文件导出,为分析 APP 做准备。这里也是利用 Runtime 的特性,将存储在mach-O文件中的 @interface 和 @protocol 信息提取出来,并生成对应的 .h 文件。

5. 热修复

「热修复」是一种不需要发布新版本,通过动态下发修复文件来修复 Bug 的方式。比如 JSPatch,就是利用 Runtime 强大的动态能力,对出问题的代码段进行替换。

源码

请到 GitHub 上查看完整例子。

参考

iOS运行时(Runtime)详解+Demo

OC最实用的runtime总结,面试、工作你看我就足够了!

iOS Runtime 详解

Method swizzling的正确姿势

Method Swizzling 安全性分析 和 RSSwizzle解决方案分析

【南峰子的技术博客】Objective-C Runtime 运行时系列

runtime 面试题
1. [self class]和[super class]返回的分别是什么?

[self class]会被编译器转化成 objc_msgSend(self, @selector(class))
[super class]会被编译器转化成 objc_msgSendSuper(super, @selector(class)).
super在这里只是一个结构体(objc_super),objc_super结构体重有一个 receiver的变量,也就是方法的接收者,这里的接收者也是当前对象self。
所以这两个消息传递的接收者都是当前对象self,两个方法的运行结果也就相同,都会返回当前对象的class。

2. 讲一下对象、类对象、元类跟元类的结构体是如何关联的?

1). 所有的OC对象的基础结构体都是objc_object,而类和元类的结构体objc_class也是继承于objc_object的,所以类和元类其实也是一种“对象”.
2). objc_object有isa指针,对象、类对象、元类对象正是通过isa指针相连.

3. 为什么对象方法没有保存到对象的结构体里,而是保存在类对象的结构体里?

1).假如保存在对象中,当我已经创建一个对象后,再为这个类动态的添加一个对象方法,这时由于已经创建的对象的内存布局已经确定,也就没办法添加这个方法了,这个对象就会有问题。也就无法实现动态添加方法的功能。

2). 将对象方法保存在类对象类,那么所有的对象方法只要在类对象中保存一份即可,而不需要再每个对象里都保存一份,会节省大量的内存。

3). 正是因为方法保存在类对象中,而每次调用时通过isa指针找到类对象再找到方法调用,才保证了OC语言的动态特性,方法可以动态添加、修改,否则就不能实现动态的效果。

4). 在方法的实际调用中有这样的过程:先找缓存->在找当前类中的对象方法列表->再逐级查找父类的对象方法列表,通过把常用的方法放入缓存中,及提高了访问速度,也节省了不必要的内存开销。通过把对象方法放在父类才能很好的实现这个功能,放在对象中实现的话,就非常浪费内存。

4.class_ro_t和class_rw_t的区别? PS: ro == readonly, rw == readwrite

1). class_ro_t是class_rw_t的一个变量,用于保存宿主类的成员变量列表信息、协议列表、对象方法列表、属性列表信息(这三个列表都是一位数组),这些是在类注册到runtime中就固定了,无法再修改.
2). class_rw_t的成员变量除了class_ro_t之外,还有methods、protocols、property三个二维数组,之所以是二维数组是因为一个类可以有多个分类,而每个分类都可以有多个方法、协议、属性,所以就是二维数组。class_rw_t是可以动态修改的,当运行时动态的给一个类添加一个方法时,这个方法就保存在methods中。

这里再说明一下,类在编译时初始化时会创建class_ro_t结构体,里边的内容都是只读的,在运行时初始化会创建class_rw_t,将class_rw_t的ro指针指向之前创建的class_ro_t结构体,并将class_ro_t中的方法列表添加到class_rw_t的methodList中,在之后进行方法查找时就不需要再查class_ro_t了.

5. SEL和IMP的区别?

SEL相当于是方法名,IMP则是函数指针,在类对象的方法缓存哈希表中,SEL和IMP相当于对应着key和value.

6. 能否向编译后的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

1). 不能,当一个类已经编译好了,它就已经注册到runtime中,其对象的内存布局已经确定,无法再修改。假如能修改的话,当我已经创建了一个对象A,这时这个A的内存大小和布局已经确定,如果这时动态向类A中增加一个Int型的变量,则意味着新的对象的内存大小和布局都要改变,而这个对象A已经确定下来无法改变,也就成了废对象,这会引起严重的后果。

2). 但在运行时创建的类还没注册到runtime中,可以为其添加成员变量,因为此时的内存布局还未确定下来。

7. 在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)?

一个是类对象,一个是元类对象,所以是pair

8. 谈一谈对消息机制的理解?

1). runtime的核心就是消息机制,而runtime又是OC语言的核心。当我们正常通过OC语言调用一个方法[objc func]时,并不是直接调用其函数实现,而是向objc对象发送了一个消息。至于具体的方法实现是什么,在编译时是并不知道的,只有在运行时才能确定下来。这就是消息机制,而通过这种消息的方式,则体现了OC语言的动态属性,使用起来更加灵活。

2). 具体在调用[objc func]时,是在调用obj对象的名称为func的对象方法,对象方法是保存在类对象中的,所以obj会通过其isa指针找到其类对象,然后先查方法缓存中是否已经有此方法(具体查找方法为哈希查找)。如果没找到的话,就在类对象的对象方法列表中查找func方法(如果方法列表已经排过序就按二分法查找,如果没排序就按遍历查找,并且以先查找分类的方法,后查找宿主类的方法)。如果还没有查找到,就逐级查找父类的方法缓存和方法列表。如果找到func方法,就把其IMP缓存到obj的类对象的方法缓存中。如果到根类都没找到的话,就进入到消息转发流程。

9.转发消息的流程?

1). 动态解析环节:查看当前类有没有实现动态解析
+(BOOL)resolveInstanceMethod:(SEL)selector 或者
+(BOOL)resolveClassMethod:(SEL)selector 如果已经实现就调用。使用这个方法解决问题的前提是,相关方法的实现代码已经存在,只是要在运行时动态的插到类里面。此方法可以用来实现@dynamic(阻止编译器自动生成类方法)

2). 快速转发环节:如果没实现就进入到快速转发流程 - (id)forwardingTargetForSelector:(SEL)selector;将当前selector消息转发到其他对象上。

3). 完整转发环节:如果还没实现则进入到最终的转发流程
3.1 先调用-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector,根据selector返回一个方法签名,这个签名按规则包含了返回值类型、传参类型(最常见的“v@:”,v表示返回值为void类型,@表示参数为id类型,:表示参数为SEL)
3.2 再调用 -(void)forwardInvocation:(NSInvocation *)invocation;可以进行消息转发。如果已经重写了此方法,那么即使没有实现转发,也不会崩溃了,因此消息转发也常用来兼容crash情况。

你可能感兴趣的:(OC-runtime简述)