通过这篇文章可以获得什么
- 分类的编译中间产物.cpp文件内的结构怎样构成的?
- 分类在objc4源码的结构体是怎样构成的?
- 分类是如何被加载的?
- 分类加载的核心因素rwe是何时被赋值的?
- 类在加载的过程中为什么会有rwe这个东西,其作用是什么?
- 分类是何时被加载的?
- attchCategories与attachLists核心源码分析
- attachLists核心算法图解
- 分类加载时机实验
探索概览
分类的结构
探索方式
- 将文件编译成
.cpp
,来查看 - 直接通过
objc4
的源码来查看
案例代码(main.m)
@interface FFPerson (AA)
@property (nonatomic, copy) NSString * cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
+ (void)cate_classMethod3;
@end
@implementation FFPerson (AA)
- (void)cate_instanceMethod1 {
NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod2 {
NSLog(@"%s",__func__);
}
+ (void)cate_classMethod3 {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
}
复制代码
将mian.m编译成mian.cpp
terminal指令
clang rewrite-objc main.m -o main.cpp
复制代码
main.cpp
截取部分跟category
相关的.cpp代码
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_FFPerson_$_AA,
};
static struct _category_t _OBJC_$_CATEGORY_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"FFPerson",
0, // &OBJC_CLASS_$_FFPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_FFPerson_$_AA,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_FFPerson_$_AA,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_FFPerson_$_AA,
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"cate_instanceMethod1", "v16@0:8", (void *)_I_FFPerson_AA_cate_instanceMethod1},
{(struct objc_selector *)"cate_instanceMethod2", "v16@0:8", (void *)_I_FFPerson_AA_cate_instanceMethod2}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"cate_classMethod3", "v16@0:8", (void *)_C_FFPerson_AA_cate_classMethod3}}
};
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"cate_name","T@\"NSString\",C,N"},
{"cate_age","Ti,N"}}
};
复制代码
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;
};
复制代码
objc4关于category_t源码
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
复制代码
通过编译的中间层代码.cpp文件于objc4底层源码比较的到结论:
name
代表的是分类
名字。分类不区分类方法于实例方法
,本质类加载过程分类属于插入数据,实例方法插入到类中,类方法插入到元类中,所以自己并不会有分元类。- 分类的属性并
不会自动生成getter、setter方法
,这在.cpp文件中可以得知。
分类是如何被加载的
在类的加载的时候最终都会走到methodizeClass
方法,此函数内有一段获取rwe
的操作,但是,rwe是在什么时候创建的就不得而知了。
static void methodizeClass(Class cls, Class previously)
{
auto rwe = rw->ext();
}
复制代码
class_rw_ext_t *ext() const {
return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
}
复制代码
rwe在什么时候赋值的
关键函数
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));
}
}
复制代码
通过搜索哪些函数调用了此函数
- static void
attachCategories
(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count, int flags) - static void
addMethods_finish
(Class cls, method_list_t *newlist) - BOOL
class_addProtocol
(Class cls, Protocol *protocol_gen) - static bool
_class_addProperty
(Class cls, const char *name, const objc_property_attribute_t *attrs, unsigned int count, bool replace) - Class
objc_duplicateClass
(Class original, const char *name, size_t extraBytes)
通过上述结果得知在附加分类
、给类添加方法
、给类添加协议
、重复类
等方法的时候会触发extAllocIfNeeded,也就是说在上述这些现象的时候ext会开辟空间了,也侧面验证了WWDC2020关于runtime的优化内Ben讲到的当类需要动态加载的时候才会触发rwe。
为什么会有rwe
通过objc4源码
我验证了类的加载其实是rw、ro装载
的过程,在内存中将数据拿出来,通过类型的强制转换
,再使用rw->set_ro(ro)
,将ro装载到rw内,然后使用cls->setData(rw)
将rw装载进class内,这样类的加载有完美的完成了。那么为什么会有rwe
,因为rw在使用的过程中属于dirty memory
,是动态的,这导致了使用rw的代价非常的高
,这里也就是解释了为什么Ben为什么讲rw的使用是非常昂贵的
,这里的昂贵指的内存的价值
,是money。这是必要拆分就变得尤为的重要,讲rw中动态的部分拆分出来。
拆分前:
拆分后:
那么分类的加载主要的探索方向就是这个rwe
分类是何时被加载的
通过rwe在什么时候赋值的我得到了一定会触发attachCategories函数
- 那么探索思路就变成了
attachCategories
何时被加载? attachCategories
被谁调用?调用顺序
是什么样的?
全局搜索attachCategories
线索一: attachToClass
-> attachCategories
线索二:load_categories_nolock
-> attachCategories
全局搜索attachToClass
调用attachToClass
只有methodizeClass
,OK,到这里就又找回来了,形成了闭环
,这里特别熟悉了,类加载的相关操作
关键源码:
// Attach categories.
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
// When a class relocates, categories with class methods
// may be registered on the class itself rather than on
// the metaclass. Tell attachToClass to look for those.
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
复制代码
通过这段源码可以得知进入attachToClass
一共有3个
入口,其中有2个入口受previously
这个变量控制,而这个变量是methodizeCLass
传递进来的参数,一路向上找去,最终得出结论只做objc4内部
调试使用。此时attachToClass
的调用范围降低至1个
,也就是objc::unattachedCategories.attachToClass(cls, cls,isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
核心源码分析
attchCategories
将方法列表、属性和协议从类别附加到类。 假设cats中的categories都加载了,并按加载顺序排序,最旧的类别在先。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/**
* 只有少数班级在发布时有超过 64 个类别。
* 这使用了一点堆栈,并避免了 malloc。
* 类别必须以正确的顺序添加,即从后到前。为了通过分块来做到这一点,我们迭代cats_list
* 从前到后,向后构建本地缓冲区,并在块上调用 attachLists。 attachLists 将列表放在前面,因此最终结果按预期顺序排列。
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "FFPerson") == 0)
{
if (!isMeta) {
printf("%s -FFPerson....\n",__func__);
}
}
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
//插入方法:重点探索
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
//方法的排序
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
//方法插入的核心算法
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
//插入属性:与method一致,没做研究
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
//插入协议与method一致,没做研究
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
复制代码
attachLists
分为三种情况:
0 lists -> 1 list
1 list -> many lists
many lists -> many lists
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
复制代码
lldb调试1 list -> many lists图解
结论:无论是
1 list -> many lists
还是many lists -> many lists
,数组结构本身都没有变化,都是一维的指针数组
,只是里面存放的指针指向的不同,有的直接指向method_t
,有的指针指向method_list_t
核心算法图解
总结:
- 如果当前
methodList不存在
,addedCount为1
的时候,list=addedLists[0]
,则当前methodList只有一个元素
。 - 当前存在
一个methodList
,将此list整体看作一个元素
,与category的method合并,category的method在新合成的methodList的0号位置
开始存放,oldMethod放在最后
,仅占用一个位置,此时该methodList末尾存放的指针为指向method_list_t的二级指针
,仅此一个特殊,其他的均指向method_t
,此时为1 list -> many lists
。 - 当前存在
一个methodList
,与category的method合并,Category的method在新合成的methodList的0号位置
开始存放,遍历oldMethod,在最末位置开始向前存放
,最终的结果依然是一个指针数组,本质上只是变成了元素更多的指针数组
,也就是说还是manyList
分类加载时机实验
实验代码
main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%s",__func__);
FFPerson *objc = [FFPerson alloc];
[objc likeGirls];
}
}
复制代码
FFPerson.h
@interface FFPerson : NSObject
- (void)likeFood;
- (void)likeLive;
- (void)likeSleep;
- (void)likeGirls;
+ (void)enjoyLife;
@end
复制代码
FFPerson.m
@implementation FFPerson
+ (void)load {
NSLog(@"%s",__func__);
}
- (void)likeFood {
NSLog(@"%s",__func__);
}
- (void)likeLive {
NSLog(@"%s",__func__);
}
- (void)likeSleep {
NSLog(@"%s",__func__);
}
- (void)likeGirls{
NSLog(@"%s",__func__);
}
+ (void)enjoyLife {
NSLog(@"%s",__func__);
}
@end
复制代码
FFPerson+BBLv.h
@interface FFPerson (BBLv)
- (void)likeGirls;
- (void)cate_bblv1;
- (void)cate_bblv2;
- (void)cate_bblv3;
- (void)cate_bblv4;
@end
复制代码
FFPerson+BBLv.m
@implementation FFPerson (BBLv)
+ (void)load {
NSLog(@"%s",__func__);
}
- (void)cate_bblv1 {
NSLog(@"%s",__func__);
}
- (void)cate_bblv2 {
NSLog(@"%s",__func__);
}
- (void)cate_bblv3 {
NSLog(@"%s",__func__);
}
- (void)cate_bblv4 {
NSLog(@"%s",__func__);
}
- (void)likeGirls{
NSLog(@"%s",__func__);
}
@end
复制代码
对FFPerson锁定,对部分关键函数插入打印代码
_read_images
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
const char *mangledName = cls->nonlazyMangledName();
const char *bblvPersonName = "FFPerson";
if (strcmp(mangledName, bblvPersonName) == 0) {
printf("Realize non-lazy classes -- %s -- BBLv -- %s\n",__func__,mangledName);
}
}
复制代码
realizeClassWithoutSwift
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "FFPerson") == 0)
{
if (!isMeta) {
printf("%s -FFPerson....\n",__func__);
}
}
}
复制代码
methodizeClass
static void methodizeClass(Class cls, Class previously)
{
bool isMeta = cls->isMetaClass();
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "FFPerson") == 0)
{
if (!isMeta) {
printf("%s -FFPerson....\n",__func__);
}
}
}
复制代码
attachToClass
void attachToClass(Class cls, Class previously, int flags)
{
bool isMeta = cls->isMetaClass();
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "FFPerson") == 0)
{
if (!isMeta) {
printf("%s -FFPerson....\n",__func__);
}
}
}
复制代码
load_categories_nolock(operator())
static void load_categories_nolock(header_info *hi)
{
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "FFPerson") == 0)
{
printf("%s -FFPerson....\n",__func__);
}
}
复制代码
attachCategories
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
bool isMeta = (flags & ATTACH_METACLASS);
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "FFPerson") == 0)
{
if (!isMeta) {
printf("%s -FFPerson....\n",__func__);
}
}
}
复制代码
上述对类与分类加载过程中必要的函数进行监听
,包括但不限于_read_images
、realizeClassWithoutSwift
、``methodizeClass、attachToClass
、load_categories_nolock
、attachCategories
。至此所有的准备工作都已经完毕。
验证阶段
load方法为类是否为懒加载
的关键因素,所以采用是否实现load方法
的方式来进行测试
验证一:FFPerson与FFPerson+BBLv均实现load方法
readClass -- BBLv -- FFPerson
Realize non-lazy classes -- _read_images -- BBLv -- FFPerson
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
operator() -FFPerson....
attachCategories -FFPerson....
2021-07-28 16:03:39.456705+0800 KCObjcBuild[6195:211286] +[FFPerson load]
2021-07-28 16:03:39.457140+0800 KCObjcBuild[6195:211286] +[FFPerson(BBLv) load]
2021-07-28 16:03:39.457257+0800 KCObjcBuild[6195:211286] main
复制代码
结论一:如果本类与分类都实现了load
方法,会执行attachCategories
关键函数,那么分类的加载方式为非懒加载方式
,即类的加载在main
函数之前
验证二:FFPerson实现load方法,FFPerson+BBLv不实现load方法
readClass -- BBLv -- FFPerson
Realize non-lazy classes -- _read_images -- BBLv -- FFPerson
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
2021-07-28 17:29:27.798125+0800 KCObjcBuild[6737:254965] +[FFPerson load]
2021-07-28 17:29:27.798714+0800 KCObjcBuild[6737:254965] main
2021-07-28 17:29:30.243474+0800 KCObjcBuild[6737:254965] -[FFPerson(BBLv) likeGirls]
复制代码
lldb调试图:
结论二:如果本类
FFPerson实现load方法
,分类FFPerson+BBLv不实现load方法
,在编译期间已经决定了类是非懒加载
,在编译期间
已经完成对方法列表的添加
,分类与主类的方法均在macho
中直接读取,不会执行attachCategories函数加载分类了。如果分类和主类又相同的方法,分类的方法在前
。
验证三:FPerson未实现load方法,FFPerson+BBLv实现load方法
readClass -- BBLv -- FFPerson
Realize non-lazy classes -- _read_images -- BBLv -- FFPerson
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
2021-07-28 18:10:19.204251+0800 KCObjcBuild[6923:272796] +[FFPerson(BBLv) load]
2021-07-28 18:10:19.204738+0800 KCObjcBuild[6923:272796] main
复制代码
lldb调试图
结论三:如果本类FFPerson未实现load方法
,分类FFPerson+BBLv实现了load方法
,在编译期间
已经决定了类是非懒加载
,这个时候就属于类被迫营业
,分类都实现了load方法,你主类必须跟。在编译期间已经完成对方法列表的添加,这个时候methodList的读取也是在macho
文件直接读取
验证四:FPerson、FFPerson+BBLv均未实现load方法
readClass -- BBLv -- FFPerson
2021-07-28 18:24:31.842952+0800 KCObjcBuild[7005:280092] main
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
2021-07-28 18:24:42.963068+0800 KCObjcBuild[7005:280092] -[FFPerson(BBLv) likeGirls]
复制代码
结论四:如果FPerson、FFPerson+BBLv均未实现load
方法,那么类的加载的方式将被定义为懒加载
,所有加载将会放在main函数
之后,也就是说,在类第一次发送消息的时候开始类的加载
。