iOS里的Category

前言

关于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中是一个重要的概念,我们在声明属性的时候,其实系统默认会帮我们生成gettersetter方法,并生产对应的成员变量(一般为_propertyName)。

我们看上面的结构体能看到Categor里面是包含有属性的列表,所以是可以存储属性的,但是它没有成员变量列表,所以创建的这个属性并没有对应的成员变量。

而且编译器也不会给Category自动生成gettersetter方法。但是结构体中是有实例方法列表的(instanceMethods),所以 gettersetter可以自己写。

那现在如果要实现属性的功能就差一个成员变量了,这里我们可以用关联对象(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_setAssociatedObjectobjc_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中调回主类的同名原方法

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,848评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,529评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,824评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,329评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,227评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,879评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,218评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,877评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,159评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,155评论 2 315
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,987评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,736评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,273评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,407评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,663评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,158评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,517评论 2 339

推荐阅读更多精彩内容