前言

由几个问题引入Category吧

  • Category的使用场合是什么
  • Category的实现原理
  • Category和Extension的区别是什么
  • Category中有load方法吗? load方法是什么时候调用的? load方法能继承吗?
  • load、initialize方法的区别是什么? 它们在Category中调用的顺序? 以及出现继承时它们之间的调用过程
  • Category能否添加成员变量? 如果可以 如何添加

Category的底层原理

结论

先说结论
Category编译之后的底层结构是struct_category_t 里面存储着分类的对象方法、类方法、属性、协议信息
在程序运行的时候 runtime会将Category的数据 合并到类信息中(类对象、元类对象中)

分析

在这里插入图片描述
这里有三个类 下面两个是WXPerson的分类

分类的方法最终会归到类里面
也就是WXPerson+TestWXPerson+Eat里的方法最终会加入WXPerson的方法列表中 不管是实例方法还是类方法 当然类方法会加入WXPerson元类的方法列表中

通过runtime动态将分类的实例方法合并到类中方法列表中
将类方法合并到元类的方法列表中

在这里插入图片描述
通过命令行将其中一个分类转为cpp代码

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;         //属性
};

底层结构我们找到是这样的

当程序编译的时候 分类中所有的数据都变成了这样的结构体对象
暂时分类和类的数据还是分开的

我还在cpp代码里找到了这样一个函数
很明显是用于上面_category_t赋值的

static struct _category_t _OBJC_$_CATEGORY_WXPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"WXPerson",
	0, // &OBJC_CLASS_$_WXPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_WXPerson_$_Test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_WXPerson_$_Test,
	0,
	0,
};

这两个参数很明显是实例方法的方法列表跟类方法的方法列表

(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_WXPerson_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_WXPerson_$_Test,

在cpp文件中往上一点就会看到

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

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_WXPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_WXPerson_Test_test2}}
};

这样的代码段

很清晰
WXPerson+Test分类中的test实例方法跟test2类方法加入了方法列表

static struct /*_protocol_list_t*/ {
	long protocol_count;  // Note, this is 32/64 bit
	struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_WXPerson_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	2,
	&_OBJC_PROTOCOL_NSCoding,
	&_OBJC_PROTOCOL_NSCopying
};

这个是协议列表 我实现了NSCoding协议跟NSCopying协议

还有一个问题
当我们实现了协议方法的时候 这时候传入的method_list_t结构体会有两个
会分开类方法实例方法 跟 协议方法

在这里插入图片描述

Category的load和initialize方法

+load方法会在runtime加载类、分类时调用

不管有没有用到这几个类 都会调用+load方法
在这里插入图片描述

我们知道调用方法的顺序是根据编译时候的顺序
谁在后面编译 调用类的不管是实例方法或者是类方法的时候
都会先调用它的

所以一般来说调用方法的时候 都会先调用到分类的方法

可是load方法可以看到 是根据先调用类 再调用分类的顺序进行的
这是为什么呢

load源码分析

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        // 准备Class list和Category list
        prepare_load_methods((const headerType *)mh);
    }

    // 调用已经准备好的Class list和Category list
    call_load_methods();
}

这里可以看到 最后调用的call_load_methods();方法是关键

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

在这里插入图片描述
这里看的清清楚楚
先调用了类的load方法 之后调用分类的load方法

调用类的load方法的源码

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

直接找到load方法的方法地址 直接调用

每个类、分类的+load方法 在程序运行过程中只调用一次

并且load方法可以继承 但是一般情况下不会主动去调用load方法 都是让系统自己调用

initialize

+initialize方法会在类第一次接受到消息的时候调用

它也是通过消息机制来发送的 根据我们之前的知识知道
如果类和分类方法同名 优先调用分类的方法 而且是后编译的先调用

并且如果出现继承的时候 会先调用父类的initialize方法 再调用子类的
而且先初始化父类 再初始化子类 每个类只会被初始化一次

这里Student是Person的子类 在Student中也写了initialize方法

直接写[WXStudent alloc];
在这里插入图片描述

如果子类没有实现initialize方法 就会调用父类的initialize方法 所以父类的方法有可能被调用多次
在这里插入图片描述

如果分类实现了initialize方法 就会覆盖类本身的initialize调用

在这里插入图片描述

load跟initialize方法的区别是什么

load是根据函数地址直接调用 initialize是通过objc_msgSend调用

load是在runtime加载类 分类的时候调用 所以只会调用一次
而initialize是在第一次接受到消息的时候调用 每个类只会initialize一次 但是父类的initialize可能被执行多次

分类能添加属性吗

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中也可以添加属性 只不过@property只会生成setter和getter的声明 不会生成setter和getter的实现以及成员变量 点语法是可以写 只不过在运行时调用到这个方法时候会报方法找不到的错误

Category为什么不能添加成员变量

Objective-C类是由Class类型来表示的 它实际上是一个指向objc_class结构体的指针

typedef struct objc_class *Class;

定义如下

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                              // 父类
    const char *name                               // 类名
    long version                                  // 类的版本信息,默认为0
    long info                                     // 类信息,供运行期使用的一些位标识
    long instance_size                            // 该类的实例变量大小
    struct objc_ivar_list *ivars                  // 该类的成员变量链表
    struct objc_method_list **methodLists        // 方法定义的链表
    struct objc_cache *cache                     // 方法缓存
    struct objc_protocol_list *protocols         // 协议链表
#endif
};

objc_class结构体的大小是固定的 不可能往这个结构体添加数据 只能修改

所以ivars指向的是一个固定区域 只能修改成员变量值 不能增加成员变量个数

Category和Extension的区别是什么

Extension在编译的时候 它的数据就已经包含在类信息中

Category是在运行时 才会将数据合并到类信息中

Extension可以添加实例变量 但是Category不可以

Extension和Category都可以添加属性 但是Category的属性不能生成成员变量和setter方法getter方法的实现

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐