十:底层探索 - 分类的加载

374 阅读3分钟

上个篇章中我们探究了类的加载,本篇章我们将对分类进行探究

🌹WYPerson类

@implementation WYPerson

- (void)eat
{
    NSLog(@"%s",__func__);
}
+ (void)walk
{
    NSLog(@"%s",__func__);
}
@end

🌹WYPerson+test分类
@implementation WYPerson (test)
- (void)instanceMethod
{
     NSLog(@"%s",__func__);
}

+ (void)classMethod
{
     NSLog(@"%s",__func__);
}

一:揭开分类的面纱

在终端输入如下命令行,将分类转化为底层的cpp文件

clang -rewrite-objc WYPerson+test.m -o category.cpp

我们打开cpp文件来到末尾

static struct _category_t _OBJC_$_CATEGORY_WYPerson_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"WYPerson",
	0, // &OBJC_CLASS_$_WYPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_WYPerson_$_test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_WYPerson_$_test,
	0,
	0,
};

我们发现,WYPerson+test分类在底层变为一个名为_OBJC_$_CATEGORY_WYPerson_$_test的结构体,类型是_category_t

下面我们看下_category_t又是什么?

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: 谁的分类
  • cls: 所属的类
  • instance_methods: 实例方法
  • class_methods:类方法
  • protocols:协议
  • properties:属性

为了进一步查看_category_t到底是什么,我们用objc的源码看一下

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);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

对比之后发现,两者基本差不多

二:分类的加载过程

在上一篇章对read_image进行分析的时候,其中也有对分类的处理,系统的分类我们不关注,只关注我们自己的分类是如何加载的,我们添加一段代码进行打印我们自己写的分类。

for (EACH_HEADER) {
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        auto processCatlist = [&](category_t * const *catlist) {
            for (i = 0; i < count; i++) {
                category_t *cat = catlist[i];
                Class cls = remapClass(cat->cls);
                locstamped_category_t lc{cat, hi};
                
                // 🌹🌹添加代码当是我们自己写的分类,打印
                const class_ro_t *ro = (const class_ro_t *)cls->data();
                const char *cname = ro->name;
                const char *oname = "WYPerson";
                if (cname && (strcmp(cname, oname) == 0)) {
                    printf("类名: %s -- %p\n",cname,cls );
                }
                
                
            ......这里省略部分代码
    }

当代码走完的时候都没有打印,意味着我们的分类压根没有加载,但是通过前面我们对类的探究,知道类的加载分为非懒加载和懒加载,那么这里没有打印是不是因为这个分类是懒加载呢?我们知道实现 +load 方法会提前加载,那么我们为分类添加+load方法。

如图验证了我们的猜想,分类也有非懒加载和懒加载之分。

类别和类常常是共同存在的,没有类的存在,分类也没有存在的必要。现在我们知道不管是类还是分类,都存在非懒加载和懒加载之分,那么他们相结合之后会是如何加载的呢?下面我们将一探究竟。

1. 分类未实现 +load方法

1.1 懒加载分类 + 懒加载类

这种情况下:

分类:未实现 +load方法

类:未实现+load方法

给分类添加+ (void)classMethod方法并在main.m中调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        [WYPerson classMethod];
        NSLog(@"Hello, World!");
        
    }
    return 0;
}

我们知道无论是懒加载类还是非懒加载类都会在realizeClassWithoutSwift(objc779.1)中进行加载,我们在此方法中加上打印的代码,并在if中执行部分打上断点,查看调用堆栈。

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
     if (PrintConnecting) {
        _objc_inform("CLASS: realizing class '%s'%s %p %p #%u %s%s",
                     cls->nameForLogging(), isMeta ? " (meta)" : "", 
                     (void*)cls, ro, cls->classArrayIndex(),
                     cls->isSwiftStable() ? "(swift)" : "",
                     cls->isSwiftLegacy() ? "(pre-stable swift)" : "");
    }
    
    // 🌹打印的代码
    if (strcmp(cls->nameForLogging(), "WYPerson") == 0) {
        _objc_inform("realizing class: %s", cls->nameForLogging());
    }
}

此时我们打印类的ro信息

......省略部分打印
(method_list_t) $8 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 1
    first = {
      name = "classMethod"
      types = 0x0000000100000fa9 "v16@0:8"
      imp = 0x0000000100000ea0 (KCObjcTest`+[WYPerson(test) classMethod] at WYPerson+test.m:16)
    }
  }
}

可以看到我们从类的ro信息中就发现了我们分类的方法,而ro是在编译器就已经确定的,所以当类和分类都是懒加载的时候,分类的方法在编译器就加载到类中。

1.2 懒加载分类 + 非懒加载类

这种情况下:

分类:未实现 +load方法

类:实现+load方法

直接上图

懒加载分类中的方法直接被编译在了ro.baseMethodList中。

2. 分类实现 +load方法

2.1 非懒加载分类 + 懒加载类

这种情况下:

分类:实现 +load方法

类:未实现+load方法

我们知道无论非懒加载还是懒加载类最终都会调用realizeClassWithoutSwift进行类的加载,而在其中的methodizeClass也有对分类的相关处理,我们在_objc_inform打上断点

static void methodizeClass(Class cls, Class previously)
{
    ......省略

    // 🌹打印我们自己的类
    if (strcmp(cls->nameForLogging(), "WYPerson") == 0) {
        _objc_inform("realizing class: %s", cls->nameForLogging());
    }
    // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

   ......省略
}

此时我们查看ro中并没有我们的方法

接着我们查看rw,发现其中已经有了我们的方法

此时我们看下调用堆栈,其实多了一个prepare_load_methods

看下prepare_load_methods定义

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

    runtimeLock.assertLocked();

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

    category_t * const *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
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

观察发现,其中也调用了realizeClassWithoutSwift来对类进行加载,这里非懒加载分类让我们的懒加载类提前加载了,所以说懒加载类并不一定只会在第一次消息发送的时候加载,还要取决于有没有非懒加载的分类,如果有非懒加载的分类,那么就走的是 load_images里面的 prepare_load_methodsrealizeClassWithoutSwift

2.2 非懒加载分类 + 非懒加载类

这种情况下:

分类:实现 +load方法

类:实现+load方法

当类为非懒加载类的时候,走的是_read_images里面的流程,这个时候我们的懒加载分类是在哪加载的呢? 我们methodizeClass中打上断点

最终会在methodizeClass中的attachToClass方法中调用attachCategories对分类进行处理

三:总结

懒加载类 + 懒加载分类

  • 类的加载在第一次消息发送的时候,而分类的加载则在编译时

懒加载类 + 非懒加载分类

  • 类的加载在_read_images处,分类的加载则在编译时

非懒加载类 + 非懒加载分类

  • 类的加载在_read_images处,分类的加载在类加载之后的reMethodizeClass

懒加载类 + 非懒加载分类

  • 类的加载在load_images处,分类的加载在类加载之后的methodizeClass