iOS 源码解析 - Runtime篇 (2 objc_msgSend)

objc-runtime 开源地址

由于OC是属于C的超集再加上runtime的存在,我们写的每一个OC方法在编译阶段被转成
id objc_msgSend(id self, SEL op, ...)

关于它的实现已经有大神提供了C语言版本的实现由于每个OC方法都会转换成这个函数调用,所以它的高效性显得尤为重要。

关于objc_msgSend的实现过程,上篇文章其实我们也有提到过,归根到底,就是利用SEL去寻找IMP,执行目标函数。
我们来分析一下这个"寻根"的过程:

YY大神在他的博客中提到:

id objc_msgSend(id self, SEL op, ...) {
    if (!self) return nil;
    IMP imp = class_getMethodImplementation(self->isa, SEL op);
    imp(self, op, ...); //调用这个函数,伪代码.
}

class_getMethodImplementation他的实现在源码里是可以找到的。

  • 第一步调用这个函数
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

然后主要是这个函数:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)

这个方法的执行过程很有趣:

// 先从cache里检查是否存在
 if (cache) {
        imp = cache_getImp(cls, sel); // 此方法在上面👆大神提供的C语言实现中,有具体实现。
        if (imp) return imp;
    }
 // cache寻找
  cls   = self->isa;
  cache = cls->cache;
  hash  = cache->mask;
  index = (unsigned int) _cmd & hash;
      
  do{
     method = cache->buckets[ index];
     if(!method) goto recache;
     index = (index + 1) & cache->mask;
  }while( method->method_name != _cmd);

  return( (*method->method_imp)( (id) self, _cmd));

为了读懂上面的代码。这里不得不提的事class的源码结构:盗Vanney大神的图

bits.png

isa superclass 这二者的作用很容易理解。
cache 缓存的方法列表
class_data_bits_t bits这个结构体非常重要!它存储了非常多的信息,包括编译时确定的类的变量信息,方法列表,协议方法列表,weak表... 关于它我们稍后再谈。

现在我们知道了Class类型的内部结构,回头我们再看下刚才的消息调用。

  • cls = self->isa; // 通过isa指针拿到当前对象的class
  • cache = cls->cache; // 通过class拿到cache—方法缓存列表
  • hash = cache->mask; index = (unsigned int) _cmd & hash; // 通过cmd和cache掩码的与运算获取method在map表中的序列号(这里我们可以看到,哈希表中其实存储的是method即SEL和IMP的映射关系),进而拿到最终的IMP指针。
struct method_t {
  SEL name;
  const char *types;
  IMP imp;
}

通过这一系列的操作我们最终获取到了函数的地址。但这仅仅是从方法缓存中获取方法。那么,如果cache里没有对应的IMP呢?

在回到IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)函数,往下走会代码会执行到这一句:
它主要对类创建了真正的运行时环境( rwlock_writer_t lock(runtimeLock); 保证线程安全)。

if (!cls->isRealized()) {
  rwlock_writer_t lock(runtimeLock);
  realizeClass(cls);
}

具体实现:

static Class realizeClass(Class cls){
...
 // 中间代码
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }
...
}

到了这里我们可以接着看class_data_bits_t结构体了:

// 只截取了部分 源码
struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;
  
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        assert(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        bits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
    }
    bool isSwift() {
        return getBit(FAST_IS_SWIFT);
    }
}

这个结构体中只有一个变量 uintptr_t bits; 它是一个拥有指针存储功能的unsigned long 类型。它只有64位大小,指针存储结构如下:

class_bits.png

通过与bits与对应flag的按位运算得到对应的指针地址。比如:

bool isSwift() {
     return getBit(FAST_IS_SWIFT);
}

#define FAST_IS_SWIFT (1UL<<0)与上图的结构一直,第一位标识位储存是否是Swift语言的flag(由编译器设置)。

不过,最关键的还是下面这个:

 class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
}

它的data函数,返回一个class_rw_t 类型的结构体指针。

根据上面的数据结构分布图,bits 里有44位储存着class_rw_t

这一点可以在#define FAST_DATA_MASK 0x00007ffffffffff8UL里解释。

然而,在源码中,ro = (const class_ro_t *)cls->data();class的data即:

    class_rw_t *data() { 
        return bits.data(); // 调用的上面的函数
    }

被强转成了const class_ro_t *这是为什么呢?
其实,在runtime调用之前,编译之后,bits.data()也就是bits的class_rw_t data是指向const class_ro_t结构的。
我们再来看class_ro_t的结构


struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

这里面正真存储了,class在编译时期就确定的属性,方法,协议等等。
而且这里的

    method_list_t * baseMethodList;
    const ivar_list_t * ivars;

都是基于entsize_list_tt实现,保证了它们在runtime期间的不可变性。
同时我们在这里也可以顺便解释下分类方法的加载过程,为什么在分类中不能添加成员变量的问题:
我们创建的分类其实在源码中属于另一种类型:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};

可以看到,它其实是没有isa指针的!但这并不能解释不能添加变量的问题。
我们要从它的装载过程说起。
在app启动后,系统会调用load_images的方法,来加载各种库文件,当然就包括runtime库,下面是objclib的加载过程:

_objc_init
└──map_2_images
    └──map_images_nolock
        └──_read_images

当执行到_read_images的时候,我们可以在源码中找到实现过程
线从boundle里获取class目录。
然后我们会发现,在这里调用了realizeClass(cls);方法!为类开辟了runtime预备环境(将bits的data重新指向了class_rw_t类型,并且将class_ro_t放入了class_rw_t的ro变量中)。
做完这些之后,在是对categories的处理。
真正实现分类中的类attach到class的方法是:

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches);
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);

可以看到,分类中的method等是被赋予到了,cls->data()中,这是cls->data()指向的是class_rw_t类型。
而在category_t中只有property_list_t没有ivar_list_t, 并且在class_rw_t ro 中的ivar_list_t又是只读的,所以分类中的属性是不会生成实例变量的(但是可以利用另一种方法变相实现“添加变量”)。

苹果这样做的目的是为了保护class的在编译时期确定的内存空间的连续性,防止runtime时期增加的变量或者方法造成的内存重叠。

继续objc_msgSend的调用过程,通过isa指针得到的method_list_t等信息,我们就直接可以得到对应的IMP,然后调用函数,同时存入cache表中。

这一切都是基于函数能够成功调用的前提。那么,如果IMP没有找到呢?runtime会被触发另一套机制——消息转发。

关于runtime 方法调用源码中还有好多细节,由于精力能力有限,以后会慢慢补充!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,198评论 6 514
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,334评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,643评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,495评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,502评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,156评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,743评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,659评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,200评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,282评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,424评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,107评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,789评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,264评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,390评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,798评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,435评论 2 359

推荐阅读更多精彩内容