程序猿阵线联盟-汇总各类技术干货iOS学习笔记IOS

objc_msgSend汇编源码分析

2018-12-01  本文已影响1人  AlexTing杂货店

引言

Objective-C是通过消息机制调用方法的,编译器会把所有消息发送转为objc_msgSend方法调用。说到objc_msgSend的汇编实现,大多数人会觉的是因为性能高才用汇编实现,几乎没有文章说其它原因。Objective-C所有方法都会转为objc_msgSend方法调用,然而每个方法参数和返回值都可能不一样,参数和返回值要怎么处理?

Objective-C对象结构

Objective-C中消息发送核心数据结构如下:

//以下代码均为arm64平台
typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
    isa_t isa;
}

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;  //class_rw_t*
}

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

union isa_t 
{
    Class cls;
    uintptr_t bits;
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
}

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}

struct bucket_t {
    cache_key_t _key;//实际上是selector
    IMP _imp; //实际上是函数指针
}

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
}

NSObject子类的实例都有个isa指针,isa指向Class,Class有superclass、cache、实例方法、属性、protocol等Runtime信息,调用实例方法的时候就是通过isa指针找到Class,然后找到IMP调用实际的方法。
Class本身也是一个对象,也有isa指针,指向meta-class,meta-class也是一个对象,有类方法等属性,调用类方法的时候,就是通过Class对象的isa指针找到meta-class,然后找到IMP调用实际的方法。
实例、Class、meta-class关系如下图,图片来源

对象关系图

消息机制

当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个,
objc_msgSend、 objc_msgSend_stret、 objc_msgSendSuper 和 objc_msgSendSuper_stret。

objc_msgSend查找selector的IMP,然后调用实际的方法,主要包括以下流程:

  1. 查看cache是否有selector的IMP,如果有的话直接调用
  2. 如果没cache,最终会调用lookUpImpOrForward,从类方法列表查找IMP并缓存到cache
  3. 如果方法列表没有则会查找基类的方法,直到最上层基类(查找基类的时候也是先查找缓存,再查找方法列表)
  4. 如果基类也没查找到,则返回_objc_msgForward的IMP,走消息转发流程。

我们也可以自己通过class_getMethodImplementation拿到方法IMP(IMP是实际方法的函数指针),然后调用:

//[view addSubview:view2]
void (*funtion_pointer)(id, SEL, UIView*) = (void(*)(id, SEL, UIView*)) class_getMethodImplementation((id)view, @selector(addSubview:));
funtion_pointer(view, @selector(addSubview:), view2)

汇编源码

objc_msgSend汇编源码在Messengers.subproj目录,具体汇编如下:

objc_msgSend汇编源码 _objc_msgSend_uncached汇编源码

objc_msgSend汇编代码不长,结合objc源码比较容易看懂。需要注意的是isa和TaggedPointer格式,isa指针不是纯粹的指针,还保存很多其它信息,具体可以参考isa_t union定义,其中只有3到35位才是class指针,所以查找之前会通过mask转换成class指针。

isa格式

iOS系统为了提高性能和减小内存,使用了TaggedPointer来表示NSNumber、NSIndexPath等对象,对象并没有分配内存空间,而是把对象值保存在指针里面,只有指针无法容纳对象才会分配实际内存。TaggedPointer具体格式如下图,tag index表示具体Class,系统有维护一个全局映射表来保存tag index和Class的关系,具体可以查看objc_tag_index_t定义,查找到具体Class之后就跟正常oc对象一样查找IMP了。

TaggedPointer指针格式

Calling Conventions

arm64架构是通过q0-q7和x0-x7来传函数参数,可以看到objc_msgSend没对这几个寄存器做任何操作,找到IMP后直接通过br x17调用IMP,br告诉cpu不是子程序调用。
Objective-C所以方法发送都是通过objc_msgSend,每个方法返回值和参数都不一样,如果objc_msgSend像普通函数一样处理参数,为了处理不同参数类型和参数个数,可以使用varargs ,Objective-C调用的地方必须包裹成varargs,这样处理非常不灵活,objc_msgSend用了个很巧妙的技巧,就是对参数不做任何处理,查找到IMP后直接调用,因为在objc_msgSend开始执行时,栈帧(stack frame)的状态、数据,和各个寄存器的组合形式、数据,跟调用具体的函数指针(IMP)时所需的状态、数据,是完全一致的,所以我们用xcode调试的时候函数栈是看不到objc_msgSend,看上去就是消息发送过程完全没发生过,跟调用普通的c方法一摸一样。

黑科技

objc_msgSend用很巧妙的技巧处理参数问题,利用这种技巧可以做很多方法,比如可以实现Aspects的效果,在调用实际方法前做些hook操作,hook完后再调实际方法。也可以使用libffi处理参数问题,可以搞很多事情。

引用

Why objc_msgSend Must be Written in Assembly
面向切面 Aspects 源码阅读
What is a meta-class in Objective-C?
Objective-C 中的消息与消息转发

上一篇下一篇

猜你喜欢

热点阅读