OC 底层原理(4)-对象原理( 符号表绑定,objc_alloc探索,isa关联对象与类)(随记)

1,029 阅读7分钟

一、alloc 过程中的符号表绑定

在第一篇中有贴出过 alloc 实现的整个流程图,alloc -> _objc_rootAlloc -> callAlloc -> ...

但在我们实际运行过中会发现在执行 _objc_rootAlloc 之前会先 objc_alloc 这一方法,在我们对 alloc 进行 Jump to Definition 的时候就直接到了 _objc_rootAlloc 这一方法,所以 objc_alloc 这一层 往往就会被我们忽略跳过了,再往里面走就如注释所描述,就执行[cls alloc]

那为什么会出现这个情况呢,这是因为在系统里面本身就做了一个符号绑定。 要看具体的,就需要借助一个工具进行查看了 MachOView

下载地址:pan.baidu.com/s/1Z71KyxPy… 提取码: picu

直接将要查看的Target 拖到 MachOView 里,就可以找到其的符号绑定

就相当于 objc_alloc 绑定到了 alloc 的 sel;换个方式解释就是,一个方法 有先是找到 sel 然后再是 imp。举个例子,字典查字,查找的过程就是 目录 -> 查找到字(sel) -> 根据页码找到字所在位置(imp) -> 字的详细含义等(函数实现)。

然后有个问题,在哪可以看到 sel_alloc 与 objc_alloc 类似绑定的源码呢?(答案看下图)

在实际运用总 objc_alloc 这个方法自会只走一次,那为什么呢?

看第二部分分析

二、objc_alloc (ps:这个过程有点会感觉很飘加无聊,都是正常的😂)

我们先打开汇编环境:Debug -> Debug Workflow -> Always Show Disassembly

然后运行一下项目,就进入到了汇编环境

我们可以看到在这里面 objc_alloc 方法对应的是一个符号标识 symbol stub for;然后再看 goodMorning 这个普通方法会调用一个 objc_msgSend,但是看到里面还有好几个都是符号形式,比如,objc_autoreleasePoolPush,objc_storeStrong,objc_autoreleasePoolPop都是符号形式,这是为什么呢?

这是因为没有走 objc_msgSend 方法的方法都是系统直接调用的符号。

那系统又是什么时候调用的呢?

再次走一下上面的步骤。我们先将允许的tagter 拖到 MacOView 查看 Symbol Table

在这里就看到了objc_alloc符号表绑定的地方,也正如此就知道在编译器就调用了。

如果在编译期调用那就不能直接进行探索了,这时就需要借助 LLVM ,因为 objc_alloc 的源码苹果官方并没有公布出来,所以这并不是 objc_alloc 源码,然后只能在 LLVM 里找到一些与 objc_alloc 相关的东西进行分析

LLVM工程获取连接:链接:pan.baidu.com/s/1R-3KSlea… 提取码:mfi8

打开工程后就开始在工程里找带有 objc_alloc 这个关键字的方法

全局搜索,只找到一个,见上图,看到 objc_alloc 在 EmitObjCAlloc 方法里,于是继续全局搜索
然后看到这些方法都有个共同点都是通过调用 emitObjCValueOperation 并返回,于是就不得不看看这个方法到时是什么了,全局搜索

虽然找到 emitObjCValueOperation 找到这个方法的源码,但是我们都一样,完全不知道这个方法这些参数是干嘛的,但是我们此行目的就是为了探索 objc_alloc ,所以我们就参照这个方法的参数,与上上长图片调用的 emitObjCValueOperation 这个方法的地方相比较找到传 objc_alloc 这个参数的地方,也就是 FunctionCallee &fn 这里,于是我们开始看这个 fn 干了什么,第一个出现 fn 的地方就是一个判断条件,前面我们已经看到确实有调用 objc_alloc 这个方法,所以这个判断条件是肯定不会成立的,于是接着往下看有fn 的地方,看到有个注释 // Call the function. 调用此方法,于是在 EmitCallOrInvoke 这里就开始调用 objc_alloc 这个方法了,于是有开始全局搜索 EmitCallOrInvoke 。

然后开始分析这个方法,查看有用的 Callee 和 Args 地方通过 CreateCall 返回一个实例对象。

到这里我们来回答几个问题:

1)为什么会走 objc_alloc 这个方法?

因为在底层有个LLVM,在这里面有个Callee函数,调用这个函数会有一个返回,这里有个返回就意味着调用 EmitCallOrInvoke 的返回

如图所示最后返回一个Value,因为我们最开始看的调用者一些步骤是从EmitObjCAlloc 这个方法实现开始的,于是我们现在就要全局搜索一下 EmitObjCAlloc 这个方法被调用的地方。

在tryGenerateSpecializedMessageSend 这个里找到了,在返回的上一步有个一个if判断,这个判断的意思是如果 sel 这个方法名称等于 “alloc”,就调用 EmitObjCAlloc,这个方法,调用这个方法就相当调用 objc_alloc 这个方法。然后大家可能有注意到这是一个Switch,然后case 为 OMF_alloc 是就进到这里面了,那我们就来全局找一下 OMF_alloc

这里先主要看关于 ‘alloc’,首先通过 IdentifierInfo *first = sel.getIdentifierInfoForSlot(0);获取到first,在通过 StringRef name = first->getName();得到 name,然后name 为 "alloc" 时就返回 OMF_alloc。

现在回答

1)为什么要调用 objc_alloc 创建对象而不是直接调用 alloc 创建就可以了呢? 在 tryGenerateSpecializedMessageSend 这个方法实现的地方有注释我将它贴出来

第一个方框概述大概意思就是: ObjC运行时可能提供的入口点可能比相应选择器的普通消息发送速度更快。

第二个方框概述大概意思就是: 如果运行时确实支持所需的入口点,则此方法将生成调用并返回结果。否则它将返回None并会生成 msgSend 替代。

2)为什么 objc_alloc 只会走一次?

这个问题其实在上个问题的第二个方框的里的注释就有提到,当 tryGenerateSpecializedMessageSend 返回None 的时候就会被 msgSend 替代了。也贴出一下判断的地方

就是如果if 调节不成立就会走普通消息调用的流程 msgSend 。通过跟踪alloc的源码结果证明,if 好像基本没成立过,只是try了一下

这部分太沉重了,不知道写的是什么就记结果吧,有兴趣的自己亲自去探索探索。

二、isa关联对象与类 从此处我们开始分析 isa 关联对象与类,那就从初始化 isa 源码开始分析

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
    
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif

        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

源码中有一处有个 shiftcls ,这个变量在 ISA_BITFIELD 结构体,这个结构体在上一篇中有解释,代表类的结构 ,根据newisa.shiftcls = (uintptr_t)cls >> 3 ,这句代码很明显标明了通过 (uintptr_t)cls >> 3 就可以得到类的结构,我们按照这个流程来复现一下看是怎么通过对象找到类。

验证方法一

便于理解上图最后一步,我将isa 的完整结构贴出来(里面的结构说明上篇有详写),上图最后一步的目的就是为了将 uintptr_t shiftcls(类结构取出来),我是在模拟器调试的,参考下面代码里模拟器isa的完整结构,第一步需要向右移3位,去掉结构的前3位;第二步向左20位(本来是17,但是第一步向右移了3位需要补上),去掉结构后面的17位;最后再向右移20位,就将中间的 uintptr_t shiftcls 取出来了。

# if __arm64__ //真机
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      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
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__ //模拟器
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

验证方法二

通过

LGTeacher  *object = [LGTeacher alloc];
object_getClass(object);

通过这句代码找到方法,查看这个函数的源码流程:object_getClass -> obj->getIsa -> ISA 最后在这里面有句

return (Class)(isa.bits & ISA_MASK);
#   define ISA_MASK        0x00007ffffffffff8ULL

通过 & 上一个 ISA_MASK 可以直接拿到这个类的结构了,实际操作一下