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

277 阅读8分钟

前言

类的加载原理(上)中,我们分析了_read_images的流程,目前还有rorw的处理没有找到。但定位到了相关代码realizeClassWithoutSwift,本文将对它进行探究

realizeClassWithoutSwift

  • 查看这里面源码时,感觉有一丝熟悉感,原来在慢速查找的条件准备阶段走到过这里。本文使用WSPerson类去研究realizeClassWithoutSwiftWSPerson类如下:
// .h
@interface WSPerson : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;

+ (void)sayNB;
+ (void)sayGood;
+ (void)sayLike;
- (void)sayABC;
- (void)sayBCD;

@end

// .m
@implementation WSPerson

+ (void)load {
    NSLog(@"🎈🎈🎈%s", __func__);
}
+ (void)initialize {
    NSLog(@"🎈🎈🎈%s", __func__);
}
+ (void)sayNB {
    NSLog(@"%s", __func__);
}
+ (void)sayGood {
    NSLog(@"%s", __func__);
}
+ (void)sayLike {
    NSLog(@"%s", __func__);
}
- (void)sayABC {
    NSLog(@"%s", __func__);
}
- (void)sayBCD {
    NSLog(@"%s", __func__);
}
- (void)sayCDE {
    NSLog(@"%s", __func__);
}
- (void)sayDEF {
    NSLog(@"%s", __func__);
}
- (void)sayEFG {
    NSLog(@"%s", __func__);
}
- (void)sayFGI {
    NSLog(@"%s", __func__);
}
@end
  • 在走到realizeClassWithoutSwift时,先要确定是不是要研究的WSPerson类,可以研究模仿readClass时的写法:
static Class realizeClassWithoutSwift(Class cls, Class previously)
{

    const char *mangledName = cls->nonlazyMangledName();
    const char *person = "WSPerson";
    if (strcmp(mangledName, person) == 0) {
        printf("🎈🎈🎈%s --- 要研究的: %s\n", __func__, mangledName);
    }
    ...
}
  • 断点到printf处运行当走到断点时也就确定了当前使用WSPerson进行研究,再Step Over往下走

ro和rw

先走到这里:

auto ro = (const class_ro_t *)cls->data(); // clean memory 干净内存
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
    // This was a future class. rw data is already allocated.
    // 未来类对 ro,rw处理
    rw = cls->data();
    ro = cls->data()->ro();
    ASSERT(!isMeta);
    cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
    // Normal class. Allocate writeable class data. // 从ro复制一份给rw
    rw = objc::zalloc<class_rw_t>(); // 开辟rw空间
    rw->set_ro(ro); //将ro复制一份给rw
    rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
    cls->setData(rw); // 
}
  • 先从data()获取ro也就是clean Memory(干净内存)
  • 如果是未来类则会走进if处理,而WSPerson是已知类,根据断点走到了else,这里面主要是对rw处理,分为三步:
      1. 开辟rw空间
      1. ro复制一份给rw
      1. 将新的rw放到classbits

缓存初始化

cls->cache.initializeToEmptyOrPreoptimizedInDisguise(); //缓存初始化

#if FAST_CACHE_META
    if (isMeta) cls->cache.setBit(FAST_CACHE_META); // 如果是元类对缓存做一些处理
#endif
  • 这里主要进行了缓存初始化,和对元类的缓存处理

父类和元类的处理

supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil); //对父类的ro,rw处理
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil); // 对元类类的ro,rw处理

#if SUPPORT_NONPOINTER_ISA
if (isMeta) {
    // Metaclasses do not need any features from non pointer ISA
    // This allows for a faspath for classes in objc_retain/objc_release.
    // 如果是元类 isa = raw isa(原始的isa)
    cls->setInstancesRequireRawIsa(); 
} else {
    // Disable non-pointer isa for some classes and/or platforms.
    // Set instancesRequireRawIsa.
    bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
    bool rawIsaIsInherited = false;
    static bool hackedDispatch = false;

    if (DisableNonpointerIsa) {
        // Non-pointer isa disabled by environment or app SDK version
        // 设置了环境变量OBJC_DISABLE_NONPOINTER_ISA,instancesRequireRawIsa = true
        instancesRequireRawIsa = true;
    }
    else if (!hackedDispatch  &&  0 == strcmp(ro->getName(), "OS_object"))
    {
        // hack for libdispatch et al - isa also acts as vtable pointer
        hackedDispatch = true;
        instancesRequireRawIsa = true;
    }
    else if (supercls  &&  supercls->getSuperclass()  &&
             supercls->instancesRequireRawIsa())
    {
        // This is also propagated by addSubclass()
        // but nonpointer isa setup needs it earlier.
        // Special case: instancesRequireRawIsa does not propagate
        // from root class to root metaclass
        instancesRequireRawIsa = true;
        rawIsaIsInherited = true;
    }

    if (instancesRequireRawIsa) {
        cls->setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
    }
}
// SUPPORT_NONPOINTER_ISA
#endif

// Update superclass and metaclass in case of remapping
cls->setSuperclass(supercls); // 设置父类
cls->initClassIsa(metacls); // 设置元类

...

// 设置父类和根类
if (supercls) {
    addSubclass(supercls, cls);
} else {
    addRootClass(cls);
}
  • 主要是获得cls的父类,然后设置cls的父类,设置clsisa指向获得的元类,然后再设置父类根类
  • DisableNonpointerIsa就是上篇文章中提到的环境变量OBJC_DISABLE_NONPOINTER_ISA,如果设置了环境变量或者是元类,isa都是纯净isa(raw isa),如果不是元类且没有设置这个环境变量,则是Non-pointer isa
  • 经过这些步骤,我们看下ro是否已经有值,但是我们知道元类的名字和本类一样,仅凭借名字是不能确定是本类,所以在加一个条件:
if (strcmp(mangledName, person) == 0) {
    if (!isMeta) {
        printf("🎉🎉🎉 %s --- 要研究的,且不是元类: %s\n", __func__, mangledName);
    }
}
  • 然后在printf处断点运行,然后打印ro的方法: 截屏2021-07-20 16.14.10.png
  • 虽然robaseMethodList有地址,但是并没有内容,相当于只有一个壳子,方法并没有加进来,接下来再去看看methodizeClass方法

methodizeClass

  • 因为要用WSPerson去研究方法列表,所以这里还是要做一下筛选,并排除元类,和上面方法一样。

分析

  • 主要代码如下: 截屏2021-07-20 17.37.02.png
  • 因为previously参数传过来为nil,所以这里代码不用看,主要是对方法的赋值排序,以及对分类的属性和协议处理

prepareMethodLists

  • 它的源码如下 截屏2021-07-20 17.45.04.png

  • 主要的排序代码是fixupMethodList,代码如下: 截屏2021-07-20 17.46.57.png

  • 这里是遍历方法列表取出方法,然后用获取方法名字,在调用setName方法将名字关联,也就是impsel关联

  • 调用std::stable_sort方法进行排序

代码验证

  • sort前后分别打印方法列表: 截屏2021-07-21 08.55.53.png
  • 运行打印结果如下: 截屏2021-07-21 09.23.04.png

得到结论:
1.对象方法是排好序的
2.类方法中方法的顺序是变化的,需要进行排序

  • 排完序后,再打印ro,发现还是nil截屏2021-07-21 10.30.20.png
  • 依然没有获取到方法列表,由于rwenil,跟着断点继续往下走走到了objc::unattachedCategories.attachToClass方法

attachToClass

  • 它的源码如下:
class UnattachedCategories : public ExplicitInitDenseMap<Class, category_list>
{
    ...
    void attachToClass(Class cls, Class previously, int flags)
    {
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        auto &map = get();
        auto it = map.find(previously);

        if (it != map.end()) {
            category_list &list = it->second;
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }
}
  • 这里主要将分类方法添加到元类,但是断点没走到if (it != map.end()),走完后rwe也没有值。而判断里attachCategories看似和分类有关,先来看看

attachCategories

  • 再方法里找到了rwe的赋值
auto rwe = cls->data()->extAllocIfNeeded()
  • 也就是调用extAllocIfNeeded方法进行的赋值,它的实现如下:
class_rw_ext_t *extAllocIfNeeded() {
    auto v = get_ro_or_rwe();
    if (fastpath(v.is<class_rw_ext_t *>())) {
        return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
    } else {
        return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
    }
}
  • 大概的意思是如果是rwe则返回,如果不是则创建。那么核心就是extAllocIfNeeded什么时候被调用了。

分类的加载

全局搜索下,结果如下:

    1. attachCategories方法中调用
    1. if (isRealized() || isFuture())中调用,也就是未来类调用
    1. class_setVersion设置版本的时候调用
    1. 在方法添加完成时addMethods_finish调用
    1. 添加协议class_addProtocol时调用
    1. 添加属性_class_addProperty时调用
    1. objc_duplicateClass重复的类中调用

通过这些调用,印证了WWDC2020中的内存,只有动态处理时系统才会对rwe进行处理,也就是运行时会加载分类

  • 由于签名走到最后进入了attachCategories方法,所以重心放到这个方法。

反向推导

  • 搜索attachCategories在哪些地方进行调用,于是就定位到了两个方法:load_categories_nolockattachToClass
attachToClass
  • 搜索attachToClass,发现只在MethodizeClass中调用,并且受previously参数约束,而签名分析过它传过来为nil,所以不会走进来
load_categories_nolock
  • 在研究前先定义一个WSPerson的分类
@interface WSPerson (App)

@property (nonatomic, copy) NSString *category_hobby;
@property (nonatomic, assign) NSInteger age;

- (void)app_categoryInstanceMethod1;
- (void)app_categoryInstanceMethod2;
+ (void)app_categoryClassMethod;

@end

@implementation WSPerson (App)

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

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

+ (void)app_categoryClassMethod {
    NSLog(@"%s", __func__);
}
@end 
  • 然后在load_categories_nolock中增加筛选WSPerson代码,然后运行,发现进不来。再断点在for循环处,发现count0,也就是根据没加载到类中?再来分析下load_categories_nolock的调用
反推
  • 全局搜索load_categories_nolock,发现有两处调用_read_imagesloadAllCategories,再继续搜索loadAllCategories,它知道load_images中调用。
_read_images中调用

read_images中调用的注释是是在启动是出现的分类,发现被推迟到_dyld_objc_notify_register调用完成后的第一个load_images调用,也就是一般不会调用。

load_images中调用

断点到load_images调用的loadAllCategories(),然后再到loadAllCategoriesload_categories_nolock处也加个断点,发现能走进来,但是此处依然没有加到类的分类列表,再类和分类都加上+load方法,再尝试,发现走到了load_categories_nolock的类筛选

分类加载流程
  • 综上所述,分类的调用流程如下:
    • load_images -> loadAllCategories -> load_categories_nolock -> attachCategories

问题:为什么类和分类加上+load方法,就可以走进load_categories_nolock方法,下一篇文章再探讨

Category

我们提到了分类,以及分类的加载流程,那么什么是分类呢,接下来去探究下

分类的本质

clang分类查看

  • clang查看C++源码:
static struct _category_t *_OBJC_LABEL_NONLAZY_CATEGORY_$[] = {
	&_OBJC_$_CATEGORY_WSPerson_$_App,
};
  • 在源码中,能够看到_category_t结构,里面能够看到WSPerson分类的名字是App_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; // 属性
};
  • _category_t结构体中
    • name是分类的名字,此处是App
    • cls是类的名字
    • instance_methodsclass_methods分别是对象方法,和类方法,为什么会有两个方法呢,是因为分类没有元类一说,所以会有两个变量分别记录对象方法和实例方法。
    • protocols是协议
    • properties是属性
  • 继续往下看,发现WSPerson_category_t结构: 截屏2021-07-21 16.41.55.png
  • _category_t第一个参数不是App么,怎么会是WSPerson,第二个参数也不是WSPerson,这是为什么?实际这是编译阶段,编译器并不知道他们是什么,所以填充个临时值,在运行时会重新进行赋值。

objc4-812中查看

  • 再与objc源码中查看下category 截屏2021-07-21 16.53.00.png
  • objc源码中和clang出来的C++代码的category结构基本一致

所以可以得到结论分类的本质是结构体

懒加载类和非懒加载类

疑问

  • 在探索方法进入realizeClassWithoutSwift前,由于在WSPerson中实现了+ load才能走进循环:
// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) {
    classref_t const *classlist = hi->nlclslist(&count);
    for (i = 0; i < count; i++) {
        Class cls = remapClass(classlist[i]);
        if (!cls) continue;

        addClassTableEntry(cls);
        ...
        realizeClassWithoutSwift(cls, nil);

    }
}
  • 这是为什么呢,注释上提示,这是个非懒加载类,只有实现了+load方法才能走进来realizeClassWithoutSwift,那么去掉+load能走进realizeClassWithoutSwift吗?我们先注释+load方法,然后再方法中添加WSPerson类的识别代码:
const char *mangledName = cls->nonlazyMangledName();
const char *person = "WSPerson";
if (strcmp(mangledName, person) == 0) {
    printf("🎈🎈🎈%s --- 要研究的: %s --- 是不是 元类:%d\n", __func__, mangledName, isMeta);
}
  • 再打印,发现也能走进来,查看堆栈,发现它不是通过read_images进来的: 截屏2021-07-21 10.07.41.png
  • 此刻恍然大悟,不实现+load时,方法加载会走到慢速查找,也就是发送消息的时候才会去加载,这也就是懒加载类

流程总结

  • 懒加载类加载流程:lookUpImpOrForward -> realizeAndInitializeIfNeeded_locked -> realizeClassMaybeSwiftAndLeaveLocked -> realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift -> methodizeClass
  • 非懒加载类加载流程:map_images -> map_images_nolock -> _read_images -> realizeClassWithoutSwift -> methodizeClass