【iOS】对象的底层结构&内存分配

对象的底层结构

要了解OC对象的底层结构,那么我们就得知道:OC本质底层实现转化其实都是C/C++代码。

  • 使用该指令将代码转换为C/C++代码。
clang -rewrite-objc main.m

对应文件目录下会生成main.cpp文件

对象的本质

  • 对象类的本质是结构体

OC源文件

@interface TestPerson : NSObject
{
    //成员变量
    //@public
    NSString* age; //4个字节
}
@property (nonatomic,copy) NSString *name; //属性

@end

@implementation TestPerson

@end

分析编译生成的main.cpp文件

#ifndef _REWRITER_typedef_TestPerson
#define _REWRITER_typedef_TestPerson
typedef struct objc_object TestPerson;
typedef struct {} _objc_exc_TestPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_TestPerson$_name;
struct TestPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *age;
	NSString *_name;
};

// @property (nonatomic,copy) NSString *name;
/* @end */


// @implementation TestPerson


static NSString * _I_TestPerson_name(TestPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_TestPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_TestPerson_setName_(TestPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct TestPerson, _name), (id)name, 0, 1); }
// @end

通过这个TestPerson类我们发现:

  • OC中的TestPerson类底层是struct TestPerson_IMPL结构体。
  • OC中@interface TestPerson : NSObjectTestPerson继承NSObject底层是typedef struct objc_object TestPerson;这样体现的。
  • 首先数入参TestPerson * self,SEL _cmd,所有OC方法都有这两个隐藏参数,所以我们可以在OC的方法中使用self指针,但是这两个参数对外是不显示的。
  • 看到getter和setter里是通过首地址指针+对应成员变量的地址值指针的偏移量的方式取和存的,最终通过(*(NSString **)还原为string类型。取值的过程就是:先拿到当前成员变量的地址,再去取这个地址里面所存的值。

struct NSObject_IMPL;具体实现是什么?

查找后发现

struct NSObject_IMPL {
	Class isa;
};

此时发现其结构体内存储的是一个Class类型的isa指针。

  • TestPerson这个类在底层编译成了TestPerson_IMP结构体。
  • 成员NSObjct_IVARS,这是一个结构体,里面只包含了isa
  • 成员age,是我们类里面的成员变量。
  • 成员_name,是我们类里面的属性,编译成了带有_下划线的成员。
  • 函数_I_TestPerson_name,实际上就是getter方法,包含两个默认参数self_cmd
  • 函数_I_TestPerson_setName_,实际上就是setter方法,包含两个默认参数self_cmd,与一个形参name

根据编译的结构我们可以得出下面结论:

  • 对象的本质在底层就是一个objc_object结构体。
  • 属性与成员变量的区别,属性是由成员变量+getter方法+setter方法组成。

类结构

objc_class部分

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

从上面的结构体,可以看出:类是一个结构体,里面存放了superClasscachebits

  • objc_class继承于objc_object,换句话说,类的本质也是个对象,其中isa指针式其继承自objc_object的,所以代码里才注释了// Class ISA;
  • isa是结构体,其中会存储类的地址,其并不是指针,“isa指向”这句话严格来说是不正确的,但是方便理解

对象、类、父类、元类关系

类的定义

对象的本质是一个结构体,而id,是指向这个结构体的指针,也就是我们平时Person *p = [Person alloc] init]; 的话,代表我们创建了一个Person对象分配了地址,并且把他的地址给了p这个指针,我们是通过p来操作对象,或者说p代表了对象,但p并不是对象本身。而对象是存在于内存里的一个结构体。

元类的定义

其原理就是OC对象在发送消息时,运行时库会追寻着对象的isa指针得到对象所属的类。这个类包含了能应用于这个类的所有实例方法以及指向父类的指针,以便可以找到父类的实例方法。运行时库检查这个类和其父类的方法列表,找到与消息对应的方法。编译器会将消息转换为消息函数objc_msgSend进行调用

同样我们也会有对类发送消息的情况:

NSString *testString = [NSString stringWithFormat:@"%d",3];
  • 从此处我们可以知道,OC的类其实也是一个对象,一个对象就要有一个它属于的类,意味着类也要有一个isa指针,指向其所属的类。那么类的类是什么?就是我们所说的元类(MetaClass),所以,元类就是类的所属类。

所以从消息机制的层面来讲:

  • 当你给对象发消息时,消息会寻找这个对象的类的方法列表
  • 当你给类发消息时,消息是在寻找这个类的元类的方法列表

元类的类

元类的类(根类)

有一个问题,如果按类的类是元类这个逻辑理解,是否可以一直循环下去?

  • 答案是肯定不行的,因为一定会有一个坐为最原始的那个类,那么这个类就是元类的类:所有的元类都使用根元类作为他们的类。根元类的 isa 指针指向了它自己。

关系图

使用最常见的这张图进行分析 instance(实例) Subclass(子类)

每一个实例变量的isa都指向自己所属的类,每一个类的isa都指向自己所属的元类,同时,父类的元类也是子类的元类的父类,父类的元类也是根元类的子类。而父类的元类和子类的元类都属于根元类。根元类的isa指向自己,同时根元类的父类也是自己的下属类(NSObject元类的父类是NSObject类)。

在这里插入图片描述

isa走位
  • 实例对象(Instance of Subclass)的isa指向类(class)
  • 类对象(class)的isa指向元类(Meta class)
  • 元类(Meta class)的isa指向根元类(Root metal class)
  • 根元类(Root metal class)的isa指向它自己本身,形成闭环,这里的根元类就是NSObject
superclass走位

类之间的继承关系:

  • 类(subClass)继承自父类(superClass)
  • 父类(superClass)继承自根类(RootClass),此时的根类是指NSObject
  • 根类继承自nil,所以根类即NSObject可以理解为万物起源,即无中生有

元类也存在继承,元类之间的继承关系如下:

  • 子类的元类(metal SubClass)继承自父类的元类(metal SuperClass)
  • 父类的元类(metal SuperClass)继承自根元类(Root metal Class)
  • 根元类(Root metal Class)继承于根类(Root class),此时的根类是指NSObject

isa详解

继承自objc_objectisa指针,不仅实例对象中有,类对象中也有,占8字节。

什么是isa?

  • isa指针保存着指向类对象的内存地址,类对象全局只有一个,因此每个类创建出来的对象都会默认有一个isa属性,保存类对象的地址,也就是class,通过class就可以查询到这个对象的属性和方法,协议等;

isa分两种类型:

  • 指针型isa:64位的0或者1的整体内容代表所指向的Class的地址,也就是可以通过isa的内容来获得类对象的地址。
  • 非指针型isaisa的值的部分代表Class的地址,之所以这样是因为我们在寻址过程中,只有三四十位数就可以保证我们寻找到所有Class地址了,多出来的位可以用来存储其他相关内容,来达到节省内存的目的。

每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。

此处我们主要研究__arm64__下共用体结构类型isa

isa指针,即isa_t结构体

union isa_t 
{
    Class cls;
    uintptr_t bits;
    //bits的结构体
    struct {
         uintptr_t nonpointer        : 1;//->表示使用优化的isa指针
         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 deallocating      : 1;//->对象是否正在销毁
         uintptr_t has_sidetable_rc  : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
        uintptr_t extra_rc          : 19;  //->存储引用计数,实际的引用计数减一,存储的是其对象以外的引用计数
    };
};

isa_t的定义中可以看出:提供了两个成员,clsbits,由联合体的定义所知:这两个成员是互斥的,也就意味着,当初始化isa指针时, 有两种初始化方式

  • 通过cls初始化,bits无默认值
  • 通过bits初始化,cls有默认值

bits的结构体里的变量:

  • nonpointer:用来标记这个对象是不是tagpointer类型的对象,因为iOS对oc对象进行了优化处理,有些对象是tagpointer类型的,因此这些对象是没有isa指针的,tagpointer的内存一般是在栈中的,而不是在堆里面;tagpointer对象一般是NSNumber类型的数值较小的数,或NSString类型的较小的字符串。
  • has_assoc:用来标记有没有关联对象。
  • has_cxx_dtor:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。
  • shiftcls:存储的isa指针地址,也就是类对象的地址。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced:对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存。
  • has_sidetable_rc:标记对象是否使用了Sidetable,当对象引用计数大于10时,则需要借用该变量存储进位。
  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减1, 例如:如果对象的引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用到下面的 has_sidetable_rc

在这里插入图片描述

isa的初始化过程

看过了isa_t的底层结构,现在我们来看一下isa 的初始化过程,来探索isa_t 的底层实现,初始化过程发生在alloc方法的底层流程中的这一步:

// 将类和指针做绑定
    obj->initInstanceIsa(cls, hasCxxDtor);

此过程包含了 isa 的初始过程,要探究 isa 的底层实现,我们就从 isa 的创建开始:

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());
	//下方函数调用就是isa的初始过程
    initIsa(cls, true, hasCxxDtor);
}

initInstanceIsa的源码实现中,主要是调用了initIsa,继续跳到initIsa的源码:

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } 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);
#endif
        newisa.extra_rc = 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_t 类型的 newisa 实例, 做了赋值操作后,返回了 newisa

该源码解释了上面初始化isa的两种方式:

  • 通过cls初始化:非nonpointer,存储着Class、Meta-Class对象的内存地址信息。
  • 通过bits初始化:nonpointer,进行一系列的初始化操作。
isa初始化流程图

请添加图片描述

class方法

首先Class这里需要注意几点

  • class ObjectClass = [[nsobject class]class]; 返回的还是class对象,并不是meta-class对象。- (Class)class, +(Class)class返回的就是类对象
  • 元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的
  • 类对象在内存中有且仅有一个对象 主要包括 isa指针 super Class指针 类的属性信息 类的对象方法信息 类的协议信息 类的成员变量信息
//instance实例对象
        NSObject *object1 = [[NSObject alloc] init];
        NSObject *object2 = [[NSObject alloc] init];

        //class对象,类对象
        //objectClass1~5都是NSObject的类对象
        Class objectClass1 = [object1 class];
        Class objectClass2 = [object2 class];
        Class objectClass3 = [NSObject class];
        Class objectClass4 = object_getClass(object1);
        Class objectClass5 = object_getClass(object2);
        
        //元类对象(将类对象当作参数传入进去)
        Class objectMetaClass = object_getClass([NSObject class]);
        Class objectMetaClass2 = [[NSObject class] class];
        
        //判断是不是元类对象

        NSLog(@"instance - %p %p", object1, object2);
        NSLog(@"class - %p %p %p %p %p %d", objectClass1,objectClass2, objectClass3, objectClass4, objectClass5, class_isMetaClass(objectClass3 ));
        NSLog(@"mateClass - %p %p %d",objectMetaClass, objectMetaClass2, class_isMetaClass(objectMetaClass));

输出情况:
instance - 0x100511920 0x10050e840
class - 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0
mateClass - 0x7fff91da20f0 0x7fff91da2118 1

无论多少次class方法得到的都还是类函数。

我们再来看一下objc_class的源码部分

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;              // 方法缓存 formerly cache pointer and vtable
    class_data_bits_t bits;    // 用于获取具体的类信息 class_rw_t * plus custom rr/alloc flagsflags
    ...

bits里面存储了类的方法列表等等的信息

  • 这里的bitsclass_data_bits_t类型的,上面objc_objectisa_t类型数据中也有一个uintptr_t类型的bits,但是这是两种结构。

我们先看bits的数据结构 class_data_bits_t

struct class_data_bits_t {
    friend objc_class;
    // Values are the FAST_ flags above.
    uintptr_t bits;
    public:
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    // Get the class's ro data, even in the presence of concurrent realization.
    // fixme this isn't really safe without a compiler barrier at least
    // and probably a memory barrier when realizeClass changes the data field
    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            return maybe_rw->ro();
        } else {
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        }
    }
}

其实最最重要的是其中的2个方法。datasafe_ro,两个方法分别返回class_rw_tclass_ro_t

  • 这里ro_t的获取也是通过data方法获取的,所有可以理解为ro_t也在rw_t之中。

内存的分配

获取内存大小

  1. 类的实例对象的大小(本质是对象中成员变量的大小)
    函数返回对齐过的实例变量(传入类)
class_getInstanceSize
  1. 获得系统实际分配的内存大小(也存在内存对齐),obj指针所指向内存的大小
malloc_size((__bridge const void *)obj)
  1. 针对类型(int/double/struct)是一个运算符,计算类型的大小(编译时确定数值)
//例如
int 4字节/(对象)指针  8字节/bool 2字节
//某个结构体类型的大小
sizeof(struct Person_IMPL)

一个NSObject对象占用多少内存?

由刚才对对象本质的探索可知:

//NSOject本质

struct NSObject_IMPL {
	Class isa;
};

首先通过分析对象类的本质得出其本质其实是结构体,由于NSObject结构体中的isa本质上是一个指针,在64位中指针占8个字节,而通过malloc得出他的实际内存是16,剩下的8位没有用。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject* obj = [[NSObject alloc] init];
        
        //类的实例对象的大小
        NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        
        //obj指针所指向内存的大小
        NSLog(@"%zd", malloc_size((__bridge const void *)obj));
        
    }
    return 0;
}

运行如下

请添加图片描述

系统分配了16字节给NSObject对象(通过malloc_size函数获得)

请添加图片描述

NSObject对象内部只使用了8字节的空间(64bit环境下,通过class_getInstanceSize函数获得,返回内存对齐后成员变量的大小)

请添加图片描述

内存对齐规则

对齐系数

每个特定的平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。我们可以通过预编译命令#pragma pack(n),n=1、2、4、8、16 来改变这一系数,其中的n就是要指定的“对齐系数”。我们iOS编译器Xcode的对齐系数就是8。

对齐规则

  • 数据成员对齐规则:(Struct或者Union的数据成员)第一个数据成员放在偏移为0的位置。以后每个数据成员的位置为min(对齐系数,自身长度)的整数倍,下个位置不为本数据成员的整数倍位置的自动补齐。
  • 数据成员为结构体:该数据成员的内最大长度的整数倍的位置开始存储。
  • 整体对齐规则:数据成员按照1,2步骤对齐之后,其自身也要对齐,对齐原则是min(对齐系数,数据成员最大长度)的整数倍。

内存对齐在OC中的优点

有时候我们会思考为什么系统开辟的内存大小会大于我们申请的内存大小呢?按照8字节对齐的方式,申请的内存就可能已经存在多余的了。

  1. 按照8字节对齐方式,对象内部里面的成员内存地址是绝对安全的。
  2. 我们无法确定申请多余的字节就在对象与对象之间,有可能会出现在对象内存段的内部某个位置,这个时候就可能会出现两个对象内存段是挨着的情况,没有那么的安全。系统开辟空间采取16字节方式,保证对象的内存空间会更大,对象与对象之间更加的安全。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值