iOS Runtime之——Category及其底层原理探索

1.概述

  苹果在Objective-C 2.0中推出了新的语言特性Category,为已有类提供新的行为、扩展现有功能。如今Category已经应用到了iOS应用的各个角落。那么本文就对其进行深入剖析,揭开其真实面目。

  源码基于objc4-750,下载地址:objc4-750

2.Category特点

  • 可以减少单个文件的体积
  • 可以把不同的功能组织到不同的Category中
  • 可以按需加载
  • 可以声明私有方法
  • 把framework的私有方法公开

2.Category真面目

  众所周知,所有的OC类和对象,在runtime层都是用struct表示的,category也不例外。在runtime层obj-runtime-new.h中,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);
};
  • name:类名
  • cls:类
  • instanceMethod:给类添加的实例方法列表
  • classMethods:给类添加的类方法列表
  • protocols:实现的所有的协议列表
  • instanceProperties:给类添加的所有属性

  从中我们可以发现,category可以添加实例方法、类方法、实现协议、添加属性,但是不能添加实例变量。因为category是在运行期执行的,而在运行期对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对变异性语言来说是不可能的。

Category与Extension的区别?

  • Category:运行时期确定、不能添加变量(添加变量会影响对象的内存布局)。
  • Extension:类的一部分、编译时期确定、一般用来隐藏类的私有属性、对象的内存布局(分配内存空间)已确。

3.Category加载逻辑

  我们知道OC程序的启动运行是依赖于runtime的,而OC的runtime是通过OS X与iOS通过dyld(苹果的动态加载器,用来加载image,即Mach-O格式的二进制文件)动态加载的。
  而dyld会将app所依赖的各种库文件加载到内存空间中,包括libobjc库、runtime等,这些工作是在main函数中进行的。
  OC的runtime入口方法为_objc_init。_objc_init主要用于读取Mach-O文件对应的Segment section,并根据其中的数据代码信息,完成OC的内存布局以及runtime相关的数据结构的初始化工作。
  下面我们就对源码进行分析。

4.Category底层原理探索

  objc源码:https://opensource.apple.com/tarballs/objc4/
  编译时期:每次编译后的东西都存放到machO可执行文件中
  category初始化

extern category_t **_getObjc2CategoryList(const header_info *hi, size_t *count);

  而category的初始化循环调用了_getObjc2CategoryList方法,_getObjc2CategoryList方法定义如下:

#define GETSECT(name, type, sectname)                                   \
    type *name(const headerType *mhdr, size_t *outCount) {              \
        return getDataSection<type>(mhdr, sectname, nil, outCount);     \
    }                                                                   \
    type *name(const header_info *hi, size_t *outCount) {               \
        return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
    }

  存放了__objc_catlist section 段记录的所有的 Category。

objc源码

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);
}
  • map_images:是一个回调函数,dyld将image(Mach-O格式的二进制文件)加载进内存时,会触发该回调函数。在该函数中将我们的分类加载到类当中的。
  • load_images:dyld初始化image模块时,load_images。。。
  • unmap_image:当将dyld移除内存时,调用该回调函数。

我们重点研究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);
}

map_images_nolock

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

_read_images函数:初始化map_images后的image,包括load所有的类、协议及Category,+load方法就是在这里边调用的,load方法后面我们会详细讲解。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
    ...
    //关键代码
     // Discover categories. 
    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);
                }
            }
        }
    }            
}

  开头定义的catlist就是编译器为我们准备的category_t数组,遍历取出其中的category_t,这段代码很容易理解,主要就是完成以下操作:

  • 将category的实例方法、协议及属性添加到类上:addUnattachedCategoryForClass(cat, cls, hi);。
  • 将类方法、协议添加到类的metaclass中:addUnattachedCategoryForClass(cat, cls->ISA(), hi);。

  addUnattachedCategoryForClass:(cat, cls, hi);是将类和category做一个关联映射。至于category的各种列表是怎么添加到类上的?请看remethodizeClass。

remethodizeClass(cls):处理类的添加操作。

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;
    runtimeLock.assertLocked();
    isMeta = cls->isMetaClass();
    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

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,]
    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];
        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;
        }
    }
    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_list中的所有方法分别取出来组成一个method_list_t,给 prepareMethodLists函数。

static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount, 
                   bool baseMethods, bool methodsFromBundle)
{
    runtimeLock.assertLocked();
    if (addedCount == 0) return;
    // Don't scan redundantly
    bool scanForCustomRR = !cls->hasCustomRR();
    bool scanForCustomAWZ = !cls->hasCustomAWZ();
    // There exist RR/AWZ special cases for some class's base methods. 
    // But this code should never need to scan base methods for RR/AWZ: 
    // default RR/AWZ cannot be set before setInitialized().
    // Therefore we need not handle any special cases here.
    if (baseMethods) {
        assert(!scanForCustomRR  &&  !scanForCustomAWZ);
    }
    // Add method lists to array.
    // Reallocate un-fixed method lists.
    // The new methods are PREPENDED to the method list array.
    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*/);
        }
        // Scan for method implementations tracked by the class's flags
        if (scanForCustomRR  &&  methodListImplementsRR(mlist)) {
            cls->setHasCustomRR();
            scanForCustomRR = false;
        }
        if (scanForCustomAWZ  &&  methodListImplementsAWZ(mlist)) {
            cls->setHasCustomAWZ();
            scanForCustomAWZ = false;
        }
    }
}

  接着将prepareMethodLists处理后的method_list_t交给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);表示将src指针指向位置开始len长度到dst。
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            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]));
        }
    }
    void tryFree() {
        if (hasArray()) {
            for (uint32_t i = 0; i < array()->count; i++) {
                try_free(array()->lists[i]);
            }
            try_free(array());
        }
        else if (list) {
            try_free(list);
        }
    }
    template<typename Result>
    Result duplicate() {
        Result result;
        if (hasArray()) {
            array_t *a = array();
            result.setArray((array_t *)memdup(a, a->byteSize()));
            for (uint32_t i = 0; i < a->count; i++) {
                result.array()->lists[i] = a->lists[i]->duplicate();
            }
        } else if (list) {
            result.list = list->duplicate();
        } else {
            result.list = nil;
        }
        return result;
    }
}
  • memmove表示表示将src指针指向位置开始len长度的数据移动到dst位置,
  • memcpy表示表示将src指针指向位置开始len长度的数据拷贝到dst位置。

最终通过attachLists函数最终将所有分类方法添加到类的方法列表中。

Category加载完成之后需要注意以下几点:
  1).Category并不会替换原类的同名方法,而是会在类方法列表中存放两个同名方法。
  2).Category的方法会被存放到新方法列表的最前面,而原类的方法存放到后边。另外运行时在查找类方法时是顺序查找的,这也是为什么Category方法会优先调用的原因。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JeffersonGO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值