日志4 OC底层

108 阅读9分钟

1. 运行时 Asssociate方法 动态关联对象,需要我们手动释放吗?

不需要,在对象 析构 / 释放 / dealloc 时,释放

2. 方法的调用顺序

类的方法 和 分类方法 重名,如何调用,是什么情况?

  a.  如果同名方法是普通方法,包括initialize -- 先调用分类方法, 因为分类方法是在后压入方法列表栈

b. 如果同名方法 是load ,主类load,后分类load(分类之间,看编译的顺序)

3. 运行时

  • runtime是由CC++汇编实现的一套API,为OC语言加入了面向对象以及运行时的功能

  • 运行时是指将数据类型的确定由编译时 推迟到了 运行时

  • ///举例:extensioncategory 的区别

  • 平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtimeC语言代码, runtimeOC的幕后工作者

  • **category 类别、分类
    **a. 用来给类添加新的方法, 
    b. 不能给类添加成员属性,即使添加了成员属性,也无法取到.
    分类中用@property 定义变量,只会生成变量的settergetter方法的声明,不能生成方法实现 和 带下划线的成员变量
    c. 注意:其实可以通过runtime 给分类添加属性,即属性关联,重写settergetter方法

  • extension 类扩展

    • 可以说成是特殊的分类 ,也可称作 匿名分类

    • 可以给类添加成员属性,但是是私有变量

    • 可以给类添加方法,也是私有方法

4. 方法的本质  

SEL是方法编号,也是方法名,在dyld加载镜像到内存时,通过_read_image方法加载到内存的表中了
imp函数实现指针 ,找imp就是找函数的过程

SELIMP的关系就可以解释为:

  • 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_imagesreadClass时已经加入表中,所以打印为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(面向对象编程).

  • OOPAOP都是一种编程的思想

  • OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元

  • AOP面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性.

  • 每个类都维护着一个方法列表,即methodListmethodList中有不同的方法即Method,每个方法中包含了方法的selIMP,方法交换就是将selimp原本的对应断开,并将sel新的IMP生成对应关系. 如下图所示,交换前后的selIMP的对应关系

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中进行了方法交换,将personimp 交换成了 TCJStudent中的cj_studentInstanceMethod,然后需要去 TCJPerson中的找cj_studentInstanceMethod,但是TCJPerson中没有cj_studentInstanceMethod方法,即相关的imp找不到,所以就崩溃了

优化:避免imp找不到

通过class_addMethod尝试添加你要交换的方法

  • 如果添加成功,即类中没有这个方法,则通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加

  • 如果添加不成功,即类中有这个方法,则通过method_exchangeImplementations进行交换

坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?

在上面测试代码的基础上加入父类TCJPersonpersonInstanceMethod的方法只写了方法声明,没有方法实现,却做了方法交换——会造成死循环

原因是 栈溢出,递归死循环了,那么为什么会发生递归呢?----主要是因为 personInstanceMethod没有实现,然后在方法交换时,始终都找不到oriMethod,然后交换了寂寞,即交换失败,当我们调用personInstanceMethod(oriMethod)时,也就是oriMethod会进入TCJ分类cj_studentInstanceMethod方法,然后这个方法中又调用了cj_studentInstanceMethod,此时的cj_studentInstanceMethod并没有指向oriMethod ,然后导致了自己调自己,即递归死循环

优化:避免递归死循环

如果oriMethod为空,为了避免方法交换没有意义,而被废弃,需要做一些事情

  • 通过class_addMethodoriSEL添加swiMethod方法

  • 通过method_setImplementationswiMethodIMP指向不做任何事的空实现

method-swizzling - 类方法

类方法和实例方法的method-swizzling的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作

  • 需要通过class_getClassMethod方法获取类方法
  • 在调用class_addMethodclass_replaceMethod方法添加和替换时,需要传入的类是元类,元类可以通过object_getClass方法获取类的元

method-swizzling的应用

method-swizzling最常用的应用是防止数组、字典等越界崩溃问题 在iOSNSNumberNSArrayNSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成.所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的.

下面列举了NSArrayNSDictionary本类的类名,可以通过Runtime函数取出本类.

注意事项

使用Method Swizzling有以下注意事项:

  • 尽可能在+load方法中交换方法

    为什么选择load方法
    由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,
    执行比较早,并且不需要我们手动调用。
    + (void)load方法,是所有NSObject子类都具备的用于运行时加载处理的一个类方法
    
  • 最好使用单例保证只交换一次 (dispatch_once)

  • 自定义方法名不能产生冲突

  • 对于系统方法要调用原始实现,避免对系统产生影响

  • 做好注释(因为方法交换比较绕)

  • 迫不得已情况下才去使用方法交换