iOS底层探索之类的结构(中)

846 阅读7分钟

在上一篇博客里面iOS底层探索之类的结构(上)已经大致的了解了类的结构

类的结构

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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
}

class_data_bits

我们主要探究是class_data_bits_t bits,在bits里面有我们关心的类的信息。 那么我们怎么拿呢?先看看下面这个JPPerson

@interface JPPerson : NSObject
{
	int age;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;

- (void)sayHello;
+ (void)sayNB;
@end
@implementation JPPerson

- (instancetype)init{
	 if (self = [super init]) {
		  self.name = @"reno";
	 }
	 return self;
}
- (void)sayHello{
}
+ (void)sayNB{
}
@end

使用lldb调试 x/4gx 命令打印了JPPerson这个类的内存信息 调试结果 第一个是isa;第二个是superclasspo 出来是 NSObjectJPPerson 是继承NSObject的;那么以此类推,第三个是cache,第四个是bits

bits

我们要了解bits里面的data信息,该怎么拿呢?光知道一个地址也不行啊?那么我们既然知道了isa(首地址),是不是就可以通过指针偏移内存偏移拿到呢?那么要偏移多少个呢? 类的结构

要想拿到bits,指针在内存中必须要平移指向bits,我知道isa是8个字节长度,superclass也是8个字节长度,那么cache_t呢?看看内部结构分析下

cache_t

cache_t内存大小 分析得到cache_t是16,那么加isasuperclass一共就是32个字节的长度。

(lldb) x/4gx JPPerson.class
0x100008358: 0x0000000100008380 0x000000010036a140
0x100008368: 0x00000001003623c0 0x0000802800000000
(lldb) p/x 0x100008358+0x20
(long) $1 = 0x0000000100008378
(lldb) p (class_data_bits_t*)0x0000000100008378
(class_data_bits_t *) $2 = 0x0000000100008378
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101b06bc0
(lldb) p *$3
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000144
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}

好尴尬啊!class_rw_t里面没有看到我们该看到的信息啊!

class_rw_t

class_rw_t结构 从没有错啊!打印的信息和在class_rw_t源码里面的结构是一样的啊? 那怎么没有呢?我们刚打印的是结构信息,里面的方法没有找到,我们继续去看看源码,看看有没有打印属性的方法。 class_rw_t方法 还真有方法,你说巧不巧啊!嘿嘿 嘿嘿 那么靓仔,直接调用properties()不就可以了吗!

properties

(lldb) p $3.properties()
(const property_array_t) $5 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100008198
      }
      arrayAndFlag = 4295000472
    }
  }
}
  Fix-it applied, fixed expression was: 
    $3->properties()
(lldb) 

调用properties()方法得到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) { }
};

list_array_tt

list_array_tt<property_t, property_list_t, RawPtr>

list_array_tt 知道了结构,我们就一层一层的往下扒

(lldb) p $3.properties()
(const property_array_t) $5 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100008198
      }
      arrayAndFlag = 4295000472
    }
  }
}
  Fix-it applied, fixed expression was: 
    $3->properties()
(lldb) p $5.list
(const RawPtr<property_list_t>) $6 = {
  ptr = 0x0000000100008198
}
(lldb) p $6.ptr
(property_list_t *const) $7 = 0x0000000100008198
(lldb) p $7*
error: <user expression 9>:2:1: expected expression
;
^
(lldb) p *$7
(property_list_t) $8 = {
  entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}
(lldb) p $8.get(0)
(property_t) $9 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) p $8.get(1)
(property_t) $10 = (name = "hobby", attributes = "T@\"NSString\",C,N,V_hobby")
(lldb) 

还有谁?靓仔看到没有,JPPerson的属性打印出来了,我们要的类的信息成功输出来了!我这该死的魅力啊!

在这里插入图片描述

methods()

上面打印了属性,接下来该看看方法了

(lldb) p $3.methods()
(const method_array_t) $15 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100008098
      }
      arrayAndFlag = 4295000216
    }
  }
}
  Fix-it applied, fixed expression was: 
    $3->methods()
(lldb) p $15.list.ptr
(method_list_t *const) $18 = 0x0000000100008098
(lldb) p *$18
(method_list_t) $19 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 6)
}
(lldb) p $19.get(0)
(method_t) $20 = {}
(lldb) p $19.get(0)
(method_t) $21 = {}
(lldb) p $19.get(1)
(method_t) $22 = {}

what ??什么鬼啊?怎么get打印不出来啊?别急继续探索下去

method_t

method_t 在源码里面发现了method_t结构体,里面嵌套了一个big结构体,big里面有个SEL 还有imp big 那么可以通过method_t结构体拿到big,就可以获取到里面的方法,源码里面也发现了获取big方法big()

big()

(lldb) p $19.get(0).big
(method_t::big) $23 = {
  name = "sayHello"
  types = 0x0000000100003f94 "v16@0:8"
  imp = 0x0000000100003cd0 (JPObjcBuild`-[JPPerson sayHello])
}
  Fix-it applied, fixed expression was: 
    $19.get(0).big()
(lldb) p $19.get(1).big
(method_t::big) $24 = {
  name = "hobby"
  types = 0x0000000100003f8c "@16@0:8"
  imp = 0x0000000100003d40 (JPObjcBuild`-[JPPerson hobby])
}
  Fix-it applied, fixed expression was: 
    $19.get(1).big()
(lldb) p $19.get(2).big
(method_t::big) $25 = {
  name = "setHobby:"
  types = 0x0000000100003f9c "v24@0:8@16"
  imp = 0x0000000100003d70 (JPObjcBuild`-[JPPerson setHobby:])
}
  Fix-it applied, fixed expression was: 
    $19.get(2).big()
(lldb) p $19.get(3).big
(method_t::big) $26 = {
  name = "init"
  types = 0x0000000100003f8c "@16@0:8"
  imp = 0x0000000100003c70 (JPObjcBuild`-[JPPerson init])
}
  Fix-it applied, fixed expression was: 
    $19.get(3).big()
(lldb) p $19.get(4).big
(method_t::big) $27 = {
  name = "name"
  types = 0x0000000100003f8c "@16@0:8"
  imp = 0x0000000100003ce0 (JPObjcBuild`-[JPPerson name])
}
  Fix-it applied, fixed expression was: 
    $19.get(4).big()
(lldb) p $19.get(5).big
(method_t::big) $28 = {
  name = "setName:"
  types = 0x0000000100003f9c "v24@0:8@16"
  imp = 0x0000000100003d10 (JPObjcBuild`-[JPPerson setName:])
}
  Fix-it applied, fixed expression was: 
    $19.get(5).big()

干的漂亮!哈哈😁,方法列表里面count值为6,一共有6个都打印了出来了,包括setter方法和getter方法,靓仔就问你服不服!

服不服

那么有的靓仔肯定不服了,我怎么没有看到方法(+方法)和成员变量打印出来呢?你在这里装什么大一瓣蒜啊!

好,不服是吧!那么我们接着往下探索。

ivars

属性和成员变量在内存中存放的位置是不一样的,在WWDC2020里面介绍了Clean MemoryDirty Memory

Clean Memory

clean memory 加载后不会发生改变的内存 class_ro_t 就属于clean memory,因为它是只读的不会,不会对齐内存进行修改 clean memory 是可以进行移除的,从而节省更多的内存空间,因为如果你有需要clean memory ,系统可以从磁盘中重新加载

Dirty Memory

dirty memory 是指在进程运行时会发生改变的内存 类结构一经使用就会变成 dirty memory,因为运行时会向它写入新的数据。例如创建一个新的方法缓存并从类中指向它,初始化类相关的子类和父类 dirty memory是类数据被分成两部分的主要原因

dirty memory要比clean memory昂贵的多,只要程序运行它就必须一直存在,通过分离出那些不会被改变的数据,可以把大部分的类数据存储为clean memory

请看下面这个图(WWDC2020视频里面截取的)

class_ro_t

从图中我们知道成员变量在class_ro_t里面,那么我们打印一下看看

ro

既然里面有ivars成员信息,那么再打印出来看看

(lldb) p $6.ivars
(const ivar_list_t *const) $7 = 0x0000000100008130
(lldb) p *$7
(const ivar_list_t) $8 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 3)
}
(lldb) p $7.get(0)
(ivar_t) $9 = {
  offset = 0x0000000100008340
  name = 0x0000000100003f2a "age"
  type = 0x0000000100003f7e "i"
  alignment_raw = 2
  size = 4
}
  Fix-it applied, fixed expression was: 
    $7->get(0)
(lldb) p $7.get(1)
(ivar_t) $10 = {
  offset = 0x0000000100008348
  name = 0x0000000100003f2e "_name"
  type = 0x0000000100003f80 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
  Fix-it applied, fixed expression was: 
    $7->get(1)
(lldb) p $7.get(2)
(ivar_t) $11 = {
  offset = 0x0000000100008350
  name = 0x0000000100003f34 "_hobby"
  type = 0x0000000100003f80 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
  Fix-it applied, fixed expression was: 
    $7->get(2)
(lldb) 

成员信息的数据类型,名称、内存大小,都打印出来了。

类方法

在上面的测试中,JPPerson的对象方法可以正常打印出来,是在JPPerson.class中获取打印的。在MachOView中,也是可以看到,类方法确实存在的。 在这里插入图片描述

那么类方法也就是加号方法,是否是在元类中的呢?对象方法在类中,类方法在元类中,这不是很符合逻辑吗?好,那就去探索验证一下

(lldb) x/4gx JPPerson.class
0x100008358: 0x0000000100008380 0x000000010036a140
0x100008368: 0x00000001003623c0 0x0000802800000000
(lldb) p/x 0x0000000100008380 + 0x20
(long) $19 = 0x00000001000083a0
(lldb) p/x (class_data_bits_t*)0x00000001000083a0
(class_data_bits_t *) $20 = 0x00000001000083a0
(lldb) p $20->data()
(class_rw_t *) $21 = 0x0000000101337080
(lldb) p *$21
(class_rw_t) $22 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4315117329
    }
  }
  firstSubclass = 0x00000001000083a8
  nextSiblingClass = 0x00007fff80111eb0
}
(lldb) p $22.methods()
(const method_array_t) $23 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100008208
      }
      arrayAndFlag = 4295000584
    }
  }
}
(lldb) p $23.list.ptr
(method_list_t *const) $24 = 0x0000000100008208
(lldb) p *$24
(method_list_t) $25 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $25.get(0).big()
(method_t::big) $26 = {
  name = "sayNB"
  types = 0x0000000100003f94 "v16@0:8"
  imp = 0x0000000100003da0 (JPObjcBuild`+[JPPerson sayNB])
}
(lldb) 

哈哈,还有谁 还有谁? 这波操作,就问你服不服!

总结

  • 元类isa指向:元类isa->根元类isa->根元类(NSObject的元类)
  • 元类继承关系:类继承isa->根元类isa->NSObject->nil
  • 类中有isa、superclass、chche、bits 成员变量,
  • bits 存储着属性列表、方法列表、成员变量列表、协议列表等
  • method_t是对方法函数的封装

补充

我们在打印方法的时候,method_t里面的big结构里面有个types,从lldb调试或者clang看底层,都经常看到类似这种"v16@0:8""v24@0:8@16",这是Type Encodings,类型编码。iOS提供了一个叫@encode的指令,可以将具体的类型表示成字符串编码! 例如"v16@0:8"

  • v 表示viod,无返回值
  • 16 表示占了16个字节
  • @ 表示 参数id self
  • 0 表示id 0号位开始
  • 表示 SEL,,是方法编号
  • 8 表示 SEL开始从8号位

id8字节,sel 也是占8个字节,刚好一共就是16个字节

  • Type Encodings图表

Objective-C type encodings

  • method_t总结

method_t总结

更多内容持续更新

iOS底层探索之类的结构(下) 🌹 请动动你的小手,点个赞👍🌹

🌹 喜欢的可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹