iOS- 18.类的加载(1)

911 阅读6分钟

ios底层文章汇总

1.镜像加载:map_images --> map_images_nolock --> _read_images

_objc_init注入回调_dyld_objc_notify_register(&map_images, load_images, unmap_image) 可知镜像的加载在map_images

调用栈: map_images --> map_images_nolock --> _read_images

read_images主要功能

    1. 条件控制进行一次的加载
    1. 修复预编译阶段的 @selector 的混乱问题
    1. 错误混乱的类处理
    1. 修复重映射一些没有被镜像文件加载进来的类
    1. 修复一些消息
    1. 当我们类里面有协议的时候 : readProtocol
    1. 修复没有被加载的协议
    1. 分类处理
    1. 类的加载处理
    1. 没有被处理的类 优化那些被侵犯的类

2 类的加载

在_read_images函数中获取到classlist后进入循环,断点调试,在readclass之前打印cls,cls中没有名字,但经过readclass后再打印cls,发现cls已经有名字了,说明readclass就是类加载的关键

注:怎么找重点----断点调试,判断和循环里设置断点,看程序会不会进断点,如果不进,可以直接过,只关注程序的运行流程

2.1 readClass分析

断点调试

  • 定义 LGPerson
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@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

NS_ASSUME_NONNULL_END



#import "LGPerson.h"

@implementation LGPerson

//+ (void)load{
//    
//}

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

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

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

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


@end

在main.m中调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person kc_instanceMethod1];
        NSLog(@"%p",person);
    }
    return 0;
}

在readClass函数的if和for中设置断点,重点关注类的加载部分

在readClass函数里并没有进入类的ro,rw赋值的if中,而只是类名称的关联addNamedClass和将类添加到表中addClassTableEntry

类的加载过程(ro,rw,rwe的赋值)在什么时候进行的?

realizeClassWithoutSwift在类加载实现之前,是以macho结构data形式存在的

app在使用类时,是需要在磁盘中app的二进制文件中读取类的信息,二进制文件中的类存储了类的元类、父类、flags和方法缓存

  • class_ro_t简称ro:read only,将类从磁盘中读取到内存中就是对ro的赋值操作。由于ro是只读的,加载到内存后不会发生改变又称为clean memory(干净内存)。

  • class_rw_t简称rw:read write,用于读写编写程序。 drity memory 在进程运行时发生更改的内存。类一经使用运行时就会分配一个额外的内存,那么这个内存变成了drity memory。

  • class_rw_ext_t简称rwe:read write ext,用于运行时存储类的方法、协议和实例变量等信息。在实际应用中,类的使用量只是10%,这样就在rw中造成了内存浪费,所以苹果就把rw中的方法、协议和实例变量等放到了class_rw_ext_t中

  • 获取ro

 const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>()->ro;//如果存在rwe,则从rwe中取ro
        }
        return v.get<const class_ro_t *>();//如果不存在rwe,则从rw中取ro
    }
  • 什么时候需要rwe 在分析attachCategrories的源码时,知道rwe = cls->data()->extAllocIfNeeded()可知extAllocIfNeeded()是开辟rwe 我们可以直接全部搜索extAllocIfNeeded,看那些地方可以是开发者控制的

    • attachCategories 添加分类
    • addMethod 添加方法
    • class_addProtocol 添加协议
    • _class_addProperty 添加属性

2.2 realizeClassWithoutSwift(类的加载)调用时机

  • 回到_read_images函数,往函数下文看,看到一个熟悉的函数realizeClassWithoutSwift,从函数名就可知道是实现类,断点验证下---·但在_read_images中并没有调用realizeClassWithoutSwift

  • 进入到realizeClassWithoutSwift的源码中查看,确实是类的实现操作 从类的MachO结构中读取到data(),给ro和rw赋值

  • 可以在 reaizeClassWithoutSwift 设置断点,并定位需要关注的类,探索什么时候调用reaizeClassWithoutSwift

  • 断点执行顺序,可知是在main函数中调用 LGPerson *person = [LGPerson alloc]之后,才调用reaizeClassWithoutSwift, 其调用堆栈为

  • reaizeClassWithoutSwift的调用时机是在消息发送的慢速查找lookUpImpOrForward中, 消息发送之前会先判断类是否实现,cls = initializeAndLeaveLocked(cls, inst, runtimeLock)----这是OC中著名的懒加载机制,将类的加载推迟到第一次方法调用的时候

问题 : 什么时候从_read_images中的realizeClassWithoutSwift函数来实现类的加载呢? 我们可以在LGPerson中实现+load函数,再断点_read_images中的realizeClassWithoutSwift函数,可以看到断点可以卡主,在进入main函数之前就进行了LGPerson类的加载

2.3 懒加载类与⾮懒加载类 — 当前类是否实现 load ⽅法

  • 懒加载类情况 数据加载推迟到第⼀次消息的时候(方法的慢速查找时判断类是否实现)

调用堆栈: [LGPerson alloc] --> objc_alloc -->callAlloc --> _objc_msgSend_uncached -->lookUpImpOrForward -->initializeAndLeaveLocked-->initializeAndMaybeRelock-->realizeClassMaybeSwiftAndUnlock-->realizeClassMaybeSwiftMaybeRelock --> realizeClassWithoutSwift

  • ⾮懒加载类情况 map_images的时候 加载所有类数据 调用栈: _dyld_start --> _objc_init --> _dyld_objc_notify_register --> dyld::registerObjcNotifiers --> dyld::notityBatchPartial --> map_images -->map_images_nolock --> _read_images --> realizeClassWithoutSwift

3 methodizeClass分析(方法化当前的类)

realizeClassWithoutSwift函数类的data加载完成后,会进入methodizeClass函数对方法、属性、协议的处理和排序等 ,关键函数调用栈:methodizeClass --> prepareMethodLists --> fixupMethodList

4 load_images解析

load_images方法的主要作用是加载镜像文件,其中最重要的有两个方法:prepare_load_methods(加载) 和 call_load_methods(调用)

image.png

  • 进入prepare_load_methods源码

    • 进入_getObjc2NonlazyClassList -> schedule_class_load源码,这里主要是根据类的继承链递归调用获取load,直到cls不存在才结束递归,目的是为了确保父类的load优先加载
    • 进入add_class_to_loadable_list,主要是将load方法和cls类名一起加到loadable_classes表中
    • _getObjc2NonlazyCategoryList -> realizeClassWithoutSwift -> add_category_to_loadable_list ,主要是将非懒加载分类的load方法加入表中
    • 进入add_category_to_loadable_list实现,获取所有的非懒加载分类中的load方法,将分类名+load加入表loadable_categories

image.png

  • 进入call_load_methods源码,主要有3部分操作

    • 反复调用类的+load,直到不再有
    • 调用一次分类的+load
    • 如果有类或更多未尝试的分类,则运行更多的+load
    • 进入call_class_loads,主要是加载类的load方法, 其中load方法中有两个隐藏参数,第一个为id 即self,第二个为sel,即cmd
    • call_category_loads,主要是加载一次分类的load方法

image.png

总结:

image.png

类方法和分类方法重名 调用哪个?

  • 如果同名方法是普通方法,包括initialize -- 先调用分类方法

    • 因为分类的方法是在类realize之后 attach进去的,插在类的方法的前面,所以优先调用分类的方法(注意:不是分类覆盖主类!!)
    • initialize方法什么时候调用? initialize方法也是主动调用,即第一次消息时调用,为了不影响整个load,可以将需要提前加载的数据写到initialize
  • 如果同名方法是load方法 -- 先 主类load,后分类load(分类之间,看编译的顺序)

image.png