# AI 全自动付流水线

AI 跑 5 小时，我干 15 分钟：一次 feature 交付的杠杆实验

<figure><img src="https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lw8fe5ohtBLKudCKXmM%2Fuploads%2FSjkLuz616Hw96vONDZng%2Fwhiteboard_exported_image%20(3).png?alt=media&amp;token=4f588e27-8184-4438-a934-a554635f88c6" alt=""><figcaption></figcaption></figure>

前两天有一个看起来不大的需求：调一下菜单分组、把一个内部页面对全员开放、再加一道权限。

**10:58** 我把需求扔给 AI，**15:23** 生产环境 `ci-deploy = success`。

中间 5 小时，我只做了 4 件事：

1. 澄清需求（5 分钟）
2. 批 plan（1 分钟）
3. 给 4 套测试账号（1 分钟）
4. 看 PR + 按 merge（2 分钟）

**加上看一眼飞书审批卡的 30 秒，全部有效介入：约 15 分钟。**

不是炫技，是想分享一种 **「让 AI 把流水线的每个工位都跑完，人只守关键的批准门」** 的协作姿势。这是一次**杠杆实验**——把自己挪到流水线最有杠杆的位置，剩下的让 AI 接力跑。一个人能干一个小团队的活。

### 一、核心思路：把交付拆成「专员」流水线

```mermaid
flowchart LR 
    Intent(["意图"]) --> Plan["计划"] 
    Plan --> Code["编码"] 
    Code --> Verify["验证"] 
    Verify --> Gate["审批门"] 
    Gate --> Deploy["部署"] 
    Deploy --> Wrap(["收尾"]) 
    
    classDef human fill:#ffe5e5,stroke:#c00,stroke-width:2.5px,color:#000 
    classDef agent fill:#e5f0ff,stroke:#06f,color:#000 
    
    class Intent,Gate human
    class Plan,Code,Verify,Deploy,Wrap agent
```

不是让一个超级 AI 做所有事，而是把交付拆成 **专门职责** 的小角色 (subagent)，每个角色：

* 有自己的 **目录边界**（越界会被 hook 拦下）
* 有自己的 **工具白名单**（只调它该调的）
* 跑完写一份**轨迹文件**留在硬盘上，让下一棒能接上

整条链：**意图 → 计划 → 编码 → 验证 → 审批 → 部署 → 收尾**，每一棒都是独立 agent。

> 关键点：人不是中介，是**审批节点**。AI 之间的 handoff 走文件，不走对话。

***

### 二、全流程时间线（5 小时内闭环）

#### 步骤 1️⃣ 澄清需求（10–15 分钟）

我把需求扔给 AI，它启动了一个「头脑风暴」流程——**一次只问一个问题**：

> "Q1：菜单这个分组改完后，普通用户在哪个分组里看到这个新功能？给你 3 个方案 + 推荐 + 各自的 trade-off"

我选 C，它接着问 Q2、Q3。聊完输出一份完整设计稿，**等我点头**才动手。

> 这一步是整条流水线**最贵也最便宜**的一段——AI 帮我把模糊念头拧成精确规格，避免后面五个 agent 在错误前提上做无用功。

→ 产物：`intents/<日期>-<slug>.md`

![Screenshot 2026-04-28 at 16.46.47.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/94f2999856af419bbf63d5a326c9d93b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3F1YXJlenc=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTgyMDQ0Njk4NTgyOTE5NyJ9\&rk3s=f64ab15b\&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\&x-orig-expires=1779706409\&x-orig-sign=c%2BRjY0Btau%2FRiHYktP%2F75Fpo%2FC0%3D)

***

#### 步骤 2️⃣ 出实施计划（2 分钟）

派一个**只读的「架构师」agent**，看完意图 + 项目元数据 + 最近 20 条踩坑日志，写出**分阶段实施计划**。每阶段都标：

* ✅ 完成判定（具体到一行命令的输出）
* ❌ 不做什么（明确范围之外的部分）

它**不能改代码**——这是设计上的边界，避免它边规划边乱搞。

→ 产物：`runs/<slug>/plan.md`

***

#### 步骤 3️⃣ 我批 plan（1 分钟）

看 plan，没异议就回"批准"。这是第一道**人类闸门**。

***

#### 步骤 4️⃣ 编码（约 10 分钟）

派**项目专员 agent**——它的工作目录被锁死在某一个 repo 里，碰其它 repo 会被 hook 弹回来。

它读 plan 后自己干：

* 改代码
* 跑类型检查
* 清前端构建缓存
* 写一份「我改了啥 + 你需要重启 dev server」的报告

**不 commit、不 push、不开 PR**——这些动作要等审批门。

→ 产物：`runs/<slug>/dev-report.md`

***

#### 步骤 5️⃣ 浏览器验证（约 80 分钟，可放着不管）

派 **「视觉验收 agent」**。它用浏览器自动化工具：

* 拿 4 套测试账号轮流登录
* 每个账号下，每张关键页都拍 **3 档屏幕尺寸**（手机 / 平板 / 笔记本）
* 跑 5 类预设探针：CJK 文字截断、表格列宽溢出、flex 兄弟挤压、纵向堆叠、网络 4xx

最后输出 **「GO / NO-GO」判定**：

| 结果                             | 数量           |
| ------------------------------ | ------------ |
| 截图归档                           | 21 张         |
| API 鉴权用例（是否 403？未登录 → 是否 401？） | 9 条全过        |
| pre-existing layout 警告（不阻塞）    | 1 条，记录留到下次治理 |

→ 产物：`runs/<slug>/verification-report.md` + `screenshots/`

![Screenshot 2026-04-28 at 16.53.30.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/4d04219d7b91490392be34746e589918~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3F1YXJlenc=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTgyMDQ0Njk4NTgyOTE5NyJ9\&rk3s=f64ab15b\&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\&x-orig-expires=1779706409\&x-orig-sign=l6hnVFCIWJRp%2FQu%2Bqqk9VDXCoqM%3D)

***

#### 步骤 6️⃣ 审批门（这一步必须有人）

派\*\*「部署看门人 agent」\*\*。它干三件事：

1. 把 diff、截图、curl 结果、安全审查凑成一张**互动审批卡**，推到飞书
2. 写 `gate.json` 标记 `status=pending`
3. 然后**就不动了**——等人在手机上点"批准"

这是设计上最关键的"人类时刻"。**AI 不能自批，无论多绿都不行**。

![Screenshot 2026-04-28 at 16.54.58.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9c6d03a0e11545688dc07b05270f5019~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3F1YXJlenc=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTgyMDQ0Njk4NTgyOTE5NyJ9\&rk3s=f64ab15b\&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\&x-orig-expires=1779706409\&x-orig-sign=yRzwOWCBxVt580auEIzmo5oT7vg%3D)

***

#### 步骤 7️⃣ 我点"批准"（30 秒）

收到飞书卡 → 我看一眼 **5 个改动文件 + 9 条 curl 结果 + 截图** → 在主对话回 `/approve <slug>`。

`gate.json` 翻成 approved → 触发后续动作。

***

#### 步骤 8️⃣ 提交 → PR → 自动部署（约 3 分钟）

批准后 AI 自己接着跑：

1. `git checkout -b feat/...` 切 feature 分支
2. **英文 commit**（写在 memory 里：PR 一律英文）
3. push 到 GitHub
4. `gh pr create` 开 PR

**第二道人类闸门**：merge 按钮我按。这条产线规则是 "merge=立即部署"，所以我必须看清楚才合。

我 merge 完跟 AI 说一声 → 它立即：

* 把 GitHub 的 commit 镜像同步到镜像源（生产服务器从那边拉代码）
* 查 CI 流水线状态：`ci-deploy = success` ✅

![Screenshot 2026-04-28 at 16.57.04.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/000d13258253416fb54b3f78570cf2b9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3F1YXJlenc=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTgyMDQ0Njk4NTgyOTE5NyJ9\&rk3s=f64ab15b\&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\&x-orig-expires=1779706409\&x-orig-sign=hoBiCgZGM2HIJTXwotiojcuOs4I%3D)

***

#### 步骤 9️⃣ 收尾（1 分钟）

AI 自动：

* `gate.json.status = shipped`
* 写一份 `done.md` 列出 PR / commit / CI / 截图位置
* 把"踩坑教训"追加到团队的 `feedback-log.md`（下次规划时其它 agent 会读）
* 把意图文件归档到这次交付的目录里
* 给飞书发一条 SHIPPED 通知

***

### 三、用到的工具与技术手段

> 不讲实现细节，只讲**用它解决了什么问题**。

| 工具 / 手段                     | 解决的痛点                                                |
| --------------------------- | ---------------------------------------------------- |
| **专职 subagent**（10+ 个角色）    | 让每个 agent 专注一件事；上下文不污染；权限边界清晰                        |
| **目录 / 工具白名单（hook 拦截）**     | 防止 agent 越界改错项目；权限即代码                                |
| **文件作为唯一 handoff 渠道**       | agent A 写 plan.md，agent B 读 plan.md；不靠"记忆传话"         |
| **审批门 + 飞书互动卡**             | 关键决策必须人类盖章；手机点一下就回到流水线                               |
| **「头脑风暴」一次一问**              | 强制把模糊需求收敛成精确设计，避免 AI 自由发挥                            |
| **多档 viewport 自动截图**        | 单分辨率会漏 50% 的 UI bug；CJK 在某些尺寸下会竖排                    |
| **角色 × 视图矩阵自动跑**            | 4 角色 × 3 屏幕 × N 页面，人工跑要 1 小时，AI 跑 80 分钟（包含登录、拍照、写报告） |
| **预设 probe（探针）**            | 把"上次踩的坑"沉淀成自动检查项，下次不再踩                               |
| **feedback-log（团队共享教训）**    | 每次交付沉淀一行经验，下一棒 planner 必读                            |
| **多 remote 同步（CI 一边，生产一边）** | 让 CI 触发与生产代码源解耦，便于跨网络环境部署                            |
| **审批 KV + webhook 轮询**      | 手机点了"批准"，桌面上的流水线立刻继续                                 |

### 四、回到杠杆这件事

回到开头那 15 分钟 vs 5 小时的账。我具体是怎么花的：

| 我做了什么          | 时长                             |
| -------------- | ------------------------------ |
| 写需求            | 5 分钟                           |
| 看 plan         | 1 分钟                           |
| 给 4 套测试账号      | 1 分钟                           |
| 看飞书卡批准         | 30 秒                           |
| 看 PR 内容点 merge | 2 分钟                           |
| **小计**         | **约 10 分钟（含等待切换的零碎时间约 15 分钟）** |

剩下 5 小时，是 AI 在跑、在等我。

这不是"AI 取代我"——是**我把自己挪到了流水线最有杠杆的位置**：定方向、把关键、踩刹车。

杠杆点在哪？三个动作：

1. **澄清** — 决定要做什么、不做什么。一次决策影响后面 5 棒
2. **批 plan** — 决定怎么做。看一遍计划，避免后面跑错方向
3. **批审批卡 + merge PR** — 决定该不该上线。最终质量门

这三件事**没法机械化**，必须人来做。其它**重复、可机械化、有明确判定标准**的工作（写 boilerplate / 跑 4 角色登录截图 / 拼审批卡 / 同步镜像）全部交给流水线。

最让我满意的不是速度，是**可追溯**——`runs/<slug>/` 这个文件夹里 11 个文件完整记录了这次交付的每一步：意图、计划、改了啥、测了啥、审批了什么、部署了什么、踩了什么坑。半年后再翻，整个故事都在。

> AI 协作的下一步不是「prompt 写得多好」，而是「**流水线设计得多清晰**」。 让人守关键门、AI 跑工位，是当下能落地、效果可验证的最优解。

***

### 附：流程图源码（Mermaid）

```mermaid
flowchart LR
    Start([需求]) --> Brainstorm["1. 澄清<br/>(头脑风暴 agent)<br/>10-15 min"]
    Brainstorm --> IntentMd[["intents/&lt;slug&gt;.md"]]
    IntentMd --> Planner["2. 出计划<br/>(只读架构师 agent)<br/>2 min"]
    Planner --> PlanMd[["runs/&lt;slug&gt;/plan.md"]]
    PlanMd --> Approve1{{"3. 我批 plan<br/>👤 1 min"}}
    Approve1 -->|批准| Dev["4. 编码<br/>(项目专员 agent)<br/>10 min"]
    Dev --> DevMd[["runs/&lt;slug&gt;/dev-report.md"]]
    DevMd --> Verify["5. 浏览器验证<br/>(视觉验收 agent)<br/>80 min"]
    Verify --> VerifyMd[["verification-report.md<br/>+ 21 截图 + 9 curl"]]
    VerifyMd --> Gate["6. 部署看门人<br/>推飞书审批卡"]
    Gate --> Approve2{{"7. 我点批准<br/>👤 30 sec"}}
    Approve2 -->|approved| Ship["8. commit/push/PR<br/>(自动)"]
    Ship --> Approve3{{"merge PR<br/>👤 2 min"}}
    Approve3 -->|merged| Sync["sync 镜像<br/>+ 监控 CI"]
    Sync --> Done["9. 收尾<br/>gate=shipped<br/>+ feedback-log"]
    Done --> End([部署成功])

    classDef human fill:#ffe5e5,stroke:#c00,stroke-width:2px
    classDef agent fill:#e5f0ff,stroke:#06f
    classDef file fill:#fff8d5,stroke:#999,stroke-dasharray: 3 3
    class Approve1,Approve2,Approve3 human
    class Brainstorm,Planner,Dev,Verify,Gate,Ship,Sync,Done agent
    class IntentMd,PlanMd,DevMd,VerifyMd file
```

**图例**：

* 🔵 蓝框 = AI 自动跑的工位
* 🔴 红框 = 必须人类介入的审批门
* 🟡 黄虚线框 = 落盘的轨迹文件（agent 之间的 handoff 介质）


# Modularization

[再谈移动端跨平台框架 Flutter 与 React Native](/mobile/modularization/zai-tan-yi-dong-duan-kua-ping-tai-kuang-jia-flutter-yu-react-native)

[打造工业级 App 开发流程 (JIRA+CI/CD)](/mobile/modularization/da-zao-gong-ye-ji-app-kai-fa-liu-cheng-jira+cicd)

[模块化设计-iOS团队协作开发](/mobile/modularization/mo-kuai-hua-she-ji-ios-tuan-dui-xie-zuo-kai-fa)

[剖析几种流行的 iOS 设计模式--MVC;MVVM;VIPER](/mobile/modularization/pou-xi-ji-zhong-liu-hang-de-ios-she-ji-mo-shi-mvcmvvmviper)


# 再谈移动端跨平台框架 Flutter 与 React Native

现如今，在跨平台方案上仍活跃于市场的，仅剩 RN 与 Flutter。让我们通过框架的设计初衷，架构，开发环境，代码风格，等多个层面看看它们的差异。看看哪一个更适合你。

## 前言

这几年在大前端的开发领域，选择跨端方案的公司和部门越来越多，一方面是跨平台的前端框架越来越成熟，另一方面也是因原生开发者正逐年减少。所以，在当下掌握一门跨平台的技术栈还是很有必要的，无论从广度还是从深度都会有所帮助。

那我们应该选择哪种技术方案呢？如果这个问题放在几年前，答案可能会有很多。不过现在看来，市面上仅剩两种主流方案，就是经常听到的 React Native 和 Flutter。一个出自 Facebook，一个出自 Google。

这两个方案的优劣已有很多点评，基本上形成了两种阵营。但在我看来，它们其实没有明显的差距。如果有，早就被市场所淘汰了。现在看来所谓的劣势，很快就会被那帮天才工程师们，想出解决方案而弥补上了。这也许是竟争帮助了整个生态的完善。反而是 Apple 一直没有跟上，可能还是源于闭源生态，没有另外两家那么的急于变革。

反观 Google 的野心其实是很大的，想通过跨平台方案（无论是 Flutter 还是 Kotlin），从社区和开发者入手一统语言，甚至操作系统([Fuchsia](https://link.juejin.cn/?target=https%3A%2F%2Ffuchsia.dev%2F))，从而扩展更大的版图。Facebook 则想利用自己多年在前端领域积累的丰富经验，通过 React 切入所有平台。这可能成为了两套框架的设计初衷。

> Microsoft 到是另辟蹊径，在 IDE（VSCode）上花大力气，帮助大家建立更好的开发体验，统一了开发环境。

**SDK 版本**

Flutter: 2.5.x\
React Native: 0.64.x

## 1. 架构

### 1.1 设计理念

在端上的开发，有前辈总结了一个很精辟的观点：端上的开发无外乎三件事，“**数据获取**”，“**状态管理**”，“**页面渲染**”。而在跨端领域的竟争，我理解是“**虚拟机**”，“**渲染引擎**”，“**原生交互**”，“**开发环境**”的竟争。而在这几点上，无论是 Flutter 还是 React Native (以下简称 RN) 都有非常棒的解决方案。

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e0dfbc16d2bc40c7a3ca457f1cbbc227~tplv-k3u1fbpfcp-watermark.awebp?)

首先从 Flutter 来看，在**虚拟机**上使用了 Dart VM，Dart 支持 JIT 与 AOT 两种编译模式，也就是我们所说的动态编译与静态编译。在开发阶段使用 JIT 编译，实现热更新预览，动态加载等，而在发布阶段使用 AOT 模式编译为机器码，保证启动速度和跨端信息的传递效率。在**渲染引擎**上，Flutter 使用了 [Skia](https://link.juejin.cn/?target=https%3A%2F%2Fskia.org%2F) 渲染引擎进行视图绘制，避开了不同平台上控件渲染差异。而且，少了这一层的交互，使得效率也得到提升。而在**原生交互**上，因为 Dart 本身跨平台的特性，底层 C++ 可以直接访问到原生的 API，加上信息使用机器码进行传递 (BinaryMessage)，所以与原生交互的效率非常高。

然后再说 RN ，在早期的架构上**虚拟机**使用的是 JSC (Javascript Core) 执行运算，这样它可以充分复用 JS 生态，吸引大量前端开发者参与。而且由于 JS 天生跨平台的特点，跨端移值 App 也顺理成章。在**渲染引擎**上 RN 没有直接使用 WebKit 或其它 Web 引擎，因为之前 Web 在构建复杂页面时带来的计算消耗，远比不上纯原生引擎的渲染。所以它直接复用了原生的渲染通道，这样就可以带来与原生近乎一致的体验。

不过说到这儿，你可能发现虽然早期的 RN 架构充分利用了现有生态，但毕竟不像 Flutter 那样从头到尾都自己来，那么的撤底。带来的问题就是，在 JSC 到原生渲染这一层，用了非常多的 Bridge，并通过 JSON 序列化在多个线程里来回传递信息，这样的消耗在简单的交互过程中可能不明显，而在大量的交互与渲染上会有明显的卡顿，这也成为广为诟病的一点。不过在新的架构中, RN 也做出了新的方案去解决这些痛点，下面会有介绍。

但我们知道 Flutter 也不是完美的，虽然什么事情都自己造自己来，但因为缺少成熟的生态，很多问题都需要官方或社区提供足够的轮子才能解决，否则开发者会在遇到特定问题时，只能自己想办法。另外，Dart 发布阶段用了静态编译，虽然效率得到了提升，但也缺少了在线动态更新的灵活性。

### 1.2 核心架构

#### 1.3.1 Flutter

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d001d2ca58564f90ba05f3595b95bfb5~tplv-k3u1fbpfcp-watermark.awebp?)

Flutter 的架构分为了三层，我们大多情况只与 Flutter Framework 层交互，更多平台无关的的底层能力已被封装好。这也使得 Flutter Framework 非常的轻，如果你需要更多的原生能力，通常使用各类 Flutter Plugin 比如 [Camera](https://link.juejin.cn/?target=https%3A%2F%2Fpub.dev%2Fpackages%2Fcamera)。

> 所以原生能力（轮子）依赖于官方和社区的产出速度

#### 1.3.2 React

**新旧架构对比**

**Old**

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/141e85ff124b4a87a7f256a80013f44f~tplv-k3u1fbpfcp-watermark.awebp?)

三个线程各自负责运算，渲染，Native 交互，中间的交互使用 Bridge 与 JSON 信息格式进行传递。

**New**

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/427e3fc8a1df4fbab42b818c82f014ad~tplv-k3u1fbpfcp-watermark.awebp?)

新的架构主要有两点改变

1. JS Bundle 不再依赖于 JSC(Javascript Core)。换句话说，它可以编译和应用在任何 JS 引擎 (V8等)。
2. 引入 [JSI](https://link.juejin.cn/?target=https%3A%2F%2Fblog.notesnook.com%2Fgetting-started-react-native-jsi%2F) 标准，基于 JSI 协议实现各自方法，使得 JS 可以直接引用 C++ 对象，反之亦然。与原生之间的交互不再用 Bridge 去做粘合。

> 渲染引擎仍是依赖原生的管道。猜测可能 FB 没有像 Google 那样，有这么多年的 Web 渲染引擎经验，轮子就不用再花时间再造了

**RN Bridge 上的变化**

**Old**

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af3d3a89fb4747179dbe0e0f44566878~tplv-k3u1fbpfcp-watermark.awebp?)

可以看到 Bridge 非常的重

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33efaffdcf7043b1ac1d57c5dff9c398~tplv-k3u1fbpfcp-watermark.awebp?)

将原先较重的 Bridge 分拆成两个模块，Farbric 处理 UI，TurboModules 处理与原生交互

> 两个模块均是遵循 JSI 协议的 C++ 模块

## 2. 核心流程

### 2.1 数据获取

#### 2.1.1 网络请求

| **Flutter**       | **React Native**                       |
| ----------------- | -------------------------------------- |
| http.dart 库C++ 实现 | 复用现有的 JS 库fetch, XMLHttpRequest, Axios |

Flutter

```dart
import 'package:http/http.dart' as http;

// 它返回一个 Flutter 的 Future 对象，类似 JS 的 Promise.
http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
复制代码
```

RN

```javascript
fetch('https://reactnative.dev/movies.json');
复制代码
```

其它 JS 生态里的网络库都是适用的

#### 2.1.2 JSON 模型化

**Flutter**

官方提供了 [json\_serializable](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fdevelopment%2Fdata-and-backend%2Fjson) 库，让你可以先定义好模型与属性后，直接通过命令行生成对应的 JSON 转模型代码。

```dart
@JsonSerializable()
class User {
  User(this.name, this.email);

  String name;
  String email;

  /// A necessary factory constructor for creating a new User instance
  /// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
  /// The constructor is named after the source class, in this case, User.
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  /// `toJson` is the convention for a class to declare support for serialization
  /// to JSON. The implementation simply calls the private, generated
  /// helper method `_$UserToJson`.
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
复制代码
```

运行脚本命令即可

```
flutter pub run build_runner build
复制代码
```

**React Native**

官方没有提供了 JSON Model 转化库，需要自己找轮子。

### 2.2 状态管理

**Flutter**

正如 Flutter 将所有控件都定义为了 Widget 一样，它也分成了两种 Widget，一种是 Stateful, 另一种是 Stateless。

**Stateless**

Stateless 是无状态的，不能通过 state 状态去更新控件

```dart
class MyScaffold extends StatelessWidget {
  const MyScaffold({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    ...
  }
}
复制代码
```

**Stateful**

Stateful 是有状态的，可以通过 state 变化去更新控件，但写法和 JS 有些不大一样，需要习惯

```dart
class FavoriteWidget extends StatefulWidget {
  const FavoriteWidget({Key? key}) : super(key: key);

  // 覆盖这个 createState 方法，实现状态管理
  @override
  _FavoriteWidgetState createState() => _FavoriteWidgetState();
}

// 状态设置通常写在私有方法这里
class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  
  // ···
  @override
  Widget build(BuildContext context) {
    ...页面构建
  }
  
  void _toggleFavorite() {
    // 用 setState 去进行状态的变更，以触发 Widget 的重新渲染
    setState(() {
      ...
        _isFavorited = false;
      ...
	  });
	}
}
复制代码
```

对于需要向上传递信息时，使用 InheritedWidget, Provider, FlutterHook 方式。

**React Native**

复用了 React 里的 [State](https://link.juejin.cn/?target=https%3A%2F%2Freactjs.org%2Fdocs%2Fstate-and-lifecycle.html) 模式，同时也支持现在流行的 Hook 方式使用 state，和 React 方式近乎类似。

```javascript
// React Native Counter Example using Hooks!

import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <View style={styles.container}>
      <Text>You clicked {count} times</Text>
      <Button
        onPress={() => setCount(count + 1)}
        title="Click me!"
      />
    </View>
  );
};

// React Native Styles
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  }
});
复制代码
```

### 2.3 页面渲染

#### 2.3.1 共性

在渲染方式上，理论基本一样，先是构建一颗平台无关性的虚拟树 (Virtual Dom Tree)，然后通过各自不同的实现自已渲染或交给原生进行渲染。

> 虚拟树的好处可以实现 UI 节点的局部更新，而不会全量刷新，具有平台无关性

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/020e6a9812df4b0992475d57a8c85315~tplv-k3u1fbpfcp-watermark.awebp?)

* 两个框架都是 UI 响应式框架（React Framework）

  `UI = f(state)`\
  UI 仅依赖于它的父类与自身的状态

  Flutter 在设计之初，也借鉴了很多 React 的设计思想。
* 所有组件都可被组合成一颗虚拟树虚拟树 (VDom)，在真正渲染前各个框架会把它们转化为各自的渲染对象 (RenderObj / VDom)。

#### 2.3.2 差异

**2.3.2.1 布局**

**Flutter**

在 Flutter 中，UI 组件称为 Widget，Flutter 将所有可能的控件都封装为 Widget ，而 RN 没有将所有控件封装，而是将样式与 Component 分离，进行自由组合。所以你不会在 RN 里看到长长的嵌套。

Flutter Widget 嵌套组合:

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c6f5899571e7438190f0784faac48d33~tplv-k3u1fbpfcp-watermark.awebp?)

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d715f0926be04ab2a618e76f687996bf~tplv-k3u1fbpfcp-watermark.awebp?)

虽然看起来组合 UI 很合理，但对于处理复杂的 UI 场景，就拙荆见肘了，比如[富文本](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fget-started%2Fflutter-for%2Fweb-devs%23making-inline-formatting-changes)。

在 RN 中，UI 组件称为 Component，布局沿用了 Component (类似 Web UI 元素) + Style (类似 CSS) 进行布局，没有像 Flutter Widget 一样先封装好各种"样式+组件"，而是把选择权交给你自己进行组合。

```javascript
import {View, Text, StyleSheet} from ‘react-native';

class HelloThere extends React.Component {
  render() {
    return (
      <View style={styles.box}>
        <Text>Hello World!</Text>
      </View>
    );
  }
}

var styles = StyleSheet.create({
  box: {
    borderColor: 'red',
    backgroundColor: '#fff',
    borderWidth: 1,
    padding: 10,
    width: 100,
    height: 100
  }
});
复制代码
```

**2.3.2.2 绘制**

**Flutter**

正如上面提到的架构所示，Flutter 不需要和原生渲染引擎打交道，直接通过 Skia （2D 渲染引擎）进行绘制 (GPU)，所以它的渲染管道非常简洁高效。

**React Native**

RN 是在通过 Yoga （布局引擎）计算好后位置后，通过不同平台的渲染管道进行渲染，所以这里在 Layout 计算与投递结果的过程中多了 Bridge 环节，效率可想而知。\\

> Flutter UI 所见即所得，在所有平台上表现一致。RN 依赖平台的原生控件样式，表现更趋于原生。

#### 2.3.3 渲染过程

**Flutter**

如前所说，Flutter 在更新完 UI Tree 后直接通过 GPU 渲染

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a986dfcc33ba44e7abc1bc153be69089~tplv-k3u1fbpfcp-watermark.awebp?)

**React Native**

和 React Render 很类似，先是更新 VDom ，然后再更新真正的组件，只是 RN 是 Native 组件

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f535b29855fa48de84824c20c4d5da0b~tplv-k3u1fbpfcp-watermark.awebp?)

### 2.4 原生交互

#### 2.4.1 混合开发 (Embed)

**Flutter**

**Flutter 内嵌入 Native 页面**

Fluttter 提供了 [AndroidView](https://link.juejin.cn/?target=https%3A%2F%2Fapi.flutter.dev%2Fflutter%2Fwidgets%2FAndroidView-class.html) 与 [UiKitView](https://link.juejin.cn/?target=https%3A%2F%2Fapi.flutter.dev%2Fflutter%2Fwidgets%2FUiKitView-class.html) 来支持原生页面的嵌入，不过这类 Widget 在使用中还要注意布局，事件的回调等诸多问题，从官方的文档来看其实不太推荐这类场景。虽然架构上没有限制，但目前桌面端的 Widget 还不支持。

```dart
if (defaultTargetPlatform == TargetPlatform.android) {
  return AndroidView(
    viewType: 'plugins.flutter.io/google_maps',
    onPlatformViewCreated: onPlatformViewCreated,
    gestureRecognizers: gestureRecognizers,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
  return UiKitView(
    viewType: 'plugins.flutter.io/google_maps',
    onPlatformViewCreated: onPlatformViewCreated,
    gestureRecognizers: gestureRecognizers,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
}
return Text(
    '$defaultTargetPlatform is not yet supported by the maps plugin');
复制代码
```

**Native 嵌入 Fluttter**

如 Flutter Demo 所示一样，它可以被嵌入任何 Activity 或 ViewController 中。

官方建议最好是在应用初始化时将 Flutter 环境加载好，或者在向用户展示 Flutter 页面前加载好。因为 Flutter 初始化要做很多事情，如 加载 Flutter 库，初始化 Dart VM, 创建 Dart Isolate(内存与线程管理)，UI 初始化等。预热的时间消耗大概是在 300ms 左右（参考[官方数据](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fdevelopment%2Fadd-to-app%2Fperformance%23memory-and-latency)）

**React Native**

React Native 与 Native 原生的控件互嵌相对比较容易。

**Native 内嵌入 RN 页面**

iOS

RCTRootView 我们可以认为是 RN 的一个容器，可以像处理普通 View 一样进行添加。但要注意 RN 里的 layout 要设置为 flex 布局，以便按容器的 size 去适配。

```swift
- (void)viewDidLoad {
    ...
    RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                       moduleName:appName
                                                initialProperties:props];
    rootView.frame = CGRectMake(0, 0, self.view.width, 200);
    [self.view addSubview:rootView];
    ...
}
复制代码
```

**RN 内嵌入 Native 页面**

iOS

继承 RCTViewManager 。然后和事件通信一样，通过 RCT\_EXPORT\_MODULE 暴露 Native 对应的类，然后实现 view 方法，返回 native 的 view 实例。

```swift
// RNTMapManager.m
#import <MapKit/MapKit.h>

#import <React/RCTViewManager.h>

@interface RNTMapManager : RCTViewManager
@end

@implementation RNTMapManager

RCT_EXPORT_MODULE(RNTMap)

- (UIView *)view
{
  return [[MKMapView alloc] init];
}

@end
复制代码
```

然后在 RN 里直接插入该 View 到对应的 UI 组件下。

```javascript
import { requireNativeComponent } from 'react-native';

// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap');

// MyApp.js

import MapView from './MapView.js';

...

render() {
  return <MapView style={{ flex: 1 }} />;
}
复制代码
```

[Android 的嵌入方式类似](https://link.juejin.cn/?target=https%3A%2F%2Freactnative.dev%2Fdocs%2Fnative-components-android)

#### 2.4.2 事件通信

**Flutter**

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df741cc7f80a4426b21e5b5964e68cfb~tplv-k3u1fbpfcp-watermark.awebp?)

> Native <-> Shell (iOS /Android) <-> MethodChannel (Flutter Framework) <-> Dart Code

Message 会被不同的平台间进行类型转换，比如 `Map -> HashMap/Dictionary`

Dart

```dart
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);
复制代码
```

iOS

```swift
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
  (call: FlutterMethodCall, result: FlutterResult) -> Void in
  switch (call.method) {
    case "bar": result("Hello, (call.arguments as! String)")
    default: result(FlutterMethodNotImplemented)
  }
}
复制代码
```

Android

```
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
  when (call.method) {
    "bar" -> result.success("Hello, ${call.arguments}")
    else -> result.notImplemented()
  }
}
复制代码
```

**React Native**

**Native**

在 Native 侧只需实现对应的协议，即可将类或方法暴露给 RN

> React 通常将要它们称为 Module

iOS

```swift
//  RCTCalendarModule.h
#import <React/RCTBridgeModule.h>
  
// 在对应的 Native Class 声明上加上 RCTBridgeModule 协议
@interface RCTCalendarModule : NSObject <RCTBridgeModule>
@end
  
  
// RCTCalendarModule.m
#import "RCTCalendarModule.h"

@implementation RCTCalendarModule

// 将这个类暴露给 RN 掉用。如果不指定名称，默认以类的名字命名
RCT_EXPORT_MODULE();

// 暴露一个方法给 RN
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)name location:(NSString *)location)
{
}

@end
复制代码
```

Android

参照以下方式引入 RN 各个库，并继承 ReactContextBaseJavaModule ，加上 @ReactMethod 标识以暴露方法

```java
package com.your-app-name; // replace com.your-app-name with your app’s name
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;

public class CalendarModule extends ReactContextBaseJavaModule {
   CalendarModule(ReactApplicationContext context) {
       super(context);
   }
}

// 暴露方法给 RN
@ReactMethod
public void createCalendarEvent(String name, String location) {
}
复制代码
```

**RN**

这个时候就可以通过下面方法访问到 Native 的类

```javascript
const { CalendarModule } = ReactNative.NativeModules;
复制代码
```

## 3. 开发环境

### 3.1 编码

**Flutter**

[声明式的语言结构](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fresources%2Ffaq%23what-programming-paradigm-does-flutters-framework-use)

每个 Widget 可以继续嵌套 Widget ，有点类似俄罗斯套娃。

SwiftUI 也是声明式的，写法很类似

```swift
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: EdgeInsets.all(16),
      width: 240, //max-width is 240
    ),
  ),
  width: 320, 
  height: 240,
  color: Colors.grey[300],
);
复制代码
```

**React Native**

RN 可以支持函数式编程 Hook 与 Class 方式编写。样式与组件代码分离，不会有长长嵌套出现。

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3f6633a15864cd9b48fbed6b7621bec~tplv-k3u1fbpfcp-watermark.awebp?)

### 3.2 调试

在 UI 调试上，两者都有对应的工具。效果上来看，RN 更加像 JS 的调试工具一样，上手比较快。

**react-devtools**

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b44a8141fbce4758bc39e53bd691b060~tplv-k3u1fbpfcp-watermark.awebp?)

**Flutter Widget Inspector**

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ee21c1bb1b494e8db8aece24dd6a7991~tplv-k3u1fbpfcp-watermark.awebp?)

但两个方案都有共同的一个问题，就是当需要 Native 与 RN/Flutter 联调时，比如在两侧都要打断点时，好像没有推荐的做法。

## 4. 维护成本

### 4.1 环境依赖

**Flutter**

* Flutter SDK
* XCode
* Android toolchain

**React Native**

* React Native SDK
* XCode
* Android toolchain
* Node

### 4.2 工程化

**Flutter**

可使用线上代码管理，进行一站式代码提交，打包 Flutter 项目，不过目前还没有国内平台支持。

* [Codemagic](https://link.juejin.cn/?target=https%3A%2F%2Fblog.codemagic.io%2Fgetting-started-with-codemagic%2F)
* [Bitrise](https://link.juejin.cn/?target=https%3A%2F%2Fdevcenter.bitrise.io%2Fgetting-started%2Fgetting-started-with-flutter-apps%2F)
* [Appcircle](https://link.juejin.cn/?target=https%3A%2F%2Fappcircle.io%2Fblog%2Fguide-to-automated-mobile-ci-cd-for-flutter-projects-with-appcircle%2F)

或者通过 [fastlane](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fdeployment%2Fcd) 手动设置 pipeline.

**React Native**

官方没有提供最佳实践，不过因为 JS 在线打包很多平台都已支持，所以只要配置对应的 Native 工程环境即可。

### 4.3 产物

**Flutter**

通过 flutter 可以用[命令行工具](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fdevelopment%2Fadd-to-app%2Fios%2Fproject-setup%23option-b---embed-frameworks-in-xcode)手动生成最终产物

iOS 生成的是两个 framework

```
flutter build ios-framework
复制代码
```

* App.framework (你的 Dart 代码产物) \~ 100 KB （模板空项目）
* Flutter.framework （依赖的 Flutter 库）\~ 100mb

Android 可以生成 aar 或 apk

```
flutter build apk
复制代码
```

* libapp.so (你的 Dart 代码产物) \~ 3.4 MB （模板空项目）
* libflutter.so (flutter 工程产物) \~ 9 MB

**React Native**

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b850efc615f74ce98863b55ebc0f51df~tplv-k3u1fbpfcp-watermark.awebp?)

**Metro**

RN 通过 Metro（专为 React Native 设计）打包工具将所有 RN 代码打包成对应的 js.bundle 产物，双端产物大小差不多。

你也可以自己通过命令行生成离线包:

```
react-native bundle --entry-file index.js --bundle-output ./bundle/ios.bundle --platform ios --dev false
复制代码
```

注意：`--dev false` 生成的非 dev 包和 dev 包，大小还是差很多的。

官方提供的一个初始化工程，生成的 bundle 大概是在 750 KB 左右

## 5. 性能

#### 5.1 渲染性能

在大多数浏览器和手机设备上都是 60HZ 刷新频率，也就我们只能在每帧 16ms 的时间内处理完所有事情，包括渲染才能保证显示的平滑。

**React Native**

在渲染效率上，官方其实也提到了，我们的大部分业务逻辑和事件处理都是在 JS 线程上的，因为架构的原因，在 JS 线程处理完数据之后，要扔给 UI 线程进行 Native 原生控件渲染，如果这个时间等于 200ms 就会丢掉 12 帧。而出现卡顿。如果任何情况下超过 100ms 就会被用户所感知。这种情况通常发生在新进一个页面时，要计算所有控件和布局进行渲染。

**Flutter**

其实 Flutter 因为少了原生控件的转化，少了一步桥接上的时间消耗。但要注意的问题仍一样，业务逻辑的处理耗时，和 UI Tree 层级。

```json
"configurations": [
  {
    "name": "Flutter",
    "request": "launch",
    "type": "dart",
    "flutterMode": "profile"
  }
]
复制代码
```

可通过 VS Code 安装 Dart Extension, 然后点击状态栏下方 status bar ，打开 DevTools 查看时时性能。

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/879be7f32c2c4ac3ba0639d71a679ec7~tplv-k3u1fbpfcp-watermark.awebp?)

####

**DevTools**

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb748a1ea45f4ac182dbea69f01cb094~tplv-k3u1fbpfcp-watermark.awebp?)

## 6. 综合比较

|                    | **Flutter**                                                                            | **React-Native**                                                                                  | **备注**                        |
| ------------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------- |
| 背后团队               | Google                                                                                 | Facebook                                                                                          |                               |
| 发布日期               | 2017.5                                                                                 | 2015.3                                                                                            | React-Native 是一个更成熟的框架        |
| 编程语言               | Dart                                                                                   | Javascript                                                                                        |                               |
| 学习曲线               | 低                                                                                      | 低                                                                                                 | 如果你已经了解 JS，将会更快上手 RN.         |
| 热加载                | 是                                                                                      | 是                                                                                                 |                               |
| 热更新                | 否                                                                                      | 是                                                                                                 | RN 可下发 JS 实现。 Flutter 产物已为二进制 |
| 开源                 | 是                                                                                      | 是                                                                                                 |                               |
| 文档完整性              | 是                                                                                      | 是                                                                                                 |                               |
| 编程架构               | State Manager                                                                          | Flux                                                                                              | 都基于状态管理                       |
| 自动化集成发布            | 官方文档                                                                                   | 无可用的官方文档                                                                                          |                               |
| 插件数量               | \~20k                                                                                  | \~30k                                                                                             | 如果算上 React 的话插件就有 200k 左右     |
| 仓库地址               | [Flutter](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter) | [React Native](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Ffacebook%2Freact-native) |                               |
| Github Stars/Forks | 132k/19k                                                                               | 99k/21k                                                                                           |                               |
| 产物                 | \~10MB (Android) \~100MB (iOS)                                                         | \~ 70M (Android) \~ 40M (iOS)                                                                     | 模板空工程，多架构产物                   |

**什么时候选择跨平台框架**

* 当你没有太多 UI 动效和复杂的交互界面时
* 如果你已有原生项目，想在部分模块提升开发效率时
* 当你新建一个项目，想快速试错时

**什么时候推荐使用 RN?**

* 已有项目，有较多场景想混合开发时
* 已有前端页面，想尽快移植时
* 有大量前端开发者，Native 人员不足时
* 有真正跨多端场景时, iOS/Native/Web/Desktop

**什么时候推荐使用 Flutter?**

* 全新项目，无太多混合开发的场景
* 现存项目，没有太多 Native 与 Flutter 页面互相嵌套的情况
* 在移动设备上对于渲染性能及 UI 一致性有较高要求时


# 打造工业级 App 开发流程 (JIRA+CI/CD)

本文不光是方法论，更有工作中的最佳实践，以此来解决和回答以下问题。

1. 如何实现从想法到产品的快速落地
2. 如何保证在团队高速发展时，开发效率依旧高效
3. 如何保证快速迭代时，依旧高质量的交付
4. 如何保证执行层面的公开透明
5. 如何体现团队或个人价值
6. 如何将产出量化

**为了回答上述问题，我们需要先熟悉以下概念和工具**

1. [Agile](https://zh.wikipedia.org/wiki/%E6%95%8F%E6%8D%B7%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91)
2. [Scrum](https://zh.wikipedia.org/wiki/Scrum)
3. [Pull Request CodeReview](https://help.github.com/en/articles/requesting-a-pull-request-review)
4. [Lean Portfolio Management](https://www.scaledagileframework.com/lean-portfolio-management/)
5. 任务管理 [JIRA](https://www.atlassian.com/software/jira)
6. CI 平台 [Travis](https://Travis-ci.com) / [TeamCity](https://www.jetbrains.com/teamcity/)
7. App 打包发布脚本工具 [Fastlane](https://fastlane.tools/)

## **开发流程**

虽然接下来要讨论的是移动端的实践，但实际上针对其它场景，流程极其相似。主要包括：

需求定义，评审，功能拆分，方案设计，开发，代码审核，CI/CD, 黑/白盒测试，上线发布这一套标准的工作流。

如果针对更大的需求/项目，还需要引入项目评审，优先级评审，跨部门合作评审 (依赖梳理), L1 (估难易度), L2 (细化的成本估计) ，RoadMap 制定，Milestone 设定，集成测试，灰度验收等等。

**如何根据不同的阶段，制定合理的开发流程，和高效的资源利用?**

我们知道，在软件开发中最重要的资源是人，其次是沉淀的技术。这些资源需要根据产品所处的不同阶段，进行不同的分配策略，再配合不同的流程，来保证产品的快速交付。

通常情况下，我们将产品分三个阶段: 产品原型期（初期），发展期（中期），成熟期（后期）。下面分别介绍一下。

### **产品原型期，也就是从 0 到 1 的阶段**

在这个阶段，团队规模越小越好，人员越精干越好，开发可以身兼测试。

技术栈选型不用太受限，也不用过于追求极致性能和体验，即使大量三方库的使用也是允许的。但有一点要注意，需要管理好它们，能封装的就封装一层，以便将来替换。同时，切记不要盲目引入，尽量采用主流的开源框架。

针对核心功能点，保证其有完整的开发流程覆盖，其它非核心需求不用重流程。什么是核心功能？需要根据不同的产品来定义。例如电商类产品中，账号体系，商品成列，交易属于核心，尽量走完整 Scrum 流程。其它非核心功能，比如样式，设置，启动画面，频道，可以放在一个大的 Task 或 Story 统一管理即可。

一句话总结就是，*小而精，轻而快*。

### **在产品发展期，也就是从 1 到 10 阶段。**

团队的规模依然需要保持精干，以资深为主，需要配合专业的测试人员。

技术选型需要配合产品的需求，有清晰的方向。对三方库的使用、二方库的开发需要有针对性。比如对于电商类产品 ，不要花太大力气在动画和交互上，而针对核心交易流程，商品展示，需要有可沉淀下来的模块。在这个阶段中，核心功能技术方案需要团队自主完成，足够掌握，有充分的灵活性便于将来扩展，不要追求流行的框架。如果技术沉淀不够，可以继续使用三方库，但一定要花大力气逐步替换。

回到开发流程，到这个阶段人员已有一定的增长，可以尝试从业务或技术方向进行模块化划分，如果是5人以下的团队就不用切了。如果超过此规模，可以从业务角度或技术角度进行切分，Scrum 模式依旧可以继续使用。产品需求和技术需求流程基本类似，只是在技术需求上，产品经理就是开发，对开发人员有一定的文档写作能力要求，这也是为将来做更大规模的重构和架构设计奠定基础。当拆出来的团队有两个以上时，团队之间的依赖尽量在其中一个团队中独立完成，换句话说，这个人或团队需要有端到端的设计和交付能力，尽量避免出现一个功能卡在另一个团队中。这也是为了让大部分人都能端到端完成开发，而不是只做自己的一亩三分地。所以这个时候 Scrum 团队里的一些成员，有可能是在某个 Sprint 中是临时存在的，当相关任务结束后，成员自然回到各自的团队中。

一句话总结就是，确定技术方向，沉淀核心技术，建立跨团队的开发模式。

### **产品成熟期，也就是从 10 到 100+ 阶段。**

基本上产品已成型，也获得了市场认可。团队的规模可以根据对未来市场的增长规模，产品的规划，逐步增加人员，这个时候人员的增加是梯队式的，有资深，中级，初级，还有架构师，Release 工程师，配管等等，根据需要设定岗位和人员。

这个时候的技术选型关注点更多是在体验和系统扩展能力上，及开发效率上。模块化也好，性能优化也好，体验升级也好要做的事情很多，但核心还是一点，根据产品的核心卖点，重点投入人力，千万不要面面俱到。比如在还没有基础库时，就开始花大力气研究模块化，要搞模块化，先要搞清楚如何切，模块与模块间的信息如何交互，协议如何制定，如何自测集成之后再来想这些事。你可以看到其中一些事情，在基础打好后，做起来就会很自然，千万不要本末倒置。如果你的产品是展示型的，建议花力气在信息的动态化，个性化上，无论是推荐算法还是自动化配管，总之能提升核心产品体验和用户粘性上的事更为合适。

另外，在开发效率上，我通常的做法是定期讨论，目前开发过程中的 Top 3 瓶颈，然后评估开发成本，制定优先级，列为技术债。和产品沟通后，依次排期解决，让团队看到短期和长期的目标与方向。

在这个阶段的开发流程，会稍复杂一些，但还是会遵循 Scrum 模式，只是会更加规范化，并引入上面所列到的项目评审，部门和团队间的评审，从Idea -> initiative -> L1 -> L2 -> Epic -> Story -> Task 等一系列不同阶段下，讨论出相应 scope 的定义与计划。而且会以月为单位，并在当月讨论评估下个月的需求，或下下个月的需求。这其中还会有需求锁定期，可变更期等等规则，目的是为了使将下来的开发紧紧有条。在这个阶段，需要产品，开发，PM 都有统一的意识。如果大家按同样的节奏，多大的需求都会运转的紧紧有条，心中有数。另外在执行过程中，可以利用一些工具，比如 JIRA 。它还会自动产出各种报告，满足不同人员想看到的各种数据，让执行过程完全透明。后面会有例子展出。

一句话总结就是，开发模式先行，稳而不乱，培养合作意识。

### **实践**

上面谈到是很多理论知识，下面我们来看看如何基于敏捷流程和 JIRA 工具，进行需求分析，跟踪，任务拆解，执行情况跟踪，上线计划，报告产出等的工作流。

**首先对于敏捷团队，有哪些会议是必要的?**

我们以 Sprint 为单位，通常两周为一个 Sprint，围绕每个 Sprint 会有几个必要的会议需要团队成员参加。

Sprint Planning Meeting (1小时)

Sprint Refinement Meeting (2小时)

Sprint Review Meeting (半小时)

而之后，每个 Sprint 的第一天，会以 Sprint Planning Meeting 开始，所有的开发，产品会在这个会议上，根据上个月 Refinement Meeting 得出的 Story 或 Task 的 Point (复杂度)加上 team 的平均速度（velocity）进行评估，决定在这个 Sprint 中目标是什么，哪些要做，哪些不做，他们的优先级是什么等等，让每个人都知道要做什么，不做什么，目标是什么。

Sprint 开始后，通常在第二周的第一天，通常这个时候紧张的工作告一段落，我们可以利用这个时候去理解下个 Sprint 我们要做的内容，评估每个 Story/Task 的点数。这个会议中同样也需要产品和开发都参与其中，如果有任何不清楚，或需要进一步评估的，都可在下个 Sprint Planning Meeting 开始前进行修正。

在 Sprint 最后一天，通常是在第二周最后一个工作日，我们的 Sprint 里的任务都完成了，现在可以叫上产品和所有相关人员，一起回顾我们所做的内容，通常由这里的最高的管理者，决定是否可最终发布。同时也让所有人了解到将要上线的新特性和成果。注意，这里只是通盘回顾，让大家看看是否完成 Sprint 初所设定的目标。实际上每个任务之前都已走完所有的流程，包括测试，验收等。

每个会议上讨论的内容和模式其实也是有讲究的，为了保证高效，信息传达准确，所有人理解都一样，通常有几点需要注意。

首先在 Sprint 会议前，意味着大的 Epic 甚至 initiative，每个人都已经听过，或多少了解了，这样保证大家知道我们的大方向是什么。另外在到 Sprint 之前，Leader 或者 Manger 已经计划了整个团队的开发目标和计划，可能比较粗，但基本是基于这个前提进行的 Sprint 切分，比如三个月，或一个月要做什么，而且产品或市场人员也有所了解，这个时间通常是根据以往经验得出。

其次在 Sprint 会议上，讨论的 Story Point 不要超过目标 Team 的平均产量，讨论多了，基本上也排不进来，少了会有空档期出现。所以这里往往要结合以往的数据，和 Leader 的经验，挑选出可以讨论或必需讨论的内容。数据我们可以在后面的 JIRA 工具中帮我们自动整理出，目的是为了能让大家保持高效、稳健的实现目标，甚至持续增长。

如果一个 Story 太大，需要继续拆分成多个，直到每个 Story 可以端到端（end to end）测试验收，而且要保证拆出来的 Point 最大也不要超过 8（Point 是斐波那契数列，1,3,5,8,13 为单位）。这样是为了每个 Story 可以按时完成，并且可以早期看到可能的风险和依赖。

在 Sprint 开始的过程中，如果有需求变更，通常处理有两种方式。

针对需求的增量变更，需要新建一个新增部分的 Story ，并和 leader , 开发，测试人员沟通，评估 Point，如果 team 能保证在这个 sprint 里完成的话，可以将它拖到这个 Sprint 里来。

如果是修改，或删除某个 Story, 同样需要找到 leader 和开发沟通，评估优先级。在评估优先级前，需要将它先设置为 “no release” 标签，表示可能不会在这个 sprint 上线。如果优先级高，和 Team 成员一起评估是否能在需求变更情况，依旧完成上线标准。如果答案是"是"，重新评估它的 Point ，注意这里的 Point 是包括原有的加上修改的部分，然后再设置回 “release ”标签。

所有的变更数据都会在最后的 report 中体现出来，这样我们可以看到每个 sprint 我们实际 plan 的任务有多少，变更的有多少。这对后面做计划将很有帮助。

<未完待续....>


# 模块化设计-iOS团队协作开发

### 现状&#x20;

当项目发展到一定阶段，功能会变得越来越庞大，修改一个功能可能有牵一发而动全身的感觉，维护变的相当吃力。如果只是扩充项目成员的话，协作也会成为一种障碍，人多反而不能提高生产率。 如果只是一、两个成员开发的项目还好说，之间的沟通与协作更加灵活而松散，也许吼一嗓子就可以完成。 但如果人员也从几个到十几个，甚至几十个。就不能再靠传统流程开发了，需要更深入系统架构层面将大家工作进行规范与限定，制定标准。

这个时候我们就需要更好的架构设计应用，使得开发与协作变得更加高效，功能的耦合性变得更小。在设计领域有一种概念是模块化设计(Block-based design).

按此概念的意思是说 "对一定范围内的不同功能或相同功能不同性能、不同规格的产品进行功能分析的基础上，划分并设计出一系列功能模块，通过模块的选择和组合构成不同的顾客定制的产品，以满足市场的不同需求"。ok, 这正是我们想要的结果.

### 模块化

模块化设计也是绿色设计方法之一,所谓绿色设计是指"在产品整个生命周期内，着重考虑产品环境属性（可拆卸性，可回收性、可维护性、可重复利用性等）并将其作为设计目标，在满足环境目标要求的同时，保证产品应有的功能、使用寿命、质量等要求".绿色设计的原则被公认为“3R”的原则，即Reduce,Reuse,Recycle，减少环境污染、减小能源消耗，产品和零部件的回收再生循环或者重新利用。

无论是从工业设计还是软件设计，实际上都是尊崇以上的原则。注意上面几点，可拆卸性，可回收性、可维护性、可重复利用性. 我们就从这里入手重新设计。(PS:无畏的重复开发也算是一种能源消耗吧，嗯，至少我们也是爱护环保人士)

从以往开发Web应用的经验来说，模块的划分可以从业务层面或功能层面上决定。业务层面上，你可以将不同的业务划分成各个子模块，好比公司的各个职能部门，事业一部，事业二部，后勤部门，人事部门等。从业务的层面上划分，会使得各个模块更加独立，不用和其它的部门打交道也能完成日常的事务。不过缺点是，一些如果处理公共的事务将会变得繁琐。 另外一个选择是从功能层面上，比如资料管理，订单管理，财务管理等,优点是职责清晰，不过缺点就是各管一方，事务缺少连续性。

举个实际的例子，有一个功能需求是查询与记录，查询有食物查询，运动查询，体重查询等等。对应的也会有食物记录，运动记录，体重记录。 如果从业务上划分可以是食物管理，运动管理，体重管理，各自记录各自的信息，各自独立，互不干扰。可在一个地方就完成所有操作。但如果现在我们要收集所有的记录信息，并且各自还需要一些协作完成操作的话，这样各个功能之间的协调就会变得困难。 所以还有一个方法是按功能来划分，查询模块，记录模块， 查询模块只负责查询，查询的类型分为三种，然后需要记录的时候将信息传送到记录模块，进行一并记录。这样的好处是当我们职责各加清晰，信息统一。

无伦从哪个角度来划分模块其实都有各自的优缺点，我们需求上来设计实际的项目，即使他们功能独立，减低耦合，又要使得信息的流转变得简单。 理论讲的差不多了，我们这里除开项目管理的基本要求，比如开发文档，注释规范，各种图等不提，拆分的方式根据你自己的业务需求自行选择拆分方式，这里只讲讲开发中如何协调和整合它们。 好吧。下面上干货了。

### iOS 团队开发实践&#x20;

我们先把现在遇到的问题列出来，然后一一解决。

**问题:** 以往各个模块整合在一起时，功能之间的调用基本是通过 import 某个类的接口头文件完成.这样使得某个功能会非常依赖另一接口的实现，假如有一天，一个专门负责这个接口的人换了实现方法或这个接口就不存在了，而又没通知到引用的人，就会发生错误，你也许会说，可以保留原有接口，加上即将作废的提示，然后用替代方法进行转换。或直接告诉团队的成员，使用新的 sdk 等。 但这些操作如果变得非常频繁或不确定就会成为障碍。

**解决方案:** 在Rails 框架中有一个叫 Routes表的文件，用来将用户的Web请求定位到某个Controller的某个Action. 我们是不是也可以将调用看作是用户的一次 Web 请求呢。只要存在这样一张 routes 表，就可以将这个请求映射到指定的路径。 所以首先想到的是有一个专门的职能模块去构造这张表，然后维护它，相当于cocoa里的 notification center。 不过和 notification center不同的是，我们的这个 routes 表，也可以称为规则表是在应用启动时就已经创建好了的。相当于智能路由器已经配置好不同的路由方向一样。如何构建这张表呢，同样在Rails里也会有引用第三方的 Plugin 概念,我们也可以把各个模块看作 Plugin，Plugin自己定义自己的路由规则，然后由 route center将所有的规则合并在一起，如果其中一个模块发生变化，只需要改写自己的routes即可，不需要通知其它引用的人，除非你的命名规则发生变化。

接下来如何构造这个请求呢？通知机制，也就是 notification , 相当于你吼一嗓子，我要去干嘛干嘛,谁来帮我处理。然后 route center 会接收此请求，根据里面的规则转送到指定的模块中处理，这样就减小了他们之间的相互依赖。

在具体实现时，还会有线程选择、广播或点到点方式、立即返回结果等规则的定义。不过需要注意的是可能发会生的广播风暴。

![one\_block\_based](http://blog.ydio.com/wp-content/uploads/2013/04/one_block_based.png)


# 剖析几种流行的 iOS 设计模式--MVC;MVVM;VIPER

Arch, iOS, MVP, MVVM, VIPER

*--* [*原文*](https://www.jianshu.com/p/569a66182dae)*首发在简书，现移值过来。*

在这篇文章里，我们争取用最精简的语言，解释清楚这几种设计模式到底给我带来了什么便利。

以看图说话的方式逐一解释，最后总结

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nW4FdNYePd0JkDkBx%2Fimage.png?alt=media\&token=813f9a01-068f-41cd-9d33-5043c7231960)

这是我们最早接触，也最熟悉的设计模式了

但要记住 Controller 不是我们通常理解的 ViewController ！Controller 你可以认为是 Helper 方法。他不掌管 View 的生命周期。也就是说和 **UIKit 无关！**&#x56;iewController 你可以把他理解为 Scene 或 装配中心。

感谢有人做了一个[精辟的总结](https://www.jianshu.com/p/eedbc820d40a#)。看图理解吧:

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nW9keLecHqfZila39%2Fimage.png?alt=media\&token=be575680-c7a0-4e32-8bf8-4fb2bd03c52c)

这才是真正的 MVC 使用姿势。

#### **MVP**

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nWEHOPDrKgsE3ak_F%2Fimage.png?alt=media\&token=ea214fc6-9c6f-44e6-bb13-fb25b1412397)

从对比可以看到 MVC 的 Controller 变成了 Presenter , UIView 或 UIViewController 为 View 层。View 持有 Presenter，Presenter 持有 Model。View 与 Model 完全隔离。

当事件发生，交由 Presenter 进行业务逻辑处理，Presenter 从 Model 拿数据，拿到数据后然后将数据返回给Presenter，Presenter 再返回给 View 。可以理解 Presenter 是一个中间人。

同样 Presenter 应完全不依赖 UIKit 。如果需要与 View 交互，可以采用 Protocol 协议进行约定，调用。保证逻辑清楚，可测试。

**最简样列代码：**

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nWHizj1SQvhcZ1Loo%2Fimage.png?alt=media\&token=9a3d29ac-fa1e-4af4-9fbc-122bdfe0b2ea)

&#x20;

注意中文标注的地方。

另外还有一个 Supervising Presenter MVP 的升级版本，将 View 与 Model 进行绑定。View 会受到 Model 的变更影响，同时 Presenter 依旧可以控制 View 状态。这种模糊的职责关系，还是少用为好。

#### MVVM

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nWMTAYKEs2OuOQiCm%2Fimage.png?alt=media\&token=2ce36513-e6bf-41f3-aad4-84d7dc5774e0)

和 **MVP** 一样，View Model 承担了类似 Presenter 中间层的角色。区别在于"数据与用户行为"的绑定是在 View 与 View Model 层上发生的。这个时候不影响这个设计架构中 View Model 应该承担的角色属性：更新 View 状态。

关于绑定我们可以用 KVO 模式及函数式编程来实现我们的所需。如果你想更高效的完成这个动作，可以参考现有的解决方案:

1. KVO: [RZDataBinding](https://link.jianshu.com/?t=https://github.com/Raizlabs/RZDataBinding) or the [SwiftBond](https://link.jianshu.com/?t=https://github.com/SwiftBond/Bond)&#x20;
2. Functional programming: [ReactiveCocoa](https://link.jianshu.com/?t=https://github.com/ReactiveCocoa/ReactiveCocoa), [RxSwift](https://link.jianshu.com/?t=https://github.com/ReactiveX/RxSwift/) or [PromiseKit](https://link.jianshu.com/?t=https://github.com/mxcl/PromiseKit)

不过，对于 ReactiveCocoa 我只能说慎用，设计起来会非常麻烦。

这里，我们还列举最简单的例子，没有用到 KVO 或 RX 之类的设计，使用最为传统的 Protocol 来实现彼此的交互过程

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nWPcXmMhD8WoNQ_vn%2Fimage.png?alt=media\&token=0792a6f5-e30d-4c96-b934-e048330661f9)

&#x20;

我们可以看重点中文标注的地方，这里通过协议里的定义，第一步 View 层发起数据请求 showGreeting 交由 ViewModel 进行数据处理，处理完成后，因 greeting 发生变化，触发 greetingDidChange 事件发生。

在这个实例里 ViewController 有设定 greetingDidChange 回调，所以实现了类似了上面所说的数据与行为绑定。

#### VIPER

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nWTtVHaLi6Q482g1_%2Fimage.png?alt=media\&token=2da8daac-0925-4cd0-b9f1-fcb2eefcde75)

从结构上来看，在 Presenter 中间多了一层 Interactor ，它主要起到了数据维护访问的职责。你可以封装更多的 Service 或 Managers 做为依赖调用。

对于页面间的跳转，这里增加了 Router 层，专门用于跳转逻辑，实际上现在不少架构已经加了各自的 Router 逻辑。这一点可能是从 Rails 框架继承而来。

下面的例子不包含 Router 层

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nWWr7iq0CWXWZK6Ad%2Fimage.png?alt=media\&token=2ca27791-71a6-4384-92e2-a47c2e8ede6c)

从以上最简的示例代码来看，View 层 (ViewController) 同样还是通过 Presenter 层 (GreetingViewEventHandler) 触发事件逻辑，只不过 Presenter 拿数据是从 Interactor 层（GreetingProvider）去拿。层与层之间还是通过协议通导，不依赖实体。

从这个架构来看，它的耦合性是最小的，不过也带了设计上的复杂性。所以使用时，要因地制宜。

### 总结&#x20;

最后附上一位专家 Bohdan Orlov 的总结，它们的横向对比图，寻找自己最适合的模式使用。

&#x20;

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M4nW15uAluHOzYwwoZn%2F-M4nWaQWjQ3sKEFwxibV%2Fimage.png?alt=media\&token=e0b424d9-2e1d-49d7-acea-e23957331c26)

* 耦合度 (Distribution)&#x20;
* 可测性 (Testability)&#x20;
* 易用性 (Ease of use)

PS：这里也提到了 Redux 模式，想了解 Redux 相关的内容话，看这个[链接](https://link.jianshu.com/?t=http://www.ctolib.com/ReSwift.html)吧。研究不深，大致是和 FP (函数式编程) 与 State 流相关。

### **总而言之**&#x20;

1. 没有杀手锏 no Silver Bullet&#x20;
2. 分层治理 分久必合，合久并分，看具体业务场景&#x20;
3. 低耦合，高内聚 基本要素&#x20;
4. 遵从流行的框架与概念，有利于团队间的沟通成本 \
   如果一定要发明，请多推广你的概念&#x20;
5. 从简到繁，再化繁为简 \
   前一个繁是业务的属性，后一个繁是框架本身


# iOS Foundation


# ARC 与内存管理

ARC: Automatic Reference Counting (自动引用计数)

ARC 是 iOS 5 后推出的一项为Objective - C程序在编译时提供自动内存管理的功能。ARC可以让你把注意力集中在你感兴趣的代码，减少开发中的内存管理步骤，简化开发。

它通过指定的语法，让编译器(LLVM 3.0)在编译代码时，自动生成实例的引用计数管理部分代码。有一点，ARC并不是GC，它只是一种代码静态分析（Static Analyzer）工具。

在过往我们通常使用的是MRC: Manual Reference Counting(手动内存管理)。这些规则将逐渐变为本能，你会发现少一个release的代码怎么看怎么别扭，从而减少或者杜绝内存管理的错误。可以说MRC的规则非常简单，但是同时也非常容易出错。往往很小的错误就将引起crash或者leak之类问题。

很多人担心内存管理不受自己控制，其实这是对于ARC机制了解不足从而不自信，所导致的对新事物的恐惧。

下面我们从几个方面来详细介绍ARC到底如何实现，如何使用，它的好处，注意事项等。

### 需要的基本环境:

ARC is supported in Xcode 4.2 for OS X v10.6 and v10.7 (64-bit applications) and for iOS 4 and iOS 5. Weak references are not supported in OS X v10.6 and iOS 4.

**注意：iOS4 不支持 weak 引用**

### 原理

![ARC Illustration](http://developer.apple.com/library/ios/releasenotes/ObjectiveC/RN-TransitioningToARC/Art/ARC_Illustration.jpg)

**ARC的一个基本原则: 只要某个对象被任一strong指针引用，那么它将不会被销毁。当对象没有被任何strong指针引用时，那么就将被销毁。**

### 默认行为

对象默认为声明为 strong 类型, ARC 确保对象在函数体内是不会被 dealloc。比如

```
- (void)takeLastNameFrom:(Person *)person {
    NSString *oldLastname = [self lastName];
    [self setLastName:[person lastName]];
    NSLog(@"Lastname changed from %@ to %@", oldLastname, [self lastName]);
}
```

### 强制规则

* 你不能再调用 dealloc 或者实现、调用 retain, release, retainCount, autorelease, 同样@selector(retain), @selector(release)也是不允许的， 当然你可以实现 dealloc 方法，来管理你的实例变量，或者你会调用 \[systemClassInstance setDelegate:nil]等.定制的 dealloc 方法不需要写 \[super dealloc],这个动作会默认调用。

  你仍然可以用 CFRetain, CFRelease 和相关Core Foundation方法。 如果要管理这类对象可以参考：(Managing Toll-Free Bridging 管理自由桥接)
* 你不能使用 NSAllocateObject 或 NSDeallocateObject

  创建对象用 alloc
* 你不能在 C 结构体中使用对象指针

  因此下面代码是不可用的

```

typedef struct {
    UIImage *selectedImage;
    UIImage *disabledImage;
} ButtonImages;
```

建议是用 OC类来管理它们

* 不能随意在 id 与 void \* 之间随意转换

  编译器同样是无法管理 void \* 这类 Core Foundation类型的东东，都要用 Managing Toll-Free Bridging 进行生命同期的管理。
* 你不能再使用 NSAutoreleasePool 对象

  ARC 提供了性能更好的 @autoreleasepool block 替换原来这类使用方式。
* 你不能使用内存区

  你不需要再使用 NSZone 等这类对象，因为现在Objective-C运行时已经忽略NSZone了，所以没必要再使用NSZone了。
* 你不能使用 new 开头的属性名，但你可以手动指定 getter 方法名

  例如:

```
// 非法:
@property NSString *newTitle;

// OK:
@property (getter=theNewTitle) NSString *newTitle;
```

## ARC 新的对象生命周期声明

* 属性

```
// 用 strong 代替 retain
@property(retain) MyClass *myObject; ->
@property(strong) MyClass *myObject;

// 用 weak 代替 assign 
@property(assign) MyClass *myObject; ->
// 实例变量被释放后，会自动赋予 nil 指针，省得我们自己在手动赋nil操作。
@property(weak) MyClass *myObject;
```

在 ARC 中 strong 将是默认的类型.

* 变量

变量同样有以下几种管理生命周期的声明

```

__strong
__weak
__unsafe_unretained
__autoreleasing
__unsafe_unretained 类似原来的 assign
```

所以你可以这样声明这些对象

```

    MyClass * __weak myWeakReference;
    MyClass * __unsafe_unretained myUnsafeReference;
```

需要注意的是 \_\_weak 变量在栈中的情况，例如:

```

     NSString * __weak string = [[NSString alloc] initWithFormat:@"First Name: %@", [self firstName]];
      NSLog(@"string: %@", string);
```

尽管 string 被实例化，但由于 string 声明为 weak 类型，它没有 strong 这个引用，所以他在赋值后立即就被释放了,在Log它时，它已经被释放了。

### 同样你也要注意对象值传递. 比如下面的代码

```

NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
    // Report the error.
    // ...
```

实际上这个代码是这样隐示声明的 NSError \* \_\_strong e;

而函数是这样被声明了的

`-(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;`

所以最后编译的结果就是:

```

NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
    // Report the error.
    // ...
```

当本地变量声明(**strong&#x20;*****error)和函数的参数((NSError*****&#x20;** autoreleasing *)error)不匹配的时候，编译器会创建一个临时变量。当你获得一个**strong变量的地址时，你可以初始化一个id** strong* 的指针来声明 ，这样你就可以获得指针的原型，或者你可以声明一个变量为 \_\_autoreleasing。

## 避免循环引用

你可以使用生命周期修饰符来避免Strong引用周期。例如，当你制作了一组父子结构的对象，而且父类要引用子类，则会出现Strong引用周期；反之，当 你将一个父类指向子类为strong引用，子类指向父类为weak引用，就可以避免出现Strong引用周期。当对象包含block objects时，这样的情况会变的更加隐性。

在MRC模式下，`__block id x`, x不会被 retaining

在ARC模式下，`__block id x`, x默认被 retaining

为了使手动内存管理模式代码可以在ARC模式下正常工作， 你可以用 `__unsafe_unretained` 来修饰 `__block id` x;。就和"**unsafe\_unretained"字面上的意思一样, 不过,这样一个non-retained变量是危险的(因为它会变成一个野指 针) 会带来不良后果。有两种更好一点的方法来处理，一是使用**weak (当你不需要支持iOS 4或OS X v10.6), 二是设\_\_block值为nil，结束他的生命周期。

* 这是MRC时代处理 \_\_block 里对象释放问题:

```

MyViewController *myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler =  ^(NSInteger result) {
   [myController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{
   [myController release];
}];
```

你可以使用 \_\_block修饰符然后设置myController的值为nil 替代上面的方式:

```

MyViewController * __block myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;
};
```

无伦哪种形式，你都可以使用一个 weak 引用对象避免循环引用:

```

MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyViewController = myController;
myController.completionHandler =  ^(NSInteger result) {
    [weakMyViewController dismissViewControllerAnimated:YES completion:nil];
};
```

在某个时候这个对象，如果放在异步执行时，对象可能已经被释放，所以需要一个 strong 的对象把它 hold 住。

```

    MyViewController *myController = [[MyViewController alloc] init…];
    // ...
    MyViewController * __weak weakMyController = myController;
    myController.completionHandler =  ^(NSInteger result) {
        MyViewController *strongMyController = weakMyController;
        if (strongMyController) {
            // ...
            [strongMyController dismissViewControllerAnimated:YES completion:nil];
            // ...
        }
        else {
            // Probably nothing...
        }
    };
```

### 栈里的变量初始化即为 nil

使用ARC后， strong, weak, autoreleasing 栈里的变量默认初始为nil

```

- (void)myMethod {
    NSString *name; // 这里 name 已经被赋予了nil指针， 所以下面的代码不会出错。
    NSLog(@"name: %@", name);
}
```

### 修改编译的Flag 打开和关闭 ARC

如果有遇到第三方插件，或有一些文件你不想用 ARC 来控制，可以在 Build Phases > Compile Sources > 某个文件上 > Compiler Flags: -fno-objc-arc

相互如果想在部分文件中用到 arc 则标记上: -fobjc-arc

![Compiler flags for arc](http://blog.ydio.com/wp-content/uploads/2013/04/compiler_flags_arc.png)

### Managing Toll-Free Bridging

由于ARC不能管理Core Foundation Object的生命周期，所以在Core Foundation和ARC之间，我们需要使用到**bridge,**&#x62;ridge\_retained和\_\_bridge\_transfer三个转换关键字。

\_\_bridge只做类型转换，但是不修改对象（内存）管理权；

\_\_bridge\_retained（也可以使用CFBridgingRetain）将Objective-C的对象转换为Core Foundation的对象，同时将对象（内存）的管理权交给我们，后续需要使用CFRelease或者相关方法来释放对象；

\_\_bridge\_transfer（也可以使用CFBridgingRelease）将Core Foundation的对象转换为Objective-C的对象，同时将对象（内存）的管理权交给ARC。

### 使用weak property声明Outlet

在被ARC处理过的iOS和OS X中，声明的outlets将会趋于统一。

一般来说outlets变量被修饰为weak，但是如果outlets变量的所有者是nib文件中的top-level对象(或者是storyboard scene)时，应被修饰为strong。

详细参考Resource Programming Guide中的“Nib Files”。

当我们使用 Interface Builder 生成Outlet对象的时候，一般都是作为 subview 来使用的。比如 UIViewController 的view。所以说Outlet的持有者就是superview对象，即有“父子”关系。我们知道，当对象间有“父子”关系时，需要使用弱参照，以避免“循环参照”。

ViewController 本身是不会作为Outlet的所有者的，所以使用weak property声明。

![arc outlet weak property](http://blog.ydio.com/wp-content/uploads/2013/04/arc_outlet_weak_property.png)

简化viewDidUnload

Outlet都使用weak property声明的时候，还有一个好处，就是简化viewDidUnload的处理。

iOS在系统内存不足的时候，UIViewController会将没有表示的所有view做unload处理，即调用viewDidUnload接口。

所以，如果是强参照的情况下，需要释放所有权，

```

@property (nonatomic, strong) IBOutlet UILabel *label;

-(void) viewDidUnload {
    self.label = nil; // 取消强参照，释放所有权
    [super viewDidUnload];
}
```

如果没有 self.label = nil 的处理，那么 UIViewController 将不会释放 label 的所有权；结果，系统是调用了unload，但是subview对象始终留在内存中。随着界面上控件的增多，内存泄露会越来越大。

如果使用的是weak property声明的话，会是怎样的呢？

@property (nonatomic, weak) IBOutlet UILabel \*label;

这时，系统在unload时，由于label没有被强参照，更加ARC的规则，这时，label的对象即被释放。并在释放的同时，变量自动指向nil。

```

- (void)viewDidUnload {
    // 这里什么也不用管
    [super viewDidUnload];
}
```

其实，如果我们的viewDidUnload只是用来释放Outlet用的话，那么该函数也可以不被重载的。

什么时候要用strong property

由上我们也可以看到，并不是所有的Outlet都用weak来声明都是正确的；当使用Interface Builder生成的第一层的view或者windows被作为Outlet来使用的话，那么不是不能声明为weak property的。（比如，Storyboard的各个scene）

## 转化原MRC项目到ARC

* 用Xcode自带工具转换MRC项目到ARC: Edit > Refactor > Convert to Objective-C ARC)

  ![Provides a tool that convert to ARC](http://blog.ydio.com/wp-content/uploads/2013/04/tool_for_convert_to_oc_arc.png)

  在这个选项下，还有一个 Convert to Modern Objective-C Syntax.. 转化成更现代的写法, 有兴趣的可以试试。:)

  在转化的过程中，编译器会先对代码进行检查，如果遇到错误警告，可以根据提示进行处理后，再进行转化 (如果你要无视这些错误可以在Preferences 里设定 Continue building after errors)
* 将项目用ARC方式编译 Build Settings -> LLVM compiler 将 Objective-C Automatic Reference Counting 设置为 Yes

  ![Convert to ARC in LLVM](http://blog.ydio.com/wp-content/uploads/2013/04/convert_to_arc_in_llvm.png)

## 常见问题

* 通常遇到的错误有这样一些：
  * Receiver type ‘X’ for instance message is a forward declaration

    ```
      这往往是引用的问题。ARC要求完整的前向引用，也就是说在MRC时代可能只需要在.h中申明@class就可以，但是在ARC中如果调用某个子类中未覆盖的父类中的方法的话，必须对父类.h引用，否则无法编译。
    ```
  * Switch case is in protected scope

    ```
    现在switch语句必须加上{}了，ARC需要知道局部变量的作用域，加上{}后switch语法更加严格，否则遇到没有break的分支的话内存管理会出现问题。
    ```
  * A name is referenced outside the NSAutoreleasePool scope that it was declared in

    ```
      这是由于写了自己的 autoreleasepool，而在转换时在原来的pool中申明的变量在新的@autoreleasepool中作用域将被局限。解决方法是把变量申明拿到pool的申请之前。
    ```
  * ARC forbids Objective-C objects in structs or unions
  * ARC 需要你指定 super init 的结果到 self

    \[super init]; // 这将是无效的

    推荐用

```
     
     self = [super init];
     if (self) {
       ...
```

```
实例变量会变成 strong 类型

在用 ARC 之前, thing 这个变量是一个 weak 类型
```

```

    @interface MyClass : Superclass {
        id thing; // Weak reference.
    }
    // ...
    @end

    @implementation MyClass
    - (id)thing {
        return thing;
    }
    - (void)setThing:(id)newThing {
        thing = newThing;
    }
    // ...
    @end
```

使用 ARC 后，thing 变量实际上是默认用了 strong 类型，所以如果你要想继续使用 weak 类型，必须显示声明

```

    @interface MyClass : Superclass {
        id __weak thing;
    }
    // ...
    @end

    @implementation MyClass
    - (id)thing {
        return thing;
    }
    - (void)setThing:(id)newThing {
        thing = newThing;
    }
    // ...
    @end
```

或

```

    @interface MyClass : Superclass
    @property (weak) id thing;
    // ...
    @end

    @implementation MyClass
    @synthesize thing;
    // ...
    @end
```

* 首先，我们需要转变一下观念, 对于在.h中申明的实例变量：

```

@interface MainViewController : UIViewController  
{ 
    NSOperationQueue *queue;
}
```

我们不妨仔细考虑一下，为什么在interface里出现了实例变量的申明？通常来说，实例变量只是在类的实例中被使用，而你所写的类的使用者并没有太多必要了解你的类中有哪些实例变量。而对于绝大部分的实例变量，应该都是protected或者private的，对它们的操作只应该用setter和getter，而这正是property所要做的工作。可以说，将实例变量写在头文件中是一种遗留的陋习。更好的写实例变量名字的地方应当与类实现关系更为密切，为了隐藏细节，我们应该考虑将它们写在@implementation里。好消息是，在LLVM3.0中，不论是否开启ARC，编译器是支持将实例变量写到实现文件中的。甚至如果没有特殊需要又用了property，我们都不应该写无意义的实例变量申明，因为在@synthesize中进行绑定时，我们就可以设置变量名字了，这样写的话可以让代码更加简洁。

在这里我们对实例变量申明移到.m里中。修改后的.h是这样的，十分简洁

```

@implementation MainViewController 
{ 
    NSOperationQueue *queue;  
}
```

这样的写法让代码相当灵活，而且不得不承认.m确实是这些实例变量的应该在的地方

参考资料:

* [About Memory Management](http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011i)

![memory managment](http://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/MemoryMgmt/Art/memory_management_2x.png)

* [Transitioning to ARC](http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html)
* [Objective-C Automatic Reference Counting](http://clang.llvm.org/docs/AutomaticReferenceCounting.html#blocks)


# 从头梳理内存与内存管理

#### 前言

对于大多数开发者，特别是 C, Objective-C, Swift 等相关的开发者来说，已经很了解如何避免内存泄漏，解决循环引用等问题。但如果继续深入讨论为何会需要手动管理内存，为何内存泄漏仅在特定场景下产生，如何尽可能减少内存开销等问题时，能回答很清楚的人就相对较少了。

所以还是有必要从头梳理一下关于内存的基本知识与概念了，如果我们掌握了这些知识，相信很多问题就会迎刃而解了。

#### 基础知识

* 位 ：( bit ) 是电子计算机中最小的数据单位。每一位的状态只能是0或1。
* 字节：1 Byte = 8 bit ，是内存基本的计量单位，
* 字："字" 由若干个字节构成，字的位数叫做字长，不同档次的机器有不同的字长。
* KB ：1KB = 1024 Byte。也就是1024个字节。
* MB : 1MB = 1024 KB。类似的还有GB、TB。
* 内存编址：计算机中的内存按字节编址，每个地址的存储单元可以存放一个字节（8个bit）的数据，CPU通过内存地址获取指令和数据，并不关心这个地址所代表的空间具体在什么位置、怎么分布，因为硬件的设计保证一个地址对应着一个固定的空间，所以说：内存地址和地址指向的空间共同构成了一个内存单元。
* 内存地址：内存地址通常用十六进制的数据表示，例如通常在C或者Objective-C中输出一个变量的地址可能为：0x7fff5fbff79c，这就是一个用十六进制的数表示的地址。

下图的整数100在三种进制中的表示：

![](https://user-gold-cdn.xitu.io/2018/4/8/162a3d6fa193468c?w=683\&amp;h=424\&amp;f=png\&amp;s=33268)

#### 什么是内存?

从硬件的角度来说，它是重要的部件之一.

![](https://user-gold-cdn.xitu.io/2018/4/8/162a3f0db0788fda?w=250\&amp;h=136\&amp;f=png\&amp;s=57164)

也是硬盘与 CPU 之间沟通的桥梁。所有的应用程序运行时都会放入内存，然后交由 CPU 进行计算执行。CPU 能通过寻址找到内存对应的数据。

严格意义上讲，内存不仅仅是我们经常所指的内存条。寄存器(Register)、缓存(Cache) 都属于内存的一种。

所以内存一般分为 RAM(main memory), SRAM(cache), Register。

寄存器是 CPU 的组成部分，因为在CPU内，在设计上和CPU同频，所以CPU对其读写速度是最快的，不需要IO传输。

部分缓存的设计也基本保持了与 CPU 同频，所以速度相对内存也是比较快。

理论上讲，缓存和寄存器是一样快的。不过因为缓存里面的东西不一定就是需要的，如果不存在就要一级一级往下找，直到内存。因为不同的缓存或内存时钟频率不一样，所以CPU在查找时需要一定的等待时间。

CPU 寻找数据进行运算的流程是:

**CPU -> Register -> L1 Cache -> L2 Cache -> 内存**

![](https://user-gold-cdn.xitu.io/2018/4/8/162a3f188d3a941e?w=441\&amp;h=427\&amp;f=png\&amp;s=30593)

`RAM 内存的主频现在主流是1600左右，单位是MHz，这比CPU的速度要低的多。`

#### 32 位与 64 位是什么概念？

所谓多少位一般是指处理器(CPU)的运算能力与寻址能力。

1. 运算能力

比如32位，一次能处理32位，也就是4个字节的数据(1字节=8位)。而64位处理器则能处理8个字节的数据。

如果我们将总长128位的指令分别按照16位、32位、64位为单位进行编辑的话：旧的16位处理器，比如Intel 80286 CPU需要8个指令，32位的处理器需要4个指令，而64位处理器则只要两个指令。

1. 寻址能力

除了运算能力之外，与32位处理器相比，64位处理器的优势还体现在系统对内存的控制上。

相比32位的CPU来说，64位CPU最为明显的变化就是增加了8个64位的通用寄存器，内存寻址能力提高到64位。

32位的内存地址空间为2的32次方，即4GB。64位可获得更大的寻址空间，能识别4G以上的内存。

另外，要实现真正意义上的64位计算，光有64位的处理器是不行的，还必须得有64位的操作系统以及64位的应用软件才行，如果应用软件不支持，反而在64位机器上会变慢。

#### 寄存器 (Register)

为了处理数据，暂时储存结果，或者做间接寻址等动作，每个处理器都具备一些内建的内存，这些能够在不延迟的状态下存取的内存就称为寄存器。

寄存器是数据处理的重要一环，如果经常使用汇编语言，对它应该会非常熟悉。

IA-32构架提供了16个基本寄存器，这16个基本寄存器可以归纳为如下几类:

* 通用寄存器
* 段寄存器
* 状态和控制寄存器
* 指令寄存器

通用寄存器 32位通用寄存器有八个，`eax, ebx, ecx, edx, esi, edi, ebp, esp`

他们主要用作逻辑运算、地址计算和内存指针，具体功能如下：

* eax 累加和结果寄存器
* ebx 数据指针寄存器
* ecx 循环计数器
* edx i/o指针
* esi 源地址寄存器
* edi 目的地址寄存器
* esp 堆栈指针
* ebp 栈指针寄存器

当然，以上功能并未限制寄存器的使用，特殊情况为了效率也可作其他用途。

在 64-bit 模式下，有16个通用寄存器，但是这16个寄存器是兼容32位模式的，

32位方式下寄存器名分别为 `eax, ebx, ecx, edx, esi, edi, ebp, esp, r8d – r15d`

在64位模式下，他们被扩展为 `rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp, r8 – r15`，其中 r8 – r15 这八个寄存器是64-bit模式下新加入的寄存器。

段寄存器 段寄存器是因为对内存的分段管理而设置的。计算机需要对内存分段，以分配给不同的程序使用（类似于硬盘分页）。

把内存分为很多段，每一段有一个段基址，当然段基址也是一个20位的内存地址。不过段寄存器仍然是16位的，它的内容代表了段基址的高16位，这个16位的地址后面再加上4个0就构成20位的段基址。而原来的16位地址只是段内的偏移量。这样，一个完整的物理内存地址就由两部分组成，高16位的段基址和低16位的段内偏移量，当然它们有12位是重叠的，它们两部分相加在一起，才构成完整的物理地址。

段寄存器又分为 `cs, ds, ss, es, fs, gs`。

* cs 代码段寄存器
* ds, es, fs, gs 数据段寄存器
* ss 堆栈段寄存器

在 64-bit 模式下，这6个寄存器并无变化，只是使用上略有区别。

状态和控制寄存器 EFLAGS 这个寄存器表示的意义非常丰富，程序中并不直接操作此寄存器，并由此衍生出很多操作指令。

指令寄存器 EIP 标志当前进程将要执行指令位置，在64位模式下扩展为 RIP 64位指令寄存器。

OK 以上，关于寄存器，在这里我们只需要了解这么多就够了，不用深入。

#### 内核空间与用户空间

为了保护操作系统不会被运行的用户程序所干扰，所以分为了内核空间与用户空间，所有的应用程序会被分配在用户空间。

![](https://user-gold-cdn.xitu.io/2018/4/8/162a3f83bd41d08b?w=556\&amp;h=286\&amp;f=png\&amp;s=40408)

内核空间是持续存在的，并且在所有进程中都映射到同样的物理内存，内核代码和数据总是可寻址的，随时准备处理中断和系统调用。

现代的操作系统都处于32位模式下。每个进程一般都能寻址4G的物理空间。但是我们的物理内存不足 4G，通常我们使用一种叫做虚拟内存的技术来实现，因为可以使用硬盘中的一部分来当作内存使用。

#### 虚拟空间中的内存布局 (Memory layout)

如果一个应用程序在运行时，会被装载到用户内存空间，并构成一个虚拟地址空间（virtual address space）。

典型的虚拟地址空间有四个部分构成

* 执行的代码 (Executable code) 这个空间包括了可被机器执行的代码，为只读区域
* 静态数据 (Static data) 这个部分包括了静态分配的变量
* 堆 (Heap) 这个部分包括了动态分配的变量
* 栈 (Stack) 这个部分包括了临时变量，返回地址，执行参数

每个部分分别占用了一块或多块连续的地址空间，他们被操作系统所管理。

如果我们想看看操作系统自己的内存分布，可以用下面的命令

```

cat /proc/self/maps

...
00aed000-00b0e000 rw-p 00000000 00:00 0 [heap]
…
```

第一列显示了内存地址块，第二列显示一些标记，如只读，可写等，三，四，五，六列分别表示偏移地址，设备，节点，名称

#### C 语言应用程序的内存布局

![](https://user-gold-cdn.xitu.io/2018/4/8/162a3fb4f0d1650b?w=606\&amp;h=463\&amp;f=png\&amp;s=127486)

除了核心的内核空间外，其它的空间分为 5 种类型:

* 文本段 (Text segment)

通常存放执行的代码，只读段。

* 初始化的数据段 (Initialized data segment)

这个区域分为"只读数据段”和"可读写的数据段"

只读数据段是程序使用的一些不会被更改的数据。 可读写数据段会放入在程序中声明，具有初值的变量，以供程序运行时读写。 如全局 ( main 函数外) 字符串定义 char s\[] = “hello world” 或 int debug=1 。

这样的全局声明 const char\* string = “hello world” 中，“hello world” 被放入只读区，而字符串指针变量 string 会被放入读写区。 这样的声明 static int i = 10 与 int I = 10 都会被存入数据段。

* 未初始化的数据段 (Uninitialized data segment)

又称为 BSS (block started by symbol) 段。 这样的声明 static int I; 或 全局的 int j; 都会放入这个区域。

* 栈 (Stack)

栈空间主要用于以下3种数据的存储：

* 函数内部的动态变量
* 函数的参数
* 函数的返回值

栈空间是动态开辟与回收的。在函数调用过程中，如果函数调用的层次比较多，所需的栈空间也逐渐加大，对于参数的传递和返回值，如果使用较大的结构体，在使用的栈空间也会比较大。

栈上的局部变量往往通过偏移地址去访问，如果通过 push/pop 会浪费大量时间。

栈通常都会放在高位区，它遵循 LIFO (后进先出) 机制。

```

#include
int main(int argc, const char * argv[]) {
int a = 100;
int b = 100;
printf("%p \n",&a); // 0x7fff5fbff79c
printf("%p \n",&b); // 0x7fff5fbff798
return 0;
}
```

变量a的地址 `0x7fff5fbff79c` 比变量 b 的地址 `0x7fff5fbff798` 要大。所以它也是从高位开始按顺序一条条出栈执行。

* 堆 (Heap)

堆开始于 BSS 段的未尾。然后往高地址增长。Heap 区通常由 malloc, realloc, and free 命令管理。Heap 区可以在一个进程中被多个库或动态加载的模块所共享。

我们来看看一个最简单的 C 程序，在 linux 系统上编译后它的内存部局是什么样的:

```

#include
int main(void)
{
return 0;
}
```

```

$ gcc memory-layout.c -o memory-layout
> size memory-layout
text data bss dec hex filename
1115 552 8 1675 68b memory-layout
```

然后我们增加一个全局变量，看看内存有什么变化

```

#include
int global; /* Uninitialized variable stored in bss*/

int main(void)
{
return 0;
}
```

重新编译

```

$ gcc memory-layout.c -o memory-layout
$ size memory-layout

text data bss dec hex filename
1115 552 12 1675 68f memory-layout
```

如果我们增加一个 static 变量

```

#include
int global; /* Uninitialized variable stored in bss*/

int main(void)
{
static int i; /* Uninitialized static variable stored in bss */
return 0;
}
```

```

text data bss dec hex filename
1115 552 16 1683 693 memory-layout
```

&#x20;让我们实例化这个变量，它会被放入 Data Segment (DS)

```

#include
int global; /* Uninitialized variable stored in bss*/

int main(void)
{
static int i = 100; /* Initialized static variable stored in DS*/
return 0;
}
```

```

text data bss dec hex filename
1115 556 12 1683 693 memory-layout
```

&#x20;如果我们实例化全局变量后，它也会被放入 DS

```

#include
int global = 10; /* Uninitialized variable stored in bss*/
...
```

```

text data bss dec hex filename
1115 560 8 1683 693 memory-layout
```

&#x20;\*\*注意：如果你是在 Mac 系统上编译，查看内存布局时，会是这样的情况\*\*

```

__TEXT __DATA __OBJC others dec hex
4096 4096 0 4294971392 4294979584 100003000
```

同样也是分为 Text 区，Data 区。

代码段、只读数据段、读写数据段、未初始化数据段属于静态区域，而堆和栈属于动态区域。代码段、只读数据段和读写数据段将在链接之后产生，未初始化数据段将在程序初始化的时候开辟，而堆和栈将在程序的运行中分配和释放。

设想一下，如果系统将程序初始化阶段把所有变量全部分配好，那得要多少内存。所以有这样段设计，将动态的部分根据需要再去分配，这才有了运行时的概念。

#### 基本数据类型的内存空间

变量在内存中以二进制形式存储，一个变量占用的存储空间，不仅和变量类型有关，还和编译环境有关，同一种类型的变量在不同编译环境下占用的存储空间不一样。比如开发中常用的基本数据类型char、int等在不同编译环境下就会占用不同大小的空间。

![](https://user-gold-cdn.xitu.io/2018/4/8/162a402de3923ced?w=590\&amp;h=351\&amp;f=png\&amp;s=39106)

在 64 位机器下的许多程序设计环境，int 变量仍然是 32 位宽，不过 long 是 64 位宽。这就是为什么 Objective-C 中会有 NSInteger 的定义，它会根据操作系统位数自由选择不同的数据类型。

我们通过一个例子来验证 int 的内存空间。

```

int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
printf("%p\n",&a); //0x7fff5fbff79b
printf("%p\n",&b); //0x7fff5fbff797
return 0;
}
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-Lxuu2oBREskiQJj0IjI%2F-LxuvfteGLHIWrsFFWeG%2Fimage.png?alt=media\&token=b2de3949-2b97-4299-82a9-db2652912c91)

我们可以看到 b 与 a 刚好是 4 个字节。

C语言中数组的存储和普通的变量不太一样，数值中存储的元素，是从所占用的低地址开始存储的。例如

```

int main(int argc, const char * argv[]) {
char chars[4] = {'l','o','v','e'};
printf("chars[0] = %p\n",&chars[0]); //0x7fff5fbff79c
printf("chars[1] = %p\n",&chars[1]); //0x7fff5fbff79d
printf("chars[2] = %p\n",&chars[2]); //0x7fff5fbff79e
printf("chars[3] = %p\n",&chars[3]); //0x7fff5fbff79f
return 0;
}
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-Lxuu2oBREskiQJj0IjI%2F-Lxuvq1w2pDhbNOobsEs%2Fimage.png?alt=media\&token=01cd0b59-8938-41cf-a5d8-4beca3f100e4)

仅仅通过上面的字符数组例子还不能完全说明数组在内存中关于其元素存储和元素中值的存储关系。如果换用一个整型数组就能看出一些差别。

```

int main(int argc, const char * argv[]) {
int nums[2] = {5, 6};
printf("nums[0] = %p\n",&nums[0]); // 0x7fff5fbff7a0
printf("nums[1] = %p\n",&nums[1]); // 0x7fff5fbff7a4
return 0;
}
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-Lxuu2oBREskiQJj0IjI%2F-Lxuvz52u_0tWj_PiLCq%2Fimage.png?alt=media\&token=9dec80e4-a917-42db-8523-85f1b4537fbf)

数组在使用过程中遇到的最多的问题可能就是下标越界，下面的代码就是越界访问数组示例：

```

int main(int argc, const char * argv[]) {
char charsOne[2] = {'a', 'b'};
char charsTwo[3] = {'c', 'd', 'e'};
charsTwo[3] = 'f';
printf("charsOne[0] = %p\n",&charsOne[0]); // 0x7fff5fbff79e
printf("charsTwo[0] = %p\n",&charsTwo[0]); // 0x7fff5fbff79b
printf("charsOne[0] = %c\n",charsOne[0]); // f
return 0;
}
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-Lxuu2oBREskiQJj0IjI%2F-Lxuw4gTnXYyCEkF764p%2Fimage.png?alt=media\&token=e86e5d4e-c216-4557-95d4-8e22583784ee)

结构体变量占用的内存空间是其成员占用最大内存空间的整数倍

```

struct Person {
int age;
int hegiht;
int weight;
};
int main(int argc, const char * argv[]) {
struct Person p1 = {1, 2, 3};
int size = sizeof(p1);
printf("%i\n",size); // 4 * 3 = 12
printf("变量的地址：%p\n",&p1); //0x7fff5fbff750
printf("%p\n",&p1.age); //0x7fff5fbff750
printf("%p\n",&p1.hegiht); //0x7fff5fbff754
printf("%p\n",&p1.weight); //0x7fff5fbff758
return 0;
}
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-Lxuu2oBREskiQJj0IjI%2F-LxuwAg5j2--ZkNjNpzO%2Fimage.png?alt=media\&token=61baabf7-ef69-40d8-8b2e-86bf8e52b18d)

#### 内存对齐

对于结构体来说，会有内存对齐的规范。 首先结构体从首个成员分配空间；如果空间不够则重新分配，如果空间剩余则会把下一个成员的数据存储到剩余的空间中

```

struct Student {
char id;
double score;
int age;
}
int main(int argc, const char * argv[]) {
struct Student s1 = {'A', 10.0, 15};
int size = sizeof(s1);
printf("%i\n",size); //24
printf("%p\n",&s1.id); //0x7fff5fbff788
printf("%p\n",&s1.score); //0x7fff5fbff790
printf("%p\n",&s1.age); //0x7fff5fbff798
return 0;
}
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-Lxuu2oBREskiQJj0IjI%2F-LxuwHLfnkONCbIQGUSn%2Fimage.png?alt=media\&token=1d87617b-64e7-4fd5-9513-7297fa726eee)

按照前面的说明，系统为 s1 分配内存时以 sizeof(double) 8个字节为单位，所以为 s1.id 分配了8个字节的空间，但是由于id定义为char类型，所以只占了8个字节中数值最小的一个内存空间；由于前面剩下的 8 - 1 = 7个字节不足以存放double类型的值，所以接着为s1.score分配8个字节并占满8个字节空间；最后为s1.age分配8个字节并占用了前4个字节空间。故s1变量在内存中占用的内存大小为 8 X 3 = 24个字节。

如果调换结构体Student成员之间的顺序如下，情况又会发生变化。

```

struct Student {
double score;
char id;
int age;
};
int main(int argc, const char * argv[]) {
struct Student s1 = {'A', 10.0, 15};
int size = sizeof(s1);
printf("%i\n",size); //16
printf("%p\n",&s1.score) //0x7fff5fbff790
printf("%p\n",&s1.id); //0x7fff5fbff790
printf("%p\n",&s1.age); //0x7fff5fbff79c
return 0;
}
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-Lxuu2oBREskiQJj0IjI%2F-LxuwNMaD6Hi8ntqs7Sw%2Fimage.png?alt=media\&token=304c8044-01d9-427e-9b09-d5f35f494b28)

这是由于第二次为 s1.id 分配内存时没有完全占满8个字节的空间，而且第三次为s1.age分配时其需要的4个字节空间也没有超出剩余的8-1 = 7个字节空间，所以s1.age的值按照内存对齐的原则就存放在了第二次分配的8个字节的后4位空间中。

#### 总结

结构体变量所占存储空间受其不同类型的成员排列顺序及编译器内存对齐影响，开发中尽量将相同类型的成员依次定义，有助于节省内存空间。

#### 内存管理

根据以上知识点，我们知道内存管理分为系统和手动管理两个部分。 BSS, Data, Text, Stack 区都是系统自动管理的。Heap 区是手动创建，所以需要手动释放。否则内存会不断的增大，直到进程被强制退出。

不过对于栈区 (Stack )，虽然是由系统管理的，但它也是动态分配的。所以如果栈特别大，比如递归函数，会不断的压栈，也会造成内存耗尽。另外，如果在函数里定义的变量，也会分配在栈区，假设有一个无限循环，局部变量不停的生成，如果不及时释放，也会造成内存溢出。

所以无论是何种内存管理方式，垃圾回收机制，无外乎要注意以上几点。比如 iOS 开发中的 ARC 机制，就是采用了引用计数，来帮我们管理 Heap 区的内容，防止我们遗忘释放对象。 Autorelease Pool 也是一种管理栈区变量的一种机制。

#### 内存泄漏

内存泄漏是指分配的内存未能及时释放，导致这部分的内存长期占用，产生了浪费，甚至还会导致系统资源不足引起崩溃。

根据上述提到的点，这种场景确实是会发生的。

**场景一**

```

int *pOld = (int*) malloc( sizeof(int) );
int *pNew = (int*) malloc( sizeof(int) );
```

这两段代码分别创建了一块内存，并且将内存的地址传给了指针 pOld 和 pNew。此时指针 pOld 和 pNew 分别指向两块内存。

如果接下来进行这样的操作：

`pOld = pNew;`

pOld 指针就指向了 pNew 指向的内存地址，这时候再进行释放内存操作：

`free(pOld);`

此时释放的 pOld 所指向的内存空间就是原来 pNew 指向的，于是这块空间被释放掉了。但是 pOld 原来指向的那块内存空间还没有被释放，不过因为没有指针指向这块内存，所以这块内存就造成了丢失。

**场景二**

另外，不应该进行类似下面这样的操作：

`malloc( sizeof(int) );`

这样的操作没有意义，因为没有指针指向分配的内存，无法使用，而且无法通过 free() 释放掉，造成了内存泄露。

**泄漏检测**

我们可以用 `leaks` 命令 (Mac 系统下);

首先在命令行下用 MallocStackLogging=1 ./test.out & 打开 Malloc 调试日志。 当你运行上面的命令后，会出现一个线程 id。然后运行命令：leaks 线程 ID， 即可看到是否有泄漏。 注意: 进程需要保持运行状态，否则退出后，所有的内存都会被销毁，就没办法跟踪了。可以用 sleep(100) 能让它一直运行

比如

```

#include
#include
void f()
{
char *c = malloc(24); // 泄漏在此，没有对应的 free 操作
}

int main()
{
f();
sleep(100);
return 0;
}
```


# 事件交付: 响应链

当你设计APP时，很可能要动态地响应事件。例如：触摸可能来自屏幕上不同的对象，你必须理解这些对象，知道如何正确的响应这些事件。

当用户生成的事件发生时，UIKit 会创建包含所需要处理的信息在一个事件对象里。然后将它放置在当前活动APP的事件队列中。对于触摸事件，它是将一组触摸封装在 UIEvent 对象里。对于运动事件，这个事件对象取决定于你使用的框架，并在里面有一些你所感兴趣的运动类型。

一个事件延特定的路径旅行，在它被交给下一个对象前，你都可以处理它。首先,单例的 UIApplication 对象从队列顶部拿到这个事件，并调度处理它。通常，它发送该事件给 **Key Window** 对象，然后它根据不同的事件类型传递给不同的实例对象。

* 触摸事件. 对于触摸事件，window 对象首先交给触摸发生的 view ，这个 view 必须是一个 hit-test view. 这个发现的过程是通过 hit-test view 调用 hit-testing 方法找到的。看下面的图例: *Hit-Testing Returns the View Where a Touch Occurred.*
* 运动与远程控制事件. 对于这些事件，window 对象发送摇晃或远程控制事件给第一个响应者(first responder)操作。关于第一响应者的描述看下面的 *The Responder Chain Is Made Up of Responder Objects*

这些事件的最终目的是找到一个对象可以处理和响应它。因此 UIKit 首先发送到最适合处理这些事件的对象. 对于触摸事件，该对象是 hit-test 视图, 对于其他的事件，是第一响应者。以下部分详细描述 hit-test视图与第一响应者。

#### Hit-Testing 返回触摸发生的视图 (Hit-Testing Returns the View Where a Touch Occurred)

iOS 使用 hit-testing 来发现发生在触摸上的视图。Hit-testing 包括检查触摸是否发生在相关 view 的范围内。如果是的，它会递归检查视图下的和个子视图. 在视图层级中最底层的视图变成 hit-test view. 最后 iOS 向这个视图发送触摸事件.

下面的例子, 假设用户触摸在视图 E 在. iOS 通过检查各个子视图来找到 hit-test view

1. 触摸发生在 view A, 所以它检查 view B 与 C
2. 解摸不在 view B 范围，但它在 C 范围内，所以检查 D 和 E
3. 触摸不在 view D 范围，但它在 E 范围

视图 E 是最底层的子视图，所以它变成 hit-test view

这个 `hitTest:withEvent:` 方法从给定的 CGPoint 与 UIEvent 返回 hit test view. 这个 `hitTest:withEvent:` 方法最初从调用它自身 `pointInside:withEvent:` 方法开始。如果这个点在这个视图范围内，`pointInside:withEvent:` 返回 YES, 然后方法会递归调用每个子视图的 `hitTest:withEvent:` .

如果传入的这一点不在视图范围内，首先调用到 `pointInside:withEvent:` 方法时返回 NO, 这个点被点忽略，并且 `hitTest:withEvent:` 返回 nil. 如果一直子视图返回 NO ，那么它下面的所有子视图分支都会被忽略，因为，如果它不在发生这个 subview 上，它也不会不生在它下面的所有子视图中。这个意思是一些子视图可能在父视图的外面，如果触摸的区域在父视图的外面，它也不会接收到事件。这可能发生在了视图的 `clipsToBounds` 属性为 **NO** 时。

一个触摸对象和它的生命周期有关，甚至它之后移出这个视图后。

这个 hit-test view 是被赋予最先处理触摸的对象，如果 hit-test view 不能处理这个事件，这个事件会延着视图响应链进行，走到发现能处理这个对象为止。见下面的详解。

#### 由响应对象组成的响应链 (The Responder Chain Is Made Up of Responder Objects)

许多类型的事件，都依赖于一个响应者链的事件传递。响应链是一系列链接的响应对象，它开始于应用的开始与结束。如果第一个响应者不能响应这个事件，它会转发这个事件给响应链上的下一个响应者.

一个响应对象是一个可以响应和操作事件的对象，UIResponder 类是所有响应对象的基类，它定义的程序接口不仅是操作事件，同样也提供了通用的响应行为。UIApplication, UIViewController, UIView 类都是响应者，意味着所有的视图和大多数关键的控制器都是一个响应者。注意，Core Animation 层不是响应者。

第一响应者被设计为最先接收事件的开始，通常第一响应者是一个视图对象，一个对象要变为第一响应者可以做下面两件事:

* 覆盖 `canBecomeFirstResponder` 方法，并返回 YES
* 接收一个 `becomeFirstResponder` 消息，如果需要，对象可以给它自己发送这个消息

&#x20;*注意: 要确保分配第一响应者时要在你的APP创建对象的图形后。例如，通常你可以在 viewDidAppear: 方法里调用 becomeFirstResponder 方法。如果你试图在 viewWillAppear: 方法里分配，这个时候你的对象图形还没有被创建，所以它的 becomeFirstResponder 方法为 NO.*&#x20;

事件不仅仅是响应链上的唯一对象，响应链还有下面几种对象被使用:

* 触摸事件: 如果 hit-test view 不能操作一个触摸事件，这个事件会被传到 hit-test view 的响应链中
* 移动事件: 操作 UIKit shake-motion 事件的第一响应者必须实现 UIResponder 类里的 motionBegan:withEvent: 或 motionEnded:withEvent: 方法。正如这里的声明:  [Detecting Shake-Motion Events with UIEvent](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/motion_event_basics/motion_event_basics.html#//apple_ref/doc/uid/TP40009541-CH6-SW2)
* 远程控制事件: 要操作远程控制事件，第一响应者必须实现 remoteControlReceivedWithEvent: 方法.
* 控制消息. 当用户操作一个控制器，比如一个按钮，或切换，而且动作指定方法的对象是空，这个消息会延着控制器的视图响应链传递下去。
* 编辑菜单消息. 当用户在编辑菜单点击时，iOS 使用所需要的方法实现响应(比如剪切，拷贝，粘贴)具体看: [Displaying and Managing the Edit Menu](https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/AddingCustomEditMenuItems/AddingCustomEditMenuItems.html#//apple_ref/doc/uid/TP40009542-CH13)
* 文本编辑. 当用户在文本视图里点击文本输入框时也会自动变为第一响应者。默认，虚拟键盘出现，而且会聚焦在文本输入框中。你可以显示一个定制的输入视图代替键盘。你也可以添加一个自定义的输入视图给任何响应对象。更多信息看这里: [Custom Views for Data Input](https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/InputViews/InputViews.html#//apple_ref/doc/uid/TP40009542-CH12)

#### 响应链遵循特定的响应路径

如果实例化的对象(hit-test view 或 第一响应者)没有操作这个事件，UIKit 将传递这个事件给响应链的下一个响应者。每个响应者决定是否操作这个事件或将它通过调用 nextResponder 方法传递给下一个响应者。这个过程持续到没有下一个响应者为止。

这个响应链最初是用 iOS 捕获事件后将它传递给一个实例化对象，通常是一个 view。这个实例化的视图有最初处理事件的机会。下面的图显示了两种不同的事件交付方法。一个APP的事件交付取决于它的特定结构，但所有的事件交付路径都采用同样的试控法则。

对于左边的APP，事件遵循下面的路径

1. initial view 试图操作这个事件或消息. 如果它不能操作这个事件，它将这个事件传给它的 superview，因为这个 initial view 不是它的视图控制器的顶级视图
2. superview 试图操作这个事件. 如果 superview 不能操作这个事件，它将这个事件传给它的 superview, 因为它也不是顶级视图
3. 在视图控制器的 topmost view 试图操作这个事件. 如果这个顶级视图不能操作这个事件，它将这个事件传递给它的视图控制器
4. view controller 试图操作这个事件，如果它也不能操作，就传递给 window
5. 如果 window 对象不能操作这个事件，它就传递给 singleton app object
6. 如果 app object 不能操作这个事件，它就会被丢弃.

右边的的图在路径上稍有不同，但所有的交付路径都遵守下列的法则

1. 一个视图传递事件给它的上级视图，直到发现它的顶级视图
2. 顶级视图传递事件给视图控制器
3. 视图控制器传递这些事件给它的顶级视图的父视图. &#x20;

   重复 1-3 步，直到事件发现根视图控制器.
4. 根视图控制器传递事件给 window 对象.
5. window 对象传递事件给应用对象(app object)

*重要: 如果你实现了一个自定义的视图去操作远程控制事件，动作消息，摇晃动作事件或编辑菜单消息，不要直接转发事件给下一个响应者(nextResponder)。要用调用父类实现来让UIKit 帮助你转发事件传递操作*


# iOS 手势识别

### 手势识别

手势识别器将抽像度低的事件操作转化为更容易理解的动作，它们是附加在视图上的对象，并允许对这些动作进行回应。手势识别器解释这些触摸事件是否是一个特定的手势，比如轻扫，缩放，旋转。如果它们能正确识别，它们会发送一个动作给目标对象。这个对象是你指定的视图控制器，如下图所示。这样的设计模式强大简单，你可以动态指定响应动作，而且你可以添加一个手势识别给一个视图，而不用子类化视图。

![一个附加在视图上的手势识别](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/gestureRecognizer_2x.png)

### 使用手势识别简化事件操作

UIKit 框架提供了预先定义的一些常用手势定义。它提供的预定义手势，大大简化了你自己去写识别代码,而且用标准的识别可以确保你的APP可以正确识别用户所期望的内容。

如果你想自己定义一个不一样的手势，比如对勾或圈圈，你可以创建属于你自己的手势识别。如果设计和实现这些专属的手势识别，可以看这里 [创建一个定制的手势识别](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW44)

#### 内嵌的常用手势识别

下表是UIKit 框架自带的手势类别

* 手势 (Gesture)    UIKit class
* 点击 (Tapping (any number of taps))     UITapGestureRecognizer
* 捏合 (Pinching in and out (for zooming a view))        UIPinchGestureRecognizer
* 滑动或拖动 (Panning or dragging)                    UIPanGestureRecognizer
* 轻扫 (Swiping (in any direction))            UISwipeGestureRecognizer
* 旋转 (Rotating (fingers moving in opposite directions))    UIRotationGestureRecognizer
* 长按 (Long press (also known as “touch and hold”))        UILongPressGestureRecognizer

你必须遵循苹果的规范去正确响应用户的手势操作 , 见 [iOS Human Interface Guidelines](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/index.html#//apple_ref/doc/uid/TP40006556)

#### 手势附加在一个视图上

任何手势都和一个视图相关联，不过，一个视图可以有多个手势，因为一个视图可能会响应多个不同的手势。为了一个视图能识别特殊的手势，你必须附加手势识别到这个视图上。当用户触摸这个视图时，手势识别会在视图动作前收到触摸的消息，然后手势可以应答这个消息。

#### 手势触发动作信息

当手势识别器识别它特定的手势，它会发送一个动作消息给它的对象。要创建一个手势识别，我要实例化它并附加一个对象和动作

#### 离散和连续的手势

手势是离散或者连续的。一个离散的手势，比如轻击，只发生一次。一个连续的手势，比如捏合，是在一段时间内发生。对于离散的手势，手势识别器只发送一个单一的动作消息给对象。而一个连续的手势会持续的发送动作消息给它的对象直到多点触摸结束，正如下图所示：

![离散和连续的手势](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/discrete_vs_continuous_2x.png)

### 响应手势识别事件

要添加一个手势给你的APP有三件事情是你要去做的

1. 创建和配置一个手势识别实例

   &#x20;这一步包括附值一个目标对象，动作，有时还会要指定特定的手势属性(比如 numberOfTapsRequired)
2. 附加这个手势识别器给视图
3. 实现响应这个动作的方法

#### 使用 Interface Builder 附加手势

PS: 用的不多，大家有空自己看苹果官方文档吧

#### 编码方式添加手势识别

你可以通过allocating 和 initializing 来实例化 UIGestureRecognizer 的子类，比如UIPinchGestureRecognizer.

如果你创建这个实例化对象后，你需要通过 addGestureRecognizer: 方法附加给一个 View. 下面是一个示例

```
- (void)viewDidLoad {
     [super viewDidLoad];

     // Create and initialize a tap gesture
     UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
          initWithTarget:self action:@selector(respondToTapGesture:)];

     // Specify that the gesture must be a single tap
     tapRecognizer.numberOfTapsRequired = 1;

     // Add the tap gesture recognizer to the view
     [self.view addGestureRecognizer:tapRecognizer];

     // Do any additional setup after loading the view, typically from a nib
}
```

#### 响应离散的手势

当你创建了手势识别并添加了 action 方法后，你就可以用此方法去响应手势操作。下面的代码是一个响应视图点击并通过 locationInView: 方法定位显示图片的例子

> 原码在这里: [Simple Gesture Recognizers](https://developer.apple.com/library/ios/samplecode/SimpleGestureRecognizers/Introduction/Intro.html#//apple_ref/doc/uid/DTS40009460)

```
- (IBAction)showGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer {
       // Get the location of the gesture
      CGPoint location = [recognizer locationInView:self.view];

       // Display an image view at that location
      [self drawImageForGestureRecognizer:recognizer atPoint:location];

       // Animate the image view so that it fades out
      [UIView animateWithDuration:0.5 animations:^{
           self.imageView.alpha = 0.0;
      }];
}
```

每个手势都有它特定的一些属性，比如下面的例子， showGestureForSwipeRecognizer: 方法使用 swipe 手势识别的 direction属性来判断用户滑动的方向是左还是右。

```
// Respond to a swipe gesture
- (IBAction)showGestureForSwipeRecognizer:(UISwipeGestureRecognizer *)recognizer {
       // Get the location of the gesture
       CGPoint location = [recognizer locationInView:self.view];

       // Display an image view at that location
       [self drawImageForGestureRecognizer:recognizer atPoint:location];

       // If gesture is a left swipe, specify an end location
       // to the left of the current location
       if (recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
            location.x -= 220.0;
       } else {
            location.x += 220.0;
       }

       // Animate the image view in the direction of the swipe as it fades out
       [UIView animateWithDuration:0.5 animations:^{
            self.imageView.alpha = 0.0;
            self.imageView.center = location;
       }];
}
```

#### 响应一个连续的手势

连续手势允许你的app响应手势正在发生的过程，例如，你可以在用户捏合时放大或缩小，或允许用户在屏幕在拖动对象

下面的例子显示当用户旋转手势时，将显示一个旋转后的图片。当用户手势放开时，将淡出并将图片回到水平方向。

```
// Respond to a rotation gesture
- (IBAction)showGestureForRotationRecognizer:(UIRotationGestureRecognizer *)recognizer {
       // Get the location of the gesture
       CGPoint location = [recognizer locationInView:self.view];

       // Set the rotation angle of the image view to
       // match the rotation of the gesture
       CGAffineTransform transform = CGAffineTransformMakeRotation([recognizer rotation]);
       self.imageView.transform = transform;

       // Display an image view at that location
       [self drawImageForGestureRecognizer:recognizer atPoint:location];

      // If the gesture has ended or is canceled, begin the animation
      // back to horizontal and fade out
      if (([recognizer state] == UIGestureRecognizerStateEnded) || ([recognizer state] == UIGestureRecognizerStateCancelled)) {
           [UIView animateWithDuration:0.5 animations:^{
                self.imageView.alpha = 0.0;
                self.imageView.transform = CGAffineTransformIdentity;
           }];
      }

}
```

每次方法调用，在 drawImageForGestureRecognizer: 方法里都会被设置为不透明。当用户手势完成时，图片又会在 animateWithDuration: 方法里设置为透明。这个 showGestureForRotationRecognizer: 方法会持续检测手势状态。关于这个状态解释可以查看下面的 "手势状态机"。

### 如何定义手势识别交互转换

常常当你添加手势识别到你的app 后，如果你想要在与其它触摸事件之间做特定的交互时，你首先要理解一下更多关于手势识别工作的细节。

#### 有限状态机中的手势识别

下图中，所有的手势开始于 Possible 状态 (UIGestureRecognizerStatePossible)，它们分析收到的多次触摸序列，然后决定转化为 fail 状态或其它状态.

![手势识别状态机](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/gr_state_transitions_2x.png)

当一个离散的手势识别器识别它是这样的手势后，它会将它的状态从 Possible 变为 Recognized (UIGestureRecognizerStateRecognized) ，然后识别完成。

对于连续的手势第一次识别时，将状态从 Possible 变为 Began (UIGestureRecognizerStateBegan). 然后，它再将状态从 Began 变为 Changed (UIGestureRecognizerStateChanged), 然后在手势发生时持续的将状态从 Changed 变为 Changed。直到用户的手指从视图离开，手势状态转变为 Ended (UIGestureRecognizerStateEnded)，然后手势识别完成.

如果你不想长时间适配手势，也只可以将一个连续的手势也可以从 Changed 变为 Canceled (UIGestureRecognizerStateCancelled)。

所有的时候，每次手势状态的改变，手势识别器都会发送一个 action message 给它的目标，除非它的状态是 Failed 或 Canceled.所以一个离散的手势，当从 Possible 到 Recognized 时 只发送一次 action message . 而一个连续的手势在状态变化时会发送很多 action message。

当手势识别状态变为 Recognized (或 Ended)状态时，它会重置经的状态为 Possible. 这个转化不会触发 action message.

### 和其它手势交互

一个视图可以有多个手势识别附加给它，你可以使用视图的 gestureRecognizers 属性来看有多少手势识别器，你也可以动态的添加或删除手势识别器，通过 addGestureRecognizer: 和 removeGestureRecognizer: 方法。

当一个视图有多个手势识别附加给它时，你可能想去解决他们产生的识别冲突。默认的情况下接收到触摸后，它们是没办法设定定顺序的，不过你可以覆盖这些默认的行为:

* 指定一个手势在识别时应该分析其它的手势识别
* 允许两个手势识别同时操作
* 通过触摸分析避免手势识别

使用 UIGestureRecognizer 类方法，delegate methods 或 子类化覆盖方法来达到这样的效果

### 声明指定两个手势的识别顺序

想象一下你想识别两种手势 swipe 与 pan 。你想区别这两种手势所触发的 action. 默认情况时，你的 swipe 手势会被识别为 pan . 这是因为它的手势必要条件被解释为了 pan 手势。

所以你可以显示的指明它们之间的关系，通过调用 requireGestureRecognizerToFail: 方法如下面所示

```
- (void)viewDidLoad {
       [super viewDidLoad];
       // Do any additional setup after loading the view, typically from a nib
       [self.panRecognizer requireGestureRecognizerToFail:self.swipeRecognizer];
}
```

这个方法指明只有其它的手势失败时才会让它自己开始识别。

> 注意如果你有单击与双击手势识别时，不能设定单击需要在双击失败时进行，因为这不符合苹果设计规范，也许用户就只是想单击而已

### 通过分析触摸来避免手势识别

你可以修改手势行为，通过添加一个 delegate 对象去体质手势识别。这个 UIGestureRecognizerDelegate 协议支持多种办法分析触摸避免识别。你可以识别 gestureRecognizer:shouldReceiveTouch: 方法或 gestureRecognizerShouldBegin: 方法，这两个方法都是 UIGestureRecognizerDelegate 协议里可选的

当手势触摸开始，如果你想立即决定是否识别本次触摸，使用 gestureRecognizer:shouldReceiveTouch: 方法，这个方法每次触摸都发被调用。返回 NO 避免识别，返回 YES 不会修改手势识别状态。

下面的例子使用 gestureRecognizer:shouldReceiveTouch: 代理方法避免收到触摸手势在定制的 subview 后识别

```
- (void)viewDidLoad {
    [super viewDidLoad];
    // Add the delegate to the tap gesture recognizer
    self.tapGestureRecognizer.delegate = self;
}

// Implement the UIGestureRecognizerDelegate method
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    // Determine if the touch is inside the custom subview
    if ([touch view] == self.customSubview){
        // If it is, prevent all of the delegate's gesture recognizers
        // from receiving the touch
        return NO;
    }
    return YES;
}
```

gestureRecognizer:shouldReceiveTouch: 与 gestureRecognizerShouldBegin: 的区别是，shouldReceiveTouch 会拿到 touch 事件，决定是否开始识别，而 gestureRecognizerShouldBegin 是准备改变手势状态时，你可以通过识别的情况来决定是否转化。 shouldReceiveTouch 会最先回调。

### 允许同时响应手势识别

默认的情况时，同一时间两个手势不能同时被识别。但有一种情况下，比如你想用户在缩放视图时同时旋转同时进行。你需要改变他的默认行为了。 可以用 gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: 这个 UIGestureRecognizerDelegate 协议里的方法。这个方法会在判定是否阻止一个方法在识别时，同时阻止另一个的识别。默认它会返回 NO. 返回 YES 的会你可以让两个手势同时识别。

> 注意：虽然返回 YES 可以让两个手势同时被识别，但设置为 NO 不一定它不会被同时识别，因为也许另一个手势的 delegate 里会设置为 YES

### 指定两个手势之间的单向关系

如果你想控制两个手势的交互的单向关系，你可以覆盖 canPreventGestureRecognizer: 或 canBePreventedByGestureRecognizer: 子类方法，返回 NO (默认为 YES). 例如，你想旋转时防止收缩手势，但你不想收缩时防止旋转，你可以这样指定:

`[rotationGestureRecognizer canPreventGestureRecognizer:pinchGestureRecognizer];`

然后覆盖这个手势子类方法返回 NO. 更多的子类化手势定制可以看下面的: 创建自定义手势

### 与其它界面控件的交互

在 iOS 6 之后，默认的控件避免了重叠的手势行为。例如，你有一个按钮并也附加了单击动作，然后你又有一个单击的手势附加在了这个按钮的父视图上，然后用户点击了这个按钮，这个按钮方法会首先拦截到这个触摸事件而不去响应手势识别。这样的规则只在默认的控件行为上，包括:

* 在 UIBUutton, UISwitch, UIStepper, UISegmentedControl, 和 UIPageControl 上单击
* 在 UISlider 上滑动
* 在 UISwitch 上平移。

如果你想定制这些控件的默认行为，请尽量参照 iOS 人机交互指南.

## 解释手势识别触摸原理

到目前为止，你已经了解到了怎样识别手势并如何响应他们，然而，如果你要创建一个自定义的手势识别，你还需要了解更多关于它的细节原理。

### 一个事件包括所有的点击序列

在 iOS 里，触摸表示手指出现在屏幕上或在上面运动。一个手势有一次或多次触摸，他们都被 UITouch 对象所表示。例如，一次捏合手势里有两个手指在屏幕上向相反的方向运动。

事件包括了一段时间里所有的触摸序列，触摸序列开始于一个手指的触摸，直到最后一个手指离开屏幕。作为一个手指移动，iOS 发送触摸对象给事件。多触摸事件被表示为 UIEventTypeTouches 类型的 UIEvent 对象.

每个触摸对象只跟踪一个手指，在一个多触摸序列中。这序列期间， UIKit 跟踪手指并更新它的触摸属性，这些属性包括触摸阶段，在视图的位置，上一次的位置，时间。

这个触摸阶段指明触摸开始，是否移动或停止等。看下图:

![触摸序列与阶段](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/event_touch_time_2x.png)

### APP 收到触摸时调用的操作方法

在多点触摸序列中，当有新的或改变的触摸阶段时都会发送信息给这些回调方法:

```
touchesBegan:withEvent: 当一个或多个手指触摸到屏幕时
touchesMoved:withEvent: 当手指开始移动时
touchesEnded:withEvent: 当一个或多个手指离开屏幕时
touchesCancelled:withEvent: 当触摸被系统取消时，比如来电话时
```

> 注意这些方法和手势识别状态无关，比如 UIGestureRecognizerStateBegan 和 UIGestureRecognizerStateEnded ，手势识别状态只表示它自己的识别状态，不是触摸的状态。

## 将触摸发送到视图

当你想修改触摸的交付路径时，你需要理解它的默认行为。在一般情况下，当触摸发生，这个触摸对象会从 UIApplication 对象传递给 UIWindow 对象。在传递触摸对象给视图之前，它会先发送触摸对象到附加在触摸到的视图（或他的父视图）上的手势识别对象，

![默认触摸的交付路径](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/path_of_touches_2x.png)

### 手势识别首先得到识别触摸的机会

一个 window 延迟交付触摸对象给这个视图，所以是手势识别首先识别手势，在延误期间，如果手势能识别这个触摸手势，接下来这个 window 永远不会将触摸事件传递给视图.

例如你有一个离散的手势识别需要两个手指，下图显示了两个手指传入时将会的变化:

![触摸信息序列](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/recognize_touch_2x.png)

注意最后当触摸被识别后，之前在视图上的触摸就会被取消，发送了 touchesCancelled:withEvent: 方法。

### 影响交付触摸到视图

你可以通过一些 UIGestureRecognizer 属性修改默认的将会行为，如果你改变了它们默认的属性，你将得到下列的行为

* delaysTouchesBegan (默认为 NO) ，默认 window 在 Began 和 Moved 阶段会发送触摸对象给视图和手势. 如果设置为 YES 的时候，window 会延迟交付给视图，这确保当手势识别成功后不再将交付给视图. 注意设置这个属性的时候要谨慎，他可能会让你的界面感觉无反应。

这样的设置提供了一个类似的行为在 UIScrollView 的 delaysContentTouches 属性中，在这样的场景下，当滚动开始不久后，scroll-view 里的子对象不会收到 touch 信息，这样不会让他看直起来闪烁。

* delaysTouchesEnded (默认为YES) , 当它为 YES 时，如果手势想取消它时，确保视图不完成 action 。当手势识别在分析手势时， window 不会在 Ended 阶段交付触摸对象给视图。当手势识别它是手势时，触摸对象被取消。如果 识别失败，window 会交付它到视图，发送 touchesEnded:withEvent: 信息。设置它为 NO 时， 在发析时，也同时发送信息给视图。

考虑这样的情部，一个视图需要双击识别，用户双击这个视图，如果 属性设置为 YES, 这个视图会得到 touchesBegan:withEvent:, touchesBegan:withEvent:, touchesCancelled:withEvent:, 和 touchesCancelled:withEvent:。如果这个属性设置为 NO, 这个视图收到这样的序列 : touchesBegan:withEvent:, touchesEnded:withEvent:, touchesBegan:withEvent:, 和 touchesCancelled:withEvent:, which means that in touchesBegan:withEvent:，这个视图可以识别一个双击

如果手势识别判定一个触摸不是它手势的一部分，它可以直接传递给它的视图，要做到这一点，可以调用手势识别方法 ignoreTouch:forEvent: 。

## 定制手势识别

实现一个定制的手势，首先要创建 UIGestureRecognizer 的子类，然后 import 这个子类的头文件

`#import <UIKit/UIGestureRecognizerSubclass.h>`

然后从 UIGestureRecognizerSubclass.h 头文件中拷贝下面的方法声明到你的子类里:

```
- (void)reset;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
```

这些方法和触摸操作一样有明确的行为，在你覆盖它们前，你要先调用父类的实现，即使它是一个空的实现。

注意 state 这个属性在你引入 `UIGestureRecognizerSubclass.h` 头文件后，就从只读变为可读写的了。

### 实现自定义的触摸实别操作

核心是在于通过下面四个方法 `touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCancelled:withEvent:` 去将抽像低的触摸行为变为可识别的高级行为。

下面这个例子中，仅有一个 view ，但很多 app 中有多个 view ， 通常，你应该转换为屏幕坐标，以正确识别手势。

```
#import <UIKit/UIGestureRecognizerSubclass.h>

// Implemented in your custom subclass
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    if ([touches count] != 1) {
        self.state = UIGestureRecognizerStateFailed;
        return;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    if (self.state == UIGestureRecognizerStateFailed) return;
    UIWindow *win = [self.view window];
    CGPoint nowPoint = [touches.anyObject locationInView:win];
    CGPoint nowPoint = [touches.anyObject locationInView:self.view];
    CGPoint prevPoint = [touches.anyObject previousLocationInView:self.view];

    // strokeUp is a property
    if (!self.strokeUp) {
        // On downstroke, both x and y increase in positive direction
        if (nowPoint.x >= prevPoint.x && nowPoint.y >= prevPoint.y) {
            self.midPoint = nowPoint;
            // Upstroke has increasing x value but decreasing y value
        } else if (nowPoint.x >= prevPoint.x && nowPoint.y <= prevPoint.y) {
            self.strokeUp = YES;
        } else {
            self.state = UIGestureRecognizerStateFailed;
        }
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    if ((self.state == UIGestureRecognizerStatePossible) && self.strokeUp) {
        self.state = UIGestureRecognizerStateRecognized;
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    self.midPoint = CGPointZero;
    self.strokeUp = NO;
    self.state = UIGestureRecognizerStateFailed;
}
```

对于状态转化离散和持续手势是不一样的，正如上面的 "有限状态机中的手势识别" 所述一样，当你创建一个自定义手势识别，你必需明确指明它的相关状态是否为离散或连续。上面的那个例子，它的状态永远不会设置为 Began 或 Changed ，因为它是离散的。

更多的自定义手势资料可以查看 [WWDC 2012: Building Advanced Gesture Recognizers](https://developer.apple.com/videos/wwdc/2012/?id=233)

#### 重置手势状态

如果你的手势识别转变为 Recognized/Ended, Canceled, 或 Failed, 这个 UIGestureRecognizer 类会在状态变为 Possible 之前调用 reset 方法

实现 reset 方法以便重置你的一些内部状态，以便你可以为下一次的识别做准备。下面是一个例子:

```
- (void)reset {
    [super reset];
    self.midPoint = CGPointZero;
    self.strokeUp = NO;
}
```


# iOS 手势操作详解

## 为什么要有手势

在我们拿起手机时，除了语音外，最重要的交互操作就是通过手指触摸了。就如游戏机的手柄，电脑的键盘一样。在iPhone设备里，因为它没有物理键盘，所以每一个手势动作，必须准确的覆盖到每一个用户想要表达的真实意图。手势的重要性也不言而喻。

## 苹果如何设计手势

### 什么时候开始识别

当我们手机里的App启动时，它就运行在一个无限循环里，这样使它不会在打开后立即退出(NSRunloop)，不过它也提供了打断这个循环的条件，比如触摸( UITrackingRunLoopMode )。当它发生时，会立即被App 接收到，可以让能响应手势事件的对象捕获到它，并根据用户的触摸动作作出回应。

### 捕捉用户触摸流程

当用户触摸屏幕或摇晃设备等操作时，iOS 会将这些操作信息，包括发生时间，动作等传递给你的App, 然后你可以根据这些信息、更自然的响应用户操作，并带来真实的体验。

### 关于 iOS 上的事件分类 (event)

事件被包装为一个对象发给你的APP, 在 iOS 设备里，事件有下面几类

![Event handling](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/events_to_app_2x.png)

* 多点触摸事件 (Multi-Touch events)
* 动作事件         (Motion events)
* 远程控制事件 (Remote control events)

### UIKit 对手势的封装

iOS APP 可以识别多种手势操作，比如放大，捏合，滚动等。实际上一些常用的手势操作被封装到了 UIKit 框架里，比如UIControl的子类，如 UIButton 或 UISlider, 他们可以响应点击或滚动。你可以通过苹果设计的 target-action 设计模式将这些手势操作绑定到这些对象上，比如

```
[button addTarget:self action:@selector(pressedButton:) forControlEvents:UIControlEventTouchUpInside];
```

```
你也可以使用这种方式在一个普通的视图里添加手势绑定，这样他就可以像一个控件器样响应你的手势操作。

手势识别是对复杂的触摸动作，做出的一种抽像封装。你可以自己定制和识别特殊的手势动作，有不少第三方库提供了开源的代码，比如利用手势识别算法 [$1 Unistroke Recognizer](http://depts.washington.edu/aimgroup/proj/dollar/)实现复杂的手势识别操作。让它可以识别文字，手势密码等。

[iOS 的手势识别文档](/ios/gesture/recognizer/2014/08/28/ios-gesture-recognizers/)
```

一个事件按照特定的路径寻找处理它的对象 当 iOS 识别一个事件后，它会将这个事件发送给和它最近的对象，比如一个被触摸的视图，如果这个对象不能处理它，它会传递给下一级的对象，走到有人可以处理它，这是一种响应链设计模式。

[iOS 事件交付: 响应链](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html#//apple_ref/doc/uid/TP40009541-CH4-SW2)

一个 UIEvent 包括了触摸(Touch), 摇晃(Shake-Motion), 或远程控制(Remote-Control)事件 很多事件都是 UIEvent 类的实例。一个 UIEvent 对象包括了很多你如何响应用户的事件. 一个用户动作，比如手指触摸和在屏幕移动上移动，都会不断有事件对象发向App 去处理它们。每个事件都有一个触摸类型，比如摇晃，远程控制等。

[多点触摸事件(Multitouch Events)](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/multitouch_background/multitouch_background.html#//apple_ref/doc/uid/TP40009541-CH5-SW9)

[位移事件(Motion Events)](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/motion_event_basics/motion_event_basics.html#//apple_ref/doc/uid/TP40009541-CH6-SW14)

[远程控制事件(Remote Control Events)](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Remote-ControlEvents/Remote-ControlEvents.html#//apple_ref/doc/uid/TP40009541-CH7-SW3)

#### 当用户触摸视图时，APP 会接收到 Multitouch 事件

APP 本身的 UIKit 控制器和手势识别基本满足你所有的触摸事件操作，甚至你可以自定义 UIView 实现自己的手势识别规则

#### 当用户移动他们的设备时，APP 会接收到 Motion 事件

Motion 事件支持的信息包括设备位置，方向，动作，他们都会被记录到。你还可以通过加速度和陀螺仪添加微妙强大的功能应用程序

Motion 事件适用于不同的场景，我可以通过不同的框架操作它们。当你摇晃设备时，你可以通过 UIKit 框架收到 UIEvent 对象到你的 app. 如果你想app 可以接收，灵敏持续的加速计或陀螺仪数据 ，你可以使用 Core Motion Framework

#### APP 可接来自多媒体控件器发出的远程控制事件

这些事件可以控制声音和视频


# UIKit Class Hierarchy

是时候复习一下基础内容了，先从最常用到的 UIKit 开始吧。

[UIKit reference introduction](https://developer.apple.com/library/ios/documentation/uikit/reference/UIKit_Framework/Introduction/Introduction.html)

先看看这个层级结构图吧： ![image](https://developer.apple.com/library/ios/documentation/uikit/reference/UIKit_Framework/Art/uikit_classes.jpg)

### [UIAcceleration](https://developer.apple.com/library/ios/documentation/uikit/reference/UIAcceleration_Class/Reference/UIAcceleration.html):

加速计类: 加速度实为UIAcceleration对象实例，又被称为加速事件，它代表即时的三维空间上，三个不同轴上的加速度数据。

![加速计三维空间](https://developer.apple.com/library/ios/documentation/uikit/reference/UIAcceleration_Class/Art/device_axes.jpg)

实用的场景中可以用于“摇晃”，游戏中用于控制对象移动等。

*iOS 5.0之后，它被放在了 CoreMotion 框架里*

### [UIAccelerometer](https://developer.apple.com/library/ios/documentation/uikit/reference/UIAccelerometer_Class/Reference/UIAccelerometer.html)

获得当前设备的加速计单例，里面的delegate 可以获得 UIAcceleration 的实例

*iOS 5.0之后，它被放在了 CoreMotion 框架里*

### [UIAccessibilityElement](https://developer.apple.com/library/ios/documentation/uikit/reference/UIAccessibilityElement_Class/Reference/Reference.html)

这个类封装的是可便捷的访问信息，主要是针对一些特殊人群，比如颜色识别有困难，残障人士等。 默认情况下你的设备设定中是不开启的，如果你要开启时，可以在设置里的Accessibility中打开相应的辅助功能。

*原生的UI组件的`isAccessibilityElement`默认是YES的。自定义的UI组件的isAccessibilityElement属性是NO，当`isAccessibilityElement`为NO时，instruments将无法捕获。所以这种情况，我们需要将自定义UI的的isAccessibilityElement属性置为YES，instruments就能获取到。*

### [UIBarItem](https://developer.apple.com/library/ios/documentation/uikit/reference/UIBarItem_Class/Reference/Reference.html)

UIBarItem 是一个抽象的超类，用来在Bar上添加道具。 类似于一个按钮。有标题，有图片，动作和 目标。

**UIBarButtonItem**

这个类的实例是用在 UIToolbar 或 UINavigationBar 上的专用对象按钮。它从它的抽象父类 UIBarItem 继承的基本按钮的行为。比如 NavigationBar 的返回，关闭按钮。

**UITabBarItem**

这个类的实现，用于 Tabbar 上的按钮对象。

### [UIBezierPath](https://developer.apple.com/library/ios/documentation/uikit/reference/UIBezierPath_class/Reference/Reference.html)

UIBezierPath类可以创建基于矢量的路径。

此类是Core Graphics框架关于path的一个封装。使用此类可以定义简单的形状，如椭圆或者矩形，或者有多个直线和曲线段组成的形状。

它还提供了添加二次贝塞尔曲线和三次贝塞尔曲线的支持。

### [UIColor](https://developer.apple.com/library/ios/documentation/uikit/reference/UIColor_Class/Reference/Reference.html)

绘制UI中用的最多的一个类了吧，一个 UIColor 对象代表颜色，alpha 值。可以使用 UIColor 对象来存储颜色数据，并在绘画过程中，你可以用它们来设置当前填充和笔触颜色。

### [UIDecive](https://developer.apple.com/library/ios/documentation/uikit/reference/UIDevice_Class/Reference/UIDevice.html)

UIDevice类提供了一个单例代表当前设备。从这个实例中，可以获取有关设备的唯一的ID，分配名称，设备型号，和操作系统名称和版本等信息。也可以使用的UIDevice实例，检测设备的特点， 如物理方向的变化。

*常用在判断 iOS 版本。*

### [UIDocumentInteractionController](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIDocumentInteractionController_class/Reference/Reference.html)

文件交互控制器委托对象，提供应用程序管理与本地系统中的文件的用户交互的支持。例如，一个电子邮件程序可能使用这个类，允许用户预览附件和其他应用程序中打开它们。使用这个类， 目前预览相应的用户界面，打开，复制或打印指定的文件。

一般在程序间共享文档可以通过UIDocumentInteractionController（该类经常被开发者忽略），用其它APP预览PDF文档等。

第三方程序只需要通过在info.plist 中注册支持的相关格式，并安装到ios设备中，便可以自由打开，无需你程序中自己检测第三方程序是否安装，而且文件之间的传输也实现了跨出沙盒的功能。

### [UIEvent](https://developer.apple.com/library/ios/documentation/uikit/reference/UIEvent_Class/Reference/Reference.html)

一个 UIEvent 对象（或者简单地说，一个事件对象）在 IOS 中有三种类型的事件：触摸事件(Touch)，运动事件(Motion)和远程控制(RemoteControl)事件。远程控制的事件使一个 Responder 对象来接收来自外部的 附件或耳机的命令，以便它可以管理管理音频和视频，例如，播放视频或跳过到下一音轨。

![event handling](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/events_to_app_2x.png)

触摸事件，会经过一系列的响应链，最后被 UIResponder 接受到。并回调：touchesBegan:withEvent: 方法。

### [UIFont](https://developer.apple.com/library/ios/documentation/uikit/reference/UIFont_Class/Reference/Reference.html)

同样常用的字体设定类，提供了用于获取和设置字体信息的接口。

除了使用系统指定的字体外，还可以使用自定义的字体，只需要将自定义字体加到你的工程资源文件中，并在 info.plist 文件中增加一名为 UIAppFonts 的key。将这个key修改成array 将你用到的所有字体的名字，作为这个array的值，一项一项填进去（包括扩展名），然后就可以在代码中直接用\[UIFont fontWithName:@”CustomFontName” size:12]取得自定义的字体了。

### [UIGestureRecognizer](https://developer.apple.com/library/ios/documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html)

UIGestureRecognizer 是手势识别的抽象基类。它有以下具体的子类：

```
UITapGestureRecognizer: 点击手势，可以识别单次与多次点击
UIPinchGestureRecognizer: 缩放手势
UIRotationGestureRecognizer: 旋转手势
UISwipeGestureRecognizer: 滑动手势
UIPanGestureRecognizer: 拖动手势
UILongPressGestureRecognizer: 长按手势
```

### [UIImage](https://developer.apple.com/library/ios/documentation/uikit/reference/UIImage_Class/Reference/Reference.html)

UIImage 封装了显示图片的高层级方法，你可以从 文件 ，Quartz ，原始图片数据对象（相机，扫描仪等）中创建。

支持的文件格式：

`.tiff, .tif, .jpg, .jpeg, .gif, .png, .bmp, .BMPf, .ico, .cur, .xbm`

*可通过 UIImagePickerController 类从iPhone照片库或照相机获取图像*

### [UILocalizedIndexedCollation](https://developer.apple.com/library/ios/documentation/iPhone/Reference/UILocalizedIndexedCollation_Class/UILocalizedIndexedCollation.html)

提供索引标题的类，常用在TableView的右方索引列。

### [UIMenuController](https://developer.apple.com/library/ios/documentation/iPhone/Reference/UIMenuController_Class/UIMenuController.html)

菜单控制器，默认的单例方法，提供了剪切，复制，粘贴， 选择，选择，和删除功能。你也可以创建自定义的 UIMenuController，

### [UIMenuItem](https://developer.apple.com/library/ios/documentation/uikit/reference/UIMenuItem_Class/Reference/MenuItem.html)

是UIMenuController 的 menuItems 数组里的每个实例。你可以自行定义他们，它只需要两个属性，title 与 action (SEL)

### [UINavigationItem](https://developer.apple.com/library/ios/documentation/uikit/reference/UINavigationItem_Class/Reference/UINavigationItem.html)

一个UINavigationItem 管理显示在 UINavigationBar 上的按钮和视图对象。每个 View Controller push 到 navigation 栈上，必须有一个在 NavigationBar 上包含按钮与被显示的视图 UINavigationItem 对象。

### [UINib](https://developer.apple.com/library/ios/documentation/uikit/reference/UINib_Ref/Reference/Reference.html)

UINib 可以从 Interface Builder nib 文件中将数据进行封装，并将它进行实例化缓存起来。当你需要用到 nib 时，不必去读 nib 文件了。

比如：

```

// Load the hoverView from HoverView.xib
UINib *hoverViewXib = [UINib nibWithNibName:@"HoverView" bundle:nil];
[hoverViewXib instantiateWithOwner:self options:nil];
```

### [UIPasteboard](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPasteboard_Class/Reference.html)

读写系统里的剪贴版内容

### [UIPopoverController](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPopoverController_class/Reference/Reference.html)

iPad 里的专有 Controller, 效果如下：

![UIPopoverController](https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewControllerCatalog/Art/splitview_portrait_popover.jpg)

### [UIPrintFormatter](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPrintFormatter_Class/Reference/Reference.html)

打印格式的抽象基类，可定制打印的页边距等。具体实现类有如下三种：

```

UISimpleTextPrintFormatter
UIMarkupTextPrintFormatter
UIViewPrintFormatter
```

### [UIPrintInfo](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPrintInfo_Class/Reference/Reference.html)

封装了关于打印工作的信息，包括打印的id,名称,输出类型，方向，双面打印模式。当它需要打印时，这些信息会被打印系统使用。

### [UIPrintInteractionController](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPrintInteractionController_Class/Reference/Reference.html#//apple_ref/occ/cl/UIPrintInteractionController)

它包括了打印的UI与，打印文稿，图片和其它打印内容之间的典型交互情景。你可以使用 UIPrintInteractionController 单例方法，将整个打印过程进行控制。

![UIPrint Center](https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/Art/print_options_ipad_2x.png)

### [UIResponder](https://developer.apple.com/library/ios/documentation/uikit/reference/UIResponder_Class/Reference/Reference.html)

该UIResponder类定义了响应和处理事件的对象接口。它是UIApplication，UIView的和它的子类（包括UIWindow）的父类。这些类的实例通常被作为应答对象，或者简单地说，应答者。

通常有两种类型的事件：触摸与运动事件。主要的触摸事件处理方法有 touchesBegan:withEvent:,touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCancelled:withEvent:. 这些方法的参数和他们新的，或有改变的特别触摸事件有关。这样响应对象可以跟踪和处理这些事件。任何时候当手指触摸屏幕，拖动，抬起时，UIEvent 对象都会被创建，这个事件对象包括了所有刚从屏幕抬起时的手指的 UITouch 对象。

```

-touchesBegan:withEvent: // 当用户触摸到屏幕时调用方法
-touchesEnded:withEvent: // 当用户触摸到屏幕并移动时调用此方法
-touchesMoved:withEvent: // 当触摸离开屏幕时调用此方法
-touchesCancled:withEvent: // 当触摸被取消时调用此方法
```

通俗一点说，一个 UIResponder 对象表示一个可以接收触摸屏上的触摸事件的对象, iOS 中，所有显示在界面上的对象都是从 UIResponder 直接或间接继承的。

iOS 3 之后开始支持运动事件：

```

-motionBegan:withEvent: // 运动开始时执行
-motionEnded:withEvent: // 运动结束时执行
-motionCancled:withEvent: // 运动被取消时执行
```

iOS 4 之后开始支持远程控制事件：

`-remoteControlReceivedWithEvent`

接收事件之后，使用到的响应链函数 `-nextResponder` 下一个响应者，在通常的UIView 实现中，一般会返回父级对象

*所以你可以自行改变响应链的响应路径*

[响应链](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html#//apple_ref/doc/uid/TP40009541-CH4-SW2)

```

-isFirstResponder  指示对象是否为第一响应者，这里的第一响应者就是当前焦点
-canBecomeFirstResponder 布尔值，指定对象是否可以变为第一响应者
-becomeFirstResponder   把对象设置为 firstResponder
-canResignFirstResponder   对象是否可以取消 firstResponder
-resignFirstResponder 注销 firstResponder
```

**UIApplication**

这个UIApplication类提供运行在 iOS 上的 app 的集中协调与控制中心，每个 app 只有一个它的实例。当 app 启动时，UIApplicationMain 函数被调用，他会创建一个 UIApplication的单例对象。之后，你就可以通过 sharedApplication 类方法访问到它。

如我们在 main.c 文件中看到的：

`return UIApplicationMain(argc, argv, nil, NSStringFromClass([SomeAppDelegate class]));`

UIApplication 的主要任务是处理传入的用户事件的初始路由。此外，他还维护所有当前打开的窗口列表（UIWindow 对象），所以通过它你可以检索到应用程序的UIView对象。

这个app对象通常分配一个 delegate,接受一些运行时的标致性的通知，比如：app 启动，低内存，app 关闭，然后作出响应。

UIApplication 允许你管理设备的以下几种行为：

* 控制界面方向
* 挂起传入的触摸事件
* 接近感应（用户面部）开和关
* 注册远程通知
* 触发撤销重做 UI
* 判断是打开app 还是 URL
* 退到后台时继续执行任务
* 注册或取消本地通知
* 执行 app 级别的恢复任务

UIApplication 定义的 delegate 必须遵循 UIApplicationDelegate 协议并且实现一些协议的方法

## [UIView](https://developer.apple.com/library/ios/documentation/uikit/reference/uiview_class/uiview/uiview.html)

UIView 类定义了一个矩形区域和管理该区域的接口。在运行时，一个 view 对象在它的区域内进行绘制和处理这些内容的交互。UIView 类本身也提供了一些带背景色的基本填充行为。更多精妙的内容，可以通过你自定义的子类实现一些所须的绘制和交互处理达成。

另外UIKit 框架也自带了一些标准子类，从简单的按钮到复杂的表格都能被使用。例如: UILabel 可以绘制文本内容，UIImageView 绘制一个图片.

参考： [View 与 Window 架构](https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/WindowsandViews/WindowsandViews.html#//apple_ref/doc/uid/TP40009503-CH2-SW1)

[View 动画](https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/AnimatingViews/AnimatingViews.html#//apple_ref/doc/uid/TP40009503-CH6-SW1)

### [UIWindow](https://developer.apple.com/library/ios/documentation/uikit/reference/UIWindow_Class/UIWindowClassReference/UIWindowClassReference.html)

UIWindow 类定义了被称为 window 的对象，它用来管理和协调在设备屏幕上出现的view。除非app有其它外部屏幕上，否则一个app只有一个 window

一个 window的主要两个功能是提供view的显示区域和发配事件到view上，要想改变你的显示内容，你可以改变 window的 root view ,但你不能创建一个新的 window. 一个 window有一个明确的等级结构, UIWindowLevelNormal--表示它相对其它 windows的z轴位置. 例如一个系统提示window出现在 normal window 上面。

### [UILabel](https://developer.apple.com/library/ios/documentation/uikit/reference/UILabel_Class/Reference/UILabel.html)

UILabel 类实现了只读文本 view. 你能使用这个类画一行或多行静态文本。基本的 UILabel 支持简单和复杂的文本样式。你也可以控制它上面的样式，比如使用shadow 绘制阴影或高亮。如果需要，你可以通过子类来实现更加个性化的样式。

参考: [在iOS上显示文本内容](https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/UsingTextClasses/UsingTextClasses.html#//apple_ref/doc/uid/TP40009542-CH2-SW1)

### [UIPickerView](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPickerView_Class/Reference/UIPickerView.html)

UIPickerView 的实现对象叫做 picker view, 它使用类似角子机（赌场里经常见的777机器）来显示一个或多个值的设置。用户通过滚动轮子来选择对应的行的值。

你也可以通过子类来定制显示内容。这个UI由包括(component)元件和行构成。元件是一个滚轮，在上面是一连串一行行的索引内容。在 picker view 上不同的元件从左至右顺虚排列。每一行的内容可以是一个字符串或view 对象。比如 UILabel 或 UIImageView.

![Picker View](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uipickerview_intro.png)

### [UIProgressView](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIProgressView_Class/Reference/Reference.html)

UIProgressView 类表现了一个进度条描述进展的时间。它也支持管理样式的属性。

![UI progress view](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uiprogressview_intro_2x.png)

### [UIActivityIndicatorView](https://developer.apple.com/library/ios/documentation/uikit/reference/UIActivityIndicatorView_Class/Reference/UIActivityIndicatorView.html)

使用一个动态的指示器显示内容正在执行中，它就象一个活动的齿轮。

![UI activity indicator](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uiactivityindicator_intro.png)

### [UIImageView](https://developer.apple.com/library/ios/documentation/uikit/reference/UIImageView_Class/Reference/Reference.html)

一个 image view 对象支持基本的视图容器，可以显示单张图片或几个动画图片。如果是动画图片，UIImageView 类支持控制动画的持续时间和频率，你也可以自由的开始和停止动画。

*iOS 6 之后，支持 “状态保留” 机制，同样也可以用于这里*&#x20;

### [UITabBar](https://developer.apple.com/library/ios/documentation/uikit/reference/UITabBar_Class/Reference/Reference.html)

一个 tabbar 是一个控制器，通常在tab bar controller上下文中，它显示屏幕的底部。在 tab bar 上的每一个按钮是一个叫 UITabBarItem 的类的实例。如果你想替换Bar上的按钮执行不同的 action ，可以使用 UIToolbar 对象。

![UI tab bar](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uitabbar_intro_2x.png)

### [UIToolbar](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIToolbar_Class/Reference/Reference.html)

一个 toolbar 可以控制显示一个或更多的按钮，叫 toolbar items. 要创建 toolbar items, 使用 UIBarButtonItem 类。 要添加 toolbar items 到 toolbar 上，使用 setItems:animated: 方法

![UI tool bar](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uitoolbar_intro_2x.png)

### [UINavigationBar](https://developer.apple.com/library/ios/documentation/uikit/reference/UINavigationBar_Class/Reference/UINavigationBar.html)

这个 UINavigationBar 类用于控制导航栏内容。它是一个bar,通常显示上屏幕的最上方。主要的属性有一个左（返回）按钮，一个中心标题，和可选择的右侧按钮。你可以使用navigation bar 作为一个标准对象或结合 navigation controller 使用.

*在iOS5之后，你也可以自行定制它的样式*

### [UI nav bar](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uinavbar_intro_2x.png)

### [UITableViewCell](https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewCell_Class/Reference/Reference.html)

UITableViewCell 定义了在 UITableView 对象里的每cell的属性和行为。这个类包括了为设置和管理 cell 内容和背景的属性和方法（包括文本，图片，自定义的 view），管理 cell的选择，高亮状态，管理附属 view和初始编辑中 cell 内容。

参考： [TableViewCell 详解](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/TableViewCells/TableViewCells.html#//apple_ref/doc/uid/TP40007451-CH7)

![cell part](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/Art/tv_cell_parts.jpg)

### [UIActionSheet](https://developer.apple.com/library/ios/documentation/uikit/reference/UIActionSheet_Class/Reference/Reference.html)

UIActionSheet 提供了一个单选器，你也可以通过它来提示用户哪些是危险的操作。这个Action sheet 包括一个可选择的标题和一个或多个按钮，每个按钮都代表一个action

![ui action sheet](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uiactionsheet_intro_2x.png)

### [UIAlertView](https://developer.apple.com/library/ios/documentation/uikit/reference/UIAlertView_Class/UIAlertView/UIAlertView.html)

使用 UIAlertView 用于提示用户一些信息，这个 alert View 类似但和 action sheet 表现上不一样。

使用类提供的属性和方法设定 title, message, 和代理来配置按钮。如果你要添加一个自定义的按钮，你必须设定一个delegate. 这个代理一定要遵循 UIAlertViewDelegate 协议。使用 show 方法来显示 alert view.

![ui alert view](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uialertview_intro_2x.png)

### [UIScrollView](https://developer.apple.com/library/ios/documentation/uikit/reference/UIScrollView_Class/Reference/UIScrollView.html)

UIScrollView 类提供了为显示超过一屏的超长内容的支持。它让用户可以支持滑动手势和收缩手势浏览内容。

UIScrollView 也是一些 UIKit 类，包括 UITableView 和 UITextView 的超类。

![UI scroll view](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uiscrollview_intro.png)

### [UITableView](https://developer.apple.com/library/ios/documentation/uikit/reference/UITableView_Class/Reference/Reference.html)

UITableView 实例是为显示和编辑列表信息。

![UI table view](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/Art/tv_plain_style.jpg)

非常详细的文档： [Table View Programming Guide for iOS](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/TableViewStyles/TableViewCharacteristics.html#//apple_ref/doc/uid/TP40007451)

### [UITextView](https://developer.apple.com/library/ios/documentation/uikit/reference/uitextview_class/Reference/UITextView.html)

UITextView 类实现了滚动的行为，多行文本区域. 这个类支持定制的文本样式，也支持文本编辑。通常用来显示多行文本，比如一个比较长的文档时。

![UI text view](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uitextview_intro_2x.png)

### [UISearchBar](https://developer.apple.com/library/ios/documentation/uikit/reference/UISearchBar_Class/Reference.html)

这个 UISearchBar 类实现了一个文本控制器为搜索提供基本的文字输入，这个控制器提供了一个文本输入区域作为文本输入，一个搜索按钮，一个书签按钮，一个取消按钮。这个 UISearchBar 对象实现上不执行搜索操作，你需要使用 遵循 UISearchBarDelegate 协议的delegate，当文本输入和按钮点击时实现它的action。

![UI search bar](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uisearchbar_intro_2x.png)

### [UIWebView](https://developer.apple.com/library/ios/documentation/uikit/reference/UIWebView_Class/Reference/Reference.html)

你可以在应用使用 UIWebView 类嵌入 web 内容。你可以非常简单的创建 UIWebView对象附加了 window上，并发送请求 web 内容。你也可以使用这个类进行返回前进到历史页面，并且你甚至可以以编程方式设定 web 内容属性。

参考： [Safari Web Content Guide](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/Introduction/Introduction.html#//apple_ref/doc/uid/TP40002051)

### [UIControl](https://developer.apple.com/library/ios/documentation/uikit/reference/uicontrol_class/reference/reference.html)

UIControl 是所有 control 对象的基础类，比如 UIButton 和 滚动条 等，他将用户的意图传递给应用。你不能直接实例化 UIControl 类, 代替它的是一些常用的UI子类.

UIControl 主要的任务是定义了一个接口，并且有一些基本实现，当一些事件调用时，会首先调用它们到它们的对象。

为了说明 target-action 原理，可以在Cocoa Fundamentals Guide 看"Target-Action in UIKit" 。如果要了解 Multi-Touch 事件信息,可以看 [Event Handling Guide for iOS](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009541)

UIControl 类也包括了 getting 和 setting control 状态的方法，如，为了确定control是否打开或者高亮。并且它还在内部定义一些触摸的跟踪方法，这些跟踪方法可以被子类覆盖。

![about UI Control](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uicontrol_intro_2x.png)

参考： [About Controls](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/UIControl.html#//apple_ref/doc/uid/TP40012857-UIControl)

### [UIButton](https://developer.apple.com/library/ios/documentation/uikit/reference/UIButton_Class/UIButton/UIButton.html)

一个 UIButton的实例实现了在屏幕上的按钮。当点击它时button截获触摸事件并发送 action 消息到 target 对象. 这个类支持设置title,image和其它外观属性方法。你还可以为不同的状态设定不同的表现形式。

![UI button](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uibutton_intro.png)

### [UIDatePicker](https://developer.apple.com/library/ios/documentation/uikit/reference/UIDatePicker_Class/Reference/UIDatePicker.html)

这个UIDatePicker类实现了一个使用多个齿轮对象，让用户选择日期和时间。iPhone 时间picker例子是一个时间闹钟设定应用，你也可以用它做为一个倒计时器。

![UI date picker](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uidatepicker_intro_2x.png)

### [UIPageControl](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPageControl_Class/Reference/Reference.html)

你可以用UIPageControl 类来创建和管理页码 control. 一个页码 control 显示一个水平的连续小点，每个点代表了文档的一部分。当前显示的页用白色的点表示。

![page control](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uipagecontrol_intro_2x.png)

### [UISegmentedControl](https://developer.apple.com/library/ios/documentation/uikit/reference/UISegmentedControl_Class/Reference/UISegmentedControl.html)

一个 UISegmentedControl 对象是一个由多个部分组成的水平control ，每个部分作为一独立的按钮。

一个 segmented control 可以显示一个title (一个 NSString 对象)或一个图片（UIImage 对象）.这个 UISegmentedControl 对象自动调整他们的大小比例在他们的父视图下，除非他们有一个指定的宽度设置。当你添加和删除部分片段时，你可以请求这个action ，他会有滑动和消退效果。

![UI Segmented control](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uisegmentedcontrol_intro_2x.png)

### [UITextField](https://developer.apple.com/library/ios/documentation/uikit/reference/UITextField_Class/Reference/UITextField.html)

一个 UITextField 也是一个 control 对象，它是一个显示可编辑文本，并且当用户按下return按钮时发送 action 消息到对象。你典型的使用场景是，搜集用户一定数量的文本，并且马上执行一些action ,比如基于文本的搜索操作。

![UI text field](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uitextfield_intro_2x.png)

### [UISilder](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UISlider_Class/Reference/Reference.html)

一个 UISilder 对象是一个可视化的 control , 它使用一个连续的值范围进行选择， Slider 通常显示为水平的滑动条。

![UI slider](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uislider_intro_2x.png)

### [UISwitch](https://developer.apple.com/library/ios/documentation/uikit/reference/UISwitch_Class/Reference/Reference.html)

你可以使用 UISwitch 类来创建和管理 On / Off 按钮。例如，在设置飞行模式和蓝牙设置时。

![UI switch](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/UIKitUICatalog/Art/uiswitch_on_2x.png)

## [UIViewController](https://developer.apple.com/library/ios/documentation/uikit/reference/UIViewController_Class/Reference/Reference.html)

这个 UIViewController 类提供了基本的 view 管理模型。你很少直接实例化UIViewController对象。相反,你实例化UIViewController类的子类执行特定的任务。

视图控制器管理一组视图组成应用程序的用户界面的一部分.作为应用程序的控制器层的一部分, 视图控制器和其它模型对象，其它视图控制器一起构成了一个一致的用户界面。

必要时，一个视图控制器可以：

* 调整和排版他们的 view
* 调整 view 里的内容
* 当用户与他们交互时，反应他们与视图的效果。

参考： [View Controller Programming Guide for iOS](https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40007457)

### [UISplitViewController](https://developer.apple.com/library/ios/documentation/uikit/reference/UISplitViewController_class/Reference/Reference.html)

UISplitViewController是一个管理包含了两边ViewController的视图控制器，你使用这个类去实现主－从界面, 在左边显示的是列表视图控制器，右边是左边点击后的详细视图控制器。它只能适用于 iPad 设备。尝试去用到其它设备都会出现异常。

它没有特定的界面，他的主要工作是管理两个视图控制器在切换方向时的表现形式。

### [UITabBarController](https://developer.apple.com/library/ios/documentation/uikit/reference/UITabBarController_Class/Reference/Reference.html)

UITabBarController 类实现了一个专门的类似radio-style的视图控制器管理界面。这个标签栏界面的标签显示在窗口的底部。它通常按它原来的样式显示，不过在 iOS6 之后可以用子类显示。

每个标签栏控制器的标签对应一个定制的视图控制器。当用户选择一个指定的标签地，这个标签栏控制器替换之前的主视图为相关的视图控制器.

![Tab bar controller views](https://developer.apple.com/library/ios/documentation/uikit/reference/UITabBarController_Class/Art/tabbar_controllerviews.jpg)

你不能直接访问标签控制器上的标签视图。要配置这个标签，你可以分配这个 viewControllers 属性来提供根视图。它的显示顺序也是由你分配viewControllers时的顺序来确定的。

标签栏内容是通过他们相应的视图控制器进行配置，通过创建一个UITabBarItem 类的实例来生成标签内容，配置合适的属性，然后赋给视图控制器的tabBarItem属性.如果你没有提供定制的标签栏内容，那它默认只会显示一个这个视图控制器的标题。

### [UITableViewController](https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewController_Class/Reference/Reference.html)

![Table View](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/Art/tv_plain_style.jpg)

UITableViewController 类创建了一个控制器管理表格视图. 它实现了下列行为:

* 如果nib 文件通过 initWithNibName:bundle: 方法指定(它是父类 UIViewController 声明的方法), UITableViewController 会从这个nib文件中读取这个表格视图结构。否则，它是不能正确配置UITableView对象和计算大小的。你可以通过tableview属性来设置它。
* 如果包含表视图的nib文件加载后，datasource(数据源)和delegate(委托)为nib文件中定义的对象(如果有的话).如果没有指定nib文件或者nib文件定义没有datasource或delegate, UITableViewController 设置表视图的datasource 与 delegate为它自己(self)
* 当表视图第一次出现时，table-view controller重新加载表视图数据。它也在每次显示时清理了选择过的行。这个 UITableViewController 类的方法是在基于父类方法的viewWillAppear:时实现的。你也可以关闭这个默认行为，通过clearsSelectionOnViewWillAppear属性。
* 当表视图被显示后，控制器刷新表视图的滚动条指示器。这个 UITableViewController 类的方法是实现在 viewDidAppear: 里的。
* 它实现了父类方法 setEditing:animated:, 所以当用户点击导航栏上的编辑|完成按钮时，会控制表格的编辑模式。

你可以创建它的子类来管理每个表视图。当你在控制器里初始化了 initWithStyle:，你必须指定表视图的样式（plain 或 grouped）。

参考： [Table View Programming Guide for iOS](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/TableViewStyles/TableViewCharacteristics.html#//apple_ref/doc/uid/TP40007451)

### [UINavigationController](https://developer.apple.com/library/ios/Documentation/UIKit/Reference/UINavigationController_Class/Reference/Reference.html)

UINavigationController 类实现了一专门的视图控制器去管理分层内容的导航。它的导航界面让数据更有效，更简单的呈现。通常你直接使用它即可。iOS 6之后你可以定制它的行为。

![A sample navigation interface](https://developer.apple.com/library/ios/Documentation/UIKit/Reference/UINavigationController_Class/Art/navigation_interface_2x.png)

导航控制器使用导航栈管理屏幕上的显示内容，它是一个视图控制器数据，数据第一个控制器对应的是根视图控制器，最后一个视图控制器对应的是当前屏幕上显示的内容。你可以使用导航控制器类代的方法编辑这个栈。比如你添加一个视图控制器到栈顶用 pushViewController:animated: 方法。

![The views of a navigation controller](https://developer.apple.com/library/ios/Documentation/UIKit/Reference/UINavigationController_Class/Art/NavigationViews_2x.png)

### [UIImagePickerController](https://developer.apple.com/library/ios/documentation/uikit/reference/UIImagePickerController_Class/UIImagePickerController/UIImagePickerController.html)

UIImagePickerController类管理可定制的，系统提供的用户界面，用于拍照和电影支持的设备上，并选择保存的图像和电影用于您的应用程序.一个图像选择控制器管理用户交互,并通过delegate对象传递交互的结果.

它的交互外观由 source type 决定：

### UIImagePickerControllerSourceTypeCamera 类型提供一个获得新照片或电影的交互界面（设备必须支持媒体捕获）

UIImagePickerControllerSourceTypePhotoLibrary 或 UIImagePickerControllerSourceTypeSavedPhotosAlbum 这两个类型提供选择已保存在设备上的照片或电影

![Taking Pictures and Movies](https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/CameraAndPhotoLib_TopicsForIOS/Art/UIImagePickerController.jpg)

参考： [Camera Programming Topics for iOS](https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/CameraAndPhotoLib_TopicsForIOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010400)

### [UIVideoEditorController](https://developer.apple.com/library/ios/documentation/uikit/reference/UIVideoEditorController_ClassReference/Reference/Reference.html)

一个UIVideoEditorController对象，或视频编辑器，管理系统提供的视频微调界面，从视频的开头到结尾帧都可以进行管理。并可以重新调整编码质量。这个对象管理用户交互，并提供电影编辑的文件路径给到你的delegate 对象 ([UIVideoEditorControllerDelegate Protocol Reference](https://developer.apple.com/library/ios/documentation/uikit/reference/UIVideoEditorControllerDelegate_ProtocolReference/Reference/Reference.html#//apple_ref/doc/uid/TP40009028))。这个特性只支持有视频录制的设备上。

### [UIScreen](https://developer.apple.com/library/ios/documentation/uikit/reference/UIScreen_Class/Reference/UIScreen.html)

UIScreen对象包含设备的整个屏幕矩形边界。设置应用程序的用户界面时, 你应该使用该对象的推荐矩形属性设置应用程序的窗口。

你可以通过它，获得整个设备的屏幕的宽，高。

通常我们在didFinishLaunchingWithOptions: 方法里经常看到这样一句话：

`self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];`

### [UIScreenMode](https://developer.apple.com/library/ios/documentation/uikit/reference/UIScreenMode_class/Reference/Reference.html)

UIScreenMode对象代表一组可以应用于一个UIScreen对象的属性.对象封装的信息是屏幕的基本显示缓冲区的大小和长宽比，及它使用的独立像素。

开发者可以不必关心这个属性，屏幕和窗口对象会根据底层硬件自动确定像素和长宽比等。不过你要是想用到像素级信息，可以考虑用它来设置。

### [UISearchDisplayController](https://developer.apple.com/library/ios/documentation/uikit/reference/UISearchDisplayController_Class/Reference/Reference.html)

一个搜索控制器用于管理搜索状态栏和伴随搜索结果的视图表。

你初始化一个带搜索栏的搜索显示控制器和视图控制器负责管理数据搜索.当用户开始搜索时， 搜索控制器添加搜索界面在原视图控制器上，当搜索后，显示一个搜索结果的表视图显示搜索结果。

当你添加这个搜索控制器，并添加和管理搜索结果时，你需要遵守以下四点:

1. 为搜索结果表视图提供数据源(searchResultsDataSource).
2. 为搜索结果表视图提供代理(searchResultsDelegate).响应用户在结果表中的选择。
3. 为搜索控制器提供代理(delegate).响应用户在搜索栏上的输入操作。
4. 为搜索栏(UISearchBar)上提供代理(delegate UISearchBarDelegate), 响应搜索条件的改变。

典型的初始化实例方法如下：

```

searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
            searchController.delegate = self;
            searchController.searchResultsDataSource = self;
            searchController.searchResultsDelegate = self;
```

### [UITextChecker](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITextChecker_Class/Reference/Reference.html)

使用UITextChecker类的实例来检查一个字符串(通常是文档的文本)拼错的单词。

### [UITextInputStringTokenizer](https://developer.apple.com/library/ios/documentation/uikit/reference/UITextInputStringTokenizer_Class/Reference/Reference.html)

这个类实现了UIKit 框架里的UITextInputTokenizer 协议。实现分词效果.

PS: 没有用过

### [UITextPosition](https://developer.apple.com/library/ios/documentation/uikit/reference/UITextPosition_Class/Reference/Reference.html)

这个类表示在一个文本内容的位置。换句话说，它是一个支持字符串在text-displaying视图的索引.

UITextView 类的实例方法 positionFromPosition:offset: 返回的就是这样的对象

### [UITextRange](https://developer.apple.com/library/ios/documentation/uikit/reference/UITextRange_Class/Reference/Reference.html)

这个类表示在一个文本内容中的区域。换句话说，它表示从哪儿开始到哪儿结束的索引。

UITextView 类的实例方法 selectedRange 返回的就是这样的对象 \[textView selectedTextRange];

### [UITouch](https://developer.apple.com/library/ios/documentation/uikit/reference/UITouch_Class/Reference/Reference.html)

UITouch对象表示手指在屏幕上点击或移动的特定事件.你可以通过UIEvent传递过来的UITouch对象访问到。

UITouch对象包括访问的方法,和在特定的视图或窗口触摸的位置.它还允许您找到接触发生时,用户是否不止一次点击,是否是滑动.从哪儿开始，哪儿结束等等。

gestureRecognizers 属性在 iOS 3.2 后被引入.返回的是手势识别器类。

![Discrete and continuous gestures](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Art/discrete_vs_continuous_2x.png)


# iOS 13 兼容性适配检查

### 可能出现问题的关键字&#x20;

复制下面的关键字在 Xcode 中正则搜索

```
(_UINavigationBarContentView|forKeyPath|_placeholderLabel|boolForKey|stringForKey|deviceToken|CNCopyCurrentNetworkInfo|presentViewController|UISearchDisplayController|UIWebView|MPMoviePlayerController|@available|_cancelButtonText|_searchField)
```

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-LxurEdLBcoWcT5FjIDY%2F-Lxus2asvloJEWq_APgp%2Fimage.png?alt=media\&token=7a1234b5-3ac2-49c6-a48b-3a02ecb297f3)

#### **\_cancelButtonText**:

crash: \[searchBar setValue:@”取消” forKey:@”\_cancelButtonText”];

#### **\_UINavigationBarContentView**

```
for (UIView *subview in self.subviews) {
if ([NSStringFromClass([subview class]) containsString:@”_UINavigationBarContentView”]) {
subview.layoutMargins = UIEdgeInsetsZero;
break;
}
}
```

这种做法在 iOS 13 中会导致崩溃，崩溃信息如下\
\*\*\* Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Client error attempting to change layout margins of a private view’

**解决方案**\
使用设置 frame 的方式，让 \_UINavigationBarContentView 向两边伸展，从而抵消两边的边距。

#### **\_placeholderLabel**

\[\_textField setValue:\[UIColor redColor] forKeyPath:@”\_placeholderLabel.textColor”];///崩溃\
\[\_textField setValue:\[UIFont systemFontOfSize:14] forKeyPath:@”\_placeholderLabel.font”];///崩溃**解决方案**\
\_textField.attributedPlaceholder = \[\[NSAttributedString alloc] initWithString:@”姓名” attributes:@{NSFontAttributeName:\[UIFont systemFontOfSize:14],NSForegroundColorAttributeName:\[UIColor redColor]}];在Xcode10上编译不会有问题，但在Xcode11上编译的会崩溃。并且- (void)setValue:(nullable id)value forKey:(NSString \*)key方法没问题，- (void)setValue:(nullable id)value forKeyPath:(NSString \*)keyPath会崩溃

#### **boolForKey**

第三方 NSDictionary 扩展重名\
打印日志：objc\[5337]: REPLACED: -\[NSDictionary boolForKey:]  by category HMFoundation  (IMP was 0x104ae8280 (/var/containers/Bundle/Application/B1486820-C2CC-4339-9D41-DBF1AC77EBF2/iHome4iPhone.app/iHome4iPhone), now 0x186d95a8c (/System/Library/PrivateFrameworks/HMFoundation.framework/HMFoundation))**解决方案**\
Category 换名字

#### **stringForKey**

stringForKey 返回的是默认值是@“”并非nil.\
if(\[dict stringForKey:@”key”]){} 会发生逻辑错误**解决方案**\
\[dict stringForKey:@”key”].length > 0

#### **Category**

见 \<Category 私有函数>

#### **deviceToken**

```
– (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSString *token = [deviceToken description];
for (NSString *symbol in @[@” “, @”<“, @”>”, @”-“]) {
token = [token stringByReplacingOccurrencesOfString:symbol withString:@””];
}
}
```

在 iOS 13 中，这种方法已经失效，NSData类型的 deviceToken 转换成的字符串变成了：\
{length = 32, bytes = 0xd7f9fe34 69be14d1 fa51be22 329ac80d … 5ad13017 b8ad0736 }

**解决方案**

`#include <arpa/inet.h>`\
`– (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {`\
`if (![deviceToken isKindOfClass:[NSData class]]) return;`\
`const unsigned *tokenBytes = [deviceToken bytes];`\
`NSString *hexToken = [NSString stringWithFormat:@”%08x%08x%08x%08x%08x%08x%08x%08x”,`\
`ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),`\
`ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),`\
`ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];`\
`NSLog(@”deviceToken:%@”, hexToken);`\
`}`<br>

#### **presentViewController**

苹果将 UIViewController 的 modalPresentationStyle 属性的默认值改成了新加的一个枚举值  UIModalPresentationAutomatic，对于多数 UIViewController，此值会映射成 UIModalPresentationPageSheet。\
这种效果弹出来的页面导航栏部分是会被砍掉的，在 storyboard 中也可以看到，页面布局时需要注意导航栏的内容不要被遮挡。注意，我们原来以全屏的样式弹出一个页面，那么将这个页面弹出的那个 ViewController 会依次调用 viewWillDisappear 和 viewDidDisappear。然后在这个页面被 dismiss 的时候，将他弹出的那个 ViewController 的 viewWillAppear 和 viewDidAppear 会被依次调用。然而使用默认的视差效果弹出页面，将他弹出的那个 ViewController 并不会调用这些方法，原先写在这四个函数中的代码以后都有可能会存在问题。**解决方案：**&#x5982;果视差效果的样式可以接受的话，就不需要修改；如果需要改回全屏显示的界面，需要手动设置弹出样式：

```
– (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}
```

或 `self.modalPresentationStyle = .fullScreen`

#### **CNCopyCurrentNetworkInfo**

CNCopyCurrentNetworkInfo 文档说明，应用还需要符合下列三项条件中的至少一项才能得到正确的值：使用 Core Location 的应用， 并获得定位服务权限。\
使用 NEHotspotConfiguration 来配置 WiFi 网络的应用。\
目前正处于启用状态的 VPN 应用。解决方案:\
获取 wift信息还要先获得位置授权

#### **UISearchDisplayController**

在 iOS 8 之前，我们在 UITableView 上添加搜索框需要使用 UISearchBar + UISearchDisplayController 的组合方式，而在 iOS 8 之后，苹果就已经推出了 UISearchController 来代替这个组合方式。在 iOS 13 中，如果还继续使用 UISearchDisplayController 会直接导致崩溃，崩溃信息如下：\
\*\*\* Terminating app due to uncaught exception ‘NSGenericException’, reason: ‘UISearchDisplayController is no longer supported when linking against this version of iOS. Please migrate your application to UISearchController.’

**解决方案:**\
使用 UISearchController 替换 UISearchBar + UISearchDisplayController 的组合方案。

#### **UIWebView**

UIWebView 将被禁止提交审核\
如果开发者将包含 UIWebView api 的应用更新上传到 App Store 审核后，其将会收到包含 ITMS-90809 信息的回复邮件**解决方案**\
用 WKWebView 替代 UIWebView，确保所有 UIWebView 的 api 都要移除，如果需要适配 iOS 7 的可以通过 openURL 的方式在 Safari 打开。

#### **MPMoviePlayerController**

在 iOS 9 之前播放视频可以使用 MediaPlayer.framework 中的MPMoviePlayerController类来完成，它支持本地视频和网络视频播放。但是在 iOS 9 开始被弃用，如果在 iOS 13 中继续使用的话会直接抛出异常：\*\*\* Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.’

**解决方案**

使用 AVFoundation 里的 AVPlayer 作为视频播放控件。

#### **@available**

使用 @available 导致旧版本 Xcode 编译出错\
在 Xcode 11 的 SDK 工程的代码里面使用了 @available 判断当前系统版本，打出来的包放在 Xcode 10 中编译，会出现一下错误：Undefine symbols for architecture i386:\
“\_\_isPlatformVersionAtLeast”, referenced from:\
…\
ld: symbol(s) not found for architecture i386复制代码从错误信息来看，是 \_\_isPlatformVersionAtLeast 方法没有具体的实现，但是工程里根本没有这个方法。实际测试无论在哪里使用@available ，并使用 Xcode 11 打包成动态库或静态库，把打包的库添加到 Xcode 10 中编译都会出现这个错误，因此可以判断是 iOS 13 的 @available 的实现中使用了新的 api。\
\
**解决方案**\
如果你的 SDK 需要适配旧版本的 Xcode，那么需要避开此方法，通过获取系统版本来进行判断：\
if (\[UIDevice currentDevice].systemVersion.floatValue >= 13.0) {\
…\
}另外，在 Xcode 10 上打开 SDK 工程也应该可以正常编译，这就需要加上编译宏进行处理：

\#ifndef \_\_IPHONE\_13\_0\
\#define \_\_IPHONE\_13\_0 130000\
\#endif

\#if \_\_IPHONE\_OS\_VERSION\_MAX\_ALLOWED >= \_\_IPHONE\_13\_0\
…\
\#endif

#### **\_searchField**

UITextField \*textField = \[searchBar valueForKey:@”\_searchField”]; // Crash

**解决方案**

// 替代方案 1，使用 iOS 13 的新属性 searchTextField\
searchBar.searchTextField.placeholder = @”search”;

#### **\_cancelButtonText**

\[searchBar setValue:@”取消” forKey:@”\_cancelButtonText”]; // Crash

**解决方案**\
// 替代方案，用同上的方法找到子类中 UIButton 类型的属性，然后设置其标题\
`UIButton *cancelButton = [self findViewWithClassName:NSStringFromClass([UIButton class]) inView:searchBar];`\
`[cancelButton setTitle:@”取消” forState:UIControlStateNormal];`

#### Category 私有函数

| 类              | Category函数               | 系统Category仓库           |
| -------------- | ------------------------ | ---------------------- |
| NSObject       | safeValueForKey          | AccessibilityUtilities |
| NSDictionary   | boolForKey               | HMFoundation           |
| NSDictionary   | stringForKey             | HMFoundation           |
| NSDictionary   | arrayForKey              | HMFoundation           |
| NSDictionary   | dictionaryForKey         | HMFoundation           |
| NSDictionary   | dataForKey               | HMFoundation           |
| NSDictionary   | numberForKey             | HMFoundation           |
| NSDictionary   | mutableDictionaryForKey  | HMFoundation           |
| NSDictionary   | mutableArrayForKey       | HMFoundation           |
| NSString       | unsignedLongLongValue    | HearingUtilities       |
| NSString       | unsignedLongLongValue    | HearingUtilities       |
| NSString       | stdStringForString       | libwebrtc              |
| NSString       | stringForStdString       | libwebrtc              |
| NSString       | unsignedIntValue         | IMFoundation           |
| NSString       | stdString                | GeoServices            |
| NSString       | containsString           | Foundation             |
| NSString       | unsignedIntValue         | IMFoundation           |
| NSString       | hexValue                 | IMFoundation           |
| NSString       | hasPrefixCaseInsensitive | CalendarFoundation     |
| NSString       | urlEncodedString         | Social                 |
| NSMutableArray | nonRetainingArray        | IMFoundation           |

#### 关键字搜索

```
(safeValueForKey|boolForKey|stringForKey|arrayForKey|dictionaryForKey|dataForKey|numberForKey|mutableDictionaryForKey|mutableArrayForKey|unsignedLongLongValue|stdStringForString|stringForStdString|unsignedIntValue|stdString|hexValue|hasPrefixCaseInsensitive|urlEncodedString|nonRetainingArray)
```


# iOS 上的蓝牙框架 Bluetooth

所须环境: iOS 6 以上

原文: [Core Bluetooth for iOS 6](http://weblog.invasivecode.com/post/39707371281/core-bluetooth-for-ios-6-core-bluetooth-was)

Core Bluetooth 是在iOS5首次引入的，它允许iOS设备可以使用健康，运动，安全，自动化，娱乐，附近等外设数据。在iOS 6 中，这个API被扩展了，让iOS也能成为数据提供方，也就是`Server(Peripheral)`端，可能使它与其它 iOS 设备交互数据。

Core Bluetooth API 基于BLE4.0规范。这个框架涵盖了BLE标准的所有细节. 不过，仅仅只有新的iOS设备和MAC是兼容BLE标准的: iPhone 4S, iPhone5, Mac Mini, New iPad, MacBook Air, MacBook Pro. 并且 iOS 6 iPhone 模拟器也支持一样的标准.这对你在没有真机时，开发APP时是非常实用的。

### 相关的类

在CoreBluetooth框架中，有两个主要的角色：外设和中心（Peripheral and Central） ，整个框架都是围绕这两个主要角色设计的，它们之间有一系列的回调交换数据。 下图1展示了外设和中心（`Peripheral and Central`）的关系。

![Fig1](http://www.doruby.com/wp-content/uploads/2016/06/core_bluetooth_client_server_naming.png)

外设创建或提供一些数据，中心使用这些设备提供的数据。在iOS6之后，iOS 设备也可以即是外设，也可以是中心，但不能在同时间扮演两个角色。

这两个组件在CoreBluetooth框架中是分别用两个类来表示的，中央是`CBCentralManager`类，外设是`CBPeripheralManager`类。

在中心，一个 `CBPeripheral` 对象表示正在连接中的外设,同样在外设里，一个 `CBCentral` 表示正在连接中的中心.

你可以理解外设是一个广播数据的设备，它开始告诉外面的世界说它这儿有一些数据，并且能提供一些服务。另一边中心开始扫描外面有没有 自己所需要的服务，如果发现后，会和外设做连接请求，一旦连接确定后，两个设备就可以传输数据了。

除了中心与外设，我们还得考虑他们用于交互的数据结构，这些数据在Services(服务)中被结构化,每个服务由不同的Characteristics(特性)所组成。特性定义为一种属性类型，并且对应一个逻辑值(比如0x2A49)。

你可以在[developer bluetooth](http://developer.bluetooth.org)这里找到标准服务与特性的列表。

比如：

## Services:

```
    SpecificationName: Blood Pressure
    SpecificationType: org.bluetooth.service.blood_pressure
    AssignedNumber: 0x1810
    SpecificationLevel: Adopted

    #Characteristics:

    SpecificationName: Blood Pressure Feature
    SpecificationType: org.bluetooth.characteristic.blood_pressure_feature
    AssignedNumber: 0x2A49
    SpecificationLevel: Adopted
```

在中心里，服务由 `CBService` 类表示，每个服务由代表特性的 `CBCharacteristic`类所构成。

同样，在外设中服务与特性由 `CBMutableService` 与 `CBMutableCharacteristicclass` 类表示。

下图解释了他们之间的关系:

![Fig2](http://www.doruby.com/wp-content/uploads/2016/06/objects_involved_in_core_bluetooth.png)

`CBUUID` 和 `CBATTRequest` 是两个苹果提供给我们的帮助类，以便于开发者更简单地操作数据，稍后你将看到如何使用它们。

### 使用

不幸的是，Apple提供的文档目前还不完整，你只有通过WWDC上两个关于 Core Bluetooth的视频和头文件，去理解这个框架是如何工作的。不过，因为我之前已经做过相关方面的事情，我决定和你分享这些内容，我希望下面的教程可以帮助到你。你也可以通过 <http://training.invasivecode.com> 查看我们的培训课程.

**创建外设 (Peripheral)**

为了可以创建一个完整的例子，你需要两台iOS设备，我将向你展示如何通过蓝牙连接这两个设备，并交换数据。记住先检查一下你的设备是不是被BLE所支持的。

开始创建一个外设需要下面几步：

1.创建并且开始Peripheral Manager

2.设置并且发布它的服务。

3.广播这个服务。

4.和中心连接。

用Single-View Application模板创建一个新的Xcode工程。命名为BlueServer （使用ARC）。工程创建完成后，添加CoreBluetooth.framework 框架。然后打开ViewController.h文件，并且添加以下代码：

`#import <CoreBluetooth/CoreBluetooth.h>`

使view controller 遵循 `CBPeripheralManagerDelegate` 协议，然后添加这个属性：

`@property (nonatomic, strong) CBPeripheralManager *manager;`

在ViewController.m中，添加以下代码到viewDidLoad方法中：

`self.manager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];`

这行代码初始化了一个 Peripheral Manager (计划中的第一项). 第一个参数是设置delegate(这里的例子就是view controller)。第二参数(队列)设置为了nil,因为Peripheral Manager 将运行在主线程中。如果你想用同步的线程做更复杂的事情，你需要单独创建一个队列并把它放在这个参数中。

一旦Peripheral Manager被初始化后，我们需要及时检查正在运行的App设备状态，是不是符合BLE标准的。所以你要实现下面的这个代理方法（如果设备不支持BLE,你可以友好地提醒用户。你还可以通过拿到的状态值做更多事情）。

```
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
    switch (peripheral.state) {
        case CBPeripheralManagerStatePoweredOn:
            [self setupService];
            break;
        default:
            NSLog(@"Peripheral Manager did change state");
            break;
    }
}
```

在这里，我检查了外设的状态，如果它的状态是`CBPeripheralManagerStatePoweredOn`，那这个设备是支持BLE并可以继续执行。

外设的状态包括有下面这些

```
typedef enum {
   CBPeripheralManagerStateUnknown = 0,
   CBPeripheralManagerStateResetting,
   CBPeripheralManagerStateUnsupported,
   CBPeripheralManagerStateUnauthorized,
   CBPeripheralManagerStatePoweredOff,
   CBPeripheralManagerStatePoweredOn,
} CBPeripheralManagerState;
```

### 服务与特性 (Service & Characteristic)

setupService 是一个辅助方法，我们让它去准备服务和特性，对于这个例子，我们仅仅需要一个服务和一个特性。

每一个服务和特性必要有一个UUID来标识，UUID是一个16位或128位的值。如果你创建的是一个 client-server(中央-外设)应用，那么你需要创建属于你自己的128位UUID，你必须确保它不能和其他已经存在的服务冲突，如果你要创建一个新的设备，你需要去符合标准委员会的UUID。

如果你创建的是你自己的client-server(正如我们现在做的)，我建议你在Terminal下用 uuidgen 命令创建128位的UUID. 所以打开 Terminal并创建两个（一个为服务，一个为特性）.之后，你需要将他们添加到中心和外设应用。这里我们先添加下面几行在 view controller中。

```
    
static NSString * const kServiceUUID = @"6BC6543C-2398-4E4A-AF28-E4E0BF58D6BC";
static NSString * const kCharacteristicUUID = @"9D69C18C-186C-45EA-A7DA-6ED7500E9C97";
```

注意：这里的UUID每个人生成的都不一样，最好是你自己生成

这里是 setupService 的实现方法:

```
- (void)setupService {
    // Creates the characteristic UUID
    CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID];

    // Creates the characteristic
    self.customCharacteristic = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];

    // Creates the service UUID
    CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];

    // Creates the service and adds the characteristic to it
    self.customService = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];

    // Sets the characteristics for this service
    [self.customService setCharacteristics:@[self.customCharacteristic]];

    // Publishes the service
    [self.peripheralManager addService:self.customService];
} 
```

首先，我使用`+UUIDWithString:`方法创建了一个 UUID 对象，之后我用这个 UUID对象创建了特性。注意，我在初始化时，第三个参数传的是nil (那个value)，之所以这样做，是因为我告诉 Core Bluetooth我将稍候添加这个特性值，当你需要动态创建数据时，经常这么做。如果你已经有一个静态的值，你可以直接传它。

在这个方法中，第一个参数是先创建好的UUID,第二个参数(那个 properties)确定你将如何使用这个特性值，下面是这些可能的值：

```
    CBCharacteristicPropertyBroadcast: 允许一个广播特性值,用于描述特性配置，不允许本地特性
    CBCharacteristicPropertyRead: 允许读一个特性值
    CBCharacteristicPropertyWriteWithoutResponse: 允许写一个特性值，没有反馈
    CBCharacteristicPropertyWrite: 允许写一个特性值
    CBCharacteristicPropertyNotify: 允许通知一个特性值，没有反馈
    CBCharacteristicPropertyIndicate: 允许标识一个特性值
    CBCharacteristicPropertyAuthenticatedSignedWrites: 允许签名一个可写的特性值
    CBCharacteristicPropertyExtendedProperties: 如果设置后，附加特性属性为一个扩展的属性说明，不允许本地特性
    CBCharacteristicPropertyNotifyEncryptionRequired: 如果设置后，仅允许信任的设备可以打开通知特性值
    CBCharacteristicPropertyIndicateEncryptionRequired: 如果设置后，仅允许信任的设备可以打开标识特性值
```

最后一个参数是属性的读、写、加密权限，有以下几种：

```
    CBAttributePermissionsReadable
    CBAttributePermissionsWriteable
    CBAttributePermissionsReadEncryptionRequired
    CBAttributePermissionsWriteEncryptionRequired
```

创建特性后，我同样通过`+UUIDWithString:`方法创建 UUID，然后通过它创建了服务。 最后我为服务设置对应的这个特性。记住，每个服务可以包括多个特性，正如下面的图3

![Fig3](http://www.doruby.com/wp-content/uploads/2016/06/services_and_characteristics.png)

所以我们需要通过一个特性数组来添加到服务中，在这个例子里，这个数组对象只有一个特性。

最后一行的代码是将服务添加到 Peripheral Manager中，用于发布这个服务。一旦这样做之后，Peripheral Manager 将会通知它的 delegate调用`peripheralManager:didAddService:error:`方法。这里如果没有错误，你可以开始广播这个服务。

```
- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error {
    if (error == nil) {
        // Starts advertising the service
        [self.peripheralManager startAdvertising:@{ CBAdvertisementDataLocalNameKey : @"ICServer", CBAdvertisementDataServiceUUIDsKey : @[[CBUUID UUIDWithString:kServiceUUID]] }];
    }
}
```

当Peripheral Manager开始广播这个服务时，delegate 会接收到 `peripheralManagerDidStartAdvertising:error:` 消息。当中心订阅 了这个服务时，它的delegate会收到 `peripheralManager:central:didSubscribeToCharacteristic:`消息，这儿你可以生成动态数据给中心。

现在，发送数据给中心你需要预先写一些数据的代码，然后发送`updateValue:forCharacteristic:onSubscribedCentrals:`到外设。

### 创建一个中心 (Central)

现在，我们已经有了一个外设，让我们创建中心(client)。记住，中心是用来处理外设提供的数据的。如上面的图2所示，这里的中心被 `CBCentralManager` 对象表示。

让我们创建一个名字为 BlueClient 的 Xcode 项目，使用ARC,并添加 CoreBluetooth.framework ，在 view controller 头添加

```
#import <CoreBluetooth/CoreBluetooth.h>
```

在中心，你必须遵循两个协议: CBCentralManagerDelegate 和 CBPeripheralDelegate

```
@interface ViewController : UIViewController <CBCentralManagerDelegate, CBPeripheralDelegate>
```

并添加两个属性:

```
@property (nonatomic, strong) CBCentralManager *manager;
@property (nonatomic, strong) NSMutableData *data;
```

现在正如我们之前为外设创建的做法一样，我们创建中心对象: self.manager = \[\[CBCentralManager alloc] initWithDelegate:self queue:nil];

同样，这里的第一个参数表示 CBCentralManager delegate (这里是 view controller). 第二个参数和之前一样也表示调度队列，如果设置为空，他会使用主队列。

一旦 Central Manager 初始化后，我们同样也要检查它的状态，是不是被 BLE 所支持的APP，实现下面的delegate 方法:

```
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    switch (central.state) {
        case CBCentralManagerStatePoweredOn:
            // Scans for any peripheral
            [self.manager scanForPeripheralsWithServices:@[ [CBUUID UUIDWithString:kServiceUUID] ] options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
            break;
        default:
            NSLog(@"Central Manager did change state");
            break;
    }
}
```

这个`scanForPeripheralsWithServices:options:` 方法用于告诉 Central Manager 开始查看特别的服务，如果你第一个参数用的是nil，这个Central Manager 开始查看所有服务。

这个 kServiceUUID 和创建外设中的 ServiceUUID 一样。所以我们再次添加下面2行代码在你的实现类中。

```
static NSString * const kServiceUUID = @"6BC6543C-2398-4E4A-AF28-E4E0BF58D6BC";
static NSString * const kCharacteristicUUID = @"9D69C18C-186C-45EA-A7DA-6ED7500E9C97";
```

一旦一个外设在扫描时被发现后，中心 delegate 会收到下面的回调：

* (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral* )peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber* )RSSI&#x20;

这个调用通知Central Manager delegate（在这个例子中就是view controller），一个附带着广播数据和信号质量(RSSI-Received Signal Strength Indicator)的周边被发现。这是一个很酷的参数，知道了信号质量，你可以用它去估计中心与外设的距离。

任何广播或扫描的响应数据保存在advertisementData 中，可以通过CBAdvertisementData key来访问它。现在，你可以停止扫描，去连接外设了：

```
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
    // Stops scanning for peripheral
    [self.manager stopScan];

    if (self.peripheral != peripheral) {
        self.peripheral = peripheral;
        NSLog(@"Connecting to peripheral %@", peripheral);
        // Connects to the discovered peripheral
        [self.manager connectPeripheral:peripheral options:nil];
    }
}
```

options 参数是一个可选的字典(NSDictionary)，如果需要，可以用以下的键(keys), 它们的值始终是一个boolean。

```
CBConnectPeripheralOptionNotifyOnConnectionKey: 这是一个NSNumber(Boolean)，表示系统会为获得的外设显示一个提示，当成功连接后这个应用被挂起，这对于没有运行在中心后台模式并不显示他们自己的提示时是有用的。如果有更多的外设连接后都会发送通知，如果附近的外设运行在前台则会收到这个提示。
CBConnectPeripheralOptionNotifyOnDisconnectionKey:  这是一个NSNumber(Boolean), 表示系统会为获得的外设显示一个关闭提示，如果这个时候关闭了连接，这个应用会挂起。
CBConnectPeripheralOptionNotifyOnNotificationKey: 这是一个NSNumber(Boolean)，表示系统会为获得的外设收到通知后显示一个提示，这个时候应用是被挂起的。
```

基于连接的结果，delegate会接收

\`

* centralManager:didFailToConnectPeripheral:error:

  \`

或者 \`

* centralManager:didConnectPeripheral:

  \`

  中的一个。如果成功了，你可以询问正在广播服务的那个外设。因此，在didConnectPeripheral 回调中，你可以写以下代码：

```
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    // Clears the data that we may already have
    [self.data setLength:0];
    // Sets the peripheral delegate
    [self.peripheral setDelegate:self];
    // Asks the peripheral to discover the service
    [self.peripheral discoverServices:@[ [CBUUID UUIDWithString:kServiceUUID] ]];
}
```

现在，外设开始用一串回调通知它的delegate。在上面一个方法中，我请求外设去寻找服务，外设代理收到 -peripheral:didDiscoverServices: 如果没有错误，外设可以去查找服务所提供特性，你可以这样做。

```
- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error {
    if (error) {
        NSLog(@"Error discovering service: %@", [error localizedDescription]);
        [self cleanup];
        return;
    }

    for (CBService *service in aPeripheral.services) {
        NSLog(@"Service found with UUID: %@", service.UUID);

        // Discovers the characteristics for a given service
        if ([service.UUID isEqual:[CBUUID UUIDWithString:kServiceUUID]]) {
            [self.peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:kCharacteristicUUID]] forService:service];
        }
    }
}
```

现在，如果一个特性被发现，外设delegate 又会接收

```
peripheral:didDiscoverCharacteristicsForService:error:
```

```
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    if (error) {
        NSLog(@"Error discovering characteristic: %@", [error localizedDescription]);
        [self cleanup];
        return;
    }
    if ([service.UUID isEqual:[CBUUID UUIDWithString:kServiceUUID]]) {
        for (CBCharacteristic *characteristic in service.characteristics) {
            if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID]]) {
                [peripheral setNotifyValue:YES forCharacteristic:characteristic];
            }
        }
    }
}
```

一旦特征的值用`setNotifyValue:forCharacteristic:` 更新后，外设就会通知它的delegate。

外设的 delegate 就会接收到 peripheral:didUpdateNotificationStateForCharacteristic:error:

这里，你可以用 `readValueForCharacteristic:` 读到新的值

```
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"Error changing notification state: %@", error.localizedDescription);
    }

    // Exits if it's not the transfer characteristic
    if (![characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID]]) {
        return;
    }

    // Notification has started
    if (characteristic.isNotifying) {
        NSLog(@"Notification began on %@", characteristic);
        [peripheral readValueForCharacteristic:characteristic];
    } else { // Notification has stopped
        // so disconnect from the peripheral
        NSLog(@"Notification stopped on %@.  Disconnecting", characteristic);
        [self.manager cancelPeripheralConnection:self.peripheral];
    }
}
```

当外设发送新的值时，外设的 delegate 会收到 `peripheral:didUpdateValueForCharacteristic:error:`，这个方法的第二个参数包含特性，你可以用 `-value` 属性来读它，这是一个包含了特性值的NSData。

这个时候，你可以为其它数据断开或等待。

### 总结

我为你展示了如何使用 Core Bluetooth 框架的基本示例，我希望通过这个教程，加上WWDC视频，有用的一些文档能帮助你创建一个 BLE 项目，同时你也可以去参考一些文档示例，那你会发现我这教程中所有的 delegate 方法。


# Performance


# App 启动优化之一：main.m 方法之前的优化

说到APP的启动优化，网上应该也有不少经验总结，不过就我所之相对有些零散，缺乏一些系统性的梳理 。为了让我们有一个完整的认识，也为了能和大家一起学习提高，这里我先抛砖引玉，引入第一篇文章从头梳理APP启动的各个环节，希望对大家有所帮助。

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5Vv02PLQ42HBXUpNXU%2Fimage.png?alt=media\&token=fd5fbfae-436d-4e8d-88a7-61b00abe6fe1)

我们首先要谈的是在开启一个 APP 的生命周期前，也就是进入 `main.m()` 方法之前，系统要经历怎样的一个加载过程呢？

如果从这里开始分析，就不得不谈到 iOS 操作系统内核的工作流程，它是如何解析我们的二进制包，如何分配内存，然后找到入口，进入 **main()** 方法中的呢？在这个过程中我们还有没有可优化的空间，或者要注意的事呢？

### 关于iOS系统加载APP过程

1. App启动流程的关键节点

   ![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VuwOiIMJp7G87fj-H%2Fimage.png?alt=media\&token=b16b74eb-886e-40d4-ae36-5574e94f46fa)

   引用自：`Jamin's Blog`
2. 可执行文件的内核流程如下图

   ![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5Vv6qeaPAYJufTSM8z%2Fimage.png?alt=media\&token=29ce8a7a-7227-448f-8067-8a76d998040a)

   引用自《Mac OS X and iOS Internals : To the Apple's Core》
3. 对应到源代码里：

   由于源代码较多，只引用关键性的代码.

```
// /bsd/kern/ker_exec.c
execve(proc_t p, struct execve_args *uap, int32_t *retval) 
{
    __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)
    {
        exec_activate_image(struct image_params *imgp)
        {
            for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {
                // 这里只分析Mach-O这种执行格式，所以exec_fat_imgact和exec_shell_imgact最终都会调到exec_mach_imgact                
                error = (*execsw[i].ex_imgact)(imgp); 

               (*execsw[i].ex_imgact)(imgp) = exec_mach_imgact(imgp)
                exec_mach_imgact(struct image_params *imgp)
                {

                    load_machfile(struct image_params *imgp, ...) 
                    {                        

                        // 设置内存映射，关键点1
                        if (create_map) {
                            vm_map_create();
                        }                                                      
                        // 解析 Mach-O 包核心，关键点2
                           parse_machfile(struct vnode *vp, ..., load_result_t *result)
                           {
                               ...
                          }

                    }

                    ...

                    // 设置入口点，关键点3
                    /* Set the entry point */
                    thread_setentrypoint(thread, load_result.entry_point);                                                      
                    ...
                }

            }
        }
    }

}
```

* 关键点1： vm\_map\_create() 设置内存映射，这和你的APP分配的初始内存大小有关
* 关键点2： 加载解析Mach-O文件，并得到APP入口,重点在这个函数: `parse_machfile()`，下面会细聊到。
* 关键点3：进入入口点（也就是main()函数的入口地址）

如想更深入研究，请下载[源代码](http://opensource.apple.com/tarballs/xnu/xnu-2782.1.97.tar.gz)

#### 关于 `parse_machfile()` 函数做了什么:

* `parse_machfile()` 是递归解析的，最初的递归深度为0，最高深度到6，防止无限递归。使用递归解析，主要是将不同Mach-O文件类型按照依赖关系，分前后进行解析。如解析可执行二进制文件类型(MH\_EXECUTABLE)的Mach-O文件需要调用load\_dylinker来处理加载命令LC\_LOAD\_DYLINKER，而动态链接器也是Mach-O文件，所以就需要递归到不同的深度进行解析；
* 其次，`parse_machfile()` 的每一次递归，在解析加载命令时，会将内核需要解析的加载命令按照加载循序划分为三组进行解析，在代码的体现上就是通过三次循环，每趟循环只关注当前趟需要解析的命令： (1)：解析线程状态，UUID和代码签名。相关命令为LC\_UNIXTHREAD、LC\_MAIN、LC\_UUID、LC\_CODE\_SIGNATURE (2)：解析代码段Segment。相关命令为LC\_SEGMENT、LC\_SEGMENT\_64； (3)：解析动态链接库、加密信息。相关命令为：LC\_ENCRYPTION\_INFO、LC\_ENCRYPTION\_INFO\_64、LC\_LOAD\_DYLINKER
* 解析完可执行二进制文件类型的Mach-O文件(假设为A)之后，我们会得到A的入口点；但线程并不立刻进入到这个入口点。这是由于我们还会加载动态链接器(dyld)，在`load_dylinker()`中，dyld会保存A的入口点，递归调用`parse_machfile()`之后，将线程的入口点设为dyld的入口点；动态链接器dyld完成加载库的工作之后，再将入口点设回A的入口点，程序启动完成；

### 关于要解析的 APP 包

我们最终上线后的 APP是一组文件所组成，Apple 称它们为 **Bundle**

```
xxx.app/
Documents/
Library/
tmp/
...
```

**xxx.app** 就是我们的 app 应用程序，主要包含了执行文件，和一堆资源文件（图片，xib文件）等。其中的执行文件是 Mach-O(Mach-Object) 格式的二进制文件。

Mach-O文件可能有多种架构组成，因为不同的iOS设备对应的CPU架构也是不同的，所以为了得到最优的执行效率，这个二进制包会有多个不同的架构所组成的包，比如 armv7, armv7s。

Mach-O 文件是一个独有的二进制可执行文件格式。虽然iOS/OS X采用了类UNIX的Darwin操作系统核心，完全符合UNIX标准系统，但在执行文件上，却没有支持UNIX的[ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format)。<br>

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VvMKD6dyCKyXYA3rN%2Fimage.png?alt=media\&token=5a21729d-e5cf-4150-bb8f-eebae583e3cb)

### 深入分析 Load Commends

这些加载命令在Mach-O文件加载解析时，被内核加载器或者动态链接器调用，指导如何设置加载对应的二进制数据段。

接下来我们来对比一下，一个最简单的APP（XCode模板创建的Signle View Application）与之后又多添加了几个动态链库的不同，观察一下Load Commends：<br>

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VvSIkcuG8AmwjqEwz%2Fimage.png?alt=media\&token=79ab69ac-4a83-41f6-b399-792e1c317b48)

可以看到即使最简单的APP，也链接了一个 APP 所必需的动态链接库(UIKit等)。如果我们在XCode 里这样引入了更多的动态链接库，像这样：

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VvWNqmMtSRbDA-78H%2Fimage.png?alt=media\&token=0faab3ef-03c6-441b-aaff-8b514d150fcb)

就会多出这几项的链接库。

*(ps: 一些黑客就是利用这一点来嵌入自己的动态链接库以达到修改APP的目的)*

**LC\_MAIN** 对应的就是APP的入口。

### 总结可能存在的优化点

有了上述的分析我们可以总结一下可能存在的优化点。

1. **减少动态链接库**

   内核在到达入口之前，会先装载所有的动态链接库。所以尽可能用静态链接库。
2. **减少不必要的 ObjectC 对象**

   OC创建的类，会在DATA SEGMENT 中占用较多的 VM Size，即使你创建了一个空的Class。因为本身这个对象就会留下一空较大的内存区域来创建与管理，虽然现在的内存分配已经够快，但也会有一定的开销。
3. **特别注意 load() 方法里的内容**

   在 main 入口被解析执行到之前，所有的OC实现类都会先执行`load()`方法。如果你有定制这个方法，特别是在方法里有太多外部依赖时，也会额外增加解析的时间。所以如果有特别耗时的操作在这里请谨慎!

#### 参考资料

* [Jamin's Blog](http://oncenote.com/2015/06/01/How-App-Launch/)
* [Mac OS iOS Internals Apples](http://www.amazon.com/Mac-OS-iOS-Internals-Apples/dp/1118057651)

以上观点仅代表个人，由于水平有限，可能有遗漏和不对的地方，欢迎一起来纠正，讨论！


# Debug


# 在Xcode里,为项目定义不同的环境配置

有时项目经常需要配置不同的开发环境，比如Debug, QA, Release, Distribution等。每个环境下，会有各自不同的环境配置项，比如变量，常量，宏定义等。

我们最早的方法是先在Build Settings里先设定 Preprocessor Macros CONFIGURATION\_$(CONFIGURATION)

这样我们就可以在代码根据该 Macro 来区分现在所处的环境。通过我们是定义在 Prefix.pch 文件里:

```

#ifdef CONFIGURATION_Debug
# import "ConfigDebug.h"
#else
# ...
#endif
```

现在想到的更好的方法是直接修改Prefix Header的引用路径

在 Project 里的 Build Settings 里设定 Prefix Header 文件的导入位置.

![settingsPrefixHeader](http://blog.ydio.com/wp-content/uploads/2016/06/settingsPrefixHeader.png)

比如: Test/Config/Test-Prefix-${CONFIGURATION}.pch

我们新建了一个Config目录，然后在里面添加了 Test-Prefix-Debug.pch , Test-Prefix-Release.pch 等不同的环境配置文件.

Test-Prefix-Debug.pch

```

import "ConfigDebug.h"
...
```

&#x20;Test-Prefix-Release.pch

```
import "ConfigRelease.h"
...
```

&#x20;  关于各项环境配置应该放在哪儿比较好? 我们有时会定义一些自己的Macro, 通常我们也是象上面一样写在Preprocessor Macros 里。 实际上更好的方法是建立不同的 .xcconfig 文件, 然后将Target的配置指向它们 比如我们建立一个 Debug.xcconfig, Release.xcconfig 将 PROJECT Info => Configurations => target Based on Configuration File 指向不同环境下的config文件。 ![targetConfigurations](http://blog.ydio.com/wp-content/uploads/2016/06/targetConfigurations.png) 我们就可以在这个文件上定义自己的Macro了，比如 Debug.xcconfig

```
TestMacroDef = 1
```

Release.xcconfig

```
TestMacroDef = 0
```

你可以在Build Settings中看到User-Defined一栏多了你自定义的Macro

我们还可以改变或添加系统默认的定义，比如上面在 Preprocessor Macros里定义的内容，可以这样写

```
GCC_PREPROCESSOR_DEFINITIONS = kShareKey=1 $(inherited) // inherited 是继承原有的定义
```

tips: 你可以选中settings的一栏copy， paste 到这个文件中，即可知道定义方法。

如果我们有一些公共的配置文件，可以创建一个 公共的 xcconfig 文件，然后 include 进来即可。

比如

`Debug.xcconfig`

```
#include "Shared.xcconfig"
```


# 调试我们的 APP (iOS)

很早就想写一篇关于调试和测试的文章了。一直没写的原因一方面是工作太忙，一方面是现在新的调试工具和调试方法层出不穷，不知道什么时候现在用的方法就会过时，所以一直犹豫是不是等到有一个系统的方案出来在总结一下，不过我觉得还是现在先把能想到和用到写下来，让大家一起探讨。

**注：以下部分英文原文因本人翻译水平有限，不能给出准确的中文释义，所以将引用的原文进行了保留。**

关于如何来调试我们的APP，其实是可以从不同的维度来划分，比如应用调试，测试，错误分析等。我们先从最基本的调试工具入手一步步介绍。

### XCode Debug Tools

**Debugger**

首先 Xcode 默认设置了你所用的 Debugger

![Debugger Setting](http://www.doruby.com/wp-content/uploads/2016/06/setting_debugger.png)

通常我们这里用默认的[LLDB](http://lldb.llvm.org/), XCode5之后默认就是用 LLDB了，XCode5以前还有 GDB。

使用LLDB之后，我们通常是在代码执行的地方，打上断点，如：

![Set BreakPoint](http://www.doruby.com/wp-content/uploads/2016/06/set_breakpoint.png)

这样程序运行遇到断点后会停下等待，这时我们可以在控制台，使用一些调试命令输出我们所需要的对象信息:

![Control NSLog](http://www.doruby.com/wp-content/uploads/2016/06/control_po.png)

还有很多命令:

```

[self.view recursiveDescription]  # 递归打印view
image lookup --address 0xffffff
```

更多调试命令可以参考上面的 LLDB 介绍，或用 help 命令显示帮助。

> 需要注意的是当Optimization Level不为None时，断点的定位会有差异，所以建议在Target 为 Debug 时调试更为准确，如果一定要在Release模式下调试，可以手动设置为Optimization Level 为 None

如果想看全部的信息，可以点这里选择:

![ALL Variables](http://www.doruby.com/wp-content/uploads/2016/06/all_variables.png)

* **Global Symbolic BreakPoints**

除了上面的指定断点设置，还可以设置Global Point, 作用域在当前用户的所有project，添加的Breakpoint 的方式有好几种，而且还可以以不同的方式呈现，比如日志输入，语音提醒等，可以在Action参数中设置。

BreakPoint 其中有几种设定类型，其中有：

1. Symbolic Breakpoint

   我们可以添加指定的方法为断点。比如添加一个 viewDidLoad Symbol，会在运行到所有的viewDidLoad方法时停下. 如果你要添加某个特定类的实例方法，可以用 -\[类名 实例方法名]。类方法是 +\[类名 方法名]

   ![Symbolic Breakpoint](http://www.doruby.com/wp-content/uploads/2016/06/symbolic_breakpoint.png)

   如果你不知道这个方法格式应该如何书写，可以在你想要打断点的方法里先做断点，然后查看左边的Show the Debugger Navigator, 里的 Thread 指向的方法名:

   ![Show The Debugger Navigator](http://www.doruby.com/wp-content/uploads/2016/06/show_the_debugger_navigator.png)

   看到 0 后面的方法调用了吗？
2. Exception Breakpoint

   另一个非常有用的断点设定,

   在开发中除了用断点调试我们的应用分析问题外，还有一种情况是，我们向被释放的对象发送了消息，导致的crash (EXC\_BAD\_ACCESS)。

   关于Zombie:

   在 Cocoa 中，zombies 是一种即使生命终止了也会到惹麻烦的对象。我们可以做的是启动一个编译设置，使对象的引用计数降为0的时候不被释放，而是将它们转化为NSZombie对象。这个类的目的是记录任何对它的实例的调用，因为这意味着代码企图用一个已经消亡的对象调用方法。

   通常我们的做法是通过设定 Exception Breakpoint 来统一排错，其中也包括了些类情况, 所以这个断点设定的方法来检查类似情况变得非常实用。

   ![Exception Breakpoint Menu](http://www.doruby.com/wp-content/uploads/2016/06/exception_breakpoint_menu.png)

   如果添加这个类型的 Breakpoint , 好多隐藏很深的 Bug 都会被发现, 类似于 @try {} @catch {} 的 catch 部分

   ![Exceptions Breakpoint](http://www.doruby.com/wp-content/uploads/2016/06/exceptions_breakpoint.png)
3. 如果使用GDB和更早版本的XCode的用户可以用下面的方式来检查：

   编辑Scheme, 将Diagnostics中的 Enable Zombie Objects 与 Malloc Stack 勾选上

   并选中 Enable Zombie Objects 与 Malloc Stack：

   当程序出现类似的问题而crash时，我们就可以找到被释放地址的真正原因

   ![Enable Zombie Objects](http://www.doruby.com/wp-content/uploads/2016/06/enable_zombie_objects.png)

* **通过 Instrument 调试应用**

XCode 另外还自带了一个非常强大的APP调试工具： Instrument

关于 Instrument 的介绍，大家可以参考： [Instrument Introduction](https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/Introduction/Introduction.html)

![Instruments intro](http://www.doruby.com/wp-content/uploads/2016/06/Instruments_intro_ss.png)

*如果你在Schema Profile 中可以直接设置 Instrument 指定的分析工具，这样运行 Profile 时就可以直接启动它了*

Instrument 包括的小工具有好几种，我们这里先介绍几个:

1. Leaks 内存泄露分析工具 ![Instruments leaks](http://www.doruby.com/wp-content/uploads/2016/06/Instruments_leaks.png) [Leak Tutorial](http://www.mobileorchard.com/find-iphone-memory-leaks-a-leaks-tool-tutorial/)
2. Zombiles

* Analyze 分析代码

试一试通过选择 Menu => Product => Analyze。检测出可能会出现内存泄露的地方，重复引用，命名冲突等地方

* **自定义警告和错误提示**

我们还可以在代码中加入自定义的警告和错误提示，对于需要特别给某段代码加标注供日后处理时或出错判断时，可以在代码的上方加上

`#warning [message]` 在编译的时候会出现警告

`#error [message]` 在编译的时候会给出现错误提示

### Crash

由于使用Objective－c 和 c ，直接执行二进制指令，自己管理内存，会出现访问错误内存的情况出现。这时，系统会直接把你的进程干掉，iOS会给你生成一个Crash Log

* 关于crash时，如果显示的是堆栈信息，如何正确定位到程序部分

```
First throw call stack:
(0x2f3a022 0x30cbcd6 0x2ee2a48 0x2ee29b9 0x2f392da 0x9cfd3 0x7f460 0x80a6e 0x103ba29 0x2f05855 0x2f05778 ...)
```

如何查看这些信息背后的真实情况，在main.m代码中加入以写部分：

```

#ifdef DEBUGvoid eHandler(NSException*);

void eHandler(NSException*exception){    
  NSLog(@"%@", exception);    
  NSLog(@"%@",[exception callStackSymbols]);
}

#endif


int main(int argc,char*argv[]){
  #ifdef DEBUG    
  NSSetUncaughtExceptionHandler(&eHandler);
  #endif
  ...rest of your main function here...
}
```

* 理解与分析 Crash Report

  在APP上线后，对 Crash Report 的监控是最为重要的环节了。itunes connect应用管理后台提供了部分的 Crash Reports，你可以在管理后台下载 .crash 文件，然后通过这个文件查找是哪儿引起的crash。

  但是这个文件中没有平时调试时候那样可以看到的函数名和函数具体调用行数。因为这里的这些信息都被转换成了16进制的地址，起到了一定的加密作用，别人拿到你的crash日志也不知道哪里崩溃了，需要利用你编译app的时候生成的dSYM文件然后将这些信息反转为可读模式。所以保留好你Archive后的dSYM文件是很有用的！

  如果你能找到 dSYM 文件，就可以利用symbolicatecrash工具查找具体的Bug发生地点了。

  `symbolicatecrash [CrashLog file] [dSYM file]`

*tips*

> 查找 symbolicatecrash 文件位置 find /Applications/Xcode.app -name symbolicatecrash

* 设定 `export DEVELOPER_DIR=/Application/Xcode.app/Contents/Developer/`
* 查看你的应用 uuid 与 dSYM的 uuid 是否能对应:

  dwarfdump --uuid yourapp.app/yourapp dwarfdump --uuid yourapp.app.dSYM

  * 搜索含有正确 uuid 的文件

    `mdfind "com_apple_xcode_dsym_uuids == 5255A87A-B23C-3AE8-B367-14B49C21C1D6"`

### 分析

日志主要由几部分组成，其中:

* **Exception Type**
  * EXC\_BAD\_ACCESS (SIGSEGV)

    这个类型的Exception的意思是，你没有权限访问你所要访问的内存。一般都是由于访问了已经被release的object导致的，或者把一个object release了两次。甚至当你访问超出数组长度的内容时，也有可能出现这种类型的错误。它的意思应该是段错误。这个SIGSEGV不是objective－c的excption，而是更底层的C部分的信号。
  * EXC\_CRASH (SIGKILL)或者(SIGABRT)

    这个类型的Exception比较特别，你需要认真查看后面所有Thread的BackTrace才能找到最终原因，因为有时候它所写的Crash Thread并不是真正引起崩溃的原因，在其中你也找不到什么有用的信息。(SIGABRT)一般是由于系统捕获到一个异常，然后把你的应用终结掉了，你可以在下面的栈信息中寻找有abort信息的那一个thread，能找到真正的原因。(SIGKILL)目前还没在自己的App中遇到过。
* **Exception codes**

  In the crash log is a line that starts with the text Exception Codes: followed by one or more hexadecimal values. These are processor-specific codes that may give you more information on the nature of the crash.

1. The exception code **0xbaaaaaad** indicates that the log is a stackshot of the entire system, not a crash report. To take a stackshot, push the home button and any volume button. Often these logs are accidentally created by users, and do not indicate an error.
2. The exception code **0xbad22222** indicates that a VoIP application has been terminated by iOS because it resumed too frequently.
3. The exception code **0x8badf00d** indicates that an application has been terminated by iOS because a watchdog timeout occurred. The application took too long to launch, terminate, or respond to system events. One common cause of this is doing synchronous networking on the main thread. Whatever operation is on Thread 0: needs to be moved to a background thread, or processed differently, so that it does not block the main thread.
4. The exception code **0xc00010ff** indicates the app was killed by the operating system in response to a thermal event. This may be due to an issue with the particular device that this crash occurred on, or the environment it was operated in. For tips on making your app run more efficiently, see iOS Performance and Power Optimization with InstrumentsWWDC session.
5. The exception code **0xdead10cc** indicates that an application has been terminated by iOS because it held on to a system resource (like the address book database) while running in the background.
6. The exception code **0xdeadfa11** indicated that an application has been force quit by the user. Force quits occur when the user first holds down the On/Off button until "slide to power off" appears, then holds down the Home button. It's reasonable to assume that the user has done this because the application has become unresponsive, but it's not guaranteed - force quit will work on any application.

* **一个单步分析 Crash Report 的方法**

  Steps to analyze crash report from apple:

  Copy the release .app file which was pushed to the appstore, the .dSYM file that was created at the time of release and the crash report receive from APPLE into a FOLDER.

  OPEN terminal application and go to the folder created above (using CD command)

  `atos -arch armv7 -o YOURAPP.app/YOURAPP MEMORY_LOCATION_OF_CRASH.` The memory location should be the one at which the app crashed as per the report.

  Ex: `atos -arch armv7 -o 'app name.app'/'app name' 0x0003b508`

  This would show you the exact line, method name which resulted in crash.

  Ex: \[classname functionName:]; -510

  Symbolicating IPA

  if we use IPA for symbolicating - just rename the extention .ipa with .zip , extract it then we can get a Payload Folder which contain app. In this case we don't need .dSYM file.

或用 dwarfdump 命令也行

`dwarfdump –lookup 0x000036d2 –arch armv7 YOURAPP.app.dSYM`

参考文章:

* [Understanding and Analyzing iOS Application Crash Reports](https://developer.apple.com/library/ios/technotes/tn2151/_index.html)
* [理解Crash Log](http://www.whoslab.me/blog/?p=608)
* [KERN\_INVALID\_ADDRESS 与 KERN\_PROTECTION\_FAILURE 的区别](http://stackoverflow.com/questions/1282428/whats-the-difference-between-kern-invalid-address-and-kern-protection-failure)

一些第三方分析工具:

* [UMeng 日志监控](http://www.umeng.com/)
* [反汇编工具Hopper分析Crash Log](http://www.hopperapp.com/)
* [Crashlytics](http://blog.devtang.com/blog/2013/07/24/use-crashlytics/)

### 几种 Debug 输出方法

在Apple Tech Note TN2239：[iOS Debugging Magic](http://developer.apple.com/library/ios/#technotes/tn2010/tn2239.html) 中提到了程序开发中Debug output 方法：

```

NSLog
stderr
system log
```

调试信息的输出主要有方式，一是通过输出到终端输出，二是输出到日志系统。下面讲介绍一下这几种输出调试信息的方式，首先从stderr说起。

* stderr （引用自TN2239）：

  Many programs, and indeed many system frameworks, print debugging messages to stderr. The destination for this output is ultimately controlled by the program: it can redirect stderr to whatever destination it chooses. However, in most cases a program does not redirect stderr, so the output goes to the default destination inherited by the program from its launch environment. This is typically one of the following:

  If you launch a GUI application as it would be launched by a normal user, the system redirects any messages printed on stderr to the system log. You can view these messages using the techniques described earlier. If you run a program from within Xcode, you can see its stderr output in Xcode’s debugger Console window (choose the Console menu item from the Run menu to see this window).

  Attaching to a running program (using Xcode’s Attach to Process menu, or the attach command in GDB) does not automatically connect the program’s stderr to your GDB window. You can do this from within GDB using the trick described in the “Seeing stdout and stderr After Attaching” section of Technical Note TN2030, ‘GDB for MacsBug Veterans’.

  这样一段代码在真机上运行：

```

    NSLog(@"This is message from NSLog");
    fprintf(stderr, "This is message from stderr\n");
```

如果是通过Xcode调试加载运行这个程序，那么

在xcode的console中打印如下：

````

  2011-03-12 18:52:26.948 Test86[7891:307] This is message from NSLog```

  This is message from stderr
````

在iPhone的system log中（通过Organizer的console查看）只打印

```
Sat Mar 12 18:52:26 unknown Test86[7891] : This is message from NSLog`
```

但是如果在iPhone上通过手指触摸启动这个程序，在iPhone的system log中会打印：

```

  Sat Mar 12 18:53:38 unknown Test86[7900] : This is message from NSLog
  Sat Mar 12 18:53:38 unknown UIKitApplication:com.yourcompany.Test86[0x7d60][7900] : This is message from stderr
```

说明确实stderr在user 自己launch的app中被重定向为system log，而且log的等级为Notice；NSLog的等级为Warning。

* system log

  其实system log是unix系统都有采用syslog协议的一个日志系统（RFC详细讲解了这种协议<http://tools.ietf.org/html/rfc5424）。每条日志是有等级的，主要分为如下等级：>

```

    Level 0 – “Emergency”
    Level 1 – “Alert”
    Level 2 – “Critical”
    Level 3 – “Error”
    Level 4 – “Warning”
    Level 5 – “Notice”
    Level 6 – “Info”
    Level 7 – “Debug”
```

在创建好日志之后，通过调用API发送日志信息给一个叫做syslogd的守护进程，然后syslogd根据自己的配置文件（位于/private/etc/syslog.conf, mac系统的在：/etc/asl.conf )

在mac os和ios那么怎样调用API将日志发送给系统日志呢？有两种API：

* [syslog API](https://developer.apple.com/library/ios/#documentation/System/Conceptual/ManPages_iPhoneOS/man3/syslog.3.html#//apple_ref/doc/man/3/syslog) - *不要和之前syslog协议混淆*
* [ASL: Apple System Log facility](https://developer.apple.com/library/ios/#documentation/System/Conceptual/ManPages_iPhoneOS/man3/asl.3.html) - *是苹果自己实现的一种可以同syslogd服务器交互，用来替换syslog API的实现*

这里还有一些讲Syslog不错的文章：

* [Accesing the iOS system log](http://www.cocoanetics.com/2011/03/accessing-the-ios-system-log/)
* [Why ASL?](http://boredzo.org/blog/archives/2008-01-20/why-asl)
* [PonyDebugger: Remote Debugging Tools for Native iOS Apps](http://corner.squareup.com/2012/08/ponydebugger-remote-debugging.html) - *远程调试*
* Charles, Mitmproxy(免费) - *使用网络代理调试API Request*

**查看iOS 设备上的APP数据**

![iOS Device Data](http://www.doruby.com/wp-content/uploads/2016/06/iOS_device_data.png)

**注：以下部分英文原文因本人翻译水平有限，不能给出准确的中文释义，所以将引用的原文进行了保留。**

关于如何来调试我们的APP，其实是可以从不同的维度来划分，比如应用调试，测试，错误分析等。我们先从最基本的调试工具入手一步步介绍。

### XCode Debug Tools

**Debugger**

首先 Xcode 默认设置了你所用的 Debugger

通常我们这里用默认的[LLDB](http://lldb.llvm.org/), XCode5之后默认就是用 LLDB了，XCode5以前还有 GDB。

使用LLDB之后，我们通常是在代码执行的地方，打上断点，如：

这样程序运行遇到断点后会停下等待，这时我们可以在控制台，使用一些调试命令输出我们所需要的对象信息:

还有很多命令:

```

[self.view recursiveDescription]  # 递归打印view
image lookup --address 0xffffff
```

更多调试命令可以参考上面的 LLDB 介绍，或用 help 命令显示帮助。

> 需要注意的是当Optimization Level不为None时，断点的定位会有差异，所以建议在Target 为 Debug 时调试更为准确，如果一定要在Release模式下调试，可以手动设置为Optimization Level 为 None

如果想看全部的信息，可以点这里选择:

* **Global Symbolic BreakPoints**

除了上面的指定断点设置，还可以设置Global Point, 作用域在当前用户的所有project，添加的Breakpoint 的方式有好几种，而且还可以以不同的方式呈现，比如日志输入，语音提醒等，可以在Action参数中设置。

BreakPoint 其中有几种设定类型，其中有：

1. Symbolic Breakpoint

   我们可以添加指定的方法为断点。比如添加一个 viewDidLoad Symbol，会在运行到所有的viewDidLoad方法时停下. 如果你要添加某个特定类的实例方法，可以用 -\[类名 实例方法名]。类方法是 +\[类名 方法名]

   如果你不知道这个方法格式应该如何书写，可以在你想要打断点的方法里先做断点，然后查看左边的Show the Debugger Navigator, 里的 Thread 指向的方法名:

   看到 0 后面的方法调用了吗？

2. Exception Breakpoint

   另一个非常有用的断点设定,

   在开发中除了用断点调试我们的应用分析问题外，还有一种情况是，我们向被释放的对象发送了消息，导致的crash (EXC\_BAD\_ACCESS)。

   关于Zombie:

   在 Cocoa 中，zombies 是一种即使生命终止了也会到惹麻烦的对象。我们可以做的是启动一个编译设置，使对象的引用计数降为0的时候不被释放，而是将它们转化为NSZombie对象。这个类的目的是记录任何对它的实例的调用，因为这意味着代码企图用一个已经消亡的对象调用方法。

   通常我们的做法是通过设定 Exception Breakpoint 来统一排错，其中也包括了些类情况, 所以这个断点设定的方法来检查类似情况变得非常实用。

   如果添加这个类型的 Breakpoint , 好多隐藏很深的 Bug 都会被发现, 类似于 @try {} @catch {} 的 catch 部分

3. 如果使用GDB和更早版本的XCode的用户可以用下面的方式来检查：

   编辑Scheme, 将Diagnostics中的 Enable Zombie Objects 与 Malloc Stack 勾选上

   并选中 Enable Zombie Objects 与 Malloc Stack：

   当程序出现类似的问题而crash时，我们就可以找到被释放地址的真正原因

* **通过 Instrument 调试应用**

XCode 另外还自带了一个非常强大的APP调试工具： Instrument

关于 Instrument 的介绍，大家可以参考： [Instrument Introduction](https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/Introduction/Introduction.html)

*如果你在Schema Profile 中可以直接设置 Instrument 指定的分析工具，这样运行 Profile 时就可以直接启动它了*

Instrument 包括的小工具有好几种，我们这里先介绍几个:

1. Leaks 内存泄露分析工具  [Leak Tutorial](http://www.mobileorchard.com/find-iphone-memory-leaks-a-leaks-tool-tutorial/)
2. Zombiles

* Analyze 分析代码

试一试通过选择 Menu => Product => Analyze。检测出可能会出现内存泄露的地方，重复引用，命名冲突等地方

* **自定义警告和错误提示**

我们还可以在代码中加入自定义的警告和错误提示，对于需要特别给某段代码加标注供日后处理时或出错判断时，可以在代码的上方加上

`#warning [message]` 在编译的时候会出现警告

`#error [message]` 在编译的时候会给出现错误提示

### Crash

由于使用Objective－c 和 c ，直接执行二进制指令，自己管理内存，会出现访问错误内存的情况出现。这时，系统会直接把你的进程干掉，iOS会给你生成一个Crash Log

* 关于crash时，如果显示的是堆栈信息，如何正确定位到程序部分

```
First throw call stack:
(0x2f3a022 0x30cbcd6 0x2ee2a48 0x2ee29b9 0x2f392da 0x9cfd3 0x7f460 0x80a6e 0x103ba29 0x2f05855 0x2f05778 ...)
```

如何查看这些信息背后的真实情况，在main.m代码中加入以写部分：

```

#ifdef DEBUGvoid eHandler(NSException*);

void eHandler(NSException*exception){    
  NSLog(@"%@", exception);    
  NSLog(@"%@",[exception callStackSymbols]);
}

#endif


int main(int argc,char*argv[]){
  #ifdef DEBUG    
  NSSetUncaughtExceptionHandler(&eHandler);
  #endif
  ...rest of your main function here...
}
```

* 理解与分析 Crash Report

  在APP上线后，对 Crash Report 的监控是最为重要的环节了。itunes connect应用管理后台提供了部分的 Crash Reports，你可以在管理后台下载 .crash 文件，然后通过这个文件查找是哪儿引起的crash。

  但是这个文件中没有平时调试时候那样可以看到的函数名和函数具体调用行数。因为这里的这些信息都被转换成了16进制的地址，起到了一定的加密作用，别人拿到你的crash日志也不知道哪里崩溃了，需要利用你编译app的时候生成的dSYM文件然后将这些信息反转为可读模式。所以保留好你Archive后的dSYM文件是很有用的！

  如果你能找到 dSYM 文件，就可以利用symbolicatecrash工具查找具体的Bug发生地点了。

  `symbolicatecrash [CrashLog file] [dSYM file]`

*tips*

> 查找 symbolicatecrash 文件位置 find /Applications/Xcode.app -name symbolicatecrash

* 设定 `export DEVELOPER_DIR=/Application/Xcode.app/Contents/Developer/`
* 查看你的应用 uuid 与 dSYM的 uuid 是否能对应:

  dwarfdump --uuid yourapp.app/yourapp dwarfdump --uuid yourapp.app.dSYM

  * 搜索含有正确 uuid 的文件

    `mdfind "com_apple_xcode_dsym_uuids == 5255A87A-B23C-3AE8-B367-14B49C21C1D6"`

### 分析

日志主要由几部分组成，其中:

* **Exception Type**
  * EXC\_BAD\_ACCESS (SIGSEGV)

    这个类型的Exception的意思是，你没有权限访问你所要访问的内存。一般都是由于访问了已经被release的object导致的，或者把一个object release了两次。甚至当你访问超出数组长度的内容时，也有可能出现这种类型的错误。它的意思应该是段错误。这个SIGSEGV不是objective－c的excption，而是更底层的C部分的信号。
  * EXC\_CRASH (SIGKILL)或者(SIGABRT)

    这个类型的Exception比较特别，你需要认真查看后面所有Thread的BackTrace才能找到最终原因，因为有时候它所写的Crash Thread并不是真正引起崩溃的原因，在其中你也找不到什么有用的信息。(SIGABRT)一般是由于系统捕获到一个异常，然后把你的应用终结掉了，你可以在下面的栈信息中寻找有abort信息的那一个thread，能找到真正的原因。(SIGKILL)目前还没在自己的App中遇到过。
* **Exception codes**

  In the crash log is a line that starts with the text Exception Codes: followed by one or more hexadecimal values. These are processor-specific codes that may give you more information on the nature of the crash.

1. The exception code **0xbaaaaaad** indicates that the log is a stackshot of the entire system, not a crash report. To take a stackshot, push the home button and any volume button. Often these logs are accidentally created by users, and do not indicate an error.
2. The exception code **0xbad22222** indicates that a VoIP application has been terminated by iOS because it resumed too frequently.
3. The exception code **0x8badf00d** indicates that an application has been terminated by iOS because a watchdog timeout occurred. The application took too long to launch, terminate, or respond to system events. One common cause of this is doing synchronous networking on the main thread. Whatever operation is on Thread 0: needs to be moved to a background thread, or processed differently, so that it does not block the main thread.
4. The exception code **0xc00010ff** indicates the app was killed by the operating system in response to a thermal event. This may be due to an issue with the particular device that this crash occurred on, or the environment it was operated in. For tips on making your app run more efficiently, see iOS Performance and Power Optimization with InstrumentsWWDC session.
5. The exception code **0xdead10cc** indicates that an application has been terminated by iOS because it held on to a system resource (like the address book database) while running in the background.
6. The exception code **0xdeadfa11** indicated that an application has been force quit by the user. Force quits occur when the user first holds down the On/Off button until "slide to power off" appears, then holds down the Home button. It's reasonable to assume that the user has done this because the application has become unresponsive, but it's not guaranteed - force quit will work on any application.

* **一个单步分析 Crash Report 的方法**

  Steps to analyze crash report from apple:

  Copy the release .app file which was pushed to the appstore, the .dSYM file that was created at the time of release and the crash report receive from APPLE into a FOLDER.

  OPEN terminal application and go to the folder created above (using CD command)

  `atos -arch armv7 -o YOURAPP.app/YOURAPP MEMORY_LOCATION_OF_CRASH.` The memory location should be the one at which the app crashed as per the report.

  Ex: `atos -arch armv7 -o 'app name.app'/'app name' 0x0003b508`

  This would show you the exact line, method name which resulted in crash.

  Ex: \[classname functionName:]; -510

  Symbolicating IPA

  if we use IPA for symbolicating - just rename the extention .ipa with .zip , extract it then we can get a Payload Folder which contain app. In this case we don't need .dSYM file.

或用 dwarfdump 命令也行

`dwarfdump –lookup 0x000036d2 –arch armv7 YOURAPP.app.dSYM`

参考文章:

* [Understanding and Analyzing iOS Application Crash Reports](https://developer.apple.com/library/ios/technotes/tn2151/_index.html)
* [理解Crash Log](http://www.whoslab.me/blog/?p=608)
* [KERN\_INVALID\_ADDRESS 与 KERN\_PROTECTION\_FAILURE 的区别](http://stackoverflow.com/questions/1282428/whats-the-difference-between-kern-invalid-address-and-kern-protection-failure)

一些第三方分析工具:

* [UMeng 日志监控](http://www.umeng.com/)
* [反汇编工具Hopper分析Crash Log](http://www.hopperapp.com/)
* [Crashlytics](http://blog.devtang.com/blog/2013/07/24/use-crashlytics/)

### 几种 Debug 输出方法

在Apple Tech Note TN2239：[iOS Debugging Magic](http://developer.apple.com/library/ios/#technotes/tn2010/tn2239.html) 中提到了程序开发中Debug output 方法：

```

NSLog
stderr
system log
```

调试信息的输出主要有方式，一是通过输出到终端输出，二是输出到日志系统。下面讲介绍一下这几种输出调试信息的方式，首先从stderr说起。

* stderr （引用自TN2239）：

  Many programs, and indeed many system frameworks, print debugging messages to stderr. The destination for this output is ultimately controlled by the program: it can redirect stderr to whatever destination it chooses. However, in most cases a program does not redirect stderr, so the output goes to the default destination inherited by the program from its launch environment. This is typically one of the following:

  If you launch a GUI application as it would be launched by a normal user, the system redirects any messages printed on stderr to the system log. You can view these messages using the techniques described earlier. If you run a program from within Xcode, you can see its stderr output in Xcode’s debugger Console window (choose the Console menu item from the Run menu to see this window).

  Attaching to a running program (using Xcode’s Attach to Process menu, or the attach command in GDB) does not automatically connect the program’s stderr to your GDB window. You can do this from within GDB using the trick described in the “Seeing stdout and stderr After Attaching” section of Technical Note TN2030, ‘GDB for MacsBug Veterans’.

  这样一段代码在真机上运行：

```

    NSLog(@"This is message from NSLog");
    fprintf(stderr, "This is message from stderr\n");
```

如果是通过Xcode调试加载运行这个程序，那么

在xcode的console中打印如下：

````

  2011-03-12 18:52:26.948 Test86[7891:307] This is message from NSLog```

  This is message from stderr
````

在iPhone的system log中（通过Organizer的console查看）只打印

```
Sat Mar 12 18:52:26 unknown Test86[7891] : This is message from NSLog`
```

但是如果在iPhone上通过手指触摸启动这个程序，在iPhone的system log中会打印：

```

  Sat Mar 12 18:53:38 unknown Test86[7900] : This is message from NSLog
  Sat Mar 12 18:53:38 unknown UIKitApplication:com.yourcompany.Test86[0x7d60][7900] : This is message from stderr
```

说明确实stderr在user 自己launch的app中被重定向为system log，而且log的等级为Notice；NSLog的等级为Warning。

* system log

  其实system log是unix系统都有采用syslog协议的一个日志系统（RFC详细讲解了这种协议<http://tools.ietf.org/html/rfc5424）。每条日志是有等级的，主要分为如下等级：>

```

    Level 0 – “Emergency”
    Level 1 – “Alert”
    Level 2 – “Critical”
    Level 3 – “Error”
    Level 4 – “Warning”
    Level 5 – “Notice”
    Level 6 – “Info”
    Level 7 – “Debug”
```

在创建好日志之后，通过调用API发送日志信息给一个叫做syslogd的守护进程，然后syslogd根据自己的配置文件（位于/private/etc/syslog.conf, mac系统的在：/etc/asl.conf )

在mac os和ios那么怎样调用API将日志发送给系统日志呢？有两种API：

* [syslog API](https://developer.apple.com/library/ios/#documentation/System/Conceptual/ManPages_iPhoneOS/man3/syslog.3.html#//apple_ref/doc/man/3/syslog) - *不要和之前syslog协议混淆*
* [ASL: Apple System Log facility](https://developer.apple.com/library/ios/#documentation/System/Conceptual/ManPages_iPhoneOS/man3/asl.3.html) - *是苹果自己实现的一种可以同syslogd服务器交互，用来替换syslog API的实现*

这里还有一些讲Syslog不错的文章：

* [Accesing the iOS system log](http://www.cocoanetics.com/2011/03/accessing-the-ios-system-log/)
* [Why ASL?](http://boredzo.org/blog/archives/2008-01-20/why-asl)
* [PonyDebugger: Remote Debugging Tools for Native iOS Apps](http://corner.squareup.com/2012/08/ponydebugger-remote-debugging.html) - *远程调试*
* Charles, Mitmproxy(免费) - *使用网络代理调试API Request*

**查看iOS 设备上的APP数据**


# URLCache 设置不当引起的 App 故障

关于数据请求是几乎每个 App 会经常遇到的事，关于数据的 Request 与 Response 及 Cache 其实我们需要了解更多细节才能运用自如，排查问题更快。

很久之前，我时常遇到一部分用户能打开页面，一部用户却不能的情况。在排除网络，地域，机型，服务器状态完全正常的情况，一直无法解释其原因。客户也说不上来有什么区别，操作路径也是一样，只是过一段时间就自然恢复了，或重装等等解决。

直到最近的一次故障让我彻底将此类问题的原头分析了一遍，才有所顿悟。

&#x20;故障

现象 服务端因某种原因使所有 API 请求返回错误的，类似源代码的片段内容。几个小时后服务器恢复。不过部分用户使用时看到的仍然是出错页面，而其他用户又是正常。

影响范围 后来仔细分析发现，在服务器宕机的那段时间内，用户如果打开过 App ，则一直会看到出错页面。即使完全退出 App 再进入错误仍旧发生。如果这段时间内没有打开过 App 的用户，在之后打开的话，就会看到正常的页面。所以影响面是所有服务器出错这段时间内，打开过 App 的用户。

&#x20;   协议缓存 NSURLRequestUseProtocolCachePolicy我们引用了官方给出的一张流程图来详细解释这个过程![](https://docs-assets.developer.apple.com/published/d24b133ea3/http_caching_2x_820a949f-9d5d-4a85-9ca2-50b42b339e18.png)从上至下我们可以看一下整个过程

1. 如果缓存内容不存在，不用说，直接发起原始请求了
2. 如果缓存内容存在，确定是否每次从原始资源地址检查。这依赖于 Response Header 里 [cach-control](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) 指定的内容

   常见的值有 private、public、no-store、no-cache、must-revalidate、max-age等。

   **no-cache**: 告诉浏览器、缓存服务器，不管本地副本是否过期，使用资源副本前，一定要到源服务器进行副本有效性校验。

   **must-revalidate**：告诉浏览器、缓存服务器，本地副本过期前，可以使用本地副本；本地副本一旦过期，必须去源服务器进行有效性校验。

本例中 Response Header 没有指定 must-revalidate，所以条件是否。

![Response Header](http://www.doruby.com/wp-content/uploads/2017/03/Screen-Shot-2017-03-06-at-7.34.11-PM.png)

1. 如果结果为是，则发起 HEAD request 请求，查看内容是否有变更，"是"则请求网络数据，如果否就用缓存
2. 如果上面的结果是否，则查看 Response 是否失效 (stale?)

这里的失效算法算是黑盒了，不过还好发现篇文章，描述了如何计算出这个失效期限。

// 首先是计算 freshness\_lifetime

freshness\_lifetime = (date\_value - last\_modified) \* 10% =>

// 例如: (\[Sat, 09 Apr 2016 10:26:51 GMT] - \[Sun, 17 Jan 2016 14:15:33 GMT]) *10% => \[82 days, 19 hours, 11 minutes and 18 seconds]* 10% => 7,153,878 \* 10% = 715388 seconds = 8 days 6 hours 43 minutes 8 seconds

// 然后判断 response 是否失效是根据 freshness lifetime 是否大于 current age response\_isfresh = (freshness\_lifetime > current\_age)

// 这里的 current age 是根据当前时间 now 减去 Response Header 里的 date\_value 来的 current\_age = now - date\_value\</pre> 5. 如果计算的结果是失效的 stale 是 true ，则去发送 issue request 请求。如果是否返回缓存数据\</li>   \</ol> \</li> 解决

避免缓存无效的 Response首先服务端对于错误的返回结果不能返回 200 status code 客户端要做强校验，对于返回的不规范的格式或非业务所需，直接 removeCachedResponse

设置强制清理缓存机制可以由客户手动清理缓存，或由服务端控制清理

区别对待不同的 API 请求 cache 策略客户端与服务端对不同的请求设定不同的缓存策略，比如配置下发那些是强制刷新不缓存的。不经常变的资源可以设置更长的缓存期限

参考资料

[NSURLCache](http://nshipster.cn/nsurlcache/)

[iOS5 Built-In HTTP DiskCache](https://gist.github.com/bogardon/3245415)

[A Primer In Http–caching And Its–native Support–by–ios](http://qnoid.com/2016/04/10/A-primer-in-HTTP-caching-and-its-native-support-by-iOS.html)

[NSURLCache Uses a Disk Cache as of iOS 5](http://petersteinberger.com/blog/2012/nsurlcache-uses-a-disk-cache-as-of-ios5/)

工具

[iOS SDK 原生抓包工具](http://www.appscaffold.net)


# ASDebugger -- 超简单的 App 网络监控调试工具

ASDebugger

不知道大家有没有这样的经历，我们的 App 经常需要和服务端打交道，访问各种 API。往往也不知道客户端到底发送了哪些请求，它们的返回值是什么，效率如何，有没有经为 API 异常导致的问题，等等。

所以以往我们都是用各种抓包工具比如 Charles，或打印 log 来调试，这样有时会被很多无用的信息掩盖，信息也不直观。所以才有了想做一个专门针对 API 调试的工具。

不过这还不够，因为有时因进度问题，客户端必须先 Mock 一些数据过 APP 使用，或者为了测试接口的强壮性，我们必须刻意的去修改 API 返回值，来进行异常数据的测试，所以我就加上了简单的 Mock 功能，主要还是为了方便实用。

**如何使用** 用 CocoaPods 的同学只需引入这个库

pod 'ASDebugger'

\[ASDebugger startWithAppKey:@"AppKey"]; # Appkey 为你在 [AppScaffold](http://www.appscaffold.net) 网站上申请到的 AppKey

也许我们还有很多需求去实现，不过在目前的工作中，是够用了，效率也得到了提升。再也不会为数据问题纠结来纠结去，最常做的事情就是打开网站，把截图丢过去，哈。。。

点击这里开始体验吧 [AppScaffold.net](http://www.appscaffold.net)

Github: <https://github.com/AppScaffold/ASDebugger>

因为人手问题，平台的前后端都是一人完成，所以难免遗漏不少地方。欢迎大家多提建议和意见，我将非常感谢，也欢迎加入开源的队伍中来，一起提高工作效率。


# Tips


# Xcode 自动生成 Objective C SDK 文档

有的时候我们经常需要查看描述项目的API文档，传统的做法是手动写Markdown或Doc文档之类，现在我们可以利用标准的代码注释方法，然后用[appledoc](http://gentlebytes.com/appledoc/)工具来自动生成和苹果官方SDK文档类似的HTMl文档。

### 配置

下载源码并编译:

```

git clone git://github.com/tomaz/appledoc.git 
cd appledoc
```

用它自带的脚本编译

`sudo sh install-appledoc.sh (如果你需要默认HTML模板可以添加 '-t default'， 默认是存放在 ~/.appledoc下的)`

你也可以自定义 bin 目录, 和模板存放地

```
mkdir ~/Library/Application\ Support/appledoc (创建Doc存放目录)
sudo sh install-appledoc.sh -b /usr/bin -t ~/Library/Application\ Support/appledoc
```

### 集成进Xcode5 脚本

1. 在你的Xcode项目中新建Target&#x20;
2. 选择 Other > Aggregate, 建议取名为Documentation
3. 选择 Documentation > Edit Scheme
4. 选择 Build > Post-actions
5. 点+ > New Run Script Action
6. Provide build settings from 选择 Documentation
7. 下面粘贴进脚本

*company 等内容修改为你自己的内容*

*outputPath 为输出html路径*

```
    
    # appledoc Xcode script 
    # Start constants 
    company="SQUARE"; 
    companyID="com.doruby";
    companyURL="http://doruby.com";
    target="iphoneos";
    #target="macosx";
    outputPath="~/help";
    # End constants
    /usr/local/bin/appledoc \
    --project-name "${PROJECT_NAME}" \
    --project-company "${company}" \
    --company-id "${companyID}" \
    --docset-atom-filename "${company}.atom" \
    --docset-feed-url "${companyURL}/${company}/%DOCSETATOMFILENAME" \
    --docset-package-url "${companyURL}/${company}/%DOCSETPACKAGEFILENAME" \
    --docset-fallback-url "${companyURL}/${company}" \
    --output "${outputPath}" \
    --publish-docset \
    --docset-platform-family "${target}" \
    --logformat xcode \
    --keep-intermediate-files \
    --no-repeat-first-par \
    --no-warn-invalid-crossref \
    --exit-threshold 2 \
    "${PROJECT_DIR}"
```

Build 后即可在 `~/help` 文件夹下看到多出了几个html 文件，那就是完整的项目API文档。

## 关于注释的写法

## Appledoc comments formatting style

Appledoc extracts documentation from specially formatted comments, giving you freedom to choose which comments will be parsed and which not. To make porting old documentation simpler, it allows you to use either doxygen or headerdoc style comments.

### Multiple line comments:

Appledoc only handles multiple line comments if they start with slash and double star and end with standard star and slash:

```
/** Some comment */
```

Lines may optionally start with whitespace and a single star char and appledoc will ignore the prefix star, like this:

```
/** Star prefixed comment
 * spreading over multiple 
 * lines
 */
```

To make transition from headerdoc a bit simpler, appledoc also accepts headerdoc style multiline comments:

```
/*! Comment */
```

### Single line comments:

appledoc also handles single line comments that start with three slashes. Two or more single line comments in consequtive lines are grouped together into a single comment:

```
/// Single line comment spreading
/// over multiple lines
```

## Laying out comments

Appledoc has been designed to keep comments in source code as readable as possible. In order to achieve this, it uses [Markdown](http://daringfireball.net/projects/markdown/)-like syntax. Specifically, it uses [Discount](http://www.pell.portland.or.us/~orc/Code/discount/) library for processing Markdown, which supports a set of extensions to original Markdown; check the [link](http://www.pell.portland.or.us/~orc/Code/discount/) for more details. Furthermore, it extracts as much information from the comment surrounding context which allows you to focus on documenting entities, instead of polluting comments with tagging which class or method a comment belongs to.

### Paragraphs

Paragraphs are simply any number of consecutive rows of text separated by one or more empty (or tab and space only) lines:

```
First paragraph.

Second paragraph with lot's of
text spread accross multiple lines.

And another paragraph.
```

The first paragraph of the comment is automatically used as short description and remaining as long description or discussion. You can change default behavior via command line switches.

### Unordered lists

Unordered lists use -, + or \* markers in front of each item:

```
- First item.
- Second item with lot's of text
  spread across multiple lines.
- Third item.
```

The list must be delimited from surrounding paragraphs by empty lines at the top and bottom. You can nest lists by prefixing them with tabs or spaces like this:

```
- Item 1
  - Item 1.1
  - Item 1.2
    - Item 1.2.1
  - Item 1.3
- Item 2
```

Note that you can combine ordered and unordered lists when nesting.

### Ordered lists

Ordered lists use a number followed by a dot as a marker in front of each item:

```
1. First item
2. Second item
```

Numbers don't need to be consequtive, you can even use the same number for all items; appledoc will simply number the items by their order. All rules for unordered lists also apply for ordered lists: separation with empty lines, nesting etc., so it won't be repeated here.

### Examples and code blocks

If you want to mark a block of text as an example, simply prefix each line with a tab or 4 (or more) spaces. Appledoc will automatically convert consequtive blocks of prefixed lines into a single code block. Similar to lists and other paragraph blocks, example section needs to be delimited from preceding and following paragraph with an empty line:

```
Normal paragraph text

    First line of example
    Second line of example

Next paragraph.
```

### Important information blocks

You mark a paragraph as important by prefixing it's first line with @warning directive. For example, this is how you can achieve effect Apple uses for these blocks:

```
@warning *Important:* Sending this message before `readValues:fromFile:` will result in unpredicted results, most likely as runtime exception!
```

*Important*: All text after `@warning` directive, up to next `"@"` directive will become part of the block - currently it's not possible to terminate warning block manually and continue with normal paragraphs! This allows you nesting lists and multiple paragraphs, but may come as surprise when not expected. Therefore it's recommended to use warning blocks at the end of "normal" paragraphs (and as any "@" directive will end previous block or paragraph, you can put them above method directives). Something to keep in mind!

### Bug information blocks

If you'd like to make a paragraph even more emphasized than a `@warning`, you can use `@bug` directive. It works just like `@warning`, so see description there for details. Not so much used, but may come handy under certain circumstances.

## Formatting text

Appledoc has been designed to keep comments in source code as readable as possible. In order to achieve this, it uses [Markdown](http://daringfireball.net/projects/markdown/)-like syntax. Specifically, it uses [Discount](http://www.pell.portland.or.us/~orc/Code/discount/) library for processing Markdown, which supports a set of extensions to original Markdown; check the [link](http://www.pell.portland.or.us/~orc/Code/discount/) for more details. Furthermore, it extracts as much information from the comment surrounding context which allows you to focus on documenting entities, instead of polluting comments with tagging which class or method a comment belongs to.

### Emphasis

Text can be emphasized with the following syntax:

* Text wrapped within stars (\*text\*) becomes strong: *text*.
* Text wrapped within underscodes (\_text\_) becomes emphasized: *text*.

You can include the marker itself in the middle of the text, so this \*some *text\\* is converted to: *some* tex&#x74;*. You can nest strong and emphasized formatting directives, so \_\\*&#x74;ext\*\_ or \*\_text\_\* becomes emphasized strong text like this: *text*.

Note: you can also use standard Markdown formatting: \*\*text\*\* or \_\_text\_\_ for **bold** and \*\*\*text\*\*\* or \_\_\_text\_\_\_ for ***emphasized strong***.

### Code spans

As you're documenting source code, chance is you might want to format text as code. Any text wrapped within backtick quotes (\`text\`) is converted into `code span`. Note that you can't nest emphasis within code spans!

### Headers and the rest

Appledoc supports full Markdown, therefore you can include headers, line breaks, block quotes, horizontal rules etc. However, there's rarely a need for many of these features and using them may result in odd looking documentation. If you need any of these, go experiment and see if you like results or not.

## Links and cross references

You can easily link to any other documented entity or URL. Note that there are command line options that change how links are parsed, examples below assume you're using default settings.

### Links to web pages and other URLs

Any valid URL address starting with `http://`, `https://`, `ftp://`, `file://` or `mailto:` is automatically converted to a link in generated HTML.

### Links to classes, categories and protocols

Any word that matches known class, category or protocol name is automatically converted to cross reference link to that object. Assuming complete documentation contains class GBClass, it's extension GBClass(), category NSError(GBError) and protocol GBProtocol, the following text will automatically convert text:

```
This text contains links to: class GBClass, it's extension GBClass(), 
category NSError(GBError) and protocol GBProtocol.
```

into something like:

This text contains links to: class GBClass, it's extension GBClass(), category NSError(GBError) and protocol GBProtocol.

### Links to local members

Any word that matches method or property selector name of "current" class, category or protocol is automatically converted to cross reference link to that member. Assuming current object contains method runWithValue:afterDelay: and property value, the following text:

```
This text contains links to:
method runWithValue:afterDelay: and
property value.
```

will automatically convert to something like:

This text contains links to: method runWithValue:afterDelay: and property value.

### Links to remote members

Creating cross reference links to members of other classes, categories or protocols requires a bit more effort as you need to state the class and the method, but still follows the same principle. Assuming complete documentation contains class GBClass which has method runWithValue:afterDelay: and property value, the following text:

```
This text contains links to:
method [GBClass runWithValue:afterDelay] and
property [GBClass value]
```

will convert to something like:

This text contains links to: method \[GBClass runWithValue:afterDelay:] and property \[GBClass value]

### Custom link descriptions

As appledoc parses Markdown links for known objects, you can take advantage of link descriptions and even reference-type links to add that fine touch to generated documentation. For example:

```
For more info check [this page](http://gentlebytes.com), 
also take a look at [this class](GBClass) 
and [this method]([GBClass method:]).

For referring to common object multiple times,
use this [class][1]. And [repeat again][1].

[1] GBClass
```

As long as `GBClass` and `[GBClass method:]` are recognized as valid cross references, the above example is converted to something like:

For more info check this page, also take a look at this class and this method.

For referring to common object multiple times, use this class. And repeat again.

## Methods and properties description

For methods and properties you may want to document their parameters, results or exceptions they may raise. To do that, there are a number of "@" directives you can use:

* `@return <description>`: Provides the description of method or property result. Alternatives: `@returns` or `@result`.
* `@param <name> <description>`: Provides the description of method parameter with the given name. You need to provide description for each parameter or appledoc will log a warning (you can suppress these warnings through command line switch).
* `@exception <name> <description>`: Provides the description of an exception that may be raised by a method. The name of the exception is given with the first parameter and description with the second.

*Note*: All of the text following the directive, up to the next directive is considered as part of the directive description. This allows you to include multiple paragraphs, unordered or ordered lists, warnings, bugs and the rest, but may come as surprise if not expected! To compensate, it's recommended to enter all these directives at the bottom of the comment text. In fact, because of this, all directives are formatted so that you enter the description as the last directive "parameter".

## Various bits and pieces

* **Generating related links**: `@see <name>` or `@sa <name>`. Although you can provide cross reference links anywhere within the paragraph text, as described above, you need to use @see directives to provide related context links for documentation sets. The name should follow cross reference guidelines described above.

  **Note**: Methods and properties keep all cross references in generated HTML, regardless of the referenced object. But when `@see` is used within class, category or protocol comment, only cross references to template documents are preserved and converted to companion guide links (generated in the table below the title). All other cross references - i.e. to other objects or members - are ignored. Oh, and remember, you can use nice descriptions using Markdown syntax, for example:

  ```
  @see [String Programming Guide](document1).
  ```
* **Grouping methods**: `@name <title>`. All methods and properties declared after @name directive will be stored into a group with the given title. These groups are then extracted as tasks in generated HTML. Important: @name must be specified in it's own separate comment preceeding the first group method or property comment for which the task is specified! So this would work:

  ```
  /** @name Section title */

  /** Method description */
  - (void)someMethod;
  ```

  and this wouldn't:

  ```
  /** @name Section title

  Method description */
  - (void)someMethod;
  ```
* **Comment delimiters**: Any comment may optionally include delimiter lines. A delimiter line is any combination of 3 or more chars from the following set: `"!@#$%^&*()_=``~,<.>/?;:'\\"-"`. Such lines are ignored, so given a comment like this:

  ```
  /// ---------------------------------
  /// comment
  /// ---------------------------------
  ```

  will automatically strip first and last line. This is mostly used for making `@name` sections more stand out.
* **Directives prefix**: Although all directives in examples above use "@" sign as a prefix, you can actually use any non-whitespace char instead, for example `\param`, `$param`, `%param` and so on...

And last, but not least, as appledoc uses standard Markdown rules, take a look at [Markdown documentation](http://daringfireball.net/projects/markdown/) and [Discount library ](http://www.pell.portland.or.us/~orc/Code/discount/)for more details and possibilities. This doc is only meant as a quick guide to general formatting and all appledoc specifics, not as comprehensive documentation of all supported Markdown features. Also take a look at [appledoc source code](https://github.com/tomaz/appledoc/) for examples of how it fits together.


# 无线分发应用--通过 Safari 安装 App

iOS 支持以无线方式安装企业级应用程序，这可让您在不使用 iTunes 的情况下将内部软件分发给用户。

### 简单几步：

1. 用户需要将设备的[UDID](http://www.innerfence.com/howto/find-iphone-unique-device-identifier-udid)加到 Apple Developer Center 中心的设备里，并更新 .mobileprovision文件
2. Scheme 里将 Archive 的 Build Configuration 换成 Debug 模式
3. Archive 后从 Organizer 找到app文件，生成 ipa 文件
4. 生成 plist 文件，将它与 ipa 文件放到服务器上，并可通过网址访问并可下载
5. 做一个网页供大家访问后点键接跳转下载此plist, 如果生成将网址生成一个二维码，就更方便了。

比如:

[Install App](itms-services://?action=download-manifest\&url=http://example.com/manifest.plist)

### plist 文件模板:

* 注: {}里的内容是要替换的

```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items</key>
    <array>
        <dict>
            <key>assets</key>
            <array>
                <dict>
                    <key>kind</key>
                    <string>software-package</string>
                    <key>url</key>
                    <string>{你的域名http://doruby.com/xxx.ipa}</string>
                </dict>
            </array>
            <key>metadata</key>
            <dict>
                <key>bundle-identifier</key>
                <string>{你的bundle identifier}</string>
                <key>bundle-version</key>
                <string>1.0</string>
                <key>kind</key>
                <string>software</string>
                <key>title</key>
                <string>{App名称}</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>
```

### 网页模板

* 注: {}里的内容是要替换的

```
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=320, height=460, user-scalable=no,
initial-scale=1.0" />
<title>Install Dev App</title>
</head>
<br/><br/><br/>
<body>
<div align="center">
<a href="itms-services://?action=download-manifest&url={http://doruby.com/xxx.plist}"  style="color:orange; font-size:24px">Install the App</a>
</div>
</body>
</html>
```

### 注意:

可能需要配置你的 Web 服务器以便正确地传输清单文件和应用程序文件。

* 对于 OS X Server，将以下 MIME 类型添加到 Web 服务的“MIME Types”（MIME 类型）设置中：

application/octet-stream ipa

text/xml plist

* 对于 IIS，使用 IIS Manager 在服务器的“属性”页面中添加 MIME 类型：

.ipa application/octet-stream

.plist text/xml

### 参考:

[wireless enterprise app distribution](http://help.apple.com/iosdeployment-apps/mac/1.1/#app43ad871e)

[enterprise deployment of ios apps](http://thirteendaysaweek.com/2012/10/01/enterprise-deployment-of-ios-apps-with-monotouch/)

example:

[my app](http://www.doruby.com/assets/food.html)

### 更新 (2014-3)

iOS 7.1修改了manifest.plist文件的访问协议，之前可以通过http协议访问，在iOS 7.1之后必须使用https协议方式访问。

比如之前的链接代码为：

`itms-services://?action=download-manifest&url=http://doruby.com/manifest.plist`

需要修改为：

`itms-services://?action=download-manifest&url=https://doruby.com/manifest.plist`

如果你没有放置或制作 SSL 证书的地方，可以将 plist 文件放到 [Dropbox](https://db.tt/2VPiCZkk) 上，然后得到文件的下载地址： <https://www.dropbox.com/s/s3zl5vzxgvjtwfw/app.plist>, 替换 `www.dropbox.com` 为 `dl.dropboxusercontent.com` 即可。


# 智能的 App Banner - Smart App Banners

Safari 在iOS6设备里有一个智能的App Banner特性，添加一行代码在你的网站里，如果用户在iPhone或iPad等iOS设备上，用Safari浏览器打开网站，他将在顶部看到如下的内容：

![Smart App Banner](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/Art/smartappbanner.png)

这行代码就是：

```
<meta name="apple-itunes-app" content="app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL">
```

app-id 就是你的AppStore上的id 号，affiliate-data 与 app-argument 是可选的两个参数

添加后，他会智能的判断你的手机上是否安装有此APP，如果有则为打开按钮，没有则出现一个AppStore下载链接。

*如果点过关闭显示\[x]后，它将不再显示，除非你去设置中清除Safari Cookies*


# 点阴影区关闭 Modal View Controller

参考：[Close Modal View Controller by tap the shadow region](http://mengxiangping.com/?p=121)

如何实现用户点击阴影区域，将当前出现的 **ModalViewController**消失

![modalViewC](https://www.doruby.com/wp-content/uploads/2016/06/modalViewC.png)

首先了解一下: **UIApplicationMain**

```

int main(int argc, char *argv[]) 
{ 
    @autoreleasepool { 
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 
    } 
}
```

argc 与 argv 是标准的 c main 函数参数。 第三个参数是接收事件响应的主要对象(principalClassName)，如果存在的话必须是继承 UIApplication , 第四个 delegateClassName, 实现 UIApplicationDelegate 中的协议方法.

任何时刻你点击屏幕，principalClassName 都会监听, 并执行 sendEvent 方法, 所以我们只要拦截这个方法，然后做我们想做的事情就可以了。

实现

测试环境：iOS5, iPad

改变 main 的第三个参数对象为 我们自己定义的一个 MyAppplication 类。

main.m

```

int main(int argc, char *argv[]) 
{ 
    @autoreleasepool { 
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 
    } 
}
```

.h file

```

#import 

@interface MyApplication : UIApplication

@end
```

.m file

```

#import "MyApplication.h"

@implementation MyApplication

-(void)sendEvent:(UIEvent *)event{

  [super sendEvent:event];

  // 关键是在这里拿到点击事件后,如果判断点击的是阴影区, 阴影区的View 是一个私有类, 名字叫UIDimmingView, 所以如果响应的点击事件是在这个View上的，我们就可以关闭当前的ModalView
  UITouch* touch = [[[event allTouches] allObjects] lastObject];

  if ([NSStringFromClass([[touch view] class]) isEqualToString:@"UIDimmingView"]) {
      UIViewController * vc = [[[self keyWindow] rootViewController] presentedViewController]; // 找到正在显示的控制器
      [vc dismissModalViewControllerAnimated:YES];
  }
}

@end
```


# 3rd Party


# MKNetworkKit Auto Caching

MKNetworkKit 网上已有不少介绍它的文章了，不过对于它提供的众多特性的实现机制，还是值得研究研究的。其中 Auto caching 是其中之一。

官方文档是这样写的：

### High performance background caching (based on HTTP 1.1 caching specs) built in

MKNetworkKit can automatically cache all your “GET” requests. When you make the same request again, MKNetworkKit calls your completion handler with the cached version of the response (if it’s available) almost immediately. It also makes a call to the remote server again. After the server data is fetched, your completion handler is called again with the new response data. This means, you don’t have to handle caching manually on your side. All you need to do is call one method

`[[MKNetworkEngine sharedEngine] useCache];`

Optionally, you can override methods in your MKNetworkEngine subclass to customize your cache directory and in-memory cache cost.

* 它是基于 [HTTP1.1](http://blog.toright.com/posts/3414/%E5%88%9D%E6%8E%A2-http-1-1-cache-%E6%A9%9F%E5%88%B6) 协议设计的Cache模式，刚好我们请求的后端服务也是基于这个协议，Cache-Control 用的是 `max-age=0, private, must-revalidate` , ETag 标记缓存 &#x20;
* 如果你启用了 useCache ，它可以将所有的GET请求进行Cache，它根据请求的 url 来判断是否是同一个请求，然后调用缓存数据
* 它在返回缓存数据时，同时也向服务器请求最新数据，如果有最新内容，会返回新的 ETag，然后MK会更新缓存中的ETag
* 当 APP 退到后端时，缓存数据会持久化到Caches目录中
* 它生成的缓存数据文件根据 unique Identifier 命名，放在 MKNetworkKitCache 目录中，生成规则可以查看 MKNetworkKitOperation uniqueIdentifier 方法。
* 用 MKNetworkEngine emptyCache 实例方法清理缓存


# 重头梳理网络协议栈

![WWW 30 years](http://upload-images.jianshu.io/upload_images/4957148-37a27ecf8db4e930.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/400)

从 1989 年 HTTP 0.9 发布开始，互联网的发展已经近三十年，各种网络概念也是层出不穷。如 Https, WebSocket, Http 2.0, spdy 等等。

相信大家或多或少能从各种途径，获取到详细的资料。 这里我想通过另一个维度，来看看它们是如何演化而来，为什么这么设计，解决了什么业务场景，以及一些基本的入门实践等。

**我们试图从以下几个问题出发，解释这篇文章的初衷。**

1. 当我们打开浏览器访问一个网页时，它是如何将数据安全的发送给我们的？
2. Http 2.0 为何时隔10年才出现，它解决了什么问题？
3. Socket 和 Http 什么关系？
4. Web socket 又是什么？它是工作在哪一层的？
5. 想要更轻量，更有效率的数据传输如何做？

为了解释以上问题，我们先从网络基础知识开始，这对解释以上问题很重要。

1\. 网络协议

1.1 七层协议 **OSI model**: The Open Systems Interconnection model 一种概念模型，由国际标准化组织（ISO）提出，一个试图使各种计算机在世界范围内互连为网络的标准框架。 通俗的说，为了全世界的计算机之间相互通信，而设计的一种协议框架，所有需要联网的设备或软件都需要遵循它的规则。另外，也为了尽可能避免所有功能混杂在一起，让不同公司或组织更聚焦在一个领域上，才有了现在的分层模型。

&#x20;&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-927e2b11c4492893.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)

我们可以看到在每一层，根据实际应用场景，都有对应的协议被设计出来。

从上面的模型我们可以知道，大的结构上，分为面向上层应用与面向网络媒介两个部分。应用层上主要是面向数据与数据的传输，网络媒介主要是面向网络通信与硬件。

1.2 TCP/IP 基于 OSI 协议模型衍生而来，现今使用最多的就是 TCP/IP 协议栈了。注意这里的 TCP/IP 协议栈并不只是指 TCP 与 IP 协议的合集，它是一种更简化的 OSI 模型。

通过下图可以看到它与 OSI 模型区别:

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-9443e3b0d39cf9bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/651)

从 tcp/ip 协议规范来看，应用层，表示层与传输层被合并在一起。主要原因是这三层，在当今的实际使用中有相当多耦合关系，也很难将它们刻意的分离。

实际上现在的 TCP/IP 模型更加简单明了，更适应现在的应用场景。

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-f400af5049eb7d5c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)

&#x20;除了以上的协议栈外，还有当今两个重要的协议：TLS 与 SPDY 。

1.3 TLS/SSL TLS/SSL 是当今一个比较重要的安全传输协议，它不在上面所列的协议栈中。但它被设计位于应用层与传输层之间。以确保数据在传输之前是安全的，但又不破坏原有的数据协议。应用层协议（例如：HTTP、FTP、Telnet等等）能透明的创建于TLS 协议之上。

我们经常听到的 HTTPS, 这个 S 指的就是 secure (安全)，同时分为 SSL 和 TLS 协议。它被 Netscape 公司在 1994 年推出。

**SSL**: (Secure Socket Layer，1994 年推出, 第一版作者: 塔希尔·盖莫尔)。 SSL通过互相认证、使用数字签名确保完整性、使用加密确保私密性，以实现客户端和服务器之间的安全通讯。

**TLS**: (Transport Layer Security, 1999 年推出) 它建立在SSL 3.0协议规范之上，作用同于 SSL, 与 SSL 3.0 差别极小，可以理解为SSL 3.1。

无论是 SSL 还是 TLS 都分为两层: 握手协议与记录协议

&#x20;

![SSL/TLS Protocol](http://upload-images.jianshu.io/upload_images/4957148-3a7e43579f69246f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/669)

* SSL 因为被报有各种安全问题，所以基本上现在所使用的都是 TLS。

以下是建立连接的过程：

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-d56da627138635a9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)

&#x20; 它必须建立在可靠的数据传输基础上，所以一般是在TCP之上。

同时，如果一但使用了 SSL/TLS 做安全验证，建立连接的时间实际上是会变长的，如图：

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-69b85d3715a655b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)

1.4 SPDY

SPDY（发音如英语：speedy），2010 年 9 月 一种开放的网络传输协议，由 Google 开发，用来发送网页内容的协议诞生，用以最小化网络延迟，提升网络速度，优化用户的网络使用体验。它基于 TCP 的应用层协议，也是 HTTP/2 的前身。它最早被应用在 Chrome 6 。

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-08476cd6e89c4869.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/567)

&#x20;SPDY 并不用于取代 HTTP，它只是修改了 HTTP 的请求与应答在网络上传输的方式；这意味着只需增加一个 SPDY 传输层，现有的所有服务端应用均不用做任何修改。 当使用 SPDY 的方式传输，HTTP 请求会被处理、标记简化和压缩。比如，每一个 SPDY 端点会持续跟踪每一个在之前的请求中已经发送的 HTTP 报文头部，从而避免重复发送还未改变的头部。而还未发送的报文的数据部分将在被压缩后被发送。

Google 之所以改动 HTTP 协议而不是 TCP/IP，是因为改 HTTP 只需更新 Browser 和 web server 就行了，而改动 TCP/IP 牵扯面太广，需要更新所有的路由器，服务器和客户端的操作系统。

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-79509165e09fc023.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)

1.3 TCP 无论应用层的协议如何定义、使用，都要基于一条已经连接的通路去发送与接收数据，我们可以试着从我们最熟悉的数据传输层开始，来了解连接的建立与传输。

这一层有两个重要的协议，TCP 与 UDP。

TCP 协议的运行可分为三个阶段：**连接创建**(connection establishment)、**数据传送**（data transfer）和**连接终止**（connection termination）

**连接创建**

TCP 用三次握手（three-way handshake）过程创建一个连接。在连接创建过程中，很多参数要被初始化，例如序号被初始化以保证按序传输和连接的强壮性。

一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端打开一个套接字（socket）然后监听来自另一方的连接，这就是通常所指的被动打开（passive open）。服务器端被被动打开以后，用户端就能开始创建主动打开（active open）。

1. 客户端通过向服务器端发送一个 SYN 来创建一个主动打开，作为三路握手的一部分。客户端把这段连接的序号设定为随机数A。
2. 服务器端应当为一个合法的 SYN 回送一个 SYN/ACK。ACK 的确认码应为 A+1，SYN/ACK 包本身又有一个随机产生的序号B。
3. 最后，客户端再发送一个 ACK。当服务端受到这个 ACK 的时候，就完成了三路握手，并进入了连接创建状态。此时包的序号被设定为收到的确认号 A+1，而响应号则为 B+1。

   &#x20;

   TCP 的三次握手  **连接重试**

![](http://upload-images.jianshu.io/upload_images/4957148-db754a3d10ffb98a.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/216)

如果服务器端接到了客户端发的SYN后回了SYN-ACK后客户端掉线了，服务器端没有收到客户端回来的ACK，那么，这个连接处于一个中间状态，即没成功，也没失败。于是，服务器端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下，默认重试次数为5次，重试的间隔时间从1s开始每次都翻倍，5次的重试时间间隔为1s, 2s, 4s, 8s, 16s，总共31s，第5次发出后还要等32s才知道第5次也超时了，所以，总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s，TCP才会断开这个连接。

**数据传输**

主机收到一个 TCP 包时，用两端的IP地址与端口号来标识这个 TCP 包属于哪个session。

TCP 或 UDP 使用了端口号（Port number）的概念来标识发送方和接收方的应用层，通常也称为 [Internet sockets](https://link.jianshu.com/?t=https://en.wikipedia.org/wiki/Internet_socket)。对每个 TCP 连接的一端都有一个相关的16位的无符号端口号分配给它们。端口被分为三类：众所周知的、注册的和动态/私有的。众所周知的端口号是由因特网赋号管理局（IANA）来分配的，并且通常被用于系统一级或根进程。众所周知的应用程序作为服务器程序来运行，并被动地侦听经常使用这些端口的连接。例如：FTP (20 and 21), SSH (22), TELNET (23), SMTP (25), HTTP over SSL/TLS (443), and HTTP (80)等。注册的端口号通常被用来作为终端用户连接服务器时短暂地使用的源端口号，但它们也可以用来标识已被第三方注册了的、被命名的服务。动态/私有的端口号在任何特定的TCP连接外不具有任何意义。可能的、被正式承认的端口号有65535个。

**超时重传**

另外，对于 TCP 数据传输 ，有一个重要的概念提到的就是 **超时重传**。这个机制是进一步来证明为何 TCP 是一种可靠的传输。

发送方使用一个保守估计的时间作为收到数据包的确认的超时上限。如果超过这个上限仍未收到确认包，发送方将重传这个数据包。每当发送方收到确认包后，会重置这个重传定时器。

这个超时定义就是 **RTT** (来回通讯延迟（Round-trip delay time）) 在的定义。

**连接终止**

使用了四次握手 (four-way handshake) ，终止一个 TCP 连接

![](http://upload-images.jianshu.io/upload_images/4957148-5a820a5abecfa988.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/375) \</div>

TCP 四次握手终止连接 \</div> 更详细的资料可以参考 [Wikipedia](https://link.jianshu.com/?t=https://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE)

1.4 UDP 除了可靠的 TCP 连接，还有一种称为不可靠的连接机制: 用户数据包协议（User Datagram Protocol，缩写为UDP），是一个简单的面向数据报的传输层协议。此协议由 David P. Reed 在 1980 年设计出来，并纳入 [RFC 768](https://link.jianshu.com/?t=https://tools.ietf.org/html/rfc768)成为正式的规范。

两者各有利弊，UDP 无需建立连接，不保证传输的可靠性，也没有重试机制，所以它的效率更高，当然数据传输的质量也不保证。

在TCP/IP模型中，UDP为网络层以上和应用层以下提供了一个简单的接口。UDP只提供数据的不可靠传递，它一旦把应用程序发给网络层的数据发送出去，就不保留数据备份（所以UDP有时候也被认为是不可靠的数据报协议）。UDP在IP数据报的头部仅仅加入了复用和数据校验（字段）。

网络上的众多使用UDP协议的关键应用一定程度上是相似的。这些应用包括域名系统（DNS）、简单网络管理协议（SNMP）、动态主机配置协议（DHCP）、路由信息协议（RIP）和某些影音流服务等等。

1.5 Socket 上面我们提到了 **Socket** ？按字面的意思是插座的意思，也就是建立一个通道，进行通信或连接。

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-31aecd2f383dae34.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/480)

Socket 工作流  根据 OSI 模型，**它是基于传输层 (TCP/UDP) 上的一种具体实现**。有了 TCP/UDP 协议，双方可以按照协议的规定，搭建起连接，一但连接搭建起来，我们就称它为 Socket 连接已建立，相当于插到插座上了。

**在计算机科学中**

它通常代表网络套接字（Network socket），又译网络插槽，是电脑网络中进程间互相通信的一种机制。使用互联网协议（Internet Protocol）为通信基础的网络套接字，称为 Internet socket。因为互联网协议的流行，现代绝大多数的网络套接字，都是属于 Internet socket。

**在操作系统中**

它是一种提供进程间通信的机制。通常会为应用程序提供一组应用程序接口（API），称为套接字接口（socket API）。应用程序可以通过套接字接口，来使用网络套接字，以进行数据交换。最早的套接字接口来自于4.2 BSD，因此现代常见的套接字接口大多源自Berkeley套接字（Berkeley sockets）标准。在套接字接口中，以IP地址及通信端口组成套接字地址（socket address）。远程的套接字地址，以及本地的套接字地址完成连接后，再加上使用的协议（protocol），这个五元组（five-element tuple)，作为套接字对（socket pairs），之后就可以彼此交换数据。

例如，再同一台计算机上，TCP 协议与 UDP 协议可以同时使用相同的port而互不干扰。 操作系统根据套接字地址，可以决定应该将数据送达特定的进程或线程。**这就像是电话系统中，以电话号码加上分机号码，来决定通话对象一般**。

所以也可以理解为 socket = IP + Port

这里有 C 写的 [Socket Demo](https://link.jianshu.com/?t=https://www.codeproject.com/Articles/586000/Networking-and-Socket-programming-tutorial-in-C) 大家可以参考。其中 accept 函数也就是实现了三次了握手的过程。

2\. 应用层

2.1 HTTP 应用层里最常用的协议之一，Hypertext Transfer Protocol (HTTP) 超文本传输协议。

**HTTP 历史**

**- 1965** 年，一个叫 [Ted Nelson](https://link.jianshu.com/?t=https://en.wikipedia.org/wiki/Ted_Nelson)的 26 岁年轻人，提出了超文本与超媒体的概念。（真是英雄出少年）

**- 1989** 年，时隔多年，一个叫 [Tim Berners-Lee](https://link.jianshu.com/?t=https://en.wikipedia.org/wiki/Tim_Berners-Lee)和他的团队在 CERN（欧洲核子研究组织）基于超文本概念，开始了 WorldWideWeb 项目。也就是我们现在耳熟能详的 WWW (万维网)，将超文本系统与互联网结合在一起，将超文本数据发送出去。第一个版本只有一个方法，叫 GET ，它可以从服务器请求一个页面到客户端，这个页面就是 HTML 页。

**- 1991** 年，第一个 HTTP 的文档 **HTTP 0.9 定义出来**。世界上第一个网站在 CERN 搭建。

**- 1996** 年，**HTTP 1.0**标准发布。[Dave Raggett](https://link.jianshu.com/?t=https://en.wikipedia.org/wiki/Dave_Raggett)领导的 HTTP Working Group (HTTP WG) 扩充了 HTTP 的协议，增加了富媒体信息，安全协议，header 域等。

**- 1997** 年，**HTTP 1.1** 标准被制定。

**- 2015** 年，**HTTP 2.0** 发布

**关于 HTTP Session**

一个 HTTP session 是一个网络的 request-response 序列。一个 HTTP 客户端初始时请求建立一个 TCP 连接到服务器的指定端口（通常为 80）。一个 HTTP 服务端一直监听来自客户端的 request 信息。基于接收的 request, 服务器返回状态行，如 "HTTP/1.1 200 OK"。

2.1 HTTP 0.9 \~ 2.0 进化之路

&#x20;

![](http://upload-images.jianshu.io/upload_images/4957148-c049ff81a4e01961.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/638)

**HTTP 0.9 :** 只有 GET 一只方法，不支持请求头

**HTTP 1.0 :** 增加了请求头，除了 GET 外，多了很多方法，如 POST, HEAD, PUT 等。

**HTTP 1.1 :** 由于 HTTP 1.0 只保持短暂的连接，每次请求后，TCP 连接都会被关闭，导致大量的时间耗费在了建立连接上。另一个问题是， HTTP 1.0 的请求像一个队列，遵循 FIFO (先进先出)的原则，当第一个请求的 Response 返回后，第二个 Request 才能发出，这就是所说的 head of line blocking (HOL blocking)。

所以 HTTP 1.1 对这两个主要问题进行了改进

a. 支持了持久连接，在一个TCP连接上可以传送多个HTTP请求和响应，减少了建立和关闭连接的消耗和延迟。

b.允许客户端不用等待上一次请求结果返回，就可以发出下一次请求，但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果，以保证客户端能够区分出每次请求的响应内容，这样也显著地减少了整个下载过程所需要的时间。

此外，1.1 协议中还支持了身份认证、状态管理和Cache缓存等机制，可以很方便的实现如断点续传等场景。

HTTP 1.0 与 HTTP 1.1 的主要区别可以看这里：[Key Differences between HTTP/1.0 and HTTP/1.1](https://link.jianshu.com/?t=http://www8.org/w8-papers/5c-protocols/key/key.html)

![HTTP 1.1 / 1.0 区别](http://upload-images.jianshu.io/upload_images/4957148-b32fae7a4b77bb94.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/450)

&#x20; **HTTP Pipelining**

它是将多个HTTP 请求整批提交的技术，而在发送过程中不需先等待服务端的回应。

请求结果管线化使得 HTML 网页加载时间动态提升，特别是在具有高延迟的连接环境下。在宽带连接中，加速不是那么显著的，因为需要服务器端应用 HTTP/1.1 协议:服务器端必须按照客户端的请求顺序恢复请求，这样整个连接还是先进先出的，对头阻塞（HOL blocking）可能会发生，造成延迟。

&#x20;

![Pipelining](http://upload-images.jianshu.io/upload_images/4957148-d64a99f379353df6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)

**HTTP 2.0:** 实际上 HTTP 1.1 使用了很长时间后，一直没有经过大的改进，基本上也就是在原来的基础上进行小范围的协议优化与扩充。直到 Google 推出了 SPDY 后，HTTP 2.0 基于 SPDY 的基础上，被正式提出。

它主要解决了 HTTP 1.0 或 1.1 中始终存在的 HOL blocking 问题。

&#x20;

![HTTP 2.0 二进制分帧](http://upload-images.jianshu.io/upload_images/4957148-b9eae46e32699bbf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/526)

它并没有破坏原有的 HTTP 结构，仅仅是将数据采用了二进制传输，比起以前的文本传输，更紧凑与高效。在二进制分帧层上，HTTP2.0 会将所有传输的信息分割为更小的消息和帧，并对它们采用二进制格式的编码，其中 HTTP1.x 的首部信息会被封装到 Headers 帧，而我们的 request body 则封装到 Data 帧里面。

&#x20;

![HTTP 2.0 连接](http://upload-images.jianshu.io/upload_images/4957148-e36510500aa20a47.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)

新的二进制分帧机制改变了客户端与服务器之间交换数据的方式。 为了说明这个过程，我们需要了解 HTTP/2 的三个概念：

**数据流**：已建立的连接内的双向字节流，可以承载一条或多条消息。

**消息**：与逻辑请求或响应消息对应的完整的一系列帧。

**帧**：HTTP/2 通信的最小单位，每个帧都包含帧头，至少也会标识出当前帧所属的数据流。

**这些概念的关系总结如下：**

1. 所有通信都在一个 TCP 连接上完成，此连接可以承载任意数量的双向数据流。
2. 每个数据流都有一个唯一的标识符和可选的优先级信息，用于承载双向消息。
3. 每条消息都是一条逻辑 HTTP 消息（例如请求或响应），包含一个或多个帧。
4. 帧是最小的通信单位，承载着特定类型的数据，例如 HTTP 标头、消息负载，等等。 来自不同数据流的帧可以交错发送，然后再根据每个帧头的数据流标识符重新组装。

**这里的关键是数据流有了优先级，而不必像以前按顺序处理，服务端可以根据自定义的优先级处理 request。**

还有一个大的改进是**服务器推送**，HTTP 2.0 不必像以前一样，必须依赖 request 才发送 response ，打破了严格的请求-响应语义。即使客户端没有 Request 请求，服务端也可以发送 Response 给客户端。

* **tips**: 大家可以在命令行下，输入以下命令，看看两者有何不同。

  telnet [www.google.com](http://www.google.com) 80

  GET / HTTP/1.0\</i>

GET / HTTP/1.1

2.2. HTTPS 关于 HTTPS 中的 S ，即安全传输，之前已经讲到了，它是在介于传输层与应用层之间的一层安全协议，基于可靠传输（如 TCP ）之后，对信息的一层加密封装。

相信大家可以在很多地方搜到 HTTPS 的详细解释。这里就不逐一展开了。我们只要记住以下几点即可。

首先是，对称加密与非对称加密，两种加密方式最大的区别在于，对称加密没有数据长度限制，非对称有长度限制，所以要使用**非对称加密**传输**对称加密**的密钥。

其次，非对称加密中的公钥，不能直接发送给对方，否则会产生中间人攻击的场景。必须有一个可靠的第三方做中间人。这就是我们大家所熟知的 CA (数字证书的颁发机构)。

这个 CA 通常会根据域名来做服务器端的认证，而且 CA 的公钥是被内置在浏览器或操作系统中的，所以 CA 的公钥是不用传输的。这样就避免了中间人的证书伪造。

之前有人已经写过一篇 [非常通俗易懂的文章](https://www.jianshu.com/p/ca7df01a9041)，大家可以做参考。

2.3. WebSocket 提到应用层协议，其实还有一个比较流行的协议就是 WebSocket ，它在 2011 年被被IETF 定为标准 RFC 6455。

WebSocket使得客户端和服务器之间的数据交换变得更加简单，允许服务端主动向客户端推送数据。在 WebSocket API 中，浏览器和服务器只需要完成一次握手，两者之间就直接可以创建持久性的连接，并进行双向数据传输。

为什么叫 WebSocket 呢？它和 Socket 什么关系？

首先 WebSocket 和 Socket 没有半毛钱关系，它之所以这么叫，完全是为了利于理解。之所以强调 Web，是因为它可以基于 HTTP 连接进行协议切换。还记得之前 HTTP /1.1 里的 request header 吗？我们可以定义 Upgrade: websocket Connection: Upgrade。然后服务器的 response 会返回: HTTP/1.1 101 Switching Protocols。即可切换到 WebSocket 协议。

&#x20;

![WebSocket communication](http://upload-images.jianshu.io/upload_images/4957148-fe4bcd2122e951ff.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/500)

web socket 使用 ws 或 wss 做为统一的标识符。如：ws\://[www.abc.com](http://www.abc.com)

实践：[基于 node JS 的 WebSocket 客户端与服务端开源项目](https://link.jianshu.com/?t=https://github.com/websockets/ws)

3\. 总结 回答开始所提出的问题

**1. 当我们打开浏览器访问一个网页时，它是如何将数据安全的发送给我们的?**

如果有域名的先通过 DNS 解析域名，如果是 IP 直接访问的就不用了。然后排开底层网络协议不看，从传输层开始，先进行三次握手，有 S 的进行加密通道的建立，拿到传输密钥。然后基于应用层的协议，比如 HTTP ，进行 Request 与 Response 的数据传输。根据使用的 HTTP 协议不同，建立的连接有可能是持续的，也有可能是一次性的。

**2. Http 2.0 为何时隔10年才出现，它解决了什么问题？**

HTTP 1.1 出来后，基本上在上面做补充与小调整，没有实质的变化。直到十年后，凭借 Google 强大的技术研发能力，加上现代的网络基础设施越来越好，提出了二进制流传输方式，解决了 HOL blocking 问题。并最终成为了 HTTP /2.0 的标准。目前，在应用层上，可优化的空间越来越小，接下来的瓶颈更多是传输层了，但传输的层的变化，会牵扯所有现有的浏览器与操作系统，所以这一块的演化没有想像的那么快。也许将来有一天，会有更新的技术出现。

**3. Socket 和 Http 什么关系？**

Socket 是通道，在网络领域，它是一个 Network Socket ，建立两个端之间的一条管道，进行数据的传输。HTTP 是基于这个管理，进行超文本的传输。简单的讲， Socket 一但建立，你可以传输任何东西，HTTP 也好， FTP 也好。取决于你的应用协议是什么。

**4. Web socket 又是什么？它是工作在哪一层的？**

Web Socket 只是一个双工通信概念，它为了解决 HTTP 只能通过 Request 与 Response 的形式发送信息的模式，无论是客户端还是服务端都可以主动的发送数据。HTTP /2.0 也加入了此概念。但 WebSocket 发明的比它早，所以它是前辈。

它工作在传输之上，应用层的一种，可以理解为 HTTP 的升级版。因为它可以利用 HTTP 的协议，进行升级使用，所以称为 Web Socket.

**5. 想要更轻量，更有效率的传输如何做？**

你可以直接面向 Socket 编程，传输层之上进行开发，自己发明更轻量，更高效的应用层协议。现有的操作系统，对 TCP 接口有非常标准，完善的编程接口，开发也不是难事了。所以你可根据应用的场景，定制更合适的应用层协议。

推荐买一本 Socket 网络编程开始学。


# Git


# Gitflow 在客户端开发中的实践

为了规范开发流程，当存在多次预发布情况时，不至于因代码不同步，遗漏Bug修复。并让流程更清晰，避免出现多个混淆分支。所以将实践方案提出供大家一起讨论。

#### 先看看标准的 Gitflow 流程

**原则**

* 新建feature、bugfix , release，全部用 git flow start 或 SourceTree 右上角的 Gitflow 流程
* feature, Bugfix 命名规范：feature/用户名/功能名 ,  bugfix/用户名/修复点
* 完成feature 或 Bugfix 时，用 git flow  finish 或 SourceTree 右上角的 Gitflow 流程
* BugFix 仅在 release 分支上进行，HotFix 仅在 master 分支上进行

**关于Code Review**

* 如果在使用Gitlab 管理代码仓库时，可以使用 Gitlab 自带的 Merge Request 工具进行代码 Review 申请
*
* 创建 Merge Request ，并指定从哪个分支合并到哪个分支，并指定到Review 人
* Review 者仔细审核过代码后，如果没有疑问，切到对应的来源分支，进行上述的 git flow 的 finish 操作。
* 如果只是 feature -> develop ，可以直接点 Accept Merge Request

#### 代码管理实践

`Hotfix 后，会有版本号后加上一个小版本号，表示有一个 hot fix 修复. x.x.x.1`

***当有多个并行预发布版本正在进行时，需要注意的是***

* `Hotfix 在完成后，除了执行标准Gitflow 流程，还需同时合到正在并行的 release ​分支`

#### 更多资料参考

* [Github flow](https://guides.github.com/introduction/flow/)
* [Gitlab flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)


# Security


# 10 分钟将你的网站升级到 HTTPS

Certbot Guide

随着 HTTPS 的普及，现在不用 HTTPS 都不好意思把网站发给别人了，所以索性也把个人博客升级到 HTTPS 。

HTTPS 带来的好处不用多说了，直入正题。

**环境配置**

先说一下服务器环境配置，根据运行环境不同，可能会有少许配置差异。

OS: Ubuntu 14.04.2

Web Server: Nginx 1.4.6

CGI: PHP-FPM

现在提供免费证书的公益组织非常多了，[Let's encrypt](https://letsencrypt.org/) 就是其中一家。为了减少配置工作，推荐使用他们的 Shell 工具 [certbot](https://certbot.eff.org/) 在服务端自动配置。官网上列有不同 OS 的下载与安装方法。

官方推荐方式： certbot --nginx

我们这里用的是指定参数安装 **certbot certonly -w /var/www/blog -d** [**doruby.com**](https://doruby.com)

-w 是你站点存放的位置，目的是在对应的目录下，创建 *well-known/acme-challenge/* 这样的目录，并放入认证文件，它会让官网根据这个文件是否可访问，来判断这个域名是不是你所拥有的。所以要保证这个目录是可以被外部所访问到的。 所以在执行这个命令前，先确认你的 nginx 配置了它可被访问：

`server{ .... location ~ /.well-known { allow all; } }`

-d 是对应的域名，多个域名可以在后面继续用 -d 添加

如果命令顺利执行完成，会收到 Congratulations 的提示，并提示证书所在位置，比如 /etc/letsencrypt/live/doruby.com/fullchain.pem 记录下来。

在 Nginx 配置文件中，添加 SSL 相关配置

`server { listen 443; # 原先应该是 80`

`ssl on ssl_certificate /etc/letsencrypt/live/doruby.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/doruby.com/privkey.pem;`

如果相让原先访问 Http 的入口自动跳转到 Https ，可以再增加一些配置

```
server { 
    listen 80; 
    server_name doruby.com; 
    return 301;
    https://$host$request_uri;
}
```

OK. 重启 Nginx ,顺利的话你现在应该可以看到你的网站跳到 Https 了，并且前面会有绿色小锁。

这里有几点需要注意

* 如果创建证书遇到问题，第一时间检查提示的日志文件
* 如果配置完成后，网站打不开，establish 不成功，先检查一下服务器有没有打开 443 端口的访问权限


# Others


# FlexBox 使用向导

### 简介

Flexbox 布局模块提供一个容器，使其中的元素，可以采用更便捷的方法实现布局、对齐、分布间距等方式。甚至当大小不定时，可以自动伸缩（它们称为"flex"）

它的思想主要是基于 flex layout，调整容器里的元素宽/高，填充合适空格（更多用于不同设备的屏幕的适配）。

### 基本述语

**在了解 Flexbox 述语前，有必要先看看 CSS 中关于** [**inline-block**](http://www.w3schools.com/css/css_inline-block.asp) **的介绍**

Flexbox 是一个完整的模块，不是一个单一属性，它包含了很多设置属性。 它们中的一些被设置为容器(父元素，被称为 "flex container")，其它一些被设置为子元素（称为 "flex items"）

对于规则的布局是基于两块之间的相对方向流线性布局，这个 flex layout 是基于 "flex-flow 方向"。 请看下图以更好解释它。

基本上它会基于主轴(从main-start 到 main-end)或交叉轴（从cross-start 到 cross-end）方向排列它们。

* main axis flex容器的主轴主要用于排列它的元素。不过要注意，它不一定是水平方向，它依赖于 flex-direction 属性 (见下面说明)
* main-start | main-end flex 容器里的元素放置从main-start 开始到 main-end 结束
* main-size flex 元素的宽或高，有 width 与 height 属性
* cross axis 与主轴相交的即为交叉轴。它的方向依赖与主轴方向
* cross-start | cross-end Flex 基于 cross-start 与 cross-end 排列它的元素
* cross size 一个 flex 元素的宽或高

### 容器

容器属性

#### display

定义一个 flex 容器，给定值指定是内联(inline)或者块(block)。它可以打开一个 flex 环境用来决定元素的填充方式。

```
.container {
    display: flex; /* 或 inline-flex */
}
```

#### flex-direction

决定主轴方向

```
.container {
    flex-direction: row | row-reverse | column | column-reverse;
}
```

* row (默认): 从左到右
* row-reverse: 从右到左
* column: 从上到下
* column-reverse: 从下到上

#### flex-wrap

默认情况下 flex 元素会在一行显示. 你可以更变属性决定折行与折行的方向

```

.container {
    flex-wrap: nowrap | wrap | wrap-reverse;
}
```

* nowrap (默认): 单行
* wrap: 折行
* wrap-reverse: 反向折行

#### justify-content

决定如何在主轴上分布空白区域

```

.container {
    justify-content: flex-start | flex-end | center | space-between | space-around;
}
```

* flex-start (默认)：基于 start line 排列
* flex-end: 基于 end line 排列
* center: 居中
* space-between: 均匀的分布间距，第一个元素在首行，最后一个在未行
* space-around: 均匀的分布空白区域在各元素周围

#### align-items

决定交叉轴的排列方式

```

.container {
    align-items: flex-start | flex-end | center | baseline | stretch;
}
```

* flex-start
* flex-end
* center
* baseline
* stretch (默认)

#### align-content

类似于 justify-content ，交叉轴的内容排列

\-- 如果只有一行时没有效果

```

.container {
    align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}
```

* flex-start
* flex-end
* center
* space-between
* space-around
* stretch (默认)

### 元素

元素属性

#### order

默认情况下，flex 元素，按来源顺序排列和个元素，你也可以分别指定元素的 order 属性来重新排列出现的顺序。

```

.item {
    order: ;
}
```

#### flex-grow

决定各元素的你伸长比例，它是一个无符号单位。如果所有元素都设置为 1 ，它们将会是同一大小的排列在容器里。如果你给定一个值是2，那他会针对其他对象的两倍。

```

.item {
  flex-grow: ; /* 默认是 0 */
}
```

#### flex-basis

```

.item {
  flex-basis:  | auto; /* default auto */
}
```

[看这张图](http://www.w3.org/TR/css3-flexbox/images/rel-vs-abs-flex.svg)

#### flex

快捷方式 flex-grow, flex-shrink and flex-basis 这三个属性。第二和第三个参数为可选, 默认为 0 1 auto

```

.item {
  flex: none | [  ? ||  ]
}
```

#### align-self

可以指定某个元素的排列行为，请参考 align-items 解释

```

.item {
  align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
```

#### 示例

解决一个经常会遇到的问题：完美居中

```

.parent {
  display: flex;
  height: 300px; /* Or whatever */
}

.child {
  width: 100px;  /* Or whatever */
  height: 100px; /* Or whatever */
  margin: auto;  /* Magic! */
}
```

### 其它相关资源

[Facebook Css Layout](https://github.com/facebook/css-layout)

[Flexbox in the CSS specifications](http://www.w3.org/TR/css3-flexbox/)

[Flexplorer by Bennett Feely](http://bennettfeely.com/flexplorer/)

[Flexbox at MDN](https://developer.mozilla.org/en-US/docs/CSS/Tutorials/Using_CSS_flexible_boxes)

[Flexbox at Opera](http://dev.opera.com/articles/view/flexbox-basics/)

[Diving into Flexbox by Bocoup](http://weblog.bocoup.com/dive-into-flexbox/)

[Mixing syntaxes for best browser support on CSS-Tricks](https://css-tricks.com/using-flexbox/)

[Flexbox by Raphael Goetter (FR)](http://www.alsacreations.com/tuto/lire/1493-css3-flexbox-layout-module.html)


# Google VP 聊职场干货

听 Google VP Yanbing 聊职场干货，特别适合已经或即将做上技术管理岗的同学。

* 为何华人不擅长推销自己
* 如何找到你的伯乐
* 如何寻找 Sponsorship
* 如何让人觉得你是值得投资的
* 如何建立自己的影响力
* 如何高质量开远程会议
* 如何选对你的老板
* 新人如何被 Sponsors 注意

{% embed url="<https://www.youtube.com/watch?v=vs0f8A38FVQ>" %}
Google VP Sharing Career Skilset
{% endembed %}

如果没有梯子的看[这里](https://www.bilibili.com/video/BV1ZF411V7Wv/)


# 无身份建立影响力 Influence without Authority

听 Leon Liu 分享无身份建立影响力，适合职场的不同阶段

## 概要

* 为何 Influence without Authority 如此有用
* 其实人们在意的不是你懂的比他多
* 用同理心引导谈话方向
* 用框架思维介绍三个非常有力的工具：倾听、同理心、好奇心
* 如何确定自己达到了同理心
* 用提问的方式引发对方产生新的思考
* 如何用好奇心解决冲突
* 在向上汇报时如何避免紧张
* 获得自信的终极方法 - egoless
* 实战--通过讲故事让人们记住
* 当矛盾不可避免时，如何通过谈判解决
* 从做事到战术，再到战略的转变

{% embed url="<https://youtu.be/4QeVLlVNqIA>" %}
Influence without Authority
{% endembed %}

### 敲黑板

#### 1. 倾听的四个目的

1. 为了欣赏而听
2. 为了理解而听
3. 为了感同身受而听
4. 为了评估而听

第三与第四混淆起来就造成了冲突的源泉

#### 2. 我们的目标不是证明我是对的，而是让我们共同做出一个对的选择

#### 3.职场初期你会得到如何做事的帮助，中期你会收到战术层面的帮助，而中后期你多半只会收到战略层面的指导。

Ref:

[Leon Liu's medium](https://realsy.medium.com/)


# 对 EM Role 非常实用的面试 Feedback

绝不能错过的面试金牌复盘：Manager 岗位的胜出关键

这段视频的精华在于面试官 Josh 对 Manager 管理岗面试表现的深度复盘。这些建议非常实用，尤其是对于那些技术出身、初入管理层或正在寻求晋升的人来说，往往是容易忽略的“软技能”核心。

{% embed url="<https://youtu.be/FAhxiQN9BOE>" %}

在这段 Mock Interview 的点评中，资深面试官 Josh 拆解了管理岗位面试中“好”与“卓越”的区别。如果你正在准备管理岗面试，以下这几点建议可能直接决定你的录用结果：

**1. 像报纸标题一样“先声夺人” (Lead with a Headline)**

不要直接陷入故事细节。建议：**先给出一个有冲击力的标题。**

* **示例：** “我最自豪的是 ABC 项目，主要原因有三点……”
* **逻辑：** 就像阅读 Apple News，先让面试官掌握核心大意（The Gist），如果他们感兴趣，再引导他们深入细节。这能极大降低面试官的认知负担。

**2. 从“战术描述”转向“价值呈现” (Tactical vs. Why it Matters)**

很多 Manager 容易把 STAR 法则讲成流水账。

* **避坑：** 不要只讲你做了什么（战术细节），那会让你的项目听起来和别人没什么两样。
* **加分：** 重点突出“为什么这件事对业务很重要”**。是提高了团队效率？还是保住了大客户？或者是解决了潜在的人才流失？**“商业影响力”才是让你的故事具有辨识度（Personal Edge）的关键。

**3. 极简沟通：去术语化 (No Jargon)**

即使面试官是资深技术主管，也请尽量避免使用公司内部缩写或晦涩的行业术语。

* **理由：** 优秀的管理者应具备将复杂问题简单化的能力。清晰、直白的沟通比炫耀专业名词更能体现你的领导力。

**4. 明确你的角色定位 (Role Clarity)**

在讲述团队成就时，必须清晰地界定**你的个人职责**。

* **做法：** “当时我是这个项目的经理，带领一支 7 人的团队……” 模糊的“我们”会让面试官怀疑你的实际贡献，明确的“我”才能体现管理者的担当。

**5. 洞察行为面试题背后的“潜台词”**

当面试官问“请自我介绍”或“讲讲你的经历”时，他们真正想听的是：

* 你如何独立解决我的难题？
* 你如何维持团队士气并降低离职率？
* 你是一个能让团队既高产又快乐的领路人吗？

**🛠 实用练习小贴士：**

* **300字法则：** 将你的核心故事写下来，控制在 300 字左右。
* **大声朗读：** 读出来，判断它听起来是像在记流水账，还是在展现管理者的洞察力？

***

**总结一句话：** 别只讲你做了什么，要讲你为什么做，以及你带来的改变。


