练习01篇runtime/RunLoop/kvo/weak/NSObject/消息补救/方法交换/分类属性

325 阅读9分钟

1. 对runtime理解

 Runtime 的四个重要的概念——动态类型,动态绑定,动态方法决议,内省

动态类型,对象的具体类型在运行时才能确定。

动态绑定,指把消息映射到方法实现的这一过程是在运行时,而不是在编译时完成的。

动态方法决议,是一种能动态的提供方法实现的能力

自省,在运行时检查对象自身信息的能力

应用场景也很多,交换方法、添加属性 objc_set/getAssociatedObject、字典转模型、自动归档和解档等

2. RunLoop的理解

通过管理 内部循环 来  接收和处理 App运行期间 消息事件的  一个对象

1线程的保活、
2处理App中的各种事件(如触摸事件、定时器事件等) 3节省CPU资源,提高程序性能

要处理以下6类事

 1、RunLoopObserver 触发
2、消息通知、非延迟   的perform、dispatch调用、block回调、KVO 4、TIMER_CALLBACK - 延迟的perform, 延迟dispatch调用
3、MAIN_DISPATCH_QUEUE - 主调度队列

5、Source0 - 处理App内部事件、App自己负责管理(触发)
,如UIEvent、CFSocket。普通函数调用,系统调用
6、Source1 - 由RunLoop和内核管理,Mach port驱动, 如CFMachPort、CFMessagePort

3.kvo 的底层实现

实际上是采用了 isa——swilling 的方法。

  • 利用 Runtime API 动态生成一个子类,并且让 instance 对象的 isa 指向这个全新的子类。并重写了被观察属性的 setter 方法。

  • 当修改 instance 对象的属性时,会调用 Foundation 的_NSSetXXXValueAndNotify 函数

    willChangeValueForKey:
    父类原来的setter
    didChangeValueForKey:
    
  • 内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context:

4. weak的原理

weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址数组.
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针.
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表
3、释放时,调用clearDeallocating函数。根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

5. NSObject对象他占用多少内存。对于64位平台的32位就不考虑了,占用多少内存, 一个指针占用多少字节?

NSObject其内部只有_ Class isa;   _Class定义  struct objc_class *Class 可知  isa  就是个指针,所以NSObject *obj = [[NSObject alloc]init];此句代码就相当于分配一块内存存放上边那个结构体,结构体内就只有一个isa指针,指针的地址赋值给obj。一个NSObject对象占用的大小其实就是一个isa指针的大小。在64bit是8字节(32bit是4)__。 但是!!系统真正分配内存的时候是分配了16字节! 系统是分配了16字节给NSObject对象,真正使用的空间其实是一个指针的大小。

@interface Person : NSObject { @public int _no; int _age; } _no的值和_age的值赋值的话各占4个字节。加上_isa指针的8字节,_刚好占满16个字节,对象就没有在开辟新的空间了

@interface Person : NSObject { @public int _no;; },实际上person对象确实只使用了12个字节。但是因为内存对齐的原因。使person对象也占用16个字节。

**大端模式**
所谓的大端模式(Big-endian),是指数据的高字节,保存在内存的低地址中,而数据的低字节,保存在内存的高地址中,
**小端模式**
所谓的小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。

6. 自己制作的一个静态库,平台库里面包括一些类别文件。引用时他一般会报错嘛,就是说那个找不到方法

Xcode中配置"Build Settings"中的“Other Linker Flags”选项添加“-all_load“ 

7.  MVC 的一些缺点

  • 厚重的View Controller

model应包括数据和操作数据的业务逻辑

在实践中,model层往往非常薄

V:视图view通常是UIKit控件或者编码定义的UIKit控件的集合。View的如何构建(PS:IB或者手写界面)何必让Controller知晓,同时View不应该直接引用model(PS:现实中,你懂的!),并且仅仅通过IBAction事件引用controller。业务逻辑很明显不归入view,视图本身没有任何业务。 C:控制器controller。Controller是app的“胶水代码”:协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的loading、appearing、disappearing等等,同时往往也会充满我们不愿暴露的model的模型逻辑以及不愿暴露给视图的业务逻辑。网络数据的请求及后续处理,本地数据库操作,以及一些带有工具性质辅助方法都加大了Massive View Controller的产生。

  • 遗失(无处安放)的网络逻辑
    苹果使用的MVC的定义是这么说的:所有的对象都可以被归类为一个model,一个view,或是一个controller。
    试着把它放在Model对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个_网络请求比持有它的model生命周期更_长,事情将变的复杂。显然View里面做网络请求那就更格格不入了,因此只剩下Controller了。若这样,这又加剧了Massive View Controller的问题。

  • 较差的可测试性

由于 C混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。

8. 消息补救的流程

第一步、动态方法解析 通过调用+resolve(Instance/Class)Method 这两个方法,来提供一个函数实现,如果有添加,就会走消息重新发生的过程。如果没有走下一步

第二步、提供后备接受者对象Fast Forwarding),如果实现了-forwardingTargetForSelector:,会将消息转发给其他对象的机会。只要不返回nil/self,整个消息发送的过程就会被重启。否则下一步

第三部、拿到方法签名,封装invocation更换其他target对象(Normal forwarding),最后一次机会。通过发送-methodSignatureForSelector:获得参数和返回值类型,如果返回函数签名,就会生成一个NSInvocation对象,并发送-forwardInvocation:消息给这个目标对象。

如果返回nil,会发送-doesNotRecognizeSelector:消息。程序挂掉

9. 那你再发使用方法交换的时候需要注意一些什么

我们在做方法替换的时候,最好是能按照继承链的顺序来执行,那么**initialize****load**都能达到这个效果;为什么选择load

  1. _在子类没有实现**initialize**时候,父类的**initialize**会执行多次,_假如在这里做替换就会出现偶数次替换,方法替换失效的问题;
  2. 类别中实现了**initialize**会覆盖类中的方法,如果有多个类别都在**initialize**中做处理的话,那么只有一个会生效其他都会失效,具体哪个生效看compile source中哪个在最后。

以上这两个副作用,load都没有,所以还是选择在load中处理,虽然load会很微弱的影响启动时间。

  • dispatch_once + load保证替换执行一次

  • load保证在继承关系中替换时,按照继承链来替换

  • 方法替换时检查类中是否实现了原方法,避免子类中没有实现,替换子类的方法时,将父类的方法替换了

    1. 之所以选在load方法中去实现, 是因为load在文件加载的时候就会被调用, 甚至早于main函数, 这样不会出现原方法被调用的时候, 还没交换的情况

    2. dispatch_once_t就是保证这段代码只执行一次

      3. 交换方法里要调一下自己

      - (void)xx_viewWillAppear:(BOOL)animated {
      //这里调用自己不会造成死循环, 看似是调用自己, 实则调用的是`viewWillAppear:`的实现
          [self xx_viewWillAppear:animated];
          NSLog(@"xx_viewWillAppear");
      }
      
Load方法: **1、 执行顺序,** 有继承关系的  **先父类后子类,最后分类。**
**无继承关系两个类由编译顺序决定(**build **Phases** - **Compile Sources**
**2、一个子类load 里调用[super load];会怎么样**    因为子类执行load前 父类已经load, 会将父类的load再次执行,如果父类有分类重写了load,此时会执行分类的load方法。

10. 为什么分类不能使用属性呢?

主要实际上还是内存分配相关。属性是由ivar 、get、set 三部分组成,
分类不能添加实例变量,Class实际上是一个指向objc_class结构体,在编译的时候 objc_class结构体大小已经固定,内存已经分配不可能往这个结构体中添加数据,只能修改

成员变量链表指向是固定地址,methodList是二维数组 可以通过修改增加方法,因此,可以动态添加方法,不能添加成员变量。也就可以通过运行时动态加载属性。

11、对象发送消息会经过以下几个步骤

在对象的类中查找selector,如果找到了,执行对应函数的IMP

查找过程会先从方法缓存中查找,然后再沿着类的继承关系链查找,

如果没有找到,就会进行三次消息转发、补救的过程。

第一次是 动态方法决议

第二次是 提供 后备接收者对象

第三次是 以其他形式实现该消息方法(返回方法签名, 直接切换调用目标,也就是该方法的**Target**)获取到的方法签名包装成Invocation,将NSInvocation多次转发到多个对象