手把手带你探索Category底层原理

2,819 阅读9分钟

前言

Category在iOS开发中使用非常的频繁,特别是在为系统类进行拓展的时候,我们可以不用继承系统类,直接给系统类添加方法,最大程度的体现了Objective-C的动态语言特性。

本文篇幅较长,但内容完整,建议能跟随文章内容探索一遍,毕竟实践出真知。

Catergory的作用

  1. 将类的实现分散到多个不同的文件或多个不同的框架中。如下:不同的功能模块用不同的Category处理

  2. 可以在不修改原来类的基础上,为一个类添加扩展方法。如我们需要给系统自带的类添加方法。

  3. 会覆盖原类中方法名相同的方法,多个Category的同名方法,会按照编译顺序,执行最后编译的Category里的方法。

  4. Extension(扩展)的区别:

    • Category 是运行期决定的,Extension 是编译期决定,是类的一部分。
    • Category 不能添加实例变量(只能通过runtime添加),Extension 可以添加实例变量。(因为在运行期,对象的内存结构是已经确定的,如果添加实例变量就会破坏类的内部结构)
    • Category 和 Extension 都可以用@property添加属性,但是Category添加的属性不能生成成员变量和getter,setter方法的实现,即不能通过_var调用,不过可以手动通过objc_get/setAssociatedObject手动实现。

Category编译期探索

上面说了一大堆,现在正式开始探索历程,探索上面解释的正确性,先来看看编译期干了什么吧。

  • 新建了一个测试工程,创建了一个实体类,和一个Test分类 , 分类里声明一个Test方法,并cmd+B编译一下

  • 打开命令行工具,cd到当前目录下,执行clang -rewrite-objc Person+Test.m命令,然后找到当前工程文件夹,找到编译后的 Person+Test.cpp文件并打开

  • 文件内容非常多,由于知道category的结构体是_categoy_t, 全局搜索找到了它的结构体

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;//属性列表
};
  • 继续搜索 _category_t,找到了我们的测试的分类
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Person",
	0, // &OBJC_CLASS_$_Person,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
	0,
	0,
	0,
};
  • 这里的section ("__DATA,__objc_const"),是一个段标识,它会存放到我们的dyld加载Macho可执行文件里的这个section段里(关于dyld及Macho文件内容较多,这里暂时不做详细解释,这里了解到它会在编译的时候会存放到Macho文件里即可)
  • 对比着上面_category_t的结构体,发现这里的类名是Person,实例方法列表里存放了一个Test方法
  • 具体搜索这个实例方法,看到它就在上面几行的代码里
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"Test", "v16@0:8", (void *)_I_Person_Test_Test}}
};
  • 这里保存了这个方法的大小,第二个值是方法类型, 通过下面找到的_objc_method结构体对照着可以看到
  • 第三个参数imp里包含了Sel方法编号,方法签名及真实的函数地址
struct _objc_method {
	struct objc_selector * _cmd;
	const char *method_type;
	void  *_imp;
};
  • 我们了解到上面的所有内容就是在编译器主要是把category编译成结构体,然后把对应的值填充到结构体里,保存在Macho可执行文件里
  • 继续搜索_category_t,发现还有如下代码
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_Person_$_Test,
};
  • 这里其实是把我们APP的所有category方法都保存到__objc_catlist里,也就是在加载到Macho文件里的对应的section段里

编译期总结

在编译期,把category编译成对应的结构体保存到Macho文件里的section段,把所有的分类方法都保存到Macho文件__objc_catlist这个section段里

Category运行期探索

了解到编译器主要做了保存的操作,那么运行期毫无疑问是做的加载操作,需要把刚刚编译期保存的内容都进行加载。

dyld加载

先来看看Category是如何被加载的

  1. dyld是苹果的动态加载器,用来加载image(image指的是Mach-O格式的二进制文件,不是图片)

  2. 当程序启动时,系统内核首先会加载dyld,而dyld会将我们APP所依赖的各种库加载到内存中,其中就包括libobjc库(OC和runtime),这些工作,是在APP的main函数执行之前完成的

  3. _objc_init是Object-C runtime 的入口函数,在这里主要是读取Mach-O文件OC对应的Segment section,并根据其中的数据代码信息,完成为OC的内存布局,以及初始化runtime相关的数据结构。

先验证一下_objc_init是否是入口函数,打开刚才的测试工程,添加一个符号断点_objc_init,然后运行工程

这里验证了我们上面的说法,入口函数是_objc_init,接下来才会执行dyld_start加载函数

探索_objc_init

源码工程objc4网盘链接 密码:tuw8

  • 这里需要用到下载的源码,打开objc4,直接搜索_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?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
  • 前面都是一些init函数,这里重点是_dyld_objc_notify_register,注册了三个回调函数
  • &map_images将image加载进内存
  • load_imagesdyld初始化加载image方法
  • unmap_images移除内存

我们要探索Category是如何被加载进内存的,所以要看&map_images到底做了什么,点进这个方法

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}
  • 继续过渡,找到关键函数_read_images
void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    //其余无关代码已省略
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
}
  • _read_images函数里的内容较多,找到与category相关的代码,这段代码较长
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//其余代码已省略
       for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
  • 首先_getObjc2CategoryList函数是读取所有的category方法,点进该方法可以看到它其实就是读取我们编译期时的_objc_catlistsection段内容
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
  • 然后遍历所有的分类方法并处理,if (cat->instanceMethods)可以看到这里判断了当前category的方法是类方法还是实例方法,并分别做不同的处理
  • addUnattachedCategoryForClass(cat, cls, hi);这里是把category与该class原类关联映射起来,可以点进去该方法看内容
  • 看到这里还有一个remethodizeClass(cls);,看名字像是重新设置类里面的函数,点进去看看具体函数内容
static void remethodizeClass(Class cls)
{
    //多余代码省略
    category_list *cats;
        // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}
  • 继续点进去看看attachCategories关联分类函数的具体实现
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    //省略无关代码
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));
        
    //省略无关代码
           prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
}
  • 这里初始化了方法列表,协议列表和集合列表,着重找mlists
  • 可以看到这里有两个重要的方法prepareMethodListsattachLists
  • 先点进去看看prepareMethodLists
//省略其他无关代码
for (int i = 0; i < addedCount; i++) {
        //把要添加原类的方法列表取出来
        method_list_t *mlist = addedLists[i];
        assert(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }
  • 取出来方法列表后,调用了fixupMethodList,点进去看看

  • 这里做的是把方法列表里的方法都注册到原类里

  • 总之,prepareMethodLists做的是添加方法列表前的准备工作

  • 回到外面,点击进入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;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            /**
             拷贝的操作
             void    *memmove(void *__dst, const void *__src, size_t __len);
             */
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
  • 可以看到这里是通过获取到要添加的分类方法列表和它的大小,然后拷贝到原有的方法列表里
  • 这里最终经过了memmovememcpy函数完成了拷贝操作,那么这两个函数具体到底是做了什么呢?

memmove是把原类里的方法列表,向后移动了要添加的方法列表的大小的距离

memcopy是把要添加的方法列表拷贝到原类刚刚的方法列表里空出来的位置上

  • 探索到这里,已经明白为什么分类的方法能覆盖原类的方法了,它只是比原类的方法先调用而已,实际上并没有覆盖这个方法。也明白为什么多个分类同名方法会执行最后编译的分类里的方法里,也是调用顺序的问题。
  • 这个还和方法调用流程有关,二分查找法,所以会优先调用前面的方法

探索Category关联对象objc_get/setAssociatedObject

众所周知,在分类里可以通过objc_get/setAssociatedObject来模拟添加属性,那么它到底是如何实现的呢?

  • 继续打开刚才的源码,搜索objc_setAssociatedObject,找到其方法内容
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
  • 继续找_object_set_associative_reference,这里的代码较多,逐行分析

  • acquireValue是进行内存管理,可以点进去看一下

  • 这里有一个AssociationsManager,看到它里面有个AssociationsHashMap,并且访问会加锁,是线程安全的

  • disguised_ptr_t disguised_object = DISGUISE(object);这里使用对象取反后的值作为key,迭代器里的value 是ObjectAssociationMap

  • 再看ObjectAssociationMap,它的key是用户传进来的自定义key,它的value是ObjcAssociation

  • 还有最后一个重要的方法setHasAssociatedObjects,这里把属性和类关联起来,并且设置isa指针的标识isa.has_assoc,以便释放的时候使用

  • 同理,objc_getAssociatedObject也是从这里取出来值的

关联属性是什么时候移除的?

在上面我们知道属性是通过类的isa关联起来的,那么理应在这个对象销毁的时候一起移除该属性。 同样的在当前objc源码里搜索dealloc,找到了它的实现

- (void)dealloc {
    _objc_rootDealloc(self);
}
  • 继续跟踪
void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
  • 继续跟踪找到了rootDealloc
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
  • 在这里先做了一些isa标识的判断,没有找到我们想要的
  • 继续跟踪object_dispose
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
  • 继续跟踪,终于在objc_destructInstance里找到了属性的销毁
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
  • 这里有通过isa.has_assoc标识符判断当前对象是否有关联属性,如果有就调用上面代码里的_object_remove_assocations移除关联属性

  • 继续看_object_remove_assocations,和我们设置关联属性的代码类似,不过这里是取出来然后delete refs删除

总结

以上就是探索Category底层原理的整个过程,也使得文章开头的Category的作用得到验证。整个过程是枯燥和冗长的,但是探索完还是有很大的收获。本文篇幅很长,希望大家也能亲自试着探索一遍,能不仅仅满足于知道这是什么,还要去探究为什么会是这样。