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

面向对象的编程语言中有经典的话:万物皆对象。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对象的内存数据 代码如下:

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的值,如图所示

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

的值,如图所示

对象的创建过程

研究类的初始化过程肯定是通过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)之间的关系。

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

  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类的信息。调试过程如下图

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

猜你喜欢

转载自juejin.im/post/5c8391f46fb9a049b7812d50