本文相关 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];
}