Objective-C 对象的底层探索

源码 2024-9-22 18:11:57 34 0 来自 中国
苹果官方资源opensource
本章节研究对象的底层探索:
1.alloc init在底层的调用、new的调用实质
2.关于编译器的优化
3.对象的本质
4.对象的内存对齐方式
5.布局体的内存对齐方式
6.对象的内存分布
7.影响对象内存的因素
8.认识位域和团结体
9.实例对象的nonPointerIsa
10.通过isa位运算后得到类对象
一、alloc在底层的调用流程

一个class的实例是通过这行代码: Person *p = [[Person alloc] init]; 或者 Person *p = [Person new]; 来创建的。
那我们的 alloc 和 init分别做了什么事,内存的分配到底是何时分配的呢?
来看看这段代码:
- (void)viewDidLoad {    [super viewDidLoad];    //  MyPerson是继续NSObject的类    MyPerson *p = [MyPerson alloc];    MyPerson *p1 = [p init];    MyPerson *p2 = [p init];    NSLog(@"p = %@, p1 = %@, p2 = %@", p, p1, p2);}留意:p、p1和p2是同一个对象,由于他们都指向同一个内存地点。
2022-04-16 14:08:43.058890+0800 AllocProcess[27584:5000030] p = <MyPerson: 0x6000001603c0>, p1 = <MyPerson: 0x6000001603c0>, p2 = <MyPerson: 0x6000001603c0>结论就是 init 方法不会去开发内存空间。
1.通过 objc4-838可编译联调源码 进行解读 alloc的调用过程:

1.png 留意:断点调试要先把源码里的断点关闭掉,等运行流程走到 main 函数的断点才把源码的断点打开,如许是防止断在体系的类创建实例。
留意:[Person alloc] 在调用的时间第一次进源码调试,并没有走到 + (id)alloc,而是直接走在 callAlloc 函数,通过objc_msgSend的方式去调用
到 + (id)alloc
4.png 再一次调用callAlloc
_class_createInstanceFromZone才是分配内存,创建对象的实质逻辑。
2.通过汇编的方式验证源码中分析的 alloc 调用流程

起首创建一个工程,并运行到断点位置
7.png 开启汇编模式
开启汇编模式后,就会看到汇编调试的代码
留意汇编找到 objc_alloc 函数并调用的逻辑是在fixupMessageRef声明的
10.png 在我调用 [MyPerson alloc] 的时间在汇编看到它是调用 objc_alloc 和源码里分析的调用 callAlloc 不一样。于是我在源码里搜索找到 objc_alloc 看看是个什么逻辑:
可以看到 objc_alloc 仍然是 callAlloc 标题不大,于是继续看看汇编底层怎么个调用法
此时读取寄存器的值:
register read x0 确认简直是MyPerson,也就是调用方法的第一个埋伏参数self
register read x1打印地点,po这个地点,确认简直是alloc,也就是调用方法的第二个埋伏参数 cmd
通过objc_msgSend发送一个 alloc 消息,于是我添加一个 [NSObject alloc]符号断点继续往下实行
汇编流程继续往下走,就找不到源码里会调用_class_createInstanceFromZone函数了。
这是由于我们的编译器给我们做了优化的缘故,这个标题第二部门讨论。
汇编流程和源码逻辑分析出来的alloc调用流程是一样的。
总结 alloc 的调用流程
3._class_createInstanceFromZone才是分配内存,创建对象的实质逻辑

/************************************************************************ class_createInstance* fixme* Locking: none** Note: this function has been carefully written so that the fastpath* takes no branch.**********************************************************************/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;    // 创建obj的逻辑    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;    }    // 此时obj还是id范例    // 关联到类    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);    }    // 终极会返回这个obj    if (fastpath(!hasCxxCtor)) {        return obj;    }    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;    return object_cxxConstructFromClass(obj, cls, construct_flags);}总结 alloc 的焦点方法
a、来看看 size = cls->instanceSize(extraBytes); 盘算对象内存空间大小
inline size_t instanceSize(size_t extraBytes) const {        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {            return cache.fastInstanceSize(extraBytes);        }                // 这里是关于内存对齐的盘算        // 在为实例开发内存空间是以8字节作为内存对齐的        size_t size = alignedInstanceSize() + extraBytes;        // CF requires all objects be at least 16 bytes.         // CF要责备部对象至少为16字节。        if (size < 16) size = 16;        return size;    }在盘算对象的内存对齐方式是以8字节作为内存对齐的(在第四部门尚有关于对象的内存对齐的分析)
在64位的iOS利用体系下,是以8字节为内存对齐的
全部创建出来的对象大小最少是16个字节
b、来看看 obj = (id)calloc(1, size); 现实分配内存的逻辑
objc4的源码内里没有calloc的实现,它的实现是另一份libmalloc源码才有。
在malloc.c找到calloc
void *calloc(size_t num_items, size_t size){    return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);}它底层找得比较深,这里就不粘贴了,直接看目的函数吧
17.png static MALLOC_INLINE size_tsegregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey){    size_t k, slot_bytes;    if (0 == size) {        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior    }        // k = (size + 16-1) >> 4    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta        // slot_bytes = k << 4    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size    *pKey = k - 1;                                                  // Zero-based!    return slot_bytes;}在分配对象内存空间对齐方式是以16字节作为内存对齐的。
为什么要在分配对象内存空间以16字节对齐呢?
这里涉及到空间换时间的概念,这是由于cpu在读取内存的时间是不是以字节为单位,而是以内存块为单位,它可以每次读2个字节,但是会造成大量盘算,cpu服从会很低很低,也可以每次读32字节,无疑大概造成空间浪费,在大量试验读取的方式,终极选用以16字节对齐的方式。
结论:
a.调用[MyPerson alloc]就能返回类的实例对象了。
b.alloc对象分配内存终极底层会走malloc。(ps: swift底层也是malloc)
c.在盘算对象的内存对齐方式是以8字节作为内存对齐的,
在分配对象内存空间对齐方式是以16字节作为内存对齐的。

4.init做了什么逻辑

活动断点到init调用的位置,开启汇编模式
添加一个 [NSObject init]符号断点,继续看看init到底做了什么事
调用init方法之后就直接返回了
20.png 于是我找到objc4源码看看init方法的逻辑
22.png 啥也没干,直接返回了对象了。以是苹果计划这个init方法有什么作用呢?
苹果计划init方法是以工厂模式头脑,给子类重写init,以提供子类的成员变量赋值利用。
5.new的调用实质

在objc4源码中找到new方法,它的底层调用和alloc底层调用是一样的,别的它还调用了init方法
24.png 通过汇编模式调试看看调用new方法是否和源码的逻辑一样:
25.png 在汇编中看到调用new方法会调用objc_opt_new
于是我在源码中找到这个objc_opt_new符号,是一样的。(这里做了OBJC2和之前版本的适配)
调用new的实质实在就是调用了alloc、init。
二、关于编译器的优化

我们的Xcode中是可以设置编译器的优化品级的
TARGETS -> Build Settings -> Optimization Level
28.png 设置这个编译器的优化品级有什么用呢?
新建一个工程 macOS -> Command Line 取名为Test
将Optimization Level的debug设置和release一样的品级,开启汇编模式运行
运行可以看到,这个赋值并没有看到3和4
然后再Optimization Level的debug设置调回来(没有任何优化品级的情况下),开启汇编模式运行
32.png 可以看到 0x3 和 0x4
这就是编译器帮我们做了优化的部门,由于对于 a 和 b 这两个变量我们根本没有去利用它,而且对于一些简单的盘算(比如声明一个 sum函数 去a+b),它一样会被优化。
如许做的话整个体系能更加地快速,别的在现实开发过程中不必要去改动默认的Optimization Level。
而上面所说的 _class_createInstanceFromZone 就是编译器优化掉的部门。
三、对象的本质

创建一个main.m
#import <Foundation/Foundation.h>@interface Person : NSObject@property (nonatomic, assign) int age;@property (nonatomic, copy) NSString *name;- (void)test;@end@implementation Person- (void)test {    }@endint main(int argc, char * argv[]) {        @autoreleasepool {        // Setup code that might create autoreleased objects goes here.    }    return 0;}通过clang指令编译一下main.m,得到main.cpp
当我们的类被编译了之后,底层会类编译成 isa + 成员变量,以是在给类的实例分配内存的话这个内存块存储的就是 isa + 成员变量的值
四、对象的内存对齐方式

上面提到过:对象的内存对齐方式是以8字节作为内存对齐的
    inline 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;    }// Class's ivar size rounded up to a pointer-size boundary.    uint32_t alignedInstanceSize() const {        return word_align(unalignedInstanceSize());    } 36.png (x + 7) & ~7    这是苹果计划的8字节对齐算法,先留下这个公式。
思索:如果让我们计划一个8字节对齐算法应该怎么做?
起首8字节对齐肯定是8的倍数
int align8Bit(int x) {    return (x+7)/8*8;}// x+7 制止x的值小于8,否则做除以8都是0了// 然后除以8 再乘以8 得到的肯定是8的倍数但是如许写除法和乘法会比位运算的服从要低,于是又可以把乘除换成位运算
int align8Bit(int x) {    return (x+7) >> 3 << 3;}// 除以8就是右移3位// 乘以8就是左移3位留意:
进行右移3位再左移3位,它的低3位永世是0
全部的数只要是8的倍数,它的低3位永世是0
再回来看看苹果的8字节对齐公式:
(x + 7) & ~7  7的二进制是 0111,~7就是对7的二进制取反即1000,末了做 & 利用如许就包管了低3位永世是0了。
固然我们计划的8字节对齐算法和苹果计划的对齐算法是一样的,但是苹果用一个函数就适配了对64位和32位的适配,真大佬呀。
五.对象的内存分布、影响对象内存的因素

在第三部门就总结: 对象的本质 = isa + 成员变量的值。
盘算下面这个MyPerson类分配的对象占用多大的内存
#import <Foundation/Foundation.h>@interface MyPerson : NSObject// isa 8字节@property (nonatomic, copy) NSString *name; // 8字节@property (nonatomic, copy) NSString *hobby; // 8字节@property (nonatomic, assign) int age; // 4字节@property (nonatomic, assign) double height; // 8字节@property (nonatomic, assign) short number; // 2字节@end得到的对象现实占用38字节,但是我们的在创建对象的时间体系会以16字节对齐的方式去给对象分配内存,即体系会为MyPerson的实例分配48字节的内存大小。
#import <Foundation/Foundation.h>#import <malloc/malloc.h>#import "MyPerson.h"int main(int argc, const char * argv[]) {    @autoreleasepool {        MyPerson *p = [MyPerson new];        p.name = @"安安";        p.hobby = @"吃吃睡睡喝喝";        p.height = 1.80;        p.age = 26;        p.number = 123;        // 48        NSLog(@"%lu", malloc_size((__bridge const void *)(p)));    }    return 0;}给MyPerson类添加一个实例方法和类方法,体系给MyPerson的实例分配内存大小是没有影响的。
料想:如果把MyPerson的属性的序次打乱是否会影响该类的实例分配的内存大小呢?(影响对象内存的因素)
请自行随意打乱MyPerson的属性序次,然后再控制台通过lldb指令调试:
lldb指令:  p   输出10进制  p/x 输出16进制  p/0 输出8进制  p/t 输出2进制  p/f 输出浮点数           x   输出地点  x/4gx  输出4个字节地点  x/6gx  输出6个字节地点  ...只管我怎样去打乱属性的序次,发现age和number属性的值存在第二个8字节里,这是个什么机制呢?由于age只占用4字节 number只占用2字节,如果让他们都单独占据一个8字节的内存,无疑造成了内存浪费,于是苹果会对这个标题做了一系列的优化。
体系怎样优化内存分配的?
解答影响对象内存的因素:
在编译器时,编译器会主动重排属性的序次,以到达节流内存空间目的。

举例重排属性序次:
#import <Foundation/Foundation.h>@interface OSTestObject : NSObject@property (nonatomic, strong) NSObject *n1;@property (nonatomic, assign) int count1;//@property (nonatomic, assign) int count2;@property (nonatomic, strong) NSObject *n2;@end@interface OSTestSubObject : OSTestObject@property (nonatomic, assign) int count3;@endint main(int argc, const char * argv[]) {    @autoreleasepool {        OSTestSubObject *objc = [[OSTestSubObject alloc] init];        objc.count1 = 10;//        objc.count2 = 11;        objc.count3 = 12;        NSLog(@"objc现实占用内存的空间为%zd",class_getInstanceSize([OSTestSubObject class]));        NSLog(@"体系为objc开发内存的空间为%zd", malloc_size((__bridge void *)objc));        NSLog(@"------------");    }    return 0;}此时我的父类属性count在中心,按理说应该在isa后面的第二个位置,但是重排后的结果却是把它安排在了第一的位置了。
接着我把count2的解释打开,继续打印,发现父类的count1和count2属性归并在一个8字节了
总结:类的属性重排不但只针对当前类,尚有父类,但是归并属性在同一个内存只针对当前类。
特别留意:类的成员变量是不能重排的!
举例父类誊写成员变量的序次对子类实例分配内存的影响:
@interface OSTestObject : NSObject{    @public    int count;// 1.若打开这个解释,为OSTestSubObject实例现实必要40字节,体系为实例分配48字节    NSObject *obj1;//    int count; // 2.若打开这个解释,为OSTestSubObject实例现实必要40字节,体系为实例分配48字节    NSObject *obj2;//    int count; // 3.若打开这个解释,为OSTestSubObject实例现实必要32字节,体系为实例分配32字节}@end@interface OSTestSubObject : OSTestObject{    @public    int count2;}@end这三个count的位置,在分别用x/6gx输出OSTestSubObject对象的时间,序次永世不会变的,但是誊写序次会影响内存分配。
为什么对类的属性序次进行重排能够优化内存空间?
由于内存对齐,在盘算对象所需内存的时间是以8字节对齐的。
六、布局体的内存对齐方式

起首相识:布局体和数组一样都是一块连续的内存空间。
41.png
布局体内存对齐方式规则:
a.布局体的第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始(比如int为4字节,则要从4的整数倍地点开始存储)
b.如果一个布局体包罗了某些布局体成员,则布局体成员要从其内部最大的元素大小的整数倍地点开始存储。
c.布局体的总大小:sizeof的结果必须是其内部最大成员的整数倍,不敷的要补齐。
//案例一:struct MyStruct1 {    double a; // [0-7]    char b; // [8]    int c; // 9 10 11不可  [12-15]    short d; // [16-17]}struct1;// 必要24字节//案例二:struct MyStruct2 {    double a; // [0-7]    int b; // [8-11]    char c; // [12]    short d; // 13不可 [14-15]}struct2;// 必要16字节//案例三:struct MyStruct1 {    double a; // [24-31]    char b; // [32]    int c; //  [36-39]    short d; // [40-41]}struct1;struct MyStruct3 {    double a; // [0-7]    int b; // [8-11]    char c; // [12]    short d; // 13不可 [14-15]    int e; // [16-19]    struct MyStruct1 stru; // 24开始,由于里边double范例最大 是8}struct3; // [0-41]// MyStruct3必要48字节七、认识位域和团结体

1.位域

struct MyStruct1 {    char a;    char b;    char c;    char d;}struct1;  // 4字节// 位域,留意:数字的大小不能小于范例的长度!struct MyStruct2 {    char a : 1;     char b : 1;    char c : 1;    char d : 1;}struct2;  // 1字节NSLog(@"%lu, %lu", sizeof(struct1), sizeof(struct2)); // 4, 1留意:数字的大小不能小于范例的长度!
char a : 1; 表现a用1个比特位来存储,以是abcd统共必要4个比特位,只必要分配1个字节富足。
如果改成char a : 7;和 char b : 2;  此中a就单独占一个字节,由于a占用7位,而b占用2位,一个字节8位,不能把ab同塞一个字节。于是sizeof(struct2)大小就是2。
这里知识仅当学习作用方便看源码,我们不建议平常开发如许去做,由于一样平常都是体系级别的才会如许处理处罚,如许做的节流内存空间极为有限。
2.团结体

struct Teacher1 {    char *name;    int age;    double height;}t1;NSLog(@"name=%s, age=%d, height=%f", t1.name, t1.age, t1.height); // name=(null), age=0, height=0.000000t1.name = "安安老师";NSLog(@"name=%s, age=%d, height=%f", t1.name, t1.age, t1.height); // name=安安老师, age=0, height=0.000000t1.age = 18;NSLog(@"name=%s, age=%d, height=%f", t1.name, t1.age, t1.height); // name=安安老师, age=18, height=0.000000t1.height = 1.80;NSLog(@"name=%s, age=%d, height=%f", t1.name, t1.age, t1.height); // name=安安老师, age=18, height=1.800000// 0x100008508: 0x100008508 -- 0x100008510 -- 0x100008518NSLog(@"%p: %p -- %p -- %p", &t1, &t1.name, &t1.age, &t1.height); // 团结体union Teacher2 {    char *name;  // 8字节    int age;    double height;}t2;t2.name = "安安老师";t2.age = 18;t2.height = 1.80;// 0x100008508: 0x100008508 -- 0x100008508 -- 0x100008508NSLog(@"%p: %p -- %p -- %p", &t2, &t2.name, &t2.age, &t2.height);团结体的全部成员变量共用同一个内存地点,赋值了一个成员会影响别的差别范例成员的取值。
团结体的大小决定于最大成员(基本数据范例的整数倍,数组不是基本数据范例)(t2最大的是char * 相当于是对象8个字节)。
union Teahcer3 {    char a[7]; // 占7字节    int b; // 占4字节}t3;  // 4字节的整数倍,至少必要8字节布局体与团结体的区别:
struct中全部变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不消,全分配;
团结体(union)中是各变量是“互斥”的——缺点就是不敷“包容”;但优点是内存利用更为风雅机动,也节流了内存空间。

八、利用isa通过位运算后得到类对象、nonPointerIsa是什么

上面报告到alloc的底层是开发内存空间,它底层是调用_class_createInstanceFromZone函数处理处罚内存分配逻辑的,现实上它还处理处罚了isa!
而obj->initInstanceIsa(cls, hasCxxDtor);底层就是调用了obj->initIsa(cls);,以是obj->initIsa(cls);就是处理处罚isa的底层逻辑
inline void objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor){     ASSERT(!isTaggedPointer());  // taggedPointer 指针优化        isa_t newisa(0); // isa_t 是团结体    // nonpointer:表现是否对 isa 指针开启指针优化  0:纯isa指针,1:不止是类对象地点,isa 中包罗了类信息、对象的引用计数等    if (!nonpointer) {         newisa.setClass(cls, this); // isa里生存了类对象    } else {        ASSERT(!DisableNonpointerIsa);        ASSERT(!cls->instancesRequireRawIsa());#if SUPPORT_INDEXED_ISA        ASSERT(cls->classArrayIndex() > 0);        newisa.bits = ISA_INDEX_MAGIC_VALUE;        // isa.magic is part of ISA_MAGIC_VALUE        // isa.nonpointer is part of ISA_MAGIC_VALUE        newisa.has_cxx_dtor = hasCxxDtor;        newisa.indexcls = (uintptr_t)cls->classArrayIndex();#else        newisa.bits = ISA_MAGIC_VALUE;        // isa.magic is part of ISA_MAGIC_VALUE        // isa.nonpointer is part of ISA_MAGIC_VALUE#   if ISA_HAS_CXX_DTOR_BIT        newisa.has_cxx_dtor = hasCxxDtor;#   endif        newisa.setClass(cls, this); // isa里生存了类对象#endif        newisa.extra_rc = 1; // isa里生存了 引用计数的值1    }    // This write must be performed in a single store in some cases    // (for example when realizing a class because other threads    // may simultaneously try to use the class).    // fixme use atomics here to guarantee single-store and to    // guarantee memory order w.r.t. the class index table    // ...but not too atomic because we don't want to hurt instantiation    isa = newisa;}可以看到往isa里生存了很多多少信息,比如类对象、引用计数等等。
其次,来看看isa_t的声明,它是一个团结体:
#include "isa.h"union isa_t {    isa_t() { }    isa_t(uintptr_t value) : bits(value) { }    uintptr_t bits;private:    // Accessing the class requires custom ptrauth operations, so    // force clients to go through setClass/getClass by making this    // private.    Class cls;public:#if defined(ISA_BITFIELD)    struct {        ISA_BITFIELD;  // defined in isa.h    };    bool isDeallocating() {        return extra_rc == 0 && has_sidetable_rc == 0;    }    void setDeallocating() {        extra_rc = 0;        has_sidetable_rc = 0;    }#endif    void setClass(Class cls, objc_object *obj);    Class getClass(bool authenticated);    Class getDecodedClass(bool authenticated);};我们知道isa指针它是一个Class范例的布局体指针,主要用来存储内存地点的的,它占用8个字节(64位),但是我们的类对象的存储用不完这64的位域,于是苹果就把一些和对象息息相干的东西,一起生存到这64位域信息里。
这些存放的东西都在 ISA_BITFIELD 这个宏界说里,它是区分平台的(arm64、x86_64等等)。
苹果计划利用团结体isa_t的目的是去兼容老版本的isa,由于老版本的isa里只有Class cls,没有别的信息,而相干的信息又会必要别的内存空间,无疑造成内存浪费。nonPointerIsa可以明白成是新版本的isa。
nonPointerIsa里64位域里的内容:
45.png 怎样利用isa通过位运算后得到类对象?
我是通过模仿器调试的而且电脑芯片是Intel Core i5的,以是我直接看x86_64的ISA_BITFIELD声明:
46.png 我要得到中心的44位要怎样得到?
就是把低3位和高17位清0,再复位即得到中心的44位。x >> 3 << (17+3) >> 17
而苹果给的方案就是:isa地点 & ISA_MASK = 类对象地点
想要得到引用计数extra_rc的值:x >> (64-8)
48.png 留意验证的时间,要看清楚机型对应的架构。
末了附上objc_object总结图:
49.png 引用计数位extra_rc存储的值超了,会存储在has_sidetable_rc,在内存管理章节会讲。
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 20:23, Processed in 0.219200 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表