Category In Objective-C
来源
Objective-C 2.0
中新增的语言特性
可以用来做什么 ?
-
扩展已有的类 (仅限于为已有类增加方法) ;
-
分割类实现 ;
官方原文: Distribute the implementation of your own classes into separate source files—for example, you could group the methods of a large class into several categories and put each category in a different file.
几个显而易见的好处 :
a. 可以减少单个文件的体积 ;
b. 可以把不同的功能组织到不同的category里 ;
c. 可以由多个开发者共同完成一个类 ;
d. 可以按需加载想要的category ; -
声明私有方法 ; (虽然是apple推荐, 但我并没有理解这么做有什么卵用) ;
/* * 官方示例代码, MyClass.m file */ #import "MyClass.h" @interface MyClass (PrivateMethods) // private method declarations @end @implementation MyClass // private method definitions @end
-
模拟多继承 ;
-
把framework的私有方法公开 ;
Category的本质是什么 ?
所有的Objective-C
的类和对象,在 runtime 层都是用 struct 表示的,category 也不例外, 在 objc-class.h
中, 找到如下定义 :
/*
* Category Template
*/
typedef struct objc_category *Category;
struct objc_category {
char *category_name;
char *class_name;
struct objc_method_list *instance_methods;
struct objc_method_list *class_methods;
struct objc_protocol_list *protocols;
};
从 Category
的定义中, 不难窥测, 其可为与不可也 ~ (可以增加 instance_methods, class_methods, protocols, 不可增加 instance, 故若在 Category 中新增 Property, 本质只会保留方法声明)
Category的实现分析
1. 生成测试代码
/*
* SQIClass.h 文件
*/
#import <Foundation/Foundation.h>
@interface SQIClass : NSObject
- (void)sqiFunc;
@end
@interface SQIClass(SQIAddition)
@property(nonatomic, copy) NSString *sqiProperty;
- (void)sqiFunc;
@end
/*
* SQIClass.m 文件
*/
#import "SQIClass.h"
@implementation SQIClass
- (void)sqiFunc
{
NSLog(@"%@",@"I'm SQIClass func");
}
@end
@implementation SQIClass(SQIAddition)
- (void)sqiFunc
{
NSLog(@"%@",@"I'm SQIAddition func");
}
@end
说明:
确定分析工具
$ clang -rewrite-objc SQIClass.m
clang -rewrite-objc
的作用是把oc代码转写成c/c++代码,这里选用它来窥探OC Category
的底层实现.
具体分析 (SQIClass.cpp文件的源码分析)
以上命令会在当前目录生成大小为 3.8MB 的文件 (SQIClass.cpp), 代码共 65475 行 🤣🤮
Category 的 C++ 定义
// 起始于 65354 行
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;
};
与上文的 struct objc_category
基本一一对应, 多出来的 properties 暂时忽略.
Category增加的实例方法列表
// 起始于 65431 行
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_SQIClass_$_SQIAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"sqiFunc", "v16@0:8", (void *)_I_SQIClass_SQIAddition_sqiFunc}}
};
实例方法列表为: _OBJC_$_CATEGORY_INSTANCE_METHODS_SQIClass_$_SQIAddition
, method_count 值为 1, 说明共有 1 个方法.
Category增加的属性列表
// 起始于 65441 行
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_SQIClass_$_SQIAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"sqiProperty","T@\"NSString\",C,N"}}
};
属性列表为: _OBJC_$_PROP_LIST_SQIClass_$_SQIAddition
, count_of_properties 的值为 1, 说明共有 1 个属性.
创建分类实体, 并用上面的列表对象初始化
static struct _category_t _OBJC_$_CATEGORY_SQIClass_$_SQIAddition __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"SQIClass",
0, // &OBJC_CLASS_$_SQIClass,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_SQIClass_$_SQIAddition,
0,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_SQIClass_$_SQIAddition,
};
在DATA段下的objc_catlist section
里保存了一个大小为1的_category_t的数组
// start line: 65472
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_SQIClass_$_SQIAddition,
};
Category是如何加载的 ?
觉得这个问题本身有问题, 没有具体说明白是加载什么, 也没有说明白加载到哪 !
Category是 Objective-C 运行时的一部分, 其所有相关数据结构都定义于 runtime 中, 而OC的runtime和其他系统库一样,是OS X和iOS通过dyld动态加载的.
Category与类的结合
OC运行时在初始化时, 会把 Category 的方法列表与类的方法列表合并 (实例方法列表与合并到类上, 类方法列表合并到类的 metaclass 上);
方法覆盖
当 Category 中的方法与原类中的方法名相同, 原类中的方法会被覆盖, 原因是新的方法列表中, Category新增的方法在前, 运行时在查找方法时, 找到一个对应名称的方法后, 不会向下继续;
注意: 方法覆盖与是否 import 分类的头文件无关, 即无需 import 分类的头文件, 就会产生覆盖现象 !
// SQIClass.h 文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SQIClass : NSObject
- (void)sqiFunc;
@end
NS_ASSUME_NONNULL_END
// SQIClass.m 文件
#import "SQIClass.h"
@implementation SQIClass
- (void)sqiFunc {
NSLog(@"SQIClass's sqiFunc");
}
@end
// SQIClass + SQIAddition.h 文件
#import "SQIClass.h"
NS_ASSUME_NONNULL_BEGIN
@interface SQIClass (SQIAddition)
- (void)sqiFunc;
@end
NS_ASSUME_NONNULL_END
// SQIClass + SQIAddition.m 文件
#import "SQIClass+SQIAddition.h"
@implementation SQIClass (SQIAddition)
// warnning: Category is implementing a method which will also be implemented by its primary class
- (void)sqiFunc {
NSLog(@"SQIAddition's sqiFunc");
}
@end
// SQITest.m 文件
#import "SQIClass.h"
@interface SQITest ()
@end
@implementation SQITest
- (void)test {
// print: SQIAddition's sqiFunc
[[SQIClass new] sqiFunc];
}
@end
+load
方法的调用
- 在 +load 方法中可以调用 Category 方法么 ?
可以, Category 方法的附加要早于 +load 方法的调用.
- 当原类, 多个原类的 Category 中都实现了 +load 方法, 调用顺序是怎样的 ?
原类早于Category, Category的 +load 调用顺序取决于编译顺序, 若在 Xcode 中编译, 可以从
Compile Sources
中查看或调整编译顺序.
备注
- 博客时间: 2023.03.18 ;
- 环境: macOS Monterey 12.6, Xcode Version 14.2, clang version 14.0.0