OC底层原理(五):Category

2021-01-07  本文已影响0人  跳跳跳跳跳跳跳

分类是我们平时很常用的一种技术,可以为现有类按模块划分添加方法。

分类的基本使用

首先创建ZJPerson类

@interface ZJPerson : NSObject
- (void)run;
@end

@implementation ZJPerson
- (void)run {
    NSLog(@"run");
}
@end

再创建两个ZJPerson分类

@interface ZJPerson (Study)
- (void)read;
@end

@implementation ZJPerson (Study)
- (void)read {
    NSLog(@"read");
}
@end
@interface ZJPerson (Life)
- (void)sleep;
@end

@implementation ZJPerson (Life)
- (void)sleep {
    NSLog(@"read");
}
@end

然后在main文件里导入头文件

#import "ZJPerson.h"
#import "ZJPerson+Study.h"
#import "ZJPerson+Life.h"

然后在main函数中创建ZJPerson对象,调用分类里的方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZJPerson *person = [ZJPerson new];
        [person run];
        [person read];
        [person sleep];
    }
    return 0;
}

分类的底层结构

我们使用命令行cd到ZJPerson+Study.m文件路径下,使用

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZJPerson+Study.m

这个命令将study分类转换成C/C++语言,然后将在ZJPerson+Study.m文件路径下生成的cpp文件拖到项目中

截屏2021-01-05 21.10.52.png 截屏2021-01-05 21.11.03.png

然后在cpp文件中搜索_category_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;
};

static struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "ZJPerson",
    0, // &OBJC_CLASS_$_ZJPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_ZJPerson_$_Study,
    0,
    0,
    0,
};

category添加方法的底层原理

首先我们打开runtime源码objc4
选中objc-os.mm类

//runtime初始化方法
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方法

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方法,然后再点击map_images_nolock里的_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);
}
……
}
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
……
if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
……
}

继续点击进入load_categories_nolock方法

static void load_categories_nolock(header_info *hi) {
……
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
……
}

attachCategories这个方法就是合并分类方法、属性、协议的核心方法了,继续点进去

attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
……
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    //cls : [ZJPerson class]
    //cats_list : [category_t(Study), category_t(Life)]
    //cats_count : 分类数量2
    //方法大数组
    method_list_t   *mlists[ATTACH_BUFSIZ];
    //属性大数组
    property_list_t *proplists[ATTACH_BUFSIZ];
    //协议大数组
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();

    for (uint32_t i = 0; i < cats_count; i++) {
        //按照编译顺序取出category
        //i = 0 : category_t(Study)
        //i = 1 : category_t(Life)
        auto& entry = cats_list[I];
        //取出category里的方法数组
        //如果是category_t(Study),则对应[read]
        //如果是category_t(Life),则对应[sleep]
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            //将方法列表倒序插入方法数组中
            //第一次循环i = 0,mlists里的数据为[[read]]
            //第二次循环i = 1,mlists里的数据为[[sleep], [read]]
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        //属性逻辑同方法
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        //协议逻辑同方法
        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }
    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        //将所有分类的方法合并到类对象里去
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
    //将所有分类的属性合并到类对象里
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    //将所有分类的协议合并到类对象里
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

我们继续点进attachLists方法

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        //addedLists : [
        //              [read],
        //              [sleep]
        //             ]
        //addedCount : 2
        if (hasArray()) {
            // many lists -> many lists
            
            //array : 类对象里的方法列表[[run]]
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            //根据分类的数量对array扩容
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; I--)
            //将类对象里的方法数组放在newArray的最后面
            //newArray : [null, null, [run]]
                newArray->lists[i + addedCount] = array()->lists[I];
            for (unsigned i = 0; i < addedCount; I++)
            //将分类里的方法数组依次插入到数组里
            //第一次循环
            //newArray : [[read], null, [run]]
            //第二次循环
            //newArray : [[read], [sleep], [run]]
                newArray->lists[i] = addedLists[I];
            free(array());
            setArray(newArray);
            validate();
        }
……
}

到这里就将分类的里的方法,属性,协议都合并到类对象里去了

category的加载处理过程总结

  1. 首先会通过runtime加载某个类的所有category数据
  2. 将所有的category方法、属性、协议数据分别合并到一个大数组中
  3. 分别将方法、属性、协议大数组插入到原来类对象数据的前面

分类方法覆盖原有的方法

我们更新ZJPerson和它的分类代码

@interface ZJPerson : NSObject
- (void)run;
- (void)play;
@end

@implementation ZJPerson
- (void)run {
    NSLog(@"run");
}

- (void)play {
    NSLog(@"ZJPerson play");
}
@end
@interface ZJPerson (Study)
- (void)read;
- (void)play;
@end

@implementation ZJPerson (Study)

- (void)read {
    NSLog(@"read");
}

- (void)play {
    NSLog(@"ZJPerson (Study) play");
}
@end
@interface ZJPerson (Life)
- (void)sleep;
- (void)play;
@end

@implementation ZJPerson (Life)
- (void)sleep {
    NSLog(@"read");
}

- (void)play {
    NSLog(@"ZJPerson (Life) play");
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZJPerson *person = [ZJPerson new];
        [person play];
    }
    return 0;
}

ZJPerson类和它的分类里都有一个play方法,那么如果调用这个方法,会调用那个play方法呢?
首先,根据前面的逻辑,分类的方法最后会合并在原有方法的前面,所以可以肯定的是,这个play方法会调用分类的的方法,然后,再根据前面的逻辑,先编译的分类数据会放在大数组的后面,后编译的分类的数据放在大数组的前面,所以这个play方法就是后编译的分类的play


截屏2021-01-07 22.03.18.png

可以看到life分类是后编译的,所以上面会输出life的play方法,我们运行看一看


截屏2021-01-07 22.13.17.png
我们在把study和life的编译顺序换一换
截屏2021-01-07 22.14.09.png
在运行一次看输出
截屏2021-01-07 22.15.08.png

面试题

  1. category的实现原理
    参见上面的category的加载处理过程总结
  2. category和class extension的区别
    extension在编译的时候,它的数据就已经合并在类信息中
    category是程序在运行时将数据合并到类信息中
上一篇 下一篇

猜你喜欢

热点阅读