跳到主要内容

Git指令整理

· 阅读需 5 分钟
BY

随便整理的一些自用的Git指令

	echo "# 项目名" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:qiubaiying/项目名.git
git push -u origin master

若仓库存在直接push

	git remote add origin git@github.com:qiubaiying/test.git
git push -u origin master

常用操作

创建仓库(初始化)

	git init

新建一个仓库目录
git init [project-name]

克隆一个远程项目
git clone [url]

添加文件到缓存区

	添加所有变化的文件
git add .

添加名称指定文件
git add text.txt```

#### 配置

设置提交代码时的用户信息 git config [--global] user.name "[name]" git config [--global] user.email "[email address]"

	
#### 提交
``` 提交暂存区到仓库区
git commit -m "msg"

# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]

# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

远程同步

	# 下载远程仓库的所有变动
$ git fetch [remote]

# 显示所有远程仓库
$ git remote -v

# 显示某个远程仓库的信息
$ git remote show [remote]

# 增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]

# 取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]

# 上传本地指定分支到远程仓库
$ git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force

# 推送所有分支到远程仓库
$ git push [remote] --all

分支

	# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 列出所有本地分支和远程分支
$ git branch -a

# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]

# 新建一个分支,并切换到该分支
$ git checkout -b [branch]

# 新建一个分支,指向指定commit
$ git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
$ git checkout [branch-name]

# 切换到上一个分支
$ git checkout -

# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch]

# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]

标签Tags

	添加标签 在当前commit
git tag -a v1.0 -m 'xxx'

添加标签 在指定commit
git tag v1.0 [commit]

查看
git tag

删除
git tag -d V1.0

删除远程tag
git push origin :refs/tags/[tagName]

推送
git push origin --tags

拉取
git fetch origin tag V1.0

新建一个分支,指向某个tag
git checkout -b [branch] [tag]

查看信息

	# 显示有变更的文件
$ git status

# 显示当前分支的版本历史
$ git log

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 搜索提交历史,根据关键词
$ git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示过去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]

# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交发生变化的文件
$ git show --name-only [commit]

# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

撤销

	# 恢复暂存区的指定文件到工作区
$ git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
$ git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
$ git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
$ git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
$ git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]

# 暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop

其他

	# 生成一个可供发布的压缩包
$ git archives

Mac 文本转音频

· 阅读需 1 分钟
BY

分享一条在Mac上将一个文本转换为音频文件的终端命令,个人认为还是蛮实用的。

来自: <http://25.io/toau/>

指令:

cat sample.txt | say -o sample.aiff

快速搭建个人博客

· 阅读需 20 分钟
BY

正所谓前人栽树,后人乘凉。

感谢Huxpro提供的博客模板

我的的博客

从 Jekyll 到 GitHub Pages 中间踩了许多坑,终于把我的个人博客BY Blog搭建出来了。。。

本教程针对的是不懂技术又想搭建个人博客的小白,操作简单暴力且快速。当然懂技术那就更好了。

看看看博客的主页样式:

在手机上的布局:

废话不多说了,开始进入正文。

快速开始

从注册一个Github账号开始

我采用的搭建博客的方式是使用 GitHub Pages + jekyll 的方式。

要使用 GitHub Pages,首先你要注册一个GitHub账号,GitHub 是全球最大的同性交友网站(吐槽下程序员~),你值得拥有。

拉取我的博客模板

注册完成后搜索 qiubaiying.github.io 进入我的仓库

点击右上角的 Fork 将我的仓库拉倒你的账号下

稍等一下,点击刷新,你会看到Fork了成功的页面

修改仓库名

点击settings进入设置

修改仓库名为 你的Github账号名.github.io,然后 Rename

这时你在在浏览器中输入 你的Github账号名.github.io 例如:baiyingqiu.github.io

你将会看到如下界面

说明已经成功一半了😀。。。当然,还需要修改博客的配置才能变成你的博客。

若是出现

则需要 [检查一下你的仓库名是否正确]

整个网站结构

修改Blog前我们来看看Jekyll 网站的基础结构,当然我们的网站比这个复杂。

├── _config.yml
├── _drafts
| ├── begin-with-the-crazy-ideas.textile
| └── on-simplicity-in-technology.markdown
├── _includes
| ├── footer.html
| └── header.html
├── _layouts
| ├── default.html
| └── post.html
├── _posts
| ├── 2007-10-29-why-every-programmer-should-play-nethack.textile
| └── 2009-04-26-barcamp-boston-4-roundup.textile
├── _data
| └── members.yml
├── _site
├── img
└── index.html

很复杂看不懂是不是,不要紧,你只要记住其中几个OK了

  • _config.yml 全局配置文件
  • _posts 放置博客文章的文件夹
  • img 存放图片的文件夹

其他的想继续深究可以看这里

修改博客配置

来到你的仓库,找到_config.yml文件,这是网站的全局配置文件。

点击修改

然后编辑_config.yml的内容

接下来我们来详细说说以下配置文件的内容:

基础设置

# Site settings
title: You Blog #你博客的标题
SEOTitle: 你的博客 | You Blog #显示在浏览器上搜索的时候显示的标题
header-img: img/post-bg-rwd.jpg #显示在首页的背景图片
email: You@gmail.com
description: "You Blog" #网站介绍
keyword: "BY, BY Blog, 柏荧的博客, qiubaiying, 邱柏荧, iOS, Apple, iPhone" #关键词
url: "https://qiubaiying.github.io" # 这个就是填写你的博客地址
baseurl: "" # 这个我们不用填写

侧边栏

# Sidebar settings
sidebar: true # 是否开启侧边栏.
sidebar-about-description: "说点装逼的话。。。"
sidebar-avatar:/img/avatar-by.JPG # 你的个人头像 这里你可以改成我在img文件夹中的两张备用照片 img/avatar-m 或 avatar-g

社交账号

展示你的其他社交平台

在下面你的社交账号的用户名就可以了,若没有可不用填

# SNS settings
RSS: false
weibo_username: username
zhihu_username: username
github_username: username
facebook_username: username
jianshu_username: jianshu_id

新加入了简书jianshu_id 在你打开你的简书主页后的地址如:http://www.jianshu.com/u/e71990ada2fd中,后面这一串数字:e71990ada2fd

评论系统

博客中使用的是 Disqus 评论系统,在 官网 注册帐号后,按下面的步骤简单的配置即可:

进入 设置页面 配置个人信息

配置 Disqus 个人信息

找到 Username

Disqus Account

这个 Username 就是我们 _config.ymldisqus_username

# Disqus settings(https://disqus.com/)
disqus_username: qiubaiying

很对人反映 Disqus 评论插件加载不出来,因为 Disqus 在国内加载缓慢,所以我新集成了 Gitalk 评论插件(感谢@FeDemo的推荐),喜欢折腾的朋友可以看这篇:《为博客添加 Gitalk 评论插件》。 我已经在_config.yml 配置就好了,只需要填写参数可以了。

网站统计

集成了 Baidu AnalyticsGoogle Analytics,到各个网站注册拿到track_id替换下面的就可以了

这是我的 Google Analytics

不要使用我的track_id😂。。。

若不想启用统计,直接删除或注释掉就可以了

# Analytics settings
# Baidu Analytics
ba_track_id: 83e259f69b37d02a4633a2b7d960139c

# Google Analytics
ga_track_id: 'UA-90855596-1' # Format: UA-xxxxxx-xx
ga_domain: auto

好友

friends: [
{
title: "简书·BY",
href: "http://www.jianshu.com/u/e71990ada2fd"
},{
title: "Apple",
href: "https://apple.com"
},{
title: "Apple Developer",
href: "https://developer.apple.com/"
}
]

保存

讲网页拉倒底部,点击 Commit changes 提交保存

再次进入你的主页,

恭喜你,你的个人博客搭建完成了😀。

写文章

利用 Github网站 ,我们可以不用学习git,就可以轻松管理自己的博客

对于轻车熟路的程序猿来说,使用git管理会更加方便。。。

创建

文章统一放在网站根目录下的 _posts 的文件夹中。

创建一个文件

在下面写文章,和标题,还能实时预览,最后提交保存就能看到自己的新文章了。

格式

每一篇文章文件命名采用的是2017-02-04-Hello-2017.md时间+标题的形式,空格用-替换连接。

文件的格式是 .mdMarkDown 文件。

我们的博客文章格式采用是 MarkDown+ YAML 的方式。

YAML 就是我们配置 _config文件用的语言。

MarkDown 是一种轻量级的「标记语言」,很简单。花半个小时看一下就能熟练使用了

大概就是这么一个结构。

---
layout: post # 使用的布局(不需要改)
title: My First Post # 标题
subtitle: Hello World, Hello Blog #副标题
date:       2017-02-06 # 时间
author: BY # 作者
header-img: img/post-bg-2015.jpg #这篇文章标题背景图片
catalog: true # 是否归档
tags: #标签
- 生活
---

## Hey
>这是我的第一篇博客。

进入你的博客主页,新的文章将会出现在你的主页上.

按格式创建文章后,提交保存。进入你的博客主页,新的文章将会出现在你的主页上.

到这里,恭喜你!

你已经成功搭建了自己的个人博客以及学会在博客上撰写文字的技能了(是不是有点小兴奋🙈)。

首页标签

在首页可以看到这些特色标签,当你的文章出现相同标签(默认相同的标签数量大于1),才会自动生成。

所以当你只放一篇文章的时候是不会出现标签的。

建站的初期,博客比较少,若你想直接在首页生成比较多的标签。你可以在 _congfig.yml中找到这段:

# Featured Tags
featured-tags: true # 是否使用首页标签
featured-condition-size: 1 # 相同标签数量大于这个数,才会出现在首页

将其修改为featured-condition-size: 0, 这样只有一个标签时也会出现在首页了。

相反,当你博客比较多,标签也很多时,这时你就需要改回 1 甚至是 2 了。

自定义域名

搭建好博客之后 你可能不想直接使用 baiyingqiu.github.io 这么长的博客域名吧, 想换成想 qiubaiying.top 这样简短的域名。那我们开始吧!

购买域名

首先,你必须购买一个自己的域名。

我是在阿里云购买的域名

阿里云 app也可以注册域名,域名的价格根据后缀的不同和域名的长度而分,比如我这个 qiubaiying.top 的域名第一年才只要4元~

域名尽量选择短一点比较好记住,注意,不能选择中文域名,比如 张三.top ,GitHub Pages 无法处理中文域名,会导致你的域名在你的主页上使用。

注册的步骤就不在介绍了

解析域名

注册好域名后,需要将域名解析到你的博客上

管理控制台 → 域名与网站(万网) → 域名

选择你注册好的域名,点击解析

添加解析

分别添加两个A 记录类型,

一个主机记录为 www,代表可以解析 www.qiubaiying.top的域名

另一个为 @, 代表 qiubaiying.top

记录值就是我们博客的IP地址,是 GitHub Pagas 在美国的服务器的地址 151.101.100.133

可以通过 这个网站 或者直接在终端输入ping 你的地址,查看博客的IP

ping qiubaiying.github.io

细心地你会发现所有人的博客都解析到 151.101.100.133 这个IP。

然后 GitHub Pages 再通过 CNAME记录 跳转到你的主页上。

修改CNAME

最后一步,只需要修改 我们github仓库下的 CNAME 文件。

选择 CNAME 文件

使用的注册的域名进行替换,然后提交保存

这时,输入你自己的域名,就可以解析到你的主页了。

大功告成!

进阶

若你对博客模板进行修改,你就要看看 Jekyll 的开发文档,是中文文档哦,对英语一般的朋友简直是福利啊(比如说我😀)。

还要学习 GitGitHub 的工作机制了及使用。

你可以先看看这个git教程,对git有个初步的了解后,那么相信你就能将自己图片传到GitHub仓库上,或者可以说掌握了 使用git管理自己的GitHub仓库 的技能呢。

对于轻车熟路的程序猿来说,这篇教程就算就结束了,因为下面的内容对于你们来说 so eazy~

但相信很多小白都一脸懵逼,那我们继续👇。

利用GithHub Desktop管理GitHub仓库

GithHub DesktopGithHub 推出的一款管理GitHub仓库的桌面软件,换句话说就是将你在Github上的文件同步到本地电脑上,并将修改后的文件同步到Github远程仓库。

下载

点击图片进入下载页面,选择对应的平台进行下载

下面以Mac平台为例:

安装

将下载好的文件解压,将这只小猫拖到应用程序文件夹中

就可以在Launchpad找到这只小猫咪~

登录

点开应用,会弹出登录框,

输入你的GitHub账号和密码进行登录

登录后关闭窗口

然后返回引导窗,一直按 Continue 继续

Continue

还是Continue~

进入主界面,先 右键Remve 删除这个用户指导,贼烦~

克隆仓库

选择你的仓库克隆到本地

管理仓库

现在文件夹中打开

打开后你会的发现文件结构和你在Github上的一模一样~

你最先关心的可能是你的头像~在img文件夹中把替换我的头像就好了。

不仅是图片,所有在Github上的的操作都可以进行。

保存修改

当你对仓库文件夹的文件下进行修改、添加或删除时,都可以在 GitHub Desktop 中看到

例如我在 img 中添加了一张图片 avatar-demo.png 添加了一张图片

就可以在看到GitHub Desktop显示了我的修改

保存修改只要按 Commit to master,然后可以写上你的修改说明

同步

将修改同步到 GitHub 远程仓库上只需要一步:点击右上角的同步按钮

完成

打开你的GitHub上的仓库,你就可以看到已经和本地同步了

可以看到你提交的详情: add img

这样,你已经能轻松管理自己的博客了。

想上传头像,背景,或者是删掉你不要的图片(我的头像😏)已经是 so eazy了吧~

注意

你在 GitHub 网站上进行 Commit 操作后,需要在GitHub Desktop上按一下 同步按键 才能同步网站上的修改到你的本地。

修改个人介绍

修改个人介绍需要修改根目录下的 about.html 文件

看不懂 HTML 标签?没关系,对照着修改就好了~ 还有注意这个有中英介绍

常见问题

最近有很多人给我提问题,我这边总结一下

配置文件修改后没有效果

刷新几遍浏览器就好了~

不行的话,先清除浏览器缓存再试试。

404错误

  1. 检查你的仓库名是否有按照要求填写
  2. 确定 Fork 的是不是我的仓库~

修改CNAME文件,域名还是不变

清除浏览器缓存就OK~

其他问题

直接在评论中提出来或私信我,我会一一替大家解决的😀

其他

最近有人往我的远程仓库不停的 push,一天连收几十封邮件!例如像这样的

原因大多是直接Clone了我的仓库到本地,没有删除我的远程仓库地址,添加完自己的仓库地址后,一口气推送到所有远程仓库(包括我的😂)~

打扰了我的工作和生活~

所以,请不要往我的仓库上推送分支

我发现一个问题是,很多人每次修改博客的内容都commit一次到远程仓库,然后再查看修改结果,这样效率非常低!

来,上车!

在本地调试博客

注:下面的操作是在 Mac 终端进行的。 Windows 环境下的配置请参考 @梦幻之云 提供的 这篇文章

有心的同学在 jekyll官网 就会发现 jekyll 的 提供的实例代码。

~ $ gem install jekyll bundler
~ $ jekyll new my-awesome-site
~ $ cd my-awesome-site
~/my-awesome-site $ bundle install
~/my-awesome-site $ bundle exec jekyll serve
# => 打开浏览器 http://localhost:4000

这段命令创建了一个默认的 jekll 网站,然后在本机的 4000 窗口展示。聪明的你应该发现怎么做了吧~

安装 jekylljekyll bundler

$ gem install jekyll
$ gem install jekyll bundler

进入你的 Blog 所在目录,然后创建本地服务器

$ jekyll s

然后会显示

 Auto-regeneration: enabled for '/Users/baiying/Blog'
Configuration file: /Users/baiying/Blog/_config.yml
Server address: http://127.0.0.1:4000/
Server running... press ctrl-c to stop.

你就可以在 <http://127.0.0.1:4000/> 看到你的博客,你对本地博客的修改都会在这个地址进行显示,这大大提高了对博客的配置效率。

使用ctrl+c就可以停止 serve

Star

若本教程顺利帮你搭建了自己的个人博客,请不要 害羞,给我的 github仓库 点个 star 吧!

因为最近发现 Fork 将近破百,加上直接 Clone 仓库的,保守估计已经帮助上百人成功的搭建了自己的博客,~~可是 Star 却仅仅只有 12!可能还是做的不够好吧!~~现在已经破百了,感谢大家的Star!

别无他求,点个 Star

心满意足!

补充

修改网站的 icon

要修改如图所示的网站 icon

在博客 img 目录下找到并替换 favicon.ico 这个图标即可,图标尺寸为32x32

修改主页的座右铭

最近有不少小伙伴私信我:如何修改主页的座右铭?

就是这个:

很简单,找到博客目录下的 index.html 文件,修改这句话就可以了。

如何在博客文章中上插入图片

博客的文章用的是 MarkDown 格式,如果没用过 MarkDown 真的 强烈推荐 花半个小时学习一下

MarkDown 中添加图片的形式是 :![](图片的URL)

例如:

![MarkDown示例图片](http://upload-images.jianshu.io/upload_images/2178672-eb2effd6b942a500.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)就会显示下面这张图片

MarkDown示例图片

https://ws3.sinaimg.cn/large/006tNc79gy1fj9xhjzobbj30yg0my75z.jpg就是这张图片的URL,我们可以在浏览器输入这个URL找到或下载这张图片。

所以,要在 MacDown 中插入图片,这张图片就需要上传到图床(网上),然后在引 用这张图片的URL。

将图片上传到图床

Mac 上的图床神器:iPic

直接在App Store上下载,谁用谁知道!

使用方法很简单,直接拖动图片到 P 图标上,或者选中图片按快捷键 ⌘+U,就能请示上传。

上传成功就能直接粘贴图片的URL。

iPic

用 iPic 上传图片后,获取URL插入文章中就可以了。

iPic上传图片

推荐几个好用软件

MarkDown编辑器

MacDown:可能是Mac上最好的MacDown编辑器了

图片压缩工具

ImageOptim

对于我们的博客来说,图片越大,加载速度越慢。

不信你用手机打开你的博客试试~

所以有必要对我们上传到博客网站中的图片:指的是你的头像,首页背景图片,文章背景图片等。对于博客文章中插入的图片,其实也可以压缩了再上传。

对博客中的所有图片进行压缩:

看看压缩结果,最高的一张压缩了78.7%,这简直是太可怕了!

ImageOptim压缩图片

好了,现在个人博客的加载速度估计要起飞了~

最后要说个事情

我在博客中的文章,你们可以保留,让更多需要帮助人的看到,当然也可以删除。

但是,我发现居然有人把文章的作者改成了自己,然后当成自己的文章放在自己的博客上,这就令人感到气愤了。

比如说向我请教问题的这位:

我在博客中的每篇文章都是我一字一句敲出来的,转载的文章我也注明了出处,表示对原作者的尊重。同时也希望大家都能尊重我的付出。

谢谢~

Mac 快速调出终端

· 阅读需 2 分钟
BY

在Mac下快速调出终端的方法是:为终端添加一个快捷键打开方式

为终端添加一个快捷键打开方式

打开Mac下自带的软件 Automator

新建文稿

创建一个服务

修改框内的脚本

on run {input, parameters}
tell application "Terminal"
reopen
activate
end tell
end run

运行:command + R,如果没有问题,则会打开终端

保存:Command + S,将其命名为打开终端或你想要的名字

设置快捷键

系统偏好设置 -> 键盘设置 -> 快捷键 -> 服务

选择我们创建好的 '打开终端',设置你想要的快捷键,比我我设置了⌘+空格

到此,设置完成。

聪明的你也许会发现,这个技巧能为所有的程序设置快捷启动。

将脚本中的 Terminal 替换成 其他程序就可以

on run {input, parameters}
tell application "Terminal"
reopen
activate
end tell
end run

黑技能

既然学了 Automator ,那就在附上一个黑技能吧。为你的代码排序。在 Xcode8以前,有个插件能为代码快速排序,不过时过境迁~ 对于没用的插件而且又有患有强迫症的的小伙伴,只能手动排序了(😂).

首先还是创建一个服务

创建一个Shell脚本,

勾选:用输出内容替换所选文本

输入:sort|uniq

保存: 存为Sort & Uniq

选中你的代代码 -> 鼠标右键 -> Servies -> Sort&Uniq

排序后的代码:

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可视化效果编程》