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 中消息的发送:演示了如何在查找方法实现时、替换缓存。
相关推荐
若水无华2 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"2 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频