iOS底层-对象里都有什么

前言

上篇文章说了iOS中alloc方法是怎么创建对象的,以及对象的本质是结构体。接下来继续探究对象的内存分布,以及对象的isa是个什么样的结构体,存储了哪些信息。

对象内存分布

已知系统给对象分配内存是16字节对齐的;

@interface FFGoods : NSObject

@property (nonatomic ,copy) NSString *goodsName;  // 0-8
@property (nonatomic ,copy) NSString *type; // 9-15
@property (nonatomic ,assign) int goodsId; // 16-19
@property (nonatomic ,assign) double weight;  // 24-31
@property (nonatomic ,assign) short number; // 32-33
// 33经过16字节对齐后 -> 48

@end

那么以上实例应该是48字节:

image-20220420095646500

如果加一个类方法和实例方法,这个对象占用的内存大小会不会发生变化呢?

image-20220420095952777

说明没有影响。对于实例对象,内存分配是由isa + 成员变量的值,这是之前的结论。

既然成员变量的值存储在对象里面,会不会受到属性的书写顺序影响?

打印goods实例的内存地址前,先解释一下将会出现的lldb指令:

p 能够知道引用类型,po只能打印值;

p/x 表示按照16字节打印,还有p/o 是8字节 , p/t 是2字节, p/f 是浮点数

x/6gx: 第一个x代表打印对象的内存地址,/后面是参数;4g代表连续4个8字节的地址,最后个x代表16进制;

image-20220420102902582

goodsName为什么没有紧跟在isa后面?

isa指针后面的8字节:0x0000007b00000038,尝试分为 0x0000007b0x00000038 2段打印:

image-20220420103058035

发现4字节的int和2字节的short会放在一个8字节的内存空间里。因为iOS会自动重排属性的顺序,达到内存优化。

再增加一个char属性(int + short + char < 8字节),

@property (nonatomic ,assign) char abc;

发现还是排在同个8字节空间里:因为对象内部是8字节对齐的。

image-20220420103639944

再看一个例子:


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FFTestObject : NSObject
{
@public
    int count;
    NSObject *subObject1;
    NSObject *subObject2;
}
@end

@interface FFTestSubObject : FFTestObject
{
@public
    int subCount;
}
@end

NS_ASSUME_NONNULL_END

打印:

image-20220420113929595

然后我调整父类 int 属性的顺序,再运行看到 subObject 实际占用变成32。这说明成员变量的顺序还是会对内存造成微小影响的

image-20220420114530398

父类在内存中是一块连续的内存空间,子类无法修改和插入。也就是iOS对子类的成员变量,不会跟其父类混合重排在一起。图中int放在尾部时,子类可以跟在父类后面,填补空白的内存。

平时开发时不用特别在意,相差几个字节对内存影响太小。

至于iOS的自动重排,还得从位域说起。

位域

C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称**“位域”( bit field)** 。

定义2个结构体:

C可以在定义结构体的同时定义结构体变量,不过OC在.h文件中定义需要使用typedef修饰。
struct Student{
// …
} student1,student2;

struct FFStructA {
    char a;
    char b;
    char c;
    char d;
}structA;

struct FFStructB {
    char a : 1;
    char b : 1;
    char c : 1;
    char d : 1;
}structB;

打印内存大小,发现structB为什么只占1个字节?

image-20220420134716472

这就是位域的作用,a:1 表明只需要1个位域(二进制的1个比特位),位域的特点是不能超过数据类型的最大长度

当前字节剩余位域不够用的时候,会直接从下个字节开始存放。修改位域,再打印发现内存占用变成2字节:

image-20220420141447070

不过位域节省的空间实在有限,只有系统级别才会用到。对于开发者不需要节约,而是不要去浪费。

联合体

新建一个结构体和联合体:

struct FFGoods {
    char *name;
    int number;
    double height;
}goodsStruct;

union UnionStudent {
    char *name;
    int number;
    double height;
}goodsUnion;

在没赋值前,打印出联合体与结构体一样都是初始值;赋值一个属性后就不一样了,其他属性会是个乱七八糟的值;

image-20220420152704551

union就是属性共用一块内存空间,系统会分配一块足够大的内存空间;成员变量就相当于开辟了几种去访问这一整块内存的途径。

上图为什么会出现乱七八糟的值?一开始的时候赋值字符,那么其他成员的数据类型指针去读取字符串的时候自然是错误的

联合体的作用也是能节省一定的内存空间,所占内存取决于最大成员变量,例子中就是double类型占8字节。

同时还得满足最大基本数据类型的整数倍:

union UnionStudent {
    char *name;
    int number;
    double height; // 8字节
    char a[9]; // 1 * 9 = 9 字节;但是要满足最大基本数据类型(这里是double)的整数倍
}goodsUnion;

这跟结构体内存对齐道理一样:

image-20220420153604682

与结构体的区别:

结构体是共存,内存开辟比较粗放,只要写了内存变量就会去开辟内存空间;

联合体是互斥的,省内存空间;

对象的isa

那么联合体在objc底层是怎么使用的?

来到objc源码的_class_createInstanceFromZone方法,上回只说到calloc,接着看后面的方法。代码中实例化的时候加一个断点,看看会发生什么。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello World!");
    
        FFPhone *phone = [FFPhone alloc]; // 这行加断点
        logPhone(phone);
    }
    return 0;
}

目前为止,还只是id类型,跟自己创建的类还没有什么关系。通过calloc开辟的内存空间如何与类产生关联?

image-20220420162854760

运行到下一个断点,po一下,看到已经是我们的类了。实际上就是通过前面的initisa函数进行关联的。

image-20220420162930499

注意initInstanceIsa这个分支,代码里最终还会调用initisa:

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

    initIsa(cls, true, hasCxxDtor);
}

来到 initIsa 看看具体做什么:

image-20220420163437623

Tips:简单提一下,第328行 isTaggedPointer 是指针优化

第355行,extra_rc代表引用计数的值;通过alloc方法创建出来的对象,引用计数就是1,并且存在于isa指针里。

可以看到isa_t 查看发现,破案了!iOS的isa指针结构居然是联合体。

image-20220420164301937

这里对代码加一些注释:有2个构造器和3个数据成员。通过94行得出,当有几东西只能同时存在一个的时候,确实适合用联合体;


#include "isa.h"

union isa_t {
    // 2个构造器
    isa_t() { } 
    isa_t(uintptr_t value) : bits(value) { }
        // 数据成员1,64位内存占用
    uintptr_t bits;
// 数据成员2,Class类型指针
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)
    // 数据成员3
    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);
};

既然是联合体,那么3个数据成员只会同时存在一个。看到这个判断条件:

#if defined(ISA_BITFIELD)

如果没定义ISA_BITFIELD,那么isa_t就是Class类型的结构体指针;既然给了64位内存,只存放Class指针肯定是不划算的。那么iOS根据情况切换成其他数据成员。

如果有定义ISA_BITFIELD,就变成另一种结构体,ISA_BITFIELD本身还代表这些结构体成员有哪些:

image-20220420170102496

看到 elif __x86_64__ ,原来这个数据成员还区分架构的。我的是intel芯片的mac系统,架构是__x86_64__

以下是arm64模拟器和真机:(__has_feature: 是否支持指针的身份验证;A12芯片后引出的;)


# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t has_cxx_dtor      : 1;                                       \
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        uintptr_t magic             : 6;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif

nonPointerIsa

注意到isa结构体成员中的uintptr_t nonpointer : 1,当nonpointer=1的时候,代表开启指针优化,这时候的isa指针也叫做nonPointerIsa指针。关于指针的内容今后会单独讲一篇,这里稍微提一下。

单独整理一下不同架构的isa结构和里面指针的含义。

arm64 (模拟器):

uintptr_t nonpointer        : 1;                                   
uintptr_t has_assoc         : 1;                                    
uintptr_t weakly_referenced : 1;                                   
uintptr_t shiftcls_and_sig  : 52;                                 
uintptr_t has_sidetable_rc  : 1;                            
uintptr_t extra_rc          : 8

arm64 (真机):

uintptr_t nonpointer        : 1;
uintptr_t has_assoc         : 1;
uintptr_t has_cxx_dtor      : 1;
uintptr_t shiftcls          : 33;
uintptr_t magic             : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused            : 1;
uintptr_t has_sidetable_rc  : 1;
uintptr_t extra_rc          : 19

x86_64:

uintptr_t nonpointer        : 1;
uintptr_t has_assoc         : 1;
uintptr_t has_cxx_dtor      : 1;
uintptr_t shiftcls          : 44;
uintptr_t magic             : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused            : 1;
uintptr_t has_sidetable_rc  : 1;
uintptr_t extra_rc          : 8

指针含义:

指针含义
nonpointer是否对 isa 指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa还包含了类信息、对象的引用计数等
has_assoc是否有关联对象
has_cxx_dtor该对象是否有 C++ 或者 Objc 的析构器。如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
shiftcls存储类指针的值。开启指针优化的情况下,来存储类指针,在不同架构中位数不同。
magic用于调试器判断当前对象是真的对象,还是没有初始化的内存空间
weakly_referenced对象是否被指向或者曾经指向一个 ARC 的弱变量, 没有弱引用的对象可以更快释放
deallocating标志对象是否正在释放内存
has_sidetable_rc是否有使用 sidetable 散列表来存储引用计数,通常是引用计数extra_rc存满了
extra_rc表示该对象的引用计数值

回到刚才的demo;打印对象内存地址后怎么通过地址来获取内部这些指针呢?

获取isa里的指针

例如__x86_64__架构ISA_BITFIELD的这个uintptr_t shiftcls : 44;指针,要单独取出这中间的44位,就意味着将左右位数据都变成0。通过位运算,左移右移能达到效果。这里就是右移3位到最右边,左移20(右3+左17)位回到最左边,再右移17位移回原来的位置。

为什么前三个在右边?内存地址右边是低位,属性又是从低位开始存。

lldb通过 p/x address >> 3 << 20 >> 17;得到后的地址就是该实例对象的类对象地址空间。验证如下:

image-20220420173230366

接下来尝试获取extra_rc,因为在高8位,直接右移 64-8 = 56 位就行了;前面会补0 ;并且为了测试再加一个指针引用:

image-20220420200042686

结论:引用计数的上限取决于cpu架构。并且次数上限 c o u n t = 2 n − 1 count = 2^n - 1 count=2n1,n为位数。

我就要问了,要是存储就是超过了怎么办?借位,存到散列表里;并用has_sidetable_rc标识;

以上位移操作获取类对象地址不用这么麻烦,系统有 ISA_MASK 掩码帮你完成了;

这与之前介绍的对象内部8字节对齐时,WORD_MASK是一样的:

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

这是ISA_MASK的定义:define ISA_MASK 0x00007ffffffffff8ULL ,直接进行与运算就能得到类对象地址。

image-20220420232116076

用计算器查看这个掩码的位:本质是就是你需要保留的位都置1,其余0。与运算后0位都是0。

image-20220420232602001

结尾的ULL = unsigned long long

总结

影响对象内存的因素

isa通过位域的形式,将对象相关的信息存储起来在8字节里面。所以影响对象内存的只有成员变量(属性会自动生成带下划线的成员变量)。

对象的内存分布

在对象的内部是以8字节进行对⻬的。苹果会自动重成员变量的顺序,将占用不足 8 字节的成员挨在一起,凑满 8 字节,以达到优化内存的目的。

联合体(union)

联合体的成员变量就相当于为这块内存空间开辟了几个访问途径,他们共享这一块内存。

联合体的大小规则:

  • 必须能容纳联合体中最大的成员变量
  • 必须是数据成员中最大的基本数据类型大小的整数倍

联合体和结构体的区别 :

  • 结构体中所有变量是“共存”的,而联合体(union)中是各变量是“互斥”的。
  • 一个完整,一个灵活。

位域

  • 位域的宽度不能超过声明的数据类型的最大⻓度。比如int占4个字节也就是32位,那:号后面的数字就不
    能超过32。
  • 如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放
    该位域。
  • 位域能够节省一定的内存空间。

nonPointerIsa

  • nonPointerIsa是内存优化的一种手段。isa是一个Class类型的结构体指针,占8个字节也就是64位。然而存储地址根本不需要这么大的内存空间。而且每个对象都有个isa指针,这样就浪费了内存。所以iOS就把和对象相关的信息,存在了这块内存空间里面。这种isa指针就叫nonPointerIsa

  • 通过isa的位运算得到类对象。借助系统提供的掩码ISA_MASK就能轻松得到,使用时注意区分架构。同时还可以通过平移得到其他对象信息。

对象的结构体关系图

image-20220506135016434

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值