iOS的OC源码分析之类的结构分析

前言

想成为一名优秀的iOS开发者,对底层的原理学习是必不可少的,笔者整理了一系列有关OC的底层文章,但愿能够帮助到你。这篇文章主要讲解的是类的底层结构分析编程

1.iOS的OC对象建立的alloc原理数组

2.iOS的OC对象的内存对齐缓存

3.iOS的OC的isa的底层原理bash

开始介绍类的结构以前,请问一下,你在开发的过程当中有没有想过一个问题。就是建立多个相同类型的对象的时候,那么这个对象的类是否是多个呢?带着这个问题,有了以下不一样形式获取到类的代码markdown

Class class1 = [TestJason class];
Class class2 = [TestJason alloc].class;
Class class3 = object_getClass([TestJason alloc]);
NSLog(@"%p====%p===%p",class1,class2,class3);

===========运行的结果===========
LGTest[1541:33345] 0x100002848====0x100002848===0x100002848
复制代码

从上面的运行结果能够知道,类在内存里面只存在一份less

1.类结构初探

仍是使用苹果的objc4-756.2源码来学习的,具体能够看iOS的OC对象建立的alloc原理这篇文章有介绍。经过Class的源码post

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

struct objc_class : objc_object {
//     Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    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);
    }
    ....
    
    
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
复制代码

从源码能够知道Class是一个objc_class的结构体,而objc_class是继承自objc_object的,由于在面向对象编程中万物皆对象,从这里也能够知道,其实类也是对象的。从中能够知道类里面分别有从父类继承的isasuperclass,cache_tclass_data_bits_t类型的bits。由于在底层中最终是编译成结构体的形式,因此你是否是很好奇大部分的基类NSObject在底层是怎样的形式呢?学习

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
复制代码

经过源码能够知道,NSObject的底层是与objc_object是同样的。在平常开发中,定义类的时候都是会有属性,变量和方法的,你是否是很好奇属性,成员变量和方法在类的底层中储存在哪里?接下来,我都会一一介绍。ui

2.class_data_bits_t存放属性和实例方法的地方

为了方便介绍接下来的内容定义了一个TestObject的类,下面是这个类的定义和实现,而且经过lldb的指令来介绍。this

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TestObject : NSObject{
    NSString *nickName;
}
@property(nonatomic,copy) NSString *name;

-(void)sayName;
+(void)sayNickName;

@end

NS_ASSUME_NONNULL_END

//实现的代码
TestObject *testObject = [TestObject alloc];
Class tClass = object_getClass(testObject);
NSLog(@"%@===%p",testObject,tClass);
复制代码

经过lldb的指令获得以下:

经过上面的结果能够知道 0x001d800100001659isa的内存值, 0x0000000100b37140superclass的, 0x00000001003da290cache_t的,那么 0x0000000000000000就是 bits了吗?这几个值从字面意思能够知道 superclass是存放父类的, cache_t是存放一些缓存的东西(这块内存后续会出一篇文章介绍),那么 bits应该就是存放咱们须要的属性和实例方法的了。下面是 class_rw_t的源码

class_rw_t *data() { 
    return bits.data();
}
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
复制代码

从上面的lldb中打印出来的0x0000000000000000是0的这样的话就是否是说直接打印不出来呢?并非的,咱们能够经过分析类里面的各个属性占的字节大小而后经过内存偏移来找到最终bits的内存值。

2.1.objc_class的各个指向值的大小

经过源码能够知道objc_class的内部定义

struct objc_class : objc_object {
//     Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ···
}
    
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
···
 }
 
 typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

复制代码

经过前面的文章能够知道,isa是一个联合体(union)占8个字节,superclass由于是一个类因此也占8个字节,由于cache是一个结构体类型(struct),这个大小是根据它里面的属性的容量来决定大小的,由于_buckets是一个结构体指针占8字节,mask_t经过源码能够知道占4字节,因此cache共占16字节。由isacache一共是32字节,转为16进制就是0x20,由上图能够知道,tClass类的内存值的起始位置是0x100001680经过内存偏移0x20能够获得0x1000016a0,那么这个值就是bits的内存值。也能够直接用x/5gx tClass来打印出来,获得的内存值也是0x1000016a0

由于 bits里面的 data()能够获得 class_rw_t,而 class_rw_t能够获得属性的值,因此经过 lldb的指令能够打印 class_rw_t里面所指向的值。如下是内存偏移到 0x1000016a0打印出来的结果

因此属性是放在 class_rw_tproperties里面,能够经过 properties里面的 list获得 property_list_t的数组,最终打印了属性 name

可是再用 $7这个数组来找的时候,发现是找不到 TestObject类的 nickName这个成员变量的,这时候能够看一下 class_rw_t里面的 const class_ro_t *ro

2.2 class_ro_t

经过源码能够知道class_ro_t的各个值

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};
复制代码

这个ro里面也有method_list_t,protocol_list_tproperty_list_t,其中多了ivar_list_t。经过lldb的指令最终找到了成员变量nickName

从中能够知道类 TestObject中有成员变量的数量为2个,其中第一个是 nickName,这时候就有点奇怪了,咱们不是只在 TestObject中只定义了一个 nickName这个成员变量吗?多出来的一个是什么呢?原来在类中定义的属性也是会被定义为带下划线的成员变量的,经过 lldb能够看到

而且在rw中的method_array_tro中的method_list_t均可以经过获得TestObject的方法,以下是经过lldb的指令来获取rw中的方法

从中能够知道类TestObject有方法数为4个,而且第一个的方法是cxx_destruct,另外的两个方法会不会是咱们在类中定义的实例方法sayName和类方法sayNickName,那么仍是多出来了一个方法数量,经过lldb打印出来的

其实就是定义的属性name的getter和setter方法的两个方法和sayName的实例方法以及一个系统的cxx_destruct方法,并无sayNickName这个类方法。这是为何呢?

注意:若是实例方法只是在类的`.h`文件声明了,可是并无在`.m`文件中实现的话,是不在`rw`和`ro`里面的

思考一个问题:为何成员变量会存在class_ro_t里面不存在class_rw_t里面呢?class_rw_tclass_ro_t有什么不同呢?

2.3 类方法

经过上面的能够知道类方法是不存在类里面的,是存在元类里面的,能够经过lldb指令来查找,先找到当前的类的isa,再经过isa&ISA_MASK能够获得元类

从中能够知道 0x0000000100002700就是 元类的内存值,再经过 x/5gx能够获得 元类中的 class_data_bits_t,此时就至关于走一遍上面介绍过的查找 bits里面的方法的流程

最终再元类中的 bits的方法中能够查找到 sayNickName这个类方法。

3.最后

从上面的介绍能够知道类在底层中的结构分别是isa,superclass,cache_tbits,其中它们所占的大小是不同的。bits里面的rw有保存着实例方法属性,可是并无包含成员变量rw中包含着roro中保存着实例方法属性成员变量成员变量属性都存在ivar里面,而且属性是如下划线_的形式存在的。类的类方法是存在元类的bits的方法里面。至此,有关类的结构分析就介绍完毕了。

相关文章
相关标签/搜索