OC底层原理探索之OC类原理

316 阅读4分钟

对象的本质

在main中新建一个Teacher类,cd到当前的main.m文件,输入命令clang -rewrite-objc xxx.m ,如果此时报以下错误 image.png 请重新输入clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m此时就得到了一个.cpp文件,打开搜索直接定位到Teacher

struct Teacher_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

我们发现对象在底层的本质就是结构体,为了看的更清晰,我们在Teacher中新增一个name属性,重新生成.cpp文件,得到

struct Teacher_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_name;
};

我们发现有一个隐藏属性NSObject_IMPL,全局搜索查看下,得到原来是一个isa

struct NSObject_IMPL {
	Class isa;
};

同时我们也发现了这些typedef

typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct objc_object Teacher;

可以推出OC中NSObject在C++的底层其实就是objc_object;OC中的Class在底层是objc_class; 我们也看到了Teacher的set和get方法,发现了两个隐藏参数cls和_cmd

static NSString * _I_Teacher_name(Teacher * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Teacher$_name)); }
static void _I_Teacher_setName_(Teacher * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_Teacher$_name)) = name; }

(char *)self + OBJC_IVAR_$_Teacher$_name))首地址+平移

nonPointerIsa分析

上篇文章讲到了在alloc开辟内存空间之后,有一个isa和类的绑定obj->initIsa(cls),顺着方法链走,我们找到了一个isa_t,它是一个联合体union

union isa_t {
    isa_t() { } //构造方法
    isa_t(uintptr_t value) : bits(value) { } //构造方法

    uintptr_t bits;

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)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
	...
#endif
	...
};

我们知道指针占用8字节也就是64位,这里64位不单单只是一个指针,实际上存储的也有记录是否正在释放、weak、引用计数、关联对象、析构函数这些都是和类息息相关的东西。所以就出现了nonPointerIsa,它不在是一个简简单单的指针,要看里面具体64位有什么,只有ISA_BITFIELD与之相关,找到它在__x86_64__下的定义

#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8

相加刚好等于64位,默认创建的类都是nonPointerIsa,这里面的东西跟类最相关的是shiftcls,回到上级继续看obj->initIsa(cls)方法,如果是!nonpointer,直接赋值,如果是nonpointer需要给联合体里面的位域赋值(也就是上面介绍的ISA_BITFIELD ),默认生成的类都是nonpointer,赋值的方法newisa.setClass(cls, this);

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 {
  		...
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

isa的赋值过程

首先我们知道x/4gx p的isa(0x011d800100008275)指向当前的类信息,通过p/x Person.class我们得到当前的类地址(0x0000000100008270),那么这个指向的过程是怎么设计的呢?

image.png

这里就用到了上面提的ISA_BITFIELD,我们知道iOS是小端模式,ISA_BITFIELD的排列方式如下,要得到shiftcls需要右移3位,左移20位,再右移17位就能拿到。 image.png 我们代码来验证一下: image.png

补充

noopointer: 表示是否对isa指针开启指针优化,0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等 has_assoc:关联对象标志位,0没有,1存在 has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针。 magic:⽤于调试器判断当前对象是真的对象还是没有初始化的空间 weakly_referenced:对象是否被指向或者曾经指向⼀个 ARC 的弱变量, 没有弱引⽤的对象可以更快释放。 deallocating:标志对象是否正在释放内存 has_sidetable_rc:当对象引⽤计数⼤于 10 时,则需要借⽤该变量存储进位 extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1, 例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10, 则需要使⽤到 has_sidetable_rc。