【iOS开发】——Category底层原理、Extension、关联对象
Category是什么?它可以用来干什么?
Category是Objective-C 2.0之后添加的语言特性,别人口中的分类、类别其实都是指的Category。Category的主要作用是为已经存在的类添加方法。
通常情况下我们可以把类的实现分开在几个不同的文件里面,这样做的好处是:
- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的category里
- 可以由多个开发者共同完成一个类
- 可以按需加载想要的category
- 声明私有方法, apple 的SDK中就大面积的使用了category这一特性。比如说:UIKit中的UIView。apple把不同的功能API进行了分类,这些分类包括
UIViewGeometry
、UIViewHierarchy
、UIViewRendering
等。
除此之外,Category还有几个应用场景:
- 模拟多继承(另外可以模拟多继承的还有
protocol
) - 把
framework
的私有方法公开:什么叫把framework的私有方法公开呢?首先我们都知道类中的私有方法是外界调用不到的,所以假如我们有一个类,类中有一个私有方法A外界不能直接调用,但是假如我们写一个分类并在分类中也声明了一个方法A(我们只声明,但是没有实现)。现在我们如果import这个分类category的话是不是就可以调用分类中的方法A了,实际上这时候就会调用私有方法这个A,我们也就通过分类将私有方法公开化了。
Category特点
category
只能给某个已有的类扩充方法,不能扩充成员变量。category
中也可以添加属性,只不过@property
只会生成setter
和getter
的声明,不会生成setter
和getter
的实现以及成员变量。- 如果
category
中的方法和类中原有方法同名,运行时会优先调用category
中的方法。 也就是,category
中的方法会覆盖掉类中原有的方法。 所以开发中尽量保证不要让分类中的方法和原有类中的方法名相同。避免出现这种情况的解决方案是给分类的方法名统一添加前缀。 比如category_。 - 如果多个
category
中存在同名的方法,运行时到底调用哪个方法由编译器决定,最后一个参与编译的方法会被调用。
所以关于方法的调用优先级:
分类(category)
> 本类
> 父类
。即,优先调用cateory中的方法,然后调用本类方法,最后调用父类方法。
注意⚠️:category是在运行时加载的,不是在编译时。
Category的实质以及实现过程
Category的使用方法十分简单,如果感兴趣可以看看iOS——Category的使用方法,所以我们这里重点讲一下Category的实质以及实现过程,首先我们需要定义一个分类:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface People : NSObject
- (void)talk;
@end
#import "People.h"
@implementation People
- (void)talk {
NSLog(@"我还不太会说话");
}
@end
#import "People.h"
NS_ASSUME_NONNULL_BEGIN
@interface People (Speak)
-(void)speak;
@end
NS_ASSUME_NONNULL_END
#import "People+Speak.h"
@implementation People (Speak)
- (void)speak {
NSLog(@"我已经学会说话了");
}
@end
我们将People+Speak.m
文件转成C++
代码:
首先我们需要cd /进入People+Speak.m
文件的上一级文件夹,然后再xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc People+Speak.m
这样就会产生一个C++文件
接着我们就可以分析对应的C++源码了
#ifndef _REWRITER_typedef_People
#define _REWRITER_typedef_People
typedef struct objc_object People;
typedef struct {
} _objc_exc_People;
#endif
struct People_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
// - (void)talk;
/* @end */
#pragma clang assume_nonnull end
#pragma clang assume_nonnull begin
// @interface People (Speak)
// -(void)speak;
/* @end */
#pragma clang assume_nonnull end
// @implementation People (Speak)
static void _I_People_Speak_speak(People * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hq_s56n6rsn2kq_8lvr0p4_2fvr0000gn_T_People_Speak_2adca8_mi_0);
}
// @end
struct _prop_t {
const char *name;
const char *attributes;
};
struct _protocol_t;
struct _objc_method {
struct objc_selector * _cmd;
const char *method_type;
void *_imp;
};
struct _protocol_t {
void * isa; // NULL
const char *protocol_name;
const struct _protocol_list_t * protocol_list; // super protocols
const struct method_list_t *instance_methods;
const struct method_list_t *class_methods;
const struct method_list_t *optionalInstanceMethods;
const struct method_list_t *optionalClassMethods;
const struct _prop_list_t * properties;
const unsigned int size; // sizeof(struct _protocol_t)
const unsigned int flags; // = 0
const char ** extendedMethodTypes;
};
struct _ivar_t {
unsigned long int *offset; // pointer to ivar offset location
const char *name;
const char *type;
unsigned int alignment;
unsigned int size;
};
struct _class_ro_t {
unsigned int flags;
unsigned int instanceStart;
unsigned int instanceSize;
const unsigned char *ivarLayout;
const char *name;
const struct _method_list_t *baseMethods;
const struct _objc_protocol_list *baseProtocols;
const struct _ivar_list_t *ivars;
const unsigned char *weakIvarLayout;
const struct _prop_list_t *properties;
};
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
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;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)
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_People_$_Speak __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{
{
(struct objc_selector *)"speak", "v16@0:8", (void *)_I_People_Speak_speak}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_People;
static struct _category_t _OBJC_$_CATEGORY_People_$_Speak __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"People",
0, // &OBJC_CLASS_$_People,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_People_$_Speak,
0,
0,
0,
};
static void OBJC_CATEGORY_SETUP_$_People_$_Speak(void ) {
_OBJC_$_CATEGORY_People_$_Speak.cls = &OBJC_CLASS_$_People;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_People_$_Speak,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_People_$_Speak,
};
static struct IMAGE_INFO {
unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = {
0, 2 };
我们看一下比较重要的几个地方
Category结构体
【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结构体赋值
static struct _category_t _OBJC_$_CATEGORY_People_$_Speak __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"People",
0, // &OBJC_CLASS_$_People,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_People_$_Speak,
0,
0,
0,
};
我们可以看到如果分类没有添加的东西此时并不会赋值,或者说赋值为0, 因为这个分类我只添加了一个对象方法,所以只给对应的对象方法进行了赋值。
然后就是一个结构体数组
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_People_$_Speak,
};
由此可见分类的底层结构也是结构体,那么我们来看看类的对象调用分类的方法的过程是怎么样的呢
Category的加载处理过程
首先我们要知道Category的加载处理过程是什么样的,主要分为三步,如下所示:
- 通过
Runtime
加载某个类的所有Category
数据 - 把所有
Category
的方法、属性、协议数据,合并到一个大数组中 后面参与编译的Category
数据,会在数组的前面 - 将合并后的
分类数据
(方法、属性、协议),插入到类原来数据的前面
一步一步来看
首先通过Runtime加载某个类的所有Category数据,那我们就从Runtime初始化函数开始看
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?Fixme延迟初始化直到找到一个使用objc的图像?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image)