分类的加载

198 阅读9分钟

前言

iOS的一道经典面试题:分类是什么? 是否可以给分类添加成员变量?如果可以,怎么添加?下面我们就来探究探究分类的前世今生。

分类的探究

分类是一个名称为category_t的结构体。

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

由代码可知,分类的元素如下:

  • 分类关联的类名
  • 分类关联的类
  • 实例方法
  • 类方法
  • 协议
  • 实例属性
  • 类属性

所以,我们可以得出,分类是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些属性、方法、协议。

根据分类的结构,我们还可以得出,分类的可以添加属性,但是需要我们手动实现其set/get方法,这样才能真正的使用属性。

在我们讨论类的加载的时候,在read_image的环节中,我们会处理所有的分类,对未绑定的分类进行绑定操作,将分类的的method、protocol、property添加到类。

for (EACH_HEADER) {
    // 外部循环遍历找到当前类,查找类对应的Category数组
    category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    for (i = 0; i < count; i++) {
        // 内部循环遍历当前类的所有Category
        category_t *cat = catlist[i];
        Class cls = remapClass(cat->cls);

        // 用来判断分类进入
        const char *catename = cat->name;
        const char *ocatename = "addition"; // addition
        if (catename && (strcmp(catename, ocatename) == 0)) {
            printf("AAAAAAAAAAAA类名 :%s  - %p\n",catename,cls);
        }

        // 首先,通过其所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表。
        bool classExists = NO;
        if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
            // 将Category添加到对应Class的value中,value是Class对应的所有category数组
            addUnattachedCategoryForClass(cat, cls, hi);
            // 将Category的method、protocol、property添加到Class
            if (cls->isRealized()) {
                remethodizeClass(cls);
                classExists = YES;
            }
        }

        // 这块和上面逻辑一样,区别在于这块是对Meta Class做操作,而上面则是对Class做操作
        // 根据下面的逻辑,从代码的角度来说,是可以对原类添加Category的
        if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties))
            {
            addUnattachedCategoryForClass(cat, cls->ISA(), hi);
            if (cls->ISA()->isRealized()) {
                remethodizeClass(cls->ISA());
            }
        }
    }
}

了解到这些相关信息之后,我们就先来创建一些文件来验证一下分类加载的具体情况:

分类的加载

首先我们创建一个TPerson类:

@interface TPerson : NSObject
@property (nonatomic, copy) NSString *name;

- (void)sayHello;
+ (void)sayYo;

@end

#import "TPerson.h"

@implementation TPerson

+ (void)load {
    NSLog(@"类-load");
}

- (void)sayHello {
    NSLog(@"%s",__func__);
}

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

@end

并且为其创建一个分类:

#import "TPerson.h"

@interface TPerson (addition)
@property (nonatomic, copy) NSString *cateProp;

- (void)cate_instanceMethod;
+ (void)cate_classMethod;

@end

@implementation TPerson (addition)

+ (void)load {
    NSLog(@"分类-load");
}

- (void)setCateProp:(NSString *)cateProp {
}

- (NSString *)cateProp {
    return @"cateProp";
}

- (void)cate_instanceMethod {
    NSLog(@"%s",__func__);
}

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

另外,我们在read_image方法中加入以下代码,便于调试:

for (EACH_HEADER) {
    classref_t *classlist = _getObjc2ClassList(hi, &count);
    for (i = 0; i < count; i++) {
        Class cls = (Class)classlist[i];
        Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
            
        const char *cname = cls->nameForLogging();
        const char *oname = "TPerson";
        if (cname && (strcmp(cname, oname) == 0)) {
            printf("_read_images - _getObjc2ClassList 类名 :%s  - %p\n",cname,cls);
        }
    }
}

1. 非懒加载类,非懒加载分类

此时,类和分类都实现了load方法,类是非懒加载类,分类也是非懒加载分类。非懒加载类在初始化的时候,对调用realizeClassWithoutSwift(Class cls),该方法会调用methodizeClass(Class cls),而在该方法中会执行下面方法:

category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);

而此时返回的cats是null,所以在此处并不会进行分类的加载操作。 我们在read_image方法中分类的相关处理,添加以下代码:

const char *catename = cat->name;
const char *ocatename = "addition"; // addition
if (catename && (strcmp(catename, ocatename) == 0)) {
    printf("AAAAAAAAAAAA类名 :%s  - %p\n",catename,cls);
}

并在打印的的地方添加调试断点,运行代码,进行调试,代码会断在此处,使用LLDB调试:

可以看出此时,ro里面只有属性的set/get方法和实例方法setHello,并没有加载分类中的方法。继续调试,进入以下代码

bool classExists = NO;
if (cat->instanceMethods ||  cat->protocols  
    ||  cat->instanceProperties) {
    addUnattachedCategoryForClass(cat, cls, hi);
    if (cls->isRealized()) {
        remethodizeClass(cls);
        classExists = YES;
    }
}

// 该方法将没有绑定的分类放入表中,并和类进行绑定。
static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertLocked();

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

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

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

由代码可以看出,在attachCategories方法中,首先我们会拿出当前类的所有分类,进行遍历操作,然后将每一个分类的方法、属性、协议全部copy一份,绑定到对应的类的rw中的methods、properties、protocols中,这样完成分类的加载。 通过LLDB调试,也能得出相应的结果。

结论:当非懒加载类和非懒加载分类配合加载时,类是在realizeClassWithoutSwift中加载,而分类是在调用addUnattachedCategoryForClass(cat, cls, hi)之后,在remethodizeClass(cls)方法中加载。

2. 非懒加载类,懒加载分类

保留类的load方法,注释掉分类中的load方法,就可以探究非懒加载类配合懒加载分类是如何加载的。我们在类的加载中添加如下代码打印结果,便于调试:

for (EACH_HEADER) {
    classref_t *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
    for (i = 0; i < count; i++) {
        Class cls = remapClass(classlist[i]);
        if (!cls) continue;

        const char *cname = cls->nameForLogging();
        const char *oname = "TPerson";//TPerson
            
        if (cname && (strcmp(cname, oname) == 0)) {
            printf("_read_images - _getObjc2NonlazyClassList 类名 :%s  - %p\n",cname,cls);
        }
            
        ......
        addClassTableEntry(cls);
        realizeClassWithoutSwift(cls);
    }
}

在打印处添加断点,使用LLDB调试:

发现此时分类中的方法和数据已经写入ro中。

结论:当非懒加载类和懒加载分类配合加载时,在realizeClassWithoutSwift的时候,已经将分类的数据写入ro中。

3. 懒加载类,非懒加载分类

注释掉类的load方法,保留分类的load方法,就可以验证懒加载类配合非懒加载分类是如何加载的。

我们依然在read_images中,对printf语句进行LLDB调试:

得出,类的ro为空。由于分类实现了load方法,根据我们的经验实现load方法会提前加载,可以推断分类的加载也提前了。我们在load_images方法中下一个断点,然后运行程序,发现程序会进入这里。跟踪流程,会进入prepare_load_methods中,

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    ......
    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    call_load_methods();
}

void 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);
        
        const char *cname = cls->nameForLogging();
        const char *oname = "TPerson";
        if (cname && (strcmp(cname, oname) == 0)) {
            printf("prepare_load_methods :非懒加载分类名 :%s \n",cname);
        }
       
        realizeClassWithoutSwift(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

非懒加载分类,则会调用realizeClassWithoutSwift以及methodizeClass(Class cls),将数据写入rw中:

结论:懒加载类配合非懒加载分类加载,在load_images的方法中,执行prepare_load_methods对分类和类进行加载。

4. 懒加载类,懒加载分类

我们分别注释掉类和分类中的load方法,来看看懒加载类配合懒加载分类是如何加载的。

首先在read_images中,对printf语句进行LLDB调试:

显而易见,类的ro是空的。在类的加载原理中,我们知道懒加载类是在该类第一次调用方法的时候才开始加载,由于调用方法就是消息发送,所以我们直接定位到消息发送流程中。我们在lookUpImpOrForward添加以下打印方法,用以调试:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{

    const char *cname = cls->nameForLogging();
    const char *oname = "TPerson"; // TPerson
    if (cname && (strcmp(cname, oname) == 0)) {
        printf("lookUpImpOrForward 类名 :%s  - %p\n",cname,cls);
    }

    // 此时类还没有加载,进行初始化操作
    if (!cls->isRealized()) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    // 绑定数据
    if (initialize && !cls->isInitialized()) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }
}

我们在printf("lookUpImpOrForward 类名 :%s - %p\n",cname,cls)进行LLDB,发现ro依然是空的,由于当前类并没有初始化,所以会执行realizeClassMaybeSwiftAndLeaveLocked,然后执行realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)方法,接着调用realizeClassWithoutSwift(Class cls)方法,初始化类,加载数据。我们也可以在if (initialize && !cls->isInitialized()) {打断点调试,验证结论:

此时,元类的ro中存在类方法,且有分类的类方法,说明,分类已经加载过了,数据都已经写入。

结论:懒加载类配合懒加载分类加载时,是在第一次调用方法的时候进行加载,即消息发送的时候,在lookUpImpOrForward的时候,由于类没有初始化,所以会调用realizeClassMaybeSwiftAndLeaveLocked进行相关的初始化加载。

总结

    1. 非懒加载类+非懒加载分类:都在read_images中加载
    • 类在realizeClassWithoutSwift
    • 分类在
      • addUnattachedCategoryForClass
      • remethodizeClass
      • attachCategories写入ro
    1. 非懒加载类+懒加载分类:都在read_images中加载
    • 类在realizeClassWithoutSwift
    • 分类在realizeClassWithoutSwift写入ro
    1. 懒加载类+非懒加载分类:在load_images中加载
    • prepare_load_methods
    • realizeClassWithoutSwift
    • remethodizeClass
    • attachCategories写入ro
    1. 懒加载类+懒加载分类:在第一次调用类的方法的时候加载
    • lookUpImpOrForward
    • realizeClassMaybeSwiftAndLeaveLocked
    • realizeClassMaybeSwiftMaybeRelock
    • realizeClassWithoutSwift 直接给dataro赋值

分类的懒加载在编译时就确定,非懒加载在运行时确定。

Tips

动态关联对象

我们前言中提到,如何给TPerson添加在TPerson的分类中添加了一个cateProp的字符串属性,但是当我们在main函数中运行如下代码,就会崩溃:

TPerson *per = [TPerson alloc];
per.cateProp = @"分类的属性";
NSLog(@"---%@----", per.cateProp);

-[TPerson setCateProp:]: unrecognized selector sent to instance提示我们TPerson没有为cateProp实现set方法,那么我们如何实现它的set/get方法呢?

使用关联引用,我们就可以实现set/get方法:

- (void)setCateProp:(NSString *)cateProp {
    objc_setAssociatedObject(self, @"cateProp", cateProp, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cateProp {
    return objc_getAssociatedObject(self, @"cateProp");
}

运行代码,该分类的属性可以正常使用。