关于ios:iOS底层系列Category

11次阅读

共计 8060 个字符,预计需要花费 21 分钟才能阅读完成。

前言

Category 是咱们平时用到的比拟多的一种技术,比如说给某个类减少办法,” 增加 ” 属性,或者用 Category 优化代码构造。

咱们通过上面这几个问题作为切入点,联合 runtime 的源码,探索一下 Category 的底层原理。

咱们在 Category 中,能够间接增加办法,而且咱们也都晓得,增加的办法会合并到本类当中,同时咱们也能够申明属性,然而此时的属性没有性能,也就是不能存值,这就相似于 Swift 中的计算属性,如果咱们想让这个属性能够贮存值,就要用 runtime 的形式,动静的增加。

探索

1. Category 为什么能增加办法不能 ” 增加 ” 属性

首先咱们先创立一个 Person 类,而后创立一个 Person+Run 的 Category,并在 Person+Run 中实现 -run 办法。

咱们能够应用命令行对 Person+Run.m 进行编译

xcrun -sdk iphonesimulator clang -rewrite-objc Person+Run.m

失去一个 Person+Run.cpp 文件,在文件的底布,能够找到这样一个构造体

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

这些字段简直都是见名知意了。

每一个 Category 都会编译而后存储在一个_category_t 类型的变量中

static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
    0,
    0,
    0,
};

因为咱们的 Person+Run 外面只有一个实例办法,所以从上述代码中来看,也只有对应的地位传值了。

通过这个_category_t 的构造构造咱们也能够看出,变量存储在_prop_list_t,并不是类中的 objc_ivar_list 构造体,而且咱们都晓得 property=ivar+set+get,Category 中不会生成 ivar,所以基本都不能 ” 增加 ” 成员变量。

如果咱们在分类中手动为成员变量增加了 set 和 get 办法之后,也能够调用,但实际上是没有内存来储值的,这就如同 Swift 中的计算属性,只起到了计算的作用,就相当于是两个办法(set 和 get),然而并不能领有真用的内存来存储值。

2. Category 的办法是何时合并到类中的

大家都晓得 Category 分类必定是咱们的利用启动是,通过运行时个性加载的,然而这个加载过程具体的细节就要联合 runtime 的源码来剖析了。

runtime 源码太多了,咱们先通过大略浏览代码来定位实现性能的相干地位。

我从 objc-runtime-new.mm 中找到了上面这个办法。

/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void methodizeClass(Class cls, Class previously)

而且他的正文一些的很分明,修复类的办法,协定和变量列表,关联还未关联的分类。

而后咱们持续找,就找到了咱们须要的这个办法。

 void attachLists(List* const * addedLists, uint32_t addedCount)

咱们从其中摘出一段代码来剖析就能够解决咱们的问题了。

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

在调用此办法之前咱们所有的分类会被办法一个 list 外面(每一个分类都是一个元素),而后再调用 attachLists 办法,咱们能够看到,在 realloc 的时候传进一个 newCount,这是因为要减少分类中的办法,所以须要对之前的数组扩容,在扩容完结后先调用了 memmove 办法,在调用 memcopy,大家能够上网查一下这两个办法具体的区别,这里简略一说,其实实现的成果都是把前面的内存的内容拷贝到后面内存中去,然而 memmove 能够解决内存重叠的问题。

其实也就是首先将原来数组中的每个元素先往后挪动(咱们要增加几个元素,就挪动几位),因为挪动后的地位,其实也是数组本人的内存空间,所以存在重叠问题,间接挪动会导致元素失落的问题,所以用 memmove(会检测是否有内存重叠)。

挪动完之后,把咱们贮存分类中办法的 list 中的元素挪动到数组后面地位。

过程就是这样子了,其实咱们第三个问题就顺便解决完了。

3. Category 办法和类中办法的执行程序

下面其实说到了,类中原来的办法是要往后面挪动的,分类的办法增加到后面的地位,而且调用办法的时候是在 list 中遍历查找,所以咱们调用办法的时候,必定会先调用到 Category 中的办法,然而这并不是笼罩,因为咱们的原办法还在,只是这中机制保障了如果分类中有重写类的办法,会被优先查找到。

4. +load 和 +initialize 的区别

对于这个问题咱们从两个角度登程剖析,调用形式 调用时刻

+load

简略的举一个例子,咱们创立一个 Person 类,而后重写 +load 办法,而后为 Person 新建两个 Category,都别离实现 +load。

@implementation Person
+ (void)load {NSLog(@"Person - load");
}
@end

@implementation Person (Test1)
+ (void)load {NSLog(@"Person Test1 - load");
}
@end

@implementation Person (Test2)
+ (void)load {NSLog(@"Person Test2 - load");
}
@end

当咱们进行我的项目的时候,会失去上面的打印后果。

2020-09-14 09:34:41.900161+0800 Category[4533:53426] Person - load
2020-09-14 09:34:41.900629+0800 Category[4533:53426] Person Test1 - load
2020-09-14 09:34:41.900700+0800 Category[4533:53426] Person Test2 - load

咱们并没有应用这个 Person 类和他的 Category,所以应该是我的项目运行后,runtime 在加载类和分类的时候,就会调用 +load 办法。

咱们从源码中找到上面这个办法
void load_images(const char *path __unused, const struct mach_header *mh)

办法中的最初一行调用了 call_load_methods(),这个call_load_methods() 中就是实现了 +load 的调用形式。

上面是 call_load_methods() 函数的实现,大家简略浏览一遍

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

从源码中很分明的能够看到,先调用call_class_loads(),再调用call_category_loads(),这就阐明了在调用所有的 +load 办法时,实现调用了所有类的 +load 办法,再去调用分类中的 +load 办法。

而后咱们在进入到 call_class_loads() 函数中

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

从两头的循环中能够看出,是取到了每个类的 +load 函数的指针,间接通过指针调用了这个函数。 call_category_loads()函数中体现进去的 Category 的 +load 办法的调用,也是同理。

同时这也解答了咱们的另一个纳闷,那就是为什么总是先调用类的 +load,在调用 Category 的 +load。

思考:如果存在继承的状况,+load 又会是怎么的调用程序呢?

从下面 call_class_loads() 函数中能够看到有一个 list:loadable_classes,咱们猜想这外面应该就是寄存着咱们所有的类,因为上面的循环是从 0 开始循环,所以咱们要研究所有的类的 +load 办法的执行程序,就要看这个 list 中的类的程序是怎么样的。

咱们从个源码中能够找到这样一个办法,prepare_load_methods,在其实现中调用了 schedule_class_load 办法,咱们看一下 schedule_class_load 的源码

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

从源码中 schedule_class_load(cls->superclass); 这一句中能够看出,递归调用本人自身,并且传入本人的父类,后果递归之后,才调用add_class_to_loadable_list,这就阐明父类总是在子类后面退出到 list 当中,所有在调用一个类的 +load 办法之前,必定要先调用其父类的 +load 办法。

那如果是其余没有继承关系的类呢,这就跟编译程序有关系了,大家能够本人尝试验证一下。

小结:

+load 办法会在 runtime 加载类和分类时调用
每个类和分类的 +load 办法之后调用一次
调用程序:
先调用类的 +load

  • 依照编译顺序调用
  • 调用子类 +load 之前,先调用父类的 +load

再调用分类的 +load

  • 依照编译顺序调用

小结中如果有我没提到的,大家能够自行验证。

+initialize

+initialize 的调用是不同的,如果某一个类咱们没有应用过,他的 +initialize 办法是不会调用的,到咱们应用这个类(调用了类的某个办法)的时候,才会触发 +initialize 办法的调用。

@implementation Person
+ (void)initialize {NSLog(@"Person - initialize");
}
@end

@implementation Person (Test1)
+ (void)initialize {NSLog(@"Person Test1 - initialize");
}
@end

@implementation Person (Test2)
+ (void)initialize {NSLog(@"Person Test2 - initialize");
}
@end

当咱们执行 [Person alloc]; 的时候,才会走 +initialize 办法,而且执行的 Category 中的 +initialize:

2020-09-14 10:40:23.579623+0800 Category[9134:94173] Person Test2 - initialize

这个咱们之前曾经说过了,Category 的办法会增加 list 的后面,所以会先被找到并且执行,所以咱们猜想 +initialize 的执行是走的失常的音讯机制,objc_msgSend。

因为 objc_msgSend 实现并没有齐全开源,都是汇编代码,所以咱们须要换一个思路来钻研源码。

objc_msgSend 实质是什么?以调用实例办法为例,其实就是通过 isa 指针找到该类,而后寻找办法,找到之后调用。如果没有找到则通过 superClass 找到父类,持续查找办法。下面的例子中,咱们仅仅是调用了一个 alloc 办法,然而也执行了 +initialize 办法,所以咱们猜想 +initialize 会在查找办法的时候调用到。通过这个思路,咱们定位到了 class_getInstanceMethod() 函数(class_getInstanceMethod 函数就是在类中查找某个 sel 时候调用的),在这个函数中,又调用了IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

在该函数中咱们能够找到上面这段代码

if ((behavior & LOOKUP_INITIALIZE)  &&  !cls->isInitialized()) {initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}

能够看出如果类还没有执行 +initialize 就会先执行,咱们再看一下 if 语句中的 initializeNonMetaClass 函数,他会先拿到 superClass,执行 superClass 的 +initialize

supercls = cls->superclass;
if (supercls  &&  !supercls->isInitialized()) {initializeNonMetaClass(supercls);
}

这就是存在继承的状况,为什么会先执行父类的 +initialize。

大总结
  1. 调用形式

    load 是依据函数地址间接调用
    initialize 是通过音讯机制 objc_msgSend 调用

  2. 调用时刻

    load 是在 runtime 加载类和分类时调用(只会调用一次)
    initialize 是在类第一次收到音讯时调用个,默认没有继承的状况下每个类只会 initialize 一次(父类的 initialize 可能会被执行屡次)

  3. 调用程序

    • load
先调用类的 load:先编译的类先调用,子类调用之前,先调用父类的
在调用 Category 的 load:先编译的先调用

- initialize
先初始化父类
在初始化子类(初始化子类可能调用父类的 initialize)

补充

下面总结的时候说到父类的 initialize 会被执行屡次,什么状况下会被执行屡次,为什么?举个例子:

@implementation Person
+ (void)initialize {NSLog(@"Person - initialize");
}
@end

@implementation Student
@end

Student 类继承 Person 类,并且只有父类 Person 中实现了 +initialize,Student 类中并没有实现

此时咱们调用[Student alloc];,会失去如下的打印。

2020-09-14 11:31:55.377569+0800 Category[11483:125034] Person - initialize
2020-09-14 11:31:55.377659+0800 Category[11483:125034] Person - initialize

Person 的 +initialize 被执行了两次,但这两个意义是不同的,第一次执行是因为在调用子类的 +initialize 办法之前必须先执行父类的了 +initialize,所以会打印一次。当要执行子类的 +initialize 时,通过音讯机制,student 类中并没有找到实现的 +initialize 的实现,所以要通过 superClass 指针去到父类中持续查找,因为父类中实现了 +initialize,所以才会有了第二次的打印。

结尾

本文的篇幅略长,笔者依照本人的思路和想法写完了此文,陈说过程不肯定那么调节和欠缺,大家在浏览过程中发现问题,能够留言交换。

感激浏览。

正文完
 0