Category深入解读

引入

众所周知,面向对象的编程语言的基础是类,方法是依赖于类存在的,类是结构和功能的基本单位。编程的主要方面就是增加新功能,也就是新增类和扩展原有的类型。扩展原有类型往往需要通过继承来实现,而每一次的继承都会带来名字和类型的增加以及继承体系的改变,这对创建方和使用方都不太友好。

Category是Objective-C特有的一种语言特性和技术,其最核心的作用是在不改变原有的继承体系的基础上,为类扩展新功能,简化扩展的难度,Objective-C的头文件声明和实现分离机制,可以隔离无关类,形成组合,结合load加载,方法覆盖等特点可以衍生出多种多样的玩法。它不但可以用作功能扩展,还可以用于架构设计,因此Category技术也是我个人最喜欢的技术之一。

常见的用法

  1. Category技术的最主要用法是在不改变类的继承体系的基础上为类添加扩展方法,原有的类甚至不需要知道Category的存在,一切由编译器掌控。
  2. 声明和实现属性,是使用动态时方法关联变量,在使用方表现为对象添加了新的数据成员。
  3. 拆分类的功能。Objective-C声明和实现文件分离可以衍生出各种玩法的。a) 将类中相似的功能分散到多个Category中实现,可以将类拆分为更小的个体。b) 将类的功能分散到多个Category头文件里声明,可以实现访问隔离。c) 声明和实现都分散在各个Category中,彼此之间互不感知,感官上类似于组件设计。
  4. 非正式协议。有了正式协议以后,已很少使用。

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

注:我这里用的Mac下objc4-706的源码。

  1. 关于name我这多聊几句。OC中几乎大部分的关键数据结构体都有name字段,比如Class,Method,Property,Protocol,而名字到代码实现体的映射是OC动态化的关键,比如Runtime就是通过Message(SEL结构体)去调用函数的,而其关键之一就是name字符串。这是嘛意思呢?举个例子说明,NSSClassFromString,可以得到一个类对象,然后可以调用new产生一个实例对象。OC中,类对象的信息(就是类记录了多少变量,各个变量大小,方法列表,属性列表)是会被加载到内存作为单例,这和C++这类纯编译语言不一样。所以你定义的类越多,就越占内存,启动越慢。集成OC的SDK和C/C++的SDK不同,其不会按需要集成相应的.o文件,而是全部一起集成,导致ipa包很大。
  2. classref_t cls,注释中描述其实未映射的class_t。嘛意思呢?就是可能还没有添加到一个全局的NXMapTable的class,需要调用remapClass来添加。从源码看,其可以直接强转为Class来看,应该是同一个东西或者至少类似。(后面我们再结合代码给出具体的分析)
// classref_t is unremapped class_t*
typedef struct classref * classref_t;
  1. instanceMethods,classMethods,Category中可以定义对象方法和类方法,不用多说。

  2. protocols,这个可能有些同学不知道或者没有思考过这个问题,Category也是可以比较规范的实现正式协议。这个特性可以在设计上玩点花样,比如:重构时为ViewController创建分类去实现UITableView,UITextView的协议拆分代码,在设计上拆分功能,这比创建新类要容易很多。

  3. instanceProperties,实例对象属性。苹果认为property=setter+getter+var,但这里就有言不符实了。Category声明的这类属性,编译器都不自动生成相应的var。即使没有var,提供setter+getter也能编译甚至可以使用别的var。当然这只是个小问题,don't care。

  4. _classProperties,类对象属性。相当于一个类域下的全局变量,这货是Xcode8新增的特性,感觉用处不是很大。就是可以通过

    @property (class, strong) NSArray *array;
    

    来声明一个类对象属性,但编译器不会自动合成。需要开发人员自己提供setter,getter和static的数据对象存储。

Category如何加载

众所周知,Category是iOS的runtime的一部分 ,而runtime等二进制库(mach-o文件)是需要通过dyld(the dynamic link editer,代码是开源的,有兴趣可以去研究)来加载的。具体我们慢慢来看

objc_init之前

简单说一下,首先是dyld调用,然后通过initializeMainExecutable,将主程序加载。接着通过ImageLoader(苹果用于辅助加载mach-o文件格式的类)生成各种image对象。接下来调用libSystem_initializer -> libdispatch_init -> _os_object_init -> ::objc_init。这其中让我不解的是为什么是由libdispatch_init去调起,我们去查了一下libdispatch的源码,确实有libdispatch_init方法,但是其中没有类似的调用,应该是开源的时候苹果删除相关调用。


objc_init被调过程
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();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
}

从前三行定义initialized并用以判断是否被加载,可以了解这个init有是不能并发的,image加载过程是否可以并发还有待商榷。

中间调用了5个init的方法,初始化相关的数据,这里就不具体说明了。

最后向dyld注册了map_2_images, load_images, unmap_image三个函数,其将会在dyld初始化过程中分别多次调用。

先说map_2_images,其调用了map_images_nolock,该函数调用addHeader尝试读取每一个image文件的是否包含Objc的segment header信息,如果有统计Objc Class的数量。

这里额外讲一下addHeader,其作用就是构建header_info,其记录了该image的mach_header和获取"__objc_imageinfo"段的信息组成header_info。

使用otool debug-objc -h -V 输出这个开源objc4-706的编译出的目标文件mach_header_64

      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL LIB64     EXECUTE    19       2752   NOUNDEFS DYLDLINK TWOLEVEL PIE

可以看到这些信息,也不难理解。如果想了解的具体些请查看https://github.com/gdbinit/MachOView/blob/master/mach-o/loader.h

可以看到ncmds有19个load command(各种不同类型command也可以查看上面的loader),使用otool -l debug-objc会列出具体的19个load command和section。

例如之前addHeader中获取的"__objc_imageinfo"

Section
  sectname __objc_imageinfo
   segname __DATA
      addr 0x00000001000020c8
      size 0x0000000000000008
    offset 8392
     align 2^2 (4)
    reloff 0
    nreloc 0
     flags 0x00000000
 reserved1 0
 reserved2 0

当然还有其他很多section,比如__cstring, __objc_classname, __cfstring, __objc_classlist, __objc_catlist等等,而__objc_catlist就是Category加载信息的列表,后面我们将有所涉及。

如果有Objc的Image需要加载,则调用_read_images,其源码如下(省略部分)

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) 
{
    
    #define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++
    
    // Discover classes. Fix up unresolved future classes. Mark bundle classes.

    for (EACH_HEADER) {
        if (! mustReadClasses(hi)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->isPreoptimized();

        classref_t *classlist = _getObjc2ClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            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;
            }
        }
    }
    
    ...

    // 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);
                }
            }
        }
    }
}

首先看第一部分,苹果注释为Discover classes,即类的加载,简单说一下。

我们直接看代码_getObjc2ClassList,追踪一下,发现其获取的是二进制代码section的“__objc_classlist”,这个具体是什么后面我们再分析。获取到classlist,则循环调用readClass(将编译器编译的class信息都读取出来)并拷贝出来一个新的class,放入全局的Map容器中,并释放原来的class。将name->class的映射放入另一个全局的Map容器中,通过这个容器可以完成NSClassFromNSString这样的操作,这种做法我称之为数据符号化。

接下来是我们的重点内容 Discover categories。

获取编译的Category二进制信息

for循环每一个image,调用_getObjc2CategoryList,获取“__objc_catlist” section的所有Category。

至于“__objc_catlist”是个啥,内容是啥,我们通过下面的汇编代码来看。
原始代码:

@interface User(Add)
- (void)add;
@end

@implementation User(Add)
- (void)add {}
@end

汇编后部分源码(也可以clang -rewrite-objc编译成C++源文件去解读更直观,我这里使用汇编,Xcode生成汇编方便快捷,也更贴近实际些)
(以"."开头的都是汇编伪指令,指导汇编器工作的,不会出现到目标代码中,但会对目标代码造成影响。绝大部分伪指令都已"."开头,但也有例外比如ldr。)

    .section    __DATA,__objc_const
    .p2align    3               ## @"\01l_OBJC_$_CATEGORY_INSTANCE_METHODS_User_$_Add"
l_OBJC_$_CATEGORY_INSTANCE_METHODS_User_$_Add:
    .long   24                      ## 0x18
    .long   1                       ## 0x1
    .quad   L_OBJC_METH_VAR_NAME_.11  ##add
    .quad   L_OBJC_METH_VAR_TYPE_   ##"v16@0:8"
    .quad   "-[User(Add) add]"
    
    
    .p2align    3               ## @"\01l_OBJC_$_CATEGORY_User_$_Add"
l_OBJC_$_CATEGORY_User_$_Add:
    .quad   L_OBJC_CLASS_NAME_.10 ##Add
    .quad   _OBJC_CLASS_$_User
    .quad   l_OBJC_$_CATEGORY_INSTANCE_METHODS_User_$_Add
    .quad   0
    .quad   0
    .quad   0
    .quad   0
    .long   64                      ## 0x40
    .space  4

    ...
    
    .section    __DATA,__objc_catlist,regular,no_dead_strip
    .p2align    3               ## @"OBJC_LABEL_CATEGORY_$"
L_OBJC_LABEL_CATEGORY_$:
    .quad   l_OBJC_$_CATEGORY_User_$_Add
    
    ...

    .section    __DATA,__objc_data
    .globl  _OBJC_CLASS_$_User      ## @"OBJC_CLASS_$_User"
    .p2align    3
_OBJC_CLASS_$_User:
    .quad   _OBJC_METACLASS_$_User
    .quad   _OBJC_CLASS_$_NSObject
    .quad   __objc_empty_cache
    .quad   0
    .quad   l_OBJC_CLASS_RO_$_User

找到中间的.section __DATA,__objc_catlist,regular,no_dead_strip,这就是该section。

我们来解读一下这些汇编是啥意思。

.p2align 3的意思是指针按3bit对齐,OC对象的指针后3个bit表示其他意思,跟寻址没有关系,寻址时都是补充0来对齐。

L_OBJC_LABEL_CATEGORY_$标签,便于汇编标记引用和跳转。

.quad 是big data,这里就是2个words,4个16进制单位,即64bit。这里引用了标签:l_OBJC_$_CATEGORY_User_$_Add,就是Category的具体定义部分。

大伙看得可能比较晕,我把Category的c++定义贴出来,对照这看,发现除了.long 64.space 4这两项没有外,其他的都是一样的(只定义了实例方法,其他的字段都是0),目前我也不了解这两项有啥用,但有没有这两项不影响,可能是编译器自用字段。

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

然后再说其指向的类_OBJC_CLASS_$_User,其前面被标记为.global,它表示对链接器可见,通俗讲就认为类似于是全局变量。

其c++定义为classref_t,至于classref_t是个啥,前面也讲过,这里再具体说一下,我们注意到这个函数

static Class remapClass(classref_t cls)
{
    return remapClass((Class)cls);
}

其将classref_t对象指针直接强转为Class(那么也就是说两者在内存布局上是一样的或者后者是前者的子集)Class我们就很熟悉了,这里再列举一下。

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits; 
}

其中前面三项isa,superclass,cache都类似,但是第四项不一样,不要着急,我们再看cache_t

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;//4Byte
    mask_t _occupied;//Byte
}

发现其占64 * 2bit,所以汇编中的.quad 0是对应mask_t。

.quad l_OBJC_CLASS_RO_$_User 对应 class_data_bits_t bits,对照下,发现两者内存布局是完全一致的。

接下来再看l_OBJC_$_CATEGORY_INSTANCE_METHODS_User_$_Add,其对应的是method_list_t,简单说一下。

.long 24是存储实体大小和标记位的,.long 1是方法的个数。接下来是方法名和签名,最后.quad "-[User(Add) add]"这个标记的是字符串,可以在汇编代码中搜索会找到一个"-[User(Add) add]:标签,即是对应的IMP入口地址。

加载到关联Map

获取了catlist之后,就是加载其内容了。如果有实例方法,实例属性或者协议则调用addUnattachedCategoryForClass

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

从category map获取对应class下的category_list,以下是对应的定义:

typedef locstamped_category_list_t category_list;

struct locstamped_category_list_t {
    uint32_t count;
#if __LP64__
    uint32_t reserved;
#endif
    locstamped_category_t list[0];
};

struct locstamped_category_t {
    category_t *cat;
    struct header_info *hi;
};

如果没有list,则calloc(sizeof(*list) + sizeof(list->list[0]), 1);分配了1个8+16bit的空间,sizeof(*list)对应的是一个或两个uint32_t,这里用了一个小技巧,直接利用了指针大小在32位和64位下不同的长度来简化代码。否则realloc,多分配一个locstamped_category_t空间。

这里locstamped_category_t list[0]的定义比较有意思,也是一种常见的处理小技巧,就是定义的时候用数组去定义,创建的时候用malloc去分配空间,使用的时候可以用下标去访问数据,既能当数组用也能当指针用。

这句list->list[list->count++] = (locstamped_category_t){cat, catHeader}之所以可以直接通过{cat, catHeader}构造结构体赋值,是因为cat和catHeader是常量级的,所以可以直接赋值。将category放到未处理的Map容器中,这里的列表将记录所有的category_t。

再检查是否有类方法,类属性,类协议,并做同样的处理。

加载Category

检查Class是否实例化,如果是则调用remethodizeClass,取出该Class对应的所有Category,将方法,属性,协议等加载到该Class。

注:有两个条件去保证Category是在Class加载完成后才加载,一个是在_read_images函数中,是优先加载Class的,一个是加载Category之前会先检查Class是否实例化。

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

这部分代码比较容易理解,前面根据该Class的Category数量分配三个2级指针列表,然后把这些列表加到Class相关的列表中去。方法,协议,属性都是二维数组。

这里有一个有必须要说明的地方

method_array_t,property_array_t,protocol_array_t都是泛型类list_array_tt的一种具体形式。这个list_array_tt有一个存储list的数组,其中list是method_list_t这种。list_array_tt定义了一个attachLists的函数,其实现如下:

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            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;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

我们需要关注的是if中的情况,当数组扩展时,调用realloc重新分配空间,然后调用memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));将以前的数据放在了数组的array()->lists + addedCount的位置,而将新数据放在了数组前面的位置。由此可见苹果是有意将Category的方法,属性,协议放在了数组的前面。这也即是为什么Category的方法比Class方法优先调用的原因。

load_image

前面是处理Category的加载,这里是处理+load的调用。load_image每次只处理一个image中所有的class和category load的调用。

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **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
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

load_images中有prepare_load_methods和call_load_methods两个函数。

前者将所有的class load和category load放入各自的数组中,后者调用。

prepare_load_methods会获取image中所有非懒加载的类,再调用schedule_class_load,其会递归调用父类的load方法,将之放入全局的loadable_classes数组中,所以父类的load方法会在数组的前面。获取所有非懒加载的category,并获取load方法放入全局loadable_categories数组中,因此先加载的category会优先调用。

call_load_methods函数将优先调用loadable_classes中的load,然后再调用loadable_categories的load方法。所以父类load优先于子类load,优先其对应的category load需要留意的是,这里使用了autoreleasepool包裹,所有load方法中如果不处理大量临时对象,可以不手动添加autoreleasepool。

从load_image可知,对于同一个class的category的load调用顺序与image的加载顺序有关,image的加载顺序和编译器打包顺序有关,也就是Xcode工程配置文件的Build Phrases标签的Compile Source中的文件顺序。这里就偷个懒,就不贴图验证了。而category覆盖的其他方法则是后编译的被调用,先编译的就被忽略了,这点需要注意一下。

Category属性和关联对象

这是Category一种比较常见的用法。我们知道Category中是可以声明属性的,属性=getter+setter+var。
我一般认为OC是个半动态的语言,为什么这么讲呢?动态是因为method是按名字映射访问的,可以动态修改,苹果设计了message来实现,但是对于成员变量的访问却不是,依旧是按照C++的指针+偏移量的方式,也就是说对于成员变量的地址访问是硬编码的,这就导致了成员变量无法动态添加。当然之所以不在实例中加成员变量应该是实现比较复杂,这种需求不多而且可以被关联对象取代。Class被定义以后,其对应的实例内存布局就已经确定了,如果再动态添加变量,会导致指针偏移出现问题 ,虽然可以引入修复机制,但这样的话category的使用与否将深度影响实例对象的解析,Class和Category将高度依赖,很容易导致非法访问,而且Category灵活组合的特性也就被削弱了,所以Category通过关联对象去解决这个问题确实是一种兼顾内存和性能的简洁办法。

闲话说完正式进入主题。

我们通过objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)来设置关联对象(注意:const void *key的const声明),其内部调用_object_set_associative_reference来设置关联对象,其使用了一个AssociationsManager的对象

class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map; 
}

其中维护了一个全局静态的AssociationsHashMap(其继承自c++ 11标准中的unordered_map,它和之前的map组织的方式完全不一样,map是红黑二叉树,而这货维护了一个__hash_table,依靠散列来索引,不需要排序,绝大多数情况下增删查改效率贼高,缺点是初始内存消耗得多,建立hash表比较耗时,hash值碰撞多了效率会大幅下降),该AssociationsHashMap以对象的地址计算散列,查找关联对象的iterator,其也是一个AssociationsHashMap,它是以void *key的地址hash化后再作键来关联的,这就需要key是最好是const的,否则一不小心很可能两次hash的值就不一样,所以一般使用static地址或者当前SEL来作为key。

因为是对象动态关联,编译器无法预先插入retain和release操作,所以需要开发者标记关联策略,_object_set_associative_reference检查这个标记通过objc_msgSend来手动做引用计数,同时这个关联策略会被记录在map中,以便后续获取和删除时做引用计数。当对象有新关联对象时,会调用setHasAssociatedObjects将其isa指针的倒数第2个bit标记为1,对象dealloc的时会检查该标记以便于remove关联对象。其触发调用顺序如下:

dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose -> objc_destructInstance ->_ object_remove_assocations

Category方法覆盖

我们知道OC message调用方法的机制,就是给函数入口或者说IMP,加了一个映射,相当于中间适配器,叫method,其包含SEL和IMP。也就是说可能会出现SEL:IMP=n:n这种情况,就是一个SEL对应多个或0个IMP,或者多个SEL对应1个或0个IMP。

Category方法覆盖就是一条SEL对应多个IMP,runtime在搜索时只会找到第一个满足条件的method返回并调用。所以如果OC要支持类似于C++函数重载的话,只需要在搜索的时候再加一个匹配条件,即SEL的type字段,其包含了方法的签名,包括返回值,参数等信息。

那么如果方法被覆盖后要怎么调用原来的方法呢?

通过class_copyMethodList获取所有的方法列表,通过名字循环调用sel_getName(method_getName(method))获取SEL,查找满足条件的方法,获取IMP,通过强转函数指针(即模仿objc_msgSend调用)调用即可。

Category和Extension

在Objective中除了在Category中可以声明方法和属性外,在Extension中也可以声明方法和属性,由于其没有名字,所以被认为是一种匿名的Category,但实际上这是不对。我们来梳理一下Extension的区别。

  1. 首先Extension是作用在编译期间的,而Category是运行时动态加载的。
  2. Extension中可以添加成员变量(属性),其和公共@interface和@implementation中定义的变量都会被添加到类的变量列表,以备后来定义对象。
  3. 已编译过的类就不能再添加Extension,但未编译的类可以添加多个Extension,但外部没有@implementation的Extension,属性不会自动合成变量和setter,getter,因为外部Extension没有自己单独的@implementation,需要依赖于主类的实现,而且@implementation只能有一个。但Extension也具有Category声明的功能,类似协议。

正是因为Extension的这些特点,导致其局限性较大,其主要用法就为类添加私有的方法,属性和变量声明或定义,无法像Category衍生出多种用法,或者说Extension被设计出来就是为类的内部服务的,而Category正好相反主要是为其他类服务的。

Category特性

一种技术可以被如何使用,往往与其特性和相关环境的特性有关,这里就聊一聊这些特性,前面聊过就一笔带过了。

  1. category声明和实现完全分离,可以分散在不同的文件里面,category还可以只声明不实现。比如作协议用,就是在A文件声明,而不实现,由使用方去实现。

  2. 功能分组 ,功能扩展,特别是局部功能扩展。比如:UIView中,将category声明放在同一个文件中, 可以很好的对功能分组,程序可读性比写注释好太多了。对UILayoutGuide做UIConstraintBasedLayoutDebuging的扩展,作为一种局部功能可以在使用UIView同时被引入,可以很好的防止被滥用出错。

  3. 组合加载。各个category可以被分散在不同的模块和文件中,彼此之间互不感知,但会被加载器组合到同一个类中,所以其可以作为一个组件化的设计手段。

    编译互不感知,可以实现访问隔离,A引入categoryA,B不知道,则编译时categoryA对B不开放,这对代码的选择性封装十分有利,这也是我比较喜欢利用的一个特性。

  4. category可以覆盖原方法,可以轻易改变主类里面的对应的方法实现,但缺点是可读性很差,建议正常情况下不要使用。(前几天我在引入阿里云实人认证SDK的时候发现其和GrowingIO的SDK冲突,GrowingIO替换了阿里WVWebView的dealloc方法,并在其中使用了weak引用,而在dealloc中使用weak是非法操作,程序直接crash了。无奈产品俩SDK都不愿意放弃,阿里云和GrowingIO的支持人员也靠不住,只能出歪招,退而求其次用category覆盖dealloc方法,虽然会导致内存泄露,但调用次数少,后果还能承受,也就只能如此了。)

    这里衍生出另外一种玩法,前面说过category可以只声明不实现,方法和属性均可。那么就可以通过category声明来开放类的私有方法,比起通过运行时来访问要优雅一些。

  5. category可以实现协议。这个用的人可能不是很多。曾经在苹果官方demo看到过其将VC中TableView的协议实现放到了VC的category中去实现。如果该协议对上下文依赖较少,分离到category中实现确实不错,但如果依赖较多,特别是依赖数据时就要注意了,毕竟category存储数据毕竟麻烦,如果主类开放数据访问权限可能会破坏其封装性,必要时需要结合一些其他手段来实现,比较规范的是私有的数据和接口访问共享协议,这在重构拆分一些大的类的时候简单易行,最重要的是代码拆分的等效性很高(相比较于新建类来承担这些职责),重构稳定性较好,不容易出bug,所以有需要的同学可以尝试一下。

  6. category方法可以被主类通过引入头文件正常调用(就是不通过运行时),这个知道的人就更少了,但是要注意最好在.m文件中引入category头文件。我们可以把可以拆分的子功能放到不同的category去实现。当然如果这部分代码会作为独立的公共代码被多个类访问,那建议拆分到其他类去做,因为如果用category的话,一旦该类被移除就意味着大量改动。所以用category的场景是和主类关联比较密切,被外部访问较少的代码。

  7. category的load必然会被调用,而且是在主类load调用之后。这也可以实现一些,初始化,注册等。

总结:Category是OC语言的精髓之一,值得大家深度挖掘。

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

推荐阅读更多精彩内容