深入解析 ObjC 中方法的结构

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 x86_64 架构下运行的,对于在 arm64 中运行的代码会特别说明。 在上一篇分析 isa 的文章从 NSObject 的初始化了解 isa中曾经说到过实例方法被调用时,会通过其持有 isa 指针寻找对应的类,然后在其中的 class_d


因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 x86_64 架构下运行的,对于在 arm64 中运行的代码会特别说明。

在上一篇分析 isa 的文章从 NSObject 的初始化了解 isa中曾经说到过实例方法被调用时,会通过其持有 isa 指针寻找对应的类,然后在其中的 class_data_bits_t 中查找对应的方法,在这一篇文章中会介绍方法在 ObjC 中是如何存储方法的。

这篇文章的首先会根据 ObjC 源代码来分析方法在内存中的存储结构,然后在 lldb 调试器中一步一步验证分析的正确性。

方法在内存中的位置

先来了解一下 ObjC 中类的结构图:

objc-method-class

  • 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-method-class-data-bits-t

在 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] 位,所以可以使用最后三位来存储关于当前类的其他信息:

objc-method-class_data_bits_t

#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-method-before-realize

然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:

  1. 从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
  2. 初始化一个 class_rw_t 结构体
  3. 设置结构体 ro 的值以及 flag
  4. 最后设置正确的 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 方法执行过后的类所占用内存的布局,你可以与上面调用方法前的内存布局对比以下,看有哪些更改:


objc-method-after-realize-class

但是,在这段代码运行之后 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 或者真机上运行,可能有一定差别。


objc-method-target

这是主程序的代码:

#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 方法中加入一个断点:

objc-method-after-compile

然后在 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
}

objc-method-lldb-print-before-realize

现在我们获取了类经过编译器处理后的只读属性 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、 ivarsweakIvarLayout 和 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)

objc-method-lldb-print-method-list

使用 $5->get(0) 时,成功获取到了 -[XXObject hello] 方法的结构体 method_t。而尝试获取下一个方法时,断言提示我们当前类只有一个方法。

realizeClass

这篇文章中不会对 realizeClass 进行详细的分析,该方法的主要作用是对类进行第一次初始化,其中包括:

  • 分配可读写数据空间
  • 返回真正的类结构
static Class realizeClass(Class cls)  

上面就是这个方法的签名,我们需要在这个方法中打一个条件断点,来判断当前类是否为XXObject

objc-method-lldb-breakpoint

这里直接判断两个指针是否相等,而不使用 [NSStringFromClass(cls) isEqualToString:@"XXObject"] 是因为在这个时间点,这些方法都不能调用,在 ObjC 中没有这些方法,所以只能通过判断类指针是否相等的方式来确认当前类是 XXObject

直接与指针比较是因为类在内存中的位置是编译期确定的,只要代码不改变,类在内存中的位置就会不变(已经说过很多遍了)。

objc-method-breakpoint-before-set-r

这个断点就设置在这里,因为 XXObject 是一个正常的类,所以会走 else 分支分配可写的类数据。

运行代码时,因为每次都会判断当前类指针是不是指向的 XXObject,所以会等一会才会进入断点。

在这时打印类结构体中的 data 的值,发现其中的布局依旧是这样的:

objc-method-before-realize

在运行完这段代码之后:

objc-method-after-realize-breakpoint

我们再来打印类的结构:

(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)

objc-method-print-class-struct-after-realize

最后一个操作实在是截取不到了

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 中进行设置:

objc-method-after-methodizeClass

在这里调用了 method_array_t 的 attachLists 方法,将 baseMethods 中的方法添加到 methods 数组之后。我们访问 methods 才会获取当前类的实例方法。

方法的结构

说了这么多,到现在我们可以简单看一下方法的结构,与类和对象一样,方法在内存中也是一个结构体。

struct method_t {  
    SEL name;
    const char *types;
    IMP imp;
};

其中包含方法名,类型还有方法的实现指针 IMP

obj-method-struct

上面的 -[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 等成员以及类在内存中的位置都是编译期决定的),才感觉到豁然开朗。

  1. 类在内存中的位置是在编译期间决定的,在之后修改代码,也不会改变内存中的位置。
  2. 类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到 realizeClass 执行之后,才放到了 class_rw_t 指向的只读区域 class_ro_t,这样我们即可以在运行时为 class_rw_t 添加方法,也不会影响类的只读结构。
  3. 在 class_ro_t 中的属性在运行期间就不能改变了,再添加方法时,会修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods,对于方法的添加会在之后的文章中分析。

参考资料


目录
相关文章
|
2月前
|
人工智能
歌词结构的巧妙安排:写歌词的方法与技巧解析,妙笔生词AI智能写歌词软件
歌词创作是一门艺术,关键在于巧妙的结构安排。开头需迅速吸引听众,主体部分要坚实且富有逻辑,结尾则应留下深刻印象。《妙笔生词智能写歌词软件》提供多种 AI 功能,帮助创作者找到灵感,优化歌词结构,写出打动人心的作品。
|
2月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
50 3
|
2月前
|
人工智能
写歌词的技巧和方法全解析:开启你的音乐创作之旅,妙笔生词智能写歌词软件
怀揣音乐梦想,渴望用歌词抒发情感?掌握关键技巧,你也能踏上创作之旅。灵感来自生活点滴,主题明确,语言简洁,韵律和谐。借助“妙笔生词智能写歌词软件”,AI辅助创作,轻松写出动人歌词,实现音乐梦想。
|
12天前
|
负载均衡 网络协议 算法
Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式
本文探讨了Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式,以及软件负载均衡器、云服务负载均衡、容器编排工具等实现手段,强调两者结合的重要性及面临挑战的应对措施。
32 3
|
29天前
|
JSON PHP 数据格式
PHP解析配置文件的常用方法
INI文件是最常见的配置文件格式之一。
46 12
|
1月前
|
机器学习/深度学习 人工智能 安全
TPAMI:安全强化学习方法、理论与应用综述,慕工大、同济、伯克利等深度解析
【10月更文挑战第27天】强化学习(RL)在实际应用中展现出巨大潜力,但其安全性问题日益凸显。为此,安全强化学习(SRL)应运而生。近日,来自慕尼黑工业大学、同济大学和加州大学伯克利分校的研究人员在《IEEE模式分析与机器智能汇刊》上发表了一篇综述论文,系统介绍了SRL的方法、理论和应用。SRL主要面临安全性定义模糊、探索与利用平衡以及鲁棒性与可靠性等挑战。研究人员提出了基于约束、基于风险和基于监督学习等多种方法来应对这些挑战。
59 2
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
22 1
|
2月前
|
SQL 监控 数据库
SQL语句是否都需要解析及其相关技巧和方法
在数据库管理中,SQL(结构化查询语言)语句的使用无处不在,它们负责数据的查询、插入、更新和删除等操作
|
1月前
|
机器学习/深度学习 自然语言处理 数据管理
GraphRAG核心组件解析:图结构与检索增强生成
【10月更文挑战第28天】在当今数据科学领域,自然语言处理(NLP)和图数据管理技术的发展日新月异。GraphRAG(Graph Retrieval-Augmented Generation)作为一种结合了图结构和检索增强生成的创新方法,已经在多个应用场景中展现出巨大的潜力。作为一名数据科学家,我对GraphRAG的核心组件进行了深入研究,并在此分享我的理解和实践经验。
60 0
|
1月前
光纤电缆(FOC)的结构深度解析
【10月更文挑战第21天】
57 0

推荐镜像

更多