上篇文章我们已经对类中的属性
(property
)、实例变量
(ivar
)、对象方法
(Method
,也叫做实例方法
)、类方法
进行了初步的探究,我们可以通过类结构体中bits成员变量获取到结构体class_rw_t
变量中存储的属性与对象方法以及获取到结构体class_ro_t
中存储的实例变量,通过元类结构体中bits成员变量获取到结构体class_rw_t
变量中存储的类方法,但是我们仍然有很多疑问,为什么需要两各结构体来存储一样的数据呢?这样不是很耗内存吗?因此我们今天就来接着探究类的结构。
1. 2020年WWDC视频主要内容
首先,我们先来看看苹果2020年WWDC视频,视频中苹果开发者主要对底层数据结构做了如下的三个修改。
1.1 类的运行时数据变化
首先,在磁盘上,在你的APP二进制文件中类是这样的,如下所示:
这个类对象本身,它包含了最常被访问的信息,就是指向元类、超类以及方法缓存的指针,除此之外,还有一个指向更多数据的指针,存储额外信息的地方叫做class_ro_t
,ro
代表只读,这结构中,包含类名、方法、协议和实例变量的信息,如下图所示:
当类第一次从磁盘加载到内存中时,它们一开始也是这样的,但是一经使用,它们就会发生变化,但是在了解这些变化之前,我们有必要了解一下什么是clean memory
以及dirty memory
的区别
clean memory
:指加载后不会发生更改的内存。dirty memory
:指在进程运行时会发生更改的内存。 而class_ro_t
就属于clean memory
,因为它是只读的,类结构一经使用就会变成dirty memory
,因为运行时会向它写入新的数据(例如:创建一个新的方法缓存并从类中指向它)。dirty memory
比clean memory
要昂贵得多,只要进程在运行,它就必须一直存在,另一方面clean memory
可以进行移除,从而节省更多的内存空间。因为如果你需要clean memory
,系统可以从磁盘中重新加载。macOS
可以选择换出dirty memory
,但因为iOS
不使用swap
,所以dirty memory
在iOS
中代价很大。dirty memory
是这个类数据被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为clean memory
,虽然这些数据足以让我们开始,但运行时需要追踪每个类的更多信息。所有,当一个类首次被使用,运行时会为它分配额外的存储容量,这个运行时分配的存储容量是class_rw_t
用于读取-编写数据,如下图所示:
在这个数据结构中,我们存储了只有在运行时才会生成的新信息(First Subclass
,Next Sibing Class
),例如:所有的类都会链接成一个树状结构。这是通过使用First Subclass
和Next Sibing Class
指针实现的。这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。但为什么方法和属性也在只读数据中时?class_rw_t
中也要有方法和属性呢?因为它们可以在运行时进行更改,当category
被加载时,它可以向类中添加新的方法,而且程序员可以使用运行时API
动态地添加它们。因为class_ro_t
是只读的,所以我们需要在class_rw_t
中追踪这些东西。
但是以上做法导致的结果是会占用相当多的内存,在任何给定的设备中都有许多类在使用,那么我们如何缩小这些结构呢?记住,我们在class_rw_t
中需要这些东西,因为这些东西可以在运行时进行更改。但是通过检查实际设备上的使用情况,我们发现大约只有%10
的类真正地更改了它们的方法,而且只有Swift
类会使用demangled name
字段,并且Swift类并不需要这一字段,除非有东西询问它们的Objective-C
名称时才需要,所有我们可以拆掉那些平时不用的部分,这将class_rw_t
的大小减少了一半,如下图所示:
对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用,如下图所示:
大约%90
的类从来不需要这些扩展数据,在系统范围内可节省大约14MB
的内存,这些内存可以用于更有效的用途,比如存储你的App
数据。这对于dirty memory
来说,这是真正的节省内存,现在,很多从类中获取数据的代码,必须同时处理那些有扩展数据和没有扩展数据的类,当然,运行时会为你处理这一切,并且从外部看,一切都像往常一样工作,只是使用更少的内存,之所以会这样,是因为读取这些结构的代码,都在运行时内并且还会同时进行更新,坚持使用这些API
真的很重要,因为任何视图直接访问这些数据结构的代码都将在今年的OS
版本中停止工作,如下图所示:
因为数据结构已经发生了变化,而且该代码不知道新的布局,除了你自己的代码,也要注意那些外部依赖性,你可能正把它们带入你的app
中,它们可能会在你没有意识到的情况下,挖掘这些数据结构,这些结构中的所有信息,都可以通过官方API
获取,一些API
如下图所示:
当你使用这些API
访问信息时,无论Apple
在后台进行什么更改,这些APIS
都将继续工作,所有的API
都可以在Objective-C
运行时说明文档中找到,而这个文档在developer.apple.com
中。
2.2 相对方法列表的变化
每个类都附带一个方法列表,如下图所示:
当你在类上编写新的方法时,它就会被添加到列表中,每个方法都包含三个信息:
-
方法的名称(选择器,也称为方法编号):这是一串字符串,具有唯一性,所以它们可以使用指针相等来进行比较。
-
方法类型编码:这是一个表示参数和返回类型的字符串,它不是用来发送消息的,但它是运行时
introspection
和消息forwarding
所必需的东西。 -
指向方法实现的指针:方法的实际代码,当你编写一个方法时,它会被编译成一个
C
函数,其中包含你的代码实现,然后方法列表中的entry
会指向该函数。
我们来看一个具体的方法,例如init
方法,它包含的条目有方法名称、类型和实现,方法列表中的每一条数据都是一个指针,在64
位系统中,每个方法条目占用24个
字节,如下图所示:
虽然这个是clean memory
,但它并不是免费的,它还是必须从磁盘中加载并且使用时会占用内存,例如,下面是一个进程中内存的放大视图:
注意这不是按比例放大的,这有一个很大的地址空间,它需要64
位来寻址,在这个地址空间内,它划分成了几个部分,分别为栈
、堆
、可执行文件
、库
或者二进制图
像。而这些都加载到了进程中,这里使用蓝色表示,让我们放大并查看其中的一个二进制图像
,这里我们显示了三个方法表条目,它们指向其二进制文件中的位置,如下图所示:
这向我们展示了另一个代价,二进制图像可以加载到内存中的任何地方,这取决于动态链接器决定把它放在哪里,这意味着,链接器需要将指针解析到二进制图像中,并在加载时将其修正为指向其在内存中的实际位置,而这也是有代价的,现在,请注意,一个来自二进制文件的类方法条目,永远只指向该二进制文件内的方法实现,我们不可能使一个方法的元数据存在于一个二进制文件中,而实现它的代码在另一个二进制文件中,这意味着,方法列表条目,实际上并不需要能够引用整个64
位地址空间,它们只需要能够引用自己二进制中的函数,而且这些函数总是在附近,因此无需使用绝对的64
位地址,它们可使用二进制中的32
位的相对偏移,如下图所示:
这样做有几个好处:
-
安全性更高:偏移量始终是相同的,不管
image
在哪里加载到内存中,所有它们从磁盘加载后不需要进行修正,而由于它们不需要进行修正,所以它们可以存储在真正的只读内存中,这样更安全。 -
节省内存空间:
32
位的偏移意味着我们已经将64
位平台上所需的内存量减少了,在一台典型iPhone
中的系统范围内,测量了约80MB
的这些方法,因为这些方法的尺寸剪版,所以节省了40MB
的内存,这样你的APP
就有更多的内存,从而可以让你的用户体验更好,如下图所示:
那么对于swizzling
又是如何处理的呢?
二进制中的方法列表,现在不能引用完整的地址空间,但如果swizzle
一个方法,它就可以在任何地方实现,而且,我们希望保存这些方法列表为只读,为了处理这个问题,苹果提供了一个全局表,这个全局表将方法映射到它们被swizzle
的实现上,swizzle
并不常见,实际上绝大多数方法从未被swizzle
过,所以这个表最终不会变得很大,更好的是,这个表会很紧凑,内存每次都是按页来“弄脏”的,使用旧式的方法列表,swizzle
一个方法,会“弄脏”它所在的整个页,一次swizzle
就会导致产生大量千字节的dirty memory
,有了这个全局表,只需为一个额外的表的条目付出代价,和往常一样,你看不见这些变化,一切都会像以前一样照样进行,这些相对方法列表在新的OS
版本上是受支持的,当你使用相应的最小部署目标进行构建时,工具会自动在你的二进制文件中生成相对方法列表,如果你需要针对旧OS
版本的方法列表,也不用担心,XCode
也会生成旧式的方法列表格式,这仍然是完全受支持的,你仍然可以从使用新的相对方法列表构建的OS
本身中受益,而且系统可以同时在同一app
中使用两种格式,不过如果你能针对今年的OS
版本进行构建,你的二进制文件会变小,并且你所使用的内存也会减少,总的来说,这在Objective-C
或Swift
中是一个不错的提示,最小的部署目标,这并不仅是关于你可以使用那些SDK API
,如果XCode
知道它不需要支持旧的OS
版本,它通常可以发布更好的优化代码或数据,我们理解你们中的许多人需要支持更旧的OS
版本,但这就是为什么无论何时你都可以增加部署目标是一个好主意。
现在,有一件事需要注意,那就是使用一个比你打算使用的更新的部署目标进行构建,XCode
一般会阻止这种情况的发生,但也有可能漏掉,特别是如果你在其他地方构建自己的库或框架,然后把它们带了进来,当在旧的OS
中运行时,旧的运行时会看到这些相对方法,但它对此一无所知,所有它会尝试像旧式的基于指针的方法一样来解释它们,这意味着,它将尝试把一对32位
的字段,作为64位
的指针来读取,这样的结果是,两个整数作为一个指针粘在一起,如下图所示:
这是一个无意义的值,它在实际使用时肯定会崩溃,你可以通过运行时读取方法信息是的崩溃,识别出何时发生了这种情况,在这种情况下,坏指针看起来就像两个32位
值平滑在一起,正如上面图片中所示,如果你运行的代码通过这些结构进行挖掘以读取值,该代码会出现和这些就运行时一样的问题,当用户升级设备时app
就会崩溃,所以不要这样做,请使用API
,不管底层的东西怎么变,那些API都能继续工作,例如:有一些函数,给定其一个方法指针,就会返回其字段的值。
2.3 arm64
上tagged pointer
格式的变化
首先,我们需要知道什么是tagged pointer,我们在此将探索底层真正的实现,但不用担心,就像我们谈论过的所有事情一样,你不需要知道这些,它指示很有趣,也许还有助于你更好地了解你的内存使用情况,首先,来看一下普通对象指针的结构,如下图所示:
通常,当我们看到这些指针时,它们会被打印成这些十六进制数字,我们在之前看到过这些数字,将它分解成二进制表示法,如下图所示:
我们有64
位,然而我们并没有真正地使用到所有这些位,我们只在一个真正的对象指针中使用了中间的这些位,如下图所示:
由于对齐要求的存在,低位始终为0
,如下图所示:
对象必须总是位于指针大小倍数的一个地址中,由于地址空间有限,所以高位始终为0
,我们实际上不会用到2^64
,这些高位和低位总是0
,如下图所示:
所以,让我们从这些始终为0
的位中,选择一个位并把它设置为1
,这可以让我们立即知道这不是一个真正的对象指针,然后我们可以给其他所有位赋予一些其他的意义,我们称这种指针为tagged pointer
,如下图所示:
例如,我们可以在其他位中塞入一个数值,只要我们想让NSNumber
如何读取这些位,并让运行时适当地处理tagged pointer
,系统的其他部分就可以把这些东西当做对象指针来处理,并且永远不会知道其中的区别,这样可以节省我们为每一种类似情况分配一个小数字对象的代价,这是一个重大的改进,顺便说一下,这些值实际上是通过与进程启动时初始化的随机值相结合而被混淆的,这一安全措施使得很难伪造tagged pointer
,在接下来的讨论中,我们将忽略这一点,因为它只是在顶部增加了一层,只是要注意,如果你真的试图在内存中查看这些值,它们会被打乱。
上图就是intel
上tagged pointer
的完整格式,低位设置为1
表示这是一个tagged pointer
,正如我们所讨论到,对于一个真正的指针,这个位必须始终为0
,所有这让我们可以把它们区分开来,接下来的3位
是标签号,如下图所示:
这表示tagged pointer
的类型,例如:3
表示它是一个NSNumber
,6
表示它是一个NSDate
,由于有三个标签位,所以有8种
可能的标签类型,剩下的位是有效负载,如下所示:
这是特定类型可以随意使用的数据,对于标记的NSNumber这是实际的数字,但是现在标签7
有一个特殊情况,它表示一个扩展标签,扩展标签使用接下来的8位
来编码类型,这允许多出256
个标签类型,但是代价是减少有效负载,这使得我们可以将tagged pointer
用于更多的类型,只要它们可以将其数据装入更小的空间,如下图所示:
这可用于一些东西,如:用户界面colors
或NSIndexSets
,现在,如果这对你来说非常方便,你可能会感到失望,因为只有运行时是维护者,即Apple
,可以添加tagged pointer
类型,但如果你是一个Swift
程序员,你会感到高兴,可以创建自己的tagged pointer
类型,如果你曾经使用过一个具有关联值的枚举,那是一个类似于tagged pointer
的类,Swift
运行时将枚举值判别器存储在关联值有效负载的备用位中,而且Swift
对值类型的使用实际上使得tagged pointer
变得没那么重要了,因为值不再需要完全是指针大小,例如:Swift UUID
类型可以是两个字并保持内联,而不是分配一个单独的对象,因为它不适合在一个指针里面,这就是intel
上的tagged pointer
。
我们再来看看ARM
,在arm64上
,这些是反过来的。
最高位设置为1
,而不是最低位用来表示一个tagged pointer
,然后在接下来的3
个位中出现标签位,如下图所示:
而有效负载使用剩余的位,如下图所示:
为什么我们在ARM上使用顶部来表示tagged pointer
,而不是像在intel
上哪有使用底部位来表示呢?这实际上是对objc_msg_Send
的一个小优化,苹果程序员希望msgSend
中最常见的路径可以尽可能的快,而最常见的路径是一个普通的指针,有两种不太常见的情况,tagged point
以及nil
,事实证明,当使用最高位时,可以通过一次比较对这两个进行检查,如下图所示:
相比于分开检查nil
和tagged pointer
,这就为msgSend
中的常见情况节省了一个条件分支,和inter中一样,对于标签7
有一个特殊情况,接下来的8位
被用作扩展标签,然后剩下的位用于有效负载,或者说这其实是iOS13
使用的旧格式,在之后的版本中,做了一些改动,苹果程序员将标签位保持在最高位,因为msgSend
的优化还是非常有用的,标签号现在移到了最下面的3
个位,如果正在使用扩展标签,那么它会占据标签位后的高8位
,如下图所示:
我们为什么要这么做呢?我们再来看看正常指针,如下所示:
我们现有的工具,比如动态链接,会忽略指针的前8位
,这是由于一个名为Top Byte Ignore
的ARM
特性,因此会将扩展标签放在Top Byte Ignore
位,如下图所示:
对于一个对齐指针,底部3
个位总是0
,如下所示:
但是可以改变这一点,只需要通过在指针上添加一个小数字,添加7
以将低位设置为1
,7
表示这是一个扩展标签,如下图所示:
这意味着我们实际上可将上面的这个指针,放入一个扩展标签指针有效负载中,这个结果是一个tagged pointer
以及其有效负载中包含一个正常指针,为什么这很有用呢?因为它开启了tagged pointer
的能力,引用二进制文件中的常量数据的能力,例如:字符串或其他数据结构,否则它们将不得不占用dirty memory
,当然这意味着在iOS 14
之后,直接访问这些位的代码将会失效,在之前,像这样的位检查是可以进行的,但在未来的OS
上会给你错误的答案,然后你的app
会开始莫名其妙地破坏用户数据,如下图所示:
所以不要使用那些依赖于苹果所谈到的任何东西的代码,要使用苹果提供的API
,像isKindOfClass
这样的类型检查,它们在旧的tagged pointer
格式上工作,在新的tagged pointer
格式上也将继续工作,所有的NSString
或NSNumber
方法都将继续工作,这些tagged pointer
中所有信息都可以通过标准的API
来检索,值得注意的是,这也适用于CF
类型,如下图所示:
2. OC类底层Bits字段分析
前一篇文章我们已经详细探讨了OC
类底层bits
字段对于类中的实例方法以及属性的存储,并且刚刚看了2020年苹果WWDC视频
,了解了苹果工程师针对类结构体数据的修改变化,因此我们根据源码做一个总结。
首先在objc_class
结构体中有一个bits
字段,如下所示:
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;
...
...
...
}
而在bits
字段实际上是一个包装了uintptr_t
(unsigned long
类型)的成员变量bits
的结构体class_data_bits_t
,部分代码如下所示:
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
...
...
...
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
...
...
...
}
在这个结构体中,可以通过data
方法获取到结构体指针(class_rw_t *
)的值,其实就是使用bits
与FAST_DATA_MASK
做与运算得到的,而FAST_DATA_MASK
在系统的定义如下:
//在Mac OS中
#define FAST_DATA_MASK 0x00007ffffffffff8UL
//在非Mac OS中
#define FAST_DATA_MASK 0xfffffffcUL
根据不同的系统架构,会使用不同的掩码值,根据wwdc
大会视频中的说法,class_rw_t
中存储的数据是会在运行时使用到的数据,而其数据结构代码如下:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
...
...
...
const ro_or_rw_ext_t get_ro_or_rwe() const {
return ro_or_rw_ext_t{ro_or_rw_ext};
}
void set_ro_or_rwe(const class_ro_t *ro) {
ro_or_rw_ext_t{ro, &ro_or_rw_ext}.storeAt(ro_or_rw_ext, memory_order_relaxed);
}
class_rw_ext_t *ext() const {
return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
}
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
}
其中,我们可以看到了两个很熟悉的函数,ext()
函数返回了一个class_rw_ext_t *
类型的指针,ro()
函数返回了一个class_ro_t *
类型的指针,其中class_rw_ext_t
结构体类型代码如下:
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
class_ro_t
结构体类型代码如下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
...
...
...
}
当类第一次从磁盘加载到内存中时的结构,如下图所示:
当类第一次使用时的结构,如下图所示:
将需要动态更新的部分提取出来,存入class_rw_ext_t
中,如下图所示:
类整体结构如下图所示:
总结:类的实例方法可以通过bits
中的methods
函数获取得到,类的属性可以通过bits
中的properties
函数获取得到,类中定义的成员变量可以通过class_ro_t
结构体中的(ivar_list_t *
类型)ivar
指针获取得到,而类方法可以通过元类中bits
中的methods
函数获取得到。
3. OC对象属性以及方法类型编码解析
3.1 OC对象属性编码解析
首先我们在类中定义几个不同类型的属性,代码如下:
@interface Person : NSObject
@property (nonatomic, weak) id delegate;
@property (nonatomic, strong) NSArray *arrays;
@property (nonatomic, assign) int age;
@property (nonatomic, readwrite) int height;
@property (nonatomic, copy) NSString *name;
@property (atomic, readonly) NSString *hobby;
@end
@implementation Person
@end
通过clang
命令,将main.m
文件编译为c++
文件,clang
命令如下:
//// 加上 -fobjc-runtime=macosx-11.0, 避免 weak 编译不过
clang -rewrite-objc -arch arm64 -fobjc-arc -stdlib=libc++ -mmacosx-version-min=11.0 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations main.m -o main.cpp
查看其中编译好的属性源代码,如下所示:
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[6];
} _OBJC_$_PROP_LIST_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
6,
{{"delegate","T@,W,N,V_delegate"},
{"arrays","T@\"NSArray\",&,N,V_arrays"},
{"age","Ti,N,V_age"},
{"height","Ti,N,V_height"},
{"name","T@\"NSString\",C,N,V_name"},
{"hobby","T@\"NSString\",R,V_hobby"}}
};
你可以参考属性的编码格式表来分析源码中各个标识的意思,编码格式表如下所示:
3.2 OC对象方法类型编码解析
首先来我们在类中自定义几个方法,来看看方法的编码格式是如何的,代码如下所示:
@interface Person : NSObject
- (void)setNewName:(NSString *)name andWeight:(CGFloat)w;
- (void)nothing;
- (void)setAge:(int)age;
- (int)sumA:(int)a andB:(int )b;
@end
@implementation Person
- (void)setNewName:(NSString *)name andWeight:(CGFloat)w {
}
- (void)nothing {
}
- (void)setAge:(int)age {
}
- (int)sumA:(int)a andB:(int )b {
return a+b;
}
@end
通过clang
命令,将main.m
文件编译为c++
文件,查看其中编译好的方法代码,如下所示:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[4];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
4,
{{(struct objc_selector *)"setNewName:andWeight:", "v32@0:8@16d24", (void *)_I_Person_setNewName_andWeight_},
{(struct objc_selector *)"nothing", "v16@0:8", (void *)_I_Person_nothing},
{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_Person_setAge_},
{(struct objc_selector *)"sumA:andB:", "i24@0:8i16i20", (void *)_I_Person_sumA_andB_}}
};
首先我们来分析一下setNewName:andWeight:
这个方法的类型编码
v32@0:8@16d24
我们将这个类型编码使用空格分隔开,就变成了:v 32 @ 0 : 8 @ 16 d 24,其中:
v : 代表函数返回值类型为void
32 : 代表所有参数所占总字节数(8 + 8 + 8 + 8)
@ : 代表参数1(id self,指针类型,占8字节)
0 : 代表参数1的起始地址
: : 代表参数2(sel _cmd,选择器,占8字节)
8 : 代表参数2的起始地址
@ : 代表参数3(NSString * name,占8字节)
16 : 代表参数3的起始地址
d : 代表参数4(CGFloat w)
24 : 代表参数4的起始地址
然后再来分析一下sumA:andB:
这个方法的类型编码
i24@0:8i16i20
我们将这个类型编码使用空格分隔开,就变成了:i 24 @ 0 : 8 i 16 i 20,其中:
i : 代表函数返回值类型为int
24 : 代表所有参数所占总字节数(8 + 8 + 4 + 4)
@ : 代表参数1(id self,指针类型,占8字节)
0 : 代表参数1的起始地址
: : 代表参数2(sel _cmd,选择器,占8字节)
8 : 代表参数2的起始地址
i : 代表参数3(int a,占4字节)
16 : 代表参数3的起始地址
i : 代表参数4(int b,占4字节)
20 : 代表参数4的起始地址
下表是OC
中各个数据类型的编码格式,你可以参照这个表来对方法类型进行解码。
4. objc_setProperty与objc_getProperty函数调用原理
4.1 objc_setProperty函数调用原理
首先,我们编写一个Person
类,代码如下:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *hobby;
@end
@implementation Person
@end
然后使用clang
命令将main.m
文件编译成C++
文件,查看这两个属性的getter
以及setter
方法,源码如下:
static NSString * _I_Person_name(Person * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_name));
}
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1);
}
static NSString * _I_Person_hobby(Person * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_hobby));
}
static void _I_Person_setHobby_(Person * self, SEL _cmd, NSString *hobby) {
(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_hobby)) = hobby;
}
我们发现,虽然这两个属性name
以及hobby
都是NSString
类型的,但是编译后的源码,name
属性的setter
方法中使用的是objc_setProperty
函数赋值,而hobby
属性的setter
方法中使用的确实内存平移赋值,这是为什么呢?
首先,我们先来探究一下为什么会需要objc_setProperty
这个函数,因为在你编写类的代码时,底层并不知道你会定义什么样的属性以及方法,所以,但是属性的setter
、getter
方法又要调用底层的接口,这就形成了一个矛盾,那该如何解决这个问题呢?由于所有属性的setter
方法只有第二个参数_cmd
是不一样的,因此,就出现了objc_setProperty
函数,用来作为中间层,替换了属性setter
方法的imp
实现,这样就保证了底层代码不会因为属性的增多而增加多余的代码,如下图所示:
接着,我们还需要探究的是objc_setProperty()
调用流程,但是现在就出现了一个问题,就是我们是去那里找objc_setProperty()
这个函数的调用流程呢,如果你去objc
的源码中查找,就会发现objc
的源码中有objc_setProperty()
函数的声明以及实现,但问题是,这里是我们自定义的类中的属性的setter
方法里面调用的objc_setProperty()
,这肯定不是objc
源码调用的,而是llvm
在编译的时候调用的,但是为什么要在编译时刻如此修改呢?为什么不在运行时修改呢?因为要知道,在一个项目工程中,我们自定义的类是很多的,类中的属性也是很多的,如果我们全部交给运行时处理,效率就会很低,因此放在编译时修改属性的setter
方法的imp
是最明智的选择。
&emsp所以,现在使用VS Code
打开llvm
源码,搜索objc_setProperty
,我们找到了如下的代码:
其实是调用了getSetPropertyFn()
这个函数,根据这个函数的名字,可以判断这个函数是用来获取setPropert
y这个函数的,因此我们需要知道在哪里调用了getSetPropertyFn()
这个函数,然后我们全局搜索getSetPropertyFn()
,发现了以下的代码:
其实是GetPropertySetFunction
函数调用了GetSetPropertyFn
这个函数,接着我们全局搜索GetPropertySetFunction()
,看看GetPropertySetFunction
在哪里调用了,然后发现了如下代码:
调用GetPropertySetFunction
函数的实际上是generateObjCSetterBody
函数,在这个函数中对strategy.getKind()
函数调用后的枚举值进行比较,当枚举值为GetSetProperty
或SetPropertyAndExpressionGet
并且UseOptimizedSetter(CGM)
这个函数调用结果不为真的时候将会调用GetPropertySetFunction
函数获取到setProperty
这个函数,并在下面的函数中进行调用,因此我们需要知道strategy
的getKind
函数是如何实现的,其代码如下:
getKind()
函数实际上是获取了类PropertyImplStrategy
中的成员变量Kind
的值,因此我们再来看看成员变量Kind
是在何时进行赋值的,最后我们在PropertyImplStrategy
的构造方法中找到了其成员变量Kind
的赋值,代码如下所示:
分析上面代码第二个红框以及第三个红框,我们就可以明白,当设置属性为Copy
时,就会给Kind
赋值为GetSetProperty
,这就是objc_setProperty()
函数底层的调用流程。
尽管底层是如此实现的,但是我们还是想做一个验证,编写代码如下:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (atomic, copy) NSString *name1;
@property (nonatomic) NSString *name2;
@property (atomic) NSString *name3;
@end
@implementation Person
@end
然后使用clang
命令将这个文件编译为c++
文件,查看属性的getter
、setter
方法,如下所示:
static NSString * _I_Person_name(Person * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_name));
}
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1);
}
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
static NSString * _I_Person_name1(Person * self, SEL _cmd) {
typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct Person, _name1), 1);
}
static void _I_Person_setName1_(Person * self, SEL _cmd, NSString *name1) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name1), (id)name1, 1, 1);
}
static NSString * _I_Person_name2(Person * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_name2));
}
static void _I_Person_setName2_(Person * self, SEL _cmd, NSString *name2) {
(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_name2)) = name2;
}
static NSString * _I_Person_name3(Person * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_name3));
}
static void _I_Person_setName3_(Person * self, SEL _cmd, NSString *name3) {
(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_name3)) = name3;
}
可以发现,只有使用copy
修饰的属性才会在其setter
方法中调用objc_setProperty
函数。
4.2 objc_getProperty函数调用原理
首先,我们编写一个Person
类,代码如下:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (atomic, copy) NSString *name1;
@property (nonatomic, assign) int age;
@property (atomic, assign) int height;
@property (nonatomic, strong) NSArray* array1;
@property (atomic, strong) NSArray* array2;
@property (nonatomic, weak) id delegate;
@property (atomic, weak) id delegate2;
@property (nonatomic, assign, readonly) CGFloat f1;
@end
@implementation Person
@end
然后使用clang
命令将main.m
文件编译成C++
文件,查看这几个属性的getter
以及setter
方法,发现只有属性name1中的getter方法调用了objc_getProperty函数,部分源码如下:
static NSString * _I_Person_name(Person * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_Person$_name));
}
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1);
}
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
static NSString * _I_Person_name1(Person * self, SEL _cmd) {
typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct Person, _name1), 1);
}
static void _I_Person_setName1_(Person * self, SEL _cmd, NSString *name1) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name1), (id)name1, 1, 1);
}
objc_getProperty()函数的底层原理与objc_setProperty()是差不多的,如下图所示:
然后我们再查看一下LLVM中objc_getProperty调用流程,首先在LLVM源码中搜索objc_getProperty关键字,搜索到的内容如下图所示:
然后搜索getGetPropertyFn()这个方法在哪里调用的,如下图所示:
再搜索GetPropertyGetFunction()函数在哪里调用的,如下图所示:
可以发现是在函数generateObjCGetterBody中也是根据strategy.getKind()这个函数的返回值进行判断,如果是GetSetProperty这个枚举值就会调用GetPropertyGetFunction()函数,而strategy.getKind()与上面的objc_setProperty函数的调用逻辑如出一辙,如下图所示:
但是看源码看到这里我们就有了一个疑惑,因为name属性与name1属性同样都是使用copy修饰的,所以Kind类型都是GetSetProperty,但是为什么name属性的getter方法是通过内存平移获取的值,而name1属性却是通过objc_getProperty()函数赋值的呢,也就是说,name这个属性的getter在llvm进行代码编译的过程中并没有调用generateObjCGetterBody,而在name1属性的getter方法编译时调用了generateObjCGetterBody函数,因此我们还需要看看哪里调用了generateObjCGetterBody函数,如下所示:
然后再来查看哪里调用了GenerateObjCGetter函数,如下图所示:
如上图所示,根据红框4以及红框5中的逻辑,如果能获取到getter合成器的声明,就会调用GenerateObjCGetter函数。
5. 使用Objc底层API接口获取类信息
先编写两个类Person以及Student,代码如下:
//Person.h文件中的代码
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)run;
- (void)saySomething:(NSString *)words;
+ (void)sleeping;
@end
//Person.m文件中的代码
#import "Person.h"
@implementation Person
- (void)run {
NSLog(@"跑了!");
}
- (void)saySomething:(NSString *)words {
NSLog(@"说了:%@", words);
}
+ (void)sleeping {
NSLog(@"睡觉...");
}
@end
//Student.h文件中的代码
@interface Student : Person
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, copy) NSString *hobby;
- (void)learning;
- (void)drinking:(NSString *)beveragesName;
+ (void)working;
@end
//Student.m文件中的代码
@implementation Student
- (void)learning{
NSLog(@"学习中...");
}
- (void)drinking:(NSString *)beveragesName {
NSLog(@"喝了: %@", beveragesName);
}
+ (void)working {
NSLog(@"工作中...");
}
@end
5.1 获取实例对象的方法列表
代码如下:
//遍历并打印对象方法名
void objcCopyMethodList(Class pClass) {
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (int i = 0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"methodName[%d] = %@", i, methodName);
}
free(methods);
}
打印Person类中实例方法列表中所有方法名
//在main函数中调用
int main(int argc, const char * argv[]) {
objcCopyMethodList([Person class]);
return 0;
}
打印结果如下:
2021-06-27 23:02:14.650596+0800 Objc底层API获取类信息[44300:4706329] methodName[0] = saySomething:
2021-06-27 23:02:14.650998+0800 Objc底层API获取类信息[44300:4706329] methodName[1] = run
2021-06-27 23:02:14.651049+0800 Objc底层API获取类信息[44300:4706329] methodName[2] = name
2021-06-27 23:02:14.651153+0800 Objc底层API获取类信息[44300:4706329] methodName[3] = .cxx_destruct
2021-06-27 23:02:14.651212+0800 Objc底层API获取类信息[44300:4706329] methodName[4] = setName:
2021-06-27 23:02:14.651274+0800 Objc底层API获取类信息[44300:4706329] methodName[5] = age
2021-06-27 23:02:14.651305+0800 Objc底层API获取类信息[44300:4706329] methodName[6] = setAge:
//打印Person类中类方法列表中所有方法名
int main(int argc, const char * argv[]) {
Class metaCls = objc_getMetaClass(class_getName([Person class]));
objcCopyMethodList(metaCls);
return 0;
}
打印结果如下:
2021-06-27 23:25:02.110763+0800 Objc底层API获取类信息[44488:4720876] methodName[0] = sleeping
5.2 获取类中某个实例变量方法或类方法
代码如下:
void instanceMethod_classToMetaclass(Class pClass) {
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(run));
Method method2 = class_getInstanceMethod(pClass, @selector(saySomething:));
Method clsMethod1 = class_getInstanceMethod(pClass, @selector(sleeping));
Method clsMethod2 = class_getInstanceMethod(metaClass, @selector(sleeping));
NSLog(@"%p - %p - %p - %p", method1, method2, clsMethod1, clsMethod2);
}
//在main函数中调用
int main(int argc, const char * argv[]) {
instanceMethod_classToMetaclass([Person class]);
return 0;
}
打印结果如下:
2021-06-27 23:31:32.886104+0800 Objc底层API获取类信息[44521:4726487] 0x1000080f0 - 0x1000080d8 - 0x0 - 0x100008070
5.3 获取类或元类的类方法
代码如下:
void classMethod_classToMetaclass(Class pClass) {
const char *clsName = class_getName(pClass);
Class metaClass = objc_getMetaClass(clsName);
Method method1 = class_getClassMethod(pClass, @selector(saySomething:));
Method method2 = class_getClassMethod(metaClass, @selector(saySomething:));
Method method3 = class_getClassMethod(pClass, @selector(sleeping));
Method method4 = class_getClassMethod(metaClass, @selector(sleeping));
NSLog(@"%p - %p - %p - %p\n", method1, method2, method3, method4);
}
//main函数中代码
int main(int argc, const char * argv[]) {
classMethod_classToMetaclass([Person class]);
return 0;
}
打印结果如下:
2021-06-28 10:03:50.078046+0800 Objc底层API获取类信息[45007:4809476] 0x0 - 0x0 - 0x100008078 - 0x100008078
5.4 获取类或元类的类方法
代码如下:
void impClassToMetaclass(Class pClass) {
const char* className = class_getName(pClass);
Class metaCls = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(pClass, @selector(run));
IMP imp2 = class_getMethodImplementation(metaCls, @selector(run));
IMP imp3 = class_getMethodImplementation(pClass, @selector(sleeping));
IMP imp4 = class_getMethodImplementation(metaCls, @selector(sleeping));
NSLog(@"%p - %p - %p - %p\n", imp1, imp2, imp3, imp4);
}
int main(int argc, const char * argv[]) {
impClassToMetaclass([Person class]);
return 0;
}
打印结果:
2021-06-28 11:08:49.830176+0800 Objc底层API获取类信息[45374:4850367] 0x100003560 - 0x7fff2029e5c0 - 0x7fff2029e5c0 - 0x1000035f0
打印结果分析:
分析以上结果,我们可以知道,由于Person
类中有run
这个实例方法,所以imp1
是有值的,但是Person
元类中并没有run
这个实例方法,那为什么不是输出nil
呢?而且Person
类中没有sleeping
这个类方法,sleeping
类方法是在Person
元类中,因此imp4
的值有值,但为什么imp3
也是与imp2
一样呢?带着这两个疑问我们,在LLDB
中进行调试,结果如下所示:
(lldb) p/x imp1
(IMP) $0 = 0x0000000100003560 (Objc底层API获取类信息`-[Person run] at Person.m:12)
(lldb) p/x imp2
(IMP) $1 = 0x00007fff2029e5c0 (libobjc.A.dylib`_objc_msgForward)
(lldb) p/x imp3
(IMP) $2 = 0x00007fff2029e5c0 (libobjc.A.dylib`_objc_msgForward)
(lldb) p/x imp4
(IMP) $3 = 0x00000001000035f0 (Objc底层API获取类信息`+[Person sleeping] at Person.m:20)
原来Person
元类中找不到run
方法、Person
类中找不到sleeping
类方法,就会调用消息转发的方法(_objc_msgForward
)。
5.5 获取类以及元类的方式
代码如下:
void printClsInfos() {
Person *p = [Person alloc];
const char* clsName = class_getName([p class]); //获取类名,C字符串
Class cls1 = objc_getClass(clsName); //Person类
Class cls2 = [Person class]; //Person类
Class cls3 = class_getSuperclass(cls2); //NSObject类
Class cls4 = object_getClass(p); //Person类
Class cls5 = object_getClass(cls2); //Person元类
Class metaCls = objc_getMetaClass(clsName); //Person元类
NSLog(@"cls1: %p - cls2: %p - cls3: %p - cls4: %p - cls5: %p - metaCls: %p \n", cls1, cls2, cls3, cls4, cls5, metaCls);
}
int main(int argc, const char * argv[]) {
printClsInfos();
return 0;
}
调试结果如下:
2021-06-28 11:14:44.163307+0800 Objc底层API获取类信息[45414:4854876] cls1: 0x1000084a0 - cls2: 0x1000084a0 - cls3: 0x7fff806ce008 - cls4: 0x1000084a0 - cls5: 0x100008478 - metaCls: 0x100008478