常用 Git 命令

Git 基础命令与运用

从根本上来讲,Git 是一个内容寻址文件系统,并在此之上提供了一个版本控制系统的用户界面。这意味着 Git 的核心部分是一个简单的键值对数据库。

基础命令

  • 配置 Git

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 配置全局用户
    git config --global user.name "Your name"
    git config --global user.email "Your email"
    # 配置命令的别名
    git config --global alias.co checkout
    git config --global alias.ss status
    git config --global alias.cm commit
    git config --global alias.br branch
    git config --global alias.rg reflog
    # 删除全局配置
    git config --global --unset alias.xxx
    git config --global --unset user.xxx
  • 查看信息

    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
    # 查看 git 配置
    git config --list
    # 查看用户配置
    git ~/.gitconfig
    # 查看当前项目的 git 配置
    git .git/config
    # 查看暂存区的文件
    git ls-files
    # 查看本地 git 命令历史
    git reflog
    # 查看当前 HEAD 指向
    cat .git/HEAD
    # git 中 D 向下翻一行 F 向下翻页 B 向上翻页
    # git 查看提交历史
    git log --oneline
    # 查找和关键字有关的 log
    git log --grep="关键字"
    # 图形化显示 log
    git log --graph
    # 查找指定作者的 log
    git log --author "username"
    # 显示最近 num 次提交
    git log -num
    # 显示每次更新的文件修改统计信息
    git log --stat
  • 初始化 git 仓库

    1
    git init    

    在本地创建一个新的仓库

  • Clone

    1
    git clone /path/to/repository 

    创建一个本地仓库的备份

    1
    git clone username@host:/path/to/repository

    创建一个服务上仓库的备份

    1
    git clone [url] <custom_name>

    为克隆的仓库指定别名

  • Add

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 添加一个或多个文件到索引
    git add [file1] [file2] ...
    # 将当前目录的所有文件变更提交到暂存区,不包括被删除的文件
    git add .
    # 将工作区已经追踪的文件变更提交到暂存区,不会提交新文件(即 untracked file)
    git add -u
    # 将工作区的所有文件变更提交到暂存区
    git add -A
    # 删除工作区/暂存区的文件
    git rm [file1] [file2] ...
    # 停止追踪指定文件,但此文件会保留在工作区
    git rm --cached [file]
    # 修改工作区/暂存区的文件的名称
    git mv [file_original] [file_renames]
  • Commit

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 提交变更到本地仓库
    git commit -m "Commit message"
    # add 和 commit 的合并的便捷写法
    git commit -am "Commit message"
    # 跳过验证继续提交
    git commit --no-verify
    git commit -n
    # 修改上一次提交的信息
    git commit --amend
    # 修复提交并修改提交信息
    git commit --amend -m "commit message"
  • Push/Pull

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 将本地仓库的文件推送到远程分支
    # 如果远程仓库没有这个分支,会创建一个同名的远程分支
    # 如果两者同名,则可省略远程分支名称
    git push <远程主机名> <本地分支名>:<远程分支名>
    # 如果省略本地分支名称,则表示删除指定的远程分支
    git push origin :<远程分支名>
    # 相当于
    git push origin --delete <远程分支名>
    # 将当前分支与远程分支进行关联
    git push -u origin <远程分支名>
    # 将本地的所有分支推送到远程仓库
    git push --all origin
    # 从关联的远程分支拉取更改到当前分支
    git pull
    # 拉取其它分支并合并到当前分支
    git pull origin <远程分支名>
  • Diff

    1
    2
    3
    4
    # 比较的是工作目录文件与暂存区文件的区别
    git diff
    # 比较的是暂存区的文件与仓库分支里的文件的区别
    git diff --cached
  • Status

    1
    git status

    列出你已经更改过但是还没有添加或提交的文件

  • Remote

    1
    2
    3
    4
    5
    6
    7
    8
    # 查看所有远程主机
    git remote
    # 查看关联的远程仓库的详细信息
    git remote -v
    # 移除与远程仓库的关联
    git remote rm <仓库名称>
    # 设置关联的远程仓库
    git remote set-url origin <远程仓库地址>
  • Branches

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 查看本地分支
    git branch | git branch -l
    # 查看远程分支
    git branch -r
    # 查看所有分支
    git branch -a
    # 查看所有分支和最新的提交
    git branch -av
    # 新建分支,新分支会复制当前分支已经提交到仓库的内容
    git branch <分支名>
    # 切换分支
    git checkout <分支名>
    # 创建一个新分支并切换到新分支
    git checkout -b <分支名>
    # 删除本地分支,会阻止删除包含未合并更改的分支
    git branch -d <分支名>
    # 强制删除一个本地分支,即使是包含未合并更改的分支
    git branch -D <分支名>
    # 删除远程分支
    git push origin :<分支名>
    # 或
    git push origin --delete <分支名>
    # 修改当前分支名称
    git branch -m <分支名>
  • Merge

    1
    2
    3
    4
    5
    6
    # 默认的合并方式,fast-forward,HEAD 指针直接指向被合并的分支
    git merge
    # 禁止快进式合并
    git merge --no-ff
    #
    git merge --squash
  • Tags

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 在指定的 commit id 上创建一个标签
    git tag <标签名> <commit_id>
    # 默认在 HEAD 上创建一个标签
    git tag <标签名>
    # 创建带有说明的表亲啊
    git tag -a <标签名> -m "tag message"
    # 查看所有标签
    git tag
    # 推送一个本地标签
    git push origin <标签名>
    # 推送所有本地标签
    git push origin --tags
    # 删除本地标签
    git tag -d <标签名>
    # 删除一个远程标签
    git push origin :refs/tags/<标签名>
  • Checkout

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 恢复暂存区的指定文件到工作区
    git checkout <filename>
    # 恢复暂存区的所有文件到工作区
    git checkout .
    # 回滚到最近的一次提交
    git checkout HEAD
    # 回滚到最近一次提交的上一个版本
    git checkout HEAD^
    # 切换到指定 commit
    git checkot <commit id>
  • Clean

    从当前文件夹开始,通过递归删除的方式删除不在版本控制中的文件。

    1
    2
    3
    4
    5
    6
    # 从工作区移除没有被追踪的文件
    git clean -f
    # 从工作区移除没有被追踪的目录和文件
    git clean -fd
    # 查看 clean 命令会删除哪些文件
    git clean -nfd
  • Reset

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 从暂存区撤销指定文件的修改,但并不改变工作区
    git reset <file>
    # 重置暂存区最近的一次提交
    git reset
    # 相当于
    git reset HEAD
    # 重置工作区和暂存区
    git reset --hard
    # 将当前分支的指针指向指定的 commit,同时重置暂存区,工作区不变
    git reset <commit id>
    # 等价于
    git reset --mixed <commit id>
    # 将当前分支的指针指向指定的 commit,不改变暂存区和工作区
    git reset --soft <commit id>
    # 将当前分支的指针指向指定的 commit,同时重置暂存区和工作区
    git reset --hard <commit id>
  • Revert

    1
    2
    3
    4
    5
    6
    # 生成一个撤销最近一次提交的新提交
    git revert HEAD
    # 生成一个撤销最近一次提交的上num次提交的新提交
    git revert HEAD~num
    # 生成一个撤销指定提交的新提交
    git revert <commit id>
  • Cherry-pick

    1
    2
    # 将指定的提交应用于当前分支
    git cherry-pick <commit id>
  • Search

    1
    git grep "search key word"

    在工作目录中查找指定的关键字

一些运用场景

  • 撤销提交到远程的某些提交

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 通过 `git log` 命令来获得指定的某次提交的 `id`
    git reset [<mode>] [<commit>]
    # 这个命令将当前分支的 `head` 指针指向指定的 `commit` 而且可能会根据选择的 `mode` 更新索引和工作树
    # 如果 `mode` 省略,则默认值是 `--mixed`
    # `--soft` 仅仅只是将 `head` 指向指定的 `commit`
    # `--mixed` 会重置索引,但不会重置工作区
    # `--hard` 重置索引和工作区,任何在指定的 `commit` 之后的修改将被丢弃
    # `--merge`
    # `--keep`
    git reset --hard <commit_id>
    # 然后将重置后的 `branch` 推送到远程仓库
    # `--force` 会使用本地分支的提交覆盖远程分支的提交
    git push origin HEAD --force
  • 保存当前未提交修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 将当前未提交的修改保存下来,工作目录的 Head 回到上一提交
    git stash
    # or
    git stash save 'message'
    # 列出通过 stash 保存的修改
    git stash list
    # 列出 stash 的修改与 上一次提交的 diff
    git stash show
    # 恢复之前通过 stash 保存的修改
    # 这个命令将第一个 stash 恢复,并从 stash 列表中删除
    git stash pop
    # 或者使用 apply 命令将修改恢复,但不从 stash 列表中删除
    git stash apply
    # 手动删除指定的 stash
    git stash drop stash@{0}
  • 更换远程仓库

    1
    2
    3
    4
    5
    6
    # 删除远程仓库地址
    git remote rm origin
    # 添加远程仓库地址
    git remote add origin <remote_git_address>
    # 推送本地所有分支
    git push --all origin -u
  • fast-forwardno-fast-forward 合并

    当执行 get merge 命令时,git 会根据当前分支和目标分支(将要合并的分支)的状况不同而选择合适的合并方式。

    • 如果当前分支与目标分支相比没有额外的提交时,执行 fast-foward 合并,fast-foward 合并不会产生新的提交,而是将目标分支的提交直接合并到当前分支
  • 如果当前分支与目标分支相比有目标分支并没有的提交,执行 no-fast-foward 合并,这会产生新的提交,这个提交的父提交既指向当前分支,也指向目标分支。

    1
    git merge dev
  • 变基

    git rebase 会将指定分支的

iOS 与设计模式

iOS 与 设计模式

设计模式 (Design Pattern) 是一套被反复使用、多数人知晓、经过分类的、代码设计经验的总结。

我们使用设计模式的目的是为了提高代码可重用性、让代码更容易被理解、保证代码可靠性,使人们可以更加简单方便地复用成功的设计和体系结构。

设计原则

开闭原则

开闭原则就是说模块应该对扩展开放,而对修改关闭,即模块应该在尽量不修改原来代码的情况下进行扩展。

任何软件都需要面临一个很重要的问题,就是需求会随着时间的推移而发生变化。当软件系统需要面对新的需求时,应该尽量保证系统的设计框架是稳定的。如果一个软件符合开闭原则,那么就可以非常方便的对系统进行扩展并无需修改现有代码,使得系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。

为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,这就实现了在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

里氏代换原则

里氏代换原则严格表述是这样的 :如果每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 代换 o2 时,程序的 P 的行为没有变化,那么类型 S 就是 类型 T 的子类型。即所有引用父类的地方必须能透明地使用其子类的对象。

里氏代换原则是实现开闭原则的重要方式之一,由于使用父类对象的地方都可以使用子类对象,因此在程序中尽量使用父类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

里氏代换原则需要注意的问题 :

  • 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。为了保证系统的可扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
  • 应该尽量把父类设计成抽象类或接口,让子类继承父类或实现接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例。

在依照里氏代换原则实现的软件中将一个父类对象替换成它的子类对象,程序将不会产生任何错误和异常,反之则不成立。

依赖倒置原则

依赖倒置原则,即抽象应该不依赖于细节,细节应该依赖于抽象,换言之,要针对接口编程,而不是针对实现编程。

依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类去做这些事情。为了确保该原则的应用,一个具体类应该只实现接口或抽象类中声明过的方法,而不要给出多余的方法。

在实现依赖倒置原则时,我们需要针对抽象层进行编程,而将具体类的对象通过依赖注入的方法注入到其它对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式包括 :构造注入、设值注入和接口注入。构造注入是通过构造函数来传入具体类的对象,设值注入是通过 setter 方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。

在大多数情况下,以上三种设计原则一同出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则时手段。

接口隔离原则

接口隔离原则,使用多个专门的接口,而不是使用单一的接口,即客户端不应该依赖那些它不需要的接口。每一个接口应该承担相对独立的角色,不干不该干的事情,该干的事情都要干。

这里的接口往往有两种不同的含义,一种是指一个类型所具有的方特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言的具体的接口定义,有严格的定义和结构。

  • 当把接口理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可称为角色隔离原则。
  • 如果把接口理解成狭义的特定语言的接口,那么接口隔离原则是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏,应该为客户端提供尽可能小的单独的接口。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的指责单一,需要将大接口中的方法根据其指责不同分别放在不同的小接口中,确保每个接口使用起来都较为方便。接口应该尽量细化,同时接口中的方法应该尽量少。

单一职责原则

单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小,即一个类只负责一个功能领域中的相应职责,或者说 :就一个类而言,应该只有一个引起它变化的原因。

单一职责原则是实现高内聚、低耦合的知道方针,它是最简单但又最难运用的原则。

最少知识法则

也叫迪米特法则,即一个软件实体应当尽可能少地与其它实体发生相互作用。

迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应该发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。

iOS 中的设计模式

单例模式

单例模式是一种常见的设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。

我们为什么需要单例模式呢?这是因为,对于系统中的某些类来说,只有一个实例很重要。比如如果不使用机制对窗口对象唯一化,将弹出多个窗口,如果这些窗口现实的内容完全一致,则是重复对象,浪费资源。如果这些窗口现实的内容不一致,则意味着某一瞬间系统有多个状态,与实际不符。

如何保证一个类只有一个实例且这个实例易于被访问呢?一般的解决方法是让类自身负责保存它的唯一实例。这个类可以保证没有其它实例被创建,并且它可以提供一个访问该实例的方法。

单例模式的优点

  • 单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
  • 因为类控制了实例化过程,所以类可以灵活更改实例化过程。

单例模式的缺点

  • 虽然数量很少,但是每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。
  • 可能的开发混淆。
  • 不能解决删除单个对象的问题。

单例模式的代码实现

Objective-C :

1
2
3
4
5
6
7
8
+ (Singleton *)sharedInstance {
static dispatch_once_t onceToken;
static Singleton *sharedInstance = nil;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

Swift :

1
2
3
4
class Singleton {
static let sharedInstance = Singleton()
private init(){}
}

代理模式

代理模式也是一种常见的设计模式,为其它对象提供一种代理以控制对这个对象的访问。

代理模式的优点 :
  • 真实的角色就是实现实际的业务逻辑,不用关心其它非本职责的事物,通过后期的代理完成一件事物,附带的结果就是编程简洁清晰。
  • 代理对象在客户端和目标对象之间起到中介作用
  • 高扩展性

iOS 中的代理模式通过 @protocol 方式实现,用来传递事件或值。

1
2
3
4
5
6
@protocol ObjectDelegate
@required
- (void)requiredImplementDelegateMethod;
@optional
- (void)optionalImplementDelegateMethod;
@end

一般使用 weak 关键字来修饰代理属性,这是为了便面形成循环引用。

观察者模式

观察者模式也是 iOS 中常见的设计模式,观察者模式定义了一种一对多的依赖关系,一个或多个观察者对象同时监听某一个对象,当被观察者对象状态发生变化时,会通知所有观察者对象,至于观察者对象如何响应这个通知,则要看具体的实现。

在 iOS 中,观察者模式的实现具体表现为 :通知机制 (Notification) 和 KVO 机制 (Kev-Value Observing)

通知机制

被观察者达到某些触发条件后,发送通知给它的观察者对象。

注册通知接收者 :

1
2
3
4
[[NSNotificationCenter defaultCenter] addObserver:self									
selector:@selector(respondNotificationMethod)
name:@"NotificationName"
object:nil];

发布通知 :

1
2
3
[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName"
object:nil
userInfo:@{}];

在 iOS 9 之后,你不需要在观察者的销毁方法中注销观察者,但是在之前的 iOS 版本中,你必须在销毁方法中注销观察者。

KVO 机制

被观察的对象的属性变化时,发送通知给它的观察者对象。

KVO 机制的观察者对象观察的是对象的属性的变化,只有遵循 KVO 变更属性值的方式才会执行 KVO 的回调部分。

添加监听者 :

1
2
3
4
[self.aProperty addObserver:self
forKeyPath:@"propertyName",
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOld
context:nil];

接收到属性变化通知时的回调 :

1
2
3
4
5
6
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// execute code
}

iOS 通过 Runtime 实现 KVO 机制 :

假设我们要观察对象 Computername 属性的变化,运行时 KVO 机制会动态的创建一个名为 NSKVONotifying_Computer 的新类,该类继承自 Computer 对象的本类,且 KVO 机制会重写 NSKVONotifying_Computername 属性的 setter 方法。这个重写的 setter 方法负责在调用 Computersetter 方法的之前和之后,通知所有观察者 name 属性的更改情况。

在这个过程中,Computerisa 指针被 KVO 机制修改为指向 NSKVONotifying_Computer 类,来实现当前类属性值改变的监听。

KVO 机制的键值观察通知依赖于 NSObjectwillChangeValueForKey:didChangeValueForKey: 两个方法。

工厂模式

工厂模式是常用的实例化对象模式,是用工厂方式代替 new 操作的一种模式。

简单工厂

简单工厂模式,又称静态工厂方法模式,是一种创建型模式,是由一个工厂对象决定创建出哪一种产品类的实例。

简单工厂的优缺点
  • 优点 :简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。明确区分了各自的指责和权利,有利于整个软件体系结构的优化。
  • 缺点 :工厂类集中了所有实例的创建逻辑,包含了过度的判断条件,维护起来不方便。
简单工厂的实现
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
typedef NS_ENUM(NSInteger, InstanceType) {
InstanceType_001 = 0,
InstanceType_002 = 1,
... = ...
InstanceType_n = n-1
}

+ (id)getInstanceWithParameter:(InstanceType)someType {
Switch (someType) {
case : InstanceType_001
reture [self initInstance_001];
break;
...
case : InstanceType_n
reture [self initInstance_n];
break;
default :
break;
}
}

- (id)initInstance_001 {
...
}

...

- (id)initInstance_n {
...
}

工厂方法

工厂方法模式,又称多态性工厂模式。在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂的角色,仅负责给出工厂子类必须实现的接口,而不负责哪一个产品类应当被实例化这种细节。

工厂方法模式就是简单工厂模式的衍生,解决了许多简单工厂模式的问题。它在基类中建立一个抽象方法,子类可以通过改写这个方法来改变创建对象的具体过程。工厂方法模式让子类来决定如何创建对象,来达到封装的目的。

工厂方法的优点
  • 优点 :
    • 子类提供挂钩。基类为工厂方法提供缺省实现,子类可以重写新的实现,也可以集成父类的实现,增加了灵活性。
    • 屏蔽产品类。产品类的实现如何变化,调用者都不需要关心,只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不会发生变化。
    • 典型的解耦框架。高层模块只需要知道产品的抽象类,其它的实现类都不需要关心。
    • 多态性,客户代码可以做到与特定应用无关,适用于任何实体类。

抽象工厂模式

抽象工厂模式提供一个接口,用于创建一个对象家族,而无需指定具体类。工厂方法只涉及到创建一个对象的情况,有时候我们需要一族对象。

抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。根据里氏替换原则,任何接受父类型的地方,都应当能够接受子类型。因此,实际上系统所需要的,仅仅是类型与这些抽象产品角色相同的一些实例,而不是这些抽象产品的实例。换言之,也就是这些抽象产品的具体子类的实例。工厂类负责创建抽象产品的具体子类的实例。

抽象工厂的优点/缺点
  • 优点:
    • 抽象工厂模式隔离了具体类的生产,使得客户并不需要知道什么被创建。
    • 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
    • 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
  • 缺点:增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。

事件传递与响应链探索


iOS App 使用响应者对象( Responder)来接收和处理事件。一个响应者对象是 UIResponder 类的实例,它常见的子类包括 UIViewUIViewControllerUIApplication。响应者对象接收原始事件数据,并且必须处理该事件或将它转发给另一个响应者对象。

当你的 App 接收到一个事件时,UIKit 自动将它发送给最适合的响应者对象,即第一响应者对象(First Responder)。未处理的事件从响应者对象被发送到正在活动的响应链中的响应者对象(响应者链是应用程序的响应者对象的动态配置,在应用程序中没有单个响应者链)。

UIKit 为事件如何从一个响应者对象传递到另一个响应者对象预定义了规则,但是你可以通过覆盖响应者对象中的属性来改变这些规则。


响应者对象 (Responder Object)

Responder 对象是 UIResponder 类的实例,构成了 UIKit 应用程序事件处理的主干。许多重要对象也是响应者,包括 UIApplication 对象,UIViewController 对象和所有 UIView 对象(包括 UIWindow)。当事件发生时,UIKit 将它们分派给你的应用程序的响应者对象进行处理。
UIResponder 对象能处理的事件,包括触摸事件,动作事件,远程控制事件和新闻事件。

为了处理特定类型的事件,响应者必须覆盖相应的方法。例如,为了处理触摸事件,响应者实现了 touchesBegan:with:touchesMoved:with:touchesEnded:with:touchesCancelled:with:方法。在触摸事件发生时,响应者使用 UIKit 提供的事件信息来跟踪触摸的变化,并适当地更新应用的界面。

除了处理事件之外,UIKit响应者还负责将未处理事件转发到应用程序的其他部分。如果给定的响应者不处理事件,则将该事件转发给响应者链中的下一个响应者对象。 UIKit 动态地管理响应者链,使用预定义的规则来确定接下来哪个对象应该接收事件。

响应者处理 UIEvent 对象,但也可以通过输入视图接受自定义输入。系统的键盘是输入视图最明显的例子。当用户点击屏幕上的 UITextFieldUITextView 对象时,视图成为第一响应者并显示其输入视图,这是系统键盘。同样,你可以创建自定义输入视图,并在其它响应者激活时显示它们。要将自定义输入视图与响应者相关联,请将该视图分配给响应者的 inputView 属性。

NOTE

与加速度计、陀螺仪和磁力计相关的运动事件不遵循响应者链。Core Motion 会直接将这些事件发送给你指定的对象。


第一响应者对象 (The First Responder)

UIKit 为各种类型的事件指定第一响应者对象并在事件发生时首先发送到第一响应者对象。

  • Touch events

    第一响应者对象是触摸发生的视图

  • Press events

    第一响应者对象是获得焦点的响应者对象

  • Shake-motion events

    第一响应者对象是你或 UIKit 指定的对象

  • Remote-control events

    第一响应者对象是你或 UIKit 指定的对象

  • Editing menu messages

    第一响应者对象是你或 UIKit 指定的对象

控件使用动作消息 (Action message) 与其关联的目标对象 (Target Object) 直接进行通信。当用户与控件进行交互时,控件调用其目标对象的动作方法 (Action Method),向目标对象(Target object)发送一个动作消息。动作消息不是一个事件,但它们仍可利用响应链。当控件的目标对象为 nil 时,UIKit 从目标对象开始,遍历响应者链,直到寻找到实现了相应方法的对象。

如果视图中有添加手势,那么手势会在视图接收之前接收触摸和按下事件。如果视图中的所有手势都无法处理这个事件,则将事件传递给视图进行处理。如果视图也不能处理,则 UIKit 会将事件传递给响应者链。


基于视图的点击测试 (View-Based Hit-Testing)

UIKit 使用基于视图的点击测试来确定触摸事件到底发生在哪里。UIKit 将触摸位置与处在视图层级中的视图的 bounds 进行比较。hitTest:withEvent: 方法遍历视图层级,寻找包含指定触摸的层级最深的子视图。 然后这个视图就成为第一响应者对象。

当用户触摸屏幕进行交互时,系统检测到手指触摸操作,并将触摸以 UIEvent 的方式加入 UIApplication 的事件队列中。UIApplication 从事件队列中取出最新的触摸事件进行分发传递到 UIWindow 进行处理。而 UIWindow 会通过 hitTest:withEvent: 方法寻找触点所在的视图,这个过程称之为 Hit-Test。

UIKit 将每个触摸永久绑定到包含它的视图对象。UIKit 在触摸第一次发生时创建 UITouch 对象,并只在触摸结束时释放它。当触摸位置或其它参数改变时,UIKit 使用新的信息来更新 UITouch 对象。唯一不改变的属性就是包含它的视图,即使触摸位置已经移出原始的视图。

Hit-Test 从顶级视图开始调用 pointInside:withEvevt: 方法判断触摸点是否在当前视图内,如果返回为 NO ,则 hitTest:withEvent: 方法返回 nil;如果返回 YES ,则向当前视图的所有子视图发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到最底层视图,直到有子视图返回非空对象或者全部子视图遍历完毕。

NOTE

如果一个触摸的位置超出了视图的边界,hitTest:withEvent: 方法就会忽略这个视图及其所有子视图。


响应者链 (Resoponder Chain)

当系统通过 Hit-Test 找到触摸点所在的视图,但是这个视图并没有或者无法正常处理此次触摸事件,这个时候,系统便会通过响应者链寻找下一个响应者,以对此次触摸事件进行响应。

https://github.com/apple272487813/codeLei.github.io/blob/master/images/iOS_Responder_Chain.jpg?raw=true

如果一个 View 有一个视图控制器,它的下一个响应者就是这个视图控制器,然后才是它的父视图,如果一直到根视图都没能处理这个事件,事件会传递到 UIWindow ,若在 UIWindow 中也没有处理,则会传递给 UIApplication ,它是一个响应者链的终点,它的下一个响应者指向 nil ,以劫数整个循环。


改变响应者链 (Altering the Responder Chain)

你可以通过覆盖你的响应者对象中的 nextResponder 属性来改变响应者链。当你这样做时,下一个响应者就是你返回的对象。

很多 UIKit 类已经覆盖了这个属性 :

  • UIView 对象。如果视图是视图控制器的根视图,那它的 nextResponder 是视图控制器,否则是视图的父视图。
  • UIViewController 对象。
    • 如果视图控制器的视图是一个窗口的根视图,nextResponder 是窗口对象。
    • 如果一个视图是被其它视图控制器推出的,nextResponder 是推出它的视图控制器。
  • UIWindow 对象。窗口的 nextResponderUIApplication 对象。
  • UIApplication 对象。只有在应用代理是 UIResponder 的实例而不是视图、视图控制器或者应用对象自身时,UIApplicationnextResponder 是应用程序代理。

Objective-C Runtime

Objective-C Runtime

动态语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化,类型的检查是在运行时做的,优点为方便阅读,清晰明了,缺点为不方便调试。

这里有三个名词容易被混淆:

Dynamic Programming Language,即动态语言或动态编程语言

Dynamically Typed Language,即动态类型语言

Statically Typed Language,即静态类型语言

所谓的动态类型语言,指的是类型的检查是在运行时做的;与此相对,静态类型语言的类型判断是在运行前判断(如编译阶段),C#、Java 等就是静态类型语言,静态类型的语言为了达到多态会采取一些类型鉴别手段,如接口、继承,而动态类型的语言却不需要,所以一般动态语言都会采用 dynamic typing,常出现于脚本语言中。

Objective-C 就是一门动态语言,它有一个运行时系统来执行编译的代码。这个运行时系统就是 Objc Runtime,它是一个由 C 和汇编语言编写的库。

Runtime 库的主要工作是封装和找出最终执行的代码。

  • 封装 :在 Objc Runtime 库中,对象可以用 C 中的结构体表示,而方法可以用 C 中的函数来实现,另外再加上一些额外的特性。这些结构体和函数被 Runtime 函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
  • 找出方法的最终执行代码:当程序执行 [object doSomething]时,会向消息接收者发送一条消息,Runtime 会根据消息接收者是否能响应该消息而做出不同的反应。

Objective-C Runtime 目前有两个版本:Modern RuntimeLegacy RuntimeModer Runtime覆盖了64位的 Mac OS X AppsiOS AppsLegacy Runtime是早期用来给32位的 Mac OS X Apps用的。

Objective-C Runtime 中的类与对象

类与对象的基本数据结构

Class

Objective-C 中的类是由 Class 类型来表示的,实际上是一个指向 objc_class 结构体的指针

1
typedef struct objc_class	*Class;	

它的结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct	objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

这些字段的意义如下:

  • isaObjective-C 中所有类自身也是对象,这个对象的 Class 里也有一个 isa 指针,它指向 metaClass 元类。
  • super_class :指向该类的父类,如果该类已经是最顶层的根类,则为 NULL
  • name :类名。
  • version :类的版本信息,默认为0。我们可以使用这个字段来提供类的版本信息,这对于对象的序列化非常有用,它可以让我们识别出不同类定义版本中实例变量布局的改变。
  • info :类信息,供运行期使用的一些位标识。
  • instance_size :类的实例变量的大小。
  • ivars :类的成员变量链表。
  • methodLists :方法定义的链表。
  • cache :用于缓存最近使用的方法。当一个接收者对象接收到一个消息时,它会根据 isa 指针去查找能够响应这消息的对象。在实际情况里,这个对象只有一部分方法是常用的,很多方法其实很少用或根本用不上。这种情况下,如果每次消息来时,都是在 methodLists 链表中遍历一遍,性能势必很差。更好的做法是,在我们每次调用过一个方法后,就将这个方法缓存到 cache 列表中,下次调用就会优先去 cache 中查找。
  • protocols :协议链表。

当我们向一个 Objective-C 对象发送消息时,Runtime 会根据实例对象的 isa 指针找到这个实例对象所属的类,然后 Runtime 会在类的方法列表及其父类的方法列表中寻找与消息对应的 selector 指向的方法,找到后即运行这个方法。

当创建一个特定类的实例对象时,分配的内存会包含一个 objc_object 数据结构,然后是类的实例变量的数据。NSObject 类的 allocallocWithZone: 方法使用函数 class_createInstance 来创建 objc_object 数据结构。

1
2
3
4
5
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
}

typedef struct objc_object *id;

常见的 id 是一个 objc_object 结构类型的指针。

objc_cache

objc_class 中的 cache 字段用于缓存调用过的方法,它是一个指向 objc_cache 结构体的指针。

1
2
3
4
5
struct	objc_cache {
unsigned int mask OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buchets[1] OBJC2_UNAVAILABLE;
}

该结构体的字段描述如下:

  • mask :一个整数,指定分配的缓存 bucket 的总数。在方法查找过程中,Runtime 使用这个字段来确定开始线性查找数组的索引位置。指向方法的 selector 指针与该字段做一个 AND 位操作。
  • occupied :一个整数,指定实际占用的缓存 bucket 的总数。
  • buckets : 指向 Method 数据结构指针的数组。这个数组可能包含不超过 mask+1 个元素。如果指针为 NULL,表明这个缓存 bucket 没有被占用,另外被占用的 bucket 可能是不连续的。

元类(Meta Class)

所有的类自身也是一个对象,我们也可以向这个对象发送消息。

当我们向一个对象发送消息时,Runtime 会在这个对象所属的类的方法列表中查找方法;而向一个类发送消息时,会在这个类的 meta-class 的方法列表中查找。

meta-class 存储着一个类的所有类方法。每个类都会有个一个单独的 meta-class ,因为每个类的类方法基本不可能完全相同。

meta-classisa 指针指向基类的 meta-class ,以此作为它的所属类。可以理解为,任何 NSObject 继承体系下的 meta-class 都使用 NSObjectmeta-class 作为自己的所属类,而基类的 meta-classisa 指针则指向它自己。

对于 NSObject 继承体系来说,其实例方法对体系中的所有实例、类和 meta-class 都是有效的;而类方法对于体系内的所有类和 meta-class 都是有效的。

类与对象操作函数

Runtime 提供了大量函数来操作类与对象。类的操作方法大部分以 class_ 为前缀,对象的操作方法大部分是以 objc_object_ 为前缀。

类名

1
2
// 获取类的类名
const char * class_getName (Class cls);

对于 class_getName 函数来说,如果传入的 clsnil,则返回一个字字符串。

父类(super_class)和元类(meta-class)

1
2
3
4
// 获取类的父类
Class class_getSuperClass (Class cls);
// 判断给定的类是否是元类
BOOL class_isMetaClass (Class cls);
  • class_getSuperClass 函数,当clsnilcls 为根类时,返回 nil
  • class_isMetaClass 函数,如果 cls 是元类,返回 YES。如果传入的 clsnil,则返回 NO

实例变量大小(instance_size)

1
2
// 获取实例大小
size_t class_getInstanceSize (Class cls);

成员变量(ivar)及属性

objc_class 中,所有的成员变量、属性的信息都是放在链表 ivars 中。ivars 是一个数组,数组中的每个元素是指向 ivar 的指针。

  • 成员变量操作函数

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取类中指定名称实例成员变量的信息
    Ivar class_getInstanceVariable (Class cls, const char *name);
    // 获取类成员变量的信息
    Ivar class_getClassVariable (Class cls, const char *name);
    // 添加成员变量
    BOOL class_addIvar (Class cls, const char *name, size_t size, uint8_t alignment, const char *types);
    // 获取整个成员变量列表
    Ivar * class_copyIvarList (Class cls, unsigned int *outCount);
    • class_getInstanceVariable 函数,它返回一个指向包含 name 指定的成员变量信息的 objc_ivar 结构体的指针 Ivar
    • class_getClassVariable 函数,目前没有找到关于 Objective-C 中类变量的信息,一般认为 Objective-C 不支持类变量。
    • Objective-C 不支持向已经存在的类中添加实例变量,但如果通过 Runtime 来创建一个类的话,就可以使用 class_addIvar 函数了。需要注意的是,这个函数只能在 objc_allocateClassPairobjc_registerClassPair 之间调用。另外这个类也不能是元类。
    • class_copyIvarList ,返回一个包含成员变量信息的数组,数组中的每个元素是指向该成员变量信息的 objc_ivar 结构体的指针。这个数组不包含在父类中声明的变量。outCount 指针返回数组的大小。需要注意的是,我们必须使用 free() 来释放这个数组。
  • 属性操作函数

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取指定的属性
    objc_property_t class_getProperty (Class cls, const char *name);
    // 获取属性列表
    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);
  • Mac OS X 系统中,Runtime 提供了几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理 strong/weak 引用。

    1
    2
    3
    4
    const	uint8_t * class_getIvarLayout	(Class cls);
    void class_setIvarLayout (Class cls, const uint8_t *layout);
    const uint_8 * class_getWeakIvarLayout (Class cls);
    void class_setWeakIvarLayout (Class cls, const uint8_t *layout);

    通常情况下,我们不需要去主动调用这些方法,在调用 objc_registerClassPair 时,会生成合理的布局。

方法(methodLists)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 添加方法
BOOL class_addMethod (Class cls, SEL name, IMP imp, const char *types);
// 获取实例方法
Method class_getInstanceMethod (Class cls, SEL name);
// 获取类方法
Method class_getClassMethod (Class cls, SEL name);
// 获取所有方法的数组
Method * class_copyMethodList (Class cls, unsigned int *outCount);
// 替代方法的实现
IMP class_replaceMethod (Class cls, SEL name, IMP imp, const char *types);
// 返回方法的具体实现
IMP class_getMethodImplementation (Class cls, SEL name);
IMP class_getMethodImplementation_stret (Class cls, SEL name);
// 类实例是否响应指定的 selector
BOOL class_respondsToSelector (Class cls, SEL sel);
  • class_addMethod 的实现会覆盖父类的方法实现,但不会取代本类中已经存在的实现,如果本类中包含一个同名的实现,则函数会返回 NO。如果要修改已经存在的实现,可以使用 method_setImplementation。一个 Objectice-C 方法是一个简单的 C 函数,它至少包含两个参数 self_cmd,所以我们实现函数至少需要两个参数。

    1
    2
    3
    void myMethodIMP (id self, SEL _cmd) {
    // implementation
    }

    与成员变量不同的是,我们可以动态的为类添加方法,不管这个类是否已经存在。

    参数 types 是一个描述传递给方法的参数类型的字符数组。

  • class_getInstanceMethodclass_getClassMethod 函数,与 class_copyMethodList 不同的是,这两个函数都会去搜索父类的实现。

  • class_copyMethodList 函数,返回包含所有实例方法的数组,如果需要获取类方法,则可使用 class_copyMethodList(object_getClass(Class cls), &count) (一个类的实例方法是定义在元类中的)。该列表不包含父类实现的方法。outCount 参数返回方法的个数。在获取到列表后,我们需要使用 free() 来释放它。

  • class_replaceMethod 函数,该函数的行为可以分为两种 :如果类中不存在参数 name 指定的方法,则类似于 class_addMethod 函数一样会添加方法;如果类中已经存在 name 指定的方法,则类似于 method_setImplementation 一样替代原方法的实现。

  • class_getMethodImplementation 函数,该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数的指针。这个函数会比 method_getImplementation(Class cls, const char * name) 更快。返回的函数指针可能是一个指向 Runtime 内部的函数,而不一定是方法的实际实现。

  • class_respondsToSelector 函数,我们经常使用 NSObject 类的 respondsToSelector:instanceRespondToSelector: 方法来达到同样的目的。

协议 (objc_protocol_list)

协议相关的操作包含以下函数::

1
2
3
4
5
6
// 添加协议
BOOL class_addProtocol (Class cls, Protocol *protocol);
// 返回类是否实现指定的协议
BOOL class_conformsToProtocol (Class cls, Protocol *protocol);
// 返回类实现的协议列表
Protocol *class_copyProtocolList (Class cls, unsigned int *outCout);

版本(version)

版本相关的操作函数:

1
2
3
4
// 获取版本号
int class_getVersion (Class cls);
// 设置版本号
void class_setVersion (Class cls, int version)

动态创建类和对象

Runtime 经常被用到的功能就是在运行时创建类和对象。

动态创建类

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

1
2
3
4
5
6
// 创建一个新类和元类
Class objc_allocateClassPair (Class superclass, const char *name, size_t extraBytes);
// 销毁一个类及其相关的类
void objc_disposeClassPair (Class cls);
// 在应用中注册由 objc_allocateClassPair 创建的类
void objc_registerClassPair (Class cls);
  • objc_allocateClassPair 函数 :如果我们要创建一个根类,则指定 superClassNilextraBytes 通常指定为 0 ,该参数是分配给类和元类对象尾部的索引 ivars 的字节数。

为了创建一个新类,我们需要调用 objc_allocateClassPair ,然后使用诸如 class_addMethod , class_addIvar 等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用 objc_registerClassPair 函数来注册类,之后这个类就可以在程序中使用了。

实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。

  • objc_disposeClassPair 函数用于销毁一个类,值得注意的是,如果程序运行中还存在类或其子类的实例,则不能对针对类调用该方法。

动态创建对象

动态创建对象的函数如下:

1
2
3
4
5
6
// 创建类实例
id class_createInstance (Class cls, size_t extraBytes);
// 在指定位置创建类实例
id objc_constructInstance (Class cls, void *bytes);
// 销毁类实例
void * objc_destructInstance (id obj);
  • class_createInstance 函数 :创建实例时,会在默认的内存区域为类分配内存。extraBytes 表示分配的额外字节数。这些额外的字节数可用于存储在类定义中所定义的实例变量之外的实例变量。该函数在ARC环境下无法使用。

    调用 class_createInstance 的效果和 +alloc 方法类似。

  • objc_constructInstance 函数 :在指定的位置(bytes)创建类实例。

  • objc_destructInstance 函数 :销毁一个类的实例,但不会释放并移除任何与其先关的引用。

实例操作函数

实例操作函数主要是针对创建的实例对象的一系列操作函数。

针对整个对象进行操作的函数

1
2
3
4
// 返回指定对象的一份拷贝
id object_copy (id obj, size_t size);
// 释放指定对象占用的内存
id object_dispose (id obj);

针对对象实例变量进行操作的函数

1
2
3
4
5
6
7
8
9
10
// 修改类实例的实例变量的值
Ivar object_setInstanceVariable (id obj, const char *name, void *value);
// 获取对象实例变量的值
Ivar object_getInstanceVariable (id obj, const char *name, void **outValue);
// 返回指向给定对象分配的任何额外字节的指针
void * object_getIndexedIvars (id obj);
// 返回对象中实例变量的值
id object_getIvar (id obj, Ivar ivar);
// 设置对象中实例变量的值
void object_setIvar (id obj, Ivar ivar, id value);

如果实例变量的 Ivar 已经知道,那么调用 objc_getIvar 会比 objc_getInstanceVariable 函数快。相同情况下,object_setIvar 也比 object_setInstanceVariable 快。

针对对象的类进行操作的函数

1
2
3
4
5
6
// 返回给定对象的类名
const char * object_getClassName (id obj);
// 返回对象的类
Class object_getClass (id obj);
// 设置对象的类
Class object_setClass (id obj, Class cls);

获取类定义

Objective-C Runtime 会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用 objc_addClass 来注册它们。

获取类定义的相关函数

1
2
3
4
5
6
7
8
9
10
// 获取以注册的类定义的列表
int objc_getClassList (Class *buffer, int bufferCount);
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList (unsigned int *outCount);
// 返回指定类的类定义
Class objc_lookUpClass (const char *name);
Class objc_getClass (const char *name);
Class objc_getRequiredClass (const char *name);
// 返回指定类的元类
Class objc_getMetaClass (const char *name);
  • objc_getClassList 函数 :获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自 NSObject 体系的,所以在这些类上调用方法时,都应该先检测一下这个方法是否在这个类中实现。
  • 获取类定义的方法有三个 :objc_lookUpClass , objc_getClassobjc_getRequiredClass。如果类在运行时未注册,则 objc_lookUpClass 会返回 nil,而 objc_getClass 会调用类处理回调,并在此确认类是否注册,如果确认未注册,再返回 nil。而 objc_getRequiredClass 函数的操作与 objc_getClass 相同,只不过没有找到类,就会杀死进程。
  • objc_getMetaClass 函数 :如果指定的类没有注册,则该函数会调用类处理回调,并在此确认类是否注册,如果确认未注册,再返回 nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义。不管它是否有效。

Objective-C Runtime 中的成员变量与属性

类型编码(Type Encoding)

作为对 Runtime 的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的 selector 关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用 @encod 编译器指令来获取它。当给定一个类型时,@encode 会返回这个类型的字符串编码。

成员变量、属性

Runtime 中关于成员变量和属性的相关数据结构并不多,只有三个。

基础数据类型

Ivar

Ivar 是表示实例变量的类型,其实际上是一个指向 objc_ivar 结构体的指针 :

1
2
3
4
5
6
7
8
9
10
11
typedef struct	objc_ivar	*Ivar;

struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 变量名
char *ivar_type OBJC2_UNAVAILABLE; // 变量类型
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节

#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
objc_property_t

objc_property_t 是声明属性的类型,是一个指向 objc_property 结构体的指针 :

1
typedef	struct	objc_property	*objc_property_t;
objc_property_attribute_t

objc_property_attribute_t 定义了属性的特性,它是一个结构体:

1
2
3
4
typedef	struct {
const char *name; // 特姓名
const char *value; // 特性值
}

关联对象(Associated Object)

关联对象是Runtime 中一个非常实用的特性。关联对象类似于成员变量,不过是在运行时添加的。我们通常会把变量(Ivar)放在类声明的头文件中,或者放在类实现的 @implementation 后面。但这有一个缺点,我们不能在分类中添加成员变量。Objective-C 针对这一问题,提出了一个解决方案:即关联对象(Associated Object)。

我们可以把关联对象想象成一个Objective-C 对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void 指针。我们还需要指定一个内存管理策略,告诉Runtime 如何管理这个对象的内存,这个内存管理策略可以由一下指定:

1
2
3
4
5
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY

当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。如果指定的是 OBJC_ASSOCIATION_ASSIGN

,则宿主释放时,关联对象不会被释放;而如果指定的是 retain 或者是 copy ,则宿主释放时,关联对象会被释放。

将一个对象连接到其它对象所需的就是下面两行代码:

1
2
static char aKey;
objc_setAssociatedObject(self, &aKey, anObject, OBJC_ASSOCIATION_RETAIN);

在这种情况下,self 对象将获取一个新的关联对象 anObject ,且内存管理策略是自动 retian 管理对象,当self 对象释放时,会自动 release 关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,在这种情况下,先前的关联对象会被妥善的处理掉,并且新的对象会使用它的内存。

1
2
// 获取指定key的关联对象
id anObject = objc_getAssociatedObject(self, &aKey);

使用 objc_removeAssociatedObjects 函数来移除一个关联对象,或者使用 objc_setAssociatedObject 函数将key指定的关联对象设置为nil

成员变量、属性的操作方法

成员变量

成员变量的操作函数包括 :

1
2
3
4
5
6
// 获取成员变量名
const char * ivar_getName (Ivar v);
// 获取成员变量类型编码
const char * ivar_getTypeEncoding (Ivar v);
// 获取成员变量的偏移量
ptrdiff_t ivar_getOffset (Ivar v);
  • ivar_getOffset 函数,对于 id 或其它对象类型的实例变量,可以调用 object_getIvarobject_setIvar 来直接访问成员变量,而不使用偏移量。
关联对象

关联对象的操作函数包括 :

1
2
3
4
5
6
// 设置关联对象
void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy);
// 获取关联对象
id objc_getAssociatedObject (id object, const void *key);
// 移除关联对象
void objc_removeAssociatedObjects (id object);
属性

属性操作相关函数包括 :

1
2
3
4
5
6
7
8
// 获取属性名
const char * property_getName (objc_property_t property);
// 获取属性特性描述字符串
const char * property_getAttributes (objc_property_t property);
// 获取属性中指定的特性
char * property_copyAttributeValue (objc_property_t property, const char *attributeName);
// 获取属性的特性列表
objc_property_attribute_t * property_copyAttributeList (objc_property_t property, unsigned int *outCount);
  • property_copyAttributeValue 函数,返回的 char* 在使用完后需要调用 free() 释放。
  • property_copuAttributeList 函数,返回值在使用完后需要调用 free() 释放。

Objective-C Runtime 中的方法与消息

基础数据类型

SEL

SEL 又叫做选择器,是表示一个方法的 selector 的指针,定义如下:

1
typedef struct objc_selector *SEL;

方法的 selector 用于表示运行时方法的名字。Objective-C 在编译时,会根据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int 类型的地址),这个标识就是 SEL

两个类之间,不管它们是不是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的 SEL 就是一样的。每一个方法都对应着一个 SEL。所以在 Objective-C 的同一个类(及类的继承体系)中,不能存在两个同名的方法,即使参数类型不同也不行。因为相同的方法只能对应一个 SEL,这导致 Objectice-C 在处理相同方法名且参数个数相同但类型不同的方法方面能力很差。

当然不同的类可以拥有相同的 selector ,这个没有问题。不同类的实例对象执行相同的 selector 时,会在各自的方法列表中根据 selector 去寻找自己对应的 IMP

工程中的所有的 SEL 组成了一个 Set 集合,Set 特点就是唯一,因此 SEL 也是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的 SEL 就行了,SEL 实际上就是根据方法名 hash 化了的一个字符串,而对于字符串的比较仅仅需要比较它们的地址就可以了。

本质上,SEL 只是一个指向方法的指针(准确地说,只是一个根据方法名 hash 化了的 KEY 值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。

我们可以在运行时添加新的 selector ,也可以在运行时获取已经存在的 selector ,我们可以通过下面三种方式来获取 SEL :

  • sel_registerName 函数
  • Objective-C 编译器提供的 @selector()
  • NSSelectorFromString() 方法

IMP

IMP 实际上是一个函数指针,指向函数实现的首地址。

1
id (*IMP)(id, SEL, ...)

这个函数使用当前 CPU 架构实现的标准 C 调用约定。第一个参数是指向 self (如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)的指针,第二个参数是 selector ,接下来是方法的实际参数列表。

通过取得 IMP ,我们可以跳过 Runtime 的消息传递机制,直接执行 IMP 指向的函数实现,这样就省去了 Runtime 消息传递过程中所做的一系列查找操作。

Method

Method 用于表示类定义中的方法

1
2
3
4
5
6
7
typedef struct objc_method *Method;

struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE; //
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}

方法操作相关函数

Runtime 提供了一系列的方法来处理与方法相关的操作,包括方法本身及 SEL

方法

方法的相关操作函数如下:

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
// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
  • method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementationmethod_getName更快。
  • method_getName函数,返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))
  • method_getReturnType函数,类型字符串会被拷贝到dst中。
  • method_setImplementation函数,注意该函数返回值是方法之前的实现。

方法选择器

选择器的相关操作函数包括 :

1
2
3
4
5
6
7
8
// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );
// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
  • sel_registerName函数:在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。

方法调用流程

Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式 [receiver message] 转化为一个消息调用的函数,即 objc_msgSend 。这个函数将消息接收者和方法名作为其基础参数。

1
objc_msgSend(receiver, selector)

如果消息中还有其它参数,则该方法的形式如下

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

这个函数完成了动态绑定的所有事情:

  • 首先它找到 selector 对应的方法实现。因为同一个方法在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到确切的实现。
  • 它调用方法实现,并将接收者对象及方法的所有参数传递给它。
  • 最后,它将实现返回的值作为它自己的返回值。

消息的关键在于结构体 objc_class ,这个结构体中有两个字段是我们在分发消息时关注的:

  • 指向父类的指针
  • 一个类的方法分发表,即 methodLists

当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa 指针也会被初始化,让对象可以访问类及类的继承体系。

当消息发送给一个对象时,objc_msgSend 通过对象的 isa 指针获取到类的结构体,然后在方法列表里查找方法的 selector 。如果没有找到 selector ,则通过 objc_class 结构体中指向父类的指针找到其父类,并在父类的方法列表里寻找方法的 selector 。依此,会一直沿着类的的继承体系到达 NSObject 类。一旦定位到 selector ,函数就会获取到实现的入口点,并传入相应的参数来执行方法的具体实现。如果没有定位到 selector ,则会走消息转发流程。

为了加速消息的处理,运行时系统缓存使用过的 selector 及对应的方法的地址。

隐藏参数

objc_msgSend 有两个隐藏参数 :

  • 消息接受对象
  • 方法的 selector

这两个参数为方法的实现提供了调用者的信息,之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明,而是在编译期被插入实现代码的。

虽然这些参数没有声明,但是我们仍然能在代码中引用它们。我们可以用 self 来引用接收者对象,使用 _cmd 来引用选择器。

获取方法地址

Runtime 中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。

我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。

NSObject 类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。

我们通过以下代码来看看methodForSelector:的使用:

1
2
3
4
5
6
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0 ; i < 1000 ; i++)
setter(targetList[i], @selector(setFilled:), YES);

这里需要注意的就是函数指针的前两个参数必须是idSEL

当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,以提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。

消息转发

当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform...的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:

1
2
3
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}

不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。

当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,并由 NSObjectdoesNotRecognizeSelector 方法抛出 ‘unrecognized selector sent to instance’ 错误信息。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发

下面我们详细讨论一下这三个步骤。

动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}

不过这种方案更多的是为了实现@dynamic属性。

备用接收者

如果在上一步无法处理消息,则 Runtime会继续调以下方法 :

1
- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

forwardInvocation:方法的实现有两个任务:

  1. 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
  2. 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

还有一个很重要的问题,我们必须重写以下方法:

1
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}

NSObjectforwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

消息转发与多重继承

回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。

不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:isKindOfClass:只能用于继承体系,而不能用于转发链。如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector])
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

Objective-C Runtime Method Swizzling

Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。

这种情况下,我们就可以使用Method Swizzling,如在代码所示:

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
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}

@end

在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。

上面的例子很好地展示了使用method swizzling来向一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling,在此不多举例。在此我们说说使用method swizzling需要注意的一些问题:

Swizzling应该总是在+load中执行

Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没有给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

选择器、方法与实现

Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。

以下是Objective-C Runtime Reference中的对这几个术语一些描述:

  1. Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
  2. Method(typedef struct objc_method *Method):在类定义中表示方法的类型
  3. Implementation(typedef id (*IMP)(id, SEL, ...)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。第一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。

理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。

为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。

调用 _cmd

我们回过头来看看前面新的方法的实现代码:

1
2
3
4
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

乍看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。

注意事项

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

  1. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
  2. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
  3. 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
  4. 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。

Objective-C Runtime 中的协议与分类

Objective-C 中的分类允许我们通过给一个类添加方法来扩充它(但是通过category不能添加新的实例变量),并且我们不需要访问类中的代码就可以做到。

Objective-C 中的协议是普遍存在的接口定义方式,即在一个类中通过@protocol定义接口,在另外类中实现接口,这种接口定义方式也称为“delegation”模式,@protocol声明了可以被其他任何方法类实现的方法,协议仅仅是定义一个接口,而由其他的类去负责实现。

基础数据类型

Category

Category 是表示一个指向分类的结构体的指针。

1
2
3
4
5
6
7
8
9
typedef struct objc_category *Category;

struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。

Protocol

Protocol 的定义如下

1
typedef struct objc_object Protocol;

可以看到,Protocol 其实就是一个对象结构体。

操作函数

Runtime并没有在<objc/runtime.h>头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在objc_class中,我们可以通过针对objc_class的操作函数来获取分类的信息。

而对于Protocol,runtime提供了一系列函数来对其进行操作,这些函数包括:

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
// 返回指定的协议
Protocol * objc_getProtocol ( const char *name );
// 获取运行时所知道的所有协议的数组
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 创建新的协议实例
Protocol * objc_allocateProtocol ( const char *name );
// 在运行时中注册新创建的协议
void objc_registerProtocol ( Protocol *proto );
// 为协议添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一个已注册的协议到协议中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 为协议添加属性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回协议名
const char * protocol_getName ( Protocol *p );
// 测试两个协议是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 获取协议中指定条件的方法的方法描述数组
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 获取协议中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 获取协议中的属性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 获取协议的指定属性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 获取协议采用的协议
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看协议是否采用了另一个协议
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
  • objc_getProtocol函数,需要注意的是如果仅仅是声明了一个协议,而未在任何类中实现这个协议,则该函数返回的是nil
  • objc_copyProtocolList函数,获取到的数组需要使用free()来释放
  • objc_allocateProtocol函数,如果同名的协议已经存在,则返回nil
  • objc_registerProtocol函数,创建一个新的协议后,必须调用该函数以在运行时中注册新的协议。协议注册后便可以使用,但不能再做修改,即注册完后不能再向协议添加方法或协议

需要强调的是,协议一旦注册后就不可再修改,即无法再通过调用protocol_addMethodDescriptionprotocol_addProtocolprotocol_addProperty往协议中添加方法等。

Objective-C Runtime 补充知识

super

Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super

如何使用super我们都知道。现在的问题是,它是如何工作的呢?

首先我们需要知道的是superself不同。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。为了理解这一点,我们先来看看super的定义:

1
struct objc_super { id receiver; Class superClass; };

这个结构体有两个成员:

  1. receiver:即消息的实际接收者
  2. superClass:指针当前类的父类

当我们使用super来接收消息时,编译器会生成一个objc_super结构体。就上面的例子而言,这个结构体的receiver就是当前对象,与self相同;superClass指向当前类的父类。

接下来,发送消息时,不是调用objc_msgSend函数,而是调用objc_msgSendSuper函数,其声明如下:

1
id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );

该函数第一个参数即为前面生成的objc_super结构体,第二个参数是方法的selector。该函数实际的操作是:从objc_super结构体指向的superClass的方法列表开始查找viewDidLoadselector,找到后以objc->receiver去调用这个selector,而此时的操作流程就是如下方式了

1
objc_msgSend(objc_super->receiver, @selector(viewDidLoad))

由于objc_super->receiver就是self本身,所以该方法实际与下面这个调用是相同的:

1
objc_msgSend(self, @selector(viewDidLoad))

库相关操作

库相关的操作主要是用于获取由系统提供的库相关的信息,主要包含以下函数:

1
2
3
4
5
6
// 获取所有加载的Objective-C框架和动态库的名称
const char ** objc_copyImageNames ( unsigned int *outCount );
// 获取指定类所在动态库
const char * class_getImageName ( Class cls );
// 获取指定库或框架中所有类的类名
const char ** objc_copyClassNamesForImage ( const char *image, unsigned int *outCount )

通过这几个函数,我们可以了解到某个类所有的库,以及某个库中包含哪些类。

块操作

我们都知道block给我们带到极大的方便,苹果也不断提供一些使用block的新的API。同时,苹果在runtime中也提供了一些函数来支持针对block的操作,这些函数包括:

1
2
3
4
5
6
// 创建一个指针函数的指针,该函数调用时会调用特定的block
IMP imp_implementationWithBlock ( id block );
// 返回与IMP(使用imp_implementationWithBlock创建的)相关的block
id imp_getBlock ( IMP anImp );
// 解除block与IMP(使用imp_implementationWithBlock创建的)的关联关系,并释放block的拷贝
BOOL imp_removeBlock ( IMP anImp );
  • imp_implementationWithBlock函数:参数block的签名必须是method_return_type ^(id self, method_args …)形式的。该方法能让我们使用block作为IMP

弱引用操作

1
2
3
4
// 加载弱引用指针引用的对象并返回
id objc_loadWeak ( id *location );
// 存储__weak变量的新值
id objc_storeWeak ( id *location, id obj );
  • objc_loadWeak函数:该函数加载一个弱指针引用的对象,并在对其做retainautoreleasing操作后返回它。这样,对象就可以在调用者使用它时保持足够长的生命周期。该函数典型的用法是在任何有使用__weak变量的表达式中使用。

    objc_storeWeak函数:该函数的典型用法是用于__weak变量做为赋值对象时。

宏定义

Runtime 中,还定义了一些宏定义供我们使用,有些值我们会经常用到,如表示BOOL值的YES/NO;而有些值不常用,如OBJC_ROOT_CLASS。在此我们做一个简单的介绍。

布尔值

1
2
#define YES  (BOOL)1
#define NO (BOOL)0

这两个宏定义定义了表示布尔值的常量,需要注意的是YES的值是1,而不是非0值。

空值

1
2
#define nil  __DARWIN_NULL
#define Nil __DARWIN_NULL

其中nil用于空的实例对象,而Nil用于空类对象。

分发函数原型

1
#define OBJC_OLD_DISPATCH_PROTOTYPES  1

该宏指明分发函数是否必须转换为合适的函数指针类型。当值为0时,必须进行转换

Objective-C根类

1
#define OBJC_ROOT_CLASS

如果我们定义了一个Objective-C根类,则编译器会报错,指明我们定义的类没有指定一个基类。这种情况下,我们就可以使用这个宏定义来避过这个编译错误。该宏在iOS 7.0后可用。

其实在NSObject的声明中,我们就可以看到这个宏的身影,如下所示:

1
2
3
4
5
6
7
8
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0)

OBJC_ROOT_CLASS
OBJC_EXPORT

@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}

我们可以参考这种方式来定义我们自己的根类。

局部变量存储时长

1
#define NS_VALID_UNTIL_END_OF_SCOPE

该宏表明存储在某些局部变量中的值在优化时不应该被编译器强制释放。

我们将局部变量标记为id类型或者是指向ObjC对象类型的指针,以便存储在这些局部变量中的值在优化时不会被编译器强制释放。相反,这些值会在变量再次被赋值之前或者局部变量的作用域结束之前都会被保存。

关联对象行为

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

Objective-C Runtime 实际应用

动态交换两个方法的实现

当第三方框架或系统原生方法功能不能满足我们的需求的时候,可以通过交换方法实现在保持原有方法功能的基础上,添加额外的功能。

1
2
3
4
5
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(originalMethodName));
Method targetMethod = class_getClassMethod(self, @selector(targetMethodName));
method_exchangeImplementations(originalMethod, targetMethod);
}

动态添加属性

属性赋值的本质就是让属性与一个对象产生关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface NSObject (AssociatedProperty)

@property NSString *propertyName;

@end

@implementation NSObject (AssociatedProperty)

- (void)setPropertyName:(NSString *)stringValue {
objc_setAssociatedObject(self, @"propertyName", stringValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)propertyName {
return objc_getAssociatedObject(self, @"propertyName");
}

@end

实现字典转模型的动态转换

利用 Runtime 遍历模型中所有属性,根据模型的属性名,去字典中查找 key,去除对应的值,给模型的属性赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 最简单实现,不考虑字典中含有数组或对象的情况
+ (instancetype)modelWithDict:(NSDictionary *)dict {
id objc = [[self alloc] init];
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSString *key = [ivarName substringFromIndex:1];
id value = dict[key];
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}

动态添加方法

如果一个类的方法很多,加载类到内存的时候比较耗费资源,使用动态给类添加方法的方式可以解决。

1
2
3
4
5
6
7
8
9
void methodAddWhenRun(id self, SEL _cmd, id argument ···) {}

+ (BOOL)resolveInstanceMethod:(SEL)self {
if (sel == NSSelectorFromString(@"methodAddWhenRun")) {
class_addMethod(self, sel, (IMP)methodAddWhenRun, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}

动态变量控制

1
2
3
4
5
6
7
8
9
10
11
12
// 假设要修改 `_propertyName`
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
const char *varName = ivar_getName(ivar);
NSString *name = [NSString stringWithUTF8String:varName];
if ([name isEqualToString:@"_propertyName"]) {
object_setIvar([self class], var, (id)newValue);
break;
}
}

实现 NSCoding 的自动归解档

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)encodeWithCoder:(NSCoder *)encoder {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([TargetClass class], &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodObject:value forKey:key];
}
free(ivarList);
}

- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvaarList([TargetClass class], &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
const char *name = ivar_getName:(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivarList);
}
return self;
}

转载整理自 南风子的技术博客

参考 百度百科 动态语言

参考 白水ln的简书

缓存

强制缓存

强缓存策略在请求数据时,如果浏览器缓存中存在未失效的缓存数据,则直接从缓存中获取数据,不与服务器进行交互。只有在缓存中不存在要请求的数据或在缓存中的数据失效时,才会从服务器获取数据。

强制缓存01

强缓存由 Expires/Cache-control/Pragma 三个 Header 属性进行控制。

  • Expires

    Expires 的值是一个 HTTP 日期,表示资源的过期时间。

    在发起请求时,将 Expires 日期与系统时间进行对比,如果系统时间超过了 Expires 日期,则认为资源过期失效。

    但由于系统时间和服务器时间可能不一致,会造成判断不准的问题。

    Expires 在三个强缓存控制属性中优先级最低

  • Cache-control

    Cache-control 是 HTTP/1.1 中新增的特性,在请求头和响应头中都能使用,可用值如下

    • max-age

      max-age 的值是一个秒数,表示从起发起时到缓存过期的时间

      max-age=10

    • no-cache

      不使用强缓存,每次请求都需要和服务器验证

    • no-store

      禁止使用缓存(包括协商缓存),每次请求都向服务器请求最新的资源

    • private

      不允许中间代理、CDN等缓存

    • public

      允许中间代理、CDN等缓存

    • must-revalidate

      缓存过期后必须向服务器验证

  • Pragma

    Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜,在 3 个头部属性中的优先级最高

协商缓存

当浏览器的强缓存失效或在请求头中设置了不使用强缓存,并在请求头中设置了 if-modified-sinceif-none-match 时,会将这两个属性值到服务器验证是否命中协商缓存。

协商缓存01

  • ETag/If-None-Match

    ETag/If-None-Match 的值是一串 hash 码,代表的是一个资源的标识符,当服务端的文件变化的时候,它的 hash码会随之改变,通过请求头中的 If-None-Match 和当前文件的 hash 值进行比较,如果相等则表示命中协商缓存。

  • Last-Modified/If-Modified-Since

    Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到 Last-Modified 响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的 Last-Modified 的时间,并放到 If-Modified-Since 请求头属性中,服务端根据文件最后一次修改时间和 If-Modified-Since 的值进行比较,如果相等,返回 304 ,并加载浏览器缓存。

缓存用处

  • 减少了冗余的数据传递,节省宽带流量
  • 减少了服务器的负担,大大提高了网站性能
  • 加快了客户端加载网页的速度 这也正是HTTP缓存属于客户端缓存的原因