CC的博客

  • 首页
  • iOS
  • Android
  • React-Native
  • 读书杂谈
  • About
CC
记录美好生活
  1. 首页
  2. 技术编程
  3. iOS
  4. 正文

category探索

2021/03/23

简介

category是Objective-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。除此之外,apple还推荐了category的另外两个使用场景

  1. 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处:

- 减少单个文件的体积
- 把不同的功能组织到不同的category里
- 由多个开发者共同完成一个类
- 按需加载想要的category等
2. 声明私有方法

category定义

category定义了category的行为,可以添加实例方法、类方法、实现协议、添加属性

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


category加载过程

1、通过runtime加载类的所有分类数据。

2、把所有的分类的方法、协议、属性数据合并到一个大数组中。

3、将合并后的分类的数据插入到类原来的数据前面。

category源码分析

定义一个Myclass,并定义MyAddition命名的category

#import <Foundation/Foundation.h>

@interface MyClass : NSObject

- (void)printName;

@end

@interface MyClass(MyAddition)

@property(nonatomic, copy) NSString *name;

- (void)printName;

@end


@implementation MyClass

- (void)printName
{
    NSLog(@"%@",@"MyClass");
}

@end

@implementation MyClass(MyAddition)

- (void)printName
{
    NSLog(@"%@",@"MyAddition");
}

@end

使用clang查看编译后的C++源代码。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MyClass.m -o MyClass.cpp

去除掉无关代码后,抽取和category相关的代码,如下所示

//分类的方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_MyClass_MyAddition_printName}}
};
//分类的属性列表
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"name","T@\"NSString\",C,N"}}
};

extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_MyClass;

static struct _category_t _OBJC_$_CATEGORY_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MyClass",//类名
    0, // &OBJC_CLASS_$_MyClass,//类cls
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition,//示例方法列表
    0,//类方法列表
    0,//遵循的协议方法列表
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MyClass_$_MyAddition,//属性列表
};
static void OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition(void ) {
    _OBJC_$_CATEGORY_MyClass_$_MyAddition.cls = &OBJC_CLASS_$_MyClass;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
    (void *)&OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition,
};
static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
    &OBJC_CLASS_$_MyClass,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_MyClass_$_MyAddition,
};

从代码中可以看到:

  1. 首先编译器生成了实例方法列表OBJC$_CATEGORY_INSTANCE_METHODSMyClass$_MyAddition和属性列表OBJC$_PROP_LISTMyClass$_MyAddition,两者的命名都遵循了公共前缀+类名+category名字的命名方式,而且实例方法列表里面填充的正是我们在MyAddition这个category里面写的方法printName,而属性列表里面填充的也正是我们在MyAddition里添加的name属性。还有一个需要注意到的事实就是category的名字用来给各种列表以及后面的category结构体本身命名,而且有static来修饰,所以在同一个编译单元里我们的category名不能重复,否则会出现编译错误。

  2. 其次,编译器生成了category本身OBJC$_CATEGORYMyClass$_MyAddition,并用前面生成的列表来初始化category本身。

  3. 最后,编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组L_OBJC_LABELCTEGORY$(当然,如果有多个category,会生成对应长度的数组),用于运行期category的加载。

Objective-C的运行是依赖OC的runtime的,而OC的runtime和其他系统库一样,是OS X和iOS通过dyld动态加载的。Category(分类)的加载发生在 _read_images 方法中
objc-runtime-new.m中定义了_read_images方法

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

主要用到了两个方法:

addUnattachedCategoryForClass(cat, cls, hi) 为类添加未依附的分类(把 Category(分类) 的对象方法、协议、属性添加到类上)
remethodizeClass(cls) 重建类的方法列表(把 Category(分类) 的类方法、协议添加到类的 metaclass 上)

addUnattachedCategoryForClass(cat, cls, hi); 方法

static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertLocked();

    // 取得存储所有未依附分类的列表:cats
    NXMapTable *cats = unattachedCategories();
    category_list *list;
    // 从 cats 列表中找到 cls 对应的未依附分类的列表:list
    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    // 将新增的分类 cat 添加 list 中
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    // 将新生成的 list 添加重新插入 cats 中,会覆盖旧的 list
    NXMapInsert(cats, cls, list);
}

addUnattachedCategoryForClass(cat, cls, hi); 的执行过程可以参考代码注释。执行完这个方法之后,系统会将当前分类 cat 放到该类 cls 对应的未依附分类的列表 list 中。简而言之,就是把类和分类做了一个关联映射。

实际上真正起到添加加载作用的是下边的 remethodizeClass(cls); 方法。

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

runtimeLock.assertLocked();

isMeta = cls->isMetaClass();

// 取得 cls 类的未依附分类的列表:cats
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
    // 将未依附分类的列表 cats 附加到 cls 类上
    attachCategories(cls, cats, true /*flush caches*/);        
    free(cats);
}

}

`remethodizeClass(cls);` 方法主要就做了一件事:调用 `attachCategories(cls, cats, true);` 方法将未依附分类的列表 cats 附加到 cls 类上。所以,我们再来看看 `attachCategories(cls, cats, true);` 方法。

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

bool isMeta = cls->isMetaClass();

// 创建方法列表、属性列表、协议列表,用来存储分类的方法、属性、协议
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;     // 记录是否是从 bundle 中取的
while (i--) { // 从后往前依次遍历
    auto& entry = cats->list[i];  // 取出当前分类

    // 取出分类中的方法列表。如果是元类,取得的是类方法列表;否则取得的是对象方法列表
    method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
    if (mlist) {
        mlists[mcount++] = mlist;            // 将方法列表放入 mlists 方法列表数组中
        fromBundle |= entry.hi->isBundle();  // 分类的头部信息中存储了是否是 bundle,将其记住
    }

    // 取出分类中的属性列表,如果是元类,取得的是 nil
    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;
    }
}

// 取出当前类 cls 的 class_rw_t 数据
auto rw = cls->data();

// 存储方法、属性、协议数组到 rw 中
// 准备方法列表 mlists 中的方法
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 将新方法列表添加到 rw 中的方法列表中
rw->methods.attachLists(mlists, mcount);
// 释放方法列表 mlists
free(mlists);
// 清除 cls 的缓存列表
if (flush_caches  &&  mcount > 0) flushCaches(cls);

// 将新属性列表添加到 rw 中的属性列表中
rw->properties.attachLists(proplists, propcount);
// 释放属性列表
free(proplists);

// 将新协议列表添加到 rw 中的协议列表中
rw->protocols.attachLists(protolists, protocount);
// 释放协议列表
free(protolists);

}

从 `attachCategories(cls, cats, true);` 方法的注释中可以看出这个方法就是存储分类的方法、属性、协议的核心代码。内部真正起作用的是`attachLists`方法
```cpp
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;
            //移动原方法列表到末尾
            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]));
        }
    }

从attachLists方法内部看到,会把分类的方法列表放前面,原类的方法列表放最后。当向对象发消息的时候,查找方法列表的实现,优先查找到分类的方法列表,所以分类的方法会被优先调用。

标签: 暂无
最后更新:2022/07/02

CC

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

COPYRIGHT © 2020 CC的博客. ALL RIGHTS RESERVED.

Theme Kratos

豫ICP备2023032048号