写在前面
前面篇章说了那么多的原理,那本篇就拿说说OC相关的题目吧...
一、Runtime Asssociate方法关联的对象,需要我们手动释放吗?
当我们对象释放时,会调用dealloc
- 1、C++函数释放 :
objc_cxxDestruct
- 2、移除关联属性:
_object_remove_assocations
- 3、将弱引用自动设置
nil
:weak_clear_no_lock(&table.weak_table, (id)this)
; - 4、引用计数处理:
table.refcnts.erase(this)
- 5、销毁对象:
free(obj)
所以,关联对象
不需要我们手动移除,会在对象析构即dealloc
时释放.
dealloc的原理详解我们将在内存管理章节详细讲解,这里先附上一张dealloc
流程图
二、方法的调用顺序
类的方法 和 分类方法 重名,如和调用,是什么情况?
- 如果同名方法是
普通方法
,包括initialize
--先调用分类方法
- 因为分类的方法是在类
realize
实现之后再attach
进去的,插在类的方法的前面,所以优先调用分类的方法
(注意:不是分类覆盖主类!!) -
initialize
方法什么时候调用?initialize
方法也是主动调用,即第一次消息发送时
调用,为了不影响整个load
,可以将需要提前加载的数据
写到initialize
中
- 因为分类的方法是在类
- 如果同名方法是
load方法
-- 先主类load
,后分类load
(分类之间,看编译的顺序)可以参考iOS之武功秘籍⑧: 类和分类加载过程文章中的load_images
原理分析
三、Runtime是什么?
-
runtime
是由C
和C++
汇编实现的一套API
,为OC
语言加入了面向对象
、以及运行时的功能
- 运行时是指
将数据类型的确定由编译时
推迟到了运行时
- 举例:
extension
和category
的区别
- 举例:
- 平时编写的
OC
代码,在程序运行的过程中,其实最终会转换成runtime
的C语言
代码,runtime
是OC
的幕后工作者
1、category 类别、分类
- 专门用来给类添加新的方法
- 不能给类添加成员属性,添加了成员属性,也无法取到
- 注意:其实可以通过
runtime
给分类添加属性,即属性关联,重写setter
、getter
方法 - 分类中用
@property
定义变量,只会生成变量的setter
、getter
方法的声明,不能生成方法实现 和 带下划线的成员变量
2、extension 类扩展
- 可以说成是
特殊的分类
,也可称作匿名分类
- 可以
给类添加成员属性
,但是是私有变量
- 可以
给类添加方法
,也是私有方法
四、方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
方法的本质:发送消息
,发送消息会有以下几个流程
- 1.快速查找流程——通过汇编
objc_msgSend
查找缓存cache_t
是否有imp
实现 - 2.慢速查找流程——通过
C++
中lookUpImpOrForward
递归查找当前类和父类的rw
中methodlist
的方法 - 3.查找不到消息:动态方法解析——通过调用
resolveInstanceMethod
和resolveClassMethod
来动态方法决议——实现消息动态处理 - 4.快速转发流程——通过
CoreFoundation
来触发消息转发流程,forwardingTargetForSelector
实现快速转发,由其他对象来实现处理方法 - 5.慢速转发流程——先调用
methodSignatureForSelector
获取到方法的签名,生成对应的invocation
;再通过forwardInvocation
来进行处理
SEL
是方法编号,也是方法名,在dyld
加载镜像到内存时,通过_read_image
方法加载到内存的表中了
imp
是函数实现指针
,找imp
就是找函数的过程
SEL
和IMP
的关系就可以解释为:
-
sel
就相当于本书的目录标题
-
imp
就相当于书本的页码
-
具体的函数
就是具体页码对应的内容
比如我们想在《程序员的自我修养——链接、装载与库》一书中找到“动态链接”(SEL)
,肯定会翻到179页(IMP)
,179页会开始讲述具体内容(函数实现)
五、能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
具体情况具体分析:
- 编译好的类不能添加实例变量
- 运行时创建的类可以添加实例变量,但若已注册到内存中就不行了
原因:
- 编译好的实例变量存储的位置在
ro
,而ro
是在编译时就已经确定了的 - ⼀旦编译完成,内存结构就完全确定就⽆法修改
- 只能修改
rw
中的方法或者可以通过关联对象的方式来添加属性
六、[self class]和[super class]的区别以及原理分析
[self class]
就是发送消息objc_msgSend
,消息接收者是self
,方法编号class
[super class]
本质就是objc_msgSendSuper
,消息的接收者还是self
,方法编号class
,在运行时,底层调用的是_objc_msgSendSuper2
只是
objc_msgSendSuper2
会更快,直接跳过self
的查找
代码调试
TCJStudent
中的init
方法中打印这两种class
调用,TCJStudent
继续自TCJPerson
.
有点出乎意料,[self class]
点进去来到NSObject.mm
文件查看源码
其底层是获取对象的isa
,当前的对象是TCJStudent
,其isa
是同名的TCJStudent
,所以[self class]
打印的是TCJStudent
[super class]
中,其中super
是语法的 关键字
,可以通过clang
看super
的本质,这是编译时的底层源码,其中第一个参数是消息接收者,是一个__rw_objc_super
结构
底层源码中搜索__rw_objc_super
,是一个中间结构体
在objc4-818.2
源码中搜索objc_msgSendSuper
,查看其隐藏参数
搜索struct objc_super
通过clang
的底层编译代码可知,当前消息的接收者 等于 self
,而self
等于 TCJStudent
,所以 [super class]
进入class
方法源码后,其中的self
是init
后的实例对象,实例对象的isa
指向的是本类,即消息接收者是TCJStudent
本类.
我们再来看[super class]
在运行时是否如上一步的底层编码所示,是objc_msgSendSuper
,打开汇编调试,调试结果如下
搜索objc_msgSendSuper2
,从注释得知,是从 类开始查找,而不是父类
查看objc_msgSendSuper2
的汇编源码,是从superclass
中的cache
中查找方法
所以,最完整的回答如下
[self class]
方法调用的本质是发送消息
,调用class
的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_images
的readClass
时已经加入表中,所以打印为TCJStudent
[super class]
打印的是TCJStudent
,原因是当前的super
是一个关键字,在这里只调用objc_msgSendSuper2
,其实他的消息接收者和[self class]
是一模一样的,所以返回的是TCJStudent
七、内存平移问题
① 原始题
程序能否运行?是否正常输出?
运行结果与普通初始化对象一模一样,可面试的时候不可能只说能或不能,还要说出个所以然来
[person saySomething]
的本质是对象发送消息
,那么当前的person
是什么?
-
person
的isa
指向类TCJPerson
即person的首地址
指向TCJPerson的首地址
,我们可以通过TCJPerson
的内存平移找到cache
,在cache
中查找方法
-
[(__bridge id)obj saySomething]
中的obj
是来自于TCJPerson
这个类,然后有一个指针obj
,将其指向TCJPerson的首地址
.
所以,person是指向TCJPerson
类的结构,obj
也是指向TCJPerson
类的结构,然后都是在TCJPerson
类中的methodList
中查找方法.
② 拓展一
修改打印方法saySomething
——不但打印方法,同时打印属性cj_name
为什么会出现打印不一致的情况?
其中person
方式的cj_name
是由于self
指向person
的内存结构,然后通过内存平移8字节,取出去cj_name
,即self指针首地址平移8字节获得
.
其中的cls
方式中的obj
指针中没有其他的,所以obj
表示8字节指针
,self.cj_name
的获取,相当于obj首地址的指针也需要平移8字节找cj_name
,那么此时的obj
的指针地址是多少?平移8字节
获取的是什么?
obj
是一个指针,是存在栈
中的,栈是一个先进后出
的结构,参数传入就是一个不断压栈的过程,其中隐藏参数会压入栈
,且每个函数都会有两个隐藏参数(id self,sel _cmd)
,可以通过clang
查看底层编译,隐藏参数压栈
的过程,其地址是递减
的,而栈是从高地址->低地址
分配的,即在栈中,参数会从前往后一直压.
super
通过clang
查看底层的编译,是objc_msgSendSuper
,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass)
,那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况
从打印结果可以得出20
先加入,再加入10
,因此结构体内部的压栈
情况是 低地址->高地址
递增的,栈中结构体内部的成员
是反向压入栈
,即低地址->高地址
是递增的.
所以到目前为止,栈中从高地址到低地址
的顺序的:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - obj - person
-
self
和_cmd
是viewDidLoad
方法的两个隐藏参数
,是高地址->低地址
正向压栈的 -
class_getSuperClass
和self
为objc_msgSendSuper2
中的结构体成员,是从最后一个成员变量,即低地址->高地址
反向压栈的
那我们来打印下栈的存储情况:
① obj
的栈的存储情况
② person
的栈的存储情况
③ obj
与person
一起的栈的存储情况
其中为什么class_getSuperclass
是ViewController
,因为objc_msgSendSuper2
返回的是当前类
,两个self
,并不是同一个self
,而是栈的指针不同,但是指向同一片内存空间
-
[(__bridge id)obj saySomething]
调用时,此时的obj
是TCJPerson: 0x7ffee0d57048
,所以saySomething
方法中传入的self
还是TCJPerson
,但并不是我们通常认为的TCJPerson
,是我们当前传入的消息接收者
,即TCJPerson: 0x7ffee0d57048
,是TCJPerson
的实例对象,此时的操作与普通的TCJPerson
是一致的,即TCJPerson
的地址内存平移8字节
. - 普通
person
流程:person -> cj_name - 内存平移8字节
-
obj
流程:0x7ffee0d57048 + 0x80 -> 0x7ffee0d57050
,即为self
,指向<ViewController: 0x7ffee0d57050>
其中 person
与 TCJPerson
的关系是 person
是以TCJPerson
为模板的实例化对象,即alloc
有一个指针地址,指向isa
,isa
指向TCJPerson
,它们之间关联是有一个isa指向
.
而obj
也是指向TCJPerson
的关系,编译器会认为obj
也是TCJPerson
的一个实例化对象,即obj
相当于isa
,即首地址,指向TCJPerson
,具有和person
一样的效果,简单来说,我们已经完全将编译器骗过了,即obj
也有cj_name
.由于person
查找cj_name
是通过内存平移8字节
,所以obj
也是通过内存平移8字节
去查找cj_name
.
③ 拓展二
修改viewDidLoad
——在obj
前面加个临时字符串变量
同样道理,在obj
入栈前已经有了temp
变量,此时访问self.cj_name
就会访问到temp
④ 拓展三
去掉临时变量,TCJPerson
类新增字符串属性cj_hobby
,打印方法改为打印cj_hobby
,运行
ViewController
就是obj
偏移16字节
拿到的super_class
.
⑤ 拓展四
将TCJPerson
类新增字符串属性cj_hobby
,改成int
类型,打印
这种情况就是野指针——指针偏移的offset
不正确,获取不到对应变量的首地址.
八、Runtime是如何实现weak的,为什么可以自动置nil
- 1、通过
SideTable
找到我们的weak_table
- 2、
weak_table
根据referent
找到或者创建weak_entry_t
- 3、然后
append_referrer(entry,referrer)
将我的新弱引用的对象加进去entry
- 4、最后
weak_entry_insert
,把entry
加入到我们的weak_table
在weak
一行打下断点运行项目
Xcode
菜单栏Debug->Debug Workflow->Always show Disassembly
打上勾查看汇编——汇编代码会来到libobjc
库的objc_initWeak
① weak创建过程
①.1 objc_initWeak
-
location
:表示__weak指针
的地址(我们研究的就是__weak指针
指向的内容怎么置为nil
) -
newObj
:所引用的对象,即例子中的person
①.2 storeWeak
-
HaveOld
:weak指针
之前是否已经指向了一个弱引用 -
HaveNew
:weak指针
是否需要指向一个新引用 -
CrashIfDeallocating
:如果被弱引用的对象正在析构,此时再弱引用该对象,是否应该crash
storeWeak
最主要的两个逻辑点
由于是第一次调用,所以走
haveNew
分支——获取到的是新的散列表SideTable
,主要执行了weak_register_no_lock
方法来进行插入
①.3 weak_register_no_lock
- 主要进行了
isTaggedPointer
和deallocating
条件判断 - 将被弱引用对象所在的
weak_table
中的weak_entry_t
哈希数组中取出对应的weak_entry_t
- 如果
weak_entry_t
不存在,则会新建一个并插入 - 如果存在就将指向被弱引用对象地址的指针
referrer
通过函数append_referrer
插入到对应的weak_entry_t
引用数组
①.4 append_referrer
找到弱引用对象的对应的weak_entry_t
哈希数组中插入
② weak创建流程
③ weak销毁过程
由于弱引用在析构
dealloc
时自动置空,所以查看dealloc
的底层实现
- _objc_rootDealloc->rootDealloc
- rootDealloc->object_dispose
- object_dispose->objc_destructInstance
- objc_destructInstance->clearDeallocating
- clearDeallocating->sidetable_clearDeallocating
- weak_clear_no_lock->table.refcnts.erase
④ weak销毁流程
九、利用runtime-API创建对象
① API介绍
①.1 动态创建类
①.2 添加成员变量
①.3 注册到内存
①.4 添加属性变量
①.5 添加方法
② 整体使用
③ 注意事项
- 记得导入
<objc/runtime.h>
- 添加成员变量
class_addIvar
必须在objc_registerClassPair
前,因为注册到内存时ro
已经确定了,不能再往ivars
添加 - 添加属性变量
class_addProperty
可以在注册内存前后,因为是往rw
中添加的 -
class_addProperty
中“属性的属性”——nonatomic/copy
是根据属性的类型变化而变化的 -
class_addProperty
不会自动生成setter
和getter
方法,因此直接调用KVC
会崩溃- 不只可以通过
KVC
打印来检验,也可以下断点查看ro、rw
的结构来检验
- 不只可以通过
十、Method Swizzing坑点
① method-swizzling 是什么?
method-swizzling
的含义是方法交换
,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现
,这就是我们常说的iOS黑魔法
.
在OC
中就是利用method-swizzling
实现AOP
,其中AOP
(Aspect Oriented Programming
,面向切面编程)是一种编程的思想,区别于OOP
(面向对象编程).
-
OOP
和AOP
都是一种编程的思想 -
OOP
编程思想更加倾向于对业务模块的封装
,划分出更加清晰的逻辑单元 - 而
AOP
是面向切面进行提取封装
,提取各个模块中的公共部分
,提高模块的复用率
,降低业务之间的耦合性.
每个类都维护着一个方法列表
,即methodList
,methodList
中有不同的方法即Method
,每个方法中包含了方法的sel
和IMP
,方法交换就是将sel
和imp
原本的对应断开,并将sel
和新的IMP
生成对应关系.
如下图所示,交换前后的sel
和IMP
的对应关系
② method-swizzling涉及的相关API
- 通过
sel
获取方法Method
-
class_getInstanceMethod:
获取实例方法 -
class_getClassMethod:
获取类方法
-
-
method_getImplementation:
获取一个方法的实现 -
method_setImplementation:
设置一个方法的实现 -
method_getTypeEncoding:
获取方法实现的编码类型 -
class_addMethod:
添加方法实现 -
class_replaceMethod:
用一个方法的实现,替换另一个方法的实现,即aIMP
指向bIMP
,但是bIMP
不一定指向aIMP
-
method_exchangeImplementations:
交换两个方法的实现,即aIMP -> bIMP
,bIMP -> aIMP
③ 坑点1:method-swizzling使用过程中的一次性问题
所谓的一次性就是:mehod-swizzling
写在load
方法中,而load
方法会主动调用多次
,这样会导致方法的重复交换
,使方法sel
的指向又恢复成原来的imp
的问题
解决方案
可以通过单例设计
原则,使方法交换只执行一次
,在OC
中可以通过dispatch_once
实现单例
④ 坑点2:子类没有实现,父类实现了
- 父类
TCJPerson
类中有-personInstanceMethod
方法,子类TCJStudent
类没有重写 - 子类
TCJStudent
类新建分类做了方法交换,新方法中调用旧方法 -
TCJPerson
类、TCJStudent
类调用-personInstanceMethod
子类打印出结果,而父类调用却崩溃了,为什么会这样呢?
-
[student personInstanceMethod];
中不报错是因为student
中的imp
交换成了cj_studentInstanceMethod
,而TCJStudent
中有这个方法(在TCJ
分类中),所以不会报错. - 崩溃的点在于
[person personInstanceMethod];
,其本质原因:TCJStudent
的分类TCJ
中进行了方法交换,将person
中imp
交换成了TCJStudent
中的cj_studentInstanceMethod
,然后需要去TCJPerson
中的找cj_studentInstanceMethod
,但是TCJPerson
中没有cj_studentInstanceMethod
方法,即相关的imp
找不到,所以就崩溃了
优化:避免imp找不到
通过class_addMethod
尝试添加你要交换的方法
- 如果
添加成功
,即类中没有这个方法,则通过class_replaceMethod
进行替换,其内部会调用class_addMethod
进行添加 - 如果添加不成功,即类中有这个方法,则通过
method_exchangeImplementations
进行交换
这样就不会报错了.
下面是class_replaceMethod
、class_addMethod
和method_exchangeImplementations
的源码实现
其中class_replaceMethod
和class_addMethod
中都调用了addMethod
方法,区别在于bool值
的判断,下面是addMethod
的源码实现
⑤ 坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?
在上面测试代码的基础上加入父类TCJPerson
的personInstanceMethod
的方法只写了方法声明,没有方法实现,却做了方法交换——会造成死循环
原因是 栈溢出,递归死循环
了,那么为什么会发生递归呢?----主要是因为 personInstanceMethod
没有实现,然后在方法交换时,始终都找不到oriMethod
,然后交换了寂寞,即交换失败,当我们调用personInstanceMethod(oriMethod)
时,也就是oriMethod
会进入TCJ分类
中cj_studentInstanceMethod
方法,然后这个方法中又调用了cj_studentInstanceMethod
,此时的cj_studentInstanceMethod
并没有指向oriMethod
,然后导致了自己调自己,即递归死循环
优化:避免递归死循环
如果oriMethod
为空,为了避免方法交换没有意义,而被废弃,需要做一些事情
- 通过
class_addMethod
给oriSEL
添加swiMethod
方法 - 通过
method_setImplementation
将swiMethod
的IMP
指向不做任何事的空实现
⑥ method-swizzling - 类方法
类方法和实例方法的method-swizzling
的原理是类似的,唯一的区别是类方法存在元类
中,所以可以做如下操作
- 需要通过
class_getClassMethod
方法获取类方法 - 在调用
class_addMethod
和class_replaceMethod
方法添加和替换时,需要传入的类是元类
,元类可以通过object_getClass
方法获取类的元类
⑦ method-swizzling的应用
method-swizzling
最常用的应用是防止数组、字典等越界崩溃
问题
在iOS
中NSNumber
、NSArray
、NSDictionary
等这些类都是类簇,一个NSArray
的实现可能由多个类组成.所以如果想对NSArray
进行Swizzling
,必须获取到其“真身”进行Swizzling
,直接对NSArray
进行操作是无效的.
下面列举了NSArray
和NSDictionary
本类的类名,可以通过Runtime
函数取出本类.
⑧ 注意事项
使用Method Swizzling
有以下注意事项:
- 尽可能在
+load
方法中交换方法 - 最好使用
单例
保证只交换一次 - 自定义方法名不能产生冲突
- 对于系统方法要调用原始实现,避免对系统产生影响
- 做好注释(因为方法交换比较绕)
- 迫不得已情况下才去使用方法交换
写在后面
和谐学习,不急不躁.我还是我,颜色不一样的烟火.