跳到主要内容

24 篇博文 含有标签「iOS」

查看所有标签

CocoaPods 安装和使用

· 阅读需 4 分钟
BY

最近换了新机器,重新搭建了开发环境,其中当然包括 CocoaPods

装完顺便更新下 CocoaPods 安装文档。

正文

安装

CocoaPods 是用 ruby 实现的,要想使用它首先需要有 ruby 的环境。

升级ruby

查看ruby版本 $ ruby -v

ruby 2.0.0p648 (2015-12-16 revision 53162) [universal.x86_64-darwin16]

CocoaPods需要2.2.2版本及以上的,我们先升级ruby。

使用 rvm 安装 ruby

curl -L get.rvm.io | bash -s stable source ~/.bashrc source ~/.bash_profile

切换 ruby 源

ruby 下载源使用亚马逊的云服务被墙了,切换国内的 ruby-china源<https://ruby.taobao.org/>已经停止维护,详情查看公告):

	$ gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/
$ gem sources -l
*** CURRENT SOURCES ***

https://gems.ruby-china.org

安装并切换 ruby

这里不建议安装最新的 2.4.0 版本,因为次版本的 ruby,在xcodebuild 自动打包时,会出现问题! 所以退一步,安装 2.3.3版本~

	rvm install 2.3.3 --disable-binary
rvm use 2.3.3 --default

到此ruby升级完毕.

有关RVM的使用可以看这篇 RVM 使用指南

安装CocoaPods

  1. 安装
		sudo gem install -n /usr/local/bin cocoapods
  1. 升级版本库
		pod setup

这里需要下载版本库(非常庞大),需要等很久

Receiving objects: 72% (865815/1197150), 150.07 MiB | 190.00 KiB/s

或者直接从其他装有cocoapod的电脑中拷贝~/.cocoapods到你的用户目录,然后再 pod setup会节省不少时间

使用

创建 podfile 文件

绝大多数人创建podfile都是用 vim Podfile 命令

其实pod 已经提供了创建 podfile 文件的命令,在工程目录下

pod init

将会自动生成 podfile 文件,并且为你写好了格式,稍做修改就能使用

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'projectName' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!

# Pods for projectName

target 'projectNameTests' do
inherit! :search_paths
# Pods for testing
end

target 'projectNameUITests' do
inherit! :search_paths
# Pods for testing
end

end

其中的

target 'projectNameTests' do
inherit! :search_paths
# Pods for testing
end

target 'projectNameUITests' do
inherit! :search_paths
# Pods for testing
end

是指定在单元测试和UI测试时导入的测试框架,若没有使用测试框架可以删除。

修改iOS版本,添加Alamofire

# Uncomment the next line to define a global platform for your project
# platform :ios, '8.0'

target 'projectName' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!

# Pods for projectName

pod 'Alamofire', '~> 4.4'

end

加载代码库

使用下面的命令,直接在本地版本库中查找对应的代码库信息,不升级版本库,节省时间

pod install --verbose --no-repo-update

若找不到库,再使用下面的命令

pod install

版本号

对版本号的操作除了指定与不指定,你还可以做其他操作:

  • \>0.1 高于0.1的任何版本
  • \>=0.1 版本0.1和任何更高版本
  • <0.1 低于0.1的任何版本
  • <=0.1 版本0.1和任何较低的版本
  • 〜>0.1.2 版本 0.1.2的版本到0.2 ,不包括0.2。 这个基于你指定的版本号的最后一个部分。这个例子等效于>= 0.1.2并且 <0.2.0,并且始终是你指定范围内的最新版本

结语

关于CocoaPods的安装和使用就这样简单的介绍完了,至于更多使用的方法(平时也用不到~)你可以用下面命令查看

$ pod

若对 CocoaPods 的个人仓库感兴趣,也可以看看我的这两篇博客

本文首次发布于 BY Blog, 作者 @柏荧(BY) ,转载请保留原文链接.

强化 swift 中的 print

· 阅读需 2 分钟
BY

在 Swift 中,最简单的输出方法就是使用 print(),在我们关心的地方输出字符串和值。

当程序变得非常复杂的时候,我们可能会输出很多内容,而想在其中寻找到我们希望的输出其实并不容易。我们往往需要更好更精确的输出,这包括输出这个 log 的文件,调用的行号以及所处的方法名字等等。

在 Swift 中,编译器为我们准备了几个很有用的编译符号,它们分别是:

符号类型描述
#fileString包含这个符号的文件的路径
#lineInt符号出现处的行号
#columnInt符号出现处的列
#functionString包含这个符号的方法名字

有了上面的这些编译符号,我们就可以自定义一个输出函数:printm

public func printm(items: Any..., filename: String = #file, function: String = #function, line: Int = #line) {
print("[\((filename as NSString).lastPathComponent) \(line) \(function)]\n",items)
}

因为输出是一个很消耗性能的操作,所以在releass环境下需要将输出函数去掉,将上面的函数换成:

#if DEBUG

public func printm(items: Any..., filename: String = #file, function: String = #function, line: Int = #line) {
print("[\((filename as NSString).lastPathComponent) \(line) \(function)]\n",items)
}

#else

public func printm(items: Any..., filename: String = #file, function: String = #function, line: Int = #line) { }

#endif

参考:

本文首次发布于 BY Blog, 作者 @柏荧(BY) ,转载请保留原文链接.

Swift 3.1 的新变化「译」

· 阅读需 12 分钟
BY

Xcode 8.3 和 Swift 3.1 现在已经发布了(3/28)!

可以通过 AppStoreApple Developer 进行下载

Xcode 8.3 优化了 Objective-C 与 Swift 混编项目的编译速度.

Swift 3.1 版本包含一些期待已久的 Swift package manager 功能和语法本身的改进。

如果您没有密切关注 Swift Evolution 进程,请继续阅读 - 本文非常适合您!

在本文中,我将强调Swift 3.1中最重要的变化,这将对您的代码产生重大影响。我们来吧!😃

开始

Swift 3.1与Swift 3.0源代码兼容,因此如果您已经使用Xcode 中的 Edit \ Convert \ To Current Swift Syntax ... 将项目迁移到Swift 3.0,新功能将不会破坏您的代码。不过,苹果已经在Xcode 8.3中支持Swift 2.3。所以如果你还没有从Swift 2.3迁移,现在是时候这样做了!

在下面的部分,您会看到链接的标签,如[SE-0001]。这些是 Swift Evolution 提案号码。我已经列出了每个提案的链接,以便您可以发现每个特定更改的完整详细信息。我建议您尝试在Playground上验证新的功能,以便更好地了解所有更改的内容。

Note:如果你想了解 swift 3.0 中的新功能,可以看这篇文章

语法改进

首先,我们来看看这个版本中的语法改进,包括关于数值类型的可失败构造器Failable Initializers),新的序列函数等等。

可失败的数值转换构造器(Failable Numeric Conversion Initializers)

Swift 3.1 为所有数值类型 (Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, Float, Float80, Double) 添加了可失败构造器

这个功能非常有用,例如,以安全、可恢复的方式处理外源松散类型数据的转换,下面来看 Student 的 JSON 数组的处理:

class Student {
let name: String
let grade: Int

init?(json: [String: Any]) {
guard let name = json["name"] as? String,
let gradeString = json["grade"] as? String,
let gradeDouble = Double(gradeString),
let grade = Int(exactly: gradeDouble) // <-- 3.1 的改动在这
else {
return nil
}
self.name = name
self.grade = grade
}
}

func makeStudents(with data: Data) -> [Student] {
guard let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
let jsonArray = json as? [[String: Any]] else {
return []
}
return jsonArray.flatMap(Student.init)
}

let rawStudents = "[{\"name\":\"Ray\", \"grade\":\"5.0\"}, {\"name\":\"Matt\", \"grade\":\"6\"},
{\"name\":\"Chris\", \"grade\":\"6.33\"}, {\"name\":\"Cosmin\", \"grade\":\"7\"},
{\"name\":\"Steven\", \"grade\":\"7.5\"}]"
let data = rawStudents.data(using: .utf8)!
let students = makeStudents(with: data)
dump(students) // [(name: "Ray", grade: 5), (name: "Matt", grade: 6), (name: "Cosmin", grade: 7)]

Student 类中使用了一个可失败构造器将 grade 属性从 Double 转变为 Int,像这样

let grade = Int(exactly: gradeDouble)

如果gradeDouble不是整数,例如6.33,它将失败。如果它可以用一个正确的表示Int,例如6.0,它将成功。

Note:虽然throwing initializers 可以用来替代 failable initializers。但是使用 failable initializers 会更好,更符合人的思维。

新的序列函数(Sequence Functions)

swift3.1添加了两个新的标准库函数在 Sequence 协议中:prefix(while:)``和prefix(while:)[SE-0045]

构造一个斐波纳契无限序列:

let fibonacci = sequence(state: (0, 1)) {
(state: inout (Int, Int)) -> Int? in
defer {state = (state.1, state.0 + state.1)}
return state.0
}

在Swift 3.0中,您只需指定迭代次数即可遍历fibonacci序列:

// Swift 3.0
for number in fibonacci.prefix(10) {
print(number) // 0 1 1 2 3 5 8 13 21 34
}

在swift 3.1中,您可以使用prefix(while:)drop(while:)获得符合条件在两个给定值之间的序列中的所有元素,就像这样:

// Swift 3.1
let interval = fibonacci.prefix(while: {$0 < 1000}).drop(while: {$0 < 100})
for element in interval {
print(element) // 144 233 377 610 987
}

prefix(while:)返回满足某个谓词的最长子序列。它从序列的开头开始,并停在给定闭包返回false的第一个元素上。

drop(while:) 相反:它返回从给定关闭返回false的第一个元素开始的子序列,并在序列结尾完成。

Note:这种情况,可以使用尾随闭包的写法:

let interval = fibonacci.prefix{$0 < 1000}.drop{$0 < 100}

Concrete Constrained Extensions(姑且翻译为类的约束扩展吧)

Swift 3.1允许您扩展具有类型约束的通用类型。以前,你不能像这样扩展类型,因为约束必须是一个协议。我们来看一个例子。

例如,Ruby on Rails提供了一种isBlank检查用户输入的非常有用的方法。以下是在Swift 3.0中用 String 类型的扩展实现这个计算型属性

// Swift 3.0
extension String {
var isBlank: Bool {
return trimmingCharacters(in: .whitespaces).isEmpty
}
}

let abc = " "
let def = "x"

abc.isBlank // true
def.isBlank // false

如果你希望isBlank计算型属性为一个可选值所用,在swift 3.0中,你将要这样做

// Swift 3.0
protocol StringProvider {
var string: String {get}
}

extension String: StringProvider {
var string: String {
return self
}
}

extension Optional where Wrapped: StringProvider {
var isBlank: Bool {
return self?.string.isBlank ?? true
}
}

let foo: String? = nil
let bar: String? = " "
let baz: String? = "x"

foo.isBlank // true
bar.isBlank // true
baz.isBlank // false

这创建了一个采用 StringStringProvider 协议而在你使用StringProvider扩展可选的 wrapped 类型时,添加isBlank方法。

Swift 3.1中,用来替代协议方法,扩展具体类型的方法像这样:

// Swift 3.1
extension Optional where Wrapped == String {
var isBlank: Bool {
return self?.isBlank ?? true
}
}

这就用更少的代码实现了和原先相同的功能~

泛型嵌套(Nested Generics)

Swift 3.1允许您将嵌套类型与泛型混合。作为一个练习,考虑这个(不是太疯狂)的例子。每当某个团队领导raywenderlich.com想在博客上发布一篇文章时,他会分配一批专门的开发人员来处理这个问题,以满足网站的高质量标准:

class Team<T> {
enum TeamType {
case swift
case iOS
case macOS
}

class BlogPost<T> {
enum BlogPostType {
case tutorial
case article
}

let title: T
let type: BlogPostType
let category: TeamType
let publishDate: Date

init(title: T, type: BlogPostType, category: TeamType, publishDate: Date) {
self.title = title
self.type = type
self.category = category
self.publishDate = publishDate
}
}

let type: TeamType
let author: T
let teamLead: T
let blogPost: BlogPost<T>

init(type: TeamType, author: T, teamLead: T, blogPost: BlogPost<T>) {
self.type = type
self.author = author
self.teamLead = teamLead
self.blogPost = blogPost
}
}

BlogPost内部类嵌套在其对应的Team外部类中,并使两个类都通用。这是团队如何寻找我在网站上发布的教程和文章:

Team(type: .swift, author: "Cosmin Pupăză", teamLead: "Ray Fix", 
blogPost: Team.BlogPost(title: "Pattern Matching", type: .tutorial,
category: .swift, publishDate: Date()))

Team(type: .swift, author: "Cosmin Pupăză", teamLead: "Ray Fix",
blogPost: Team.BlogPost(title: "What's New in Swift 3.1?", type: .article,
category: .swift, publishDate: Date()))

但实际上,在这种情况下,您可以简化该代码。如果嵌套的内部类型使用通用外部类型,那么它默认继承父类的类型。因此,您不需要如此声明:

class Team<T> {
// original code

class BlogPost {
// original code
}

// original code
let blogPost: BlogPost

init(type: TeamType, author: T, teamLead: T, blogPost: BlogPost) {
// original code
}
}

Note:如果您想了解更多关于Swift中的泛型,请阅读我们最近更新的Swift泛型入门的教程

Swift版本的可用性

您可以使用**#if swift(>= N)** 静态构造来检查特定的Swift版本:

// Swift 3.0
#if swift(>=3.1)
func intVersion(number: Double) -> Int? {
return Int(exactly: number)
}
#elseif swift(>=3.0)
func intVersion(number: Double) -> Int {
return Int(number)
}
#endif

然而,当使用Swift标准库时,这种方法有一个主要缺点。它需要为每个受支持的旧语言版本编译标准库。这是因为当您以向后兼容模式运行Swift编译器时,例如您要使用Swift 3.0行为,则需要使用针对该特定兼容性版本编译的标准库版本。如果您使用版本3.1模式编译的,那么您根本就没有正确的代码

因此,@available除了现有平台版本 [SE-0141] 之外,Swift 3.1扩展了该属性以支持指定Swift版本号:

// Swift 3.1

@available(swift 3.1)
func intVersion(number: Double) -> Int? {
return Int(exactly: number)
}

@available(swift, introduced: 3.0, obsoleted: 3.1)
func intVersion(number: Double) -> Int {
return Int(number)
}

这个新功能提供了与intVersionSwift版本有关的方法相同的行为。但是,它只允许像标准库这样的库被编译一次。编译器然后简单地选择可用于所选择的给定兼容性版本的功能。

Note:注意:如果您想了解更多关于Swift 的可用性属性( availability attributes),请参阅我们关于Swift中可用性属性的教程

逃逸闭包(Escaping Closures)

在Swift 3.0 [ SE-0103 ] 中函数中的闭包的参数是默认是不逃逸的(non-escaping)。在Swift 3.1中,您可以使用新的函数withoutActuallyEscaping()将非逃逸闭包转换为临时逃逸。

func perform(_ f: () -> Void, simultaneouslyWith g: () -> Void,
on queue: DispatchQueue) {
withoutActuallyEscaping(f) { escapableF in // 1
withoutActuallyEscaping(g) { escapableG in
queue.async(execute: escapableF) // 2
queue.async(execute: escapableG)

queue.sync(flags: .barrier) {} // 3
} // 4
}
}

此函数同时加载两个闭包,然后在两个完成之后返回。

  1. fg 进入函数后由非逃逸状态,分别转换为逃逸闭包:escapableFescapableG
  2. async(execute:) 的调用需要逃逸闭包,我们在上面已经进行了转换。
  3. 通过运行sync(flags: .barrier),您确保async(execute:)方法完全完成,稍后将不会调用闭包。
  4. 在范围内使用 escapableF and escapableG.

如果你存储临时逃离闭包(即真正逃脱)这将是一个Bug。未来版本的标准库可以检测这个陷阱,如果你试图调用它们。

Swift Package Manager 更新

啊,期待已久的 Swift Package Manage 的更新了!

可编辑软件包(Editable Packages)

Swift 3.1将可编辑软件包(editable packages)的概念添加到Swift软件包管理器 [ SE-0082 ]

swift package edit命令使用现有的Packages并将其转换为editable Packages。使用--end-edit命令将 package manager 还原回 规范解析的软件包(canonical resolved packag)

版本固定(Version Pinning)

Swift 3.1 添加了版本固定的概念[ SE-0145 ]。该 pin 命令 固定一个或所有依赖关系如下所示:

$ swift package pin --all      // 固定所有的依赖
$ swift package pin Foo // 固定 Foo 在当前的闭包
$ swift package pin Foo --version 1.2.3 // 固定 Foo 在 1.2.3 版本

使用unpin命令恢复到以前的包版本:

$ swift package unpin —all
$ swift package unpin Foo

Package manager 将每个依赖库的版本固定信息存储在 Package.pins 文件中。如果该文件不存在,则Package manager 会自动创建。

其他

swift package reset 命令将会把 Package 重置干净。

swift test --parallel 命令 执行测试。

其他改动

在 swift 3.1 中还有一些小改动

多重返回函数

C函数返回两次,例如vforkvfork 已经不用了。他们以有趣的方式改变了程序的控制流程。所以 Swift 社区 已经禁止了该行为,以免导致编译错误。

自动链接失效(Disable Auto-Linking)

Swift Package Manager 禁用了在C语言 模块映射(module maps)中的自动链接的功能:

// Swift 3.0
module MyCLib {
header “foo.h"
link “MyCLib"
export *
}

// Swift 3.1
module MyCLib {
header “foo.h”
export *
}

结语

Swift 3.1改善了Swift 3.0的一些功能,为即将到来的Swift 4.0的大改动做准备。这些包括对泛型,正则表达式,更科学的String等方面的作出极大的改进。

如果你想了解更多,请转到 Swift standard library diffs 或者查看官方的的Swift CHANGELOG,您可以在其中阅读所有更改的信息。或者您可以使用它来了解 Swift 4.0 中的内容!

著作权声明

本文译自 What’s New in Swift 3.1?

@柏荧(BY)进行翻译,首次发布于 BY Blog,转载请保留原文链接.

Swift 代理模式

· 阅读需 2 分钟
BY

Xcode 8.2 | Swift 3.0

在iOS开发中,无论是 Objective-C 还是 SwiftDelegate 有着具足轻重的位置,如TabelViewDelegateTableViewDataSource

Swift 中的代理模式 和 Objective-C 除了语法外,几乎一样。

Objective-C 代理模式

在介绍 Swift 代理模式前,先来看回顾一下 Objective-C 中的代理模式如何实现

Objective-C 中用代理实现反向传值:

委托方(子控制器)

委托方需要实现

  • 创建协议 、声明协议方法
		@protocol SubViewDelegate <NSObject>

- (void)backWithStr:(NSString *) str;

@end
  • 创建一个代理属性
		// weak声明
@property (nonatomic, weak) id<SubViewDelegate> delegate;
  • 执行协议方法
		// 判断代理是实现该方法,避免carsh
if ([self.delegate respondsToSelector:@selector(backWithStr:)]) {
[self.delegate backWithStr:self.textField.text];
}

代理方(主控制器)

代理方需要实现

  • 遵守(继承)协议
		@interface ViewController () <SubViewDelegate>```
- 将代理设为自己

subVC.delegate = self;```

  • 实现代理方法
		- (void)backWithStr:(NSString *)str {
self.label.text = str;
}

Swift 代理模式

Swift 代理模式 与 Objective-C 一样,只是语法不同。

Swift 中用代理实现反向传值:

委托方(子控制器)

  • 创建协议 、声明协议方法
		protocol SubViewDelegate {
func backStr(str: String)
} ```
- 创建一个代理属性

var delegate: SubViewDelegate?```

  • 执行协议方法
		/// 执行代理方法,将值回传
delegate?.backStr(str: textField.text ?? "")

代理方(主控制器)

  • 继承协议
		class ViewController: UIViewController, SubViewDelegate```
- 将代理设为自己

subVC.delegate = self

- 实现代理方法

```swift
func backStr(str: String) {
self.textF.text = str
}

总结

对比可以方法 Swift 代理模式 与 Objective-C 用法完全相同,只是语法发生了变化。

值得一提的是Swift 的扩展 extension可以用来继承协议,实现代码隔离,便于维护。

/// 使用扩展继承协议 实现协议方法 可以分离代码
extension ViewController: SubViewDelegate{
/// 实现代理方法
func backStr(str: String) {
self.textF.text = str
}
}

Demo源码

最后附上Demo源码

如果对你有帮助的话,Star✨下一吧!

Objective-C Runtime 详解

· 阅读需 34 分钟
BY

最近在学习Runtime的知识,恰巧发现了这篇博客《Objective-C Runtime》,在此基础上,进行了些许补充说明,如有错误或其他想法,欢迎提出交流。

基础知识

  • 引言
  • 简介
  • 与Runtime交互
  • RunTime术语
  • 消息
  • 动态方法解析
  • 消息转发
  • 健壮的实例变量
  • 动态添加属性(Object-C Associated Objects)
  • 方法调剂(Method Swizzling)
  • 总结

引言

Objective-C的方法调用实则为“发送消息”,我们来看[dog eat]实际会被编译器转化为

objc_msgSend(dog, SEL)//SEL为eat方法的标识符@selector(@"eat")

若方法中函数参数,则为:

objc_msgSend(dog, SEL, arg1, arg2, ...)

如果消息的接收者能够找到对应的方法,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个方法对应的实现内容,要么就干脆就crash掉。

现在可以看出[dog eat]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送eat这条消息,而dog将要如何响应这条消息,那就要看运行时发生的情况来决定了。

Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。

简介

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

与Runtime交互

Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。 还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的idSEL都是啥)

NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

Runtime术语

还记得引言中的objc_msgSend:方法吧,它的真身是这样的

id objc_msgSend ( id self, SEL op, ... );

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。

SEL

objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的标识,而这个标识的数据结构是SEL:

typedef struct objc_selector *SEL;

本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。

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

  1. sel_registerName函数

  2. Objective-C编译器提供的@selector()

  3. NSSelectorFromString()方法

id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object *id;

objc_object又是啥呢:

struct objc_object { Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档的这句段说明

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

Class

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

typedef struct objc_class *Class;

objc_class就是我们摸到的那个瓜,里面的东西多着呢:

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;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议

PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码

Objective-C 2.0 的头文件虽然没暴露出objc_class结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义中小窥端倪

objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。而最新版的 Runtime 源码对这一块的描述已经有很大变化,可以参考下美团技术团队的深入理解Objective-C:Category.

PS:任性的话可以在Category中添加@dynamic的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)

其中objc_ivar_listobjc_method_list分别是成员变量列表和方法列表:

struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;

int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

如果你C语言不是特别好,可以理解为objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。

最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。

不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

上图实线是 super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类

####Method

Method是一种代表类中的某个方法的类型。

typedef struct objc_method *Method;

objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

  • 方法名 method_name 类型为 SEL, 相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型method_types是个char指针,存储着方法的 参数类型 和 返回值 类型。
  • method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

Ivar

Ivar是一种代表类中实例变量的类型。定义如下:

typedef struct objc_ivar *Ivar;

它是一个指向objc_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
} OBJC2_UNAVAILABLE;

这里我们注意第三个成员 ivar_offset。它表示基地址偏移字节。

在编译我们的类时,编译器生成了一个 ivar 布局,显示了在类中从哪可以访问我们的 ivars

我们对 ivar 的访问就可以通过 对象地址ivar偏移字节的方法。

但是当我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。

而Objective-C Runtime中使用了Non Fragile ivars来避免这个问题

使用Non Fragile ivars时,Runtime会进行检测来调整类中新增的ivar的偏移量。 这样我们就可以通过 对象地址 + 基类大小 + ivar偏移字节的方法来计算出ivar相应的地址,并访问到相应的ivar

可以根据实例查找其在类中的名字,也就是“反射”:

-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}

class_copyIvarList 函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。(属性的本质就是 _属性名+set+get方法)

IMP

IMPobjc.h中的定义是:

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

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

我们再来看看objc_msgSend()的定义: id objc_msgSend(id self, SEL op, ...)

你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组idSEL参数就能确定唯一的方法实现地址。

Cache

runtime.h中Cache的定义如下:

typedef struct objc_cache *Cache

还记得之前 objc_class 结构体中有一个 struct objc_cache *cache 吧,它到底是缓存啥的呢,先看看 objc_cache 的实现:

struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

objc_cache 的定义看起来很简单,它包含了下面三个变量:

  • mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
  • occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
  • buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存

(buckets定义在objc_cache的最后,说明这是一个可变长度的数组)

Cache为方法调用的性能进行优化,下面我们来看看objc_msgSend具体又是如何分发的呢? 我们来看下runtime层objc_msgSend的源码。

objc-msg-arm.s中,objc_msgSend的代码如下:

ps:Apple为了高度优化objc_msgSend的性能,这个文件是汇编写成的,不过即使我们不懂汇编,详尽的注释也可以让我们一窥其真面目

ENTRY objc_msgSend
# check whether receiver is nil
teq a1, #0
beq LMsgSendNilReceiver
# save registers and load receiver's class for CacheLookup
stmfd sp!, {a4,v1}
ldr v1, [a1, #ISA]
# receiver is non-nil: search the cache
CacheLookup a2, v1, LMsgSendCacheMiss
# cache hit (imp in ip) and CacheLookup returns with nonstret (eq) set, restore registers and call
ldmfd sp!, {a4,v1}
bx ip
# cache miss: go search the method lists
LMsgSendCacheMiss:
ldmfd sp!, {a4,v1}
b _objc_msgSend_uncached
LMsgSendNilReceiver:
mov a2, #0
bx lr
LMsgSendExit:
END_ENTRY objc_msgSend
STATIC_ENTRY objc_msgSend_uncached
# Push stack frame
stmfd sp!, {a1-a4,r7,lr}
add r7, sp, #16
# Load class and selector
ldr a3, [a1, #ISA] /* class = receiver->isa */
/* selector already in a2 */
/* receiver already in a1 */
# Do the lookup
MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache3)
MOVE ip, a1
# Prep for forwarding, Pop stack frame and call imp
teq v1, v1 /* set nonstret (eq) */
ldmfd sp!, {a1-a4,r7,lr}
bx ip

如果向更深入了解 objc_cache ,可以看看这篇博文深入理解Objective-C:方法缓存

从上述代码中可以看到,objc_msgSend(就ARM平台而言)的消息分发分为以下几个步骤:

  1. 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
  2. 从缓存里寻找,找到了则分发,否则
  3. 利用objc-class.mm中_class_lookupMethodAndLoadCache3(为什么有个这么奇怪的方法。本文末尾会解释)方法去寻找selector
  4. 如果支持GC,忽略掉非GC环境的方法(retain等)
  5. 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则
  6. 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则
  7. 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
  8. 转发这个selector,否则
  9. 报错,抛出异常

从上面的分析中我们可以看到,当一个方法在比较“上层”的类中,用比较“下层”(继承关系上的上下层)对象去调用的时候,如果没有缓存,那么整个查找链是相当长的。就算方法是在这个类里面,当方法比较多的时候,每次都查找也是费事费力的一件事情。

当我们需要去调用一个方法数十万次甚至更多地时候,查找方法的消耗会变的非常显著。就算我们平常的非大规模调用,除非一个方法只会调用一次,否则缓存都是有用的。在运行时,那么多对象,那么多方法调用,节省下来的时间也是非常可观的。可见缓存的重要性。

方法缓存存在什么地方?

让我们再去去翻看 objc_class 的定义,

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;

我们看到在类的定义里就有cache字段,没错,类的所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。

子类类即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。

Property

@property标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property结构体的指针:

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

现在在类中声明声明属性和成员变量:

@interface ViewController ()
{
int age;
NSString *name;
}
@property (nonatomic, strong) NSString *property1;
@property (nonatomic, strong) NSString *property2;
@property (nonatomic, assign) int age;//这里的age为属性,对应变量:_age
@property (nonatomic, assign) long ID;

@end

然后用下面的方法来获取类中属性列表:

id LenderClass = objc_getClass("ViewController");//获取calss
//id LenderClass = [MyViewController class];//同上
unsigned int outCount;//属性数量
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);//获取属性列表
for (int i = 0; i < outCount; i++) {// 遍历
objc_property_t property = properties[i];
const char *propertyName = property_getName(property);
const char *propertyAttributes = property_getAttributes(property);
printf("propertyName:%s \n", propertyName);
printf("propertyAttributes:%s\n--------\n", propertyAttributes);//属性名及描述
}

控制台输出:

propertyName:property1 
propertyAttributes:T@"NSString",&,N,V_property1
--------
propertyName:property2
propertyAttributes:T@"NSString",&,N,V_property2
--------
propertyName:age
propertyAttributes:Ti,N,V_age
--------
propertyName:ID
propertyAttributes:Tq,N,V_ID

我们再来来看看获取成员变量的方法:

id selfClass = [self class];
unsigned int numIvars = 0;
Ivar *ivars = class_copyIvarList(selfClass, &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar ivar = ivars[i];
const char *ivarType = ivar_getTypeEncoding(ivar);// 获取类型
const char *ivarName = ivar_getName(ivar);
printf("ivarName:%s\n", ivarName);
printf("ivarType:%s\n------\n", ivarType);
}

控制台输出:

ivarName:age
ivarType:i
------
ivarName:name
ivarType:@"NSString"
------
ivarName:_age
ivarType:i
------
ivarName:_property1
ivarType:@"NSString"
------
ivarName:_property2
ivarType:@"NSString"
------
ivarName:_ID
ivarType:q

我们会发现与 class_copyIvarList 函数不同,使用 class_copyPropertyList 函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的,得到属性或者变量名后我们就可以使用KVC去修改访问类中的私有属性或变量。所以OC中没有真正意义上的私有变量,私有方法也是。

消息

前面做了这么多铺垫,现在终于说到了消息了。Objc 中发送消息是用中括号 [] 把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。

有关消息发送和消息转发机制的原理,可以查看这篇文章

objc_msgSend函数

在引言中已经对 objc_msgSend 进行了一点介绍,看起来像是 objc_msgSend 返回了数据,其实 objc_msgSend 从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:

  1. 检测这个 消息 是不是要忽略的。比如 Mac OS X 开发,在ARC中有了垃圾回收就不理会MRC的 retain, release 这些函数了。
  2. 检测这个 目标对象 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
  6. 如果还找不到就要开始进入动态方法解析了,后面会提到。

PS:这里说的分发表其实就是 Class 中的方法列表,它将方法选择器和方法实现地址联系起来。

其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法

PS:有木有发现这些函数的命名规律哦?带 “Super” 的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表 “struct”“return”“fpret”就是 “fp” + “ret”,分别代表“floating-point”“return”

方法中的隐藏参数

我们经常在方法中使用 self 关键字来引用实例本身,但从没有想过为什么 self 就能取到调用当前方法的对象吧。其实 self 的内容是在方法运行时被偷偷的动态传入的

objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)

之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:

- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();

if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}

在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径

而当方法中的 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定父类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]获取超类时,编译器只是将指向selfid指针和classSEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。

获取方法地址

IMP 那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。 NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的 IMP ,举个栗子:

void (*imp)(id, SEL, BOOL);//定义一个函数指针
imp = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];//获取setFilled:函数的IMP

动态方法解析

你可以动态地提供一个方法的实现。例如我们可以用 @dynamic 关键字在类的实现文件中修饰一个属性:

@dynamic propertyName;

这表明我们会为这个属性提供存取方法,也就是说编译器不会默认为我们生成 setPropertyName:prepertyName 方法,而需要我们自己提供动态方法。我们可以通过分别重载 resolveIntanceMethod:resolvrClassMethod: 方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在 Cache 和方法分发表中(包括父类)找不到要执行的方法时,Runtime会调用 resolveIntanceMethod:resolvrClassMethod: 来给我们一次动态添加实现的机会。我们需要 class_addMethod函数完成向特定类添加特定方法实现的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding

PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:instancesRespondToSelector: 方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod: 返回 NO

消息转发

重定向

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector 方法替换消息的接受者为其他对象:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回self,因为那样会死循环

转发

当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}

该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation: 消息通知该对象。每个对象都从NSObject类中继承了 forwardInvocation: 方法。然而,NSObject中的方法实现只是简单地调用了 doesNotRecognizeSelector: 。通过实现我们自己的 forwardInvocation: 方法,我们可以在该方法实现中将消息转发给其它对象。

forwardInvocation: 方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

注意: forwardInvocation: 方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

转发和多继承

转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。

这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中 WarriorDiplomat 没有继承关系,但是 Warriornegotiate 消息转发给了 Diplomat 后,就好似 DiplomatWarrior 的超类一样。 消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的

替代者对象(Surrogate Objects)

转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档

转发于继承

尽管转发很像继承,但是NSObject类不会将两者混淆。像 respondsToSelector:isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个 Warrior 对象如果被问到是否能响应 negotiate 消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

结果是 NO ,尽管它能够接受 negotiate 消息而不报错,因为它靠转发消息给 Diplomat 类来响应消息。

如果你为了某些意图偏要“弄虚作假”让别人以为Warrior 继承到了 Diplomatnegotiate 方法,你得重新实现 respondsToSelector: isKindOfClass: 来加入你的转发算法:

- (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;
}

除了respondsToSelector: isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector::

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}

健壮的实例变量(Non Fragile ivars)

在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。从对象头部地址开始,实例变量依次根据自己所占空间而产生位移:

再翻出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
} OBJC2_UNAVAILABLE;

ivar 的访问可以通过 对象地址ivar偏移字节(ivar_offset)的方法。

当我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。

在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了

需要注意的是在健壮的实例变量下,不要使用 sizeof(SomeClass),而是用 class_getInstanceSize([SomeClass class]) 代替;也不要使用 offsetof(SomeClass, SomeIvar) ,而要用 ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar")) 来代替。

/* 定义一个Student类 */
@interface Student : NSObject
{
@private
int age;
}
@end

@implementation Student
// 重写%@输出方法
- (NSString *)description
{
NSLog(@"current pointer = %p", self);
NSLog(@"age pointer = %p", &age);
return [NSString stringWithFormat:@"age = %d", age];
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...

Student *student = [[Student alloc] init];
Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");//获取"age"的ivar
int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));//定义一个指向age_ivar的指针:指向地址为 student对象地址 + age_ivar的偏移量(ivar_offset)
NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));//输出offset偏移量
*age_pointer = 10;//对指针age_pointer指向的变量(age_ivar)赋值
NSLog(@"%@", student);//输出重写的description方法

}
return 0;
}

观察控制台输出:

2016-11-11 16:22:56.364 Iavr_offset[1501:928608] age ivar offset = 8
2016-11-11 16:22:56.365 Iavr_offset[1501:928608] current pointer = 0x100400170
2016-11-11 16:22:56.365 Iavr_offset[1501:928608] age pointer = 0x100400178
2016-11-11 16:22:56.366 Iavr_offset[1501:928608] age = 10

我们发现age pointer = current pointer + age ivar offset

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:

 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 );

这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:

enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

Method Swizzling

之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

这里摘抄一个 NSHipster 的例子

#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

上面的代码通过添加一个 Tracking 类别到 UIViewController 类中,将 UIViewController 类的 viewWillAppear: 方法和 Tracking 类别中 xxx_viewWillAppear: 方法的实现相互调换。Swizzling 应该在 +load 方法中实现,因为 +load 是在一个类最开始加载时调用。dispatch_once 是GCD中的一次性方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。

先用 class_addMethodclass_replaceMethod 函数将两个方法的实现进行调换,如果类中已经有了 viewWillAppear: 方法的实现,那么就调用 method_exchangeImplementations 函数交换了两个方法的 IMP ,这是苹果提供给我们用于实现 Method Swizzling 的便捷方法。 最后 xxx_viewWillAppear: 方法的定义看似是递归调用引发死循环,其实不会的。因为 [self xxx_viewWillAppear:animated] 消息会动态找到 xxx_viewWillAppear: 方法的实现,而它的实现已经被我们与 viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把 [self xxx_viewWillAppear:animated] 换成 [self viewWillAppear:animated] 反而会引发死循环。 看到有人说 +load方法本身就是线程安全的,因为它在程序刚开始就被调用,很少会碰到并发问题,于是 stackoverflow 上也有大神给出了另一个 Method Swizzling 的实现:

- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
NSLog(@"arg1 is %@", arg1);
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}

其实也就是去掉了dispatch_once的部分罢了。

Method Swizzling 的确是一个值得深入研究的话题,Method Swizzling 的最佳实现是什么呢?小弟才疏学浅理解的不深刻,找了几篇不错的资源推荐给大家:

总结

我们之所以让自己的类继承 NSObject 不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。

参考链接

Objective-C Runtime 基本使用

· 阅读需 14 分钟
BY

在上一篇文章《Objective-C Runtime详解》中我们探讨了Runtime的基本原理,这篇文章我们将总结一下Runtime的一些基本使用

使用方法

  • 查询方法
  • 给分类添加属性
  • 更换代码的实现方法
  • 动态添加方法
  • 字典转属性

准备

先创建两个类

ClassA.h

#import <Foundation/Foundation.h>

@interface ClassA : NSObject {
// 公有变量
NSString *_publicVar1;
NSString *_publicVar2;

}
// 公有属性
@property(nonatomic,copy) NSString *publicProperty1;
@property(nonatomic,copy) NSString *publicProperty2;

/* 公有方法 */
-(void)methodAOfClassAWithArg:(NSString *)arg;

@end

ClassA.m

#import "ClassA.h"

@interface ClassA()
// 私有属性
@property(nonatomic,copy) NSString *privateProperty1;
@property(nonatomic,copy) NSString *privateProperty2;

@end

@implementation ClassA {
// 私有变量
NSString *_privateVar1;
NSString *_privateVar2;
}

/* 公有方法 */
-(void)methodAOfClassAWithArg:(NSString *)arg {
NSLog(@" methodAOfClassA arg = %@", arg);
}

/* 私有方法 */
-(void)MethodBOfClassAWithArg:(NSString *)arg {
NSLog(@" methodBOfClassA arg = %@", arg);
}
@end

ClassB.h

#import <Foundation/Foundation.h>

@interface ClassB : NSObject

/* 公有方法 */
-(void)methodAOfClassBWithArg:(NSString *)arg;

@end

ClassB.m

#import "ClassB.h"

@implementation ClassB
- (void)methodAOfClassBWithArg:(NSString *)arg {
NSLog(@" methodAOfClassB arg = %@", arg);
}

-(void)methodBOfClassBWithArg:(NSString *)arg {
NSLog(@" methodBOfClassB arg = %@", arg);
}

@end

查询方法


在Objective-C Runtime下没有真正意义上的私有变量和方法,因为这些私有变量和方法都可以通过Runtime方法获取,这当然包括系统的私有API。接下来我们来一一介绍获取类中属性和方法的方法。当然不要忘了#import <objc/runtime.h>.

获取类的名称

方法:const char *object_getClassName(id obj),使用比较简单,传入对象即可得到对应分类名。

ClassA *classA = [[ClassA alloc] init];
const char *className = object_getClassName(classA);
NSLog(@"className = %@", [NSString stringWithUTF8String:className]);

//输出
className = ClassA

获取类中的方法

方法:Method *class_copyMethodList(Class cls, unsigned int *outCount)

上代码:

UInt32 count;
char dst;
Method *methods = class_copyMethodList([classA class], &count);//获取方法列表
for (int i = 0; i < count; i++) {
Method method = methods[i];// 获取方法
SEL methodName = method_getName(method);// 获取方法名
method_getReturnType(method, &dst, sizeof(char));// 获取方法返回类型
const char *methodType = method_getTypeEncoding(method);// 获取方法参数类型和返回类型
NSLog(@"methodName = %@",NSStringFromSelector(methodName));
NSLog(@"dst = %c", dst);
}

// 输出
methodName = methodAOfClassAWithArg:
dst = v
methodType = v24@0:8@16
methodName = MethodBOfClassAWithArg:
dst = v
methodType = v24@0:8@16
methodName = publicProperty1
dst = @
methodType = @16@0:8
methodName = setPublicProperty1:
dst = v
methodType = v24@0:8@16
methodName = publicProperty2
dst = @
methodType = @16@0:8
methodName = setPublicProperty2:
dst = v
methodType = v24@0:8@16
methodName = privateProperty1
dst = @
methodType = @16@0:8
methodName = setPrivateProperty1:
dst = v
methodType = v24@0:8@16
methodName = privateProperty2
dst = @
methodType = @16@0:8
methodName = setPrivateProperty2:
dst = v
methodType = v24@0:8@16
methodName = .cxx_destruct
dst = v
methodType = v16@0:8

class_copyMethodList([classA class], &count) 传入元类和计数器地址,返回方法列表。这里注意,返回的是Method结构体类型的C数组,Method类型我们在上篇文章中已经详细说明,

typedef struct objc_method *Method;

struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

但要区分Method *methodsMethod method的区别,这是比较基础C语言知识。还有Uint32是OC定义的unsigned int类型typedef unsigned int UInt32;

这里我们来看看 method_getReturnType(method, &dst, sizeof(char)) 方法简单输出返回值类型,输出为 v@ ,参考Apple文档可知道返回类型为 voidid

A void v
A method selector (SEL) :
An object (whether statically typed or typed id) @

method_getTypeEncoding(method)方法可以输出返回值,参数类型以及接收器类型。我们看输出的v24@0:8@16,分析上面的说明就可以知道: v24返回类型为viod,@0接收器类型为id,@16参数类型为id

至于类型后面的值观察可以发现都是相差8,我认为是在method中的位置,分别以8bit存储不同类型的数据。

若有两个参数返回值为 v32@0:8@16@24 ,对比可以猜测,在method中各个成员的排列是这样的: 接收器|SEl标识|参数1|参数2|...|返回值,然后由 method_getTypeEncoding(method) 输出的顺序为: 返回值类型|接收器类型|SEL标识|参数1|参数2|... 此处为个人见解,如有错误或不同意见欢迎提出探讨。

最后发现了一个奇怪的方法 .cxx_destruct ,在中这篇文章中:

ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

和《Effective Objective-C 2.0》中提到的:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

可以了解到,.cxx_destruct 方法原本是为了C++对象析构的,ARC借用了这个方法插入代码实现了自动内存释放的工作

关于 .cxx_destruct 可以参考这篇文章:ARC下dealloc过程及.cxx_destruct的探究

获取类中的属性

上篇文章Property 中我们也提到了获取类中的属性的方法,如下:

id LenderClass = objc_getClass("ClassA");//获取classA 的元类同[ClassA class]
unsigned int outCount;//属性数量
// 获取属性列表
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

// 遍历
for (int i = 0; i < outCount; i++) {

objc_property_t property = properties[i];

const char *propertyName = property_getName(property);// 获取属性名
const char *propertyAttributes = property_getAttributes(property);// 获取属性描述

printf("propertyName:%s \n", propertyName);
printf("propertyAttributes:%s\n--------\n", propertyAttributes);//属性名及描述
}
// 输出
propertyName:privateProperty1
propertyAttributes:T@"NSString",C,N,V_privateProperty1
--------
propertyName:privateProperty2
propertyAttributes:T@"NSString",C,N,V_privateProperty2
--------
propertyName:publicProperty1
propertyAttributes:T@"NSString",C,N,V_publicProperty1
--------
propertyName:publicProperty2
propertyAttributes:T@"NSString",C,N,V_publicProperty2
--------

发现会输出公有属性以及私有属性。

获取类中的成员变量

我们可以发现获取类中的方法,属性过程基本一致:通过元类获取方法列表或属性列表,然后在进行遍历。获取成员变量也一样:

id selfClass = [Calss class];
unsigned int numIvars = 0;
Ivar *ivars = class_copyIvarList(selfClass, &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar ivar = ivars[i];
const char *ivarName = ivar_getName(ivar);
const char *ivarType = ivar_getTypeEncoding(ivar);// 获取类型

printf("ivarName:%s\n", ivarName);
printf("ivarType:%s\n------\n", ivarType);
}
// 输出
ivarName:_publicVar1
ivarType:@"NSString"
------
ivarName:_publicVar2
ivarType:@"NSString"
------
ivarName:_privateVar1
ivarType:@"NSString"
------
ivarName:_privateVar2
ivarType:@"NSString"
------
ivarName:_publicProperty1
ivarType:@"NSString"
------
ivarName:_publicProperty2
ivarType:@"NSString"
------
ivarName:_privateProperty1
ivarType:@"NSString"
------
ivarName:_privateProperty2
ivarType:@

可以发现输出了所有的成员变量,包括属性声明的 _+属性名 变量。

给分类添加属性


众所周知,分类中是不能声明属性的。

我们创建一个 CalssA 的分类 ClassA+CategoryA ,在 ClassA+CategoryA 中添加一个属性 name

#import "ClassA.h"

@interface ClassA (CategoryA)

@property (nonatomic, strong) NSString *name;

@end

若在我们调用CalssA分类的name 将会crash,原因是分类中使用 @property 声明属性并不会生成settergetter方法,但是我们会想,我们可以自己实现呀,没错,看下面的代码

#import "ClassA+CategoryA.h"
#import <objc/runtime.h>

@implementation ClassA (CategoryA)

- (NSString *)name {
return name;
}

- (void)setName:(NSString *)name {
_name = name;
}

@end

这里会报编译错误,因为分类中使用 @property 声明属性也不会生成成员变量 _name,并且手动声明也不行

编译错误,提示实例变量无法添加到分类中,用正常的方法确实无法在分类中添加属性。

但是可以通过Runtim机制进行“添加”。其本质是给这个类添加属性关联,而非把这个属性添加到类中。

#import "ClassA+CategoryA.h"
#import <objc/runtime.h>


@implementation ClassA (CategoryA)

- (NSString *)name {
return objc_getAssociatedObject(self, @selector(name));
}

- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

调用:

classA.name = @"邱帅";
NSLog(@"%@",classA.name);

// 输出
2016-11-21 16:18:48.084 UseRuntime[4392:1325037] 邱帅

可以看出添加属性成功!

我们来看看关联属性的这几个方法:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

OBJC_EXPORT void objc_removeAssociatedObjects(id object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

objc_setAssociatedObject() 方法为关联属性,参数如下:

  • object:属性关联的源对象,这里使用了self,代表关联本类的对象
  • key:区分属性的唯一标识,因为关联的属性可能不止一个,我们使用了- (NSString *)name方法的SEL @selector(name)作为唯一标示,当然也可以用下面的方法来生成Key :
//利用静态变量地址唯一不变的特性
1、static void *strKey = &strKey;

2、static NSString *strKey = @"strKey";

3、static char strKey;
  • value:关联的属性值
  • policy:设置关联对象的copystorynonatomic等参数:

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

objc_getAssociatedObject() 方法通过 objectKey 直接获取关联的属性值

objc_removeAssociatedObjects() 移除关联

我们使用上面的获取类中属性和成员变量的方法,发现输出:

// 有属性输出
propertyName:name
propertyAttributes:T@"NSString",&,N

没有成员变量 _name,进一步说明分类中不能添加成员变量!其本质是添加属性与分类之间关联。

更换代码实现方法(Method Swizzling)


上篇中详细介绍了Method Swizzling的原理,其本质是更换了 selectorIMP

#import "ViewController.h"
#import <objc/runtime.h>
#import "ClassA.h"
#import "ClassB.h"

@interface ViewController ()

@end

@implementation ViewController

+ (void)load {
Method classA_method = class_getInstanceMethod([ClassA class], @selector(methodAOfClassAWithArg:));
Method classB_method = class_getInstanceMethod([ClassB class], @selector(methodAOfClassBWithArg:));
method_exchangeImplementations(classA_method, classB_method);
}
- (void)viewDidLoad {
[super viewDidLoad];

[classA methodAOfClassAWithArg:@"classA 发出的 A方法"];
[classB methodAOfClassBWithArg:@"classB 发出的 A方法"];
}

// 输出

2016-11-22 13:07:15.151 UseRuntime[1015:533335] methodAOfClassB arg = classA 发出的 A方法
2016-11-22 13:07:15.151 UseRuntime[1015:533335] methodAOfClassA arg = classB 发出的 A方法

首先交换方法写在 +(void)load,在程序的一开始就调用执行,你将不会碰到并发问题。

我们可以发现两个方法的实现过程以及对换。

当然,平时使用我们并不会这么做,当我们要在系统提供的方法上再扩充功能时(不能重写系统方法),就可以使用Method Swizzling.

我们给NSArray添加一个分类AddLog,给 arrayByAddingObject:方法添加一个输出方法:

#import "NSArray+AddLog.h"
#import <objc/runtime.h>

@implementation NSArray (AddLog)

+ (void)load {

SEL ori_selector = @selector(arrayByAddingObject:);
SEL my_selector = @selector(my_arrayByAddingObject:);

Method ori_method = class_getInstanceMethod([NSArray class], ori_selector);
Method my_method = class_getInstanceMethod([NSArray class], my_selector);

if (([NSArray class], ori_selector, method_getImplementation(my_method), method_getTypeEncoding(my_method))) {

class_replaceMethod([NSArray class], my_selector, method_getImplementation(ori_method), method_getTypeEncoding(ori_method));

} else {
method_exchangeImplementations(ori_method, my_method);
}

}

- (NSArray *)my_arrayByAddingObject:(id)anObject {

NSArray *array = [self my_arrayByAddingObject:anObject];
NSLog(@"添加了一个元素 %@", anObject);
return array;
}

@end

我们来看看这三个方法:

  • class_addMethod():给一个方法添加新的方法和实现
  • class_replaceMethod():取代了对于一个给定的类的实现方法
  • method_exchangeImplementations():交换两个类的实现方法

这里我们先使用 class_addMethod() 在类中添加方法,若返回Yes说明类中没有该方法,然后再使用 class_replaceMethod() 方法进行取代;若返回NO,说明类中有该方法,使用method_exchangeImplementations()直接交换两者的 IMP.

其实在这里直接使用method_exchangeImplementations()进行交换就可以了。因为类中必定有arrayByAddingObject:方法。

我给我们自己的方法命名为my_arrayByAddingObject:,在原来的方法名上加上前缀,既可以防止命名冲突,又方便阅读,在我们my_arrayByAddingObject:方法中调用本身

NSArray *array = [self my_arrayByAddingObject:anObject];

看似会陷入递归调用,其实则不会,因为我们已经在+ (void)load 方法中更换了IMP,他会调用arrayByAddingObject:方法,然后在后面添加我们需要添加的功能。

arrayByAddingObject:方法的调用不变;

NSArray *arr1 = @[@"one", @"two"];
NSArray *arr2 = [arr1 arrayByAddingObject:@"three"];
NSLog(@"arr2 = %@", arr2);
// 输出
2016-11-22 13:57:00.021 UseRuntime[1147:743449] 添加了一个元素 three
2016-11-22 13:57:00.021 UseRuntime[1147:743449] arr2 = (
one,
two,
three
)

动态添加方法

动态添加方法就是在消息转发前在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中使用class_addMethod() 添加方法。

下面我面添加一个名为resolveThisMethodDynamically的方法:

void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
printf("执行了dynamicMethodIMP!!!!");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {

if (sel == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

调用:

performSelector:@selector(resolveThisMethodDynamically)];

// 输出
执行了dynamicMethodIMP!!!!

对于上面添加的的方法 resolveThisMethodDynamically ,使用 [self performSelector:@selector(resolveThisMethodDynamically)] 进行调用,不能使用[self resolveThisMethodDynamically],因为压根就没有声明 -(void)resolveThisMethodDynamically,会报编译错误。

整个过程就是,performSelector:调用resolveThisMethodDynamically方法,然后在列表中找不到(因为类中根本就没有注册该方法),然后跳入 + (BOOL)resolveInstanceMethod: 中,我们再为resolveThisMethodDynamically方法添加具体实现。

字典转属性

将字典转化为模型,是在我们iOS开发中最为常用的技能。iOS的模型框架如JSONModel,MJExtension,MJExtension等皆是利用了runtime,将字典转为模型,不过兼顾的细节更多。下面我们来实现一个简易的字典转模型框架。

先上代码:

#import "NSObject+BYModel.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (BYModel)

- (void)by_modelSetDictionary:(NSDictionary *)dic {

Class cls = [self class];

// 遍历本类和父类的变量
while (cls) {
//获取所有成员变量
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(cls, &outCount);

for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];

// 获取变量名
NSMutableString *ivar_Name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];

[ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:@""];// _ivar -> ivar

//
NSString *key = [ivar_Name copy];
if ([key isEqualToString:@"dece"]) {
key = @"description";
}
if ([key isEqualToString:@"ID"]) {
key = @"id";
}

id value = dic[key];
if (!value) continue;

// 拼接SEL ivar -> setIvar:

NSString *cap = [ivar_Name substringToIndex:1];
cap = cap.uppercaseString; // a->A
[ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:cap];
[ivar_Name insertString:@"set" atIndex:0];
[ivar_Name appendString:@":"];

SEL selector = NSSelectorFromString(ivar_Name);

// 判断类型并发送消息
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

if ([type hasPrefix:@"@"]) { // 对象类型
objc_msgSend(self, selector, value);
} else { // 非对象类型
if ([type isEqualToString:@"d"]) {
objc_msgSend(self, selector, [value doubleValue]);
} else if ([type isEqualToString:@"f"]) {
objc_msgSend(self, selector, [value floatValue]);
} else if ([type isEqualToString:@"i"]) {
objc_msgSend(self, selector, [value intValue]);
} else {
objc_msgSend(self, selector, [value longLongValue]);
}
}


}
// 获取父类进行遍历变量
cls = class_getSuperclass(cls);
}

}

这个这个段代码可能出现编译错误:

解决办法很简单:

将项目 Project -> Build Settings -> Enable strct checking of objc_msgSend Calls 设置为 NO 即可

接下来我们创建一个模型类Student

#import <Foundation/Foundation.h>

@interface Student : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int idNumber;

@end


使用我们的转模型方法:

NSDictionary *dic = @{ @"name":@"邱帅", @"age": @(23), @"idNumber":@(1234567)};

Student *stu = [Student new];
[stu by_modelSetDictionary:dic];

NSLog(@"%@", [NSString stringWithFormat:@"%@, %d, %d", stu.name, stu.age, stu.idNumber]);

// 输出
2016-11-24 15:32:46.351 Demo_字典转模型(Runtime)[2131:884627] 邱帅, 23, 1234567

该方法先利用我们上面介绍的class_copyIvarList()获取类中的成员变量列表,然后进行遍历,拼接字符串setIvar:,最后调用objc_msgSend()直接发送设置变量的消息,完成属性的赋值。

while (cls) {

//code..

cls = class_getSuperclass(cls);
}

这个循环是则获取父类中的属性:当前类的属性遍历结束之后,指向父类,若父类存在则在继续遍历属性,否则就退出循环。

当然,这个方法只是介绍了利用runtime进行字典转模型的原理,实际中还有很多需要考虑的细节,项目中我还是推荐使用像YYModel这些比较成熟而且安全的模型框架。

关于快速字典转模型可以参考我写的一篇《快速完成JSON\字典转模型 For YYModel》

ReactiveCocoa 基础

· 阅读需 17 分钟
BY

本文修改自最快让你上手ReactiveCocoa之基础篇

有关对 ReactiveCocoa 的看法可以看一下唐巧的这篇ReactiveCocoa 讨论会

ReactiveCocoa思维导图

ReactiveCocoa简介

ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾。

在我们iOS开发过程中,当某些事件响应的时候,需要处理某些业务逻辑,这些事件都用不同的方式来处理。

比如按钮的点击使用action,ScrollView滚动使用delegate,属性值改变使用KVO等系统提供的方式。其实这些事件,都可以通过RAC处理

ReactiveCocoa为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。

非常符合我们开发中高聚合,低耦合的思想。

ReactiveCocoa编程思想

在开发中我们也不能太依赖于某个框架,否则这个框架不更新了,导致项目后期没办法维护,比如之前Facebook提供的 Three20 框架,在当时也是神器,但是后来不更新了,也就没什么人用了。因此我感觉学习一个框架,还是有必要了解它的编程思想。

先简单介绍下目前咱们已知的编程思想:

响应式编程思想

响应式编程思想:不需要考虑调用顺序,只需要知道考虑结果,类似于蝴蝶效应,产生一个事件,会影响很多东西,这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆是流。

代表:KVO

链式编程思想

链式编程 是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好。如:

make.add(1).add(2).sub(5).muilt(-4).divide(4);

特点:方法的返回值是block,block必须有返回值(本身对象),block参数(需要操作的值)

代表:masonry框架。

实现:模仿masonry,写一个加法计算器,练习链式编程思想。

NSObject+Caculator.h

# import <Foundation/Foundation.h>

@class CaculatorMaker;

@interface NSObject (Caculator)

// 计算
+ (int)makeCaculators:(void (^)(CaculatorMaker *))block;

@end

NSObject+Caculator.m

@implementation NSObject (Caculator)

+ (int)makeCaculators:(void (^)(CaculatorMaker *))block {

CaculatorMaker *mgr = [[CaculatorMaker alloc] init];

block(mgr);

return (mgr.result);
}

@end

CaculatorMaker.h

# import <Foundation/Foundation.h>

@class CaculatorMaker;

typedef CaculatorMaker *(^CasulatorBlock)(int);

@interface CaculatorMaker : NSObject

@property (nonatomic, assign) int result;

// 算数方法
- (CaculatorMaker *(^)(int))add;
- (CasulatorBlock)sub;
- (CasulatorBlock)muilt;
- (CasulatorBlock)divide;


@end

CaculatorMaker.m

# import "CaculatorMaker.h"

@implementation CaculatorMaker

- (CaculatorMaker *(^)(int))add {

return ^CaculatorMaker *(int value) {

_result += value;

return self;
};
}

- (CasulatorBlock)sub {

return ^CaculatorMaker *(int value) {

_result -= value;

return self;
};
}

- (CasulatorBlock)muilt {

return ^CaculatorMaker *(int value) {

_result *= value;

return self;
};
}

- (CasulatorBlock)divide {

return ^CaculatorMaker *(int value) {

_result /= value;

return self;
};
}

@end

使用:

int result = [NSObject makeCaculators:^(CaculatorMaker *make) {

// ( 1 + 2 - 5 ) * (-4) / 4
make.add(1).add(2).sub(5).muilt(-4).divide(4);

}];

NSLog(@"%d", result);

函数式编程思想

函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用。

特点:每个方法必须有返回值(本身对象),把函数或者Block当做参数,block参数(需要操作的值)block返回值(操作结果)

代表ReactiveCocoa

实现:用函数式编程实现,写一个加法计算器,并且加法计算器自带判断是否等于某个值.

    Calculator *caculator = [[Calculator alloc] init];

BOOL isqule = [[[caculator caculator:^int(int result) {

result += 2;
result *= 5;
return result;

}] equle:^BOOL(int result) {

return result == 10;

}] isEqule];

NSLog(@"%d", isqule);

Calculator.h

#import <Foundation/Foundation.h>

@interface Calculator : NSObject

@property (nonatomic, assign) BOOL isEqule;

@property (nonatomic, assign) int result;

- (Calculator *)caculator:(int (^)(int result))caculator;

- (Calculator *)equle:(BOOL (^)(int result))operation;

@end

Calculator.m

#import "Calculator.h"

@implementation Calculator

- (Calculator *)caculator:(int (^)(int))caculator {

_result = caculator(_result);

return self;

}


- (Calculator *)equle:(BOOL (^)(int))operation {

_isEqule = operation(_result);

return self;
}

@end

ReactiveCocoa 结合了这两种种编程风格:

  • 函数式编程(Functional Programming)

  • 响应式编程(Reactive Programming)

所以,你可能听说过 ReactiveCocoa 被描述为函数响应式编程(FRP)框架。

以后使用RAC解决问题,就不需要考虑调用顺序,直接考虑结果,把每一次操作都写成一系列嵌套的方法中,使代码高聚合,方便管理。

导入ReactiveCocoa


ReactiveCocoa的GitHub地址

Objective-C

ReactiveCocoa 2.5版本以后改用了Swift,所以Objective-C项目需要导入2.5版本

CocoaPods集成:

platform :ios, '8.0'

target 'YouProjectName' do

use_frameworks!
pod 'ReactiveCocoa', '~> 2.5'

end

PS:新版本的CocoaPods需要加入

target 'YouProjectName' do 
...
end

这句话来限定项目,否则导入失败。

Swift

Swift项目导入2.5后的版本

platform :ios, '8.0'

target 'YouProjectName' do

use_frameworks!
pod 'ReactiveCocoa'

end

使用时在全局头文件导入头文件即可

PrefixHeader.pch

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#import <ReactiveCocoa/ReactiveCocoa.h>

#endif

ReactiveCocoa常见类

RACSiganl 信号类

信号类,一般表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据。

注意:

  • 信号类(RACSiganl),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。
  • 默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。
  • 如何订阅信号:调用信号RACSignal的subscribeNext就能订阅

使用:

// RACSignal使用步骤:
// 1.创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
// 2.订阅信号,才会激活信号. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 3.发送信号 - (void)sendNext:(id)value


// RACSignal底层实现:
// 1.创建信号,首先把didSubscribe保存到信号中,还不会触发。
// 2.当信号被订阅,也就是调用signal的subscribeNext:nextBlock
// 2.2 subscribeNext内部会创建订阅者subscriber,并且把nextBlock保存到subscriber中。
// 2.1 subscribeNext内部会调用siganl的didSubscribe
// 3.siganl的didSubscribe中调用[subscriber sendNext:@1];
// 3.1 sendNext底层其实就是执行subscriber的nextBlock

// 1.创建信号
RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

// block调用时刻:每当有订阅者订阅信号,就会调用block。

// 2.发送信号
[subscriber sendNext:@1];

// 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
[subscriber sendCompleted];

return [RACDisposable disposableWithBlock:^{

// block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。

// 执行完Block后,当前信号就不在被订阅了。

NSLog(@"信号被销毁");

}];
}];

// 3.订阅信号,才会激活信号.
[siganl subscribeNext:^(id x) {
// block调用时刻:每当有信号发出数据,就会调用block.
NSLog(@"接收到数据:%@",x);
}];

RACSubscriber

表示订阅者的意思,用于发送信号,这是一个协议,不是一个类,只要遵守这个协议,并且实现方法才能成为订阅者。通过create创建的信号,都有一个订阅者,帮助他发送数据。

RACDisposable

用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。

使用场景:不想监听某个信号时,可以通过它主动取消订阅信号。

RACSubject

RACSubject:信号提供者,自己可以充当信号,又能发送信号。

使用场景:通常用来代替代理,有了它,就不必要定义代理了。

RACReplaySubject

重复提供信号类,RACSubject的子类。

RACReplaySubjectRACSubject区别:

RACReplaySubject可以先发送信号,在订阅信号,RACSubject就不可以。

使用场景一:如果一个信号每被订阅一次,就需要把之前的值重复发送一遍,使用重复提供信号类。

使用场景二:可以设置capacity数量来限制缓存的value的数量,即只缓充最新的几个值。

ACSubjectRACReplaySubject 简单使用:

ACSubject

    // RACSubject使用步骤
// 1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
// 2.订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 3.发送信号 sendNext:(id)value

// RACSubject:底层实现和RACSignal不一样。
// 1.调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。
// 2.调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。


// 1. 创建信号
RACSubject *subject = [RACSubject subject];

// 2.订阅信号
[subject subscribeNext:^(id x) {

// block调用时机:当信号发出新值,就会调用
NSLog(@"收到信号");

}];

// 3.发送信号
NSLog(@"发送信号");
[subject sendNext:@"1"];
    // RACReplaySubject使用步骤:
// 1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
// 2.可以先订阅信号,也可以先发送信号。
// 2.1 订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 2.2 发送信号 sendNext:(id)value

// RACReplaySubject:底层实现和RACSubject不一样。
// 1.调用sendNext发送信号,把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。
// 2.调用subscribeNext订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock

// 如果想当一个信号被订阅,就重复播放之前所有值,需要先发送信号,在订阅信号。
// 也就是先保存值,在订阅值。


// 1.创建信号
RACReplaySubject *replaySubject = [RACReplaySubject subject];



// 3.先订阅信号
[replaySubject subscribeNext:^(id x) {

NSLog(@"第一个订阅者接受到的数据%@", x);
}];

// 2.发送信号
[replaySubject sendNext:@1];
[replaySubject sendNext:@2];

// 后订阅信号
[replaySubject subscribeNext:^(id x) {

NSLog(@"第二个订阅者接收到的数据%@",x);
}];

RACSubject替换代理(与block类似)

// 需求:
// 1.给当前控制器添加一个按钮,modal到另一个控制器界面
// 2.另一个控制器view中有个按钮,点击按钮,通知当前控制器

步骤一:在第二个控制器.h,添加一个RACSubject代替代理。
@interface TwoViewController : UIViewController

@property (nonatomic, strong) RACSubject *delegateSignal;

@end

步骤二:监听第二个控制器按钮点击
@implementation TwoViewController
- (IBAction)notice:(id)sender {
// 通知第一个控制器,告诉它,按钮被点了

// 通知代理
// 判断代理信号是否有值
if (self.delegateSignal) {
// 有值,才需要通知
[self.delegateSignal sendNext:nil];
}
}
@end

步骤三:在第一个控制器中,监听跳转按钮,给第二个控制器的代理信号赋值,并且监听.
@implementation OneViewController
- (IBAction)btnClick:(id)sender {

// 创建第二个控制器
TwoViewController *twoVc = [[TwoViewController alloc] init];

// 设置代理信号
twoVc.delegateSignal = [RACSubject subject];

// 订阅代理信号
[twoVc.delegateSignal subscribeNext:^(id x) {

NSLog(@"点击了通知按钮 %@", x);
}];

// 跳转到第二个控制器
[self presentViewController:twoVc animated:YES completion:@"hi"];

}
@end

RACTuple

元组类,类似NSArray,用来包装值.(@[key, value])

RACSequence

RAC中的集合类,用于代替NSArray,NSDictionary,可以使用它来快速遍历数组和字典。

使用场景:字典转模型

    // 1.遍历数组
NSArray *numbers = @[@1,@2,@3,@4];

// 这里其实是三步
// 第一步: 把数组转换成集合RACSequence numbers.rac_sequence
// 第二步: 把集合RACSequence转换RACSignal信号类,numbers.rac_sequence.signal
// 第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。

[numbers.rac_sequence.signal subscribeNext:^(id x) {

NSLog(@"%@", x);
}];



// 2.遍历字典,遍历出来的键值对 都会包装成 RACTuple(元组对象) @[key, value]
NSDictionary *dic = @{@"name": @"BYqiu", @"age": @18};

[dic.rac_sequence.signal subscribeNext:^(RACTuple *x) {

// 解元组包,会把元组的值,按顺序给参数里的变量赋值
// 写法相当与
// NSString *key = x[0];
// NSString *value = x[1];
RACTupleUnpack(NSString *key, NSString *value) = x;

NSLog(@"key:%@, value:%@", key, value);

}];

// 3.字典转模型

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];

NSArray *dicArray = [NSArray arrayWithContentsOfFile:filePath];

NSMutableArray *items = [NSMutableArray array];

// OC写法
for (NSDictionary *dic in dicArray) {

//FlagItem *item = [FlagItem flagWithDict:dict];
//[items addObject:item];
}


// RAC写法
[dicArray.rac_sequence.signal subscribeNext:^(id x) {
// 利用RAC遍历, x:字典

//FlagItem *item = [FlagItem flagWithDict:x];
//[items addObject:item];
}];

// RAC高级用法(函数式编程)
NSArray *flags = [[dicArray.rac_sequence map:^id(id value) {

return [FlagItem flagWithDict:value];

}] array];

RACCommand

RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。

一、RACCommand使用步骤:

  1. 创建命令 initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
  2. 在signalBlock中,创建RACSignal,并且作为signalBlock的返回值
  3. 执行命令 - (RACSignal *)execute:(id)input

二、RACCommand使用注意:

  1. signalBlock必须要返回一个信号,不能传nil.
  2. 如果不想要传递信号,直接创建空的信号[RACSignal empty];
  3. RACCommand中信号如果数据传递完,必须调用[subscriber sendCompleted],这时命令才会执行完毕,否则永远处于执行中。
  4. RACCommand需要被强引用,否则接收不到RACCommand中的信号,因此RACCommand中的信号是延迟发送的。

三、RACCommand设计思想:

内部signalBlock为什么要返回一个信号,这个信号有什么用。

  1. 在RAC开发中,通常会把网络请求封装到RACCommand,直接执行某个RACCommand就能发送请求。
  2. 当RACCommand内部请求到数据的时候,需要把请求的数据传递给外界,这时候就需要通过signalBlock返回的信号传递了。

四、如何拿到RACCommand中返回信号发出的数据。

  1. RACCommand有个执行信号源executionSignals,这个是signal of signals(信号的信号),意思是信号发出的数据是信号,不是普通的类型。
  2. 订阅executionSignals就能拿到RACCommand中返回的信号,然后订阅signalBlock返回的信号,就能获取发出的值。

五、监听当前命令是否正在执行executing

六、使用场景,监听按钮点击,网络请求

使用:

// 1.创建命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(@"执行命令");

// 返回空信号
//return [RACSignal empty];

// 2.创建信号 传递数据
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

[subscriber sendNext:@"请求数据"];

// 注意:数据传递完,最好调用sendCompleted,这时命令才执行完毕
[subscriber sendCompleted];

return nil;
}];
}];

// 强引用命令,不要被销毁,否则接收不到数据
_command = command;

// 3.订阅RACCommand的信号
[command.executionSignals subscribeNext:^(id x) {
[x subscribeNext:^(id x) {

NSLog(@"订阅RACCommand的信号: %@", x);
}];
}];

// RAC高级用法
// switchToLatest:用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
[command.executionSignals.switchToLatest subscribeNext:^(id x) {

NSLog(@"RAC高级用法: %@", x);
}];

// 4.监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
[[command.executing skip:1] subscribeNext:^(id x) {

if ([x boolValue] == YES) {

// 正在执行
NSLog(@"正在执行");

} else {

// 执行完毕
NSLog(@"执行完成");
}
}];

// 5.执行命名
[self.command execute:@1];

RACMulticastConnection

用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。

注意:RACMulticastConnection通过RACSignal的-publish或者-muticast:方法创建.

RACMulticastConnection使用步骤:

  1. 创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
  2. 创建连接 RACMulticastConnection *connect = [signal publish];
  3. 订阅信号,注意:订阅的不在是之前的信号,而是连接的信号。 [connect.signal subscribeNext:nextBlock]
  4. 连接 [connect connect]

RACMulticastConnection底层原理:

  1. 创建connect,connect.sourceSignal -> RACSignal(原始信号) connect.signal -> RACSubject
  2. 订阅connect.signal,会调用RACSubject的subscribeNext,创建订阅者,而且把订阅者保存起来,不会执行block。
  3. [connect connect]内部会订阅RACSignal(原始信号),并且订阅者是RACSubject
    1. 订阅原始信号,就会调用原始信号中的didSubscribe
    2. didSubscribe,拿到订阅者调用sendNext,其实是调用RACSubject的sendNext
  4. RACSubject的sendNext,会遍历RACSubject所有订阅者发送信号。
    • 因为刚刚第二步,都是在订阅RACSubject,因此会拿到第二步所有的订阅者,调用他们的nextBlock

需求:假设在一个信号中发送请求,每次订阅一次都会发送请求,这样就会导致多次请求。

解决:使用RACMulticastConnection就能解决.

问题:每次订阅一次都会发送请求

// 创建请求信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

NSLog(@"发送请求");
[subscriber sendNext:@1];

return nil;
}];

// 订阅信号
[signal subscribeNext:^(id x) {

NSLog(@"接受数据: %@", x);
}];

// 再次订阅信号,会再次执行发送请求,也就是每次订阅都会发送一次请求
[signal subscribeNext:^(id x) {

NSLog(@"接受数据: %@", x);
}];

输出:

2016-12-28 11:37:04.397 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 接受数据: 1
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 接受数据: 1

可以发现每次订阅都会重新发送请求.

下面我们使用RACMulticastConnection:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

NSLog(@"发送请求");
[subscriber sendNext:@1];

return nil;
}];

// 创建连接
RACMulticastConnection *connect = [signal publish];

// 订阅信号
// 注意:订阅信号,也不能激活信号,只是保存订阅者到数组,必须通过连接,当调用连接,就会一次性调用所有订阅者的SendNext
[connect.signal subscribeNext:^(id x) {

NSLog(@"订阅者1信号: %@", x);
}];

[connect.signal subscribeNext:^(id x) {

NSLog(@"订阅者2信号: %@", x);
}];

// 连接、激活信号
[connect connect];

输出:

2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 订阅者1信号: 1
2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 订阅者2信号: 1

RACScheduler

RAC中的队列,用GCD封装的。

RACUnit

表⽰stream不包含有意义的值,也就是看到这个,可以直接理解为nil.

RACEven

把数据包装成信号事件(signal event)。它主要通过RACSignal的-materialize来使用,然并卵。

ReactiveCocoa开发中常见用法

  1. 替换代理
  2. 替换KVO
  3. 监听事件
  4. 替换通知
  5. 监听文本框文字改变
  6. 统一处理多个网络请求

替换代理:

rac_signalForSelector:

rac_signalForSelector: 直接监听 Selector 事件的调用

应用场景:监听 RedViewController 中按钮的点击事件 btnTap:

跳转到RedViewController前,先使用rac_signalForSelector订阅rvc中的 btnTap: 点击事件

// 使用segue跳转
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
-
if ([segue.identifier isEqualToString:@"goRedVC"]) {

RedViewController *rvc = segue.destinationViewController;

// 订阅rvc中的 btnTap: 点击事件
[[rvc rac_signalForSelector:@selector(btnTap:)] subscribeNext:^(id x) {

NSLog(@"RedVC btnTap!");
}];
}
}

RedViewController.m 中的按钮事件

- (IBAction)btnTap:(id)sender {

NSLog(@"!");
}

替换KVO

rac_valuesForKeyPath:

// KVO
// 监听 slider 的 value 变化
[[self.slider rac_valuesForKeyPath:@"value" observer:nil] subscribeNext:^(id x) {

NSLog(@"slider value Change:%@", x);
}];

替换通知

rac_addObserverForName

// 原生的订阅通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(userDidChange:)
name:kTTCurrentUserLoggedOffNotification
object:nil];

// 使用RAC订阅通知 ,takeUntil限定信号的声明周期
[[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil]
takeUntil:[self rac_willDeallocSignal]]
subscribeNext:^(id x) {
NSLog(@"Notification received");
}];

监听事件

rac_signalForControlEvents:

// 监听 btn 的 UIControlEventTouchUpInside 点击事件
[[self.btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {

NSLog(@"btnTap");
}];

监听 textField 文字变化

rac_textSignal

[[self.textField rac_textSignal] subscribeNext:^(id x) {

NSLog(@"textField change: %@", x);
}];

统一处理多个网络请求

rac_liftSelector:

- (void)viewDidLoad {
[super viewDidLoad];

// 处理多个请求都返回结果的时候,统一处理
// 如同时进行多个网络请求,每个请求都正确返回时,再去刷新页面

RACSignal *signalOne = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

// 网络请求1
// ...

// 返回成功
[subscriber sendNext:@"网络请求1 data"];

return nil;
}];

RACSignal *signalTwo = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

// 网络请求2
// ...

// 返回成功
[subscriber sendNext:@"网络请求2 data"];

return nil;
}];

[self rac_liftSelector:@selector(updateWithR1:R2:) withSignalsFromArray:@[signalOne, signalTwo]];

}

// 更新界面
- (void)updateWithR1:(id)r1 R2:(id)r2 {

NSLog(@"R1:%@, R2:%@ 完成!", r1, r2);

}

注意

  • 替换KVO监听文本框文字改变 方法在创建监听方法时就会执行一次。

2016-12-28 16:53:50.746 ReactiveCacoa[4956:1246592] slider value Change:0.5 2016-12-28 16:53:50.748 ReactiveCacoa[4956:1246592] textField change:


- 使用`rac_liftSelector`时 `@selector(updateWithR1:R2:) `中的方 **参数个数** 要与 **signal个数** 相同,否则会被断言Crash!

定时器 你真的会使用吗?

· 阅读需 12 分钟
BY

定时器的使用是软件开发基础技能,用于延时执行或重复执行某些方法。

我相信大部分人接触iOS的定时器都是从这段代码开始的:

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES]

但是你真的会用吗?

正文

iOS定时器

首先来介绍iOS中的定时器

iOS中的定时器大致分为这几类:

  • NSTimer
  • CADisplayLink
  • GCD定时器

NSTimer

使用方法

NSTime定时器是我们比较常使用的定时器,比较常使用的方法有两种:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

这两种方法都是创建一个定时器,区别是用timerWithTimeInterval:方法创建的定时器需要手动加入RunLoop中。

// 创建NSTimer对象
NSTimer *timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 加入RunLoop中
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

需要注意的是: UIScrollView 滑动时执行的是 UITrackingRunLoopModeNSDefaultRunLoopMode被挂起,会导致定时器失效,等恢复为滑动结束时才恢复定时器。其原因可以查看我这篇《Objective-C RunLoop 详解》中的 “RunLoop 的 Mode“章节,有详细的介绍。

举个例子:

- (void)startTimer{
NSTimer *UIScrollView = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(action:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

- (void)action:(NSTimer *)sender {
static int i = 0;
NSLog(@"NSTimer: %d",i);
i++;
}

timer添加到NSDefaultRunLoopMode中,没0.5秒打印一次,然后滑动UIScrollView.

打印台输出:

可以看出在滑动UIScrollView时,定时器被暂停了。

所以如果需要定时器在 UIScrollView 拖动时也不影响的话,有两种解决方法

  1. timer分别添加到 UITrackingRunLoopModeNSDefaultRunLoopMode
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop mainRunLoop] addTimer:timer forMode: UITrackingRunLoopMode];
  1. 直接将timer添加到NSRunLoopCommonModes 中:
[[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes]; 

但并不是都timer所有的需要在滑动UIScrollView时继续执行,比如使用NSTimer完成的帧动画,滑动UIScrollView时就可以停止帧动画,保证滑动的流程性。

若没有特殊要求的话,一般使用第二种方法创建完timer,会自动添加到NSDefaultRunLoopMode中去执行,也是平时最常用的方法。

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES];

参数:

TimeInterval:延时时间

target:目标对象,一般就是self本身

selector:执行方法

userInfo:传入信息

repeats:是否重复执行

以上创建的定时器,若repeats参数设为NO,执行一次后就会被释放掉;

repeats参数设为YES重复执行时,必须手动关闭,否则定时器不会释放(停止)。

释放方法:

// 停止定时器
[timer invalidate];

实际开发中,我们会将NSTimer对象设置为属性,这样方便释放。

iOS10.0 推出了两个新的API,与上面的方法相比,selector换成Block回调以、减少传入的参数(那几个参数真是鸡肋)。不过开发中一般需要适配低版本,还是尽量使用上面的方法吧。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

###特点

  • 必须加入Runloop

    上面不管使用哪种方法,实际最后都会加入RunLoop中执行,区别就在于是否手动加入而已。

  • 存在延迟

    不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行,这个延迟时间大概为50-100毫秒.

    所以NSTimer不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了.

  • UIScrollView滑动会暂停计时

    添加到NSDefaultRunLoopModetimerUIScrollView滑动时会暂停,若不想被UIScrollView滑动影响,需要将 timer 添加再到 UITrackingRunLoopMode 或 直接添加到NSRunLoopCommonModes

##CADisplayLink

CADisplayLink官方介绍:

A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display

CADisplayLink对象是一个和屏幕刷新率同步的定时器对象。每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的 selector 就会被调用一次。

从原理上可以看出,CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染,或者做动画。 ###使用方法

创建:

@property (nonatomic, strong) CADisplayLink *displayLink;

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];

// 每隔1帧调用一次
self.displayLink.frameInterval = 1;

[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

释放方法:

[self.displayLink invalidate];  

self.displayLink = nil;

当把CADisplayLink对象添加到runloop中后,selector就能被周期性调用,类似于重复的NSTimer被启动了;执行invalidate操作时,CADisplayLink对象就会从runloop中移除,selector调用也随即停止,类似于NSTimer的invalidate方法。

CADisplayLink中有两个重要的属性:

  • frameInterval

    NSInteger类型的值,用来设置间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。

  • duration

    CFTimeInterval值为readOnly,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval

###特点

  • 刷新频率固定

    正常情况iOS设备的屏幕刷新频率是固定60Hz,如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。

  • 屏幕刷新时调用

    CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会

  • 适合做界面渲染

    CADisplayLink可以确保系统渲染每一帧的时候我们的方法都被调用,从而保证了动画的流畅性。

##GCD定时器

GCD定时器和NSTimer是不一样的,NSTimer受RunLoop影响,但是GCD的定时器不受影响,因为通过源码可知RunLoop也是基于GCD的实现的,所以GCD定时器有非常高的精度。关于GCD的使用可一看看这篇博客

###使用方法 创建GCD定时器定时器的方法稍微比较复杂,看下面的代码:

####单次的延时调用 NSObject中的performSelector:withObject:afterDelay:以及 performSelector:withObject:afterDelay:inModes: 这两个方法在调用的时候会设置当前 runloop 中 timer ,前者设置的 timerNSDefaultRunLoopMode 运行,后者则可以指定 NSRunLoopmode 来执行。我们上面介绍过 runloop 中 timerUITrackingRunLoopMode 被挂起,就导致了代码就会一直等待 timer 的调度,解决办法在上面也有说明。

不过我们可以用另一套方案来解决这个问题,就是使用GCD中的 dispatch_after 来实现单次的延时调用:

double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self someMethod];
});

####循环调用

// 创建GCD定时器
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0); //每秒执行

// 事件回调
dispatch_source_set_event_handler(_timer, ^{

dispatch_async(dispatch_get_main_queue(), ^{
// 在主线程中实现需要的功能

}
}

});

// 开启定时器
dispatch_resume(_timer);

// 挂起定时器(dispatch_suspend 之后的 Timer,是不能被释放的!会引起崩溃)
dispatch_suspend(_timer);

// 关闭定时器
dispatch_source_cancel(_timer);

上面代码中要注意的是:

  1. dispatch_source_set_event_handler()中的任务实在子线程中执行的,若需要回到主线程,要调用dispatch_async(dispatch_get_main_queue(), ^{}.
  • dispatch_source_set_timer 中第二个参数,当我们使用 dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时.

  • 第三个参数, 1.0 * NSEC_PER_SEC 为每秒执行一次,对应的还有毫秒,分秒,纳秒可以选择.

  • dispatch_source_set_event_handler 这个函数在执行完之后,block 会立马执行一遍,后面隔一定时间间隔再执行一次。而 NSTimer 第一次执行是到计时器触发之后。这也是和 NSTimer 之间的一个显著区别。

  • 挂起(暂停)定时器, dispatch_suspend 之后的 Timer,不能被释放的,会引起崩溃.

  • 创建的timer一定要有dispatch_suspend(_timer)dispatch_source_cancel(_timer)这两句话来指定出口,否则定时器将不执行,若我们想无限循环可将 dispatch_source_cancel(_timer) 写在一句永不执行的if判断语句中。

##使用场景

介绍完iOS中的各种定时器,接下来我们来说说这几种定时器在开发中的几种用法。 ###短信重发倒计时

短信倒计时使我们登录注册常用的功能,一般设置为60s,实现方法如下:

// 计时时间
@property (nonatomic, assign) int timeout;

/** 开启倒计时 */
- (void)startCountdown {

if (_timeout > 0) {
return;
}

_timeout = 60;

// GCD定时器
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0); //每秒执行

dispatch_source_set_event_handler(_timer, ^{

if(_timeout <= 0 ){// 倒计时结束

// 关闭定时器
dispatch_source_cancel(_timer);

dispatch_async(dispatch_get_main_queue(), ^{

//设置界面的按钮显示 根据自己需求设置
[self.sendMsgBtn setTitle:@"发送" forState:UIControlStateNormal];

self.sendMsgBtn.enabled = YES;

});

}else{// 倒计时中

// 显示倒计时结果

NSString *strTime = [NSString stringWithFormat:@"重发(%.2d)", _timeout];

dispatch_async(dispatch_get_main_queue(), ^{

//设置界面的按钮显示 根据自己需求设置

[self.sendMsgBtn setTitle:[NSString stringWithFormat:@"%@",strTime] forState:UIControlStateNormal];

self.sendMsgBtn.enabled = NO;

});

_timeout--;
}
});

// 开启定时器
dispatch_resume(_timer);

}

在上面代码中,我们设置了一个60s循环倒计时,当我们向服务器获取短信验证码成功时 调用该方法开始倒计时。每秒刷新按钮的倒计时数,倒计时结束时再将按钮 Title 恢复为“发送”.

有一点需要注意的是,按钮的样式要设置为 UIButtonTypeCustom,否则会出现刷新 Title 时闪烁.

我们可以把这个方法封装一下,方便调用,否则在控制器中写这么一大段代码确实也不优雅。

效果如下:

代码链接

###每个几分钟向服务器发送数据

在有定位服务的APP中,我们需要每个一段时间将定位数据发送到服务器,比如每5s定位一次每隔5分钟将再统一将数据发送服务器,这样会处理比较省电。 一般程序进入后台时,定时器会停止,但是在定位APP中,需要持续进行定位,APP在后台时依旧可以运行,所以在后台定时器也是可以运行的。

注:关于iOS后台常驻,可以查看这篇博客

在使用GCD定时的时候发现GCD定时器也可以在后代运行,创建方法同上面的短信倒计时.

这里我们使用NSTimer来创建一个每个5分钟执行一次的定时器.

#import <Foundation/Foundation.h>

typedef void(^TimerBlock)();

@interface BYTimer : NSObject

- (void)startTimerWithBlock:(TimerBlock)timerBlock;

- (void)stopTimer;

@end

#import "BYTimer.h"

@interface BYTimer ()

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TimerBlock timerBlock;

@end

@implementation BYTimer

- (void)startTimerWithBlock:(TimerBlock)timerBlock {

self.timer = [NSTimer timerWithTimeInterval:300 target:self selector:@selector(_timerAction) userInfo:nil repeats:YES];

[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
_timerBlock = timerBlock;

}

- (void)_timerAction {
if (self.timerBlock) {
self.timerBlock();
}
}

- (void)stopTimer {
[self.timer invalidate];
}

@end

该接口的实现很简单,就是 NSTimer 创建了一个300s执行一次的定时器,但是要注意定时器需要加入NSRunLoopCommonModes中。

要使定时器在后台能运行,app 就需要在 后台常驻

结语

最后总结一下:

NSTimer 使用简单方便,但是应用条件有限。

CADisplayLink 刷新频率与屏幕帧数相同,用于绘制动画。具体使用可看我封装好的一个 水波纹动画

GCD定时器 精度高,可控性强,使用稍复杂。

忽略 Xcode 8 中的注释警告

· 阅读需 1 分钟
BY

原因

从Xcode8.0开始,引入了文档注释警告,虽然是件好事,可是各种三方库爆出了一大堆警告:

解决方法:

Bulid Settings -> Documentation Comments -> NO

快速添加圆角和描边

· 阅读需 3 分钟
BY

对于习惯使用Storyboard的人来说,设置圆角、描边是一件比较蛋疼的事,因为苹果没有在xcode的Interface Builder上直接提供修改控件的圆角,边框设置。

我们来说说如何对某个控件进行圆角、描边处理:

初级

对于一个初学者来说,如果要进行某个控件的圆角、描边设置,就要从Storyboard关联出属性,然后再对属性进行代码处理。

如下代码:

self.myButton.layer.cornerRadius = 20;
self.myButton.layer.masksToBounds = YES;
self.myButton.layer.borderWidth = 2;
self.myButton.layer.borderColor = [UIColor blackColor].CGColor;

这样不仅需要Storyboard关联出属性,还要写一堆代码对属性进行设置,不得不说实在麻烦~

中级

更聪明的做法是使用Storyboard提供的Runtime Attributes为控件添加圆角描边。

选中控件,然后在Runtime Attributes框中输入对应的KeyTypeValue,这样程序在运行时就会通过KVC为你的控件属性进行赋值。(不仅仅是圆角、描边~)

如下图

设置圆角、描边的Key为:

layer.borderWidth
layer.borderColorFromUIColor
layer.cornerRadius
clipsToBounds

我这次在测试时,

这样做不用关联出属性,但是需要输入大串字符串,也是不够方便。

高级

创建UIView的分类,使用IBInspectable+ IB_DESIGNABLE关键字:

#import <UIKit/UIKit.h>

IB_DESIGNABLE

@interface UIView (Inspectable)

@property(nonatomic,assign) IBInspectable CGFloat cornerRadius;
@property(nonatomic,assign) IBInspectable CGFloat borderWidth;
@property(nonatomic,assign) IBInspectable UIColor *borderColor;

@end
#import "UIView+Inspectable.h"

@implementation UIView (Inspectable)

-(void)setCornerRadius:(CGFloat)cornerRadius{
self.layer.masksToBounds = YES;
self.layer.cornerRadius = cornerRadius;
}
-(void)setBorderColor:(UIColor *)borderColor{
self.layer.borderColor = borderColor.CGColor;
}
-(void)setBorderWidth:(CGFloat)borderWidth{
self.layer.borderWidth = borderWidth;
}

- (CGFloat)cornerRadius{
return self.layer.cornerRadius;
}
- (CGFloat)borderWidth{
return self.layer.borderWidth;
}
- (UIColor *)borderColor{
return [UIColor colorWithCGColor:self.layer.borderColor];
}

@end

附上:GitHub地址

直接使用

直接将这两个文件拖入项目中即可使用,在右边栏将会显示圆角和描边的属性设置

如图:

动态显示设置效果

直接使用的话只有在运行时才能看到效果,

例如要实时显示一个UIBUtton圆角、描边效果,需要创建一个类继承UIButton

#import <UIKit/UIKit.h>
#import "UIView+Inspectable.h"

@interface myButton : UIButton

@end
#import "myButton.h"

@implementation myButton

@end

只要将button的Class选择该空白类即可

关于IBInspectableIB_DESIGNABLE的使用详情可以参考这篇文章《谈不完美的IBDesignable/IBInspectable可视化效果编程》