iOS底层之alloc、init探究

我们先从下面案例看看alloc和init分别做了什么事?

    BKPerson *p1 = [BKPerson alloc];
    BKPerson *p2 = [p1 init];
    BKPerson *p3 = [p1 init];
    BKNSLog(@"%@ - %p - %p",p1,p1,&p1);
    BKNSLog(@"%@ - %p - %p",p2,p2,&p2);
    BKNSLog(@"%@ - %p - %p",p3,p3,&p3);  

输出打印结果


打印的分别是对象描述、指针指向的地址(即当前指针存放的对象的地址)、当前指针地址(当前指针被存储在哪里)。
可以看出p1,p2,p3指向了同一个内存地址0x600001138790,而分别使用了3个指针保存这个内存地址的值。

猜想:在alloc一步就已经创建出了一个对象??
我们再通过objc源码来验证这个猜想

源码探究

一步一步跟进alloc的源码

  1. alloc ——> _objc_rootAlloc
+ (id)alloc {
    return _objc_rootAlloc(self);
}`
  1. _objc_rootAlloc——> callAlloc
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
  1. callAlloc——> _objc_rootAllocWithZone
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

这里面系统用到了两个宏定义

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
  • __builtin_expect这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)。意思是:EXP==N的概率很大。
  • __builtin_expect()是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。
  • 也就是说fastpath(x)告诉编译器x值为真的可能性最大。编译器会更大可能编译fastpath条件分支里的指令,也就是告诉编译器很大概率走这条分支。
  • slowpath(x)告诉编译器x值为假的可能性最大,也就是x为真的可能性很小,当x==0为真才执行这个条件分支下的语句。编译器会更大可能编译else部分的指令,也就是告诉编译器很小概率会走if slowpath(x)这条分支的指令。
  • 通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降。

在回到callAlloc方法里,由于这里有几个分支,可以通过断点调试跟方法,看是走的哪一个分支。
if (fastpath(!cls->ISA()->hasCustomAWZ()))中,cls->ISA()->hasCustomAWZ())判断一个类是否有自定义的 +allocWithZone 实现,所以fastpath(!cls->ISA()->hasCustomAWZ())表示这个类没有自定义的+allocWithZone 时走这部分代码。所以执行了_objc_rootAllocWithZone(cls, nil);

  1. _objc_rootAllocWithZone——> _class_createInstanceFromZone
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
  1. _class_createInstanceFromZone
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
    // 1:需要开辟的内存大小,可以看到外部传入的extraBytes为0
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 2;向系统申请内存,返回地址指针
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    // 3: 关联到相应的类
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

这个方法主要做了三个事情:计算需要的内存大小、申请内存返回地址指针、关联到对应的类。下面分析这个三个方法都做了哪些工作

alloc核心方法

1.计算内存大小

计算需要开辟的内存空间大小是通过size = cls->instanceSize(extraBytes);内部实现过程如下

size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

在这里可以发现,if (size < 16) size = 16;不够16个字节,会手动分配16个字节。之后调试跟进走的是cache.fastInstanceSize快速计算实例大小的方法。

fastInstanceSize中会执行到align16

size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

align16的实现,可以看到当前使用的是16字节对齐的方式。

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

图示16字节对齐运算


16字节对齐运算

16字节对齐的目的

  • 提高性能,加快存取速度:通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取。频繁存取字节未对齐的数据,会极大降低cpu的性能。固定16字节的存取长度,可以更快存取数据。
  • 更安全:苹果如今采用16字节对齐,由于在一个对象中,isa占8字节,而对象每个属性也占8字节,当对象无属性时,会预留8字节,即16字节对齐,如果不预留,CPU存取时以16字节长度会导致访问到相邻的其他对象,造成访问混乱。

在执行完cls->instanceSize(extraBytes)就可以打印出计算出的内存大小size为16


我们知道OC代码底层是用C/C++实现的,将OC转成C/C++代码可以发现,NSObject实际是一个结构体,结构体里只有个isa指针,既然是指针,在64位机环境下占了8个字节,在32位环境下占了4个字节。而NSObject这个结构体内部只有这么一个指针,那NSObject本身也是占用8字节。

struct NSObject_IMPL {
    Class isa;
};

可以用runtime的一个函数,获取类的实例大小

class_getInstanceSize(Class _Nullable cls)

需要引入#import ,打印结果果然是8个字节

NSLog(@"InstanceSize:%zd",class_getInstanceSize([NSObject class]));

结果:InstanceSize:8

我们还能通过导入#import ,查看到这个函数

extern size_t malloc_size(const void *ptr);

使用这个函数打印

NSLog(@"%zd",malloc_size((__bridge const void *)(obejct)));

结果是 16

由此看出对象本身占用8个字节,而系统开辟了16个字节用来保存这个对象。

进一步验证

上面只是NSObject类的情况,而通常类都会有相关属性。
BKPerson类增加4个属性,两个字符串类型,两个int类型

@interface BKPerson : NSObject

@property (nonatomic,strong) NSString *name;     // Dog
@property (nonatomic,strong) NSString *nick; // KK
@property (nonatomic) int age;
@property (nonatomic) int hobby;

@end

运行调试



查看对象内存地址


可以看到对象isa指针占据了8字节,两个string属性每个占据8字节,一共8+8+8=24个字节,但是系统开辟了32字节,也就是有8个字节为空。

再赋值两个int属性,而64位机下int类型占据了4个字节,可以看到之前空的8个字节,刚好放了age和hobby两个属性的值。

结合以上16字节对齐分配空间法则,进一步得出结论:
类的实例占用8字节,而每一次申请的内存空间是16字节。类的实例在第一次申请内存空间就申请了包括isa和所有属性的内存空间大小,跟属性有没有赋值无关

2.申请内存,返回地址指针

return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC);从第三个参数可以看到传入的zone为nil,由于iOS 8以后废除了用zone开辟内存的方式,所以是用obj = (id)calloc(1, size);的方式申请内存。这里面size就是我们上面字节对齐算法得出的内存大小。

打印执行calloc之后的地址


而这里只打印出了开辟的内存地址,没有类的信息例如,验证了calloc这一步只是向系统申请内存空间。

3.关联相应的类

将地址指针与类相关联

obj->initInstanceIsa(cls, hasCxxDtor);
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

这一步会init一个isa指针,与类关联起来

打印指针描述,可以看到已经关联上类了


总结:以上对alloc源码的探究,可以得知alloc的主要作用就是使用16字节对齐算法计算内存,开辟内存,关联类。

alloc的整体流程图示


那么init帮我们做了什么事呢?

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

- (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;
}
  • 由上面源码可以知晓init的类方法和对象方法返回的都是对象本身。
  • 不同的是类方法返回了一个id类型的self,这是为了可以给开发者提供自定义构造方法的入口,通过id强转类型实现工厂设计,返回我们定义的类型。

new

我们习惯于用new一个对象,可以更省略代码,通过源码可以知道它跟alloc+init的方式本质并没有区别

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

但是一般开发中并不建议使用new,主要是工厂设计重载init方法我们如果做一些业务的操作,用new初始化则无法执行到里面去。

你可能感兴趣的:(iOS底层之alloc、init探究)