iOS消息机制的慢速查找流程

一、前言,

在iOS消息机制过程中存在两种查找imp 的方式,另外一种就是慢速查找,我们都知道快速就是走汇编流程,因为汇编本身就计算机能识别的语言。所以并且上一篇文章已经着重介绍了快速查找流程,本文我们着重介绍一下慢速查找流程。

我们在项目中创建一个类LGPerson 继承自NSObject然后创建改类对象的示例方法;

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

- (void)sayNB;
- (void)sayMaster;
- (void)say666;
- (void)sayHello;

@end

在main.m 文件中生成相应的对象实现

         LGPerson *person = [LGPerson alloc];
        [person say666];

我们用断点调试相关的say666 方法,然后进入相应的方法查找流程;

二,慢速查找

1,断定定位

首先我们跟着断点调试进入编译流程,进入XCodeDebug > 下的 Debug WorkFlow 下的 Always show Disamessbly 界面如下

图片.png

在此信息中我们能看你的的是我们创建的当前对象 LGPerson 以及我们调用的方法 say666,我们再次 control + step into 进入

图片.png

此时我们能看到相应的调试信息已经跳转的相应位置是_objc_msgSend_uncached,我们顺着再次进入调试信息到:

图片.png

通过调试方法我们最终定位到,慢速查找流程最终执行的是lookUpImpOrForwardobjc-runtime-new.mm:的 弟 6099行
当然我们用快速查找流程的 CheckMiss 也可以进入同样的位置信息。定义如下;

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

我们使用的是NORMAL,所以执行的是 __objc_msgSend_uncached 和上边的查找流程一模一样;

2,源码分析

顺着代码我们定位到相应的方法里;

lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

进入后代码的实现如下:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

........................................ 

此处代码很多,我们逐一的分析;

  • 代码段1
 const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

首先定义了一个forward_imp 类型为 _objc_msgForward_impcache,吧imp 设置为nil; 然后开始共缓存本类的缓存中取imp;因为我们在做某些操作的时候很有可能内存已经进行缓存;所以优先取自己的缓存列表;

  • 代码段2 checkIsKnownClass(cls); 判断当前类是否是我们已知的类,因为只有系统熟悉,也就是我们创建的类对象,才能进行相应的属性、列表,协议以及分类等方法的写入,从而才能进行查找流程。

  • 代码段3

 if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

进入到最终的实现代码定义如下

 class_rw_t *rw;
    Class supercls;
    Class metacls;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    ASSERT(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }
   supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

代码只摘取了片段,次片段的主要作用是取出当前类对象的data()数据,和我们之前读取的类的bits_t一样,取出过后对相应的ro,rw等赋值,确保类对象所有的数据结构都存在并且不为空;

最后两句代码也是非常的重要,起作用大致就是把相关的继承链都实现,从而确保了isa走位图的流通,
以及元类都存在;

isa走位图.png

也就是进行相应的初始化操作,确保我们所需与查找的流程以及环境的初始化

  • 代码段4
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);

    }

也就是系统调用的一些初始化方法,包括loadinitialize,这些方法都不需要我们自己手动的调用,系统在创建的时候就已经为我们调用了;

3,imp的查找

所有的方法查找都是先从自己的缓存方法列表中查询,因为自己有实现了就没必要查找父类方法了,这样会节省时间和性能,从而节约资源

Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

从当前类方法列表查找代码如下

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

从代码的源码中我们就知道相关的查找算法是二分查找
二分法原理

在用二分法进行查找时,查找对象的数组必须是有序的,即各数组元素的次序是按其值的大小顺序存储的。其基本思想是先确定待查数据的范围(可用 [left,right] 区间表示),然后逐步缩小范围直到找到或找不到该记录为止。具体做法是:先取数组中间位置(mid=(left+right)/2)的数据元素与给定值比较。若相等,则查找成功;否则,若给定值比该数据元素的值小(或大),则给定值必在数组的前半部分[left,mid-1](或后半部分[mid+1,right]),然后在新的查找范围内进行同样的查找。如此反复进行,直到找到数组元素值与给定值相等的元素或确定数组中没有待查找的数据为止。因此,二分查找每查找一次,或成功,或使查找数组中元素的个数减少一半,当查找数组中不再有数据元素时,查找失败。

时间复杂度

1.最坏情况查找最后一个元素(或者第一个元素)Master定理T(n)=T(n/2)+O(1)所以T(n)=O(log2n)
2.最好情况查找中间元素O(1)查找的元素即为中间元素(奇数长度数列的正中间,偶数长度数列的中间靠左的元素)

空间复杂度
  1. S(n)=logn

有若干个数按由小到大的顺序存放在一个一维数组中,输入一个数x,用二分查找法找出x是数组中第几个数组元素的值。如果x不在数组中,则输出“无此数!”。
(1)编程思路。
设有一数组a[n],数组中的元素按值从小到大排列有序。用变量low、high和mid分别指示待查元素所在区间的下界、上界和中间位置。初始时,low=0,high=n-1。
1)令 mid = (low+ high) /2 。
2)比较给定值x与a[mid]值的大小
若a[mid] == x ,则查找成功,结束查找;
若a[mid]> x ,则表明给定值x只可能在区间low ~ mid-1内,修改检索范围。令high=mid-1,low值保持不变;
若a[mid]< x ,则表明给定值x只可能在区间mid+1~high内,修改检索范围。令low=mid+1,high值保持不变。
3)比较当前变量low和high的值,若low≤high,重复执行第1)、2)两步,若low>high,表明数组中不存在待查找的元素,查找失败。
例如,设一有序的数组中有11个数据元素,它们的值依次为{3,8,15,21,35,54,63,79,82,92,97},用二分查找在该数组中查找值为82和87的元素的过程如图1所示。

image

这样我们就能快速查找到我们相应的方法了,通过方法编号去查找,快速有效;

  • 1 如果查找成功就进行相应的 log_and_fill_cache操作,即 cache_fill(cls, sel, imp, receiver);cache->insert(cls, sel, imp, receiver); 也就是我上一篇文章所提到的cache_t流程,写入自己的缓存,方便以后方法调用的时候查找,从而达到快速的目的;

  • 2 如果查找失败,则进入父类的方法列表中查询;

if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

此时我们能知道所有类对象和父类之间是一个双向列表的关系;

curClass = cls;
curClass = curClass->superclass

如果查找成功就进行相应的 log_and_fill_cache操作,即 cache_fill(cls, sel, imp, receiver);cache->insert(cls, sel, imp, receiver); 如果查找失败,跳出循环;

4,递归查找

当从我们创建的类对象的method_list 和父类method_list都没找到相应的方法实现是;我们就会进入

  imp = cache_getImp(curClass, sel);

cache_getImp(curClass, sel);;
全局搜索能发现我们的方法进入

STATIC_ENTRY _cache_getImp

    mov r9, r0
    CacheLookup NORMAL, _cache_getImp
    // cache hit, IMP in r12
    mov r0, r12
    bx  lr          // return imp
    
    CacheLookup2 GETIMP, _cache_getImp
    // cache miss, return nil
    mov r0, #0
    bx  lr

    END_ENTRY _cache_getImp

所有我们进入了CacheLookup 然后又会进入到 lookUpImpOrForward,所以此方法会来回的递归,知道找到nil为止,

5、动态方法解析;

以上4个步骤就是所有的查找流程,从创建的对象LGPerson 自身开始,一直带父类,再到根类一直到NSObject如果都没找到相应的方法实现,那么就会进入一下代码,然后进行一次动态拯救的机会

if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
   }

我们断点调试进入会发现程序在执行到这里之前是不会报错的


图片.png

所以即使所有的类中都查找了没找到我们调用的方法实现,但是只要我们在这之前进行了次动态方法解析,告诉系统我们会执行相应的操作,那么系统是不会存在报错问题的。

进行动态解析后系统又会继续调用lookUpImpOrForward 从而检测到该方法的实现

三,总结;

上一篇文章我们已经详细的介绍了快速的查找流程,此次是着重介绍方法机制的慢速查找,这样就完整的介绍了在消息调用机制中的方法查找流程,此次学习是个人的一个记录和成长,还请各位大神多多指教。

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