前言
上一篇 iOS 底层原理:类的加载原理中,懒加载类与非懒加载类
的探索,涉及了rw、ro
的操作、方法列表排序
的操作等,今天将紧接上文进行分类加载的探索。
准备工作
一、分类的本质探索
clang 探索分类的本质
先在main.m
中添加SSLPerson
的分类:
@interface SSLPerson (SSL)
@property (nonatomic, copy) NSString *ssl_name;
@property (nonatomic, assign) int *ssl_age;
- (void)ssl_instanceMethod1;
- (void)ssl_instanceMethod2;
+ (void)ssl_classMethod3;
@end
@implementation SSLPerson (SSL)
- (void)ssl_instanceMethod1
{
NSLog(@"%s",__func__);
}
- (void)ssl_instanceMethod2
{
NSLog(@"%s",__func__);
}
+ (void)ssl_classMethod3
{
NSLog(@"%s",__func__);
}
@end
clang -rewrite-objc main.m -o main.cpp
得到main.cpp
文件:
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
static struct _category_t _OBJC_$_CATEGORY_SSLPerson_$_SSL __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"SSLPerson",
0, // &OBJC_CLASS_$_SSLPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_SSLPerson_$_SSL,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_SSLPerson_$_SSL,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_SSLPerson_$_SSL,
};
static void _I_SSLPerson_SSL_ssl_instanceMethod1(SSLPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_1d79f7_mi_0,__func__);
}
static void _I_SSLPerson_SSL_ssl_instanceMethod2(SSLPerson * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_1d79f7_mi_1,__func__);
}
static void _C_SSLPerson_SSL_ssl_classMethod3(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_1d79f7_mi_2,__func__);
}
- 分类被译成
C++
以后是_category_t
结构体,通过_OBJC_$_CATEGORY_SSLPerson_$_SSL
构造函数进行赋值。 - 分类没有
元类
的概念,所以分类中有两个方法列表:instance_methods
存储实例方法,class_methods
存储类方法。 - 我们可以找到
ssl_instanceMethod1
、ssl_instanceMethod2
、ssl_instanceMethod3
方法的相关代码,但是没有ssl_name
、ssl_age
相关方法的代码,说明属性没有自动生成getter
和setter
方法。 name
是分类名,cls
是类。
源码 探索分类的本质
打开源码,看一下源码中分类的定义:
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
- 源码中的结构跟上面编译出来的
C++
源码基本相似,实例方法
,类方法
,协议
,和属性
都可以找到对应的存储方式。 - 我们并没有找到
成员变量
的定义,因此分类中是不能添加成员变量的。
分类的本质 总结
分类的本质是一个category_t
的结构体。
- 有两个
method_list_t
类型的方法列表:instanceMethods
:实例方法列表。classMethods
:类方法列表。
- 有
property_list_t
类型的属性列表,表示分类中定义的属性,但是不会自动生成Getter
和Setter
方法。 - 没有
成员变量
的定义,因此分类中是不能添加成员变量。 - 一个
protocol_list_t
类型的协议列表,表示分类中实现的协议。 - 有
name
(分类的名称)和cls
(类对象)。
二、分类的加载引入 反推思路
rwe 反推
再次回到methodizeClass
函数:
static void methodizeClass(Class cls, Class previously)
{
runtimeLock.assertLocked();
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro();
auto rwe = rw->ext();
// Methodizing for the first time
if (PrintConnecting) {
_objc_inform("CLASS: methodizing class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// Install methods and properties that the class implements itself.
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
if (rwe) rwe->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (rwe && proplist) {
rwe->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (rwe && protolist) {
rwe->protocols.attachLists(&protolist, 1);
}
}
- 我们发现
方法列表
、属性列表
和协议列表
的加载都用到了rwe
的条件判断,rwe
通过rw->ext()
来赋值。
点击查看rwe
:
extAllocIfNeeded()
是rwe
的初始化函数,如果没有值就去创建,如果有值时直接获取返回。
attachCategories 反推
搜素extAllocIfNeeded()
的调用:
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
...
auto rwe = cls->data()->extAllocIfNeeded();
...
}
搜索attachCategories
的调用:
- 如图,
load_categories_nolock
函数和attachToClass
函数都调用了attachCategories
。 - 我们接下来,将以这两个函数反向推导,推导到我们熟悉的代码,来分析
分类
的加载。
attachToClass 反推
我们先用attachToClass
进行反推,全局搜索attachToClass
,发现只有在methodizeClass
函数中被调用了:
static void methodizeClass(Class cls, Class previously)
{
...
// Attach categories.
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
// When a class relocates, categories with class methods
// may be registered on the class itself rather than on
// the metaclass. Tell attachToClass to look for those.
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
}
previously
是个备用参数,值一直都是nil
,通过realizeClassWithoutSwift(cls, nil)
传值过来的。- 所以,真正调用
attachToClass
的地方只有objc::unattachedCategories.attachToClass(cls, cls,sMeta ? ATTACH_METACLASS : ATTACH_CLASS)
这一个地方。 - 可以得出
attachToClass
这条线的调用流程是:_read_images
->realizeClassWithoutSwift
->methodizeClass
->attachToClass
->attachCategories
,前半部分是我们熟悉的懒加载流程。
load_categories_nolock 反推
load_categories_nolock
这边我们就不去搜索函数的调用了,下面会通过断点调试的方式来推导,现在的流程是:load_categories_nolock
-> attachCategories
。
三、分类和主类加载的4种情况
上一篇 iOS 底层原理:类的加载原理中 我们讲到过懒加载和非懒加载,类加载时调用函数,跟是否调用load
是有关系的。
那么接下来,针对分类和主类有没有实现load
方法进行函数调用的测试,看一下不同情况是怎么调用到attachCategories
的。
准备
工程中添加下面代码:
_read_images
、realizeClassWithoutSwift
、methodizeClass
、attachToClass
、load_categories_nolock
、attachCategories
中分别添加打印代码:
- 大家可以对这些位置自行加断点,进行相关调试。
main.m
中添加断点:
准备工作完毕,下面开始调试。
分类load + 主类load
分类和主类同时实现load
,运行项目:
-
根据断点调试、打印结果和堆栈信息,这种情况下函数的调用顺序为:
_read_images
->realizeClassWithoutSwift
->methodizeClass
->attachToClass
->_read_images
函数走完。load_images
->loadAllCategories
->load_categories_nolock
->attachCategories
。
-
用图来表示这种情况的函数调用:
分类load + 主类没有
-
这种情况下,
attachCategories
函数没有被调用,只是_read_images
相关函数的调用。 -
用图来表示这种情况的函数调用:
分类没有 + 主类load
-
这种情况和上面的结果一样,
attachCategories
函数也没有被调用。 -
用图来表示这种情况的函数调用:
分类没有 + 主类没有
- 这种情况下,前面的这些函数都没有调用。
四、分类加载流程跟踪
分类和主类同时实现load
时,上面分析出了attachCategories
的函数是如何调用到的,接下来进行断点调试。
load_categories_nolock 跟踪
先断点进入load_categories_nolock
:
lc
是locstamped_category_t
类型的,hi
是符号表的信息,cat
是分类。
打印一下cat
:
- 可以看到,分类名是
SSL1
,instanceMethod
和classMethods
也是有值的。
单步向下走,走到了attachCategories
的函数调用处:
ATTACH_EXISTING
的值是8
,接下来进入attachCategories
函数。
attachCategories 跟踪
先看下attachCategories
函数:
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
...
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
断点进入attachCategories
,然后走到下面的代码:
mlist
是分类的方法结构体,count = 2
代表方法的数量,分别是say1
和say2
方法。
继续向下走:
ATTACH_BUFSIZ
的值是64
,mcount
初始值为0
,mlists
共有64
个位置。- 这里将
mlist
赋值到mlists
中64 - 1
的位置,并对mcount
进行+1
操作。
继续向下走:
- 通过
prepareMethodLists
对mlists
中的方法进行排序,这里mcount
是1
,取的是mlists
的第64 - 1
的位置的值。 - 接下来进入
attachLists
函数,传入mlists + ATTACH_BUFSIZ - mcount
是method_list_t **
类型,传入mcount
是1
。
attachLists 跟踪
先看attachLists
函数:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
断点进入attachLists
:
- 这个
list
的count
是3
,存的是我们主类的3
个方法。
断点向下走:
oldCount
是1
,newCount
是2
,addedLists[i]
是method_list_t *
类型,所以oldList
也是method_list_t *
类型。- 将
oldList
也就是主类的method_list_t *
,存储在array()->lists
的第1
个位置。 - 将
addedLists[i]
也就是分类的method_list_t *
,存储在array()->lists
的第0
个位置,可以看到分类
被添加到了主类
的前面。
五、多个分类加载
上面分析了加载一个分类
的情况,接下来探索多个分类
的加载。
准备
再添加两个分类:
load_categories_nolock 跟踪
断点进入load_categories_nolock
:
当i = 1
时停住断点,进行相关打印:
- 此时的
count = 3
,i = 1
,分类是SSL3
,接下来断点进入attachCategories
。
attachCategories 跟踪
- 传入的
mlists + ATTACH_BUFSIZ - mcount
依然是method_list_t **
类型,也就是method_list_t
指针地址类型。
attachLists 跟踪
断点进入attachLists
:
oldCount = 2
,根据之前的分析我们可以知道,它们是主类的method_list_t *
和第一个分类的method_list_t *
。newCount = 3
,用这个大小开辟了一个新的newArray
。- 第一个
for
循环,取array()->lists
的值也就是old
的值,从后向前存储到newArray
的第2
和第1
个位置。 - 第二个
for
循环,将addedLists
的值从前向后,存储到newArray
中。这里只有一个值,存储到第0
位置。
六、五种情况 Categories 数据的加载
我们现在只知道分类
和主类
同时实现load
时,Categories
的加载情况,下面分析其他情况下Categories
是如何加载的。
分类load + 主类没有
为方便测试只保留SSLPerson
和SSLPerson+SSL1
,运行程序,断点进入realizeClassWithoutSwift
:
lldb
打印:
- 可以看到,此时的方法数量
count
已经是5
了,是SSLPerson
中的3
个,和SSLPerson+SSL1
中的2
个。方法已经存在了,说明是通过data()
加载的,已经被编译器处理了。
分类没有 + 主类load
经过测试,这种情况和上面的一样,在realizeClassWithoutSwift
中时,方法数量count
也已经是5
了,是SSLPerson
中的3
个,和SSLPerson+SSL1
中的2
个。方法已经存在了,说明是通过data()
加载的,已经被编译器处理了。
分类没有 + 主类没有
分类和主类都没有实现load
时,运行程序:
- 之前的函数都没有调用,会推迟到第一次消息的发送,接下来消息的第一次发送,断点继续调试。
断点进入到realizeClassWithoutSwift
:
- 可以看到
count
也变成5
了,也是通过data()
加载的
多个分类不全有 + 主类有
将SSLPerson+SSL2
、SSLPerson+SSL3
重新添加回来,让SSLPerson+SSL3
不去实现load
,运行程序。
断点进入到了load_categories_nolock
函数:
-
进入到了
load_categories_nolock
函数,count = 3
,非懒加载的相关函数也被调用了。说明这种情况和分类主类都实现load
时是一样的,最后都是通过attachCategories
来实现分类的加载的。 -
函数调用也是一样的:
七、methodList -> 数据结构
attachCategories
-> methods.attachLists
这种加载分类的方式,attachLists
函数中是方法列表加载的核心代码。
methodList
中存储的是method_list_t
的数组指针,接下来分析一下method_list_t
的数据结构。
method_list_t 结构分析
断点进入attachCategories
,走到methods.attachLists
调用前的位置:
- 根据打印结果,每个分类中取出的方法列表是在
method_list_t
中取得,通过get
方法获取,取出来的是method_t
类型。
看一下method_list_t
和method_t
的数据结构:
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
...
}
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
// 计算方法大小
uint32_t entsize() const {
return entsizeAndFlags & ~FlagMask;
}
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
// 平移操作
return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
}
Element& `get`方法(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
}
struct method_t {
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
...
}
- 可以看到
method_list_t
继承于entsize_list_tt
,获取方法的代码是在entsize_list_tt
中实现的。 get
方法的调用,内部是调用了getOrEnd
方法简化一下:this + sizeof(*this) + i*entsize()
方法。getOrEnd
方法简化一下:this + sizeof(*this) + i*entsize()
- 先从结构体头平移到结构体尾。
- 通过
entsize()
计算出方法的大小。 - 再通过平移
i
个方法的大小,就获取到相应的方法了。
通过lldb
打印验证:
method_list_t
的地址是0x0000000100008030
,method_list_t
的大小是8
。- 第
0
个method_t
的地址是0x0000000100008038
,和method_list_t
的地址正好相差8
,证明了我们上面的说法。
methodList 图例
八、分类加载 是否需要排序
通过methods.attachLists
进行加载的类,在调用时是否进行排序了呢,在 OC 原理探索:方法的慢速查找流程 中我们分析过二分查找
,今天通过它来进行探索。
getMethodNoSuper_nolock 源码分析
创建下面这些类:
- 主类有
name
、setName
、say1
三个方法,分类都实现了say1
和say2
方法。
在main.m
中调用say1
方法:
int main(int argc, const char * argv[]) {
@autoreleasepool {
SSLPerson *person = [SSLPerson alloc];
[person say1];
}
return 0;
}
运行程序,断点进入二分查找函数getMethodNoSuper_nolock
:
- 可以看到
count = 2
,是分类
方法的数量,说明方法列表的结构跟上面methods.attachLists
结束时一样的。 - 所以方法列表并没有进行排序,如果方法列表进行排序所有的方法应该都整合在一个数组里,
count
应该是3+2+2+2 = 9
。
但是这里有一个矛盾的点,什么矛盾的点呢,下面继续看。
findMethodInSortedMethodList 源码分析
通过getMethodNoSuper_nolock
-> search_method_list_inline
进入 findMethodInSortedMethodList
函数。
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
- 看源码,当
keyValue == probeValue
时,会有一个while
循环继续向前寻找的操作,这明明又是说明对方法列表
进行了组合排序
,这是为什么呢。 - 因为
分类
的加载除了attachCategories
的方式,还有一种data()
加载的方式,data()
加载的方法列表只有一个
列表,并且是排好序
的,那么data()
中的ro
是怎么加载的呢。
九、class_ro_t 数据加载
ro
的数据加载,是编译阶段
就完成了,所以我们用llvm
源码来进行分析。
class_ro_t 在 llvm 中的数据结构
打开llvm
源码,全局搜索class_ro_t
:
- 可以看到
llvm
中的class_ro_t
,和objc
中的class_ro_t
成员变量是一样的,只是llvm
中多了m_
的前缀。 - 我们注意到有个重要函数
Read
,这个函数中应该就是从MachO
中读取了数据,接下来继续分析。
Read 函数
全局搜索class_ro_t::Read
,得到结果:
- 源码还是比较清晰的,先计算
class
大小得到size
。process
通过size
、addr
读取内存数据。 extractor
将所有数据进行包装
、计算
,把计算好的结果赋值给m_flags
、m_instanceStart
等成员变量。
我们对Read
函数的实现基本了解了,那么它又是在哪儿里被调用的呢,全局搜索ro->Read(
:
OK!!
,成功找到了Read
函数的调用。
有问题可以评论区多多交流,点个赞支持一下吧!!😄😄😄