分类是我们平时很常用的一种技术,可以为现有类按模块划分添加方法。
分类的基本使用
首先创建ZJPerson类
@interface ZJPerson : NSObject
- (void)run;
@end
@implementation ZJPerson
- (void)run {
NSLog(@"run");
}
@end
再创建两个ZJPerson分类
@interface ZJPerson (Study)
- (void)read;
@end
@implementation ZJPerson (Study)
- (void)read {
NSLog(@"read");
}
@end
@interface ZJPerson (Life)
- (void)sleep;
@end
@implementation ZJPerson (Life)
- (void)sleep {
NSLog(@"read");
}
@end
然后在main文件里导入头文件
#import "ZJPerson.h"
#import "ZJPerson+Study.h"
#import "ZJPerson+Life.h"
然后在main函数中创建ZJPerson对象,调用分类里的方法
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZJPerson *person = [ZJPerson new];
[person run];
[person read];
[person sleep];
}
return 0;
}
分类的底层结构
我们使用命令行cd到ZJPerson+Study.m文件路径下,使用
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZJPerson+Study.m
这个命令将study分类转换成C/C++语言,然后将在ZJPerson+Study.m文件路径下生成的cpp文件拖到项目中
截屏2021-01-05 21.10.52.png 截屏2021-01-05 21.11.03.png然后在cpp文件中搜索_category_t
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;
};
static struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"ZJPerson",
0, // &OBJC_CLASS_$_ZJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_ZJPerson_$_Study,
0,
0,
0,
};
-
ZJPerson+Study.m转换成来的结构体就是struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Study
在编译的时候,一个分类对应一个结构体,比如
study分类对应struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Study
life分类对应struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Life -
分类里的实例方法存放在结构体里的instance_methods数组中
-
类方法和协议方法还有属性同理
category添加方法的底层原理
首先我们打开runtime源码objc4
选中objc-os.mm类
//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);
}
点击进入map_images方法
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
继续点击进入map_images_nolock方法,然后再点击map_images_nolock里的_read_images方法
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
……
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
……
}
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
……
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
……
}
继续点击进入load_categories_nolock方法
static void load_categories_nolock(header_info *hi) {
……
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
……
}
attachCategories这个方法就是合并分类方法、属性、协议的核心方法了,继续点进去
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
……
constexpr uint32_t ATTACH_BUFSIZ = 64;
//cls : [ZJPerson class]
//cats_list : [category_t(Study), category_t(Life)]
//cats_count : 分类数量2
//方法大数组
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++) {
//按照编译顺序取出category
//i = 0 : category_t(Study)
//i = 1 : category_t(Life)
auto& entry = cats_list[I];
//取出category里的方法数组
//如果是category_t(Study),则对应[read]
//如果是category_t(Life),则对应[sleep]
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
//将方法列表倒序插入方法数组中
//第一次循环i = 0,mlists里的数据为[[read]]
//第二次循环i = 1,mlists里的数据为[[sleep], [read]]
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) {
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) {
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);
}
我们继续点进attachLists方法
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
//addedLists : [
// [read],
// [sleep]
// ]
//addedCount : 2
if (hasArray()) {
// many lists -> many lists
//array : 类对象里的方法列表[[run]]
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
//根据分类的数量对array扩容
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; I--)
//将类对象里的方法数组放在newArray的最后面
//newArray : [null, null, [run]]
newArray->lists[i + addedCount] = array()->lists[I];
for (unsigned i = 0; i < addedCount; I++)
//将分类里的方法数组依次插入到数组里
//第一次循环
//newArray : [[read], null, [run]]
//第二次循环
//newArray : [[read], [sleep], [run]]
newArray->lists[i] = addedLists[I];
free(array());
setArray(newArray);
validate();
}
……
}
到这里就将分类的里的方法,属性,协议都合并到类对象里去了
category的加载处理过程总结
- 首先会通过runtime加载某个类的所有category数据
- 将所有的category方法、属性、协议数据分别合并到一个大数组中
- 分别将方法、属性、协议大数组插入到原来类对象数据的前面
分类方法覆盖原有的方法
我们更新ZJPerson和它的分类代码
@interface ZJPerson : NSObject
- (void)run;
- (void)play;
@end
@implementation ZJPerson
- (void)run {
NSLog(@"run");
}
- (void)play {
NSLog(@"ZJPerson play");
}
@end
@interface ZJPerson (Study)
- (void)read;
- (void)play;
@end
@implementation ZJPerson (Study)
- (void)read {
NSLog(@"read");
}
- (void)play {
NSLog(@"ZJPerson (Study) play");
}
@end
@interface ZJPerson (Life)
- (void)sleep;
- (void)play;
@end
@implementation ZJPerson (Life)
- (void)sleep {
NSLog(@"read");
}
- (void)play {
NSLog(@"ZJPerson (Life) play");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZJPerson *person = [ZJPerson new];
[person play];
}
return 0;
}
ZJPerson类和它的分类里都有一个play方法,那么如果调用这个方法,会调用那个play方法呢?
首先,根据前面的逻辑,分类的方法最后会合并在原有方法的前面,所以可以肯定的是,这个play方法会调用分类的的方法,然后,再根据前面的逻辑,先编译的分类数据会放在大数组的后面,后编译的分类的数据放在大数组的前面,所以这个play方法就是后编译的分类的play
截屏2021-01-07 22.03.18.png
可以看到life分类是后编译的,所以上面会输出life的play方法,我们运行看一看
截屏2021-01-07 22.13.17.png
我们在把study和life的编译顺序换一换
截屏2021-01-07 22.14.09.png
在运行一次看输出
截屏2021-01-07 22.15.08.png
面试题
- category的实现原理
参见上面的category的加载处理过程总结 - category和class extension的区别
extension在编译的时候,它的数据就已经合并在类信息中
category是程序在运行时将数据合并到类信息中
网友评论