iOS底层原理计划-内存分配和初始化

当我们有一个继承 NSObjectPerson 类:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

问题一:当我们对实例对象只 allocinit ,能否直接给其成员变量进行赋值?

Person *person = [Person alloc];
person.name = @"SunSatan";

问题二:person1person2 有区别吗?

Person *person  = [Person alloc];
Person *person1 = [person init];
Person *person2 = [person init];

问题三:person1.name 是多少?

Person *person = [Person alloc];
person.name = @"SunSatan";
Person *person1 = [person init];

带着这三个问题,我们来探究一下iOS底层的内存分配和初始化。

在源码里,我给基类和 NSObject 的alloc 方法和 init 方法都加上了打印信息,以此来看内存分配和初始化调用过程。

allow

Person *person = [Person alloc];
log:
objc_alloc 
callAlloc 

从调用过程看出,[class alloc] 实际调用的是基类的 objc_alloc() , 而不是 NSObject+ (id)alloc

我们来看一下 objc_alloc() 的源码:

id objc_alloc(Class cls) { // Calls [cls alloc].
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
    if (slowpath(checkNil && !cls))  return nil;
    //没有自定义的 allocWithZone 方法,就调用 _objc_rootAllocWithZone
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);//基本执行这一步
    }
    //调用类自定义的 alloc 或 allocWithZone: 方法
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

我们经常会重写 init ,但基本不会重写 alloc ,所以都是最终都是调用了 _objc_rootAllocWithZone() 方法:

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused) {
    return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC);// objc2以后都忽略 zone
}

id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, int construct_flags = OBJECT_CONSTRUCT_NONE, bool cxxConstruct = true, size_t *outAllocatedSize = nil) {
		...
    size_t size;// 计算实例变量的内存大小
    size = cls->instanceSize(extraBytes);//extraBytes = 0
    ...
    id obj; 
    if (zone) { //zone = nil
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size); //执行这一步,分配内存
    }
    ...
    if (!zone && fast) {  //fast 指是不是优化过的 isa,现在基本都是优化的 isa 了
        obj->initInstanceIsa(cls, hasCxxDtor); //所以执行这一步,初始化实例的isa
    } else {
        obj->initIsa(cls);
    }
    if (fastpath(!hasCxxCtor)) { // 一般 hasCxxCtor = false
        return obj;   //然后就返回
    }
		...
}

可以看到 _class_createInstanceFromZone() 为实例对象计算了内存大小,接着分配内存,初始化实力对象的 isa ,最后返回一个完整的实例对象。

所以我们现在知道问题一的答案了,多数情况下是可以的,除了抽象工厂模式创建的类簇对象,需要初始化后才能确定具体的类型。

instanceSize

我们看一下是如何为实例对象计算内存大小的:

size_t instanceSize(size_t extraBytes) const {
    // objc_class 缓存了 Instance Size
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
    //我调试的时候,下面一次都没执行过,不知道什么情况下会执行
    size_t size = alignedInstanceSize() + extraBytes;
    if (size < 16) size = 16;//至少也会分配16字节
    return size;
}

通常,类对象已经缓存了实例对象的内存大小,直接取出来,然后进行了16字节对齐:

size_t align16(size_t x) { //16字节对齐算法
    return (x + size_t(15)) & ~size_t(15);
}

size_t fastInstanceSize(size_t extra) const {
    size_t size = _flags & FAST_CACHE_ALLOC_MASK;// 取出缓存在 _flags 中的内存大小
    // 移除在 setFastInstanceSize 里加的 FAST_CACHE_ALLOC_DELTA16 = 8
    return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);// extra等于0,不应管
}

在类对象没缓存的情况下,取出未对齐的内存大小,进行8字节对齐:

#define WORD_MASK 7UL
uint32_t word_align(uint32_t x) { //8字节对齐算法
    return (x + WORD_MASK) & ~WORD_MASK;
}

uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());//取未对齐的内存大小,进行字节对齐
}

我们可以来验证一下,在 _class_createInstanceFromZone() 里把计算好的 size 打印出来:

if (strcmp(cls->demangledName(), "Person") == 0) {
    printf("- %s final instanceSize:%zu \n", "Person", size);
}

首先继续使用之前的 Person 类,删除原有的 name 属性,然后创建一个 Person 的实例对象,运行后打印如下:

- Person final instanceSize:16 

最少会分配16字节的内存给实例对象,哪怕实例对象只使用了8字节内存。

接着给 Person 类添加 nameage 属性:

@interface Person : NSObject
//isa  8字节
@property (nonatomic, copy) NSString *name; //8字节
@property (nonatomic, assign) int age;      //1字节
@end

此时 Person 使用的内存大小为17字节小于24字节,如果使用8字节对齐则会分配24字节,如果使用16字节对齐则会分配32字节。

运行后打印如下:

- Person final instanceSize:32 

说明底层确实使用的是16字节对齐。

allocWithZone

Person *person = [Person allocWithZone:nil];
log:
objc_allocWithZone 
callAlloc

zonenil 时,[class allocWithZone:nil] 实际调用的也是基类的 objc_allocWithZone() ,最终调用 callAlloc()

id objc_allocWithZone(Class cls) { // Calls [cls allocWithZone:nil].
    return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}

zone 不为 nil 时,就会调用NSObject+ (id)allocWithZone: 方法

Person *person = [Person allocWithZone:NSDefaultMallocZone()];
log:
+ (id)allocWithZone 

+ (id)allocWithZone: 直接越过 callAlloc(),调用 _objc_rootAllocWithZone() ,把 zone 传进去:

+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

但我们通过之前的分析,知道 _objc_rootAllocWithZone() 是忽略了 zone 的,所以 allocallocWithZone 本质是一样的。

init

person = [person init];
log:
- (id)init
_objc_rootInit

我们来看看 - (id)init 方法和 _objc_rootInit() 的源码:

+ (id)init {
    return (id)self;
}

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

id _objc_rootInit(id obj) {
    return obj;
}

其实 init 就直接把调用者返回来了,什么都没做。因为 init 使用了工厂方法,让你为自己的类进行初始化的工作。

因此问题二和问题三的答案就显而易见了。

new

Person *person = [Person new];
log:
objc_opt_new 
callAlloc 
- (id)init 
_objc_rootInit

[NSObject new] 实际执行的也是基类的实现 objc_opt_new(),而不是 + (id)new

id objc_opt_new(Class cls) {// Calls [cls new]
    if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
        return [callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/) init];
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}

new 或者 objc_opt_new() 就是把 allowinit 合成为了一步,并没有做其他的事。

因此,如果我们不需要使用特殊的 init 来创建,我们用 new 来创建实例对象,代码更少更简洁。

疑问

通过研究,我反而产生了一个疑问:+ (id)alloc 方法一直都没有调用,到底有什么用呢?

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id _objc_rootAlloc(Class cls) {
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

根据我的理解和测试,应该是为了给我们重写的 + (id)alloc 里面调用 [super alloc] 的。

注1:本文中出现的源码,我已经把其中无关的预编译、断言、代码段等删除了。

注2:源码版本为objc4-779.1,一个可以调试的源码的下载地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值