美文网首页
白话文之Runtime

白话文之Runtime

作者: 旺仔Milk | 来源:发表于2019-04-10 21:57 被阅读0次

关于Objective-C / Swift

首先大部分开发流程基本都是
编写代码 -> 编译代码(连接) -> 运行程序

  • 先来看看 C / C++
    它是静态语言,大意就是 "代码写的是什么,编译完运行程序的时候就是什么了"
  • OC / Swift
    这是动态语言, "写的是什么,编译完了运行的时候不一定就会运行写的代码"
    (可以在程序运行时动态的修改代码)
    这就是Runtime

ISA指针

相信有点基础的同学肯定都知道了:
实例对象可以通过 isa 查到到类对象( Class );
类对象可以通过 isa 查到到元类对象( MetaClass);

32位架构

在以前的32位架构中 isa 本身就是 类对象( Class ) / 元类对象( MetaClass )的指针地址;
Class isa

ARM64位架构

  • 在 ARM64架构下,对 isa 进行了优化,需要使用 &ISA_MASK进行位运算后得到值才是真实的 类对象 / 元类对象的地址值
  • 这里可能会有疑问了 "64位下这么一搞那不是更麻烦了么"?, 从直观上来看确实变麻烦了,还需要位运算一次 才能拿到真实的地址值,不过如果你真正理解 isa 之后再来回答这个问题就会给出相反的答案, 这样做的确是一个优化方案
    isa_t isa

--> isa_t

首先你可能需要了解一下 <共用体> 这个概念,
大概的意思就是通过"位域'的技术,使用"位(bit)"在存储相关的值(通过位运算来取值)
这样使用 "位(bit)"来存储而不是字节(Byte)可以更加节省空间, 从而使内存进一步优化,这也证明了为什么说ARM64位架构下 isa 是一个优化方案

比如 我有3个属性要保存 在代码中直接写的话 可能就需要 12个字节(假设一个变量占4个字节);
但是如果用 二进制来存储的话 只需要占用3位(每个变量占一位),连一个字节都不到;
这样通过二进制位来存储比普通的字节来存储要节省空间多的多
1字节(Byte) = 8位(bit)

tips
知识点1: 位域(点击跳转百度百科)共用体(点击跳转百度百科)
知识点2: 类对象 / 元类对象的地址值最后3位永远都是0

struct objc_object {
private:
    isa_t isa;
}
struct isa_t {
    // 右边为最低位
    nopointer:  是否为非指针类型( isa 经过优化的, 引用计数存储在 extra_rc 中)
    has_assoc: <曾经>是否有设置过关联对象, 如果为 0  释放的会更快
    has_cxx_dtor:  是否有 cpp 析构函数(类似OC的dealloc 释放内存函数), 如果为 0  释放的会更块
    shiftcls: 类对象 / 元类对象 的指针地址
    magic: 在调试时候用于分辨 是否已经初始化
    weakly_referenced : <曾经>是否有弱引用指向  如果为0 释放的会更快
    deallocating:  是否正在被释放
    has_sidetable_rc : (值为1的话)如果引用计数 在 extra_rc 放不下(大于19位) 就会放到 sidetable 中
    extra_rc: 在 isa 指针中存放的引用计数(存储的值是 引用计数器值 - 1)
}

Class 对象结构

// 元类对象可以看做特殊的类对象
objc_class

struct objc_class : objc_object {
    // Class ISA;  isa在父类中
    Class superclass;
    cache_t cache // 方法缓存
    class_data_bits_t bits // 用于存放具体的类信息  通过bits & FAST_DATA_MASK 可以得到 class_rw_t
}

上面bits & FAST_DATA_MASK 得到 class_rw_t

struct class_rw_t{
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;   // 类的原始信息(只读)
    // methods, properties, protocols 都是二维数组 包含了 类本身的 和分类添加的 都在里面
    method_array_t methods;         // 方法列表(二维数组  [ method_list_t ][ method_t ])
    property_array_t properties;    // 属性列表(二维数组)
    protocol_array_t protocols;     // 协议列表(二维数组)
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
}

上面class_rw_t中的class_ro_t

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;  // 内存中占用的空间
    uint32_t reserved;
    const uint8_t * ivarLayout;
    const char * name;  // 类名
    method_list_t * baseMethodList;  // 这里直接就是一维数组  [ method_t ]
    protocol_list_t * baseProtocols;  // 这里直接就是一维数组
    const ivar_list_t * ivars;  //成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

方法列表(二维数组)的结构
method_array_t -> [method_list_t]
method_list_t -> [method_t]
method_t方法的结构

struct method_t {
  //*** 不同类中的相同方法名的 SEL地址其实是一个, 唯一的一个 
  SEL name   // 函数名   SEL类型 可以看做char *类型,就是一个字符串  
  const char *type  // 编码 type codeing  ( v : @  balabala...)
  IMP imp  //指向函数实现方法的指针(函数指针)
}

在回到 objc_class中的 cache_t 结构
cache_t 这个东西也引出来了很重要的一个面试内容: 消息传递机制

先来看看 cache_t 是什么?

方法缓存, 会缓存曾经调用过的方法, 使用 散列表 的结构存储
散列表又叫哈希表,那么什么是散列表(点击跳转百度百科)

struct cache_t {
  struct bucket_t * _buckets;  // 散列表
  mask_t _mask;        // 值为 散列表的长度 - 1
  mask_t _occupied;  // 已经缓存的方法数量
}
struct bucket_t {
cache_key_t  _key;   // SEL内存址作为key,  上面提到过  相同方法名的SEL内存地址是一样的
IMP _imp;   // 方法实现函数的内存地址
}
Q: 为什么要用 SEL作为key存在散列表里面?

A: 因为 objc_msgSend(objc, @selector(func)) 发送消息 是通过 @selector()给对象发送消息的

Q:为什么要用散列表当做cache_t的数据结构?

A: 因为散列表可以利用算法 生成对应的索引 key的地址 & _mask(cache_t结构里的属性) = 对应的索引(这也是散列表的核心概念, 散列表 之所以快是牺牲内存空间换效率)
tips: key的地址 & _mask 永远小于_mask值(知识点: 位运算)

objc_msgSend

OC中的方法调用在底层都是转为objc_msgSend函数的调用
白话文 : 给方法调用者发送消息

objc_msgSend( id , _cmd_ )
_cmd_传入参数(sel_registerName()等价于 @selector())

objc_msgSend执行流程(底层实现)

1. 消息发送(消息传递)
2. 消息转发
3.如果能走到这里就会报错 unrecignized selector sent to instance

_1. 消息传递机制流程(方法调用 / 消息发送流程)

  1. objc_msgSend(objc, @selector(func))开始
  2. 会通过objcisa到对应的Class / mateClass 中的cache_t缓存列表中查找是否hit(命中)
    • isHit 直接返回
    • noHit 进入3
  3. 在查找当前类的 methods方法列表中查找
    • methods中查找方法分 有序数组(二分查找)和 无序数组(遍历)
    • isHit 将方法缓存到当前类的cache_t并返回
    • noHit 进入4
  4. 根据superclass指针到父类的cache_t方法列表中查找
    • isHit 将方法缓存到当前类(上面的objc的类)cache_t并返回
    • noHit 进入5
  5. 根据superclass指针到父类的methods方法列表中查找
    • isHit 将方法缓存到当前类(上面的objc的类)cache_t并返回
    • noHit 进入6
  6. 继续沿着superclass指针向上查找直至 根类 / 根元类的cache_tmethods
    • isHit 将方法缓存到当当前类(上面的objc的类)cache_t并返回
    • noHit 则进入6

6.进入消息转发流程

_2. 消息转发流程

  1. 先会进入 resolveInstanceMethod / resolveClassMethod 方法进行动态解析
    • 第一次进入
      a. 查看 triedResolver 标记 (第一次进入为 false
      b1. 通过 class_addMethod添加方法 return true (其实这里 return true 或者 false 都一样 只要有处理 就不会进入下一步了,不过苹果官方 推荐return true)
      b1.1.class_addMethod会将方法动态添加到class_rw_tmethods
      or b2. return [super resolveInstanceMethod]
      c. triedResolver标记为 true
      c. 然后 回到消息传递的 1. 步骤重新走一遍
    • 非第一次进入(说明第一次进入没有动态添加方法实现)
      a.查看 triedResolver 标记(此时标记已经为 true)进入 2
  2. 如果上面1步骤没有得到处理 则会进入fowardingTargetForSelector方法 注意!!这里有对应的- 对象方法+ 类方法
    • 可以将消息(SEL)转发给其他对象去处理; ___forwarding___(不开源) 通过汇编大概可以追溯到 拿到return [[objc alloc] init]; 之后 objc_msgSend(objc, sel) (这里表述的过于简单了,详细细节可以自行google)
    • return nil 则进入3
  3. 这是会进入methodSignatureForSelector方法来创建一个方法签名 注意!!这里有对应的- 对象方法+ 类方法
    • 如果创建了一个有效的方法签名则进入 4
    • 如果return nil或仍未处理则进入5
  4. 会进入 forwardInvocation 注意!!这里有对应的- 对象方法+ 类方法
    • anIvocation.target指定方法调用对象(谁来处理这个消息,和 2. 转发对象意义相同)
    • 其实能进到这个方法 就可以随意处置消息了 哪怕是随便打印个NSLog 甚至 不写代码 也不会进入5 进而报错
  5. 此时会进入doesNotRecognizeSelector方法
    • 抛出异常 unrecignized selector sent to instance app崩溃

_3.关于消息传递与消息转发基本就这么多,部分细节如果深究请自行阅读原代码

Class

关于 isKindOfClass & isMemberOfClass 面试题

相关文章

  • 白话文之Runtime

    关于Objective-C / Swift 首先大部分开发流程基本都是编写代码 -> 编译代码(连接) -> 运行...

  • iOS runtime(三)runtime之method(1)m

    iOS runtime(一)runtime之Property 详尽iOS runtime(二)runtime之Iv...

  • 基础篇

    Runtime之必备C知识 Runtime之类的本质 Runtime之消息处理策略 Runtime之常用API 进...

  • Runtime

    kyson老师 iOS开发之runtime(1):runtime调试环境搭建iOS开发之runtime(2):浅析...

  • iOS开发之Runtime常用示例总结

    iOS开发之Runtime常用示例总结 iOS开发之Runtime常用示例总结

  • Runtime全面剖析之原理篇

    如果想了解Runtime的实际应用请看Runtime全面剖析之简单使用 一:Runtime简介二: Runtime...

  • 事岀无常必有妖-iOS捉妖记之(Runtime)

    事岀无常必有妖-iOS捉妖记之(Runtime) 事岀无常必有妖-iOS捉妖记之(Runtime)

  • 文言论

    文言论 文言,言简意赅而精致典雅之文也,而五四青年以为无益而废之,代以欧化白话文,彼等虽有拥戴白话文之功,然亦成文...

  • Runtime专题

    参考资料 1、iOS Runtime详解 2、iOS runtime 之 Class 和 MetaClass 3、...

  • iOS Runtime之方法查找

    Runtime系列导读 iOS Runtime之方法查找[https://www.jianshu.com/p/f6...

网友评论

      本文标题:白话文之Runtime

      本文链接:https://www.haomeiwen.com/subject/otciiqtx.html