在平日编程中或阅读第三方代码时,category可以说是无处不在。category也可以说是OC作为一门动态语言的一大特色。category为我们动态扩展类的功能提供了可能,或者我们也可以把一个庞大的类进行功能分解,按照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_t在源码中的数据结构
struct category_t {
//分类名字
const char *name;
//对应的类
classref_t cls;
//实例对象的方法
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
//类方法
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
//协议
struct protocol_list_t *protocols;
//属性
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
/****获取方法、属性、协议的方法****/
//获取分类中的方法
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
//获取分类中的属性
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
//获取分类中的协议
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
从源码和编译后的结构来看,category包含了以下内容:
const char *name
: 对应类的名字
struct _class_t *cls
: 对应的类,在加载阶段,使用该参数查找对应的类
const struct _method_list_t *instance_methods
: 实例方法列表,在分类中编写的实例方法,会存储在该列表中
const struct _method_list_t *class_methods
: 类方法列表,分类中的类方法,会存储在该列表中
const struct _prop_list_t *properties
: 属性列表,分类中定义的属性会存储在该列表
const struct _protocol_list_t *protocols
: 协议列表,分类中的协议会存储在该列表
category的编译
分类经过编译后
1)、生成了实例方法列表
_OBJC_$_CATEGORY_INSTANCE_METHODS_MethodSend_$_cate
和类方法列表_OBJC_$_CATEGORY_CLASS_METHODS_MethodSend_$_cate
,两者的命名都遵循了公共前缀+类名+category名字的命名方式2)、生成了category的结构
_OBJC_$_CATEGORY_MethodSend_$_cate
3)、在最后执行了
section ("__DATA, __objc_catlist,regular,no_dead_strip")
,表明分类是存储在 __DATA 段的__objc_catlist section
里面的。这部分是Mach-o的数据,在运行时加载category时,从这部分取出所有的category然后添加到对应类的方法列表中。category的加载流程
当动态连接器(dyld)加载处理好的OC代码时,系统会调用objc_init()
方法。
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();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
//向dyld注册监听Mach-O中OC相关section被加载入\载出内存的事件
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
这里主要关注_dyld_objc_notify_register(&map_images, load_images, unmap_image);
方法。该方法会向dyld注册监听Mach-O中OC相关section被加载入\载出内存的事件。
具体有三个事件:
_dyld_objc_notify_mapped(对应map_images
回调):当dyld已将images加载入内存时。
_dyld_objc_notify_init(对应load_images
回调):当dyld初始化image后。OC调用类的+load方法,就是在这时进行的。
_dyld_objc_notify_unmapped(对应unmap_image
回调):当dyld将images移除内存时。
而category写入类的的方法列表,是在_dyld_objc_notify_mapped
,也就是调用map_images
方法时进行的写入操作,即将Mach-O相关sections都加载到内存之后所发生的。
(1)、map_images
方法实现
// _dyld_objc_notify_register监听到dyld已将images加载入内存的通知 处理方法
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
//调用map_images_nolock
return map_images_nolock(count, paths, mhdrs);
}
//接受到dyld已将images加载入内存的通知后的操作
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
static bool firstTime = YES;
header_info *hList[mhCount];
uint32_t hCount;
size_t selrefCount = 0;
/*其他相关代码*/
// Find all images with Objective-C metadata.
hCount = 0;
//计算class数量,根据总数调整各种表的大小。
// Count classes. Size various table based on the total.
int totalClasses = 0;
int unoptimizedTotalClasses = 0;
{
uint32_t i = mhCount;
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[i];
//循环读取mach head 信息,从共享缓存中读取header_info
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
//将读取到的header_info信息添加到hList
hList[hCount++] = hi;
}
}
//从mach中读取到了head_info
if (hCount > 0) {
//直接开始image读取
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
// Call image load funcs after everything is set up.
for (auto func : loadImageFuncs) {
for (uint32_t i = 0; i < mhCount; i++) {
func(mhdrs[i]);
}
}
}
map_images
是调用了map_images_nolock
方法,由map_images_nolock
处理后续的逻辑。
在map_images_nolock
方法中,最主要的工作是遍历可执行文件,从共享缓存中读取header_info
信息,并且把遍历出来的header_info
信息存储到hList
中,以供后面的处理流程使用。读取到所有的header_info
后,调用_read_images
方法。
(2)、_read_images
方法实现
//从mach-o中加载类、协议、分类、selector等
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
bool launchTime = NO;
TimeLogger ts(PrintImageTimes);
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
// 开始遍历头文件,进行类与元类的读取操作并标记(旧类改动后会生成新的类,并重映射到新的类上)
for (EACH_HEADER) {
////从头文件中拿到类的信息
classref_t const *classlist = _getObjc2ClassList(hi, &count);
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
//!核心操作,readClass读取类的信息及类的更新
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
// 处理分类category,并rebuild重建这个类的方法列表method list
// Discover categories. Only do this after the initial category
// attachment has been done. For categories present at startup,
// discovery is deferred until the first load_images call after
// the call to _dyld_objc_notify_register completes. rdar://problem/53119145
if (didInitialAttachCategories) {
for (EACH_HEADER) { // for(hIndex = 0; hIndex < hCount && (hi = hList[hIndex]); hIndex++)
load_categories_nolock(hi);
}
}
/*
其他相关代码
*/
#undef EACH_HEADER
}
_read_images
方法是非常重要的方法,代码量也是很大的,大概三百多行,涉及到类、协议、方法和分类的加载等等。这里主要以分类的加载进行说明。
方法中,使用宏EACH_HEADER
定义了一个for循环。对map_images_nolock
方法中归集出来的header_info
进行遍历,调用load_categories_nolock
方法处理header_info
的catlist
。
(3)、load_categories_nolock
方法实现
static void load_categories_nolock(header_info *hi) {
// hi是mach-o的头,表头
// 这个count的数量是下面hi->catlist(&count)
/*
*processCatlist(hi->catlist(&count));
*processCatlist(hi->catlist2(&count));
*/
//有几个分类,就是cout就是多少
size_t count;
//获取全部类的全部的分类
// C++ 闭包 [&](category_t * const *catlist) {};
auto processCatlist = [&](category_t * const *catlist) {
//遍历每个分类,处理类方法和对应类的绑定、对象方法和对应类的绑定
for (unsigned i = 0; i < count; i++) {
//取出i位置的category
category_t *cat = catlist[i];
//取出category中保存的类的信息
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, hi};
//处理这个分类
// Process this category.
//如果当前类是rootClass
if (cls->isStubClass()) {
// Stub classes are never realized. Stub classes
// don't know their metaclass until they're
// initialized, so we have to add categories with
// class methods or properties to the stub itself.
// methodizeClass() will find them and add them to
// the metaclass as appropriate.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
//如果当前类是 非rootClass
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
//实例方法、协议、属性
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
//ATTACH_EXISTING = 1 << 3 = 0001 << 3 = 1000 = 8
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
//类方法
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
//ATTACH_EXISTING = 1 << 3 = 0001 << 3 = 1000 = 8
//ATTACH_METACLASS = 1 << 1 = 0001 << 1 = 0010 = 2
//ATTACH_EXISTING | ATTACH_METACLASS = 1010 = 10
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};
// 就是从mach-o中获取分类,但是为什么分两个?_getObjc2CategoryList 不必关心,没意义,是编译时的东西
// _getObjc2CategoryList
processCatlist(hi->catlist(&count));
// _getObjc2CategoryList2
processCatlist(hi->catlist2(&count));
}
load_categories_nolock
方法对header_info
中的catlist
做处理。
首先定义一个回调processCatlist
(可以类比block
),在回调中遍历catlist
每一个category,并调用attachCategories
方法处理category中的类方法和实例方法的绑定。然后调用这个回调,实现方法的绑定流程。
(4)、attachCategories
方法实现
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;
//实例对象,传值为8,即1000 ;类对象传值为10,即 1010
//ATTACH_METACLASS = 1 << 1 = 0010
//isMeta 在实例对象时,= 0; 类对象时,=1
bool isMeta = (flags & ATTACH_METACLASS);
/*
*ro属于cleanmemory,在编译即确定的内存空间,只读,加载后不会改变内容的空间
*rw属于dirtymemory,rw是运行时结构,可读可写,可以向类中添加属性、方法等,在运行时会改变的内存;
*rwe相当于类的额外信息,因为在实际使用过程中,只有很少的类会真正的改变他们的内容,所以为避免资源的消耗就有了rwe;
*
*ro被复制一份到rw里面,这是因为在运行时分类可以添加方法,而程序员也可以动态添加方法或者属性到类里面,而ro是只读的,所以需要在rw里面来追踪这些东西。
*不是每一个类都会动态添加,所以如果这片内存写在rw里面,那么就会对脏内存有影响,所以把这些东西放在rwe里面
*/
auto rwe = cls->data()->extAllocIfNeeded();
//拿出来类的全部分类,并获取到所有分类里面的方法、属性、和协议
for (uint32_t i = 0; i < cats_count; i++) {
//拿到一个分类category
auto& entry = cats_list[i];
//从分类中拿出来方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//如果分类中方法超过64个,数组存满了,就先添加方法列表.
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
// 添加方法
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
//最开始查到的分类中的方法,放到最后一个位置,从后向前插入到mlists中
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
}
//把之前获取到的所有分类中的方法、属性、和协议,添加到类中
if (mcount > 0) {
//把分类的方法添加到方法列表
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
//调用list_array_tt的方法attachLists
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
//
//ATTACH_EXISTING = 1 << 3 = 1000 = 8 = 1000
//flags 对象方法是 1 << 3 = 8 = 1000; 类方法是ATTACH_EXISTING | ATTACH_METACLASS = 8 | 2 = 10 = 1010
//在加载分类的流程中,这个缓存的刷新都会走到
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);
}
auto rwe = cls->data()->extAllocIfNeeded();
:该方法获取到类的class_ro_t
,使用这个变量支持修改类的信息。
该方法把分类中的方法、协议、属性添加到类中。
methods 类型为
method_array_t
、properties 类型为 property_array_t
、protocols 类型为 protocol_array_t
。而这三个类型都是list_array_tt
,所以添加到类的时候,都是调用的list_array_tt
的方法attachLists
。
(5)、list_array_tt
中attachLists
方法实现
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
//拿到原数组的count
uint32_t oldCount = array()->count;
//根据原数组的count和新加进来的count,得到新数组的size
uint32_t newCount = oldCount + addedCount;
//根据新数组的size,开辟空间,得到新的数组
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->lists[i + addedCount] = array()->lists[i];
//新来的数组,从前向后,插入到新数组中
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();
}
}
attachLists
方法添加数据的时候,把新进来的数据添加到前面,原来的数据是排在后面。
category的一些问题
(1)、category和类定义相同的方法,方法的调用顺序
- 分类与类的同名方法调用
根据attachLists
方法,可以看出,分类中的方法是插入在类原有方法前面,所以分类方法会优先调用。
但是这种并不是简单的方法覆盖,只是按照顺序优先调用category中的方法。 -
分类与分类中同名方法的调用
根据编译顺序,最后编译的category优先调用。
(2)、category中为什么不能添加属性
对属性的修饰符@property
。使用@property
的时候,系统会自动生成带“_”的成员变量和该变量的setter和getter方法。
属性归根结底是由成员变量和setter + gettr
方法组成。而这部分的生成的时期是编译期。
经过category的学习,能清楚category的加载是在运行时。即使是使用@property
修饰了,也已经过了编译期,不会生成成员变量和setter+getter方法。
如果有需要在分类中添加属性,可以借助关联对象
的方法,手动创建属性的setter
和getter
,并与属性的进行关联。
代码如下:
/*
*MethodSend (cate)分类的interface
*/
@interface MethodSend (cate)
//定义属性。这里是通过重写setter和getter方法,MethodSend实例和cate_Name进行的关联。
//和编译期的@property创建成员变量、setter、getter是不同的逻辑
//在分类中添加的属性,没有成员变量
@property (nonatomic, copy) NSString * cate_Name;
@end
/*
*MethodSend (cate)分类的实现
*/
@implementation MethodSend (cate)
- (void)setCate_Name:(NSString *)cate_Name{
NSLog(@"setCate_Name is cate_Name = %@",cate_Name);
objc_setAssociatedObject(self,
"cate_Name",
cate_Name,
OBJC_ASSOCIATION_COPY);
}
- (NSString *)cate_Name{
NSString*string = objc_getAssociatedObject(self, "cate_Name");
NSLog(@"cate_Name is cate_Name = %@",string);
//只是MethodSend对象关联了cate_Name,并不能真正的生成成员变量、setter、getter
//NSLog(@"cate_Name is _cate_name = %@",_cate_name);
return string;
}
(3)、category和extension的区别
加载时机:
category
:在运行时加载
extension
:在编译期加载
添加属性:
category
:不能添加属性,准确点说添加了属性,但是没有成员变量、setter、getter,不能正常使用
extension
:由于在编译期加载,所以extension
可以添加属性,并声称对应的成员变量、setter、getter
添加方法:
category
:可以添加方法
extension
:只能是方法的定义,没有方法的实现
用处:
category
:可以用来为系统类添加方法;或者使用category
来对自定义类进行功能的拆分。
extension
:一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension,除非创建子类再添加extension。而category不需要有类的源码,我们可以给系统提供的类添加category。