普通视图
iOS底层之分类的加载
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序言
在前面文章《iOS底层之类的加载》中探索了类的加载流程,本篇将对分类展开探索,从分类的结构到分类的加载流程,来探索分类的本质。
Runtime优化
在《WWDC 2020 关于Runtime的优化》中介绍了关于Runtime的优化内容,核心内容是对rw扩展出rwe,来优化整个运行时的性能。
我们的应用程序装载到设备时,系统会为安装程序分配一段内存,这段内存是不可变的称为clean memory也就是ro,当程序运行启动时,系统会开辟新的内存来运行程序,这段内存是可变化的称之为dirty memory也就是rw,因为系统内存有限,所以rw这段内存是比较宝贵的。
但是在rw中的数据很多是不会改变的,直接从ro读取即可,需要改变的数据通过Runtime运行时操作的数据,比如类的方法、属性、协议等,将这些数据存放在rwe上,这样就可以达到对dirty memory的优化。
分类的意义就是要动态的给类添加方法等,那么分类的探索就从rwe入手。
分类的结构
自定义类LGPerson和分类LGPerson (Cat),然后通过clang生成cpp文件查看
@interface LGPerson : NSObject
{
NSString * name;
}
@property (nonatomic, copy) NSString * nickName;
- (void)instanceMethod;
+ (void)classMethod;
@end
@implementation LGPerson
- (void)instanceMethod {
NSLog(@"%s", __func__ );
}
+ (void)classMethod {
NSLog(@"%s", __func__ );
}
@interface LGPerson (Cat)
@property (nonatomic, copy) NSString * lg_nickName;
- (void)lg_categoryInstanceMethod;
+ (void)lg_categoryClassMethod;
@end
@implementation LGPerson (Cat)
- (void)lg_categoryInstanceMethod {
NSLog(@"%s", **__func__** );
}
+ (void)lg_categoryClassMethod {
NSLog(@"%s", **__func__** );
}
@end
主类的cpp代码
![]()
在
LGPerson的主类cpp中有实例方法、类方法、以及属性的set和get方法;
分类的cpp代码
![]()
到在分类中有
实例方法和类方法,并没有属性lg_nickName的set和get方法
让LGPerson (Cat)分类遵守协议NSObject,重新生成cpp
分类的类型是_category_t,通过category_t在源码中可以找到分类的结构定义
![]()
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
WrappedPtr<method_list_t, method_list_t::Ptrauth> 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;
}
};
-
name: 分类名称; -
cls:主类; -
instanceMethods:实例方法; -
classMethods: 类方法; -
protocols: 所遵守的协议; -
instanceProperties:实例属性,并没有set和get方法; -
_classProperties:类属性
通过
分类的结构可以看出,分类是没有元类的,主类的类方法是在元类中,分类的类方法是在classMethods中。
分类的加载
rwe是通过类中的extAllocIfNeeded方法创建,如果已有值直接返回rwe,如果没有则通过extAlloc创建。
![]()
在源码中全局搜索extAllocIfNeeded,调用的方法有attachCategories分类、class_setVersion设置版本、addMethods_finish动态添加方法、class_addProtocol添加协议、_class_addProperty添加属性、objc_duplicateClass类的复制、demangledName修改类名
这些方法中有关分类的只有attachCategories方法
attachCategories方法探究
// 将方法列表、属性和协议从类别附加到类。假设cats中的类别都是按加载顺序加载和排序的,最旧的类别优先。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
* 只有少数类在启动期间具有超过64个类别。这使用了一个小堆栈,并避免了malloc。
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
* 类别必须以正确的顺序添加,即从后到前。
* 为了实现分块,我们从前到后迭代cats_list,向后构建本地缓冲区,
* 并对块调用attachList。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(); // 初始化rwe
// cats_count分类数量,循环处理分类
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) { // 第一次mcount = 0
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__ );
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
//++mcount,将mlist放在mlists的最后
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
//++propcount,将proplist放在proplists的最后
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
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中添加方法
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中添加属性
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
// rwe中添加协议
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
分析一下方法
- 初始化
rwe;- 通过分类数量
cats_count,循环处理分类中的方法、属性、协议;- 循环中按倒序插入法,将所有的方法存在
mlists,协议存放在protolists,属性存放在proplists;- 如果
mcount大于0,说明有分类方法,通过prepareMethodLists向类中添加方法并排序,然后rwe中的methods调用attachLists,添加分类方法到rwe;rwe中的properties调用attachLists,添加分类属性到rwe;rwe中的protocols调用attachLists,添加分类协议到rwe;
rwe中的方法、属性和协议都是调用的attachLists插入数据,是因为他们的定义都继承list_array_tt,我们看一下list_array_tt中的attachLists方法
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; // 有旧值oldCount=1否则oldCount=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();
}
}
算法分析:根据已有数据做不同情况处理
-
0 -> 1:无数据且新插入数据为1条
- 直接插入到
list
-
1 list -> many lists: 只有1条数据或插入多条数据
- 取出旧值;
- 对
oldCount赋值:有旧值oldCount=1否则oldCount=0;- 计算插入后数据总量
newCount = oldCount + addedCount;- 开辟新空间;
- 将旧值存放在新空间最后;
- 根据
addedCount从前往后插入新值;
-
many lists -> many lists: 已有多条数据
- 取出旧值;
- 计算插入后数据总量
newCount = oldCount + addedCount;- 开辟新空间;
- 根据
oldCount循环从后往前取出旧值,存放在新空间的后面位置;- 根据
addedCount从前往后插入新值;
根据hasArray和setArray判断,只要调用setArray或array()->count中的count值大于0,hasArray即为YES。
![]()
所有分类加载后,rwe的methods结构应该为
![]()
分类加载实例探究
在类的加载探索中,我们知道了类的加载时机区分为懒加载类和非懒加载类,即是否实现+load方法,分类的加载我们同样按照懒加载和非懒加载的形式探索。
- 主类和分类都为懒加载;
- 主类非懒加载,分类懒加载;
- 主类懒加载,分类非懒加载;
- 主类和分类都非懒加载;
实例类为LGPerson,就在attachCategories方法中通过类名mangledName比较LGPerson来精确加断点、打印输出调试
![]()
1.主类和分类都为懒加载
主类LGPerson声明实例方法sayHello和sayByeBye
分类CatA中声明实例方法sayHello_A,以及重写主类方法sayHello
在main函数中调用sayHello
并没有进入attachCategories函数中,同时可以看到sayHello是调用的分类的方法,由此可以知道如果主类和分类都未实现+load方法,分类的方法等信息是在编译时就和主类编译在一起了,在类的加载流程中验证
在main函数中调用LGPerson的alloc方法开始加载类,这里的method_list_t是从ro中读取的数据,输出查看
![]()
此时,list中的方法数主类和分类的方法集合,主类方法放在集合的最后位置,但这里方法还没有经过排序处理,通过prepareMethodLists会对list进行排序修复。
通过断点跟进,再输出一下经过fixupMethodList处理后的list
处理后,主类的sayHello方法排在了分类sayHello后面。在调用sayHello时,消息查找流程在对排序好的list进行二分法查找,并且会通过while循环找到最前面的同名方法,这样分类方法就覆盖了主类方法。
![]()
2.主类非懒加载、分类懒加载
LGPerson的主类实现+load方法,分类不实现
在main函数中调用sayHello
也没有进入attachCategories函数中,sayHello是调用的分类的方法,由此可以知道如果主类非懒加载和分类懒加载,分类的方法等信息也是在编译时就和主类编译在一起了。
下面在类的加载流程中验证
这里可以看出
-
LGPerson是在程序启动时_read_images实现加载的, - 分类的方法也是
编译时就和主类方法编译在一起了,这里是通过ro获取的method_list_t,分类方法也在ro中。
3.主类懒加载、分类非懒加载
主类LGPerson不实现+load方法,分类实现
运行查看
也没有进入attachCategories函数中,sayHello是调用的分类的方法,由此可以知道如果主类懒加载和分类非懒加载,分类的方法等信息也是在编译时就和主类编译在一起了。
在类的加载流程中验证
![]()
-
LGPerson是在程序启动时_read_images实现加载的,分类非懒加载会导致主类被迫成为非懒加载类; - 分类的方法也是
编译时就和主类方法编译在一起了,这里是通过ro获取的method_list_t,分类方法也在ro中。
4.主类和分类都为非懒加载
主类LGPerson和分类CatA都实现+load方法,运行查看
![]()
通过输出打印可以看出,
主类的加载和分类的加载是在不同的流程中
主类加载
![]()
可以看出主类的加载是在
_read_images流程,这里从ro读取的method_list_t中只有主类的两个方法。
分类的加载
![]()
分类的加载流程是
load_images->loadAllCategories->load_categories_nolock->attachCategories,在attachCategories中会创建rwe,并调用prepareMethodLists对分类方法进行排序处理,然后调用rwe中methods的attachLists插入分类的mlist
这里的LGPerson的分类有3个,只在分类CatA中实现了+load方法,我们调用分类CatC的方法
![]()
发现加载分类的时候,并没有输出分类CatC的名字,也就是没有分类CatC的加载,为什么可以调用sayHello_C成功了呢?
猜测:分类
CatC没有实现+load,会不会是编译时和主类编译在一起了
再跟踪一下主类的加载流程
![]()
这里看到,主类
ro中只有主类自己实现的两个方法,所以猜想不成立。
再跟踪一下分类的加载流程
![]()
这里看到,在加载分类
CatB时,确有分类CatC的方法,他们的共同点是都没有实现+load,系统在编译时会把未实现+load方法的分类合并成一个分类来处理,从而简化分类的加载流程。
我们再增加一个分类CatD做验证
![]()
可以看到,分类加载时把未实现+load方法的CatB、CatC和CatD一起加载的,验证了系统在编译时会把未实现+load方法的分类合并成一个分类来处理。
总结
分类的加载原理和类同样区分为是否为懒加载,两者组合可分为4种情况
![]()
以上是对分类的加载流程探索过程的总结,难免有不足和错误之处,如有疑问请在评论区留言吧
iOS底层之类的加载
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序言
前面的文章中探究了类的结构,知道了类中都有哪些内容,那么今天就来探究一下,类到底是怎么加载进内存的呢?在什么时候加载到内存的呢?
我们定义的类.h和.m文件,首先需要通过编译器生产可执行文件,这个过程称为编译阶段,然后安装在设备上加载运行。
编译
- 预编译:编译之前的一些先前的处理工作,处理一些
#开头的文件,#include和#define以及条件编译等;- 编译:对预编译后的文件进行
词法分析、语法分析和语义分析,并进行代码优化,生成汇编代码;- 汇编:将汇编文件代码转换为机器可以执行的指令,并生成目标文件
.o;- 链接:将所有
目标文件以及链接的第三方库,链接成可执行文件macho;这一过程中,链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用的函数,比如我们常用的系统库。
![]()
动态库与静态库
- 静态库:链接阶段将汇编生成的目标文件和引用库一起链接打包到可执行文件中,如:
.a、.lib。- 优点:编译成功后可执行文件可以独立运行,不需要依赖外部环境;
- 缺点:编译的文件会变大,如果静态库更新必须重新编译;
- 动态库:链接时不复制,程序运行时由系统加载到内存中,供系统调用,如:
.dylib、.framework。- 优点:系统只需加载一次,多次使用,共用节省内存,通过更新动态库,达到更新程序的目的;
- 缺点:可执行文件不可以单独运行,必须依赖外部环境;
系统的framework是动态的,开发者创建的framework是静态的
![]()
dyld动态链接器
dyld是iOS操作系统的一个重要组成部分,在系统内核做好程序准备工作之后,会交由dyld负责余下的工作。dyld的作用:加载各个库,也就是image镜像文件,由dyld从内存中读到表中,加载主程序,link链接各个动静态库,进行主程序的初始化工作。
![]()
dyld负责链接、加载程序,但是dyld的探索过程比较繁琐就不详细展开了,直接进入类的加载核心_objc_init方法探索。
_objc_init探索
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init(); // 读取影响运行时的环境变量
tls_init(); // 关于线程key的绑定
static_init(); // 运行C++静态构造函数
runtime_init(); // runtime运行时环境初始化
exception_init();// 初始化libobjc库的异常处理
#if __OBJC2__
cache_t::init(); // 缓存条件初始化
#endif
_imp_implementationWithBlock_init(); // 启动回调机制
// 注册处理程序,以便在映射、取消映射和初始化objc图像时调用,仅供运行时Runtime使用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = **true**;
#endif
}
environ_init环境变量
/***********************************************************************
* environ_init
* Read environment variables that affect the runtime.
* Also print environment variable help, if requested.
************************************************************************ ** **/
void environ_init(void)
{
// 部分核心代码
// Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
if (PrintHelp || PrintOptions) {
if (PrintHelp) {
_objc_inform("Objective-C runtime debugging. Set variable=YES to enable.");
_objc_inform("OBJC_HELP: describe available environment variables");
if (PrintOptions) {
_objc_inform("OBJC_HELP is set");
}
_objc_inform("OBJC_PRINT_OPTIONS: list which options are set");
}
if (PrintOptions) {
_objc_inform("OBJC_PRINT_OPTIONS is set");
}
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
// if (opt->internal
// && !os_variant_allows_internal_security_policies("com.apple.obj-c"))
// continue;
if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
}
}
}
通过控制PrintHelp和PrintOptions可以打印当前环境变量的配置信息,我们在源码环境中把for循环代码复制出来改一下,运行
![]()
![]()
也可以通过终端命令export OBJC_HELP = 1,在终端上显示
![]()
可以通过Edit shceme,在Environment Variables配置相关变量
OBJC_DISABLE_NONPOINTER_ISA:isa的优化开关,如果YES表示不使用,就是存指针;如果NO开启指针优化,为nonpointer isa;OBJC_PRINT_LOAD_METHODS:是否开启打印所有load方法,可以判断哪些类使用了load方法,做相应的优化处理,以优化启动速度;
![]()
tls_init线程key的绑定
tls_init关于线程key的绑定,比如每个线程数据的析构函数
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
static_init运行C++静态构造函数
运行C++静态构造函数。
libc在dyld调用静态构造函数之前调用objc_init(),因此我们必须自己执行。
static void static_init()
{
size_t count1;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count1);
for (size_t i = 0; i < count1; i++) {
inits[i]();
}
size_t count2;
auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count2);
for (size_t i = 0; i < count2; i++) {
UnsignedInitializer init(offsets[i]);
init();
}
#if DEBUG
if (count1 == 0 && count2 == 0)
_objc_inform("No static initializers found in libobjc. This is unexpected for a debug build. Make sure the 'markgc' build phase ran on this dylib. This process is probably going to crash momentarily due to using uninitialized global data.");
#endif
}
runtime_init运行时环境初始化
Runtime运行时环境初始化,主要是unattachedCategories和allocatedClasses两张表的初始化
void runtime_init(void)
{
objc::disableEnforceClassRXPtrAuth = DisableClassRXSigningEnforcement;
objc::unattachedCategories.init(32); // 分类表
objc::allocatedClasses.init(); // 已开辟类的表
}
exception_init 异常系统初始化
初始化libobjc的异常处理系统,由map_images()调用。注册异常处理的回调,从而监控异常的处理
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
异常处理系统初始化后,当程序运行不符合底层规则时,比如:数组越界、方法未实现等,系统就会发出异常信号。
有异常发生时,uncaught_handler函数会把异常信息e抛出
![]()
uncaught_handler就是这里传进来的fn,这个fn就是我们检测异常的句柄。我们可以自定义异常处理类,通过NSSetUncaughtExceptionHandler把我们句柄函数地址传进去
有异常发生时,系统会回调给我们异常exception,然后自定义上传等处理操作。
![]()
cache_t::init缓存条件初始化
void cache_t::init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
mach_msg_type_number_t count = 0;
kern_return_t kr;
while (objc_restartableRanges[count].location) {
count++;
}
kr = task_restartable_ranges_register(mach_task_self(),
objc_restartableRanges, count);
if (kr == KERN_SUCCESS) return;
_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}
![]()
_imp_implementationWithBlock_init
通常情况下,这没有任何作用,因为所有的初始化都是惰性的,但对于某些进程,我们急切地加载trampolines dylib。
在某些过程中急切地加载libobjc-tropolines.dylib。一些程序(最著名的是早期版本的嵌入式Chromium使用的QtWebEngineProcess)启用了一个限制性很强的沙盒配置文件,该文件阻止对该dylib的访问。如果有任何东西调用imp_implementationWithBlock(正如AppKit已经开始做的那样),那么我们将在尝试加载它时崩溃。在这里加载它会在启用沙盒配置文件并阻止它之前设置它。
void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
// Eagerly load libobjc-trampolines.dylib in certain processes. Some
// programs (most notably QtWebEngineProcess used by older versions of
// embedded Chromium) enable a highly restrictive sandbox profile which
// blocks access to that dylib. If anything calls
// imp_implementationWithBlock (as AppKit has started doing) then we'll
// crash trying to load it. Loading it here sets it up before the sandbox
// profile is enabled and blocks it.
//
// This fixes EA Origin (rdar://problem/50813789)
// and Steam (rdar://problem/55286131)
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}
_dyld_objc_notify_register
void
_dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
_dyld_objc_notify_register中的三个参数含义如下
-
&map_images:dyld将image加载到内存中会调用该函数 -
load_images:dyld初始化所有的image文件会调用 -
unmap_image:将image移除时会调用
我们重点看的就是将image加载到内存中调用的函数map_image
在map_image中调用map_images_nolock
![]()
map_images_nolock中的代码比较多,我们这里直接看重点_read_images
_read_images解读
在_read_images方法中有360行代码,有点长,把里面的大括号折叠,苹果的代码流程和注释是很好的,可以先整体把握一下,里面的ts.log很清晰的告诉了我们整个流程。
void _read_images(header_info hList, uint32_t hCount, int
totalClasses, int
unoptimizedTotalClasses)
{
... //表示省略部分代码
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
// 条件控制进行一次的加载
if (!doneOnce) { ... }
// 修复预编译阶段的`@selector`的混乱的问题
// 就是不同类中有相同的方法 但是相同的方法地址是不一样的
// Fix up @selector references
static size_t UnfixedSelectors;
{ ... }
ts.log("IMAGE TIMES: fix up selector references");
// 错误混乱的类处理
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
for (EACH_HEADER) { ... }
ts.log("IMAGE TIMES: discover classes");
// 修复重映射一些没有被镜像文件加载进来的类
// Fix up remapped classes
// Class list and nonlazy class list remain unremapped.
// Class refs and super refs are remapped for message dispatching.
if (!noClassesRemapped()) { ... }
ts.log("IMAGE TIMES: remap classes");
#if SUPPORT_FIXUP
// 修复一些消息
// Fix up old objc_msgSend_fixup call sites
for (EACH_HEADER) { ... }
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
// 当类中有协议时:`readProtocol`
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) { ... }
ts.log("IMAGE TIMES: discover protocols");
// 修复没有被加载的协议
// Fix up @protocol references
// Preoptimized images may have the right
// answer already but we don't know for sure.
for (EACH_HEADER) { ... }
ts.log("IMAGE TIMES: fix up @protocol references");
// 分类的处理
// Discover categories. Only do this after the initial category
// attachment has been done. For categories present at startup,
// discovery is deferred until the first load_images call after
// the call to _dyld_objc_notify_register completes.
if (didInitialAttachCategories) { ... }
ts.log("IMAGE TIMES: discover categories");
// 类的加载处理
// Category discovery MUST BE Late to avoid potential races
// when other threads call the new category code befor
// this thread finishes its fixups.
// +load handled by prepare_load_methods()
// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) { ... }
ts.log("IMAGE TIMES: realize non-lazy classes");
// 没有被处理的类,优化那些被侵犯的类
// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) { ... }
ts.log("IMAGE TIMES: realize future classes");
...
#undef EACH_HEADER
}
- 条件控制,进行一次加载;
- 修复预编译阶段的
@selecter混乱问题;- 错误混乱的类处理;
- 修复重新映射一些没有被镜像文件加载进来的类;
- 修复一些
消息;- 当类里面有协议的时候:
readProtocol;- 修复没有被加载进来的协议;
- 分类处理;
- 类的加载处理;
- 没有被处理的类
doneOnce加载一次
if (!doneOnce) {
doneOnce = YES;
launchTime = YES;
#if SUPPORT_NONPOINTER_ISA
// Disable non-pointer isa under some conditions.
# if SUPPORT_INDEXED_ISA
// Disable nonpointer isa if any image contains old Swift code
for (EACH_HEADER) {
if (hi->info()->containsSwift() &&
hi->info()->swiftUnstableVersion() < objc_image_info::SwiftVersion3)
{
DisableNonpointerIsa = true;
if (PrintRawIsa) {
_objc_inform("RAW ISA: disabling non-pointer isa because "
"the app or a framework contains Swift code "
"older than Swift 3.0");
}
break;
}
}
# endif
#endif
if (DisableTaggedPointers) {
disableTaggedPointers();
}
// 小对象地址混淆
initializeTaggedPointerObfuscator();
if (PrintConnecting) {
_objc_inform("CLASS: found %d classes during launch", totalClasses);
}
// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
// 容量:总数 * 4 / 3
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
// 创建哈希表,用于存放所有的类
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
ts.log("IMAGE TIMES: first time tasks");
}
通过对doneOnce的判断,只会进来一次条件语句,这里主要处理对类表的开辟创建处理gdb_objc_realized_classes。
修复@selecter混乱问题
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->hasPreoptimizedSelectors()) continue;
bool isBundle = hi->isBundle();
// 从macho文件中获取方法名列表
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
// sel通过name从dyld中查找获取
SEL sel = sel_registerNameNoLock(name, isBundle);
if (sels[i] != sel) { // 修复地址,以dyld为准
sels[i] = sel;
}
}
}
}
ts.log("IMAGE TIMES: fix up selector references");
对sel进行修复,因为从编译后的macho读取的sel地址不一定是真实的sel地址,在这里做修复。
错误混乱的类处理
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
for (EACH_HEADER) {
if (! mustReadClasses(hi, hasDyldRoots)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
// 从macho中读取的类列表
classref_t const *classlist = _getObjc2ClassList(hi, &count);
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
// 通过readClass,将cls的类名和地址做关联
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
// 如果不一致,则加入修复
if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
ts.log("IMAGE TIMES: discover classes");
- 通过
_getObjc2ClassList从macho中读取的所有的类; - 遍历所有的类,通过
readClass讲类的地址和类名关联;
![]()
popFutureNamedClass返回已实现的类,我们添加的类未实现,这里if语句不成立;mangledName是有值的,调用addNamedClass将name=>cls添加到命名的非元类映射中。addClassTableEntry:将类添加到所有类的表中。如果addMeta为true,则自动添加类的元类。- 返回已处理的类
可以看出readClass函数是把传进来的cls,重新映射并添加cls和其元类到所有的类表中。
修复重新映射的类
类列表和非懒加载类列表仍然未被添加。 类引用和父类引用被重新映射以用于消息调度。
if (!noClassesRemapped()) {
for (EACH_HEADER) {
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// fixme why doesn't test future1 catch the absence of this?
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
}
ts.log("IMAGE TIMES: remap classes");
修复一些消息
// Fix up old objc_msgSend_fixup call sites | 修复旧的objc_msgSend_fixup调用站点
for (EACH_HEADER) {
message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
if (count == 0) continue;
if (PrintVtables) {
_objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
"call sites in %s", count, hi->fname());
}
for (i = 0; i < count; i++) {
// 内部将常用的alloc、objc_msgSend等函数指针进行注册,并fix为新的函数指针
fixupMessageRef(refs+i);
}
}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
![]()
添加协议
当类里面有协议的时候,调用readProtocol绑定协议
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
ASSERT(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->hasPreoptimizedProtocols();
// Skip reading protocols if this is an image from the shared cache
// and we support roots
// Note, after launch we do need to walk the protocol as the protocol
// in the shared cache is marked with isCanonical() and that may not
// be true if some non-shared cache binary was chosen as the canonical
// definition
if (launchTime && isPreoptimized) {
if (PrintProtocols) {
_objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
hi->fname());
}
continue;
}
bool isBundle = hi->isBundle();
protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
ts.log("IMAGE TIMES: discover protocols");
![]()
修复协议列表引用
上面做了协议和类的关联,这里是对协议进行重新映射
![]()
分类的处理
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
ts.log("IMAGE TIMES: discover categories");
分类的流程是比较重要的,在《iOS底层之分类的加载》专讲一下。
类的加载处理
// Realize non-lazy classes (for +load methods and static instances)
// 实现非懒加载类(实现了+load或静态实例方法)
for (EACH_HEADER) {
// 通过_getObjc2NonlazyClassList获取所有非懒加载类
classref_t const *classlist = hi->nlclslist(&count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
// 再次添加到所有类表中,如果已添加就不会添加进去,确保整个结构都被添加
addClassTableEntry(cls);
if (cls->isSwiftStable()) {
if (cls->swiftMetadataInitializer()) {
_objc_fatal("Swift class %s with a metadata initializer "
"is not allowed to be non-lazy",
cls->nameForLogging());
}
// fixme also disallow relocatable classes
// We can't disallow all Swift classes because of
// classes like Swift.__EmptyArrayStorage
}
// 对类cls执行首次初始化,包括分配其读写数据。不执行任何Swift端初始化。
realizeClassWithoutSwift(cls, **nil**);
}
}
ts.log("IMAGE TIMES: realize non-lazy classes");
本文重点
- 调用
nlclslist(里面是调用_getObjc2NonlazyClassList)获取所有非懒加载(non-lazy)的类;- 循环实现,再次添加到所有的类表中,如果已添加就不会添加进去,确保整个结构都被添加;
- 调用
realizeClassWithoutSwift对类cls执行首次初始化,包括分配其读写数据;
通过realizeClassWithoutSwift实现所有非懒加载类的第一次初始化,那我们就看realizeClassWithoutSwift如何实现的
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
runtimeLock.assertLocked();
class_rw_t *rw;
Class supercls;
Class metacls;
if (!cls) return nil;
if (cls->isRealized()) {
// 验证已实现的类
validateAlreadyRealizedClass(cls);
return cls;
}
ASSERT(cls == remapClass(cls));
// fixme verify class is not in an un-dlopened part of the shared cache?
auto ro = cls->safe_ro();
auto isMeta = ro->flags & RO_META; // 是否元类
if (ro->flags & RO_FUTURE) { // future类,rw的data已经初始化
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}
cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
#if FAST_CACHE_META
if (isMeta) cls->cache.setBit(FAST_CACHE_META);
#endif
// Choose an index for this class.
// Sets cls->instancesRequireRawIsa if indexes no more indexes are available
cls->chooseClassArrayIndex();
if (PrintConnecting) {
_objc_inform("CLASS: realizing class '%s'%s %p %p #%u %s%s",
cls->nameForLogging(), isMeta ? " (meta)" : "",
(**void***)cls, ro, cls->classArrayIndex(),
cls->isSwiftStable() ? "(swift)" : "",
cls->isSwiftLegacy() ? "(pre-stable swift)" : "");
}
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
// This assumes that none of those classes have Swift contents,
// or that Swift's initializers have already been called.
// fixme that assumption will be wrong if we add support
// for ObjC subclasses of Swift classes.
// 实现父类和元类
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
// 纯isa还是nonpointer isa
#if SUPPORT_NONPOINTER_ISA
if (isMeta) {
// Metaclasses do not need any features from non pointer ISA
// This allows for a faspath for classes in objc_retain/objc_release.
cls->setInstancesRequireRawIsa(); // 纯指针isa
} else {
// Disable non-pointer isa for some classes and/or platforms.
// Set instancesRequireRawIsa.
bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
bool rawIsaIsInherited = false;
static bool hackedDispatch = false;
if (DisableNonpointerIsa) {
// Non-pointer isa disabled by environment or app SDK version
instancesRequireRawIsa = true;
}
else if (!hackedDispatch && 0 == strcmp(ro->getName(), "OS_object"))
{
// hack for libdispatch et al - isa also acts as vtable pointer
hackedDispatch = true;
instancesRequireRawIsa = true;
}
else if (supercls && supercls->getSuperclass() &&
supercls->instancesRequireRawIsa())
{
// This is also propagated by addSubclass()
// but nonpointer isa setup needs it earlier.
// Special case: instancesRequireRawIsa does not propagate
// from root class to root metaclass
instancesRequireRawIsa = true;
rawIsaIsInherited = true;
}
if (instancesRequireRawIsa) { // 纯指针isa
cls->setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
}
}
// SUPPORT_NONPOINTER_ISA
#endif
// Update superclass and metaclass in case of remapping
cls->setSuperclass(supercls); // 设置superclass指向父类
cls->initClassIsa(metacls); // 设置isa指向元类
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);
// Copy some flags from ro to rw
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}
// Propagate the associated objects forbidden flag from ro or from
// the superclass.
if ((ro->flags & RO_FORBIDS_ASSOCIATED_OBJECTS) ||
(supercls && supercls->forbidsAssociatedObjects()))
{
rw->flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;
}
// Connect this class to its superclass's subclass lists
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
// Attach categories
// 方法、属性、协议、分类的实现
methodizeClass(cls, previously);
return cls;
}
initClassIsa时会根据nonpointer区别设置
- 先类判断是否已经实现,如果已实现通过
validateAlreadyRealizedClass验证;- 未实现,
cls->setData(rw)处理类的data也就是rw和ro,初始化rw并拷贝ro数据到rw中;- 将事件往上层传递,来实现
父类以及元类;- 判断
isa指针类型,是纯指针还是nonpointer指针,在设置isa时有不同;cls->setSuperclass(supercls):设置superclass指向父类;cls->initClassIsa(metacls):设置isa指向元类;methodizeClass:在这里进行方法、属性、协议、分类的实现;
![]()
看一下methodizeClass的实现
static void methodizeClass(Class cls, Class previously)
{
runtimeLock.assertLocked();
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro();
auto rwe = rw->ext();
// Methodizing for the first time
if (PrintConnecting) {
_objc_inform("CLASS: methodizing class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// Install methods and properties that the class implements itself.
// rwe:方法、属性、协议
method_list_t *list = ro->baseMethods;
if (list) {
// 写入方法,并对方法进行排序
prepareMethodLists(cls, &list, 1, **YES**, isBundleClass(cls), **nullptr**);
if (rwe) rwe->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (rwe && proplist) {
rwe->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (rwe && protolist) {
rwe->protocols.attachLists(&protolist, 1);
}
// Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
// 如果根类还没有额外的方法实现,那么它们将获得额外的方法。这些适用于类别替换之前。
if (cls->isRootMetaclass()) {
// root metaclass 根元类添加initialize初始化方法
addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
}
// 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);
#if DEBUG
// Debug: sanity-check all SELs; log method list contents
for (const auto& meth : rw->methods()) {
if (PrintConnecting) {
_objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-',
cls->nameForLogging(), sel_getName(meth.name()));
}
ASSERT(sel_registerName(sel_getName(meth.name())) == meth.name());
}
#endif
}
methodizeClass中主要就是对method、property、protocol存放在rwe的处理,其实这里的rwe并没有值,因为还没有完成初始化,- 为根元类添加
initialize初始化方法,- 对分类处理。
上面是非懒加载类的处理,那么懒加载类是在什么时候完成初始化的呢?根据懒加载原则,应该就是在用的时候再去调用吧,那就验证一下
源码环境中,我们在methodizeClass通过类名字加断点
先在LGTeacher里面实现+load方法,运行
![]()
这里是从
_objc_init和_read_images进入的
在LGTeacher里面去掉+load方法,运行
这里可以看到,整个流程是在main函数里调用LGTeacher的alloc方法来的,通过objc_msgSend消息查找流程的lookUpImpOrForward走到了这里。
总结
类的加载通过类是否实现+load或静态实例方法,区分为懒加载类和非懒加载类
非懒加载类:是在启动时map_images时加载进内存的,通过_getObjc2NonlazyClassList得到所有非懒加载类,循环调用realizeClassWithoutSwift到methodizeClass完成初始化。
懒加载类:是在第一次消息发送的时候,检查类是否初始化,为完成初始化再去完成初始化流程。
关于消息发送文章:
iOS底层之Runtime探索(一)
iOS底层之Runtime探索(二)
iOS底层之Runtime探索(三)
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
分类加载的流程分析 《iOS底层之分类的加载》
以上是对iOS中类的加载通过源码的探索过程,如有疑问或错误之处请留言或私信我
iOS底层之Runtime探索(三)
序言
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
iOS底层之Runtime探索(一)
iOS底层之Runtime探索(二)
前面的文章中讲到了objc_msgSend的方法查找过程,在通过lookUpImpOrForward方法的慢速查找没有找到imp后,就到了动态方法决议流程resolveMethod_locked。
动态方法决议
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 元类判断
if (! cls->isMetaClass()) { // 对象方法流程
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else { // 类方法流程
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
- 判断当前的
class是否为元类; - 不是元类,走实力方法流程
resolveInstanceMethod; - 是
元类,走类方法流程resolveClassMethod,判断resolveClassMethod是否已解决问题,未解决再走resolveInstanceMethod流程; - 走完
动态决议流程,重新找一次imp;
resolveInstanceMethod方法解析
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 1.查询元类是否实现了resolveInstanceMethod:方法
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
// 2.发送消息resolveInstanceMethod:
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 3.再次查询sel对应的imp,resolveInstanceMethod可动态修改
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
- 定义
resolveInstanceMethod:方法;lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true)):查询元类是否实现了resolveInstanceMethod:方法,cls->ISA(true)为类isa指向的元类,如果未实现直接返回;- 通过
objc_msgSend发送resolveInstanceMethod:消息,将返回结果给resolved;lookUpImpOrNilTryCache(inst, sel, cls):再次查询当前类cls是否实现了sel对应的imp。
lookUpImpOrNilTryCache方法解析
![]()
-
lookUpImpOrNilTryCache:是直接调用了_lookUpImpTryCache,behavior值为LOOKUP_NIL。 -
_lookUpImpTryCache:的核心功能是再次走一次快速和慢速方法查询。behavior值为LOOKUP_NIL,所以即使再次查不到也不会再走resolveMethod_locked流程。
resolveClassMethod方法解析
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
// 1.检查元类是否实现方法resolveClassMethod:
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
// 2.通过cls,获取处理的nonmeta
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
// 3.发送resolveClassMethod: 消息
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
// 4.再次查询imp是否实现
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
- 定义
resolveClassMethod:方法; - 通过方法
lookUpImpOrNilTryCache,检查元类是否实现方法resolveClassMethod:,如果未实现直接返回; - 通过
cls,获取处理的nonmeta; - 通过
objc_msgSend发送resolveClassMethod:消息,将返回结果给resolved; -
lookUpImpOrNilTryCache(inst, sel, cls):再次查询当前元类cls是否实现了sel对应的imp。
动态方法决议流程图
![]()
动态决议实例
定义一个类LGTeacher,声明两个方法,不在m文件实现。
- 实例方法
sayHelllo;- 类方法
sayByeBye;
实现方法resolveInstanceMethod:但不做处理
![]()
实例方法
调用实例方法sayHello方法运行
![]()
LGTeacher中的resolveInstanceMethod:方法成功进来,并且进来了两次,然后打开resolveInstanceMethod:中的注释代码运行。
运行成功,我们成功通过动态向类添加方法lg_instanceMethod处理了sayHello方法找不到,引起的崩溃问题。
类方法
调用类方法sayByeBye方法运行
![]()
LGTeacher中的resolveClassMethod:方法成功进来,也是进来了两次,然后打开resolveClassMethod:中的注释代码运行。
运行成功,通过动态向元类添加方法lg_instanceMethod处理了sayByeBye方法找不到,引起的崩溃问题。
总结
我们可以在给公共父类
NSObject添加分类,重写方法实现resolveInstanceMethod:,这样就可以成功解决所有imp找不到引起的崩溃问题了,同时我们也可以通过方法命名规范知道是哪个模块的哪个功能引起的问题,做BUG收集。
缺点:
- 如果要对各个功能进行不同处理,需要大量业务功能逻辑判断,变得很复杂;
- 在
NSObject的分类中,很多系统的方法处理也会进来这里,会降低运行效率。
消息转发
如果我们未实现动态决议方法resolveInstanceMethod:,调用未实现的方法系统就会崩溃,这是我们通过bt指令查看堆栈信息
是通过doesNotRecognizeSelector:抛出错误的,在这之前还会经过_CF_forwarding_prep_0和___forwarding___流程,这都是在CoreFoundation库里,苹果并没有真正开源CoreFoundation库。
![]()
在查阅苹果文档动态方法解析中,提到了消息转发 Message Forwarding,消息转发正是苹果给你提供的imp找不到的第二次机会。
Sending a message to an object that does not handle that message is an error. However, before announcing the error,
the runtime system gives the receiving object a second chance to handle the message.
原理
- 当对象发送的
sel找不到imp时,系统会发送forwardInvocation:来通知该对象;forwardInvocation:是从NSObject继承而来的,而在NSObject中只是调用doesNotRecognizeSelector:抛出错误;- 我们可以重写
forwardInvocation:来实现消息的转发。forwardInvocation:的唯一参数是NSInvocation;- 在
NSInvocation中,可以指定消息接收者target,调用invoke实现消息转发;- 消息转发完成,
返回值将会返回给原始消息发送者;
文档里还介绍了有意思的点,在我们自己的类中要实现一个特定功能的方法,该功能已经在别的类中实现,我们又不能继承这个类,因为可能设计在不同的模块中,那我们就可以通过forwardInvocation:把消息转发特有的类去实现,这就实现类似多继承了。
![]()
实例
在关于forwardInvocation:的api文档中,有个重要提示:
![]()
除了
forwardInvocation:之外,还必要重写methodSignatureForSelector:转发消息的机制使用从methodSignatureForSelector获得的信息来创建要转发的NSInvocation对象。重写方法必须为给定的选择器提供适当的方法签名,方法签名可以是预先制定的,也可以是向另一个对象请求方法签名。
在LGTeacher中加上methodSignatureForSelector和forwardInvocation:方法后,再调用sayHello运行
成功运行,进入methodSignatureForSelector和forwardInvocation:方法,没有引起崩溃。
定义LGStudent实现sayHello方法,在LGTeacher中的forwardInvocation:,将消息转发给LGStudent
![]()
同样,在类方法的消息流程转发中,跟实例方法的实现是一致的,不过方法上要注意是+
![]()
forwardingTargetForSelector
在文档 forwardInvocation中还提供了forwardingTargetForSelector:方法
![]()
该方法的主要作用
无法识别的消息首先应
指向的对象。如果对象实现(或继承)此方法,并返回非nil(和非self)结果,则返回的
对象将用作新的接收方对象,消息分派将恢复到该新对象。(显然,如果从该方法返回self,代码将陷入无限循环。)如果您在
非根类中实现此方法,如果您的类对于给定的选择器没有要返回的内容,那么您应该返回调用super实现的结果。该方法为对象提供了一个机会,在更昂贵的
forwardInvocation:机械接管之前重定向发送给它的未知消息。当您只想将消息重定向到另一个对象,并且可以比常规转发快一个数量级时,这非常有用。如果转发的目标是捕获NSInvocation,或在转发过程中操纵参数或返回值,则此选项不适用。
- 大概意思是,如果只需要对未识别的消息,做简单的重定向,那么用此方法会比
forwardInvocation:流程快很多很多,如果有额外的复杂操作,如捕获NSInvocation、操纵参数、返回值则不适用该方法。 -
forwardingTargetForSelector:的调用要在forwardInvocation:的前面。
![]()
在forwardingTargetForSelector:实现重定向到LGStudent
![]()
流程图
![]()
消息总结
到这里整个Runtime中对消息的处理流程objc_msgSend的解读都已完成,可能不够全面,中间有些细节难免有遗漏,不过重在整个流程的思维探索。
- 方法调用 -->
objc_msgSend(id receiver, Sel);- 通过
isa获取类的地址cls;- 快速查找流程
CacheLookup;- 慢速查找流程
lookUpImpOrForward;- 动态方法解析
resolveMethod_locked;- 消息重定向
forwardingTargetForSelector:;- 消息转发
forwardInvocation:;
有兴趣的伙伴也可以搞一份源码跑一跑
iOS 全网最新objc4 可调式/编译源码
还有编译好的源码的下载地址
iOS底层之Runtime探索(二)
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序言
在前一篇iOS底层之Runtime探索(一)中,已经知道了在sel找imp的整个缓存查找过程,这个过程是用汇编实现,是一个快速方法查找流程,今天就来探究一下缓存没有查找到后面的流程。
__objc_msgSend_uncached 方法探究
前面流程是对缓存进行查找imp,如果找不到就走方法__objc_msgSend_uncached方法
在__objc_msgSend_uncached中,共两条语句MethodTableLookup和TailCallFunctionPointer x17
MethodTableLookup方法解析
![]()
MethodTableLookup中的代码逻辑
x0是receiver,x1是sel;mov x2, x16: 将x16也就是cls给x2;mov x3, #3: 将常量值3给x3;- 调用
_lookUpImpOrForward:x0~x3是传的参数;mov x17, x0: 将_lookUpImpOrForward的返回值x0给x17;
通过注释也可以大致看出,调用方法lookUpImpOrForward获取imp,并返回。
TailCallFunctionPointer定义
这里仅仅是对传入的$0直接调用。
我们可以总结,
__objc_msgSend_uncached方法流程是通过MethodTableLookup获取imp,然后传值到TailCallFunctionPointer定义调用imp。这里的核心就是
MethodTableLookup中的lookUpImpOrForward是怎么获取的imp。
lookUpImpOrForward方法探究
lookUpImpOrForward通过方法命名,可以看出一些端倪,就是查找imp找不到就forward操作。
实例分析
- 对上面定义的
LGPerson添加分类,并在分类中同样实现sayHello方法; - 对
NSObject添加分类,并自定义方法sayNB; - 对
LGTeacher定义方法sayByeBye,但不添加实现;
结果
sayHello调用的是LGPerson的分类方法,why?- 用类对象
LGTeacher调用NSObject的sayNB方法成功,why?- 未实现方法
sayByeBye报错unrecognized selector sent to instance 0x600000008000,why?
lookUpImpOrForward源码分析
源代码添加了中文注释
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//定义消息转发forward_imp
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// 判断当前类是否初始化,
if (slowpath(!cls->isInitialized())) {
/*
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};
*/
// 如果没有初始化 behavior = LOOKUP_INITIALIZE | LOOKUP_RESOLVER | LOOKUP_NOCACHE
behavior |= LOOKUP_NOCACHE;
}
// 加锁防止多线程访问出现错乱
runtimeLock.lock();
// 检查当前类是否被dyld加载的类
checkIsKnownClass(cls);
// 如果尚未实现给定的类,则实现该类;如果尚未初始化,则初始化该类。
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
//在我们获取锁后,用于再次查找类缓存的代码,但对于大多数情况,证据表明这在大多数情况下都是失败的,因此会造成时间损失。
//在没有执行某种缓存查找的情况下,唯一调用此函数的代码路径是class_getInstanceMethod()。
for (unsigned attempts = unreasonableClassCount();;) {// 开启循环查找imp
// 继续检查共享缓存是否有方法
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel); // cache_getImp流程获取imp
if (imp) goto done_unlock; // 找到imp走”done_unlock“流程
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// 从curClass的method list中查找method.
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done; // 查找到imp,跳转done流程
}
// 未从curClass的method list中查找到对应的method,继续往父类查找,直到父类为nil
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp; // 查找不到跳出for循环
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel); // 这里curClass已指向父类,查找父类的缓存中的imp
if (slowpath(imp == forward_imp)) { // 表示未查找到跳出循环
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) { // 从父类缓存中查找到imp,跳转到done
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
// 为找到imp,开始resolveMethod_locked流程,动态方法决议
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) { // 已完成类的初始化
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass); // 将获取的imp插入缓存
}
done_unlock:
runtimeLock.unlock(); //runtimeLock 解锁
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
逻辑流程分析
- 判断当前类是否初始化,若未初始化执行
behavior |= LOOKUP_NOCACHE;checkIsKnownClass(cls)检查当前类是否被加载到dyld,类的加载属于懒加载,未加载这里会做加载处理;realizeAndInitializeIfNeeded_locked如果尚未实现给定的类,则实现该类;如果尚未初始化,则初始化该类;- 进入
for循环开始一层层遍历查找imp;curClass->cache.isConstantOptimizedCache检查共享缓存,这个时候,可能别的地方进行缓存了,如果有则直接跳转done_unlock返回imp;- 上面没有缓存,
getMethodNoSuper_nolock从当前类的methodlist中查找method;- 如果找到跳转
done,将找到的imp进行缓存插入,再返回imp;- 如果未找到,
if (slowpath((curClass = curClass->getSuperclass()) == nil))这里将curClass指向父类,并判断如果父类为nil,将imp指向forward_imp,break跳出for循环;- 如果父类存在,通过
cache_getImp检查父类缓存是否有imp,如果imp为forward_imp则跳出循环,然后再检查imp,如果imp有效则跳转done流程;- 查找
for循环结束,没有找到imp,按LOOKUP_RESOLVER判断走动态方法决议流程resolveMethod_locked
归总分析就是,消息查找会沿着继承链一层一层往上查找,直到找到nil,如果有找到则插入缓存并返回imp,如果找不到则imp指向forward_imp也就是_objc_msgForward_impcache。
上面LGTeacher调用NSObject的分类方法sayNB的过程,就是LGTeacher沿着元类的继承链找到了NSObject,并调用sayNB方法。
lookUpImpOrForward流程图
![]()
getMethodNoSuper_nolock方法查找解析
在getMethodNoSuper_nolock的方法中,查找method的算法就是findMethodInSortedMethodList方法中的二分查找算法实现的。
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
// method_list_t 数组是通过将sel转为uintptr_t类型的value值进行排序的
auto first = list->begin(); // 起始位置 0
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key; // 目标sel
uint32_t count; // 数组个数
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1); // probe = base + floor(count / 2)
uintptr_t probeValue = (uintptr_t)getName(probe)
if (keyValue == probeValue) { // 目标命中
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 一直循环找最前面的同名方法
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) { // 目标value大于,二分法的中间数
base = probe + 1;
count--;
}
}
return nil;
}
这里的二分算法设计还是挺巧妙的,可以慢慢品玩;其中在keyValue == probeValue的时候,会进入while循环,让probe--,直到找到同名方法中最前面的方法,同名方法就是我们分类中重写的方法,分类中方法会放在前面,所以调用是会优先调用分类中的方法实现。
cache_getImp方法解析
根据之前对CacheLookup的分析,cache_getImp中MissLabelDynamic传的是LGetImpMissDynamic,因此如果CacheLookup中找不到imp就会进入LGetImpMissDynamic,这里仅仅是返回了0值,所以cache_getImp流程在这里也就断了返回的是0。
forward_imp解析
在取父类(curClass = curClass->getSuperclass()) == nil)的判断中,如果往上没有父类了,即条件成立,那imp会指向_objc_msgForward_impcache类型的forward_imp,并返回执行imp。
![]()
-
_objc_msgForward_impcache中只是对__objc_msgForward进行调用; -
__objc_msgForward是对__objc_forward_handler相关的方法操作返回值到x17,并通过TailCallFunctionPointer调用x17。
![]()
__objc_forward_handler就是函数objc_defaultForwardHandler,这里面就是直接抛出错误unrecognized selector sent to instance
这里的错误信息字符串中,对
+和-的处理是通过元类判断的,底层中没有+号方法或-号方法,都是OC的语法设计。
总结
- 在缓存
cache中没有找到imp,就会通过lookUpImpOrForward开启从当前类到NSObject的方法列表进行层层遍历,这个过程为“慢速查找”过程;- 如果找到了就会返回
imp,并执行;- 如果找不到就会让
imp指向forward_imp并返回,执行forward_imp系统会直接抛出方法未找到的错误;- 在返回
forward_imp之前,还有一步resolveMethod_locked操作,这就是动态方法决议的流程,在下一篇幅展开细讲。
以上是Runtime中对objc_msgSned方法的慢速查找流程解析,如有疑问或错误之处,请在评论区留言或私信我,感谢支持~❤
iOS底层之Runtime探索(一)
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
Runtime简介
Runtime 简称运行时,Objective-C语言将尽可能多的决策从编译时和链接时推迟到运行时。只要可能,它都会动态地进行操作。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译的代码。运行时系统充当Objective-C语言的一种操作系统(官方翻译)。
了解Objective-C运行时系统的工作原理以及如何利用它。但是,通常情况下,编写Cocoa应用程序时,您不需要了解和理解这些材料(官方翻译🐶)。
-
编译时:顾名思义就是正在编译的时候。就是编译器帮你把源代码翻译成机器能识别的代码。编译器进行代码的语法分析,发现其中的编译错误和警告等,叫做静态类型检查。 -
运行时:代码跑起来被装载到内存中,运行时类型检查和编译时类型检查不一样,不是简单的代码扫描分析,而是在内存中做些操作。
Runtime官方介绍:Objective-C Runtime Programming Guide
Runtim探究
按照官方文档:
Objective-C programs interact with the runtime system at three distinct levels: through Objective-C source code; through methods defined in the
NSObjectclass of the Foundation framework; and through direct calls to runtime functions.
- 自定义方法调用:
[person sayHello]- 系统动态库api:
isKindOfClassRuntime的api:class_getInstanceSize
我们探究Runtime就从最熟悉的自定义方法调用开始入手。
cpp方式查看
自定义类LGPerson,LGPerson中自定义实例方法sayHello,然后在main函数中调用,并生成cpp文件查看。
共调用了4个方法
-
LGPerson的alloc类方法; - 实例方法
sayPerson; -
NSObject的方法isKindOfClass:; -
NSObject的class类方法;
我们来看cpp中的代码实现
可以看到,不论实例方法还是类方法都是调用的函数objc_msgSend,我们对objc_msgSend进行梳理发现它的结构是objc_msgSend(id receiver, sel),那我们是不是也可以直接调用objc_msgSend呢?
objc_msgSend调用实现
调用成功,这里也就验证了方法的调用其实就是消息发送。在查看objc_msgSend时我还发现了一个方法objc_msgSendSuper
objc_msgSendSuper调用实现
查看objc_msgSendSuper定义
有2个参数,一个objc_super类型的指针,一个SEL,看一下objc_super
这里的成员super_class是第一要查找的类。
我们自定义LGTeacher类继承自LGPerson,调用父类的方法sayHello,objc_msgSend,objc_msgSendSuper
可以看到,三种方式都能实现,那么objc_msgSend是怎么实现消息发送的呢?
objc_msgSend探究
通过汇编调试方法,发现objc_msgSend的定义是在libobjc库中
那我们就去源码找objc_msgSend,通过全局搜索锁定汇编文件objc-msg-arm64,接下来我们就来看objc_msgSend的汇编流程,加了一些注释
objc_msgSend汇编源码
// _objc_msgSend调用时有两个参数, id receiver(isa), SEL
ENTRY _objc_msgSend // _objc_msgSend 入口
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // 第一个参数receiver和0比较
#if SUPPORT_TAGGED_POINTERS // 是否支持Taggedpointer类型
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif //cmp比较 receiver有值就走 endif
ldr p13, [x0] // p13 = isa (取出x0=isa赋值给p13)
GetClassFromIsa_p16 p13, 1, x0 // p16 = class (调用GetClassFromIsa_p16方法,p13, 1, x0作为参数传入)
LGetIsaDone: // 一个标记符号,拿到isa后操作完后,继续后面流程
// calls imp or objc_msgSend_uncached(调用CacheLookup,NORMAL, _objc_msgSend, __objc_msgSend_uncached作为参数传递)
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
ENTRY _objc_msgLookup
UNWIND _objc_msgLookup, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LLookup_NilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LLookup_Nil
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LLookup_GetIsaDone:
// returns imp
CacheLookup LOOKUP, _objc_msgLookup, __objc_msgLookup_uncached
伪代码复现一下代码逻辑
- 判断参数
receider也就是isa是否为nil; - 是nil再判断是否支持
Taggedpointer类型,如果支持则走LNilOrTagged流程,否则就走LReturnZero流程; -
receider不为nil,取出isa赋值给p13; - 调用
GetClassFromIsa_p16,并传参数p13, 1, x0也就是isa,1,x0,回去class地址赋值给p16; - 调用方法
CacheLookup,并传参数NORMAL,_objc_msgSend,__objc_msgSend_uncached
GetClassFromIsa_p16方法解析
同样看一下GetClassFromIsa_p16源码,其核心功能是获取isa指向的class地址,这里也加了注释
// src = p13, needs_auth = 1, auth_address = x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA // armv7k || (arm64 && !LP64)
// Indexed isa
mov p16, \src // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else // 这里穿的needs_auth = 1,所以走else流程
// 64-bit packed isa
/**
解析:
src = p13(isa), needs_auth = 1, auth_address = x0
.macro ExtractISA and $0, $1, #ISA_MASK
等于:
(isa & #ISA_MASK) 赋值给 p16 --> 这里就是去出isa指向的class地址
*/
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
-
SUPPORT_INDEXED_ISA为armv7k或arm64切非LP64; -
needs_auth参数为1;
根据上面两个条件GetClassFromIsa_p16的核心代码就是ExtractISA p16, \src, \auth_address,ExtractISA也是宏定义源码为
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
结合GetClassFromIsa_p16和ExtractISA解析
p16为ExtractISA里面的$0;src为p13也就是isa为ExtractISA里面的$1;and $0, $1, #ISA_MASK:isa & ISA_MASK=cls类的地址,即为从对象的isa获取class的过程。
这里得到$0也就是p16为cls,继续走流程看CacheLookup
缓存查找
CacheLookup汇编源码解析
根据CacheLookup名称,我们也能猜出大概即从缓存中查找,从前面《类的缓存cache_分析》我们知道方法调用后是缓存在cache_t关联的bucket_t中,前面得到了p16也就是class,下面就是找类的bucket_t。
CacheLookup源码
// NORMAL, _objc_msgSend, __objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
mov x15, x16 //x16 (p16 = isa) 取值 --> x15 (stash the original isa)
LLookupStart\Function:
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// (看真机环境)
ldr p11, [x16, #CACHE] // p11 = mask|buckets = cache
#if CONFIG_USE_PREOPT_CACHES // arm64 下为1
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function // 比较
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
2: CacheHit \Mode // hit: call or return imp
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
补充几个定义
- 真机的
CACHE_MASK_STORAGE为CACHE_MASK_STORAGE_HIGH_16,我们看真机环境;#CACHE为(2 * __SIZEOF_POINTER__)2倍指针大小2 * 8 = 16;arm64环境下CONFIG_USE_PREOPT_CACHES值为1;__has_feature(ptrauth_calls): 是否为A12及更高处理器,我们看通用版本,默认这里为0;PTRSHIFT值为3
按照上面定义复现一下代码逻辑
-
mov x15, x16: 取x16也是p16(cls)给x15; -
ldr p11, [x16, #CACHE]:p16(cls)平移16字节得到cache,存在p11就是cache的地址; -
and p10, p11, #0x0000fffffffffffe:0x0000fffffffffffe为bucketsMask掩码值,所以这里用cache与bucketMask掩码值取得buckets地址存在p10; -
eor p12, p1, p1, LSR #7:p1为SEL,这里对应源码cache_hash方法中的sel ^= sel >> 7,p1右移7位得到的值再异或p1,存到p12; -
and p12, p12, p11, LSR #48:p11右移48位得到mask值,再和p12相与,即sel & mask得到sel的哈希下标值存在p12; -
add p13, p10, p12, LSL #(1+PTRSHIFT):bucket_t的成员是sel和imp,内存大小为16字节,p12, LSL #(1+PTRSHIFT)相当于哈希下标值index左移4位,得到index对应与buckets首地址的偏移量, 通过p10也就是buckets首地址向下移动p12, LSL #(1+PTRSHIFT),取到bucket_t地址存在p13;
![]()
-
1:中的ldp p17, p9, [x13], #-BUCKET_SIZE相当于取出p13 bucket_t中的sel给p9,取imp给p17,#-BUCKET_SIZE为*buckets--先取值后--; -
cmp p9, p1:比较缓存里的sel和p1是否一致,如果一致则走2:中的CacheHit缓存命中,\Mode为第一个参数值NORMAL,否则可能为哈希冲突也可能没有缓存该sel,进入3:语句; -
3:中先判断p9是否有值,没有则说明没有缓存sel走MissLabelDynamic,也就是传入的第三个参数__objc_msgSend_uncached, - 如果p9有值,则比较
p10和p13是否同一个地址,不是则继续1:流程循环,如果是同一个地址,因为1:中是*buckets--遍历查找,也就意味着找到了buckets的首地址位置,那就跳转到buckets的最后位置继续循环。
![]()
-
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)): 取buckets中的最后一个bucket_t地址存在p13; -
add p12, p10, p12, LSL #(1+PTRSHIFT):用p12记录第一次查找的位置; -
4:中的逻辑是遍历最后一个位置到第一次查找的位置中的所有bucket_t,找到了就CacheHit,否则就MissLabelDynamic
在CacheLookup中如果能够找到缓存方法,则走CacheHit中的NORMAL逻辑,找不到就走__objc_msgSend_uncached。
CacheHit源码解析
在CacheHit中$0为传进来的NORMAL,所以这里的代码逻辑就是用TailCallCachedImp对缓存里找到的imp先解码再调用。
![]()
缓存查找流程图
![]()
总结
- 汇编源码真恶心,慢慢跟流程还算能啃下来。
- 通过上面的流程分析到
objc_msgSend的调用,其实就是通过SEL查找IMP的过程,这个过程越快越好;汇编是比较接近机器码的,所以OC的设计是用汇编实现方法的缓存查找会提高方法调用的效率;objc_msgSend流程就是先去类的缓存中找有没有对应的sel,找到了则直接调用缓存中的imp;- 找不到
imp就是下一个流程了,objc_msgSend的慢速查找流程。
以上是对Runtime的一些分析,以及方法调用过程中objc_msgSend的缓存查找实现流程分析,如有疑问或错误之处,请评论区留言或私信我。
iOS底层之类的cache分析
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序言
在前面文章类的结构中,我们分析了bits的结构,isa以及superclass是为指针类型,还剩下一个cache没有分析,cache顾名思义就是缓存相关的,今天就来看一下cache是怎么个原理。
cache的数据结构
先自定义一个类LGPerson,代码实现
![]()
LLDB输出数据结构
用LLDB调试输出,查看cache的数据
有几个关键数据:_bucketsAndMaybeMask、_maybeMask、_flags、_occupied、_originalPreoptCache。
cache源码数据结构
然后我们再看cache_t的源码结构
总结
_bucketsAndMaybeMask:一个uintptr_t类型的指针;- 联合体:一个结构体和
preopt_cache_t结构体类型的指针变量_originalPreoptCache;_maybeMask:mask_t泛型的变量;_flags:uint16_t类型变量;_occupied:uint16_t类型变量preopt_cache_t:preopt_cache_t结构体类型的指针变量;
这里我们还无从知道cache是怎样缓存的数据,以及缓存的是什么数据,是属性还是方法呢?
cache缓存数据类型
既然通过cache_t的数据结构看不出来,那我们就找方法。
缓存应该有增删改查等方法,那就从这些方法下手吧。通过阅读源码,我们看到有一个insert方法和copyCacheNolock方法
在insert方法中,插入的是SEL和IMP,由此可以看出cache缓存的数据是方法method,然后再看一下insert的实现,找一下SEL和IMP是缓存在哪里。
cache缓存的存储位置
这里很明显是一个bucket_t类型的b,调用set方法插入SEL和IMP以及关联的Class。
看一下bucket_t的结构。
这里我们可以简单总结一下类中cache_t的结构
![]()
cache缓存数据输出查看
现在我们已经找到了cache缓存的方法是存在bucket_t中,并且bucket_t有成员变量_sel和_imp,在insert中是通过方法buckets()获取到的bucket_t,那我们就找到输出一下。
LLDB找到cache缓存数据
在cache_t的结构体定义中,正好有buckets()方法,那我们在LLDB中获取到cache的地址变量就可以输出bucket_t。
![]()
声明一个LGPerson类型的变量p,并调用对象方法sayHello
然后我们用LLDB调试输出信息
我们成功获取到了bucket_t类型的$3,但当我查看$3的内容是缺还是空值。why!!!why!!!why!!!
还是回归到insert源码,看一下到底是怎么插入的缓存吧。
天呢!漏了一个细节,这里缓存插入的时候是用了hash算法取下标的方式,那我们上面取到的第一个bucket_t的就可能为空值。
既然这样,buckets()的存储结构是一个哈希数组,那我们就继续往下面找bucket_t
这里的_sel中的Value和_imp中的Value明显和上面的不一样了,不再是nil和0,那我们可以猜测这是一个有效的bucket_t。
找到bucket_t结构体中的方法sel()和imp(),输出一下
![]()
Done!!
这里我们成功找到了缓存的方法sayHello,但是我发现在LLDB这样调试很是麻烦,而且还依赖于源码的运行环境,如果有系统升级或者源码有更新,编译不了源码,难道只能GG吗,所以能不能脱离源码编译环境也能搞定上面的步骤呢
脱离源码分析cache
我们的目的是获取cache_t里面的bucket_t,cache_t是在objc_class里面,那我们就按照源码objc_class的结构去自定义一个相似的结构体,这样就可以通过NSLog输出获取的内容信息。
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct lg_bucket_t {
SEL _sel;
IMP _imp;
};
struct lg_cache_t {
struct lg_bucket_t * _buckets;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct lg_class_data_bits_t {
uintptr_t bits;
};
struct lg_objc_class {
Class isa;
Class superclass;
struct lg_cache_t cache; // formerly cache pointer and vtable
struct lg_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson * p = [LGPerson alloc];
[p sayHello];
Class pClass = [LGPerson class];
struct lg_objc_class *lg_class = ( __bridge struct lg_objc_class *)(pClass);
NSLog(@" - %hu - %u",lg_class->cache._occupied,lg_class->cache._maybeMask);
for (int i = 0; i < lg_class->cache._maybeMask; i++) {
struct lg_bucket_t bucket = lg_class->cache._buckets[i];
NSLog(@"SEL = %@ --- IMP = %p", NSStringFromSelector(bucket._sel), bucket._imp);
}
NSLog(@"Hello, World!");
}
return 0;
}
运行上面的代码,查看输出
![]()
成功输出,这里的_occupied为1,_maybeMask为3,我们再调两个方法sayHello_1和sayHello_2验证一下。
![]()
这里发生了蹊跷,_occupied为1,_maybeMask变成了7,而缓存中只有方法sayHello_2,我们调用的sayHello和sayHello_1却不在缓存中。既然这样,那就从头捋一遍源码,看看是不是又漏下什么细节了。
cache底层原理分析
对于底层原理分析,就从cache_t的插入方法insert入手
insert源码
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {
return;
}
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) { // 1.判断当前缓存是否为空的
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; // 1 << 2 = 4
reallocate(oldCapacity, capacity, /* freeOld */false); //开辟内存
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
整个代码流程我们按调用次数分布解析
第一次调用方法insert
第一次插入缓存时
-
_occupied的值为0,所以newOccupied为1; -
capacity()取的是_maybeMask的值,所以oldCapacity和capacity值都为0; -
isConstantEmptyCache判断当前缓存是否为空,条件成立,进入if语句; -
capacity值为0,赋值为INIT_CACHE_SIZE,INIT_CACHE_SIZE = 1 << 2值为4; - 调用
reallocate开辟内存;
再reallocate方法中,开辟新内存,然后调用setBucketsAndMask方法,使cache_t中的成员变量_bucketsAndMaybeMask、_maybeMask、_occupied做关联
之后是再开辟的缓存空间中存入SEL和IMP
在存入SEL和IMP的方法中,有对IMP进行编码,实际上存入的是编码后的newImp
对imp编码的源代码
![]()
我们用到的CACHE_IMP_ENCODING情况为CACHE_IMP_ENCODING_ISA_XOR,所以上面的编码算法是imp & cls。
我们知道了
第一次调用方法,会开辟空间为4的缓存空间,当我们调用更多方法的时候,应该在什么时候扩容呢?
四分之三扩容
当我们不是第一次调用方法时,就会进入一个剩余空间容量判断
![]()
newOccupied:进入缓存的第几个方法;CACHE_END_MARKER:宏定义值为1;cache_fill_ratio(capacity):capacity * 3 / 4容量的3/4值;
这里我们知道当新的调用方法进入缓存时
- 如果
不满足扩容条件,就会继续往开辟的缓存空间插入一条缓存数据。比如:调用sayHello_1时,newOccupied为2,capacity为4,2 + 1 <= 4 *3 / 4的条件满足。 - 如果到达
扩容条件,就会先开辟2倍的新内存,然后再插入新的缓存数据。比如:调用sayHello_2时,newOccupied为3,capacity为4,3 + 1 <= 4 *3 / 4的条件不满足,就会进入else语句,开辟2倍容量的新内存。
注
在开辟新内存中调用方法reallocate时,传入的最后一个参数freeOld为true,会把旧的缓存空间清理释放掉,不会copy旧缓存数据到新的缓存空间,这也是为什么调用sayHello_2时,输出的只有sayHello_2。
![]()
总结
关于objc_class中的cache的原理分析,我们先是查看cache_t的数据结构,根据数据结构我们无法知道其工作原理,然后我们通过结构体中的方法去找线索,最后锁定insert方法,根据insert方法来大致了解整个缓存插入的流程。cache_t的工作原理流程图:
![]()
iOS底层之类的结构分析
isa流程分析
首先来看一张比较熟悉的isa的流程走位图
![]()
isa指向链
- 实例对象
instance的isa指向类class; - 类对象
class也有isa指向的是元类meta, - 元类
mata中也有isa指向的是根元类root meta;
![]()
类的继承链
-
子类继承与父类,父类继承与根类,根类指向的是nil; - 在
元类中也存在继承,子类的元类继承与父类的元类,父类的元类继承与根元类,根元类又继承与根类;
![]()
isa流程实例验证
补充:在获取到对象的
isa值后,可以通过&(与)一个掩码ISA_MASK0x007ffffffffffff8ULL来获取到对象关联的类地址。
![]()
isa指向链验证
定义一个LGPerson继承NSObject,同时定义LGTeacher继承LGPerson。
LGPerson *p = [LGPerson alloc];
NSLog(@"%@",p);
LGTeacher *t = [LGTeacher alloc];
NSLog(@"%@",t);
在LLDB调试对象t,通过t的isa找到关联类LGTeacher的地址为0x0000000100008310。
接着我们按同样的方式输出,找到LGTeacher类对象的isa关联类地址为0x00000001000082e8。
往下找,找到LGTeacher元类对象的isa关联类地址为0x00007ff85e5f2220。
按同样的方式,找到NSObject类对象的isa关联地址为0x00007ff85e5f2220,和LGTeacher元类对象的isa关联类地址一致,可以验证元类的isa指向根元类。
![]()
根元类的isa指向自己。
![]()
类的继承链验证
同样按照LGPerson继承NSObject,LGTeacher继承LGPerson的定义。
Class tClass = LGTeacher.class;
Class pClass = class_getSuperclass(tClass);
Class nClass = class_getSuperclass(pClass);
Class rClass = class_getSuperclass(nClass);
NSLog(@"\n tClass-%@ \n pClass-%@ \n nClass-%@ \n rClass-%@ \n", tClass, pClass, nClass, rClass);
![]()
通过上面代码输出打印,很明显看出
subClass->superClass->NSObject->nil的继承链关系。
LGTeacher * teacher = [LGTeacher alloc];
Class tClass = object_getClass(teacher);
Class mtClass = object_getClass(tClass);
Class mtSuperClass = class_getSuperclass(mtClass);
NSLog(@"\n teacher %p 实例对象 -- %p 类 -- %p 元类 -- %p 元类父类", teacher, tClass, mtClass, mtSuperClass);
LGPerson * person = [LGPerson alloc];
Class pClass = object_getClass(person);
Class mpClass = object_getClass(pClass);
Class mpSuperClass = class_getSuperclass(mpClass);
NSLog(@"\n person %p 实例对象 -- %p 类 -- %p 元类 -- %p 元类父类", person, pClass, mpClass, mpSuperClass);
NSObject * obj = [NSObject alloc];
Class objClass = object_getClass(obj);
Class mobjClass = object_getClass(objClass);
Class mobjSuperClass = class_getSuperclass(mobjClass);
NSLog(@"\n NSObject %p 实例对象 -- %p 类 -- %p 元类 -- %p 元类父类", obj, objClass, mobjClass, mobjSuperClass);
teacher 0x600000201ba0 实例对象 -- 0x100008310 类 -- 0x1000082e8 元类 -- 0x100008338 元类父类
person 0x600000201bc0 实例对象 -- 0x100008360 类 -- 0x100008338 元类 -- 0x7ff85e5f2220 元类父类
NSObject 0x600000004030 实例对象 -- 0x7ff85e5f2270 类 -- 0x7ff85e5f2220 元类 -- 0x7ff85e5f2270 元类父类
通过输出信息我们可以看出
-
teacher的元类父类地址0x100008338==person的元类地址0x100008338; -
person的元类父类地址0x7ff85e5f2220==NSObject的元类地址0x7ff85e5f2220; -
NSObject的元类父类地址0x7ff85e5f2270==NSObject的类地址0x7ff85e5f2270;
综合,我们可以得出元类的继承链关系为
Sub Meta->Super Meta->Root Meta->NSObject。
类的结构
前面对象的本质中,我们知道Class的定义是objc_class结构体类型,那么就看一下objc_class的结构体的结构是怎样定义的。
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
... 其他代码
}
这里
objc_class的继承与objc_object,objc_object的结构中只有一个isa指针, 因此,objc_class的成员组成为:
- 从
objc_object继承的指针isa;superclass:Class类型指向父类的指针cache:缓存相关的东西bits:数据
![]()
cache结构
cache_t也是一个结构体定义
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
union {
struct {
explicit_atomic<mask_t> _maybeMask; // uint32_t 4字节
#if __LP64__
uint16_t _flags; // 2字节
#endif
uint16_t _occupied; // 2字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字节
};
}
分析整个cache_t的结构,发现cache_t的内存总共为16字节。
关于cache_t的探究专门写了一篇文章。
bits探究
bits是今天的探究重点,bits到底存储了哪些数据,在objc_class里有一段源码是data操作。
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
这里data的类型是class_rw_t,在class_rw_t的源码中,我们可以看到几个方法:
![]()
ro:成员变量、methods:方法、properties:属性、protocols协议 ,我们在类中定义的方法,属性等就行通过调取class_rw_t结构体中的方法获取的。
实例探究
下面我们通过实例,来验证一下类的结构是否如上面分析的一致。
我们自定义一个类
LGPerson继承自NSObject,定义一些属性和实力方法以及类方法。
![]()
LLDB调试输出
![]()
这里我们可以看出第一个地址
0x0000000100008228是类的第一个成员isa,第二个地址0x0000000100816140是类的第二个成员superclass。
前面我们知道isa指针8字节,superclass指针8字节,cache结构体16字节,通过LGPerson的首地址便宜8 + 8 + 16 = 32字节我们就可以得到bits的内存地址。
这里用LGPerson的首地址0x100008250加上32字节,得到地址0x0000000100008270,然后转换一下指针类型,得到$9为class_data_bits_t类型,调用data方法就得到$10地址为class_rw_t类型,下面就来探究class_rw_t中的属性、方法、协议。
注意
在转换
class_data_bits_t这一步,需要在源码环境调试,关于源码的下载和编译,介绍一个靓仔,# iOS 全网最新objc4 可调式/编译源码还有编译好的源码的下载地址
属性properties
调用class_rw_t的properties()方法,得到property_array_t类型的数组,继承与list_array_tt,找到list下的ptr。
![]()
![]()
ptr为property_list_t类型,是继承与entsize_list_tt,
![]()
entsize_list_tt部分源码
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count; // 数量
uint32_t entsize() const {
return entsizeAndFlags & ~FlagMask;
}
uint32_t flags() const {
return entsizeAndFlags & FlagMask;
}
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
}
Element& get(uint32_t i) const { // 获取元素方法
ASSERT(i < count);
return getOrEnd(i);
}
// ... 省略
};
通过get方法,获取元素,可以看到下面的结果,LGPerson中的属性name和age在properties()里,而成员变量hobby不在这里。
![]()
方法methods
调用class_rw_t的methods()方法,得到method_array_t类型的数组,继承与list_array_tt,同样找到list下的ptr。
这里看到ptr是method_list_t类型,同样继承与entsize_list_tt,其中有count为6,输出查看
![]()
这里的元素为method_t类型,method_t为结构体类型,其中的一个成员变量为big的结构体,里面是方法名称等信息。
我们在调用method_t的big方法,查看输出
这里看到6个方法分别是
- 实例方法:
sayHello;- 属性name的get方法;
C++析构函数:.cxx_destruct- 属性
name的set方法;- 属性
age的get方法;- 属性
age的get方法;
这里的6个方法都是实力对象方法,并没有我们定义的类方法sayWorld。
协议protocols
我们自定义一个协议LGPersonDelegate,让LGPerson遵守并实现协议方法
调用class_rw_t的protocols()方法,得到protocol_array_t类型的数组,继承与list_array_tt,同样找到list下的ptr。
这里看到ptr是protocol_list_t类型,不是继承自entsize_list_tt,那我们看一下protocol_list_t的定义
看到protocol_list_t的定义,我们知道count值为1,说明是有值,但是其成员是protocol_ref_t为uintptr_t类型,那怎么输出查看这个count中的1到底是什么呢
查看protocol_ref_t的定义,通过注释信息,我们可以看到protocol_ref_t未映射到protocol_t类型,那我们就找protocol_t的定义
这里看到protocol_t中有mangledName以及instanceMethods等,只要得到protocol_t就可以输出我们想要的名称方法等信息,怎么才能从protocol_ref_t映射到protocol_t呢,全局找一下吧
一不小心找到了😁,这里我们看到,protocol_ref_t是可以强转protocol_t的,那我们就试试
强转成功,调用demangledName方法,我们就得到了LGPersonDelegate,那我们再找一下协议方法
按照method输出的经验,成功找到协议方法personDelegateMethod。
ro
调用class_rw_t的ro方法,得到class_ro_t的结构体
查看class_ro_t的内容,看到这里有成员ivars,这引起了我们的注意
接着查看ivars,是ivar_list_t类型的结构体,也是继承entsize_list_tt,那么我们就可以调用get方法查看成员。
![]()
![]()
调用get方法查看输出,这里就可以看到3个成员变量,自定义hobby和系统帮我们定义的属性生成带_的成员变量。
![]()
类方法
在上面的methods方法中,我们并没有得到定义的类方法sayWorld,不由得我陷入了沉思,我们定义的类结构中,没有我们定义的类方法,那类方法会存在哪里呢?
这里我们换个思考角度想一下,在methods中全都是实例方法,也就是对象方法,对象方法是存在类中;类方法是类对象在调用,那类方法是不是存在元类中呢?不得而知,那么我们就去验证一下。
![]()
首先获取到LGPerson的元类地址,然后按照methods方面的方法步骤,我们成功输出sayWorld方法,也验证了上面的猜想。
这里我们不由的想,
OC的底层是C和C++实现的,在C语言和C++中,不存在对象方法和类方法的区分,有的都是函数实现,在OC的设计中,一个类可以new出无数个对象,因此把方法存在类中,而不是动态创建的对象中,是合理的。因为
OC的对象方法和类方法的定义是-和+的区分,那么方法名称就会有重名的存在,因此才会引入元类的概念,元类的存在就是解决类方法的问题。
类的结构总结
类的结构总结用一张流程图
其中protocol_ref_t是一个无符号长整型,可以强转为protocol_t,这是后面才找到的。
以上就是对于OC中类的结构分析,如有疑问或错误之处,请评论区留言或私信我,非常感谢!!!
iOS底层之对象的本质
序言
在iOS日常开发中,我们每天都会创建对象,那么对象的本质是什么呢?对象和类又是如何绑定关联的呢?
clang
OC的底层实现是C++代码,我们可以通过clang中的命令,用C++来还原OC代码。
-
Clang是一个C语言、C++、Objective-C语言的轻量级编译器,是由Apple主导编写的 -
Clang主要用于把源文件编译成底层文件,比如把main.m文件编译成main.cpp、main.o或者可执行文件。
clang 常用命令
clang -rewrite-objc main.m -o main.cpp
//UIKit报错
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// xcrun命令基于clang基础上进行了封装更好用
//3、模拟器编译
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
对象本质探索
在工程中的mian.m文件中定义如下代码,然后编译成main.cpp文件
OC代码
@interface LGPerson : NSObject
{
NSString * LGName;
}
@property (nonatomic,assign) NSInteger age;
@end
@implementation LGPerson
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
cpp代码
![]()
- 在
cpp文件中,可以看出LGPerson的定义其实是struct,也就是对象的本质是结构体。- 在
LGPerson_IMP的定义中,有一个struct NSObject_IMPL NSObject_IVARS;,这里的NSObject_IVARS是继承自NSObject中ivars,在NSObject_IMPL的定义中,可以看到其ivars只有isa一个成员。
![]()
isa的定义是Class,那我们再找到Class的定义
![]()
Class 类型实际是一个 objc_class类型的结构体指针,这里也能看到我们常用的id其实是objc_object类型的结构体指针,以及SEL是函数的一个结构体指针。
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *LGName;
NSInteger _age;
};
static NSInteger _I_LGPerson_age(LGPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_LGPerson$_age)); }
static void _I_LGPerson_setAge_(LGPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_LGPerson$_age)) = age; }
可以看到age属性变量有生成get和set方法,同时生成了_age变量,而声明的LGName则没有这些方法变化。
总结
对象的本质其实就是一个结构体,其内存大小和成员变量有关,也就是isa+成员+属性,这也验证了在alloc探索中开辟内存大小和ivars有关;- 所有自定义的类都继承自
NSObject,而NSObject是objc_object类型的结构体,其内部有且只有一个成员属性isa。
isa指针探究
在前面alloc的底层探究中,我们知道了对象通过obj->initInstanceIsa初始化isa关联类。接下来就看一下isa的结构是怎么样的,以及如何通过isa知道关联类的?
initInstanceIsa方法
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
// 初始化isa
initIsa(cls, true, hasCxxDtor);
}
initIsa 方法
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0); // isa初始化
if (!nonpointer) { // 非nonpointer指针直接绑定cls
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
在
initIsa方法中,如果是非nonpointer类型指针,则直接调setClass方法绑定;否则现有一些位域赋值操作,再调setClass方法绑定。
联合体isa_t
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
在联合体isa_t中,有cls和bits两个变量,还有一个ISA_BITFIELD是结构体里面的宏定义变量,是按位域定义的,ISA_BITFIELD的结构如下:
![]()
各变量的解释:
nonpointer:表示是否对isa指针开启指针优化, 0:纯isa指针,1:不止是类对象地址,isa中包含了类信息、对象的引用计数等;has_assoc:关联对象标志位,0-没有,1-存在;has_cxx_dtor:该对象时候有C++或Objc的析构器,如果有析构函数,则需要做析构逻辑,没有则可以更快的释放对象;shiftcls:存储类指针的值,开启指针优化的情况下,在ARM64架构中有33位来存储类指针值;magic:用于调试器判断当前对象是真的对象还是未初始化的空间;weakly_referenced:标志对象是否被指向或曾经指向一个ARC的弱变量,没有弱引用对象则可以更快的释放;unused:对象是否释放;has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位;extra_rc:对象的引用计数值(实际上是引用计数减1),例如:引用计数是10,那么extra_rc为9,如果引用计数大于10,则需要使用到has_sidetable_rc。
isa分析总结
-
isa指针分为nonpointer和非nonpointer类型,非nonpointer类型就是一个纯指针,nonpointer类型指针开启了指针优化。 -
指针优化的设计,可以更好的利用isa的内存,程序中有大量的对象,针对isa的优化节省了大量内存。
通过对对象的本质和
isa指针的分析,我们已经知道对象是如何与类进行关联的,后面就来探究一下OC中类的结构。
以上是关于OC中对象的本质分析,如有错误或疑问之处,请在评论区留言吧!!!
iOS 底层之内存对齐
序言
数据类型都有固定的内存大小,在结构体当中属性的内存排序遵循内存对齐原则,那么内存对齐的原理是怎么回事,内存对齐有什么好处?在OC中是怎么实现内存对齐的呢?
这里是不同数据类型的字节数
![]()
内存对⻬的原则
-
数据成员对⻬规则
结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。 -
结构体作为成员
如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.) -
收尾工作
结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。
实例
代码
先来看这三个结构体的内存大小
struct LGStruct1 {
double a; //8
char b; //1
int c; //4
short d; //2
}struct1;
struct LGStruct2 {
double a; //8
int b; //4
char c; //1
short d; //2
}struct2;
struct LGStruct3 {
double a; //8
int b; //4
char c; //1
short d; //2
int e; //4
struct LGStruct1 str; //
}struct3;
通过sizeof获取到struct1的内存是24,struct2的内存是16,struct3的内存是48,struct1和struct2这两个结构体所包含的数据成员是一样的,位置不一样,从而造成内存大小不一样,这就是内存对齐原则造成的。
具体分析
1.结构体struct1
- a: 8字节,offset为0,即[0 ~ 7]存放a;
- b: 1字节,offset为8,即[8]存放b;
- c: 4字节,offset为9,但要找4的倍数,即[12 ~ 15]存放c;
- d: 2字节,offset为16,16为2的倍数,即[16 ~ 17]存放d;
根据内部最大成员的整数倍原则,struct1的内存大小为24。
2.结构体struct2
- a: 8字节,offset为0,即[0 ~ 7]存放a;
- b: 4字节,offset为8,即[8~11]存放b;
- c: 1字节,offset为12,即[12]存放c;
- d: 2字节,offset为13,但要找2的倍数,即[14 ~ 15]存放d;
根据内部最大成员的整数倍原则,struct2的内存大小为16。
3.结构体struct3
- a: 8字节,offset为0,即[0 ~ 7]存放a;
- b: 4字节,offset为8,即[8~11]存放b;
- c: 1字节,offset为12,即[12]存放c;
- d: 2字节,offset为13,但要找2的倍数,即[14 ~ 15]存放d;
- e: 4字节, offset为16,16为4的整数倍,即[16 ~ 19]存放e;
- str: 24字节,offset为20,找24的整数倍,即[24 ~ 47]存放str;
根据内部最大成员的整数倍原则,struct3的内存大小为48,其实按结构体成员要从其内部最大元素大小的整数倍地址开始存储原则,str中最大元素是8,找到str的起始位置也是24。
内存对齐原因
-
内存是以字节为基本单位,
cpu在存取数据时,是以块为单位存取,并不是以字节为单位存取。频繁存取未对齐的数据,会极大降低cpu的性能。字节对齐后,会减低cpu的存取次数,这种以空间换时间的做法目的降低cpu的开销。 -
cpu存取是以块为单位,存取未对齐的数据可能开始在上一个内存块,结束在另一个内存块。这样中间可能要经过复杂运算在合并在一起,降低了效率。字节对齐后,提高了cpu的访问速率。
思考
在OC中,我们所定义的类其实也是结构体,那么我们是不是在开发中也要注意声明变量的位置,系统会不会帮我们处理这个问题呢。
这里我们新建一个类LGPerson,他的成员和结构体struct1一样,
![]()
LGPerson *person = [LGPerson alloc];
person.a = 8.0;
person.b = 'b';
person.c = 5;
person.d = 2;
NSLog(@"%lu", class_getInstanceSize(LGPerson.class));
通过输出我们发现
LGPerson的实际内存大小也是24字节,但已知类中有isa要占8个字节,所以LGPerson的成员所占内存大小是16字节。
![]()
通过上图lldb调试信息可以看出,在类的成员属性存储中,系统是进行过优化的。
总结
内存对齐可以提高cpu的存取效率同时提升安全性,会有部分内存的浪费,但是系统又会根据数据存储情况进行内存优化,尽可能降低内存浪费,这样即保证了性能又减少了浪费。
图中是malloc_size对内存大小的计算,其中就有一个先左移4位,又右移4位的算法,其实就是16字节对齐算法。