1. 运行时 Asssociate方法 动态关联对象,需要我们手动释放吗?
不需要,在对象 析构 / 释放 / dealloc 时,释放
2. 方法的调用顺序
类的方法 和 分类方法 重名,如何调用,是什么情况?
a. 如果同名方法是普通方法,包括initialize -- 先调用分类方法, 因为分类方法是在后压入方法列表栈
b. 如果同名方法 是load , 先 主类load,后分类load(分类之间,看编译的顺序)
3. 运行时
-
runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象、以及运行时的功能 -
运行时是指
将数据类型的确定由编译时推迟到了运行时 -
///举例:
extension和category的区别 -
平时编写的
OC代码,在程序运行的过程中,其实最终会转换成runtime的C语言代码,runtime是OC的幕后工作者 -
**category 类别、分类
**a. 用来给类添加新的方法,
b. 不能给类添加成员属性,即使添加了成员属性,也无法取到.
分类中用@property定义变量,只会生成变量的setter、getter方法的声明,不能生成方法实现 和 带下划线的成员变量
c. 注意:其实可以通过runtime给分类添加属性,即属性关联,重写setter、getter方法 -
extension 类扩展
-
可以说成是
特殊的分类,也可称作匿名分类 -
可以
给类添加成员属性,但是是私有变量 -
可以
给类添加方法,也是私有方法
-
4. 方法的本质
SEL是方法编号,也是方法名,在dyld加载镜像到内存时,通过_read_image方法加载到内存的表中了
imp是函数实现指针 ,找imp就是找函数的过程
SEL和IMP的关系就可以解释为:
-
sel就相当于本书的目录标题 -
imp就相当于书本的页码 -
具体的函数就是具体页码对应的内容方法的本质:发送消息,发送消息会有以下几个流程
1.快速查找流程——通过汇编objc_msgSend查找缓存cache_t是否有imp实现 2.慢速查找流程——通过C++中lookUpImpOrForward递归查找当前类和父类的rw中methodlist的方法 3.查找不到消息:动态方法解析——通过调用resolveInstanceMethod和 resolveClassMethod来动态方法决议——实现消息动态处理 4.快速转发流程——通过CoreFoundation来触发消息转发流程,forwardingTargetForSelector实现快速转发, 由其他对象来实现处理方法 5.慢速转发流程——先调用methodSignatureForSelector获取到方法的签名,生成对应的invocation; 再通过forwardInvocation来进行处理
5. 能否向编译后得到的类中增加实例变量? 不能
能否向运行时创建的类中添加实例变量? 能,不一定
具体情况具体分析:
- 编译好的类不能添加实例变量
- 运行时创建的类可以添加实例变量,但若已注册到内存中就不行了
原因:
- 编译好的实例变量存储的位置在
ro,而ro是在编译时就已经确定了的 - ⼀旦编译完成,内存结构就完全确定就⽆法修改
- 只能修改
rw中的方法或者可以通过关联对象的方式来添加属性
class_ro_t存储了当前类在编译期就已经确定的属性、方法以及遵循的协议,里面是没有分类的方法的。那些运行时添加的方法将会存储在运行时生成的class_rw_t中。
6. [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] = TCJStudent , [super class] = TCJStudent
解释:
[self class]方法调用的本质是发送消息,调用class的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_images的readClass时已经加入表中,所以打印为TCJStudent
[super class]打印的是TCJStudent,原因是当前的super是一个关键字,在这里只调用objc_msgSendSuper2,其实他的消息接收者和[self class]是一模一样的,所以返回的是TCJStudent
7. Runtime是如何实现weak的,为什么可以自动置nil
-
1、通过
SideTable找到我们的weak_table SideTable主要存放了OC对象的引用计数和弱引用相关信息。 -
2、
weak_table根据referent找到或者创建 `weak_entry_t`
-
3、然后
append_referrer(entry,referrer)将我的新弱引用的对象加进去entry -
4、最后
weak_entry_insert,把entry加入到我们的weak_table由于弱引用在析构
dealloc时自动置空,所以查看dealloc的底层实现
8 . 利用runtime api 动态创建
1. 动态创建类 2. 动态创建成员变量 3. 向内存注册类 4. 属性 5. 方法
9. Method Swizzing - 方法交换 黑魔法
含义是方法交换,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的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获取方法Methodclass_getInstanceMethod:获取实例方法class_getClassMethod:获取类方法
method_getImplementation:获取一个方法的实现method_setImplementation:设置一个方法的实现method_getTypeEncoding:获取方法实现的编码类型class_addMethod:添加方法实现class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即aIMP指向bIMP,但是bIMP不一定指向aIMPmethod_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进行交换
坑点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方法中交换方法为什么选择load方法 由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法, 执行比较早,并且不需要我们手动调用。 + (void)load方法,是所有NSObject子类都具备的用于运行时加载处理的一个类方法 -
最好使用
单例保证只交换一次 (dispatch_once) -
自定义方法名不能产生冲突
-
对于系统方法要调用原始实现,避免对系统产生影响
-
做好注释(因为方法交换比较绕)
-
迫不得已情况下才去使用方法交换