iOS - Category 的探究

image

Category 主要的功能是给现有的类增加新的方法,Category 的优点是:

  • 可以“分解”庞大的逻辑,进行业务分离
  • 可以实现多继承
  • 声明私有方法
  • ...

基本使用

现有类 Valenti,以及 Valenti 的两个分类:

#import "Valenti.h"

NS_ASSUME_NONNULL_BEGIN

@interface Valenti (Purchase)

- (void)purchase;

@end

NS_ASSUME_NONNULL_END

和:

#import "Valenti.h"

NS_ASSUME_NONNULL_BEGIN

@interface Valenti (Listen)

- (void)listen;
+ (void)listen;

@end

NS_ASSUME_NONNULL_END

以上显然再熟悉不过,在外部引入两个分类的头文件即可通过 Valenti 的对象或者 Velenti 类来调用:

Valenti* v = [[Valenti alloc] init];
[v listen];
[Valenti listen];

内部探究

Objetive-C 对象的分类以及 isa、superclass 指针 中已经知道,实例方法放在类对象中,类方法放在元类对象中。

同样,分类的实例方法和类方法同样放在该类的类对象和元类对象中。

一个类,有且只有一个类对象和元类对象。

我们将 Valenti+Listen.m 通过命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Valenti+Listen.m

得到 C++ 源码文件发现分类结构体:

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

也就是当程序在编译的时候,分类的实例方法和类方法都会先放到该结构体里。另:

  • name 表示类名,也就是 Valenti
  • instance_methods 表示实例方法列表
  • class_methods 类方法列表
  • protocols 协议信息
  • properties 属性相关

在 C++ 实现中还可以发现:

static struct _category_t _OBJC_$_CATEGORY_Valenti_$_Listen __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Valenti",
    0, // &OBJC_CLASS_$_Valenti,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Valenti_$_Listen,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Valenti_$_Listen,
    0,
    0,
};

这个 _OBJC_$_CATEGORY_Valenti_$_Listen 就表示分类 Listen,内部第三、第四个参数就对应 instance_methodsclass_methods由于我的分类没有遵守任何协议,也没有增加属性,所以最后两个参数为 0。
若增加协议以及属性:

#import "Valenti.h"

NS_ASSUME_NONNULL_BEGIN

@interface Valenti (Listen)<NSCopying, NSCoding>

@property (nonatomic, copy) NSString* albumName;
@property (nonatomic, assign) NSInteger disc;

- (void)listen;
+ (void)listen;

@end

NS_ASSUME_NONNULL_END

用 C++ 重写后的源码为:

static struct _category_t _OBJC_$_CATEGORY_Valenti_$_Listen __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Valenti",
    0, // &OBJC_CLASS_$_Valenti,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Valenti_$_Listen,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Valenti_$_Listen,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Valenti_$_Listen,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Valenti_$_Listen,
};

借助 objc 源码探究

首先找到 objc-os.mm 文件,该文件为运行时的入口文件,该文件的 _objc_init 方法为运行时的初始化方法:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    ...

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

&map_images 为方法地址,可看到 void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) 方法中调用了 map_images_nolock 方法。而该方法又会调用 _read_images 方法:

if (hCount > 0) {
    _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}

该方法从方法名可得知内部是实现一些读取加载的功能。其中 totalClasses 这个参数表示项目中所有的类,再看 _read_images 内部有:

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_t **catlist 内部存储了所有的分类信息。有关分类的核心处理在 remethodizeClass 方法中,该方法就是对类对象或者元类对象的方法列表进行整理组织:

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    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 方法格外引人注意,它接受的 clscats 参数正是类对象和分类,attachCategories 内部实现为:

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);
    
    // 判断是否是元类对象
    bool isMeta = cls->isMetaClass();

    // ** 二维数组
    // 方法数组
    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));

    ...
    while (i--) {
    
        // cats 中存放着例子中的 Listen 和 Purchase 分类
        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);
}

所以这个方法就是实现了分类中方法、属性、协议的所有信息向类对象中整合的过程。
attachLists 方法的内部实现为:

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

        if (hasArray()) {
            uint32_t oldCount = array()->count;
            // 扩容
            uint32_t newCount = oldCount + addedCount;
            // 给数组重新分配内存空间
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            
            // array()->lists 原来的方法列表
            // 移动内存
            memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
            
            // addedLists 为所有分类的方法列表     
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
           ...
        } 
        else {
           ...
        }
}

为弄懂这段代码的关系首先明白 rw->methods 的指向如下图:


image

如图所示,中间的蓝色方块是一个二维数组,此时只有一个元素(因为此时数组中无任何分类的方法列表),存储的是初始方法列表的地址。然后执行了:

memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));

进行移动内存,假如 newCount 为 2,那么执行完上句代码后新的指向为:


image

然后执行:

memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));

将所有的方法列表拷贝到 methods 中,新的指向为:

image

所以假如 Valenti 中和 Valenti+Listen 中同时出现了 listen 实例方法,那么一定是优先调用分类中的 listen 方法。

那么 N 个分类同时都有 listen 方法优先调用那个?这完全取决于编译顺序,最后加进去的分类最先调用。

Extension 和 Category

Extension(扩展)和 Category 原理完全不同,扩展是在编译之前就已经将各种信息整合到类对象或者元类对象中了。

Category 中的 load 和 initialize

load 方法

load方法会在 runtime 加载类、分类的时候调用。*假如在 Valenti 类以及 Valenti+Listen 分类中同时打印 load 信息,并且不调用任何 Valenti 和其任何分类的任何一个方法,仅仅是运行程序发现:

Valenti -- load
Valenti+Listen -- load

就"自行"调用了 load 方法。看来,load 方法是无视你是否用到该类的,只要本类或者分类被加到内存就会调用 load 方法。

假如,使用了 Valenti 类以及分类,运行,打印结果依然是:

Valenti -- load
Valenti+Listen -- load

由上节得知,分类中的同名方法明明会覆盖本类的同名方法,但是为何两个函数都执行了,并且本类的方法优于分类的方法?借助:

unsigned int count = 0;
Method *methodArray = class_copyMethodList(object_getClass([Valenti class]), &count);
unsigned int i;
for(i = 0; i < count; i++) {
    NSLog(@"%@", NSStringFromSelector(method_getName(methodArray[i])));
}
free(methodArray);

打印结果为:

listen
load
load

这说明分类的 load 方法合并到了本类中。那么 Valenti 的 load 方法会优先执行?看来还需要在 objc 源码中找答案。
我们在 objc-os.mm_objc_init 方法中可注意到另一个函数 —— load_images

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    ...
    call_load_methods();
}

call_load_methods(); 为调用 load 方法,其内部为:

void call_load_methods(void)
{
    ...
    do {
        while (loadable_classes_used > 0) {
            // 先调用类的 load 方法
            call_class_loads();
        }
        // 调用分类的 load 方法
        more_categories = call_category_loads();
    } while (loadable_classes_used > 0  ||  more_categories);
    ... 
}

这里答案已经明确,这个顺序和编译顺序无关。无论谁先加载,类的 load 的方法总是先执行,那么类的 load 方法是如何调用的:

static void call_class_loads(void)
{
   ...
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        // load_method 指针指向的是 load 的方法的内存地址
        load_method_t load_method = (load_method_t)classes[i].method;
        ...
    }
    ...
    // 调用
    (*load_method)(cls, SEL_load);
}

load_method 定义为:

typedef void(*load_method_t)(id, SEL);

在后面 (*load_method)(cls, SEL_load) 是找到类中的方法直接调用,而不是像其他的方法从各个分类中找然后调用。分类的 load 方法也是一样:

static call_category_loads(void)
{
   ...
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        // load_method 指针指向的是 load 的方法的内存地址
        load_method_t load_method = (load_method_t)cats[i].method;
        ...
    }
    ...
    // 调用
    (*load_method)(cls, SEL_load);
    ... 
}

而且我们可看到无论是类还是其分类的 loadable_category 定义是这样的:

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

可断定,两个结构体中的 mehod 就是 load 方法。这个结构体的作用就是加载类的,所以只放了 load 方法。

到这里,其他方法和 load 的方法的本质区别已经很明显,load 方法是在运行时通过指针直接找到内存中的函数地址进行调用,而其他方法则是通过消息机制进行调用,先通过 isa 指针找到其类对象或者元类对象,然后遍历类对象和元类对象的方法列表,找到即调用。

继承关系的 load 方法

我们增加难度,新增 SubValenti继承自 Valenti 类,并给 SubValenti 添加分类 SubValenti+Listen,在各自的方法里添加打印,运行结果如下:

Valenti -- load
SubValenti -- load
Valenti+Listen -- load
SubValenti+Listen -- load

这个顺序依然是无关编译顺序。那么这个顺序由什么决定?或者说有什么规律?在上面的介绍里,类方法是优先调用,在 call_class_loads 方法中:

load_method_t load_method = (load_method_t)classes[i].method;

系统会遍历 classes 中所有的类并进行 load 方法的调用。那么得知,数组的添加顺序遍决定了类的 load 调用顺序。此时我们就得搞清楚 call_class_loads 方法中 loadable_classes 这个数组,我们在 void load_images(const char *path __unused, const struct mach_header *mh) 方法中有:

prepare_load_methods((const headerType *)mh);

这个方法的作用是在类加载之前做一些准备操作。其内部:

void prepare_load_methods(const headerType *mhdr)
{
   ...
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
    ...
}

schedule_class_load 方法中我们看到了和 loadable_classes 有关联的影子:

static void schedule_class_load(Class cls)
{
    ...
    if (cls->data()->flags & RW_LOADED) return;
    schedule_class_load(cls->superclass);
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

add_class_to_loadable_list(cls) 内部便是将类放到 loadable_classes 中去。在这里我们发现一个递归调用:

schedule_class_load(cls->superclass);

假如 cls 是 SubValenti 那么,这句代码会先添加 SubValenti 的父类 —— Valenti 类,若 Valenti 没有父类,则直接添加到 loadable_classes 中,所以 Valenti 的 load 调用永远优于其子类的 load 调用。

还有一个值得注意的地方是在 prepare_load_methods 方法内:

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

类的调用顺序还取决于这里的 _getObjc2NonlazyClassList 方法,这个方法和编译顺序有关。
新建 Rex 类继承 NSObject 打印 load 方法。并将 Rex 的编译顺序提前:

image

运行得到:

Rex -- load
Valenti -- load
SubValenti -- load
Valenti+Listen -- load
SubValenti+Listen -- load

得以验证。

类的 load 方法调用顺序已经明确,再看分类的 load 方法调用顺序发现原理和类方法的调用过程是一样的。也是有一个 loadable_categories 存储着所有的分类。这个数组的顺序同样取决于 prepare_load_methods 方法中 categorylist 的顺序:

oid prepare_load_methods(const headerType *mhdr)
{
    size_t count, 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;  
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

initialize 方法

initialize 方法是在类第一次接收到消息的时候调用。也就是类第一次接收到消息转发的时候就调用 initialize 方法。最简单的,调用 alloc 方法的时候就会调用 initialize 方法。

我们同时在 Valenti 以及 Valenti+Listen 这个分类中打印 initialize 方法,得到以下结果:

Valenti+Listen -- initialize

只打印了分类的 initialize 方法。
可得知,initialize 和 load 方法不同,是通过 objc_msgSend 消息机制调用,通过 isa 找到类对象或者元类对象对应的方法调用,同名的方法分类中优先调用,并且只调用一次。

运行:

[Valenti alloc];
[Valenti alloc];
[Valenti alloc];
[Valenti alloc];

发现 initialize 也只调用一次。

还有一点值得注意的是,当我们在 Valenti 的子类 —— SubValenti 类中打印 initialize 方法,并且只给 SubValenti 对象发消息,注释掉任何 给 Valenti 发消息的代码,会得到如下打印:

Valenti+Listen -- initialize
SubValenti -- initialize

在给子类发消息的时候,系统会先调用一次父类的 initialize 方法。

我们知道 [Valenti alloc] 方法会转成 objc_msgSend([Valenti class], @selector(alloc)); 这便是 Valenti 类第一次接收到消息,然后调用了 initialize 方法。

换而言之 objc_msgSend 方法内部会调用 initialize 方法。

我们在源码中搜寻 objc_msgSend 的影子可发现有关 objc_msgSend 的东西时通过汇编来实现的:

...
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached
...

这里我们看到了老朋友 isa 指针和 class。但其他过多的消息无法便利的得知。于是只能换一种思路去探索 initialize。

我们知道在类第一次接受消息的时候会调用 initialize 方法,而在消息转发的时候会通过 isa 指针寻找方法,找到则调用,那么 initialize 是在什么时机调用的?是在寻找方法的时候调用?还是在调用目标方法之前调用 initialize?我们在 obj_msgSend 的汇编实现的注释中看到有:

IMP objc_msgLookup(id self, SEL _cmd, ...);

其对应 C 语言实现为:

Method class_getInstanceMethod(Class cls, SEL sel)

遵循这样的调用顺序我们可找到 initialize 的影子:


image

其中:

if (initialize  &&  !cls->isInitialized()) {
    runtimeLock.unlock();
    _class_initialize (_class_getNonMetaClass(cls, inst));
    runtimeLock.lock();
}

这段判断类是否初始化,若未初始化则调用 _class_initialize 方法。在 _class_initialize 中:

void _class_initialize(Class cls)
{
    ...
    
    supercls = cls->superclass;
    // 递归,判断该类是否有父类,有父类的父类是否初始化?若未初始化调用本方法进行 initialize
    if (supercls  &&  !supercls->isInitialized()) {
        // 在这里,父类优于子类
        _class_initialize(supercls);
    }
    ...
#if __OBJC2__
        @try
#endif
        {
            // 若父类已经初始化,则本类 initialize
            callInitialize(cls);
            ...
        }
    ...
}

callInitialize 为初始化方法。其逻辑:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

得到验证,initialize 方法也是通过 objc_msgSend 调用的。
那么可推导:
假如存在更多层继承关系,最顶层的父类的 initialize 方法调用,然后是其子类的 …… 最后才是本类的。

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

推荐阅读更多精彩内容