OC底层研究4--类的结构分析


本页所使用的objc runtime 756.2,来自GITHUB

1.概念

1.1 类Class 的类型

Class在源码里的真正类型为objc_class结构体

先查看源码——在Xcode按下Shift+Command+O,选择objc-runtime-new.h,搜索objc_class,可以得知,Class底层编译实现的前4行如下:

可知类结构如下:

  • A. //Class ISA

    ​ 继承自父类 objc_classisa ,指向类的指针不需显示(内存占8位),这里它返回一个nonpointer的指针,相关源码如下:

    /// Represents an instance of a class.
    struct objc_object {
    Class _Nonnull isa OBJC_ISA_AVAILABILITY;
    };
  • B. Class superclass;

    ​ 指向本类的父类(内存占8位)

  • C. cache_t cache;

    ​ 存储指针和vtable,加速方法调用(内存占8位)

  • D. class_data_bits_t bits;

    ​ 存储类的方法、属性、遵循的协议等信息的地方(内存占8位)

  • E. 其他方法/函数 – 不占内存

为何cache_t cache 这个变量占位位16,而不是8?

原因是因为这里的cache_t是结构体,其占内存为根据内部属性结构而定,为防止溢出,给予16位字节,而不是指针所需的8位。

Talk is cheap, show me the code, 伸伸手指,点开cache_t 的内部结构

我们来看看属性:

  • bucket_t, 结构体,占8位字节
  • mask_t,点开后具体实现为如下:
typedef uint32_t mask_t; 

​ 为整型,占4个字节

  • mask_t
typedef uint32_t mask_t;

​ 也是整形,占4个字节

共计8+4+4 = 16个字节

1.2 objc_class继承

在底层实现里,objc_class 继承自objc_object结构体

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

1.3 关于NSobject

万物皆对象 - Class 继承自NSObject

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

2. 类的属性与成员变量

  1. 属性的存储

    上面提到了类对象的结构原理,我们知道了类结构里,类型位class_data_bits_tbits下面我们来探寻一下,类里面的属性上怎么保存的。

    我们在代码里生成一个类,分别有成员变量hobby,以及属性nickName

    代码如下:

    @interface Person : NSObject{
    NSString *hobby;
    }

    @property (nonatomic, copy) NSString *nickName;

    在主线代码引入:

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    LGPerson *person = [LGPerson alloc];
    Class pClass = object_getClass(person);
    NSLog(@"%@ -- %p", person, pClass);
    }
    return 0;
    }

    然后打印得到的类pClass,通过x/4gx 获取类结构如下:

    (lldb) x/4gx pClass
    0x1000023d0: 0x001d8001000023a9 0x0000000100b37140
    0x1000023e0: 0x00000001003da280 0x0000000000000000

    观察结果得知0x1000023d0pClass在内存中的首地址,根据内存偏移原则,加上16+8+8共32位,即可得到bits的结构。

    0x1000023d0,偏移32位,得到0x1000023f0。注意,这里要用p打印指针,而非打印值用的po

    (lldb) p 0x1000023f0
    4294976496

    oh NO!得到的是具体的值,这里需要强转一下:

    (lldb) p (class_data_bits_t *)0x1000023f0
    (class_data_bits_t *) $3 = 0x00000001000023f0

    得到$3 的值,并不是我们想要的结构,留意到objc_class里的方法 data() 方法:

    struct objc_class : objc_object {
    // Class ISA; // 8
    Class superclass; // 8
    cache_t cache; // 16 不是8 // formerly cache pointer and vtable
    class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

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

    data() 返回是class_rw_t,点进去看看,

    struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    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;

    留意到里面有 methodspropertiesprotocols等,原来这里等值便是类的相关属性方法等等结构。

    现在对$3 执行data() 方法:

    (lldb) p $3->data()
    (class_rw_t *) $5 = 0x0000000000000000

    对得到的$5 进行值打印:

    (lldb) p *$5
    (class_rw_t) $5 = {
    flags = 2148139008
    version = 0
    ro = 0x0000000100002308
    methods = {
    list_array_tt<method_t, method_list_t> = {
    = {
    list = 0x0000000100002240
    arrayAndFlag = 4294976064
    }
    }
    }
    properties = {
    list_array_tt<property_t, property_list_t> = {
    = {
    list = 0x00000001000022f0
    arrayAndFlag = 4294976240
    }
    }
    }
    protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
    = {
    list = 0x0000000000000000
    arrayAndFlag = 0
    }
    }
    }
    firstSubclass = nil
    nextSiblingClass = NSUUID
    demangledName = 0x0000000000000000
    }

    如上所示,$5 内结构一目了然,方法,属性,协议都显示了。

    继续获取属性值:

    (lldb) p $5.properties
    (property_array_t) $7 = {
    list_array_tt<property_t, property_list_t> = {
    = {
    list = 0x00000001000022f0
    arrayAndFlag = 4294976240
    }
    }
    }

    得到一个二维数组的$7,观察属性列表的类型为property_array_t,继承自list_array_tt,内部拥有

    (lldb) p $7.list
    (property_list_t *) $8 = 0x00000001000022f0

    再次对$8 进行* 取值,得到如下结果:

    (lldb) p *$8
    (property_list_t) $9 = {
    entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
    }
    }

    从结果可以看出,我们的属性,就保存在properties 里中。

  2. 成员变量的存储

    上面分析了属性的存储,但是我们没有看到成员变量 hobby,我们在properties 属性里,没有看到它,到底在哪里呢?下面我们继续查找。

    既然不在properties 里,那尝试一下在$5 的 ro 里进行取值

    (lldb) p $5.ro
    (const class_ro_t *) $10 = 0x0000000100002308

    得到class_ro_t 类型的结构体,继续对他取值

    (lldb) p *$10
    (const class_ro_t) $11 = {
    flags = 388
    instanceStart = 8
    instanceSize = 24
    reserved = 0
    ivarLayout = 0x0000000100001f8a "\x02"
    name = 0x0000000100001f81 "LGPerson"
    baseMethodList = 0x0000000100002240
    baseProtocols = 0x0000000000000000
    ivars = 0x00000001000022a8
    weakIvarLayout = 0x0000000000000000
    baseProperties = 0x00000001000022f0
    }

    值这里可以看到ivars,就是我们需要的变量列表,打印一下:

    (lldb) p $11.ivars
    (const ivar_list_t *const) $12 = 0x00000001000022a8

    继续对 ivar_list_t 类型的$12 取值:

    (lldb) p *$12
    (const ivar_list_t) $13 = {
    entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
    offset = 0x0000000100002378
    name = 0x0000000100001e64 "hobby"
    type = 0x0000000100001fa7 "@\"NSString\""
    alignment_raw = 3
    size = 8
    }
    }
    }
    (lldb) p $.first
    (const ivar_t) $13 = {
    offset = 0x0000000100002378
    name = 0x0000000100001e64 "hobby"
    type = 0x0000000100001fa7 "@\"NSString\""
    alignment_raw = 3
    size = 8
    }

    可以看到成员变量存在类的 ivars 属性

3. 类的方法

3.1 类的实例方法

我们继续对类进行代码编辑,对Person类添加实例方法和类方法

- (void)sayHello;		// 实例方法
+ (void)sayHappy; // 类方法

还是对类结构$5 进行解析,这次我们打印它的 methods 属性

(lldb) p $5.methods
(method_array_t) $14 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002240
arrayAndFlag = 4294976064
}
}
}

得到的是method_array_t 类型的$14, 是个数组类型,继续打印:

(lldb) p $14.list
(method_list_t *) $15 = 0x0000000100002240
(lldb) p *$15
(method_list_t) $16 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHello"
types = 0x0000000100001f8c "v16@0:8"
imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}
}
}

可见,实例方法sayHello 方法,存储在类的methods属性里

3.2 类的类方法存储

类方法在上述的过程中没有看到,那么它会在哪里呢?

为了找到它,我们重新回到最初的pClass

(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000

得到isa值为0x001d800100002389,这时,唤出它的掩码 ISA_MASK 来获取它的元类

(lldb) p/x 0x001d800100002389 & 0x0000000ffffffff8
(long) $1 = 0x0000000100002388

继续对元类**$1**的结构进行解析:

(lldb) x/4gx 0x0000000100002388
0x100002388: 0x001d800100b370f1 0x0000000100b370f0
0x100002398: 0x0000000100f946c0 0x0000000100000003

可知0x100002388是这个元类的地址,根据上文提到的内存便宜,我们继续加上2个16进制位,得到0x1000023a8,p一下:

(lldb) p (class_data_bits_t *)$2
(class_data_bits_t *) $3 = 0x00000001000023a8

继续执行上文实例方法类似的方案,执行data() — 取值 – 获取methods 的思路,具体代码如下:

(lldb) p $3->data()
(class_rw_t *) $4 = 0x0000000100f94620
(lldb) p *$4
(class_rw_t) $5 = {
flags = 2685075456
version = 7
ro = 0x00000001000021f8
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000021d8
arrayAndFlag = 4294975960
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff942e6990
demangledName = 0x0000000000000000
}
(lldb) p $5.methods
(method_array_t) $10 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000021d8
arrayAndFlag = 4294975960
}
}
}
(lldb) p $10.list
(method_list_t *) $11 = 0x00000001000021d8
(lldb) p *$11
(method_list_t) $12 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "sayHappy"
types = 0x0000000100001f8c "v16@0:8"
imp = 0x0000000100001bc0 (LGTest`+[LGPerson sayHappy] at LGPerson.m:17)
}
}
}
(lldb)

可见,类方法sayHappy 方法,存储在类的元类 methods属性里

4. 总结

  • 类的本质上一个类型为objc_class的结构体,包含有isa、父类、属性、成员变量及方法列表等
  • 类属性存在结构体的properties里
  • 类的成员变量存在结构体的 ivars里
  • 类的实例方法,存在结构体 的methods 属性里
  • 类的类方法,存在父类的class_ro_t 里的methods里

如下图示:


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
方法的本质1--cache_t方法缓存分析 方法的本质1--cache_t方法缓存分析
本页所使用的objc runtime 756.2,来自GITHUB 1.概念1.1 objc_class 结构前面探索了类的结构,知道了类的结构本质上是objc_class的结构体,而在 C 源码例, objc_class 结构体的结构
2020-01-21 李佳
下一篇 
2020新年感想 2020新年感想
2020年是一个闰年,第一天从星期三开始。 今天看了小羊肖恩大电影,故事讲得真好看,小朋友全程开心,全情投入,大朋友也觉得耳目一新,深受感动。 这是我们带他看的第一部电影,新的一年,从第一开始。 用一句涂鸦来鼓舞自己 宁愿最后徒劳无
2020-01-02 李佳
  目录