iOS面试题收纳-底层原理

544 阅读1小时+

程序编译->启动过程

一个程序从简单易读的代码到可执行文件往往要经历以下步骤

源代码 > 预处理器 > 编译器 > 汇编器 > 机器码 > 链接器 > 可执行文件

预处理(Prepressing)hello.c->hello.i

主要是处理源代码中以#开始的预编译指令,比如#include,#define等。

  1. 将所有的宏定义#define删除并展开
  2. 处理所有的条件预编译指令,比如#if,#ifdef,#elif,#else,#endif等。
  3. 处理#include,#import等预编译指令,将被包含的文件插入到该预编译指令的位置。这个插入的过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  4. 删除所有注释///***/
  5. 添加行号和文件标识,以便编译时产生调试的行号及编译错误报警行号
  6. 保留所有的#pragma 编译器指令,因为编译器需要使用他们。

编译(Compilation)hello.i -> hello.s

词法分析

扫描器(Scanner)将源代码的字符序列分割成一系列的记号(Token)。

注:lex工具可实现按照用户描述的词法规则将输入的字符串分割为一个一个记号。

语法分析

语法分析器将记号(Token)产生语法树(Syntax Tree)。

注:yacc工具(yacc: Yet Another Compiler Compiler)可实现语法分析,根据用户给定的语法规则对输入的记号序列进行解析,从而构建一个语法树,所以它也被称为“编译器编译器(Compiler Compiler)”。

语义分析

编译器所分析的语义是静态语义:指在编译期可以确定的语义,通常包括声明和类型的匹配,类型的转换

注:与之相对的为动态语义分析,只有在运行期才能确定的语义。

中间代码生成

源代码优化器(Source Code Optimizer)将语法树转换成中间代码,然后进行源码级别进行优化,例如2+6,其值在编译期就可以确定,直接优化成8。

中间代码使得编译器被分为前端和后端,不同的平台可以利用不同的编译器后端将中间代码转为机器代码,实现跨平台。

目标代码生成

此后的过程属于编译器后端,代码生成器(Code Generator)将中间代码转换成目标代码(汇编代码)

目标代码优化

目标代码优化器(Target Code Optimizer)对目标代码进行优化,比如调整寻址方式、使用位移代替乘法、删除多余指令、调整指令顺序等

汇编(Assembly)hello.s-> hello.o

汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。

怎么理解符号表

  1. iOS 构建时产生的符号表,是内存地址、函数名、文件名和行号的映射表。
  2. 格式大概是
<起始地址> <结束地址> <函数> [<文件名:行号>]

作用

Crash时的堆栈信息,全是二进制的地址信息。如果利用这些二进制的地址信息来定位问题是不可能的,因此我们需要将这些二进制的地址信息还原成源代码中的函数以及行号,这时候符号表就起作用了。

  1. 利用符号表将原始Crash的二进制堆栈信息还原成包含行号的源代码文件信息,可以快速定位问题。
  2. iOS 中的符号表文件(DSYM) 是在编译源代码后,处理完 Asset Catalog 资源和 info.plist 文件后开始生成的
  3. 生成符号表文件(DSYM)之后,再进行后续的链接、打包、签名、校验等步骤。

链接(Linking)

  • 源文件经过一系列处理以后,会生成对应的.o文件
  • 一个项目会有许多.obj文件,并且这些文件之间会有各种各样的联系,例如函数调用。
  • 链接器做的事就是把这些目标文件和所用的一些库链接在一起形成一个完整的可执行文件

静态链接 a.o,b.o->xx.a

  1. hello.o文件(目标文件)是以分段的形式组织在一起的。

    • 简单来说,把程序运行的地址划分为了一段一段的片段
    • 有的片段用来存放代码叫代码段,给这个段加个只读的权限,防止程序被修改;
    • 有的片段用来存放数据叫数据段,数据经常修改,所以可读写;
    • 有的片段用来存放标识符的名字,比如某个变量 ,某个函数,叫符号表;等等。
    • 由于有这么多段,所以为了方便管理又引入了一个段,叫段表,方便查找每个段的位置
  2. 当文件之间相互需要链接的时候,就把相同的段合并,然后把函数,变量地址修改到正确的地址上。这就是静态链接

    • 假如有多个程序都链接一个相同的静态库,这样程序运行起来后就有了多个副本。对于计算机的内存和磁盘的空间浪费比较严重
    • 程序的更新、部署和发布会带来很多麻烦。一旦程序任何位置有一个小小的改动,都会导致重新下载整个程序

Xcode里的 -ObjC、-all_load、-force_load 的作用

  • OC的链接器并不会为每个方法建立符号表,而是 仅仅为类建立符号表

  • 因此,如果静态库中定义了一个已存在类的分类,链接器就会以为这个类已经存在,从而不会把分类和核心代码结合起来

  • 这样的话,在最后的可执行文件中,就会由于缺少分类里的代码,导致函数调用失败

    -ObjC
    • 加了这个参数后,链接器会把静态库中所有的OC类和分类都加载到最后的可执行文件
    • 但是当静态库中只有分类而没有主类的时候,-ObjC参数就会失效,这是编译器的一个bug。这时候就需要使用-all_load或者-force_load
    -all_load
    • 让链接器把所有找到的目标文件都加载到可执行文件
    • 千万不要随便使用这个参数!假如你使用了不止一个静态库文件,然后又使用了这个参数,那么你很有可能会遇到ld:duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件
    • 建议在遇到-ObjC失效的情况下使用-force_load参数
    -force_load
    • 所做的事情跟-all_load其实是一样的
    • 但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载

动态链接

  1. 当我们在应用程序中使用动态库(dylib,framework 等)中的函数,类等内容时
  2. 静态链接生成的二进制产物中会包含这些符号,并且标记为“unbound symbol”
  3. 在将 APP 加载到内存中启动时,操作系统会读取这些动态库并将它们加载到内存中
  4. 同时,动态链接器会找到我们引用的属于动态库的符号被加载到内存中的位置,并将二进制产物中的这些 unbound symbol 所对应的地址修正为内存中这些符号的实际地址

动态链接器:在程序运行的时候,把程序中所有未定义的符号(比如调了动态库的一个函数,或者访问了一个变量)绑定到动态链接库中

动态链接文件,把那些需要修改的部分分离出来,与数据放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这种方案就是目前被称为地址无关代码(PIC,Position-independent Code)的技术

什么是fixup chain?

  1. Fixup chain 是 Apple 在 iOS15 系统上所应用的一种新的动态链接技术
  2. 如果开发者使用了 fixup chain 动态链接方案,那么生成的 Mach-O 文件则会通过一种类似于链表的方式来储存动态链接过程中所需要的 rebase & bind 信息

lazy symbol 和 non-lazy symbol

这两种符号的区别是:它们的地址被修正为内存中这些符号真实地址的时机;

  1. 对于 lazy symbol 而言,这些符号的加载是在第一次调用它们的时候,由 dyld 来找到外部动态库中这些符号的正确地址然后进行调用
  2. 对于 non-lazy symbol 而言,这些符号在二进制程序被加载进内存时就被 dyld 定位到符号地址并记录下来,在调用时可以直接调用

dyld?

  1. dyld 是苹果 MacOS / iOS 系统上的动态链接器
  2. 负责在程序装载进内存时 / 程序运行时,将主二进制中引用外部符号的地址指向加载到内存中的外部动态库中对应符号所定义的位置
  3. 并对内部的一些数据指针所存储的地址的值进行修正;这个过程有两个核心阶段,rebase 和 binding。
aslr
  1. ASLR(Address Space Layout Randomization,地址空间随机化)是一种操作系统用来抵御缓冲区溢出攻击的内存保护机制
  2. 当 Mach-O 文件载入虚拟内存的时候,ASLR 会随机向加载到内存的 Mach-O 文件向后移动一段距离(被称为 slide),使得起始地址不从 0x0000 开始,而是从一个随机值,例如 0x5000 开始(也就是 slide=0x5000),并且后面的函数地址都会增加 0x5000
rebase

Rebase 表示 dyld 根据 ASLR 产生的 slide 来修正二进制程序中对应地址的过程

binding
  1. Binding 表示 dyld 将二进制程序中引用的外部动态库符号的地址指向内存中加载好的这些动态库的地址;
  2. 这个过程需要借助二进制程序(在苹果的操作系统上为 Mach-O 格式)的 __DATA segment 中 __la_symbol_ptr(lazy symbol)和 nl_symbol_ptr(non-lazy symbol)两个 section;
  3. 这两个 section 用来保存指向符号真实地址位置的指针
  4. 但第一次访问这个 section 中的符号时,dyld 还没有找到这些符号的真实地址,因此此时它们会指向 __TEXT segment 中 __stub_helper section 的位置
  5. 然后调用 dyld_stub_binder 寻找加载好的动态库中相应符号的地址
  6. 找到地址之后,dyld 会用这个地址覆盖 __la_symbol_ptr / __no_symbol_ptr 中原本指向 stub_helper section 的内容
  7. __这样下一次访问这些符号时,可以直接跳转到符号的真实地址,无需再去通过 __stub_helper 定位符号地址。

压缩字节流方案

在 iOS15 之前,rebase 和 binding 操作都是通过一系列 rebase / binding info 来完成,其大概流程如下:

在 Mach-O 的 LC_DYLD_INFO_ONLY load command 中,会说明以下五种 info 的偏移量和大小:

  • Rebase info:用来进行 rebase 操作的一系列 opcode stream

  • Bind info:用来进行 bind 操作的一系列 opcode stream,用于 non-lazy symbol(__nl_symbol_ptr section 中的符号)

  • Lazy info:用来进行 bind 操作的一系列 opcode stream,用于 lazy symbol(__la_symbol_ptr section 中的符号)

  • Weak info:用来进行 bind 操作的一系列 opcode stream,与 bind info 的区别是这部分 info 用于 bind weak symbols,也就是说如果由于没找到这个符号而链接失败的话,不会直接触发 SIGTRAP 导致程序崩溃,而是返回一个 NULL 表示未找到这个符号,并且将后续使用这个符号时的处理流程交给开发者来解决;

  • Export info:用于 export 符号

所谓的 opcode stream 就是类似于汇编指令一般,是一系列由opcode和立即数组成的命令的合集。通过一行一行解释执行 stream 中的指令,dyld 就可以完成 rebase 和 binding 的过程

装载(Loading)

目标文件在内部结构上和可执行文件的结构几乎是一样的,一般跟可执行文件用同一种格式进行存储。

  1. 创建虚拟地址空间
  2. 读取可执行文件头,建立虚拟地址空间与可执行文件的映射关系
  3. 将CPU的指令寄存器设置为运行库的初始函数(初始函数不止一个,第一个启动函数为:_start),初始了main()函数的环境,然后指向可执行文件的入口。

启动过程

底层基础

一个NSObject对象占用多少内存

  • 操作系统在给对象分配内存空间时,会进行内存对齐。在 iOS 中是按照16字节对齐的,也就是说分配的空间都是16字节的整数倍,并且最小为16字节。所以一个 NSObject对象会分配 16byte 的内存空间(malloc_size函数)

    #import <malloc/malloc.h>
    // 创建一个实例对象,实际上分配了多少内存
    malloc_size((__bridge const void *)obj); 
    
  • 实际上:在 64位下只使用了 8 byte,在32位下只使用了 4 byteclass_getInstanceSize函数)

    #import <Objc/Runtime>
    // 创建一个实例对象,至少需要多少内存
    class_getInstanceSize([NSObject Class])
      
    size_t class_getInstanceSize(Class cls) {
        if (!cls) return 0;
        return cls->alignedInstanceSize();
    }
    

一个Objective-C对象如何进行内存布局?(考虑有父类的情况)

  • 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
  • 父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中
  • 每一个对象内部都有一个isa指针,指向它的类对象,类对象中存放着本对象的如下信息
    • 对象方法列表
    • 成员变量列表
    • 属性列表
  • 每个 Objective-C 对象都有相同的结构,如下图所示
Objective-C 对象的结构图
ISA指针
根类(NSObject)的实例变量
倒数第二层父类的实例变量
...
父类的实例变量
类的实例变量
  • 根类对象就是NSObject,它的super class指针指向nil
  • 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类

img

NSObject对象的本质是什么

NSObject对象的本质就是结构体

typedef struct objc_class *Class;
struct NSObject_IMPL {
  Class isa;
}

objc_nsobject_0.png

OC对象的分类

instance对象(实例对象)

  • instance对象就是通过类 alloc出来的对象,每次调用alloc都会产生一个新的instance对象
  • instance对象在内存中存储的信息包括
    • isa指针
    • instance对象成员变量的值

class对象(类对象)

  • 每个类在内存中有且只有一个class对象
  • class对象在内存中存储的信息主要包括
    • isa指针
    • superclass指针
    • cache 方法缓存列表
    • 对象属性信息(@property)、对象方法信息(instance method)、类的协议信息(protocol)、类的成员变量信息(ivar)
    • bits原始数据:存放着class_rw_t的指针

MetaClass对象(元类对象)

  • 每个类在内存中有且只有一个MetaClass对象
  • meta-class对象和class对象的内存结构是一样的,但是用途不一样。在内存中存储的信息主要包括
    • isa指针
    • superclass指针
    • cache方法缓存列表
    • 类的类方法信息(class method)
    • bits原始数据:bits里存放着class_rw_t的指针

OC的类信息存放在哪里

  • 对象方法列表、属性列表、成员变量列表、遵循的协议列表信息,存放在class对象中
  • 类方法、类属性,存放在MetaClass对象中
  • isa指针、成员变量的具体值,存放在instance对象

对象的isa指针指向哪里,有什么作用

  • 在OC中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有 isa 指针

  • isa 是一个Class 类型的指针。isa主要的作用在于从它所属的类/元类对象上查找方法

objc_isa.png

  • instance对象(实例对象)的isa指向class对象
    • 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用
    • 如果找不到,就通过class的superclass在父类找方法实现
  • class对象(类对象)的isa指向类的MetaClass对象
    • 当调用类方法时,通过class的isa找到MetaClass,最后找到类方法的实现进行调用
    • 如果找不到,就通过MetaClass的superclass在父类找方法实现
  • MetaClass对象(元类对象)的isa指向NSObject的MetaClass对象,NSObject MetaClass对象的isa指向自己
  • NSObject MetaClass的superClass是基类NSObject,这样就形成了一个闭环。

为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?

  • 方法是每个对象可以共用的,如果每个对象都存储一份方法列表太浪费内存。
  • 由于对象的isa是指向类对象的,当调用的时候,直接去类对象中查找就行了。可以节约很多内存空间

说一下对 isa 指针的理解, isa 指针有哪两种类型?

isa 有两种类型

objc_isa_map.png

  • 纯指针,指向内存地址

  • NON_POINTER_ISA,除了内存地址,还存有一些其他信息

  • 在Runtime源码中查看isa_t的简化结构如下:

    • 在arm64构架之前,isa的值就是 类对象的地址值
    • 在arm64构架开始的时候,对isa进行了优化, 使用了联合的技术。
    • 在64位的内存地址中存储了很多东西,其中33位存储的是isa具体的地址值。
    • 联合体中 前三位 代表特殊的标记,所以& isa_mask出来的类对象地址值的二进制 后面三位永远都是000, 十六进制就是8或者0结尾的地址值
    union isa_t  {
        Class cls;
        uintptr_t bits;
        # if __arm64__ // arm64架构
    #   define ISA_MASK        0x0000000ffffffff8ULL //用来取出33位内存地址使用(&)操作
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
            uintptr_t nonpointer        : 1; //0:代表普通指针,1:表示优化过的,可以存储更多信息。
            uintptr_t has_assoc         : 1; //是否设置过关联对象。如果没设置过,释放会更快
            uintptr_t has_cxx_dtor      : 1; //是否有C++的析构函数
            uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 内存地址值
            uintptr_t magic             : 6; //用于在调试时分辨对象是否未完成初始化
            uintptr_t weakly_referenced : 1; //是否有被弱引用指向过
            uintptr_t deallocating      : 1; //是否正在释放
            uintptr_t has_sidetable_rc  : 1; //引用计数器是否过大无法存储在ISA中。如果为1,那么引用计数会存储在一个叫做SideTable的类的属性中
            uintptr_t extra_rc          : 19; //里面存储的值是引用计数器减1
    
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    }
    

说说你对方法调用的理解

  • 函数调用实际上就是给对象发送一条消息,底层是调用了objc_msgSend(对象, @selector(对象方法))
  • 寻找顺序(对象方法):根据instance的isa指针找到类对象,在类对象中寻找方法,若没有则去class对象的superClass中查找
  • 寻找顺序(类方法):根据instance的isa指针找到MetaClass,在MetaClass对象中寻找类方法,若没有则去MetaClass的superClass中查找

class方法和objc_getClass方法有什么区别

  • object_getClass(obj) 返回的是obj中的isa指针
  • [obj class] 则分两种情况
    • obj为实例对象,[obj class]中class是实例方法 - (Class)class;返回的是obj对象的isa指针
    • obj为类或元类对象,调用的是类方法:+ (Class)class,返回的结果为其本身

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

  • 不能向编译后得到的类中增加实例变量
    • 编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的列表和 instance_size 实例变量的内存大小都已经确定
    • 同时runtime 会调用 class_setIvarLayoutclass_setWeakIvarLayout 来处理 strong和weak 引用。所以不能向存在的类中添加实例变量
  • 能向运行时创建的类中添加实例变量
    • 运行时创建的类可以通过调用 class_addIvar 函数添加实例变量。
    • 但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前。

在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)

动态创建类涉及到以下几个函数:

/*
// 给一个新类和元类分配空间
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes);
如果我们要创建一个根类,则superclass指定为Nil。extraBytes通常指定为0,该参数是分配给类和元类对象尾部的索引ivars的字节数

然后使用诸如class_addMethod,class_addIvar等函数来为新创建的类添加方法、实例变量和属性等
实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。

// 在应用中注册由objc_allocateClassPair创建的类
void objc_registerClassPair(Class cls);

// 销毁一个类及其相关联的类
void objc_disposeClassPair(Class cls);
如果程序运行中还存在类或其子类的实例,则不能针对类调用该方法。
*/

因为 此方法会创建一个类以及元类,正好组成一对

Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes){
    ...省略了部分代码
    //生成一个类对象
    cls  = alloc_class_for_subclass(superclass, extraBytes);
    //生成一个类对象元类对象
    meta = alloc_class_for_subclass(superclass, extraBytes);
    objc_initializeClassPair_internal(superclass, name, cls, meta);
    return cls;
}

objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)

  • 向 nil 发送消息并不会引起程序crash,只是在运行时不会有任何作用。但是对[NSNull null]对象发送消息是会crash的。
  • 如果方法返回值是对象,发送给nil的消息将返回0(nil)
  • 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*)
  • float,double,long double 或者long long的整型标量,发送给nil的消息将返回0
  • 如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0
  • 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的

ARC 都帮我们做了什么?

  • LLVM + Runtime 会为我们代码自动插入retainrelease以及 autorelease等代码,不需要我们手动管理
  • 每个指向OC对象的指针,都被赋上了所有权修饰符。一共有__strong__weak__unsafe_unretained__autoreleasing四种所有权修饰符
  • 当一个对象被赋值给一个使用__autoreleasing修饰符修饰的指针时,相当于这个对象在MRC下被发送了autorelease消息,也就是说它被注册到了autorelease pool中
  • 全局变量和实例变量是无法用__autoreleasing来修饰的,不然编译器会报错
  • 局部变量用__autoreleasing修饰后,其指向的对象,在当前autorelease pool结束之前不会被回收

ivar、getter、setter是如何生成并添加到这个类中的?

  • 使用“自动合成”( autosynthesis),这个过程由编译器在编译阶段执行,自动合成,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码

  • 除了生成getter、setter方法之外,编译器还要自动向类中添加成员变量(在属性名前面加下划线,以此作为实例变量的名字)

  • 为了搞清属性是怎么实现的,通过反编译相关的代码,大致生成了五个东西

      // 该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远
      OBJC_IVAR_$类名$属性名称
    
      // 方法对应的实现函数
      settergetter
    
      // 成员变量列表
      ivar_list
    
      // 方法列表
      method_list
    
      // 属性列表
      prop_list
    
    • 每次增加一个属性,系统都会在ivar_list中添加一个成员变量的描述

    • 在method_list中增加setter与getter方法的描述

    • 在prop_list中增加一个属性的描述

    • 计算该属性在对象中的偏移量

    • 然后给出setter与getter方法对应的实现,

      • 在setter方法中,从偏移量的位置开始赋值

      • 在getter方法中,从偏移量开始取值

        为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转

简述一下 dealloc 调用流程

当一个对象引用计数为0,要释放时,会自动调用dealloc

  1. dealloc 调用流程

    • 首先调用 _objc_rootDealloc()
    • 接下来调用 rootDealloc()
    • 这时候会判断是否可以被释放,判断的依据主要有5个
      • NONPointer_ISA
      • weakly_reference
      • has_assoc
      • has_cxx_dtor
      • has_sidetable_rc
    • 如果有以上五中任意一种,将会调用 object_dispose()方法,做下一步的处理
    • 如果没有之前五种情况的任意一种,则可以执行释放操作,C函数的 free()
    • 执行完毕
  2. object_dispose() 调用流程

    • 直接调用 objc_destructInstance()
    • 之后调用 C函数的 free()
  3. objc_destructInstance() 调用流程

    • 先判断 hasCxxDtor,如果有 C++ 的相关内容,要调用 object_cxxDestruct() ,销毁 C++ 相关的内容
    • 再判断 hasAssocitatedObjects,如果有的话,要调用 object_remove_associations(),销毁关联对象的一系列操作
    • 然后调用 clearDeallocating()
    • 执行完毕
  4. clearDeallocating() 调用流程

    • 先执行 sideTable_clearDellocating()
    • 再执行 weak_clear_no_lock,在这一步骤中,会将指向该对象的弱引用指针置为 nil
    • 接下来执行 table.refcnts.erase(),从引用计数表中擦除该对象的引用计数
    • 至此为止,Dealloc 的执行流程结束

Dealloc注意事项

zhangferry.com/2022/02/24/…

什么是响应者链(事件传递、响应链)

  1. 响应者链是用于确定事件响应的一种机制,事件主要是指触摸事件(touch Event)
  2. 该机制与UIKit中的UIResponder类密切相关,响应触摸事件的必须是继承自UIResponder的类,最常用的比如UIView 、UIViewController、UIWindow

事件传递过程

事件识别

  1. 苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
  2. 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收
  3. SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、以及传感器等几种 Event,随后用 mach port 转发给需要的 App 进程
  4. 之后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发
  5. _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发
    • 其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。
    • 通常事件比如 UIButton 点击touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的

事件传递

  1. 上一步已经把识别到的事件存放到了一个事件队列里

  2. Application从事件队列取出事件,接着需要找到命中视图。命中视图依赖UIView的2个方法

    // 返回能够相应该事件的视图
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event  
    // 查看点击的位置是否在这个视图上
    -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 
    
  3. 寻找事件的命中视图是通过对视图调用hitTest和pointInside完成的

  4. hitTest的调用顺序是从UIWindow开始,对视图的每个子视图依次调用,也可以说是从显示最上面到最下面 遍历直到找到命中视图

objc_eventchin_find_code.png

传递终止条件

  • 不接收用户交互,即:userInteractionEnabled = NO
  • 隐藏,即:hidden = YES
  • 透明,即:alpha <= 0.01
  • 未启用,即:enabled = NO

如何找到最合适处理事件的控件:

  • 首先,判断自己能否接收触摸事件
    • 可以通过重写**hitTest:withEvent:**方法验证
  • 其次,判断触摸点是否在自己身上
    • 对应方法pointInside:withEvent:
  • 从后往前(先遍历最后添加的子控件)遍历子控件,重复前面的两个步骤
  • 如果没有符合条件的子控件,那么就自己处理

响应链传递

  1. 找到命中视图后,任务并未完成,因为命中者不一定是事件的响应者
  2. 事件会从此视图开始,沿着响应链nextResponder进行传递,直到找到处理事件的响应视图,如果没有处理的事件会被丢弃(UIControl子类不适用)
    • 如果当前view是控制器的view,那么就传递给控制器
    • 如果控制器不存在,则将其传递给它的父控件
    • 在视图层次结构的最顶层视图也不能处理接收到的事件或消息,则将事件或消息传递给UIWindow对象进行处理
    • 如果UIWindow对象也不处理,则将事件或消息传递给UIApplication对象
    • 如果UIApplication也不能处理该事件或消息,则将其丢弃
    • 它们都是通过重写nextResponder来实现
  3. UIControl的子类和UIGestureRecognizer优先级较高,会打断响应链

如何判断上一个响应者

  • 如果当前这个view是控制器的view,那么控制器就是上一个响应者
  • 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

解释一下 手势识别 的过程?

  1. 当上面的 _UIApplicationHandleEventQueue()识别了一个手势时,首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断
  2. 随后系统将对应的 UIGestureRecognizer 标记为待处理
  3. 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件
  4. 这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer 的回调
  5. 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理

dSYM你是如何分析的?

dSYM是什么?

  1. Xcode编译项目后,我们会看到一个同名的dSYM文件

  2. dSYM是 保存16进制函数地址映射信息的中转文件,我们调试的symbols都会包含在这个文件中

  3. 每次编译项目的时候都会生成一个新的dSYM文件,位于 /Users/用户名/Library/Developer/Xcode/Archives 目录下

dSYM文件有什么作用?

  1. 用于分析crash report文件
  2. 导出崩溃文件后可以根据出错的函数地址,通过dSYM文件查找对应的函数名和文件名

如何将文件一一对应

  1. 每一个xx.app和xx.app.dSYM文件都有对应的UUID
  2. crash文件也有自己的UUID,如果这三个文件的UUID一致,就可以通过他们解析出正确的错误信息

如何查看UUID

  1. 通过 dwarfdump --uuid xx.app/xx 查看xx.app文件的UUID
  2. 通过 dwarfdump --uuid xx.app.dSYM 查看 xx.app.dSYM文件的UUID
  3. crash文件内第一行 incident Identifier就是该crash文件的UUID

写出调用以下方法的几种方式

- (void)fun:(NSString*)name {
    NSLog(@"name === %@",name);
}

// 直接调用
[self fun:@"name"];

// 使用performselector
[self performSelector:@selector(fun:) withObject:@"funname"];

// 使用NSInvocation调用
SEL funSel = @selector(fun:);
NSMethodSignature * sign = [self methodSignatureForSelector:funSel];
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:sign];
[invocation setTarget:self];
[invocation setSelector:funSel];
//index下标中 0是self, 1是Selector,所以这里从2开始
NSString * s = @"invocationname";
[invocation setArgument:&(s) atIndex:2];
[invocation invoke];

// 使用runtime发送消息给对象
((void (*)(id, SEL, NSString*))objc_msgSend)(self, @selector(fun:), @"name2");

KVO

什么是KVO(KeyValueObserver)?

KVO是观察者模式的一种实现,对象A订阅对象B的某个属性变化,只要对象B的属性发生变化,就会触发对象A的观察方法进行回调。

iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • KVO是通过isa-swizzling技术实现的

    • 利用Runtime API动态生成一个子类NSKVONotifying_XXX,并且让instance对象的isa指向这个子类
    • 将父类的setter方法指向_NSSetYYYValueAndNotify等函数
    • 重写classdealloc方法,新增_isKVOA方法
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数

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

  • 轻量级KVO框架:GitHub - facebook/KVOController

objc_kvo_map.png

如何手动触发KVO

  1. 重写对应类的 automaticallyNotifiesObserversForKey,判断是否是指定属性,是的话返回NO
  2. 手动调用 willChangeValueForKey: 和 didChangeValueForKey:等方法

KVO中的context有什么作用?

observeValueForKeyPath:ofObject:change:context: 中的context是一个void*类型的,可以传递任何指针。

我们可以用它来做一些需要区别的事情,比如我们子类和父类都监听了一个类的属性,这时候就可以根据回调监听方法里的context判断具体是哪个类再做相应的动作

直接修改成员变量会触发KVO么?

self->_myBool 不会触发KVO,必须通过KVC或者setter方法才会触发

通过KVC修改属性会触发KVO么?

使用kvc修改属性时,也会查找对应类属性的 setXXX并调用,从而触发KVO

如果声明属性时用setter指定了setter方法,kvo是否还会监听到

如果我们使用 @property(nonatomic, strong, setter=updateMyName:) NSString *name;定义属性。

此时再对name进行赋值,是收不到属性变化的。

KVC

KVC(KeyValueCoding)

KVC的全称是Key-Value Coding,俗称“键值编码”

KVC可以通过key直接访问对象的属性或者给对象的属性赋值,这样可以在运行时动态的访问或修改对象的属性

-(id)valueForKey:(NSString *)key;
-(void)setValue:(id)value forKey:(NSString *)key;

当调用setValue:forKey:时,底层的执行机制如下

objc_kvc_set.png

  • 程序首先尝试调用 set<Key>: 方法,通过setter方法完成设置
    • 注意:这里的<key>是指成员变量名,首字母大小写要符合KVC的命名规则,下同
  • 如果没有找到set<Key>:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法
    • 该方法默认会返回YES,KVC机制会搜索该类里面有没有名为<Key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只要存在以<Key>命名的变量,KVC都可以对该成员变量赋值
    • 如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法
  • 如果该类既没有set<Key>:方法,也没有_<Key>成员变量,KVC机制会搜索_is<Key>的成员变量。
  • 如果该类既没有set<Key>:方法,也没有_<Key>和_is<Key>成员变量,KVC机制再会继续搜索<Key>is<Key>的成员变量。再给它们赋值。
  • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

如果开发者想让这个类禁用KVC,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:方法时,会直接用setValue:forUndefinedKey:方法。

当调用valueForKey:时,其搜索方式如下

objc_kvc_get.png

  • 首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
  • 如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。
    • 如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。
    • 还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
  • 如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。
    • 如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。
  • 如果还没有找到,再检查类方法+(BOOL)accessInstanceVariablesDirectly,
    • 如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。
    • 如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:方法,默认是抛出异常

Category

Category的好处和使用场合是什么

  • 在不修改原有类代码的情况下,为类添加对象方法或者类方法
  • 分解庞大的类文件,减少单个文件的体积
  • 把不同的功能组织到不同的Category里
  • 由多个开发者共同完成一个类
  • 声明私有方法,用于单独引入
  • 可以按需加载想要的category 等等

使用场合:

  • 添加实例方法
  • 添加类方法
  • 添加协议
  • 添加属性
  • 关联成员变量

Category的原理是什么?实现过程

原理

Category的底层结构是结构体category_t,代码编写分类之后,分两个阶段加载到运行时:

struct category_t {
  	// 所属类的名称
    const char *name;
  	// 所属类的Class引用指针
    classref_t cls;
  	//  给类添加的所有实例方法的列表
    WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
    // 给类添加的所有类方法的列表
    WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;
    // 给类实现的所有协议的列表
    struct protocol_list_t *protocols;
    // 给类添加的所有实例属性 @property(nontomic, strong) 
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    // 给类添加的所有类属性 @property(nontomic, strong, class) 
    struct property_list_t *_classProperties;
}
  • 编译阶段:

    • 将每一个分类都编译生成对应的category_t结构体
    • 将这些结构体写入到macho文件中
  • Runtime运行时阶段:

    • dyld从macho中读取并加载category_t结构体到内存中
    • 将生成的分类数据合并到原始的类中去,会添加到class_rw_ext这个结构中
    • 合并后的方法列表,属性列表,协议列表等都放在二维数组当中

调用顺序

  • 为什么Category的中的方法会优先调用?

    • 类中的方法列表是一个二维数组,数组中每个元素是一个一维方法列表。

    • 查找方法的时候是先遍历外层二维数组,然后遍历内层一维方法数组。

    • 因为分类方法列表在二维数组前面,所以调用方法时会先调用分类中的方法

  • 如果多个分类中都实现了同一个方法,那么在调用该方法的时候会优先调用哪一个方法?

    • 在多个分类中拥有相同的方法的时候, 会根据编译的先后顺序来添加分类方法列表
    • 后编译的分类方法在最前面,所以要看 Build Phases --> compile Sources中的顺序。

源码阅读顺序

  • objc-os.mm
    • _objc_init
    • map_images
    • map_images_nolock
  • objc-runtime-new.mm
    • load_images
      • loadAllCategories
        • load_categories_nolock
          • attachCategories
            • attachLists
    • _read_images
      • realizeClassWithoutSwift
        • methodizeClass
          • objc::unattachedCategories.attachToClass
            • attachCategories

分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?

分类和扩展的区别

  1. 分类是在运行时把分类信息合并到类信息中,而扩展是在编译时就把信息合并到类中
  2. 分类声明的属性,只会生成getter/setter方法的声明,不会自动生成成员变量和getter/setter方法的实现,而扩展会
  3. 分类不可用为类添加实例变量,而扩展可以
  4. 分类可以为类添加方法的实现,而扩展只能声明方法,而不能实现

可以分别用来做什么

  1. 分类主要用来为某个类添加方法,属性,协议(我一般用来为系统的类扩展方法或者把某个复杂的类的按照功能拆到不同的文件里)

  2. 扩展主要用来为某个类添加原来没有的成员变量、属性、方法

    注:方法只是声明(一般用扩展来声明私有属性,或者把.h的只读属性重写成可读写的)

分类的局限性

  1. 无法为类添加实例变量,但可通过关联对象进行实现

    注:关联对象中内存管理没有weak,用时需要注意野指针的问题,可通过其他办法来实现,具体可参考iOS weak 关键字漫谈

  2. 分类的方法若和类中原本的实现重名,会覆盖原本方法的实现

    注:并不是真正的覆盖

  3. 多个分类的方法重名,会调用最后编译的那个分类的实现

Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

  • 有load方法
  • load方法在runtime加载类、分类的时候调用
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

load、initialize方法的区别是什么?当一个类有分类的时候为什么+load能多次调用而initialize只调用了一次?

load、initialize方法的区别是什么

调用方式

  • load 根据函数地址直接调用
  • initialize 是通过 runtime 的消息机制 ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize)); 进行调用

调用时刻

  • load是runtime加载类、分类到内存的时候调用(只会调用1次),main函数之前
  • 当类第一次收到消息的时候会调用类的initialize方法,通常是在alloc的时候。每一个类只会调用1次(但是父类的initialize方法可能会调用多次),有些子类没有initialize方法所以调用父类的。

调用顺序

  • load:父类->子类->分类
    • 先调用主类的+load方法
      • 按照编译的先后顺序调用(先编译、先调用)
      • 调用子类的+load方法之前,会先调用父类的+load
    • 再调用分类的+load方法
      • 按照编译的先后顺序调用(先编译、先调用)
  • initialize:分类->子类->父类
    • initialize就是按照普通的消息发送流程进行调用
    • 优先调用分类的 initialize,如果没有分类,会调用子类的,如果子类未实现则调用父类的

当一个类有分类的时候,为什么+load能多次调用而initialize只调用了一次

  • 根据源码,+load 直接通过函数指针指向函数,拿到函数地址,找到函数代码,直接调用,它是分开来直接调用的,不是通过objc_msgsend调用的
  • 而 initialize是通过消息发送机制,isa找到类对象,然后找到方法调用的,所以只调用一次

源码阅读参考

  • load方法objc源码阅读过程:objc-os.mm
    • _objc_init
    • load_images
    • prepare_load_methods
      • schedule_class_load
        • add_class_to_loadable_list
      • add_category_to_loadable_list
    • call_load_methods
      • call_class_loads
      • call_category_loads
      • (*load_method)(cls, @selector(load));
  • initialize方法objc4源码阅读过程
    • ojbc-msg-arm64.s
      • objc_msgSend
    • objc-runtime-new.mm
      • class_getInstanceMethod
        • lookUpImpOrForward
          • realizeAndInitializeIfNeeded_locked
            • initializeAndLeaveLocked
              • initializeAndMaybeRelock
    • objc-initialize.mm
      • initializeNonMetaClass
        • callInitialize

Category能否添加成员变量?如果可以,如何给Category添加成员变量?

  • 不能直接给Category添加成员变量,但是可以间接添加。

  • Category是发生在运行时,编译完毕后类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构)

    • 使用一个全局的字典 (缺点: 每一个属性都需要一套相同的代码)
     // DLPerson+Test.h
     @interface DLPerson (Test)
     // 如果直接使用 @property 只会生成方法的声名 不会生成成员变量和set、get方法的实现。
     @property (nonatomic, assign) int weigjt;
     @end
     
     // DLPerson+Test.m
    #import "DLPerson+Test.h"
    @implemention DLPerson (Test)
    NSMutableDictionary weights_;
    + (void)load{
       weights_ = [NSMutableDictionary alloc]init];
    }
    - (void)setWeight:(int)weight{
       NSString *key = [NSString stringWithFormat:@"%p",self];
       weights_[key] = @(weight);
    }
    - (int)weight{
       NSString *key = [NSString stringWithFormat:@"%p",self];
       return [weights_[key] intValue] 
    }
    @end
    
    • 使用runtime机制给分类添加属性
    #import<objc/runtime.h>
    const void *DLNameKey = &DLNameKey
    // 添加关联对象
    void objc_setAssociatedObject(
    id object,          //  给哪一个对象添加关联对象
    const void * key,   //   指针(赋值取值的key)  &DLNameKey
    id value,           //  关联的值
    objc_AssociationPolicy policy //  关联策略 下方表格
    )
      
    objc_setAssociatedObject(self,@selector(name),name,OBJC_ASSOCIATION_COPY_NONATOMIC);
    // 获得关联对象
    id objc_getAssociatedObject(
    id object,           //  哪一个对象的关联对象
    const void * key     //   指针(赋值取值的key) 
    )
    
    objc_getAssociatedObject(self,@selector(name));
    // _cmd  == @selector(name); 
    objc_getAssociatedObject(self,_cmd);
    
    // 移除所有的关联对象
    void objc_removeAssociatedObjects(id object)
    
    • objc_AssociationPolicy(关联策略)
    objc_AssociationPolicy(关联策略)对应的修饰符
    OBJC_ASSOCIATION_ASSIGNassign
    OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
    OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
    OBJC_ASSOCIATION_RETAINstrong, atomic
    OBJC_ASSOCIATION_COPYcopy, atomic

如何给Category添加 weak 属性

  • runtime的objc_AssociationPolicy没有开放weak解决方案,但是object对象是可以有weak属性的

  • 实现思路:通过**存取一个strong对象(这个对象间接持有weak属性)**就可以。这样依然有一个问题:weak真正的对象被释放后,这个属性并不为nil。以下提供两个方案

    // 方案一
    // 定义一个包装类,将weak属性包装起来
    @interface WeakProWrapper : NSObject
    @property (nonatomic, weak) id proWeak;
    @end
    @implementation WeakProWrapper
    @end
    
    /*
    - (NSString *)myWeakPro {
        WeakProWrapper *wrapper = objc_getAssociatedObject(self, @selector(myWeakPro));
        return wrapper.proWeak;
    }
    
    - (void)setMyWeakPro:(NSString *)myWeakPro {
        WeakProWrapper *wrapper = nil;
        if (myWeakPro) {
            wrapper = [[WeakProWrapper alloc] init];
            wrapper.proWeak = myWeakPro;
        }
        objc_setAssociatedObject(self, @selector(myWeakPro), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    */
    
    // 方案二 使用block捕获机制。其实和方案一一个道理
    typedef id WeakId;
    typedef WeakId(^WeakReference)(void);
    
    WeakReference PackWeakReference(id ref) {
        __weak __typeof(WeakId) weakRef = ref;
        return ^{
            return weakRef;
        };
    }
    
    WeakId UnPackWeakReference(WeakReference closure) {
        return closure ? closure() : nil;
    }
    
    /*
    - (NSString *)myWeakPro {
        return UnPackWeakReference(objc_getAssociatedObject(self, @selector(myWeakPro)));
    }
    
    - (void)setMyWeakPro:(NSString *)myWeakPro {
        objc_setAssociatedObject(self, @selector(myWeakPro), PackWeakReference(myWeakPro), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    */
    

protocol 和 category 中如何使用属性

  • 在protocol中使用@property只会生成setter和getter方法声明,我们使用属性的目的是希望遵守我协议的对象能实现该属性

  • 在category中使用 @property也是只会生成setter和getter方法声明,如果我们真的需要给category增加属性的实现,需要借助于运行时的两个函数

    objc_setAssociatedObject
    objc_getAssociatedObject
    

Block

什么是Block

  1. 闭包在实现上是一个结构体
  2. 它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)
  3. 环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。

block的本质是什么

block的本质就是一个oc对象,内部也有isa指针, 是封装了函数及函数调用环境的OC对象

objc_block_1.png

Block是如何实现的?Block对应的数据结构是什么样子的

block本质是一个对象,底层用struct实现。 数据结构如下:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    // isa 指针,所有对象都有该指针,用于实现对象相关的功能
    void *isa;
    // 用于按 bit 位表示一些 block 的附加信息
    int flags;
    // 保留变量
    int reserved;
    // 函数指针,指向具体的 block 实现的函数调用地址
    void (*invoke)(void *, ...);
    // 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针
    struct Block_descriptor *descriptor;
  	// 捕获的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中
    /* Imported variables. */
};

既然block是一个OC对象,那么block的对象类型是什么?

block在iOS平台有三种类型,最终都继承自NSBlock,基类也是NSObject

block类型环境存放位置
NSConcreteGlobalBlock(MRC、ARC)没有访问auto变量静态区(data段)
NSConcreteStackBlock(MRC)访问了auto变量
NSConcreteMallocBlock(MRC、ARC)NSConcreteStackBlock进行了赋值或者调用了copy

每一种类型的block调用了copy之后结果如下所示

block的类型副本源的配置存储域复制后的区域
NSConcreteGlobalBlock程序的数据区域什么都不做
NSConcreteStackBlock从栈复制到堆
NSConcreteMallocBlock引用计数器+1

block捕获机制

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

oc方法都至少有self和_cmd参数,block捕获的self其实是这个局部变量

变量类型修饰符捕获到Block内部访问方式
局部变量(非对象)auto值传递
局部变量或对象static指针传递
全局变量直接访问

在什么情况下,编译器会根据情况自动将栈上的block复制到堆上?

ARC环境下,编译器根据情况自动将栈上的block复制到堆上

在ARC中对__NSStackBlock__调用copy变成__NSMallocBlock____NSMallocBlock__调用copy还是__NSMallocBlock__,引用计数+1,_NSGlobalBlock__调用copy啥都不做。

  • block作为函数的返回值并赋值给一个强引用

  • 将block赋值给__strong指针

  • block作为 Cocoa API方法中含有usingBlock的方法参数

    NSArray *arr = @[];
    // 遍历数组中包含  usingBlock方法的参数
    [arr enumerateObjectUsingBlock:^(id _Nonnullobj, NSUInteger idx, Bool _Nonnull stop){}];
    
  • block作为GCD API的方法参数

  • ARC环境下使用copy修饰属性

Block_copy底层原理

1、通过_Block_object_assign来对OC对象进行强引用或弱引用 2、通过_Block_object_dispose对OC进行清理

block声明的建议写法

  • block作为GCD属性的建议写法

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{});
    disPatch_after(disPatch_time(IDSPATCH_TIME_NOW, (int64_t)(delayInSecounds *NSEC_PER_SEC)), dispatch_get_main_queue(), ^{});
    
  • MRC下block属性建议写法

    • @property (copy, nonatomic) void (^block)(void);
  • ARC下block属性建议写法

    • @property (strong, nonatomic) void (^block)(void);
    • @property (copy, nonatomic) void (^block)(void);

__block变量

Block内部为什么不能修改局部变量,需要加__block?

  • 通过查看Block 源码,可以发现

    • block 内部如果单纯使用外部变量,会在 block 内部创建一个同样的变量
    • 并且将外部变量值拷贝到 block 内部,此时这个内部变量和外部实际已经没关系了
  • 加了__block 以后,block会将外部变量的内存拷贝到堆中,内存由 block 去管理

在block内部使用NSMutableArray需不需要添加__block?

不需要,可以理解为block捕获了指向对象的变量的不可变副本,这个副本不可被赋值,只可操作它指向的对象。

NSMutableArray *array = [[NSMutableView alloc]init]
void (^block)(void) = ^{
  array = nil;   //  这样操作是需要__block的。  // 
  // 下面这个是不需要 __block修饰的,因为这个只是使用它的指针而不是修改它的值
	[array addObject:@"aa"];
	[array addObject:@"aa"];
}

__block作用

  • __block只能修饰非静态局部变量,不能修饰静态变量和全局变量,否则编译器报错
  • 如果需要在 block 内部修改外部的 局部变量的值,就需要使用__block 修饰。否则,编译不过

底层实现

__block 修饰以后,局部变量的数据结构就会发生改变,底层会变成一个结构体的对象,其中通过__forwarding指向自己,来访问真实的变量

  • 结构内部会声明一个和 __block修饰的变量同名的成员变量,并且将 __block修饰的变量的地址保存到堆内存中
  • 后面如果修改这个变量的值,可以通过 isa 指针找到这个结构体,进而修改这个变量的值;

objc_block_block.png

  • 使用注意点: 在MRC环境下不会对指向的对象产生强引用的

为什么要通过__forwarding访问?

因为如果__block修饰的变量在栈上,是可以直接访问的,但是如果已经拷贝到了堆上,访问的时候,还去访问栈上的,就会出问题,所以,先根据__forwarding找到堆上的地址,然后再取值

Block的forwarding指针

objc_block_forwarding.png

为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?

__weak __typeof(self) weakSelf  = self;
self.block = ^{
    __strong __typeof(self) strongSelf = weakSelf; 
    [strongSelf doSomeThing];
    [strongSelf doOtherThing];
};

1、为什么使用weakSelf
   因为block截获self之后,self属于block结构体中的一个由__strong修饰的属性,会强引用self。
   需要使用__weak修饰的weakSelf防止循环引用

2、为什么在block里面需要使用strongSelf
	block使用__strong修饰的weakSelf是为了在block生命周期中self不会提前释放。
  但存在执行前self就已经被释放的情况,导致strongSelf=nil,注意判空处理

3、为什么在block外边使用了__weak修饰self,里面使用__strong修饰weakSelf的时候不会发生循环引用
   strongSelf实质是一个局部变量(在block这个"函数"里面的局部变量)
   当block执行完毕就会释放自动变量strongSelf,不会对self一直进行强引用

总结:
1. 用__weak修饰之后,block不会对该对象进行retain,只是持有了weak指针,在block执行之前或执行的过程时,随时都有可能被释放并将weak指针置为nil,产生一些未知的错误。
2. 在内部用__strong修饰,会在block执行时,对该对象进行一次retain,保证在执行时若该指针不指向nil,则在执行过程中不会指向nil。但有可能在执行执行之前已经为nil了

__weak的作用是什么?

  • __weak 是一个修饰符,用于修饰对象引用计数不会加1,且在对象销毁时,该引用自动置为nil

  • iOS中总共有__strong,__weak,__unsafe_unretained,__autoreleasing四种修饰符

block内一定要使用weakSelf来解决循环引用?

  • 不一定。self不直接或间接的持有block是不会存在循环引用的,所以也不需要使用weakSelf
  • 比如系统的dispatch里面的block,self并没有持有它,所以不会造成循环引用。
  • NSArray的enumerateObjectsUsingBlock是同步block并且Array实例没有持有这个block,所以也不会造成循环引用

对象类型的auto变量、__block变量

使用clang rewrite-objc时,遇到__weak,需要加上 --fobjc-arc -fobjc-runtime=ios-9.0

  • 当block在栈上时,对它们都不会产生强引用

  • 当block拷贝到堆上时,都会通过copy函数来处理它们

objc_block_copy.png

*   会调用block内部的copy函数
*   copy函数内部会调用`_Block_object_assign`函数
*   `_Block_object_assign`函数会根据auto变量的修饰符(`__strong、__weak、__unsafe_unretained`)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里**仅限于ARC时会retain,MRC时不会retain**)
*   `_Block_object_assign`函数会对\_\_block变量形成强引用(retain)
    *   \_\_block变量(假设变量名叫做a)
        *   \_Block\_object\_assign((void\*)\&dst->a, (void\*)src->a, 8);
    *   对象类型的auto变量(假设变量名叫做p)
        *   \_Block\_object\_assign((void\*)\&dst->p, (void\*)src->p, 3);
  • 当block从堆上移除时,都会通过dispose函数来释放它们

objc_block_copy2.png

*   会调用block内部的dispose函数
*   dispose函数内部会调用`_Block_object_dispose`函数
*   `_Block_object_dispose`函数会自动释放引用的\_\_block/auto变量(release)
    *   \_\_block变量(假设变量名叫做a)
        *   \_Block\_object\_dispose((void\*)src->a, 8);
    *   对象类型的auto变量(假设变量名叫做p)
        *   \_Block\_object\_dispose((void\*)src->p, 3);
  • 其他细节

    • 访问的是对象类型,block的desc里面就会增加copy和dispose函数
    • 多层嵌套block,以捕获的强引用的最长生命周期为准
函数调用时机
copy函数栈上的Block复制到堆上时
dispose函数堆上的Block被废弃时

block类型的属性的修饰词是什么?为什么?使用block有哪些注意点?

  • 修饰词是copy。MRC下block 如果没有进行copy操作就不会在堆上,在堆上才能控制它的生命周期

  • 注意循环引用的问题

  • 在ARC环境下 使用strong和copy都可以,没有区别,在MRC环境下有区别,需要使用copy

  • block是一个对象,所以block理论上是可以retain/release的。

    block在创建的时候内存是默认分配在栈(stack)上,而不是堆(heap)上。 所以它的作用域仅限创建时候的当前上下文(函数, 方法...),,当你在该作用域外调用该block时,程序就会崩溃。

函数指针和 Block区别

  • 相同点:

    • 二者都可以看成是一个代码片段

    • 函数指针类型和 Block 类型都可以作为变量和函数参数的类型

  • 不同点:

    • 函数指针只能指向预先定义好的函数代码块,函数地址是在编译链接时就已经确定好的

      从内存的角度看,函数指针只不过是指向代码区的一段可执行代码

    • block 本质是 OC对象,是 NSObject的子类,是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期

GCD中的Block是在堆上还是栈上?

堆上。可以通过block的isa指针确认

看代码解释原因

int main(int argc, const char *argv[]){
    @autoreleasepool{
      int age = 10;
      void  (^block)(void) = ^{
          NSLog(@" age is %d ",age);
      };
      age = 20;
      block();
    }
  }
  /*
  输出结果为? 为什么?
  输出结果是: 10
  如果没有修饰符,默认是auto
  为了能访问外部的变量,block有一个变量捕获的机制 
  因为他是局部变量 并且没有用static修饰 
  所以age被捕获到block中是一个值,外部再次改变时 block中的age不会改变。
  */
int main(int argc, const char *argv[]){
  @autoreleasepool{
    int age = 10;
    static int height = 10;
    void  (^block)(void) = ^{
        NSLog(@" age is %d, height is %d",age, height);
    };
    age = 20;
    height = 20;
    block();
  }
}
/*
输出结果为? 为什么?
age is 10, height is 20
局部变量用static修饰之后,捕获到block中的是height的指针,
因此修改通过指针修改变量之后 外部的变量也被修改了
*/
int age = 10;
static int height = 10;
int main(int argc, const char *argv[]){
  @autoreleasepool{
    void  (^block)(void) = ^{
        NSLog(@" age is %d, height is %d",age, height);
    };
    age = 20;
    height = 20;
    block();
  }
}
/*
输出结果为? 为什么?
 age is 20, height is 20
 因为 age 和 height是全局变量不需要捕获直接就可以修改
 全局变量 可以直接访问,
 局部变量 需要跨函数访问,所以需要捕获
 因此修改通过指针修改变量之后 外部的变量也被修改了
*/

Runtime

什么是runtime? 平时项目中有用过吗?

Objective-C runtime是一个运行时库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码。

类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用

具体应用

  • 关联对象,给分类添加属性,set和get的实现
  • 遍历类的成员变量,归档解档、字典转模型
  • 交换方法(系统的交换方法)
  • 利用消息转发机制解决方法找不到的异常问题

isa位域详解

  • 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址

  • 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息

    • nonpointer

      • 0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
      • 1,代表优化过,使用位域存储更多的信息
    • has_assoc

      • 是否有设置过关联对象,如果没有,释放时会更快
    • has_cxx_dtor

      • 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
    • shiftcls

      • 存储着Class、Meta-Class对象的内存地址信息
    • magic

      • 用于在调试时分辨对象是否未完成初始化
    • weakly_referenced

      • 是否有被弱引用指向过,如果没有,释放时会更快
    • deallocating

      • 对象是否正在释放
    • extra_rc

      • 里面存储的值是引用计数器减1
    • has_sidetable_rc

      • 引用计数器是否过大无法存储在isa中
      • 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

实例对象的数据结构

具体可以参看 Runtime 源代码,在文件 objc-private.h中有定义

struct objc_object {
private:
    isa_t isa;
  //...
}

本质上 objc_object 的私有属性只有一个 isa 指针。指向 类对象 的内存地址

类对象的数据结构

objc_object_structure.png

struct objc_class : objc_object {
    // Class ISA;
    Class superclass; //父类指针
	  // 用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度
   	/*
		struct bucket_t {
			cache_key_t _key; // SEL作为key
			IMP _imp;//函数的内存地址
		}   	
   	
    struct cache_t {
    	struct bucket_t *_buckets; // 散列表
    	mask_t_ mask; // 散列表的长度 - 1
    	mas_t _occupied;  // 已经缓存的方法数量
    }
   	*/
    cache_t cache;             // formerly cache pointer and vtable 方法缓存
  	
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 用于获取地址

    class_rw_t *data() { 
        return bits.data(); // &FAST_DATA_MASK 获取地址值
    }

它的结构相对丰富一些。继承自objc_object结构体,所以包含isa指针

  • isa:指向元类
  • superClass: 指向父类
  • Cache: 方法的缓存列表
  • data: 顾名思义,就是数据。是一个被封装好的 class_rw_t

objc_class结构

objc_class_jiegou.png

说一下对 class_rw_t 的理解

rw代表可读可写。

class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容

objc_class_rw.png

// 可读可写
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro; // 指向只读的结构体,存放类初始信息

    /*
     这三个都是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
     methods中,存储 method_list_t ----> method_t
     二维数组,method_list_t --> method_t
     这三个二维数组中的数据有一部分是从class_ro_t中合并过来的。
     */
    method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
    property_array_t properties; // 属性列表
    protocol_array_t protocols; //协议列表

    Class firstSubclass;
    Class nextSiblingClass;
  
    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
    //...
}

说一下对 class_ro_t 的理解

存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容

objc_class_ro.png

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
	  //...
}

class_ro_t和class_rw_t的区别

  1. class_rw_t提供了运行时对类拓展的能力,而class_ro_t存储的大多是类在编译时就已经确定的信息。
  2. 二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。
    1. class_rw_t存储列表使用的二维数组,class_ro_t使用的一维数组。
    2. class_ro_t存储于class_rw_t结构体中,是不可改变的。保存着类的在编译时就已经确定的信息。而运行时修改类的方法,属性,协议等都存储于class_rw_t中

method_t

method_t 是对方法的封装

struct method_t {
	SEL name; // 函数名
	const char *types; 编码(返回值类型,参数类型)
	IMP imp; // 指向函数的指针(函数地址)
}
  • IMP代表函数的具体实现

    typedef id _Nullable (*IMP)(id _Nonull, SEL _Nonull, ...);

  • SEL代表方法名,一般叫做选择器,底层结构跟char *类似

    typedef struct objc_selector *SEL;

    • 可以通过@selector()和sel_registerName()获得
  • 可以通过sel_getName()和NSStringFromSelector()转成字符串

    • 不同类中相同名字的方法,所对应的方法选择器是相同的
  • types包含了函数返回值、参数编码的字符串

objc_method_typecoding.png

方法缓存

Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度

struct bucket_t {
  cache_key_t _key; // SEL作为key
  IMP _imp;//函数的内存地址
}   	
		
struct cache_t {
	struct bucket_t *_buckets; // 散列表
	mask_t_ mask; // 散列表的长度 - 1
	mas_t _occupied;  // 已经缓存的方法数量
}

说一下 Runtime 的方法缓存?存储的形式、数据结构以及查找的过程

  1. cache_t 用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用。

  2. 哈希表内部存储的是 bucket_tbucket_t 中存储的是 SELIMP的键值对

    1. 如果是有序方法列表,采用二分查找
    2. 如果是无序方法列表,直接遍历查找
// 缓存曾经调用过的方法,提高查找速率
struct cache_t {
    struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
    mask_t _mask;//分配用来缓存bucket的总数
    mask_t _occupied;//表明目前实际占用的缓存bucket的个数
    //...
}

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    uintptr_t _imp;// 函数的内存地址
    SEL _sel;//SEL作为Key @selector()
#else
    SEL _sel;
    uintptr_t _imp;
#endif
  //...
}

散列表查找过程,在objc-cache.mm文件中

// 查询散列表,k
bucket_t * cache_t::find(SEL s, id receiver) 
{
    assert(k != 0); // 断言

    bucket_t *b = buckets(); // 获取散列表
    mask_t m = mask(); // 散列表长度 - 1
    mask_t begin = cache_hash(s, m); // & 操作
    mask_t i = begin; // 索引值
    do {
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
    // i 的值最大等于mask,最小等于0。

	 // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}

上面是查询散列表函数,其中cache_hash(k, m)是静态内联方法,将传入的sel和mask进行&操作返回uint32_t索引值。do-while循环查找过程,当发生冲突cache_next`方法将索引值减1

objc_msgSend源码阅读顺序

  • objc-msg-arm64.s

    • ENTRY _objc_msgSend
    • b.le LNilOrTagged
    • CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    • .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    • STATIC_ENTRY __objc_msgSend_uncached
    • .macro MethodTableLookup
    • _lookUpImpOrForward
  • objc-runtime-new.mm

    • lookUpImpOrForward
    • getMethodNoSuper_nolock、search_method_list_inline、log_and_fill_cache
    • cache_getImp、log_and_fill_cache
    • resolveMethod_locked、resolveInstanceMethod、resolveClassMethod
    • _objc_msgForward_impcache
  • objc-msg-arm64.s

    • STATIC_ENTRY __objc_msgForward_impcache
    • ENTRY __objc_msgForward
  • Core Foundation

    • __forwarding__(不开源)

消息发送机制流程

OC的消息机制

  • OC中的方法调用最后都是转为objc_msgSend函数调用,给receiver(方法调用者)发送了一条消息(selector方法名)
  • objc_msgSend底层有三大模块,消息发送(当前类、父类中查找)、动态方法解析、消息转发

runtime如何通过selector找到对应的IMP地址

  • 每一个类对象中都有一个对象方法列表(对象方法缓存)
  • 类方法列表是存放在类对象isa指针指向的元类对象中(类方法缓存)
  • 方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现
  • 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找
  • 当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找

消息发送阶段

objc_msg_send_0.png

  • 给当前类发送一条消息,判断消息是否要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数
  • 判断对象是否为nil,若为nil直接退出消息发送,返回对应类型的默认值
  • 从当前类的缓存中查找
    • 缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找
  • 如果没有,去遍历 class_rw_t 方法列表查找
    • 当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历
  • 如果没有,再去父类的缓存查找
    • 父类逐级查找:先判断父类是否为nil,为nil则结束,
    • 否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程
  • 如果没有再去父类的class_rw_t方法列表中查找
  • 循环反复,如果找到,调用方法, 并且将方法缓存到方法调用者的方法缓存
  • 如果一直没有,转到下一个阶段:动态解析阶段

动态解析阶段

objc_msg_send_1.png

  • 动态解析会调用-resolveInstanceMethod 或者 +resolveClassMethod 方法,在方法中手动添加class_addMethod方法的调用。
  • 只会解析一次,会将是否解析过的参数置位YES,然后重新走消息发送阶段
    • 如果我们实现了方法的添加,则在消息发送阶段可以找到这个方法。调用方法并将方法缓存到方法调用者的缓存中
    • 如果没有实现, 再第二次走到动态解析阶段,不会进入动态解析,因为上一次已经解析过并返回了YES。所以会走到下一个阶段:消息转发阶段

消息转发阶段

objc_msg_send_2.png

快转发

实现了forwardingTargetForSelector方法

  • 调用forwardingTargetForSelector 方法(返回一个类对象), 直接使用我们设置的类去发送消息。

慢转发

没有实现forwardingTargetForSelector

  • 会去调用 methodSignatureForSelector 方法,在这个方法添加方法签名
  • 之后会调用forwardInvocation 方法, 在这个方法中我们调用 [anInvocation invokeWithTarget:类对象];
  • 或者其他操作都可以,这里没有什么限制

objc_method_forward_map.png

使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么

无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在 NSObject -dealloc 调用的object_dispose()方法中释放

/*
1、调用 -release :引用计数变为零
	对象正在被销毁,生命周期即将结束. 
	不能再有新的 __weak 弱引用,否则将指向 nil.
	调用 [self dealloc]

2、父类调用 -dealloc 
	继承关系中最直接继承的父类再调用 -dealloc 
	如果是 MRC 代码 则会手动释放实例变量们(iVars)
	继承关系中每一层的父类 都再调用 -dealloc

3、NSObject 调 -dealloc 
	只做一件事:调用 Objective-C runtime 中object_dispose() 方法

4. 调用 object_dispose()内部调用 objc_destructInstance
	为 C++ 的实例变量们(iVars)调用 object_cxxDestruct
	解除所有使用 runtime Associate方法关联的对象  _object_remove_assocations
	解除所有 __weak 引用和引用计数 clearDeallocating,clearDeallocating_slow,weak_clear_no_lock,refcnts.erase
	调用 free()
*/

什么是method swizzling(俗称黑魔法)

Method Swizzle 指的是改变一个已存在的选择器对应的实现的过程

  1. 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。
  2. 利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
  3. 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。

几种实现方式

  • 利用 method_exchangeImplementations 交换两个方法的实现

  • 利用 class_replaceMethod替换方法的实现

  • 利用 method_setImplementation 来直接设置某个方法的IMP

objc_method_swizzling.png

使用method swizzling要注意什么?

  • 方式无限循环

  • 进行版本迭代的时候需要进行一些检验,防止系统库的函数发生了变化

一个系统方法被多次交换,会有什么影响吗?最后的调用顺序是怎样的?

/*
没有影响,都会执行并且后交换的会先调用.
                         
第一次交换   viewwillAppAppear 和 test1 的指向的方法实现地址发生变化
第二次交换   viewwillAppAppear 和 test2 实际上等于是 test2 和 test1 进行了交换,因为 viewwillAppAppear 已经变为了 test1了.

调用 --> viewwillAppAppear
实际调用顺序 -->test2--->test1-->viewwillAppAppear
形成一个闭环:viewwillAppAppear 也只会调用一次
*/

什么时候会报unrecognized selector的异常

  • 当调用该对象上某个方法,而该对象上没有实现这个方法的时候,可以通过“消息转发”进行解决,如果还是不行就会报unrecognized selector异常
  • objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:
    • objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
    • 然后在该类中的方法列表以及其父类方法列表中寻找方法运行
    • 如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会
  • 三次拯救程序崩溃的机会
    • Method resolution
      • objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。
      • 如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程
      • 如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发
    • Fast forwarding
      • 如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会
      • 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。
      • 否则,就会继续Normal Fowarding。
      • 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但Normal forwarding转发会创建一个NSInvocation对象,相对Normal forwarding转发更快点,所以这里叫Fast forwarding
    • Normal forwarding
      • 这一步是Runtime最后一次给你挽救的机会。
      • 首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。
      • 如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。
      • 如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象

关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?

关联对象有什么应用?

一般用于在分类中给类添加实例变量

系统如何管理关联对象?

objc_association.png

  1. 首先系统中有一个全局AssociationsManager,里面有个AssociationsHashMap哈希表。哈希表中的key是对象的内存地址,value是ObjectAssociationMap
  2. ObjectAssociationMap也是一个哈希表,其中key是我们设置关联对象所设置的key,value是ObjcAssociation。里面存放着关联对象设置的值和内存管理的策略。
  3. void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy)为例
    1. 首先会通过AssociationsManager获取AssociationsHashMap
    2. object的内存地址为key,从AssociationsHashMap中取出ObjectAssociationMap,若没有,则新创建一个
    3. 通过key获取旧值以及通过key和policy生成新值ObjcAssociation(policy, new_value),把新值存放到ObjectAssociationMap
    4. 若新值不为nil,并且内存管理策略为retain,则会对新值进行一次retain
    5. 若新值为nil,则会删除旧值,若旧值不为空并且内存管理的策略是retain,则对旧值进行一次release

其被释放的时候需要手动将所有的关联对象的指针置空么?

不需要,因为在对象的dealloc中,若发现对象有关联对象时,会调用_object_remove_assocations方法来移除所有的关联对象,并根据内存策略来判断是否需要对关联对象的值进行release

如果向一个nil对象发消息不会crash的话,那么message sent to deallocated instance的错误是怎么回事?

  1. 这是因为这个对象已经被释放了(引用计数为0了),那么这个时候再去调用方法肯定是会Crash的,因为此时这个对象是一个野指针(指向僵尸对象(对象的引用计数为0,指针指向的内存已经不可用)的指针)
  2. 安全的做法是释放后将对象重新置为nil,使它成为一个空指针

runtime如何实现weak变量的自动置nil?知道SideTable吗

weak指针的实现原理

Runtime维护了一个全局的weak表,用于存储指向某个对象的所有weak指针,weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组

知道SideTable吗

SideTable结构体是负责管理类的引用计数表和weak表

struct SideTable {
    // 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

如何访问SideTables

  1. 首先会取得对象的地址,将地址进行哈希运算,与 SideTables 中 SideTable 的个数取余,最后得到的结果就是该对象所要访问的 SideTable
  2. 在取得的 SideTable 中的 RefcountMap 表中再进行一次哈希查找,找到该对象在引用计数表中对应的位置
  3. 如果该位置存在对应的引用计数,则对其进行操作。如果没有对应的引用计数,则创建一个对应的 size_t 对象,其实就是一个 uint 类型的无符号整型
  4. 弱引用表也是一张哈希表的结构,其内部包含了每个对象对应的弱引用表 weak_entry_t,而 weak_entry_t 是一个结构体数组,其中包含的则是每一个对象弱引用的对象所对应的弱引用指针。

Swift的SideTable怎么实现的?

zhangferry.com/2022/03/03/…

了解StripeMap吗

zhangferry.com/2022/03/31/…

weak 具体过程

初始化时

  1. 当我们初始化一个weak变量时,runtime会调用 NSObject.mm 中的objc_initWeak函数,初始化"附有weak修饰符的变量(obj1)"
  2. 初始化一个新的weak指针指向对象的地址
  3. 在变量作用域结束时通过objc_destoryWeak函数释放该变量obj1
{
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}

// 通过objc_initWeak函数初始化 “附有weak修饰符的变量(obj1)”
// 在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)

// 编译器的模拟代码
 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用计数变为0,变量作用域结束*/
 objc_destroyWeak(&obj1);

更新引用时

  1. objc_initWeak函数会调用 storeWeak 函数, storeWeak 的作用是更新指针指向,创建对应的弱引用表

  2. objc_storeWeak函数以第二个参数的内存地址作为键值,将第一个参数的内存地址注册到 weak 表中。如果第二个参数为0(nil),那么把第一个参数的地址从weak表中删除。

  3. 由于一个对象可同时赋值给多个附有__weak修饰符的变量,所以对于一个键值,可注册多个变量的地址。

obj1 = 0;
obj_storeWeak(&obj1, obj);
/* ... obj的引用计数变为0,被置nil ... */
objc_storeWeak(&obj1, 0);

// objc_initWeak函数将 “附有weak修饰符的变量(obj1)” 初始化为0(nil)后
// 会将 “赋值对象”(obj)作为参数,调用objc_storeWeak函数。

释放时

  1. 当对象释放时,会调用objc_rootDealloc->rootDealloc->object_dispose->objc_destructInstance->clearDeallocating->clearDeallocating_slow->weak_clear_no_lock函数。

  2. weak_clear_no_lock函数

    1. 根据对象地址获取所有weak指针地址的数组
    2. 遍历这个数组把其中的数据设为nil
    3. 把这个entry从weak表中删除
    4. 从引用计数表中删除以对象的地址为键值的记录

引用计数表

  1. 少量的引用计数是不会直接存放在 SideTables 表中的,对象的引用计数会先存放在 extra_rc 中
  2. 当其被存满时,才会存入相应的 SideTables 散列表中

objc_ref_0.png

_objc_msgForward 函数是做什么的?直接调用它将会发生什么?

  • _objc_msgForward是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发
  • 直接调用_objc_msgForward 是非常危险的事,这是把双刃刀,如果用不好会直接导致程序 Crash,但是如果用得好,能做很多非常酷的事
  • JSPatch 就是直接调用_objc_msgForward 来实现其核心功能的

Runtime 常用API

// 动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
// 注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls) 
// 销毁一个类
void objc_disposeClassPair(Class cls)
// 获取isa指向的Class
Class object_getClass(id obj)
// 设置isa指向的Class
Class object_setClass(id obj, Class cls)
// 判断一个OC对象是否为Class
// BOOL object_isClass(id obj)
// 判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
// 获取父类
Class class_getSuperclass(Class cls)

成员变量

// 获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
// 拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
// 设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
// id object_getIvar(id obj, Ivar ivar)
// 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
// 获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

属性

// 获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
// 拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
// 动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
// 动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
// 获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

方法

// 获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
// 方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name) 
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2) 
// 拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
// 动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
// 动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
// 选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
// 用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

LLVM简述

  • Objective-C在变为机器代码之前,会被LLVM编译器转换为中间代码(Intermediate Representation)
  • 可以使用以下命令行指令生成中间代码
    • clang -emit-llvm -S main.m
  • 语法简介
    • @ - 全局变量
    • % - 局部变量
    • alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存
    • i32 - 32位4字节的整数
    • align - 对齐
    • load - 读出,store 写入
    • icmp - 两个整数值比较,返回布尔值
    • br - 选择分支,根据条件来转向label,不根据条件跳转的话类似 goto
    • label - 代码标签
    • call - 调用函数
  • 具体可以参考官方文档:llvm.org/docs/LangRe…

lldb(gdb)常用的调试命令?

  • po:打印对象,会调用对象description方法。是print-object的简写
  • expr:可以在调试时动态执行指定表达式,并将结果打印出来,很有用的命令
  • print:也是打印命令,需要指定类型
  • bt:打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈
  • br l:是breakpoint list的简写

Runloop

RunLoop概念

RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。

  1. 设计RunLoop的目的是让线程有事件的时候处理事件,没事件的时候处于休眠。
  2. 没有消息处理时休眠,以避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
  3. 有消息需要处理时,立刻被唤醒,由内核态切换到用户态

RunLoop原理

苹果官方原理图,RunLoop运行在线程中,接收Input Source 和 Timer Source并且进行处理。 image

Input Source 和 Timer Source

两个都是 Runloop 事件的来源。Input Source 可以分为三类

InputSource

  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef
  • Custom Input Sources,用户手动创建的 Source
  • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源

TimerSource

  • Timer Source指定时器事件,该事件的优先级是最低的

区别

  • 按照上面的图,事件处理优先级是Port > Custom > performSelector > Timer
  • Input Source 异步投递事件到线程中,Timer Source同步投递事件到线程中

获取RunLoop

  1. RunLoop是由线程创建的,我们只能获取。通过CFRunLoopGetCurrent获取当前线程的RunLoop
  2. 子线程的RunLoop在子线程中第一次调用CFRunLoopGetCurrent创建
  3. 主线程的RunLoop在整个App第一次调用CFRunLoopGetCurrent创建
  4. 由UIApplication 的run方法调用

RunLoop类有哪些

  • iOS中有2套API来访问和使用RunLoop

    • Foundation:NSRunLoop(不是线程安全的)
    • Core Foundation:CFRunLoopRef(线程安全)
  • NSRunLoop和CFRunLoopRef都代表着RunLoop对象

    • NSRunLoop是基于CFRunLoopRef的一层OC包装
    • CFRunLoopRef是开源的

讲讲Runloop在项目中的应用

  • runloop 运行循环,保证程序一直运行,主线程默认开启

  • 用于处理线程上的各种事件,定时器等

  • 可以提高程序性能,节约CPU资源,有事情做就做,没事情做就让线程休眠

  • 应用范畴: 定时器(Timer),PerformSelector,GCD Async Main Quue,事件响应,手势识别,界面刷新以及

    网络请求、autoreleasePool 等等

Runloop内部实现逻辑

对于RunLoop而言,最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。

  1. RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。
  2. 在用户态调用 mach_msg_trap()时会切换到内核态;内核态中实现的mach_msg()函数会完成实际的工作。

Mach消息发送机制 大致逻辑为:

  1. 通知Observers:RunLoop 即将启动。

  2. 通知Observers:即将要处理Timers事件。

  3. 通知Observers:即将要处理source0事件。

  4. 处理source0事件。

  5. 如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。

  6. 通知观察者线程即将进入休眠状态。

  7. 将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

    • 一个基于 port 的Source1 的事件(图里应该是source0)。
    • 一个 Timer 到时间了。
    • RunLoop 自身的超时时间到了。
    • 被其他调用者手动唤醒。
  8. 通知观察者线程将被唤醒。

  9. 处理唤醒时收到的事件。

    • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
    • 如果输入源启动,传递相应的消息。
    • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
  10. 通知Observers: 退出Loop

objc_runloop.png

Runloop和线程的关系

  • 每条线程都有唯一的一个与之对应的RunLoop对象

  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value

  • 线程刚创建时并没有RunLoop对象,RunLoop会 在第一次获取它时创建

  • RunLoop会在线程结束时销毁

  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

怎么创建一个常驻线程

  1. 为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop)
  2. 向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)
  3. 启动该RunLoop
@autoreleasepool {
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
}

怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作

  1. 当我们在子线程请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。
  2. 可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI
[self performSelectorOnMainThread:@selector(reloadData) 
											 withObject:nil
									  waitUntilDone:NO
													  modes:@[NSDefaultRunLoopMode]];

Mode 与 Runloop 的关系

  • CFRunLoopModeRef代表着RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode作为currentMode
  • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
    • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
  • OS 中公开暴露出来的只有 NSDefaultRunLoopModeNSRunLoopCommonModes
    • NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode
  • 视图滑动会切换到 UITrackingRunLoopMode。如果需要在多种 mode 下运行则需要手动设置为kCFRunLoopCommonModes;
    • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
    • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    • kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultModeUITrackingRunLoopMode用,并不是一种真正的Mode

CommonMode

不是一种真正的运行模式 是同步Source/Timer/Observer到多个Mode中的一种解决方案

  • 一个Mode可以将自己标记为'Common'属性(通过将其ModeName添加到RunLoop的'commonModes'中)。
  • 每当RunLoop的内容发生变化时,RunLoop 都会自动将_commonModelItems里的Source/Observer/Timer同步到具有'Common'标记的所有Mode里。
  • 即被标记了'Common' 的Mode可以同步不同Mode下的Source/Observer/Timer。
  • NSRunLoopCommonModes 中就包含了两种被标记为'Common'的Mode 即KCFRunLoopDefaultMode和UITrackingRunLoopMode。

Source有两种类型

Source0 (非基于port)

  • 只包含了一个回调,需要手动触发。
  • 使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,
  • 然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1 (基于port)

  • 可以主动触发。
  • 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。

解释一下 NSTimer

  1. NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。
  2. 一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。
  3. RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer
  4. Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
  5. 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。

程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?

  • 滑动scrollView时,主线程的RunLoop会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer

  • 将 timer 对象添加到 runloop 中,并修改 runloop 的运行 mode

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:nil];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

在子线程中怎么开启和关闭runloop

  • 想要子线程的runloop开启,mode里必须有timer/source中的至少一个

  • 子线程开启runloop:

    // runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法
    - (void)run;
    // 可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法
    - (void)runUntilDate:(NSDate *)limitDate;
    // runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出
    - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
    
  • 子线程关闭runloop:

    // 如果不想退出runloop可以使用第一种方式启动runloop;
    // 使用第二种方式启动runloop,可以通过设置超时时间来退出;
    // 使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出
    

AutoreleasePoolRunLoop 有什么联系?

  1. 应用程序刚刚启动时默认注册了很多个Observer,其中有两个 Observer 管理和维护 AutoreleasePool

  2. 这两个Observer的回调都是 _ wrapRunLoopWithAutoreleasePoolHandler

    • 第一个 Observer 会监听 RunLoop 的进入

      • 它会回调objc_autoreleasePoolPush向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。
      • 这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。
    • 第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 两种状态

      • 在即将进入休眠时会调用 objc_autoreleasePoolPop 和 objc_autoreleasePoolPush

        根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。

      • 在即将退出 RunLoop 时会调用objc_autoreleasePoolPop释放自动释放池内对象。

      • 这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。

Runloop 主线程监听卡顿

  • 用户层面感知的卡顿都是来自处理所有UI的主线程上,包括在主线程上进行的大量计算,大量的IO操作,或者比较重的绘制工作。

  • 如何监控主线程呢

    • 首先需要知道的是主线程和其它线程一样都是靠NSRunLoop来驱动的。
    • NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后。
    • 也就是:如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿
    • 只需要另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手
  • 具体操作

    • 用GCD里的dispatch_semaphore_t开启一个新线程,设置一个极限值和出现次数的值
    • 然后获取主线程上在kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting两个状态之间的超过了极限值和出现次数的场景
    • 将堆栈dump下来发到服务器做收集
    • 通过堆栈能够找到对应出问题的那个方法

    如何实现高可用的卡顿监控

performSelector:withObject:afterDelay: 的实现原理

  1. 当调用 NSObject 的 performSelector:withObject:afterDelay: 后,其内部会创建一个 Timer 并添加到当前线程的 RunLoop 的DefaultMode中。如果当前线程没有 RunLoop,则这个方法会失效。
  2. 当调用 performSelector:onThread:withObject:waitUntilDone:时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效

RunLoop的数据结构

NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,提供了面向对象的API

使用Runloop是为了让线程保持激活状态

CFRunLoop 对象

pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成

CFRunLoopMode 运行模式

name、source0、source1、observers、timers构成

CFRunLoopSource 输入源/事件源

分为source0和source1两种

source0

即非基于port的,也就是用户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到用户态

source1:具备唤醒线程的能力

基于port的,包含一个 mach_port 和一个回调,可监听系统端口和通过内核或其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。

CFRunLoopTimer 定时源

  1. 基于时间的触发器,基本上说的就是NSTimer。
  2. 在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的

CFRunLoopObserver 观察者

监听以下事件: CFRunLoopActivity

  • kCFRunLoopEntry:RunLoop准备启动
  • kCFRunLoopBeforeTimers:RunLoop将要处理一些Timer相关事件
  • kCFRunLoopBeforeSources:RunLoop将要处理一些Source事件
  • kCFRunLoopBeforeWaiting:RunLoop将要进行休眠状态,即将由用户态切换到内核态
  • kCFRunLoopAfterWaiting:RunLoop被唤醒,即从内核态切换到用户态后
  • kCFRunLoopExit:RunLoop退出
  • kCFRunLoopAllActivities:监听所有状态

各数据结构之间的联系

线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

看代码解释原因

- (void)test{
    NSLog(@"2");
}

- (void)touchesBegan03{
//    NSThread *thread = [[NSThread alloc]initWithBlock:^{
//        NSLog(@"1");
//    }];
//    [thread start];
//    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
//    运行后会崩溃  因为子线程 performSelector方法 没有开启runloop, 当执行test的时候这个线程已经没有了。
    
    NSThread *thread = [[NSThread alloc]initWithBlock:^{
        NSLog(@"1");
        [[NSRunLoop  currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
    // 添加开启runloop后,在线程中有runloop存在线程就不会死掉,之后调用performSelect就没有问题了
}

- (void)touchesBegan02{
    // 创建全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");
        //[self performSelector:@selector(test) withObject:nil];//  打印结果  1  2  3   等价于[self test]
        // 这句代码点进去发现是在Runloop中的方法
        // 本质就是向Runloop中添加了一个定时器。  子线程默认是没有启动 Runloop的
        [self performSelector:@selector(test) withObject:nil afterDelay:.0]; //  打印结果  1  3 2
        NSLog(@"3");
        // 启动runloop,若没有下面的代码就只会输出1 3
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    });
}

AutoreleasePool

Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

Autoreleasepool所使用的数据结构是什么?

  • 自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
  • 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
  • 源码分析
    • clang重写@autoreleasepool
    • objc4源码:NSObject.mm
      • objc_autoreleasePoolPush
        • AutoreleasePoolPage::push
          • autoreleaseNewPage
          • autoreleaseFast
      • objc_autoreleasePoolPop
        • AutoreleasePoolPage::pop
          • popPage

AutoreleasePoolPage结构体了解么?

  1. Autoreleasepool是由多个AutoreleasePoolPage双向链表的形式连接起来的
  2. Autoreleasepool的基本原理:
  • 在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位,
  • 在此期间,当有对象调用autorelsease时,会把对象添加到AutoreleasePoolPage
  • 若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为hotPage
  • 当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位。
  1. AutoreleasePoolPage结构如下
class AutoreleasePoolPage {
    magic_t const magic;
    id *next;//下一个存放autorelease对象的地址
    pthread_t const thread; //AutoreleasePoolPage 所在的线程
    AutoreleasePoolPage * const parent;//父节点
    AutoreleasePoolPage *child;//子节点
    uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
    uint32_t hiwat;
}

ARC下什么样的对象由 Autoreleasepool 管理

  • 当使用alloc/new/copy/mutableCopy开始的方法进行初始化时,会生成并持有对象(系统会自动的帮它在合适位置release),不需要pool进行管理
  • 一般类方法创建的对象需要使用Autoreleasepool进行管理

如何实现AutoreleasePool

  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)

autorelease_pool_page.png

  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)

  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址

  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage链接到链表中,后来的autorelease对象会添加到这个新的page

  • 调用objc_autoreleasePoolPush方法会将一个POOL_BOUNDARY值为0(也就是个nil)入栈,并且返回其存放的内存地址

objc_autoreleasepool_push.png

  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,根据传入的哨兵对象地址找到哨兵对象所处的page,从最后一个入栈的对象开始,给每个对象发送release消息,并向回移动next指针直到遇到这个POOL_BOUNDARY

objc_autoreleasepool_pop.png

  • id *next指向了下一个能存放autorelease对象地址的区域

释放时机

分两种情况:手动干预释放时机、系统自动去释放。

手动干预释放

  • 指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。

系统自动去释放

  • 不手动指定autoreleasepool, Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放

  • 而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池PushPop

autorelease_release0.png

Autoreleasepool 与 Runloop 的关系,autorelease对象在什么时机会被调用release

  • 主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理

  • iOS在主线程的Runloop中注册了2个Observer

    • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
    • 第2个Observer
      • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
      • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop() autorelease 对象是在 runloop 的即将进入休眠时进行释放的
  • objc_autoreleasePoolPop()调用时候会给 pool 中的对象发送一次 release 消息

子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?

  • 在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。
  • 如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。
    • 在这个方法中,会自动帮你创建一个 hotpage(当前正在使用的 AutoreleasePoolPage)
    • 调用 page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!

zhangferry.com/2022/03/10/…

关联对象

关联对象涉及到哪些类

objc_association0.png

objc_association.png

objc_setAssociatedObject中key的几种形式

  • const void * name; 不推荐
    • 其实就是个0,如果还定义了其他的key,再取出来会错乱
    • 其次如果在其他地方声明了extern name;那么也可以访问name。可以添加static
  • static const char name;使用的时候使用 &name;不推荐
  • @selector
  • _cmd

集合实现原理

可变数组实现原理

内存结构

NSMutableArray内部使用了__NSArrayM子类,__NSArrayM使用storage存储环形缓冲区结构。主要结构

unsigned int _list:缓冲区元素数组首地址指针。
unsigned int _offset:缓冲区首元素的位置索引。
unsigned int _size:缓冲区总大小。
unsigned int _used:缓冲区实际使用量大小。

环形特性

__NSArrayM内部使用的环形缓冲区结构,主要有以下特性:

  1. 2的倍数扩容。初始化或添加元素的时候,按照2的倍数扩容。
  2. 首尾插入删除很快,O(1),只需要更新_offset
  3. 单纯移除某个元素,缓冲区大小_size不会减小,移除所有元素操作,缓冲区才会清0。
  4. 中间插入删除,效率比较低,会选择移动较少的一方的元素进行移动。不过仍然比普通可变数组中间插入删除效率要高。
  5. 删除元素,元素指针不会清除,只会被其他元素指针移动覆盖。但是最后一个移动的元素指针就不会清除了,因为没有被覆盖的指针。