博客> 初探runtime 类以及类的继承
初探runtime 类以及类的继承
1小时前 评论:0 阅读:1210 huangqingyi
ios runtime

这些知识老生常谈了,我也写过相关博客。可是感觉还是不咋地。今天从头来过,弄个专题分模块专门仔细研究下。

NSObject定义

objc2.0 以前

typedef struct objc_class *Class;

@interface NSObject { Class isa OBJC_ISA_AVAILABILITY; }

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 ,方法列表methodLists,缓存cache和协议protocols。

objc 2.0 以后

objc 2.0 以后,数据结构发生了变化

@interface NSObject { Class isa OBJC_ISA_AVAILABILITY; } 从上面我们 看出来,NSObject 只有一个成员变量就是 isa ,类型是Class ,接下来我们看看class 是什么。

typedef struct objc_class *Class; 从上面我们看出来了 Class 是struct objc_class结构体指针 ,那接下来我们看看这个结构体是什么

struct objc_class : objc_object {

    Class superclass;

    cache_t cache;             // formerly cache pointer and vtable

    class_data_bits_t bits; 

}

objc_class 是继承objc_object 对象。接下来看objc_object 的结构体(在文件objc_private.h中)

struct objc_object {

private:

isa_t isa;

}

这里又出现了一个union isa_t 。接下来我们看看这个isa_t 是什么

union isa_t { Class cls; uintptr_t bits;

# if __arm64__

#define ISA_MASK 0x0000000ffffffff8ULL

#define ISA_MAGIC_MASK 0x000003f000000001ULL

#define ISA_MAGIC_VALUE 0x000001a000000001ULL

struct {

        uintptr_t indexed           : 1;

        uintptr_t has_assoc         : 1;

        uintptr_t has_cxx_dtor      : 1;

        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000

        uintptr_t magic             : 6;

        uintptr_t weakly_referenced : 1;

        uintptr_t deallocating      : 1;

        uintptr_t has_sidetable_rc  : 1;

        uintptr_t extra_rc          : 19;

#define RC_ONE (1ULL<<45 xss=removed xss=removed xss=removed xss=removed xss=removed>isa);

}

  • (void)viewDidLoad {

    [super viewDidLoad];
    
    [self printClassInfo:"YoungMan"];
    
    [self printClassInfo:"Man"];
    
    [self printClassInfo:"Persion"];
    
    [self printClassInfo:"NSObject"];

}

测试结果

2018-04-13 16:01:10.553250+0800 Runtime-class[2551:9731051] className YoungMan objectClass 0x10074a040 superClass 0x100749fa0 metaClass 0x10074a018 superMetaClass 0x100749f78 metaClass isa 0x101701e58 2018-04-13 16:01:10.553455+0800 Runtime-class[2551:9731051] className Man objectClass 0x100749fa0 superClass 0x100749f00 metaClass 0x100749f78 superMetaClass 0x100749ed8 metaClass isa 0x101701e58 2018-04-13 16:01:10.553594+0800 Runtime-class[2551:9731051] className Persion objectClass 0x100749f00 superClass 0x101701ea8 metaClass 0x100749ed8 superMetaClass 0x101701e58 metaClass isa 0x101701e58 2018-04-13 16:01:10.553723+0800 Runtime-class[2551:9731051] className NSObject objectClass 0x101701ea8 superClass 0x0 metaClass 0x101701e58 superMetaClass 0x101701ea8 metaClass isa 0x101701e58

测试结果绘制成图如下图

 Enter your image description here:

总结 1 实例对象的superclass 指向父类,最后指向rootClass(NSObject),RootClass 的父类是nil

实例对象的isa,指向元类 元类对象的superclass 指向元类父类最后指向RootClass的元类,而RootClass 的元类的父类执行RootClass 4.元类对象的isa 都是指向根元类,包括根源类自己。

结构体isa_t

我们从依赖和继承关系来分析,isa_t 是最底层的。那么我们接下来看isa_t

isa_t 是个union类型结构体 ,因此cls bits 或者定义的结构体arm 或者x86等共用这一块内存。因为这些指针 、结构体或者bits 都是64 位的。因此isa_t 也是64位。

两种结构体见图

 Enter your image description here:

isa_t 这里我们重点讨论 这个x86 结构体

struct {

        uintptr_t indexed           : 1;

        uintptr_t has_assoc         : 1;

        uintptr_t has_cxx_dtor      : 1;

        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000

        uintptr_t magic             : 6;

        uintptr_t weakly_referenced : 1;

        uintptr_t deallocating      : 1;

        uintptr_t has_sidetable_rc  : 1;

        uintptr_t extra_rc          : 8;

#define RC_ONE (1ULL<<56 xss=removed xss=removed xss=removed xss=removed>> 3;

}

}

从初始化我们能看出来,我们传入的参数 indexed hasCxxDotor 都是 false 因此我们进入的流程是 判断语句的else

isa.bits = ISA_MAGIC_VALUE;

    // isa.magic is part of ISA_MAGIC_VALUE

    // isa.indexed is part of ISA_MAGIC_VALUE

    isa.has_cxx_dtor = hasCxxDtor;

    isa.shiftcls = (uintptr_t)cls >> 3;

这里有个ISA_MAGIC_VALUE 宏定义

#define ISA_MAGIC_VALUE 0x001d800000000001ULL 给bits 赋值,其实就是给_x86 结构体赋值,因此我们需要弄懂这个结构体给x86 的那些地方赋值了。 这里先把ISA_MAGIC_VALUE 转换成二进制

0 0 1 d 8 0 0 0 0 0 0 0 0 0 0 1 0000 0000 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001

然后和结构体对号入座,如下图

 Enter your image description here:

x86结构 从这张图中我们能看出来,我们其实初始化的位置是indexed 和margic 位置。

在结构体 x86 中 的首位 indexed 表示 isa_t 的类型

0 表示 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。 1 表示当前 isa 不是指针,但是其中也有 cls 的信息,只是其中关于类的指针都是保存在 shiftcls 中。 margic 的值是 111011 = 0x3b; 用于调试器判断当前对象是真的对象还是没有初始化的空间。

我们再看看has_cxx_dtor,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。

我们再来看 shiftcls ,给其赋值

isa.shiftcls = (uintptr_t)cls >> 3; 从这里看出来,shiftcls 其实是保存类的指针地址。

不过这里是先将类向右移动三位在赋值给了shiftcls。

这是因为将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清楚减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。 绝大多数机器的架构都是 byte-addressable 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址。 ObjC 中的类指针的地址后三位也为 0 对象指针的后四位都是0 使用整个指针大小的内存来存储 isa 指针有些浪费,尤其在 64 位的 CPU 上。在 ARM64 运行的 iOS 只使用了 33 位作为指针(与结构体中的 33 位无关,Mac OS 上为 47 位),而剩下的 31 位用于其它目的。类的指针也同样根据字节对齐了,每一个类指针的地址都能够被 8 整除,也就是使最后 3 bits 为 0,为 isa 留下 34 位用于性能的优化。

Using an entire pointer-sized piece of memory for the isa pointer is a bit wasteful, especially on 64-bit CPUs which don't use all 64 bits of a pointer. ARM64 running iOS currently uses only 33 bits of a pointer, leaving 31 bits for other purposes. Class pointers are also aligned, meaning that a class pointer is guaranteed to be divisible by 8, which frees up another three bits, leaving 34 bits of the isa available for other uses. Apple's ARM64 runtime takes advantage of this for some great performance improvements. from ARM64 and You

做个实验验证下。 测试代码

NSString *binaryWithInteger(NSUInteger decInt){

    NSString * string =@"";

    NSUInteger dec = decInt;

    while (dec>0) {

        string = [[NSString stringWithFormat:@"%lu",dec&1] stringByAppendingString:string];

        dec =dec>>1;

    }

    while(string.length!=64) {

        string = [@"0" stringByAppendingString:string];

    }

    return string;

}

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        struct objc_object * obj = (__bridge  struct objc_object *)[NSObject new];

        NSLog(@"%@",binaryWithInteger(obj->isa));

        NSLog(@"%@", binaryWithInteger((uintptr_t)[NSObject class]));

    }

    return 0;

}

测试结果是

2018-04-13 16:49:05.683009+0800 测试Shifts 指针[3532:9769304] 0000000001011101111111111111111110010111100101100000000101000001 2018-04-13 16:49:05.684092+0800 测试Shifts 指针[3532:9769304] 0000000000000000011111111111111110010111100101100000000101000000 我们将所有的打印指针都补全到64 位 对象指针: 0000000001011101111111111111111110010111100101100000000101000001

类指针: 0000000000000000011111111111111110010111100101100000000101000000

对比下,指针完全相同。

接下来我们看其他的bits

has_assoc:对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存 weakly_referenced:对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放

deallocating:对象正在释放内存 has_sidetable_rc:对象的引用计数太大了,存不下 extra_rc:对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc 的值就为 9

这里我们使用结构体取代了原有的isa指针,因此,我们提供了一个方法返回原来类指针

#define ISA_MASK 0x00007ffffffffff8ULL

inline Class

objc_object::ISA()

{

    return (Class)(isa.bits & ISA_MASK);

}

这里有个宏定义 ISA_MASK 这个mask 就是获取指shiftcls位置的数据用的。

cache_t

我们看完类的结构,接下来我们看objc_object中的cache_t

结构

struct cache_t {

   struct bucket_t *_buckets;

   mask_t _mask;

   mask_t _occupied;

}

typedef uint32_t mask_t;

struct bucket_t {

    private:

   cache_key_t _key;

   IMP _imp;

}

typedef uintptr_t cache_key_t;

UML 图

 Enter your image description here:

cache_t 根据源码,我们知道 cache_t 持有 bucket_t 的指针 mask:分配用来缓存bucket的总数。 occupied:表明目前实际占用的缓存bucket的个数。

bucket_t的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。 (这里Cache_key_t _key 为什么是unsinged long,我们知道地址的长度也是unsinged long 其实就是把指针转换成了个数字)

cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。

Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

class_data_bits_t bits

这里好好分析下这个 class_data_bits_t bits 结构体里对class_data_bits_t bits 定义的部分的参数和相关方法是

struct objc_class : objc_object{

 class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

   class_rw_t *data() { 

        return bits.data();

    }

  void setData(class_rw_t *newData) {

        bits.setData(newData);

    }

..... 下面的方法都是针对bits }

好多方法都是针对这个bits 的那么我们就应该看看这个bits 是什么了。 set 方法

void setData(class_rw_t *newData) {

        bits.setData(newData);

}

这里我们看出来了。这里其实是传入了一个 class_rw_t 类型的指针 bits 调用了setData 方法。bits8 是struct class_data_bits_t 调用该结构体的setData方法。

struct class_data_bits_t{

 uintptr_t bits;

public://标记public 公共方法

  class_rw_t* data() {

        return (class_rw_t *)(bits & FAST_DATA_MASK);

    }

void setData(class_rw_t *newData)

    {

        assert(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));

        // Set during realization or construction only. No locking needed.

        bits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;

    }

}

在get 和 set data 方法中 ,有这个宏定义

#define FAST_DATA_MASK 0x00007ffffffffff8UL 这个我们能看出来,0x00000(44 个1 )000。指针是最多44 位 的。(因为x86 是44 位,其他的是33位,满足最长要求嘛) 接下来分析setData方法

1 第一步判断有没有data , 并且将第flag 的19 位 和 30 位设置值

就是给bits 位 赋值,其实就是记录class_rw_t 指针。 class_rw_t 结构体

class_rw_t 这里method_array_t property_array_t protocol_array_t 可以理解为一个数组吧。

因此这里把class 图完全绘制出来

 Enter your image description here:

0bjc2 完整图 上面这张图是objc1.0 和2.0 的对比图

我们把objc1.0的中的元素到objc 2.0中找对应关系

 Enter your image description here:

对应关系 这里怎么找对应关系呢? 就拿name 来说吧

找到 objc_class 类,从中找获取name 方法。

const char *mangledName() {

        // fixme can't assert locks here

        assert(this);

        if (isRealized()  ||  isFuture()) {

            return data()->ro->name;

        } else {

            return ((const class_ro_t *)data())->name;

        }

    }

data()函数就是找到class_rw_t 指针.接着找到该指针的ro 指针,ro 是class_ro_t 结构体,从这个结构体中获取name,如图。其他一样的方法。 图中没有标记objc info 对应0bjc2 中的位置,其实就是class_rw_t 指针的flags。

看大神博客,入院考试这里我还是忒下吧。为了以后看方便

入院考试

(一)[self class] 与 [super class]

下面代码输出什么?

@implementation Son : Father

  • (id)init

{

    self = [super init];

    if (self)

    {

        NSLog(@"%@", NSStringFromClass([self class]));

        NSLog(@"%@", NSStringFromClass([super class]));

    }

return self;

}

@end

self和super的区别:

self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。

super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。

在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend

OBJC_EXPORT void objc_msgSendSuper(void / struct objc_super super, SEL op, ... */ )

/// Specifies the superclass of an instance.

struct objc_super { /// Specifies an instance of a class. __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 

#if !defined(__cplusplus) && !__OBJC2__

    /* For compatibility with old objc-runtime.h header */

    __unsafe_unretained Class class;

#else

    __unsafe_unretained Class super_class;

#endif

    /* super_class is the first class to search */

};

在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class。

入院考试第一题错误的原因就在这里,误认为[super class]是调用的[super_class class]。

objc_msgSendSuper的工作原理应该是这样的: 从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用父类的这个selector。注意,最后的调用者是objc->receiver,而不是super_class! 那么objc_msgSendSuper最后就转变成 // 注意这里是从父类开始msgSend,而不是从本类开始,谢谢@Josscii 和他同事共同指点出此处描述的不妥。

objc_msgSend(objc_super->receiver, @selector(class))

/// Specifies an instance of a class. 这是类的一个实例 __unsafe_unretained id receiver;

// 由于是实例调用,所以是减号方法

  • (Class)class {

    return object_getClass(self);

}

由于找到了父类NSObject里面的class方法的IMP,又因为传入的入参objc_super->receiver = self。self就是son,调用class,所以父类的方法class执行IMP之后,输出还是son,最后输出两个都一样,都是输出son。

(二)isKindOfClass 与 isMemberOfClass

下面代码输出什么?

@interface Sark : NSObject

@end

@implementation Sark

@end

int main(int argc, const char * argv[]) {

@autoreleasepool {

    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]];

   NSLog(@"%d %d %d %d", res1, res2, res3, res4);

}

return 0;

}

这里需要对着几个函数搞清楚

  • (Class)class {

    return self;

}

  • (Class)class {

    return object_getClass(self);

}

Class object_getClass(id obj)

{

    if (obj) return obj->getIsa();

    else return Nil;

}

inline Class

objc_object::getIsa()

{

    if (isTaggedPointer()) {

        uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;

        return objc_tag_classes[slot];

}

    return ISA();

}

inline Class

objc_object::ISA()

{

    assert(!isTaggedPointer()); 

    return (Class)(isa.bits & ISA_MASK);

}

  • (BOOL)isKindOfClass:(Class)cls {

    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
    
        if (tcls == cls) return YES;
    
    }
    
    return NO;

}

  • (BOOL)isKindOfClass:(Class)cls {

    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
    
        if (tcls == cls) return YES;
    
    }
    
    return NO;

}

  • (BOOL)isMemberOfClass:(Class)cls {

    return object_getClass((id)self) == cls;

}

  • (BOOL)isMemberOfClass:(Class)cls {

    return [self class] == cls;

} (BOOL)isKindOfClass:(Class)cls方法内部,会先去获得object_getClass的类,而object_getClass的源码实现是去调用当前类的obj->getIsa(),最后在ISA()方法中获得meta class的指针。 接着在isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于super class,不等再继续取super class,如此循环下去。

同理,[Sark class]执行完之后调用isKindOfClass,第一次for循环,Sark的Meta Class与[Sark class]不等,第二次for循环,Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等。第三次for循环,NSObject Meta Class的super class指向的是NSObject Class,和 Sark Class 不相等。第四次循环,NSObject Class 的super class 指向 nil, 和 Sark Class不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO。

如果把这里的Sark改成它的实例对象,[sark isKindOfClass:[Sark class],那么此时就应该输出YES了。因为在isKindOfClass函数中,判断sark的isa指向是否是自己的类Sark,第一次for循环就能输出YES了。

isMemberOfClass的源码实现是拿到自己的isa指针和自己比较,是否相等。 第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都输出NO。

+ (BOOL)isKindOfClass:(Class)cls 寻找元类,以及元类的父类进行比较,这里注意,元类的最后父类是Root (NSObject ) root 指向nil + (Class)class 返回的是self, 是对象自己。 - (BOOL)isKindOfClass:(Class)cls 比较的是class 不是元类 + (BOOL)isMemberOfClass:(Class)cls, 直接比较

(三)Class与内存地址

下面的代码会?Compile Error / Runtime Crash / NSLog…?

这个有点难呀,感觉回到了高中考试一样。

@interface Sark : NSObject

@property (nonatomic, copy) NSString *name;

  • (void)speak;

@end

@implementation Sark

  • (void)speak {

    NSLog(@"my name's %@", self.name);

} @end

@implementation ViewController

  • (void)viewDidLoad {

    [super viewDidLoad];
    
    id cls = [Sark class];
    
    void *obj = &cls;
    
    [(__bridge id)obj speak];

} @end 这道题有两个难点。难点一,obj调用speak方法,到底会不会崩溃。难点二,如果speak方法不崩溃,应该输出什么?

首先需要谈谈隐藏参数self和_cmd的问题。 当[receiver message]调用方法时,系统会在运行时偷偷地动态传入两个隐藏参数self和_cmd,之所以称它们为隐藏参数,是因为在源代码中没有声明和定义这两个参数。self在上面已经讲解明白了,接下来就来说说_cmd。_cmd表示当前调用方法,其实它就是一个方法选择器SEL。

难点一,能不能调用speak方法?

id cls = [Sark class];

void *obj = &cls;

答案是可以的。obj被转换成了一个指向Sark Class的指针,然后使用id转换成了objc_object类型。obj现在已经是一个Sark类型的实例对象了。当然接下来可以调用speak的方法。

调用 这个方法,发生了什么 第一步

id cls = [Sark class];

见图  Enter your image description here:

在栈上生成cls 而 Dark Class 在堆上

第二步

void *obj = &cls;

见图  Enter your image description here:

这张图 这里obj 指向了cls 栈上的地址。

难点二,如果能调用speak,会输出什么呢? 很多人可能会认为会输出sark相关的信息。这样答案就错误了。 正确的答案会输出

my name is 内存地址每次运行都不同,但是前面一定是ViewController。why?

正常我们想要调用speak 方法流程是这样的

Dark * dark = [[Dark alloc]init];

[dark speak];

看内存变化 第一步

Dark * dark = [[Dark alloc]init];

 Enter your image description here:

我们会在堆区生成一个Dark对象,我们在栈上有dark 对象的引用

当我们调用 [dark speak];的时候,我们从栈上的dark 中取出地址,找到堆上的 Dark Object dark 对象有方法表引用,在从方法表中找到speak 方法。(变量都是在堆上保存的)

而题目中上述例子,

[(__bridge id)obj speak];

流程应该和dark 生成对象一样。 我们找到 obj内存中的id cls(是在栈上的)。 这里还有两个知识点

实质:Objc中的对象是一个指向ClassObject地址的变量,即 id obj = &ClassObject , 而对象的实例变量 void *ivar = &obj + offset(N) oc 方法调用的时候,我们都知道有两个默认参数,self ,_cmd. 为什么是打印的是ViewController 呢?看大神博客吧。讲的太详细了。(我就是把我刚看一脸懵逼的地方给重新梳理了下)

简书链接:https://www.jianshu.com/p/4d1f236b9d64

收藏
1
sina weixin mail 回到顶部