对象的底层探索上

103 阅读3分钟
YcPerson *p = [YcPerson alloc];        
YcPerson *p1 = [p init];    
YcPerson *p2 = [p init];        
YcPerson *p3 = [p init];

NSLog(@"%@-----------%p,%p",p1,p1,&p1);
NSLog(@"%@-----------%p,%p",p2,p2,&p2);
NSLog(@"%@-----------%p,%p",p3,p3,&p3);
        
2022-04-18 09:50:45.925545+0800 objc[11276:5563625] <YcPerson: 0x10121d500>-----------0x10121d500,0x7ff7bfeff340
2022-04-18 09:50:45.926195+0800 objc[11276:5563625] <YcPerson: 0x10121d500>-----------0x10121d500,0x7ff7bfeff338
2022-04-18 09:50:45.926259+0800 objc[11276:5563625] <YcPerson: 0x10121d500>-----------0x10121d500,0x7ff7bfeff330

0X7:栈内存地址 0X6:堆内存地址 

alloc做的事情:

alloc开辟了一块内存区域,指针p1,p2,p3,为栈地址连续的三个指针,指向这一块内存区域

查看实现:

  1. 符号断点:objc_alloc
  2. 通过查看汇编语言Debug->Debug Workflow->Always Show Disassembly
一些汇编命令:

b,bl,callq都是调用函数,b是简单的跳转,bl是带返回值的跳转
b,blarm架构,callqX86架构,M系列之后都改成了arm架构
ret 函数的返回     :;注释
mov 是将数据从一个位置(寄存器或内存中)复制到另一个位置(寄存器或内存中)
movbmovwmovlmovq;主要区别是传送数据的大小不同,分别是1248字节
x86-64中有一条限制,前后两两操作数不能都指向内存位置
  1. 查看苹果开源源码: opensource.apple.com/releases/

opensource.apple.com/tarballs/

alloc 调用顺序

点击源码的方法跳转,得到的调用顺序:

alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone

通过汇编验证alloc调用顺序:

分别给+alloc,callAlloc,_objc_rootAlloc 打断点,得出三者顺序:

callAlloc -> alloc -> _objc_rootAlloc -> callAlloc -> 

在[Person alloc]添加断点后运行,开启汇编模式,可以看到:

image.png

先调用了 objc_alloc方法,然后control+单步调试显示如下:

image.png

在NSObject.mm 1914行找到objc_alloc方法:

image.png

注释也显示,class 调用alloc方法会调用这个objc_alloc方法,然后在objc_alloc方法里调用了callAlloc方法. 而之所以是objc_alloc方法是因为在源码的objc-runtime-new种有个fixupMessageRef方法: image.png

里面定义了当方法名为alloc时,调用imp为objc_alloc方法地址.

所以,实际调用顺序为:

[NSObject alloc] -> objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone -> 

image.png

这里callAlloc调用了两次的原因:

首先要知道一点:调用allocWithZone时,会在内部调用_objc_rootAllocWithZone.

第一次进入callAlloc:参数allocWithZone为NO,fastpath(!cls->ISA()->hasCustomAWZ())为判断class
或superClass是否实现了allocWithZone方法,此时类还没有初始化,判断不成立->通过msgSend走alloc方法.

第二次通过_objc_rootAlloc进入callAlloc:参数allocWithZone为YES,此时如果此时没有实现
allocWithZone方法,则判断为YES,rentun _objc_rootAllocWithZone,开辟内存,
如果实现了allocWithZone方法,判断为NO,下方的判断allocWithZone为YES,依然通过msgSend(allocWithZone)走
_objc_rootAllocWithZone方法,最终结果都是_class_createInstanceFromZone方法

对象内存对齐

_class_createInstanceFromZone里面有个方法instanceSize,作用为计算给创建的实例对象需要多大的内存空间,最小为16字节.对象内部以8字节对齐,8字节算法可以写成:

int func (int x) {
    return (x + 7)/8*8 ;
}
位运算:
int func (int x) {
    return (x + 7) >> 3<<3 ;
}

位运算可以扩展为: n字节对齐的算法都可以写成:
int func (int x) {
    return (x + n) & ~n;
}

与对象内部不同,系统为对象分配内存时,以16字节对齐(以空间换时间).

对象内存大小由成员变量决定,与方法协议等无关.

// May be unaligned depending on class's ivars.

image.png

结构体内存对齐

对象的本质都是objc_object的结构体,其中包含着一个NSObject_IMPL的isa指针+成员变量的值

isa 也是一个objc_class的结构体,所以占8个字节.

objc_class继承于一个最原始的类型:objc_object

内存对齐规则:

image.png

关于其中数据类型占用内存:

  • 32位:
char : 1个字节
char*(即指针变量): 4个字节(32位的地址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节
int : 4个字节                        范围  -21474836482147483647
unsigned int : 4个字节
long : 4个字节                         范围 和int一样
long long : 8个字节                 范围  -92233720368547758089223372036854775807
unsigned long long : 8个字节    最大值:1844674407370955161
float : 4个字节
double : 8个字节
BOOL : 1个字节
  • 64位
char : 1个字节
char*(即指针变量) : 8个字节
short int : 2个字节
int : 4个字节                      范围  -21474836482147483647
unsigned int : 4个字节
long : 8个字节                     范围  -92233720368547758089223372036854775807
long long : 8个字节            范围  -92233720368547758089223372036854775807
unsigned long long : 8个字节    最大值:1844674407370955161
float : 4个字节
double : 8个字节
BOOL : 1个字节

接下来几个例子:

struct Person1 {

    double a; //8   [0 7]

    char b; //1     [8]

    int c;//4       [12 13 14 15]

    short d;//2     [16 17]       8的整数倍,不足补齐->24

};

struct Person2 {

    double a; //8   [0 7]

    int b; //4      [8 9 10 11]

    char c;//1      [12]

    short d;//2     [14 15]        一共16

};

struct Person3 {

    double a; //8   [0 7]

    int b; //4      [8 9 10 11]

    char c;//1      [12]

    short d;//2     [14 15]

    int e;//4       [16 17 18 19]

    struct Person1 per;//           24的整数倍,不足补齐->48

};

打印结果没毛病:

image.png

init做的事情:

init只是返回了obj本身,init是个工厂模式,存在只是为了让开发者重写,在里面做一些初始化操作

编译器的优化:

taget ->Build Setting ->Optimization Level,从None到Smallest,
通常Release使用Fastest[Os]等级
用户优化编译器,提高效率