前言
我们在开发中,一定或多或少的使用到了分类,对于分类的使用,对于iOS开发者来说也是必不可少的技能。但是添加的分类如何将方法灌入原类中,他和runtime 有什么关系,分类先后顺序又是如何呢?可能被问及可能有点懵,接下来我们就一起走进 category的世界。
extension
在说道category 时,我们总是不得不提 extension 。他们有什么区别这个话题经久不衰,其实理解起来很简单,我们这边做一个简单的介绍。
extension 是类的一部分,在编译期和头文件里的 @interface 以及实现文件里的 @implement 一起形成一个完整的类,它伴随类的产生而产生亦随之一起消亡。extension 一般用来隐藏类的私有信息,必须有一个类的源码才能为一个类添加 extension,所以无法为系统的类extension。
所以category 与 extension的主要区别其实就是 extension相当于把变量属性的访问权限改为私有了,编译后就己经合并到底层的C++代码中了,category是在运行时才合并到类的方法列表中。
category
为了方便理解 我们这里首先创建 Animal 的文件,同时创建他的一个分类Eat 如下图所示
终端利用clang输出cpp
clang -rewrite-objc Animal+Eat.m -o cate.cpp
通过 Clang 编译之后的文件,看看编译时做了什么内容:
定义
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;// 属性
};
// 存放在 _DATA 数据段中的 __objc__const 字段中
static struct _category_t _OBJC_$_CATEGORY_Animal_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
// 类名
"Animal",
0, // &OBJC_CLASS_$_Animal, 这个刚开始没值,实现后就是前面这东西
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Eat, // 实例方法
0,
0,
0,
};
static void OBJC_CATEGORY_SETUP_$_Animal_$_Eat(void ) {
_OBJC_$_CATEGORY_Animal_$_Eat.cls = &OBJC_CLASS_$_Animal;
}
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Animal_$_Eat,
};
// // 最后,这个类的 category 生成了一个数组,存在了 __DATA 字段下的 __objc_catlistsection 里
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
我们在上面分别看到了 _category_t 这个就是分类对应的结构体,在他的底下我们看到了 对这个结构体的 “实例化” ,为了方便理解我们做一个简单介绍
__attribute__ ((used, section ("__DATA,__objc_const")))该语法是 GCC 编译器特性(attributes),用于提供某些编译指令,并且它们在 LLVM/Clang 编译器中也支持。这个特性部分指定了类别对象的存储位置和使用策略。
__attribute__ ((used))
used指示编译器即使没有直接引用该变量或函数,也不要移除它。这在优化过程中可以防止未引用的代码被剔除。
__attribute__ ((section ("__DATA,__objc_const")))
section ("__DATA,__objc_const")指示编译器将变量放置到指定的段(section)中。这里,类别对象被存储在__DATA段中的__objc_const部分。 在 macOS 或 iOS 系统中,内存分段通常用于区分不同类型的数据和代码,帮助链接器和加载器更有效地管理这些数据。
我们从上面看到了我们的分类的实例方法 我们再看看这个方法干了什么事情,- 搜索一下 _OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Eat:
static struct /*_method_list_t*/ {
// 方法的定义
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
// Selector
// "v16@0:8":方法签名
// 函数指针地址
{{(struct objc_selector *)"eatFood", "v16@0:8", (void *)_I_Animal_Eat_eatFood}}
};
继续搜索 _I_Animal_Eat_eatFood
// @implementation Animal (Eat)
static void _I_Animal_Eat_eatFood(Animal * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_7r_d8xxgvyj49q6l2m7nng6gsgh0000gn_T_Animal_Eat_f7acbb_mi_0);
}
通过前面的分析我们发现:分类是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中,
接下来我们再回到runtime源码查看 category_t 存储的方法,属性,协议等是如何存储在类对象中的。
我们在objc-rumtime-new.mm中找到的category_t的具体实现:
Category的加载
通过 Clang 知道了系统在编译时期,将分类内容存储在地方,具体做了什么操作。那他到底是如何被加入到类中的呢。程序的入口点在 iOS 中被称之为 main 函数:所写的所有代码,它的执行的第一步,均是由 main 函数开始的。但其实,在程序进入 main 函数之前,内核已经为程序加载和运行做了许多的事情。
在上一篇我们的 ### iOS源码objc 编译以及调试派上用场了,我们在 objc-os.m 文件的 _objc_init打上断点。
从断点的方法列表 我们可以发现 _objc_init 在这里我们进入我们的代码。
我们先进行浅浅的分析一下里面的方法。
void _objc_init(void)
{
//确保只运行一次
static bool initialized = false;
if (initialized) return;
initialized = true;
// 初始化避免并发访问未定义行为的掩码。
// fixme defer initialization until an objc-using image is found?
masks_init();
// 初始化用于同步的锁。
locks_init();
// 读取影响运行时的环境变量,如果需要,还可以打开环境变量帮助 export OBJC_HELP = 1
environ_init();
// 关于线程key的绑定,例如线程数据的析构函数
runtime_tls_init();
// 初始化同步机制。
_objc_sync_init();
//初始化访问器相关的数据结构。
accessors_init();
//初始化侧表(用于存储弱引用表、对象关联等)。
side_tables_init();
// 运行C++静态构造函数,在dyld调用我们的静态析构函数之前,libc会调用_objc_init(),因此必须自己做
static_init();
// runtime运行时环境初始化,里面主要是unattachedCategories、allocatedClasses,分类初始化
runtime_init();
// 初始化libobjc的异常处理系统
exception_init();
// 缓存条件初始化
cache_t::init();
#if !TARGET_OS_EXCLAVEKIT
// // 如果不是在 exclavekit 环境中,初始化 block 实现。
_imp_implementationWithBlock_init();
#endif
// 通过这种方式,确保回调数据结构在初始化结束后不会在内存中留有残留。有可能的问题是未正确清除残留会导致安全问题或使用伪造的回调指针。
1.
// _dyld_objc_register_callbacks -- dyld 注册的地方
// - 仅供objc运行时使用
// - 注册处理程序,以便在映射、取消映射 和初始化objc镜像文件时使用,dyld将使用包含objc_image_info的镜像文件数组,回调 mapped 函数
// map_images:dyld将image镜像文件加载进内存时,会触发该函数
// load_images:dyld初始化image会触发该函数
// unmap_image:dyld将image移除时会触发该函数
volatile _dyld_objc_callbacks_v3 callbacks = {
3, // version
};
callbacks.mapped = &map_images;
callbacks.init = &load_images;
callbacks.unmapped = unmap_image;
callbacks.patches = _objc_patch_root_of_class;
_dyld_objc_register_callbacks((_dyld_objc_callbacks*)&callbacks);
memset_s((void *)&callbacks, sizeof(callbacks), 0, sizeof(callbacks));
// // 标记 dyld 通知注册已调用。
didCallDyldNotifyRegister = true;
}
- 上图为
_objc_init解释,我们可以看出这个方法 就是Bootstrap初始化。将我们的资源通知器注册到dyld。保证在初始化之前由libSystem调用。 - 而
dyld是 Apple 的动态链接编辑器,它是 macOS 和 iOS 系统中动态库装载和链接的核心组件。 - image(Mach-O 的二进制文件)
从上图我们又能看到对应方法所在地。
_dyld_objc_notify_mapped3对应map_image回调:当dyld已将images加入内存时;_dyld_objc_notify_init2对应load_image回调:当dyld初始化image时,OC 调用类的+load方法,就是在这个时候进行的;_dyld_objc_notify_unmapped对应unmap_image回调,当dyld将images移除内存时。- 而
category写入 target class 的方法列表,则是在_dyld_objc_notify_mapped,即将Mach-0相关sections都加载到内存之后所发生的。
系统类的Category
步骤
我们看看 runtime-new.mm 如何处理的 map_images
/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const struct _dyld_objc_notify_mapped_info infos[],
_dyld_objc_mark_image_mutable makeImageMutable)
{
bool takeEnforcmentDisableFault;
{
mutex_locker_t lock(runtimeLock);
map_images_nolock(count, infos, &takeEnforcementDisableFault, makeImageMutable);
}
}
我们再来看看 map_images_nolock
/***********************************************************************
* map_images_nolock
* Process the given images which are being mapped in by dyld.
* All class registration and fixups are performed (or deferred pending
* discovery of missing superclasses etc), and +load methods are called.
*
* info[] is in bottom-up order i.e. libobjc will be earlier in the
* array than any library that links to libobjc.
*
* Locking: loadMethodLock(old) or runtimeLock(new) acquired by map_images.
**********************************************************************/
#include "objc-file.h"
void
map_images_nolock(unsigned mhCount, const struct _dyld_objc_notify_mapped_info infos[],
bool *disabledClassROEnforcement,
_dyld_objc_mark_image_mutable makeImageMutable)
{
//...
if (hCount > 0) {
_read_images(mappedInfos, hCount, totalClasses, unoptimizedTotalClasses, makeImageMutable);
}
//...
}
随着断点的一步一步执行 我们发现 在 load_categories_nolock方法里面对 categories 进行了attatch(但是注意 这里我们并没有我们定义的Animal类)
static void load_categories_nolock(header_info *hi) {
bool hasPreoptimizedCategories = hi->info()->dyldCategoriesOptimized() && !DisablePreattachedCategories;
bool hasRoot = dyld_shared_cache_some_image_overridden();
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
size_t count;
auto processCatlist = [&](category_t * const *catlist, bool stubCategories) {
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, cls, hi};
// Process this category.
if (cls->isStubClass()) {
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
// // 如果分类中有实例方法、协议、实例属性,就会改写 target class 的结构
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
if (!didInitialAttachCategories && hasPreoptimizedCategories && objc::inSharedCache((uintptr_t)cls) && objc::inSharedCache((uintptr_t)cls->safe_ro()))
continue;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
if (slowpath(PrintConnecting))
_objc_inform("CLASS: Attaching category (%s) %p to class %s", cat->name, cat, cls->nameForLogging());
attachCategories(cls, &lc, 1, cls, ATTACH_EXISTING);
} else {
if (slowpath(PrintConnecting))
_objc_inform("CLASS: Adding unattached category (%s) %p for class %s", cat->name, cat, cls->nameForLogging());
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
if (slowpath(PrintConnecting))
_objc_inform("CLASS: Attaching category (%s) %p to metaclass %s", cat->name, cat, cls->nameForLogging());
attachCategories(cls->ISA(), &lc, 1, cls, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
if (slowpath(PrintConnecting))
_objc_inform("CLASS: Adding unattached category (%s) %p for metaclass %s", cat->name, cat, cls->nameForLogging());
objc::unattachedCategories.addForClass(lc.reSignedForMetaclass(cls), cls->ISA());
}
}
}
}
};
processCatlist(hi->catlist(&count), /*stubCategories*/**false**);
}
- 遍历每个类别
catlist[i],并重映射类别cat中指向的类cls。 - 检查
cls是否为有效类,若无效则忽略。 - 对于存根类,检查类别中不同种类的方法和属性,若存在则添加到未附加类别列表。
- 对于非存根类,检查是否在初始加载期间,避免重复附加预优化类别。
- 检查并附加实例方法或协议等类别。
- 如果类已实现,直接附加类别;否则添加到未附加类别列表。
- 同样的逻辑处理
metaclass中的方法和属性。
我们继续在看看 attachCategories
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
method_list_t *mlist = entry.getCategory(catsListKey)->methodsForMeta(isMeta);
bool isPreattached = entry.hi->info()->dyldCategoriesOptimized() && !DisablePreattachedCategories;
Lists *lists = isPreattached ? &preattachedLists : &normalLists;
if (mlist) {
if (lists->methods.isFull()) {
prepareMethodLists(cls, lists->methods.array, lists->methods.count, NO, fromBundle, __func__);
rwe->methods.attachLists(lists->methods.array, lists->methods.count, isPreattached, PrintPreopt ? "methods" : nullptr);
lists->methods.clear();
}
lists->methods.add(mlist);
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist = entry.getCategory(catsListKey)->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (lists->properties.isFull()) {
rwe->properties.attachLists(lists->properties.array, lists->properties.count, isPreattached, PrintPreopt ? "properties" : nullptr);
lists->properties.clear();
}
lists->properties.add(proplist);
}
protocol_list_t *protolist = entry.getCategory(catsListKey)->protocolsForMeta(isMeta);
if (protolist) {
if (lists->protocols.isFull()) {
rwe->protocols.attachLists(lists->protocols.array, lists->protocols.count, isPreattached, PrintPreopt ? "protocols" : nullptr);
lists->protocols.clear();
}
lists->protocols.add(protolist);
}
}
- 遍历每个类别
cats_list[i],并获取相关的method_list_t、property_list_t和protocol_list_t。 - 根据类别是否预附加,将列表添加到
preattachedLists或normalLists。 - 如果列表已满,则处理并附加当前列表,同时清空列表以继续处理下一个类别。
加入方法的顺序如下:
总结
- 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init;
- 然后会 map_images;
- 接下来调用 map_images_nolock;
- 再然后就是 read_images,这个方法会读取所有的类的相关信息;
- 再调用 map_images,加载资源;
- 再调用 load_categories_nolock;
- 在 load_categories_nolock 方法内部会调用 attachCategories,这个方法会传入 Class 和 Category,会将方法列表,协议列表等与原有的类合并;
- 最后加入到 class_rw_t 结构体中。
Animal 类
上面我们跟着 attachCategories 的调用栈一步一步执行我们发现了系统类的Category 加载过程。但是我们在 attachCategories 添加以下代码并没有在这个断点断住。难道我们断错了地方吗???
const char * oname = "Animal";
// 在这里ro里面已经有值了
const char *cname = cls->nameForLogging();
if(cname && (strcmp(cname, oname) == 0)) {
printf("attachCategories 类名:%s -%p\n",cname,cls);
}
_read_images
我们调整策略从最开始的 _read_images 开始查看,我们在 OBJC_RUNTIME_DISCOVER_CLASSES_START class 开始的地方,添加上面这段话
我们发现此时走进了我们的断点。我们尝试通过LLDB 获取他的相关内容
我们发现
- 目前的该类还未被 Realized
- rw 内容的中的没有方法列表。(可通过
rw->flags & RW_REALIZED和methods 不存在) 所以目前为Animal类的最初形态,此刻我们有点兴奋。应该是找到方向了。至少是他的最初形态。
所以接下来做了什么呢
methodizeClass
我们换一个方法,在 methodizeClass打上断点我们发现断点断住了,查看他的方法调用栈我们可以发现:
我们发现在第一次被 调用的时候会调用这个方法 ,并且会调用两遍。通过两次发现
一次是元类的调用,一次是类对象的调用。
auto ro = cls->safe_ro();
auto isMeta = ro->flags & RO_META;
照猫画虎,有样学样 我们根据 methodizeClass 获取方法列表
static void
readMethodForAnimal(Class cls,const char *methodName) {
if(!cls) {
return;
}
const char * oname = "Animal";
const char *cname = cls->nameForLogging();
auto ro = cls->safe_ro();
auto isMeta = ro->flags & RO_META;
_objc_inform("the %s class name is %s",
isMeta ? "(meta)" : "",cls->nameForLogging());
if(cname && (strcmp(cname, oname) == 0)) {
printf("***************func is %s, ro methods*************\n",methodName);
if (method_list_t *list = ro->baseMethods.template dyn_cast<method_list_t *>()) {
method_list_t **addedLists = &list;
for (int i = 0; i < 1; i++) {
method_list_t *mlist = addedLists[i];
// Unique selectors in list.
for (auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
printf("ro func name:%s\n",name);
}
}
}
auto rw = cls->data();
// auto methods = rw->methodAlternates();
if(rw->flags & RW_REALIZED) {
printf("***************func is %s, rw methods*************\n",methodName);
// do something
auto methods = rw->methodAlternates();
method_list_t *list = methods.list;
method_list_t **addedLists = &list;
for (int i = 0; i < 1; i++) {
method_list_t *mlist = addedLists[i];
if(mlist) {
// Unique selectors in list.
for (auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
printf("rw func name:%s\n",name);
}
}
}
}
}
}
第一次元类输出
第一次类输出
我们此刻发现在methodizeClass 已经添加上了方法列表!!所以在这两个方法中间一定发生了什么事情使得方法列表加上了。
我们往前回溯。在realizeClassWithoutSwift 我们发现了 rw的REALIZE!
在 rw 的前后分别调用我们调试代码 我们发现了端倪!
RW拥有了方法!所以我们得出通过设置将RO的方法属性注入了RW中! 那RO里面的方法呢开始是有还是没有的呢?
back to _read_images
我们再次回到原来的 _read_images 将我们的调试代码输出我们得到
所以我们得出结论
在编译时期,分类的方法会被注入到该类的ro中,在运行时通过第一次的调用,将RO 注入到RW中。
懒加载类
当我们没有在类中实现 +load 方法时,类的加载实在main函数后(第一次调用类的方法)。当我们实现了+load类的加载实现在main函数之前。(默认大家都知晓哈)
我们在类和分类中分别实现 +load
我们还是按照以上的步骤一步一步执行 我们发现
在 _read_images 方法中输出ro 的方法列表没有了分类的方法,分类的添加发生了变化。
我们继续执行 方法 在 OBJC_RUNTIME_REALIZE_NON_LAZY_CLASSES_START()下,类被加载了!调用了 realizeClassWithoutSwift方法!说明 添加+load 方法后,类的加载是在_read_images 中!
我们继续,RW realize 了,但Category里的方法并没有加载到类里面。
有没有可能这次是通过
attachCategories添加的呢?我们在该方法里面添加调试代码,重新执行,神奇的事情发生了,调用了 attachCategories!!
这里我们不再复述 attachCategories 过程。我们看看调用 attachCategories 时候的方法栈
我们在分类和类的
+load分别按照一个加一个不加进行探索。这里我们就不再赘述。我们直接说结果。
结论
-
懒加载类 + 懒加载分类
- 类的加载在
第一次消息发送的时候(realizeClassWithoutSwift),在编译时将分类的方法加载到类的RO中
- 类的加载在
-
非懒加载类 + 懒加载分类
- 类的加载在
_read_images中,在编译时将分类的方法加载到类的RO中
- 类的加载在
-
非懒加载类 + 非懒加载分类
- 类的加载在
_read_images中,分类方法的添加是 在load_images中attachCategories将方法内容添加到了类中。
- 类的加载在
-
懒加载类 + 非懒加载分类
- 类的加载在
_read_images中,在编译时将分类的方法加载到类的RO中
- 类的加载在