类方法和分类方法重名 调用哪个?
一般方法调用分类的,+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_nolock
和 addToClass
-
load_categories_nolock
全局搜索load_categories_nolock的调用,有两处调用:
- loadAllCategories方法中调用load_categories_nolock
- _read_images方法中调用load_categories_nolock
- loadAllCategories方法中调用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_ro
和kc_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