iOS category 源码解析

前言:

iOS的分类(Category)是做iOS开发常用的一种模式,它可以让我们在不知道原有类内部结构的情况下添加属性,方法。合理的运用分类可以将代码很好的进行解耦,让代码更加清晰明了。

那么分类是如何做到的动态添加属性和方法的呢?要是在分类里面添加了和原类同名方法会发生什么呢?

1.Category简介

category是Object-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。使用场景:

  • 可以将类的实现分开放到不同的文件里面。好处:
    • 可以减少单个文件体积
    • 可以将不同的功能组织到不同的category中
    • 可以由多个开发者完成同一个类
    • 可以按需要加载想要的category等等
  • 生命私有方法
  • 模拟多继承
  • 把framework的私有方法公开

2.Category真面目

想要搞清楚category的真面目有一个最好的方法,那就是看源码。通过apple官方开放的runtiem源码,我们一起来看下category的内部实现和它的调用过程。

category 内部实现:

category实质上还是一个结构体category_t:

struct category_t {
    const char *name;   //分类名称
    classref_t cls;     //关联的类
    struct method_list_t *instanceMethods;  //实例方法列表
    struct method_list_t *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);
};

可以看到category的内部有一个对class的引用,用于指向对应的类。在category中实现的实例方法,类方法等都是在category内的。那么category添加的方法是如何使用的呢?

category的调用

想要看出category怎么调用的,可以使用clang命令来查看对应的C++实现,这里就不赘述了。

调用栈

实际上category的加载是动态加载,在初始化加载的。所以,它实际上的调用栈的栈底是 void _objc_init(void)方法。

那我们看下这个方法的内部实现:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    //环境变量初始化
    environ_init();
    //线程独立键初始化
    tls_init();
    //静态构造函数初始化
    static_init();
    //线程锁初始化
    lock_init();
    //异常初始化
    exception_init();

    //通知注册
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

可以看到这个方法内部先做了一些初始化的操作,然后进行通知的注册。我们接着看下 map_images 函数。

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    //锁
    rwlock_writer_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

这个函数内部又调用了 map_images_nolock 方法,我们再看下这个方法,因为这个方法内部的实现太多。所以,提取和category有关的内容:

if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

而 _read_images 函数内部重要的category调用为 remethodizeClass 。

所以category的调用栈如下:

# _objc_init
## map_images
### map_images_nolock
#### _read_images
##### remethodizeClass

remethodizeClass实现

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

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    //获取cls中未完成拼接的分类列表
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        //将分类列表拼接到cls上
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

可以看出来在remethodizeClass中先判断是否存在未拼接的分类列表,若是有的话,则进行分类的拼接过程attachCategories

下面看下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
    /*
     初始化二维数组的临时变量,空间大小由分类列表的count决定
     */
    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;
    int i = cats->count;//分类数量
    bool fromBundle = NO;
    
    while (i--) { //倒叙遍历,先访问最后编译的分类
        
        //获取一个分类
        auto& entry = cats->list[i];
        
        //获取分类的方法列表,若ismeta是YES的话 则获取分类的元类的方法列表;
        //若ismeta为No的话,则获取分类的实例方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        
        if (mlist) {
            //将编译的分类从后往前添加到分类数组中
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        
        //属性列表添加,同上
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        //协议列表添加,同上
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    //获取class的读写数据,其中包含主类的方法列表信息等
    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);
}

从上面的代码中我们可以看出,系统会将编译的分类中的所有方法列表、属性列表、协议列表全都取出来。

并且将它们全都倒序添加到数组中,用于最后拼接到宿主类中。

注意,最关键的方法来了。attachLists这个方法是最终实现将分类的列表拼接到宿主类中的工程

attachLists

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            
            //原有类的元素总数
            uint32_t oldCount = array()->count;
            //拼接后的类的元素总数
            uint32_t newCount = oldCount + addedCount;
            //根据新的重新分配内存
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            //设置原有类的元素count
            array()->count = newCount;
            //进行内存的移动,将原有类的列表后移到新分配内存的尾部
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            //将分类的列表数据拷贝到新分配内存的头部
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

这段代码可以看出,分类方法的拼接是通过从新开辟内存,将原有类的方法移到尾部,分类方法从头开始copy的方法来实现的。最后将这个list重写写入原有类的rw信息中。

总结

通过对源码的查看,我们应该可以理解几个点:

  • 为什么在分类中实现原有类的同名方法会造成覆盖效果?那是因为内存移动的时候讲原有类的方法移到了尾部。
  • 同名分类方法谁起作用取决于编译顺序。

至此关于分类的调用的主要代码差不多看完了,具体的一些分支想要更详细的了解的话,可以去查看runtime源码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值