OC 对象原理

244 阅读5分钟

本文相关 objc 源码可以到 苹果开源官网 获取最新源码,但是官网的源码需要经过一连串设置才可进行编译 debug,所以也可以到 github 查找可以直接编译的源码库,比如 这里 下载的 objc 源码工程可以直接编译 debug。

本文相关源码基于 objc4-781.2 版本。

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

/// 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);
    }

    void setInfo(uint32_t set) {
        assert(isFuture()  ||  isRealized());
        data()->setFlags(set);
    }
//...
}

源码分析可知,最简单的 NSObject 类只有一个指向 Class 的 isa 指针,Class 则是指向 objc_object 结构体的指针。

NSObject 类本质上是 objc_object 结构体。

实例的方法并没有存储在实例的结构体中,这也是出于内存使用的考虑,因为如果每个实例都保存了自己能执行的方法,那会占用很多内存。

当实例方法被调用时,实例会通过 isa 指针找到相应的类,在类的 class_data_bits_t bits 结构体中查找对应的实例方法。同时,每一个 objc_class 也有一个指向自己父类的 superclass 用来查找继承的实例方法。

如果调用的是类方法,则是通过类的 isa 指针查找对应的元类(类的 isa 指针指向的是它的元类),到元类中调用类方法。

  • cache - 用来存储最近调用过的方法。对象调用方法之后,这个方法是会被缓存起来的。下次再调用这个方法的时候,直接从缓存里面去找,而不用再去遍历从类到父类再到祖宗类的方法列表了。
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}
  • bits - 上述源码中我们知道 bits & FAST_DATA_MASK 位运算之后,可以得到class_rw_t,而 class_rw_t 中存储着方法列表、属性列表以及协议列表。
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;
};

上述源码中,method_array_t、property_array_t、protocol_array_t其实都是二维数组。

struct cache_t {
//...
	public:
    static bucket_t *emptyBuckets();
    
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();

//...
}

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
//...

}

buckets: 数组,bucket_t 结构体的数组,bucket_t 是用来存放方法的 SEL 内存地址和 IMP 的。

occupied:当前已缓存的方法数。

一个最简单的实例对象,只包含一个 isa 指针,即一个对象至少需要真正占用内存大小是8个字节。

@interface Person : NSObject

@end

@implementation Person

@end

内存对齐规则

对齐系数:

每个特定的平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。我们可以通过预编译命令#pragma pack(n),n=1、2、4、8、16 来改变这一系数,其中的n就是要指定的“对齐系数”。我们iOS编译器Xcode的对齐系数就是8。

对齐规则:

1、数据成员对齐规则:(Struct或者Union的数据成员)第一个数据成员放在偏移为0的位置。以后每个数据成员的位置为min(对齐系数,自身长度)的整数倍,下个位置不为本数据成员的整数倍位置的自动补齐。

2、数据成员为结构体:该数据成员的内最大长度的整数倍的位置开始存储。

3、整体对齐规则:数据成员按照1,2步骤对齐之后,其自身也要对齐,对齐原则是min(对齐系数,数据成员最大长度)的整数倍。

结构体内存分析:

1、不用变量的内存分析

struct Struct1 {
    double a;8
    int b;4
    char c;1
    short d;2
}myStruct1;

struct Struct2 {
    int a;4
    double b;8
    int c;4
    char d;1
}myStruct2;

NSLog(@"myStruct1 - %lu",sizeof(myStruct1)); 16字节
NSLog(@"myStruct2 - %lu",sizeof(myStruct2)); 24字节

2、相同变量的内存分析

struct Struct1 {
    double a;
    int b;
    char c;
    short d;
}myStruct1;

struct Struct2 {
    int a;
    double b;
    char d;
    short e;
}myStruct2;

NSLog(@"myStruct1 - %lu",sizeof(myStruct1)); 16字节
NSLog(@"myStruct2 - %lu",sizeof(myStruct2));24字节

3、对象字节对齐

@interface XDPerson : NSObject
@property (nonatomic, copy) NSString *name;//8
@property (nonatomic, assign) int age;//4
@property (nonatomic, assign) long height;//8
@property (nonatomic, copy) NSString *sex;//8
@property (nonatomic) char ch1;//1
@property (nonatomic) char ch2;//1

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
   
    XDPerson *p1 = [XDPerson alloc];
    p1.name = @"xiedong";
    p1.age = 18;
    p1.height = 180;
    p1.sex = @"男";
    p1.ch1 = 'a';
    p1.ch2 = 'b';
    
   NSLog(@"%lu - %lu",class_getInstanceSize([p1 class]),malloc_size((__bridge const void *)(p1)));
}

输出结果 40 - 48。

规则:

1、对象申请的内存空间 <= 系统开辟的内存空间。

2、对象申请的内存空间是以8字节对齐方式。在objc源码里面是可以得到验证的。

3、系统开辟内存空间是以16字节对齐方式。在malloc源码里面segregated_size_to_fit()可以看到是以16字节对齐的。

内存对齐的原因:

1、内存对齐是编译器处理的。

2、CPU读取未对齐的内存时,其性能会大大的降低,此时CPU会进入到异常状态,并且通知程序不能继续进行。

3、CPU并不是以字节为单位来存取数据的,它会把内存当成一块一块的,其块的大小可以是2、4、8、16、32字节,每次读取都是一个固定的开销,减少内存存取次数提升应用程序的性能。

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

init 源码实现

init 直接返回自身,这么设计的原因是:上层可以重写 init 方法,做一些自定义的初始化逻辑。

- (id)init {
    return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}