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的内容补充

  1. 通过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();
    
  1. 3/4扩容, 负载因子,当在0.75的时候,空间利用率会更好。哈希冲突低 。

一、runtime是什么?

提起runtime,就知道它是运行时,那就不得不提起下编译时,那两者的定义是什么? ==编译时==: 程序还未装入内存, 编译器把源代码编译成机器码,并在这个过程中,对编写的代码进行静态类型检查。

运行时: 程序已装入内存, 和编译时不同,仅仅对其作出类型检查,代码扫描, 程序已经开始操作运行了。

二、runtime调用的三种方式

  1. Objective-C , 方法调用,列如 [xx dosomething];
  2. Framework, 调用 isKindofClass 方法。
  3. 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;
}
  1. 解释,如:((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"))
  2. 结论: 调用方法 = 消息发送: objc_msgSend(消息的接受者,消息的主题 sel+参数)
  3. 通过上面的调用也知道,所有的方法调用,其实都是通过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]

通过上述的例子,得出一部分心得,后续在一一验证

  1. objc_msgSend(person, @selector(sayHello)); 和 通过 objc_msgSendSuper 都可以调用到父类的方法, 验证了方法的本质其实是消息发送,子类调用的方法没有找到,会到父类中去查找此方法。
  2. objc_super结构里面包含了两个成员,
    receiver 消息接受者 , super_class 父类。

三、Objc_msgSend 汇编探索

  1. msg_send的底层代码查询截图, 需要objc源码。
    在这里插入图片描述
  2. 解读下汇编的意思。
	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

整体思路整理:

  1. 进入msg_send入口
  2. 判断receiver是否是nil ,在判断是否支持SUPPORT_TAGGED_POINTERS这个类型。
  3. 支持AGGED_POINTERS对象,isa为空,就直接返回nil ,不为空,就走 CacheLookup流程。
  4. 不支持AGGED_POINTERS对象, isa为空,就走LReturnZero返回nil.
  5. 不支持AGGED_POINTERS对象, isa有值, GetClassFromIsa_p16流程,把获取到的class放到p16寄存器。
  6. GetClassFromIsa_p16 可以自己去探索下,我不展开了。 里面的主要操作就是获取class 存放到p16寄存器。

四、Class中protocol底层补充。

  1. 前面我们探索了属性,成员变量,实例方法,类方法的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
}

本篇小结

  1. 本篇文章主要也是回顾了以前的一些底层知识点,并进行了串联。
  2. runtime的初步了解。 三种方式调起运行时
  3. 方法的本质其实就是消息转发。 objc_msgSend
  4. 解读objc_msgSend汇编的源码。

以上是关于iOS开发底层之RuntimeObjc_msgSend探究 - 08的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发底层之RuntimeObjc_msgSend探究 - 08

iOS开发之结构体底层探索

iOS开发底层之对象的本质-04

iOS开发底层之内存对齐详解-03

iOS开发底层之NSObject-alloc源码分析-02

iOS开发底层之消息的快速与慢速转发 - 11