【objc4-NSObject】(二):isa与类

708 阅读17分钟

都知道isa指针是指向类对象,除了类对象,还存储了哪些信息呢?

联合体&位域

在了解联合体之前,首先我们要了解一下联合体与位域。因为isa的数据结构就是由这俩组成的。runtime的有很多数据结构都是基于联合体。

联合体

联合体(共用体)的所有成员占用同一段内存,修改一个成员会影响其余所有成员。 说到联合体(union) 的意义就不得不提结构体(struct), 因为这两者定义形式完全相同。

两者差别:结构体的各个成员会占用不同的内存块,互相之间没有影响;而联合体的所有成员占用同一段内存块,修改其中一个成员会对整个内存块保存的值产生影响。

union test_a {
    long a;
    int b;
    char c[9];
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        union test_a test = {0};
        NSLog(@"\ntest_a size is %ld", sizeof(test));
        test.a = 97;
        NSLog(@"\na is : %ld\nb is %d \nc is %s", test.a, test.b, test.c);
        test.b = 122;
        NSLog(@"\na is : %ld\nb is %d \nc is %s", test.a, test.b, test.c);
    }
    return 0;
}

//输出:
2021-09-02 22:30:33.201485+0800 objc[73997:948863]
test_a size is 16
2021-09-02 22:30:34.658421+0800 objc[73997:948863]
a is : 97
b is 97
c is a
2021-09-02 22:30:34.658615+0800 objc[73997:948863]
a is : 122
b is 122
c is z
Program ended with exit code: 0

上面定义了一个test_a的联合体,有三个成员变量。先看一下联合体的大小,共用一块内存,最大的就是成员变量c,占用9个字节,又遵循对齐原则,大小需要能被其他成员变量整除,所以当前test_a需要16字节。当赋值a之后,可以看到a、b、c的值都发生了变化,都变成了97(c的值变成了a,可查看ASCII)。

大小端序

使用联合体可查看当前系统的大小端序,代码如下:

    union data{
      int a;  //4 bytes
      char b; //1 byte
    } ;
  
    data.a = 1; //占4 bytes,十六进制可表示为 0x 00 00 00 01

    //b因为是char型只占1Byte,a因为是int型占4Byte
    //所以,在联合体data所占内存中,b所占内存等于a所占内存的低地址部分 
    if(1 == data.b)
    { 
        //走该case说明a的低字节,被取给到了b,即a的低字节存在了联合体所占内存的(起始)低地址,符合小端模式特征
        printf("Little_Endian\n");
    }
    else
    {
        printf("Big_Endian\n");
    }

iOS就是小端模式。

位域

有些信息在存储时,并不需要占用一个完整的字节,而只需要一个或几个二进制位即可;比如:在存放一个开关变量时,只有0和1两种状态,只需要使用一个二进制位即可存储;为了节省存储空间,C语言提供了一种数据结构,称为"位域"或"位段";所谓"位域"就是把一个字节中的8个二进制位划分为几个不同的区域,并说明每个区域的二进制位数;每一个位域都有一个位域名,允许程序员在程序中按照位域名进行访问;这样就可以把几个不同的对象用一个字节的二进制位域来表示;

定义如下:

struct 位域结构名
{
     类型说明符1 位域名1:位域长度1; //最低位;
     类型说明符2 位域名2:位域长度2; //次低位;
     类型说明符3 位域名3:位域长度3;
     ......
     类型说明符N 位域名N:位域长度M; //最高位;
};

可以看到位域的第一个成员变量是处于内存中低位的, 最后一个是处于高位的。

ISA

objc4源码中可以找到以下代码

struct objc_object {
private:
    isa_t isa;
    ...... //剩下的就是函数了
};

结构体objc_object就是用来表示OC对象的,isa_t就是用来存储isa信息的。

isa_t

isa_t是一个联合体(共用体),

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
    };
    ...... //剩下的就是函数了
};

既然isa_t是一个联合体,数据存在bitsclsstruct里都是一样的。在非nonpointer情况下,isa的数据是存在cls中的,反之,isa的数据会存储在bits中。

struct展开信息如下(这里只拿来了arm64真机下的):   

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

这个结构体就是位域

  • nonpointer:是否是优化过的isa指针,0:未优化,1:优化过,可以存储更多信息;
  • has_assoc:是否有关联对象,如果有,会影响释放速度变慢;
  • has_cxx_dtor:是否有C++的析构函数,如果有,会影响释放速度变慢;
  • shiftcls:类、元类的内存地址信息;
  • magic:用于在调试时分辨对象是否未完成初始化;
  • weakly_referenced:是否被弱引用(weak)指向过,如果有,会影响释放速度变慢;
  • unused:
  • has_sidetable_rc:引用计数是否过大而存储到了sidetable中;
  • extra_rc:引用计数; image.png

shiftcls

上面的配图就是isa_t在内存中的布局,尝试一下取出其中的shiftclsshiftcls前28位存储了一些信息,后3位存储了一些信息,中间的33位数据即是shiftcls,可以通过位移的方式把它取出来。

  1. 先右移3位:将后3位移除,前3位用0补齐; image.png
  2. 在左移31位:将前31位移除,后31位用0补齐; image.png
  3. 最后在右移28位:将后28位移除,前28位用0补齐,这样shiftcls就回到了最初的位置。 image.png 注:配置中以灰色为底的块中都是位移操作之后移除的数据,以白色为底的块中都是用0补齐的数据。

下面是LLDB调试过程: image.png 最后$3$4的值是一摸一样的。

ISA_MASK

ISA_MASK的定义

objc4源码的isa.h中可以找到ISA_MASK的定义

# if __arm64__
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR /* 模拟器 */
#     define ISA_MASK        0x007ffffffffffff8ULL
# ......
#   else /* 真机 与 M1 芯片机器 */
#     define ISA_MASK        0x0000000ffffffff8ULL
# ......
#   endif
# elif __x86_64__ /* intel芯片机器 */
#     define ISA_MASK        0x00007ffffffffff8ULL
# ......
# endif

ISA_MASK是如何算出来的?

从上面isa_t在内存中的布局图可以看出,如果想要从中取出shiftcls,也可以通过进行按位与运算,需要与上:前面为28个0,中间为33个1,后面为3个0,具体值:0000000000000000000000000000111111111111111111111111111111111000,换算成16进制就是ffffffff8,在按照8字节补齐就是0x0000000ffffffff8,正好与0x0000000ffffffff8ULL是一样的,ULL是表示当前数据的类型unsigned long long.这里进行了arm64下真机的运算,其他平台可自行尝试。

ISA_MASK的使用

如何使用,isa是存在当前对象第一个8字节内,只要取出对象的第一个8字节,在进行&ISA_MASK与运算即可得到类对象的内存地址。

image.png

  1. x/4gx p 查看当前指针p在内存中所存储的数据;
  2. po 0x01000001000081b9 & 0x0000000ffffffff8ULL,取出对象在内存中存储的第一个8字节,进行&ISA_MASK与运算得到Person类;
  3. p/x 0x01000001000081b9 & 0x0000000ffffffff8ULL以16进制的方式查看与运算的值,也就是当前Person类的内存地址;
  4. 查看真实的Person类的内存地址;

可以看到,在isanonpointer的情况下,可以存储更多关于对象的数据信息,但是isa需要进行运算才能获取到类对象的内存地址。而在非nonpointer是直接指向类对象的内存地址的

Environment Variables中添加OBJC_DISABLE_NONPOINTER_ISA为YES即可禁用nonpointerimage.png

ISA 绑定流程

ISA的绑定是在_class_createInstanceFromZone中开辟内存空间之后进行的处理

  1. obj->initInstanceIsa(cls, hasCxxDtor);
  2. initIsa(cls, true, hasCxxDtor);
  3. objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor) 前两部就是正常调用加一些特处理,主要看第三部:(中间省略了一些代码)
    inline void 
    objc_object::initIsa(Class cls, bool nonpointer,
    UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
    { 
        ASSERT(!isTaggedPointer())
        isa_t newisa(0); // 创建isa结构体
        if (!nonpointer) { // 如果不是nonpointer,直接赋值cls
            newisa.setClass(cls, this);
        } else {
            ASSERT(!DisableNonpointerIsa);
            ASSERT(!cls->instancesRequireRawIsa());
            //
            newisa.bits = ISA\_MAGIC\_VALUE;

            #if ISA\_HAS\_CXX\_DTOR\_BIT
                 newisa.has_cxx_dtor = hasCxxDtor;
            #endif
                 // 赋值cls
                 newisa.setClass(cls, this);
            #endif
            // retain count
            newisa.extra_rc = 1;
        }
        isa = newisa;
    }

首先创建创建isa结构体,如果是非nonpointer,直接复制cls,否则先赋值bits,此时会将isa的nonpointer赋值为1,在赋值magic。在赋值isa的has_cxx_dtor,接下来会赋值cls。

isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
    ......
    shiftcls = (uintptr_t)newCls >> 3;
    ......
}

将类的指针地址右移3位存入到shiftcls,在此就完成了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
  1. 和对象一样,类对象也有一个isa指针,指向的是元类
  2. super_class,指向的是父类
  3. cache,方法缓存,是一个哈希表;
  4. bits,方法数据,存储着class_rw_trw代表着readwrite,可进行读写操作,该结构体内存储着属性列表property_array_t,方法列表method_array_t,协议列表protocol_array_t。里面还有class_ro_t,存储着类原始数据,ro代表着readonlyimage.png

LLDB调试探索类存储信息

class_rw_t

image.png 上图是苹果介绍存储类的数据结构,class_ro_t是只读的,里面存储着类原始的属性、方法、协议,class_rw_t是可读写的,可以对当前类动态添加属性、方法、协议,但是大部分类用不到动态添加,并且class_ro_t中有这些数据,所以将class_rw_t中的这些数据放在class_rw_ext_t中,如果当前类没有动态添加属性、方法、协议,是可以减少内存开销的。 Advancements in the Objective-C runtime

获取class_rw_t

下面是objc_class结构体获取class_rw_t的方法

class_rw_t *data() const {
    return bits.data();
}

可通过data()方法获取到class_rw_t,通过LLBD调试的方式是无法直接将class转成(objc_class *),也就无法直接使用objc_classdata()方法(如果知道为什么不能这样操作的,请在评论留言,共同学习),只能通过bits取出class_rw_t,通过上面的图可以看到,bitsisa内存相差32位,只要在isa的内存地址加上32,即可获得bits,在通过data()方法,即可拿到class_rw_t

image.png

entsize_list_tt&list_array_tt

在了解property_array_tmethod_array_tprotocol_array_t之前,先要了解entsize_list_ttlist_array_tt

list_array_tt

template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
    ......
    union {
        Ptr<List> list;
        uintptr_t arrayAndFlag;
    };
    ......
}

list_array_tt封装了一个数组的类,即可以是普通数组,也可以是一个二维数组,可指定数组里的元素类型,property_array_tmethod_array_tprotocol_array_t都是继承自list_array_tt, 一个 list_array_tt 的值可能有三种情况:

  • 空的
  • 一个指针指向一个单独的列表
  • 一个数组,数组中都是指针,每个指针分别指向一个列表 尤其要关注里面的void attachLists(List* const * addedLists, uint32_t addedCount)方法,对理解类、分类加载有一定帮助。下面是详细代码:
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<List> oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

代码执行步骤:

  1. 首次添加列表:会将list指向该列表的首地址,对应着代码中// 0 lists -> 1 list下面的操作
  2. 第二次添加列表:创建一个array_t赋值给arrayAndFlaglistarrayAndFlag是在一个匿名联合提,两个数据共用一块内存。也可以通过arrayAndFlag取出list指针),并将老的list存储的数据放在最后一位,新增的数据放在数组前方;对应着代码中// 1 list -> many lists下面的操作。
  3. 在两次之后的继续添加:继续创建一个array_t赋值给arrayAndFlag,此时也是将新来的列表放在前面,老的列表放在最后面。 通过上面的执行步骤可得知,在添加类别的时候,类别方法肯定会放在原始方法前面,方法查找时,也是使用此方法列表,这就说明了类别方法为什么会覆盖类原来的方法。还有一个就是类别越多,启动速度相对较慢。(这里需要了解一点类加载的原理)

entsize_list_tt

entsize_list_tt其实就是一个列表,用来存储编译完成后类的属性。property_array_t->property_list_tmethod_array_t->method_list_t的一维数组都是继承自entsize_list_tt,详细内容可阅读链接的资料。里面有一个Element& get(uint32_t i)方法,下面调试时会用到,就是根据索引获取元素。(protocol_list_t为什么没有继承entsize_list_tt?)

通过class_rw_t探索属性在类中的存储:property_array_t

property_array_t的数据结构

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;
 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};

property_array_t继承自list_array_tt,其中存储的元素是property_t,一维数组数据类形是property_list_t未命名文件 (1).png

property_t数据结构如下:

struct property_t {
    const char *name;
    const char *attributes;
};

调试方式

  1. 通过class_rw_t获取property_array_tclass_rw_t中有properties()方法,可以直接获取到property_array_t
  2. 通过property_array_t获取property_list_tproperty_array_t里面有一个Ptr指针存储着property_list_t,只要通过地址取值就可以取到一维数组property_list_t
  3. 通过property_list_tget()方法即可获取property_t

image.png Person类里面有一个name属性。

通过class_rw_t探索协议列表protocol_array_t

protocol_array_t的数据结构

class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t, RawPtr> Super;
 public:
    protocol_array_t() : Super() { }
    protocol_array_t(protocol_list_t *l) : Super(l) { }
};

继承自list_array_tt,其中存储的元素是protocol_ref_t,一维数组数据类形是protocol_list_tprotocol_array_t.png

调试方式

  1. 通过class_rw_t获取protocol_array_tclass_rw_t中有protocols()方法,可以直接获取到protocol_array_t
  2. 通过protocol_array_t获取protocol_list_tprotocol_array_t里面有一个Ptr指针存储着protocol_list_t,只要通过地址取值就可以取到一维数组protocol_list_t
  3. 通过protocol_list_t获取protocol_ref_tprotocol_list_t与属性、方法列表不同,不能直接通过get()方法获取,需要使用protocol_list_t结构体里的begin()方法拿到存储protocol_ref_tlist,是一个数组。
  4. 通过protocol_ref_t获取protocol_tprotocol_ref_t其实是一个指针,指向的是protocol_t,只需要做一个强转即可以,在通过指针取值,就可以看到协议相关的数据了;

KKPerson类定义如下: image.png 获取第一个协议NSCopying,调试方法如下: image.png 获取第二个协议KKPersonInterface,调试方法如下: image.png

protocol_t数据结构

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;

里面存储着协议名称、协议遵循的协议(继承?)、实例方法、类方法、可选实例方法、可选类方法、属性。其中的方法探索方式可参考下面method_array_t的方法,属性可参考上面的探索属性方法。

通过class_rw_t探索协议在类中的存储:method_array_t

数据结构

class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
    typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;
 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }
    const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
        return beginLists();
    }
    const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};

继承自list_array_tt,其中存储的元素是method_t,一维数组数据类形是method_list_t

未命名文件.png

调试方式

  1. 通过class_rw_t获取method_array_tclass_rw_t中有methods()方法,可以直接获取到method_array_t
  2. 通过method_array_t获取method_list_tmethod_array_t里面有一个Ptr指针存储着method_list_t,只要通过地址取值就可以取到一维数组method_list_t
  3. 通过method_list_tget()方法即可获取method_t
  4. 通过method_t获取SELmethod_tproperty_t数据结构还不太一样,无法直接打印出内部数据,method_t中有一个name()方法可直接获取SEL

image.png 通过调试可以看到,类里面存储的都是实例方法,没有类方法。类的方法都存储在了元类中。

元类

类的isa指向元类,可以推倒,其实类的方法是存储在元类中的。元类在我们自己写的代码里是找不到的,可以通过构建产物查看到。 image.png 也可以通过object_getClass获取到元类信息,可以看到类名都是KKPerson,但他俩的内存地址确不同; image.png

类的继承关系

类的继承关系很好理解,子类继承父类,父类继承父父类,一直到NSObject类,而NSObject的父类是nil。

元类的继承关系

子类的元类继承自父类,父类元类继承父父类元类,一直到NSObject元类,而NSObject的父类元类是nil。 image.png

isa指向与superclass继承关系

下面是苹果官方isa指向与superclass继承关系,其中实线是类的继承关系,包括元类,虚线是isa指向关系,包括元类; isa流程图.png

isa指向

  1. 实例(Instance)的isa指向类(Class);
  2. 类(Class)的isa指向元类(Meta Class);
  3. 元类(Meta Class)的isa指向根元类(Meta Class),即NSobject的元类;
  4. 跟元类(Meta Class)的isa指向它自己;

类的继承

  1. 子类继承父类,一直继承到根类,跟类继承为nil;
  2. 子类元类继承父类元类,一直继承到根类元类,跟类元类继承自跟类(NSObject);

参考资料