Repository: windstormeye/iOS-Course
Branch: master
Commit: caa019bf38e8
Files: 151
Total size: 612.8 KB
Directory structure:
gitextract_3vegyaaq/
├── .github/
│ └── workflows/
│ └── jekyll-gh-pages.yml
├── .gitignore
├── AI/
│ └── basic.md
├── Android/
│ ├── Java.md
│ ├── Kotlin.md
│ ├── feature.md
│ ├── 内存.md
│ ├── 基础知识.md
│ └── 问题汇总.md
├── Back-end/
│ ├── DB.md
│ ├── Docker.md
│ ├── RESTful.md
│ ├── Vapor.md
│ ├── django.md
│ ├── jwt.md
│ ├── mysql.md
│ ├── nginx.md
│ ├── web服务器.md
│ └── 后端学习.md
├── Base/
│ ├── C++.md
│ ├── UML.md
│ ├── algorithm-java.md
│ ├── leetCode.md
│ ├── leetcode/
│ │ ├── 两个排序数组的中位数.md
│ │ ├── 两数之和.md
│ │ ├── 两数相加.md
│ │ ├── 无重复字符的最长子串.md
│ │ └── 最长回文子串.md
│ ├── nowCode.md
│ ├── python.md
│ ├── 操作系统.md
│ └── 网络相关知识.md
├── Blockchain/
│ └── basic.md
├── Books/
│ └── iOS面试之道.md
├── CV/
│ └── basic.md
├── Flutter/
│ ├── Dart.md
│ ├── Flutter_2.md
│ ├── Flutter_3.md
│ └── Flutter问题汇总.md
├── Front-end/
│ ├── CSS.md
│ ├── FCC.md
│ ├── JavaScript.md
│ ├── Vue.md
│ ├── basic.md
│ ├── vue-context-mune.md
│ ├── 前端学习.md
│ └── 图解HTTP学习笔记.md
├── Game/
│ └── Cocos/
│ └── basic.md
├── Graphics/
│ ├── app.md
│ └── metal.md
├── History/
│ ├── 2_Apple_History.md
│ ├── 3_Mac_OS_X.md
│ └── 4_iOS.md
├── LICENSE
├── Media/
│ ├── basic.md
│ └── feature.md
├── MiniProgram/
│ ├── 小程序初探.md
│ └── 小程序初探(二).md
├── NLP/
│ └── NLP.md
├── Others/
│ ├── myinterview.md
│ ├── 招一个靠谱的iOS实习生(附参考答案).md
│ ├── 简介.md
│ └── 面试准备.md
├── Product/
│ └── Map.md
├── Project/
│ ├── Bonfire.md
│ ├── CocosCreator——方块弹球.md
│ ├── ONEUIKit-ONEProgressHUD.md
│ ├── PFollow.md
│ ├── PLook.md
│ ├── coding-interview-university学习笔记.md
│ ├── iBistu4-0(先导篇).md
│ ├── iBistu4-0(地图).md
│ ├── iBistu4-0(失物).md
│ ├── iBistu4-0(新闻).md
│ ├── iBistu4-0(黄页).md
│ ├── 上架.md
│ ├── 第三方库管理.md
│ └── 翻译——ViewsprogrammingGuideforiOS.md
├── Qt/
│ ├── C++.md
│ ├── UI.md
│ ├── base.md
│ ├── crossPlatform.md
│ ├── opt.md
│ └── project.md
├── README.md
├── React-Native/
│ ├── React-Native记〇.md
│ ├── React-Native记(一).md
│ └── React-Native记(二).md
├── Test/
│ └── 单元测试.md
├── Tools/
│ ├── 2_百家汇.md
│ ├── 3_GitHub.md
│ ├── 4_Xcode.md
│ ├── 5_Xcode.md
│ ├── Playerground.md
│ ├── Xcode.md
│ ├── XcodeGuide.md
│ └── 开发中可能会用到的内容.md
├── Toturial/
│ └── 剪刀石头布.md
├── UI/
│ └── 3_StoryBoard.md
├── Weex/
│ └── Weex新手记.md
├── Win/
│ └── basic.md
├── iOS/
│ ├── Layout.md
│ ├── More-弹幕.md
│ ├── Objective-C/
│ │ ├── More-Audio.md
│ │ ├── More-DesignPattern.md
│ │ ├── More-iOS上的相机.md
│ │ ├── More-iOS国际化一站式解决方案.md
│ │ ├── More-视频相关.md
│ │ ├── More-页面传值.md
│ │ ├── Objective-C注意点.md
│ │ ├── ping.md
│ │ ├── runtime.md
│ │ ├── tips-自定义tabBar大加号引发的思考.md
│ │ ├── 并发编程.md
│ │ └── 系统相关.md
│ ├── Swift/
│ │ ├── Cache.md
│ │ ├── CoreData.md
│ │ ├── OC转Swift.md
│ │ ├── PFollow.md
│ │ ├── PJPickerView开发总结.md
│ │ ├── PJPickerView开发总结.md).md
│ │ ├── PhotosKit开发总结(一).md
│ │ ├── Playgrounds.md
│ │ ├── SpriteKit.md
│ │ ├── SwiftUI.md
│ │ ├── Swift注意点.md
│ │ ├── UIDynamic.md
│ │ ├── code.md
│ │ ├── debug.md
│ │ ├── landscapeandportrait.md
│ │ ├── tips.md
│ │ ├── 七牛图片上传助手.md
│ │ ├── 品种选择器总结.md
│ │ └── 自定义NavigationBar.md
│ ├── Today_Extension.md
│ ├── UI.md
│ ├── UICollectionView.md
│ ├── UITableView.md
│ ├── basic.md
│ ├── code.md
│ ├── debug.md
│ └── system.md
├── macOS/
│ ├── TranslateP.md
│ ├── basic.md
│ ├── crash.md
│ ├── kindle.md
│ ├── macOS开发(词法分析器).md
│ ├── performance.md
│ ├── playground.md
│ └── 一台设备多个git账号.md
└── ruby/
└── basic.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/jekyll-gh-pages.yml
================================================
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll with GitHub Pages dependencies preinstalled
on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .gitignore
================================================
.vscode
================================================
FILE: AI/basic.md
================================================
# AI
## 各种原则
### 金发姑娘原则
* [相关链接](https://en.wikipedia.org/wiki/Goldilocks_principle)
* 解释“金发姑娘原则”指出,凡事都必须有度,而不能超越极限。按照这一原则行事产生的效应就称为“金发姑娘效应”。
### 学习速率
* 通过一个图来解释

* 当「学习速率」过高,容易导致每一步都在曲线上进行跳跃,沿着曲线向上爬,而不是降到底部。
### 随机梯度下降法/ SGD
### 小批量随机梯度下降法/小批量 SGD
### 协同过滤
* 基于用户的协同过滤算法 UserCF
* 统计两个用户的相似度。两个用户阅读过的内容 ID 列表如果重合度越高,说明越相似。
* 适合用在个性化需求不强,热点很明显的领域,比如新闻,电影推荐。
* 基于物品的协同过滤算法 ItemCF
* 适合用在个性化需求比较强,长尾比较长的领域,比如书、电商的推荐。
### 如何判断用户会不会点一个物品:特征
* 用户特征
* 历史上点击的文章列表
* 历史上点击的文章的关键词分布
* 历史上点击的文章的作者分布
* 文章特征
* 文章的作者,关键词
* 用户和文章的交叉特征
* 用户历史上有没有点过这篇文章的作者发表的其他文章
* 用户历史上有没有点击过和这篇文章关键词类似的其他文章
### 文章冷启动
* 推荐系统是所有的文章在一个候选池互相 PK,找到对当前用户兴趣最好的
* 新文章因为展现少,在 PK 中往往落在下风
* 要做一个专门的冷启动机制,保证新文章在展现小于 X 前在 PK 中取得更大的获胜概率,直到充分展现,可以在 PK 中公平竞争
================================================
FILE: Android/Java.md
================================================
## Java
### 注解 `@`
提到注解就要带上注释,注释是给开发者看的,而注解就是给程序自己看的。比较类似做一些小方法检查,压缩代码量,本质上与 Swift 5.5 引入的 `@PropertyWrapper` 作用一致。
注解分位编译期注解和运行时注解,作用范围不同。运行时注解可以通过反射进行获取使用,但反射本身有性能损耗。
================================================
FILE: Android/Kotlin.md
================================================
# Kotlin 问题汇总
## 语法
### `var` 和 `val` 的区别
`var` 与我们之前见到的 `var` 概念一致,但 `val` 取代了以往 `let` 作用。kotlin 中 `let` 另有他用。
```kotlin
var a: String = "initial" // 1
println(a)
val b: Int = 1 // 2
b = 2
// 报错:Val cannot be reassigned
```
### `vararg`
本质上是个 `Array` 的语法糖,但可以用“逗号”分隔开参数。在保证参数类型一致的情况下可以这么传参:
```kotlin
class MutableStack(vararg items: E) { // 1
private val elements = items.toMutableList()
fun push(element: E) = elements.add(element) // 2
fun peek(): E = elements.last() // 3
fun pop(): E = elements.removeAt(elements.size - 1)
fun isEmpty() = elements.isEmpty()
fun size() = elements.size
override fun toString() = "MutableStack(${elements.joinToString()})"
}
fun mutableStackOf(vararg elements: E) = MutableStack(*elements)
fun main() {
val stack = mutableStackOf(0.62, 3.14, 2.7)
println(stack)
}
```
### class
```kotlin
// 可以声明一个没有任何属性的 class,kotlin 会自动创建一个无参构造函数
class Customer
```
* kotlin 的类默认是 `final`,如果该类想要被继承,需要用 `open` 进行修饰;
* kotlin 的方法默认同样也是 `final`,在类中,如果想要的该方法可以被重载,需要用 `open` 进行修饰,并且在重载方法前加上 `override` 修饰;
```kotlin
open class Dog { // 1
open fun sayHello() { // 2
println("wow wow!")
}
}
class Yorkshire : Dog() { // 3
override fun sayHello() { // 4
println("wif wif!")
}
}
fun main() {
val dog: Dog = Yorkshire()
dog.sayHello()
}
```
#### 继承有参构造类
不得不说,这种写法真是太奇特了。
```kotlin
open class Tiger(val origin: String) {
fun sayHello() {
println("A tiger from $origin says: grrhhh!")
}
}
class SiberianTiger : Tiger("Siberia") // 1
fun main() {
val tiger: Tiger = SiberianTiger()
tiger.sayHello()
}
```
================================================
FILE: Android/feature.md
================================================
## feature 思考
### 拼多多自动拉起 app
android 的四大基础组件中,只有 activity 是完整的用户可见应用程序入口,剩下的三大基础组件用户都不可见,可以通过 Service 来注册一个不可见的服务,在某些条件下唤起主 activity。
还有另外一种做法是 app 间互相拉起,接入一个中间层 SDK,目前猜测可能是友盟有类似的服务,在接入友盟后某个 app 被杀掉后,可以由另外的 app 拉起。
================================================
FILE: Android/内存.md
================================================
## 内存知识
### Java GC
大概流程详见:[https://zhuanlan.zhihu.com/p/23102625]()
Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报 OOM(out of memory)的错误,Java应用将停止。
================================================
FILE: Android/基础知识.md
================================================
# Android 基础
## 名词解释
* APK:Android application package,Android 应用程序包;
*
## APK 基础
应用安装到设备后,每个 APK 都运行在自己的**安全沙箱**中(这点与 ipa 一致):
* Android 是一种多用户的 `Linux` 系统,其中每个应用都是一个不同的用户;
* 默认情况下,系统会为每个应用分配一个**唯一的 Linux 用户 ID**(应用不知晓)。系统为应用中的所有文件设置权限,使得只有分配给该应用的用户 ID 才能访问这些文件;
* 每个进程都具有自己的虚拟机,因此应用之间被隔离;
* 默认情况下,每个应用都在自己的 Linux 进程内运行。 Android 会在需要执行任何应用组件时启动该进程,然后在不需要时将该进程或系统必须为其它应用程序恢复内存时杀掉该进程;
Android 系统通过这种方式实现**最小权限原则**。默认情况下,每个英语都只能访问其工作所需的组件,而不能访问其它组件,在这个非常安全的环境中,应用无法访问系统中未获得权限的部分。但应用仍然可以通过一些途径与其它应用共享数据以及访问系统服务:
* 可以让两个应用共享同一个 Linux 用户 ID,它们可以相互访问彼此的文件。为了节省资源,可以让具有**相同用户 ID **、**相同签名证书**的应用在同一 Linux 进程中运行,并共享同一虚拟机
* 应用可以请求访问设备数据权限(联系人、相机、蓝牙等),用户必须明确授予这些权限。
## 应用组件(应用的基本构建基块)
系统都可以通过这些组件直接进入应用,但并非所以组件都是用户的实际入口。共有**四种不同的应用组件类型**,每种类型服务于不同的目的,并且具有定义组件创建和销毁方式的不同生命周期。
* **`Activity`**:`Activity` 表示具有用户界面的单一屏幕。每一个 `Activity` 都独立于其它 `Activity` 而存在,其它应用可以启动其中任何一个 `Activity`。例如,相机应用可以启动 email 应用内用于撰写新电子邮件的 `Activity`,以共享图片。
* **服务(`Service`)**:服务是一种在后台运行的组件,用于执行长时间运行的操作或为远程进程执行作业,且不提供用户界面。当用户设备前台处于其它应用时,服务可能在后台播放音乐或正在通过网络获取数据,此时并不会阻碍与 `Activity` 的交互。 `Activity` 等其它组件可以启动服务。
* **内容提供程序(`ContentProvider`)**:内容提供程序管理一组共享的应用数据,您可以将数据存储在文件系统、SQLite 数据库、网络或其它可以被应用访问到的地方。其它应用可以通过内容提供程序查询、修改数据。任何具有适当权限的应用都可以查询内容提供程序的某一部分。
* **广播接收器(`BroadcastReceiver`)**:广播接收器时一种用于响应系统范围广播通知的组件。许多广播都是由系统发起的,如通知屏幕已关闭、电池电量不足等,应用也可以发起广播,如其它应用某些数据已下载至该设备。同样,广播接收器也没有用户界面,但它们可以创建**状态栏通知**。
当系统启动某个组件时,会启动该应用的进程(如果还未运行),并实例化该组件所需的类。如果应用启动相机应用中拍摄照片的 `Activity` ,则该 `Activity` 会在属于相机应用的进程,而不是我们应用的进程中运行。所以,Android 应用并没有单一入口点(`main()` 函数)。
由于系统在单独的进程中运行每个应用,且其文件权限会限制对其它应用的访问,故我们的应用无法直接启动其它应用中的组件,但可通过 Android 系统传递消息,说明我们想要启动特定组件的 `intent`,系统随后便会启动。
### 启动组件
`Activity` 、服务和广播接收器通过 `intent` 的异步消息进行启动。 `intent` 会在运行时将各个组件相互绑定(可将 `intent` 视为从其它组件请求操作的信使),无论该组件是否为我们的应用。 `intent` 可以是显式的也可以是隐式的。对 `Activity` 和服务, `intent` 定义要执行的操作,并且可以指定要执行操作数据的 URI。
`intent` 不会启动内容提供程序组件,它会在成为 `ContentResolver` 的请求目标时启动,内容解析程序通过内容提供程序处理所有直接事物,使得通过提供程序执行事物的组件可以无需执行事物,而是改为在 `ContentResolver` 对象上调用方法,已留出一个抽象层确保安全。
每种类型的组件有不同的启动方法:
* 可以通过将 `Intent` 传递到 `startActivity()` 或 `startActivityForResult()`(当您想让 `Activity` 返回结果时)来启动 `Activity`(或为其安排新任务)。
* 可以通过将 `Intent` 传递到 `startService()` 来启动服务(或对执行中的服务下达新指令)。 或者,您也可以通过将 `Intent` 传递到 `bindService()` 来绑定到该服务。
* 可以通过将 `Intent` 传递到 `sendBroadcast()`、`sendOrderedBroadcast()` 或 `sendStickyBroadcast()` 等方法来发起广播;
* 可以通过在 `ContentResolver` 上调用 `query()` 来对内容提供程序执行查询。
## 清单文件
在 Android 系统启动应用组件之前,系统必须通过读取应用的 `AndroidManifest.xml` 文件(清单文件)来确认组件存在,应用必须在此文件中声明所有组件,该文件必须位于应用项目目录的根目录中。清单文件还有以下作用:
* 确定应用需要的任何用户权限,如互联网访问权限或对用户联系人的读取权限;
* 根据应用使用的 API,声明应用所需的最低 API 级别;
* 声明应用使用或需要的硬件和软件功能,如相机、蓝牙服务;
* 应用需要链接的 API 库(系统 API 除外);
通过以下方式声明所有应用组件:
* Activity 的 `` 元素
* 服务的 `` 元素
* 广播接收器的 `` 元素
* 内容提供程序的 `` 元素
如果在源码中写明的组件,但未再清单文件中声明的 `Activity`、服务和内容提供程序将对系统不可见且永远不会运行。不过广播接收器可以在清单文件中声明或在代码中动态创建,并在系统中注册即可。
### 声明组件功能
当在应用的清单文件中声明 `Activity` 时,可以选择性加入声明 `Activity` 功能的 `Intent` 过滤器,以便响应来自其它应用的 `Intent`,可以使用 `` 元素作为组件声明元素的子项进行添加来为您的组件声明 `intent` 过滤器。
例如,电子邮件应用包含一个用于撰写新电子邮件的 `Activity`,则可以像下面这样声明一个 `Intent` 过滤器来响应“send” Intent(以发送新电子邮件):
```xml
...
```
如果另一个应用创建了一个包含 `ACTION_SEND` 操作的 `Intent`,并将其传递到 `startActivity()`,则系统可能会启动您的 `Activity`,以便用户能够草拟并发送电子邮件。
### 声明应用要求
例如,如果您的应用需要相机,并使用 Android 2.1(API 级别 7)中引入的 API,您应该像下面这样在清单文件中以要求形式声明这些信息:
```xml
...
```
现在,没有相机且 Android 版本低于 2.1 的设备将无法从 Google Play 安装您的应用。
不过,您也可以声明您的应用使用相机,但并不要求必须使用。 在这种情况下,您的应用必须将 `required` 属性设置为 "false",并在运行时检查设备是否具有相机,然后根据需要停用任何相机功能。
## 应用资源
如果应用包含一个名为 `logo.png` 的图像文件(保存在 `res/drawable/` 目录中),则 SDK 工具会生成一个名为 `R.drawable.logo` 的资源 ID,可以利用它来引用该图像并将其插入用户界面。
================================================
FILE: Android/问题汇总.md
================================================
# Andriod 问题汇总
## 设备
新购了一台开发机 meizu 15。之前有考虑过小米6,但京东和淘宝上都没有找到靠谱的卖家,接着看了华为和三星,华为的低端机的外观实在是不敢恭维,三星的 A9 让我惊艳了一番,但价格有些稍贵,最后逛了 meizu,发现居然有了我当初高一高二时火爆的 meizu MX2 外观类似的机型!由于情怀因素,就购置了 meizu 15。
## 开发者模式
Andriod 与 iOS 不一致的地方在于设备默认是“不可调试”的,必须打开“开发者模式”后才能进行调试,在 meizu 15 上,打开开发者模式的流程如下:
* 设置-关于手机。滑动到最底下,找到“Andriod 版本”,连续点击 7 下,即可看到“已经打开开发者模式”的 toast 提示;
* 设置-辅助功能-开发者选项(上一步未完成是看不到的)。开启开发者选型,且启动“USB 调试”。
================================================
FILE: Back-end/DB.md
================================================
# 数据库相关知识点
## 备份
* 先写日志,再写 SQL。这样可以保证当 SQL 写入出现问题时,可以查到日志。
### 备份策略
周日 | 周一 | 周二 | 周三 | 周四 | 周五 | 周六 |
--- | --- | --- | --- | --- | --- | --- |
完全备份|增量备份|增量备份|增量备份|差量备份|增量备份|增量备份|
周四差量备份可以保证当周五或周六出现问题时,不用一次次的反复回复一二三的增量备份。
================================================
FILE: Back-end/Docker.md
================================================
# Docker
## 虚拟化和容器化技术
### 虚拟化技术
虚拟化技术是一种将计算机物理资源进行抽象、转换为虚拟的计算机资源提供给程序使用的技术。这些资源包括了 CPU 提供的运算控制资源,硬盘提供的数据存储资源,网卡提供的网络传输资源等。
#### 跨平台
保证程序跨平台兼容,也就是要保证操作系统或物理硬件所提供的接口调用方式一致,程序便不需要兼容不同硬件平台的接口。此时突然想到,使用 `Swift` 编写 iOS app 时,构建出包后总是会带上 `Swift` 的整个运行时,以保证随着 iOS 系统版本的升级 app 的正常运行,因其 `ABI` 并未稳定,还不能内置在操作系统中。
#### 资源管理
可将虚拟化技术运用于计算机资源的管理,其中最实用的就是“虚拟内存”虚拟化技术能够提高计算机资源的使用率,是指利用虚拟化,可以将原来程序用不到的一些资源拿出来,分享给另外一些程序,让计算机资源不被浪费。
### 虚拟化技术的分类
主要分为两大类:**硬件虚拟化**和**软件虚拟化**。
* 硬件虚拟化:比如假设 iOS 基于的 arm 架构 CPU 能够运行基于 x86 架构的 macOS 应用程序,这是因为 CPU 能够将另外一个平台的指令集转换为自身的指令集执行(但实际上并不可能)。
* 软件虚拟化:在 2018 WWDC 中,宣布可以在 `UIKit` 层面提供一部分把 iOS app 转移到 macOS app 中的特性,可以理解为是 Apple 在 Xcode 层面协助开发者构建了迁移代码,帮开发者解决了不同平台指令的转换。也就是说,软件虚拟化实际上是通过一层夹杂在应用程序和硬件平台上的虚拟化实现软件来进行指令的转换。
其它虚拟化技术的分类:
* **平台虚拟化**:在操作系统和硬件平台间搭建虚拟化设施,使得整个操作系统都运行在虚拟后的环境中。类似 `VMware`、`PD`;
* **应用程序虚拟化**:在操作系统和应用程序间实现虚拟化,只让应用程序运行在虚拟化环境中。类似 `Python` 的虚拟环境;
* **内存虚拟化**:将不相邻的内存区,甚至硬盘空间虚拟成统一连续的内存地址,即虚拟内存;
* **桌面虚拟化**:让本地桌面程序利用远程计算机资源运行,达到控制远程计算机的目的。类似华为云的云桌面以及各种远程桌面控制软件,如 Teamviewer。
* ......
### 虚拟机
虚拟机通常说法是通过一个**虚拟机监视器( Virtual Machine Monitor )** 的设施来隔离操作系统与硬件或应用程序和操作系统,以达到虚拟化的目的。这个虚拟机监视器,通常被称为:**`Hypervisor`**。
虚拟机有一个永远都逃不掉的问题:性能低下。这种效率的低下有时候是无法容忍的,故真实的虚拟机程序常常不完全遵守 `Hypervisor` 的设计结构,而是引入一些其它技术来解决效率低下问题,比如解释执行、即时编译(Just In Time)运行机制,但这些技术的引入已不属于虚拟化的范畴了。
### 容器技术
按分类或者实现方式来说,容器技术应该属于**操作系统虚拟化**,也就是在由操作系统提供虚拟化的支持。总的来说,容器技术指的是操作系统自身支持一些接口,能够让应用程序间可以互不干扰的独立运行,并能够对其在运行中所使用的资源进行干预。
那这也不应该被称为“容器”呀?是的,这里所谓的容器指的是由于应用程序的运行被隔离在了一个独立的运行环境之中,这个独立的运行环境就好似一个容器,包裹了应用程序。
容器这么火爆,火到一心扑在 iOS 上的我都要好好梳理一番,很重要的一个原因是其在运行性能上远超虚拟机等其它虚拟化实现,甚至在运行效率上与真实运行在物理平台的应用程序不相上下。但注意,容器技术并没有进行指令转换,运行爱容器中的应用程序自身必须支持在真实操作系统上运行,也就是必须遵守硬件平台的指令规则。
曾经看到一篇文章说 `linux` **内核命名空间**的改进,直接推动了容器的最大化发展。
> 利用内核命名空间,从进程 ID 到网络名称,一切都可在 Linux 内核中实现虚拟化。新增的用户命名空间“使得用户和组 ID 可以按命名空间进行映射。对于容器而言,这意味着用户和组可以在容器内部拥有执行某些操作的特权,而在容器外部则没有这种特权。”Linux 容器项目 (LXC) 还添加了用户亟需的一些工具、模板、库和语言绑定,从而推动了进步,改善了使用容器的用户体验。LXC 使得用户能够通过简单的命令行界面轻松地启动容器。(来源 `redhat` 官网)
容器由于没有虚拟操作系统和虚拟机监视器这两个层次,大幅减少了应用程序带来的额外消耗。所以在容器中的应用程序其实完全运行在了宿主操作系统中,与其它真实运行在其中的应用程序在指令运行层面是完全没有任何区别的。
## `Docker` 的核心组成
### 四大组成对象
#### 镜像
可以理解为一个只读的文件包,其中包含了虚拟环境运行的最原始文件系统的内容。
因为 `Docker` 采用 `AUFS` 作为底层文件系统的实现,实现了一种**增量式**的镜像结构。每次对镜像内容修改,`Docker` 都会将这些修改铸造成一个镜像层,而一个镜像本质上是由其下层所有的镜像层所组成的,而每一个镜像层单独拿出来,都可以与它之下的镜像层组成一个镜像。正是由于这种结构,`Docker` 的镜像本质上是无法被修改的,因为所以的镜像修改只会产生新的镜像,而不是更新原有的镜像。
#### 容器
在容器技术中,容器是用来隔离虚拟环境的基础设施,但在 `Docker` 中,被引申为隔离出来的虚拟环境。如果我们把镜像理解为类,则容器为实例对象。镜像内存放的是不可变化的东西,当以他们为基础的容器启动后,容器内也就成为类一个“活”的空间。
`Docker` 的容器应该有三项内容组成:
* 一个 `Docker` 镜像;
* 一个程序运行环境;
* 一个指令集合。
#### 网络
在 `Docker` 中可对每个容器进行单独的网络配置,也可对各个容器间建立虚拟网络,将数个容器包裹其中,同时与其它网络环境隔离,并且 `Docker` 还能在容器中构造独立的 `DNS`,我们可以在不修改代码和配置的前提下直接迁移容器。
#### 数据卷
在以往的虚拟机中,大部分情况下都直接使用虚拟机的文件系统作为应用数据等文件的存储位置,但并未是完全安全的,当虚拟机或容器出现问题导致文件系统无法使用时,虽可直接通过快速的镜像进行重制文件系统以至于恢复,但数据也就丢失了。
为保证数据的独立性,通常会单独挂在一个文件系统来存放数据,得意与 `Docker` 底层的 `Union File System` 技术,我们可以不用管类似于搞定挂载在不同宿主机中实现的方法、考虑挂载文件系统兼容性、虚拟机操作系统配置等问题。
## 镜像与容器
### `Docker` 镜像
所有的 `Docker` 镜像都是按照 `Docker` 所设定的逻辑打包的,也是收到 `Docker Engine` 所控制。常见的虚拟机镜像都是由其它用户通过各自熟悉的方式打包成镜像文件,公布到网上再被其它用户所下载后,恢复到虚拟机中的文件系统中,但 `Docker` 的镜像必须通过 `Docker` 来打包,也必须通过 `Docker` 下载或导入后使用,不能单独直接恢复成容器中的文件系统。这样,我们就可以直接在服务器之间传递 `Docker` 镜像,并配合 `Docker` 自身对镜像的管理功能,使得在不同的机器中传递和共享变得非常方便。
每一个记录文件系统修改的镜像层 `Docker` 都会根据它们的信息生产一个64位的 `hash` 码,正是因为这个编码,可以能够区分不同的镜像层并保证内容和编码是一致的,我们可以在镜像之间共享镜像层。当 `A` 镜像依赖了 `C` 镜像,且 `B` 镜像也依赖了 `C` 镜像,在实际使用过程中,`A` 和 `B` 两个镜像是可以公用 `C` 镜像内部的镜像层的。
#### 查看镜像
```
$ docker images
```
#### 镜像命名
可以分为三部分:
* **username**:一般都是镜像创作者,但如果不写则是由官方进行维护。
* **repository**:一般都是该镜像中所包含的软件名。但镜像名归镜像名,镜像归镜像,`Docker` 对容器的设计和定义是微型容器而不是庞大臃肿的完整环境,所有通常只会在一个容器中运行一个应用程序,能够大幅降低程序之间互相的影响,利用容器技术控制每个程序所使用的资源。
* **tag**:
#### 主进程
在 `Docker` 的设计中,容器的生命周期与容器中 `PID` 为 1 这个进程由密切的关系,容器的启动本质上可以理解为这个进程的启动,而容器的停止也就意味着这个进程的停止。
### 写时复制
通过镜像运行容器时并不是立即把镜像里所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 `UnionFS` 将镜像以只读方式挂载到沙盒文件系统中,只有在容器对文件的修改时,修改才会体现到沙盒环境上。
## 从镜像仓库获得镜像
### 获取镜像
```
docker pull ubuntu
```
### 获取镜像更详细的信息
```
docker inspect ubuntu
```
### 搜索镜像
```
docker search django
```
### 删除镜像
```
docker rmi ubuntu
```
## 运行和管理容器
### 容器的生命周期
* **Created**
* **Running**
* **Paused**
* **Stopped**:容器的停止状态下,占用的资源和沙盒环境都存在,只是容器中的应用程序均已停止
* **Deleted**
#### 创建容器
```
$ docker create ubuntu
```
如果我们之前选择的 `docker pull` 容器并不是默认的 `latest` 版本,而是手动选择了一个版本,那镜像的名字将会比如 `nginx:1.12`,对于后续的操作都十分的不方便,对此,我们可以采用 `--name` 进行重命名:
```
$ docker create --name nginx nginx:1.12
```
#### 启动容器
```
$ docker start ubuntu
```
通过 `docker run` 可将上述两个命令进行合并:
```
$ docker run --name nginx nginx:1.12
```
以上命令跑起来的容器运行都是运行在前台,如果我们想要容器运行在后台,可以通过 `-d`,其是 `-detach` 的简称,告诉 `Docker` 在启动后将程序和控制进行分离。:
```
$ docker run -d ubuntu
```
#### 管理容器
列出运行中的所有容器
```
$ docker ps
```
列出所有容器
```
$ docker ps -a/-all
```
其中打印出的列表需要注意的是 **STATUS** 字段,常见的状态表示有三种:
* **Create**:容器已创建,没有启动过;
* **Up[ Time ]**:容器正在运行,[ Time ] 代表从开始运行到查看时的时间;
* **Exited([ Code ]) [ Time ]**:容器已结束运行,[ Code ] 表示容器结束运行时,主程序返回的程序退出码,而 [ Time ] 则表示容器结束到查看时的时间。
#### 停止和删除容器
```
$ docker stop ubuntu
```
容器停止后,其维持的文件系统沙盒环境会一直保存,内部被修改的内容也会被保留。通过 `docker start` 将容器继续启动。
当需要把容器完全删除容器,可以使用:
```
$ docker rm ubuntu
```
但在运行中的容器默认情况下是不能被删除的,但我们可以通过以下命令进行删除:
```
$ docker rm -f ubuntu
```
#### 随时删除容器
`Docker` 与其它虚拟机不同,其所定位的轻量级设计讲究随用随开,随关随删,当我们短时间内不需要使用容器时,最佳的做法是删除它而不是仅仅停止它。
如果我们要对程序做一些环境配置,完全可以直接将这些配置打包至一个新的镜像中,下次直接使用该镜像创建容器即可。对于一些重要的文件资料,不能随着容器的删除而删除,可以使用 `Docker` 中的**数据卷**来单独存放。
### 进入容器
#### 直接创建,进入
```
$ docker run -it --name ubuntu ubuntu
```
#### 已经创建完成,进入
```
$ docker exec -it ubuntu /bin/bash
```
* `-i` 表示保持我们的输入流;
* `-t` 表示启用一个伪终端,形成我们与 bash 的交互。
当容器运行在后台,想要在将当前的输入输出流连接到指定的容器上,可以这么做:
```
$ docker attach ubuntu
```
通过 `docker attach` 启动的容器,可以理解为与 `docker run -d` 做了相反的事情,把当前容器从后台拉回了前台。
## 为容器配置网络
### 容器网络
在 `Docker` 网络中,有三个比较核心的概念,形成了 `Docker` 的网络核心模型,即**容器网络模型(Container Network Model)**:
* 沙盒:提供容器的虚拟网络栈。比如端口套接、`IP` 路由表、防火墙等;
* 网络:`Docker` 内部的虚拟子网,网络内的参与者相互可见并能够进行通讯。需要注意的是,这种虚拟网络与宿主机存在隔离关系。
* 端点:主要目的是形成一个可以控制的突破封闭网络环境的出入口,当容器的端点与网络的端点形成配对后,就如同在这两者之间搭建了桥梁,可进行数据传输。
### `Docker` 的网络实现
目前官方提供了五种网络驱动:
* Bridge Driver(default):通过基于硬件或软件的网桥来实现通讯
* Host Driver
* Overlay Driver:借助 `Docker` 的集群模块 `Docker Swarm` 来搭建的跨 `Docker Daemon` 网络,可以通过它搭建跨物理主机的虚拟网络,从而让不同物理机中运行的容器感知不到多个物理机的存在。
* MacLan Driver
* None Driver
### 容器互联
让一个容器连接到另外一个容器,可以通过 `docker create` 或 `docker run` 创建时通过 `--link` 选项进行配置。例如,创建一个 `Mysql` 容器,将运行的 `web` 容器连接到这个 `Mysql` 容器中:
```shell
$ sudo docker run -d --name PJMysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes mysql
$ sudo docker run -d --name webapp --link mysql webapp:latest
```
网络已经打通,在 `web` 应用程序中连接到 `Mysql` 数据库可以使用 `Docker` 提供的简便方式,只需要通过**容器的网络命名**填入到连接地址中即可访问需要连接的容器,连接地址中的 `PJMysql` 类似于域名解析,`Docker` 会将其指向 `Mysql` 容器的 `IP` 地址,从此映射 `IP` 的工作就交给 `Docker` 完成了!以 `Django` 配置文件为例:
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'pigpen',
'USER': 'pigpen',
'PASSWORD': 'pigpen_2018',
# 在此次填入 mysql 容器的网络命名,我的是 PJMysql
'HOST': 'PJMysql',
'PORT': '3306',
'ATOMIC_REQUESTS': True,
}
}
```
#### 暴露端口
容器与容器间的网络打通了,但我们还是不能访问已经连接容器中的任何服务。`Docker` 为容器网络增加了一套**安全机制**,只有容器自身允许的端口,才能被其它容器所访问。这个容器自我标记端口可被访问的过程,通常称为`暴露端口`。通过 `docker ps` 命令可以看到容器暴露给其它容器访问的端口(`PORTS` 字段下将会列出):
```shell
PJ@localhost:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e1c348025713 ubuntu "/bin/bash" 6 days ago Up 6 days ubuntu
```
如果想增加容器对外暴露的端口,可以在容器创建时使用 `--expose` 选项进行增加:
```shell
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
```
但还需要注意的是,容器中所暴露出的端口可以认为我们只是打开了容器的防火墙,能否通过这个端口去访问容器中的服务还需要容器中的应用监听并处理来自这个端口的请求,比如我们虽然打开了 `Nginx` 容器的 `443` 端口,并不意味着该容器能够直接对来自 `443` 端口的数据进行处理,需要在 `Nginx` 容器中的对应文件中进行配置处理。
#### 别名连接
`Docker` 还支持连接时使用别名来摆脱对容器名的限制:
```shell
$ sudo docker run -d --name webapp --link mysql:database webapp:latest
```
以使用 `JDBC`进行数据库连接的配置为例,对 `Mysql` 容器进行别名设置后,可以改为:
```java
String url = "jdbc:mysql://database:3306/webapp";
```
### 网络管理
容器之间能够相互连接的前提是两者处于同一个网络之中,这里网络概念可以理解为 `Docker` 所处的虚拟子网,而容器网络沙盒可以看作是虚拟的主机,只有当多个主机在同一个子网里时,才能互相看到并进行网络数据的交换。
当我们启动一个 `Docker` 服务时,默认会给我们创建一个 `bridge` 网络,而我们创建的容器如果不显式指定网络的情况下都会连接到这个网络上。通过 `docker inspect` 命令查看容器,可以在打印出的信息中看到容器网络相关的信息:
```shell
$ PJ@localhost:~$ docker inspect ubuntu
"NetworkSettings": {
"Bridge": "",
"SandboxID": "a70241c1c304d46e60ca2ee4e95df7474cf1318e316ef104abe87d22b68588bb",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {},
"SandboxKey": "/var/run/docker/netns/a70241c1c304",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "41e89e7a31382f5a28e9c0e5618ab37cc6427430e6b83f24936c263f74a81381",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:02",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "482a88e80b5585ef83a0b5bcd41eb3550aa4056bd22fd49884ded618be9bbe80",
"EndpointID": "41e89e7a31382f5a28e9c0e5618ab37cc6427430e6b83f24936c263f74a81381",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
}
```
在打印出的信息中,我们可以看到该容器在 `bridge` 网络中所分配的 `IP` 地址、自身的端点、`Mac` 地址、`bridge` 网络的网关地址等信息。
#### 创建网络
`Docker` 也能够创建网络,形成自己定义虚拟子网的目的。`Docker` 里与网络相关的命令都以 `docker network` 开头,使用 `docker network create` 来创建网络:
```shell
sudo docker network create -d bridge PJNetwork
```
通过添加 `-d` 选项可以为新的网络指定驱动类型,可以是之前所提及的 `bridge`、`host`、`overlay`、`maclan`、`none`,也可以是其它网络驱动插件所定义的类型,当我们不指定网络驱动时,`Docker` 也会默认采用 `Bridge Driver` 作为网络驱动。
通过 `docker network ls/list` 可以查看 `Docker` 中已存在的网络,我的如下所示:
```shell
PJ@localhost:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
482a88e80b55 bridge bridge local
4a3f4ba8daf8 host host local
2f6f7bb9f46e none null local
```
在创建容器时,可以通过 `--network` 来指定容器所加入的网络,一旦该选项参数被指定,容器则不会再加入到 `bridge` 该网络中,但后续仍可通过 `--network bridge` 使其加入:
```shell
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network PJNetwork mysql:5.7
```
### 端口映射
如果我们需要在容器外通过网络访问容器中的应用,比如提供了 `web` 服务,那就需要提供一种方式访问运行在容器中的 `web` 应用。在 `Docker` 中,提供了**端口映射**的功能来实现。
通过 `Docker` 的端口映射功能,可以把容器的端口映射到宿主操作系统的端口上,当从外部访问宿主操作系统的端口时,数据请求就会自动发送给与之关联的容器端口。在创建容器时,可以使用 `-p/--publish` 选项来指定映射端口。
```shell
$ sudo docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
```
使用端口映射选项的格式是 `-p ::`,其中 `ip` 是宿主操作系统的监听 `ip`,可以用来控制监听的网卡,默认为 `0.0.0.0`,也就是监听所有网卡。`host-port` 和 `container-port` 分别表示映射到宿主操作系统的端口和容器的端口,这两者是可以不一样的,我们可以将容器的 `80` 端口映射到宿主操作系统的 `8080` 端口,传入 `-p 8080:80` 即可。
## 管理和存储数据
#### 挂载方式
基于底层存储实现,`Docker` 提供了三种适用于不同场景的文件系统挂载方式:
* **Bind Mount**:将宿主操作系统中的目录和文件挂载到容器内的文件系统中,通过指定容器外的路径和容器内的路径,形成挂载映射关系,在容器内外对文件的读写,都是相互可见的。
* **Volume**:
* **Tmpfs Mount**
================================================
FILE: Back-end/RESTful.md
================================================
# REST
`HTTP` 是一种**应用层**协议,能从 `HTTP` 基础设施中获取多少收益,主要取决于把它用做应用层协议用得有多好。
`HTTP` 实际上是为 `REST` 而生的,它能够表达状态和状态转移,者就是它位于应用层而非传输层的原因。
## 使用统一接口
### 如何保持交互的可见性
可见性是 `HTTP` 的一个核心特征。可见性是“一个组件能够对其他两个组件之间的交互进行监视或仲裁的能了”。当协议是可见的时,缓存、代理、防火墙等组件就可以监视甚至参与其中。
`HTTP` 通过一下途径来实现可见性:
* `HTTP` 的交互是无状态的,任何 `HTTP` 中介都可以推断出给定请求和响应的意义,而无须关联过去或将来的请求和响应。
* `HTTP` 使用一个统一接口,包括有 `OPTIONS`,`GET`,`HEAD`,`POST`,`DELETE` 和 `TRACE` 方法。接口中的每一个方法操作一个且仅有一个资源。每个方法的语法和含义不会因应用程序或资源的不同而发生改变。
* `HTTP` 使用一种与 [`MIME`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types) 类似的信封格式进行表述编码。这种格式明确区分标头和内容。标头时可见的,除了创建、处理消息的部分,软件的其他部分都可以不用关心消息的内容。
对于 `RESTful web` 服务,主要目标是尽可能保持可见性。保持可见性非常简单,使用 `HTTP` 方法时,其语义要与 `HTTP` 所规定的语义保持一致,并添加适当的标头来描述请求和响应。
## HTTP 方法的安全性和幂等性
### 安全性
安全性并不意味着服务器每次都必须返回同一结果。它只是表明客户端可以发送请求,并指导它不会改变资源的状态。
### 幂等性
幂等性保证客户的发起多次请求获取到的结果和一次请求获取到的结果一致。
================================================
FILE: Back-end/Vapor.md
================================================
# Vapor
在这里将记录使用 Vapor 的过程中遇到的问题。感觉特别一些设计模式的 tips 杂糅在一起后,就特别像 `Django`。
## 如何快速开始
### 下载 `vapor`
[详见官网](https://docs.vapor.codes/3.0/install/macos/)。
### 运行 `Hello, world!`
* `vapor new yourProjectName`。创建模版工程,当然可以加上 `--template=api` 来创建提供对应服务的模版工程,但我测试了一下好像没什么区别。
* `vapor xcode`。创建 Xcode 工程,特别特别慢,而且会有一定几率失败。
### MVC
`Vapor` 默认是 `SQLite` 的**内存**数据库。我原本想看看 `Vapor` 自带的 `SQLite` 数据库中的表,但没翻着,最后想了一下,这是内存数据库啊,也就是说,每次 `Run` 数据都会被清空。可以从 `config.swift` 中看出:
```swift
// ...
let sqlite = try SQLiteDatabase(storage: .memory)
// ...
```
在 `Vapor` 文档中写了推荐使用 `Fluent` ORM 框架进行数据库表结构的管理,刚开始我们并不了解关于 `Fluent` 的任何内容,可以查看模版文件中的 `Todo.swift`:
```swift
import FluentSQLite
import Vapor
final class Todo: SQLiteModel {
/// 唯一标识符
var id: Int?
var title: String
init(id: Int? = nil, title: String) {
self.id = id
self.title = title
}
}
/// 实现数据库操作。如增加表字段,更新表结构
extension Todo: Migration { }
/// 允许从 HTTP 消息中编解码出对应数据
extension Todo: Content { }
/// 允许使用动态的使用在路由中定义的参数
extension Todo: Parameter { }
```
从模版文件中的 `Model` 可以看出来创建一张表结构相当于是**描述一个类**,之前有使用过 `Django` 的经验,看到 `Vapor` 的这种 ORM 这么 `Swifty` 确实眼前一亮。`Vapor` 同样可以遵循 `MVC` 设计模式进行构建,在生成的模版文件中确实是基于 `MVC` 去做的。
#################### 没写完
### 从 `SQLite` 到 `Mysql`
这部分 `Vapor` 官方文档讲的不够系统,虽然都点到了但是过于分散,而且我感觉 `Vapor` 的文档是不是跟 Apple 学了一套,细节都不展开,遇到一些字段问题得亲自写下代码,然后看实现和注释,不写之前你是很难知道在描述什么。
#### `Package.swift`
在 `Package.swift` 中写下对应的依赖,
```swift
import PackageDescription
let package = Package(
name: "Unicorn-Server",
products: [
.library(name: "Unicorn-Server", targets: ["App"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
// here
.package(url: "https://github.com/vapor/fluent-mysql.git", from: "3.0.0"),
],
targets: [
.target(name: "App",
dependencies: [
"Vapor",
"FluentMySQL"
]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"])
]
)
```
触发更新
```shell
vapor xcode
```
`Vapor` 搞了我几次,更新依赖的时候特别慢,而且还更新失败,导致我现在每次更新时都要去确认一遍依赖是否更新成功。
#### 更新 ORM
更新成功后,我们就可以根据之前生成的模版文件 `Todo.swift` 的样式改成 `MySQL` 版本的 ORM:
```swift
import FluentMySQL
import Vapor
/// A simple user.
final class User: MySQLModel {
/// The unique identifier for this user.
var id: Int?
/// The user's full name.
var name: String
/// The user's current age in years.
var age: Int
/// Creates a new user.
init(id: Int? = nil, name: String, age: Int) {
self.id = id
self.name = name
self.age = age
}
}
/// Allows `User` to be used as a dynamic migration.
extension User: Migration { }
/// Allows `User` to be encoded to and decoded from HTTP messages.
extension User: Content { }
/// Allows `User` to be used as a dynamic parameter in route definitions.
extension User: Parameter { }
```
其实改动的地方只有两个,`import FluentMySQL` 和继承自 `MySQLModel`。
#### 修改 `config.swift`
```swift
import FluentMySQL
import Vapor
/// 应用初始化完会被调用
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
// === mysql ===
// 首先注册数据库
try services.register(FluentMySQLProvider())
// 注册路由到路由器中进行管理
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
// 注册中间件
// 创建一个中间件配置文件
var middlewares = MiddlewareConfig()
// 错误中间件。捕获错误并转化到 HTTP 返回体中
middlewares.use(ErrorMiddleware.self)
services.register(middlewares)
// === mysql ===
// 配置 MySQL 数据库
let mysql = MySQLDatabase(config: MySQLDatabaseConfig(hostname: "", port: 3306, username: "", password: "", database: "", capabilities: .default, characterSet: .utf8mb4_unicode_ci, transport: .unverifiedTLS))
// 注册 SQLite 数据库配置文件到数据库配置中心
var databases = DatabasesConfig()
// === mysql ===
databases.add(database: mysql, as: .mysql)
services.register(databases)
// 配置迁移文件。相当于注册表
var migrations = MigrationConfig()
// === mysql ===
migrations.add(model: User.self, database: .mysql)
services.register(migrations)
}
```
注意 `MySQLDatabaseConfig` 的配置信息。如果我们的 mysql 版本在 **8** 以上,目前只能选择 `unverifiedTLS` 进行验证连接MySQL容器时使用的安全连接选项,也即 `transport` 字段。在代码中用 `// === mysql ===` 进行标记的代码块是跟模版文件中使用 `SQLite` 所不同的地方。
#### 运行
运行工程,打开 mysql 进行查看。
```shell
mysql> show tables;
+----------------------+
| Tables_in_unicorn_db |
+----------------------+
| fluent |
| Sticker |
| User |
+----------------------+
3 rows in set (0.01 sec)
mysql> desc User;
+-------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| age | bigint(20) | NO | | NULL | |
+-------+--------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)
```
`Vapor` 不像 `Django` 那般在生成的表加上前缀,而是你 ORM 类名是什么,最终生成的表名就是什么,这点很喜欢!
### 对表字段的修改
`Vapor` 没有像 `Django` 那么强大的工作流,很多人都说 `Perfect` 像 `Django`,我自己的认为 `Vapor` 像 `Flask`。
对 `Vapor` 修改表字段,不仅仅只是修改 `Model` 属性这么简单,同样也不像 `Django` 中修改完后,执行 `python manage.py makemigrations` 和 `python manage.py migrate` 就结束了,我们需要自己创建迁移文件,自己写清楚此次表结构到底发生了什么改变。
在泊学的[这篇文章](https://boxueio.com/series/vapor-fluent/ebook/473)中推荐在 `App` 目录下创建一个 `Migrations group`,方便操作。但我思考了一下,这么做势必会造成 `Model` 和对应的迁移文件割裂,然后在另外一个上级文件夹中又要对不同迁移文件所属的 `Model` 做切分,这很显然是有一些问题的。最后,我脑子冒出了一个非常可怕的想法:“`Django` 是一个非常强大、架构非常良好的框架!”。
所以,最后我的目录是这样的:
```shell
Models
└── User
├── Migrations
│ ├── 19-04-30-AddUserCreatedTime.swift
│ └── 19-04-30-DeleteUserNickname.swift
├── UserController.swift
└── User.swift
```
这是 `Django` 中的一个 `app` 文件树:
```shell
user_avatar
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20190303_2154.py
│ ├── 0002_auto_20190303_2209.py
│ ├── 0003_auto_20190303_2154.py
│ ├── 0003_auto_20190322_1638.py
│ ├── 0004_merge_20190408_2131.py
│ └── __init__.py
├── models.py
├── tests.py
├── urls.py
└── views.py
```
已经删除掉了一些非重要信息。可以看到,`Django` 的 `app` 文件夹结构非常好!注意看 `migrations` 文件夹下的迁移文件命名。如果开发能力不错的话,我们是可以做到与业务无关的 `app` 发布供他人直接导入到工程中。
不过关于工程文件的管理,这是一个智者见智的事情啦~对于我个人来说,我反而更加喜欢 `Vapor`/`Flask` 一系,因为需要什么再加什么,整个设计模式也可以按照自己的喜好来做。
#### 删除一个表字段
使用 `Swift` 开发服务端很容易受到使用 `Swift` 做其它开发的影响。刚开始时我确实认为在 `Model` 中把需要删除的字段删除就好了,然而运行工程后去查数据库发现并不是这么一回事。
首先,我们需要先创建一个文件来写 `Model` 的迁移代码,但这不是必须的,你可以把该 `Model` 后续需要进行表字段的 CURD 都写在同一个文件中,因为没一个迁移都是一个 `struct`。我的做法是像上文所说,对每一个迁移都做新文件,并且每一个迁移文件都写上“时间”和“做了什么”。
```swift
import FluentMySQL
struct DeleteUserNickname: MySQLMigration {
static func prepare(on conn: MySQLConnection) -> EventLoopFuture {
return MySQLDatabase.create(User.self, on: conn, closure: {
$0.field(for: \.id, isIdentifier: true)
$0.field(for: \.nickname)
})
}
static func revert(on conn: MySQLConnection) -> EventLoopFuture {
return MySQLDatabase.delete(User.self, on: conn)
}
}
```
发现了一个问题,如果我们从 `Model` 中已经提前删除掉了需要移除的字段,那么在 `migrations` 中,这个字段就没法被索引,因为已经被移除了,那么就无法被 `deleteField`。最终我的解决办法是,因为这个字段已经不需要了,那么直接写 SQL 删除掉这个字段。
隐约觉得,这不是 `Vapor` 的最佳实践。
#### 增加/修改一个表字段
```swift
import FluentMySQL
struct AddUserCreatedTime: MySQLMigration {
static func prepare(on conn: MySQLConnection) -> EventLoopFuture {
return MySQLDatabase.update(User.self, on: conn, closure: {
$0.field(for: \.fluentCreatedAt)
})
}
static func revert(on conn: MySQLConnection) -> EventLoopFuture {
return MySQLDatabase.delete(User.self, on: conn)
}
}
```
需要注意的是,不管你是要做 CURD 中的任何一个功能,你都需要实现 `prepare` 和 `revert` 两个方法,`revert` 方法的作用是用于撤销 `prepare` 方法中的逻辑。
### Auth
在 `Vapor` 中有两种对用户鉴权的方式。一为适用 `API` 服务的 `Stateless` 方式,二为适用于 `Web` 的 `Sessions`,
#### 添加依赖
```swift
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "Unicorn-Server",
products: [
.library(name: "Unicorn-Server", targets: ["App"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-mysql.git", from: "3.0.0"),
// 添加 auth
.package(url: "https://github.com/vapor/auth.git", from: "2.0.0"),
],
targets: [
.target(name: "App",
dependencies: [
"Vapor",
"SwiftyJSON",
"FluentMySQL",
// 添加 auth
"Authentication"
]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"])
]
)
```
执行 `vapor xcode` 拉取依赖并重新生成 `Xcode` 工程。
#### 注册
在 `config.swift` 中增加:
```swift
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
// ...
try services.register(AuthenticationProvider())
// ...
}
```
#### Basic Authorization
简单来说,该方式就是验证密码。我们需要维护一个做 `Basic Authorization` 方式进行鉴权的 `Path` 集合。请求属于该集合中的 `Path` 时,都需要把用户名和密码用 `:` 进行连接成新的字符串,且做 `base64` 加密,例如,`username` 为 `pjhubs`,`password` 为 `pjhubs123`,则,拼接后的结果为 `pjhubs:pjhubs123`,加密完的结果为 `cGpodWJzOnBqaHViczEyMw==`。按照如下格式添加到每次发起 `HTTP` 请求的 `header` 中:
```
Authorization: Basic cGpodWJzOnBqaHViczEyMw==
```
#### Bearer Authorization
当用户登录成功后,我们应该返回一个完整的 `token` 用于标识该用户已经在我们系统中登录且验证成功,并让该 `token` 和用户进行关联。使用 `Bearer Authorization` 方式进行权限验证,我们需要自行生成 `token`,可以使用任何方法进行生成,`Vapor` 官方并没有提供对应的生成工具,只要能够保持全局唯一即可。每次进行 `HTTP` 请求时,把 `token` 按照如下格式直接添加到 `HTTP request` 中,假设此次请求的 `token` 为 `pxoGJUtBVn7MXWoajWH+iw==`,则完整的 `HTTP header` 为:
```
Authorization: Bearer pxoGJUtBVn7MXWoajWH+iw==
```
#### 创建 `Token` Model
```swift
import Foundation
import Vapor
import FluentMySQL
import Authentication
final class Token: MySQLModel {
var id: Int?
var userId: User.ID
var token: String
var fluentCreatedAt: Date?
init(token: String, userId: User.ID) {
self.token = token
self.userId = userId
}
}
extension Token {
var user: Parent {
return parent(\.userId)
}
}
// 实现 `BearerAuthenticatable` 协议,并返回绑定的 `tokenKey` 以告知使用 `Token` Model 的哪个属性作为真正的 `token`
extension Token: BearerAuthenticatable {
static var tokenKey: WritableKeyPath { return \Token.token }
}
extension Token: Migration { }
extension Token: Content { }
extension Token: Parameter { }
// 实现 `Authentication.Token` 协议,使 `Token` 成为 `Authentication.Token`
extension Token: Authentication.Token {
// 指定协议中的 `UserType` 为自定义的 `User`
typealias UserType = User
// 置顶协议中的 `UserIDType` 为自定义的 `User.ID`
typealias UserIDType = User.ID
// `token` 与 `user` 进行绑定
static var userIDKey: WritableKeyPath {
return \Token.userId
}
}
extension Token {
/// `token` 生成
static func generate(for user: User) throws -> Token {
let random = try CryptoRandom().generateData(count: 16)
return try Token(token: random.base64EncodedString(), userId: user.requireID())
}
}
```
#### 添加配置
在 `config.swift` 中写下 `Token` 的配置信息。
```swift
migrations.add(model: Token.self, database: .mysql)
```
#### 修改 `User` Model
让 `User` 和 `Token` 进行关联。
```Swift
import Vapor
import FluentMySQL
import Authentication
final class User: MySQLModel {
var id: Int?
var phoneNumber: String
var nickname: String
var password: String
init(id: Int? = nil,
phoneNumber: String,
password: String,
nickname: String) {
self.id = id
self.nickname = nickname
self.password = password
self.phoneNumber = phoneNumber
}
}
extension User: Migration { }
extension User: Content { }
extension User: Parameter { }
// 实现 `TokenAuthenticatable`。当 `User` 中的方法需要进行 `token` 验证时,需要关联哪个 Model
extension User: TokenAuthenticatable {
typealias TokenType = Token
}
extension User {
func toPublic() -> User.Public {
return User.Public(id: self.id!, nickname: self.nickname)
}
}
extension User {
/// User 对外输出信息,因为并不想把整个 `User` 实体的所有属性都暴露出去
struct Public: Content {
let id: Int
let nickname: String
}
}
extension Future where T: User {
func toPublic() -> Future {
return map(to: User.Public.self) { (user) in
return user.toPublic()
}
}
}
```
#### 路由方法
使用 `Basic Authorization` 方式做用户鉴权后,我们就可以把需要使用鉴权的方法和非鉴权的方法按照如下方式在 `UserController.swift` 文件分开进行路由,如果这个文件你没有,需要新建一个。
```swift
import Vapor
import Authentication
final class UserController: RouteCollection {
// 重载 `boot` 方法,在控制器中定义路由
func boot(router: Router) throws {
let userRouter = router.grouped("api", "user")
// 正常路由
let userController = UserController()
router.post("register", use: userController.register)
router.post("login", use: userController.login)
// `tokenAuthMiddleware` 该中间件能够自行寻找当前 `HTTP header` 的 `Authorization` 字段中的值,并取出与该 `token` 对应的 `user`,并把结果缓存到请求缓存中供后续其它方法使用
// 需要进行 `token` 鉴权的路由
let tokenAuthenticationMiddleware = User.tokenAuthMiddleware()
let authedRoutes = userRouter.grouped(tokenAuthenticationMiddleware)
authedRoutes.get("profile", use: userController.profile)
authedRoutes.get("logout", use: userController.logout)
authedRoutes.get("", use: userController.all)
authedRoutes.get("delete", use: userController.delete)
authedRoutes.get("update", use: userController.update)
}
func logout(_ req: Request) throws -> Future {
let user = try req.requireAuthenticated(User.self)
return try Token
.query(on: req)
.filter(\Token.userId, .equal, user.requireID())
.delete()
.transform(to: HTTPResponse(status: .ok))
}
func profile(_ req: Request) throws -> Future {
let user = try req.requireAuthenticated(User.self)
return req.future(user.toPublic())
}
func all(_ req: Request) throws -> Future<[User.Public]> {
return User.query(on: req).decode(data: User.Public.self).all()
}
func register(_ req: Request) throws -> Future {
return try req.content.decode(User.self).flatMap({
return $0.save(on: req).toPublic()
})
}
func delete(_ req: Request) throws -> Future {
return try req.parameters.next(User.self).flatMap { todo in
return todo.delete(on: req)
}.transform(to: .ok)
}
func update(_ req: Request) throws -> Future {
return try flatMap(to: User.Public.self, req.parameters.next(User.self), req.content.decode(User.self)) { (user, updatedUser) in
user.nickname = updatedUser.nickname
user.password = updatedUser.password
return user.save(on: req).toPublic()
}
}
}
```
需要注意的是,如果某个路由方法需要从 `token` 关联的用户取信息才需要 `let user = try req.requireAuthenticated(User.self)` 这行代码取用户,否则如果我们仅仅只是需要对某个路由方法进行鉴权,只需要加入到 `tokenAuthenticationMiddleware` 的路由组中即可。
#### 修改 `config.swift`
最后,把我们实现了 `RouteCollection` 协议的 `userController` 加入到 `config.swift` 中进行路由注册即可。
```swift
import Vapor
public func routes(_ router: Router) throws {
// 用户路由
let usersController = UserController()
try router.register(collection: usersController)
}
```
================================================
FILE: Back-end/django.md
================================================
这个文章主要是记录我在学习django过程中所遇到的问题,为后续其它个人项目做铺垫,之前陆陆续续的在使用djano,但是一直都没有好好的去记录一些内容,借此机会完整的记录下学习过程遇到的问题和注意点。django最适合用于做网站,从django官网上的slogan看的出来了。至于django有多适合进行网站开发估计得要接着后续的学习才能知道了,不过我用django主要还是用与做api服务,在考虑之中的还有flask,flask比django更加简单,其中[这个repo](https://github.com/windstormeye/watchDog)是基于flask做的api服务。
### 创建一个django项目
```django-admin startproject <项目名称>```
### __init__.py文件
python包文件必须包含的,只要有这个文件,说明该Python文件目录下的所有文件都是一个包。
### 启动本地服务
```python manage.py runserver```
### 通过域名访问(dev environment)
```python manager.py runserver 0.0.0.0:8000```并且需要把域名在setting.py中的`ALLOWED_HOSTS`字段中。
### 创建超级管理员
忘记命令`manage.py`提供的命令,可以采用`python manage.py help`进行查看。
可以通过`python manage.py createsuperuser`创建创建超级管理员
### 创建一个django app
```django-admin startproject <项目名称>```
### 同步数据库
制造迁移(迁移数据库可以只保存这个)```python manage.py makemigrations```
迁移```python manage.py migrate```
### path
写url时最好是通过path方式,而不是用正则走`re_path/url`的方式,因为会更加直观一些。
### 异常
eg:
```python
try:
article = Artcle.objects.get(id=article_id)
except Artcle.DoesNotExist:
return HttpResponse("不存在”)
```
### 模板
前端页面和后端代码进行分离,降低耦合性。django中规定了模板文件(html)的存放位置和格式,在project下的setting.py文件中的`TEMPLATES`可以看到且修改,如果采用默认配置,需要在app中新建`templates`文件夹,在其中写好对应的模板文件,
eg:这是循环打印文章列表的模板例子,
```html
{% for article in articles %}
{{ article.title }}
{% endfor %}
{{article_obj.title}}
{{article_obj.content}}
```
### 模板渲染
使用`django.shortcuts`app中的`render/render_to_response/get_objects_or_404`都可以进行模板渲染和调起。
### response的部分写法
这是一种返回数据的写法:
```python
def article_details(request, article_id):
try:
article = Artcle.objects.get(id=article_id)
context = {}
context['article_obj'] = article
return render(request, "article_details.html", context)
except Artcle.DoesNotExist:
raise Http404("not exist")
return HttpResponse("文章标题:%s
文章内容:%s" % (article.title, article.content))
```
这是另外一种,使用from django.shortcuts import get_object_or_404
```python
def article_details(request, article_id):
article = get_object_or_404(Artcle, pk=article_id)
context = {}
context['article_obj'] = article
return render(request, "article_details.html", context)
```
这样做会简洁很多,这也是django所推崇的做法。
### 模板中列出所有文章
列出所有文章。模板中可以采用`{/article/{{ article.pk }}`或者`{% url ‘article_list’ article.pk %}`,用第二种方法需要在对应的`urls.py`文件中添加对应的path.name。
### 分离url
很多时候我们不应该把所有的路由设置都放在project下的`urls.py`文件中,最佳的做法应该是把对应的url放在各自的app`urls.py`文件中(需新建),做法如下所示:
```python
# project `urls.py`
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('article/', include('artcle.urls'))
]
```
```python
# article `urls.py`
from django.urls import path, include
from . import views
urlpatterns = [
path('', views.article_list, name="article_list"),
path('', views.article_details, name="article_details"),
]
```
千万要注意对应文件的路径!!!
### Python定制类
通过使用定制类中的__str__给用户定制显示内容,当然python中的定制类不止只有这些,详情可以参考[廖雪峰](https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013946328809098c1be08a2c7e4319bd60269f62be04fa000)
```python
def __str__(self):
return "Article: %s" % self.title
```
### 在admin页面中显示字段
想要在admin管理页面中显示字段,可以在对应app的`admin.py`中这么做:
```py
class ArticleAdmin(admin.ModelAdmin):
list_display = ("title", "content”)
# admin注册(其中的一种方法)
admin.site.register(Artcle, ArticleAdmin)
```
### 给admin管理页面某个字段排序
```py
class ArticleAdmin(admin.ModelAdmin):
list_display = ("id", "title", "content")
ordering = ("id", )
```
ordering默认是升序,改为-id,则为降序排序
### 给现有模型添加时间字段。
`from django.utils import timezone`,第一第二种需要这个库。
1. 如果是直接写。`created_time = models.DateTimeField()`并在对应app中的models.py文件中添加进了新字段,运行时会告知模型和数据库匹配不上,需要去同步数据库,当同步数据库的时候,系统添加默认值,两种方法,第一种在terminal中直接添加,第二种退出然后再添加默认值。
2. 同上第二种,退出后再添加默认值。`created_time = models.DateTimeField(default=timezone.now)`
3.
```py
created_time = models.DateTimeField(auto_now_add=True)
last_updated_time = models.DateTimeField(auto_now=True)
```
### 修改时间类型。
在project的`setting.py`文件中找到`TIME_ZONE`字段,`TIME_ZONE = 'Asia/Shanghai’`(不知道为啥没有北京时间
### 使用自带用户类型。
导入`from django.contrib.auth.models import User`。给每篇文章添加作者信息,外键。 `author = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=1)`
### 文章删除。
最好不要真的删除,而是作为用户不可见。文章模型添加字段`is_deleted = models.BooleanField(default=False)`。文章`views.py`的添加数据筛选
```py
def article_list(request):
articles = Artcle.objects.filter(is_deleted=False)
context = {}
context['articles'] = articles
return render_to_response("article_list.html", context)
```
### 下载virtualenv
`pip install virtualenv`
下载过程中若出现如下信息:
```shell
Traceback (most recent call last):
File "/usr/bin/pip3", line 11, in
sys.exit(main())
File "/usr/lib/python3/dist-packages/pip/__init__.py", line 215, in main
locale.setlocale(locale.LC_ALL, '')
File "/usr/lib/python3.5/locale.py", line 594, in setlocale
return _setlocale(category, locale)
locale.Error: unsupported locale setting
```
则是因为语言配置所导致的,执行如下命令即可:
```shell
$ export LC_ALL=C
```
目的是为了去除所有本地化的设置,让命令能够正确执行。`LC_ALL` 它是一个宏,如果该值设置了,则该值会覆盖所有LC_*的设置值。注意,LANG的值不受该宏影响。`C` 是系统默认的locale,"POSIX"是"C"的别名。所以当我们新安装完一个系统时,默认的locale就是C或POSIX。
### 创建虚拟环境
```py
$ cd env
$ source ./bin/activate
```
### 内容显示截断。
`{{ blog.content|truncatechars:30 }}
`,也可以使用`truncatewords`,但是要求词和词中间要有空格,针对的是一个个的词,英文可以直接用。
### 模板文件
模板文件(一般都是html)如果是跟着项目走,那就应该放到全局模板文件里,如果是跟着app走应该放到app的模板文件里。
### html中标签的id一般写在class前面
### django的命名空间
在app中新建的static文件夹中再新建一个跟app名字一样的文件夹,然后把需要的静态文件放进行。注意:要重启服务器(CSS没效果也可以重启)
### 加载静态文件的声明
{% load staticfiles %}要放在需要用到的静态文件之前,而不是拆开。
```python
{% load staticfiles %}
{% block header_extends %}
{% endblock %}
```
### bootstrap和jquery最好都down下来,毕竟也不是特别大。使用bootstrap推荐直接上官网查资料。www.bootcss.com
### python2的range()出来后是个list,而python3得到的是个生成器
### filter筛选符合条件,exclude筛选不符合条件。
### 条件中的双下划线:字段查询类型、外键拓展(以博客分类为例)、日期拓展(以月份分类为例)、支持链式查询:可以一直链接下去
### 去除该段文本中的h5比标签内容,只保留文本
`{{ blog.content|striptags|truncatechars:120 }}
。`
### 过滤器safa显示h5标签内容
`{{ blog.content|safe }}
`
### 富文本编辑库。django-ckeditor
### admin中文简体——zh-hans
### django2.0后url的设置可以直接用path
### pypi.org。可以查到相关的pip库
### 框架引用顺序:先是python自带,后是django自带,最后才是自己写的
----
## 事物
`Django` 中实现事物主要有两种方式:一是通过 `Django ORM` 框架的事物处理;另外一种是基于原生执行 `SQL` 语句的 `transaction` 处理。
至于为什么需要做事物管理,简单来说就是需要对用户提交的本次操作做完整性保证,有可能用户提交的本次操作涉及10多条 `SQL` 语句,但如果执行到其中第七第八条时出现问题后,之前执行的却已经被写入数据库中了,按道理说,如果中途出现错误,应该把之前的以及执行完的语句全都撤回。具体细节可参考我的[另一篇文章](http://pjhubs.com/2018/03/25/软件开发项目实践(一)/)
据官方文档中所描述的内容[https://docs.djangoproject.com/zh-hans/2.0/topics/db/transactions/](https://docs.djangoproject.com/zh-hans/2.0/topics/db/transactions/) ,`Django` 中的默认事物级别为 `auto-commit` ,用文档中举的例子来说,当我们在 `Django` 中做的 `model().save()` 和 `model().delete` 操作所有的改动都会立即提交,没有 `rollback` 。
### `Django ORM` 框架的事物处理
1. **将 `http request` 数据库操作全都包括。** 这种做法相当的简单粗暴,具体配置如下:
```json
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': '',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
'ATOMIC_REQUESTS': True, // !!!
}
}
```
这种方法是在 `django` 调用每个视图方法前都启动一个事物,并且要保证该响应没有任何问题, `django` 最终才会提交这个事物;如果在这其中出现了其它问题, `django` 会自动的回滚该事物。 这样会非常的简单,但不适合流量大的服务,因为这要对每个视图函数都要开启事物,而且回滚的只是数据库操作,如果本次操作中途涉及到了一些其它操作,比如执行了到了一半发送了邮件,但是下一步操作后发现有错误要回滚,但邮件此时已经发送出去了,没法回滚除了数据库之外的操作。
2. **中间件拦截。** 这个办法是我最开始采用的,但无奈一直 `Run` 不起来,需要添加上这个中间件 `django.middleware.transaction.TransactionMiddleware` 。我在 django 官方文档中一直没找到这个中间件,估计在 2.0 被丢弃了吧。
3. **手动通过装饰器进行管理。** 这是官方文档中描述最清楚的内容,也是最方便的内容,具体的细节可以直接去看[文档](https://docs.djangoproject.com/zh-hans/2.0/topics/db/transactions/)。因为现在项目还有很多变化的地方,等后续业务逻辑稳定了再使用该方法。
### 原生 SQL 语句的处理
使用这种方法基本上就是存粹的手写 `SQL` 语句了,如果我们需要做更多细致的东西可以直接采用这种做法。通过连接定义一个游标 `cursor`,通过 `cursor` 执行sql语句,最后通过 `transaction.commit_unless_managed()` 来提交事务。
## 其它
需要注意的地方是,使用原生 SQL 处理事物和使用 django ORM 框架进行处理的区别在于,如果视图函数中出现的问题是视图函数本身而不是数据库的问题,那么使用 django ORM 框架也会回滚,而使用原生 SQL 处理却不会。
### 数据库表生成
写完 model 后,需要把对应的 app 放到 setting 配置文件中。
### 重新生成表结构
很多时候当我们在修改 django 的 model 结构时,会因为某些“特殊”情况(搞不懂为啥)没有更新数据库表结构,这个时候可以参考如下做法:
1. 把数据库中对应的表删除;
2. 把 `django_migrations` 表中 `app` 对应字段中的 model 删除;
3. 重新执行 `makemigrations` 和 `migrate` 。
### 让媒体文件(图片)可被访问
按照正常的 django 流程做图片(或其它资源文件)上传即可,如最终生成的 json 格式如下:
```json
{
avatar = "/media/avatar/pjhubs.jpg";
"masuser_id" = 7028492784;
}
```
此时在浏览器中直接访问 `http://hostname/media/avatar/pjhubs.jpg` ,是访问不到资源文件的,需要这么做:
1. 在 project app 下(与 settings.py 同级别)的 url.py 文件中添加:
```py
from . import settings
from django.conf.urls.static import static
```
2. 在末尾添加上:
```py
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```
可根据自己的要去进行设置,此时就可通过 `http://hostname/media/avatar/pjhubs.jpg` 访问到资源文件了。
### request.POST 接收不到客户端发送的参数
Django 的 `request.POST` 方法获取到的 POST 方法参数只支持 `Content-Type` 类型为:
* `multipart/form-dat`
* `application/x-www-form-urlencoded`
其它类型的 `Content-Type` 通过 `request.POST` 方法获取到的参数列表均为空。但一般来说我们都希望在 POST 请求中参数类型为 `json`,所以我们需要让客户端同学把 POST 请求的 `Content-Type` 设置为 `application/json` 类型。
在 iOS 的一个常用网络请求框架 `AFNetworking` 中,默认的 `POST` 请求 `Content-type` 就是为 `application/json`。
### Django 如何做 `OR` 查询
使用 `Q` 对象,不可使用 `filter`,因为 Django 默认为 `AND` 关系。
**注意:**
* 如果涉及到多参数时,`Q` 对象应该在前,其它参数在后。
### 搜索
原本是想基于 `ES` 来一套搜索全家桶的,但无奈 `ES` 太重了,基于 `django-haystack` 最后完成了需求,相关配置如下:
* 下载相关依赖
```shell
pip install django-haystack whoosh
```
* 创建相关文件
- `settings.py`
添加 `app`
```python
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'haystack',
# 在所有自定义 app 之上
]
```
```python
# 配置全文搜索
# 指定搜索引擎
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
},
}
# 指定如何对搜索结果分页,这里设置为每 10 项结果为一页,默认是 20 项为一页
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 20
# 添加此项,当数据库改变时,会自动更新索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
```
**注意:** 需要给 `settings.py` 中设置好 `templates` 文件夹目录
```python
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# 重点
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
```
- `tempaltes` 文件夹。估计要新建,最终创建出的目录层级为:
```shell
├── templates
└── search
└── indexes
└── user
└── xxx_text.txt
```
xxx 即为需要创建搜索索引的 `app` 名称,若有大小写,则全小写即可。该 `txt` 文件写下需要进行被索引的字段即可,`objct` 即为 `xxx` 的传入对象实体,不需要修改。
```python
{{ object.nick_name }}
```
- `models.py` 需要做索引 `app`。在 `app` 的目录下创建新文件 `search_indexes.py`,作为索引类
```python
from .models import MasUser
from haystack import indexes
class MasUserIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True,
use_template=True)
# 需要搜索的字段
nick_name = indexes.CharField(model_attr='nick_name')
def get_model(self):
return MasUser
def index_queryset(self, using=None):
return self.get_model().objects.all()
```
- `views.py` 中的使用。
```python
@decorator.request_methon('GET')
@decorator.request_check_args(['s_nick_name'])
def searchFriend(request):
nick_name = request.GET.get('s_nick_name')
users = SearchQuerySet().models(MasUser).filter(nick_name__contains=nick_name)
f_users = []
for user in users:
f_users.append(user.object.toJSON())
json = {
'users': f_users
}
return utils.SuccessResponse(json, request)
```
- 建立索引
```shell
python manage.py rebuild_index
```
最后自己配置一下路由即可。
### uwsgi 安装失败
`sudo apt-get install python3.6-dev`
### `python manage.py makemigrations` 的触发
使用 `python manage.py makemigrations` 命令来触发数据库表的生成和更新,需要保证在 app 目录下有 `migrations` 这个包。注意,这里说的是包!包!包!!!
### 如何给 `DecimalField` 类型的模型字段赋值
```python
from decimal import Decimal
current_drink_score.score = Decimal(str(10))
```
### 在同一局域网下,如何让其他电脑连接上自己的 `django` 服务器
* 查找到自己的 ip 地址。
* 在 django 的 `ALLOWED_HOSTS` 中添加上该 ip 。
* 通过 `python runserver ip:port` 允许项目即可。
或者直接修改 pyCharm 中的 web 服务器工程配置:

### 如何做「不包含」/「不等于」操作
```python
from django.db.models import Q
myapps = App.objects.filter(~Q(name= ''))
```
### 如果出现了本地的 `django_migrations` 和服务器的 `django_migrations` 记录不一致
导致执行 `migrate` 时各个 app 的上下游依赖出现问题,报错:
```shell
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration user_avatar.0001_initial is applied before its dependency user.0002_auto_20190705_2356 on database 'default'.
```
这是因为在 `django_migrations` 表中有可能因为 `user_avatar.0001_initial` 已经有记录了,且依赖 `user.0002_auto_20190705_2356`,但此时 `user.0002_auto_20190705_2356` 并未生成。
解决办法:把 `user_avatar.0001_initial` 这条记录删掉,但非常不好。
### 直接 copy 别人的 django 文件夹
如果是直接 copy 了别人的 django 文件夹,可能会出现它的 env 与本机的各种不匹配问题,需要删除重新生成
================================================
FILE: Back-end/jwt.md
================================================
# JWT
这里将会记录我在学习 JWT 的过程中遇到的问题、思考和总结。
## JWT 简介
全称为 **J**SON **W**eb **T**oken。
## 彩虹表
================================================
FILE: Back-end/mysql.md
================================================
# Mysql
## Mysql 的客户端/服务器架构
`mysql` 的服务器进程默认名称为 `mysqld`,常用的 `mysql` 客户端进程的默认名称为 `mysql`。
## 启动
`mysql -h主机名 -u用户名 -p密码`
* `-h/--host=主机名`:后可跟服务器域名或 IP 地址,若 mysql 服务器进程运行在本机则可以省略;
* `-u/--user=用户名`;
* `-p/--password=密码`。
### 如果 mysql 默认端口号 3306 被占用
在启动 `mysql` 服务器时使用 `mysqld -P3307`,启动 `mysql` 客户端程序时使用 `mysql -u root -P3307 -p`,打写的 `-P` 代表选择
## 服务器处理客户端请求
### 连接管理
每当有一个客户端连接到 `mysql` 服务器进程时,服务器进程都会创建一个线程来处理与客户端的交互。当该客户端与服务器断开连接时,服务器并不会立即把该线程销毁,而是缓存起来供下一次使用。但线程开辟太多会影响系统性能,需要限制同时连接服务器的客户端数量。
### 查询缓存
`mysql` 服务器会把已经处理过的查询请求和结果缓存起来,若下一次有相同的请求时,则从缓存中直接返回。但如果两个查询请求在任何自负上的不同,都会导致缓存不会命中。
`mysql` 缓存系统会检测当前缓存中设计到的每张表,只要该表的结果或者数据被修改,则该表的所有高速缓存都将变为无效并删除。**从 mysql 8.0 中已移除**。
## 存储引擎
表,是有一行一行的记录组成的,但这只是逻辑上的概念,在物理上如何表示记录,怎么从表中读取数据,怎么把数据写入具体的物理存储器上,这都是存储引擎做的事情。我常用的 mysql 存储引擎为具备外键支持的事务存储引擎 **`InnoDB`** 。
其它常见的 `mysql` 存储引擎有:
存储引擎 | 描述
---- | ---- s
MEMORY | 置于内存的表
MyISAM | 主要的非事务处理存储引擎
### 设置表的存储引擎
存储引擎负责对表中的数据进行提取和写入工作的,可以**为不同的表设置白虎通你的存储引擎**,不同的表可以有不同的物理存储结构,不同的提取和写入方式。
## 启动选项和配置文件
* mysql 服务器默认的客户端连接数量为:151;
* 表的默认存储引擎为:`InnoDB`;
* 通过 `-h` 方式来启动服务器程序时,客户端和服务器进程之间使用 `TCP/IP` 网络进行通信,如果想要禁止这种方式,可以在启动服务器命令选项中加上 `--skip_networking` 来禁止使用 `TCP/IP` 方式来进行连接。
================================================
FILE: Back-end/nginx.md
================================================
# nginx
## 错误日志地址
`etc/log/nginx`
##
================================================
FILE: Back-end/web服务器.md
================================================
# 浅析 Web 服务器的工作原理(Java)
## 什么是 Web 服务器,应用服务器和 web 容器?
### web 服务器
在过去很长的一段时间中,它们是有区别的,但是这两个分类慢慢的合并了,而如今在大多情况下可以把它们看成一个整体。在早期,引发出“ web 服务器”的概念是因为通过了 HTTP 协议来提供静态页面内容和图片的服务,当时大部分内容都是静态的,并且 HTTP 1.0 只是一种传送文件的方式,但在不久后 web 服务器提供了 CGI 功能,意味着我们能够为每一个 web 请求启动一个进程来产生动态内容。现在 HTTP 协议已经非常成熟了并且 web 服务器变得更加复杂,拥有了例如缓存、安全和 session 管理等这些附加功能。

### 应用服务器
在同一时期,应用服务器已经存在并发展了很长一段时间了,大部分产品都指定了“封闭的”产品专用通信协议来互连胖客户端和服务器,在 90 年代,传统的应用服务器产品开始嵌入了 HTTP 通信功能,准备利用网关来实现。不久之后这两者的界限开始变得模糊。同时,web 服务器变得越来越成熟,可以处理更高的负载、更多的并发和拥有更好的特性;应用服务器开始添加越来越多的机遇 HTTP 的通信功能。所有的这些导致了 web 服务器与应用服务器的界线变得更窄了。
目前,应用服务器和 web 服务器之间的界线已经变得模糊不清了,但是人们还把这两个术语分开来。当有人说到 web 服务器时,我们通常要把它认为是以 HTTP 为核心、web UI 为向导的应用。当有人说到应用服务器时,你可能想到“高负载、企业级特性、事物和队列、多通道通行(HTTP 和更多的协议)”,但现在提供这些需求的基本上都是同一个产品。
### web 容器
在 java 中,web 容器一般就是指 Servlet 容器。Servlet 容器是与 Java Servlet 交互的 web 容器的组件。web 容器负责管理 Servlet 的生命周期、把 URL 映射到特定的 Servlet 、确保 URL 请求拥有正确的访问权限和更多类似的服务。综合来看,Servlet 容器就是用来运行你的 Servlet 和维护它的生命周期的运行环境。

### 什么是 Servlet?它们有什么用?
在 java 中,Servlet 使你能够编写根据请求动态生成内容的服务端组件。事实上,Servlet 是一个在 javax.servlet 包里定义的接口。它为 Servlet 的生命周期声明里三个基本方法 —— init()、service() 和 destroy() 。每个 Servlet 都要实现这些方法(在 SDK 里定义或者用户定义)并在它们的生命周期的特定时间由服务器来调用这些方法。
类加载器通过懒加载或者预加载自动地把 Service 类加载到容器里,每个请求都拥有自己的线程,而一个 Service 对象可以同时为多个线程服务。当 Service 对象不再被使用时,它就会被 JVM 当作垃圾回收掉。
### 什么是 ServletContext?它由谁创建
当 Servlet 容器启动时,它会部署并加载所有的 web 应用。当 web 应用被加载时,Servlet 容器会一次性为每个应用创建 Servlet 上下文(ServletContext)并把它保存在内存里。Servlet 容器会处理 web 应用的 web.xml 文件,并且一次性创建在 web.xml 文件里定义的 Servlet、Filter 和 Listener ,同样也会把它们保存在内存里。当 Servlet 容器关闭时,它会卸载所有的 web 应用和 ServletContext ,所有的 Servlet、Filter 和 Listener 实例都会被销毁。
从 java 的文档中可知,ServletContext 定义了一组方法,Servlet 使用这些方法来与它的 Servlet 容器进行通信。例如,用来获取文件的 MIME 类型、转发请求或编写日志文件。在 web 应用的部署文件(deployment describtor)标明“分布式”的情况下,web 应用的每一个虚拟机都拥有一个上下文实例。在这种情况下,不能把 Servlet 上下文当作共享全局信息的变量(因为它的信息已经不具有全局性了)。可以使用外部资源来代替,比如数据库。
### ServletRequest 和 ServletResponse 从哪里进入生命周期?
Servlet 容器包含在 web 服务器中,web 服务器监听来自特定端口的 HTTP 请求,这个端口通常是 80 。当客户端发送一个 HTTP 请求时,Servlet 容器会创建新的 HttpServletRequest 和 HttpServletResponse 对象,并且把它们传递给已经创建的 Filter 和 URL 模式与请求 URL 匹配的 Servlet 实例的方法,所有的这些都使用同一个线程。
request 对象提供了获取 HTTP 请求的所有信息的入口,比如请求头和请求实体。response 对象提供了控制和发送 HTTP 响应的便利方法,比如设置请求头和请求实体。response 对象提供了控制和发送 HTTP 响应的便利方法,比如设置响应头和响应实体(通常是 JSP 生成的 HTML 内容)。当 HTTP 响应被提交并结束后,request 和 response 对象都会被销毁。
### 如何管理 Session?cookie 呢?
当客户端第一次访问 web 应用或第一次使用 request.getSession() 获取 HttpSession 时,Servlet 容器会创建 Session,生成一个 long 类型的唯一 ID (可以使用 session.getId() 获取它)并把它保存在服务器的内存里。Servlet 容器同样会在 HTTP 响应里设置一个 Cookie ,cookie 的名字是 JSESSIONID 并且 cookie 的值是 session 的唯一 ID 。
根据 HTTP cookie 规范(正规的 web 浏览器和 web 服务器必须遵守的约定),在 cookie 的有效期间,客户端(浏览器)之后的每个请求都要把该 cookie 返回给服务器,Servlet 容器会利用带有名为 JSESSIONID 的 cookie 检测每一个到来的 HTTP 请求头,并使用 cookie 的值从服务器内容里获取相关的 HttpSession 。
HttpSession 会一直存活着,除非超过一段时间没使用。可以在 web.xml 文件中设置该时间段,默认时间段是 30 分钟。因此,如果客户端已经超过 30 分钟没有访问 web 应用的话,Servlet 容器就会销毁 Session 。之后的每一个请求,即使带有特定的 cookie ,都再也不会访问到同一个 Session 了,ServletContainer 会创建一个新的 Session 。
另外,在客户端的 session cookie 拥有一个默认的存活时间,这个时间与浏览器的运行时间相同,因此,当用户关闭浏览器后(所以的标签或窗口),客户端的 Session 就会被销毁,重新打开浏览器后,与之前的 Session 关联的 cookie 就再也不会被发送出去了。再次使用 request.getSession() 会返回一个全新的 HttpSession 并且使用一个全新的 session ID 来设置 cookie 。
### 如何确保线程安全?
我们现在已经知道了所有的请求都在共享 Servlet 和 Filter 。它是多线程的并且不同的线程(HTTP 请求)可以使用同一个实例。否则,对每一个请求都重新创建一个实体会耗费很多的资源。同样也要知道,不应该使用 Servlet 或者 Filter 的实例变量来存放任何的请求或者会话范围内的数据,这些数据会被其它 Session 的所有请求共享,这是非线程安全的。
================================================
FILE: Back-end/后端学习.md
================================================
## 用户态与核心态
### 用户态与核心态是什么?
将内核程序和基于内核程序之上构建的用户程序分开处理,使其分别运行在用户态和核心态
### 为什么需要用户态和核心态?
在CPU的所有指令中,有一些指令是非常危险的(不是`rm -rf`这种),如果错用,将导致整个系统奔溃,比如:清空内存,修改时钟等。如果所有的程序代码都能够直接使用这些指令,那么很有可能我们的系统一天将会死n次。
所以,CPU将指令分为 **特权指令** 和 **非特权指令** ,对于较为危险的指令,只允许操作系统本身及其相关模块进行调用,普通的、用户自行编写的应用程序只能使用那些不会造成危险的指令。intel的CPU将指令级别分为了4个等级,分别是:`RING0`, `RING1`, `RING2`, `RING3`。
当一个程序或一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称该程序、该任务处于内核态,此时处理器处于特权级别最高的`RING0`内核代码中执行,当进程处于内核态时,执行的内核代码将会使用该进程的内核栈,每个进程都拥有自己的内核栈。
当程序、进程在执行用户自己的代码时,我们就称该程序、该进程处在用户态,此时处理器处在特权级别最低的`RING3`用户代码中运行。
当正在执行用户程序而突然被中断程序(被打断了),此时用户程序也可以象征性的称为处于核心态,因为中断处理程序将使用当前进程的内核栈。
CPU总是处于以下状态中的一种:
* 内核态:运行于进程上下文,内核代表进程运行于内核空间;
* 内核态:运行于中断上下文,内核代表硬件运行于内核空间;
* 用户态:运行于用户空间。
### 什么是系统调用?
处在用户空间的应用程序,通过系统调用,进入内核空间。此时用户空间的进程要传递很多变量、参数给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等等。所谓的“进程上下文”,就是用户进程传递给内核的参数以及内核要保存的那一整套变量、寄存器值和当时的环境等。比如`fork()`开启一个新的进程。
### 什么是中断程序?
硬件通过触发信号,导致内核调用中断处理程序,进入内核的空间。在这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看做就是硬件传递的参数和内核需要保存的一些其它环境信息(被打断执行的进程环境)。
### 什么是内核?
操作系统大致经历了无结构操作系统(第一代)、模块化操作系统(第二代)、分层式操作系统(第三代),它们操作系统被称为“传统操作系统结构”,而单内核和微内核是目前“线代操作系统结构”中最重要的两个。比如Linux是单内核,windows和macOS都是微内核的。
操作系统的内核作用是对软件、硬件资源的管理。内核的最重要的一个作用就是实现任务调度,让计算机的资源被均衡的调度。如果一个操作系统没有内核,那么所有的任务(进程)都是完全独占计算机的,聊天的同时不能刷浏览器。
没有操作系统的内核,内存的动态管理也不存在了,后果就是可能没有虚拟内存,也就是说,如果此时运行的程序需要占用内存2G,但此时计算机只有1G内存,这个程序就跑不起来了。同时,设备的管理功能也就不存在了,比如动态设备的插拔(U盘)等等,而且计算机启动的时间会将会非常漫长,因为需要检测并加载各种驱动。
没有操作系统内核,多线程的操作都不能支持,因为信号量、消息队列这些都是操作系统提供的,所以如果真的把操作系统的内核剔除掉,现在大部分应用程序都要彻底重写来支持无内核的操作系统。并且多核CPU也发挥不了作用。
早期的操作系统有些游戏和软件就是直接拿软盘启动,是因为那时操作系统很小、功能弱、系统资源紧缺,没有什么内核也是OK的,但是现在计算机的硬件资源非常丰富,操作系统很庞大,没有内核进行统一管理的话,要么太浪费要么这个应用程序的负担(要处理的事情)会很重。
### 什么是虚拟内存?
以下内容摘自wiki,我没理解好虚拟内存的大概实现思路,先把wiki的结果放在着:
```
虚拟内存是电脑系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。
注意:虚拟内存不只是“用磁盘空间来扩展物理内存”的意思——这只是扩充内存级别以使其包含硬盘驱动器而已。把内存扩展到磁盘只是使用虚拟内存技术的一个结果,它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。对虚拟内存的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟内存地址”,以借此“欺骗”程序,使它们以为自己正在使用一大块的“连续”地址。
现代所有用于一般应用的操作系统都对普通的应用程序使用虚拟内存技术,例如文字处理软件,电子制表软件,多媒体播放器等等。老一些的操作系统,如DOS和1980年代的Windows,或者那些1960年代的大型机,一般都没有虚拟内存的功能——但是Atlas,B5000和苹果公司的Lisa都是很值得注意的例外。[1]
那些需要快速访问或者反应时间非常一致的嵌入式系统,和其他的具有特殊应用的电脑系统,可能会为了避免让运算结果的可预测性降低,而选择不使用虚拟内存。
```
在Linux中用户进程空间和内核进程控件占用的虚拟内存比例为3:1。32位机虚拟内存最高2^32 = 4 GB,64位机虚拟内存最高2^64 ≈ 16000 PB。
### 计算机启动,检测并加载各种驱动
差一篇讲计算器启动全流程的讲解。
### 中间件
中间件的概念挺容易理解,就是整体来看要把分布式集群之间联系起来,业界一直在发布新的轮子,知乎这篇文章供参考:[https://www.zhihu.com/question/19730582](https://www.zhihu.com/question/19730582)
### 并发和并行
引用:[https://www.zhihu.com/question/33515481](https://www.zhihu.com/question/33515481)
```
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』。
```
* 并发在于「发生」
* 并行在于「运行」
### 生产者消费者模式
找到一篇讲解得比较细致的文章,但是文章本身内容太长,不易于总结,但实际上前后语义又通畅,就不做摘要了,原文链接:[https://www.jianshu.com/p/0d1c950e6614](https://www.jianshu.com/p/0d1c950e6614)
### C10K问题
留给自己以后看,这个问题不是目前自己需要考虑的。[https://blog.csdn.net/yeasy/article/details/43152115](https://blog.csdn.net/yeasy/article/details/43152115)
### kill 掉对应端口信息
以 `8000` 端口为例:
```shell
lsof -i tcp:8000
```
================================================
FILE: Base/C++.md
================================================
### ends 的差异
ends 在 windows 和类 unix 系统下有差异,[http://www.cplusplus.com/reference/ostream/ends/](http://www.cplusplus.com/reference/ostream/ends/)上说的是加入了一个 '\0' ,而不是空格,之所以是空格是因为操作系统的原因
* Qt 的信号槽机制是动态插入代码
================================================
FILE: Base/UML.md
================================================
# UML
## 面向对象技术概述
面向对象的基本建模原则:抽象、封装、继承和分类。
面向对象的基本软件工程:OOA(面向对象的分析)、OOD(面向对象的设计)、OOP(面向对象的编程)和OOSM(面向对象的软件维护)
对象的概念是:对问题域中某个实体的抽象;类的概念是:对具有项目属性和行为的一个或多个对象的描述
属性的定义:描述对象静态特征的数据项;服务的定义:描述对象的动态特征(行为)的一个操作序列。
类的定义要包括:名称、属性和操作三要素。
面向对象呈现设计的三大特性:封装、继承和多态。
面向对象的系统分析要确立 3 个系统模型是对象模型、功能模型和动态模型。
## 用例图
### 参与者
**参与者**是指系统以外的、需要使用系统或与系统交互的外部实体,包括**人、设备、外部系统**等。
### 用例
用例是对一个活动者使用系统的一项功能时所进行的交互过程的一个文字描述序列。可以说,软件开发的过程是**用例驱动**的。
用例是对系统行为的动态描述,属于 UML 的动态建模部分。UML 中的建模机制包括**静态建模**和**动态建模**两部分,其中静态建模机制包括类图、对象图、构件图和部署图;动态建模机制包括用例图、顺序图、协作图、状态图和活动图。
理论上可以把一个软件系统的所有用例都画出来,但实际开发过程中,进行用例分析时只需把那些**重要的、交互过程复杂的用例**找出来。不要试图把所有的需求都以用例的方式表示出来。需求有两种基本形式:功能性需求和非功能性需求。用例描述的只是功能性方面的需求,那些难以用 UML 表示的需求很多是非功能性需求。
#### 泛化关系
官方解释:泛化代表一般的与特殊的关系,在用例之间的泛化关系中,子用例继承了父用例的行为和含义,子用例也可以增加新的行为和含义或覆盖父用例中的行为和含义。
PJ 的解释:子类和父类的关系。

#### 包含关系
官方解释:包含关系指的是两个用例之间的关系,其中一个用例(基本用例)的行为包含了另一个用例(包含用例)的行为。
PJ 的解释:把某一个功能进行重用。
【例 1】银行的 ATM 系统中,有“存款”、“取款”、“账户余额查询”和“转账”四个用例,都要求用户必须登录了 ATM 机。也就是说,它们都包含了用户登录系统的行为。因此,用户登录系统的行为是这些用例中相同的动作,可以将它提取出来,单独的作为一个包含用例。
“存款”、“取款”、“查询用户余额”和“转账”是基本用例,“登录”是包含用例,如下图所示:

由于将共同的用户登录系统行为提取出来,“存款”、“取款”、“查询用户余额”和“转账”四个基本用例都不再含有用户登录系统的行为。
【例 2】网上购物系统,当注册会员在线购物时,网上购物系统需要对顾客的信用卡进行检查,检查输入的信用卡号是否有效,信用卡是否有足够的资金进行支付。

上图中有没有必要将检查信用的行为提取出来,单独构成一个用例(作为包含用例),当信用检查的行为只发生在“在线购物”活动中时,可以不用提取出来。当信用检查的行为还发生在其它活动中时,应该提取出来,以便实现软件重用。
#### 拓展关系
官方解释:在拓展关系中,对于拓展用例的执行有更多的规则限制,基本用例必须声明若干个“拓展点”,而拓展用例只能在这些拓展点上增加新的行为和含义。
PJ 的解释:基本用例在满足一定条件后可进行选择执行拓展用例。
【例 3】图书借阅系统。当读者还书时,如果借书时间超期,则需要缴纳一定的滞纳金,作为罚款。

#### 综合
【例 4】 网上购物系统,当注册会员浏览网站时,他可能临时决定购买商品,当他决定购买商品后,就必须将商品放进购物车,然后下订单。

如果网上购物系统的需求改为了:注册会员即可以直接在线购物,又可以浏览商品后临时决定在线购物,则可以改为下图所示:

### 用例描述
> 没有描述的用例就像是一本书的目录,人们只知道该目录的标题,但并不知道该目录的具体内容是什么,仅用图形符号表示的用例本身并不能提供该用例所具备的全部信息,必须通过文本的方式描述该用例的完整功能。实际上,用例的描述才是用例的主要部分,是后续的交互图分析和类图分析必不可少的部分。
用例描述了参与者和软件系统进行交互时,系统所执行的一系列动作序列,因此这些动作序列应该包含正常使用的各种动作序列(主事件流),而且还包含对非正常使用时软件系统的动作序列(子事件流)。
【例 1】 在银行 ATM 系统的 ATM 机上“取款”用例一个简单用例描述可以采取如下格式
描述项 | 说明
--- | ---
用例名称 | 取款。
用例描述 | 在储户账户有足够金额的情况下,为储户提供现金,并从储户账户中减去所取金额。
参与者 | 储户。
前置条件 | 储户正确登录系统。
后置条件 | 储户账户余额被调整。
主事件流 | (1)储户在主界面选择“取款”选项,**开始用例**(这个词的出现很重要)。(2)ATM 机提示储户输入欲取金额。(3)储户输入欲取金额。(4)ATM 确认该储户账户是否有足够的金额。如果金额不够,则执行子事件流 `b` 。**如果与主机连接有问题,则执行异常事件流 `e`**。(5)ATM 机从储户帐号中减去所取金额。(6)ATM 机向储户提供要取的现金。(7)ATM 机打印取款凭证。(8)进入主界面。ATM 机提供以下选项:存款、取款、查询和转账。**用例结束**(这个词的出现同样很重要)。
子事件流 `b` | b1. 提示储户余额不够。b2. 返回主界面,等待储户重新选择选项。
异常事件流 `e` | e1. 提示储户主机连接不上。e2. 系统自动关闭,退出储户银行卡,用例结束。
一个复杂用例主要体现在基本操作流程和可选操作流程的步骤和分之过多,此时,可以采用“场景(或称脚本)”的技术来描述用例,而不是用大量的分之和附属流来描述用例。
### 用例建模
用例模型主要应用在需求分析时使用。
#### 步骤
* 找出**系统外部**的参与者和外部系统,确定系统的边界和范围;
* 确定每一个参与者所期望的系统行为,参与者对系统的基本业务需求;
* 把这些**系统行为作为基本用例**;
* 区分用例的优先级;
* 细化用例。使用泛化、包含、拓展等关系进行处理;
* 编写每个用例的用例描述;
* 绘制用例图;
* 编写项目词汇表。
#### 确定系统边界
系统边界是指系统与系统之间的界限。系统可以认为是一系列的相互作用的元素形成的具有特定功能的有机整体。不属于这个有机整体的部分可以认为是**外部系统**。因此系统边界定义了**油谁或什么参与者来使用系统**,系统能够为参与者提供什么特定服务。**系统边界决定了参与者**。
【例 1】在一个仅为交易客户提供买卖基金的基金交易系统中,**参与者**为交易客户,交易客户能够操作的系统功能有买入基金和卖出基金。因此,系统有两个用例:买入基金和卖出基金。
进一步分析发现,基金的品种应该存在与该系统中,否则交易客户无法进行基金的买卖。但系统已存的两个用例都不能完成基金品种的管理,所以可以确认基金品种的管理应该在别的系统中完成。
所以,我们需要开发这个系统,仅存在两个用例:买入基金、卖出基金。

【例 2】对例 1 做个调整。在一个既提供基金买卖又提供基金品种录入的基金交易系统中,交易客户,能够进行基金的买入和卖出。因为还需要对基金品种进行管理(录入、修改、删除和查询),由基金公司员工进行操作。所以该系统的参与者有交易客户和基金公司员工。系统边界可以改为下图所示:

#### 如何确定参与者
* 谁将使用系统的主要功能?
* 谁将需要系统的支持来完成她们的日常工作?
* 谁将必须维护、管理和确保系统正常工作?
* 谁将给系统提供信息、使用信息和维护信息?
* 系统需要处理哪些硬件设备?
* 系统使用外部资源吗?
* 系统需要与其他系统交互吗?
* 谁对系统产生的结果感兴趣?
需要注意的问题:
* 只要是参与者,对于子系统而言都是外部的;
* 参与者直接与系统进行交互;
* 参与者指的与系统直接交互时所扮演的角色,而不是特定的人或事物。比如,不是 PJ 与教务系统产生交互而是学生与教务系统产生交互;
#### 如何确定用例
识别用例可以从列出的参与者列表中从头开始寻找,考虑每个参与者如何使用系统,需要系统提供什么样的服务。
* 参与者要向系统请求什么功能?
* 每个参与者的特定任务是什么?
* 参与者需要读取、创建、撤销、修改和存储系统的某些信息吗?
* 参与者是否有需要通知系统的事件?系统是否有需要通知参与者的事件?
* 这些事件代表了哪些功能?
* 系统需要哪些输入输出功能?
* 是否所有的功能需求都被用例使用了?
需要注意以下问题:
* 每个用例至少有一个参与者;
* 每个参与者至少一个用例;
* 如果存在没有参与者的用例,再三检查后,还是没有参与者,可以考虑把该用例并入其他用例中;
* 如果存在没有用例的参与者,再三检查后,该参与者还是没有用例,可以考虑该参与者是如何与系统产生交互的,或由该参与者确定一个新的用例,或实际上该参与者本身就是多余的。
#### 项目词汇表
这什么鬼,没见过,没听说过......
### 其他问题
#### 需求应该有层次的组织
系统的高层需求一版用不超过 12 个左右的用例进行表示,在其下的层次中,用例的数量不应超过当前用例的 5~10 倍。可以将用例划分为**业务用例**、**系统用例**和**组件用例**等。
#### 不要从用例直接推导设计
用例应该描述参与者使用系统所遵循的顺序,但用例绝不说明系统内部采用什么步骤来响应参与者的刺激。
### 用例包
#### 用例模型的调整
如果两个用例总是以同样的顺序被激活,可能需要将它们合并为一个用例。
#### 不要过于详细
在进行用例描述时还没有考虑系统的设计方案,那么也不会涉及用户界面。
================================================
FILE: Base/algorithm-java.md
================================================
1. `public static void`和`public void`的区别:`public static void`所标明的静态方法能够用类名直接调用,但是静态方法不能够调用非静态方法。
2. final关键字浅析:[https://www.cnblogs.com/dolphin0520/p/3736238.html](https://www.cnblogs.com/dolphin0520/p/3736238.html)
3. `const`和`static`关键词:
* [http://blog.sina.com.cn/s/blog_668aae780101m4ex.html](http://blog.sina.com.cn/s/blog_668aae780101m4ex.html)
* [https://www.jianshu.com/p/1598004e8215](https://www.jianshu.com/p/1598004e8215)
`const` 修饰为只读常量,只能初始化一次,`static` 修饰的变量和函数只能在当前模块或文件中可见。
4. 如何才能将一个`double`变量初始化为无穷大?使用java内置常数,`Double.POSITIVE_INFINITY`和`Double.NEGATIVE_INFINITY`
5. 能够将`double`类型的值和`int`类型的值相互比较吗?不通过类型转换不行!但是java一般会自动进行所需的类型转换,例如,如果`int x = 3`,则`(x < 3.1)`为`true`,java会在比较前将x转换为`double`类型(因为3.1为`double`类型的字面量)
6. java二维数组表示:`int[][] arr = { {1,2,3}, {4,5,6} };`
================================================
FILE: Base/leetCode.md
================================================
- [x][两数相加](./leetcode/两数相加.md)
- [x][两数之和](./leetcode/两数之和.md)
- [x][无重复字符的最长子串](./leetcode/无重复字符的最长子串.md)
- [x][两个排序数组的中位数](./leetcode/两个排序数组的中位数.md)
- [ ][最长回文子串](./leetcode/最长回文子串.md)
================================================
FILE: Base/leetcode/两个排序数组的中位数.md
================================================
## 两个排序数组的中位数
惊呆了,用自己最初的想法居然直接一把 AC 掉了困难题目,真不知道这困难是不是放错了哈哈哈,总之很开心就是了。刚开始想的巨多,一直在纠结怎么把两个有序的数组用一个较好的方法直接合并,然后又考虑到了题目是个有序数组,接着想到了用二分balabala,总之就是题还没开始写,我就已经想得乱七八糟,最后差点被自己吓屎去翻参考答案了,这又给了我一个提醒,做题之前确实是要好好的构思题目怎么来,但是要注意不要想太多,因为其实很多东西都是水到渠成的hhhh
```swift
func findMedianSortedArrays(_ nums1: [Int], _ nums2: [Int]) -> Double {
var finalArray: Array = []
var i = 0, j = 0
// 合并两个有序数组
while (i < nums1.count && j < nums2.count) {
if (nums1[i] < nums2[j]) {
finalArray.append(nums1[i])
i += 1
} else {
finalArray.append(nums2[j])
j += 1
}
}
// 添加剩余内容
while true {1`
if i < nums1.count {
finalArray.append(nums1[i])
i += 1
}
if j < nums2.count {
finalArray.append(nums2[j])
j += 1
}
if i >= nums1.count && j >= nums2.count {
break
}
}
// 返回中位数
if finalArray.count % 2 != 0 {
return Double(finalArray[finalArray.count / 2])
} else {
let v = (finalArray[finalArray.count / 2] + finalArray[finalArray.count / 2 - 1])
if v % 2 != 0 {
return Double(v / 2) + 0.5
}
return Double(v / 2)
}
}
```
================================================
FILE: Base/leetcode/两数之和.md
================================================
## TwoSum
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
```
Example:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].
```
### AC Code
```swift
class Solution {
func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
var final: [Int] = [0, 0]
var index: Int = 0
for _ in nums {
var index_i:Int = index+1
for _ in index+1.. [Int] {
var index = 0
var final:[Int] = [0 ,0]
for num in nums {
let tempNum = target - num
let tempNums = nums[index+1.. [Int] {
var final = [Int]()
var dict = [Int: Int]()
for index in 0.. ListNode? {
// 判断是否为空
if l1 == nil && l2 != nil {
return l2
} else if l1 != nil && l2 == nil {
return l1
} else if (l1 == nil && l2 == nil) {
return nil
} else {
var finalNode = ListNode(0)
var tempNode = l1
var otherNode = l2
var currentNode = finalNode
while true {
// 判断当前是否为空
if tempNode != nil {
currentNode.val += (tempNode?.val)!
}
if otherNode != nil {
currentNode.val += (otherNode?.val)!
}
tempNode = tempNode?.next
otherNode = otherNode?.next
if currentNode.val - 10 >= 0 {
currentNode.val = currentNode.val - 10
currentNode.next = ListNode(1)
currentNode = currentNode.next!
} else {
if tempNode == nil && otherNode == nil {
break
}
currentNode.next = ListNode(0)
currentNode = currentNode.next!
}
}
return finalNode
}
}
}
```
================================================
FILE: Base/leetcode/无重复字符的最长子串.md
================================================
## 无重复字符的最长子串
啊哈哈,这道题真有趣,刚开始没看懂题意中为什么要指出 "pwke" 是 子序列 而不是子串 这句话,然后自己用 k-v 过了前两个样例后到这个样例才恍然大悟,原来是这么个意思哈哈。
### 搞笑的code
```swift
func lengthOfLongestSubstring(_ s: String) -> Int {
var dict = [Character: Character]()
for c in s {
guard dict[c] != nil else {
dict[c] = c
continue
}
}
print(dict.values)
return dict.keys.count
}
```
### 一次超时的代码
```Swift
class Solution {
func lengthOfLongestSubstring(_ s: String) -> Int {
var dict = [Character: Character]()
var longestSubStringLength = 0
for c in s {
if dict[c] != nil {
if longestSubStringLength < dict.keys.count {
longestSubStringLength = dict.keys.count
}
dict.removeValue(forKey: c)
}
dict[c] = c
}
if longestSubStringLength < dict.keys.count {
longestSubStringLength = dict.keys.count
}
return longestSubStringLength
}
}
```
### AC 代码
```swift
func lengthOfLongestSubstring(_ s: String) -> Int {
var longestSubStringLength = 0
var finalString = ""
var index = 0
for c in s {
if finalString.contains(c) {
longestSubStringLength = max(longestSubStringLength, finalString.count)
let endIndex = finalString.index(of: c)
let stringRange = finalString.startIndex...endIndex!
finalString.removeSubrange(stringRange)
}
finalString.append(c)
index += 1
}
return max(longestSubStringLength, finalString.count)
}
```
其实挺惭愧的,看了官方题解没看懂哈哈哈,不过官方题解倒是给了三个大的解题方向,直接字符串暴力遍历、滑动窗口、Set 和 HashMap 三种。刚开始我受到了昨天(应该是前天)的结题思路影响,因为这道题的核心是返回“最长不重复子串”的长度,只需要知道个数就行,所以我就想到了直接刚 dictionary ,不过这也就给我后续的挖了一个巨大的坑,知道第二天早上(也就是今天)才解决掉,之前做的时候漏掉了一个问题,dictionary 没法删掉从某一个下标开始之前的所有字符,都是离散的,之前一直绕在这个里边,但是 dictionary 的解法是 O(1) 的,非常诱人。不过最后实在没绕出这个圈,又换了字符串再刚了一次,换了字符串直接暴力思路就非常顺畅了。
没错,因为受到了题解给的思路,就直接字符串做了,确实也 AC 了。AC 后我在想字符串按照我之前的经验不管是只要是遍历都是 O(n) 这一趟趟又 index(of:) 又 subRange 的早就 O(n^3) 了吧,岂能不超时?但实际上就是没超时 😑 。查阅了一波资料后才发现,原来 Apple 对 Swift 的 String 居然花了如此大的力气进行打磨,详见[这篇文章](https://github.com/apple/swift/blob/master/docs/StringManifesto.md#string-should-be-a-collection-of-characters-again),总结来说是 Swift 1.0 的时候只是字符的集合,相当于就是HashSet 了,Swift 2.0 后又把它移除掉了,一直到 Swift 4.0 又加了回来,在刚给出的那篇文章中,Apple 的开发团队写了不少为什么要加回来的思考,推荐阅读。
知道了 String 是 collection 后还是翻 collection 的文档,没想到不管是 index(of:) 还是 subRange() 等等得这些居然统统都是 O(1) !所以我写出了一个 O(n) 的解法 😑 。这还真是令人惊讶呢!(其实我觉得应该还是 O(n^2))
================================================
FILE: Base/leetcode/最长回文子串.md
================================================
## 最长回文子串
刚开始做的时候直接按照了自己的思路去搞,这一弄问题就出来了,因为没有考虑好回文字符串最重要的特点,导致最开始就想错了,大概的思路是这样的,还是一个字符一个字符的从目标字符串中读取,然后以当前读取到的字符出发,再起一个循环往后接着读取,一直读到下一个字符与当前字符相等,然后把从当前字符到下一个相等字符之间的字符串进行翻转,判断翻转前和翻转后是否相等,如果相等就记录下此时字符串和长度,然后接着循环。
好吧,其实看到这大家也都知道这种做法是一定会超时的,但是刚开始的时候因为我并不知道要考虑哪些测试样例,题目给的和自己想的一些简单样例都过了,交了一发,没报超时,只是一些边界条件没考虑好,加上这些边界条件后再交一发,好吧,从此一 wa 到底,统统超时。
### 超时代码1
```Swift
func longestPalindrome(_ s: String) -> String {
var stringindex = 0
var finalString = ""
var finalStringMaxLength = 0
while true {
if stringindex == s.count {
break
}
var tempString = ""
let currentStringStart = s.index(s.startIndex, offsetBy: stringindex)
for c in s[currentStringStart.. tempString.count else {
finalString = tempString
continue
}
}
}
stringindex += 1
}
return finalString
}
```
### 超时代码2
```Swift
func longestPalindrome(_ s: String) -> String {
var stringindex = 0
var finalString = ""
var finalStringMaxLength = 0
while true {
if stringindex == s.count {
break
}
var tempString = ""
let currentStringStart = s.index(s.startIndex, offsetBy: stringindex)
for c in s[currentStringStart.. tempString.count else {
finalString = tempString
continue
}
continue
}
if tempString.count < s.count - stringindex {
continue
}
tempString = ""
break
}
}
tempString.append(c)
}
stringindex += 1
}
if finalString == "" {
if s == "" {
return ""
}
return String(s[s.startIndex])
}
return finalString
}
```
思考了一下,我的做法就是循环太多,而且循环的次数也很多,赶紧试着调了一下,发现问题还是没解决,但是又不想看答案,接着再纠结了一波,还是超时。翻了参考的思路,才意识到自己的出发点出了,应该要从当前字符两侧同时出发,同时判断左右两个字符,但是一直没写起来。
看了道长写的一个答案,看上去是打表做的,但是这思路自己完全看不懂 🙄 ,真是不明白这种题是怎么打表做的hhh,等以后能力够了再去回味一番吧。[repo 在此](https://github.com/soapyigu/LeetCode-Swift/blob/master/DP/LongestPalindromicSubstring.swift)
今天下午又在 github 上翻到了一个自己能够看懂的方法,用的就是官方题解的思路,但实际上却是超时代码。😓 ,[repo 在此](https://github.com/lexrus/LeetCode.swift/blob/swift4/Tests/5.swift)
超时的样例实在是太好玩了。hhhh
```
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
```
================================================
FILE: Base/nowCode.md
================================================
## 有一棵二叉树,请设计一个算法,按照层次打印这棵二叉树
给定二叉树的根结点root,请返回打印结果,结果按照每一层一个数组进行储存,所有数组的顺序按照层数从上往下,且每一层的数组内元素按照从左往右排列。保证结点数小于等于500。
### AC code
```c++
#include
#include
#include
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {}
};
class TreePrinter {
public:
std::vector > printTree(TreeNode* root) {
std::queue queue;
std::vector > finalVec;
if (root) {
queue.push(root);
queue.push(NULL);
std::vector layerVec;
while (!queue.empty()) {
TreeNode *frontNode = queue.front();
queue.pop();
if (frontNode) {
layerVec.push_back(frontNode->val);
if (frontNode->left != NULL) {
queue.push(frontNode->left);
}
if (frontNode->right != NULL) {
queue.push(frontNode->right);
}
} else {
finalVec.push_back(layerVec);
layerVec.clear();
if (!queue.empty()) {
queue.push(NULL);
}
}
}
}
return finalVec;
}
};
int main(int argc, const char * argv[]) {
TreeNode *rootNode = new TreeNode(1);
TreeNode *node1 = new TreeNode(2);
rootNode->left = node1;
TreeNode *node2 = new TreeNode(3);
rootNode->right = node2;
TreeNode *node3 = new TreeNode(4);
node1->left = node3;
TreeNode *node4 = new TreeNode(5);
node1->right = node4;
TreeNode *node5 = new TreeNode(6);
node2->left = node5;
TreeNode *node6 = new TreeNode(7);
node2->right = node6;
TreePrinter *printer = new TreePrinter();
printer->printTree(rootNode);
std::vector> finalVec = printer->printTree(rootNode);
for (std::vector >::iterator vec = finalVec.begin(); vec != finalVec.end(); vec ++) {
std::vector tempVec = *vec;
for (std::vector::iterator instance = tempVec.begin(); instance != tempVec.end(); instance ++) {
std::cout << *instance << std::ends;
}
std::cout << std::endl;
}
return 0;
}
```
## 总结
刚开始一点思路都没有,可能是太久没有摸算法了吧,而且也不知道要用上队列,更别说要考虑的一些细节了。总之这道题让我反思了很多,确实是不应该让自己的目光一直放在工程上了。
因为这道题十分经典,网上已经有了大量的题解,左神讲的意思是用一个队列 Q 去管理且用两个变量 last 和 nlash 去做记录,last 作为当前层级的最右节点,nlast 为当前下一层级的最右节点。刚开始先让 last 等于 rootNode ,然后 rootNode push() 入队,接着再让 rootNode pop() 出队,让 nlast 等于 rootNode->leftNode 入队,接着让 nlast 等于 rootNode->rightNode 且入队,此时发现 last 所代表的节点和 出队的 rootNode 相等,换行。继续用 last 等于 root->rightNode ,重复以上过程。
刚开始我觉得云里雾里的,虽然自己也确实没有思路,但是总感觉怪怪的。于是先来纠结一番如何进行按层遍历,也就是宽度优先遍历,当初自己前中后序遍历“嗖嗖”的就写出来了,现如今还得缓好久才知道应该怎么动手,😔。刚开始根本就不知道要用到队列,一直在纠结怎么递归。翻了翻网上的题解,一看到大家都在使用队列思路就开始活跃起来了。
经过一番修整,代码撸出来了,但是跟左神的做法不太一样,没有用到任何标记变量,当前层级如果要结束了直接在 push 进一个 NULL ,后边再去遍历的时候遇到 NULL 就标记着当前层级已经结束了,开始进行下一层级的宽度优先遍历。
再调整一下,代码也撸出来了,但是格式不对,研究了一下,发现是因为 push 进 finalVec 的时机不对,又调整了一下才得以 AC 。
## 二叉树的序列化
没有题意,意思就是要让我们把二叉树进行序列化,做二叉树的序列化左神讲到了一个重点,要进行区分节点的孩子,尤其对于 BFS 来说,需要标识清楚层级关系。
### BFS
```C++
bool serializationBinaryTree(std::vector > finalVec) {
std::ofstream fileOut;
fileOut.open("traversalOfBinaryTree.txt");
if (!fileOut) {
return false;
}
for (std::vector >::iterator vec = finalVec.begin(); vec != finalVec.end(); vec ++) {
std::vector tempVec = *vec;
for (std::vector::iterator instance = tempVec.begin(); instance != tempVec.end(); instance ++) {
fileOut << *instance;
}
fileOut << std::endl;
}
fileOut.close();
return true;
}
```
### 总结
直接用上了上一题的输出的二维 vector 。
### 二叉树的序列化
二叉树的反序列化相对于序列化来说考虑的事情更多一些。同样,我也将实现二叉树的前中后序以及 BFS 反序列化。
### BFS
```C++
TreeNode* deserializationBinaryTree() {
std::ifstream fileIn;
fileIn.open("traversalOfBinaryTree.txt");
std::vector> finalVec;
if (!fileIn) {
return nullptr;
}
TreeNode *rootNode = nullptr, *currentNode = nullptr;
std::queue queue;
std::queue nodeQueue;
while(!fileIn.eof()) {
std::string tmpString;
std::getline(fileIn, tmpString);
for (int i = 0; i < tmpString.length(); i ++) {
queue.push(tmpString[i] - 48);
}
// int queue don't accept nullptr, becauese of it's real null pointer
queue.push(-1);
}
while (!queue.empty()) {
if (!rootNode) {
rootNode = new TreeNode(queue.front());
nodeQueue.push(rootNode);
queue.pop();
currentNode = rootNode;
} else {
if (queue.front() == -1) {
queue.pop();
} else {
if (!currentNode->left) {
currentNode->left = new TreeNode(queue.front());
nodeQueue.push(currentNode->left);
queue.pop();
} else if (!currentNode->right) {
currentNode->right = new TreeNode(queue.front());
nodeQueue.push(currentNode->right);
queue.pop();
} else {
currentNode = nodeQueue.front();
nodeQueue.pop();
}
}
}
}
fileIn.close();
return rootNode;
}
```
### 总结
做 BFS 的反序列化纠结得有些久,刚开始也没想到用队列去存 getline 切割出来的内容,到最后想起了左神说的用什么方法进行序列化就用什么方法进行反序列化后,才点醒了我,撸完了第一波代码后,又卡住了,又掉进了死胡同,currentNode 无法和 parentNode 进行交互,也就说到了 rootNode 后,没法再进行循环建树了,差点又要放弃。在去刷牙的过程中,脑子活了一下,发现可以再用一个 nodeQueue 去 push queue pop 出来的当前层 node,遂搞定。
================================================
FILE: Base/python.md
================================================
## 模块
每个 `.py` 文件的主文件就是模块名称,想要导入模块时必须使用 `import` 关键词来制定模块名称。若要引用模块中定义的名称,则必须在名称前加上模块名称与一个 “`.`" 符号。
## __pycache__
CPython 将 .py 文件编译后的字节码文件,如果之后再次导入同一个模块,检测到源码文件没有更改过,就不会再对源码重头开始进行语义和语法解析等操作。
## 获取命令行参数
可通过 `sys` 模块中的 `argv` 列表,argv[0] 保存的是源码的文件名:
```python
import sys
print('hello', sys.argv[1])
```
## 包
文件夹中一定要有一个 `__init__.py` 文件,该文件夹才会被视为一个软件包,并且软件包名称会成为命名空间的一部分。
## python 中所有数据都是对象
## python3 之后,整数类型为 `int`,不再区分 `int` 和 `long`
## type()
想知道某个数据的类型可以使用 `type()` 函数:
```python
>>>type(10)
```
## python 支持复数的直接表示法
```python
>>> a = 3 + 2j
>>> b = 5 + 3j
>>> a + b
(8 + 5j)
```
## 格式化字符串
### 旧方式
```python
>>> 'hello, %s!' % 'world'
```
### 新方式
```python
>>> 'hello, {}!'.format('world')
```
## 字典
当从字典中通过某个 key 取值时,当 key 不存在,则会报 `KeyError` 的错误,此时可以通过 `in` 来判断:
```python
>>> nums = {'aha': 2333}
>>> 'wow' in nums
False
>>> nums['wow']
Traceback (most recent call last):
File "", line 1, in
KeyError: 'wow'
```
## 需要处理小数且还需要精确的结果
`import decimal`,其它细节查资料
## 指数运算
```python
>>> 2 ** 10
1024
```
## 比较与赋值
`== != < >` 等符号用来比较值,`is, is not` 用来比较两个对象的引用是否相等
## * 与 **
### `*`
如果事先无法预期要传入的自变量个数,可以在定义函数的参数时使用 `*`,表示该参数接受不定长度的自变量:
```python
def sum(*numbers):
total = 0
for number in numbers:
total += number
return total
```
### `**`
让指定的关键词自变量收集为一个字典:
```python
def ahaha(**user_settings):
methon = user_settings.get('methon', 'GET')
contents = user_settings.get('contents', '')
```
================================================
FILE: Base/操作系统.md
================================================
# 第一章 计算机操作系统概述
## 第一节
时间 | 硬件 | 语言 | 用途
--------- | --------- | --------- | -------- |
1945 | 电子真空管 | 机器语言 | 应用于科学计算
1956 | 晶体管 批处理控制 | Fortran/COBOL | 数据处理领域
1959 | 集成电路 多道程序 | 操作系统/数据库/高级语言 | 应用领域得到了进一步的拓展
1976 | 大规模/超大规模集成电路 | | 向快速化/小型化/系统化/网络化/智能化等方面发展
1980 | 微机出现 | | 廉价化促使应用领域快速膨胀 |
1990 | 图形化人机交互技术 | | 友善化推动了应用人群的快速扩展
2003 | 移动计算 | | |
### 计算机系统组成
* 硬件子系统和软件子系统
* 硬件:CPU、I/O控制系统等
* 软件:操作系统等
### 计算机系统的用户视图
## 第二节
### 计算机硬件系统
* 中央处理器:运算单元,用于执行机器指令的运算;控制单元:解义机器指令。
* 主存储器:存储正在执行的指令和数据。
* 外围设备:显示器等输出设备,键盘鼠标等输入设备,硬盘等存储设备,机机网络设备。
* 总线:连接
### 冯诺依曼模型
存储程序计算机在体系结构上主要特点:
1. 以运算单元为中心,控制流由指令流产生;
2. 采用存储程序原理,面向主存组织数据流;
3. 主存是按地址访问、线形编址的空间;
4. 指令由操作码和地址码组成;
5. 数据以二进制编码。
### 总线
* 总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是 CPU 、内存、输入输出设备传递信息的公用通道。
* 计算机的各个部件通过总线相连接,外围设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统
* 按照所传输的信息种类,总线包括一组控制线、一组数据线和一组地址线。
### 总线类型
* 内部总线:用于 CPU 芯片内部连接各元件;
* 系统总线:用于连接 CPU 、存储器和各种 I/O 模块等主要部件
* 通信总线:用于计算机系统之间通信。
### 中央处理器(CPU)
是计算机运算核心(Core)和控制单元(Control Unit),主要包括:
* 运算逻辑部件:一个或多个运算器;
* 寄存器部件:包括通用寄存器、控制与状态寄存器,以及高速缓冲存储器(Cache)
* 控制部件:
* 实现各部件间联系的数据、控制及状态的内部总线
* 负责对指令译码、发出为完成每条指令所要执行操作的控制信号、实现数据传输等功能的部件。
### 存储器的组织结构
L0 ~ L4 :挥发性存储器,断电数据消失。
### 外围设备及其控制
设备类型:
* 输入设备
* 输出设备
* 存储设备
* 机机通信设备:不同网络设备块大小不一致,以字节、以包等进行数据传输
设备控制方式:
* 轮询方式:CPU 忙式控制,CPU 执行内存数据交换;
* 中断方式:CPU 启动外设,外设中断 CPU ,CPU 执行内存数据交换。
* DMA (直接内存存取)方式:CPU 启动 DMA ,DMA 执行输入输出与内存数据交换,DMA 中断 CPU 。举个例子,移动一个外部内存的区块到芯片内部更快的内存区,这样的操作并没有让处理器工作拖延,反而可以重新排程处理其它工作。
## 第三节
### 计算机软件系统的组成
* 系统软件:操作系统、实用程序、语言处理程序和数据库管理系统。操作系统为实施对各种软硬件资源的管理控制;实用程序为方便用户所设,如文本编辑;语言处理程序把用汇编语言/高级语言编写的程序,翻译成可执行的机器语言程序。
* 支撑软件:有接口软件、工具软件、环境数据库,支持用户使用计算机的环境,提供开发工具。(也可以认为是系统软件的一部分)
* 应用软件:是用户按其需要自行编写的专用程序。
### 软件开发的不同层次
* 计算机硬件系统:机器语言;
* 操作系统之资源管理:机器语言 + 广义指令(拓充了硬件资源管理);
* 操作系统之文件系统:机器语言 + 系统调用(扩充了信息资源管理);
* 数据库管理系统:+ 数据库语言(扩充了功能更强的信息资源管理);
* 语言处理程序:面向问题的语言。
## 第四节
### 计算机的手工操作
哈哈,跟之前上计算机组成原理一样,做个大实验手指疼两天。
### 装入程序的引进
* 引入卡片和纸带描述程序指令与数据
* 引入装入程序(Loader)。自动化执行程序装入,必要时进行地址转换;通常存放在 ROM 中。
### 引入汇编语言后的计算机控制
* 计算机操作变成:汇编和执行两个阶段。
### 高级语言之后
* 计算机操作变成:编译、**连接**和执行三步骤。
### 简单批处理系统的操作
* 原先,手工操作与计算机速度严重不匹配的问题。现在,引入**作业控制语言**,用户编写作业说明书,描述对一次计算机求解(作业)的控制;
* 操作员控制计算机成批输入作业,成批执行作业;
* 这一方式明显缩短了手工操作时间,提高了计算机系利用率;
* 这一阶段,磁带的出现,使得卡片与纸带等机械输入方式得以进一步提高。
只是一个简单的批处理系统,谈不上是操作系统,只提供了一个半自动化操作方式,并没有解决与中央处理器电子速度不匹配的矛盾,要让多个程序同时进入计算机系统,而想要多道程序同时进行,就需要进行程序的切换,而该切换的解决需要更加高速的外存储设备的支撑。

### 操作系统与自动化系统
磁盘的出现,计算机操作系统从此出现,实现了真正的自动化控制。
## 第五节
### 操作系统概念
* 操作系统简称:OS
* OS 是计算机系统最基础的系统软件,管理软硬件资源、控制程序执行,改善人机界面,合理组织计算机工作流程,为用户使用计算机提供良好运行环境。
### 操作系统
操作系统是方便用户、管理和控制计算机软硬件资源的系统程序集合。
* 从用户角度看,OS 管理计算机系统的各种资源,扩充硬件的功能,控制程序的执行;
* 从人机交互看,OS 是用户与机器的接口,提高良好的人机界面,方便用户使用计算机;
* 从系统结构看,OS 是一个大型软件系统,其功能复杂,体系庞大,采用层次式、模块化的程序结构。
### 操作系统的组成
* 进程调度子系统;
* 进程通信子系统;
* 内存管理子系统;
* 设备管理子系统;
* 文件管理子系统;
* 网络通信子系统;
* 作业控制子系统;
### 操作系统的类型
* **从操作方式看**
* 多道批处理操作系统,采用脱机控制方式;
* 分时操作系统,采用交互控制方式;
* 实时操作系统。
* **从应用领域看**
* 服务器操作系统、并行操作系统;
* 网络操作系统、分布式操作系统;
* 个人机操作系统、手机操作系统;
* 嵌入式操作系统、传感器操作系统。
## 第六节
### 操作系统的资源
* 硬件资源:处理器、内存、外设。
* 信息资源:数据、程序。
### 管理计算机系统的软硬件资源
* 处理器资源:哪个程序占有处理器运行?
* 内存资源:程序/数据在内存中如何分布?
* 设备管理:如何分配、去配和使用设备?
* 信息资源管理:如何访问文件信息?
* 信号量资源:如何管理进程之间的通信?
### 屏蔽资源使用的底层细节
* **驱动程序**:最底层的、直接控制和监视各类硬件(或文件)资源的部分;
* 职责是隐藏底层硬件的具体细节,并向其它部分提供一个抽象的通用的接口;
* 比如说:打印一段文字或一个文件,既不需要知道文件信息存储在硬盘上的细节,也不必知道具体打印机类型和控制细节。
### 资源的共享与分配方式
* 资源共享方式:独占使用方式、并发使用方式;
* 资源分配策略:静态分配方式(无死锁)、动态分配方式(注意死锁)、资源抢占方式(无死锁)。
## 第七节
### 多道程序同时计算
* CPU 速度与 I/O 速度不匹配的矛盾,非常突出。
* 只有让多道程序同时进入内存争抢 CPU 运行,才能够使得 CPU 和外围设备充分并行,从而提高计算机系统的使用效率。
### 多道程序设计
* 多道程序设计:指让多个程序同时进入计算机的主存储器进行计算;
* 多道程序设计的特点
* CPU 与外部设备充分并行;
* 外部设备之间充分并行;
* 发挥 CPU 的使用效率;
* 提高单位时间的算题量。
### 多道程序系统的实现
* 为进入内存执行的程序建立管理实体:**进程**
* OS 应能管理与控制进程程序的执行;
* OS 协调管理各类资源在进程间的使用
* 处理器的管理和调度;
* 主存储器的管理和调度;
* 其它资源的管理和调度。
### 多道程序系统的实现要点:
* 如何使用资源:调用操作系统提供的服务例程(如何陷入操作系统);
* 如何复用 CPU :调度程序(在 CPU 空闲时让其他程序运行);
* 如何使 CPU 与 I/O 设备充分并行:设备控制器与通道(专用的 I/O 处理器)
* 如何让正在运行的程序让出 CPU :中断(中断正在执行的程序,引入 OS 处理)
## 第八节
### 计算机系统操作方式
* OS 规定了合理操作计算机的工作流程;
* OS 的操作接口 —— 系统程序;
* OS 提供给用户的功能级接口,为用户提供的解决操作计算机和计算共性问题所有服务的集合。
* OS 的两类作业级接口:
* 脱机作业控制方式:作业控制语言;
* 联机作业控制方式:操作控制命令。
### 脱机作业控制方式
* OS : 提供作业说明语言包;
* 用户:编写作业说明书,确定作业加工控制步骤,并与程序数据一并提交;
* 操作员:通过控制台输入作业;
* OS :通过作业控制程序自动控制作业的执行;
* 例如:批处理 OS 的作业控制方式,UNIX 的 shell 程序,DOS 的 bat 文件。
### 联机作业控制方式
* 计算机:提供终端(键盘/显示器);
* 用户:登录系统;
* OS :提供命令解释程序;
* 用户:联机输入命令,直接控制作业步的执行;
* 例如:分时 OS 的交互控制方式。
### 命令解释程序
* 命令解释程序:接受和执行一条用户提出的对作业的加工处理命令;
* 当一个新的批作业被启动,或新的交互型用户登录进系统时,系统就自动地执行命令解释程序,负责读入控制卡或命令行,作出相应解释,并予以执行;
* 会话语言:可编程的命令解释程序;
* 图形化的命令控制方式;
* 多通道交互的命令控制方式。
### 命令解释程序的处理过程
* OS 启动命令解释程序,输出命令提示符,等待键盘中断/鼠标点击/多通道识别;
* 每当用户输入一条命令(暂存在命令缓冲区)并按回车换行时,申请中断;
* CPU 响应后,将控制权交给命令解释程序,接着读入命令缓冲区内容,分析命令、接受参数,执行处理代码;
* 前台命令执行结束后,再次输出命令提示符,等待下一条命令;
* 后台命令处理启动后,即可接收下条命令。
## 第九节
### 操作系统的人机交互部分
* OS 改善人机界面,为用户使用计算机提供良好的环境;
* 人机交互设备包括传统的终端设备和新型的模式识别设备;
* OS 的人机交互部分用于控制有关设备运行和理解执行设备传来的命令;
* 人机交互功能是决定计算机系统友善性的重要因素,是当今 OS 研发热点。
### 人机交互的初期发展
* 交互式控制方式:
* 行命令控制方式:1960 年代开始使用;
* 全屏幕控制方式:1970 年代开始使用;
* 斯坦福研究所提出的发展计划:
* 始于 1960 年代,1980 年代广泛应用;
* 强调人而不是技术是人机交互的中心;
* 代表性成果:鼠标、菜单与窗口控制。
### 人机交互发展 —— WIMP 界面
* 缘起:70年代后期施乐的原型机 Star;
* 特征:窗口(Windows)、图标(Icons)、菜单(Menu)和指示装置(Pointing Devices)为基础的图形用户界面 WIMP 。
* 得益:Apple 最初采用并大力推动;
* 时间:1990 年代开始广泛使用;
* 不足:不能运行同时使用多个交互通道,从而产生人-机交互的不平衡。
### 人机交互发展 —— 多媒体计算机
* 缘起:1985年的 MPC
* 把音视频、图形图像和人机交互控制结合起来,进行综合处理的计算机系统;
* 构成:多媒体硬件平台、多媒体 OS 、图形用户接口、多媒体数据开发工具。
* 提供与时间有关的时变媒体界面,既控制信息呈现,也控制何时呈现/如何呈现;
* 人机交互界面需要使用多种媒体,同时支持多通道交互整合,改善用户体验。
### 人机交互发展 —— 虚拟现实系统
* 缘起:1980年代的虚拟现实新型用户界面;
* VR 通过计算机模拟三位虚拟世界,根据观察点、观察点改变的导航和对周围对象的操作,来模拟临境体验;
* 支持多通道交互整合,提供良好用户体验;
* 支持用户主动参与的高度自然三位 HCI ,以及语音识别、头部跟踪、视觉跟踪、姿势识别等新型 HCI 。
## 第十节
### 操作系统的程序接口
* 操作系统功能的程序接口 —— 系统调用;
* 操作系统实现的完成某种特定功能的过程;为所有运行程序提供访问操作系统的接口。
### 系统调用的实现机制
* 陷入处理机制:计算机系统中控制和实现系统调用的机制;
* 陷入指令:也称访管指令,或异常中断指令;
* 每个系统调用都事先规定了编号,并在约定寄存器中规定了传递给内部处理程序的参数。
### 系统调用的实现要点
* 编写系统调用处理程序;
* 设计一张系统调用入口地址表,每个入口地址指向一个系统调用的处理程序,并包含系统调用自带参数的个数;
* 陷入处理机制需要开辟现场保护区,以保持发生系统调用时的处理器现场。

## 第十一节
### 操作系统软件的规模
* 在计算机软件发展史上, OS 是第一个大规模的软件系统;
* 1960 年代,由 OS 开发所衍生的体系结构、模块化开发、测试与验证、演化与维护等研究直接催生了软件工程这一新兴研究领域(另一个催生来源是 DB 应用引发的需求与规格)
### 操作系统软件的结构设计
* OS 构件:内核、进程、线程、管程等;
* 设计概念:模块化、层次式、虚拟化。
### 操作系统内核
* 单内核:内核中各部件杂然混居的形态,始于1960年代,广泛使用。如 Unix/Linux ,及 Windows (官方称为混合内核的 CS 结构);
* 微内核:1980 年代开始,强调结构性部件与功能性部件的分离,大部分 OS 研究都集中在此;
* 混合内核:微内核和单内核的折中,较多组件在核心态中运行;
* 外内核:尽可能减少内核的软件抽象化和传统微内核的消息传递机制,使得开发者专注于硬件的抽象化(部分嵌入式系统使用)


# 第二章 处理器管理
## 第一节 处理器与寄存器
操作系统是对计算机硬件的第一次扩充,操作系统在设计的实话贯彻了软硬件协同的概念,操作系统对硬件设计提出了一系列要求。
### 处理器部件的简单示意

### 用户可见寄存器
* 可以使程序员减少访问主存储器的次数,提高指令执行的效率;
* 所有程序可使用,包括应用呈现和系统程序:
* 数据寄存器:又称通用寄存器(AX、BX、CX、DX);
* 地址寄存器:索引寄存器(SI、DI)、栈指针寄存器(SP、BP)、段地址寄存器(CS、DS、SS、ES)等;
### 控制与状态寄存器
* 用于控制处理器的操作。主要被具有特权的操作系统程序使用,以控制程序的执行。
* 程序计数器 PC :存储将取指令的地址;
* 指令寄存器 IR :存储最近使用的指令;
* 条件码 CC :CPU 为指令操作结果设置的位,标志正/负/零/溢出等结果;
* 标志位:中断位、中断允许位、中断屏蔽位、处理器模式位、内存保护位等。
### 程序状态字 PSW
* PSW 即是操作系统的概念,指记录当前程序运行的动态信息,通常包含:
* 程序计数器,指令寄存器,条件码;
* 中断字,中断运行/禁止,中断屏蔽,处理器模式,内存包含,调试控制。
* PSW 也是计算机系统的寄存器
* 通常设置一组控制与状态寄存器;
* 也可以专设一个 PSW 寄存器。
## 第二节 指令与处理器模式
### 机器指令
* 机器指令是 **计算机系统执行** 的基本命令,是 **中央处理器执行** 的基本单位;
* 指令由一个或多个字节组成,包括操作码字段、一个或多个操作数地址字段以及一些表征机器状态的状态字或特征码;
* 指令完成各种算术逻辑运算、数据传输、控制流跳转特征码。
### 指令执行过程
* CPU 根据 PC (程序计数器)取出指令,放入 IR (指令暂存器),并对指令译码,然后发出各种控制命令,执行微操作系列,从而完成一条指令的执行。
* 一种指令执行步骤如下:
* 取指:根据 PC 从存储器或高速缓冲存储器中取指令到 IR ;
* 解码:解译 IR 中的指令来决定其执行行为;
* 执行:连接到 CPU 部件,执行运算,产生结果并写回,同时在 CC 里设置运算结论标志;跳转指令操作 PC ,其它指令递增 PC 值。
### 特权指令与非特权指令
* 用户程序并非能够使用全部机器指令,那些与计算机核心资源相关的特殊指令会被保护。
* 如:启动 I/O 指令、置 PC 指令等
* 核心资源相关的指令只能被操作系统程序使用;
* 特权指令:只能被操作系统内核使用的指令;
* 非特权指令:能够被所有程序使用的指令。
### 处理器模式
* 计算机通过设置处理器模式实现特权指令管理;
* 计算机一般设置 0 、 1 、2 、 3 等四种运行模式,建议分别对应:
值 | 保护级别
--- | ---
0 | 操作系统内核
1 | 系统调用
2 | 共享库程序
3 | 用户程序
* 一般来说,现代操作系统只使用 0 和 3 两种模式,对应与内核模式和用户模式。
### 模式的切换
* 简称模式切换,包括“用户模式 -> 内核模式”和“内核模式 -> 用户模式”的转换;
* 中断、异常或系统异常等事件导致用户程序向 OS 内核切换,触发:用户模式 -> 内核模式;
* 程序运行时发生并响应中断
* 程序运行时发生异常
* 程序请求操作系统服务
* OS 内核处理完成后,调用中断返回指令触发:内核模式 -> 用户模式。
## 中断
### 中断的概念
* 中断是指程序执行过程中,遇到急需处理的事件时,暂时中止 CPU 上现行程序的运行,转去执行相应的事件处理程序,待处理完成后再返回原程序被中断处或调度其他程序执行的过程。
* **操作系统时“中断驱动”的**;换言之,中断时激活操作系统的唯一方式。
* 中断有广义和狭义之分,上述中断是广义的中断。
### 中断、异常与系统异常
* 狭义的中断指来源于处理器之外的中断事件,即与当前运行指令无关的中断事件,如 I/O 中断、时钟中断、外部信号中断等。
* 异常指当前运行指令引起的中断事件,如地址异常、算数异常、处理器硬件故障等。
* 系统异常指执行陷入指令而触发系统调用引起的中断事件,如请求设备、请求 I/O 、创建进程等。
================================================
FILE: Base/网络相关知识.md
================================================
# HTTPS 通信原理剖析
## 基本概念
### 公钥密码体制(public-key cryptography)
公钥密码体制分为三个部分:**公钥**、**私钥**和**加密解密算法**。它的加密解密过程如下:
* 加密:通过加密算法和公钥对内容(或明文)进行加密,得到密文。加密过程需要用到公钥;
* 解密:通过解密算法和私钥对密文进行解密,得到明文。解密过程需要用到解密算法和私钥。注意,由公钥加密的内容,只能由私钥进行解密。
公钥密码体制的公钥和算法都是公开的,私钥是保密的。大家都使用公钥进行加密,但只有私钥的持有者才能进行解密,在实际的使用中,有需要的人会生成一对公钥和私钥,把公钥发布出去,私钥自己留着。
### 对称加密算法(symmetric key algorithms)
在对称加密算法中,加密使用的密钥和解密使用的密钥是相同的,因此对称加密算法要保证安全性的话,密钥要做好保密,只能让使用的人知道,不能对外公开。与上文中国呢的公钥密码体制有所不同,公钥密码体制中加密是用公钥,解密是用私钥,而对称加密算法中,加密和解密都是使用同一个密钥,不区分公钥和私钥。
### 非对称加密算法(asymmetric key algorithms)
在非对称加密算法中,加密使用的密钥和解密使用的密钥是不相同的。前面所说的公钥密码体制就是一种非对称加密算法,它的公钥和私钥是不能相同的。
### RSA 简介
RSA 密码体制是一种公钥密码体制,公钥公开,私钥保密,它的加密解密算法是公开的,由公钥加密的内容可以并且只能由私钥进行解密,并且由私钥加密的内容可以并且只能由公钥进行解密。
### 签名和加密
加密,是指对某个内容加密,加密后的内容还可以通过解密进行还原。比如我们把电子邮件进行加密,在网络上传输给对方,对方通过解密后可以还原电子邮件的真实内容。
签名,是指在信息后面再加上一段内容,可以证明信息没有被修改过。一般是对信息做一个 hash 计算得到一个 hash 值(总之要不可逆),在把信息发出去后,把这个 hash 值作为一个签名和信息一块发出去。接收方在收到信息后,会重新计算信息的 hash 值,并和信息所附带的 hash 值(解密完)进行对比,如果一致,就说明信息的内容没有被修改过。当然,这么做还是有可能会让 hacker 在修改真实内容的同时修改 hash 值,为了防止这种情况,签名一般会加密后再和真实内容一起发送出去,保证该签名不被修改。(如何解密该签名,涉及到数字证书,后文讲解)
## 练练
### 一
假设服务器和客户端要在网络上通信,并且打算使用 RSA 来对通信进行加密以保证内容传输的安全,由于是使用 RSA 这种公钥密码体制,服务器需要对外发布公钥(算法不需要公布,RSA 算法大家都知道),自己留着私钥,客户端通过某些途径拿到了服务器发布的公钥,客户端并不知道私钥。

如果有人 hack ,则会出现:

故客户端在接收到消息后,并不能肯定这个该消息就是由真实服务器发出的,可以被第三者冒充真实服务器发出该消息。那该如何确定该信息是由真实服务器发送过来的呢?我们知道,因为只有服务器才有私钥,所以能够确认只有对方才拥有私钥,那对方就是真实服务器,因此可为:

* {!@#%} 为 RSA 加密后的内容;
* < a | b > 为用 a 密钥和 b 算法进行加密。
* {!@#%}<私钥 | RSA> 表示为用 私钥 对 “Server come” 进行加密后的结果。
为了向客户端证明自己是服务器,服务器先把一个字符串(Server come)用自己的私钥加密,把明文和加密后的密文一起发给客户端。客户端收到消息后,用自己的公钥解密密文,和明文进行比较,如果一致,说明信息的确是由服务器发过来的。因为由服务器用私钥加密后的内容由且只能由公钥进行解密,私钥只有服务器持有,所以如果解密出来的内容是能够对得上,那说明信息一定是从服务器发送过来的。
如果想 hack 服务器,

在上图中的第四步{@@@@}<私钥 | RSA> ,这块是无法 hack 的,因为不知道私钥,无法用私钥加密,可以认定对方是个假货。
到这里,客户端可以确认服务器的身份了,可以放心的和服务器进行通信,但还有个问题,通信的内容在网络上还是无法保密,

最后一步把用户的个人信息通过私钥加密发送出去了,因为公钥是所有人都知道的,除了这一个客户端,其它拥有公钥的客户端也能够对该条使用私钥进行加密的消息进行解密。一般会采用对称加密来解决这个问题。

在上图所示的通信过程中,客户端在确认了服务器身份后,自己选择一个对称加密算法和密钥,把选择的加密算法和密钥通过公钥进行加密发送给服务端。这个过程如果被 hack 了,但因为没有私钥也无法解密使用公钥加密后的内容。
RSA 加密算法在这个通信过程中所起到的作用主要有两个:
* 私钥只在服务器上,客户端可通过判断对方是有拥有私钥来保证是否为“真”服务器;
* 客户端通过 RSA 的掩护,安全的和服务器商量好一个对称加密算法和密钥来保证后续通信过程内容的安全。
这引发出了一个新的问题,服务器要对外发布公钥,服务器如何把公钥发送给客户端呢?可能会想到:
* 把公钥放到网络上的某个地址,都去这拿;
* 每次通信开始都下发公钥。
第一个方法的问题出在下载地址会被伪造,第二个方法的容易被中间人伪造,因为我们都可以生成公钥和私钥,无法确认公钥到底是谁的,如果能够确认公钥到底是谁的即可解决。
为了解决这个问题,数字证书出现了。一个证书包含了以下内容:
* 证书的发布机构
* 证书的有效期
* 公钥
* 证书的所有者
* 签名所使用的算法
* 指纹及指纹算法
这样,通信流程就变成了:

上图中的第二次通信,服务器把自己的证书发给了客户端而不是公钥。客户端根据证书校验来确认证书是否为服务器的,后续操作都是一致的。
为了保证安全,在证书的发布机构发布证书时,证书的指纹和指纹算法都会加密后再和证书放到一起发布,以防有人修改指纹后伪造相应的数字证书。那证书的指纹和指纹算法用什么加密呢?答案是用那个证书发布机构的私钥进行加密的,也就是说,证书发布机构除了给别人发布证书外,自己本身也有自己的证书(一般由自己生成)。在 OS 安装好时,这些证书发布机构的数字证书就已经被安装在其中了,操作系统厂商会根据一些权威安全机构的评估选取一些信誉很好并且通过一定安全认证的证书发布机构,把这些证书发布机构的证书默认安装在 OS 中,且设置为 OS 信任的数字证书。这些证书发布机构自己持有与自己的数字证书对应的私钥,会用这个私钥加密所有发布证书的指纹作为数字签名。
-----
## 初涉 HTTPS (超文本传输安全协议,也被称为 HTTP over TLS,HTTP over SSL 或 HTTP Secure )
首先明确一个概念,HTTPS 并没有推翻之前的 HTTP 协议,而是一个安全的 HTTP 。
> HTTPS 开发的主要目的,是提供对网络服务器的认证,保证交换信息的机密性和完整性。
末尾的 S 指的是 SSL ( Secure Sockets Layer 安全套接层), /TLS (传输层安全性协议,英语:Transport Layer Security,缩写为 TLS )。该层协议位于 HTTP 协议和 TCP/IP 协议的中间
所谓的信息传输安全指的是:
1. 客户端和服务器传输信息只有双方才能看懂。
2. 为了防止第三方就算看不懂数据也会瞎改的情况,客户端和服务器要有能力去验证数据是否被修改过;
3. 客户端必须要防止避免中间人攻击,除了真正要建立连接的服务器外,任何第三方都无法冒充真实服务器。
对于信息的加密,可以通过对称和非对称加密。简单来说,对称加密是客户端和服务器双方都约定俗成了一套加密规则,当然这套规则可以是客户端和服务器开始建立连接之前就已经规定好,也可以在已经建立连接时,向服务器先请求加密规则。
此时的 HTTPS 握手流程多了两步:
> 客户端:服务器,我需要发起一个 HTTPS 请求
> 服务器:客户端,你的秘钥是 xxxx
而非对称加密也可以简单得认为是客户端有自己的一套加解密规则(公钥),服务器有自己的一套加解密规则(私钥),经过服务器的加解密规则(私钥)加密后的数据只有客户端的加解密规则(公钥)才能解析,经过客户端加解密规则(公钥)只有服务器的加解密规则(私钥)才能解析。
由此可见,用对称加密进行数据传输肯定比非对称加密快得多。当然,私钥是服务器自己留着的,不对外公开的,而公钥是可对外公开获取的。
那么现在又引入了一个问题,对称加密的秘钥怎么传输?服务器直接明文返回对称加密的秘钥肯定是不科学的,而且我们还不能直接用一个新的对称加密算法去加密原来的对称秘钥,因为这又涉及了新的对称加密秘钥如何传输的问题,这是个悖论。
OK,为了解决这个问题,就用上了之前我们说的非对称加密方式,从上文我们所讲的非对称加密特点,服务器用私钥加密的数据实际上并不是真正意义上的加密,因为只要有私钥与之对应的公钥即可解密,更何况公钥谁都可以有,谁都可是是客户端,所有服务器的密码能被所有人进行解析,但私钥只存在服务器上,这就说明了:
1. 服务器下发的内容不可被伪造,因为私钥唯一,如果第三方 **强行二次加密** 则客户端的公钥无法解密;
2. 任何用公钥加密的内容都是 **绝对安全** 的,因为私钥唯一,只有拥有私钥的真正服务器才可进行解密。
故解决了我们之前的问题,秘钥并不是服务器生成,而是客户端自行用公钥生成且主动告诉服务器的,此时 HTTPS 的握手流程就变成了:
> 客户端:服务器,我要发起一个 HTTPS 请求,这是我用你下发的公钥生成的秘钥。
> 服务器:我知道了,以后咱们用这个秘钥进行验证。
OK,现在进入下一个问题,那这个公钥如何下发给客户端?啊哈,其实之前用“下发”这个词是为了好理解,实际上应该是每个使用了 HTTPS 协议的服务器都应该去一个专门的证书机构注册一个证书,这个证书中保存了权威证书机构私钥加密的公钥,客户端就用这个权威证书机构的公钥作为其的 HTTPS 公钥即可。
因此,HTTPS 握手流程就变为了:
> 客户端:服务器!我要发起一个 HTTPS 请求,给我公钥!
> 服务器:好的,我给你个证书,自己从里边拿。
> 客户端:(解密成功后)这是我解密完后的秘钥
> 服务器:我知道了,以后咱们用这个秘钥(公钥)进行验证。
实际上 `HTTPS` 并不是重新构建了一套传输协议,而是与上文中所说的一样,只是在原有传输协议的应用层和传输层之间多添加了一个安全层(会话层),如下图所示:
emmm,其实我弄到这也懵逼了,这所谓的权威证书机构公钥又如何传输?查了相关资料后发现,其实就是内置在了 OS 或者浏览器中,但是这有个问题,我们不可能穷举所有权威证书机构服务器,太多了根本存不完,而且 OS 会对其产生怀疑,凭啥你说这证书可靠就是可靠?
故,我们可以认为全世界上的权威认证机构只有一个(实践上并不),其它的想搞证书这门生意的公司得去这个唯一权威认证机构去取得认证,所以 OS 或浏览器只需要维护这一个权威认证机构的公钥即可。每次客户端只需要获取这个公钥即可。
到现在算是把我的 HTTPS 的疑惑解决得差不多了,但是还有个问题,现在证书也有个唯一的机构去做认证了,但是我们却没法知道这个证书是否真的可靠,就好像我们都知道人民币都是中国人民银行唯一认证和发行的,但是没人保证每张人民币都是真币,紫外线验证是一种人民币有效性验证的手段,那对于证书来说,如何做有效性验证呢?
又查了一波资料,每份证书会有各自对应的 hash 值,在传输证书的时候也会同时传输对应证书的 hash 值。如果此时有中间人进行攻击,因为公钥不唯一,谁都可以进行解密,但是其伪造的数据经过中间人的私钥加密后,无法正确加密,再次返回给客户端的数据经过客户端公钥解密后是乱码,如果凑巧对上了,但是也无法通过 hash 校验(至于如何校验,我还没查到)
从以上观点我们可以看出,貌似 HTTPS 坚不可破啊,它真的是无敌了么?其实从某种意义上来看,它还真的就无敌了,但也不是万无一失,因为如果我们第一次请求的就不是真的服务器,而是一个攻击者,这就完全有机会进行所谓的中间人攻击。正常的流程是在第一次握手时,服务器会下发给客户端证明自己身份的证书,客户端再用预设在设备上的公钥来解密。
但是如果我们不小心在自己的设备上安装了非权威认证机构的根证书,比如 Charles 的私有根证书,那么我们的设备上就多了一个预设的公钥,那通过Charles的私钥加密的证书就能够被正常解析出来,Charles对于我们的设备来说相当于是设备的服务器,对真的服务器来说,Charles是客户端,所以相当于Charles既拿到了私钥又拿到了公钥,能够解析并修改数据也就不在话下了,不过也不要觉得 Charles 是啥恐怖的东西,我们之所以使用 Charles 进行抓包,是因为我们信任它,你都信任了还有啥欺骗不欺骗的,中间人攻击也就不存在了,但如果你的 Charles 是个盗版的,很有可能下发这个盗版 Charles 的开发者就已经给你开了个后门。支持正版,从我做起。
### 更进一步
#### TLS协议做了什么?
正如上文所说, `HTTPS` 只是比 `HTTP` 多了一个安全层,那么这个传输层安全协议到底是怎么一回事呢?在此做了一张图分享如下, `TLS` 运行在一个可靠的 `TCP` 协议上。
1. 客户端和服务器还是跟原来一样进行 `TCP` 三次握手,握手完后,客户端和服务器建立起了连接;
2. 客户端像服务器发送一系列说明,比如客户端使用的 `TLS` 协议版本,支持的加密算法等等;
3. 服务器拿到了客户端发送而来的说明,从中获取到客户端支持的 `TLS` 协议版本和支持的加密算法列表,从列表中选择一个合适的加密算法,将选择的加密算法和证书一同发送给客户端;
4. 客户端拿到确定的 `TLS` 版本和加密算法,并检测服务端的证书,通过后使用公钥进行加密某个数据(例如:“完成”);
5. 服务器使用私钥解密客户端公钥加密过的消息,并验证 `MAC` ( Message Authentication Code ,消息认证码)把解密出的消息(例如:“完成”)使用私钥加密发送给客户端;
6. 客户端使用公钥解密消息,并验证 `MAC` ,通过后加密通道建立,以后在该加密通道进行的数据传输都采用对称秘钥对数据加密。
由此可见,是先经过了非对称加密,最后再进行对称加密,也即——对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,服务器使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行数据传输。流程如下图所示:
## iOS 中的 HTTPS
`AFSecurityPolicy` 这个类为 AFN 设置 SSL 钢钉的类,有 3 种验证方式:
1. `AFSSLPinningModeNone`:这个模式表示不做 SSL pining ,跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。如果证书是信任机构签发的就会通过,如果是自己服务器生成的证书,就不会通过。(注意:该模式是不安全的,HTTPS API 能正常访问,但没有校验证书,失去了 HTTPS 的意义)
2. `AFSSLPinningModePublicKey`:代表客户端回将服务器端返回的证书与本地保存的证书中的 `PublickKey` 部分进行校验,校验通过后才继续进行。
3. `AFSSLPinningModeCertificate`:代表客户端会将服务器端返回的证书和本地保存的证书中的所有内容进行匹配,全部进行校验,如果正确,才继续进行通信。
2 和 3 都需要把证书内置在 app bundle 中。
## 超文本链接
原来超文本链接可以用指针去理解,指针是指向了一块内存地址,那超文本链接实际上就是指向了服务器上的一个资源位置哇!!!
## `HTTP` 协议特点
优点:解放了服务器,每一次请求“点到为止”,不会造成不必要的连接占用。
缺点:每次请求会传输大量的重复内容信息。
* 支持客户端/服务器模式
* 简单快捷
* 灵活
* 无连接
* 无状态
**无连接**:服务端处理完客户端一次请求,等到客户端作出回应之后(确定收到)便断开连接。这种方式节省传输时间,但随着业务量的庞大,如果还采用原来的方式,会在建立和断开连接上话费大部分时间。`HTTP` 借助底层的 `TCP` 虚拟连接(并不是真实的电路连接),`HTTP` 协议无需连接,比如 A 和 B 打电话,A 和 B 两者并没有进行“连接”,而是借助了电话简化了连接从而进行交换信息。
**无状态**:服务端对客户端每次发送的请求都认为是一个新的请求,上一次会话和下一次会话之间没有联系。之所以这么设计,也是为了让 http 变得简单,可以处理大量事物。但无状态的特效,也导致了一些问题,比如说一个用户登录一家网站后,跳到另一个页面,应该还保持着登录状态,所以后面就推出了 cookie 状态管理技术。
**请求只能从客户端开始**:客户端不可以接收除响应之外的指令,服务器必须等待客户端的请求,才能给客户端发送响应数据,服务器时不能主动给客户端推送数据的,对于一些实时监控的功能,常用 websocket 来代替。
**没有用户认证,任何人都可以发起请求**:不存在确认通信方的处理步骤,任何人都可以发起请求,且服务器只要收到请求,无论是谁,都会返回一个响应,所以会存在伪装的隐患,https 可以解决这个问题。
**通信使用的是明文**
**无法验证报文完整性**
**可任意选择数据压缩格式,非强制性压缩发送**
**HTTP 0.9**:短连接。每个 `HTTP` 请求都要经历一次 `DNS` 解析,三次握手,传输和四次挥手。
**HTTP 1.0**:持久连接(长连接)被提出来。在此之前,每次连接只处理一个请求,且每个连接的获取都需要创建一个独立的 `TCP` 连接,因为 `HTTP` 是基于 `TCP/IP` 协议的,创建一个 `TCP` 连接需要经过三个步骤,有一定的开销,如果每次通讯如果每次都需要重新建立连接,对性能有影响,所以最好是需要维护一个长连接。当一个 `TCP` 连接对服务器做了多次请求:客户端可以在 `request header` 中携带 `Connection: Keep-Alive` 字段向服务器请求持久连接,若服务器允许就会在 `response header` 中加上相同字段。
双方都确认后,客户端便可继续使用同一个 `TCP` 连接发送接下来若干请求。`Keep-Alive` 默认是 `[timeput=5, max=100]` ,即每一个 `TCP` 连接可以服务最多 5 秒内的 100 次请求。当服务端主动切断一个长连接时(或不支持),则会在 `response header` 中携带 `Connection:Close` ,要求客户端停止使用这一连接。
长连接机制仍然是串行的,如果某个请求出现网络阻塞等问题,会导致同一条连接上的后续请求被阻塞。
**HTTP 1.1**:提出 `piplining` (管线化)机制,且默认支持长连接,就算客户端 `request header` 中未携带 `Connection:Keep-Alive` ,传输也会默认支持。客户端发起一次请求时不必等待响应便直接发起第二个请求;服务端根据请求顺序一次放回结果。该机制基于长连接完成,且只有 `GET` 和 `HEAD` 请求可进行 `piplining` , `POST` 请求会有所限制。第一次建立连接时服务器不一定支持 `HTTP 1.1` 协议。
该机制可将 `HTTP` 请求大批量提交,将多个请求同时塞入一个 `TCP` 分组中,达到只提交一个分组即可同时发出多个要求,大幅缩短页面加载时间(特别是在传输延迟较高的情况下),减少网络上多余的分组并降低线路负载。
支持只发送 `header` 信息( `HEAD` 方法),如果服务器认为客户端有权限请求,则返回 100 ,否则返回 401 。客户端如果接受到 100 ,才开始把请求 `body` 发送到服务器,并且还支持传送内容的一部分,当客户端已经有了一部分资源后,只需要跟服务器请求另外部分资源即可(断点续传的基础)
**HTTP 2.0**:多路复用技术出现。能够让多个 `request` 和 `response` 杂糅在一起,通过 `streamID` 区别。
## TCP
* `TCP` 提供一种面向连接的、可靠的字节流服务;
* 在一个 `TCP` 连接中,仅有双方进行彼此通信。广播和多播不能用于 `TCP`;
* `TCP` 使用校验和、确认和重传机制来保证可靠传输;
* `TCP` 给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复;
* `TCP` 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制。(🧐 没搞懂)
`TCP` 连接有一个“预热”过程,先检查数据是否传输成功,一旦传输成功过,则慢慢加大传输速度。如果对应瞬时并发的连接,服务器的响应就会变慢。
### 三次握手
三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送 **3** 个包。三次握手的目的是为了连接服务器制定端口,建立 TCP 连接,并同步连接双方的 **序列号** 和 **确认号** ,交换 TCP 窗口大小信息。
* 第一次握手:客户端发送一个位码 SYN = 1 ,以及随机产生的 seq number = x 。发送完毕后,客户端进入 `SYN_SEND` 状态。服务器由 syn = 1 得知客户端需要建立连接。
* 第二次握手:服务器要确认连接信息,向客户端发送 ack number = y ( x + 1 ), syn = 1 , ack = 1 ,并随机产生 seq = y。发送完毕后,服务器进入 `SYN_RCVD` 状态。
* 第三次握手:客户端收到数据包后,验证 y == x + 1 ,位码 ack == 1 。验证通过后,客户端发送 ack number = z(y + 1) , ack = 1 ,发送完毕后,客户端进入 `ESTABLISHED` 状态,服务器收到后验证 z == y + 1 ,ack == 1 ,验证通过后,建立连接,服务器进入 `ESTABLISHED` 状态,TCP 三次握手建立连接结束。
## ARP
ARP 协议 `OSI` 五层模型中的数据链路层,是把 IP 地址转化成 MAC 地址的一个 `TCP/IP` 协议。
## 负载均衡
客户端将请求发送至服务端,单一服务器无法承受过高并发量,可将请求转发到其它服务器,但真正的负载均衡架构并不是一台 server 转发到另一台 server ,而是在客户端和服务器中间加入一个专门**负责分配请求**的负载均衡硬件(软件)
### DNS(Domain Name System)
是客户端发送请求中一个非常重要的中转,它的作用是讲用户请求的 URL 地址映射为具体的 IP 地址,全世界有 13 台根服务器,但通常对我们做域名解析的并不是根服务器,而是直接访问我们的 LDNS (Local DNS Server),通常由 ISP 维护。
最开始的负载均衡就是利用搭建本地 DNS 服务器实现的,实现的方式简单易懂,为同一个主机名分配多个映射,可采用轮询、随机等方式分配请求。
但在使用过程中会发现,如果其中一个地址宕机,我们无法及时发现。若有用户被分配到了该主机则会出现访问失败的状况,同时我们也无法判断每个 server 的负载,可能会出现某个 server 几乎闲置,另外一个 server 负载压力极高的情况。
### 负载均衡器(Load Balancer)
负载均衡器通常作为独立的硬件置于客户端和服务器之间。其拥有非常好的负载均衡性能,用户众多的负载均衡策略,如权重、动态比率、最快模式、最小连接数等,可以保证以相对较优的方式分配请求,但价格过高。
### 反向代理
一般使用 `Nginx` ,其高性能、轻量级已经成了人们对 Nginx 的第一印象。 Nginx 可作为 HTTP 服务器,在处理高并发请求时拥有比现在主流 Apach 服务器更高的性能,同时它也是一个优秀的反向代理服务器。
正向代理通常由客户端主动链接,比如科学上网。反向代理在服务器端,无需主动连接,当我们访问拥有反代的网站时,实际上访问的是其反代服务器,而非真正的服务器,当请求到达反代服务器时,其再将请求转发至服务器。
## CDN(Content Delivery Network)
简单来说其为存储一些静态文件的一台或多台服务器,通过复制、缓存等方式,将文件保存其中。
### 哪些属于静态文件?
CSS,HTML,图片等多媒体都属于静态文件,用户发送的请求不会影响静态文件的内容,而 JSP、PHP 等文件不属于静态文件,因为内容会随着用户的请求而改变。
### CDN 如何实现加速?
一般情况下,我们要获取的数据都在主服务器中,但用户若在北方,主服务器在南方,访问速度就会变慢,而变慢的原因有很多,如传输距离,运营商,带宽等等因素。而使用 CDN 技术,我们会将 CDN 节点分布在各地,当用户发送请求到达服务器时,服务器会根据用户的区域信息,为用户分配最近的 CDN 服务器。
### CDN 数据从哪里来?
复制、缓存、CDN 服务器可以在用户请求后缓存文件,也可以主动抓取主服务器内容。
## 从输入一个 url 到返回数据,中间到底发生了什么?
### 浏览器解析出主机名
例如从搜索框中拿到的域名为:`http://pjhubs.com`
### 浏览器查询这个主机名的 ip 地址(DNS)
DNS 解析的作用就是把域名解析成 ip 地址,这样才能在广域网路由器转发报文给目标 ip ,不然路由器不知道要把报文发给谁。
* 浏览器启动时,首先会从 OS 获取 DNS 服务器地址,然后把地址缓存下来,同时浏览器还会去读取和解析 hosts 文件,同样进行缓存。浏览器对解析过的域名和 ip 地址都会保存着这两者的映射关系(存到 cache 中)。
* 当解析域名时,首先浏览器回去 cache 中查找有没有缓存好的映射关系;如果没有,则去 hosts 文件中查找;如果没有,浏览器则会发起请求去 DNS 服务器缓存查询;如果没有,最后则去 DNS 服务器查询。
假设 pjhubs.com 的 ip 地址为:123.123.123.123
### 浏览器获取端口号
假设为 123.123.123.123:80
### 浏览器向目标 ip 发起一条 123.123.123.123:80 的 tcp 连接
为了传输的可靠性,tcp 协议要有“三次握手”的过程(细节上文):
* 浏览器向服务器发起一个连接请求;
* 服务器对请求作出响应,表示同意建立连接;
* 浏览器收到响应后,再告知对方,它知道服务器同意它建立连接了。
### 数据包在 ip 层传输,通过多台计算机和网络设备中转,在中转时利用中转设备的 mac 地址搜索下一个中转目标(采用 `ARP` 协议,根据通信方的 ip 地址就可反查出对应的 mac 地址),直到目标 ip 地址。
### 数据链路层处理网络连接的硬件部分,比如找到服务器的网卡
### 浏览器向服务器发送一条 http 报文
每一条 http 报文的组成:起始行 + 首部 + 主体(可选)
* **起始行**:http/1.1 200 OK (一般包括 http 版本、状态码、状态码信息)
* **首部**:Content-Type:text/plain Content-Length:19
* **主体**:请求字段数据
### 服务器接受客户端请求,进行一些处理,返回响应报文
web 服务器接收到请求之后,实际上会做:
* 建立连接:如果接受一个客户端连接,就建立连接,如果不同意,就将其关闭;
* 接收请求:读取 http 请求报文;
* 访问资源:访问报文中制定的资源;
* 构建响应:创建带有首部的 http 响应报文;
* 发送响应:将响应回送给客户端。
### 浏览器读取 http 响应报文
### 浏览器关闭连接
### `0.0.0.0` 和 `255.255.255.255`
计算机的 `ip` 获取方式有**静态ip** 和 **动态ip** 两种获取方式,但大部分的设置都是动态 ip 获取方式,除非我们能保证的 ip 地址的唯一性,否则会不小心配置了一个已经
被别人使用过的 ip 地址(还包括 DNS、网关等)。
那什么是动态获取 ip 地址呢?如果计算机重启之后,此时啥 ip 也没有,需要找到 `DHCP 服务器` 发送一个报文来动态获取 ip,但此时计算机并不知道谁是 `DHCP 服务器`,
需要发送一个**广播**告诉当前局域网内的所有设备,该报文的**目的ip**部分填入 `255.255.255.255`,代表这是一个**广播报文**。直到真正的 `DHCP 服务器`收到这个报
文,但是 `DHCP 服务器`并不知道这个报文是要要**获取ip**,所以在计算机发出这个报文之前,要在这个报文的**源ip**地址部分填入 `0.0.0.0`,借此来标记出 `DHCP 服务器`
此时收到的该报文是要获取ip。
但是 `DHCP 服务器` 怎么知道是当前局域网中的哪台计算机需要获取 ip 呢?利用 `Mac` 地址。所以 `255.255.255.255` 该地址一般用来**广播**时使用,而 `0.0.0.0` 可以
代表这是一个还没有分配 ip 的主机。当然 `0.0.0.0` 还有其它作用,用到了再更。s
================================================
FILE: Blockchain/basic.md
================================================
## 区块链技术综述
1. 区块链是从比特币中脱离出来的。
2. 加密货币
3. 分布式共识
* 想要生成一个加密货币,还要所有人去承认它
4. ecash 盲签名技术
5. hashcash 解决邮件系统中 DoS 攻击问题
* 提出使用「工作量证明」(POW Proof of Work)机制来获取额度
6. B-money 引入数字货币生成过程。
7. 比特币
* 加密基础理论发展:RSA 算法 & 公钥私钥加密体系 PPKC
* P2P 技术开发成熟
* hash 现金解决双重支付问题(痛点)
8. 量子计算机对 RSA 有破坏性
9. 分布式数据库存储
* 没有中心系统
10. 如何保证每个节点的数据一致性问题?
* 如果有中心系统的话,只需要保证中心系统这一个节点的数据一致性就好了。
* 为什么要引入分布式?
* 高可用、稳定性问题
* 一致性 hash
* 冗余存储
* 强一致性、弱一致性、最终一致性。根据业务特征去选择
* FLP 不可能性原理
* 在一个分布式系统中不可能在同一个时刻保证数据一致性(强一致性
* 通过「共识算法」解决
* 拜占庭问题。打不打这个仗,需要五个将军来投票,超过 1/2 就打,而不是让一个将军自己去决定,防止被策反。
* 通过 POW 算法进行优化
* POW:工作量证明,通过计算来猜测一个数值,得以解决规定的 hash 问题。保证在一段时间内,系统中只能出现少数合法提案。
11. hash 算法
12. 加解密算法
* 解决我发给你的消息,只有你才能知道
13. 数字签名
* 解决大家都知道这个消息是我发的
14. 密钥、地址和钱包
* 密钥 => 私钥,不需要在网络上进行传播
* LevelDB
* 钱包不存钱,钱包存的是交易记录,谁给你转了钱,你给谁转了钱
* 基于椭圆曲线的公钥、私钥
* 私钥丢了就是丢了!!!没人知道这个私钥是你的私钥
15. 交易
* 创建一个交易
* 在网络上广播这个交易
* 比特币找零:别人给我输入了 15 个比特币,我给别人输出 13 个比特币,还剩 2 个比特币再输入给自己。产生三笔交易
* 交易的确认
* 确认 A 是 A,通过 A 的签名
*
16. 挖矿
* 把交易写入区块链,就是挖矿
* 交易不产生比特币,挖矿产生比特币
* 挖矿的过程中,回去监听比特币网络,正在挖 78 这个块时监听到其它矿工已经挖完了 78 这个块,自己必须要立马放弃,因为要让自己利益最大化,去挖 79 这个块。
* 博弈论。
17. 贪心算法
18. 哈希二叉树。用作快速归纳和椒盐大规模数据完整性的数据结构,包含加密哈希值。
19. 图灵不完备系统
20. 智能合约
================================================
FILE: Books/iOS面试之道.md
================================================
## 第一天
### 字典和集合
一般的字典和集合都要求它们的 Key 都必须实现 Hashable 协议,Cocoa中的基本数据类型都满足这一点。
### 字符串
Swift 中的字符串为值类型,而不是 OC 中的引用类型。
#### 判断字符串是否由数字构成
```Swift
var str1 = "123ws"
// nil
Int(str1)
var str2 = "123"
// not nil
Int(str2)
```
### Swift 的访问修饰符
`private`: 当前类内使用
`fileprivate`:当前文件内使用
`internal`:(默认访问级别)整个 module 里使用
`public`:可被任何地方使用,但除了 ·mudule 外不可以被继承和 override
`open`:可被任何地方使用
### 如何检测一个链表中是否有环?
用 **快行指针** 的做法,一个指针在前,一个在后,两个指针的间隔一般为 2 ,循环终止条件为链表尾,如果有快指针和慢指针走到一起了,则该链表成环。
### 栈和队列的转化
用栈实现队列(腾讯一面):
先写一个转换函数,把栈 A 的元素都 pop 到 栈 B 中,进队相当于给栈 A push ,出队之前先执行转换函数,然后再 pop 栈 B 元素。
用队列实现栈(腾讯一面):
先写一个转换函数,这个函数把队列 A 中的除了队尾元素外的所有元素都入队到队列 B 中,那么经过这个转换函数转换之后队列 A 中剩下的就是栈顶元素。
再写一个函数,把队列 B 和队列 A 进行对调,此时因为队列 A 已经全部出队,所以队列 B 为空,队列 A 为少了之前的队尾元素队列
---
## 第二天
### 二叉树的遍历
因为二叉树本身是由递归定义的,从原理上讲,所有二叉树的题目都可以用递归来解。二叉树的遍历主要由 `BFS` (前中后序遍历)和 `DFS` (层级遍历)两种组成,需要注意的是,广度优先遍历需要用队列进行搭配
### 排序算法
动画和代码展示:[https://www.cnblogs.com/onepixel/articles/7674659.html](https://www.cnblogs.com/onepixel/articles/7674659.html)
## 第三天
### 几乎都是算法
比如 排序、动态规划、二分查找等等。慢慢总结吧
---
## 第四天
### inout 关键字
使用 `inout` 关键字可以修改传入参数的原始值,调用的时候需要在对应的参数前加上符号 `&` ,类似 `C/C++` 中的指针。
### protocol
在转 `Swift` 将近三四个月的过程中,我居然一点都没有感到在写 `protocol` 时代理对象居然不加 `weak` 关键字感到奇怪。
举个例子,之前我是这么粗暴的写 `protocol` :
```Swift
protocol PjhubsDelegate {
func pjhubsDeleagteFunction()
}
class Pjhubs: UIView {
var viewDelegate: PjhubsDelegate?
}
```
就这么写了三四个月,看书看着看着才猛的发现,为啥我要把 `weak` 去掉,遂改成了以下代码:
```swift
weak var viewDelegate: PjhubsDelegate?
```
此时,`Xcode` 给我报了个错,
`'weak' must not be applied to non-class-bound 'PjhubsDelegate'; consider adding a protocol conformance that has a class bound`
也就是说:`weak` 只能能添加到非类绑定的 `PjhubsDelegate` 上,考虑给其添加上一个类绑定。根据提示,代码修改为:
```Swift
protocol PjhubsDelegate: class {
func pjhubsDeleagteFunction()
}
class Pjhubs: UIView {
weak var viewDelegate: PjhubsDelegate?
}
```
总的来说, `weak` 修饰引用类型,而上文中我所定义的 `protocol` 为值类型,所以 `Xcode` 报了错。如果不加 `weak` 修饰,则表明我们的 `protocol` 可为枚举、结构体所使用,所以当我们使用 `weak` 修饰了代理对象,那么就要求代理(协议)为 `class-only` (只类使用)
### copy-on-write
值类型在复制时,新对象和原对象实际上在内存中指向同一块区域,只有当新对象发生改变时(增加或删除一个对象),才会给新对象开辟新内存区域。
### 属性观察
最开始的时候,我直接在 `Swift` 中拿了 `OC` 的思想做了 `setter & getter` ,但是当我想只要 `setter` 时,一定要把 `getter` 写上,就算 `getter` 什么也不做,只是返回一个存储属性的值而已。
后边理解到了 `Swift` 中的属性观察,即 `willSet & didSet` ,意思就是方法名所代表的意思。需要注意的是,在初始化器中对属性的设定,以及在 `willSet & didSet` 方法中对属性的再次设定,都会出发调用属性观察。
### @autoclosure
[http://swifter.tips/autoclosure/](http://swifter.tips/autoclosure/)
---
## 第五天
### 柯里化(Curring)
[http://swifter.tips/currying/](http://swifter.tips/currying/)。说实话,书中和喵神的这篇 blog 描述的代码都很简单,主要是这种神奇的写法根本没见过,书中的例子是这样的,要求写一个函数满足只传入一个整数参数,返回该整数 +2 的值,这很简单对吧,但是实际的要求是只写这么一个函数,然后满足 +2、+3、+4 等,其实我内心直接就冒出多态、模版、范型啦这些东西,但仔细一想,不对啊,只能有一个参数,瞬间缓过神来,柯里化牛逼啊。😂。
```Swift
func add(_ num: Int) -> (Int) -> Int {
return { val in
return num + val
}
}
let number2 = add(2)
print(number2(2))
```
### 实现一个函数:求 0 ~ 100 (包括 0 和 100)中为偶数并且恰好是其他数字平方的数字
```Swift
(0...100).map { $0 * $0 }.filter { $0 % 2 == 0 }
```
Swift 函数式编程的一些资料:[https://www.jianshu.com/p/7233f140e6c3](https://www.jianshu.com/p/7233f140e6c3)
### ARC
ARC 和 Garbage Collection 的区别在于:Garbage Collection 在运行时管理内存,可以解决 retain cycle, 而 ARC 在编译时管理内存
### @property 关键字
在 OC 中基本数据类型的默认关键字是 `atomic`、`readwrite` 和 `assign`,普通属性的默认关键字为 `atomic`,`readwrite` 和 `strong`
### 关键字 automic 和 nonatomic
* automic : 修饰的对象保证 setter 和 getter 的完整性,任何线程访问它都可以的哦大一个完整的初始化对象。正是因为要保证操作的完整性,所以速度较慢。automic 比 nonatomic 安全,但也不是绝对的线程安全,当多个线程同时调用 set 和 get 时,会导致获得的对象值不一样。想要获得线程绝对安全,使用 @synchronized (个人觉得 @synchronized 的做法也不好,这是 iOS 中最垃圾的锁哈哈)
* nonatomic : 修饰的对象不保证 set 和 get 的完整性,所以当多个线程访问它时可能会返回未初始化的对象,故其速度会比 atomic 快,但线程也不是安全的。
### runloop 和 线程的关系
runloop 是每一个线程一直运行的一个对象,它主要用来负责响应需要处理的各种事件和消息。每一个线程都有且仅有一个 runloop 与之对应,没有线程,就没有 runloop 。在所有线程中,只有主线程的 runloop 是默认启动的,main 函数会设置一个 NSRunLoop 对象,而其它线程的 runLoop 默认是没有启动的,可以通过 `[NSRunLoop currentRunLoop]` 启动。
### code show
```objc
NSString *firstStr = @"helloworld";
NSString *secondStr = @"helloworld";
if (firstString == secondStr) {
NSLog(@"Equal");
} else {
NSLong(@"Not Equal");
}
```
最终将打印出 "Equal",`==` 该符号是判断着两个指针是否指向同一个对象。上段代码尽管指向不同对象,但它们的值相同,iOS 编译器优化了内存分配,当两个指针指向两个值一样的 `NSString` 时,两者指向同一个内存地址。
## 后续内容已分门别类进行了归档。
================================================
FILE: CV/basic.md
================================================
# 图形学 & 视觉
## 基础概念
### OpenGL
OpenGL 本身不提供源码实现,其只是定义了一堆协议接口,各个平台自行根据统一规范好的协议,去实现各自平台上 OpenGL 协议接口,几乎没有不支持 OpenGL 协议的硬件平台。
## 微软的 Direct3D
一开始各家系统厂商都提供了 OpenGL 的实现,微软也实现了一份,但在当时的时代北京下,PC 无法良好的运行 OpenGL 相关的能力,单是跑起来事例程序就已经很慢了。再加上当时微软看上了 video game 的市场,想要做 3D 相关的事情,顺带收购了一家做 3D 渲染框架的公司,其中 Direct3D 就是这套框架的 API 集合。
在同时支持 OpenGL 时微软也在持续完善 D3D 的能力,使其成为 win 平台上的最佳图形 API,最终 OpenGL 的版本更新停滞在了 1.1,后续在 win 平台上都是通过 D3D 来完成图形操作。
## 一些小 case
### 如何去除视频封面的黑边?
* 逐行检测去黑边,设置黑边的阈值为 <10 都是黑边
* 如果遇到有水印的视频,做「腐蚀」
### 反转视频检测
* 通过「人脸检测」去做这个事情;
* 设原视频的人脸检测数据为 `face`,旋转 90 度之后视频的人脸检测为数据为 `face_r`;
* 如果 `face` > `face_r` 说明原视频是反转的。
注意前提:人脸检测不经过调整的情况下只能检测正常角度。
================================================
FILE: Flutter/Dart.md
================================================
# Dart
## 语言
### 私有变量
变量以下划线(_)开头,在 `Dart` 语言中使用下划线前缀标识符,会强制其变成私有的。
### null
dart 中任何类型变量都可以判 `null`。
================================================
FILE: Flutter/Flutter_2.md
================================================
原文地址:[PJHubs](http://pjhubs.com/2019/01/14/flutter-2/)
> 在上篇文章中,已经大致的描述出为什么要接触 Flutter 以及对 Flutter 初体验的一些总结,整体上 Flutter 给我的第一印象是不太好的,但这次不一样了!这篇文章主要描述了我在使用 Flutter 实现的豆瓣电影 Top250 demo 过程,让我领略到了 Flutter 在 UI 层面的魅力!
## 前言
目前只完成了 demo 的 **UI 部分**,主要体验了 Flutter 在基本 UI 层面上友好度。从整体来看,在一些细节的地方确实没有原生(iOS & Android)有太大的优势,在某些 UI 上的实现还比较麻烦。如果仅仅从跨端开发这一点上看,优势就相当明显了,如[上篇文章](http://pjhubs.com/2019/01/11/Flutter-1/)中所说,Flutter 在 SDK 层面直接替换掉了整个与原生相关的框架,采用了 `Skia` 替换,在跨端上能够很好的保证 UI 最终渲染出来的结果统一。 demo 如下:

### 数据来源
原本想直接使用公司 API 进行测试,这样能够快速验证在接触到实际数据的过程中,Flutter 在 UI 层面上和原生的优劣,但因保密等原因,只能另寻它路。最终从[历史上的今天](http://www.ipip5.com/today/api.php?type=json)和[豆瓣电影 Top250 ](https://api.douban.com/v2/movie/top250)两个 API 中选择了后者,原本只是想验证在长列表的展示上的优劣,但后来又考虑到了豆瓣电影 Top250 的 API 所提供的资源较丰富、数据格式也够复杂,算是比较贴合生产环境。


### 涉及 Flutter 知识点
* HTTP;
* JSON 数据格式解析和模型转换;
* `Row` 、 `Column` 、 `Padding` 、 布局;
* `ListView`、`Text` 等基本 `Widget` 使用;
* `Container` 设置图片圆角、阴影等属性设置。
## 实践
有了数据源,就可以准备上手搭建 UI 了。因为是豆瓣电影 Top250 的数据源,就直接 copy 了官方 App 的设计,同时也为了保证后续做性能验证时各种跨端和原生技术互相对比时遵循“单一变量”原则,但中途还是因为数据源的关系,有些数据并未暴露出来,导致没法 100% 的 copy 。

### 数据处理
Flutter 中进行 `RESTful` web API 请求是一件比较流畅的事情。在 flutter 中使用 http 拉取豆瓣电影 Top250 的数据,我是这么做的:
```Dart
import 'dart:io';
import 'dart:convert';
import 'package:movie_top_250/movieModel.dart';
class MovieAPI {
Future getMovieList(int start) async {
var client = HttpClient();
var request = await client.getUrl(Uri.parse(
'https://api.douban.com/v2/movie/top250?start=$start&count=100'));
var response = await request.close();
var responseBody = await response.transform(utf8.decoder).join();
Map data = json.decode(responseBody);
return MovieEnvelope.fromJSON(data);
}
}
```
对 API 数据请求方法做了简单的封装,并返回 `Future` 类型数据。flutter 中进行异步操作,返回的都是“懒加载”数据,上文我们也说到,豆瓣电影 Top250 API 返回的数据格式较复杂,直接使用 response 中的数据成本很大,所以在此又封装了两个 Model ,分别为 `Movies` 和 `Movie`。
```Dart
class Movie {
String id;
String rating;
String stars;
String title;
String director;
String year;
String poster;
List genres;
Movie({
this.id,
this.rating,
this.title,
this.director,
this.year,
this.stars,
this.poster,
this.genres
});
Movie.fromJSON(Map json) {
this.id = json['id'];
this.rating = json['rating']['average'].toString();
this.stars = json['rating']['stars'];
this.title = json['title'];
this.director = json['directors'][0]['name'];
this.year = json['year'];
this.poster = json['images']['small'];
this.genres = new List.from(json['genres']);
}
}
class Movies {
int count;
int start;
int total;
List movies;
Movies({this.count, this.start, this.total, this.movies});
Movies.fromJSON(Map data) {
this.count = data['count'];
this.start = data['start'];
this.total = data['total'];
List movies = [];
(data['subjects'] as List).forEach((item) {
Movie movie = Movie.fromJSON(item);
movies.add(movie);
});
this.movies = movies;
}
}
```
### UI
UI 部分在上篇文章中快速入门了一下,但比较 `Dart` 对于我来说是一门全新的语言,光是在“数据处理”环节中就耗费了一部分精力(虽然最后的代码较精简),原本打算就将就的写写就完事了,但睡了一觉醒来后,告诉自己并不能放弃!继续开始对豆瓣电影 Top250 App 页面的布局进行分析。

因为本来就没打算把这个 demo 做得多么完美,只想尽可能的做到贴近 app 的展示,所以没有采用其它更适合的布局 `Widget`,经过分析后发现只用简单的 `Row` 和 `Column` 布局几乎可以完成大部分工作。
#### 主体
```Dart
import 'package:flutter/material.dart';
import 'package:movie_top_250/movieApi.dart';
import 'package:movie_top_250/movieModel.dart';
void main() => runApp(MyApp(movies: MovieAPI().getMovieList(0)));
class MyApp extends StatelessWidget {
final Future movies;
var page = 0;
MyApp({Key key, this.movies}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '豆瓣电影 Top250',
theme: ThemeData(
primaryColor: Colors.black,
),
home: Scaffold(
appBar: AppBar(
title: Text('豆瓣电影 Top250'),
),
body: _buildList(),
),
);
}
}
```
排除掉其它 `Widget` 组件后,整个 demo 的骨架如上图所示,整体来看还算清晰,而且与 HTML 的骨架也基本类似,可以说 `Dart` 当初的为了替换 JS 目的的影子还是十分明显的。
#### ListView Widget
在 `_buildList()` 方法中主要利用了 `ListView Widget` 进行搭建。令我感到意外的是,`ListView Widget` 居然没有属性进行设置分割线!当然,在 `Weex` 和 `React-Native` 中同样也是没有的,这两个框架本质上就是写的 HTML ,接着我又想到 `Dart` 不也是要替换前端三剑客霸主的么?这么一想就没啥问题了。
```Dart
// body List Widget
Widget _buildList() {
return FutureBuilder(
future: movies,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
// 加了分割线,长度需要为两倍
itemCount: snapshot.data.movies.length * 2,
itemBuilder: (context, index) {
if (snapshot.data.movies.length - index < 10) {
MovieAPI().getMovieList(++page);
}
if (index.isOdd) {
//是奇数
return new Divider();
} else {
index = index ~/ 2;
return _buildListRow(snapshot.data.movies[index]);
}
});
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return new Center(
child: new CircularProgressIndicator(
backgroundColor: Colors.black
)
);
},
);
}
```
分割线的设置利用了 `Divider Widget` 进行设置,而且还不能直接添加到单一 `item` 的渲染节点树中,必须重新起一个 `item` ,单独占据 `ListView` 的一个索引。上文这段代码的核心来自官方 demo,但写完后,个人觉得这么做有点不妥,总是担心后续在使用 `ListView` 树节点进行某些“特殊”操作时引发一些问题。
需要注意的地方是 `ListView` 渲染的 `cell` 条数会因为分割线的加入而减少一半,所以我们要对 `itemCount` 属性值变为模型列表的两倍长,以此来恢复正确需要渲染的 `cell` 数据。
#### ListViewRowPoster Widget
`_buildListRow(movie)` 方法主要是用来搭建 `cell widget` 而进行的简单封装。不得不说在编写该 `widget` 时,整体给我一种以为是在写 `CSS` 布局的错觉,这点给有一些 web 基础的同学上手会十分快!
```Dart
Widget _buildListRow(Movie movie) {
return Padding(
padding: EdgeInsets.all(10),
child: new Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new Container(
width: 100,
height: 150,
decoration: new BoxDecoration(
image: DecorationImage(image: NetworkImage(movie.poster)),
boxShadow: [
new BoxShadow(
color: Colors.grey,
offset: new Offset(0.0, 2.0),
blurRadius: 4.0,
)
],
borderRadius: new BorderRadius.all(
const Radius.circular(8.0),
),
),
),
_buildTextContent(movie),
]),
);
}
```
刚开始使用 `Image widget` 进行图片的加载,非常顺畅的就把图片资源加载出来了,等到后边开始统一美化数据时被“圆角”和“阴影”坑惨了,本以为给 `Image widget` 添加这两个属性是非常容易的事情,就像在 iOS 中给 `UIImageView` 或者 `UIView` 那般简单粗暴,但后来磕磕碰碰的查阅资料写出了“圆角”和“阴影”效果后才恍然大悟!
在 iOS 中之所以能够简单粗暴快速的给 `UIView` 和 `UIImageView` 添加上“圆角”和“阴影”属性,是因为二者都父类之一是 `UIView`,`UIView` 实现了 `CALayerDelegate` 协议,所谓的“圆角”和“阴影”都是 `CALayer` 的属性,所以设置的时候通常都是这么写:
```Swift
yourIamgeView.layer.cornerRadius = 8
yourIamgeView.layer.shadowColor = .black
```
但是在 flutter 中的 `Image widget` 只是继承自 `StatefulWidget` ,而 `StatefulWidget` 是继承自 `Widget` ,并不具备绘图能力,所以为什么没有“圆角”和“阴影”属性也就水落石出了。最后的做法是通过利用 `Container widget` 的 `decoration` 属性来添加相关修饰。
#### ListViewRowTextContent Widget
`ListViewRow Widget` 的左侧部分已经完成了,接下来就到了稍微复杂的右侧部分。在文章开头部分,我们已经看到了相关相关的设计图,主要是个整体纵向布局和几块小部分的横向布局。
```Dart
Widget _buildTextContent(Movie movie) {
return new Padding(
padding: EdgeInsets.fromLTRB(10, 0, 0, 0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(movie),
_buildRatingStar(movie),
_buildDetails(movie),
],
),
);
}
Widget _buildTitle(Movie movie) {
return new Row(
children: [
new Text(
movie.title,
style: new TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
new Text(
' (' + movie.year + ')',
style: new TextStyle(
color: Color.fromRGBO(150, 150, 150, 1),
fontWeight: FontWeight.w600,
fontSize: 18,
),
)
],
);
}
Widget _buildRatingStar(Movie movie) {
List icons = [];
int fS = int.parse(movie.stars) ~/ 10;
int f = 0;
while (f < fS) {
icons.add(new Icon(Icons.star, color: Colors.orange, size: 13));
f++;
}
while (icons.length != 5) {
icons.add(new Icon(Icons.star,
color: Color.fromRGBO(220, 220, 220, 1), size: 13));
}
icons.add(new Padding(
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
child: new Text(
movie.rating,
style: new TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(180, 180, 180, 1),
),
),
));
return new Padding(
padding: EdgeInsets.fromLTRB(0, 10, 0, 10),
child: new Row(children: icons),
);
}
Widget _buildDetails(Movie movie) {
var detailsString = '';
detailsString = movie.director;
detailsString += '/';
for (String name in movie.genres) {
detailsString += ' ' + name;
}
return new Container(
width: 230.0,
child: new Text(detailsString,
softWrap: true,
)
);
}
```
在这部分中,稍微费劲点的地方是 `_buildRatingStar()` 方法所构造的“评分组件”,在豆瓣 App 中浏览了好一会儿“评分组件”的星星是怎么个显示规则,琢磨了一会儿得出了一个结论:
* **9.2+**:五星;
* **8.1~9.1+**:四星半;
正准备拍手叫好接入数据时,突然发现了一个令人尴尬的事情,

其实 API 中已经给出了具体的评分,根本不需要我们自己去算!当时还奇怪怎么翻了前面几条数据的 `star` 字段数据都是 `"50"`,实际上是五星的意思......经过这样一番折腾后,后续的重点就转移到了星星的显示上,好在 flutter 提供了 `star` 这个 icon,但是却没有半个星星的 icon。权衡了一下决定就先这样吧,没有半星就没有了。
## 总结
因为时间和内容消化等因素,豆瓣电影 Top250 demo 将在下篇文章中完成:
* 下拉刷新;
* 上拉加载;
* 跳转页面,查看影片详情;
* 性能测试。
经过本次对 Flutter 在 UI 层面上的学习,对 flutter 的认识又更深了一步,反驳掉了自己在上篇文章中说 flutter 要凉的一部分。
================================================
FILE: Flutter/Flutter_3.md
================================================
## Flutter 三探
> 历时一个星期对 Flutter 一期调研在这篇文章中就告一段落了,这篇文章中继续完善上篇文章中利用豆瓣电影 Top250 公开 API demo。
## 前言
在这两三天的继续完善 demo 的时间中,首先是对 Flutter 在基本 UI 视觉方面的实现表示赞赏,有些地方的 UI 布局的代码编写习惯了 Flutter 的思维后,会有一个非常快速的反应。下面是具体 demo 具体的完成图:



整体 demo 做完后,全程都是在使用 meizu 15 这台开发机进行调试,在 flutter 的 IDE 选择上一直都在使用 `Android Studio`,在断点调试、查看渲染节点、性能对比等活动上都非常方便的解决了,依然强推!但整体没有遵循 Flutter 官方推荐的[ `BloC` ](https://cloud.tencent.com/developer/article/1345645) 设计模式,还是采用“设计模式之王”的 `MVC`,同样是考虑到了后续在对比其它跨段方案时尽可能的保证一致性。
### 数据来源
[豆瓣电影详情 API ](https://api.douban.com/v2/movie/subject/26942674)同样不需要做验证,传入对应电影的 id 即可,但会限制同一 IP 在一定间隔时间内的访问次数,如果在一定间隔时间内容访问 API 的速度过于频繁,则会拒绝服务,不过得益于 Flutter 的 `hot reload` 技术,可以不需要每次都重新拉去数据。该详情 API 多了一些更具体的数据,但依然没有达到豆瓣 App 本身那般丰富。

### 涉及 Flutter 知识点
* 下拉刷新;
* 上拉加载;
* 利用 `GestureDetector Widget` 进行页面跳转(动态路由方式);
* 利用 `SingleChildScrollView Widget` 进行滚动视图的构建;
* 简单性能分析。
## 实践
### 目录结构

### 数据处理
电影详情 API 返回的字段更多,同样可以确认的是 Model 也一定要从网络数据源中进行抛离,这同样也为后续构建子组件回填数据时提供方便,我的电影详情 Model 如下所示:
```Dart
class MovieMember {
String id;
// 成员姓名
String name;
// 详情 URL
String detailUrl;
// 中清晰度头像
String avatarUrl;
MovieMember({
this.name,
this.detailUrl,
this.avatarUrl,
});
MovieMember.fromJSON(Map json) {
this.id = json['id'];
this.avatarUrl = json['avatars']['medium'];
this.detailUrl = json['alt'];
this.name = json['name'];
}
}
class MovieDetail {
// 标题
String title;
// 上映年份
String year;
// 原名
String originalTitle;
// 所属国家或地区
List countries;
// 评分
String rating;
// "想看"人数
String wishCount;
// 星星
String stars;
// 高清晰度海报
String poster;
// 电影类型
List genres;
// 评分人数
int ratingsCount;
// 主要导演
List director;
// 主要演员
List casts;
// 简介
String summary;
MovieDetail({
this.title,
this.year,
this.countries,
this.rating,
this.stars,
this.poster,
this.genres,
this.ratingsCount,
this.director,
this.casts,
this.summary,
this.wishCount,
});
MovieDetail.fromJSON(Map json) {
this.title = json['title'];
this.year = json['year'];
this.summary = json['summary'];
this.poster = json['images']['large'];
this.ratingsCount = json['ratings_count'];
this.originalTitle = json['original_title'];
this.wishCount = json['wish_count'].toString();
this.rating = json['rating']['average'].toString();
this.stars = json['rating']['stars'].toString();
this.countries = new List.from(json['countries']);
this.genres = new List.from(json['genres']);
List castsMembers = [];
(json['directors'] as List).forEach((item) {
MovieMember movieMember = MovieMember.fromJSON(item);
castsMembers.add(movieMember);
});
this.director = castsMembers;
List directorMembers = [];
(json['casts'] as List).forEach((item) {
MovieMember movieMember = MovieMember.fromJSON(item);
directorMembers.add(movieMember);
});
this.casts = directorMembers;
}
}
```
在写 `MovieDetail` Model 时发现电影详情 API 返回数据源中的“演员”数据存在多字段必要数据,为了后续方便调用同样也抽离了一个 `MovieMember` Model(二期调研估计会继续做演员详情)。

网络数据的获取因为有了上篇文章的铺垫,这次再写一个速度明显快了很多,一期调研完整的网络层方法如下:
```Dart
import 'dart:io';
import 'dart:convert';
import 'package:movie_top_250/Model/movieModel.dart';
class MovieAPI {
Future getMovieList(int start) async {
var client = HttpClient();
int page = start * 50;
var request = await client.getUrl(Uri.parse(
'https://api.douban.com/v2/movie/top250?start=$page&count=50'));
var response = await request.close();
var responseBody = await response.transform(utf8.decoder).join();
Map data = json.decode(responseBody);
return Movies.fromJSON(data);
}
Future getMovieDetail(String movieId) async {
var client = HttpClient();
var request = await client.getUrl(Uri.parse(
'https://api.douban.com/v2/movie/subject/$movieId'));
var response = await request.close();
var responseBody = await response.transform(utf8.decoder).join();
Map data = json.decode(responseBody);
return MovieDetail.fromJSON(data);
}
}
```
### 下拉刷新与上拉加载
下拉刷新的整体与 Native 开发思路一致。上拉加载有根据业务有很多种实现方案,因为只是为了做验证性 demo ,直接采取了使用“静默加载”的思路,当然因为一次加载数据量太大(一页 50 条),所以在快速滑动列表时会导致下一页数据未载入等待的体验。如果不考虑过多的定制化操作,直接使用 flutter 系统组件是一件非常舒服的事情。完整代码如下:
```Dart
import 'package:flutter/material.dart';
import 'package:movie_top_250/Service/movieApi.dart';
import 'package:movie_top_250/Model/movieModel.dart';
import 'package:movie_top_250/View/List/movieListViewRowWidget.dart';
class MovieWidget extends StatefulWidget {
@override
State createState() {
return _DouBanMovieState();
}
}
class _DouBanMovieState extends State {
// 数据源
List movies = [];
// 分页
int page = 0;
@override
void initState() {
super.initState();
_requestData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('豆瓣电影 Top250'),
),
body: new RefreshIndicator(
child: _buildList(context),
onRefresh: _requestData,
color: Colors.black,
),
);
}
// 下拉刷新
Future _requestData() async {
movies.clear();
await MovieAPI().getMovieList(0).then((moviesData) {
setState(() {
movies = moviesData.movies;
});
});
return;
}
// 上拉加载
_requestMoreData(int page) {
print('page = $page');
MovieAPI().getMovieList(page).then((moviesData) {
setState(() {
movies += moviesData.movies;
});
});
}
// body List Widget
Widget _buildList(BuildContext context) {
var screenWidth = MediaQuery.of(context).size.width;
if (movies.length != 0) {
return ListView.separated(
itemBuilder: (context, index) {
// 还剩 15 条数据的时去拉取新数据
if (movies.length - index == 15) {
_requestMoreData(++page);
}
return new Container(
width: screenWidth,
child: buildListRow(index, movies[index], context),
);
},
separatorBuilder: (context, index) => Divider(
height: 1,
),
itemCount: movies.length);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}
}
```
### UI 分析
上文中也已经说到,因为豆瓣电影详情公开 API 所暴露出的数据有限,导致未能 100% 的重写。经过分析后主要将页面分为了以下几部分:

#### 第一部分
第一部分与上篇文章中所讲述的布局编写思路大部分一致,对于我自己来说有个需要注意的地方,在第一部分中有个“豆瓣电影排名”的 `badge`,原本打算是用 `RichText Widget` 进行实现的,但翻完属性后发现并没有提供 `decoration` 字段进行修饰,最后直接使用了两个 `DecoratedBox Widget` 作为父容器,在其 `decoration` 属性下使用 `BoxDecoration Widget` 完成“一左一右”圆角的 `badge` 组件编写,flutter 在组件“半圆角”的实现过程比 iOS 原生实现的代码量上少太多了(不封装的话),实现代码如下:
```Dart
Widget _buildBadge(int index, MovieDetail movieDetail) {
index++;
return new Row(
children: [
DecoratedBox(
child: Padding(
padding: EdgeInsets.fromLTRB(7, 3, 7, 3),
child: Text('No.$index',
style: TextStyle(color: Colors.brown, fontSize: 14))),
decoration: new BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(5),
bottomLeft: Radius.circular(5)))),
DecoratedBox(
child: Padding(
padding: EdgeInsets.fromLTRB(7, 3, 7, 3),
child: Text('豆瓣Top250',
style: TextStyle(color: Colors.orangeAccent, fontSize: 12))),
decoration: new BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.only(
topRight: Radius.circular(5),
bottomRight: Radius.circular(5)))),
],
);
}
```
在实现“想看”和“看过”两个按钮组件时,我使用了 `RaisedButton Widget` 。一开始是这么写的:
```Dart
new RaisedButton(
onPressed: null,
color: Colors.white,
child: new Text('B'),
textColor: Colors.black,
)
```
当显示出来后,不管怎么调整样式、修改颜色、文字等都不管用。最后带着郁闷的心情浏览官方文档,居然发现了这么一段话:

嗯,就算我们并不想让这个 Button 响应任何点击事件也不能给这个属性置空,并且也不能删除,因为这是个必须参数......这点需要注意。让同样也没想到的是 `RaisedButton` 没有 `text` 或类似设置按钮文本的属性,而是给了一个 `child` 属性,被 `UIButton` 虐过几次后在 `Flutter` 中看到某个组件提供了 `child` 属性现在就两眼放光!完成的代码如下:
```Dart
Widget _buildButton() {
return new Padding(
padding: EdgeInsets.fromLTRB(0, 20, 0, 0),
child: new Row(
children: [
new Padding(
padding: EdgeInsets.fromLTRB(0, 0, 10, 0),
child: new RaisedButton(
onPressed: () {},
color: Colors.white,
child: new Row(
children: [
new Padding(
padding: EdgeInsets.fromLTRB(0, 0, 5, 0),
child: new Icon(
Icons.remove_red_eye,
size: 18,
color: Colors.orange,
)
),
new Text('想看',
style: new TextStyle(
color: Color.fromRGBO(100, 100, 100, 1),
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
textColor: Colors.black,
),
),
new RaisedButton(
onPressed: () {},
color: Colors.white,
child: new Row(
children: [
new Padding(
padding: EdgeInsets.fromLTRB(0, 0, 5, 0),
child: new Icon(
Icons.star_border,
size: 18,
color: Colors.orange,
)
),
new Text('看过',
style: new TextStyle(
color: Color.fromRGBO(100, 100, 100, 1),
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
textColor: Colors.black,
),
],
),
);
}
```
#### 第二部分
这部分布局与上篇文章所讲述的内容都是一致的。并且我也偷懒了,主要是没有太多值得花费心思的地方,都是常规的布局.本来想实现下“进度条”,但无奈并没有真实数据,也就懒得弄了。完整代码如下:
```Dart
import 'package:flutter/material.dart';
import 'package:movie_top_250/Model/movieModel.dart';
Widget movieDetailStarWidget(MovieDetail movieDetail) {
return new DecoratedBox(
decoration: new BoxDecoration(
color: Color.fromRGBO(65, 46, 37, 1),
borderRadius: BorderRadius.all(Radius.circular(5))),
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new Padding(
padding: EdgeInsets.all(10),
child: new Text(
'豆瓣评分™',
style: TextStyle(color: Colors.white),
)
)
],
),
_buildRatingStar(movieDetail),
],
)
);
}
Widget _buildRatingStar(MovieDetail movieDetail) {
List icons = [];
int fS = int.parse(movieDetail.stars) ~/ 10;
int f = 0;
while (f < fS) {
icons.add(new Icon(Icons.star, color: Colors.orange, size: 15));
f++;
}
while (icons.length != 5) {
icons.add(new Icon(Icons.star,
color: Color.fromRGBO(220, 220, 220, 1), size: 15));
}
return new Padding(
padding: EdgeInsets.fromLTRB(0, 5, 0, 10),
child: new Column(children: [
new Padding(
padding: EdgeInsets.fromLTRB(5, 0, 0, 10),
child: new Text(
movieDetail.rating,
style: new TextStyle(
fontSize: 35,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(220, 220, 220, 1),
),
),
),
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: icons
),
]),
);
}
```
#### 第三部分
这部分涉及到了长文本,flutter 中同样也没有提供长文本显示组件,但好在 flutter 的 `Text Widget` 本身就适用于长文本展示的组件,默认开启 `softWrap` 属性(自动换行)。`Text Widget` 会从自身节点树里一直向上寻找能够提供宽度约束的父组件,并以此作为单行文本最长显示宽度,这点还是比较惊讶的,省了非常多的事情。完整的代码如下:
```Dart
import 'package:flutter/material.dart';
import 'package:movie_top_250/Model/movieModel.dart';
Widget movieDetailSummaryWidget(MovieDetail movieDetail) {
return new Padding(
padding: EdgeInsets.all(15),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new Padding(
padding: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: new Text(
'简介',
style: new TextStyle(
fontSize: 20,
color: Colors.white,
fontWeight: FontWeight.w600,
),
)
),
new Text(
movieDetail.summary,
style: new TextStyle(
color: Colors.white,
fontSize: 15,
)
)
],
)
);
}
```
当数据展示出来后,发现超出当前页面的显示范围了,这也是意料之中。按照之前的做法,会把 `UIScrollView` 作为当前页面所有原始的父容器,等所有 UI 元素都回填数据重新渲染完后,再把位于最底部的 UI 组件 bottom 值赋给 `scrollView.contentSize`。
翻了 flutter 文档后,发现了提供常规滑动视图能力的 Widget 不只一个,最后选择了 `SingleChildScrollView`。本以为设置滑动区域的步骤也会向在 Native 中那般麻烦,但实际上只需要把需要滑动视图组件的父节点赋给 `child` 属性即可,`SingleChildScrollView` 同样会自动的拓展自己的滑动区域进行适配,如下所示:
```Dart
Widget _buildBody(BuildContext context) {
// 数据源没来时展示 loading
if (movieDetail == null) {
return new Center(
child: new CircularProgressIndicator(),
);
} else {
return new SingleChildScrollView(
child: new Padding(
padding: EdgeInsets.all(10),
child: new Column(children: [
movieDetailHeaderWidget(rankIndex, movieDetail, context),
movieDetailStarWidget(movieDetail),
movieDetailSummaryWidget(movieDetail),
movieDetailMemberWidget(movieDetail),
])
),
);
}
}
```
#### 第四部分
这部分是整体比较纠结的地方,到底是基于 `GridView Widget` 还是 `SingleChildScrollView Widget` 配合着其它布局 Widget 去做呢?如果这在 iOS 中,我会毫不犹豫的选择 `UICollectionView` 进行构建,因为又快又好~
最后还是抱着“又快又好”目的出发,选择了 `SingleChildScrollView Widget` 配合着其它布局 Widget 去做。需要注意是的 `SingleChildScrollView Widget` 默认是纵向滚动,该部分豆瓣 App 进行的横行滚动,需要改变滚动视图方式。完整代码如下:
```Dart
import 'package:flutter/material.dart';
import 'package:movie_top_250/Model/movieModel.dart';
Widget movieDetailMemberWidget(MovieDetail movieDetail) {
List memberWidgets = [];
for (MovieMember member in movieDetail.director) {
memberWidgets.add(_buildMemberWidget(member, true));
}
for (MovieMember member in movieDetail.casts) {
memberWidgets.add(_buildMemberWidget(member, false));
}
return new SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: new Row(
children: memberWidgets,
),
);
}
Widget _buildMemberWidget(MovieMember member, bool isDirector) {
var col = new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new Container(
width: 110,
height: 160,
decoration: new BoxDecoration(
image: DecorationImage(image: NetworkImage(member.avatarUrl)),
borderRadius: new BorderRadius.all(
const Radius.circular(8.0),
),
),
),
new Text(member.name,
style: new TextStyle(
color: Colors.white,
),
),
],
);
if (isDirector) {
col.children.add(
new Text(
'导演',
style: new TextStyle(
fontSize: 13,
color: Color.fromRGBO(150, 150, 150, 1)
),
)
);
} else {
col.children.add(
new Text(
'演员',
style: new TextStyle(
fontSize: 13,
color: Color.fromRGBO(150, 150, 150, 1)
),
)
);
}
return new Container(
width: 110,
child: new Padding(
padding: EdgeInsets.all(15),
child: col,
),
);
}
```
## 分析工具
在本 demo 中的 `ListView Widget` 未做太多优化的地方,所以会导致在启动 App 完成后直接开始上拉页面会体验到卡顿,但实际上的做法是先把当前页数据源的条数给 `ListView` 设置上,当用户停止滑动页面后,再开始 load 当前在可视区域范围内的 `ListViewRow Widget` 相关子组件数据。这一点优化在 iOS 上通过 `UITableView` 配合 `RunLoop` 就可以解决,但在对 flutter 的一期调研中并未打算开始此项调优工作。
使用各种跨平台工具最令人感到窒息的莫过于调试了,基于 `JSCore` 比如 `Weex`、`React-Native` 等框架还行,能够利用 web 开发者工具搭配进行。但比如 `Xamarin`、`Qt` 等框架想要进行调试基本上就比较费劲了,如果框架开发者不提供一些功能完备的 IDE 或插件,调试几乎等于噩梦。好在 `Android Stuido` 对 flutter 的支持是相当丰富,具体见下图:




## 其它
### webView
今天原本还想做跳转 `webView`,以为也只是直接调个 `webView Widget`,填入 `requestUrl` 属性就完事了。但当我输入 web ,IDE 并未提示任何相关信息时,开始发觉有点不太对劲,不会 flutter 没有提供 `webView Widget` 吧?仔细浏览后,确认 flutter 还真的没有提供官方 `webView` 组件,但在 Pub 上已经有了对应的插件。
接着去掘金的 flutter 交流群里咨询,讨论在 flutter 中 `webView` 以及 `JSBridge` 最佳思路,最后讨论出了两个插件:
* webView 插件(带 JSBridge):[https://pub.flutter-io.cn/packages/interactive_webview](https://pub.flutter-io.cn/packages/interactive_webview)
* webView 插件:[https://pub.dartlang.org/packages/flutter_webview_plugin](https://pub.dartlang.org/packages/flutter_webview_plugin)
对于 `webView` 这块不是特别满意,而且看了 flutter 在 github 上的 issue,推荐自己做一个 `webView plugin`,暴露给 flutter 进行调用,这样可以最大程度上的降低基础组件重写成本。仔细一想,其实还是不满意,所以这部分内容也延后到二期调研中了。
### `StatefulWidget` 和 `StatelessWidget` 的选择
对于开发一个新的组件时,到底是基于 `StatefulWidget` 还是 `StatelessWidget` ,我认为只需要明确两个概念即可:
* 是否需要更新组件数据源;
* 是否需要利用组件各种生命周期;
如果以上两个条件都符合,那就选择 `StatefulWidget`。
## 总结
### 源码地址
本次 flutter 一期调研学习产出的豆瓣电影 Top250 demo 链接地址:[movie_top_250](https://github.com/windstormeye/flutter-practices/tree/master/movie_top_250)
flutter 的这种“声明式”编码体验,我认为对于第一次接触的新手来说,肯定有需要一定的学习成本,当逼迫自己去熟悉开发思路后,就会觉得真的很过瘾。在一期调研的学习中,没有涉及到的方面有:
* 设计模式;
* `webView` 及 `JSBridge`;
* flutter 与 native 的交互;
* 复杂 UI 的构建;
* 音视频处理(这还是得通过 native 进行暴露);
以上几块是构建一个 App 所需要具备的基本组件。经过一期学习后,对 flutter 也有了自己的理解,给我最大的感受是因为不用像 `React-Native`、`Weex` 等需要回调节点树给中间层通知 native 利用原生 UI 框架进行渲染,首先在 UI 绘制速度上已经远超其它框架,这点是毋庸置疑的。但因为 flutter 还太年轻,一些基础设施和社区都做得不算太好。所以如果非要选择一个跨端技术投入实际开发中,我还是会选择 `React-Native`,所以将重新捡起来 `React-Native`, 对其同样重新进行一期调研。
================================================
FILE: Flutter/Flutter问题汇总.md
================================================
# Flutter 问题汇总
## 环境配置
根据[ `flutter` 中文官网](https://flutterchina.club)上所引导的步骤进行配置,中途可以根据 `flutter doctor` 命令进行检查相关依赖是否配置完成。
### 设备
* iOS: iPhone 7, iOS 12.1.2
* Android: meizu 15, Andriod 7.1.1
### 遇到的问题
* 在环境配置中,官方推荐使用 `Andriod Studio` 进行开发,因为体验是最好的,当然同时也支持 `VS Code` 和 `IntelliJ`。因为开发机“常年”连接公司内网,导致无法在 `Andriod Studio` 中下载 `Dart` 和 `Flutter` 插件,尝试好几次,网上的资料都翻遍了,突然灵光一闪!我特么这是在内网啊!切回外网后,一切顺畅......
## 初体验
Flutter 官方上说的优势之一为“热重载”,新建 flutter 测试项目分别运行在 iOS 和 Andriod 两台测试设备上,iOS 的热重载只要每次 `cmd + s` 即可,但 Andriod 需要执行两次,看第一次打印出来的信息提示已经完成 `hot reload`,但设备上什么都没出现,必须执行第二次 `cmd + s` 操作后,才能看到真正的 `hot reload` 的效果。

flutter 官网上对于“热重载”是这么描述的:
> 通过将更新后的源代码文件注入正在运行的 `Dart` 虚拟机(VM)中来实现热重载。在虚拟机使用新的的字段和函数更新类后,`Flutter` 框架会自动重新构建 `widget` 树,以便您快速查看更改的效果。
所以对于在 meizu 15 上需要执行两次保存操作才能触发“热重载”后的效果展示,我的推测是,在第一次执行保存操作时要么没有把新更新后的代码注入进 `Dart` 虚拟机中,要么就是注入了但未触发重新自动构建 `widget` 树。
### 渲染

### 差异点
* 入口的 Main 函数入口使用了 `=>` 语法糖,官方说是“这是 `Dart` 中单行函数或方法的简写”:
```Dart
void main() => runApp(new MyApp());
// 我的推测:上下两者相等,论简洁性,确实是好看一丢丢
void main() { runApp(new MyApp()) }
```
* 每一个 `Widget` 都会有一个 `build()` 方法,用于描述如何根据其他较低级别的 `widget` 来显示自己。我的理解就是 `initView` 方法;
* 在 `Dart` 中“万物”(包括布局)都是 `Widget`,这点就类似与 `Objective-C` 中的“万物”都是 `NSObject`;
* `Scaffold Widget` 是 `Material library` 中的一个 `Widget`,提供了 `Material` 风格的基本组件。
* Flutter 中并没有类似 iOS 中的 `UITableViewCell`,直接在 `ListView Widget` 中构建了 `cell`,正是因为没有 `cell` 的概念,所以原本每个 `cell` 之间的“分割线”也需要手动使用 `Divider Widget` 进行索引模拟。推荐一篇关于 `Scaffold Widget` 的[内容介绍](http://flutter.link/2018/03/20/Scaffold/)
* Flutter 的 `Widget` 分为 `StatefulWidget(有状态)` 和 `StatelessWidget(无状态)` 两种,这跟在 iOS 中只要是继承了 `UIResponder` 就具备与用户产生交互进行状态的改变不一样。在 flutter 中如果我们需要实现设计要这个组件是否需要有状态的改变。
### 一些简单操作
#### 当打开一个工程时
`flutter packages get` 来下载工程中所依赖的库。
#### 格式化代码
`Dart` 疯狂嵌套的代码风格已经被吐槽烂了,好在可以在写完代码后,利用 `Android Studio` 中提供的 `Dart` 格式化代码工具:选择任何一个 `Dart` 代码文件,右键选择“Reformat Code with dartfmt”,代码格式立马变得好看了许多。
### 总结
经过这次对 Flutter 的初体验,对其惊叹的地方有:
* 真的做到了一套代码可以“无脑”运行在 iOS 和 Android 两个平台上,使用 `Andriod Studio` 编写完主体代码后,完全不需要做任何平台差异化设置,直接选择不同平台设备直接运行即可,在加上真的脱离了 `JS Core` 的“热重载”技术,在 iOS 上的开发体验非常流畅和方便!
* 在 iOS 上真的抛弃了 `UIKit` 的所有内容,全都基于 `Skia` 自己渲染,这点跟 `Texture` 在 UI 渲染上有异曲同工之处。
* `Dart` 这门语言本身有着与 `JSX` 类似的代码风格痕迹,尤其是对 `Widget` 做属性的定义时,但从整体上来看因为前身是准备要替代 `JS`,所以在很多地方也有 `JS` 痕迹,在一些细节上又透露着 `Java` 的微小细节,所以从语言本身的上手难度不算大,并没有在语法层面上做出太多的革新。
* 强烈推荐使用 `Android Studio` 进行开发!!!
* 创建 Flutter 工程下的 iOS 平台工程居然主体基于 `Swift`,这点让我十分意外!
目前来看不满意的地方只有一个:
* 在 iOS 上的长列表滑动卡顿十分严重!!!在快速滑动下,估计只有两三帧,而且每一个 `ListTitle Widget` 上只放了一个 `Text Widget` 啊!太辣眼睛了......[视频在此](https://www.bilibili.com/video/av40402669/)
## 二探 Flutter
### 一些细节
* `MaterialApp` 下的 `title` 属性代表的是在 Android 任务管理器中的名称(iOS 下只看 app name),`home` 属性下的 `appBar` 中所返回的 `AppBar` 中 `title` 属性才是定义 app 当前页面的标题;
### http
想要在 `Flutter` 中使用 `http` 请求,需要先在 `pubspec.yaml` 文件中加入对应依赖:
```yaml
dependencies:
# ...
http: ^0.12.0+1
```
随后可在对应 `dart` 文件中进行引入:
```dart
import 'package:http/http.dart' as http;
```
## HTTP 相关
### 刷新数据
因为 Flutter 对数据和视图已经进行了绑定,如果想要在网络请求完成后刷新视图所绑定的数据源,需要使用 `setState` 方法进行数据源状态的刷新。当然,使用 `setState` 方法的前提得是我们的 `Widget` 得是一个 `StatefulWidget` ,具备“状态改变的能力”。
### 异步加载后获取数据
通过 flutter 的自带 HTTP 请求库开启异步获取数据后,要求把数据为 `Future` 类型格式,以便在组件中进行“懒加载”,对于 `Future` 类型数据的解析可以采用如下方法():
```Dart
class MovieAPI {
Future getMovieList(int start) async {
var client = HttpClient();
var request = await client.getUrl(Uri.parse(
'https://api.douban.com/v2/movie/top250?start=$start&count=100'));
var response = await request.close();
var responseBody = await response.transform(utf8.decoder).join();
Map data = json.decode(responseBody);
return Movies.fromJSON(data);
}
}
// 上拉加载
_requestMoreData(int page) {
MovieAPI().getMovieList(page).then((moviesData) {
setState(() {
movies = moviesData.movies;
});
});
}
```
### 点击事件
在 iOS 和 Android 中所有的 `View` 都可以添加点击或其它多手势事件,但在 Flutter 中除了少数几个自带 `onPress` 或 `onTap` 事件的 `Widget` ,剩下绝大部分 `Widget` 都不带事件,需要我们自己使用 `GestureDetector Widget` 作为父组件进行包裹。
```Dart
return GestureDetector(
onTap: () {
// 写下单机后触发的内容,当然还有双击、长按等事件
},
child: yourWidget,
);
```
### Flutter Widget Inspector
### 关于 `RaisedButton` 问题
```Dart
new RaisedButton(
onPressed: null,
color: Colors.white,
child: new Text('B'),
textColor: Colors.black,
)
```
如果不对 `onPressed` 设置处理事件,则对 `RaisedButton` 设置的所有修饰都不生效。
================================================
FILE: Front-end/CSS.md
================================================
# CSS
## 基本
### `` 实现 `` 等分
```html
```
```css
#father{
display:flex;
width:100%;
height:10rem;
}
.children{
flex:1;
text-align:center;
}
```
### 横向布局
1. 使用 `float` 进行浮动;
2. 使用 `inline-block` 行块标签;
3. 使用 `flex` 布局。
### 使用 `div` 还算 `button` 设置按钮呢?
考虑语义化时尽量都按照标准来,但做个性化定制时,需要修改 `button` 的默认属性。考虑兼容性等适配问题时,直接上 `div`,且更容易定制化。
### `float: right`
当多个 `div` 进行右浮动时,顺序会颠倒,可以让颠倒的 `div` 再嵌套一个容器 `div`,对容器 `div` 设置 `float: right`,对容器内部的 `div` 做 `float: left` 即可。
### `div` 如何设置背景图片
```css
.more-button {
float: left;
width: 30px;
height: 30px;
background: url(../../../static/images/normal-report/more.png) no-repeat;
background-size: 30px 30px;
}
```
### 设置 `padding` 再设置 `width: 100%` 后超出父节点宽度
> 在 CSS 盒子模型的默认定义里,你对一个元素所设置的 width 与 height 只会应用到这个元素的内容区。如果这个元素有任何的 border 或 padding ,绘制到屏幕上时的盒子宽度和高度会加上设置的边框和内边距值。——来源 MDN
此时可以使用 `box-sizing` 属性进行调整:
* `content-box`:默认值,如果设置 `width: 100px`,此时再设置 `padding` 或者 `margin` 都会进行叠加;
* `border-box`:设置 `padding` 或者 `margin` 将会包括在 `width` 中,不会进行叠加。
### `let` 和 `var`
在 ES6 之后,使用 `let` 修饰变量在代码块中有效。
### 跨域问题
#### 什么是跨域
#### 浏览器层防护
1. 同源策略
#### 解决
1. 让本地的 node 服务器代理前端发出去的请求,服务器之间的请求是没有跨域一说的。
### 页面跳转
方式 | 解释
--- | ---
`self.location.href` | 当前页面打开URL页面
`window.location.href` | 当前页面打开URL页面
`this.location.href` | 当前页面打开URL页面
`location.href` | 当前页面打开URL页面
`parent.location.href` | 在父页面打开新页面
`top.location.href` | 在顶层页面打开新页面
#### 单页面
如果构建的是一个单页面应用,可以使用 `vue-router`。
单页面应用一次性载入所有资源,按需加载。页面中部分模块的更新,例如 `div1` 到 `div2` 只是插入而已~
#### 多页面
如果构建的是一个多页面应用,则应该使用以上描述的方法进行页面跳转。
多页面按需加载对应资源。
### 本地测试没法获取 cookie 怎么办?
可以在 chrome 的 console 中通过 `document.cookie` 进行设置暂时 cookie。
### 修改特定 index 元素 css
给单数 `li` 添加右边框
```css
li {
&:nth-of-type(2n + 1) {
border-right: 1px solid #ddd;
}
}
```
### 边框
```css
border: 1px solid rgb(253, 185, 152);
```
### 文字水平居中
设置 `line-height` 为父视图高度
================================================
FILE: Front-end/FCC.md
================================================
# FCC 学习笔记
这是我在 [freecodecamp.one](http://freecodecamp.one) 上学习遇到的问题以及值得记录的点。
## `` 标签
该标签用与在 `` 标签内标识出“内容主体”,因为同时还会有 `、、` 等标签,把页面主体内容写在 `` 标签中,有这样搜索引擎进行检索,它只应包含与页面中心主题相关的信息,而不应包含如导航连接、网页横幅等可以在多个页面中重复出现的内容。
`main` 标签的语义化特性可以使辅助技术快速定位到页面的主体。有些页面中有 “跳转到主要内容” 的链接,使用 `main` 标签可以使辅助设备自动获得这个功能。例子如下:
```html
Using main
[url=#]Home[/url]
My article
Content
```
## HTML 5 `` 标签的 `target` 属性
target 属性规定在何处打开被链接文档。只能在 href 属性存在时使用。
` Visit W3School `
值 | 描述
---- | ----
_blank | 在新窗口中打开被链接文档。
_self | 在被点击时的同一框架中打开被链接文档(默认)。
_parent | 在父框架中打开被链接文档。
_top | 在窗口主体中打开被链接文档。
## ` `
` ` 标签是空标签(意味着它没有结束标签,因此这是错误的:` `)。在 XHTML 中,把结束标签放在开始标签中,也就是 ` `。
## HTML ` ` 标签
` ` 元素可提供有关页面的元信息(meta-information),比如针对搜索引擎和更新频度的描述和关键词。
## `class` 选择器
在 `` 区域,这么写:
```html
```
在 `` 区域,这么写:
```html
2333
```
### 其它
在 `
```
当你在:root里创建变量时,这些变量的作用域是整个页面。如果在元素里创建相同的变量,会重写:root变量设置的值。
## 使用媒体查询更改变量
CSS 变量可以简化媒体查询的方式。例如,当屏幕小于或大于媒体查询所设置的值,通过改变变量的值,那么应用了变量的元素样式都会得到响应修改:
```css
@media (max-width: 350px) {
:root {
/* add code below */
--penguin-size: 200px;
--penguin-skin: black;
/* add code above */
}
}
```
## `text-align`
* `text-align: justify;` 可以让除最后一行之外的文字两端对齐,即每行的左右两端都紧贴行的边缘。
* `text-align: center;` 可以让文本居中对齐。
* `text-align: right;` 可以让文本右对齐。
* `text-align: left;` 是 `text-align` 的默认值,它可以让文本左对齐。
## `strong` 标签加粗文本
你可以使用 `strong` 标签来加粗文字。添加了 `strong` 标签后,浏览器会自动给元素应用 `font-weight:bold;` 。
## `u` 标签
你可以使用u标签来给文字添加下划线。添加了 `u` 标签后,浏览器会自动给元素应用 `text-decoration: underline;` 。
## `em` 标签
你可以使用 `em` 标签来强调文本。由于浏览器会自动给元素应用 `font-style: italic;`,所以文本会显示为斜体。
## `s` 标签
你可以用 `s` 标签来给文字添加删除线,它代表着这段文字不再有效。添加了 `s` 标签后,浏览器会自动给元素应用`text-decoration: line-through;`。
## `hr` 标签
你可以用 `hr` 标签来创建一条宽度撑满父元素的水平线。它一般用来表示文档主题的改变,在视觉上将文档分隔成几个部分。在 `HTML` 里,`hr` 是自关闭标签,所以不需要一个单独的关闭标签。
## `text-transform` 给文本添加大小写效果
`CSS` 里面的 `text-transform` 属性来改变英文中字母的大小写。它通常用来统一页面里英文的显示,且无需直接改变 `HTML` 元素中的文本。下面的表格展示了 `text-transform` 的不同值对文字 “Transform me” 的影响。
Value | Result
--- | ---
lowercase | "transform me"
uppercase | "TRANSFORM ME"
capitalize | "Transform Me"
initial | 使用默认值
inherit | 使用父元素的text-transform值。
none | Default:不改变文字。
## `font-weight`
`font-weight` 属性用于设置文本中所用的字体的粗细。
## `line-height`
CSS 提供 `line-height` 属性来设置行间的距离。行高,顾名思义,用来设置每行文字所占据的垂直空间。
## 伪类
比如,超链接可以使用 `:hover` **伪类选择器**定义它的悬停状态样式。下面是悬停超链接时改变超链接颜色的 CSS:
```css
a:hover {
color: red;
}
```
## 更改元素的相对位置——`position`
在 CSS 里一切 HTML 元素皆为盒子,也就是通常所说的 **盒模型**。块级元素自动从新的一行开始(比如标题、段落以及 div),行内元素(也称:内联元素)排列在上一个元素后(比如图片以及 span)。元素默认按照这种方式布局称为文档的**普通流**,同时 CSS 提供了 `position` 属性来覆盖它。
```css
p {
position: relative;
bottom: 10px;
}
```
## 更改元素的绝对位置——`absolute`
CSS `position` 属性的取值选项 `absolute`,`absolute` 相对于其包含块定位。和 `relative` 定位不一样,`absolute` 定位会将元素从当前的文档流里面移除,周围的元素会忽略它。可以用 CSS 的 `top`、`bottom`、`left `和 `right` 属性来调整元素的位置。
`absolute` 定位比较特殊的一点是元素的定位参照于最近的已定位祖先元素。如果它的父元素没有添加定位规则(默认是`position:relative;`),浏览器会继续寻找直到默认的 `body` 标签。
## `fixed` 定位
`fixed` 定位,它是一种特殊的绝对(absolute)定位,区别是其包含块是浏览器窗口。和绝对定位类似,`fixed` 定位使用 `top`、`bottom`、`left` 和 `right` 属性来调整元素的位置,并且会将元素从当前的文档流里面移除,其它元素会忽略它的存在。
`fixed` 定位和 `absolute` 定位的最明显的区别是 `fixed` 定位元素不会随着屏幕滚动而移动。
## 元素浮动
浮动元素不在文档流中,它向左或向右浮动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。通常需要用 `width` 属性来指定浮动元素占据的水平空间。
## `z-index` 属性更改重叠元素的位置
在 HTML 里后出现的元素会默认显示在更早出现的元素的上面。你可以使用 `z-index` 属性指定元素的堆叠次序。`z-index` 的取值是整数,数值大的元素优先于数值小的元素显示。
## 让元素水平居中
一种常见的实现方式是把块级元素的 `margin` 值设置为 `auto`。同样的,这个方法也对图片奏效。图片默认是**内联元素**,但是可以通过设置其 `display` 属性为 `block` 来把它变成**块级元素**。
## 线性渐变器
`background: linear-gradient(45deg, #CCFFFF, #FFCCCC);`
## 重复线性渐变器
```css
background: repeating-linear-gradient(
45deg,
yellow 0px,
yellow 40px,
black 40px,
black 80px
);
```
## 通过添加细微图案作为背景图像来创建纹理
终于明白了公司内网上标记名字的背景图像是怎么做的了
`background: url(https://i.imgur.com/MJAkxbh.png);`
## 更改元素大小
`transform: scale(1.5);`
## 鼠标悬停时缩放元素
```css
div:hover {
transform: scale(1.1);
}
```
## 倾斜
```css
transform: skewX(24deg);
transform: skewY(-10deg);
```
## 画一个月亮 🌛
`blur-radius` => 模糊半径,`spread-radius` => 辐射半径,`transparent` => 透明的,`border-radius` => 圆角边框
```css
background-color: transparent;
border-radius: 50%;
box-shadow: 25px 10px 0px 0px green;
```
## 画一个爱心 ❤️
```css
```
## 动画
```css
#anim {
animation-name: colorful;
animation-duration: 3s;
}
@keyframes colorful {
0% {
background-color: blue;
}
100% {
background-color: yellow;
}
}
```
## 修改动画的填充模式
动画在持续时间之后重置了,所以按钮又变成了之前的颜色。如果我们想要的效果是按钮在悬停时始终高亮。
`animation-fill-mode: forwards;`
## 动画次数
`animation-iteration-count: infinite;`
## 使用关键字更改动画定时器
`animation-timing-function: ease-out;`
## 当图片无法读取或需要提供描述信息时
` `
## 每个页面应该只有一个h1标签,用来说明页面主要内容。h1标签和其他的标题标签可以让搜索引擎获取页面的大纲
## 音频播放
```css
```
## `label` 标签的 `for` 属性
`label` 元素不会向用户呈现任何特殊效果。不过,它为鼠标用户改进了可用性。如果您在 `label` 元素内点击文本,就会触发此控件。就是说,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上。前提得跟表单内的输入组件 id 相同,例如:
```html
```
## `` 标签
`` 标签可定义上标文本。
包含在 `` 标签和其结束标签 ` ` 中的内容将会以当前文本流中字符高度的一半来显示,但是与当前文本流中文字的字体和字号都是一样的。
## 将键盘焦点添加到元素中
```html
Instructions: Fill in ALL your information then click Submit
```
这样就会当用户使用 `tab` 键进行切换时,会把键盘焦点聚集在该标签上。
# 响应式 Web 设计原则
## 创建一个媒介查询
设备的高度小于或等于 `800p`x 时,`p` 标签的 `font-size` 为 `12px` 的媒体查询:
```css
@media (min-height: 800px) {
p {
font-size: 12px;
}
}
@media (max-height: 800px) {
p {
font-size: 12px;
}
}
```
## 使排版根据设备尺寸自如响应
视窗单位相对于设备的视窗尺寸 (宽度或高度) ,百分比是相对于父级元素的大小。
四个不同的视窗单位分别是:
* vw:如 10vw 的意思是视窗宽度的 10%。
* vh: 如 3vh 的意思是视窗高度的 3%。
* vmin: 如 70vmin 的意思是视窗中较小尺寸的 70% (高度 VS 宽度)。
* vmax: 如 100vmax 的意思是视窗中较大尺寸的 100% (高度 VS 宽度)。
================================================
FILE: Front-end/JavaScript.md
================================================
# JavaScript
## 指定 `Array` 的元素类型
不好意思做不到!js 是弱类型语言,我们不能真正的做到指定元素类型,但是可以先指定元素长度,然后再循环赋值哈哈哈~
## `HttpOnly`
产品上出现了一个奇怪的问题,一个 H5 页面在测试环境下(hostName 为 xxx-test.xxx.com)完全没有任何问题,`Cookie` 可以取到所有信息,但是发布到线上后,发现了一个十分捉鸡的问题,线上环境(xxx.xxx.com)居然取不到 `Cookie` 中的 `xxx_ticket` 信息!!!
刚开始吭哧吭哧的整了一波后,始终在怀疑是自己写的逻辑有问题,查来查去,还是没啥头绪,没办法只能去请教下前辈。一波操作后,发现了如下情况:
线上环境:

测试环境:

原因就在这个 `HttpOnly` 上,关于 `HttpOnly` 的解释如下:
>>> HttpOnly: HttpOnly is an additional flag included in a Set-Cookie HTTP response header. Using the HttpOnly flag when generating a cookie helps mitigate the risk of client side script accessing the protected cookie (if the browser supports it).
换句话说,客户端/浏览器是无法对 `Cookie` 中的这个 `key` 进行操作的,并且不可见。
## 连接两个数组
不可使用 `+=`,而是 `concat`。
## 基础语法
### ES6
#### 数组展开
可用使用 `...` 对一维数组下另外塞入的完整一维数组进行进行展开,防止变为二维数组。
```js
ES6 扩展运算符...可以将两重数组转换为单层数组:
[].concat(...[1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"]); // [1, 2, 3, Array(1), "a", "b", "c", "d", Array(1), "e", "f"]
// 利用 some 方法,我们可以实现多重转换为单层:
function flatten(origin) { while(origin.some(item=> Array.isArray(item))) {
origin = [].concat(...origin);
} return origin;
}
var arr = [1, [2, 3, [4]], "a", "b", ["c", "d"], [["d"],"e"], "f"];
console.log(flatten(arr)) // [1, 2, 3, 4, "a", "b", "c", "d", "d", "e", "f"]
```
================================================
FILE: Front-end/Vue.md
================================================
# Vue
## 基础知识
### MVVM 数据绑定
在学习 `Vue` 的过程中,非常好奇 `Vue` 自身的数据绑定模式是否与我之前在 iOS 上接触到的数据绑定认识一致,然后又在资料中发现了 `Object.defineProperty()` 和 `Object.defineProperties` 方法,相当于对一个属性进行了 `setter` 和 `getter` 的监听,然后根据监听结果重新更新元素,大致如下所示:
```html
```
看到这段代码后,瞬间就大彻大悟!其实也不是什么特别高深的内容(当然还是有其它值得学习的地方~),而且这么做耦合度十分的高,就目前的学习内容来看,`Vue` 中对 **组件化** 的核心思路跟之前在 iOS 中流程还是一致的~
## 构建
### 生成 package.json 文件(需要手动选择配置)
`npm init`
### 生成 package.json 文件(使用默认配置)
`npm init -y`
### 一键安装 package.json 下的依赖包
`npm i`
### 在项目中安装包名为 xxx 的依赖包(配置在 dependencies 下)
`npm i xxx`
### 在项目中安装包名为 xxx 的依赖包(配置在 dependencies 下)
`npm i xxx --save`
### 在项目中安装包名为 xxx 的依赖包(配置在 devDependencies 下)
`npm i xxx --save-dev`
### 全局安装包名为 xxx 的依赖包
`npm i -g xxx...`
### 自定义执行命令
也就是会运行 `package.json` 中 `scripts` 下的命令:
```shell
npm run xxx
```
## 懒加载
H5 页面也可以像之前原生中的“懒加载”思想一致,等到需要真的需要使用这个组件时,再对其进行渲染,如果你跟我一样使用 `vue-router`,则可以这么写:
```js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
routes: [
{
// 该页面为直接加载
path: '/',
name: 'home',
component: Home
},
{
// 该页面为懒加载
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: function () {
return import(/* webpackChunkName: "about" */ './views/About.vue')
}
}
]
})
```
## Vue 的一些属性描述
### `name`
只有作为**组件选项**时起作用。允许组件模版递归地调用自身,注意,组件在全局用 `Vue.component()` 注册时,全局 ID 自动作为组件的 name;指定 `name` 选项的另一个好处是便于调试。有名字的组件有更友好的警告信息。另外,当在有 `vue-devtools`,未命名组件将显示成 ``,这很没有语义。通过提供 `name` 选项,可以获得更有语义信息的组件树。
## Props
### `Array`
如果我们要传递一个字符串数组给子组件,错误的传递方式:
```html
```
正确的传递方式:
```html
```
vue 官网上是这么说的,
> 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue,这是一个 `JavaScript` 表达式而不是一个字符串。
###m mint-UI 是怎么实现下拉刷新的呢?
简单来说获取下拉手势后通过 `transform` 来做动画偏移。
================================================
FILE: Front-end/basic.md
================================================
# Basic HTML and HTML5
* 作为惯例,所有的 HTML 标签都是小写,比如是
,而不是
* 所有的 img 标签都必须有 alt 修饰。该标签可被用户读屏和如果图片加载不出来时进行文本展示。
* 使用 a 标签通过 href 进行跳转时,增加 target="_blank" 可以在新的标签页打开。
* 一般来说我们在 标签中通过 href 进行跳转,但如果在 href 中使用 #about 等类似的标识符,这将会挑战到当前页面对于的 id 的标签上,如果此时只给了 #,则只会刷新当前页面。
* 如果为必填框,可以设置 required 标记
* 每一个 type 为 radio 的标签都应该内嵌在各自独立的 标签中。
* 标签作用在 类型标签的作用在于 accessibility,供读屏软件定位到输入区域时给用户提示
# Basic CSS
* CSS 下的 font-family 设置字体时,先在编辑器最顶部导入字体源,然后再写入对应的字体名字,若当前字体不可用时,可以多写一个浏览器自带字体,做个捞底。
* Id 修饰有助于JS 调用,而且应该全局唯一。浏览器虽然不会强制要求,但这是一个最佳实践。
* Id 不可被服用,且应该只对一个元素生效。当 id 和 class 所挂载的 css 发生冲突时,id 的优先级大于 class。
* 类似 标签给了 type 后,想要针对不同的 type 做 CSS,可以通过这种方式
[type = ‘checkbox’] = {
}
* Class 中修饰的内容大于 body 中修饰的内容,后边更新的属性会覆盖前一个属性
* 写在同一个元素中的不同 class,第二个 class 修饰的内容会复写第一个修饰的 class 中修饰的内容
* 不管 class 修饰了多少次,id 再去修饰同一个属性的优先级是最高的
* 当比较难重写 CSS 时,可以使用 !important 进行强制使用
* 十六进制颜色值是按照 #RRGGBB 的格式进行排布,0 为最低色值,F 为最高。
* 如果涉及到一个 CSS 属性很多地方都有用到,可以使用 CSS 变量进行统一处理:
```css
.penguin {
--penguin-skin: black;
}
.penguin-top {
background: var(--pengiun-skin);
}
```
* CSS 变量有些浏览器并不支持,为了提升可用性,可以重复定义一个相同的 CSS 属性。
```css
.red-box {
background: red;
background: var(--red-color);
}
```
* CSS 变量可以被重写
* 媒体查询
```css
:root {
--penguin-size: 300px;
--penguin-skin: gray;
--penguin-belly: white;
--penguin-beak: orange;
}
@media (max-width: 350px) {
:root {
--penguin-skin: black;
--penguin-size: 200px;
}
}
```
* CSS :root 伪类选择器?
* :root 这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 元素,除了优先级更高之外,与 html 选择器相同。
* CSS 伪类 是添加到选择器的关键字,指定要选择的元素的特殊状态。例如,:hover 可被用于在用户将鼠标悬停在按钮上时改变按钮的颜色。
# Applied Visual Design
* text-align: justify;
* 文字向两侧对齐,对最后一行无效。
* text-align: justify-all;
* 同上,但强制最后一行两端对齐。
* 标签可以替代 CSS 属性 font-weight: bold; 作用。
* 标签加下划线
* 加斜体
* 加横线
* 所有的 CSS 属性设置后都必须添加分号。
* position: relative
* 让元素脱离当前的文档流,但盒子原先占据的位置还在,适合做微调。
* float 属性把当前元素推出文档流。
* z-index 属性可以调整元素和元素之间在 z 轴上的位置
================================================
FILE: Front-end/vue-context-mune.md
================================================
# 使用 Vue 实现 Context-Menu 的思考与总结
## 简介
先来看最终成果:

操作逻辑为:
* 点击 `...` 弹出 `context-menu`;
* 点击非 `context-menu` 区域,隐藏 `context-menu`;
* 点击 `context-menu` 中的任何一个选项,隐藏 `context-menu`;
## 思考
项目是基于 `vux` 做的,本想着偷懒直接在 `vux` 库翻组件用,但看了一圈下来,居然这么通用的组件在 `vuex` 中没有!接着又去翻开源的解决方案,看了几个库还算 ok,但此时前端小哥来了,说实现这个菜单不需要用到这么重的东西,直接写就行了。
当时我的脑海中在思考了把 `context-menu` 封装成一个 `component`,通过数据配置的方式动态拓展菜单选项。但没想到前端小哥直接给我干了回来,没必要进行封装,这个组件对页面依赖性太强,就算封装完了下次也不一定能直接用,PM 的思路十分清奇。
所以,最后的做法就直接硬上了。
## 实现
### 调整操作逻辑
该页面是一个通俗意义上的列表展示页,使用了 `vux` 的 `swipeout` 表单组件,给用户提供了侧滑操作,需要把原先写好的侧滑功能删除。
### 调整 UI
在调整 UI 的过程中我感到了 CSS 满满的恶意,当然说是这么说,但实际上还是因为太久没有用而导致的不够熟悉。非常费劲的终于调整了好了新 UI,此时已经过去了整整一天了,非常怀念 `autoLayout`。
### context-mune
在正式开始写之前,上文已经说了我一直在翻开源库,主要是不懂得如何下手去,因为距离上一次写 `vue` 已经过去了快两个月了,而且也没搞清楚如何写一个组件,所以中间也有一段时间浪费在了这个上。最后的解决思路让我感到意外:
```vue
```
没想到使用无序列表就可以完成了~在 iOS 中,我会在 `UITableView` 和 `UIStackView` 中纠结。当然只有这样是不行的,当又调整了 UI 后,发现 `...` 和 `context-menu` “融合”在一起了,没有设计图中的“悬浮”效果,最后的解决方法是:
```css
.more-wrapper {
/* ... */
position: absolute;
.more-menu-wrapper {
position: relative;
/* ... */
}
}
```
当继续调整 CSS 时又发现 `context-menu` 的会被其父组件挡住,`context-menu` 的显示范围会限制与其父组件的显示高度,最后得知是 `overflow` 这个属性在最底层的父组件中设置了 `overflow: hidden;`,删除掉,使其为默认的 `visible` 即可显示为 `context-menu` 高度溢出的效果。
### 事件绑定
UI 都调整完后开始绑定事件。因为只是改造 UI,并没有涉及到多少的新逻辑,所以很快的就写出了以下代码:
```vue
```
`context-menu` 的显示依赖 `v-show`,当页面首次拉取到网络数据时,`data` 中对每个 `listData` 的 `item` 新增了 `context-menu` 显示隐藏的初始化标志位 `item.showOption = false`,且在这四个入口方法中都控制了 `item.showOption` 的改变:
```js
//...
moveUp(item) {
item.showOption = false;
// ...
}
//...
```
刷新页面,很愉快的看到了 `context-menu` 的显示,但在点击菜单选项时没有任何反应!一开始以为是标识位的问题,但看来看去没有任何问题哇~本来想去找前端小哥看一眼,但一直不在工位上,最后问了下同组的前端实习生,他认为是 `item.showOption` 字段在数据更新时没有加上,导致后续直接读取时不存在。
但我其实一直纳闷如果 `item.showOption` 字段数据不存在的话,那第一次的页面渲染实际上是有错误的。我们两个人看了一会也没发现具体是哪有问题,最后只能四处寻找前端小哥,没想到他已经被封闭起来做商业化了,我说怎么四处找不到人。
前端小哥在文件中加上了 `debugger` 进行调试,发现进入到 `moveUp` 等一类事件时虽然 `item.showOption` 被修改成功了,一旦出去事件周期外,又被改回去了。
最后发现,问题出在被**冒泡**到了父组件中,调用了 `...` 所绑定的 `onMore` 事件中,而在 `onMore` 事件中 `item.showOption = true`,所以实际上是执行了 `context-menu` 和 `...` 的两者所绑定的事件。解决的方法是:
```vue
```
使用 `@click.stop` 来阻止冒泡事件。解决完问题后,前端小哥还好奇我做 iOS 怎么会不知道冒泡事件的问题,但实际上在 iOS 中跟前端的思路完全是反过来的。iOS 的事件响应链是逐级传递到子组件中,也就是向下传递,而不是像前端中的向上传递。所以在遇到这个问题时也就完全没有往冒泡的方面去思考。
### 触摸其它区域消失 `context-menu`
在 iOS 中,我会直接封装出一个带有 `UIWindow` 的组件。与 `context-menu` 有关的所有操作与主 `window` 没有任何关系,更别说事件穿透了。所以最终我的做法是多加了一个遮罩层,显示和隐藏的时机与 `context-menu` 的时机保持一致。
最后在我拿着最终的成果去找前端小哥复查时,他对这个做法不满意,还是觉得要使用 `outside-click` 的做法。也就是使用 js 中的事件代理,通过 `e.targe` 去判断。最后找到了可以使用 [`v-outside-click`](https://github.com/ndelvalle/v-click-outside) 进行。`v-outside-click` 有两种引入的方式,为了简洁,我选择了“指令”的方式引入。
在使用 `v-outside-click` 这个库的过程中遇到了一个比较大的问题。`v-outside-click` 此库给我的感觉是用于单个组件,而不适用于多个组件。列表中的每一个 `cell` 都需要带上一个单独的 `context-menu`,如果给每一个 `context-menu` 都绑上一个单独的 `outside-click` 事件,一旦用户的触摸范围不在 `context-menu` 中,则视图上的所有 `context-menu` 都会响应这个 `outside-click` 事件,列表数据一旦多起来,事件响应次数将线性增长。
这个问题跟前端小哥说过后,他说问题不大,那就这样吧~接下来的问题就到了怎么在 `outside-click` 事件中标识出是哪个 `context-menu` 需要隐藏呢?刚开始就按照了以往的套路,直接使用了如下所示的方式:
```html
```
然后开心看到了报错 `Binding value must be a function or an object`。提示需要传入一个方法?!翻了源码后发现了这么一段:
```js
function processDirectiveArguments(bindingValue) {
const isFunction = typeof bindingValue === 'function'
if (!isFunction && typeof bindingValue !== 'object') {
throw new Error('v-click-outside: Binding value must be a function or an object')
}
// ...
}
```
回过头去看之前写的代码,没有问题啊!思来想去还是没弄明白,又去找了前端小哥请求帮忙,经过了一番折腾了,他的结论是这个库应该是有问题的。最后采取的解决方法是:
```html
```
```js
onClickOutside (event, el) {
let queryInstance = el.querySelector('.more-menu-wrapper')
if (queryInstance) {
let metricId = el.querySelector('.more-menu-wrapper').id;
if (metricId != "") {
this.listData.some((item) => {
if (item.metricId == metricId) {
item.showOption = false;
return true;
}
});
}
}
}
```
通过设置 `context-menu` 的 `id` 作为标识,然后在 `v-outside-click` 的指令方法中获取 `id`,通过这个 `id` 去数据源中找到对应的 `item`,从而设置 `item.showOption = false` 来隐藏 `context-menu`。
## 总结
这算是转大前端完成的第一个功能吧,因为不熟悉导致中间出现了一些好玩的事情。客户端和前端的开发流程说大也不大,但要是说没有是绝对不可能的。在一些小的问题上,没有踩过坑或者没有大佬带一带,真的会爬不起来或者就弃坑了,说到底其实还是需要多加学习啊!前端有时候真的挺香的。
================================================
FILE: Front-end/前端学习.md
================================================
## https协议的主要作用是:建立一个信息安全通道,来确保数据的传输,确保网站的真实性。
## TCP和UDP的区别
* TCP是面向连接的,UDP是无连接的即发送数据前不需要先建立链接。
面向连接基于电话系统模型,而面向无连接则基于邮政系统模型。相对于面向连接的建立连接的三个过程,面向无连接只有“传送数据”的过程。
* TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。 并且因为tcp可靠,面向连接,不会丢失数据因此适合大数据量的交换。
* TCP是面向字节流,UDP面向报文,并且网络出现拥塞不会使得发送速率降低(因此会出现丢包,对实时的应用比如IP电话和视频会议等)。
* TCP只能是1对1的,UDP支持1对1,1对多。
* TCP的首部较大为20字节,而UDP只有8字节。
* TCP是面向连接的可靠性传输,而UDP是不可靠的。
## 一个图片url访问后直接下载怎样实现
浏览器会通过头信息进行判断,一旦没有找到头信息浏览器则根据自己的既定规则进行解析。如果浏览器能识别该文件,则会以相应的方式显示该文件;如果浏览器不能识别该文件,则会弹出下载保存窗口供用户进行下载保存。
参考文章:[https://blog.csdn.net/qq_39759115/article/details/78611732](https://blog.csdn.net/qq_39759115/article/details/78611732)、[https://scarletsky.github.io/2016/07/03/download-file-using-javascript/](https://scarletsky.github.io/2016/07/03/download-file-using-javascript/)
## 一句话概括RESTFUL
就是用URL定位资源,用HTTP描述操作
## COOKIE和SESSION有什么区别?
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
1,session 在服务器端,cookie 在客户端(浏览器)
2,session 默认被存在在服务器的一个文件里(不是内存)
3,session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id)
4,session 可以放在 文件、数据库、或内存中都可以。
5,用户验证这种场合一般会用 session 因此,维持一个会话的核心就是客户端的唯一标识,即 session id
参考文章:[https://www.zhihu.com/question/19786827](https://www.zhihu.com/question/19786827)
## click在ios上有300ms延迟,原因及如何解决?
* 粗暴型,禁用缩放
` `
* 利用FastClick,其原理是:
检测到touchend事件后,立刻出发模拟click事件,并且把浏览器300毫秒之后真正出发的事件给阻断掉
## `` 的妙用
因为 `` 标签是内联元素不是块级元素,不会破坏文档流,可以做一些 JS hook。
================================================
FILE: Front-end/图解HTTP学习笔记.md
================================================
这是我在学习《图解HTTP》这本书时做的笔记,可能有些凌乱,会加入自己的一些思考和之前计网课上已习得的内容。
### 第一章:了解Web及网络基础
1. 通过发送请求获取服务器资源的Web浏览器等,都可称为客户端。
2. **CGI**:通用网关接口,用于处理动态请求内容,根据参数找程序显示出内容。
3. 通常使用的网络(包括互联网)是在 TCP/IP 协议的基础上运作 的。而 **HTTP属于它内部的一个子集**。
4. **应用层**:应用层决定了向用户提供应用服务时通信的活动。
**传输层**:传输层对上层应用层,提供处于网络连接中的两台计算机之间的数据传输。
**网络层(又名网络互连层)**:网络层用来处理在网络上流动的数据包。数据包的网络传输的最小数据单元。该层规定了通过怎样的路径到达对方计算机,并把数据包传送给对方。与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所起的作用就是在众多的选项内选择一条传输线路。
**链路层(又名数据链路层,网络接口层)**:用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(网卡)及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。
5. 利用TCP/IP协议族进行网络通信时,发送端从应用层往下走,接收端则从应用层往上走。
6. 传输层把从应用层处接收到的数据(HTTP请求报文)进行**分割**,并在各个报文上打上标记序号及端口号后转发给网络层。
7. 在网络层(IP协议),增加作为通信目的地的**MAC地址**后转发给链路层。
8. 发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层时会把对应的首部消去。这种把数据信息包装起来的做法称为**封装**。
9. 负责传输的IP协议。IP协议的作用是把各种数据包传送给对方,其中两个重要的条件是**IP地址**和**MAC地址**。IP地址指明了节点被分配到的地址,MAC地址是指网卡所属的固定地址。IP地址可以和MAC地址进行配对。**IP地址可以变换,但MAC地址基本上不会更改**。
10. 使用ARP协议凭借MAC地址进行通信。IP间的通信依赖MAC地址。在进行中转时,会利用下一站中转设备的MAC地址来搜索下一个中转目标,此时会采用ARP协议,ARP协议是一种解析地址的协议,根据通信方的IP地址就可以反查出对应的MAC地址。
11. 确保可靠性的TCP协议。TCP位于传输层,提供可靠的字节流服务。字节流服务是指,为了方便传输,将大块数据分割以报文段为单位的数据包进行管理,而且TCP协议能够确认数据最终是否送达到对方。采用**三次握手**。
12. 负责域名解析的DNS服务。它提供域名到IP地址之间的解析服务,通过域名查找IP地址,或逆向从IP地址反查域名。
================================================
FILE: Game/Cocos/basic.md
================================================
# Cocos 基础知识点
## 布局
### 约束
之前在做“方块弹球”时没考虑好多屏幕适配的问题,导致在 Android 上玩时会出现黑边。如果想要对的组件进行约束,可以选择**像素值**或**百分比**进行约束,如下图所示:

================================================
FILE: Graphics/app.md
================================================
## 多媒体工具
### Pr
通过 Creative Creator 下载的 pr 版本需要跟随购买时所选择的国家授权,中国大陆只认简体中文,选择英文后下载的 pr 授权无法获取,太操蛋了。
### Ae
同上,需要在系统设置中手动修改 app 语言为简体中文后才可打开。
================================================
FILE: Graphics/metal.md
================================================
## Metal
* 标准化:提供面向 3D 图像和数据并行化计算范式。
* 更底层:提供更底层的 API 访问 GPU。
* 低开销:通过提供多线程计算和预编译资源来降低运行时消耗。
除了以上所说,Metal 不但使用了大量的 GPU 并行计算的能力来进行数据可视化或做数学计算,还可定制化于机器学习、图片和视频处理或图形图像渲染。
WWDC14 推出基于 C++11 的 MSL(Metal Shading Language)。
GPU 的优化方向是“在单位时间内可以处理多少数据”,而 CPU 的优化方向是“处理单个数据需要花费多少时间”。
### 基础知识
#### 渲染流程
**什么是渲染?**
在 3D 计算机图形学下,把一堆点汇集在一起创建出一副图像,这个图像就被称为渲染。

### 文章链接
- [Metal - Apple](https://developer.apple.com/documentation/metal)
- [Metal 语法说明](https://xiaozhuanlan.com/star)
- [iOS 图像处理](https://xiaozhuanlan.com/colin)
### 代码链接
- [LearnMetal](https://github.com/loyinglin/LearnMetal)
- [Metal-Practice](https://github.com/colin1994/Metal-Practice)
================================================
FILE: History/2_Apple_History.md
================================================
# Apple history
苹果公司,截止到2017年5月25日为止,市值首次突破8000亿美元大关,达历史最高位。笔者估计乔帮主一定不会想到当初他和沃兹尼亚克一同在家里的车库创造出来的苹果会达到今天这种高度。
苹果公司可以说是一个极具传奇色彩的企业,不但创始人乔帮主是奇葩,整个公司上下都充满了奇葩的气氛。为什么要说乔帮主是个奇葩呢,关于奇葩的定义大家各有所见,但是笔者在此所说奇葩绝对是个褒义词。如果大家有阅读过一些关于乔帮主的书,那么或多或少都会知道关于乔帮主为人处世的一些东西,一千个读者就会一千个哈姆雷特,所以很多人在阅读这类书籍时,很容易就会产生误解。市面上的很多书籍为了达到宣传的效果,把乔帮主宣传得上天入地无所不能,与乔帮主一起开创苹果公司的沃兹尼亚克前段时间出来澄清了,他和乔帮主其实并未在位于洛思阿图斯的车库里进行创业,在接受《外电》采访时,沃兹尼亚克称:“外界对那间车库知之甚少,而媒体对它也过分渲染了。那个车库虽然最能够代表我们的初期创业,但是我们在那没做任何设计工作。我们将已组装好的产品运到车库,确保它们能用,然后我们再把这些电脑运到商店拿钱。”
沃兹尼亚克解释称,羽翼丰满的苹果很快就离开那间车库了。他还补充,“那个车库很少同时有超过两个人,并且大多数情况是,他们呆在那闲晃,什么也不做。”,不过人们都希望听到些附有传奇色彩的创业故事,最好是能够拍成电影那种,尽管这会有些失真
微软和苹果真的是对欢喜冤家。乔帮主一直都很憎恨盖茨盗取了他在Lisa OS上的图形界面设计,不过乔帮主的这种图形界面的人机交互操作方式好像也是在参观施乐的帕洛阿尔托研究中心时看到了一款早期的操作系统版本,之后产生了图形用户界面的创意。关于谁才是图形界面的发明者,微软、苹果和施乐打了近六年的官司,最后法院把这场官司给打回原告了,具体原因笔者一直没找到确切的相关资料,不过现有的一种说法是,最高法院也无法做出谁才是最终的图形界面发明者。
还有这么一件趣事,乔帮主一直都对Android怀恨在心,曾经落下狠话:“如果有必要的话,我愿意耗尽最后一口气,花掉苹果公司存在银行里的400亿美元,一定要纠正这个错误……我要毁掉安卓,因为它是一件剽窃产品,我将为此发动热核战争。”。Android之父Andy Rubin原本可是想把Android用在数码相机上的,直到iPhone的出现,才让Andy Rubin意识到移动设备的潮流已经非常清晰,数码相机的市场还不够大。
其实对于乔帮主这种独特的性格和气质,很多人如果不能理解的话,是非常痛苦的。比如在苹果公司工作的工程师们,很多次都向公司高层管理人员抱怨过乔帮主那奇怪的性格,他是那种喜欢直接指挥的管理者,他经常直接过问员工的具体工作,而且又总是朝令夕改,让员工感到无所适从,不少人跑去和高管们抱怨。但正是因为乔帮主这种独特的性格,才让苹果公司推出了这么多激动人心的产品。关于乔帮主在1985年因为他特立独行的性格导致了被公司董事会集体卸下CEO的职务,关于这方面的内容网上有非常多的资料,大家可以自行网上查找。
苹果公司董事会看不惯乔帮主的这种挥霍资金的做法,强烈要求他交出管理权力,由“更具经验”的职业经理人来操盘苹果公司,而乔帮主唯一争取到的权力,就是这个信任CEO的候选名单可以由他来提供。这个CEO候选人就是当时被乔布斯一语“Do you prefer to sell sugar water for the rest of your life or come with me and change the world?”击中要害的约翰·斯卡利。
斯卡利在乔帮主离开苹果公司后的担任CEO期间,为了增收营业额,还做了很多悲剧的事情。他曾一度让苹果公司进入了服装、家居、生活甚至是玩具市场,并且生产制造出了大量的商品成品。
公允的说,约翰·斯卡利并非一无是处的蠢货,只是这个不太适合苹果的家伙,被阴差阳错的放到了苹果公司掌舵人的位置上——事实上,除了乔布斯以外,可能全世界都没剩下几个人能够挽救当时的苹果——如今,约翰·斯卡利创办并经营着一家情况不错的投资公司,主要关注于可穿戴设备领域,我们也祝他一切顺利。
// 上图就是约翰·斯卡利在对着镜头演示其最新投产的智能手环产品Misfit,只支持连接iOS系统。“Misfit”这个名称就来自乔布斯及苹果公司最著名的广告《不同凡响》中:“Here’s to the crazy ones. The misfits(致那些疯狂的人们,那些与众不同的人)”。
推荐大家几个关于苹果公司和乔布斯的资料,仅供参考:
1、[乔布斯传](http://list.youku.com/show/id_z1eaf3b7c404c11e2b16f.html?tpa=dW5pb25faWQ9MTAzNzUzXzEwMDAwMV8wMV8wMQ)
2、[成为乔布斯(Kindle)](http://pan.baidu.com/s/1jIKEM7k)
**以上内容部分来自网络**
================================================
FILE: History/3_Mac_OS_X.md
================================================
# macOS(Mac OS X)
关于macOS系统笔者认为除了计算机相关或者工作需要的小部分同学外,剩下的大部分人还是对它不是很了解。估计这个不了解也是来源于macOS目前在全球市场份额太小的原因所致,因其高昂的价格挡住了非常多的人购买它的欲望。反观国内,说起Windows大家就一点都不陌生了,上个世纪首先打入国内个人电脑市场的操作系统就是Windows(DOS是最早进入中国市场,而不是进入国内个人电脑市场),毕竟先入为主的优势让很多人习惯了去使用Windows。习惯了Windows系统的操作习惯后,再去切换另外一个新的操作系统使用的话,这成本有点高,而且大家想想,Windows操作系统在上个时间80年代引入中国,那段时间能使用上个人电脑的现在都已经快四十岁了,快四十岁的人,如果不是从事计算机专业的相关工作,他已经很难去学习一种新的操作方式了(macOS和Windows的使用习惯上还确实是有很多不一样的地方),当然不可否认,也确实是因为有了Windows的授权安装在各种品牌的电脑上,才让全国甚至全世界快速的迈进了信息时代。
**一.最早期Mac OS**
**1. System 1-6**
1984年的苹果发布了其第一台Mac个人电脑,与其一起发布的操作系统当时被简单的称为System Software,第一代System 1打破了字符终端的模式,最早使用图形界面和用户交互设计:基于窗口操作并使用了图标,鼠标可以在屏幕上进行移动,并可以通过拖拽来拷贝文件及文件夹。这让它成为图形界面设计的先驱,但后续直到System 7界面始终没有大改变。
**2. System 7**
1991年的System 7开始引入彩色,图标也增加了隐约的灰色,蓝色和黄色阴影。但Mac OS整体界面却始终没有显著的变化。与此同时微软家的Windows从黑屏的DOS到全屏幕的Windows 1,再到成熟的Windows 3,最后演变到奠定当今Windows界面基础的炫丽多彩的Windows 95。用当时的眼光来看,这个变化是相当惊人的。而Mac OS因为因循守旧,在界面设计上从领先掉到了最后。
另外从System 7的7.6版本开始被苹果公司改名为Mac OS ,这一年是1997年。
**3. Mac OS 8**
1997年,苹果发布的Mac OS 8开始加入更多的颜色,默认支持256色的图标,并较早的采用了等距风格图标,也称伪3D图标,但整体界面变化依旧不大。
**二. NeXT附体**
已经有十年历史的Mac OS已经遇到了瓶颈限制,为了让Mac OS现代化内部做了一番尝试和舍弃后,最后决定收购NeXT,因为不仅可以带来用户界面的变化,还可以使整个系统设计的全盘革新。
买完NeXT后,乔布斯回来了,“你们就是一群白痴!”他把所有团队的人叫到一个房间里以乔布斯风格把所有地方都骂了一遍,之后包括拉茨拉夫在内的设计师们的日子便越来越难熬了。
注:拉茨拉夫,当时Mac OS人机界面设计负责人
**三. 过渡OS X**
**1. Rhapsody**
1997年苹果发布了过渡时代的Rhapsody,整合了Mac OS 8的外观与NeXT-based界面,它是介于NeXT以及Mac OS X之间的操作系统,也可以理解为是套了壳的NeXT操作系统。
而代号Rhapsody是依循苹果在1990年代以音乐名词作为操作系统代号的模式所命名的,其他代号包括Harmony \(Mac OS 7.6\),Tempo \(Mac OS 8\),Allegro \(Mac OS 8.5\)及Sonata \(Mac OS 9\)。
**2. Mac OS 9**
1999年发布,是乔布斯宣布过渡到Mac OS X阶段路线上最后一个Mac OS系列。
**3. Mac OS X Server 1.0**
1999年3月苹果发布Mac OS X Server 1.0,即第一个版本的Mac OS X开发者预览版,它是苹果第一个真正基于NeXT的操作系统,和Rhapsody很像。
**四. 早期Mac OS X**
**1. OS X公开测试版**
2000年9月发布,在这个版本中全新的用户界面Aqua初次亮相。另外它也是所有Mac OS中惟一一个将苹果菜单置于屏幕顶部中央的版本,这个修改因饱受用家诟病而在Mac OS X 10.0中被恢复。
注:Aqua是Mac OS X的GUI商标名称。
**2.OS X 10.0 Cheetah猎豹**
2001年3月,经历了四个开发者预览版和一个公共测试版之后的Aqua界面终于跟随10.0正式发布,发布后改变了人们对计算机界面的印象,在随后的10年里苹果一直沿用这套界面风格。
另外伴随Aqua一起来的还有苹果一整堪称经典的套拟物化的图标,也是一直基本持续沿用到现在,以及一个全新的方式组织Mac OS X应用程序的用户界面:Dock栏,以及组成Aqua界面的那些细节:菜单、按钮、进度条、滚动条等等,其中一根看似简单得不能再简单的滚动条,就耗费了苹果设计组整整六个月。这些都影响了整整一代图形界面设计者。
One more thing,从Mac OS X苹果开启了以猫科动物系列为代号的命名史。
**3. OS X 10.1 Puma美洲狮**
OS X 10.0半年后即2001年9月,苹果就推出了OS X 10.1美洲狮,没有新增太多功能,主要聚焦于改善系统表现。
**4. OS X 10.2 Jaguar美洲豹**
2002年8月发布,在这一版本中Aqua界面的装饰风格达到新高峰:窗口背景底纹,非活动窗口标题栏半透明、滚动条的抽空效果。另外也是从这个版本有了新的起始画面和新的苹果标志,过往Happy Mac的标志不再出现,取而代之的是如今那颗被偷咬了一口的苹果。
**五. 进化Mac OS X**
Aqua的衰退,这种变化事实上从OS X 10.3 Panther就开始了。
**1. OS X 10.3 Panther黑豹**
2003年10月发布,在这一版本Aqua界面引入了新的Brush风格,即金属拉丝质感,并在最常用的Finder上使用。
**2. OS X 10.4 Tiger老虎**
2005年4月发布,顶栏最右侧新增了一个蓝色的Spotlight搜索按钮。
**3. OS X 10.5 Leopard豹**
2007年10月发布,用户界面上改进幅度比较大的一个版本,虽然基本的界面仍为Aqua和其糖果滚动条,但新加入了一些铂灰色和蓝色,另外重新设计的3D Dock和更多的动画交互使得新界面看上去3D效果更强,此外还改进了Finder、半透明菜单条并新增了最初只用于iTunes的Cover Flow界面。
整体来说这一版本的界面相比之前有了翻天覆地的变化。
**4. OS X 10.6 Snow Leopard雪豹**
2009年8月发布,就像雪豹的名字,只比豹多了个雪字,它是以OS X 10.5的版本为基础跟进开发的,不过OS X 10.6还跑去向iOS偷师,引进了Mac App Store即应用商店。
**六. 现代Mac OS X**
**1. OS X 10.7 Lion狮子**
2011年7月发布,重新设计了Aqua GUI元素、按钮、进度条、滚动条(不使用时会自动隐藏)以及“滑动切换”的选项卡,全新设计了拟物化iCal界面,新的通信录和邮件应用都使用了类似iPad的界面。此外还引进了像iOS那样的应用启动器Lauchpad界面。
**2. OS X 10.8 Mountain Lion美洲狮**
2012年7月发布,和Lion同样的的风格,更多类iPad及iOS的功能界面被引入(一些内置应用程序甚至被更名以与iOS保持一致),整体界面变化不大。
**3. OS X 10.9 Mavericks**
2013年10月发布,可以说是近期苹果的突破之作,界面上它看起来则更像是OS X 10.6的继承者,OS X 10.7和OS X 10.8里面的拟物化设计在OS X 10.9中被移除。
另外从这一版本苹果不再以动物命名Mac OS X,取而代之的是加州地名,如Mavericks就是加州某个冲浪景点。
**4. OS X 10.10 Yosemite优胜美地**
2014年10月发布,苹果历年来变化最大的操作系统,包括趋于扁平化的界面风格、类似iOS7的图标设计、半透明导航栏及全新字体设计等。
**5、至今**
在去年的WWDC大会上Mac OS X正式更名为macOS,同时也在当年的WWDC上把所有的平台的名字都变成了统一的风格。进入到macOS后界面上还是没有多大的变化,不过,比界面上的变化更大的变化那就是苹果把文件管理系统换成了APFS,升级成功后就多了好几个G。这不得不说是一个很大的进步,非常大的进步。
推荐一本关于macOS的书:[Mac OS X背后的故事](http://pan.baidu.com/s/1bp1Wn9P)
**以上内容部分来自网络**
================================================
FILE: History/4_iOS.md
================================================
# iOS(iPhone OS)
iOS,经历了十年的发展,现在已经发展为移动操作系统家族中不可或缺的一员。从当初iPhone Runs OS X到现在最新的iOS 10,乔帮主改变了世界,改变了我们对移动设备的看法。说实话,在iOS发展到现在的这十年间,曾经无比辉煌的功能机厂商一个接着一个倒在了移动浪潮之中,就算是有微软这个好爹的Windows phone也即将与我们告别。
先不说这是否合情合理,但就从市场占有率上来说,Android和iOS两家已经把整个移动设备的操作系统占了绝大部分,留给后来者的份额微乎其微,相当于没有。但是就算这样,当初的Windows phone最高全球市场占有率也没达到3%,黑莓更不用说了,在全球每一个国家,黑莓的占有率都不足0.5%,翻身是基本不可能的了。
通过Android和iOS这几年的发展,双方互相吸收优点,互相成长。现在基本上已经不可能有任何一个操作系统能够达到这种高度。前段时间,Android全球设备拥有量正式超过Windows,成为全球用户量最大的操作系统,可见当前的移动操作系统是多么受人们欢迎。
在这十年中,iOS不断的向前发展,不断的推陈出新。App Store 2.0已经把与开发者原先三七分成的比例改成了1.5:8.5,这对我们开发者来说是一个巨大的好处。iOS的生态环境正在变得越来越好,用户的操作习惯这几年被逐渐的固化下来,这对我们来说是一个好消息,因为这不会像iPhone刚推出那会儿,所有厂商并且好包括大部分消费者都在嘲笑iPhone的操作方式,但是iOS和iPhone相辅相成发展到今天,已经给世人一份最好的答卷。苹果在完成这份答卷的过程中,苹果公司做出了很多不为人知的努力,相关内容可以参考[这篇文章](https://www.zhihu.com/question/39684892)。
接下来,我们来看看在iOS发展的这十年中都经历了哪些变化
**iPhone Runs OS X——革命的开始**
2007年1月的MacWorld大会上乔布斯发布了苹果的第一款手机,当时iOS系统还没有一个正式的名称,只是被叫做iPhone Runs OS X。全世界的同行都觉得这是一个笑话,一个不能更换铃声和壁纸,不能运行后台程序,甚至根本没有第三方应用的手机,怎么好意思被称作“智能手机”?
但一向擅长创新的苹果公司还是让我们见到了许多新奇的玩法,像3.5英寸480\*320分辨率的大屏幕、多点触控的交互方式以及从未见过的简洁UI,都颠覆了人们对于传统意义上手机的认识。尽管iPhone的出现起初只是为了挽救在当时急剧萎缩的iPod市场,但苹果也许自己都没有想到,这一部手机将会引起巨大的行业革命,颠覆整个市场的格局。
**iPhone OS 2.0——生态坏境初建成**
在iPhone OS诞生初期,还没有应用商店可供下载第三方的应用程序。乔布斯在当时鼓励开发者开发网页应用而不是原生应用,导致在当时应用程序质量不高,功能有限。直到几个月后,苹果改变了主意,并在2008年3月发布了第一款iOS软件开发包。并在当年7月推出App Store,这是iOS历史上的一个重要里程碑,它的出现开启了iOS和整个移动应用时代。
收入三七分成的制度和良好的生态环境迅速吸引了大量开发者。很快,iPhone几乎变成了一款“万能”的手机:量角器,水平仪,游戏机,其中还不乏一些相当具有逼格的“喝啤酒”,“吹蜡烛”等游戏。并且在此后的几年中苹果不停地完善App Store的功能。直到现在,App Store里的应用数量都是苹果自己最值得骄傲的地方之一。
**iPhone OS 3.0——再度完善**
iPhone OS 3.0更像是填补前两代系统的空白。例如键盘的横向模式、新邮件和短信的推送通知、彩信、数字杂志,以及最初的语音控制功能,能够帮助用户寻找/播放音乐以及调用联系人,还有最重要的复制粘贴功能。实际上在前两代系统中,人们在用惯比较成熟的塞班系统后,iPhone OS系统还是存在诸多差异或者不便,尽管在当时人们看来iPhone提供了一种新奇的使用体验。
但在3.0版本中对着些缺失的功能进行补齐之后,iPhone的实用性也大大增强了。随后,在2010年4月,苹果发布了iOS 3.2。iOS 3.2是一次划时代的演变,因为这是第一款针对“大屏”iPad平板优化的移动系统。
**iOS4——高颜值模式开启**
iPhone OS操作系统在这一代正式更名为iOS。iOS 4是前四代iOS系统中外观改善最大的一代操作系统,乔布斯及其设计团队为界面上的图标设计了复杂的光影效果,让让界面看上去更加漂亮。
Game Center是我们看到的第一个变化很大的例子。它的界面颜色丰富,绿色、酒红色、黄色等,上下底部则是类实木设计。正是在这一版系统中,“skeuomorphic(仿真拟物风格)”开始完善起来。不仅如此,界面可切换壁纸终于被带进了iOS。苹果还在iOS 4中加入了文件夹功能,全新亚麻质地背景的文件夹中,用户可以存放相关应用内容。
甚至还可把这个图标放入底部的Dock,不失为一个非常实用的功能。此外还带来全新的多任务处理新功能。通过双击Home键,用户会在屏幕底部看到一排常用应用程序列表。有了它,用户无需翻页,便能快速地在应用间切换。当然除了操作系统之外,与iOS4同期的iPhone4也是前所未有的漂亮,首次引入了前后双玻璃的设计,厚度也仅有9.2mm,创下了全球最薄智能手机的记录。
iPhone4被认为是乔布斯最经典的杰作之一,也是乔帮主临终前最后一部杰作。至少在大众审美的角度,iPhone4在当初的市场上当之无愧的成为颜值最高的手机,供不应求的现象屡见不鲜。
**iOS5——你好,Siri**
界面与iOS 4基本相同的iOS 5,为苹果用户带来了一项非常重要的新功能:Siri。尽管最初被批功能有限,但这是苹果第一次尝试让用户以不同的方式使用自己的iOS设备,并将Siri打造成为iOS中的个人助理服务,从此以后,调戏Siri就成了iPhone 4S用户的一大乐趣。
仿真拟物设计在iOS 5中可谓达到了极致,苹果的软件界面中大量模仿现实世界中的实物纹理,例如,黄色纸张背景的“备忘录”和亚麻纹理的“提醒”应用。通知中心也在此版本中被引入了,用户从此不再需要从icon的角标中寻找通知了。iOS5时代还有一项重大的改变:App Store终于支持人民币支付了。
在App Store必须使用VISA和万事达信用卡的时候,越狱成了很多用户安装收费软件的唯一办法。而这一状况终于在iOS5时代得到改善,App Store支持网银转账充值了,我不知道有多少人为此踏上了正版App之路,但笔者就是其中之一。
**iOS6——扭曲的世界**
在这一版本中,苹果采用了全新设计的地图软件,放弃已经合作了多个版本的谷歌地图。地图元素基于矢量,即使你放大画面,图形和文字的细节仍然存在。3D模式可以让你用倾斜和旋转的角度查看一个区域。然而这一全新的地图软件并未受到广大用户的喜爱,不少用户抱怨新的地图软件是iPhone5上最大的倒退。
其地图数据并不完善,还存在大量图形扭曲现象,可以说是“一致差评”。然而在中国地区得到的评价则大不相同,由于中国地区的地图数据由高德提供,不少地方要比谷歌做得更好。除了地图之外,苹果也添加了诸多功能,Passbook,全景相机,蜂窝数据状态下的FaceTime,丢失模式等等都在iOS6中加入。
**iOS7——扁平化or拟物化?**
乔纳森带领的设计团队这一次彻头彻尾重新设计了iOS系统。如果说这是iOS系统诞生以来变化最大的一次那绝对不为过。这一次更新引发了人们对扁平和拟物两种设计风格的强烈探讨。它采用全新的图标界面设计,总计有上百项改动,其中包括控制中心、通知中心、多任务处理能力等等。
除了UI的巨大变化之外,笔者认为iOS7也不乏很多非常实用的功能,像控制中心的出现很大程度上简化了iOS系统的操作繁杂之处,我们不必为了开一个Wi-Fi而进入设置打开开关了。在这个版本中还添加了中国人较为喜爱的九宫格输入法,用户也因此少了一个越狱的理由。
**iOS8——强大的生态环境**
经过了iOS7这样翻天覆地的变化,很多用户认为iOS8的改动相对而言就小了许多。但事实上笔者认为iOS8最出彩的地方不在于这个系统本身,而是苹果对旗下所有平台进行了整合,使其生态环境愈发完善。Continuity功能的加入使苹果旗下的产品联系更紧密了,利用iOS 8中新增的Continuity,可以继续在另一台iOS设备上未做完的事情了。
比如写了一半的电子邮件或读了一半的网页等等。只要你的Mac或iPad与iPhone使用的是同一个WiFi网络,你就可以直接在Mac或iPad上接电话。这样的设计很大程度上加大了用户对苹果产品的依赖,但不得不说确实能够加强用户体验。在这一版本中苹果还为开发者带来了一些新玩具,新的编程语言Swift和Metal渲染接口,还开放了Touch ID的API,开发者还能编写额外的通知中心控件,总而言之iOS8比以往的iOS系统要更开放了。联想起在当年Symbian系统对开发者严格的约束,这样的待遇确实是非常吸引开发者的。
**iOS9——大幅降低的升级门槛**
在WWDC2015中,苹果公布了最新一代的iOS9系统,除了功能上的各种升级,最令人意外的是苹果这次把iOS 9的升级门槛控制在与上一代的iOS8相同,支持的升级设备与iOS 8相同,也就是说连iPhone4s也可以取得升级,iPhone 4s作为历史最受欢迎的iPhone手机之一,这无疑是一个天大的好消息。
与此同时iPad 2平板和第五代iPod Touch相同可以享受iOS 9升级服务。同时苹果也大幅度降低了iOS 9升级安装所需的存储空间,从原来的4.6GB下降到现在的1.3GB,对于广大16GB内存空间的iPhone用户来说又少了一个不升级的理由了。在系统上的一些功能完善也很有看点,原生地图支持公交查询,新的News新闻软件,iPad的分屏模式等等,都是一些比较实用的功能。最重要的当然是3D Touch了!随着iOS9一同发布的还有iPhone 6S & Plus,新设备的推出也就是意味着新的交互方式产生。3D Touch直接把以往横向上的二维操作变为了横向和纵向上的三维操作,大大加强了用户和设备的交互性。
**iOS10——迄今为止最好用的iOS系统**
北京时间2016年6月14日凌晨一点,苹果开发者大会WWDC2016在旧金山召开,苹果系统iOS10正式亮相,苹果为iOS 10带来了十大项更新。关于iOS10是自iOS由拟物化风格变为扁平化风格以来界面变化最大的一次系统升级,新的通知中心和新的推送界面设计,融入了卡片式风格,更加的贴合了3D Touch的操作,给用户一种直观上的反馈,同时在这一次的系统版本更新中,苹果把文件管理系统换成了更为先进的APFS,让小存储容量设备空闲出了几个G空间,同时也大大的减小了零碎文件的整理,不得不说这才是最为iOS 10中最激动人心的升级!
**iOS 11——带来的众多新功能为 iPad 注入了强大新动力,也让 iPhone 进一步成为你日常必备。**
iOS11中,苹果增加了对AR增强现实的支持,为开发者提供ARKit。该功能使用iPhone传感器来确定平面,照明,尺度估计等,用户可以加入不同的物品, AR应用能够实现更加真实的效果,光影效果更出色。苹果表示,ARKit将使iOS成为世界上最大的AR平台。iOS 11采用HEVC视频压缩标准和HEIF照片压缩标准,它们可以在保留视频和照片高质量的同时,有效地缩小这些多媒体文件的大小。并且还重点优化了相机的功能,包括样张质量、低光拍摄、光学稳定和HDR模式等方面的改善。更新了控制界面,并加入了更多选项。新的控制中心变为了一整页,所有功能都集中到了这一页,包括锁屏、3D Touch等。在锁屏界面,iOS 11更加重视一体化,用户可以通过滑动实现所有的事情。
**以上内容部分来自网络**
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 PJHubs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Media/basic.md
================================================
## Windows 系统知识
### 注册表
注册表确实是 windows 所特有的东西,早期用于解决繁杂的系统配置,类 unix 直接选择了以文件为最下单位进行管理,但大量琐碎的文件对 windwos 这种 FAT 文件管理系统是噩梦,而如 Linux 所采用的 EXT 文件系统则可以较好的处理这个问题,所以为了避免零碎文件过多而放大当时 win 文件系统的缺点,引入注册表来提高性能是非常重要的。
注册表本身是一个数据库,具备数据库的特性。
### UAC 提权
UAC(User Account Control)用户账户控制,是 Windows Vista 及以上版本中引入。以下是会导致 UAC 提权才能进行的操作:
* 配置Windows Update
* 增加、删除账户;更改账户类型
* 更改UAC的设置
* 安装ActiveX;安装、卸载程序
* **安装设备驱动程序**
* **将文件移动/复制到ProgramFiles或Windows目录下**
* 查看其它用户的文件夹
## 多媒体
### 基础知识
#### 框架
WebGL 是 OpenGL ES 的 js 封装。
OpenGL 并不是库,只是一组规范不包括实现,类似菜单。每家饭馆都有京酱肉丝,顾客可以去不同的饭馆都点京酱肉丝,不同的饭馆都有自己实现的一份京酱肉丝。一般做 OpenGL 接口实现都是 GPU 厂商去做,做完的东西被称为“驱动”,也可以不依赖 GPU 厂商提供的“硬实现”,比如 Windows 就提供了一份脱离硬件的“软实现”系统库。
================================================
FILE: Media/feature.md
================================================
## feature
### 视频变速

变速的本质:就是修改视频帧的时间戳。比如2倍速,就是 视频帧的pts(Presentation Time Stamp/显示时间戳)=原始帧pts/2,0.5倍速,就是 视频视频帧的pts=原始帧pts/0.5;外部取帧逻辑这些是不变的。
普通变速和曲线变速的区别
* 普通变速:速度是恒定的,整个视频的帧的pts计算方法是固定的,只需要除以一个固定的速度。
* 曲线变速:曲线变速的速度不是固定的,具体的计算公式根据不同的曲线类型而不同
逆变速也是一样的道理,但稍微麻烦一些。
================================================
FILE: MiniProgram/小程序初探.md
================================================
近期筹备期末考试,但每天晚上还是有些空闲时间的,想着今年要好好经营一番GitHub,因为今年给自己定下了一个“宏大”的计划——打满小绿点!当然在执行这个计划的过程中可能会由于某些突发情况导致“断点”,但梦想还是要有的嘛。
因此,啃下小程序就成为了我今年的第二件事情(第一件事为iBistu 4.0),完成这个去年就十分想踏入的“坑”。在去年也就是上个学期的某一天得知小程序面向个人开发者后,就一直按捺不住自己,总想着试一试,但因为当时开始了第一份实习工作,导致这个计划迟迟未能展开。最难受的是没想到在上学期末总共收到了两份小程序开发的外包需求,一咬牙用了大概一晚上加一个白天的时间吭吭哧哧的摸索了一番,最后的结果是——放弃。🙂把这个外包需求移交给其它同学后,草草了事。
但是现在不一样了啊,当时是因为动机不纯,现在有大把的时间可以快活~也就把这个事情提上了日程。小程序,张小龙同学给微信生态又注入的一个新成员,用张小龙同学的话来说,之所以要推出小程序,初衷是因为,“让信息触手可及,改变应用程序需要下载、安装的繁琐过程”。🙄,俗话说的好,话不能说的太满,后续发生的事情大家想必都已经了解。
在小程序推出至今也用了大大小小不下十几个小程序,不得不说确实是达到了张小龙同学所说的“用完即走”的概念,而且依托于微信开发团队强大的封装能力,为小程序提供了调用非常简单的API,使用起来难度比Native降了不止一个层级。在此给大家推荐两本书,
我主要也是通过这两本书和网上的一些教学视频等资料习得小程序开发的核心内容,并且采用[V2EX](https://www.v2ex.com)所公开的API做了一个简单的小程序[demo](https://github.com/windstormeye/V2EX-Little-Program),为后续继续搞事情做铺垫。
我们先来明确几个概念,
1. 微信对小程序的包大小限制在了1M以内。所以尽可能的进行封装;
2. 如果你有使用到tabBar,微信对小程序的tabBar图片资源推荐尺寸为81*81px,并且大小不超过40KB;
3. 小程序与目前的前端开发技术有部分区别,比如无div概念,但有view标签,可充当div;无p、span标签,但text标签,且不能渲染HTML,只能text标签才能被选中;
4. 如果你有使用到< image>标签,但未设置size,小程序的默认图片大小为300*255;
5. 跳转页面的wx.navigateTo() 只能跳转非tabBar页面;
6. 小程序官方强烈推荐使用flex布局(弹性布局);
7. 如果你要在小程序中访问URL资源,及得一定要先到小程序后台进行安全域名配置安全域名,否则你将无法进行网络请求。
8. 小程序一个页面(假设为home页面)有以下四个文件组成:
a. home.js:当前页面的网络请求、页面跳转等逻辑;
b. home.json:当前页面的一些配置,比如是否支持山下拉刷新等;
c. home.wxml:当前页面UI布局(相当于HTML);
d. home.wxss:当前页面的UI美化(相当于CSS)
---
如果你之前有过一点web开发经验,会发现它的web端设计实际上是可以直接套用在小程序上的,只不过你还需要进行做很多标签的适配,比如像上文所说的,需要把div换成view等,但因此次只是学习小程序的核心内容,所以样式部分我们就不花费太大心思了,凑合着写写。😀
做任何事情都需要提前准备,给V2EX做小程序端也是一样的,我们需要设计一套简单的交互及UI,当然你也可以用纸笔直接画,在此我推荐大家去使用[墨刀](https://modao.cc/),这个原型设计工具目前在产品中传播得很广,使用和接受程度较高。因为也是原型设计,我就不把自己设计得丑陋不堪的UI个稿放出来了。设计如下:
参照V2EX的web端设计,我们可以发现其是一个列表,只需要用到一个image、四个text和几个view标签做布局就好了
该列表下展示出了各个tab下的相关内容,所以,我就直接套用了它的信息流展示展示方式,下面是做好的最终展示,
---
经过三天晚上的摸索,慢慢的发现了开发小程序的一些套路,[工程在这](https://github.com/windstormeye/V2EX-Little-Program),第一次写小程序,很多地方考虑的还是比较肤浅,只能说是勉强入了门。给我整体的感觉小程序确实不能承载很多以往Native能够做到的事情,缺点还是不少,而且越写越觉得就是在写web啊,但它就不是web,运行效率跟Native不相上下(还是有些粗糙的地方),它能做的事情真的是少之又少,如果你只是想在微信这个生态里玩,那没问题你会很嗨皮,如果你要想稍微跳出这个圈子发挥点特长,对不起不行。
不过换句话来说,小程序的这些缺点其实也正是它的优势所在,它抛弃了众多设备特性抹平了各种不同设备的棱角,大大的缩小了适配所带来的庞大工作量,确实还真挺适合作为初创团队或者个人开发者搞些小事情的(而且我觉得也就适合做点小事情了)。不过我们最终做出一个小程序,千万记得一定要在真机上进行测试。关于小程序开发的更多细节,大家可以看[官方文档](https://mp.weixin.qq.com/debug/wxadoc/dev/),说句心里话,小程序学起来一点都不难,但是想要设计出一个好的小程序确实得需要下很大功夫的,在这么一个受限的环境下想玩出不一样的东西,嗯。。各位同学加油吧💪。
后续想做一个iBistu新闻小程序,把现有在iBistu上的新闻模块抽离出来,单独开发一个小程序去作为承载的入口,也算是继续开拓自己的技能吧。🙂
[下篇:小程序初探(二)](./项目/小程序初探(二).md)
================================================
FILE: MiniProgram/小程序初探(二).md
================================================
iBistu 4.0终于在几位同学的努力下正式推出,我也没想到iBistu怎么就发展到了4.0,四年多过去了,还能“隐约”看到学长们的代码,这种“恍如隔世”的感觉每写下一行代码就会冒出。
在学习开发微信小程序的过程中,一直在苦于追求到底哪些项目才适合与新手小白去磨练,某天下午突然想到iBistu新闻模块的这种“不干不湿”情况正适合于练手各种前端框架(我认为小程序也是一种前端框架)
iBistu新闻模块在[API文档](https://github.com/ifLab/iBistu-API)上写的非常清楚,我们只需要传入一些参数即可获取到校内新闻的JSON数据,而对于微信小程序来说,解析JSON格式的数据是最为方便不过了。再加上iBistu 4.0简洁的UI风格,不会让我这种第一次上手前端开发的同学感到阻力很大,反而还会有一种“过五关,斩六将”的feel~😝
经过上次的小程序初探,了解了小程序的整体架构、开发要求和规范,再加上参与了iBistu 4.0的iOS端开发,预估开发成本不大且获取开发经验可观,遂,开干~
---
从iBistu的[API文档](https://github.com/ifLab/iBistu-API)中可以看到,想要获取新闻数据,我们需要传入category(新闻分类)、page(分页数)、api_key和session_token(login后获取)四个参数给api.iflab.org/api/v2/newsapi/newslist这个接口
其实这第一步就有点恶心了,最开始我想的是,把iBistu新闻模块单独抽出来,作为一个独立的小程序供大家使用,但是获取iBistu所有数据的前提是,你得登录!!!想了一会儿,有两种解决方案,要么在小程序上单独做一个login入口,要么就自己再搭一个后台,第一次获取用户iBistu用户名和密码后把这些数据都存下来,以后每次做网络请求发现token过期时在后台自动刷新token。
其实从开发成本上来说,我会毫不犹豫的选择第一种,这样会很快,但是给用户的体验非常不好,在iBistu端上要登录,用你这个小程序也要登录,单从开发者角度上来想就很扯,做完自己都不想用。
现目前暂定第二种方案,但是为了加速开发时间,这部分工作挪到寒假再做吧。🙂来看看最终的效果:
从完成图效果上来看,除了不能侧滑切换新闻主题外,剩余部分已经达到甚至部分细节超过了Native,而且这还是在微信开发工具里的模拟器上显示出来的效果,因为没有注册AppID,要不然就能在微信端上预览效果了。
那,为什么不注册AppID呢?因为iBistu的数据源挂在在api.iflab.org下,iflab.org并没有在国内备案,如果你在微信小程序后台注册了AppID,你就得添加可信域名,这个可信域名不但要求是添加了SSL证书还得是通过了备案的,遂,iflab.org因此GG。
不过这样也好,新闻数据因为都是开发团队里的一个学长去学校官网上爬的,学校官网本身就没有加SSL证书,通过image标签去加载拿到的imageLink还得再去小程序后台上添加一次对应的域名,加上还没SSL证书,因此也GG。_(:зゝ∠)_
---
这是iBistu新闻小程序的整体文件结构,就比微信小程序提供的开发模板多了四个文件而已,整个小程序总体大小为49KB(大家千万记住,小程序对开发文件大小限制在了1M以内)。
index是新闻列表页,content为列表item内容页,我们首先来拆分列表页,红色为cell中的单个标签,蓝色为一个cell
iBistu在iOS端上采取了新建两个xib来完成两种不同样式的加载只有title和什么都有的cell,但在小程序中,我们只需要一个样式就足够了,因为新闻的整体listView和每条新闻cell都采用了flex弹性布局,如果布局中的元素hidden了能够自动“弹”回去,因此,我们的index.wxml可以这么来写cell,
```xml
标题
内容
```
关于顶部的tab导航栏,实际上是一个水平滚动的scroll-view(小程序也提供了跟UIScrollView一样的scroll-view喔~),关于它的元素布局我们可以在index.wxml中这么写,
```xml
tab的内容
```
这样,我们就拿到了一个搭好初步框架的新闻主体,接下来,我们去美化它。在index.wxss中可以这么写,
```css
.contentView {
margin-top: 30px;
}
.cell {
display: flex;
flex-direction: column;
margin-top: 10px;
border-top: 10px solid #efeff3;
}
.cellTitle {
padding: 10px;
font-weight: bold;
font-size: 17px;
}
.cellImage {
width: 100%;
height: 200px;
}
.cellContent {
/* padding对换行布局有冲突只能用这种傻傻的方式去写了 */
margin-left: 10px;
margin-top: 10px;
margin-right: 10px;
display: -webkit-box;
font-size: 28rpx;
line-height: 40rpx;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.titleHeadView {
white-space: nowrap;
position: fixed;
background-color: #FFF;
top: 0;
}
.singleHeadView {
display: flex;
flex-direction: column;
display: inline-block;
margin-left: 5px;
margin-right: 5px;
}
.titleHeadText {
font-size: 14px;
}
.titleHeadBottomLineView {
height: 2px;
width: 100%;
background-color: #000;
}
```
当然,wxss的样式定义是像素级copyiBistu的,如果大家觉得不好看的话,自行修改吧。现在,我们就已经把View层的东西都整理好了,接下来要做的事情就是去做网络请求拉到数据,再填充到View层中来即可。
---
首先来看顶部的scroll-view的数据填充,从API文档中可以看到,要求我们传入分类,而不是从一个接口取得所有新闻分类的信息,也就是说,新闻的分类信息我们要本地写死,
```js
data: {
// 新闻列表数据
resData: [],
titleData_en: ['zhxw', 'tpxw', 'rcpy', 'jxky', 'whhd', 'xyrw', 'jlhz', 'shfw', 'mtgz'],
titleData_cn: ['综合新闻', '图片新闻', '人才培养', '教学科研', '文化活动', '校园人物', '交流合作', '社会服务', '媒体关注'],
// 标记ScrollView每个item的底部lineView是否显示
titleIsHiddens: [false, true, true, true, true, true, true, true, true, true, true],
// 记录当前点击的ScrollView.item的下标
titleIndex: 0,
},
```
正式开始进行网络请求工作之前,我们还差一个也是最重要的参数未知,session_token,这个参数是用户登录后返回的字段,24小时之内未带上这个token进行请求,则失效。emmm,我的做法就是先去运行一个iBistu的Xcode工程,拿到token后再粘回来,先这么简单粗暴的做着。
对api.iflab.org/api/v2/newsapi/newslist这个接口请求数据,需要附带四个参数,拼接完的wx.request如下,
```js
// 在onLoad中写下这个函数
wx.request({
url: 'https://api.iflab.org/api/v2/newsapi/newslist',
method: 'GET',
data: {
// 综合新闻
category: 'zhxw',
// 首页
page: 0,
api_key: getApp().globalData.api_key,
session_token: getApp().globalData.session_token
},
success: function (res) {
// 打印出请求回来的数据
console.log(res)
}, fail: function (res) {
}, complete: function () {
}
})
```
因为有几个地方需要用到api_key和token,因此我们可以把它丢入到app.js中,然后再通过使用跟上文一样的getApp().globalData即可取到全局数据(可以说跟pch文件和NSUserDefault非常类似了。)
```js
globalData: {
userInfo: null,
session_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzYTE4OWM0NDZhOWNlMzQ0M2NjMDQ1YmQyZTM4ZDA4YyIsImlzcyI6Imh0dHBzOi8vYXBpLmlmbGFiLm9yZy9hcGkvdjIvdXNlci9zZXNzaW9uIiwiaWF0IjoxNTE1NzcwNzczLCJleHAiOjE1MTU4NTcxNzMsIm5iZiI6MTUxNTc3MDc3MywianRpIjoiZUFLZ3FkVXZUQk8xOXdldiIsInVzZXJfaWQiOjQyLCJmb3JldmVyIjpmYWxzZX0.EuW_8rxXPv-EuB1oKe9OQMMuGrEQTFuDC5QGebqP3J4',
api_key: '3528bd808dde403b83b456e986ce1632d513f7a06c19f5a582058be87be0d8c2'
}
```
此时,我们运行小程序,即可看到在控制台看到请求回来的信息。现在要把信息都展示到view上,我们需要在wxml中用到wx:for和wx:if这两个东西,补充完后的wx:request如下,
```js
wx.request({
url: 'https://api.iflab.org/api/v2/newsapi/newslist',
method: 'GET',
data: {
category: 'zhxw',
page: 0,
api_key: getApp().globalData.api_key,
session_token: getApp().globalData.session_token
},
success: function (res) {
wx.hideLoading()
that.setData({
resData: res.data
})
console.log(res)
}, fail: function (res) {
}, complete: function () {
}
})
```
补充完的index.wxml如下,
```xml
{{ item }}
{{ item.newsTitle }}
{{ item.newsIntro }}
```
需要注意的是,当我们使用wx:for去动态加载wxml中的标签时,在填充数据的时候要可以使用小程序提供的遍历对象item,这个item你可以认为是C++里的迭代器,通过迭代器去访问遍历到的每一个对象中的值。当然,如果你不想用它提供的item,你可以使用当前的循环遍历index,index代表了当前循环到的下标,然后通过下标去指定输出内容。
此时再次运行小程序,即可看到新闻数据和顶部的tab数据加载出来啦~~!!!我们再进一步,点击scroll-view上的tab标签切换新闻内容!想要做到这一点,首先要给tab添加点击事件,小程序提供了冒泡事件和非冒泡事件,简单来说就是一个是触摸事件可以逐层向上传递,另外一个不能,具体使用方法大家可以去看小程序开发文档,那里的讲解更加详细。在此,根据需求我给view绑定的是非冒泡事件catchtap,实现如下
```js
titleHeadViewTapClick: function (event) {
var that = this
wx.showLoading({
title: '加载中',
})
that.data.titleIndex = event.currentTarget.id
this.setData({
titleIndex: this.data.titleIndex
})
wx.request({
url: 'https://api.iflab.org/api/v2/newsapi/newslist',
method: 'GET',
data: {
// 根据获取点击的id来选择传入的分类字段数据
category: this.data.titleData_en[event.currentTarget.id],
page: 0,
api_key: getApp().globalData.api_key,
session_token: getApp().globalData.session_token
},
success: function (res) {
wx.hideLoading()
that.setData({
resData: res.data
})
}, fail: function (res) {
}, complete: function () {
}
})
}
```
再运行工程,可以通过点击顶部tab来切换新闻啦~!!我们再往前推进,把上拉加载也做了,因为毕竟是新闻嘛,信息流的展示还是趋于给用户“无限”的感觉。在index.json中添加enablePullDownRefresh字段,开启上拉功能,
```json
{
"enablePullDownRefresh": true
}
```
因为是上拉加载,那么必定会涉及到数据的分页,也就是前文中我们所说的page字段的作用,page字段每+1,数据就会返回时间上相对之前返回的时间更早一些的数据。我们要做的效果是追加数据,注意!是追加!更新完后的整个index.js如下所示
```js
var p = 0
// 拉取分页数据方法
var GetList = function (that) {
wx.request({
url: 'https://api.iflab.org/api/v2/newsapi/newslist',
method: 'GET',
data: {
category: 'zhxw',
page: p,
api_key: getApp().globalData.api_key,
session_token: getApp().globalData.session_token
},
success: function (res) {
wx.hideLoading()
var l = that.data.resData
for (var i = 0; i < res.data.length; i++) {
l.push(res.data[i])
}
that.setData({
resData: l
});
p++;
}, fail: function (res) {
}, complete: function () {
}
})
}
Page({
data: {
resData: [],
titleData_en: ['zhxw', 'tpxw', 'rcpy', 'jxky', 'whhd', 'xyrw', 'jlhz', 'shfw', 'mtgz'],
titleData_cn: ['综合新闻', '图片新闻', '人才培养', '教学科研', '文化活动', '校园人物', '交流合作', '社会服务', '媒体关注'],
titleIsHiddens: [false, true, true, true, true, true, true, true, true, true, true],
titleIndex: 0,
},
// 页面加载
onLoad: function () {
var that = this
GetList(that)
wx.showLoading({
title: '加载中',
})
},
// 顶部tab点击事件
titleHeadViewTapClick: function (event) {
var that = this
wx.showLoading({
title: '加载中',
})
that.data.titleIndex = event.currentTarget.id
this.setData({
titleIndex: this.data.titleIndex
})
wx.request({
url: 'https://api.iflab.org/api/v2/newsapi/newslist',
method: 'GET',
data: {
category: this.data.titleData_en[event.currentTarget.id],
page: 0,
api_key: getApp().globalData.api_key,
session_token: getApp().globalData.session_token
},
success: function (res) {
wx.hideLoading()
that.setData({
resData: res.data
})
console.log(res)
}, fail: function (res) {
}, complete: function () {
}
})
},
// 点击新闻跳转新闻详情
contentViewTapClick: function(event) {
var link = this.data.resData[event.currentTarget.id].newsLink
wx.navigateTo({
url: '../content/content?link=' + link,
})
},
onReachBottom: function () {
//上拉
var that = this
GetList(that)
}
})
```
大家从上文也看到了多了一个contentViewTapClick方法,这个方法就是我们后边要展开说的内容,从API文档上我们找到新闻详情接口,需要我们传入link字段数据,这个数据是个URL,问了学长,实际上就是给这个接口丢一个让它自己去实时抓数据的地址。hhhhh,这个做法确实巧妙。因此,我们需要在点击每条新闻的wx.navigationTo跳转方法时传入link字段的数据。
---
在content.js中写下,
```js
onLoad: function (options) {
var that = this
wx.showLoading({
title: '加载中',
})
wx.request({
url: 'http://api.iflab.org/api/v2/newsapi/newsdetail',
method: 'GET',
data: {
link: options.link,
api_key: getApp().globalData.api_key,
session_token: getApp().globalData.session_token
},
success: function (res) {
console.log(res)
wx.hideLoading()
}, fail: function (res) {
}, complete: function () {
}
})
}
```
运行小程序,随便点击一条新闻,就会在控制台中打印出来了相关信息。刚开始我想偷个懒,想直接navigationTo拿到的link,没想到小程序居然不支持H5外链,只能跳转自身页面,所以只能自己拼数据了。
iBistu iOS端的新闻不是我写的,对于其中的一些实现新闻详情的巧妙方法我是一点都不了解,所以看到返回来的新闻详情数据后,整个人都不好了。
我的乖乖,您看懂是什么意思了么?一堆回车符的地方就是要插入图片的地方。😱。所以我们要根据回车符出现的地方来判断是否应该插入图片,看了看iBistu的新闻详情部分的实现,确实是这么做的。
但是更伤的问题来了,根据回车符我截断字符串在数组里,然后po出了内容,一看更呆了,图片在一个地方集中出现得越多,那么这块地方的换行符也就越多,换句话说,得根据回车符的多少来决定插入的图片数量。
嗯,其实这还不是最伤的,按照这个思路弄完了以后,浏览了前面几条新闻,完美对上了,但是!!!到了后边的几条新闻就全乱了。该出现图片的地方没出现,不该出现的图片的地方空一大片
原因是因为刚开始找到的规则是,三个换行放一张图片,如果当前区域放超过一张图片,比如说是两张图片,换行数则由三个变成了七个,也就是说,以三位基数,每多加两个换行多一张图片。
但事实上不是这样啊!😭。后边几条新闻的换行数跟图片数对应关系完全不符合之前找到的,全乱了。这就非常的难受了。弄到最后,实在没办法,决定了如果是多张图片就只放一张😔。
如果这部分你没能拿到真实数据好好研究一番的话,就算看了代码意义也不太大,
```js
Page({
/**
* 页面的初始数据
*/
data: {
resData_cn: [],
resData_image: [],
resData_display: [],
resDataCount: 0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var that = this
wx.showLoading({
title: '加载中',
})
wx.request({
url: 'http://api.iflab.org/api/v2/newsapi/newsdetail',
method: 'GET',
data: {
// 拿到NavigationTo拼接而来的参数
link: options.link,
api_key: getApp().globalData.api_key,
session_token: getApp().globalData.session_token
},
success: function (res) {
wx.hideLoading()
var resString = res.data.article.split('\n')
var resdata_cn = []
var resdata_cn_index = -1
var resdata_display = []
var tempIndex = 0
for (var i = 0; i < resString.length; i++) {
// 判断是否含有中文,若不含有中文则为图片
// 即换行符
if (/.*[\u4e00-\u9fa5]+.*$/.test(resString[i])) {
tempIndex = 0
resdata_cn_index++
resdata_cn[resdata_cn_index] = resString[i]
resdata_display[resdata_cn_index] = true
} else {
tempIndex++
// 这么搞新闻图片加载不全
if (tempIndex == 3) {
resdata_display[resdata_cn_index] = false
}
}
}
var count = resdata_cn.length + res.data.imgList.length
var displayArr = []
var displayIndex = 0
for (var j = 0; j < resdata_display.length; j++) {
if (resdata_display[j]) {
displayArr[j] = ""
} else {
displayArr[j] = res.data.imgList[displayIndex]
displayIndex ++
}
}
that.setData({
resData_image: displayArr,
resData_cn: resdata_cn,
resDataCount: count,
resData_display: resdata_display,
})
wx.hideLoading()
}, fail: function (res) {
}, complete: function () {
}
})
}
})
```
其中设置了很多BOOL变量,为啥要设置这么多的BOOL变量呢?我们来看一张图,
我对content.wxml做了如上的分割,实际上也是一个listView,每一个cell(蓝色)中都有text和image,其写法如下所示,
```xml
{{ item }}
```
通过控制image标签的hidden来达到效果,这就是为什么设置了这么多BOOL变量的原因。😝
---
以上就是我二探小程序的历程,可能说的有些不够清晰,你可以在[GitHub](https://github.com/windstormeye/iBistu-News-Mini-Program)下载源码,自行研究一番,就能够理解我以上所讲述的东西了。
这次二刷小程序,给我的感觉是以前端上觉得做起来的简单的东西,在小程序上会越发的更加简单,在端上觉得比较困难的东西,小程序说不定会有一些比较奇妙的解法,甚至会有出其不意的地方。这两次学习小程序的开发,总的来说,让我很意外,小程序不但是给用户“用完即走”的感觉,给开发者也一身轻松,不过,也只是“小程序”。
================================================
FILE: NLP/NLP.md
================================================
# 基础知识
* 单词边界界定
* 中文缺少词和词之间的界限符(中文自动分词)
* 上下文关联
* 基于统计学习的分词工具优于人工规则的分词工具。
* 未登录词造成的分词精度下降至少比分词歧义大 5 倍。
* 字标注统计学习方法能够大幅提高未登录词的识别率。
## 分词是最重要的
* 开源的中文分词工具,大多使用的也是开源的语料库,缺少大量的专有词汇。
* 貌似 HanLP 不是基于语料库的实现。
## 基础技术
* 词法分析:分词、词性标注、实体识别
* 词向量表示:语义挖掘、词义相似度
* 文本相似度:计算两个文本语义相似度,实现推荐和排序
* 依存句法分析:自动分析文本中的依存语法结构信息
* DNN(深度神经网络)语言模型:判断一句话是否符合语言表达习惯
## 应用技术
* 文本纠错
* 情感趋向分析
* 评论观点抽取
* 对话情绪识别
* 文本标签
* 文章分类
* 新闻摘要
* 文本审核
* 文本翻译
## 涉及到的知识点
* 统计学:最大概率、凝固度
* 信息论:信息熵、交叉熵
* 图模型:有向无环图、HMM 模型、CRF 模型
* 数据结构:Trie树、图结构
* 线性代数:余弦相似度
* 深度学习:神经网络、词向量训练及使用
## 什么是词?
最小的能够独立运用的语言单位,能单说有意义或用来造句的最小单位。
## 先分词后理解
## 中文分词歧义
鉴别一个人是否真的做过 NLP 就问这个
* 交集性歧义
* ABC。可以是 AB/C,也可以是 A/BC
* 组合型歧义
* AB。可以是 AB,也可以是 A/B
## HMM(隐马尔可夫模型)
统计模型,用来描述一个含有未知参数的马尔可夫过程。
## CRF(条件随机场)
给定一组输入序列条件下,另一组输出序列的条件概率分布模型
# 解决中文分词歧义
## 最大匹配法
是一种贪心算法,很可能错过更优的切分路径。因为贪心算法在每一步选择中都采取在当前情况下最好的选择,可能会陷入局部最优解的情况。
* 正向最大匹配法
* 逆向最大匹配法
* 双向最大匹配法。通过对比正向和逆向的匹配结果,如果结果不同,需要一个评估函数来决定最优的切分路径。
特殊情况:自建《不能单独成词的字表》
## 词总数最少法
我们为词总数最少法,添加上“不成词字表”的规则,每出现一个不成词的单字,就加罚一分。上面的例子就会得到如下的结果:
* “为人 / 民 / 服务”的罚分:1 + 2 + 1 = 4
* “为 / 人民 / 服务”的罚分:1 + 1 + 1 = 3
词总数最少法,切分路径结果里有多少词,就罚多少分,每出现一个不成词的单字,就加罚一分,罚分最少的就是最优的分词结果。
具体细节可看:
https://juejin.im/book/5d9ea8fff265da5b81794756/section/5da3e576f265da5b576be5a5
## 动态规划
https://juejin.im/post/5a29d52cf265da43333e4da7
## 语素
语言中最小的音义结合体,一个语言单位必须同时满足三个条件,「最小、有音、有义」才能被称做语素。
# N-Gram 切词法
一种基于统计语言模型的算法。将文本里的内容按照字节进行大小为 N 的滑动窗口切分,形成长度为 N 的字节片段序列。
* 泛化能力:一个机器学习算法对于没有见过的样本的识别能力。
## 马尔可夫假设
每隔词出现的概率只跟它前面的少数几个词有关。如一阶马尔可夫假设,只考虑前面一个词。
## N-Gram 模型在中文分词中的作用
当一段文本根据词典切分存在多种可能的结果时,如何选择最优的切分路径,可以使用 N-Gram 模型利用统计信息找出一条概率最大的路径,得到最终的分分词结果。
## 有向无环图(DAG)
在图论中,如果一个有向图从任意顶点出发无法经过若干条边回到该顶点。
## 归一化
目的将不同尺度上的评判结果统一到一个尺度上,从而可以做比较。例如,A 当月薪水为一头牛十斤麦子,B 当月薪水为一头羊一百斤玉米,很难比较到底谁的薪水更高,通过货币来进行归一化计算。

================================================
FILE: Others/myinterview.md
================================================
# 我的大学——实习生涯
> 去年暑假旅行回来后,开始萌生要着手用在大学最后一年的时间来写完自己的大学回忆录。可在网易云音乐中搜索电台《PJHubs》收听语音版。此篇文章为第三篇,主要讲述了大学四年中,我所经历的工作。
## 简介
很多时候我都在思考一个事情,“我来到这个学校到底需要的是什么?”。从大一上结束后,基本上每个人都可以预知到自己的大学生活接下来要怎么过了。身边的很多同学有把大学四年每天都过成一样的,也有每天都过得让我十分羡慕的。只能说每个人的追求、认识这个世界的方式都不一样。
很多我们以为是无法理解、惊掉下巴的事情,在他人眼中看来就是一件稀疏平常的事情。有这么一个同学让我到现在都忘不掉,他有一辆公路自行车,全车身碳纤维,据他所说全北京只有他才能修自己的自行车,单是车轮就五千多(甚至更高?)。我的第一辆自行车是在中关村的家乐福花了 800 块买的永久牌 22 寸小山地,当时说服自己从生活费中拿出这 800 块买这辆车,内心纠结了将近一个星期,最后跟另外一个舍友互相说了几个理由,把对方都说服了,才搭伙一同去买了。关于这辆自行车的事,后续会继续展开。
我说这个事情存粹只是想证明一下人与人之间的差距真的从一开始就有了。正是因为在日后的与这位拥有全身碳纤维自行车的同学生活了一年多,慢慢的逼迫我要更早的认识自己,更早打开自己对这个世界、对这个社会的认识。
## 第一步
迈出面向社会的第一步是在大二下刚开学那一会儿,群里有位学长在发布一个外包,仅仅只需要写一个 iOS 客户端,并不需要做其它额外的事情。这个机会被我抓住了,当时心里只想着能够让自己通过技术来赚到自己的第一桶金。
在后续的开发工作中,因为我相当于是乙方,做的东西上有很多需求不明确的地方,但在沟通的过程中也看出来了这个 app 并不是一定要上线。磕磕碰碰的集成了一个三方绘图库就草草了事,过了一个星期甲方居然没动静了。最后实在等不了就自己开口问了这个事情,当时对接了快一个月,第一次明白了开口让人拿钱是一件多么需要勇气的事情。
不过最后还算顺利,自己通过所学技术赚到了第一桶金两千块,对于当时的我来说数额是相当客观了。虽然最后看着多出的这两千块不知如何是好,但现在想起来这快一个月的时间其实让自己感受最深的是转变了自己的思维,同时也认识到了只待在学校里是一件多么可怕的事情,待人接物和学会说话是一件多么重要的事情。
同时也因为这件事让我确信了学习 iOS 开发的这条路,毕竟因为它让我赚到了第一桶金。
## 第二步
接下里,继续上了几天课。但是越上课我就越迷茫,我以为经过了外包的事情能够让自己安心下来继续上课,但事实证明实际上我还是没能给自己找到一个合适的答案。
随后又有一位学长(暂且称为 X 学长)在群里发布了招聘 iOS 和 Android 实习生的消息,我立马联系了这位学长,把面试约在了一周后。在这一周的时间中,我把自己做过的所有关于 iOS 的内容都整理在了一个单独的文件夹中,还问了 X 学长是否需要准备算法,学长很志趣的说,“大家都是明白人”。
到了面试当天,学长问了我都做过什么,我一五一十的把自己的所有都展示了出来,唯一值得算得上是“工程”的项目只有当时拿来参加比赛的“大学+”了,我个人觉得是“大学+”救了我,因为当时和小伙伴设计了很多小的细节,虽然对于那时的我来说实现起来非常困难,但最后用了一些“奇技淫巧”给实现了。而且“大学+”整体来看功能相当完整,甚至还集成了一个完整的“IM”,现在想起来才大一完就敢这么玩真是有点佩服自己了。
印象毕竟深的是,学长最后考了我一个当时几乎所有 app 都会放的一个功能,“如何实现带有 `headerView` 下拉放大的功能?”,当时听到脑子里懵了,学长让我慢慢想,有什么想法直接说出来就好。但是我实在是怕说错,以前根本没想过这个事情。最后说的内容大致为:“可以获取到 `scrollView` 的距离,通过距离去调整 `headerView` 的宽高”,没想到当时居然被我蒙对了,学长笑了一下说就是这么做的,后面还问了一些其它的事情现在已经记不得了。
在学校等了一两天,学长说通过了,下周可以来上班了。当时开心不得了,我居然可以实习了?还真有人想要我这个刚上大二的小屁孩?睡觉都要笑醒。
进去后才发现,其实我十分的不喜欢的这个环境,是家外包公司,灯光昏暗,工位狭小,我经常性的被挤到一角,而且偶尔椅子还会被占用,只能坐板凳,板凳坐一天的滋味真是无法言语。最要命的是开发流程我现在看来十分不严谨,而且几乎是不顾及规范,怎么方便怎么来,怎么快怎么来。
但对于当时的我来说每天也是开开心心的来,开开心心的回去,因为每一天都在学习新的东西,正是因为这段现在看来十分难受的经历,让我完全熟悉来 iOS 中的 `MVC` 架构到底是怎么回事,同时也熟悉了工厂模式等一些之前完全没听过的设计模式。换句话来说,这段两个多月的时间,我是去熟悉 API 的哈哈,给自己一个大量练习的机会。
## 第三步
实习中负责的两个项目结束后,基本上没我什么活了,也就慢慢的不去了,但没想到的是第三个月的工资居然没给我发!从那时起彻底对外包公司产生了恶意。也就这么莫名其妙的“离职”了。
中途调整了差不多一个月,暑假快到了,想着自己暑假不能闲下来,又踏上了找实习的道路。在这次找的实习中,我的胆子放大了很多,投了京东、银客网等一些看上去好像挺不错的公司。当时自己还是没有习得多上面试套路,基本都是被面试官带着耍,自己之前做过的很多东西都没办法去展示,最难受的在银客网的面试,面试官上来就问了一堆 C++ 的问题,他的脸慢慢的黑了下来,我的脸也慢慢的黑了下来,双方都不满意。
在京东的面试让我感到最意外,整体非常舒服,确实是对准在了 iOS 方面上,也确实是让我说到了自己不能说为止,记得非常清楚,第一题就是要自己实现一个图片下载器。当时脑子一下子也懵了,完全不知道如何回答,同样是因为平时没有往这方面去想过。面试官在最后推荐了我几本书去加强基础,非常感谢他!在骑车回学校时,我一路狂奔,开始对自己的选择产生了些许怀疑,“出来面对社会这件事情是否是正确的?”,当时我已经发现了在学校里漫无目的学习一些不知道用来干什么的知识,我更愿意去接触实际生产环境中遇到的问题,通过遇到的问题来折射出我应该去学习什么知识,这样有目的性的学习动力会更强。
随后在蜗牛睡眠的面试中,一进门我就有种“就是它了!”的感觉。当时的蜗牛睡眠是在望京凯德 mall。出电梯后一开门就闻到了异常的芳香!留给了我一个非常好的印象!在接下来的面试过程中,因为经历过了前几次的教训,我还是借用还是“还是大二,目前还有很多东西需要继续学习”等的思路对待一些我回答不上来的问题。非常高兴的是,一两天后我得到了 HR 小姐妹的回复,下周可以直接入职。
刚开始我以为做的是“蜗牛睡眠” app,在待入职的这几天的时间中,一直在使用,还找出了一个 bug,但进去后才发现,其实并不是这样的。boss 给我和另外一个同为实习生的同学开了一个新的业务线——游戏。需要在接下来的时间中做一个类似与“方块弹珠”的游戏。确实,又把我给整懵了。
到后来还好,我们选择了使用 `Cocos2D-X` 这个跨平台游戏框架,虽然用了跨平台游戏框架,但实际开发中却不是跨平台,我和另外一个实习生都各自针对 iOS 和 Android 各写了一套完全独立的代码,估计当时我对自己的成果比较看重,不愿意与人直接分享自己的工作成果,现在想起来感到很奇怪,为什么当时自己要有这么幼稚的想法,导致项目进行到最后我都在纠结一些毫无意义的点子上。
不过也正是因为在蜗牛睡眠的这段游戏开发的经历,让我对游戏开发有了一个完整的流程认识,真正的理解了一个游戏是怎么从无到有开发出来的,同时也因为这是一个全新的产品,也让我学习到了如何去与团队中的其它同学进行沟通,如何跟 UI、PM 等同学进行描述自己遇到的问题,同时也熟悉了如何在 App store 上架一个 app,也收到了苹果爸爸发来涉及抄袭的邮件。
在蜗牛睡眠的最后时间是在等待上架,但当时正值国内游戏行业规范阶段,版号相当难搞,最后在我去新疆旅行的飞机上收到了我被迫离职的消息。到现在想起来我都很伤心啊!三个小伙伴打磨了三个月的产品说没就没了,心情极度郁闷,这个游戏后续我有开源计划,大家可以期待。
## 第四步
经过了蜗牛睡眠的实习后,我已经大三了,对学校的厌恶更上一层楼。看到身边的同学们对未来依旧没有想法,让我更不愿意呆在学校。从新疆旅行回来休息了一个多星期时,我原本想着让自己继续休息,但在完成 [`PLook`](https://github.com/windstormeye/Peek) 第一期开发后的某个夜晚难以入眠,又开始思考起了我大学的意义。最后我打开实习僧,浏览当时在招的实习生岗位。这次我的胆子又大了,没听说过的公司一概不投,爱奇艺、美团、滴滴等大公司都投了一遍,最后看着简历都发送出去后,才迷迷糊糊的睡着了。
第二天在社团实验室中继续写着 `PLook`,没记错的话,应该是 10:00 一过,我的手机就开始一个接着一个的响,各大公司的 HR 都给我打了面试邀请电话,按照电话的拨打顺序,我把滴滴的面试排在第一。
面试当天我来到了位于数字山谷的滴滴大厦。共经历了两面,这次的面试我开始运用了一些技巧,比如把一些技术问题转移到我曾经做过的项目中来,其中 `PLook` 帮了我很大的忙!面试的细节总体来看还算正常,没有问一些稀奇古怪的问题,最让我没想到的是,面试我的两位 leader 都不是写客户端的,但是他们在面试过程中对客户端问题把握得实在是稳,虽然有现场查题的嫌疑,但这种从容不迫的老司机实在是让我又爱又恨。面试结束,我走去公交站乘车回校,刚走到公交站牌 HR 就给我打了电话,恭喜我通过了面试,什么适合可以入职。当时我激动得差点跳了起来,滴滴的环境十分优异!我二话不说立马答应下周入职。
进去滴滴的第一天,leader 找我的谈话时对我做的 `PLook` 赞善有加,十分喜欢,还问了我准备什么时候上架等一些问题。直到现在我还在滴滴,并且已经拿到了滴滴的秋招转正 offer。仔细一算,到今天为止,我在滴滴待了一年零三个月。
在这一年零三个月中,我也慢慢的发生了很大的变化,从最开始“跪舔”各大互联网公司,到现在已经看透这些互联网公司的本质,对这种日复一日的工作早已厌倦。在这一年零三个月中,我也慢慢的开始正视工作这件事情,不再抱着所谓“美好”的一面去看待这些事情,有不算讨厌的事情做,有钱拿,这就够了。
至于以后的安排,如果不出意外,我还会继续在滴滴待下去,直到我真的无法忍受国内互联网公司的各种劣性,比如 996、大小周等毫无意义的事情,不过我之所以能够在滴滴呆的这么久,有很大一部分原因是滴滴没有我目前所讨厌因素中的任何一个,就算资本寒冬,滴滴也保持着它最初“可爱”的样子,要不然,我早就走了,不会来来去去的思考了 N 多次后继续这份工作。
## 第五步
以后啊~以后的事情很难决定呢!但是现在可以确定的是,我会慢慢的迁移到大前端方向上,虽然其它的一些东西也杂七杂八的都做过,但回过头来看,其实自己最喜欢的还是跟用户打交道,iOS 还是一直会做下去。
非常遗憾的事情是,大学四年,我居然还没有发布任何一个完全属于自己的 App,在接下来的时间中,我会逐渐的走向这个过程,而且会进入更多的 Apple 平台设备,比如朝思暮想的 Apple Watch,在 Watch 上,这是一个更加容易引起开发者思考的平台,会让开发者从本质上去思考,用户到底需要的是什么。
## 总结
我的实习生涯从 2017 年 3 月到今天,过去了整整两年的时间,在这两年的时间中,真的非常感谢当初自己对自己下的目标,逼着自己提前去接触这个社会,去看看在实际的工作到底是怎么样的。
到现在,我还是那句话,如果你也想像我一样,像我一样去认真的发掘自己是否真的适合这个行业,那么越早出去实习越好。有同学会说,
================================================
FILE: Others/招一个靠谱的iOS实习生(附参考答案).md
================================================
以下是我列出来的能够帮助大家招到一个 **靠谱的iOS实习生** 需要掌握的点,再次说明下情况:
1. 此份题适用于电面和face to face,更加偏向于电面;
2. 能够较为流畅的说到每道题的点上,基本上可以认为是掌握了;
3. 考虑到电面过程中,对被电面者心理素质考验非常大,所以,我本人抵制电面过程中考算法(这是一个流氓行为)此套题不涉及任何关于算法方面知识。若有此需求,推荐找专门的在线OJ进行测评。
## 概念部分
### struct和class的区别?
* **struct中不能定义函数**(针对面向过程语言,例如C)// 可不用
* **使用大括号进行初始化** class和struct如果定义了构造函数,就不能用大括号初始化,若没有,则struct可以,class只有在所有成员变量均为public时才行。 // 可不用
* **默认访问权限** class默认成员访问权限为private;struct默认访问你权限为public。 // 重点
* **继承方式** class默认private;struct默认public。 // 重点
### 说出以下指针的含义:
```C
int **a : 指向一个指针的指针,该指针指向一个整数。
int *a[10] : 指向一个有10个指针的数组,每个指针指向一个整数。
int (*a)[10] : 指向一个有10个整数数组的指针。
int (*a)(int) : 指向一个函数的指针,该函数有一个整数参数,并返回一个整数。
```
### int 和 NSInterger 的区别:
* 以C语言举例,int和long的字节数和当前操作系统中的指针所占位数是相等的,也就是说,long的长度永远 ≥ int,并且我们需要去考虑此时是使用int还是long比较合适,会不会因为一时疏忽选择了int而导致位数不够造成溢出。
* 而在OC中使用 NSInterger ,苹果对其进行了一个宏定义的判断(cmd+鼠标左键进去看吧),这个宏定义会自动判断当前App运行的硬件环境,到底是32位机还是64位机等等,从而自动返回最大的类型,而不用我们去思考此时到底应该是用int还是long。
### 深拷贝和浅拷贝的区别:
* 浅拷贝:又称“指针拷贝”。不增加新内存,只增加一个指针指向原来的内存区域。
* 深拷贝:又称“内容拷贝”。同时拷贝指正和指针所指向的内存,新增指针指向新增内存。
* // 可对OC中的可变对象和不可变对象做拓展,此问题只是单纯的概念。
### 内存中的区域是怎么划分的?
* 用之前做的一张图进行描述:
## 语言部分
### nil、Nil、NULL和NSNULL的区别:
* **nil:** 把对象置空,置空后是一个空对象且完全从内存中释放;
* **Nil:** 用nil的地方均可用Nil替换,Nil表示置空一个类;
* **NULL:** 表示把一个指针置空。(空指针)
* **NSNULL:** 把一个OC对象置空,但想保留其容器(大小)。
### category和extension的区别:
* **category:**为已知类增加新方法。
- 新增方法被子类集成;
- 新增的方法比原有类具备更高的优先级,且不可重名,防止被覆盖;
- 不能增加成员变量。
* **extension:** 为当前类增加私有变量和私有方法,添加的方法是必须实现的。
### @Property关键词及其相关关键字的理解:
* 根据被修改的可能性,、@Property中关键字的排列推荐为:原子性、读写性、内存管理特性;
* **原子性:** automatic和nonautomatic。决定了该属性是否为原子性的,即在多线程的操作中,不能被其它线程打断的特性,一旦使用了该变量的操作不能被完整执行时,将会回到该变量操作之前的状态,但原子性即automatic因为是原语操作(保证setter/getter的原语执行),会损耗性能,在iOS开发中一般不用,而在macOS开发中随意。
* **读写性:** readOnly和readWrite。默认为readWrite,编译器会帮助生成serter/getter方法,而readOnly只会帮助生成getter方法。 // 此处可拓展,非要修改readOnly修饰的变量怎么办,可用KVC,又可继续拓展KVC相关知识。
* **内存管理特性:** assign、weak、strong、unsafe_unretained。
- assign:一般用于值类型,比如int、BOOL等(还可用于修饰OC对象);
- weak:用于修饰引用类型(弱引用,只能修饰OC对象);
- strong:用于修饰引用类型(强引用);
- unsafe_unretained:只用于修饰引用类型(弱引用),与weak的区别在于,被unsafe_unretained修饰的对象被销毁后,其指针并不会被自动置空,此时指向了一个野地址。
### OC中如何定义一个枚举?
* 在OC中定义一个枚举有三种做法:
- 因为OC是兼容C的,所以可以使用C语言风格的enum进行定义。
- 使用`NS_ENUM`宏进行定义;
- 使用`NS_OPTIONS`宏进行定义;
* `NS_ENUM`为定义通用性枚举,只能单选,`NS_OPTIONS`为定义位移枚举,可多选。 // 枚举为啥要这么分?因为涉及到是否使用C++模式进行编译有关。
### Block和函数的关系(对Block的理解)?
* Block与函数指针非常类似,但Block能够访问函数以外、词法作用域以外的外部变量的值;
* Block不仅实现了函数的功能,还携带了函数的执行环境;
* Block实际上是指向结构体的指针;(可参考[这篇文章](https://www.cnblogs.com/yoon/p/4953618.html))
* Block会把进入其内部的基本数据类型变量当做常量处理。】
* Block执行的是一个回调,并不知道其中的对象合适被释放,所以为了防止在使用对象之前就被释放掉了,会自动给其内部所使用的对象进行retain一次。
* Block使用`copy`修饰符进行修饰,且不能使用`retain`进行修饰,因为`retain`只是进行了一次回调,但block的内存还是放在了栈空间中,在栈上的变量随时会被系统回收,且Block在创建的时候内存默认就已经分配在栈空间中,其本身的作用域限于其创建时,一旦在超出其创建时的作用域之外使用,则会导致程序的崩溃,故使用`copy`修饰,使其拷贝到堆空间中,block有时还会用到一些本地变量,只有将其copy到堆空间中,才能使用这些变量。
### deletegate需要weak修饰的原因?
* 以图说明,图中所表示的是VC对tableView的持有,如果此时的tableView.deletegate对VC也是强引用,会导致循环引用,同时也给了我们敲了警钟,当出现两个对象都是强引用时,万分小心!
### 解释一下这段代码的输出:
* 简写:
```objc
Computer : NSObject
Mac : Computer
@implementation Mac
NSLog(@"%@", [self class]);
NSLog(@"%@", [super class])
@end
```
* 二者都会输出Mac。
- [self class]:当使用self调用方法时,从当前类方法列表中找,若没有则再去父类中找。调用[self class]时,会转化成`objc_msgSend`函数,其定义为`id objc_msgSend(id self, SEL op, ...)`,第一个参数是Mac实例,但其并无`-(Class)class`方法,此时去父类Computer中寻找,发现也没有,再去其父类`NSObject`中找,找到了!返回的就是`self`其本身,可猜测其方法实现如下:
```objc
- (Class)class {
return object_getClass(self);
}
```
- [super class]:从父类方法列表中开始找,调用父类方法。当调用[super class]时,转换成`objc_msgSendSuper`函数,其定义为`id objc_msgSendSuper(struct objc_super *super, SEL op, ...)`,而`struct objc_super`结构体的定义为:
```objc
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
}
```
所以转换成`objc_msgSendSuper`函数后,第一步要先去构造`objc_super`结构体,结构的第一个成员`receiver`就是`self`,第二个成员是`(id)class_getSuperclass(object_getClass("Mac"))`,该函数输出的结果为super_class值,即`Computer`,第二步,则去`Computer`类中去找`- (Class)class`,发现并未找到,接着去NSObject中找,找到了!最后是使用了`objc_msgSend(objc_super->receiver, @selector(class))`去调用了,这个时候已经跟之前的[self class]调用输出结果重复了,返回结果还是`Mac`。
## iOS部分
### UITableView性能调优的方法:
* Cell重用:
- 数据源方法优化:创建一个静态变量重用ID,例如:`static NSString *cellID = @"cellID";`防止因为调用次数过多,static保证只创建一次,提高性能(感觉性能的提升可以忽略不记emmm)
- 缓存池获取可重用Cell的两个方法:`dequeueReusableCellWithIdentifier:(NSString *)ID`会查询可重用Cell,若注册了原型Cell则能够查询到,否则为nil,故需要先判断`if(cell == nil)`
- `dequeueReusableCellWithIdentifier:(NSString *)ID indexPath:(NSIndexPath *)indexPath`,使用之前必须通过SB/class进行可重用Cell的注册(registerNib/registerClass),不需要判断nil,一定会返回cell,若缓冲区Cell不存在,会使用原型Cell重新实例化一个新Cell。
* 尽量使用一种类型的Cell:能够减少代码量,减小Nib文件的数量;保证只有一种类型的Cell,实际上App运后只有N个Cell,但若有M种Cell,则实际上运行最多却可能会是MxN 个。
* 善用hidden隐藏subview:把所有不同类型的view都定义好,通过cell的枚举类型变量及hidden显示/隐藏不同类型的内容,因为在实际快速滑动中,单纯的显示/隐藏subview比实时创建快得多。
* 提前计算并缓存Cell的高度。如果我们不预估行高,则优先调用`heightForRowAtIndexPath`获取每个Cell即将显示的高度,实际上就是要确定总的tableView.contenSize,最后才又接着调用`cellForRowAtIndexPath`,可以建一个frame模型,保存下提前计算好的cell高度。
* 异步绘制:这是目前最火的tableView性能调优方法,新浪微博是这么做的,可以使用`ASDK`这个库进行。
* tableView滑动时,按需加载:识别tableView静止或减速滑动结束后,异步加载,在快速滑动过程中,只按需加载目标方位内的Cell。
* 避免大量使用图片缩放、颜色渐变、透明图层、CALayer特效(阴影)等操作,尽量显示大小刚好合适的图片资源。
### 内存优化方案:
* 首先ARC。但要注意防止循环引用,避免内存泄露;
* 懒加载。延迟创建对象,用时再创建;
* 复用。比如tableView、collectionView单元格的复用;
* 巧妙使用单例,而不是全都使用单例!
### 单例的写法?
```objc
static User *user;
+ (User *)shareInstance {
if (user == nil) {
@synchronized(self) {
// 加锁
user = [User alloc] init];
}
}
return user;
}
+ (User *)shareInstance {
static dispatch_onec_t onecToken;
dispatch_onece(&onceToken, ^{
user = [User alloc] init;
})
return user;
}
```
### iOS的远程推送过程?
* 以图讲解:
### iOS中多线程的概念:(单问概念)
* 多线程优点:提高程序执行效率。缺点:开启线程需要一定的内存控件。
* 同步和异步:决定了要不要开启新的线程。同步:在当前线程中执行任务,不具备开启新线程能力;异步:在新线程中执行任务,具备开启新线程的能力。
* 并行和串行:决定了任务的执行方式。并行:多个任务并发(同时)执行,类似迅雷多任务同时下载;串行:一个任务执行完毕后,再执行下一个任务,类似一个一个下载。
* **重点:** 必须要明确iOS中只有一个主线程——UI线程,且不可将耗时任务放在主线程执行,否则会造成卡顿。
总结:再次说明,实际电面过程中,本套题只可作为参考,而且在电面过程中会有追问和等待的过程,所以最佳电面时间为三十分钟内最佳,嫌短?记好了!这招的是实习生,而且这还是电面!!!别学啥大公司的戾气,一个实习生的电面你要搞一个小时甚至快两个小时,没有必要。如果是face to face随你问一两个小时,但重点是这是电面!不要把这种“隔空喊话”的面试方式作为考核一个人是否具备对应的工作能力的最终标准,除非能够确定以后的工作方式就是“隔空喊话”和“隔空撸码”。
也不推荐把本套题中的内容作为一面,一面应该是作为了解应聘者的职业状况,基础水平(也是就是逻辑能力),推荐本套题作为二面,涵盖基本的iOS开发基础知识,而且时间能够较好的把握在三十分钟内,三面应该侧重项目实际情况,而且最好face to face。四面就HR面了吧,尽量不要有五面了,太难受了。其实三面是最佳的。
记住!不要套题,而应该是触类旁通,引出其它问题。
================================================
FILE: Others/简介.md
================================================
# 前言
## 关于自己
笔者为[北京信息科技大学网络实践创新联盟](http://iflab.org)(以下简称ifLab)iOS副组长,目前为我校计算机学院软件工程系大二学生。
这是笔者的[GitHub](https://github.com/windstormeye),GitHub上有笔者从大一到现在做的一些好玩的东西,比如课堂练习、暑假时参加的移动应用比赛成果,以及部分之前没写完的iOS开发系列教程等等。
这是笔者的[个人网站](http://www.pjhubs.com),网站上的东西比较杂,有笔者个人的一些愤青文字、对生活未来的想法、一些读后感等等的东西,当然最多的还是关于计算机方面的东西,比如老师在课上布置的一些课后练习、学校开的一些项目训练课甚至是自己做的好玩的东西都写成了教程,如果大家感兴趣的话,可以参照笔者写好的这些教程实现一遍。当然,如果大家能在笔者的基础上发挥出自己的创意,那是最好,如果做出了什么好玩的东西记得告诉我哦!(后文附联系方式)
## 关于iOS开发
iOS开发这条路是大一开学那会儿开始的,笔者上大学之前一点都没接触过编程之类的东西,不过立志学习计算机这条路早就已经定下来了,在大学之前也经常用每月的生活费购买计算机相关的书籍。
笔者这个人呢,爱耍小聪明,从小到大历届班主任都这么跟笔者爸妈说的。其实我一直都不怎么在意这个看上去很像是褒义词的贬义词,直到来了大学,笔者深为拥有这种与生俱来的天赋感到自豪,可以说,它让我对计算机的热爱,对移动互联网的热爱,对iOS开发的热爱一直持续到了现在。
当初我在学习iOS开发相关内容时,有幸加入了ifLab这个大家庭。ifLab给了笔者无比多的关顾和指导。在学习iOS开发的过程中,我经历了从模拟器到iPod touch 6再到iPhone 7的过程,算是比较理解使用各个调试设备的优劣了吧(关于这方面的内容我们将在后面会讲到)。如果大家跟我一样也是一个不想太过于依赖家里经济支持的话,可以采取笔者这种做法,一步一步来,先用自带的模拟器完成前期开发基础知识的铺垫,然后再购买入门级iOS设备进行更深入的学习,最后等到需要调用高级功能,比如电话,短信等这些功能后再考虑买一部iPhone。
无论是任何一门学科或者技能的学习,我们都要坚持本心,千万不能轻言放弃。笔者身边就有非常多的同学忘记了初心,甚至很多同学已经彻底堕落在了滚滚游戏中。我承认,我们身边有很多诱惑,但是我们如果能够从这些诱惑中继续坚持我们当初来到大学,或者开始学习之前定下的目标,并且为之坚持,你就会发现,你会越来越喜欢坚持初心,努力奋斗的自己。
也许有这么一种声音,“移动互联网已经快要过时了,下一个风口将是AI!”,我承认,无论任何东西,都存在过时,毕竟时间在流逝,我们也在老去。也许有人又会说,“那你既然知道,为什么还要搞iOS开发呢?”,笔者想说,任何东西我们都不能只看表面,我们要挖掘出它的本质,做iOS开发,甚至是Android开发等等其它开发,我们都是在学习啊,学习的本质不就是养成一种学习的方法么?养成在学习的过程中遇到的问题,解决问题的思想,不是么?难道你会否认十二年的教育对你没有一点点的影响么?
这很显然是不对的。如果你不是计算机相关专业的同学,有幸看到了这篇教程,你大可把它当做你进入计算机领域的一个敲门砖,通过它来消除你对计算机因为陌生而产生的恐惧心理。笔者在这里真诚的告诉大家一句话,
#### “优秀的人遵守规则,顶尖的人创造规则”
我们做开发,就是在别人定好的规则下发挥我们自己的创意做二次开发,相当于,你媳妇儿给你生了一个白胖小子,他刚生下来什么都不知道,只不过带有一些特有的天赋(类比,iOS有苹果这个大老虎撑腰,Android有Google撑腰),这些特有的天赋会在未来成长的道路上发挥出一些特定的优势(类比,WWDC,Google I/O)。在它成长起来的过程中,需要我们不停的对它做教育,告诉它应该怎么做人(开发时,按照产品的需求写代码)。直到它成人了,能够自己出去闯荡了(上架),看它能闯出多大的天地。
很多时候,我们在编写一款APP时,很像在教育自己的孩子,到底应该怎么做才能使它成为所谓的“金榜题名”呢?如果我们把每一款开发的APP都当做自己的孩子一样对待,那么它也会把你当做爸爸一样对待,在未来某个时刻给你一份大大的惊喜!
## 关于本教程
1、如果你有过Android或者Windows Phone开发经验,你会在阅读本教程时发现很多Android和Windows Phone开发相通的地方,如果你是一名移动应用开发的新手,那么你可以把本教程作为一个学习iOS开发的参考,技多不压身,多看几门教程有助于更好的理解。
2、本教程与我校[在线教育平台](http://x.bistu.edu.cn)下的iOS基础教程(笔者负责的那块)和[个人网站](http://www.pjhubs.com)同步更新,内容适当删减。
3、本教程从开始到结束时间跨度可能较长,并且因为只有笔者一个人在编写,不保证每节内容准确无误,希望各位同学在参考本教程时如果发现与你的认知出现了差错的地方请及时告知!
## 联系方式
QQ:877302410
微信:18889737779
================================================
FILE: Others/面试准备.md
================================================
# 面试准备
这篇文章是结合了《iOS 面试之道》的第一章“iOS 工程师的面试”部分以及自己的所掌握的知识内容所得,主要用于提醒自己和帮助其它同学。
## 简历的准备
### 页数
简历通常最好是两页,一页的简历会显得过于简洁,正常来说不管一个人在某个行业做得好与坏,都能够把自己的所掌握的东西转换到两张 A4 纸上。
### 精简简历
* **删除不必要的自我介绍**:不要写上任何与课程有关的东西,操作系统、计算机网络、数据结构等等这些计算机专业核心课程或知识,是我们必须要掌握的,没有必要突出你上过这些课,这些课只要是国内高校相关专业都会上。这就好像应聘一个环卫工人,你要跟雇主说我会用芭蕉叶扫把、竹子叶扫把以及会使用大剪刀修建枝芽,这些东西都是一名合格环卫工人必须要掌握的,完全没必要突出。
* **删除不必要的工作或实习、实践经历**:这点我深有感触,我的第一份实习实在一家外包公司,干了两个月,在这两个月中,我没有做多么深入的性能调优,也不允许我去做(两个月的时间上了两个 app),这段时间给我的最大感触就是我对 iOS 的相关需求有了非常大的熟练度,也就是说,我在这两个月的时间中存粹就是去练代码熟练度的。所以,这段实习经历完全没必要摆出来。
而且要注意分类!如果我们找的是一家 **社会企业**,社会企业可能会要求我们有“风险精神”,这就可以把我们的各种志愿者经历放到简历中;如果我们要找的是一家 **互联网企业**,志愿者经历等等的类似的信息就不要放上去了。
* **删除不必要的证书**:我觉得四六级都没必要放上去,计算机是变化最快的行业之一,没人保证你拿了什么证就能够做什么事情,也没人能保证没有这个证你就不会做什么事情。如果我们自己都觉得这个证书没什么技术含量,那就不要放上去。
* **删除不必要的细节**:简历上写自己做过的项目时,不要连这个项目用到的环境都上去,比如,xxx项目的环境为 macOS ,设备为 Macbook pro 等等。
### 重要的信息写在简历的最前面
最开始的时候我的简历十分尴尬,我居然是按照时间顺序排序的,隔了大半年的外包实习经历我居然放在了最前面,这就导致了每次面试官都从第一个项目开始问起,而我自己已经记不起太多细节了(也没多少细节可说),所以最后我决定在简历中删掉了这段实习经历。
所以,最好的做法是先把重要的信息放在简历的相对靠前的位置。比如个人的一些基本信息,写清楚自己的名字、邮箱、院校、博客地址、GitHub 等等即可,接下来就要重点“渲染”自己最厉害的经历放出来,可以是名气较大的公示实习,也可以是比赛奖项等。
最好不要去网上下模版,最佳的做法是直接 markdown 搞起,让面试官专注你的内容而不是那些酷炫的排版。
### 不要简单的罗列工作经历
不要在简历上写太多关于某个项目的事情,也不要一笔带过。个人推荐最好的做法是分版本描述,在每个版本的描述中概括性的描述出自己做了哪些事情即可,如果面试官对你做的这个项目中的某个版本中的某个功能点感兴趣,他是会问我们的。这有个例子:
* v1.5.0 :数据缓存、网络监控
* v1.6.0 :弱网体验提升、数据异常中间件
* ......
需要注意的是,必须要保证只要我们写在简历上的东西就是自己做过、摸过的,要不然如果只是为了充数好看,被问到而答不上来就十分尴尬了。
### 不要写任何虚假或夸大的信息
千万千万不要出现“精通”二字,甚至也不要出现“熟练”,摸着自己的良心说话,你真的精通,真的熟悉了吗?我始终不认为少于 10000 个小时或者少于 10 万行代码量的锻炼就能够称得上精通 or 熟悉。但是如果你对自己非常有自信,确实在某些方面有自己的心得,那可以这么说:
```
主攻 iOS 开发,正在学习以 python 为主的后端开发技术栈。
```
短短的一句话涵盖了非常多的东西,“主攻”二字没有自狂但很实在。但如果你的情况是真的没什么项目可写,自己参加的又是一些“边角料”的工作,可以把面试官引导到自己比较稳妥的方向上来,比如说可以让面试官问你算法和数据结构。
### 留下更多的信息
我推荐大家留下:
* github
* 掘金/简书或者自己的 blog
* 邮箱
* 社交账号,如微博等
需要注意的是,要确保我们留下的这些账号是真实有效的,其中的信息都是可考证的。比如说,如果你留下了 github 地址,我是一定会去看的,而且会去思考哪部分代码是你写的,写的怎么样,甚至会去观察你的 contributes, 还会去看你代码中的命名等等,如果发现了你只是因为两个月或者一个月要找工作而开始“疯狂”的 commit ,那么你还不如不放,我知道大部分同学的 github 上没有多少优秀的 repo (我也一样),但是一旦你放到 github 上了,并且还把你的 github 地址放在简历上了,这就表明了你是有内容的,想给面试官去查看。
### 不要附加任何带来负面影响的消息
1. 不要放个人照片。(我曾经犯过这个错,现在想起来真是太傻了)这是你的简历!不是你的相亲!工作就是创造价值,你的性格如何,人品如何,会有专门的面试去考量,只要你能保质保量的完成任务,单臂编程也没问题。
2. 如果你没有出色爱好证明,比如国家二级运动员等等荣誉,那请也不要把你的个人爱好放到简历上来。等你入职了,会有专门的自我介绍环节,这个时候请你多多表现,不要哑口无言。
3. 如果要求电子简历,请转成 PDF!
4. 如果我们要留邮箱,最好留一个能够及时收到消息提醒的邮箱,唐巧在书中说的是不要留 QQ 邮箱,但是我明确知道在国内 QQ 邮箱的体验还算不错,很多时候我之所以留 QQ 邮箱是因为我绑定了 QQ 和微信的收件提醒,而 QQ 和微信是我几乎不会关闭的 app 。
5. 不要写上任何职业培训信息。我一点都不反对培训出来的程序员同学,因为他们至少证明了一点,他们想做这个事情,然后有所付出。至于培训出来的程序员质量如何,这里不做考究。从我自身来说,我是 mooc 的常客,coursera 和 edX 是我经常活跃的地方。但是你没必要把参加这些课程或者培训写到简历上,因为参加这些课程或者培训是我们应该做的,这是提升我们自己的手段,换种方式,如果连这种事情都要放在简历上,是不是也要把自己看过什么书也放到简历上呢?
## 寻找机会
网上流传这么一句话,大部分的工作都不是面试来的。
### 寻找内推机会
1. 找师兄师姐(也可以找我 😃)。
2. 微博、知乎等社交平台。
3. github。这是一个极好的平台,曾经我开源了[ PLook ](https://github.com/windstormeye/Peek),两天后就有人找到了我。
你要相信,只要是个人才,谁都喜欢,更何况还有丰厚的内推奖金等着呢。
### 其它常见的招聘渠道
实习僧(找实习非常靠谱)、拉钩、100 offer 、Boss 直聘(不是很推荐)、智联招聘等
## 面试流程
### 自我介绍
在这个环节中我们千万要把握好时间和要阐述的内容,时间把握在 3 ~ 5 分钟。这个环节我跟唐巧老师的看法是一致的,非常重要。我记得去年的暑期实习面试中,我临时采取了一种方法,就是按照简历上的内容一个一个介绍,但是这个介绍不是细致无比的介绍,而是笼统的大概介绍自己都做了什么,用到了哪些技术,这个项目是服务于谁,用于做什么的。
我反而不推荐大家去背自我介绍,只要我们的简历写得足够清晰明了,错落有致,那我们就先在自我介绍环节引导面试官去看我们的简历,让面试官把注意力放到我们的简历上,而不是让他把注意力集中到自己之前准备好的问题上(虽然也有一定几率转移注意力失败)。在这个环节中,其实我觉得自我介绍根本没啥用,与我相关的内容早都已经写在简历上了,让我做个自我介绍不就代表着简历你都没看就来面试我?当然,我觉得面试官们让我们做的应该不是自我介绍,应该是给我们一个梳理自己思路,让自己进入面试状态的阶段。
需要注意的是,不要在这个环节表现得自己无话可说,磕磕碰碰,支支吾吾。适当停顿是好的,但是不要停顿了很久,然后才接着说,如果真的在面试过程中出现了这个情况,我们需要用一些“自我调侃”的语气来匀一下,比如:“之前的做这个的时候,我想着是学习一个产品怎么从 0 到 1 ,但是没想到最后 xxxxx”,这部分因为是我们缓解自我介绍过程中断片用的,不要一直都在“调侃”,适当抓住时机回到正轨上。
如果我是面试官的话,我会非常喜欢听面试者谈论他当初为什么要做/加入这个产品/项目。用唐巧老师的话来说:“优秀的应聘者会通过自我介绍“引导”面试官问到自己擅长的领域知识”,用我自己的话来说,就是忽悠面试官到我们自己擅长的领域或者项目中来,最后别让面试官上来就问,“会手写快排么?来一个”,说真的,如果你没啥说的,我是会问的,但是如果你已经做过很多东西了,我要问的会往架构和设计方面的更多一些,因为之所以问我们算法和数据结构,是因为这是面对一个与自己毫不相干的人时最直接的方法,因为这更能省事,如果你已经做了很多东西了,并且也能够给我看到,那么我当然会认为你已经会写代码了。
### 项目介绍
这里有一个我自己认为是禁忌的地方,当面试官问到你 xxx 项目时,千万不要表现出这个项目简单啊、没什么说的等等,因为这会让面试官觉得,既然你自己都觉得没什么可说的,那你为什么要写在简历上呢?并且当我们确定要把一个项目放在简历上时,千万要记得这就是你身上的一块肉,如果这块肉是你身上的,点一下都会有感觉,更不要说这块肉被割了一下,所以我的意思是,这个项目是你做的,那就要事无巨细的从零到一的所有细节你都要知道,因为比如说我,我就会抓住其中一个点问下去,反正也是你做/参与其中的,这其中肯定有你构思了很久的地方,所以我是一定会问下去的。
需要注意的地方有:
* 这个工作具体的产品需求是什么样的?
* 大概做了多长的时间?
* 整体的软件架构师什么样的?(划重点)
* 涉及哪些人合作?几个开发几个测试?(这问题我不会问,只要是个项目肯定不只一个人,更别说在公司中了)
* 项目的时间排期是怎么定的?
* 你主要负责的部分有哪些?(划重点)
* 项目进行中有没有遇到什么问题?(划重点,会被面试官问:“那你有没有想过其它方案?”,然后就开始了......)
* 这个项目最大的收获是什么?遗憾是什么?
* 项目中最难实现的一个需求是什么?具体是怎么实现的?
### 写代码
这个环节是一场面试中重中之重的环节,如果这个环节表现得好了,可以覆盖掉之前所有的表现,因为招一个程序员进来,最终的目的就是写代码。当然考察代码写得如何有非常多的方法,其中最基本的就是从数据结构、算法一直到系统架构,我个人把这个环节分成了两部分——电面和面试。
电面中千万不要让人给你做题,我自己就被这么玩过,简直痛不欲生,一是信号不号,无法想象在跟你通电话的面试官是在一个什么样的环境下跟你沟通,更何况有些走的还是 IP 电话,不想骂人;二是讲不明白,这点可以理解为双方都不知道对方在说什么,如果“隔空能力”表达不好,需要借助纸笔来辅助回答,那就别问让面试者需要纸笔辅助回答的问题,这样会让双方都很尴尬;三是沟通费劲,这点不用多说。之前我在给团队面试 iOS 实习生时出了一份电面题,基本上都是基础知识,答对这份电面题只能说明能够具备面试的要求,不是说就具备了入职的要求,千万别搞错了,[链接在此](https://github.com/windstormeye/iOS-Course/blob/master/Others/招一个靠谱的iOS实习生(附参考答案).md)
面试过程中因人而异,需要顾及的问题很多,不可随便网上找份题直接肝,这样体现不出面试者的差异性。个人推荐做法还是就事论事,简历上写了哪些东西就问哪些东西,然后结合团队本身产品及业务进行拓展,别上来就让人“手写快排”、“翻转二叉树”......这些东西需要时间去准备,更何况真正的大佬根本不会去准备面试的,因为他没时间。但是这些知识不可能不问,因为工作中真的会用到,那怎么办?给个业务例子或造一个场景,让面试者自己去发散思维,说不定面试者还能给我们长长见识呢,如果我们出的业务例子或场景只能用某个数据结构去完成,那符合要求的面试者是一定会想到的,他也应该知道怎么去调优,去实现,反而是根本想不出来或者就是错误方向的面试者才值得我们斟酌,如果出现了这种情况,十有八九是个幌子。
说到底,我还是推荐如果面试官们真的要考面试者的数据结构和算法能力(纯粹),直接让它现场上机撸码吧。
### 系统设计
这部分内容不一定会单独拎出来,可能会跟写代码环节有重叠,但现在问的最多的基本是这个套路:
“有看过 xxx 库么?”
如果你看过,“能说说看这其中的设计么?”
如果你没看过,“如果要你设计,你要怎么做呢?”
这是个大坑!如果你真的认认真真的研究过了,钻研过了,那你大可放心的说,“我看过!”,可问题是不是每个人都有时间去钻,我唯一重头到尾每一行代码都认认真真的看完是这个库:[DOFavoriteButton](https://github.com/okmr-d/DOFavoriteButton),因为我觉得他写的很烂,如果我们给的是一个镂空图,它依旧会在点击状态完后给你填充,当时把我气得不行,怎么会有这么 zz 的库,把代码全部翻完后,加了个属性(当然,可能现在已经改过来了)。
由此可见,如果用到的库完全能够满足自己的需求,我还真的不一定会有时间去看源码,除非用到的库质量不高或者设计上有问题,但是其中有些地方导致了没法舍弃,只能去看源码改代码。所以,就算看过 xxx 库的源码,我也会说没看过,如果你说看过了,有些神奇的面试官还真的会问你一些犄角旮旯的问题,但如果我们说了没看过,然后自己设计一套出来,就算某些地方说的不好,也会有加分的地方。
### 提问
这个环节可以认为是我们全场面试中唯一一个主动出击的机会了,如果整场面试下来觉得这家公司不错,那么我会问如果我进来的话,主要负责的产品是什么?业务是什么样的?谁带我?(这个问题慎重)现在团队有多少人在负责这个产品?千万不要问:
* 我刚才的面试情况如何?
* 刚才 xxx 面试题的答案应该是什么?
* 大概能有多少钱?
## 其它
### 笔试、代码考察准备
首先要明确一点,如果我们非常喜欢、期待进入某家公司,一定一定要记得用尽自己的一切手段去获取这家公司的所有细节,包括面试官都会问哪些问题、面试题/笔试题大概都有哪些、多看一些面经等等,并且如果我们真的确定了自己非这家公司不去,那就一定要记得去刷题!我十分推荐去刷 leetcode ,前段时间本想着每天一道题持续到明年春招,但是没想到自己可以转正,而且转正三面全过了,就把重心偏移到其它方面去了。
关于系统设计的准备,我个人推荐大家在平常撸码的过程中多跟后台沟通沟通,或者在业余时间做一些自己的全栈 side-project ,这样有助于从大局考虑系统设计,当然,最重要的还是对日常需求的思考,还是那句话“再简单的事情也能过做到完美!”
### Offer 的比较和选择
这部分内容我跟巧哥的思路不太一样,也有可能是因为我个人经历的原因,我始终认为时间是最宝贵的,我每天都必须给自己留下一定量的时间放空自己。在这段放空的时间内,玩游戏是非常非常小的一部分,甚至可以忽略不计,大部分的时间我会选择去看书,各种书都看,可以是技术书籍也可以是人文书籍,不要小看用这些时间去阅读取得的效果,看书真的能够使人进步和加深思考,初中的时候因为还是走读生,每天回到家都有时间给自己一本一本的去看书,导致一次作文满分,好几次只扣了三四分,甚至一两分。
当然,我说这些原因主要还是想突出一点,如果我们现阶段不是特别的缺钱,当然,钱多少都不嫌多,我的意思是说,在保证自己各项开支都均衡的情况下,尽量选择能够给自己留下空余时间的 offer ,如果真的能够可以少拿钱,然后提前下班(强制性赶人),我真的愿意这么做!
至于多出来的时间要怎么消费,这就看个人的需求了,如果是我的话,我会先学做饭,把小时候妈妈逼我学的几道菜再精进一番。如果还有更多的时间,我会用自己所学的知识做一些传承的事情,比如做一些游戏化编程的事情等等,每天都有多余的时间真的很重要,因为这至少能够保证我们的热情不减,还是当初那个一腔热血的少年。
这有一张当时写下的转正面经图片:

================================================
FILE: Product/Map.md
================================================
## 地图 SDK
几乎所有的地图 SDK 都是基于 OpenGL 自行绘制。其中提供给 Qt 的插件基本也是通过提供 JS API 间接 webView 渲染完成,剩余的 Android、iOS 下是基于同一套渲染接口的 OpenGL ES 版本的原生实现。
奇怪的是通过查看高德地图 API 文档,发现只有 JS API 提供了行政区域图层填充的能力,可以解释为什么其他地图产品如“灵敢足迹”切换省市类型下有割裂感。
================================================
FILE: Project/Bonfire.md
================================================
这篇文章主要记录我在开发Bonfire的过程中遇到的问题。
1. [CAShapeLayer的相关属性介绍](https://www.jianshu.com/p/98ff8012362a)
2. 出现`dyld: Library not loaded: @rpath/libswiftAVFoundation.dylib`[解决方案](https://www.jianshu.com/p/46c3d65a996b)
3. `NSInvocation`使用。[参考文章](https://my.oschina.net/u/2340880/blog/398552),当`SEL`需要传递多个参数时可以采用`NSInvocation`或者`block`
4. [基于responderChain的传值方式](https://casatwy.com/responder_chain_communication.html)
5. 屏蔽相机声音,最好是不要屏蔽,直接提示告诉用户开启静默模式之前应该打开静音,如果非要在关闭静音的模式下进行静默拍照,则:**使用和iOS系统拍照声音相反音波的音乐,在will方法中播放该音乐**。网上有例子。
6. `mapView.userTrackingMode`状态改变
```swift
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
// 当userTrackingMode发生改变时再把它改回去
}
```
================================================
FILE: Project/CocosCreator——方块弹球.md
================================================
## 简介
这是我的游戏开发系列第一讲,我会在拥有下一个大串空闲的时间中持续开展此系列内容。这其中可能会涉及Cocos、Unity、白鹭、虚幻、LayaBox等游戏引擎。
先说明一点,方块弹珠这个名字我是直接拿微信小游戏上一块名为《方块弹珠》的微信小游戏的名字,去年暑假在某公司用了Cocos2D-X做了同类型两个版本的游戏,这次做的方块弹珠是第一个版本的精简版。在开始做这个系列之前,因为不再想用Cocos2D-X做任何东西了(当初第一次碰游戏开发太费劲了),有考虑过到底是用Cocos还是Unity,其实当初很大的动摇是来自于Cocos优秀的跨平台特性,而且App Store上前十的排行榜也告诉了我Cocos是多么“独秀”,不是说Unity不好啊,而是说Unity有更适合它的地方(等着我的AR开发系列😝)。
所以在“方块弹珠”这个游戏中我并未花费太多的时间去扣细节,花了两天半的时间完成了它。之前用Cocos2D-X中所踩下的坑,在使用Cocos Creator的过程中一样会遇到,不过Cocos Creator(以下简称:cc)却提供了一些“骚操作”直接告诉你应该如何绕过或者解决它。在使用cc的这段几天中,给了我一个非常幸福并且直观的感受——**做游戏就应该有个好的IDE**。😂
当然cc就目前市面上的游戏引擎来看,确实是做到了“世界第二,国内第一”的名号,但这也不保证了它是一点缺点没有。首先,我要吐槽的是API文档写的真的一点都不全,或者你也可以认为对萌新一点都不友好。大量的问题直接搜API文档都没结果,找到的只能是像“蜻蜓点水”般一语带过,或者上下文没法衔接上,不过这还是比去年暑假时的那版好多了,至少简练了很多。其次,cc这界面做的跟简直Unity一毛一样,曾一度怀疑是不是cc沾了Unity的光而已😓。最后,在使用cc的过程当中某些时候会出现一些迷之bug,不过在1.9.1版本的cc对这些bug处理的已经比较好了,不过还是有漏网之鱼,比如在当前Scene下添加新的Canvas后,原先的Canvas上的布局会有一定几率乱了。
项目体验:[http://api.pjhubs.com/bounceGame/](http://api.pjhubs.com/bounceGame/)
## 构建
因为cc现在的主体就是js全家桶,反正现在js啥都能做了,个人觉得跟py是平分秋色了。正是因为cc是js全家桶,也就导致了其能利用Native的runtime特性输出比肩Native的性能。这其中涉及到很多细节,包括如何拖拽sprite、修改相关属性等等,如果还用之前文章的写法估计会被累死,因此中会划分功能模块进行讲解。
### 第一步——初识套路
如果你之前没有做过任何与游戏相关的内容,没有使用过IDE进行开发工作,那么推荐先**仔仔细细**研究一番这篇关于[制作第一个Hello World游戏](http://docs.cocos.com/creator/manual/zh/getting-started/quick-start.html)的官方教程。
### 第二步——拆分需求
先给大家放几张图,看看我是如何进行需求拆分的。
其实可以从上边的图中找出一点共性:
1. 都有指引小球发射的轨迹且发射小球;
2. 方块或星球能够对小球的撞击做出反应;
3. 小球能够累计;
4. 方块能够随着等级的上升增加难度。
以上是我抽出来的共性,当然还有一些其它的小的共性就不说了。这其中最大的共性是小球能够和周围的物体,包括方块、地面、边框等物体发生物理效果。这个物理效果才是我们去用这些诸如Cocos、Unity、LayaBox等所谓的物理引擎去完成,物理引擎就是用来处理如何在2D平面或者3D空间中模拟真实物理环境中的物理效果的。
### 功能模块搭建
1. 搭建游戏主体框架,我们需要一个“框”充当围墙框住整个游戏界面,下图中箭头所指示的三条线都是`Sprite`,而且是加了碰撞体和刚体的精灵,记得在给`Sprite`添加碰撞体和刚体后开启**接触接听器**,这样才能够在对应的回调方法中接收到碰撞双方。而被红框框住的部分,也是个`Sprite`,需要的操作同围墙一致。
2. 通过第一步的搭建过程和上文所展示的三张图,相信大家也都看出来了方块和小球是持续不断的在增加并且我们无法预知玩家最后会进行到哪一步,所以我们是不能直接像添加围墙和地面一样直接进行拖拽,而是要动态的生成,而进行动态生成,我们可以直接就new一个sprite,然后再设置相关属性,还可以把这些重复操作封装成一个新的类进行使用,但是我们都用上cc了,肯定是不能这么玩的,cc提供了`prefab`(预制资源)这么个东西,其实说白了它相当于一个“静态”类,这个“静态”类只需要在cc中通过图形界面去设置相关属性即可,不用自己手撸代码去封装一个类(官方文档上有讲怎么做)。我们需要把方块、小球都做成预制资源。
3. 有了方块和小球的预制资源后,我们还需要诸如分数Label、玩法提示Label、小球数量Label等等“独立”的静态资源,这些资源可以直接进行拖拽显示,因为在整个场景的生命周期中,它们只需要出现一次就好了。
4. 在发射之前小球得知道自己的发射方向,而这个发射方向是通过指针的指向去确定的,通过指针的指向去判断小球的发射方向需要用到三角函数的知识,涉及到了弧度角度转化等的问题,以下是小球根据指针方向发射的代码,
5. 小球撞击到方块上时,方块颜色会随着方块本身的数值减小而变化,这个变化我们是不可能去通过rgb这三个通道直接去计算的,因为这得涉及到三个值的规律变化,我们得通过其他色值变化的方法去进行,而且还是只修改一个值就可引起颜色的变化,那其实指的就是HSV模型了。在HSV颜色模型中,H代表的是色相/色调,S代表饱和度,V代表亮度。H的取值是角度即0~360°,红色是0,240是蓝,其余的颜色可以根据角度自行计算啦~[丢个链接](https://baike.baidu.com/item/HSV/547122)。每当生成新方块时均调用一次根据当前生成方块的数值计算而出的HSV模型值,再转成RGB赋值回去就行啦~
6. 这其中还涉及到了很多比如节点之间互相持有的问题,因为自己从cocos2d-x转换到cc上,从一个面向页面的开发到面向数据流的开发其实很难转换过来,在开发过程中老是按照老一套的MVC思想去做。
哎呀。千言万语难以说明白,本想要通过三篇文章的量去讲明白这个小游戏开发的过程,但时间上不太允许我写的太多了,先在此跟大家说声抱歉,不能够很好的讲解。当然这个项目只完成了1.0,这其中有一个隐藏bug,小球在一定数量的积累下,标识球会消失了,而且方块按照我之前的想法应该是当小球对其产生碰撞之后,方块的颜色应该要要被递减更新颜色。
### 项目地址
[https://github.com/windstormeye/cocos/tree/master/bounce](https://github.com/windstormeye/cocos/tree/master/bounce)
================================================
FILE: Project/ONEUIKit-ONEProgressHUD.md
================================================
## ONEUIKit/ONEProgressHUD
ONEUIKit/ONEProgressHUD为当前滴滴出行App中的HUD,我们先来看看其的样式,
图中的“网络异常,请稍后再试”的整个HUD样式,就是ONEUIKit下的ONEProgressHUD,这次就来一次仔细看看它到底是怎么一回事。
跟`ONEProgressHUD`同样的类库还有`SVProgressHUD`和`MBProgressHUD`,ONEProgressHUD同时也受到了MBProgressHUD的影响,参考了其部分实现。滴滴数据App本身之前用的SVProgressHUD,这个HUD库可以说是目前iOS开发最火的第三方HUD库了,整理了一下ONEProgressHUD的实现流程,如下所示,
因为并不能贴任何有关该库代码(内部库),所以大家就看我总结的图吧,图中展开了一个secondary方法,并且按照该方法的调用栈进一步展开,因为并不能贴代码,只能讲一些大概的核心方法,大家明白这么个意思就行,再说一句,请期待我们的`ifLabKit`喔~😜
================================================
FILE: Project/PFollow.md
================================================
## PFollow
在这一系列文章中主要记录了我在开发 PFollow 过程中遇到的问题和总结。
### 解决点击用户定位蓝点出现 callout 的问题:
func mapView(_ mapView: MAMapView!, didAddAnnotationViews views: [Any]!) {
for view in views {
let v = view as! MAAnnotationView
if v.tag == 0 {
v.isUserInteractionEnabled = false
}
}
}
### 获取当前地图可视区域中的所有大头针
哇,高德地图的文档真的太垃圾了。虽然功能确实挺全的,但是很多东西都没有指导性,获取当前地图可视区域的所有大头针,搞了好久都没有弄明白怎么获取这个可视区域到底怎么获取,幸亏搜到了 `MKMapView` 的相同方法,直接丢个 `mapView.visibleMapRect` 就完事了。😅
### 分块加载
之前对于根据用户缩放地图等级进行大头针图片的切换用了一个非常不好的办法,今天狠下心来逼自己必须改进哈哈哈。
具体的思考流程是这样的,从以往使用地图经验来看,不管是哪一家地图产品都是分块加载,只有当用户滑动到了地图上的某块为加载过的区域才进行地图的加载,如果滑到了已经加载过的区域则不会再进行地图渲染,因为之前的数据已经 `load` 到了内存中。
所以正是因为这一点,我坚决的认为高德是一定能够完成根据用户缩放地图的等级来进行切换大头针的,经过一番纠结后发现能够拿到目前屏幕可视区域中的所有标注点集合 `Set` 而且还是包括了用户定位的小蓝点在内。
因此做法是,首先在 `mapDidZoomByUser` 用户缩放地图完毕后的回调中判断 `mapView.zommLevel` ,在我们需要的数值内通过 `mapView.annotations(in: <#T##MAMapRect#>)` 方法获取到上文所说的 `Set` ,当然此时的 `Set` 是无法使用的,因为是 `Any` 类型,而且里边还有 `MAUserLocation` ,此时可以使用 `Swift` 的精华函数式编程的一个方法 `filter`,如下所示:
```Swift
let annotationSet = mapView.annotations(in: mapView.visibleMapRect).filter { (item) in
!(item is MAUserLocation) } as! Set
```
当然,这种写法还不是最优的,但这是我第一次使用 `FP` 的一些思想,还没上道,但是就上边这两行代码真的比之前那种特别傻的 `for-in` 循环好太多了。😄
接着拿到筛出来的 `MAAnnotation` 集合转为 `Array` 遍历(没找到怎么给 `Set` 遍历),然后就根据 `longitude` 和 `latitude` 在之前创建的自定义大头针数组中 找到分别判断该大头针的 `.image` 是否为想要改变的大头针,不是再修改。
因为是分块加载计算,所以并不会像之前写的那种特别傻的方法,直接全部移除再添加,改进后的这个只是在当前用户屏幕可视区域内进行移除和添加,再加上这个缩放初始化为 15 ,缩放要小于 12.8 才改小的大头针图片,所以实际上并不需要占用多大的计算资源,但是算法还是需要改进,现在是两个 `for` 循环,O(n^2) ,有点伤。
### 一个报错
`Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x1c064ccc0> was mutated while being enumerated.'` 又遇到了这个问题,这个的问题的出现是说,遍历了这个数组,然后又修改了这个数组中的内容。但是仔细看了我的代码,其中完全没有任何遍历数组的操作。开始纠结,后边猜想,是不是因为我在主线程中遍历,然后在子线程中 `addChild` 所导致的呢?因为这个流程也确实挺像
================================================
FILE: Project/PLook.md
================================================
这篇文章主要记录我在开发PLook的过程中遇到的问题。
1. UIButton的各种状态参考文章:[https://www.jianshu.com/p/57b2c41448bf](https://www.jianshu.com/p/57b2c41448bf)
2. **照片编辑**。重写`scrollView`的`hitTest`方法。当用户手指发生触摸时,前150ms会被`scrollView`截获,当150ms后用户的手指还在原位的话,`scrollView`则自动下发给对应的可以进行处理的控件,需要做的事情就是重写`scrollView`的`hitTest`方法,`isKindOf`用户想要触摸上的控件类([参考文章](https://www.jianshu.com/p/2bb30c3d2408))
```swift
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if (view?.isKind(of: PJEditImageTouchView.self))! {
self.isScrollEnabled = false
} else {
self.isScrollEnabled = true
}
return view
}
```
3. 记录一下。又忘了`UIImageView.userInterfaceEnable`默认是`NO`
4. `viewDidLoad`和`loadView`区别。`loadView`创建`ViewController`一个空白的`view`,默认会创建一个空白的`view`,`viewDidLoad`是通过xib加载视图后的调用方法,不过最后都会去调用`viewDidLoad`,所以之前一直就在`viewDidLoad`方法中初始化视图。[参考文章1](https://www.cnblogs.com/mjios/archive/2013/02/26/2933667.html)和[参考文章2](https://github.com/bestswifter/blog/blob/master/articles/uiview-life-time.md)
5. 遇到一个非常奇怪的问题,`Swift上`触发了通知、闭包、代理后,OC接受到执行pushVC,特别特别特慢,不知道为啥的。😳。
**解决:**点击拍照的时候报了一个错:
`This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.`
用回到主线程的方法后重新调用`viewDelegate`回调代理方法,搞定!因此猜测,`captureOutput didOutput`是在子线程的跑的方法
`videoDataOutPut.setSampleBufferDelegate(self, queue: .global())`是这个方法导致的,给output流传入的是一个正常等级全局子线程,故应该合并到主线程中再进行各种操作。如果不想这么做,那就直接丢入主线程`videoDataOutPut.setSampleBufferDelegate(self, queue: DispatchQueue.main)`
6. `NSTimer`和`CADisplayLink`做定时任务时,默认加到的`runloop`是`NSRunLoopDefaultMode`,当视图滑动时,`runloop`只会处理`UITrackingRunLoopMode`,并不会处理`timer`和`CADisplayLink`,可以把`runloop`的`mode`改为`NSRunLoopCommonMode`s即可。[参考文章](https://blog.csdn.net/wzzvictory/article/details/22417181)
7. 遇到一个深浅拷贝的问题。
```
vc.imageArray = [self.imageArray mutableCopy];
……
[self.imageArray removeAllObjects];
```
这会引发崩溃,崩溃信息:`*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndexedSubscript:]: index 0 beyond bounds for empty array’`。简单来说就是数组为空,原因是因为之前做的是深拷贝,后续再`remove`的时候数组的里的元素都没了,需要做值引用,`mutableCopy`即可。
8. 昨天出现push后原页面数据没了是因为重新addSubview到了下一个页面,导致上一个页面的imageView们都跑到了下一个页面中去了。
9. 遮罩效果的实现可参考文章[https://www.jianshu.com/p/d8f060c21056](https://www.jianshu.com/p/d8f060c21056)
10. 放大缩小、拖动手势结合(UIPinch和UIPan),如果既要放大也要缩小,记得打开多手势识别
11. UIImageView拖拽:[http://www.voidcn.com/article/p-tryprmmp-dw.html](http://www.voidcn.com/article/p-tryprmmp-dw.html)。
12. UIPinch和UIPan的问题:[https://blog.csdn.net/gocy015/article/details/22992929](https://blog.csdn.net/gocy015/article/details/22992929)。
================================================
FILE: Project/coding-interview-university学习笔记.md
================================================
这是我在学习一套完整的学习手册帮助自己准备 Google 的面试的笔记,其想通过这种方式去提升自己姿势水平,链接在此,欢迎大家一块学习。[https://github.com/jwasham/coding-interview-university/blob/master/translations/README-cn.md](https://github.com/jwasham/coding-interview-university/blob/master/translations/README-cn.md)
### 2018-04-25
1. 今天主要的学习内容是关于CPU执行指令、机器码指令转换、编译器工作等内容,算是给自己复习了之前的内容吧。🙄(图较大,再加上国外图床,耐心等待喔)
2. 分享一个有趣的命令,当我们使用`clang`编译器去编译程序时(C/C++、Objective-C/C++、Swift),可以使用`clang -S filePath`来查看经过编译器处理完后的汇编代码。如下所示,
这是原C代码:
这是经过编译器转换之后的汇编代码:
================================================
FILE: Project/iBistu4-0(先导篇).md
================================================
最开始我知道iBistu的时候非常憧憬,那会应该是大一,得知咱们社团有给学校做了一个移动校园App,这无疑给当时正在学习iOS开发的我打了一剂强心针,为啥?那个时候社团还没现在这般“自由”,只有每周日晚上的小组活动时间才能和各位学长和同学交流,那时候虽然也像现在这般open,但实在是不好意思,估计可能是因为以小组学习的名义起的活动太少了,也不敢经常去。也就因为这样,基本上可以认为大一上的iOS我搞的非常非常非常痛苦,不光是要熟悉大学生活,还要督促自己在喜欢的事情上持续进步,但现在想起来还是很痛苦,大家应该都应该有对一门技术感到迷茫的时候,我也同样如此,甚至比各位同学当时更加迷茫。
随着时间的流逝,一年多过去了,到了大二下受胡博学长的邀请,非常有幸的加入到了iBistu 3.3的开发中来,“士别三日,刮目相看”嘛,此时的我已经结束了第一家实习公司的工作,算是真正踏入了iOS开发的坑中,对技术的辨别能力已不同往日。那时拉下了iBistu的工程,看了一眼,说实话,当我看到了所有的页面逻辑都是用的StoryBoard连起来了以后,萌生退意,因为想要把这一坨StoryBoard的逻辑看懂,估计没有当时拖这个逻辑的学长带着看,是搞不定了。😅
真正进入iBistu 3.3的开发是在去年的五一劳动节假期,此时我再把iBistu的工程拉下来一看,emmm,胡博学长已经把之前的老代码通通kill掉了,这波操作我很喜欢。3.3的iBistu还是用了最开始的设计,而且也仅仅只是用了设计而已,每个入口里边的东西都不一样了。而且当时的我看胡博和徐鼎学长这架势,是要把iBistu之前的所有设计统统推翻,据说是不想跟教务处有关系了,而且很多功能也都不想要了。
看了相关的后端文档,发现工作量不大,而且没有设计稿!这就意味着可以个人发挥的空间非常大,在3.3中我的主要工作是负责校车、黄页、失物招领和个人中心模块。根据之前的设计来看,使用了collectionView来切割并作为这几大模块的入口,彼此之间的独立性较大,高铭学长跟我说,之前他们在写iBistu的时候,每个人负责不同的模块,各个模块之间就相当于是一个独立的App,风格都不统一。我呢就完全按照个人思路在五一假期持续推进,因为整体工作量并不大,大部分工作很快就结束了。
过了一段时间,iBistu 3.3版本上架了,但是整体的开发流程下来给我感觉非常糟糕,首先是很多细节没考虑到,其次是没有一个统一纲领,不知道如何下手,只知道自己埋头苦干,写完了就完了,也没人跟自己说这么做到底好不好,靠不靠谱,再加上当时并没有测试的概念,导致很多小的细节完全就忽略掉了。
以上就是我切入iBistu的一些经历。在iBistu 3.3的开发过程中,慢慢的理解了老师和各位学长们做这个事情的初衷,也明白了为什么社团三年多来都在坚持做这件事,可能它的比重在当时比较小,甚至我能感受到老师和学长们貌似对其都有些懈怠,当时我很爱社团,但是因为自己的话语权较小,很多事情自己是做不了的。
---
新的开发周期来了!!!社团也迎来了新的导师团和同学加入,我开始谋策着iBistu 4.0版本的开发,其实3.3中很多事情只是达到了做完的度,但并没有做好。再加上说真的,虽然我也认为4.0的设计某些地方比较欠缺,但是比起3.3真的好太多了,在4.0中我们开发团队主打黑白简约风,大量留白,但是留白带来的后果就是整体很空,因此我们再进一步,把字体加重,把用户的注意力从大量留白区转移到内容中来,这部分在新闻模块表现得较好。
4.0的主要工作是UI和模块优化,UI部分其实改动压力很大,因为社团里的同学基本上都是走开发路线,没有人对产品设计方面有过研究,我们也没法大改,只能还是在之前的基础上优化。
首当其冲的就是UI风格的统一,我首先要大刀砍掉的就是之前版本中给用户体现出独立性非常强的collectionView设计,改用tabBar的方式把一块儿一块儿的入口风格改为一连串、连续的风格,虽然本质上还是非常独立的,但是从用户的角度上看,不会给他们一种这是不同东西的错觉。
统一完整体入口风格后,开始着手考虑修改整个产品中使用频率最大部分——“新闻”。对比了iBistu目前的新闻设计和主流市场上的资讯类App,发现大家对咨询类App的设计如出一辙,基本上都差不多,因此决定对iBisut新闻模块设计不做修改,只是完善并统一风格,加粗了字体,把3.3中的“图左字右”的方式改为了copy for 知乎。😄。但是因为跟以往的设计风格不一致了,且后台新闻数据接口未改变,因此没法适配copy for 知乎的设计。(这种设计要求图要大且清晰)我们能做的就是等待,如果哪位同学有兴趣研究一波Node.js,可以借此机会锻炼一番。
黄页部分因为都是与学校有关的部门,导致设计上不能过于突出,还是按照之前设计来,直接单拎出来了一个tableView加载数据源即可
地图部分跟团队开发同学说要把之前中的大头针给去掉,换成校标logo,用于标识各个校区。但是这部分工作弄完后,我们发现logo“漂移”太严重,地图缩小时,只是logo的中心对上号了,但是logo底部的尖端并未对上号,给人的感觉就像明明标识的是健翔桥校区,但地图缩小后却是健翔桥家乐福店,emmm。
因此,这个做法也抛弃掉了,最后没办法,为了保证简约的风格,还是改为大头针吧,但出乎意料的是没想到在iOS 11上的地图大头针样式还十分的好看,显示的效果也不错。(大家可以在iOS 11上自行查看一番)因此也并未进行修改,反而减少掉了一个入口——“校车”,我们把“校车”入口放在地图VC上的左上角leftBarButton位置上,设想是因为校车也是作为地图容器的一部分,校车中也暴露出了地图概念中的路线思路。
校车部分,在当时3.3版本中还觉得一般般能看得过去,但是统一完样式风格后,再回过头来看校车的设计,真...真心丑。😅。因此我们采取的做法是大量采取黑白和字符设计,把3.3中的以图代表“来”和“回”的标识通通采用字符左右箭头的的样式,减少对用户观感的冲击,从而把用户注意力转移到区别“教学班车”和“通勤班车”中来,防止用户看错班车排班。
在校车详情部分,之前是通过颜色块串的方式区别该次班车“来”和“回”的时间,虽然确实是能够较为清晰的做了区别,但整体的感观还是非常的单调和突兀(虽然现在4.0还是有些做的不太好的地方),肯定要去除颜色块链的区别方式的。最后我们把校车详情的样式变为了如下所示,
在失物部分,没见过老版本的iBistu失物招领是怎么做的,3.3版本参考了闲鱼的设计,用了scrollView去作为全部失物图片的载体,4.0版本想着用瀑布流的方式去作为整体的展示主体,但时间上不允许了,现在唯一不满意的地方也就是失物和失物详情了,如下所示,
在我的部分,第一个要搞掉的就是把之前自己写的“退出登入”给kill掉,当初这个页面是用StoryBoard搭的静态tableView,然后为了炫技(其实并没有技术含量)才加了奇丑无比尴尬的“退出登入”,巨大的败笔,最后的做法是把“退出登入”入口移到了“我的”右上角rightBarButton位置上,但估计有可能是因为放置的icon图片有歧义,个别同学以为是“分享”。
iBistu 3.3开发完的时候,咱们说心里话,给我的感觉是埋汰了。4.0开发完后虽然它还不够完美,但是我能拍着胸脯说,“学长们!我们帮你们续了一秒!”😝
[下篇:iBistu4-0(黄页)](./iBistu4-0(黄页).md)
================================================
FILE: Project/iBistu4-0(地图).md
================================================
# 数据源
## 获取校区位置数据
**接口: **http://api.iflab.org/api/v2/ibistu/_table/module_map
**请求方法:**get
**参数:**无
**示例请求成功返回值:**
```json
{
"resource": [
{
"id": 5,
"areaName": "小营校区",
"areaAddress": "北京市海淀区清河小营东路12号",
"zipCode": "100192",
"longitude": 40.036,
"latitude": 116.349,
"zoom": 10
},
{
"id": 6,
"areaName": "健翔桥校区",
"areaAddress": "北京市朝阳区北四环中路35号",
"zipCode": "100101",
"longitude": 39.988,
"latitude": 116.392,
"zoom": 10
},
{
"id": 9,
"areaName": "酒仙桥校区",
"areaAddress": "北京市朝阳区酒仙桥六街坊1号院",
"zipCode": "100016 ",
"longitude": 39.963,
"latitude": 116.49,
"zoom": 10
}
]
}
```
毕竟是地图模块,因此是一定要使用Map,为了降低打包后的额外增长体积,考虑使用原生MapKit进行开发。使用MapKit呈现具体的地理位置最重要的就是经纬度,从数据源中的`longitude`(经度)和`latitude`(纬度)中可以拿到。
在iOS中的Map中有“大头针”的概念,而我们给大头针经纬度也只是把“大头针”的位置在Map上给标记出来,实际上一个“大头针”代表着什么信息我们并不知道。此时,数据源中的`areaName`和`areaAddress`字段数据就可以较好的给用户一个“大头针”专属提示。
## 获取班车数据
**接口:**http://api.iflab.org/api/v2/ibistu/_table/module_bus
**请求方法:**get
**参数:**无
**示例请求成功返回值:**
```json
{
"resource": [
{
"id": 5,
"busName": "西线班车",
"busType": "通勤班车",
"departureTime": "06:55:00",
"returnTime": "17:10:00",
"busLine": "[{"station":"公主坟","arrivalTime":"06: 55"},{"station":"当代商城","arrivalTime":"07: 05"},{"station":"蓝旗营清华西南门","arrivalTime":"07: 15"},{"station":"小营校区","arrivalTime":"07: 45"},{"station":"小营校区","arrivalTime":"17: 10"},{"station":"蓝旗营清华西南门","arrivalTime":"17: 35"},{"station":"当代商城","arrivalTime":"17: 50"},{"station":"公主坟","arrivalTime":"18: 20"}]",
"busIntro": "西线班车"
},
{
"id": 11,
"busName": "望京-健翔桥",
"busType": "通勤班车",
"departureTime": "07:15:00",
"returnTime": "17:10:00",
"busLine": "[{"station":"望兴园","arrivalTime":"07: 15"},{"station":"望京花园","arrivalTime":"07: 17"},{"station":"健翔桥校区","arrivalTime":"07: 45"},{"station":"健翔桥校区","arrivalTime":"17: 10"},{"station":"望京花园","arrivalTime":"17: 45"},{"station":"望兴园","arrivalTime":"17: 47"}]",
"busIntro": "望京-健翔桥"
},
{
"id": 12,
"busName": "小营-清河校区南门-健翔桥",
"busType": "教学班车",
"departureTime": "09:50:00",
"returnTime": "",
"busLine": "[{"station":"小营校区","arrivalTime":"09: 50"},{"station":"清河小区南门","arrivalTime":"09: 51"},{"station":"健翔桥校区","arrivalTime":"10: 35"}]",
"busIntro": "小营-清河小区南门-健翔桥"
},
{
"id": 17,
"busName": "健翔桥-小营",
"busType": "教学班车",
"departureTime": "09:50:00",
"returnTime": "",
"busLine": "[{"station":"健翔桥校区","arrivalTime":"09: 50"},{"station":"小营校区","arrivalTime":"10: 10"}]",
"busIntro": "健翔桥-小营"
}
]
}
```
为什么在地图模块中也要引入班车呢?在3.3中班车和地图独占一方,但在4.0中我们把班车和地图杂糅在了一块,大家从数据源中可以看到每个班车列表排班中都有“地点”属性,而地图模块就是“地点”属性集合的载体,因此,我们做出了这个决定。
班车模块的数据源中还有“通勤班车”和“教学班车”两种类型,需要我们对班车类型从用户体验角度出发做区别。再加上各个车次有非常明显的排班时间和到达地点,这也给了我们一个暗示,可以对“busLine”字段做一个类似于“时间轴”的设计。
## 导航
这个功能在后端文档上没有体现出来,其实也不需要写啥问题。我们想到达到的效果在Map上选择一个“大头针”,然后再点击“导航”按钮,即可开启导航功能。
# 设计
我们设想在把leftBarButton作为班车入口,rightBarButton作为导航入口,分别使用push的方式切入。而班车模块,还是使用了tableView作为数据源载体,并且把每俩班车的来回时间段都做二次push,重新丢入一个VC中做呈现。
导航功能我们天真的认为用户应该不会把原生地图App删除,默认直接跳进原生MapApp🙂。
# 编码
1. [详细编码见工程](https://github.com/ifLab/iCampus-iOS)
2. 在Map的改动部分,3.3是原生的大头针,到了4.0我们认为大头针太丑,遂想改动为自定义的大头针,用校标作为大头针logo。但问题又来了,因为在素材上没法保证高清和风格统一,所以最后的解决办法只能又撤回使用大头针,令人非常意外的是在iOS 11上的大头针出人意料的好看。远没有iOS 10中的那般突兀。
还有一点非常出乎我的意料,当我们给大头针设置好相关的提示信息后放大地图点击大头针后可以发现,效果真的是超赞👍。虽然没有体现出“差异性”,但比起iOS 10那令人尴尬的大头针效果真的好很多。
iOS 10地图大头针样式
3. 校车模块的入口在map VC的leftBarButton。在3.3中因为不但要熟悉流程而且还要出活,因此没有在UI方面下多大功夫,用的基本上之前的素材,因此整体呈现出来的效果也不够理想,如下所示:
大家可以看到效果实在是不佳,而且还有莫名的素材错位。😓。从用户整体的角度来看比较尴尬,再加上3.3本身也是一个产品过渡期,部分模块产生了一种不上不下的中间位置,没有一种“大刀挥舞”的做事快感,反而多了“婆婆妈妈”的细节。
以下是4.0的校车模块,我们弱化掉了3.3中校车模块中其它元素的影响,比如“往、返”icon等,对“班车类型”做了重点区分,校车模块也算是一个咨询类模块吧,最担心的问题应该就是怕“找错信息”,因此我们想让用户目标聚集在先确定“班车类型”,随后才接着展开下一步操作。
而点击进入某一条班次后的实际编码过程也遇到了很多问题,之前的做法是通过使用“色块”的方式来区分往返车次,但问题就出在这大量使用的“色块”上,原意是用于区分往返车次,但实际上过于抢眼,看班车详情不但是想要去看到什么时间出发、什么时间返回,其实更重要是还要知道中途都都在哪些时间点经过哪些地方,因此,我们在实际编码中前前后后做了以下参考。
其实最后的设计我还挺中意,但开发团队部分同学还是觉得不太合适,最终我们采取的做法如下,emmm这就是之前在先导篇中所说“前后闭包”设计,虽然我觉得还是哪有点怪怪的,但不管怎么说都比3.3中的清晰明了。
如果各位同学有兴趣的话,可以去翻源码你会发现做“时间轴”的效果非常的有趣,嘿嘿嘿。
4. 在导航部分,之所以要限制用户必须要选择“大头针”后再进行导航操作就是因为跳转到自带地图应用需要传入地点、经纬度所以我们要先“告诉”代理方法数据源是什么才行。
```Objc
- (void)gothereWithAddress:(NSString *)address andLat:(NSString *)lat andLon:(NSString *)lon {
//跳转系统地图
CLLocationCoordinate2D loc = CLLocationCoordinate2DMake([lat doubleValue], [lon doubleValue]);
MKMapItem *currentLocation = [MKMapItem mapItemForCurrentLocation];
MKMapItem *toLocation = [[MKMapItem alloc] initWithPlacemark:[[MKPlacemark alloc] initWithCoordinate:loc addressDictionary:nil]];
toLocation.name = address;
[MKMapItem openMapsWithItems:@[currentLocation, toLocation]
launchOptions:@{
MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving,
MKLaunchOptionsShowsTrafficKey: [NSNumber numberWithBool:YES]
}];
return;
}
```
而且在rightBarButton的点击事件中,我们要保证传入的调起系统地图代理方法地点的正确性,所以,我们要多加一个“北京信息科技大学”前缀,
```ObjC
- (void)rightItemClick {
if (self.isSelectAnnotation) {
NSDate *date = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"MM-dd HH:mm:ss"];
NSString *dateString = [formatter stringFromDate:date];
NSDictionary *dic = @{
@"username" : [PJUser currentUser].first_name,
@"uploadtime" : dateString,
@"goto" : self.kAnnotationView.annotation.title
};
[MobClick event:@"ibistu_map_nav" attributes:dic];
[NSString stringWithFormat:@"%@", self.kAnnotationView];
// 加上学校名称前缀
[self gothereWithAddress:[NSString stringWithFormat:@"北京信息科技大学%@", self.kAnnotationView.annotation.title]
andLat:[NSString stringWithFormat:@"%f", self.kAnnotationView.annotation.coordinate.latitude]
andLon:[NSString stringWithFormat:@"%f", self.kAnnotationView.annotation.coordinate.longitude]];
}else {
[PJHUD showErrorWithStatus:@"请先选择地点"];
}
}
```
# 总结
1. 地图模块是iOS开发初级进阶内容,适合熟悉了iOS开发基本内容后的同学进一步锻炼,且涉及到了数据源的二次定义和更多的自定义视图,对开发同学的产品设计和把控提出进一步要求。
2. 地图模块是3.3中我参与开发的个人因素带入最多的地方,融合了很多个人思想,在某些细节上考虑得必定不够周到和完善,这部分还是有一些细节可以持续提升,比如“时间轴”的改进。
3. 对于校车详情部分在4.0中主要有两个方向的设想,一是在校车列表中直接展开获取路线,二是同样采用push进行新的VC方式,但是路线都在地图上标记出来。我更倾向于第二种做法,但是由于时间关系(当时快期末了)未能持续推进。
[下篇:iBistu4-0(失物)](./iBistu4-0(失物).md)
================================================
FILE: Project/iBistu4-0(失物).md
================================================
本系列文章为记录iBistu 4.0各个模块开发中进行的思考、设计和编码总结,供同学们参考。
---
# 数据源
## 获取失物招领列表数据
**接口:**http://api.iflab.org/api/v2/ibistu/_table/module_lost_found?limit=10&order=createTime%20desc
**请求方法:**get
**参数:**
`offset:`必需参数,招领信息(页数-1)的10倍,当offset为0时,返回的是第一页的数据,offset为10时,返回的是第二页,20时返回的是第三页,依次类推。
`filter:`必需参数,只有两个值,值为“isFound=false”时,返回普通列表数据;当值为“(isFound=false)And(author=用户名)”时,返回的是该用户发布的所有招领信息
**示例:**获取第2页的用户名为mphone的已发布信息列表,:(此处参数值为:offset=10,filter=(isFound=false)And(author=mphone),如果想获取普通列表,则filter=isFound=false):http://api.iflab.org/api/v2/ibistu/_table/module_lost_found?limit=10&order=createTime%20desc&offset=10&filter=isFound=false
示例请求成功返回值:
```json
{
"resource": [
{
"id": 86,
"details": "捡到一堆小玩意儿",
"createTime": "2016-10-13 13:26:30",
"author": "mphone",
"phone": "13622251463",
"isFound": false,
"imgUrlList": "[{\"url\":\"files/ibistu/lost_found/image/14763363892752229.jpg\"},{\"url\":\"files/ibistu/lost_found/image/14763363331344112.jpg\"}]"
},
{
"id": 85,
"details": "捡到一盒餐具,不知道是谁的,如图所示",
"createTime": "2016-10-13 13:25:33",
"author": "mphone",
"phone": "13688881425",
"isFound": false,
"imgUrlList": "[{\"url\":\"files/ibistu/lost_found/image/14763363331344112.jpg\"}]"
}
]
}
```
从获取失物招领数据源中可以看到,我们可以拿到该条失物招领的“描述信息”、“发起者”、“创建时间”、“发起者联系方式”、“失物图集”。并且还是个数组源,给我们的暗示可以采用tableView作为内容载体,并且“失物图集”也是个数组源。
## 发布新招领信息
**接口:**http://api.iflab.org/api/v2/ibistu/_table/module_lost_found
**请求方法:**post
**参数:**
`details`:失物招领细节;
`author`:失物发起者;
`phone`:失物发起者联系方式;
`imgUrlList`:失物图集相对地址。(下文讲解)
**请求体:**(其中imgUrlList中的url为:“files/上传图片成功后返回的path”)
```json
[
{
"details": "捡到一盒餐具,不知道是谁的,如图所示",
"author": "mphone",
"phone": "13688881425",
"imgUrlList": "[{\"url\":\"files/ibistu/lost_found/image/14763363892752229.jpg\"},{\"url\":\"files/ibistu/lost_found/image/14763363331344112.jpg\"}]"
}
]
示例请求成功返回值:(可忽略)
{
"resource": [
{
"id": 93
}
]
}
```
该API需要我们对其进行POST请求,关于HTTP请求方式POST和GET的区别可以参考知乎上的[这个问题](https://www.zhihu.com/question/28586791),我们需要构造入口先获取到失物和用户信息详情。
## 上传图片(多图上传)
**接口:**http://api.iflab.org/api/v2/files/ibistu/lost_found/image/
**请求方法:**post
**请求体:**(上传图片需要将图片转换为base64格式的编码字符串,然后作为content)
**请求参数:**
`name`:图片文件名(随意,目前我采用的是当前时间戳)
`type`:统一默认为file;
`is_base64`:统一默认为true;
`content`:base64对压缩完的图片进行编码。
```json
{
"resource": [
{
"name": "filename1.jpg",
"type": "file",
"is_base64": true,
"content": "base64字符串"
},
{
"name": "filename2.jpg",
"type": "file",
"is_base64": true,
"content": "base64字符串"
},
...,
{
"name": "filename3.jpg",
"type": "file",
"is_base64": true,
"content": "base64字符串"
}
]
}
```
**示例请求成功返回值:**(上传成功后文件的真实存储路径为:“http://api.iflab.org/api/v2/files/上传成功后返回的path值”)
```json
{
"resource": [{
"name": "filename1.jpg",
"type": "file",
"path": "图片存放的相对路径"
}
,{
"name": "filename2.jpg",
"type": "file",
"path": "图片存放的相对路径"
}
...
,
{
"name": "filename3.jpg",
"type": "file",
"path": "图片存放的相对路径"
}]
}
```
要求我们要把需要上传的图片用base64进行编码,千万记得先进行压缩,要不然base64对图片进行编码完后的数据量会非常非常非常大😓,因为base64是对其进行像素转化编码,分辨率越高,编码后得到的加密字符串也就越长,耗费的网络资源也就越高。
关于iOS中对图片的压缩,我们可以使用`drawInRect`重新对该张图片进行规定大小的绘制拿到降低size的图片,或者使用一些第三方库即可(我没找到合适的)
```ObjC
-(UIImage *) imageCompress:(CGFloat)targetWidth {
CGFloat width = self.size.width;
CGFloat height = self.size.height;
CGFloat targetHeight = (targetWidth / width) * height;
UIGraphicsBeginImageContext(CGSizeMake(targetWidth, targetHeight));
[self drawInRect:CGRectMake(0,0,targetWidth, targetHeight)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
```
以上是UIImage的分类方法,跟失物详情获取分享图片的做法是一样的,需要开启当前图片上下文进行缩放的size绘制,绘制完成后拿到一个压缩后的图片。之所以能够这么做是因为,我们把原始图片按照一定缩小比例重绘拿到新的图片后,并未对UIImageView进行大小缩放,还是原先的大小,这就导致用大框(UIImageView)作为了小图(UIImage)的载体。肯定还有其它的压缩方式,而且肯定也还有很多无损压缩方式,这是一种投机的有损压缩(不知道这么说大家明白没)
# 设计
根据之前的说法,我们根据失物列表数据源的特性使用tableView作为载体,点击tableViewCell后跳转到对应的失物详情中。在失物列表,除了“失物图集”外其它信息都是简略的,进入到对应的失物招领详情后才能够获取到所有详细信息,并且可直接调起拨打电话和发送短信。
# 编码
1. [详细编码见工程](https://github.com/ifLab/iCampus-iOS)
2. 失物列表的“失物图集”部分3.3中的设想是scrollView直接把图集左右摆平,然后把imageView的用户点击事件打开,点击图集中的某一张图片后再放大即可。
3. 失物详情是4.0才加入的新模块,原本只能在失物列表中查看和拨打电话,这就导致了用户在失物列表中查看某一条失物招领信息时并不能快速的获取有效信息,为了达到“一站式”的浏览失物,我们不但在3.3能够直接在APP中拨打电话的情况下还继续添加了直接发送短信、分享失物到主流社交平台上。(虽然一点难度没有emmm)
4. 失物详情的分享部分采取的是使用Core Graphics框架相关API。失物详情整体用scrollView作为载体,此时我们把scrollView作为绘制的母体,获取其size作为绘制画布的大小和内容,最后拿到一张供分享的图片。(不知道为啥配色又没了。)核心实现如下,首先要先设置要开启绘图上下文的一些选项,比如绘图区域、是否透明、缩放等等。其次我们还要设置开启绘图的内容,把失物详情scrollView作为绘图内容,最后关闭绘图上下文。
```ObjC
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, 0.0);
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];//renderInContext呈现接受者及其子范围到指定的上下文
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();//返回一个基于当前图形上下文的图片
UIGraphicsEndImageContext();
```
5. 在“发布新失物”部分,需要直接提取当前登录用户名,保证失物发布的真实性,用户手动补充联系电话、描述信息和失物图集,且默认至少发布一张失物照片。
# 总结
1. 失物模块整体上可以说是黄页模块的进阶,黄页模块整体都是tableView一路走到底,但是失物杂糅了tableView、scrollView和一些自定义部分页面,适合新手切入学习。
2. 失物模块在4.0原本是想换成瀑布流样式的,但由于时间因素并未能持续推进。
================================================
FILE: Project/iBistu4-0(新闻).md
================================================
# 数据源
## 获取新闻列表数据
**接口:** http://api.iflab.org/api/v2/newsapi/newslist
**请求方法:** get
**参数:**
`category`:必需参数,新闻分类,可选的值为"zhxw", "tpxw", "rcpy", "jxky", "whhd", "xyrw", "jlhz", "shfw", "mtgz"
`page`:必需参数,当前页数,从0开始,0表示第一页
**示例:**获取第5页的综合新闻:(此处参数值为:category=zhxw,page=4):http://api.iflab.org/api/v2/newsapi/newslist?category=zhxw&page=5
**示例请求成功返回值:**
```json
[
{
"newsTitle": "我校召开2016年本科招生录取工作总结会",
"newsTime": "2016-09-14",
"newsLink": "http://news.bistu.edu.cn/zhxw/201609/t20160914_39069.html",
"newsImage": "http://news.bistu.edu.cn/zhxw/201609/W020160914608238897988_135.jpg",
"newsIntro": "9月14日上午,学校在小营校区召开2016年度本科生招生录取工作总结会。副校长许宝杰主持会议并做总结讲话,相关职能部门负责人及招生录取工作相关人员参加了会议。\n \n"
},
{
"newsTitle": "学校领导走访校友企业“佰能蓝天”",
"newsTime": "2016-09-13",
"newsLink": "http://news.bistu.edu.cn/zhxw/201609/t20160914_39066.html",
"newsImage": "",
"newsIntro": "9月13日下午,校友会常务副会长、副校长韩秋实,校友会副会长、校长助理林国策,校友校史工作办公室、自动化学院相关领导及工作人员一行前往校友企业“北京佰能蓝天科技股份公司”调研,并祝贺“佰能蓝天”公司成...\n"
},
...
{
"newsTitle": "【院长访谈】 李宁教授畅谈大类招生分流培养",
"newsTime": "2016-09-12",
"newsLink": "http://news.bistu.edu.cn/zhxw/201609/t20160912_39011.html",
"newsImage": "http://news.bistu.edu.cn/zhxw/201609/W020160912316469392018_135.jpg",
"newsIntro": "“大类招生、分流培养”作为一种新的人才培养模式正逐渐被国内外高校所采用。计算机学院是我校实行大类招生的试点学院。大类招生始于2014年,至今已有3年时间。\n"
}
]
```
从数据源中我们可以看出,新闻模块数据源跟目前市面上的新闻资讯类App展示出的数据种类大致相同,有`newsTitle`、`newsTime`、`newsImage`、`newsIntro`等,所以此处给了我们一个暗示可以参考目前市面上已有的资讯类App的设计。
## 获取新闻详情
**接口:**http://api.iflab.org/api/v2/newsapi/newsdetail
**请求方法:**get
**参数:**
`link`:必需参数,新闻列表接口中返回的newsLink内容
**示例请求成功返回值:**
```json
{
"title": "【院长访谈】 李宁教授畅谈大类招生分流培养",
"time": "2016-09-12",
"article": " “大类招生、分流培养”作为一种新的人才培养模式正逐渐被国内外高校所采用。计算机学院是我校实行大类招生的试点学院。大类招生始于2014年,...是需要改革的,而不是完全听命于受教育者和当前的就业市场,因而大类招生也同时考量着教育者和管理者的胆识和办学自信。(供稿:党委宣传部)",
"imgList": [
"http://news.bistu.edu.cn/zhxw/201609/W020160912316468133294.jpg",
"http://news.bistu.edu.cn/zhxw/201609/W020160912316468133295.jpg",
"http://news.bistu.edu.cn/zhxw/201609/W020160912316468133296.jpg"
]
}
```
新闻详情不是必须的,但是学校官网并没有做移动端适配,导致直接设置webView的requestURL出来的是的full size规格的页面。
新闻详情的数据源太长了,大家有兴趣的话可以参考iBistu的后端文档看看,其中有趣的是你会发现返回的新闻详情数据源实体并未是H5标签,而是字!符!串!,那么新闻中的图片去哪了?对!就是`imgList`字段,如果你真的去看了后端文档**新闻详情**部分的接口,你会发现那一大段的新闻详情实体介绍居然中间有回车换行符!!!这部分我是用的chrome的JSONVIEW格式化的JSON数据后发现的有几个可爱的“回车符”!
再仔细对比原新闻和3.3版本iBistu,emmm这几个换行符的位置就是要插入图片的位置,关于搞定iBistu新闻详情模块的细节可以参考之前为iBistu新闻做的微信小程序[开发总结](http://pjhubs.cn/2018/01/13/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%88%9D%E6%8E%A2%EF%BC%88%E4%BA%8C%EF%BC%89/),以下为节选,
>······我的乖乖,您看懂是什么意思了么?一堆回车符的地方就是要插入图片的地方。😱。所以我们要根据回车符出现的地方来判断是否应该插入图片,看了看iBistu的新闻详情部分的实现,确实是这么做的。
>但是更伤的问题来了,根据回车符我截断字符串在数组里,然后po出了内容,一看更呆了,图片在一个地方集中出现得越多,那么这块地方的换行符也就越多,换句话说,得根据回车符的多少来决定插入的图片数量。
>嗯,其实这还不是最伤的,按照这个思路弄完了以后,浏览了前面几条新闻,完美对上了,但是!!!到了后边的几条新闻就全乱了。该出现图片的地方没出现,不该出现的图片的地方空一大片
>原因是因为刚开始找到的规则是,三个换行放一张图片,如果当前区域放超过一张图片,比如说是两张图片,换行数则由三个变成了七个,也就是说,以三位基数,每多加两个换行多一张图片。
>但事实上不是这样啊!😭。后边几条新闻的换行数跟图片数对应关系完全不符合之前找到的,全乱了。这就非常的难受了。弄到最后,实在没办法,决定了如果是多张图片就只放一张😔。
>如果这部分你没能拿到真实数据好好研究一番的话,就算看了代码意义也不太大······
iBistu 4.0延用了之前3.3的实现,同样也是判断`\n`的出现,具体实现细节我们编码部分细说。
# 设计
仔细看喔,新闻详情居然是个tableView!!!我也不知道为啥之前设计的时候要用到tableView(这部分代码不是我写的,逃)
# 编码
1. [详细编码见工程](https://github.com/ifLab/iCampus-iOS)
2. 关于新闻分类的segementView,本着“多快好省”的理念🙂。直接用了[HMSegmentedControl](https://github.com/HeshamMegid/HMSegmentedControl),使用起来非常简单,但是现在iBistu我要慢慢转变思维,以往都是直接看上哪个第三方库好用就二话不说直接拿来就开造,虽然能够让接手开发的同学快速出成果,但是iBistu毕竟是要体现出社团实际开发水平的一个东西,因此我有一个粗浅的想法,把iBistu用到的一些第三方库如果能够我们自己实现就自己实现。
我们要不是说要取代这些用到的第三方库,而是要丢弃一些第三方库中所多余的东西,很多东西不是说它不好,而是说不合适,再加上现在社团的同学一届比一届强,我们有足够的实力造一些开源组件🙂。
3. 新闻列表,我们经历了几次变化,3.3中是沿袭了以往的设计,4.0刚开始进入开发阶段时我们采取的是简书的设计,做完了以后我发现图模糊而且设计上还是有些尴尬,比如字重的比例、文字的位置等
在4.0快要结束的时候终于下定决心,抛弃copy for 简书的风格,改为copy for 知乎,是老版本的知乎(现在新版知乎也改了,改为了“字左图右”的样式,让用户更加专注于每个问题的文字内容而不是图片),从下图中可以看到,应该能够让大家更加喜欢喜欢看iBistu新闻吧!🙂
4. 新闻详情部分,使用到的是[DTCoreText](https://github.com/Cocoanetics/DTCoreText)来渲染HTML,因为这部分内容是3.3遗留下来的,然而事实上HTML能够直接用webView进行渲染的,并不知道这是为啥😓。po一段success块代码:
```ObjC
ICNewsDetail *news = [[ICNewsDetail alloc] init];
news.title = dic[@"title"];
news.creationTime = dic[@"time"]; news.pcURL = dic[@"imgList"]; news.body = [NSString stringWithFormat:@"%@
", dic[@"article"]];
news.body = [news.body stringByReplacingOccurrencesOfString:@"\n" withString:@"
"];
news.body = [news.body stringByReplacingOccurrencesOfString:@"
" withString:@"
"];
for (int i=0; i
"] withString:[NSString stringWithFormat:@" ", news.pcURL[i]]];
} @catch (NSException *exception) {
if ([exception.name isEqualToString:NSRangeException]) {
break;
}
}
}
success(news);
```
可以看到是判断`\n`字符的出现来做`
`标签替换,然后再根据`
`的出现位置,插入` `标签。
新闻详情样式在4.1中会得到修改。
5. 新闻的分享功能采取的做法与失物模块的分享做法一直,新闻详情的数据源主体是tableView,tableView继承于scrollView,所以接下来的组佛就跟失物模块中的分享功能一样啦,有兴趣的同学可以自行转移阵地。
# 总结
1. 新闻模块为iBistu 4.0中技术难度相对较高的一部分,适合新手切入iBistu开发中的拔高模块。涉及到的难度主要是位于新闻详情(虽然这部分工作之前已经做好了。🙂)
2. 新闻模块的改动迭代次数较多而且每次迭代都是大改,虽然从整体上看难度不大,但很多地方都是团队开发同学一路来慢慢摸索,有时候费老大劲才做出来的效果,很多时候其它同学有了更好的思路就立马直接开始重新改了,现在总体看起来效果还算不错,但还算有改进的空间。
[下篇:iBistu4-0(黄页)](./iBistu4-0(黄页).md)
================================================
FILE: Project/iBistu4-0(黄页).md
================================================
本系列文章为记录iBistu 4.0各个模块开发中进行的思考、设计和编码总结,供同学们参考。
---
# 数据源
## 获取黄页部门列表数据
**接口:**
ibistu/_table/module_yellowpage?filter=isDisplay%3D1&offset=1&group=department
**请求方法:**get
**参数:**无
**示例请求成功返回值:**
```json
{
"resource": [
{
"id": 94,
"name": "研究生部(党委研究生工作部)",
"telephone": "1",
"department": 10,
"isDisplay": true
},
{
"id": 102,
"name": "人事处",
"telephone": "1",
"department": 11,
"isDisplay": true
}
]
}
```
通过以上数据源实例,我们能够发现返回的“resource”是个数组,我们可以把其转化为一个数组对象进行使用,并且我们还能够从数据源中发现,每个数组中的单个“对象”提供的信息非常有限,能够提取的出来的信息只有“name”能够用上。
这也就代表了,我们如果想要在“黄页”上玩出花来是一件非常困难的事情。
## 获取黄页某一部门下的电话号码数据
**接口:**http://api.iflab.org/api/v2/ibistu/_table/module_yellowpage
**请求方法:**get
**参数:**
offset:固定参数,值为1
filter:固定前缀department=,值为黄页接口1返回的数据中的department字段值
**示例:**获取研究生工作办公室的电话号码:(此处参数值为:department=10):http://api.iflab.org/api/v2/ibistu/_table/module_yellowpage?offset=1&filter=department=10
**示例请求成功返回值:**
```json
{
"resource": [
{
"id": 95,
"name": "主任室",
"telephone": "82426837",
"department": 10,
"isDisplay": true
},
{
"id": 97,
"name": "副主任室",
"telephone": "82426097",
"department": 10,
"isDisplay": true
},
{
"id": 101,
"name": "学位与学科建设/行政办公室",
"telephone": "82426838",
"department": 10,
"isDisplay": true
}
]
}
```
从部门详情中可以看到,跟之前的数据源格式一模一样,不过我们可以利用的信息多了“telephone”字段。
因此,综上所诉,为了顾及简单明了的显示规则,对此不作过多的修改,直接使用tableView作为所有部门和各个部门详情的数据源载体。
# 设计
部门列表我们采用tableView去展示数据源,点击部门列表cell将会push进对应部门详情,同样在部门详情也使用tableView去展示数据源,但点击部门详情cell时我们将会调起原生电话API,拨打展示出的对应部门电话号码。
# 编码
1. [详细编码见工程](https://github.com/ifLab/iCampus-iOS)
2. 在部门列表顶部有一个搜索框,要实现模糊搜索,因为受到API的限制并没有单独开一个接口去接收textField文本内容变化后而返回新的数据。
因此,我们的做法只能查找静态数据源,相同的点还是在搜索框的文本内容改变代理方法中进行处理。在处理体中,因为数据源都已经异步拿到了,虽然有异步的时间差,但一般来说没有用户会在列表数据源没出来直接进行搜索的。
在端上直接进行模糊搜索有两种做法,一是通过NSRegularExpression类创建正则表达式后再进行匹配,二是使用NSPredicate类创建谓词匹配实例,关于这两部分内容大家可以参考[这篇文章](https://www.cnblogs.com/pruple/p/5865208.html)
```Objc
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText; {
// 使用谓词匹配
NSPredicate *preicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[c] %@", searchText];
if (_kSearchArr != nil) {
[_kSearchArr removeAllObjects];
}
for (NSDictionary *dict in _dataArr) {
NSString *str = dict[@"name"];
if ([preicate evaluateWithObject:str]) {
[_kSearchArr addObject:dict];
}
}
[self reloadData];
}
```
在黄页搜索框中,我们采取了谓词的CONTAINS做了模糊搜索,谓词匹配的使用比较简单直接调用evaluateWithObject方法,传入str,只要str中有一个字符与searchText中的任何字符匹配即可。注意,此处并不需要对中文字符做编码集的替换,因为两者对比的都是NSString类型,NSString默认是Unicode编码(如果没记错的话)所以没必要可以直接对中文字符进行比对。
# 总结
1. 黄页模块是iBistu for iOS整体最经典的部分,运用了tableView的基本思想和操作,可以快速帮助新手快速切入iBistu开发。
2. 此处有一个待优化的地方。每次点击一个部门详情后,都会拉一次数据,这种做法针对咨询类产品是正确的,但是针对这种黄页类产品,数据更新允许滞后,这部分应该做缓存,只有每次在所有部门列表下拉时才进行数据源的全部更新。
[下篇:iBistu4-0(地图)](./iBistu4-0(地图).md)
================================================
FILE: Project/上架.md
================================================
1. iOS开发账号区别于联系。说起来容易懵逼,以之前做的一张图说明,如下所示:
2. 沙盒的目录结构。在iOS开发中,我们或多或少都听说过沙盒这个东西。整个App的专用数据存储空间,包括缓存、持久化存储、数据库等等跟数据相关的内容,都会被系统保存每个App唯一化的区域中,这个区域就是沙盒。而沙盒一般来说(除非你有自己创建文件夹)分为以下几个主要目录:
* **Documents**:保存应用运行运行时生成的需要持久化的数据,如游戏进度、画图类App的相关绘图信息等数据,且会被iTunes备份。
* **Libraty/chaches**:保存应用运行时生成的需要持久化的数据,iTunes不会备份该目录。一般存储体积大,不需要备份的非重要数据,如缓存图片或离线数据等。
* **Library/Preference**:保存应用的所有偏好设置,系统的设置App会在该目录查找应用的设置信息,会被iTunes备份该目录。
* **tmp**:保存应用运行时所需的临时数据,不会被iTunes备份,手机重启、系统存储空间不足等情况下均会被清理掉。
3.
================================================
FILE: Project/第三方库管理.md
================================================
## 背景
在iOS开发中,我们很多时候都会去使用到其它人写好的功能、函数甚至组件,在刚开始学习iOS开发时,我想用别人写好的代码怎么办呢?当时我是真的一点都不知道有第三方库管理工具这么个东西,只能去GitHub、CSDN、OSChina等平台下载别人的工程,然后再从工程里拖出自己需要的代码。
这种事情看上去就非常的难受对吧?但当时的我却忍受了快半年之久😂,直到有一天我发现当引入的一个平台框架如果还是手动添加相关依赖,来来去去就得弄快半小时!!!正是因为这个原因,我终于有了机会用上包管理器......
## 简介
在iOS开发中的包管理器(或者第三方库管理)目前基本上用的最多的就`cocoaPods`和`Carthage`。
### cocoapods
这是目前最火的 **中心化** 包管理工具,注意我的用词喔~,中心化!!!这中心就托管在GitHub上,其汇集了所有使用`cocoaPods`进行管理的 **公开** 第三方库,当然我们也可以利用`cocoaPods`的特性构建自己的私有库,使用Ruby语言编写,使用`cocoaPods`有个很明显的优点,特别的方便,如果只是简单的、单纯的进行第三方库管理,只需要三分钟不到的时间即可完成管理,因为其提供了一个单独的workspace,这其中的各种的依赖、编译链接等等问题统统都由其进行管理,我们只需要在生成的workspace中进行开发工作即可。
但同时它的优点在国内也恰恰是其缺点,因为毕竟是在国内,而且其中心又是托管在GitHub上,而GitHub有时又抽风,虽然一般我们用都会切换成淘宝或者清华的源(科学上网除外),但这只是解决掉了部分问题,其它问题依旧存在,更何况这种中心化的管理再加上高度封装的workspace在某些时候是非常的令人苦恼(高度封装好的东西遇到特殊需求时,怎么对其做修改呢?对吧?)
### Carthage
`Carthage`,直译过来叫迦太基(并不知道这其中的含义),`Carthage`做的事情跟`cocoaPods`一模一样没有任何区别,同样也是第三方库管理,但这两个工具就像是对立面一样,`Carthage`的优点就是`cocoaPods`的缺点,`cocoaPods`的优点就是`Carthage`的缺点。(基本上差不多)
首先,`Carthage`是去中心化的,使用Swift语言编写,没有所谓的中心托管仓库一说(非常的符合当前风口的趋势嘛🙄),那怎么集成第三方库?这就是`Carthage`的特点,只要能够保证当前库支持了`Carthage`,在相关的配置文件下写好索引位置,一般都是
```
github "xxxxxx"
```
最后`Carthage`执行命令后(后文说到)即可完成集成,这其中是有用到`xcodebuild`等相关命令编译出了一个framework,我们只需要把这个编译出来的framework添加到工程后即可,保存单一workspace的完整、干净,并且每次项目build时,不用再像使用`cocoaPods`那般需要rebuild所有相关的依赖库,其实更爽的是,正因为如此,`Carthage`和`cocoaPods`可以直接混用,完全兼容!
同时,你会发现因为没有统一的托管中心,无法使用类似`pod search`相似的命令进行索引,只能靠自己或者相其它平台的记录,而且framework有的缺点`Carthage`都有。
## 使用
两者的基本使用官方文档写的都非常清晰明了,满足了简单使用的需求,可以前往相关地址进行阅读,
[cocoaPods.org](https://cocoapods.org/)
[cocoaPods——GitHub](https://github.com/CocoaPods/CocoaPods)
[Carthage](https://github.com/Carthage/Carthage)
[Carthage使用教程](https://blog.csdn.net/Mazy_ma/article/details/70185547)
## 坑🗯
此处汇集了在使用第三方库管理工具中所遇到的坑!!!
### cocoaPods
### Carthage
1. 使用carthage时,链接库需要链接两个地方,否则会出现`dyld: Library not loaded: @rpath/SwiftyJSON.framework/SwiftyJSON Refere`。详见这篇博客[https://blog.csdn.net/asdf_2012/article/details/50800791](https://blog.csdn.net/asdf_2012/article/details/50800791)
## 组件化
### cocoapods
iOS开发中的组件化很多都是利用cocoapods进行的,就我目前经验所知,至少滴滴是这样的。现在我们先来迈出组件化的第一步,托管某个组件到cocoapods中。就不一一展开细说了,推荐[一篇博客](https://xcqromance.top/2017/08/25/2017-08-25/)
在此说一些踩到的坑:
1. **description必须要比summary长**。在`.podspec`文件中的千万要记得description中写下的内容要比summary长!!!
2. **source_files是最容易出现问题的地方**。因为经常会设置错库的路径,尤其是在对已有工程基础上添加,推荐在最开始`用cocoapods的pod lib create 'yourKit’`命令直接生成对应的库工程。
3. 每次有新改动后,都需要更新version。
4. `git tag -a 0.0.1 -m "testKit finish”` 通过该命令打tag,`git push origin 0.0.1:0.0.1`,通过该命令提交tag。注意两条命令中的tag值要对上。
5. **推完tag后也要把所有文件推到对应的git仓库里**。因为coocapods会在执行pod install时去对应的仓库里找’yourKit.podspec’文件。
6. **`[!] {"name"=>["is already taken”]}`**。当发布库时出现这个问题,那就是因为cocoapods的公开库中已经有了跟你这个库一样的名字,重新改名字再发布即可。
以上所有步骤都没问题后,看到terminal出现如下所示内容,就大功告成啦~
如果不放心的话,可以执行`pod search yourkit`命令,查查看到底有没有,当出现如下图所示内容时,那就是靠谱啦~
不过也有可能执行该命令后一样找不到,此时可以执行`pod setup`,执行完毕后再查一次即可,如果最后还是非常顽强的没找到,但是明明就是已经发布成功了,那就得把这玩意儿删掉`~/Library/Caches/CocoaPods/search_index.json`
新建一个测试工程,新建`podfile`文件,在该文件中写入对应依赖库,执行`pod install`,成功后如下所示,
集成好后,我们的测试工程目录应该长成这个样子,
使用过程.
================================================
FILE: Project/翻译——ViewsprogrammingGuideforiOS.md
================================================
原文地址:[https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/Introduction/Introduction.html](https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/Introduction/Introduction.html)
## iOS中的视图编程指导
在iOS中,我们会使用多个 `window` 和多个 `View` 在屏幕上展示你的应用程序内容。`Window` 本身不会包涵任何的可见内容,但提供了一个基础容器给我们应用程序中的 `view` 使用。`View` 定义了一部分你想要填入 `window` 中的内容。举个例子,你可以有用于显示图片、文本、形状或者以上内容的组合 `view`,甚至你还可以使用 `view` 去组织和管理其它 `view`。
### 概览
每一个应用程序至少拥有一个 `window` 和 `view` 去展示它的内容。`UIKit` 框架和其它系统框架预先定义了一些 `view` 供你去展示你的内容。这些 `view` 涵盖了简单的 `button` 和文本 `lable` 甚至是复杂的 `view`,比如 `table view`、`picker view` 和 `scroll view`。在这些预先定义好的 `view` 中如果没有提供你想要的 `view`,你可以自定义 `view` 并自己管理绘制和事件处理。
### `view` 管理你的应用程序可视内容
一个 `view` 是 `UIView` 类(或者它的一个子类)的实例并管理了一个应用程序 `window` 中的矩形区域。`view` 能够进行内容的绘制,处理多点触控事件和管理该 `views` 中子视图的约束。在一个矩形 `view` 区域中绘图会涉及到使用图像技术,比如 `Core Graphics`、`OpenGL ES` 或者 `UIKit` 去绘制形状、图片和文本。`view` 能够直接响应手势识别或触摸事件。在 `view` 的视图层级中,父视图负责其子视图的位置和大小,且还能动态的完成。父视图的这个动态修改的能力能够让你的 `view` 去适应条件的改变,比如说界面旋转和动画。
你可以认为 `view` 是构建你的用户界面中的一个砖块。你会经常的使用一系列的 `view` 去构建一个等级视图去展示内容,而不是只使用一个 `view`。每一个在等级视图中的 `view` 都表现了用户界面中的一部分,并且通常是用于优化一个具体的内容类型,例如,`UIKit` 有很多个用于优化图像、文本和其它类型内容的 `view`。
> 相关章节:[View and Window Architecture](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/WindowsandViews/WindowsandViews.html#//apple_ref/doc/uid/TP40009503-CH2-SW1),[Views](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingViews/CreatingViews.html#//apple_ref/doc/uid/TP40009503-CH5-SW1)
### 配合 `window` 显示你的 `view`
`window` 是 `UIWindow` 类的实例且能够处理全部呈现在应用程序用户界面上的内容。`window` 使用 `view`(及其视图控制器)来管理视图层级的交互和改变。在大部分的时候,你的应用程序 `window` 并不会发生改变。`window` 创建之后,它不会发生改变,只有通过它显示` view` 才会发生改变。每一个应用程序都至少拥有一个 `window` 在设备的主屏幕上去显示这个应用程序的用户界面。如果有外部显示设备连接到了这个设备,应用程序能够很好的在这个外部显示设备上创建第二个 `window` 去呈现内容。
> 相关章节:[Windows](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingWindows/CreatingWindows.html#//apple_ref/doc/uid/TP40009503-CH4-SW1)
### 动画提供了用户可见界面改变的反馈
动画提供了关于用户可见视图层级改变的反馈。系统定义了呈现模态视图(或单一视图)和位于不同组合视图之间的标准过渡动画。但是,`view` 的许多属性可以进行动画。例如,可以通过动画改变 `view` 的透明度,在屏幕中的位置,大小,背景颜色或者其它的属性。如果你直接使用底层的 `Core Animation` 层对象,你能够作出很多不错的动画。
> 相关章节:[Animations](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/AnimatingViews/AnimatingViews.html#//apple_ref/doc/uid/TP40009503-CH6-SW1)
### `Interface Builder` 扮演的角色
`Interface Builder` 是一个图形构造和配置你的应用程序 `window` 和 `view` 的工具。使用`Interface Builder` 能够把 `view` 集中在 `nib` 文件中进行处理,`nib` 是一个存储了你可以自由操控版本的 `view` 或其它对象资源的文件,当你在 `runtime` 中加载一个 `nib` 文件时,你可以使用你的代码去操纵这些位于真实对象中的 `view`。
当你不得不去进行创建应用程序界面的工作时,使用 `Interface Builder` 会变得非常简单 。因为在 iOS 中已经整合进了对 `Interface Builder` 和 `nib` 的支持,只需要一点时间就可以把你应用程序的设计工作合并到 `nib` 中。
关于如何使用 `Interface Builder` 的其它信息,可查看 [Interface Builder User Guide](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/IB_UserGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005344)。关于如何使用 `view controller` 管理包涵这些 `view` 的 `nib` 文件,可查看 [View Controller Programming Guide for iOS](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457) 中的创建自定义视图控制器部分。
### 查看更多
因为视图是非常复杂和灵活的对象,在一个文档中完全讲解是很困难的。但是,下面的这些文档能够很好的帮助你去学习关于如何管理视图交互和用户界面的内容。
* 视图控制器是管理应用中的视图很重要的一部分。视图控制器控制着在其单一视图层级之中的所有视图,并协助这些视图在屏幕上的显示。查看 [View Controller Programming Guide for iOS](https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457) 来获得更多关于视图控制器的内容以及它运行规则的信息。
* 视图是应用的手势和触摸事件的关键接收者。查看 [Event Handling Guide for iOS]() 来获得更多关于如何使用手势识别器和处理触摸事件。
* 自定义视图必须要使用可信赖的绘制技术去渲染其内容。查看 [Drawing and Printing Guide for iOS](https://developer.apple.com/library/archive/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010156) 来获得更多关于使用这些技术去绘制你的视图内容。
* 在一些标准视图动画无法满足的部分,你可以使用核心动画库 `Core Animation`。查看 [Core Animation Programming Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004514) 来获得更多关于使用核心动画库 `Core Animation` 实现动画的内容。
## `view` 和 `window` 的结构
`view` 和 `window` 展示应用程序用户界面和处理界面上的交互。`UIKit` 和其它系统框架提供了一些只需要做一点修改或完全不修改就可以使用的 `view`。你还可以自定义视图去展示标准视图不允许的位置内容。
### 视图结构的基础知识
我们想要通过可视化完成的事情大多都是基于 `view` 对象的,也就是 `UIView` 类的实例。`view` 对象在屏幕上定义了一个矩形区域,该区域能够绘制并和响应触摸事件。`view` 能够作为其它 `view` 的父容器,管理它们的位置和大小。`UIView` 类本身管理了这些工作的大部分,但我们也可以根据需要自定义视图之间的默认行为。
`view` 与 `Core Animation` 层共同处理内容的渲染和动画。每一个 `view` 都拥有一个 `layer` 对象(通常是 `CALayer` 的实例),它负责 `view` 的渲染内容和与 `view` 相关的动画。我们大多数情况下的操作应该通过 `UIView` 提供的接口进行,但在某些需要对渲染或动画行为进行更多的控制时,我们可以替换为对 `layer` 进行操作。
为了便于我们理解 `view` 和 `layer` 的关系,下面这个例子可以提供帮助。图 1-1 展示了来自于 [ViewTransitions](https://developer.apple.com/library/archive/samplecode/ViewTransitions/Introduction/Intro.html#//apple_ref/doc/uid/DTS40007411) 这个简单应用的视图结构和它与底层 `Core Animation` 层的关系。这个应用程序的视图包括一个 `window`(本质上是个 `UIView`),它是一个继承于 `UIView` 类且作为 `view` 容器的对象,一个图像 `view`,一个用于显示按钮的 `toolbar` 以及一个 `bar button item`(它不是 `view`,但其内部有一个可共使用的 `view`)。(实际上 [ViewTransitions](https://developer.apple.com/library/archive/samplecode/ViewTransitions/Introduction/Intro.html#//apple_ref/doc/uid/DTS40007411) 这个简单应用还包括了一个用于实现变换的图像 `view`,但为了保证简单,在图 1-1 中并没有描述出来)。每个 `view` 都有与之匹配的 `layer` 对象,可以通过 `view` 的 `layer` 属性来访问它。(因为 `bar buttom item` 不是 `view`,所以你不能直接访问它的 `layer`)`layer` 层对象的背后是 `Core Animation` 渲染对象,并且最终用于管理硬件缓冲区中显示在屏幕上的实际字节。

使用 `Core Animation` `layer` 对象对性能提升有很大的帮助。要尽可能的少调用视图对象的绘制部分代码,当这部分代码被调用时,其结果会被 `Core Animation` 进行缓存,并在未来进行多次重用。在需要更新视图时重用渲染出内容的会消耗大量的绘制周期。在动画中重用这部分缓存的内容是非常重要的,因为这些内容是可被操控的。使用这些缓存的内容会比创建新内容成本小很多。
### `view` 的视图层级以及管理子视图
除了提供自身显示的内容,`view` 还可以作为其它 `view` 的容器。当一个 `view` 包含了另外一个 `view`,父子关系就在其之中建立了。在这个父子关系中,子视图通常被称为 `subView` 父视图被称为 `superView`。这种关系类型的创建对应用程序的可视化内容和行为都造成了影响。
看上去子视图中的内容会遮挡其父视图的部分或全部内容,当子视图是非透明时,则其所占据的区域将会完全遮挡父视图。如果子视图是部分透明的,则父子视图中的内容会先进行融合再显示于屏幕上。每个 `superView` 都一个存储 `subView` 的数组,数组中元素的位置会影响每个 `subview` 的可见性。 当同为一个 `superView` 的两个 `subView` 互相重叠在了一起,最后添加进 `superView` 中的(或移动到 `subView` 数组最后的) `subView` 将会显示在在上面。
`superView` 与 `subView` 的关系还影响到了一系列的视图行为。改变一个父视图的大小时会波及到其子视图的大小和位置。当改变父视图的大小时,我们可以适当的通过一些配置方法来重新设置每个子视图大小。例如隐藏父视图,修改父视图的透明度或者对父视图的坐标系统做数学变换(3D)等这些事件同样会影响子视图。
对视图层级的排布还意味着应用程序如何响应各种事件。当一个特殊的视图发生了触摸,系统立即将这个触摸信息发送给这个视图进行处理。但是如果该视图没有对这个事件进行单独处理,则系统将会把该事件对象传递给父视图。如果父视图同样没有处理这个事件,系统将会把该事件继续传递给父视图,在整个响应链上以此类推。
更多关于如何创建视图层级的内容,可查看 [Creating and Managing a View Hierarchy](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingViews/CreatingViews.html#//apple_ref/doc/uid/TP40009503-CH5-SW47)。
## 视图绘制周期
`UIView` 类使用了“按需变化”的绘制模型去展示内容。当视图第一次呈现在屏幕上时,系统要求其进行内容绘制。系统对渲染内容进行快照捕获并且使用该快照作为该视图的视觉呈现。如果你从未对视图内容进行修改,执行视图内容绘制的代码将不会再执行。对视图进行的大多数操作会重复使用快照图片替代。如果你对内容进行了修改,需要通知系统视图已经修改。视图将会重复进行绘制视图内容的过程,并捕获新的图像内容为快照。
当你的视图内容修改时,你不必立即重绘这些修改的内容。相反,你可以使用 `setNeedsDisplay` 或 `setNeedsDisplayInRect` 方法使视图内容失效。这些方法将会告诉系统视图的内容已经修改,并且需要在下个时机中进行重绘。系统将会等待到当前 `runloop` 结束后才进行初始化任何的绘图操作。这个延迟给你一个机会去使多个视图内容失效,从视图层级中添加或者移除视图,隐藏视图,调整视图大小以及调整视图位置。你对视图进行所有修改都会在同一时间进行调整。
> 注意:修改视图的几何形状并不会自动触发系统对视图内容的重绘。视图的 `contentMode` 属性决定了如何解释视图的几何形状修改。大多数 `contentMode` 在延伸和重定位已经存在快照的边界,而不是创建一个新的快照。获取更多 `contentMode` 是怎么影响你的视图绘制周期的,可以查看 [Content Modes](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/WindowsandViews/WindowsandViews.html#//apple_ref/doc/uid/TP40009503-CH2-SW2)。
当渲染视图内容时,实际的绘制过程的变化会依赖于视图及其配置。系统视图一般通过实现私有绘图方法来完成视图内容的渲染。这些相同的系统视图经常会暴露出接口供我们配置视图的实际外观。自定义 `UIView` 类型的子类,通常你要针对你的视图去重载 `drawRect:` 方法去绘制你视图的内容。这还有一些其它方法提供完成内容的绘制,例如直接设置底层的内容,但是重载 `drawRect` 方法是目前最常用的方法。
获取更多关于如何绘制自定义视图的内容,可见 [ Implementing Your Drawing Code](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingViews/CreatingViews.html#//apple_ref/doc/uid/TP40009503-CH5-SW3)
## 内容模式(`content modes`)
每个视图都有一个内容模式,其控制了如何根据视图的几何尺寸发生变化时回收视图内容,以及是否回收视图内容。当视图第一次显示时,按照流程绘制其内容并捕获其内容于底层位图中。接下来改变视图几何尺寸并不会导致位图的重新创建。反而,`contentMode` 属性目的在于位图是否缩放以进行适配新的界限,还是仅简单的贴在一个视图的角或者边上。
视图的内容模式在你做以下操作时生效:
* 改变视图的 `frame` 或 `bounds` 矩形区域的宽或高。
* 赋值带有缩放功能的 transform 变换给视图的 `transform` 属性。
默认情况下大多数视图的 `contentMode` 属性是 `UIViewContentModeScaleToFill`,它能够让视图内容缩放适配至新的 frame 大小。图 1-2 展示了一些可用的内容模式结果。从图中可以看到,不是所有的内容模式都会完全充满视图界限,并且有一些还会导致视图内容的失真。

内容模式对于视图回收非常适合,但如果你特别想在缩放或者重设尺寸操作时自定义视图重绘它,还可以使用 `UIViewContentModeRedraw` 该内容模式。给视图模式设置为该值能够当几何尺寸改变时强制让系统调用 `drawRect:` 方法。一般情况下,应该尽可能的避免在任何时候使用该值,除非你非常确定你不会使用标志系统视图。
查看更多关于可用的内容模型内容,可看 [UIView Class Reference](https://developer.apple.com/documentation/uikit/uiview)。
## 可伸缩的视图
你可以指定视图的一部分作为可伸缩区域,以至于当视图改变其大小时只修改了可伸缩区域。通常会在按钮或者其它视图中使用可伸缩区域,其中视图的部分区域定义了一个可重复模式。视图中指定的可伸缩区域允许在单独一个轴或者两个轴上进行伸缩。需要注意的是,当对两个轴进行伸缩,对视图的边来说还要定义一个重复模式避免任何变形。图 1-3 清晰的展示了失真是怎么在视图中展示出来的。来自原图中每一个像素点的颜色重复的通过一致的排列充满了大图。

使用 `contentStretch` 属性指定视图的可伸缩区域。这个属性接受一个矩形区域,该值是 0.0 到 1.0 范围内的标准值。当伸缩视图时,系统将会对视图当前的 bounds 值和缩放值与该标准值进行乘积,以达到确定对视图一个像素或多个像素的伸缩。使用标准值能够降低当视图界限改变时更新 `contentStretch` 属性值的次数。
视图的内容模式还扮演了如何使用视图的伸缩区域决定者的角色。当内容模式导致内容区域的缩放时才会使用可伸缩的区域。这意味着可伸缩视图只支持 `UIViewContentModeScaleToFill`、`UIViewContentModeScaleAspectFit` 和 `UIViewContentModeScaleAspectFill` 内容模式。如果你指定了内容模式对内容进行了贴边或贴角(因此实际上导致内容并不能缩放),该视图将会忽略可伸缩区域。
> 注意:指定背景视图时,建议创建可伸缩的 `UIImage` 对象时使用 `contentStretch` 属性。可伸缩视图可以在 Core Animation Layer 中完全处理,其通常提供更好的性能。
## 内置动画支持
每一个视图后都有一个层对象的好处之一是你可以对与视图相关的更改进行动画化。动画是向用户传递信息交流的有效方法,在设计应用程时应该一直思考这个问题。许多 `UIView` 的属性都是可动画化的,也就是说支持半自动的从一个值到另外一个值。提高这些其中一个可动画化属性的性能,我们只需要做:
1. 告诉 UIKit 你想要一个高性能动画。
2. 改变属性值。
你可以对 `UIView` 对象中执行动画的属性如下:
* `frame` -- 使用这个属性能够对视图位置和大小的改变进行动画变化。
* `bounds` -- 使用这个属性能对视图大小的改变进行动画变化。
* `center` -- 使用这个属性能对视图位置的改变进行动画变化。
* `transform` -- 使用这个属性能够对视图选择和缩放。
* `alpha` -- 使用这个属性能够修改视图进行透明度的。
* `backgroundColor` -- 使用这个属性能够修改视图的背景颜色。
* `contentStrech` -- 使用这个属性能够修改视图的中心延伸。
从一组视图变换到另外一组视图是动画非常重要的一点。通常情况下会使用视图控制器通过动画去管理用户界面各部分中主要修改的部分。举个例子,展示从高级别到低级别视图的过渡信息,通常情况下会使用导航控制器管理每一个成功显示的视图数据。甚至你还可以创建两个视图集合之间的动画过渡来替代视图控制器。在使用标准控制器动画达不到你想要的结果时,可以通过这个方法做到。
除了使用 UIKit 进行动画的创建外,还可以使用 Core Animation layer 完成。下降到图层级别能够对动画的时间和属性进行更多的控制。
查看更多关于如果提升基于视图的动画性能可看 [Animations](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/AnimatingViews/AnimatingViews.html#//apple_ref/doc/uid/TP40009503-CH6-SW1)。查看更多关于如何使用 Core Animation 创建动画可看[Core Animation Programming Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004514) 和 [Core Animation Cookbook.](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreAnimation_Cookbook/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005406)。
## 视图的几何形状和定位系统
UIKit 中默认的坐标系统的原点在左上角,并且坐标系从原点向下和向右进行延伸。坐标值使用浮点数表示,它能够进行精确的约束且可让定位的内容不管其下的屏幕分辨率约束。图 1-4 展示了这个相对于屏幕的坐标系统。除了屏幕坐标系统,窗体和视图都定义了它们自己的本地坐标系统,本地坐标系统允许你指定相对与源窗体或源视图的坐标来替换掉相对与屏幕的坐标。

================================================
FILE: Qt/C++.md
================================================
## C++ 拾遗
## 为什么 main 函数要 return 0?
在大多数系统中,main 的返回值被用来指示状态。返回值 0 表明成功,非 0 的返回值的含义由系统定义,通常用来指出错误类型。
## do {...} while(0)
可能在一个方法中会出现 ABCD 四段代码块,其中 BC 两块有虚拟的包含关系,B 执行不了 C 也不执行,直接到 D。不用 do-while 也能解,就是不好看罢了。
## 带符号类型和无符号类型
带符号类型可以表示正数、负数或 0,无符号类型仅能表示大于 0 的值。
## 创建一个 C++ 对象何时用 new 何时不用?
一般来说,通过 `new` 关键词创建出的对象可以超出当前逻辑作用域,除非手动 delete 释放,否则不会自动释放。而通过类型 `Object obj;` 方式初始化的对象其生命周期终止与当前作用域。
## 顶层 const 和底层 const 之分
`const int i` 为顶层 const,i 的值无法被改变。
`int* const i` 为顶层 const,i 的值也无法改变,但可以修改指针所只指向的地址。
`const int* i` 为底层 const,i 的值可以改变,但无法修改指针所指向的地址。
综上,const 最近修饰的内容不可变。
## C++11 里的 const 和 constexpr
`const` 只作为“只读”。
`constexpr` 只作为“常量”。
## std::string size()
size 函数返回值的类型是 `size_type`,从 C++ Primer 书中看到的推测是一个无符号类型的值,因此绝对不可以与一个有符号且可能未负数的值进行比较,否则会出现明明比它大,却比它小的情况。
```c++
int a = -1;
std::string b = "123";
if (b.size() < a) {
std::cout << "woc?"; // 会打印出 woc?
}
```
因此,当我们需要定义一个变量作为遍历或取 `std::string` 类型里的值时,可以把该变量类型定义为 `std::size_type` 类型,可以保证肯定不会出现小于 0 的场景。
## std::string 相加
当两个变量其中一个明确为 `std::string` 类型时,可以通过 + 运算符进行相加操作,若两个变量都通过字面量的方式进行相加,则是非法的,因为历史原因,也为了和 C 兼容,C++ 里的字符串字面量并不是标准库 `std::string` 类型。
## std::vector
是模板而不是类型,其为 C++ 的“类模板”。
所有使用了迭代器的循环体,都不要向迭代器所属的容器增删元素。
## 在类的成员函数后加 const
当我们明确外部调用某些成员函数不可修改类内成员变量的内容时,可以通过在对应的函数声明后添加 const 关键字来告诉编译器,该方法不允许修改任何类内成员变量。
## class 和 struct 的区别
只有一个,默认的访问权限。如果我们明确了定义类的所有成员都是 public 的,则可以使用 struct。
## 如何声明一个使用默认构造函数初始化的对象?
```c++
Object obj; // 正确
Object obj(); // 错误,初始化了一个函数
```
## 类的静态成员变量
当一个变量只存在这个类内,但该类又会创建出多份,且都会使用同一个该变量,这种情况下可以使用静态成员变量。
## 迭代器的删除
对于关联容器,删除当前的 iterator,仅仅会使当前的 iterator 失效,只要在调用 erase 时,递增当前的 iterator 即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。
对于顺序容器,删除当前的 iterator 会使后面所有元素的 iterator 都失效。这是因为 vector 、deque 使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。不过 erase 方法可以返回下一个有效的 iterator 。
## std::string_view
C++17 引入`std::string_view` 来优化string的性能。`string_view` 本身不own内存,它只维护了一个指针和长度。而 `string_view` 仅存储了对原始数据的引用,该过程不涉及任何复制操作,所以它在处理大字符串时,性能上要比 string 更优。
```c++
#include
static const string_view str = "HELLO ByteDance!"; // 没有任何开销
char foo() {
return str[2];
}
等价
#include
char foo() {
return 'L';
}
```
使用`string_view`的注意事项:
- `std::string_view` 可以与`std::string`互相转换,但要注意`string_view`的生命周期问题。由于`std::string_view`并不持有字符串的内存,所以它的生命周期一定要比源字符串的生命周期长,源字符串被消毁,行为未定义。
```C++
std::string_view PrintStringView() {
std::string s = "How are you..";
std::string_view str_view = s;
return str_view;
}
// 行为未定义,悬垂指针
std::cout << "PrintLocalStringView: " << PrintStringView() << std::endl;
```
- `std::string_view`并不提供修改其引用的字符串的方法。任何尝试修改string_view引用的字符串的操作都可能导致未定义的行为。
- 与其他字符串类型不同,应该按值传递`string_view`,就像传递int或double一样,因为`string_view`是一个小值。
- `string_view`不一定以null结尾。因此,使用printf函数输出`string_view`是不安全的:
```c++
printf("%s\n", sv.data()); // DON’T DO THIS
// 可以像string或const char*使用<<输出string_view:
std::cout << "Took '" << s << "'";
```
## 尽量提前使用reserve/resize来避免不必要的内存分配
对于vector和string,增长过程是这样来实现的:每当需要更多空间时,就调用与realloc类似的操作。这一类似于realloc的操作分为四个步聚:
1. 分配一块大小为当前容量的某个倍数的新内存。在大多数实现中,vector和string的容量每次以2的倍数增长,即,每当容器需要扩张时,它们的容量即加倍。
2. 把容器的所有元素从旧的内存拷贝到新的内存中。
3. 析构掉旧内存中的对象。
4. 释放旧内存。
```C++
// Bad Code !!!
std::vector container;
for (int i = 0; i < 1000; ++i)
{
container.push_back(i);
}
改为:
std::vector container;
container.reserve(1000);
for (int i = 0; i < 1000; ++i)
{
container.push_back(i);
}
```
## 容器元素
元素拷贝:当向容器中加入对象时,存入容器的是你所指定的对象的拷贝。当从容器中取出一个对象时,你所得到的是容器中所保存的对象的拷贝。进去的是拷贝,出来的也是拷贝(copy in, copy out)。
所以如果容器存储大对象,一般都会存指针以提升性能(指针的拷贝比对象拷贝代价少)
元素析构:STL的容器自身被析构时,它们会自动析构容器内所包含的每个对象。如果容器内的元素是通过new创建的,那么在释放容器的时候,指针的“析构函数”不会做任何事情,因此new所创建的内存就不会释放掉。如果不手动delete掉,那么就会造成内存泄漏。
为了防止内存泄漏,最简单的方法就是通过delete释放防止内存泄漏
但是这种方式仍存在一个问题:如果在new操作和delete操作之间程序抛出了异常导致程序终止,那么delete语句将永远不会执行,同样也会产生内存泄漏。
使用智能指针来保证内存不会泄露,特别是map的value如果存储大对象,使用智能指针可提升性能,指针的拷贝比对象拷贝代价少,因此建议存储为map>,这样既能保证指针高性能,又不需要担心erase的析构的问题。
与析构类似,remove操作也需要注意:当容器中存放的是new分配对象的指针时,应该避免使用remove和remove_if。如果容器中存放的不是普通指针,而是具有引用计数功能的智能指针,那么就可以直接使用erase-remove的习惯用法。
================================================
FILE: Qt/UI.md
================================================
## 鼠标
## onActivChanged
该事件只要鼠标光标在非目标 `Item` 区按下即可收到信号,与之前的 `onClicked` 这种鼠标光标按下再抬起才接收到信号的事件不同。
### 悬浮
```qml
MouseArea{
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
```
`cursorShape` 可以修改鼠标光标移动到 `Item` 上时的样式
## 布局
### GridView
使用 `gridView` 时需要给对应的 delegate 包一层 `Component`,这样才能在其中拿到对应数据源子项的 model,否则会提示找不到 model。
```qml
Component {
id: item
ImageBrowserItemView {
id: itemView
coverUrl: asset.fileUrl
}
}
GridView {
id: gridView
model: viewModel
delegate: item
cellWidth: 80
cellHeight: gridView.height
anchors.fill: parent
}
```
================================================
FILE: Qt/base.md
================================================
## 内存管理
### Qt 对象树内存管理机制
Qt 是一个基于 C++ 的跨平台 GUI 框架,C++ 本身是没有自动垃圾回收机制的,但 Qt 为了解决 C++ 中大量重复性的 `new` 和 `delete`,引入了“对象树”的“半自动内存管理机制”。在 Qt 中所有的 C++ 类如果想要利用上 Qt 的对象树内存管理机制,需要派生自 `QObject`。`QObject` 中有 `parent` 字段标记了其父对象指针,以及 `list` 容器对象保存了所有子对象指针。如果一个对象的 `parent` 非 0,则该对象 `parent` 在执行析构时会析构该对象。
可以手动设置一个对象的 `parent` 对象,需要注意二者需要在同一线程。
### 利用智能指针优化内存管理
```C++
#include
#include
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QLabel *label = new QLabel("PJHubs");
label->show();
return app.exec();
}
```
在 QtWidgets 下这是一份非常典型的内存泄露代码,修复该问题可以有:
* 将 label 对象分配到 stack 中而不是 heap(堆)中。
* 给 label 设置标记位 `Qt::WA_DeleteOnClose`。
* 手动调用 `delete`。
* 使用智能指针。
前三种比较熟悉了,而使用智能指针可以写出以下逻辑:
```C++
std::auto_ptr label(new QLabel("PJHubs"));
```
详见[这篇文章](https://blog.csdn.net/L_Andy/article/details/107645633)。
## 线程/异步
### QThread
不要使用 `run` 方法来完成,对创建出线程对象和操作对象当前所处线程有要求,直接通过 `moveToThred` 方式创建,并可采用 `QMetaObject::invokeMethod` 注入线程操作。
### QFuture
* 基于 `Qt::Concurrent` 实现。
* QFuture 运行线程同步的获得一个或多个在将来某个时间点才准备好的结果。
* 这些结果可以时任何具有默认构造函数和拷贝构造函数的类型。
* 如果在调用该类的 `result()`、`resultAt()`、`results()` 函数时某个结果还不可用,QFuture 会等待直到结果可用。
* 可用使用 `isResualtReadyAy()` 来判断某个结果是否已经准备好。
* `waitForFinished()` 函数调用会导致调用线程阻塞来等待异步计算结束,以确保所有的结果都是可用的。
`QFuture` 不支持信号和槽,`QFutureWatcher` 可支持,可读取到任务状态。
工程里没有搜到用该方式的地方,可参考文章:
* [使用QFuture类监控异步计算的结果](https://blog.csdn.net/Amnes1a/article/details/65630701)
* [QFuture的使用:多线程与进度条](https://blog.csdn.net/gongjianbo1992/article/details/106957888/)
================================================
FILE: Qt/crossPlatform.md
================================================
## 本地构建 Qt
去 [download.qt.io](https://download.qt.io/archive/qt/) 下载对应的 Qt 源码后,可用通过该[文档中的方式](https://wiki.qt.io/Building_Qt_6_from_Git)本地自行构建 Qt 二进制。
直接执行 `./configura` 可能回到下图中的问题:

原因很奇怪,说是对应的 ninja 版本太老,但纳闷的是这 ninja 不是集成在内部的嘛,刚开始以为真的是 ninja 版本的问题所以还是去到 github 下了一份新版 ninja,但问题依旧,仔细想了想,还是决定先把关联出错的 `qtwebengin` 组件给移除了,遂成行。
最终执行 `cmake --build .` 后开始了漫长的等待,应该一开始就把不需要的 qt 组件全都给移除,build qt 的过程实在是漫长。

最后再执行 `cmake --install .` 即可拿到二进制包,集成只 Qt Creator 或 CMakeLists 中写明版本即可。
### 通过 MaintenaceTool 工具安装
可以下载到完整的二进制包,避免本地 build 过程耗时将近 2 小时。
## 路径转换
因为历史原因 win 平台上文件路径都是以 `/` 反斜杠切割,除此之外的所有系统均以 `\` 正斜杆来切割。在 Qt 中为了解决不同平台之间的路径切割符的问题,可用使用 `QDir::toNativeSeparators()` 方法进行转换
================================================
FILE: Qt/opt.md
================================================
## processEvents
`QCoreApplication::processEvents()` 可以解决 `while(1)` 这种卡死当前线程的事情
有些情况下,我们并不想卡死 UI 线程的操作,但又必须等待某件事的完成,用户此时可以选择不等待直接关闭程序或停止该逻辑的执行。在 Qt 中可以使用 `processEvents` 进行。详见[文章](https://www.qter.org/forum.php?mod=viewthread&tid=1838)
## Loader
Loader 用于延迟加载组件,例如登录等,详见[文章](https://www.cnblogs.com/linuxAndMcu/p/11960251.html)
================================================
FILE: Qt/project.md
================================================
## 工程配置
### 使用 VS 进行开发
#### 设置 Qt 版本路径
如果默认使用 Qt Creator 进行开发毫无问题,但都已经使用 win 了不好好利用上 vs 实在是浪费。在 vs 中下载好对于的 qt 拓展插件后,extensions 中找到 qt tools,选择 msvs 版本的构建工具路径即可。

#### 构建 Qt 程序
**CMake 生成 VS 工程**
下载好 msvs 构建工具后,执行 `cmake ..` 默认通过 msvs 构建工具生成 vs 工程。
**VS 直接打开 CMake 工程**
VS - file - Open - CMake
**设置 Qt SDK 索引**
这里比较奇怪,如果我们使用的时 Qt Creator 进行开发,创建好工程后一路顺畅直接 build 即可,而且工程目录下也没有生成任何的冗余 IDE 相关文件,但生成 vs 工程后会多出大量的 vs IDE 相关的文件不说,此时直接跑 CMakeLists.txt 会提示需要添加 Qt6 目录。
`set(CMAKE_PREFIX_PATH your_qt_file_path/6.2.4/msvc2019_64)`
**自动补齐依赖**
自动补齐 Qt app 运行时依赖的 dll 动态库
`qt_file_path/bin/windeployqt.exe your_app_fila_path/apptest.exe`

等待一会儿后,对应的 `out` 目录下补齐了一堆缺失的动态库,此时就可以正常通过 vs IDE 来愉快的进行 Qt 开发了。

## CMake 自动索引文件
`file(GLOB_RECURSE sources *.cpp)`
### qt_add_qml_module
使用 qt6.2 新增的 qml module 管理 qml 时,需要注意 qml 文件路径,若直接通过给 `QML_FILES` 塞入 `file(GLOB_RECURSE )` 读取到的 qml 文件 list 变量,会因为路径问题报错,需要去除掉路径中的工程目录名前缀。
```cmake
file(GLOB_RECURSE qmls *.qml)
foreach(filepath ${qmls})
string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/" "" filename ${filepath})
list(APPEND qml_files ${filename})
endforeach(filepath)
qt_add_qml_module(appImageEditor
URI ImageEditor
VERSION 1.0
QML_FILES ${qml_files}
)
```
可参考[文章](https://zhuanlan.zhihu.com/p/488065537)。
================================================
FILE: README.md
================================================
## 前言
这是我在学习iOS开发相关内容过程中的总结,包括在日常coding、工作中、灵光一闪后的想法实践等都会在此进行分析总结。欢迎star、fork、PR。[一篇老文](./Others/简介.md),当初开这个库时的一些想法。
## 文化
类型 | 文章
---- | ----
Apple Company | [苹果公司的历史](./文化/2_Apple_History.md)
macOS | [macOS的历史](./文化/3_Mac_OS_X.md)
iOS | [iOS的历史](./文化/4_iOS.md)
## 基础知识
类型 | 文章
---- | ----
网络 | [网络相关知识](./基础知识/网络相关知识.md)
操作系统 | [操作系统相关知识](./基础知识/操作系统.md)
算法 & 数据结构 | [algorithm - java](./基础知识/algorithm-java.md)
算法 & 数据结构 | [nowCoder](./基础知识/nowCode.md)
算法 & 数据结构 | [LeetCode](./基础知识/leetCode.md)
语言 | [C++](./基础知识/C++.md)
语言 | [Python](./基础知识/python.md)
UML | [UML](./基础知识/UML.md)
## iOS
类型 | 文章
----- | -----
短代码 | [iOS 短代码](./iOS/code.md)
基础知识 | [基础知识](./iOS/basic.md)
Swift |[OC 转 Swift 注意点](./iOS/Swift/OC转Swift.md)
Swift | [Swift 注意点](./iOS/Swift/Swift注意点.md)
Swift | [Swift 构造器](./iOS/Swift/构造器.md)
Objective-C | [Objective-C 注意点](./iOS/Objective-C/Objective-C注意点.md)
animation & UIKit | [More - 弹幕](./iOS/More-弹幕.md)
UITableView | [UITableView 相关使用总结](./iOS/UITableView.md)
UICollectionView | [UICollectionView 相关使用总结](./iOS/UICollectionView.md)
Today Extension| [Today Extension](./iOS/Today_Extension.md)
Objective-C & Swift | [并发编程](./iOS/Objective-C/并发编程.md)
Objective-C | [More - 页面传值](./iOS/Objective-C/More-页面传值.md)
Objective-C | [一个 Ping 工具 ](./iOS/Objective-C/ping.md)
Objective-C | [More - iOS 上的相机](./iOS/Objective-C/More-iOS上的相机.md)
Objective-C | [More - iOS 国际化一站式解决方案](./iOS/Objective-C/More-iOS国际化一站式解决方案.md)
Objective-C | [More - 设计模式](./iOS/Objective-C/More-DesignPattern.md)
Objective-C | [More - 音频相关](./iOS/Objective-C/More-Audio.md)
Objective-C | [More - 视频相关](./iOS/Objective-C/More-视频相关.md)
Objective-C | [runtime](./iOS/Objective-C/runtime.md)
Swift & Objective-C | [系统相关](./iOS/Objective-C/系统相关.md)
Swift | [tips-自定义tabBar大加号引发的思考](./iOS/Objective-C/tips-自定义tabBar大加号引发的思考.md)
Swift | [Cache](./iOS/Swift/Cache.md)
Swift & Objective-C | [自定义NavigationBar](./iOS/Swift/自定义NavigationBar.md)
Swift | [PJBreedsViewController 开发总结](./iOS/Swift/品种选择器总结.md)
Swift | [PJPickerView 开发总结](./iOS/Swift/PJPickerView开发总结.md)
Swift | [PhotosKit开发总结(一)](./iOS/Swift/PhotosKit开发总结(一).md)
基础知识| [Layout](./iOS/Layout.md)
Swift | [SwiftUI](./iOS/Swift/SwiftUI.md)
Swift | [有趣的代码段](./iOS/Swift/code.md)
UI | [UI 相关](./iOS/UI.md)
Swift | [Core Data](./iOS/Swift/CoreData.md)
Swift | [Playgrounds](./iOS/Swift/Playgrounds.md)
## macOS
类型 | 文章
---- | ----
编译原理 | [macOS开发(词法分析器)](./macOS/macOS开发(词法分析器).md)
git | [一台设备多个git账号](./macOS/一台设备多个git账号.md)
Crash | [解析 crash log(一)](./macOS/crash.md)
Playground | [来一次完整的使用 Playground(一)](./macOS/playground.md)
Kindle | [Kindle 开发](./macOS/kindle.md)
性能优化 | [iOS 和 macOS 性能优化](./macOS/performance.md)
## Android
类型 | 文章
---- | ----
基础 | [基础知识](./Android/基础知识.md)
基础 | [问题汇总](./Android/问题汇总.md)
## Back - end
类型 | 文章·
---- | ----
总结 | [后端相关知识总结](./Back-end/后端学习.md)
Django | [Django学习](./Back-end/django.md)
Web | [ Web 服务器](./Back-end/web服务器.md)
RESTful | [RESTful ](./Back-end/RESTful.md)
Docker | [Docker ](./Back-end/Docker.md)
Mysql | [Mysql ](./Back-end/mysql.md)
Vapor | [Vapor ](./Back-end/Vapor.md)
Vapor | [Vapor ](./Back-end/jwt.md)
数据库 | [Vapor ](./Back-end/DB.md)
## Front - end
类型 | 文章
---- | ----
基础概念 | [基础知识](./Front-end/basic.md)
总结 | [前端学习总结](./Front-end/前端学习.md)
总结 | [图解HTTP学习笔记](./Front-end/图解HTTP学习笔记.md)
总结 | [FCC 学习笔记](./Front-end/FCC.md)
JS | [JavaScript 学习笔记](./Front-end/JavaScript.md)
CSS | [CSS 学习笔记](./Front-end/CSS.md)
总结 | [使用 Vue 实现 Context-Menu 的思考与总结](./Front-end/vue-context-mune.md)
总结 | [Vue 学习笔记](./Front-end/Vue.md)
## Flutter
类型 | 文章
---- | ----
Dart | [Dart 学习](./Flutter/Dart.md)
Flutter | [Flutter 问题汇总](./Flutter/Flutter问题汇总.md)
Flutter | [Flutter 二探](./Flutter/Flutter_2.md)
Flutter | [Flutter 三探](./Flutter/Flutter_3.md)
## React-Native
类型 | 文章
---- | ----
React-Native | [React-Native记(〇)](./React-Native/React-Native记〇.md)
React-Native | [React-Native记(一)](./React-Native/React-Native记(一).md)
React-Native | [React-Native记(二)](./React-Native/React-Native记(二).md)
## Weex
类型 | 文章
---- | ----
Weex | [Weex新手记](./Weex/Weex新手记.md)
## 小程序
类型 | 文章
---- | ----
小程序 | [小程序初探](./小程序/小程序初探.md)
小程序 | [小程序初探(二)](./小程序/小程序初探(二).md)
## Cocos
类型 | 文章
---- | ----
基础 | [Cocos 基础](Game/Cocos/basic.md)
## 图形学 & 视觉
类型 | 文章
--- | ---
basic | [基础知识](CV/basic.md)
## 区块链
类型 | 文章
--- | ---
basic | [基础知识](Blockchain/basic.md)
## AI
类型 | 文章
---- | ----
基础概念 | [相关领域涉及到的基础知识](./AI/basic.md)
## NLP
类型 | 文章
---- | ----
基础概念 | [基础知识](./NLP/NLP.md)
## ruby
类型 | 文章
--- | ---
基础概念 | [ruby 基础](./ruby/basic.md)
## Qt
类型 | 文章
--- | ---
基础 | [Qt 基础相关](./Qt/base.md)
UI | [UI](./Qt/UI.md)
跨平台 | [跨平台、本地编译 Qt 等](./Qt/crossPlatform.md)
优化 | [性能、UI 优化等](./Qt/opt.md)
C++ | [技巧、难点等](/Qt/C%2B%2B.md)
工程配置 | [工程配置](/Qt/project.md)
## 测试
类型 | 文章
--- | ---
单元测试 | [iOS 中的单元测试](./测试/单元测试.md)
## 书籍
类型 | 文章
---- | ----
面试 |[iOS 面试之道](./书籍/iOS面试之道.md)
## 教程
类型 | 文章
---- | ----
新生实践 | [剪刀石头布](./教程/剪刀石头布.md)
## 相关工具使用
类型 | 文章
---- | ----
杂烩 | [百家汇](./工具使用/2_百家汇.md)
GitHub | [GitHub](./工具使用/3_GitHub.md)
Xcode | [Xcode](./工具使用/Xcode.md)
## 其它
类型 | 文章
---- | ----
工作 | [面试准备](./Others/面试准备.md)
工作 | [招一个靠谱的iOS实习生(附参考答案)](./Others/招一个靠谱的iOS实习生(附参考答案).md)
链接 | [ 开发中可能会用到的](./工具使用/开发中可能会用到的内容.md)
笔记 | [图解 HTTP 学习笔记](./Front-end/图解HTTP学习笔记.md)
笔记 | [ coding-interview-university学习笔记](./项目/coding-interview-university学习笔记.md)
源码分析 | [ONEUIKit-ONEProgressHUD](./项目/ONEUIKit-ONEProgressHUD.md)
翻译 | [ Views programming Guide for iOS](./项目/翻译——ViewsprogrammingGuideforiOS.md)
总结 | [我的大学——实习生涯](./Others/myinterview.md)
## 项目
类型 | 文章
---- | ----
项目管理 | [第三方库管理](./项目/第三方库管理.md)
上架 | [上架的相关内容](./项目/上架.md)·
PLook | [PFollow 开发相关问题汇总](./项目/PFollow.md)
PLook | [ PLook 开发相关问题汇总](./项目/PLook.md)
Bonfire | [ Bonfire 开发相关问题汇总](./项目/Bonfire.md)
iBistu | [iBistu 4.0开发总结(先导篇)](./项目/iBistu4-0(先导篇).md)
iBistu | [iBistu 4.0开发总结(新闻)](./项目/iBistu4-0(新闻).md)
iBistu | [iBistu 4.0开发总结(黄页)](./项目/iBistu4-0(黄页).md)
iBistu | [iBistu 4.0开发总结(地图)](./项目/iBistu4-0(地图).md)
iBistu | [iBistu 4.0开发总结(失物)](./项目/iBistu4-0(失物).md)
Cocos Creator | [CocosCreator - 方块弹球](./项目/CocosCreator——方块弹球.md)
================================================
FILE: React-Native/React-Native记〇.md
================================================
emmm,这是我今年的第三件事。微信小程序的学习告一段落,开始正式进入RN的学习,RN到现在发展了将近三年,业界有对其有非常大的赞誉,而且现目前不管是整体架构成熟度还是社区的活跃度都非常靠前,可以说是杀入的好时机(其实已经很晚了。)正适合我这种吃不了第一口肉的人。
之所以要学习RN有这么几点原因,其一是有即将开始的项目;其二同样是观望了RN很久,一直很想投身学习;其三就是拓展自己的技能树了。🙂。这次折腾了一下RN的开发环境,刚开始以为会非常顺利,一步到位,实际上却把自己坑得不浅,在此记录一番,给各位也想杀入RN的同学提个醒,不要把时间再次浪费。
在正式进入开发之前,需要我们本地环境上已经安装好Node.js,而安装Node.js有两种方式,你可以去[官网](https://nodejs.org/en/)上下载安装包,直接点击解压出的pkg即可,另外一个方式通过Homebrew(Mac),Homebrew是一款Mac下的一款非常强大软件包管理工具,如果之前没安装过的话,可以通过这行原汁原味的官方安装方法搬运,
```shell
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
```
安装好Homebrew后,我们通过Homebrew来一步下载node,
```shell
$ brew install node
```
RN官网上是这么写的,但我下载下来的node中并没有顺便也把npm也给一块儿下了(npm是管理node.js的包管理工具),可能每个人的环境不一样?还是我下的node版本太低,没自带npm,如果你发现通过直接install node下载好的node跟我一样也没有npm,先卸载掉之前安装的npm,
```shell
brew uninstall node
```
随后再补充参数,
```shell
$ brew install node --with -npm
```
当然,如果你是从官网上下载手动安装的话,就不用管这个问题了。此时可以执行**node -v和npm -v**来查看这两个东西是否都安装完毕。
接着,我们再通过npm来安装react-native,
```shell
$ npm install -g react-native-cli
```
官网上说,如果你在安装过程中遇到提示npmlog模块找不到的情况,可以先尝试这么做,(我没遇到过)
```shell
$ curl -0 -L https://npmjs.org/install.sh | sudo sh.
```
根据官网的建议,我们还需要再安装[watchman](https://github.com/facebook/watchman),它的最大作用是用来监听文件状态的改变,也就是当被管理的目录下发生任何文件的增删改查它都会进行监听记录,使用它能够加速每次RN工程目录发生变化时再编译的过程,其实咱们不使用watchman也是OK的,无非就是等嘛,RN工程大了以后每次编译就出去散个步嘛。加了watchman后,针对比前后两次提交编译的请求,只编译有改动的文件,这样能够减少编译所耗费的时间。RN推荐的做法是,装,
```shell
brew install watchman
```
以上内容都完成后,此时我们RN的本地开发环境都已经弄好了。我们来创建一个工程,
```shell
$ react-native init '你的工程名字'
# 此处就以pjhubsssssss为例 #
```
此时,我们会等待一段巨特么长(长到我要骂人了)的时间,等到项目新建完了以后,我们可以看到以下的目录结构,
**\__tests\__:**RN的单元测试文件夹;
**android:**Android工程;
**ios:**iOS工程;
**node_modules:**RN的模块依赖管理(可以认为是Maven或pod);
**App.js:**相当于对应平台下的MainActivity.java或ViewController.m;
**app.json:**RN工程的一些配置,比如工程名等;
**index.js:**目前我对这个理解是一个bridge,Android和iOS工程分别对其读取签名。
**package-lock.json和package.json:**都是npm用来管理RN的。
工程初始化后,我们此时可以如此这般运行demo
```shell
$ react-native run-ios
```
执行完命令后,会再次等到一次又特么巨长的编译时间(跟Native对比起来差距真的太明显了)。等了一会,我们会发现跑起了一个iPhone 6的模拟器。iOS工程demo就这么顺利的运行起来了,接下来,我们来好好的整一整Android。🙂
既然运行iOS工程是run-ios那么运行Android工程就一定是run-android了,看了一眼官方文档,要求我们先把模拟器运行起来。打开我一年前下的Android studio,emmmm,它开始提示我要更新一堆东西了,刚开始没管,直接去把模拟器打开,发生了各种迷之错误(心里开始怀念Xcode的各种好),比如Gradle版本、SDK位置错误等等,最后不行了,重新去下了3.0.1,所有问题全部解决。🙂
运行模拟器后执行,
```shell
$ react-native run-android
```
此时发生了一个错误,
```shell
java.lang.RuntimeException: SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.
```
意思就是在local.properties这个文件中没找到ANDROID_HOME,也即android SDK。解决的办法就是在Android工程目录下的local.properties文件中添加对应的android SDK路径,
```shell
sdk.dir = /Users/incloud/Library/Android/sdk
# 供参考
```
此时再执行run命令,即可看到下载了对应工程名的apk已经下载到了应用程序列表里,并且还会自动起一个终端记录对应的操作log。是的,iOS是直接帮你run起来,而Android是给你load到应用程序列表里,你自己去翻吧。2333
如果此时你并未关闭这个终端,而再次新建了一个RN工程,并且还run了它,那么会告诉你端口被占用,因为RN默认端口是8081,我们可以这么做来更改端口号,
```shell
react-native start --port 8083
```
这个问题解决后,它可能还会告诉你'...xxx has been register...',刚开始出现这问题,我一直以为是iOS或Android工程的签名没弄好,苦恼了好久,最后没想到居然是因为上一个工程自动起的进程终端没关闭,导致下一个新建的工程运行后判断log进程终端还在,就套用了之前的终端,就导致签名冲突。
可能还会出现这么个问题,
```shell
* What went wrong:
A problem occurred configuring project ':app'.
> Failed to notify project evaluation listener.
> com.android.build.gradle.tasks.factory.AndroidJavaCompile.setDependencyCacheDir(Ljava/io/File;)V
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
* Get more help at https://help.gradle.org
BUILD FAILED in 0s
Could not install the app on the device, read the error above for details.
Make sure you have an Android emulator running or a device connected and have
set up your Android development environment:
https://facebook.github.io/react-native/docs/android-setup.html
```
懵逼了好久,才发现原来是因为之前创建好RN工程后,打开了Android Studio,它提示有更新我就更新了,导致Android Studio和RN工程中的gradle版本不一致导致的,现在还没找到更改的手段,我只能重新init了一个工程,大家如果知道一定要告诉我🙏🙏🙏🙏
po一张完成图,
---
以上就是本次React Native探路的第一步,总的来看,坑是不少,只能祝愿自己今后能避免部分坑吧。😂
[下篇:React-Native记(一)](./React-Native记(一).md)
================================================
FILE: React-Native/React-Native记(一).md
================================================
期末考完了,RN的学习一直在进行着,这段时间初步的学习了RN的一些基本组件、触摸事件及相关的生命周期等,整体给我的感觉与之前的微信小程序开发流程非常相似,甚至某些地方如出一辙,不能说微信小程序抄了RN,反而微信小程序的开发体验上会给人一种“阉割版”RN的感觉(可能这么形容不太恰当),但是总的来说,RN对于前端同学上手开发端会有一个无比大的优势,这优势甚至超过了做端的同学上手RN。
以上就是这段时间继续学习RN中给我的一些直观感受,因为也还在持续的学习当中,目前所遇到的情况都是“挖,RN这么强的吗?”,“这都行?”,“这么快?”等balabala,为啥我会发出这种感叹呢?按照以往来说,虽然纯Native开发整体给人会比较流畅并且开发体验也较好,但是唯一的痛点就是一个产品需要两波端同学同时开发,人力成本瞬间就上去了,而引入RN,甚至只需要前端同学就能够直接上了。
我在此总结了一番这段时间学习RN所遇到的或者可能给各位同学在未来学习RN少踩坑的总结,
1. 使用RN进行跨平台开发,最重要的就是“所见即所得”的开发快感,只需要保存一下,即可在对应的调试设备或者模拟器上看到修改后的效果。开启这种“所见即所得”的功能,如果你是iPhone真机,摇一摇弹出sheet后选择“Enable Live Reload”,Android真机需要点击“菜单键”即可弹出进行选择,如果你是iPhone模拟器,cmd+d即可弹出,若你是Android模拟器,我找了半天,模拟器自带的界面上是没有菜单键的,据说好像是google并不推荐Android上有菜单键这种东西?cmd+m弹出Android模拟器菜单。
2. 并且我们还能够使用浏览器来进行调试,当我们选择了“Debug JS Remotely”,那么会自动调出我们的默认浏览器,如果你是选择了Chrome作为默认浏览器,此时,我们把对应模拟器上作为chrome的View,使用开发者工具尽情调试吧~
3. RN跟我以往的开发流程不太一样,RN是面向组件开发的。比如我单独把登录页面拎出来,作为一个组件,
而想要使用QQLoginView这个组件的时候,我只需要在对应的页面js中,通过require引入这个组件,按照模仿H5标签的写法,即可引入。
这种思想跟面向对象的开发思路非常类似,到目前为止给我的感受就是换了种说法,😓。
4. 我是从iOS开发转入RN学习中来的,在这段时间中让我感到比较困惑的,就是RN每个页面的生命周期,在iOS中我们根据方法名基本上就能够推断出每一个类生命周期的对应阶段,比如ViewDidLoad、ViewWillAppear等,但是在RN中,emmmm。
RN组件的生命周期大致上可以分为**实例化阶段**、**存在阶段**和**销毁阶段**,其中最常用的为实例化阶段,基本上我们常用的所有的组件都在此阶段进行构建和展示等。
此外,现目前RN支持ES5和ES6,但是部分浏览器不支持ES6(其实我没懂到底是哪些浏览器,这都快三年了)市面上找到的资料大部分还是ES5,少部分高频组件或者方法都有ES6的做法,给我自己的感觉ES5和ES6差别还是很大的,ES6中较为完整的给JS补充了面向对象的基本内容,比ES5看上去更加的舒服。如果大家对ES5和ES6的区别感兴趣,可以参考[这篇文章](http://bbs.reactnative.cn/topic/15/react-react-native-%E7%9A%84es5-es6%E5%86%99%E6%B3%95%E5%AF%B9%E7%85%A7%E8%A1%A8/2)。(今后均以ES6进行讲解)
这是每个组件的生命周期开始时最先进行的两个步骤,先把可变变量和不可变变量都给你先执行,
```js
// 不可变 变量定义
static defaultProps={
name: '1222'
}
// 可变 变量定义
constructor() {
super();
this.state = {
title: '4666',
}
}
```
如果我们要修改相关变量,可以这么做,
```js
// renderPress是一个普通的JS方法
renderPress(event) {
this.setState({
title: this.props.name,
})
}
```
如果组件要被加载在视图上之前做调用或者做一些其它的事情,比如iOS中的ViewWillAppear,在RN中,需要在**componentWillMount**方法中进行。
**render**方法是一个组件中必须有的方法,相当于init(虽然在iOS中可以不用显式重载init),其本质上是一个函数,并返回其它调用或者组件构成DOM。在render函数中,只可通过this.state和this.props来访问在之前函数中初始化的数据值。
**componentDidMount**在调用了render方法后,组件加载成功并被成功渲染出来以后,所要执行的后续操作,一般会在这个方法中处理网络请求等加载数据的操作;这一点跟iOS区别较大,RN中的这个方法相当于规避掉了以往端开发时手动异步的过程,只需要在该方法中重新setData一遍即可刷新当前页面。
5. 以上就是RN实例化阶段的方法分析,存在期阶段的相关方法因为才刚开始学习一段时间,具体的方法细节并未涉及到,在此就先po出来,后续填坑。
**componentWillReceiveProps**指父元素对组件的props或state进行了修改
**shouldComponentUpdate**一般用于优化,可以返回false或true来控制是否进行渲染
**componentWillUpdate**组件刷新前调用,类似componentWillMount
**componentDidUpdate**更新后的hook
6. RN和微信小程序数据更新后,如果需要重新刷新页面按照之前的说法,直接就是this.setData即可,但这是怎么做到的呢?可以说这是RN的一大创新,将组件看成是一个状态机,开始会有一个初始状态,如果用户跟组件有互动导致状态变化,即可从而触发重新渲染UI。如果我们想要拿到当前页面中的某个组件,也就是DOM树下的某个节点,在RN中可以通过this.refs.'DOM节点名'去获取,但前提得是给这个组件设置了ref属性值。在RN中,各个组件并不是真实的DOM节点,而是存在于内存之中的一种数据结构,叫做虚拟DOM。只有当虚拟DOM插入文档以后,才会变成真实的 DOM。根据RN的设计,所有的DOM变动,都先在虚拟DOM上发生,然后再将实际发生变动的部分,反映在真实DOM上,这种算法叫做**DOM diff**,它可以极大提高网页的性能表现。具体的应用场景就是一个输入框,如果我们需要拿到用户的输入,就得通过DOM去拿到当前输入框,最后才能取到对应的输入值。
7. 在普通的web开发中,我们是能够直接在空的div中写东西的,但是在RN中是不允许直接在view标签中直接写东西的,我感觉应该是通过这种JSX的写法应该没法识别没有标签的内容吧。2333。并且如果RN中的View里什么都没有,那我们实际上在设备中是看不到这个View,你必须保证这个View中是有内容的。并且在一个单独的组件中,你必须要把一个View标签套在其它标签之外,在iOS中,你可以认为是self.view。
8. 这是RN的注释写法,{/**/}。每次写注释都觉得很烦emmm。。并且RN同样也是推荐使用flex进行布局,在flex中有主轴和侧轴之分,默认使用'column'也就是纵向布局,其余的主轴对齐方式有flex-start, flex-end,space-between, space-around。如果你的元素并未设置高度,那么它将占满整个距离它最近的View的高度,若未设置宽度,则将自动把宽度设置为最小适应数值。所以,我们在考虑布局的时候,一定要组织好主侧轴的布局方式,因为真的很容易乱😂。
9. RN中同样也有内外联样式表的说法,如果你要对一个组件进行内联样式表的设置,可以直接这么写'styl={{ backgroundColor:’red' }}。并且我们最好是从Xcode工程中打开模拟器,而不是通过命令行run-ios的方式去启动模拟器,因为工程调起的模拟器和命令行调起的并不是同一个,导致在Xcode工程中加入的资源,并没有命令行调起的模拟器中找到。
---
以上就是这段时间学习RN的一些要点,po一张当前学习效果图
[下篇:React-Native记(二)](./React-Native记(二).md)
================================================
FILE: React-Native/React-Native记(二).md
================================================
停了一段时间后RN的学习又持续了💪。在停止的这段时间中主要是去做了关于iOS的一些细节加强,这部分内容也是自己之前一直想去总结出来容易遗忘和出错的地方。
随后跟进的RN学习主要是学习`ScrollView相关使用`、`手撸轮播图`、`ListView的使用`、`tabBar的使用`、`Navigator的使用`、`网络请求的简单认识`,也快过年啦,先在此祝各位看友过年好~,因为回到老家网实在是不好,很多事情不方便去做😔,一些零零散散的琐事也时不时的打扰着自己,只能每天推进一点点。
在这段学习RN的时间中,我主要认识到了RN“真的很垃圾”🙂(注意:这是个双性词!),做一部分内容比Native真的方便超多的(甚至都觉得有些傻),至于哪些地方方便,之前文章中也说的七七八八了,但是重点在于做一些比如轮播图、TabBar这些Native支持得非常好的组件,在RN中就尴尬得不行(后文细谈)。所以在这段时间的学习中我差点萌生了“劝退自己”的想法,因为实在是太恶心了,先不管RN真正的精髓与我目前所理解的到底对不对得上,单是这几个基本组件在RN中实现起来都如此“恶心”(用官方的话来说,底层就是这么做的🙄),说句良心话,我实在是受不了。而且RN目前来说,虽然说是“Learn once,run anywhere”(有可能记错了?),但是iOS和Android现在位于RN生态中的分割还是太大,也有可能是自己的功夫不到家,单是一个tabBarIOS我就受不了(当然,也有可能是因为RN还需要再发展?)
我们先来看看使用ScrollView实现的轮播图效果
看上去效果还行,能够实现`触摸拖动中断`,但是因为整体没搭建完,并未能实现滑动整个View让轮播图进行中断停止(具体效果可参考京东)。整体实现思想跟iOS Native是一样的,同样都是需要ScrollView作为轮播容器,使用Image标签作为轮播实体,其与原生ScrollView使用起来无二意,就是基于iOS的runtime的封装
* **需要设置高度**
* 直接设置高度(不推荐)
* 在CSS中不加flex:1。让ScrollView中的内容自动的填充扩张
* ScrollView内部的其他响应者尚无法阻止ScrollView本身成为响应者,ScrollView在其所有子控件的最上层,所以ScrollView内部的其它响应者并不能阻止ScrollView本身成为响应者
```js
this.onAnimationEnd(e)}
onScrollBeginDrag={(e) => this.onScrollBeginDrag(e)}
onScrollEndDrag={(e) => this.onScrollEndDrag(e)}
>
{this.renderChildView()}
renderChildView() {
var childViews = [];
var imgArr = imageData.data;
for (var i = 0; i < imgArr.length; i++) {
var imgItem = imgArr[i];
console.log(imgItem)
childViews.push(
);
}
return childViews;
}
```
以上就是轮播图的架构(非常简单)。数据结构如下,用的是本地图片资源,(这里有个大坑,后文详解)
```json
{
"data" : [
{
"img" : "lunbo1",
"title" : "2333"
},
{
"img" : "lunbo2",
"title" : "2333"
},
{
"img" : "lunbo3",
"title" : "2333"
}
]
}
```
想要让轮播图在一定时间间隔后进行自动轮播,必不可少的需要一个定时器进行时间的计算,据部分资料显示,ES5需要一个`react-timer-mixin`组件,而采用ES6后可以大大减少累赘的写法,
```js
// 组件都载入完毕后,调起计时器方法
componentDidMount() {
this.startTimer();
}
startTimer() {
var scrollView = this.refs.scrollView;
var imgCount = imageData.data.length;
// 有setTimeOut和setInterval,需注意分清
this.timer1 = setInterval(() => {
var activePage = 0;
// 判断当前分页
if ((this.state.currentPage + 1) >= imgCount) {
activePage = 0
} else {
activePage = this.state.currentPage + 1;
}
this.setState({
currentPage: activePage
});
// 进行偏移
var offSetX = activePage * screenWidth;
scrollView.scrollResponderScrollTo({x: offSetX , y: 0, animated: true})}, 1000);
}
// 开始拖拽
// 需要把ScrollView自身作为参数传入
onScrollBeginDrag(e) {
// ES6清除计时器
clearInterval(this.timer1);
}
// 结束拖拽
onScrollEndDrag(e) {
this.startTimer();
}
```
此时运行起工程,就能够发现轮播图的效果已经实现了,但是如果我们想要添加一个分页指示器那该怎么做呢?当时我的脑子就冒出了RN肯定也是封装了UIPageController,但又被事实啪啪啪的打脸,根本没有,查阅了一番资料后发现居然有网友通过了HTML中的特殊转义字符`•`达到了效果。
这是补充完的render方法,
```js
render() {
return (
this.onAnimationEnd(e)}
onScrollBeginDrag={(e) => this.onScrollBeginDrag(e)}
onScrollEndDrag={(e) => this.onScrollEndDrag(e)}
>
{this.renderChildView()}
{/* 返回指示器 */}
{this.renderPageCircle()}
);
}
```
这是补充完的`renderChildView`和`renderPageCircle`方法,
```js
// 轮播图创建方法
renderChildView() {
var childViews = [];
var imgArr = imageData.data;
for (var i = 0; i < imgArr.length; i++) {
var imgItem = imgArr[i];
console.log(imgItem)
childViews.push(
);
}
return childViews;
}
// 分页指示器创建方法
renderPageCircle() {
var indicatorArr = [];
var style;
var imgArr = imageData.data;
for (var i = 0; i < imgArr.length; i++) {
style = (i === this.state.currentPage) ? {color: 'orange'} : {color: '#ffffff'};
indicatorArr.push(
•
)
}
return indicatorArr;
}
```
以上就是使用RN实现的简单轮播图组件demo,整体来看比iOS Native实现起来更为方便快捷(重点是简明🙂,项目工程地址文后),同样也是通过获取ScrollView的偏移量进行操作,从思想上看给人感觉全都是一样的呢🙂,接着我们来看看RN中的ListView。
在学习ListView过程中实现了三个小demo,普通列表展示、吸顶列表展示(对比iOS tableView的group样式)、九宫格。其中,给我的感觉如果只是使用到ListView做一些简单的数据展示,那真的是无比的舒服,并且得益于RN自身的架构设计,我们能够很好的处理数据源的切换。
使用ListView需要这两步:
* 创建一个ListView.DataSource数据源,给它传递一个普通的数据数组;
* 使用数据源(data source)实例化一个ListView组件,定义一个回调函数,函数接受数组中的每个数据作为参数,并返回一个可渲染的组件(就是每一个Cell)
普通列表的实现方法在吸顶列表中有重复,在此我们只对吸顶列列表做讲解(在讲解中大家将会看见是有多么的恶心🙂)
首先我们需要给ListView设置数据源,而ListView的数据源设置带了我一个无比的震撼(太尼玛麻烦了🙂)
```js
constructor(props) {
super(props);
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
};
}
```
通过以上代码我们就定义好了关于ListView的数据加载方法,当r1行不等于r2行时,才进行ListView的数据加载。我们先不管中间数据源如何更替,我们先来看如何填充ListView的核心数据源,
```js
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs)
});
```
通过以上方法,我们会调起RN的setState异步赋值方法,其实我觉得RN从架构上就是MVVM(不知道猜的对不对),当我们在setState方法中更改了相关的属性值,使用到这些相关属性值的对应组件会自动异步的刷新UI,这点超强大的好不好🙂。在iOS中单是要实现MVVM架构思想,就得拉出KVO这种看上去非常难受的东西🙂。
在ListView中要实现黏性头部,需要使用 cloneWithRowsAndSections方法,将 dataBlob(object), sectionIDs (array), rowIDs (array) 三个值传进去。
如果你曾经有过iOS开发经验(没有也行),要实现的吸顶效果,实际上就是对ListView做了分组,取名为Section,组内的每一行称之为Row,因此我们实现section和row的数据源赋值,这些东西都需要在页面初始化方法中得到解决,因此补充完的constructor方法为,
```js
constructor(props){
super(props);
// 初始化section方法
var getSectionData = (dataBlob, sectionID) => {
return dataBlob[sectionID];
}
// 初始化row方法
var getRowData = (dataBlob, sectionID, rowID) => {
return dataBlob[sectionID + ":" + rowID];
}
this.state = {
dataSource: new ListView.DataSource({
// 绑定section数据刷新
getSectionData: getSectionData,
// 绑定row数据刷新,只更新渲染数据变化的那一行 ,rowHasChanged方法会告诉ListView组件是否需要重新渲染当前那一行。
getRowData: getRowData,
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
})
};
};
```
我们需要搭建的界面代码如下所示,
```js
render() {
return (
// 先假装有一个NavigationBar🙂
PJHubs
)
}
// 创建ListViewRow
renderRow(rowData) {
return (
{rowData.name}
);
}
// 创建ListViewSection
renderSectionHeader(sectionData, sectionID) {
return(
{sectionData}
)
}
```
因为ListView的数据加载属于耗时操作(不管数据在本地还是网络上),RN官方推荐所有的耗时操作统统给我放到`componentDidMount`方法中(组件加载完毕后),因此补充完的`componentDidMount`方法如下所示,
```js
componentDidMount() {
this.loadDataFromJson()
}
loadDataFromJson() {
var jsonData = wine.data;
var dataBlob = {},
sectionIDs = [],
rowIDs = [],
cars = [];
for (var i = 0; i < jsonData.length; i++) {
sectionIDs.push(i);
dataBlob[i] = jsonData[i].title;
cars = jsonData[i].cars;
rowIDs[i] = [];
for (var j = 0; j < cars.length; j ++) {
rowIDs[i].push(j);
// 这是最恶心的地方,我们需要按照固定格式去填充输数据见后文,
dataBlob[i + ":" + j] = cars[j];
}
}
// 随后数据更新即可异步刷新UI
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs)
});
}
```
之所以说恶心,主要是因为我们要实现ListView的数据刷新,需要格式化数据格式按照`sectionID:rowID`的方式才能被ListView的黏性属性特性所识别,数据是下属一个sectionID才会被归纳为一个组中,具体数据样式如下所示,
```json
{
"sectionID1" : "2333",
"sectionID1:rowID1" : "2333",
"sectionID1:rowID2" : "2333",
"sectionID2" : "2333",
"sectionID2:rowID1" : "233"
}
```
一些其它的小细节和CSS就不放出来了,项目细节见文末,po一张吸顶完成图
在RN中实现九宫格,你可以认为是iOS中collectionView,主要还是用到了ListView组件,只不过修改的内容在ListView的CSS中,核心ListView CSS如下所示,
```CSS
listViewStyle: {
flexDirection: 'row',
flexWrap: 'wrap'
},
```
剩下的实现方法与在iOS中手撸九宫格效果是一样的,比如根据当前屏幕宽度算每个Cell的左右边距,设置个数等等。
tabBar是我最喜欢RN的地方(实现是太方便了🙂),因为tabBar在RN中有平台差异性,在此我们先用RN本身提供的tabBarIOS进行实现,先来看看到底是怎么写的,
```js
render() {
return (
PJHubs
{this.setState({selectedTabBarItem: "home"})}}
>
首页
{this.setState({selectedTabBarItem: "second"})}}
>
首页
{this.setState({selectedTabBarItem: "third"})}}
>
首页
{this.setState({selectedTabBarItem: "four"})}}
>
首页
);
}
```
从以上代码中可以看到,主要是就是`tabBarIOS`和`tabBarIOS.Item`这两个组件,关于每个横向的tabBar中要显示什么,直接在tabBarItem闭合标签中写明即可。需要注意的地方是,RN中tabBar比iOS原生TabBar多了一步手动选中的操作(也可以认为是更加简明🙂),我们需要先指明一个第一次进来选中的tab,
```js
constructor(props) {
super(props);
this.state = {
selectedTabBarItem: "home"
};
}
```
随后,如上图tabBar创建代码所示,根据`selected`属性和`onPress`回调函数进行tab的选中判断和变量赋值。实际上Native的tabBar选中判断方法也如此这般的操作,只不过我还是喜欢Native的做法🙂。
[详细代码见工程,learnRN3、learnRN_ListView](https://github.com/windstormeye/ReactNativePractices)
================================================
FILE: Test/单元测试.md
================================================
# 单元测试
## 前言
这小半年的几次发版本把自己搞累得不行,可以说这一方面来源于留给测试的时间不多,大致也就一周时间左右,其二是因为很多功能在不停的重构,因为 `pv`、 `uv` 都上去了,承载的业务量也慢慢的变大,从 UI 层面上看跟一年前已经完全不是一个东西了,更别说业务逻辑了。
这就引发出了一个非常难受的问题:**很多 `feature` 都是揉入之前代码中**,要保证原先逻辑的正常无误,还要保证重构后的代码质量、简洁。上半年曾经有段时间实在受不了前辈留下的代码,顶着按时发板、跟进新需求的压力下,使用 `Swift` 进行了诸多组件的重构,最开始的重构十分简单粗暴,实际上都不能称之为重构,只能算是重写,在做把 `Objective-C` “翻译”为 `Swift` 的事情。
在“翻译”的过程中,也逐渐的熟悉了十分美好的 `Swift`,现在正朝着 100% `Swift` 代码占有率迈进(目前 40% 左右),为了完成这个目标,并且不想再像之前那般痛苦,在接下的时间里,会重点偏向 `Unit Test` 和 `UI Test` 内容上来。
虽然不能保证 100% 的测试覆盖率,先给自己立个 flag,争取测试覆盖率在 **70%+**。
## XCTest
遵循“尽可能少的使用三方库”(找虐)原则,直接从使用 Xcode 自带的 `XCTest` 测试库入手,并结合一些 TDD 开发思路进行。
### 如何开始测试?
首先确定我们需要对哪个类进行测试,然后在 **Inspectors**(检查器)的 `target membership` 中选择需要把这个类关联到哪个 `target` (打勾)即可,这样就可以在 `xxxUnitTests` 的 target 下直接写出我们需要对该类进行测试的方法。
### XCTest 的所有断言方法
```Swift
// 生成一个失败的测试;
XCTFail(format…)
// 为空判断,a1为空时通过,反之不通过;
XCTAssertNil(a1, format...)
// 不为空判断,a1不为空时通过,反之不通过;
XCTAssertNotNil(a1, format…)
// 当expression求值为TRUE时通过;
XCTAssert(expression, format...)
// 当expression求值为TRUE时通过;
XCTAssertTrue(expression, format...)
// 当expression求值为False时通过;
XCTAssertFalse(expression, format...)
// 判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertEqualObjects(a1, a2, format...)
// 判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertNotEqualObjects(a1, a2, format...)
// 判断相等(当a1和a2是 C语言标量、结构体或联合体时使用, 判断的是变量的地址,如果地址相同则返回TRUE,否则返回NO)
XCTAssertEqual(a1, a2, format...)
// 判断不等(当a1和a2是 C语言标量、结构体或联合体时使用)
XCTAssertNotEqual(a1, a2, format...)
// 判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)
// 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...)
// 异常测试,当expression发生异常时通过;反之不通过;(很变态)
XCTAssertThrows(expression, format...)
// 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
XCTAssertThrowsSpecific(expression, specificException, format...)
// 异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
// 异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrow(expression, format…)
// 异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrowSpecific(expression, specificException, format...)
// 异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)
```
================================================
FILE: Tools/2_百家汇.md
================================================
# 百家汇
在这一节内容中,笔者将向大家介绍一些在iOS开发中可能会用到的工具,并且还会提供部分工具的下载链接。这些工具都是在开发过程中遇到了部分瓶颈后发现的工具。笔者会在介绍部分工具的时候附带上一些使用教程。
---
Postman:**Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。**当开发人员需要调试一个网页是否运行正常,并不是简简单单地调试网页的HTML、CSS、脚本等信息是否运行正常,更加重要的是网页能够正确是处理各种HTTP请求,毕竟网页的HTTP请求是网站与用户之间进行交互的非常重要的一种方式,在动态网站中,用户的大部分数据都需要通过HTTP请求来与服务器进行交互。Postman插件就充当着这种交互方式的“桥梁”,它可以利用Chrome插件的形式把各种模拟用户HTTP请求的数据发送到服务器,以便开发人员能够及时地作出正确的响应,或者是对产品发布之前的错误信息提前处理,进而保证产品上线之后的稳定性和安全性。
笔者目前安装在电脑上的Postman版本为4.11.0,好像已经落后一些了,不管没关系,我们用Postman的最重要的一点就是使用它提供的各种便捷HTTP调试功能(当然HTTPS也是可以的),那么我来看看Postman到底是怎么一回事吧,
Postman默认给你的请求设置为了GET,如果你有其它的请求需求,则在下拉框中选择相应的请求方式即可,
下面是用GET请求进行了一次北京当天的天气预报详情情况,
大家可以看到,这是一个text格式,也就是普通的文本格式,如果大家知道JSON格式的话,能够一眼看出这就是一个JSON格式的文本,我们需要对其做一个JSON格式的格式化,从下拉框中,即可选择JSON格式的文本显示。
如果你要选择POST请求方式,则需要进行HTTP请求的参数填写,那么我们应该怎么在Postman里边填写相关参数呢?不但可以填写POST请求参数,还可以填写HTTP头内容,如下图所示,
Postman还提供了非常多的功能供各种需求,**Postman发布在Google的插件商店,需要大家翻墙下载**,最重要的是,如此美好的东西是免费,免费,免费的!!!!
---
在上一节内容中,笔者向大家介绍了Git和GitHub的相关内容,大家都有了部分的了解,**Git是一套版本控制工具**,在这套工具中最直接的方式就是命令行操作,但是也就是这一点内容,阻挡住了非常多想使用Git进行版本控制的同学,笔者在此介绍两个两个工具,SourceTree和GitHub Desktop。
SourceTree和GitHubDesktop最大的区别就在于GitHub Desktop只能提供给托管在GitHub上的项目运行,而SourceTree提供给了任何一个使用了Git进行版本控制的项目。它们两个都以图形化界面的方式提供了一种展示和操作Git的交互。
上图为SourceTree的项目展示界面,其中最左边为本地Git的修改记录,左侧白色带向下箭头的为远端Git库中为在本地Git库中的修改记录。
上图为我校开源应用iBistu的commit记录展示,上图中红框的部分为整个sourceTree中比较重要的内容,大家看图就能够知道大概是什么个意思了。
Git的相关操作,都在SourceTree中得到体现,还算挺人性化的吧,大家有真正的需要Git进行版本控制的需求后自己摸索一番就全都明白是怎么一回事 了。。。
### 当修改了 git 密码,怎么在 SourceTree 中进行更新呢?
进入如下目录:`~/Library/Application Support/SourceTree`,删除掉对应 git 账户文件,下次执行 `git push` 即可弹出重新输入密码。
上图为GitHub Desktop的项目展示界面,其中位于顶部的偏黑色的那部分内容就是commit记录展示。因为笔者的用到Git进行版本控制的项目分为商业项目和个人项目,商业项目的话只是用到Git做版本控制,并不能开源(GitHub选择闭源则需要收费五美元,虽然数额不大,但是对学生来说还是一笔不小的开支),所以呢所有的商业项目中用到Git的地方都不是放在GitHub中,而是国内优秀的一些Git托管平台,比如码云,Coding等。个人项目的话,本着分享供大家学习参考以及提出修改意见的原则,全都放在了GitHub上。笔者用GitHub Desktop的地方不是太多,因为个人项目中用到的Git操作不是很复杂,所以笔者在此就不多说关于GitHub Desktop的操作了,这部分内容大家可以到网上自行搜索一番。
大家完全可以不需要使用SourceTree和GitHub Desktop进行Git操作管理使用Terminal也是可以的,只不过使用它们后会大大的减少在使用Git时的操作难度和时间,也更加利于管理。
---
**“随着移动互联网的快速发展,越来越多的软件移居到了mobile device上,作为一名Coder或是Designer,必须学习新的移动平台开发技术才能跟上潮流,PaintCode是Apple Designer入门APP开发最合适的辅助工具之一,她可以把你绘制的矢量UI自动转化为适用于iOS/OS X的Objective-C代码,可以被视当年网页制作神器DW的今世转生版。”** —— 摘自“百度百科”
打开程序后就看到了左上角最显眼的几块特定内容,在自带的这部分内容中,我们很容易的就能够通过拖动的方式“画”出代码,
把这部分代码复制到Xcode中对应的文件中即可。
在上图中红框部分还提供了部分语言选择、iOS版本以及是否使用ARC等(关于ARC是什么我们后边的章节中再说),
当然,最良心的地方它还提供了绘制贝塞尔曲线的工具,如下图所示,
PaintCode:[下载链接](http://www.sdifen.com/paintcode312.html)
---
Flinto可能更加的偏向于产品经理或者UI/UX一些,不过对于开发者开说同样也是很有用的,比如APP的原型设计。在产品未正式开始编写之前,如果我们单单通过UI设计图来编写APP的话,其实很多时候并不能很好的把整套产品的理念给表现出来,经常会出现开发者花费了大把的时间按照UI设计图编写完了APP,但是最终APP呈现的效果并不好,比如页面的过渡与产品经理或者客户的设想不一致,很有可能会推翻很多之前花费大力气来写出来的效果,甚至还会给客户留下不好印象,所以Flinto可以有机的结合UI设计图,在产品未正式开始编写时,先通过Flinto和UI设计图搭建出APP原型,经过研究后提出修改的地方,最后才交由开发工程师编写。经过这么一套流程,能减少非常多的问题,大大减少了时间和人力成本。
在Flinto中有这么一个思想——“万般皆图层”,也就是说,我们日常使用的APP中的返回按钮、确定按钮等按钮在Flinto中都是图层,需要我们自己放上去,其实这一点并没有什么奇怪的地方,因为就算Flinto给你按钮选项,最终在开发APP的时候,你还是得真实的创建一个按钮,如果是这样的话,那干脆直接把UI设计图往上一放,然后给固定区域添加矩形,给这个矩形添加手势,再给这个矩形创建链接,并且还可以选择页面切换时的过渡动画。
创建一个新屏幕,给创建出来的新屏幕中添加图层,然后在原先屏幕中添加一个矩形。右键矩形,给这个矩形创建链接,
点击创建链接后,会出来一个可移动小突触,鼠标移动这个小突触,选择你要链接到的屏幕中,
在弹出的页面中,可选择点击矩形时的手势判断,以及新屏幕出现时的动画效果,以上步骤都做好后,点击右上角的预览即可看到制作完成后的效果
Flinto下载链接:[http://www.sdifen.com/flinto22.html](http://www.sdifen.com/flinto22.html)
---
Dash是一款文档查看工具,比如各类API文档等,在写代码的时候经常会遇到某个方法或者函数不知道是叫什么,运气好的话,一下就能够看到,如果运气不好的话,你想用的API别人都没用过,那就得去该门语言的官网去看,恰恰如此,很多语言的官网都在国外,你要是能够忍受每次查看相关API是都去对应慢的一批的官网当我没说。。。😂
Dash目前支持下图所示的主流平台及语言,当然你还可以手动导入你所需要的文档,比如各种电器的说明书啊等等,Dash的检索精度和速度都非常的快,当然,如果你选择了免费版的话,第一次打开应用程序进行检索时会等待10秒(不知道更新够还是不是这样),以后就会快一些了,笔者认为这点干扰还是可以忍受的。
笔者事先通过Dash下载了了苹果和Arduino的整套API文档,
可以看到,Dash解析出了文档中的所有标题,非常的详细,
检索NSString的相关方法,会看到Dash列出了所有跟NSString相关内容,而且还列出了所有的类方法和实例方法,右侧的详细类介绍排版非常的舒服。
Dash提供免费版和收费版,大家如果需要Dash的话,直接进入官网下载即可
---
**随着WWDC 17结束,苹果发布了非常多新的API,同时很多厂商也会跟进推出一系列的开发工具供我们使用,本节内容随着时间的流逝,不间断持续更新!**
================================================
FILE: Tools/3_GitHub.md
================================================
# **GitHub**
**Github吉祥物Octocat**
GitHub,全球最大的同性交友网站。。。其实应该说是全球最大的社交编程及代码托管网站,说的正式一点呢,GitHub 是一个面向开源及私有软件项目的托管平台,因为只支持 Git 作为唯一的版本库格式进行托管,故名 GitHub。
关于GitHub,如果你是计算机相关专业的话,应该是听过的。上面集齐了很多众多一线互联网公司及业界大佬,比如Linus等,Linus在2011年的时候把整个Linux源代码都托管在了GitHub上,给大家展示一下Linus的commit记录,
你没看错,就是将近六十八万的commit。。。。。。不得不说GitHub的出现带动了一堆开源项目的产生,包括很多业内大佬也在慢慢的向开源靠拢,比如,曾经与开源势不两立的微软,现如今居然成了GitHub上开源项目最多的组织,
上图就是截止到2016年九月八号为止的开源组织的项目排名,微软的开源项目已经超过 Facebook 的 15682 个,进一步拉开了与 Docker、Angular、Google 和 Apache 的距离。
实际上,微软在开源之路上算出发比较晚的。直到 2014 年,微软才开始在 GitHub 上建立账户,这一年,微软宣布了.NET 的开源。在此之前,微软还成立了微软开放技术公司(Microsoft Open Technologies.Inc),这家相对独立的项目也有自己的 GitHub 账户。除了微软自己在[GitHub 上的主页](https://github.com/microsoft)之外,微软还创立了一个 [microsoft.github.io](https://microsoft.github.io/)的网站,用来展示自己在 GitHub 上的开源成果。其中像是 vscode、TypeScript 等等代码仓库(Repos)获得了上万的 Star,**在 GitHub 上,Star 的数量和质量是挂钩的**。
有这么一种说法,在未来,聘用程序员的方式将发生重大改变,不再是以往包括现在所采取的聘用方式,“在将来,你不再需要简历,人们可以直接通过谷歌来了解你,并且还可以在开源社区里创立自己的个人品牌和声誉。HR通过邮件列表、bug 跟踪表单以及提交到 Mercurial、Subversion 、GitHub和 CVS 仓库的源代码与其他软件工程师进行交流。所有的这些交流,都是公开的,并且可以被谷歌进行索引。”一位工作在红帽公司的员工如是说。
你也许会感到奇怪,这是iOS开发基础教程,关这个代码托管的平台什么事呢?确实这样,在刚开始学习iOS开发的时候,笔者也是一样产生过这样的疑问。导致在做开发的时候没有利用上GitHub等一些代码托管平台上开源的一些第三方库或者工具,导致大大的增加了开发过程中所耗费的时间和精力等不必要的成本。
比如,在你的项目中需要进行网络请求时,你完全可以自己从头开始写 一套网络请求库,写一套在进行网络请求时处理异常的机制等等,这些东西我们都可以自己完成。重点是,当你写完了以后,估计你的项目进度就会拉下一大半,得不偿失。笔者不是不让大家实现这些功能,而是不推荐,业内有句话叫“不要重复造轮子”。
但是呢,很多人会把这句话给理解错了,在实际开发过程中,存粹就去当了“调包侠”了,搞得如果网上没有现成的相关代码,自己就完不成了任务。这样就和上文中笔者所阐述的思想背道而驰了。
总结一句话,不是不让大家造轮子,也不是让大家啥都用现成的。而是说,重新审视产品需求,产品最核心的内容我们要有自己的控,把各个轮子用活,给自己在开发的过程中创造最大的利益。
### 在使用GitHub中涉及的部分相关术语解释
* Commit(提交):在指定时间点对系统差异进行的注释 “快照”。
* Local(本地):指任意时刻工作时正在使用的电脑。
* Remote(远程): 指某个联网的位置。
* Repository \(仓库,简称 repo\):配置了Git超级权限的特定文件夹,包含了你的项目或系统相关的所有文件。
* Pushing(推送):取得本地Git提交(以及相关的所有工作),然后将其上传到在线Github。
* Pulling(拉取):从在线的Github上获取最新的提交记录,然后合并到本地电脑上。
* Master \(branch\):主分支,提交历史 “树”的 “树干”,包含所有已审核的内容/代码。
* Feature branch(功能分支/特性分支):一个基于主分支的独立的位置,在再次并入到主分支之前,你可以在这里安全地写工作中的新任务。
* Pull Request(发布请求):一个 Github 工具,允许用户轻松地查看某功能分支的更改 (the difference或 “diff”),同时允许用户在该分支合并到主分支之前对其进行讨论和调整。
* Merging(合并):该操作指获取功能分支的提交,加入到主分支提交历史的顶部。
* Checking out(切换):该操作指从一个分支切换到另一个分支。
因为GitHub是业界非常火爆的一个代码托管平台,所以关于GitHub的使用笔者在此点到为止,对大家做一个简单的介绍,关于Git的相关操作网上有非常多细致的、美观的操作教程,笔者就不做过多的赘述,大家可以参考文末提供的链接进行学习。
以下是笔者收集的一些资源:
* [Treehouse – 写给设计师的 Git 入门介绍](http://blog.teamtreehouse.com/git-for-designers-part-1)
* [Roger Dudler – Git 简易教程](http://rogerdudler.github.io/git-guide/)
* [Pluralsight – Github:初学者指南](https://www.pluralsight.com/blog/software-development/github-tutorial)
## 子模块 git submodule
**为什么需要?**
一般情况下不同的语言下都会有自身的三方库依赖管理工具,如 Swift 的 Swift Package Manager,Ruby 的 Ruby Gem 等等,但依旧存在某些语言不提供官方默认的依赖管理工具,需要开发者自行处理依赖关系,如 C++ 等。
这种情况下可以通过 git submodule 子模块的形式单独依赖完整的 git 仓库,且拥有完整的 git history,集成进主仓时也仅通过文件夹的方式引入到主仓目录下,与平常直接引入全部文件的方式无异。
**使用**
细节可参考
[Git 工具 - 子模块](https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97)
[Using Git Submodules With Xcode — Tutorial For iOS](https://medium.com/@aestusLabs/using-git-submodules-with-xcode-tutorial-for-ios-dcfc28a82c20)
================================================
FILE: Tools/4_Xcode.md
================================================
# Xcode
Xcode,前身为乔帮主创建NeXT公司时开发的Project Builder,NeXT被苹果公司收购后于2003年正式发布Xcode 1.0。在Xcode出现之前开发Mac OS X应用程序的开发工具很凌乱,直到Xcode 3.0的发布,它成为了开发人员建立 Mac OS X 应用程序的最快捷方式,也是利用苹果电脑公司新技术的最简单途径。到3.1版本后,附加了iOS SDK,这样开发者从此可以不用再单独下载iOS SDK了。
SDK是什么?它的全称——“software developer kit” 直译过来就是一个“软件开发包”,iOS SDK提供一堆与iOS有关的各种方法接口(也叫API),可以通过调用iOS SDK里提供的各种方法来实现我们想要的功能。
在4.0版本之前,如果想要下载Xcode进行Mac OS X或者iOS系统下的应用程序开发的话,要求必须购买开发者证书,成为开发者才行。4.0版本到来后,苹果公司不仅仅向个人开发者开放了下载权限,非开发者也可以进行下载,但得支付4.99美元。到了4.1版本,Xcode开始面向Mac OS X10.6和10.7版本的所有用户提供免费下载。这种“区别”对待的情况,直到4.5版本,Xcode才正式提供免费下载。
以上就是关于Xcode简单的背景介绍,目前最新的Xcode版本已经到了8.3.2。可以说,Xcode正在变得越来越好,对给开发者提供了非常多的帮助。接下来,我们将从Xcode的首页开始与大家一起探索Xcode。
上图为Xcode启动完毕后出现的首页界面。在这个页面中,我们看到了有“Get start with a playground” 、“Create a new Xcode project”和“Check out an existing project”三个选项。
笼统的来说,“Get start with a playground”的作用有以下几点:
1、可以让你快速的玩转Swift;
2、验证算法。这一点在用Swift来刷各种编程题时非常的舒服;
3、享受即时编译带来的快感。
那么就来看看这个开发利剑 —— “Swift playground”是怎么一回事吧,
在这里,我们设置了Name为默认的“Myplayground”(平台随意)选择playground文件存放的路径,点击Next。
上图中所示的为Swift playground默认创建好的一段代码,这段代码的含义已经很清晰了,第三行告诉我们import,也就是导入了一个东西,这个东西叫做UIKit。第五行代码是创建了一个变量,变量的内容是一个字符串,字符串的内容是"Hello, playground"。var就是variable变量的简写,与之对应的是let,Swift在这一块内容中据说是借鉴了Scala语言的var和val。该变量的名字叫做str,这个名字大家可以随意选取。
既然,我们现在知道了str是个字符串变量,那么还可以玩一些这样的事情,
甚至,你还可以在Swift playground中做这样的事情,
你完全可以在Swift playground中以图形化的方式显示出某个值的变化趋势,对使用Swift语言来调试算法的同学来说是一个莫大的帮助。当然,关于Swift playground还有一堆很好玩的功能,笔者在此把大家领进门,剩下的就靠同学们自己努力了。
关于Swift playground,笔者就介绍到这里。苹果推出Swift playground主要目的就是让大家快速的学习Swift语法,用Swift搞点事情。
接下来,我们来看看第二个选项 —— “Create a new Xcode project”,创建一个Xcode工程。
这就引入了一个新的问题,什么是工程?工程是什么意思?
百度百科是这么说的“工程是科学和数学的某种应用,通过这一应用,使自然界的物质和能源的特性能够通过各种结构、机器、产品、系统和过程,是以最短的时间和最少的人力、物力做出高效、可靠且对人类有用的东西”。
ok,我们要的就是这种效果,Xcode中所指的工程就是计算机科学和数学的应用,通过这一应用,使代码通过各种结构、系统和过程,以最短的时间和最少的人力、物力做出高校、可靠且对人类有用的东西。
上面这段话是笔者按照传统的工程定义上翻译过来对应上Xcode的工程概念,再说的通俗一些,就是你通过创建工程,来缩短各种文件之间的关联关系,缩短开发时间,让开发者把注意力集中在编码而不是各种文件之间的编译和链接上,就Xcode来说,它还提供了一套缩短UI界面搭建时间的工具(这部分的内容我们将在后边讲到)。
毕竟我们是在IDE里创建的是一个工程,工程具备的属性和功能,IDE都能够提供,比如上文中我们所讲到的通过各种结构帮助开发者更加快速的构建工程,这一点在IDE中,提供非常多的便捷功能,利用这些IDE开发者写好的功能,我们在编码时,就能够方便很多。
在了解Xcode中的工程是什么意思后,来创建一个工程,
在上图中的红框部分,其中提供了包括iOS、watchOS、tvOS、macOS这四大系统的模板文件。
说到模板文件,这又是一个什么东西呢?模板模板,就是提供了一个系统在你创建文件的时候就给你写好部分基础内容,如果大家有过一些使用IDE进行编程的实践,能发现,写C、C++或者java程序性的时候,有些IDE会默认在创建出来的工程文件中写好“hello,world”,这就是模板文件下的简单模板代码。
如果你是一个刚入门的新手,没关系。在你以后的编程生涯中,会经常遇到这种情况,以至于你会特别烦,你就会像笔者一样,翻各种IDE的应用程序包文件,修改它创建各种文件时自带的一些模板内容。
在这里,我们选择macOS下的“command Line Tool”,翻译过来就是命令行工具,创建一个命令行工具文件,
**Product Name:**你的工程文件的名字
**Team:**这里需要选择开发者,现在的Xcode版本能够生成个人免费开发者证书,但这几年面向个人开发者的免费证书所具备的功能非常的有限,不过就前期开发来说,完全足够。如果你是第一次使用Xcode创建工程,不知道去哪生成个人开发者免费证书的话,稍等文后会讲到。
**Organization Name:**这里是组织名字,这部分的内容会出现在每个文件的开头,标识出这个文件是谁写的如下图所示,
**Organization Identifier:**这是组织标识符,标明的是你这个工程文件的所属的组织,随意写,或者使用Xcode默认的。
**Bundle Identifier:**这是一个非常重要的东西,所以苹果不会让你自己填写。不过它的构造规则我们已经可以猜出来了,就是你的Organization Identifier.Product Name。这样的话,就可以在数以亿计的APP中唯一的表示你的APP(虽然我们现在写的只是一个命令行程序)
**Language:**目前Xcode默认自带的的语言只有Swift、Objective-C、C++、C四种语言,如果你想让Xcode支持Java等其它语言,得做一些设置,这部分内容不是本次教程重点,大家有兴趣可以自己网上搜索一番。
---
我们来看看上文中所说的如何生成一个个人免费的开发者证书,**这一步不是必须的,只是作为一个拓展**
首先在Xcode的菜单栏中,选择Xcode -> Preferences
在新展出的页面中选择Account,然后在在右下角选择“+”,Add Apple ID,如下图所示,
然后在出现的页面中,填写你的Apple ID和密码,即可创建一个免费的个人开发者证书,
记住,这只是一个**免费,免费,免费**的个人开发者证书,别想着有了它就能打包你写好的APP然后上架,千万记得,这是绝对不可以的,想要把你写好的APP打包上架的话,你需要花费688元购买正式的开发者证书。
---
经过以上操作,选择好工程文件所在路径后,我们进入到了如下界面,
你会发现跟以往的main.c、main.cpp、main.java不一样了,这回出现了main.m,后缀名为.m的文件就是专门编写Objective-C语言的文件,如果你用创建的是一个Swift语言编写的文件,它的名字会是main.swift。所以大家可以从这些文件的后缀名中看出是用什么语言编写的。
笔者上文所说的模板文件,在此得到了验证。Xcode在我们创建工程文件的时候,给我们自动内置了一个Objective-C版的“Hello,world”程序。
接下来,我们再重复一遍刚才创建工程的步骤,重新创建一个编程语言为C的工程文件,
按下command+R,就会触发Xcode的“Run”功能,也就是运行,
按下了command+B,则触发了Xcode的“Build”功能,也就是编译。这只是起到了编译的作用,只是把你写好的C代码转化为了二进制码,通过这个功能可以起到一些语法的检查(虽然Xcode的语法检查非常的强大)和编译时错误检查等(有可能是文件链接失败等原因)。
左上角的这部分内容,只有到iOS开发中才能用的到,
Xcode中的报错和警告有一下几种情况,
**1、可由Xcode自动修复的错误。**如果出现了这种情况,大家可以点击红点,有Xcode自动帮我们修复语法上的错误。不过这有一点需要注意的地方是,这种情况也有可能是因为我们的回车换行引起的,导致代码无法正常结束,Xcode认为你需要加上 ; 。
点击红点后,出现“Fix-it”,回车即可自动修复。
**2、可由Xcode自动修复的警告错误。**
意思是你写了一个不是很符合要求的代码,给你抛出一个警告,因为是警告并不是让代码运行不了的报错,所以你大可放心的按下Command + R运行你的代码,不过,需要注意的地方是,
如果你写了一个类型下文中的代码,虽然Xcode也只给你报了一个警告,但是也要注意,这很明显是一个类型错误的警告。那为什么都是类型错误还不报错,只是一个警告呢?大家可以做个测试,就算你写了这种代码,Xcode照样会给你编译通过,甚至运行通过。但,如果你是在做真正开发的话,一不小心写了这种代码,你就会出现值为NULL的情况,因为两个变量的类型不一样,所以做赋值操作时,就会取不到需要的值。
但是,也就是因为警告并不会让代码不能运行,所以很多同学在开发的时候就会在脑海中产生一种思想,反正我的代码能跑,报那么多的警告也没关系,就这样的吧。这种思想万万不可取,虽然Xcode给你报的警告确实不会导致你的代码不可运行,万一你写的这部分代码就在赋值的时候因为两种变量的类型差别导致取到了NULL(空)值,有极大的可能会导致整个APP崩掉。
所以,笔者推荐的做法是,先查看一遍警告的内容是什么,如果警告的内容危险度比较小,比如说,我们可能是用了一些老的API,老的函数、方法等,现在已经有了新的替代方案,所以Xcode会给你抛出一个警告,告诉你该换方法啦,这有一个比你这个好的方法。在这种情况下,我们就可以考虑暂时不用管。
Xcode中经常的出现的报错和警告情况就以上的这么两种,这是最经常出现的,还有一些别的情况,例如可能是你在导入头文件的时候,被导入的文件并不在该工程路径下,此时Xcode就会在你按下Command + R的时候给你报了一个编译链接时错误,报出这个错误后经常会带上Xcode在编译时的日志信息,大家可以在这个日志信息中看到具体是到哪步编译链接时出现的错误
如果大家在使用Xcode写代码时,发现需要进行代码分层,比如说不想把一大段代码挤在main函数里边,想要进行多文件编码 ,那怎么新建一个文件呢?
在文件索引区中随意选中一个文件或者文件夹,右键 -> New File,
然后在弹出的新页面中,选中你要创建的文件,
如果大家觉得白色的编辑区背景刺眼的话,想要更换配色的话,可以在顶部的菜单栏中选择Xcode -> Preferences,
在弹出的新页面中,选择Font & color。挑选适合自己的配色,如果大家不喜欢Xcode自带的配色,可以在页面的左下角“+”号中添加外部配色方案。外部配色方案可以在网上找到,比如笔者的这个配色就是从网上找的,毕竟每天都要面对着它,眼睛舒服是第一。
如果大家觉得字号有点小的话,可以按照下图的方式进行操作,
Xcode的简单入门就到这里了,有关更加深入的Xcode功能介绍,我们将在后面内容中进行介绍。笔者在上文中所讲述的内容都是一些基础内容,把这些基础内容都掌握了以后,用Xcode来写C和C++代码都不在话下,而且你还可以享受Xcode人性化的代码提示和自动纠错技术。
以上笔者介绍的Xcode内容与实际的iOS开发所用到的Xcode功能还差很多,上文中笔者介绍的Xcode部分使用功能,只是带大家入个门,使用Xcode完成一些课上或者课下的一些小实验或者刷刷OJ什么的,更加细致和编辑的Xcode使用技巧,我们在后天的章节中将为做一个详细讲解。
================================================
FILE: Tools/5_Xcode.md
================================================
# Xcode
Xcode,前身为乔帮主创建NeXT公司时开发的Project Builder,NeXT被苹果公司收购后于2003年正式发布Xcode 1.0。在Xcode出现之前开发Mac OS X应用程序的开发工具很凌乱,直到Xcode 3.0的发布,它成为了开发人员建立 Mac OS X 应用程序的最快捷方式,也是利用苹果电脑公司新技术的最简单途径。到3.1版本后,附加了iOS SDK,这样开发者从此可以不用再单独下载iOS SDK了。
SDK是什么?它的全称——“software developer kit” 直译过来就是一个“软件开发包”,iOS SDK提供一堆与iOS有关的各种方法接口(也叫API),可以通过调用iOS SDK里提供的各种方法来实现我们想要的功能。
在4.0版本之前,如果想要下载Xcode进行Mac OS X或者iOS系统下的应用程序开发的话,要求必须购买开发者证书,成为开发者才行。4.0版本到来后,苹果公司不仅仅向个人开发者开放了下载权限,非开发者也可以进行下载,但得支付4.99美元。到了4.1版本,Xcode开始面向Mac OS X10.6和10.7版本的所有用户提供免费下载。这种“区别”对待的情况,直到4.5版本,Xcode才正式提供免费下载。
以上就是关于Xcode简单的背景介绍,目前最新的Xcode版本已经到了8.3.2。可以说,Xcode正在变得越来越好,对给开发者提供了非常多的帮助。接下来,我们将从Xcode的首页开始与大家一起探索Xcode。
上图为Xcode启动完毕后出现的首页界面。在这个页面中,我们看到了有“Get start with a playground” 、“Create a new Xcode project”和“Check out an existing project”三个选项。
笼统的来说,“Get start with a playground”的作用有以下几点:
1、可以让你快速的玩转Swift;
2、验证算法。这一点在用Swift来刷各种编程题时非常的舒服;
3、享受即时编译带来的快感。
那么就来看看这个开发利剑 —— “Swift playground”是怎么一回事吧,
在这里,我们设置了Name为默认的“Myplayground”(平台随意)选择playground文件存放的路径,点击Next。
上图中所示的为Swift playground默认创建好的一段代码,这段代码的含义已经很清晰了,第三行告诉我们import,也就是导入了一个东西,这个东西叫做UIKit。第五行代码是创建了一个变量,变量的内容是一个字符串,字符串的内容是"Hello, playground"。var就是variable变量的简写,与之对应的是let,Swift在这一块内容中据说是借鉴了Scala语言的var和val。该变量的名字叫做str,这个名字大家可以随意选取。
既然,我们现在知道了str是个字符串变量,那么还可以玩一些这样的事情,
甚至,你还可以在Swift playground中做这样的事情,
你完全可以在Swift playground中以图形化的方式显示出某个值的变化趋势,对使用Swift语言来调试算法的同学来说是一个莫大的帮助。当然,关于Swift playground还有一堆很好玩的功能,笔者在此把大家领进门,剩下的就靠同学们自己努力了。
关于Swift playground,笔者就介绍到这里。苹果推出Swift playground主要目的就是让大家快速的学习Swift语法,用Swift搞点事情。
接下来,我们来看看第二个选项 —— “Create a new Xcode project”,创建一个Xcode工程。
这就引入了一个新的问题,什么是工程?工程是什么意思?
百度百科是这么说的“工程是科学和数学的某种应用,通过这一应用,使自然界的物质和能源的特性能够通过各种结构、机器、产品、系统和过程,是以最短的时间和最少的人力、物力做出高效、可靠且对人类有用的东西”。
ok,我们要的就是这种效果,Xcode中所指的工程就是计算机科学和数学的应用,通过这一应用,使代码通过各种结构、系统和过程,以最短的时间和最少的人力、物力做出高校、可靠且对人类有用的东西。
上面这段话是笔者按照传统的工程定义上翻译过来对应上Xcode的工程概念,再说的通俗一些,就是你通过创建工程,来缩短各种文件之间的关联关系,缩短开发时间,让开发者把注意力集中在编码而不是各种文件之间的编译和链接上,就Xcode来说,它还提供了一套缩短UI界面搭建时间的工具(这部分的内容我们将在后边讲到)。
毕竟我们是在IDE里创建的是一个工程,工程具备的属性和功能,IDE都能够提供,比如上文中我们所讲到的通过各种结构帮助开发者更加快速的构建工程,这一点在IDE中,提供非常多的便捷功能,利用这些IDE开发者写好的功能,我们在编码时,就能够方便很多。
在了解Xcode中的工程是什么意思后,来创建一个工程,
在上图中的红框部分,其中提供了包括iOS、watchOS、tvOS、macOS这四大系统的模板文件。
说到模板文件,这又是一个什么东西呢?模板模板,就是提供了一个系统在你创建文件的时候就给你写好部分基础内容,如果大家有过一些使用IDE进行编程的实践,能发现,写C、C++或者java程序性的时候,有些IDE会默认在创建出来的工程文件中写好“hello,world”,这就是模板文件下的简单模板代码。
如果你是一个刚入门的新手,没关系。在你以后的编程生涯中,会经常遇到这种情况,以至于你会特别烦,你就会像笔者一样,翻各种IDE的应用程序包文件,修改它创建各种文件时自带的一些模板内容。
在这里,我们选择macOS下的“command Line Tool”,翻译过来就是命令行工具,创建一个命令行工具文件,
**Product Name:**你的工程文件的名字
**Team:**这里需要选择开发者,现在的Xcode版本能够生成个人免费开发者证书,但这几年面向个人开发者的免费证书所具备的功能非常的有限,不过就前期开发来说,完全足够。如果你是第一次使用Xcode创建工程,不知道去哪生成个人开发者免费证书的话,稍等文后会讲到。
**Organization Name:**这里是组织名字,这部分的内容会出现在每个文件的开头,标识出这个文件是谁写的如下图所示,
**Organization Identifier:**这是组织标识符,标明的是你这个工程文件的所属的组织,随意写,或者使用Xcode默认的。
**Bundle Identifier:**这是一个非常重要的东西,所以苹果不会让你自己填写。不过它的构造规则我们已经可以猜出来了,就是你的Organization Identifier.Product Name。这样的话,就可以在数以亿计的APP中唯一的表示你的APP(虽然我们现在写的只是一个命令行程序)
**Language:**目前Xcode默认自带的的语言只有Swift、Objective-C、C++、C四种语言,如果你想让Xcode支持Java等其它语言,得做一些设置,这部分内容不是本次教程重点,大家有兴趣可以自己网上搜索一番。
---
我们来看看上文中所说的如何生成一个个人免费的开发者证书,**这一步不是必须的,只是作为一个拓展**
首先在Xcode的菜单栏中,选择Xcode -> Preferences
在新展出的页面中选择Account,然后在在右下角选择“+”,Add Apple ID,如下图所示,
然后在出现的页面中,填写你的Apple ID和密码,即可创建一个免费的个人开发者证书,
记住,这只是一个**免费,免费,免费**的个人开发者证书,别想着有了它就能打包你写好的APP然后上架,千万记得,这是绝对不可以的,想要把你写好的APP打包上架的话,你需要花费688元购买正式的开发者证书。
---
经过以上操作,选择好工程文件所在路径后,我们进入到了如下界面,
你会发现跟以往的main.c、main.cpp、main.java不一样了,这回出现了main.m,后缀名为.m的文件就是专门编写Objective-C语言的文件,如果你用创建的是一个Swift语言编写的文件,它的名字会是main.swift。所以大家可以从这些文件的后缀名中看出是用什么语言编写的。
笔者上文所说的模板文件,在此得到了验证。Xcode在我们创建工程文件的时候,给我们自动内置了一个Objective-C版的“Hello,world”程序。
接下来,我们再重复一遍刚才创建工程的步骤,重新创建一个编程语言为C的工程文件,
按下command+R,就会触发Xcode的“Run”功能,也就是运行,
按下了command+B,则触发了Xcode的“Build”功能,也就是编译。这只是起到了编译的作用,只是把你写好的C代码转化为了二进制码,通过这个功能可以起到一些语法的检查(虽然Xcode的语法检查非常的强大)和编译时错误检查等(有可能是文件链接失败等原因)。
左上角的这部分内容,只有到iOS开发中才能用的到,
Xcode中的报错和警告有一下几种情况,
**1、可由Xcode自动修复的错误。**如果出现了这种情况,大家可以点击红点,有Xcode自动帮我们修复语法上的错误。不过这有一点需要注意的地方是,这种情况也有可能是因为我们的回车换行引起的,导致代码无法正常结束,Xcode认为你需要加上 ; 。
点击红点后,出现“Fix-it”,回车即可自动修复。
**2、可由Xcode自动修复的警告错误。**
意思是你写了一个不是很符合要求的代码,给你抛出一个警告,因为是警告并不是让代码运行不了的报错,所以你大可放心的按下Command + R运行你的代码,不过,需要注意的地方是,
如果你写了一个类型下文中的代码,虽然Xcode也只给你报了一个警告,但是也要注意,这很明显是一个类型错误的警告。那为什么都是类型错误还不报错,只是一个警告呢?大家可以做个测试,就算你写了这种代码,Xcode照样会给你编译通过,甚至运行通过。但,如果你是在做真正开发的话,一不小心写了这种代码,你就会出现值为NULL的情况,因为两个变量的类型不一样,所以做赋值操作时,就会取不到需要的值。
但是,也就是因为警告并不会让代码不能运行,所以很多同学在开发的时候就会在脑海中产生一种思想,反正我的代码能跑,报那么多的警告也没关系,就这样的吧。这种思想万万不可取,虽然Xcode给你报的警告确实不会导致你的代码不可运行,万一你写的这部分代码就在赋值的时候因为两种变量的类型差别导致取到了NULL(空)值,有极大的可能会导致整个APP崩掉。
所以,笔者推荐的做法是,先查看一遍警告的内容是什么,如果警告的内容危险度比较小,比如说,我们可能是用了一些老的API,老的函数、方法等,现在已经有了新的替代方案,所以Xcode会给你抛出一个警告,告诉你该换方法啦,这有一个比你这个好的方法。在这种情况下,我们就可以考虑暂时不用管。
Xcode中经常的出现的报错和警告情况就以上的这么两种,这是最经常出现的,还有一些别的情况,例如可能是你在导入头文件的时候,被导入的文件并不在该工程路径下,此时Xcode就会在你按下Command + R的时候给你报了一个编译链接时错误,报出这个错误后经常会带上Xcode在编译时的日志信息,大家可以在这个日志信息中看到具体是到哪步编译链接时出现的错误
如果大家在使用Xcode写代码时,发现需要进行代码分层,比如说不想把一大段代码挤在main函数里边,想要进行多文件编码 ,那怎么新建一个文件呢?
在文件索引区中随意选中一个文件或者文件夹,右键 -> New File,
然后在弹出的新页面中,选中你要创建的文件,
如果大家觉得白色的编辑区背景刺眼的话,想要更换配色的话,可以在顶部的菜单栏中选择Xcode -> Preferences,
在弹出的新页面中,选择Font & color。挑选适合自己的配色,如果大家不喜欢Xcode自带的配色,可以在页面的左下角“+”号中添加外部配色方案。外部配色方案可以在网上找到,比如笔者的这个配色就是从网上找的,毕竟每天都要面对着它,眼睛舒服是第一。
如果大家觉得字号有点小的话,可以按照下图的方式进行操作,
Xcode的简单入门就到这里了,有关更加深入的Xcode功能介绍,我们将在后面内容中进行介绍。笔者在上文中所讲述的内容都是一些基础内容,把这些基础内容都掌握了以后,用Xcode来写C和C++代码都不在话下,而且你还可以享受Xcode人性化的代码提示和自动纠错技术。
以上笔者介绍的Xcode内容与实际的iOS开发所用到的Xcode功能还差很多,上文中笔者介绍的Xcode部分使用功能,只是带大家入个门,使用Xcode完成一些课上或者课下的一些小实验或者刷刷OJ什么的,更加细致和编辑的Xcode使用技巧,我们在后天的章节中将为做一个详细讲解。
================================================
FILE: Tools/Playerground.md
================================================
## Playground
在这篇文章中将记录我在使用 `Playground` 中遇到的问题。
### 让 Playground 具备多线程调试功能
记得之前用 Playgound 做了一些关于 UI 方面的练习,涉及到了回归主线程的操作,但是一直没有效果,加了延时代码也不行,遂作罢。今天得知原来是需要在 `Playground` 中打开它的“延时运行”特性,代码如下:
```Swift
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
```
### 使用 Playground 做 UI 测试
首先需要导入 `PlaygroundSupport` 库,然后正常写 UI 代码,最后给 `PlaygroundPage.current.liveView` 赋值上我们需要展示出来的 view 即可,如下所示:
```Swift
import UIKit
import PlaygroundSupport
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(Int(arc4random_uniform(100)))"
return cell
}
}
PlaygroundPage.current.liveView = ViewController()
```
当然,如果这样直接运行是肯定不能看到 UI 测试界面的,需要 `View` -> `Assistant Editor` -> `show Assistant Editor` 。
================================================
FILE: Tools/Xcode.md
================================================
## Xcode
在这篇文章中主要讲述了我在使用 `Xcode` 过程中遇到的问题
### Xcode 简单入门
[Xcode 简单入门](./4_Xcode.md)
### 观察 App 启动时间
App 的启动时间分为“ `main` 函数加载之前的时间(`t1`)” 和“`main` 函数加载之后的时间(`t2`)” 组成的,对于 `t1` 时间来说,需要分析 app 的启动日志,打开这个日志方法如下所示:
Product -> Scheme -> Edit Scheme -> Run -> Arguments -> Enviouronment Variables -> 填入 `DYLD_PRINT_STATISTICS` ,设值为 `1` 。运行工程后,即可看到 `Xcode` 控制台中打印出:
```
platform initialization successful
Total pre-main time: 1.0 seconds (100.0%)
dylib loading time: 739.36 milliseconds (73.7%)
rebase/binding time: 24.84 milliseconds (2.4%)
ObjC setup time: 92.96 milliseconds (9.2%)
initializer time: 145.57 milliseconds (14.5%)
slowest intializers :
libSystem.B.dylib : 5.09 milliseconds (0.5%)
libglInterpose.dylib : 71.06 milliseconds (7.0%)
```
* 从打印出的信息可以看出,我的这个项目的 `t1` 时间中的主要耗时是在动态库的加载上,而动态库又分为系统界别和开发者自己添加的动态库。一般系统级别的动态库加载时间我们几乎不用进行考虑,重点要放在我们自己添加的第三方动态库上。Apple 官方推荐 app 的启动时间最好控制在 400ms 内,我这个单是动态库加载就已经快 740ms 了 😅 ,当 app 20s 后还没有启动成功,则会被系统强杀掉也就是`8badf00d`,而解决动态库加载耗时多长的办法要么合并要么删除,我目前没有太多的经验;
* 减少 OC 类数量也是个办法,同样也是要么合并要么删除。这样做可以加快动态链接,降低重定位/绑定所耗费的时间;
* 还可以使用 `initializer` 替换 `load` 方法,或是尽量将 `load` 方法中的代码尽量延后(懒加载),对象的初始化所耗费的时间会减少。但是我从未使用过 `load` 方法哈哈。
而对于 `t2` 时间来说,其主要关注的是第一个页面构建并完成的时间,比如在 `viewDidLoad` 和 `viewWillAppear` 这两方法中做的事情要尽可能简单。
### 断点行为
可以创建一些断点,只在某些条件下才触发,并且在命中断点时执行一些复杂操作。
================================================
FILE: Tools/XcodeGuide.md
================================================
# Xcode
## `liveView`
在Xcode 11 中推出了针对 `SwiftUI` 的 `liveView` 功能。前提是需要 macOS 10.15 及 Xcode 11。
在安装好以上两个必须的条件后,此时如果出现了 `Failed to code sign ContentView.swift` 的提示,说明还没有针对 Xcode 11 下载 `command line tools`,手动执行 `xcode-select --install`。
如果还是不行,原因很有可能是因为更新了系统,需要重新下载一遍 `command line tools`,下载完成后,使用命令 `xcode-select --s` 指定工作目录
## 共存 Xcode 后可能会出现的问题
1. 使用 `sudo xcode-select -s /Applications/Xcode7.app` 来切换不同的 Xcode 相关工具依赖命令。
================================================
FILE: Tools/开发中可能会用到的内容.md
================================================
* [iOS--再也不用担心数组越界](https://mp.weixin.qq.com/s/RYHquy5r33NzQNgWfbnMdA):文章主要用了给 NSArray 等集合类型添加分类,或者用 runtime 做方法替换,判断出如果下标越界直接返回 `nil`,改造成本较小。
[马赛克算法及iOS代码实现](http://www.cnblogs.com/vicstudio/p/3358358.html)
[iOS手指涂抹位置变马赛克的实现](https://www.jianshu.com/p/e4bebae1b36f)
[成熟的夜间模式解决方案
](https://draveness.me/night)
[iOS 程序 main 函数之前发生了什么](http://blog.sunnyxx.com/2014/08/30/objc-pre-main/)
[iOS 注释方法大全 代码块加快捷键自定义注释](https://www.jianshu.com/p/78b8693d87cd)
[最快让你上手ReactiveCocoa之基础篇](https://www.jianshu.com/p/87ef6720a096%20)
[iOS下JS与OC互相调用(二)--WKWebView 拦截URL](https://blog.csdn.net/u011619283/article/details/52135982)
[【iOS】二维码生成及定制
](https://mp.weixin.qq.com/s?__biz=MzA3NzM0NzkxMQ==&mid=2655359384&idx=1&sn=de5a29fc75c681e2b7b32adda1fd7978&chksm=84e25cb0b395d5a619ca46ffad4200a8fb5f085052dee7dc9e601258a9e76d4dbe2266617758&mpshare=1&scene=23&srcid=1230OAfobfb1r4Anz4ycCRog%23rd)
[对 Strong-Weak Dance的思考](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652557836&idx=2&sn=b28c54b93e6b259c0952c9a36827c426&chksm=bcd297028ba51e142bdaa2d2a70920631c697136c9ca1743571ae151a8ae09f952755fc89f87&mpshare=1&scene=23&srcid=0125GFF2yA4mDAf0lw6Rt4C4%23rd)
[基于swift4.0实现视频播放、屏幕旋转、倍速播放、手势调节,锁屏面板等功能](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652557836&idx=1&sn=1e3c0a36b0877114358e10b4e196f3ca&chksm=bcd297028ba51e14ba55589abf47731d921f71a5a071004233f2767e6ad72fc64291b3f3fed1&mpshare=1&scene=23&srcid=0125M4sq0dR788P4K5DeZJrx%23rd)
[iOS之使用CoreImage进行人脸识别](https://mp.weixin.qq.com/s?__biz=MzA5OTU3NjAxNA==&mid=2653646397&idx=1&sn=895773ae0a95a18397c46b9590e8e364&chksm=8b5fff9cbc28768a803db676189b18a8cbc8c8363c12aa4fc5820b3945b87b30566a680ce0d5&mpshare=1&scene=23&srcid=01274YNsaRSk0vwsUxI1Djbn%23rd)
[iOS表情键盘的完整实现](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652557976&idx=2&sn=0da2d0412049ca08b43dbfb8f254819c&chksm=bcd297968ba51e80517fd6886c2db98f31c6ca45606f78a38ceb90e555fb333f417765d8abcb&mpshare=1&scene=23&srcid=0128pSHyRalKpaqf1JTIhl4Z%23rd)
[iOS WebViewJavascriptBridge源码解析](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652557976&idx=3&sn=04926bdaa39d022fd04fafc6ad4fdbc5&chksm=bcd297968ba51e80ecc38f956d04b00b3b6ca239f096251f4de1db2b534cc22eadcdce16fa36&mpshare=1&scene=23&srcid=0128qr23WbkVW9uG8QYfJzoS%23rd)
[Xcode 9 —进阶的 iOS Simulator](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652557872&idx=2&sn=ceafb4906f5026216e33600321f00609&chksm=bcd2973e8ba51e28d38b5474998cb15b005c56404e184c881a1d334282310420656394596f8a&mpshare=1&scene=23&srcid=0128bDqpwQHqOwnMoFpG7oCR%23rd)
[最全iOS数据存储方法介绍:FMDB,SQLite3 ,Core Data,Plist,Preference偏好设置,NSKeyedArchiver归档,Realm](https://www.jianshu.com/p/e88880be794f)
[iOS开发·必会的算法操作:字符串数组排序+模型对象数组排序](https://mp.weixin.qq.com/s?__biz=MzA3NzM0NzkxMQ==&mid=2655359440&idx=1&sn=5b17e680c035cdd6ef43728d7bd6c851&chksm=84e25cf8b395d5eeacf09b10f05bcf475b798091926dbd13ebd5369615b9cc089af3c39f937f&mpshare=1&scene=23&srcid=0130ITPrAulxVoIDMJrhAk4a%23rd)
[探索iOS内存分配](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558092&idx=1&sn=819f05b18513e3a1a7e786dc0fb0e57a&chksm=bcd296028ba51f14865218cf6a685fd2566955bb0b03fd6593a09e93f01599264d683e09a4ae&mpshare=1&scene=23&srcid=0201WSlq1IqmXhfP6NPWPPyU%23rd)
[数据结构 & 算法 in Swift (一):Swift基础和数据结构](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558092&idx=2&sn=2f7e7a356aead2df251e27c29fb9aa82&chksm=bcd296028ba51f146d608ef31ead77d45886d5d94fa745707a4ab328ceb4b3c2661bf2b90770&mpshare=1&scene=23&srcid=0201VBElkFnGYnLDKyC2u4p2%23rd)
[在 iOS 中实现区块链](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558119&idx=1&sn=746701ab3da2b61a205811c321b1ade8&chksm=bcd296298ba51f3f4b21e2127767d6b39d84081c806b2b7f1b302374f16a945304c20982ea7e&mpshare=1&scene=23&srcid=0202BqOslUjUWXkIK9dCdehk%23rd)
[iOS - 关于数据持久化不看我看谁(一)](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558052&idx=3&sn=6261d9a56cda6398032bcbf244241c2f&chksm=bcd297ea8ba51efc0e99b7ff95ccf6ca498e9c5273bed3d9a70de373f6ab3d3348b935d32408&mpshare=1&scene=23&srcid=02023Sh9dmBpkTJzkU9JhGi7%23rd)
[GCD队列、同步异步
](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558031&idx=2&sn=a068ba558b1aeff01e0bc7cf5d6cbf5c&chksm=bcd297c18ba51ed778d73daed05a1d994fe8581a40c61f60452e444a44fb3993cbaa5897f3aa&mpshare=1&scene=23&srcid=020265ssiqObut98pMV92mP6%23rd)
[博客百篇,其义自见](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558194&idx=3&sn=4e6951d72288b9bd807924de49d302fb&chksm=bcd2967c8ba51f6ac9f3f7e507a483d0919a3a3df2509a848e3179d9c7727723dadf6ebef60e&mpshare=1&scene=23&srcid=0204MZK95ylfM9lElnbNvocN%23rd)
[Runtime在工程开发中的应用: 一行代码监听手机摇动!](https://zhuanlan.zhihu.com/p/33057126?utm_medium=social&utm_source=qq)
[从零开始学基于ARKit的Unity3d游戏开发系列1](https://zhuanlan.zhihu.com/p/32224102?utm_medium=social&utm_source=qq)
[玩转CocoaPods
](https://zhuanlan.zhihu.com/p/33537788?utm_medium=social&utm_source=qq)
[iOS 中 iBeacon 开发](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558410&idx=1&sn=221802a22de741e16b8496ba84950b98&chksm=bcd291448ba51852c7138ed5801856b11976c9554d9e8b37946c69ec9a82652da916233ea1f1&mpshare=1&scene=23&srcid=02125xwXy0dmMpODZJiwqouG%23rd)
[iOS-Charts看这个就够了](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652558346&idx=2&sn=7980c187c388cfe6fbb8ecdb7d7ddb68&chksm=bcd291048ba5181264b3de96b3c3f0425514ecb400e10f9b5c9f2954f21648d4679b4c8f64f0&mpshare=1&scene=23&srcid=0212nKyk4xdTZQfkADk3wVX9%23rd)
[iOS--UISearchBar 属性、方法详解及应用(自定义搜索框样式)](https://mp.weixin.qq.com/s/xQKQrDZtFBg-gAF-tL3U7w)
[iOS启动原理(一)](https://mp.weixin.qq.com/s?__biz=MjM5OTM0MzIwMQ==&mid=2652559625&idx=3&sn=2e1d803d6a3659b539c33a012a83aa96&chksm=bcd29c078ba51511dad574ac97e3db542f0577ccc834cad6bb077f35e4648444ee1c1591f5e3&mpshare=1&scene=23&srcid=0408iwz51NEttufn5b5sPORL%23rd)
[(iOS)视频添加动效水印实现介绍](https://www.jianshu.com/p/9f4f37e5abc6)
[可以在不用打开Xcode打开模拟器的命令行工具](http://cloudstone.xin/2015/07/08/%E5%9C%A8OS%20X%E4%B8%AD%E5%BF%AB%E9%80%9F%E6%89%93%E5%BC%80iOS%E8%AE%BE%E5%A4%87%E6%A8%A1%E6%8B%9F%E5%99%A8/)
// 持续更新
================================================
FILE: Toturial/剪刀石头布.md
================================================
## 剪刀石头布
该项目为[ifLab](https://iflab.org)的iOS方向新生练习项目之一,为接触了Objective-C语言后的第一个UI小练习。
主要涉及的内容有:
1. Objective-C语言初等语法实践;
2. StoryBoard基本功能使用;
3. Xcode基本功能使用。
这是最终完成图:
玩家选择要本局游戏是出“拳”、“布”还是“剪刀”,点击“开始”按钮后,电脑(以下简称AI)自动算出要出的子,最后进行游戏输赢判断。
如果你是第一次接触iOS开发UI方面的东西,或许会对其产生疑惑,怎么就把之前写的“黑框框”程序变成了带有UI元素的App了呢?其实我们可以把App分为三层,其中负责写“剪刀石头布”逻辑代码的部分,我们称之为`Model`层,`Model`层主要负责的事情就是输出数据,每次我们访问一次`Model`层的相关方法都能获得数据。
而显示出的UI,比如上图中所示的emoji和蓝色的按钮,这一白色背景上的东西我们可以称之为`View`层,在这一层上,主要负责UI控件的布局及更新,比如我们选择了“剪刀、石头、布”三者之一,就会给其套上一个边框,继续选择其它两个该边框会“移动”过去,这就是`View`层的更新操作。
但是我们怎么把从`Model`层获取到的数据放到`View`层中去呢?我们还需要一个管理者,也即`Controller`层,它的主要责任就是决定何时何地把`Model`层获取到的数据更新到`View`层中。
关于Xcode的介绍和简单使用,大家可以参考[这篇文章](../iOS/相关工具使用/4_Xcode.md)。
#### 创建工程
打开Xcode -> "create a new Xcode project" -> "Single View App" -> 填写相关信息。
此时点击Xcode左上角类似播放的按钮(下文简称"run"),此时Xcode将会编译该工程,且运行起一个模拟器,如果发现并没有模拟器运行起来,可以选择`run`按钮右边项目工程名字边上的设备下拉框,选择一个你所喜欢模拟器,再次`run`即可。
你会看到模拟器运行起来是一片空白,对,它就应该是一片空白,如果我们此时想要对它做些改变,在Xcode的左边栏的文件目录下,找到并点击一个名为`Main.storyboard`的文件。
没错,它就是导致了模拟器运行起来一片空白的原因。打开Xcode的右侧边栏,在右侧边栏的最下边找到一个类似铜钱的按钮,如下所示,
点击这个类似铜钱的按钮,在其中随意拖拽一个控件(按钮鼠标或者触控板)到刚才看到的空白页面上即可,如下所示,
现在再次`run`工程,模拟器跑起来后,你会发现上边多了刚才你所拖拽进去的控件。现在你已经会进行拖拽控件了,我们要做本文开始之前的页面,这个页面就是用拖拽控件的方式进行搭建的。
想要完成本文开始的页面(以下简称首页),我们需要`UIButton`、`UILabel`、`UIProgressView`这这种控件,“剪刀、石头、布”以及“开始”按钮都是`UIButton`类型,位于“开始”按钮之下的提示语控件类型为`UILabel`,而位于头部和底部用于记录双方各自得分的控件为`UIProgressView`。
#### 界面搭建
在对`UIButton`类型控件的自定义时,注意下图所示要点:
搭建完的界面属于`View`层,而我们想要对`View`层的数据进行修改,必须要拿到这个控件,因此,接下里我们需要把这些位于`View`层上的控件拖拽到对应的`Controller`层中供后续的`Model`层数据源刷新后`View`层的刷新。
我们来尝试拖拽第一个`View`层上的控件,如果你是第一次进行拖拽控件,肯定会出问题🙂,具体操作如下图所示,
我们要把拖拽的文件丢入哪个文件呢?单针对Objective-C来说,我们一般把控件拖拽在`@interface ...... @end`之间,如果你的这个控件想要被外部类访问,那就放在`.h`文件中,如果这个控件你只要被该类内部自己使用,那就拖拽到`.m`文件的`@interface ...... @end`之间。针对“剪刀石头布”这个项目,因为我们只有一个`Controller`文件,还没涉及到分层,并且`View`层和`Model`层都混在`Controller`层中,因此,我们选择拖拽到`ViewController.m`的`@interface ...... @end`之间。
如果发现按照上图的操作并不能自动的帮你定位到相关文件,可以按照如下图所示操作进行资源定位,
现在,相信你已经完全的把所有控件都拖拽好了。是的,我们需要把所有控件都拖拽到对应文件中。现在`ViewController.m`文件的`@interface ...... @end`之间看起来应该差不多是这个样子的,
#### 逻辑部分
现在,让我们再一起来梳理一遍“剪刀石头布”的游戏逻辑。不确定大家是否有自己编写过相关代码,再次就先当大家都不知道吧,简单的来说一说.
我们分别给“剪刀”、“石头”、“布”三个子设置标签(以下简称为tag),且假设剪刀tag为`V`,石头tag为`O`,布tag为`W`,对于玩家来说,在这三个tag中选一个就好,但是对AI来说,它需要随机的自动选择一个对应的tag。
重点就在这个随机上,如果大家有过C语言的基础,就一定知道如如何取随机数,而对于这个简单的“石头剪刀布”项目来说,我们只需要对取出的随机数对3求余即可。最后拿到AI随机出来的tag和玩家选择好的tag进行正常的游戏规则匹配即可。为了标识出玩家和AI各自选择的tag,对玩家和AI我分别设置了`_kUserStr`和`_kMachineStr`这两个`NSString`类型对象,记录下双方所选择tag。
对于AI部分的随机取数逻辑可以为这样:
```Objc
- (void)machineMethon {
switch (arc4random() % 3) {
case 0:
_kMachineStr = @"✊️";
break;
case 1:
_kMachineStr = @"🖐";
break;
case 2:
_kMachineStr = @"✌️";
break;
}
}
```
而对于玩家来说,有三个按钮“剪刀”、“石头”、“布”可供选择,而且这三个按钮的方法逻辑是一样的。我们来给这三个按钮绑定同样的点击方法,
```Objc
[_ManRockBtn addTarget:self action:@selector(ManBtnClick:) forControlEvents:UIControlEventTouchUpInside];
[_ManPaperBtn addTarget:self action:@selector(ManBtnClick:) forControlEvents:UIControlEventTouchUpInside];
[_ManScissorsBtn addTarget:self action:@selector(ManBtnClick:) forControlEvents:UIControlEventTouchUpInside];
```
而AI和玩家的判赢方法,主要被“开始”按钮触发,
```Objc
- (void)judgeMethonWithUserAndMachine {
[self machineMethon];
if ([_kUserStr isEqualToString:_kMachineStr]) {
_statusLabel.text = @"平局";
} else if ([_kUserStr isEqualToString:@"✊️"] && [_kMachineStr isEqualToString:@"✌️"]) {
_statusLabel.text = @"你赢了";
[self progressViewUpdate];
} else if ([_kUserStr isEqualToString:@"✊️"] && [_kMachineStr isEqualToString:@"🖐"]) {
_statusLabel.text = @"你输了";
[self machineProgressUpdate];
} else if ([_kUserStr isEqualToString:@"✌️"] && [_kMachineStr isEqualToString:@"🖐"]) {
_statusLabel.text = @"你赢了";
[self progressViewUpdate];
} else if ([_kUserStr isEqualToString:@"✌️"] && [_kMachineStr isEqualToString:@"✊️"]) {
_statusLabel.text = @"你输了";
[self machineProgressUpdate];
} else if ([_kUserStr isEqualToString:@"🖐"] && [_kMachineStr isEqualToString:@"✌️"]) {
_statusLabel.text = @"你输了";
[self machineProgressUpdate];
} else if ([_kUserStr isEqualToString:@"🖐"] && [_kMachineStr isEqualToString:@"✊️"]) {
_statusLabel.text = @"你赢了";
[self progressViewUpdate];
}
[_MachineBtn setTitle:_kMachineStr forState:UIControlStateNormal];
for (UIButton *btn in _kBtnArr) {
btn.layer.borderColor = [UIColor whiteColor].CGColor;
}
isBegin = false;
}
```
在判赢方法中你会发现我们多了一个数组`_kBtnArr`,而且还遍历了这个数组的中的内容,给该数组的对象设置了边框颜色。这是怎么一回事呢?
为了标识出玩家到底选择了“剪刀”、“石头”、“布”三个按钮中的哪一个,我们使用了`UIButton`类型控件的一个`边框`属性,当用户点击到对应的按钮,就给该按钮设置一个边框,并且其他未选中的按钮边框选中效果清空(clear颜色)
因此,我们的按钮点击方法可以这么写,
```Objc
- (void)ManBtnClick:(UIButton *)sender {
sender.layer.borderColor = [UIColor blueColor].CGColor;
sender.layer.borderWidth = 2.f;
if ([sender.titleLabel.text isEqualToString:@"✊️"]) {
_kUserStr = @"✊️";
} else if ([sender.titleLabel.text isEqualToString:@"🖐"]) {
_kUserStr = @"🖐";
} else {
_kUserStr = @"✌️";
}
for (UIButton *btn in _kBtnArr) {
if ([btn isEqual:sender]) {
continue;
} else {
btn.layer.borderColor = [UIColor whiteColor].CGColor;
}
}
// 点击了按钮即可认为游戏开始
isBegin = true;
}
```
以上就是“剪刀石头布”项目的核心代码,注意!是核心代码,不是完整代码,如果你对本项目感兴趣,可以在文末原项目链接down下后进行研究。再次说明该项目为[ifLab](https://iflab.org)的iOS方向新生练习项目之一,为接触了Objective-C语言后的第一个UI小练习,同时也适用于刚接触iOS开发的同学。
项目地址:[https://github.com/windstormeye/FreshManPractice](https://github.com/windstormeye/FreshManPractice)
================================================
FILE: UI/3_StoryBoard.md
================================================
# **StoryBoard**
Storyboard是苹果官方主推的一个代替xib的策略。下图为StoryBoard整体的一个介绍(来源网络),当然,我们不会在本节内容中把每个知识点都讲清楚,笔者将会以几集视频为主,告诉大家如何使用StoryBoard搭建UI界面。

哔哩哔哩地址:[StoryBoard 01](https://www.bilibili.com/video/av22231774/?p=1)
哔哩哔哩地址:[StoryBoard 02](https://www.bilibili.com/video/av22231774/?p=2)
哔哩哔哩地址:[StoryBoard 03](https://www.bilibili.com/video/av22231774/?p=3)
================================================
FILE: Weex/Weex新手记.md
================================================
# Weex 新手记
## 前言
上周五 leader 跟我说了一下,想让我转大前端,周一让前端组长跟我聊一下,当时我内心还是比较兴奋的,因为这跟我最开始对自己的定位是完全一致的,但后续做了一些后端的东西后,发现自己对后端也有感觉,不过实话实说都还在比较浅薄的层面上。
上周末我想了两天,发现自己确实得在大前端这块上继续“抛头颅,洒热血”,不单是再精进一些之前已掌握的知识,更是拓展了自己在稍微擅长的一个领域站得更稳,刚好最近负责的产品有往“跨平台”的开发模式上转,需要提前做些调研工作,再跟上最近团队人员的变动,新来的同学有意愿切入 Weex 跨平台框架。
当然,对于我个人而已,我完全不喜欢任何非原生的东西,甚至目前到了用别人的库都觉得“怪异”,今年年初的时候把当时最火的两个跨平台框架“小程序”和 “React-Native” 都摸了一遍,也都把 demo 写了出来,但就仅仅从 demo 这个级别出发,当时就已经非常的厌恶各种跨平台的开发框架了,总有些说不出的“怪异”之处,但也不能说它们毫无是处,在编写 demo 的过程中,很多基础组件上手就用,比 Native 不管是从速度上还是便利上都大大提升了不只一两倍这么简单。
经过一周短暂的、磕磕碰碰的学习,也把今年学习的第三个目前也十分火爆的跨平台框架—— Weex 写出了 demo,其中大部分思路来源于 Weex 的官方文档和教学视频。如果你感兴趣的话,可以扫描下方二维码进行试玩:
## Weex 简单介绍
对于 Weex 的介绍在网上已有大量的讲解,在此用自己的话可以总结如下:
* Weex 写起来很爽,前提是:对动画无要求;对交互无要求;对性能无要求;业务逻辑不复杂。
* 如果你司技术栈 `Javascript` 为主体且依赖 `Vue`,排除第一条后,上 Weex 几乎没有悬念,毕竟到现在 Weex 都被认为是 `Vue-Native`。
* 如果你司已经沉淀出了大量 Native 经验,上 Weex 几乎可以认定公司要倒(只是比喻 😄 );反之,如果你司是 web 主导,想切入 Native 端,满足第一条后,再考虑 React-Native 是否符合自身技术栈,其次再来考虑 Weex。验证我这番话,可以对比 RN 和 Weex 的官方文档和社区,当然 Weex 还是十分的年轻,但和 React-Native 二者正式开源也就前后相隔一年左右。
* 据我所知,目前“极客时间“、”企鹅电竞“等 App 已经是纯 Weex 开发,甚至桔厂”顺风车“也全面拥抱,可见其威力有多大!
## 知识点
该 demo 中运用到的主要相关知识点如下:
* Weex 内置组件:`div`,`text`
* Weex 内置模块:`navigator`,`storage`,`dom`
* Weex custom Component
* `CSS` 基本内容
* `Vue.js` 基本内容
* `Javascript` 基本内容
## 页面展示
### index.html
### add.html
### detail.html
## 开发过程
Weex 吸收了目前最流行的 MVVM 和面向组件开发的思想,上文中我所说的“爽”就来自于此!举一个例子,`navbar` 组件,编写一个组件相对 Native 来说,真的是又快又爽!在 `` 中写好组件模版代码,在 `
```
## 相关注意点
### 安装 Weex 工具包
`npm install weex-toolkit -g`
### 从零开始创建 Weex 工程
`weex create awesome-app`
在创建工程的过程中,会提示一些关键信息,比如作者、是否使用 `vue-router`,`ESLint` 等等,根据提示等待即可。
### 添加 iOS 工程
`weex platform add ios`
### 构建 js bundle
`weex run build`
在 dist 文件夹下拿到对应的 js bundle 文件。
### 切换显示
在工厂目录下执行 `npm start` 后,会在浏览器打开一个“套壳”的页面,有很多不需要的元素,如果不需要的话,可以这么做:
* 假设执行 `nmp start` 后,打开的地址为:`http://172.20.10.4:8081/web/preview.html?page=index.js`
* 把地址改为:`http://172.20.10.4:8081/index.html`,这样就去除掉了多余不需要的元素了,页面变得十分干净
### 新增页面
新增页面后,此时如果通过浏览器直接输入地址访问会 404,因为此次 build 出来的资源文件中并未包含我们新增的页面,需要重新执行 `npm start` 进行重新构建。
### flex-direction
决定你的页面布局主要方向,是**row**(水平)还是**column**(垂直布局)。
### align-items
决定父容器中的元素在水平方向上的布局,想要居中则设置为 `center`。
### align-content
决定父容器中的元素在垂直方向上的布局,想要居中则设置为 `center`。
### justify-content
决定父容器中的元素在主轴上如何排列,如果想要等分布局,则设置为 `space-around`,左右边距将为中间间隔的一半。
## align-items
决定元素在交叉轴上如何排列
### dist
通过 webpack 打包后生成的 `JS Bundle` 文件都在 `dist` 文件夹下。
### 在模版中,Vue 会把驼峰命名的组件自动转换成短横线连接
### Boolean
在 Weex 中关于 bool 值,本质上为字符串,比如`"true"` 这样才是“真”,`true`这样什么也不是,官方说会在未来版本中进行修复,还有很多类似这种容易引起“差评”的地方。
### Weex SDK
构建出来的 js bundle 直接直接可以拖入工程使用,在 iOS 下,看到的渲染后的页面层级如下:
查看 WeexSDK,可以看到基本上把原生组件都按照 Weex 支持的格式封装了一遍,所以加入跨平台框架后,app 体积不上升是不可能的,只不过得看用什么个优化方法了(删删删哈哈哈~)
## 总结
本次 Weex demo 的练习,让自己对 Weex 和 Vue 都有了一个直观的感受,到现在给我印象最深刻的不是 Weex 而是 Vue,感触良多。对原生开发的喜爱又多了不少,在今天这个时代背景下,求快不求稳,不管怎么说,我还是一个鉴定的原生开发者~
================================================
FILE: Win/basic.md
================================================
## win
### 程序包
#### exe
#### WPF
Windows Presentation Foundation
#### UWP
Universal Windows Platform,该类型应用本质上也是一个 exe,但会经过一层包装,需要被微软统一审核,使用受限的 API。
有些 win32 程序重写比较费劲,但可以通过 win32 转译到 UWP,同样给其装上一个容器,使其运行在其中。
##### 缺点
* 添加入口进右键菜单需要 UAC 提权。
================================================
FILE: iOS/Layout.md
================================================
# Layout
这篇文章中将记录 iOS 中 `Layout` 相关的内容。
## `Auto Layout` 介绍
### 有歧义的布局
想要测试一个 `UIVIew/NSView` 的布局约束是否充分,可以在 `loadView` 方法中或者其它建立新视图并添加约束的地方,使用以下属性进行判断。
```swift
view.hasAmbiguousLayout ? "Ambiguous" : "Unambiguous"
```
### 纠正有歧义的布局 & 可视化约束
如果我们想要可视化约束布局,可以使用 macOS 中特有的方法。在 macOS 中,可以通过在任何 `NSWindow` 实例上调用 `visualizeConstraints` 方法,传入需要进行可视化的所以约束集合作为参数即可。

我们可以点击约束,该约束的信息就会显示在 Xcode console 中。
### 内在内容大小
每一个 `UIView` 都一个「内在内容大小」的属性 `view.intrinsicContentSize`,能够直接获取到其内容的真实大小。
> 引发的思考:那这么说做 `UILabel` 的精准伸缩就有救了哈哈哈哈~
### 本章小结
* **问题 1**:一个 54 * 54 点的图像由一个 50 * 50 点的正方形加上一个下拉阴影组成,阴影偏移量为向右 4 点和向下 4 点。将该图像添加到一个图像视图中,并且在两个坐标轴上均相对于其父视图居中时,这个图像中的哪几个几何点位于父视图的中心?写出将 `inset` 赋予该图像的代码。
- 中心点位于未调整图像的 (27, 27),已调整图像的 (25, 25)
- ```swift
let img = UIImageView(image: UIImage(named: "233"))
img.alignmentRectInsets = UIEdgeInsets(top: 0, left: 0, bottom: 4, right: 4)
```
## 约束
### 本章小结
* **问题 1**:当两个相互冲突的规则恰好有相同的优先级时,运行时会发生什么情况?
- 会自动打破其中一个规则
* **问题 2**:为什么使用 251 和 249 之类的优先级要优先于使用像 257 和 243 之类的优先级?
- 250 是 `Auto Layout` 预定义的“低”优先级
- 一个相对标准值的个位数偏移量,表示一个相对于该标准值设置的优先级
================================================
FILE: iOS/More-弹幕.md
================================================
这是iOS开发More系列的弹幕练习总结。关于弹幕的实现在GitHub上已经有一堆的实现了,国内外都有大量的第三方库,并且做的都不错,但是给我的感觉弹幕的简单实现并不需要多少精力,遂有了这次练习。
先来看整体实现(可能有些丑😓),
此次的弹幕实现只是个练习,很多地方都做得不够完善,比如并未加入实时视频流,所以实际上实现的甚至连demo都不是,只能勉强说是造了个型,只抓住了最核心的部分而已。
```shell
.
├── 11.png # 头像
├── AppDelegate.h
├── AppDelegate.m
├── BulletManage.h # 弹幕管理类
├── BulletManage.m
├── BulletView.h # 弹幕View
├── BulletView.m
├── ViewController.h
├── ViewController.m
└── main.m
```
实现弹幕练习的主要文件目录结构如上所示,可以看到实际上核心类只有BulletManager和BulletView而已,BulletManager负责管理弹幕整体的开始和结束,比如弹幕数据源的获取、弹幕View的初始化、根据弹幕的Start,Enter,End三个状态分别管理对应状态弹幕等,而BulletView则负责管理每个弹幕本身,包括动画时长、何时进入、位于哪个弹道、自身当前状态等。虽然只是个弹幕练习,但是我猜测应该不是使用原生的视频播放器类,要么继承要么重写,否则弹幕整体的View层级和原生视频播放器类是会冲突的,导致弹幕上不去。
弹幕练习涉及到的UI部分功能编写较多,所以总结中不会涉及到从零开始进行讲解,而是重点放在核心代码部分,具体细节可在文末项目地址load工程进行查看。
```ObjC
- (void)startAnimation {
// 根据弹幕长度执行
// v = s / t
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
CGFloat duration = 4.0f;
CGFloat wholeWidth = screenWidth + CGRectGetWidth(self.bounds);
// 弹幕开始
if (self.moveStatusBlock) {
self.moveStatusBlock(Start);
}
// t = s / v
CGFloat speed = wholeWidth / duration;
CGFloat enterDuration = CGRectGetWidth(self.bounds) / speed;
[self performSelector:@selector(enterScreen) withObject:nil afterDelay:enterDuration];
__block CGRect frame = self.frame;
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
frame.origin.x -= wholeWidth;
self.frame = frame;
} completion:^(BOOL finished) {
[self removeFromSuperview];
if (self.moveStatusBlock) {
self.moveStatusBlock(End);
}
}];
}
- (void)enterScreen {
if (self.moveStatusBlock) {
self.moveStatusBlock(Enter);
}
}
- (void)stopAnimation {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self.layer removeAllAnimations];
[self removeFromSuperview];
}
```
以上是BulletView的开始动画方法实现,我们默认每一条弹幕都是从手机屏幕最右边移动到屏幕最左边,移动时间定义为4秒,执行该方法时需要给一个值回调,告诉外部初始化弹幕的类,该条弹幕现在的状态为Start,当弹幕从屏幕最右边即将出现的那一瞬间我们需要把弹幕的状态改为Enter,Enter状态一直持续到弹幕移动到手机屏幕最左边即将消失的那一瞬间。
保持Enter状态的距离注意应该是由当前手机屏幕的宽度+弹幕的实时长度而不只是屏幕的自身宽度而已,关于计算弹幕的实时长度在此推荐使用NSString的`sizeWithAttributes`方法。并且,刚开始我使用的是GCD的after方法去做`enterDuration`时间过后的弹幕销毁,但实际上使用GCD的after方法会一直在`enterDuration`后循环执行,会导致空指针异常,推荐使用基于runtime的`performSelector`延迟方法。
接下里我们来瞅瞅BulletManager弹幕管理类都做了哪些工作。首先是初始化弹幕,默认弹道为三个,
```Objc
- (void)initBulletComment {
NSMutableArray* trajectorys = [NSMutableArray arrayWithArray:@[@(0), @(1), @(2)]];
for (int i = 0; i < 3; i++) {
if (self.bulletComment.count > 0) {
// 通过随机数获取到弹幕轨迹
NSInteger index = arc4random() % trajectorys.count;
int trajectory = [[trajectorys objectAtIndex:index] intValue];
[trajectorys removeObjectAtIndex:index];
// 去除弹幕数据
NSString* comment = [self.bulletComment firstObject];
[self.bulletComment removeObjectAtIndex:0];
// 创建弹幕
[self createBulletView:comment trajectory:trajectory];
}
}
}
```
在创建弹幕的方法中,每创建一个弹幕我们都会拿到一个block回调`moveStatusBlock`,其有一个状态参数Status,当status发生变化时,都会进入到该block回调中,从上边的弹幕初始化方法中我们也看到了实际上只创建出了三个弹幕而已,而余下的弹幕我们通过了每个弹幕都持有的block回调进行创建。使用block回调能够较为简约的处理一个实例的各种状态值变化时所引发的二次操作。
```ObjC
- (void)createBulletView:(NSString *)comment trajectory:(int)trajectory {
if (self.isStopAnimation) {
return ;
}
BulletView* bulletView = [[BulletView alloc] initWithComment:comment];
bulletView.trajectory = trajectory;
[self.bulletViews addObject:bulletView];
__weak typeof (bulletView) weakBulletView = bulletView;
__weak typeof (self) weakSelf = self;
bulletView.moveStatusBlock = ^(MoveStatus status){
if (self.isStopAnimation) {
return ;
}
switch (status) {
case Start: {
// 弹幕开始进入屏幕,将view加入弹幕管理的变量bulletViews中
[weakSelf.bulletViews addObject:weakBulletView];
break;
}
case Enter: {
// 弹幕完全进入屏幕,判断是否还有其他内容,如果有则在改弹幕轨迹中创建一个弹幕
NSString *comment = [weakSelf nextComment];
if (comment) {
[weakSelf createBulletView:comment trajectory:trajectory];
}
break;
}
case End: {
// 弹幕飞出屏幕后从bulletView中删除,释放资源
if ([weakSelf.bulletViews containsObject:weakBulletView]) {
[weakBulletView stopAnimation];
[weakSelf.bulletViews removeObject:weakBulletView];
}
if (weakSelf.bulletViews.count == 0) {
// 此时屏幕上已无弹幕,开始循环播放
self.isStopAnimation = true;
[weakSelf start];
}
break;
}
}
};
if (self.generateViewBlock) {
self.generateViewBlock(bulletView);
}
}
// 取下一个弹幕
- (NSString *)nextComment {
if (self.bulletComment.count == 0) {
return nil;
}
NSString *comment = [self.bulletComment firstObject];
if (comment) {
[self.bulletComment removeObjectAtIndex:0];
}
return comment;
}
// 弹幕停止
- (void)stop {
if (self.isStopAnimation) {
return ;
}
self.isStopAnimation = true;
[self.bulletViews enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
BulletView* view = obj;
[view stopAnimation];
view = nil;
}];
[self.bulletViews removeAllObjects];
}
```
在弹幕的`stop`方法中,使用到了一个枚举器,而枚举器是一种苹果官方推荐的更加面向对象的一种遍历方式,相比于for循环,它具有高度解耦、面向对象、使用方便等优势,当然,你会发现其和for-in有一丢丢思想上的相似,id类型对象`obj`为遍历枚举到的每一个对象,`idx`为当前枚举到的所在数组的下标,NSDictionary同样也支持该方法,`idx`换为了`key`,BOOL类型`stop`为跳出枚举循环的标记,赋值为true即可退出。
---
以上就是本次弹幕练习的总结,只涉及到了核心代码,还有写小的细节没有说到,[详细代码见工程😝](https://github.com/windstormeye/iOSMorePractices/tree/master/liveCommentingPratices)
================================================
FILE: iOS/Objective-C/More-Audio.md
================================================
这段时间陆陆续续的在做一些关于iOS开发细节的东西,先是跟进了音频部分(以下简称为Audio),主要分为以下几大部分:
1. Audio的架构和框架
2. 编解码/文件封装格式
3. 播放系统声音/震动/提示声音
4. 综合demo
5. 使用AVFoundation框架进行中英文语音识别
说起iOS中的Audio,耳熟能详的就是AVFoundation,毕竟它是个全能型的框架,不过的AVFoundation现在的地位可以类比JavaScript现在的地位,JavaScript现在甚至都插手嵌入式开发了🙂。
但也就是这种什么所谓的全能型选手,拥有大而全的技能,却缺少了一些底蕴。也就是在这段时间中,我才发现,居然还有专门针对3D音效的openAL、擅长编解码过程的AudioToolBox等等一些非常优秀的音频处理框架,重点是这些框架都是iOS SDK中本身就提供了的。
根据网上资料,梳理了如下一张在iOS中的音频处理各个框架所处的位置,
## 高层服务
### AVAudioPlayer
**基本操作:**播放、暂停、停止、循环等等一些基本的音频播放功能。
**控制:**可对音频进行任意时间位置播放;进度控制。
**其它:**可从文件或缓冲区播放声音;获取音视频关键参数,如音频标题、作者、功率等等。
如果我们并不想实现比如3D立体音效,精确的音频歌词同步等功能,那么这个框架所提供的API是完全足够的,但是如果我们想要的进行一些比如对音频流的捕获,捕获后还要进行一些RTSP、RTMP等流媒体协议的处理,再或者进行一些RAC、PCM或PCM转MP3等一些音频的转码方式处理,那这个框架就非常捉鸡了。🙂但是它能够非常轻松的进行简单的音频操作,如上所示基本操作、控制等。
### AudioQueue
相对于AVAudioPlayer来说,其更加强大!它不仅能够完成播放音频和录制音频,还能够通过AudioQueue拿到音频的原始信息,想想看!我们能够拿到音频的原始信息,那就可以做比如任意的编码解码、一些特效转化如变音等等骚操作!我们还可以进行任意的应用层封装,比如说封装成适用于RTMP、RTSP的流媒体协议处理。
使用Audio Queue,我们只需要进行三个步骤即可:
1. 初始化Audio Queue。添加一些播放源、音频格式等。
2. 管理回调方法。在回调方法中我们可以拿到音频的原始数据。
3. 实例化Audio Queue。使用AudioQueueOutput完成音频的最终播放。
### openAL
emmm,看到openAL我会想到openGL,openGL主要是用于处理一些3D的图像或变化,openAL主要是在声源物体、音效缓冲和收听者这三者之间进行设置来实现3D效果,比如可以设置声源的方向、速度、状态等,所以我们可以听到声音由远及近的这种3D效果。
总的来说,openAL主要有三个方面,
1. 声源的设置;
2. 接收者的控制;
3. 声源模式的设置。例如声源是由远及近运动,还是由近及远运动,我们还可以把声源设置在一个3D空间中。
### AudioFile
对音频文件的信息进行读取(注意不是对音频文件进行编解码),通过AudioFile框架的相关API对一个音频文件信息进行读取,主要有以下几大步骤:
1. AudioFileOpenURL。首先我们要通过一个URL打开音频文件。
2. AudioFileGetPropertyInfo。获取我们想要读取的音频文件信息类型。
3. AudioFileGetProperty。得到相关音频的属性NSLog出来即可。
4. AudioFileClose。关闭音频文件。(打开文件就要关闭文件🙂)
从上我们看到基本上都是归类于Get方法,但是AudioFile也提供了一个丰富的set方法,可以实时的修改对应音频相关信息。
举个🌰🍐!!!
我们首先得引入`#import
`框架,从Xcode 7开始,我们就不需要手动引入framework了,因为当我们引入iOS SDK中对应的framework中的相关.h文件时,Xcode会自动帮我们导入对应的framework。
```ObjC
// 首先从应用沙盒中提取音频文件路径
NSString *audioPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"mp3"];
// 转置成URL
NSURL *audioURL = [NSURL fileURLWithPath:audioPath];
// 打开音频
// 设置音频文件标识符
AudioFileID audioFile;
// 通过转置后的音频文件URL,打开获取到的音频文件
// kAudioFileReadPermission:只读方式打开音频文件;(__bridge CFURLRef):只接受C语言风格类型变量,所以我们要用一个强转桥接类型转回去
AudioFileOpenURL((__bridge CFURLRef)audioURL, kAudioFileReadPermission, 0, &audioFile);
// 读取
UInt32 dictionarySize = 0;
AudioFileGetPropertyInfo(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, 0);
CFDictionaryRef dictionary;
AudioFileGetProperty(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, &dictionary);
// 经过以上两步,我们就拿到了对应音频的相关信息。再强转桥接类型回去即可。
NSDictionary *audioDic = (__bridge NSDictionary *)dictionary;
for (int i = 0; i < [audioDic allKeys].count; i++) {
NSString *key = [[audioDic allKeys] objectAtIndex:i];
NSString *value = [audioDic valueForKey:key];
NSLog(@"%@-%@", key, value);
}
CFRelease(dictionary);
AudioFileClose(audioFile);
```
运行工程后,即可看到对应的log,
与iOS Audio有关的framework有:
| framework Name | uses |
| - | -: |
| MediaPlayer.framework | VC,提供一些控制类ViewController,使用起来较为简单,致命缺点:功能单一,对底层API高度封装、高度集成,不利于自定义 |
| AudioIUnit.framework | 底层,提供核心音频处理插件,例如音频单元类型、音频组件接口、音频输入输出单元,用于控制音频的底层交互 |
| OpenAL.framework | 3D,提供3D音频效果 |
| AVFoundation.framework | 全能型,音频的录制、播放及后期处理等(基于C) |
| AudioToolbox.framework | 编解码,音频编解码格式转化 |
综上所述,在日常开发中我和大家也要重点关注iOS音频架构中的高层服务框架,这部分框架是日常开发中经常会手撸代码的地方,而在framework层面,我们要重点关注AVFoundation,虽然它是一个基于C的framework。🙂,但是它却能够对音频进行精细入微的控制,当我们使用AVFoundation进行录音和播放时,能够拿到音频的原始PCM解码之后的数据,拿到这些数据能够对音频进行特效的处理。如果我们要做一个音频播放类的产品,那么用到MediaPlayer.framework的次数会很多。
在中层服务中,如果大家有对音频做了一些比如RTMP、RTSP等流媒体处理的时,可能会用到Audio Convert Services(感觉我是用不到了😂)。比如这么个场景,当我们使用RTMP进行语音直播的时候,通过麦克风采集到的数据可能是原始的PCM数据,但是我们想在播放时候使用AAC格式进行播放,那就得把PCM转成AAC,那就得用Audio Convert Services这个中间层服务。
当我们想做一些音频加密算法或音频的加密声波,那可能就会使用到中间层的Audio Unit Services,它可以对硬件层进行一些精细的控制。而Audio File Services是对音频文件的封装和变化。因此啊,除了底层服务的相关框架外,中间层和高层服务是需要我们(尤其是我自己🙂)去重点掌握的。
## Audio SystemSound
SystemSound框架用于播放系统声音,比如某些特殊的提示音、震动等,若我们要使用该框架来播放自定义声音,要求对应的音频编码方式为PCM的原始音频,长度一般不超过30秒(你要想超过也没法,只不过不推荐🙂)。
当我们使用该框架调用震动功能时,只能用于iPhone系列设备,iPod和iPad系列均无效,因为只有iPhone系列设备的厚度能够允许塞下震动模块(而且还是改进后的Tapic Engine)。当我们使用该框架播放系统音乐效果时,静音情况下无效;播放提示音乐效果时,无论静音与否均有效。
因此使用SystemSound适用于播放提示音及游戏中的特殊短音效用处会更大。
举个🌰🍐!
```ObjC
NSString *deviceType = [[UIDevice currentDevice] model];
if ([deviceType isEqualToString:@"iPhone"]) {
// 调用正常的震动模块,静音后无效
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
} else {
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"注意" message:@"您的设备不支持震动" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alertVC animated:true completion:^{
}];
}
```
以上是我们进行调用震动模块的测试代码,上文已经说明只有iPhone系列设备中才能体现效果,因此我们最好是加上设备类型判断(当然你可以不加🙂),改框架也是基于C的(比较直接操作底层硬件),代码风格也是趋向于C,实际上就这一句话` AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
`,大家可以从[这篇文章](http://blog.csdn.net/wlm0813/article/details/51170574)中找到其它SystemSoundID,如果系统提供的音效并不适合我们,那么我们可以载入自定义音效,
```ObjC
NSURL *systemSoundURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"test" ofType:@"mp3"]];
// 创建ID
SystemSoundID systemSoundID;
AudioServicesCreateSystemSoundID((CFURLRef)CFBridgingRetain(systemSoundURL), &systemSoundID);
// 注册callBack
AudioServicesAddSystemSoundCompletion(systemSoundID, nil, nil, soundFinishPlaying, nil);
// 播放声音
AudioServicesPlaySystemSound(systemSoundID);
```
分析以上测试代码发现一个有趣的现象,就算是自定义音效也是要通过AudioServicesPlaySystemSound去载入音频文件标识符,所以可以大胆的推测!之所以iOS系统占用这么大的存储空间是有相当大的一部分为系统音效音频资源。不用的音效还没法删除,估计也是怕其他App会用到吧。🙂
## 音频参数(了解的不多,先记录一波)
### 采样率:
常用的如44100,CD就是。还有一些其它的32千赫兹。采样频率越高,所能描绘的声波频率也就越高。
### 量化精度
精度嘛,衡量一个东西的精确程度。是将模拟信号分成多个等级的量化单位。量化的精度越高,声音的振幅就越接近原音。因为我们平时听到的音乐或者声音都是模拟信号,而经过计算机处理的都是数字信号,将模拟信号转换为数字信号的这个过程我们称之为量化。而量化,我们得需要一定的信号来逼近它,这种逼近的过程,也就是量化的过程,这种逼近的精度,也就成为量化精度。所以不管我们如何逼近,那也只是逼近而已,与原来的模拟信息还是有些不同。精度越高,听起来就越细腻
### 比特率
数字信号每秒钟传输的信号量。
看一个综合实例,🌰🍐
通过使用``和` `框架来完成这个实例,在这个实例中,讲读取一个音频文件,对其进行播放、暂停、停止等操作,并可设置是否静音、循环播放次数、调节音量、时间,并可看到当前音频播放进度。
界面的搭建非常简单,大家自定义即可,只需要拖拽出对应的相关控件属性及方法即可。
```ObjC
// 播放按钮点击事件
- (IBAction)playerBtnClick:(id)sender {
// 设置音频资源路径
NSString *playMusicPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"mp3"];
if (playMusicPath) {
// 开启Audio会话实例
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
NSURL *musicURL = [NSURL fileURLWithPath:playMusicPath];
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:musicURL error:nil];
audioPlayer.delegate = self;
audioPlayer.meteringEnabled = true;
// 设置定时器,每隔0.1秒刷新音频对应文件信息(伪装成实时🙂)
timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(monitor) userInfo:nil repeats:true];
[audioPlayer play];
}
}
```
```ObjC
// 定时器任务
- (void)monitor {
// numberOfChannels声道数,一般都是2吧,代表左右双声道
NSUInteger channels = audioPlayer.numberOfChannels;
NSTimeInterval duration = audioPlayer.duration;
[audioPlayer updateMeters];
NSString *peakValue = [NSString stringWithFormat:@"%f, %f\n channels=%lu duration=%lu\n currentTime=%f", [audioPlayer peakPowerForChannel:0], [audioPlayer peakPowerForChannel:1], (unsigned long)channels, (unsigned long)duration, audioPlayer.currentTime];
self.audioInfo.text = peakValue;
self.musicProgress.progress = audioPlayer.currentTime / audioPlayer.duration;
}
```
```ObjC
// 暂停按钮点击事件
- (IBAction)pauseBtnClick:(id)sender {
// 再次点击暂停才会播放
if ([audioPlayer isPlaying]) {
[audioPlayer pause];
} else {
[audioPlayer play];
}
}
```
```Objc
// 停止按钮点击事件
- (IBAction)stopBtnClick:(id)sender {
self.volSlider.value = 0;
self.timeSlider.value = 0;
[audioPlayer stop];
}
```
```Objc
// 静音按钮点击方法
- (IBAction)muteSwitchClick:(id)sender {
// 实际上音量为0即静音
// 刚好这还是个Switch开关
audioPlayer.volume = [sender isOn];
}
```
```ObjC
// 调节音频时间方法(UIProgress)
- (IBAction)timeSliderClick:(id)sender {
[audioPlayer pause];
// 防止归一化(Xcode默认都是0~1,转化为实际值)
[audioPlayer setCurrentTime:(NSTimeInterval)self.timeSlider.value * audioPlayer.duration];
[audioPlayer play];
}
```
```ObjC
// UIStepper点击事件(音频循环播放)
- (IBAction)cycBtnClick:(id)sender {
audioPlayer.numberOfLoops = self.cyc.value;
}
```
## 语音识别
在iOS 7之后,AVFoundation提供了语音识别功能,使用它非常的简单,
```ObjC
// 语音识别控制器
AVSpeechSynthesizer* speechManager = [[AVSpeechSynthesizer alloc] init];
speechManager.delegate = self;
// 语音识别单元
AVSpeechUtterance* uts = [[AVSpeechUtterance alloc] initWithString:@"23333"];
uts.rate = 0.5;
[speechManager speakUtterance:uts];
```
需要注意,如果本机系统语言设置成了英文是不能够识别中文的喔!
[相关Demo见这。](https://github.com/windstormeye/iOSMorePractices/tree/master/audioPractices)
================================================
FILE: iOS/Objective-C/More-DesignPattern.md
================================================
设计模式,这是一个可以持续投入研究的问题,当初我一直不能理解学长们口中谈论的设计模式到底是什么意思,什么是MVC、MVP、MVVM甚至CDD呢?以及现在层出不穷的MVX等等🙄。有人这么跟我说,“架构,其实是一个设计上的东西,它可以小到类与类之间的一个交互,可以大到不同的模块之间,或者说不同的业务部门之间的交互都可以从架构的层面去理解它。”
好了,说完后我更加懵逼了,这还是没说明白啊。也就一直拖着。随后我开始了第一个自己所谓的“项目”——[“大学+”](https://github.com/windstormeye/CampusPlus),咱们实话实说,开始大学+之前时间上我有在帮一个学长做他的个人项目一部分,跟我说这个项目整体的架构是MVC,但是当时我哪知道啥是MVC啊,刚开始他丢给我做一个用户登陆模块,我只能依葫芦画瓢,当时根本就不知道啥叫Model,啥叫block,可是当时项目中却充满着大量的Model和block以及各种delegate。😅。迷茫了好几天,最后不管怎么说也是瞎做完了,给学长review的时候居然被他发现了我没用二次封装的AFNetworking网络请求manager,而是自己又搞了一个贼差劲的破东西,被数落了一番后,我当时还是没啥概念,还是不知道为啥要这么做,怎么做。
开始“大学+”项目后,刚开始我同样还是没有拎清楚到底什么是设计模式,导致在项目开展过程中很多模块的实现方式都是乱七八糟,数据源都是瞎给的,甚至有些页面的数据源都重复获取了好几次,但是神奇的地方就在于居然能够把这个项目做完了!!!😅
在前年暑假的重构期间,我在习得了一些设计模式的思想以及大量的实践之后,慢慢的发现!原来我当初的设计是在趋向于MVC的,只不过当时实在是无法hold得住到底什么是MVC才会导致在View中不但做了逻辑还做了model的事情。
随后展开了艰难的重构之路,在重构期间,我又对设计模式有了一个新的认识,开始发现不是某个项目去迎合设计模式、架构,而是设计模式、架构来迎合项目的实际,也就因为是这种情况的出现,最开始软件行业基本上都套用MVC,但是在越来越多的实际开发过程中发现,一昧的死守MVC实际上还有破坏项目实际的耦合,随后才慢慢的衍生出根据不同的开发平台适用的MVP、MVX、MVVM、CDD等。
在我日后的学习和工作中,运用到最多的就是MVC,甚至说基本上都是MVC,毕竟MVC是软件行业的“常青树”,基本上都能够用MVC来构建每一个软件产品,而且MVP、MVVM等可以说都是的MVC的变种,本质上也都还是MVC。
在这篇文章中,我将结合以往学习和工作经验梳理一遍关于耦合、MVC、MVP、MVVM的核心知识点,并编写对应实例进行讲解,也作为自己在设计模式上的理解与总结。
## 架构基础
在讲解三大设计模式之前,我们先来做一些架构基础的工作。之所以要对项目做整体的分层架构设计是因为随着项目进度的展开,日益增长业务逻辑和代码数量远远超出了开发人员所能精确分析掌握的能力。一旦嗅探出项目中有即将“腐烂”的部分,如果再不加以维护日后就一定会变得更加腐烂。🙂
为了防止以上问题的发生,慢慢的萌生出了“软件工程”的科学指导软件开发方法,以工程的思路去规范、进行软件开发工作,同时其衍生品——“设计模式”的思路也慢慢的被广大软件从业者所接受,从此软件行业走进了有科学思想指导的春天!😓。
架构核心是耦合,简单来说耦合可以是两个类之间的交互,当然也可以是三个类甚至更多的类,从大的角度来说,可以是不同的业务模块之间的交互,如何让这些的模块之间的联系或者影响更少,这就我们所说的解耦的概念。
处理好项目中各个类甚至各个模块之间的耦合关系是长久以来软件工程专家甚至开发人员所追求的“至上宝典”,因为产品的不同,其业务流程模型也不同,需要解决的核心问题也不同,围绕其做的架构设计也不能一概而论,而在iOS中解决耦合关系,可以分为三个层次。
1. 直接耦合。双方都知道对方的存在。
2. delegate。只有一方知道对方的存在。
3. notification。双方均不知道对方的存在。
以上三种为架构设计所采用的基本耦合方式,当然还有一些其它的方式,不过这些方式都牵扯到了平台差异性,非iOS端做不到,比如KVO等。以上列举的三种方式具备通俗性,各大平台均可实现。
### 直接耦合
直接耦合做法是最差的一种耦合方式,甚至可以说耦合度最高的一种,类与类或者模块与模块之间互相都知道了双方的存在。当然,这种直接耦合的方式不能说很差,只能说它的用处体现的地方非常局限,不过,大部分同学(包括我自己)在最开始写东西的时候都是“直接耦合”的实践者,它的“简单粗暴”是最吸引人的地方(当然这也是它的致命缺点)
实现“直接耦合”模式需要用到一下场景,Manager发布Task,Worker执行Task,执行Task完成后告诉Manager,Manager庆祝Task完成。因此我们的文件目录结构如下所示,
```shell
|____Worker.m
|____Manager.h
|____Worker.h
|____Manager.m
|____decoupleViewController.h
|____decoupleViewController.m
```
```ObjC
// ----- manager -----
#import "Manager.h"
#import "Worker.h"
@implementation Manager
// 庆祝Task完成
- (void)celebratePrintTask {
NSLog(@"celebrate Task!");
}
// 发布Task给Worker
- (void)beginPrintTask {
Worker *woker = [[Worker alloc] init];
[woker doPrintTask];
}
@end
```
```ObjC
// ----- worker -----
#import "Worker.h"
#import "Manager.h"
@implementation Worker
// 执行Task
- (void)doPrintTask {
NSLog(@"finish work!");
Manager *manager = [[Manager alloc] init];
[manager celebratePrintTask];
}
@end
```
而想要把Manage和Worker联系起来,我们得通过decoupleViewController,
```ObjC
#import "decoupleViewController.h"
#import "Manager.h"
@implementation decoupleViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
// 创建Manage,让Manage制作Task
Manager *manager = [[Manager alloc] init];
[manager beginPrintTask];
}
```
从以上代码中我们可以看到这种直接耦合的写法根本算不上是设计模式,就是一种“随用随写”的风格,缺点大家应该也能看得清楚,以上边代码来说,如果要完成一个Task,Manager是要知道Worker的存在,而且也只能用Worker去完成,而不能让比如说Student去完成。(除非你要再生成一个Student)当然,跟产品的实际设计还是有很大关系的。如果我们要做的东西非常小,或者某个模块比较小,使用这种模式或风格去完成会大大缩小开发成本。
### delegate
delegate,代理设计模式,主要用于反向传值。关于代理的细节在[上一篇文章](http://pjhubs.cn/2018/01/30/More-%E9%A1%B5%E9%9D%A2%E4%BC%A0%E5%80%BC/)中已经做了讲解,如果还是套用Manager和Worker的思路去讲解,使用delega后Worker可以不用管是Manager还是Student甚至是Father去发布的Task,它只管完成。(反过来也可以),因此实际上Worker是不知道manager的存在的,只有manager才知道到底是谁去给他完成了任务。
映射到生活中,这个例子就相当于“我”这个程序员屌丝根本就不管甲方是谁,来活我就做,我相当于worker,甲方可以是BAT,可以是山西煤老板,也可以是美少女战士,这些相当于manager,manager来找到我这个worker,指定我去完成他们的Task。
创建好的文件目录结构为:(跟之前并无区别)
```shell
|____delegateViewController.h
|____delelgateManager.h
|____delegateWoker.h
|____delegateViewController.m
|____delegateWoker.m
|____delelgateManager.m
```
worker的改动为:(相当于指定了工作协议)
```ObjC
// ----- delegateWorker.h -----
#import
@protocol delegateWorkerDelegate
- (void)donePrintTask;
@end
@interface delegateWoker : NSObject
@property (nonatomic, weak) id workerDelegate;
- (void)doPrintTask;
@end
// ----- delegateWorker.m -----
#import "delegateWoker.h"
@implementation delegateWoker
- (void)doPrintTask {
NSLog(@"finish work!");
[_workerDelegate donePrintTask];
}
@end
```
worker变得简单一些,它只管做东西。而manager变为了,
```ObjC
// ----- delegateManager.h -----
#import
@interface delelgateManager : NSObject
- (void)beginPrintTask;
@end
// ----- delegateManager.m -----
#import "delelgateManager.h"
#import "delegateWoker.h"
@interface delelgateManager ()
@end
@implementation delelgateManager
- (void)beginPrintTask {
delegateWoker *woker = [[delegateWoker alloc] init];
woker.workerDelegate = self;
[woker doPrintTask];
}
- (void)donePrintTask {
NSLog(@"celebrate Task!");
}
```
从上以上代码中我们可以看到,manager遵守了worker的delegate(相当于给worker的工作协议签了名)并实现了delegate的代理方法(相当于work的工作成果),在donePrintTask的代理方法中可以庆祝Task完成。delegateViewController的代码跟之前是一样的。
```ObjC
#import "delegateViewController.h"
#import "delelgateManager.h"
@implementation delegateViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
delelgateManager *manager = [[delelgateManager alloc] init];
[manager beginPrintTask];
}
@end
```
### notification
通知,这部分内容统一也在[上一篇文章](http://pjhubs.cn/2018/01/30/More-%E9%A1%B5%E9%9D%A2%E4%BC%A0%E5%80%BC/)中做了较为详细的讲解。
使用通知进行架构耦合的文件目录为:
```shell
|____notification.h
|____notifyWorker.h
|____notifyManager.m
|____notificationViewController.m
|____notifyWorker.m
|____notificationViewController.h
|____notifyManager.h
```
因为很多东西在上一篇文章中都已经一一讨论过了,在此不做过多赘述,我们来看使用通知的核心代码,
```ObjC
// ----- Manager -----
#import "notifyManager.h"
#import "notification.h"
@implementation notifyManager
- (instancetype)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(celebrateWork) name:NOTIFICATION_PRINTTASKDONE object:nil];
}
return self;
}
- (void)beginPrintTask {
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_BEGINPRINTTASK object:nil userInfo:nil];
}
- (void)celebrateWork {
NSLog(@"celebrate work!");
}
@end
```
```ObjC
// ----- worker -----
#import "notifyWorker.h"
#import "notification.h"
@implementation notifyWorker
- (instancetype)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doPrintTask) name:NOTIFICATION_BEGINPRINTTASK object:nil];
}
return self;
}
- (void)doPrintTask {
NSLog(@"finish work!");
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_PRINTTASKDONE object:nil];
}
@end
```
从以上两段代码中我们可以看出,manager不知道发布Task后谁去完成,Work不知道完成的Task是谁发布的,也就是双方都不知道是谁,它们只知道如果有人发送了`NOTIFICATION_BEGINPRINTTASK`(开始工作)和`NOTIFICATION_PRINTTASKDONE`(工作结束)的通知后就调用各自的处理方法。
虽然通知看上去好像是解决两个类或模块之间耦合度最低的方法,但同时也是风险较高的一个方法,如果通知管理得不好,debug起来可是一件异常痛苦的事情。😝
以上就是需要大家提前了解的一些架构基础知识,其它的比如MVC、MVP、MVVM等设计模式都需要用到对应的知识,在后续的设计模式讲解过程中会大量保留此类做法。
## MVC
MVC为苹果官方推荐的设计模式,其为**Model-View-Controller**的缩写。简单来说Model就是数据源,访问Model中的属性或者方法即可拿到相应的数据源;View为展示给用户的视图,上边可以堆积入一些button、label或者ImageView等等,并且还负责把从Model中获取到的数据渲染出来;Controller主要做的事情就是搞定Model何时去拉取数据,View何时去加载拉取到的Model以及View的操作何时响应给Model重新拉取数据,需要注意的是,View和Model并无直接联系,View只是有一个Model的属性,View利用该Model属性解析给对应控件进行赋值,它们之间并不能直接操作,如下所示,
综上所述就是MVC的核心思想,但实际上不会有人严格遵守这么做的,都是给你瞎搞,这都是摆我大天朝产品经理所赐,某些奇葩需求还真能让你写出来“四不像”(也有可能实力不足?🙄)
举个🌰🍐!!!以下为MVC架构的最小集文件目录,
```shell
|____MVCView.m
|____MVCModel.h
|____MVCModel.m
|____MVCView.h
|____MVCViewController.m
|____MVCViewController.h
```
在MVCView中我们要写明需要加载的控件,其中我们加载了一个UILabel和UIButton,
```ObjC
- (void)initView {
self.backgroundColor = [UIColor darkGrayColor];
self.tipsLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self addSubview:self.tipsLabel];
self.tipsLabel.font = [UIFont systemFontOfSize:25];
self.tipsLabel.textAlignment = NSTextAlignmentCenter;
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 200, 30)];
[self addSubview:btn];
[btn addTarget:self action:@selector(btnClick) forControlEvents:1<<6];
[btn setTitle:@"点我啊!" forState:UIControlStateNormal];
}
```
因为View只负责做视图和数据的展示,其中涉及到数据的逻辑交互都尽量少甚至不要在View的处理,因此我们要把View中的UIButton的点击事件代理出去给Controller进行处理,并且我们的View也是不能自己去拉取数据的,而是应该暴露出一个Model属性供Controller自行调配,因此我们的MVCView.h中可以这么写,
```ObjC
#import
#import "MVCModel.h"
@protocol MVCViewDelegete
- (void)MVCViewBtnClick;
@end
@interface MVCView : UIView
@property (nonatomic, strong) MVCModel *model;
@property (nonatomic, weak) id viewDelegate;
@end
```
而完整的MVCView.m我们可以把对应的逻辑补充完整,
```ObjC
#import "MVCView.h"
@interface MVCView ()
@property (nonatomic, strong) UILabel* tipsLabel;
@end
@implementation MVCView
- (id)init {
self = [super init];
if (self) {
[self initView];
}
return self;
}
- (void)initView {
self.backgroundColor = [UIColor darkGrayColor];
self.tipsLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self addSubview:self.tipsLabel];
self.tipsLabel.font = [UIFont systemFontOfSize:25];
self.tipsLabel.textAlignment = NSTextAlignmentCenter;
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 200, 30)];
[self addSubview:btn];
[btn addTarget:self action:@selector(btnClick) forControlEvents:1<<6];
[btn setTitle:@"点我啊!" forState:UIControlStateNormal];
}
- (void)setModel:(MVCModel *)model {
_model = model;
self.tipsLabel.text = model.contentString;
}
- (void)btnClick {
if (_viewDelegate) {
[_viewDelegate MVCViewBtnClick];
}
}
@end
```
从以上MVCView.m代码中我们可以看到,重写了Model的setter方法,拿到model后我们再接着给对应的label赋值数据源即可,在button对应的点击事件中代理出去,
在MVCController部分,我们不但要把MVCView和MVCModel的关系都确认联系起来,还要明确这两者何时进行交互(例子可能不够复杂,并不能体现出何时进行交互🙂)
```ObjC
#import "MVCViewController.h"
#import "MVCView.h"
#import "MVCModel.h"
@interface MVCViewController ()
@property (nonatomic, strong) MVCModel* model;
@property (nonatomic, strong) MVCView* MVCView;
@end
@implementation MVCViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.model = [[MVCModel alloc] init];
self.model.contentString = @"MVC model";
self.MVCView = [[MVCView alloc] init];
self.MVCView.frame = self.view.bounds;
self.MVCView.model = self.model;
self.MVCView.viewDelegate = self;
[self.view addSubview:self.MVCView];
}
- (void)MVCViewBtnClick {
NSInteger interger = random() % 10;
self.model.contentString = [NSString stringWithFormat:@"%ld", (long)interger];
self.MVCView.model = self.model;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
```
而我们的MCVModel,因为我们只需要的对MVCView上的Label进行文本的替换,因此我们的model实体只需要一个NSString属性即可。
```ObjC
// ----- MVCModel.h -----
#import
@interface MVCModel : NSObject
@property (nonatomic, copy) NSString* contentString;
@end
// ----- MVCModel.m -----
#import "MVCModel.h"
@implementation MVCModel
@end
```
通过以上操作,我们即可完成MVC设计模式的最小集设计,大家应该能够对MVC有一个初步的认识,MVC架构做到最后会导致C层非常庞大,甚至四五千行代码都是有可能的。
## MVP
MVP的全称为Model-View-Presenter,可以看到缺少了Controller,替换成了Presenter。但是不管怎么说其本质还是MVC的分层架构的思想,只不过把Controller的要做的事情降低了。
不过在iOS中并不推荐使用MVP用于项目架构,因为在iOS中是“原生支持”MVC,导致如果我们硬是要用上MVP,整体的项目文件目录就变成了:
```shell
|____MVPModel.h
|____MVPModel.m
|____MVPView.h
|____MVPView.m
|____Presenter.h
|____Presenter.m
|____MVPViewController.h
|____MVPViewController.m
```
我们还是要创建出来Controller,只不过这里的Controller可以认为是当前该MVP模块的容器(因为在iOS中就是用各种Controller联系起来的😓。),我们先来看看此时的Controller做了哪些事情,
```ObjC
#import "MVPViewController.h"
#import "Presenter.h"
#import "MVPView.h"
#import "MVPModel.h"
@interface MVPViewController ()
@property (nonatomic, strong) MVPView* mvpView;
@property (nonatomic, strong) MVPModel* mvpModel;
@property (nonatomic, strong) Presenter* presenter;
@end
@implementation MVPViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self initView];
}
- (void)initView {
self.view.backgroundColor = [UIColor lightGrayColor];
self.presenter = [Presenter new];
self.mvpView = [MVPView new];
self.mvpView.frame = self.view.bounds;
[self.view addSubview:self.mvpView];
self.mvpView.viewDelegate = self.presenter;
self.mvpModel = [MVPModel new];
self.presenter.mvpModel = self.mvpModel;
self.presenter.mvpView = self.mvpView;
self.mvpModel.contentString = @"2333";
[self.presenter doPrintWork];
}
@end
```
可以看到,实际上Controller的用处只是把Model-View-Presenter这三个东西联系起来而已,逻辑都在Presenter里,
```ObjC
// ----- Presenter.h -----
#import
#import "MVPModel.h"
#import "MVPView.h"
@interface Presenter : NSObject
@property (nonatomic, strong) MVPModel* mvpModel;
@property (nonatomic, strong) MVPView* mvpView;
- (void)doPrintWork;
@end
// ----- Presenter.m -----
#import "Presenter.h"
@implementation Presenter
- (void)doPrintWork {
NSString *content = self.mvpModel.contentString;
self.mvpView.content = content;
}
- (void)MVPViewBtnClick {
NSInteger interger = random() % 10;
self.mvpModel.contentString = [NSString stringWithFormat:@"%ld", (long)interger];
self.mvpView.content = self.mvpModel.contentString;
}
@end
```
MVPView和MVCView有一个不一样的地方,
```ObjC
// ----- MVPView.h -----
#import
#import "MVPModel.h"
@protocol MVPViewDelegete
- (void)MVPViewBtnClick;
@end
@interface MVPView : UIView
@property (nonatomic, strong) NSString* content;
@property (nonatomic, weak) id viewDelegate;
@end
// ----- MVCView.h -----
#import
#import "MVCModel.h"
@protocol MVCViewDelegete
- (void)MVCViewBtnClick;
@end
@interface MVCView : UIView
@property (nonatomic, strong) MVCModel *model;
@property (nonatomic, weak) id viewDelegate;
@end
```
我们可以看到,在MVC模式中的View的数据源是Model类型,而在MVP中的View是不知道Model的类型,只知道View需要什么数据(可以是任意基本数据类型NSString、NSDictionary等),而不管Model。因此可以有个初步的感受,MVC中的View和Model有跟隐含的虚线连接着,View是知道Model的,而在MVP中除了Presenter外,View和Model都是互相不知道的,可以说这是又进一步的把耦合度减低了。
综上所述,实际上MVP在iOS中并不适用,也可以说我不喜欢,可能写的实例还没体现出来我为什么不喜欢MVP的实际原因,因为不管怎么搞你总是会拉着一个拖油瓶Controller,MVP的核心思想是用Presenter去替代Controller,让View和Model之间的联系完全取消,但是我们无法改变Controller在iOS中的地位😓,反而MVP在Android中会大放异彩,因为在Android中没有像在iOS中“万事皆需Controller”的概念。
## MVVM
终于到了MVVM这个我最喜欢的架构了😝。MVVM全称为Model-View-ViewModel,同时也是基于MVC的延伸品,只不过它没MVP那般强硬,使用MVVM我们只需要记住一个思想——“双向绑定”,我们只要达到View和ViewModel、Model和ViewModel的双向绑定即可。
不需要管是否有Controller的存在,而且MVVM也不允许View和Model直接联系,而是通过一个ViewModel实例去联系起来,而且这个ViewModel还是和View与Model进行了双向绑定的,只要Model中的数据发生了改变,View就会监听到这个改变,从而赋值达到重新渲染数据刷新UI。
所以我们要解决的就是如何进行“双向绑定”,而这个“双向绑定”只是个指导思想,我们完全可以用前文“架构基础”中讲述的三个方法完成,而之前我一直觉得使用苹果自己提供的KVO(key——value-Observe)写起来太累了就只用了delegate去实现,当然也可能是因为团队小伙伴们对MVVM跟我当初一样比较迷茫,再加上我偷懒把ViewModel揉在Controller里,使用了delegate来实现“双向绑定”,就导致了大家看得云里雾里。😂。
刚好在这段时间中有网友推荐使用Facebook开源的KVOController能够有效降低手撸原生KVO API的痛苦(我是觉得很痛苦),借此机会我们来举个KVO实现MVVM最小集的🌰🍐。
### MVVMController
同样Controller也是要完成Model和View关系建立
```ObjC
#import "MVVMViewController.h"
#import "MVVMViewModel.h"
#import "MVVMModel.h"
#import "MVVMView.h"
@interface MVVMViewController ()
@property (nonatomic, strong) MVVMViewModel* viewModel;
@property (nonatomic, strong) MVVMView* mvvmView;
@property (nonatomic, strong) MVVMModel* mvvmModel;
@end
@implementation MVVMViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.mvvmView = [[MVVMView alloc] init];
self.mvvmView.frame = self.view.bounds;
[self.view addSubview:self.mvvmView];
self.mvvmModel = [[MVVMModel alloc] init];
self.mvvmModel.content = @"2333";
self.viewModel = [[MVVMViewModel alloc] init];
self.viewModel.contentString = self.mvvmModel.content;
[self.mvvmView setWithViewModel:self.viewModel];
[self.viewModel setWithModel:self.mvvmModel];
}
@end
```
### MVVMView
```ObjC
// ----- MVVMView.h -----
#import
#import
#import "MVVMViewModel.h"
@interface MVVMView : UIView
@property (nonatomic, strong) NSString* content;
- (void)setWithViewModel:(MVVMViewModel *)vm;
@end
// ----- MVVMView.m -----
#import "MVVMView.h"
#import "FBKVOController.h"
#import "MVVMViewModel.h"
#import "NSObject+FBKVOController.h"
@interface MVVMView ()
@property (nonatomic, strong) UILabel* tipsLabel;
@property (nonatomic, strong) MVVMViewModel* vm;
@end
@implementation MVVMView
- (instancetype)init {
self = [super init];
if (self) {
[self initView];
}
return self;
}
- (void)initView {
self.backgroundColor = [UIColor lightGrayColor];
self.tipsLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self addSubview:self.tipsLabel];
self.tipsLabel.font = [UIFont systemFontOfSize:25];
self.tipsLabel.textAlignment = NSTextAlignmentCenter;
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 200, 30)];
[self addSubview:btn];
[btn addTarget:self action:@selector(btnClick) forControlEvents:1<<6];
[btn setTitle:@"点我啊!" forState:UIControlStateNormal];
}
- (void)setContent:(NSString *)content {
_content = content;
self.tipsLabel.text = content;
}
- (void)btnClick {
[self.vm doPrintWork];
}
- (void)setWithViewModel:(MVVMViewModel *)vm {
self.vm = vm;
[self.KVOController observe:vm keyPath:@"contentString" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld block:^(id _Nullable observer, id _Nonnull object, NSDictionary * _Nonnull change) {
NSString *newContent = change[NSKeyValueChangeNewKey];
self.tipsLabel.text = newContent;
}];
}
```
MVVMView中,我们使用了Facebook开源的KVOController封装好的苹果提供的原生KVO API,MVVMView的其它东西跟之前一样,只不过它的数据源获取方法变成了,
```ObjC
- (void)setWithViewModel:(MVVMViewModel *)vm {
self.vm = vm;
[self.KVOController observe:vm keyPath:@"contentString" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld block:^(id _Nullable observer, id _Nonnull object, NSDictionary * _Nonnull change) {
NSString *newContent = change[NSKeyValueChangeNewKey];
self.tipsLabel.text = newContent;
}];
}
```
它在此使用KVO监听了vm对象的contentString属性,只要我们把MVVMView和MVVMViewModel的对应关系都确定了,当contentString发生变化时,能够实时的修改数据刷新UI。当然,如果不想达到这种自动的效果,那就跟我当初一样用delegate去手动实现“双向绑定”吧🙂。
### MVVMViewModel
```ObjC
// ----- MVVMViewModel -----
#import
#import "MVVMModel.h"
@interface MVVMViewModel : NSObject
@property (nonatomic, strong) NSString* contentString;
- (void)setWithModel:(MVVMModel *)model;
- (void)doPrintWork;
@end
// ----- MVVMViewModel -----
#import "MVVMViewModel.h"
@interface MVVMViewModel ()
@property (nonatomic, strong) MVVMModel* mvvmModel;
@end
@implementation MVVMViewModel
- (instancetype)init {
self = [super init];
if (self) {
}
return self;
}
-(void)setWithModel:(MVVMModel *)model {
self.mvvmModel = model;
self.contentString = model.content;
}
- (void)doPrintWork {
NSInteger interger = random() % 10;
self.mvvmModel.content = [NSString stringWithFormat:@"%ld", (long)interger];
self.contentString = self.mvvmModel.content;
}
@end
```
其它类都是一样的。
---
================================================
FILE: iOS/Objective-C/More-iOS上的相机.md
================================================
这篇文章主要是用于记录我在使用iOS上进行相机开发的过程中的相关内容总结,因为多媒体是iOS中很大的一块内容,因此不太能够用一篇完整的文章进行描述,因此这篇文章将会持续更新。
在iOS中启用相机功能可以使用`UIImagePickerController`和`AVFoundation`两种做法。
### UIImagePickerController
从类名上可以看出,该类是一个应该是具备了较为完整的相机功能,但是从Apple以往的风格可以猜出这个类已经把相关属性做了高度的封装,开发者唯一能够自定义的地方除了中间4:3的相机画面下的区域,如下所示:
如果你对`UIImagePickerController`设置了`showsCameraControls = NO`,此时运行起工程,会发现上图中红线所勾画的区域没了,如下所示:
对`UIImagePickerController`属性设置可参考如下:
```ObjC
- (UIImagePickerController *)imagePicker{
if (!_imagePicker) {
_imagePicker = [[UIImagePickerController alloc]init];
// 判断现在可以获得多媒体的方式
if ([UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]) {
// 设置image picker的来源,这里设置为摄像头
_imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
// 设置使用哪个摄像头,这里默认设置为前置摄像头
_imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
// 设置摄像头模式为照相
_imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto;
}
}
// 允许编辑
// _imagePicker.allowsEditing=YES;
// 设置代理,检测操作
_imagePicker.delegate=self;
return _imagePicker;
}
```
其代理为:`UIImagePickerControllerDelegate`,我们创建好想要的自定义`view`后,重新复制给对应的`imagePickerController`变量的`cameraOverlayView`属性即可,接着在该`view`上我们自定义的拍照`Button`的点击事件中调用对应`[imagePickerController takePicture]`方法即可进行拍照,而获取的相机拍照图片会在,
```ObjC
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
UIImage * image = info[UIImagePickerControllerEditedImage];
}
```
### AVFoundation
如果我们想彻底的自定义相机界面,需要直接走`AVFoundation`框架,该框架给我们一系列完整做法,这是我在项目中自定义的一套方法,
```ObjC
//
// PJCameraView.swift
// Bonfire
//
// Created by pjpjpj on 2018/5/27.
// Copyright © 2018年 #incloud. All rights reserved.
//
import UIKit
import AVFoundation
import Photos
protocol PJCameraViewDelegate {
func takePhotoImage(image: UIImage)
}
class PJCameraView: UIView, AVCapturePhotoCaptureDelegate {
private var session: AVCaptureSession?
private var videoInput: AVCaptureDeviceInput?
private var imageOutput: AVCapturePhotoOutput?
private var previewLayer: AVCaptureVideoPreviewLayer?
private(set) var isFrontCamera: Bool?
public var delegate: PJCameraViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
initView()
initAVCaptureSession()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func initView() {
self.backgroundColor = UIColor.black
isFrontCamera = false
}
private func initAVCaptureSession() {
session = AVCaptureSession.init()
let device = AVCaptureDevice.default(for: AVMediaType.video)
videoInput = try! AVCaptureDeviceInput.init(device: device!)
imageOutput = AVCapturePhotoOutput.init()
let setDic = [
AVVideoCodecKey : AVVideoCodecType.jpeg
]
let imageSetting = AVCapturePhotoSettings.init(format: setDic)
imageOutput?.photoSettingsForSceneMonitoring = imageSetting
if (session?.canAddInput(videoInput!))! {
session?.addInput(videoInput!)
}
if (session?.canAddOutput(imageOutput!))! {
session?.addOutput(imageOutput!)
}
previewLayer = AVCaptureVideoPreviewLayer.init(session: session!)
previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
previewLayer?.frame = self.frame
self.layer.addSublayer(previewLayer!)
session?.startRunning()
}
public func switchCameraControl() {
let animation = CATransition()
animation.duration = 0.35
animation.timingFunction = CAMediaTimingFunction.easeInOut
animation.type = "oglFlip"
var position: AVCaptureDevice.Position?
if isFrontCamera! {
position = AVCaptureDevice.Position.back
animation.subtype = kCATransitionFromRight
} else {
position = AVCaptureDevice.Position.front
animation.subtype = kCATransitionFromLeft
}
for d: AVCaptureDevice in AVCaptureDevice.DiscoverySession.init(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: position!).devices {
if d.position == position {
previewLayer?.add(animation, forKey: nil)
previewLayer?.session?.beginConfiguration()
let input = try? AVCaptureDeviceInput(device: d)
for oldInput in (previewLayer?.session?.inputs)! {
previewLayer?.session?.removeInput(oldInput)
}
previewLayer?.session?.addInput(input!)
previewLayer?.session?.commitConfiguration()
break
}
}
isFrontCamera = !isFrontCamera!
}
public func takePhoto() {
imageOutput?.capturePhoto(with: AVCapturePhotoSettings.init(format: [
AVVideoCodecKey : AVVideoCodecType.jpeg
]), delegate: self)
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
let data = photo.fileDataRepresentation()
if data != nil {
let image = UIImage.init(data: data!)
delegate?.takePhotoImage(image: image!)
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image!)
}, completionHandler: nil)
}
}
}
```
以上是我的一个完整相机模块,直接调用`takePhoto`方法可拍照,调用`switchCameraControl`方法可切换前后相机。
以上为使用两种框架进行基础相机的搭建。
### 注意点
此节为在进行相机开发过程中需要注意的地方。
================================================
FILE: iOS/Objective-C/More-iOS国际化一站式解决方案.md
================================================
关于iOS开发中的国际化(也可称为多语言)在网上的文章多如牛毛,不过总结起来就那么一回事,不是说他们写的不好我写的多好,而是说过于零散。
现在,我将结合实际场景需求进行国际化做法详解。可以肯定的是,Android的国际化做法大同小异,无非也就是各个语言版本的文件替换,我们先来分析下真实的需求是怎么一回事。
## 国际化需求:
1. 只提供English和Chinese Simplified两种语言;
2. App名称跟随系统语言变化;
2. 用户首次打开app时,app的语言与系统语言保持一致(系统语言为非简体中文,默认app都是英文)用户手动更改语言之后,之后都记忆用户选择的语言;
3. 用户在App内切换语言后,App本身所有文本信息全部替换成对应语言。
根据需求,我比较纠结的地方是,App的静态文本数据可以存两份在本地,也就是English一份Chinese Simplified一份,但请求的API是同时返回两份中英文数据or分中英文两个接口?如果是要一个接口同时返回了中英文两份数据,显然会加大数据包的大小,其次用户很有可能从安装App的那天开始就不再切换App语言,甚至平均几个星期才换一次,同时返回两份数据是否多余,但是这么做几乎可以达到“无感知”数据源切换,相当于是说,一旦用户选择好了要切换语言,“啪嗒”点了完成,立马pop掉当前页面,然后整个App的数据源中英文切换可以几乎用“瞬间完成”来形容。
如果是分中英文两个接口,实际上就会出现微信在进行语言切换时的loading菊花,因为要重新拉取英文版数据,不过好处是可以减少上一种做法的数据包整体大小。这两种做法我都有实践过,如果你的App是非常固定,不会频繁出现语言切换的需求,那么可以使用第二种;如果App有一天之内可能会频繁切换多次语言的情况,第一种无疑。
经过一番探讨,虽然要供给拉美、北美和欧洲的同学使用,但是不会出现频繁切换语言的情况,所以,最终我们选择了第二种解决方案。先来看一张最终成果gif图,
从上图中可以看到其实并没有对数据源进行切换,因为。。。。后台没写完😓。
不过也不影响我们的讲解,首先,明确一个概念,我们能够做的国际化语言支持iOS系统中自带的所有语言,只要你能在系统设置中找到的语言,就能够对你的App做对应版本的国际化适配;其次,每对App适配一种语言,就要单创建出一个语言文件(要不然会引起冲突)。OK,我们正式进入讲解。
### 首先创建一个工程
我起名为`languageTest`。
### 工程初始化
```objc
// AppDelegate.m
#import "navOneViewController.h"
#import "navTwoViewController.h"
@interface AppDelegate ()
@property (nonatomic, strong) UITabBarController *rootTabBar;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
self.rootTabBar = [[UITabBarController alloc]init];
self.rootTabBar.delegate = (id)self;
self.window.rootViewController = self.rootTabBar;
[[UITabBar appearance] setBarTintColor:[UIColor whiteColor]];
navOneViewController *navOneController = [navOneViewController new];
UINavigationController *nav1 = [[UINavigationController alloc] initWithRootViewController:navOneController];
nav1.title = @"首页";
navTwoViewController *navTwoController = [navTwoViewController new];
UINavigationController *nav2 = [[UINavigationController alloc] initWithRootViewController:navTwoController];
nav2.title = @"发现";
self.rootTabBar.viewControllers = @[nav1, nav2];
[self.window makeKeyAndVisible];
return YES;
}
```
在`Appdelegate`中,我们创建一个具备基本展示功能的tabBar及挂载在其之上的VC,
```objc
// navOneViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self.view addSubview:label];
label.font = [UIFont systemFontOfSize:25];
label.textColor = [UIColor whiteColor];
label.text = @"这是首页";
}
// navTwoViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self.view addSubview:label];
label.font = [UIFont systemFontOfSize:25];
label.textColor = [UIColor whiteColor];
label.text = @"这是发现";
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
[self.view addSubview:button];
[button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
button.backgroundColor = [UIColor blueColor];
[button setTitle:@"改变语言" forState:UIControlStateNormal];
}
- (void)buttonClick {
}
```
在各自对应的VC中写下相关UI,并预留相关Button点击事件即可。
### 创建语言文件
创建路径: file -> new -> file... -> String File,文件名严格命名为——“Localizable”,创建好该文件后,点击该文件,并打开Xcode的右边功能区(不知道应该叫啥),在Localization功能区勾选语言版本,如果此时你并未看到或者只有English可选,我们需要到PROJECT -> info -> Localization,添加需要的语言。
添加完成后,会在之前创建的Localization.string文件下看到多出来的语言文件,我选择了English和Chinese Simplified。现在,我们已经可以在对应生成的语言文件中进行需要多语言替换的字段编写了。
```objc
// Localizable.string/English文件
"home" = "home";
"homeString" = "I'm home";
"discover" = "discover";
"discoverString" = "I'm discover";
"change" = "change";
// Localizable.string/Chinese(Simplified)文件
"home" = "首页";
"homeString" = "我是首页";
"discover" = "发现";
"discoverString" = "我是发现";
"change" = "改变语言";
```
并新建一个pch文件,pch文件同样也是头文件,不过这是一个特殊头文件,是一个预编译文件,位于该文件中的所有内容,能够被其他所有源文件共享和访问,相信你也看出来了,如果在pch文件中写了大量的不是必须文件,则会延长编译期时间,我们可以在.pch文件中放:
1. 全局宏;
2. 整个工程中都能用上的头文件;
3. 动态更加当前App运行的环境切换相关宏(debug or release)。
因此,我们需要创建一个pch文件去存放接下来要在整个工程中都要用到的判断语言环境的中英文宏。创建一个pch文件的方式为,file -> new -> file... -> 搜“pch”关键字,创建它。
进入工程配置 -> TARGET -> Build Settings -> 搜pch关键词 -> 在“Apple LLVM 9.0 - Language”下的Prefix Header中,双击输入你的.pch文件路径,我写的是`$(SRCROOT)/PrefixHeader.pch`,填写完毕,回车,会看到生成的绝对路径,确定pch文件路径是否正确。一切都没问题后,编译通过即可。
```
在pch文件中,写入以下宏定义,
```objc
#define AppLanguage @"appLanguage"
#define PJLocalString(key) \
[[NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@",[[NSUserDefaults standardUserDefaults] objectForKey:@"appLanguage"]] ofType:@"lproj"]] localizedStringForKey:(key) value:@"" table:nil]
```
首先定义了一个`AppLanguage`宏,推荐大家的命名更加多样化一些,因为OC并没有namespace,如果我们的命名过于简单,就会导致和Apple本身自定义的NSUserDefaults默认值产生冲突。
`PJLocalString(key)`这个宏“定义”了一个更长的方法,我们也都明确了一个概念,在iOS中的每个国际化语言,就对应着一个文件,这个文件就保存在App沙盒的根目录中,我们要做的就是在某个时机替换系统所采用的语言文件即可,而`PJLocalString(key)`这个宏所做的事情,就是替换!先从NSUserDefaults中取出对应的语言key(en还是zh-Hans),根据语言key去索引到对应的.lproj文件,最后把要替换的关键词传入,抛出找到的对应值(我觉得找的这个过程用的结构应该不是hashmap,真的很快。😨)
多语言文件有了,宏也有了,那怎么用呢?举个例子!
```objc
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self.view addSubview:label];
label.font = [UIFont systemFontOfSize:25];
label.textColor = [UIColor whiteColor];
label.text = PJLocalString(@"homeString");
}
```
只需要在多语言文字的地方调用`PJLocalString()`宏,传入对应key即可。但是此时运行工程,会发现啥都没了,是因为我们并未对NSUserDefaults中做当前语言的设置,这就导致了取出的值为nil。所以,还需要在`AppDelegate`文件中设置初始语言,
```objc
if(![[NSUserDefaults standardUserDefaults] objectForKey:AppLanguage]){
[[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
[[NSUserDefaults standardUserDefaults] synchronize];
}
```
这样,我们即可完成第一次进入App时初始化基础语言,如果我们想要实时更改呢?这就需要用到了通知,使用通知机制去给监听语言设置改变的监听者进行相应的处理,
```objc
// 给Appdelegate.m更新以下方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if(![[NSUserDefaults standardUserDefaults] objectForKey:AppLanguage]){
[[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
[[NSUserDefaults standardUserDefaults] synchronize];
}
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
self.rootTabBar = [[UITabBarController alloc]init];
self.rootTabBar.delegate = (id)self;
self.window.rootViewController = self.rootTabBar;
[[UITabBar appearance] setBarTintColor:[UIColor whiteColor]];
navOneViewController *navOneController = [navOneViewController new];
UINavigationController *nav1 = [[UINavigationController alloc] initWithRootViewController:navOneController];
nav1.title = PJLocalString(@"home");
navTwoViewController *navTwoController = [navTwoViewController new];
UINavigationController *nav2 = [[UINavigationController alloc] initWithRootViewController:navTwoController];
nav2.title = PJLocalString(@"discover");
self.rootTabBar.viewControllers = @[nav1, nav2];
// 新增监听方法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeLanguage:) name:@"changeLanguage" object:nil];
[self.window makeKeyAndVisible];
return YES;
}
- (void)changeLanguage:(NSNotification *)notify {
self.rootTabBar.viewControllers[0].tabBarItem.title = PJLocalString(@"home");
self.rootTabBar.viewControllers[1].tabBarItem.title = PJLocalString(@"discover");
}
// navOneViewController.m更新以下方法
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self.view addSubview:label];
label.font = [UIFont systemFontOfSize:25];
label.textColor = [UIColor whiteColor];
label.text = PJLocalString(@"homeString");
// 新增监听方法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeLanguage:) name:@"changeLanguage" object:nil];
}
- (void)changeLanguage:(NSNotification *)notify {
self.label.text = PJLocalString(@"discoverString");
}
// navTwoViewController.m更新以下方法
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
self.label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
[self.view addSubview:self.label];
self.label.font = [UIFont systemFontOfSize:25];
self.label.textColor = [UIColor whiteColor];
self.label.text = PJLocalString(@"discoverString");
self.button = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
[self.view addSubview:self.button];
[self.button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
self.button.backgroundColor = [UIColor blueColor];
[self.button setTitle:PJLocalString(@"change") forState:UIControlStateNormal];
// 新增监听方法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeLanguage:) name:@"changeLanguage" object:nil];
}
- (void)changeLanguage:(NSNotification *)notify {
self.label.text = PJLocalString(@"discoverString");
[self.button setTitle:PJLocalString(@"change") forState:UIControlStateNormal];
}
- (void)buttonClick {
[[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
[[NSUserDefaults standardUserDefaults] synchronize];
// 同步完NSUserDefault后,发送语言更改通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"changeLanguage" object:nil];
}
```
编译运行吧,见证奇迹的时刻到了~点击“更改语言”button,怎么样,是不是瞬间全都改过了。。😝
但是现在只完成了第一和第四个需求,我们接着来完成第三个需求,“用户首次打开app时,app的语言与系统语言保持一致(系统语言为非简体中文,默认app都是英文)用户手动更改语言之后,之后都记忆用户选择的语言”。
分析一下,该需求的重点在于用户第一次打开App时整体App语言设置跟随系统语言设置,非简体中文之外的语言,都设置成英文,因此,我们需要对`AppDelegate.m`文件进行改造,
```objc
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// App第一次启动跟随系统语言设置
if(![[NSUserDefaults standardUserDefaults] boolForKey:@"firstLaunch"]){
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"firstLaunch"];
NSArray *allLanguages = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"];
NSString *preferredLanguage = allLanguages[0];
if([preferredLanguage rangeOfString:@"zh-Hans"].location != NSNotFound) {
[[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
} else {
[[NSUserDefaults standardUserDefaults] setObject:@"en" forKey:AppLanguage];
}
}
........
```
接下来完成最后一个需求,把App的名称也做国际化适配,如果你之前有过在`info.plist`文件中修改过App的名字,我们现在要做的事情同样也是改名字,而且是针对`info.plist`整个文件做国际化,同样新建一个`string file`文件,命名严格填写为`infoPlist.strings`,并且在Xcode的右边拓展栏中选择`Localizable`,点击生成English和Chinese Simplified多语言文件
```objc
// 在English中写下
CFBundleName = "your english name";
CFBundleDisplayName = "your english name";
// 在Chinese Simplified中写下
CFBundleName = "你的中文名";
CFBundleDisplayName = "你的中文名";
```
OK,以上就是本篇文章所要表达的所有内容,当然这些都是demo级别的code,如果此文对你有帮助,记得对其进行多多改造!
demo地址:
[https://github.com/windstormeye/iOSMorePractices/tree/master/languageTest](https://github.com/windstormeye/iOSMorePractices/tree/master/languageTest)
原文链接:[pjhubs.com](http://pjhubs.com/2018/04/10/More-iOS%E5%9B%BD%E9%99%85%E5%8C%96%E4%B8%80%E7%AB%99%E5%BC%8F%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/)
================================================
FILE: iOS/Objective-C/More-视频相关.md
================================================
## 简介
在iOS开发中的视频相关内容简介,用一张图说明,如下所示:
## 使用
// 持续更新....
================================================
FILE: iOS/Objective-C/More-页面传值.md
================================================
这篇文章来梳理一番在iOS页面间的传值方式,说到页面传值,不管在任何平台开发中都是一个非常的重要的事情,这就让我想起了当初大一那会儿对Qt还不够熟悉,居然对两个Window之间的传值用了一个全局变量来实现,然后在其它Window中显式声明一个extern标记变量作为数据源。emmm,现在想起来当初真的是蠢洁又扇凉。
在iOS中的页面传值方式主要以下六种:
1. 属性传值(一般)
2. 单例传值(一般)
3. NSUserDefault传值(一般用作用户偏好设置)
4. 代理传值(常用)
5. block传值(常用)
在以上六种传值方式中,1、3、4使用得最为广泛,而且广泛存在与iOS SDK和各种第三方库中。
---
### 属性传值(正向传值)
属性传值不太常用于页面传值,因为通过属性传值有些时候需要顾及到各个页面的调用时机次序,反而通过属性设置一些页面的特殊功能标识的作用会更大。
举个🌰🍐!!!我们首先要进行跳转的第二个页面设置属性值propertyPassValue。
```ObjC
@interface NextViewController : UIViewController
@property (nonatomic, copy) NSString* propertyPassValue;
@end
```
重写propertyPassValue的set方法,属性传值的写法还有其它方式,如果我们对相关的对象做了lazy load,那么我们应该在lazy load block中进行赋值。
```ObjC
- (void)setPropertyPassValue:(NSString *)propertyPassValue {
_propertyPassValue = propertyPassValue;
self.tipsLabel.text = propertyPassValue;
}
```
此时我们通过实例化NextViewController,通过push或者present模态推出该vc即可看到对应的Label显示出propertyPassValue内容。
### 单例传值(可正可反)
单例传值,说是传值实际上应该说是利用了单例的“持久化”状态来达到持久化属性的一种方法,单例应该说是一种设计模式,单例是一把非常好使的“武器”,放在会使的人手里,单例会变成一把利剑,能够很好的解决跨多个页面的值传递问题,但是如果放在不熟悉的人手里,很有可能给他这个一高整个项目就散发着弄弄的java味了。🙂。(关于设计模式还会有一篇文章专门对其做了总结,大家可以持续关注。)
单例从字面上来理解,肯定是不同于往先alloc再init一个对象,最常见的用法就是NSUserDefault。(这个后边再说)
```ObjC
+ (instancetype)shareInstance {
static instanceModel *model = nil;
if (!model) {
model = [[instanceModel alloc] init];
}
return model;
}
```
通过以上代码我们就创建出来了一个单例,实际上单例就是利用了static标识符告诉Xcode这个instanceModel变量要放在静态存储区,静态存储区是内存在程序编译的时候就已经分配好的,这块内存在App整个运行期间都将存在。它主要存放静态数据、全局数据和常量。
instanceModel是单拎出来的一个NSObject对象,以上代码只是创建出来一个存在整个App运行期间的单例,当我们把App从后台kill掉后,整个单例也就不存在了,因为此时App的运行期已经结束了,如果你要持久化数据那就得使用其它方法(比如存文件)。
```ObjC
self.tipsLabel.text = [instanceModel shareInstance].instanceString;
```
instanceString是多加的一个NSString对象,我们通过其起到传值的作用。
### NSUserDefault传值(可正可反)
NSUserDefault是苹果原本用于提供用户偏好设置的,但是我大天朝神奇的程序员怎么可能听命与你?所以在实际开发中NSUserDefault有时候起到了鬼斧神工的作用,并且我们还可以使用它来作为页面之间传值的工具。
```ObjC
// 设置NSUserDefault对应的k-v
[[NSUserDefaults standardUserDefaults] setObject:@"NSUserDefaults传值" forKey:@"NSUserDefaults"];
// 数据同步
[[NSUserDefaults standardUserDefaults] synchronize];
```
而使用NSUserDefault也非常的简单,
```ObjC
self.tipsLabel.text = [[NSUserDefaults standardUserDefaults] objectForKey:@"NSUserDefaults"];
```
NSUserDefault底层是个文件,被苹果存在了对应的App沙盒中,只有我们去使用过它才会出现在沙盒中。(沙盒实际上就是对应的App文件夹)因此除非我们把这个App删掉和手动清洗掉对应的数据,否则我们对NSUserDefault设置的value将一直存在,同时它也是数据持久化的一直方式。在使用NSUserDefault作为页面间传值的工具时,千万要注意记得要手动显式调用NSUserDefault的数据同步方法,而且使用的时候尽量避免同时对一个key进行同时“写”操作。(不过也没人这么无聊对一个用户偏好key同时写吧?🙂)
### 代理传值(反向传值)
代理传值我可以拍着胸脯说这绝对是目前iOS中使用范围最广和使用次数最多的传值方式,而且基本上每一个VC的编写都会接触到代理,而且代理传值对于我自己来说也是一个非常常用的反向传值方式。
同时代理也是一种设计模式,第一次接触代理模式的同学(包括我)都会有些摸不着头脑,虽然说是和日常生活中找中介租/买房子非常像,但是转换到代码层面就是迷迷糊糊。🙂。先po一张图,
从上图中我们可以看到,需求方相当于是代理制定者,而受理方则是代理实现者(说得我都懵逼了😂)。直接瞅代码吧,
```ObjC
// 协议
@protocol NextViewControllerDelegate
- (void)passValueOfProtocol:(NSString *)string;
@end
@interface NextViewController : UIViewController
@property (nonatomic, weak) id vcDelegate;
@end
```
从以上代码中,我们看到了制定代理使用@protocol关键字即可,@protocol中的内容可以认为是“合同内容”,我们还需要声明一个id指针变量vcDelegate,这个变量可以认为是“合同书”本身,而如何让需求方发布这个协议或者合同呢?
```ObjC
[_vcDelegate passValueOfProtocol:self.tipsLabel.text];
```
在需要的地方通过id指针变量_vcDelegate调用passValueOfProtocol方法即可,参数即为要填入合同书的内容🙂。通过显式调用以上方法即可完成所谓的“填写合同书”环节。接下来,我们要在对应的类中的遵守协议(合同书)即可使用需求方填入合同书中的内容。
```ObjC
// 写明要遵守的协议
@interface ViewController () < NextViewControllerDelegate>
@end
```
```ObjC
NextViewController *vc = [[NextViewController alloc] init];
// 写明要受理的代理对象
vc.vcDelegate = self;
```
```ObjC
// 要实现的代理方法
- (void)passValueOfProtocol:(NSString *)string {
self.tableView.headTitleLabel.text = string;
}
```
受理方遵守协议的过程千万要注意设置代理对象,`vc.vcDelegate = self;`,如果你忘了设置对应的代理对象,而只是实现了协议方法,这就相当于我们把合同给了对方而已,对方并未**签名**,这份合同对甲乙双方实际上是无效的!因此千万别忘了设置代理对象。通过以上步骤,我们即可从需求方拿到数据,从而显示在代理方,是一种非常有效(但是有些麻烦)的反向传值方法。
### block传值
block,是苹果这几年来强烈推荐的回调传值方式。你可以认为是C中匿名函数,C++/java中的lambda表达式,JS中的闭包等。只不过在iOS中换了个说法称之为block(我是这么认为的🙂)。
```ObjC
@interface NextViewController : UIViewController
@property (nonatomic, copy) NSString* propertyPassValue;
@property (nonatomic, weak) id vcDelegate;
@property (nonatomic, copy) void (^passValueBlock)(NSString *);
@end
```
在此我们把block作为了一个属性,而赋值block,
```ObjC
self.passValueBlock(self.tipsLabel.text);
```
我们只需要在对应的地方给block传入NSString类型参数即可。而使用block,我们在对应的VC中参考以下代码即可获取到对应的值。
```ObjC
NextViewController *vc = [[NextViewController alloc] init];
vc.passValueBlock = ^(NSString *string) {
self.tableView.headTitleLabel.text = string;
};
```
以上只是block的简单使用,关于block更加细节的一些用法,大家可以参考[这篇文章(操蛋的block语法🙂)](http://fuckingblocksyntax.com/)。
### 通知传值
通知也是一把利器,尤其是涉及跨多个页面间的传值时它的好处就提现出来了,而且最重要的是在项目中巧妙的使用通知能够打破以往纵向传递的繁杂性,从而达到一种“星型”发射状的模型。之所以这么说是因为通知可以说是最切合真正的面向对象的核心,真正的面向对象语言——smalltalk,如果没记错的话,smalltalk应该是一切面向对象语言的开山鼻祖吧😀。面向对象的真正核心应该是“一切操作皆消息”,也就是说,就算是简单加法、减法也是通过“发送消息”完成的,而不是像各种课本及老师说的什么继承、多态等等这些外壳,这些外壳用C也能够实现,根本就不算是面向对象语言的标志,反观现在比如C++、java这些当初跟smalltalk分道扬镳的语言,现如今的更新也都是在越来越像smalltalk罢了。(包括ruby🙂),OC就是基于smalltalk的封装,不过很多东西也没拿完。
而在iOS中实现通知得益于苹果爸爸的高度封装能力,把很多问题都给我们搞定了,
```ObjC
[[NSNotificationCenter defaultCenter] postNotificationName:@"notify" object:nil userInfo:@{@"notify" : self.tipsLabel.text}];
```
通过以上代码,我们就注册了一个名为notify的通知,其带有了一个key为notify且value为self.tipsLabel.text的字典。使用通知需要有一个通知中心作为沟通的桥梁,发送通知的object为告诉通知中心要把这个通知消息发送给哪个对象,如果想要群发消息通知,那就nil。
而接收通知,我们需要给当前类添加一个观察者,用于监听对应name的通知。
```ObjC
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifyMesg:) name:@"notify" object:nil];
```
以上代码中的object参数为接受哪个对象发送而来的消息,如果我们想接收任何对象发送而来的消息那就nil吧。接着,我们在处理消息通知的notifyMesg:方法中取得消息实体,
```ObjC
- (void)notifyMesg:(NSNotification *)notify {
self.tableView.headTitleLabel.text = notify.userInfo[@"notify"];
}
```
当然,我们对当前类添加了观察者去监听某个通知,那就要在适当的时机去取消监听,当然你完全可以不用移除通知,但是如果多个消息通知重叠在一个项目中时,很容易就导致消息的监听迷之问题,因为一旦上升到通知层面的debug那就不是线性的了,可想而知难度得有多大。🙂。一般来说我们会在当前类的dealloc方法中移除通知。
================================================
FILE: iOS/Objective-C/Objective-C注意点.md
================================================
本篇文章为我在日常coding过程中使用OC进行了一些骚操作或者被虐得很惨的记录,可能会记得比较乱,因为有时候我也不知道应该怎么分类,但是我会努力哒!(๑•̀ㅂ•́)و✧
1. 实例对象所属的类成为类对象,而类对象所属的类被成为元类`MetaClass`。
类的类型为`Class`类型,而`Class`类型为一个`objc_class`结构体类型指针。该结构体大致成员变量如下所示:
```c
struct objc_class {
Class isa;
Class super_class;
const char *name
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list *methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
```
* **isa** 指针是和Class同类型的objc_class结构体指针,类对象的指针指向其所属的类,即元类(上文已经说过啦~),元类中存储着类对象的类方法,当访问某个类的类方法时会通过该isa指针从元类中寻找方法对应的函数指针;
* **super_class** 为该类所继承的父类对象,如果该类已经是最顶层的根类则为NULL(比如NSObjc或NSProxy);
* **ivars** 是一个指向objc_ivar_list类型的指针,用来存储每一个实例变量的地址;
* **info** 为运行期使用的一些标识,比如`CLS_CLASS(0x1L)`该类为普通类,`CLS_META(0x2L)`表示该类为元类;
* **methodLists** 为存放该类的方法列表,根据info中的标识信息,存储的方法可为实例方法或类方法;
* **cache** 用于缓存最近使用的方法。系统在调用方法时先去cache中search,为空时才回重新去methodLists中寻找。
2. 实例对象是类对象`alloc`或`new`操作创建的,该操作会拷贝实例所属的类成员变量,但并不拷贝类定义的方法。
* 调用实例方法时,系统会根据`isa`指针去类和父类的方法列表`methodLists`中寻找与发送的消息对应的`selector`指向的方法。
* 任何带有以指针开始并指向类结构的结构都可以被看做`objc_object`。
* 在OC中(或者一个面向对象语言)一个对象最重要的特点是可以给它发送消息。
总结以上两点,我做了一张图说明,希望能帮助大家稍微的理清这其中的关系,如下:
* 当发送消息给实例对象时,“消息”是在寻找这个对象的类的方法列表。当发送消息给类对象时,“消息”是在寻找类的元类方法列表。
* 元类,其也是一个对象,我们同样也可以调用它的方法。所有的元类都使用根元类作为他们的类,那根元类的元类呢?对,就是根元类的元类就是它们自己,其`isa`指针指向它自己。此处再放一张图来帮助大家梳理一遍这其中的关系,如下所示:
* 实例对象的`isa`指针指向该实例对象的类,类的`isa`指针指向元类;
* 类的`super class`指向其父类,上文已经说了,如果该类为根类,则其父类为nil;
* 元类的`isa`指针指向根元类(注意不是父元类),若该类本身就是根元类,则指向其本身。
* 元类的`super class`指向父元类,若该类为根元类则指向该根类。
3. `id`类型。其为`objc_object`结构类型的指针,该类型的对象可转换为任何一种对象,类似C语言中的`void *`。
4. `@property` = `ivar` + `gettter` + `setter`,即属性是添加了 **存取方法** 方法的成员变量。
5. 针对`String`的`copy`和`strong`的理解。
* 若为可变数据类型,即当前为`NSMutableString`,分别设定`copyString`和`strongString`,进行的赋值操作如下所示:
```objc
@property (copy) copyString;
@property (strong) strongString;
NSMutableString *string = @"2333";
copyString = string;
strongString = string;
[ts appendString:@"4666"];
NSLog(@"%@", copyString); // 2333
NSLog(@"%@", strongString); // 23334666
```
由上可见,当用`strong`修饰可变数据类型`NSMutableString`时,其会因为原始数据的值的改变而改变。
* 当为不可变数据类型,即`NSString`时,分别设定`copyString`和`strongString`,进行如上所示操作时两者均不会改变,来看`copyString`的setter方法实现:
```ObjC
- (void)setCopyString:(NSString *)copyString {
[_copyString release];
// 拷贝了参数内容,创建了一块新的内存
_copyString = [copyString copy];
}
```
接下来看`strongString`的setter方法实现:
```ObjC
- (void)setStrongString:(NSString *)strongString {
[_strongString release];
// copy了指针
[strongString retain];
_strongString = strongString;
}
```
6. `#import`和`#include`的区别
* `#import`确保引用的文件只会被引用一次,不会引起交叉编译;
* 两者均把后边的文件名所代表的文件拷贝到指令所在的文件;
* `#import`会链入该头文件的全部信息,包括实例变量和方法;
* `@class`只告诉编译器其后跟内容为类的名称,不用管该类是如何定义的,且一般在头文件中使用。
* 使用`#import`的优点:可解决头文件中的循环依赖问题
7. `nonatomic`和`atomic`
* `atomic`:默认值。只有一个线程可以访问,至少在当前线程的读取是安全的,但由于使用 **同步锁** 开销过大,会损耗性能(macOS性能较好,可不考虑该问题),其是一个原语操作,编译器会自动生成一些互斥加锁的相关代码,避免变量读写不同步等的问题。保证必须当前一个线程执行完相关的`setter`方法后,另一个线程才执行`setter`;
* `nonatomic`:不保证`setter/getter`的原语执行,故可能会取到不完整的值。因此我们可以得到一个约束:**多线程环境下的原子操作是非常非常非常必要的!!!**
* 解释两个概念:原语和原语操作,
- **原语**:内核或微内核提供核外调用的过程或函数,是一段用机器指令编写的、完成特定功能的程序代码,且执行过程不允许中断;
- **原子操作**:在多进程(或线程)的操作系统中不能被其它进程(或线程)打断的操作。当该操作不能完成时,必须回到该操作之前的状态,原子操作是不可拆分的,原子操作是中断且安全的。其本质实现还用到了 **自旋锁** ,自旋锁大概的意思是,当被其它对象使用时,待使用对象一直在循环等待并查看是否被释放。
8. `@Property`关键词及其相关关键字的理解:
* 根据被修改的可能性,、@Property中关键字的排列推荐为:原子性、读写性、内存管理特性;
* **原子性:** automatic和nonautomatic。决定了该属性是否为原子性的,即在多线程的操作中,不能被其它线程打断的特性,一旦使用了该变量的操作不能被完整执行时,将会回到该变量操作之前的状态,但原子性即automatic因为是原语操作(保证setter/getter的原语执行),会损耗性能,在iOS开发中一般不用,而在macOS开发中随意。
* **读写性:** readOnly和readWrite。默认为readWrite,编译器会帮助生成serter/getter方法,而readOnly只会帮助生成getter方法。 // 此处可拓展,非要修改readOnly修饰的变量怎么办,可用KVC,又可继续拓展KVC相关知识。
* **内存管理特性:** assign、weak、strong、unsafe_unretained。
- assign:一般用于值类型,比如int、BOOL等(还可用于修饰OC对象);
- weak:用于修饰引用类型(弱引用,只能修饰OC对象);
- strong:用于修饰引用类型(强引用);
- unsafe_unretained:只用于修饰引用类型(弱引用),与weak的区别在于,被unsafe_unretained修饰的对象被销毁后,其指针并不会被自动置空,此时指向了一个野地址。
9. `Block`的理解:
* Block与函数指针非常类似,但Block能够访问函数以外、词法作用域以外的外部变量的值;
* Block不仅实现了函数的功能,还携带了函数的执行环境;
* Block实际上是指向结构体的指针;(可参考[这篇文章](https://www.cnblogs.com/yoon/p/4953618.html))
* Block会把进入其内部的基本数据类型变量当做常量处理。】
* Block执行的是一个回调,并不知道其中的对象合适被释放,所以为了防止在使用对象之前就被释放掉了,会自动给其内部所使用的对象进行retain一次。
* Block使用`copy`修饰符进行修饰,且不能使用`retain`进行修饰,因为`retain`只是进行了一次回调,但block的内存还是放在了栈空间中,在栈上的变量随时会被系统回收,且Block在创建的时候内存默认就已经分配在栈空间中,其本身的作用域限于其创建时,一旦在超出其创建时的作用域之外使用,则会导致程序的崩溃,故使用`copy`修饰,使其拷贝到堆空间中,block有时还会用到一些本地变量,只有将其copy到堆空间中,才能使用这些变量。
10. 循环引用的几种情况:
* **NSTimer**:
* **block**:
* **delegate**:
11. Objective-C中的反射机制
Foundation框提供了反射API,可通过API把字符串转为`SEL`操作,且因为OC的动态性,这些操作都发生在**运行时**。我们可以在运行时选择需要创建的实例,并动态的选择调用方法,这些操作可以由服务器下发的参数进行控制。
什么意思呢?比如说,我们可以通过后台推送过来的数据进行动态跳转,跳转到页面后再根据返回的数据执行对应的操作。比如,
```json
// 假设返回了这些数据
{
"vc" : "homeViewController",
"methon" : "reloadData",
"propertys" : [
{"url" : "www.baidu.com"},
{"title" : "百度"},
],
}
```
我们可以这么写,
```ObjC
Class class = NSClassFromString(dict[@"vc"]);
UIViewController *vc = [[class alloc] init];
NSDictionary *parameter = dict[@"propertys"];
[parameter enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([vc respondsToSelector:NSSelectorFromString(key)]) {
[vc setValue:obj forKey:key];
}
}];
[self.navigationController pushViewController:vc animated:YES];
SEL selector = NSSelectorFromString(dict[@"method"]);
[vc performSelector:selector];
```
12. iOS 中有以下几种随机数取法:
* `random()` :伪随机数,需要自己加种子,否则每次产生的随机数都是一样的。,一般都是以当前时间做种子。不推荐;
* `arc4random()` :真随机数,但如果要获取一定范围内的随机数,需要自己做模运算;
* `arc4random_uniform()` :真随机数,只需要填入范围上边界数字即可获取到对应范围内的真随机数。
持续更新中.....
13. 如果在 `category` 中声明了一个和原类同名的方法,或和该类的另一个 `category` 中的方法同名,那么在运行时究竟哪个方法会被执行是不确定的。
14. 用 `Objective-C` 编写的程序不能直接编译成可令机器读懂的机器语言,而是在程序运行时,通过运行时(runtime)把程序转译成可令机器读懂的机器语言;用 `C++` 编写的程序,编译时就直接编译成了可令机器读懂的机器语言。这也就是为什么把 `Objective-C` 视为一门动态开发语言的原因。
15. 开发语言的三个不同层次:
* 传统的面向过程的语言开发。
* 改进的开发面向对象的语言。
* 动态开发语言。
16. **在头文件中尽量减少其他头文件的引用**
* 通过 `#import` 修饰符来建立被引用类的指针。用 `#import` 建立类之间的复合关系时,也暴露类所引用类的实体变量和方法,可以使用 `@class` 来告诉编译器:这是一个类。
* `#import` 引用类同一个头文件,或者这些文件是依次引用的,如 A->B、B->C、C->D,当最开始的那个头文件有变化时,后面所有引用它的类都需要重新编译,如果自己的类有很多的话,这将耗费大量时间,使用 `@class` 则不会。
* 注意「类循环引用」的问题。
16. 尽量使用模块方式与多类建立复合关系
* `#include` 做的事情其实就是简单的复制、粘贴,将目标 `.h` 文件中的内容一字不落的复制到当前文件中。
* `#import` 实际上与 `#include` 是一样的,不过 `Objctive-C` 为了避免重复引用可能带来的编译错误(比如 B 和 C 都引用类 A,D 又同时引用了 B 和 C,这样 A 中定义的东西就在 D 中被定义了两次,造成重复)而加入了 `#import`,保证每个头文件只会被引用一次。`#import` 的实现是通过对 `#ifdef` 一个标志进行判断,然后再引入 `#define` 这个标志来避免重复引用。
* **使用 `.pch` 预编译头文件**不实用。确实能缩短编译时间,但在工程中不能随处访问的东西,却都暴露了。
* 使用模块(`Modules`)来解决。在 `Build Settings` 中搜索 `Modules` 修改为 `YES`。默认情况下,模块功能在所有的新工程中都是开启的,语法修改如下:
```objc
@import UIKit;
@import MaKit;
```
* 如果只导入框架中自己需要的部分可以这么做:
```objc
@import UIKit.UIView;
```
* 在技术上,我们不需要把所有 `#import` 都换成 `@import`,因为编译器会隐式的转换它们,但建议尽可能的使用新语法。
17. 尽量避免使用 `#define`,`#define` 预处理指令不包含任何的类型信息,仅仅是在编译前做替换操作,它们在重复定义时不会发出警告,容易在整个程序中产生不一样的值。
18. 处理隐藏的返回类型,优先选择 `instancetype` 而非 `id`。
19. `NSLog` 并不是向 Xcode 控制台中输出信息,而是向苹果系统日志(Apple System Log,ASL)中输出错误信息。因此我们要把 `NSLog` 看作是 `printf` 和 `syslog` 的结合体:在调试时将消息发送到 Xcode 控制台,在设备上运行时将消息发送到系统全局日志。然后 `NSLog` 记录的数据就可以被任何拿到物理设备的人获取。在发布应用之前把 `NSLog` 从代码中删除。
- 在发布版本中禁用 `NSLog`:
```objc
#ifdef DEBUG
#define NSLog(...) NSLog(__VA_ARGS__);
#else
#define NSLog(...)
#endif
```
20. `%x` 和 `%n` 分类符对攻击者来说非常有用。
21. `strcpy` 缓冲区溢出攻击。如果输入超出了固定字符长度的字符,超出的那部分字符就会覆盖相邻栈变量的内存,这就意味着,攻击者可以重载函数的返回地址,让程序执行恶意代码,攻击者可以把恶意代码直接放在输入当中,也可以放在内存中的其它位置。
22. 防止 `XSS` 攻击:
- 设置黑名单。告知用户哪些字符不允许输入。
- 设置白名单。只允许输入哪些字符。
- 显示字符时,选转为 `HTML` 字符串,再输出,保证了 `<` 和 `>` 等特殊字符被转译。
23. 空指针(NULL 指针)是指没有存储任何内存地址的指针。野指针,是指向“垃圾内存”的指针。
24. `@autoreleasepool`。在 ARC 下,没有办法手动通知系统对某个对象执行 `autorelease`,当给一个对象设置了 `__autorelease` 修饰符修饰时,相当于这个对象在 MRC 下给这个对象发送了 `autorelease` 消息,注册到了 `autorelease pool` 中。
25. 指针地址对齐。为了加快内存的 CPU 访问,包括 macOS 和 iOS 在内的几乎所有系统架构都使用了**指针地址对齐**概念,其指在分配堆中的内存时往往采用**偶数倍**或以 2 为倍数的内存地址作为地址边界。
26. **标记指针**。由于指针地址对齐和 64 位超大地址的出现,指针地址仅仅作为内存的地址比较浪费,故可以在指针地址中保存或附加更多的信息,进而引入了**标记指针**的概念。其指的是那些指针中包含特殊属性或信息的指针。
27. 利用标记指针处理 `NSNumber`,直接可以把实际的值保存到指针中,而无须再去访问堆中的数据,提高内存访问速度和整体运算速度。
28. 标记指针堆 `isa` 指针的优化。在 OC 中所有的类都继承自 `NSObject`,因此每个对象都有一个 `isa` 指针指向它所属的类。在 32 位和 64 位环境下, `isa` 指针会产生不同的变化。
- 在 32 位环境下,对象的引用计数都保存在一个外部的表中,对引用计数的增减操作都要先锁定这个表,操作完成后才解锁,效率比较慢。
- 在 64 位环境下,`isa` 指针也是 64 位,实际作为指针的部分只用到其中的 33 位,剩余的部分会运用到**标记**指针的概念。其中的 19 位将保存对象的引用计数,这样对引用计数的操作只需要**原子**的修改这个指针即可。如果引用计数超过 19 位,才会将引用计数保存到外部表,情况较少,故效率可以大大提高。
29. 兼容 32 位和 64 位环境下代码编写事项(其实没啥用了)。
- **不要将长整型数据赋予整型**。
- **善用 `NSInteger` 来处理 32 位和 64 位之间的转换**。`NSInteget` 在 32 位运行时是 32 位整数,在 64 位运行时是 64 位整数。
- **创建数据结构要注意固定大小和对齐**。
- **选择一种紧凑的数据表示类型**。
30. 常量字符串和一般字符串的区别。
- 由于编译器的优化,相同内容的常量字符串的地址值是完全相同的。
- 如果使用常量字符串来初始化一个字符串,那么这个字符串也将是相同的常量。
- 对常量字符串永远不要 `release`。
31. 在访问集合时要优先考虑使用快速美剧。
- 使用快速枚举,枚举更安全。因为枚举会监控枚举对象的变化,如果在枚举的过程中枚举对象发送变化会抛出一个异常。
- 多个枚举可以同时进行,因为在循环中被循环对象是禁止修改的。
32. 同一数组(`NSArray`)可以保存不同的对象,但不能存储 `float`、`int`、`double` 等基本类型和 `nil`,否则存储基本类型都会被设置为 0,不能存储 `nil` 是因为数组必须用 `nil`。
33. `autorelease pool` 提供一种机制:让对象延迟 `release`。这个对象放弃所有权,但又想避免立即释放(如何函数的返回值)。有些时候,可能会使用自己的 `autorelease` 池块。
- 通常情况下,应该使用 `release`,而不是 `autorelease`,只有在不适合立即回收对象的情况下,才应该使用 `autorelease`。
- 当返回一个新创建的(拥有)的对象时,应该使用 `autorelease` 而不是 `release` 来释放所有权。
- 对于拥有 `alloc` 返回的对象而言,失去释放所有权之前,应先失去对该对象的引用。
34. 对象的 `isa` 实例变量指向对象的类。
35. `alloc` 和 `init` 不仅进行对象的内存分配,还要对它的 `isa` 实例变量和 `retain count` 初始化。
36. 对象销毁或者被移除一定考虑所有权的释放。
- 从集合中移除对象,集合要释放对被移除对象的所有权。
- 防止出现父对象被释放前而子对象的所有权已经释放。
- 释放对象前,要确保其他对象对该对象的所有权已经释放。
37. 编译指令。
指令 | 含义
---- | ----
`@private` | 变量只限于声明它的类访问
`@protected` | 变量可以被声明它的类及继承该类的类使用。没有明确指定访问范围的变量默认为 `@protected`
`@public` | 变量可以在任何位置访问
`@package` | 变量可以在同一个 `framework` 中访问
38. 动态属性。在 OC 2.0 中增加了一个新的关键字 `@dynamic`,用于定义动态属性。动态属性相对于 `@synthesis` 不是由编译器自动生成 `setter` 和 `getter`,也不是由开发者自己写的 `setter` 或 `getter`,而是在运行时动态添加的 `setter` 和 `getter`。
实现动态属性需要在代码中覆盖 `resolveInstanceMethond` 来动态添加 `name` 的 `setter` 和 `getter`。这个方法在每次找不到方法时都会被调用。`NSObject` 的默认实现就是抛出异常。
39. 在覆盖基类的方法决定是否调用 `super`,基于打算如何重新重写方法,可以注意以下亮点:
- 如果打算**补充**基类实现的行为,调用 `super`。
- 如果打算**替换**基类实现的行为,不调用 `super`。
40. 在 OC 中,所有的方法都是虚方法。实现纯虚方法依赖协议来实现。
41. 类的对象支持归档和解档,该类必须遵循 `NSCoding` 协议;必须实现对对象进行编码(`encodeWithCoder:`)和解码(`initWithCoder:`)的方法。
42. `KVC` 的实现原理主要是运用了 `isa-swizzling` 技术(类型混合指针机制),通过其来实现内部查找定位。`isa` 指针指向的是对象的类,这个类也是一个对象,有自己的权限,根据类的定义编译而来。类对象负责维护一个方法调度表,该表本质上是由指向类方法的指针组成的,类对象中还保留一个基类指针,该指针也有自己的方法调度表和基类,还有所有通过继承得到的公共和保护的实例变量。`isa` 指针对消息分发机制和 Cocoa 对象的动态能力很关键。
43. 在 `swift` 中可以使用 `extension` 对类的实现进行拆分,在 `ObjC` 中可以选择使用 `category` 对类的实现进行拆分。
44. 内省是对象揭示自己作为一个运行时对象的详细信息的一种能力,这些详细信息包括对象在继承树上的位置、对象是否遵循特定的协议,以及是否可以响应特定的消息。
45. `isEqual` 方法先检查指针的等同性,然后是类的等同性,最好调用对象的比较器进行比较。
46. 使用 `new` 创建对象时,实际发生了两个步骤:第一个步骤,为对象分配内存,也就是说对象活动存储其实例变了的内存快;第二步,自动调用 `init` 方法,初始化对象使其处于可用状态。没有被初始化的指针都是 `nil`。
47. 使用类拓展隐藏私有信息。
48. 父对象应该强引用子对象,子对象变量应该弱引用父对象。
49. 对一些不支持 `__weak` 引用的类,可通过 `Unsafe Unretained` 引用来暗度陈仓。
50. 类别的一些内容:
- 子类体现了类的上下级关系,而类别是类间的平级关系。
- 类别具有替换特性,如果类别方法与类内某个方法具有同样的方法签名,类别里的方法将会替换类的原有方法。
- 类别是为类**增加外部方法**的话,类扩展是用做类的**内部拓展**。
51. 类簇。基于抽象工程模式,可以用于隐藏实现的详细细节,为调用者提供一个简单的接口。看《编写高质量代码:改善 OC 程序的 61 个建议》第 48 个建议。
52. `alloc` 方法使用应用程序默认的虚存区,区是一个按页对齐的内存区域,用于存放应用程序分配的对象和数据。除了分配内存之外,还做 了:
- 将对象的保持数设置为 1。
- 使初始化对象的 `isa` 实例变量指向对象的类。对象类是一个根据类定义编译得到的运行时对象。
- 将其他所有的实例变量初始化为 0(或与 0 等价的类型,比如 `nil`,`NULL`,`0.0`)
53. 在创建对象时,通常应该在处理之前检查返回值是否为 nil。
54. 需要 OC 对象的存取器来帮助进行引用计数。
55. 通过调用 `[xxx setValueForKey:xxx]` 要比 `[xxx setValue:xxx]` 要慢得多,因为编译器无法检查传递给 `valueForKey:` 的字符串是否有效,同时效率也变成了原来的 5%,如果需要获取值的运行参数,则使用 `[xxx performSelector: xxx]` 是直接消息发送速度的 2 倍,比 `valueForKey:` 快 10 倍。
56. KVO 和 Cocoa 绑定是基于 KVC 的,其速度不会很快。
57. **OpenUDID 是什么?**实际上是跟着 app 走,每次重装 app 都会重新生成一个 id,一般都会把它放到 keychain 中进行系统级的持久化。
58. `NSUserDefaults` 实际上是在 Library 文件夹下生成一个 plist 文件,如果该文件太大,读取时会比较耗时,因为加载的时候是直接全部 load 到内存中。头条主端通过测试,200 多个缓存数据,通过符号断点 `+[NSUserDefaults standardUserDefaults]` 确定最早一次的 `+load()` 从执行到结束耗时 1.8ms
59. `mach_absolute_time` 获取当前时间的「纳秒」,需要 mach 库。
60. 忽略警告的大概做法:
```swift
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
[UNUserNotificationCenter currentNotificationCenter].delegate = [TTNotificationCenterDelegate sharedNotificationCenterDelegate];
#pragma clang diagnostic pop
```
61. 想要成为一个 `AppDelegate` 需要:
* 继承 `UIResponder` 和 `UIApplicationDelegate` 协议
* 在 `main.m` 中通过 `UIApplicationMain` 进行初始化
62. ARC 在「编译」的时候插入内存管理代码
63. bitcode。上传 app store 的时候实际上上传的是一个「平台无关的代码」,用户在下载 app 的时候 App Store 会根据用户的机型翻译成对应的机器代码进行下发。
64. Link 链接
* 解决依赖
* 确定地址引用
* Mach-O 结构
* 生成可执行文件
65. Xcode 提示一个符号找不到声明是在「语法解析生成 AST」时出错。
66. 打包提示`missing symbols`是在「链接」出错。
67. OC 中的 ARC 是在编译的**机器码**生成支持的。
68. 代码中使用了静态库中的某个方法,是在**链接**时确定符号地址的
69. OC 中在方法里跑另外一个 方法/代码块的做法:
```objc
- (void)_enterFullScreenWithAngle:(CGFloat)angle animted:(BOOL)animated {
void (^animation)() = ^{
};
animation()
}
```
70. NSCache
* 线程安全,键不会发生复制操作
* 拥有 LRU,不需要自己写缓存置换算法,如果用 NSCache 去做的话,就需要了
* `NSCache` 可以设置缓存中的对象数量
71. 为什么在 iOS 上用 nonamatic,macos 不用?
* iOS同步锁开销很大,会带来性能问题。一般情况下不要求必须是原子性的,因为使用了也并不能确保真正的线程安全。如果一个线程多次读取某属性值的过程中有别的线程在同时改写该值,那么即便使用了atomic,也还是会读到不同的属性值。
72. category 和 extension
* OC 的 category 相当于 Swift 的 extension
* OC 的 extension 加私有方法,直接在创建各种 UIView 的时候就已经带上了
73. __auto_type
* 自动类型推倒
* https://pspdfkit.com/blog/2017/even-swiftier-objective-c/
```objc
#define let __auto_type const
#define var __auto_type
let anElegantView = [UIView new];
let something = (TheType *)array.firstObject;
var something = array.firstObject;
```
74. Enum 关联对象
```swift
enum CSSColor {
case named(ColorName)
case rgb(UInt8, UInt8, UInt8)
}
var color1 = CSSColor.named(.black)
var color2 = CSSColor.rgb(0xAA, 0xAA, 0xAA)
switch color2 {
case let .named(color):
print("\(color)")
case .rgb(let r, let g, let b):
print("\(r), \(g), \(b)")
}
```
75. 集合的可变类,属性不使用copy修饰符的原因?
* [文章解释](https://juejin.im/post/5bedfdaa6fb9a049cd53c56d)
* 在 ARC 下,编译器在合成 `setter` 方法时,走的是 `copy`,就会把原先的例如 `NSMutableArray` 变成了 `NSArray`,再执行 `addObject` 方法时会找不到方法而报错。
* copy 默认调用的是 `copyWithZone `
* [相关 session](https://developer.apple.com/videos/play/wwdc2017/411/)
76. 预编译阶段处理的宏定义,在组件进行二进制化后会失效,特别是某些依赖 `DEBUG` 宏的调试工具,在二进制化之后就不可见了。针对这种情况可以单独抽出一个类来替换宏,把需要用到宏的地方归类到一个中间者去完成,并且不让这个中间者去做二进制化。
```objc
// TDFMacro.h
@interface TDFMacro : NSObject
+ (BOOL)enterprise;
+ (BOOL)debug;
+ (void)debugExecute:(void(^)(void))debugExecute elseExecute:(void(^)(void))elseExecute;
+ (void)enterpriseExecute:(void(^)(void))enterpriseExecute elseExecute:(void(^)(void))elseExecute;
@end
// TDFMacro.m
@implementation TDFMacro
+ (BOOL)enterprise {
#if ENTERPRISE
return YES;
#else
return NO;
#endif
}
+ (BOOL)debug {
#if DEBUG
return YES;
#else
return NO;
#endif
}
+ (void)debugExecute:(void (^)(void))debugExecute elseExecute:(void (^)(void))elseExecute {
if ([self debug]) {
!debugExecute ?: debugExecute();
} else {
!elseExecute ?: elseExecute();
}
}
+ (void)enterpriseExecute:(void (^)(void))enterpriseExecute elseExecute:(void (^)(void))elseExecute {
if ([self enterprise]) {
!enterpriseExecute ?: enterpriseExecute();
} else {
!elseExecute ?: elseExecute();
}
}
@end
```
77. 使用 `printf` 语句输出内容可以保值某段计算代码不会被视为死代码,然后被计算机优化掉。
78. 选取相片后,通过 `asset` 拿到具体的 `UIImage`,可以通过以下字符串拼接 `URL` 获取:
```objc
if (item.asset) {
NSString *assetID = [item.asset.localIdentifier substringToIndex:(item.asset.localIdentifier.length - 7)];
imageURL = [NSURL URLWithString:[NSString stringWithFormat:@"assets-library://asset/asset.jpg?id=%@&ext=jpg", assetID]];
} else {
imageURL = [NSURL URLWithString:item.fullpathLink];
}
```
79. OC 自定义 `setter` 和 `getter` 命名
```objc
// You can customize the getter and setter names instead of using default 'set' name:
@property (getter=lengthGet, setter=lengthSet:) int length;
```
80. `valueForKeyPath` 为什么慢,因为走的是 hash
81. 一个 Button 的点击事件 @selector 如何优雅的传递多参数
* 使用 block 捕获,包装一下
82. OC 没有办法将方法标为私有。其每个对象都可以响应任意消息,而且可在运行期检视某个对象所能直接响应的消息,跟进给定的消息查出其对应的方法,这一工作要在运行期才能完成。
* 定义私有方法时最好在方法前加上前缀 `p_xxx`
83. 在使用协议的时候,每次都要在原类中检查 delete 是否实现了该协议中的某个方法,可以选择使用标志位的方法去缓存检查的值。
* 这么做的前提是检查的方法会被调用很多次,并且也确实是因为检查了很多次的这些方法造成了性能瓶颈,我们采取优化它。
84. 分类中的方法是直接添加到类里面的,他们就好比这个类中的固有方法。
* 将分类方法加入类中的这一操作是在运行期系统加在分类时完成的,在运行期系统会把分类中所实现的每个方法都加入到类的方法列表中。
* 如果类中已有此方法,分类中又实现了一遍,那么分类中的方法将会覆盖类中实现的相关方法,而另外一个分类中的方法由覆盖了这个分类的方法。
* 多次覆盖的结果,以最后一个分类为准。
85. OC 对象所占内存在 release 后,只是放回“可用内存池”,如果执行 `NSLog` 时尚未覆写对象,那么该对象仍然有效。
86. 避免悬挂指针,在释放完对象后置 `nil`
87. 遇到保留环时,在「垃圾回收器」环境下会把三个对象全都收走,在引用计数架构中,需要使用「弱引用」来打破。
88. CoreFoundation 对象不归 ARC 管理。
89. C++ 对象由于抛出异常会缩短其生命周期,所以发生异常时必须析构,不然就会泄漏。
90. 自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送 `release` 消息。
91. GCD 机制中的线程默认都有自动释放池,每次执行“事件循环”时就会将其清空,故不需要在 GCD 部分创建自动释放池。
92. 向已回收的对象发送消息是不安全的,但这么做有时可以,有时不行,完全取决于对象所占内存有没有为其他内容所覆写。
* 在没有崩溃的情况下,那块内存可能只复用了其中一部分,该对象中的某些二进制数据依然有效。
* 还有一种可能,那块内存恰好为另外一个有效且存活的对象所占据,运行期系统会把消息发送到新对象哪里,新对象可能会应答,也可能不会,如果不能应答就崩溃。
93. 单例对象的「保留计数」都很大很大。
94. 浮点数的 `NSNumber` 对象保留计数是 1。
`Block` 会把它所捕获的所有变量都拷贝一份,拷贝的不是对象本身,而是指向这些对象的指针变量。
95. crash 可以分为 四类
* OC Exception
* Mach Exception
* Unix Signal
* C++ Exception

96. 发生 OOM 时app 在前台的话,会 crash
97. 使用 `GCD` 执行异步派发时,需要拷贝块。
98. 想让几段代码按顺序执行,或者执行 A 代码块时,B 代码块不能执行,可以考虑用 GCD 的串行队列,能够保证一个代码块在执行时,另外一个代码块在等待执行。
99. **读可以并行读,写要求顺序写。**在队列中,`barrier` 队列(栅栏队列)必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序执行。并发队列如果发现接下来要处理的块是个 `barrier` 块(栅栏块),那么就一直要等待当前所有并发块都执行完毕,才会单独执行这个栅栏块。栅栏块执行完毕后,才按照正常的方式继续向下处理。
100. `performSelector` 系列方法在内存管理方法容易有遗漏。它无法确定将要执行的选择子具体是什么,因而 ARC 编译器也就无法插入适当的内存管理方法。
101. `sizeThatFits` 与 `sizeToFit`
* `sizeToFit` 可以自动计算宽高,并且还会修改视图的 `frame`
* `sizeThatFits` 只能自动计算宽高
102. `NSDateFormatter` 会造成性能损耗
* 过度的创建其用于 `NSDate` 和 `NSString` 的转化,会造成性能下降。
* 如果需要 `NSDateFormatter` 进行频繁的操作,推荐对其缓存起来。
103. 为什么 `cornerRadius` 会造成性能下降
* 其会触发离屏渲染
* 指图像在绘制到屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕
* `alloc` 一块内存,进行渲染。
* onScreen 和 offScreen 之间上下文切换代价比较大。
104. 转屏逻辑可以写在 `layoutSubView时` 中。因为每次 `frame` 切换都会导致该方法的调用。
* 当时如果视图的 `frame` 为0,则不会被调用。
105. 每一个 `NSThread` 对象都是一个完整的线程。
106. 遍历集合的几种方式:
* `for`
* `NSEnumerator`
* `for-in`
* `块枚举法`
107. 实现缓存功能时优先选用 `NSCache` 而不是 `NSDictionary` 对象。因为其可以提供优雅的**自动删减**功能,且是**线程安全**的,与字典不同,不会拷贝键。还可以给其设置上线,用于限制缓存中的对象总个数及总成本。
108. 在**加载阶段**,如果类实现了 `load` 方法,那么系统就会调用它。分类里也可以定义此方法,类的 `load` 方法要比分类中的先调用。与其他方法不同,`load` 方法不参与覆写机制,也就是说,父类和子类都写了 `load` 方法,不会向上执行,各执行各的。
109. 首次使用某个类之前,系统会向其发送 `initialize` 消息。由于此方法遵从普通的覆写机制,所以通常应该在里面判断当前要初始化的是哪个类,也就是说,如果父类写了该方法,子类没写,父类在执行该方法时,子类也会被执行。
* 无法在编译器设定的全局常量,可以放在 `initialize` 方法里初始化
110. GCD 相当于是个线程池。
111. 队列设置优先级时,低优先级队列任务可能会阻塞高优先级,尽量用默认优先级,因为可能会发生「优先级反转」。
> 优先级翻转是当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。
112. 异步操作同步返回可以使用 GCD 的 `dispatch_semaphore`。
113. 子线程发通知主线程收不到,因为在哪个线程发送通知就在哪个线程接收通知,有两种做法可以解决。
* 在发起通知的时候检查一遍当前线程是不是主线程,不是主线程切回主线程发送。
* 在收到通知的地方统一归并到主线程中。
114. 如何判断当前线程是否为主线程?每个线程都会有自己的名字,没有名字的线程则会打印出 `(null)`,因此可以参考 `SDWebImage` 的做法:
```objc
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
```
其中运用到了 `strcmp` 这个 C 语言函数,其对两个字符串判断的逻辑如下:
* 字符串1=字符串2,返回0。
* 字符串1>字符串2,返回一个正整数。
* 字符串1<字符串2,返回一个负整数。
115. 转屏逻辑可以写在 `layoutSubView时` 中。因为每次 `frame` 切换都会导致该方法的调用。
* 当时如果视图的 `frame` 为 0,则不会被调用
116. 使用以下名称开头的方法名意味着自己生成的对象只有自己持有:
* `alloc`
* `new`
* `copy`
* `mutableCopy`
当对方法进行命名时,如果出现了下列类似的方法名,也意味着自己生成并持有对象:
* `allocMyObject`
* `newThisObject`
* `copyThis`
* `mutableCopyYourObject`
117. 调用类似 `[NSMutableArray array]` 方法使得取的对象存在,但自己不持有对象:
```objc
- (id)objc {
id obj = [[NSObject alloc] init];
// 自己持有对象
[obj autorelease];
// 取得的对象存在,但自己不持有对象
return obj;
}
```
118. 想要让自己原本并不持有的对象,变为持有,给该对象加上一个 `[obj retain]` 进行持有。
119. 重复释放或释放了自己不持有的对象,会导致崩溃。
120. `dealloc` 方法到底什么时候调用?
每次执行单次 `[obj release]` 或系统自动执行统一 `release` 时,判断某个对象的 `retainCount` 是否为 0,为 0 则手动调用 `[self release]` 方法,`free()` 掉该对象的内存。
121. Apple 通过散列表来管理引用计数。表 `key` 为内存块地址的散列值,`value` 为内存块的引用计数。这么做可以通过计数表的各个记录追溯到各对象的内存块,即使出现故障导致对象占用的内存块损坏,但只要引用计数没有被破坏,就能够确认各内存块的位置。
122. OC 中同时重写 `setter` 和 `getter` 需要把属性改为 `@dynamic` 修饰,告知 Xcode 不要帮我自动生成。
123. 该方法返回 `autorelease` 对象。
```objc
id array = [NSMutableArray arrayWithCapacity:1];
// 相当于
id array = [[NSMutableArray alloc] initWithCapacity:1] autorelease];
```
124. 调用 `[obj autorelease]` 本质上是调用:
```objc
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
```
为了能够高效地运行应用程序中频繁调用的 `autorelease` 方法,使用了 `IMP Caching` 的机制,在框架初始化的时候对其结果值进行缓存。运行效率一般是其它方法的 2 倍。
124. 如果嵌套生成多个 `NSAutoreleasePool` 对象,`[obj autorelease]` 会使用最内侧的 `NSAutoreleasePool` 对象。
125. `NSAutoreleasePool` 的 `drain` 方法实现细节:
* 调用 `drain` 方法,本质上是在调用 `[self dealloc]` 方法。
* 调用 `[self dealloc]` 方法,本质上是在调用 `[self emptyPool]` 和 `[array release]`,清空 pool 和自己本身管理对象数组的 release。
* 调用 `[self emptyPool]` 本质上是在循环遍历对象数组中 `autorelease` 添加进来的对象的 `[obj release]` 方法。
总的来说,就是会让每个对象都会被 `release`。
126. 对 `[NSAutoreleasePoolObjc autorelease]` 会怎样?
运行时会发生异常,因为 `NSAutoreleasePool` 类中已经重载了 `autorelease` 方法,运行时会报错。
127. `id` 和对象类型在没有明确指定所有权修饰符时,默认为 `__strong` 修饰符。
128. `+load()` 在这个文件被装载时调用。只要是在 Compile Sources 中出现的文件总是会被装载,这与该类是否被用到无关,因此 `load` 方法总是在 `main()` 函数被调用。子类实现 `load` 方法时,会先调用父类的 `load` 方法。当类的加载是耗时或者需要消耗比较多的内存的时候,尽量不要在 `load` 方法里面做这些耗时的工作,因为这样会**增加App的启动时间**,降低用户的体验。由于调用load方法时的环境很不安全,我们应该尽量减少 `load` 方法的逻辑。另一个原因是load方法是线程安全的,它内部使用了锁,所以我们应该避免线程阻塞在 `load` 方法中。一个常见的使用场景是在 `load` 方法中实现 `Method Swizzle`
* 这个方法会在类的第一个方法调用前被调用。首先会先调用父类的 `initialize` 方法,如果子类没有实现 `initialize` 方法,那么父类会多次触发这个方法,为了避免这种情况的发生,可以在实现的方法里面添加一个判断。`initialize` 其实可以被认为是延迟加载的方法,类加载的时候并不会执行这个方法,只有当类实例化的时候,或者类的第一个方法被调用的时候才会执行这个方法。
* `load` 方法通常用来进行 `Method Swizzle`,`initialize` 方法一般用于初始化全局变量或静态变量。
129. `__autoreleasing` 一个有趣的地方:
```objc
NSError *error = nil;
BOOL result = [pbj performOperationWithError:&error];
```
该方法的声明为:
```objc
- (BOOL) performOperationWithError:(NSError **)error;
```
`id` 的指针或对象的指针会默认加上 `__autoreleasing` 修饰符,所以等同于以下代码:
```objc
- (BOOL) performOpertaionWithError:(NSError * __autoreleasing *)error;
```
但是如果是这样直接赋值的话,会产生编译器错误:
```objc
NSError *error = nil;
NSError **pError = &error;
```
因为赋值给对象指针时,所有权修饰符必须一致,修改:
```objc
NSError *error = nil;
NSError * __strong *pError = &error;
```
对于其它所有权修饰符也是一样的。
130. 以 `init` 开始的方法的规则要比 `alloc\new\copy\mutableCopy` 更严格。该方法必须是实例方法,并且必须要返回对象。返回的对象应为 `id` 类型或该方法声明类的对象类型,或者是该类的超类或子类。该返回的对象不注册到 `autoreleasepool` 上,基本只对 `alloc` 方法返回值的对象进行初始化处理并返回该对象。
131. 被 `__unsafe_unretained` 修饰的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便有可能遭遇内存泄漏或程序崩溃。
132. `id` 和 `void *` 类型对象的转换可以基于 `__bridge` 进行,但 `__bridge` 转换不改变对象的持有状况。
133. `id *` 类型默认为 `id __autoreleasing` 类型。
134. 使用 `__weak` 修饰符的变量所引用的对象被废弃,将自动赋值为 `nil`,且该对象会被自动注册到 `autoreleasepoll`中。
135. `__weak` 有关的这行代码
```objc
id __weak obj1 = obj0
```
实现细节最后会调用一个 `objc_storeWeak(&obj1, 0);` 函数,该函数把第二参数的赋值对象的地址作为 `key`,将第一参数的附有 `__weak` 修饰符的变量的地址注册到 `weak` 表中。如果第二个参数为 0,则把变量的地址从 `weak` 表中删除。
`weak` 表和引用技术表相同,作为**散列表**被实现。
136. 释放 `__weak` 修饰的对象时,最后会调用 `objc_clear_deallocating` 函数,其动作为:
* 从 `weak` 表中获取废弃对象的地址为 key 的记录;
* 将包含在记录中的所有附有 `__weak` 修饰符变量的地址,赋值为 `nil`;
* 从 `weak` 表中删除该记录;
* 从引用计数表中删除废弃对象的地址为 key 的记录。
可见,大量使用 `__weak` 修饰符的对象会消耗对应的 CPU 资源,只需要在避免循环引用时使用该修饰符。
137. 每使用一个被 `__weak` 修饰符修饰的对象时,都会被加入到 `autoreleasepool` 中,为了避免这个问题,可以先把其用 `__strong` 修饰符修饰的对象接一下。
138. 做埋点时如果不能保证取的值都是存在的话,使用字典 `setValue:forKey:` 中 value 能够为 nil,但是当 value 为 nil 的时候,会自动调用`removeObject:forKey` 方法。
139. 现在的 `blocks` 并没有实现对 C 语言数组的截获,可以使用指针解决。
```objc
const char *text = "hello";
void (^blk)(void) = ^ {
printf("%c\n", text[2]);
}
```
140. `block` 会被转换为 C 语言源码编译。同时也为 OC 的对象。
141. 所谓“截获自动变量值”意味着在执行 `block` 语法时,`block` 语法表达式所使用的自动变量值被保存到 `block` 的结构体实例中(`block` 本身)。
142. iOS 13 中返回的 `device token` 变化异常。
iOS 13 之前,基本上都是这么去获取的 device token:
```objc
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSString *deviceTokenString = [[[[deviceToken description]
stringByReplacingOccurrencesOfString: @"<" withString: @""]
stringByReplacingOccurrencesOfString: @">" withString: @""]
stringByReplacingOccurrencesOfString: @" " withString: @""];
}
```
```shell
```
iOS 13 后,变为了
```objc
{length = 32, bytes = 0x778a7995 29f32fb6 74ba8167 b6bddb4e ... b4d6b95f 65ac4587 }
```
所以我们需要这么做,并且该代码也是向下兼容的:
```objc
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(nonnull NSData *)deviceToken
{
const unsigned *tokenBytes = [deviceToken bytes];
NSString *deviceTokenString = [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])];
}
```
143. URL 中的 `?` 是保留字符,所以在判断 URL 中最后一位是不是 `?` 没有必要,直接看当前 URL 是否包含 `?`,即可判断。
144. 通过 `NSStringFromSelector` 来获取方法选择器名字。
145. 使用 `Asset Catalog` 管理资源图片,其中添加的 2x 图和 3x 图会在提交 app store 时被创建成不同的变体以减小 App 安装包的大小,用户下载 app 时会拉取到不同的变体文件文件。
146. 可以使用 AppCode 的 `inspect Code` 选项来初步检查出无用的类和方法,但注意会有一些问题。
147. 本地的 @2x 和 @3x 转成 webp 以后,调用的时候是否要判断设备分辨率,根据不同的设备分辨率调用不同倍数的 webp。
148. 在多个 Block 中使用 `__block` 变量时,因为最先会将所有的 Block 配置在栈上,所以 `__block` 变量也会配置在栈上。在任何一个 Block 从栈复制到堆时,`__block` 变量也会一并从栈复制到堆并被该 Block 所持有。当剩下的 Block 从栈复制到堆时,被复制的 Block 持有 `__block` 变量,并增加 `__block` 变量的引用计数。
149. 什么时候栈上的 Block 会被复制到堆上呢?
* 调用 Block 的 `copy` 方法;
* 将 Block 作为函数返回值;
* 将 Block 赋值给附有 __strong 修饰符 `id` 类型的类或 Block 类型成员变量时;
* 方法名中含有 `usingBlock` 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时。
150. 推荐调用 Block 的 `copy` 方法
* 将 Block 作为函数返回值;
* 将 Block 赋值给附有 __strong 修饰符 `id` 类型的类或 Block 类型成员变量时;
* 方法名中含有 `usingBlock` 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时。
151. 当监控系统内存的县城发现某 App 内存有压力,发出通知,内存有压力的 App 就会去执行对于的代理,也就是 `didReceiveMemoryWarning` 代理。通过这个代理,可以获得最后一个编写逻辑代码释放内存的机会。这段代码的执行,有可能会避免 App 被系统强杀。
152. iOS 系统内核里有一个数组,用于维护线程的优先级。这个优先级规定就是:内核用线程的优先级是最高的,操作系统的优先级其次,App 的优先级排在最后。且前台 App 的优先级高于后台 App。线程使用优先级时,CPU 占用多的线程的优先级会被降低。
153. 系统因为内存占用原因强杀 App 前,至少有 6s 的时间可以用来做优先级判断,`JetSamEvent` 日志在这段时间内生成。
* 在收到内存警告时,如何获取当前 app 的内存?
```objc
struct mach_task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kl = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
```
```objc
float used_mem = info.resident_size;
NSLog(@" 使用了 %f MB 内存 ", used_mem / 1024.0f / 1024.0f)
```
154. NSLog 实际上是一个 C 函数。它的作用是输出信息到标准的 Error 控制台和系统日志中,在内部实现上,实际上是用 ASL (Apple System Logger)的 API,将日志消息直接存储在磁盘上。
155. ARC 有效时,`id` 类型以及对象类型变量必定附加所有权修饰符,却省为 `__strong`。
156. 推荐通过 `copy` 方法来持有 block,把 block 从栈上复制到堆上。
157. **ARC 无效时**,`__block` 说明符被用来避免 Block 中的循环引用。因为 Block 从栈复制到堆时,不会被 `retain`,反之会被 `retain`。
158. `NSURLConnection` 发起请求后,所在的线程需要一直存活,以等待接收 `NSURLConnectionDelegate` 回调方法,但是网络返回的时间不确定,所以这个线程需要一直常驻内存中。
159. 线程保活:
* 使用 `NSRunLoop`
* `runUntilDate:`
* `runMode:beforeDate`
160. 在进行数据读写操作时,需要一段时间来等待磁盘响应,如果此时通过 GCD 发起一个任务,GCD 就会本着最大化利用 CPU 原则,会在等待磁盘响应这个空档,再创建一个新线程来保证能够充分利用 CPU。
161. 类似数据库这种需要频繁读写磁盘操作的任务,尽量使用串行队列来管理,避免因为多线程并发而出现内存问题。
162. 创建线程引发内存问题:
* 创建线程的过程需要用到物理内存,CPU 也会消耗时间
* 创建一个线程,系统需要为这个进程空间分配一定的内存作为线程堆栈。堆栈大小时 4KB,在 iOS 中主线程堆栈大小时 1MB,新创建的子线程堆栈大小是 512KB。
* 线程创建多了,CPU 在切换线程上下文时,还会更新寄存器,更新寄存器的时候需要寻址,而寻址的过程还会有较大的 CPU 消耗。
163. 除了加锁还有什么其它方法能够保证数据线程安全?
* 串行队列
164. 判断耗电量可以从 CPU 使用量入手。
165. `dispatch_after` 函数并不是在指定时间后执行处理,而只是在指定时间追加处理到 `Dispatch Queue`。
166. 通过 `Dispatch Group` 可以统一管理 GCD,在其中各个 GCD 执行完后处理或者设置等待时间。
167. 如果想提高文件读取速度,可以尝试使用 `Dispatch I/O`。
168. 使用 `CADisplayLink` 的获取屏幕刷新的方法
```swift
import UIKit
import QuartzCore
class ViewController: UIViewController {
var index = 0
override func viewDidLoad() {
super.viewDidLoad()
let displayLink = CADisplayLink(target: self, selector: #selector(screenUpdate(_:)))
displayLink.add(to: .main, forMode: .common)
}
@objc
func screenUpdate(_ displayLink: CADisplayLink) {
index += 1
print(index)
}
}
```
168. 多播代理
* https://juejin.im/post/5bd6842f6fb9a05d0045f925
* https://www.jianshu.com/p/8f2b9d6b9c85
* http://saitjr.com/ios/design-a-singleton-block-callback.html?utm_source=tuicool&utm_medium=referral
* https://cloud.tencent.com/developer/article/1446558
* NSHashTable
* GCD 信号量:dispatch_semaphore_t
* 多播确实能够解决一些一对多订阅的问题
* 可以在 `prepareForReuse` 方法中做一些重用前的操作。
* 苹果用 `NS_ASSUME_NONNULL_BEGIN`,`NS_ASSUME_NONNULL_END` 这两个宏来统一给属性和方法参数和返回值加上`nonnull` 修饰,`NS_ASSUME_NONNULL_BEGIN` 和 `NS_ASSUME_NONNULL_END` 之间,定义的所有对象属性和方法默认都是 `nonnull`。
================================================
FILE: iOS/Objective-C/ping.md
================================================
**这篇文章是我在项目中需要判断内外网环境根据网上的资料及自己的改造所得结果,有些不足之处望指出。**
使用`ping`命令来检测数据包(ICMP,Internet Control Message Protocol,互联网控制报文协议)能够通过IP协议到达特定主机,并收到主机的应答,以检查网络是否连通和网络连接速度,帮助我们分析和判定网络故障。因为互联网操作是路由器严密监控的。当路由器端处理报文时若有意外发生,事件通过ICMP报告给发送端。
SimplePing是Appl给开发者提供的一套封装了底层`BSD Sockets ping`函数的类,SimplePing下载地址:[https://developer.apple.com/library/content/samplecode/SimplePing/Introduction/Intro.html](https://developer.apple.com/library/content/samplecode/SimplePing/Introduction/Intro.html)
下面我们一一介绍 SimplePing 类的各个属性、方法以及`delegate`回调方法的含义及作用。
### 初始化方法
```ObjC
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithHostName:(NSString *)hostName NS_DESIGNATED_INITIALIZER;
```
SimplePing中,禁用了`init`方法,只提供`initWithHostName:`这个指定构造方法,它可以用于初始化一个`ping`指定的主机实例对象。其中`hostName`参数可以是主机的`DNS`域名,或者是`IPv4/IPv6`地址的字符串形式。
```Objc
@property (nonatomic, copy, readonly) NSString * hostName;
```
hostName:只读,保存由初始化方法`initWithHostName:`传入的`ping`操作要连接的主机域名或`IP`地址。
```ObjC
@property (nonatomic, assign, readwrite) SimplePingAddressStyle addressStyle;
```
addressStyle:主机的`IP`地址类型,如`IPv4/IPv6`等,其中`SimplePingAddressStyle`枚举类型的定义如下:
```ObjC
typedef NS_ENUM(NSInteger, SimplePingAddressStyle) {
SimplePingAddressStyleAny, // IPv4 或 IPv6
SimplePingAddressStyleICMPv4, // IPv4
SimplePingAddressStyleICMPv6 // IPv6
};
```
```ObjC
@property (nonatomic, copy, readonly, nullable) NSData * hostAddress;
```
hostAddress:只读,在`start`方法调用之后,根据`hostName`得到的要`ping`的主机的`IP`地址,它是`struct sockaddr`形式的`NSData`数据。当`SimplePing`实例处于`stopped`状态,或者实例调用了`start`方法,但在`simplePing:didStartWithAddress:`方法被调用之前,hostAddress 的值都是`nil`。
```ObjC
@property (nonatomic, assign, readonly) sa_family_t hostAddressFamily;
```
hostAddressFamily:只读,`hostAddress`的地址族,如果`hostAddress`为`nil`,则其值为:`AF_UNSPEC`。
```ObjC
@property (nonatomic, assign, readonly) uint16_t identifier;
```
identifier:只读,当创建一个`SimplePing`实例对象时,会自动生成一个的随机的标识符,用来唯一标识当前`ping`对象。
```ObjC
@property (nonatomic, assign, readonly) uint16_t nextSequenceNumber;
```
nextSequenceNumber:只读,`ping`每发送一次数据包都会有一个对应的序列号`sequence number`,此值为下一次`ping`操作要发送数据时的序列号,从0开始递增,当`ping`成功发送一次数据到主机并收到应答时,该值+1。而对于本次`ping`的`sequence number`在成功发送数据(request)和成功接收到响应(response)的`delegate`回调方法里都会以方法参数返回,以便进行`ping`操作耗时的计算等等。
```ObjC
@property (nonatomic, weak, readwrite, nullable) id delegate;
```
delegate:当前对象的回调,`delegate`中的回调方法将在对象调用`start`方法所在的线程对应的`run loop`中以默认的`run loop model`执行。
### 实例方法:
```ObjC
- (void)start;
```
start 方法:开始一个`ping`操作,在调用此方法前,必须给`SimplePing`实例对象的`delegete`以及其他参数赋值。当`start`方法成功执行时,会回调`delegate`中的`simplePing:didStartWithAddress:`方法,在该回调方法里,就可以通过`sendPingWithData:`开始发送`ICMP`数据包,并等待接受主机应答的数据包。另外需要注意的是,当一个实例已经`started`,又一次调用此`start`方法会出错。
```ObjC
- (void)sendPingWithData:(nullable NSData *)data;
```
sendPingWithData: 方法:向主机发送特定格式的`ICMP`数据包,调用此方法前必须保证实例已经`started`并且要等待`simplePing:didStartWithAddress:`回调执行才能开始发送数据。参数`data`为要向主机发送的`ICMP`数据包,可以为`nil`,默认会发一个标准的`64 byte`数据包。
```ObjC
- (void)stop;
```
stop 方法:当结束要`ping`操作时,调用此方法。与`start`方法不同的是,当一个实例已经`stopped`,再次调用此方法也没事。
`delegate`回调方法:
`start`方法执行结果的回调:
```ObjC
// start 方法成功执行,可在此开始发送数据,其中 address 为主机的 IP 地址;
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address;
// start 方法执行失败,返回错误信息;
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error;
```
sendPingWithData: 方法执行结果的回调,每发送一次数据,都会同步地回调以下两个方法其中一个(除非你在发送途中调用了`stop`方法):
```ObjC
// 成功发送 ICMP 数据包到指定主机,在此传回已发送的数据包以及本次 ping 对应的序列号;
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;
// 发送数据失败,并返回错误信息,绝大部分原因由于 hostName 解析失败。另,当此方法调用时,ping 实例状态会自动转为stopped,不用再显示调用stop方法;
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error;
```
接收到主机返回应答数据的回调:
```ObjC
// 成功接收到主机回传的与之前发送相匹配的 ICMP 数据包;
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;
// 收到的未知的数据包。
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet;
```
注:以上回调方法中的 packet 数据包只包含了 ICMP header 和 sendPingWithData: 中传入的数据,但不包含任何 IP 层的 header。
封装了一个简单`SimplePing`类,因为只有一个类,就不开repo了:
```ObjC
//
// PJPingManager.h
// DiDiData
//
// Created by PJ on 2018/5/25.
// Copyright © 2018年 pjhubs All rights reserved.
//
#import
typedef void(^PingSuccessCallback)();
typedef void(^PingFailureCallback)();
@interface IPLPingManager : NSObject
@property (nonatomic, copy) PingSuccessCallback pingSuccessCallback;
@property (nonatomic, copy) PingFailureCallback pingFailureCallback;
- (void)startPing;
@end
```
```ObjC
//
// PJPingManager.m
// DiDiData
//
// Created by PJ on 2018/5/25.
// Copyright © 2018年 pjhubs All rights reserved.
//
#import "PJPingManager.h"
#import "SimplePing.h"
#include
@interface PJPingManager ()
@property (nonatomic, strong) SimplePing *pinger;
@property (nonatomic, strong) NSTimer *sendTimer;
@end
@implementation PJPingManager
- (instancetype)init {
self = [super init];
if (self) {
NSString *hostName = @"your hostName";
self.pinger = [[SimplePing alloc] initWithHostName:hostName];
self.pinger.addressStyle = SimplePingAddressStyleAny;
self.pinger.delegate = self;
}
return self;
}
- (void)startPing {
[self start];
}
- (void)start {
[self.pinger start];
}
- (void)stop {
[self.pinger stop];
self.pinger = nil;
if ([self.sendTimer isValid])
{
[self.sendTimer invalidate];
}
self.sendTimer = nil;
}
- (void)sendPing {
[self.pinger sendPingWithData:nil];
}
#pragma mark - pinger delegate
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address {
NSLog(@"pinging %@", [self displayAddressForAddress:address]);
[self sendPing];
}
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error {
NSLog(@"failed: %@", [self shortErrorFromError:error]);
[self stop];
if (self.pingFailureCallback) {
self.pingFailureCallback();
}
}
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber {
NSLog(@"#%u sent", (unsigned int) sequenceNumber);
}
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error {
NSLog(@"#%u send failed: %@", (unsigned int) sequenceNumber, [self shortErrorFromError:error]);
[self stop];
if (self.pingFailureCallback) {
self.pingFailureCallback();
}
}
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber {
NSLog(@"#%u received, size=%zu", (unsigned int) sequenceNumber, (size_t) packet.length);
[self stop];
if (self.pingSuccessCallback) {
self.pingSuccessCallback();
}
}
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet {
NSLog(@"unexpected packet, size=%zu", (size_t) packet.length);
[self stop];
if (self.pingSuccessCallback) {
self.pingSuccessCallback();
}
}
#pragma mark - Others mothods
/**
* 将ping接收的数据转换成ip地址
* @param address 接受的ping数据
*/
- (NSString *)displayAddressForAddress:(NSData *)address {
int err;
NSString *result;
char hostStr[NI_MAXHOST];
result = nil;
if (address != nil) {
err = getnameinfo([address bytes], (socklen_t)[address length], hostStr, sizeof(hostStr),
NULL, 0, NI_NUMERICHOST);
if (err == 0) {
result = [NSString stringWithCString:hostStr encoding:NSASCIIStringEncoding];
}
}
if (result == nil) {
result = @"?";
}
return result;
}
/*
* 解析错误数据并翻译
*/
- (NSString *)shortErrorFromError:(NSError *)error {
NSString *result;
NSNumber *failureNum;
int failure;
const char *failureStr;
result = nil;
// Handle DNS errors as a special case.
if ([[error domain] isEqual:(NSString *)kCFErrorDomainCFNetwork] &&
([error code] == kCFHostErrorUnknown)) {
failureNum = [[error userInfo] objectForKey:(id)kCFGetAddrInfoFailureKey];
if ([failureNum isKindOfClass:[NSNumber class]]) {
failure = [failureNum intValue];
if (failure != 0) {
failureStr = gai_strerror(failure);
if (failureStr != NULL) {
result = [NSString stringWithUTF8String:failureStr];
}
}
}
}
if (result == nil) {
result = [error localizedFailureReason];
}
if (result == nil) {
result = [error localizedDescription];
}
if (result == nil) {
result = [error description];
}
return result;
}
@end
```
给出的代码中使用到的`netdb`库为Unix和Linux特有的头文件,主要定义了与网络有关的结构、变量类型、宏、函数等。这里有篇在Unix中该函数库相关介绍:[http://pubs.opengroup.org/onlinepubs/7908799/xns/netdb.h.html](http://pubs.opengroup.org/onlinepubs/7908799/xns/netdb.h.html)
`NI_MAXHOST`给出主机字符串存储空间的最大长度,值为1025;
`NI_MAXSERV`给出服务字符串存储空间的最大长度,值为32.
其中还有一些会使用到的宏,如下所示:
宏|解释
--|--
#define NI_NOFQDN 0x00000001 | 只返回FQDN的主机名部分
#define NI_NUMERICHOST 0x00000002 | 以数串格式返回主机字符串
#define NI_NAMEREQD 0x00000004 | 若不能从地址解析出名字则返回错误
#define NI_NUMERICSERV 0x00000008 | 以数串格式返回服务字符串
#define NI_NUMERICSCOPE 0x00000100 | 以数串格式返回范围标识字符串
#define NI_DGRAM 0x00000010 | 数据报服务
使用方法:
1. 新建`ping`类,复制以上代码;
2. 创建一个`NSTimer`类型属性且初始化timer。`self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(pingNetWork) userInfo:nil repeats:YES];`。在这里为啥要初始化一个NSTimer呢?因为如果`ping`失败后,也就是发送的测试报文成功,但一直没收到响应的报文,此时却不会有任何的回调方法告知我们,而只`ping`一次结果也不准确,更何况`ping`花费的时间非常之短,故我在此加了个`NSTimer`多次进行`ping`,或者也可以使用`performSelector`进行延时判断,一般0.3~0.8s的延时即可。如果在这期间内未收到响应则可视为超时。
3. 对应的方法为:
```ObjC
- (void)pingNetWork{
self.pingManager = [[IPLPingManager alloc] init];
self.pingManager.pingSuccessCallback = ^{
};
self.pingManager.pingFailureCallback = ^{
};
[self.pingManager startPing];
}
```
跑起工程后每秒都会`ping`目标主机,如果你不并想每秒都执行一次,再自定义一个属性去标记吧。当然,你也可也完全不必使用我提供的这个封装,直接使用`SimplePing`自行编写也行。
================================================
FILE: iOS/Objective-C/runtime.md
================================================
# Runtime
我终于来啃这个大骨头了,从开始学 iOS 开发起,`runtime` 和 `runloop` 一直挥之不去,网络上的各种资料写的关于这两个方向的内容也十分庞大,弄得我一直不敢碰(梳理)。趁着最近时间较充裕,学习学习 `runtime` 中的相关知识并加入到实际项目中,需要注意的是**本篇文章部分内容来自网络**。
## runtime 是什么?
`runtime` 是 `Objective-C`(后文简称 OC ) 的主要一个特性,从学习并使用 OC 脑海中就一直在回响着“它是一门动态语言,动态语言”,而在 OC 中实现动态特性所采用的技术或者实现方案就是 `runtime` ,它基本上是 C 和汇编语言构成,目前有 Apple 和 GNU 分别维护的两套 `runtime` 版本,但这两个版本大体上无差别,都在尽力的保持着一致。在 OC 2.0 中采用的是最新版本的 `runtime` 。
动态语言,相较于我们所认为的静态语言(注意:这不是说的强弱类型语言)一言以蔽之:在程序运行的过程中可以改变其结构,例如新的函数可以被引进,已有的函数可以被删除,可以给类添加新的属性等在结构上的变化。总是想办法把一些决定工作从编译链接推迟到运行时,也就是说只有编译器是不行的,还需要一个运行时系统( runtime system )来执行编译后的代码,这就是 `runtime` 存在的意义,是整个 OC 运行框架的一个基石。
但是 `runtime` 也有缺点,混乱的运行时代码会改变运行在其架构之上的所有代码。
## runtime 可以用来做什么?
在 OC 中,我们可以利用 runtime 做以下但不限于此的事情:
* 利用关联对象为分类增加伪属性;
* 利用 `Methon Swizzling` 交换方法;
* 利用 class_copyIvarList 实现 NSCoding 的自动归档解析;
* 利用 objc_allocateClassPair 、objcct_setClass 等 API 来实现 KVO Block;
* 利用消息转发机制实现多播委托(蹦床模式);
* 字典模型之间的转换;
* 页面无侵入埋点;
* 监听 App 网络流量。
## 来玩玩 runtime 吧!
### 利用关联对象为分类增加伪属性
在项目的开发中,很多时候会遇到要为已经存在的类添加属性或方法,比如给 `NSString` 添加一个取当前时间戳的方法、给 `UINavigationBar` 添加自定义属性等。因为分类结构的特殊性,当我们需要做到给现有类添加属性时,分类并不会自动的为我们创建实例变量和存储方法,需要我们自己去实现存取方法:
通常推荐的做法是添加的属性最好为 `static char` 类型,当然最好是指针类型,通常来说该属性应该是常量、唯一的。
首先创建一个分类文件(下文不再赘述):
缺图:
1 2 3
在 `NSString+test.h` 文件中写下我们想要添加的属性:
```Objc
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSString (test)
@property (nonatomic, strong) NSString *title;
@end
NS_ASSUME_NONNULL_END
```
在 `NSString+test.m` 文件中写下对应的实现:
```Objc
#import "NSString+test.h"
// 注意引入 runtime.h
#import
@implementation NSString (test)
- (NSString *)title {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setTitle:(NSString *)title {
// 注意要根据添加的属性不同选择不同的修饰符,此时我添加的属性是 NSString 类型,故为:OBJC_ASSOCIATION_COPY_NONATOMIC
objc_setAssociatedObject(self, @selector(title), title, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
```
这样就完成了给已有类添加额外属性的工作,接下来在我们需要用到的地方这么使用;
```Swift
#import "ViewController.h"
// 注意导入对应分类文件
#import "NSString+test.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *string = [NSString new];
string.title = @"2333";
NSLog(@"%@", string.title);
}
@end
```
## 为什么使用关联对象?
在代码中有些地方不适合拓展多个属性进行设置,同时也不适合写太多的冗余代码,从设计模式上看,需要改造现有的类。而想要在不增加类属性的方法去做到一些事情,可以通过 `category` 加分类的方法,但是 `category` 中做不到直接声明一个新的 `@property` 属性,因为在 `category` 中并没有为我们生成实例变量以及存取方法,需要我们手动实现。
所以一般使用关联对象为已经存在的类添加「属性」。在分类中,因为**类的实例变量的布局**已经固定,使用 `@property` 已经无法向固定的布局中添加新的实例变量(这样做可能会覆盖子类的实例变量),所以我们要用关联对象以及两个方法来模拟构成属性的三个要素
### 思考和总结:
其实这应该从实际角度出发,还是得回到最开始遇到的问题上。我们需要的是对现有类在不增加额外的多余代码前提下,增加新的属性(或者其它的一些做法),那对于一个属性来说,其核心就是三点:
* 生成实例变量 `_property`
* 生成 `getter` 方法 `- property`
* 生成 `setter` 方法 `- setProperty`
所以,想办法通过 `runtime` 去完成就完事了。
如果我们把属性看作是「通过方法访问的实例变量」,那很明显在分类中因为类的实例变量布局已经固定,所以是不能添加新的属性的。但如果我们把属性看做是一个「存取方法以及存取值的容器的集合」,那这是 OK 的。因为分类中对属性的实习其实只是实现了一个看起来像属性的接口而已。实现细节可以看[这篇文章](https://draveness.me/ao)
### 使用 Runtime 可以完成的一些事情
在运行期间动态创建一个类 MyRuntimeClass,里面含有一个方法print,打印出 Hello World! [详情可见](http://southpeak.github.io/2014/10/25/objective-c-runtime-1/)
### 给某个类动态添加上某个方法
```objc
// 动态添加eat方法
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
```
### 小知识点
* OC 对象收到消息后,究竟会调用哪种方法需要在运行时才能解析出来
* 如果类无法立即响应某个选择子,那么就会立即启动消息转发流程
* 对象类型并非在编译期就绑定好了,而是要在运行期查找
* `Id` 类型能指代任意的 OC 对象类型,编译器假定它能响应所有消息
* 每个 OC 对象实例都是指向某块内存数据的指针
* OC 没有其它语言类似的命名空间机制
* 如果发生命名冲突,那么应用程序的链接过程就会出错,因为其中出现了重复符号
```shell
duplicate symbol …
```
================================================
FILE: iOS/Objective-C/tips-自定义tabBar大加号引发的思考.md
================================================
## 前言
最近在琢磨一些之前非常想玩的点,最让我感到有趣的是天朝才有的 tabBar 中间大加号,而且我还发现好像咸鱼的没做好,如果我们去点击咸鱼中间的加号上边部分,会发现其实是没有响应的,必须要点进 tabBar 里边才能被响应到。其实这个需求我自己之前也有遇到过,有些时候我会撸码撸眼花了,添加一个 button 在 bottomView 上后,y 坐标跑到了 superView 的顶部以上的区域,然后死活响应不了任何的点击事件,郁闷了许久后才发现原来是自己的坐标设置错了😑。
事后我就在想,为什么会出现响应不了的情况呢?明明已经 addSubview 到对应的视图中去了呀,在 iOS 中视图能够响应用户的触摸事件主要有以下几种情况:
1. 视图是不隐藏的:`self.hidden = NO`
2. 视图是允许交互的:`self.userinterfactionEnable = YES`
3. 视图透明度大于 `0.1:self.alpha > 0.01`
4. 视图包含这个点 `pointInside:withEvent: = YES`
而我之前犯的错误就在于第四点,添加上去的 button 所在父视图不包含这个点。如果想要解决我之前遇到的问题,那就要重写父视图的 hitTest:withEvent: 方法,并且还要在其中做坐标转化。
## 那,hitTest:withEvent: 方法是什么?
想要了解 `hitTest` 方法是做什么的,就得先了解该方法作用在哪个对象上, `Xcode` 的帮助文档是这么描述的:`Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.`,是说通过这个方法可以返回触摸点所触摸到的视图以及包含该视图的所有视图(直译过来真是太拗口了,我意译了),再简单一些来说,用我之前的例子,如果我现在点击了这个 `button` ,那么我在这个方法中就可以获取到包含这个 `button` 视图的所有父视图(包括爷爷视图、祖父视图)。在这个方法中返回的 `View` 是本次点击事件中需要的响应的 `View` ,也就是说,我们理论上可以在这个方法中返回任何想要响应该点击事件的视图。
其实到到这块问题已经解决得差不多了,我们只需要在 `hitTest:withEvent:` 方法中拦截到 `tabBar` 的用户点击事件,判断一下如果当前的 `point` 是落在了我们需要响应的视图上,就返回这个视图,否则都直接 `return [super ...]` ,让父视图自己去找最佳响应视图,如果父视图也找不到,那就丢给 `controller` 的 `view` ,如果 `controller` 的 `view` 也没找到,那就丢给 `controller` 所在的 `window` ,如果 `window` 也不处理,那就给 `UIApplication` ,到了 `UIApplication` 这一层真的还没人处理的话,那就直接丢了,不过我觉得应该没有这么极端的情况吧。
其实调用 `hitTest:withEvent` 方法时实际上是去调了 `pointInside:withEvent:` 这个方法注意(这两个方法都可以实现需求,为了方便调试,我就不删掉了),请看调用栈。
触发 `pointInside:withEvent:` 方法是在调用 `[super hitTest:withEvent]` 方法时,因为之前也说过了,当我们调用 `super` 方法实际上是让系统自己去找合适的响应视图,那系统是怎么找合适的响应视图?就是通过 `pointInside:withEvent:` 方法去寻找,如果我们不对 `pointInside:withEvent:` 方法的选择做干扰的话是按照系统的查找方法去做的。所以我们也可以直接在 `pointInside:withEvent:` 方法中做判断。
仔细一看,其实会发现 `hitTest:withEvent` 方法被调用了两次,是因为 `touch` 方法的 `begin` 和 `end` 所调用的,其实我们也就能看出一点,事件的传递其实是来源于触摸状态的变化(相当于没说😑,手机就是事件驱动的完全体)
OK,那问题就回到了如何判断 `point` 是否落在了我们需要响应的视图上,如果我们还是按照之前的想法去做,直接判断 `point` 的 `x` 和 `y` 是永远也得不到我们想要的结果,因为是按照系统坐标去计算的 `point` (不知道这么说大家能不能理解),我们需要做的就是把这个 `point` 坐标转化到某一个视图上,通过对这个视图做相对坐标来判断,而不是系统的绝对坐标。那 `iOS` 中如何进行坐标转换呢?系统中提供了这四个方法,这四个方法其实只需要弄懂其中的两个方法即可,其它的两个方法都是与之对应的。
```swift
public protocol UICoordinateSpace : NSObjectProtocol {
@available(iOS 8.0, *)
public func convert(_ point: CGPoint, to coordinateSpace: UICoordinateSpace) -> CGPoint
@available(iOS 8.0, *)
public func convert(_ point: CGPoint, from coordinateSpace: UICoordinateSpace) -> CGPoint
@available(iOS 8.0, *)
public func convert(_ rect: CGRect, to coordinateSpace: UICoordinateSpace) -> CGRect
@available(iOS 8.0, *)
public func convert(_ rect: CGRect, from coordinateSpace: UICoordinateSpace) -> CGRect
@available(iOS 8.0, *)
public var bounds: CGRect { get }
}
```
在这四个方法中,我觉得有点比较拗口的地方,但是只要理解好了 to 和 from 这两个介词就没关系了。
`public func convert(_ point: CGPoint, to coordinateSpace: UICoordinateSpace) -> CGPoint`
比如这个方法,注意是介词 to ,我们假设是这么调用
`self.convert(point, to: centerButton)`
文档是这么说的:“Converts a point from scene coordinates to view coordinates.”(这其实是 SpriteKit 的解释,不过都差不多),直译是“将一个点从场景坐标转换为视图坐标”,人话就是说:“把 point 这个点坐标从 self 视图坐标上转换成在 centerButton 视图坐标上”,也就是坐标原点从 self 视图的左上角变为了 centerButton 的左上角。经过这么一变换 point 坐标的值就约束在了 centerButton 的坐标系中,只要 point 坐标是在 centerButton 上就一定会被接收,当然这部分也需要做个 point(inside:with) 的判断。
另外一个方法,
`public func convert(_ point: CGPoint, from coordinateSpace: UICoordinateSpace) -> CGPoint`
这个方法就是上边 to 的反例,我是这么用的,
`centerButton.convert(point, from: self)`
翻译过来就是:“把来自 self 视图坐标系的 point 坐标转换成 centerButton 坐标系的坐标”。由上所诉,我们可以思考一下 `hitTest:withEvent` 的内部实现大概逻辑是怎么样的:
1. 先判断当前控件能否接受事件,满足这四个条件:可 `userInterfaceEnabled`、没有 `hidden` 、`alpha > 0.01` 、且点在当前控件内
2.从当前视图的根开始判断,举个例子:
用户当前的触摸点在紫色位置,首先 `A` 视图接收到了触摸事件(其实应该先是 `keyWindow` ),`A` 开始轮询自己的子视图,问到 `B` 中,问 `B` 在不在这个点在不在你里面,`B` 说在,`B` 开始轮询自己的子视图,问到了 `C` ,这个点在不在你里面,`C` 说在,`C` 开始轮询自己的子视图,`C` 发现除了自己没有子孙视图了,OK,那这个触摸事件就给 `C` 了,把 `C` 视图给 `return` 回去。所以大家可以发现,这其中实际上是从根视图一步步问下来的。为了更加详细的描述 `hitTest:withEvent` 方法的流程又画了张图做讲解(因为真的很有趣):
假设现在视图变成了这样,白圈代表用户触摸的位置,
`hitTest:withEvent` 所采用的是 逆前序深度遍历,也就是先访问根节点,然后从该树索引最大的子视图到最小的子视图进行遍历,也即从右到左进行遍历。默认最左子树的最左叶节点为 `0` 。
当遇到上图所示的,用户的触摸位置为两个视图的重叠之处时,根据该算法将得到最右子树中的最深视图,而该视图就是距离屏幕最近的视图(好像这么说不太对),如下图所示,经过计算,我们最终将得到 `B-a` 这个视图。原本还想写一写伪代码,但是发现好像上面的那段大白话解释得更清楚一些,而且本来这部分代码苹果并没有开源,大家都只是猜测。
总结一下,如果我们也想玩 `tabBar` 中间凸起的大按钮,按照正常逻辑去做,凸出的部分是肯定接收不到用户的触摸事件的。如果我们想要让凸出部分也接收到的用户的触摸部分,需要把用户在凸出的点击事件在 `pointInside:withEvent:` 或 `hitTest:withEvent` 进行拦截,在拦截的代码中做坐标的转换,判断是否用户的触摸点真的位于凸出位置即可。但是要注意,因为我们是直接自定义了一个 `tabBar` , 而且还是直接添加上了一个 `button` ,在 `push` 到下一个页面的时候,对应的凸出的区域还是能点击的,因为 `tabBar` 虽然按照我们最初的想法是直接 `hidden` ,但是生命周期还在,所以触摸区域也生效。
## 再深挖一下
再往下挖一下,`iPhone` 是怎么知道用户的点击事件的呢?用户在屏幕上这个物理的点击事件是怎么传递到我们对应的 `App` 中去的呢?
`CPU` 处于睡眠状态,等待事件驱动(直接理解为待机 好了 😑)
↓↓↓↓↓
用户手指触摸屏幕
↓↓↓↓↓
屏幕收到触摸事件的响应,将该响应事件传递给 `IOKit `
↓↓↓↓↓↓
`IOKit` 把该触摸事件封装成了 `IOHIDEvent` 对象(这其中包括了所有的基本信息比如几根手指、时间戳等等信息,但基本上都是 `data` ,详见苹果代码 https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-308/IOHIDFamily/IOHIDEvent.h.auto.html)。该对象由 `IOHIDService` 生成,与之对等的还有 `IOHIDDiplays`
↓↓↓↓↓
`IOKit` 通过 `IPC` 将对象转发给 `SpringBoard.app` ,这个就是系统桌面进程了,它接收物理按键、触摸、加速、传感器等等 `Event` ,记得之前有人在越狱的 `iPhone` 上做了各种高大上的桌面,就是通过替换了这个进程,不过建议大家不要自己玩,`SpringBoard.app` 进程是唯一一个不能通过 `Home` 键和电源键进行开关机重启的。实际上严格来说应该是 `IOKit` 把这个对象发给了 `UIKit` ,`UIKit` 判断出了给它的是 `IOHIDEvent` 类型的对象,判断出了应该交由 `SpringBoard.app` 进行处理,最后才通过 `mach port`(进程端口)转发给 `SpringBoard.app` 。其实也就是说,不管用户在屏幕上做了什么操作,比如刷微博啦、发短信啦等等这些操作,也只有到了 `SpringBoard.app` 这一层才能进行才处理,之前进行的操作都是判断、封装和转发。说明一下:`mach port` 是 `iOS` 中多进程的一种方式,其它还有 `Distributed Notification` 、`Distributed Objects` , `XPC` 等等方法
↓↓↓↓↓
`SpringBoard.app` 也就是桌面的主线程 `RunLoop` 收到 `IOKit` 转发来的消息后 `awake` ,并触发对应的 `mach port` 进行 `Source1` 回调, `__IOHIDEventSystemClientQueueCallback()`。`RunLoop` 的 `Source1` 回调,该回调由 `RunLoop` 和内核管理,`mach port` 驱动,如 `CFMachPort`(非常类似 `pipe` ,Mac下的 `NSPort` )、`CFMessagePort`。
↓↓↓↓↓
如果 `SpringBoard.app` 监测到如果有 `app` 在前台运行(假设为 `pjhubs.app` ),`SpringBoard.app` 通过 `mach port` 转发给 `pjhubs.app` 。如果监测到前台有 `app` 在运行,则 `SpringBoard.app` 将触发 `Source0` 回调进入 `App` 内部响应阶段。`Source0` 回调处理 `App` 内部事件、`App` 自己负责管理(触发),如 `UIEvent`、`CFSocket` 等。
↓↓↓↓↓
`App` 主线程 `RunLoop` 接收到 `SpringBoard.app` 转发来的消息 `awake` ,注意这里 `App` `RunLoop` 不是 `SpringBoard.app` 的 `RunLoop` ,每一个进程都有自己主线程 `RunLoop` ,互不影响(其它平台都有的,只不过叫法不一样)。`awake` 后触发对应的 `mach port` 的 `source1` 回调 `__IOHIDEventSystemClientQueueCallback()`
↓↓↓↓↓
`Source1` 回调内部触发了 `Source0` 回调 `__UIApplicationHandleEventQueue()` 。
↓↓↓↓↓
`Source0` 内部回调,把 `App` 之前接收到的 `IOHIDEvent` 对象重新封装成了 `UIEvent` 对象(能够拿到事件类型和产生时间),一个事件将产生一个 `UIEvent` 对象,举个例子,如果用户的两个手指同时触摸在 `View` 上,那么 `View` 只调用一次 `touchBegin` 方法,但是 `allTouches` 参数将返回两个 `UITouch` 对象;如果用户是一前一后分别触摸 `View` 上,那么将分别调用两次 `touchBegin` ,但是用户两次都触摸到了同一个点上,也就是双击,这将只会创建一个 `UITouch` 对象,并且在第二次触摸时更新之前的 `UITouch` 对象 `tapCount` 属性进行加一操作。 `allTouches` 属性将返回一个 `UITouch` 对象,这个对象就是用于描述 `app` 和单个用户交互的对象。`UIEvent` 的事件类型,苹果归类成了三种:触摸事件、加速计事件和远程遥控事件。
↓↓↓↓↓↓
`Source0` 回调 内部去调用 `UIApplication` 对象的 `sendEvent:`方法,将 `UIEvent` 对象传给 `UIWindow` ,丢给 `UIWindow` 后,接下来的事情就是上文中我们已经做过的了。一般来说触摸事件添加到 `UIApplication` 的事件管理队列中(注意不是栈,因为先产生的事件要先处理,要不然就乱了)后,`UIApplication` 会在事件的最前端取出事件,然后分发下去,通常会先给 `keyWindow` 进行处理,通过 `keyWindow` 进行一层层的往下分发,流程可以概括为:`UIApplication` → `UIWindow` → `SuperView` → `SubView`
↓↓↓↓↓
通过递归调用 `UIView` 层级的 `hitTest:with:`和 `point ( inside:with: )`方法找到 `UIEvent` 中每个 `UITouch` 所属的 `UIView` ,因为一个 `UIEvent` 事件上是拥有多个 `touchs` 的(毕竟多点触控嘛),找到这个所属的 `UIView` ,实际上也是想找到距离触摸点最近的 `UIView` 。寻找最适合的 `UIView` 过程是从每个 `UIView` 的最顶层往最底层去找的,这和 `UIResponder` 响应链的过程不同,事件响应是 `UIEvent` 中的每个 `UITouch` 所属的 `UIView` 都确定后才开始的。
↓↓↓↓↓
以上就是触摸事件的基本流程,也就是找到了最合适的 `UITouch` 进行接收 `UIEvent` 。那 `UITouch` 如何找到合适的 `gestureRecognizers` 呢?系统会围绕 `UITouch` 所属的 `UIView` 及其祖先 `UIView` 的 `gesturerecognizer` 来确定一个 `UITouch` 的最佳 `gesturerecognizer` 。再次说明,从这一步开始的之前步骤,都是在找用户触摸点的最佳响应视图,也就是找到了用户触摸的视图是那个,但是并没法响应用户进行触摸操作,比如点击、双击、滑动等等,这就需要让 `UITouch` 再进行寻找最佳的 `UIResponder` 对象进行处理。注意,在该例子中,我并未对各个视图添加手势识别器,如果我们对各个视图添加了手势识别器,因为手势识别器比 `UIResponder` 具有更高的事件响应优先级,所以将会先把该事件传递给手势识别器,举个例子:在一个 `view` 上添加了 `tap` 手势识别器,并且还添加了一个 `tableView` ,此时当我们点击 `cell` 时是不会进行任何操作的,因为 `view` 已经把触摸事件给截获了。如果手势识别器没有进行对该次事件进行接收,才会被选中的最佳 `hitTest View` 进行捕获。再提示一下:手势实际上分为了两种,其一为离散型手势,包括点按、轻扫手势,其二为连续性手势,除了离散型手势外剩下的手势全都是,比如旋转、放大等等
↓↓↓↓↓
`UITouch` 所属的 `UIView` 和 `gesturerecognizer` 收到对应的 `UITouch` 和 `UIEvent` 对象后,按照 `UITouch` 目前所处的状态进行分别调用 `touchBeng` 、`touchMoved` 、`touchEnded` 、`touchCancled` 四大方法中的一个,也就是开始了正式的事件响应。这四大响应事件会按照 `UIResponder` 响应链一直从下往上传递,直到某个 `UIResponder` 因为主动响应了触摸事件而结束,切断了响应链,如果这一条响应链找上去都没有对其进行响应的对象,那最终会被 `UIApplication` 对象直接杀掉。也就是说,如果我们在 `scrollView` 中放了 `UImageView` 和 `UIButton` ,从 `UIImageView` 出发的滑动事件不会被其本身响应,而是被传递到了 `scrollView` 中,但是如果我们是从 `UIButton` 出发的事件,`UIButton` 首先会进行拦截判断,如果是点击事件,自己就接收进行处理,如果不是,例如滑动事件,那就丢掉,让该事件继续沿着响应链向上找到 `scrollView` 进行处理。还有一个时期需要注意,如果我们开启了并发手势,也就是允许多个手势同时执行,各自对应的 `gesturerecognizer` 方法直接拦截跟自己手势相同的 `UITouch` 对象,剩余的 `UITouch` 对象 `gesturerecognizer` 处理不了就会让它跟着响应链继续往上走。需要注意的是,每个事件响应者必定是 `UIResponder` 对象,且通过这四大方法进行响应触摸事件,并且每个 `UIResponder` 对象都已经实现了这四个方法,不过并没有对这四个方法做任何处理,如果我们需要做一些额外的操作,需要重写这四大方法。
↓↓↓↓↓
到这个环节,`UITouch` 对象的所有相关事件通通流动完毕,`CPU` 继续进行等待状态。
总结一下,触摸发生时,系统内核生成触摸事件,先由 `IOKit` 处理封装成 `IOHIDEvent` 对象,通过 `IPC` 传递给系统进程 `SpringBoard` ,而后再传递给前台 `APP` 处理。事件传递到 `APP` 内部时被封装成开发者可见的 `UIEvent` 对象,先经过 `hit-testing` 寻找第一响应者,而后由 `Window` 对象将事件传递给 `hit-tested view` ,并开始在响应链上的传递。一句话说就是从用户触摸到屏幕再到 `app` 做出处理,大概要经过三个阶段,系统处理阶段 → `SpringBoard.app` 处理阶段 → `App` 内部处理阶段,在开发中几乎我们也只在第三阶段搞事情。而且还要注意,在 `iOS` 中不是任何对象都能够去处理对象,而是只有继承了 `UIResponder` 的对象才能接收并处理事件,也就是我们所说的响应者对象,比如我们常见的: `UIApplication` 、`UIViewController` 、`UIView` 等等
================================================
FILE: iOS/Objective-C/并发编程.md
================================================
## 简介
明确几个概念:
* **进程**:进程是资源拥有单位,同一个进程内的线程共享进程里的资源。
* **线程**:线程是资源分配的最小单元,CPU调度的基本
* **多线程** 多线程是针对单核CPU设计的,目的是是为了让CPU快速的在多个线程之间进行调度。使用多线程可以提高程序执行效率,但开启线程需要一定的内存空间。
* **同步和异步**:同步和异步决定了是否要开启新的线程。同步,在当前的线程中执行任务,不具备开启新现场能力;异步:在新线程中执行任务,具备开启新现场的能力。
* **并行与串行**:决定了任务的执行方式。并行:多个任何并发(同时)执行。串行:一个任务执行完毕后,再执行下一个任务。
* **iOS中只有一个主线程,即UI线程。** 不可将耗时任何放在主线程执行,否则会引起卡顿。
## 分类
### NSTread
直接操作线程对象,需手动管理生命周期,经常使用其来查看当前线程。
### GCD(Grand Central Dispatch)
底层采用C语言编写,较为灵活,可根据徐彤负荷增减线程,性能较好。
### Cocoa NSperation
对GCD的封装,封装对NSOperation,添加到`NSOperationQueue`对象中,注意此Queue非彼Queue,更像是一个`Pool`。
## 死锁
### 串行队列
串行异步中嵌套同步:
```Swift
print(1)
serialQueue.async {
print(2)
serialQueue.sync {
print(3)
}
print(4)
}
print(5)
```
以上代码只会打印出 12 ,当执行完 `print(2)` 完后,把 `print(3)` 加入当前串行队列(主队列)中进行执行,但是 `print(3)` 想要执行就得让主队列中的上一个任务执行完毕,但现在上一个任务正在等待 `print(3)` 这个任务执行完,双方都进入列等待状态,死锁产生。
如果是串行同步中嵌套异步,如下代码所示:
```Swift
print(1)
serialQueue.sync {
print(2)
serialQueue.async {
print(3)
}
print(4)
}
print(5)
```
这样可以打印出 12345 或 12435 。在此的同步操作相当于是“插队”完成任务,第二段代码中,当执行到 `print(2)` 时,同步代码会立即插入当前主队列中进行执行,剩下 `print(3)` 和 `print(4)` 进行 CPU 争抢,也就是 34 或 43 。但在第一段代码中,当执行到进入到异步代码中,执行 `print(2)` 完后,遇到的是同步操作,该操作会立即插入主队列中进行执行,但此时 `print(3)` 没法执行,因为上一步的异步操作代码还没执行完。**切记,不要在主线程中进行同步操作**
================================================
FILE: iOS/Objective-C/系统相关.md
================================================
# 系统相关
在这里记录了跟 Apple 相关操作系统中遇到的问题和记录。
### iOS中的内存区域划分,如下图所示:
### iOS中的推送相关,如下图所示:
### 程序的可执行文件类型:
* `ELF`:Linux下的可执行文件格式;
* `PE32/PE32+`:Windows下的可执行文件格式;
* `Mach-o`:Mac和iOS下的可执行文件格式,比如可执行文件、库文件、dsym文件、动态库、动态链接器
### 代码编译要经过的几个器件(C语言):
* 预处理器:将.c 文件转化成 .i文件,使用的gcc命令是:gcc –E,对应于预处理命令cpp;
* 编译器:将.c/.h文件转换成.s文件,使用的gcc命令是:gcc –S,对应于编译命令 cc –S;
* 汇编器:将.s 文件转化成 .o文件,使用的gcc 命令是:gcc –c,对应于汇编命令是 as;
* 链接器:将.o文件转化成可执行程序,使用的gcc 命令是: gcc,对应于链接命令是 ld;
* 加载器:将可执行程序加载到内存并进行执行,loader和ld-linux.so。
### NSTimer
```obj
// 该方法默认是添加到 NSDefaultRunLoopMode
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// 该方法需要手动添加到的NSRunLoop中
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// 添加方法
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
```
* NSDefaultRunLoopMode :默认mode,当UIScrollView拖动时会影响。点击事件、普通回调事件、不滑动,是个默认状态、空闲状态,程序启动后,自动被切到这个 mode
* UITrackingRunLoopMode: 界面跟踪mode,用于scrollView跟踪触摸滑动,保证界面滑动时不受其它Mode影响
* UIInitializationRunLoopMode: 启动App时进入的第一个mode,启动完成后就不再使用。程序启动之后,私有,当出现第一个页面时就被切了。
* GSEventReceiveRunLoopMode:Graphic相关事件、接收系统事件内部的mode,通常用不到
* NSRunLoopCommonModes: 占位用的mode,不是真的mode,包含第一个和第二个。
scheduledTimerWithTimeInterval 使用该方法创建出来的timer默认加入到当前线程的RunLoop中,且模式为 NSDefaultRunLoopMode,如果当线程是主线程(UI线程),某些UI事件比如UIScrollView的拖动操作,会将Run Loop 切换成 NSEventTrackingRunLoopMode模式,在这个过程中,创建出来的timer的注册的事件是不会被执行的。需要把创建出来的 timer 使用 NSRunLoopCommonModes 模式添加到 Run Loop中,这个模式等效于 NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。
### NSCoding 协议和 runtime 的结合使用
iOS开发之NSCoding协议(使用runtime):https://www.jianshu.com/p/b33bdbccfa57。正常对用NSCoding写数据持久化,如果该对象的属性较少直接开干没啥问题,但是如果是一个多属性的对象,那就得写死人了。直接用runtime的特性去遍历出当前对象的所有属性。
### 终结掉 self.var 和 _var 的纠结
`property = _var + setter + getter`。`self` 类似 `java` 中的 `this` 和 `C++` 中的 `self` ,`OC` 中 `self` 同样也具备相同的职能,只不过因为 `property` 机制的存在,事情并且不简单。
实例变量(成员变量)具有私有性,一般情况下只在类内部使用,为了方便给外界读写这个实例变量,就有了属性(`@property`),编译器会自动的(`@synthesize var = _var`)为我们生成对应的一个以下划线加属性名命名的实例变量、 `setter` 和 `getter` 方法,并且 `self.var` 会让 `var` 计数器 + 1,而 `_var` 不会。需要注意在 `var` 的 `getter` 方法中,我们必须要使用 `_var` 进行访问,如果还是 `self.var` 则会进入死循环,但是在 `setter` 中如果还是使用 `self.var` 则也会触发 `setter` 。懒加载中也需要用 `_var` 来访问实例变量。一句话就是,“获取 `var` 只能 `_var` ,赋值 `var` 可以 `_var` 和 `self.var`”。如果我们同时对 `var` 写了 `setter` 和 `getter` 方法,`@property` 机制将不会生效,我们要自己声明 `_var`
使用 `readonly` 关键字修饰后,编译器只会为我们生成 `getter`。如果一个属性被 `@dynamic` 修饰,则编译器不会为其自动生成对应的 `setter` 和 `getter`,如果没有我们没有自行编写 `getter` 和 `setter` ,将不会在编译器得到提醒,在程序运行时将会发生 `crash` 。我们还可以使用 `@dynamic` 替换到某个类中本来就存在的 `property`
在 `Swift` 中没有 `@property` 类似的机制,属性是否对外部可见通过 `private` 关键词进行决定,Swift 中的属性分为计算属性和存储属性。计算属性不直接存储值,而是通过 `getter` 和 `setter` 方法间接访问其它属性,类似 `OC` 中的属性。存储属性充当存储值的角色,可直接被外部访问,类似 `OC` 中的实例变量。
### UIView 和 CALayer 的区别
* **UIView 和 CALayer 一样都是 UI 操作的对象** :两者都是 `NSObject` 的子类,发生在 UIView 上的操作本质上也发生在 CALayer 上。
* **UIView 是 CALayer 用于交互的对象** :UIView 是 UIResponder 的子类( UIResponder 是 NSObject 的子类),UIView 提供了很多 CALayer 交互上所没有的接口,其主要负责处理用户触发的各种操作。
* **CALayer 在图像和动画上渲染性能更好** :正是因为 UIView 有冗余的交互接口,而且相比 CALayer 还有层级之分,而 CALayer 在无须处理交互时进行渲染,可以节省大量时间。
### layeroutIfNeeded ,layoutSubviews 和 setNeedsLayout 的区别
* **`layeroutIfNeeded`** : 该方法一旦被调用,主线程会立即强制重新布局。它从当前视图开始,一直到完成所有子视图的布局;
* **`layoutSubviews`** :该方法用于自定义视图尺寸。系统调用,开发者只能重写该方法,让系统在调整尺寸时能够按照开发者希望的效果进行布局。该方法主要用在屏幕旋转、滑动或触摸界面、修改子视图时被触发。
* **`setNeedsLayout`** :与 `layoutSubviews` 方法作用类似,不同的是它不会立刻强制视图重新布局,而是在下一个布局周期才会触发更新。该方法主要用于多个视图布局先后更新的场景下。比如,在两个位置不断变化的点之间连一条线,该条线的布局就可以调用 `setNeedsLayout` 方法。
## 适配 iPad
### `UIAlertController` crash
详见:[https://stackoverflow.com/questions/31577140/uialertcontroller-is-crashed-ipad](https://stackoverflow.com/questions/31577140/uialertcontroller-is-crashed-ipad)
## 打开第三方 app 及被第三方 app 打开
如果能够保证这个 scheme 一定是存在的,可以直接执行:
```Objc
[[UIApplication sharedApplication] openURL:request.URL
options:@{}
completionHandler:nil];
```
如果不能保证,以免其它特殊问题(我没遇到过),可以先 `canOpenURL` 判断一下:
```Objc
BOOL result = [[UIApplication sharedApplication] canOpenURL:request.URL];
if(result) {
[[UIApplication sharedApplication] openURL:request.URL
options:@{}
completionHandler:nil];
} else {
// 是否需要提示?
}
```
执行 `canOpenURL` 方法,记得现在 `target` -> `Info` -> `Custom iOS Target Properties` 中添加字段 `LSApplicationQueriesSchemes`,类型为 `Array`,在 `item` 中添加对应需要打开的 app scheme 即可。
在 URL Types 中写的 scheme 指的是被其它 app 回调我方 app 时的标识符。
### `removeFromSuperView` 删除 `View`
我们经常会使用 `[UIView removeFromSuperView]` 方法把一个 `UIView` 及其子类(下文称 `ViewA`)从当前视图中进行移除,当我们执行这个操作时,潜意识里是想把这个视图彻底从当前页面上进行移除,但是这个方法只能保证把 `ViewA` 从当前视图上进行移除,并不能保证该 `ViewA` 真的被移除”,内存中还是存在 `ViewA` 。
还需要执行 `ViewA = nil`
### 如何修改 `UIAlertAction` 的文字颜色
`applyAction.setValue(UIColor.lightGray, forKey: "_titleTextColor")`
### 关于使用友盟推送收不到 push 消息的原因之一
可以使用友盟后台进行“推送测试”,如果是自建了集成友盟推送 SDK 的后台,进行“推送测试”的话,需要把 app 进行构建,通过安装并使用构建出来的 `ipa` 包才能够收到自建集成了友盟推送 SDK 的后台推送。
注意:测试设备例如 iPhone、iPad 等设备请登陆 **Apple ID**,否则 app 中集成友盟推送 SDK 将无法对该测试设备进行 `device Token` 的生成,也即无法推送至友盟中心。
### 什么是元编程
元编程。一件事情通过正常编程去做到是「正常编程」,元编程是通过编程的方法做到其他需要编程的事情,可以理解为「元编程」的本质上在做一个新的 DSL。`@property` 就是一个「元编程」思路。
### 统计启动耗时。统计启动到初始化结束的耗时,需要的是对关键业务的统计。
### 自旋锁
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该现场将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会推出循环。
存在的问题:如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗 CPU。
优点:自旋锁不会使线程状态发生切换,其会一直处于用户态,线程一直都是 active 的,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。非自旋锁在获取不到锁时会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换,因为线程被阻塞后便进入「内核调度状态」,会导致系统在用户态和内核态之间来回切换。
可查看:https://zhuanlan.zhihu.com/p/40729293
### CocoaPods 都做了什么?
首先明确一点,pods 是一个依赖管理工具,能够解析出用在 `Podfile` 文件中的配置信息,解析配置信息的过程不是特别复杂, 但解析的逻辑代码量一点都不小,考虑了很多的 case,尤其是依赖解析部分,自建了一个依赖图算法完成各个依赖库之间的重复依赖关系。
pods 是基于 `ruby` 的一个依赖管理工具,虽然 `Podfile` 写起来给人感觉一点都不像是写代码,而是在写一个描述文档,这是因为 `ruby` 的方法调用可以不写方法调用时的左右括号,而且也支持 block 的调用(与 OC block 不一致)。解析出依赖库后,会调用一个下载器,下载器会根据当前的执行命令是 `pod install` 还是 `pod update` 来决定下载器的入参。[详情可见](https://github.com/draveness/analyze/blob/master/contents/CocoaPods/CocoaPods%20都做了什么?.md)
### 如何造一个 Router
看了头条的 Router 实现大概流程,在开始的做法是通过 `.strings` 文件填写好 URL 做好映射,再通过 OC 的反射机制把字符串反射为类,现在虽然已经修改为了不需要引入配置文件,之前在应用初始化的时候就能初始化好了 Router,但实际上的做法还是基于反射。
个人认为 router 的好处在于能够应对多个 type 的跳转。
### 打包过程
* 源代码编译
* 静态库链接
* 资源编译、优化、导入
* 配置文件生成
* 签名打包
### 编译
编译器前端部分主要负责把「编程语言」翻译成「平台无关语言」,编译器后端主要作用是把「平台无关语言」翻译成「平台相关语言」

### LLVM 架构

### Clang
* 基于 LLVM 的编译器前端
* 基于 LLVM 的 C 语言编译工具集,兼容 GCC
### 预处理
* 引入头文件
* 预处理指令
* 去除注释
* 宏定义展开。`clang -E -fmodules test.m`,命令执行后效果:

### Lexer 词法解析
`clang -fsyntax-only -fmodules -Xclang -dump-tokens text.m`,命令执行效果:

### AST(抽象语法树)
`clang -fsyntax-only -fmodules -Xclang -ast-dump`

### iOS 不与磁盘交换
iOS 上没有「内存交换机制」,应用的性能不会逐步下滑,只会直接撞上南墙。,所以脏页面在 Apple 的移动设备上成本更高。无论使用多少。脏页面永远不会写入磁盘,所以 IOS 必须更积极地交换干净页面(可执行代码、映射文件)或终止进程。
### 内存警告的问题
发生「内存警告」时,其会在主线程上进行传递,如果 app 在主线程进入了忙碌状态时接收到了「内存警告」,那么 app 会被标记为无响应状态,然后就会被移除。
将内存分配操作移到后台也不行,当主线程在前台尝试处理「内存警告」时,后台线程还在大块大块的分配内存,此时 app 同样也会被移除。
#### 解决思路
在进行分配大量内存的后台线程即将进行工作时,先向主线程利用 `performSelector:onMainThread`(将 `waitUnitDone` 标志位设置为 `YES`) 发送一条 `ping` 消息。如果主线程在处理「内存警告」,那么这条语句会阻塞后台线程中的内存分配操作,并允许在恢复操作之前将内存释放掉。
### 栈比堆快
栈是计算机自动管理,有单独的存储区域。堆需要手动管理。[细节看这个](https://blog.csdn.net/maochengtao/article/details/8840690)
### CocoaPods 的本地开发模式
如果是多组件并行开发的情况下,先在 Podfile 中修改对应的组件 `:path` 为本地路径,开发完毕后,本地 `pod` 组件库更新,再修改 `Podfile` 为原先的 `Pod` 地址。
### 用户态与内核态的切换为什么会耗费资源
内核相当于是控制计算机硬件资源的「软件」,用户态里包括了 `shell` 等一系列应用程序,当应用程序需要调用系统硬件资源时,就跑到了内核态中。
因为这相当于涉及到了两个「软件」的切换使用,所以从用户态切换到内核态时,需要保持用户态下的当前软件环境中的各种信息,再去调用内核态中的各种资源,也就是系统调用。[详情可见这篇文章](https://blog.csdn.net/JH_Zhai/article/details/79861169)
### UNIX 中的文件本质上就是「字节流」
只不过添加了诸如名称、所有者、访问权限及访问日期之类的元数据。
### `UIDocument`
* 比 `NSFileManager` 更好用,提供了文档同步,异步性能优化等问题
* 需要被继承,实现方法。不能直接使用
* [http://swiftcafe.io/2015/11/14/uidocument/](http://swiftcafe.io/2015/11/14/uidocument/)
* [https://developer.apple.com/documentation/uikit/uidocument](https://developer.apple.com/documentation/uikit/uidocument)
### 设置图标上的角标数字
* `[UIApplication sharedApplication].applicationIconBadgeNumber`
### `Core Graphic` 有时候也被称为 `Quartz`
### 纯函数。相同的输入,永远相同的输出
* 纯函数是函数式编程的概念,必须遵守以下一些约束。
* 不得改写参数
* 不能调用系统 I/O 的API
* 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果
### 项目中如何做到防 crash
* 如何防止 `UIView` 的刷新不在主线程之类的问题
* 那就写个分类!帮它移动到主线程,但我觉得这种问题应该被暴露出来而不是被放过
* 如何做「对象找不到」方法时的防 crash
* `runtime` 里在找不到方法时有三次几乎可以触发保护
1. `resolveInstanceMethod。一般做` crash 防护时不会去碰这个方法,因为这个方法很有可能是本类在做一些动态插入的事情。
2. `forwardingTargetForSelector`。一般在这个方法中做处理,通过方法交换,把本应该是 A 类处理,但 A 类没有相关处理的方法转移到另外一个 B 类中,通过在 B 类中替换 A 类的方法,可以做记录 crash 发生的参数等。
3. `forwardingInvocation`。一般也不在这个方法中做处理,因为需要自行创建 `NSInvocation` 对象来做对象替换,并调用对应方法。
### WKWebView 起 POST 请求时,不带 body?
由于 `WKWebView` 在独立进程里执行网络请求。一旦注册http(s) scheme后,网络请求将从 Network Process发送到 App Process,这样 `NSURLProtocol` 才能拦截网络请求。在 webkit2 的设计里使用 `MessageQueue` 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。
出于性能的原因,encode 的时候 `HTTPBody` 和 `HTTPBodyStream` 这两个字段被丢弃掉了。
因此,如果通过 `registerSchemeForCustomProtocol` 注册了http(s) scheme, 那么由 WKWebView 发起的所有http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 `body` 被清空。
### 如何通过脚本自动生成一个 pod 库
```shell
#!/bin/sh
read -p "Please enter the Module name: " module
script_dir=$(dirname $0)
dev_path="${script_dir}/../../Development Pods/"
git_path="template.git"
module_name="${module}"
cd "${dev_path}"
pod lib create --template-url=${git_path} ${module_name}
```
### iOS 给每个 app s分配的内存的上限为本机内存的一半再多一点
### cocoapods 组件库多依赖问题的解决
比如 A 库依赖 `AFNetworking` 3.0,B 依赖 `AFNetworking` 2.0,此时 B 要引入 A 组件库,按照以往的昨晚是会出现重名库错误。
解决办法可以把 A 库达成二进制库,只暴露出 A 库应该对外暴露的方法,或者勇另外一个笨方法,手动把 A 库依赖的 `AFNetworking` 里方法都加上 A 库的前缀,但这种做法只能在不麻烦的情况下使用啦。
### CATransaction 动画事物管理
* UIView 修改 CALayer 动画时,实际上做的隐式事物,在下一个 RunLoop 中进行提交,显示事物会被立即提交
* [CATransction commit]。不管当前 RunLoop 进行剩下多少耗时操作动画都会被理解提交
* 把暴力操作移交到子线程去做
## CocoaPods 一些基础知识
### `pod setup`
`pod setup` 很智障,会把 github 上的所有的托管的 repo 都拉到本地,所以造成了第一次巨慢...
### `Podfile.lock`
第一次执行 `pod install` 时生成,除非 `Podfile` 有修改或执行 `pod update` 否则不会改变
### `Headers`
该目录下存放对应库的**头文件**软链接
### `Manifest.lock`
`Podfile.lock` 的拷贝文件
### `Target Support Files`
存放工程 build 时所依赖的一些文件
### 一些小点
* 每一个 Pod 都会有 `.xccoinfig`, `.pch`, `dummy.m`(类的空实现)
* `debug.xcconfig` 和 `release.xcconfig` 对应主工程里的 `Configurations` 的 `xcconfig`
* `resource.sh` 用来编译 storyboard 等资源文件或者 copy `xcassets` 等资源文件
* `Frameworks.sh` 用于实现 framework 类型第三方库的链接

* 首次使用 `pod update` 和 `pod install` 没有任何区别
* `pod install` 会参考 `Podfile.lock` 和 `Podfile`,在生成 `Podfile.lock` 之后,除非 `Podfile` 有所改动,否则运行 `pod install` 结果依然不变
* Pods/ 文件夹不需要添加到 `.gitignore` 里。`Podfile.lock` 应该加入项目版本控制中,可以保证所有人在执行 `pod install` 后所依赖的三方库版本完全一致。
### 通过一些简单的 ruby 语法来调整 `Podfile`

### 如何友好的调试本地 pod 库

### 如何提高因为 Pod 库太多导致编译太慢?
提前将三方库打成静态库,存到公司内部仓库,每次 `pod install` 直接通过内网拉去静态库,build 时省去 Pod 的编译。
### 如何把主工程的宏同步到 Pod 项目上
1. 直接把需要用到的东西都同步到一个新的 Pod 库里
2. 利用 `cocoapods` 的特性。在 `pod install` 时,利用 `pre_install` 和 `post_install` 两个钩子函数,如下图所示:

### 传统工程和组件化工程的区别

### 拷贝问题
Foundation 框架中的所有集合类在默认情况下都执行浅拷贝,也就是说,只拷贝容器本身,而不复制其中的数据。这样做的原因在于容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器的时一并拷贝其中的每个对象。
### 如何执行深拷贝?
* `NSSet` 方法有对应的初始化方法直接执行深拷贝 `initWithSet:copyItems`
* OC 中没有专门定义深拷贝的协议,具体执行由每个类自行决定。
* 不要假定遵循了 `NSCopying` 协议的对象都会执行深拷贝,在绝大多数情况下执行的都是浅拷贝。除非文档中说明了其深拷贝时基于
### HTTPS 就是不想被抓包怎么办?
* 再加一层密
### 动态库
将一系列代码封装为动态库,并在其中放入描述其接口的头文件,这就叫框架。iOS 上的第三方框架几乎都是静态库,因为 Apple 不允许 iOS 应用程序中包含动态库,但 Apple 自身的系统框架都是动态库。
### 对 pod 库使用 `resource_bundles` 来管理资源
Cocoapods 的 `bundle_resources` 和 `resources` 可以并存,分别制定资源的加载方式,如果不通过 bundle 的方式去做,用 `imageWithName` 的方法去做即可。因为用之前的 `resources` 方法会导致 pod 库中的资源合并到主工程里,多个 pod 库之间都这么做的话,那同名资源会出现异常。
使用 `bundle_resources` 方式引入资源时,如果原先是 `resources` 方式引入的资源,各个 pod 库里的资源会被合并到主工程中,所以其它 pod 库可以通过「资源合并」的方式间接引用到其它 pod 里的资源,改用 `bundle_resources` 时,其它 pod 库原先引用的地方都要改为对应的 bundle 里的资源,如果在全局替换搜索时,很有可能会因为 pod 库饮用时是二进制包导致搜索不到,还有些没有暴露 .m 文件。
解决思路:
* 各个 pod 库所属的各自资源都加上自己的前缀。
* 使用 `resource_bundles` 的方式。
```swift
+ (NSBundle *)mainBundle {
static NSBundle *bundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *bundlePath = [[NSBundle bundleForClass:TTVideoBusinessBundle.class].resourcePath stringByAppendingPathComponent:@"TTVideoBusinessResource.bundle"];
bundle = [NSBundle bundleWithPath:bundlePath];
if (!bundle) {
NSAssert(!bundle, @"未正确加载 TTVideoBusinessResource.bundle");
}
});
return bundle;
}
```
```swift
+ (UIImage *)ttv_bundleImageName:(NSString *)imageName {
return [UIImage imageNamed:imageName inBundle:[TTVideoBusinessBundle mainBundle] compatibleWithTraitCollection:nil];
}
```
### [UIView setNeedsLayout]
本质上只是加个个标记,在下一个 runloop 布局。
### 子线程没有 runloop
在子线程中手动起一个 runloop,直接 run,停不下来,之后的内容执行不到。
### Auto Layout 的实现
* 基于 `cassowary` 算法
* `cassowary` 还有 JS、Java 和 C++ 等库的实现
* Auto Layout 不只有布局算法,还包含了布局在运行时的生命周期等,这一套布局引擎系统叫 `Layout Engine`
* 每个视图在获取到自己的布局之前, `Layout Engine` 会将试图、约束、优先级、固定大小通过计算转换成最终的大小和位置。
* 在 `Layout Engine` 里,每当约束发送变化,就会触发 `Deffered Layout Pass`,完成后进入监听约束变化的状态。
* 监听到约束变化时,即进入下一轮 `RunLoop` 循环中。
* 添加、删除视图时会触发约束变化,设置约束或优先级也会触发约束变化
* `Layout Engine` 在约束变化时,会重新计算布局。获取到布局后调用 `superView.setNeedLayout()` 然后进入 `Defferred Layout Pass`。
* `Defferred Layout Pass` 的主要作用是做容错处理。如果视图在更新约束时没有确定或缺失布局声明的话,会先在这里做容错处理。
* 接下来 `Layout Engine` 会从上到下调用 `layoutSubview()`,通过 `cassowary` 算法计算各个子视图的位置,计算完成后,再从子视图的 frame 从 `Layout Engine` 里 copy 出去。
* 与 frame 布局多了一个通过 `cassowary` 布局计算的过程。
### 链接器的作用
就是完成变量、函数符号和其地址绑定这样的任务。
### 合并 `Mach-O` 文件的作用
可以理解为是把分离的源代码文件合并在一起,解决调用其它文件中的方法问题。
### 提供一个 pod 库给不同的 app 使用时
可以使用不同的 `podspec`,引用不同的宏定义,通过宏去执行不同的逻辑。
### 矢量图片在编译时会生成位图。
### 载动态库的方式有两种:
* 在程序开始运行时通过 `dyld` 动态加载。需要在编译时进行链接,链接时会做标记,绑定的地址在加载后再决定。
* 显式运行时链接,在运行时通过动态链接器提供的 API `dlopen` 和 `dlsym` 来动态加载。这种方式,在编译时是不需要参与链接的。但不允许上线,但可通过其加速调试。
### XNU 内核决定应当使用的线程数
并只生成所需的线程执行处理。当处理结束,应当执行的处理数减少,XNU 内核会结束不再需要的线程。
### 多 icon 可以通过多 target 完成。
### 怎么通过多 target 做隔离。
* New 一个 target
* copy 一个 target
* copy 只会多一些配置文件,可以在这些配置文件中读取一些特定的字段进行操作。
## app 长期保活
保活指的是 app 在后台不会因为资源不足而被系统结束进程。
- 地理位置更新,打点range:涉及到后台获取地理位置。
- 录音。Plan A。
- 持续下载文件。带宽问题。
- 静默loop播放音乐。
静默播放音乐可能最优。
优点:无权限,无带宽,性能损耗低,无影响。
缺点:某些情况下play也会失效。比如和平精英的语音转文字场景。(目前还没有有效方案)。
## 在不同 app 间传递大文件
如果是同一个 groupID 的 app 集合,可以通过共享文件夹的方式随意存取文件,但一个公司/集团不可能只有一个开发者账号,不同的开发者账号不同的 appID,如何做到互相传递大文件呢?
直接用剪贴板,针对被传递出去的文件做加密,剪贴板传资源不费事,但对传输出去的文件做加密比较费事。
================================================
FILE: iOS/Swift/Cache.md
================================================
# Cache
缓存相关内容
1. CPU 接收到指令后,它会最先像 CPU 中的一级缓存(L1 Cache)去寻找相关的数据,虽然一级缓存是与 CPU 同频运行的,但由于容量小,所有不可能每次都命中。这是 CPU 会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有找到的话,会继续转向 L3 Cache(如果有三级缓存的话,如 Xeon,Phenom)、内存和硬盘。
================================================
FILE: iOS/Swift/CoreData.md
================================================
# CoreData
* 要把 `Core Data` 作为一个对象图管理系统来使用,而不是关系型数据库
* `Core Data` 的架构图:

- **托管对象**:我们创建的数据模型。
- **托管对象上下文**:托管对象上下文记录了它管理的对象,以及对这些对象的 CRUD,每个被托管的对象都知道自己属于哪个上下文。
- `Core Data` 支持多个上下文。
- 数据底层实际上会被存储在 `SQLite` 数据库里。
* `Core Data` 存储结构化的数据。所以为了使用 `Core Data` 需要先创建一个数据模型来描述我们的数据结构。
* **实体**:一个实体应该代表你的应用程序里有意义的一个部分数据。实体名称以大写字母开头。
* **属性**:属性名称应该以小写字母开头。因为 `Array` 已经遵循里 `NSCoding` 协议,所以可以直接把这样的数组直接存入一个可转换类型的属性 `Transformable` 里。
* 属性选项:**必选属性** `non-optional` 必须要赋给它们恰当的值,才能保存这些数据。把属性标记为可索引时 `indexed`,`Core Data` 会在底层 `SQLite` 数据库表里创建一个索引,可以加速这个属性的搜索和排序,但代价是插入数据时的性能下降和额外的存储空间。
* **托管对象子类**:实体只是面熟了那些数据属于某个对象,为了能够在代码中使用这个数据,还需要一个具有和实体里定义的属性们相对应的属性的类。
- 实体和类都叫同一个名字。
- 不建议使用 `Xcode` 的代码生成工程工具,而是直接手写。
- 代码通常会如下所示:
```swift
final class Mood: NSManagedObject { @NSManaged leprivate(set) var date: Date @NSManaged leprivate(set) var colors: [UIColor]
}
```
- `@NSManaged` 标签告诉编译器这些属性由 `Core Data` 来实现。
- 在模型编辑器中选中这个实体,然后在 `data model inspector` 里输入它的类名完成实体和类的关联。
* **设置 `Core Data` 栈**
* **获取请求**
每次执行一个获取请求,都会直达文件系统,是一个相对昂贵的操作,可能是一个潜在的性能瓶颈。
* **Fetched Results Controller**
- 与 `tableView` 的交互:

* 始终把 `Core Data` 对象交互的代码封装进类似的一个 `block` 里。
* **子实体**
- 
* 在 `Core Data` 的模型编辑器中设置好关系
* 在代码中写入实体关系代码不是必须的,只要在「模型编辑器」中设置好了对应的关系,实际上就可以开始工作了,但为了在代码里直接使用它们,可以定义一下。
* 自定义删除规则。
- 使用 `prepareForDeletion` 方法,该方法会在对象被删除之前被调用。
* CoreData 推荐在大数据集合时,分批处理,最佳的测试结果显示,单批次 1000 个左右比较好
### 获取请求操作的这两行代码具体做了什么操作?
```swift
let request = NSFetchRequest(entityName: "Mood")
let moods = try! context.fetch(request)
```
* `context` 通过调用 `execute(_request:withcontext:)` 方法把请求转交给它的持久化存储协调器。
* 持久化存储协调器通过调用每个存储的 `execute(_request:withcontext:)` 方法把获取到的请求转发给所有的持久化存储。此时的 `context` 被传递给了持久化存储。
* 持久化存储把获取请求转换成一个 `SQL` 语句,并把这个 `SQL` 语句发送给 `SQLite`。
* `SQLite` 在存储数据库文件里执行该语句,并将所有匹配查询条件的所有行(`row`)返回给存储。`row` 里包括了对象的 `ID` 和其它属性数据(`includesPropertyValues = true`)。
返回的原始数据是由数字、字符串和二进制大对象 (BLOB, Binary Large Objects) 这样 的简单的数据类型组成的。它被存储在持久化存储的行缓存 (row cache) 里,一起存储 的还有对象 ID 和缓存条目最后更新的时间戳。只要在上下文里存在某个特定对象 ID 的
托管对象,含有这个对象 ID 的行缓存条目就会一直存在,无论这个对象是不是惰值 (fault)。
* 持久化存储把它从 `SQLite` 存储接收到的对象 ID 实例化为托管对象,并把这些对象返回给协调器。这些对象默认是**惰性**的。在相同的托管对象上下文里,表示相同数据
* 持久化存储协调器把它从持久化存储拿到的托管对象数组返回给上下文。
* 最后,一个匹配该获取请求的托管对象数组被返回给调用者。
需要注意的是,以上这一切的操作都是**同步**的,而且直到获取请求完成为止,托管对象上下文都会被阻塞。

### 批量获取
```swift
let request = Mood.sortedFetchRequest(with: moodSource.predicate) request.returnsObjectsAsFaults = false
request.fetchBatchSize = 20
```
这个查询结果只是一个对象 ID 的列表,而不是与它们相关联的所有数据。持久化存储协调器创建一个特殊的,由这些 ID 组成的数组,并将这个数组返回给上下文,并且这个数组没有被任何数据填充,它只在必要的时候才会去获取数据。
一旦我们访问数组里的数据,Core Data 才会去「按页加载」真正的数据,处理流程如下:
1. 这个分批处理的数组注意到它缺少你正尝试访问的元素的数据,它会要求上下文加载你 请求的索引附近数量为 fetchBatchSize 的一批对象。
2. 像往常一样,这个请求被持久化存储协调器转发到持久化存储,在那里执行适当的SQL 语句来从 SQLite 加载这批数据。原始数据被存储在行缓存里,托管对象则被返回给协 调器。
3. 因为我们已经设置了returnsObjectsAsFaults为false,协调器会要求存储提供全部数 据,然后用这些数据来填充对象,并将这些对象返回给上下文。
4. 这一批次的数组将返回你所请求的对象,并持有本批次里的其他对象,这样接下来如果 你需要使用其中某个对象的话,就不必再次获取了。
当我们在遍历数组去加载数据时,Core Data 会以 LRU 的原则来控制数据集合,也就是以最近使用作为原则来保持少量批次,而较早的批次将会被释放。
### 异步获取请求
```swift
let fetchRequest = NSFetchRequest(entityName: "Mood") let asyncRequest = NSAsynchronousFetchRequest(
fetchRequest: fetchRequest) { result in
if let result = result.nalResult { // 获取到结果了
} }
try! context.execute(asyncRequest)
```
### 内存考量
为了减小内存消耗和回应内存警告的处理,可以使用 `context` 的 `refreshAllObject()` 方法来将不包含待保存改变的对象惰值化。
### 关系的循环引用

解决以上问题,需要至少刷新一个对象。通过调用上下文的 `refresh(_ object:mergeChanges:)` 方法
### 对性能影响最大的是获取请求 (fetch request)。
获取请求会遍历整个 Core Data 栈。一个获取请求,按照它的 API 约定,就算是从托管对象上下文中发起的,也会查询到文件系统的 SQLite 存储。
### 索引
是否添加索引,取决于,在你的应用程序中,获取数据与修改数据频繁程度的比例。如果更新或者插入非常频繁,最好不要添加索引。如果更新或插入不频繁,而查询和搜索非常频繁,那么添加索引会是个好主意。
有一个因素是数据集的大小:如果实体的数目相对较小,添加索引并不能给我们带来多少帮助,因为数据库扫描所有数据也很快。但是如果数量巨大,添加索引就可能可以显著改善性能。
### 手动管理实体代码生成器
Core Data 默认创建出来的实体是**自动生成代码**,如果我们想要手动管理实体代码,需要按照如下图进行修改 `codegen`:

### NSFetchResultController 获取数据
* 第一次初始化时,可以通过 `try! fetchedResultsController.performFetch()` 来获取数据,正常执行完方法后即可拿到数据。`NSFetchedResultsController` 既是 fetch request 的包装,也是一个获取数据用的 container,我们可以从中获取到数据。
* 后续再执行增删时,通过代理响应数据变化。
================================================
FILE: iOS/Swift/OC转Swift.md
================================================
这篇文章主要是记录我在把主Objective-C的项目迁移到Swift上遇到的问题总结,不得不说,真的很坑。🙄
1. 第一次创建Swift文件时,会提示是否创建桥接文件,如果选了,会在当前路径下创建出桥接文件,当移动该桥接文件时,记得去,Build Setting -> Swift Compiler - General -> Objective-C Bridging Header,修改为调整后的路径即可。
2. Swift中的权限访问控制。http://www.hangge.com/blog/cache/detail_524.html
3. Swift中的指定构造器(designated Initializer)必须有一个,而且对其中初始化的所有属性,必须要在super方法之后。https://www.cnblogs.com/sunshine-anycall/p/4036932.html
4. OC和Swift互相调用。https://www.jianshu.com/p/754396e7e1bd
5. 遇到了一个迷之问题,OC调Swift方法,一直编译不过,看了对应的product Name-Swift.h文件后,发现无用tableView代理方法也暴露出来了,这样不是很好,就给所以相关代理方法加了private,Xcode提示不行,要用internal,改完后,居然神TM编译过了,最后琢磨来琢磨去,还是没搞懂这是为啥,去相关讨论群中咨询时,把internal关键词删掉,又神特么编译过了。。。到现在都不知道是为啥,感觉Swift是不是不太稳定。。
6. Swift中没有NSBundle了,叫Bundle。😑
================================================
FILE: iOS/Swift/PFollow.md
================================================
## PFollow 开发总结
### 点加载
从相册中每次都全量读取一次数据,还要比对是否重复和变更,虽然速度不慢,但直接拿 SwiftUI 进行点渲染,一万多张照片需要渲染一万多个点,有较大性能问题。
可以使用自定义 MapView,使用聚类来进行调优,点跟随缩放素质进行增加或减少,降低 UI 渲染压力。
================================================
FILE: iOS/Swift/PJPickerView开发总结.md
================================================
# PJPickerView 开发总结
今天周日继续撸码,继续完成另一个组件,给之取名为——`PJPickerView`,别以为它真的只是个`View` 哦,为了让它看上去显得不是太“重”,从而取了这个名字,本质上是个 `UIViewController`,可能你会觉得有些奇怪,为什么一个组件要上 `UIViewController` 呢?刚开始我也不想这么玩,听我慢慢道来。
## UI
还是先来看 UI,
UI 已经画得十分清楚了,就是要让我们分离出一个组件来,而且还是能够自定义数据源的。
## 思考
* 肯定要用到 `UIPickerView` 和 `UIDatePickerView` ,只不过需要在 `UIPickerView` 上自定义一下;
* 要处理好蒙版。如果这还像之前那般偷懒,直接把整个组件添加到当前控制器视图上,蒙版的显示区域只能是 `UINavigationBar` 下的区域,这样会少了头部遮罩,十分奇怪;如果是把组件添加到当前显示的 `UIWindow` 上,那么 `statusBar` 里的运营商、电量和时间等信息也不会被遮罩,而且会异常明显的被高亮出来,如果你感兴趣的话,可以尝试把一个黑色的 `UIView` 直接添加到当前 `UIWindow` 上。
* 因为是个组件,所以是肯定不能走代理回调的。第一,Apple 自家的各种系统组件基本上都走的代理回调,再多写几个代理给自己或者其它人调用估计得炸了;第二,这可是高大上的 `Swift`,怎么还能屈服于老土的 `Objective-C` 时代的各种回调呢?闭包是一定要闭的!
## 实践
### 自定义 UIPickerView
`UIPickerView` 的各种回调使用方式和流程与 `UITableView` 及其类似,同样需要继承 `UIPickerViewDelegate, UIPickerViewDataSource`,并实现以下几个方法即可:
```Swift
// MARK: - Delegate
func numberOfComponents(in pickerView: UIPickerView) -> Int {
// 告诉 UIPickerView 有多少组
}
func pickerView(_ pickerView: UIPickerView,
numberOfRowsInComponent component: Int) -> Int {
// 告诉 UIPickerView 每组下有多少条数据,component 为组别
}
func pickerView(_ pickerView: UIPickerView,
titleForRow row: Int,
forComponent component: Int) -> String? {
// 返回 UIPickerView 每组下每条数据需要显示的内容,只能是字符串,如果要自定义 View 走 `pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView` 这个方法
}
func pickerView(_ pickerView: UIPickerView,
didSelectRow row: Int,
inComponent component: Int) {
// 拿到 UIPickerView 当前组别和条数,相当于 section 和 row,注意:如果用户什么都选,默认在第一条,但此时因为用户并未进行操作,所以该代理方法里写的内容不会被执行
}
```
只要按照对应代理方法所提供的作用填写代码即可,因为 `PJPickerView` 最多只做两组数据,所以直接拿了一个二维数组去做了数据源,当然,如果调用者非得塞下超过两列的内容也不是不行,但显示出来的效果就会畸变,目前我除了再自定义一个数据源模型替代二维字符串数组外没有更好的想法。
### 闭包回调
在之前很长的一段时间里,我非常喜欢用代理回调做组件间,甚至 vc 间的事件处理回调,可能因为当时觉得这是最简单的一种方式了吧,到今年这段时间强制性压迫自己且到 `Swift` 上,如果在 `Swift` 上还用 OC 那一套流程去写代理回调,出来的效果全是浓浓到 OC 味道,一点都不 `Swifty`。
所以,我采用如下方式来进行处理回调:
```Swift
// 声明一个中间闭包,作为后边逃逸闭包的引用
private var complationHandler: ((String) -> Void)?
// ...
// MARK: - Public
class func showPickerView(viewModel: ((_ model: inout PickerModel) -> Void)?, complationHandler: @escaping (String) -> Void) {
let picker = PJPickerView()
picker.viewModel = PickerModel()
if viewModel != nil {
viewModel!(&picker.viewModel!)
picker.initView()
}
picker.complationHandler = complationHandler
// 这是重点方法,后文讲解
picker.showPicker()
}
```
因为涉及到许多变量,所以在此我用了一个结构体去做了承载:
```Swift
struct PickerModel {
var pickerType: pickerType = .time
var dataArray = [[String]]()
var titleString = ""
}
enum pickerType {
case time
case custom
}
```
不想在外部调用初始化器对 `PJPickerView` 做初始化,采用了类方法供外部调用,且在类方法内部对 `viewModel` 做初始化,通过 `inout` 关键字修改其为可变参数传出给外部,这样就可以达到在外部对 `viewModel` 设置好相关参数后,在类内部直接使用即可。
最后使用 `@escaping` 关键字把跟随的闭包设置为了逃逸闭包,用之前声明的 `complationHandler` 对该逃逸闭包进行引用,供对应方法进行调用,调用方式所示:
```Swift
@objc fileprivate func okButtonTapped() {
// ...
// finalString 为 UIPickerView 选中的字符串,在 didSelectRow 方法进行设置
if complationHandler != nil {
complationHandler!(finalString)
}
}
```
这样就完成了当对 `UIPickerView` 进行选择时可以回调给调用方,而调用方可以这么来进行调用:
```Swift
PJPickerView.showPickerView(viewModel: { (viewModel) in
viewModel.titleString = "感情状态"
viewModel.pickerType = .custom
viewModel.dataArray = [["单身", "约会中", "已婚"]]
}) { [weak self] finalString in
if let `self` = self {
self.loveTextField.text = finalString
}
}
```
以上的这种调用方式就是为内心中相对较为完美的调用方法了!🤓
### 蒙版
经过以上几个步骤后,我们基本上已经把 `UIPickerView` 的主体搭建完毕,接下来进行蒙版的设计。
如果此时我们把 `PJPickerView` 带上蒙版(实际就是个 `UIView`)直接添加到 `ViewController.view` 上,蒙版只会占据 `ViewController.view.frame` 的区域,如果当前的这个 `ViewController` 在 `UINavigationBar` 下,会导致头部区域无法被蒙版覆盖,所以是肯定不能直接添加到 `ViewController` 上的。
之前我的偷懒做法是直接把组件添加到当前 `topWindow` 上,这样就能够除了顶部状态栏上以外全覆盖了,但问题是如果我们就想把包括顶部状态栏也一起覆盖掉呢?此时直接用 `UIApplications` 里的 `UIWindow`,比如这么把最上层 `UIWindow` 拿出来:
```Objc
+ (UIWindow *)TopWindow {
UIWindow * window = [[UIApplication sharedApplication].delegate window];
if ([[UIApplication sharedApplication] windows].count > 1) {
NSArray *windowsArray = [[UIApplication sharedApplication] windows];
window = [windowsArray lastObject];
}
return window;
}
```
默认情况且我们不做其它任何修改,这样拿到的 `UIWindow` 的 `windowLevel` 是 `normal`,而我们的状态栏所在的 `UIWindow` 是 `statusBar` 级别, `UIWindowLevel` 的三种级别排序为:`normal` < `statusBar` < `alert`,所以这才会出现了如果我们直接把组件添加到当前 `UIWindow` 上蒙版并不能覆盖到顶部状态栏部分。
所以解决办法时,再造一个 `UIWindow.Level == .alert` 的 `UIWindow` 作为组件的容器,为了更好的让 `UIWindow` 对组件进行管理,此时也就引出了为什么 `PJPickerView` 底层是个 `UIViewController` 而不是 `UIView` 的原因:
```Swift
private func initView() {
// 把当前 window 拿到
mainWindow = windowFromLevel(level: .normal)
pickerWindow = windowFromLevel(level: .alert)
if pickerWindow == nil {
pickerWindow = UIWindow(frame: UIScreen.main.bounds)
pickerWindow?.windowLevel = .alert
pickerWindow?.backgroundColor = .clear
}
pickerWindow?.rootViewController = self
pickerWindow?.isUserInteractionEnabled = true
// ...
}
func windowFromLevel(level: UIWindow.Level) -> UIWindow? {
let windows = UIApplication.shared.windows
for window in windows {
if (level == window.windowLevel) {
return window
}
}
return nil
}
// show 方法
private func showPicker() {
pickerWindow?.makeKeyAndVisible()
// ...
}
// MARK: - Actions
@objc fileprivate func dismissView() {
UIView.animate(withDuration: 0.25, animations: {
// ...
}) { (finished) in
if finished {
UIView.animate(withDuration: 0.25, animations: {
self.pickerWindow?.isHidden = true
self.pickerWindow?.removeFromSuperview()
self.pickerWindow?.rootViewController = nil
self.pickerWindow = nil
}, completion: { (finished) in
if finished {
self.mainWindow?.makeKeyAndVisible()
}
})
}
}
}
```
### 成果
## 总结
在实现 `PJPickerView` 的过程中,第一场较为完整的学习和经历了以下事情:
·
* 自定义 `UIPickerView`;
* 简单的闭包回调的设计;
* 对蒙版的思考;
总的来说在实现的过程中自己主要是在反思“高内聚,低耦合”的指导,之前的做法都太简单粗暴,而且太过啰嗦,第一次较为完整的思考了整个流程,肯定还是有不足之处,等到后续功力慢慢增长再来对它好好修补一翻吧~
只放出了部分核心代码,不保证能够完全复现,只提供个思路~不管怎么说这周末的过的很开心,把手上的事情又往前推进了一大步!
================================================
FILE: iOS/Swift/PJPickerView开发总结.md).md
================================================
> 这个组件做的实在是太久了,最近终于从一大堆事儿中慢慢的恢复过来了,继续肝!
## 前言
这次的组件开发换了个思路继续精进,也还是 `MVC` 的模式,前段时间自己非常纠结到底哪种模式才是“最佳”设计模式?翻阅了大量资料,后来在[这篇文章](https://www.jianshu.com/p/33c7e2f3a613)中得到了“救赎”,让我真正的从回归到从实际问题出发,而不是一昧的为了“用”而用,尤其是在昨天的迭代总结会上,android 同学“夸夸其谈”的列出了许多所谓的“优化点”,某些“优化点”在我看来却是十分的可笑,本来以为来了个大佬,现在看来却是个“大佬”。
从 11 月末开始就着手准备开发这一新组件,但因为刚好与三方、新版本迭代期、期末课设等各种因素导致组件开发一再延后。该组件利用了 `PhotosKit` 框架,完成了从系统相册读取并自定义相册的功能,设计稿如下;

## 思考
一开始看到设计图后,感觉并没有多少东西需要去做,玩好 `PhotosKit` 即可,经过了一段时间后,再三确认后,最终的产品效果是要对齐 `Instagram` 里的“照片浏览器”体验一致,截图如下所示:

接着我就去玩了 `Instagram`,越玩越感觉这是个“大坑”,如果要做到 100% 的交互相似,估计做完直接丢出去开源又会拉到一波 star,对 `Instagram` 的”照片浏览器“分析如下:
* 可以简单的进行上下拆分。上部分为”选中视图“,可以直接套 `UIImageView`,下部分为“浏览视图”,可用 `UICollectionView`;
* 当”浏览视图“进行“上滑”操作时,无论滑动多么快速都不会触发“选中视图”的连带“上滑”操作;
* 当用户从“浏览视图”的范围滑动到“选中视图”中时,也就是手指触摸区域到达“选中视图”区域,将触发“选中视图”的连带“上滑”操作。
* 当“选中视图”已经到顶时,用户从“浏览视图”进行”下拉“操作,直接触发“选中视图”的连带“下拉”操作。
以上是目前总结出 `Instagram` 的“照片浏览器”四大要点,后续的开发工作也围绕着这四点进行。
## 需求拆分
在**思考**环节明确了该组件的开发难点后,开始对需求进行拆分,细致工作量。前几天还冒出了一个“笑话”,组件都快开发完了,自己却不放心,多嘴再去沟通了一遍,发现原来最终的效果和目前所实现的差距有些大,不得不又反工重来。
经过一番调研,设定的耗时为:2天,包括前后端联调。整理出的需求大致如下:
Feature | UI | Power
--- | --- | ---
浏览相册 | `UITableView` | 0.5
浏览相册照片 | `UICollectionView` | 1
交互 | - | 1
## 实现
一开始在数据源的获取上就跪了,之前在读取系统相册资源这方面需求仅仅只是“获取”这一方面,并没有对交互有太多的要求,也就一直没有精进,这回需要对相册做一个自定义就跪在数据源的获取上了,构思后,觉得有必要拉出一层 `DataManager`,虽然只是从系统中拉数据,但为了“高内聚、低耦合”的理念,应该降低“调用方”的使用成本。
挑出了一部分 `PhotosKit` 框架核心知识点列举如下:
* `PHObject`:`Photos` 的资源集合和集合列表的抽象父类;
* `PHAssetCollection`:一个相册;
* `PHCollectionList`:一个包含多个相册的集合;
* `PHFetchResult`:
* 作为 `PHAsset`(Live Photo)、`PHCollection`、`PHAssetCollection`、`PHCollectionList` 相关方法的返回结果对象;
* 内容可动态加载,并不是直接把某个相册中的内容直接全部遍历出来,而是当需要一部分内容后才会去照片库中获取,这可以在处理大量结果时提供一个最佳性能;
* 默认**线程安全**;
* 缓存规则个人看法是利用了 `LRU`,但实际上是不是这么一回事有待考证。
### 读取所有所需相册
加了个关键词——“所需”,`PhotosKit` 框架提供了一套十分完整的获取不同类型相册 API,在 `PJAlbumDataManager` 中,我是这么做的:
```Swift
/// 获取所有相册
private func allAlbumCollection() -> [PHAssetCollection] {
var collections = [PHAssetCollection]()
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum,
subtype: .any,
options: nil) as! PHFetchResult
let userAlbums = PHCollectionList.fetchTopLevelUserCollections(with: nil)
for album in [smartAlbums, userAlbums] {
for s_i in 0..` 类型,且因未实现 `Sequence` 协议,而无法进行遍历,而采取了一个简单的做法,至于比较优雅的做法暂未实现。
### 获取相册封面
通过以上方法,我们就拿到了当前用户设备中的所有所需相册集合。那如何获取一个相册的封面以及其所包含的照片数呢?经过一番研究后发现 `PHAssetCollection` 中提供了获取并未提供“封面”这个属性,同时也没有提供单独的 API 去获取一个相册封面,但是通过一个比较尴尬的方法,即通过获取相册中的所有照片 API 去锁定第一张照片,直接作为封面 😅,`PJAlbumDataManager` 中的实现如下:
```Swift
/// 获取所有相册封面及照片数
func getAlbumCovers(complateHandler: @escaping (_ coverPhotos: [Photo], _ albumPhotosCounts: [Int]) -> Void) {
let albumCollections = albums
var photos = [Photo]()
var photosCount = [Int]()
// 获取单张照片资源是异步过程,需要等待所有相册的封面图片一起 append 完后再统一通过逃逸闭包进行返回
var c_index = 0
for collection in albumCollections {
let assets = albumPHAssets(collection)
// 有些系统自带相册类型如果用户没有进行照片归类则会导致取到的相片数为0
guard assets.count != 0 else {
c_index += 1
continue
}
photosCount.append(assets.count)
var photo = Photo()
photo.photoTitle = collection.localizedTitle
convertPHAssetToUIImage(asset: assets[0],
size: CGSize(width: 150, height: 150),
mode: .fastFormat) { (photoImage) in
photo.photoImage = photoImage
photos.append(photo)
c_index += 1
if c_index == albumCollections.count - 1 {
complateHandler(photos, photosCount)
}
}
}
}
```
同样在上文中我们也说明了,一张张的照片是 `PHAsset` 资源对象,而 `PHAsset` 是从 `PHAssetCollection` 取出的,并且取出的资源集合类型中不需要包含视频且按照时间“由近到远”对集合进行排序。在 iOS 中对集合进行检索最佳做法是通过**“谓词”**进行限制,`PJAlbumDataManager` 实现 `albumPHAssets` 方法如下所示:
```Swift
/// 当前相册的所有 PJAsset
private func albumPHAssets(_ collection: PHAssetCollection) -> PHFetchResult {
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let fetchResult = PHAsset.fetchAssets(in: collection, options: options)
return fetchResult
}
```
一个相册中的所有 `PHAsset` 资源是全都拿到了,但是 `PHAsset` 资源却无法直接与 `UIKit` 进行协作,还需要对 `PHAsset` 对象转为 `UIImage` 对象,`PJAlbumDataManager` 中是这么做的:
```Swift
/// PHAsset 转 UIImage
func convertPHAssetToUIImage(asset: PHAsset,
size: CGSize,
mode: PHImageRequestOptionsDeliveryMode,
complateHandler: @escaping (_ photo: UIImage?) -> Void) {
let coverSize = size
let options = PHImageRequestOptions()
options.isSynchronous = false
options.deliveryMode = mode
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset,
targetSize: coverSize,
contentMode: .default,
options: options) { result, info in
guard result != nil else { return complateHandler(nil) }
complateHandler(result)
}
}
```
在 `requestImage` 方法中,我们可以对最终要生成 `UIImage` 对象做一些额外的设置,例如“目标尺寸”、“是否异步操作”等。需要注意的是,如果开启了“异步操作”,要记得处理好全部 `PHAsset` 资源集合的异步获取时间节点,否则一张照片都拿不到。
#### UI 搭建
显示所有相册的封面及其照片数,上文解决了数据来源后,剩下的事情就正常操作 `UITableView` 即可,完工截图如下所示:

### 获取一个相册下的所有照片
这部分思路已经上节中描述过了,把“只取第一张的照片”限制条件放开即可,`PJAlbumDataManager` 的实现如下:
```Swift
/// 获取某一相册的所有照片
func getAlbumPhotos(albumCollection: PHAssetCollection,
complateHandler: @escaping (([Photo], PHFetchResult) -> Void)) {
let assets = albumPHAssets(albumCollection)
var photos = [Photo]()
for a_index in 0.. 这个组件做的实在是太久了,最近终于从一大堆事儿中慢慢的恢复过来了,继续肝!
## 前言
这次的组件开发换了个思路继续精进,也还是 `MVC` 的模式,前段时间自己非常纠结到底哪种模式才是“最佳”设计模式?翻阅了大量资料,后来在[这篇文章](https://www.jianshu.com/p/33c7e2f3a613)中得到了“救赎”,让我真正的从回归到从实际问题出发,而不是一昧的为了“用”而用,尤其是在昨天的迭代总结会上,android 同学“夸夸其谈”的列出了许多所谓的“优化点”,某些“优化点”在我看来却是十分的可笑,本来以为来了个大佬,现在看来却是个“大佬”。
从 11 月末开始就着手准备开发这一新组件,但因为刚好与三方、新版本迭代期、期末课设等各种因素导致组件开发一再延后。该组件利用了 `PhotosKit` 框架,完成了从系统相册读取并自定义相册的功能,设计稿如下;

## 思考
一开始看到设计图后,感觉并没有多少东西需要去做,玩好 `PhotosKit` 即可,经过了一段时间后,再三确认后,最终的产品效果是要对齐 `Instagram` 里的“照片浏览器”体验一致,截图如下所示:

接着我就去玩了 `Instagram`,越玩越感觉这是个“大坑”,如果要做到 100% 的交互相似,估计做完直接丢出去开源又会拉到一波 star,对 `Instagram` 的”照片浏览器“分析如下:
* 可以简单的进行上下拆分。上部分为”选中视图“,可以直接套 `UIImageView`,下部分为“浏览视图”,可用 `UICollectionView`;
* 当”浏览视图“进行“上滑”操作时,无论滑动多么快速都不会触发“选中视图”的连带“上滑”操作;
* 当用户从“浏览视图”的范围滑动到“选中视图”中时,也就是手指触摸区域到达“选中视图”区域,将触发“选中视图”的连带“上滑”操作。
* 当“选中视图”已经到顶时,用户从“浏览视图”进行”下拉“操作,直接触发“选中视图”的连带“下拉”操作。
以上是目前总结出 `Instagram` 的“照片浏览器”四大要点,后续的开发工作也围绕着这四点进行。
## 需求拆分
在**思考**环节明确了该组件的开发难点后,开始对需求进行拆分,细致工作量。前几天还冒出了一个“笑话”,组件都快开发完了,自己却不放心,多嘴再去沟通了一遍,发现原来最终的效果和目前所实现的差距有些大,不得不又反工重来。
经过一番调研,设定的耗时为:2天,包括前后端联调。整理出的需求大致如下:
Feature | UI | Power
--- | --- | ---
浏览相册 | `UITableView` | 0.5
浏览相册照片 | `UICollectionView` | 1
交互 | - | 1
## 实现
一开始在数据源的获取上就跪了,之前在读取系统相册资源这方面需求仅仅只是“获取”这一方面,并没有对交互有太多的要求,也就一直没有精进,这回需要对相册做一个自定义就跪在数据源的获取上了,构思后,觉得有必要拉出一层 `DataManager`,虽然只是从系统中拉数据,但为了“高内聚、低耦合”的理念,应该降低“调用方”的使用成本。
挑出了一部分 `PhotosKit` 框架核心知识点列举如下:
* `PHObject`:`Photos` 的资源集合和集合列表的抽象父类;
* `PHAssetCollection`:一个相册;
* `PHCollectionList`:一个包含多个相册的集合;
* `PHFetchResult`:
* 作为 `PHAsset`(Live Photo)、`PHCollection`、`PHAssetCollection`、`PHCollectionList` 相关方法的返回结果对象;
* 内容可动态加载,并不是直接把某个相册中的内容直接全部遍历出来,而是当需要一部分内容后才会去照片库中获取,这可以在处理大量结果时提供一个最佳性能;
* 默认**线程安全**;
* 缓存规则个人看法是利用了 `LRU`,但实际上是不是这么一回事有待考证。
### 读取所有所需相册
加了个关键词——“所需”,`PhotosKit` 框架提供了一套十分完整的获取不同类型相册 API,在 `PJAlbumDataManager` 中,我是这么做的:
```Swift
/// 获取所有相册
private func allAlbumCollection() -> [PHAssetCollection] {
var collections = [PHAssetCollection]()
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum,
subtype: .any,
options: nil) as! PHFetchResult
let userAlbums = PHCollectionList.fetchTopLevelUserCollections(with: nil)
for album in [smartAlbums, userAlbums] {
for s_i in 0..` 类型,且因未实现 `Sequence` 协议,而无法进行遍历,而采取了一个简单的做法,至于比较优雅的做法暂未实现。
### 获取相册封面
通过以上方法,我们就拿到了当前用户设备中的所有所需相册集合。那如何获取一个相册的封面以及其所包含的照片数呢?经过一番研究后发现 `PHAssetCollection` 中提供了获取并未提供“封面”这个属性,同时也没有提供单独的 API 去获取一个相册封面,但是通过一个比较尴尬的方法,即通过获取相册中的所有照片 API 去锁定第一张照片,直接作为封面 😅,`PJAlbumDataManager` 中的实现如下:
```Swift
/// 获取所有相册封面及照片数
func getAlbumCovers(complateHandler: @escaping (_ coverPhotos: [Photo], _ albumPhotosCounts: [Int]) -> Void) {
let albumCollections = albums
var photos = [Photo]()
var photosCount = [Int]()
// 获取单张照片资源是异步过程,需要等待所有相册的封面图片一起 append 完后再统一通过逃逸闭包进行返回
var c_index = 0
for collection in albumCollections {
let assets = albumPHAssets(collection)
// 有些系统自带相册类型如果用户没有进行照片归类则会导致取到的相片数为0
guard assets.count != 0 else {
c_index += 1
continue
}
photosCount.append(assets.count)
var photo = Photo()
photo.photoTitle = collection.localizedTitle
convertPHAssetToUIImage(asset: assets[0],
size: CGSize(width: 150, height: 150),
mode: .fastFormat) { (photoImage) in
photo.photoImage = photoImage
photos.append(photo)
c_index += 1
if c_index == albumCollections.count - 1 {
complateHandler(photos, photosCount)
}
}
}
}
```
同样在上文中我们也说明了,一张张的照片是 `PHAsset` 资源对象,而 `PHAsset` 是从 `PHAssetCollection` 取出的,并且取出的资源集合类型中不需要包含视频且按照时间“由近到远”对集合进行排序。在 iOS 中对集合进行检索最佳做法是通过**“谓词”**进行限制,`PJAlbumDataManager` 实现 `albumPHAssets` 方法如下所示:
```Swift
/// 当前相册的所有 PJAsset
private func albumPHAssets(_ collection: PHAssetCollection) -> PHFetchResult {
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let fetchResult = PHAsset.fetchAssets(in: collection, options: options)
return fetchResult
}
```
一个相册中的所有 `PHAsset` 资源是全都拿到了,但是 `PHAsset` 资源却无法直接与 `UIKit` 进行协作,还需要对 `PHAsset` 对象转为 `UIImage` 对象,`PJAlbumDataManager` 中是这么做的:
```Swift
/// PHAsset 转 UIImage
func convertPHAssetToUIImage(asset: PHAsset,
size: CGSize,
mode: PHImageRequestOptionsDeliveryMode,
complateHandler: @escaping (_ photo: UIImage?) -> Void) {
let coverSize = size
let options = PHImageRequestOptions()
options.isSynchronous = false
options.deliveryMode = mode
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset,
targetSize: coverSize,
contentMode: .default,
options: options) { result, info in
guard result != nil else { return complateHandler(nil) }
complateHandler(result)
}
}
```
在 `requestImage` 方法中,我们可以对最终要生成 `UIImage` 对象做一些额外的设置,例如“目标尺寸”、“是否异步操作”等。需要注意的是,如果开启了“异步操作”,要记得处理好全部 `PHAsset` 资源集合的异步获取时间节点,否则一张照片都拿不到。
#### UI 搭建
显示所有相册的封面及其照片数,上文解决了数据来源后,剩下的事情就正常操作 `UITableView` 即可,完工截图如下所示:

### 获取一个相册下的所有照片
这部分思路已经上节中描述过了,把“只取第一张的照片”限制条件放开即可,`PJAlbumDataManager` 的实现如下:
```Swift
/// 获取某一相册的所有照片
func getAlbumPhotos(albumCollection: PHAssetCollection,
complateHandler: @escaping (([Photo], PHFetchResult) -> Void)) {
let assets = albumPHAssets(albumCollection)
var photos = [Photo]()
for a_index in 0..) -> ViewControllerWrapper.UIViewControllerType {
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewControllerWrapper.UIViewControllerType, context: UIViewControllerRepresentableContext) {
//
}
}
struct MyView : View {
var body: some View {
ViewControllerWrapper()
}
}
```
### `@State`、`@ObjcetBinding` 和 `@EnvironmentObject` 的区别
* 对于不变的常量直接传递给 SwiftUI 即可。
* 对于控件上需要管理的状态使用 @State 管理。
* 对于外部的事件变化使用 BindableObject 发送通知。
* 对于需要共享的视图可变数据使用 @ObjectBinding 管理。
* 不要出现多个状态同步管理,使用 @Binding 共享一个 Source of truth。
* 对于系统环境使用 @Enviroment 管理。
* 对于需要共享的不可变数据使用 @EnviromemntObject 管理。
* @Binding 具有引用语义,可以很好的和 @Binding @objectBinding @State 协作,避免出现多个数据不同步。
### SwiftUI 如何进行渲染子元素
1. 父视图为子视图提供预估尺寸
2. 子视图计算自己的实际尺寸
3. 父视图根据子视图的尺寸将子视图放在自身的坐标系中
实际上在写 `SwiftUI` 的代码,并不是在声明/创建和一个对象,就算是写了上百行,你都没有创建出任何一个 UI 对象,你一直都是在写 DSL,一直在写布局约束。
有时候任何的 `padding` 都没有写,却发现元素和元素之间实际上是有一点点间距的,这是因为 Apple 针对自家的人机交互指南自动填充的。
### 到底怎么样才是正确的 `SwiftUI` 开发模式
首先,需要确定的是 `SwiftUI` 提供了很多数据监听的方案,我们不再需要像之前那般手动同步“数据至视图”和“视图到数据”这两个环节,统统都可以交由 `Combine` 去处理。
那也就是说,之前 `ViewController` 中负责处理这两个环节的代码统统都没了,但是这不是说 `UIViewController` 没了,按照之前写 `Vue` 经验,做法是这样的:
在父组件(相当于是 `UIViewController`)中的 `created` 方法中发起网络请求。
- 在「发起请求」到「元素渲染」这一环节之间是有时间差的。
- 为了提供一个良好的用户体验,需要在这一环节中涉及到的变量做「默认值」处理,例如,列表变量要先给空之类。
换句话说,发起网络请求的时机可以是「元素渲染」之前或之后,但是渲染什么元素以及元素上的内容是是什么,这与网络请求无关,也就是说,我们在写 UI 时,要按照网络请求失败或网络请求数据为空来做。
#### 如果不需要依赖视图的创建,要怎么做?
通过 `BindableObject` 方式创建出数据中心,然后在创建视图组件的时候,创建该 `BindableObject` 对象,该对象在创建时去调用网络请求方法,网络请求不管再怎么快,都会比创建一个对象要慢得多。
所有在网络请求数据还未回来之前,UI 组件显示的内容为没有数据的内容,网络请求数据回来后再进行渲染。
### Redux
* 适用的场景
* 多交互、多数据源
* 从组件的角度去看
* 某个组件的状态,需要共享
* 某个状态需要在任何地方都可以拿到
* 一个组件需要改变全局状态
* 一个组件需要改变另一个组件的状态
* Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
* State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
* View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
* Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
* Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
* 由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象。
* 最好把 State 对象设成只读。你没法改变它,要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变的对象。
### 在使用 `HStack` 时如何使用设置两个元素左右布局
```swift
HStack(alignment: .center) {
Image("5")
.resizable()
.frame(width: 50.0, height: 50.0)
// 重点
Spacer()
Button(action: {
}) {
Image(systemName: "paperplane.fill")
.imageScale(.large)
.foregroundColor(.primary)
}
}
```
### 把 `SwiftUI` 页面嵌入到 `UIKit` 页面中
* `UIViewController` -> `UIHostingController`
* `UIView` -> `UIHostingView`
### 在 `SwiftUI` 中如何设置代理
因为 `SwiftUI` 中没有 `TextView`,如果我们非要一个 `TextView` 只能从 `UIKit` 中「嫁接」一个包装过的 `TextView`过去,而不创建一个 `UITextView`。
经过一番操作后,把 `TextView` 创建出来了,但是发现需要获取一些例如「开始编辑」、「正在编辑」的状态,这个时候就需要 `Coordinator` 去协助了,
```swift
struct MASTextView: UIViewRepresentable {
let now = Date()
var isBeginEditng = false
var nowTimeString: String {
get {
let dformatter = DateFormatter()
dformatter.dateFormat = "yyyy年MM月dd日 HH:mm"
return dformatter.string(from: now)
}
}
// 显示声明协调器
func makeCoordinator() -> MASTextView.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let tv = UITextView()
tv.tintColor = .black
tv.font = UIFont.systemFont(ofSize: 18)
tv.delegate = context.coordinator
tv.text = "在 \(nowTimeString) 写下"
return tv
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
class Coordinator: NSObject, UITextViewDelegate {
var textView: MASTextView
init(_ textView: MASTextView) {
self.textView = textView
}
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
if !self.textView.isBeginEditng {
self.textView.isBeginEditng = true
textView.text = ""
}
return true
}
}
}
```
### 设置 `BindableObject` 时的一些问题
```swift
class AritcleManager: BindableObject {
var willChange = PassthroughSubject()
var article = [Article]() {
willSet {
willChange.send(())
}
}
}
```
上述代码中 `PassthroughSubject` 里的 `Void` 和 `Never` 是什么意思?
* `Void`。指明我们要在数据改变时传递什么值,因为在例如用 `BindableObject` 来实现用户管理类,在用户管理类中又有用户的 `viewModel` 和 `token`,当用户退出登录时,`token` 清空,`viewModel` 也要清空;用户再次登录时,`token` 被赋值,`viewModel` 也拿到了新用户的信息数据,此时只需要监听 `token` 的变化,并把 `viewModel` 发布出去即可。
* `Never`。指明我们是否要连带错误类型也通知出去。`Never` 表示通知时什么错误类型也不带上,可以按照需求定义为 `NetworkError`。
### 千万注意代码是有顺序的
如果我们想要给一个 `View` 添加触摸事件,会下意识的按照 `UIKit` 的做法去做,可能设置这个 `View` 的 `frame` 属性,也可能先给这个 `View` 这个添加触摸手势,在 `UIKit` 中代码的先后顺序显得不是那么重要。
但在 `SwiftUI` 中就非常重要了,必须先设置好 `View` 的 `frame` 才能添加成功触摸手势,要不会失效。
### 当遇到单个 `View` 无法撑起整个布局
善用 `Spacer()`
### SwiftUI 的重新预览问题
* Xcode 的预览使用了动态替换 `body` 属性的特性,但是它有一些局限,当 `body` 以外的部分被改变时,将导致 `ContentView` 需要整个重新编译时,比如在 `body` 之外添加一个存储属性 `var a = 2333`,必须再次点击 Resume 按钮才能重新开始预览。
### 圆角设置
`cornerRadius` 通过包装的方式为 `View` 添加圆角,并返回**新的 `View`**。
### `ForEach` 遍历怎么做到不需要写 `id:\.balabala`
* `ForEach` 是个 `DynamicViewContent` 类型,所以在实现 `List` 的侧滑删除时,需要把内容到 `ForEach` 中进行处理,因为 `onDelete` 事件要求 `DynamicViewContent` 类型。
* `ForEach` 用来列举元素,并生成对应的 view collection 类型,🉑️一个数组,且数组中的元素需要满足 `Identifiable` 协议。如果数组元素不满足 `Identifiable` 协议,需要使用 `ForEach(_:id:)` 来通过某个支持 `Hashable` 的 key path 获取一个等效的元素是 `Identifiable` 的数组。
### `@State` 的一些细节
和一般的存储属性不同,`@State` 修饰的值,在 SwiftUI 内部会被自动转换为一对 setter 和 getter,对这个属性进行赋值的操作将会触发 `View` 的刷新,它的 `body` 会再次被调用,底层渲染引擎会找出洁面上与这个值相关的的更改部分,并进行刷新。
### 为什么不能对各个层级的组件都使用 `@State` ?
* `@State` 仅能在属性本身被设置时触发 UI 刷新,所以一般会直接拿一个值类型变量用于记录状态;
* 但值类型的变量在多个组件层级之间进行传递时,该值将会遵循值语义发生复制,而不是引用。导致每个组件的状态值都不一样。
* `@Binding` 就是用来解决这个问题的。所做的事情时将值语义属性「转换」为引用语义。
* 对 `@Binding` 的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递。
* 所以是,每个组件如果需要被其它组件里的值影响,都需要使用 `@Bingding` 去修饰。
### 在传递属性的时候,在前面加上一个美元符号 `$`。
在 Swift5 中,在一个 `@` 符号修饰的属性前几上 `$` 所取得的值,称之为**投影属性**(projecttion property)。
* 不是所有的 `@` 属性都有提供 `$` 的投影访问方式。
* `$var` 将 `State` 转换成了引用语义的 `Binding`,并向下传递。
### `@` 属性在 Swift 中的正式名称是属性包装(property Wrapper)。
### 使用 `ObservableObject` 有两种写法:
```swift
let objectWillChange = PassthroughSubject()
var obj: objModel = ObjModel() {
willSet { objectWillChange.send()
}
}
```
```swift
@Published var brain: CalculatorBrain = .left("0")
```
### 多个 State 发送改变时,SwiftUI 要怎么变化?
我的猜测:在当前 `RunLoop` 中,收到多个 State 的改变,在该 `RunLoop` 结束时用最后一个 State 进行计算。
### `Publisher` 可以发布的事件类型
* 类型为 Output 的新值:这代表事件流中出现了新的值。
* 类型为 Failure 的错误:这代表事件流中发生了问题,事件流到此终止。
* 完成事件:表示事件流中所有的元素都已经发布结束,事件流到此终止。
我们将最终会终结的事件流称为**有限事件流**,而将不会发出 failure 或者 finished 的事件流 称为**无限事件流**。
Publisher 的结束事件有两种可能:代表正常完成的 `.finished` 和代表发生了某个错误的 `.failure`,两者都表示 Publisher 不再会有新的事件发出。
### 多个 `Publisher` 怎么办?
通过一系列组合,我们可以得到一个响应式的 `Publisher` 链条:当链条最上端的 `Publisher` 发布某个事件后,链条中的各个 `Operator` 对事件和数据进行处理。在链条的末端我们希望最终能得到可以直接驱动 UI 状态的事件和数据。
### `sink`
可以通过 `sink` 订阅 Publisher 事件。Subscriber 可以指定想要接收的新值的个数,这不仅在订阅初期可以通过 `Subscription.request` 来告知 Publisher,也可以通过 `Subscriber.receive` 返回特定的 `Subscribers.Demand` 值来指定接下来能够处理的值的个数。通 过这些机制,Combine 将可以实现背压 (Backpressure)。`.unlimited` 表示不设上限,但当上游 Publisher 的值生产速度大于下游的消费速度时,下游的缓冲区就会发生溢出。指定合适的背压策略,通过控制上限,可以让系统下游不发生崩溃的同时,有机会对部分溢出事件做额外处理 (比如丢弃或者告 知上游不要再接受新的事件)。
在客户端开发中,需要处理背压的场景非常有限,但是在服务端开发处理大 规模数据时,这会是无法绕过机制。
### `assign`
通过 `assign` 绑定 Publisher 值。Combine 里还有另一个内建的 Subscriber: `Subscribers.Assign`,它可以用来将 Publisher 的输出值通过 key path 绑定到一个 对象的属性上去。
* 注意 assign 所 接受的第一个参数的类型为 ReferenceWritableKeyPath,也就是说,只有 class 上 用 var 声明的属性可以通过 assign 来直接赋值。
* assign 的另一个 “限制” 是,上游 Publisher 的 `Failure` 的类型必须是 `Never`。如果 上游 Publisher 可能会发生错误,我们则必须先对它进行处理,比如使用 `replaceError` 或者 `catch` 来把错误在绑定之前就 “消化” 掉。
### Subject
`sink` 提供了由函数响应式向指令式编程转变的露肩的话,`Subject` 则补全了这条通路的另一侧:它让你可以将传统的指令式异步 API 里的事件和信号转换到响应式的世界中去。
Combine 中内置提供了两种常用的 `Subject` 类型:`PassthroughSubject` 和 `CurrentValueSubject`。
* `PassthroughSubject` 简单地将通过 `send` 接收到的事件转 发给下游的其他 Publisher 或 Subscriber。
* `CurrentValueSubject` 则会包装和持有一个值,并在 设置该值时发送事件并保留新的值。
### Scheduler
如果说 `Publisher` 决定了发布怎样的 (what) 事件流的话,`Scheduler` 所要解决的就 是两个问题:在什么地方 (where),以及在什么时候 (when) 来发布事件和执行代码。
* Combine 里提供了 `receive(on:options:) ` 来让下游在指定的线程中接收事件。
* `RunLoop` 就是一个实现了 `Scheduler` 协议的类型,它知道要如何执行后续的订阅任务。
* 比较常见的两种操作是 `delay` 和 `debounce`。`delay` 简单地将所有事件按照一定事件 延后。`debounce` 则是设置了一个计时器,在事件第一次到来时,计时器启动。在计 时器有效期间,每次接收到新值,则将计时器时间重置。当且仅当计时窗口中没有新 的值到来时,最后一次事件的值才会被当作新的事件发送出去。主要的一个运用场景:用户键入内容时,实时地给出搜索结果,可使用 `debounce` 进行 1s 的延时操作。
* 它们都是 Publisher 上的扩展方 法,并返回一个新的 Publisher。
### `Operator`
在 `Publisher` 上也存在一个 `map` 函数,我们可以通过类似的方式,对 output 的元素进行变形:
```swift
check("Map") {
// " 注意我们是在 `Publisher` 上调用了 `map`
[1,2,3]
.publisher
.map{$0*2}
}
```
* 经过 `reduce` 变形后,新的 `Publisher` 只会在接到上游发出的 `finished` 事件后,才会将 `reduce` 后的结果发布出来。
### `scan`
类一边进行重复操作,一边将每一步中间状态发送出去的场景十 分普遍,因此 `Combine` 内置提供了 `scan` 这个 `Operator`。
`scan` 一个最常见的使用场景是在某个下载任务执行期间,接受 `URLSession` 的数据 回调,将接收到的数据量做累加来提供一个下载进度条的界面。
### 为 `Array` 标准库添加 `scan` 操作
有些情况下,除了最终的结果,我们也有可能会想要把中途的过程保存下来。在 `Array` 中,这种操作一般叫做 `scan`。这个方法在标准库中并不存在,不过我们可以很 容易地添加一个:
```swift
extension Sequence {
public func scan(
_ initial: ResultElement,
_ nextPartialResult: (ResultElement, Element) !" ResultElement ) !" [ResultElement] {
var result: [ResultElement] = []
forxinself{
result.append(nextPartialResult(result.last ?? initial, x))
}
return result
}
}
```
调用该方法的方式和 reduce 几乎相同:
```swift
[1,2,3,4,5].scan(0, +)
// " [1, 3, 6, 10, 15]
```
### `compactMap`
它的作用是将 `map` 结果中那些 `nil` 的元素去除掉,这个操
作通常会 “压缩” 结果,让其中的元素数减少。
```swift
["1", "2", "3", "cat", "5"]
.publisher
.compactMap { Int($0) }
```
直接使用 Swift 进行函数式编程是这样的:
```swift
["1", "2", "3", "cat", "5"]
.publisher
.map { Int($0) } .filter { $0 !" nil } .map { $0! }
```
### `flatMap`
`flatMap` 的变形闭包里需要返回 一个 `Publisher`。也就是说,`flatMap` 将会涉及两个 `Publisher`:一个是 `flatMap` 操作本身所作用的外层 `Publisher`,一个是 `flatMap` 所接受的变形闭包中返回的内层 `Publisher`。flatMap 将外层 Publisher 发出的事件中的值传递给内层 `Publisher`,然 后汇总内层 `Publisher` 给出的事件输出,作为最终变形后的结果。
### `removeDuplicates`
```swift
["S", "Sw", "Sw", "Sw", "Swi","Swif", "Swift", "Swift", "Swif"]
.publisher
.removeDuplicates()
```
上例中,“Sw” 连续出现了三次,“Swift” 出现了两次,而经过移除操作后,我们得到 的是一系列没有重复的字符串事件。`removeDuplicates` 经常被用来减少那些非常消 耗资源的操作,比如由事件触发造成的网络请求或者图片渲染。如果当作为源头的 数据没有改变时,所预期得到的结果也不会变化的话,那么就没有必要去重复这样 操作。在源头将重复的事件移除,可以让下游的事件流也变得简单。
### 错误类型不一致转换
`map` 对 Output 进行转换,`mapError` 对 Failure 进行转换。
可以对各种 Operator 加上 `try`,如 `tryMap`,`tryReduce` 等,当你有需要在数据转换或者处理时,将事件流以错误进行终止,都可以使用对应操作的 `try` 版本来进行抛出,并在订阅者一侧接收到对应的错误事件。
### 错误替换
在 Combine 里,有一些 Operator 是专门帮助事件流从错误中恢复的,最简单的是 `replaceError`,它会把错误替换成一个给定的值,并且立即发送 `finisheds` 事件。
### `Just`
如果我们想要 publisher 在完成之前发出一个值的话,可以使用 `Just`,它表示一个单一的值,在被订阅后,这个值会被发送出去,紧接着是 `finished`。
### 使用 `merge` 整个事件流
### `zip`
`zip` 将从两个序列中取出 `index` 相同的元素,把它们组合为**多元组**,然后放到返回的序列中去:
```swift
zip([1, 2, 3, 4, 5], ["A", "B", "C", "D"])
// [(1, "A"), (2, "B"), (3, "C"), (4, "D")]
```
`zip` 在时序语义上更接近于 “当...且...”,当 Publisher1 发布值,且 Publisher2 发布值时,将两个值合并,作为新的事件发布出去。在实践中,`zip` 经常被用在合并多个 异步事件的结果,比如同时发出了多个网络请求,希望在它们全部完成的时候把结 果合并在一起。
### `combineLatest`
`combineLatest` 是一个很典型的例子,和 `zip` 相对,它的语义接近于 “当...或...”,当 Publisher1 发布 值,或者 Publisher2 发布值时,将两个值合并,作为新的事件发布出去。
combineLatest 被用来处理多个可变状态,在其中某一个状态发生变化时,获取这些全部状态的最新值。比如你的 UI 上有多个 `TextField`,你想要在其中某 一个值变动时获取到所有 `TextField` 中的值进行检查,例如在**用户注册**时。
### `Future`
`Future` 只能为我们提供一次性 Publisher:对于提供的 promise,你只 有两种选择:发送一个值并让 Publisher 正常结束,或者发送一个错误。因此, Future 只适用于那些必然会产生事件结果,且至多只会产生一个结果的场景。比如网络请求:它要么成功并返回数据及响应,要么直接失败并给出 `URLError`。一个 `dataTask` 的网络请求不会永远不发送任何事件,也不会产生多次的 响应,用 Future 进行包装恰得其所。如果你的异步 API 有可能不发送任何一个值,而是可能发布两个或更多的值的话,你会需要一个更加一般性的 Publisher 类型来把指令式程序转换为响应式程序。
### 对于多个 Subscriber 对应一个 Publisher 的情况
如果我们不想让订阅行为反复发 生 (比如上例中订阅时会发生网络请求),而是想要共享这个 Publisher 的话,使用 share() 将它转变为引用类型的 class。
### `throttle`
它在收到一个事件后开始计时,并忽略计时周期内的后续输入。
### 每一个 `@State` 都是一个数据源
### * 当 `Text` 布局约束太小,以至于产生了异常的内容缩减
例如缩减了尾部文字,但我们却想要缩减头部文字,此时可以使用 `View` 的 `layoutPriority(1)` 方法从默认的 0 更改为 1。
### 如果想要 `Image` 和 `Text` 进行同一个基线进行对齐
* 可以对 `Image` 使用 `.alighnmentGuide` 自定义基线距离。
* 文本基线对齐的方法:`HStack(alignment: .lastTextBaseline) {}`


### 图形绘制
当需要绘制大量的内容时,比如一组图形或一组文字,可以使用 `.drawingGroup` 进行,开启后,将会把绘制任务丢到 `Metal` 里使用 GPU 进行绘制加速。

## 参考资料
* [100 Days of SwiftUI](https://www.hackingwithswift.com/100/swiftui)
* [ChaoCode - Youbute](https://www.youtube.com/@ChaoCode)
================================================
FILE: iOS/Swift/Swift注意点.md
================================================
# Swift
这篇文章主要记录我在学习Swift的一些记录、Swift是14年的WWDC上苹果推出的一门新语言,这是一门非常新的语言,而且在不停的发展当中,对新手非常的友好,可以断定的是Swift将来一定是苹果推的主流开发语言。Objective-C在五年不会消亡,因为OC的强大不是Swift能一时半会取代的,或者说将来会大量存在由OC和Swift混编的项目吧(虽然说现在也有,但还是比较少,估计以后都会这样了)。
以下内容为 Swift 学习过程中需要注意的地方,当前版本为 4.2 ,来源于 [iOSCaff](https://ioscaff.com) 社区,欢迎大家一同来玩耍。
## Swift 4.2 概览
* Swift 的不需要为了输入/输出或者字符串处理而去导入一个单独的库;
* 全局作用域中的代码会自动作为程序的入口,所以你并不需要 `main()` 函数;
* 不需要在句尾写分号;
### 简单值
* `let` 声明常量,`var` 声明变量。常量在编译时不需要赋初值,但后续只能对它赋值一次。
* 你不用明确的声明类型,因为编译器会根据你所创建的常量或变量来推断它们的类型。
* 对于占用多行的字符串可使用三个引号 `"""` ,如:
```Swift
let quotation = """
I said "I have \(apples) apples."
And then I said "I have \(apples + oranges) pieces of fruit."
"""
```
* 使用 `[]` 创建空数组,使用 `[:]` 创建空字典。
### 控制流
* 在 `if` 语句中,条件语句必须是布尔表达式,所以 `if score {...}` 等类似的代码将会报错,而且不会与 0 做隐式的比较,也就是说在 `OC` 中经常用于判断一个变量是否存在的写法在 `Swift` 中要使用 `if` 和 `let` 来结合处理值缺失的情况,如下所示:
```Swift
if let s = score {
printf(s)
}
```
* 处理可选值还可以使用 `??` 加入 **默认值** 的做法,如下所示:
```Swift·
let score: String? = nil
let myScore = score ?? "zore"
```
* 运行完 `switch` 语句中与 `case` 匹配的代码后,程序会直接从 `switch` 语句退出,下一个 `case` 语句不会被执行;
### 函数和闭包
* 使用元组来生成复合值,例如使用元组来让一个函数返回多个值。该元组的元素可以通过名称或数字还获取,例如:
```Swift
func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
var min = scores[0]
var max = scores[0]
var sum = 0
for score in scores {
if score > max {
max = score
} else if score < min {
min = score
}
sum += score
}
return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
print(statistics.2)
```
* 函数其实是一种特殊的闭包,它是可以延后执行的一段代码。在闭包里的代码可以访问到闭包作用域范围内的变量和函数(即使是在不同的作用域执行)。可以通过使用 `{}` 来创建一个 **匿名闭包** ,使用 `in` 将参数和返回值类型与闭包函数体分离,如下所示:
```Swift
numbers.map({ (number: Int) -> Int in
let result = 3 * number
return result
})
```
* 当我们一直一个闭包的类型,比如作为一个代理的回调,可以完全忽略参赛、返回值,甚至两个都忽略,单个语句闭包会把它的语句值当做结果返回,如下所示:
```Swift
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)
```
* 还可以通过参数位置而不是参赛名字来引用参数(在短闭包方法中非常有用)。当一个闭包作为最后一个参数传给一个函数时,它可以 **直接跟在括号后面** 。当一个闭包是传给函数的唯一参数时,则可以 **完全忽略括号** :
```Swift
let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)
```
### 协议和拓展
* 与 `OC` 不同的是,结构体和枚举可以拥有方法,其中方法也可以为实例方法,可以为类方法。虽然结构体和枚举可以定义自己的方法,但是默认情况下,实例方法中是不能修改值类型的属性的,为了能够在实例方法中修改属性值,可以在方法定义前添加关键字 `mutating` ,如下所示:
```Swift
struct Point {
var x = 0, y = 0
mutating func moveXBy(x:Int,yBy y:Int) {
self.x += x
self.y += y
}
}
var p = Point(x: 5, y: 5)
p.moveXBy(3, yBy: 3)
```
* 使用 `extension` 可以为现有类型添加功能,例如新方法和计算属性。可以使用拓展将协议一致性添加到其它地方声明的类型,甚至是从其它库或者框架导入的类型:
```Swift
extension Int: ExampleProtocol {
var simpleDescription: String {
return "The number \(self)"
}
mutating func adjust() {
self += 42
}
}
print(7.simpleDescription)
```
### 错误处理
* 使用 `defer` 来处理函数执行完毕后需要处理的事情,无论这个函数是否抛出异常,该部分代码都会被执行。
```Swift
func fridgeContains(_ food: String) -> Bool {
fridgeIsOpen = true
defer {
fridgeIsOpen = false
}
let result = fridgeContent.contains(food)
return result
}
```
### 泛型
* 在类型名称后紧接 `where` 来明确表示一系列需求——例如,要求类型实现一个协议,要求两个类型必须相同,或者要求类必须继承来自特定的父类。
```Swift
func anyCommonElements(_ lhs: T, _ rhs: U) -> Bool
where T.Iterator.Element: Equatable, T.Iterator.Element == U.Iterator.Element {
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}
anyCommonElements([1, 2, 3], [3])
```
## 基础
### 类型注解
* 我们可以在一行中定义多个相同类型的变量,使用逗号来分割,在最后的变量名后面加上一个类型注解:
```Swift
var red, green, blue: Double
```
### 命名常量和变量
* 如果我们需要使用 Swift 预留关键字来命名常量或变量时,用反引号 `(``)` 包围关键字,但最好不要这么做。
### 打印常量与变量
* 可以通过 `print(_:separator:terminator:)` 来打印一个常量或变量当前的值,该方法默认会添加一个换行符来终止它打印的行,如果我们想要一个没有换行符的值,可以这么写 `print(someValue, terminator: "")` 。
## 基本运算符
### 算术运算符
* 与 C 以及 Objective-C 不同的是,在 Swift 中默认情况下算术运算符不允许值溢出。但我们能通过 Swift 的溢出符号加入值溢出的行为:
符号 | 作用
--- | ---
&+ | 溢出加法
&- | 溢出减法
&* | 溢出乘法
&/ | 溢出除法
&% | 溢出求模
### 空合运算符 ??
其中有个写法需要非常有趣,利用三元运算符进行解包
```Swift
let c = (a != nil ? a! : b)
```
这种写法相当于 `??` :
```Swift
let c = a ?? b
```
### 单侧区间
如果想要从数组中遍历出从索引为 2 的下标到结尾的所有元素,可以这么写:
```Swift
for name in names[2...] {
print(name)
}
```
如果想要从数组中遍历从开始至倒数第二个元素,可以这么写:
```Swift
for name in names[...2] {
print(name)
}
```
## 数组
在 Swift 中把 OC 的 `NSArray` 和 `NSMutableArray` 都统一成了 `Array` ,看起来好像就一种数据结构,但实际上它的实现有三种:
* `ContiguousArray`:效率最高,元素分配在连续的内存上。如果元素的是值类型(zhan shang),则 Swift 会自动调用 Array 的这种实现;如果注重效率推荐声明这种类型,尤其当元素大量是类类型时。
* `Array`: 会自动桥接到 OC 的 `NSArray` 上,如果是值类型,则其性能与 `ContiguousArray` 无差别。
* `ArraySlice`:它不是一个新的数组,只是一个片段,在内存上与原数组享用同一区域。
关于数组的简单的操作:
```Swift
// 声明并初始化重复值
let nums = [Int](repeating: 0, count: 5)
// 对数组进行升序排序
nums.sort()
// 对数组进行降序排序
nums.sort(by: >)
```
用数组实现栈:
```Swift
class Stack {
var stack: [Any]
var isEmpty: Bool { return stack.isEmpty }
var peek: Any? { return stack.last }
init() {
stack = [Any]()
}
func push(object: Any) {
stack.append(object)
}
func pop() -> Any? {
if !isEmpty {
return stack.removeLast()
} else {
return nil
}
}
}
```
注意一个操作 `reserveCapacity()` 。它为原数组预留空间,防止数组在增加或删除元素时反复申请内存空间或是创建新数组,适合用于创建和 `removeAll()` 时进行调用。以上这段代码还引入了一个新的问题,`Any` 和 `AnyObject` 有什么区别?官方编程指南中指出:
> AnyObjct 可以代表任何 class 类型的实例
> Any 可以表示任意类型,甚至方法(func)类型
在 OC 中有个 `id` 类型,编译器不会对声明为 `id` 类型的变量进行类型检查,因为它可以表示任意类型。在 Cocoa 框架中很多地方都使用了 `id` 来进行如参数传递和方法返回的操作,这是 OC 动态特性的一种表现,但现在的 Swift 主要还是使用 Cocoa 框架进行 iOS app 开发,因此为了与 Cocoa 框架协作,将原来的 `id` 类型使用了一个可以表示任意 class 类型的 `AnyObject` 类型进行替代。
但 `id` 和 `AnyObject` 是有区别的。在 Swift 中编译器不仅不会对 `AnyObject` 进行实例的方法调用做检查,甚至会对 `AnyObject` 的所有方法调用都返回 `Opitional` 结果,因为这是符合 OC 理念的,但在 Swift 中却会很危险,应该先确定 `AnyObjct` 真正的类型并进行转换以后再进行调用,如下代码所示:
```Swift
func someMethod() -> AnyObject? {
// ...
// 返回一个 AnyObject?,等价于在 Objective-C 中返回一个 id
return result
}
let anyObject: AnyObject? = SomeClass.someMethod()
if let someInstance = anyObject as? SomeRealClass {
// ...
// 这里我们拿到了具体 SomeRealClass 的实例
someInstance.funcOfSomeRealClass()
}
```
所有的 class 都隐式的实现了 `AnyObject` 接口,这也就是为什么只适用于 class 类型的原因,但在 Swift 中所有的基本数据类型,比如 `Array` 、 `Dictionary` 在 OC 中为 class 的类型却通通都是 `struct` 类型,所以应该用 `Any` 类型进行表示,它除了能够表示 class 外,还可以表示 `struct` 和 `enum` 在内的所有类型。再来举个例子:
```Swift
let swiftInt: Int = 1
let swiftString: String = "miao"
var array: [AnyObject] = []
array.append(swiftInt)
array.append(swiftString)
```
在这段代码中,`swiftInt` 实际上的类型为 `NSNumber`,`swiftString` 实际的类型为 `NSString` ,因为在 Swift 和 Cocoa 中的这几个类型是可以自动转换,我们显式地声明了需要 `AnyObject` ,编译器认为我们需要的是 Cocoa 类型而非原生类型,帮我们进行了自动的转换。如果我们这么做:
```Swift
let swiftInt: Int = 1
let swiftString: String = "miao"
var array: [Any] = []
array.append(swiftInt)
array.append(swiftString)
array
```
这就拿到了 Swift 的原生数据类型 `Int` 和 `String`,值得一提的是如果我们只使用 Swift 类型而不转为 Cocoa 类型,性能将会得到提升,所以应该尽可能的使用原生类型。但不要在代码中出现太多次,如果 `Any` 和 `AnyObject` 在代码中出现了很多次,这说明设计上出了问题,可以通过**泛型**做改造。
## 字典和集合
字典和集合(专指 `HashSet`)经常被使用很重要的一点查找数据的时间复杂度为 **O(1)** 。字典和集合要求它们的 `Key` 都必须遵守 `Hashable` 协议,Cocoa 中的基本数据类型都满足这一点。自定义的 class 需要实现 `Hashable` 而又因为 `Hashable` 是对 `Equable` 的拓展,所以还要重载 `==` 操作符。
一些关于字典和集合的使用操作:
```Swift
let primeNums: Set = [3, 5, 7, 9]
let oddNums: Set = [1, 3, 5, 6]
/// 交集
/// Intersection()的操作不影响原集合,而 `formIntersection()` 则会已影响原集合。
print(primeNums.intersection(oddNums))
/// 并集
/// union()的操作不影响原集合,而formUnion()则会已影响原集合。
print(primeNums.union(oddNums))
/// 差集
/// subtracting() 不影响原集合,subtract() 影响原集合
print(oddNums.subtracting(primeNums))
// 用字典和高阶函数计算字符串中每个字符的出现概率
Dictionary("hello".map { ($0, 1) }, uniquingKeysWith: +)
```
这块有道非常经典的题目“2Sum”,题目大概是:给出一个整型数组和一个目标值,判断数组中是否有两个数之和等于目标值。这道题我是在 leetcode 上做的,刚开始根本就不知道用 Set ,折腾了很久,到后边慢慢就变好了。还有个变种题目,还要把当前的下标拿到,此时可以加入字典进行解题。
## 字符串和字符
Swift 的字符串类型于 Foundation 的 `NSString` 类型进行了无缝桥接,我们可以不用经过类型转换,可以直接在 `String` 中调用 `NSString` 的方法。
### 多行字符串字面量
如果我们需要跨越多行的字符串,可以使用多行字符串字面量:
```Swift
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
```
如果文本太长,可以在适当的地方添加上反斜杠:
```Swift
let softWrappedQuotation = """
The White Rabbit put on his spectacles. "Where shall I begin,\
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on\
till you come to the end; then stop."
"""
```
### 字符串(String)是值类型
Swift 编译器优化了字符串的使用,实际拷贝只会在需要的时候才进行。在 Swift 中字符串不同于其他语言(包括 OC),它是**值类型**而非引用类型。
```Swift·
// 检测字符串是否由数字构成
func isStrNum(str: String) -> Bool {
return Int(str) != nil
}
```
### 可拓展的字形群集 & 字符串索引
在底层,Swift 中的原生 `String` 类型是由 `Unicode 标量` 构造而来的,而 `Unicode` 是一个在不同书写系统中编码,表示和处理文本的国际标准。它使我们能够以一种标准化形式表示几乎任何语言中的任何字符,Swift 中 `String` 和 `Character` 都完全符合 `Unicode` 标准的。
每一个 Swift 的 `Character` 类型代表一个 **可拓展** 的字符集,而拓展字符集由可以由多个不同的 `Unicode` 标量组成,这就意味着相同字符的不同表示需要占据不同的内存空间去存储,因此,在字符串的各种表示中 Swift 字符占据的内存不并不一样,这样造成的结果就是,字符串的字符数量并不能通过遍历该字符串去计算。
所以,`count` 属性返回的字符个数不会一直都与包含相同字符的 `NSString` 的 `length` 属性返回的字符个数相同,因为 `NSString` 的长度是基于 **UTF-16** 表示的字符串所占据的 16 位代码单元的个数决定,而不是字符串的字符集个数决定。
这也就不难理解,为什么 Swift 必须通过 `beginIndex` 或者 `endIndex` 等索引属性或方法来寻找字符在字符串中的位置,而不能像 `NSString` 那般通过确定的下标直接获取。
### inout
`inout` 关键字是按值传递,然后再写回原变量,而不是按引用传递。
### 给出一个字符串要求将其按照单词顺序进行反转
工程写法:
```Swift
let str = "the sky is blue"
var strArray = str.split(separator: " ")
strArray = strArray.reversed()
for s in strArray {
print("\(s) ", terminator: "")
}
```
书中写法:
```Swift
fileprivate func _reverse(_ chars: inout [T], _ start: Int, _ end: Int) {
var start = start, end = end
while start < end {
_swap(&chars, start, end)
start += 1
end -= 1
}
}
fileprivate func _swap(_ chars: inout [T], _ p: Int, _ q: Int) {
(chars[p], chars[q]) = (chars[q], chars[p])
}
func reverseWord(s: String?) -> String? {
guard let s = s else {
return nil
}
var chars = Array(Substring(s)), start = 0
_reverse(&chars, 0, chars.count - 1)
for i in 0 ..< chars.count {
if i == chars.count - 1 || chars[i + 1] == " " {
_reverse(&chars, start, i)
start = i + 2
}
}
return String(chars)
}
let str = "the sky is blue"
print(reverseWord(s: str) as! String)
```
## 链表
### 链表的基本概念
```Swift
// 链表节点
class ListNode {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
}
// 链表
class List {
var head: ListNode?
var tail: ListNode?
// 头插法
func appendToTail(_ val: Int) {
if tail == nil {
tail = ListNode(val)
head = tail
} else {
tail!.next = ListNode(val)
tail = tail!.next
}
}
// 尾插法
func appendToHead(_ val: Int) {
if head == nil {
head = ListNode(val)
tail = head
} else {
let temp = ListNode(val)
temp.next = head
head = temp
}
}
}
```
有这么一道题目:给出一个链表和一个值 x ,要求将链表中所有小于 x 的值放到左边,所有大于或等于 x 的值放到右边,并且原链表的节点顺序不能变。例如 1 -> 5 -> 3 -> 2 -> 4 -> 2 ,给定 x=3 ,则要返回 1 -> 2 -> 2 -> 5 -> 3 -> 4 。
这道题的难点在于不能改变链表中原节点的位置,所以肯定不能只要一条链表,因为这样代码会写得非常难以理解(要保证原节点位置不变),所以根据书中给出的思路,我们可以先把问题简化,给定一个链表,只保留比给定值小的节点,这个方法写完后,我们再利用同样的思路在 else 分支中补上比给定值的代码即可。
```Swift
unc getLeftList(_ head: ListNode?, _ x: Int) -> ListNode? {
// dummy 相当于是个哨兵,一直盯着头节点看!
let dummy = ListNode(0)
var pre = dummy, node = head
while node != nil {
if node!.val < x {
pre.next = node
pre = node!
}
node = node!.next
}
pre.next = nil
return dummy.next
}
func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
let prevDummy = ListNode(0), postDummy = ListNode(0)
var prev = prevDummy, post = postDummy
var node = head
// 尾插法处理左边和右边
while node != nil {
if node!.val < x {
prev.next = node
prev = node!
} else {
post.next = node
post = node!
}
node = node!.next
}
// 防止构成环
post.next = nil
// 左右拼接
prev.next = postDummy.next
return prevDummy.next
}
let node0 = ListNode(1)·
let node1 = ListNode(5)
node0.next = node1
let node2 = ListNode(3)
node1.next = node2
let node3 = ListNode(2)
node2.next = node3
let node4 = ListNode(4)
node3.next = node4
let node5 = ListNode(2)
node4.next = node5
//var head = getLeftList(node1, 3)
var head = partition(node0, 3)
while head != nil {
print(head!.val)
head = head!.next
}
```
### ===
在 OC 中可以通过 `==` 来进行两个对象指针的判定,在 Swift 中提供的是另一个操作符 `===` ,用来判断两个 `AnyObject` 是否为同一个引用。
### 快行指针
快行指针是解决“链表成环”问题的较好思路,只需耗费 O(1) 空间,O(n) 时间即可。
```swift
func hasCycle(_ head: ListNode?) -> Bool {
var slow = head
var fast = head
while fast != nil && fast!.next != nil {
slow = slow!.next
fast = fast!.next!.next
if slow === fast {
return true
}
}
return false
}
let node0 = ListNode(1)
let node1 = ListNode(5)
node0.next = node1
let node2 = ListNode(3)
node1.next = node2
let node3 = ListNode(2)
node2.next = node3
let node4 = ListNode(4)
node3.next = node4
let node5 = ListNode(2)
node4.next = node0
print(hasCycle(node0))
// true
```
在来看道题:删除链表中倒数第 n 个节点。例: 1 -> 2 -> 3 -> 4 -> 5 , n = 2,返回 1 -> 2 -> 3 -> 5。给定的 n 小于等于链表的长度。
```Swift
func removeNthFromEnd(head: ListNode?, _ n: Int) -> ListNode? {
guard let head = head else {
return nil
}
let dummy = ListNode(0)
dummy.next = head
var prev: ListNode? = dummy
var post: ListNode? = dummy
for _ in 0 ..< n {
if post == nil {
break
}
post = post!.next
}
while post != nil && post!.next != nil {
prev = prev!.next
post = post!.next
}
prev!.next = prev!.next!.next
return dummy.next
}
var node0 = ListNode(1)
let node1 = ListNode(5)
node0.next = node1
let node2 = ListNode(3)
node1.next = node2
let node3 = ListNode(2)
node2.next = node3
let node4 = ListNode(4)
node3.next = node4
let node5 = ListNode(2)
node4.next = node5
var head = removeNthFromEnd(head: node0, 3)
while head != nil {
print(head!.val)
head = head!.next
}
```
-----
1. **mutating**:为了能够在struct和enume中修改方法中修改属性值,可以在方法定义前添加关键字。详见:[https://blog.csdn.net/jeffasd/article/details/55104351](https://blog.csdn.net/jeffasd/article/details/55104351)
2. **@autoclosure**:主要是为了化简闭包嵌套写法,详见:[https://www.jianshu.com/p/99ade4feb8c1](https://www.jianshu.com/p/99ade4feb8c1)
3. Swift注释中的注释写法真的是不停的刷新我的三观。😂。。。详见:[https://blog.csdn.net/ruglcc/article/details/53007850](https://blog.csdn.net/ruglcc/article/details/53007850)和[https://www.jianshu.com/p/eda4a8dc0b3f](https://www.jianshu.com/p/eda4a8dc0b3f)
4. **typealias**:定义别名,我觉得它最好用的地方在于给闭包使用,详见:[https://www.jianshu.com/p/082202b9dc17](https://www.jianshu.com/p/082202b9dc17)
5. **Equatable**:自定义相等协议。不过我觉得貌似也能通过一个方法去搞定?不管怎么说,通过`==`符号更加直观吧,关于使用它的有点和缺点,详见:[https://www.natashatherobot.com/implementing-equatable-for-protocols-swift/](https://www.natashatherobot.com/implementing-equatable-for-protocols-swift/)
6. `Array<[String : String]>`的取key和value问题。
背景:最近在上编译原理的课,第一个实验为编写一个词法分析器,原本我的想法是使用Qt来完成,写C/C++。但是突然一闪,哈!这是使用Swift进行开发的好时机!因此,我就开始了......省略五千字,详细见[这篇文章](../macOS/macOS开发(词法分析器).md)
在开发词法分析器的过程中,我遇到了这么个问题,知道词法分析的同学都能明白,在词法分析阶段需要一个tokens二元组接收词法分析后的结果,因此我还是用了OC中的那套思想,可以用一个`NSMutableArray`,其中装一个个的字典,其实也有相关放的是弄个二维数组,但是觉得套个字典数组可能比较好吧。
但也就是这个“可能比较好吧”的想法,直接导致了这么个坑的出现,发现不管怎么搞,都没法使用自己之前已掌握的能力去筛选出数组中每个字典的key和value。搞到最后,发现如果各位同学也是跟我一样,有`Array`这个格式数据的需求,想要遍历出数组中每个元素的key和value,那么就可以参考下边这种做法:
```Swift
// 给NSTextField刷新上key,row为当前数组的下标,先获取到所有keys,然后取第一个,虽然实际上只有一个
textField.stringValue = Array(tokenArray[row].keys)[0]
//给NSTextField刷新上value
textField.stringValue = Array(tokenArray[row].values)[0]
```
7. Swift中的!和?(解包实在是太恶心的一件事了)[http://www.jb51.net/article/100382.htm](http://www.jb51.net/article/100382.htm),感觉这篇文章是抄的,但是写的内容还算明了。
8. Swift中的值类型和引用类型。[https://www.jianshu.com/p/ba12b64f6350](https://www.jianshu.com/p/ba12b64f6350)
9. Swift中的属性相关。神奇的set和get。[https://www.jianshu.com/p/071024b38a8b](https://www.jianshu.com/p/071024b38a8b)
10. **继承**。对于自定义的类而言,Objective-C的类,不能继承自Swift的类,即要混编的OC类不能是Swift类的子类。反过来,需要混编的Swift类可以继承自OC的类。
11. Swift中的方法选择器#selector还是用到了OC的runtime。😔,还是不够Swifty。
12. 混编项目中,如果你的协议是用的Swift写的,而且其中有`option`方法,那就要在对应的方法前面加上`@ObjC`关键词。
13. 如何用给当前`WKWebView`的request添加`Cookie`?
首先是`setCookie`方法,
```Swift
private func setCookie() {
var ticketCookieProperties = [AnyHashable: Any]()
ticketCookieProperties[HTTPCookiePropertyKey.domain] = "Your hostname"
ticketCookieProperties[HTTPCookiePropertyKey.name] = "Your Cookie.name"
ticketCookieProperties[HTTPCookiePropertyKey.value] = "Your Cookie.value"
ticketCookieProperties[HTTPCookiePropertyKey.path] = "/"
ticketCookieProperties[HTTPCookiePropertyKey.expires] = Date().addingTimeInterval(3600)
let ticketCookie = HTTPCookie.init(properties: ticketCookieProperties as! [HTTPCookiePropertyKey : Any] )
HTTPCookieStorage.shared.setCookie(ticketCookie!)
var usernameCookieProperties = [AnyHashable: Any]()
usernameCookieProperties[HTTPCookiePropertyKey.domain] = "Your hostname"
usernameCookieProperties[HTTPCookiePropertyKey.name] = "Your Cookie.name"
usernameCookieProperties[HTTPCookiePropertyKey.value] = "Your Cookie.value"
usernameCookieProperties[HTTPCookiePropertyKey.path] = "/"
usernameCookieProperties[HTTPCookiePropertyKey.expires] = Date().addingTimeInterval(3600)
let usernameCookie = HTTPCookie.init(properties: usernameCookieProperties as! [HTTPCookiePropertyKey : Any] )
HTTPCookieStorage.shared.setCookie(usernameCookie!)
}
```
接着是`readCurrentCookie`方法,把之前设置全局`Cookie`取出来,
```Swift
private func readCurrentCookie() -> String {
let cookieJar = HTTPCookieStorage.shared
var cookieString = ""
for cookie: HTTPCookie in cookieJar.cookies! as Array {
cookieString = cookieString + "\(cookie.name)=\(cookie.value);"
}
return cookieString
}
```
最后是给当前的`WKWebView`的request添加`Cookie`,
```Swift
var request = URLRequest.init(url: URL(string: requestURL!)!)
// 注入公网所需Cookie
request.addValue(readCurrentCookie(), forHTTPHeaderField: "Cookie")
webView?.load(request)
```
记得使用之前先调用`setCookie()`方法把我们需要的相关`Cookie`给种上。
14. 更换当前App的启动图时,如果运行App时未出现,应该删掉App重装一次,强制删除安装App的缓存。
15. 监听系统音量三部曲:
```Swift
// 1
import AVFoundation
// 2
try! AVAudioSession.sharedInstance().setActive(true)
UIApplication.shared.beginReceivingRemoteControlEvents()
NotificationCenter.default.addObserver(self, selector: #selector(volumeValueChange), name: NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
// 3
// 在对应的方法中通过AVAudioSession来获取音量
var tempVolume = AVAudioSession.sharedInstance().outputVolume
```
16. 想要隐藏系统音量界面,需要在对应VC的`viewDidLoad` or `viewWillAppear`中写下:
```Swift
UIApplication.shared.keyWindow?.insertSubview(MPVolumeView(frame: CGRect.init(x: -2000, y: -2000,
width: 1, height: 1)), at: 0)
```
当然,需求前提得是隐藏,如果你要想自定义,那就不要把该视图移除当前可视区域中,如果我们对该视图啥都不做,那就会只出现一条Slider,拖动该Slider即可调节音量。
17. 要善于使用 `guard` 和 `defer` 进行代码优化。 `guard` 做函数预处理,筛出不能进入函数执行代码块的情况;`defer` 用于处理函数执行完后的变量处理。
### 菱形问题和菱形继承(菱形,也称钻石)
* **菱形继承**:当一个子类继承多个父类时,多个父类最终继承了同一个类,这种情况称为“菱形继承”。会导致比如祖先类的初始化方法中有一个计数器,父类 A 继承了该祖先类,父类 B 也继承类该祖先类,子类 C 则继承了 A 和 B ,则该祖先类的计数器则被计数了两次,若菱形结构重叠,还会计数更多次。
在 C++ 中可以使用 **虚继承** 的方式解决,详情可看 ·[https://www.cnblogs.com/BeyondAnyTime/archive/2012/06/05/2537451.html](https://www.cnblogs.com/BeyondAnyTime/archive/2012/06/05/2537451.html)
* **菱形问题**:多个父类实现了同一方法,子类无法判断继承哪个父类的情况。`Java` 中可用 `interface` 的方式解决, `Swift` 中可用 `protocol` 的方式解决。
18. 从 xib 或 sb 拖拽出来的控件设置为 `weak` 是因为对应的 `view` 已经强引用它了,其生命周期和 `view` 是一致的了,除非 `view` 被释放,否则该控件不会被释放;而代码自定义控件,要设置为 `strong` 。
### `Expression resolves to an unused property`
当我在为自定义相册封装一个易于使用的类在对应的 `ViewController` 中使用时,我的代码是这么写的:
```Swift
PJAlbumDataManager.manager().albums
```
Xcode 给我报了这么一个错 `Expression resolves to an unused property`,此时十分的郁闷,说我这个属性未被使用,开始自查代码,过了好几遍,还是发现没啥问题,记者又去 SO 上翻,突然有人给了这么一个回答:
```Swift
let _ = stockPriceData3[dataIndex]
let _ = stockPriceData4[dataIndex]
```
突然觉得不对劲!然后在 Xcode 中把代码改为了:
```Swift
let r = PJAlbumDataManager.manager().albums
```
居然解决了.....开始思考刚才 Xcode 给我报的错,嗯,确实是“表达式解析出来的值未被使用”,可是我用不用管你 Xcode 啥事啊???
### 计算属性和存储属性
* **计算属性**:执行函数返回其他内存地址.
* 只实现 `getter` 方法的属性被称为**计算属性**,等同于 `OC` 中的 `readOnly` 属性。
* 可以这么简写:
```Swift
var title: String {
return "Mr " + (name ?? "")
}
```
* 不分配独立的存储空间保存计算结果。
* 每次调用时都会被执行。
* **懒加载属性**:
* 在第一次调用时,执行闭包并且分配空间存储闭包返回的数值,会分配独立的存储空间。
* 与 OC 不同的是,lazy 属性即使被设置为 nil 也不会被再次调用。
* **存储属性**:需要开辟空间,以存储数据
### 使用系统自带气泡弹窗
```Swift
private var fontBottomView: UNBottomFontsTableViewController {
get {
let sb = UIStor`yboard(name: "UNBottomFontsTableViewController", bundle: nil)
let fontPopover = sb.instantiateViewController(withIdentifier: "UNBottomFontsTableViewController") as! UNBottomFontsTableViewController;
fontPopover.preferredContentSize = CGSize(width: 200, height: 250)
fontPopover.modalPresentationStyle = .popover
fontPopover.fonts = self.textFonts
let fontPopoverPVC = fontPopover.popoverPresentationController
fontPopoverPVC?.sourceView = self.bottomCollectionView
fontPopoverPVC?.sourceRect = CGRect(x: bottomCollectionView!.cellCenterXs[0], y: 0, width: 0, height: 0)
fontPopoverPVC?.permittedArrowDirections = .down
fontPopoverPVC?.delegate = self
fontPopoverPVC?.backgroundColor = .white
return fontPopover
}
}
```
* 气泡弹窗本质上是个 `UIViewController`;
* 每次显示都需要重新创建;
### Codable 协议需要注意的地方
* `Int`,`String` 等基本数据类型都遵循了 `Codable` 协议;
* 想要 `enum` 类型也遵循 `Codable` 协议,则需要声明为具有原始值的形式,并且原始值的类型需要支持 `Codable` 协议:
```swif
enum StickerType: Int, Codable {
case image = 0
case icon
case text
}
```
### `MemoryLayout` 使用
`MemoryLayout` 是 `Swift 3.0` 推出的一个内存查看工具,使用方法如下:
```swift
import Foundation
class PJPet {
static let shared = PJPet()
}
extension PJPet {
struct Pet: Codable {
/// 宠物标识符
var pet_id: Int
/// 宠物昵称
var nick_name: String
/// 宠物类型:0 = 猫,1 = 狗
var pet_type: Int
/// 体重
var weight: Int
/// 绝育状态
var ppp_status: Int
/// 感情状态
var love_status: Int
/// 生日
var birth_time: Int
/// 性别
var gender: Int
/// 品种
var breed_type: String
/// 创建时间
var created_time: Int
}
}
MemoryLayout.size(ofValue: PJPet.shared) // 8
MemoryLayout.stride(ofValue: PJPet.shared) // 8
MemoryLayout.size // 96
MemoryLayout.size // 8
MemoryLayout>.size // 9
struct Point {
var a: Double?
var b: Double
}
MemoryLayout.size // 24
struct Point2 {
var a: Double
var b: Double
}
MemoryLayout.size // 16
```
`Point` 和 `Point2` 两个结构体会差 8 个字节的原因是因为在 `Point` 结构体中的 `a` 为可选类型。因为内存对齐的原因,当 `a` 为可选类型时,占用 9 个字节,导致下一块内存区域不满 8 个字节,`b` 将继续移动寻找另外一个 8 字节区域,导致最终耗费 24 个字节。
在 `Swift` 中
### Swift 中所有的类都是**引用类型**
### eval
Swift 中有一个类似 eval 的东西,能够自动解析字符串作为代码运算,[可见文章](http://etrex.blogspot.com/2015/06/swift-js-eval.html)
### UICollectionView
在 iOS 9 和 iOS 10 在生命周期的调用上是不一样的,在 iOS 10 上提供了更加流畅的操作。
* UITableView 和 UICollectionView 在 iOS 10 中都提供了预加载
* UICollectionView 实现拖动时只需要多实现几个方法
```swift
class UICollectionView : UIScrollView {
func beginInteractiveMovementForItem(at indexPath: NSIndexPath) -> Bool
func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint)
func endInteractiveMovement()
func cancelInteractiveMovement()
}
```
* 如果使用的是 UICollectionViewController 会更加的简单,只需要开启一个属性即可
```swift
class UICollectionViewController : UIViewController {
var installsStandardGestureForInteractiveMovement: Bool
}
```
* 还可以通过 `collectionView.isPagingEnabled = true` 来增加分页
### 安全区域改变时新增的回调方法
```objc
UIViewController中新增:
- (void)viewSafeAreaInsetsDidChange;
UIView中新增:
- (void)viewSafeAreaInsetsDidChange;
```
### 判断用户设备种类,不要用户设备型号,可以通过 `UIUserInterfaceIdiom` 进行。
### 在 pod 时直接使用二进制包能够加快打包速度。对比源码依赖,二进制依赖的组件只需要进行链接而无需编译。
### Swift 5.0 省略 `return`
在 `Swift 5.0` 之前,如果闭包中只有一个表达式,还是得需要写 `return` 返回语句,但在 `Swift 5.0` 之后,可以省略。
```swift
// before swift 5.0
struct Rectangle {
var width = 0.0, height = 0.0
var area1: Double {
return width * height
}
func area2() -> Double {
return width * height
}
}
// after switft 5.1
struct Rectangle {
var width = 0.0, height = 0.0
var area1: Double { width * height }
func area2() -> Double { width * height }
}
```
### 结构体变得更加智能
在 `Swift 5.0` 之前的结构体声明时如果都对各个属性给了默认值,要按照需要写出便捷构造方法,但在 `Swift 5.0` 之后,编译器可以智能的根据调用的方法自动推断。
```swift
struct Dog {
var name = "Generic dog name"
var age = 0
}
let boltNewborn = Dog()
let daisyNewborn = Dog(name: "Daisy", age: 0)
// before swift 5.0 ❎
let benjiNewborn = Dog(name: "Benji")
// after switft 5.1 ✅
let benjiNewborn = Dog(name: "Benji")
```
### 值类型和引用类型
当你设计一个数据结构时,优先选择结构体或者枚举,它们都是值类型,值类型具有以下几个有点。
* 值类型在**栈上分配**,性能要远远大于引用类型,且 Swift Runtime 有 COW 优化。
* 值类型**没有引用计数**,不会引起奇怪的多线程安全问题。
* 值类型的存储属性是**扁平化**的,避免在类继承情况下一个子类继承过多的存储属性导致实例在内存中过大,如 SwiftUI 使用 `Modifier` 的结构体优化设计。
那什么时候我们才需要使用引用类型呢? 只有当以下几个场景存在时才有必要使用。
* 你需要引用计数和构造和析构的时机。
* 数据需要集中管理或共享,如单例或者数据缓存实例等。
* ID 语义和 Equal语义冲突的时候。
### 判断 `UIScrollView` 相关类是否滑动到底部
```swift
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentSize.height - scrollView.contentOffset.y <= scrollView.pj_height {
// YES
} else {
// NO
}
}
```
### 调用一个有返回值的方法,但是允许外部不接收该返回值
可以使用 `@discardableResult` 来消除警告。
### Swift 支持的 landmark
```swift
// MARK: Section mark
// MARK: - Section mark with a separator line
// FIXME: Fix this code
```
### Swift 一些好玩的点:
* let intValue = 0007 // 7
* let largeIntValue = 77_000 // 77000
### Swift 元组的使用
```swift
// 不带名字,直接通过下标去取
// Function that returns multiple items in a tuple
func getGasPrices() -> (Double, Double, Double) {
return (3.59, 3.69, 3.79)
}
let pricesTuple = getGasPrices()
let price = pricesTuple.2 // 3.79
// Ignore Tuple (or other) values by using _ (underscore)
let (_, price1, _) = pricesTuple // price1 == 3.69
print(price1 == pricesTuple.1) // true
print("Gas price: \(price)")
// 通过给返回的元组命名,通过名字来取对应的数值
// Labeled/named tuple params
func getGasPrices2() -> (lowestPrice: Double, highestPrice: Double, midPrice: Double) {
return (1.77, 37.70, 7.37)
}
let pricesTuple2 = getGasPrices2()
let price2 = pricesTuple2.lowestPrice
let (_, price3, _) = pricesTuple2
print(pricesTuple2.highestPrice == pricesTuple2.1) // true
print("Highest gas price: \(pricesTuple2.highestPrice)")
```
### Swift 结构体中有列表,可以自定义下表存取方法
```swift
// Structures and classes have very similar capabilities
struct NamesTable {
let names: [String]
// Custom subscript
subscript(index: Int) -> String {
return names[index]
}
}
// Structures have an auto-generated (implicit) designated initializer
let namesTable = NamesTable(names: ["Me", "Them"])
let name = namesTable[1]
print("Name is \(name)") // Name is Them
```
### `==` 和 `===`
Swift 用 `===` 来确定两个比较的对象是否为同一个对象,引用的同一个指针地址。`==` 比较的是值本身
## 获取当前视图上的 `UIView` 截图
调用 `toContainer.snapshotView(afterScreenUpdates: false)` 方法即可。
================================================
FILE: iOS/Swift/UIDynamic.md
================================================
## UI Dynamic
这篇文章中将记录我在使用 UI Dynamic 功能进行编码时遇到的问题。
### 简介
在使用 UIDynamic 时需要有一个全局的动画者,把我们需要发生的动态行为 `UIGravityBehavior` 进行统一管理,其实这跟做游戏也是一样的,首先需要创建出一个物理世界,然后在物理世界中加入我们要发生物理现象的节点,这些节点之间就会因为叠加上去的物理属性从而在相互影响时,因为叠加上去的物理属性,比如重力、弹力、摩擦力等等发生影响,从而模拟出真实物理世界中的效果。
### 创建一个动画者
因为这是管理全局动态的变量,所以它的生命周期要充满整个 vc ,相当于需要一个类内全局变量。动画者提供动力学相关的能力,同时还提供一个管理上下文。
`private var animator: UIDynamicAnimator?`
### 创建仿真行为
总共提供了 `UIGravityBehavior` 、`UICollisionBehavior` 、 `UIAttachmentBehavior` 、 `UISnapBehavior` 、`UIPushbehavior` 以及 `UIDynamicItemBehavior` 。
以下代码是场景一个重力仿真行为的缩略代码:
```Swift
let gravity = UIGravityBehavior()
let collisionBehavior = UICollisionBehavior()
collisionBehavior.collisionMode = .everything
collisionBehavior.translatesReferenceBoundsIntoBoundary = true
```
添加边界代码:
```Swift
collisionBehavior.addBoundary(withIdentifier: NSCopying, from: CGPoint() ,to:CGPoint())
```
仿真行为器可以创建多个,各个仿真行为器管控着不同的行为特性,比如重力仿真行为器 `UIGravityBehavior` 用于模拟重力相关的动力学效果, `UICollisionBehavior` 用于模拟物理世界中的边界相关动力学效果。
### 添加动力学元素
如果我们想要给一个仿真行为器添加动力学元素,则只需要
```Swift
behavior.addItem(entity)
```
### 发生效果
最后,如果我们想要让整套效果运行起来,需要把仿真行为添加到动画者中去,
```Swift
animator.addBehavior(behavior)
```
### 注意
我们需要自己维护谁和谁发生碰撞,也就是说,新增的动力学元素和哪个仿真行为下的动力学元素发生物理效果是需要我们自己维护的。
================================================
FILE: iOS/Swift/code.md
================================================
# 有趣的代码段
### `Bool` 怎么转 `Int`
```swift
return boolValue ? 1 : 0
```
### `Int` 怎么转 `Bool`
```swift
return intValue != 0
// or
(intValue != 0)
```
### Swift 做域
```swift
extension Tracker {
struct SVE {}
}
extension Tracker.SVE {
func xxx() {
}
}
Tracker.SVE.xxx()
```
### `@PropertyWrapper` 如何使用
详见:[https://medium.com/@EvangelistApps/property-wrappers-in-swift-51cee87e2c32](https://medium.com/@EvangelistApps/property-wrappers-in-swift-51cee87e2c32)
简单来说其本质也是方法,只不过这个方法是编译器确认,能够在写代码的时候就调用,加入了检查环节,保证未开始构建时就能够发现问题。
================================================
FILE: iOS/Swift/debug.md
================================================
## Swift 调试
## lldb
OC 代码中可以很方便的直接通过发消息的方式调用私有方法,但 Swift 中却因为静态语言的原因无法直接通过类似 OC 中发消息的方式调用私有方法,但 apple 对 Swift 保留了通过 `perform` 的方式,可以直接通过传入私有方法字符串的方式调用。
```shell
(lldb) po pipController.perform("_ivarDescription")
```
```oc
[foo _ivarDescription]; // 列举了所有的实例变量、类型和值。
[foo _methodDescription]; // 大致和_shortMethodDesctiption一样,但是更加详细还包含了超类的方法;
[foo _shortMethodDescription]; // 列举了所有实例和类方法;
```
================================================
FILE: iOS/Swift/landscapeandportrait.md
================================================
# 横竖屏切换的一些思考
最近在优化负责产品中的横竖屏切换功能,这部分是最开始接手这个产品时的第一个功能,过去了一年多了,有很多架构上不完整的地方,其中最忍受不了的是在 `Notification` 中使用中文作为 `value`。下文将详细的描述了重构该功能时所做的工作。
## 需求来源
横竖屏在一些视频类、游戏类等 app 中非常常见,目前来看横竖屏的展现方式有以下两种:
* 在用户打开“系统转屏”的前提下,提供手动切换横竖屏的能力,例如微信公众号文章;
* 不管用户是否打开“系统转屏”的功能,强制且被动的切换横竖屏,例如视频类 app 的全屏播放。
其实总的来看区别就在于是“主动”还是“被动”的触发,一般来说,在一个 app 中不会同时出现这两种情况,但我所负责的产品中同时出现了这两种场景,所需要做的工作自然也就多了一点。
## 最开始的做法
当时接手时开发这个功能时,我是和另外一个实习生一块搭伙做的。我正式入职时,他已经做了一个星期了,所以在架构上我没有投入多大的精力,只是一直在配合他。在配合的过程中确实是出现了很多问题,比如文章开头中所说的使用中文作为 Notification 的 `value` 等。
最开始的做法简单来说是这样的。在 Xcode 中支持 `Portrait` 和 `LandScape Right`(只需要一个横屏方向),监听 `UIApplicationDidChangeStatusBarFrame` 通知,在接收通知的方法中更新所有相关视图的 `frame`。也就是说,没有使用从 iOS 8 就已经引入的 `size-class` 特性进行设备的横竖屏适配,没有利用 `autoLayout` 而是使用了手写 `frame` 生算坐标横屏和竖屏各一套 `frame`。
问题当然很严重,当我发现这么干下去真的不行时,已经快到发版的时间了......于是,日后的好几个版本都延续了这种做法
## 经过了一段时间
经过一段时间后,那位实习生已经撤了,我开始着手一边跟进需求一边重构代码。在跟进需求时,PM 说出了让我十分震惊的一件事——**并不是所有的页面都需要做横竖屏!!!**
这也就是说,当初花了将近两个多星期的时间把所有页面适配的横竖屏布局代码付出的精力是...是完全没必要的!
================================================
FILE: iOS/Swift/tips.md
================================================
## 小功能实现细节
## 悬浮窗口
类似网易云桌面悬浮单词,本质上都是基于画中画做的,第一种方案是把 view 转视频,按时塞帧,第二种方案是直接获取 pip window 贴自定义 view。
可见:[https://github.com/CaiWanFeng/PiP](https://github.com/CaiWanFeng/PiP)
## 防录屏
系统提供的截图和录屏的通知都是结束之后的。
最佳方案是 DRM,但本地 DRM 太复杂了,而且成本比较大。
目前看了几个投机取巧的方案都是基于 UITextField 开启 isSecureTextEntry 后拿私有视图 _UITextFieldCanvasView 做的,太 trick 了,还在找有没有牛逼的方案
基于 _UITextFieldCanvasView 可见:https://blog.wyan.vip/2024/02/iOS_avoid_screen_capture.html
但系统又提供了 `UIScreen.capturedDidChangeNotification` 这个通知,可以直接拿到是否在录屏时机,且从 iOS 11 开始支持。
```swift
NotificationCenter.default.addObserver(self, selector: #selector(screenCaptureChanged), name: UIScreen.capturedDidChangeNotification, object: nil)
@objc func screenCaptureChanged(notifi: Notification) {
if let screen = view.window?.windowScene?.screen, screen.isCaptured {
captureView.isHidden = false
} else {
captureView.isHidden = true
}
}
```
## 防截图
方案一:监听 UIApplicationUserDidTakeScreenshotNotification 通知,然后去删相册里最新的一张照片,需要相册访问权限。这方案肯定不行。
================================================
FILE: iOS/Swift/七牛图片上传助手.md
================================================
# 七牛图片上传助手
podfile 中需要使用 `use_frameworks!`
```ruby
platform :ios, '10.0'
target 'PIGPEN' do
use_frameworks!
pod 'mob_smssdk'
pod 'Qiniu'
pod 'IQKeyboardManagerSwift'
pod 'Alamofire'
pod 'SwiftyJSON'
pod 'CryptoSwift'
pod 'Schedule'
end
```
================================================
FILE: iOS/Swift/品种选择器总结.md
================================================
# 品种选择器组件开发历程
这是我另外一个项目其中一个组件——品种选择器,因为今天是周六,磨磨唧唧的造出了它。一眼看过去跟现有的通讯录样式和操作方式基本一致,但大家也知道我的尿性,从最开始能用三方组件就用三方到现在能自己写就自己写。
因为前后端都是我自己一个人在做(创业狗就是惨 = =)所以在今天萌生了很多好玩的想法,刚把这个组建弄出来后觉得有必要跟大家分享一些好玩的事情。
## UI
先来看看 UI 样式,
最开始看到设计图时,并不认为这是一个有多少搞头的东西,一直拖到今天。这是最终实现的成果,
## 思考(一)
给到我的文案是个 .docx 格式的文档,如下所示:
之前沟通过了一次,给我按照字母表顺序排好就行了。最开始我的设计非常简单,因为后端是用 python 写的,直接从文件中读出数据,`split` 一下丢入库里就好了,接口直接返回 `id` 和 `zh_name` 即可,遂开干。
## 实践(一)
```python
# 初始化:尽量通过 python shell 调用该方法
def init_dog_breed():
f = open(settings.DOG_BREED_DIR, 'r')
f_str = f.read()
f_str_arr = f_str.split()
for dog_name in f_str_arr:
dog_breed(zh_name=dog_name).save()
f.close()
```
从本地路径读取转化成 `.txt` 文件(本人对直接读 .docx 没把握)后简单的操作下入库完事,这个方法并未暴露在接口中,而且只是第一次初始化数据时需要调用该方法。为了方便后续产品迭代添加宠物品种信息,做了另外一个简单的方法:
```python
# 新增狗品种
def add_dog_breed(breed_name):
dog_breed(zh_name=breed_name).save()
```
当然,也会有猫的,因为基本上差不多就不展开了。接口上这么写:
```python
@decorator.request_methon('GET')
@decorator.request_check_args(['pet_type'])
def get_breeds(request):
pet_type = request.GET.get('pet_type', '')
functions = {
'dog': dog(),
'cat': cat()
}
if pet_type in functions.keys():
json = {
'breeds': functions[pet_type]
}
return utils.SuccessResponse(json, request)
else:
return utils.ErrorResponse('2333', '不支持该物种', request)
# 获取所有狗品种
def dog():
dog_breeds = dog_breed.objects.all()
breeds = []
for breed in dog_breeds:
json = {
'id': breed.pk,
'zh_name': breed.zh_name,
}
breeds.append(json)
return breeds
```
猜测后续产品可能还会引入其它宠物,毕竟现代人对宠物的需求是越来越奇葩了,没有直接 `if-else` ,想用 `switch` ,但发现 python 中并没有 `switch` 语句,查阅一番资料后,发现居然可以用 `key-value` 完成,虽然有些稍许麻烦,但第一次见还可以把键值对玩成这样!
访问对应接口后拿到的 JSON 格式数据如下:
```python
{
"msgCode": 666,
"msg": {
"breeds": [
{
"id": 89,
"zh_name": "拉布拉多寻回犬"
},
{·
"id": 90,
"zh_name": "拉萨犬"
},
{
"id": 91,
"zh_name": "腊肠犬"
},
{
"id": 92,
"zh_name": "兰波格犬"
},
{
"id": 93,
"zh_name": "猎水獭犬"
},
]
}
}
```
一切顺利,看起来不错,开始造客户端 UI。客户端上的实现同样也是比较轻松,一个 `tableView` 的正常渲染流程即可。
数据渲染出来后,脑子已经在快速运转,站起来活动活动,发现肚子有些饿,纠结了一会是食堂呢还是饿了么,最后因为贫穷而选择了食堂。
## 思考(二)
午饭结束后,继续干活。开始做数据分组,思考并发现了问题所在,如果按照上午接口所返回的数据格式去做,那么就需要端上做数据分组,把宠物品种按照 `A~Z` 的顺序放到一个个的 `section` 中,这样不但 iOS 需要做一遍,以后 Android 也要再做一遍,而且极其有可能还是我写,本来我就十分厌烦 Android,多花费一分钟甚至一秒钟都是极其不乐意的。
所有,重新思考接口返回的数据格式。可以确保的是,数据都已经按照字母序排好了,我们只需要对数据做分组,把第一个字的拼音的第一个字母相同的品种归类为一组,最后把所有组都放到一个大的列表中,序列化为 JSON 返回即可完事。
遂又开干!
## 实践(二)
首先给品种模型新增了一个字段 `group` 用于标记所属组别,中途考虑到了不想多增迁移文件,居然脑残的把之前生成的表给删了,导致后边生成迁移文件时对不上,最后又删库重来,真是多此一举 = =。
重新把基本操作都弄完后,改造初始化数据的方法,用到了一个中文转拼音的库 `pinyin`:
```python
# 初始化:尽量通过 python shell 调用该方法
def init_dog_breed():
f = open(settings.DOG_BREED_DIR, 'r')
f_str = f.read()
f_str_arr = f_str.split()
# 删除 array 中的第一个 'A'
del f_str_arr[0]
group = 'A'
for dog_name in f_str_arr:
first_cat_name = pinyin.get(dog_name, format='strip')[0:1].upper()
if first_cat_name != group:
group = first_cat_name
# 切换 group 时跳过
continue
dog_breed(zh_name=dog_name, group=group).save()
f.close()
```
这样清洗过数据后,数据就十分清晰漂亮了:
```shell
+-----+-------+--------------------------------+
| id | group | zh_name |
+-----+-------+--------------------------------+
| 1 | A | 阿富汗猎犬 |
| 2 | A | 阿拉斯加雪橇犬 |
| 3 | A | 爱尔兰梗 |
| 4 | A | 爱尔兰红白雪达犬 |
| 5 | A | 爱尔兰猎狼犬 |
| 6 | A | 爱尔兰软毛梗 |
| 7 | A | 爱尔兰水猎犬 |
| 8 | A | 爱尔兰峡谷梗 |
+-----+-------+--------------------------------+
```
而接口,只需要进行拼接同类数据即可,
```python
# 获取所有狗品种
def dog():
dog_breeds = dog_breed.objects.all()
# 所有种类
breeds = []
# 当前种类名
breed_groups = []
group = "A"
for breed in dog_breeds:
if breed.group != group:
breed_group = {
'group': group,
'breeds': breed_groups,
}
breeds.append(breed_group)
group = breed.group
breed_groups = []
b_group = {
'id': breed.pk,
'zh_name': breed.zh_name,
}
breed_groups.append(b_group)
return breeds
```
这样,客户端就能够拿到已经分组好的数据:
```json
{
"msgCode": 666,
"msg": {
"breeds": [
{
"group": "A",
"breeds": [
{
"group": "T",
"breeds": [
{
"id": 137,
"zh_name": "田野小猎犬"
}
]
},
]
},
{
"group": "W",
"breeds": [
{
"id": 138,
"zh_name": "玩具猎狐梗"
},
{
"id": 139,
"zh_name": "玩具曼彻斯特犬"
},
]
}
]
}
}
```
那客户端接下来要做的事情稍微冗余一些,但不复杂。首先先确定 `tableView.sections` 的值,然后返回 `sectionHeaderView`,接着编写 `cellForRow` 渲染 cell 的方法,依然是正常的 `tableView` 渲染流程。
剩下的就是一些其它 UI 和交互细节上的修修补补了。
## 思考和总结
这次做的这个组件前后端花费的时间比例大约在 7:3,主要时间都花在客户端上,因为是第一次做类似于这种通讯录组件的开发,再加上是周六,让自己的大脑和心情都放松了下来,没有把时间抓得特别紧。
给我最大的收获是最开始只考虑了后端处理数据的便利,而忘了前端处理数据的复杂,到后边转换了思维,用前端的思维对接口格式进行了修改,这一来一回让自己更加明白了前后端配合是才能够把一个东西做好,做到极致。
================================================
FILE: iOS/Swift/自定义NavigationBar.md
================================================
## 自定义 NavigationBar
自定义 NavigationBar 几乎已经存在 99% app 中,也可以说差不多的 app 或多或少都对原生 NavigationBar 做了一些改造工作。在这篇文章中我将总结我在项目中遇到的需要对 NavigationBar 自定义的一些需求总结,同时也会结合目前市面上使用人数较多的 app 进行参考(copy)。
## 默认效果
这是 QQ 中的侧滑返回效果,可以说 QQ 的所有 NavigationBar 都是遵循了苹果的设计规范,都是默认效果,而且这种效果从体验上来讲,也给用户一种非常自然的感受。
### 纯自定义
以上是新浪微博中 NavigationBar 的侧滑返回效果,这个效果也是我之前一直使用的,因为自定义效果非常的好,几乎可以在 NavigationBar 上做任何想做的事情,但也是这段时间才发现,虽然纯自定义 NavigationBar 能够做出一些非常灵活的功能,但是从用户体验上来看,会给人一种两个页面分割开了没有一种联动性的感觉(因为这毕竟还是同一个 NavigationController)。
### 默认 + 自定义
这是微信的 NavigationBar 侧滑返回效,(记不得是哪个版本才加入的这种过渡效果了)给了我一种非常取巧的感觉,不管是从用户体验还是开发层面上都兼顾到了,而且还会给用户一个虽然这两个页面确实是位于一个 NavigationController 上,但实际上这两个页面确实没有联系性很强的的视觉感受。
基本上目前市面上的自定义 NavigationBar 样式就这三种了,就我个人而言,如果我要做的一个从功能上来说比较复杂的,bar 需要承载的内容比较多,甚至加上了渐变、动态 UI 等等需求,我会选择“纯自定义”的方法去做,因为这相当于就是给我们一个 View 自由发挥。如果说要交互占了比较大的一部分,也就是更看重各个组件之间衔接等,其实说白了就是不但想要渐变效果还想要较高的自定义,那我会选择“默认 + 自定义”的方法。最后,如果是时间不够了、做的东西比较简单等,直接用原生的吧,又快又方便,而且用户体验极佳。
### 实现
接下来将分别实现这三种 NavigationBar 返回效果,后续如果在项目中有遇到更多的自定义需求,也会持续更新。
### 纯自定义
就像上文所说的,在过去的时间中,我最开始是直接用了 iOS 原生的 navigationBar 侧滑返回效果,刚开始没理解好 navigationBar 中很多内容,直接上手就干,具体细节大家如果有兴趣的话可以看[这个 repo]()
================================================
FILE: iOS/Today_Extension.md
================================================
## 这篇文章将记录我在进行 Today Extension 开发时遇到的问题
### 自定义视图
一般来说,我们都不会去使用 SB 来搭建界面,几乎都是纯代码的方式直接手撸,因为这样会更加直观一些,而且也更加易于协作,虽然效率上确实会确实一点点。在 Today Extension 的 target 中的代码风格几乎跟平常写的一模一样,甚至可以认为说就是一样的东西,只不过在一些细节上会有所不同,比如自定义视图这个大坑!!!
因为我最近慢慢的在全力往 Swift 上转,所以创建的 Today Extension Target 必然也是 Swift ,必然也会把 target 生成的 SB 给删掉,这一删出问题了,当然 plist 文件入口是肯定该对了的,Run 了十几次,创建了三四个工程,再三确定没有一丝错误,但是就不行,就必须把系统生成的 SB 留着,否则 widget 就会出现 `load enabled`(具体忘了)加载失败,换了 OC 再试一遍,没想到居然 OC 允许删掉 SB让开发者完全纯代码自定义,Swift 就是不行。🙄
### 输入文本
因为 widget 的代码风格和样式跟以往的开发过程完全一样,所以一开始我想让其接受用户输入的键盘事件当然就会直接想到使用 UITextField/UITextView ,但是问题来了,没想到苹果不让这么做,只会出现光标在那一直闪啊闪啊,还在钟颖的[这篇文章](https://zhuanlan.zhihu.com/p/24447089)较为详细的说明了这个问题。最终我的解决办法就是按照钟颖大大的这篇文章中所提供的解决方法,present 出来一个带有 textField 的 VC ,看了看效果只能说还行,还没仔细调。就冲钟颖大大的这篇文章,他的 JSBox 我买定了!而且还要好好玩!!!
================================================
FILE: iOS/UI.md
================================================
# UI
### UIKit 架构图

## 布局
### 界面布局(frame)
* `setNeedsLayout` 做标记,`runloop` 周期内重绘「下次 runloop」更新
* `layoutIfNeeded` 立即重绘做过标记的涂层「马上更新」
* `layoutSubviews` 不能直接调用,要通过 `setNeedsLayout` 间接调用
### 界面布局(auto layout)解释同上
* `setNeedsUpdateConstraints`
*`updateConstraintsIfNeeded`
* `updateConstraints`
### 布局约束计算


### Auto Layout 优化(避免规则扰动)
* 不要删除所有的约束(update 的时候)
* 若是一个静态约束,仅做一次添加操作即可
* 仅改变需要改变的约束
* 尽量不要做删除视图的操作,反之用 hide() 方法替代
* `intrinsicContentSize`
- 如果能够确定 Size,直接返回
- 如果不能,使用 `CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)`
* 不要过度使用 `systemLayoutSizeFitting()`
## 渲染
* CPU 渲染:`CoreGraphics`
* GPU 渲染:`OpenGL ES` 和 `Metal`
* CPU 很杂,可以什么都干
* GPU 只干渲染,还可以开启硬件加速
### CPU
* 对象创建、跳转和销毁
* 布局计算
* 文本计算
* 绘制(drawRect)。不能直接调用,不要滥用,会为视图分配一个寄宿图,寄宿图的像素尺寸等于视图大小 x contentScale 的值
* 图片解码
* 提交位图
### GPU
* 接收提交的纹理
* 纹理渲染
* 视图合成
* 光栅化
### GPU 视图合成
多个子视图需要将视图纹理合成,将多个 View 的纹理拼接在一起,RGB 通道混合
### `drawRect:` 方法是 CPU 计算
### UIView 绘制
渲染是 CPU 和 GPU 共同协助完成的。在 `runloop` 中注册了一个 `Observer`,监听到 `BeforeWaiting` 和 `Exit` 事件进行渲染。

### UIView 渲染时机
Core Animation 在 `RubLoop` 中注册了一个 `Observer`,监听到 `BeforeWaiting` 和 `Exit` 事件进行渲染,`runloop` 的周期是 1/60s。
### 为什么会卡顿?
因为 `runloop` 1/60s 渲染一次,当 CPU 和 GPU 一次渲染耗时超过 16.7ms 那么就会来不及渲染,造成卡顿。

### 卡顿监测
子线程在主线程处于 Before Waiting 时,ping 主线程,如果超时即发生卡顿。监测到卡顿后,获取当前堆栈信息上报(BSBacktraceLogger)
### 卡顿优化
* 尽量使用不使用透明
* 像素对齐
* 避免离屏渲染,造成离屏渲染可能的做法:

### ASDK 的核心
* 尽可能的将主线程的任务挪到异步线程
* 异步任务回调主线程是模型 Core Animation,在 `runloop` 注册 `BeforeWaiting` 或者 `Exit` 事件,优先级比 Core Animation 更低。收到事件后执行提交任务。

### 键盘选择 `return` 时,回收键盘
点击k键盘 `return` 键时,会输入一个 `\n`,然后实现 `textView` 每次内容改变获取改变内容回调的方法,识别 `\n`,取消对应的 `textView` 成为第一响应者。
```swift
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
textView.resignFirstResponder()
return false
}
return true
}
```
### `makeUIView`
该方法是 `SwiftUI` 在使用 `UIKit` 组件时,创建视图所调用。
### `updateUIView`
刚方法时 `SwiftUI` 更新 `UIKit` 组件数据时,自动调用的方法。比如 `UIKit` 组件绑定了 `SwiftUI` 的一个变量 `textString`,当 `SwiftUI` 改变 `textString` 值时,`UIKit` 组件将调用 `updateUIView` 方法。
使用类似 `UITextView` 之类的输入组件可能看不出有什么效果,但如果像使用类似官方那般的 `MapKit` 相关组件时,因为我们并不能把外部的经纬度直接绑定到 `mapView` 的经纬度上,得调用 `mapView` 经纬度的 `setter` 方法,所以需要把这部分 `setter` 方法逻辑写在 `updateUIView` 中。
这样就完成了当外部修改 `SwiftUI` 和 `UIKit` 使用`@Binding` 关键字修饰的关联变量的值时,就会得到正确的修改。
================================================
FILE: iOS/UICollectionView.md
================================================
# UICollectionView 的使用、注意点和优化
## 让 UICollectionView 进行滚动
这是我今天刚遇到的问题,虽然 `UICollectionView` 是继承 `UIScrollView` 的,而且也有 `contentSize` 属性,但是对其设置 `CGSize` 并无效果,因为我已经使用了 `UICollectionViewFlowLayout` 布局风格,当 `cell` 数量超出一屏时,将会自动拓展 `contentSize` ,我们需要做的就是 `collectionView?.alwaysBounceVertical = true`
## `UICollectionView` 执行 `reloadData` 方法时闪烁
在进行 Vary 2.0 开发时,发现了首页的 `UICollectionView` 在滑动时卡片的 `WKWebView` 在重用时会闪烁,一直以为是 `WKWebView` 的缓存问题,纠结了好几天,一直没搞懂 `WKWebView` 为啥会在重用时闪烁,也就是内容会跳动,而跳动的内容是基于之前载入过的内容。
来来回回好几次,尝试各种奇淫巧技都没能搞定 `WKWebView` 的闪烁问题,后来换个思路,开始查 `UITableView` 重用的时闪烁问题,边玩着 Vary 边纠结到底是哪出了问题呢?
突然发现在滑动卡片,并且正要切换时间时,卡片的内容居然都跳动并闪烁了!发现不对劲,开始跟 log,一条一条的 log 跟下来,发现执行了部分 `[collectionView reloadData]` 操作,开始隐约觉得这有问题!继续查阅资料,资料上写明,如果 `UICollectionView` 在执行 `reloadData` 操作时是会进行闪烁的!如果不需要这个闪烁的动画,可以关闭,代码如下所示:
```Objc
- (void)collectionViewReloadData {
useWeakSelf
[UIView setAnimationsEnabled:NO];
[weakSelf.cardListView performBatchUpdates:^{
[weakSelf.cardListView reloadData];
} completion:^(BOOL finished) {
[UIView setAnimationsEnabled:YES];
}];
}
```
这么一来,滑动卡片时卡片再也不跳动闪烁了!哈哈~
## 排除 UICollectionView 有一个前置 UIView 的问题
UIStackView 为什么前面会有一个 UIView 进行遮挡,刚开始以为是自己的代码问题,后来用代码创建了一个 UIView,发现还是有问题。
百思不得其解时,刚才突然发现!在 UICollectionView 中我的 Cell 前面也出现了一个不知道干嘛用的 UIView,瞬间就明白了。
原来是昨天偷懒了,应该创建 UICollectionViewCell,但创建时写成了 UICollectionView,直接改了名字,导致在 xib 中并未使用 UICollectionView 的 ContentView 作为容器,导致最终进行渲染时,在 UICollectionViewCell 的生命周期顺序中并没有其没有任何子视图挂载,导致 UICollectionView 把 ContentView 前置,遮挡了 addSubView 的子视图。
所以可以在 `xib` 中把之前的 `UIView` 控件删除,重新拖拽出一个 `UICollectionViewCell` 进行替换。
================================================
FILE: iOS/UITableView.md
================================================
本文主要总结在日常撸码中,遇到`UITableView`的各种需求demo及各种问题和骚操作集合。持续更新中......
## 差异
### Android
如果你有做过Android,那么`UITableView`就是`ListView`,`ListView`中的`item`对应的就是`UITableView`的`UITableViewCell`,相同点是它们两者都有重用机制,都可供用户进行滑动操作进行数据的展示,而关于不同点从基础的父类等其它方面来看,差异性大于相同性,其中我觉得最大的差别就是显示风格上,`UITableView`有`Group`和`Plain`样式,而`ListView`只有一个`Plain`样式,这两种的模式的区别大家可以打开微信,微信的`微信`和`通讯录`两个tabVC就是`UITableView`的两个风格展示。
### React-Native
React-Native中并没有iOS和Android中传统意义的上的`UITableView`或`ListView`,但真的有`ListView`这个组件,因为是支持`flex`布局,因此在RN中的这个`ListView`可以认为是iOS中的`UITableView`和`UICollectionView`的融合,Android中的`ListView`和`GridView/Layout`(或者其它布局方式)的融合。
关于React-Native的相关内容介绍,可以参考我的[这一系列文章](../项目/React-Native记〇.md)
### 微信小程序
微信小程序中也没有单独的一个控件做列表展示,而用``组件堆积起来后自动延伸成一个列表,而且同样也是支持`flex`布局,因此其实际上也是iOS中的`UITableView`和`UICollectionView`的融合,Android中的`ListView`和`GridView/Layout`的融合。
关于微信小程序的相关内容介绍,可以参考我的[这一系列文章](../项目/小程序初探.md)
## 搭建
在iOS开发中搭建`UITableView`,可以通过的`StoryBoard / Xib`和纯代码的方式进行,一般来说,并不推荐直接使用`StoryBoard / Xib`进行界面搭建,但这有一个的例外,如果你的页面是静态的,比如个人中心,类似微信的`我`,基本上除了每次的版本更新外,没见过动态更新的时候(当然,有可能也会有)。
类似这种页面,我们都可以使用`StoryBoard / Xib`来搭建,而且会更快更简洁明了,但是如果我们的页面类似微博首页、朋友圈等的信息流动态更新十分频繁,那么尽可能的使用纯代码方式进行布局,因为你无法保证列表的每个单元格是否都一致,显示的内容同样是否也一致,其实最致命的,如果我们不用纯代码布局,很难保证后续接手的同学是否能够快速上手撸码。😂。
### 使用`StoryBoard / Xib`搭建
使用`StoryBoard / Xib`进行`UITableView`的搭建有两种方式,一种是在已有的`ViewController`上拖拽进`UITableView`;第二种是直接在SB中拖拽一个`UITableViewController`。简单使用方法如下图所示:
那这两张方式的区别在哪呢?先抛开`UITableView`,就拿微信朋友圈的例子来说,如果是我自己去做一个微信朋友圈,最底层我会拿一个`UIViewController`,然后用纯代码的方式搭建出一个`UITableView`,用`Xib`搭建出每一个单元格`Cell`,每条朋友圈下的评论用纯代码的方式去动态计算cell的行高,当然这其中肯定涉及到了很多其它内容。
这其中有个需要注意的地方,每个cell使用`Xib`去做,为啥cell要通过`Xib`去做呢?首先,如果你的页面比较复杂,比如去年年初时我实习做的其中一个App:
如上图所示的页面,如果是拿纯代码布局,真的,我说实话,非常有可能一上午的时间都都搭在上边了,但是如果拿`StoryBoard / Xib`搭建,很有可能半小时不到就好了。
通过以上的说法,大家应该能有对纯代码和`StoryBoard / Xib`搭建页面有了一个基本的认识,各自的优缺点也有了浅显的认识。接下来,我们将通过结合几个小案例疏通`UITableView`的基本使用。
## 进阶
1. `UITableView`可进行性能优化相关的点
* Cell重用:
- 数据源方法优化:创建一个静态变量重用ID,例如:`static NSString *cellID = @"cellID";`防止因为调用次数过多,static保证只创建一次,提高性能(感觉性能的提升可以忽略不记emmm)
- 缓存池获取可重用Cell的两个方法:`dequeueReusableCellWithIdentifier:(NSString *)ID`会查询可重用Cell,若注册了原型Cell则能够查询到,否则为nil,故需要先判断`if(cell == nil)`
- `dequeueReusableCellWithIdentifier:(NSString *)ID indexPath:(NSIndexPath *)indexPath`,使用之前必须通过SB/class进行可重用Cell的注册(registerNib/registerClass),不需要判断nil,一定会返回cell,若缓冲区Cell不存在,会使用原型Cell重新实例化一个新Cell。
* 尽量使用一种类型的Cell:能够减少代码量,减小Nib文件的数量;保证只有一种类型的Cell,实际上App运后只有N个Cell,但若有M种Cell,则实际上运行最多却可能会是MxN 个。
* 善用hidden隐藏subview:把所有不同类型的view都定义好,通过cell的枚举类型变量及hidden显示/隐藏不同类型的内容,因为在实际快速滑动中,单纯的显示/隐藏subview比实时创建快得多。
* 提前计算并缓存Cell的高度。如果我们不预估行高,则优先调用`heightForRowAtIndexPath`获取每个Cell即将显示的高度,实际上就是要确定总的tableView.contenSize,最后才又接着调用`cellForRowAtIndexPath`,可以建一个frame模型,保存下提前计算好的cell高度。
* 异步绘制:这是目前最火的tableView性能调优方法,新浪微博是这么做的,可以使用`ASDK`这个库进行。
* tableView滑动时,按需加载:识别tableView静止或减速滑动结束后,异步加载,在快速滑动过程中,只按需加载目标方位内的Cell。
* 避免大量使用图片缩放、颜色渐变、透明图层、CALayer特效(阴影)等操作,尽量显示大小刚好合适的图片资源。
2. `UITableView` 的拖动
首先要保证 `tableView` 进入了编辑状态 `isEditing = true` , 其次重写代理方法如下:
```Swift
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let movenum = nums[sourceIndexPath.row]
nums.remove(at: sourceIndexPath.row)
nums.insert(movenum, at: destinationIndexPath.row)
}
```
当然,该代理方法里我做的是数据源的数据替换,不写数据源的数据替换代码是没有问题的,拖动效果一样会存在。
================================================
FILE: iOS/basic.md
================================================
# iOS 基础
强推 [Apple Developer Documentation](https://developer.apple.com/documentation/)
## 名词解释
* ipa:iPhone application archiv;
## 使用不同版本的 `Cocoapods`
很多时候不同项目都会使用不同的 `pod` 版本进行项目管理,比如我公司的项目选择了 `1.3.1` 版本的 `pod`,而我自己电脑上的是 `1.5.3` 版本,这样势必会造成使用 `pod` 各种命令时出现问题,因此可以使用 `Bundler` 这个 `ruby` 版本管理工具。
### 下载
```shell
gem install bundler
```
若提示:
```
Fetching: bundler-2.0.1.gem (100%)
ERROR: While executing gem ... (Gem::FilePermissionError)
You don't have write permissions for the /usr/bin directory.
```
可以使用该命令:
```shell
sudo gem install -n /usr/local/bin bundler
```
### 进入目录
进入到带有 `Podfile` 文件的目录。
### 创建 `Gemfile`
```shell
bundle init
```
### 设置版本
```ruby
source "https://rubygems.org"
# 或其它你的版本
gem 'cocoapods','1.3.1'
```
### 安装
```shell
bundle install
```
### 运行 `pod install`
```shell
bundle exec pod install
```
## iOS 进程间通信(IPC)
### `URL Scheme` 和 `openURL` 方法
需要注意的是:iOS 系统应用的 `URL scheme` 无法重复注册,但是对于其它应用来说,结果是「未定义」,官方说法:
> 如果有多个第三方应用程序注册同一个 `URL scheme`,那么无法确定将 `scheme` 传递给哪一个应用程序。
### 通用链接
在 iOS 9 引入了通用链接的原因之一,解决 `URL Scheme` 劫持问题。在 Xcode 中打开 `Associated Domains` 选项。
### `UIActivity` 共享数据
### 应用程序扩展
应用程序扩展并不是应用程序,它们必须被捆绑到某个应用程序中,也就是「容器应用」,使用扩展的第三方应用程序,也就是「宿主应用」,可以与容器应用内的扩展程序包进行通信。但容器应用本身并不能直接与扩展应用通信。
### 剪贴板
需要注意的是:通用剪贴板在所有应用程序之间都是共享的,可以被设备砂锅的任何进程读取信息。
* 通过 App Groups 增加「同组应用」
* 使用 NSUserDefault suiteName 初始化并传递数据
### local socket
### KeyChain
================================================
FILE: iOS/code.md
================================================
# iOS 短代码集合
## 查询当前最上层 VC
```objc
+ (UIViewController *)currentViewController {
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
return [UIViewController findBestViewController:viewController];
}
+ (UIViewController *)findBestViewController:(UIViewController *)vc {
if (vc.presentedViewController) {
// Return presented view controller
return [UIViewController ttmuFindBestViewController:vc.presentedViewController];
} else if ([vc isKindOfClass:[UISplitViewController class]]) {
// Return right hand side
UISplitViewController* svc = (UISplitViewController*) vc;
if (svc.viewControllers.count > 0)
return [UIViewController ttmuFindBestViewController:svc.viewControllers.lastObject];
else
return vc;
} else if ([vc isKindOfClass:[UINavigationController class]]) {
// Return top view
UINavigationController* svc = (UINavigationController*) vc;
if (svc.viewControllers.count > 0)
return [UIViewController ttmuFindBestViewController:svc.topViewController];
else
return vc;
} else if ([vc isKindOfClass:[UITabBarController class]]) {
// Return visible view
UITabBarController* svc = (UITabBarController*) vc;
if (svc.viewControllers.count > 0)
return [UIViewController ttmuFindBestViewController:svc.selectedViewController];
else
return vc;
} else {
// Unknown view controller type, return last child view controller
return vc;
}
}
```
================================================
FILE: iOS/debug.md
================================================
## debug 技巧
### `_shortMethodDescription`
动态打印类信息
================================================
FILE: iOS/system.md
================================================
## 系统架构
### mach-O 文件
macOS 和 iOS 和可执行文件。主要分为了三大块,Header、Load Command 和 Section。Header 的作用是配置和说明,Load Command 说明了需要加载的库、流程和数据,Section 是 Load Command 的详细描述。
================================================
FILE: macOS/TranslateP.md
================================================
## TranslateP 开发过程
## 选型
使用 SwiftUI,因为 AppKit 实在是太难用了,痛苦。并且作为一个无主界面的 app,运行后只有菜单栏程序,使用[`MenuBarExtra`](https://developer.apple.com/documentation/swiftui/menubarextra)这个 macOS 13 开始支持的 API。
## Accessibility 权限
正在调试运行中的 app 没有加在 Applicatios 文件夹中,需要在 Xcode 的缓存目录下找到你正在运行中的 app,添加到 Accessibility 中。注意用户目录下的 Library 为隐藏目录,使用 shift + command + . 展示 Finder 的隐藏目录。
```bash
/Users/你的用户名/Library/Developer/Xcode/DerivedData
```
================================================
FILE: macOS/basic.md
================================================
## 基础知识
### 窗体
macOS 的窗体大小由内容决定,只需要限制窗体其中内容最小 size 即可。
### 如何判断是否能引入一个库/平台类型
```swift
#if canImport(UIKit)
import UIKit
public typealias PlatformImageType = UIImage
#else
import Cocoa
public typealias PlatformImageType = NSImage
#endif
```
### dmg 软件分发
* 本质利用了 `.DS_Store` 文件记录了文件和文件夹的位置信息,可以做出精美的图标展示。
* 参考文章:[再谈 .DS_Store:兼论 Windows 与 macOS Finder 的布局理念差异](https://sspai.com/prime/story/on-dsstore)
================================================
FILE: macOS/crash.md
================================================
# 解析 crash log(一)
## 前言
在负责的产品中有最近一段时间有极个别用户老是反馈有偶尔闪退的情况,而且就这几个用户反复出现,其它用户,甚至就坐在他边上的用户进行了一样的操作都没有任何问题。
刚开始丢了个重现构建的新包给这几位用户将就的用着,但直到今天 PM 受不了了,让我无论如何都要想办法解决这个问题,但我也一脸懵逼啊,按照用户的操作路径来看我也没能复现,甚至分析平台都抓不到对应信息,更何况这还是个企业级应用,没上到 AppStore,也就没法从 iTunes Connect 中拿到崩溃日志。
所以开始酝酿了一个事情......
## 分析问题
为了快速定位到问题所在,PM 和 leader 再三的跟用户进行交流,大家都没有一个比较好的方案,最后我厚着脸皮让用户照着下面这张图从设备中导出了一份崩溃日志发送给我:

从真机中拿到这份 `ips` 文件后就好办了很多,如果此时直接打开这个文件,可以看到如下图所示内容:

能够拿到真机的情况下,
* 在不删掉 app 的情况下直接调试;
* 无法复现问题,则把真机插入电脑,打开 Xcode -> Window -> Devices and Simulators -> View Device Logs,直接找到需要的 log;
在拿不到真机的情况下,先来直接讲解一个能够解决问题的流程:
### 第一步:`ips` 文件
“死皮赖脸”的让用户通过图 1 所示方法导出一份最新的 `.ips` 文件,并让用户分享给自己,并把文件名修改为 `.crash` 后缀,使其标识为 `crash` 类型;
### 第二步:`.dSYM` 文件
`.dSYM` 文件(debugging SYMBols,调试符号表)。从打包机(如果是通过打包机隔离构建的话)或本机上导出一份与用户设备中安装的 app 版本一致的 `.dSYM` 文件,该文件中详细的记录了 16 进制下的函数地址的映射信息。
需要注意的是,Xcode 的默认设置是会在 release 和 debug 环境下已经配置好了 archive 时自动带出 `.dSYM` 文件,如果你发现打开包内容时并没有发现 `.dSYM` 文件,可以到 Xcode 的 `Build Settings` 中查看 `Debug Infomation Format` 字段的配置进行修改。
`.dSYM` 文件对于后续排查问题十分重要,每一次 release 版本都最好要保存对应的 `dSYM` 文件或把整个 `Archives` 文件进行保存;
### 第三步:`symbolicatecrash` 工具
`symbolicatecrash` 工具。该工具跟随 Xcode,是获取符号化结果的最方便工具。`symbolicatecrash` 的地址视 Xcode 的安装路径而定,大致的地址为:
```shell
你的Xcode安装路径/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
```
### 第四步:获取崩溃代码信息
有了 `.ips` 文件、`.dSYM` 文件和 `symbolicatecrash` 工具后就可以直接进行解析日志了~因为我把这三个文件都统一放在了一个文件夹中,所以实际上命令看起来会是这样:
`./symbolicatecrash ./yourApp.crash ./yourApp.dSYM > crash.log`
如果在输入这条命令时告知找不到 `DEVELOPER_DIR`,可以导出一份:
```
export DEVELOPER_DIR="你的Xcode安装位置/Xcode.app/Contents/Developer"
```
对输出的结果重定向到了当前路径下的 `crash.log` 中,此时打开该文件,看到的内容是这样的:

很崩溃啊!重要的细节一个都没有展示出来!随后我换了一个思路,我们重新回到第三步
### 回到第三步:`atos` 命令
关于 `atos` 命令的描述,可以使用 `man atos` 查看具体信息:
```
...
DESCRIPTION
The atos command converts numeric addresses to their symbolic equiva-
lents. If full debug symbol information is available, for example in a
.app.dSYM sitting beside a .app, then the output of atos will include
file name and source line number information.
...
```
这是一个专用于 macOS 的控制台工具,从描述中可以看出,可以将地址转换为实际二进制图像的符号化字符串(实际代码)。上文中说到的 `symbolicatecrash` 工具是 apple 基于 `atos` 方便开发者进行的优化封装,但不知为何我的 `symbolicatecrash` 并不能完整的符号化所有 crash log 中的内容。所以现在将直接使用 `atos` 进行符号化,操作稍微繁琐一些,我们可以把对应的命令修改为:
`atos -o yourApp.dSYM/Contents/Resources/DWARF/yourApp -arch arm64 -l 0x104e40000 0x0000000104f90198`
`0x104e40000` 和 `0x0000000104f90198` 这两个地址是什么意思呢?我们再看这张图:

`0x104e40000` 为 `local address`,`0x0000000104f90198` 为 `address`,这两个地址在不借助其它工具的前提下可以直接手算出来,如果你对手算地址感兴趣的话,可以看[这篇文章](http://foggry.com/blog/2015/07/27/ru-he-shou-dong-jie-xi-crashlog/)。
通过执行上述 `atos` 命令,可以看到输出了正确的信息,如下图所示:

接下来就可以把多个地址进行解析,配合着在堆栈中的这几个关键信息基本上就可以定位到具体的 crash 代码文件和行数。
### 其它工具
关于符号化奔溃报告,还有以下几种:
* `dwarfdump`。这个工具用来应付普通的 crash 日志符号化完全是小题大做,但不排除某些极端下的情况。这个我没使用过,不做展开。
* `lldb`。`lldb` 在日常使用 Xcode 进行开发的过程中已经非常熟悉了,是 Xcode 的默认调试器。这部分我也没试过,大家可以自行搜索进行尝试。
### 总结
总不能每次出问题都要经过上述这么费劲的流程吧?当然可以说把这些流程写成一个脚本,每次只需要输入 `local address` 和 `address` 即可,但实际上这样的效果并不令人感到舒服。所以,为了节省后续更多的时间,接下来将准备写一个 Mac App,每次进行符号化时,只需要根据提示提前配置好对应的文件,再写下对应的 `local address` 和 `address` 就可以完成。
本系列下篇文章中将继续讲解......
================================================
FILE: macOS/kindle.md
================================================
# kindle
以后可能会做个 kindle 管理的小工具吧,这里记录一下提前了解到的知识和思考的内容。
## 如何读取 kindle 标注?
有很多管理 kindle 标注的处理软件,几乎都需要在 mac 上使用 USB 连接 kindle,剩下的工作其实就是数据处理和数据渲染了。
可以拿到的数据包括每本书的阅读进度、封面等,可以说相当丰富了,可以做一个对新手更加友好的 kindle 管理工具

================================================
FILE: macOS/macOS开发(词法分析器).md
================================================
这是我的第一个存粹基于Swift的project,重点是用Swift进行macOS的开发,而不是词法分析器。
对于开发桌面应用来说,目前可供参考的框有windows原生“古老的”MFC、常规的跨平台Qt、目前最火的跨平台[electron](https://github.com/electron/electron),以及macOS原生的Cocoa。
之前已经尝试过了使用Qt进行构建桌面程序,做了好几个小东西后,越来越发现Qt的强大,不过目前从Qt官方透露出来的消息可以看出,目前他们正在对嵌入式开发,也就是车载系统的开发下大力气,使用Qt类库即可完成车载系统应用的开发,这确实是个更加值得玩味的事情。在做出最终使用Swift进行macOS开发前,一直在纠结到底是学习使用跨平台框架进行开发还是直接走原生,最后思来想去,一直缺少使用Swift进行开发的机会,可以借助这个机会切入。因此,
**在保证编译原理的实验的要求下,使用了Swift进行了原生的macOS开发。(只能作为参考)**
首先来看效果完成图,
**改进版本一**
上一版本的讲解没有加入字符表,真是尴尬,这是加好字符表的完成图,
### 概念明晰
毕竟本质上是macOS的开发,所以此篇文章会稍微往开发方面侧重一些。在macOS上来说,其使用的`Cocoa`框架进行GUI程序的搭建,而不是iOS中的`cocoa Touch`框架,两者的差别不只是说换个名字这么简单,其中最大的差别在iOS开发中一般来说,我们都是只使用一个`window`,使用`viewController`进行相关页面的跳转,而在macOS的开发中,一个App可以有理论上无数多个`window`(一般没这么无聊的需求),而且还有`windowController`的概念,以及`keyWindow`和`mainWindow`的区别,坐标系换了,甚至连各大基础组件的继承父类也换了。
而且在macOS开发上开始更加推荐使用SB进行布局,而不用再去手撸布局了,就单一个`NSTableView`,如果我们还是像iOS开发中的那样直接在SB中按住控件拖拽入相关.m文件中,你会发现拖拽进来的居然是个`NSScrollView`, 而正确的想要拖拽进一个`NSTableView`,你要做的是需要在SB已拖拽控件详情列表选择对应的`View Controller Scene`,然后在其中选择对应的`NSScrollView` -> `NSClipView` -> `NSTableView`,这样才能正确的选择出对应的`NSTableView`。
这算是一个比较大的区别吧,因为在iOS中`UITableView`继承于`UIScrollView`,其又继承于`UIView`,但是在macOS中却多出了一个`UIClipView`。其实还有个比较大的区别,对应`NSTableView`的使用,同样也是有`delegate`和`dataSource`两个代理对象,但是其中的相关代理方法和iOS却是大不相同,甚至可以说是非常的奇妙了。🙂
关于这个项目的怎么布局就不说啦,因为已经关掉了放大缩小功能,可以先暂时不用考虑布局相关内容,所以给大家放张图即可。再强调一次,在iOS开发中我们有时会拒绝使用的SB或者其它布局方式,但是在macOS开发中,说句良心话,“真的,去使用自动布局吧😂”
### 实验相关
实验要求完成一个词法分析器,看了实验指导书上的相关要求,莫名感觉老师应该挖了坑🙂,给了一个非常简单的样例输入和输出,如下:
```
可以参考下面的示例:
输入字符串if i>=15 then x := y;
输出:
(3,‘if’)
(1,0) // i的符号表入口为0
(4,‘>=’)
(2,‘15’)
(3,‘then’)
(1,1) // x的符号表的入口为1
(4,‘:=’)
(1,2) // y的符号表的入口为2
(5,‘;’)
```
目前来说,我做到现在的词法分析器能够分析出如下所示的C语言代码:
```c
#include "stdio.h"
int main() {
/*这是注释*/
int a = 0, b = 0, c = 0;
printf("%d %d %d", &a, &b, &c);
return 0;
}
// int a = 0, b = 1;
```
词法分析器的关键就在于这个词法分析上,词法分析接受源码的输入,输出token表,而这个token是语法分析阶段的输入。那如何进行词法分析呢?书上讲了一堆的相关方法,梳理了一遍,我觉得逻辑应该是这样的,首先拿到对应的正则表达式,一般来说就是你要进行分析的对应编程语言的正则表达式集,接着对正则表达式集做出NFA(不确定的有穷自动机)状态图,一般来说到NFA这就行了,但这是对我们正常人来说的,对于人来说看NFA是最符合逻辑的,但是对于计算机来说还差点意思,我们需要把所以的相关逻辑,何时进何时退等逻辑都告诉它,因此最后一步为把NFA状态图转为DFA(确定的的有穷自动机)状态图,DFA则相当于NFA来说满足了计算机的正常处理逻辑。
emmm。以上都是官话,现在来看真实的样子。🙂。根据之前分析的结果,我们需要接收一个由客户端界面上传递而来的字符串,当然,这个字符串是带有`\t \n`等特殊字符的,我们也要对其做出判断。需要一个字典数组,用来保存token对,需要多个特殊字符数组,用于判断当前接收到的字符串所属类型,为了判断的方便,我把这坨特殊字符做了八大归类,分别为操作符、界符、注释、非法字符、输入输出字符、关键字、正常字符、头文件。
这个块有个需要注意的地方,在Swift中,如果你对成员变量(属性)在声明的时候没做初始化,那么在init这个对象的时候会给你报错,告诉你缺少实例化。我们可以这么解决,第一种:在对应的类中成员变量声明时,先显式的说明初始值;第二种:设置成员变量为可选类型,Swift中的可选类型包括nil嘛,嘿嘿嘿;第三种:在对应类的init方法中初始化对应的成员变量即可。我采用了第一种方法。
```swift
public var inputCodeString: String = ""
public var token = Array<[String : String]>()
// 操作符 OPT
private let operatorWords = ["+", "-", "*", "/", "%", "<", ">", "=", ">=", "<=", "==", "!="]
// 界符 MAR
private let marginalWords = [";", "{", "}", "(", ")", " ", "\r", "\t", "\"", ",", "&"]
// 注释 ANT
private let annotatedWords = ["//", "/*", "*/"]
// 非法字符 ILG
private let illegalWords = [".", "?", "!", "@", "#", "$", "^", "~", ":"]
// 输入输出 INO
private let intoutWords = ["%d", "%c", "%f", "%lf"]
// 关键字 KEY
private let keyWords = ["break", "case", "char", "const", "printf",
"continue", "default", "double", "else",
"enum", "extern", "float", "for", "goto",
"if", "int", "long", "redister", "return",
"sizeof", "static", "struct", "while",
"switch", "typedef", "unsigned", "void", "#include"]
// 正常字符 NOR
// 头文件 HED
```
而我们的init方法如下所示,接收一个字符串,赋值给对应的成员变量即可。如果你想连init方法都不写也行, 用对应的setter方法即可,我非常推荐这种写法,Swift的setter/getter方法的写法真的是太美妙了😂。
```swift
// MARK: init方法
// designer
public init(inputCodeString : String) {
self.inputCodeString = inputCodeString
}
```
接下来是我们的类型判断方法,丢入一个切割好的String,返回一个对应的String类型,其中在判断是否为正常字符,即数字、大小写字母用到了Swift中的正则写法,也被称为“谓词匹配”。
```swift
private func contanierType(keyString: String) -> String {
if operatorWords.contains(keyString) {
return "操作符"
} else if marginalWords.contains(keyString) {
return "界符"
} else if annotatedWords.contains(keyString) {
return "注释"
} else if keyWords.contains(keyString) {
return "关键词"
} else if illegalWords.contains(keyString) {
return "非法字符"
} else if inputNumberOrLetters(keyString) {
return "正常字符"
} else if intoutWords.contains(keyString) {
return "输入输出"
} else {
return ""
}
}
private func inputNumberOrLetters(_ name: String) -> Bool {
if name.isEmpty {
return false
}
let regex = "[a-zA-Z0-9]*"
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
let inputString = predicate.evaluate(with: name)
return inputString
}
```
最后就是我们的词法分析核心方法了,在这个方法中,我们需要先进行字符串的切割,而这个切割的标准就是从一个空格、一行的开始或者`\t`tab键的开始,结束为空格或者`\n`回车字符,所以,当每次进行扫描时遇到空格即为当前追加字符的开始或者结束,同时也可以说是遇到了界符,即可结束。我把注释`\\`也做为了界符,因为当扫描到注释符号时,实际上后边的内容我们都不需要关心,直接`continue`跳过即可。
关于字符表的插入,主要的实现逻辑有,当检测到当前的缓冲字符串为正常字符时,开启正常字符串BOOL,在检测下一个字符时,若为操作符,则开启操作符BOOL,当接收的第三个缓冲字符串还为正常字符串时,且正常字符串BOOL和操作符BOOL同时为真,即可进行插入字符表,并且进行三次删除操作,把之前的操作符、正常字符、关键词三个都删掉。
同时也做了头文件的判断,判断头文件就只需要坚持出当前扫描到的字符串中是否带有`.h`即可😂。需要稍微注意的是,每次执行完一次扫描字符串的类型判断,记得把扫描字符串清空即可,最后把整个token返回出去。
```swift
// MARK: 词法分析方法
public func lexicalAnalysis() -> Array<[String : String]> {
var tampString = ""
// 注释检测符号
var annotatedStatus = false
var normalWordStatus = false
var operatorStatus = false
for singleChar in inputCodeString {
if singleChar == "\n" {
annotatedStatus = false
continue
}
// 检测界符
if !marginalWords.contains(String(singleChar)) {
tampString.append(singleChar)
} else {
// 检测注释
if !annotatedStatus {
if annotatedWords.contains(tampString) {
annotatedStatus = true
}
if tampString.contains(".h") {
token.append(["头文件": tampString])
}
let tokenString = contanierType(keyString: tampString)
if !tokenString.isEmpty {
// 插入字符表表相关逻辑
if tokenString == "正常字符" {
normalWordStatus = true
}
if normalWordStatus {
if tokenString == "操作符" {
if tampString == "=" {
operatorStatus = true
normalWordStatus = false
}
}
}
if operatorStatus {
if tokenString == "正常字符" {
workTable.append(tampString)
operatorStatus = false
normalWordStatus = false
token.append(["字符表": String(workTable.count - 1)])
if (token[token.count - 2]["操作符"] != nil) {
token.remove(at: token.count - 2)
}
if (token[token.count - 2]["正常字符"] != nil) {
token.remove(at: token.count - 2)
}
if (token[token.count - 2]["关键词"] != nil) {
token.remove(at: token.count - 2)
}
// 填充到字符表中后需要把当前缓存字符串清空
tampString = ""
continue
}
}
// 除了注释都可以被加入
if !annotatedStatus {
token.append([tokenString: tampString])
}
}
if ![" ", "\n", ""].contains(String(singleChar)) {
token.append(["界符" : String(singleChar)])
}
tampString = ""
}
}
}
return token
}
```
### macOS开发部分
进入到了macOS开发部分。在该部分中,我们主要完成的事情后:
1. 界面搭建;
2. 数据获取;
3. 界面刷新。
界面搭建大家就用SB吧,对自己狠一些🙂。刚开始我也是十分的抵制SB,布局什么的不经过自己的手总感觉缺少了点什么。
在数据获取这块,我们首先需要对输入做一个判断,该词法分析器提供“文件输入”和“文本输入”两种输入方式,对于“文本输入”,直接就从`NSTextField`中获取即可,找了半天,不是`.text`也不是`.textString`,试了很多属性都不行,翻了`NSTextField`的头文件,也没写明白到底通过哪个属性获取,最后发现,居然是`.stringValue`。🙄。真是无语。
```swift
@IBAction func lexicalAnyButton(_ sender: Any) {
let analysisTool = PJAnalysisTool.init(inputCodeString: inputTextField.stringValue)
tokenArray = analysisTool.lexicalAnalysis()
outputTableView.reloadData()
}
```
而通过“文件输入”,过程有些曲折,使用了`NSOpenPanel`类,苹果官方开发文档对其有这么个介绍`The Open panel for the Cocoa user interface.`,打开一个为Cocoa用户界面面板(就是文档选择器啦),我对该面板做了如下设置,能选择文件,不能选择文件夹,不允许多选。
当我们按下面板上的`OK`或者`确定`按钮,通过Data(相当于NSDate)类载入文件内容,将该文件内容转码为`utf-8`编码,拿到最终的编码字符串后,丢入词法分析类,返回一个字典数组,而该字典数组就是我们`NSTableView`进行数据刷新的数据源。
```Swift
@IBAction func selectFile(_ sender: Any) {
let panel = NSOpenPanel.init()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
let finded : Int = panel.runModal().rawValue
if finded == NSApplication.ModalResponse.OK.rawValue {
for url in panel.urls {
let codeData = try? Data.init(contentsOf: url)
let codeString = String(data: codeData!, encoding: String.Encoding.utf8)
print(codeString!)
let analysisTool = PJAnalysisTool.init(inputCodeString: codeString!)
tokenArray = analysisTool.lexicalAnalysis()
outputTableView.reloadData()
}
}
}
```
接下来就到了`NSTableView`的相关代理方法实现了,`NSTableView`有两个必须实现的代理方法`numberOfRows`和`objectValueFor`,其中`objectValueFor`可以给空值。
剩下的还有一个值得注意的地方,目前我只找到了对`NSTableView`进行分栏通过`identifier`标识符去做,也就是说,一个分栏对应一个重用符,应该还会有其它的设置方法。
```swift
func numberOfRows(in tableView: NSTableView) -> Int {
return tokenArray.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return nil
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
return 20
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let idString = tableColumn?.identifier
if idString!.rawValue == "AutomaticTableColumnIdentifier.0" {
var cellView = tableView.makeView(withIdentifier: idString!, owner: self)
if (cellView != nil) {
cellView = NSTableCellView.init(frame: NSMakeRect(0, 0, (tableColumn?.width)!, 20))
} else {
for view in (cellView?.subviews)! {
view.removeFromSuperview()
}
}
let textField = NSTextField.init(frame: NSMakeRect(0, 0, (tableColumn?.width)!, (cellView?.frame.size.height)!))
textField.stringValue = Array(tokenArray[row].keys)[0]
textField.isBordered = false
textField.isEditable = false
textField.alignment = .left
textField.backgroundColor = NSColor.clear
cellView?.addSubview(textField)
return cellView
} else {
var cellView = tableView.makeView(withIdentifier: idString!, owner: self)
if (cellView != nil) {
cellView = NSTableCellView.init(frame: NSMakeRect(0, 0, (tableColumn?.width)!, 20))
} else {
for view in (cellView?.subviews)! {
view.removeFromSuperview()
}
}
let textField = NSTextField.init(frame: NSMakeRect(0, 0, (tableColumn?.width)!, (cellView?.frame.size.height)!))
textField.stringValue = Array(tokenArray[row].values)[0]
textField.isBordered = false
textField.isEditable = false
textField.alignment = .left
textField.backgroundColor = NSColor.clear
cellView?.addSubview(textField)
return cellView
}
}
```
好了。以上就是我使用Swift完成的第一个小实验,如果在课程持续进行的过程中发现了有不足,会持续更新,目前完成的词法分析器的识别功能还是太简单。后续我会尽量把做的新东西都往Swift上迁,慢慢的把Objective-C的地位给抹掉,毕竟它现在的地位在国内还是太硬了。
相关demo见:[词法分析器](https://github.com/windstormeye/MacMorePractices/tree/master/lexicalAnalysisTool)
================================================
FILE: macOS/performance.md
================================================
# iOS 和 macOS 性能优化
## 命令行工具
### `top`
`top` 命令能够连续动态地更新显示大量有关系统性能的参数。`top -u` 能够把 CPU 当前的占用比排序,置顶最活跃的进程。
### `time`
使用 `time` 命令测试程序的耗时。
### `simple`
查看程序的调用树。可以集合 `grep` 来过滤掉一些干扰数据。
### `getrusage()`
调用该方法与使用 `top` 和 `time` 命令获取的信息相同,但没有启动单独进程的开销。
## Instruments
### macOS 冷启动性能分析
当需要测试应用程序「冷启动」性能时使用它进行测试非常有效。冷启动是指启动和登录后的过程,此时 UI 程序还未驻留内存。但是启动 Instruments GUI 程序会加载大多数 GUI 框架,这意味着使用应用程序时,收集的任何信息不再代表冷启动。这种情况一般出现在 macOS 上。
Instruments 可以允许在不与 UI 交互的情况下启动性能分析会话。例如:
```shell
instruments -t "Time Profile" -l 2000 -D sumintsm-cpu.trace.sumintsm
```
通过启动以上命令启动 Instruments 命令行工具,使用 `Time Profile` 工具配置 2s,然后将结果数据写入跟踪文档 `sumintsm-cpu.trace`,如果文件已经存在,则追加数据。
命令行程序的占用空间比 GUI 应用程序小得多,不加载任何 UI 框架或资源。
### 热启动
指所需要的大部分数据已经存在于缓存之中,而冷启动需要从磁盘中读取所有代码、资源和数据。
### 最简单和最快速保存状态的方法
进行「内存转储」。只需要找到保存数据的基地址,然后将其写入 `write()` 或包装在 `NSData()` 中,以便通过 `writeTo...` 这类方法执行写入操作,最后记得要使用非复制版本的初始化方法 `initWithBytesNoCooy...`。
可以这么做的前提是,需要掌握完整的内部数据结构才能读取和写入对应的文件格式。
### 关于启动过程的性能优化
* 冷启动:App 在启动之前它的进程不在系统里,需要系统创建一个进程分配给它进行启动。
* 热启动:App 在冷启动后,将 App 退到后台,App 的进程还在系统里,用户重新进入 App 的过程,这个过程做的事情非常少。
* App 启动主要包括三个阶段:
* `main()` 函数执行前;
* 加载可执行文件(`.o` 文件的集合;
* 加载动态链接库,进行 `rebase` 指针调整和 `bind` 符号绑定;
* Objc 运行时的初始处理,包括 Objc 相关类的主持、`category` 注册、`selector` 唯一性检查等;
* 初始化。包括执行 `+load()` 方法、`attribute((constructor))` 修饰的函数的调用、创建 C++ 静态全局变量。
* 在 `main()` 函数执行前阶段可以做的优化:
* 减少动态库加载。Apple 建议使用更少的动态库,并建议多个动态库进行合并,最多一个动态库可以支持 6 个非系统动态库合并。
* 减少加载启动后不会去使用的类或者方法。
* `+load()` 方法里的内容可以放到首屏渲染完成后再执行操作,或者使用 `+initialize()` 方法替换掉。因为在 `+load()` 方法里,进行运行时方法替换会带来 4ms 的消耗。
* 控制 C++ 全局变量的数量。为什么会有问题?
* `main()` 函数执行后;
* 指的是从 `main()` 函数执行开始,到 `appDelegate` 的 `didFinishLaunchingWithOptions` 方法里首屏渲染相关方法执行完成。
* 首屏初始化所需配置文件的读写操作;
* 首屏列表大数据的读取;
* 首屏渲染的大量计算。
* 首屏渲染完成后。
* 对 App 启动速度的监控:
* 定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。Xcode 的 Time Profiler 就是这种方式。一般时间设置在 0.01 秒。
* 对 `objc_msgSend` 方法进行 hook
* OC 方法可以使用 `objc_msgSend` 方法进行 hook,对于 C 函数和 `block` 可以使用 `libffi`里的 `ffi_call` 完成。
* `objc_msgSend` 本身使用汇编写的。
* 因为调用频次最高,汇编在性能上属于原子级优化。
* 使用其它语言难以做到**未知参数跳转任意函数指针**的功能。
* 其自身的执行逻辑为:先获取对象对应类的信息,再获取方法的缓存,根据方法的 selector 查找函数指针,经过异常错误处理后,最后跳到对应函数的实现。
================================================
FILE: macOS/playground.md
================================================
# 来一次完整的使用 Playground(一)
## 简介
在使用 `Xcode Playground` 进行 WWDC 2019 scholarship 项目的开发时,原本以为 apple 指定的 Xcode 10.1 版本上会有不错的优化,但没想到跟两年前使用的效果出奇一致的差劲!!!让我不仅怀疑,“这是 Apple 开发的工具吗?”。从我知道 `Xcode Playground` 开始到现在一直都给我“这是一个神器”的感觉,曾经也尝试着在给社团同学讲解相关开发知识点时使用过 `Xcode Playground`,但每次想坚持尝试时都不得不放弃。
接下来,我将介绍一遍我在使用 `Xcode Playground` 中遇到的问题和解决思路。
## 怎么写啊?
我们创建一个 `Single View` 的工程,如下图所示:

然后你会看到一个简洁明了的工程样式:
```Swift
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
```
OK,代码非常容易理解,如果你使用也是 Xcode 10.1 那么在 `PlaygroundPage.current.liveView = MyViewController()` 的左边,会看到一个“播放”键,点击它即可运行。
但运行后的界面呢?可以按照下图箭头所指向之处,选择视图展示方向:

一切正常!这跟之前毫无两样,就多了设置一下 `PlaygroundPage` 的活动视图而已。此时我们信心满满一定能够玩好 `Xcode Playground` !结合按照以往的工程习惯,开始对项目进行分层,分模块进行项目的划分。
## 问题来了!
正准备按照文件夹的方式在 `Sources` 文件夹下划分模块,发现居然无法创建文件夹???是的,在 `Xode Playground` 无法直接创建文件夹,如果非要创建文件夹,可以在 `Finder` 中创建。
那好吧,既然没法方便的场景文件夹那就算了吧~估计 Apple 并不想我们在 `Xcode Playground` 中进行大工程的开发。这回终于可以开始写代码了!
那就先来把 `HomeViewController` 给写了吧。根据示例代码你写出来的代码可能会是这样:
```Swift
class PJHomeViewController: UIViewController {
override func loadView() {
view.backgroundColor = .red
}
}
```
下一步在 playground 中使用它!
“嗯?怎么没有代码提示?好奇怪,算了吧~反正 Xcode 写 Swift 也会偶尔没有提示,习惯就好”——这是你在 playground 中写代码时内心可能和自己的对话。凭借着简单的类名,我们可以很快的写出以下代码:
```Swift
import UIKit
import PlaygroundSupport
PlaygroundPage.current.liveView = PJHomeViewController()
```
等了一会儿后......(可能是一分钟,可能是 10 秒,也可能 3 秒)
你收到了这样的一个提示:`Use of unresolved identifier 'PJHomeViewController'`。告诉你 `PJHomeViewController` 这个标识符不存在!
“不可能啊!这不可能啊!”,然后陷入了深深的怀疑当中,“难道我写了这么久的 iOS,居然连一个 `class` 都会创建错?”。反复查看代码后,你确实自己写的确实没有问题,开始从网上搜索答案。
终于,你找到了解决方案!`Sources` 中的类与 playground 并不位于同一 `Module` 下,我们需要把 `PJHomeViewController` 暴露出去。你把代码修改为了:
```Swift
import UIKit
public class PJHomeViewController: UIViewController {
public override func loadView() {
view.backgroundColor = .red
}
}
```
然后再回到 playground 中,运行!嗯?怎么一直在转圈?重启 Xcode!嗯?怎么还是不行?检查代码,然后你发现了在 `loadView` 并没有给 `view` 设置 `frame`,看来是自己的学术不精。修改后代码为:
```Swift
import UIKit
public class PJHomeViewController: UIViewController {
public override func loadView() {
view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 667))
view.backgroundColor = .red
}
}
```
此时再运行工程,我们终于看到了红色的画面!吐了一口气~
## 准备大干一场!
摸熟套路后,我们终于可以大干一场了!
在大干一场一场的过程中,你会被“代码提示怎么又没有了?!”“报错异常怎么还不消失?”“运行怎么需要这么久?”“文件删除后怎么一直没更新目录?”等问题困扰,所以!
我十分推荐大家优先使用 Xcode 进行源码的编写和调试,在 playground 中进行编码和调试都是一个“十分痛苦”的体验,如果再加上你的设备比较差劲的话,那 `Xcode Playground` 的“实时预览”的效果几乎可以说废掉了。
关于 `Xcode Playground` 的实现猜测是跑在了一个单独的进程中做文件差分编译,所以才会导致在某些情况下出现“卡死”甚至 Xcode 整个崩掉的情况。
但是这个情况在 iPad 上完全没有,可以尝试使用使用 `Swift Playground` 而不是 `Xcode Playground`。
## 总结
`Xcode Playground` 的优势很大,但就目前来看劣势在慢慢缩短,我在使用 2015 款的 15-inch 高配的 MBP 和 2017 款的 15-inch 高配两款机器下的 `Xcode Playground` 对老款机器十分不友好,让我举步维艰,最后放弃转而使用 Xcode 开发完再丢入 `Xcode Playground` 进行效果查看。
下篇文章中将完整的介绍我的 WWDC 2019 scholarship 项目完整内容。
================================================
FILE: macOS/一台设备多个git账号.md
================================================
# 一台设备多个 git 账号
## 取消全局用户信息
```shell
# 通过以下语句查看设置的账户信息
$ git config --global user.name
$ git config --global user.email
# 取消设置的全局账户
$ git config --global --unset user.name
$ git config --global --unset user.email
# 通过以下语句设置的不同文件夹下的账户信息
$ git config user.name "your name"
$ git config user.email "your email"
```
## 重新生成新的 ssh_key
`$ ssh-keygen -t rsa -f ~/.ssh/id_rsa_pjhubs -C "877302410@qq.com"`
## 在 github or gitlab 上添加生产密钥
```shell
$ cd ~/.ssh
# cat 出来的内容全部复制
$ cat id_rsa_pjhubs.pub
```
## 本地识别
```shell
# 使用-K可以将私钥添加到钥匙串,不用每次开机后还要再次输入这条命令
$ ssh-add -K ~/.ssh/id_rsa_pjhubs
# 查看已添加的内容
$ ssh-add -l
```
## 创建 config 文件,使不同的项目可用不同的信息进行处理
```shell
$ cd ~/.ssh/
# 没有config文件时,用以下命令生成
$ touch config
```
config 文件中键入以下内容:
```shell
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_pjhubs
Host git.xxx.com
HostName git.xxx.com
User git
IdentityFile ~/.ssh/id_rsa
# 如果还有继续添加
```
## 验证
```shell
$ ssh -vT git@github.com
$ ssh -vT git@git.xxx.com
# 出现 Hi,your name 等信息就稳妥了
```
================================================
FILE: ruby/basic.md
================================================
# Ruby
## 概念
### 使用 `irb` 进入交互式终端环境
### 单引号和双引号的区别
单引号可以直接把包括的特殊字符进行输出。
### `print`、`puts` 和 `p` 的区别
* 使用 `p` 进行输出,特殊字符不会被转义。

### 字符串插值
`print "表面积 = #{area}\n"`
### `times` 方法
```ruby
100.times do
print "All work and no play makes Jack a dull boy.\n"
end
```
### 符号
```ruby
sym = :foo # 表示符号“:foo”
sym2 = :"foo" # 意思同上
```
```ruby
> irb --simple-prompt
>> sym = :foo
=> :foo
>> sym.to_s # 将符号转换为字符串
=> "foo"
>> "foo".to_sym # 将字符串转换为符号
=> :foo
```
### 散列的创建
```ruby
address = {:name => "高桥", :pinyin => "gaoqiao", :postal => "1234567"}
```
将符号当作键来使用时,程序还可以像下面这么写:
```ruby
address = {name: "高桥", pinyin: "gaoqiao", postal: "1234567"}
```
### 接收输入
```ruby
puts "首个参数: #{ARGV[0]}"
puts "第2 个参数: #{ARGV[1]}"
puts "第3 个参数: #{ARGV[2]}"
```
```shell
> ruby print_argv.rb 1st 2nd 3rd
首个参数: 1st
第 2 个参数: 2nd
第 3 个参数: 3rd
```
### 变量类型
#### 局部变量
以英文字母或者 `_` 开头。
#### 常量
全大写
#### 全局变量
以 `$` 开头。
#### 实例变量
以 `@` 开头。
#### 类变量
以 `@@` 开头
* 变量前加上*,表示 Ruby 会将未分配的值封装为数组赋值给该变量。
```ruby
a, b, *c = 1, 2, 3, 4, 5
p [a, b, c] #=> [1, 2, [3, 4, 5]]
a, * b, c = 1, 2, 3, 4, 5
p [a, b, c] #-> [1, [2, 3, 4], 5]
```
### `=~` 符号
The `=~` operator matches the regular expression against a string, and it returns either the offset of the match from the string if it is found, otherwise nil.