深入解析 ObjC 中方法的结构
2009-04-14 00:00
1381 查看
方法在内存中的位置
class_data_bits_t 结构体
class_rw_t 和 class_ro_t
XXObject
编译后内存中类的结构
realizeClass
方法的结构
小结
参考资料
Blog: Draveness
关注仓库,及时获得更新:iOS-Source-Code-Analyze
因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是
在上一篇分析
这篇文章的首先会根据 ObjC 源代码来分析方法在内存中的存储结构,然后在 lldb 调试器中一步一步验证分析的正确性。
先来了解一下 ObjC 中类的结构图:
这一小结会分析类结构体中的
下面就是 ObjC 中
在
它为我们提供了便捷方法用于返回其中的
将
在 x86_64 架构上,Mac OS 只使用了其中的 47 位来为对象分配地址。而且由于地址要按字节在内存中按字节对齐,所以掩码的后三位都是 0。
因为
执行
ObjC 类中的属性、方法还有遵循的协议等信息都保存在
其中还有一个指向常量的指针
在编译期间类的结构中的
然后在加载 ObjC 运行时的过程中在
从
初始化一个
设置结构体
最后设置正确的
下图是
但是,在这段代码运行之后
下面,我们将分析一个类
这段代码是运行在 Mac OS X 10.11.3 (x86_64)版本中,而不是运行在 iPhone 模拟器或者真机上的,如果你在 iPhone 或者真机上运行,可能有一定差别。
这是主程序的代码:
因为类在内存中的位置是编译期就确定的,先运行一次代码获取
接下来,在整个 ObjC 运行时初始化之前,也就是
然后在 lldb 中输入以下命令:
现在我们获取了类经过编译器处理后的只读属性
可以看到这里面只有
通过下面的命令查看
使用
这篇文章中不会对
分配可读写数据空间
返回真正的类结构
上面就是这个方法的签名,我们需要在这个方法中打一个条件断点,来判断当前类是否为
这里直接判断两个指针是否相等,而不使用
直接与指针比较是因为类在内存中的位置是编译期确定的,只要代码不改变,类在内存中的位置就会不变(已经说过很多遍了)。
这个断点就设置在这里,因为
运行代码时,因为每次都会判断当前类指针是不是指向的
在这时打印类结构体中的
在运行完这段代码之后:
我们再来打印类的结构:
最后一个操作实在是截取不到了
在上述的代码运行之后,类的只读指针
在这里调用了
说了这么多,到现在我们可以简单看一下方法的结构,与类和对象一样,方法在内存中也是一个结构体。
其中包含方法名,类型还有方法的实现指针
上面的
方法的名字在这里没有什么好说的。其中,方法的类型是一个非常奇怪的字符串
对于方法的实现,lldb 为我们标注了方法在文件中实现的位置。
在分析方法在内存中的位置时,笔者最开始一直在尝试寻找只读结构体
而且在 runtime 初始化之后,
后来突然想到类的一些方法、属性和协议实在编译期决定的(
类在内存中的位置是在编译期间决定的,在之后修改代码,也不会改变内存中的位置。
类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到
在
Classes and Metaclasses
Tagged Pointer
类型编码
Type Encodings
关注仓库,及时获得更新:iOS-Source-Code-Analyze
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。
class_data_bits_t 结构体
class_rw_t 和 class_ro_t
XXObject
编译后内存中类的结构
realizeClass
方法的结构
小结
参考资料
Blog: Draveness
关注仓库,及时获得更新:iOS-Source-Code-Analyze
因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是
x86_64架构下运行的,对于在 arm64 中运行的代码会特别说明。
在上一篇分析
isa的文章从 NSObject 的初始化了解 isa中曾经说到过实例方法被调用时,会通过其持有
isa指针寻找对应的类,然后在其中的
class_data_bits_t中查找对应的方法,在这一篇文章中会介绍方法在 ObjC 中是如何存储方法的。
这篇文章的首先会根据 ObjC 源代码来分析方法在内存中的存储结构,然后在 lldb 调试器中一步一步验证分析的正确性。
方法在内存中的位置
先来了解一下 ObjC 中类的结构图:isa是指向元类的指针,不了解元类的可以看 Classes and Metaclasses
super_class指向当前类的父类
cache用于缓存指针和
vtable,加速方法的调用
bits就是存储类的方法、属性、遵循的协议等信息的地方
class_data_bits_t
结构体
这一小结会分析类结构体中的 class_data_bits_t bits。
下面就是 ObjC 中
class_data_bits_t的结构体,其中只含有一个 64 位的
bits用于存储与类有关的信息:
在
objc_class结构体中的注释写到
class_data_bits_t相当于
class_rw_t指针加上 rr/alloc 的标志。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
它为我们提供了便捷方法用于返回其中的
class_rw_t *指针:
class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); }
将
bits与
FAST_DATA_MASK进行位运算,只取其中的
[3, 47]位转换成
class_rw_t *返回。
在 x86_64 架构上,Mac OS 只使用了其中的 47 位来为对象分配地址。而且由于地址要按字节在内存中按字节对齐,所以掩码的后三位都是 0。
因为
class_rw_t *指针只存于第
[3, 47]位,所以可以使用最后三位来存储关于当前类的其他信息:
#define FAST_IS_SWIFT (1UL<<0) #define FAST_HAS_DEFAULT_RR (1UL<<1) #define FAST_REQUIRES_RAW_ISA (1UL<<2) #define FAST_DATA_MASK 0x00007ffffffffff8UL
isSwift()
FAST_IS_SWIFT用于判断 Swift 类
hasDefaultRR()
FAST_HAS_DEFAULT_RR当前类或者父类含有默认的
retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference方法
requiresRawIsa()
FAST_REQUIRES_RAW_ISA当前类的实例需要 raw
isa
执行
class_data_bits_t结构体中的
data()方法或者调用
objc_class中的
data()方法会返回同一个
class_rw_t *指针,因为
objc_class中的方法只是对
class_data_bits_t中对应方法的封装。
// objc_class 中的 data() 方法
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
// class_data_bits_t 中的 data() 方法
uintptr_t bits;
class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); }
class_rw_t
和 class_ro_t
ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t中:
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; };
其中还有一个指向常量的指针
ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
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_data_bits_t *data指向的是一个
class_ro_t *指针:
然后在加载 ObjC 运行时的过程中在
realizeClass方法中:
从
class_data_bits_t调用
data方法,将结果从
class_rw_t强制转换为
class_ro_t指针
初始化一个
class_rw_t结构体
设置结构体
ro的值以及
flag
最后设置正确的
data。
const class_ro_t *ro = (const class_ro_t *)cls->data(); class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); rw->ro = ro; rw->flags = RW_REALIZED|RW_REALIZING; cls->setData(rw);
下图是
realizeClass方法执行过后的类所占用内存的布局,你可以与上面调用方法前的内存布局对比以下,看有哪些更改:
但是,在这段代码运行之后
class_rw_t中的方法,属性以及协议列表均为空。这时需要
realizeClass调用
methodizeClass方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到
methods、
properties和
protocols列表中。
XXObject
下面,我们将分析一个类 XXObject在运行时初始化过程中内存的更改,这是
XXObject的接口与实现:
// XXObject.h 文件 #import <Foundation/Foundation.h> @interface XXObject : NSObject - (void)hello; @end // XXObject.m 文件 #import "XXObject.h" @implementation XXObject - (void)hello { NSLog(@"Hello"); } @end
这段代码是运行在 Mac OS X 10.11.3 (x86_64)版本中,而不是运行在 iPhone 模拟器或者真机上的,如果你在 iPhone 或者真机上运行,可能有一定差别。
这是主程序的代码:
#import <Foundation/Foundation.h> #import "XXObject.h" int main(int argc, const char * argv[]) { @autoreleasepool { Class cls = [XXObject class]; NSLog(@"%p", cls); } return 0; }
编译后内存中类的结构
因为类在内存中的位置是编译期就确定的,先运行一次代码获取 XXObject在内存中的地址。
0x100001168
接下来,在整个 ObjC 运行时初始化之前,也就是
_objc_init方法中加入一个断点:
然后在 lldb 中输入以下命令:
(lldb) p (objc_class *)0x100001168(objc_class *) $0 = 0x0000000100001168
(lldb) p (class_data_bits_t *)0x100001188
(class_data_bits_t *) $1 = 0x0000000100001188
(lldb) p $1->data()
warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
(class_rw_t *) $2 = 0x00000001000010e8
(lldb) p (class_ro_t *)$2 // 将 class_rw_t 强制转化为 class_ro_t
(class_ro_t *) $3 = 0x00000001000010e8
(lldb) p *$3
(class_ro_t) $4 = { flags = 128 instanceStart = 8 instanceSize = 8 reserved = 0 ivarLayout = 0x0000000000000000 <no value available> name = 0x0000000100000f7a "XXObject" baseMethodList = 0x00000001000010c8 baseProtocols = 0x0000000000000000 ivars = 0x0000000000000000 weakIvarLayout = 0x0000000000000000 <no value available> baseProperties = 0x0000000000000000 }
现在我们获取了类经过编译器处理后的只读属性
class_ro_t:
(class_ro_t) $4 = { flags = 128 instanceStart = 8 instanceSize = 8 reserved = 0 ivarLayout = 0x0000000000000000 <no value available> name = 0x0000000100000f7a "XXObject" baseMethodList = 0x00000001000010c8 baseProtocols = 0x0000000000000000 ivars = 0x0000000000000000 weakIvarLayout = 0x0000000000000000 <no value available> baseProperties = 0x0000000000000000 }
可以看到这里面只有
baseMethodList和
name是有值的,其它的
ivarLayout、
baseProtocols、
ivars、
weakIvarLayout和
baseProperties都指向了空指针,因为类中没有实例变量,协议以及属性。所以这里的结构体符合我们的预期。
通过下面的命令查看
baseMethodList中的内容:
(lldb) p $4.baseMethodList (method_list_t *) $5 = 0x00000001000010c8 (lldb) p $5->get(0) (method_t) $6 = { name = "hello" types = 0x0000000100000fa4 "v16@0:8" imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13) } (lldb) p $5->get(1) Assertion failed: (i < count), function get, file /Users/apple/Desktop/objc-runtime/runtime/objc-runtime-new.h, line 110. error: Execution was interrupted, reason: signal SIGABRT. The process has been returned to the state before expression evaluation. (lldb)
使用
$5->get(0)时,成功获取到了
-[XXObject hello]方法的结构体
method_t。而尝试获取下一个方法时,断言提示我们当前类只有一个方法。
realizeClass
这篇文章中不会对 realizeClass进行详细的分析,该方法的主要作用是对类进行第一次初始化,其中包括:
分配可读写数据空间
返回真正的类结构
static Class realizeClass(Class cls)
上面就是这个方法的签名,我们需要在这个方法中打一个条件断点,来判断当前类是否为
XXObject:
这里直接判断两个指针是否相等,而不使用
[NSStringFromClass(cls) isEqualToString:@"XXObject"]是因为在这个时间点,这些方法都不能调用,在 ObjC 中没有这些方法,所以只能通过判断类指针是否相等的方式来确认当前类是
XXObject。
直接与指针比较是因为类在内存中的位置是编译期确定的,只要代码不改变,类在内存中的位置就会不变(已经说过很多遍了)。
这个断点就设置在这里,因为
XXObject是一个正常的类,所以会走
else分支分配可写的类数据。
运行代码时,因为每次都会判断当前类指针是不是指向的
XXObject,所以会等一会才会进入断点。
在这时打印类结构体中的
data的值,发现其中的布局依旧是这样的:
在运行完这段代码之后:
我们再来打印类的结构:
(lldb) p (objc_class *)cls // 打印类指针 (objc_class *) $262 = 0x0000000100001168 (lldb) p (class_data_bits_t *)0x0000000100001188 // 在类指针上加 32 的 offset 打印 class_data_bits_t 指针 (class_data_bits_t *) $263 = 0x0000000100001188 (lldb) p *$263 // 访问 class_data_bits_t 指针的内容 (class_data_bits_t) $264 = (bits = 4302315312) (lldb) p $264.data() // 获取 class_rw_t (class_rw_t *) $265 = 0x0000000100701f30 (lldb) p *$265 // 访问 class_rw_t 指针的内容,发现它的 ro 已经设置好了 (class_rw_t) $266 = { flags = 2148007936 version = 0 ro = 0x00000001000010e8 methods = { list_array_tt<method_t, method_list_t> = { = { list = 0x0000000000000000 arrayAndFlag = 0 } } } 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 = nil demangledName = 0x0000000000000000 <no value available> } (lldb) p $266.ro // 获取 class_ro_t 指针 (const class_ro_t *) $267 = 0x00000001000010e8 (lldb) p *$267 // 访问 class_ro_t 指针的内容 (const class_ro_t) $268 = { flags = 128 instanceStart = 8 instanceSize = 8 reserved = 0 ivarLayout = 0x0000000000000000 <no value available> name = 0x0000000100000f7a "XXObject" baseMethodList = 0x00000001000010c8 baseProtocols = 0x0000000000000000 ivars = 0x0000000000000000 weakIvarLayout = 0x0000000000000000 <no value available> baseProperties = 0x0000000000000000 } (lldb) p $268.baseMethodList // 获取基本方法列表 (method_list_t *const) $269 = 0x00000001000010c8 (lldb) p $269->get(0) // 访问第一个方法 (method_t) $270 = { name = "hello" types = 0x0000000100000fa4 "v16@0:8" imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13) } (lldb) p $269->get(1) // 尝试访问第二个方法,越界 error: Execution was interrupted, reason: signal SIGABRT. The process has been returned to the state before expression evaluation. Assertion failed: (i < count), function get, file /Users/apple/Desktop/objc-runtime/runtime/objc-runtime-new.h, line 110. (lldb)
最后一个操作实在是截取不到了
const class_ro_t *ro = (const class_ro_t *)cls->data(); class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); rw->ro = ro; rw->flags = RW_REALIZED|RW_REALIZING; cls->setData(rw);
在上述的代码运行之后,类的只读指针
class_ro_t以及可读写指针
class_rw_t都被正确的设置了。但是到这里,其
class_rw_t部分的方法等成员都指针均为空,这些会在
methodizeClass中进行设置:
在这里调用了
method_array_t的
attachLists方法,将
baseMethods中的方法添加到
methods数组之后。我们访问
methods才会获取当前类的实例方法。
方法的结构
说了这么多,到现在我们可以简单看一下方法的结构,与类和对象一样,方法在内存中也是一个结构体。struct method_t { SEL name; const char *types; IMP imp; };
其中包含方法名,类型还有方法的实现指针
IMP:
上面的
-[XXObject hello]方法的结构体是这样的:
name = "hello" types = 0x0000000100000fa4 "v16@0:8" imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13
方法的名字在这里没有什么好说的。其中,方法的类型是一个非常奇怪的字符串
"v16@0:8"这在 ObjC 中叫做类型编码(Type Encoding),你可以看这篇官方文档了解与类型编码相关的信息。
对于方法的实现,lldb 为我们标注了方法在文件中实现的位置。
小结
在分析方法在内存中的位置时,笔者最开始一直在尝试寻找只读结构体 class_ro_t中的
baseMethods第一次设置的位置(了解类的方法是如何被加载的)。尝试从
methodizeClass方法一直向上找,直到
_obj_init方法也没有找到设置只读区域的
baseMethods的方法。
而且在 runtime 初始化之后,
realizeClass之前,从
class_data_bits_t结构体中获取的
class_rw_t一直都是错误的,这个问题在最开始非常让我困惑,直到后来在
realizeClass中发现原来在这时并不是
class_rw_t结构体,而是
class_ro_t,才明白错误的原因。
后来突然想到类的一些方法、属性和协议实在编译期决定的(
baseMethods等成员以及类在内存中的位置都是编译期决定的),才感觉到豁然开朗。
类在内存中的位置是在编译期间决定的,在之后修改代码,也不会改变内存中的位置。
类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到
realizeClass执行之后,才放到了
class_rw_t指向的只读区域
class_ro_t,这样我们即可以在运行时为
class_rw_t添加方法,也不会影响类的只读结构。
在
class_ro_t中的属性在运行期间就不能改变了,再添加方法时,会修改
class_rw_t中的
methods列表,而不是
class_ro_t中的
baseMethods,对于方法的添加会在之后的文章中分析。
参考资料
Classes and MetaclassesTagged Pointer
类型编码
Type Encodings
关注仓库,及时获得更新:iOS-Source-Code-Analyze
关于图片和转载
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。
关于评论和留言
如果对本文 深入解析 ObjC 中方法的结构相关文章推荐
- 深入解析 ObjC 中方法的结构
- 深入解析 ObjC 中方法的结构
- 深入解析 Objective-C 中方法的结构
- 深入解析Android类库打包方法
- java中String的一些方法深入解析
- 深入学习python解析并读取PDF文件内容的方法
- 深入解析Java编程中方法的参数传递
- 深入解析Swift编程中的构造方法
- 基于mysql体系结构的深入解析
- 扩展方法从简单应用到深入解析,读这一篇文章就够了
- 动态加载css方法实现和深入解析
- 深入解析MFC -- CString的内存结构
- 深入解析DB2 - 高级管理、内部体系结构与诊断案例
- java 8 Hashmap深入解析 —— put get 方法源码
- Java equals 方法与hashcode 方法的深入解析
- 基于mysql体系结构的深入解析
- 深入解析:半连接与反连接的原理和等价改写方法
- 深入解析硬盘结构
- java-String类深入剖析和 intern()方法解析
- 动态加载css方法实现和深入解析