Category与Extension

在Objective-C中,要扩展一个类的方法,首先想到的应该是继承,这是面向对象语言的一个特性。继承可以很方便的增加方法,属性等,同时还可以覆写父类的方法。但是,对于大型而复杂的类,继承会导致维护困难。这时Category就可以发挥作用了。

什么是Category

Category是Objective-C 2.0之后添加的语言特性,其主要作用是为已经存在的类添加方法。通过它,你可以

  • 把代码分散到多个类中-比如把类中的不同模块的方法放入几个不同的category中。
  • 申明私有方法。
  • 模拟多继承。
  • 公开framework的私有方法。

Category的使用注意

因为谁都可以扩展一个类,所以在使用Category时有几点需要注意的地方。

比如你的应用扩展了NSString类,同时你链接的第三方库也扩展了NSString类。刚好两者扩展了一个相同的方法名。由于Category是在runtime时实现的,这是加载哪个实现是不确定的,从而会导致不确定的结果。

又比如你扩展了NSSortDescriptor类,增加了一个sortDescriptorWithKey:ascending:方法。在低版本的iOS中这个方法是不存在的,但是高版本的iOS中,这个方法是默认被实现的。这时就会有一个命名冲突。

因此为了避免上述的情况,建议是在扩展类的方法名前加入前缀。比如:

@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end

Category与Extension的区别

Extension像一个匿名的Category,但是两者差别很大。Extension只能附加在源码的类上面,它是编译时决定的,从而可以添加实例变量。而Category是在运行时决定的,内存布局已经确定,从而不可以添加实例变量。Extension常用来隐藏实现细节,比如不想对外公开的方法和实例变量等。

Category源码实现

通过查看runtime源码,发现category实际上是一个叫做category_t的结构体

typedef 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;
}

从中我们可以发现,它可以添加实例方法、类方法、协议、实例属性。
接下去我们看一下category是如何加载的。

void _objc_init(void) {
  ...
  dyld_register_image_state_change_handler(dyld_image_state_bound,
                                     1/*batch*/, &map_2_images);
  ...
}

首先通过runtime的入口函数_objc_init方法中加载map_2_images

const char * map_2_images(enum dyld_image_states state, uint32_t infoCount,
         const struct dyld_image_info infoList[])
{
    rwlock_writer_t lock(runtimeLock);
    return map_images_nolock(state, infoCount, infoList);
}

然后map_2_images通过加锁访问map_images_nolock

const char *map_images_nolock(enum dyld_image_states state, uint32_t infoCount,
            const struct dyld_image_info infoList[])
{
  ...
  _read_images(hList, hCount);
  ...
}

在这里通过_read_images去读取image。

void _read_images(header_info **hList, uint32_t hCount)
{
  ...
  // Discover categories.
  for (EACH_HEADER) {
    category_t **catlist =
      _getObjc2CategoryList(hi, &count);
      for (i = 0; i < count; i++) {
        category_t *cat = catlist[i];
        Class cls = remapClass(cat->cls);
        ...
        // 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 (cat->classMethods  ||  cat->protocols  
          /* ||  cat->classProperties */)
        {
          addUnattachedCategoryForClass(cat, cls->ISA(), hi);
          if (cls->ISA()->isRealized()) {
              remethodizeClass(cls->ISA());
          }
          ...
        }
      }
    }
    ...
}

上述代码执行的操作就是找到相应的category,分别把category的实例方法、协议、属性加入到类上,类方法、协议添加到元类上面。首先注册category到目标类上去,然后如果类或则元类已经实现,则重构它的方法列表。
具体的执行是addUnattachedCategoryForClassremethodizeClass方法。

static void addUnattachedCategoryForClass(category_t *cat, Class cls,
                                          header_info *catHeader)
{
    runtimeLock.assertWriting();

    // DO NOT use cat->cls! cls may be cat->cls->isa instead
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}

addUnattachedCategoryForClass方法把类和category做了一个关联映射。

static void remethodizeClass(Class cls)
{
...
attachCategories(cls, cats, true /*flush caches*/);        
...
}

remethodizeClass方法内部其实调用了attachCategories方法。attachCategories方法是真正把category里面的东西加入到类中去。

static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
...
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);
    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);
}

从中我们可以看到,这里把category中的方法、属性、协议添加到原有的类上面。

这里说明一下,category中的方法并不会覆盖原有的方法,如果存在两个相同的方法。但是由于category中的方法是放在前面的,所以在消息转发查找方法时会先找到category的方法,从而形成了覆盖原有方法的错觉。

Category与+load()

runtime在加载类和分类时,是通过调用各自的指针分开加载的,因此既会执行类的+load()方法,也会执行分类的+load()方法。但是当我们手动调用+load()方法时,则分类的+load()方法会先于类的+load()方法,并造成覆盖的错觉。测试代码如下:

#import "Person.h"

@implementation Person

+ (void)load {
  NSLog(@"main load");
}

@end

@implementation Person (Fly)

+ (void)load {
  NSLog(@"category load");
}

- (void)fly {
  NSLog(@"I can fly");
}

- (void)jump {
  NSLog(@"I can jump");
}

@end


#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [Person load];
}

@end

结果如下:

2018-01-30 17:02:44.442283+0800 CategoryDemo[23354:4396068] main load
2018-01-30 17:02:44.442758+0800 CategoryDemo[23354:4396068] category load
2018-01-30 17:02:44.599671+0800 CategoryDemo[23354:4396068] category load

从结果中可知先调用了类的+load()方法,再调用了分类的+load()方法。最后主动调用时,调用了分类的+load()方法。

Category与实例变量

从源码中可知,category是无法添加实例变量的。但是有时往往需要实例变量,这时可以通过runtime关联对象做一个假的实例变量。

-------------------------------
@interface Person (Fly)

@property(nonatomic,copy) NSString *name;

@end


-------------------------------
static NSString *associateKey = @"name";

@implementation Person (Fly)


- (void)setName:(NSString *)name {
  objc_setAssociatedObject(self, &associateKey, name, OBJC_ASSOCIATION_COPY);
}

- (NSString *)name {
  return objc_getAssociatedObject(self, &associateKey);
}

@end

然后我们分析一下objc_setAssociatedObject的源码。

void objc_setAssociatedObject(id object, const void *key, id value,
                     objc_AssociationPolicy policy)
{
  ObjcAssociation old_association(0, nil);
  id new_value = value ? acquireValue(value, policy) : nil;
  {
      AssociationsManager manager;
      AssociationsHashMap &associations(manager.associations());
      disguised_ptr_t disguised_object = DISGUISE(object);
      if (new_value) {
        ...
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        ObjectAssociationMap *refs = i->second;
        ObjectAssociationMap::iterator j = refs->find(key);
        (*refs)[key] = ObjcAssociation(policy, new_value);
        ...
      }
      ...
  }
  ...   
}

从中可以发现,这个associateObject是由AssociationsManager管理的,AssociationsManager里面有一个AssociationsHashMap的哈希表,用来存储所有的object,其key值为这个objcet的地址。

参考

1.Category
2.Customizing Existing Classes
3.Objective-C Category 的实现原理

4.深入理解Objective-C:Category

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

推荐阅读更多精彩内容