iOS 之category

277 阅读8分钟

1.分类(category)的作用

1.可以在不修改原来类的基础上,为一个类扩展方法。
2.最主要的用法:给系统自带的类扩展方法.

2.分类中能写点啥?

1.分类中只能添加“方法”,不能增加成员变量。
2.分类中可以访问原来类中的成员变量,但是只能访问@protect和@public形式的变量。如果想要访问本类中的私有变量,分类和子类一样,只能通过方法来访问。
3.如果一定要在分类中添加成员变量,可以通过getter,setter手段进行添加.

3.运行加载时

通过runtime动态运行时将category合并到类对象,元类对象当中

4. 底层实现

1.所有的OC类和对象,在runtime层都是用struct表示的,category也不例外,在runtime层,category用结构体category_t.
2.通过以下命令将分类转化为C++文件
 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc GGPerson+Test.m

category结构体

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

从category结构体定义中可以看出category可以添加类方法,方法,协议,属性,无法添加实例变量。

_method_list_t 结构体源码的实现

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_GGPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_GGPerson_Test_test}}
};

由此我们发现这个结构体_OBJC__CATEGORY_INSTANCE_METHODS_GGPerson_$_Test从名称可以看出是INSTANCE_METHODS对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存,方法数量,以及方法列表。并且从上图中找到分类中我们实现对应的对象方法test方法

从runtime源码分析

在runtime源码分析catagory_t存储的方法,属性,协议等是如何存储在类对象中的。

1.在 objc-os.mm 文件中, _objc_init 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);
}

2.接着我们来到 &map_images读取模块(images这里代表模块),来到map_images_nolock函数中找到_read_images函数,在_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);
            }
        }
    }
}

上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls);函数。我们来到remethodizeClass(cls);函数内部查看。

static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
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*/);//cls 类对象, cats: 分类对象
    free(cats);
    }
}

通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在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;
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);
 //将所有分类的对象方法,添加到类对象的方法列表中,之后释放mlists
rw->methods.attachLists(mlists, mcount);
free(mlists);
//将所有分类的属性,添加到类对象的属性列表中,之后释放mlists
if (flush_caches  &&  mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
//将所有分类的协议,添加到类对象的协议列表中,之后释放mlists
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}

上述源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着所有分类的方法,属性和协议。 之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。 之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。

分析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;
        //内存移动,将array()-lists的内存移动oldCount*sizeof(array()->lists[0])个内存到array()->lists+addedCound中
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        //内存复制:将addedList的内存复制addedCount*suzeof(array()->lists[0])个内存到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]));
    }
}

上述源代码中有两个重要的数组 array()->lists: 类对象原来的方法列表,属性列表,协议列表。 addedLists:传入所有分类的方法列表,属性列表,协议列表。 attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。我们先来分别看一下这两个函数

// memmove :内存移动。
/*  __dst : 移动内存的目的地
*   __src : 被移动的内存首地址
*   __len : 被移动的内存长度
*   将__src的内存移动__len块内存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :内存拷贝。
/*  __dst : 拷贝内存的拷贝目的地
*   __src : 被拷贝的内存首地址
*   __n : 被移动的内存长度
*   将__src的内存移动__n块内存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

下面我们图示经过memmove和memcpy方法过后的内存变化。

经过memmove方法之后,内存变化为

// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists,
              oldCount * sizeof(array()->lists[0]));

经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。 memcpy方法之后,内存变化

// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists,
        addedCount * sizeof(array()->lists[0]));

我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。
那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。
其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看

分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。

Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。