iOS Category底层原理分析(一)

531 阅读7分钟

目录

1. Category的使用场景

2. Category的底层结构

3. Category的加载处理过程

4. Category和Extension区别


1. Category的使用场景

  • Category可以在不改变或不继承原类的情况下,动态地给类添加方法。除此之外还有一些其他的应用场景:
  1. 把类的的实现分开在几个不同的文件里面。这样做有几个显而易见的好处:

可以减少单个文件的体积;
可以把不同的功能组织到不同的 category 里面,降低耦合;
可以由多个开发者共同完成一个类;
可以按需加载想要的 category;
声明私有方法。

  1. 为系统类添加方法(开发中应该是最常用的吧)。

  2. 模拟多继承(另外可以模拟多继承的还有 protocol)。

  3. 把framework 的私有方法公开。


2. Category的底层结构

  • 我们知道OC中所有的对象在runtime层都是用struct表示的,当然category也是,这里我们给NSObject加一个category,并且新增play和eat方法,看看内部到底是啥构造
//NSObject+Like.h
#import <Foundation/Foundation.h>
@interface NSObject (Like)

-(void)paly;

-(void)eat;

@end

//NSObject+Like.m
#import "NSObject+Like.h"
@implementation NSObject (Like)

-(void)paly{
    
    NSLog(@"like play");
}

-(void)eat{
    
    NSLog(@"like eat");
}
@end

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx.m -o xxx.cpp 转为.cpp看看里面的内部结构,主要函数如下:

///分类的结构体 这个就是category结构体
struct _category_t {
	const char *name; //category的名字
	struct _class_t *cls;//类结构体指针,包含类的一些基本信息(下面类结构体)
	const struct _method_list_t *instance_methods;// category中所有给类添加的实例方法的列表
	const struct _method_list_t *class_methods;// category中所有添加的类方法的列表
	const struct _protocol_list_t *protocols;//category实现的所有协议的列表
	const struct _prop_list_t *properties;//category中添加的所有属性
};

///_category_t的初始化
static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Like __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"NSObject", //category的名字
	0, // &OBJC_CLASS_$_NSObject,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Like,//有对象方法即paly和eat
	0,//category没有类方法为0
	0,//category没有协议方法为0
	0,//category没有属性则为0
};

///类结构体
struct _class_t {
	struct _class_t *isa;
	struct _class_t *superclass;
	void *cache;
	void *vtable;
	struct _class_ro_t *ro;
};

///方法列表
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2]; ///方法列表,2表示分类有两个方法即play和eat
} _OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Like __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"paly", "v16@0:8", (void *)_I_NSObject_Like_paly},
	{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_NSObject_Like_eat}}
};

///用于运行期category的加载
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_NSObject_$_Like,
};

从上面可以看到:

  1. 编译器生成了实例方法列表OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Like命名遵循了公共前缀+类名+category名字的命名方式,如果我们在category添加了属性或者协议同样也会生成属性/协议列表.

  2. 其次编译器生成了category本身_category_t _OBJC_$_CATEGORY_NSObject_$_Like,并用前面生成的列表来初始化category本身。

  3. 最后,编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组L_OBJC_LABELCATEGORY$,用于运行时category的加载。

通过上面C++代码的实现我们知道Category底层就是 _category_t结构体,主要包含以下信息:

  • name : category的名字
  • cls : 类的信息(包含isa、superclass等信息)
  • instance_methods : category中所有给类添加的实例方法的列表
  • class_methods: category中所有添加的类方法的列表
  • protocols : category实现的所有协议的列表
  • properties: category中添加的所有属性

3. Category的加载处理过程

  • Category的加载处理过程主要分为以下几步:
  1. 在运行时,通过runtime加载某个类的所有Category数据
  2. 所有Category的方法、属性、协议数据,都会被分别合并到一个大数组的中,并且后面编译的会在数组前面;
  3. 将合并后的分类数据(方法、属性、协议),分类插入到类原来的数据前面。

方法执行顺序:

  • 当分类和类都有相同的方法,调用此方法会调用分类的还是类的?

通过上面3.的分析,类的方法在方法列表中是排在最后的,所以当对象调用方法去方法列表里找的时候,会先找到分类的方法执行返回。

  • 当多个分类都有相同的方法,会优先调用哪个分类的?

这个就是通过编译顺序决定的,后编译的文件中的同名方法会排在前面。

源码分析:Objc源码下载地址
这里取的是目前最新的objc4-818.2版本来证实一下上面所说的加载过程

因为源码太多,这里分析主要的实现函数:

objc-os.mm 文件 (主要是初始化)
  _objc_init  
  map_images
  map_images_nolock
  
objc-runtime-new.mm文件
  _read_images    //读取信息
  attachCategories //将方法列表、属性和协议从类别附加到类中
  
objc-runtime-new.h文件
  attachLists  //合并所有方法列表、属性和协议到新的数组中
//为了方便学习,保留关键代码
static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,int flags)
{
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    //方法列表
    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++) {
        auto& entry = cats_list[i];
        //取出方法数组
        //isMeta == Yes 取出类方法
        //isMeta == No 取出对象方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                // 将mlist中的方法添加到添加到rwe(原类中的类方法列表)中去
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            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) {
                // 将proplist中的属性添加到添加到rwe(原类中的属性表)中去
                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) {
                // 将protolist中的协议添加到添加到rwe(原类中的属性表)中去
                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);
}
    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        if (hasArray()) {
            //主要是这部分:
            //这里如果有分类对象的方法/协议/属性列表的话
            //会重新malloc一个数组,大小为oldCount + addedCount
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;
            
           //这里遍历是i--,即i是从大到小,原来的旧数据会放在newArray的最后面
           //这里也就从源码的角度解释了,原类的方法在方法列表中是排在最后的
            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
           //这里遍历是i++,即新增的数据会依次加到newArray的前面
           //最终newArray刚好填满,组成一个新的方法/协议/属性列表
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<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;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

通过上面关键部分代码和注释,应该对Category的加载处理过程有了一个更清晰的认识吧。


4. Category和Extension区别

  1. Extension在编译的时候,它的数据就已经包含在类信息中,Category是在运行时,才会将数据合并到类信息中。

  2. 因为1.的原因,extension 可以添加成员变量,category 不能添加成员变量。运行时加载类到内存以后,才会加载分类,这时类的内存布局已经确定(编译器还会对成员变量顺序做出优化,保证遵循内存对齐原则下类占用内存容量最少),如果再去添加成员变量就会破坏类的内存布局。各个成员变量的访问地址是在编译时确定的,每个成员变量的地址偏移都是固定的(相对于类的起始地址的内存偏移(硬编码))

  3. extension 和 category 都可以添加属性,但是 category 中的属性不能生成对应的成员变量以及getter 和 setter方法的实现(可以通过关联对象添加属性生成 getter和setter方法)

  4. extension 不能像 category 那样拥有独立的实现部分(@implementation 部分),extension 所声明的方法必须依托对应类的实现部分来实现。

  5. category 可以给系统提供的类添加分类,而extension一般用来隐藏类的私有信息,无法直接为系统的类扩展。