1、iOS底层分析 — alloc分析

一、alloc  init探索

简介:

iOS中alloc是为对象申请开辟内存的方发, 初始化的时候用的最多的就是如下形式。

[[xxx alloc] init];

问题:

1、想要分析alloc从哪入手,怎么分析?

2、alloc在底层都做了什么?在底层怎么实现的?

3、alloc的时候已经创建了对象,init里面什么都没有写(看过源码才知道的),那么init到底用来干什么呢?

 

分析:

探索alloc常用的方法


探索alloc 从 int main 函数入手

以下是三种常用的探索手法,简单介绍

1)、下断点: control + in 找到  objc_alloc

2)、下符号断点:libobjc.A.dylib`+[NSObject alloc]

断点– symbolic Breakpoint – 对应方法名

3)、汇编查看流程(注意使用真机调试)

Debug - Debug workflow - Always show Disassembly

下载源码及环境配置

需要下载并在源码中编译,进一步探索alloc。配置如下

objc4-750源码 + Xcode11 + MacOS 10.15

官方源码下载地址

iOS_objc4-756.2 最新源码编译调试

开始问题分析

alloc init

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"LG_Cooci: alloc 探索");
    // alloc 探索
    // alloc 已经创建了对象 init
    // alloc 怎么创建的呢?
    // alloc 实现 - 原理 - 源码实现
    // 介绍三种方式
    // libobjc.A.dylib
    // 1: 下断点 : control + in - objc_alloc
    // 2: 下符号断点 : libobjc.A.dylib`+[NSObject alloc]:
    // 3: 汇编  libobjc.A.dylib`objc_alloc:
    
    LGPerson *p1 = [LGPerson alloc];
    LGPerson *p2 = [p1 init];
    LGPerson *p3 = [p1 init];
    LGNSLog(@"%@ - %p",p1,&p1);
    LGNSLog(@"%@ - %p",p2,&p2);
    LGNSLog(@"%@ - %p",p3,&p3);

//打印出来的结果是p1 p2 p3结果一样,地址不一样

有三个指针,有三个指针地址。但是这三个指针指向的地址是相同给的。

3个指针指向同一个空间

}

这个时候可以知道p1  p2  p3 三个指针指向的都是[LGPerson alloc] 开辟的空间

这个时候就会有有最开始的疑问,那alloc就已经创建了对象,那init又干了什么呢?,这个是就要开始alloc的分析

alloc 分析

从int main函数入手,打断点,然后 command+左键 点击进去一步一步分析(在源码中分析)

1、alloc

NSObject.mm (在底层是混编所以是.mm)

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

 2、_objc_rootAlloc

NSObject.mm 返回一个callAlloc

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

3、callAlloc(cls,falsee,true)

NSObject.mm

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;//容错无用

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        上边的都不重要,判断一下非自定义的类的话都走这,下边的这些是重点
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif
目前真机使用的都是objc—2,所以下边的不会走省略不是重点
}

4、class_createInstance(cls, 0) (创建实例)

其内部调用了_class_createInstanceFromZone方法,并在其中进行size计算,内存申请,以及isa初始化

id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

5、_class_createInstanceFromZone

根据不同的条件,选用calloc或者malloc_zone_calloc进行内存申请,并且初始化isa指针,申请完成大小为size(size的大小5.1讲述)的对象obj已经,并且返回

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

5.1、cls->instanceSize(extraBytes),计算出size,

为了方便读取和提高性能进行字节对齐,然后计算出对齐之后实际所需要的大小
其中64位系统下,对象大小采用8字节对齐,但是alloc实际申请的内存最低为16字节,也就是说系统分配内存按照16字节对齐分配

8字节对齐是用空间换取时间,保证读写速度。

// 此方法,用来字节对齐
uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
// 字节对齐的详细方法,WORD_MASK在64位下为7,32位下为3,用来进行字节对齐
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

// 申请地址,此处有16字节限制
size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
}

总结:

alloc

–>_objc_rootAlloc

-> callAlloc

-> class_createInstance(创建实例)

->_class_createInstanceFromZone(从专有区创建实例)

{1、_class_instanceSize(开辟空间大小,64位8字节对齐、32位4字节对齐同时大于等于16)

2、calloc

3、objc_initInstanceIsa(实例Isa)

附上以为大神总结的流程图

扩展延伸其他知识点

1、断点控制台调试部分命令

All Variables  所有的变量

register read  读寄存器

2、OC 编译到下层的时候调用的仍是OC的方法,然后下层混编调用函数处理。下层的时候是混编

3、alloc 创建对象 申请内存空间 – 指针(有了指针不再是空乏的类LGPerson)

alloc 创建了对象,会返回相应的对象地址,返回了对象的地址就有了申请内存空间的能力,返回值存在x0。

x0既是第一个参数的传递口,也是返回值的存储地方。

先开一个内存空间(开辟的内存空间大小是8字节的倍数(64位机),同时要大于16),开辟成功后关联内存空间。

isa 是个联合体 指向其地址空间。

4、alloc

  1. 开辟申请内存
  2. 伴随初始化了isa

5、init

  1. _objc_rootInit(obj)
  2. return obj

工厂设计,交给子类去自定义重写   自由

init是规范

6、new

相当于alloc init

[callAlloc() init]

7、系统分配对象内存算法

开辟内存大小(且、取反)

(x+WORO_MASK)&~ WORO_MASK

64位#define WORO_MASK 7UL    32位  3UL

64 是8的倍数  32位是4的倍数   同时满足大于等于16

if(size < 16) size = 16;

8、内存对齐

SHIFT_NANO_QUANTUM 16
K = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM


slot_bytes = k << SHIFT_NANO_QUANTUM

系统开辟内存以16位倍数。

9、汇编分析,编译器会优化部分的代码

1、     build setting

  1. optimization
  2. Fastsamallest

2、    编译器优化的作用

  1. 编译时间
  2. 链接时间
  3. 运行时间
  4. 空闲时间

LLDB调试配置objc4  malloc源码

调试需要用真的原因是

需要真机

Arm64 X0- X30

模拟器是

X86

发布了83 篇原创文章 · 获赞 12 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/104028275