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

1,355 阅读7分钟

前言

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

1.iOS的OC对象创建的alloc原理

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

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

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

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

从上面的运行结果可以知道,类在内存里面只存在一份

1.类结构初探

还是使用苹果的objc4-756.2源码来学习的,具体可以看iOS的OC对象创建的alloc原理这篇文章有介绍。通过Class的源码

/// 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是一样的。在日常开发中,定义类的时候都是会有属性,变量和方法的,你是不是很好奇属性,成员变量和方法在类的底层中储存在哪里?接下来,我都会一一介绍。

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

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

#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的方法里面。至此,有关类的结构分析就介绍完毕了。