iOS底层原理之类,元类数据结构探索(下)
一,class_rw_t
中firstSubclass
探索
(lldb) p/x GoodOne.class
(Class) $0 = 0x0000000100008398 GoodOne
(lldb) p/x 0x0000000100008398 + 0x20
(long) $1 = 0x00000001000083b8
(lldb) p (class_data_bits_t*)0x00000001000083b8
(class_data_bits_t *) $2 = 0x00000001000083b8
(lldb) p $2->data()
(class_rw_t *) $3 = 0x000000010069a990
(lldb) p *$3
(class_rw_t) $5 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000128
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p GoodTwo.class
(Class) $10 = GoodTwo
(lldb) p $2->data()
(class_rw_t *) $11 = 0x000000010069a990
(lldb) p *$11
(class_rw_t) $12 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000128
}
}
firstSubclass = GoodTwo
nextSiblingClass = NSUUID
}
我们通过打印可得出结论:GoodTwo
继承于 GoodOne
,当我们查看GoodOne 中的 class_rw_t 数据
时 firstSubclass = nil
这时他的子类GoodTwo 并没有加载
,当我们调用p GoodTwo.class
时 ,在打印可以发现 firstSubclass = GoodTwo
,所以我们有一个推测,类的加载是一种懒加载的形式
。
二,class_ro_t
中的baseProperties
探究
(lldb) p $12.ro()
(const class_ro_t *) $13 = 0x0000000100008040
(lldb) p *$13
(const class_ro_t) $14 = {
flags = 0
instanceStart = 8
instanceSize = 16
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "GoodOne" {
Value = 0x0000000100003ea9 "GoodOne"
}
}
baseMethodList = 0x0000000100008088
baseProtocols = nil
ivars = 0x00000001000080f0
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100008118
_swiftMetadataInitializer_NEVER_USE = {}
(lldb) p *$14.baseProperties
(property_list_t) $15 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 1)
}
(lldb) p $15.get(0)
(property_t) $16 = (name = "hobby", attributes = "T@\"NSString\",C,N,V_hobby")
}
通过内存偏移取值可以看到,baseProperties
中也是存放当前类的属性。
三, class_ro_t
中的ivars
探索
(lldb) p/x GoodOne.class
(Class) $0 = 0x0000000100008388 GoodOne
(lldb) p/x 0x0000000100008388 + 0x20
(long) $1 = 0x00000001000083a8
(lldb) p (class_data_bits_t*)0x00000001000083a8
(class_data_bits_t *) $2 = 0x00000001000083a8
(lldb) p *$2->data()
(class_rw_t) $3 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000128
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p *$3.ro()
(const class_ro_t) $4 = {
flags = 0
instanceStart = 8
instanceSize = 24
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "GoodOne" {
Value = 0x0000000100003ec3 "GoodOne"
}
}
baseMethodList = 0x0000000100008088
baseProtocols = nil
ivars = 0x00000001000080f0
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100008138
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $4.ivars
(const ivar_list_t *const) $5 = 0x00000001000080f0
(lldb) p *$5
(const ivar_list_t) $6 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 2)
}
(lldb) p $6.get(0)
(ivar_t) $7 = {
offset = 0x0000000100008368
name = 0x0000000100003eeb "count"
type = 0x0000000100003f79 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $6.get(1)
(ivar_t) $8 = {
offset = 0x0000000100008370
name = 0x0000000100003ef1 "_name"
type = 0x0000000100003f79 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb)
通过打印我们可以看出原来类中的成员变量
信息都放在ivar_list_t
中。
四,成员变量 VS 实例变量VS 属性。
@interface GoodOne : NSObject{
NSString *count; 成员变量
NSObject *obg; 实例变量
}
@property (nonatomic, copy) NSString *name; 属性
结论:
属性 = 带下划线成员变量 + setter + getter 方法
这也是为什么ProperiesList中只有属性没有成员变量
,而IvarList中有对应的成员变量,和带下划线的成员变量。
实例变量 : 特殊的成员变量 (类的实例化)
六,setter方法在底层的实现原理
1,我们可以确认一点,setter方法本质就是对某一块内存赋值。那么不同的属性set方法是不一样的,那么不可能我们每一个不同的setter方法都在最底层去实现。所以这里就有一个中间层对所有的set方法做一层处理。然后在通过imp 重定向到setPropety 方法最后找到对应的内存存储区域。
@property (nonatomic, copy) NSArray *array1;
@property (nonatomic, strong) NSArray *array;
static void _I_Goods_setArray1_(Goods * self, SEL _cmd, NSArray * _Nonnull array1) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Goods, _array1), (id)array1, 0, 1); }
static NSArray * _Nonnull _I_Goods_array(Goods * self, SEL _cmd) { return (*(NSArray * _Nonnull *)((char *)self + OBJC_IVAR_$_Goods$_array)); }
static void _I_Goods_setArray_(Goods * self, SEL _cmd, NSArray * _Nonnull array) { (*(NSArray * _Nonnull *)((char *)self + OBJC_IVAR_$_Goods$_array)) = array; }
结论:copy修饰的属性使用objc_setProperty方式实现,其它属性使用内存偏移实现
七,类方法信息的储存位置
(lldb) p/x GoodOne.class
(Class) $0 = 0x00000001000083b0 GoodOne
(lldb) x/4gx GoodOne.class
0x1000083b0: 0x00000001000083d8 0x000000010036a140
0x1000083c0: 0x00000001007a6020 0x0002802800000007
(lldb) p/x 0x00000001000083d8 & 0x00007ffffffffff8
(long) $2 = 0x00000001000083d8
(lldb) p/x 0x00000001000083d8 + 0x20
(long) $3 = 0x00000001000083f8
(lldb) p (class_data_bits_t*)$3
(class_data_bits_t *) $4 = 0x00000001000083f8
(lldb) p *$4->data()
(class_rw_t) $5 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000432
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff802c5eb0
}
(lldb) p $5.methods()
(const method_array_t) $6 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000081b8
}
arrayAndFlag = 4295000504
}
}
}
(lldb) p $6.list.ptr
(method_list_t *const) $7 = 0x00000001000081b8
(lldb) p *$7
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p *$8.get(0)
error: <user expression 10>:1:1: indirection requires pointer operand ('method_t' invalid)
*$8.get(0)
^~~~~~~~~~
(lldb) p $8.get(0)
(method_t) $9 = {}
(lldb) p $8.get(0).big()
(method_t::big) $10 = {
name = "stGood"
types = 0x0000000100003f8d "v16@0:8"
imp = 0x0000000100003cf0 (KCObjcBuild`+[GoodOne stGood])
}
结论:类方法存储在元类中。这可能也是为什么有元类这个概念的原因之一,对象的依附是类,那类的依附就是元类
补充(一) 针对2020年苹果对类的优化
先来了解两个概念
Clean Memory
clear memory是指加载后不会发生更改的内存
。
例如:class_ro_t
就属于clean memory
,因为它是只读
的
clean memory 的优点:可以通过系统调控,回收或者重新加载 ,从而节省更多的内存空间。
Dirty Memory
通过heap Xcode | egrep 'class_rw|COUNT’我们可以看一下class_rw_t和class_rw_ext_t在Xcode中的占用
dirty memory是指在进程动行时会发生更改的内存
。
例如:class_rw_t
就属于 dirty memory
,当进程运行时类的数据结构发生改变时 内存也相应的发生变化即成为 dirty memory
,
由于dirty memory
要比clean memory
昂贵的多,只要进行运行它就必须一直存在,所以苹果针对
class_rw_t
做了相关优化,将class_rw_t
中常用和非常用扩展数据做了分离。将常用的扩展数据放到class_rw_ext_t
中,这样class_rw_t
的内存空间节约将近一倍。
运行时需要追踪每个类的更多的信息,所以当一个类首次被使用,运行时会为它分配额外的存储容量,也就是class_rw_t
,用于读取-编写数据。在class_rw_t
中存储了只有运行时才会生成的新信息,例如,所有的类都会链接成一个树状结构,通过First Subclass和Next Sibling Class指针实现,这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。
当类第一次从磁盘加载到内存时,它们像上图这样开始,但是一旦使用它们便会更改。Swift类和Objective-C类共享此基础结构,因此每个Swift类也具有这些数据结构。
要了解随后发生的情况,了解干净内存和脏内存之间的区别很有用。
类结构一经使用就会变为dirty memory
,因为动行时会向它写入新的数据。dirty memory比clear memory要昂贵得多
,只要进程在运行,它就必须一直存在;另一方面,clear memory可以进行移除从而节省更多的运行空间,因为如果你需要clear memory系统可以从磁盘中重新加载。macOS可以选择换出dirty memory,但是因为iOS不使用Swap,所以dirty memory在iOS中代价很大。dirty memory是类数据分为两部分的原因。
那么,为什么当Mehods、Properties、Protocols在class_ro_t中已经存在,还要在class_rw_t中设计呢?因为它们可以在运行时进行更改,当category被加载时它可以向类中添加新的方法,而且程序员可以通过使用运行时API动态地添加它们。因为class_ro_t是只读的,所以我们需要在class_rw_t中追踪这些东西。现在这样做会占用相当多的内存,在任何给定的设备中都有很多类在使用,但是通过检查实际设备上的使用情况,发现大约只有10%的类真正的更改了它们的方法,而且只有Swift类会使用Demangled Name字段并且Swift类并不需要这个字段,除非访问它们的Object-C名称时才需要,所以我们可以拆掉那些平时不用的部分,这将class_rw_t的大小减少了一半,对于那些确实需要这些额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用。
补充(二)类中的方法编码
{(struct objc_selector *)"setArray1:", "v24@0:8@16", (void *)_I_Goods_setArray1_},
v
表示:void返回类型。
24
表示:setArray1函数占用的字节数。
@
表示:参数,id self。
0
表示:从0号位置开始。
:
表示: SEL。
8
表示:从8号位置开始。
@
表示:参数,setArray1。
16
表示:从16号位置开始。
通过xcode查看编码图:打开xcode–> command+shift+0–> 搜索ivar_getTypeEncoding–> 点击Type Encodings
未完待续。。。