iOS 底层原理 1 —— 简单解析 objc_msgSend

1. 初入宝地 - objc_msgSend 的作用

objc_msgSend 的作用就是根据两个参数------ self 和 selector 找到 IMP、并执行 IMP

selector:selector 是 SEL 的一个实例,是方法在运行时的标识符。 IMP :函数指针,就是函数执行的入口。 Method 对象就是函数对象,它是一个结构体,结构体包含 selector 和 IMP:
/// Method struct objc_method {

SEL method_name;

char *method_types;

IMP method_imp; };

objc_msgSend的伪代码如下:

objc 复制代码
id objc_msgSend(id self, SEL _cmd, ...){
    // 判空

    // 根据 isa 指针,找到类对象
    Class c = object_getClass(self);
    IMP imp = cache_lookup(c, _cmd);
    if(!imp)
        imp = class_getMethodImplementation(c, _cmd);
    return imp(self, _cmd, ...);
}

这个函数的主要作用是找到 IMP,执行 IMP。

是可以在 Objective-C 函数中直接调用 objc_msgSend ,来给一个对象发送消息

objc 复制代码
#import <objc/runtime.h>
#import <objc/message.h>

- (void)testObjcMsgSend {
    SEL sel = @selector(getString:); // 先获取方法SEL
    
    // 这样就可以成功执行方法,相当于[self addSubviewTemp:[UIView new] with:@"Temp"];
    // 在调用 objc_msgSend 时,要把它转换成对应的函数指针类型,所以前面加了一大串类型转换的代码
    // These functions must be cast to an appropriate function pointer type before being called.
    
    NSString *str = ((id (*)(id, SEL, NSString*))objc_msgSend)(self, sel, @"Temp");
    
    // 如果把 Xcode-> target -> build setting -> Enable Strict of objc_msgSend Calls 置为 NO,就可以用简单写法(无需对objc_msgSend 函数的类型进行转换)
    // NSString *str = objc_msgSend(self, sel);
    NSLog(@"执行了getString后 获得的值是%@", str);
}

- (NSString*)getString:(id)obj {
    return @"Eason";
}

2. 挑灯细览 - objc_msgSend 的实现

细看objc_msgSend 分析它的汇编实现,那就是四个步骤

1. 传入对象判空

2. 获取对象的 isa指针,找到对应的 Class 对象

对象有两种,普通对象和Tagged Pointer 类型的对象

Tagged Pointer 类型的对象,是小对象,对象指针本身就存放了对象的值(比如 NSString, NSNumber)。这种对象的好处是可以节省内存空间 。但是也带来了新的问题,就是Tagged Pointer 类型的对象本身已经没有多的空间存放完整的 isa 指针信息了,所以只能有一个索引值。

索引值有啥用?

系统定义了两个数组,存放「Tagged Pointer 类型对象」的类信息,如下:

objc 复制代码
 extern "C" { 
    extern Class objc_debug_taggedpointer_classes[16*2];
    extern Class objc_debug_taggedpointer_ext_classes[256];
}

那索引值就可以用来在两个数组中找到对应的类信息了。

这是享元模式的应用,对于大量只有细微之处不一样的对象,就只创建一个对象存放基本不变的数据。对象们共享这个大对象,从而减少了内存的占用。

3. 在 Class 对象的缓存中找 IMP

哈希查找。

哈希桶在运行时会动态添加,在多线程环境下如何保证同步和数据安全呢?

写写冲突:引入全局互斥锁。

读写冲突:编译屏障。简单来说就是使用 ldp 汇编命令,把哈希桶中的数据 buckets 和 mask|occupied读到两个寄存器中,后续就用这两个寄存器进行哈希表查找。更新这两个寄存器的函数是 setBucketsAndMask,

cpp 复制代码
//设置更新缓存的哈希桶内存和mask值。
  void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    // objc_msgSend uses mask and buckets with no locks.
    // It is safe for objc_msgSend to see new buckets but old mask.
    // (It will get a cache miss but not overrun the buckets' bounds).
    // It is unsafe for objc_msgSend to see old buckets and new mask.
    // Therefore we write new buckets, wait a lot, then write new mask.
    // objc_msgSend reads mask first, then buckets.

    //编译屏障
    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    buckets = newBuckets;
    
    // ensure other threads see new buckets before new mask
    mega_barrier();
    
    mask = newMask;
    occupied = 0;
}

编译屏障保证了先执行 buckets 赋值而后执行 mask 赋值,从而使得多线程环境下读写也不会有数据一致性问题。

4. 找到了 IMP 就执行 IMP,没找到就执行objc_msgSend_uncached 函数(查找方法并执行)

这一步就是我们熟悉的、去 class 的 method_list 找,找不到就去父类中找。

然后动态方法决议。

然后消息转发。


参考文章

  1. iOS 调用 IMP/objc_msgSend 详细说明: 讲了 objc_msgSend 的定义,调用方法;如何直接获取 IMP 并调用
  2. 深入解构 objc_msgSend 函数的实现:objc_msgSend 的汇编实现,以及用到的技术
  3. 从源代码看 ObjC 中消息的发送:演示了如何在查找方法实现时、替换缓存。
相关推荐
2501_916013742 小时前
HTTPS 抓包难点分析,从端口到工具的实战应对
网络协议·http·ios·小程序·https·uni-app·iphone
2501_915918414 小时前
uni-app 项目 iOS 上架效率优化 从工具选择到流程改进的实战经验
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张4 小时前
如何在不同 iOS 设备上测试和上架 uni-app 应用 实战全流程解析
android·ios·小程序·https·uni-app·iphone·webview
wjm0410065 小时前
ios面试八股文
ios·面试
张较瘦_8 小时前
[论文阅读] 人工智能 + 软件工程 | 大模型破局跨平台测试!LLMRR让iOS/安卓/鸿蒙脚本无缝迁移
论文阅读·人工智能·ios
m0_6410310517 小时前
在选择iOS代签服务前,你必须了解的三大安全风险
ios
开开心心loky18 小时前
[iOS] push 和 present Controller 的区别
ui·ios·objective-c·cocoa
白玉cfc1 天前
【iOS】push,pop和present,dismiss
macos·ios·cocoa
低调小一1 天前
iOS 开发入门指南-HelloWorld
ios
2501_915918411 天前
iOS 开发全流程实战 基于 uni-app 的 iOS 应用开发、打包、测试与上架流程详解
android·ios·小程序·https·uni-app·iphone·webview