iOS开发底层之RuntimeObjc_msgSend探究 - 08
Posted iOS_developer_zhong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发底层之RuntimeObjc_msgSend探究 - 08相关的知识,希望对你有一定的参考价值。
文章目录
前言
底层探索的内容回顾- ios的底层 -对象分析
- alloc 分析的方式,通过 x/4gx, 再结合汇编断点源码进行分析
- 内存申请的算法,字节对齐等
- 对象、对象元类、根元类的ISA走位关系图探索。
- 类的内存结构探索,包含了isa,superClass, cache_t bit 等
- bit 有属性, 有方法, 也是通过x/4gx,偏移0x20,通过lldb去打印。
- cache_t 缓存的是方法,结构有, mask ocp, buckets( 哈希数组)
- 哈希函数 insert 3/4处理, 内存扩容等。
提示:以下是本篇文章正文内容,下面案例可供参考
Cache_t的内容补充
- 通过lldb得到bucket_t,注释重点
// 源码为
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
[p say1];
[p say2];
[p say3];
[p say4];
[p say5];
[p say6];
[p say7];
[p say8];
}
return 0;
}
下面是lldb操作, 重点
(lldb) p/x LGPerson.class // 1. 类对象地址
(Class) $0 = 0x0000000100008720 LGPerson
(lldb) p (cache_t *)0x0000000100008730 //2. 增加偏移地址0x10, 并且类型转换为 cache_t
(cache_t *) $1 = 0x0000000100008730
(lldb) p *$1 //3. 打印cache_t对象
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4302422928
}
}
...省略部分..........
}
(lldb) p $1.buckets(); // 4. 调用cache_t对象下的buckets方法,得到 bucket_t
(bucket_t *) $3 = 0x000000010071c390
Fix-it applied, fixed expression was:
$1->buckets();
(lldb) p *$3 // 5. 现在去objc源码中,查询bucket_t对象是什么结构,发现它内部是一个结构体,并且是联合体, 成员变量互斥。
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 45264
}
}
}
// 第一种方法打印bucket_t
(lldb) p $3[0].sel(); //6. 上面是buckets,应该是个数组结构,但内部结构是个结构体,其实这里是结构体指针,取内存平移, 取出第一个元素,并打印出方法名。
(SEL) $6 = "say1"
2021-06-29 14:08:01.523036+0800 KCObjcBuild[4077:153742] -[LGPerson say2]
(lldb) p $3[1].sel();
(SEL) $7 = "say2"
// 另外一种方式打印具体的sel
(lldb) p $3+1;
(bucket_t *) $8 = 0x000000010071c3a0
(lldb) p $8.sel();
(SEL) $10 = "say2"
Fix-it applied, fixed expression was:
$8->sel();
- 3/4扩容, 负载因子,当在0.75的时候,空间利用率会更好。哈希冲突低 。
一、runtime是什么?
提起runtime,就知道它是运行时,那就不得不提起下编译时,那两者的定义是什么? ==编译时==: 程序还未装入内存, 编译器把源代码编译成机器码,并在这个过程中,对编写的代码进行静态类型检查。运行时: 程序已装入内存, 和编译时不同,仅仅对其作出类型检查,代码扫描, 程序已经开始操作运行了。
二、runtime调用的三种方式
- Objective-C , 方法调用,列如 [xx dosomething];
- Framework, 调用 isKindofClass 方法。
- Runtime API : class_getinstanceSize 方法。
2.1. 编译时,c++源码
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];
}
return 0;
}
终端运行: clang -rewrite-objc main.m -o main.cpp
//c++ main.cpp 源码展示
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_k6_15546gzd3gxchqf10hh9g4y40000gq_T_main_e8f188_mi_2);
}
return 0;
}
- 解释,如:
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"))
- 结论: 调用方法 = 消息发送: objc_msgSend(消息的接受者,消息的主题 sel+参数)
- 通过上面的调用也知道,所有的方法调用,其实都是通过objc_msgSend, 方法的本质,就是消息的发送。
下面来测试下通过代码objc_msgSend开始调用方法。
3.1. 修改xcode的设置 build setting -> == apple Clang - preparocessing ==-> enable strict Checking of objc_msgsendCalls ==改为 false
3.4. 源码展示
@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
NSLog(@"666 %s",__func__);
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 定义一个类
LGTeacher *teach = [LGTeacher alloc];
// 继承于LGTeacher
LGPerson *person = [LGPerson alloc];
// 直接调用自己的方法
objc_msgSend(person, @selector(sayNB));
// 子类调用父类的方法
objc_msgSend(person, @selector(sayHello));
/*
struct objc_super {
#if !defined(__cplusplus) && !__OBJC2__
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
};
*/
struct objc_super zgr_object;
zgr_object.receiver = person;
zgr_object.super_class = LGTeacher.class;
// 直接给父类发送方法
objc_msgSendSuper(&zgr_object, @selector(sayHello));
}
return 0;
}
打印的结果为:
2021-06-29 16:54:37.864957+0800 001-[14107:269155] 666
2021-06-29 16:54:37.865416+0800 001-[14107:269155] 666 -[LGTeacher sayHello]
2021-06-29 16:54:37.865465+0800 001-[14107:269155] 666 -[LGTeacher sayHello]
通过上述的例子,得出一部分心得,后续在一一验证
- objc_msgSend(person, @selector(sayHello)); 和 通过 objc_msgSendSuper 都可以调用到父类的方法, 验证了方法的本质其实是消息发送,子类调用的方法没有找到,会到父类中去查找此方法。
- objc_super结构里面包含了两个成员,
receiver 消息接受者 , super_class 父类。
三、Objc_msgSend 汇编探索
- msg_send的底层代码查询截图, 需要objc源码。
- 解读下汇编的意思。
ENTRY _objc_msgSend // 入口,两个参数, isa(receiver) ,selector.
UNWIND _objc_msgSend, NoFrame
// 用isa与 #0对比
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
// 小于0 ,并且支持 TAGGED_POINTERS ,就返回这个分支的流程
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// 等于 0 ,就直接返回nil
b.eq LReturnZero
#endif
// 走到这里说明, isa是有值的。
ldr p13, [x0] // p13 = isa
// 赋值操作,把 p13, 1, x0都p16
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or 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
整体思路整理:
- 进入msg_send入口
- 判断receiver是否是nil ,在判断是否支持SUPPORT_TAGGED_POINTERS这个类型。
- 支持AGGED_POINTERS对象,isa为空,就直接返回nil ,不为空,就走 CacheLookup流程。
- 不支持AGGED_POINTERS对象, isa为空,就走LReturnZero返回nil.
- 不支持AGGED_POINTERS对象, isa有值, GetClassFromIsa_p16流程,把获取到的class放到p16寄存器。
- GetClassFromIsa_p16 可以自己去探索下,我不展开了。 里面的主要操作就是获取class 存放到p16寄存器。
四、Class中protocol底层补充。
- 前面我们探索了属性,成员变量,实例方法,类方法的lldb探索,现在补充下 ,代理方法protocol方法是放在哪儿的呢,下面上调试源码。
源码
@protocol PersonDelegate <NSObject>
-(void)dosomethingForDelegate;
@end
@interface LGPerson : NSObject<PersonDelegate>{
NSString *hobby;
}
@end
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s",__func__);
}
下面是调试的lldb操作,重点看注释.
(lldb) p/x LGPerson.class //1.获取类对象
(Class) $0 = 0x0000000100004850 LGPerson
(lldb) p (class_data_bits_t *) 0x0000000100004870 // 2. 内存偏移0x20 + 强转成 class_data_bits_t 对象
(class_data_bits_t *) $1 = 0x0000000100004870
(lldb) p $1.data(); // 3. 转换成 class_rw_t 对象
(class_rw_t *) $2 = 0x00000001006a75a0
Fix-it applied, fixed expression was:
$1->data();
(lldb) p *$2 // 4.打印 class_rw_t 对象
(class_rw_t) $3 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4294984272
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $3.protocols(); // 5. 调用class_rw_t方法中的protocols()方法
(const protocol_array_t) $4 = {
list_array_tt<unsigned long, protocol_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100004348
}
arrayAndFlag = 4294984520
}
}
}
(lldb) p $4.list; // 6. 获取protocol_array_t的list .
(const RawPtr<protocol_list_t>) $5 = {
ptr = 0x0000000100004348
}
(lldb) p $5.ptr; // 7. 获取ptr;
(protocol_list_t *const) $6 = 0x0000000100004348
(lldb) p *$6 // 8. 打印protocol_list_t
(protocol_list_t) $7 = (count = 1, list = protocol_ref_t [] @ 0x00007f8c42647468)
(lldb) p $7.list[0]; // 9. 打印protocol_list_t中list的第一个对象
(protocol_ref_t) $8 = 4294985888
(lldb) p (protocol_t *)$8; // 10. 发现对象为protocol_ref_t, 经过注释,或者protocol_ref_t方法在objc源码中的使用,发现这个是可以转换为protocol_t使用的。 所以进行强转。
(protocol_t *) $9 = 0x00000001000048a0
(lldb) p *$9; //11.打印出了protocol_t内容
(protocol_t) $10 = {
objc_object = {
isa = {
bits = 4298453192
cls = Protocol
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 537306649
magic = 0
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
}
mangledName = 0x0000000100003e8e "PersonDelegate" //找到了代理的名称
protocols = 0x0000000100004430
instanceMethods = 0x0000000100004448
classMethods = 0x0000000000000000
optionalInstanceMethods = 0x0000000000000000
optionalClassMethods = 0x0000000000000000
instanceProperties = 0x0000000000000000
size = 96
flags = 0
_extendedMethodTypes = 0x0000000100004468
_demangledName = 0x0000000000000000
_classProperties = 0x0000000000000000
}
(lldb) p $10.instanceMethods; //12.找到protocol_t中instanceMethods
(method_list_t *) $14 = 0x0000000100004448
(lldb) p *$14 //13.打印method_list_t
(method_list_t) $15 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $15.get(0); //13.打印method_list_t中的第一个对象
(method_t) $16 = {} //14.不明显,需要调用big方法进行打印
(lldb) p $15.get(0).big(); //15. 成功找到代理的方法
(method_t::big) $17 = {
name = "dosomethingForDelegate"
types = 0x0000000100003eba "v16@0:8"
imp = 0x0000000000000000
}
本篇小结
- 本篇文章主要也是回顾了以前的一些底层知识点,并进行了串联。
- runtime的初步了解。 三种方式调起运行时
- 方法的本质其实就是消息转发。 objc_msgSend
- 解读objc_msgSend汇编的源码。
以上是关于iOS开发底层之RuntimeObjc_msgSend探究 - 08的主要内容,如果未能解决你的问题,请参考以下文章