探秘Runtime - 剖析Runtime结构体

栏目: IOS · 发布时间: 4年前

内容简介:在之前的后来可能苹果也不太想让开发者知道

OC1.0 中, Runtime 很多定义都写在 NSObject.h 文件中,如果之前研究过 Runtime 的同学可以应该见过下面的定义,定义了一些基础的信息。

// 声明Class和id
typedef struct objc_class *Class;
typedef struct objc_object *id;

// 声明常用变量
typedef struct objc_method *Method;
typedef struct objc_ivar *Ivar;
typedef struct objc_category *Category;
typedef struct objc_property *objc_property_t;

// objc_object和objc_class
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
    
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
    
} OBJC2_UNAVAILABLE;
复制代码

之前的 Runtime 结构也比较简单,都是一些很直接的结构体定义,现在新版的 Runtime 在操作的时候,各种地址偏移操作和位运算。

之后的定义

后来可能苹果也不太想让开发者知道 Runtime 内部的实现,所以就把源码定义从 NSObject 中搬到 Runtime 中了。而且之前的定义也不用了,通过 OBJC_TYPES_DEFINED 预编译指令,将之前的代码废弃调了。

现在 NSObject 中的定义非常简单,直接就是一个 Class 类型的 isa 变量,其他信息都隐藏起来了。

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
复制代码

这是最新的一些常用 Runtime 定义,和之前的定义也不太一样了,用了最新的结构体对象,之前的结构体也都废弃了。

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

typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
复制代码

对象结构体

objc_object定义

在OC中每个对象都是一个结构体,结构体中都包含一个isa的成员变量,其位于成员变量的第一位。 isa 的成员变量之前都是 Class 类型的,后来苹果将其改为 isa_t

struct objc_object {
private:
    isa_t isa;
};
复制代码

OC中的类和元类也是一样,都是结构体构成的。由于类的结构体定义继承自 objc_object ,所以其也是一个对象,并且具有对象的 isa 特征。

探秘Runtime - 剖析Runtime结构体

所以可以通过 isa_t 来查找对应的类或元类,查找方法应该是通过 uintptr_t 类型的 bits ,通过按位操作来查找 isa_t 指向的类的地址。

实例对象或类对象的方法,并不会定义在各个对象中,而是都定义在 isa_t 指向的类中。查找到对应的类后,通过类的 class_data_bits_t 类型的 bits 结构体查找方法,对象、类、元类都是同样的查找原理。

isa_t定义

isa_t 是一个 union 的结构对象, union 类似于 C++ 结构体,其内部可以定义成员变量和函数。在 isa_t 中定义了 clsbitsisa_t 三部分,下面的 struct 结构体就是 isa_t 的结构体构成。

下面对 isa_t 中的结构体进行了位域声明,地址从 nonpointer 起到 extra_rc 结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,通过下面的结构体成员变量可以直接操作某个地址。位域总共占8字节,所有的位域加在一起正好是64位。

小提示: unionbits 可以操作整个内存区,而位域只能操作对应的位。

下面的代码是不完整代码,只保留了 arm64 部分,其他部分被忽略掉了。

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

    Class cls;
    uintptr_t bits;

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1; // 是32位还是64位
        uintptr_t has_assoc         : 1; // 对象是否含有或曾经含有关联引用,如果没有关联引用,可以更快的释放对象
        uintptr_t has_cxx_dtor      : 1; // 表示是否有C++析构函数或OC的析构函数
        uintptr_t shiftcls          : 33; // 对象指向类的内存地址,也就是isa指向的地址
        uintptr_t magic             : 6; // 对象是否初始化完成
        uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用
        uintptr_t deallocating      : 1; // 对象是否被释放中
        uintptr_t has_sidetable_rc  : 1; // 对象引用计数太大,是否超出存储区域
        uintptr_t extra_rc          : 19; // 对象引用计数
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__
// ····
# else
// ····
# endif
};
复制代码

ARM64 架构下, isa_t 以以下结构进行布局。在不同的 CPU 架构下,布局方式会有所不同,但参数都是一样的。

探秘Runtime - 剖析Runtime结构体

类结构体

objc_class结构体

Runtime 中类也是一个对象,类的结构体 objc_class 是继承自 objc_object 的,具备对象所有的特征。在 objc_class 中定义了三个成员变量, superclass 是一个 objc_class 类型的指针,指向其父类的 objc_class 结构体。 cache 用来处理已调用方法的缓存。

bitsobjc_class 的主角,其内部只定义了一个 uintptr_t 类型的 bits 成员变量,存储了 class_rw_t 的地址。 bits 中还定义了一些基本操作,例如获取 class_rw_traw isa 状态、是否 swift 等函数。 objc_class 结构体中定义的一些函数,其内部都是通过 bits 实现的。

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             
    class_data_bits_t bits;    

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    // .....
}
复制代码

objc_class 的源码可以看出,可以通过 bits 结构体的 data() 函数,获取 class_rw_t 指针。我们进入源代码中看一下,可以看出是通过对 uintptr_t 类型的 bits 变量,做位运算查找对应的值。

class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}
复制代码

uintptr_t 本质上是一个 unsigned longtypedefunsigned long 在64位处理器中占8字节,正好是64位二进制。通过 FAST_DATA_MASK 转换为二进制后,是取 bits 中的 47-3 的位置,正好是取出 class_rw_t 指针。

在OC中一个指针的长度是47,例如打印一个 UIViewController 的地址是 0x7faf1b580450 ,转换为二进制是 11111111010111100011011010110000000010001010000 ,最后面三位是占位的,所以在取地址的时候会忽略最后三位。

// 查找第0位,表示是否swift
#define FAST_IS_SWIFT           (1UL<<0)
// 当前类或父类是否定义了retain、release等方法
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
// 类或父类需要初始化isa
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
// 数据段的指针
#define FAST_DATA_MASK          0x00007ffffffffff8UL
// 11111111111111111111111111111111111111111111000 总共47位
复制代码

因为在 bits 中最后三位是没用的,所以可以用来存储一些其他信息。在 class_data_bits_t 还定义了三个宏,用来对后三位做位运算。

class_ro_t和class_rw_t

class_data_bits_t 相关的有两个很重要结构体, class_rw_tclass_ro_t ,其中都定义着 method listprotocol listproperty list 等关键信息。

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
};
复制代码

在编译后 class_data_bits_t 指向的是一个 class_ro_t 的地址,这个结构体是不可变的(只读)。在运行时,才会通过 realizeClass 函数将 bits 指向 class_rw_t

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};
复制代码

在程序开始运行后会初始化 Class ,在这个过程中,会把编译器存储在 bits 中的 class_ro_t 取出,然后创建 class_rw_t ,并把 ro 赋值给 rw ,成为 rw 的一个成员变量,最后把 rw 设置给 bits ,替代之前 bits 中存储的 ro 。除了这些操作外,还会有一些其他赋值的操作,下面是初始化 Class 的精简版代码。

static Class realizeClass(Class cls) 
{
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;

    ro = (const class_ro_t *)cls->data();
    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);

    isMeta = ro->flags & RO_META;
    rw->version = isMeta ? 7 : 0;

    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()))

    cls->superclass = supercls;
    cls->initClassIsa(metacls);
    cls->setInstanceSize(ro->instanceSize);

    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    methodizeClass(cls);
    return cls;
}
复制代码

在上面的代码中我们还发现了两个函数, addRootClassaddSubclass 函数,这两个函数的职责是将某个类的子类串成一个列表,大致是下面的链接顺序。由此可知,我们是可以通过 class_rw_t ,获取到当前类的所有子类。

superClass.firstSubclass -> subClass1.nextSiblingClass -> subClass2.nextSiblingClass -> ...
复制代码

初始化 rwro 之后, rwmethod listprotocol listproperty list 都是空的,需要在下面 methodizeClass 函数中进行赋值。函数中会把 rolist 都取出来,然后赋值给 rw ,如果在运行时动态修改,也是对 rw 做的操作。所以 ro 中存储的是编译时就已经决定的原数据, rw 才是运行时动态修改的数据。

static void methodizeClass(Class cls)
{
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
}
复制代码

假设创建一个类 LXZObject ,继承自 NSObject ,并为其加入一个 testMethod 方法,不做其他操作。因为在编译后 objc_classbits 对应的是 class_ro_t 结构体,所以我们打印一下结构体的成员变量,看一下编译后的 class_ro_t 是什么样的。

struct class_ro_t {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000 <no value available>
  name = 0x0000000100000f7a "LXZObject"
  baseMethodList = 0x00000001000010c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000 <no value available>
  baseProperties = 0x0000000000000000
}
复制代码

经过打印可以看出,一个类的 class_ro_t 中只会包含当前类的信息,不会包含其父类的信息,在 LXZObject 类中只会包含 namebaseMethodList 两个字段,而 baseMethodList 中只有一个 testMethod 方法。由此可知, class_rw_t 结构体也是一样的。

探秘Runtime - 剖析Runtime结构体

初始化过程

下面是已经初始化后的 isa_t 结构体的布局,以及各个结构体成员在结构体中的位置。

探秘Runtime - 剖析Runtime结构体

union 经常配合结构体使用,第一次使用 union 就是对结构体区域做初始化。在对象初始化时,会对 isa_tbits 字段赋值为 ISA_MAGIC_VALUE ,这就是对 union 联合体初始化的过程。

// 在objc-723中已经没有了
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) 
{
    if (!indexed) {
        isa.cls = cls;
    } else {
        isa.bits = ISA_MAGIC_VALUE;
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
    }
}
复制代码

在对象通过 initIsa() 函数初始化时,会通过 ISA_MAGIC_VALUEisa 进行初始化。 ISA_MAGIC_VALUE 是一个16进制的值,将其转换为二进制后,会发现 ISA_MAGIC_VALUE 是对 nonpointermagic 做初始化。

nonpointer 是对之前32位处理器的兼容。在访问对象所属的类时,如果是32位则返回之前的 isa 指针地址,否则表示是64位处理器,则返回 isa_t 结构体。

# define ISA_MAGIC_VALUE 0x000001a000000001ULL
二进制:11010000000000000000000000000000000000001
补全二进制:23个零+11010000000000000000000000000000000000001
复制代码

随后会通过位域,对 has_cxx_dtorshiftcls 做初始化,这时候就已经有四个字段被初始化了。 has_cxx_dtor 表示是否有 C++ 或OC的析构方法,在打印方法列表时,经常能看到一个名为 .cxx_destruct 的方法,就和这个字段有关系。

在计算机中为了对存储区 (Memory or Disk) 读取方便,所以在写入和读取时,会对内存有对其操作。一般是以字节为单位进行对其,这样也是对读写速度的优化。在对 shiftcls 进行赋值时,对 Class 的指针进行了位移操作,向右位移三位。这是因为类指针为了内存对其,将最后三位用0填充,所以这三位是没有意义的。

isa结构体
0000000001011101100000000000000100000000001110101110000011111001
0x5d8001003ae0f8

类对象地址
100000000001110101110000011111000
0x1003ae0f8

将类对象地址右移三位为100000000001110101110000011111,正好符合isa_t地址中shiftcls的部分,前面不足补零。
复制代码

外界获取 Class 时,应该通过 ISA() 函数,而不是像之前一样直接访问 isa 指针。在 ISA() 函数中,是对 isa_t 的结构体做与运算,是通过 ISA_MASK 宏进行的,转换为二进制的话,正好是把 shiftcls 的地址取出来。

inline Class 
objc_object::ISA() 
{
    return (Class)(isa.bits & ISA_MASK);
}

#define ISA_MASK 0x0000000ffffffff8ULL
111111111111111111111111111111111000
复制代码

Tagged Pointer

iPhone5s 开始, iOS 设备开始引入了64位处理器,之前的处理器一直都是32位的。

但是在64位处理器中,指针长度以及一些变量所占内存都发生了改变,32位一个指针占用4字节,但64位一个指针占用8字节;32位一个 long 占用4字节,64位一个 long 占用8字节等,所以在64位上内存占用会多出很多。

苹果为了优化这个问题,推出了Tagged Pointer新特性。之前一个指针指向一个地址,而Tagged Pointer中一个指针就代表一个值,以NSNumber为例。

NSNumber *number1 = @1;
NSNumber *number2 = @3;
NSNumber *number3 = @54;

// 输出
(lldb) p number1
(__NSCFNumber *) $3 = 0xb000000000000012 (int)1
(lldb) p number2
(__NSCFNumber *) $4 = 0xb000000000000032 (int)3
(lldb) p number3
(__NSCFNumber *) $5 = 0xb000000000000362 (int)54
复制代码

通过上面代码可以看出,使用了 Tagged Pointer 新特性后,指针中就存储着对象的值。例如一个值为1的 NSNumber ,指针就是 0xb000000000000012 ,如果抛去前面的 0xb 和后面的2,中间正好就是16进制的值。

苹果通过 Tagged Pointer 的特性,明显的提升了执行效率并节省了很多内存。在64位处理器下,内存占用减少了将近一半,执行效率也大大提升。由于通过指针来直接表示数值,所以没有了 mallocfree 的过程,对象的创建和销毁速度提升几十倍。

isa_t

对于对象指针也是一样,在 OC1.0 时代 isa 是一个真的指针,指向一个堆区的地址。而 OC2.0 时代,一个指针长度是八字节也就是64位,在64位中直接存储着对象的信息。当查找对象所属的类时,直接在 isa 指针中进行位运算即可,而且由于是在栈区进行操作,查找速度是非常快的。

struct {
    uintptr_t nonpointer        : 1;
    uintptr_t has_assoc         : 1;
    uintptr_t has_cxx_dtor      : 1;
    uintptr_t shiftcls          : 33;
    uintptr_t magic             : 6;
    uintptr_t weakly_referenced : 1;
    uintptr_t deallocating      : 1;
    uintptr_t has_sidetable_rc  : 1;
    uintptr_t extra_rc          : 19;
};
复制代码

例如 isa_t 本质上是一个结构体,如果创建结构体再用指针指向这个结构体,内存占用是很大的。但是 Tagged Pointer 特性中,直接把结构体的值都存储到指针中,这就相当节省内存了。

苹果不允许直接访问 isa 指针,和 Tagged Pointer 也是有关系的。因为在 Tagged Pointer 的情况下, isa 并不是一个指针指向另一块内存区,而是直接表示对象的值,所以通过直接访问 isa 获取到的信息是错误的。

Tagged Pointer

简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。

探秘Runtime - 剖析Runtime结构体

下载地址: Runtime PDF 麻烦各位大佬点个赞,谢谢!:grin:


以上所述就是小编给大家介绍的《探秘Runtime - 剖析Runtime结构体》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Haskell Programming from first principles

Haskell Programming from first principles

Christopher Allen、Julie Moronuki / 2015 / USD 59.00

I am writing this book because I had a hard time learning Haskell. It doesn't have to be that way. I've spent the last couple years actively teaching Haskell online and in person. Along the way, I ......一起来看看 《Haskell Programming from first principles》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具