分析oc对象的内存结构及其创建过程

栏目: Objective-C · 发布时间: 5年前

内容简介:面向对象的编程语言中有经典的话:万物皆对象。首先分析对象的内存结构,其实我们知道利用

面向对象的编程语言中有经典的话:万物皆对象。 objective-c 就是一门面向对象的语言,那么在 oc 的编程中就离不开对象的创建,下面分析oc对象的内存结构及其创建的过程

内存结构

首先分析对象的内存结构,其实我们知道 oc 的对象指针其实就是结构体指针,也就是说oc的对象转成c++代码后其实就是一个结构体。定义一个简单的类代码如下:

@interface Person : NSObject
@property (nonatomic,assign) NSUInteger age;
@property (nonatomic,copy) NSString *name;
-(void)say;
@end

@implementation Person
-(void)say{
    NSLog(@"person say");
}
@end
复制代码

利用 clang 编译器吧这个类转成 c++ 代码后可以发现对应的 Person 类其实就是一个结构体,代码如下:

struct Person_IMPL {
	Class isa;
	NSUInteger _age;
	NSString * _Nonnull _name;
};

复制代码

先忽略 say 方法的存在(方法跟结构体的 isa 指针相关,稍后再分析),可以看出来 struct Person_IMPL 的结构体定义跟 Person 类的属性定义是吻合的。所以,其实我们平常 创建对象其实就是给类的对应的结构体在堆上开辟一块合适的空间,并返回这块空间的指针给用户,这个指针就是我们平时操作对象(包括对象方法的调用,对象属相的更改)的指针,只是oc把这个结构体指针包装成一个oc类型的指针( Person * )而已

接下来利用指针强转把oc对象类似指针转换为 c语言 结构体指针来验证一下oc类其实底层就是c语言的结构体。

运行代码:

Person *p = [[Person alloc] init];
p.age = 15;
p.name = @"Mike";
//指针强转
struct Person_IMPL *sp = (__bridge struct Person_IMPL *)(p);
NSLog(@"通过oc对象类型指针转为结构体指针后访问的结构体Person_IMPL值 : _age = %zd , _name = %@" , sp->_age , sp->_name);
复制代码

打印结果:

通过oc对象类型指针转为结构体指针后访问的结构体Person_IMPL值 : _age = 15 , _name = Mike
复制代码

验证结果符合预期。

既然oc对象的底层数据结构是c语言的结构体,那么对象的属性或成员的存取其实跟c语言结构体的成员变量的存取原理其实是一样的:通过指针的偏移操作内存的数据:用一个通俗一点的公式可以表达为 propertyValue(对象的成员变量值) = objcPointer(对象指针) + offset(偏移量)。对象内存及其指针的关系用下图表示:

分析oc对象的内存结构及其创建过程

我们可以用代码验证通过指针的操作能否访问到oc对象的内存数据 代码如下:

Person *p = [[Person alloc] init];
p.age = 15;
p.name = @"Mike";
struct Person_IMPL *sp = (__bridge struct Person_IMPL *)(p);
//通过指针的偏移操作 , 或的结构体内部的成员地址,也是oc对象指针(Person * p)的对象成员地址。
long long ageAdress =  (long long)((char *)sp+8);
long long nameAdress = (long long)((char *)sp+16);
//打断点通过lldb指令调试验证
NSLog(@"===");
复制代码

在NSLog处打断点后,通过lldb打印相关指令查看到p或sp指针向上偏移8字节可以获取到时成员变量age的值,如图所示

分析oc对象的内存结构及其创建过程

p或sp指针向上偏移8字节可以获取到时成员变量name

分析oc对象的内存结构及其创建过程

的值,如图所示

对象的创建过程

研究类的初始化过程肯定是通过 objc 官方源码分析 , 本人用的是 objc4-750 的版本进行分析。

通过我们创建对象都是调用 +alloc 方法进行创建的,此方法调用到了下面两个方法,我把该方法的简化如下

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    //此函数调用到下面的 _class_createInstanceFromZone 
    id obj = class_createInstance(cls, 0); 
    return obj;
}

//class_createInstance 调用到此方法
static __attribute__((always_inline))  id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    bool hasCxxDtor = cls->hasCxxDtor();
    
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    obj = (id)calloc(1, size);
    if (!obj) return nil;
    obj->initInstanceIsa(cls, hasCxxDtor);
    
    return obj;
}


复制代码

上面函数的作用有两个

  1. 获取创建的对象所需的空间,并分配相应的空间(在获取空间大小的时候内部逻辑会判断 size >= 16,并且对齐二进制后面3位为0)
  2. 把分配好的空间内存指针转为( struct objc_object * 就是我们的 id 指针,这也反映了 NSObjcet * 对应 struct objc_object * ,下面会分析)
  3. 把初始化完isa的指针作为对象指针返回给调用者

对象定义分析

在分析初始化isa指针前先弄清楚 oc对象指针( NSObjcet * , id )对应在 objc 源码中那些结构体的关系可能会容易理解一点

// NSObject 定义
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//struct objc_object 定义
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

//struct objc_class 定义
struct objc_class : objc_object {
    // 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
}

//id 指针的定义
typedef struct objc_object *id;

//Class 定义
typedef struct objc_class *Class;

复制代码

通过上面的代码可以发现,我们平常使用的 NSObject * 或者 id 指针其实底层是 struct objc_object 指针,而平常使用的Class类型其实底层就是 struct objc_class 指针,而且还可以发现 Class 类型( objc_class * )其实是继承自 objc_class ,就是说我们 Class 类型其实也是一个对象。

在oc中对象(object),对象的父类(SuperClass),对象的类(Class),对象的元类(MetaClass)都是通过指针来进行关联的。 SuperClass 对应的是 objc_classsuperclass 指针 , Class 对应的是 objc_classisa 指针(OBJC2中的isa指针已经不是直接指向Class的地址了,而是用来位域的技术存储了Class的地址外还有其他一些额外的信息)。

isa指针

首先我们开看下isa结构的定义( objc-private.h + isa.h ) 这里紧列举 __x86__64__ 架构的情况进行分析

#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    struct {
      uintptr_t nonpointer        : 1;//代表是否有开启指针isa指针优化(位域技术存储更多信息)                                       
      uintptr_t has_assoc         : 1;//是否有设置关联对象                                        
      uintptr_t has_cxx_dtor      : 1;//是否有c++析构函数                                        
      uintptr_t shiftcls          : 44;//存储Class或MetaClass的内存地址信息
      uintptr_t magic             : 6;//验证对象是否初始化完成                                         
      uintptr_t weakly_referenced : 1;//是否有被弱引用指针指向                                         
      uintptr_t deallocating      : 1;//对象是否正在释放                                         
      uintptr_t has_sidetable_rc  : 1;//extra_rc无法存储过大的数值时,次标志位为1,把extra_rc部分的值存储到一个全局的SideTable中                                    
      uintptr_t extra_rc          : 8//存储引用计数存储 (引用值 = 存储值 - 1)
    };

};
复制代码

可以看 isa_t 其实是一个共用体union : 一个8字节指针(64位) = cls = bits = 使用位域的struct

了解了 isa_t 的结构后我们看下 struct objc_object 初始化isa的方法实现

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) { // Taggedpointer
        isa.cls = cls;
    } else { // 非Taggedpointer , 平时我们经常使用的对象
      
        isa_t newisa(0);
        
         //对isa的 index 、magic 初始化
        newisa.bits = ISA_MAGIC_VALUE;
        
        //对isa的 has_cxx_dtor 初始化
        newisa.has_cxx_dtor = hasCxxDtor;
        
        //把传进来的Class指针值右移3位赋值给shiftcls
        newisa.shiftcls = (uintptr_t)cls >> 3;
        
        //更新objc_object的isa指针
         isa = newisa;
    }
}
复制代码

在复制 Class 的指针值是为什么要右移三位在赋值,其实原因可以在从上面获取内存大小时进行的对齐规则可以看出Class的地址转成64位二进制时指针的后三位都是0,右移3位后再存进 isa 的47位的shiftcls,这样节省可内存的空间。通过打印Class的地址值可以看出47位的内存是可以存放的下一个右移3位的Class的地址值的,并不一定要64d的的存储空间。

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

一个对象调用它的实例方法,其实是先通过isa指针找到类对象的内存地址,通过访问其成员 class_data_bits_t bits 获取到实例方法。类对象调用的类方法其实与实例方法的原理是一样的通过isa找到元类( MetaClass )的内存地址,通过访问 MetaClassclass_data_bits_t bits 获取类方法进行调用。两者的方法查找都是在当前类中如果找不到对象的方法就会沿着 superClass 指针往父类的方法里面查找,直到找到位置,如果找不到就会进行方法的动态解析或者消息的转发,还没解决就会抛出找不到方法的错误。下面的图片很好的展示了这实例对象( objc )与其 类对象( Class )、元类对象( MetaClass )、父类( SuperClass )之间的关系。

分析oc对象的内存结构及其创建过程

细心观察上面的图片,其实可以发现几个注意点

  1. 根类对象(图中的RootClass)的superclass指针最终指向nil , 其isa指向根元类(图中的Root MetaClass)
  2. 根元类(图中的Root MetaClass)的isa指针指向的是其本身,superclass指针指向根类(图中的RootClass) 3.所有的MetaClass的isa都是指向同一个对象,那就是RootMetaClass

那么分析清楚了这几个对象间的关系后,接下来开始分析对象实例方法究竟是如何初始化的。

class_data_bits_t 分析

源码定义

struct class_data_bits_t {

    uintptr_t bits;
    
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}
复制代码

其实 class_data_bits_t bit 就是一个指针而已。真正的方法存储在 data() 返回的指针指向的那块内存中。该内存其实是一个 class_rw_t 的类型值。继续分析返回的 class_rw_t * 类型值

struct class_rw_t {

    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;//通过编译的类信息(只读不能写入)

    method_array_t methods; //方法列表
    property_array_t properties;//属性列表
    protocol_array_t protocols; //协议列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; //类名
    method_list_t * baseMethodList; //编译其已经确定的方法(没有分类方法)
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;//属性列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};
复制代码

通过查阅资料及源码,加上实践验证可以知道,我们平是定义的类属性或者方法,经过编译器的处理转成C或C++代码其实底层由多种结构体和函数共同协作生成包含只读方法和属性的 struct class_ro_t 类型变量。就用上面的Person类作为例子。通过clang编译器指令转成 c++ 代码后我摘取一些重要片段

//包含对象属性信息的变量 , 用于初始化 _class_ro_t 变量
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	2,
	{{"age","TQ,N,V_age"},
	{"name","T@\"NSString\",C,N,V_name"}}
};

//包含对象成员变量信息的变量 , 用于初始化 _class_ro_t 变量
static struct /*_ivar_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count;
	struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_ivar_t),
	2,
	{{(unsigned long int *)&OBJC_IVAR_$_Person$_age, "_age", "Q", 3, 8},
	 {(unsigned long int *)&OBJC_IVAR_$_Person$_name, "_name", "@\"NSString\"", 3, 8}}
};

//包含对象方法信息的变量 , 用于初始化 _class_ro_t 变量
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	5,
	{{(struct objc_selector *)"say", "v16@0:8", (void *)_I_Person_say/**方法对应的函数指针*/},
	{(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
	{(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_}}
};

// _class_ro_t 类型变量
tatic struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	0, __OFFSETOFIVAR__(struct Person, _age), sizeof(struct Person_IMPL), 
	0, 
	"Person",
	(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
	0, 
	(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Person,
	0, 
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person,
};

//下面几个方法都是为初始化 Person 类做准备工作
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
	0, // &OBJC_METACLASS_$_Person,
	0, // &OBJC_CLASS_$_NSObject,
	0, // (void *)&_objc_empty_cache,
	0, // unused, was (void *)&_objc_empty_vtable,
	&_OBJC_CLASS_RO_$_Person,
};
static void OBJC_CLASS_SETUP_$_Person(void ) {
	OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
	OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
	OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
	OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}

static void OBJC_CLASS_SETUP_$_Person(void ) {
	OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
	OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
	OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
	OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}

复制代码

从上面的源码可以看出在程序编译完成后类的信息已经被编译器处理完了大部分的工作,剩下小部分工作是通过 runtime 机制来处理的。

runtime机制处理类信息

objc源码中有一个函数 realizeClass ,负责处理编译信息及运行时信息的转接返回类的真实结构体。我简化下函数留下处理 _class_ro_tclass_rw_t 关系的源码

static Class realizeClass(Class cls){
    
    ro = (const class_ro_t *)cls->data();
    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
    
    return cls;
}
复制代码

从上面的源码可以看出,类在未经过调用函数 realizeClass(Class cls) 前, objc_class 结构体方法调用的 class_rw_t *data() 方法返回的其实是 class_ro_t 类型的指针,在经过 realizeClass 处理后才把 class_rw_t 类型变量创建好,并把原来的 class_ro_t 指针赋值给 class_rw_t 变量的 ro 成员变量,并赋值给cls。

接下来我们通过 objc 源码验证一下

在调试是先获取 [Person Class] 的地址,接着 realizeClass 开始前打断条件断点(cls == Person地址值)配合lldb指令,通过指针的偏移获得 class_data_bits_t 的值,再通过其调用data()方法获得对应的指针,通过把改指针强转为 class_ro_t 类型打印出来的值符合之前定义Person类的信息。调试过程如下图

分析oc对象的内存结构及其创建过程

逻辑分析可能有点乱,见谅 ^__^ !


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构与算法分析

数据结构与算法分析

韦斯 (Mark Allen Weiss) / 机械工业出版社 / 2013-2-1 / 79.00元

本书是国外数据结构与算法分析方面的经典教材,使用卓越的Java编程语言作为实现工具讨论了数据结构(组织大量数据的方法)和算法分析(对算法运行时间的估计)。 随着计算机速度的不断增加和功能的日益强大,人们对有效编程和算法分析的要求也不断增长。本书将算法分析与最有效率的Java程序的开发有机地结合起来,深入分析每种算法,并细致讲解精心构造程序的方法,内容全面、缜密严格。 第3版的主要更新如......一起来看看 《数据结构与算法分析》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换