前言
关于Category的详细介绍推荐阅读深入理解Objective-C:Category,这篇文章是我个人感觉对Category的解释说的最清楚的一篇。以下关于源码部分的解读都是参考自这篇文章,只是这篇文章比较老了,里面的源码部分苹果也做了更新,所以个人做了一点稍微的修改和整理。但我个人感觉已经没有什么好多说的了,下面也只是做为个人笔记的一个整理,不推荐阅读。
介绍
Category其实是OC里面一个很重要的特性,几乎在我们日常的开发中随处可见。它可以扩展我们已有类的方法,我们在开发中最常用的就是扩展一些系统类的方法。比如这里有一份我早几年收集的一些Category:DSCategories,都是一些对系统类的扩展,当然这个库我早已没维护了,这里只是拿出来做一个简单的示例。
当然扩展已有类的方法是Category的一个基本应用,它还有一些其他的应用,比如减少单个文件的体积,把不同的功能组织到不同的category中等。
所以如果只是使用Category的话其实很简单,没有什么好说的,很多初学者刚开始应该就是接触的Category,也基本上马上就会使用了。因此我们本篇文章主要讲解一下Category的一些底层原理和问题。
Category的优缺点:
优点:改动小,耦合性小,仅对本category有效,不会影响其他类与原有类的关系。
缺点:分类里的方法跟原有类的方法相同,同一个类的不同分类里面有方法冲突,这些都会发生一些奇怪的问题,互相覆盖之类的。类别里的方法优先级高于原有类的方法。
抛出问题
在开始了解底层之前,我们先抛出几个问题,然后通过底层源码来对这几个问题进行解读。
- Category可以添加属性吗?
- Category是如何附加到主类上面的?
- Category的方法和主类上面的方法同名了会先调用哪个?
Category的结构
我们在Runtime源码地址里下载最新的Runtime源码objc4-723.tar.gz
。
然后我们在objc-runtime-new.h
文件中看到Category
的结构体如下定义:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *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);
};
- name是指class_name而不是category_name
- cls是要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
- instanceMethods表示实例方法列表
- classMethods表示类方法列表
- protocols表示实现的所有协议的列表
- instanceProperties表示Category里所有的properties。
给Category添加属性
这里我们回答上面抛出的第一个问题:Category可以添加属性吗?答案是不可以,但是可以利用关联对象来实现属性的功能。
我们先来确定一下什么是属性,属性在Objective-C中是一个重要的概念,我们在声明属性的时候,其实系统默认会帮我们生成getter
和setter
方法,并生产对应的成员变量(一般为_propertyName)。
我们看上面的结构体能看到Categor
里面是包含有属性的列表,所以是可以存储属性的,但是它没有成员变量列表,所以创建的这个属性并没有对应的成员变量。
而且编译器也不会给Category
自动生成getter
和setter
方法。但是结构体中是有实例方法列表的(instanceMethods
),所以 getter
和setter
可以自己写。
那现在如果要实现属性的功能就差一个成员变量了,这里我们可以用关联对象(Associated Objects
)来实现成员变量。
Runtime Associate
Runtime Associate
其实就算用来Category
关联对象用的。它有如下几个对应的方法:
objc_setAssociatedObject
objc_getAssociatedObject
objc_removeAssociatedObjects
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
我们发现上面这两个方法都需要一个key,这个key其实就是一个唯一常量,用来标识对应的关联对象用的。
我们在Category
中添加关联对象使用Key一般有如下几种:
- 第一种:固定地址
static char studentNameKey;
@implementation NSObject (Student)
- (NSString *)name{
return objc_getAssociatedObject(self, &studentNameKey);
}
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, &studentNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- 第二种:固定地址
static const void *studentNameKey = &studentNameKey;
@implementation NSObject (Student)
- (NSString *)name{
return objc_getAssociatedObject(self, studentNameKey);
}
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, studentNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- 第三种:使用getter的方法名作为key
@implementation NSObject (Student)
- (NSString *)name{
return objc_getAssociatedObject(self, @selector(name));
}
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
说完了objc_setAssociatedObject
和objc_getAssociatedObject
方法,那objc_removeAssociatedObjects
又是用来干嘛的呢?其实我们一看名字就知道是用来移除关联对象用的。这个方法一般我们不会直接去使用它,都是系统在对象释放的时候调用的。
那关联对象是什么时候被释放的呢?
我们先来看看对象的销毁流程:
- 调用objc_release
- 因为对象的引用计数为0,所以执行dealloc
- 在dealloc中,调用了_objc_rootDealloc函数
- 在_objc_rootDealloc中,调用了object_dispose函数
- 调用objc_destructInstance
- 最后调用clearDeallocating
在runtime
源码的objc-runtime-new.mm
文件中我们看到objc_destructInstance
的定义如下,可以看到里面有个_object_remove_assocations
执行了移除关联对象的操作:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}
return obj;
}
个人认为这个关联对象有点像类的weak对象,他们都会有一个专门的hash表来维护,当对象释放的时候,系统通过对应的方法找到hash表并一一清除。
Category如何加载
对于OC运行时,入口方法如下(在objc-os.mm文件中):
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);
}
category被附加到类上面是在map_images的时候发生的,在new-ABI的标准下,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
首先,我们拿到的catlist就是编译器为我们准备的category_t
数组,关于是如何加载catlist本身的,我们暂且不表,这和category本身的关系也不大,有兴趣的同学可以去研究以下Apple的二进制格式和load机制。
略去PrintConnecting
这个用于log的东西,这段代码很容易理解:
1)、把category的实例方法、协议以及属性添加到类上
2)、把category的类方法和协议添加到类的metaclass上
值得注意的是,在代码中有一小段注释 /* || cat->classProperties */,看来苹果有过给类添加属性的计划啊。
我们接着往里看,category的各种列表是怎么最终添加到类上的,就拿实例方法列表来说吧:
在上述的代码片段里,addUnattachedCategoryForClass
把类和category做一个关联映射,remethodizeClass
去处理添加Category
。
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
最后attachCategories
这里真正的处理Categories的方法、属性、协议到类上面:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
我们这里能看到方法是从cats
数组中倒序取出方法添加到方法数组中的,最后得到一个Category所有方法的数组,然后再调用prepareMethodLists
函数把得到的Category
方法数组插入到类的方法列表前面。
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
runtimeLock.assertWriting();
if (addedCount == 0) return;
// Don't scan redundantly
bool scanForCustomRR = !cls->hasCustomRR();
bool scanForCustomAWZ = !cls->hasCustomAWZ();
// There exist RR/AWZ special cases for some class's base methods.
// But this code should never need to scan base methods for RR/AWZ:
// default RR/AWZ cannot be set before setInitialized().
// Therefore we need not handle any special cases here.
if (baseMethods) {
assert(!scanForCustomRR && !scanForCustomAWZ);
}
// Add method lists to array.
// Reallocate un-fixed method lists.
// The new methods are PREPENDED to the method list array.
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[i];
assert(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
// Scan for method implementations tracked by the class's flags
if (scanForCustomRR && methodListImplementsRR(mlist)) {
cls->setHasCustomRR();
scanForCustomRR = false;
}
if (scanForCustomAWZ && methodListImplementsAWZ(mlist)) {
cls->setHasCustomAWZ();
scanForCustomAWZ = false;
}
}
}
所以最后总结Category和主类方法的执行顺序如下:
编译的时候系统应该是把类对应的所有
category
方法都找到并前序添加到method list
中,也就是说后编译的category
的方法在method list
的最前面。比如先编译的category1
的方法列表为d,后编译的方法列表为c。那么插入之后的方法列表将会是c,d。最后把这个分类的
method list
前序添加到类的method list
中,如果原来类的方法列表是a,b,Category
的方法列表是c,d。那么插入之后的方法列表将会是c,d,a,b。所有说覆盖方法的优先级是:后编译的Category
的方法>先编译的Category
方法>类的方法。注意:+(void)load;方法的执行顺序是先类,然后是先编译的
Category
,最后是后编译的Category
。
示例
@implementation Person
- (void)myName{
NSLog(@"person");
}
@end
#import "Person+Chinese.h"
@implementation Person (Chinese)
- (void)myName{
NSLog(@"person chinese");
}
@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
Class currentClass = [Person class];
Person *my = [[Person alloc] init];
if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList([Person class], &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
encoding:NSUTF8StringEncoding];
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
typedef void (*fn)(id,SEL);
fn f = (fn)lastImp;
f(my,lastSel);
NSLog(@"%@ %p %p",methodName, lastSel ,lastImp);
}
free(methodList);
}
return YES;
}
输出结果(可以看到分类的方法先执行):
2018-09-14 21:01:10.992143+0800 test[81215:5981234] person chinese
2018-09-14 21:01:10.992302+0800 test[81215:5981234] myName 0x10d7b0a34 0x10d7b0460
2018-09-14 21:01:10.992732+0800 test[81215:5981234] person
2018-09-14 21:01:10.992986+0800 test[81215:5981234] myName 0x10d7b0a34 0x10d7b0520
参考
深入理解Objective-C:Category
objc - Category中调回主类的同名原方法