0%

This chapter guides you through the process of finding the best architecture for your project.

There’s no shortage of architecture patterns. Unfortunately, most patterns only scratch the surface and leave you to figure out the fine details. In addition, many patterns are similar to one another and have only minor differences here and there.

There are pragmatic steps you can take to ensure your architecture is effective:

  1. Understand the current state of your codebase.
  2. Identify problems you’d like to solve or code you’d like to improve.
  3. Evaluate different architecture patterns.
  4. Try a couple patterns on for size before committing to one.
  5. Draw a line in the sand and define your app’s baseline architecture.
  6. Look back and determine if your architecture is effectively addressing the problems you want to solve.
  7. Iterate and evolve your app’s architecture over time.
阅读全文 »

Spine-runtimes

根据Spine版本在https://github.com/EsotericSoftware/spine-runtimes/releases下载对应的运行库版本。

这里使用的版本为3.4.02。

cmake

  1. 终端输入brew install cmake安装cmake;
  2. cd进入spine-runtimes/spine-cocos2d-objc目录;
  3. 输入mkdir build && cd build && cmake ../..下载cocos2d环境;

下载失败重试时,需要删除build目录。

运行示例项目

打开spine-runtimes/spine-cocos2d-objc/spine-cocos2d-objc.xcodeproj项目。

  1. 项目中cocos2d文件夹显示为红色:这是由于Inspectors面板中Full Path不正确,将其修正为/YourPath/spine-runtimes-3.4.02/spine-cocos2d-objc/cocos2d即可;
  2. CCBlendModeCache类中将objectForKey:方法的参数类型由id<NSCopying>修改为NSDictionary *;
  3. 调整示例项目支持的最低iOS系统版本
  4. 运行项目即可

现有项目集成

  1. 将示例程序中的cocos2d文件夹复制到现有项目工程目录;
  2. cocos2d.xcodeproj直接拖拽到项目中;
  3. 选中现有项目Target,选择Build Phases选项卡,点击Dependencies下的+号,选择cocos2d-ios;
  4. 选中现有项目Target,选择General选项卡下的Frameworks,libraries,and Embedded Content,点击+号,选择libcocos2d.a;
  5. spine-runtimes-3.4.02/spine-c文件夹导入现有工程;
  6. spine-runtimes-3.4.02/spine-cocos2d-objc/src/spine文件夹导入现有工程;
  7. 参考示例程序中将部分文件设置为-fno-objc-arc
  8. 参考示例程序设置Header Search Paths;

创建Flutter应用

可以通过命令行输入下面的命令创建一个flutter项目:

1
flutter create first_flutter_app

注意,项目名不支持特殊符号和大写,只能使用下划线进行分隔。

创建成功后通过相应的开发工具打开项目即可。

阅读全文 »

Hello World

Dart的入口也是main函数:

1
2
3
main(List<String> args) {
print("Hello World");
}

变量声明

显式变量声明的方式如下:

1
2
3
4
//变量类型 变量名称 = 赋值;
String name = "dart fans";
int age = 20;

另外,也可以省略变量类型,由Dart进行类型推断(类似swift)。

1
var/dynamic/const/final 变量名称 = 赋值;

Dart本身是一个强类型语言,任何变量都是有确定类型的,声明变量后,只可以修改变量值,不能修改变量类型。

阅读全文 »

Flutter SDK下载

下载SDK:
https://flutter.dev/docs/development/tools/sdk/releases

配置环境变量

环境变量配置文件路径: ~/.bash_profile

1
2
3
export FLUTTER_HOME=/YourPath/flutter
export PATH=$PATH:$FLUTTER_HOME/bin
export PATH=$PATH:$FLUTTER_HOME/bin/cache/dart-sdk/bin

然后通过命令: source ~/.bash_profile重新加载即可。

对终端使用zsh的系统版本,环境变量路径为:~/.zshrc

可以通过命令echo $PATH查看环境变量。

遇到的问题

问题描述:

Exception: Flutter failed to create a directory at “YourPath/flutter”. The flutter tool cannot access the file or directory.

解决: sudo chown -R $USER path

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例1:

1
2
3
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例2:

1
2
3
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例3:

1
2
3
4
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
  请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串

解法(C语言):

阅读全文 »

App启动步骤

iOS应用的启动可以分为:

  1. pre-main阶段
    • 系统exec()调用
    • 加载Dyld
    • Dyld递归加载App依赖的Dylib
    • Rebase
    • Bind
    • ObjC
    • Initialiser
  2. main阶段
    • main()调用
    • UIApplicationMain()调用
    • application:willFinishLaunchingWithOptions:调用
    • application:didFinishLaunchingWithOptions:调用
    • 首屏渲染

pre-main阶段概括来说,系统内核会先调用exec()函数,将App加载到内存中。然后系统内核会再加载一个Dyld程序,Dyld负责加载App所依赖的所有动态链接库。再接着Dyld会调整这些动态链接库的指针指向。最后加载Runtime组件,向需要初始化的对象发送初始化消息。

本文主要涉及pre-main阶段。

原理篇

内核简介

内核是操作系统的核心。iOS和OS X使用的都是XNU内核。在这里,我们不需要知道XNU内核的详细架构,只需要知道它的功能即可,例如:提供基础服务,像线程,进程管理,IPC(进程间通信),文件系统等等。

Mach-O

Mach-O (Mac object file format) 是一种运行时可执行文件的文件类型。具体的文件类型包括:

  • Executable:应用主要的二进制文件;
  • Dylib: 动态链接库(类似其他平台上的DSO或DLL);
  • Bundle:特殊的Dylib,不能被链接,只能在运行时使用dlopen()函数打开,Mac OS的插件会用到;
  • Image: 代指一个Executable、Dylib或者Bundle;
  • Framework:包含了资源和头文件目录结构的Dylib;

Segment

一个Mach-O文件被划分为多个Segment,Segment使用全大写字母命名。每一个Segment的大小都是一个内存页大小的整数倍。几乎所有的Mach-O文件都包含这三个Segment:

  • __TEXT: readonly,包含了Mach的头文件,代码,以及常量(比如C字符串);
  • __DATA: read-write,包含了全局变量,静态变量等;
  • __LINKEDIT: 包含了如何加载程序的元数据信息;

例如在下图中,TEXT段的大小为3页,DATA段和LINKEDIT段的大小都为1页。

页面大小则取决于具体的硬件。

Section

每个Segment又被划分为多个Section,Section使用全小写字母命名。Segment的大小与页面大小无关,但是Segment之间互不重叠。

Mach-O Universal File

假如一个Mach-O文件需要同时在32bit和64bit的iOS设备上运行,Xcode会生成两个Mach-O文件,一个支持在32bit设备上运行(armv7s),另一个支持在64bit设备上运行(arm64)。然后将这两个文件合并为一个文件,合并生成的文件就叫做Mach-O通用文件。

Mach-O通用文件会包含一个占用一个页面大小的头部,该头部会包含所支持的体系结构的列表以及相应的偏移量。如图所示:

那为什么segment大小必须是页面大小的整数倍?为什么一个头部要占用一个页面大小?这样是否浪费了大量空间?这就涉及到下部分的内容虚拟内存了。

虚拟内存

可以将虚拟内存理解为一个中间层,其要解决的问题是当多个进程同时存在时,如何对物理内存进行管理。虚拟内存技术提高了CPU利用率,使多个进程可以同时、按需加载。

虚拟内存被划分为一个个大小相同的页面(Page),提高管理和读写的效率。 页面大小则取决于具体的硬件,在arm64处理器上一页的大小为16K,其他处理器上一页的大小为4K。

逻辑地址到物理RAM的映射

每个进程都是一个逻辑地址空间,映射到RAM的某个物理页面。这种映射关系不一定是一对一的,逻辑地址可以不对应任何物理RAM,多个逻辑地址也可能映射到同一块物理RAM。

如果一个逻辑地址不映射到任何物理RAM,当进程要访问该地址时,就会产生页面错误(Page fault),内核将中断该进程,寻找可用的物理内存,接着继续执行当前程序。

如果两个进程的逻辑地址映射到了同一块物理RAM,这两个进程将共享这一块物理内存。

File backed mapping

虚拟内存另一个有意思的特性是基于文件的映射(File backed mapping)。在进行文件读取时,不需要将整个文件都读入到RAM,而是调用mmap()函数将文件的某个片段映射到逻辑内存的某个地址范围。如果要访问的文件内容不在内存中,即发生Page fault时,内核才会去读取要访问的文件内容,从而实现了文件的懒加载。

Copy-On-Write

总的来说,Mach-O文件中的__TEXT段可以使用懒加载的方式映射到多个进程中,这些进程会共享这一块内存。__DATA段是可读可写的,这就涉及到了Copy-On-Write 技术,简称COW。

当多个进程共享同一个内存页时,一旦其中一个进程要对__DATA段进行写入操作时,就会发生COW。这时,内核会复制这一页内存,并重定向之前的映射关系,这样进行写入操作的进程就拥有了该页内存的拷贝副本,写入操作完成后不会对其他进程造成影响。

由于包含了进程相关的特定信息,拷贝生成的新内存页被称为Dirty Page。与之相对的被称为Clean Page,可以被内核重新生成(比如重新从磁盘读取)。所以,Dirty Page的内存代价要远远大于Clean Page

所以在多个进程加载同一个Dylib时,__TEXT__LINKEDIT因为是只读的,是可以共享内存的。而__DATA因为是可读写的,就会产生Dirty Page(参见下文Rebase和Bind的介绍)。当对Dylib的操作执行结束后,__LINKEDIT就没用了,对应的内存页会被回收。

页面权限

可以将一个内存页标记为readable, writable, executable, 或者这三者的任意组合。

在iOS上,当内存不足的时候,会尝试释放那些被标记为只读的页,因为只读的页在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的App(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台的App。

安全性相关

以下两项技术提升了应用安全性:

  • ASLR:Address Space Layout Randomization,地址空间布局随机化
  • Code Signing: 代码签名

ASLR

简单来说,就是当Mach-O文件映射到逻辑地址空间的时候,利用ASLR技术,可以使得文件的起始地址总是随机的,以避免黑客通过起始地址+偏移量找到函数的地址。

Code Signing

在编译阶段,Mach-O文件的每一个页面都会进行单独的哈希算法加密,所有的哈希值存储在__LINKEDIT中。这保证了每个页面在被加载的过程中都能得到及时验证。

exec()调用

exec()是一个系统调用。当打开一个应用时,ASLR会将应用映射到一个起点随机的地址空间。然后将该随机地址到0的整个地址空间标记为不可访问,即不可读,不可写,不可访问(下图中的PAGEZERO)。

PAGEZERO是一个安全领域, NULL指针就是指向这里。其大小为:

  • 32bit进程:4KB+
  • 64bit进程:4GB+

Dylb

Dyld(Dynamic loader)是一个用来加载动态链接库(Dylib)的帮助程序。当系统内核将应用加载到内存的一个随机逻辑地址之后,就会将Dyld加载到内存的另一个随机逻辑地址。然后程序计数器(PC寄存器)的PC指针指向Dyld,由Dyld完成应用的启动。Dyld的任务是加载应用所需要的所有Dylibs,其操作权限与应用权限相同。

Dyld具体的加载步骤为:

  1. Load dylibs:Map all dependent dylibs, recurse
  2. Fix-ups:Rebasing and Binding
  3. ObjC: Objc prepare images
  4. Initializersre: Run initializers

Load dylibs

加载dylibs的过程可以分为以下几步:

  1. Parse list of dependent dylibs:Dyld首先会读取主执行文件的头部,该头部中包含了所有需要依赖的库的列表;
  2. Find requested mach-o file: 找到列表中依赖库对应的Mach-O文件;
  3. Open and read start of file: 打开Mach-O文件,并读取头部信息,确保文件正确;
  4. Validate mach-o: 验证mach-o文件,找到其代码签名;
  5. Register code signature: 将上一步中找到的代码签名注册到内核中;
  6. Call mmap() for each segment: 在该Mach-o文件的每一个segment上调用mmap()函数

首先,应用直接依赖的库的会被加载完成。但是这些被依赖的库自身可能还会依赖其他的库,Dyld会递归加载这些依赖库,直到所以需要依赖的库都加载完成。平均来说,一个进程会加载1到400个库,这个数量很大,好在大部分都是系统库,系统会进行提前计算和缓存,所以系统库的加载速度非常快。

Dyld是开源的,其地址为:https://opensource.apple.com/source/dyld/

Fix-ups

在加载完所有依赖的dylib之后,这些dylib暂时相互独立,需要将它们绑定到一起。这就是Fix-ups。

由于代码签名的存在,我们不能对指令进行修改。所以如果一个dylib要调用另外一个dylib,只能通过添加间接层来实现。当调用发生时,code-gen,也就是动态PIC(Position Independent Code, 地址无关编码),会在__DATA段创建一个指针,指向要调用的对象,然后加载这个指针并进行跳转。

也就是说,dylibA想调用dylibB的sayHello方法,code-gen会先在dylibA的__DATA段中建立一个指针指向sayHello,再通过这个指针实现间接调用。

所以Dyld要做的就是修正这些指针和数据。

Fix-ups有两种类型:Rebasing和Binding。

Rebasing 和 Binding

Rebasing是修正指向Mach-O文件内部的指针。Binding则是修正指向Mach-O文件外部的指针。如下图所示: 指向_malloc和_free的指针是修正后的外部指针指向。而指向__TEXT的指针则是被修复后的内部指针指向。

这个步骤产生的原因是上文提到过的ASLR。由于起始地址的偏移,所有__DATA段内指向Mach-O文件内部的指针都需要增加这个偏移量, 这些指针的信息包含在__LINKEDIT段中。既然Rebase需要写入数据到__DATA段,那也就意味着Copy-On-Write势必会发生,Dirty page的创建,IO的开销也无法避免。但Rebase的顺序是按地址排列的,所以从内核的角度来看这是个有次序的任务,它会预先读入数据,减少 I/O 消耗。

Binding是__DATA段对外部符号的引用。不过和Rebase不同的是,Binding是靠字符串的匹配来查找符号表的,虽说没有多少IO,但是计算多,效率比Rebasing慢。

可以通过下面的命令查看 rebase 和 bind 的信息:

1
xcrun dyldinfo -rebase -bind -lazy_bind myapp.app/myapp

ObjC Runtime

大部分ObjC数据结构中的指针(比如IMP或superclass)都会在Fix-ups的过程中被修复。不过Runtime还需要进行一些其他操作:

  1. 将所有类的类名注册到一个全局表中(所以ObjC可以通过类名实例化一个对象)
  2. 将分类中的方法添加到类的方法列表中
  3. 检查Selector的唯一性

Initializers

ObjC中有一个+load方法,不过已经不推荐使用,建议使用+initialize方法进行替代。

如果存在+load方法,此时将会被调用。然后会自底向上的,即从底部的dylib开始,逐步向上加载,直到主执行文件,依次进行初始化。这种加载顺序确保了安全性,加载某个 dylib 前,其所依赖的其余 dylib 文件肯定已经被预先加载。

最后 dyld 会调用 main() 函数。main() 会调用 UIApplicationMain()。

实践篇

启动时间的计算标准

应用启动分为冷启动和热启动,这里只讨论冷启动。为了准确测量冷启动耗时,测量前需要重启设备。

应用的冷启动时间与具体的设备有关,但最好控制在400ms以内。一旦启动时间超过20s,系统会认为应用进入了死循环,并终结该进程。启动时间的测试应该以应用支持的最低配置设备为参考。直到 applicationWillFinishLaunching方法被调动,应用才算启动结束。

要注意的是,UIApplicationMain()和applicationWillFinishLaunching都计算在应用的启动时间中。

测量启动时间

Dyld可以测量main()方法执行之前所消耗的时间,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1。运行应用,可以看到如下类似的打印输出:

1
2
3
4
5
6
7
8
Total pre-main time: 228.41 milliseconds (100.0%)
dylib loading time: 82.35 milliseconds (36.0%)
rebase/binding time: 6.12 milliseconds (2.6%)
ObjC setup time: 7.82 milliseconds (3.4%)
initializer time: 132.02 milliseconds (57.8%)
slowest intializers :
libSystem.B.dylib : 122.07 milliseconds (53.4%)
CoreFoundation : 5.59 milliseconds (2.4%)

优化建议

Dylib加载阶段

可以减少使用的Dylib数量,将多个Dylib进行合并。Apple的WWDC2016(Session 406)给出了一个例子: 一个项目依赖26个动态库,dylibs加载时间是240毫秒; 当将其合并成2个动态库时,加载时间变为20毫秒,可以说性能显著提高。

也可以使用静态库。

不建议使用dlopen()对Dylib进行懒加载,可能会造成一些其他问题,而且实际工作量可能会更多。

Rebasing/Binding阶段

根据Rebasing和Binding所进行的操作,可以减少要Fix-ups的指针数量。

具体来说,就是减少ObjC元数据(Class,selector,category)的数量。很多设计模式都会生成大量的类,但这会影响应用的启动速度。

还可以减少使用C++的虚函数,C++会生成一个虚函数表,这也会在__DATA段中创建。

最后推荐使用Swift结构体,因为struct是值类型。

ObjC setup阶段

针对这步所能事情很少,Rebasing/Binding阶段的优化工作也会使得这步耗时减少。

Initializer阶段

在iOS平台下,如果项目使用ObjC,尽量减少使用+load方法,。如果项目使用Swift,Apple已经帮我们调用了他们自己的initializer(dispatch_once), 确保Swift的Class不会被初始化多次。

在这一阶段,可以做的优化有:

  • 减少使用+load方法,如果必须使用, 替换为+initialize
  • 减少构造函数的数量,尽量不要在构造函数里进行耗时任务
  • 减少C++静态全局变量的数量

另外,不要在初始化方法中调用 dlopen(),这会对性能有影响。因为 Dyld 在 App 开始前运行,由于此时是单线程运行所以系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这就严重影响了性能,还可能会造成死锁以及产生未知的后果。所以也不要在初始化器中创建线程。

补充:Dyld3

上文的讲解都是dyld2的加载方式。而在iOS 13系统中,将全面采用新的dyld3以替代之前版本的dyld2。 因为dyld3完全兼容dyld2,API接口是一样的,所以在大部分情况下,不需要做额外的适配就能平滑过渡。

dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。

dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情:

  • 分析Mach-o Headers
  • 分析依赖的动态库
  • 查找需要Rebase & Bind之类的符号
  • 把上述结果写入缓存

这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。

参考链接

iOS中的常见多线程方案

GCD

GCD源码地址:https://github.com/apple/swift-corelibs-libdispatch

GCD使用概要:Objective-C之GCD概要

常用函数

GCD中有2个用来执行任务的函数:

  1. 用同步的方式执行任务
1
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
  1. 用异步的方式执行任务
1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

队列

GCD的队列可以分为2大类型:

  • 并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步(dispatch_async)函数下才有效
  • 串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

同步和异步主要影响:能不能开启新的线程

  • 同步:在当前线程中执行任务,不具备开启新线程的能力
  • 异步:在新的线程中执行任务,具备开启新线程的能力

并发和串行主要影响:任务的执行方式

  • 并发:多个任务并发(同时)执行
  • 串行:一个任务执行完毕后,再执行下一个任务

各种队列的执行效果:

使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)。

队列组

参见GCD使用概要:Objective-C之GCD概要

iOS中的线程同步方案

性能从高到低排序:

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex(default)
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

OSSpinLock

OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。目前已经不再安全,可能会出现优先级反转问题,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。

1
2
3
4
5
6
7
8
9
10
#import <libkern/OSAtomic.h>

// 初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁,如果需要等待就不加锁直接返回false,否则加锁后返回true
bool result = OSSpinLockLock(&lock);
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);

os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持。从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。

1
2
3
4
5
6
7
8
9
10
#import <os/lock.h>

// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <pthread.h>

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 销毁锁
pthread_mutex_destroy(&mutex);

锁的类型有:

1
2
3
4
#define PTHREAD_MUTEX_NORMAL		0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2 //递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL

递归锁:允许同一个线程对一把锁进行重复加锁。

pthread_mutex条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化锁
pthread_mutex_t mutex;
// NULL表示使用默认属性
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t cond
pthread_cond_init(&cond, NULL);
// 等待条件(进入休眠,放开mutex锁,被唤醒后会再次对mutex加锁
pthread_cond_wait(&cond, &mutex)
// 激活一个等待该条件的线程
pthread_cond_signal(&cond);
// 激活所有等待该条件的线程
pthread_cond_broadcast(&cond);
// 销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

NSLock

NSLock是对mutex普通锁的封装。

核心定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking>

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end

NSRecursiveLock

NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致。

NSCondition

NSCondition是对mutex和cond的封装。

核心定义如下:

1
2
3
4
5
6
7
8
9
@interface NSCondition : NSObject <NSLocking> 
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name;

@end

NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。条件值默认为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface NSConditionLock : NSObject <NSLocking>

// 初始化, 同时设置 condition
- (instancetype)initWithCondition:(NSInteger)condition;

// condition值
@property (readonly) NSInteger condition;

// 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (void)lockWhenCondition:(NSInteger)condition;
// 尝试加锁
- (BOOL)tryLock;
// 尝试加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
// 解锁, 同时设置NSConditionLock实例中的condition值
- (void)unlockWithCondition:(NSInteger)condition;
// 加锁, 如果锁已经使用, 那么一直等到limit为止, 如果过时, 不会加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁, 时间限制到limit, 超时加锁失败
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
// 锁的name
@property (nullable, copy) NSString *name;

@end

dispatch_queue

直接使用GCD的串行队列,也是可以实现线程同步的。

1
2
3
4
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//do something;
});

dispatch_semaphore

semaphore叫做”信号量”。信号量的初始值,可以用来控制线程并发访问的最大数量。信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。

1
2
3
4
5
6
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(long value)
// 计数为0时休眠等待,计数为1或大于1时,减去1而不等待继续执行。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 信号量计数器加1
dispatch_semaphore_signal(semaphore);

@synchronized

@synchronized是对mutex递归锁的封装。@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作。

1
2
3
@synchronized(obj) {
//do something
}

自旋锁、互斥锁比较

什么情况使用自旋锁比较划算?

  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  • CPU资源不紧张
  • 多核处理器

什么情况使用互斥锁比较划算?

  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

atomic

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的自旋锁。但是它并不能保证使用属性的过程是线程安全的。

可以参考源码objc4的objc-accessors.mm。

iOS中的读写安全

考虑如下场景:

  • 同一时间,只能有1个线程进行写的操作
  • 同一时间,允许有多个线程进行读的操作
  • 同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有:

  • pthread_rwlock:读写锁
  • dispatch_barrier_async:异步栅栏调用

pthread_rwlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义读写锁
pthread_rwlock_t rwlock;
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 读取加锁
pthread_rwlock_rdlock(&rwlock);
// 尝试读取加锁
pthread_rwlock_tryrdlock(&rwlock);
// 写入加锁
pthread_rwlock_wrlock(&rwlock);
// 尝试写入加锁
pthread_rwlock_trywrlock(&rwlock);
// 解锁
pthread_rwlock_unlock(&rwlock);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);

dispatch_barrier_async

该函数会等到追加到并发队列上的并行处理全部结束之后, 再将指定的处理追加到该并发队列中。 然后在等该函数追加的处理执行完成后, 该并发队列才恢复为一般动作, 开始执行之后追加的并行处理.

该函数传入的并发队列必须通过dispatch_queue_cretate创建。

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_t queue = dispatch_queue_create("rwQueue", DISPATCH_QUEUE_CONCURRENT)

// 读
dispatch_async(queue, ^{

});

// 写
dispatch_barrier_async(queue, ^{

});

RunLoop是iOS/macOS下的事件循环机制,同时也是一个OC对象,该对象管理了其需要处理的时间和消息,并提供了一个入口函数来执行。

Runloop的代码逻辑如下:

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

RunLoop的基本作用:

  • 保持程序的持续运行
  • 处理App中的各种事件(比如触摸事件、定时器事件等)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  • ……

以下都属于Runloop的应用范畴:

  • 定时器(Timer)、方法调用(PerformSelector)
  • GCD Async Main Queue
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • 自动释放池 AutoreleasePool

Runloop对象

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

  • Foundation:NSRunLoop
  • Core Foundation:CFRunLoopRef

NSRunLoop和CFRunLoopRef都代表着RunLoop对象,NSRunLoop是基于CFRunLoopRef的一层OC包装。CFRunLoopRef是开源的:https://opensource.apple.com/tarballs/CF/

1
2
3
4
5
6
//获取当前线程的runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
//获取主线程的runloop
NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop];
CFRunLoopRef mainRunloop2 = CFRunLoopGetMain();

RunLoop与线程

Runloop与线程有如下关系:

  • 每条线程都有唯一的一个与之对应的RunLoop对象;
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;
  • RunLoop会在线程结束时销毁;
  • 主线程会在UIApplicationMain方法中获取Runloop,子线程默认没有开启RunLoop;

在这里需要注意的是,performSelector:withObject:afterDelay:方法的的本质是往Runloop中添加定时器,而子线程默认没有启动Runloop,所以在子线程中调用该方法不会得到正确的响应。

RunLoop与线程的相关源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//  全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
// 访问__CFRunLoops的锁
static CFLock_t loopsLock = CFLockInit;

// 获取pthread 对应的 RunLoop。
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
// pthread为空时,获取主线程
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
__CFLock(&loopsLock);
}
// 从全局字典里获取对应的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果取不到,就创建一个新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//设值
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
}
return loop;
}

Runloop相关的类

Core Foundation中关于RunLoop的5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。

RunLoop启动时只能选择其中一个Mode,作为currentMode,如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,这样保证了不同组的Model(Source0/Source1/Timer/Observer)能分隔开来,互不影响。

如果当前Runloop的Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。

常见的2种Mode:

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行;
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;

另外还有个概念叫CommonModes:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

主线程的RunLoop中预置的两个Mode:kCFRunLoopDefaultMode和 UITrackingRunLoopMode都已经被标记为”Common”属性。

CFRunLoopSourceRef

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

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

Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程

主要用于:

  • Source0
    • 触摸事件处理
    • performSelector:onThread:
  • Source1
    • 基于Port的线程间通信
    • 系统事件捕捉

屏幕交互事件通过Source1捕捉,然后分发到Source0处理。

CFRunLoopTimerRef

CFRunLoopTimerRef是基于时间的触发器,其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。主要应用有:

  • NSTimer
  • performSelector:withObject:afterDelay:

CFRunLoopObserverRef

CFRunLoopObserverRef是Runloop的观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。主要用于:

  • 用于监听RunLoop的状态
  • UI刷新(BeforeWaiting)
  • Autorelease pool(BeforeWaiting)

一个RunLoop有如下几种状态:

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

给Runloop添加Observer的代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}

case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

Runloop的运行逻辑

RunLoop 内部的逻辑大致如下:

其代码逻辑整理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;

/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {

/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}

/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

/// 收到消息,处理消息。
handle_msg:

/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}

/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}

/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}

/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);


if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

RunLoop在实际开中的应用

  • 控制线程生命周期(线程保活)
  • 解决NSTimer在滑动时停止工作的问题
  • 监控应用卡顿
  • 性能优化

Objective-C的动态性是由Runtime API来支撑的,Runtime API提供的接口基本都是C语言的,源代码由C\C++\汇编语言编写。

isa详解

在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构isa_t,还使用位域来存储更多的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//arm64架构下,isa_t的结构参考如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19
};
};

字段含义如下:

  • 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的类的属性中;

arm64架构下取出shiftcls的掩码ISA_MASK为0x0000000ffffffff8ULL,由此可见,class对象和meta-class对象的地址值最后3位都是0.

Class结构

Class本质上为一个结构体类型:

1
2
typedef struct objc_class *Class;

与该结构体相关的主要定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct objc_object {
private:
isa_t isa;
//以下省略
}

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 类的具体信息
class_rw_t *data() {
return bits.data();
}
//以下省略
}

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;

method_array_t methods; //方法列表
property_array_t properties; //属性信息
protocol_array_t protocols; //协议列表
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //instance对象占用的内存大小
#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_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容。

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

method_t是对方法、函数的封装:

1
2
3
4
5
struct method_t {
SEL name; //函数名
const char *types; //编码(返回值类型,参数类型)
MethodListIMP imp; //指向函数的指针(函数地址)
};

IMP代表函数的具体实现:

1
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

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

  • 可以通过@selector()和sel_registerName()获得;
  • 可以通过sel_getName()和NSStringFromSelector()转成字符串;
  • 不同类中相同名字的方法,所对应的方法选择器是相同的;
1
typedef struct objc_selector *SEL;

types包含了函数返回值、参数编码的字符串。 相关介绍可以参考:Type Encodings

方法缓存

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

1
2
3
4
5
6
7
8
9
10
struct cache_t {
struct bucket_t *_buckets; //散列表
mask_t _mask; //散列表长度-1
mask_t _occupied; //已经缓存的方法数量
}

struct bucket_t {
uintptr_t _imp; //函数地址
SEL _sel; //缓存的key
}

方法调用

OC中的方法调用,其实都是转换为objc_msgSend函数的调用。objc_msgSend的执行流程可以分为3大阶段:

  1. 消息发送
  2. 动态方法解析
  3. 消息转发

objc_msgSend源码导读

消息发送

从源码归纳出如下流程:

动态方法解析

假如在消息发送过程中,没有查找到方法,那么就会进入动态方法解析。动态方法解析就是在运行时临时添加一个方法实现,来进行消息的处理。

开发者可以实现以下方法,来动态添加方法实现:

  • resolveInstanceMethod:
  • resolveClassMethod:

下面是代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void notFound_eat(id self, SEL _cmd)
{
// implementation ....
NSLog(@"%@ - %@", self, NSStringFromSelector(_cmd));
NSLog(@"current in method %s", __func__);
}

//对象方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 注意添加到self,此处即类对象
class_addMethod(self, sel, (IMP)notFound_eat, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}

//类方法解析
+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(learn)) {
// 第一个参数是object_getClass(self)
class_addMethod(object_getClass(self), sel, (IMP)notFound_learn, "v16@0:8");
return YES;
}
return [super resolveClassMethod:sel];
}

下面是class_addMethod添加的另一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)notFound_eat
{
// implementation ....
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 获取其他方法
Method method = class_getInstanceMethod(self, @selector(notFound_eat));

// 动态添加test方法的实现
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));

// 返回YES代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}

动态解析过后,会重新进入到“消息发送”的流程,“从receiverClass的cache中查找方法”这一步开始执行。

在动态方法解析完成后,会将标识tridResolver设置为YES,表示已经进行过动态解析,避免消息发送和动态方法解析之间出现死循环。

动态方法解析最佳的一个实践用例就是@dynamic的实现。

消息转发

下面是消息转发阶段的流程图:

super

super调用底层会转换为objc_msgSendSuper2()函数调用, 相关定义及注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 
* Sends a message with a simple return value to the superclass of an instance of a class.
*
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
* context the message was sent to, including the instance of the class that is to receive the
* message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method identified by \e op.
*
* @see objc_msgSend
*/
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

struct objc_super2 {
id receiver; //消息接收者
Class cls; // the class to search,消息接收者的父类
}

使用super调用时,消息的接收者仍然是self,只是会从父类中开始寻找方法。

Runtime API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
动态创建一个类(参数:父类,类名,额外的内存空间)
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)

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
获取一个实例变量信息
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)

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
获取一个属性
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)

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
获得一个实例方法、类方法
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)