iOS 模块分解— Runtime

相信对于从事开发人员来讲 runtime 这个名称都不陌生,就像我起初只知道「 runtime 叫运行时 」,后来知道 runtime 一样能够像 KVC 同样访问私有成员变量,还有「 给类动态添加属性:LNTextField.placeholderColor || 交换方法:imageNamed => ln_imageNamed 」,还有深刻的 「 消息机制的调用流程 || 字典转模型 || 实现NSCoding归解档 」以及咱们常说的“黑魔法” 是什么?git

runtime 是编程中比较难的模块,想要深刻学习,这个模块你必须掌握,一样还有写的另外一篇 runloop 模块,下面是我对 runtime 的整理,从零开始,由浅入深,且带了几个 Runtime 实践场景 --> 大厂来的工友们可选择性路过。github

目录: 🤡 、 👨‍💻👙面试

  1. runtime.h 释义
  2. 消息机制
    1.isa指针释义
    2.方法调用,是否真的是转换为消息机制?
    2.objc_msgSend 参数概念释义
  3. 消息机制(方法调用流程)
  4. 常见做用
  5. 开发场景「工做掌握」
    1.交换方法
    2.给系统分类动态添加属性
    3.字典转模型(Runtime 考虑三种状况实现)
  6. 其它做用「面试熟悉」
    1.动态添加方法
    2.动态变量控制
    3.实现NSCoding的自动归档和解档
    4.runtime 部分函数
    5.method swizzling(俗称黑魔法)
  7. 一道面试题的注解
  8. 模块博文推荐(❤️数量较多)
  9. Runtime & Runloop 常面问题整理(附答案)
  10. Demo 重要的部分代码中都有相应的注解和文字打印,运行程序能够很直观的表现
  11. iOS 模块注解—「Runloop面试、工做」看我就 🐒 了 ^_^.

释义


Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将不少静态语言在编译和连接时期作的事放到了 runtime 运行时来处理,能够说 runtime 是咱们 Objective-C 幕后工做者。
1.runtime简称运行时),是一套 纯C(C和汇编)写的API。而 OC 就是运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制编程

2.对于 C 语言,函数的调用在编译的时候会决定调用哪一个函数数组

3.运行时机制原理:OC的函数调用称为消息发送,属于 动态调用过程。在 编译的时候 并不能决定真正调用哪一个函数,只有在真 正运行的时候 才会根据函数的名称找到对应的函数来调用。xcode

4.事实证实:在编译阶段,OC 能够 调用任何函数,即便这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是由于OC是运行时动态调用的。而 C 语言 调用未实现的函数 就会报错缓存

消息机制


咱们写 OC 代码,它在运行的时候也是转换成了 runtime 方式运行的。任何方法调用本质:就是发送一个消息(用 runtime发送消息,OC 底层实现经过 runtime 实现),每个 OC 的方法,底层必然有一个与之对应的 runtime 方法。
框架

验证示例:方法调用,是否真的是转换为消息机制?

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
注解
1.必需要导入头文件 #import <objc/message.h>
2.咱们导入系统的头文件,通常用尖括号。
3.OC 解决消息机制方法提示步骤【查找build setting -> 搜索msg -> objc_msgSend(YES --> NO)】
4.最终生成消息机制,编译器作的事情,最终代码,须要把当前代码用xcode从新编译,【clang -rewrite-objc main.m 查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)
5.这里通常不会直接导入<objc/runtime.h>
ide

示例代码:OC 方法 <--> runtime 方法函数

 
objc_msgSend 参数概念
 

消息机制「方法调用流程」


面试:消息机制方法调用流程❓
怎么去调用eat方法,
对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。

1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。
2.注册方法编号(这里用方法编号的好处,能够快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

补充:一个objc 对象的 isa 的指针指向什么?有什么做用?
每个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道未来调用哪一个类的方法。

isa指针相关释义

上面也提到OC底层都是转化为runtime方式来实现的,类和类的实例(对象)都相对于的isa指针
咱们能够在Xcode中使用 [Shift+Cmd+O ] 快速打开文件objc.h 能看到类的定义:
objc.h
isa:是一个Class 类型的指针
runtime 对象,类,元类的isa指针关系图.png
总结:runtime 对象,类,元类的isa指针关系图
一、每个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象经过对象的isa指针指向所属类
二、每个类本质上都是一个对象,类实际上是元类(meteClass)的实例。元类定义了类方法的列表。类经过类的isa指针指向元类
三、元类保存了类方法的列表。当类方法被调用时,先会从自己查找类方法的实现,若是没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类经过isa指针最终指向的是一个根元类(root meteClass)
四、根元类的isa指针指向自己,这样造成了一个封闭的内循环

常见做用


 

开发场景「工做掌握」


runtime 交换方法

场景:当第三方框架 或者 系统原生方法功能不能知足咱们的时候,咱们能够在保持系统原有方法功能的基础上,添加额外的功能。

需求:加载一张图片直接用[UIImage imageNamed:@"image"];是没法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
方案一:继承系统的类,重写方法.(弊端:每次使用都须要导入)
方案二:使用 runtime,交换方法.

步骤
1.给系统的方法添加分类
2.本身实现一个带有扩展功能的方法
3.交换方法,只须要交换一次,

场景代码:方法+调用+打印输出

 

总结
咱们所作的就是在方法调用流程第三步的时候,交换两个方法地址指向。并且咱们改变指向要在系统的imageNamed:方法调用前,因此将代码写在了分类的load方法里。最后当运行的时候系统的方法就会去找咱们的方法的实现。

给系统分类动态添加属性

场景:给系统的类添加额外属性的时候,可使用runtime动态添加属性方法。
原理:给一个类声明属性,其实本质就是给这个类添加关联,并非直接把这个值的内存空间添加到类存空间。
注解:给系统 NSObject 添加一个分类,咱们知道在分类中是不可以添加成员属性的,虽然咱们用了@property,可是仅仅会自动生成getset方法的声明,并无带下划线的属性和方法实现生成。可是咱们能够经过runtime就能够作到给它方法的实现。

需求:给系统 NSObject 类动态添加属性 name 字符串。

场景代码:方法+调用+打印

 

总结
其实,属性赋值的本质,就是让属性与一个对象产生关联,因此要给NSObject的分类的name属性赋值就是让nameNSObject产生关联,而runtime能够作到这一点。

字典转模型

字典转模型的方式

  • 给模型中属性,在 .m 依次赋值(初学者)。
  • 字典转模型 KVC 实现
    • KVC 字典转模型弊端:必须保证,模型中的属性和字典中的key 一一对应。
    • 若是不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 报key找不到的错。
    • 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
    • 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。
  • 字典转模型 Runtime 实现
    • 思路:利用运行时,遍历模型中全部属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不必定要所有取出来);提供一个NSObject分类,专门字典转模型,之后全部模型均可以经过这个分类实现字典转模型。

    • 考虑状况
      1.当字典的key和模型的属性匹配不上。
      2.模型中嵌套模型(模型属性是另一个模型对象)。
      3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。

    • 注解
      根据上面的三种特殊状况,先是字典的key和模型的属性不对应的状况。不对应有两种,一种是字典的键值大于模型属性数量,这时候咱们不须要任何处理,由于runtime是先遍历模型全部属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也固然不会去看了;另一种是模型属性数量大于字典的键值对,这时候因为属性没有对应值会被赋值为nil,就会致使crash,咱们只需加一个判断便可。考虑三种状况下面一一注解
  • MJExtension 字典转模型实现
    • 底层也是对 runtime 的封装,才能够把一个模型中全部属性遍历出来。(我之因此看不懂,是MJ封装了不少层而已^_^.)。

示例:runtime 字典转模型考虑三种状况
Runtime 字典转模型

一、runtime 字典转模型-->字典的 key 和模型的属性不匹配「模型属性数量大于字典键值对数」,这种状况处理以下:
 

注解
这里在获取模型类中的全部属性名,是采起 class_copyIvarList 先获取成员变量(如下划线开头) ,而后再处理成员变量名,字典中的key(去掉 _ ,从第一个角标开始截取) 获得属性名。

缘由

 
二、runtime 字典转模型-->模型中嵌套模型「模型属性是另一个模型对象」,这种状况处理以下:
 
三、runtime 字典转模型-->数组中装着模型「模型的属性是一个数组,数组中是字典模型对象」,这种状况处理以下:
 

总结
咱们既然能获取到属性类型,那就能够拦截到模型的那个数组属性,进而对数组中每一个模型遍历并字典转模型,可是咱们不知道数组中的模型都是什么类型,咱们能够声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。

这里提到的你若是不是很清楚,建议参考个人Demo,重要的部分代码中都有相应的注解和文字打印,运行程序能够很直观的表现。

其它做用「面试熟悉」


动态添加方法

场景:若是一个类方法很是多,加载类到内存的时候也比较耗费资源,须要给每一个方法生成映射表,可使用动态给某个类,添加方法解决。

注解:OC 中咱们很习惯的会用懒加载,当用到的时候才去加载它,可是实际上只要一个类实现了某个方法,就会被加载进内存。当咱们不想加载这么多方法的时候,就会使用到 runtime 动态的添加方法。

需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。

场景代码:方法+调用+打印输出

 

实现NSCoding的自动归档和解档

若是你实现过自定义模型数据持久化的过程,那么你也确定明白,若是一个模型有许多个属性,那么咱们须要对每一个属性都实现一遍encodeObject 和 decodeObjectForKey方法,若是这样的模型又有不少个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。

假设如今有一个Movie类,有3个属性。先看下 .h文件

 

若是是正常写法,.m 文件应该是这样的:

 

若是这里有100个属性,那么咱们也只能把100个属性都给写一遍吗。
不过你会使用runtime后,这里就有更简便的方法,以下。

 

这样的方式实现,无论有多少个属性,写这几行代码就搞定了。
下面看看更加简便的方法:两句代码搞定。

 

优化
上面是encodeWithCoder 和 initWithCoder这两个方法抽成宏。咱们能够把这两个宏单独放到一个文件里面,这里之后须要进行数据持久化的模型均可以直接使用这两个宏。

runtime 下Class的各项操做

1.runtime 部分函数

 

method swizzling(俗称黑魔法)


  • 简单说就是进行方法交换
  • Objective-C中调用一个方法,实际上是向一个对象发送消息,查找消息的惟一依据是selector的名字。利用Objective-C的动态特性,能够实如今运行时偷换selector对应的方法实现,达到给方法挂钩的目的
  • 每一个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点相似函数指针,指向具体的Method实现,经过selector就能够找到对应的IMP

selector --> 对应的IMP

  • 交换方法的几种实现方式
    • 利用 method_exchangeImplementations 交换两个方法的实现
    • 利用 class_replaceMethod 替换方法的实现
    • 利用 method_setImplementation 来直接设置某个方法的IMP

交换方法

这里能够参考简友这篇:Runtime Method Swizzling开发实例汇总

一道面试题的注解


下面的代码输出什么?

 

先思考一下,会打印出来什么❓


答案:都输出 Son

  • class 获取当前方法的调用者的类,superClass 获取当前方法的调用者的父类,super 仅仅是一个编译指示器,就是给编译器看的,不是一个指针。
  • 本质:只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质仍是当前对象在调用

这个题目主要是考察关于objc中对 self 和 super 的理解:

  • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者

  • 当使用 self 调用方法时,会从当前类的方法列表中开始找,若是没有,就从父类中再找;

  • 而当使用 super时,则从父类的方法列表中开始找。而后调用父类的这个方法

  • 调用 [self class] 时,会转化成 objc_msgSend 函数

 

Runtime 模块博文推荐 (❤️数量较多)


做者 Runtime 模块推荐阅读博文
西木 完整总结 http://www.jianshu.com/p/6b905584f536
天口三水羊 objc_msgSend http://www.jianshu.com/p/9e1bc8d890f9
夜千寻墨 详解 http://www.jianshu.com/p/46dd81402f63
袁峥Seemygo 快速上手 http://www.jianshu.com/p/e071206103a4
郑钦洪_ 实现自动化归档 http://www.jianshu.com/p/bd24c3f3cd0a
HenryCheng 消息机制 http://www.jianshu.com/p/f6300eb3ec3d
卖报的小画家Sure Method Swizzling开发实例汇总 http://www.jianshu.com/p/f6dad8e1b848
滕大鸟 OC最实用的runtime总结 http://www.jianshu.com/p/ab966e8a82e2
黑花白花 Runtime在实际开发中的应用 http://www.jianshu.com/p/851b21870d91

Runtime & Runloop 常面问题整理(附答案)

同一个面试问题并不是只有一个答案,而同一个答案并非在任何面试场合都有效,关键在于应聘者掌握了规律后,对面试的具体状况进行把握,有意识地揣摩面试官提出问题的心理 (真实问答),要 get 的到问的点,而后答其所问,算是“ 投其所好 ”吧。
摘录:
http://www.jianshu.com/p/56e40ea56813
http://www.jianshu.com/p/f9eb6b315c08

Runtime
01 / objc在向一个对象发送消息时,发生了什么?
参考1:根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,若是没有在好到,在沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),在发送消息;
02 / 问题:何时会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
参考1:当发送消息的时候,咱们会根据类里面的 methodLists 列表去查询咱们要动用的SEL,当查询不到的时候,咱们会一直沿着父类查询,当最终查询不到的时候咱们会报 unrecognized selector 错误,当系统查询不到方法的时候,会调用 +(BOOL)resolveInstanceMethod:(SEL)sel 动态解释的方法来给我一次机会来添加,调用不到的方法。或者咱们能够再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法来告诉系统,该调用什么方法,一来保证不会崩溃。
03 / 问题:可否向编译后获得的类中增长实例变量?可否向运行时建立的类中添加实例变量?为何?
参考1:一、不能向编译后获得的类增长实例变量 二、能向运行时建立的类中添加实例变量。
分析:1. 编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经肯定,runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.因此不能向存在的类中添加实例变量。2. 运行时建立的类是能够添加实例变量,调用class_addIvar函数. 可是的在调用 objc_allocateClassPair 以后,objc_registerClassPair 以前,缘由同上.
04 / 问题:runtime如何实现weak变量的自动置nil?
参考1:runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址做为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到全部以a为键的 weak 对象,从而设置为 nil。
05 / 问题:给类添加一个属性后,在类结构体里哪些元素会发生变化?
参考1:instance_size :实例的内存大小;objc_ivar_list *ivars:属性列表
RunLoop
01 / 问题:runloop是来作什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
参考1:runloop: 从字面意思看:运行循环、跑圈,其实它内部就是do-while循环,在这个循环内部不断地处理各类任务(好比Source、Timer、Observer)事件。runloop和线程的关系:一个线程对应一个RunLoop,主线程的RunLoop默认建立并启动,子线程的RunLoop需手动建立且手动启动(调用run方法)。RunLoop只能选择一个Mode启动,若是当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。
02 / 问题:runloop的mode是用来作什么的?有几种mode?
参考1:model:是runloop里面的运行模式,不一样的模式下的runloop处理的事件和消息有必定的差异。系统默认注册了5个Mode:(1)kCFRunLoopDefaultMode: App的默认 Mode,一般主线程是在这个 Mode 下运行的。(2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其余 Mode 影响。(3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就再也不使用。(4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,一般用不到。(5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际做用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes
03 / 问题:为何把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环之后,滑动scrollview的时候NSTimer却不动了?
参考1:nstime对象是在 NSDefaultRunLoopMode下面调用消息的,可是当咱们滑动scrollview的时候,NSDefaultRunLoopMode模式就自动切换到UITrackingRunLoopMode模式下面,却不能够继续响应nstime发送的消息。因此若是想在滑动scrollview的状况下面还调用nstime的消息,咱们能够把nsrunloop的模式更改成NSRunLoopCommonModes.
04 / 问题:苹果是如何实现Autorelease Pool的?
参考1:Autorelease Pool做用:缓存池,能够避免咱们常常写relase的一种方式。其实就是延迟release,将建立的对象,添加到最近的autoreleasePool中,等到autoreleasePool做用域结束的时候,会将里面全部的对象的引用计数器 - autorelease.

附上写的小样 Demo,重要的部分代码中都有相应的注解和文字打印,运行程序能够很直观的表现

Reading


    • 各位厂友,因为「时间 & 知识」有限,总结的文章不免有「未全、不足」,该模块将系统化学习,后替换、补充文章内容 ~
    • 熬夜写者不易,不知名开发者,更多模块文章
相关文章
相关标签/搜索