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) * 解释“金发姑娘原则”指出,凡事都必须有度,而不能超越极限。按照这一原则行事产生的效应就称为“金发姑娘效应”。 ### 学习速率 * 通过一个图来解释 ![优化学习速率](https://i.loli.net/2019/07/18/5d3002110b29b87060.png) * 当「学习速率」过高,容易导致每一步都在曲线上进行跳跃,沿着曲线向上爬,而不是降到底部。 ### 随机梯度下降法/ 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 服务器工程配置: ![pyCharm 中的 web 服务器配置](https://i.loli.net/2019/07/04/5d1df687dab1d59260.png) ### 如何做「不包含」/「不等于」操作 ```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 管理等这些附加功能。 ![WX20180930-155912@2x.png](https://i.loli.net/2018/09/30/5bb082c0d9a2e.png) ### 应用服务器 在同一时期,应用服务器已经存在并发展了很长一段时间了,大部分产品都指定了“封闭的”产品专用通信协议来互连胖客户端和服务器,在 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 和维护它的生命周期的运行环境。 ![20180930161346.png](https://i.loli.net/2018/09/30/5bb085e59d02c.png) ### 什么是 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 的解释:子类和父类的关系。 ![用例图——泛化关系.png](https://i.loli.net/2019/01/01/5c2b5f04f3e3a.png) #### 包含关系 官方解释:包含关系指的是两个用例之间的关系,其中一个用例(基本用例)的行为包含了另一个用例(包含用例)的行为。 PJ 的解释:把某一个功能进行重用。 【例 1】银行的 ATM 系统中,有“存款”、“取款”、“账户余额查询”和“转账”四个用例,都要求用户必须登录了 ATM 机。也就是说,它们都包含了用户登录系统的行为。因此,用户登录系统的行为是这些用例中相同的动作,可以将它提取出来,单独的作为一个包含用例。 “存款”、“取款”、“查询用户余额”和“转账”是基本用例,“登录”是包含用例,如下图所示: ![银行 ATM 系统中用例之间的包含关系.png](https://i.loli.net/2019/01/01/5c2b61c637949.png) 由于将共同的用户登录系统行为提取出来,“存款”、“取款”、“查询用户余额”和“转账”四个基本用例都不再含有用户登录系统的行为。 【例 2】网上购物系统,当注册会员在线购物时,网上购物系统需要对顾客的信用卡进行检查,检查输入的信用卡号是否有效,信用卡是否有足够的资金进行支付。 ![网上购物系统中用例之间的包含关系.png](https://i.loli.net/2019/01/01/5c2b65c9f225d.png) 上图中有没有必要将检查信用的行为提取出来,单独构成一个用例(作为包含用例),当信用检查的行为只发生在“在线购物”活动中时,可以不用提取出来。当信用检查的行为还发生在其它活动中时,应该提取出来,以便实现软件重用。 #### 拓展关系 官方解释:在拓展关系中,对于拓展用例的执行有更多的规则限制,基本用例必须声明若干个“拓展点”,而拓展用例只能在这些拓展点上增加新的行为和含义。 PJ 的解释:基本用例在满足一定条件后可进行选择执行拓展用例。 【例 3】图书借阅系统。当读者还书时,如果借书时间超期,则需要缴纳一定的滞纳金,作为罚款。 ![图书借阅系统中还书时用例之间的拓展关系.png](https://i.loli.net/2019/01/01/5c2b69e8c68e5.png) #### 综合 【例 4】 网上购物系统,当注册会员浏览网站时,他可能临时决定购买商品,当他决定购买商品后,就必须将商品放进购物车,然后下订单。 ![客户网站购物的用例图(一).png](https://i.loli.net/2019/01/01/5c2b6c1e44ed8.png) 如果网上购物系统的需求改为了:注册会员即可以直接在线购物,又可以浏览商品后临时决定在线购物,则可以改为下图所示: ![客户网站购物的用例图(二).png](https://i.loli.net/2019/01/01/5c2b6d2106196.png) ### 用例描述 > 没有描述的用例就像是一本书的目录,人们只知道该目录的标题,但并不知道该目录的具体内容是什么,仅用图形符号表示的用例本身并不能提供该用例所具备的全部信息,必须通过文本的方式描述该用例的完整功能。实际上,用例的描述才是用例的主要部分,是后续的交互图分析和类图分析必不可少的部分。 用例描述了参与者和软件系统进行交互时,系统所执行的一系列动作序列,因此这些动作序列应该包含正常使用的各种动作序列(主事件流),而且还包含对非正常使用时软件系统的动作序列(子事件流)。 【例 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】在一个仅为交易客户提供买卖基金的基金交易系统中,**参与者**为交易客户,交易客户能够操作的系统功能有买入基金和卖出基金。因此,系统有两个用例:买入基金和卖出基金。 进一步分析发现,基金的品种应该存在与该系统中,否则交易客户无法进行基金的买卖。但系统已存的两个用例都不能完成基金品种的管理,所以可以确认基金品种的管理应该在别的系统中完成。 所以,我们需要开发这个系统,仅存在两个用例:买入基金、卖出基金。 ![仅完成基金买卖的“基金交易系统”的系统边界.png](https://i.loli.net/2019/01/04/5c2f81bb9cd84.png) 【例 2】对例 1 做个调整。在一个既提供基金买卖又提供基金品种录入的基金交易系统中,交易客户,能够进行基金的买入和卖出。因为还需要对基金品种进行管理(录入、修改、删除和查询),由基金公司员工进行操作。所以该系统的参与者有交易客户和基金公司员工。系统边界可以改为下图所示: ![拓展了基金品种管理的“基金交易系统”的系统边界.png](https://i.loli.net/2019/01/05/5c309b74d8f8d.png) #### 如何确定参与者 * 谁将使用系统的主要功能? * 谁将需要系统的支持来完成她们的日常工作? * 谁将必须维护、管理和确保系统正常工作? * 谁将给系统提供信息、使用信息和维护信息? * 系统需要处理哪些硬件设备? * 系统使用外部资源吗? * 系统需要与其他系统交互吗? * 谁对系统产生的结果感兴趣? 需要注意的问题: * 只要是参与者,对于子系统而言都是外部的; * 参与者直接与系统进行交互; * 参与者指的与系统直接交互时所扮演的角色,而不是特定的人或事物。比如,不是 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 中。 ### 引入汇编语言后的计算机控制 * 计算机操作变成:汇编和执行两个阶段。 ### 高级语言之后 * 计算机操作变成:编译、**连接**和执行三步骤。 ### 简单批处理系统的操作 * 原先,手工操作与计算机速度严重不匹配的问题。现在,引入**作业控制语言**,用户编写作业说明书,描述对一次计算机求解(作业)的控制; * 操作员控制计算机成批输入作业,成批执行作业; * 这一方式明显缩短了手工操作时间,提高了计算机系利用率; * 这一阶段,磁带的出现,使得卡片与纸带等机械输入方式得以进一步提高。 只是一个简单的批处理系统,谈不上是操作系统,只提供了一个半自动化操作方式,并没有解决与中央处理器电子速度不匹配的矛盾,要让多个程序同时进入计算机系统,而想要多道程序同时进行,就需要进行程序的切换,而该切换的解决需要更加高速的外存储设备的支撑。 ![IMG_39B4C3077F37-1.jpeg](https://i.loli.net/2018/09/17/5b9fcc1063f88.jpeg) ### 操作系统与自动化系统 磁盘的出现,计算机操作系统从此出现,实现了真正的自动化控制。 ## 第五节 ### 操作系统概念 * 操作系统简称: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 。 ## 第十节 ### 操作系统的程序接口 * 操作系统功能的程序接口 —— 系统调用; * 操作系统实现的完成某种特定功能的过程;为所有运行程序提供访问操作系统的接口。 ### 系统调用的实现机制 * 陷入处理机制:计算机系统中控制和实现系统调用的机制; * 陷入指令:也称访管指令,或异常中断指令; * 每个系统调用都事先规定了编号,并在约定寄存器中规定了传递给内部处理程序的参数。 ### 系统调用的实现要点 * 编写系统调用处理程序; * 设计一张系统调用入口地址表,每个入口地址指向一个系统调用的处理程序,并包含系统调用自带参数的个数; * 陷入处理机制需要开辟现场保护区,以保持发生系统调用时的处理器现场。 ![IMG_0282.jpg](https://i.loli.net/2018/09/23/5ba7abe57a6de.jpg) ## 第十一节 ### 操作系统软件的规模 * 在计算机软件发展史上, OS 是第一个大规模的软件系统; * 1960 年代,由 OS 开发所衍生的体系结构、模块化开发、测试与验证、演化与维护等研究直接催生了软件工程这一新兴研究领域(另一个催生来源是 DB 应用引发的需求与规格) ### 操作系统软件的结构设计 * OS 构件:内核、进程、线程、管程等; * 设计概念:模块化、层次式、虚拟化。 ### 操作系统内核 * 单内核:内核中各部件杂然混居的形态,始于1960年代,广泛使用。如 Unix/Linux ,及 Windows (官方称为混合内核的 CS 结构); * 微内核:1980 年代开始,强调结构性部件与功能性部件的分离,大部分 OS 研究都集中在此; * 混合内核:微内核和单内核的折中,较多组件在核心态中运行; * 外内核:尽可能减少内核的软件抽象化和传统微内核的消息传递机制,使得开发者专注于硬件的抽象化(部分嵌入式系统使用) ![IMG_A6FF6590554B-1.jpeg](https://i.loli.net/2018/09/23/5ba7ac4d14c82.jpeg) ![IMG_FA03C006D2D8-1.jpeg](https://i.loli.net/2018/09/23/5ba7ac5ca1ec7.jpeg) # 第二章 处理器管理 ## 第一节 处理器与寄存器 操作系统是对计算机硬件的第一次扩充,操作系统在设计的实话贯彻了软硬件协同的概念,操作系统对硬件设计提出了一系列要求。 ### 处理器部件的简单示意 ![屏幕快照 2018-09-25 18.16.28.png](https://i.loli.net/2018/09/25/5baa0cc4b1976.png) ### 用户可见寄存器 * 可以使程序员减少访问主存储器的次数,提高指令执行的效率; * 所有程序可使用,包括应用呈现和系统程序: * 数据寄存器:又称通用寄存器(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 算法大家都知道),自己留着私钥,客户端通过某些途径拿到了服务器发布的公钥,客户端并不知道私钥。 ![20180930180914.png](https://i.loli.net/2018/09/30/5bb0a18be4a51.png) 如果有人 hack ,则会出现: ![20180930181454.png](https://i.loli.net/2018/09/30/5bb0a22a3aa27.png) 故客户端在接收到消息后,并不能肯定这个该消息就是由真实服务器发出的,可以被第三者冒充真实服务器发出该消息。那该如何确定该信息是由真实服务器发送过来的呢?我们知道,因为只有服务器才有私钥,所以能够确认只有对方才拥有私钥,那对方就是真实服务器,因此可为: ![20180930182012.png](https://i.loli.net/2018/09/30/5bb0a36c6d4a6.png) * {!@#%} 为 RSA 加密后的内容; * < a | b > 为用 a 密钥和 b 算法进行加密。 * {!@#%}<私钥 | RSA> 表示为用 私钥 对 “Server come” 进行加密后的结果。 为了向客户端证明自己是服务器,服务器先把一个字符串(Server come)用自己的私钥加密,把明文和加密后的密文一起发给客户端。客户端收到消息后,用自己的公钥解密密文,和明文进行比较,如果一致,说明信息的确是由服务器发过来的。因为由服务器用私钥加密后的内容由且只能由公钥进行解密,私钥只有服务器持有,所以如果解密出来的内容是能够对得上,那说明信息一定是从服务器发送过来的。 如果想 hack 服务器, ![WX20181008-153048@2x.png](https://i.loli.net/2018/10/08/5bbb080e06757.png) 在上图中的第四步{@@@@}<私钥 | RSA> ,这块是无法 hack 的,因为不知道私钥,无法用私钥加密,可以认定对方是个假货。 到这里,客户端可以确认服务器的身份了,可以放心的和服务器进行通信,但还有个问题,通信的内容在网络上还是无法保密, ![WX20181008-153912@2x.png](https://i.loli.net/2018/10/08/5bbb09b3a2a0c.png) 最后一步把用户的个人信息通过私钥加密发送出去了,因为公钥是所有人都知道的,除了这一个客户端,其它拥有公钥的客户端也能够对该条使用私钥进行加密的消息进行解密。一般会采用对称加密来解决这个问题。 ![WX20181008-154655@2x.png](https://i.loli.net/2018/10/08/5bbb0b80ee78e.png) 在上图所示的通信过程中,客户端在确认了服务器身份后,自己选择一个对称加密算法和密钥,把选择的加密算法和密钥通过公钥进行加密发送给服务端。这个过程如果被 hack 了,但因为没有私钥也无法解密使用公钥加密后的内容。 RSA 加密算法在这个通信过程中所起到的作用主要有两个: * 私钥只在服务器上,客户端可通过判断对方是有拥有私钥来保证是否为“真”服务器; * 客户端通过 RSA 的掩护,安全的和服务器商量好一个对称加密算法和密钥来保证后续通信过程内容的安全。 这引发出了一个新的问题,服务器要对外发布公钥,服务器如何把公钥发送给客户端呢?可能会想到: * 把公钥放到网络上的某个地址,都去这拿; * 每次通信开始都下发公钥。 第一个方法的问题出在下载地址会被伪造,第二个方法的容易被中间人伪造,因为我们都可以生成公钥和私钥,无法确认公钥到底是谁的,如果能够确认公钥到底是谁的即可解决。 为了解决这个问题,数字证书出现了。一个证书包含了以下内容: * 证书的发布机构 * 证书的有效期 * 公钥 * 证书的所有者 * 签名所使用的算法 * 指纹及指纹算法 这样,通信流程就变成了: ![WX20181008-155723@2x.png](https://i.loli.net/2018/10/08/5bbb0df548216.png) 上图中的第二次通信,服务器把自己的证书发给了客户端而不是公钥。客户端根据证书校验来确认证书是否为服务器的,后续操作都是一致的。 为了保证安全,在证书的发布机构发布证书时,证书的指纹和指纹算法都会加密后再和证书放到一起发布,以防有人修改指纹后伪造相应的数字证书。那证书的指纹和指纹算法用什么加密呢?答案是用那个证书发布机构的私钥进行加密的,也就是说,证书发布机构除了给别人发布证书外,自己本身也有自己的证书(一般由自己生成)。在 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 如下: ![demo 完成图](https://i.loli.net/2019/01/14/5c3c9fadb01a2.png) ### 数据来源 原本想直接使用公司 API 进行测试,这样能够快速验证在接触到实际数据的过程中,Flutter 在 UI 层面上和原生的优劣,但因保密等原因,只能另寻它路。最终从[历史上的今天](http://www.ipip5.com/today/api.php?type=json)和[豆瓣电影 Top250 ](https://api.douban.com/v2/movie/top250)两个 API 中选择了后者,原本只是想验证在长列表的展示上的优劣,但后来又考虑到了豆瓣电影 Top250 的 API 所提供的资源较丰富、数据格式也够复杂,算是比较贴合生产环境。 ![历史上的今天 数据格式](https://i.loli.net/2019/01/14/5c3ca009d4d51.png) ![豆瓣电影 Top250 数据格式(节选)](https://i.loli.net/2019/01/14/5c3c76e46258b.png) ### 涉及 Flutter 知识点 * HTTP; * JSON 数据格式解析和模型转换; * `Row` 、 `Column` 、 `Padding` 、 布局; * `ListView`、`Text` 等基本 `Widget` 使用; * `Container` 设置图片圆角、阴影等属性设置。 ## 实践 有了数据源,就可以准备上手搭建 UI 了。因为是豆瓣电影 Top250 的数据源,就直接 copy 了官方 App 的设计,同时也为了保证后续做性能验证时各种跨端和原生技术互相对比时遵循“单一变量”原则,但中途还是因为数据源的关系,有些数据并未暴露出来,导致没法 100% 的 copy 。 ![豆瓣电影 Top250 官方 UI](https://i.loli.net/2019/01/14/5c3ca05a29d29.png) ### 数据处理 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 页面的布局进行分析。 ![豆瓣电影 Top250 App 页面布局分析](https://i.loli.net/2019/01/14/5c3ca09e12042.png) 因为本来就没打算把这个 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+**:四星半; 正准备拍手叫好接入数据时,突然发现了一个令人尴尬的事情, ![《肖申克的救赎》 五星 | 《大话西游之大圣娶亲》 四星半](https://i.loli.net/2019/01/14/5c3ca12147a4c.png) 其实 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 具体的完成图: ![首页.png](https://i.loli.net/2019/01/17/5c3ff42164da7.png) ![下拉刷新.png](https://i.loli.net/2019/01/17/5c3ff4608b766.png) ![详情.png](https://i.loli.net/2019/01/17/5c3ff4ac36213.png) 整体 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 本身那般丰富。 ![《神秘巨星》电影详情数据.png](https://i.loli.net/2019/01/17/5c3ff7debdb93.png) ### 涉及 Flutter 知识点 * 下拉刷新; * 上拉加载; * 利用 `GestureDetector Widget` 进行页面跳转(动态路由方式); * 利用 `SingleChildScrollView Widget` 进行滚动视图的构建; * 简单性能分析。 ## 实践 ### 目录结构 ![目录结构.png](https://i.loli.net/2019/01/17/5c402f8110e56.png) ### 数据处理 电影详情 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(二期调研估计会继续做演员详情)。 ![演员数据格式.png](https://i.loli.net/2019/01/17/5c40188d4b459.png) 网络数据的获取因为有了上篇文章的铺垫,这次再写一个速度明显快了很多,一期调研完整的网络层方法如下: ```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% 的重写。经过分析后主要将页面分为了以下几部分: ![豆瓣电影详情 UI 布局分析](https://i.loli.net/2019/01/17/5c403b4144258.png) #### 第一部分 第一部分与上篇文章中所讲述的布局编写思路大部分一致,对于我自己来说有个需要注意的地方,在第一部分中有个“豆瓣电影排名”的 `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, ) ``` 当显示出来后,不管怎么调整样式、修改颜色、文字等都不管用。最后带着郁闷的心情浏览官方文档,居然发现了这么一段话: ![RaisedButton 官方解释](https://i.loli.net/2019/01/17/5c401d92c1bdf.png) 嗯,就算我们并不想让这个 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 的支持是相当丰富,具体见下图: ![查看当前页面节点树](https://i.loli.net/2019/01/17/5c402c1916281.png) ![分析工具](https://i.loli.net/2019/01/17/5c4030aedce1e.png) ![分析工具.png](https://i.loli.net/2019/01/17/5c4031b790f79.png) ![性能相关(部分)](https://i.loli.net/2019/01/17/5c402f3580f77.png) ## 其它 ### 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` 的效果。 ![左:iPhone 7,右:meizu 15](https://i.loli.net/2019/01/10/5c36f04c618e7.jpg) flutter 官网上对于“热重载”是这么描述的: > 通过将更新后的源代码文件注入正在运行的 `Dart` 虚拟机(VM)中来实现热重载。在虚拟机使用新的的字段和函数更新类后,`Flutter` 框架会自动重新构建 `widget` 树,以便您快速查看更改的效果。 所以对于在 meizu 15 上需要执行两次保存操作才能触发“热重载”后的效果展示,我的推测是,在第一次执行保存操作时要么没有把新更新后的代码注入进 `Dart` 虚拟机中,要么就是注入了但未触发重新自动构建 `widget` 树。 ### 渲染 ![Flutter 在 iOS 上的视图层级](https://i.loli.net/2019/01/10/5c37187ca736f.png) ### 差异点 * 入口的 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 ## 基本 ### `