iOS 对象isa指针的底层探索

引言

众所周知,oc是面向对象编程,最重要的一个概念就是,最终OC中的类都要编译成c++的,那么OC重的类在c++的底层是怎么呈现的呢?
打开#import<objc/objc.h>文件

 /// An opaque type that represents an Objective-C class.
//一个不透明类型用于表示Objective-C类
 typedef struct objc_class *Class;

 /// Represents an instance of a class. 
  //表示类的实例
 struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
 };

/// A pointer to an instance of a class.
typedef struct objc_object *id;

从上面代码可以看出,OC中的类(Class)编译后在c++中是以 struct objc_class呈现的,我们众所周知的NSObject类编译后在c++中是一个 struct objc_object,里面只有一个成员变量,即Class类型的变量isa
在这里,我们也明白了为什么oc中的大部分对象可以用 id接收,因为id本身就是一个objc_object类型的结构体指针
那么objc_objectobjc_class是什么关系呢?其实我们自定义(继承于NSObject)的类,编译成c++对应的就是objc_class

查看源码runtime.h,发现结构体objc_class如下定义,这个是老版本OBJC1_(系统低于10.15)时候的定义

struct objc_class {
   Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class                              OBJC2_UNAVAILABLE;
const char * _Nonnull name                               OBJC2_UNAVAILABLE;
long version                                             OBJC2_UNAVAILABLE;
long info                                                OBJC2_UNAVAILABLE;
long instance_size                                       OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

查看源码objc-runtime-new.h,结构体objc_class在新版本OBJC2_定义如下,isa是继承的父类objc_object里面的isa

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
...
}

下面我们重点研究isa,问题来了,想要研究isa,该从哪里入手?

我们前面研究了对象alloc流程的底层源码,方法调用顺序为:alloc--->_objc_rootAlloc--->callAlloc--->_objc_rootAllocWithZone--->_class_createInstanceFromZone.

 static ALWAYS_INLINE id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                          int construct_flags = OBJECT_CONSTRUCT_NONE,
                          bool cxxConstruct = true,
                          size_t *outAllocatedSize = nil)
{
 ASSERT(cls->isRealized());

// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;

size = cls->instanceSize(extraBytes); //计算对象内存大小
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (zone) {
    obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
    obj = (id)calloc(1, size); //calloc 分配堆内存
}
if (slowpath(!obj)) {
    if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
        return _objc_callBadAllocHandler(cls);
    }
    return nil;
}

if (!zone && fast) {
    obj->initInstanceIsa(cls, hasCxxDtor);//初始化对象的isa
} else {
    // Use raw pointer isa on the assumption that they might be
    // doing something weird with the zone or RR.
    obj->initIsa(cls);
}

if (fastpath(!hasCxxCtor)) {
    return obj;
}

construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}

代码中我们可以清晰的看到:_class_createInstanceFromZone做了三件事情。

  • size = cls->instanceSize(extraBytes); //计算对象内存大小
  • obj = (id)calloc(1, size); //calloc 分配堆内存
  • obj->initIsa(cls);//初始化对象的isa

因此,initIsa就顺利成章的成为了我们研究isa的切入点。isa的初始化代码如下:

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

initIsa代码看出,该方法做了3件事。

1.初始化一个isa_t类型的newisa
2.newisa.bits位域的初始化
3.将newisa赋值给isa,完成isa的初始化工作

那么isa_t到底是什么类型?

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

bool isDeallocating() {
    return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
    extra_rc = 0;
    has_sidetable_rc = 0;
}
#endif

void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};

可以看出,isa_tunion(联合体),使用union也是基于内存优化,isa指针8字节,即64位,已经足够存储很多的信息了,由于union的特性互斥性(内存共用),极大得节省了内存
提供了两个成员变量,clsbits,由union的互斥性知只能存在一种形式初始化,那么根据什么判断呢?

image.png

不难看出,根据nonpointer判断用哪种方式初始化。

还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD是一个宏定义。

image.png
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     ...
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                      \
    uintptr_t nonpointer        : 1;                                       \
    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 unused            : 1;                                       \
    uintptr_t has_sidetable_rc  : 1;                                       \
    uintptr_t extra_rc          : 19
   #     define RC_ONE   (1ULL<<45)
  #     define RC_HALF  (1ULL<<18)
  #   endif

  # elif __x86_64__
  #   define ISA_MASK        0x00007ffffffffff8ULL
  #   define ISA_MAGIC_MASK  0x001f800000000001ULL
  #   define ISA_MAGIC_VALUE 0x001d800000000001ULL
  #   define ISA_HAS_CXX_DTOR_BIT 1
  #   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
 #   define RC_ONE   (1ULL<<56)
 #   define RC_HALF  (1ULL<<7)

# else
 #   error unknown architecture for packed isa
 # endif

解释: : 1;: 44;分别表示该成员占1位44位
下面我们来看不同位域的作用:

  • nonpointer:是否开启指针优化0:纯isa指针只包含类对象地址1isa中包含了类对象地址、类信息、对象的引用计数等
    还得回到_class_createInstanceFromZone

      if (!zone && fast) {
      obj->initInstanceIsa(cls, hasCxxDtor);
      } else {
      obj->initIsa(cls);
       }
    

obj->initInstanceIsa(cls, hasCxxDtor);调用的initIsanonpointer传的值为true
obj->initIsa(cls);调用的initIsanonpointer传的值为false

image.png

nonpointer == NO时候,走if判断,即只对newisa的class设置。
nonpointer == YES时候,走else判断,对newisa多项赋值。

  • has_assoc:是否设置过关联对象,如果没有,释放时会更快。
  • has_cxx_dtor:是否含有c++的析构函数。
  • shiftcls:存储着ClassMeta-Class对象的内存地址信息,是我们研究的重点(arm64:33位,x86_64:44位)
  • magic:判断当前对象是否进行了初始化
  • weakly_referenced:是否被弱引用指向过。
  • has_sidetable_rc:判断是否需要用sidetable去处理引用计数(extra_rc的大小影响整个变量)
  • extra_rc:里面存储的值是引用计数器减1

这里重点提到的是shiftcls,那么shiftcls存的地址信息和我们的地址有什么关系呢?

image.png

从上面可以清晰的看到,shiftcls的地址 = WJPerson类地址 >> 3(向右移动了三位)得到的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容