iOS Runtime part1:概要

一提到Runtime就是

[receiver message];-->objc_msgSend(receiver, selector);

当然一切的功能都构造相关,所以我们说说receiver(对象,NSObject)构造

1.isa and super_class

在OC中id可以指向任何一个对象实例

1.1 那么什么是id呢?

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

可以看到在typedef内已经有*,所以id任何一个对象实例都不需要加*

1.2 那么objc_object又是什么呢?

/// Represents an instance of a class.
struct objc_object {
    Class isa;
};

1.3 那么什么是Class呢?

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

1.4 那么什么是objc_class呢?

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
} OBJC2_UNAVAILABLE;

下载objc源代码,在 objc-runtime-new.h 中,我们发现 objc_class有如下定义:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;   
    ...
    ...
}

可以看出无论是类的对象实例还是都是objc_object
也就是说也是对象
所以在OC中万物皆对象

写两个类:Father+Son,Son继承自Father
回到objc_class的定义

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
} OBJC2_UNAVAILABLE;

Son的super_class指向Father
Father的super_class指向NSObject
那么
Son的isa又指向什么呢?
Father的isa又指向什么呢?
这里要引入一个新的概念叫元类.

iOS Runtime part1:概要_第1张图片
Class&MetaClass.001.jpg

Son实例对象的isa指向 Son
Son的isa指向 Son的元类
Father的isa指向 Father的元类
Son的元类的父类指向 Father的元类的父类

最奇怪的是循环链的顶层,
NSObject的superclass指向nil
NSObject的isa指向NSObject的元类
NSObject的元类的superclass指向NSObject
NSObject的元类的isa指向NSObject的元类自己

2.objc_class内部的各种list

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
} OBJC2_UNAVAILABLE;

在objc_class结构体中:

  • ivars

ivars是指向objc_ivar_list成员变量列表的指针。

  • methodLists

methodLists是指向objc_method_list指针的指针,objc_method_list就是用来存储当前类的方法链表,objc_method存储了类的某个方法的信息。

  • cache

cache是指向objc_cache结构体的指针;objc_cache 用来缓存用过的方法,提高性能。

typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;
实际指向objc_cache结构体,如下:

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

mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置
occupied: 实际占用cache buckets的总数
buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

cache和methodLists都存储着一些Method,Method的结构我们会在说消息发送的时候具体说

3. 属性与成员变量

3.1 属性说白了还是成员变量

@interface Son : Father
{
    NSInteger _age;
}

@property(nonatomic,copy)NSString * name;

-(void)logMethods;
-(void)logIvars;
-(void)logPropertys;

@end
-(void)logMethods{
    unsigned int outCount = 0;
    Method * methodList = class_copyMethodList(self.class, &outCount);
    for(int i = 0; i < outCount; i++) {
        NSLog(@"Method-%@",NSStringFromSelector(method_getName(methodList[i])));
    }
}

-(void)logIvars
{
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList(self.class, &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        const char *propertyName = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:propertyName];
        NSLog(@"Ivar-%@",key);
    }
}
-(void)logPropertys
{
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList(self.class, &outCount);
    for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        const char *propertyName = property_getName(property);
        NSString *key = [NSString stringWithUTF8String:propertyName];
        NSLog(@"Property-%@",key);
    }
}
Son * son = [Son new];
[son logPropertys];
[son logIvars];
[son logMethods];

打印:

2017-03-16 11:31:06.218 RuntimeTrain[91944:1371911] Property-name
2017-03-16 11:31:06.219 RuntimeTrain[91944:1371911] Ivar-_age
2017-03-16 11:31:06.219 RuntimeTrain[91944:1371911] Ivar-_name
2017-03-16 11:31:06.219 RuntimeTrain[91944:1371911] Method-logPropertys
2017-03-16 11:31:06.220 RuntimeTrain[91944:1371911] Method-logIvars
2017-03-16 11:31:06.220 RuntimeTrain[91944:1371911] Method-logMethods
2017-03-16 11:31:06.220 RuntimeTrain[91944:1371911] Method-.cxx_destruct
2017-03-16 11:31:06.220 RuntimeTrain[91944:1371911] Method-name
2017-03-16 11:31:06.220 RuntimeTrain[91944:1371911] Method-setName:
2017-03-16 11:31:06.221 RuntimeTrain[91944:1371911] Method-init

可以看出属性也在包含在成员变量内,只不过系统自动的加了一个set和一个get方法而已

3.2 Non Fragile ivars

那么什么是Ivar呢?

typedef struct objc_ivar *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
}   

这里我们注意第三个成员 ivar_offset。它表示基地址偏移字节。

在编译我们的类时,编译器生成了一个 ivar布局,显示了在类中从哪可以访问我们的 ivars 。看下图:


iOS Runtime part1:概要_第2张图片
Non_Fragile_ivars_1.png

上图中,左侧的数据就是地址偏移字节,我们对 ivar 的访问就可以通过 对象地址 + ivar偏移字节的方法。但是这又引发一个问题,看下图:


iOS Runtime part1:概要_第3张图片
Non_Fragile_ivars_2.png

我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。
而Objective-C Runtime中使用了Non Fragile ivars,看下图:
iOS Runtime part1:概要_第4张图片
Non_Fragile_ivars_3.png

使用Non Fragile ivars时,Runtime会进行检测来调整类中新增的ivar的偏移量。 这样我们就可以通过 对象地址 + 基类大小 + ivar偏移字节的方法来计算出ivar相应的地址,并访问到相应的ivar。

@interface Student : NSObject
{
    @private
    int age;
}
@end

@implementation Student

- (NSString *)description
{
    NSLog(@"current pointer = %p", self);
    NSLog(@"age pointer = %p", &age);
    return [NSString stringWithFormat:@"age = %d", age];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *student = [[Student alloc] init];
        Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");
        int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));
        NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));
        *age_pointer = 10;
        NSLog(@"%@", student);
    }
    return 0;
}

打印:

2014-11-08 18:24:38.892 Test[4143:466864] age ivar offset = 8
2014-11-08 18:24:38.893 Test[4143:466864] current pointer = 0x1001002d0
2014-11-08 18:24:38.893 Test[4143:466864] age pointer = 0x1001002d8
2014-11-08 18:24:38.894 Test[4143:466864] age = 10

4. 消息发送+转发流程

4.1 消息发送

  • SEL

selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

typedef struct objc_selector *SEL;

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器

  • IMP
typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法

  • Mothed
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

objc_class内的objc_method_list结构体存储着objc_method数组列表
objc_class内的cache结构体存储着使用过objc_method的数组列表

以上两个objc_method的数组列表就像字典:一个Method代表一个键值对,键就是选择器(SEL),值是一个实现(IMP),消息在发送过程中的方法查找也就是用了这样的键值对进行查找的

1.检查selector是否需要忽略。(ps: Mac开发中开启GC就会忽略retain,release方法。)
2.检查target是否为nil。如果为nil,直接cleanup,然后return。(这就是我们可以向nil发送消息的原因。)
3.然后在target的Class中根据Selector去找IMP

寻找IMP的过程:

4.先从当前class的cache方法列表(cache methodLists)里去找
5.找到了,跳到对应函数实现
6.没找到,就从class的方法列表(methodLists)里找
7.还找不到,就到super class的方法列表里找,直到找到基类(NSObject)为止
8.最后再找不到,就会进入动态方法解析和消息转发的机制

4.2 消息转发

当你给一个对象发送了一条它无法识别的消息的时候,就有了消息转发

// 第一步:实现此方法,在调用对象的某方法找不到时,会先调用此方法,允许
// 我们动态添加方法实现
+(BOOL)resolveInstanceMethod:(SEL)sel//1
{
   // 我们这里没有声明有eat方法,因此,我们可以动态添加eat方法
    if ([NSStringFromSelector(sel) isEqualToString:@"dynamicMehod"]) {
        class_addMethod(self, sel, (IMP)dynamicMehod, "v@:");//dynamicMehod是C函数
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
// 第二步:返回一个其他的对象来处理这个SEL
- (id)forwardingTargetForSelector:(SEL)aSelector//2
{
    return someObject or nil;
}
// 第三步:返回方法签名。如果返回nil,则表示无法处理消息 调用-doesNotRecognizeSelector
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector//3
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"dynamicMehod"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
// 第四步,返回了方法签名,都会进入这一步,这一步用户调用方法
// 改变调用对象等
// 当我们实现了此方法后,-doesNotRecognizeSelector:不会再被调用
// 如果要测试找不到方法,可以注释掉这一个方法
-(void)forwardInvocation:(NSInvocation *)invocation//4
{
 // 我们还可以改变方法选择器
    [anInvocation setSelector:@selector(anyMethod)];//另一个OC方法
    // 改变方法选择器后,还需要指定是哪个对象的方法
    [anInvocation invokeWithTarget:self];
}
-(void)doesNotRecognizeSelector:(SEL)aSelector
{
}

消息转发一旦奏效,表面是看是一个对象也能对一条它无法识别的消息做出反应,但万不能以为就是用respondsToSelector:去检测receivermessage的关系也会返回YES,那就大错特错了.转发是假象,respondsToSelector:是走最传统的消息发送机制来检测的.

5.category

一个类的设计不可能完美,也不会完全按照你的想象来,我们想给一个类加属性加方法,除了写一个类继承这个类,category也是很好的选择
具体请看美团的这篇文章
在part2:runtime的应用中我们会大量的使用category

文章参考:

  • 刨根问底Objective-C Runtime
  • 神经病院Objective-C Runtime
  • ios runtime浅析

你可能感兴趣的:(iOS Runtime part1:概要)