iOS底层IOS开发知识点

IOS底层(三): alloc相关: 初探 alloc, ini

2021-03-10  本文已影响0人  ShawnAlex

OC底层源码/原理合集

首先我们先看下, 这三个打印

exp

依次输出的是 内容内存地址指针地址

打印结果

result

因为3个对象的指针指向同一内存空间, 所以内容, 内存地址是相同的, 但是对象的指针是不同的

这里留意下
%p -> &p1: 是对象的指针地址
%p -> p1: 是对象指针指向的的内存地址

由上面例子我们也可以看出, alloc是申请内存空间, 而init根本不会对内存空间做任何修改, p1, p2, p3 的指针地址是不同的, 但是他们都指向同一内存空间

exp2

接下来进入我们的主题看一下 alloc, init都做了些什么

我这边准备的是 objc4-781 源码项目
下载或者自己编译相可参考我之前写的 IOS底层(一): objc4-781 官方源码编译以及调试

alloc源码探索

首先先看一张图, 希望牢牢记住, 图片内容很重要


alloc+init整体源码探索流程

接下来我们拿objc4-781 源码走下流程

首先我们见一个 NSObject 类, 我这里名字叫 SAPerson

alloc-1.1
// alloc源码分析---第一步
+ (id)alloc {
    return _objc_rootAlloc(self);
}
// alloc源码分析---第二步
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
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));
}

这里如果我们不清楚走了哪个方法可以通过打断点来调试, 会发现这边走的是_objc_rootAllocWithZone

关于上面方法解释下
//进来之后按顺序执行, 先执行 slowpath(checkNil && !cls)这个判断

关于slowpathfastpath我们点进去看下定义

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

其中的__builtin_expect指令是由编译器gcc引入的

1. 目的: 编译器可以对代码进行优化, 以减少指令跳转带来的性能下降。(性能优化)

2. 作用: 允许程序员将最有可能执行的分支告诉编译器

3. 写法: __builtin_expect(EXP, N)。表示 EXP==N的概率很大。

4. fastpath: __builtin_expect(bool(x), 1)。表示x值为真的概率很大如果方法放在if判断中, 执行真的if的几率很大。

5. slowpath: __builtin_expect(bool(x), 0)。表示x值为假的概率很大如果方法放在if判断中, 执行假的else的几率很大。

6. 在日常的开发中, 也可以通过设置来优化编译器 , 达到性能优化的目的, 设置位置 Build SettingsOptimization Level

alloc-编译优化设置

我们接着回头接着看callAlloc中判断

#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

因为进来的checkNil为false, cls存在为true, 但!clsfalse
slowpath这个方法为false不执行return nil; 继续往下走

cls->ISA()->hasCustomAWZ()是用来判断一个类是否自定义了 +allocWithZone 方法, 我们是没有的, 所以这里走if里面的代码, 即走到_objc_rootAllocWithZone

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

我们可以看到zone参数不再使用, 接下来走类创建实例对象内存空间方法

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;

    // 计算内存空间大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 申请内存
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        // 将cls类与obj的指针(即: isa)关联
        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.cls->instanceSize计算开辟内存空间大小

  1. calloc开辟内存, 返回地址指针(obj)

3.obj->initInstanceIsa 类与isa指针关联 (其中isa是指示关系)

下面普及一个小知识点, 针对于下面例子, 点击进入NSObject, 可看到

NSObject *objc = [NSObject alloc];
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 就会占用一部分内存空间, 为什么呢?
NSObject 默认有个isa对象, 对象就是个指针, isa需要开辟8字节(arm64)

我们看个流程图便于理解下


alloc-_class_createInstanceFromZone流程

1. cls->instanceSize:计算所需内存大小

首先通过断点可看到extraBytes为0

extraBytes

进入instanceSize源码,

    size_t instanceSize(size_t extraBytes) const {
        //  编译器快速计算内存空间大小方法
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        //  alignedInstanceSize 对齐我们当前的实例size
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        // 最小16 
        if (size < 16) size = 16;
        return size;
    }

我们先看下alignedInstanceSize (虽然打断点, 会发现走的时候fastInstanceSize快速计算内存空间方法)

    uint32_t alignedInstanceSize() const {
        // word_align 字节对齐
        //这个方法主要做了, 将没有字节对齐的一些东西, 调整为字节对齐返还回去
        return word_align(unalignedInstanceSize());
    }

那没有字节对齐的是什么东西呢?我们再点击进去

    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

data是当前类的信息(类是结构体), data()->ro->instanceSize就是获取这个类所有属性内存的大小, 这里只有继承NSObject的一个属性isa——返回8字节

再看下word_align 方法

#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;
}

例如: 传进来是一个8字节的isa指针, x = 8, 因为WORD_MASK = 8
则有8 + 7 & ~7, 15 & ~7

0000 1111 & ~7, 7为 0000 0111, ~7为 1111 1000

0000 1111 &
1111 1000 =
0000 1000 = 8 即返回8字节

我们看下流程图便于理解下

alloc-instanceSize

不过通过断点调试,会走cache.fastInstanceSize快速计算内存空间大小方法

点击进入看下源码

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

这里留意下__builtin_constant_p 也是编译器gcc的内建函数 用于判断一个值是否为编译时常数,如果传入参数的值是常数,函数返回 1,否则返回 0

fastInstanceSize

通过断点调试,会执行到align16

其中

#define FAST_CACHE_ALLOC_MASK         0x1ff8
#define FAST_CACHE_ALLOC_MASK16       0x1ff0
#define FAST_CACHE_ALLOC_DELTA16      0x0008
#if __LP64__
            uint16_t                   _flags;
#endif

_flags 是个16位无符号整数 与 FAST_CACHE_ALLOC_MASK进行按位与运算
其中 FAST_CACHE_ALLOC_MASK = 0x1ff8 = 8184 = 1111111111000

按位与运算

1111111111000   &
0000000010000   = 
0000000010000   = 16

所以size = 16
FAST_CACHE_ALLOC_DELTA16 = 0x0008 = 8
extra = 0

size + extra - FAST_CACHE_ALLOC_DELTA16 = 16 + 0 - 8 = 8

align16 方法传入 8

跳转align16源码实现

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

我们可以看到, 这是个16字节对齐方法, 跟刚才类似虽然传入的是8

x + 15 & ~15
23 & ~15,  15为 0000 1111, ~ 15为 1111 0000
0001 0111  & 
1111 0000  =
00010000 = 16 可看到返回16

最后的结果为 16,即内存的大小是以16的倍数增加的

这里先介绍下几个知识点

结构体的内存布局依赖于CPU操作系统编译器及编译时的对齐选项。结构体内部成员的对齐要求,结构体本身的对齐要求。最重要的有三点

(一)成员对齐对于结构体内部成员,通常会有这样的规定:各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。但是也可以看到,有时候某些字段如果严格按照大小紧密排列,根本无法达到这样的目的,因此有时候必须进行padding。各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充也就是padding。

(二)然后,还要考虑整个结构体的对齐需求。ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格。实际上要求结构体至少是其中的那个最大的元素大小的整数倍。因为有时候我们使用的是结构体数组,所以结构体的大小还得保证结构体数组中各个结构体满足对齐要求,同时独立的结构体与结构体数组中单个结构体的大小应当是一致的。

(三)编译器的对齐指令。VC 中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数。

整理一下

内存字节对齐原则

那么OC为什么需要16字节对齐

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

我们之前已经通过instanceSize计算了内存大小
申请size大小, 并赋值给obj, obj是指向内存地址的指针 指针是obj

obj = (id)calloc(1, size);

这里我们可以通过断点来验证calloc

在平常的开发中,一般一个对象的打印的格式都是类似于这样的<NSObject: 0x01111111f>(是一个指针)。为什么这里不是呢?

3. obj->initInstanceIsa:类与isa关联

经过calloc,内存已经申请好了,类也已经传入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示


obj->initInstanceIsa

主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行关联

可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针


initInstanceIsa

总结
通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍, 核心步骤有3步:计算 -- 申请 -- 关联

init源码探索

先看下init源码, 有两种类方法init, 实例方法init

类方法init

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

实例方法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;
}

也可以看到返回的是传入的self 本身

new源码探索

先看底层, 可看到本身调用了callAlloc, init函数, 可以得出new等价于 [XXX alloc ] init]

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

但是针对于重写初始化方法

- (instancetype)init {
    self = [super init];
    if (self != nil) {
        
        NSLog(@"1");
   
    }
    return self;
}

- (instancetype)initWithText {
    self = [super init];
    
    if (self != nil) {
        
        NSLog(@"2");
    
    }
    return self;
}
TestObj *obj1 = [[TestObj alloc] initWithText]; 走2
TestObj *obj2 = [TestObj new];   走1

new调用只会走1, 不难看出如果

上一篇下一篇

猜你喜欢

热点阅读