iOS 底层原理-类的加载(中)

在上一篇 iOS 底层原理-类的加载(上) 分析了 map_images 的流程,那么问题来了,一个应用程序有很多的类,如果都按照这个流程走,太耗费性能了,这是苹果不允许的,那么它真实的流程是什么样子呢?下面我们一起来探索下

如何定位当前研究的类

我们知道一个应用程序运行需要加载很多的类文件,那么我们如何去定位到我们要研究的指定的类呢?这里我们需要对源码进行一些处理,让它只针对我们需要研究的类。上一篇中讲到 objc 源码通过 cls->mangledName() 来获取类名,那么我们判断自定义研究的类名与 mangledName 是否一致,如果一致,则就是我们需要研究的,反之,则不需要研究。如下

const char *mangledName  = cls->mangledName();
const char *LCPersonName = "LCPerson";

if (strcmp(mangledName, LCPersonName) == 0) {
    bool lc_isMeta = cls->isMetaClass(); // 用来判断是否是元类,排除干扰(如果需要)
    if (!lc_isMeta) {
        printf("%s: 这个是我要研究的 %s \n", __func__, LCPersonName);
    }
}

这样我们就可以避免了其他类的干扰,只关注我们自定义的类。核心的方法在上一篇中提到了,只需要将自定义的代码添加上去就可以了

懒加载与非懒加载类

ObjC 中,判断一个类是否是懒加载类,就是看它是否实现了 +load 方法

  • 实现了 +load 方法,它就是非懒加载类

  • 反之,就是懒加载类

+load 方法会提前加载(+load 会在 load_images 调用,前提是类存在,这个会在后面进行验证)。如果没实现 +load 方法,会在第一次调用方法时加载

实现 load 方法的类加载

创建一个 LCPerson 类,声明并实现一个实例方法以及重写 +load 方法

@interface LCPerson : NSObject

- (void)lc_instanceMethod1;

@end

@implementation LCPerson

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

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

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello!");
    }
    return 0;
}

运行 objc-781 源码,查看打印结果,如下

非懒加载的流程图如下

未实现 load 方法的类加载

删除 +load 方法,此时我们运行 objc-781 源码,发现只会打印 readClass 方法,那么它是什么时候加载的呢?现在我们在 main 函数中添加如下代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [LCPerson alloc];
        [person lc_instanceMethod1];
        NSLog(@"Hello!");
    }
    return 0;
}
1. 断点调试

此时在 main 函数处打个断点,我们运行 objc-781 源码,调用了 readClass 方法后直接来到断点处,再次执行下一步,可以看到调用流程如下

2.堆栈信息

我们在 realizeClassWithoutSwift 处打个断点,运行 objc-781 源码,查看堆栈信息

方法调用流程为什么能来 realizeClassWithoutSwift?,本质上调用 allocalloc 的本质是消息的发送。因为是第一次调用,会走消息的慢速查找 lookUpImpOrForward,类没有初始化,会调用 realizeClassMaybeSwiftAndLeaveLocked,后续调用 realizeClassMaybeSwiftMaybeRelock,最后调用 realizeClassWithoutSwift,后面是实现类。

懒加载类的流程图如下

分类

分类是什么?要研究分类,首先我们需要知道分类是什么,怎么研究呢?可以从下面三种方法探索,首先在 main 文件中定义个分类,如下

@interface LCPerson (LC)

@property (nonatomic, copy) NSString *lc_name;
@property (nonatomic, assign) int lc_age;

- (void)lc_instanceMethod3;
- (void)lc_instanceMethod1;
- (void)lc_instanceMethod2;

@end

@implementation LCPerson (LC)

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

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

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

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

@end

1. 通过 clang 探索

cdmain.m 所在的文件夹,执行 clang -rewrite-objc main.m -o main.cpp,文件夹中会多出一个 main.cpp 类文件,打开 main.cpp,就可以看到底层编译,分类的类型是 _category_t 结构体

搜索 struct _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;
};

从上面对应关系可以看出,由于 LCPerson (LC) 没有协议,所以对应的为 0,结构体中有两个 _method_list_t,分别表示的是 实例方法列表类方法列表。搜索 _CATEGORY_INSTANCE_METHODS_LCPerson_ 查看它的实例方法列表底层实现

对应分类中我们添加的三个实例方法,其格式为 sel + 签名 + 地址,看着很熟悉是不是?就是 method_t 结构体的属性

struct method_t {
    SEL name; // 方法名
    const char *types; // 方法签名
    MethodListIMP imp; // 函数地址

    struct SortBySELAddress :
        public std::binary_function
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

我们再来看下分类的属性列表

可以看到,我们在分类里定义了属性,但是在底层编译中并没有看到它的成员变量,而且在实例方法列表中也没有看到属性的 setter 以及 getter 方法。这是因为分类中定义的属性不会生成成员变量,只是有 settergetter 的声明,并没有实现它的 settergetter 方法。我们可以通过关联对象来设置(objc_setAssociatedObjectobjc_getAssociatedObject)。

2. 通过 Xcode 官方文档探索

如果不会 clang,可以通过 Xcode 文档搜索 Category 查看

3. 通过 objc 源码探索

打开 objc 源码,搜索 category_t

分类的本质是一个 _category_t 的结构体类型,它有两个属性: name(类名)和 cls(本类对象);两个方法列表:实例方法列表和类方法列表;一个协议列表;一个属性列表。另外分类中的属性是没有成员变量的,只有 settergetter 的声明,并没有实现 settergetter 方法。

分类数据的加载时机

在上一篇 iOS 底层原理-类的加载(上) 中分析了分类数据的加载是在 attachCategories 方法中实现的,且分类的加载顺序是根据编译器编译的先后顺序加载到类中,越晚加进来,越在前面。

但是它在什么时机调用的,我们还不得而知,下面就让我们就一起探索下吧

什么时机调用

下面我们通过反推法和堆栈信息两种方法去探索

1. 反推法
  • objc 源码中全局搜索 attachCategories(,发现只有两处调用,分别是 attachToClassload_categories_nolock

通过调试发现不会走 attachToClass 中的 attachCategories(这里我们设置的主类和分类都实现了 +load 方法,如果主类未实现 +load 方法,分类有实现 +load 方法,则会调用 attachToClass 中的 attachCategories,后面会分析到)

  • 全局搜索 load_categories_nolock 的调用,发现有两处调用 _read_imagesloadAllCategories

_read_images 中的调用如下

通过调试,不会走 _read_imagesif 流程,走的是 loadAllCategories 的流程

  • 再次全局搜索 loadAllCategories 的调用,发现只有一次调用,是在 load_images 时调用的
2. 堆栈信息

现在我们在 attachCategories 中加上自定义的断点,bt 查看它的堆栈

这里也验证了我们刚刚反推的流程,反推流程和正常流程图如下

分类与类的搭配使用(+load 方法实现与否)

上面我们分析了主类的懒加载与非懒加载,下面我们看下它们搭配使用(+load 实现与否)的加载情况。大致可以分为四种情况

分类实现 +load 分类未实现 +load
主类实现 +load 非懒加载类 + 非懒加载分类 非懒加载类 + 懒加载分类
主类未实现 +load 懒加载类 + 非懒加载分类 懒加载类 + 懒加载分类

主类源码

/*------ .h ------**/
@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *kc_name;
@property (nonatomic, assign) int kc_age;

- (void)kc_instanceMethod1;
- (void)kc_instanceMethod2;
- (void)kc_instanceMethod3;

+ (void)kc_sayClassMethod;

@end

/*------ .m ------**/
#import "LGPerson.h"

@implementation LGPerson

+ (void)load {

}

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

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

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

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

@end

分类源码

/*------分类LGA.h ------**/
@interface LGPerson (LGA)

- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;

@end

/*------分类LGA.m ------**/
#import "LGPerson+LGA.h"

@implementation LGPerson (LGA)

+ (void)load{

}

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

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

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

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

@end

/*------分类LGB.h ------**/
@interface LGPerson (LGB)

- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;

@end

/*------分类LGB.m ------**/
#import "LGPerson+LGB.h"

@implementation LGPerson (LGB)

+ (void)load{

}

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

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

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

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

@end

非懒加载类与非懒加载分类

这种情况是 主类实现了 +load 方法,分类也实现了 +load 方法,前面我们分析的就是这种情况,我们可以得出如下结论

  • 类的数据加载是在 _read_images 中调用 _getObjc2NonlazyClassList 加载,插入表操作,ro、rw 的操作。调用路径:

map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,后面会走 load_images 方法

  • 分类的数据加载是通过 load_images 加载到类中的,调用路径为

load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists

通过我们自定义的打印数据,运行程序,打印日志如下

非懒加载类与懒加载分类

这种情况是 主类实现了 +load 方法,分类没有实现 +load 方法

  • 首先,我们在 realizeClassWithoutSwift 的自定义代码中下个断点,查看 ro 情况

从上面可以看到,方法列表中有 16 个方法,但是在主类中没有这么多,那剩余的方法是从哪里来的呢?我们通过 lldb 命令一一打印出方法列表中的方法

从上面的打印信息可以看出,除了主类的方法外,分类的方法也被加载进来了,依次是 LGA->LGB->LGPerson。方法还没有排序,说明分类的数据没有进行非懒加载时,通过 cls->data() 读取到 mach-o 可执行文件时,数据就已经进来了,不需要在运行时添加进去了

  • 下面我们进入 methodizeClass 方法中查看排序后的方法列表数据

通过打印发现,方法排序只对 同名方法进行了排序,而类中的其他方法则是按照 imp地址有序排列,排序的源码如下(核心代码)

static void 
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                   bool baseMethods, bool methodsFromBundle)
{
    for (int i = 0; i < addedCount; i++) {
        method_list_t *mlist = addedLists[I];
        ASSERT(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }
}

static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    // sel - imp
    // Sort by selector address.
    if (sort) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(mlist->begin(), mlist->end(), sorter);
    }
    
    // Mark method list as uniqued and sorted
    mlist->setFixedUp();
}

通过我们自定义的打印数据,运行程序,打印日志如下

懒加载类与懒加载分类

这种情况是 主类和分类都没有实现 +load 方法,这里我们需要在 main 函数中调用类的实例方法来辅助,添加代码如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person kc_instanceMethod1];
    }
    return 0;
}
  • 我们先通过添加我们自定义的打印,不加任何断点,直接运行程序,看下打印日志

其中的 realizeClassMaybeSwiftMaybeRelock 方法调用是消息发送流程的慢速查找的函数,在第一次发送消息时才走的函数

  • 我们在 readClass 处下个断点,读取此时的 ro 情况

此时的 baseMethodList 的个数是 16 个,说明也是从 data 中读取出来的

懒加载类 与 非懒加载分类

这种情况是 主类没有实现 +load 方法,分类实现了 +load 方法

  • 我们先运行程序,获取打印日志
  • 我们在 readClass 处打个断点,查看 ro 情况

可以看到,baseMethodList 的 count 是 8 个,我们打印出每个方法如下

可以看到方法列表里是 LGPerson 的三个实例方法和属性的 settergetter 方法以及 1 个 cxx 方法,说明 ro 中只有主类的数据。那么怎么查看分类的数据呢?为了调试分类的数据加载,继续往下执行:load_images -> loadAllCategories -> load_categories_nolock。打印此时的堆栈信息

继续执行,在 attachToClass 方法打个断点,继续点击下一步,走到 attachCategories

主类未实现 +load,分类实现了 +load,会迫使主类提前加载,即主类强行转换为非懒加载类样式

总结

类和分类搭配使用,其数据的加载时机总结如下:

  • 非懒加载类 + 非懒加载分类:类的数据加载是在 _read_images 中调用 _getObjc2NonlazyClassList 加载;分类的数据加载是通过 load_images 加载到类中的

  • 懒加载类 + 非懒加载分类:分类实现了 +load,会迫使主类提前加载,即在 _read_images 中不会对类做实现操作,在 load_images 方法中触发类的数据加载,同时加载分类数据。

  • 非懒加载类 + 懒加载分类:数据加载在read_image就加载数据,数据来自data,data在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起。

  • 懒加载类 + 懒加载分类:其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成。

你可能感兴趣的:(iOS 底层原理-类的加载(中))