OC底层原理-Category

17 阅读5分钟

最近有在面试iOS职位,以前的一些知识点很多都忘记了,因此在复习的过程中也总结出文章发出来一起共享下。

首先我们先从问题出发

什么是分类?

分类是Object-C语言的一种特性,允许你在不修改原有类的前提下,为已有类动态添加实例方法、类方法、属性和协议。

分类的作用?

  1. 扩展已有类的功能,为系统类和三方类添加新的方法。
  2. 分体体积庞大的类,便于代码维护。
  3. 可以在分类中**声明和实现私有方法,**只在本模块使用,避免暴漏到头文件。

分类不能做什么?

分类不能添加实例变量,需要通过关联对象技术实现添加实例变量的效果。


源码分析

源码是基于runtime 680版本

通过上面的三个问题,我们对分类有了大体的认识,接下来我们需要通过源码分析,在使用分类的时候需要注意的问题。

//运行时环境初始化入口
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();
        
    // Register for unmap first, in case some +load unmaps something
    //在dyld中注册回调函数
    _dyld_register_func_for_remove_image(&unmap_image);
    dyld_register_image_state_change_handler(dyld_image_state_bound,
                                             1/*batch*/, &map_2_images);
    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}

oc runtime的初始化入口,app启动后由dyld调用这个入口。在dyld_register_image_state_change_handler函数调用里面注册了map_2_images方法回调。这个是镜像加载方法的回调,接下来在去查看map_2_images的函数实现。

const char *
map_2_images(enum dyld_image_states state, uint32_t infoCount,
             const struct dyld_image_info infoList[])
{
    rwlock_writer_t lock(runtimeLock);
    return map_images_nolock(state, infoCount, infoList);
}

map_2_images函数中返回了map_images_nolock函数的实现。

const char *
map_images_nolock(enum dyld_image_states state, uint32_t infoCount,
                  const struct dyld_image_info infoList[])
{
   
   //.......
   //其它不相关代码

    _read_images(hList, hCount);

    firstTime = NO;

    return NULL;
}

map_images_nolock方法中,很多不相关的代码我们直接忽略,只关注_read_images方法。

void _read_images(header_info **hList, uint32_t hCount)
{
   
   
   //.....
   //其它代码

            // 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) 
            {
            //把分类未拼接的方法,放到一个全局的hash表中。
                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  
                /* ||  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);
                }
            }
        }
    }
    
    //.....
    //其它代码

}

当cls有方法列表且完成初始化后,会调用remethodizeClass处理分类方法。

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    //获取未拼接category_list
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        //传入未拼接category_list
        attachCategories(cls, cats, true /*flush caches*/);
        free(cats);
    }
}

unattachedCategoriesForClass方法获取未拼接category_list 然后调用关键方法attachCategories

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    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));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    //i是分类的个数
    int i = cats->count;
    bool fromBundle = NO;
    //倒序遍历
    while (i--) {
        //获取一个category_t
        auto& entry = cats->list[i];
        
        //从获取一个category_t获取方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            //mlists是二维数组,结构大概是这样[[method_t,method_t],[method_t,method_t,method_t]]
            //                                  分类1                  分类2
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //这里是真正往宿主类拼接分类方法的入口
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

倒序遍历数组中的每一个category_t,从category_t获取method_list_t,最后把method_list_t添加到mlists里面,执行rw->methods.attachLists(mlists, mcount);进行最后的拼接。

 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;
            /*
             array()->lists + addedCount 数组首元素新的指针地址
             array()->lists 原数组首个元素指针地址
             oldCount * sizeof(array()->lists[0]) 移动的内存大小
             总结来说就是把以前的方法列表向后移动,把需要增加的方法列表腾出地方。
             */
            
            memmove(array()->lists + addedCount, array()->lists,
                    oldCount * sizeof(array()->lists[0]));
            //addedLists拷贝到array()->lists的前面。
            memcpy(array()->lists, addedLists,
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            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;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

最终的方法列表的拼接是在这里面完成。首先是通过memmove()以前的方法列表向后移动,然后在经过memcpy() 把需要新增加的方法列表插入到以前方法列表的前面,完成分类的拼接工作。

runtime 在方法调用时,会使用selector查询方法列表,首次查到后立即返回。

经过上面的代码分析,我们得出了以下几个结论

  1. 分类同名方法会覆盖宿主类同名方法。
  2. 多个分类存在同名方法时候,最后编译的分类的同名方法会生效。