runtime 由浅入深--分类、拓展

330 阅读13分钟

前言

我们在开发中,一定或多或少的使用到了分类,对于分类的使用,对于iOS开发者来说也是必不可少的技能。但是添加的分类如何将方法灌入原类中,他和runtime 有什么关系,分类先后顺序又是如何呢?可能被问及可能有点懵,接下来我们就一起走进 category的世界。

extension

在说道category 时,我们总是不得不提 extension 。他们有什么区别这个话题经久不衰,其实理解起来很简单,我们这边做一个简单的介绍。 extension 是类的一部分,在编译期和头文件里的 @interface 以及实现文件里的 @implement 一起形成一个完整的类,它伴随类的产生而产生亦随之一起消亡。extension 一般用来隐藏类的私有信息,必须有一个类的源码才能为一个类添加 extension,所以无法为系统的类extension
所以category 与 extension的主要区别其实就是 extension相当于把变量属性的访问权限改为私有了,编译后就己经合并到底层的C++代码中了,category是在运行时才合并到类的方法列表中。

category

为了方便理解 我们这里首先创建 Animal 的文件,同时创建他的一个分类Eat 如下图所示

image.png

image.png

终端利用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的具体实现:

image.png

Category的加载

通过 Clang 知道了系统在编译时期,将分类内容存储在地方,具体做了什么操作。那他到底是如何被加入到类中的呢。程序的入口点在 iOS 中被称之为 main 函数:所写的所有代码,它的执行的第一步,均是由 main 函数开始的。但其实,在程序进入 main 函数之前,内核已经为程序加载和运行做了许多的事情。 在上一篇我们的 ### iOS源码objc 编译以及调试派上用场了,我们在 objc-os.m 文件的 _objc_init打上断点。

从断点的方法列表 我们可以发现 _objc_init 在这里我们进入我们的代码。

image.png

我们先进行浅浅的分析一下里面的方法。

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;
}

image.png

  • 上图为 _objc_init 解释,我们可以看出这个方法 就是Bootstrap初始化。将我们的资源通知器注册到 dyld。保证在初始化之前由 libSystem 调用。
  • dyld 是 Apple 的动态链接编辑器,它是 macOS 和 iOS 系统中动态库装载和链接的核心组件。
  • image(Mach-O 的二进制文件)

image.png 从上图我们又能看到对应方法所在地。

  • _dyld_objc_notify_mapped3 对应 map_image 回调:当 dyld 已将 images 加入内存时;
  • _dyld_objc_notify_init2 对应 load_image 回调:当 dyld 初始化 image 时,OC 调用类的 +load 方法,就是在这个时候进行的;
  • _dyld_objc_notify_unmapped 对应 unmap_image 回调,当 dyldimages 移除内存时。
  • 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_tproperty_list_t 和 protocol_list_t
  • 根据类别是否预附加,将列表添加到 preattachedLists 或 normalLists
  • 如果列表已满,则处理并附加当前列表,同时清空列表以继续处理下一个类别。

加入方法的顺序如下:

image.png

总结
  • 程序启动后,通过编译之后,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 开始的地方,添加上面这段话

image.png

我们发现此时走进了我们的断点。我们尝试通过LLDB 获取他的相关内容

image.png

我们发现

  • 目前的该类还未被 Realized
  • rw 内容的中的没有方法列表。(可通过rw->flags & RW_REALIZED和methods 不存在) 所以目前为Animal类的最初形态,此刻我们有点兴奋。应该是找到方向了。至少是他的最初形态。
    所以接下来做了什么呢
methodizeClass

我们换一个方法,在 methodizeClass打上断点我们发现断点断住了,查看他的方法调用栈我们可以发现:

image.png

image.png 我们发现在第一次被 调用的时候会调用这个方法 ,并且会调用两遍。通过两次发现一次是元类的调用一次是类对象的调用

auto ro = cls->safe_ro();
auto isMeta = ro->flags & RO_META;

照猫画虎,有样学样 我们根据 methodizeClass 获取方法列表

image.png

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);
                    }
                }
                
                
            }
        }
    }
}

第一次元类输出

image.png 第一次类输出

image.png

我们此刻发现在methodizeClass 已经添加上了方法列表!!所以在这两个方法中间一定发生了什么事情使得方法列表加上了。
我们往前回溯。在realizeClassWithoutSwift 我们发现了 rw的REALIZE

image.png 在 rw 的前后分别调用我们调试代码 我们发现了端倪! image.png RW拥有了方法!所以我们得出通过设置将RO的方法属性注入了RW中! 那RO里面的方法呢开始是有还是没有的呢?

back to _read_images

我们再次回到原来的 _read_images 将我们的调试代码输出我们得到

image.png

所以我们得出结论

在编译时期,分类的方法会被注入到该类的ro中,在运行时通过第一次的调用,将RO 注入到RW中。
懒加载类

当我们没有在类中实现 +load 方法时,类的加载实在main函数后(第一次调用类的方法)。当我们实现了+load类的加载实现在main函数之前。(默认大家都知晓哈)

我们在类和分类中分别实现 +load

image.png

image.png

我们还是按照以上的步骤一步一步执行 我们发现

 在 _read_images 方法中输出ro 的方法列表没有了分类的方法,分类的添加发生了变化。

image.png

我们继续执行 方法 在 OBJC_RUNTIME_REALIZE_NON_LAZY_CLASSES_START()下,类被加载了!调用了 realizeClassWithoutSwift方法!说明 添加+load 方法后,类的加载是在_read_images 中
我们继续,RW realize 了,但Category里的方法并没有加载到类里面。

image.png 有没有可能这次是通过 attachCategories添加的呢?我们在该方法里面添加调试代码,重新执行,神奇的事情发生了,调用了 attachCategories!!
这里我们不再复述 attachCategories 过程。我们看看调用 attachCategories 时候的方法栈

image.png 我们在分类和类的 +load分别按照一个加一个不加进行探索。这里我们就不再赘述。我们直接说结果。

结论
  • 懒加载类 + 懒加载分类

    • 类的加载在第一次消息发送的时候(realizeClassWithoutSwift),在编译时将分类的方法加载到类的RO
  • 非懒加载类 + 懒加载分类

    • 类的加载在_read_images中,在编译时将分类的方法加载到类的RO
  • 非懒加载类 + 非懒加载分类

    • 类的加载在_read_images中,分类方法的添加是 在load_imagesattachCategories将方法内容添加到了类中。
  • 懒加载类 + 非懒加载分类

    • 类的加载在_read_images中,在编译时将分类的方法加载到类的RO