前言
上一篇 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函数的调用。
有问题可以评论区多多交流,点个赞支持一下吧!!😄😄😄