iOS - 19.类的加载(2)- 分类

347 阅读8分钟

ios底层文章汇总

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

一般方法调用分类的,+load方法先调用主类,再调用分类的load方法

1 引入-分类

接着iOS- 18.类的加载(1),给LGPerson添加一个分类LG

#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>

// 分类 怎么研究?  clang -rewrite-objc main.m -o main.cpp

@interface LGPerson (LG)

@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, copy) NSString * cate_age;

- (void)cate_instanceMethod;
+ (void)cate_ClassMethod;

@end

@implementation LGPerson (LG)

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

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


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

2 分类的本质

2.1 clang编译main.m

clang -rewrite-objc main.m -o main.cpp

可知分类的本质是一个 category_t的结构

LGPerson (LG)分类编译成C++为

其中instance_methods为实例方法列表是一个method_t的数组_OBJC_$_CATEGORY_INSTANCE_METHODS_LGPerson_$_LG

其中的class_methods为类方法列表是一个method_t的数组_OBJC_$_CATEGORY_CLASS_METHODS_LGPerson_$_LG

其中properties为属性列表_OBJC_$_PROP_LIST_LGPerson_$_LG 分类中定义的属性在底层编译中并没有属性,因为分类中定义的属性只定义了相应的set、get方法,没有定义实例变量可以通过关联对象来设置

2.2 帮助文档搜索Catagory

可知分类的是一个struct

typedef struct objc_category *Category;

2.3 在objc源码中搜索 category_t

可查看struct category_t的结构

2.4 分类的本质 是一个_category_t的结构体

成员列表:

  • name : 类的名称

  • cls: 类对象

  • instance_methods: 实例方法列表

  • class_methods:类方法列表

  • protocols:协议列表

  • properties:属性列表(没有set/get方法,需要通过关联对象来实现)

3 分类的加载

3.1 定义类

//类的定义 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

@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

3.2 定义分类LGA

#import "LGPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson (LGA)


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

@end

NS_ASSUME_NONNULL_END


@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

3.3 定义分类LGB

#import "LGPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson (LGB)
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end

NS_ASSUME_NONNULL_END



@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

3.4 源码分析methodizeClass

查看objc源码的methodizeClass函数实现,可以发现类的加载和分类的加载时分开处理的,主要是因为在编译阶段类的数据data指针已经分配完成,等待类的加载时将指针数据拷贝到内存中,形成Macho结构数据,而分类是后面attachToClass->attachCategories到类的

3.5 源码分析attachCategories

3.6 什么时候进行分类数据加载的?

分析attachCategories的源码,可知进行分类数据的加载,可以反推,看什么时候调用attachCategories,就知道分类是什么时候进行分类数据加载的 全局搜索attachCategories,可查找到两个方法中有调用: load_categories_nolockaddToClass

  • load_categories_nolock 全局搜索load_categories_nolock的调用,有两处调用:

    • loadAllCategories方法中调用load_categories_nolock
    • _read_images方法中调用load_categories_nolock

断点调试发现不会走_read_images方法中的if流程的,而是走的loadAllCategories方法调用load_categories_nolock。

调用堆栈为: load_images -> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories->attachLists

💐 类非懒加载(实现+load方法) + 分类非懒加载(实现+load方法)

分类有实现+load方法(非懒加载分类) ,在attachCategories中加定位类的的断点,查看调用栈,

分类加载到内存的流程为:load_images -> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories->attachLists

类的数据从machO加载到内存的调用流程为: map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass

由此我么可以知道attachCategories的调用有两个分支

尝试去掉分类中的+load方法实现(懒加载分类),在attachCategories中加定位类的断点,查看调用堆栈

💐 类非懒加载(实现+load方法) + 分类懒加载(不实现+load方法)

打开realizeClassWithoutSwift中的自定义断点,查看kc_rokc_ro->baseMethodList

打印baseMethodList,从输出可以看出,方法的顺序是 LGB—LGA-LGPerson类,此时分类方法已经加载进来了,但是还没有排序,在类的加载_read_images时,通过cls->data读取Mach-O数据分类数据就已经编译进来了不需要运行时动态添加

(lldb) p kc_ro->baseMethodList
(method_list_t *const) $0 = 0x0000000100008048
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 16
    first = {
      name = "kc_instanceMethod1"
      types = 0x0000000100003dc8 "v16@0:8"
      imp = 0x0000000100003b80 (KCObjc`-[LGPerson(LGB) kc_instanceMethod1])
    }
  }
}
(lldb) p $0->get(0)
(method_t) $2 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003b80 (KCObjc`-[LGPerson(LGB) kc_instanceMethod1])
}
(lldb) p $0->get(1)
(method_t) $3 = {
  name = "cateB_2"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003bb0 (KCObjc`-[LGPerson(LGB) cateB_2])
}
(lldb) p $0->get(2)
(method_t) $4 = {
  name = "cateB_1"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003be0 (KCObjc`-[LGPerson(LGB) cateB_1])
}
(lldb) p $0->get(3)
(method_t) $5 = {
  name = "cateB_3"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003c10 (KCObjc`-[LGPerson(LGB) cateB_3])
}
(lldb) p $0->get(4)
(method_t) $6 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003920 (KCObjc`-[LGPerson(LGA) kc_instanceMethod1])
}
(lldb) p $0->get(5)
(method_t) $7 = {
  name = "cateA_2"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003950 (KCObjc`-[LGPerson(LGA) cateA_2])
}
(lldb) p $0->get(6)
(method_t) $8 = {
  name = "cateA_1"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003980 (KCObjc`-[LGPerson(LGA) cateA_1])
}
(lldb) p $0->get(7)
(method_t) $9 = {
  name = "cateA_3"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x00000001000039b0 (KCObjc`-[LGPerson(LGA) cateA_3])
}
(lldb) p $0->get(8)
(method_t) $10 = {
  name = "kc_instanceMethod3"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003a20 (KCObjc`-[LGPerson kc_instanceMethod3])
}
(lldb) p $0->get(9)
(method_t) $11 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003a50 (KCObjc`-[LGPerson kc_instanceMethod1])
}
(lldb) p $0->get(10)
(method_t) $12 = {
  name = "kc_instanceMethod2"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003a80 (KCObjc`-[LGPerson kc_instanceMethod2])
}
(lldb) p $0->get(11)
(method_t) $13 = {
  name = "kc_name"
  types = 0x0000000100003dde "@16@0:8"
  imp = 0x0000000100003ab0 (KCObjc`-[LGPerson kc_name])
}
(lldb) p $0->get(12)
(method_t) $14 = {
  name = "setKc_name:"
  types = 0x0000000100003de6 "v24@0:8@16"
  imp = 0x0000000100003ae0 (KCObjc`-[LGPerson setKc_name:])
}
(lldb) p $0->get(13)
(method_t) $15 = {
  name = "kc_age"
  types = 0x0000000100003df1 "i16@0:8"
  imp = 0x0000000100003b10 (KCObjc`-[LGPerson kc_age])
}
(lldb) p $0->get(14)
(method_t) $16 = {
  name = "setKc_age:"
  types = 0x0000000100003df9 "v20@0:8i16"
  imp = 0x0000000100003b30 (KCObjc`-[LGPerson setKc_age:])
}
(lldb) p $0->get(15)
(method_t) $17 = {
  name = ".cxx_destruct"
  types = 0x0000000100003dc8 "v16@0:8"
  imp = 0x0000000100003b50 (KCObjc`-[LGPerson .cxx_destruct])
}
(lldb) p $0->get(16)
Assertion failed: (i < count), function get, file /Users/mac/Downloads/V9.0/V9_大师班课程资料/20201016-大师班-第13节课-类的加载下/01--课堂代码/001-分类分析/runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
(lldb) 

什么时候进行的方法排序?

来到methodizeClass方法中定位断点打开,进行探索

排序的规则:方法的name地址升序排列,name地址相同时后编译的分类的同名方法排在前面 排序源码跟踪:prepareMethodLists->fixupMethodList

为什么后编译的分类的同名方法排在前面?

算法原则:如果主类和已实现分类的方法已经完成了需要的功能,那么后实现的分类就没有必要实现同名方法了 分类实现同名方法,就是为了覆盖主类已实现的方法的功能

💐 类懒加载(不实现+load方法) + 分类懒加载(不实现+load方法)

在消息第一次调用时加载类和分类

💐 类懒加载(不实现+load方法) + 分类非懒加载(实现load方法)

这个会迫使类也是非懒加载 其中baseMethodList的count是8个(对象方法3个+属性的setget方法共4个+1个cxx方法 )只有主类的数据

⚠️ 当不配置环境变量时,多个分类就好多次进入load_categories_nolock,一个分类为非懒加载分类,所有分类都会非懒加载,只开辟一次rwe;如果配置环境变量OBJC_DISABLE_NONPOINTER_ISA = YES,则没有实现load方法的分类,仍为懒加载,不会提起加载

那为什么要懒加载呢?

类和分类的加载,有很多的代码,排序,临时变量,如果把所有的类都推迟到main函数启动之前,整个main函数的启动就会非常非常的慢,也有可能实现写进的这个类,从来都没有被调用过,造成内存资源的浪费,还有必要去耗费内存嘛?这也是为什么+(void)load方法尽量不要随便写的原因了。

苹果默认是懒加载类,但给了开发人员更多自由

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

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

类的数据从machO加载到内存(类的实现流程)的调用流程为:map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass

分类加载到内存的流程为:load_images -> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories->attachLists -> attachCategories

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

只要分类实现了load,会迫使主类提前加载,在_read_images中不会对类做实现操作,需要在load_images方法中触发类的数据加载,即rwe初始化,同时加载分类数据

类加载到表中,数据未加载,类未实现:_read_images -> readClass-> addNamedClass & >addClassTableEntry

类的实现和分类加载流程为: load_images -> prepare_load_methods -> >realizeClassWithoutSwift类的实现 -> methodizeClass -> >objc::unattachedCategories.attachToClass -> attachToClass分类的attach

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

流程为: lookUpImpOrForward -> initializeAndLeaveLocked -> initializeAndMaybeRelock -> realizeClassMaybeSwiftAndUnlock -> realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift类的实现 -> methodizeClass分类加载 -> attachToClass

  • 非懒加载类 + 懒加载分类read_image就加载数据,数据来自data,data在编译时期就已经完成,通过cls->data读取Mach-O数据分类数据就已经编译进来了,分类的数据与类绑定在一起,不需要运行时动态添加,也不需要在load_images中进行操作

类和分类的加载流程:map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass--> prepareMethodLists(此时分类数据已经attach到类了)--> objc::unattachedCategories.attachToClass

分析总结:

懒加载的分类在编译期,数据就已经在data中,在运行时不会处理;

非懒加载的分类在编译期不处理,会推迟到运行时额外来添加处理

4 attachLists添加方法的算法逻辑

attachCategories --> rwe->methods.attachLists