OC分类
一、分类的作用
对现有已存在的类添加方法,但是不想在现有类中进行修改,如果团队开发,直接在现有类中进行修改,会导致别人的代码出现bug或者其他意外,所以有了分类。
疑问:
1、分类中能否添加成员变量?
首先分类中没有isa指针,而且在objc源码中查看分类的结构体时会发现,并没有存储成员变量的列表。所以没有办法给分类添加成员变量
2、分类中能否添加属性?
分类中可以添加属性,系统会给我们声明setter以及getter方法,但是在setter和getter方法里面并没有进行赋值以及取值操作,所以正常理论上来说是不能添加属性的。但是结合runtime就可以实现给分类添加属性,runtime有两个API可以辅助我们实现
id objc_getAssociatedObject(id object, const void *key)
实现取值,void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
赋值。如下示例:
#import "LGPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson (KGPerson)
@property (nonatomic,copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
#import "LGPerson+KGPerson.h"
#import <objc/runtime.h>
static NSString *nameKey = @"NAME_KEY";
@implementation LGPerson (KGPerson)
- (NSString *)name{
return objc_getAssociatedObject(self, &nameKey);
}
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
二、分类的结构体
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;
}
};
从以上分类的结构体可以看出,我们可以在分类中添加对象方法、类方法、协议以及属性,但是需要注意的就是系统只会生成setter和getter方法的声明,需要我们自己去实现setter和getter方法。
针对属性实现setter方法赋值和getter方法取值,我们考虑有哪些方法处理?如果有怎么实现,方案的缺点?
有如下几种方案:
(1)、声明一个全局的变量。缺点:如果有多个有多个对象,就会共用一个全局变量,其中一个对象修改,所有对象的值都会改变。
(2)、声明一个全局的字典存储。优点:避免了对象之间赋值相互影响问题。缺点:线程不安全
(3)、使用runtime的api。优点:避免了对象之间赋值相互影响问题。
疑问:在分类中添加的方法以及属性那些,为什么我们在类对象中可以访问到?我们并没有去创建分类对象,怎么去访问到的?下面带着疑问开始重点Category分类的加载过程探索,
三、分类的加载过程
由于整个合并加载过程由runtime去完成,所以我们从runtime的初始化方法开始进行分析,以下是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();
//调用C++静态构造函数
static_init();
//初始化runtime
runtime_init();
//初始化libobjc的异常处理系统。
exception_init();
//初始化缓存
cache_t::init();
_imp_implementationWithBlock_init();
//注册函数通知(镜像的映射,加载,解除映射),在这里主要看map_images的实现
/* _dyld_objc_notify_register*
* map_images:方法回调的地址作为参数,主要是读取处理给定的镜像文件
* load_images:这个方法就是将经过处理的镜像文件进行加载
* unmap_image:处理未映射的镜像
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
从初始化方法中我们看到我们需要的Category的加载过程是在void map_images(unsigned count, const char * const paths[],const struct mach_header * const mhdrs[])
中实现的,所以重点看下这个方法的实现。处理dyld映射到的给定图像
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
方法,我们继续往下查找。
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);
}
...
}
在这个方法里面主要做了四件事情:
1、拿到dyld传过来的header,进行封装
2、初始化selector
3、初始化autorelease pool page
4、读取images
经过一系列的处理,最后开始去读取镜像_read_images
,这个方法内容比较多,先将判断以及循环等条件关系进行折叠,然后我们会发现系统给出了很清晰的注释,按照注释我们逐步去寻找解读我们需要的内容,大概内容如下:

1、在这个方法中第一个条件判断是否是首次加载,在这个判断中主要做了以下几件事情:
(1)、如果镜像文件中包含swift代码,并且该swift框架早于版本swift3.0,那么禁用isa指针。
(2)、如果是Mac OS X系统环境,第一:系统版本是在10.11版本以前的旧版本,那么禁用isa指针,但是这块的代码被注释了,具体原因不太清楚,可能是苹果对以前的旧版本进行了某些兼容,或者不在支持以前的旧版本。第二:如果镜像文件中包含
__DATA
和__objc_rawisa
字段,那么禁用ISA指针
(3)、主要就是这块,在这里将发现的类添加到类的
gdb_objc_realized_classes
表中。
2、修复选择器引用
3、发现类。修复未解决的未来类
4、重新映射类
5、修复旧的objc_msgSend_fixup调用站点
6、发现协议。修复协议引用
7、修复@protocol引用
8、发现类别,在这块代码中有一些注释,如下:
(1)、只有在完成了初始的类别附件后才能这样做。对于在启动时出现的类别,发现延迟到对_dyld_objc_notify_register的调用完成后的第一次load_images调用
(2)、类别发现必须延迟,以避免潜在的竞争,当其他线程调用新的类别代码之前,这个线程完成它的修复
(3)、在
prepare_load_methods
方法中处理所有类别的加载
9、实现非懒惰类
10、实现未来类
到这里map_images
对镜像文件的处理意见完成,那么接下来看下,Category内的属性、方法、协议等是怎么添加到类中的,我们跟随系统断点一直跟到load_images
函数中,前面map_images
这个函数只会走一次,而load_images
函数会走很多次。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
/*
* didInitialAttachCategories启动时出现的类别初始附件是否有
* didCallDyldNotifyRegister对_dyld_objc_notify_register的调用是否已经完成
* 只要进入时didInitialAttachCategories=false didCallDyldNotifyRegister=false,这个方法就不会走
*/
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
// 这里判断镜像有没有+load方法,如果没有直接返回。
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
// 发现加载方法
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
// 调用+加载方法(没有runtimlock - re-entrant)
call_load_methods();
}
通过加断点验证发现,load_images
这个方法确实走了很多次,同时验证了第一个判断,当启动时出现的类别初始附件没有并且_dyld_objc_notify_register
函数调用未完成的时候,是不会走加载所有分类的函数。所以按照流程先跳过这个函数,去看下void prepare_load_methods(const headerType *mhdr)
这个函数的实现,函数如下:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
// 操作前先进行加锁保护
runtimeLock.assertLocked();
// 获取所有类的方法加入到数组中
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
// 获取所有分类的方法加入到数组中
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class 被忽略的弱链接类的类别
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
既然找到了分类加载到数组中的方法,那么继续查看是如何进行处理的,继续查看add_category_to_loadable_list
函数的实现,如下:
/***********************************************************************
* add_category_to_loadable_list
* 添加类别到可加载列表
* Category cat's parent class exists and the category has been attached to its class. Schedule this category for +load after its parent class becomes connected and has its own +load method called.
* 类别cat的父类存在,并且类别已经被附加到它的类上。在其父类连接并调用自己的+load方法后,为+load调度这个类别。
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
// 获取分类的load方法
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
// 如果分类中没有load方法,那么直接返回
if (!method) return;
/**
* PrintLoading:记录对class和category +load方法的调用
*/
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
/**
* loadable_categories_allocated:需要+load调用的类别列表(挂起的父类+load)
*/
if (loadable_categories_used == loadable_categories_allocated) {
// 需要扩展的空间大小计算
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
// 开辟内存空间
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
// 绑定分类和分类的load方法关系,保存到数中
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
在这个函数中主要进行了一下几个操作:
(1)、从class_ro_t中获取load方法
(2)、如果内存已满,进行扩容,申请现有内存的2倍+16字节大小的空间
(3)、保存分类的分类和方法到数组中
然后调用call_load_methods
方法加载类以及分类的load方法到数组中,函数实现如下:
***********************************************************************
* call_load_methods
*调用所有挂起的类和类别+加载方法。
*类+加载方法被称为超类优先。
在父类的+load之后才会调用Category +load方法。
*这个方法必须是可重入的,因为+load会触发更多的图像映射。此外,在面对重入调用时,必须保持超类优先顺序。因此,只有该函数的最外层调用才会执行任何操作,而该调用将处理所有可加载类,甚至是在它运行时生成的类。
*下面的序列在+load加载图像时保持+load顺序,并确保no +load方法被遗忘,因为它是在+load调用时添加的。
*序列:
* 1。反复调用class +load,直到没有其他的类为止
* 2。调用category +load一次。
* 3。如果:
* (a)有更多的类需要加载,或
* (b)有一些潜在的类别+负载,仍然没有尝试。
* Category +load只运行一次,以确保“父类优先”的排序,即使一个Category +load触发了一个新的可加载类和一个新的可加载类别附加到这个类。
* Locking: loadMethodLock必须被调用者持有
*所有其他锁都不能被持有。
**********************************************************************/
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// 重入调用不做任何事情;最外层的调用将完成任务。
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. 反复调用class +load,直到没有其他的类为止
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. 调用category +load一次
more_categories = call_category_loads();
// 3. 如果有类或更多未尝试的类别,则运行更多+load
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
在这个函数中,主要进行了以下操作:
(1)、创建一个自动释放池
(2)、循环调用class的load方法,直到没有其他的类为止
(3)、调用Category的load方法,这个只调用一次,因为要保证父类优先的排序
(4)、如果有类或更多未尝试的类别,则运行更多+load
(5)、从函数实现可以看到,为什么分类load方法加载要比宿主类的load方法要晚一些的原因,是因为先执行while循环,加载宿主类的load方法,然后再去加载分类的laod方法
(6)、销毁自动释放池
当所有类以及分类的load方法加载完成后,开始执行load_categories_nolock
方法,下面看下这个函数的实现:
static void load_categories_nolock(header_info *hi) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
size_t count;
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, hi};
if (!cls) {
// 类别的目标类不见了(可能是弱链接)。
// 忽略的类别。
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// 这个类别的过程。
if (cls->isStubClass()) {
// 存根类永远不会实现。存根类在初始化之前不知道它们的元类,所以我们必须向存根本身添加带有类方法或属性的类别。methodizeclass()会找到它们并将它们适当添加到元类中。
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
// 从分类添加方法到宿主类
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
// 首先,将类别注册到它的目标类。然后,如果实现了类,重新构建类的方法列表(等等)。
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};
processCatlist(hi->catlist(&count));
processCatlist(hi->catlist2(&count));
}
在这个函数中主要进行以下操作:
1、遍历获取分类以及宿主类
2、在这个方法中首先去判断类的宿主类是否实现,或许会问为什么会有类是否实现这一说呢?因为无论是类还是分类都是有懒加载和非懒加载模式两种,在这里就是判断宿主类是否是懒加载模式,有以下几种情况以及处理方式:(1)、类懒加载+分类懒加载
当宿主类与分类都没有实现load方法时,那么这个类都不会加载,在消息第一次调用时进行数据加载
(2)、类懒加载+分类非懒加载
当宿主类没有实现load方法而分类实现load方法时,分类会迫使宿主类从懒加载变成非懒加载模式来提前加载数据
(3)、类非懒加载+分类懒加载
当宿主类实现了load方法而分类没有实现load方法是,当前分类会变成非懒加载模式加载数据
(4)、类非懒加载+分类非懒加载
宿主类与分类都在运行时加载,即提前加载
3、从判断可以看出如果已经实现类,直接将分类的方法等添加到宿主类中,如果没有实现,先将分类注册到宿主类,然后进行方法添加到宿主类
网友评论