Objective-C 原理-一类型设计

458 阅读9分钟

编译器会把 OC(Objective-C) 代码重写(-rewrite-objc)为 C 和 C++ 代码,即把 OC 的类、block、属性、方法、协议、实例变量、类别、实例变量列表、方法类别、属性列表等表达为 C 结构体,通过运行时库的消息机制实现 OC 的动态特性,完成从 C 和 C++ 到 OC 语言特性的跃变。这是一个以低级概念(结构体)为基础,通过对函数调用流程的逻辑构建形成了以消息发送为基础的编码 API;通过 isa 指针构建数据与代码之间的内聚与互服务关系,通过 superclass 指针构建结构体之间的继承关系,形成二维的网状结构,具有丰富表现能力。

结构体、isa、superclass 和函数动态查找,这些 C 和 C++ 的概念与构造块,在运行时库和代码重写机制的加持下,最终以类和消息发送两个高级概念提供给程序设计人员,完全屏蔽掉了底层实现机制的复杂性。

本文不涉及函数动态查找的内容,只研究类和类中各元素与结构体的映射关系、对象的类指针 isa 和 父类指针 superclass 构造的对象体系和类体系结构、isa 和 superclass 联合体 union isa_t 的内部机制。

对象的本质

万物皆对象,实例对象、类对象和元类对象,都是用结构体表达的,对象具有相同的头部结构即一个 isa 字段。在 OC 中一个以 isa 开始的堆上的连续内存区域就是一个对象,8字节长的 isa 字段的内部各个 bit 的定义就是对象的本质,它是实例对象与实例类对象、类对象与元类对象、元类对象和根元类对象之间的桥梁。

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

struct objc_object {
private:
    isa_t isa;
public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ...
}

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

实例对象之间互相独立,是以类对象为模版,请求系统开辟出来的一块连续可读写的内存区域,一个分配在堆上的结构体,其首个字段 isa 指向类对象,即存储类对象的内存起始地址。同理,类对象的 isa 指向元类对象,元类对象的 isa 指向根元类,根元类的 isa 指向自己。

类 Class 和 对象 id 本质上是一样的,都是在堆上以都是 **isa **开始的一个占用连续内存区域的结构体

继承机制比较简单,不同的类体系(如NSObject 和 NSProxy)中类和元类最终的父类都是根类(NSObject 或NSProxy)

模型重写

@interface NSObject1 : NSObject
@end
@implementation NSObject1
@end 

clang -rewrite-objc main.m -o main.cpp 重写后:

#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
struct NSObject_IMPL {
	Class isa;
};

#ifndef _REWRITER_typedef_NSObject1
#define _REWRITER_typedef_NSObject1
typedef struct objc_object NSObject1;
typedef struct {} _objc_exc_NSObject1;
#endif
struct NSObject1_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
        ...
};

所有 OC 类重写后对应的类型都是 struct objc_object 。类就是一个 objc_object 类型结构体体,是一个对象,通过 typedef 为 objc_object 定义的别名。但是 objc_object 结构体的字段是固定不可变的,无法表述类中定义的实例变量、属性、协议等等元素。这一个定义丢失了类对数据和行为封装后所有获得的丰富语意,一定需要其它的结构体来承载数据与行为的封装所代理的丰富性。

OC 层面的类经过重写后被二分抽象化。类型被 OC 运行时预定义的 objc_object 结构体的别名类型;数据与行为通过动态产生的多个结构体承载,即 _IMPL 结构体。系统用 objc_object 的 isa 赋予各个别名类型以丰富的语意,为 _IMPL 结构体封装的数据提供行为规范,构建成了以类型封装的行为和 *_IMPL 结构体封装的数据的二维体系结构

isa 结构

万物皆对象,对象都有自己模版即类或元类。对象和其模版之间通过 isa 构建的指向关系是构建高效应用的关键。

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

实例对象 isa 结构分析

实例对象封装数据,首字段存 isa 存储类对象在内存中的地址,后续字段存储实例变量

@interface NSObject1 : NSObject
@property (copy) NSString *name;
@end
@implementation NSObject1
@end

NSObject1 * obj = [[NSObject1 alloc] init];
obj.name = @"name_123456";

(lldb) x/4gx obj
0x101718580: 0x001d800100001355 0x0000000100001048
0x101718590: 0x0000000101718660 0x00000001017188a0

**类对象内存地址 **

(lldb) p/x 0x001d800100001355 &  0x00007ffffffffff8ULL
(unsigned long long) $44 = 0x0000000100001350
(lldb) po 0x0000000100001350
NSObject1

x/4gx 0x0000000100001350 //显示元类对象内存布局

重复上述过程,可从对象追溯到类对象,到元类对象,... ,直到根元类对象

元类对象也在堆上,紧邻类对象在其之上

类对象 name 属性

(lldb) po 0x0000000100001048
name_123456

isa bit 结构 分析

isa_t 是Class cls、uintptr_t bits、struct { ISA_BITFIELD;} 三个属性的联合体,同样的数据对外展现出两三种完全不同的概念系,其中 Class cls 以数据的整体性为面向对象编程范式提供类信息,uintptr_t bits 提供无结构无概念的纯 bit 信息,struct { ISA_BITFIELD;} 以结构化的 bit 信息为运行时库提供紧凑的数据存储与查询服务,是分析和学习的重点。

指针优化(nonpointer: 1)

有两种查找类的方式:

  1. SUPPORT_PACKED_ISA 通过 isa_t 中存储的内存位置信息直接寻址类对象

  2. SUPPORT_INDEXED_ISA 在 isa_t  存储类对象在表中的下标,通过下标找到类对象。

    ALWAYS_INLINE Class & classForIndex(uintptr_t index) { assert(index > 0); assert(index < (uintptr_t)objc_indexed_classes_count); return objc_indexed_classes[index]; }

x86_64 和 arm64 都采用 PACKED,下面是 x86_64 架构中的 ISA_BITFIELD 都格式(采用的测试环境);其它架构都采用 INDEXED 方式查找类对象。

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   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 deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

PACKED 和 INDEXED 都采用指针优化,即 SUPPORT_NONPOINTER_ISA === 1,isa_t 都经结构化把每一个 bit 赋予了不同的含义。

这里 SUPPORT_NONPOINTER_ISA 存储在 ISA_BITFIELD 中的 nonpointer,存储在类对象的 isa 字段中

// Define SUPPORT_NONPOINTER_ISA=1 on any platform that may store something
// in the isa field that is not a raw pointer.
#if !SUPPORT_INDEXED_ISA  &&  !SUPPORT_PACKED_ISA
#   define SUPPORT_NONPOINTER_ISA 0
#else
#   define SUPPORT_NONPOINTER_ISA 1
#endif

标号指针(: 1)

标号指针的标志位存储在最低位 bit。标号指针是一个伪指针,id 变量中不存储内存地址值,存储对象的值、类索引信息和其它标号信息。

标号指针是一个伪指针,是一个INDEX对象(32位系统中存储在 objc_indexed_classes 中的和64位中存储在 objc_tag_ext_classes 和 objc_tag_classes 中的)。标号指针是一个伪指针,是真真切切的对象,是在常规指针的空间内存储对象的机制。

用指针寻址对象十分灵活(实例对象的指针寻址实例对象,isa 指针寻址类对象或元类对象),同时中间层也消耗非常多的系统资源。在 32 位系统中,指针中间层引入的消耗对于大多数数据类型是合理的可以接受的,如对于 int 类型的 NSNumber来说,指针占4字节、isa 4字节、数据 4 字节,一共12字节,有4字节的浪费。对于 64 系统,中间层引入的消耗有时是不必要的,指针 8 字节、isa 8 字节 、数据 8 字节,一共 24 字节,64 位系统比 32 位系统多浪费12字节。

苹果在 iPhone5s 搭载的 64 位系统中提出 Tagged Pointer 概念用以优化小对象(如NSNumberNSDate)对内存的使用方式:把类对象的索引信息、信息和其它标号信息直接存储原来的指针变量中,其中最低位赋值为1,用以标记当前指针是一个标号指针。彻底消除指针间接层的浪费、小值域类型对多余二进制位的浪费、对象指针编码浪费

NSNumber *num2 = @(16);
(lldb) p num2
(__NSCFNumber *) $4 = 0x97f9bc34e0749a27 (int)16
(lldb) x num2
error: memory read failed for 0x97f9bc34e0749a00

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA(); //非
    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {//256 个扩展标号位
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {//7 个扩展标号位
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}

关联存储(has_assoc: 1)

类对象在注册到运行时系统后,对象的内存模型就再无法变更。关联存储是运行时提供的动态修改对象关系图的强大机制,补充对象内存模型的在可扩展性方面的不足。has_assoc 是一个标志位,用来标志当前对象是否有关联的对象。

关联对象通过当前对象的隐藏指针,把关联的对象存储到一个全局的哈希表中,使用 5 种关联策略动态地构建对象图

AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);

objc_AssociationPolicy                      modifier
OBJC_ASSOCIATION_ASSIGN                 assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC       nonatomic, strong 
OBJC_ASSOCIATION_COPY_NONATOMIC         nonatomic, copy
OBJC_ASSOCIATION_RETAIN                 atomic, strong
OBJC_ASSOCIATION_COPY                   atomic, copy

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}

在对象析构过程中,如果检测到 has_assoc == 1 ,会解除通过关联对象而构建起来的对象图。

关联对象不支持 weak

析构函数(has_cxx_dtor: 1)

在 ARC 下对象的 has_cxx_dtor === 1 ,编译器自动插入 .cxx_destructor 析构函数。当前对象通过 dealloc 析构时会通过 objc_destructInstance 调用 object_cxxDestructFromClass 按从子类到父类的顺序调用 .cxx_destructor, 释放关联的实例对象。

static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);
    // Call cls's dtor first, then superclasses's dtors.
    for ( ; cls; cls = cls->superclass) {
        if (!cls->hasCxxDtor()) return; 
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            if (PrintCxxCtors) {
                _objc_inform("CXX: calling C++ destructors for class %s", 
                             cls->nameForLogging());
            }
            (*dtor)(obj);
        }
    }
}

编译器为ARC代码自动插入 .cxx_construct 和 .cxx_destructor ,在非 ARC 下只用于 C++对象的构建和析构

类对象指针(shiftcls)

可以通过 (0x001d800100001355 & ISA_MASK)>>>3  得到类对象指针或类对象索引。

在 arm64 架构上,shiftcls  占用 33 位;在 x86_64 架构上,shiftcls 占用 44 位;其它架构上,shiftcls 占用 15 位 。如果是扩展标号指针占用 8 bit ,常规标号指针占用 4 bit,节省下来的 bit 用于存储更多的数据

类对象指针(magic )

在 arm64 架构上,magic 占用 6 位,magic == 65 || 0;

在 x86_64 架构上,magic 占用 6 位,magic == 65|| 0;

其它架构上,magic 占用 4 位,magic == 15 || 0。

固定值,用于标志当前对象是否初始化。

这里的 magic 与 自动释放池的 struct magic_t 没有关系

弱引用标志(weakly_referenced: 1)

当给 weak 属性赋值时,系统生成的 setter 会自定调用 setWeaklyReferenced_nolock 设置被弱引用的对象 isa 的 weakly_referenced bit ,标志属性对象被其它对象引用了,用以在析构的的时候自动清除相关对象上的弱属性为 nil。

inline void
objc_object::setWeaklyReferenced_nolock()
{
 retry:
    isa_t oldisa = LoadExclusive(&isa.bits);
    isa_t newisa = oldisa;
    if (slowpath(!newisa.nonpointer)) {
        ClearExclusive(&isa.bits);
        sidetable_setWeaklyReferenced_nolock();
        //nonpointer == 0 时,即没有指针优化时,弱引用标号存储在边表上
        return;
    }
    if (newisa.weakly_referenced) {
        ClearExclusive(&isa.bits);
        return;
    }
    newisa.weakly_referenced = true;//设置弱引用标志位在 
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}

void 
objc_object::sidetable_setWeaklyReferenced_nolock()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
}

在引用计数超过 extra_rc 的存储能力时,引用计数会转移部分值到边表中的哈希表 refcnts 存储。最低的两位即保留用来标志当前对象是否被弱引用、是否正在被析构。引用计数是以 4 为一个单位存储在边表的哈希表 refcnts 中。

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
    ...
}

析构标志(deallocating: 1)

请参考弱引用标志(weakly_referenced: 1)

引用计数溢出存储(has_sidetable_rc: 1)

在指针优化下,引用计数溢出直接存储在 isa 的 has_sidetable_rc bit 中即可,否则引用计数直接存储在边表的引用计数哈希表中,不需要标志引用计数是否溢出。

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    ....
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {//非优化指针,不需要标记引用计数是否溢出
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        ...
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        if (slowpath(carry)) {//引用计数 extra_rc溢出,标记溢出标志
            ...
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);//转移部分计数到边表的哈希表中
    }
    ...
    return (id)this;
}

局部引用计数器(extra_rc)

请参考引用计数溢出存储(has_sidetable_rc: 1)

类内存结构分析

类对象存储类的元信息,即实例变量信息、属性信息、方法信息、协议信息等。

在类对象的内存结构中,isa 存储了到原类的指针,其首地址 0x100001350 就存储在对象内存结构

struct objc_class : objc_object {
    // Class ISA;       //占用8字节
    Class superclass;   //占用8字节
    cache_t cache;             // formerly cache pointer and vtable 占用16字节
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
}

(lldb) x/4gx NSObject1.class / x/6gx obj.class
0x100001350: 0x001d800100001329 0x0000000100afd140
0x100001360: 0x0000000100f074d0 0x0000000400000007
0x100001370: 0x0000000101716834 0x0000000100afd0f0

0x100001350 == $44 == ****0x0000000100001350 ,上面计算的类对象内存地址是正确的。

0x001d800100001329 是 isa 值,存储元类对象地址和类对象相关的各种信息,如extra_rc、类地址等。详细信息可参考 isa bit 结构分析

(lldb) po 0x0000000100afd140
NSObject 

0x0000000100afd140 存储  superclass 指针

(lldb) p (cache_t *)0x100001360
(cache_t *) $33 = 0x0000000100001360
(lldb) p *$33
(cache_t) $34 = {
  _buckets = 0x0000000100f074d0
  _mask = 7
  _occupied = 4

0x100001360: 0x0000000100f074d0 0x0000000400000007 存储 cache_t ,方法缓存。

下面 0x100001370: 0x0000000101716834 0x0000000100afd0f0 是 class_rw_t 内存结构

(lldb) p (class_data_bits_t *)0x100001370
(class_data_bits_t *) $26 = 0x0000000100001370
(lldb) p $26->data()
(class_rw_t *) $27 = 0x0000000101716830
(lldb) p *$27
(class_rw_t) $28 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100001100
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100001148
        arrayAndFlag = 4294971720
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000011c0
        arrayAndFlag = 4294971840
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSDate
  demangledName = 0x0000000100000ea6 "NSObject1"
}

class_ro_t 内存结构

(lldb) p (class_ro_t *)0x0000000101716838
(class_ro_t *) $39 = 0x0000000101716838
(lldb) p *$39
(class_ro_t) $42 = {
  flags = 4352
  instanceStart = 1
  instanceSize = 4424
  reserved = 1
  ivarLayout = 0x00000001000011c0 "\x10"
  name = 0x0000000000000000 <no value available>
  baseMethodList = 0x0000000000000000
  baseProtocols = 0x00007fffa34dfea8
  ivars = 0x0000000100000ea6
  weakIvarLayout = 0xbac3bac304bfc34c <no value available>
  baseProperties = 0x4000000010171600
}

结论

本文从 OC 向 C 和 C++ 的映射入手,回答了什么是 OC 对象,较详细的分析了 isa 指针的各个 bit 的含义及其在运行时的行为,使得 OC 底层的原理中的部分清晰的展现出来。同时,我们打印出类对象的内存结构,使我们对类对象有了一个初步感性的认识,关于类对象的详情分析请参考下一片文章《Objective-C 原理-二类对象元素管理》,其中详细阐述实例变量、属性、实例方法、类方法、协议、扩展、类别、类属性等类元素是如何管理的。