对象本质 & isa解析

之前我们分析了alloc流程和内存对齐算法, 今天我们来研究对象的本质,并对isa进行详细解析。

对象的本质探究方法 - Clang

Clang是一个由苹果主导编写,基于LVVM的C/C++/OC编译器,通过Clang的rewrite命令可以将OC代码还原为源码
新建一个工程,在main函数中创建一个类,随意添加个属性:


image.png

打开终端,cd到文件所在目录,执行clang -rewrite-objc main.m -o main.cpp

  • 打开main.cpp文件,直接搜索FCPerson类名即可直接定位到我们要关注的位置,可以直接看到对象的本质是结构体
#ifndef _REWRITER_typedef_FCPerson
#define _REWRITER_typedef_FCPerson
typedef struct objc_object FCPerson;
typedef struct {} _objc_exc_FCPerson;
#endif

struct FCPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_fcName;
};
  • 其中objc_object就是NSObject的底层写法,_fcName是我们添加的属性,NSObject_IMPL就是isa:
struct NSObject_IMPL {
    Class isa;
};
  • 在下面还可以看到FCPerson的setter和getter方法:
static NSString * _I_FCPerson_fcName(FCPerson * self, SEL _cmd) { 
      return (*(NSString **)((char *)self + OBJC_IVAR_$_FCPerson$_fcName));
 }
static void _I_FCPerson_setFcName_(FCPerson * self, SEL _cmd, NSString *fcName) {
      (*(NSString **)((char *)self + OBJC_IVAR_$_FCPerson$_fcName)) = fcName; 
}

setter和getter在内存中读取属性的原理是什么?(*(NSString **)((char *)self + OBJC_IVAR_$_FCPerson$_fcName))是怎么找到fcName这个属性值的?
在取值时,系统只知道对象首地址,并不知道具体属性的地址信息。方法中(char *)self其实就是FCPerson对象的首地址,而OBJC_IVAR_$_FCPerson$_fcName则是fcName属性距离首地址的偏移量,系统就是通过首地址+偏移量的方式进行属性的读写的。

  • 接下来我们还可以看到很多类相关的结构体,method,protocol,ivar,class_ro_t,class_t
    等等,我也会在以后的文章中为大家做详细介绍,到目前为止,我们已经知道了对象的本质是结构体。

  • 之前在介绍alloc流程时,提到过在_class_createInstanceFromZone中可以看到将isa与cls绑定的代码:

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);
    }

Isa解析

通过initIsa进入到objc_object::initIsa方法中,首先看到的就是isa的初始化方法isa_t newisa(0);,具体看一下isa_t的内部实现,删除掉多余代码后:

union isa_t {
    uintptr_t bits;

private:
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

可以看到isa_t是一个联合体(联合体中成员互斥,即bits,cls,struct{ISA_BITFIELD}只能是其中一种),依次分析三个成员变量:

  • 苹果把isa根据需要进行了区分,苹果提出了TaggedPointer和NonpointerIsa。对于小对象采用TaggedPointet方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。

  • 对于NSDateNSNumber这样的小对象存储的值,绝大多数情况并不会大于20亿这个量级。如果采用指针、堆内存的方式,那势必会造成内存的浪费和性能损耗。苹果采用将value值直接存储在isa_t中的uintptr_t bits;上,并且用一些特殊标识来标明此isa是TaggedPoint类型的。这样用isa就存储了值,而不需要在堆上分配内存再去存储值。要知道堆内存的分配、释放及访问,要比栈内存慢很多的。

  • objc_object::initIsa方法中,可以看到如果不是nonpointer则会直接进行cls赋值newisa.setClass(cls, this);

  • struct{ISA_BITFIELD}ISA_BITFIELD是一个宏定义:

    image.png

    objc_object::initIsa方法中,如果是nonpointer的话,则会依次对ISA_BITFIELD中的属性进行赋值:

#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;

因此,如果是nonPointerIsa的话,isa已经不再是一个单纯只想cls的指针了,携带了很多附加信息。

遗留问题

通过探究源码得知,initIsa方法写死的创建非nonPointerIsa, initInstanceIsa和initClass创建的是nonPointerIsa,具体原因以后研究完会继续补充~

你可能感兴趣的:(对象本质 & isa解析)