类的加载原理
背景
- 在前面我们App的加载流程我们知道了dyld动态链接器的作用和原理,了解了map_images(),load_images(),那么他们是什么时候调用的又是如何执行的呢?
- 类是怎么加载的呢?类的方法,协议,属性,ro,rw如何加载的呢?类加载的慢速流程是什么?
- 懒加载,非懒加载的执行逻辑是什么呢?
- load_images() 的load方法,cxx + load + main 调用顺序是什么,又是如何调用的呢?
- dyld 到 主程序main的过程是什么?
- objc_init()里面的一些初始化函数调用,它们都做了什么?
准备
- 可编译的
objc4
工程 LLVM
源码Clang
环境dyld
源码
_objc_init分析:
- environ_init():环境变量初始化,主要调试时候使用
- tls_init():线程暂存缓存时,线程key的绑定,主要是本地线程池的初始化以及析构
- static_init():全局静态C++函数调用
- runtime_init():运行时机制两个表的初始化,主要分:分类、类相关的表初始化
- exception_init():初始化libobjc的异常处理系统,由map_images()调用
- cache_t::init():缓存初始化
- _imp_implementationWithBlock_init():初始化回调机制,通常这不会做什么,所有进程都是惰性初始化的,但对于某些进程,我们是主动加载的,libobjc-trampolines.dylib
- _dyld_objc_notify_register(&map_images, load_images, unmap_image):初始化注册dyld,处理map_images和load_images关联,dyld与objc的关联
- didCallDyldNotifyRegister = true:设置dyld寄存器通知
environ_init:
环境变量初始化,主要调试时候使用
- 核心代码:
- opt环境变量
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
_objc_inform("%s: %s", opt->env, opt->help);
}
- 第一个是disable关闭环境变量可以控制isa优化开关,从而优化整个内存结构,第二个load_method监控工程+(void)load方法使用场景
- 还有一种终端log打印,
export OBJC_HELP=1
export OBJC_HELP=1
tls_init
线程暂存缓存时,线程key的绑定,主要是本地线程池的初始化以及析构
static_init
根据头部注释和验证
- Run C++ static constructor functions.
- libc calls _objc_init() before dyld would call our static constructors,
- so we have to do it ourselves.
运行c++静态构造函数。
libc在dyld调用静态构造函数之前调用_objc_init()
所以我们必须自己做
runtime_init
运行时机制两个表的初始化,主要分:分类、类相关的表初始化
exception_init
初始化libobjc的异常处理系统。
由map_images()调用。
cache_t::init()
_imp_implementationWithBlock_init()
初始化回调机制,通常这不会做什么,所有进程都是惰性初始化的,但对于某些进程,我们是主动加载的,libobjc-trampolines.dylib
_dyld_objc_notify_register(&map_images, load_images, unmap_image)
初始化注册dyld,处理map_images和load_images关联,dyld与objc的关联:
//⚠️注意:仅供objc运行时使用
//注册当objc图像被映射,未映射和初始化时调用的处理程序。
// Dyld将回调"mapped"函数,它包含一个包含objc-image-info section的图像数组。
//那些dylibs的图像将会有refcount自动碰撞,所以objc将不再需要
//调用dlopen()来阻止它们被卸载。 在调用_dyld_objc_notify_register()时,
// dyld将调用已加载的objc图像的"mapped"函数。 在以后的任何dlopen()调用期间,
// dyld也会调用“mapped”函数。 当Dyld被调用时,Dyld将调用“init”函数
//初始化器。 这是当objc调用图像中的任何+load方法时。
didCallDyldNotifyRegister = true;
设置调用dyld寄存器通知,默认为true
dyld与objc关联
_dyld_objc_notify_register入口进入:
代码 -> 编译 -> macho -> 内存
registerObjcNotifiers
_dyld_objc_notify_register(&map_images, load_images, unmap_image)
&map_images 重要数据通过&进行指针传递,
load_images调用load方法,跟随map_images变化而同步变化
objc中map_images源码:
探索map_images_nolock,目标找到image的读取和映射(_read_images):
_read_images
⭐️
_read_images(),镜像文件的读取和映射
,代码过多关闭各判断代码块,从打印的log开始入手分析:
read_images函数内各模块含义:
1: 条件控制进行一次的加载
2: 修复预编译阶段的 @selector
的混乱问题
3: 错误混乱的类处理 4:修复重映射一些没有被镜像文件加载进来的 类 5: 修复一些消息!
6: 当我们类里面有协议的时候 : readProtocol 7: 修复没有被加载的协议
8: 分类处理
9: 类的加载处理
10 : 没有被处理的类 优化那些被侵犯的类
由于sel与sels[i]中的地址不同,因此要赋值。
类发生移动会把原始类删除,但有时候会删除不干净就会导致紊乱和野指针。
class通过readClass读取由编译器编写的类和元类。
*返回新的类指针。
readClass
分析
readClass读取由编译器编写的类和元类。
*返回新的类指针。 这可能是:
*——cls
-
- nil (cls缺少弱链接超类)
- -其他的东西(这个类的空间被一个未来的类保留)
*
*注意,所有由该函数执行的工作都由 - mustReadClasses()。 不要在没有更新那个函数的情况下更改这个函数。
*
*locking:由map_images或objc_readClassPair获取的runtimeLock
readClass->其中的addRemappedClass为哈希map结构
readClass->其中的addClassTableEntry添加类到表中
代码试探分析:
//测试代码
const char *mangledName = cls->nonlazyMangledName();
const char *LMPersonName = "LMPerson";
if (strcmp(mangledName, LMPersonName) == 0) {
//输出普通类
printf("%s -resolvedFutureClass--:--%s\n", __func__, mangledName);
}
realizeClassWithoutSwift(cls, nil)
类的实现分析
realizeClassWithoutSwift
*对类cls执行首次初始化,
*包括分配读写数据。
*不执行任何Swift端初始化。
*返回类的实际类结构。
*锁定:runtimeLock必须由调用方进行写锁定
ro->rw,ro赋值到rw的过程:
内容中:
- 继承链:
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
- isa走位:
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);
要注意元类和类的名字一样,因此需要判断isMeta
Class过滤掉元类的判断:
经过ro和rw赋值发现其实还是NULL,那么methodizeClass是不是有什么特殊处理呢?
methodizeClass
分析
realizeClassWithoutSwift->methodizeClass
fixupMethodList
然后进到fixupMethodList方法,此方法是为了method修复及方法排序
:
拿到sel设置到method,这样就可以正常打印了。
再然后经过下面排序:
// Sort by selector address.
// Don't try to sort small lists, as they're immutable.
// Don't try to sort big lists of nonstandard size, as stable_sort
// won't copy the entries properly.
if (sort && !mlist->isSmallList() && mlist->entsize() == method_t::bigSize) {
method_t::SortBySELAddress sorter;
std::stable_sort(&mlist->begin()->big(), &mlist->end()->big(), sorter);
}
处理完排序之后列表mlist->setFixedUp();
但是经过这一系列ro,rw及排序 操作,发现ro和rw还是打印NULL
小节:read_images
->readClass
(名字和类匹配)->realizeClassWithoutSwift
(ro ,rw,superclass,isa处理)->methodizeClass()
->prepareMethodLists
(写入方法名以及对method排序)
问题来了rwe什么时候赋值?
类与分类
- 1,非懒加载类 +非懒加载分类
非懒加载类 | 非懒加载分类 |
---|---|
_getObjc2NonlazyClassList、readClass、realizeClassWithoutSwift、 methodizeClass | load_categories_nolock、attachCategories、attachToClass |
一维数组结构 | 二维数组结构 |
- 2,懒加载类 + 懒加载分类 : 消息第一次调用 (加载数据)- 编译时期就完成
- 3,非懒加载类 + 懒加载分类 : read_image 就加载数据
- 4,懒加载类 + 非懒加载分类 : 迫使类成为非懒加载类
类 的 非懒加载与懒加载(+load)
- 看底层代码注释,如果写+load方法会走这些方法,那么我们跟进流程探索一下:
类别必须推迟,以避免潜在的竞争
其他线程之前调用新类别代码时
如果写了+load 会走 prepare_load_methods()处理加载
经lldb验证,类中去除+load方法的调用,就不会走上面这些流程的判断, 因此非懒加载
执行prepare_load_methods。 - 那么
懒加载
的按需加载是节省性能了,但怎么执行呢?
代码执行经过bt 看到走了消息慢速查找中的lookUpImpOrForward->realizeAndInitializeIfNeeded_locked
跟着流程走到了realizeClassMaybeSwiftMaybeRelock
里面同样有realizeClassWithoutSwift
方法
protocol_list_t *protolist = ro->baseProtocols; ro操作
分类与rwe赋值
- 了解分类
main文件经过Clang:
- _category_t结构体里的name是分类的名字
分类没有元类
- 分类定义的属性没有get和set,so需要关联对象
通过查看objc源码,看到Category数据结构:
- 那么分类是怎么加载到内存的呢?
通过在methodizeClass
方法里面有个attachToClass
- 而methodizeClass方法里面rwe=ext(),进入ext方法:
attachCategories分析
-
源码看
rwe跟extAllocIfNeeded方法开辟内存有关
,全局搜索源码中extAllocIfNeeded
-
- 在
attachCategories
方法里面,rwe在extAllocIfNeeded
里面赋值
- 在
-
- 表明rwe是在程序动态运行时处理赋值,
-
- 那么所有的重点就集中在了
attachCategories
方法
- 那么所有的重点就集中在了
-
- 全局搜索
attachCategories
->发现跟attachToClass
和load_categories_nolock
方法有关
- 全局搜索
-
- 我们检索
attachToClass
和load_categories_nolock
这两个方法:其中attachToClass
在methodizeClass
方法里面调用,而load_categories_nolock
在loadAllCategories
和_read_images
方法调用
- 我们检索
-
- Ro是CleanMemary,rw是DirtyMemary
把Ro复制到rw,rw中有脏内存但是其内存比较昂贵,因此需要优化rw而节省内存,而runtime(运行时),添加分类、api、method、协议、属性等等,并不是每个类都是动态添加,这个时候写在rw会浪费rw内存性能,因此就有了rwe,rwe对rw的扩展。
- Ro是CleanMemary,rw是DirtyMemary
attachLists分析
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();
}
}
- oldList:存放的是主类的方法指针列表,数组指针
- addLists:分类的方法指针列表
- 通过先一维数组遍历,然后一维转成二维数组,最后二维数组与二维数组遍历添加
类与分类小结:
- 分类在Clang时类名是与类的类名一样,但是在runtime时才会变化为相应的分类名
- 分类与类搭配加载:
1,两个都有+load方法时,_read_images 非懒加载类 -> realizeClassWithoutSwift -> load_categories_nolock -> attachCategories
2,主类中有load 分类没有load时,(data()里面获取),_read_images 懒加载类 -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> 没有走attachCategories
3,主类没有load 分类有load时,(data()里面获取),_read_images 懒加载类 -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> 没有走attachCategories
4,主类和分类都没有load时,推迟到runtime运行时 第一次消息发送的时候初始化 (data()里面获取)
5,多个分类,超过一个分类实现load时会迫使走attachCategories,如果创建一个分类里面没有任何实现时methodList里面并不会有这个没有任何实现的分类
类扩展vs分类
- 分类加载是否需要排序?
不用排序 - class_ro_t:指针地址数据格式什么样的?
通过格式数据结构赋值
通过LLVM源码
我们读取看到:
bool ClassDescriptorV2::class_ro_t::Read(Process *process, lldb::addr_t addr) {
size_t ptr_size = process->GetAddressByteSize();
size_t size = sizeof(uint32_t) // uint32_t flags;
+ sizeof(uint32_t) // uint32_t instanceStart;
+ sizeof(uint32_t) // uint32_t instanceSize;
+ (ptr_size == 8 ? sizeof(uint32_t)
: 0) // uint32_t reserved; // __LP64__ only
+ ptr_size // const uint8_t *ivarLayout;
+ ptr_size // const char *name;
+ ptr_size // const method_list_t *baseMethods;
+ ptr_size // const protocol_list_t *baseProtocols;
+ ptr_size // const ivar_list_t *ivars;
+ ptr_size // const uint8_t *weakIvarLayout;
+ ptr_size; // const property_list_t *baseProperties;
DataBufferHeap buffer(size, '\0');
Status error;
process->ReadMemory(addr, buffer.GetBytes(), size, error);
if (error.Fail()) {
return false;
}
DataExtractor extractor(buffer.GetBytes(), size, process->GetByteOrder(),
process->GetAddressByteSize());
lldb::offset_t cursor = 0;
m_flags = extractor.GetU32_unchecked(&cursor);
m_instanceStart = extractor.GetU32_unchecked(&cursor);
m_instanceSize = extractor.GetU32_unchecked(&cursor);
if (ptr_size == 8)
m_reserved = extractor.GetU32_unchecked(&cursor);
else
m_reserved = 0;
m_ivarLayout_ptr = extractor.GetAddress_unchecked(&cursor);
m_name_ptr = extractor.GetAddress_unchecked(&cursor);
m_baseMethods_ptr = extractor.GetAddress_unchecked(&cursor);
m_baseProtocols_ptr = extractor.GetAddress_unchecked(&cursor);
m_ivars_ptr = extractor.GetAddress_unchecked(&cursor);
m_weakIvarLayout_ptr = extractor.GetAddress_unchecked(&cursor);
m_baseProperties_ptr = extractor.GetAddress_unchecked(&cursor);
DataBufferHeap name_buf(1024, '\0');
process->ReadCStringFromMemory(m_name_ptr,
类扩展
eg:
@interface ViewController ()
@end
类扩展是在类的声明之后,实现之前
。通过clang我们可以看到是编译到主类中,跟主类的加载流程一样 而分类的底层是category_t结构体类型
类扩展具有跟主类同样的成员变量,属性,方法等。一般的私有属性或者想要独立区别的属性写到类扩展。
在_read_images断点:
关联对象
关联对象:意义是用来存储,因为分类没有属性,因此在set和get方法时需要关联对象来存储value。然后根据policy关联策略(retain,copy等),把cate_name和value关联
关联对象:底层分析(_object_set_associative_reference)
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
DisguisedPtr<objc_object> disguised{(objc_object *)object};
//
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
if (value) {
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
isFirstAssociation = true;
}
/* establish or replace the association */
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else {
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
associations.erase(refs_it);
}
}
}
}
}
// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue();
}
而其中AssociationsManager析构函数,
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;//作用域,全局变量
public:
AssociationsManager() { AssociationsManagerLock.lock(); }//构造函数
~AssociationsManager() { AssociationsManagerLock.unlock(); }//析构函数
AssociationsHashMap &get() {//单例
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
}
AssociationsHashMap单例,唯一的一张表
try_emplace:
InsertIntoBucket:插入bucket筒子,对Ptr赋值,insertIntoBucketImpl:3/4扩容并两倍扩容:
然后第二次进入try_emplace:
由于第一次进入try_emplace时刚创建插入bucket,都为空,因此第二次走try_emplace ObjectAssociationMap,在bucket里面插入ObjectAssociation,里面是key和value。
AssociationsManager ->AssociationsHashMap(Buckets 内部结构):
DisguisedPtr<objc_object> ObjectAssociationMap
DisguisedPtr<objc_object> ObjectAssociationMap
DisguisedPtr<objc_object> ObjectAssociationMap
….
-> ObjectAssociationMap(内部结构):
const void * ObjcAssociation
const void * ObjcAssociation
const void * ObjcAssociation
….
-> ObjectAssociation
uintptr_t _policy
id _value
….
AssociationsHashMap结构:
其中Buckets结构:
关联对象:设置流程
:
1: 创建⼀个 AssociationsManager 管理类
2: 获取唯⼀的全局静态哈希Map
3: 判断是否插⼊的关联值是否存在:
3.1: 存在⾛第4步
3.2: 不存在就⾛ : 关联对象插⼊空流程
4: 创建⼀个空的 ObjectAssociationMap 去取查询的键值对
5: 如果发现没有这个 key 就插⼊⼀个 空的 BucketT进去 返回
6: 标记对象存在关联对象
7: ⽤当前 修饰策略 和 值 组成了⼀个 ObjcAssociation 替换原来 BucketT 中的空
8: 标记⼀下 ObjectAssociationMap 的第⼀次为 false
关联对象:插⼊空流程
1: 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
2: 清理迭代器
3: 其实如果插⼊空置 相当于清除
关联对象:取值流程
1: 创建⼀个 AssociationsManager 管理类
2: 获取唯⼀的全局静态哈希Map
3: 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
4: 如果这个迭代查询器不是最后⼀个 获取 : ObjectAssociationMap (这⾥有策略和value)
5: 找到ObjectAssociationMap的迭代查询器获取⼀个经过属性修饰符修饰的value
6: 返回_value
总结:
其实就是两层哈希map , 存取的时候两层处理(类似⼆位数组)
总结分析
- 我们通过探索_objc_init函数知道了里面初始化内容的意义
- 通过分析_dyld_objc_notify_register(&map_images, load_images, unmap_image)我们知道map_images和load_images的关联
- 通过分析map_images知道了read_images中readClass读取由编译器编写的类和元类
- 通过对read_iamges->realizeClassWithoutSwift分析知道了类中ro和rw的处理
- dyld->images->内存->类(方法、协议、…)
- images(macho)->地址->表->类->初始化(rw-ro)->methodlist
- read_images->read_class(名字+类)->realizeClassWithoutSwift(ro-rw superclass isa)->methodizeClass()->prepareMethodLists(方法名写入+排序)