流式响应(Chat):HTTP 响应体字节流的读取与解析

主要通过 web fetch api 的 ReadableStream 能力,解释 HTTP 通道中的流式响应。 流式关键在于 HTTP 协议层解析 Body 体,和 TCP 粘包和拆包的处理类似。各种编程语言都具有 body 二进制数据流的拦截和解析能力。 Fetch API 速览 await fetch(url) → 返回 Response 对象:这时浏览器通常已经从底层连接(TCP/QUIC)里拿到并解析完 HTTP 响应行(status)+ 响应头(headers);但 响应体(body) 还没被消费 / 解析(它会通过单独的流式接口暴露出来,供上层代码增量读取)。 await res.json() / await res.text() → 一次性读取并解析完整 Body,适用于非流式场景。 res.body → ReadableStream<Uint8Array>,字节流接口,可增量消费。 res.body.getReader().read() → 手动 Pull 模式,每次返回 { value: Uint8Array, done: boolean }。 可以这样理解:fetch() 先把 status/headers 拿到手并封装成 Response;至于 body 字节 怎么读、怎么解析,由调用方选择: 通过 ReadableStream 增量读取(适合 AI token、NDJSON、SSE 等流式场景)。 或调用 json() / text() 这类封装好的方法,让它们内部把 body 全部读完后再一次性解析。 核心区别:res.json() 等 “便捷方法” 会等待整个响应体下载完毕后一次性解析;reader.read() 则支持边接收边处理,是流式读取的基础。 1 2 3 4 5 6 7 8 9 10 11 12 13 // 非流式:等 Body 全部接收完再解析 const full = await (await fetch(url)).json(); // 流式:边接收边处理(Chunk 边界不等于消息边界) const res = await fetch(url); const reader = res.body!.getReader(); const decoder = new TextDecoder("utf-8"); for (;;) { const { value, done } = await reader.read(); if (done) break; const chunkText = decoder.decode(value, { stream: true }); // handle(chunkText) } SSE 与 Fetch 很多 “Chat 流式输出” 看起来像 Server-Sent Events (SSE),但底层实现常见就两类: SSE(协议 + API):text/event-stream + EventSource(浏览器负责按协议分帧并提供重连等语义)。参考 SSE 指南。 Fetch 响应体流式读取(传输能力 + 自定义分帧):fetch() + Response.body(拿到的是 ReadableStream<Uint8Array>,需要自己定义消息边界与语义,如 NDJSON / 长度前缀等)。 顺带一提:很多实现也在从 SSE/EventSource 转向 fetch stream,因为 SSE 本身有不少限制(比如只能 GET、Header / 鉴权不够灵活等)。 共同点:两者都依赖 “长连接 + 增量写入 + 及时 Flush”,最后在客户端看起来都是 HTTP 响应体字节流。 一句话区别:SSE 把 “怎么切消息 + 事件语义” 都标准化了;Fetch 流式读取只把字节暴露给应用层,剩下由应用协议来定。 分帧:SSE 固定是 data: 行 + 空行;Fetch Stream 的边界完全自定义(NDJSON、长度前缀、分隔符等)。 语义:SSE 浏览器内建重连、Last-Event-ID;Fetch Stream 想要重连 / 断点续传 / 错误语义,需要在应用层设计。 适用场景:SSE 更像 “标准事件流”;而 Chat 经常需要 POST、鉴权 Header、以及自定义协议时,Fetch Stream 会更顺手。 链路传输(从服务端 write 到客户端拿到 chunk) 想要真的 “边推边显示”,关键往往不是 HTTP 语法本身,而是:字节在链路的哪一段被缓冲住了。 服务端应用写出:应用调用 write()/send() 将字节写入 Socket。若仅写入用户态缓冲而未执行 flush(或被框架 / 中间件缓冲),客户端将无法接收增量数据。 TCP Socket 缓冲区(发送 / 接收):send() 通常只是把数据拷贝进 TCP 发送缓冲区,真正 “发出去” 要看 TCP 栈怎么分段、流控 / 拥塞控制允不允许。结果就是:应用 write 的粒度,基本不等于 对端 read 到的粒度(还会受 Nagle / 延迟 ACK/cwnd/rwnd 等影响)。 HTTP 承载方式: HTTP/1.1:通常使用分块传输编码(Chunked Transfer Encoding)或在未知 Content-Length 时持续写入。 HTTP/2 / HTTP/3:基于 DATA Frame 或 QUIC Stream 持续传输,受多路复用与流控机制影响。 浏览器网络栈 → ReadableStream:浏览器将 “已到达且可用” 的字节推入 ReadableStream 内部队列,JavaScript 通过 reader.read() 或 pipeThrough() 以 Pull 模式消费。 Chunk 边界 ≠ 消息边界:一次 read() 获取的 Uint8Array 仅是当前可用的字节片段,可能截断在任意位置(如 UTF-8 字符中间、JSON 结构中间或自定义帧头中间)。 全链路缓冲会 “假装不流式”:应用层 Flush、反向代理 Buffering、压缩器缓冲、CDN 策略、浏览器内部队列…… 任何一段在攒数据,都会让 Token 看起来变成 “凑一批才到”。 Fetch 流式读取的基本模型 fetch() 返回的 Response 对象包含 body 属性,其类型为 ReadableStream<Uint8Array>。消费方式主要有两种: 手动读取:获取 reader 并循环调用 read()。 管道处理:使用 pipeThrough() 构建解码、分帧、解析的流水线(推荐)。 整体可以当成 pull 模式:每次 read() 拿到的是 “目前已经到手的那点字节”。如果处理速度慢于网络进入速度,队列就会堆起来,进而触发 背压(Backpressure)(后续传输会被放慢)。 示例:解析消息流 字节解码:处理增量 UTF-8 网络传输交付的是 Uint8Array,而 Chat 最终要的是文本 Token。注意 UTF-8 是变长编码,一个字符可能被拆到两个 Chunk 里;如果直接 decoder.decode(chunk)(默认非流式),边界处就可能乱码 / 丢字。这里要用增量解码: 使用 TextDecoder 的 { stream: true } 选项。 或使用 TextDecoderStream 管道:response.body.pipeThrough(new TextDecoderStream()),直接获得 ReadableStream<string>。 分帧策略:定义消息边界 想做到 “边接收边渲染”,需要先定一个能增量解析的分帧(Framing)规则:到底每条消息怎么切出来? NDJSON / JSON Lines(推荐) 格式:每条消息占一行,如 {"type":"delta","text":"..."}\n。 优点:解析简单,调试友好,兼容 JSON.parse。 注意:需确保 Payload 内无未转义的换行符(标准 JSON 字符串会将换行编码为 \n,通常安全)。 分隔符协议 格式:使用自定义分隔符(如 \n\n 或特定 Boundary)切分消息。 风险:若 Payload 包含分隔符,需进行转义或设计复杂的 Boundary 机制。 长度前缀(二进制 Framing) 格式:[Length][Payload]...。 优点:对任意二进制或文本内容安全,不受内容字符影响。 缺点:实现复杂度较高,需维护字节级状态机。 一般优先选 NDJSON 或 长度前缀;千万别指望 Chunk 边界刚好对齐业务消息边界。 Fetch + NDJSON 字节读取 → 文本解码 → 行分帧 → JSON 解析 → UI 更新 的完整流程: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 type ChatChunk = { type: "delta"; text: string } | { type: "done" } | { type: "error"; message: string }; export async function streamChat( input: { prompt: string }, onChunk: (c: ChatChunk) => void, signal?: AbortSignal, ) { const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), signal, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.body) throw new Error("ReadableStream not supported"); // 1. 字节读取层:获取 ReadableStream reader const reader = res.body.getReader(); // 2. 文本解码层:处理 UTF-8 边界 const decoder = new TextDecoder("utf-8"); // 3. 行分帧层:缓冲未闭合的行 let lineBuffer = ""; try { while (true) { // 读取下一批字节(Chunk 边界随机) const { value, done } = await reader.read(); if (done) { // 流结束,处理缓冲区剩余内容 if (lineBuffer.trim()) { const msg = JSON.parse(lineBuffer.trim()) as ChatChunk; onChunk(msg); } break; } // 增量解码:stream: true 保留不完整字节序列 const text = decoder.decode(value, { stream: true }); lineBuffer += text; // 按换行符切分 const lines = lineBuffer.split("\n"); // 最后一个元素可能是不完整行,留待下轮处理 lineBuffer = lines.pop() ?? ""; // 4. JSON 解析层:逐行解析 for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; // 跳过空行 const msg = JSON.parse(trimmed) as ChatChunk; onChunk(msg); // 5. 业务终止:显式结束信号 if (msg.type === "done") return; } } } catch (e) { throw e; } finally { reader.releaseLock(); } } 服务端 send: 将增量 Token 封装为可切分的消息单元(行),确保客户端始终解析完整的 JSON 对象。 增量 Token:{"type":"delta","text":"..."}\n + Flush 结束信号:{"type":"done"}\n + End Response

2025/12/29
articleCard.readMore

iPhone 截屏高效翻译

工作生活中可能有以下多语言的困扰: a. 在 iPhone 小屏幕上工作,临时切不到 Mac 等多效率平台。 b. 多国语言的文本需要阅读、处理,如 email、app 使用、社交媒体等。 c. 语言学习能力很渣。 AI 在文本领域已经足够强大,翻译领域更是和 code 领域一起,是第一批被攻下神坛的大山。 以下提供一些 iPhone 设备上能够『较快、较高质量』的完成文本翻译的方案。 系统 Translate (翻译) app Version: iOS 26+ iPhone 系统自带的 Translate 功能,目前已经提供了较完备的翻译能力。 打开 app 后甚至可以和其他人面对面语音输入并翻译沟通。除了需要打开 app 外,iPhone 还提供了以下翻译的隐藏入口: a. 网页内容,长按文本,弹出框中选择『翻译』 b. 截屏,文本 OCR 识别后,旁边显示『翻译』 c. 拍照,文本 OCR 识别后,旁边显示『翻译』(对准文本区块即可,不用点击拍摄) 以上三个隐藏入口,会唤起系统级别的翻译弹窗面板,可以快速的解决近乎所有需要翻译的场景(尤其 b - 截屏)。特别说明: 如果是 Safari 浏览器阅读文本,通过配置广为流传的「沉浸式翻译」,搭配自定义 ai 模型后,效果会很好。 如果是 app 中内嵌的 web 浏览器,通过 a 方案,效率就已经很高了。 有些网页实在是烂,长按选择文本很吃力,就切到 b 方案。 对于 c 方案,如果有大片文本在不同区域,可能只识别一个区块。可以直接截屏切到 b 方案。 截屏方案是非常好用的大杀器,对任何文本、app 都生效。还可以自定义很多快捷方式来启动截屏。 有一个显著的优化建议:有时候「截屏 - 文本识别 - 翻译」后,文本会很小,看不清。这个时候可以双指捏合来放大截屏内容,然后再翻译。 Translate app iOS 26+ 系统对非官方 app 开放了系统级的翻译入口。有不少 ai app 都接入了这个入口。好处是: a. 普通 app 可以直接通过上面系统 Translate 的入口(截屏、长按等)来唤起普通 app 的翻译面板。 b. 普通 app 可以使用 ai 模型进行翻译,提升质量。 走到这一步,还是 Apple 太邋遢。它掌握着翻译的入口、但翻译能力有时候真不太行。 操作:在系统设置中,将翻译的默认 app 设置成自己正在使用的 ai app 就可以了(如 OpenCat、FlowDown)。 其他建议 a. AI 模型推荐使用 Gemini-2.5-flash-lite。速度贼快、质量极好、价格低廉。在文本处理上,不可能三角被它打下来了。 b. 如果有条件,一定切到 AI 模型。系统 Translate 的质量有时候真的堪忧。尤其很多工作生活场景,文本会出现换行,它处理的就不好。 c. 若使用 iPhone 的同时也正在使用 Mac,通过「iPhone Mirror」可以投屏。Mac 上翻译的工具就太多了(如 Bob)。

2025/12/14
articleCard.readMore

Flash Open Terminal

问题 (TLDR):zsh 已经成为标配,但每次启动它都需要等待 1-2s,很烦。 问题 (Detail):.zshrc 配置中有很多耗时插件和功能,如 nvm、jenv 等等,打开终端很慢。所有 lazy load 等方案都会在各个旮旯里影响原有的启动逻辑,且维护成本高。 解决方案:ITerm2 & Tmux。 场景还原: .zshrc 有很多功能需求配置,已经影响到了 terminal 启动速度。 工作期间需要开启 n 个 app 画面,非常乱。而对于 ternimal 总是习惯性使用完 kill。每次打开都会 waiting 很久。 终端使用 Warp 和 Iterm2,app launch 使用 Raycast。 解决方案: 使用 iterm2 创建一个 profile,设置为 default (即 app 启动后默认打开当前 profile)。 General - Command 选择 Command,命令配置: 1 /usr/bin/env zsh -c '/opt/homebrew/bin/tmux new -As dev' tmux 使用较简单,了解下就可以熟练操作。 后续工作中需要快速执行命令,通过 Raycast 打开 iterm2 即可,0ms 延迟启动。 使用完可直接 kill iterm2 窗口,依靠 tmux 做会话保持。 其他场景,按个人喜好使用 Warp 即可。 结果: Iterm2 自身启动耗时 0ms,tmux attach sesstion 0ms。 配合一下,终端秒开,快速执行工作生活中的小命令,再也不用处理 .zshrc 多配置启动慢问题。 扩展: shell 启动参数 -l:以登录 shell 启动,读取登录相关配置(如 ~/.zprofile),保证 PATH、环境变量齐全。 -i:以交互式 shell 启动,启用提示符、job control,并读取交互配置(如 ~/.zshrc)。 -c:执行后面的一条命令字符串并退出,常与 -l、-i 组合,用来在「完整环境」里执行单条命令。 配置了 -lic,就会走完整的模拟器登录交互环境,保证各种环境变量、插件都能正常加载。这也是文档开头说到的问题根源。即 .zshrc 等配置启动太耗时了。 配置 -c,就在裸环境执行 shell 命令,非常快。但这种场景下是裸环境,各种 shell 配置都没有。只应该在必要的场景使用。 更多 Command 配置示例 -lic 示例: /usr/bin/env zsh -c "/opt/homebrew/bin/tmux has-session -t dev 2>/dev/null && /opt/homebrew/bin/tmux attach -t dev || zsh -lic '/opt/homebrew/bin/tmux new -s dev'" 完全等价于: /usr/bin/env zsh -c '/opt/homebrew/bin/tmux new -As dev' 解析:tmux Check 和 Attach 的时候使用的 -c,new 的时候,tmux 会在 session 内部通过 -lic 启动 shell (tmux 默认行为)。 tmux 跟随待执行命令: /usr/bin/env zsh -c '/opt/homebrew/bin/tmux new -As xxx "zsh -lic \"exec npx xxx@latest\""' 解析:tmux new session 的时候,如果跟随了待执行命令,就不在走上面提供的默认行为 (-lic),即 tmux 默认在裸环境下执行待执行命令。需要手动补齐登录交互环境,即 zsh -lic "exec npx xxx@latest"。

2025/11/13
articleCard.readMore

iPhone 侧载安装

iPhone 上安装一个非 App Store 的 app 太难了,Apple 生态基于【公私钥】非对称检测,使得专业的行内人员也无法很好的在开发机上随意安装三方 app。 这里有一些对普通人来说也比较方便的实施方案。安全性上,除了被安装的 ipa 包本身可能有风险,其他链路很安全。 非 App Store 版本的 app,来源有 github、三方分享平台等。 app 安装需要 ipa 包。App Store 上下载的就是 ipa 包,这里需要通过 github、telegram 等渠道获取。 普通、小白、非专业、非 IT 用户 通过 AltStore,就可以非常方便的安装 ipa 包了,具体流程需要查看下文档。 需要 Mac / Window 电脑 + 个人的 AppleId。安全性上,非常安全,技术方案是: Apple 开发人员需要 Apple Account 才能开发 / 发布 app 到 App Store。前些年 Apple 开放了门槛,使用个人 Apple Id(free account) 也能够进入开发流程,但是 app 不能上架 App Store。 free account 有限制:一个 iPhone 同一时间最多只能安装 3 个 app + 一周只能激活 10 个 app(每个 app 都有唯一 id,一周最多 10 个 id) 通过对三方渠道获取的 ipa 使用 free account 进行重签名(骗过 iPhone 公私钥检测),假装自己是 app 的开发人员,就可以安装到自己的 iPhone 上。 缺点: 每个 app 安装 7 天后就打不开了,需要通过 AltStore 将 iPhone 连接上 Mac/Window 后重新安装下(free account 限制)。 同一时间最多安装 2 个 app(free account 合计 3 个,有一个是 AltStore app)。 更进一步 上面列出的 2 个缺点,一般人用用也够了。如果想方便一些,通过 SideStore 可以更进一步的解决一个小痛点 (其他缺点均无法解决): SideStore 的重签流程放到了 iPhone 上,可以脱离 AltStore Mac/Window 了。 这样,可以很方便的在 iPhone 上完成 7 天失效的【续重签名】,不用打开电脑了。 Apple 开发人员 Apple 开发者都有 Apple Account,AltSotre 除了支持个人 AppleId,也支持开发者账号。上面的两个缺点就都解决了。 开发者证书签名的 app 有 1 年有效期,足够用了。 单点登录 Apple Account 有些开发者账号是公司提供的,管理员关闭了账号密码直接登录,就无法在 AltStore 使用了。这个时候要进行深度探索了。 有一个 app/ipa,叫【全能签】。主动设置开发证书后,全能签通过证书来完成 ipa 重签名并安装到 iPhone。原理也是和 AltStore 一样,都是 ipa 重签名,骗过 iPhone 公私钥检测。 这个流程的具体操作是: 通过 AltStore 安装 全能签(全能签 a,7 天有效期) 设置 全能签(开发证书等) 通过 全能签 安装 全能签(全能签 b,1 年有效期) 第三步通过全能签 a 自举 安装的 全能签 b,有 1 年有效期。 这个时候,就可以把 AltStore、全能签 a 等全部卸载了,仅保留 全能签 b。 后续所有的 ipa 包,都可以通过 全能签 b 来安装,有 1 年有效期,够用了。 其他 之前越狱很有名,现在基本灭绝了。不过衍生出来一个新的分支,叫【巨魔】,又称【半越狱】。 巨魔 可以方便的安装 ipa 包,因为走的系统漏洞,app 不会过期,长久有效。 巨魔 技术上需要使用系统漏洞,而 Apple 会更新补丁,所以最新系统基本都无法使用 巨魔。 而巨魔之所以存在,因为非 App Store app 依旧有一批市场。 So,如果想了解上面介绍的【侧载】安装的应用场景,还是需要先进入这片字面意义上,不正规 / 灰色的领域。

2025/9/7
articleCard.readMore

Xcode Symbolic Debug

Xcode 的 Symbolic Breakpoint(符号断点)在排查问题的时候非常好用,尤其在三方闭源库联调的时候。 在闭源三方库中,如果能根据公开的 Api 找到一些有用的信息,还是非常 nice 的。 不知道什么时候开始,符号断点的内容格式非常严格,不然无法被断点。虽然 Xcode 给了如下提示,但那么多符号,很难短时间处理好: 1 2 3 4 5 Xcode won't pause at this breakpoint because it has not been resolved. Resolving it requires that: • The symbolic name is spelled correctly. • The symbol actually exists in its library. • The library for the breakpoint is loaded. 简单来说,之前通过快捷键可以将当前指针所在的 Symbolic 快速录入到搜索框中进行搜索(xcode 支持符号检索),然后把输入框内容复制到 Breakpoint 就能进行符号断点了。现在死活断不到。 需要:绝对准确的符号签名,包括 static、参数、返回值 等等全量信息,少一点就断不成。 快速的方案,是在 Debug 中执行 image lookup -rn xxx 来查找整个工程中特定符号的内容,在其中找到准确的符号签名后,再设置到 Breakpoint 中。这里使用的 xcode lldb 命令,输出内容一不小心就闪瞎眼。 还有一个比较快捷的方案是使用 https://github.com/DerekSelander/LLDB,通过其 lookup xxx 命令,就可以非常整洁的整理出来所需符号的完整签名信息。copy 一下就能使用了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // e.g. (lldb) lookup initApp **************************************************** 1 hits in: EleSDK **************************************************** static EleSDK.Ele.initApp(key: Swift.String, apiURLString: Swift.Optional<Swift.String>) -> () **************************************************** 4 hits in: ManagedConfiguration **************************************************** +[MCLazyInitializationUtilities initAppleIDSSOAuthentication] __61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke __61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke_2 objc_msgSend$initAppleIDSSOAuthentication 最后,也比较一下 image lookup -rn xxx 的结果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 (lldb) image lookup -rn initApp 1 match found in /xxx/Ele-ios-demo-swift-gfeqjptpbrkqcrborvnyugpagjfl/Build/Products/Debug-iphoneos/Ele-ios-demo-swift.app/Frameworks/EleSDK.framework/EleSDK: Address: EleSDK[0x000000041efc] (EleSDK.__TEXT.__text + 237308) Summary: EleSDK`static EleSDK.Ele.initApp(key: Swift.String, apiURLString: Swift.Optional<Swift.String>) -> () 4 matches found in /Users/hailv/Library/Developer/Xcode/iOS DeviceSupport/iPhone17,3 18.6.2 (22G100)/Symbols/System/Library/PrivateFrameworks/ManagedConfiguration.framework/ManagedConfiguration: Address: ManagedConfiguration[0x00000001a1bc9a2c] (ManagedConfiguration.__TEXT.__text + 225740) Summary: ManagedConfiguration`+[MCLazyInitializationUtilities initAppleIDSSOAuthentication] Address: ManagedConfiguration[0x00000001a1bc9ab4] (ManagedConfiguration.__TEXT.__text + 225876) Summary: ManagedConfiguration`__61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke Address: ManagedConfiguration[0x00000001a1bc9b18] (ManagedConfiguration.__TEXT.__text + 225976) Summary: ManagedConfiguration`__61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke_2 Address: ManagedConfiguration[0x00000001a1cc8a20] (ManagedConfiguration.__TEXT.__objc_stubs + 22656) Summary: ManagedConfiguration`objc_msgSend$initAppleIDSSOAuthentication 找到 func 签名,挺费事儿的。

2025/8/22
articleCard.readMore

AI 指北

整理 & 学习了 AI 相关的知识点。这里把 PPT 内容放一下,详细内容可查阅 via https://yigegongjiang.notion.site/AI 主题 神经网络抽象了现实世界 ToC 1. 数学对现实的抽象 2. 二元感知机 - 19573. 感知机升维困境 - AI 寒冬 4. 正向传播 & 反向传播 - 19745. 梯度下降 - 古老的算法 6. 多层感知机 MLP - 1986 7. 卷积神经 CNN - 19988. 自注意力机制 - 20179. Transformer 流程解析 10. AI - 缸中大脑 11. Prompts 是 AI 入场券 12. 调参 - 进一步掌控 AI 13. AI 悬停点 - 2025.06 缩略图预览 大图预览

2025/6/30
articleCard.readMore

投资风险收益评估

风险等级说明: R1: 极低风险 (年化回报率: 3.0-5.5%) R2: 低风险 (年化回报率: 3.5-6.0%) R3: 中低风险 (年化回报率: 4.5-7.5%) R4: 中等风险 (年化回报率: 5.0-10.0%) R5: 中高风险 (年化回报率: 7.0-13.0%) R6: 高风险 (年化回报率: 5.0-20.0%, 损失风险: -50% 至 - 70%) R7: 极高风险 (年化回报率: 15% 以上 (部分品种远超此范围), 损失风险: -50% 至 - 100%) 金融投资产品(按风险从低到高排列): R1 年化回报率: 3.0-5.5% 储蓄账户 (Savings Accounts) 年化回报: 0.5-4.5% 国库券 (Treasury Bills) 年化回报: 4-5.5% 存款证 (CDs - Certificates of Deposit) 年化回报: 3.5-5.0% 货币市场基金 (Money Market Funds) 年化回报: 4.0-5.5% (通常属于共同基金的一种) R2 年化回报率: 3.5-6.0% 政府债券 (Government Bonds) 年化回报: 4.0-5.0% 市政债券 (Municipal Bonds) 年化回报: 3.0-5.5% (风险通常低于公司债券,但高于国债,税前回报) 年金 (Annuities) 年化回报: 4.0-7.0% (固定年金部分参考: 4.5-5.8%,变额年金风险和回报波动大) R3 年化回报率: 4.5-7.5% 债券 (Bonds) 年化回报: 4-7% (泛指,风险高于政府债券) 公司债券 (Corporate Bonds) 年化回报: 5.0-6.5% (投资级公司债) 债券基金 (Bond Funds) 年化回报: 3.5-6.0% (投资于上述债券的基金,回报取决于具体配置) 优先股 (Preferred Stock) 年化回报: 5.5-8.0% (风险通常介于债券和普通股之间) R4 年化回报率: 5.0-10.0% 共同基金 (Mutual Funds) 年化回报: 6-12% (这是一个大类,其下具体基金风险各异,回报取决于投资标的) 混合基金 (Balanced Funds) 年化回报: 6.0-9.0% (同时投资于股票和债券) 指数基金 (Index Funds) 年化回报: 4.0-7.0% (主要指债券指数基金及其他低风险指数基金;股票指数基金回报见 R5) 交易所交易基金 (ETFs - Exchange Traded Funds) 年化回报: 4.0-7.0% (主要指债券 ETF 及其他低风险 ETF;股票 ETF 回报见 R5) 可转换证券 (Convertible Securities) 年化回报: 5.0-9.0% 外汇 (现汇交易 / 非杠杆) (Foreign Exchange - Spot/Non-leveraged) 年化回报: 1-8% (高度不确定,含利率差和汇率波动) R5 年化回报率: 7.0-13.0% (损失风险: -20% 至 - 40%) 股票 (Stocks) 年化回报: 8-12% (蓝筹股或大型稳定公司股票) 股票基金 (Stock Funds) 年化回报: 9-13% (投资于股票的基金,主动管理型) 股票指数基金 (Stock Index Funds) 年化回报: 8-12% (追踪股票指数的基金) 股票类 ETFs (Stock ETFs) 年化回报: 8-12% (追踪股票指数的交易所基金) 房地产投资信托 (REITs - Real Estate Investment Trusts) 年化回报: 7-11% (总回报,含股息和资本增值) 房地产 (Real Estate) 年化回报: 6-10% (直接投资,总回报,注意高持有成本和低流动性) 住宅房地产 (Residential Real Estate) 年化回报: 5-8% 商业房地产 (Commercial Real Estate) 年化回报: 7-12% (近年面临挑战) 企业年金 / 退休计划 (Pension Plans/Retirement Plans) 年化回报: 6-10% (风险和回报高度取决于计划内的具体投资组合) R6 年化回报率: 5.0-20.0% (损失风险: -50% 至 - 70%) P2P 借贷 (Peer-to-Peer Lending) 年化回报: 5-12% (净回报预期,高违约风险) 高收益债券 / 垃圾债券 (High-yield Bonds/Junk Bonds) 年化回报: 6-10% (总回报预期,或 Yield to Worst 7-9%) 小盘股 (Small-cap Stocks) 年化回报: 9-15% (成长型小公司股票,高波动性) 新兴市场股票 (Emerging Market Stocks) 年化回报: 5-15% (长期平均回报,高波动性) 大宗商品 (Commodities) 年化回报: 0-10% (长期平均回报,波动极大,可能负回报) 贵金属 (Precious Metals - e.g., Gold, Silver) 年化回报: 2-10% (黄金波动相对较低,白银波动大) 能源 (Energy - e.g., Oil, Natural Gas) 年化回报: -5-15% (波动极大,高风险) 农产品 (Agricultural Products - e.g., Corn, Wheat) 年化回报: 0-12% (波动极大,高风险) 众筹投资 (Crowdfunding Investments) 年化回报: -50% 至 + 50% (通常是初创企业股权,风险极高,高失败率) 结构性产品 (Structured Products) 年化回报: 5-20% (风险复杂且不透明,回报高度依赖具体结构) 收藏品 (Collectibles) 年化回报: 2-10% (流动性极差,价值波动大,持有成本高) R7 年化回报率: 15% 以上 (部分品种远超此范围) (损失风险: -50% 至 - 100%) 私募股权 (Private Equity) 年化回报: 12-25% (净 IRR 预期,高风险、高门槛、长周期、低流动性) 风险投资基金 (Venture Capital) _年化回报: 15-30%+ _ (早期高成长性投资预期,整体回报差异极大,高失败率) 对冲基金 (Hedge Funds) 年化回报: 5-20% (策略多样,大类资产平均预期,少数可能更高) 期权 (Options) 年化回报: -100% 至 + 数倍 (极高风险,专业性要求高) 期货 (Futures) 年化回报: -100% 至 + 数倍 (极高风险,专业性要求高) 杠杆 ETF (Leveraged ETFs) 年化回报:高度波动 (-90% 至 + 数倍可能), 不宜长期持有 外汇 (Forex - Foreign Exchange) 年化回报: -100% 至 + 数倍 (杠杆交易风险极高,专业性要求高) 二元期权 (Binary Options) 年化回报:高投机,回报范围极大,通常不被视为正规投资 加密货币 (Cryptocurrencies) 年化回报:极高波动 (-90% 至 + 数千百分比可能), 投机性强,风险巨大 微盘股 (Penny Stocks) 年化回报:极高风险 (-95% 至 + 数倍可能,大概率亏损)

2025/5/25
articleCard.readMore

Mac 快速修改系统快捷键

Macos 系统快速调整快捷键方案: 在 system settings 中清理不需要的快捷键,把核心快捷键用来自定义 Raycast - 最方便的快速创建自定义快捷键解决方案 Hammerspoon - 复杂疑难病症的解决方案 操作说明和快捷键建议指南如下。 系统快捷键设置 入口:system settings - keyboard Press 地球 key to 调整为 nothing 候选项有 Change Input Source、Show Emoji & Symbols、Start Dictation Input Source 通过 Input Source Pro app 可以快速设置(支持快捷键) Emoji & Symbols 通过系统全局 Control + CMD + Space 呼出,不需要快捷键 Start Dictation 如果不需要这个功能,不用开启 Dictation - Shortcut: 改成不需要常用快捷键的方式 Keyboard Shortcuts - 这里面是系统提供的很多没必要的快捷键,不需要的都可以取消掉 上面三个步骤设置完成后,核心的快捷键都可以留给我们自己来分配了。 通过 Raycast 修改 Raycast 非常好用,通过 Hotkey 可以快速设置系统级快捷键,非常方便。 打开 app:一般不需要,通过 Raycast 启动入口打开 app 已经很快了 Script Command:这个非常好用,很多系统级别的能力,可以通过 sh/applescript 的形式执行,分配一个快捷键后非常方便。 e.g. 写一个【当前最前置 window】移动到下一个屏幕上(多屏幕场景),增加 [Double Click CMD] 快捷键。 通过 Hammerspoon 修改 Hammerspoon 是非常强大的快捷键执行器,解决 any 疑难杂症。 在多屏幕场景下,很难实现【移动鼠标到指定屏幕】,Hammerspoon 可以实现。 快捷键建议 1 只手能操作的快捷键,绝不留给 2 只手 Ctrl、Option、Cmd 和 q/w/e/r/a/s/d/f/z/x/c/v 组合 尽量少用 Shift Double Click Ctrl、Double Click Option、Double Click Cmd 这三个最方便,要留给最常用的操作 很多时候快捷键操作的都是【文件 / 文件夹】,Mac Finder 支持太弱了,上 QSpace Pro 就非常方便(使用 Raycast Script Cammand 也很方便)。 快捷键的脚本怎么写,别自己写,让 AI 写。

2025/5/19
articleCard.readMore

Git Record

SSH / Personal Access Tokens / GPG Keys / Signing Key / ssh-agent 裸中央仓库 /worktree … Email 通过 ssh key 等方式操作 github 等仓库平台时,只要 ssh 验权通过就可以进行仓库操作。不过对于 git commit 等提交操作,git 会强制要求配置 username 和 email。 不过,email 一定要配置好,和账号的 email 一致。github 虽然不对 email 进行操作验证,但是在显示 verified 等标记的时候,还是会校验 email 的。如果 email 不对,则标记 Unverified。 整体来说,虽然不强求,但尽量配置好。 SSH Key github 这些 git 平台,除了 https 之外,也支持 git 协议的 ssh 操作。 对于公私钥,本机主要使用私钥,公钥在 github 远程平台上设置。 1 2 3 4 5 6 7 8 9 10 tree ~/.ssh/ /Users/example/.ssh/ ├── config ├── id_ed25519_personal ├── id_ed25519_personal.pub ├── id_ed25519_work ├── id_ed25519_work.pub ├── known_hosts ├── company-key └── company-key.pub 当需要在一台主机上管理多个 github 账号的时候,需要在 config 文件中进行如下配置: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 > cat ~/.ssh/config Host github-personal HostName github.com User git IdentityFile ~/.ssh/id_ed25519_personal Host github-work HostName github.com User git IdentityFile ~/.ssh/id_ed25519_work Host company-github HostName github.company.com User git IdentityFile ~/.ssh/company-key 对应的 git clone 地址也需要做改变: 1 2 3 4 5 git clone git@github.com:octocat/test.git -> git clone git@github-personal:octocat/test.git SSH 与 Personal Access Tokens 的关系 一般操作,使用 SSH Key 的情况下,不需要设置 PAT。 核心区别 SSH 认证 使用 git@github.com:user/repo.git 格式地址 依赖本地 ~/.ssh/ 目录的密钥对 通过 ssh -T git@github.com 验证连接 HTTPS 认证 使用 https://github.com/user/repo.git 格式地址 需要配置 PAT 替代密码(GitHub 已禁用密码认证) 协议检查方法 1 2 3 git remote -v # 显示 git@github.com → 使用 SSH # 显示 https://github.com → 需要 PAT 需要 PAT 的场景 调用 GitHub REST API 使用 GitHub CLI (gh) 操作敏感资源 访问 GitHub Packages 服务(npm/Docker 等) 操作其他 HTTPS 协议的仓库时 第三方 CI/CD 工具集成 使用 GitHub Actions 需要访问仓库时 使用双重认证 (2FA) 的账户通过 HTTPS 协议操作仓库 mac 钥匙串存储 PAT git 客户端(命令)会在用户输入 PAT 后,将 PAT 存储到 mac 的 keychain 中,并和 github domain 绑定。 这个时候,如果有多个 github 账号,就会发生冲突,导致其中一个 github 账号下面的 pat 验证失败。 所以,需要使用 https://github.company.com/xxx/xx.git 或 https://alias.github.com/xxx/xx.git 这样的格式,来对不同的 github 账号进行区分。 URL 使用 PAT URL 协议本身是支持增加 username 和 password 的,即 https://username:password@github.com/xxx/xx.git。 这个时候,可以把 username 换成 github 账号,password 换成 PAT,就可以在通过 github 授权的情况下操作 xx 仓库了。 原理: git 客户端会解析 URL,将 username 和 password 提取成 a:b 的格式,进行 base64 编码,放到 “Authorization: Basic ???” 头中。 服务器端接收到请求后,会进行 base64 解码,获取到 username 和 password,然后进行验证。 不推荐,简单实用一下还是可以的。 Signing Key github 推出的 commit/tag 等操作的签名认证。被签名的 commit 会在历史记录中显示一个绿色的 verified 标记。 GPG Keys 是专门做这个事情的,其中,github 也推出了自己的 Signing Key,可以复用 SSH Keys 的设置(操作入口在同一个地方)。 具体配置如下,通过在 ~/.gitconfig 中增加 url 分流配置,可以为不同的 host 组设置不同的 signing 规则。 1 2 3 4 5 6 7 8 9 10 // ~/.gitconfig-github-personal [user] signingkey = ~/.ssh/id_ed25519_personal [commit] gpgsign = true [gpg] format = ssh [tag] gpgsign = true 1 2 3 4 5 6 7 8 9 10 // ~/.gitconfig-github-work [user] signingkey = ~/.ssh/id_ed25519_work [commit] gpgsign = true [gpg] format = ssh [tag] gpgsign = true 1 2 3 4 5 6 7 8 9 10 // ~/.gitconfig ... [includeIf "hasconfig:remote.*.url:git@github-personal:*/**"] path = ~/.gitconfig-github-personal [includeIf "hasconfig:remote.*.url:git@github-work:*/**"] path = ~/.gitconfig-github-work ... ssh-agent ssh 和 ssh-agent 都是 openssh 的一部分。对于 ssh 而言,公私钥中,私钥是可以被加密的,加密后,私钥就无法被直接使用,需要使用 passphrase 进行解密后才能使用。 这个时候操作流程就比较复杂,每次使用 ssh 的时候,ssh 命令通过操作系统弹窗,让用户输入 passphrase,然后对私钥原始内容进行解密获取私钥再使用。 ssh-agent 就是一个内存服务,ssh 拿到解密后的私钥后,直接放到 ssh-agent 中,后续直接从 ssh-agent 中进行读取。 这个时候又会遇到一个问题,就是 macos 系统重启了,ssh-agent 内存数据消失了。所以 ssh 会把 passphrase 存储到 keychain 中,下次启动时,会自动从 keychain 中读取 passphrase。 具体流程是: macos 重启,ssh-agent 数据清空 用户操作了 ssh 命令 ssh 客户端,通过 secret api 读取 keychain 中的 passphrase 对密钥原始数据进行解密后,把密钥放到 ssh-agent 中 后续直接从 ssh-agent 中读取 配置 ssh-agent 示例 (UseKeychain & AddKeysToAgent): 1 2 3 4 5 6 7 8 9 > cat ~/.ssh/config Host example.com HostName example.com User git PreferredAuthentications publickey IdentityFile ~/.ssh/id_ed25519_personal UseKeychain yes AddKeysToAgent yes git 如何存储文件的修改 via https://swiftrocks.com/what-happens-when-you-move-a-file-in-git 裸仓库 【裸仓库】是一个不包含工作区(working tree)的 Git 仓库,只包含 Git 版本库数据(.git 目录中的内容)。在 github 等平台上,我们通过 push、pull 操作的远程仓库,都是【裸仓库】,因为它们只需要存储版本数据,不需要工作区。 裸仓库 一般使用 xx.git 命名文件夹,内部没有工程目录,完全是 git 操作目录,如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 tree . ├── HEAD ├── config ├── description ├── hooks │ ├── applypatch-msg.sample │ ├── commit-msg.sample │ ├── fsmonitor-watchman.sample │ ├── post-update.sample │ ├── pre-applypatch.sample │ ├── pre-commit.sample │ ├── pre-merge-commit.sample │ ├── pre-push.sample │ ├── pre-rebase.sample │ ├── pre-receive.sample │ ├── prepare-commit-msg.sample │ ├── push-to-checkout.sample │ └── update.sample ├── info │ └── exclude ├── objects │ ├── info │ └── pack └── refs ├── heads └── tags 创建 裸仓库:git init --bare example.git clone:git clone ./path/to/example.git example worktree 1 2 3 4 5 6 7 8 9 10 11 12 13 NAME git-worktree - 管理多个工作目录 SYNOPSIS git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]] [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>] git worktree list [-v | --porcelain [-z]] git worktree lock [--reason <string>] <worktree> git worktree move <worktree> <new-path> git worktree prune [-n] [-v] [--expire <expire>] git worktree remove [-f] <worktree> git worktree repair [<path>...] git worktree unlock <worktree> git worktree - 管理多个工作目录 核心功能:允许你从一个 Git 仓库创建多个独立的工作目录,方便同时处理不同分支或任务。 常用命令: git worktree add <path> [<branch>] - 创建新的工作目录 作用:在 <path> 创建新的工作目录,并检出 [<branch>] 分支 (默认当前分支)。 常用选项: f: 强制创建,即使目录已存在 (小心数据丢失)。 -detach: 创建分离 HEAD 的工作目录 (不关联分支)。 b <new-branch>: 创建并检出新分支。 示例: git worktree add -b feature-branch ../feature-branch: 创建新分支 feature-branch 并检出到新工作目录。 git worktree add ../working-dir feature-branch: 检出已存在的 feature-branch 分支到新工作目录。 git worktree add ../hotfix origin/main: 创建 hotfix 工作目录 (基于 origin/main)。 git worktree list - 列出工作目录 作用:显示所有已创建的工作目录及其路径和分支信息。 常用选项: v: 显示更详细的信息。 git worktree remove <path> - 删除工作目录 作用:删除 <path> 指定的工作目录。 常用选项: f: 强制删除,即使工作目录不干净 (小心数据丢失)。 注意:仅删除工作目录,不影响仓库本身。 git worktree lock <worktree> - 锁定工作目录 作用:锁定 <worktree>,防止被 git worktree remove 删除。 常用选项: -reason <string>: 添加锁定原因。 git worktree unlock <worktree> - 解锁工作目录 作用:解锁 <worktree>,使其可以被 git worktree remove 删除。 git worktree move <worktree> <new-path> - 移动工作目录 作用:将 <worktree> 移动到 <new-path>。 git worktree prune - 清理无效信息 作用:清理已手动删除的工作目录在 Git 仓库中残留的记录。 常用选项: n: 模拟运行,不实际删除。 git worktree repair - 修复工作目录 作用:修复工作目录的元数据 (通常自动维护,极少手动使用)。 文件夹大小写 Git 对文件夹和文件的大小写,是不敏感的。有些 IDE 会默认做很多事情,让用户无感知。当不通过 IDE 操作 Git 的时候,开发过程中有可能遇到【修改文件夹 / 文件名】无效的场景。 orphan branch 孤儿分支,是指没有父分支的分支。场景: 希望从当前 git 工程独立一份完全没有 git 记录的 new-branch,又需要保留当前 git old-branch 所有的 git 状态 还希望 new-branch 在当前 git 仓库中被管理。 1 2 3 git switch --orphan new-branch git checkout old-branch -- . new-branch 将成为独立 commit 节点,没有任何父节点,并且 git 状态和 old-branch 一致。 后面可以新增 remote url,将 new-branch 推送到远程仓库。

2025/5/13
articleCard.readMore

passkey

在很久以前的 数字签名 文章中,有介绍过 passkey。当时主要是为了说明【公私钥】。目前很多服务商都支持 passkey 作为【登录】和【多重认证】的手段,于是再重点介绍一下 passkey。 FIDO FIDO 是 passkey 技术流程的执行标准,明确了使用公私钥认证的方式来实现用户安全登陆。其中,私钥的存储、获取也进行了安全规定的制定。 FIDO(快速身份在线)是指由 FIDO 联盟开发的一套开放标准,用于增强在线身份验证的安全性和便捷性。FIDO 标准主要通过公钥密码学替代传统的密码,以减少欺诈风险并提高用户体验。FIDO 2 是该联盟的最新标准,支持 WebAuthn 和 CTAP,允许用户通过生物识别、安全设备或 PIN 进行无密码认证。 安全终端有哪些 FIDO 约定了私钥的存储和获取。其中,存储必须是对用户不可见、不可导出,即对人类而言,是完全隐秘的黑盒。常见的 passkey 私钥源头是: 手机,如 iPhone、Android 电脑,如 Macos、Windows passkey security key device,专用的移动安全设备,具有 USB 等插口,可以插入电脑中。 哪些不是 passkey 对于用户的安全登陆,web 站点会提供很多【多重认证】的方案,如 SIM 短信验证码 email 验证码 App 短期有效的验证码 通过 App 对 web 二维码扫码验证 通过外部安全软件如 mac/iPhone 的 password app 生成 30s 有效的 Verification Code。 以上这些,有的场景可以直接用于用户注册、登陆,有些场景可以作为用户的【二次安全验证】,但它们都不属于 passkey。 passkey 的首要条件有: 基于人类的安全识别,如指纹、面容、眼球 基于公私钥,不能无密钥,也不能是对称密钥。 如 password app 生成的 Verification Code,是在设置的时候,server 生成对称密钥并把密钥办法给 app。后期 server 和 app 基于同一时间按照同一个密钥生成 Verification Code,并根据该 Code 是否相等来做验证。技术手段属于【对称加密】。 示例(参数包含:对称密钥、算法、长度、过期时间):otpauth://totp/PeerAuth:M?issuer=PeerAuth&secret=6T7PVVPTZRPWLXXTLZPACRM52QIUAQPE&algorithm=SHA1&digits=6&period=30 passkey 可以做什么 诞生的使命就是做用户认证。即可以用于【二次认证】,也可以用于【一次认证】。即: 可以通过 passkey 直接登陆 server 服务。用户不需要拥有账号密码,只要有 passkey 就可以了。 用户已经输入账号密码后,通过 passkey 进行二次安全确认。 这一步,可以减少互联网诞生以来,各种类型的验证码对用户的影响,甚至上面提到的那些常见的【多重认证】对用户登陆的阻断性干扰。 技术流程 当前设备作为 passkey 终端,如 Mac 用户注册 passkey 或者 绑定 passkey(用户开始阶段有无 web 站点的账号,皆可): web 申请 passkey 并提供【本机认证】入口,如 “创建通行密钥” 入口按钮。点击后进入 Macos 系统认证页面。 用户确认后,操作系统将在系统安全元件中存储【私钥】,并把【公钥】给到 web。 web 将公钥上传到 server 后续,用户登陆或者二次验证的时候: web 提供【通信密钥登陆】入口。用户点击后,web 向 server 申请【挑战 (一个复杂的计算)】。 server 使用公钥对【挑战】进行加密,并返回 web。 web 唤醒并进入 Macos 认证页面(【挑战】也一并给予),用户进行指纹验证通过后,操作系统用指定的私钥对【挑战】进行解密,返回 web 最终是否解密成功。 web 根据操作系统返回的结果,来判断私钥是否合法。如合法,则顺利进入 web 站点(登陆成功,或者二次验证成功)。 移动设备作为 passkey 终端,如 iPhone 整体流程和 上面 一致,只是多了一个终端:iPhone。在上面创建 passkey 的时候,还有一个【换部设备】的入口,点击后调用系统 api 并展示系统 passkey FIDO 弹窗: // 二维码解密后内容 FIDO:/246202778134775422047073255772941326219821733578350172887457888589614449494332690387759208522109390157096037518904470510056446099681926406642559495045890109321447142660 iPhone 扫码后,会打开 passkey 移动端系统弹窗。iPhone 在设备上生成公私钥,私钥在安全元件中存储,并把公钥通过【一些通信方式】给到 web 站点。 初期,使用 websocket,即 chrome 等浏览器打开 websocket,iPhone 通过 websocket 把公钥给到 chrome。chrome 进而给到 web 站点。 现在,已经不用 websocket 来。iPhone 会通过私有协议,把公钥给到 macos,macos 通过 系统 api 返回给 chrome/web 站点。 除了以上多了一个二维码,供 iPhone 用来识别之外,其他就没有流程上的变化来。 核心差异点就是:【私钥到底存在哪】。当然,存在哪里,哪里就需要作为后续提供私钥的源头。即: 用户若使用 iPhone 绑定 passkey。 后续,用户也需要使用 iPhone 主动扫码,来进行公私钥验证。 当然,这一块也有很大的优化空间,比如 Apple 可以实现操作系统级别的【私钥同步】,把 iPhone 中的私钥同步到 Macos 中,这样 mac 也可以直接进行公私钥验证,就不需要 iPhone 扫码了。【目前是否有这个能力,还不清楚。】 用户负担 如果完全使用 passkey 作为服务商的用户注册(拒绝 账号、密码),可能遇到的问题如下: 在自己的 apple 全家桶中,passkey 可以无缝同步。但如果哪一天迁移到了 android、window 后,因为没有账号密码、又没有私钥,就无法登陆服务了。 在自己的设备体系中使用良好。如果哪一天需要在同事的电脑上登陆,就没有 passkey 私钥了。 最近一些年,可能更常见的方案,还是在已有的账号密码的用户体系下,新增【绑定 passkey】能力。 若以后有更丝滑的【私钥迁移】能力,那么伴随互联网发展长河的账号密码体系,可能会成为历史。

2024/12/16
articleCard.readMore

How to pay

All pays https://stripe.com/zh-sg/payments/payment-methods Stripe 支持了很多支付方式,可以窥见一些。Stripe 主要对接线上支付,对于很多线下支付的渠道如【nanaco】等电子货币,就看不到了。 japan 货币 电子货币 - 支付链路 Suica、PASMO、nanaco、WAON、Edy(Rakuten Edy)、QUICPay、iD 如上文【货币】-【电子货币】中所描述,E-money(电子货币)是法定货币的 1:n 等值替换,即电子货币 = 法定货币。 电子货币无法凭空产生,需要基于预付费模式工作的,这意味着用户需要先充值然后才能消费。 用户将一定金额的资金存入电子钱包或智能卡中,然后在各种接受 E-money 支付的地点使用这些资金进行消费。 消费方式:实体卡刷卡、Apple/Google Pay、App 出示识别码等等。 钱包 - 支付链路 市场上有各种 X Pay,均为钱包。除了 Apple/Google Pay 这种专门为 信用卡、预付卡、借记卡、电子货币 提供聚合服务的产品外,其他钱包均有实际公司主体。 比如常见的 Paypay、wechat、alipay 等等,它们提供各式各样的终端消费能力。 但用户在钱包中的钱并不能凭空产生。 一种方式是依靠【信用卡】,信用卡 的链路最为复杂,下一章会单独讲诉。 一种方式是依靠【预充值】,用户将法定货币预先充值到平台侧。形式有:预付卡、借记卡、平台账户余额。 通过 预充值 的形式,钱包相当于中间平台,为用户的【纸币、硬币】这些法定货币,提供了中间放置平台,以进一步通过该平台向外部商家进行支付。 当用户把钱充值到钱包后,钱包就需要一系列的管理功能。一来确保用户账户数额不能有差错,二来确保用户可以随意把钱花出去。 所以平台需要做一个大管家,提供各种安全保障能力。 甚至与,哪个商户若需要对平台用户进行收款,哪个商户就也要在该平台进行商家注册并绑定收款银行账号。 这样,商户的账户也被平台管控,平台就很容易进行交易清算。 信用卡 - 预付卡 - 借记卡 概要 信用卡关键的能力有两点: 提供个人信用背书,个人没有钱,也能够通过信用卡消费。 有稳定的支付通道,为世界各地的信用卡提供清算能力。 信用卡普及且应用广泛后,整个体系就非常成熟,商家已经对信用卡支付形成依赖。此时,一些不符合 1 条件的用户就没有信用卡,但自身也有钱 / 能力进行支付,就衍生出了预付卡、借记卡。 预付卡、借记卡 借用 2 支付通道,不提供信用背书,直接用现金支付,从而完成交易。 预付卡、借记卡 也包括电子货币。只是在信用卡体系里,预付卡、借记卡 一定依赖 visa、mastercard、… 等支付网络,一定属于 visa、mastercard、… 卡。 若商户对 个人信用 十分在意,那么只能使用 信用卡 完成交易,预付卡、借记卡 无法支付。 品牌贴牌 目前大范围使用的信用卡支付网络,只有 Visa、MasterCard、American Express (AmEx)、Discover、UnionPay、JCB。其中,大部分不发行卡,只提供【支付网络】。支付网络将是下一个章节重点说明的内容。 American Express 和 Discover 提供发卡服务。即提供支付网络,也下场作为发卡行。<这两位,拥有银行执照> 大众最常见的信用卡,是从银行处申请获取。发卡行一定是具有【银行执照】的金融机构,银行天然具有该优势。基本上,所有的信用卡,都是银行发行的。 但还有一种常见的信用卡,是从【钱包】公司处获取,如 Paypay、MerPay 等。这些公司并不具备银行执照,即没有发卡能力。 这是另一种常见的【品牌贴牌】信用卡,即 公司 对接发卡行。公司对用户进行信用评估等审核,并提供品牌福利,而发卡行承担用户的消费风险及盈利。 信用卡支付网络 【支付网络】是信用卡交易的核心,也是连接全球所有信用卡的枢纽。 所有的发卡行、收单行、有能力对接支付网络 的机构、单位、环节,其【资质、技术、安全】等都需要满足【支付网络】提供商的要求。 【支付网络】决定了整条交易链路的【规则、安全保障、清算、结算】等。 所有的信用卡,不分地区,通过【支付网络】都可以进行联通、交易。发卡行承担不同币种的汇率、消费额度等工作。 发卡行会在交易前进行验卡,防止卡滥用。如有些卡不允许国际支付,会拒付。 著名的国际支付服务提供商 上文中,很多商业产品都会通过【支付服务提供商】来对接【支付网络】最终完成交易。以下是一些有名的提供商列表: via https://developer.apple.com/tap-to-pay/regions/ Apple Pay 如何支持 EMV & FaliCa EMV 是 Europay MasterCard Visa 的缩写,是信用卡的协议标准,不同支付网络的信用卡均遵守该协议。 所以,信用卡在世界各地支持 EMV 读卡器的机子上均能【插卡支付】并消费。 但是在 Touch (contactless) 方面,日本提前广泛使用了 FaliCa 标准,没能很好的支持 EMV,导致前期不管国内还是国外的信用卡,都不支持【非触控支付】。 在技术专区里,会重点说明 FaliCa 的这段历史,以及 Quicpay/iD 如何解决这个问题。 信用卡的不同支付链路 信用卡最终需要【发卡行】通过【支付网络】转账到【收单行】。 发卡行 依靠哪些信息来判定一张信用卡的合法性,是十分关键的。 在不同的 信用卡 使用场景中,发卡行 从【支付网络】侧获取到的卡信息是不一样的。 特别支付场景介绍 Tap to pay 基于 NFC 技术,可以【技术专区】中详细了解实现过程。 通过移动设备终端 (iPhone/Android) 识别 visa 等实体卡或者 apple pay 等物理硬件,获取到卡片的【代理卡号 / 虚拟码】,并完成支付的方式。 通过 NFC 协议,完成数据读取 移动设备会有一个 app,用来对接 nfc api,完成数据读取、服务器交互等工作 Android 对于 NFC 较开放,可以方便实施。 iPhone 使用了 Core NFC 中【卡读取】的能力。对个别国家和供应商有开放。 example via https://www.apple.com/business/tap-to-pay-on-iphone/ apple 供应地区说明 via https://developer.apple.com/tap-to-pay/ Stripe 支持地区说明 via https://docs.stripe.com/terminal/payments/setup-reader/tap-to-pay?platform=ios Alipay 碰一下 基于 NFC 技术,可以【技术专区】中详细了解实现过程。 支付宝给商家提供 NFC 卡模拟芯片。用户侧的支付宝 app 充当读卡器,读取商家的 NFC 芯片信息。app 获取商家信息后,进行网络处理,完成支付。 以往的支付,读卡器处于商家侧,如 Tap to pay、POS 机等。只要商家具有稳定的网络,就可以完成支付。 alipay 碰一碰方案,读卡器处于用户侧 app 中。这就需要用户侧具有稳定的网络,以完成支付。 技术专区 Visa - 支付网关和清算平台 - 跨国际交易 Visa 不发行卡,不对接个人用户。它只对接银行、大企业主。 Visa 负责两个银行之间的资金流动,它会使用一天的固定汇率 (高于实时汇率) 加上自身的手续费进行计价。 因为 Visa 自身做的比较大、有信任、打广告,所以只要牵涉到跨国之间的终端用户级别的资金流动,都会有 Visa 的影子。 如果仅仅是本地企业、同一货币,那么不需要使用 Visa,就可以省去手续费。如银联。 所有的 Visa 信用卡交易,都需要过 Visa 的网关进行清算。最终也不是实时扣款,visa 会在结算时间到来后,统一将所有银行的账单进行清算。 扩展: Visa 和 SWIFT 是两条赛道。SWIFT 主要处理两个国家之间银行界资金的流动。是直接流动,金额更大、级别更高。SWFIT 处理的金钱要比 Visa 多的多。 SWIFT 一日处理 5W 亿美元的交易,Visa 一年处理 10W 亿美元的交易。交易量不在一个量级。 Visa 等一众【支付网络】的具体运转,详见上文中的【信用卡支付网络】章节。 非银行如何对接【支付网络】 Stripe 对 Visa 的一些解释:https://stripe.com/zh-sg/resources/more/what-is-visa#shui-zai-shi-yong-visa Stripe 在 2015 年开始直接对接 Visa,不在走【收单行】对接:https://www.businesswire.com/news/home/20150729005841/zh-CN/ Apple Pay 原理概要 Apple 提供的【apple pay】方案,是将用户的信用卡信息,使用 “令牌化” 方案,通过【代理卡号】的方式存储于 iPhone 设备,后续将 DAN 提供给读取者,读取者使用 DAN 完成后续的支付链路。 代理卡号(Device Account Number, DAN) 不存储卡号、有效期、cvs,是银行侧生成的一个 code。银行会根据这个 code 在结算的时候进行卡账户校验。 每次进行 apple wallet 录入的时候,都会生成一个新的 DAN。 使用 DAN 的业务方: Apple Wallet:iPhone 作为 NFC 卡模拟,供系统级别的 Wallet app 使用。 wallet app 将银行卡信息存储到 iPhone 中(录入 DAN) 有外部 NFC 读卡器的时候,wallet 被激活,完成用户身份认证,并读取 DAN 并提供给外部 NFC 读卡器。 外部读卡器获取到 DAN 后,对接 Stripe 或者卡服务商。最终在发卡行完成 DAN 的校验,完成扣款和资金转移。 Native app:通过 passkit sdk,swift/oc 对接 sdk 在 app 内部完成【支付信息】的读取 native app 绑定 【Merchant ID】出口,未绑定的 id 不允许支付。 native app 弹出 apple pay 弹窗,获取 pay token(公钥加密) 将 token 给到 self server 或者 stripe 等平台,这些服务平台需要对 token 进行私钥解密。 私钥的来源有一套比较复杂的流程 server 将解密后的信息给到【发卡行】,发卡行完成校验及扣款事宜。 Web app: 整体和 native app 一致 因为不在 app 内部,缺少必要的 Merchant ID 信息。而是在 safari 中唤醒 apple pay 支付,所有多了一个【Identity】认证 通过公私钥证书来完成 该认证,主要用于对服务器进行确认,进而获取商户信息。后续流程和 native app 一致。 技术流程 native - self server Apple Developer 申请 Merchant ID(id) dev 申请 CRS 文件(本机绑定私钥 private key),在 developer 后台绑定 id 生成 cer 证书。 dev 将 cer 证书安装到本机后,导出 p12 文件(包含公私钥 public + private key) 将 p12 给到 self server native app 在 xcode 中绑定 id 该行为使得 app 可以确定允许的商户,可绑定多个 id web app 没有这一步,所以需要 【identity】认证 native app 完成 apple pay 弹窗并获取 payment token token 给到 server,server 通过 p12 拿到 private key,对 token 进行解密后,提交发卡行进行验证扣款。 native - stripe Apple Developer 申请 Merchant ID(id) 在 stripe 后台申请 CRS 文件后,在 developer 绑定 id 生成 cer 证书,再将 cer 证书上传到 stripe 后台。 stripe 这里就拥有了 p12 (private key + public key) native app 调用 stripe api,完成 apple pay 弹窗 等所有操作,直接获取支付结果 stripe sdk 将 payment token 给到 stripe server 完成 token 解密 stripe server 进行发卡行提交 stripe 处理支付结果并返回给业务 web - self server 和 native 基本流程一致,在 id 绑定证书环节,除了 【Apple Pay Payment Processing Certificate】cer 证书外,还需要以下: Apple Pay Merchant Identity Certificate 商户需要通过该证书向 apple 获取 Merchant ID 的详细信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // example const https = require('https'); const fs = require('fs'); function getApplePaySession(validationURL, callback) { const options = { url: validationURL, method: 'POST', cert: fs.readFileSync('path/to/merchant_identity_certificate.pem'), key: fs.readFileSync('path/to/merchant_identity_private_key.pem'), headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ merchantIdentifier: 'merchant.com.yourdomain.yourmerchantname', domainName: 'yourdomain.com', displayName: '您的商户名称' }) }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { callback(null, JSON.parse(data)); }); }); req.on('error', (e) => { callback(e); }); req.end(); } Merchant Domains 需要将必要的 apple 文件放置于 domain 服务器上,供 apple 对 domain 进行验证。未验证通过的 domain 无法进行 apple pay 支付。 NFC NFC(近场通信)的技术原理基于无线电频率识别(RFID)技术,使用磁场感应来实现在设备间的通信。NFC 设备在 13.56 MHz 频率上操作,通常用于非接触式数据传输,距离范围非常短,通常在几厘米内,传输速率慢,在 400kbps (50kb/s) 左右。 技术原理 识卡器发出信号(电磁感应),激活了终端(手机自动点亮),然后进行数据交互,并可能需要机主进行身份验证,最后完成信息的交互(支付、上公交车、开门等)。 NFC 有三种工作模式:点对点通信模式、读卡器模式、卡模拟模式。又分为【主动模式】和【被动模式】,其中一个设备提供射频场,另一个设备利用这个射频场进行通信。 使用 NFC,需要两个终端,一个做控制器,用于发射磁场来识别信息。一个做无电源的数据芯片,通过接收到的磁场来感应并传输信息。 对于 NFC 设备来说: 点对点通信:两个 NFC 设备相互交换信息。 读卡器模式:一个 NFC 作为控制器,读取其他 NFC 芯片中的信息。 卡模拟模式:一个 NFC 作为数据芯片,其他读卡器可以读取其中的信息。 现代电子产品中,Android 和 iPhone 都支持 NFC 技术,手机作为 NFC 设备,使用读卡器模式和卡模拟模式,已经可以完成很多事情。 当作为 NFC 控制器的时候,手机可以主动的读取外部 NFC 芯片中的信息,也可以将必要的信息写入到外部 NFC 芯片。(物流中,可以通过手机对商品挂载的 NFC 芯片进行记录) 当作为 NFC 芯片的时候,手机可以模拟一个 NFC 芯片,通过软件将信息提前写入手机中,其他读卡器就可以直接读取手机中的信息。(可以实现门禁卡等) Android 对 NFC 的 API 开放较多,app 可通过 api 来控制 NFC 进行 读取、写入、模拟 的操作,来实现快捷的智能家居、门禁卡等场景。 iPhone 上则比较保守,在【卡模拟】、【卡读取】方面,都有不少限制。 门禁卡 普通门禁卡: 门禁卡中有 【微芯片】(存储卡片的识别信息和其他数据)和 【天线】(用于接收和发送无线信号)。 门禁卡靠近门禁系统的读卡器 → 读卡器会发出一个射频信号 → 信号通过天线供电给门禁卡上的微芯片 → 微芯片被激活,通过天线将存储在芯片上的识别信息发送回读卡器 → 读卡器接收到信息后,将数据传输给后端的门禁控制系统。 NFC 门禁卡: 原理基本和普通门禁卡一样,不过,NFC 提供了更高的安全性。它支持双向通信,卡和识卡器之间可以通信。它们之间会进行密钥交换,通过对称、非对称加密来完成数据的安全传输。相比普通门禁卡,NFC 门禁卡会更加的安全。 NFC 是一种普适性的技术方案,手机也可以作为 NFC 终端。这里就可以把 NFC 门禁卡的信息保存在 手机中,使得手机可以充当 NFC 门禁卡的功能。 蓝牙门锁: 有些 app 会通过 蓝牙的方式,和门锁连接。这在智能门锁中非常常见。因为距离很远,就可以连接上。而 NFC 需要非常短的距离 (4cm) 才能通信。 移动支付 通过 NFC 进行移动支付,主要有三种方案: 卡模拟方案。mobile 录入支付卡信息,被外部读取器识别 系统级别的支持,如 apple pay。开发人员没有掌控能力。用户只能通过 apple wallet 录入银行卡信息,然后通过 apple pay 进行支付。 应用级别的支持。Android 支持的较好,iPhone 限制很多。 iPhone 在 iOS 18.1 放开了该限制,app 可以将支付卡信息写入 app 中,支付的时候调用 app 完成支付。 不对普通开发者开放,需要和 apple 签订商务协议,支付费用,一般都是支付中间商如 Stripe。并且只对个别国家开放。 读卡器方案。mobile 作为读取器识别外部实体卡(信用卡等)。Android 支持的叫好,iPhone 限制很多。 Apple Tap to pay。商家可以在自己的手机中,打开 m app,然后用户把信用卡、iWatch 靠近手机,即可完成支付。 普通开发人员没有太多的控制能力。也需要签订商务协议,一般都是支付中间商如 Stripe。它们提供 SDK 并和 Apple Api 交互完成支付。 Apple 和 Stripe 中间商会对地区等有限制。只在少有的地区开放了 Tap To Pay 能力。 alipay 碰一碰。非常聪明的通过 NFC 实现支付的方案。 支付宝给商家提供 NFC 卡模拟芯片。用户侧的支付宝 app 充当读卡器,读取商家的 NFC 芯片信息。app 获取商家信息后,进行网络处理,完成支付。 以往的支付,读卡器处于商家侧,如 Tap to pay、POS 机等。只要商家具有稳定的网络,就可以完成支付。 alipay 碰一碰方案,读卡器处于用户侧 app 中。这就需要用户侧具有稳定的网络,以完成支付。 Apple iPhone NFC 简单介绍一下 iPhone 对 NFC 支持的历史: WWDC 2017:引入 Core NFC,并具备 NDEF 标签【读取】 功能。 WWDC 2018:在较新设备上对 NDEF 消息进行后台标签读取。 WWDC 2019:重大扩展,允许 NDEF【写入】,支持 ISO 7816、ISO 15693 和 MIFARE 标签,并支持自定义命令。 WWDC 2020:多标签检测,VAS 协议支持和 ISO 15693 标签的后台读取。 WWDC 2021-2023:专注于稳定性、性能提升和小幅增强,没有重大 API 更改。 WWDC 2024:支持【卡模拟】。 EMV vs FeliCa EMV:基于 NFC Type A/B 设计的通信标准。NFC 的【读卡器】和【卡芯片】均使用 EMV 协议。 FeliCa:Sony 开发,基于 NFC Type F 设计的通信标准,速度快,成本高,仅在日本大范围使用。NFC 的【读卡器】和【卡芯片】均使用 FeliCa 协议。 这两者,是使用 NFC 实现的两套不兼容的通信协议标准。详细技术解释可参考【Quicpay】章节。 Quicpay - iD 为什么称为【电子货币】 Quicpay 官网: https://www.quicpay.jp 日本很早期,地铁等交通就很繁华,对于人流量大的场景,过关卡排队就需要很长时间。 One day,Sony 基于 NFC Type F 研发了 FeliCa 协议的高频无线通信技术。 FeliCa 主要用于 Touch/Contactless【非触控】场景,Pasmo、Suica 都是基于 FeliCa 实现的刷卡,特点是:速度快。 很快,FeliCa 成为日本在【非触控】刷卡领域的事实标准,所有能刷卡的地方,都是基于 FeliCa 实现。 前面介绍 NFC 的时候,说到 NFC 需要两个终端(读卡器、卡模拟)才能工作,日本所有的读卡器也都是基于 FeliCa 实现的。 而卡模拟一侧,包括 Visa 信用卡、Pasmo 公交卡、电子货币实体卡 等,都是支持 FeliCa 的。 前面介绍 Apple Wallet 对接 Quicpay 的时候,流程图中有提到 Quicpay DAN。在 Apple Pay 还没有进入日本之前,Quicpay、iD 就已经发行实体卡进行消费,同时 Visa 等信用卡也都是支持 Quicpay/iD 支付。 交易的链路和现在相比没有改变,依旧是读卡器通过 FeliCa 读取卡信息后提交到 Quicpay 后台,后台通过 收单行 对接 Visa 支付网络,完成交易。 只是这个时候没有 DAN,DAN 安全码是 Apple Wallet 特有的产物。 但是 FeliCa 的硬件成本比普通的 NFC Type A/B 协议高,国际上普遍使用的都是 EMV 协议。EMV 是基于 NFC Type A/B 实现的通信协议。 在刷卡过程中,数据是需要加密的,而这套加密规则,也是和 FeliCa/EMV 的设计绑定的。 此时,日本基于 FeliCa 的读卡器,在【非触控】刷卡的时候,就无法读取基于 EMV 协议设计的国际信用卡。 但是对于插卡消费是没有影响的。 Visa、masterCard 等官方平台,制定的通信规则就是【信用卡基于 EMV 协议】。所以日本的信用卡本身绝对是符合 EMV 协议的。 所以日本的信用卡在插卡消费的时候,也同样使用 EMV 协议。因为 FeliCa 协议只在【非触控】场景下使用。 So,这个时期,外国游客在日消费,可以使用信用卡插卡消费,但无法使用 Touch/Contactless【非触控】刷卡消费。 对于在日本申请的信用卡实体卡,本身就是支持 EMV 和 FeliCa 两种通信协议。在日本的时候,读卡器可以插卡识别 EMV,也可以【非触控】识别 FeliCa。 当日本人出国在境外刷卡,外国的读卡器都是支持 EMV 插卡 &【非触控】识别,所以日本信用卡在国外使用完全不受影响。 对于国外信用卡在日本不能很好使用【非触控】的体验问题,并没有好的解决办法。FeliCa 历史已久,在速度方面很优秀,Pasmo 等众多卡都使用这个协议,根子是不能替换的。 所以日本开始升级读卡器,截至目前,很多读卡器也都同时支持 EMV 和 FeliCa 两种协议的 NFC 识别了。 同时,随着 Apple/Google Pay 的发展,现在 Apple Wallet 也支持 Quicpay/iD 支付了,在日本称为【电子货币】。 在 Apple Wallet 中见到 Quicpay/iD 的场景,都是信用卡场景。即 wallet 中,一张卡的右下角,同时具有 Quicpay + Visa 或者 iD + masterCard Logo。 有些钱包公司发行的卡,仅仅支持 Quicpay 或者 iD,就没有 Visa 或者 masterCard Logo 了,即这个卡就不能在国外使用啦。 但从 Apple Wallet 中 Visa、masterCard 支持 Quicpay/iD,被称为【电子货币】或许有些奇怪。 若从 Quicpay/iD 自身的功能场景出发,它们本身就是提供预充值的实体卡而后刷卡消费,的确属于【电子货币】。 只是 信用卡 虽然走了 Quicpay/iD 的支付通道,又的确和【电子货币】没关联,其实依旧属于【信用卡支付】。 so,Quicpay/iD 被称为【电子支付】,完全是根据其自身的原始功能,下的定义。 最终在说明一点,在日本银行发行的信用卡,基本上都是和 Quicpay 或者 iD 合作的。有些钱包公司借壳发卡行发行的卡,有可能没有 Quicpay/iD。 这里有一份列表,via https://atadistance.net/apple-pay-japan-credit-cards/

2024/12/9
articleCard.readMore

apple frameworks

动态库 & 静态库,若单独输出并接入主工程,逻辑倒很清晰。如果牵涉到多个 framework 不同类型并且相互依赖,会增加不少复杂度。 以下说明中,动态库使用 DFramework 表示,静态库使用 SFramework 表示。 1 framework 仅需要提供 1 个 framework,直接提供 DFramework 即可。DFramework 优先,必要的时候再提供 SFramework n framework 因为代码结构分组,可能需要提供多个 framework: DFramework 优先,能不提供 SFramework 就不要提供 有 n 个 DFramework 就提供 n 个。相互之间做好依赖文档,App 需要根据依赖文档 flat 平铺接入所有需要的 DFramework。 如果 b DFramework 仅仅被 a DFramework 依赖,可以将 b 打入 a DFramework 的 frameworks 文件夹中,通过 embed 实现。 这样就可以不再文档中说明 b DFramework 的存在,对外界透明。 这种方案,最终的 app 中 framework 不是 flat 平铺的,而是具有层级结构,如 :App - a DFramework - b DFramework 如果 b DFramework 同时被 m DFramework 和 n DFramework 依赖,不能将 b 隐藏(打入 m 和 n 中)。 否则,App 集成 m 和 n 的时候,b 会出现多份。如果 b 的版本不一样,将按照打包顺序只使用其中一个。 如果提供 SFramework,则将所有的 framework 做成依赖,输出一份 SFramework 即可。 如果有按需接入的场景,按按照场景输出 n 个 SFramework。 依赖开源库 一般这种场景是因为开发 framework 的同时,又使用了开源的库。这个时候如何处理开源库是个问题(开源库一般都是 源码 提供的,不讨论 闭源包 提供方式,这个和上面的场景一致)。 由于 apple 的 Swift Package Manager 三方库管理的介入,一个 module 在最终的 app 中到底是 dymanic 还是 static,已经不可控了。SPM 会自行管理,不像 cocoapods 的时候我们可以自行控制。 如果是源码提供者,可以增加 dymanic /static 约束。但很多源码都不会提供这个选项,而是让 SPM 自行管理采用哪一个。 在这种场景下,开发的 framework 如果对开源库有依赖,就必须考虑是否【去开源】事宜了。 另外,除了自己使用开源库,App 主工程开发者,可能也需要使用同一个开源库,这时候还会出现【版本不一致问题】。 以下场景,假设有 T 开源代码需要使用: 不封装,让外界接入 framework 需要依赖 T 如果不是强依赖,在 xcode 中使用 -weak_framework 进行标记,这样业务也可以不接入,相当于默认不使用这个功能。 如果是强依赖,并且在 文档 中说明业务需要接入 x 的具体版本范围。不接入无法编译或者 app 启动闪退。 封装,外界无感 第一步就是需要对 T 进行【模块隔离】,否则外界也接入 T 的时候,App 主工程和我们提供的 framework 同时有 T 代码,会有问题。 虽然 xcode 在编译连接的时候,会处理好这个问题,使得同一份代码尽量在 app 中仅保留一份。 但如果开发人员使用不恰当或者配置错误,很可能导致出现两份 T 源码在项目中。 这个时候如果版本还不兼容,就会有非常大的调试和异常隐患。 模块隔离方案:修改 Product Name,将 T 变成 TT 模块 然后,还需要考虑将 TT 变成静态库还是动态库: 静态库: 这是完全去开源的方案。变成静态库后,TT 源码将打入 framework 二进制中一起提供。因为模块隔离,外部也无法调用和感知。 通过对 framework 二进制字符表进行分析,还是能够看到 T 的踪迹。 如果不做模块隔离,也能成功。主 app 也使用 T 模块的时候,链接的时候将自动保留 1 份静态代码。有版本差异的话,出现异常问题将非常难以排查。 动态库: 使用动态库的话,如前面【需要提供多个 framework】所描述的,有两种方案: TT 可以提供给业务,让业务自行接入。是 flat 平铺 结构。 也可以内嵌到 m framework 的 frameworks 文件夹中专门供 m framework 使用。是 内嵌 结构。 如果不做模块隔离,也可以内嵌。但主 app 可能也使用了 T 模块,使得同一个工程包含 2 个版本的 T 模块,运行的时候按照 xcode 编译顺序,只有一份被使用。有版本差异的话,出现异常问题将非常难以排查。

2024/11/14
articleCard.readMore

Mac - Command Line Tools

Mac 系统上,默认没有提供 git、xcodebuild 这些开发者命令。所以当一些终端命令 (brew 等) 被触发后,macos 系统会弹窗提醒用户进行【开发者工具】的安装,即【Command Lines Tools】(下面简称 tools)。 如果安装了不同版本的 xcode,则每个版本都会携带各自 xcode 版本的【tools】。 上面提到的系统弹窗,是不需要安装 xcode 也可以安装【tools】。但这个 tools 版本会比较低,内部的命令数量也会少于 xcode 携带的。即:最新版本的 xcode,携带的 tools 也是最新的。 这里就可以发现有多个 tools 目录了,如下: 非 xcode 携带:/Library/Developer/CommandLineTools xcode:/Applications/Xcode.app/Contents/Developer xcode beta: /Applications/Xcode-16.2.0-Beta.2.app/Contents/Developer 在终端执行 git 、xcodebuild 的时候,一定会使用某一个 tools 中的命令,可具体使用哪一个呢? xcode-select 通过 xcode-select 可以方便的切换 main tools,默认也只有 main tools 会生效。可以通过 man xcode-select 查看简介。 1 2 3 xcode-select --install // 安装非 xcode 版本的 tools xcode-select --switch <path> // 切换不同的 tools 作为 main tools xcode-select -p // 打印当前 main tools path 当 tools 被指定为 xcode-xx-beta.app/xx/Develop 目录的时候: 在终端中执行 git 命令,最终就会使用:/Applications/Xcode-16.2.0-Beta.2.app/Contents/Developer/usr/bin/git xcode-select 默认只有 1 个 tools 被激活,是操作系统级别的全局有效。 如果在 a tools 激活的同时,希望使用 b tools 该怎么办呢?在多版本 xcode 共存的时候,可能需要使用不同的 tools 命令进行开发。 这个时候在当前的命令行环境中设置 DEVELOPER_DIR 就可以临时切换 tools 目录: 1 2 3 4 5 6 7 8 // 1 > xcrun --find git /Applications/Xcode-16.2.0-Beta.2.app/Contents/Developer/usr/bin/git // 2 export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" // 3 > xcrun --find git /Applications/Xcode.app/Contents/Developer/usr/bin/git xcrun 当在终端中执行 git 命令的时候,使用的是 tools 中的 git,这里需要探究一下。 1 2 3 4 5 6 // 1 > where git /usr/bin/git // 2 > xcrun --find git /Applications/Xcode.app/Contents/Developer/usr/bin/git 通过 where 发现,/usr/bin 目录下是存在 git 二进制可执行文件的。为什么说执行的是 tools 中的 git 可执行文件呢? 这就要说到 xcode-select 维护多个 tools 的意义了。 /usr/bin 是 path 路径,可以直接找到命令。但是像 git 这些命令依托不同的 tools 环境提供,总不能在切换 main tools 的时候,把这些命令 copy 到 /usr/bin 中。所以对于 /usr/bin 中的 git、xcodebuild 等命令,在具体执行的时候都执行的是 main tools 中的可执行文件。 实现这个能力的,就是 【xcrun】。 xcrun 的目标只有一个,就是根据 xcode-select 设定的 mail tools 目录,在其下面寻找命令。可以通过 man xcrun 查看详细: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Options: -h, --help show this help message and exit --version show the xcrun version -v, --verbose show verbose logging output --sdk <sdk name> find the tool for the given SDK name --toolchain <name> find the tool for the given toolchain -l, --log show commands to be executed (with --run) -f, --find only find and print the tool path -r, --run find and execute the tool (the default behavior) -n, --no-cache do not use the lookup cache -k, --kill-cache invalidate all existing cache entries --show-sdk-path show selected SDK install path --show-sdk-version show selected SDK version --show-sdk-build-version show selected SDK build version --show-sdk-platform-path show selected SDK platform path --show-sdk-platform-version show selected SDK platform version 上面示例中的 xcrun --find git 就是用来找 git 二进制可执行文件路径的。 当 git 在 tools 中的路径找到后,原先执行的 /usr/bin/git 命令,也就通过这个新的执行文件来实现了。

2024/11/11
articleCard.readMore

DNS 的 CNAME 是如何工作的

今天配置 cloudflare 中站点的 dns,遇到一些关于 cname、tls、cloudflare 代理相关的问题,做了下梳理。重点有下: cname 负责提供目标 ip,在机制上类似【权威域名服务器】 目标 ip 所在的服务如果非自行掌控(比如强行设置一个目标域名),很可能无法工作。 cloudflare 设置 cname 的时候有一个【代理】功能。这个设计非常糟糕,完全脱离了 cname 的本意。 以下介绍中,使用 cloudflare 配置 test.yigegongjiang.com 的 cname 为 httpbin.org,默认不配置【代理】。 CNAME 是做什么用的 DNS 解析中,通过 dig A test.yigegongjiang.com,dig A httpbin.org 会有如下返回(参数 A 表示返回目标域名的 ipv4): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // dig A test.yigegongjiang.com ;; ANSWER SECTION: -> test.yigegongjiang.com. 300 IN CNAME httpbin.org. httpbin.org. 60 IN A 54.243.34.18 httpbin.org. 60 IN A 34.206.181.91 httpbin.org. 60 IN A 3.222.34.231 httpbin.org. 60 IN A 35.172.59.156 httpbin.org. 60 IN A 54.237.204.19 httpbin.org. 60 IN A 184.73.239.81 // dig A httpbin.org ;; ANSWER SECTION: httpbin.org. 60 IN A 3.222.34.231 httpbin.org. 60 IN A 34.206.181.91 httpbin.org. 60 IN A 54.243.34.18 httpbin.org. 60 IN A 184.73.239.81 httpbin.org. 60 IN A 35.172.59.156 httpbin.org. 60 IN A 54.237.204.19 可以发现,如果需要找到 test.yigegongjiang.com 的 A 记录,一定需要先找到 CNAME 记录,再通过 CNAME 指向的域名继续寻找目标 ip。 所以这里提出 CNAME 的第一个作用,就是【设定 IP】。使用 github pages 搭建博客的时候: 购买了域名 m ,希望将 m 域名映射到 n.github.io Blog 域名上。 后续直接访问 m,ip 被重定向到 n.github.io,从而完成 Blog 的访问。 其次,CNAME 还有一个作用是【负载均衡】。如使用 xx 云平台部署服务: 在 xx 云平台上的多个边缘节点部署了服务,并统一使用域名 m 向用户提供服务。 无需自行搭建【权威域名服务器】,通过 xx 云平台提供的 n.xx.com 做域名 m 的 cname 指向。 用户请求域名 m 的时候,DNS 解析会进入 n.xx.com,xx 云平台负责根据用户的地理位置通过 n.xx.com 提供动态的 ip,实现【边缘访问】和【负载均衡】。 Anyway,不管 CNAME 通过中继域名实现【设定 IP】,还是做【负载均衡】,CNAME 本质上都是为初始域名提供 ip。 不像 A/AAAA 记录,强制设定了一个固定 ip。CNAME 提供了一个【缓冲层】,可以做更多的事情。 这里也会出现一些问题,通过缓冲层获取到的 ip 所在的服务器,可能无法很好的处理我们的 m 域名请求。 CNAME 目标服务器注意事项 假设我们回到了没有 tls/ssl 安全验证的 http 场景,将 test.yigegongjiang.com 的 cname 指向 www.google.com 或者 www.facebook.com,可以访问 Google 和 facebook 吗? 理论上可以,实际并不行。如果把 cname 指向 httpbin.org,却又可以正常访问。 进行 http 请求的时候,主机域名 test.yigegongjiang.com 会通过 【host】字段携带到目标服务器,此时目标服务器完全可以主动拒绝服务,因为不是它们自己的域名。 而 httpbin.org 能正常访问,是它没有限制请求中的 host。 回到 https 场景,有了 tls/ssl 安全证书,又变得不一样了。 同样进行上面的设定,不管 cname 指向哪个域名,浏览器都会弹窗告诉我们不安全。因为 www.google.com 返回的 tls 证书,验证的域名是 *.google.com,而用户访问的域名是 test.yigeogngjiang.com,不匹配!证书验证不通过! 甚至于有些目标域名,连安全弹窗都不会弹起。 服务器如何返回证书,是通过 tls 认证中的【SNI】标记识别具体服务的(一个服务器可以开 n 个服务,有 n 个证书)。 如何服务器都不认识 test.yigegongjiang.com 这个 SNI 标记,那么它可能连 tls 证书都不会返回。 上面提到的 google、facebook、httpbin 服务,虽然不认识 test.yigegongjiang.com 这个域名,但还是返回了他们各自默认的 tls 证书。此时,如果用户选择强制信任证书: google、facebook 回到了 http 时候一样,因为 host 不匹配,主动拒绝服务了。 httpbin 也回到了 http 时候一样,可以正常访问。因为 httpbin 默认没有限制 host。 对于一台 Nginx 配置的目标服务器(假设默认域名是 a.com),如果需要完美兼容 test.yigegongjiang.com 这个域名的 cname 映射,需要做两件事: 识别到 SNI 是 test.yigegongjiang.com 后,需要返回 yigegongjinag.com 的 tls 证书,供浏览器进行安全链验证。 a. 或者返回一个 tls 证书,该证书同时包含 a.com 和 yigegongjiang.com 的认证。 将 a.com 和 test.yigegongjiang.com 这两个 host 同时指向同一个内部的 port 服务。 a. 或者不识别 host,所有 host 都指向同一个内部的 port 服务。 上面的两个方案,如果是特殊的服务场景倒还能实现,如 github page 提供了 static blog 服务,可以根据我们配置的域名来进行设置,从而完成访问。 对于 fly.io、koyeb.com 这些云平台,自定义域名就是它们的收费项,那肯定不会放开这个口子。 通过 代理 实现 koyeb.com 云平台自定义域名? 如果有一个免费的中间代理,我们访问 test.yigegongjiang.com 的所有请求,这个中间代理帮我们将所有流量正向代理到 koyeb.com 平台上,不就可以既不买 koyeb 的服务,又能实现自定义域名了吗? cloudflare 提供了这个能力,也是它的安全、缓存等一系列功能的起点。 cloudflare 配置 dns cname 的时候,有一个【代理】选项。选中后,当前配置虽然看起来是 cname,但完全脱离了 cname 本意。 此时不是 cname 了,通过 dig 工具会发现此时返回的是 cloudflare 的 ip 访问域名的流量,会被 cloudflare 转发到 cname 指向的域名上,这里的 cname 充当【代理】的角色。 实际上,依旧访问不通。显然 koyeb 这些云平台不会留下这么大的空子给别人钻。 因为 cloudflare、aws 这些正规的代理服务,在请求目标服务的时候,都会提供完善的信息已告知目标服务:我是代理。 它不仅把原始的域名等信息提供给了 koyeb,还会把自己的信息都提供给 koyeb。所以,koyeb 直接拒绝服务就行了。 当然,我们可以自己搭建一个【猥琐】的【三级代理】,将 cloudflare 那边的代理流量打过来,然后再去请求 koyeb,此时【猥琐代理】完全装作正常用户的浏览器,那 koyeb 的确是察觉不出来的。 此时,cloudflare 又能够捕获到完整的数据,实现缓存、边缘节点、cdn 等等能力。

2024/11/10
articleCard.readMore

WebRTC 的一些解释

最近工作上在使用 WebRTC,在公司内部做了技术分享。这里把内容进行脱敏,整理公开。 快速进入 WebRTC 媒体协商 概要 每方确定自己的编解码、网络等信息(媒体) 每方将自己的信息给到对方(协商) 信令 概要 webrtc 建联前,在各方之间传输【协商数据】 offer、answer、candidate webrtc 建联后,在各方之间传输【控制信息】 mute、leave、join、… 多样化应用场景 webrtc 通道多样: 每条 webrtc 通道连接两个端 每个端可以有 n 个 webrtc 通道 服务器可以成为其中一个端 对【端】的影响: 上行 和 下行带宽 视频编解码对 CPU/GPU 的消耗 2 人参会,两个【客户端】直接打通,最舒适 4 人参会,每个【客户端】打开多个通道,上下行剧增,硬件消耗剧增 8 人参会,【服务端】作为中转通道,相比 4 人场景,下行剧增,上行剧减,硬件消耗增加 50 人参会,【服务端】作为终极合流通道。相比 8 人场景,客户端回退到 2 人消耗,并把所有消耗转移到【服务端】 Tips:当【服务端】作为 webrtc 的一个通道后,玩法就非常多样了。比如【说话人大屏】、【服务端滤镜】等 流媒体的质量约束 清晰度 vs 流畅度 vs 码率 分辨率影响清晰度,越大越好,流尺寸也会越大。 补充:同样分辨率在不同的显示屏上,也会有清晰度的差异。详见 分辨率解释,介绍了【光源】、【虚拟像素】、【像素软件化】、【高清屏】对图片的影响。 帧率影响流畅度。视频由 帧 画面组成,同一秒内 帧 画面越多,肉眼感知越流畅,流尺寸也会越大。 电影:24fps,电视机:30 fps,电脑 & 手机:60 fps - 120 fps 码率:每秒传输的比特数据量。单位:[number] bps 码率 = 分辨率 × 比特深度 × 帧率 × 压缩比 比特深度:描述一个 px 所需要的 bit。通常 RGB 为 3 色彩通道,每个色彩需要 8 个 bit,即 24 bit。 压缩比:H.264/H.265 压缩效率极高,根据视频画面的【前后帧之间的 px 差异】,极大降低视频流尺寸。 示例:(场景设定:分辨率:1080p,色彩通道:3,帧率:30,编码:H.264) 码率 = 1920 × 1080 × 24 × 30 x 1/100 = 14.93 Mbps = 14930Kbps = 1866.25KBps = 1.87 MBps 即:每秒需要 1.87 M 的流量 在 webrtc 中,分辨率和帧率都是由开发人员控制的(主动调用 sdk api 将 stream 给到 sdk)。其中: stream 本身具有尺寸,即 分辨率。 调用 sdk api 的频次,即 帧率。 webrtc 控制码率有两种行为: 主动控制:当发现网络状态不佳,webrtc 会通过内部算法自行调整码率,包括自行调整码率,甚至直接丢弃数据包。 被动约束:开发人员预设期望码率范围,但【主动控制】优先。 编解码 H.264/5 等编码。很重要,但还不会。这里留坑。 媒体协商的多样性 SDP 是什么 SDP - Session Description Protocol(会话描述协议)。用于在两个通信终端之间描述细节。 强制依赖【协议描述规则】进行解析 a - b 之间约定了 n 条通信规则,就有 n 种 SDP 通信数据。 SDP - offer/answer SDP Format: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 示例//=============会话描述==================== v=0 o=- 7017624586836067756 2 IN IP4 127.0.0.1 s=- t=0 0 ... //================媒体描述================= //================音频媒体================= /* * 音频使用端口1024收发数据 * UDP/TLS/RTP/SAVPF 表示使用 dtls/srtp 协议对数据加密传输 * 111、103 ... 表示本会话音频数据的 Payload Type */ m=audio 1024 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126 //==============网络描述================== //指明接收或者发送音频使用的IP地址,由于WebRTC使用ICE传输,这个被忽略。 c=IN IP4 0.0.0.0 //用来设置rtcp地址和端口,WebRTC不使用 a=rtcp:9 IN IP4 0.0.0.0 ... //==============音频安全描述================ //ICE协商过程中的安全验证信息 a=ice-ufrag:khLS a=ice-pwd:cxLzteJaJBou3DspNaPsJhlQ a=fingerprint:sha-256 FA:14:42:3B:C7:97:1B:E8:AE:0C2:71:03:05:05:16:8F:B9:C7:98:E9:60:43:4B:5B:2C:28:EE:5C:8F3:17 ... //==============音频流媒体描述================ a=rtpmap:111 opus/48000/2 //minptime代表最小打包时长是10ms,useinbandfec=1代表使用opus编码内置fec特性 a=fmtp:111 minptime=10;useinbandfec=1 ... a=rtpmap:103 ISAC/16000 a=rtpmap:104 ISAC/32000 a=rtpmap:9 G722/8000 ... //=================视频媒体================= m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98 ... //=================网络描述================= c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 ... //=================视频安全描述================= a=ice-ufrag:khLS a=ice-pwd:cxLzteJaJBou3DspNaPsJhlQ a=fingerprint:sha-256 FA:14:42:3B:C7:97:1B:E8:AE:0C2:71:03:05:05:16:8F:B9:C7:98:E9:60:43:4B:5B:2C:28:EE:5C:8F3:17 ... //================视频流描述=============== a=mid:video ... a=rtpmap:100 VP8/90000 //================服务质量描述=============== a=rtcp-fb:100 ccm fir a=rtcp-fb:100 nack //支持丢包重传,参考rfc4585 a=rtcp-fb:100 nack pli a=rtcp-fb:100 goog-remb //支持使用rtcp包来控制发送方的码流 a=rtcp-fb:100 transport-cc ... SDP Raw Example: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 v=0 o=- 5595748951276187774 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE 0 1 2 a=extmap-allow-mixed a=msid-semantic: WMS FAMS m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 102 0 8 13 110 126 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:2D6J a=ice-pwd:jWsbseLplx44YJwXRravag7Y a=ice-options:trickle renomination a=fingerprint:sha-256 F4:57:8B:7E:6C:A1:0F:08:95:72:67:68:3C:18:67:40:39:D9:D5:0A:A4:6E:C0:3E:67:BA:E0:02:B5:FF:EF:40 a=setup:actpass a=mid:0 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=sendrecv a=msid:FAMS FAMSa0 a=rtcp-mux a=rtpmap:111 opus/48000/2 a=rtcp-fb:111 transport-cc a=fmtp:111 minptime=10;useinbandfec=1 a=rtpmap:63 red/48000/2 a=fmtp:63 111/111 a=rtpmap:9 G722/8000 a=rtpmap:102 ILBC/8000 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:13 CN/8000 a=rtpmap:110 telephone-event/48000 a=rtpmap:126 telephone-event/8000 a=ssrc:3316953618 cname:cXfVAjx41LASIlBT a=ssrc:3316953618 msid:FAMS FAMSa0 m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:2D6J a=ice-pwd:jWsbseLplx44YJwXRravag7Y a=ice-options:trickle renomination a=fingerprint:sha-256 F4:57:8B:7E:6C:A1:0F:08:95:72:67:68:3C:18:67:40:39:D9:D5:0A:A4:6E:C0:3E:67:BA:E0:02:B5:FF:EF:40 a=setup:actpass a=mid:1 a=extmap:14 urn:ietf:params:rtp-hdrext:toffset a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:13 urn:3gpp:video-orientation a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id a=sendonly a=msid:FAMS FAMSv1 a=rtcp-mux a=rtcp-rsize a=rtpmap:96 H264/90000 a=rtcp-fb:96 goog-remb a=rtcp-fb:96 transport-cc a=rtcp-fb:96 ccm fir a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c34 a=rtpmap:97 rtx/90000 a=fmtp:97 apt=96 a=rtpmap:98 H264/90000 a=rtcp-fb:98 goog-remb a=rtcp-fb:98 transport-cc a=rtcp-fb:98 ccm fir a=rtcp-fb:98 nack a=rtcp-fb:98 nack pli a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e034 a=rtpmap:99 rtx/90000 a=fmtp:99 apt=98 a=rtpmap:100 red/90000 a=rtpmap:101 rtx/90000 a=fmtp:101 apt=100 a=rtpmap:127 ulpfec/90000 a=ssrc-group:FID 439044504 650966991 a=ssrc:439044504 cname:cXfVAjx41LASIlBT a=ssrc:439044504 msid:FAMS FAMSv1 a=ssrc:650966991 cname:cXfVAjx41LASIlBT a=ssrc:650966991 msid:FAMS FAMSv1 m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=ice-ufrag:2D6J a=ice-pwd:jWsbseLplx44YJwXRravag7Y a=ice-options:trickle renomination a=fingerprint:sha-256 F4:57:8B:7E:6C:A1:0F:08:95:72:67:68:3C:18:67:40:39:D9:D5:0A:A4:6E:C0:3E:67:BA:E0:02:B5:FF:EF:40 a=setup:actpass a=mid:2 a=sctp-port:5000 a=max-message-size:262144 SDP Raw Example Desc: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 //==会话描述== v=0 - **协议版本**:SDP 协议版本号,这里是版本0。 o=- 5595748951276187774 2 IN IP4 127.0.0.1 - **会话起源**: - 用户名:`-`(匿名) - 会话ID:`5595748951276187774` - 会话版本:`2` - 网络类型:`IN`(互联网) - 地址类型:`IP4` - 地址:`127.0.0.1`(本地回环地址) s=- - **会话名称**:`-`(未指定) t=0 0 - **会话活动时间**:从`0`到`0`,表示会话无限期有效。 a=group:BUNDLE 0 1 2 - **媒体流分组**:使用`BUNDLE`机制将媒体流`0`、`1`和`2`绑定在一起,共享一个传输通道。 a=extmap-allow-mixed - **扩展映射混合允许**:允许同时使用一字节和两字节的RTP头扩展(参考RFC 8285)。 a=msid-semantic: WMS FAMS - **媒体流标识语义**:`WMS`表示WebRTC媒体流,`FAMS`是媒体流的标识符。 //==音频媒体描述== m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 102 0 8 13 110 126 - **媒体描述**: - 媒体类型:`audio`(音频) - 端口:`9`(通常为`9`表示使用UDP/TLS/RTP协议并通过ICE协商实际端口) - 传输协议:`UDP/TLS/RTP/SAVPF`(安全的RTP协议,带反馈机制) - 有效负载类型(编码):`111`、`63`、`9`、`102`、`0`、`8`、`13`、`110`、`126` c=IN IP4 0.0.0.0 - **连接信息**: - 网络类型:`IN`(互联网) - 地址类型:`IP4` - 地址:`0.0.0.0`(占位符,表示实际地址通过ICE协商) a=rtcp:9 IN IP4 0.0.0.0 - **RTCP连接信息**: - 端口:`9` - 网络类型:`IN` - 地址类型:`IP4` - 地址:`0.0.0.0` a=ice-ufrag:2D6J - **ICE用户名碎片**:`2D6J`,用于ICE候选者的身份认证。 a=ice-pwd:jWsbseLplx44YJwXRravag7Y - **ICE密码**:`jWsbseLplx44YJwXRravag7Y`,用于ICE身份验证。 a=ice-options:trickle renomination - **ICE选项**: - `trickle`:支持逐步收集和发送ICE候选者(Trickle ICE)。 - `renomination`:支持重新提名最佳候选者。 a=fingerprint:sha-256 F4:57:8B:7E:6C:A1:0F:08:95:72:67:68:3C:18:67:40:39:D9:D5:0A:A4:6E:C0:3E:67:BA:E0:02:B5:FF:EF:40 - **DTLS指纹**: - 哈希算法:`sha-256` - 指纹值:用于验证DTLS握手的安全性。 a=setup:actpass - **DTLS连接角色**:`actpass`表示主动-被动,双方可协商谁主动发起DTLS握手。 a=mid:0 - **媒体标识符**:`0`,用于在BUNDLE中标识此媒体流。 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level - **RTP头扩展**: - ID:`1` - URI:`ssrc-audio-level`,用于传输音频电平信息。 a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time - **RTP头扩展**: - ID:`2` - URI:`abs-send-time`,用于同步媒体流的发送时间。 a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 - **RTP头扩展**: - ID:`3` - URI:`transport-wide-cc`,用于传输层的拥塞控制。 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid - **RTP头扩展**: - ID:`4` - URI:`mid`,用于标识媒体流的MID。 a=sendrecv - **发送接收方向**:支持同时发送和接收媒体流。 a=msid:FAMS FAMSa0 - **媒体流标识**: - 流ID:`FAMS` - 轨道ID:`FAMSa0` a=rtcp-mux - **RTCP复用**:音频的RTCP流与RTP流复用在同一个端口。 a=rtpmap:111 opus/48000/2 - **RTP映射**: - 编码:`opus` - 采样率:`48000`Hz - 声道数:`2` a=rtcp-fb:111 transport-cc - **RTCP反馈机制**: - 针对编码`111`(opus),支持`transport-cc`(传输层拥塞控制)。 a=fmtp:111 minptime=10;useinbandfec=1 - **格式参数**: - 针对编码`111`(opus) - `minptime=10`:最小包时间为10毫秒 - `useinbandfec=1`:启用带内前向纠错(FEC),提高音频质量 a=rtpmap:63 red/48000/2 - **RTP映射**: - 编码:`red`(冗余编码) - 采样率:`48000`Hz - 声道数:`2` a=fmtp:63 111/111 - **格式参数**: - 针对编码`63`(red) - `111/111`:表示冗余编码中使用的主、次编码类型都是`111`(opus) a=rtpmap:9 G722/8000 - **RTP映射**: - 编码:`G722` - 采样率:`8000`Hz a=rtpmap:102 ILBC/8000 - **RTP映射**: - 编码:`iLBC` - 采样率:`8000`Hz a=rtpmap:0 PCMU/8000 - **RTP映射**: - 编码:`PCMU`(G.711 μ-law) - 采样率:`8000`Hz a=rtpmap:8 PCMA/8000 - **RTP映射**: - 编码:`PCMA`(G.711 A-law) - 采样率:`8000`Hz a=rtpmap:13 CN/8000 - **RTP映射**: - 编码:`CN`(舒适噪声) - 采样率:`8000`Hz a=rtpmap:110 telephone-event/48000 - **RTP映射**: - 编码:`telephone-event`(DTMF信号) - 采样率:`48000`Hz a=rtpmap:126 telephone-event/8000 - **RTP映射**: - 编码:`telephone-event`(DTMF信号) - 采样率:`8000`Hz a=ssrc:3316953618 cname:cXfVAjx41LASIlBT - **同步源(SSRC)标识**: - SSRC:`3316953618` - CNAME:`cXfVAjx41LASIlBT`,用于跨媒体流同步 a=ssrc:3316953618 msid:FAMS FAMSa0 - **媒体流标识**: - 流ID:`FAMS` - 轨道ID:`FAMSa0` //==视频媒体描述== m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 - **媒体描述**: - 媒体类型:`video`(视频) - 端口:`9`(通过ICE协商实际端口) - 传输协议:`UDP/TLS/RTP/SAVPF`(安全的RTP协议,带反馈机制) - 有效负载类型(编码):`96`、`97`、`98`、`99`、`100`、`101`、`127` c=IN IP4 0.0.0.0 - **连接信息**:与音频部分相同 a=rtcp:9 IN IP4 0.0.0.0 - **RTCP连接信息**:与音频部分相同 a=ice-ufrag:2D6J - **ICE用户名碎片**:`2D6J`,与音频部分相同 a=ice-pwd:jWsbseLplx44YJwXRravag7Y - **ICE密码**:`jWsbseLplx44YJwXRravag7Y`,与音频部分相同 a=ice-options:trickle renomination - **ICE选项**:支持Trickle ICE和重新提名机制 a=fingerprint:sha-256 F4:57:8B:7E:6C:A1:0F:08:95:72:67:68:3C:18:67:40:39:D9:D5:0A:A4:6E:C0:3E:67:BA:E0:02:B5:FF:EF:40 - **DTLS指纹**:用于DTLS握手的安全验证 a=setup:actpass - **DTLS连接角色**:双方均可主动或被动,待协商 a=mid:1 - **媒体标识符**:`1`,用于BUNDLE中的媒体流标识 a=extmap:14 urn:ietf:params:rtp-hdrext:toffset - **RTP头扩展**: - ID:`14` - URI:`toffset`,传输时间偏移 a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time - **RTP头扩展**:ID `2`,绝对发送时间 a=extmap:13 urn:3gpp:video-orientation - **RTP头扩展**: - ID:`13` - URI:`video-orientation`,视频方向信息 a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 - **RTP头扩展**:ID `3`,传输层拥塞控制 a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay - **RTP头扩展**: - ID:`5` - URI:`playout-delay`,播放延迟建议 a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type - **RTP头扩展**: - ID:`6` - URI:`video-content-type`,视频内容类型 a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing - **RTP头扩展**: - ID:`7` - URI:`video-timing`,视频定时信息 a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space - **RTP头扩展**: - ID:`8` - URI:`color-space`,视频色彩空间信息 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid - **RTP头扩展**:ID `4`,媒体标识(MID) a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id - **RTP头扩展**: - ID:`10` - URI:`rtp-stream-id`,RTP流标识 a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id - **RTP头扩展**: - ID:`11` - URI:`repaired-rtp-stream-id`,修复的RTP流标识 a=sendonly - **发送方向**:只发送,不接收 a=msid:FAMS FAMSv1 - **媒体流标识**: - 流ID:`FAMS` - 轨道ID:`FAMSv1` a=rtcp-mux - **RTCP复用**:将RTCP与RTP复用同一端口 a=rtcp-rsize - **RTCP缩减大小**:使用较小的RTCP报文(RFC 5506) a=rtpmap:96 H264/90000 - **RTP映射**: - 编码:`H264` - 采样率:`90000`Hz a=rtcp-fb:96 goog-remb - **RTCP反馈**: - 针对编码`96`,支持`goog-remb`带宽估计 a=rtcp-fb:96 transport-cc - **RTCP反馈**: - 支持传输层拥塞控制 a=rtcp-fb:96 ccm fir - **RTCP反馈**: - 支持`ccm fir`(全帧请求) a=rtcp-fb:96 nack - **RTCP反馈**: - 支持`nack`(否定确认)用于丢包重传 a=rtcp-fb:96 nack pli - **RTCP反馈**: - 支持`pli`(图片丢失指示),请求关键帧 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c34 - **格式参数**: - 允许非对称级别:`level-asymmetry-allowed=1` - 分包模式:`packetization-mode=1`(非交错模式) - 配置文件级别:`profile-level-id=640c34`(High Profile,Level 3.4) a=rtpmap:97 rtx/90000 - **RTP映射**: - 编码:`rtx`(重传) - 采样率:`90000`Hz a=fmtp:97 apt=96 - **格式参数**: - 关联有效负载类型:`apt=96`(对应H264编码) a=rtpmap:98 H264/90000 - **RTP映射**: - 编码:`H264` - 采样率:`90000`Hz a=rtcp-fb:98 goog-remb - **RTCP反馈**:同编码`96` a=rtcp-fb:98 transport-cc - **RTCP反馈**:同编码`96` a=rtcp-fb:98 ccm fir - **RTCP反馈**:同编码`96` a=rtcp-fb:98 nack - **RTCP反馈**:同编码`96` a=rtcp-fb:98 nack pli - **RTCP反馈**:同编码`96` a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e034 - **格式参数**: - 配置文件级别:`profile-level-id=42e034`(Baseline Profile,Level 3.4) a=rtpmap:99 rtx/90000 - **RTP映射**: - 编码:`rtx` - 采样率:`90000`Hz a=fmtp:99 apt=98 - **格式参数**: - 关联有效负载类型:`apt=98`(对应H264编码) a=rtpmap:100 red/90000 - **RTP映射**: - 编码:`red`(冗余编码) - 采样率:`90000`Hz a=rtpmap:101 rtx/90000 - **RTP映射**: - 编码:`rtx` - 采样率:`90000`Hz a=fmtp:101 apt=100 - **格式参数**: - 关联有效负载类型:`apt=100`(对应red编码) a=rtpmap:127 ulpfec/90000 - **RTP映射**: - 编码:`ulpfec`(前向纠错) - 采样率:`90000`Hz a=ssrc-group:FID 439044504 650966991 - **SSRC组**: - 组语义:`FID`(流标识) - SSRC列表:`439044504`(主流),`650966991`(重传流) a=ssrc:439044504 cname:cXfVAjx41LASIlBT - **同步源(SSRC)标识**: - SSRC:`439044504` - CNAME:`cXfVAjx41LASIlBT` a=ssrc:439044504 msid:FAMS FAMSv1 - **媒体流标识**: - 流ID:`FAMS` - 轨道ID:`FAMSv1` a=ssrc:650966991 cname:cXfVAjx41LASIlBT - **同步源(SSRC)标识**: - SSRC:`650966991` - CNAME:`cXfVAjx41LASIlBT` a=ssrc:650966991 msid:FAMS FAMSv1 - **媒体流标识**: - 流ID:`FAMS` - 轨道ID:`FAMSv1` //==数据通道描述== m=application 9 UDP/DTLS/SCTP webrtc-datachannel - **媒体描述**: - 媒体类型:`application`(应用数据) - 端口:`9`(通过ICE协商) - 传输协议:`UDP/DTLS/SCTP`(基于DTLS的SCTP传输) - 格式:`webrtc-datachannel`,表示WebRTC数据通道 c=IN IP4 0.0.0.0 - **连接信息**:与前面一致 a=ice-ufrag:2D6J - **ICE用户名碎片**:`2D6J`,与前面一致 a=ice-pwd:jWsbseLplx44YJwXRravag7Y - **ICE密码**:`jWsbseLplx44YJwXRravag7Y`,与前面一致 a=ice-options:trickle renomination - **ICE选项**:支持Trickle ICE和重新提名 a=fingerprint:sha-256 F4:57:8B:7E:6C:A1:0F:08:95:72:67:68:3C:18:67:40:39:D9:D5:0A:A4:6E:C0:3E:67:BA:E0:02:B5:FF:EF:40 - **DTLS指纹**:用于安全验证 a=setup:actpass - **DTLS连接角色**:双方均可主动或被动 a=mid:2 - **媒体标识符**:`2`,用于BUNDLE中的媒体流标识 a=sctp-port:5000 - **SCTP端口**:`5000`,用于SCTP协议的传输 a=max-message-size:262144 - **最大消息大小**:`262144`字节,数据通道单次消息的最大尺寸 SDP - Candidate candidate: 一个可能的网络连接点的候选项。网络节点负责最终的音视频流传输的数据通道,很重要。 sdpMid/sdpMLineIndex: 用来标记 SDP 中的媒体流索引。即每一个 candidate 候选,都属于某一个媒体流通道,如 音频流、视频流等。 1 2 3 4 5 6 7 8 9 { "candidate": "candidate:4109260943 1 udp 8331263 x.x.x.x 36634 typ relay raddr x.x.x.x rport 63859 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" } 优先级排序: 1. 候选类型:host > srflx > relay 2. 网络成本 network-cost:wifi > 蜂窝 下面示例,是使用 Cloudflare TURN 服务采集到的一个网络发现节点: candidate:4109260943 1 udp 8331263 x.x.x.x 36634 typ relay raddr x.x.x.x rport 63859 generation 0 ufrag 2D6J network-id 1 network-cost 10 tips: TCP or UDP:webrtc 使用 SRTP 来传输音视频流,该应用层协议可同时在 TCP 和 UDP 上工作。其中,默认使用 UDP。只有 UDP 不可用后会回退到 TCP。 N 个 candidate 都是候选并被 webrtc sdk 存储。当 webrtc 使用过程中【中断】了,会自行寻找其他的 candidate 作为候选,对开发者透明(自动重连)。 Candidate Raw Example: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 [ { "candidate": "candidate:2524315334 1 udp 2122260223 10.0.3.39 59309 typ host generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:2489915706 1 udp 2122194687 169.254.125.209 51656 typ host generation 0 ufrag 2D6J network-id 2 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:1463068104 1 udp 2121998079 192.0.0.1 54311 typ host generation 0 ufrag 2D6J network-id 7 network-cost 900", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:878085806 1 udp 2122131711 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 50281 typ host generation 0 ufrag 2D6J network-id 8 network-cost 900", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:3258950406 1 udp 2122066175 240b:c010:823:5c21:40a5:9a43:ff0c:906 58647 typ host generation 0 ufrag 2D6J network-id 9 network-cost 900", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:306562041 1 udp 2121935103 240b:c010:823:5c21:18b6:b94e:abe4:cec2 53502 typ host generation 0 ufrag 2D6J network-id 3 network-cost 50", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:306562041 1 udp 2121869567 240b:c010:823:5c21:18b6:b94e:abe4:cec2 54246 typ host generation 0 ufrag 2D6J network-id 4 network-cost 50", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:2524315334 1 udp 2122260223 10.0.3.39 49555 typ host generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:2489915706 1 udp 2122194687 169.254.125.209 50166 typ host generation 0 ufrag 2D6J network-id 2 network-cost 10", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:1463068104 1 udp 2121998079 192.0.0.1 53998 typ host generation 0 ufrag 2D6J network-id 7 network-cost 900", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:878085806 1 udp 2122131711 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 52161 typ host generation 0 ufrag 2D6J network-id 8 network-cost 900", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:3258950406 1 udp 2122066175 240b:c010:823:5c21:40a5:9a43:ff0c:906 60559 typ host generation 0 ufrag 2D6J network-id 9 network-cost 900", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:306562041 1 udp 2121935103 240b:c010:823:5c21:18b6:b94e:abe4:cec2 50196 typ host generation 0 ufrag 2D6J network-id 3 network-cost 50", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:3975092077 1 tcp 1517955327 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63867 typ host tcptype passive generation 0 ufrag 2D6J network-id 3 network-cost 50", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:3975092077 1 tcp 1517889791 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63868 typ host tcptype passive generation 0 ufrag 2D6J network-id 4 network-cost 50", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:1759455826 1 tcp 1518280447 10.0.3.39 63862 typ host tcptype passive generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:1791217070 1 tcp 1518214911 169.254.125.209 63863 typ host tcptype passive generation 0 ufrag 2D6J network-id 2 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:2845733212 1 tcp 1518018303 192.0.0.1 63864 typ host tcptype passive generation 0 ufrag 2D6J network-id 7 network-cost 900", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:3405533754 1 tcp 1518151935 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 63865 typ host tcptype passive generation 0 ufrag 2D6J network-id 8 network-cost 900", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:1016428434 1 tcp 1518086399 240b:c010:823:5c21:40a5:9a43:ff0c:906 63866 typ host tcptype passive generation 0 ufrag 2D6J network-id 9 network-cost 900", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:1759455826 1 tcp 1518280447 10.0.3.39 63869 typ host tcptype passive generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:1791217070 1 tcp 1518214911 169.254.125.209 63870 typ host tcptype passive generation 0 ufrag 2D6J network-id 2 network-cost 10", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:2845733212 1 tcp 1518018303 192.0.0.1 63871 typ host tcptype passive generation 0 ufrag 2D6J network-id 7 network-cost 900", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:3405533754 1 tcp 1518151935 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 63872 typ host tcptype passive generation 0 ufrag 2D6J network-id 8 network-cost 900", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:1016428434 1 tcp 1518086399 240b:c010:823:5c21:40a5:9a43:ff0c:906 63873 typ host tcptype passive generation 0 ufrag 2D6J network-id 9 network-cost 900", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:3975092077 1 tcp 1517955327 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63874 typ host tcptype passive generation 0 ufrag 2D6J network-id 3 network-cost 50", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:1759455826 1 tcp 1518280447 10.0.3.39 63876 typ host tcptype passive generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:1791217070 1 tcp 1518214911 169.254.125.209 63877 typ host tcptype passive generation 0 ufrag 2D6J network-id 2 network-cost 10", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:2845733212 1 tcp 1518018303 192.0.0.1 63878 typ host tcptype passive generation 0 ufrag 2D6J network-id 7 network-cost 900", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:3405533754 1 tcp 1518151935 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 63879 typ host tcptype passive generation 0 ufrag 2D6J network-id 8 network-cost 900", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:1016428434 1 tcp 1518086399 240b:c010:823:5c21:40a5:9a43:ff0c:906 63880 typ host tcptype passive generation 0 ufrag 2D6J network-id 9 network-cost 900", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:3975092077 1 tcp 1517955327 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63881 typ host tcptype passive generation 0 ufrag 2D6J network-id 3 network-cost 50", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:3975092077 1 tcp 1517889791 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63882 typ host tcptype passive generation 0 ufrag 2D6J network-id 4 network-cost 50", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:1410854543 1 udp 25108735 x.x.x.x 53914 typ relay raddr x.x.x.x rport 63839 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:1101771793 1 udp 41886207 x.x.x.x 53010 typ relay raddr x.x.x.x rport 49555 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:1101771793 1 udp 41886207 x.x.x.x 52333 typ relay raddr x.x.x.x rport 59309 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:1541054091 1 udp 25108735 x.x.x.x 52668 typ relay raddr x.x.x.x rport 63829 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" }, { "candidate": "candidate:3011387645 1 udp 25108735 x.x.x.x 44090 typ relay raddr x.x.x.x rport 63857 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:351976030 1 udp 8331263 x.x.x.x 46887 typ relay raddr x.x.x.x rport 63834 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 1, "sdpMid": "1" }, { "candidate": "candidate:4109260943 1 udp 8331263 x.x.x.x 36634 typ relay raddr x.x.x.x rport 63859 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 2, "sdpMid": "2" }, { "candidate": "candidate:2106314174 1 udp 8331263 x.x.x.x 23955 typ relay raddr x.x.x.x rport 63831 generation 0 ufrag 2D6J network-id 1 network-cost 10", "sdpMLineIndex": 0, "sdpMid": "0" } ] Candidate lines: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 candidate:2524315334 1 udp 2122260223 10.0.3.39 59309 typ host generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:2489915706 1 udp 2122194687 169.254.125.209 51656 typ host generation 0 ufrag 2D6J network-id 2 network-cost 10 candidate:1463068104 1 udp 2121998079 192.0.0.1 54311 typ host generation 0 ufrag 2D6J network-id 7 network-cost 900 candidate:878085806 1 udp 2122131711 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 50281 typ host generation 0 ufrag 2D6J network-id 8 network-cost 900 candidate:3258950406 1 udp 2122066175 240b:c010:823:5c21:40a5:9a43:ff0c:906 58647 typ host generation 0 ufrag 2D6J network-id 9 network-cost 900 candidate:306562041 1 udp 2121935103 240b:c010:823:5c21:18b6:b94e:abe4:cec2 53502 typ host generation 0 ufrag 2D6J network-id 3 network-cost 50 candidate:306562041 1 udp 2121869567 240b:c010:823:5c21:18b6:b94e:abe4:cec2 54246 typ host generation 0 ufrag 2D6J network-id 4 network-cost 50 candidate:2524315334 1 udp 2122260223 10.0.3.39 49555 typ host generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:2489915706 1 udp 2122194687 169.254.125.209 50166 typ host generation 0 ufrag 2D6J network-id 2 network-cost 10 candidate:1463068104 1 udp 2121998079 192.0.0.1 53998 typ host generation 0 ufrag 2D6J network-id 7 network-cost 900 candidate:878085806 1 udp 2122131711 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 52161 typ host generation 0 ufrag 2D6J network-id 8 network-cost 900 candidate:3258950406 1 udp 2122066175 240b:c010:823:5c21:40a5:9a43:ff0c:906 60559 typ host generation 0 ufrag 2D6J network-id 9 network-cost 900 candidate:306562041 1 udp 2121935103 240b:c010:823:5c21:18b6:b94e:abe4:cec2 50196 typ host generation 0 ufrag 2D6J network-id 3 network-cost 50 candidate:3975092077 1 tcp 1517955327 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63867 typ host tcptype passive generation 0 ufrag 2D6J network-id 3 network-cost 50 candidate:3975092077 1 tcp 1517889791 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63868 typ host tcptype passive generation 0 ufrag 2D6J network-id 4 network-cost 50 candidate:1759455826 1 tcp 1518280447 10.0.3.39 63862 typ host tcptype passive generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:1791217070 1 tcp 1518214911 169.254.125.209 63863 typ host tcptype passive generation 0 ufrag 2D6J network-id 2 network-cost 10 candidate:2845733212 1 tcp 1518018303 192.0.0.1 63864 typ host tcptype passive generation 0 ufrag 2D6J network-id 7 network-cost 900 candidate:3405533754 1 tcp 1518151935 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 63865 typ host tcptype passive generation 0 ufrag 2D6J network-id 8 network-cost 900 candidate:1016428434 1 tcp 1518086399 240b:c010:823:5c21:40a5:9a43:ff0c:906 63866 typ host tcptype passive generation 0 ufrag 2D6J network-id 9 network-cost 900 candidate:1759455826 1 tcp 1518280447 10.0.3.39 63869 typ host tcptype passive generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:1791217070 1 tcp 1518214911 169.254.125.209 63870 typ host tcptype passive generation 0 ufrag 2D6J network-id 2 network-cost 10 candidate:2845733212 1 tcp 1518018303 192.0.0.1 63871 typ host tcptype passive generation 0 ufrag 2D6J network-id 7 network-cost 900 candidate:3405533754 1 tcp 1518151935 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 63872 typ host tcptype passive generation 0 ufrag 2D6J network-id 8 network-cost 900 candidate:1016428434 1 tcp 1518086399 240b:c010:823:5c21:40a5:9a43:ff0c:906 63873 typ host tcptype passive generation 0 ufrag 2D6J network-id 9 network-cost 900 candidate:3975092077 1 tcp 1517955327 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63874 typ host tcptype passive generation 0 ufrag 2D6J network-id 3 network-cost 50 candidate:1759455826 1 tcp 1518280447 10.0.3.39 63876 typ host tcptype passive generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:1791217070 1 tcp 1518214911 169.254.125.209 63877 typ host tcptype passive generation 0 ufrag 2D6J network-id 2 network-cost 10 candidate:2845733212 1 tcp 1518018303 192.0.0.1 63878 typ host tcptype passive generation 0 ufrag 2D6J network-id 7 network-cost 900 candidate:3405533754 1 tcp 1518151935 240b:c010:401:2c70:518d:9e8c:aa43:2dc4 63879 typ host tcptype passive generation 0 ufrag 2D6J network-id 8 network-cost 900 candidate:1016428434 1 tcp 1518086399 240b:c010:823:5c21:40a5:9a43:ff0c:906 63880 typ host tcptype passive generation 0 ufrag 2D6J network-id 9 network-cost 900 candidate:3975092077 1 tcp 1517955327 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63881 typ host tcptype passive generation 0 ufrag 2D6J network-id 3 network-cost 50 candidate:3975092077 1 tcp 1517889791 240b:c010:823:5c21:18b6:b94e:abe4:cec2 63882 typ host tcptype passive generation 0 ufrag 2D6J network-id 4 network-cost 50 candidate:1410854543 1 udp 25108735 x.x.x.x 53914 typ relay raddr x.x.x.x rport 63839 generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:1101771793 1 udp 41886207 x.x.x.x 53010 typ relay raddr x.x.x.x rport 49555 generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:1101771793 1 udp 41886207 x.x.x.x 52333 typ relay raddr x.x.x.x rport 59309 generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:1541054091 1 udp 25108735 x.x.x.x 52668 typ relay raddr x.x.x.x rport 63829 generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:3011387645 1 udp 25108735 x.x.x.x 44090 typ relay raddr x.x.x.x rport 63857 generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:351976030 1 udp 8331263 x.x.x.x 46887 typ relay raddr x.x.x.x rport 63834 generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:4109260943 1 udp 8331263 x.x.x.x 36634 typ relay raddr x.x.x.x rport 63859 generation 0 ufrag 2D6J network-id 1 network-cost 10 candidate:2106314174 1 udp 8331263 x.x.x.x 23955 typ relay raddr x.x.x.x rport 63831 generation 0 ufrag 2D6J network-id 1 network-cost 10, 网络发现 - STUN 以上媒体协商的过程中,SDP 的很多信息都可以由开发者控制,比如编码类型。但是 candidate 网络节点的收集会比较复杂。 数据包如何进入公网 NAT 路由映射表: NAT 的多样性 路由器具有特定的类型:完全锥形、地址限制锥形、端口限制锥形、对称 NAT 映射表规则: 映射端口: 对称 NAT → {source_ip, source_port, dest_ip, dest_port} 有一个变化,将新建映射端口 端口限制锥形 / 地址限制锥形 → {source_ip, source_port} 有一个变化,将新建映射端口 回流数据包: 对称 NAT / 端口限制锥形:被【映射表】映射过的 {dest_ip, dest_port} 可以回流 地址限制锥形:被【映射表】映射过的 {dest_ip} 可以回流 P2P 打洞 对于 p2p 打洞失败的场景,就需要 TURN 做中继服务,转发音视频流。 中继服务 - TURN 协商回退 在 ipv6 越多的场景下,上面提到的 ipv4 NAT 问题可以有效解决。但 STUN 网络发现依旧不可缺少。local、多 ipv6、防火墙等场景依旧需要网络择优。 ipv6 场景下,不需要打洞就能直连,最具有性价比。以下场景可以专门做【协商回退】处理: server 端 ipv6 和 ipv4 共存,那么 client 端只要有 ipv6 就明确使用 ipv6 1 2 3 4 5 6 7 8 pc.onicecandidate = event => { if (event.candidate) { if (event.candidate.address.indexOf(':') !== -1) { // IPv6候选者,可能优先处理 handleIPv6Candidate(event.candidate); } } }; 回退(ipv6 → ipv4 → turn 中继) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pc.oniceconnectionstatechange = () => { switch(this.pc.iceConnectionState) { case 'checking': // 设置连接超时 connectionTimer = setTimeout(() => { this.handleConnectionTimeout(); }, this.connectionTimeout); break; } }; async handleConnectionTimeout() { console.log('连接超时,尝试回退策略'); if (this.preferredProtocol === 'IPv6') { console.log('切换到IPv4'); this.preferredProtocol = 'IPv4'; await this.restartConnection(); } else { console.log('尝试TURN服务器'); await this.switchToTURN(); } } 信令服务的演变 自建 IM - socket /websocket。 最复杂,最可控。可以把媒体协商、会控等一系列功能做体系化的整合。 私有独立服务使用该场景较多,如某个独立的 app 等。 SSE 长链接推送 / HTTP 轮询, SSE 指南 firebase realtime database - db + im RTDB 指南 WHIP & WHEP:正在逐渐形成标准,普遍适用于各大推流平台。(适用于中央服务器,因为服务器具有固定的 sdp - candidate) 通过 restful api 控制媒体协商和会话生命周期 可以通过 header、body 增加授权、编码定义、期望格式和分辨率等等参数 平台场景使用较多,如直播平台、云厂商。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Example: https://webrtcpush.tlivewebrtcpush.com/webrtc/v2/whip // WHIP 服务 const server = http.createServer((req, res) => { // 处理 WHIP 客户端请求 switch (req.method) { case "POST": // 生成 SDP Answer const sdpAnswer = generateSdpAnswer(sdpOffer); res.statusCode = 201; res.setHeader("Location", `/whip/sessions/${randomUUID()}`);// 用于 delete res.end(sdpAnswer); case "PATCH": // 处理 ICE 候选 let iceCandidate = req.text(); handleIceCandidate(iceCandidate); case "DELETE": // 处理会话删除 deleteSession(req.sessionId); case "OPTIONS": // 处理能力协商 res.setHeader("Allow", "POST, PATCH, DELETE, OPTIONS"); res.setHeader("Accept", "application/sdp"); } res.end(); }); function generateSdpAnswer(sdpOffer) { return `v=0 o=- 123456789 123456789 IN IP4 0.0.0.0 s=- t=0 0 m=video 9 UDP/TLS/RTP/SAVPF 96 c=IN IP4 0.0.0.0 a=rtpmap:96 VP8/90000 a=setup:active a=mid:0 `; } function handleIceCandidate(iceCandidate) { console.log("Received ICE candidate:", iceCandidate); } function deleteSession(sessionId) { console.log("Deleting session:", sessionId); } RTC DataChannel 当 webrtc 的两个端媒体协商完成后,可通过 webrtc 在两个端创建 全双工 的通信通道,即 RTC Data Channel。 应用场景: 聊天、简单的会控 会议白版、云游戏 飞书完成了高效的协同编辑 - 妙享 Magic Share 会议中,A 进行文档共享(非投屏) 其他人员默认跟随 A 的操作进行文档阅读(根队) 其他人员可自行操作、编辑文档(离队) 核心算法还是云文档协作,但通过 DataChannel 非常高效的完成会议结果的沉淀。 RTC SFU - MCU 架构 在不同的参会规模场景下,有多种流传输方案。 P2P 打洞场景最特殊,因为是终端直连,服务器无法监控通话质量。这种场景也最安全,视频流不会被服务器侧拦截和分析。 telegram 在 webrtc 自身的 dtls 流加密基础上,增加了 MTProto 端到端 加密。可以避免被中间服务分析流数据。 MCU 方案,server 需要对大量的【视频流、音频流】进行合流,增加了延时。可控性也最高,可以做各种场景的滤镜、变声、流组合特效等。 mcu 方案,不支持端到端加密。 现实场景下,除了视频聊天这种 1-1 场景对 webrtc 是强依赖外,其他流媒体服务如直播,都是多协议共存。 推流:webrtc、rtmp、基于 webrtc 的私有协议 (优化延迟) 拉流:webrtc、hls、rtmp、flv 辅助:直播拉流场景,CDN 必不可少 WebRTC 源码宝藏 WebRTC 中 3A (AEC、AGC、ANC) 音频算法最顶级处理音频的算法,可以直接拿来用。 对于网络方向,网络带宽的评估、平滑处理、网络协议的实现在 WebRTC 中是应有尽有。

2024/11/9
articleCard.readMore

SSE 指南

Server-Sent Events(SSE)是一种允许服务器通过 HTTP 连接主动向客户端发送数据的技术。它主要被用于创建实时应用,如消息推送和实时通知。SSE 使用简单的文本格式发送消息,这种格式使得其易于在浏览器中实现和使用。 SSE 注意事项: 通过 HTTP 协议通道 建立单向长连接,即 Client 连接 Server 后,Server 不断开连接,并持续的通过 socket 套接字发送 data 给 Client。 网关等设备会主动关闭 tcp 通道,需要 SSE Server 端增加心跳。很多种场景都会导致 tcp 连接中断,和 IM 心跳一致。这里需要 SSE Server 增加应用层心跳,非 TCP 层心跳。 数据格式 通过具有明显分割线的消息体,来分割数据字段: 1 2 3 4 5 6 7 8 9 10 // 以下文本消息体,最终通过编译成二进制的形式被传递和解析,通过 \n 等标记符号进行【行】分割。 event: message/userupdate/custom...\n data: {xxx}\n id: 98769879675\n retry: 10000\n \n // 一个完整的消息体如下,通过 \n 分割行,末尾通过 \n\n 分割单个消息体 id: event-id-1\ndata:event-data-first\n\n 以上 event/data/id/retry 四个字段中,data 是必须字段,其他三个是可选字段。每个消息体,必须以 \n\n 作为末尾标记。 在 ts 中,可行的生成消息体的 code 如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 private encodeMessage(message: ChatMessage) { const data = { type: message.type, payload: message.data, }; const content = [ `id: ${message.id}`, `data: ${JSON.stringify(data)}`, '', '', ].join('\n'); // 这里,通过空行分割,在消息体的末尾增加 `\n\n` 标记 return this.encoder.encode(content); } 对消息体的解析,也同样遵循固定的规律,即对二进制中的 \n 和 \n\n 进行解析。 解析流程: 1 2 3 4 1. 通过 \n\n 对消息体进行分割,获得一个消息体的二进制内容并开始解析 2. 通过 \n 对行进行解析,获得 xxx:xxx 这样的一行内容 3. 通过 : 符号,对行进行解析,获得 key:xxx 和 value:xxx 两个内容 4. 整合获得的所有内容,聚合成 {id:xxx,event:xxx,data:xxx,retry:xxx} 这样的消息体 消息类型 SSE 通过 event 字段,可以自定义各种消息体。约定通用的消息体是 message。有如下两种消息体: 1 2 id: xxx\ndata:xxx\nevent:message\n\n id:xxx\ndata:xxx\nevent:{custom-name}\n\n 如果 event 为空,默认当作且应该当作 message 消息来解析和处理。 重连 http 通道可能发生中断,每条消息题都可以携带一个 retry 字段,用来告知 client 在断开后多久应该重连。 而重连的逻辑就是重新发起 http 连接。这里为了保持和之前的通道一致,应该在重连的时候在 header 中携带最后一次收到的消息的 id,可以让 server 侧知道从哪里中断的,以保持连续的服务。 心跳 SSE 应该使用应用层心跳,即发送一条空消息体,以保持 http tcp 套接字不被网关、nat 等场景强制关闭。格式如下: 1 2 :\n\n :heartbeat\n\n 这里,心跳可以不遵循消息体的约定。 之前提到消息体一定需要有 data 字段,因为每一条消息都是需要传递信息,如果没有 data 字段,那么这条消息就无法被解析,也就没有传递的必要。 但是这套约定,并不是说没有 data 字段,消息的发送、接收、解析 整套流程就会失败,因为对于 TCP/UDP/HTTP 这套协议来说,它并不关心消息体的内容是什么。 而 Client 端,对消息体的解析应该是包容的,即消息体如果不符合约定,那么就应该抛弃。 这里的应用层心跳,就会走到这套逻辑里面。IM 是双向通信,为了保障心跳的到达,Client 端需要解析完整的心跳并回执。在 SSE 单向通道里面,只需要保障有一条消息从 Server 发往 Client 即可(没有成功率保障,因为是单向通信)。

2024/11/1
articleCard.readMore

1.1.1.1、WARP 和 MASQUE

实际测试结果,虽然 Cloudflare 号称服务部署非常多,但使用 Warp 的 【VPN / 代理】功能,会明显增加网络延迟。(location & ip: Japan) Cloudflare 提供了 1.1.1.1 DNS 服务,以及 WARP VPN + 代理。 公共 DNS 运营商 DNS 有什么问题? 直接使用运营商的 DNS 服务,有下面两个重要的问题: ISP 会通过 DNS 查询的域名,分析用户访问的网站。(DNS 查询是明文的 UDP) ISP DNS 会有劫持、污染、缓存等 那么公共 DNS 可以解决上面的问题吗?不行。 对于普通用户来说,使用运营商 DNS 没有任何问题,也会加快域名解析的速度。 如果想要安全,避免被运营商做【DNS 查询】分析,公共 DNS 无能为力。不管使用的公共 DNS 是 ipv4、ipv6、DoH,都无法解决。 DoH 是能用到最安全的级别了,但还是有 SNI 泄漏。 1 2 3 4 5 - IPv4 和 IPv6: - 1.1.1.1 和 2606:4700:4700::1111 - 1.0.0.1 和 2606:4700:4700::1001 - DoH: - https://cloudflare-dns.com/dns-query 公共 DNS 如何实现负载均衡 通过域名,可以在 DNS 权威服务器上做 IP 的负载均衡。 那么,对于只有 IP 地址而没有域名的 DNS 服务,如何实现负载均衡呢? 每个 IP 能被发现,是通过网络节点之间的 BGP(边界网关协议)进行广播。 通过 Anycast 技术,可以在不同地理位置的多台主机上部署相同的 IP,从而让客户端就近连接到 IP 服务器。 WARP WARP 是 Cloudflare 提供的一种 VPN 服务,使用了 WireGuard 协议。 不同的 VPN 协议分别工作在网络模型的不同层次,其中 WireGuard 工作在第三层(网络层)。 因为 iOS 等系统,同一时间只能开启一个 VPN 服务,所以使用 WARP 的时候,数据包流向为: WARP app 拦截所有三层数据包并转发到 WARP 的 WireGuard 服务器,服务器向具体的 domain 发起请求。 在 Mac 系统上,除了 WARP 还可以手动设置网络代理(Thrid Proxys),这些网络代理一般工作在应用层。这个时候数据包流向为: 数据包转到 Third Proxys 进行封装,目标 domain 变成【代理 服务器】 WARP app 拦截所有三层数据包转发到【WARP 服务器】 【WARP 服务器】向【代理 服务器】发起请求 【代理 服务器】向具体的 domain 发起请求 MASQUE 通常使用的翻墙工具并不是 VPN,而是网络代理(更准确地说,称为 “正向代理”)。 之所以经常将它们称为 VPN,是因为在 iOS 等设备上,需要开启 VPN 服务,这些软件才能拦截数据流量,实现代理转发。 但它们与通常所指的 WireGuard、OpenVPN 等 VPN 协议并不一致。 WARP 推出了基于 MASQUE 协议的版本,用于替代 WireGuard。MASQUE 使用了 QUIC 协议,属于应用层的网络数据转发。 从本质上来说,此时的 WARP 已经不能称为 VPN,而是 “正向代理”。具体来说: WARP 在网络分层的底层(第三层网络层)捕获系统流量,这一点与使用 WireGuard 时相同。不同的是后续的处理方式: WireGuard:使用该协议时,采用的是 VPN 方案。对拦截的数据包进行加密,然后通过隧道发送。 MASQUE:使用该协议时,采用的是正向代理方案。对拦截的数据包,通过 QUIC 协议代理到 WARP 服务端节点。 这种技术方案与 macOS 上著名的代理软件 Surge 非常相似。Surge 有一个功能叫 “增强模式”。 在正常情况下,Surge 通过在系统网络设置中的代理配置设置 localhost 代理,捕获应用层的流量进行转发。 开启增强模式后,Surge 会通过第三层捕获所有流量包,接管后进行转发。 WARP 为什么抛弃【WireGuard】使用 【MASQUE】,可以参考官方介绍: https://blog.cloudflare.com/masque-building-a-new-protocol-into-cloudflare-warp/

2024/10/27
articleCard.readMore

移动端换端方案与场景还原

换端能力 在移动应用生态中,换端 (App Switching) 是一个常见的需求。同一台设备上,有以下三个端: M(Web网页)、 A(移动应用 app)、 B(待唤端的应用 app)。 当 M 或 A 需要跳转至 B 时,有两种技术方案: URL Scheme 和 Universal Link。 URL Scheme App 端先行验证能力: A 可以通过 canOpen 方法预先判断 B 是否已安装,且此操作不会触发跳转 Web 端无先行验证能力: M (Web) 无法预先判断 B 是否安装 通常采用延迟判断方案:执行跳转后等待 2 秒,根据页面状态判断跳转是否成功 URL Scheme 存在一个严重的安全问题:不同的应用可以注册相同的 scheme。这意味着当 A 尝试跳转 B 时,可能会被恶意应用 X/Y/Z 截获。正是由于这个安全隐患,URL Scheme 正逐渐被 Universal Link 替代。 Universal Link 预判限制: M 和 A 都无法预先判断 B 是否安装 跳转控制: A 可以通过跳转参数强制使用 Universal Link 进行跳转 跳转执行后可以判断目标应用是否安装 原理 & 挑战 应用安装时,操作系统会向配置的 domain 请求资源文件 (开发人员提前配置), 该文件包含应用约定的 Router 信息 如果 HTTPS 请求失败,应用将暂时无法被 Universal Link 唤醒 Apple 未明确指定重试机制,仅表示系统会在适当时机重新请求 微信的二次回跳验证机制 二次回跳说明 当应用 A 首次使用微信 SDK 的 Universal Link 功能进行分享时,会出现双重跳转: 1 A -> 微信 -> A -> 微信 这种情况仅在首次分享时出现,后续分享操作将直接完成。 双重验证有效性 Universal Link 在安全的前提下,成功率较高,能保证其他 App 跳转到我的 App 的成功率。只要我的 App 配置了 Universal Link,大概率其他 App 跳转到我的 App 是没有问题的。 但是,我的 App 也需要跳转回其他 App,此时我的 App 并不信任其他 App(或者说不信任其他 App 的 Universal Link 配置)。 举个分享流程的例子,如果 A 通过 Universal Link 跳转到微信后,微信处理完分享流程。用户点击返回 A,但无法返回,此时就会造成严重的用户体验缺失(Bug)。 二次回跳处理 微信采用类似 TCP 三次握手的验证机制,提前打通 A 和微信之间的换端通路。这可能涉及到降级处理,即 Universal Link 降级为 URL Scheme。仍以分享流程为例: A(通过微信 SDK API)使用 Universal Link 跳转到微信。 SDK API 增加了选项控制,指定使用 Universal Link 方式。如果失败,直接降级到 URL Scheme 进行跳转,此时不会有二次回跳验证。 微信拉取服务端资源,将 A 的 URL Scheme 和 Universal Link 拉取到本地(A 提前在微信的开发者平台进行了配置)。 微信通过 Universal Link 跳转回 A。 指定使用 Universal Link。如果失败,说明 A 的 Universal Link 配置有问题,降级到 URL Scheme 通知 A。后续 A 和微信的操作(分享操作仍需继续)都通过 URL Scheme 完成。 A(通过微信 SDK API)判断微信通过 Universal Link 成功唤起了自己,表明 A 和微信之间的 Universal Link 通路畅通。 后续所有的换端操作,全部通过 Universal Link 完成。 A(通过微信 SDK API)发起真正的分享换端,微信被唤醒后执行分享流程(朋友、朋友圈、收藏等),然后回跳到 A。 … 之后,所有换端操作都直接通过 Universal Link 进行,不再需要之前的二次回跳验证。 Web 向原生 App 发起登录验证 web 页面在需要登陆的场景,可能需要跳转到 app 中完成登陆,然后回到到网页中。 处理流程 唤起阶段: Web 页面 (url_a) 通过 Universal Link 或 URL Scheme 唤起 App 携带必要的身份标识参数 登录处理: 方案一:服务端传递 Token App 将 token 和身份标识发送至服务端 App 通过 openURL(url_a) 在 Safari 中刷新网页(不会新开页面) Web 根据本地存储的标识参数,从服务器获取 Token。 方案二:换端传递 Token App 将 Token 拼接到 url_b_{path} 中,通过 openURL 唤醒。 Safari 会新开页面,新的页面是登录成功后的落地页。 Delay Deep Link(场景还原 - 一般用于广告投放) 通过 Universal Link,如果目标 App 未安装,此时可以跳转到 Web 中间页并转跳到 App Store。希望用户下载 App 并首次打开后,立即跳转到当初 Universal Link 的承接页。这也叫【场景还原】。 核心原理是,全新的 App 在第一次被打开后,需要通过某种途径,找到之前 Universal Link 中标记的落地页信息。 方案一:本地 在使用 Universal Link 进行换端时,发起换端的一方负责将换端链接存储到用户的剪贴板中。 当目标 App 打开后,自行读取剪贴板并进行落地页跳转。剪贴板有预探功能,可以先不触发用户的权限弹框,提前知道是否有需要的口令内容。 方案二:设备标识(Device ID) 这里的 Device ID 并不是真正的设备 ID,而是大家共同遵守的一套规则,可近乎唯一地标记一台设备,并保证漂移率很低。可以根据设备的开机时间、升级时间、系统版本等信息,组合成密钥字符。 在使用 Universal Link 进行换端时,发起换端的一方负责将 Device ID 和落地页信息存储到云端。 当目标 App 打开后,自行获取 Device ID,然后从云端回拉落地页信息。 一般来说,这些场景都牵涉到资金投放,需要尽可能高的识别精度。所以方案都是混合使用,有一个匹配上就算【场景还原】成功了。

2024/10/27
articleCard.readMore

设备发现

Bonjour local net 服务发现。核心是两个 api:NSNetService & NSNetServiceBrowser。 Bonjour 的目的,是希望发现局域网中的设备,包括这些设备的 ip、port 等信息,从而进行下一步业务操作。因为有了 ip、port,就可以精确定位一台设备了,就可以做很多事情了,比如打通 socket 等。 NSNetService 用来发布服务,表明 m 提供了哪些能力,如打印机、http 服务等等。 1 2 // 通过指定服务名称、类型和端口号来创建 NSNetService 实例 netService = NSNetService(domain: "local.", type: "_http._tcp.", name: "MyService", port: 8080) NSNetServiceBrowser 用来检索服务,查找当前 local net 中存在哪些与查询内容相匹配的服务,进而可以获取到 ip 和 port。 1 2 3 netServiceBrowser = NSNetServiceBrowser() // 查找局域网中所有 _http._tcp. 类型的服务 netServiceBrowser?.searchForServices(ofType: "_http._tcp.", inDomain: "local.") 参数约定 - from ai 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 `NSNetService` 的参数 `domain` 和 `type` 并不是可以随意设置的,它们有明确的约定和用途,尤其是为了确保服务发现的兼容性和正确性。 ### 1. `domain` 参数: - **约定**: - 通常,局域网内使用的域名是 `"local."`,这是 Bonjour 标准中定义的默认域,表示当前局域网范围内的服务。 - Bonjour 中一般使用的是 `"local."`,它会自动将服务广播到本地网络。 - 如果你要在其他特定域中发布服务,也可以指定其他域名,但在局域网服务发现中几乎总是使用 `"local."`。 - **是否可以随意更改?**: - 对于局域网内的服务发现,推荐使用 `"local."`,它是 Bonjour 默认的局域网广播域。 - 如果你有自己的 DNS 域名或网络环境,你可以指定自定义域名,但这通常涉及更复杂的网络设置,且在局域网内使用可能不兼容。 ### 2. `type` 参数: - **约定**: - `type` 定义了服务的协议和传输层信息。它遵循 IANA(Internet Assigned Numbers Authority)发布的服务类型命名标准,并且通常采用如下格式: - `_<protocol>._<transport>` - 例如: - `"_http._tcp."`:表示 HTTP 服务使用 TCP 传输协议。 - `"_ftp._tcp."`:表示 FTP 服务使用 TCP 传输协议。 - `"_ipp._tcp."`:表示 IPP (Internet Printing Protocol) 使用 TCP 传输协议。 - **标准服务类型**:有许多常见的服务类型,诸如: - `"_http._tcp."`:HTTP 服务 - `"_ftp._tcp."`:FTP 服务 - `"_airplay._tcp."`:AirPlay 服务 - `"_ipp._tcp."`:网络打印协议 - `"_ssh._tcp."`:SSH 服务 - **是否可以随意更改?**: - 如果你想发布一个标准协议的服务,必须遵循该协议的命名约定(例如,`"_http._tcp."` 代表 HTTP 服务)。 - 如果是自定义服务(非标准服务),你可以创建自定义的类型名称。例如,你可以创建 `"_mycustomservice._tcp."` 这样自定义的服务类型。但是,自定义类型必须以下划线 `_` 开头,以确保与标准服务类型区分。 ### 总结: - **`domain`**:一般使用 `"local."`,这是 Bonjour 的局域网发现默认域。如果你在局域网中进行服务发现,最好不要随意修改这个参数。 - **`type`**:对于标准协议服务,如 HTTP、FTP 等,请使用标准的 IANA 定义的服务类型。如果是自定义服务类型,可以按格式自定义命名,但仍需遵循命名规范(下划线开头)。 确保遵循这些约定可以提高服务的兼容性,并确保局域网中的其他设备能够正确发现和解析服务。 MultipeerConnectivity 是 Apple 提供的近场通信,基于蓝牙和局域网实现。 原理也是服务的发布和发现,和 Bonjour 的不同点是,除了局域网之外,还可以通过 蓝牙 来做服务发现。 和 Bonjour 的差异: Bonjour 是为了发布、发现目标设备的 ip、port。 MultipeerConnectivity 是为了发布、发现目标设备。注意,这是的设备是一个 peer、一个实体,通过这个 peer,可以直接进行 data 的发送等,即这是一个对等的实体对象,可以直接进行通信。发送图片、文字、视频等。 Network.framework 这是 Apple 推出的兼容高层级 API 和 低层级 API 的库,它有高级代码封装部分可供开发人员直接使用,也有底层代码部分可供开发人员拼装各种能力。 在使用上,Network.framewok 完全可以代替 Bonjour 了,因为它集成了 Bonjour 的能力。 但还不能代替 MultipeerConnectivity,因为 MultipeerConnectivity 的蓝牙通信能力,Network.framework 并没有。 MultipeerConnectivity 对于 p2p 场景是高度封装的,使用方便、使用场景范围大,但不支持细粒度的调整优化。比如发送数据,就是调用 api 进行数据 send。具体如何 send 并不能控制。 但是 Network.framework 提供了细粒度的 api 操作,可以选择 tcp、udp 等不同方案以及细节参数。 Network.framework 更多用于具有网络能力的设备间通信。当然,如果只有蓝牙场景,那只能使用 MultipeerConnectivity 了。 WI-FI Direct 上面三种局域网近景通信,都不具有普适性。蓝牙是很有普适性的产物,但是蓝牙发展这么多年,有很大的局限性,如速度、距离等。 目前,WI-FI Direct 已经基本上进入了所有的电子设备中,通过 WI-FI direct,可以非常方便的在两台设备之间打通 p2p 通信通道,进而实现高速数据传输。 对于苹果全家桶,基本上所有的跨设备互通能力,都是通过 Wi-Fi Direct 能力来实现的,比如 Handoff、通用剪贴板(Universal Clipboard)等。 Wi-Fi Direct 技术原理 - from ai 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 1. **基本概念** - Wi-Fi Direct(也称 Wi-Fi P2P)是一种无线通信标准 - 允许设备间直接通信,无需传统的无线接入点(如路由器) - 工作在与普通 Wi-Fi 相同的 2.4GHz 或 5GHz 频段 1. **网络架构** - **组网模式**: 采用对等(P2P)网络架构 - **角色分配**: - 组主(Group Owner, GO): 类似微型接入点,负责管理网络 - 客户端: 连接到组主形成网络 - **组主职责**: - 信道选择与管理 - 网络参数配置(SSID、安全设置等) - 资源分配与网络维护 1. **连接建立流程** - 设备发现阶段: 1. 设备相互扫描并发现对方 2. 通过设备发现协议交换基本信息 - 组建立阶段: 1. 协商确定组主身份 2. 组主配置并广播网络 - 连接认证阶段: 1. 客户端扫描并识别组主网络 2. 通过 WPS 进行安全认证 3. 建立加密连接 1. **安全机制** - 采用 Wi-Fi Protected Setup(WPS)快速配置 - 支持多种连接方式: - 按钮配对 - PIN 码认证 - NFC 近场通信(如支持) - 使用 WPA2 等标准 Wi-Fi 安全协议加密通信 1. **服务层功能** - 服务发现协议: - 设备可广播自身提供的服务 - 其他设备可在连接前发现可用服务 - 支持的服务类型: - 文件传输 - 打印服务 - 多媒体流传输 - 屏幕镜像等 1. **数据传输特点** - 直接设备间传输,无需经过中间节点 - 支持高速数据传输 - 较低的通信延迟 - 不依赖互联网连接 AWDL AWDL(Apple Wireless Direct Link)与 Wi-Fi Direct 和蓝牙都是无线通信技术,用于设备之间的直接连接。主要用于 Apple 设备间的通信,支持如 AirDrop、AirPlay 等服务。AWDL 能在 Wi-Fi 频带上动态频道跳跃,优化连接质量和减少干扰,主要优化了点对点的数据传输速度和效率。 Apple 的跨设备数据同步功能,基本上都是使用的 AWDL。 相比蓝牙来说,Wifi-Direct 和 AWDL 的缺点就是建立连接慢(服务发现慢)。所以蓝牙更多的时候用来当作 AWDL 的前置条件,即通过 蓝牙 来发现设备建立连接,然后通过 AWDL 的进行数据传输。 Apple 提供了非常多的跨端通信功能如 Handoff、AirDrop、Universal Clipboard、Continuity Camera 等。苹果建议打开蓝牙,是为了更快的做服务发现和建立通道。但是蓝牙不打开,很多功能也都是可以正常使用的。因为还有很多备选的发现方案,如上面的 Bonjour 可以在局域网 wifi 下工作。甚至没有 wifi 的时候,也能使用 AWDL 自己的服务发现(速度慢)。 但是蓝牙,依旧是服务发现的第一优先级。 apple 服务列表 - from ai 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ### 1. 基于Wi-Fi的直接通信技术: - **AirDrop**: - 使用Wi-Fi和蓝牙结合的方式,进行设备发现和建立安全的点对点Wi-Fi网络以传输文件。 - **AirPlay**: - 通过Wi-Fi网络将音频、视频和图片流式传输到支持AirPlay的接收设备上。 ### 2. 基于云服务的同步技术: - **iCloud Drive**: - 利用云存储来同步和共享文件和文档。 - **Messages in iCloud**: - 通过iCloud同步所有设备上的iMessage,保证消息的一致性。 - **iCloud Photos**: - 照片和视频通过iCloud自动同步到所有设备。 ### 3. 基于Continuity的互操作功能: - **Handoff**: - 利用蓝牙和Wi-Fi来实现在设备之间无缝切换正在进行的活动(如邮件撰写、网页浏览)。 - **Universal Clipboard**: - 使用蓝牙和Wi-Fi通过Continuity功能实现剪贴板内容在设备间的共享。 - **Continuity Camera**、**Continuity Markup** 和 **Continuity Sketch**: - 这些服务通过Wi-Fi和蓝牙连接,允许用户使用一个设备上的功能来直接影响另一个设备上的内容。 ### 4. 扩展显示和图形共享技术: - **Sidecar**: - 将iPad作为外部显示器使用,通过Wi-Fi或有线连接实现与Mac的连接。 Handoff Apple 全家桶之间,可以通过 Handoff 进行操作转移。 通过蓝牙做设备发现,蓝牙不可用的时候切换到其他设备发现渠道 通过 AWDL 做数据传输 app 通过 Activity api 实现 handoff 功能 Universal Clipboard 和 Handoff 基本一致,通过 UIPasteboard api 实现跨设备 copy-paste 功能 NFC NFC(近场通信),和 以上 通信方式都不同。 NFC 的技术原理基于无线电频率识别(RFID)技术,使用磁场感应来实现在设备间的通信。NFC 设备在 13.56 MHz 频率上操作,通常用于非接触式数据传输,距离范围非常短,通常在几厘米内,传输速率慢,在 400kbps (50kb/s) 左右。 技术原理就是:识卡器发出信号(电磁感应),激活了终端(手机自动点亮),然后进行数据交互,并可能需要机主进行身份验证,最后完成信息的交互(支付、上公交车、开门等)。 NFC 有三种工作模式:点对点通信模式、读卡器模式、卡模拟模式。又分为【主动模式】和【被动模式】,其中一个设备提供射频场,另一个设备利用这个射频场进行通信。 使用 NFC,需要两个终端,一个做控制器,用于发射磁场来识别信息。一个做无电源的数据芯片,通过接收到的磁场来感应并传输信息。 对于 NFC 设备来说: 点对点通信:两个 NFC 设备相互交换信息。 读卡器模式:一个 NFC 作为控制器,读取其他 NFC 芯片中的信息。 卡模拟模式:一个 NFC 作为数据芯片,其他读卡器可以读取其中的信息。 现代电子产品中,Android 和 iPhone 都支持 NFC 技术,手机作为 NFC 设备,使用读卡器模式和卡模拟模式,已经可以完成很多事情。 当作为 NFC 控制器的时候,手机可以主动的读取外部 NFC 芯片中的信息,也可以将必要的信息写入到外部 NFC 芯片。(物流中,可以通过手机对商品挂载的 NFC 芯片进行记录) 当作为 NFC 芯片的时候,手机可以模拟一个 NFC 芯片,通过软件将信息提前写入手机中,其他读卡器就可以直接读取手机中的信息。(可以实现门禁卡等) Android 对 NFC 的 API 开放较多,app 可通过 api 来控制 NFC 进行 读取、写入、模拟 的操作,来实现快捷的智能家居、门禁卡等场景。 iPhone 上则比较保守,在【卡模拟】、【卡读取】方面,都有不少限制。 门禁卡 普通门禁卡: 门禁卡中有 【微芯片】(存储卡片的识别信息和其他数据)和 【天线】(用于接收和发送无线信号)。 门禁卡靠近门禁系统的读卡器 → 读卡器会发出一个射频信号 → 信号通过天线供电给门禁卡上的微芯片 → 微芯片被激活,通过天线将存储在芯片上的识别信息发送回读卡器 → 读卡器接收到信息后,将数据传输给后端的门禁控制系统。 NFC 门禁卡: 原理基本和普通门禁卡一样,不过,NFC 提供了更高的安全性。它支持双向通信,卡和识卡器之间可以通信。它们之间会进行密钥交换,通过对称、非对称加密来完成数据的安全传输。相比普通门禁卡,NFC 门禁卡会更加的安全。 NFC 是一种普适性的技术方案,手机也可以作为 NFC 终端。这里就可以把 NFC 门禁卡的信息保存在 手机中,使得手机可以充当 NFC 门禁卡的功能。 蓝牙: 有些 app 会通过 蓝牙的方式,和门锁连接。这在智能门锁中非常常见。因为距离很远,就可以连接上。而 NFC 需要非常短的距离 (4cm) 才能通信。 移动支付 通过 NFC 进行移动支付,主要有三种方案: 卡模拟方案。mobile 录入支付卡信息,被外部读取器识别 a. 系统级别的支持,如 apple pay。开发人员没有掌控能力。用户只能通过 apple wallet 录入银行卡信息,然后通过 apple pay 进行支付。 b. 应用级别的支持。Android 支持的较好,iPhone 限制很多。 iPhone 在 iOS 18.1 放开了该限制,app 可以将支付卡信息写入 app 中,支付的时候调用 app 完成支付。 不对普通开发者开放,需要和 apple 签订商务协议,支付费用,一般都是支付中间商如 Stripe。并且只对个别国家开放。 读卡器方案。mobile 作为读取器识别外部实体卡(信用卡等)。Android 支持的叫好,iPhone 限制很多。 a. Apple Tap to pay。商家可以在自己的手机中,打开 m app,然后用户把信用卡、iWatch 靠近手机,即可完成支付。 普通开发人员没有太多的控制能力。也需要签订商务协议,一般都是支付中间商如 Stripe。它们提供 SDK 并和 Apple Api 交互完成支付。 Apple 和 Stripe 中间商会对地区等有限制。只在少有的地区开放了 Tap To Pay 能力。 b. alipay 碰一碰。非常聪明的通过 NFC 实现支付的方案。 支付宝给商家提供 NFC 卡模拟芯片。用户侧的支付宝 app 充当读卡器,读取商家的 NFC 芯片信息。app 获取商家信息后,进行网络处理,完成支付。 以往的支付,读卡器处于商家侧,如 Tap to pay、POS 机等。只要商家具有稳定的网络,就可以完成支付。 alipay 碰一碰方案,读卡器处于用户侧 app 中。这就需要用户侧具有稳定的网络,以完成支付。 Apple iPhone NFC 简单介绍一下 iPhone 对 NFC 支持的历史: WWDC 2017:引入 Core NFC,并具备 NDEF 标签 读取 功能。 WWDC 2018:在较新设备上对 NDEF 消息进行后台标签读取。 WWDC 2019:重大扩展,允许 NDEF 写入,支持 ISO 7816、ISO 15693 和 MIFARE 标签,并支持自定义命令。 WWDC 2020:多标签检测,VAS 协议支持和 ISO 15693 标签的后台读取。 WWDC 2021-2023:专注于稳定性、性能提升和小幅增强,没有重大 API 更改。 WWDC 2024:支持 卡模拟。

2024/10/23
articleCard.readMore

分辨率 - 像素密度 - HiDPI

对于电子显示屏而言,第一要务是要显示信息。与信息显示直接相关的硬件,是光源。每一个光源称为【物理像素】。 光源 - 像素密度 - 物理像素 像素本身是没有大小和尺寸的。同一个显示屏,可以有 100 * 100 个像素(光源),也可以有 10000 * 10000 个像素。但是显示屏的大小并没有改变,改变的是 光源 的多少。如果显示屏有更多的 光源,那么就可以显示更加细腻的画面。这是【像素密度】。 对于硬件来说,提升【像素密度】,可以在单个面积上,展示更加丰富的色彩。这需要制作工艺的提升,iPhone 4 首次将其商业化,开发出了 Retina (HiDPI)屏幕。当时称为 2 倍屏,和 iPhone 3G 相比,屏幕的物理尺寸没有改变,但是像素密度提升了 4 倍,即 宽、高 的密度提升了 2 倍。 这是硬件制作工艺上的提升,但如何有效的使用塞了更多光源的屏幕,是软件来工作的。实际上,很多复杂的逻辑,都是软件来规定的。毕竟硬件只提供了【像素密度】更高、更多的【光源】,其他并没有做。比如软件开发中如何实现 0.5px 的宽高、如何调整设置系统的分辨率等等。 光源,也称为【物理像素】,是真实的提供发光和色彩的源头。 分辨率 - 虚拟像素 px 的定义 随着制作工艺的提升,光源 会趋向于越来越小。所以 光源 是没有尺寸概念的,但是开发人员需要有这个概念,不然怎么设置一个图片的大小或者一个矩形的宽高呢? 所有的软件都是在操作系统上运行的,而操作系统对像素进行了抽象,即【分辨率】。 对于同一台电脑,可以在系统设置中调整不同的分辨率,如 100 *100 或者 1000 * 1000。这些分辨率是【虚拟像素】,即把当前屏幕的宽度分割成 100 或者 1000 份,每一份就是 1px。 如果一台笔记本的屏幕光源(物理像素)是 2000 * 2000,系统设置分辨率为 2 * 2,即将整个屏幕的宽度和高度分割成 2 份。那么开发人员设置一个矩形的大小是 1 * 1,这个矩形就占据整个屏幕的 1/4,且使用了 500 * 500 个光源 px 是虚拟的 通过上面的定义,可以看出 px 是操作系统层面根据当前屏幕的大小分割成 n 份后虚拟出来的尺寸。同样的分辨率,如果屏幕大小不同,那么 1px 的大小也是不同的。 px 是和 光源 直接相关的,如果 px 是真实的,那么就和 光源 绑定。而我们可以设置 0.5px,但 1 个光源 是不可能只亮一半的。 根据上面的例子 (2000 * 2000 物理像素的例子),如果设置 0.5px,实际上是使用 250 * 250 个光源。 像素软件化 之前提到物理硬件上,只需要提高制作工艺来增加单位面积内的 光源 数量,即【物理像素密度】。除此之外,光源 出了提供不同色彩的亮度之外,并没有更多的变化性。 而软件,将对 光源 进行复杂的设定,来实现 高清晰度 等不同场景的使用效果。 准确来说,软件层面对 光源 最根本的对接是:对于需要展示的内容,如何更有效的使用 光源 来展示更加细腻、丰富的色彩。对接的不好,内容会有锯齿。对接的好,内容会很细腻。 单纯提高硬件制成 对于同一个尺寸大小的显示器,因为制成的提升,我们来看下渲染的变化,假设现在需要展示 100 * 100 大小的花朵: 原始:1000 * 1000 物理像素。设定屏幕分辨率(虚拟像素)为 500 * 500。一个虚拟像素需要 2 个物理像素。此时花朵的视觉效果是:宽度和高度都是屏幕的 1/5,占用 200 * 200 个光源。 制成提升:10000 * 10000 物理像素。 如果屏幕分辨率保持 500 * 500 不变,一个虚拟像素需要 20 个物理像素。此时花朵的视觉效果是:宽度和高度都是屏幕的 1/5,占用 2000 * 2000 个光源。此时,在花朵的边缘部分因为有更多的光源,会变得清晰(清晰是加强原始的表现。若花朵像素不佳,这里的清晰会变成锯齿)。 如果屏幕分辨率调整 1000 * 1000,一个虚拟像素需要 10 个物理像素。此时花朵的视觉效果是:宽度和高度都是屏幕的 1/10,占用 1000 * 1000 个光源。此时,花朵在屏幕上看起来非常小。无法正常浏览。 在 2 的基础上,把屏幕设置 scale,即 200% 放大。此时花朵的视觉效果是:宽度和高度都是屏幕的 1/5,回到了正常效果。占用 2000 * 2000 个光源。此时,和 1 情况基本没有变化。 通过上面的观察,可以发现单纯提高硬件制成,并不能有效的带来清晰度的提升。一来无法提高分辨率(导致无法使用),二来无法有效的提高清晰度(本来渲染内容的质量就不佳)。 此时,还会无效的增加 CPU、GPU 的算力。渲染成指数的增加,但没有实际的视觉产出。 这时候就需要软件的配合。在 iPhone 4 的 Retina 屏幕商用前,也有其他厂商提高了物理像素密度,但并没有 Retina 的效果。 HiDPI - Retina 我们把具有物理像素高密度的屏幕,称为 HiDPI 屏幕,苹果把它叫做 Retina 屏幕,一样的。 实际上,并不是说需要单独制作 HiDPI 屏幕,而是只要一个屏幕的像素密度达到了一个标准,那么它就是 HiDPI 屏幕了。HiDPI 并不是一个功能开关,而是对具有高像素密度的屏幕的总称。 前面说到提高硬件制成并不能有效的带来视觉效果的提升,即 软件 需要参与进来。如果软件不主动的使用高密度,那就像前面(屏幕分辨率保持 500 * 500 不变)的例子中说明的,并不会带来显著的变化。 软件提升视觉质量 使用 HiDPI,即创建了一个 N 倍虚拟分辨率来更加高效的使用高密度的光源。 假设有原始 10 * 10 图像,需要在 50 * 50 分辨率的显示屏上显示,显示屏的物理光源也是 50 * 50。 在这种情况下不开启 HiDPI,前面已经介绍过。下面说下开启 2 倍 HiDPI 的流程: 原始图像中的 10 * 10 像素区域首先被放大到 20 * 20 的渲染像素。软件层面会使用插值算法 (如双线性插值) 来创建新的像素,提供了更多的颜色过渡空间,使过渡更平滑。 然后这个 20 * 20 的渲染区域被映射到 50 * 50 的物理像素区域。 下面是对比效果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // raw px R G B W // 不使用 HiDPI 直接进行渲染 RRRRRGGGGG RRRRRGGGGG RRRRRGGGGG RRRRRGGGGG RRRRRGGGGG BBBBBWWWWW BBBBBWWWWW BBBBBWWWWW BBBBBWWWWW BBBBBWWWWW // 使用 HiDPI 直接进行渲染 // > 使用 2 倍 HiDPI 软件层面放大 R R' G' G R' R'' G'' G' B' B'' W'' W' B B' W' W // > 将 2 倍虚拟分辨率通过光源进行渲染 RRRRRGGGGG RRRRRGGGGG RR''GGGGGG R''GGGGGG' R'GGGGG''W BBBBWWWWWW BBBBWWWWWW BB''WWWWWW B''WWWWWW' BBWWWWWW'W 在这个例子中: 原始的单一颜色区域 (如纯红色) 仍然保持清晰。 颜色过渡区域 (如红色到绿色的边界) 有更多的中间色,使得过渡更加平滑。 边缘和细节更加平滑,减少了锯齿感。 这种渲染方法的优势: 更多的渲染像素允许更精细的颜色渐变。 插值算法可以创建原始图像中不存在的中间色,使边缘更平滑。 当这些渲染像素映射到物理像素时,即使不是完全一一对应,也能保留更多细节。 480p 对于一个 480p 的视频,它的具体尺寸是多少?没有具体的尺寸,多大多小都可以。 480p 表示 640 * 480 px,即宽 640 个像素,高 480 个像素。每个像素存储的是 RGB 色彩信息,是数字信息。 480p 表示共计 640 * 480 个色彩点位的数字信息。具体到渲染的时候,可以放大缩小,放的太大了,就会模糊了。因为当使用 3000 个光源来表达 640 个色彩点,肯定有很多地方是填充补位的,那些光源并没有实际表达视频色彩,只是填充了过度色彩(甚至没有过度,锯齿会更加厉害)。 High Dynamic Range - from ai HDR(高动态范围)技术允许显示器在更宽的亮度范围内展示图像,能够同时提供更深的黑色和更亮的白色。这种技术增强了图像的总体对比度,使得色彩更加丰富和深邃。HDR 显示器可以更精确地展示从暗部到亮部的细节,这在标准动态范围(SDR)显示器中往往无法实现。 Extreme Dynamic Range (XDR) XDR(极致动态范围)是 Apple 公司对 HDR 技术的一种营销称呼,用于其某些显示产品。XDR 旨在提供比传统 HDR 更高的亮度和对比度水平,以达到更加出色的视觉效果。这种技术特别适合专业视频编辑和高端图像处理,其中对色彩准确性和细节展示的需求极高。

2024/10/22
articleCard.readMore

Mac 字体

很久之前,写过一篇关于 【计算机字符编码与内存编码 - Unicode】 的快照,根据 码位、码表 对字符进行了介绍。这里特别说明一下 Mac 系统上的字体库。 系统自带软件:Font book 字体文件夹: /System/Library/Fonts 、/Library/Fonts、~/Library/Fonts 苹果提供的字体:Applexxx 、Apple xxx 、SFxxx 、PingFangxxx 如何使用字体 操作系统根据语言的不同,会使用默认字体。比如中文系统,会使用 PingFang 字体。英文系统,会使用 SF 字体。 app、dmg 等软件,可以直接使用 defaultfont:xxx 的形式直接使用系统字体,或者使用 fontname:xxx 的形式自行选择字体。自行选择的时候,可以使用系统提供的字体,也可以将 xx 字体打入 app 中来使用。 app 提供修改字体的功能,用户可以自行选择需要的字体。 图标 Apple 平台提供了两个图标字体,分别是:Apple Color Emoji 和 Apple Symbols 。 字体库能够包含图标、颜色,在之前阐述 Unicode 的时候已经说明过,因为它们都依靠码位 进行检索。所以在不同的平台上,都会有自己的图标库的系统级别实现,显示效果会不一样。 字体回退 每个字体可以适配多种语言,但没有一个字体是全能的。即【一定需要字体回退】来对当前字体无法渲染的文字进行兜底。 在 mac font app 中,打开一个 font,会列出其 support 的文字集合。 部分 app 支持设置 1 个字体:回退到系统字体。 部分 app 支持设置 n 个字体,按照优先级进行回退,最后回退到系统字体。如 browser 等 css:通过设置 font-family 属性来实现字体回退。例如:font-family: "MyFont", "FallbackFont", sans-serif; Nerd Font nerd font 是一个项目集合,它将非常多的字体进行扩展,增加了许多 icon,从而让已经非常优秀的字体扩展了更多的功能。 可以通过 brew 安装 nerd 字体。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 > brew search nerd-font font-0xproto-nerd-font font-iosevka-nerd-font font-3270-nerd-font font-iosevka-term-nerd-font font-agave-nerd-font font-iosevka-term-slab-nerd-font font-anonymice-nerd-font font-jetbrains-mono-nerd-font ✔ font-arimo-nerd-font font-lekton-nerd-font font-aurulent-sans-mono-nerd-font font-liberation-nerd-font font-bigblue-terminal-nerd-font font-lilex-nerd-font font-bitstream-vera-sans-mono-nerd-font font-m+-nerd-font font-blex-mono-nerd-font font-martian-mono-nerd-font font-caskaydia-cove-nerd-font font-meslo-lg-nerd-font font-caskaydia-mono-nerd-font font-monaspace-nerd-font font-code-new-roman-nerd-font font-monocraft-nerd-font font-comic-shanns-mono-nerd-font font-monofur-nerd-font font-commit-mono-nerd-font font-monoid-nerd-font font-cousine-nerd-font font-mononoki-nerd-font font-d2coding-nerd-font font-noto-nerd-font font-daddy-time-mono-nerd-font font-open-dyslexic-nerd-font font-dejavu-sans-mono-nerd-font font-overpass-nerd-font font-droid-sans-mono-nerd-font ✔ font-profont-nerd-font font-envy-code-r-nerd-font font-proggy-clean-tt-nerd-font font-fantasque-sans-mono-nerd-font font-recursive-mono-nerd-font font-fira-code-nerd-font ✔ font-roboto-mono-nerd-font font-fira-mono-nerd-font font-sauce-code-pro-nerd-font font-geist-mono-nerd-font font-shure-tech-mono-nerd-font font-go-mono-nerd-font font-space-mono-nerd-font font-gohufont-nerd-font font-symbols-only-nerd-font font-hack-nerd-font ✔ font-terminess-ttf-nerd-font font-hasklug-nerd-font font-tinos-nerd-font font-heavy-data-nerd-font font-ubuntu-mono-nerd-font font-hurmit-nerd-font font-ubuntu-nerd-font font-im-writing-nerd-font font-ubuntu-sans-nerd-font font-inconsolata-go-nerd-font font-victor-mono-nerd-font font-inconsolata-lgc-nerd-font font-zed-mono-nerd-font font-inconsolata-nerd-font netron ✔ font-intone-mono-nerd-font 通过 brew install --cask font-hack-nerd-font 安装的字体,会被安装到 ~/Library/Fonts 文件夹中。 等宽字体 有 Fira Code、hack 等,在英文场景非常舒服,IDE 场景经常使用。 zsh zsh 有 powerlevel10k 主题。该主题在字体方面比较丰富,主要是采用了很多字体 icon。有些字体如果提供不了 所需 的 icon,显示就会异常。 但是 powerlevel10k 本身仅仅是配置文件,它不提供字体的安装。所以用户需要自行安装所需要的字体。它和 nerd font 配合比较友好,只要是 nerd font 项目中的字体,都可以被 powerlevel10k 很好的使用。 操作流程: 通过 brew 安装 nerd 项目中的字体,如 hack。 ITerm、Warp 中选择 nerd 字体。 浏览器切换字体 安装 hack 等字体后,很多天天见面的 IDE 或者 app,就可以切换喜欢的字体了。 其中,浏览器可以设置全局的字体切换,这对爱好某一个字体的同学来说,将非常友好。 全局 css 内容推荐如下,后面配置的时候会用到: 1 2 3 4 5 6 7 span:not(.material-symbols-outlined, .material-icons, .google-material-icons, .fa, .fas, .far, .fal, .fab, .fad, [class*="icon"], svg, [class*="DP"], [class*="hd"]), i:not(.material-symbols-outlined, .material-icons, .google-material-icons, .fa, .fas, .far, .fal, .fab, .fad, [class*="icon"], svg, [class*="DP"], [class*="hd"]), body, li, p, div, h1, h2, h3, h4, h5, h6, a, ul, ol, dl, dt, dd, button, input, textarea, select, option, optgroup, label, pre, code, kbd, samp, var, table, th, td, tr, thead, tbody, tfoot, caption, blockquote, cite, q, strong, em, b, small, sub, sup, mark, del, ins, abbr, acronym, address, time, form, fieldset, legend, nav, header, footer, section, article, aside, main, details, summary { font-family: "FiraCode Nerd Font Mono", sans-serif !important; -webkit-font-smoothing: antialiased !important; } Tips:最好不要使用 body, * {} 以及 !important,这样会影响到一些网站的显示效果。因为有些字体是图标字体,会紊乱变形。 safari safari 浏览器提供了全局样式表:【setting - advanced - style sheet】,这里可以在选择本地一个 xx.css 文件,来设置全局的样式。 文件内容就是上面提供的示例 css。 chrome 家族 chrome、arc 等,都可以通过插件 Stylus 来设置全局样式表,这个插件非常棒,可以定制很多网站的样式,有很多网友制作的样式,可以直接使用。 这里,我们可以通过该插件设定一个全局样式表,内容就是上面提供的示例 css。

2024/10/22
articleCard.readMore

ESP32

嵌入式 特定功能:嵌入式是为了完成特定的物理功能,如温度捕捉、机器人。 特定组合:通过小巧的不可修改的硬件、软件,协同后直接发布。后期,不太好修改硬件或者软件(可以修改,但一般不会主动修改)。 一次成型:很难进行二次改造。因为组合比较特定,硬件和软件之间的 api 都是私有的,无法迁移。 ESP32 它本身是芯片,台积电加工,40 纳米制程 (中国应该也能造了)。 官方提供了主板,即芯片 + 板 + 辅助硬件,产品名如 ESP32-devkit-6 等。 开发人员可以在主板上进行开发工作。 开发流程: 部署环境。官方已经支持 mac、linux、window 下的 IDE 开发。 SDK。官方提供完善的 SDK 和 Api。Api 可以控制 LED、摄像头等大量硬件。 开发 & 测试。通过 IDE 编写完代码,可将生成的 二进制 bin 文件上传到 ESP32 中,并完成运行。 硬件输出:如果有 LED 等,代码中可以控制针脚,完成高低电平的设定,可以看到效果。 软件输出:可以在 IDE 中看到 ESP32 print 的 值。 原理: ESP32 是完全的芯片,无 OS,所以只能无脑的执行指令。因配置较弱,只能执行编译好的非常小巧的二进制 bin 文件。 在启动后,主动执行 boot 引导的 bin 文件。 没有很好的内存管理,必要的使用,需要自己进行内存控制。 周边: 通过 ESPHome,可以不用写代码,仅仅使用别人写好的软件,自己增加一些 yaml 配置,即可编译一个完善的 bin 出来。可以控制家里的 小米扫地机 等。 局限性: 一个 ESP32 或者 主板,只能支持一个 bin 文件的执行。启动后,默认加载指定位置的 bin 文件,且只有 1 个。(相当于 pc,启动后 boot 引导操作系统。这里引导烧录的 bin 文件) 树莓派 性能强劲的计算机,但尺寸小、耗电低,适用于复杂一些的嵌入式场景。

2024/10/22
articleCard.readMore

trump - 川普

川普 于 美东时间 2024 年 7 月 13 日 18 时 11 分 在 宾夕法尼亚州 竞选集会 上 遭遇枪击。 真没想到,美国的政治已经到了这一步。上一次总统暗杀还是肯尼迪,那已经是 60 年前的事情了。 从视频上看,川普 被击中和不被击中,概率上没有大的偏差,即 50%。结局是:贯穿了耳朵,没击中大脑。 上一次川普参加竞选,是和希拉里竞争。那时候希拉里明显有票数优势,阿桑奇 泄漏了 邮件门,让希拉里很多丑闻曝光,使得川普反败为胜。 这一次,在很多原因如疫情、战争的作用下,世界范围都很不稳定,乱世换人本来有优势。 但这次的刺客,一弹定江山。没杀死川普,那就得保送了。 川普上一次在任期间,对华政策很严厉。尤其增加的进口税,拜登在任期间也没有去除。 那一次,川普是作为一个商人的影子上台,权利分散、命不下达。甚至于内斗不断,能干事的时间并不多,落地的也少。 一转眼 4 年已经过去了,这四年世界范围内日子都不好过,但看起来国内更不好过。而川普的势力,不能同日而语了。 后面几年中国咋办哦,一届可就是 4 年啊。现在是拔出了刀互看局势,但真要针锋对麦芒的话,还是那句话:在绝对的实力面前,一切都是虚的。 同一个场景大概率不会有两次好运。川普这次不弄出来大动静,是不可能的。

2024/7/14
articleCard.readMore

币圈 - 虚拟货币

赵长鹏先生在美国认罪后,虚拟货币的江湖地位就算是稳定了。 虚拟货币一直游离在法律的边缘,中国早就封杀了。而今最大的交易平台老板因 “大量非法货币流通” 在美国认罪,让很多人觉得未来迷茫。 我倒是觉得不用担心了。这个圈子乱到没有点资产都不敢进去,但后面可能会越发趋于平稳。 因为至少在美国看来,“币安” 已经合法了,可以被美联储约束了。而不久的将来,中国一定会开放,中国从不会放过割韭菜的好工具。 两个月前,在国内的网约车上,司机小哥问我懂不懂虚拟货币。我说我不太懂,但理解起来应该比较快,希望他多讲讲。 他说他也不懂,就是前两天拉了一个客人。那位客人很客气,在车里长时间打电话,内容是盗了很多币,在和同伙计划跑路的细节。 小哥说,那位客人很随和,但是在电话里,他一直在嘲笑被盗的人一早起来发现巨额货币丢失后的绝望。 小哥说他不懂,看我戴眼镜,想问问我懂不懂,问我警察会不会去抓捕。 我说也不懂。只听说在币圈玩的,都挺野的。 比特币刚出来的时候,我刚参加工作,那时候听到了挖矿。等再听到巨大消息的时候,比特币已经 2w 美元了,现在已经 6w 了。 后面只听说不能玩,风险大,也就避而远之。 最近实在是有些穷疯了,就想看看币圈到底是什么,哪来的那么多种虚拟币,都还能兑换美金。 能兑换美金又不是法定货币,没有法律保护又能交易和购买实物。 还有很很多多那些 “币安、以太坊、区块链、去中心、web3” 等等名词,我想去弄明白到底咋回事。 下面内容就比较散乱了,主要是资料查询过程中的一些备忘。还是先说几个结论吧,tldr。 整个技术层面,没感觉到特色。 一切的基石 “区块链” 是单向链表,最简单的数据结构。里面的各种加密,也都是已经成熟的加密方案。 “去中心化”,就是非常成熟的 p2p。 在基石之上搭建起来的 “智能合约”,就是 faas 服务。 从技术层面看,太弱了。所有参与币圈的,都是在豪赌,赌一个新瓶装旧酒能不能讲更多的故事。 所有的上层建设如币安、NFT 等,都基于最基础的 “区块链”,即相信它的 “不可篡改”。 然后加了两大护法,分别是基于 “p2p” 的 “去中心化” 和基于 “faas” 的 “智能合约”。这两个护法,是用来增加信心的。 一切的一切,都在于 信任 并 遵守 “区块链” 链表中保存的数据。这里的信任,是实打实的,就像白纸黑纸一样。但是遵守,一直都是空头支票。 因为 遵守 的空缺,整个币圈都是在豪赌,赌一个一落千丈的时间点,或者产业被法律认可。 转机是赵长鹏先生和币安,向美国认罪。遵守 已经有政府背书了(但 “去中心化” 的安全性就名存实亡了)。已经成型的产业大概率不会没落,这里将变成投资机构。但 DAO、web3 是走不下去的。 建议 USDT,和美元 1:1,可以让资产不贬值。说千道万,就是信不信得过。基于 10,我觉得以后可以简单的信一点。 整个产业,都在等待法律背书。可现实世界本就有法律、律师、法院了。这是一个矛盾的产业。 区块链 区块链是一种数据结构和算法集合。通过这套结构,可以保障以下特性: 不可篡改 高度安全 透明 区块链就是链表,通过加签增加了修改的复杂度。区块链就是让人相信:你看到的数据一定是真实的。 因为以上特性,可以基于区块链,做一些事情。只要产品的形态能够使用区块链的数据结构和算法来表达,就可以使用区块链技术。 比如,药品跟踪:有问题的药品有可能无法溯源。如果基于区块链系统,就可以做到无法篡改、透明。每一支药,从研发、出厂、销售、使用都可以被记录。 区块链中,对于每一个区块的产生,有一个方案是使用 PoW(工作量证明)。 这个方案需要使用巨大的算力才能够输出一个有效的区块,从而可以保障不可篡改。 投入到 PoW 矿业的大量计算资源和电力,如果用于科学研究、数据分析、医疗研究等其他领域,可能会带来更直接的人类福祉增益。 but,目前需要把这些巨大的资源用来计算一个区块。这个投入和产出的资源配比,的确很不好理解,也只能玩币的人能够下狠心。 比特币 比特币没有其他应用如 NFT 玩的花,比特币是完全遵守 区块链 的技术规则来实现的。 通过 https://bitinfocharts.com/bitcoin/ 可以查看当前比特币的一些信息,如区块链总区块数、大小、生成时间等。 区块 每个区块,都有一个 nonce(随机数),这个 nonce 可以保障区块被挖出来的时间在 10 分钟左右。通过定期(两周)可能性的调整 nonce 值来保障。 通过 nonce 计算出一个符合当前区块定义的值,就是说这个区块被挖掘出来了。这就是挖矿。挖矿就是挖每一个区块。 区块中记录 1000 - 2000 笔交易,其中第一行记录,就是当前挖矿的收益交易。 每个区块大约 2-4 M 左右,大约每 10 分钟产生一个区块。 交易 交易中包含交易 ID、input、output。input 就是来源账户,可能有多个账户。output 就是流向账户,可能有多个账户。 input 中包含解锁脚本,用来解锁来源账户在上一个记录中的锁定脚本。output 中包含锁定脚本,用来供下一次交易的时候给 input 解锁脚本使用。 原理:把解锁脚本和锁定脚本合起来,通过比特币脚本运行器执行,能执行成功,则表示当前来源账号是有效的。即 A 用户可以消费 A 用户自己的钱,不能被 B 用户消费。 示例:A 持有 5 个比特币并支付给 B 2 个比特币: input:A output:A (3) + B (2) ⇒ 实际上:A (2.99) + B (1.99),0.02 是手续费。 对于上面的示例,实际上还有手续费,这些手续费是给矿工的。即矿工挖出来一个区块后,不仅得到了比特币奖励,还拿到了每一个交易的交易费用。 比特币地址 私钥生成公钥,公钥生成比特币地址。 比特币地址是公开的,即别人拿到该地址,就可以往这个地址打币。别人也可以检索区块链中的区块,查到该地址的资金来源和流向。 1 个用户可以持有 n 个比特币地址用于交易,这样可以保护隐私。也代表需要 n 个公私钥。 钱包的作用,就是管理公私钥和比特币地址。 钱包也可以通过 1 个私钥管理 1 个公钥和 n 个比特币地址。这样更方便一些,比较保存 1 个私钥和保存 n 个私钥的复杂度是不一样的。这个技术叫:HD。 将所有的比特币地址的 UTXO (剩余币) 总和计算出来,就是当前用户持有的币总量。 交易池 比特币网络中一个临时存储的区域,用来保存尚未被包括在区块中的有效交易。 A 向 B 支付币,这笔交易,首先进入交易池。其中记录了详细的且验证有效的交易信息以及费用(交易费,交易发起人可以指定奖励矿工多少费用)。 交易池在哪里:存储在 “全节点” 中。详见下面的【比特币链存储空间】。 交易流程 我 (A) 现在通过钱包向 B 支付了 0.5 比特币: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 当你通过钱包向另一个用户(B)支付比特币时,整个过程涉及几个关键步骤,从创建交易到最终确认在区块链上的记录。以下是支付0.5比特币的技术流程: 1. 创建交易 - 输入信息:钱包首先检查你的比特币地址中有足够的余额可用来发送0.5比特币给B。这通常涉及到选择足够的输入(之前的交易输出),来覆盖发送金额和可能的交易费用。 - 输出信息:在交易中创建两个输出: - 第一个输出是支付给B的0.5比特币。 - 如果选定的输入超过了0.5比特币加上交易费,第二个输出将是找零金额,返回到你自己的一个地址上。 2. 签名交易 - 你的钱包使用你的私钥对交易进行数字签名。这个签名过程是为了证明你有权使用这些输入,并授权这次交易。 3. 广播交易 - 签名的交易被发送到比特币网络。钱包软件将交易数据广播给网络中的节点(比如全节点)。这些节点接收到交易后,会进行初步验证,比如检查签名是否有效,输入是否未被其他交易双重支付。 4. 交易验证与确认 - 一旦交易被网络节点接受,它会被放入交易池中,并等待矿工将其打包入区块。 - 矿工选择交易池中的交易,并尝试创建新的区块。矿工在创建新区块的过程中需要解决一个数学难题,这个过程称为工作量证明(Proof of Work)。 - 当一个矿工成功解决难题并创建了一个新区块,包含你的交易在内的区块被添加到区块链上。这个区块随后被广播给所有的节点。 5. 确认数增加 - 一旦你的交易被包含在一个区块中,它获得了第一次确认。随着更多区块被添加到这个区块之后,你的交易的确认数会逐渐增加。一般来说,六个确认后,交易被认为是非常安全的。 这个流程确保了比特币交易的安全性和不可逆性。通过网络分布式的节点和矿工的共同工作,比特币网络维护了其透明度和去中心化的特性。 比特币链存储空间 目前 (2024.07.06),比特币链共计 850,932 个区块,每个区块生成时间 11m 10s,每个区块 800.35 KBytes,整个区块链 491.50 GB,第一个区块生成时间是 2009-01-09。 对于普通用户,不可能存储近 500G 的链在本地。但比特币本身又是去中心化的。原理是: 区块链存储有两种方式: 全节点:存储整个区块链数据,并实时新增。 轻节点:只保存区块头部的信息。来验证交易数据的正确性。比如手机钱包。 钱包是公司、个人、开源组织开发的,钱包虽然在手机、电脑上安装,但还是要访问对应的后台服务器。 后台服务器一般都是全节点存储区块链数据。 全节点 全节点,是软件,可以由大机构和个人或者开源组织发布。 当一个用户安装了全节点之后,就会拉取所有区块链数据并成为同步链中的一部分,包括新增区块、交易池等。 而且,安装了节点软件之后,基于节点发现算法,当前节点会被其他节点感知到。 这样,A 向 B 的交易转账,就会被同步到当前节点的交易池中,并在后期可能会被矿工拉取过去,成为区块计算的一部分。区块只要计算成功,就代表当前 A 向 B 的交易完成了。 以太坊 认识以太坊之前,需要先理解比特币。因为以太坊有很多和比特币相似的概念。但他们是为了不同的场景。 概念 区块链:和比特币的区块链概念相同。因为它们都是基于区块链自身的那些特性发展起来的,如 “不可篡改”。 以太币:是以太坊的核心之一。以太币和比特币在概念层级上等价。 智能合约:是以太坊的另一个核心(总共两个核心基础)。以太坊的区块链上不仅可以记录以太币,还可以记录智能合约。 区块链中的每一条记录都是交易,交易中可以仅有以太币的交易,还包含一个字段用来存储合约。 非智能合约的交易,该字段为空。 虚拟机 (Ethereum Virtual Machine, EVM):用来执行智能合约。 有一套计费标准。即合约的执行是需要消耗以太币的。 全局状态:是区块链的抽象层。 以太坊区块链 和比特币基本相同,有些许差异,如区块挖矿的标准不同、同一时间同样两个区块被挖出来后的处理结果不同(比特币:使用其中一个,以太坊:两个都使用,最多 7 层)。 还有就是,存储的数据不同。比特币主要是存储每一笔交易,其中第一个交易项是当前矿工的比特币奖励。以太坊存储的也差不多,不过里面多了一个字段,叫 “智能合约”,即代码。这个代码可以是通过 class 和 func 的形式定义,可以在后期被以太坊专门设计的 EVM 执行。 全局状态 比特币中,需要通过全节点,访问每一个区块才能知道某一个比特币地址的消费记录和余额。在以太坊中,查询数据不再需要遍历所有的区块了,因为有一个抽象层,记录了所有以太坊账号的最终结果,就是全局状态。 全局状态可以理解为是一个非常高效的多叉树,它记录了全量的最终数据,通过它,就可以快速获取某个账号的余额和智能合约。 在每个区块被挖出来后,就会更新这个全局状态。 所以,全局状态和区块链一样,是独立存储的。而且,它和区块链一起,被 全节点 持有。它们都是去中心化的。 实际上,全节点还有一个数据,和比特币一样,就是 “交易池”。所有待录入区块的交易,都会先存储与 “交易池” 中,等待被矿工验证和挖矿成功。 智能合约 智能合约,就是代码。一段通过 class 和 func 组成的图灵完备的 脚本,需要在 EVM 上部署。 它有一个研发生态,可以通过众多后台语言和前端语言进行调用和执行(执行需付费 - 以太币)。 这些代码存储在区块链中,保障不可更改等特性。 举个例子,背景是这样:A 将一个项目给 B 做,约定 B 完成 30%、70%、100% 的时候分别收到 3w、5w、2w 的以太币收款。 A 或者 B 或者 第三方,写智能合约 (写代码),这个合约里面包含一系列的约定函数,如 stageA (…)、stageB (…)、stageC (…),这些函数中需要包含:验收、打款等细节操作。 创建一条交易,交易中包含 A、B、合约。 部署合约(需要部署在以太坊平台上)。 开发前端页面,比如有三个按钮 stage_a、stage_b、stage_c。每个按钮都执行对应的验收逻辑,并最终调用 合约 接口【stageA (xxx)】。 当 B 完成 30% 后,需要 A 验收并且通过,那么 A 就点击 stage_a 按钮,这个使用 stageA 合约 api 被执行,执行结果是将 A 账户的 3w 以太坊币打到 B 账户。 说明: 可以认为这样的合约没有用,比如 B 完成 30% 了,A 也验收通过了,但就是不去点 stage_a 按钮。这样的话,当前合约形同虚设。 但我们不能说这样的合约不存在,因为在区块链上,这是真实记录的,即 A 和 B 之间一定有这个合约。 但现在 A 就是不付款,这没有办法。即 智能合约 能够提供约定的准确性,但没有办法保障约定一定被执行。这是君子法则。 那智能合约和普通的合同相比,就显得操作流程更加复杂但价值也没有提升? 不。 普通的合同有可能丢失或者认为破坏了,但智能合约可以保障:它就在这里,当时什么样,现在也一定什么样。 还是得依靠【君子法则】。 代币 代币,是完全基于 “智能合约” 建立起来的。通过 faas 平台,来做各种类型的计算,从而实现业务逻辑,如 “币名称、发行币、交易”,就是调用 faas api。 ERC-20(通用代币标准) USDT(泰达币) 是一个使用 ERC-20 标准的代币示例。它是一种稳定币,通常与美元的价值挂钩,这意味着每一个 USDT 通常值 1 美元。USDT 在加密货币交易中非常流行,因为它提供了一种相对稳定的资产存储方式,使交易者可以在不直接兑换成法币的情况下减少价格波动的风险。 ERC-721(非同质化代币标准) NFT,常用于数字收藏品和艺术品。 DAO - 组织自治 也是基于智能合约。想不通,怎么就能往这方面硬挤(DAO 是不可能的)。 假设公司决定开发一个新的产品。你可以在 DAO 中启动一个提案,所有员工都可以投票决定是否启动这个新项目。如果多数人同意,智能合约将自动从公司的资金中划拨预算给这个项目。同样地,项目的进展更新和最终的成果也需要报告给 DAO,以便员工可以继续监督和投票决定后续的资金和资源分配。 提高透明度:由于所有的决策和交易都记录在区块链上,员工可以清楚地看到公司的每一笔支出和每一个决策。 增强员工参与感:员工直接参与到公司决策中,可以提高他们的归属感和积极性。 减少管理层:自动执行的智能合约减少了管理层的需要,降低了管理成本。 和 HTTPS 做对比 我们都知道 http 是不安全的,但是我们都相信 https 是安全的。 但 https 其实也不安全,为什么我们还相信它? 为什么不安全: 通过中间人 MITM,依旧可以解密本已经加密的数据并明文查看。 有 MITM 的存在,C 和 S 通过 https 传输的数据,依旧会被部分人看到。有两种场景: C 主动信任了 中间人 证书,主动查看数据。如开发人员。 C 上当了,信任了黑客的证书。 基于以上两个场景,S 就没有办法信任 https 了,因为 S 给到 C 的数据可以被明文分析了。 方案一:将数据再次加密,使得中间人拿到加密后的数据。这里只能使用对称加密了,对称密钥也可能被从内存中挖取。本质上只是增加了破解的成本。 方案二:进一步做双向加密认证,使得中间人 MITM 失效。进一步的安全处理上会使用该方案。 所以,对于 https 而言,C 和 S 之间的数据依旧有可能被窃听。如何严格按照安全标准,https 也是不安全的。因为安全本身的定义,就是为了处理窃听风险。 为什么我们还是相信 https 协议: 我们认为 https 已经能够覆盖大部分安全场景了。 默认对开发人员和上当信任了黑客证书的人,不在关注了。 开发人员:看就看吧。 上当人员:上当就上当了吧,黑客你就看数据吧,防不了你。 https 的安全,是基于 TLS/SSL 实现的。https 是应用层,使用了 TLS/SSL 这套安全套件。 https 调用 tls api,tls 完全安全验证后对数据加密,在调用 socket api 将数据给到 tcp 进行传输。 tls 完成安全校验的前提是 tcp 安全通道的建立。 区块链 和 https 有很多相似的地方:区块链和 tls 处于同一个层级,而 比特币和以太坊 就如同 https 应用层协议。即 https 使用 tls,以太坊 基于 区块链 技术 。 区块链用来保障一个很重要的点:不可伪造。它保障了不可伪造,但上层应用基于这个不可伪造的特性,并不能 100% 的完成上层业务。 A 通过 区块链 记录了一条向 B 借款 1w 的记录。 借款到期后,A 就是不还。A 和 B 以及所有人都知道 A 向 B 借了钱,但 A 就是不还。 通过区块链做了一个 NFT 数字画 J 并卖给了用户 M,用户 M 具有 J 的唯一权。但用户 N 依旧将数字画 J 印在广告上做产品宣传。 用户 N 就是盗版、侵权了,但法律的不完善可能无法制约这种行为。 为什么还相信区块链: 我们默认相信 https 是安全的。 我们以君子的视角来看待区块链本身。 所有的一切,都是以君子视角来看待。 币圈一骑红尘,智能合约改变世界,之前得依靠 “君子协议”。显然整个币圈,没有君子。 往后,还是得依靠法律,成为协议的保护神。 那有法律背书后,还要这些干什么呢?现在不就是有法律、律师、法院么。 只能说法律愿意来背书,让这个行业不灭,看看后面能不能有新的价值。

2024/7/10
articleCard.readMore

【旅游】富士山

忘记更新了,后面补上照片

2024/4/29
articleCard.readMore

【旅游】sakura(樱花)

忘记更新了,后面补上照片

2024/4/29
articleCard.readMore

【旅游】千岛湖

对浙江杭州的千岛湖进行了两日游,简单分享下攻略。 如果对鱼头和坐船有感情,可以体验下。 如果想看好风景,以及看群山,请避坑。建议去威海。 有朋友之前推荐过,说 “好玩”!网上查了攻略,都很棒。千岛湖的名声弄的很好。 实地去玩了两天,失落比较明显。因赞美不多,就列一些事项吧。 建议: 建议淡季前往,人少旅途顺畅。主要是如果旺季去,投入成本剧增,此时与预期可能落差更大,愉快的旅游变得不开心。 有两个湖区,去一个就行了!带小孩建议去 “东南湖区”,不带小孩建议去 “中心湖区”。 坐船,肯定要升仓的 (33 / 人),不然连坐船的乐趣也没有了。一家三口玩这个湖区,花费约 ¥550。亏到心疼。 若去 “东南湖区”(我们去的这个),一定选择 10 点之前的开船,后面船就不过猴子园了。猴子园值得一看,赢在数量和活动范围,很小的猴子爬树的时候太可爱了。(山顶是买票看表演的,不用看!) 第二站是天池,啥玩意。导游会建议做电车爬坡,我们带孩子走路 25 分钟就走完了。这一站血亏。 第三站是黄山尖。我想着怎么也能看看树、摸一摸水、在湖边坐一会。反正带孩子 20 分钟上山,15 分钟下山,山顶不想待,风景没什么好看的。这一站血亏。 电车、缆车都不要坐 (电车 15 / 人,缆车 50 / 人)。整体来说,3 个人近 600 元,看了猴子,坐着船吹了湖风,浪费了 8H (甚至一天)。 骑龙巷,不要去,地点小,东西贵。这一站血亏,浪费了 2.5H。 月光之恋,不要去,死贵没意思。 天屿,不要去,没有太贵没意思。 上面就差不多是有名的景点了,基本都要落坑。门票贵,景点差,都是血亏。 有人说可以多住几天,环湖骑行。ennn,那得搭进去更多的时间和金钱。这湖是大,骑行的话,背景差不多也就是千篇一律。 有个比较大的花销是房租,这也是建议淡季来的原因。旺季只能住很偏的地方,体感将会差上加差。 我们选的是 “开元颐居湖水房”,挨着湖建的酒店,窗户面朝整个千岛湖。风景是挺不错的,淡季一晚 ¥450。 宣传是 “五星级 / 豪华酒店”,ennn,也还好吧,肯定是没达到预期。 来千岛湖鱼头少不了。 在天屿那边吃了一个 “山水丰味” 家的鱼头,挺好的。本地人是推荐不要景点吃,谁都知道那租金不宰客没法活。 这家的鱼头实不相瞒,是这么多年吃的最好的,点的东西不多,¥310。说到价格,也感觉小亏,值不了这个价。 整体而言,名不符实,差评。 千岛湖的湖底,是一座城 (村镇)。 当年开国后要建堤坝,一声令下,千岛湖的村民都搬走了。从浙江迁徙到了安徽等周边省地。 现在千岛湖发展旅游,政府收钱卖地盖高楼,也不知道那些迁徙的原住民若还在世的话,有没有骂。

2024/2/26
articleCard.readMore

我的 2023

怀着沉重的心情,还是开始落笔写年终了,相比很多人迟了 2 个月。 这一年发生了不少事,感觉清晰可见,又像梦一样不知哪里谈起。离职、旅游、待业、润,每一步都在计划之内逐步落地,但每一步都如履薄冰、心有恐惧。 人生中已经有很多重大变动,中考、高考、结婚生子、择业、多次工作变更等,现在回头看每一次都是巨大的转折。但当初处于那些时刻的时候,并没感觉到刻骨铭心。 而现在,年过三十、拖家带小的我,依旧在努力的尝试着重大的拐点突破,但底气不是很足。 记得中考时候很清楚历史和政治两门考的非常糟糕 (加起来都没到 100 分),很清楚弱小的身体没啥体育加分,但考试结束后就在田埂上无忧无虑的瞎转悠。 中考能升的高中只有两个,要么 “一中”,要么 “二中”。这两所高中是天壤之别,一个是继续上大学,一个是大专或者打工就业。 记得高考结束后傻乎乎的二指禅敲键盘,在网吧里通宵玩着飞车和农场。不知道 985/211,也不知道专业、就业、指南。填志愿一把梭哈,只因为里面有 “计算机” 三个字。 那时候以为终于离开了地狱,谁成想那是最后一次无忧无虑的天堂。 当时,只知道家人、老师、同学一起塑造的氛围,是一场 0 或者 1 的战役。但要说黑灯瞎火的努力是为了什么,不知道! 都没坐过电梯与公交,也没坐过马桶。我可不知道 “书中自有黄金屋” 里的 黄金屋 意味着什么,有饭能吃饱不饿,天塌下来有爸妈。 上大学时还未成年,有些签字还需要等假期回家后让父母签。经常在自习室弄着 高数、物理,想着考试也能顺带给别人抄。不知道在干什么,没有方向的耗费着时间。 后来有了个女朋友,当时还想着能够天长地久,真几把扯蛋。 大学,懊悔终生。 大四开学后,实在学不下去了。主修 C 和 Java 以及一些 JSP Web 页面,面试了 “科大讯飞”,记得面试官有个问题是:Tomcat 怎么改端口。结尾面试官说我冲动气盛,当场没给过。 再往后一段时间,有个老板在会议室等人。刚好从门口经过看到了,贸然进去说想去他公司工作,做 iOS App 开发。他说可以,工资 2300 (后面克扣了 600)。 我问 “JSP 展示的 Web 页面,数据在服务端已经拼好了。那 App 是怎么拿到 Server 的数据呢?” 老板说有一套数据解析格式。我问是 XML 吗?他说他们刚换成 JSON 了。 人生的行业选择,就在这几分钟落地了。进入 iOS 移动开发行业,没有深思熟虑,只因路过会议室门口,老板缺 iOS 实习生,而我想工作了。 之后在各个创业小公司频繁跳,只因一个接一个的倒闭。 在互联网井喷的那几年,有想过作为初始员工是否可以一跃龙门,终究是没赶上这样的机遇。有公司只差临门一脚,但终究是没熬过去。 那些年,为了公司的存亡,我这个低微的蝼蚁可是瞎操碎了心。结婚也没去蜜月,孩子出生也没休假,就那样努力的傻傻奋斗着,活成了傻逼。 之后又迷糊的做了一个错误的规划:想找一个闲工作,多些时间照顾刚出生的孩子。 打听到了一家不太忙的混吃喝的大公司,降薪跑了去。入职后的确空闲了一个月,哪知道后面开始改革,又忙碌了几年。 结束这份工作的时候,已经 27 岁了。 回忆了这么多重要时刻的抉择,只想说:人生的前 27 年,都是混沌的。重大的抉择,完全凭运气跨过了。 中考侥幸垫底上了好高中,高考误打误撞选择了喜欢的专业,大学迷离糊涂选择了有钱赚的行业,工作后 0 成本找到了非常优秀的老婆,等等。 真的,完全凭运气。这些都没有慎重思考过,但都没有走空。 27 岁那年,做了一个决定,想出去了。 这个念想开始膨胀的时候,都没敢和老婆说。联系了一个国外生活的老哥,给了一些可选择的列表,看下来也就只有日本。 本就是一个极度平庸的人,只有老婆说我非常棒,那棒不棒自己能不知道么。就连想要出去的可选择国,都没有第二个选择的能力。 想着还是得有一些积蓄或者固定资产,于是死磕了几个月的算法和八股文,去阿里工作几年弄了些保底钱。 我是一个没多少运气在身的人。如果有可选项,在懵逼中选的话,十之八九选错。 如上而言,这些年大的人生拐点,已经耗费了那少有的运气家底,以至于没走空。 27 岁那年的决定,在 2023 年已经按计划落地。这一次不是靠运气做的抉择,也实难估计收益,可能需要数十年来做验证。 2023 年,开始自行抉择,摆脱运气约束的元年。这一年,虽已三十而立,又好似成人礼。 年度数据 博客:(Google 数据) 日访问量 50-100,日曝光展示 400-800。年新增文章 23(技术文章 12)。< 数据惨淡 > 收入:待业期较长,收入锐减 60%。外部欠款降低 50%,外部应收款增加 150%。 工作:毕业后最开心的一年。工作时期体感 70%,待业时期体感 85%。整体满意度为 “无业一生轻”。 旅游:国内游 2 次。<威海 - 青岛、千岛湖> 家庭:陪孩子时间增长 +300%,夫妻和睦度增加 +50%。 技能: 普通家常蔬菜可简单制作,口感一般,视觉较差。荤菜还需要进一步尝试,可简单做猪牛肉炖菜。硬菜还不会。 开始迷恋咖啡,2-3 杯 / 日。 软考拿到了证书,日高度人才签可加 5 分。 日语进展缓慢。虽跨过了五十音,但后续无进展。 算法能力增强 200%。目前依旧为菜鸟水平,依据下一年工作内容判断是否持续。 社交: 微信已经不在使用,定期翻看朋友圈及联系人。 开始通过 Twitter 社交,fo 增加 100 人。 宗教:虽自诩基督徒,但心知不虔诚。今年对主归属感增加 10%。愿主持续教化,以马内利。 性情:平和了很多,没有大的起伏。宽容度剧增,存在即合理。已经变的 “自私自利”,愿意与他人给予或索取帮助,但更在乎自己的心情和体验。 政治:22 年达到顶峰 (顶峰时期关注度也很低)。23 年猛跌 100%,已经不在关心。 宗旨:差生文具多,又不是不能用拥垒。 工作 年初和公司协商离职,于九月份离开了工作三年的公司。 阿里虽然下跌的厉害,但依旧是一家非常优秀的公司。那里有非常完善的制度,是一个规则社会。 在那里,有非常多的优点可以学习。完善的工作流程、优秀的同事、丰富的经验沉淀等。 同时,那里也充满了压力。时时刻刻都在打磨着人的心性。 皮实耐操、拥抱变化,是活下去的本钱。 离职的时候,很开心。工作压力真的挺大,交还电脑前还在处理着杂七杂八的琐事。解脱了没有理由不开心一把。 离职后就没打算短期找工作。前老板让过去忙忙,也拒绝了。 就是想没日没夜的睡觉。实际上也不怎么睡得着,还是想找些事情做做。于是学习了一些新的语言方面的技能,也把前些年写的不太完善 / 认知错误的技术博客更新了一下。 年初提出离职的时候,也在规划着是不是直接润,还是再找个工作攒些家底 (还去字节的老朋友那边试了一把)。 后面又和将要去日本与妻子团聚的朋友欢聚了一把,聊了一通也下定了决心,离职后就润吧。 孩子已经上小学,性格、习性也已经到了固化的年纪。对于我这样拖家带口的组合,时间真的不多了。这一次不赌,后面只会更加的难。 离职一个月后,敲定了日本工作,是在日本做电子支付业务的,和国内的支付宝有些类似。觉得这是一个有机会的行业,选择本身也是一个赌注。 性情 妻子直言,从进入阿里开始,我就变了。变得苛刻、少有欢笑,陪孩子的时间骤减,家庭和睦度急速下降。 这几年成长的确很快,如开头所言,这段时间差不多就如成人礼前的极速成长期。在想法、实践、认知方面都有快速的增长。 这不是好事,人的确变得郁郁寡欢。总是喜欢思考一些小的事情,期望能发掘出大的道理,这实属变态。 最直接的改变就是没有多少笑容了。只有和孩子在一起,面对那个空白干净的、未被打磨过的世界,想着以后可能的五彩缤纷,会情不自禁漏出自己的笑容。 2023 年,抽了很多烟,喝了很多酒。尤其下半年,烟酒数量和上半年比,有 2-3 倍的增加。 虽自诩基督徒,但实在不够虔诚。但能感觉到自己对信仰的依赖在持续加深,主对我比较深的影响有两个: 我的人生有上帝的规划和参与,目前的境遇是最好的,是上帝给予的。少有后悔,只会坚定的走下去。 我没有偶像也不崇拜任何人,免受他们一言一行的影响。对所有人都只有尊敬而无仰望,会不停的借鉴和学习。 孩子与家庭 孩子的一年级已经渡过了半个学期,每日都很恐惧,我们的安抚不能解决丝毫。 5 个月里除节假日,没有一天的早上是开心的,前期哭了 3.5 个月,后面 1.5 个月也是闷闷不乐。 和班主任说过,不在乎孩子成绩,在班里不用管和打骂我家孩子,但这并没有解决问题。 孩子期末考试考了第一名,老师发来了祝贺。但我是不开心的。 孩子对学习的乐趣已经没了,仅仅在忧心中努力做好一些以防止被批评和责骂。我上学的时候也是这样,后面工作了也有阴影。 这颗不善的种子已经发芽了,不想孩子走我已经走过的路。这种时时刻刻在意别人的标签而活不出自己的人生,太累了。 因为收入锐减的原因,2023 年的生活水平有明显的下降。减少了很多外出吃饭的次数,很多时候都是在家做饭吃。而 21/22 年时常去一家自助餐的消费差不多都过万了。 有个习惯,对想要的东西,争取做到短期过瘾。对吃来说,就是疯狂的吃,吃到不想吃,吃到没有念想。 这些年,打败的瘾有:鸡肉、苹果、梨、格瓦斯、榴莲、三文鱼、西瓜等。 我这种山旮旯出来的,打小没见过海鲜。沾上了,就总是想吃。今年把螃蟹、三文鱼吃够了,打字这一刻,无念想。 体重也降了 10 斤。 今年对老婆和这个家庭更加的喜爱和尊重了。 老婆一直没有怨言的照顾着家庭,真的很累的。尤其对孩子的照顾和教育,非常细致繁琐,这种焦躁的生活很难容忍。 在结婚、生子两个重大的时间点,我都还像未成年的儿童一样,没有什么深刻的生活感知。上帝把我们组成了家庭,非常感谢。 感谢老婆对我持续的包容,每想到此,都会心生愧疚。希望做更多的付出,以弥补多年对家人陪伴、感情上的缺失。 老婆老家现在结个婚,房车彩礼都是标配,得小 200w 才能搞下来。即使当年,单纯彩礼这一项没有 20-30w 也弄不下来。 前段时间无意翻看到很多年前和老婆的聊天记录,感慨颇多。 那时好穷,结婚时候只有 3w,没固定资产。爸妈只要了 10w 彩礼。向亲戚借,没借到。向朋友借,没借到多少。图省事,贷款了 5.5w,凑齐了彩礼。后面爸妈回礼 7w,挪过来补了窟窿。 这些年搬家六次,新房仅住了一年,又要规划去日本从零开始。 老婆是有怨言的,我非常清楚,这无可厚非完全情理之中。但老婆一直在支持我,她一直在相信着我。每念及此,都有一种感情想要痛哭流涕。 感谢主的眷顾,愿老婆和孩子都好,愿我能给她们更多。 也感谢宠物狗闹闹,这些年都在带给家庭欢乐。你比孩子要长一岁,在我们的生活中,你就是孩子的姐姐,是家庭的一份子。这次日本计划若顺利,也一定会把你带去的。 日本 这次日本行,几年前就在计划。虽然也想着给家庭和父母留退路,但直到落地这一刻,也没留下什么。 选择日本,因为它得分最高,在距离、经济、生活、工作、育儿等方面综合考虑。 国外的月亮没有更圆,人性的善与恶,放之四海都一样。 生活工作这么多年,清晰的明白解决问题没有银弹。如同恋爱,前任总有优缺点难以忘怀,现任也会带来新的感动与悲伤。 不过是换一种生活方式,体验两个不同人生。 制定这个计划,和很多人的政治不满不同。 我是一个不关心政治的人,没有恨。毕业后的职业也是顺风,跟着国家的建设讨了不少巧,赚钱养家给了家人不少的支持。这也是恩情。 即使恐怖的疫情三年,也没有经历过居家、隔离。 我就是一个自私的小人儿。那些人祸、灾难,只多不少,只重不轻,但和我有什么关系呢?我给不了最基础的帮助,也对转机做不出些许改变。 即使搅了几天,搅了个天翻地覆,最终也会趋于平静。我连浪花都算不上,能做什么呢? 直接来说,铁拳没有一下子落在身上,的确无法感同身受。 但,那些悄无声音的、有长有短的束绳索,的确是我这种低微小人儿的顾虑。 房子从买来至今,已经跌了 30-40%。虽是自住非投资,但银行贷款花的钱还的贷是实打实的。 资产大幅缩水,以及周围不少朋友转行、待业的情形,谁能来买单?大家都只是简单的混口饭吃,讨生活的小人儿咋就难上加难。 改革是必然,这些年开放取得的成就有目共睹,不能吃老本,也应该有更多的推进。历史的车轮一定是向前的,将时间的跨度拉大,文明、经济、生活一定都是向前进步的。这些年也一直和孩子说,要勇敢面对变化,没有什么是一成不变的,内部外部都在时刻发生着变化。 但在时间的长河中,会有多少下坡?需要多久的摸索后才能及时掉转方向?这一届好,下一届、下下届还会好吗? 能人志士,下坡也能逆行,可我不是这些聪明人。疫情虽然没被隔离,家里的宠物也安好,但社会和经济的停滞也对我有不少影响。其他如期而至的各种管制也让行业受创,收入和工资都受到影响。 我不想下这个赌注,只想每次都能在及格线上增长就行。堵高速里最怕车子不动,只要还在龟行,就还在进步。 在三体星球,因三颗星体的引力拉扯,那里无法预测规则。 可能白天持续几小时后就迎来了漫长的黑夜。还有那可将石头冻成粉末的寒冬与酷暑,也不知道会持续几个月或者几年。 三体人梦寐以求的是像地球这样的四季分明,清晰的黑夜与白天。它们侵略了地球,不是为了资源和土地,是为了稳定和有序的规则。 姥爷姥姥八十好几了,在农村有车有房,好多儿女,自身存款好几十万了。现在每天辛苦做生意啥的,小麦水稻也都在弄。他们养老不愁,什么都不做,也花不完手里的钱。可还是辛苦劳作,以预防什么。我父母也在走完全同样的路。 他们想法都是,哪天真的干不动了就不干了。什么是退休?不知道。 而我比他们只是在城市多呆了些时日,从农田耕种搬到了坐办公室。过不了几年,也会走他们差不多的路,只是换一种讨钱形式,但换汤不换药。 没有完善的养老、医药制度,安全感就无法建立。 医保有没有用?当然有。任何一件事物的存在,都会有哪怕最低微的作用。 前两天妈妈手断了,要住院才给报。平常病要用药,医保也需要拿特定的药才行。 大伯身体要换支架,国产的 20w 能续命 10 年,进口的贵了好多倍但可以保障正常寿命。筹钱弄了国产,也没得报销。 有没有给,给了。能用吗?能用。好用吗?不好用。什么都给了,又设置了层层卡口。 到了一定年纪,病不是能躲过去的。它一定会如约而至,没有例外。而这些条条框框的卡口,就是最后一击。 当然,对于养老、医药政策,不出几年,肯定会有改革。但这个漫长摸索期和结果本身,也是赌注。 6 岁前,我和妻子给了孩子很好的家庭教育,主要归功于妻子。6 岁前是孩子性情、习性养成期的关键期,我们很重视。 但在 5.5 岁上了小学后,多年的努力废了很多。学校给了孩子不一样的培养方式。 如上面 “孩子与家庭” 所描述的,孩子对学习的兴趣已经非常淡薄。几个月前,孩子装病不愿上学,带孩子捡了一天垃圾,孩子翻着脏垃圾桶,也不愿意回学校。 我们给孩子尊重、选择和自信,学校给孩子批评、恐吓,迎头一棒的打杀。 教育有改革,包括期终成绩等级制 (日常考试还是真实分数),还有 “双减” 等政策。这些改革很无力,弱肉强食的竞争大环境没有改变,对生活和未来的恐惧没有降低,读书依旧是最快捷且近乎唯一的幸福通道。 “书中自有颜如玉、知识改变命运” 的钢印,以及 “屎难吃钱难挣” 的现实,孩子只能接受家长和学校同时施加的全负荷填鸭应试教育。好成绩大概率有好未来,一代又一代的轮回。 对于下一代成长和学习的二十年里,哪个国度都是辛苦的。想更加优秀,肯定要付出更多的精力、金钱、辛劳。可以不劳,但也无获。但在 “只能通过学习来 xx” 和 “可以通过学习来 xx” 之间,希望给孩子更多的可能性和选择。 不担心红色和歪曲的教育,这些可以补救。 但希望孩子心智是完整的,成长不是同质化的,童年和学习旅程是健全愉快的,“身心、思维、认知、逻辑、心性” 等受益终生的种子,有足够的时间和空间在心田慢慢发芽成长。 除此之外呢?其实不多了。日本面试官当时问:为什么要来日本?我的回复很简单,是对未来的一个保障。目前生活的也不错,但不可持续。 对于很多生活困难或客观原因的人来说,出去是更好的选择。但与我的家庭而言,至少后面十年内,这里是最安逸的。 总能找到比较好的养家工作、爸妈按期寄来新鲜的食物、日常用品也能弄到国外的渠道。 可一二十年后呢?孩子面临成年、工作、婚姻,父母面临老去、疾病,而自己的人生也完全定型。那时候上下三代人的生活处境,我有极大的忧虑,因能力跟不上梦想,这是刻骨铭心的痛。 出去后日子肯定很艰难,新的生活需要整个家庭来适应,包括收入与支出的巨大波动和差异、语言、教育、稳定性等等。 还是希望做些改变,就安慰自己是长期收益吧,搏一个 “未来可期”。 规划 人生基本快过半了,时间真的不多了,能挣扎的也不多了。如今把未来压在移民,基本也把少有的拼搏希望给抹灭了。 出名要趁早,只因为晚了,希望就不大了。自知很可贵,坦然面对普普通通的现实,别做无谓的挣扎。 后面几年,希望达成一些目标: 24 年要熟悉日本的工作、生活,努力安全降落。如有困难想要退缩,也要为自己当初的抉择买单,撑到 24 年底。 25 年拿到摩托车驾照、买一辆摩托车。【挑战:24 年拿到驾照】 24 年日语达到 N4 水平,25 年通过 N2 考试,25 年拿到高度人才签证,28 年之前拿到永驻。 24 年在日本租独立套房,完成软硬装。 25 年,将妻儿带到日本生活。(必须完成,否则考虑回国)【挑战:24 年底之前完成】 26 年家庭总收入达到 1000w 日元。 28 年购置房产。【挑战:27 年完成】 24 年降低抽烟量,达到 3 天一包。25 年完成戒烟。 24 年体重维持 70±1.5KG,25 年维持 68±0.5KG。周锻炼时长 > 4H。 26 年往后,家庭年外出旅游次数 >= 2。

2024/2/24
articleCard.readMore

【Swift】中文词语纠错

一直有一个写字痛点,就是错别字。尤其在发帖子和写文章这样的正式场合,错别字会引起很大的误解,而每次检查都会很吃力。 试用了人气较高的 “写作猫” 和 “火龙果” 两个纠错平台,都有很大的局限性,并不适合我这样的人使用。 写作猫的缺点是 word 走天下,一点也不 nice。 火龙果的缺点是没有纠错能力,我用测试文档只检测出来两个不存在的英文错误。 它们都不支持 markdown 检测。 付费较高,对于非高频使用人员不友好。 于是,尝试自己写一个纠错工具,目前做了开源,支持 CLI 和 GUI,详见:HLVSentence、HLVZhCorrect。 技术方案是:检测文本文件 -> 文本分句 -> 词语检测 -> 工具集成。 遇到的核心问题有: 准确识别文本文件。文本文件无 Magic Number 二进制特征,如何准确识别当前文件为文本文件并读取内容。 文本分句校准。对于纯中文场景,通过常用的标点符号即可正则分句。但一般会参杂英文、特殊符号,还有 markdown。 中文纠错检测和校准。找了不少方案,最后选择 pycorrector 提供的中文纠错模型。工具需要和 python 环境做对接,并对待检测文本做进一步校准以满足模型。 非终端环境和 python 脚本互联。需要支持 python 虚拟安装环境。 SwiftUI 开发 “命令行 CLI 工具” 和 “Mac 菜单栏工具”,将工具对接 brew 平台。 下面是开发文本纠错工具的一些历程。 Demo 演示 可查阅 HLVSentence、HLVZhCorrect 两个项目的 readme 主页,查看 CLI 和 GUI 的效果和使用说明。 文本文件识别 对于具有特定使用场景的二进制文件,它们不需要遵循 Unicode 字符集规范。因为使用场景专一,一般都会通过 Magic Number 对文件类型进行标识,如 ELF、Zip 等。 但对于一个文件是否是文本文件,并没有完全行之有效的识别方案。只能通过尝试去理解内容,这是有一定误差的。 对于文本文件所具有的识别属性,可以从以下几点考虑: 文件后缀。【特定场景下稳定,对用户信任】 magic number 排除。【文本文件均无稳定的 magic number】 编码识别。【文本文件一定有一致的编码和解码方案】 通过文本编码可以最为精确的识别当前文件是否是文本文件,从而进一步依靠对应的编码来读取文本内容。但这也会有一些问题: 性能问题。文本文件一般较小,可以进行全量解码。但待解码的文件可能是图片或者压缩包,这会有极大的性能损耗。 二进制中存在文本标记。二进制文件中部分位置可能插入文本,用于记录一些信息。如果读取部分内容且正好读取了这一部分,则有一定的误判概率。 所以,有一个可行的方案: 先判断有没有 magic number。如果有,则可以识别到当前文件的具体类型。一般只要有 maigc,就不再是文本文件了。 再对文件内容进行多个位置的截取解码。根据文件大小动态设置采样点,且所有采样点解码全部通过,则判定为文本文件。 Swift 文本文件识别 源码详见:HLVFileDump 对于文本编码的详细介绍可查阅 计算机字符编码与内存编码 文本分句 纯中文环境下的分句,比较好实现。只需要控制将?、;等几个特别的断句符号进行识别即可。 但很少有纯中文的段落,一般都是中英文混杂,而且还有个数算术符号。如下面几个例子: 今天的温度是 25.3°C,天空晴朗,风速约 10 mph.,这里末尾是英文的. 收尾,且数字中含有. 无法做断句。这里只能通过 NLP 语言模型进行分句,Apple 提供了 NaturalLanguage 物料库,可以在多语言混合的场景下,进行语句分割,且能够处理数字符号。 今天天气不错(适合出游),你要参加吗?,这里 NLP 就不能很好识别,会把中文的(进行分句,导致 () 两个字符被分割到两个分句里面。 1. 吃饭 2. 睡觉 3. 打豆豆,这样有数字前缀或者其他特殊字符前后缀的,在分句后理应去除序号或者特殊描述符号,只保留汉字内容。这个 NLP 能初步识别,但也有误差,基本够用。 特殊标记符号如 \t 等,需要特别处理。它们会影响到分句的质量。 这里的一个可行方案是: 先根据中文符号进行一次分句。此时结果基本符合中文语意,但中英文和符号混杂的地方无法分割。 将上一步的分句结果使用 NLP 二次分句,此时基本符合分句诉求,偏差不大。 但是在可行方案执行前,还有一个重要的点,是 NLP 对 markdown 识别不敏感,误差率较高。还需要先进行 markdown 的去格式化。 文本纠错 找了很多文本纠错开源方案,大多不理想。甚至文章开头说到的商业平台,测试下来也没有很优秀。 一个很主要的点,是纠错是针对单词而非语境的。针对单词的纠错通过大量的单词匹配即可,但误差率较高。 最后使用的是 python3 实现的 pycorrector 模型,能够部分识别语境。 对模型参数进行一些调整处理后,目前能够做到: 如果有错,非常大的概率是能够被检测到。 如果没错,有小部分概率认为有错。 就像布隆过滤器一样,没有漏,也是可以的。又不是不能用。 当然,模型对数据更严格,稍有不慎,就检测失败了。最大的问题是不支持英文和特殊符号。 所有在文本纠错的时候,又对上面的分句内容进行中文提取,然后交由 pycorrector 检测。 python 环境数据互通 前面分句的结果在 Swift 可执行程序的内存里,后面需要交用 python 脚本执行并拿到处理结果。 一个可行的办法是 app 中嵌入 python 解释器,但这会凭空增加复杂度,且不好维护 pycorrector 模型库。 最后通过 SwiftShell 高效的命令行工具 实现了数据互通。 因为 app 环境中缺失终端环境变量,这在使用 Conda python 虚拟环境的时候会找不到 python 可执行三方库。 最后的解决方案是委屈用户,需要自行通过 echo $PATH 获取终端 path 后设置到 app 中。 对于 CLI 环境倒比较友好,因为 CLI 本来就是在终端中运行的,已经拥有了终端上下文 Env,没有遇到太多的问题。 CLI 和 SwiftUI Swift 开发 CLI 命令行,整体还是比较快速简便的,官方有较好的支持。 入门可以参考:Swift 脚本开发环境搭建,当前项目也是完全按照入门说明来操作的。尤其文中的 不换行更新上一次的输出结果 和 异步等待,在命令行开发过程中很有用。 SwiftUI 开发 Mac 还是有不少概念需要理清。 AppKit 和 UIKit 有很大的差异,比如 Window/WindowController/NSMenu/NSStatus 等。SwiftUI 又要抹平两者的差异,所以 SwiftUI 整体上和 AppKit/UIKit 都有较大差异。 建议一个入门方式,是通过 Xcode 的 Development Documentation 快速查看 SwiftUI 的编写规则,混个脸熟。 然后在 Github 上面搜索 Api 找到一些比较新的 SwiftUI 项目阅读源码。 最后,频繁的咨询 ChatGPT,有时候它给的答案并不对,但能给一个大致的方向。 brew 可参考当前工程的私有配置:homebrew-tap。 brew 自定义仓库等相关资料较少,本人也没有进一步学习。 目前把 CLI 和 GUI 都部署到了自定义仓库中,有需要的可以简单参考下。 App Store 真的做到了 “花开两朵,各表一支”。 在 Mac 上,Mac App Store (MAS) 犹如鸡肋,复杂的沙盒安全形同虚设。 Apple 看着 MAS 生意惨淡,却毫无作为。既不能做为平台为优秀的软件做宣传,也阻止不了垃圾软件的恶意收费。

2024/1/6
articleCard.readMore

计算机字符编码与内存编码 - Unicode、UTF8、String

只要使用电子产品,每时每刻都在和字符打交道。目之所及的字符,在存储、显示、执行等环节都离不开编码的支持。 但对于字符编码,也会有一些误区,比如:Unicode 和 UTF 是否都是编码、字节和编码的关系、āáǎà 的音标字符表达、内存字符编码 等等。 本文将循序渐进的,从 ASCII/ Unicode 介绍到 UTF8/16/32,以及文本二进制存储和 URL 编码 / 多次编码等场景,最后解释内存中字符串的编码格式。 每个环节都通过二进制调试的方式给予实际的场景输出,争取一遍将编码吃透。 .table-container:nth-of-type(1) table { width: 500px;}.table-container:nth-of-type(2) table { width: 800px;}.table-container:nth-of-type(3) table { width: 1000px;}.table-container:nth-of-type(4) table { width: 1000px;}.table-container:nth-of-type(5) table { width: 800px;} Unicode ASCII 是大学计算机教科书入门知识点,如果不联系上码位和 Unicode ,还是不好理解的。知道有这个东西在,但是怎么玩的却不是很清楚。 比如,C 语言是古老的伟大语言,印象中 C 语言教程都使用的 ASCII 字符,没有出现中文。那么 C 语言可以通过 printf 输出中文吗? ASCII 和 Unicode 都是字符集,又称码表,字符集可以看做一个数组,里面的每个元素称为 码位 / 码点 /Code Point。 字符集是约定俗成的一套全世界都默认遵守的规则矩阵,这套规则矩阵里面的每个元素(码位),都表示一个特定的符号含义且不可改变。 ASCII 并不是表示 0-9 或者 a-z 这些我们肉眼看到的符号。这些符号是对于人类友好的视觉表达。 比如 @ 字符,可以脑海里回忆一下,这些年是不是见过很多样式的 @ 符号。但这些不同的视觉表达并不是 ASCII。 @ 字母的 ASCII 码位是 0x40 (64),这个才是 ASCII 的含义。即 ASCII 定义了 0x40 这个码位只能用来表达 @ 这个字母的含义,不能表示 a/b 或其他。 从 ASCII 进化到 Unicode,就是增加了很多码位,以建立全世界的规则矩阵,大家都得遵守。 emoji 等各种表情符号就是 Unicode 里面的特定码位。应用层识别到具体的表情码位后,做的符号映射和视觉显示。 切换字体,也同样是对同一个码位,做不同的视觉表达。 所以,这里很重要的一点,是对 码位 的理解。码位才是每一个字符的互联网流通语言。后面介绍的文本文件存储、二进制文件存储等,都离不开码位。 对于字符来说,计算机也只对码位做存储,在没有 Unicode 的时候,计算机存储的就是 ASCII 里面的共计 256 个码位 (00000000 - 11111111)。 下面例子中,将 abcde 存储到文件中,共计 5 个字节。通过 xxd 命令确认每个字节的存储内容都是这些字母的 ASCII 码位。(后面统一使用十六进制 hexdump 查看)。 1 2 3 4 5 6 7 8 9 10 11 > cat test.txt abcde > wc -c test.txt 5 test.txt > xxd -b test.txt 00000000: 01100001 01100010 01100011 01100100 01100101 abcde > hexdump -C test.txt 61 62 63 64 65 |abcde| 有了 Unicode 之后,当然也可以对 Unicode 的码位做存储,这是最简单的复刻 ASCII 的模式,但这有一些问题。 Unicode 采用平面码位设计,共计 17 个平面,每个平面有 65536 个码位,共计 1114112 个码位。 二进制是:xxxxx 00000000 00000000 - xxxxx 11111111 11111111。xx 表示平面,从 0 - 16,范围是:00000 - 10000,绝大部分字符的 xx 都是 00000,称为 零平面。 如果要将 Unicode 的码位做存储,为了补齐每个字节,那么每个码位都需要 3 字节的存储空间。 这对于磁盘和内存都无法接受,尤其英文是世界的流通语言,在 ASCII 的时候,它们仅需 1 个字节的存储空间。 所以,这就需要对 Unicode 的码位进行压缩,也就是下面要说的 UTF 编码了。 再进入到下面之前,先来熟悉几个 Unicode 码位,后面会频繁的使用这几个字符做说明。 字符 Unicode 码位 十六进制 Unicode 码位 二进制 a0x6100000 00000000 01100001 中 0x4e2d00000 01001110 00101101 😀0x1f60000001 11110110 00000000 UTF UTF,是字符编码,是一套算法实现,实际上是对码位进行编码。 它们服务于前面说到的 Unicode 字符集,具体职责是:如何对码位进行压缩,以节省空间,并兼顾效率 (编码 & 解码) 。 这里介绍一下 UTF-8 和 UTF-16。 UTF-8 UTF-8 使用多字节来表达一个 Unicode 码位 字节数 Unicode 码位十进制 UTF-8 编码 1000000-00007F0-1270xxxxxxx 2000080-0007FF128-2047110xxxxx 10xxxxxx 3000800-00FFFF2048-655351110xxxx 10xxxxxx 10xxxxxx 4010000-10FFFF65536-111411111110xxx 10xxxxxx 10xxxxxx 10xxxxxx 这里举几个例子,可以详细的看下: 符号码位字节 UTF-8 编码十六进制 a00000000 00000000 01100001(0x61)1011000010x61 中 00000000 01001110 00101101(0x4E2D)3<1110>0100 <10>111000 <10>1011010xE4B8AD 😀00000001 11110110 00000000(0x1F600)4<11110>000 <10>011111 <10>011000 <10>0000000xF09F9880 可以换算一下,将 UTF-8 的二进制编码,去掉前面的标记位,就是原本的码位二进制。 UTF-16 UTF-16 是固定编码,要么是 2 字节,要么是 4 字节,根据平面来决定。对于第一个平面 (零平面) 均使用 2 字节,其他的使用 4 字节。 但是 UTF-16 有个默认 2 字节前缀,即最小也是 4 字节。还是上面的例子: 符号码位字节 UTF-8 编码十六进制 a00000000 00000000 01100001(0x61)200000000 011000010x0061 中 00000000 01001110 00101101(0x4E2D)201001110 001011010x4E2D 😀00000001 11110110 00000000(0x1F600)4<11011>000 00111101 <110111>10 000000000xD83D 0xDE00 这里有个小小的注意点,对于 UTF16 来说,零平面的码点,码位和 UTF16 值,是一样的。这一点对于后面要说到的 内存编码 很重要,这里先了解下,后面再细说。 UTF 小结 UTF32 就不介绍了,用的也少,4 个固定字节大小,也没什么特别要说的。比较下 UTF8 和 UTF16 的差异: UTF-8 占用空间最小,但是解析速度低,因为不能按序解析字节,UTF-8 对字节大小是变化非常大的。(空间小,适合磁盘存储) UTF-16 占用空间中等,解析速度快。因为更多的码位用 2 个字节表示,字节大小基本固定(少部分字符通过 4 个字节表示),解析非常快。(速度快,适合编解码) UTF-32 占用空间最大,解析速度最快。一个码位用 4 个字节表示,字节大小全部固定,解析非常快。(空间太大,基本不用) 整体来说,UTF8 对英文场景比较合适,可以显著的减少体积,因为英文基本都是 1 字节来表达。 而 UTF16 对中文场景比较合适,中文在 UTF8 一般都需要 3 字节,在 UTF16 里面一般只需要 2 字节。 这种差异不仅体现在存储上,在内存中也会有体现。比如程序中比较 中、文 两个汉字的大小,对于 UTF8 需要比较 3 个字节,而 UTF16 只需要比较 2 字节。这点在下面的内存编码中会介绍。 URL 对字符非常敏感的一个环境,就是 URL。URL 对组成其内容的元素要求非常严格,只允许下面这些字符: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 - _ . ~ ASCII 字符集也没有全部包含,只有上面这些字符是可以在 url 里面直接使用的。不在这个列表里面的,全部都需要进行百分号转码。 百分号转码是 URL 特有的一套规则,默认使用 UTF-8 作为字符编码依据,对编码后的字节产物,进行百分号分割。 百分号转码后还需要还原回去,这套流程分别叫做 URL Encoding 和 URL Decoding。 所以,对 URL 来说,它使用的编解码是 URL Encoding 和 URL Decoding,这套编码内部通过百分号拼接 UTF-8 字节来实现。 举个例子: http://www.xx.com/中 http%3a%2f%2fwww.xx.com%2f%e4%b8%ad 原始 url 里面的 :、/、中 均被转码了。其中 中 的 UTF-8 形式是 0xE4B8AD,上面有介绍过。这里会通过 % 号进行每个字节分割。 这里有个小的疑惑点,即 % 号本身,并不是 URL 允许的字符,但是它可以直接用在 URL 中。举个例子: http://www.xx.com/% http%3a%2f%2fwww.xx.com%2f%25 这里可以发现,% 本身在 URL Encoding 中也会被转成 %25,因为它的 UTF-8 字节是 0x25。 这也是 URL Encoding 规则的定义,通过 UTF-8 编码的字节,需要通过 % 进行字节分割。% 虽然不是 URL 允许的字符,但可以出现在 URL 中。 如此之后,URL 实际上可以被多次 encoding,每次 encoding 的时候 % 都会被换成 %25。 对 % URL Encoding 5 次之后结果是:%25252525。如果需要拿到最开始的 % 符号,也同样需要对应 URL Decoding 5 次。 所以开发过程中,Encoding 和 Decoding 的次数需要一致,这个非常重要。否则就拿不到正确的 URL。 音调 对于音调来说,每个字母都有 4 个音调。为 āáǎà 每个字符都设置一个码位有些不合适。而且除了音调,还有其他一些场景,也具有同样的现象。 Unicode 设计上,会通过两个码位来表示一个字符。即码位和字符并不总是 1:1 的关系,有时候是 2:1。 比如,ā字符,就是由 a 码位 和 ˉ 码位连续组合。 1 2 3 4 5 6 > cat test.txt ā > wc -c test.txt 3 test.txt > hexdump -C test.txt 61 cc 84 |a..| 其中,a 和 ˉ 的 Unicode 码位分别是: 符号码位字节 UTF-8 编码十六进制 a00000000 00000000 01100001(0x61)1011000010x61 ˉ00000000 00000011 00000100(0x0304)2<110>01100 <10>0001000xcc84 存储的时候,两个码位还是单独存储的,上面 wc 命令查看共计 3 字节。只是展示的时候,需要应用层做识别,将音调显示在前面字母的顶部。 注意,两个码位组合代表一个字符,这是 Unicode 的规定,需要使用方遵守约定。 但 Unicode 本身并没有一套计算公式,将两个码位组合成一个码位。实际上就是两个码位按照前后顺序写入即可。 文本文件的识别 我们认为的文本文件,一般都是通过特定文本编码写入文本的文件,比如 .txt 或者 .m/.java 等文件。而非 .zip/.jpg 等文件。 对于文件而言,存储的内容都是二进制。虽然可以根据后缀认为某个文件是文本文件,但鉴于后缀也可以更改,所以这并不准确。 有一种方案是读取文件头的 magic number。但这种方案对于识别文件二进制类型比较有用,并不能识别出文本文件。因为文本文件一般都是没有特定的 magic number 的。 对于图片 / 视频等文件,读取它们的二进制内容和读取 .txt 等文本文件的技术方案一致。 不过对于图片等二进制,显然无法进行有效的 UTF 解码,所以读取后会是乱码。 对同一个文件进行一致的编码和解码,这样写入的二进制就具有一定的规则。读取的时候按照同样的规则进行解析,当然可以识别出当初写入的内容。 当然,可以对文件的二进制内容全部读出来,然后通过 UTF 进行解码,若能解出来,那就是文本文件。 这是稳定的方案,但具有极大的性能损耗。因为一个图片或者视频,它的二进制内容是非常多的,IO 成本过大。 有一个比较小巧的技术方案,即对文本内容主动进行多个位置的截取解码,以较小的性能开销来对文本文件进行识别。 比如从 文件头 N 偏移的位置截取 10 个字节数据。对这 10 字节进行不同维度的解码。 只要能有一次解出来,说明这 10 字节数据是符合文本编码规范的。(大概率无法一次解出。因为 10 字节里面只有一部分是完整的编码数据,两头很可能是被截断的,无法被解码) 同样的操作可以进行 M 次,需要 M 次全部命中,才能认为当前文件的确是文本文件。合理的设置 M 值,对于真的文本文件会具有很高的识别效率。 但只要有一次不命中,既可以确认当前文件不是文本文件。即非文本文件,可以一次命中,效率极高。 这样,也可以过滤图片文件中插入文本这种操作。 这种方案以读取少量的文本内容和多次匹配作为代价,可以比较稳定的确认当前文件是否是文本文件。 源代码可参考:HLVFileDump 内存字符编码 终于要说到内存中的字符编码了。这是非常重要的一环,也是很多人对于字符编码的卡点瓶颈所在。 前面说的字符使用 UTF 编码,都是以写入文本来说明的,主要阐述的是 UTF 编码的存储能力。 这一方面 UTF-8 通过多字节能力,可以非常有效的压缩文件大小。这在磁盘、网络传输等方面是绝对的王者。 但是在程序运行过程中,比如字符串大小匹配场景,也会对字符串进行操作。这时候的字符,在内存中是什么样的表现呢? 它一定会有一个表现形式,否则它就没法存在。 它可以通过码位 二进制的形式在内存中表达。上面也提到过磁盘存储也可以直接存储码位,但固定 3 字节的大小太费存储了,在内存这里显然更拘谨,所以更不合适。 也没更多选项了,要么 UTF-8,要么 UTF-16。因为 UTF-32 是 4 字节,那还不如直接存储码位了。 实际上,在内存中对于字符使用 UTF 的那个存储,不是由操作系统决定的,而是由编程语言决定的。对于高级语言一般都使用 UTF-16,但 C 语言使用 UTF-8。 在介绍内存编码之前,还是先简单说明一下二进制文件吧。文本文件属于二进制文件,但二进制文件具有更多的表现能力。 理解了它们,除了方便理解后面的内存字符编码,也对文件中的字节有更好的认识 (对于本文而言,这属于番外内容)。 二进制文件 所有文件都是二进制文件。 高级语言对文件进行读写的时候,需要通过内核塌陷的模式,调用操作系统 api 拿到文件句柄然后操作 read/write api。 这里 read 和 write 的数据,都是 Data 二进制。即文件、文件系统、操作系统、计算机,只理解二进制,不理解具体的字符。 对于二进制文件,也有很多种用途。比如: 文本文件,主要用来记录文字,将文字编码后写入,解码后还原。每个字节都是一致的含义,即都表示文字 (也可以认为没有含义)。 图片 / 视频文件,主要用来存储特定格式的数据。它的每个字节所表达的含义可能都不一样,比如 m-n 字节的数据表示地理位置,i-j 字节的数据表示拍摄机器的型号等。 可执行文件,如 Twitter,主要用来严丝合缝的存储自身需要的数据。它不需要被外部读取,它本身可运行读取自己。比如 ELF 文件,每个 section、每个段、符号表等,都需要存储在特定的区域。 这样看下来,从文件的角度来看,都是二进制数据,没有差别。但是对文件使用者来说,二进制文件的含义千差万别。 在很多年前,我希望同事将 jpg 图片改成 png 图片。拿到 png 图片后程序的 bug 依旧没有解决。最后排查发现同事把 .jpg 的后缀改成了 .png :( 程序走 .png 的逻辑,但内部数据是 .jpg 的格式,这就会导致无法解析。 值编码 说到内存编码,是针对刚才提到的可执行二进制文件来说的。因为文件执行起来后,才会使用字符和内存。 对于下面三种场景,同样的字符 1,具有同样的字节大小,但使用方式一点都不一样: 1 2 3 int8 i = 1; char i = '1'; String i = "1"; 对于 1 字符,在 二进制数、ASCII 和 Unicode 中分别是如下定义: 1 2 3 数字二进制 int8:00000001(0x1) ASCII 码位:00110001(0x31) Unicode 码位: 00000 00000000 00110001(0x31) 编译器需要对 int 类型的 1 进行数字操作,通过数字二进制就可以表达,不需要使用字符集码位。 对于 char 类型,编译器明确该类型只需要满足 ASCII,直接使用 ASCII 码位即可。 对于 String 类型,下面细说。 这就是值编码。即这个字符使用什么编码,是和它的类型有关,编译器最终会决定编码类型。 内存字符编码 在文本文件的时候,文件使用 UTF8 编码,但是读取文件的时候还需要解码,目的是解出码位,通过码位识别当前字符是哪个坑位,以做应用层显示。 但对于可执行程序而言,内存中的数据可以从可执行文件中读取,且不需要解码。即可执行文件中该字符是什么内容,原封不动的读取放入内存或者寄存器即可。 这样的目的是省去解码操作,直接进行二进制的读取即可,可执行文件是速度优先,整个编译器都在为速度服务。 所以对于上面的 String 类型,在内存中使用何种编码,直接通过查看可执行文件来确认。下面开始准备物料进行分析: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // Swift let swift_x = "a1"; let swift_y = "中1"; let swift_z = "😀1"; // C void test_c(void) { char *c_x = "a3"; char *c_y = "中3"; char *c_z = "😀3"; } // Objective-C + (void)load { NSString *oc_x = @"a2"; NSString *oc_y = @"中2"; NSString *oc_z = @"😀2"; } 选了三个不同的语言,字符串末尾增加了 1/2/3 标记。下面看下编译产物: 显然,内存中各种高级语言也是使用了 UTF 编码,这没得选择,Unicode 和 UTF 是绝配。 C 语言统一使用 UTF8 编码 Objective-C 同时使用了 UTF8 和 UTF16 Swift 使用了 UTF8(查阅资料说 Swift 也会在某些场景下使用 UTF16,没能复现) 但各类语言的内存编码使用情况并不是一边倒的偏向某一方。 比如 Java/Python/JS 使用的是 UTF16,Go/Swift 使用的是 UTF8。还有些是两者同时使用如 Objective-C。 在内存中使用 UTF8 或者 UTF16,主要考虑的并不是存储和编解码性能。 因为在程序运行过程中操作的字符频率并不会很多,远远不如一个 .txt 或者 .png 里面动不动几十 K 或者几十 M。 内存中在乎的是字节操作性能。在算法执行过程中,对字符的 比较 / 截取 / 复制 等操作,那是越快越好。 在配置相同的情况下,哪个编码的字节小,哪个就会快一些。 比如对 a、b 两个字符进行比较,若使用 UTF8,则只需要比较 1 个字节。若使用 UTF16,则需要比较 2 个字节。 对于 中、文 两个字符比较,UTF8 需要比较 3 个字节,UTF16 需要比较 2 个字节。 因为 Unicode 码位的不同位置区块,是偏向不同国家语言的。比如英文主要集中在前面的 ASCII 部分,中文就比较靠后。 在 UTF8 场景,英文肯定占有优势,字符串操作的字节数直接比中文少了一大半。 但大家又集中处于 零平面 区块,在 UTF16 里面基本所有国家的字符都是 2 字节,从上帝视角来看这又比较公平。 最好的办法当然是根据文本内容的不同,选择不同的编码。 目前 Objective-C 的做法是:若字符中全是 ASCII,则使用 UTF8。只要有不是 ASCII 的字符,则使用 UTF16。这是一定程度上的优化。 听说 Swift 可以根据内容,智能的选择哪一个更合适 (若英文偏多则使用 UTF8 这样子),但本地没能复现。但显然这是最优解。 体毛具有生长期、休止期。头发的生长期很长,可以持续 2-6 年。腿毛的生长期只有几个月。 生长期结束后就不长了,休止期会退毛,然后毛囊长出新的毛发。重复生长期过程。 所以头发可以很长但是腿毛就很短。

2023/12/25
articleCard.readMore

从汇编角度理解 “函数调用栈” 和 “有栈协程”

函数调用栈虽然表面意思很容易理解,但是说到汇编层面,对于 call、ret 等命令的具体表现,还是有一些理解复杂度。 尤其在协程大放异彩后,通过函数调用栈来表达协程的逻辑,也尤为重要。 本篇文章会从基础汇编的角度理解函数调用过程,并给出有栈协程的汇编实现。这样会对诸多高级语言的协程有进一步理解。 Start 函数执行过程大致如图中所示。其中函数栈的内存布局因操作系统和 cpu 架构的不同或许有差异,但不是重点。图中展示为 Mac + X86 系统。 从图中可以看到,程序执行的流程就是函数调用的过程。当函数调用完毕,程序也就停止了。所以函数调用对程序执行的影响还是非常大的,函数是指令层级的超高度抽象。 这里就先引出一些名词,后面不在特别说明。 内存布局:从上往下依次是内核区、栈区、共享数据区 (动态库)、堆区、数据区、代码区、留置区。这些区域的划分都是虚拟内存,并非物理内存。越往上内存地址越大,越往下内存地址越小。这里研究的函数调用栈就是在栈区。 栈区:从上往下分配空间,即先执行的函数的内部变量的虚拟内存地址一定大于后执行的子函数的内部变量的虚拟内存地址。 call:call 指令用于调用子函数。 push: push 指令用于将寄存器的值入栈保存到栈区内存中。 pop: pop 指令用于将当前栈区指向内存的值保存到寄存器中。 ret: ret 指令用于结束当前子函数调用,回到被调用处继续后续指令的执行。 rbp:栈底指针寄存器,值为栈底内存地址 rsp: 栈顶指针寄存器,值为栈顶内存地址 (下文会说到,rsp 不一定非要指向栈顶) 子函数调用前的准备 函数的作用是给予特定的输入,给出特定的输出。所以对子函数的调用,一个是传给子函数参数,一个是获取子函数的返回值。返回值在后面说,这里先说传参。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> int sub(int a, int b) { int t = a + b; printf("The sub value is:%d\n", t); return t; } int main(void) { int a = 1; int b = 2; int p = sub(a, b); printf("the return value is:%d\n", p); return 0; } 示例代码中,main 对 sub 函数的调用,需要 a 和 b 两个参数,下面通过 main 的汇编,可以看下参数是如何准备起来的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 main: 0x100003f30 <+0>: pushq %rbp 0x100003f31 <+1>: movq %rsp, %rbp 0x100003f34 <+4>: subq $0x10, %rsp # 准备 0x10 大小即 16 字节的留置内存区域,用于存放各种参数 0x100003f38 <+8>: movl $0x0, -0x4(%rbp) # 0 值写入,是默认预留的大小空间,无特别场景,不会使用 0x100003f3f <+15>: movl $0x1, -0x8(%rbp) # 1 值写入变量 a,实际是写到栈内存中 rbp - 8 地址处 0x100003f46 <+22>: movl $0x2, -0xc(%rbp) # 2 值写入变量 b,实际是写到栈内存中 rbp - 12 地址处 0x100003f4d <+29>: movl -0x8(%rbp), %edi # 将 rbp - 8 地址处的内存值写入 edi 寄存器,即 1 写入 edi 0x100003f50 <+32>: movl -0xc(%rbp), %esi # 将 rbp - 12 地址处的内存值写入 esi 寄存器,即 2 写入 edi 0x100003f53 <+35>: callq 0x100003f10 ; sub at main.c:15 0x100003f58 <+40>: movl %eax, -0x10(%rbp) 0x100003f5b <+43>: movl -0x10(%rbp), %esi 0x100003f5e <+46>: leaq 0x31(%rip), %rdi ; "the return value is:%d\n" 0x100003f65 <+53>: movb $0x0, %al 0x100003f67 <+55>: callq 0x100003f74 ; symbol stub for: printf 0x100003f6c <+60>: xorl %eax, %eax 0x100003f6e <+62>: addq $0x10, %rsp 0x100003f72 <+66>: popq %rbp 0x100003f73 <+67>: retq 我们重点讨论 a 和 b 两个入参的准备过程,对于 push,call 等,后面会再说。上面的 <+4> 到 <+32> 行是入参的栈填充过程,到了 <+32> 行,就开始正式调用子函数了。 首先,将 a、b 变量值写入栈的留置内存区域,而后将变量值从内存中读到寄存器中。这里的 edi 和 esi 寄存器就是用来传参的。 这里得到一种结论,即寄存器可以用来传递参数给子函数。 但寄存器的数量是有限的,如果需要传递非常多的参数,是无法都通过寄存器来解决的,比如下面的例子: 1 2 3 4 5 6 7 8 9 10 int sub(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o) { int t = a + b; printf("The sub value is:%d\n", t); return t; } int main(void) { int p = sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); printf("the return value is:%d\n", p); return 0; } 汇编如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 main: 0x100003ee0 <+0>: pushq %rbp 0x100003ee1 <+1>: movq %rsp, %rbp 0x100003ee4 <+4>: subq $0x50, %rsp # 预留 80 字节大小的栈内存空间 0x100003ee8 <+8>: movl $0x0, -0x4(%rbp) # 0 值写入,默认预留的大小空间,无特别场景,不会使用 0x100003eef <+15>: movl $0x1, %edi # 参数入 寄存器 0x100003ef4 <+20>: movl $0x2, %esi # 参数入 寄存器 0x100003ef9 <+25>: movl $0x3, %edx # 参数入 寄存器 0x100003efe <+30>: movl $0x4, %ecx # 参数入 寄存器 0x100003f03 <+35>: movl $0x5, %r8d # 参数入 寄存器 0x100003f09 <+41>: movl $0x6, %r9d # 参数入 寄存器 0x100003f0f <+47>: movl $0x7, (%rsp) # 参数入 栈内存 0x100003f16 <+54>: movl $0x8, 0x8(%rsp) # 参数入 栈内存 0x100003f1e <+62>: movl $0x9, 0x10(%rsp) # 参数入 栈内存 0x100003f26 <+70>: movl $0xa, 0x18(%rsp) # 参数入 栈内存 0x100003f2e <+78>: movl $0xb, 0x20(%rsp) # 参数入 栈内存 0x100003f36 <+86>: movl $0xc, 0x28(%rsp) # 参数入 栈内存 0x100003f3e <+94>: movl $0xd, 0x30(%rsp) # 参数入 栈内存 0x100003f46 <+102>: movl $0xe, 0x38(%rsp) # 参数入 栈内存 0x100003f4e <+110>: movl $0xf, 0x40(%rsp) # 参数入 栈内存 0x100003f56 <+118>: callq 0x100003e90 ; sub at main.c:10 0x100003f5b <+123>: movl %eax, -0x8(%rbp) 0x100003f5e <+126>: movl -0x8(%rbp), %esi 0x100003f61 <+129>: leaq 0x32(%rip), %rdi ; "the return value is:%d\n" 0x100003f68 <+136>: movb $0x0, %al 0x100003f6a <+138>: callq 0x100003f78 ; symbol stub for: printf 0x100003f6f <+143>: xorl %eax, %eax 0x100003f71 <+145>: addq $0x50, %rsp 0x100003f75 <+149>: popq %rbp 0x100003f76 <+150>: retq 可以发现,这边测试环境下,传参超过 6 个,就需要写入栈内存。超出的部分,就不能通过寄存器传参了。 在上面有 a 和 b 两个变量的例子中 (int a = 1; int b = 2; int p = sub(a, b);),数据虽然也写到了 -0x8(%rbp) 和 -0xc(%rbp) 栈内存中,但那是因为 a /b 两个变量当前函数需要使用,不得不写入栈内存,如果像下面这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int sub(int a, int b) { int t = a + b; return t; } int main(void) { int p = sub(1, 2); printf("the return value is:%d\n", p); return 0; } ############ main: 0x100003f30 <+0>: pushq %rbp 0x100003f31 <+1>: movq %rsp, %rbp 0x100003f34 <+4>: subq $0x10, %rsp 0x100003f38 <+8>: movl $0x0, -0x4(%rbp) 0x100003f3f <+15>: movl $0x1, %edi # 直接写入 edi 寄存器,不在写入栈内存 0x100003f44 <+20>: movl $0x2, %esi # 直接写入 esi 寄存器,不在写入栈内存 0x100003f49 <+25>: callq 0x100003ef0 ; sub at main.c:15 这里就很清晰了,1 和 2 两个参数,都是直接写入寄存器中的,不会再写到栈内存中。 对于 sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15) 的子函数调用,栈内存如下: 备注 1: 大家看到栈内存里面,从上往下的内存顺序正好和我们代码里面的顺序是反的,这是 Mac + X86 环境。不同环境下这个规则是不一样的,有些环境下先写的参数会在上面。 备注 2: 当前 7-15 的入参,是在当前函数栈里面存储的。即子函数执行完毕后,当前函数栈依旧保存这些入参。 子函数被调用 通过 call 指令即可完成函数帧的切换,即可以正常调用子函数了。上面例子中 callq 0x100003e90 ; sub at main.c:10 即表示子函数跳转。 在调用子函数前,需要完成两个工作: 首先是当前指令的下一行指令地址(即 ip 寄存器的值)需要存入栈内存,为了在子函数执行完毕后可以继续执行下去。 其次是需要将子函数的函数内存地址 (代码区) 放入 ip 寄存器,这样 CPU 才能去执行子函数。 而这两个操作都是在 call 指令中完成的,所以 call 指令实际上等同于下面操作: 1 2 pushq %rip movl <子函数内存地址> %rip 对于 sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15) 的子函数调用,栈内存如下: 这里再说明一下,为什么要先把 ip 程序寄存器的值写入栈内存,然后又写入新的值进去。 因为程序运行的原理就是晶震驱动下的指令不间断的执行。指令执行需要内存地址,就是从 ip 程序寄存器里面读的。 当执行到 call 指令的时候,ip 寄存器里面已经存储了下一条指令的地址,即 0x100003f5b。这里因为函数调转,不能执行下一条指令了,所以需要把 ip 寄存器先入栈内存暂时存储,即 pushq %rip。 入栈后,要去执行子函数,又需要把子函数的内存地址写入 ip 寄存器,这样 CPU 才会去执行子函数。即 movl 0x100003e90, %rip。 这里还有一个隐藏的点,即 rsp 寄存器变化了。原先指向入参0x7 的,这个时候就指向下一条指令的地址 ,即 0x100003f5b 位置。rsp 之所以能够变化,是因为 pushq %rip 指令会改变 rsp 的指向,下面会说到。 子函数执行中 这里就是重点了,rbp 和 rsp 有非常大的用途。它是函数调用栈的核心,也是理解协程的基础。 rsp 是栈顶内存指针,rbp 是栈底内存指针。当栈内存需要更多的空间的时候,rsp 会指向更小的内存区域以时刻指向栈顶。 一般来说 rbp 和 rsp 的差值,就是栈内存空间的实时大小。但这个不是绝对的,因为依靠编译器的实现,为了高效并不会绝对的控制 rsp 寄存器的值,即有可能出现栈内存实际大小大于 rbp 和 rsp 的差值。后面会提到这个 case。 其中 rbp 有三个非常大的作用,是函数调用栈的中流砥柱。 在 rbp 基础上增加值偏移,可以访问到父函数的栈内存数据(如入参)。 在 rbp 基础上减少值偏移,可以访问到子函数的栈内存数据(如局部变量)。 rbp 本身指向父函数的 rbp 的存储地址,用于子函数执行完毕后回到父函数时的 rbp 还原。 下面看下 sub 函数的汇编: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 int sub(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o) { int t = a + b; printf("The return value is:%d\n", t); return t; } sub: 0x100003e80 <+0>: pushq %rbp # 以上,将父函数的 rbp 值存入栈底 0x100003e81 <+1>: movq %rsp, %rbp # 以上,将当前函数的 rsp 值赋予 rbp,此时 rbp 是子函数的栈底 0x100003e84 <+4>: subq $0x20, %rsp # 以上,将 rsp 值减少 32 字节偏移,开辟栈预留内存空间 0x100003e88 <+8>: movl 0x50(%rbp), %eax 0x100003e8b <+11>: movl 0x48(%rbp), %eax 0x100003e8e <+14>: movl 0x40(%rbp), %eax 0x100003e91 <+17>: movl 0x38(%rbp), %eax 0x100003e94 <+20>: movl 0x30(%rbp), %eax 0x100003e97 <+23>: movl 0x28(%rbp), %eax 0x100003e9a <+26>: movl 0x20(%rbp), %eax 0x100003e9d <+29>: movl 0x18(%rbp), %eax 0x100003ea0 <+32>: movl 0x10(%rbp), %eax # 以上,根据 栈底 rbp 做增加值偏移,获取父函数的栈内存数据,即入参 0x100003ea3 <+35>: movl %edi, -0x4(%rbp) 0x100003ea6 <+38>: movl %esi, -0x8(%rbp) 0x100003ea9 <+41>: movl %edx, -0xc(%rbp) 0x100003eac <+44>: movl %ecx, -0x10(%rbp) 0x100003eaf <+47>: movl %r8d, -0x14(%rbp) 0x100003eb3 <+51>: movl %r9d, -0x18(%rbp) # 以上,将入参寄存器的值存入当前栈内存空间,做减小值偏移 0x100003eb7 <+55>: movl -0x4(%rbp), %eax 0x100003eba <+58>: addl -0x8(%rbp), %eax # 以上,完成 a + b 操作 0x100003ebd <+61>: movl %eax, -0x1c(%rbp) # 以上,将 a + b 的结果,存入栈内存空间 0x100003ec0 <+64>: movl -0x1c(%rbp), %esi 0x100003ec3 <+67>: leaq 0xd4(%rip), %rdi ; "The return value is:%d\n" 0x100003eca <+74>: movb $0x0, %al 0x100003ecc <+76>: callq 0x100003f7e ; symbol stub for: printf # 以上,调用 printf 函数开始打印 a + b 的值 0x100003ed1 <+81>: movl -0x1c(%rbp), %eax 0x100003ed4 <+84>: addq $0x20, %rsp 0x100003ed8 <+88>: popq %rbp 0x100003ed9 <+89>: retq # 以上,是子函数执行结束后的父函数还原,下一趴说 汇编注释做了说明,主要就是 rbp 值和 rsp 值的变化,如下所示: 这里有一个点,从汇编里面不太容易看出来 rsp 的变化。在子函数没有调用之前,rsp 的值是指向 main 函数栈顶的 p: 0x100003F5B。在调用 pushq %rbp 将父函数的 rbp 值存入栈底后,rsp 的值就已经变化了。因为 push 实际上是完成了两个工作,如下: 1 2 3 4 5 6 pushq %rbp = subq $0x8, %rsp movl. %rbp, %rsp 这两个步骤,一个将 rsp 减大小偏移 8 个字节,一个将 父函数的 rbp 值写入到偏移后的栈内存里面。 然后执行 movq %rsp, %rbp 是为了将 rbp 置为当前子函数栈的栈底,即此时 rbp = rsp。到这里,子函数栈的两个重要参数 rbp 和 rsp 就已经设置好了。 再往后就开始预留栈内存空间了,subq $0x20, %rsp,预留内存空间的方式就是将 rsp 做减大小偏移。 有一个注意点,就是预留大小并不一定非要 rsp 做减大小偏移,其实只要能够持续稳定的访问到一个栈内存地址即可,如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int sub(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o) { int t = a + b; // printf("The return value is:%d\n", t); return t; } `sub: 0x100003e90 <+0>: pushq %rbp 0x100003e91 <+1>: movq %rsp, %rbp 0x100003e94 <+4>: movl 0x50(%rbp), %eax 0x100003e97 <+7>: movl 0x48(%rbp), %eax 0x100003e9a <+10>: movl 0x40(%rbp), %eax 0x100003e9d <+13>: movl 0x38(%rbp), %eax 0x100003ea0 <+16>: movl 0x30(%rbp), %eax 0x100003ea3 <+19>: movl 0x28(%rbp), %eax 0x100003ea6 <+22>: movl 0x20(%rbp), %eax 0x100003ea9 <+25>: movl 0x18(%rbp), %eax 0x100003eac <+28>: movl 0x10(%rbp), %eax 0x100003eaf <+31>: movl %edi, -0x4(%rbp) 0x100003eb2 <+34>: movl %esi, -0x8(%rbp) 0x100003eb5 <+37>: movl %edx, -0xc(%rbp) 0x100003eb8 <+40>: movl %ecx, -0x10(%rbp) 0x100003ebb <+43>: movl %r8d, -0x14(%rbp) 0x100003ebf <+47>: movl %r9d, -0x18(%rbp) 0x100003ec3 <+51>: movl -0x4(%rbp), %eax 0x100003ec6 <+54>: addl -0x8(%rbp), %eax 0x100003ec9 <+57>: movl %eax, -0x1c(%rbp) 0x100003ecc <+60>: movl -0x1c(%rbp), %eax 0x100003ecf <+63>: popq %rbp 0x100003ed0 <+64>: retq 上面的 sub 函数,注释掉 printf 子函数调用。这个时候,就不会有 subq $0x20, %rsp 和 addq $0x20, %rsp 两个 rsp 减大小偏移的操作了。因为 sub 就是当前函数调用栈的最后一个子函数,它可以保障当前函数执行过程中的栈数据是稳定可靠的,因为不会再有子函数来打扰它。但是 sub 函数依旧可以通过 -0x4(%rbp) 方式访问当前栈内存空间。 所以前面说过,rbp 和 rsp 的差值,不一定是栈内存空间的大小。上面的例子里面,rbp = rsp,即差值为 0,但当前栈内存依旧保存有 6 个寄存器的入参。 子函数执行结束后 快要接近尾声了。 子函数需要把返回值返回到父函数,是通过 eax 寄存器实现的。 1 2 3 4 5 6 7 8 9 10 11 12 sub: 0x100003eb7 <+55>: movl -0x4(%rbp), %eax 0x100003eba <+58>: addl -0x8(%rbp), %eax # 以上,完成 a + b 操作 0x100003ebd <+61>: movl %eax, -0x1c(%rbp) # 以上,将 a + b 的结果,存入栈内存空间 0x100003ed1 <+81>: movl -0x1c(%rbp), %eax # 以上,从栈内存空间中取出返回值,存入 eax 寄存器。父函数通过eax 拿到子函数的返回值。 main: 0x100003f5b <+123>: movl %eax, -0x8(%rbp) # 父函数拿到 eax 返回值,目前是写入栈内存。也可以直接使用,看代码逻辑决定。 目前子函数快要执行完毕后,参考上面的汇编,最后还需要执行三个指令: 1 2 3 4 0x100003ed4 <+84>: addq $0x20, %rsp 0x100003ed8 <+88>: popq %rbp 0x100003ed9 <+89>: retq # 以上,是子函数执行结束后的父函数还原 其中做了这么几件事情: 还原父函数的 rbp 还原父函数的 rsp 使得父函数继续执行下去 其中 pop 和 ret 也有多个意图,如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 popq %rbp = movl %rsp, %rbp addq $0x8, %rsp ------------ retq = movl %rsp, %rip addq $0x8, %rsp 可以发现,pop 和 ret 都会操作 rsp 寄存器,将 rsp 寄存器的值做增大小偏移,从而还原到父函数的 rsp 状态。 最后将 call 指令存储到栈内存的下一行指令地址写回到 ip 寄存器,使得 CPU 从子函数调用处继续执行下去。 有栈协程如何实现 函数调用的过程,核心在于维护一个子函数的栈,在这个栈里面,会同样使用父函数 (父栈) 已经使用过的寄存器等硬件而互不干扰,还会将父栈必要的信息给予保留,如 rbp。 这样的调用流程,使得子函数完全执行完毕,最后执行 ret 指令后,回到父函数并继续执行下去。 而协程的实现,可以在父子函数之间不停的跳跃,它的实现依旧可以通过函数调用栈来完成。 如果要实现这种效果,需要下面几个点要先解决: 两个函数不停跳跃,一定有一个地方实现了 ret 指令。这个 ret 指令实现的地方,也就是 yield (一般都通过这个来让出当前协程) 实现的地方。 yield 无法在当前函数中实现 ret 指令。因为 yield 如果直接表达了 ret (yield 汇编结果),那么 yield 就具有和 return 一样的效果,即当前函数意义上执行完毕了。 所以,yield 或许可以被实现为一个中间函数。这个中间函数内部实现 ret 指令,并强制跳转到另一个函数处。 这样的设计,就可以让原先的两个函数不受任何影响,完全被中间函数来间接的完成跳转。像下面这样: 在进入 yield 之前,先回顾一下上面提到的函数栈中使用的汇编指令。因为其中有一个关键寄存器 rsp 需要重点说明下。用最简单的一个例子来看: 1 2 3 4 5 #include <stdio.h> void test(void) { } int main(int argc, const char * argv[]) { test(); return 0; } 这里 test 函数的汇编如下: 1 2 3 4 5 TestProject`test: 0x100003f70 <+0>: pushq %rbp 0x100003f71 <+1>: movq %rsp, %rbp 0x100003f74 <+4>: popq %rbp 0x100003f75 <+5>: retq 在 main 里面进入 test 函数后,什么都没有执行,汇编非常简洁。这里说一下 rsp 为什么非常重要: rsp 在当前栈向下增长的过程中,表示当前栈的内存空间。 rsp 代替 rbp。rbp 是栈底寄存器,上面说到可以对它增减偏移以连接父函数和子函数。rbp 也是可以通过 rsp 来间接获取的。上面子函数执行结束后章节中有一个汇编 addq $0x20, %rsp,就是将 rsp 移动到 rbp 的位置。可以在使用 rbp 的时候,通过 0x50(%rbp) 来间接拿到栈底点。 rsp 决定最后的回跳地址。ret 指令执行的过程,是对 rsp 操作的。详见上面的 movl %rsp, %rip & addq $0x8, %rsp 所以,如果在 yield 中间函数的设计中,我们若能够巧妙的设置 rsp,就可以让当前 yield 回跳到任意需要的位置,从而实现上面图中的协程效果。比如下面的伪码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 TestProject`yield: 0x100003f70 <+0>: pushq %rbp 0x100003f71 <+1>: movq %rsp, %rbp // 上面两个用于将父函数 rbp 存储,当前 rsp 置于栈底,即操作 rsp 和操作 rbp 等价。 mov %rsp, addressM // 将父函数的 rbp 暂存 M 地址中。通过这个缓存的 M,可以拿到父函数的回跳地址。(rbp - 1 位置存储回跳执行地址,IP 程序计数器的值。) mov addressN, %rsp // 将 rsp 修改为新函数的钩子地址(需要特别配制) 0x100003f74 <+4>: popq %rbp 0x100003f75 <+5>: retq // 这时候,pop %rbp 会将钩子地址处的相关数据设置到 rbp,作为新的栈底。 // ret 会将钩子地址处的相关数据设置为 IP 程序计数器并跳转过去 // rsp 已经在上一步被钩子地址填充好 // 函数跳转的三要素,基本都配置完成了,坐等跳转发生。 如上面注释里面说明的,只需要有一个巧妙的数据,这份数据能够被 pop rbp 和 ret 的执行逻辑获取到。那么,只需要控制 rsp 以指向这份巧妙的数据,就可以实现自定义跳转。 在刚进入 yield 的时候,rsp 和 rbp 都在栈底,相互可以等价操作。而我们配置的这份巧妙数据,只需要存储当前 yield 栈中的 rbp,就可以在以后的某个时刻重新设置给 rsp 用于回跳 (rbp-1 即回跳地址)。如下图所示: 这里,找一个全局内存区域 custom_point,用于存放每次 yield 时候 rsp 数据。 每次跳转的时候,yield 先备份当前的 rsp 为 _rsp,然后从 custom_point 中取出老数据并设置给 rsp,然后再将 _rsp 存储进 custom_point,最后坐等 ret 跳转发生。 (第一次跳转的时候,需要预先将待跳转函数的地址放进去,不然没数据) 下面分步介绍一下(假设图中第一次跳转已经完成,那时候需要初始化数据,还需要一些逻辑代码): 图中第二步,test () yield 到 main () 的时候,yield 拿到 test () 的 rbp 并存储。(rbp -1 即这一次的回跳地址,图中的 ... 部分) 图中第三步,main () yield 到 test () 的时候,yield 拿到先前存储的 test () 的 rbp 并设置到 rsp 里面,ret 的时候直接跳转到 ... 部分 如此,就可以不间断的完成两个函数之间的跳转。这也是有栈协程的方案。 原来,取蚕丝的时候,需要把蚕宝用热水杀死在茧内,以获得完整的丝。 蚕宝不会变成飞蛾了。

2023/12/24
articleCard.readMore

在 Xcode 中更好的使用 Swift

在 OC 时代,通过 .h 头文件以及 include 标记,还有那长长的 api 名称,可以很方便的意会和找到某个类。使用 Xcode 十年,在 Vim 的加持下,基本没用过啥快捷键。 最近在 Swift 里面一顿拾掇,理解源码也太复杂了。 1 个 class 在 3 个文件中增加了 5 个扩展并实现了 4 个协议,1 个文件中包含 400 行注释 500 行代码 10 多个杂七杂八的 enum/struct/class/extension/protocol,去他妈的。 一直都是夸奖 Swift 的,作为后现代语言,集百家长补百家短。 但不好的编程习惯对 Swift 来说会是灾难,文件名不在作为当前文件内容的强约束、无需 import 后的 extension 可以写在任意位置、enum/struct/class 是值类型值语义引用类型引用语义搞不愣清后的混用,等等,这些都会骤然增加 Swift 源码的理解。 不得已对 Xcode 进行一些调教。 目标是快速查阅继承、搜索、实现协议方、三方库文档等代码阅读操作。 0x01 搜索快捷治理 vscode 的搜索是出奇的快和方便查阅。很喜欢做的一件事情,是把公司几百个仓库 down 下来,通过 vscode 搜索要抄的作业。这时候通过搜索简短的注释级别的语意,就能找到期望的代码片段,AI 都得落泪。 对于 xcode,感觉就像老人,多操作几个功能就菊花圈转啊转的。搜索符号、文本的时候也经常有卡顿。可能是电脑太差了吧。 最近把搜索切到快捷键后,感觉快了很多。这应该是不用鼠标操作 UI,避免了主线程的卡顿。xcode 中主线程易卡死,最烂 IDE 不是吹的。 新增的快捷搜索如下: 1 2 3 4 5 6 ⌥C -> Find Call Hierarchy ⌥A -> Find Ancestor Types ⌥C -> Find Conforming Types ⌥D -> Find Descendent Types ⌥S -> Find Selected Symbol in Workspace ⌥T -> Find Selected Text in Workspace 在 setting - Key Bindings - filter【find】进行修改,使用 Option 做前缀,这样不会产生快捷键冲突。后缀使用语意单词的首字母,方便记忆。 在需要搜索的单词上按下快捷键,就可以切到 搜索 区域展示搜索结果了(无需全选单词,xcode 默认会做单词内容全选)。 对其中的几个搜索做下介绍: Find Ancestor Types: 展示当前 class 的逐个父类 (祖先类)。其中,协议也属于祖先类,会一并展示。 Find Descendent Types: 和上一个相反,展示子类。不过无法展示协议的实现类。和上一个有差异。 Find Conforming Types: 展示当前协议的实现类。其中若 A 协议继承 B 协议,搜索 B 也会将 A 展示出来。 Find Selected Symbol in Workspace: 根据符号进行搜索,相比文本搜索会更准确。如 函数、属性 的搜索,直接展示所有使用方。 这几个快捷键,基本覆盖找代码过程中的大部分场景。里面还有一些小技巧,如前后缀及单词匹配、大小写敏感、展示方法和属性等。 缺陷 对于 Procotol,若 ProtocolB 继承 ProtocolA。无法通过以上 Find 操作从 ProtocolB 找到其祖先协议 ProtocolA。这是 Xcode 的缺陷。 这个有一个补救的办法,是通过下面说到的 DocC 来查阅。DocC 文档的末尾,一般有一个 Conformed to 列表,用于标记当前 Procotol 实现了哪些祖先 Protocol。 技巧 1 Xcode Index 对于 ⌥T 文本搜索,任何条件下使用都没有问题。但是其他的搜索如符号、祖先等,就需要 xcode 先解析项目生成 Index 索引,然后才能通过读取 db 缓存来使用。 这里的解析项目,是不需要编译通过的。只需要 Xcode 把项目的词法分析操作完即可 (默认打开项目就会解析)。 但这里也会有一个小问题,就是多个 Group 下有大量无法编译通过的文件,Index 索引生成会失败。 这时候可以通过新建 Target,将文件添加到不同的 Target 中。可以解决 Index 索引失效问题。 技巧 2 Scheme 去除干扰 如 Xcode Index 中描述,对于有些项目,如 github 源码、自己建立的测试工程等,某些原因下可能无法编译通过。 这个时候项目里面会有很多 error,使用搜索的时候,会有很多刺眼的红色警告条干扰。 这个时候可以操作 Manager Schemes - show,将所有 scheme 取消勾选。因为 xcode 已经把项目解析完成,搜索依旧是可以使用的。这样可以有效的去除警告干扰。 技巧 3 Search Scopes 巧妙设置 Scopes,可以大范围缩小部分文本的搜索范围。搜索范围可以设置为部分主项目文件夹和任意三方库的关联。 有个小技巧,是按着 Control 选中需要搜索的文件夹或者三方库,然后右键,会有一个快捷添加 Scopes 的入口。 技巧 4 正则搜索 配合 ChatGPT 写正则搜索,也非常棒。如果需要对正则结果使用文本替换功能 (replace),最好在 vscode 里面操作,效果会更好。 0x02 Swift 官方源码 上面说到的搜索,仅仅对于有源码的当前项目或者三方库进行使用。Swift Foundation 等模块,只能通过其生成的 api 接口来预览。 这个时候弊端非常明显,就是很难查找当前一个系统类的父子及实现协议之间的关系 (如:想要查看 Array 实现了哪些 Protocol 以及父子类)。 因为 Swift 是开源的,这里一个办法是直接获取 Swift 源码。具体方法可以参考这篇文章。swift-5.9-RELEASE 源码编译(Xcode) 整套流程非常耗费时间和磁盘大小 (62.5G),若非查看 cpp 实现源码,可通过 lite 版本进行 swift 源码查阅 swift5.9.2_rawcode_lite。 当然,cpp 源码包含非常多的有价值内容。如 LLVM 项目中有整套编译器和平台架构的实现。Swift 项目中有对 ELF/Mach-0 Image 等详细操作流程的实现。 0x03 使用 Assistant 视图 在编辑窗口使用默认快捷键 ⌃⌥⌘↩ 可打开 Assistant 视图。在做 Storyboard/SwiftUI 等 UI 面板开发的时候经常用里面的 auto 功能以打开对应源码文件。 其中还有一些如 Procotol/extension/superclass 等,毕竟方便展示当前打开页面的清单。 尤其里面有一个 interface 能力,可以展示当前 swift 源码对应的头文件。这样可以边看源码边看接口定义,非常方便。 0x04 首选 DocC 文档 苹果开放了 DocC 能力,可以快速输出任意项目 / 三方库的接口文档。具有和官方文档一样的体感,非常好用。 DocC 文档可通过 ⇧⌘0 快捷键快速体验。通过 Product - Build Documentation 可快速输出项目接口文档。 0x05 Bookmark 新版 xcode (15) 增加了书签 Bookmark 功能。可以在文本任意行右键找到入口,在搜索面板的左侧找到书签总入口。 以前,都是通过打断点然后把该断点设置取消状态的方案,在断点总入口里面刻意做到标记。把断点当书签用。 现在有了单独的书签,展示上也更加友好。这些 IDE 必备的功能,虽迟但到吧。 0x06 加餐:vim xcode 自建了 vim 的支持。vim 对中文场景一直有诟病,切换输入法的瞬间秒变灾难。vscode / 终端 均是如此。 但是 xcode 完美支持中文。在普通状态中文场景下,按键会自动表意为英文并执行对应的命令,不会变灾难。 xcode 是我见过的对 vim 支持最友好的 IDE。 如今,我经常中文写作,对于 md 文档也希望通过 xcode 来管理。我的方案是这样的: 建立一个命名为 EditBlog 的 workspace 和 library,这样可以提供一个最小化的 xcode 项目模版。 将 md 文档的文件夹设置 alias,并将 alias 放置于 EditBlog 项目中。在 Xcode 里面操作 add files to EditBlog (不选中 target,不选中 copy,使用 folder reference)。这样就完全将 md 文档接入到 xcode 工程里面了。 如果误操作了 xcode 的什么项目快捷键,当前工程也不会有什么错误响应。最主要的是什么时候想写字了,就即可。 优点: 实时同步。xcode 里面可以实时创建新 md。若外部创建了新 md,xcode 里面也会同步展示出来。(不选中 target,不选中 copy,使用 folder reference) 打开项目方便。打开 xcode,通过 recent 打开 EditBlog 项目即可。 快捷键误操作,不会触发什么 error。因当前 xcode 项目是最小化的模版,且 md 没有链接到 target 中。 自然增长即无刻意操作情况下,自然能达到的数量级。 一个千万级日活的 app,新增一个重要的功能入口。 那么如何评估这个功能的数据是否优秀,不能看 UV 或者 DAU。即使什么都不做,这个量级的用户基数也会有上百万的点击。 这对国家层面数据指标的鉴别也有重要参考,因为这里的基数更大。正向数据表达的可能是增长,也可能是降低。

2023/12/15
articleCard.readMore

通过 Swift Package 制作二进制库

Swift Package Manager (SPM) 已经被苹果放置于一个很重要的位置,在历史文章中对其做了一定的分析,Package 目前的定位和 Xcode Project .xcodeproj 同级别。SPM 不仅仅用于替代 Cocoapods,而是 Apple 后期语言研发生态的一部分。 鉴于 Xcode 对 Package 的支持,可以很方便的将项目中的代码进行组件化设计,做一定的逻辑分析即可拆分成多个 Package,这在开发过程中非常有利于项目的架构、单测、可持续性。 以前若这样做,需要对 Cocoapods 有深入的了解,这是一个比较复杂且细节的过程。通过 SPM 只需要 New - Package 即可,将复杂度从项目级别压缩到文件级别。 SPM 出现后,很多 Pods 模块通过增加 Package.swift 配置文件可以很方便交由 SPM 管理。但对于直接通过 Package.swift 创建的独立库,并没有方便的方案转为 Pods 管理,还是需要走一遍 Pods 的流程。 不过,这里做一个预言,Pods 终将被 SPM 取代,因为 SPM 是更具有可持续性和生态深度耦合,在 Xcode 整合 (编辑 / 搜索 / 联调等)、多平台兼容,源码管理、二进制库 / 仓库管理、CI 等方面,都具有得天独厚的优势。 相比 Pods 完善的脚本自定义能力 (基于 ruby 的生态),SPM 是有一定的短板,但不推荐。 很多大公司在做统一基建的时候,会深度魔改 Pods。简单的东西越做越复杂的原因,除了增加一些” 又不是不能用” 的功能,还有就是在复杂度提升后打的各种补丁。 虽然这么说,Pods 完善的自定义能力,SPM 也一样可以做到,毕竟这些能力很大部分都属于扩展能力,如插件。执行流程中大家都可以在 xcodebuild 等相关命令的任意位置,随时可以写一些定制脚本做插入执行。 初期,Swift Package 只能做源码 (开源) 共享,后面增加了 binaryTarget 能力,可以在提供了二进制库 (framework/xcframework) 的情况下,直接通过 SPM 做分发。(闭源共享) 但二进制库从哪里来,普遍的方案还是通过 .xcodeproj 的形式编译导出 framework/xcframework 库。 Swift Package 虽然支持导出动态库和静态库,但流程上还不彻底,并不能直接交付使用,下面对此做一些解释说明。 0x01 闭源 framework 说明 在描述 Swift Package 制作二进制库之前,先简要描述下苹果生态里面闭源二进制库的设计。 初期,大家都使用 .a 制作静态库,因苹果生态管控严格,除系统库可使用动态库外,开发人员是不允许在上架项目中使用动态库的。 后面因为桌面小组件 (Widget) 等功能的开放,苹果没有开放多进程能力,而是通过 .framework 动态库在主应用和小组件之间进行通信。即没有在主应用中通过子进程开发小组件,而是小组件本身就是独立 app。 同期,.framework 也支持封装静态库,即可以不用 .a 了,通过 .framework 可以同时支持 静态库 和 动态库。 再往后,随着 Swift 的发布(OC-Swift 混编)、Widget 功能升级,通过对 .framework 进行签名 (不允许通过 dlopen 动态调用动态库) 并内置到主应用中的方案一直持续到现在。 这中间,Apple 也设计了 xcframework 用于代替 fat framework,以方便兼容 Apple 所有产品线的研发设计。 当前还有 .tbd/.tdlib 等动态库类型,更多用于系统库或者 Mac 可执行文件等平台,需要将库防于特定文件夹位置,这里不多介绍。 整体来说,时至今日,.framework 就是苹果生态里二进制分发包的表现形式。 Xcode 里的 xcworkspace 是个大管家,可以非常方便的将各个 project/target 进行组织和联动,其核心也是在处理不同 project/target 产出的 .framework 包。(Pods 是自动化的接管 xcworkspace) 至此,可以看下 .framework 里面都有啥。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 . ├── Headers │   ├── ProjLibrary-Swift.h │   └── ProjLibrary.h ├── Info.plist ├── Modules │   ├── ProjLibrary.swiftmodule │   │   ├── Project │   │   │   └── x86_64-apple-ios-simulator.swiftsourceinfo │   │   ├── x86_64-apple-ios-simulator.abi.json │   │   ├── x86_64-apple-ios-simulator.swiftdoc │   │   └── x86_64-apple-ios-simulator.swiftmodule │   └── module.modulemap ├── ProjLibrary └── _CodeSignature └── CodeResources Info.plist: 信息文件,存储适用平台、打包环境等信息 binaryName (ProjLibrary): 用于和其他项目做链接 / 动态绑定的库 (静态 / 动态),是核心产物。 CodeSignature: Apple 严格规定所有的动态库都需要签名,以防止动态下发动态库后任意执行。 Headers: 这是 OC 时代的头文件。让其他项目知道当前库有哪些接口,IDE 也会根据这些头文件给予自动化提示。 Modules: module 化推出很多年了,一直都是不温不火。swift 推出后,对库进行 module 化是必要前提 (默认)。编译会更高效,OC 环境下可以使用 import 导入 module 化的库 (非 include)。 Modules/xxx.swiftmodule: Swift 的头文件。上面说到 Headers 是 OC 的头文件,这里是 Swift 的。Headers 里面如果有 N 个公开头文件,就需要 N 个 .h 输出。Swift 只需要这里列出来的几个文件即可。IDE 会进行解析。 对于库来说,只要有二进制产物即可,即上面的 binary (ProjLibrary)。但若给其他人使用,还需要单独提供 api 文件。 苹果就将 api 和 binary 合并在一起,还可能封装图片等资源库,一起放在了 framework 里面。用于交付给他人使用。 0x02 Package 输出 Binary 卡点 通过 new - project - framework,可以很方便的建立一个 .xcodeproj framework 工程并打包。 这里我们说的是通过 Package 打包 framework(new - package),毕竟如前面说到的,Apple 很重视 Package 并将其提高到了 xcodeproj 同级别。Package 已经自成生态,而不仅仅是和 Pods 做功能对其。 Swift Package 天然支持动静态库,这是一开始推出的时候就和 Pods 做对齐的能力。这在 Xcode 高度集成的生态里非常好用,如下图: Package 可以放在很多位置,以和主项目配合。Xcode 会自动进行 .framework 中 头文件 和 二进制库 的链接工作。 但如果想从 Package 直接输出闭源的 .framework 以供其他项目使用,就不太方便了。准确来说,是至今 (23 年底) Apple 还没有做这一块的支持。 以下是对 Package 直接进行 Xcode Build/swift build <-c release> 等方式,导出 framework 的结果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Build/Products/Debug-iphonesimulator/ ├── MyLibrary.o ├── MyLibrary.swiftmodule │   ├── Project │   │   └── x86_64-apple-ios-simulator.swiftsourceinfo │   ├── x86_64-apple-ios-simulator.abi.json │   ├── x86_64-apple-ios-simulator.swiftdoc │   └── x86_64-apple-ios-simulator.swiftmodule └── PackageFrameworks └── MyLibrary.framework ├── Info.plist ├── MyLibrary └── _CodeSignature └── CodeResources 需要的核心二进制文件所处的路径为:PackageFrameworks/MyLibrary.framework/MyLibrary,但是头文件并不在 framework 里面,而是和 PackageFrameworks 平级。 这样就使得 framework 并不能直接交付,因为其他项目没有头文件的 api,无法使用。 以上是 Package 的产物配置为 动态库 的结果。如果是 静态库,则更加严重: 1 2 3 4 5 6 7 8 9 Build/Products/Debug-iphonesimulator/ ├── MyLibrary.o ├── MyLibrary.swiftmodule │   ├── Project │   │   └── x86_64-apple-ios-simulator.swiftsourceinfo │   ├── x86_64-apple-ios-simulator.abi.json │   ├── x86_64-apple-ios-simulator.swiftdoc │   └── x86_64-apple-ios-simulator.swiftmodule └── PackageFrameworks <-empty 并没有输出 framework,只剩下已经编译好的 .o 目标文件。 其实,若 Package 按上图中直接集成项目中,输出的产物也是这样,只是 Xcode 默认会进行目标文件的链接或者头文件查询等工作。 只是当独立进行 Package framework 输出的时候,上面两个问题官方是没有给予支持。 0x03 Package 输出 Binary 方案 对于 Package 输出 动态库 framework,目前的问题是头文件不在 framework 里面,无法直接给其他项目使用。 前面我们已经对 framework 里的内容做了分析,Headers 是 OC 的头文件产物,Package 是明确不支持 OC 的,只能写 Swift 代码。所以不用关心。 我们只需要把 swiftmodule 头文件夹,按照 framework 的标准复制到 PackageFrameworks/MyLibrary.framework/Modules 里面即可。Modules 里面的 module.modulemap 在这里也不需要了,它主要是为 OC module 化服务的,如 sub module 等。 所以,如果是 xcode 打包,我们可以在 scheme 的 Build Post-Actions 或者 project 的 Build Phases Run Script 中加入一些脚本,把 swiftmodule 复制到 .../xx.framework/Modules/ 里面,则 xx.framework 即可以作为动态 framework 给其他项目使用。 对于 Package 输出 静态库 framework,目前的问题是 framework 都没有生成,只有编译好的 .o 目标文件。 静态 framework 是对 .a 和 头文件的封装,一个可行的办法是通过 ar -crs libXXX.a XXX1.o XXX2.o 命令,将所有的 .o 进行压缩 .a 文件(需要移除 .a 后缀,framework 主动识别,不要 .a 后缀)。 然后把 .a 文件和 swiftmodule 放到新建的 xx.framework 中。 虽然可行,但也看得出来,这有些复杂。还有 sign/info.plist 文件没有处理,当然实际上它们也并不需要。 这个方案有些复杂,但是一个曲线救国的方案却很方便,就是 xcframework。 0x04 xcframework 简单介绍一下 xcframework,它是苹果生态里面最近几年提出来的多平台二进制输出方案。framework 是多架构二进制输出方案。 通过 xcode 或者 xcodebuild 打包的 framework,虽然看起来是一个文件,但其实是 fat framework,它里面可能封装了多个架构 (arm64/x86_64) 的二进制,以供不同架构平台 (真机 / 模拟器) 使用。 通过 file 命令可以看见其支持的架构: 1 2 3 4 5 6 7 8 file ...//Build/Products/Debug-iphonesimulator/ProjLibrary.framework/ProjLibrary // (Mach-O universal binary with 2 architectures 表示支持两个架构) ...//Build/Products/Debug-iphonesimulator/ProjLibrary.framework/ProjLibrary: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [arm64:Mach-O 64-bit dynamically linked shared library arm64] // (x86_64) ...//Build/Products/Debug-iphonesimulator/ProjLibrary.framework/ProjLibrary (for architecture x86_64): Mach-O 64-bit dynamically linked shared library x86_64 // (arm64) ...//Build/Products/Debug-iphonesimulator/ProjLibrary.framework/ProjLibrary (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64 如果提供给其他项目的 framework 只有 x86_64 架构,真机运行的时候,编译就会报错。同样,只有 arm64 架构的话,非 M 系列的 Mac 电脑的模拟器也编译不通过 (M1 等系列的模拟器已经支持 arm64)。 所以在以往,大家交付 framework 的时候,都需要手动或者通过 CI 的方式执行脚本,把 模拟器 / 真机 的独立 framework 进行合并,输出 fat framework 后做交付。 这些脚本是如何实现的,这里就不解释了,可以查询更多相关介绍。 需要注意的一点是,多架构还是多平台,和动态或者静态没有关系。framework 本身是 动态库 或者 静态库的封装,但它们都有 多架构 / 多平台 方案。 后面苹果的生态原来越多样和复杂,就推出了 xcframework 方案,即不在按照架构进行 framework 的整合,而按照 平台 来。这一套方案已经实施很多年了,这些年大家提供的包一般都是按照 xcframework 来,不用过多质疑。 具体实现也非常简单,就是 m.xcframework 是一个文件夹,里面的每个子文件都是对应平台的命名如 ios/iosimulator/mac 等,子文件夹里面是具体的单架构 framework 包。下面是一个示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 MyLibrary.xcframework ├── Info.plist ├── ios-arm64 <- iOS真机使用的 │   └── MyLibrary.framework │   ├── Info.plist │   ├── Modules │   │   └── MyLibrary.swiftmodule │   │   ├── Project │   │   │   └── arm64-apple-ios.swiftsourceinfo │   │   ├── arm64-apple-ios.abi.json │   │   ├── arm64-apple-ios.swiftdoc │   │   └── arm64-apple-ios.swiftmodule │   └── MyLibrary └── ios-arm64_x86_64-simulator <- iOS模拟器使用的 └── MyLibrary.framework ├── Info.plist ├── Modules │   └── MyLibrary.swiftmodule │   ├── Project │   │   └── x86_64-apple-ios-simulator.swiftsourceinfo │   ├── x86_64-apple-ios-simulator.abi.json │   ├── x86_64-apple-ios-simulator.swiftdoc │   └── x86_64-apple-ios-simulator.swiftmodule ├── MyLibrary └── _CodeSignature └── CodeResources 实际上,xcframework 并不管 x.framework 是不是真的拥有当前平台的架构,它是按照文件夹进行过滤的。如果编译的时候不符合,就报错这样子。甚至每一个具体的二进制文件依旧是包含多架构的 fat framework,xcframework 也不会过问。这些信息,也同样会在聚合的 Info.plist 里有标记。 所以这里有个注意点,即最好准确的输出每个二进制文件的架构并输入到对应的子文件夹中,若包含多架构,xcframework 的尺寸会凭空大很多。(一般是通过命令行操作,不会有误差。若通过 xcode 手动打包,得注意 目标设备不要选中 any ios devices simulator 等,这会打出多架构的包出来) 对于如何输出 xcframework,也提供了友好的命令,即 xcodebuild -create-xcframework -framework pathA/N.framework -framework pathB/N.framework -framework pathC/N.framework -allow-internal-distribution -output N.xcframework,这是把多个 framework 合并成一个 xcframework 的方式,具体可查阅更多资料。 在上一节说到曲线救国的 Package 输出 静态库 framework,就是巧妙的使用 xcodebuild -create-xcframework 命令。具体如下: xcodebuild -create-xcframework -library pathA/N.a -library pathB/N.a -library pathC/N.a -allow-internal-distribution -output N.xcframework 即待合并的目标文件使用 -library 标记,这样可以将多个 .a 文件聚合为 xcframework,而不用关心手动将 .a 转为 .framework 的复杂问题。(当然,这里还需要手动 / 脚本将 swiftmodule 复制到对应 framework 的 modules 文件夹中。) 0x05 Swift Package 输出 xcframework 正确方案 不要使用 Swift Pakcage 开发 framework。Swift Pakcage 还是应该做源码依赖的分发。 应该将工程转换成 xcode framework 工程,通过 xcodebuild 命令进行打包输出 xcframework。 可以自行增加这个 xcode 工程文件,也可以通过下面这个开源项目来实现: swift-create-xcframework https://github.com/segment-integrations/swift-create-xcframework 该项目,通过将 SPM 项目转化为 xcode proj 项目,从而实现 framework 的制作。然后通过脚本,将多架构多平台 framework 制作成 xcframework,并提供 zip 服务。 该项目有些时候会有一些小问题,但总体来说问题不大。 Carthage 输出 xcframework 对于非 Swift Package 工程,即 Xcode Project 工程,开发人员可以通过 xcode build lipo 等方案,自行输出 xcframework 包。 但也有一个比较友好、快捷的方案,即通过 Carthage 来实现。这个在 CI 场景下可能不太好用。如果是本地打包,倒是挺方便的。 0x06 小结 至此,说明了如何在 Package 环境下,快速输出二进制给到其他项目使用。 对于 fat framework 或者 xcframework 会不会增加线上包大小,不用担心。因为当前平台 & 架构的包,准备提交的时候以及 App Store (iOS/Mac) 均会进行过滤。准备提交的时候,若参杂 模拟器 包,会不允通过(一般 Pods/SPM 已经默认做了去除。若手动管理,可增加 build phases 脚本进行剔除)。同时包含 armV7 和 arm64,在提交审核后也会被苹果后台根据不同平台进行剔除,App Store 上看到的已经是去除冗余架构后的大小。 但是对于非 App Store 的包,就不一定了。iPhone 有巨魔等自签平台,量很少不用关心。在 Mac 平台上,自签的 dmg 为了适配 Intel 和 M 架构,会同时包含多个架构,这会增加包大小。当然也可以为不同平台提供不同的包,增加少许复杂度。 实际上闭源的 framework/xcframework 不仅仅用于输出二进制给到其他项目使用。 对于大型项目,在长年累积过程中,可能会有好几百个模块库。使用源码的话,会严重增加编译时长 (编译一次耗时 1H 不夸张)。 这时候将每个模块直接输出二进制库,对编译时长有质的飞跃。 现在不方便眨眼睛,请不要问太多。

2023/12/15
articleCard.readMore

Arc 浏览器 - 意外的惊喜

最喜爱 世界之窗,那个快如闪电的 Browser。 2025.05.18 更新: 时至今日,Arc 已经停止功能更新很久了,但还是我的主力浏览器。同时,Dia 也已经成为我的默认浏览器长达 1 个月了。 Dia 的体验很特别。Arc 的标签管理非常棒,Dia 主要用于已经熟悉的 url web 网页浏览。 实际上 Dia 每日的使用时间非常长,因为每日浏览的网页 url 都是固定和熟悉的。 每个人都有自己喜欢的浏览器,但每个人都不满意自己喜欢的浏览器。这是当今 Browser 之乱现象。 归根结底,在于浏览器内核太过复杂,没有推倒革新的可能。只能依托 webkit/blink 内核之上做层层补补的体感优化。 但千人千面,一个浏览器能够适配多个人的使用习惯已经很困难,更何况兼容上亿用户的心情。 也属实没想到,在早已过了 Browser 大拼杀的今天,还有 Arc 这样的黑马闯了出来。 已经重度使用两月有余,忍不住做一下使用过程中的体验和分享。 Arc 使用的还是 Chrome-Blink 内核,重在用户使用体感上的创新。 早期通过会员邀请制预约排号(目前已经完全开放),当时体验了半小时有余,放弃了,感觉不到又什么亮点。重新尝试后,发现了很多亮点。 应该还是使用的多了,就发现了很多早期没有注意到的功能。也侧面反映一个点,一开始使用有可能劝退。 希望分享可以降低初次使用的劝退门槛。 但也如开头所言,可能您最后并不喜欢也不选择 Arc,概率或许还很大。 大致对比 Safari:网页响应较慢,有时候菊花圈转的非常糟心。网页搜索功能超级糟糕。密码管理对接苹果生态,这非常棒。但随着 Chrome 有插件支持 Apple 密码同步,吸引力也不大了。虽然省电做的非常好,但我的电脑常年对接电源,无意义。 Chrome:速度很快,非常棒。操作很简洁。内存消耗过大。已打开网页不能很好的分组汇总,造成打开网页过多及紊乱。 其他 Chrome (Blink) 内核:和 Chrome 比没发现特别的优势,花里胡哨的功能较多,没有发现特别的吸引力。 Arc:保持了 Chrome 的速度,内存消耗不大。解决了多 tab 和多 window 的难题,使得已打开网页比较方便分组隔离。手势操作很简洁,花里胡哨的功能也比较实用。 对我而言,最大的吸引力是解决了多 tab 和多 window 难题。查阅资料较多的时候,往往打开 10-40 个网页,后面又不知道从哪个开始关,一头乱麻。 使用 Arc 后,从不再有这个困扰。因为 Arc 的网页浮窗设计,对于无关紧要的网页及时做了关闭,根本不会打开多余的 tab 和 window。 Browser 必备能力 这里是一些无关痛痒的必备能力,基本上只有 Safari 是个异类。 导入 Safari 和 Chrome 等浏览器的历史记录、收藏标签、密码等。 支持 Chrome 商店 网页打开速度快 内存占用低 双击选中文本后,ctrl+f 直接高光所有匹配文本,网页不发生 offset 滚动。(也只有 Safari,把文本搜索做的如此 low。) 实际上我一直在使用 Safari,因为 Chrome 内存占用太高了,电脑有些搞不来,偶尔用用。 至今怀念那个快如闪电的 世界之窗。 尽快关闭网页 以往动不动打开 10-40 个 tab,1-3 个 window。后面要关闭的时候非常痛苦,完全不知道哪些 tab 对应哪些内容,要一个一个的回刷,担心误关了。 在使用 Arc 两个月,常打开的网页 tab 不超过 10 个,一次新增 window 都没有。 做为浏览器,Arc 的目标不是打开网页,而是尽快关闭网页。这个反向的设计,是最大的推荐理由。 一般来说,打开陌生的新网页有三种途径: 常用网页打开新链接(如 Google) 陌生页面打开新链接 其他 app 通过默认 browser 打开链接 对于 1 场景,Arc 的设计是:被 Pin (favorite) 的网页(Google 一般加入 Pin),点击链接,默认弹出网页浮层。该浮层具有完整的网页体验,若发现不感兴趣,通过手势可及时关闭浮层。 对于 2 场景,按着 Shift 键后打开链接,和 1 的效果一样,即出现网页浮层。 对于 3 场景,Arc 提供了一个 Little 简化模式,并不在 Arc 主页面打开链接。Little 也是一个浮层,不感兴趣也可及时关闭。 所以 Arc 对于尽快关闭网页的设计,就是提供浮层,这种强视觉的方式,让用户尽快且方便的关闭不需要打开的链接。 网页全屏 各个浏览器基本上都在顶部有一个丑丑的标签栏,这会占据一定的网页视图空间。而且在网页打开过多后,只能看到 logo,识别网页内容的功能已经丢失。 更甚至,一般书签栏还占据一行,进一步减少了网页视图空间。虽然有快捷键打开和关闭,但是基本不会操作。 Arc 的改进,是引入了侧边栏。将 Pin (favorite)、书签栏、已打开 tab 等统一在侧边栏展示。 通过 ctrl+s 快捷键,可以打开关闭侧边栏。关闭后,当前网页就是真全屏状态,这实在是太棒了。 最神的是 ctrl+s 快捷键,这个即方便又不被使用的鸡肋快捷键,被用于这个功能,真是妙不可言。 Pin Arc 的另一个挖掘用户痛点,是书签栏 并不常用,常用的网页就那么几个,可能还不在书签栏。 所以在侧边栏上,顶部通过区块 Icon 的方式置顶了被 Pin 的网页。非常方便点击和识别,完全弱化了书签栏存在的意义,且十分有效和方便。 Chrome 和 Safari 也在尝试做这个,它们是把 Pin 的网页移动至 所有 tab 的最左侧,且通过颜色进行分组区分。 但他们鸡肋的两点是:不方便识别且不能关闭。被 Pin 的网页,Safari 只能长久打开,Chrome 关闭后就被移除不在 Pin 分组。 Arc 的设计是:随时可以关闭,降低内存消耗。再次打开,回到原始状态。 Split View (2024.04.10 更新) 在新的版本中,Arc 支持拖动的方式,指定两个网页在同一个 window 中展示 (up&down、left&right)。这对于外接大屏显示器的用户来说,是极度友好的刚需。 Space 为了让用户不创建多个 Window,以防止屏幕视图过多,干扰筛选,Arc 通过 Space 来间接实现 Window 功能。 Space 可以通过手势滚动进行切换,非常方便。 每个 Space 都是独立的,有独立的 Profile,也可以 copy 另一个 Space。 这就很方便做常用功能的拆分和隔离,如 Pin / 书签 等。 经常在 A space 做 chatgpt 和 google 的搜索,在 B space 也做 chatgpt 和 google 的搜索。 虽然可以通过快捷键来新建 tab,但是手势滚动一下,还是要比按下键盘舒服的多。 而且,新建 tab 那是新开网页,而切 space 那是切到已经打开网页。chatgpt 新开网页还是很慢的。 Max Ai 这是比较潮流的点,用的不多,但也有收益。 Arc 默认已经提供免费的 Ai 场景的应用,如鼠标移动到链接上,会提示当前链接的内容概要。 一个亮点是:在当前页面 ctrl+f 输入问题,Ai 会整理当前页面的内容作为语料,对输入问题进行解答。这很有趣和有用。 还有个亮点是:下载文件和新开 tab,Ai 会重命名文件名称或者 tab 名称,以更符合对应场景的含义。还很准,这也很有用。 月薪 3k 买 8k 羽绒服,远不如辞职卧床 3 个月。

2023/12/14
articleCard.readMore

【Swift 三方源码 1】SwiftShell 高效的命令行工具

不推荐使用 Swift 写脚本,和 Python 比起来,该生态链相对匮乏,开发耗时会增加很多。 但对于偏 Swift 的同学来说,这也的确是更可控和方便维护的方式之一。尤其对于公司内部工具,有问题可以更快的找到原因并处理,不至于手忙脚乱。 通过 Swift 进行脚本开发的环境和入门,可参考:Swift 脚本开发环境搭建 在开发脚本的时候,经常会使用到在终端环境中安装 / 预制的命令、管道、文件读取等。这些能力,SwiftShell 做了完备的封装,很方便使用。 这里对该库源码进行一些解读。同时,也会做前置知识点如文件、管道、描述符、子进程进行简单介绍。 前置 在进行源码分析之前,需要先对 Swift 中的 文件读取 和 进程 进行概要描述,这是 SwiftShell 重点依靠的系统能力。熟悉它们之后,可以更熟悉 SwiftShell 的 Api 到底接管和实现了哪些能力。 0x01 FileHandle Swift 中 Filehandle 属于 FileManager 这一类文件操作 api 的一部分。FileManager 一般使用场景是创建文件 / 文件夹,毕竟容易理解。不过 Filehandle 中有些知识点,还是先熟悉一下比较有益。 stdin/stdout/stderr 这三者即标准输入流 / 输出流 / 错误输出流,经常会听到,使用也比较简单,但若没有使用过,还是有些雾里探花。 这三个标准流,一般不需要设置,操作系统或者运行时环境会在启动应用程序的时候,默认进行设置。 其中,stdin 默认是键盘输入,stdout 默认是屏幕,stderr 默认也是屏幕。所以,使用 print 进行打印的时候会在电脑屏幕上进行展示,也可以在终端里面输入一些字符以和程序进行互动响应。 对于命令行可执行文件,当然依旧遵从该法则。 但是对于手机或者电脑里面的应用程序,默认就会关闭 stdout 和 stderr 了,因为这个时候已经离开调试环境,print 输出是没有必要的,更需要的是日志,而这需要磁盘存储。 当然,也可以在应用程序启动后,手动更改 stdout 的指向,从屏幕改为文件,这样就可以将 print 的内容输出到指定文件夹,可以这样设置:freopen(file,"a+",stdout) 对于系统的设计,有一个规则即一切皆文件,比如内存、键盘、缓存、网络 Socket 等。即只要是有字节输入输出场景的,都可以通过高层级的文件进行抽象和描述。 stdin/stdout/stderr 也不例外,实际上它们就是三个文件,文件描述符分别是 0/1/2。 下面通过不同的编程层级进行描述,详见注释: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 底层: unistd.h #define STDIN_FILENO 0 /* standard input file descriptor */ #define STDOUT_FILENO 1 /* standard output file descriptor */ #define STDERR_FILENO 2 /* standard error file descriptor */ > 这是底层 POSIX 接口,进行底层开发的时候会用到上面三个宏定义,根据后面的注释可知,0/1/2 是三个基础输入输出流的文件描述符,其他描述符不会暂用。 C 运行时:stdio.h extern FILE *__stdinp; extern FILE *__stdoutp; extern FILE *__stderrp; #define stdin __stdinp #define stdout __stdoutp #define stderr __stderrp > 这是 C 语言(高级语言)运行时提供的接口,进行 C 语言编写或者高层级语言编写的时候可以使用。这里把基础输入输出抽象为文件了,通过文件指针进行定义。 Objective-C:NSFileHandle.h @property (class, readonly, strong) NSFileHandle *fileHandleWithStandardInput; @property (class, readonly, strong) NSFileHandle *fileHandleWithStandardOutput; @property (class, readonly, strong) NSFileHandle *fileHandleWithStandardError; @property (class, readonly, strong) NSFileHandle *fileHandleWithNullDevice; > 这是 OC 语言 (高级语言) Foundation 库提供的接口,更高层级的开发过程可以使用,如应用、app开发。这里做了更高层级的抽象,即抽象为 FileHandle 对象。 Swift:FileHandle open class var standardInput: FileHandle { get } open class var standardOutput: FileHandle { get } open class var standardError: FileHandle { get } open class var nullDevice: FileHandle { get } > Swift 的 FileHandle 是对 OC 的桥接,所以基本一致。 这里有个小小的剧透,在可执行文件中执行终端命令的时候,默认是有输出的(可执行文件在终端运行,输出默认显示在屏幕 / 终端)。 而这可能不是我们想要的,因为我们可能希望拿到命令执行的结果或者根本不在乎命令执行的过程。 这个时候,可以在启动终端命令子进行的时候,手动设置其输出属性。 前者,可以提供一个自定义的 FileHandle 对象 (内存数据抽象的文件或者真实磁盘文件都可以),这样终端命令的结果就会输出到自定义的对象中,而后从该对象取数据即可。 后者,可以提供 nullDevice,将不会有任何输出。 当然,这里还有一个小提醒,线上程序最好是使用日志记录代替 print。这种场景下,操作系统虽然没有为输出设置为 stdout,即不会有显示,但 print 的用户态代码还是会执行的,只是到了内核态会找不到 stdout,不做输出而已。这会有性能损耗。 这里对 stdout 和 stderr 做下区别,即 stdout 用作输出,stderr 用作错误输出,这是不同的。 苹果平台用了很多年的 NSLog (ASL 日志系统) 就没有准确认清这两者的概念,NSLog 是使用 stderr 做输出的,不会走到 stdout。 这是不对的。后面改用了 oslog,但为时已晚,NSLog 已经铺天盖地了。所幸在 Swift 里面停用了 NSLog 的桥接。 文件读取和文件指针 FileHandle 类是对文件进行面向对象的描述,操作文件应该通过该对象进行。当然各个高级语言均有对应的操作 api,这里通过 swift/OC 的 Filehandle 进行介绍。 文件有最基础的三个访问属性:可读 / 可写 / 可读写,在 FileHandle 里分别对应三个初始化函数。(除此之外,FileHandle 也可以通过文件描述符进行初始化,文件描述符是一个文件的访问替身,其本身定义的时候,也需要设置这三个属性) 在成功访问了文件之后,还有一个重要的属性,即 seek (文件指针),它表示当前正在操作文件的哪个位置。初始化 FileHandle 对象后,默认位置是 0,即文件开头。 seek 在文件处理过程中非常重要,很多 api 的访问会默认修改 seek 的值。对于追加文件内容的场景,可能追加在不同的位置,这里就需要自行控制 seek。 对于可读场景,可以通过 readDataToEndOfFile api 直接读取整个文件的内容,过后 seek 会被修改为文件尾。 也可以通过 readData(length) api 读取部分内容,过后 seek 会做 length 大小的 增偏移。 之所以可以循环调用 readData(length) api 直至读取全部内容,是因为内部 seek 值一直在变化,这样 readData api 才知道从哪里开始读。 readLine 也是同理。 看起来,可读场景下开发人员对 seek 并不感冒,因为 api 内部做了很多工作。但是可写场景就不一样了。 对于一个有内容的文件,如果要追加内容,必须注意是否及时调用了 seekToEndOfFile api,提前将 seek 指向文件末尾。不然写的内容就可能错位了。 这里给一个该场景需要注意的例子,对于可读写的文件同时做读和写的操作,就需要时刻关注 seek 所处的位置,详见注释: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 这里通过文件描述符打开一个文件,设置可读可写属性 O_RDWR let fileDesc = open(path!, O_RDWR) let fileHandle = FileHandle(fileDescriptor: fileDesc,closeOnDealloc: true) // 下面通过 filehandle 对文件进行读取,追加写入后立刻从文件头按字节读数据 for i in 0...1000000 { // 写操作 try fileHandle.seek(toOffset: fileHandle.seekToEnd()) // 将 seek 移动到文件尾,以在尾部追加当前 i 到值(1/12/123/1234/...) fileHandle.write(String(i)) Thread.sleep(forTimeInterval: 0.5) // 读操作 try fileHandle.seek(toOffset: 0) // 将 seek 移动到文件头 print(fileHandle.readData(ofLength: 2 * i)) // 每次循环从文件头开始多读取 2 个字节。(1/12/123/1234/...) Thread.sleep(forTimeInterval: 0.5) } 通过一个 FileHandle 对象对文件进行读 & 写操作,还是有一定风险的。毕竟上面的示例只是最基础场景的应用。 所以一般都不这么做,而是对同一个文件设置 read 和 write 两个 fileHandle 对象,这样就可以分别操作互不影响。 通过 tail -f 查看实时日志就是这种场景,即一个 handle 只负责追加内容到文本。另一个 handle 只负责读取。 这个时候,有一个便利的 api,即 availableData。它可以读取 seek 至文件尾的内容,并自动将 seek 调整至文件尾。 这样只需要定时的访问 availableData,就可以实时读取到另一个 fileHandle 写入的新增内容。 当然,如果需要监听者模式的响应,可以不使用定时,使用 FileHandle 的 readabilityHandler: (@Sendable (FileHandle) -> Void)? api。 当文件被追加写入后,该回调就会被调用。这个时候在调用 availableData 拿数据,就非常顺理成章了。 Pipe 管道 对于通过两个 Filehandle 对一个文件分别进行读写的例子,就是 Pipe 管道。管道的定义很简单: 1 2 3 4 open class Pipe : NSObject, @unchecked Sendable { open var fileHandleForReading: FileHandle { get } open var fileHandleForWriting: FileHandle { get } } 提供了两个 FileHandle,这两个 FileHandle 返回的并不是同一个可读可写 handle,而分别是读 handle 和写 handle。 通过上面的介绍,应该对 Pipe (管道) 的作用更加清楚了吧。 就是 fileHandleForWriting 写入的内容,可以从 fileHandleForReading 读出,像一根管子的两端,实际上是文件的抽象,仅此而已。 0x02 Process (进程) 在 Shell 和进程 章节中,详细的介绍了进程和 Shell 的关系。 在可执行文件中调用终端命令,就需要创建子进程,并在子进程中运行命令,而后跨进程通信以拿到返回值。 基本原理详见 Shell 和进程 一文,这里仅对 Process 模块 api 进行介绍。 对于使用 OC 进行移动开发的同学来说,可能没有使用过 Process 模块,毕竟 iPhone 是不允许多进程的。甚至小组件等能力,也通过新的独立 app (扩展) 来实现,而不是在主应用中开放子进程接口。 简单来说,Process 是 Swift 进行多进程编程的实现,但是是阉割版。 实际上,Swift 依旧只提供了 GCD/Combine 这样的多线程能力,而没有开放多进程能力,即可以随意创建子进程并在子进程中执行任务。 Process 简单来说仅仅是为终端命令的执行提供了场所环境。其 api 也完全对应 终端命令 的参数格式。 接口比较简单,如下: 1 2 3 4 5 6 7 8 9 executableURL(同废弃字段 launchPath):终端命令的完整路径,如`/bin/pwd`,`/bin/ls`。 arguments:携带参数,如`-t`,`-v`,`--help`。 run(同废弃字段 launch):启动 process,开始执行命令。 waitUntilExit:和信号量的 wait 一样,等待命令返回。 terminationHandler:是 waitUntilExit 的监听者模式的实现。通过回调告知命令执行状态。 terminationStatus:当前命令的返回值,默认正常,即 0。如果命令返回 error,一般使用 -1 等非 0 值。 standardInput/standardOutput/standardOutput:前面介绍的三个标准输入输出及错误流。这里是从命令的视角来看的,即这三个字段的赋值代表数据的流向。可以为 standardOutput 指定一个磁盘文件,这样命令的输出就会写入文件。 environment:环境变量,可以读取父进程的 env后,携带到子进程。在 `Shell 和进程` 一文中有介绍。 suspend/resume/isRunning:意如其名 这里重点说下 executableURL (launchPath),最好是提供命令的完整路径,如 /bin/pwd。完整路径可以通过先执行 /usr/bin/which pwd 命令来拿到。 如果想简单使用,可以仅提供 pwd 也一样可以,默认会从系统的环境变量中查找。 但这有一定的风险,因为除了系统命令,还有用户自己安装的其他命令,被放置在各个位置。且 Swift 是跨平台的,部分系统用户还可能会自定义系统路径的位置,可能会出现找不到命令的情况。 还有一个办法是将 executableURL 设置为 /usr/bin/env,然后将 pwd 写入 arguments 参数中。 这样的话,默认会读取 env 里面的 PATH 以获取 pwd 的完整路径。而 env 中 PATH 基本是全的。 源码分析 SwiftShell 源码已经停止更新较久了,部分 api 已经不符合当前 Swift (5.9.2) 的标准建议。 但 SwiftShell 本身是一套非常精妙小巧的框架设计,可以快速在项目中进行改写并使用。实际上,目前 Swift 5.9.2 版本使用,并不需要做调整,即可正常使用。 如需要进一步调优以适用于自己的项目,可在查阅原作者源码或下文分析后,自行修改。 源码分析注释已经同步更新于 Github 项目中,可直接阅读该项目:SwiftShell 源码解析 前置 中已经对 FileHandle、stdin/stdout、file seek、Pipe、Process 等做了介绍。了解了这些后,可以比较方便的阅读源码,注释中也对必要的环境进行了阐述。 还有一些 SwiftShell 中用到的知识点,这里也做一些说明,或许可以更利于理解项目: 终端 env 参数传递 打开终端解释器后,bash/zsh 等环境初始化成功后,当前进程会有一些已经配置好的环境变量。 这些环境变量有些是需要传递给通过 SwiftShell 执行的子进程命令的。 具体的操作流程并不复杂,但是对于环境变量是如何传递给子进程的,以及子进程对变量的操作是否会影响到父进程,可以查看 Shell 和进程 以获取更详细的说明。 Lazy 延迟计算 鉴于 FileHandle 为文本 IO 操作,SwiftShell 内部为防止耗时,已经尽可能的使用 lazy 延迟迭代器对 IO 数据进行解析。 Swift 中对 Sequence 序列进行 Lazy 操作,是通过在内部新建一个内部类迭代器完成的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 自定义 `CycleIndex` 迭代器,同时实现 `Sequence` 协议,作为序列使用 lazy 操作及高阶操作 class CycleIndex: IteratorProtocol, Sequence { typealias Element = Int var index: Int = 0 func next() -> Int? { defer { index += 1 } guard index < 10 else { return nil } return index } } 1 2 3 4 1. var i = CycleIndex().lazy 2. var j = i.map { pass } 3. for _ in j {} OR j.makeIterator().next() 第 1 步,通过 lazy 计算属性会根据 CycleIndex () 对象生成一份新的对象 LazySequence i。 第 2 步,对 i 进行 map 访问,会继续生成一个 LazyMapSequence 对象 j 并将 map 闭包存储于对象 j 中,这就是 lazy map 不会立刻执行的原因。 在 j 内部有一个内部类 Iterator n。该内部类实现了 IteratorProtocol 协议,即 n 是一个可迭代对象。 第 3 步通过 for…in 或 next 对 j 进行访问的时候,就会调用到 n 的 next () 方法,该 next () 会对 j 持有的 map 闭包进行执行。外部每调用一次 next (),map 闭包就会执行一次。 这是 lazy 能够对 序列 进行延迟访问的原因。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // lazy 计算属性实现 public var lazy: LazySequence<Self> { return LazySequence(_base: self) } // 可迭代内部类设计 extension LazyMapSequence { public struct Iterator { ... } } extension LazyMapSequence.Iterator: IteratorProtocol { public mutating func next() -> Element? { return _base.next().map(_transform) } } 类型檫除 对于范型协议,是不能直接用作返回值的,如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protocol Fly { associatedtype action func canFly() -> action } } class Cat: Fly { typealias action = Bool func canFly() -> action { false } } class Bird: Fly { typealias action = String func canFly() -> action { "flase" } } 以上定义了 Fly 协议,两个实现类中对于是否可以 Fly 的返回类型是不一样的。 1 2 3 4 5 6 7 8 func theAnimal() -> Fly { ... { return Cat() } ... { return Bird() } } 函数返回 Fly 协议,Xcode 检查会不通过,编译报错。因为当前返回值具有多样性,Xcode 并不能知道具体的返回类型是什么。 比如:let m = theAnimal().canFly(),此时 m 是 Bool 类型还是 String 类型,就无法判断。 私认为这是 Swift 对 protocol 实现范型 不友好 的地方,官方没有提供友好的解决方案,而问题又的确是需要解决。 公认的解决方案是:类型檫除。使用中间层代理的形式,以 Anyxxx (AnyIterator、AnySequence) 对原有的数据进行中间层封装。 Swift 内部也在大量使用该方案,可查阅 Swift 源码 ExistentialCollection.swift - AnyIterator。源码可参考:Swift 官方源码 Lite 在 AnyIterator 中,定义了 _AnyIteratorBoxBase、_IteratorBox 两个关键类,用于明确 Iterator 的 next () 返回值到底是什么类型。 这样可以在任意位置正常使用 AnyIterator,因为通过范型参数的形式,Xcode 已经明确知道迭代值是什么。 理想的类型檫除,是 Xcode 明确知道类型是什么,可以使用静态调用,代码执行会更高效。 后期 Swift 的确推出了较友好的一个方案用于类型檫除,即 some。用于 Xcode 对返回类型进行智能推断,很多场景下均可以使用 some 做类型檫除。 除 some 外,还有 any 也可以用于类型檫除。使用 any 后,Xcode 就完全不管具体类型,而是交由运行时判断。这相对而言是有一定风险和性能损耗的。 鉴于 “又不是不能用” 原则,在降低代码复杂度、可维护等理由下,若 some 无法满足,还是推荐 any 的。 准确来说,如果项目中需要考虑到 any 的性能损耗,那一定是还有其他更需要解决的问题。 但对于开源的项目,还是建议使用公认的类型檫除方案,即 Swift 官方也在大量使用的 中间层 代理方案。 虽然略显复杂,但却十分行之有效。 荣耀归于主,因祂配得我们一切的赞美和敬拜。

2023/12/13
articleCard.readMore

Swift 脚本开发环境搭建

2024.01.06 更:欢迎查阅 Swift 开发的文本纠错命令行工具。 不推荐使用 Swift 写脚本,和 Python 比起来,该生态链相对匮乏,开发耗时会增加很多。 但对于偏 Swift 的同学来说,这也的确是更可控和方便维护的方式之一。尤其对于公司内部工具,有问题可以更快的找到原因并处理,不至于手忙脚乱。 相对于 Python 环境丰富的基建资源,使用 Swift 做脚本开发的优点在于生态的一致性。 苹果官方抹平了 脚本开发 和 应用开发 的界限,即 开发环境、三方库导入和使用 (苹果自身的开源库 / 三方库)、系统平台 api (存储、网络、协程、并发等,UI api 除外) 等,均表现一致。 可以像应用开发的流程一样,进行脚本命令行的开发。 0x01 Swift 生态 一言两语,先概要描述下 Swift 目前的生态,以明确脚本开发所处的位置。 Swift 做为苹果大力推出的 iOS/Mac 多平台应用开发语言,目前广泛用于开发 app (ipa/dmg)。做为古老的 Objective-C 替代者,基本上对接了 OC 里面所有的 Api,可以直接调用。这对新语言的铺广有很大好处,使用成本有非常大的降低。 但 Swift 也抛弃了 OC 的消息传递机制,使得 patch 等能力很难实现。这也是大公司不愿意迁移的原因,在苹果不允许热更新之后,它们都耗费大精力维护一套不公开的 patch 能力,以在紧急时刻做补救。 Swift 做为后浪,完全集百家之长,也躲过了这些年高级语言已知的不完美,这是相当优秀的点。 至目前 (23 年底),已经对范型、协程、并发等高级场景做了完备的支持。 尤其值得关注的是发源地 app 开发,Swift 提供了 SwiftUI 做为原始 UIkit/AppKit 的替代,这是目前非常跟随潮流的 UI 基础建设。 Swift 以跨平台为前提,也同样可以做服务端开发、输出命令行可执行文件等。但刚才提到的 UI 能力并不能服务于此,毕竟这是不需要 UI 能力的。 playground & repl 对于学习 Swift 来说,playground 是一个不错的学习乐园。 playground 有两种,一种是 iPad 和 Mac 上都适配的 app,名字叫 Swift Playground,通过 SwiftUI 开发项目,可以直接上 App Store。 还有一种是通过 xcode - new - playground 生成的后缀名为 .playground 的项目,主要用于测试一些代码。 第一种完全是鸡肋,我是这么认为。第二种,也倾向于鸡肋,用处不大(更多用于调试,可 xcode 里创建一个项目已经足够方便)。 这里建议,对于 Swift 研发同学来说,可以不用看 playground 了,这样可以避免理解不少概念,如 PlaygroundSupport、Sources、Resources、多target分隔等。 这些概念和实践,在研发历程上,基本是无用的。凭空增加了 Swift 的知识点复杂度。 和 playground 类似的,还有 repl,即在终端通过 swift repl 进入的交互式编程环境。很多语言都带有这个环境,快速调试无可厚非,但我还是给予鸡肋的标签。 当然,对于其他语言使用者,仅仅希望了解一下 Swift,playground 和 repl 都是不错的选择,尤其 repl。可以不用理解完整项目里面的很多概念,如 AppDelegate/AppScene/Navigation 等。 !!! Package Apple 在 Swift 出来的时候,提出了 SPM (Swift Package Manager) 概念,和 Cocoapods 一致,做为三方库的管理工具。其中描述文件就是 Package.swift 文件。 后期,SPM 扩展了能力,和 .xcodeproj 处于同样的地位。通过对 Package.swift 文件进行配置,Package 所在的文件夹可以做为 动静态库 被 workspace 管理,也可以设置多个 Target 进行依赖控制,还可以引入三方库,甚至可以直接配置成可执行文件和插件 (如代码格式化等)。 这使得对 Package 的理解复杂度增加。 一开始,Package 对 target、exec 的支持,是通过 swift package generate-xcodeproj 命令,生成辅助 .xcodeproj 文件来完成的。后期 Swift 升级,删除了这个命令,完全通过 Package.swift 配置文件来完成了。 这一改变,应该是为 Swift Playground 这个 app 做的。如上面说到的,这个 app 可以独立制作项目并上传 App Store,通过该 app 生成的项目文件是 .swiftpm,不用理解 .xcodeproj - build setting 等众多概念,多 target、三方库 均通过 Package.swift 配置。 可能因为这个原因,提升了 Package.swift 的级别,和 .xoceproj 处于同样的地位。 所以,对 .xcodeproj 熟悉的同学,不用刻意理解 Package.swift 里面的诸多配置,这些配置仅仅是对 .xcodeproj 配置的迁移。用到哪里,查一下资料即可。 下面给示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "pcd", // 该 Package 的名字,如果是动静态库,可以理解为 pcd.framework。外部通过 `import pcd` 来引入 products: [ // Build 和 Archive 输出的产物 .executable(name: "pcd", targets: ["pcd"]), // 输出可执行文件(脚本工具等) .library(name: "pcdCore", type: .dynamic, targets: ["pcdCore"]) // 输出动静态库 ], dependencies: [ // 三方依赖。若通过 path 进行本地依赖,则可在当前工程直接编辑其内容,便于开发联调。同 cocoapods。 .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), .package(url: "../Progress.swift", branch: "master"), .package(path: "/Users/gebiwanger/Downloads/swift_public_project/SwiftShell") // 本地依赖,可在当前工程直接修改库代码 ], targets: [ .target( name: "pcdCore" // 同 `.xcodeproj` 里面的 target 设置,这里设置一个动静态库 ), .executableTarget( // 本身是 Target,同 `.xcodeproj` 主项目,可通过 Archive 进行打包(产物类似 a.out,非 app/ipa/dmg) name: "pcd", dependencies: [ // 多 target 依赖。可以依赖当前项目的 target,也可以依赖三方库(同 Cocoapods)。注意,三方库一定需要依赖,才能在当前项目中被使用(Pods 也是一样,只是Pods工具帮忙做了)。 .target(name: "pcdCore"), .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Progress", package: "Progress.swift"), .product(name: "SwiftShell", package: "SwiftShell"), ] ), ] ) 上图是单独的 Package 项目示例,可以通过 Xcode 进行和 .xcodeproj 一致的调试。 从上图可以验证,Package 项目,具有和 .xcodeproj 一致的功能,概念层级是和 .xcodeproj 一致的。 至此,Package 的概念和使用,基本已经介绍完毕,都是旧瓶装新酒。 Package 插件 除此之外,还想介绍一个 Package 特有的能力,就是插件。插件是一个可执行程序,可以本地运行或做为三方库接入,但不允许上架包引用,仅允许做本地调试。(不排除后期会开放 App Store 的能力) 对于目前 Xcode 支持的扩展,有些能力是可以通过插件来完成的,比如代码格式化、代码转换等。 插件的整体开发过程,和 动静态库 / 可执行文件 的开发流程基本一致,通过下面的配置进行区分: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 let package = Package( products: [ // 新增了 plugin 产物 .plugin(name: "FormatSwift", targets: ["FormatSwift"]), ], targets: [ .plugin( // 新增了 plugin Target name: "FormatSwift", capability: .command( // 权限控制,支持读取/写入项目文件。在代码格式化的时候需要该权限 intent: .custom( verb: "format", description: "Formats Swift source files according to the Airbnb Swift Style Guide"), permissions: [ .writeToPackageDirectory(reason: "Format Swift source files"), ] ), ), .executableTarget( // 可以有 exec target 或者动静态库 target,便于调试或者被 plugin 依赖。 name: "AirbnbSwiftFormatTool", ), ]) 通过 Archive 打包出的产物,可以做为可执行文件单独执行。其他项目也可以引入插件模块,右键主工程就可以看到插件的运行选项。 插件这一块的需求并不大,这里也不做更多介绍了。参考项目有 airbnb-swift-format ,有需要可以查阅更多相关资料。 生态小结 至此,Swift 的开发生态介绍的差不多了,整体和 Xcode IDE 依旧有非常深的绑定。 当然也可以通过 swift xxx 等命令行进行开发和调试,但对于 Mac 平台的开发人员来说,这完全是多此一举。 除此之外,前面也说到 Swift 是一门非常新也非常优秀的高级语言,其中的 协程、并发、多线程 等能力,也都是需要单独学习和掌握的。这属于语言本身,在环境篇就不再过多介绍。 命令行开发 这里进入核心环节,即通过 Swift 进行命令行开发的环境。 其实上面的环境篇,已经介绍了大部分命令行开发所需要的前期准备,如工程的搭建、项目依赖的设定等。 在 Objective-C 时期,也可以通过 Xcode 进行 Command line 的开发,通过 new - project - macOS - Command Line Tool 既可以建立一个工程,Archive 后即可输出可执行文件。 但 OC 终究不是为命令行而生的,有很多局限性和短板。这也导致十几年来一直都有的这个功能,更多被用于 C/C++ 的调试入口。 目前 Swift 做命令行开发,依旧可以走之前的流程建立一个 .xcodeproj 工程进行开发,区别点是语言选项上,勾选 Swift 即可。 同时,上面的流程也显得有些落伍,毕竟如上面介绍,Package 具有和 .xcodeproj 同样的层级定义,我们可以通过 Package 来创建可执行程序。 这样的一个明显的好处,是不用理解 AppleId / Build Setting 等复杂的配置。显然,做为一个通用的命令行工具,我们可以提交到 brew 或者随意分发给有需要的朋友,哪里还需要 AppleId 开发者账号呢? Swift Package 命令行的项目创建 / 编译 / 调试 / 运行,可以通过 swift xx 命令行操作。有了 Xcode 这个高度集成的工具,显然不用如此费力。 创建项目的流程为:new - Package - macOS - Command Line Tool / Executable。 这样创建的工程就是 Package 工程,而不再是 .xcodeproj 工程。Command Line Tool 和 Executable 基本没有区别,前者多了一个 ArgumentParser 这个 apple 自带的库依赖。也正因为这个依赖,推荐使用前者,这在命令行开发的过程中会方便很多。 命令行输入参数解析自动化 可以通过 CommandLine 模块做参数解析,提供了 argc 和 arguments 等 api,可以获取用户在执行命令的时候携带的参数。 不过后面,Apple 提供了 ArgumentParser 模块,这是可以快速搭建 命令行参数定义 的框架,比如命令中需要用户携带文件路径,通过 @Argument(help: "Please Input File Path.") var path: String 这样的定义,就可以指定用户一定要传文件路径并会被赋值到 path 变量中。如果用户输入错误或者使用 xx --help,Please Input File Path. 这个注释也会输出到命令行中以提醒用户。 当然,ArgumentParser 模块的能力远远不止这些。但可以看出来,ArgumentParser 定义了一套命令行参数的标准,开发人员不用再通过 CommandLine 模块调用 api 进行解析。 更重要的是,ArgumentParser 提供了一套命令行开发的流程框架,这可以通过阅读官方的示例文档进行学习。 前面说到 Command Line Tool 和 Executable 两种方式创建的项目相比,前者多了 ArgumentParser 引入。这也是推荐使用前者的原因。 当然,对于 ArgumentParser 的使用,可以参考官方的示例 (Examples 文件夹)。也可以查阅更多相关资料。 善用 Xcode IDE 的调试技巧 命令行开发过程中,一样需要断点调试,同时也需要真实环境下调试。这两者往往是同时进行的。 这里有一个小技巧,即通过在 Xcode Build 过程中插入脚本,和 Xcode Run 过程中插入参数,来完成快速的开发调试。 插入脚本 可以在 Scheme - Build - Post actions 中嵌入 bash 脚本,从而将编译好的可执行文件自动复制到 /usr/local/bin 目录,这样写完代码 Build 完成后,既可以在终端中进行真实场景下的调试和测试。 值得注意的是,一定要将 Provide build settings from 选择为当前编译的项目,这样才可以使用 Xcode 预定义的脚本变量。 1 2 cp "$TARGET_BUILD_DIR/pcd" "/usr/local/bin" echo "move $TARGET_BUILD_DIR/pcd to /usr/local/bin success." 插入参数 进度条的输出 进度条输出,和以往的 print 不太一样。因为 print 是叠加的,下一条输出和上一条会间隔一行。 而进度条的输出,是重写上一行。 这里需要一些小技巧,即:不换行更新上一次的输出结果 方案 1(不建议) 1 2 3 while true: print("Count: \(counter)", terminator: "\r") fflush(stdout) 通过 \r 将当前输出的 seek 切换到当前输出行的行首。这样下一次输出的时候,就从当前行的行首输出,对上一次的输出进行覆盖。 因为没有回车符了,而终端是识别到回车符,然后将回车符之前缓冲区的内容一次性进行输出的。 所以需要使用 fflush(stdout) 进行强制缓冲区刷新,即强制输出。 但上面介绍的这种方式有个缺点,即覆盖内容只能按照字节长度进行覆盖。如上一次输出是 abc,下一次输出是 12,此时显示的内容是 12c。 这在进度条中不容易出现,但面对复杂场景显然是不行的。 方案 2(建议) 这里推荐第二种方式,可以有效解决这个问题: 1 2 while true: print("\u{1B}[1A\u{1B}[K\(progressBar.value)") 输出文本加上 \u{1B}[1A\u{1B}[K 前缀即可。技术方案和上面是一样的,但可以规避上面的问题。 不过这里有个小 case,即单纯这样使用,会发现第一次输出的时候,整个命令行已有的内容都消失了。所以还需要加一个补丁: 1 2 3 4 5 // the cursor is moved up before printing the progress bar. // have to move the cursor down one line initially. print("") while true: print("\u{1B}[1A\u{1B}[K\(progressBar.value)") 在进行输出的时候,先输出一个空内容,这样就不会有任何异常了。 异步等待 命令行执行完毕,进程就销毁了。如果需要进行网络请求等异步操作,就需要特别处理,以等待异步请求的结果。 这里可以通过 runloop 将当前线程永久激活,然后通过 exit 强制退出。 1 2 3 4 5 6 async { ... exit(0/-1) } RunLoop.current.run() 显然,这不是一个好的方案,一来局限性很多。 在者,runloop 是 app/ipa/dmg 开发中的概念,虽然利用了 Swift api 完备性的优势,但在这里使用很不合适。 这里更好的解决方案是使用信号量: 1 2 3 4 5 6 7 let semaphore = DispatchSemaphore(value: 0) async { ... semaphore.signal() } semaphore.wait() 可以将信号量的操作,进行单独封装,后续需要 wait 的时候,只需要便捷的调用一些 api 即可。 当然,鉴于 GCD 强大的多线程管理能力,还可以使用 group 等方式,进行多个异步操作的并行操作等。 系统命令 当然,命令行开发过程中,不可避免需要使用到丰富的系统命令,如 ls/grep 等。 这可以通过 Process Api 来操作,但这里不做过多介绍,因为有强大的三方库可以使用。 详见:【Swift 三方源码 1】SwiftShell 高效的命令行工具 命令行小结 以上,就是通过 Swift 做命令行开发中的一些概要和部分细节的地方了。 如最开始说到的那样,使用 Swift 开发命令行,除了 UIKit/AppKit/SwiftUI 这些 UI 属性不能使用之外,其他并没有太多的 api 限制。 甚至可以使用 UserDefault 存储,这样可以极大的简化用户的 cookie 等信息的保存。 除此之外,大概就是对 Swift 高效和美丽的使用了。 Swift 这门语言虽然上手容易,但美丽的使用并不简单。 巧妙的使用 enum、protocol、async、combine 等高级功能,并且不搞乱代码,就很优秀。 什么都给了,又什么都没给。 什么都干了,又什么都没干。 什么都说了,又什么都没说。 放开了所有权限,又加上精细的卡口。 鸡和蛋的轮回,永不停息的魔咒。

2023/12/13
articleCard.readMore

rime - 小鹤双拼

2023.12.14 更:mac 14.2 版本,已经完全解决了中文输入法的问题。 2023.10.03 更:采用 twitter 网友的办法,我又换回系统输入法了。详见:https://x.com/hai_lv_/status/1704597086346649778?s=20 Mac 上原生的双拼输入法,有时候间歇性的卡死。在网上找了不少办法,均不能很好的解决。 我对原生输入法还是有不少感情的。自从 Mac 和 iPhone 支持小鹤双拼后,就迁移到了原生输入法,到目前也有很多年了。 这次把 Mac 侧迁移到 Rime,也尽量和原生的体验保持一致。鉴于 Rime 本身配置较为复杂,这里把自己使用的精简版做下记录。 体验说明(和 原生 的输入方式保持一致,包括中英文切换方式): 使用 CapsLock 键作为 英文 和 中文 的切换键。 Rime 仅仅有 小鹤双拼 的输入能力,其他输入法包括英文均做了阉割。 各种类型的符号输入,和原生保持一致。还有 半角、全角、生僻字、Shift 快捷键 等。 基本上,就是无感切换了。 配置,都已经处理好了。真要配置这些细节,还是挺花费时间的,我前后共计花了 4-5 小时。 如果你和我的输入法使用习惯一致,建议直接拿去用,不要想着二次配置了。毕竟这只是工具。浪费时间不值得。 预计 10 分钟以内,可以完成本文配制,顺利使用 Rime 的小鹤双拼。 0x1 brew 安装 输入法 brew reinstall --cask squirrel 然后退出当前用户重新登录,或者重启电脑。 现在可以在设置里面找到 Squirrel 输入法了。 完成后,就可以通过 CapsLock 键切换输入法了。这里希望你也是这样的用户习惯,不然后面的配置可能不适用。因为有些同学习惯 Shift 切换输入法。 0x2 点击设置,会打开 输入法 的配置文件夹。这里下载配置文件,全部复制到当前配置文件夹里面。https://github.com/yigegongjiang/rime 然后点击上面截图里面,setting 上面有一个 Deploy 部署按扭,点击后等 3-5 秒中,配置就部署完成了。 0x3 目前已经可以正常是用 rime 的双拼了。enjoy yourself. 如果正常的话,应该可以出现下面两种不同模式 (正常 / 暗黑) 下的输入项,基本和 原生 的还原。 其中暗黑模式无法做到 100% 还原,因为系统会根据输入窗口的配色,对输入面版做颜色调整,但是 Rime 只能做固定配色。 进阶说明 在 rime 输入法下,按 control+~ 键,光标出会出配制项。这里可以做进一步的配制,如启用 utf8 打开生僻字,打开 emoji,切换繁体和简体等。 如果这里满足不了诉求,可以修改下面的配制项,重新部署一下即可。这里有一份快速的配置指南。 主要配制都是在 double_pinyin_flypy.custom.yaml 文件中,这是双拼的配制文件。 打开生僻字 默如和原生的逻辑一致,是关闭生僻字的。这样可以有效的减少侯选字的数量。如果需要打开生僻字,可以做如下操作: 1 2 3 4 5 6 - options: ["gbk","utf8"] # 这里是屏蔽生僻字用的。不然候选里面有很多生僻字。UTF8 会打开生僻字,GBK 不会。 reset: 0 states: - GBK - UTF-8 把 reset 修改成 1 打开 emoji 一定要在打开 生僻字 的基础上,才能打开 emoji。 1 2 3 - name: emoji_suggestion reset: 0 states: [ "N", "Y" ] # 是否需要 Emoji。注意,只能在 选中 UTF8 的时候,才可以打开 emoji,否则有概率系统 crash(实测结果)。 把 reset 修改成 1 打开 繁体字 1 2 3 - name: simplification reset: 1 states: [ 繁, 简 ] # 繁体、间体切换。默认繁体,这里需要通过 reset 强制简体 把 reset 修改成 0 关闭 词库 1 2 3 #載入朙月拼音擴充詞庫 #'translator/dictionary': luna_pinyin.extended 'translator/preedit_format': {} 把 translator/dictionary 这一行注释掉即可。下一行千万不能注释,否则双拼输入的时候,输入框会显示全拼字符。 彻底关闭 emoji 全局搜索一下,把 emoji_suggestion 相关的,都删掉即可 自定义表情和符号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 自定义符号上屏 punctuator: import_preset: symbols symbols: "/bq": [😀,😁,😂,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗] half_shape: "#": "#" "*": "*" "`": "`" "~": "~" "@": "@" "=": "=" '\': "、" "%": "%" "$": "¥" "|": "|" "/": "/" "'": { pair: ["「", "」"] } "[": "【" "]": "】" "<": "《" ">": "》" 修改这里,可以自定义键盘对应的符号,也可以自定义表情。 自定义快捷语句 在 custom_phrase.txt 文件夹中,可以把需要的快捷短句录入进去。 切记,一定要用 tab 做间隔,不能直接使用空格。(文件顶部有描述) GFW 不仅可以控制流量的出口,也可以反向控制流量的入口。 我们看到的外面不是真实的,外面看到的我们也不是真实的。

2023/9/21
articleCard.readMore

【旅游】沿海旅游 (威海、青岛)

最近难得休了一次长假,计划和家人出去旅游。近十几年来,基本没有什么旅游经历。如果公司团建也算旅游的话,那也可以算增加一些经验值。 老婆说一直想看海,于是趁着旺季,把山东的威海和青岛逛了一圈。 简单记录一下威海和青岛两座城市的备忘,给有缘人。 威海主打的就是海,政府也对旅游提供了较大的支持,建设了一个环海公路。 去威海旅游,一辆电瓶车就可以解决一大半行程。有很多租电瓶车的地方,60 元 / 天。 沿着环海公路,可以直接从国际沙滩骑到海上公园,6 小时可以玩完,整个旅途都适合小孩子。 住的话,一定要选择在韩乐坊附近,最好距离韩乐坊 500-1000 米以内。这个位置距离海边也近,晚上有比较多的活动选择,尤其吃喝方便。 华夏城那里有一个小孩子的户外娱乐场所,非常棒。玩累了可以带孩子去耍,只在白天开放。 青岛虽然整体是比威海好,不管是娱乐设施还是沙滩环境。但几个著名的景点,并没有什么惊喜的特色。 因为缺少短途的交通工具,不如威海玩的开心。旅游景点都有地铁,但走路还是需要一些时间的。行程上比较累,不痛快。 崂山有些失望,作为著名的旅游景点,并没有什么特别的地方。 旅途中,住宿是一个大难题。一定要提前预定住宿,防止临时找的房子太差。 住宿不好,绝对会影响整个旅游的心情。尤其房间有异味、拥挤、破旧。千万不要因为房子,耽误了旅游的心情。 旺季的时候,房子普遍偏贵,威海和青岛我预定的都是 400 元 / 晚,不算贵。 提前花时间挑选,面积也都比较大,30-40 平米左右。环境也较好,整体都是很满意的。 因为有孩子,衣服比较多,我选的也都是民宿,有洗衣机。如家等经济酒店,是没有洗衣机的,而且面积偏小。 有几个技巧,一是用美团订房,多看评论。飞猪在定房赛道上被美团打趴下了。 二是预定多天,鸡蛋分开放虽然可以抵御风险,但提前规划好,还是可以一次选到好房源的。玩累了早上会睡懒觉,中午搬家会很麻烦,不要给旅途凭添麻烦。 这次旅途,吃的上面并没有带来惊喜。海鲜是挺多的,但各地都能吃,这里也没有便宜太多,顶多算新鲜。韩国烤肉还算可以,不过不算惊喜。青岛一厂的啤酒是挺不错的。 我们基本去的都是 top 餐厅,怕踩坑。但没有被什么美食惊艳到,挺出乎意料的。 路途上,基本超过 1 公里,就会选择打车,这样会比较轻松,也节省时间。 高德打车发展的真不错,车源很多,是可以和滴滴拼一拼了。但是论服务质量,还是滴滴的优享专车更好一些。 还有一个很重要的点,就是旅游地点规划,一定要提前做。有些景点会来不及去,但一定要提前规划有哪些景点可以逛。 这样可以更好的做时间排挡。 最后一点,关闭钉钉、微信通知,工作会扰乱旅游的心情和节奏。 旅游,如果仅仅是看风景的话,意义看起来不大。至少我这几天已经疲惫。 美食、山水、风景、花草、乐园等等,各个地方都有,千篇一律。除非没见过山海的,可以多认识这个世界。 如果在不同的风景、环境中,有不同的成长和感悟,就非常棒。 单纯行千里路,卵用没有。 旅途中,看到国内出现抢盐了。什么时候,才能不听到这些搞笑、诡异、荒诞、离奇的全民运动啊。 一天天的,一年年的。 有日本华人在朋友圈拉黑了很多国内人,说这是一次清洗的机会。 也有很多国内人去干预日本店铺,说是爱国和惩戒。 一群人看着另一群人,相互认为是小丑,这是平行的世界,近几十年是无法调和了。 不在乎你生死,也不会关心你健康。自以为的守护神,手里可是一直拿着镰刀。 说辐射致癌,不少人买了仪器来测试周边风险,发现本身就超标 N 倍。这样的套娃喜剧,什么时候才能够结束啊。

2023/8/28
articleCard.readMore

好记性远远不如烂笔头

对于生活的记录,很多人已经完全依靠照片了。这些年手机和移动互联网起来后,生活记录一个在照片,一个在社交互动平台。 但是这有很大的局限性,通过这些,是没法把一个过程给串起来的,会出现很大篇幅的上下文缺失。 这和学习中的知识体系很相似,学习不能想到哪学到哪,而应该成体系。书、专栏等有章节的知识学习,才能够对一个方向有较强的认识。 在视频、照片、文字三种传播属性中,文字的穿透力是最强的,所以洗脑一定得依赖软文。 文字,是最能够全面记录一段过程的。很多人到了一定年纪,会想着出书,以留下一些东西,而不是出一个摄影集或者其他。 现在也很少人写日记了,我本人也有七八年没写日记了。前老板说,离开老家出来工作后,就把日记都给锁在了小屋子里,现在也很多年没写了。 日记,最能记录一个人的一生。只是在匆忙的工作生活中还夹带隐私,都不方便写了。 对日常生活和学习的记录,我从来不相信脑子,过了一个时间阀值,肯定会忘。大脑不是用来存储的,是用来思考的,这点和 CPU 与硬盘的功能划分一致。 今天想介绍的是一款软件,Notion。 前些年它还没有什么特别,和同事聊起它,定性是一款笔记软件,终究还是工具,市场有限。而且一来大厂不愿意收购,二来大厂也在发展这类协同工具。这一点,从 印象笔记 的离场可以佐证。 那时,Notion 超越同行的,是使用块来作为文本的基础属性。这个也没看出来啥亮点,这个功能还很快被同行抄了过去。 后来,Notion 实现了数据库,被惊艳到了许多。从没想过在笔记软件里面,可以通过数据库把信息给整合的如此流畅。 再后来,第一时间集成了 GPT 能力推出写作 AI。在推广期的时候我使用了很多次,效果相当好。 对于日常记录、学习整理,Notion 着实可以独当一面了。数据库的使用还是有一些难度的,多个场景多玩几次,也就会了。 它适合用于一些比较沉重严肃的记录场景,这和笔记的属性有关。 对于待办事项这些便捷轻量的操作,本人还是使用 Apple 的 Reminders,无与伦比。 Notion 目前已经相当优秀了,不过有两个场景,我还是没法依赖它。 Notion 不能原生支持流程图。这算是刚需,不知道为什么 Notion 这么多年了,还没有支持。 Notion 不支持 Vim,虽然小众,但对于使用 Vim 的人来说,无法支持 Vim 的打字环境,其实拒绝率超级高。 这两个场景,因为 Notion 没有提供插件能力,只能期待日后官方有没有想法了。 永远不要指望通过脑子记住什么东西。 人生旅途中的诸多备忘,该记得记。就像硬盘中的数据,硬盘有价,数据无价。 我一直想着,有些人,说好也不好,说坏也不坏,站在不同的角度分析,会有不同的评价。 大部分情况下,不用一味的肯定和否定一个人,因为普适的坏或者好,都是在特定的时间,做过一些特别的好事又或者是特别的坏事。 而这样的好事和坏事,每个人也都在时时刻刻的做着,只是对于无名小辈,没人挂念而已。 但真的有一个摄像头,把自己时时刻刻的直播出去,那么自己也就成了那个好人,或者那个坏人了。 聚光灯下面,哪有什么真相。偏偏人们非要对聚光灯下的人和事,给一个历史的评断。断章取义在所难免。 不过有些人,就是恶和坏,以至于模糊的听到名字,就想咬牙切齿。 还有些人,是蠢的可怜可爱,脑子什么时候被偷走的,这些人自己也说不清楚。

2023/7/16
articleCard.readMore

高级语言小记

最近学习回顾了几门高级语言和周边工具。这里做个小总结,学习暂时告一段落,以后会在需要的时候再捡起来。 Swift: Swift 这门语言,真的是大杂烩。集合了不知道多少语言的混杂体。 Swift 语法都觉得似成相识,比如 范型、异步、多线程、函数编程、闭包、下标、嵌套、元组、虚函数。不过这些能力都是一门高级语言本该支持的,也无可厚非。 后起的语言,的确是有后天优势的。很多语言都有历史遗留问题,比如 python 为了解决作用域问题,弄出来 global、local、nolocal 这样反人类的设计。 值类型和引用类型 (struct /class) 借鉴的 C++,详见之前的文章:Swift struct (值类型) 与 class (引用类型) 区别及三种传参机制深刻关系和使用 元组,借鉴的 C++ 的 tuple。这是真好用,尤其在写算法的时候,可以省去不少逻辑复杂度。 属性包装器 propertyWrapper 借鉴 Java 的注解。 函数调用中的虚函数,借鉴 C++ 的虚函数设计。不过在函数调用这个问题上,也没有那么多选择,要么静态编译直接派发,要么虚函数存储方法列表派发,要么像 OC 一样动态派发,要么就得和 Java 这类一样靠虚拟机动态解析 (JIT 是直接派发)。本质就是找函数指针的过程。 范型,谈不上借鉴,各个语言都有实现。不过最古老的,应该还是 C++。 闭包,没有什么特别的。也是在吃 OC 的老本,没有什么创新。但是对于函数是第一公民这一点上,闭包将 Swift 带进了函数式编程的大家庭。 subString 共用内存,借鉴的 Go。 空合运算符??是从 TS 弄过来的,就是一个语法糖,但很好用。 enum,不可否认,算是一个伟大的创新,使用起来很方便。不过将枚举提升到了 struct 和 class 的程度,感觉是过度了。现在的设计,完全可以将 enum 的设计潜入到 struct 里面,这会少一个复杂知识的认知。 区间运算符,也是一个创新。不过我认为还是借鉴了 Go 的切片设计。 Swift 目前还是极度不适合服务端开发(2023)。简单项目也不建议尝试,因为意义真的不大。如果项目真的简单,希望快速开发,其实更愿意推荐 Python。 Swift 的服务端周边生态太差了,社区也匮乏,Vapor 这样的 web 框架真的是简陋。 很多人还推荐使用 Swift 来写脚本或者可执行命令,简直就是脱裤子放屁。十分不推荐。时间很宝贵,要善待。 这类脚本开发,能有 python 写的爽?退一步说,即使非 PC 场景命令,是 app 执行中的脚步,那 python 也可以做一个阉割版的解释器放在 app 中,在 Mac 和 iPhone 都能跑。 但是在 苹果 生态里面,only swift,也不用在留念 Objective-C 了。 随着很多 api 只开放 swift 接口,OC 的影子会逐渐退去。 SwiftUI 也是大势所趋,结合 Combine 还是很方便的做 MVVM 架构开发的。 很想试试 Swift 做 Vision Pro 的开发。 Swift 用起来还是挺爽的,3 年前用 swift 做过一段时间的开发,这次使用下来,发现新增了 async/await 协程,而且非常好用。 这里特别说明一下通过 协程 整合了 异步 & 并发 两大能力的 async/await,寥寥几行代码实现了这么多功能,简直相当惊艳。 很少有语言会把协程能力和 多线程 做耦合,一般都是需要开发同学自己加多线程逻辑的。 整体感触: Swift 使用起来还是很爽的。尤其很好的支持了函数式编程。整体比 OC 爽的不止一点点。 但 Swift 目前还是得围绕苹果生态使用,想破圈,还是很难。 之前 tensorflow 增加对 Swift 的支持,后来也放弃了。IBM 也推出并停止了 Swift 服务端的建设道路。 之前还对 Swift 成为全能型开发语言有一些期待的,目前来看,难了,基本不可能了。 这里大胆做个预期:截止当前 2023 年,Swift 已经没有破圈的可能了。以后如果没有惊艳的 case,机会也不会有了。 SwiftUI 如果没有经历过动态化开发,很多人会不理解为什么复杂的应用界面可以通过 SwiftUI 这样的层级形式给描绘出来。 我之前和同事聊过,GUI 开发,最主要的是数据的稳定,UI 一定是跟着数据转的。只要数据稳定,就不会有大的问题。 界面 (UI),是一层皮肤,SwiftUI 和 Flutter 的 dart,都是用来描述这层皮肤的样式的。当然可以拎出来单独设计。 大厂也都会自建 DSL 的 UI 描述方式和专用 IDE,一般是通过 JSON/TS & VSCode 来做。 编码上没有太多差异,更多的是功能 api 上的联动性。SwiftUI 官方出品,直接在写 UI 的时候就能联动系统 API,使用起来也是最爽的。 SwiftUI 开发中,界面只是小部分,更主要的还是 Combine 和 async/await 这些 Swift 的能力,配合 MVVM 架构,来保障数据层面的互通,从而驱动 SwiftUI 的展示。 不得不承认,SwiftUI 开发体验爽爆了。 动态化方案的设计,避免不了跨平台问题 (研发效率),也就避免不了 JSON/Tree 形式的界面描述。 而 SwiftUI 这种官方出品的界面描述系统,可以和自身 Swift 能力完美融洽,这解决了很多动态化方案性能低下、跨平台 & 跨设备兼容的问题。尤其兼容性问题,对于跨平台 (Android/iOS) 的 Flutter 或者自建,最为复杂。 一个是原生编译,一个则是运行时 AST 抽象解析,这之间的鸿沟,是巨大的。 Java: Java 这些年发展很快,大学时候使用 Java 写过简单的后台,那时候还是 SSH,即 Spring/Struct/hibernate,使用的还是外部 tomcat + JSP 的方案。 看现在的技术方案,都已经被 Spring 全家桶吃完了。基于前后端分离逐渐统一,Java 开发已经不再需要配置 JSP 这些页面模版了。 我感触比较大的是 Spring core 和 Spring 周边 对 Java 研发生态的影响是如此之大。 其中 依赖注入 和 AOP 已经通过框架层面提高了研发同学的生产力,真棒。大学写 Java 的时候,经常使用的是各种 xml 配置。现在变成了各种注解。 Java 的周边是真的多,从 数据库到微服务,鉴权、限流、消息队列、Docker、Nginx 等。这些周边依托 Spring 全家桶,简单用起来是不难,但是在并发上来后的负载均衡、熔断等机制,还是挺考验工程和技术能力的。 当然,对这些周边,停留在使用的层面是远远不够的。深入研究也需要大量的时间,这是非常耗费精力的。 Java 这门语言,的确相当强大。多年前还被人诟病依托字节码做多平台的性能较低,在 JIT 发展起来后就完全抹平了和编译语言的执行效率。 Java 还有一个优点,就是设计模式在 Java 这个体系里面能够充分的发挥作用。甚至有一种感觉:设计模式就是为 Java 而贴合设计的。 Java 的容器,和 C++ 的 STL 有的一拼,或者说相似度非常大,挺好用的。 不像 Objective-C 那个老古董,连 堆、队列、栈 都要自己实现,没见过哪个平台支持 OC 写算法。 虽然如此,但是我还是不喜欢 Java。10 年前,我实习期间因为对 Java 的抵触而换了赛道。 当时我觉得 Java 太模式化了,在 Java 的世界里面,很多都是规定好的,条条框框。如果不按照这些约束走,那么就需要重新造轮子,甚至引发不可预期的问题。而我不喜欢那种被约束的感觉。 现在回头看,Java 在 Spring 加持下,其实更加模式化了。 有一点,Java 发展这么多年,都没有出现协程的官方实现。完全基于 用户态 的协程,实现起来复杂度并不大,Java 也有不少协程相关的三方框架,但是官方迟迟没有支持。 整体感触: 不得不承认,Java 是最优秀的高级语言。再大的项目,使用 Java 可以非常协调的开发和维护。 Java 生态框架化非常深,高层业务开发上手简单且不容易出错。对于完全新手,以前需要 6 个月入门,现在 1 个月就能入门,大大的降低了学习门槛。 Docker Docker 真是一个好工具,以至于我希望把它单独拎出来。这个被 Google 不断实践后却被 dotCloud 异地开花的产物,实打实的改变了服务部署的游戏规则。 以前,做 web 服务的时候,好多周边配置,动不动就项目跑不起来了。现在通过 Docker 脚本化部署真的太方便了。 配合 Docker Compose 和 Google 的 kubernetes,真的做到了一键部署、动态部署,真香。 Nginx Nginx 做 静态 / 动态 代理服务器,真棒。之前用 node 写小服务,就是在 vps 上用 nginx 做的反向代理,当时也没觉得多棒,只觉得是不错的流量路由。 现在看 Java 各种微服务的代理、负载均衡、证书、跨域、限流、资源路由,没有 Nginx 真不行。 C: C 语言,非嵌入式或者底层开发等场景基本没有用武之地。但是,C 语言是最应该深入学习的,而且是必学的。 高级语言最简单的部分就是语法糖了,看半天一天的也就差不多了。但是看到一门语言的语法糖,就能猜到内部的大致实现,离不开对 C 语言的理解。 C 语言这门瑞士军刀,可以真真切切的还原内存和算法,而这是所有高级语言避不开的基石,乃至于操作系统,本质也是对内存和算法的极限控制。 举例来说,struct 是 C 唯一的结构体,也是 C++/Objective-C/Swift/Go 等众多语言的基础结构,也在操作系统里面随处可见,进程、线程的数据结构都使用 struct 来描述。 通过掌握 struct 的指针,可以对数据如何在内存中流动,有清晰的认识。任何高级语言都离不开指针,即使像 Swift/Java/Python 这些语言会刻意避开指针的概念。 在编写代码的时候,能够感觉到数据在内存中是如何流动的,就会刻意的追求更高的执行效率,也会加快 bug 处理的速度。 对于 C 的面向过程编程,其实和面向对象编程在运行层面就没有本质的区别。 仅仅是高级语言把函数调用的方式封装到了 class 里面,展开后,还是面向过程的。这是面向对象的语言对 ITer 的欺骗。 面向对象的封装、继承,通过 C 一样可以实现。比如 封装 可以通过 struct 实现。继承可以通过组合实现。对于多态,因为使用组合实现继承,也就没有多态一说了。 我的建议是通过算法来学习 C 语言。因为对于很多人来说,C 在工作中没有直接的应用,语法的淡忘周期会非常短。 但是对于算法,C 是除了汇编之外最底层的语言描述。可以更加清晰的描绘算法、精确掌握执行过程。这是 Java/C++ 这些语言带不来的,因为相对算法的执行过程,它们都包装过度了。 应该用 C 来学习算法,写算法还是用 C++ 爽一些。不然太多的容器轮子需要开发,太累了。 C++ 的 auto 类型推断 和 tuple 元组 太好用了。STL 容器实在强大。 整体感触: 瑞士军刀,每一名 ITer 都应该必学必会。 尤其要对 指针 和 结构体 有深入的认知,这两个是对内存的直接映射,不理解它们,就无法理解内存。而内存和算法,没有语言可以避的开。 私心:本人非常喜欢 C 的面向过程的开发方式。相比面向对象,在使用 C 的时候,有一种莫名的欢喜,不管是变量的定义或是函数的调用。 这基于 C 的命令式编程范式,让操控欲得到满足,可以精细化的描述一个过程。而面向对象没有那种操控感觉,每一个实例对象都像牵线木偶一样的流水线工人。 Python: 简单写服务、脚本,千万别往 Java/C++ 上面凑。2-3 小时就能看完 Python 的语法,用 Google 搜索下诉求,就能找到开源库。 Python,当之无愧的开箱即用。 Anaconda 很好用, 通过 conda 来代替 pip 和 环境隔离非常方便。 小技巧: 学习高级语言语法的过程中,肯定是要看文档的。但文档只有介绍,有时候不能很好的提供示例。 这里有个好用的技巧,就是找一个代码量比较大的开源项目。遇到文档里面提到的关键词,就去开源项目里面搜。很快就能理解相关知识点了。 还有一个小技巧,就是 chatGPT 可以很方便的解答很多疑问,要勤于提问。 最后,还有这两个技巧的组合。就是对于一些比较难以检索的字符,如:for ... in ... 这种,可以问 gpt 要正则表达,如 for .* in .*,然后在 IDE 里面通过正则来检索。 结束语: 编程世界,主要还是在于内功的磨炼,有 计算机原理、网络、数据结构 & 算法。反而真的需要使用某一个高级语言来完成工作的时候,其实挺快的。 比如协程,当对进程、线程的数据结构有了解,理解汇编下函数调用栈的执行流程后,协程的实现其实都是千篇一律,不管是 Go、Swift、Python、TS 等。 再比如网络,当对网络底层比较熟悉,高级语言都只是在封装各自语言的 SDK 给开发者使用而已。 还有各个语言的容器实现,都摆脱不了 数据结构 & 算法 的组合。很多容器的实现,真的没有什么魔法,很简单的数据结构 & 算法的封装而已。 还有设计模式,这是通吃的,所有语言都一样,只是表现形式不同。 高级语言真的只是工具,实现目的的工具。 上学的时候,老师说美帝国主义不安好心,让人民币对美元的汇率升高了,说这会对国家造成巨大损失。 当时我就是理解不过来,1 美元可以换更多的人民币,为什么就有损失了。如果我手里有 1 美元,不是相当于更多的人民币了么,这是好事嘛。 现在我才知道,原来是站的角度不一样,我是站在自己有美元并且期望换成人民币来消费,而老师站在很高很冷的地方,我是格局没达到。 如果 1 美元可以换更多的人民币,就是人民币贬值了。反之,则是人民币升值。 人民币贬值的影响如下($1=¥10) => ($1=¥20): 国内商品出口增加:原来 ¥20 ($2) 的商品,现在只需要 $1,所以国外抢着要中国商品。 国外商品进口减少:原来 ¥10 ($1) 的商品,现在需要 ¥20,所以国内减少国外进口。 这里的进口和出口,可以理解是泛化的。比如国际旅游、海购等物品流通,都适用。 汇率造成的人民币贬值,不仅仅影响国际贸易。当进口困难,会对内部市场也造成影响。 比如一件商品因为原材料少了,价格开始变贵。以前能买得起的,现在消费不起了。 大陆从 2005 年汇率整改后,从 8.x 的利率一路降到了 6.x,一路升值。 最近一年破 7,2023.6.30 达到了 7.26。一年前还是 6.7。一年贬值 7.7%。 这也不算啥小题大做,不从汇率做贬值计算口径,大陆内部人民币每年也是 10% 的贬值速度嘛。 明明存钱等于花钱,还一直存在,真是傻逼。

2023/7/2
articleCard.readMore

又一年高考

2023 高考过去了,这两天老家也开始农忙了。体验过农忙的学生,一般都想着靠读书改变命运,不愿面对面朝黄土背朝天的广袤天地。 农村,是一片复杂的土地,无数的人想要离开,最后还是回到了那里。高考,就是一个敲门砖,离开农村的那块垫脚的砖。 在这片困乏饥饿的神州大地上,学历不一定代表未来,但没学历,是真的没有未来。 有学历,依旧会承受疲劳工作、生儿育女、职场危机、生活一地鸡毛、老年浑噩。 没有学历,对有学历所要承受的一个都少不了,只会更刻骨和铭记。还要承受低能、工作学历卡口等歧视。 所以,还是考的好一点吧。很多人都能上大学,但 985/211 和普通本科,是完全不一样的,本科和学院也是不一样的。 如果考的不好,请复习 1 年甚至 2 年,不要失去耐心。 多年之后,会发现上学是好轻松的活儿。上学也是上班,工作的好点,省去了以后多少的精神崩溃。 高考、教育 从来就没有过公平,也很难实现阶级的跨越。 父母亲期待高考让孩子鲤鱼跃龙门,成家立业在一片新的天地,这是不对的。 阶级的固化,做父母亲的应该理解。 自己不是编制内,也就不要想着让孩子进入多好的编制。 自己不是有钱人,也就不要想着让孩子成为有权有钱的人。 高考以及之后的大学教育,更多的当作孩子精神层面的成长,足以。技能方面的成长,当作付出的利息吧,这样心态可以放正。 简而言之,个人的成功与否是没有一个标准的。高考考得好,可以离成功更进一步。考的不好,请复习再考一次。 不知道 10 年之后,高考会变成什么样,但现在,还是和 10 年前一样。 我希望有一天,高考不再成为一个学生命运的归宿。 如果一个人只能通过高考来实现价值,太无趣了,太狭隘了。 大学教育入门门槛高,出门门槛低,这让太多人误解了大学,以至于四年没有收获。 大学,应该是要比高中更加劳累的。高中的应试教育被书本掩盖,大学才是百花齐放的精神舞台。 得有更多的营养补充,才能撑得起大学教育这个舞台。这里面的营养,还包括和高中一样的书本知识。 大学太累了,这本应该是那些意志坚定的能玩得起的高级玩家所走的一条路。 奈何社会没有给芸芸众生很好的包容和归宿,都走向了高考这座独木桥。 很多人以为高考是离开了地狱,其实那是天堂。 我自己高考一般般。前段时间一位赢家在推荐上这样描述我:虽然学历不好,但 xxx。 学历好,敲门砖的确高了很多,但对于人生来说,如果后期没有及时踮起脚,那道无形的槛,依旧高立着阻碍着槛下的囚徒。 在这片土地上,不遇到事,什么都好。好风景,好人情,好生活。 但只要遇到事情,不管是病还是各种乱七八糟的磨难,那么就完了,有时候甚至人生的终点就提前抵达了。 这是一个没有保障的国度,谁能保证一辈子不遇到点什么事情呢?人的一辈子,总归要遇到一些杂七杂八的事情的,不然,人这辈子好像躺的太平了,太没意思了。 相比那些被命运捉弄的人,大部分人都是可以很好的喘气的。但也时刻在和命运做些斗争,苟延残喘也要活着,因为有牵挂。

2023/6/10
articleCard.readMore

Vision 未来已来

2024.01.03 更:Vision Pro 已经在美国发售,很多国内的人也通过代购等方式拿到的设备。 2024.02.26 更:到目前为止,没看到 vision pro 爆出来亮眼的消息,这一个版本是完了,挺失落,科技滑铁卢。游戏和办公,都没亮点出来。比如游戏没有双人合作通关,办公也么看到啥惊喜。想着至少健身环大冒险、神庙或者网页浏览,都能出一些不一样的体验。目前看完全没有。 Apple WWDC 大会上,发布了 Vision 设备,24 年初向美国售卖,3499 美元。预计不久也会在国内市场透出,加上税应该在 2.5W 左右。 在大会没有开始前,有不少人已经从不少渠道了解到了这款设备的一些信息。在正式宣传和媒体体验后,其实大家都是很吃惊。 一来功能非常实用刚需, 二来完成度非常高,不是卖残缺品 PPT 的。 简要总结几个点: 视前屏幕是不透明的。即戴上 Vision 后,眼前就是一片黑。 内置的 M2&R1 芯片和 12 枚摄像头,可以将内外部的画面,在视前屏幕的内外屏上 10ms 延迟内实时展示。即内部看到的是外部建模后的投影,外部也能看到内部的眼睛区域,也是投影建模后的。 Vision 能绘出非常大的视觉区域和 3D 视觉效果,突破了显示器等外接设备的物理限制。 Vision 的摄像头和传感器,通过对眼部、手部、身体的感知,可以比用户更早一步的知道用户想要干嘛。已经能够通过 虚拟键盘 / 手势 / 眼势 做到输出和输出。 Vision 有自己的操作系统,和 Apple 生态可以互通。即可以在 Vision 上自行开发应用,现有的 Mac/iOS 应用也可以在 Vision 上使用。 Vision 覆盖了 游戏、办公、生活娱乐、出行 等多个方面,即年龄段、时间段,不受限制。 我对 Vision 非常满意的地方,在于它从另一个维度,实现了我的一个渴望。 我一直都希望有一个非常大的屏幕,可以用来浏览网络信息、办公。 但是这样的屏幕因为像素和空间尺寸等原因,对我来说其实无法满足。我还一直很有遗憾。 真没想到,Vision 在虚拟现实里面做到了。 GPT 的发布,客观评价,未来将来。 Vision 的发布,客观评价,未来已来。 GPT 让大家知道人工智能来了,那是一个充满人类智慧的智能体。但它怎么更好的服务于用户,带来惊艳的社会进步,其实就像一个项目一样,没有落地。这还需要时间,可能也就在这两年。 但这份对未来的渴望,目前被 Vision 打破了。因为 Vision 落地了未来的样子,它描绘了后面 20-30 年革命的形态。 GPT 也会成为这个形态里的一部分。 Vision 和 GPT 的科技难度,都是史无前例的高。Vision 内部,也必将内嵌 GPT 这样的智能。 可惜这两轮革命到来的时候,我好像生活在大清,又好像生活在现代文明。之所以这样感受,是因为目前的社会环境,包括生活、认知、经济,都和 Vision 所描绘的,有些格格不入。 我想在 Vision 上做点什么。这是一个刺激的时代。 时来天地皆同力,时去英雄不自由 不要太遵从敬佩者和名人的话,他们的只言片语,是他们几个月甚至几年的总结。没有那个环境和经历,不仅体会不出深意,还可能误入歧途。 而你自己的经历凝聚出的认知,对自己是沉淀,对别人是鸡肋。

2023/6/8
articleCard.readMore

致敬耗子哥

左耳朵耗子,原名陈浩,很多人称呼耗子哥。 互联网布道者,骨灰级程序员。 因心梗离开了。 我受到不少耗子哥的影响,对其博客中的 VIM 教程尤其深刻。 他的很多观点,都具有过来人深刻的教诲。 早上看到消息,本以为是假消息,毕竟这是一个没有真消息的年代。 但是没过几分钟,查证了,也沉默了。 他生前影响了很多人,他的离开,让整个中文技术圈轰动。 致敬耗子哥。

2023/5/15
articleCard.readMore

Socket 与 ICMP

关于网络相关的历史文章中,很多次提到 Socket 套接字的重要性。最近看到 ICMP 的一些知识,发现和 Socket 有非常强的依赖。于是对 Socket 和周边,再做一次逻辑的梳理。 本文会对 ICMP 查询 / 差错报文 如何 NAT 网关 和写入 Socket 接收缓冲区进行具体阐释。 Socket、文件描述符、API、套接字 Socket 在系统内部创建后是唯一的。和 源 ip (本机 ip)、目标 ip (服务端 ip)、源端口、目标端口 有一定的绑定关系,根据具体协议而定。 通过这种绑定关系,可以将对应的数据包写入到对应的 Socket 接收缓冲区中供上层读取。 如果绑定关系出现 1-N,即一个数据包找到了多个 Socket,那么数据包就会往多个 Socket 中都写入一份,在后面 ICMP 中做分析。 Socket api,各个语言都有上层实现,底层都是对 Socket C Api 的封装。就是大家知道的 gethostbyname、bind/connect/write/sendto 这些。 Socket 文件描述符,不是真的文件。在 Unix 下习惯对资源进行抽象,即一切接文件,包括内存数据。Socket 文件描述符是内核内存区域的指针。 套接字,就是指 Socket。一般也就是指 api 或者 文件描述符 其中的一种,看具体场景。 Socket api 可以直接操作传输层 (TCP/UDP) 或者网络层 (ICMP),从 Socket 往下 (传输层 / 网络层…) 开始,都属于操作系统管辖。 所以 HTTP 经历很多版本的优化,依旧差强人意,就是因为核心层 TCP 不好改动,需要系统升级。最后通过 UDP 做上层改造来实现 QUIC。 TCP Socket Socket 是 TCP 的中流砥柱。TCP 需要那么多的控制信息,都是需要记录的。这些记录,传输层是无法完成的。也都会存储于 Socket 文件中。 TCP 的 Socket,可以理解是一个隧道。即 A Socket 写入,B Socket 读出,像隧道的两个出入口,比喻很形象。 服务端 B bind 只需要一个 Socket,但是接收到每一个客户端请求后,都会单独为该客户端生成一个独立的 B Socket。 之后,A Socket 和 B Socket 在 connect (三次握手) 阶段为 Socket 文件描述符确定 src_ip & src_port & des_ip & des_port 四个绑定元素。 这四个绑定元素,可以使得隧道的入口数据包,一定能够在隧道的出口出现,即双向通行。 UDP Socket 因为 TCP 非常重要,又是 HTTP 的御用,所以很多人对 TCP 更了解,反而对直来直往的 UDP 陌生了。 TCP 是数据流,读取和接收都是不固定的字节,所以需要进行黏包处理。 UDP 是数据报,即发送 n 个字节,接收 n 个字节,每个消息都有边界。 对于 UDP 开发来说,要主动做四个最基础的核心事情:超时检测、回执、包序号、分段。 包序号和分段,也没有什么复杂的逻辑,用 UDP 上传一张图片来理解: 客户端要发送的图片为 8000 字节,分成 8 个包,每个包 1000 字节。增加序号 0 - 7。客户端把这套数据结构缓存在内存里面,随时应对失败等场景的重发。 服务端收到乱序后的包,立刻发送回执,并根据序号进行重组。 还需要一些控制用的数据包,比如客户端发送完成的标识、图片分组标识等。 UDP 的服务端,不会像 TCP 一样,为每个客户端都单独维护一个 Socket。UDP 只有一个 Socket,这个 Socket 只会绑定 Server_ip & Server_port。 UDP 的客户端,不用进行 connect,UDP 的 Socket 只会绑定 Client_ip & Client_port。 这样 Socket 也就变成了单向隧道,即等待对方发给自己数据 (对方拥有自己的 ip 和 port)。发给对方数据,则通过 sendto 指定 ip&port 的方式空投给对方数据包。 备注 1: 实际上没有单向隧道一说,UDP 也是通过 Socket 隧道的发送缓存区发送数据的。只是通过 sendto,不像 TCP 的 write 那样默认会填充 ip&port。 备注 2: 实际上也有单向隧道一说,因为 Socket 只有自己的 ip&port,所以反向的 ICMP 差错报文数据包是没有办法接收的。后面再说。 总之,因为 Socket 只有自己的 ip&port,没有对方的 ip&port,实际上和 TCP 会有不少变化。 比如: 对于 NAT 不严格类型,多个服务端都可以向 Client 发送数据 (sendto (Client_ip,Client_port))。这一点相比 TCP (双方 Socket 是 1-1 的),会比较方便实现 NAT 穿越。 UDP 可以通过同一个 Socket,使用 sendto 向不同的 Server 发送数据。 ICMP 差错报文可能会通知 Client 目的地址和端口不可达。Socket 中没有 Server_ip & Server_port,内核协议栈无法找到 Socket,也就无法通知到 Client。(Client 会干等,一直等) 当然,UDP Socket 也可以执行 connect。这样就可以绑定服务端的 ip&port 了。这样,上面的几个问题也就解决了。 Socket bind/connect 的作用 bind,是自定义自己的 ip 和 port 到 Socket 中。 connect,是自定义对方的 ip 和 port 到 Socket 中。 所以,客户端也可以 bind 的,这样就可以指定自己的端口了,就不需要操作系统随机指定了。如果两台公网的 UDP,就可以这样操作,摆脱 Client-Server 模型,两台设备都可以随时向对方发送消息,即两台设备同时充当客户端和服务端。 所以,UDP 也可以 connect,这样就可以将四元组信息绑定到 Socket 中,接收到 ICMP 差错报文,就可以立刻接收到错误信息了。(下面会说明为什么) !!ICMP 是如何穿过 NAT 的 先简要描述下 NAT 场景。 一个数据包,在正常的路由器、交换机传输中,是不会改变网络层 src_ip 和 des_ip 的。所以我们收到的数据包,网络层的 des_ip 都是服务端 ip,不会变成网关或者中间路由器的 ip。 上面这种场景是转发网关。 但是对于内网设备,在 NAT 路由的时候,会把 内网 IP 映射为 NAT 设备的公网 IP,以解决 ipv4 短缺问题。这个时候数据包网络层的 src_ip 会从内网 ip 192.xx 变为公网 ip xx.xx。 上面这种场景是 NAT 网关。 NAT 网关会有很多有类型,按照严格程度,有锥形、完全锥形等,主要通过数据包的 src_ip&src_port 建立一个映射表。这对 NAT 穿越增加不小的难度。 ICMP 是网络层协议,没有端口的概念。NAT 是需要根据端口做内网和公网映射表,来实现内网访问互联网和外网数据包进入内网。 显然,这有些矛盾。预期来说,从内网出去的 ICMP 查询报文,公网返回后就 NAT 不进来了。从公网返回的 ICMP 差错报文 也同样 NAT 不进来。 ICMP 包括查询和差错两种。这两种方式穿过 NAT 的方式是不一样的。 查询报文,有 type/code/identifier 字段,NAT 设备会将这些字段组合出一个虚拟的 Client port,写入映射表中。公网返回的时候,type 会有规律变化 (8->0),code/identifier 不会变,可以逆向从 映射表 中找到对应的内网 ip&port。 差错报文,数据包体会把源出错数据包的内容也携带上,里面会有 ip&port。NAT 设备将这个 ip&port 拿出来,就可以从 映射表 中找到对应的内网 ip&port。 !!ICMP 是如何写入进程 (Socket) 的 ICMP 数据包,也是通过 Socket 进行发送的。Socket 创建的时候,可以选择 SOCK_STREAM (TCP)、SOCK_DGRAM (UDP),还有一个选项就是 SOCK_RAW(源数据包)。 通过 SOCK_RAW,再指定一个协议类型 IPPROTO_ICMP,然后按照 ICMP 查询 / 差错 报文的数据包格式填充数据,就是 ICMP 数据包了。 对于 ICMP 差错报文,都是外部 (网关、路由器、交换机等) 发给 Client,这个时候操作系统会根据 ICMP 携带的 目标 ip&port,找到可能的 Socket 并写入接收缓冲区。 注意,这里找到的 Socket 可能不是一个。因为 ICMP 不是传输层协议,不和 TCP/UDP 有强依赖。(马上回说到) 举例来说,UDP 如果没有 connect,那么 Socket 在 sendto 后,会把 src_id & src_port 绑定在 Socket 中。这个时候,所有能发到 src_id & src_port 的 UDP 数据包,都会被写入该 Socket 的接收缓冲区中。 但是对于 ICMP,此时是无法写入的。因为 Client 可能会 sendto 到多个服务端,其中一个服务端不可达,返回了 ICMP,如果被 Socket 写入,那么代表所有服务端都不可达了,这显然不合理。 所以对于 TCP/UDP 来说,ICMP 要写入到 Socket 缓冲区中,一定需要四元组完备。这个时候 UDP 可以调用 connect,将 Socket 绑定 des_id & des_port,这个时候 UDP 就不能在给其他服务端发送消息了,收到的 ICMP 一定是当前服务端出了问题。 对于 ICMP 查询报文,操作系统干预的就少了。 ICMP 查询报文是通过开启进程使用 Socket api 发送 type=8 的数据包,然后接受 type=0 的数据包。 这个 Socket 里面,只绑定了 src_ip。因为 ICMP 没有端口号概念 (非传输层协议),所以在 sendto 后,Socket 只绑定了 src_ip。 那么,这个 Socket 就只能强制接收所有发往这个 src_ip 的 ICMP 报文,包括查询和差错报文。 或者,这个 Socket 就一个 ICMP 报文都不接收,这显然不可能。 所以,开发 ICMP 查询报文的进程,会收到所有发往本机的 ICMP 报文,需要通过 type/code/identifier 进行过滤,才能拿到最终的查询回显报文。 这也就是上面刚说到的,ICMP 差错报文,不仅仅会给到 UDP 的 Socket,还会给到发出 ICMP 查询报文的 Socket。 下面会给出 ICMP 查询报文的 chatGPT 版本实现,在 recvfrom 里面,如果本机同时 traceroute,那么是可以收到 traceroute UDP ICMP 差错报文的。 注意代码内部 recv_ping 里面的过滤,判断了 icmp->icmp_type & icmp->icmp_id & icmp->icmp_seq。 icmp_type 是 0,即 ICMP 查询回显类型 (此时 icmp_code 按照协议一定也为 0)。 icmp_id 是进程号,即前面说的 identifier,同一个机子可能同时多进程发送多个 ICMP 查询。 icmp_seq 是单次请求号,一个进程可能发送多个 ICMP 查询。 如果没有这三个过滤,实际上是会接收到所有本机 ICMP 数据包的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 问:用 C 语言实现一个 ping icmp 功能 chatGPT: 要使用C语言实现一个ping ICMP功能,你可以使用原始套接字(raw socket)来发送ICMP回显请求(ping)并接收ICMP回显应答。下面是一个简单的示例代码。 在这个示例中,我们使用原始套接字创建一个ICMP套接字,然后发送ICMP回显请求(ping)并接收ICMP回显应答。 代码中的send_ping函数负责构造并发送ICMP回显请求,recv_ping函数负责接收并验证ICMP回显应答。time_diff函数用于计算时间差。 请注意,使用原始套接字需要具有管理员权限或root权限才能运行。在运行代码之前,请确保你具备适当的权限。 这只是一个简单的示例,实际的ping程序可能需要更多的错误处理和参数处理。此外,由于ICMP协议的使用可能受到网络和防火墙配置的限制,因此在某些情况下可能无法正常工作。 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <sys/time.h> #define PACKET_SIZE 64 #define MAX_WAIT_TIME 5 #define MAX_NO_PACKETS 3 // IP首部结构 struct ipheader { unsigned char iph_ihl:4, iph_ver:4; unsigned char iph_tos; unsigned short int iph_len; unsigned short int iph_ident; unsigned short int iph_flags:3, iph_offset:13; unsigned char iph_ttl; unsigned char iph_protocol; unsigned short int iph_chksum; unsigned int iph_sourceip; unsigned int iph_destip; }; // ICMP首部结构 struct icmpheader { unsigned char icmp_type; unsigned char icmp_code; unsigned short int icmp_chksum; unsigned short int icmp_id; unsigned short int icmp_seq; }; // 计算校验和 unsigned short checksum(void *b, int len) { unsigned short *buf = b; unsigned int sum = 0; unsigned short result; for (sum = 0; len > 1; len -= 2) sum += *buf++; if (len == 1) sum += *(unsigned char *)buf; sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); result = ~sum; return result; } // 发送ICMP回显请求 void send_ping(int sockfd, struct sockaddr_in *addr, int seq) { struct icmpheader icmp; icmp.icmp_type = ICMP_ECHO; icmp.icmp_code = 0; icmp.icmp_id = getpid(); icmp.icmp_seq = seq; icmp.icmp_chksum = 0; icmp.icmp_chksum = checksum(&icmp, sizeof(icmp)); sendto(sockfd, &icmp, sizeof(icmp), 0, (struct sockaddr *)addr, sizeof(*addr)); } // 接收ICMP回显应答 int recv_ping(int sockfd, struct sockaddr_in *addr, int seq, struct timeval *tv) { fd_set readfds; FD_ZERO(&readfds); FD_SET(sockfd, &readfds); struct timeval timeout; timeout.tv_sec = MAX_WAIT_TIME; timeout.tv_usec = 0; int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout); if (ret <= 0) return -1; struct sockaddr_in from; socklen_t fromlen = sizeof(from); char packet[PACKET_SIZE]; ret = recvfrom(sockfd, packet, sizeof(packet), 0, (struct sockaddr *)&from, &fromlen); if (ret <= 0) return -1; struct ipheader *ip = (struct ipheader *)packet; struct icmpheader *icmp = (struct icmpheader *)(packet + ip->iph_ihl * 4); if (icmp->icmp_type == ICMP_ECHOREPLY && icmp->icmp_id == getpid() && icmp->icmp_seq == seq) { gettimeofday(tv, NULL); return 0; } return -1; } // 计算时间差(毫秒) long long time_diff(struct timeval *start, struct timeval *end) { long long start_ms = start->tv_sec * 1000 + start->tv_usec / 1000; long long end_ms = end->tv_sec * 1000 + end->tv_usec / 1000; return end_ms - start_ms; } int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage: %s <hostname/IP address>\n", argv[0]); return 1; } struct hostent *host = gethostbyname(argv[1]); if (host == NULL) { printf("Failed to resolve hostname\n"); return 1; } int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sockfd < 0) { printf("Failed to create socket\n"); return 1; } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr = *(struct in_addr *)host->h_addr; int seq; for (seq = 0; seq < MAX_NO_PACKETS; seq++) { struct timeval start, end; gettimeofday(&start, NULL); send_ping(sockfd, &addr, seq); if (recv_ping(sockfd, &addr, seq, &end) == 0) { printf("Ping response from %s: seq=%d time=%lldms\n", argv[1], seq, time_diff(&start, &end)); } else { printf("Ping timeout for seq=%d\n", seq); } sleep(1); } close(sockfd); return 0; } 小玛蒂尔达:生活是否永远如此艰辛,还是仅仅童年才如此? 里昂:总是如此。

2023/5/13
articleCard.readMore

对互联网隐私的一些想法

人们都对个人隐私比较在乎,但是对个人互联网隐私比较忽视。 前些年还流行一句话:在互联网上,没人知道你是一条狗,还有那幅经典的狗在屏幕前的画。 这句话旨在强调 “用户能够在互联网上以一种不透漏个人信息的方式发送和接收信息”。但是很多人却理解为:在互联网上很难留下踪迹,没人知道狗是我,或者我是狗。 只要在网络上行动,就一定会留下痕迹。只是痕迹留下的多少,以及持有痕迹的一方愿不愿意向外透露而已。 说网络是虚拟的,是从产品形态上来描述的。但网络本身并不是虚拟的,每一个网页、消息、短视频,都是要通过数据包进行电信号传输的,是真实存在的实体。 因为数据包是实体的,所以在互联网行走,一定会有痕迹。 这些痕迹算不算隐私,标准就是:用户愿不愿意把自己使用的痕迹向外公开,比如直播或者开源的形式,展示自己的互联网动线。 如果不愿意,那么这些痕迹,就算用户个人隐私。至于个人隐私范围,就不太固定。从内部来说可能对亲人公开对朋友不公开,从外部来说国家层面可能强制获取这份隐私。 1 《中华人民共和国刑法》的相关规定: 第二百五十三条之一【侵犯公民个人信息罪】违反国家有关规定,向他人出售或者提供公民个人信息,情节严重的,处三年以下有期徒刑或者拘役,并处或者单处罚金;情节特别严重的,处三年以上七年以下有期徒刑,并处罚金。 从国家层面《网络安全法》来认定,互联网用户数据,属于个人隐私 / 私人资料,并且这份数据对于公安和安全部门是无条件公开。 人们之所以对网络隐私忽视,主要原因是:因为看不见网络,所以不知道网络里有什么,或者网络里有自己的哪些隐私。 国人大多都没有信仰,而我是一名基督徒。所以我能够理解那种情绪,即没见过 God,说圣经是 God 的教义,那 God 在哪?网络隐私也是这样。 如果在公共场所有一张纸,写了张三几时几分访问了什么网站。为了证明张三就是张三,还贴了张三的银行卡号和身份证号。 那我们就知道,这样做是不对的。因为此时的张三,可能有一天会变成自己。 但是在互联网,一小时前访问了什么网站,浏览了多少在线店铺,连自己都忘了。也没见着那张记录这些信息的纸,所以就不在乎了。没见过上帝,还需要祷告吗? 真实情况是,互联网上的隐私,比生活中隐私多得多。最直观的,昨天访问了什么网站,我们自己都忘记了这份隐私,但是这份数据在各个互联网上下游的磁盘中存储着。 除了网站,还有账号及密码、家庭住址、个人身份信息、个人体征信息、出行信息等等。这些平时不在乎的隐私,其实在各个互联网上下游中都有备份。 比如数据采集工作。对于公司来说就是希望尽可能的采集足够的软件使用数据。如果是单纯的软件使用数据,那么这份数据的归属我认为还是属于公司。 但是如果软件使用数据和用户有 1-1 绑定,那么这份数据还应该属于公司吗?国内每个软件的使用条款里面,都有对此的说明,大概意思就是说,解释权还是归公司所有的。 苹果公司对于系统数据日志的采集,使用了一种 “差分隐私” 的技术方案,即收集到的特征数据不能反向推出个体,用这种技术方案还是挺文明的。 但是对于大量的公司,还是强调用户和日志 1-1 绑定的,甚至国家层面也会推出 caid 这种跨应用设备绑定方案,用于用户广告服务和换端等场景。 公司对外肯定不能有主动的日志泄漏,但是对内的权限控制还是有限的。因为数据就在那,很多业务或者问题也都需要这些数据,对内权限肯定有一些约束,但无法避免。 当前的互联网,隐私外泄非常严重。 大约一年前,telegram 上还有一个社工库,可以通过身份证 / 手机号等信息,查到各大平台的账号密码。 我试了一下,100% 还原。我查到了自己的,也查到了别人的。 这些从各种途径被泄漏的数据,包括账号及密码、酒店入住、身份证号手机号、购物记录、点餐记录等。 泄漏途径有大有小,国内外都有。比如 CSDN 600W 账号和明文密码泄漏、万豪酒店 N 亿条入住信息泄漏、上海公安 N 亿居民资料泄漏 (未证实)。 曾经我捡到一个身份证,我还希望找到这个人的手机号,好打电话返还给人家。查到少量信息,但没找到手机号,嫌麻烦,给送到了警察局。 幸好没有查到,后知后觉惊了一身汗。 互联网隐私,很多人不在乎,当作免费使用一些服务的资源互换。 其实应该在乎,因为免费使用一些服务,这是服务自身的规则。隐私和服务本身,在不同的维度,需要分开来对待。 比如一种场景:用户需要身份认证才能继续进行服务。那么,用户身份认证完毕后,就可以标记当前用户已完成认证,还有必要对用户的认证信息进行存储吗?没必要。即使为了后期回溯,那么也可以对认证过程进行脱敏存储。 互联网隐私和线下的生活隐私有一点不同。线下用户可以关了门再脱裤子,线上用户就不能管控自己的隐私,文明还需要提供服务的一方来坚守。 这里最大的检察方,应该是国家。一来国家不能自己做的不对,二来国家还需要对大小企业进行法律约束。 隐私,就应该是隐私。隐私,不应该被存储。因为不存在不透风的互联网黑盒。有人的地方,就有泄漏。 其实,写下此文的心情是沉重的。 仅以此文,致敬【编程随想】。

2023/4/25
articleCard.readMore

网络代理是如何工作的 (致敬 Surge)

最近本网站的 HTTPS SSL 证书过期了,于是去域名管理平台重新申请了一下。 无意中通过 dig 发现,网站的 DNS 解析 IP 一直是 198.18.1.xxx 这些。因为域名是 CNAME 映射到 github 的,所以又 dig 了一下 github 对应的 ip,发现也是 198.18.1.xxx。 从哪个角度来看,至少都有些问题。使用的 DNSPod 域名解析平台,dig 自定义域名和 github page 域名,怎么也不能在同一个网段里。 搜索了一下才知道,原来 ip 198 不是公网 ip,之前以为内网 ip 是 10/192 这些,知识还是有局限。 198.18.0.0/15 198.18.0.0 – 198.19.255.255 131,072 专用网络 用于测试两个不同的子网的网间通信。 https://zh.wikipedia.org/wiki/ 保留 IP 地址 在家庭网络中 dig 互联网的域名,为什么 dns 解析成了 198.x 呢?最后发现是最常使用的网络软件 Surge 引起的。 Surge 这些年给了我很多帮助,很感谢。特意开此文,讲解 Surge 工作原理,以致敬 Surge。 本文会对 Socket、Wireshark、网络系统代理 (http/s、socket5、POSIX)、网络网卡代理 (VIF)、VPN、DNS、DOH (SNI) 等知识点进行描述,以更加全面的讲解 Surge 的工作原理。 致敬 Surge Surge 是一款非常强大的网络调试工具,很多人都对它极为陌生,主要原因是它的售价过高,宣传和使用的人也不多。 每一个 ITer 都应该使用它。网络在 IT 工作中时刻都需要关注,如 DNS 解析、网络流量查看和 hook、网络代理等等,这些 Surge 都可以做到。是发现和排查网络问题的神器,也可以协助工作。 这里会先介绍下 Surge 的使用,或许你会感兴趣。如果以后 Surge 帮助到了你,那也是一件幸事。 Surge 只有 iOS 和 Mac 版,是订阅制,简单介绍下订阅制,如下图: Surge iOS 和 Mac 需要单独购买,iOS 只能在非国区 apple store 下载,Mac 可以在官网下载。 价格是有些贵,iOS 和 Mac 差不多都要 $49.9。Mac 版建议拼车,140 元左右,iOS 不建议拼车,比较麻烦。两个平台都可以试用下,效果不错再订阅。 这里有 Surge 的答疑,如:Mac release notes、iOS release notes、常见问题、订阅说明。 下面是一些 Surge 的常见使用: http/https/socks5 代理、机场流量转发 (懂的)、软路由 (相当强悍)、MitM&readWrite (比金瓶梅 Charles 好用)、DNS 代理、模块和脚本 (强大功能,非专业人士不常用)。 iOS 和 Mac 可以互联,通过 Mac 连上 iOS 后,可以快速设置 & 查看 iOS 上的网络流量。 如果对于刚需人群,软路由功能就已经让 Surge 的售价低到了尘埃里,懂得已懂。 下面对 Surge 是如何做网络流量劫持代理转发等核心功能,做技术分析。 基石 Socket 在之前的 IM 和 Socket 的关系及 Heart 的必要性文章中,对 Socket 套接字和 Socket 库 有说明。 一定要理解 gethostbyname()、socket()、bind()、connect()、listen()、accept()、send ()/recv () 和 write ()/read () 这些 socket api,否则无法理解 tcp/udp 的数据包传输,也无法理解下面要说的网络代理。 Wireshark 在之前的 TCP 数据传输过程分析文章中,有对 Wireshark 如何抓包和数据分析进行过阐述。Wireshark 是网络流量分析的神器,在理解 surge 的过程中,我对 tls、sni、dns、vif、lookback 等场景都进行了抓包分析和验证,非常有帮助。 如果不能对虚拟网卡和物理网卡的数据包进行详细的参数级观测,理解网络代理会有不小的 gap。 网络代理 - 系统代理 网络代理分为两种,分别是正向代理和反向代理。对于非服务端的 IT 工作者,可能只是听过但是不理解。用图片描述下: 正向代理:客户端知道有这个代理的存在,并且主动把数据包给到这个代理,希望这个代理能够自行处理数据包,最后给自己一个满意的返回。有些时候希望代理不做任何包的修改,有些时候又希望代理能够主动移除广告等数据包等。anyway,客户端知道代理会干什么,并且知道和真实服务器之间,有这么一个代理的存在。 反向代理:客户端不知道有这个代理存在,客户端以为在和真实服务器做数据包的流通,实际上数据包已经被反向代理给劫持。反向代理会对数据包做什么处理,客户端也不知道,如监控过滤阻拦有风险的数据包等。反向代理会优化对服务器的访问,会通过数据包的解析,知道客户端的 ip 等信息,做对应服务器节点的访问。反向代理有一个大杀器,就是负载均衡。anyway,反向代理对客户端是透明的。 对于 Surge 来说,对网络流量的劫持属于正向代理,即用户主动配置了 Surge 代理,使得本机流量让 Surge 来做处理。 对于机器的数据包流量,操作系统会给予一定的支持,提供不同层级的 api 开放能力,使得软件层面能够获取到流量。主要有三种途径: 系统代理。操作系统会提供代理服务器配置,主动将网络流量给到代理服务器。提供的数据包是层级比较高的,高于 TCP/UDP 传输层。 虚拟网卡。操作系统会提供网络协议栈里面 IP 网络层数据包的 AOP 切面,使得软件层面能被动监听到所有的 IP 层数据包。 socket hook。操作系统会提供数据包 hook 的能力,和 AOP 切面不同,hook 的影响未知,会对系统服务有破坏性。 Surge 使用了三种途径里面的前两种,即系统代理和虚拟网卡。 系统代理因为是操作系统层面直接支持,数据包层级较高,数据操作最为简单,因为系统已经做了封装。 系统代理就是三个,分别是 http/https/socks5 代理,相比后面要说的虚拟网卡,代理的数据有限,但也足够用了。下面着重说明这三个系统代理。 http http 代理最为简单,也是 https 代理的基础。因为是应用层协议,所以操作起来也最方便。简单画图理解下: 提取一下关键点: Client (浏览器或者应用程序),在发起 http 请求的时候,不在进行被请求域名的 DNS 解析,流量强制被操作系统转到 Proxy,Client 强制和 Proxy 之间建立 Socket 通道。 基于 TCP 三次握手,Client 和 Proxy 之间建立虚拟 socket 通道,各自生成 socket 套接字用于数据包的写入和接收。 Proxy 接收到所有 Client 原本要发到目标服务器的数据包,并做内部处理。(如果是广告,可能返回 404。如果流量正常,可以继续去请求一下目标服务器。反正可以做各种处理。) Proxy 处理完成后,对 socket_proxy 进行写入,socket_client 会读取到。 着重说一下上面的第 1 点和第 3 点,即 DNS 解析和 Proxy 内部处理的细节。 对于 DNS 解析,因为数据将要被 Proxy 代理,Client 将要和 Proxy 建立 socket 虚拟通道。 这时候,Client 已经和目标服务器之间没有任何关联,而目标域名的 DNS 解析就是为了拿到目标服务器的 IP 然后建立 Socket 套接字。 所以 Client 没有目标域名 DNS 解析的必要。 但是 Client 需要对 Proxy 做域名解析,因为有了 Proxy 的 IP,才能建立 Socket。Proxy 可能是本机的一个服务,也可能是外网服务。如果是本机服务,那就是 127.0.0.1 回路 ip 了。 对于 Proxy 内部处理,因为 HTTP 是明文的,所以 Proxy 可以拿到明文的数据。这样,Proxy 就可以拿到要请求的域名。 此时,Proxy 可以内建域名白名单,匹配上广告,可以直接返回 404。匹配上国内域名,可以直接代请求一下然后将数据返回。匹配上 google,根据自己是否能够直连外网,做直接请求或者二次转发之类的。 Mac http 代理的设置如下: http proxy python 实现如下,要理解 client_socket 和 server_socket 两个 socket 的生成和处理。 (From ChatGPT - “用 python 写一个 http 代理,处理 get post delete head 等请求。”) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 // 2025.01.21 update: using Claude to replace the previous ChatGPT 3.5 import socket import threading class HTTPProxy: """ HTTP代理服务器实现 注意:这个实现只处理HTTP请求,不处理HTTPS请求 HTTP代理请求格式说明: 1. 直接访问服务器时的请求格式: GET /index.html HTTP/1.1 Host: www.example.com 2. 通过代理访问时的请求格式: GET http://www.example.com/index.html HTTP/1.1 Host: www.example.com 因此代理服务器需要: 1. 解析完整URL获取目标服务器信息 2. 将代理格式请求转换为直接访问格式 3. 转发修改后的请求到目标服务器 """ def __init__(self, host='127.0.0.1', port=8080): self.host = host self.port = port def start(self): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((self.host, self.port)) server.listen(5) print(f"HTTP 代理服务器运行在 {self.host}:{self.port}") while True: try: client_socket, client_addr = server.accept() thread = threading.Thread(target=self.handle_client, args=(client_socket,)) thread.start() except Exception as e: print(f"接受连接时出错: {e}") def handle_client(self, client_socket): try: request = client_socket.recv(8192) if not request: return # 解析HTTP请求 first_line = request.decode('utf-8').split('\r\n')[0] method, full_path, version = first_line.split(' ') # 处理普通HTTP请求 if not full_path.startswith('http'): client_socket.close() return # 移除 http:// path = full_path.split('://', 1)[1] hostname = path.split('/')[0] port = 80 if ':' in hostname: hostname, port = hostname.split(':') port = int(port) # 连接目标服务器 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.connect((hostname, port)) # 转发修改后的请求 modified_request = self.modify_request(request, hostname) server_socket.send(modified_request) # 开始双向转发数据 self.forward_data(client_socket, server_socket) except Exception as e: print(f"处理客户端请求时出错: {e}") finally: client_socket.close() def modify_request(self, request, hostname): # 修改HTTP请求,移除完整URL,只保留路径部分 lines = request.decode('utf-8').split('\r\n') method, full_path, version = lines[0].split(' ') path = '/' + full_path.split('://', 1)[1].split('/', 1)[1] if '/' in full_path else '/' lines[0] = f"{method} {path} {version}" # 确保Host头部正确 has_host = False for i, line in enumerate(lines[1:], 1): if line.lower().startswith('host:'): lines[i] = f"Host: {hostname}" has_host = True break if not has_host: lines.insert(1, f"Host: {hostname}") return '\r\n'.join(lines).encode('utf-8') def forward_data(self, client_socket, server_socket): def forward(source, destination, description): try: while True: data = source.recv(8192) if not data: break destination.send(data) except: pass finally: try: source.close() destination.close() except: pass threading.Thread(target=forward, args=(client_socket, server_socket, "客户端 -> 服务器")).start() threading.Thread(target=forward, args=(server_socket, client_socket, "服务器 -> 客户端")).start() if __name__ == '__main__': proxy = HTTPProxy() proxy.start() https 理解了 http 代理之后,理解 https 就不困难了。 上面我们需要知道目标域名才能够做上面提到的转发过滤等工作。 可 https 是加密的,作为 proxy,我们不应该知道 client 和目标服务器之间的数据包内容。可是不知道数据包内容,我们就不知道目标服务器的域名。这是一个死循环。 https 的 ssl/tls 本身很复杂,但因为是系统代理,所以操作系统简化了不少事情。核心在于两个: 操作系统主动发送 http 的 connect 数据包,将目标服务器的域名给到 proxy。 proxy 不能感知数据包内容,只能根据域名做相关处理。 https 代理整体流程和 http 没有变化,就是多了一个 http connect 操作,之后的数据包都是加密的转发 (包括 tls 认证过程和之后的对称加密传输)。 大家使用金瓶梅 Charles 的时候,虽然没有设置 MitM 证书,依旧可以看到当前正在进行哪个 https 域名的访问。 按照预期,https 数据是加密的,域名也在加密数据包里面,Charles 不应该看到。 到这里大家应该理解了吧,http connect 操作会将域名给到 proxy,所以 Charles 虽然不知道数据包内容是啥,依旧可以展示在面板上。 https proxy python 实现如下,要注意对 connect 的处理,是返回 client 200 Established,这样 client 才会继续发送后续的数据包。 (From ChatGPT - “用 python 写一个 https 代理,处理 connect 请求。”) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 // 2025.01.21 update: using Claude to replace the previous ChatGPT 3.5 import socket import threading class ProxyServer: def __init__(self, host='127.0.0.1', port=8443): # 修改默认 host 为 127.0.0.1 self.host = host self.port = port def start(self): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((self.host, self.port)) server.listen(5) print(f"HTTPS 代理服务器运行在 {self.host}:{self.port}") while True: try: client_socket, client_addr = server.accept() thread = threading.Thread(target=self.handle_client, args=(client_socket,)) thread.start() except Exception as e: print(f"接受连接时出错: {e}") def handle_client(self, client_socket): try: data = client_socket.recv(8192) if not data: return first_line = data.decode('utf-8').split('\r\n')[0] method, target_host, _ = first_line.split(' ') if method != 'CONNECT': client_socket.close() return hostname = target_host.split(':')[0] port = int(target_host.split(':')[1]) if ':' in target_host else 443 try: server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.settimeout(10) # 添加超时设置 server_socket.connect((hostname, port)) client_socket.send(b'HTTP/1.1 200 Connection Established\r\n\r\n') self.forward_data(client_socket, server_socket) except socket.timeout: print(f"连接 {hostname}:{port} 超时") except ConnectionRefusedError: print(f"连接 {hostname}:{port} 被拒绝") except Exception as e: print(f"连接目标服务器时出错: {e}") except Exception as e: print(f"处理客户端请求时出错: {e}") finally: client_socket.close() def forward_data(self, client_socket, server_socket): def forward(source, destination, description): try: while True: data = source.recv(8192) if not data: break destination.send(data) except (ConnectionResetError, BrokenPipeError) as e: print(f"{description} 连接断开: {e}") except Exception as e: print(f"{description} 转发错误: {e}") finally: try: source.close() destination.close() except: pass client_to_server = threading.Thread( target=forward, args=(client_socket, server_socket, "客户端到服务器") ) server_to_client = threading.Thread( target=forward, args=(server_socket, client_socket, "服务器到客户端") ) client_to_server.start() server_to_client.start() if __name__ == '__main__': proxy = ProxyServer() proxy.start() socks5 对于系统代理,操作系统除了可以将 http/https 这两个应用层数据包给到 proxy 外,还可以将 socket 套接字给到 proxy。http/https 是基于 socket 套接字编程的,所以 socket 的数据包范围会更大一些,还能够转发其他应用层协议或者自定义协议。 这里就要用到 socks5 协议。 socks5 本身是一套协议,非应用层协议,而是一种系统代理传输协议,即操作系统如何将 socket 数据包给到 proxy 的协议。 具体协议可以看一下 Wiki。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 SOCKS5请求格式(以字节为单位): VER-CMD-RSV-ATYP-DST.ADDR-DST.PORT 1 - 1 - 0x00 - 1 - 动态 - 2 VER是SOCKS版本,这里应该是0x05; CMD是SOCK的命令码 ->0x01表示CONNECT请求 ->0x02表示BIND请求 ->0x03表示UDP转发 RSV 0x00,保留 ATYP DST.ADDR类型 ->0x01 IPv4地址,DST.ADDR部分4字节长度 ->0x03 域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。 ->0x04 IPv6地址,16个字节长度。 DST.ADDR 目的地址 DST.PORT 网络字节序表示的目的端口 具体来说,我们还是可以在 proxy 里面拿到数据包,但是需要进行字节级别的偏移计算,才能够拿到我们想要的数据。操作系统会将 socket 数据包,按照 socks5 协议的标准提供到 Proxy。 而我们的目标还是要拿到域名。https 是通过 connect 给的,socks5 就需要字节偏移来获取。 画图理解下: socks5 proxy python 实现如下,要注意对域名的获取。 (From ChatGPT - “用 python 写一个 socks5 代理。”) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 // 2025.01.21 update: using Claude to replace the previous ChatGPT 3.5 import socket import threading import struct class SOCKS5Proxy: def __init__(self, host='127.0.0.1', port=1080): self.host = host self.port = port def start(self): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((self.host, self.port)) server.listen(5) print(f"SOCKS5 代理服务器运行在 {self.host}:{self.port}") while True: try: client_socket, client_addr = server.accept() thread = threading.Thread(target=self.handle_client, args=(client_socket,)) thread.start() except Exception as e: print(f"接受连接时出错: {e}") def handle_client(self, client_socket): try: # SOCKS5 认证协商 if not self.handle_auth(client_socket): return # SOCKS5 请求处理 if not self.handle_request(client_socket): return except Exception as e: print(f"处理客户端请求时出错: {e}") finally: client_socket.close() def handle_auth(self, client_socket): # 接收客户端支持的认证方法 version, nmethods = struct.unpack('!BB', client_socket.recv(2)) methods = client_socket.recv(nmethods) # 目前仅支持无认证方式(0x00) client_socket.send(struct.pack('!BB', 0x05, 0x00)) return True def handle_request(self, client_socket): # 接收请求详情 version, cmd, _, addr_type = struct.unpack('!BBBB', client_socket.recv(4)) if cmd != 0x01: # 仅支持 CONNECT 命令 self.send_reply(client_socket, 0x07) # Command not supported return False # 解析目标地址 if addr_type == 0x01: # IPv4 target_addr = socket.inet_ntoa(client_socket.recv(4)) elif addr_type == 0x03: # Domain name addr_len = ord(client_socket.recv(1)) target_addr = client_socket.recv(addr_len).decode('utf-8') elif addr_type == 0x04: # IPv6 target_addr = socket.inet_ntop(socket.AF_INET6, client_socket.recv(16)) else: self.send_reply(client_socket, 0x08) # Address type not supported return False # 获取端口号 target_port = struct.unpack('!H', client_socket.recv(2))[0] try: # 连接目标服务器 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.connect((target_addr, target_port)) bind_addr = server_socket.getsockname() # 发送成功响应 self.send_reply(client_socket, 0x00, bind_addr[0], bind_addr[1]) # 开始转发数据 self.forward_data(client_socket, server_socket) return True except Exception as e: print(f"连接目标服务器失败: {e}") self.send_reply(client_socket, 0x04) # Host unreachable return False def send_reply(self, client_socket, reply_code, bind_addr='0.0.0.0', bind_port=0): # 构造响应包 response = struct.pack('!BBBB', 0x05, reply_code, 0x00, 0x01) response += socket.inet_aton(bind_addr) response += struct.pack('!H', bind_port) client_socket.send(response) def forward_data(self, client_socket, server_socket): def forward(source, destination, description): try: while True: data = source.recv(8192) if not data: break destination.send(data) except: pass finally: try: source.close() destination.close() except: pass threading.Thread(target=forward, args=(client_socket, server_socket, "客户端 -> 服务器")).start() threading.Thread(target=forward, args=(server_socket, client_socket, "服务器 -> 客户端")).start() if __name__ == '__main__': proxy = SOCKS5Proxy() proxy.start() 终端默认为什么不走代理 使用过网络代理工具的同学都知道,终端默认是不走代理的,即使开了全局代理。 但是很多人不知道为什么,更不知道为什么加了 http_proxy、https_proxy 这些配置后,就能够走代理了。 其实这也是系统代理的一些弊端,那就是系统代理的数据有限,更多的是按照当前系统定制接口开发的网络流量,可以被系统代理接管。比如 iOS/Mac 上面的 Cocoa 套件,默认就是支持系统代理的。 终端的工具,都不是基于 Cocoa 套件开发的,一般都是跨平台的,使用 c/ruby 等运行时网络库来实现网络请求。这些网络请求,是 POSIX 标准的,而 POSIX 标准的网络流量默认不过系统代理(别问,操作系统就是不给过)。 这里说到了 POSIX 标准。但是如果不理解什么是 POSIX 标准,那还是不理解终端流量为什么不过代理。POSIX 其实一敲就懂。 POSIX 是什么 POSIX 可以从 Wiki 上面查,但是大概率看了也不理解。其实它就是一套 API 统一标准,就和 “书同文车同轨统一度量衡” 一样,这里用 C 的运行时库里面的线程来说明: C 语言有标准库和运行时库,具体可以看一下之前的文章:从 Core Foundation 看更大世界 -> C 运行时和 C 标准库的关系。 如果要通过 C 语言来实现多线程操作,就需要使用线程 api。毕竟如果 C 语言本身没有实现,那么我们还要处理各种锁机制和内核线程之间的关系,几乎不可能。关于锁有多么的麻烦,可以看之前文章:锁 - 共享数据安全指↑ 恰巧,C 标准库就是没有制定多线程技术的实现 (最近才制定),所以 Windows 平台的 MCRT 运行时库和 Linux 平台的 glibc 运行时库,都没有一个 C 语言标准的多线程实现。 事实上 MCRT 和 glibc 本身都实现了多线程 api,因为没有 C 标准库制定标准,如果 MCRT 和 glibc 各自为政,两边的 thread api 就会有命名和功能上的差异,开发人员就不能夸平台执行了。 这时候就有了 POSIX 线程 标准,现在 MCRT 和 glibc 都依据 POSIX 线程标准来实现功能和开放 api,两端就统一了。 这就是 C 运行时库里面 <pthread.h> 的标准 api,也是大家一直在使用的,可以在 UNIX、Linux、MacOS、iOS、Android 等各个系统平台上使用。 后面 C 标准库更新,也做了自身线程的标准,就是 <threads.h> ,但它不是 POSIX 标准的,更多用于 C++11 标准中的线程库,主要适用于 C++ 开发。 终端走系统代理 终端要走系统代理,就需要在 zshrc 等配置文件中配置 http_proxy、https_proxy、all_proxy 这些。如果一次窗口周期使用,还可以用 export 来做。 为什么 export 可以使得本次窗口周期内等所有子进程都走代理,可以看之前的文章:Shell 和进程 而 http_proxy 这些配置可以使得终端网络走系统代理,是因为 POSIX 标准下的网络实现,认 http_proxy 这个流量转发配置。 然而这个配置的认证,也是可以由开发人员自定义的,所以安全起见,还是做如下配置,把一些可能的情况都配全。 1 2 3 4 5 6 export http_proxy=http://127.0.0.1:6152 export HTTP_PROXY=http://127.0.0.1:6152 export https_proxy=http://127.0.0.1:6152 export HTTPS_PROXY=http://127.0.0.1:6152 export all_proxy=socks5h://127.0.0.1:6153 export ALL_PROXY=socks5h://127.0.0.1:6153 网络代理 - 虚拟网卡 前面说到系统代理的数据包比较有限,就是 http/https/socks5 三种。对于很多非应用层协议,就不会过系统代理了。 Surge 有一个选项,叫增强模式,开启增强模式后,就会通过虚拟网卡接管 IP 网络层数据包实现转发。这样是可以接管本机全局流量的,除了有丢丢费 CPU 和 Mitm 有误判,其他都好。 尤其,在查看一些使用底层网络库的 app 流量的时候,虚拟网卡就是扛把子的。真要通过 wireshark 来快速查看本机或者移动设备的流量,那还是要耗费不少时间的,而且不直观。 对虚拟网卡做代理实现,现在就 Surge 有,其他工具都没有。wireshark 只能看流量,金瓶梅 charles 只能做系统代理 (上面提到的 https)。 因为虚拟网卡工作在 IP 网络层,对这一层的数据进行代理,为了识别到域名,就需要不少的工作量了。核心在于两点: 域名捕获 分片包重组 在系统代理的时候,操作系统会将域名给到 Proxy,拿到了域名就好办事情了。但是到了 IP 网络层,DNS 域名解析早就完成了,这个时候只能拿到 IP,不可能在拿到域名了 (除非 http 这样的明文,但没有意义)。 所以这个时候,就需要主动对域名进行捕获。Surge 的做法是捕获到 DNS 解析的数据包后,强制返回 198.18 网段的内网 ip,并将内网 ip 和域名做映射(这就是文章开头说到的,dig 本网站 ip 是 198 的困惑)。 这样 client 以为 google 的 ip 是 198.18.x,发送的数据包就会携带这个 ip。在 IP 网络层收到这个 ip 的数据包之后,就知道访问的是 google 了。 这样就完成了域名捕获的工作。 但是域名捕获又会带来另一个问题,就是分片的数据包必须要做重组。 对于数据包分片,在 TCP 数据传输过程分析 里面有说明,可以查阅下。 概要来说,IP 网络层的数据包,有 icmp/ping 这些不过传输层的数据,也有 tcp/udp 这些传输层过来的数据。tcp 会尽量阻止数据包分片的发生甚至可以强制不分片,而 udp 就完全不会管这个了。 综上,因为 dns 解析过程中分片的数据包标识非目标主机,这里又对数据包强制接管及转发,那么一定要做分片重组,否则目标主机无法完成重组操作。 当然重组逻辑本身可控,主要通过 IP 数据包里面的 16 位包标识 ID 进行重组。 下面还是大图看下具体流程: 整体来说,走虚拟网卡接管网络流量,还是比较费事的。需要多个 socket 的数据中转。 Surge 和 VPN 的差异 Surge 不是 VPN,两者的技术实现有重叠。 我们一直说访问不了 Google,就买个 VPN,其实是泛化的概念。很多时候我们不是在使用 VPN,而是指网络代理。 VPN 的确可以实现翻墙,但不是主要目的,VPN 也不是为了翻墙而做的。 VPN 是虚拟隧道,主要是为了保护数据隐私。VPN 是系统提供的能力,在 系统代理 之外,操作系统还提供了 VPN 的配置,VPN 还有多个配置协议,这些协议工作在不同的网络模型层级上。 最终,VPN 也是通过整合应用层、传输层、IP 网络层、数据链路层的数据包,然后根据不同配置协议进行加密,最后将数据包传输到具体的中转服务器。 鉴于 VPN 的使用场景,中转服务器一般都是企业内部网络,员工用来内部访问企业内网。 而我们使用 VPN 翻墙,主要是这个中转服务器,变成了机场,相当于 VPN 帮我们通过机场转发了本机流量。 VPN 是操作系统层面提供的数据代理,支持的协议也比较有限,如 IPSec、L2TP 等。 对于过墙用户来说,VPN 的网络数据包具有明显的特征 (操作系统提供的嘛,具有一定规律),根据这些特征可以很明显的知道这些数据是过墙的敏感数据,从而予以拦截(虽然看不到数据包内容,但是根据特征分析大概率是过墙敏感数据)。 我们将机场给的配置,配置在特定的软件上,实现网络代理。 虽然一样能访问 Google,但这不是 VPN。 虽然不是 VPN,但和 VPN 技术方案有一致。 上面 Surge 实现的系统代理和虚拟网卡,本身不是 VPN,但是可以为本机流量实现中继代理 (机场),从而过墙。 Surge 这些软件,主要就是走特定协议,抹平数据包的特征,使得数据包不具有明显特征,骗过墙。 没有特征本身就是特征。没有墙过滤不了的数据,只是相互博弈的一个过程。 还有一个点,VPN 本身不是为了过墙而做的,所以对于所有流量,都会发往中继代理。而 Surge 这些代理软件,可以根据域名来做规则引擎验证,从而实现流量分流。 这也是前面一直在说的,Surge 希望拿到域名的原因。系统代理的时候,操作系统主动提供了域名。虚拟网卡的情况下,Surge 捕获了域名。 看大图吧: 这里还有一个补充,也是大家认为过墙专指 VPN 的原因。 在 iOS 系统上,是没法像 Mac 上面进行 http/https/socks5 配置的。也就是没法直接进行系统代理。 iOS 系统开放了 Network Extension,通过 Extension 可以在应用内部进行系统全局配置,可以做系统代理和虚拟网卡等开发工作。 但是如果使用 Extension,就需要开启 iOS 系统的 VPN,这是唯一接管 iOS 系统网络流量的方式。 所以很多人有过墙就是 VPN 的误区。 MitM MitM 不做过多解释了,对 SSL/TLS 熟悉一些,理解 MitM 并不难。 在 Surge 的实现里,对系统代理里面的 HTTPS/socks5,比较方便实现 MitM,因为 ssl/tls 数据包特征明显,直接 MitM 即可。 但 Surge 强大的地方,在于还可以对 虚拟网卡 的劫持流量进行 MitM。 原理也是一样,只是这个时候数据包不仅仅有 HTTPS 流量,还有其他各种数据包,所以会有误判。 但挺香,尤其有些 app 没有走系统网络接口编程,系统代理抓不到。通过虚拟网卡来查看明文,在工作上还是挺有帮助的。 我这边的经验是,的确可以看到明文,但有时候有截断,只能看到部分。但总比没有强。 这个如果通过 wireshark 来分析,其实挺累的。 很多成熟的大型应用,也都对 MitM 有校验。比如在进行证书验证的时候,判断证书是否符合预期,不符合预先设置的,则不予通过,这时候 MitM 就会失败了。 了解 MitM 的原理,防护也都会有相应的办法。 这里也顺带批评一下国内银行 app。在开启 VPN 的时候,有些 app 给弹窗提示,有些直接不给使用,挺恶心。 它们应该防护 MitM 和 数据加密,而不是 VPN。MitM 防止应用层截包,数据加密防止应用层以下截包如 Wireshark。 DNS Surge 对 DNS 解析的处理方案,是:完全抛弃了系统的 DNS 解析,全部自行实现。 Surge 之所以能做到这一点,是因为: 系统代理的时候,数据包给到 Proxy 之前,并没有进行 DNS 解析。给到 Proxy 之后,Surge 会拿到域名,完全可以自己来做 DNS 解析了。 虚拟网卡的时候,数据包到达网络层之前,DNS 解析被 Surge hook 了,返回了 198.18.x 内网 ip。等到组装分片的网络层数据包后,完全可以自己目标域名的 DNS 解析了。 所以,Surge 完全有时机来自行完成 DNS 解析工作。Surge 是怎么做 DNS 解析的呢? 实际上,Surge 虽然抛弃了系统的 DNS 解析,但也没有完全重写。Surge 采用了两个策略来优化 DNS 解析: 并发解析:Surge 是对多个 DNS 解析服务器同时发起 DNS 解析工作,谁最先解析完成了,谁就是第一响应者。 缓存解析 (乐观解析):DNS 解析会存在过期,如果完全按照 DNS 过期时间来计算,那么会有很多无效的解析,因为服务器的 IP 一般都不会变。Surge 会继续使用上一次的 DNS 解析结果,当建立连接失败后,会重新使用刚才解析得到的 IP 重新完成建联工作。这会有效减少 DNS 过期时间带来的影响。 Surge 默认对 DNS 的解析操作,的确有效减少了解析耗时。但并没有解决 DNS 劫持等问题。这里推荐看一下网红 IM 网站:全面了解移动端 DNS 域名劫持等杂症:原理、根源、HttpDNS 解决方案等,对于 DNS 带来的问题,说的非常详细。 大家如果对 DNS 解析有疑惑,最好的方式是实战,非常简单的验证即可。这里推荐两个捷径: 使用 dig/host/ping 等命令,来验证指定域名的解析。其中 dig 是神器,通过 +trace 可以看到完成的解析流程。 查看具体的 DNS 解析平台对于域名的解析配置,这里给出本网站的解析后台截图 (没啥隐私): 这样,后面在看到 wireshark DNS 过滤结果里面的 A/AAAA,就不会迷惑茫然失措了。 实践是最好的理解,1 小时,就对 DNS 的递归解析、迭代查询掌握清楚了。 anyway,DNS 也是 socket 遍历里面的一环,即 gethostbyname () 函数。对于这个函数,其实有一个重要的杀器,用于解决 DNS 劫持的问题,那就是 DOH。 DOH(httpdns) 记的有一次问过一个候选人问题,就是对于一个网络请求,是 TCP 三次握手先发生,还是 HTTPS 安全认证先发生。 这里也会有一个小问题:https 网络请求发出前,会先做 ssl/tls 认证,认证通过后发送的加密数据包才会携带域名。那么 ssl/tls 认证的时候,服务器怎么知道请求的域名,然后给予相应域名的证书呢? 这里其实有个小知识,就是 SNI,即 tls 的 client hello 消息发出的时候,数据包里面会携带当前请求域名: SNI,就是上面截图里面的:Server Name Indication。 如果服务器 Nginx 配置来 N 个域名的解析,那么 Nginx 就会根据 SNI,知道当前请求的域名是什么,然后返回对应的 ssl/tls CA 证书(如果 Nginx 只配置一个域名,那其实有没有 SNI 都没有关系了)。 Surge 默认已经对 DOH 做支持,在软件的控制面板里面有 DNS over https/https/3/QUIC 的配置。 DOH 的支持需要一定的条件,准确来说,需要 client 支持,因为 DOH 的实现,会带来跨域、http 请求头 host 变为 ip 的预准备工作等: client 需要先进行 https 请求,获取目标域名的 ip,而后将域名请求转为 ip 请求(1. 实现 gethostbyname 2. 强行把请求的域名变成 ip)。 client 需要在 ssl/tls 认证的时候,将 SNI 变为域名,而不能是 ip,否则服务器不知道返回那个域名的证书。 client 拿到服务器证书后,需要自行进行证书校验,需要再把 ip 转为域名,和证书里面的域名做匹配,匹配成功才算证书校验合格。 这些在 Mac 端并没有什么问题,上面的系统代理和网卡代理都可以看到,并不需要 client 主动的感知 DNS,Surge 完全可以全部劫持。目前 Chrome 等浏览器都都做了 DOH 的支持。 但是在 app 上还没法完美支持,这里说的就是 Android 和 iOS。 Android 基于常用的 OKHttp 还比较方便实现,因为 OKHttp 提供 gethostbyname () 接口的实现,这样在 DNS 阶段就可以做 DNS 的 hook。然后对 SNI 做一个反射,就可以完成。 iOS 就相当麻烦了,主要原因是系统没有提供 SNI 的 hook 方式,即使通过 fishhook 完成了 gethostbyname () 的 hook,也没法顺利实现 SNI 的 hook。当然也有解,但是难度很大,很容易出错。这里还是期待系统级别的支持。 其实国内对于 DOH 的支持还不足,几个大的互联网公司的确有支持,但是比较黑盒,更多是解决企业问题。 但 DNS 的劫持问题,确实极为恶劣。大企业即使不用担心墙,也实实在在被 DNS 劫持伤害很大。普通用户更是大千世界一个跨不过去的坎。 尾 谨以此文,致敬 Surge。 感谢互联网带来的繁荣与进步,也感谢窃火者,给岁月以文明。 Surge 官网技术说明 为众人抱薪者,不可使其冻毙于风雪。 为大众谋福利者,不可使其孤军奋战。 为自由开路者,不可使其困顿于荆棘。 - from 窃火者

2023/4/16
articleCard.readMore

又十年 - 至三十而立

我一直都在回味前不久过了三十岁的生日,这个年龄坎悄无声息的就来了。 那晚,我和以往一样,回家的比较晚。老婆孩子都在等我,他们做了火锅,庆祝我生日。孩子还用压岁钱,送了我一瓶香水。 我知道那天是我生日,只是我自己也没有多少在乎和关注。但破防的夜宵,还是让我语言沉默和大脑峰回路转。 三十而立了。 当自己走到了三十的路口,停下来看看左右,再看看回头路,才发现目前正在停驻的路口,心里是多么的不想面对以及情绪复杂。 哥哥什么时候三十的,我没有注意。老婆三十的坎,我也没什么顿悟。而父母今年也是六十了,我写文章这一刻才发觉。奶奶也已经九十八了。 其实我本该对亲人,嗯,本该多一些关心的。老婆评价过我,说我对亲人没得感情。我不承认,但也不否认。真要做个度量的话,还是 “不否认” 占据多数的。 张学友近期发布了「又十年」,听着听着,就感慨万千,想表达的能表达的,都在一首歌里。 「一眨眼 又是一個十年」 「生命給我們什麼都不可能拒絕 這句話我當年不了解」 「那些瓜葛糾結 在某一天突然迎刃而解」 十年前这个时候,我还在大学里。应该再过几个月,我就要去无锡工作了。 算一下,这个时候我应该刚和前女友分开,我依稀记得是过年那些天发生的事。有过迷茫有过愤怒,也都显得幼稚,谈恋爱的时候竟没想过分开。 要不是这次回忆,我感觉已经快忘记前女友名字了,当年可都是生离死别的,真是反差。“那些瓜葛糾結,在某一天突然迎刃而解。” 过后这十年,颠簸流离。离开合肥后,流转江苏、上海,现在定居杭州,下一步还未可知。 十年里,在 it 行业经历过 底层爬坡、team leader、pua、996,下一步是什么角色,也未可知。 十年里,从独自生活到结婚生子。举办婚礼的时候还朦朦胧胧,现在看着孩子长成了小大人。 十年里,朋友渐少。最常见的朋友社交就是参加婚礼。很多老同学老朋友也见一次少一次,话不投机以及改变太大,都使得没法做朋友交流。 十年里,体重从 120 升到了 160,最近又降到了 150。十年里酗过酒酗过烟但没赌嫖。 这十年,可真是彻底改变一个人的十年,心境和环境上,不断的打磨着。 三十岁,是一个很重要的人生驿站。 二十岁前是拿着一根吸管小心翼翼的吸取核心养分,有性格心境、知识、社交。这时候对人生了解的不多,都是针对性的喜好。 三十岁前吸收养分的工具变成了八爪鱼,每个触角都在不停的输送着不同的感知,有压力、欢喜、刺激、痛苦、矛盾等等。这是矛盾的十年,很多世界观的强烈反差。 以前我总是想着提出自己的想法。我记得小时候我对老家的一个叔叔说你这样计算数量好累,他问我怎么搞?我说我不知道。 那时候,仅仅是看一件事不舒服,也要提出自己的想法。 现在我就变了,对于周边一些事物,会想着要不要说,或者从其他角度来阐释,甚至会尝试着融入。 有时候孩子有些想法或者动作,我知道是不好的或者不对的,但是并不想直接提出来,我愿意看着这颗种子会不会发芽。 我渴望从另一个维度,来观看或者影响当前的变化,而非直接干预。后面可能会变得更好或者更糟,但我期待着事态的发展。 以前我认为世界就是我想的那样。我的世界观可以 1-1 映射到这个世界。 我猜测这个社会,是所有的人以诚相待,多数人和睦相处,少数人常来常往这样子。我知道有些恶,只是笃定那些恶只是尘埃里的那部分。光,还是铺满大地的。 后来呢,就发现生活、工作中很多都是黑的,连光本身也是黑的。 我得靠自己的分辨能力去识别有用的、无用的、假的、真的、香的、臭的。 经过好多年不断的锤炼,我变得举止谨慎、观望大于发言、思考多于行动。 我的分辨能力有了很大的提升,对待事物会从多个方面来评测和做选择。对于投入价值比,也会刻意的取舍。 多维度的思考、分辨、取舍,是我这些年少有的让我满意的地方。算做送给三十岁的自己的礼物吧。 而我理解的三十而立,这里的立就是个人在环境中的独立。能够有独立的人格、思维、想法、判断、立场,不逐流也不自我否定,知晓短缺点和风险。 站在三十的路口,其实我更多的还是迷茫、担忧。知道和经历的多了,渴望也就越多,满足感的上限也在不断拔高。能力跟不上梦想,是贴心挖心的痛。 前些年我还在挥霍着青春,看着网上一闪而过的「三十、三十五、年薪、工作、家庭」焦虑文,莫得感情。 现在我开始理解了,因为我有了经历、正在经历,这些让人无法言出又深入其中的焦虑,像钉子一样,看着没危险,踩上了又拔不出来。 我感觉自己有时候会变成两个小人,其中一个小人总是对这个社会有说不完的控诉。控诉这个社会的残酷,也表演自己的无能为力给另一个小人看。 另一个小人就知趣的说:“时来天地皆同力,你已经很努力了。万般皆下苦,我们就是那个下。” 这两个小人就是这样安慰着我,我就看着他俩的表演,瞅一下自己的梦想,再瞅一下自己的能力。 已至三十,人生路还要继续。重要的十年不算多,上一个已经走完,下一个刚刚启程。 人生如逆旅,道阻且艰,都是行人。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 又十年 - 原唱張學友 聽說你在去年為了健康已戒了煙 我在每個週末還是會小小喝一點 朋友不太容易約到不再像從前 那些日子可說已完結 有些夜裡我會開車到城裡兜圈 我才領會到什麼叫做往事如煙 一眨眼 又是一個十年 那些瓜葛糾結 在某一天突然迎刃而解 一眨眼 又是一個十年 對於人生起跌 一知半解 我卻已經懂得 應變 一生有幾個理想能夠扛得住歲月 還不如好好地過越來越少的明天 生命給我們什麼都不可能拒絕 這句話我當年不了解 時間它殘忍起來毫無道理可言 我也不忍對你說熱情總會冷卻 一眨眼 又是一個十年 那些瓜葛糾結 在某一天突然迎刃而解 一眨眼 又是一個十年 那些轟轟烈烈 讓人懷念已經跟我沒有 關聯 一眨眼 又是一個十年 那些轟轟烈烈 讓人懷念已經跟我沒有 關聯 還能有多少個十年

2023/4/8
articleCard.readMore

双拼 - 少有的高回报效率技能

前两天教孩子玩 9 皇后版的数独游戏,对于全盘多个空格,如何尽可能快的填充。我提到得找到最容易解决的哪些空格,解决的空格越多,后面空格的数字约束也就越多,也就越好解决。 这个游戏的本质就是如何把每一个空格填充,并且效能最大,贪婪嘛。 今天说的双拼,就是这样一个文本处理的软技能,工作和生活上都是通杀,在之前的提高效率的手艺里也有提到。 生活上手机聊天打字,工作上项目汇报打字,各行各业各个人群都可以用得到。 2 天时间就可以熟练掌握的受益终生的软技能。 什么是双拼 一种汉字的键盘表达方式。是全拼的压缩,同样包含声母和韵母,拼音的更简单表达方式。 使用示例 1. 单字母重复打 如:a→>aa /o→>oo /e→>ee 2. 双字母保持全拼方式 如:an→an /er→er /ou→ou 3. 多字母为首字母 + 韵母所在键。熟悉规则之后,会形成肌肉记忆,和键盘盲打一样。 如:ang 一 > ah  /  shuang pin  一 >  ul pb 双拼方案 双拼方案有很多,有小鹤 / 微软 / 搜狗 / 自然码 / 紫光 / 大牛 / 小月 / 小浪等近 20 种。 推荐 “小鹤双拼”。在设计上,小鹤双拼脱胎于较为古老的自然码双拼,调整了部分韵母的位置,但基本上把使用频率较高的韵母放在了比较易于发力的食指和中指上。不推荐小众或者爱好者的方案,对于研发同学机器切换频率较高,非常见方案可能需要重新搭建环境,影响效率。 码表 首字母 + 韵母 规则: 首字母:U/I/V 比较特别 韵母:都需要背一下。每个按键表示 1-2 个韵母,不会超过 3 个。 其他键盘:和全拼一样功能,无需记忆 双拼特点 所有汉字,最多使用两个按键就可以表达,打字速度非常快,相比全拼提升 50%+。 减少按键频次和肌肉劳损,减小打字出错概率以及出错后的修改复杂度 工作消息经常有错别字,懒得改,让同事难以理解甚至理解错意思。用双拼后,可以减少错字概率,出错后修改复杂度也不大。 生活上聊天消息也一样,打错字的概率和错了之后的修正复杂度,都要低一些,才有可能会去修正。双拼可以做到这一点。 有一定的节奏感,打字过程比较会愉悦 保留了汉字的发音规则,降低入门门槛。原有的全拼技术优化也可以重用,比如高频、方言等。和需要转换思维模式的拆字五笔有较大区别。 支持情况 常见系统 (linux/windows/mac/iphone/android) 均支持双拼,包括系统输入法和三方输入法。(windows 系统输入法默认支持微软双拼,不支持小鹤,需要自行改一下码表,10 分钟可以改完。) 个别双拼方式可能不支持,小众和爱好者方案一般不用,所以影响不大。 学习成本 有一定的学习曲线,但不高。主要就是短期内多使用,尽快形成肌肉记忆。 2-3 天左右可以熟悉使用,1 周可以熟练使用,2 周可以形成肌肉记忆。 肌肉记忆就和键盘盲打一样,让你说键冒 x 的左边是什么字母或者 ;号键冒在哪个位置,你可以说不上来。但是只要一打字,你就能准确的按到那个位置。这就是肌肉记忆。 在线练习:https://api.ihint.me/shuang/ 微信小程序:双拼练习

2023/1/27
articleCard.readMore

Vim 技巧

一年前写了一篇提高效率的手艺,提到了 Vim 作为划时代的文本编辑工具,可以有效的提高文本编辑的效率,对于写作或者 coding 都非常有用。 今天整理一个稍微进阶的教程。vimer 十几年的 95% 的操作使用,应该都在这里。 Vim 作者,Bram 于 2023.08.05 日去世了,享年 62 岁。像我这种只要碰键盘就离不开 vim 的人,对 vim 的感激,是十分强烈的。悼念 Bram。 计算机的基石和发展,也就这几十年。那一批有卓越贡献的人,后面二十年会相继离去。C 语言之父丹尼斯十年前走了,70 岁。后面会越来越多。 按键 单个行为操作 移动:hjkl /w/e /b/ $ / 0 (^) 删除:dd /x 行合并:J 换行:o / O 撤回 / 返回:u / C+r 插入:i / (I-A-a) 查找:f (-;-,) /t/ * / # 替换:r 括号匹配:% 黏贴复制:yy-dd/p 拖屏:HLM /zz (zt-zb) /c+e /c+y /c+f (d-b-u) / :n / G /gg 位置标记:ma-‘a (mx - moving - ‘x) 保存退出::wq(https://stackoverflow.com/questions/11828270/how-do-i-exit-vim) 4 种模式 esc:普通模式 i/a:插入模式 shift+::命令模式 ctrl+v:区块模式 多个行为组合 动词:d /c/y /v/ … 介词:i /a/f /t/ … 名词:w /e/p / {/} / ( / ) / [ / ] / “ / ‘ / 字符 / … 动词 - (数字) - (介词) - 名词 dw / dt? / ci) / ca} / d2w / y$ / y3w / v2i) 数字 - 动词 - (名词) 6ixy / 4p / 2dw / 0y$ 寄存器 录制:q [a-z] - anyaction - q 使用录制:3@[a-z] 寄存器使用 多行文本的行首 / 行尾增加同样的内容 使用区块:c+v -> j/h -> $/0/^ -> I/A -> input -> esc 使用寄存器:qa -> $/0/^ -> I/A -> input -> esc -> j -> q | 3@a 多行文本,增加序号 :let i=0(定义变量) qa(开始录制) 0 -> i -> input -> esc -> 0(到行首输入需要的分割等内容,然后回到普通模式,再次回到行首) :let @n=i(变量放入寄存器) :let i=i+1(变量 + 1,供下次使用) “nP(将寄存器的值复制到行首之前) j(进入下一行) q(结束录制) 3@a(回放 3 次) 配置 ~/.vimrc,示例如下: vimrc/basic.vim at master · amix/vimrc 插件管理器 Vundle https://github.com/VundleVim/Vundle.vim 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 set nocompatible " be iMproved, required filetype off " required " set the runtime path to include Vundle and initialize set rtp+=~/.vim/bundle/Vundle.vim call vundle#begin() " alternatively, pass a path where Vundle should install plugins "call vundle#begin('~/some/path/here') " let Vundle manage Vundle, required Plugin 'VundleVim/Vundle.vim' " The following are examples of different formats supported. " Keep Plugin commands between vundle#begin/end. " plugin on GitHub repo Plugin 'tpope/vim-fugitive' " plugin from <http://vim-scripts.org/vim/scripts.html> " Plugin 'L9' " Git plugin not hosted on GitHub Plugin 'git://git.wincent.com/command-t.git' " git repos on your local machine (i.e. when working on your own plugin) Plugin 'file:///home/gmarik/path/to/plugin' " The sparkup vim script is in a subdirectory of this repo called vim. " Pass the path to set the runtimepath properly. Plugin 'rstacruz/sparkup', {'rtp': 'vim/'} " Install L9 and avoid a Naming conflict if you've already installed a " different version somewhere else. " Plugin 'ascenator/L9', {'name': 'newL9'} " All of your Plugins must be added before the following line call vundle#end() " required filetype plugin indent on " required " To ignore plugin indent changes, instead use: "filetype plugin on " " Brief help " :PluginList - lists configured plugins " :PluginInstall - installs plugins; append `!` to update or just :PluginUpdate " :PluginSearch foo - searches for foo; append `!` to refresh local cache " :PluginClean - confirms removal of unused plugins; append `!` to auto-approve removal " " see :h vundle for more details or wiki for FAQ " Put your non-Plugin stuff after this line Dein https://github.com/Shougo/dein.vim 1 2 sh -c "$(wget -O- <https://raw.githubusercontent.com/Shougo/dein-installer.vim/master/installer.sh>)" 插件 示例:https://github.com/preservim/nerdtree 1 2 3 4 5 6 7 8 9 10 11 // Vundle call vundle#begin() Plugin 'preservim/nerdtree' call vundle#end() --- // Dein call dein#begin() call dein#add('preservim/nerdtree') call dein#end() 插件就 Google 尽情的搜索吧,选择自己喜欢和必须的就好。Vim 插件装多了会卡,NeoVim 不会。 插件都是程序员非商用的产出,都是没有什么美感的。有些配置还比较繁琐,使用问题不大。 就这样。 NeoVim 和 vim 有配置上的差异,使用上一样。推荐使用 NeoVim。 上面的配置、插件管理器等都是 Vim 的,NeoVim 会有一些不一样,但大体也都是一致的。插件也都是能共用的,很多插件也为 NeoVim 做了专门适配。 IDE https://spacevim.org/ 多种开发语言的配置箱 缓存 / 窗口管理方便 撤销树 提供的快捷帮助命令使用很方便 简化配置 命令终端使用非常方便 工作使用 Vim 终端 单文件查看 / 编辑 系统文件配置 coding 集成 IDE vscode (安装 vim 插件):全局搜索 / 代码阅读 /json 格式化 xcode (打开 vim 模式):函数调用链 /code 离开鼠标 网页 vim 插件(chrome/safari) 文件浏览器 Ranger 全键盘 homerow https://www.homerow.app/ 写作推荐 问题: 频繁修改前面写过的内容,使用 Vim 可以更专注于内容。避免手的移动干扰思路。 中文场景下 vim 体验不好(普通模式下的键盘操作需要英文) 推荐: Xcode xcode 官方自定义了一套 vim,适配中文场景非常棒。写作过程中基本不用再切换输入法。 如果需要搭建复杂文档,有多个文件和文件夹,可以通过 git 、workspace、playground 自行配置。 git:云存储 workspace:多个文件系统做隔离 playground:markdown 和 预览

2023/1/26
articleCard.readMore

移动端日志系统怎么搭建

端上日志系统非常重要,对于用户侧的异常、排障、动线、行为等很多重要数据,都可以通过端上日志来做检索。如何搭建一套准确、高性能的完备日志 SDK 就显得尤为重要。 (另一个重要的排障信息源是埋点,通过埋点可以获取更精准的用户动线。后面有时间做一下埋点数据化方面的总结)。 移动端日志系统,将承载 Native、h5、动态化等多技术栈环境下的日志收口工作,同时要兼顾日志不丢不乱和高性能,其实还是有不少挑战的。 这一方面 CocoaLumberjack 其实已经做的很好,很多公司都用它作为自己的日志系统的基础框架。但它还不能作为大型 app 的流量日志收口系统。因为流量大了以后,少量的日志丢失也会带来很大的缺口,而性能方面它也有很多短板。 mmap 可以在 IO 性能方面有显著的提升,也就是后端比较通用的零拷贝技术。在移动端上 FastImageCache 对 mmap 有较深的应用,但它业务绑定太强,一般无法直接使用,更多的是学习 mmap 的落地。 本文会对日志系统的一些完备要素做一些说明,并特别讲解下 CocoaLumberjack、FastImageCache 两个技术库。 日志是否全量。很多业务开发同学不使用日志 SDK 做日志输出,可能使用系统日志做打印,这一部分日志是否需要做收口,需要权衡一下。 分等级和模块。分 Level 和 modules 进行记录和检索,可以提供问题排查速度。这方面有专业的 Debug/Info/Error 等标准。技术上比较好实现。 性能。主要是卡顿和耗电。日志系统会底层基础架构,大流量的打入,会频繁的内存释放、I/O,过多的占用 CPU 会导致卡顿和耗电,也会影响拖延上层业务异步代码的执行时机。 数据不丢不乱。没有办法做到完全的不丢日志,只能尽可能少的减少。在 crash / CPU 繁忙 / 压缩加密严重耗时拖延队列等异常场景,这方面问题尤其突出。不乱就需要增加串型队列或者锁,这些同步机制都需要保障阻塞和性能。 实时观测。开发和测试同学需要能够实时看到日志打印情况,这在开发和提测阶段非常有用。 压缩 + 加密。压缩和加密都是耗时操作,对 CPU 的压力比较大。 上报。得有回捞机制,得保障数据传输安全。 后端系统。前置的采集完成后,后端系统的数据化检索、数据可视化等工作都是重中之重。 隐私安全。这一块国内所有厂商都极度匮乏。大厂的开发同学也可以随意捞取用户日志,异常日志在内部系统可以随意传播。如果有这方面的诉求,那么整套安全体系都需要建立起来。 全量日志的收集 端上打日志的方式有很多,尤其在日志系统没有搭建之前,每个开发同学可能搭建了自己的小型日志记录框架或者系统特有的 log api。如果业务有需要,这部分日志 SDK 之外的流量是可以收口的。 对于 iOS 来说,比较常见的就是 nslog、printf、os_log,少见的是 write,NSFileHandle,syslog,nslogv。其中 nslog 场景最多,需要特别处理。 下面分别对这些系统日志系统做一些技术说明。 日志接口说明 stdin/stdout/stderr 2023.12.14 更:【Swift 三方源码 1】SwiftShell 高效的命令行工具 有更完善的描述 这是三个默认的 Unix 系统输入输出终端(终端也是文件)。printf 使用的 stdout 输出,即标准输出。而 nslog,苹果对其定义是异常和错误记录,使用的是 stderr 输出。 所以如果想屏蔽 / 重定向 xcode 的日志输出,printf 和 nslog 需要分别使用 freopen(file,"a+",stdout); 和 freopen(file,"a+",stderr); 进行输出重定向。 1 2 3 4 5 // unistd.h #define STDIN_FILENO 0 /* standard input file descriptor */ #define STDOUT_FILENO 1 /* standard output file descriptor */ #define STDERR_FILENO 2 /* standard error file descriptor */ printf、write printf 是终端打印 api,默认做 stdout 标准输出打印。如果没有终端,就不会有打印了。write 是文件写入 api,可以将文本、二进制等数据写入到指定的文件中。 printf 底层是用 write 实现的,即 write 的指定文件是 stdout,即终端。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // uio.h struct iovec v[13]; // 拼装 v v[0].iov_base = "testLog:"; v[0].iov_len = 8; v[1].iov_base = (char *)msg; v[1].iov_len = msgLength; // 输出到 stderr 终端,模仿 nslog writev(STDERR_FILENO, v, 2); // 输出到 stdout 终端,模仿 printf writev(STDOUT_FILENO, v, 2); // 输出到文件 writev(file, v, 2); 这里有个注意的地方,如果没有终端,如真机运行的时候,虽然不会打印日志,但是 printf 的代码执行流程不会停止。 因为有重定向等操作,在调用 printf 的时候,是不知道最后到底要不要输出的,这个时候还处于用户态。所以 printf 的所有代码都会执行一遍,最后进行系统调用。 系统调用的时候,才知道输出到哪里,如果没有终端那就不输出。 所以对于 release 线上包,即使 printf 最终没有终端来打印,但用户态的代码逻辑都会被执行。这其实是无用的损耗。 syslog syslog 是 Unix 系统下的常用日志系统,iOS 通过引入 sys/syslog.h 也可以使用。 syslog 会将信息打印到终端,并保存到 var/log/syslog 文件中,还可以将日志文件进行压缩,以及实时网络传输功能。syslog 是一整套日志服务工具,跨平台使用非常棒。 直观来看,可以认为 syslog 是 printf 的大号升级版。printf 只能做终端输出,syslog 还可以做文件存储等工作。syslog 是跨平台的日志方案,可以进行多样的配置以满足多样需求。 1 2 3 4 5 // sys/syslog.h openlog("info",LOG_PID,LOG_LOCAL5); syslog(LOG_INFO, "hello %s","woring"); closelog(); nslog、nslogv、asl、oslog nslog 和 syslog 在同一个级别,对于自家系统,苹果可能觉得 syslog 太大或者性能等综合考虑,重新做了一套 ASL 系统,后面又做了一套 oslog 系统。 ASL 主要做了两件事,oslog 也有同样的功能。 输出日志到 asl 系统。asl 系统会对日志做文件存储,也可以通过 mac 的 console.app 软件进行日志的实时查看、检索、过滤。 输出日志到 xcode 控制台 在 iOS 10 之前,苹果使用的是 ASL 系统。后面觉得性能有待提升以及跨平台,升级到了 oslog。 在 CocoaLumberjack 中作者是这么描述 nslog 和 ASL 的: 1 2 3 4 5 6 7 8 9 10 11 12 13 // nslog * the traditional NSLog() function directs its output to two places: * * - Apple System Log * - StdErr (if stderr is a TTY) so log statements show up in Xcode console // asl NSLog does 2 things: * It writes log messages to the Apple System Logging (asl) facility. This allows log messages to show up in Console.app. * It also checks to see if the application’s stderr stream is going to a terminal (such as when the application is being run via Xcode). If so it writes the log message to stderr (so that it shows up in the Xcode console). To send a log message to the ASL facility, you basically open a client connection to the ASL daemon and send the message. BUT - each thread must use a separate client connection. So, to be thread safe, every time NSLog is called it opens a new asl client connection, sends the message, and then closes the connection. NSLog 会向 ASL 发送日志信息,会出现在Console.app 中。同时向 Terminal 发送日志信息。 并且每一次 NSLog 的输出,都会新建一个ASL client 并向 ASL 守护进程发起连接,发送日志信息之后再关闭连接。 oslog 会做一些内存和磁盘的优化,具体没有看,性能肯定有一些提升。而且做到了多平台,在苹果全家桶上面都可以用了。 nslog 在 iOS 10 之前使用的是 asl,之后也迁移到了 oslog 了。nslog 是通过 nslogv 实现的,它们没有太多区别。 nslog 对于终端的输出,最终会通过 fprintf 实现,fprintf(stderr, "%s\n", buf);,最后也是和 printf 一样通过 write 对 stderr 进行写入。 nslog 对 asl 的操作,是通过 asl.h 接口实现的。 1 2 3 4 5 6 7 #import <asl.h> aslclient _client = asl_open(NULL, "com.apple.console", 0); aslmsg m = asl_new(ASL_TYPE_MSG); asl_set(m, ASL_KEY_MSG, "msg") asl_send(_client, m); asl_free(m); 如何捕获全量日志 其实最全的捕获方式,就是 hook write 接口。因为这是 C 对于文件写入的用户态底层接口,只要是日志都会走到这里。通过 fishhook 可以做进一步的捕获。 不过这个接口是文件写入接口,除了日志,其他写操作也会走到这里,会有很大的数据干扰。最好还是在上层进行流量捕获。 printf 的捕获,可以直接 hook,没有太多办法。 基于 ASL 的 nslog,可以通过系统 api 进行捕获,在 CocoaLumberjack 的 DDASLLogCapture.{h/m} 文件中有详细说明,可以借鉴。 基于 oslog 的 nslog,就没有系统 api 了。可以直接 hook nslog/nslogv 来捕获。建议 hook nslogv,因为有些业务同学可能自己搭建了一套小型的业务日志系统,会直接使用 nslogv。如果 hook nslog,会有遗漏。 对于 nslog 的捕获,也有 stderr 重定向 (dup2,pipe) 等方式,但感觉没有 hook 来的方便,因为重定向需要操作文本,pipe 需要操作数据管道,实时性、性能、复杂度都比不上直接 hook。 对于 os_log 的专有日志 api,也可以 hook。 所以捕获全量日志,就靠 fishhook 了,它依靠动态库的函数符号三级跳的特性,改写符号地址来实现 hook,本身非常小巧。但是 fishhook 只能 hook 动态库的函数符号,自己写的一些 C 函数是 hook 不了的。 日志系统的流量如何实时观测 开发和测试同学需要能够实时看到日志打印情况,这在开发和提测阶段非常有用。业务多了之后日志会大量刷屏,一定要对观测数据做行过滤。 所以日志的实时观测有这三个强诉求: 打印内容足够多。xcode 和 console.app 会对内容做剪裁,超过一定长度大小后的内容就不展示了,这个需要处理。 支持行过滤,需要对字典 / 数组等容器对象做序列化,将内容拼接成一行文本。 内容不乱。xcode 的终端输出,会出现文本相互嵌套乱序的情况。主要是因为输出缓冲区的缘故,容器内容输出有太多换行符。将每个日志拼接成一行文本可以解决这个问题。 打印内容足够多 通过 nslog/os_log 输出,xcode 和 console.app 会有字符长度限制。通过 printf 输出可以突破这个限制,但是 printf 前面说过只会输出到 stdout,并不会输出到 console.app,对于测试同学就看不到日志了。 单独使用 nslog/printf 是无法同时满足开发和测试同学的诉求的。 可以在 ide 环境将日志做 printf 输出,先满足开发同学的全量数据观测。 提测阶段,不在 xcode 下运行,字符长度限制会更小。有两个解决方案: 将长内容按照 200 长度大小做分割编号后打印(开发阶段字符长度限制大约在 800-1000) 开发一个 pc 版本的在线日志工具。通过 CocoaHttpServer 这样的轮子,将端上日志通过 http/socket 实时传输到 pc 端 支持行过滤 & 内容不乱 这个没什么要多说的,主要是为了方便检索和过滤,用处非常大。 nslog/os_log 的使用 其实这里可以看到,nslog 可以完全不用的。我们使用 nslog 主要是做调试输出,到线上后就基本不关注了。 从上面对 nslog 的解释可以看到,nslog 在苹果看来是异常 / 错误的日志输出,而且有很大的性能开销,不管使用 asl 还是 oslog 系统。 而日志的磁盘记录和上报,则需要专门的文件接口实现,下面会说到。 如果排除测试同学的数据观测,我们可以直接使用 printf 来实现日志打印。 但还是要注意,线上不能开 printf。前面介绍 printf 的时候说到过,虽然线上没有 stdout,但 printf 的用户态代码还是会执行的,只是内核态会找不到 stdout,不做输出而已。 nslog/os_log 也不应该到线上,线上包是可以通过 console.app 查看 nslog 打印出来的日志的。一些调试信息很容易被检索到,会有数据安全问题。 终端日志轮子 CocoaLumberjack 的 DDTTYLogger.{h/m} 文件,对 xcode 的终端输出有现成的实现,可以直接使用。 它使用的是 writev(file, v, 2); 接口,前面有说明,write 是 nslog、printf 的底层实现。 它里面有个技巧,就是如果日志内容不多,会通过 alloca 申请栈空间做 msg 存储。日志程度较大的时候,才会通过 calloc 申请堆空间。 日志采集系统设计 CocoaLumberjack 做的很好了,基本上自研的日志系统或者小型日志模块都会对它进行各种程度的借鉴。 CocoaLumberjack 主要是通过一个流量入口,进行多个渠道的流量分发。 研发同学通过宏定义可以减少代码量和日志代码侵入,在 sdk 内部,做 磁盘文件 / 终端 / 数据库 / 网络 等多渠道的分发,通过 level 做不同层级的日志隔离。可以再加一下 modules 的区分,它里面没有实现,可以在上层实现。 CocoaLumberjack 亮点是支持业务侧的高度自定义。可以对日志进行 format,在上层可以快速接入压缩层、加密层、网络层等业务逻辑。 但 CocoaLumberjack 并不是一个高效率的日志采集 sdk,因为它核心的功能磁盘文件存储性能不高,每条日志都会直接操作 IO,内部会频繁的进行文件排序 / 文件大小检查等耗时操作。 业务自定义的压缩加密等模块很可能有超时情况也会进一步增加它内部的队列堆积。 在大用户量的 app 中,上层业务非常复杂,日志流量会非常大。这些问题很可能导致数据丢失和 CPU 爆增。 解决不丢不乱和性能问题 CocoaLumberjack 的性能问题产生原因就是因为日志数据直接入串型队列。虽然它内部通过子线程消费队列,但改变不了队列锁粒度太大的事实。 它内部会将每一个日志分发到所有日志处理源,各个处理源在内部队列中消费完毕后,通过 group notice 结束当前日志的处理,然后处理串型队列里面的下一个日志。 整个分发流程的锁粒度非常大,任何一个环节有耗时操作,都会造成当前任务延迟完成,造成队列任务堆积。这个时候用户退出 app,就会导致数据丢失。 为了使得日志不乱,对 IO 的操作一定得是单线程的,否则日志内容会嵌套就没法看了,这个无法改变。 既然锁存在的事实无法改变,那么就需要在前置链路进一步降低锁的粒度。 日志分发到每个渠道后,各个渠道可以自行消费,无需阻塞日志队列。 各个分发渠道需要尽可能的使用并发队列和多线程。 磁盘存储 sdk 模块可以对日志内容进行并发压缩 / 加密处理(流式处理),通过容器增加序列号用作最后的快速排序。也可以不排序,数据上报后让后端服务进行排序。 日志数据可以先组装到内存,内存容器多线程写入,保持一个小粒度的锁防止异常。在时机满足后内存数据批量存储磁盘,减少 IO 次数。 磁盘存储可以使用后端常用的 零拷贝 /mmap 技术,使得用户态共享内核态的页缓存,避免多次无效数据拷贝。还可以共享系统的文件脏数据写入,不用在上层做冗余的 crash 等异常维护。 这里可以通过并发队列 + 多线程减少锁粒度来快速消费数据,通过内存暂存快速保存数据并减少 IO,通过 mmap 减少磁盘文件的写入耗时和异常处理。 其中有一个环节就是并发处理后的日志排序,建议是数据上报后让后端来做。毕竟前端的时间少一点,带来的价值就是非常大的。每一个优化,不都是争取减少那么一点时间么。后端只会处理上报的异常用户数据,即使增加几秒排序的时间,也是值得的。 这里的未排序并不是乱序,乱序是多个日志内容出现嵌套,这里虽然没有排序,但每条日志都是独立的,不会出现嵌套情况。 mmap 的应用,在下面会通过 FastImageCache 做单独说明。 回捞、上报、数据安全、其他 回捞功能必不可少,需要用户日志,基本都是回捞场景。 回捞肯定得通过 socket 来做,如果已经有 IM 功能,那么可以直接集成。如果没有实时通讯能力,依靠 push 也可以来做。 回捞的时候需要注意一下回捞哪些时间的文件,以及文件大小处理。如果文件过大,建议分批回捞,防止一次性回捞的时候每次都失败。 上报可以走统一的网络服务了。日志文件一般都会比较大,动不动就是十几 M 了。可以走 oss 服务,切片上传。 数据安全还是比较重要的,虽然国内公司都不重视这个。数据安全不仅仅是研发侧打的日志是否有敏感内容、网络过程的数据安全,还包括研发同学捞取用户日志的申请流程等。 加密一般都是使用的对称加密,速度快一些。密钥都会存储在 app 内部,安全性高一些的还会通过图片来存储密钥。但总归是写死到本地的,破解人员可以静态资源或者二进制串改密钥等方式进行破解,还是有风险的。 这里可以借鉴 HTTPS,客户端生成公私钥对,把公钥给到服务端,让服务端把对称密钥通过公钥加密给到客户端。这样可以降低日志被解密的风险。 还有一个安全点就是基于 asl 和 oslog 的日志打印。很多同学不知道这个日志打印的内容,是可以通过 console.app 查看的,即使是线上包。很多大厂 app 都有这个问题,比如网络的重联、内存释放信息等调试数据,在大厂的 app 线上包,都有存在。如果打印了用户的一些敏感信息,就很危险了。 对于 PC 侧的用户动线等数据消费可视化,可以通过日志的 level、module、page、压后台、进前台等各种行为做可视化。 FastImageCache&mmap mmap 在高性能日志场景都被使用了,包括微信的 xlog 和滴滴的 logan。主要用于解决 IO 频繁操作、脏数据写入、异常场景处理问题。 FastImageCache 是对 mmap 用的比较深入的 iOS 图片缓存库。 它和 sdwebimage 等其他的图片缓存库相比,唯一的特色就是对解码后的数据进行本地缓存。 它本身的设计,是业务强依赖的设计,并不好直接使用。但是它的技术实现并不复杂,而且还有一些缓存周期等技术瑕疵。完全可以脱离它的设计,自己设计一套符合自身业务场景的图片缓存。 FastImageCache 的核心就是下面一些代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 通过 mmap 将文件直接映射到虚拟内存 _bytes = mmap(NULL, _length, (PROT_READ|PROT_WRITE), (MAP_FILE|MAP_SHARED), fileDescriptor, _fileOffset); // 将上下文映射到虚拟内存,待上下文绘制完毕,生成的解码数据也就在虚拟内存里。通过 msync 将数据同步写入文件 CGContextRef context = CGBitmapContextCreate([entryData bytes], pixelSize.width, pixelSize.height, bitsPerComponent, _imageRowLength, colorSpace, bitmapInfo); imageDrawingBlock(context, [_imageFormat imageSize]); CGContextRelease(context); int result = msync(pageAlignedAddress, bytesToFlush, MS_SYNC); // 对虚拟内存进行读取,拿到文件中的图片解码 data,然后直接生成 uiimage 对象。 // 1. 通过 mmap 拿到文件缓存,不用通过内核态和用户态做二次拷贝 // 2. 通过解码数据直接生成 uiimage 对象,避免了 CPU 硬解耗时 CGDataProviderRef dataProvider = CGDataProviderCreateWithData((__bridge_retained void *)entryData, [entryData bytes], [entryData imageLength], _FICReleaseImageData); CGImageRef imageRef = CGImageCreate(pixelSize.width, pixelSize.height, bitsPerComponent, bitsPerPixel, _imageRowLength, colorSpace, bitmapInfo, dataProvider, NULL, false, (CGColorRenderingIntent)0); image = [[UIImage alloc] initWithCGImage:imageRef scale:_screenScale orientation:UIImageOrientationUp]; 其他代码也有很多,但都是为了维持 FastImageCache 它自身设计的那份数据结构,包括 table/entry/chunk 等,就是不断的套娃,方便数据的检索。 FastImageCache 最好不要直接拿来用,业务改动会非常大。自己模仿写一个不会太复杂,而且可以规避它那边现有的一些缺陷。 比如它有一个 maximumCount 参数用来设置每个 table 的最大 entry 容量,实际上内部没有太多的实现,想超多少超多少。而且这个值重新设置后,会因为配置内容发生改变,所有缓存文件会全部删除。 它内部对每个 mmap 的切片大小也有些小,也不支持外部改变。毕竟都是七八年前的开源库了,那时候存储和流量还都比较贵。 其他还有很多,比如缓存文件的种类、大小等设计,都有很大的局限性。 但是它里面的像素对齐和字节对齐是非常有用的,如果想写一套,这个一定要抄下作业。 又一年没看春晚,看样子是戒了,开心。

2023/1/23
articleCard.readMore

数字签名

数字签名可以解决数据安全里面的完整性,身份认证和不可否认三大特性,但是解决不了机密性问题。机密性需要通过对称密钥 / 公私钥解决,所以数字签名其实和加解密 / 密文 / 机密性这些在概念上非一个层次。 签名的本质不在于加解密,而是加验签。 数字签名,定义上是拿着私钥的一方,通过私钥 X 对消息 M 进行加密 (加签),生成签名 N,并把消息 M 和签名 N 一起给出去。拿着公钥的一方,用公钥 Y 对签名 N 进行解密 (验签) 生成 M’。用消息 M 和 M’ 做比较,如果相等,则数据没有被篡改。如果不想等,则数据不再安全。 对于公私钥,我们一般是使用公钥加密私钥解密,这样可以保障单向数据机密性。而私钥加密公钥解密是解决不了机密性问题的,如果要通过公私钥完全解决数据机密性,则需要双向认证。 数字签名就使用了私钥加密公钥解密这套方案,所以数字签名是没有密文 / 机密性可言的。在数据签名里,一般把私钥加密叫做加签,公钥解密叫做验签。 对称场景下摘要保障完整性 摘要和摘要算法就不解释了。摘要可以保障完整性,说的是对称加密场景下。 这里有一个疑问点,那就是对称加密场景下,数据本身已经安全了 (机密性)。即中间人无法解开信息,也就无法更改内容。这时候为什么还需要保障完整性? 这里说的对称加密,是排除密钥不安全的场景的。我们已经确定双方持有的对称密钥一定是安全的。这时候黑客虽然拿不到会话密钥,无法破解密文,但可以通过窃听收集到足够多的密文,再尝试着修改、重组后发给服务端。因为没有完整性保证,服务端会 “照单全收”,然后黑客就可以通过服务端的响应获取进一步的线索,最终就会破解出明文。 如果对称加密算法足够安全、对称密钥足够长、使用一次性对称密钥减少通信密文的数量,这些都可以进一步增加破解明文的时间。 但如果因为一些原因,没有上面的安全性保障,就有可能让黑客拿到足够多的样本,增加风险。这时候可以通过摘要来保障密文的篡改: 有了摘要保障完整性,服务端拿到数据后就可以校验一下,不符合完整性要求就可以直接拒绝服务了。 从这里可以看到,摘要就是原数据小且精确的数据替身。通过这个替身只能做到数据完整性校验,对于身份认证这些就做不了了。 实际上单纯在对称场景下,是无法保障数据安全的。因为最多只能做到机密性和完整性,而无法确定密钥的有效性,即身份认证和不可否认。 非对称场景下的摘要和数字签名 开头我们说到了数字签名的定义,从定义上可以看到,数字签名不依赖摘要。前面说到摘要是原数据小且精确的替身,既然是替身,那么数字签名就可以直接操作原数据避开摘要。 因为非对称加密的性能原因,公私钥对大数据的加解密是非常耗时的。有些数据可能达到几百 M 或者几个 G,用公私钥对这些原数据操作的代价是非常大的。 所以数字签名一般不直接对原数据进行加验签,而是通过摘要实现。数据签名并不一定非要使用摘要,但是一般都使用摘要。 1 2 3 性能原因主要发生在原数据过大的场景: 1. 私钥加签耗时严重 2. 签名过大,增加传输时间和流量,验签的相等性依靠字符串匹配算法,也会增加耗时。 数字签名流程如下: 和对称场景下的密文 / 机密性 / 完整性不同,数据签名只用于非对称场景下的明文数据。即: 1.(完整性)我给了你一份明文数据,这份数据虽然大家都可以看到,但我通过数据签名可以保障给你的数据一定是完整的,不会被其他人修改里面的内容,篡改了内容你肯定验签失败。 2.(身份认证)我还可以保障给你的这份数据一定是我给你的,因为你用我的公钥解开了数据,如果不是我亲自用私钥加签,你那边不可能解开数据,验签肯定失败。 3. (不可否认)而且以后我也否认不了本次交易的事实。因为基于完整性和身份认证两个前提,你拿到的数据一定是我亲自给你的有效数据。我无法否认本次交易的数据内容和本次交易的存在事实。 完成这套机制的核心就在于,私钥是非公开的。因为私钥的绝对安全和单一持有,保障了加签人和加签数据的唯一性。 这里说数据签名用于明文场景,是因为数据签名定义上,就是使用私钥进行加签。但加签的数据可以被所有公钥验签后获取,所以没有私密性可言。这也符合非对称加密的特点,即单向安全。数据签名正好使用的是反向,自然就没有密文一说了。 非对称加密只有在公钥加密私钥解密情况下才是安全的,这就是单向认证。如果希望双方的数据都是安全的,就需要使用双向认证了。 数字签名应用场景:PassKey 天下苦 登陆 & 验证码 久矣。 passkey 无密码登陆,是相当期待的功能。目前 Google 平台已经全面上线,Apple 和 微软 都已经对 passkey 做了支持。 无密码登陆,就是通过在终端如 iPhone/Android/Mac/Window 上,建立一份公私钥。 注册的时候,将终端私钥上传到服务端。 登陆的时候,服务端给一个校验字符串,让终端来加签。最后服务端用当初的公钥来解签。能解开并且和校验字符串一致,则表示当前用户值得信任。 https://www.passkeys.io/ 已经做了 passkey 注册的 demo,我体验了一遍,效果很棒。部分流程如下: 图中,是通过 iPhone 终端登录的。其实也可以通过 Mac 端直接登录,这样就可以少了换端的成本。 当通过其他端同步的时候,其实 chrome 浏览器是通过开启一个 socket 通道,使用中继服务器完成 passkey 的获取。 这里其实还有两个问题可以解答: 终端的公私钥,该怎么保存?这里 iPhone 会通过端侧加密和 iCloud 同步的方案,在 Apple 生态的机子上共享。还可以分享给他人。Android 等终端也都有差不多的能力。 终端如何确保不是他人来申请 passkey 能力?这里一般通过指纹和面部识别,也可以通过其他硬件辅助验证。 下面是整套 passkey 注册和登录的流程图: 数字签名应用场景:SSL (2025.01.18 更) 在 7 层网络协议里,SSL 分为握手协议和记录协议,分别处于表示层和会话层,主要负责网络数据的传输安全。 从对称场景下的摘要可以知道,对称场景下是没有身份认证的,这样就会使得密钥存疑。如果 A 和 B 通信的密钥被 C 给更换了,那么 C 就可以假冒 B 与 A 通信。所以 HTTP 退出了历史舞台。 非对称加密的双向认证是可以解决这个问题,但因为非对称加密耗时厉害,没有被有效的采用。所以就有了通过数字签名来传输公钥,通过单向安全性把临时对称密钥给到对方,而后双方使用对称密钥通信的策略。这就是 HTTPS (SSL/TSL) 使用的方案。 HTTPS 的安全核心在于对称密钥 (或者种子) 的安全传输。根据非对称加密单向安全性,只要拥有公钥的一方生成对称密钥并把密钥通过公钥加密给到另一方,拥有私钥的对方才可以解开数据并拿到密钥,这样就安全传输了。 而数据签名之所以能传递有效的公钥,就是因为它具有完整性、身份认证、不可否认的特点。拿到的公钥,一定是安全的公钥,如果被动了手脚,一定会被发现。 SSL 证书格式 SSL 证书,是一个二进制文件或者 base64 文件,包含源信息(公钥、网站名称、有效期等)和数字签名(背书机构通过其私钥对源信息进行的签名)。 其中,每个 SSL pem 证书包含的公钥是当前证书管理者的公钥,数字签名是上级证书管理者对当前证书的签名。 对于用户申请的 SSL 证书(向中间 CA 机构申请)pem 格式,一般包含以下几个部分: 证书信息:包含证书的有效期、颁发机构、域名、签名算法等 公钥:当前用户网站 / 域名的公钥(私钥也只有当前用户知晓,保存于用户的服务器,用于后续的 https 对称通信密钥的生成) 数字签名:中间 CA 机构私钥对当前源信息(证书信息 + 公钥 + Other)进行加签后生成的签名,用于验证当前证书完整性 SSL 证书是通过证书链来进行验证的,下一级证书(中间 CA)的信息如下: 证书信息:包含证书的有效期、颁发机构、签名算法等 公钥:中间 CA 机构的公钥 数字签名:根 CA 机构私钥对当前源信息 (中间 CA 证书) 进行加签后生成的签名,用于验证当前证书完整性 根 CA 证书信息如下: 证书信息:包含证书的有效期、颁发机构、签名算法等 公钥:根 CA 机构的公钥 数字签名:根 CA 机构私钥对当前源信息 (根 CA 证书) 进行加签后生成的签名,用于验证当前证书完整性 证书链 在具有安全效益的证书中,都是采用【证书链】机制的。即每一个【终端证书】,都是通过【根证书】或者【中间证书】签发的(层层签发)。 比如 A 网站,它向 M 中间 CA 机构申请 SSL 证书的时候,机构会提供证书链供下载,包括【A 终端证书】、【M 中间证书】和【根证书】。 用户 T 访问 A 访问的时候,服务器返回的 pem 会包含【A 终端证书】、【M 中间证书】,一般不用返回根证书,因为用户的电脑里面有【根证书】,这是信任链的源头。 证书链验证流程: 正向解析证书链 浏览器根据 A 证书、M 证书,分析出 根证书,从而确定证书链:A -> M -> 本机根证书 逆向验证证书链 默认相信本机根证书,这是信任的源头 浏览器根据本机根证书里面的公钥,验证 M 证书是否有效 a. 浏览器验证 M 证书有效期等信息是否合法 b. 浏览器根据 M 证书的签名算法规则,对【源信息】生成【摘要】 c. 浏览器根据根证书的公钥,对 M 证书里面的签名进行解签,得到【解签后的摘要】 d. 比较【源信息摘要】和【解签后的摘要】是否一致。若一致,则 M 证书有效。提取 M 证书里面的公钥进入下一层使用。 浏览器根据 M 证书里面的公钥,验证 A 证书是否有效 a. 浏览器验证 A 证书有效期等信息是否合法 b. 浏览器根据 A 证书的签名算法规则,对【源信息】生成【摘要】 c. 浏览器根据上一层验证拿到的 M 证书的公钥,对 A 证书里面的签名进行解签,得到【解签后的摘要】 d. 比较【源信息摘要】和【解签后的摘要】是否一致。若一致,则 A 证书有效。提取 A 证书里面的公钥进入下一层使用。 验证 A 证书有效后,后续的 HTTPS 通信就可以使用 A 证书里面的公钥来生成对称密钥了。 特别说明 - 交叉验证 证书链的验证是一个链条过程,只要有一环验证失败,整个链条就会失败。这就需要根 CA 预先存储于本机中。 一般来说,操作系统和浏览器,都会主动的更新本机的根 CA 证书,但如果本机真的没有根 CA 证书,就会影响到证书链的验证。 比如 Google 在 2017 年也成为根 CA 发行机构,那就有可能出现一些机器本机根本没有 Google 的根证书,那么它发行的中间 CA 证书就无法验证了。 Google 根证书:GTS Root R4, Google 中间证书:WE1, a.com 证书: A 有一种交叉验证的方式,即 这里的 WE1 证书,不仅仅可以通过 GTS Root R4 根证书加签,还可能通过其他根证书加签(确保本机已经存在的根证书)。 这样,虽然本机没有 GTS Root R4 根证书,但是通过其他根证书,也可以验证 WE1 证书的有效性,进一步验证 A 证书的有效性。 自签名证书 对于非 CA 机构签发的用于自行测试的【自签名证书】,有两种方案: 先生成自签名的【根证书】,而后通过【自签名根证书】来生成所有的【终端证书】 直接生成【终端证书】 采用方案一的话,直接把【自签名根证书】导入用户设备并信任,后续所有的【终端证书】都能够使用了,通常用在企业内部或者测试场景。 数字签名应用场景:SSH SSH 全称 Secure Shell,即 “安全壳协议 “。SSH 突破安全枷锁的方式和 SSL 是一样的,相比来说少了证书链校验这个环节,即 Client 和 Service 需要自行保障公钥的可靠。 具体来说很直接,就是如果 Client 需要登陆 Service,第一次登陆的时候 SSH 工具会提示 Service 的公钥,人们需要看下这个公钥和 Service 那边公开的公钥是否一致。一致后 Client 侧会做公钥缓存,后面就不会再提示了。 其他的都和 HTTPS 是一样的了,通过公私钥确认对称密钥,通过对称密钥进行数据安全传输。 数字签名应用场景:iPhone App iPhone 的 ipa 包,有非常多的安装限制,都是通过数字签名来控制的。Apple 使用的双重认证方案,即开发者和 Apple 公司同时提供公私钥签名,来使得 ipa 包不会被滥用和滥安装。 总结 数字签名是通过私钥加签,所有公钥都能验签,所以没有机密性可言。数字签名实现了完整性、身份认证、不可否认,而机密性则是通过对称加密 / 非对称加密来完成密文保障。 数字签名,是数据安全的基石。

2023/1/1
articleCard.readMore

我的 2022

时间流的实在是太快了,又是白劳的一年。也不忍回首,也不堪展望。站在年尾的节点,四处都是狼藉。 工作上没有大的建树,生活上没有啥起色。好像什么都不做,也应该是现在这样子。但今年实在太累了,在工作和生活上都投入了巨大的心血,也正因如此,越发感觉到徒劳。 新的财年我调整了工作内容,从业务开发转到了数据化领域。这是个深水区,对数据的把控非常细致,是非常贴近业务的数据操盘手。在这个方向做了半年多了,但是并没有感觉到长远的出路。 一来已有的建设已经稳定,我和老板都想不到有啥重大的利好可投入。二来公司的技术耦合性太大,并没有什么普适性的亮眼的数据方案。 今年上线了不少文章,也算是达成了博客的 kpi。输出经验和总结,让我感觉非常好。就是有些技术博客,打磨耗时非常长,会耽误不少重要的事情。 工作成长上基本就这两大块了。本职工作上花费了巨量的时间,经常加班,但收效不高。学习上也有大量的投入,有不少收获,稍感慰藉。 生活上是水到渠成的乔迁。年中的时候,把最后的软硬装都完成了。孩子开学前搬了过来。住起来很舒服,除了离市区和工作地点很远。 房子整体软硬装配套,大多还是我的主观想法的落地。没有床没有电视,买了个大桌子和大墙柜。整个客厅比较空余,视觉效果较好。白天看着外面的此起彼伏的青山,心情会很放松。 其他生活细节就不怎么明显了,比如头发很长了,也换了新头像。我基本时间都耗费在了工作和学习上,并没有体会生活,生活质量有些低。 孩子又看着长大了一岁,现在性格已经成型,明显是个小大人了。 我也适当培养孩子的责任感,每日都有工作内容如照顾宠物、洗刷马桶、搽桌子,每月换一波内容组合。 今年,她离开了好玩伴,进入了新校园。没有感觉到有啥不适,毕竟还小,不懂得什么离开和感情。她的舞蹈也暂停了,这里并没有一家好的舞蹈学校或者老师。 和孩子放了好几次的风筝,这是和孩子的承诺。但一直没有放飞成功。唯一飞起来的一次,孩子不在身边。过了一会线断了风筝飞了,飞得好远,我最后也没有找到。 疫情也是今年一个特色点。疫情时代算是结束了,赶在了新年开始前。我们全家一周前都感染并康复了。 疫情并没有对我们家庭造成实质的伤害,我三年都是绿码,也没有被隔离过。但三年的疫情,还是对工作、经济、生活造成了相当大的损伤,很多人都留下了永远的伤痛。 今年并没有什么可圈可点的,回头看都是些鸡毛蒜皮的个人或者家庭小事。 但其实内心处,更多的是失望,对自己的能力的失望。能力跟不上梦想,一味的追赶。 我也想着是不是应该改变想法和计划,不要好高骛远了,可心里不甘。我有一个两年之约的计划,我还想斗一下。 今年给自己一个关键词:苟活 来年, 生活上打算把健身的权重升高。其他有:习惯、睡眠等,计划还需要从长制定。 学习上把算法能力再精细化提升。 和自己有密切关系的事,只能靠自己。不要期待别人的帮忙,那会不如意的。放任不管的水到渠成,也会差强人意。还是需要注入一些时间去打磨,才能把和自己有关的事给很好的落地,至少让自己满意。

2022/12/24
articleCard.readMore

三年疫情五味杂陈 - 下

在两个月前写《三年疫情五味杂陈》的时候,就是要放开了,所以当时想着写一篇,对疫情三年稍作记录,毕竟是一个时代的拐点。 那片文章过后半个月,就开始有放开的动作了,部分城市已经松动,我还给被隔离的小伙伴留言,说赶上了最后一波封控。 只是没想到,这个国家干啥都是一根筋。我以为这么大的人口,怎么也是省级别的逐步放开,没想到是全国一下子放开。赌气呢吧,一下子就放开了。 从 11.26 开始,全国各地的几百万人上街游行,喊啥口号的都有,让习近平和党下台的口号都喊出来了。大范围游行了 4 天,后面开始小范围游行。 再之后,江泽民去世,平息了游行,也彻底放开了管控。距离现在,差不多 3 周。目前每日 3700 万阳性新增,共计感染 2.4 亿人民。现在看下来,年前是要把第一波给结束掉。 我能说啥,人民都是铁做的,想怎么揉就怎么揉。我开始以为是和大节奏一样,每日加上隐瞒的也就 500 万新增。按照现在的速度,还有一个月吧,就能全民免疫了。两次看到中央对很多地方的感染速度不满意,有批评,真实性百分百。这就是政府推动下的全面放开,不管这一刻的后果,只要感染覆盖率。 最近医院可走了不少人,后面只会越走越多。三年里,欺骗和谎言,就差按着人民的头说 YES 了。三年后放开的这一刻,ICU、特效药、疫苗、医用设备、病房,啥都没准备。现在市场上退烧药根本没有,感染病毒了连个最基础的退烧药都买不到。我什么都不想说了,这操蛋的政府啊,从来不曾把人命给重视过。 我哥说没药是因为很多人抢,抢的多。说人太多了,不好管。我哥是相当的明白人,可还是不了解政府,他还在认为政府的能力有限。 疫情这三年,哪个国家能够做到这个程度的封控?又有哪个团体能够做到连花清瘟的全面入户?根本没有第二个了,就这一个,only only one。就三年疫情这个镜子,就可以清晰的看到政府的能力有多大,管人方面已经无所不能了。那是傻子么还看不到? 这样的政府,会处理不了布洛芬的产能和发放问题?仅仅是不愿做甚至想都懒得去想一下而已。中国可是布洛芬这个基础药的生产大国,但凡提前通知下产能,1 周绝对能够把这次疫情需要的药都补齐,个别人抢的多也没有问题,绰绰有余。 可是这 1 周,政府不愿意等,仅仅是不愿意甚至懒得想一下而已,并没有什么阴谋或者心思。 人命,从来是刍狗。纵观中华上下几千年,历朝历代,皆是如此。 最近腾讯还上线易药小平台,就一个 H5,我体验了下,实用性不高。还有媒体发文,让有药的把药平摊一些给需要的人。还有种种,看着都是暖人的事迹,实际上几把用都没有。 饿了么当时上线 “愿意多等骑手 5 分钟” 功能,被骂死了。说饿了么用用户的感情来发电。 现在这些暖人的言论,比饿了么这个功能还要恶心。这些根本就不应该是人民做的,而应该是政府做的,就这么简简单单的逻辑。人民是交了税,交了保护费的啊。 我自己前两天也感染了,家里人相继感染。我能体会到这个病毒的威力还是不小的。我和家里人说,得多照顾一下奶奶、外公、外婆,他们有年龄近百的,有诸多基础病的,可能真不定能扛过去。 有那么多医疗条件好的,家庭资源好的,不都是没能扛过去么,我们不能假装是幸运狗。前车之鉴太多,我们不需要摸石头过河。 我家里有药,因为我不相信政府,这几年食物一直都有补充,药也有剩余。遇事不慌。 那些相信政府和专家的,我能说什么呢?这三年,在家里病死、无法就医、挨饿的,比比皆是。亲人离开了你们也会流下眼泪。可谁让你们就是不开窍呢。这个社会时时刻刻在教会你们,可你们的脑子动都不愿意动一下。 中国,是一个没有记忆的国家,因为没有历史,所以遍地孝子。 再过一段时间,三年封控的痛苦会荡然无存,放开后的失控和死亡,也会烟消云散。 就像三年自然灾害、十年文化大革命、六四运动一样,没有总结,掩饰过往。下一波没人知道会是啥,也没人知道有多大。 而人民呢,不会关心谁为他们斗争过,也懒得关心什么法律、政治、医药、科技。几千年来,人民都是没有记忆和传承的,最凶的时候是抢人血馒头。 大家都在坐井观天。哪一天,看着另一个灾难到来然后过去,周而复始。 堕落已经到了极点,不悲不伤,不抱希望,都是生活。

2022/12/24
articleCard.readMore

老梗 - 浏览器按回车后发生了什么

想起了一个老问题:浏览器按下回车的时候,后续流程是怎么变化的。 这个问题非常精妙,基本上把网络问题一次打包了。 对相关知识面了解越多的人,能说的内容也越多。越资深的人,能说的时间也越长。 我对网络也一直感兴趣,索性这次就做了大图,越做就盘子越大耗时越多,每个点都可以延伸一个举足轻重的行业。 浏览器按下回车键后,发生了太多太多事情,我文字理下图中写不下的重点,详细看下面大图。大图下载地址 DNS 重点是时机。DNS 查询是为了域名和 IP 映射,所以它的时机非常非常靠前,是所有网络活动的第一步。 发挥大作用的是负载均衡。 误解最大的是根 DNS。有镜像 DNS 在,不要拿封锁大陆域名后会怎么怎么样来做文章,技术同学说这话是要丢人的。如果这么做,我能想到的最大影响就是:大陆主动拒绝外界网络,外界都不愿意打开大陆访问,其实都是自由的选择。 httpDNS 可以解决 DNS 污染和劫持问题,还有运营商偷懒导致的跨网非最近节点不准确问题,加快域名和 ip 的映射。一般只用在移动端,技术方案是 hook gethostbyname api 这个环节。别想着请求前把域名换成 ip,坑很多,尤其是 TLS 证书验证的时候。 Socket 套接字 网络通信的基石,只要是网络通信,这就是绕不开的大山,这就是中流砥柱。 socket 的核心在于 socket 描述符和发送接收缓冲区。发送接收缓冲区不是 socket 特有的,是计算机基础的一部分,我们使用终端的时候,输入指令和参数的时候都在使用这套缓冲区。 大名鼎鼎的三次握手就在 socket 建联的时候发生的。成也萧何败也萧何,因为 socket 是内嵌计算机的底层服务,也包括 TCP。所以即使三次握手已经拖慢了互联网这么多年,但依旧无法做升级。Quic 的出现就是忍不了 socket tcp 的队头阻塞而破釜沉舟的东西。 socket tcp 的详细过程,可查阅之前文章:TCP 数据传输过程分析 谈 socket 离不开端口,谈端口离不开进程。可查阅之前文章:Shell 和进程 - 两种进程创建方式 socket 本身可查阅之前文章:IM 和 Socket 的关系及 Heart 的必要性 用户网络 基站的圆形辐射覆盖范围有限,做火车的时候就是在频繁的更换基站。尤其偏远的地方,在火车周边每隔一段距离就有一个基站。基站应该是中国铺的最广的民用基建了,这也是运营商升级缓慢的原因,成本很大。 国内 sim 卡,在国外有的能上 twitter 有的不能上,是虚拟隧道的那一端到底在国内还是国外的原因。 疫情期间能知道哪些人经过了什么地方,就是因为基站的背后就是路由器和互联网,手机到哪就知道人到哪了。我疫情初期听过这么一个案例:一个人黄码了,然后他是内部工作人员,就去删了自己手机号的行程数据,黄码就立刻变绿码了。3 年过去了,现在健康码的操作权限还是很大,不仅可以手动删数据,还可以手动加数据,可以指定哪些人展示什么码。 七层五层网络协议 网络通信的基石,和 Socket 一样是中流砥柱。不过 socket 那一层需要复杂的编码,这一层只能躺着使用,可以窥看它的实现,有不爽的地方也无能为力。 会话层的 TLS 是 HTTP 和 HTTPS 的差异,这一块集中了密码学的知识。TLS 的耗时是非常可观的,要想躺着减小耗时,就尽快升级到 TLS1.3。其次,尽快用 ECDHE 代替 RSA,ECDHE 减小了签名大小、解密时间,还支持抢跑。 传输层的 TCP 详细过程,可查阅之前文章:TCP 数据传输过程分析 这里有两个重要的 case,就是 MTU 和 MSS。传输层的 MSS 会拆包,叫分段,IP 层的 MTU 也会拆包,叫分片。当包在网线传输的时候,其他的路由器属于三层设备,可以将包解到第三层即 IP 层,这个时候也就拿到了 MTU 范围内的全部包数据,如果包数据不符合当前路由器的 MTU 大小设置,可以将包抛弃,或者拆成更小的包后继续传给服务器,服务器会根据编号进行重组 (分片重组)。包里面可以设置一个参数,当路由器 MTU 过小需要拆包的时候,不允许拆包,强制直接丢弃。因为 IP 层面的拆包,是风险很大的,这个时候包里面没有了传输层的头部,有可能会服务器侧各种拦截。 MAC 层会通过 ARP 拿到下一跳的路由器或者主机的 MAC 地址。MAC 地址并不是电脑本身的一部分,而是网卡的一部分。在电脑主机启动后,网卡驱动会对网卡进行初始化,这个时候网卡有一片 ROM 区域会被驱动配置。后续拿的 mac 地址,其实就是从网卡的这个 rom 区域拿的。 UDP 和 TCP 的根本差异,不在于稳定性和使用方式,而在于包大小。如果每个包都很小,那么 UDP 要比 TCP 传输的更高效。只是有些包比较大,UDP 传输的话,一个包丢了就得全部重传。TCP 可以通过稳定的复杂机制来保障快速重传。比如 DNS 查询,就是通过 UDP 来发包的。DNS 查询很重要,为什么没有使用 TCP 的稳定通道?就是所有数据就一个包大小,丢了就丢了,没有回执就是丢了嘛,那就再重传一下。 NAT 路由 核心啊,能上网就靠它了。大图里面详细说了 NAT 的流程,包括 MAC 层 MAC 地址与 网络层 IP 地址的变化,以及 NAT 映射表和端到端隧道穿越的实现。 浏览器渲染 这里重点说了 V8 对 javascript 支持。详细可查阅之前文章:Shell 和进程 - 解释编译混合型语言 图片渲染部分,以及如何实现渐进式图片,可查阅之前文章:文件存储差异 - 编码 CDN 加速 和 DNS 一样,CDN 负载均衡服务器非常有用处。 要提高加速的命中率。即预推要及时,减少回流的概率。 缓存策略要控制好。可以通过 HTTP head 在客户端和服务端同时做缓存。 大图来了

2022/10/23
articleCard.readMore

三年疫情五味杂陈

update:2022.10.29 昨天听说开始有解封迹象了,今天我收到了运营商发出的 “奥密克戎感染后的身体事项” 的说明短信。能收到这样的短信,就是一叶落知天下秋,看样子是要逐步解封了,应该会在一两年内全部解封。不好的消息是:没有听说 ICU、医疗、特效药、病房有做准备,即使逐渐放开,这些必要的医疗准备工作也要提前半年到一年就要开展起来的。目前看是打算逐渐解封 + 硬扛,要带走不少老年人了。老年人得注意。 COVID-19,从 2019 年可能在 10 - 11 月初步感染人体,到 12 月底被媒体曝光,再到现在,已经 3 年了。 这三年发生了太多太多事情,刻骨铭心的不计其数。我开始以为以前也是有很多很多事情,只是我没有关注。后来想想,或许就是这几年,发生的重大事情比较多。因为疫情和国际局势,中华人民坎坷的这几年。 金字塔的每个层级每个人都受到了很大影响,所有影响最终都会归到生活和金钱,质量总归是下降了不少。 也有很多人说没有受到影响,甚至蒸蒸日上,有些是身不由己,有些是笃信不疑。差异也都能理解,毕竟各种原因,真的是各种原因,不想再去细究,意义不大。 简单回忆下三年疫情,因为不知道什么时候才会结束,就当作是对个人疫情的期中总结。 我应该是最早一批知道武汉有疫情的人,当时只知道有人被感染,还没联想到非典这些传染病。当时有些朦朦胧胧,也有些对未知的恐惧。当时也快过年了,还有不少同学的婚场。 再过了几天,听到疫情严重了不少。已经记不清是在李文亮之前还是之后了。 我和亲人说过年可能回不去了,这次病毒没有媒体报道的那么简单,感染会很严重。过年人流大,火车站这些场所风险比较大。亲人说就是非典一样吧,开车应该好些。最后我们开车回去,老家过了三天赶着高速封路前回到了居住地。我姥姥说这就是古代的 “瘟疫 / 人瘟” 吧,好久没有听到这个词了,但很赞同。 我大学同学的婚礼是过年前段时间。我之前已经答应了我和老婆都会到场了,但是婚礼前我还是和同学说了抱歉,我说肺炎(那是还不叫 covid 或者疫情,只叫做武汉肺炎)不简单,火车站有风险,到场计划得取消了。 当时肺炎在武汉已经很严重了,很多人也都知道这个事了。但因为信息的封锁,大家对最基础的是否人传染人都没怎么了解和关心,更别说肺炎本身的症状和死亡情况。 结果就是那场过年的人口大迁移,病毒传播的突破口从少数变成了无穷大,那次过年很多人没能返程,因为过年那几天疫情突然恶化,许多高速和村镇出入口都被封了。我不少同事都没能返程,我是在高速被封的前一天和亲人返程的。 我经常会想到疫情开始的那个阶段,部门和媒体是重大责任缺失。肺炎的症状、传染性、风险已经有数据了,但遮遮掩掩不作为、不停的辟谣。 所以年后回到公司,同事说辟谣很有意义,我说:“谣言不应该被辟谣和封锁,而应该交由市场来评论。谣言应该在各种不同的评论之中出结果,而不是被封锁。如果谣言在市场评论下变成了事实,那应该考虑市场或者人是否出了问题以至于使谣言变成了现实”。其实这里的关键点就是:谣言到底是不是事实。如果谣言不是事实,那么市场评论本应该给予驳回。那如果谣言本身就是事实呢?封锁谣言就是封锁真相啊。其实这就是简单的一个前后逻辑,但很多人不愿意去思考稍微有一点逻辑的东西。疫情的大范围出圈,造成后面的不可堵截,就是辟谣造成的血淋淋的现实。 在那之后,武汉就正式封城了。那时疫情刚出来,毒性最凶的时候,虽然全国支援武汉,但免不了群体免疫的事实。事后人口数据来看,除去正常死亡,病毒带走了 3W 人,太悲壮了。现在看看就是一串数字,可一个城市短期上万人的死亡,医疗系统早已崩溃,那时的病人是怎样的绝望。想都不敢想,实在太悲壮了。 再往后,就三年过去了。这三年,世界被 COVID 轰动。 国外基本都走了全体免疫,其中以印度最为悲惨,带走了 400w。美国也带走了 100W。这些数据也都会有很大的偏差,因为各国对死亡的口径不一样。所以真实数据,只有上帝知道吧。 国内一直在走封控,从村、镇、县、市、省各个层级做封控,效果也显著,没有被带走多少人,但具体被带走多少人,这个拿不到数据。 其次,对疫情的态度,这三年也发生了巨大的变化。 开始的时候,全世界都在恐惧之中,国与国之间都限制飞机航行。 病毒发生了多次变异,毒性逐渐降低。 外面虽然政策上前期依旧是控制人员流动,但实际上都是走了全民免疫的道路,目前也都宣布了疫情正式结束。疫情回到流感水平。 里面目前还在封控。因为外面是全民免疫,病毒退化到流感程度,简而言之就是病毒更加四窜。里面因为严防疫情的原因,和国际间的经济合作也受到很大阻碍。当前,这个阻碍其实也不是首要原因。 在技术上,因为疫情也有了进步。 首先疫情使得高精尖的 mRNA 技术得到了突破。之前这项技术一直在试验阶段,这次疫情使得这个技术被落地。mRNA 产生的 COVID 疫苗,已经接种在了国外人的身上,防治率非常高。 虽然 mRNA 很好用并且在世界范围内被注射,但 mRNA 疫苗因为种种原因并没有在国内被普及,可能只有权贵们打了。关于 mRNA 技术和疫苗和国内疫情的关系,本可以有很多话能说,但不说了,没啥意义。(mRNA 详细详见文章底部小知识) 国内最大的科技突破是连花清瘟。也不说了吧,没啥意义。 国内疫苗没有技术突破,灭火疫苗是较安全的稳定路子,对于 COVID 这类 RNA 病毒,肯定有效率,但效率并不高。 但是这次的疫情,国内国外的疫苗,都有很大的安全问题,主要是实验太少就上马了。只可意会,不可言谈。 在生活上,就是核酸和隔离。我经历过一日一检、两日一检、三日一检、七日一检。我同学同事经历过一日两检、居家隔离、小区隔离、酒店隔离、方舱隔离、城市转运,这些我都没有经历过,我经历过最近的一次是 1 公里外的小区有感染,整个小区被隔离。当时我非常担心社区整体隔离,最后是没有。当然,虽然我自己没有经历隔离,但隔离在我周围一直在发生。 国内一直在做核算和隔离,动态清零已经成为基本国策,即使从外面看病毒已经退化到流感程度。 国内对于隔离,也没有做有效的预案和准备,次次隔离次次有灾难,各省各自求自保,经验总结基本为 0。其中以上海那次最为有影响,有句话这样说:当我老了,孙子浪费了粮食,我批评了他。我当年也没能想到,在世界级大都市的上海,我当年差点被饿死。 上海那次是最有影响的一次,但不是最严重的,毕竟上海是国际化大都市,城市的运转还是有经济底盘来保障的。 居家隔离最常发生,但依旧没能形成统一的经验输出,管理混乱、电话打不通、极度消极都时常发生,被隔离者敢怒不敢言,只能弱弱批评。 也因为这个,我养成了屯粮的习惯。家里常备大米、压缩饼干、蔬菜包,以备紧急需要。我告诉妻子,这玩意真到了那一天,或许真能救一命,就像游泳技能一样。即时不会被饿死,要是吃的少了生了病,就医困难病死了呢?活着总比死强,那么多前车之鉴。 这场病毒,让我感触最深的不是病毒本身,而是人心。不少人有了防疫的权利之后,心就变了。不知道是一直如此,还是突然变的。甚至,很多人已经没有人性了,比如已经地震了,却赶着下楼的人回到楼里隔离。以至于人们都在反思,人们到底害怕的是疫情本身,还是疫情背后。我呢,把这些人归咎于无知吧,没有脑子也不知变通的无知,他们是机器不是人。 我不反对封控,因为初期病毒毒性强的时候,封控拯救了太多人的命,这点大陆人都应该感恩。要不是只有国内才能开展的封控政策,很多人可能要面临亲人的离开。 但把封控作为长久国策,和政治挂钩而不是从民生来考虑,我肯定是反对的。毕竟封控造成的伤害,也太大了,不管是经济还是民生。 为什么不把有效的疫苗、ICU 病房、医疗等等刚需事项列入 KPI,及时为面对疫情做转型呢?一条路走到黑,走不下去了怎么办? 这条路真的会走不下去的。就目前世界的情形,外面都是全体免疫了,外面都是病毒。外面都宣布疫情结束都开放了港口。现在经济贸易旅游全球化,怎么隔离外面的病毒? 那些指望封控能够解决病毒的人啊,动下脑子走下逻辑吧,靠这条路有生之年是等不到清零的。(科学角度根本原因详见文章底部小知识) 全民免疫已经是避不开的选择,当碾压的车轮到来的时候,会不会再次手忙脚乱的无措? 再者,为什么目前这么严格的封控,对于单个城市来说 1 个月甚至 2 个月都无法清零?城也封了人也都居家隔离了,为什么一两个月还是大批量的感染?这个问题真值得细品一下,不品了不品了,没有意义。 这场基本国策迟早会被取消的,现实摆在面前没得选择(现实原因请查看上面一段文字或者底部小知识)。只是国内如此大的人口基数,即便毒性已经和流感一样,能带走的人也是众多的。 封控总有一天会结束,只是那一天到来之前,希望 ICU 病房、医疗等必要措施都准备起来,不要再迷糊了。虽然这依旧解决不了不少老年人的感染死亡,但是概率会降低。不然,有太多的人得做好失去老年亲人朋友的准备。 有违国策的疫情言论,一家之言,多有世俗违背,见谅了。 小知识: 即使目前科技这么发达,但感冒、流感还是没有药物治疗的。吃的药只是缓解病症的表现,让人熬过了那段时间后自愈。 因为感冒流感去世的,大多不是病毒本身,而是其他疾病。药物缓解病症可以降低其他疾病的发生。所以老年人在感冒流感的死亡率上比儿童、年轻人要多,因为并发症多。 病毒会变异,且变异频率非常快。这就是对于 COVID,不管国外还是国内,都要接种 2/3 针的原因。而 mRNA 的技术突破,就是应对这种变异问题的解决方案。相比灭活疫苗,mRNA 只需要少量的改动就可以适配新的变异,时间周期非常短。 耳熟的天花是人类第一个消灭的病毒,少有的被消灭的病毒。因为病毒的遗传物质分为 RNA 和 DNA。天花属于 DNA,遗传物质稳定,所以疫苗出来就可以持续免疫了。但是 COVID 是 RNA 的,会经常变异,和流感一样。。 病毒只能在生物活细胞内繁殖和传播,产生了,就不会消失了,除非没有生物可以传播了。病毒和生物是共存的关系。这也是为什么 COVID 出来没多久,国外科学家就说可能要和它做长久共存的准备了,这就是科学和口号的差异。 实际上人类发展才多少年,病毒才是这个地球从古至今的主宰。人类在病毒面前太弱小,更别提抵抗了。

2022/10/16
articleCard.readMore

生活推荐清单

之前写有提高效率的手艺和这些年我的软件清单两个推荐,一个用来提高文字编辑的效率,一个推荐了手机和电脑可以安装的一些高效率的软件。 这次推荐一些日常生活的避坑指北。生活还有几十年,掉坑在所难免,所以这篇推荐清单打算做长期更新。 指北概要 日常物品避免线下消费,可以线下试用。线下的售后保障和价格上都比不上国内线上。 不要担心购物资损,电商平台都有 7 天无理由退货。有些人已经有了线下消费阴影,不敢退换货了。其实就是手机点下退货,接一下快递员上门电话的事情。 同一个系列,在价格承受范围内可以选贵一点的。也许多花了一点没意义的钱,也许底线就在那个便宜和贵中间。别想着讨巧,便宜是有原因的,商人从来不吃亏。 买东西前多了解下要买的东西,包括组成、作用或者配置等,花不了多少时间。别瞎几把乱买,比如震动去脂、啥离子之类的,不主动了解就得做好被动收割的准备。平台的评价系统真假参半,但也是避雷的最好途径。 对于想买的贵重物品,延迟一周后再决定下不下手。不缺那一周的时间,但钱是真金白银。 对国产保持警惕,尤其是贴近皮肤和肠道的物品。 拼多多的百亿补贴挺香,比其他知名平台要便宜不少。百亿补贴里面的海淘也非常好用,东西不多,都是实用系列。 洗漱 男士洗面奶 - UNO(拼多多海淘 ¥15 一支,130g) 牙膏 - 狮王 white&white,不要买国产那款,买产地日本的(拼多多海淘 ¥8-11 一个,天猫海淘 ¥15,150g) 儿童洗手液 - 日本狮王泡沫式(拼多多海淘 ¥20-25,500ml) 护发素 - Fino(又称红宝石,拼多多海淘 ¥30-40 一罐,230g) 牙线 - 刚需,一定要用。不用一下都不会知道牙缝有多脏。 抽纸 - 清风,金色外包装款 剃须刀 - 吉列锋隐手动版。如果胡子比较茂盛,那可以把剃须当作生活的一部分。建议不要用电动的,虽然方便,但没有剃须的快乐。(要配合剃须泡,洗澡的时候胡子会变软,是动刀的好时机。剃须膏体感非常差。) 水杯水壶 - 买好一些的自己喜欢一些的。自己喜欢的,就是可以多喝一些水。多喝水非常重要。 浴巾 - 毛巾给身子擦水,又慢又累。洗澡后用浴巾两下子就擦干净了,省事。省的时间干其他的事不也很香么。 衣物 袜子 - 建议船袜,对鞋子搭配不会有影响。 男士内裤 - 买大一号。建议四角,三角太紧束对下体不好,容易真菌感染 (湿疹)。 棉质 - 棉质会好一些。涤纶都是用塑料材质做的,不是内行不懂影响,但是主观评断还是棉质好一些。 被子 - 就多花一些钱吧。价格低的,都不知道里面塞的是什么,虽然价格高的也不确定。但是多一些钱,概率上来说的确可以多一些保障,没拿到这个保障那也得认命。 床垫 - 买贵一些的,¥4000+。柔软度适中的。太软的一定不要买。有人就喜欢硬板床,但试一下软一些的,也许就回不去了。有些人就是不愿意尝试,守旧不可取。 食物 啤酒 - 百威(啤酒口感得看麦汁浓度,一般大于 9.5 的,入喉咙的时候会有一种黏稠顿挫感,体感会好一些。) 白酒 - 市场上的别喝了,问题很大。有些农家酿的酒可以尝试,好喝且不头疼,但可遇不可求。白酒标准:喝多头不能疼,头疼的话这酒别喝。市场上的白酒喝多了头肯定疼。(贵的我没喝过) 白葡萄酒 - 吃海鲜的时候绝配,比如刺身、生蚝、虾等 牛肉干 - 别买颗粒的那种,味同嚼蜡。得买内蒙那边风干的长条状的,黑不溜秋的那种,嚼起来不要太爽。 鸡精 - 别吃 国产零食 - 不要吃,都脏的厉害。其实大家都知道脏,都是边吃边默念着:真香真脏。 酱醋等发酵食材配料 - 亚马逊海淘。谁能想到这些国内的也不健康呢?发酵需要时间,只要耗时的,国内厂家都会想办法缩短时间无一例外,白酒也是。对于企业来说也无可厚非,这是企业生存之道。改变不了就想办法避开。 油 - 还是选非转基因的吧,我只是建议,不是专家。或许这个连专家也没有答案吧。就像电子烟的危害一样,危害肯定很大,但是和实体烟相比哪个危害更大,除了那些瞎扯淡的,至今没有有效的论文报告评估。 电器 冰箱 - 西门子 洗衣机 - 西门子 热水器 - 林内 烤箱 - 刚需,自己烤要比外面健康卫生太多了 电脑 - 普通用户 & 白领 mac air,资深用户 & 视频剪辑 & 开发人员 macbook pro 键盘 - 一定用机械键盘,手感棒很多。如果是 mac 用户,京东上面买京造 K2 ¥300 左右。是 Keychron 的贴牌,海淘 ¥700 起。 电脑支架 - 只要是铝合金的,不管升降还是固定版,都不好用,要么容器坏,要么高度达不到。我现在买了木块,手动 DIY 的。 手机 - iPhone。虽然都不可靠,但是以我做 iOS 开发的角度分析,不要选安卓 (只能选 iPhone 了)。像这种长久贴身的东西,建议买好一点贵一点的,使用起来开心一些。有时候那份开心,就是无价之宝。 手机外壳 - 不建议使用,会丢失使用手机过程中的手感。每一款手机的外形设计,都是考虑的裸机的舒适度。 降噪耳机 - 得有一个,用了就放不下来。别买便宜的,至少也要 ¥400 起。小心外耳道炎及时抹药,但即使疼痛还是会带,可以想像降噪耳机的用途多大。(耳入式 & 头戴式凭场景和喜好吧)。推荐 Airpods 和索尼。 电动晾衣架 - 非必需不建议购买。有些装修的房子其实没得选,只能用电动晾衣架了。 打火机 - 有闲钱的,可以买一个 Zippo。个人:很喜欢,但没买。 清洁 除锈、润滑 - wd40(wd40 即是公司名也是产品名,被称为万能神器。拼多多 ¥15-20,55ml) 擦镜纸 - 刚需,未找到价格合适的产品。听说蔡司不错,但是好贵 尼龙扎带 - 生活好帮手 魔术贴绑线带 - 电线杂乱的时候用处很大 垃圾袋 - 带抽绳的那种 扳手螺丝刀等 - 家庭要备一个,很多时候都要用。 健康 健身环大冒险 - 适合孩子和成年女性,挺有意思的。 避孕套 - 必需品,一定不要不带套,一次也不要,对双方都好 夜宵 - 别经常吃,很容易长胖。体重上去容易下去难如登天。 !!!不管什么毛病,都不要去药店和小医院,而应该去大医院,少花钱少受罪。 !!!中大病一定选择最少两家医院做检查对比,切记,省挂号费相当不值得。 !!!信现代医学别信偏方,别拿中药拿西药。医生下单后,看一下药品有哪些,非常不满意就不要付款拿药了。久病可成医,别总信医生。 出行 雨伞 - 家庭可以配置这款:左都超大直柄 3 人款,直径 130cm,高度 100cm 自行车 - 迪卡侬(儿童 ¥500-800,成人 ¥2000-3000)。其他外出穿戴类的就去迪卡侬看看,比如鞋子、防风衣服等,价格很地道,质量非常好。 头盔 - 野马 电动车锂电池 - 阵风。不要用铅酸电池了,锂电池贵点,但性价比舒适度更高。 晕车药 - 有一款日本的,还在试,有效果我后面补充。晕车厉害的成人,可以购买口服晕车药。小孩子不要服用,即使商家说可以服用。 其他 熊孩子 - 别依靠长辈和老师,要自己好好教育 | 舍得花钱,满足孩子好奇心 | 6 岁前培养孩子勇敢、好奇、不惧生、反抗等性格,以后就改不掉了 | 小学培养逻辑能力,这玩意贼重要 | 培养拳头自卫,可以学习跆拳道和拳击不被欺负,小时候的欺负有时候是一辈子的阴影 | 写好英语。 宠物 - 能带来很多开心,也有很多闹心。地位也就比熊孩子低一点。一定多角度评估后选择是否养宠物,以及养什么宠物。 讨论 - 讨论适可而止,不要追求完美和绝杀。大多数争议讨论的最后,都是谁都说服不了谁,假装愉快的不欢而散。 界限 - 和亲人、朋友、同事等熟悉人之间,得知道有界限存在。尊重,是这道界限最清晰的标记。和医生、商家、路人等陌生人之间,警惕钱的威力,钱是人性的试剂,别被坑。但也别疑神疑鬼,要落落大方。 脑子 - 脑子这玩意得带着,时时刻刻需要它。多问问自己,脑子现在还在我头里吗?

2022/10/7
articleCard.readMore

Shell 和进程

曾经有位老师问过:在 Linux 的 Shell 中运行程序时,操作系统是怎样对程序进行处理的吗? 我当时回复是这样的:操作系统对 Shell 的执行,是靠 Shell 解释器完成的。在操作系统运行后,Shell 解释器本身就加载并运行了。其中如 pwd,cd 这些是内部命令,本质是函数调用,可以直接使用。ls 这些是外部命令,需要 fork 一个新进程执行当前命令。一个 Shell 脚本,有很多个这些内外部命令组成,通过 Shell 解释器逐行解释完毕后执行。Shell 解释器也是一个应用程序,本质是一个 C 程序,不过在该程序中,手动模拟了函数调用栈,和 JVM 有相似之处。所以 Shell 解释器,也有静态库 / 动态库 / 静态链接 / 动态链接这些,为 Shell 命令的执行保障护航。 现在看起来,当时的回复虽然没有大的问题,但还是不够。只是浅表的认知了 Shell 解释器的用途,但是对 Shell 并没有深刻的理解。 最近看到子 Shell (SubShell),发现 Shell 和进程之间的关系非常密切。可以从进程的角度来理解操作系统是怎么运行 Shell 的。 这篇文章的目的,是让你只关心 Shell 脚本的语法,而其他细节,都变成顺理成章。其实各个高级语言也都是这样,语言本身其实并不难,像工具一样使用而已。 0x00 温故知新 对于 Shell 的运行,有一些前置知识需要粗略的描述一下。这些前置知识非常重要但也可以不写 (属于基础范畴),因为它对理解 Shell 非常重要,所以这里还是加以补充说明。 没办法,计算机的知识都是一层套一层垒起来的,一个已经定型的知识点会不断的被重复使用,如果不能直接使用了,就再加一个中间层去使用它。 如果已经对这些内容比较熟悉,还是建议快速的过一下,也许会有新的发现。 解释型语言 VS 编译型语言 目前的高级语言,都是需要被转化成指令码才能被计算机运行。而因为转化的时机不同,高级语言被分为两个阵营:解释型和编译型。 其中对于解释型语言,为了更好的效率,也更多的加入了编译型的影子,就是 JIT 即时编译。所以也可以认为还有一个阵营,即解释编译混合型。 编译型 像 C、C++、Objective-C、Swift、Go 这些,都是编译型的。典型的特征就是:项目代码需要被处理成可执行文件后,才能运行。 如果改动了哪怕一个字母,也需要把编译、汇编、链接这套流程走一遍,才能重新生成可执行文件并运行。有时候项目比较大,重新编译运行一下需要耗时 5+ 分钟,所以开发效率很低。 可以用 C 写一套编程代码,但针对 Mac、Linux、iOS、Android 这些平台,是需要针对不同平台,用不同的编译方式打不同的可执行文件后,才能运行。这就是编译型语言的另一个特点,就是不能跨平台。 编译型语言的最大优点是超高效率,编译型语言是经过前后端编译后形成的二进制可执行文件,CPU 的运行指令直接存储在二进制可执行文件中。编译器还会对指令做进一步优化,优化幅度非常大,以使得可执行文件运行的更快。 这里说的高效率,仅仅是说 CPU 可以直接从内存中对可执行文件进行取指、译码、运行,这一套流程非常快。而不是说语言本身高效率,因为不同语言侧重对方向不一样,有些 CPU 密集型,有些 IO 密集型,从这个维度来说高效则更侧重业务本身。 解释型 像 Python、Java、Javascript、Shell 都属于解释型。典型的特征就是:项目代码无序被处理成可执行文件,就可以执行。解释型语言和编译型的特征都是相反的。 编译型需要经过前后端编译后,生成可执行文件。而解释型只需要经过前端编译,即词法分析、语法分析、语义分析三个步骤。如果精简一下,语义分析还可以不要,靠开发人员维护语义的正确性。 通过词法分析将代码转化成 N 个 Token,通过语法分析将 Token 转化成抽象语法树 (AST),而后通过解释器 深度遍历 AST 即可将项目代码执行完成。 下面先针对最简化版本的解释型语言运行过程进行说明。 解释型语言的开发效率非常高,因为可以做到逐行翻译代码,所以修改了 N 行代码,就把修改的部分重新词法、语法分析一下,整合到 AST 中去。(编译型语言也非常渴望这个能力,有一种 hot reload 技术实现方案)。 解释型语言有一个核心,就是解释器。每个语言都会有不同公司做不同的解释器,刚才提到的解释型语言,就有 Phthon 解释器 (PVM)、JVM 虚拟机、V8 引擎、bash 解释器、zsh 解释器等等。 解释器承载了一个很大的技术地盘,就是跨平台。同样一套 js 编程代码,只要有解释器,就能够运行且等同运行环境一致。因为解释器抹平了端的差异,使得一致的代码按照一致的运行逻辑被解释器执行。 而解释器本身一般都是用 C 写的二进制可执行文件,所以解释型语言的运行,离不开编译型语言做基础。这一点很重要,解释型的代码之所以能够被逐行执行,是编译型语言先打包成可执行文件,当文件执行后,不断的逐行读取解释型代码并解释执行。而解释器的二进制可执行文件本身,也会有代码区和函数,这些函数也可以执行并且和解释型代码逐行翻译执行是不同的,这些函数就是 Shell 里面的内部命令,比如 pwd 命令,其实就是执行解释器本身的一个 pwd 函数。Shell 的外部命令就比如 ls,在解释器内部并没有一个 ls 函数与之对应,ls 实际上是另一个可执行文件,后面说到 Shell 内部 & 外部命令的时候再详细说明。 开发效率高和跨平台都是解释型语言的优点,而缺点也就是编译型语言的优点,就是低效率。 最简单的例子,对于 1 + 2*3 这个加乘运算,在编译型可执行文件里面就是几个寄存器调用 (几个时钟周期) 的事情,但是在解释型逐行翻译的过程中,需要先被翻译成 5 个 Token 并生成 AST 树,这里的每一个 Token 还有读取操作和加法 & 乘法两个运算,至少需要 7 个函数的调用。而每个函数的内部还有一些额外函数调用,整体下来没有 20 个函数调用是下不来的。这和编译型语言来说速度上就是天壤之别。 解释编译混合型 上面最简化版本的解释型语言描述,其实就是解释器初期版本,是纯粹的解释型语言执行流程。这个时候应用在执行效率没有那么高但是重视开发效率和跨平台要求的场景。后来,解释器做了极大的升级,主攻执行效率问题,引出了虚拟机的概念。 虚拟机对于执行效率问题的解决方案核心在两个方面,一个是通过栈机 / 寄存器机来模拟编译型语言的函数调用栈,一个是增加 JIT 实时编译的能力使得代码在运行时被编译成二进制执行,此时执行效率等同于编译型语言。 从这个角度来看,虚拟机可以称作解释器的升级版本,或者把虚拟机叫做解释器也没有问题。但是把这个中间层叫做虚拟机这么一个大的名字,显然能力不限于此。 实际上虚拟机在设计之初,就是模拟了一个物理机器,有自己的指令集和内存管理,还有并发 / IO / 线程调度能力,就是一个虚拟化的硬件 + 操作系统。从这个角度来看,虚拟机已经远远超出解释器范围了,毕竟最纯粹的解释器,就是对 AST 进行一次深度遍历。 虚拟机做的第一步,就是不再进行 AST 深度遍历。 代码执行的宏观表现,就是函数的调用。大多语言都是从 main 函数开始,不停的在子函数里面执行下去。所以无规则深度遍历可以切换成子函数的调用维度,通过对每个子函数的优化来提升整个代码的执行效率。 在编译型语言里面,函数栈帧是通过 CPU 执行可执行文件的指令在内存栈区维护的函数调用栈。虚拟机就需要在代码层面模拟这套栈帧。这里就需要定义一套栈和栈帧的数据结构,在虚拟机这里做入栈和出栈操作。 对函数调用栈有更深了解的,可以查阅:从汇编角度理解 “函数调用栈” 和 “有栈协程” 虚拟机做的第二步,是将复杂的逻辑前置。 AST 这棵树还是非常复杂,如果依旧在这个树上做深度遍历,执行效率难以有质的提升,不做深度遍历用其他的方式遍历也没有质的提升。概括就是底层数据结构不良好,上层算法难以用上力。 这里虚拟机自己定义了一套私有指令集,比如 iadd 指令,用来做加法运算。这套私有指令是和 CPU 的指令集完全不一样的,因为解析这套私有指令的是虚拟机本身,而不是 CPU。 有了这套私有指令集后,虚拟机只要认这套符合自己规则的私有指令集并执行就可以了,再也不用管 AST 这棵树了。 这里中间层又会发挥作用了。首先 AST 上面说了是语法分析的产物,这一个环节必不可少。而虚拟机需要的又是私有指令集代码,不在关心 AST 了。所以这里需要有个工具把 AST 转化成私有指令集。 这个工具就是字节码编译。虚拟机把自己的私有指令集定义为 8 位长度,即一个字节,最多有 255 个指令。因为每个指令都是一个字节,所以虚拟机把它叫做字节码。而字节码编译要做的事情,就是把 AST 转化成字节码。 虚拟机做的第三步,是指令优化。 AST 这棵抽象语法树结构现在已经变成了字节码指令结构。对于 1+10 这样的运算,数据结构上就不再是 AST 树,而是 load x, y 这样的指令了。 是指令就需要操作数据,对于数据的存取,虚拟机有两种不同的实现,分别是栈机和寄存器机。 栈机是通过栈这个数据结构来对数据进行存取。对于 1+10 这个运算,转化成的字节码是这样子:iconst_1;bipush 10;iadd; ,首先将数字 1 放入栈,然后将数字 10 放入栈,最后做加法运算就是把栈连续 pop 两次,把两次拿到的 1 和 10 做加法运算,运算结果再 push 到栈里面供其他消费。 寄存器机对于数据的存取主要依靠寄存器。对于 1+10 这个运算,转化成字节码是这样子:Ldasmi 1; Star r0; Ldasmi 10; Star r1; Add r0 r1; Star r2; ,首先将数字 1 读取写入寄存器 r0,然后将数字 10 读取写入寄存器 r1。最后将 r0 和 r1 寄存器的值做加法后再写入寄存器 r2 供其他消费。 显而易见,寄存器机是要比栈机快很多的。从 AST 转换成寄存器机要比转换成栈机困难很多,主要是虚拟机的寄存器也是映射的 CPU 的寄存器,数据是有限的。如何有效的处理这几十个寄存器显得非常重要了,而栈机依靠栈这种数据结构,只要内存够用,栈机的数据读取就不是问题。 目前,Java 虚拟机 JVM 使用的栈机,V8 引擎、PVM 使用的寄存器机。 虚拟机做的第四步,是 JIT 即时编译。 虚拟机发现某块代码经常被执行,那么虚拟机就会做一件事情,把这块代码编译成 CPU 指令集的二进制。然后把编译好的二进制放到内存的一块区域,并调用系统调用接口赋予这块内存可执行权限。 后续再次调用这块代码的时候,就直接转到内存处直接执行编译后的 CPU 指令。这里就和编译型的执行效率一样高了,都是直接跑在 CPU 上。 到这一步,解释型其实已经过渡到编译型了,只是编译的时机不同。编译型是必须做了后端编译,打成可执行文件后才能运行。JIT 是某块代码经常被运行后再做后端编译,并把产物放到具有可执行权限的内存区域后运行。 从这个角度来看,就不能再说 Java 是解释型语言,所以不够高效了。很多人抨击 Java 是解释型、和编译型相比中间有个 JVM 耗时层等等,最终指向 Java 慢或者效率不高。这就是对技术这个产业认知不到位。技术这个产业是只要有利益,需求都能满足,只要利益够大,需求就会被超出预期的解决。更多的摇摆因素是时间周期不固定,但需求一定会被解决。Java 在有了 JIT 之后,就是满负荷的编译型语言,哪还有什么效率差?即使说 GCC 或者 LLVM 耕耘多年后端能力底子深厚,Java 这些年的 JIT 能力相比也不会有太多出入了。至于 Python 的 PVM 虚拟机也有 JIT 能力但效率还是不如其他语言,那还有语言本身的限制在,比如 Python 的 GIL 锁、Python 动态语言特性等。 两种进程创建方式 By fork & exec 在 Linux 系统下,进程创建只有一个方案,就是 fork 系统调用。 如果要创建一个新的进程,那么就得找到一个已经存在的进程,在这个进程里面调用 fork api,然后生成一个一模一样的新进程(可以认为一模一样,肯定会有差异)。新进程和已经存在的进程是父子关系。 从这个角度来看,整个 Linux 操作系统的进程图谱就是一个多叉树。操作系统启动的时候会创建 1 号进程 init。后续所有的进程的祖先,都是这个 1 号 init 进程。 对于 fork 有两个返回值,理解起来其实并不难: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int cal() { // 这里是非常复杂的计算,耗时严重 int i = xxx; return i; } int main() { int index = 0; pid_t pid; pid = fork(); if (pid == 0) {// 子进程 printf("This is sub process, and pid is: %d\n", getpid()); printf("the index is: %d, address is: %p\n", index, index); index = 10; printf("the index is: %d, address is: %p\n", index, index); int ret = cal(); // 和父进程通过进程间通信将 ret 值给到父进程 } else {// 父进程 printf("This is origin process, and sub process pid is: %d\n", pid); printf("the index is: %d, address is: %p\n", index, index); } return 0; } 上面 C 语言中,调用 fork 系统调用后,会有两次返回。为了保障子进程的优先级,一般子进程会先返回。 第二次返回的时候,pid 是子进程的进程号。这个时候代表父进程的执行流程。父进程后面该怎么做就怎么做,不受影响。 但是第一次返回的时候,pid 是 0,表示当前处于子进程。进程是应用级别的单位,即两个应用肯定是两个进程。而 fork 的作用就是复制一个一模一样的进程出来,所以这个时候,表示新的进程的代码区也执行到了这里。可以这么理解,你已经使用 Telegram 15 分钟,在使用期间,你和 N 个好友聊天过,并且查看了 M 次订阅和 T 次群聊。但是在 15 分钟这一刻,Telegram 调用了 fork 接口,那么就打开了一个新的 Telegram 进程,在这个新的 Telegram 进程里面,你同样和 N 个好友有一样的聊天,也看了 M 次同样的订阅和 T 次同样的群聊,以至于新的 Telegram 进程里面,局部变量、全局变量都是一样的。 所以,fork 的这一刻,是两个进程的分界岭,之前的代码逻辑全部一样,在这一刻的数据也都是一致的。但之后就各走各的路了,不然开新进程干嘛呢。 我们可以在新的进程里面调用一个复杂的运算,比如上面的 cal 函数。在子进程完成运算后,将运算结果通过进程间通信给到父进程,这样可以有效使用多核 CPU 了。 所以 fork 为什么有两次返回?其实根本不是 fork 函数有两次返回,没有一个函数能够返回两次,这违背了函数调用栈的原理。详见:从汇编角度理解 “函数调用栈” 和 “有栈协程” 而是这本来就是两个完全一样的进程,执行到了同一个内存代码地址。在这之前,其中一个进程偷懒,复制了另一个进程的执行流程。在这之后,两个进程就各走各的路了。 这里为什么父子进程拥有完全一样的局部变量、全局变量以及堆栈,是因为从进程的数据结构层面做的复制,所以虚拟指针这些都是一样的,映射的物理内存现在也还是一样的。 之后就各走各的路了,是因为进程数据结构复制的时候做了标记,后面两个不同的进程对物理内存进程写操作的时候,会把虚拟内存通过 MMU 单元映射到不同的物理内存上。比如代码区,两个进程肯定要执行不同的代码了,那么前面做的标记会出现缺页异常,从磁盘拿到不同的可执行文件的指令放置到新的内存代码区中。 这个技术叫做写时复制,可以有效提高子进程的创建效率。 这里也会出现一个现象,就是同样的虚拟地址,对应的数据确实不一样的。比如上面的 C 代码中,子进程将 index 赋值为 10,但是在父进程中读取的 index 还是 0。但是这个时候两个进程的 index 的虚拟地址都是一样的。其实就是同一个虚拟地址,通过两个进程的 MMU 映射到了不同的物理内存栈区地址。他们已经是两个进程了,相互隔离,互不影响了。 对于虚拟地址和 MMU 不理解的,可以看深度说明:内存分段与分页 exec 前面开头就说到,在 Linux 系统下,进程创建只有一个方案,就是 fork 系统调用。 但是 fork 产生的新进程,是和原来进程一模一样的。这对于有些进程来说,或许并不希望这样的结果。 进程是应用程序的基本单位,我们打开 Twitter 和 Telegram,其实就是打开两个进程。那对于 Twitter 和 Telegram 来说,其实他们希望他们有一个纯净的虚拟环境,而不是从父进程带进来很多杂七杂八的全局变量啥的。 这和 fork 的机制有些矛盾,所以这个时候就需要 exec 了。也叫做创建进程的第二种方式,即连续调用 fork () 和 exec () 两个 api。 exec 的作用很清晰,清空当前进程的所有数据,包括变量、代码区,完全变成一个新的进程环境。 比如上面 C 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 int main() { int index = 0; pid_t pid; pid = fork(); if (pid == 0) {// 子进程 exec(); printf("the index is: %d, address is: %p\n", index, index); } else {// 父进程 // other } return 0; } 在子进程中调用 exec 后,就找不到 index 局部变量了,会运行时报错。因为这个时候数据都被清空了,代码区也不在了,所有都是新的。 如果像这样:exec load twitter;(伪代码),就可以唤醒 Twitter 进程,Twitter 就可以从 0 加载起来了。(这里不说 Twitter 从 main 函数开始执行了,因为 main 函数不是一个程序第一个执行的函数,前置还有动态库加载等很多操作,详细可参考《程序员的自我修养》) 其次,exec 是一个接口簇,比如 execl () 等等。不同的接口有不同的表现形式。 最后,还有一个重要的点,并不一定非要调用 fork 了才能调用 exec。在任意时刻都可以调用 exec。只是调用 exec 后,当前所在进程的数据都会被清空。如果 exec 之后没有执行单元或者指定单元执行完毕,当前所在进程也就会被销毁了。 0x01 Shell 和 Shell 解释器 Shell 是我们对于命令行指令的统称。有时候我们也直接把 bash 叫做 Shell,虽然这有些不对。 我们可以这样定义 Shell,就是用于和内核进行交互的用户应用层软件。 在硬件层,是 CPU、内存这些硬件设备,我们虽然可以直接使用,但是那会很痛苦,于是操作系统出现了。操作系统可以帮我们做非常多的事情,比如进程管理、时间片轮转等。 操作系统是一个大而全的东西,它有一个核心,即内核。整个操作系统以及上层应用,都会通过内核和硬件层通信。所以内核对上层应用做了收口,我们经常说的系统调用,也就是调用的内核开放出来的 Api。 Shell 就是在内核的上一层,通过对内核进行各种系统调用,来完成各个命令的功能。而使用这些命令的人,即用户。所以 Shell 是一个应用层的应用程序。 即然 Shell 是一个应用程序,那 Shell 就是我们打开的终端软件吗 (比如 iTerm)?也不是。 我们使用的终端,早期叫控制台。那时候一台电脑只有一个输入输出,就是通过控制台来操作。 后来电脑支持了多用户,一个控制台不够用了,就每个用户一个虚拟终端用来接入。这个虚拟终端也叫做终端模拟器 (终端仿真器)。 所以 iTerm 这些,都是终端模拟仿真器,简称终端。 其实 Shell 是对一个系列的应用程序的统称。这些系列包括 bash 解释器、sh 解释器、zsh 解释器等。 bash、sh、zsh 这些,我们称为脚本语言,也是上面说到的解释型语言。对于这些语言,还不需要使用虚拟机用来提速,所以他们使用解释器对脚本指令逐行翻译执行即可。 脚本文件是没有提前编译,就在终端里面被执行了,这就是 bash 解释器做的事情。而 bash 解释器逐行翻译脚本的过程,就是上面的解释型语言里面说到的 AST 深度遍历。 0x02 Shell 内部命令 & 外部命令 bash 解释器本身是一个应用程序,通过 C 语言编译成的。它本身是完整的 COFF 格式的可执行文件,是编译后的产物。 即然是可执行文件,那么就有代码区及函数。而我们使用的命令也分为内部命令和外部命令,比如 pwd 就是内部命令。 pwd 其实就是代码区的一个 pwd () 函数实现。内部命令是在 bash 解释器应用程序内部完成的,就像 main 函数可以对应一个 main 命令一样 (假设)。 除了内部命令,bash 解释器还可以执行 ls 这样的外部命令。 外部命令的执行流程和内部命令是完全不一样的。外部命令本身都是一个应用程序,比如 ls。它的执行会是这个样子: 1 2 3 4 5 6 7 pid_t pid; pid = fork(); if (pid == 0) { exec ls; } else { wait(NULL);// 等待子进程执行完成 } 就是在当前进程创建子进程后,就 wait 等待子进程执行完成。而子进程就通过 exec 初始化进程环境然后执行另一个应用程序。 大家回想一下终端里面操作命令的时候,是不是就和 wait 这种情况一样?我们执行 ls 后,就不能输入了,等 ls 执行完毕后,我们才能继续输入命令。 从上面来看,内部命令,其实是解释器内部的一个函数实现。而外部命令,其实是另一个应用程序。 理解了前面说的解释型语言,Shell 命令其实就这样。 0x03 Shell 和 SubShell Shell 里面有一个很重要的概念,是子进程和子 shell。其实子进程就是 fork + exec,子 shell 就是 fork。 那子进程和子 shell 有什么不同呢?看这里: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ~ ❯ name=wanger 02:45:08 ~ ❯ echo "current process - the name is ${name}." 02:49:32 current process - the name is wanger. ~ ❯ { echo "current process - the name is ${name}." } 02:49:53 current process - the name is wanger. ~ ❯ (echo "fork process - the name is ${name}.") 02:51:03 fork process - the name is wanger. ~ ❯ cat d.txt 02:51:44 #!/bin/zsh echo "fork&exec process - the name is ${name}." ~ ❯ bash d.txt 02:51:48 fork&exec process - the name is . 对于全局变量 name,在当前进程和 fork 进程均能正确读取,但是在子 shell (fork&exec) 里面就读取不到了。 这个现象,就是前面两种进程创建方式 By fork & exec 里面说到的,前面的理解了,这里就搬一下场景。 对于 bash x.sh 和 ./x.sh 执行方式,默认都是使用的 fork + exec 进程创建方式。当然也包括 ls 这些外部命令。 对于小括号组合命令、命令替换、管道,默认都是使用的 fork 进程创建方式。 对于大括号组合命令,则不创建进程,在当前进程执行。 验证当前是子进程还是 subShell,有两个环境变量:SHLVL (子进程) 和 BASH_SUBSHELL (子 shell)(zsh 使用 ZSH_SUBSHELL)。打印如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ~ ❯ echo "SHLVL-${SHLVL}, ZSH_SUBSHELL-${ZSH_SUBSHELL}" 03:00:34 SHLVL-1, ZSH_SUBSHELL-0 ~ ❯ { echo "SHLVL-${SHLVL}, ZSH_SUBSHELL-${ZSH_SUBSHELL}" } 03:06:16 SHLVL-1, ZSH_SUBSHELL-0 ~ ❯ (echo "SHLVL-${SHLVL}, ZSH_SUBSHELL-${ZSH_SUBSHELL}") 03:06:28 SHLVL-1, ZSH_SUBSHELL-1 ~ ❯ cat d.txt 03:07:12 #!/bin/zsh echo "SHLVL-${SHLVL}, ZSH_SUBSHELL-${ZSH_SUBSHELL}" ~ ❯ zsh d.txt 03:07:23 SHLVL-2, ZSH_SUBSHELL-0 因为打开终端后,当前 Shell 环境就是一个子进程,所以 SHLVL 默认是 1。从上面可以看到,子 shell 场景 ZSH_SUBSHELL 会 +1,在进程场景 SHLVL 会 +1。 0x04 Shell 变量作用域(全局变量 & 环境变量) 对于 Shell 的变量作用域,从上面的 name 全局变量和 fork&exec 的分析,其实就可以有大概结果,那就是全局变量在当前进程和子 shell 都是有效的,在 fork&exec 子进程中是无效的。 但是环境变量有一个特殊的地方,就是在子进程中也有效,这是怎么做到的呢? 其实并不复杂,首先子进程的 fork 和 exec 肯定都是执行的,那么数据就一定会被清掉,环境变量能够在子进程中有效,肯定是父进程传参给子进程的,子进程在 exec 之后还原了入参,仅此而已。 如果一个外部命令使用 C 语言写的,那么 main 函数会是这样: 1 2 3 4 int main(int argc, char *argv[]) { // 入参就是 argv return 0; } 执行到 main 的时候,外部命令创建的进程的 exec 都已经被执行完毕了,但是在外部命令内部还是可以读到入参的。 像 bash x.sh 这种形式,默认帮我们做了 fork 和 exec,那么入参的读取也默认帮我们做了,所以我们能够在子进程中读到环境变量。 但是子进程的环境变量有个局限性,就是只能在子进程内部使用,在子进程内部修改或者新增的环境变量都不会影响到父进程。这也完全合理,可以从前面说到的进程创建来分析,他们就完全是两个进程了,当然不会互相影响。如果要影响,只能走进程间通信了。 这里还有一个点要说明一下,就是为什么很多配置脚本修改完成后,要 source 一下。比如在 bashrc 里面增加了一个 expore 环境变量后,我们会 source ~/.bashrc。 source 命令就是把文本内容逐行取出来,在当前进程解释执行一遍(前面说到的 AST 深度遍历一下)。这样新增的 expore 环境变量,就会在当前进程生效了。 这样就避免了重启终端刷新 bashrc 配置文件才能生效的耗时了。 整体下来,Shell 的运行原理没有高级语言那么多深奥的东西。核心的基础知识:解释型语言如何解释执行以及 fork & exec 进程创建。

2022/10/4
articleCard.readMore

内存分段与分页

一年前写了 “段页内存管理” 的部分章节,后面一直搁置在草稿箱中。最近发现内存相关的知识非常重要,最近几十年科技文明的巅峰,硬件侧就是 CPU,软件侧就围绕着操作系统对内存的控制了。就又整理补齐,形成内存分段分页较完整说明。 硬件 CPU 的不断革新,从 X86 和 ARM 架构的铺天盖地,到 RISC-V 新星崛起和 MIPS 的消退。还有各种流水线优化、APU-ZPU 的推成出新,以及 Apple M1 的展露头脚。虽然 CPU 本身短时间内看不到跨越式的提升,但 CPU 周边是玩出了花。 而已经趋于稳定的操作系统,最近好多年都没有啥惊世骇俗的壮举出现。依托着 CPU 侧的大腿,操作系统躺着进行升级。但换一个角度,也可以认为操作系统已经设计的足够完备,在商业化的时代,有需求就一定会有满足,或许对于当前操作系统来说,目前的设计应对有余。 操作系统的很多设计,都摆脱不了内存这尊大神。首先,操作系统本身和上层应用,就是放在内存里运行的。其次,操作系统的基石进程和线程,就贴着内存进行设计。多核心后的 L0-2 级缓存同步也是为内存定制,当然这个算 CPU 对内存的依赖。IO / 文件系统 / 网络也都离不开内存的影子。 内存本身的制作工艺门槛不高,有 N 多厂商做这个事情。但内存在科技长河中的位置,绝对举足轻重。整个计算机的发展史,内存表现不多,但是中流砥柱。 本文可以顺带解决如下几个问题: 地址总线、数据总线、控制总线是什么?CPU 如何通过地址总线找到内存地址? CPU 和内存之间的高速缓存引发的缓存一致性问题是怎么回事?锁 - 共享数据安全指↑ 16 位 CPU 是如何操作 1M 内存空间的 (2^16=64kb)?32 位 CPU 是如何操作 64G 内存空间的 (2^32=4G)?他们的原理一样吗? 分段内存管理,里面的段指的是什么? 除了虚拟地址、物理地址,还有线性地址和逻辑地址,它们是什么? 两个进程的虚拟地址相同,是如何指向不同的物理地址的?虚拟地址的页目录 & 页偏移均一致,如何通过 m = f(n) 函数,以相同的 n 输入,返回不同的 m 输出。 推荐书:《汇编语言》- 王爽、《程序员的自我修养 - 链接装载与库》- 俞甲子 推荐网文:《深入浅出计算机组成原理》- 徐文浩、《操作系统实战》-LMOS、 一定要读的另一篇内存文章:锁 - 共享数据安全指↑ 内存相关的知识非常重要,需要很大篇幅和示例图来阐述。在锁 - 共享数据安全指↑中文字数超过了 4W,本文的文字数也超过了 1W,均需要多张配图。重要性和难理解程度,都很高。 0x00 内存是如何被 CPU 消费的 虽然操作系统和各类应用程序,都需要使用内存。但是对内存的访问,却是 CPU 做的。软件层能直接通信的硬件对象,只有 CPU。CPU 是集合了运算器 (ALU)、控制器、存储器的大部件,其中存储器,指的就是内存 & 磁盘等信息存储设备。 CPU 对内存有一套统一的读取控制,上层软件对内存的操作皆通过 CPU 进行收口。操作系统的进程设计,如数据隔离 & 共享,就是通过 CPU 对内存空间进行增删改查。 CPU 和内存都是硬件,硬件之间相互通信就得依靠有线和无线传输,显然 CPU 和内存通信需要有线。这里有一个南北桥的历史,CPU 通过北桥和高速设备连接,通过南桥和低速设备连接,南北桥就是 CPU 和外部设备通信收口的地方。现在高速设备的连接,都是集成在 CPU 内部,不再通过北桥来做了,但是南桥还是依旧在的。但不管有没有北桥,CPU 和内存通信,都需要三根总线,即地址总线、数据总线、控制总线。 地址总线,就是对内存区域进行定位的。内存中每个内存地址代表一个字节,共 8 位。一个 256 字节的内存条,内存地址从 0x00 (00000000) 到 0xFF (11111111)。其中每个字节都有一个地址,如 0x03 表示第 3 字节。 控制总线,就是 CPU 对内存的控制指令。是读 (load) 数据,还是写 (store) 数据等。 数据总线,就是用于 CPU 和内存之间双向传输数据用的。如果有 8 根总线,那么一次只能传输 1 个字节的数据。如果 CPU 想写 2 个字节的数据到内存中,就需要传输 2 次。 下图是 8086 CPU 对内存地址 0x03 进行 c 字符读取的流程图。 顺带提一个,CPU 其实不喜欢所有的外部存储设备。因为即使内存速度已经比较快,但相比 CPU 来说,还是太慢。 CPU 内的运算器、控制器都是晶体三极管制作,通过各种门电路进行电信号传输,寄存器内部元件的运行速度是非常快的,速度和 CPU 同步。 内存是动态随机存储器 (DRAM),通过一个晶体管和一个电容来保存一个比特的数据。如果要表示 1,就需要输入高电压,往电容里面充电。当开关关闭,就需要输入低电压,电容开始放电,表示 0。电容还会漏电,所以每隔一段时间,就需要往所有开关打开的电容里面补充电量。电容的这个机制,就是为什么内存不能做断电存储的原因,也是为什么叫动态随机存储器里面动态两个字的原因。 所以 CPU 在和内存进行数据读取的时候,时间差就会非常大,CPU 发出指令,需要在 100ns 后拿到内存数据。如果把 ns 作为数量级,那么 CPU 和内存之间的速度差有 2 个数量级 (100ns)。当然磁盘更大,达到 6 个数量级 (150us)。 所以 CPU 就在它和内存之间架起了一个中间层,即 L0-2 高速缓存。 高速缓存是静态随机存储器 (SRAM),需要 6-8 个晶体管来保存一个比特的数据。通过晶体管的组合可以形成锁存器,对 1&0 进行记忆,就不需要电容来保持高低电平的状态了,速度也会更快。CPU 和高速缓存之间速度上有 0 个数量级 (1ns)。 高速缓存是必须的,因为 CPU 和内存的速度鸿沟很大。但是高速缓存也引发了缓存一致性问题,在我的另一个 blog 里面有详细说明和解决方案,都是干货。锁 - 共享数据安全指↑ 好,这里就把 CPU 如何与内存通信的硬件链路说通了。但在 CPU 读取内存之前,还有操作系统的干预,属于前置链路。有下面几个问题: 应用程序通过 CPU 操作内存,那么有没有可能 A 应用程序通过 CPU 发送的内存地址是 B 应用程序的?这会导致各个应用程序的数据窃取和乱改,非常可怕。 A/B 应用程序都占用一段内存空间,还剩下一部分空间 M 没有使用。这时候如果关闭了 A 程序,打开了 C 程序。C 需要的内存空间大于 A 和 M,小于 A+M,这时候如果执行 C 程序? A 程序会释放部分它不再使用的内存空间,这些内存空洞该如何处理? 围绕这三个问题,就展开了内存分段分页黄金追逐时代。 0x01 内存蛮荒时代 - 分段 在 8086 芯片被 intel 破土成功的时候,内存是被程序员直接控制使用的。那时候还没有虚拟内存、MMU、分页等等这些概念 (这个时候叫逻辑地址,后面会说)。简单来说,那时候内存控制是低级别控制,得开发人员自行汇编控制。想读取 0x?? 地址的内存数据,得汇编写出来这个地址。 8086 的硬件配置是数据总线宽度 16、地址总线宽度 20。而寄存器的位数一般都是和数据总线宽度相同,即 16 位寄存器。 CPU 的运算器、控制器都是通过寄存器表达的,这时候就会有个问题,向内存地址总线传递的地址如果直接从寄存器来,那么范围不够。16 位寄存器只能确定 0-2^16 字节 (64kb) 的内存空间范围,而 20 根地址总线可以确定 0-2^20 字节 (1M) 的内存空间(开头提到的第三个问题,这里就是 16 位 CPU 能操作 1M 内存空间的原因,因为有 20 根地址总线。32 位 CPU 后面再说)。所以 64kb-1M 的空间都只能被浪费掉,这显然是不行的。 所以 intel 想了一个办法,增加了 4 个段寄存器,分别是 cs、ds、ss、rs。通过 cs:ip 两个寄存器来确定 pc 寄存器的地址,即 cs*16+ip,其他段寄存器也是一样的计算方式。 cs*16,即将 cs 的值左移 4 位。这样就通过段寄存器 * 16 + 段偏移量的方式,增加到 20 位,也满足了 20 根地址总线的最大内存检索范围。 而 cs、ds、ss、rs 也分别表示代码段、数据段、栈段、其他段,可以做数据安全。比如数据段,就需要 ds 来做段基地址,可读可写。如果写成了 cs 代码段寄存器,代码段权限只读,那么写数据的时候就会异常。 虽然话是这么说,但是 8086 CPU 并没有做这层校验,ds*16+ip (ds 是数据段,ip 是代码区) 这样的写法,也不会报错,能够正常读取到指令。但也没人刻意这么做。 这些段寄存器,就表示了内存的分段模型。 这里有一个误区,即可执行文件的二进制格式,也是通过代码区、数据区这些区来表达的,是不是和分段模型有关联? 对于可执行文件,不管是 window 的 PE,还是 Linux 的 ELF,都是从 COFF 发展而来。COFF 格式的可执行文件,就是将指令和数据分开存储,即我们的代码在代码区,只读。一些 const 变量则放置于常量区,也是只读。一个全局变量则放置于静态区,可读可写。函数方法中的局部变量,则放置于栈区,可读可写。 COFF 这套二进制可执行文件的区域划分,本身是和内存分段没有关系。如果没有 8086 CPU 的 16 位寄存器和 20 位地址总线的约束,比如有一个 CPU 正好地址总线也是 16 位,那么内存访问就不需要段寄存器了,可以直接寻址。这就不是内存分段模型了。 主要是内存分段模型,通过段寄存器解决了 16 位寄存器和 20 根地址总线不对应的 CPU 内存数据互通问题,我们把这种 CPU 和 内存不对应的关系的解决方案,叫做分段模型。其实现在的 64 位 CPU,可执行文件还是 COFF 格式,可他们已经不在是分段模型了。 8086 时期,CPU 访问内存的这种方式,也导致了不少问题,有内存安全、内存大空洞 (应用关闭)、内存小空洞 (碎片化)。 大多人对这几个问题的理解有出入,他们认为导致这些问题的原因是分段模型,我认为非常不准确。当然,如果在 8086 时期,就搞出了分页模型,当然不会有这些问题。但当时为什么没有分页模型出现?我认为是当时的环境,使用分段模型就已经能够解决问题了。毕竟那时一个 CPU 大多只跑一个应用程序,都不需要操作系统的计算机发展的初级阶段。那个时候,能把 8086 CPU 做出来,就已经是科技发展的巅峰了。所以分页能够解决的问题,在那个时候,可能仅仅是一小戳人的需求。 内存安全 这个阶段还没有虚拟地址的概念,也没有分页内存的概念。所以应用程序是全部加在到内存中后再执行。基于分段模型,应用程序可以直接使用物理内存地址进行数据的增删改查。 这时候会产生非常严重的破坏性操作,即 A 程序修改或者读取了 B 程序的数据。如下图所示: 内存空洞 内存空洞使得这个阶段的应用程序执行非常艰难,需要提前规划好内存分配情况。 操作系统这个时候也可以将部分内存空间 swap 到磁盘中,从而空出一块比较连续的内存空间给合适的应用程序执行。但磁盘的换入换出性能消耗也非常大。因为磁盘和 CPU 之间,有 6 个数量级的时间差。 所以本质上来说,这时候的内存分段是比较混乱的,段寄存器更多的用于 16 位寄存器和 20 根地址总线之间的不协调的适配。仅仅是因为 8086 CPU 的寄存器位数和地址总线不一致,所以才有了段寄存器这种内存定位的方式。 但也不能把仅仅两个字说的这么轻描淡写,因为 8086 这套规则,影响实在太大了。 8086 是计算机初期发展使用范围最广的 CPU 版本。其 x86 架构现在还在征服着 PC 机市场,而它的分段模型,也直接影响着后续的 CPU 升级,即兼容。 Inter 的 CPU 发展,一直在做架构指令集的兼容。这种兼容是 Inter 必须要走的路线,因为 8086 太成功,有太多商业应用了。从兼容了第一个版本开始,就要一直兼容下去。其中有一款和 AMD 竞争的酷睿处理器,就是因为没有做兼容,导致 AMD 崛起,预先做出了 64 位指令集。这套指令集兼容了 x86,又叫做 x86-64,基本上现在的 PC 机,所使用的 CPU 都是这套指令集。 在 Inter 发展 32 位 CPU 的时候,因为兼容,段寄存器的概念也无法丢弃,又开始做改造了。 0x02 内存青铜时代 - 分段分页共存 说多少位 CPU,其实就是寄存器是多少位的。如果说一个 CPU 是 N 位,那么 CPU 的寄存器一定也是 N 位,数据总线一般的宽度一般也是 N,地址总线的宽度就不确定了(8086 16 位 CPU,地址总线宽度 20。32 位 CPU,地址总线宽度也是 32。64 位 CPU,地址总线宽度是 46,因为没有那么大的内存条,更宽的总线是浪费)。 到了 Inter 32 位 CPU 发布的时候,这个时候寄存器是 32 位,数据总线、地址总线也都是 32 根。32 位 CPU 使用的还是分段模型,但是 32 位 CPU 还有一个平坦模式可以切换,平坦模式使用的就是分页模型了。 32 位 CPU 叫做保护模式了,因为增加了很多安全控制,比如 R0 - 3 四个特权级。Windows 和 Linux 只使用了其中的两个特权级。相比 16 位 CPU,那个时候叫做实模式。 保护模式未开启平坦模式 虽然寄存器是 32 位,但 32 位本身也可以作为独立的 16 位使用,比如 32 位的 AX 寄存器,高 16 位用作 AH,低 16 位用作 AL,可以独立使用,这也是兼容。 但是段寄存器还是 16 位。至于为什么这么做,我猜有一定原因是为了弱化内存分段模型,毕竟 CPU 的晶体三极管,也是寸土寸金。 在 16 位 CPU 的时候,段寄存器存放的是段基地址,把段基地址左移 4 位和段偏移量做和运算,即最终的物理地址。 那么 16 位的段寄存器还怎么存储 32 位的段基地址呢? 这个时候多了一个段描述符,段描述符是一个列表。运行程序的时候,先将程序需要的各个区的大小整理好,写入程序内存中,生成段描述符列表。并将段描述符列表的地址和长度保存在 GDTR 寄存器中 (后面再说)。 每个表项长度是 64 个字节,存储有段基地址和段长度,用于表示代码区的基地址是多少,代码区共有多大这样子。还存储了其他一些信息,比如有个 G 标志位,表示当前段长度的单位是 1 字节还是 4 kb。因为段长度总共分配了 20 位,如果单位是 1 字节,那么该段长度就是 1M。如果单位是 4kb,那么该段段长度就是 4G。除了 G 标志位,还有 T 表示代码段或者数据段,R 表示是否可读,C 表示是否可执行等等。 段描述符兼容了 16 位的段寄存器,所以 64 位的长度里做了很多取舍和兼容。 有了段描述符列表,只需要在 16 位的段寄存器里面,指定当前需要段描述符列表的第几位,拿到列表项后,列表项里面存储着段基地址和段长度等信息。 段寄存器里面只存放段描述符列表的索引,还需要一个地址标记着段描述符列表在哪里,这样才能找到对应的列表项。段描述符列表的地址就存放在 GDTR 寄存器中,上面已经说到。 这样,32 位保护模式未开启平坦模式的时候,内存读取操作的表现就是和 16 位 CPU 一样的。只是多了段描述符和 GDTR 寄存器这些中间层。 保护模式开启平坦模式 CPU 不会主动开启平坦模式,当然,操作系统会帮我们开启。当开启了平坦模式后,就从分段模型切换到分页模型了。这里就说下什么是分页模型。 首先,根据时间 / 空间局部性,程序运行过程中,同一段时间只会有一部分代码区指令在执行,其他代码区指令都没有被执行。而执行的这些指令,一般都在一块。 那么我们可以在某块代码区被执行的时候,再把这些指令加在到内存中,其他的指令依旧保存在磁盘中,这样可以减少应用程序对内存的占用。 刚才说到某块代码区,如果我们可以标记这个代码区的大小,就可以对代码区的加载做自动化。即需要某块代码区的时候,就加载固定大小的磁盘空间到内存中来。过一会这些指令执行完了,那就从磁盘再加载下一个区块的指令。 那,这个代码区的大小,设置多少合适呢?目前主流的操作系统都设置为 4kb,也有设置 4M 的。 刚才说的某块代码区 4kb,说的还是二进制可执行文件。这个时候文件还是存储在磁盘上的。我们是按照 4kb 对文件进行了分割。那分割并加载这 4kb 的文件到内存后,内存也理应有 4kb 大小的区域对这 4kb 文件进行存储。 所以我们将内存也分割成 N 个 4kb 大小的虚拟区块,注意,这是虚拟区块,物理上内存区块是连续的。4kb 虚拟区块,就是分页模式的基石。 有了这 4kb 的虚拟区块,因为颗粒度很细且固定,所以可以完成很多分段模式不好完成的事情。比如分配和回收,而分段模型最难的地方就在于分配和回收,因为段长度不固定,颗粒度太大。 分页模型,主要的三个理解名词,一个是 4kb,一个是虚拟地址,还有一个就是页表。 在上面 16 位 CPU 实模式的时候,以及 32 位未开启平坦模式的时候,使用的都是物理地址。就是开发人员在汇编里面写入内存的物理地址,或者编译高级语言的时候链接器指定物理地址,然后程序通过物理地址进行内存读取。其实分段模型最大的问题就是使得虚拟地址无法实施。使得分段模型下没有虚拟地址。 虚拟地址,就完全不需要开发人员来做了,开发人员根本做不来了。可执行文件的虚拟地址都是通过链接器来实现的(非编译器)。 虚拟地址,就是给一个应用程序错觉,让每个可执行文件都认为自己拥有内存的所有区域的使用权限。所以对于 32 位 CPU 来说,每个可执行文件的虚拟地址范围都是 0x00000000 - 0xFFFFFFFF。 然后趁应用程序不注意的时候,通过页表这个数据结构,将虚拟地址转换成物理地址,并进行真实的内存读取。 虚拟地址到物理地址到转换,肯定是要牺牲一些性能的。为了更加的高效,就在 CPU 里面配了一个 MMU 硬件。 MMU 默认会去读页表的数据。页表里面一开始是空白的,如果发现空白,就会发出缺页异常,去磁盘加载对应的 4kb 大小的指令到物理内存中,并把物理内存的地址更新到页表中。然后,后续这 4kb 指令的读取,MMU 会去拿页表里面存储的物理地址。 上面二级页表视图里面,有一个 CR3 寄存器,存放着当前应用程序的页目录地址。拿到这个一级页目录地址后,再用虚拟地址里面的页目录索引做偏移,就可以拿到二级页表的地址。 当应用程序切换的时候,只需要更新 CR3 寄存器的值为当前应用程序的页目录地址,就可以使得每个应用程序都有独立的页表了。 这个 CR3 寄存器,在一项 PAE 的技术里面也有使用。末尾彩蛋会说。 32 位 CPU,地址总线也是 32 位,所以最大的内存空间就是 4G 了。这里使用了二级页表。整体来说,页表的级数越多,页表本身会越省空间。但是这个中间层也是耗时的,级数越多,操作耗时也就越多。所以一般 32 位 CPU 使用的是二级页表,64 位 CPU 就使用的四级页表了。64 位 CPU 在后面的长模式里面说。 操作系统会帮我们开启平坦模式,所以大家使用的一般都是保护模式的平坦模式分页模型。使用了平坦模型,上面的段寄存器不是说就丢掉了,因为要兼容。 兼容的办法就是将所有的段描述符列表里面的所有项,段基地址都修改成 0x00000000,段长度都修改成 0xFFFFFFFF,然后把 G 标志位 (段长度单位) 修改成 4kb。这样,所有的段基地址都是 0,段长度都是 4G (2^20^4kb) 了。 这样,就可以当作断寄存器不存在了,完全又分页模型来控制。但实际上这层段地址的运算还是存在的,因为要兼容。 对于虚拟地址,开头有个题目 6,可能有同学会不理解。两个应用程序的虚拟地址是一样的,那么 MMU 如果通过页表找到的物理地址不一样?因为按照上面二级页表的寻址规则,入参一样,出参应该也一样。 其实上面的图示里面,是已经发生缺页异常后的场景。因为发生了缺页异常,于是从磁盘里面加载了 4kb 放置到内存中,并把放置该 4kb 的内存地址写入到了页表中。 对于不同的应用程序,在发生缺页异常的时候,放置 4kb 的内存区块肯定是不一样的,这是操作系统来维护的,不需要我们担心。所以缺页异常后不同的应用即使虚拟地址相同,写入到页表中的物理地址也是不同的。 当然,这也有一个专业的名词,叫做页表管理。 还有为什么多级页表会节省空间,这个就自己画一下整理一下,就能理解了。 保护模式到底属于什么内存模型 i386,就是 32 位 CPU。我们把这种 CPU 叫做段式管理和页式管理混合模式。其实没啥新意,就通过平坦模式来进行区分。他们并没有过多的混合,更准确的说法,应该叫段页隔离模式。 0x03 内存文明时代 - 分页 前面说到 Inter 在弄一个酷睿处理器的时候,不仅高性能的酷睿处理器没弄好,还错失先机,让 AMD 弄出了 x86 兼容 64 位,即 x86-64。 32 位 CPU 能支持的最大内存就是 4G。当然通过 PAE 技术也能使用到 64G 的内存,但需要特别改造,后面的加餐会说。 在内存快速发展的时间点,64 位 CPU 可以搭配更大的地址总线,毕竟更大的内存谁会不喜欢呢。 这就来到了 Inter x86 架构的长模式。 在实模式的时候,只能使用分段模型的物理地址。在保护模式的时候,分段和分页模型可以并存,但是操作系统会修改成平坦模式,大多使用的都是分页模型。到了长模式,就只有分页模型了。 一方面只能使用分页模型,一方面 x86 的分段模型还要兼容。32 位平坦模式的兼容办法是把段基地址修改成 0x00000000,段长度修改成 0xFFFFFFFF。长模式的兼容办法是把段基地址和段长度都设置为无效位,避免了多余的运算。 因为整体的分页模型没有改变,所以对内存的操作上,大的方向没有改变。这里额外说明另外两个知识点,一个是线性地址,一个是 TLB。 线性地址 & 逻辑地址 虚拟地址是给应用程序看的,物理地址是给内存看的。从上面的兼容可以看到,段寄存器一直存在,那么虚拟地址还会经过分段模型走一遍,产生另一个地址 N。然后 N 被 MMU 硬解成物理地址。 这里的 N,就是线性地址 (有小彩蛋), 即分页模型下经过段寄存器处理过后的地址。 对于 32 位保护模式下的平坦模式,段基地址为 0,段长度为 4G,所以 N 等于虚拟地址,即段寄存器空运算了一次。 对于 64 位长模式,段基地址和段长度都是无效位,所以 N 等于虚拟地址,即段寄存器没有参与运算。 所以分页模型下,虚拟地址和线性地址是一样的值。 !!因为虚拟地址和线性地址是一样的,而我们一直都习惯叫虚拟地址,其实是先入为主了。 !!其实,虚拟地址和线性地址的定义刚好反过来。即程序里面看到的,其实是线性地址,经过分段模型后产生的 N,其实是虚拟地址。 !!更宽泛点来说,线性地址就是虚拟地址,虚拟地址就是线性地址。虚拟地址和线性地址相互是 alias,毕竟他们之间完全相等。但上面的分段模型运算就是他们之间的差异。 把同样的逻辑,搬到 16 位 CPU 实模式分段模型的场景,那时我们是通过 cs*16+ip 这种形式来确定物理地址的。cs 和 ip 寄存器的值。我们可以提前计算这个值,而最后也通过这个值进行物理内存地址寻址。 我们认为我们通过物理地址来寻址,其实我们用的是 cs 和 ip。这种通过段基地址左移加偏移的方式,即 base*16+offset,就是逻辑地址。逻辑地址不是计算后的值,而是指代 cs 和 ip 本身,是一对。 逻辑地址通过分段模型后,生成的值就是物理地址。 所以分段模型下,逻辑地址和物理地址虽然定义不一样,但实际上也可以说是一样的值。 TLB TLB 高速缓存,和内存的 L0-2 三级缓存是一样的。也是硬件集成在 CPU 中。是对 MMU 页表运算的高速缓存。 TLB 的 L0 缓存,分为指令地址缓存和数据地址缓存。L1-2 级缓存,就是全地址缓存了。 基本和 CPU 对内存的 L0-2 高速缓存一模一样。脏数据也同样需要被标记和写回。 这一块就去看锁 - 共享数据安全指↑就可以了。 0x04 实模式、保护模式、长模式的开启 对于目前所有市场上的 Inter 的 CPU,不管 32 位还是 64 位,实模式都是一直存在的。在给 CPU 加电的时候,就是实模式。开启电脑后进入的 BIOS 系统,就是实模式。 保护模式和长模式都是需要主动开启的。这个操作系统帮我们做了。 其中,如果要开启长模式,必须要先开启保护模式。所以实模式、保护模式、长模式,每一次开机过程中,都会体验一次。这是操作系统都帮我们做了。 在开启保护模式和长模式之前,都需要配置全局段符号表,这个也是操作系统帮我们做了。 如果要开启保护模式的平坦模式和长模式,还需要配置成分页模式,这个也是操作系统帮我们做了, 0x05 加餐 - 文章开头的问题 3 问题 3: 16 位 CPU 是如何操作 1M 内存空间的 (2^16=64kb)?32 位 CPU 是如何操作 64G 内存空间的 (2^32=4G)?他们的原理一样吗? 答: 在 0x01 内存蛮荒时代 - 分段章节,已经说了 16 位 CPU 是如何操作 1M 内存空间的。主要依靠 20 根地址总线。所以这 1M 的内存空间,都是实打实的可以访问的。逻辑地址完全能定位所有内存地址。 32 位 CPU 的地址总线也是 32 根,但是对于特殊场景如大型研究院等,32 位 CPU 仅支持 4G 内存可能不够用。所以 Windows 系统自身做了特殊处理,以使得 32 位 CPU 也可能使用超过 4G 的内存。 微软的这个技术,叫 PAE。 其实和 16 位支持 1M 内存空间大同小异,也是扩展了地址总线。从 32 根扩展到了 36 根。最大寻址从 4G 扩展到了 64G。 但不再通过逻辑地址的分段模型,而是在分页模型的基础上调整页表实现的。 from wiki: 在保护模式开启平坦模式章节,我们说的二级页表中,有一个 CR3 寄存器。这里的 PAE 技术,就是将 CR3 寄存器本身不再存放页目录的地址,而是指向了页目录指针表。相当于向上又增加了一个维度(增加一级)。 所以对于 16 位 CPU 操作 1M 内存空间,和 32 位 CPU 操作 64G 内存空间,都需要扩展地址总线。因为不扩展地址总线,就无法表达更大的内存地址。 扩展了地址总线宽度后,16 位 CPU 基于分段模型给了解决方案。32 位 CPU 基于分页模型给了解决方案。

2022/9/24
articleCard.readMore

有计划的努力

又一年中秋过去了,时间过的真快!中秋节的几天假,我是算着日子过的。准确来说,近几个月的节假日,我都是计算着过去了几个小时又过去了几天。我艰难的珍惜着时间,算计着时间的流逝,心里压力很大,劳累无穷。 因为有件事情需要落地,只有节假日这些非工作的时间才能够静下心来忙碌。最近又拖延症犯了,心里总想着那件没落地的事情,大山越压越重,就非常疲惫。 这次的中长期计划和预期差距较大,我也做了复盘,发现有很多需要注意的点值得记录。可以为大大小小的耗时耗力又重要的计划摆正方向。 健康 / 心情 / 拖延症 / 拥抱变化 / 分轻重 / 专心做好一件事 / 拆解计划 / 被计划推着前进 健康 健康非常重要,感觉身体跟不上了,那锻炼的时间就是立刻马上,就不要管那些还没有完成的事项了。 因为身体非常重要,而锻炼事项通常耗时较短,这就是第一优先级。那些未完成的事项可能动不动就是几小时或者几天,相比来说可以拖一拖。 睡眠时间,一定要分配好。靠脑子吃饭的人,不要吝啬自己的睡眠时间。这里不是说不要熬夜晚睡,是要保障睡眠时间。 睡眠不足大脑不在状态,绝对非常耽误事。思考能力下降会带来各种问题,扣着点时间不值得。 心情 心情很重要,不要为无足轻重的小事乱了心境。也别把自己的坏情绪带给亲人,那样只会越来越糟。 当自己的心情不好或者家庭的整体气氛都比较压抑的时候,判断能力跳崖式下降,很多鸡毛蒜皮的小事都会变成大问题,而且还会波及到其他的方方面面。事后来看,非常不值得,极为浪费时间。 当心情不好的时候,就想一个问题:目前状态能不能解决问题?基本上心情不好是解决不了任何问题的,因为愤怒和问题之间没有啥联系,纯属自嗨。放松有很多种途径,这种途径的代价有些大,有时间、金钱、亲情甚至更多。 拖延症 有件事必须要做,就尽快做,越拖代价越大。时间一定可以压出来,但拖下去的危害复利相当大。 如果是短时间就能做的,那就尽快找时间来做。如果耗时比较长,那就写到计划列表里面。 拥抱变化 变化在所难免,拥抱变化,而不是抵触,因为变化一定会发生。 在一个中长期计划的落地过程中,不要想着顺顺利利一路顺风,那是不可能的。不要畏惧变化,它一定会到来,但也一定要解决,这是中长期计划里面的一环,该有的就不会少。 当然对于可提前预知或者感知到的变化,也要及时做好准备。这可以提高应对已经变化的命中率。迎接变化总归要好过变化突然上门。 分轻重 除了当前主线任务,当然会有各种不相关的细索事项比如缴电费修理自行车啥的。要理清细索事项和当前主线任务的占比。如果细索事项评估没有价值,要及时止损,不要去做。 不要把时间浪费在无聊的事项上,感动自己没有意义。 专心做好一件事 专心做一件事,不要分心。计划是可以并行的,可以一天之内完成多个计划的子事项,但不要一个小时内分摊经历到多个子事项中。做一个事情的时候,就要足够的用心,别浪费了这一刻的大脑周期。 拆解计划 每一个中长期的大计划,都要有短期的目标。计划拆解很重要,可以将计划的价值、时间、难度、收益都直观的体现出来。无法拆解的中长期计划,就不值得去做,没有价值。 明确制定短期计划列表,让走的每一步路都留下脚印。这样小的子事项能够按期完成,大的计划不再是空中楼阁。 被计划推着前进 不要没有计划事项了。那个代办笔记里面,中长期计划空白的周期不要太长。 应该让代办计划推着我们往前走,而不是浑噩的度过一个月又一个月。 代办计划应该是深思熟虑的,不要做没有价值的不值得的事情。每一个计划的落地应该会带来持久的成就感,而不是自嗨,单纯的自己感动没有意义。 人生啊,还是走的太快,经不起回首。 能够精确的度量掌控会比较放心,这样可以明白去年、今年、明年自己做了啥。付出了什么、得到了什么、又产生了什么价值。 这个精确度量的刻度,如果以季或者年来计算,找不到价值点。如果以天来计算又显得繁重和不现实。中长期的计划感觉刚刚好,一个月到半年,平摊了时间成本,也能抹平那些没有价值或者价值小的事项。 别瞎努力,人生虽长,但禁不起几次重大的折腾。而这几次重大的折腾,可能就在中长期代办计划里。毕竟几个有连贯性的中长期计划,连起来很可能就对人生起了决定性的作用。 说不要虚度了一生,这样的空谈大话真没有意义。 每个人需要的东西都是不同的,到了不同的年龄、阶段、环境后,需要的东西才都会改变。 所以怎么才算不虚度一生?这除了画了个大饼,什么都没有。 每个人都是自己人生的王,觉醒了才知道自己在某个阶段需要什么,奋斗着去争取什么。 需要知识,会有很多种途径去获取知识,但前提是我们得知道我们需要知识。 细节都会有解决的办法,但大的方向往往很难被察觉,每个人需要的大方向都是只有自己才知道,就是至亲也帮不了。 多思考一些,多过滤一些自己的需求项,抓住那些对自己举足轻重的,剩下的就交给自己和时间去落地吧。

2022/9/13
articleCard.readMore

随时间的变化

时间,永远不会成为你的朋友。它会从前面拉着你,从后面推着你,有时候温文尔雅的放任你像个君子,有时候拿着皮鞭抽打下来像个魔鬼,最后把你拖到年老与色暮,拖到离别和死亡。在生命的最后一刻,有不甘,有妥协,但是时间何曾念及你是它的盟友或者朋友? 我最近几年,真的是怕了时间了。也不是贪生怕死之类的情绪,真遇到突发性的死亡谁都得尿下裤子。更多的是纯粹的对时间的恐惧。 时间它有时候像个女人一样慢慢推着我,有时候像个男人一样粗鲁的拉着我,它一直在逼着我做点什么。当我停下来单纯就在休息的时候,我就感觉到它在我面前盯着我,让我做点什么。 你还记得失恋后,你是怎么释怀的吗?即使当时多么的撕心裂肺。以及很严重的手术后,身体怎么恢复的吗?失恋和手术,都不会一两天好转,可就是不知道什么时候开始,就恢复了。 这些年,发生了很多意想不到的变化。比如之前上厕所一定要用蹲坑,现在也习惯马桶了。还有进屋穿鞋,之前我是想着以后有房子了,进屋也要穿鞋才不硌脚,现在是自觉的脱鞋进屋了。还有游戏,我几波起伏,从小学痛恨到中学迷恋到后来的无感,现在又给孩子买了 Switch。对游戏有很多次认知上的翻转。 总体下来,自以为的底线被不断翻篇。不知道这些年发生了什么,可就是实打实的变化着。变化的方向说不上好也说不上坏,谁知道过几年会不会再次变化呢。 曾经渴望着长大,长大了就可以依靠自己做很多的事情了。现在我每每看到孩子,就希望能像她一样的无顾虑,这简直是天大的奢望。可是孩子也想长大,也在想象着成年后的自由和洒脱。你看,这不就是人生路上的一个抉择吗?只是这个抉择,没得选,它一定会到来,时间,会不快不慢的推着孩子走到这个时间节点。 再长大一点,有了恋爱。我还记得和初恋分开的时候,我不知道下一步怎么办了。那段时间,举足无措,不知西东。那份痛苦,很多很多人都懂。后来呢,我结婚、生子、工作,这些成长曲也都没有拉下。时间是最厉害的魔鬼,在它的拉扯下,有些事情的确被盖棺论定,更多的是无疾而终。谁也改变不了什么,而那些决心扭曲上个节点的人,不是伟人就是罪犯。 再后来,工作和生活忙碌起来,开始在迷茫中寻找出路,迷茫中夹带着后悔和不甘。谁的青春不迷茫?挺好。只有迷茫过,才体会到做刻骨铭心的选择时候的纠结。没有过大是大非的选择,真谈不上人生这条路是自己主动走的,还是被动走的。只要目标是明确清晰的,在这些人生的拐点所耗费的精力也就不是没有意义。 青年到成年,也明白一些道理。那些还没有拥有的,不必奢望,和你本来也没有什么关系。你所后悔的人和物,也是在某个岔路口做的选择。没有什么迁就或者指责,还是得用一种坦然的心态去承受、品味。压抑情绪解决不了问题,释放也不会有很好的途径,身体或者心里上的痛苦,还是需要去承受磨练的。这些必要的磨练,躲不掉的,无需刻意绕路走,时间会带着你面对它,也会带着你离开它。 时间就是这样的推着拉着拖拽着每个人,选择了就会翻篇,过去了就回不去。在那些没有意义和结果的混沌世界里蹉跎着,只会在下一个时间点,继续回望着这一刻的迷茫。这些循环,当然可以乐在其中,用上帝视角预览上一个时间点的多个岔路口的发展,是挺有趣的。 我很佩服理想中的人,他们是精明的猎手,狩猎着自己的人生拐点。他们开开心心的,把每一步都走的那么漂亮。他们不后悔,无遗憾。他们与人为善,和睦相处。 我成不了他们,我只能做到在迷茫的低落期,尽快的往前走。走的快一点,离那个拐点就远了一些。远的看不见了,我就当作放下了,这是时间教会我的。 时间教了我很多东西,它不断的让我接受变化,也拥抱变化。绝大部分想法或者习惯以及自以为的底线,都会在时间长河里面有波动,更准确的说是趋向完整。那些被动变化的,都在填充不完整时候的沟沟壑壑。 时间最大的问题其实是小气,它只分出来一甲子多的时间来做我们的护道人,过时不候。它不断的提醒我催促我,快一些,再快一些。那些还没排上档的事项,请尽快安排吧。 即使大家还有很多事情想做,没走的路没体验的风情,可时间,真的不多了。 丁香园被封,是科普方向的终结。科普平台不多,打杀了一两个就能够杀鸡儆猴,不像教培行业,要大刀阔斧。 随着互联网飞起,好不容易起来的科普平台也一个接一个的被关闭,松鼠会、大象公会、回形针。我前两天取关的差评君,也离科普的道路走远了。现在想来,差评君也无可厚非。 讲科学讲道理,以后会越来越难。因为大家都不知道什么是科学,什么是道理了。 没有科学基础做支撑,下一代、下下一代会觉得一个飞机模型的起飞都是神仙吹了口仙气。不知道怎么起飞的,只能归于神仙。至于为什么不是精怪,因为开国后就不允许鬼怪成精了。 还有一个更可怕的点,至今没有人知道丁香园为什么被封。内部肯定知道,可是外部就都摸瞎了。 不知道怎么没的,这就是不讲法律。国人本来就对法律意识淡泊,依法治国这条路,形同虚设。法律人也在苟延残喘。

2022/8/14
articleCard.readMore

内卷

最近我的职场上发生了很多事,想痛快的表达出来。鉴于时间真的不多,就算草草的一篇水文当作备忘吧。 大老板这两年换了几波,越高层的变动对我们这些底层的影响越小。这次换的是我大部门的老板,这影响就有些大了。最近这两天邮件时不时的在夜里 20 - 24 点之间收到月报提醒,原来只有小部门老板才发月报,现在是 team leader 也需要发了,有些团队看样子是全员发。我自然不反对月报这种总结的形式,我自身每周都有 2-3 次的日回顾。但是这种大部门内部上百人公晒的形式,就有些上纲上线的形式了。这已经变成了一种形式,虽然大家月报的内容没有变化,但性质发生了变化,更多的是一种对高层的委屈妥协。虽然变化总所难免,但很多不舒服的变化一起扑面而来,就让人心情很沉重了。 这都到年中了,公司的毕业还在继续,火还在继续烧。这两天轮到我这边了,团队内部做业务分工的调整,我被划到了另一个团队。后面我和老板说了自己的想法,老板也很尊重我,同意我横向保留现有的工作内容,过了两天说维持我的组织架构不做调整,我也避免了换岗。我接触了两天另外团队的老板,发现管理方式还是有些大的,PUA、高度卷都有发生。工作群里鼓励周末上线需求的同学说是战斗力、运营配置的资源文件要识别有没有 cdn 加速说这是基操等等。联系到最近上班不断提前,下班不断推迟,开始注重工时代码量,以及部门渲染出来的压抑气氛,这肯定有一场大风要吹来啊,只是会有人起,也会有人落。 我是没想到这么成熟的公司,还会有这些不成熟的波动发生。我理解最近很多变动,应该在不同的团队和 BU 这么多年的经验中被有效验证过,这已经能产出非常有效的方法论,可取或者不可取。 如果这么多年这些方法论被不断验证的话,那么可取的话不会等到现在做变动,不可取的话为什么要现在做变动呢? 所以,表面上的成熟,其实并没有成熟。每个部门也在摸石头过河,等着某位大老板看着心情的空降。 这是人才的管理,这么的不成熟,真不应该。 我认为规则一定要有,而这些规则在有效验证后就应该从始而终,除非重大变革。大家随时都能够明了,什么事能做什么事不能做,而不应该朝令夕改,那这样哪能让人有一个良好的工作和发展环境? 比如现在的月报和周报,就是被无数同学和部门验证过的,为什么现在突然要大范围的搞?是以往的历史沉淀数据丢失了,现在要再补一份? 职场上工作是挺累的,因为工作内容的本身只是职场工作的一部分,还有一部分是勾心斗角。一将功成万骨枯,那么将军只有寥寥几个,大家都没有希望。可大家都会成为那片枯骨,都变成了代价,变成了职场、社会的代价。而最后,将军也会成为大将军的代价,只要时间足够长。大家都避不开这场游戏的轮回。 最怕有权的人做重大变革,重大变革一定是牺牲很多的前提下才能达成的。循序渐进是发展的好姿态,暴发富会失去和得到的一样快。那些决心重大变革的极端人士,非常可怕。不管最后变成是往一个好的极端或者另一个坏的极端走,变革的权利都将使得很多牺牲毫无意义,但这些牺牲对于个体而言都是一座山。但从 “个人应该服务于群体” 这样高层的觉悟来说,舍小保大也是一种美德吧。 只是我并不看好这种美德,如果我变成了那个牺牲,那我肯定不愿意扛下这座山。如果我不是那个牺牲,我也不想看着别人扛下。可重大变革总会有很多座大山落下,我不希望看到有人去抗,谁抗都得脱层皮。 现在的我,经过最近的几波变动,也想去看看新的市场。当前城市并没有我喜欢的岗位,我可能要离开家人去其他城市寻找机会。我也是一场代价,一半因为目前的工作环境,一半因为自己的想法。整体来说我是家庭往前发展的代价,只是还算循序渐进,承受下来问题不大。 想要更好的生活,终究避免不了内卷。我不反对内卷,也不反对躺平。人各有志,人各不同。这个社会就应该有足够的包容性,而不是一味的奋斗以实现伟大复兴。职场也应该更好的分摊机会给予做了不同准备的人。发展一定是渐进的,崇尚奋斗内卷和躺平度日都不可取,应该把这份选择的权利给到真实做出选择的人,而不是通过大环境来做高维度的降维逼迫。 谁能想到,未来五年,也会变成敏感词呢?汉字博大精深。

2022/6/30
articleCard.readMore

不确定性

最近偶然看到人生的痛苦在于追求确定性这句话,感应非常强烈。我一直想用一个词或者一个简短的话,来描述和表达我两年来观察到的一个现象,可一直找不到,这下找到了,也就记录下来。我认为不确定性这个词,将影响我的余生。 几个月前的一个夜晚,我深度思考过这个问题,就是当发生了一些不在预期内的事情的时候,不同人的处理态度是完全不一样的。 当不可预期的事情发生后,需要做决策,有人是立刻选一个,效率非常高,不管利弊都依照这个选择继续走下去。有人呢,是立刻停下来,花费很长的时间开始思考该怎么办,在经过长时间的权衡利弊之后痛苦的做一个选择,还有的人就不知所措,因为事发突然就破罐子破摔的宣泄郁闷脾气,然后故意把这件事往后面拖,拖到不能拖的时候再处理。 已经规划好的事情,做到一半,突然就面临抉择,这是很让人不爽的。就像旅游的路线图设计了好几天,真在旅游的时候,发现旅途中计划乘坐的公交车临时取消运营了。类似这样的抉择,每时每刻都在面临。 我之所以深度思考这个现象,是这个现象给我带来了实打实的苦恼。前面那些年,我感觉挺顺风顺水的,因为学习生涯没啥大的波动,工作刚开始也没啥大的抉择产生,自己也没有啥理想抱负。但最近几年,我苦恼的厉害,因为每天都有很多不可预期的事情发生,工作和生活上都有,这让我疲惫不堪。 从一个没有那么波动性的人生切到满是波动性的人生,一下子让我适应不过来。我没有意识到这是怎么回事,只是一味的感觉到变化,也试着去适应变化。可当我发现计划经常没法按需完成,经常做到一半就跑出来一个产生阻断性的幺蛾子,这样的事每时每刻不在发生,我觉得是时候坦然面对这些生活中的不确定性了。我需要坦然的面对这些幺蛾子,而不被这些幺蛾子牵着鼻子走。 人各不同,那些喜欢刺激和颠簸的人肯定很多,但我想绝大部分的人还是喜欢确定性的人生的。比如明天的工作有哪些可以落地,明天的家庭外出路线图规划,下个月的薪资和支出分别是多少等等,这些事项如果有一张表,你只要等着时间到,表里的事项就可以实现并划除,这就类似确定性的人生。在确定性的世界里,我们可以规划好短期的各种代办,并且没有顾虑的付出时间或者体力后,就可以等着各项代办完成。 但是呢,偏偏事与愿违,这些确定性的规划偏偏不是人生的主旋律,甚至明天的计划也有可能因为不可预期而被打断。今晚孩子的突然发烧,就会使得明天的家庭外出计划终止。不如意事常八九,不确定性,才是需要直面接受的挑战。 人生的痛苦在于追求确定性,刚听到这句话的时候,我是想当震撼的。我们是自己人生的王,我们也都在认认真真的规划着自己的人生。 学习中,我们期待每天花费多少时间在哪些科目上,以期待整体成绩的提高。工作上,我们努力做好手里的项目,以期待年底的奖金。我们还会盘算着年薪,算着什么时候可以买房买车,还有孩子下个学期是否要学习游泳或者篮球。 如果学习的时间付出了,成绩没有提高呢? 工作努力了没有奖金怎么办? 突然失业了经济来源断了那房和车还考虑吗? 我们都在规划,规划那个确定性的结果,这是人的本性,我们都期待并且认为付出后会得到预期的收获。 那如果事与愿违呢?事与愿违的概率,不算小吧。我们不能把头埋到沙子里,故意避开那些非常有可能出现的非预期结果。 我们应该看清生活本来的样子,很多很多事项本来就不是确定性的。如果打满了鸡血以期待设定的目标,目标达不成,那能不失望么,能不痛苦么。 对于这些违背预定计划产生的幺蛾子,我是被它牵着鼻子走了好久,很痛苦。 单从工作上来说,10 天前制定了方案,今天实施发现有问题跨不过去,就拉一群同事讨论改变方案。 认认真真的改了方案,认为一定不会有问题了。又实施了一天,发现又产生阻断了,这次不需要拉同事讨论了,但需要额外增加自己 N 小时的工作量。 10 天前制定方案的时候,已经拉着同事仔细思考把可能性的风险都过了一遍,但真想不到后面这么多幺蛾子。这种不确定性的幺蛾子呢,发生一次,崩溃一次,然后又发生一次又崩溃一次。 直到我最后发现,不管工作还是生活,这些幺蛾子都一直存在,我不能继续被牵着鼻子走了,一个人经常心情崩溃那能健康生活么,肯定会有心理疾病产生的。 于是我就调整了心态,告诉自己不如意事常八九,平常心面对。不要那么多抱怨,因为那些幺蛾子注定会发生,遇到了就解决这样子。 现在呢,既然有了不确定性这样精确的词来对这种幺蛾子做了定性,那我也想提出一些体系化的方案来面对,而不仅仅是心态的调整。 首先呢,还是心态要端正。 对风险做了全面评估的规划,还是有可能出现问题。不要急躁不要慌,因为这些问题注定会发生。这些问题不再是幺蛾子,而是规划里的未知部分,它一定在,只是不可预期。所以我们要坦然面对。 其次,每个未知幺蛾子出现了,都要花一些时间来分析和思考,尽可能的抉择有利的方向。 这个很重要,问题都会有,但如何解决问题很重要。一定不能头脑发热扔骰子做抉择,而是要系统性的分析。 然后,尽快做决定,不放过,少摇摆。 要立刻做抉择不能拖。很多人害怕做抉择,总是想把问题往后拖,拖到不能拖了再处理。拖延会持续加重心理负担,因为刻意的拖延,会使得这个问题一直放在心上,心理压力会很大。 最后,身体要健康。 这个建议有些意外,但相当受用。不确定性存在人生的各个方面,很多大的事项,还是相当熬心费神的,心态上端正了,身体也可能吃不消。那些需要奔波、熬夜来处理的幺蛾子,还是需要一个良好的身体做支撑的。 以后呢,我打算按照这四个流程来体系化的调整自己,正确的面对人生中的不确定性。 我已经做到了前面两个,不拖延和身体健康,我还欠缺,尤其身体健康。常年抽烟不锻炼对我的身体影响非常大,我很清楚,这个我也要改变。 这里,我想起了一部小说,很有借鉴意义。 就像三体小说里面的三体人一样,因为三体人的星球被三个外部星球拉扯,呈现出完全没有规律性的自然现象,比如日出日落。他们无时无刻不在担心明天的太阳还会不会升起,无时无刻不在担心寒冷和酷热在哪一刻会到来并持续几年甚至几十年。 在不确定性中,三体人甚至有了脱水的技能,脱水后,他们只剩下一层皮,像衣服一样被集中存储,几十年的周期里,很可能被老鼠这些生物咬坏,泡水后就会出现缺胳膊断腿的现象。 三体人就是在这样的环境下发展出了高级文明,比地球文明早早就制造出了光速飞船以及高级人工智能的水滴和智子。三体人世世代代生活在不确定性中,而他们所有的梦想,就是希望能生活在确定性的自然环境中,有周期性的日落日出和春夏秋冬,地球,是他们所有希望的终极载体。三体共计三部,都是围绕着三体文明和地球文明之间的明争暗斗展开的。主线就是三体人希望占领地球,在地球上生活。 最后的结局呢,三体和地球文明都毁了,三体人很坦然,智子和地球人友好道别,这是让我出乎意料的。我觉得作者刘慈欣把不确定性对一个民族的影响刻画的很深刻,因为三体人已经在不确定性中生活了这么久,他们虽然渴望确定性,但其实他们知道确定性也是暂时的。他们不是渴望在确定性中长存,而是希望在确定性中更快的发展,以面对更多的不确定性。 三体人对这个多维度的宇宙了解的更多,黑暗森林法则三体人早就烂熟于心了。而这些实打实的危险,地球人也知道,但不重视。就像书本上的公式,知道但没有实际应用。没有经历就理解的不深刻。而三体人就无时无刻不在为避开这些风险努力着。 不确定性才是人生的主旋律。人的一个痛苦就是不断的想寻找一个确定性的点,来使得自己不再担惊受怕,不再被外部环境所压制。 比如财富自由,有多少资金才算财富自由呢? 这样一个确定的点,是何其难找。人本身处于波荡的历史长河中,想要寻找的确定的点更是波动的。这样看来,确定的点,还存在吗? 经历的更多,也会更加的从容。知道没有什么东西会长久,也愈发的珍惜当前、规划未来、步步为营。固步自封是一个人和一个民族的囚笼,正确面对不确定性才使得车轮不断向前。 最近有一片文章,说只有胡锡进还在关心经济,马化腾还转到了朋友圈以示赞同。 我日了天下哦,胡锡进这个狗,那是真的狗。 只有胡锡进关心经济?我不关心吗?我就稍微关心了一下,微信公众号就被封了。我还关心了,我说即使封控如此,中国的 GDP 还是会保持增长的。 我是谁?微不足道的小小小人。其他关心的人呢?那么多举足轻重的人物都在关心经济,是你们瞎眼了看不见?哦,你们把他们的号都封杀了,然后说没有人关心经济。 像胡锡进这些没有底线的舆论作恶的人,我找不到什么华丽的词来进行批评,只想说:是真的狗。 但是话又说回来,没有胡锡进,也还有其他人在同样的位置上,和胡锡进说着同样的话做着同样的事。 现在胡锡进也退休了,或许一段时间后,有人开始怀念起胡锡进了,怀念胡锡进在位的时光,还是挺亮堂的。

2022/5/29
articleCard.readMore

数学可以保障逻辑自洽

昨天,看一个知识点的时候,看到了概率论知识。发现概率的符号不认识了。就网上想着查一下,不查不要紧,一查发现没有认识的符号了。当年初中高中也都学习过的知识,工作这些年忘的杂七杂八了。抱着复习一下的态度,断断续续的看了近 8 个小时,才捡回来一些。 这 8 个小时,我把初中、高中的人教版数学教材都下载了,跟着目录找自己需要的知识点。看目录的时候,没有有印象的记忆了。啥 n 元 n 次方程式和函数这些基本的概念都忘记了。更别说开方、指数、对数、导数这些运算了,还有那些解析几何,看的云里雾里的。不得不说,真不知道那些年都是怎么过来的。 数学这门课是非常重要的,我认为应该放在学生的 Top1 位置。现在很多人的脑子像浆糊一样,连基本的认知都没有,就是因为数学不好。基本见到这样的人,大概率判断数学成渣。这样的杠精,思维已经固化,只能躲开。 因为数学和逻辑思维能力是正相比强相关的,数学学深肯定难,但学会并不难。 只要有初步的逻辑思维能力,能够根据已有的基础来推导现有的知识,那么逻辑能力就满足了。比如导数和曲线面积,能够在求曲线面积的过程中,对导数和原函数做公式的代入,理解导数可以解决不规则面积问题,这就是逻辑推导能力。再比如 n 元 n 次方程式的解法,可以通过公式笔算和画坐标图求解来做,就能做到思维活跃了。 学生的求学时代,是一种习惯的培养过程。我认为每个大人都应该了解这一点,这样可以更好的看待孩子的学习历程。工作很多年后回头来看,那些物理、化学、数学等知识,并没有实际用于工作或者生活中,基本都还给老师了。那学那些东西做什么用?依靠高学历有一个好的未来是一个方面,但高学历的也就那么些人,更多人只是进入普通大学。我认为学习的过程是培养一种习惯。数学是逻辑思维的能力、物理是实践的探知、英语是不同语感的培养和新事物新世界的认知。 当然,这里我也 battle 一下政治和历史,本来呢,它们是非常非常非常重要的课程,让孩子理解什么是法律、政府、职责、权力、历史、人文等等信息,这些信息是数学这些理论学科无法给予的。这些信息对孩子的人生观、价值观、世界观等等,都有举足轻重的作用。本来呢,它很重要。可是呢,这两门学科做成了脑残的培养皿。不愿细说,言此为止吧。 虽然很多东西即使学会了也很快会忘记,但都是埋下的一颗种子,有些需要很长久才能发芽,而数学培养逻辑分辨能力,则从始而终的在生长,即使高中或者大学毕业后停止了学习,种子也已经发芽足够撑起一个中年人余生的逻辑能力。在解题过程中,学生需要多角度多思路的思考一个问题的解法,并每日每日的重复训练,使得学生对于逻辑感非常敏感并且有很深的烙印。 逻辑分辨能力就很重要了。有特定的 “逻辑学” 学科进行研究,不过这个研究的就深入一些了。而我们在日常生活和工作中,会时常遇到需要逻辑分辨的事情,比如买了过度打折的产品又抱怨产品不够好,那产品为什么要过度打折卖给你?既然买了心里就要有预期。再比如歌星犯罪了然后把歌星所有的歌都下架了,那么歌星犯的罪和歌星唱的歌之间,有什么必然联系吗?诛九族的行为方式并不可取。还有工作上产品经理经常要遇到的业务逻辑闭环问题。如果产品经理设定的业务场景都没法闭环,让用户的行为操作进入一个死胡同,那么就会被用户骂了,现在的互联网产品,很多让用户骂爹的设计。类似这样的逻辑不自洽的生活常识,但凡有一点逻辑分辨能力,就可以很快的识别并避免走入误区。 还记得高中数学老师讲微积分的历史,说到莱布尼兹的悲惨晚年。说到莱布尼兹和牛顿争夺微积分的归属头衔,因为身份和影响力差别太大,败于牛顿。当时老师说,莱布尼兹死的和王熙凤差不多,一卷凉席就埋了的那种。我这次查看了莱布尼兹的历史,挺震撼的。各种头衔啊,而且远远不拘束于数学圈,比如基础物理等都非常出名,本身人家还是律师出生。像莱布尼兹这样多才多艺的人还非常多,查看名人历史,感觉每个因为公式或者发明而名流千史的那些时代伟人,都不局限于莫个圈子,而是在数学、物理、化学、人物、地理、历史等都有卓出的贡献。这里我并没有含沙射影什么,只能说佩服这些伟人。毕竟,这些伟人的其中一项成就,在国内就是顶级教授头衔,各种采访、出书,吃老本,能吃好几代。 这两天复习了很多数学知识,我工作在 IT 行业,很多东西还是挺有用的。最初其实是因为想看看概率论的知识,结果陷进来这么多。 疫情刚开始的时候,听一位老者说,人生理解了概率,会好过很多。当时的阐述是疫情、车祸、不治之症等客观因素导致死亡的概率,疫情应该注意到什么程度。如果疫情死亡率小于车祸死亡率,那么大家不还是需要上街,也就没有必要过度担心疫情这样子的阐述。 概率论我后面还是要学习的,它真的很有意义。但是概率论是大学知识,我看了一些,嗯,脑子有些懵。 除了逻辑思维能力,还有一个能力非常重要,就是动手能力。这个能力在应试教育中无法培养,物理学科或许可以培养一些,但远远不够。动手能力是科技进步的必要条件。 折纸飞机、修闹钟、换电脑设备、拆解风扇这些都是动手能力,手动制作无人机、高精尖仪器的组合和制作、高科技产品的创新和研发,这些更是动手能力。只有有了动手能力,才能够不断的创新和突破,才能够有兴趣学习理论基础并运用于实践。有个很有趣的现象,就是国人组装一个电脑那是相当的吃力,普通人制作一个滑翔机要经历许多许多许多次失败最后交付一个半成品,录个视频就很多人羡慕说真厉害。而在一些限制访问的平台上,比如油管上,很多很多这样的视频,人家一次两次就成功了。原因是什么呢?就是国人动手能力差是一个方面,更重要的一点是没有基础理论知识做基础,没有深入学习的能力。滑翔机的物理理论基础、电机如何使用、机翼角度如何偏移等等,都要一步一步的实验出来,那失败次数能不多么?国内互联网这么发达,从业人员知道了解熟悉计算机底层原理的有多少?大家都在上层搭业务讨生活,鲜有深入底层做研究的。这样怎么能创新? 这里呢,我就很佩服一个人,“耿哥 “,又叫 “手工耿”,很多人都知道他,我就想当佩服他。大家都能看出来耿哥学历不高,其实耿哥 18 岁就外出打工了,教育上完全谈不上深入。可是耿哥的创作能力,不能说强悍,但一定可以说稀缺。耿哥出道的四五年里,没有人出来分他的羹。 如果耿哥这样的人加上高学历,能够把知识也运用到创作中,科技进步一定是飞快的。现在国内只能用国外技术,却不能独立制造,不管是科技公司还是机械公司。阿里巴巴、腾讯这些科技公司的确带动了国内互联网的高速发展,比日本、英国这些发达国家都要发达,但没有核心技术。这些大公司没有科技创新,都是在使用国外已经成型的技术来发展拓宽自己的业务,比如数据库、编程语言、芯片、工具软件等。学习能力很强,创新能力为零。 虽然教育没啥改革,但课标也是十年更新一次。这次我也有发现,我们上学时候都没有啥 A B 版教材,数学也没有选修一说。这次找高中教材,着实混乱了一把。而且,能感觉到内容增多了,不少当年我们没有接触过的知识。 看 2022 年的新课标,把英语的重要性降低到体育的后面,道德与法制 (政治) 做了提高,艺术和体育齐平,历史和地理压箱底, 鄙人眼拙思想觉悟低,和我的认知有些出入。 我倒是觉得英语还应该排为主科,虽然翻译软件可以很好的代替人工翻译,但是良好的外语基础才能更好的认识外部世界。没有良好的口语,也不能和外国人沟通。我现在就是英语不好,而我又非常迫切希望自己英语好。不到某个阶段,不会知道英语好是多么的重要。而那些说翻译软件一样能解决问题的人,永远不会理解。 道德与法制,那就是政治的升级版嘛。占比 6% - 8% 的课时,英语也是这个占比,看样子要增强洗脑的深度和广度了。政治是虚假,法律是摆设,不知道要这么多课时干嘛。 历史和地理共计占比 3%,我一半不赞同吧。历史本身非常重要,历史可以为鉴,不知晓历史是要毁国的。但因为历史书都假这么多年了,我宁愿它占比 0%。地理是孩子对世界的认知,应该放大比例。在只有少数人坐过飞机出过国的年代,还是让孩子们多多认识这个美丽的世界吧。 艺术占比 9% - 11%,艺术可以展开孩子的想象力、抽象能力、思维能力,还可以成为孩子一辈子的业余技能。我非常赞同。希望不是一天到晚的唱红歌,如果是,我就骂娘了。 语文占比依旧最高,20%-22% 的课时占比。不知道现在是不是还和我那时一样,写某句话表达了作者什么感情思想。虽然看起来好像是宽泛性的解答,但其实这是有唯一标准答案的,你敢想象…

2022/5/4
articleCard.readMore

这些年我的软件清单

2025.05.18 更新:Dia browser、Hopper Disassembler(分析二进制文件)、Obsidian、语雀(画流程图)、飞书 Lark(多维表格)、Longshot(截图)、EasyFind(快速搜索文件)、PopClip、Cursor、Zed、ChatWise、MacUpdater、QSpace 这里是工作中的软件清单,可以使得工作愉悦和提效。 还有一份生活清单可以点这里 终端命令工具 brew 神器,mac 必备。90% 的软件都靠它。 zsh & autojump & zsh-autosuggestions & zsh-syntax-highlighting vim & neovim & SpaceVim tree 快速查看目录和文件 TLDR to long donot reader,命令组合提示好工具 alias 巧妙的使用,会很方便。 开发工具 Vim (浏览器安装插件,浏览网页也非常棒) VSCode Xcode,写 C & C++ / 2022.10.08 更:配置 Vim 写中文文档非常棒 Iterm2 Surge 抓包 & 翻 X,神器 Kaleidoscope 文本 Diff,和 Git 配合非常好用 MachOView 分析 Mac/iOS ipa 包 Hopper Disassembler 分析二进制文件 / 2025.05.18 add SourceTree Cursor - 需要付费 / 2025.05.18 add Zed IDE,Vim 支持非常棒 / 2025.05.18 add 效率工作 Raycast 窗口管理、快速启动 app、Scripe Command 执行 Bob 文字 OCR + 翻译,配合 AI 翻译非常棒 Reeder rss 订阅 / 2023.02.12 更:已经支持 iCloud 同步,可以不用 feedly 了。 input Source Pro 根据 app 快速切换输入法。可以设置快捷键快速切指定输入法,对于超过两个输入法的人很有用。 / 2024.04.06 add Arc browser 非常好用,但需要适应。尤其它的 Pin 和 Split 功能,可以玩出很多花。 / 2024.04.06 add Dia browser / 2025.05.18 add Longshot 截图非常方便 - 需付费 / 2025.05.18 add EasyFind 快速搜索文件 / 2025.05.18 add PopClip / 2025.05.18 add ChatWise - 需要付费 / 2025.05.18 add MacUpdater - 需要付费 / 2025.05.18 add QSpace - 需要付费 / 2025.05.18 add Obsidian / 2025.05.18 add - 已经从 Notion 迁移到 Obsidian 语雀,画流程图非常棒 / 2025.05.18 add 飞书 Lark,多维表格非常棒 / 2025.05.18 add 写作阅读 PicGo + Github 图床 Kiddle 社交媒体 Telegram 日常生活 Apple 提醒 Reminders(久经考验,完全图一个完善的苹果生态。即使 Things 也非常好用,但还是放弃了) Apple 备忘录 Notes(简单记笔记还是非常好用的。) 双拼 极力推荐,详情 Apple 快捷指令 shortcuts 拼多多 - 百亿补贴 高铁管家 - 买火车票。抢票啥的非常方便,车次、余票,展示的非常清晰。 随手记:理财很方便。/ 2024.04.06 add,支持 mac 端,默认无广告。 记者:你在家打老婆孩子吗? 答:他们以前连饭都吃不饱,现在生活条件已经改善了很多。 记者:我是问你打过老婆孩子吗? 答:我们家的发展成就是全村人有目共睹的。 记者:我没问你那些,我就问你打老婆孩子了吗? 答:老刘家打老婆孩子你怎么不问? 记者:我问的是你,在家打老婆孩子吗? 答:我已经把不打老婆孩子写进了家规。 记者:那你按照家规去做了吗? 答:家规是我们家的内政,别人无权干涉。 记者:你到底打老婆孩子了吗? 答:我谴责打老婆孩子的行为! 记者:我就问你打老婆孩子了吗? 答:你提这种问题是不负责任的。你去过我们家吗?我邀请你去我家做客,亲眼看一看。 记者:我就问你打老婆孩子了没?? 答:我可以告诉你,最了解我们夫妻关系的人不是你,是我和我的家人。我有发言权,你没有。 记者:你到底打没打过老婆孩子??? 答:你的这个问题充满了对我家的偏见和不知道哪来的傲慢,我们家欢迎一切善意的建议,但是拒绝任何无端的指责!所以请你不要再有这样不负责任的提问! From https://www.zhihu.com/question/387656660/answer/2452557902

2022/5/1
articleCard.readMore

决定做一些改变

做决定还是应该慎重的。 有些人很果断,遇到事情会立刻做出选择,不犹豫。这本身是好事,可如果对选择过程不加以分析,那就是概率性问题嘛,选择的恰当性五五分。我老婆就是这样的人,所以我理解的很深刻。 每年的开年,朋友圈也都能看到一些 Flag,我认为能列出 3-4 个就已经相当不错了,何况 10+。时间是很可怕的妖魔,一年时间很快就过去了。不求多,但求精吧。 我对改变的总结是,人在每个年龄段,都会有不同的想法和认知。改变,应该是在自知的前提下作出的决定,千万不能贸然行动。改变不一定说朝着好的方向走,人各不同,已有的行为习惯不一定是坏的,只是说在当前的自我认知下,可以做出更符合当前的自身情况也会带来更大收益的改变。 鉴于我目前对自己的认知,想列一些 Flag,作为对自己中长期的约束。 考虑到一个月或者一年后,可能还会有一些改变。所以本文作为只新增不删除修改的迭代文,每次更新加上时间标记。 2022.05.01 更新于五一劳动节 新增改变 新闻概念 减少政治及社会内容的摄取和输出。 对社会现状也有了明确的认知,对权力的泛滥和法律的践踏有了底线的触碰。但是自身做不了任何改变,目前没有任何影响力以帮助他人做觉醒。必要的时候,还会让朋友越来越少。 决定还是暂停对这些让自身不开心的内容的接收。 我知道那些问题一直都在并且时时刻刻都在发生着,我时时刻刻都感觉到痛心。但是我不会主动的去获取了。往深处来说,这也是对我自己的保护。 这样还可以有效减少我的时间消耗,也省去了很多内心的不愉快。 时间观念 对 Remind app 生成强依赖,通过被动提醒及主动查看来保障时间片分割的有效性。 苹果手机自带的 Remind 软件,还是非常好用的,是我这些年筛选了很多提醒类 app 后保留下来的。后面会把更多的和时间有关系的事项,放在 Remind 中,好做到及时提醒。 我能够初步做到量化时间了,这些量化的事项也记录到了 Remind 中。后面要做到及时消化,不拖延的消化掉这些事项。这对我来说有些难度,但是一定要这样做。 社交 微信仅保留聊天、搜索、订阅号功能 微信的时间消耗大头 “朋友圈”,要做关闭处理。朋友圈里面基本没有有价值的信息,但是近乎无限 feeds 流的信息,非常消耗时间。后面做到一周翻看一次,防止有重要信息遗漏。 继续保持不动摇的 日常记录: * 工作上记录每日工作事项* 生活上记录每日见闻 碎碎念: * 对一些事情的想法,更新到碎碎念目录下 时间观念: * 量化时间,不做时间魔鬼的努力

2022/5/1
articleCard.readMore

低效率工作沟通

职场沟通和人际沟通有根本上的区别。 人际沟通是模糊的,比如你问对方年龄,可能得到 “90 后”、“比你大一些” 这些回复,那对方到底多大?你并不确切的想知道。 而工作沟通,大家都希望有精确的响应。比如几分几秒量跌了多少,对方的业务几月几号能够上线。大家希望对方能够提供一个可量化的数据。 很多工作难做,就是员工们总是把人际的沟通代入到了工作场景中,老油条的职场戏骨们尤其显著。一场对话下来,好像什么都说了,又好像什么都没说。 沟通问题,绝对是职场头疼 Top1 问题。很多时候,精力焦脆、时间不足、压力过大等等原因,都是没有有效的沟通导致的。肯定有一套完备的方法论用于有效沟通,这里我想提出的一个场景,就是有人故意破坏这套方法论,这些人门儿清,可邪恶了。 一个是啥都说,就是不说重点。 一个是啥都不说,让对方不停的挤牙膏。 还有一个是普适性较低的黑话。 拐弯系列 群聊 A:我需要在 xx 场景下调用 xx api,你们这边提供吗? 群聊 B:不能直接调用,你要理解这个设计。这是文档,你先看一下。(1) 群聊 A:我看了文档,目前判断下来,xx 场景下无法调用 xx api,各位知晓一下。 群聊 B:你这是哪里来的逻辑?文档里不是有说明吗?(2) 群聊 A B:沟通… 最后结论:xx 场景下是无法调用 xx api 的。 上面是非常常见的场景,这里的拐弯已经很简短了。着重说明下: 1 处: 1. 啥是不能 “直接” 调用? 2. 理解啥 “设计”?是为了安全还是逻辑聚合? 3. 给文档说明书干嘛?是有间接调用的方法还是为了说明为什么这样设计? 4. 直接回复能不能调用不就好了吗?为什么要整这么多幺儿子? 2 处: 1. 哪里来的逻辑?那肯定看了文档最后权衡下来的啊。 2. 文档里有说明,直接贴文档说明的 api 截图不就好了,干嘛扔整个文档让别人阅读? 3. 典型的 A 说不能调用后,B 开始甩锅了。 最后呢,的确是没法调用。作为 api 的设计和维护者,对于上下游的咨询,完全给不了有效的信息,还一味的作为绊脚石。 上面的场景,如果 api 不能调用就直接说。如果自己不清楚,那么需要自查,这是自身的工作。 拐弯系列是我最厌烦的,口水仗打的很没有意思。B 看是回答了 A 的问题,但实际上完全摸不着主脉络,云里雾里。有些同学的回答那可真是精彩,卧槽,就是几十个字的回复,你找不到缺陷,读起来顺畅无比,但是你找不到任何有价值的信息,真是操了狗的心情。 我想到一个解决方案,也就是上面 A 的方式,那就是把问题最糟糕的情况明确下来,这个情况一定是和对方直接相关的。这样对方就会来尽力的阐述或者说明,否则可以打几天的口水仗。 还有一种方式是 180 度的反着说。比如你问 xx api 可不可以调用,对方 balabala 一大堆。如果能调用,对方不会 balabala 一大堆的,这时候你就说 “那我理解下来是可以调用的。我这边 qps 有 1500,qps 你们那边能保障吗?”,马上谈话就会回到正轨上。 但这种方式有时候对自己很不利,但从文字信息上来看,很可能表现出自己的判断失误或者武断不思考等现象。anyway,这的确是能够很快终结问题的方式。 牙膏系列 群聊 A:@B,有个线上问题,帮忙排查一下。 .. 3 小时后 群聊 B:应该不是我这边问题,@C 看一下 .. 1 小时后 群聊 C:描述下问题 群聊 A:问题在描述一边,balabala .. 3 小时后 群聊 C:这个还是需要 B 看下,你让他查下链路 群聊 A:@B,看下这个问题 .. 第二天 群聊 B:用户 id 是多少? 群聊 A:xxxx .. 6 小时后 群聊 B:和客服小二说一声,建个工单我们这边排查 群聊 A:我就是从客服小二那边过来的 .. 未知 A 是怎么崩溃的我不知道,但我知道 A 恨死了 B 和 C。这样低效率的工作沟通,两天了,回到了原点。 我到现在依旧对公司的有些同学铭记在心,就是那种你找他,那是找不到人的。他找你,也不发消息,直接打电话。你想学他的操作,有问题也打电话给他,嘿,打不通! 这种人就是严重的职业道德缺失,见一次想打一次。你还指责不了对方,对方事后会说工作繁忙努力追进度等各种原因。如果问题向上闹大了,对方的 leader 还会为下属辩解,防止影响自己的 KPI。 这种场景的解决方案只有一个,就是发觉问题解决缓慢了,就把对方的老板立刻拉到群里,一起艾特一起 ding。 普适性低的黑话 我遇到这样的评审会议,发言人说某个 API 可以自然侧提供,产品侧说他们要增加反转实验,设计同学说要做一个无极滚动的效果。 一下子把我搞懵子。反转实验或者空跑实验,我倒是比较容易查,也很快找到了说明,但是自然侧是哪一侧?无极滚动是什么样的交互? 骂娘哦。 当然遇到不理解的黑话是可以提问的,但不是每个人都会在 10-20 人的会议上单独抛出这个问题,因为现场的其他同学没有疑问,好像大家都很理解这套方法论。如果就自己不知道,那么抛出这个问题,解释一遍就需要 10-20 人的时间消耗,这个时间成本是很高的。 普适性高容易理解的黑话,没问题,比如方法论、底层设计、抓手这些。大家都理解,或者根据文字意思能够快速明白他人的表达意图。但那些八股文,去死吧。 这些低效率的黑话,没办法,要么当场提出疑问,要么事后向其他同学咨询。上面的无极滚动,我问了一圈也没问出来答案,大家都是第一次听说。 有效的职场沟通 最优解:能见面的见面,带上需要的资料。 次优解:不能见面的语音 + 桌面共享。 最后解:文字说明,消息或者邮件。适当增加图解。 普适性:确定沟通的主题,别 TM 聊着聊着聊到地球文明处于零点几级了。 职场上,再小的工作也需要预留 1.5 倍的时间 Buffer。你完全不知道有谁在哪个角落使拌子。职场上是没法保全自身的,就尽量减少自身的消耗这样子。 但公司再大,也就那么大。所有的问题基本都是内部消耗,坏影响传不到外面的。So,可劲的掀起风浪吧,风险都是可控的。

2022/4/28
articleCard.readMore

要不要归零

2022.04.14 日,我的订阅号被官方永久封杀了。对我呢,没啥影响,毕竟 4 年了,就 54 个粉丝。我也都是直接发文字,不是文章,没啥原创不原创的概念。 我对这次封杀,是很不满的。说有人举报了我,查明后给予了封杀。 但其实并没有人看我发的文字,我看后台其实就一位读者断断续续的漂两眼我的号,其实这个本身并不好判断,后台不提供哪些用户看了什么文章。只是因为我的关注量太少了,后台有一个阅读排名,刚好就是这位顾客。他是 18 年订阅的。我内心可以从容的断定,不是他举报的,也不是其他订阅者举报的。 这明明就是官方主动封杀的,却在封杀理由上写着因为用户举报,可无聊了。 我文字发出去前,你官方也审核通过了,为什么发出去后 30 分钟,就把我的号永久关停了? 我没有发啥敏感内容,如果真的敏感,肯定是发不出去的。我也没有刻意用拼音代替汉字,就那么一个读者,我犯不着还用心的躲避审查。 只是我的文字,是关于上海的一些想法,就被封了。 上海,一个国际化大都市,也变成敏感话题了。今天,“四月之声” 的视频被全网封杀后,我又看了一些其他公众号的控诉,才发现仅仅是审查门槛降低了,就稍微不满意就封杀的那种,可搞笑了。 订阅号被封,我还反馈了一下,说 7-10 天给反馈好像,也一直没有反馈我。我也不指望反馈,因为订阅号我不会再申请,不依靠它吃饭,对我意义不大。就是自己说的话都不兑现,可丢大公司的脸了。 之前,我还想着自媒体不能称为媒体,因为自媒体没有媒体的要素。现在想想,自媒体能不被封号就不错了,哪还能自由表达自己的想法。那些依靠订阅号生存的人,一旦封号,可就是没饭碗了,能留下一些读者吃广告费养家,不容易的。 Anyway,我的号是归零了。 最近,我咨询了一些移民的事情,发现自己时而坚定时而徘徊,心是有顾虑的。投资,我是没那个钱。国内的房产,让我这么多年除了银行负债,只有一两万的存款。黑下去,我也不能带着家人过那种生活,那还不如国内耗着。而工作,或许是个办法。 可是,真的到外面生活了,那可是从零起步呢,我心里也发毛。之前没想过,现在想着就越发心里没底了。而除了自身之外,周围的牵挂也是挥之不去的。这些,都无形中有顾忌。 我想着归零是需要很大的决心。归零,时常有人提起。可真到了某一个方面归零的时候,还是会有很多犹豫的。不像工作上提到的归零,还只是复盘的一个节点,或者是找个台阶的文艺说法,没有那么伤精痛骨的意义。 比如技能归零。大学生毕业后从事的工作很可能就是他几十年的工作了,因为不知道除了已经会的,其他的该如何走下去,转行可不仅仅是一个工作内容的变更,还有新的征途和未知的风险。 比如婚姻归零。那些婚姻不满意对方而期望离婚的,也会有孩子的考虑以及日后少一个人陪伴在身边的忧虑。 比如刚才说的移民,生活上基本归零,啥都是新的,像刚进入社会的学生一样。而且不在本土,日后的杰出与伟大或许只能臆想。 本次疫情,上海也是一次归零。期望从封城之外寻找疫情下城市发展不停滞的路线,这是相当值得支持并且赞扬的。不过,这次归零是完完全全的失败,因为归零的路线定了,也执行一段时间了,最后又把归零的方案本身给否决回到原始方案了。目前大家都可以看到,这代价是何其的大。上海的归零灾难,就是没有上峰的支持而冒然行动的后果。 我更倾向于认为,归零,是一种选择。选择路千万,而归零是其中最干净和纯粹的一个。 人生虽路漫漫,历史长河中,一个人和一粒沙一样的微不足道。一个人的价值,我认为在于三个方面: 一是个人思维的成长。几十年的光阴,每时每刻都在接受信息,都在学习,不断的输入和输出。每个人都是自己人生的王,是自己戏本里的主角。对个人而已,就是让自己的思维跟得上年龄的脚步,这样才能在各个年龄段都在戏本里相得益彰、和平相处。 二是家庭的付出。家庭需要经营,戏本里除了自己,还有父母妻儿。我们保障了自己,也要保障他们的方方面面。他们在戏本里有举足轻重的地址。 三是社会的输出。每个人都在为当前的社会奉献着自己,即使再微不足道,也真真实实的有付出。这其中有时间,有体力,有脑力,有资金等等,有人付出的多,有人付出的少,但都有输出。 一个人的贡献,看起来很大。可这世上那么多人,社会贡献池巨大无比,那个人的贡献其实是可以量化的,那就是十分微小。伟人自然贡献巨大,但个人能不能称为伟人,自知即可。 人在做选择的时候,更应该知晓在三个贡献方面进行权衡。自知而言,都是在个人成长和家庭付出上取最大值即可。 比如换个职业,选自己更感兴趣的方向,那对个人而已肯定是有价值增幅。对家庭来说有可能是价值降低的。选更能赚钱的方向,对个人有可能比较痛苦,对家庭可能有更好的生活条件。这个要做权衡。 归零,就是自己戏本里的大改变。我们作为自己人生戏本里的王,可以尝试着改写剧本。归零后,个人和家庭的权衡如果都朝着有益的方向,那为什么不去拼一把呢? 归零谈不上豪赌,但选择了这条路还是要做好心理准备。有得有失,没有完美。剧本都是人生道路一步一步搭起来的,前面的路不走,后面的剧本也没法写。 选择这条纯粹干净的路,也要评估时间等额外成本。比如上海的疫情防疫,是一场归零,奈何上峰都没同意就去实践,走回头路的代价可是很大的。再比如年过半百,已无更大精力去践行归零后的行动,强归无力徒伤悲。 前两天写了半篇文章,关于 “世界那么多人,大家都不一样” 的。写了 3 个小时,放弃了。文笔不好,无法表达自己的感情,就放弃了。 今天想再回头看看还能不能补救,发现当时自己看不下去,给删除了。 其实是有些懊恼的,放在草稿里不就好了,抱怨下自己。

2022/4/23
articleCard.readMore

函数调用栈之彻底理解

2023.12.24 更:请移步 从汇编角度理解 “函数调用栈” 和 “有栈协程”。 新增了有栈协程的汇编实现章节,为适应爬虫也做了 url 更新。因本文有较多用户访问,担心有收藏会失效,所以新开一文,请做移步。 函数调用栈虽然表面意思很容易理解,但是说到汇编层面,对于 call、ret 等命令的具体表现,还是有一些理解复杂度。 尤其在协程大放异彩后,通过函数调用栈来表达协程的逻辑,也尤为重要。 本篇文章会从基础汇编的角度理解函数执行过程。这样,对于 Go/Python/Swift 等语言的协程的实现,也会更容易理解。 Start 函数执行过程大致如图中所示。其中函数栈的内存布局因操作系统和 cpu 架构的不同或许有差异,但不是重点。图中展示为 Mac + X86 系统。 从图中可以看到,程序执行的流程就是函数调用的过程。当函数调用完毕,程序也就停止了。所以函数调用对程序执行的影响还是非常大的,函数是指令层级的超高度抽象。 这里就先引出一些名词,后面不在特别说明。 内存布局:从上往下依次是内核区、栈区、共享数据区 (动态库)、堆区、数据区、代码区、留置区。这些区域的划分都是虚拟内存,并非物理内存。越往上内存地址越大,越往下内存地址越小。这里研究的函数调用栈就是在栈区。 栈区:从上往下分配空间,即先执行的函数的内部变量的虚拟内存地址一定大于后执行的子函数的内部变量的虚拟内存地址。 call:call 指令用于调用子函数。 push: push 指令用于将寄存器的值入栈保存到栈区内存中。 pop: pop 指令用于将当前栈区指向内存的值保存到寄存器中。 ret: ret 指令用于结束当前子函数调用,回到被调用处继续后续指令的执行。 rbp:栈底指针寄存器,值为栈底内存地址 rsp: 栈顶指针寄存器,值为栈顶内存地址 (下文会说到,rsp 不一定非要指向栈顶) 子函数调用前的准备 函数的作用是给予特定的输入,给出特定的输出。所以对子函数的调用,一个是传给子函数参数,一个是获取子函数的返回值。返回值在后面说,这里先说传参。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> int sub(int a, int b) { int t = a + b; printf("The sub value is:%d\n", t); return t; } int main(void) { int a = 1; int b = 2; int p = sub(a, b); printf("the return value is:%d\n", p); return 0; } 示例代码中,main 对 sub 函数的调用,需要 a 和 b 两个参数,下面通过 main 的汇编,可以看下参数是如何准备起来的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 main: 0x100003f30 <+0>: pushq %rbp 0x100003f31 <+1>: movq %rsp, %rbp 0x100003f34 <+4>: subq $0x10, %rsp# 准备 0x10 大小即 16 字节的留置内存区域,用于存放各种参数 0x100003f38 <+8>: movl $0x0, -0x4(%rbp)# 0 值写入,是默认预留的大小空间,无特别场景,不会使用 0x100003f3f <+15>: movl $0x1, -0x8(%rbp)# 1 值写入变量 a,实际是写到栈内存中 rbp - 8 地址处 0x100003f46 <+22>: movl $0x2, -0xc(%rbp)# 2 值写入变量 b,实际是写到栈内存中 rbp - 12 地址处 0x100003f4d <+29>: movl -0x8(%rbp), %edi# 将 rbp - 8 地址处的内存值写入 edi 寄存器,即 1 写入 edi 0x100003f50 <+32>: movl -0xc(%rbp), %esi# 将 rbp - 12 地址处的内存值写入 esi 寄存器,即 2 写入 edi 0x100003f53 <+35>: callq 0x100003f10 ; sub at main.c:15 0x100003f58 <+40>: movl %eax, -0x10(%rbp) 0x100003f5b <+43>: movl -0x10(%rbp), %esi 0x100003f5e <+46>: leaq 0x31(%rip), %rdi ; "the return value is:%d\n" 0x100003f65 <+53>: movb $0x0, %al 0x100003f67 <+55>: callq 0x100003f74 ; symbol stub for: printf 0x100003f6c <+60>: xorl %eax, %eax 0x100003f6e <+62>: addq $0x10, %rsp 0x100003f72 <+66>: popq %rbp 0x100003f73 <+67>: retq 我们重点讨论 a 和 b 两个入参的准备过程,对于 push,call 等,后面会再说。上面的 <+4> 到 <+32> 行是入参的栈填充过程,到了 <+32> 行,就开始正式调用子函数了。 首先,将 a、b 变量值写入栈的留置内存区域,而后将变量值从内存中读到寄存器中。这里的 edi 和 esi 寄存器就是用来传参的。 这里得到一种结论,即寄存器可以用来传递参数给子函数。 但寄存器的数量是有限的,如果需要传递非常多的参数,是无法都通过寄存器来解决的,比如下面的例子: 1 2 3 4 5 6 7 8 9 10 int sub(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o) { int t = a + b; printf("The sub value is:%d\n", t); return t; } int main(void) { int p = sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); printf("the return value is:%d\n", p); return 0; } 汇编如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 main: 0x100003ee0 <+0>: pushq %rbp 0x100003ee1 <+1>: movq %rsp, %rbp 0x100003ee4 <+4>: subq $0x50, %rsp # 预留 80 字节大小的栈内存空间 0x100003ee8 <+8>: movl $0x0, -0x4(%rbp)# 0 值写入,默认预留的大小空间,无特别场景,不会使用 0x100003eef <+15>: movl $0x1, %edi# 参数入 寄存器 0x100003ef4 <+20>: movl $0x2, %esi# 参数入 寄存器 0x100003ef9 <+25>: movl $0x3, %edx# 参数入 寄存器 0x100003efe <+30>: movl $0x4, %ecx# 参数入 寄存器 0x100003f03 <+35>: movl $0x5, %r8d# 参数入 寄存器 0x100003f09 <+41>: movl $0x6, %r9d# 参数入 寄存器 0x100003f0f <+47>: movl $0x7, (%rsp) # 参数入 栈内存 0x100003f16 <+54>: movl $0x8, 0x8(%rsp)# 参数入 栈内存 0x100003f1e <+62>: movl $0x9, 0x10(%rsp)# 参数入 栈内存 0x100003f26 <+70>: movl $0xa, 0x18(%rsp)# 参数入 栈内存 0x100003f2e <+78>: movl $0xb, 0x20(%rsp)# 参数入 栈内存 0x100003f36 <+86>: movl $0xc, 0x28(%rsp)# 参数入 栈内存 0x100003f3e <+94>: movl $0xd, 0x30(%rsp)# 参数入 栈内存 0x100003f46 <+102>: movl $0xe, 0x38(%rsp)# 参数入 栈内存 0x100003f4e <+110>: movl $0xf, 0x40(%rsp)# 参数入 栈内存 0x100003f56 <+118>: callq 0x100003e90 ; sub at main.c:10 0x100003f5b <+123>: movl %eax, -0x8(%rbp) 0x100003f5e <+126>: movl -0x8(%rbp), %esi 0x100003f61 <+129>: leaq 0x32(%rip), %rdi ; "the return value is:%d\n" 0x100003f68 <+136>: movb $0x0, %al 0x100003f6a <+138>: callq 0x100003f78 ; symbol stub for: printf 0x100003f6f <+143>: xorl %eax, %eax 0x100003f71 <+145>: addq $0x50, %rsp 0x100003f75 <+149>: popq %rbp 0x100003f76 <+150>: retq 可以发现,这边测试环境下,传参超过 6 个,就需要写入栈内存。超出的部分,就不能通过寄存器传参了。 在上面有 a 和 b 两个变量的例子中 (int a = 1; int b = 2; int p = sub(a, b);),数据虽然也写到了 -0x8(%rbp) 和 -0xc(%rbp) 栈内存中,但那是因为 a /b 两个变量当前函数需要使用,不得不写入栈内存,如果像下面这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int sub(int a, int b) { int t = a + b; return t; } int main(void) { int p = sub(1, 2); printf("the return value is:%d\n", p); return 0; } ############ main: 0x100003f30 <+0>: pushq %rbp 0x100003f31 <+1>: movq %rsp, %rbp 0x100003f34 <+4>: subq $0x10, %rsp 0x100003f38 <+8>: movl $0x0, -0x4(%rbp) 0x100003f3f <+15>: movl $0x1, %edi# 直接写入 edi 寄存器,不在写入栈内存 0x100003f44 <+20>: movl $0x2, %esi# 直接写入 esi 寄存器,不在写入栈内存 0x100003f49 <+25>: callq 0x100003ef0 ; sub at main.c:15 这里就很清晰了,1 和 2 两个参数,都是直接写入寄存器中的,不会再写到栈内存中。 对于 sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15) 的子函数调用,栈内存如下: 备注 1: 大家看到栈内存里面,从上往下的内存顺序正好和我们代码里面的顺序是反的,这是 Mac + X86 环境。不同环境下这个规则是不一样的,有些环境下先写的参数会在上面。 备注 2: 当前 7-15 的入参,是在当前函数栈里面存储的。即子函数执行完毕后,当前函数栈依旧保存这些入参。 子函数被调用 通过 call 指令即可完成函数帧的切换,即可以正常调用子函数了。上面例子中 callq 0x100003e90 ; sub at main.c:10 即表示子函数跳转。 在调用子函数前,需要完成两个工作: 首先是当前指令的下一行指令地址(即 ip 寄存器的值)需要存入栈内存,为了在子函数执行完毕后可以继续执行下去。 其次是需要将子函数的函数内存地址 (代码区) 放入 ip 寄存器,这样 CPU 才能去执行子函数。 而这两个操作都是在 call 指令中完成的,所以 call 指令实际上等同于下面操作: 1 2 pushq %rip movl <子函数内存地址> %rip 对于 sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15) 的子函数调用,栈内存如下: 这里再说明一下,为什么要先把 ip 程序寄存器的值写入栈内存,然后又写入新的值进去。 因为程序运行的原理就是晶震驱动下的指令不间断的执行。指令执行需要内存地址,就是从 ip 程序寄存器里面读的。 当执行到 call 指令的时候,ip 寄存器里面已经存储了下一条指令的地址,即 0x100003f5b。这里因为函数调转,不能执行下一条指令了,所以需要把 ip 寄存器先入栈内存暂时存储,即 pushq %rip。 入栈后,要去执行子函数,又需要把子函数的内存地址写入 ip 寄存器,这样 CPU 才会去执行子函数。即 movl 0x100003e90, %rip。 这里还有一个隐藏的点,即 rsp 寄存器变化了。原先指向入参0x7 的,这个时候就指向下一条指令的地址 ,即 0x100003f5b 位置。rsp 之所以能够变化,是因为 pushq %rip 指令会改变 rsp 的指向,下面会说到。 子函数执行中 这里就是重点了,rbp 和 rsp 有非常大的用途。它是函数调用栈的核心,也是理解协程的基础。 rsp 是栈顶内存指针,rbp 是栈底内存指针。当栈内存需要更多的空间的时候,rsp 会指向更小的内存区域以时刻指向栈顶。 一般来说 rbp 和 rsp 的差值,就是栈内存空间的实时大小。但这个不是绝对的,因为依靠编译器的实现,为了高效并不会绝对的控制 rsp 寄存器的值,即有可能出现栈内存实际大小大于 rbp 和 rsp 的差值。后面会提到这个 case。 其中 rbp 有三个非常大的作用,是函数调用栈的中流砥柱。 在 rbp 基础上增加值偏移,可以访问到父函数的栈内存数据(如入参)。 在 rbp 基础上减少值偏移,可以访问到子函数的栈内存数据(如局部变量)。 rbp 本身指向父函数的 rbp 的存储地址,用于子函数执行完毕后回到父函数时的 rbp 还原。 下面看下 sub 函数的汇编: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 int sub(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o) { int t = a + b; printf("The return value is:%d\n", t); return t; } sub: 0x100003e80 <+0>: pushq %rbp # 以上,将父函数的 rbp 值存入栈底 0x100003e81 <+1>: movq %rsp, %rbp # 以上,将当前函数的 rsp 值赋予 rbp,此时 rbp 是子函数的栈底 0x100003e84 <+4>: subq $0x20, %rsp # 以上,将 rsp 值减少 32 字节偏移,开辟栈预留内存空间 0x100003e88 <+8>: movl 0x50(%rbp), %eax 0x100003e8b <+11>: movl 0x48(%rbp), %eax 0x100003e8e <+14>: movl 0x40(%rbp), %eax 0x100003e91 <+17>: movl 0x38(%rbp), %eax 0x100003e94 <+20>: movl 0x30(%rbp), %eax 0x100003e97 <+23>: movl 0x28(%rbp), %eax 0x100003e9a <+26>: movl 0x20(%rbp), %eax 0x100003e9d <+29>: movl 0x18(%rbp), %eax 0x100003ea0 <+32>: movl 0x10(%rbp), %eax # 以上,根据 栈底 rbp 做增加值偏移,获取父函数的栈内存数据,即入参 0x100003ea3 <+35>: movl %edi, -0x4(%rbp) 0x100003ea6 <+38>: movl %esi, -0x8(%rbp) 0x100003ea9 <+41>: movl %edx, -0xc(%rbp) 0x100003eac <+44>: movl %ecx, -0x10(%rbp) 0x100003eaf <+47>: movl %r8d, -0x14(%rbp) 0x100003eb3 <+51>: movl %r9d, -0x18(%rbp) # 以上,将入参寄存器的值存入当前栈内存空间,做减小值偏移 0x100003eb7 <+55>: movl -0x4(%rbp), %eax 0x100003eba <+58>: addl -0x8(%rbp), %eax # 以上,完成 a + b 操作 0x100003ebd <+61>: movl %eax, -0x1c(%rbp) # 以上,将 a + b 的结果,存入栈内存空间 0x100003ec0 <+64>: movl -0x1c(%rbp), %esi 0x100003ec3 <+67>: leaq 0xd4(%rip), %rdi ; "The return value is:%d\n" 0x100003eca <+74>: movb $0x0, %al 0x100003ecc <+76>: callq 0x100003f7e ; symbol stub for: printf # 以上,调用 printf 函数开始打印 a + b 的值 0x100003ed1 <+81>: movl -0x1c(%rbp), %eax 0x100003ed4 <+84>: addq $0x20, %rsp 0x100003ed8 <+88>: popq %rbp 0x100003ed9 <+89>: retq # 以上,是子函数执行结束后的父函数还原,下一趴说 汇编注释做了说明,主要就是 rbp 值和 rsp 值的变化,如下所示: 这里有一个点,从汇编里面不太容易看出来 rsp 的变化。在子函数没有调用之前,rsp 的值是指向 main 函数栈顶的 p: 0x100003F5B。在调用 pushq %rbp 将父函数的 rbp 值存入栈底后,rsp 的值就已经变化了。因为 push 实际上是完成了两个工作,如下: 1 2 3 4 5 6 pushq %rbp = subq $0x8, %rsp movl. %rbp, %rsp 这两个步骤,一个将 rsp 减大小偏移 8 个字节,一个将 父函数的 rbp 值写入到偏移后的栈内存里面。 然后执行 movq %rsp, %rbp 是为了将 rbp 置为当前子函数栈的栈底,即此时 rbp = rsp。到这里,子函数栈的两个重要参数 rbp 和 rsp 就已经设置好了。 再往后就开始预留栈内存空间了,subq $0x20, %rsp,预留内存空间的方式就是将 rsp 做减大小偏移。 有一个注意点,就是预留大小并不一定非要 rsp 做减大小偏移,其实只要能够持续稳定的访问到一个栈内存地址即可,如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int sub(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o) { int t = a + b; // printf("The return value is:%d\n", t); return t; } `sub: 0x100003e90 <+0>: pushq %rbp 0x100003e91 <+1>: movq %rsp, %rbp 0x100003e94 <+4>: movl 0x50(%rbp), %eax 0x100003e97 <+7>: movl 0x48(%rbp), %eax 0x100003e9a <+10>: movl 0x40(%rbp), %eax 0x100003e9d <+13>: movl 0x38(%rbp), %eax 0x100003ea0 <+16>: movl 0x30(%rbp), %eax 0x100003ea3 <+19>: movl 0x28(%rbp), %eax 0x100003ea6 <+22>: movl 0x20(%rbp), %eax 0x100003ea9 <+25>: movl 0x18(%rbp), %eax 0x100003eac <+28>: movl 0x10(%rbp), %eax 0x100003eaf <+31>: movl %edi, -0x4(%rbp) 0x100003eb2 <+34>: movl %esi, -0x8(%rbp) 0x100003eb5 <+37>: movl %edx, -0xc(%rbp) 0x100003eb8 <+40>: movl %ecx, -0x10(%rbp) 0x100003ebb <+43>: movl %r8d, -0x14(%rbp) 0x100003ebf <+47>: movl %r9d, -0x18(%rbp) 0x100003ec3 <+51>: movl -0x4(%rbp), %eax 0x100003ec6 <+54>: addl -0x8(%rbp), %eax 0x100003ec9 <+57>: movl %eax, -0x1c(%rbp) 0x100003ecc <+60>: movl -0x1c(%rbp), %eax 0x100003ecf <+63>: popq %rbp 0x100003ed0 <+64>: retq 上面的 sub 函数,注释掉 printf 子函数调用。这个时候,就不会有 subq $0x20, %rsp 和 addq $0x20, %rsp 两个 rsp 减大小偏移的操作了。因为 sub 就是当前函数调用栈的最后一个子函数,它可以保障当前函数执行过程中的栈数据是稳定可靠的,因为不会再有子函数来打扰它。但是 sub 函数依旧可以通过 -0x4(%rbp) 方式访问当前栈内存空间。 所以前面说过,rbp 和 rsp 的差值,不一定是栈内存空间的大小。上面的例子里面,rbp = rsp,即差值为 0,但当前栈内存依旧保存有 6 个寄存器的入参。 子函数执行结束后 快要接近尾声了。 子函数需要把返回值返回到父函数,是通过 eax 寄存器实现的。 1 2 3 4 5 6 7 8 9 10 11 12 sub: 0x100003eb7 <+55>: movl -0x4(%rbp), %eax 0x100003eba <+58>: addl -0x8(%rbp), %eax # 以上,完成 a + b 操作 0x100003ebd <+61>: movl %eax, -0x1c(%rbp) # 以上,将 a + b 的结果,存入栈内存空间 0x100003ed1 <+81>: movl -0x1c(%rbp), %eax # 以上,从栈内存空间中取出返回值,存入 eax 寄存器。父函数通过eax 拿到子函数的返回值。 main: 0x100003f5b <+123>: movl %eax, -0x8(%rbp) # 父函数拿到 eax 返回值,目前是写入栈内存。也可以直接使用,看代码逻辑决定。 目前子函数快要执行完毕后,参考上面的汇编,最后还需要执行三个指令: 1 2 3 4 0x100003ed4 <+84>: addq $0x20, %rsp 0x100003ed8 <+88>: popq %rbp 0x100003ed9 <+89>: retq # 以上,是子函数执行结束后的父函数还原 其中做了这么几件事情: 还原父函数的 rbp 还原父函数的 rsp 使得父函数继续执行下去 其中 pop 和 ret 也有多个意图,如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 popq %rbp = movl %rsp, %rbp addq $0x8, %rsp ------------ retq = movl %rsp, %rip addq $0x8, %rsp 可以发现,pop 和 ret 都会操作 rsp 寄存器,将 rsp 寄存器的值做增大小偏移,从而还原到父函数的 rsp 状态。 最后将 call 指令存储到栈内存的下一行指令地址写回到 ip 寄存器,使得 CPU 从子函数调用处继续执行下去。 有栈协程如何实现 2023.12.24 更:新增了有栈协程的汇编实现章节,为适应爬虫也做了 url 更新。请移步 从汇编角度理解 “函数调用栈” 和 “有栈协程”。

2022/3/20
articleCard.readMore

提高效率的手艺

本来有更重要的事情要做,但那个事情急不得。我也就当作拖延时间一样,不去做它,转而分享两个对我近些年十分有帮助的手艺。 近十年有两个对我非常有益的手艺,极大的提高了我的工作舒适度。手艺一般称为个人经过学习从而有了一个能够吃饭的看家本领,那我这个称为手艺,其实有些夸大的,更多算作技巧。但我没有改动这个形容词,也只是想要描述这两个技巧对我的帮助很大。 双拼 这个对我的帮助最大,是我极力推荐的。很多人都不知道双拼,我是在十年前接触了双拼,从此打字不再是苦恼。 双拼是一种打字的方式,和普通的拼音打字不同,在双拼的字典里面,任何一个汉子都只需要两个字母即可表达。比如双拼这两个汉字,键盘表达就是 ul 和 pb。 双拼的学习曲线不是很陡,但需要熬过一个艰难的心里不适应期,一般在 3-5 天。有一套双拼码表需要背,虽然熟悉之后就是肌肉记忆了,但是刚学习的时候,还是要背的。 双拼有好几种码表方案,我开始的时候使用的搜狗双拼,后来改成了小鹤双拼,这个我觉得更好用。小鹤双拼的码表如下: 举个例子,“这是一个测试” 这几个汉字,用小鹤双拼打,就是这样:ve ui yi ge ce ui。双拼也会有常用缓存,比如一个字你打的久了,后面打前一个字母,也就给这个字的推荐了。不过我不常用,因为两个字母已经够简洁了,而且肌肉记忆之后,打一个字母反而不习惯。 现在的双拼输入法,已经很普遍了,完全没有输入法选择困难。 我刚开始用双拼的时候,苹果键盘还没有小鹤双拼,我只能使用搜狗双拼。后面还是想用小鹤,因为其他平台有小鹤,转换起来两套码表把我都搞晕了,于是苹果上面我就用 Gboard,它支持小鹤。再往后苹果也支持了小鹤,我就转移到苹果的全拼音键盘了,很好用。 还有呢,我建议即使最后没有选择双拼,我也建议你不要用搜狗输入法,这是一个没有底线的侵犯隐私的输入法。我们打字过程中会有非常多的隐私泄漏,一定要非常注意。 VIM Vim 也是一个划时代工具,从青铜时代开始划的,一直划到现在,也会一直划下去。 我开始使用 Vim 是因为服务器操作只能使用 Vi 类编辑器,没得选择。 而后使用各类 IDE,快捷键呢完全记不住,脑子笨,就一直使用 Vim 插件了。 Vim 的曲线很高,需要 3-7 天适应期,也是肌肉记忆,熟悉了就很难忘记怎么用,但初期也是要背 Vim 快捷键的。 使用 Vim 的方便在于,学习一套快捷键,各大平台及 IDE 使用。可以做到不管在哪里编码,编码方式都不会断片。(除了一些特立独行的 IDE 如 xcode,目前 xcode 也原生支持 vim 了,但之前有 4 年左右的空档期) Vim 的进阶难度很高,我使用 Vim 近十年,也是不思进取,只是简单使用,没有把 Vim 作为 IDE 使用。但怎么说呢,Vim 入门后基本就可以放心使用了,那些你不知道的奇思淫技,完全不使用也完全不影响工作,只能说知道的越多食用效果越佳。 Vim 解放了鼠标的移动和快捷键的记忆,非常有用。浏览器上面使用 Vim 也是超级棒,页面滚动和 tab 跳转都非常方便,效率 Double。 我其实很少用到鼠标 (触控板),网页、IDE、应用切换等常用的操作,都通过键盘来完成,速度快体感好。 Xcode & 机械键盘 对于我这种习惯使用 vim 的人来说,写作其实很痛苦。因为写作要频繁的在中英文之间切换,不如写代码总是英文环境。这个时候 vim 很有局限性,要频繁的切换输入法才能切换模式。 经典和偏门的写作工具,我都体验了一遍,但不理想。主要局限性在两点:1. 不支持 vim。2. vim 频繁中英文切换体感非常差。 最后,我发现 Xcode 的 vim 模式解决了我的问题。Xcode 里面的 vim 是 apple 定制的,不需要特别安装插件。它可以在不切换输入法的情况下进行 vim 模式切换等各种复杂操作。体感相当爽,一用就停不下来了。 顺带说一下,Xcode 是 Apple 公司的官方 app 编辑器 (IDE),一般只有 iOS/Mac 开发同学使用,比较大,最近版本大小缩小到 6-7G,之前动不动就是 10G+。 现在我不仅对于 markdown 格式的 blog 使用 xcode 管理,对于笔记也迁移到了 xcode 管理。 blog 我通过外部文件夹引入,相当于 ln 命令。笔记我通过 swift playground + workspace + private github 实现。都是几分钟就可以完成的简易配置。效果图如下: 还有一个 case,就是频繁使用电脑的人,一定要使用机械键盘。体感完爆自带键盘,比如虽然 Apple 系列的自带键盘其实已经很好用了。 机械键盘可以在青轴和茶轴之间选择,青轴相比茶轴声音大一些,其实打字最舒服,但是比较吵人,如果在办公场景,最好使用茶轴。 还有就是键位的问题,Windows 选一个合适价位的机械键盘就可以了,Mac 的就比较难选,主要是键位和 Windows 不匹配。我推荐京造 K2,少有的低价且匹配 Mac 系列的机械键盘了。 好的工具,可以极大提高生产力和舒适度。而且工具往往不容易坏,可以用很久,一次投入长久享受。比如降噪的耳机、电瓶车的锂电次、高价位的床垫等等。 希望孩子有一天可以出国学习,出去后,就别回来了。这个事呢,多说无益,鸡同鸭讲。 我就是担心那一天真的到来了后,我经不住亲人的劝阻,我希望我能够坚持下去。 我想了一下,如果我不坚持,孩子应该是没有机会出去的。

2022/3/13
articleCard.readMore

锁 - 共享数据安全指↑

锁,是并发 & 并行编程下的万能钥匙,也是最容易产生性能瓶颈的源头。在锁的前面有软硬件在协同铺路,在锁的后面还有数据结构和算法在帮忙黄袍加身。 提前说明,本篇涵盖内容非常多,达到 4W 字。可分多餐多时间段食用,每个章节可独立阅读,问题不大。一定不要一口气阅读完,选章节阅读确认对自己有用,再细看不迟。 内存相关的知识非常重要,需要很大篇幅和示例图来阐述。另一篇文章内存分段与分页对 CPU 和内存的数据读取做了详细阐述。在内存分段与分页中文字数超过了 1W,本文的文字数也超过了 4W,均需要多张配图。重要性和难理解程度,都很高。 共享数据安全牵涉到的点很多,从硬件层面的多核心高速缓存、MESI 缓存一致性模型、CPU 乱序执行 & 中断、总线 & 缓存原子锁,到操作系统提供的锁和编译器优化 & 重排,以及高级语言为不同业务场景不断叠加的中间层锁优化,最后还有算法如何进一步保障锁的性能。 大家都知道并发并行场景下共享数据会不安全,本文不是阐述数据不安全会造成的严重影响,而是为什么共享数据会不安全。 数据安全产生的物理原因 单核 - 时间片轮转下的数据安全问题(并发 & 中断) 时间片轮转是针对分时系统来说的,即单个 CPU 对于每个任务的执行,都只执行一小段时间,不断的在多个任务之间快速的切换。初期是在多个进程之间不停的切,嫌弃进程切换代价太高,出现了多线程技术方案后,CPU 又在多个进程和线程之间不停的切换执行。多进程和多线程是一种提高 CPU 使用率的方案,即并发。 CPU 在任务执行过程中,本身是感知不到时间片存在。在晶振产生的时钟周期驱动下,CPU 会不间断的根据 PC 寄存器里的地址进行取址译码执行。晶振是一个中断,中断的频率越高,CPU 执行的速度也就越快。而时间片一切,PC 寄存器的地址改变,CPU 就被动的在多个任务之间切换执行。 时间片切换有两个来源,一个是操作系统控制的并发,一个是软硬件中断。并发本身也是通过晶振中断来实现的,因为由操作系统控制,额外增加了优先级队列等一系列控制,所以单独作为一个来源来分析比较清晰。 所以现在就通过并发和中断,来分析时间片切换可能会导致的共享数据安全问题。单核场景会遇到并发和中断,多核在单核的基础上又遇到了其他问题,这里就先说单核,下一趴说多核。 非原子操作 - 并发 就是大家都知道的,我们写的代码如果在多线程场景下,如果有共享数据,那么共享数据会不安全,会产生数据紊乱。 1 2 3 4 5 int flag = 0; void threads(void) { flag++; } 上面代码,flag 是共享数据,threads 被并发执行。当 threads 函数被 1000 个线程并发执行的时候,最后 flag 的值 < 1000,极小概率 = 1000。因为 flag++ 需要最少三条指令才能运行完毕,分别是读内存、寄存器赋值、写内存。这里就会出现 a 线程执行了读内存指令后,时间片切到了 b 线程,b 线程完成了读、赋值、写指令,又切回到 a 线程,a 继续完成赋值和写指令。因为 a、b 读到的内存值是一样的,最后写入的也就是一样的,所以 flag 相当于少了一次 + 1 操作。时间片轮转对于指令执行流程来说是随机的,所以 a 和 b 的三个指令完全有可能任意交叉执行。详见下表: 非原子操作 - 中断 上面也说到,并发是根据晶振中断实现的。除了晶振中断,还有其他的软硬件中断会改变 CPU 的执行流程。如果改写了中断向量表的中断指向或者我们在监控到中断到来时执行特定函数,一样会遇到和上面的并发一样的数据安全问题,代码如下: 1 2 3 4 5 6 7 int flag = 0; void interrupt_run() { flag++; } void thread_run(void) { flag++; } 我们假设 thread_run 函数是单线程执行的,因为中断时机是未知的,完全有可能 interrupt_run 和 thread_run 的执行时机会出现上面并发场景下的情况,这个时候 flag 也不再数据安全。和并发一样,详见下表: 非原子操作阐述 非原子操作,即任务执行过程中可能会因为时间片轮转发生执行中断情况的操作。如果非原子操作中出现共享数据,则共享数据不在安全,可能会产生紊乱。 这里有两个前提,即非原子操作和共享数据。如果没有共享数据,相当于 1 个 CPU 的两条任务线独立执行,是没有问题的。那如果是原子操作呢? 原子操作 - 并发 & 中断 对于原子操作,即执行周期内不会被打断的指令。该指令可能需要多个时钟周期才能运行完毕,因为有取址、译码、执行一套动作,最少也需要 1 个时钟周期,全过程称为执行周期。在执行周期内,该指令一定有头有尾的被执行完毕,即要么不执行,要么全执行。 所以可以猜想和非原子操作的不同,因为没有产生执行中断,那么共享数据或许是安全的。下面举几个特别的原子操作来说明共享数据的安全性。 内存原子操作 1 2 3 4 5 6 // 内存原子操作指令。 mov *lock, 0; // 初始化 void threads(void) { addl &lock, 1;// lock 内存地址值 + 1 } 上面代码中,lock 初始值为 0,完成了上面 flag++ 同样的工作。threads 不管在并发场景还是中断场景,lock 内存地址里面的值都是安全的。即 1000 个线程并发执行完成后,lock 值一定是 1000。 addl 内部是需要多个操作完成的,首先,需要取 lock 内存地址里面的值到中间寄存器 ax,然后,需要将 ax 寄存器中的值加 1,最后,再将 ax 寄存器中的值写入 lock 内存地址中,所以 addl 和上面高级语言中的 flag++ 是一样的内存操作流程。上面的 flag++ 大家已经知道不是数据安全的,因为他的执行流程会因为时间片轮转的原因被打断导致共享数据被其他任务修改,那么 addl 在时间片轮转下如何表现呢? 比如 addl 已经取了 lock 内存地址里面的值,还没进行 ax 寄存器加 1 操作,这个时候有没有可能因为时间片轮转导致 *lock = 100; 这个其他任务里面的命令被执行呢?不会的,因为 addl 是原子操作,addl 没有执行完,时间片轮转不会发生,这个时候共享数据不会被其他任务更改。 通过上面的分析,即使 addl 指令需要很多个时钟周期才能完成一次寄存器和内存数据的交换操作,和 flag++ 有一样的内存操作流程,但 lock 是共享数据安全的。即内存原子操作,在并发 / 中断场景下,共享数据是安全的。 寄存器原子操作 1 2 3 4 5 6 7 // 寄存器原子操作指令。 mov ax, 0; void threads(void) { add ax, 1; } 因为我们这里讨论的是共享数据安全问题,这种问题一般都不出现在寄存器中,而是出现在高速缓存或者内存中。比如前面的 flag++ 和 addl &lock, 1 以及后面要说的 xchg 和 cmpxchg 这些,指令中都可以操作内存地址。但是这里还是提一下,对于示例中 ax 寄存器的原子操作,没法说安全不安全,因为寄存器不能当作一个共享变量来看待,即 add ax, 0; 这里的 ax,根本不可能是两个线程里面的共享寄存器。所以上面的示例代码其实是不存在的。并发场景下,从 a 线程切到 b 线程,会对 a 线程里面的 ax 寄存器值保存到内存,然后到 b 线程重新配置使用 ax 寄存器。后面回到 a 线程后,ax 寄存器会从 内存中重新拿到上一次保存到值。所以在并发场景下,ax 寄存器是独立存在的,不存在数据共享问题。 xchg 原子操作 1 2 3 4 5 6 7 8 // xchg 内存交换指令 *lock = 0; mov ax, 1; void threads(void) { xchg ax, &lock; } 上面代码中,xchg 是 x86 提供的内存交换指令,即将一个寄存器值和一个内存地址中的值进行原子操作交换。比如上面例子,lock 内存地址默认值为 0,ax 寄存器默认值为 1。在 threads 执行一次后,lock 内存地址中的值会变成 1,ax 寄存器中值会变成 0。再执行一次,则因为 ax 寄存器中值为 0,所以再次互换后,就回到了初始的默认状态,即 lock 为 0,ax 为 1。 可以得到一个规律,执行奇数次 “并发 / 中断” 操作,则 lock 内存地址值为 1,ax 寄存器值为 0。执行偶数次 “并发 / 中断” 操作,则 lock 内存地址值为 0,ax 寄存器值为 1。这个时候,不管我们怎么执行,只要控制住次数,那么结果就是确定的。我们可以以此理解 xchg 原子操作在并发 / 中断场景下是安全的,但不准确,xchg 原子操作的安全,就和上面的内存原子操作是安全的一样。 xchg 和上面的 addl &lock, 1; 指令相比,xchg 内部只是多了一个 ax 寄存器,其他都是一样的。首先,需要取 lock 内存地址里面的值到中间寄存器 bx,然后,需要将 ax 寄存器中的值写入 lock 内存地址,最后,再将 bx 寄存器中的值写入 ax 寄存器中。所以可以和内存原子操作一样理解。 xchg 原子操作很重要,是自旋锁的底层实现。下面锁的篇章会再次说到。 cmpxchg 原子操作 1 2 3 4 5 6 7 8 9 10 11 // cmpxchg 比较交换指令 *lock = 0;// 或者初始化为 1,代表 cmpxchg 指令的首操作数 mov ax, 1;// 代表 cmpxchg 指令的比较值 mov bx, 0; // 或者初始化为 1,代表 cmpxchg 指令的首操作数 mov cx, 2;// 代表 cmpxchg 指令的源操作数 void threads(void) { cmpxchg &lock, cx; // 或 cmpxchg bx, cx; } 上面代码中,cmpxchg 是 x86 提供的比较交换指令,共需要 2 个寄存器和 1 个寄存器或者内存地址。 首先需要有一个用于比较的值,这个值需要在寄存器中,上面我们用 ax 存储。 还需要一个 “首操作数”,即 cmpxchg 指令后面的第一个操作数,这个操作数可以是寄存器或者内存地址,上面我们用 bx 或者 &lock 表示。 最后还需要 “源操作数”,即 cmpxchg 指令后面的第二个操作数,这个操作数需要在寄存器中,上面我们用 cx 存储。 cmpxchg 就是让比较值和首操作数比大小,如果相等,则首操作数赋值为源操作数。如果不想等,则比较值赋值为源操作数。 我们分析 cmpxchg &lock, cx; 这种场景,因为 ax 为 1,lock 内存地址值为 0,两者不想等,所以 cmpxchg 指令执行完成后,lock 没有变化,ax 寄存器变成了 2。如果按照上面注释里面 lock 初始化为 1,则 ax 和 lock 内存地址值都是 1,两者想等,这个时候 lock 内存地址值会变成 2。 cmpxchg 也是数据安全的,和上面 xchg 的分析一样,即使 cmpxchg 的执行需要很多个时钟周期,包含内存写、寄存器读等多个操作,但是 cmpxchg 的执行流程不会被时间片轮转所打算,从开头到结尾一鼓作气执行完毕,也是数据安全的原子操作。 cmpxchg 原子操作很重要,是 CAS 无锁的底层实现。下面锁的篇章会再次说到。 原子操作阐述 当一个任务不会被时间片轮转后中途暂停执行,那么这个任务在单核场景下就是安全的。 其实 CPU 提供的指令集基本都是原子操作的,比如读写内存的 “load xxx” 和 “store xxx”,这些指令在单核下都是安全的。如果我们都写汇编并且实际任务运算都可以通过原子操作完成,那么在单核分时机制下就不会有数据安全问题。但实际上,即使我们都写汇编,但我们真实执行的任务都不是原子操作可以完成的,即我们需要解决的任务需要 N 个原子操作配合才能够完成。只要 >= 2 个原子操作配合的任务流,都有可能在时间片轮转的情况下被中断执行,中断过程中共享数据就有可能被其他任务修改,不在数据安全。 多核 - 高速缓存场景数据安全问题(并行 & 时间片) 在单核心的时候,硬件产生的共享数据安全问题就上面提到的时间片问题,因为并发和中断都会产生时间片轮转,所以可以把单核问题归于并发和中断两个方面。 在多核的时候,问题就一下子变复杂了,不仅仅单核时候的问题一个没少,甚至单核原子操作也不在安全,而且更增加了非常多难以解决的其他问题。多核的硬件问题,就源于两个场景:并行 和 高速缓存。 并行 并发是针对单核 CPU 分时间片执行多个任务来说的,直观来说就是多进程 & 多线程。但是这时 CPU 还是只有一个,即同一时间,只有可能有一个任务在执行。N 核 CPU 就代表着不仅同一时间每一个 CPU 核心保持着时间片乱转 (并发 & 中断) 的操作,而且 N 个 CPU 核心保持着独立同时执行。这种 N 核同时运行即并行。 备注一下,并行和时间片乱转没有强相关,没有分时系统的时候,单任务系统依旧可以有多核。并行相比高速缓存来说,还没那么卷,可以先来分析。还是从单核时期的原子操作和非原子操作来说。 非原子操作 1 2 3 4 5 int flag = 0; void cores(void) { flag++; } 非原子操作在单核下因为执行过程被打断,会出现数据紊乱。在并行也是下一样的,只是执行过程被打断的原因不是因为时间片轮转,而是同时操作。 core1 拿到 flag 为 0,core2 拿到 flag 也为 0。两个核心执行完毕后,flag 没有变成 2,而是 1。 和并发 & 中断相比,现象是一样的,原因的本质也是一样的,只是原因的表现有些不同。所以非原子操作在多核下,并发 & 中断 & 并行 一起导致了数据不安全。 原子操作 原子操作在单核下是安全的,但是在多核下原子操作就不在安全了。拿 cmpxchg 举例子来说 1 2 3 4 5 6 7 8 9 10 11 // cmpxchg 比较交换指令 *lock = 0;// 或者初始化为 1,代表 cmpxchg 指令的首操作数 mov ax, 1;// 代表 cmpxchg 指令的比较值 mov bx, 0; // 或者初始化为 1,代表 cmpxchg 指令的首操作数 mov cx, 2;// 代表 cmpxchg 指令的源操作数 void threads(void) { cmpxchg &lock, cx; // 或 cmpxchg bx, cx; } 前面说到 cmpxchg 的执行需要很多个时钟周期,包含内存写、寄存器读等多个操作,但是 cmpxchg 的执行流程不会被时间片轮转所打算,从开头到结尾一鼓作气执行完毕,所以在单核场景下是数据安全的原子操作。 但是在多核场景下,有可能会出现一个 CPU 核心把 “首操作数” 取值完毕,另一个 CPU 核心同时把 lock 内存地址的值给改了。因为 cmpxchg 需要好几个任务流程,需要很多时钟周期,很难说执行过程中会不会有另一个 CPU 也对共享内存值做了其他的操作。这时候就有可能出现 ax 寄存器值和 “首操作数” 开始的时候不一样,cx 被赋值到 ax 寄存器。可是指令执行结束后,发现 lock 内存地址的值和当时 cx 寄存器值却是一样的,这就产生了问题。所以 cmpxchg 没有死于 时间片轮转,却死在了并行上。 这种打时间差插边球的操作会使得最终的数据不符合任务流程的预期,所以上面提到的 addl、xchg、cmpxchg 在多核下都不是数据安全的。 高速缓存 高速缓存对数据完整性简直就是摧枯拉朽的摧残。为了应对各个信息存储器件之间的速度不一致,高速缓存的存在是必要的。在 CPU 和内存之间加入高速缓存,通过对程序局部性原理的评估,可以有效增加缓存命中率,即 CPU 绝大部分时间都不用和内存打招呼,只要从高速缓存里面读取和写入相关数据即可。这可以极大的提高 CPU 的生产力。下面先分析一下单核时期的高速缓存以及它的重要意义。 高速缓存存在的意义是为了弥补各类存储器件之间速度的巨大差异性。如下图所示,容量越大,造价越低,速度越慢。 我们最常见的是网络资源,浏览器无时无刻不在使用网络资源,存储大小接近无限。其次常见的是本地磁盘,一般 250G-2T 的都有。常见的内存一般只有 1G-16G,容量远远小于本地磁盘。而容量更小的寄存器和 L1/L2/L3 高速缓存基本就见不到了,他们被封装在了芯片中。L3 能达到 32M,L2 能达到 256kb,L1 能达到 16kb,寄存器甚至只能有 1-8 个字节大小。 CPU 由晶振驱动,晶振的周期越快,CPU 跑的也就越快。虽然因为散热问题,CPU 的速度近十年没有提升了,但依旧达到了 4GHZ,大约每秒可以跑 40 亿次时钟周期,如果是简单指令,每秒可以执行 40 亿次。但是内存的速度在 50-100ns 之间,即每秒最多只有 2 千万次读取,这之间的速度鸿沟就使得 CPU 一直处于空等状态。所以银弹就是高速缓存,没有中间层解决不了的事情,基于时间和空间局部性原理,只要缓存的命中率达到预期,那么 CPU 就可以不和内存打交道,转而和高速缓存进行通信。目前高速缓存的命中率能达到 90%,可以说极大的提高了生产力。 对于 PC 机系列,一般是每个 CPU 核心有自己的 L1 和 L2 高速缓存,多核心共用 L3 高速缓存。对于手机系列,一般 CPU 核心分大小核簇,每个核心有自己的 L1 高速缓存,每个核簇有自己的 L 2 高速缓存,所有核心共有 L 3 高速缓存。对于手机系列,这样设计可以使的大小核簇之间更好的切换,能够更省电,在待机等状态下,可以仅运行小核心,大核心完全处于空转睡眠状态。 补刀 - 高速缓存为什么高效 下面举两个例子来说有了高速缓存后在单核场景下数据操作流程以及它们是如何使用 “时间局部性” 和 “空间局部性” 的: 1 2 3 4 5 6 7 8 9 10 11 12 // 例子 1 int flag = 0; void thread(void) { flag++; } void main() { // 连续调用两次 thread 函数,会先将全局变量 flag 读入高速缓存后进行读取。两次函数调用完成后,在一定时机触发后,才会将高速缓存中的值写回主内存。 thread(); thread(); } 上面采用的是 “写回” 的工作方式,即高速缓存的数据变化后,不会立即同步到主内存中。只有在一定时机触发了,比如缓存块需要被替换的时候,才会将缓存写入主内存。 除了 “写回” 的方式,还有一种是 “写直达”。即高速缓存中的数据变化后,会理解写入主内存。但这种效率很低,一般都不用。 上面的例子是 “时间局部性” 的体现,因为 flag 会被短期内多次使用运算,所以寄存器会从高速缓存多次取值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 例子 2 typedef struct _person { int age; int sex; } Person; int nums = [0,1,2,3,4,5,6,7,8,9]; void main() { Person p; p.age = 18; p.sex = 1; for (int i = 0; i < 10; i++) { print("the num is %d", nums[i]); } } 上面的例子是 “空间局部性” 的体现,因为 Person 结构体和 nums 数组的内存是连续的,CPU 在取值的时候,并不会一次只取需要的大小到高速缓存,而是一次取批量数据。上面的 Person 的 age 和 sex 就会一次性读入高速缓存,而 nums 里面的 10 个数字也会一并读入。这样在读取 age 完毕后,就不需要重新将 sex 从内存读入高速缓存之后再读取,而是可以从高速缓存直接读,nums 的第 0 位使用完毕后,可以直接使用第 1-9 位,也不需要再次从内存读。 所以高速缓存是从硬件层面极大的提高了程序的运行速度,甚至软件上为了适应高速缓存,还会做内存对齐为了能够一并读入高速缓存。 可见性 上面介绍了高速缓存的作用,下面分析高速缓存下共享数据安全性问题。上一趴说单核的时候,都没有说高速缓存。是因为单核下高速缓存对数据安全是没有影响的,这时候高速缓存有效的提高了生产力,但是到了多核后,高速缓存会产生严重的缓存一致性问题,也就是可见性。 1 2 3 4 5 6 7 8 9 10 11 12 13 // 高速缓存导致的内存不可见 mov &lock, 0; // CPU0 void core0() { mov &lock, 1; } // CPU1 void core1() { print("the flag is %d", *lock); } 如上图分析的流程,当 CPU0 将 lock 读取到高速缓存并且将值由 0 置为 1 后,CPU1 打印 lock 的值,依旧为 0。这就是可见性导致的共享数据不安全,即一个核心的缓存数据对另一个核心不可见。 上面只是非常常见的一个例子,我们一直都在使用多核 CPU,在写多线程代码的时候上面的共享数据的场景会经常出现。因为多个线程中,很可能是由多个核心分别执行的。由此可见,在高速缓存的情况下,数据也不再安全。 上面例子非常简单常见,很容器会出现问题。因为容易发生,所以灾难非常大。实际上为了解决这个问题,可是耗费了太多太多的精力,下文可见性解决方案中会讨论。 但是这里要特别说明一点,即上一趴说单核的时候没有说高速缓存的影响,实际上高速缓存只会产生多核场景下的缓存不可见。在单核情况下,因为只有一个核心,所有任务都只使用同样的 L1 和 L2 缓存,不会出现可见性问题。即上面的例子如何在单核下执行,最终打印的 flag 值是多少,完全取决于两个函数哪个先执行。 CPU 乱序场景数据安全问题 也许可以用 “智商不够,努力来凑” 吧,当初冯诺依曼和图灵搞出来的计算机原型,就一直在修修补补的路上,一修补就是几十年。也许这就是计算机的最优解,也许并不是。那个时候啥都是起步,如果从这一刻起有机会翻头重来,我认为科学家们是能够弄出更高效的计算机原型的。但是随着商业化的车轮滚滚向前,就连 x86 指令集都只能不断的兼容无法完全翻身,所以这样的机会肯定是没有的。 CPU 的摩尔定律十几年前就已经失效。虽然这些年还是在不断进步,但都是另辟蹊径。晶振的频率完全可以提高,但目前的 CPU 撑不住更高的频率了。奔腾 4 当年朝着 10GHZ 进军,随着过深的流水线设计,不仅没能提升指令吞吐率,更导致了过大的散热问题,最终退出 10GHZ 的研发赛道,后面各类 CPU 主频只能在 4GHZ 停滞不前。摩尔定律之所以还在正常跑,就是因为从 CPU 硬件层面修修补补了一大堆不可思议的骚操作。 CPU 乱序这一趴,我主要参考了徐文浩老师的《深入浅出计算机组成原理》,因为他写的是在太好了,配图也浅显易懂,我就直接拿图,大家也能看的明白。 这里呢,郑重的提前说明一下,CPU 乱序,不会产生有序性问题。不管你相不相信,但就是不会产生。 如果你明了,这一趴可以不用看。如果你不明了,这一趴一定要看看。因为我查阅资料的过程中,太多太多的同学把 CPU 乱序做为 “有序性” 不能保障,从而做为共享数据不安全的依据。我们不求知识多,但一定要求知识准确。 流水线设计">单指令周期处理器 => 流水线设计 CPU 乱序产生的原因就是 CPU 为了不断的提高运行效率,好追上摩尔定律的速度,在流水线设计基础上不断加骚操作导致的。这里先说明一下什么是流水线设计。 在开荒初期,对于每个指令的执行是串行的。每个指令需要的时钟周期数量不等,最少也是一个时钟周期用于取指。而时钟周期可以由晶振控制,即时钟周期可长可短。 如果有 100 个指令需要执行,所以我们就希望这 100 个指令的总时钟周期和每个时钟周期耗时足够小,这样就可以提高程序的执行效率。 前面说了每个指令最少需要一个时钟周期用于取值,如果没有更多运算,那么这个指令最少需要一个时钟周期。所以如果我们能控制 100 个指令的总时钟周期为 100,即最少时钟周期数,那么每个时钟周期越小,执行效率也就越高。 但是现在就遇到一个问题,发现这个时钟周期可以小,但是小的度量却没法确定,即如何才算小。如下图: 有些指令非常简单,很快就可以完全。有些指令则比较复杂,需要更长的时间才能执行完成。这里我们就只能根据木桶理论,使用那个更长的时间做为时钟周期。如下图所示,但凡时间短一些,就会导致某个执行没有执行完成,会导致异常。而使用更长的时间做为时钟周期,一来短时间能够执行完毕的指令则 CPU 需要空等,再者多长的时间才够长呢?这好像不是一个好的解决方案。 上面说的就是单指令周期处理器,在开荒初期,可以这么用。那时候指令还非常少,业务的复杂性也不是很高。后期就全部推向流水线的设计了。 因为每个指令最少需要一个取指的时钟周期,那么复杂指令也就需要更多的时钟周期。上面说的 100 个指令的执行效率上,就不在考虑总时钟周期数,而是考虑每个时钟周期耗时如何足够小。 一个指令的执行周期,可以分为取指、译码、执行三个阶段。这样我们就可以用三个阶段里面的最大耗时做为时钟周期,这个时钟周期肯定比上面 “但指令周期处理器” 时候要小得多。这里执行阶段最为耗时,因为有 ALU 运算、数据写回等操作,那么把执行阶段进一步细分为指令执行、内存访问、数据写回,就把一个指令的执行周期分为了 5 个阶段,这 5 个阶段里面的最大耗时肯定比刚才 3 个阶段的最大耗时低,相应的时钟周期又会低很多。 我们这里先分成 5 个阶段来考虑。虽然分成 5 个阶段,时钟周期比 “单指令周期处理器” 耗时低了很多,但 “总时钟周期” 也变多了,所以整个程序的执行效率或许并没有优化多少。或许参考了福特汽车的流水线工艺,发现可以用在 CPU 指令执行设计上,科学家们就又弄出了 “流水线” 设计来分摊 “总时钟周期” 的增多量。如下图所示: 因为同一时间只能进行一次取指,也只能进行一次译码,也只能进行一次地址总线的访问进行内存读取。所以科学家们设计了上图的指令执行流程,即取指电路把上一个指令的取指操作完成后,立刻进行下一个指令的取指,因为有时钟周期做为每个执行阶段的校准,所以下一条指令的每个执行阶段相比上一个指令都延后一个时钟周期。那么,在指令的最终执行过程中,就会出现 5 条流水线一同执行的场景,如下图所示: 即在第 5 个时钟周期之后,同一时间实际上有 5 个指令在并发执行。这样通过并发的方式的确可以有效的平摊 “总时钟周期” 的增量。而且,如果觉得 5 个阶段还是分的太少,那么可以将一个指令分成 10 个阶段,这样每个时钟周期的耗时会进一步降低,而且会产生 10 个指令的并发执行。 这里将每个指令分成 5 个阶段或者 10 个阶段,其实就是 5 级流水线和 10 级流水线。现在一般能做到 14 级流水线深度。但也不是越深越好,奔腾 4 曾经希望做到 31 级,但是因为流水线本身需要处理很多复杂性,导致 CPU 的设计越发复杂,而且复杂过后发现效率并没有提升,反而增加了十分严重的耗电。 这里的复杂性就是下面要说到的冒险和预测。其实单纯的流水线并不会乱序执行,因为每条指令的执行阶段,都比上一个阶段晚一个时钟周期,这样步步紧逼的扣环操作,使的上一个执行的执行结果对于下一个指令的执行过程完全可见,所以就和串行执行是一样的。 但是为了更快更强,冒险和预测就登上了历史舞台,使的不同指令的每个阶段的执行过程完全是乱序的,即 A 指令的内存写回或许还没执行,下一条 B 指令就已经拿到 A 指令的执行结果开始参与 ALU 运算了。 乱序也有一个乱的保障,即保障单线程场景下的数据准确性。 冒险 - 结构 冒险总共有结构冒险、数据冒险和控制冒险。这里先说结构冒险,如下图所示: 同一时间,CPU 无法同时操作地址总线对内存做读取两个操作,这里会有冲突,一个是读之令,一个是写数据。 当初设计计算机原型的时候,对于内存里面指令和数据的存放,有两个意见。一个是哈弗架构,即数据和指令分别存放在两个内存里面,这样就会有两条内存总线,可以同时支持读取。但是冯诺依曼架构没有采取这种方案,使的数据和指令是一起存放到同一块内存里面,这就使的上面的结构冒险不可避免。 不过呢,不过。上面在写高速缓存的时候,大家有没发现对于 L1 Cache,有两个,一个是 L1 D Cache,一个是 L1 I Cache。其实 L1 Cache 是使用了哈弗架构,即数据和指令分别存放。而因为 CPU 和内存之间难以逾越的速度鸿沟,使的目前 CPU 都是直接操作 L1 缓存的,所以这就使的指令和数据是可以同时读取的。虽然这样婉转的解决了上面的结构冒险问题,但也实属瞎猫碰到死耗子,赚了。 冒险 - 数据 数据冒险就是数据依赖问题,如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 场景1 int main() { int a = 1;// 指令1 int b = 2;// 指令2 a = a + 2;// 指令3 b = a + 3;// 指令4 } // 场景2 int main() { int a = 1;// 指令1 a = 2;// 指令2 } // 场景3 int a = 10 + 5; // 指令1 int b = a * 2; // 指令2 float c = b * 1.0f; // 指令3 前面我们说了,乱序也有一个乱的保障,即保障单线程场景下的数据准确性。 所以场景 1 中,指令 3/4 必须等待指令 1 写入完成。场景 2 中,指令 2 必须等待指令 1 写入完成。场景 3 中,指令 3 依赖指令 2,指令 2 又依赖 指令 1。 前面说了多级流水线,上一个指令取指完毕,下一个指令就开始取值了。很有可能会出现有一个指令的 ALU 执行完毕,还没有写入高速缓存或者内存,下一个指令的 ALU 就已经开始执行了。这个时候就会出现上面的依赖问题,比如场景 1,如果指令 1 的高速缓存操作还没有完成,即高速缓存里面 a 还是 0,还不是 1,这个时候指令 3 开始进行 ALU 运算了,那么拿到的 a 就是 0,计算结果就是 a = 2。这显然不符合单线程场景下的数据准确性了。 为了解决这个问题,就提出了插入无效指令即 NOP 停顿的解决方案,如下图所示: 可以看到,就是单纯的让该指令的某个阶段无操作,空等。等上一个指令的某个操作必须完成,才开始继续执行。比如刚才说的场景 1,指令 3 就是要等到指令 1 的写高速缓存执行完成后,才能继续执行,否则就会出错。 其实上面说到的结构冒险,虽然 L1 高速缓存解决了内存同时读取的问题,但是如果同时写回到主内存,也一样会冲突。在上面高速缓存的图解里,大家可以看到 L3 高速缓存的数据可以写入主内存。如果两个指令要同时执行这个操作,还是会冲突。解决方案也和这里一样,把某个指令加入一个 NOP 停顿,空等上一个指令写入完毕,下一个指令再写入。 但是增加 NOP 停顿的方案,其实就是最省事和低效的方案,不仅没有提效,还让 CPU 空转降低了性能。所以呢,就出现了乱七八糟的其他解决方案,如操作数前推: 在代码的场景 1 中,指令 3 依赖 指令 1 将 a 的值写入高速缓存才能读取并执行。那么就不等指令 1 的写入了,直接把指令 1 产生的值,通过流水线寄存器直接给到指令 3,指令 3 拿到了同样的数据,直接开干(真 TM 是鬼才)。这种方案叫 “操作数前推”,字面意思,也好理解。 但是 “操作数前推” 也不是都能解决问题,比如下面这个: 代码是这样的: 1 2 load ax, &flag; add ax, 1; 这里 add 指令的 ALU 执行阶段,必须要 ax 寄存器里面的值。但是 load 指令必须要执行读取高速缓存的操作后,才能拿到 ax 的值。这里,操作数无法前推了,只能再次加入 NOP 停顿来解决。 科学家们总是不愿意停止脚步,想要榨干 CPU 最后一丝气息。科学家们怎么也想不到,他们耗费那么多的精力让 CPU 越发高效的结果,就是使得许多人只是用了 CPU 0.1% 的能力不断的刷抖音。 1 2 3 4 5 6 7 8 9 10 11 12 // 场景4 int a = 10 + 5; // 指令1 int b = a * 2; // 指令2 float c = b * 1.0f; // 指令3 int x = 10 + 5; // 指令4 int y = a * 2; // 指令5 float z = b * 1.0f; // 指令6 int o = 10 + 5; // 指令7 int p = a * 2; // 指令8 float q = b * 1.0f; // 指令9 科学家们发现 NOP 停顿有时候也在所难免,还能不能更高效?上面场景 4 的代码,就发现虽然有依赖,但是指令 1、3、7 是可以提前执行的。其他的指令对 1、3、7 有依赖的,那么能前推就前推,要加 NOP 的就加 NOP。所以呢,丧心病狂啊: 我们写好的手动排序的代码,直接给乱序执行了。即指令 2、3、5、6、8、9 还没有执行完,指令 1、3、7 就已经执行完了。所以我们可能会发现,在多核场景下,尤其高速缓存那一趴说到的,这时候肯定共享数据不安全了。比如场景 4 如果在 Core0 里面执行,那么 Core1 看到的 b 还没有计算出来,x 和 o 就已经能够取值了。这显然会导致共享数据不一致。真实情况真的是这样吗? CPU 乱序不会导致共享数据不一致! 上面的数据冒险,即 CPU 乱序。其实这里 CPU 做了一个保障,如下图所示: CPU 的乱序只出现在指令执行阶段,即取指、译码、访存、写回这些,都是顺序执行的。 上面的场景 4 里面,虽然指令 1、3、7 被提起执行了,但是 CPU 有一个重排序缓冲区,这个缓冲区会对场景 4 里面的 9 条指令按照真实顺序排好序输出,即最后输出的顺序依旧是:a,b,c,x,y,z,o,p,q。 CPU 乱序,是为了指令执行阶段的高效,但并没有使指令的最终顺序变紊乱。这个和 TCP 协议的分包传输就 100% 相似,TCP 协议里,虽然 Msg 被分成很多个包独立传输,但是最后还是要排序才能确定是否接受消息成功。如果有一个包没有收到,那么还会有多次协商等待。和刚才的 1、3、7 号指令就是一个模子出来的。 CPU 乱序不会导致有序性,真正的 “有序性” 是下文中要说的编译器和解释器导致的。 冒险 - 控制 和本文主题无关,不说了。 预测 - 静态分支预测 & 动态分支预测 和本文主题无关,不说了。 数据安全产生的编译器 & 解释器原因 有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=1;b=2;” 编译器优化后可能变成 “b=2;a=1;”,在这个例子中,编译器调整了语句的顺序,但是不影响单线程程序的最终结果。但是在多线程情况下,就会有问题了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 文本代码 int a = 0; int b = 0; void thread1() { a = 1; b = 2; } void thread2() { while(b=2) continue; asset(a=1); } 上面代码如果在单核多线程中,按照预期,thread2 的 asset 一定能够通过。但是偏偏编译器或者解释器在处理的时候,因为种种原因将 thread1 修改成了下面这样: 1 2 3 4 void thread1() { b = 2; a = 1; } 这就出问题了。当 thread1 执行完 b = 2 这个指令后,thread2 的 while 循环被打破,但是执行 asset 的时候却不通过,因为 thread1 的 a = 1 还没有被执行。 编译性语言先编译在执行,像上面的示例。解释性语言是即时翻译执行,但也会出现上面的翻译优化。而且现在很多语言在编译性和解释性上已经不分家了,比如 Java,所以编译性解释性语言都会有上面的问题。 这个就是因为编译器或者解释器产生的有序性问题,也会导致共享数据不安全。 共享数据不一致原因总结 上面分别通过单核讨论了原子性、多核讨论了可见性、编译器 / 解释器优化讨论了有序性三个重要的问题,其实这就是共享数据安全的三大核心。 这三个原因都会导致共享数据不再安全,使得我们写的代码稍不注意就会有错误风险。 如果要解决共享数据安全问题,就可以从这三个方面找切入点。而且这三个核心因素之间还有一层间接的联系,即三大核心的包含关系。 为什么有包含关系,后面会再说。数据安全的处理方式随着范围的增加,效率也递减。所以数据安全处理第一步,就是不要跨范围处理,即能通过有序性解决,则不要通过可见性 / 原子性解决。能通过可见性解决,也不要通过原子性解决。 但是一般事与愿违,基本开发人员都是锁走天下,通过汇编或者不常用的描述符解决可见性、原子性等情况,还不利于团队协同。 但有些语言对这些汇编做了一层高级语言对封装,如 volatile,C/CPP 和 Java 都有不同程度的包装,如果遇到了非原子性问题,那么我们可以通过 volatile 来方便的解决可见性或者有序性问题。在下面的解决方案一趴也会说到。 数据安全阶梯式解决方案 为什么是阶梯式解决方案呢,也就是上图中的包含关系。但是这个包含关系并不是它们之间真的有包含,而是他们的解决方案有包含。即如果要解决可见性问题,那么有序性问题就一定要先解决。如果要解决原子性问题,那么可见性也要先解决。所以就从最小范围的解决方案开始说起。 有序性解决方案 也没啥特效药。编译器或者解释器,都会提供一些前缀给开发人员。开发人员只要觉得一个问题能够单纯通过有序性来解决,那就可以这么做。 比如 C/CPP 中,可以通过下面禁用: 1 2 3 4 5 #pragma GCC push_options #pragma GCC optimize("O0") void func() { } #pragma GCC pop_options C/CPP 中还提供了 volatile 描述符,该描述符可以停止编译器优化,也可以直接用。Java 中也提供了 volatile,比 C 的 volatile 含义要丰富很多,也可以停止编译器优化。 可见性解决方案 这一趴真是一个大工程。共享数据安全性很难弄清楚,就是因为前置知识细节太多了,而且很多细节还是硬件基础性的。 在多核那一趴,说到了可见性,也没过多描述,当时看图解的执行流程,就能知道数据不安全了。 1 2 3 4 5 6 7 8 9 10 11 12 13 // 高速缓存导致的内存不可见 mov &lock, 0; // CPU0 void core0() { mov &lock, 1; } // CPU1 void core1() { print("the flag is %d", *lock); } 描述不多,实则问题就越大。为了解决可见性问题,硬件和高级语言都引出了各自的内存模型,这可真是一个大工程。 可见性 - 高速缓存内存一致性 (MESI) 因为可见性是多核 + 高速缓存引起的,是硬件原因导致的,所以硬件工程师们想了一些办法,来处理多核场景下高速缓存数据一致性问题。 首先想到的是 “写传播” 方案。即然多个高速缓存的数据不一致,那么就再数据有变动的时候立刻让他们一致。即一个核心高速缓存改变了缓存块的数据,立刻同步传播到其他核心的高速缓存。 如上图所示,这就是 “写传播” 方案。方案名词如下: PrRd:Processor request read action,处理器发起读请求,向高速缓存读数据 PrWr:Processor request write action,处理器发起写请求,向高速缓存写数据 BusWr:总线的写通知。如果一个高速缓存有写操作,可以产生 BusWr 事件,其他核心会收到 BusWr 事件并作相应处理 Valid:状态机里面的准确态。即一个高速缓存的缓存块数据,是和主内存一致的。这个时候读高速缓存的数据,等同于读主内存。 Invalid:状态机里面的实效态。即一个高速缓存的缓存块数据,是和主内存不一致的。这个时候不能读高速缓存数据,因为不准确。 这个方案有下面几个约束条件: 所有缓存写操作直接 写到 主内存,即 “写直达” 策略 缓存操作是否 读 主内存根据当前状态是否失效判断 上面图示中的 a、b、c、d、e 5 个状态就是缓存块的所有生命周期。状态有 2 个,外部事件有 3 个,本来应该有 6 种情况。上图中少的一种就是 I 状态下收到 BusWr 事件。这个状态下无需处理,因为当前状态是失效状态,需要 PrRd 的时候会触发 d 生命周期,直接读取主内存,所以这个时候完全不需要触发啥动作,前 5 个状态已经能够保障数据安全了。对应上面代码的流程描述如图: But,“写传播” 的性能消耗非常大,在实际运作过程中,Bus 总线时时刻刻都在做时间的广播。而且写高速缓存的操作必须立刻同步到主内存,这会有很大的性能开销。所以后面就又弄出了伟大的 MESI 缓存一致性协议。 MESI 和 “写传播” 的不同点如下: 所有缓存写操作,仅仅写入高速缓存,不在写入主内存。采用 “写回” 策略 总线事件有了很大的增强,如下: BusRd:总线侦听到一个来自另一个处理器的读出缓存请求; BusRdX:总线侦听到来自另一个尚未取得该缓存块所有权的处理器读独占(或者写)缓存的请求; BusUpgr:侦听到一个其他处理器要写入本地缓存块上的数据的请求; Flush:总线侦听到一个缓存块被另一个处理器写回到主存的请求; FlushOpt:侦听到一个缓存块被放置在总线以提供给另一个处理器的请求,和 Flush 类似,但只不过是从缓存到缓存的传输请求。 状态机增加了两个状态,现在有 4 个状态: Modified(M):缓存块有效,但是是 “脏” 的,其数据与主存中的原始数据不同,同时还表示处理器对于该缓存块的唯一所有权,表示数据只在这个处理器的缓存上是有效的; Exclusive(E):缓存块是干净有效且唯一的; Shared(S):缓存块是有效且干净的,有多个处理器持有相同的缓存副本; Invalid(I):缓存块无效。 MESI 协议那可以说是相当复杂了,很容易绕晕。 首先需要明确 M、E、S、I 四种状态,分别阶梯式说明: E (Exclusive) 状态:当前共享数据只被一个核心的高速缓存拥有。这个时候是独占的,等同于单核场景。 S (Shared) 状态:当前共享数据被多个核心的高速缓存同时拥有。这个时候多个高速缓存里的数据和主内存是一样的,相当于多个核心都做了读请求,主内存数据加载到了多个核心中。如果有 4 个核心,那么现在 4 个高速缓存里的数据都一样,并且和主内存一样。 M (Modified) 状态:这个时候,当前缓存的数据是最新的,也就是最准确的。在 E 状态如果核心做了写操作,那么会把数据写入高速缓存,但不是写入主内存。这个时候缓存和主内存不一致,就是 “脏” 的修改状态,会从 E 状态迁移到 M 状态。同时,如果之前处于上面的 S 状态,这个时候一个核心做了写操作,也会把当前核心的状态从 S 变为 M,当前核心的数据就是最新的,同时把其他三个核心的状态全部标记为 I 状态,因为其他三个核心的数据都不在准确了。 I (Invalid) 状态:就是失效状态。上面 M 状态里面有一个说明,三个核心的高速缓存从 S 迁移到 I 以标记当前数据不是最新的了。如果读取 I 状态的数据,要看当前数据有没有被其他核心拥有。如果没有,那么当前核心就是独占,就会迁移到 E 状态。如果已经有其他核心拥有,那么就是共享状态被多个核心持有,就会迁移到 S 状态。 MESI 的状态迁移有一个特点,就是 Core 处理器对当前高速缓存发起的 read 和 write 请求,总会将 E、S、I 三个状态往 M 状态迁移,而 Bus 总线发起的通知事件,总会将 E、S、M 三个状态往 I 状态迁移。 状态迁移流程如下图所示。为了方便理解,我把上面代码的迁移过程也标注在图上: 至此,MESI 通过不立刻写入主内存的方式,有效的提高了 “写传播” 带来的损耗。也通过硬件层面解决了共享数据不安全的问题。 But,MESI 的总线事件比 “写传播” 方式更多更复杂了,这也一样有很大的性能消耗。 可见性 - 内存一致性优化 (Store buffer / Store forward / Invalid queue) 丧心病狂! 从上图中,可以看到当处于 S 和 I 状态的时候,如果产生了写操作,会迁移到 M 状态,而且产生 BusUpgr 事件。在其他状态下,都基本不产生啥事件。所以如果不是多核共享数据的场景下,MESI 的确非常有效的提高了效率,寄存器只要和高速缓存之间交互就好了。 但是上面 S 和 I 状态迁移 M 状态这种场景,科学家们无法忍受,要再做优化。原因是这个状态迁移,实打实的阻碍了 CPU 的执行: 当 A 核心从 S 迁移 M 的时候,首先需要发起 BusUpgr,并等待其他核心的回复。这里有一段漫长的等待耗时,CPU 是卡顿的。 其他 B/C/D 核心收到 BusUpgr 事件,需要将状态置为 I 状态。这个置为 I 的操作也是耗时的,虽然 CPU 没有卡顿,但需要时间。 其他 B/C/D 核心将状态置为 I 状态后,会向总线回复一个 ack 事件,分别告知 A 核心它这边处理完了。 A 核心收到 3 个 ack 事件后,才能将新的值写入高速缓存。写入高速缓存后,才能继续往下执行。 于是呢,比 L1 高速缓存速度更快的 buffer 出现了,如下图所示: Store Buffer 处于 L1 的更上一层,所以价格比 L1 更贵,容量比 L1 更小, 速度比 L1 更快。 所有对 L1 的写操作,都会先写入到 Store Buffer 中。CPU 写入 Store Buffer 后,就不管了,相当于是一个异步执行,直接开始执行后面的指令了。这样,就把上面的那 4 个步骤里面的步骤 1、4,原来通过 CPU 来完成,现在让 Store Buffer 去完成,CPU 不管了。 那如果 CPU 对 L1 写操作之后立刻执行读操作呢?上面 Store Buffer 是异步延后执行读,如果 CPU 立刻对 L1 进行读操作,肯定读不到数据。上图中 Store Buffer 上面还有一个指向寄存器的箭头,即 Store Buffer 可以直接返回数据到寄存器中。这个叫 Store Forward。即如果判断当前数据在 Store Buffer 中,那么当前 CPU 不会读 L1,而是直接读 Store Buffer。 增加了 Store Buffer 后,上面步骤 1、4 被 Store Buffer 接管。但是步骤 2、3 还是被其他 CPU 执行的,依旧会卡顿。这个时候就很容易出现 Store Buffer 不断塞入数据,而其他 CPU 还处理不完导致 Store Buffer 无法出数据。Store Buffer 很快就被延迟数据塞满了,毕竟容量比 L1 还要小。这个时候呢,丧心病狂啊,Invalid queue 来了。 如上图所示,增加了 Invalid queue 后,步骤 2、3 也不再由 CPU 处理了,而是直接将收到的 BusUpgr 事件存储到 Invalid 队列中,并立刻返回 ack,在后面 CPU 空下来后再执行 Invalid 队列里面的消息。所以这个时候高速缓存的状态并没有改变,只有等 Invalid 队列的消息被处理完成后,高速缓存的状态才会改变。 这样步骤 1、2、3、4 都被异步了,极大的提高了生产效率。。。。。。。。。百因必有果,程序员的报应就是这群丧心病狂的科学家,因为 Store Buffer 和 Invalid queue,又有新的恶魔。 可见性 - 高速缓存顺序一致性 (内存屏障) 上面还没说 MESI 的 Store Buffer 和 Invalid queue 优化之前,MESI 协议是共享数据安全的,我们也画图说明了。但是有了优化之后,共享数据再一次变得不安全了,如下图: Core0 把数据存入了 Store Buffer,这个时候其他 Core 要读取数据的时候,Core0 的高速缓存还不知道数据将要做修改,保持修改前的逻辑和其他 Core 通信。这个时候高速缓存的可见性其实还有,只是不准确了。过一段时间 Store Buffer 处理完成后,其他核又能看到数据了。那其实再并行编程中,哪个核先执行本身就是未知的,我们的程序一定要做一些依赖处理,比如循环等待某个变量达到预期,这个时候就知道其他核处理完了数据,就可以继续后续的任务了。所以上面的可见性问题,并没有达到无法接受的程度,鉴于程序本身的并行考虑,我们的程序也会做这层处理,只要等到可见性重新恢复即可。 偏偏 Store Buffer 和 Invalid queue 不仅仅导致短时间的不可见性,还引来了更大的问题,即内存顺序一执行。 比如下面示例: 1 2 3 4 5 6 7 8 9 10 11 12 // CPU0 void core0() { a = 1; b = 1; } // CPU1 void core1() { while (b == 0) continue; assert(a == 1); } 这个示例会因为 Store Buffer 和 Invalid queue 分别导致 assert 断言失败。 上图中把 Store Buffer 和 Invalid queue 一起操作,使得最后 assert 断言不通过。其实单独分析 Store Buffer 或者 Invalid queue,也一样会断言不通过。 这个就是 MESI 优化导致的顺序不一致问题。即一个核心对共享数据的修改顺序,对另一个核心不可见。上图中对于 core0 来说,应该是 a 比 b 先执行写入操作,但是在 core1 看来,b 先写入了,经过一段时间之后,a 也会被写入。这样的顺序正好和 core0 的执行流程是相反的。 为了解决顺序不一致问题,就引入了内存屏障。内存屏障直观来说就是操作 Store Buffer 和 Invalid queue。对于操作 Store Buffer 的,我们叫做读屏障,对于操作 Invalid queue 的,我们叫做写屏障。如果两个一起操作,那么就是全屏障。 所以上面代码加入读写屏障后,即: 1 2 3 4 5 6 7 8 9 10 11 12 13 // CPU0 void core0() { a = 1; write_barrier(); b = 1; } // CPU1 void core1() { while (b == 0) continue; read_barrier(); assert(a == 1); } 上面示例中,write_barrier 读屏障就是将 store buffer 立刻全部写入,写完成后才能继续后面的操作。而 read_barrier 写屏障就是将 Invalid queue 全部执行,执行完成后才能继续后面的操作。full_barrier 就是全屏障了。 有了上面的屏障,大家就可以发现,屏障就是使得 MESI 的优化失效,达到 MESI 未优化的状态。这个未优化状态我们前面说过,是共享数据安全的。但是会牺牲性能 这个内存读写屏障呢,就是硬件工程师手把手教会软件工程师的,然后往后的岁月里,软件工程师都得不断的注意要不要写屏障,哪里写屏障,写什么类型的屏障。硬件工程师教会了后,就不管了。。。 大家不要觉得夸张,现在所有的语言中,都离不开内存屏障。因为这是硬件层面的共享数据不安全,而语言需要适应多核处理器以需求更快的性能,所以没有一个能避开内存屏障。 但是我们其实很少写内存屏障的。就是上面说的,MESI 已经非常复杂了,如果还要让所有开发人员理解这套硬件知识来写屏障,那门槛太高了。屏障一般都通过语义化的形式在高级语言中实现了,比如 C/CPP/Java 里面的 volatile。我们只要理解并使用 volatile 的内存语义,即使不理解上面的 MESI 模型,也能写出具有可见性和有序性的代码。 大家注意上一句的结尾,只要使用 volatile,就可以解决可见性和有序性。前一趴在说有序性解决方案的时候,也提到了 volatile。其实这就是高级语言的好处,全包了。 高级语言递出的可见性解决方案 volatile 在 C/Cpp 和 Java 中分别代表那些含义 loadLoad /loadStore/storeStore /storeLoad 并不真实存在,只是 JVM 的一个概念。针对不同硬件架构,会有不同的指令集实现。 在物理原因那一趴,我们说明了高速缓存的不可或缺的重要性。在可见性这一趴,我们尝试通过 MESI 解决高速缓存带来的可见性问题。MESI 虽然能解决,但是性能遇到威胁,硬件上只能开启 MESI 的 Store Buffer 和 Invalid queue 优化。优化过后呢,可见性被延迟了,而且带来了严重的顺序一致性问题。引入内存屏障之后呢,将部分代码强制关闭优化,使的 MESI 仍旧可以解决可见性问题。 但是显而易见,内存屏障是关闭 MESI 优化,即内存屏障是有损耗的。我们只应该在需要的地方加,即共享数据不安全的地方加,如果哪里都加,那么就使得 MESI 性能遇到威胁。 这一大趴是数据安全的阶梯式解决方案,前面也说过,有序性、可见性、原子性是递进的包含关系。其实这里引入内存屏障后,编译器或者解释器就会默认关闭优化,即不会出现有序性问题了。比如上面的例子,可见性上我们用内存屏障做了那么多努力,结果编译器就是把 b=1 编译到了 a=1 的前面,那么我们用内存屏障解决什么问题呢?什么都解决不了了。 所以可见性的解决方案 - 内存屏障,默认已经解决了有序性问题,编译器或者解释器帮我们做了这一步。 volatile 可以解决有序性问题,即告知编译器不要优化,同时也会加读写屏障,保障可见性问题。 volatile 在 C/CPP 和 Java 中语义不同。因为两门语言对于各自的内存模型有不同的定义。 在 C/CPP 中,volatile 就是刚才说的,比较直接,就是告知编译器不要优化,同时对修饰的变量或者函数增加读写屏障。如下,注释做了说明: 1 2 3 4 5 6 7 8 9 10 11 12 typedef struct s_ATOMIC { //在变量前加上volatile, // 1. 是为了禁止编译器优化 // 2. 是在调用变量代码的前后增加屏障以清空 store buffer 和 invalid queue,使其每次获取的值都是和内存同步的 volatile s32_t a_count; }atomic_t; //原子读 static inline s32_t atomic_read(const atomic_t *v){ //x86平台取地址处是原子 return (*(volatile u32_t*)&(v)->a_count);// a_count 已经加了 volatile,本身是可见性安全的。这里再次加 volatile 是为了防止编译器对当前这一行做优化。 } Java 中 volatile 更加复杂,JMM 的内存语义增加了 LoadLoad、LoadStore、StoreStore、StoreLoad 四种内存屏障。一开始我不理解,现在我也不完全理解,就把我理解的先阐释出来,希望八九不离十。 JMM 是定义了一套内存语义,但不是实现。即 JMM 是一套标准,但具体怎么实现,得看实现方。 JMM 中定义了一套 Happens-before 语义,该语义定义了一套准则,使得要想保证操作 B 可以看到操作 A 的结果,A 和 B 就必须满足 Happens-Before 关系。 那么现在的问题就是 JMM 的这套 Happens-before 语义如何在 JVM 中实现,JMM 是理念,JVM 才是落地的产品。 在 JVM 的实现手册中,链接,明确说明了刚才说到的四种 load 和 store 的组合关系,如下: 即被 volatile 修饰的变量或者函数,在它的前面的指令和后面的指令之间,需要插入对应的 load x store 组合的屏障,以保障 Happens-before 语义。那 load x store 组合的屏障该如何实现呢? 在 JVM 实现手册的底部,给出了实现方案。原来 JVM 并没有抛弃前面说到的硬件层屏障,JVM 在 Happens-before 语义的实现上,为了兼容多个处理器架构,使用了各自架构对应的内存屏障。 上图中,我们上面说的 read_barrier 和 write_barrier 以及 full_barrier 就是 alpha 架构的。在表中,StoreStore 使用的就是 wmb,即 write_barrier。但是不明白为什么 LoadLoad 没有使用 rmb,而是使用了 mb。mb 即 full_barrier,LoadStore 和 StoreLoad 都是用 mb 实现的。 不同架构对于内存屏障的实现是不一样的,这里着重说一下 x86 和 ARM。 x86 的内存屏障只有 StoreLoad。x86 架构比较特别,它采用的是 TSO 模型,没有设计 Invalid queue,所以没有读屏障。Store buffer 设计成队列,即先进先出,所以缓存的写入是顺序的,不会产生顺序一致性。直观来说,x86 就没有可见性问题。但是 x86 提供了 StoreLoad 屏障,也并不是为了解决缓存一致性问题而存在,但这个屏障也是要被使用。我没有深究,我理解就是如果不加 StoreLoad 屏障,那么不会导致缓存一致性问题,但是会导致其他问题。所以上面的表中,JVM 在 loadload/loadstore/storestore 都加入了 NOP 无效指令停顿。NOP 在前面说流水线的时候说过,单纯的使得 CPU 空转一个时钟周期。在 storeload 处增加了 mfence 屏障。 ARM 的架构 和 Alpha 不同。ARM 没有使用 读写屏障,而是使用了单向屏障。分别是 stlr (store release register) 和 ldar (load acquire register),相当于 Alpha 的读写屏障吧,但是比读写屏障的范围大,功能也更大一些,但也代表着额外做的开销也大一些。ARM 还有 一个 dmb (data memory barrier) 指令,就是全屏障了。stlr 是 release 语义,有 storestore 和 loadstore 的语义,ldar 是 acquire 语义,有 loadload 和 storeload 语义。所以上表中,JVM 的四种屏障类型,应该通过 stlr 和 ldar 就能实现,而不需要使用 dmb。我理解上表中只是说对应屏障语义在各自架构上面的屏障类型,而没有细化到具体的实现。 有一点说明,在四种组合屏障里面,storeload 是最大的屏障,即 storeload 可以完成其他三个屏障的功能。所以一般 storeload 也都是用 full_barrier 实现,是代价最高的屏障。至于为什么 storeload 是最大的屏障,我初步理解就是先写后读的场景对数据的要求更严格这样子。 这里对高级语言的 volatile 做了详细的说明,但是 volatile 主要解决可见性,顺带解决有序性,至于原子性问题,volatile 是解决不了的。 原子性解决方案 终于要说到锁了,这里是分界岭。前面那么多内容都不是说锁,后面所有的内容基本都围绕锁转了。 本来这篇文章就是说锁的,为什么前面要说那么多铺垫?就是因为需要锁的物理因素太多了。如果物理因素不明了,那么业务中的那么多锁,该怎么用?哪个锁性能更高或者更贴合业务? 而且,如果我们的多线程代码本身没有原子性问题,但数据就是产生了紊乱,这个原因也不好分析。只好在注释里面加一句: 1 2 3 4 5 6 7 // 下面的代码会多线程访问。我分析过,没有原子性问题。不知道怎么回事线上就数据紊乱了。 // 我加了锁,上线后看好了。我不知道它怎么好的,但锁解决了我的问题。 void threads { lock(); xxxxxx unlock(); } 硬件层面支持指令原子性安全 在单核的时候,我们提到的原子性指令都是安全的,但是在多核并行的时候,就不再安全了。在并行 - 原子操作那节,我们用 cmpxchg 做为示例说明了原子指令的不安全。 所以原子性解决方案里面,第一步就是如何把那些 CPU 的原子性指令变得安全。因为这些指令不安全,那到底是用不是不用呢?不用感觉可惜,他们是很强大的指令,用吧就不安全。所以这个问题必须要解决。 解决原子性问题的方案有两种,一种是 Bus 总线锁,一种是高速缓存行锁。但具体使用那个锁,是由硬件决定的,我们要做的仅仅是加锁: 1 2 3 4 5 6 7 8 // xchg 内存交换指令 *lock = 0; mov ax, 1; void threads(void) { __asm__ __volatile__ "lock; xchg ax, &lock"; } 上面的 volatile 是为了防止编译器优化,为了防止有序性和可见性问题。lock 则是 xchg 的锁。 如果是总线锁,则 core0 开始执行 xchg 的时候,会将整个 bus 总线锁住,其他的任何总线操作都不允许执行。这样的性能开销非常大,所以出现了缓存锁。 缓存锁即对于 &lock 的缓存行加锁。如果 core0 加了缓存锁,那么其他核在访问 &lock 的时候,因为不同的高速缓存的 &lock 缓存行均被锁住,所以其他核心无法执行。只有当 core0 的 xchg 指令执行完毕,解开了缓存锁,其他指令才会继续执行下去。 所以通过总线锁或者缓存锁,就可以使得 xchg 和 cmpxchg 这些原子指令在多核并行场景下也能够正常执行。 其实还有一个中断问题,我们只要在 xchg 执行前关闭中断,在 xchg 执行完毕后打开中断,就可以解决。这个比较好解决,不在说明。 1 2 3 4 // 关闭中断 __asm__ __volatile__("cli": : :"memory"); // 打开中断 __asm__ __volatile__("sti": : :"memory"); 操作系统 & 高级语言支持任务原子性安全 在说到 xchg 的时候,我们说到它是自旋锁的实现,而 cmpxchg 则是 CAS 无锁的实现。其实在硬件的上一层,根本无法解决原子性安全问题。因为问题出在硬件,软件再怎么解决,也解决不了。 上面说的指令原子性安全,是硬件提供了 lock 指令实现了多核并行安全。而文章开头写到的 flag++ 任务是由多个指令组合而成,单纯从软件层面是无法使得这多个指令完成原子性操作的,因为硬件都无法保障。目前看,其实硬件层面实现的原子性安全操作也寥寥无几,就那么一些,比如原子增、原子减等。但就是这些个少有的原子性安全指令,被操作系统和高级语言玩出了花。 自旋锁 自旋锁是其他锁的基石,基于内存交换指令的 CPU 原子性安全指令 lock; xchg 实现。xchg 和 lock 已经在 “单核 - xchg 原子操作” 和 “原子性解决方案 - 硬件支持” 两趴说明过了。下面看看自旋锁如何实现。 自旋锁的原理是首先读取锁变量,判断其值是否已经加锁,如果未加锁则执行加锁,然后返回,表示加锁成功;如果已经加锁了,就要返回第一步继续判断其值是否已经加锁不断循环,因而得名自旋锁。 想要自旋锁正确运行,就必须保证读取锁变量、判断是否加锁、加锁这三个操作是原子操作执行的。因为一旦执行过程中锁变量被修改,就不能达到获取锁的正确性。而 lock + xchg 就可以做到这个。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 //自旋锁结构 typedef struct _spinlock_t { //在变量前加上volatile, // 1. 是为了禁止编译器优化 // 2. 是在调用变量代码的前后增加屏障以清空 store buffer 和 invalid queue,使其每次获取的值都是和内存同步的 volatile s32_t a_count; }spinlock_t; //锁初始化函数 static inline void spin_lock_init(spinlock_t * lock) { lock->lock = 0;// 锁值初始化为0是未加锁状态 } //加锁函数 static inline void spin_lock(spinlock_t * lock) { __asm__ __volatile__ ( // mark 1 "1:" // %0 即 mark3 下一行的 r(1),表示把 1 写入 %0 寄存器。 // %1 即 mark3 下一行的 *lock 地址。 // 这里把值为1的寄存器和lock内存中的值进行交换。 "lock; xchg %0, %1" // 用 0 值和 xchg 交换完毕后的 %0 寄存器的值进行比较 "cmpl $0, %0" // 不等于0,则上面 cmp 比较不想等。 // 这个时候获取锁失败,跳到 mark2 处开始自旋。 // 此时 %0 寄存器的值是 1,且 &lock 内存地址处值也是 1。即 xchg 等于没执行。 // 所以获取锁失败,相当于 xchg 没有执行,不会影响到 &lock 值。 "jnz 2f" // 若等于 0,则上面 cmp 比较想等。 // 这个时候获取锁成功,跳到 mark3 处继续执行。后面执行的就是自旋锁包围的业务代码了。 // 此时 %0 寄存器的值是 0,且 &lock 内存地址处值是 1。即 xchg 使得 &lock 的值从 0 变成 1,相当于加锁了。 "jmp 3f" // mark 2 "2:" // 这里就开始自旋了。不断的将 0 和 &lock 内存值进行比较。 // 只有当 &lock 内存值为 0 的时候,才表示上一个拿到锁的线程解开锁了。 // 如果比较值不等于 0,则继续回到 mark2 ,不停的自旋。 "cmpl $0, %1" "jne 2b" // 到这里,比较值等于 0 了,说明 &lock 内存值变成 0。 // 这里会尝试跳到 mark1 处重新获取锁。 // 但是这里不一定获取锁成功,因为多个线程可能同时获取同一把锁,只有一个线程会获取成功,其他的线程机会回到 mark2 的自旋状态。 "jmp 1b" // mark 3 "3:" : : "r"(1), "m"(&(lock->lock))); } //解锁函数 static inline void spin_unlock(spinlock_t * lock) { __asm__ __volatile__( //解锁把lock内存中的值设为0就行 "movl $0, %0" : : "m"(&(lock->lock))); } // --- 使用 int flag = 0; void threads(spinlock_t * lock) { spin_lock(lock) flag++; spin_unlock(lock); } void main() { spinlock_t lock; spin_lock_init(&lock); for (int i = 0; i < 10000; i++) { // 这里开启 10000 个线程并行并发操作 flag pthread_create{ threads(lock) }; } } 使用自旋锁,会产生非常大的性能消耗。因为在没有拿到锁的时候,会一直循环尝试获取锁,会使得 CPU 的使用率飙升,但只要上一把锁还没有释放,飙升的 CPU 使用率都是徒劳的。 但是自旋锁却又是最高效的,因为下一把等待锁的线程一直在尝试加锁,所以只要上一把锁被释放,下一把锁就会立刻响应。 毫不夸张的说,除了硬件层面的 lock 锁,自旋锁的所有锁中效率最高的。因为其他锁都是依靠自旋锁不断加临界区的判断条件,不可能效率上比得过自旋锁。 所以,目前对于自旋锁的使用都很谨慎,主要是担心过大的性能消耗。比较好的办法呢,是即使用自旋锁的高效率,又让自旋锁仅仅执行非常少的时间,这样就可以低消耗、高性能的使用自旋锁。所以操作系统和高级语言就依靠以自旋锁为底层实现,依靠银弹 “中间层” 这个神器,群魔乱舞了。介绍其他妖魔之前,再说一下自旋锁的优先级反转问题。 优先级反转字面意思,高优先级线程晚于低优先级线程执行。 普遍的理解是当遇到有资源依赖的时候,有依赖的线程会因为资源正在被使用而无法被相应,即使有依赖的线程优先级很高,但资源正在被使用,也没办法,这就使得其他无依赖的优先级低的线程早于高优先级线程执行完毕。这种资源依赖场景尤其为锁比较常见。我倒不是很理解,那你优先级再高,有资源依赖那也没有办法啊,谁让你有依赖呢。 再有其他的理解,就是高优先级依赖资源,该资源正在被低优先级持有。此时高优先级处于等待状态。但是因为高优先级能够获得更多的时间片,导致高优先级不断的抢占时间片做无作用的执行,此时低优先级无法获得更多的时间片导致任务一直完不成,加剧了高优先级被阻碍的时间。尤其是自旋锁场景,因为高优先级没有睡眠,一直在自旋,就导致低优先级迟迟获取不到时间片,卡死了。 基于上面的两种理解,最常见的做法就是提高正在持有资源的线程的优先级,使得持有资源阻碍后面高优先级线程的那个线程能够获得更多的时间片,从而尽快执行完毕,让出资源。 上面的解决方案也挺好的,但是这个优先级反转问题,却最终使得 iOS 上面的 OSSpinLock 自旋锁不建议被使用了。 我不是很理解,我觉得是苹果的工程师想摸鱼。有消息说 iOS 的 OSSpinLock 自旋锁,没有记录线程信息,所以不知道优先级高低,,,啊这,,,这不是虾扯蛋么。 所以 iOS 上面的 OSSpinLock 自旋锁不建议被使用,以至于现在各大开源项目都抛弃了 OSSpinLock 的原因,我认为,我个人认为,就是因为自旋锁本身的巨大性能消耗导致的。虽然优先级反转问题可以通过提升持有锁的线程的优先级来解决,但苹果工程师不建议开发人员使用,苹果工程师不想把这个具有非常大的性能消耗的地雷提供给开发人员。因为即使解决了优先级反转问题,自旋锁消耗的代价依旧是巨大的。开发人员们动不动的用 OSSpinLock,复杂场景下卡出翔了也不好。 除非,自旋锁里的任务能够被快速执行完毕,这就是前面说的,即使用了自旋锁的高效,也降低了 CPU 消耗。苹果提供的互斥锁、信号量也都是基于自旋锁做的,肯定是苹果工程师们相信,对于这些锁的内部,自旋锁的效率非常高,所以让开发人员都使用这些锁,就不会懵逼情况下使用 OSSpinLock 卡出翔了。(以上纯属我个人认为,也是一派胡言,因为我没查到相关原因,但总比网上的解释靠谱些) 信号量 信号量应该是对所有高级语言影响最大的锁,其他的锁都是单进单出,信号量可以做多进多出,这就可以控制并发流量了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 // 信号量实现,spinlock_t 即使用上面的自旋锁,spin_lock 和 spin_unlock 分别用于加锁核解锁 //信号量数据结构 typedef struct s_SEM { // 维护sem_t自身数据的自旋锁 spinlock_t sem_lock; // 信号量计数值,初始化 sem_count 为 N,限流就是 N。 // 如果 N 为 1,则相当于互斥锁 // 如果 N 不为 1,则为限流锁 sint_t sem_count; // kwlst_t 等待链数据结构,用于挂载等待代码执行流(线程)结构 // 这是自旋锁没有的。自旋锁里面,获取不到锁就一直轮询,这里将线程睡眠后放入列表,等待锁释放后唤醒 kwlst_t sem_waitlst; }sem_t; //获取信号量 void krlsem_down(sem_t* sem) { // start_step 用于线程睡眠唤醒后立刻跳转点。这里线程唤醒后立刻请求重新获取锁 start_step: // 该函数是获取信号量的函数,在该函数执行的全程都被自旋锁保护。 // 但是从下面的代码可以看出,当前自旋锁运行的时间非常短。 // 所以是自旋锁那一趴说到的,即使用了自旋锁的高效,又没有长时间运行消耗性能。 spin_lock(&sem->sem_lock); if(sem->sem_count < 0) {//如果信号量值小于0,则让代码执行流(线程)睡眠 krlwlst_wait(&sem->sem_waitlst);// 线程放入睡眠队列 spin_unlock(&sem->sem_lock);// 释放自旋锁 krlschedul();// 开始睡眠 goto start_step; // 唤醒后立刻跳到 start_step 开始抢锁 } sem->sem_count--;// 信号量值减1,表示成功获取信号量 // 成功获取到信号量后,释放自旋锁 // 此时当前线程已经拿到了信号量的入场券 spin_unlock(&sem->sem_lock); return; } //释放信号量 void krlsem_up(sem_t* sem) { // 这里释放信号量,也是需要获取到自旋锁后才能释放,防止 sem_count 紊乱 spin_lock(&sem->sem_lock); sem->sem_count++;//释放信号量 if(sem->sem_count < 0) {//如果小于0,则说数据结构出错了,挂起系统 spin_unlock(&sem->sem_lock); hal_sysdie("sem up err"); } //唤醒该信号量上所有等待的代码执行流(线程) krlwlst_allup(&sem->sem_waitlst); spin_unlock(&sem->sem_lock); return; } // --- 使用 int flag = 0; void threads(void) { flag++; } void main() { spinlock_t lock; spin_lock_init(&lock); sem_t sem; sem.sem_lock = lock; sem.sem_count = 5; sem.sem_waitlst = xxx; for (int i = 0; i < 10000; i++) { // 这里并行并发操作 10000 次,但同一时间仅支持 5 个并行 & 并发 krlsem_down(sem); pthread_create{ threads() krlsem_up(sem); }; } } 信号量就是上面代码实现里面说到的,是依靠自旋锁实现的。信号量很重要,它的流量控制能力,是其他锁没有的。已经足够高效,也没啥替代者。 互斥锁 “同一时刻只有一个线程执行” 这个条件非常重要,称为互斥。其实我理解,除了信号量语义上具有流量控制以外,其他的锁都可以称为互斥锁。因为信号量也有单个流量的控制,也可以说信号量也是互斥锁的一种。 这样看来,只要是锁,就应该互斥,就都属于互斥锁。因为锁的目标就是为了保障资源的安全,控制同一时间的线程访问数量为 1。 意义上,互斥锁实现的技术方案,就是内存中有一个地址,这个地址里面只有可能是 0 或者 1。如果是 1,就表示访问资源被加锁了,应该暂停访问。如果是 0,就表示资源无锁,可以正常访问。 所以上面的自旋锁就是完成这个技术方案的中流砥柱。 如果给自旋锁增加一些睡眠时间,就是 sleeplock。 如果给自旋锁增加等待队列,就是 mutex。 加锁的任务可能是线程,也可能是进程。那么操作系统就可以根据同一个进程加解锁设定临界区,同一个线程加解锁设定互斥量,这样就可以防止 A 加的锁被 B 给解了,增加并发并行安全性。 操作系统可以降低同一个线程获取锁的难度,即 A 线程获得了锁,在还没有解锁的时候,依旧可以再次获得锁,就生成了递归锁 。 操作系统可以将 mutex 的等待队列设置成串行,这样就是公平锁。还可以根据线程或者进程的优先级,设置成可调度锁。 以上的锁,都还只有一个获取锁的要素,即内存地址的值是 0 还是 1。高级语言在此基础上又增加一个对象的概念,即锁还需要和资源进行绑定,就变成了 synchronized 锁。 对读和写的多线程分别存储到两个队列中,就可以控制生成读写锁。 … 等等。从上面可以看出,只要在自旋锁的基础上,增加更进一步的约束限制,就多了一种锁出来。而业务需要多样性的锁,高级语言就可劲的造吧。 synchronized synchronized 锁在 iOS 里面是性能最低的锁,不知道在 Java 里面是不是。但是 synchronized 又是最简单实用的锁。 前面说到,高级语言可以将锁和特定的资源进行绑定,这样可以进一步增强锁的实用性。一个锁可以绑定 N 个资源,代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Person { // 1 修饰非静态方法 synchronized void foo() { // 临界区 } // 2 修饰非静态方法 synchronized void getMoney() { // 临界区 } // 3 修饰非静态方法 synchronized void setMoney() { // 临界区 } // 4 修饰静态方法 synchronized static void bar() { // 临界区 } // 5 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } } } 上面总共定义了三把锁,分别保护三个资源。 1、2、3 是同一把锁,保护当前 Person 的每一个实例对象。 4 是一把锁,保护当前 Person.class 类。5 也是一把锁,保护 obj 局部对象。 synchronized 底层肯定还是要使用到自旋锁这些,但是保护对象存储在哪里? 只能存在内存里!而且对这块内存的操作,还得加锁,以防止这块内存区域被破坏。这块内存可能会通过 hashMap 实现。 一般是先获取受保护对象的内存地址,以该内存地址做为唯一值做 hash,将该 hash 放入一个数据结构中。 此数据结构一般肯定包含一个自旋锁,还有一个等待列表做睡眠唤醒用,再标记当前正在使用锁的线程是哪个等等额外的信息。 然后就将该数据结构存入 hashMap 中。为了 HashMap 的安全,对 hashMap 的修改本身还要加一层互斥锁。 所以 synchronized 肯定是比其他锁要慢的,慢的主要原因是需要额外的存储 / 读取 synchronized 要保护的那个对象的信息。HashMap 从数据结构上设计的再高效,可还是需要额外时间的。 这里 synchronized 还有一个使用风险,即 synchronized 锁的是对象。如果被锁的对象变成 nil 了,那么这个锁就不在生效了。获取 nil 的锁,会直接返回获取锁成功。 所以在考虑 synchronized 锁的对象的时候,一定要小心,不是什么对象都可以用来上锁的。还要防止对象被释放,开始有锁后面无锁的情况。 无锁 CAS 无锁又能够保障共享数据安全,其实是有些不现实的。Java 里面的 CAS 是为了保障业务代码的安全性问题,但是业务代码是非原子性的。上面已经举了很多例子阐述了非原子性操作在并发 & 并行上的不安全。 这里的无锁,其实指的是操作系统层面的无锁。就是说,其实有一把硬件层面的锁,在保障着业务代码处于原子性的操作之下。 硬件锁前面也分析过了,就是 lock,依靠总线锁或者缓存锁实现的原子性并行安全操作。lock 是用来锁单核场景下数据安全指令的,CAS 就是用到了 cmpxchg 比较交换指令。 cmpxchg 的使用在 “单核 - 原子操作 - cmpxchg” 已经详细说了,下面看看 Java 里面的 atomicInteger 的递增如何通过不加编程锁实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // cmpxchg 比较交换指令 //在变量前加上volatile, // 1. 是为了禁止编译器优化 // 2. 是在调用变量代码的前后增加屏障以清空 store buffer 和 invalid queue,使其每次获取的值都是和内存同步的 volatile s32_t lock = 0; void increment() { int cmpAndSwap(inspect, &lock, value) = { // ax 和 lock 地址值做比较,想等,则 value 写入 lock。不等,则 value 写入 ax mov ax, inspect; __asm__ __volatile__ ("lock; cmpxchg(lock, value)"); return ax; } // 下面使用循环,如果两个线程同时进入,则两个线程的 bx 值相同 // 最后只有一个线程执行 cmpAndSwap 函数后,cmpl 比较后为想等 // 另一个线程则重新进入 do-while 循环,再执行一次后成功。 do { mov bx, &lock;// 用于cmpxchg 执行后和 ax 做比较 mov cx, bx; add cx, 1;// 对递后的期望值,cx = bx + 1 }while( __asm__ __volatile__ ("lock; cmpl bx, cmpAndSwap(bx, &lock, cx)")); } // --- 使用 void main() { for (int i = 0; i < 10000; i++) { // 这里并行并发操作 10000 次 pthread_create{ increment() }; } // 待 10000 次操作结束后,lock 的值一定是 10000 } 其实 Java 的 atomicInteger 实现,还是有不少损耗的。JVM 也是像上面代码一样,内部通过 do-while 循环实现的递增等安全操作。cmpxchg 不仅仅可以完成上面的操作,因为其特殊的 “比较交换” 的功能,可以在很多场景建立一个比较有效的数据安全隔离区。很多组件业务也都是通过 cmpxchg 建立比普通锁更加高效的原子性操作。 听说 CAS 的容易忽略的小问题很多,除了 ABA 问题,还有其他很多坑,一般不建议使用 CAS 做无锁。 对于 Java 的 CAS 实现,我是有个疑问,其实指令也提供了递增的原子操作指令,可以不用走 do-while 循环的,不知道为什么 JVM 没有用。 1 2 3 4 __asm__ __volatile__ ("lock; addl &lock, n");// n 可以为一个整数,原子的将 lock 内存地址 +n __asm__ __volatile__ ("lock; subl &lock, n");// n 可以为一个整数,原子的将 lock 内存地址 -n __asm__ __volatile__ ("lock; incl &lock");// 原子的将 lock 内存地址 +1 __asm__ __volatile__ ("lock; decl &lock");// 原子的将 lock 内存地址 -1 高效解决共享数据安全的典范 关于锁,还有一个中断问题,我们只要在锁的前后关闭中断打开中断,就可以解决可能会因为中断导致的锁的获取紊乱。一般锁都已经做了中断处理。 1 2 3 4 // 关闭中断 __asm__ __volatile__("cli": : :"memory"); // 打开中断 __asm__ __volatile__("sti": : :"memory"); 还有一个锁默认会做了的事情,就是可见性。所有的锁,都保持着比如 ARM 架构的 acquire 和 release 语义,和 Happens-before 类似。锁的实现里面,也和 valotile 一样增加了内存屏障。 而且,锁也会在获得锁和释放锁的前后,防止编译器或者解释器优化,以防止有序性。 这里要特别说明一单,可见性和有序性的处理,只针对获取锁之前和释放锁之后。被锁保护起来的区域,是没有做有序性和可见性的。因为这块区域有锁保护,都是单线程执行,基本可以保障安全。但也有例外,比如一会要说到的 “双重检查锁” 问题。 所以,有序性、可见性、原子性是递进的解决方案,他们的解决方案有包含。即如果要解决可见性问题,那么有序性问题就一定要先解决。如果要解决原子性问题,那么可见性也要先解决。 数据安全的处理方式随着范围的增加,效率也递减。所以数据安全处理第一步,就是不要跨范围处理,即能通过有序性解决,则不要通过可见性 / 原子性解决。能通过可见性解决,也不要通过原子性解决。 下面用 Java 里的双重锁验证,来做一个共享数据安全的说明: 1 2 3 4 5 6 7 8 9 10 public class Singleton { private static Singleton uniqueSingleton; private Singleton() {} public Singleton getInstance() { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); } return uniqueSingleton; } } 上面是一个单例模式的 Singleton 单例对象获取,在多线程场景下可能会出错。因为两个同时获取 uniqueSingleton 均为 null,导致单例被创建两遍。 为了保障安全,就进一步的加锁: 1 2 3 4 5 6 public synchronized Singleton getInstance() { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); } return uniqueSingleton; } 增加 synchronized 锁之后,是数据安全的。但是每一个获取单例的线程都需要验证下锁,也会有性能开销。就进一步提出了双重检查,以降低原子性的覆盖区域: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Singleton { private static Singleton uniqueSingleton; private Singleton() {} public Singleton getInstance() { if (null == uniqueSingleton) { synchronized (Singleton.class) { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); } } } return uniqueSingleton; } } 这样就是的锁的覆盖范围最低了。只有初次一同获取单例对象的线程可能会加锁,后期获取单例的线程都是无锁的。 但是上面的双重检查还不是安全的。上面说到锁解决的有序性和可见性的解决方案是加锁前和释放锁后,对于锁的保护区域是没有的。这里的风险是 uniqueSingleton = new Singleton(); 本来应该被如下执行: 创建内存 初始化内存(初始化变量等) 给 uniqueSingleton 赋值创建内存的内存地址 但是因为有序性问题,前面的 2、3 有可能被颠倒,就会导致 A 线程给 uniqueSingleton 赋值了内存地址,但是还没初始化。这个时候 B 线程就会发现 uniqueSingleton 不为 null,那么在 B 正常使用 uniqueSingleton 的时候,就可能因为变量还未初始化导致 crash 等异常。 所以呢,volatile 来帮忙: 1 private volatile static Singleton uniqueSingleton; 将 uniqueSingleton 被 volatile 修饰后,就可以防止有序性问题,上面的 2 和 3 操作就不会颠倒了。 这样,“双重检测锁” 下的单例,就非常高效了。 iOS 的单例比 Java 的好用一些: 1 2 3 4 5 6 7 8 + (id)sharedInstance { static TestClass *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } dispatch_once 直接做到了 “双重检测锁” 级别,开发人员直接用就好了。锁、有序性,都已经默认处理好了。 dispatch_once 不和 Java 一样使用的 synchronized 锁,而是使用了上面提到的无锁方案。使用了 os_atomic_cmpxchg 和 os_atomic_xchg,所以 iOS 的单例和 Java 有一点不同,就是等待队列。synchronized 是有等待队列的,所以没抢到锁的线程应该会睡眠等待。dispatch_once 使用的原子指令,好像是没有做等待队列,和自旋锁有点相似,没抢到锁的线程会不断的重试。当然,前面说自旋锁的时候也说过,这种方式是最高效的,就是有点费 CPU。所以 dispatch_once 里面不要做复杂操作。 锁与数据结构的必要性 现实场景下,有序性和可见性都是顺带被解决的,大家都是用锁。因为锁在解决原子性的同时,都强制处理了加锁前和解锁后的有序性与可见性。所以锁是并发 & 并行编程下的万能钥匙,但也很容易产生性能瓶颈。 高级语言提供的锁的种类很多,选择一个业务场景大致需要的锁,然后搜一下这个锁的坑,基本上就可以避免很多意外了。把刚才的话再反过来说一下好加强记忆:要搜一下需要的锁有哪些坑,否则就会掉到很多坑里面。 通过严格控制原子性的粒度,不需要加锁的任务不要放进锁里。还可以用不同的锁对受保护资源进行精细化管理,能够提升性能,叫细粒度锁。 毕竟锁的性能真的不高,单线程完全没办法享受到近十年时间的 CPU 快速迭代的红利。 前面我们说 synchronized 的实现的时候,说到 synchronized 需要锁一个对象,那么在加锁和解锁过程中,这个对象就需要根据一定的规则存储到一块内存区域,然后再通过相同的规则从内存区域拿到这个对象。那么存储这个对象的内存区域,就很可能会成为 synchronized 的性能瓶颈。 在锁的内部要保障性能,被锁住的资源也要保障性能。否则就会出现高并发 & 高并行场景下,获得锁的线程一个时间片处理不完,睡眠一下,可能几个时间片又过去了。后面等待的睡眠线程又不断积压。 那现在就卷起来了,并发场景如果确定需要高性能,除了要选用高效率的锁,更优的数据结构和算法,也是关键。 用 iOS 里面的 YYCache 的内存缓存举例,YYCache 使用 pthread_mutex 互斥锁,原子性粒度控制的很小,在对数据进行操作的时候才开始加锁和解锁。 缓存使用来 LRU 算法,通过双向链表来实现数据对增和删的复杂度为 O (1)。 但是链表的查询复杂度是比较高的,因为链表无法做随机寻址,也没法用数组的空间局部性缓存加速。 所以作者通过空间换时间的方式,引入了 hash map,将缓存数据存入 hash map 中实现查询复杂度为 O (1)。 这样,整体内存缓存的数据的操作复杂度都将为 O (1)。 iOS Runtime 里面也是各种 hash table 和 hash map table 的使用,hash 函数为了高效,也尽可能使用位操作来计算索引值。

2022/3/10
articleCard.readMore

江苏女有感

兵不荒马不乱的年代,却变成了荒诞。荒诞一词很严重了,得发展成啥样才能用荒诞来形容? 在老一辈人眼里,江苏女这事就不是大事。农村或多或少都有一个买来的媳妇。有的孩子也是买的,但没妇女多。 至于为啥舆论消不下去,那肯定不是网警过年放假了。有很多高技术数据研发人员,早就做到审查自动化了。很多人都不知道现在监控技术有多发达,但一直都被这些高科技服务着。监控技术发达是因为技术研究都被用在了数据分析和监控上,举全国之力。 江苏上峰一次次的发布公告,虽然背后是调戏了韭菜,但他们在发布公告之初是认为这公告代表的权威就是铁律,一小戳异议韭菜被和谐根本无足轻重。没人知道为啥这次舆论审查怎么不严谨了,上峰也不知道原因。 天网这么浩大的工程,还没发挥出重大价值。人身监控、肇事逃逸这些重大场景已经有实打实的落地,但那些小场面也应该介入进去,比如丢了电动车丢了妇女孩子什么的,可每遇到这些小场面的时候,摄像头总是坏掉。几年前我去警局看监控找电动车,警察小哥说 “你看,被挡住了”。我还看过这样的言论,说警察数量有限,管不了那么多琐事,脑回路就挺弯曲。人民日报批评星巴克驱赶警察那个,也很玄,真不知道哪个群体敢惹警察,尤其还是企业单位。 江苏女这事虽然棺材板没压住,过两天肯定就被解决了,在上峰眼里也不是啥大事。几百年后的后代们或许会惊讶,这事,就这么过去了?也或许他们会唱赞歌,赞美一位江苏兄弟给一流浪女长期提供食物和生活补助,写故事的人可不都天马行空么,尤其是无法考证的历史故事。 读书认字接受教育不为了听谎言,也不是反抗啥,至少保持独立分析能力。虽然被活在小世界里,但得知道还有天外天。 以前以为很多文章下面的评论肯定是水军,有一天发现那些评论都是真真实实的有学问的键盘侠打出来的,也惊呆了。 最后呢,希望这次江苏女事件,上峰最后能给个不糊弄版本的交代。毕竟当年蜘蛛网查封了食指的阳台的时候,食指还是坚定的向我们呼喊着:相信未来,热爱生命。 媒体, 以不持立场而神圣, 以不下定义得自由。 如果媒体不能保持新闻的中立性,那一定会变成利益的工具。

2022/2/20
articleCard.readMore

我的 2021

转眼一晃,一年又过去了。我和孩子都长了一岁。不过我们成年人感受到的时间飞逝,和孩子有一些异同。 孩子在一年过程中,是没心没肺的玩耍娱乐,这些经验和快乐,有些是没有记忆的,比如哪一天的楼下滑车。有一些是潜移默化的,比如哭闹后的面壁,让孩子对道理有深刻认识。对于孩子而言,一年中的经历,是无形中植入的种子,谁也不知道哪一天会发芽,或者早已经在孩子成长的过程中生根。其中弊利,谁也无法能给个断定。因为孩子的可能是巨大的,谁又知道这一刻的同伴辱骂,会让其走向懦弱暴躁或者虚怀若谷的两个极端,抑或只是无声无响等待被抹除的记忆? 我们成年人的一年和孩子是不一样的体感。一年的时间,并不是一个成年人的基本单位。有人一无所有,有人衣食无忧,有人捶胸顿足,有人把酒言欢,诸多改变,很可能都不是一年之内养成的。一个赌博的习惯培养了十多年,才有可能让一个中年人一夜走向穷困。一个二十多年的烟民,才可能在短时间内走向生命尽头。我们对一年内发生的每件事,都有自己的思考和认知,是有明确的利弊权衡,这和孩子的被动接受是有区别的。 时间是极为可怕的怪物,对任何事物都有一致的光阴长河。比如铁片在生锈、沙漠在升温、高山在变矮,孩子在长高等等。在光阴长河里,万事万物都在同一个度量单位里面平滑的向前移动。这种温水煮青蛙的方式对于万事万物都是一样的刻量,有生命的、有物质形态的甚至虚无缥缈的万事万物,都随着时光的流逝,不同程度的发生了不可逆转的变化。 对于我们这些置身于怪物掌控下的一粒沙来说,短时间内有大的行为波动是不现实的。就像那些一年内暴富的机缘,不可遇更不可求。我们都是在一步步的被时间打磨,一年时间,说长不长,说短也不短,值得做一个总结和回味,也值得在以后的岁月中慢慢咀嚼,毕竟人生能有几个一年? 又一年过去了,我也慢慢逼近三十岁的门槛。2021 年,不是一个平坦年,有人欢喜但更多的人是哭。我没啥机缘,所以没有啥特别欢喜的,感觉这一年也都是水到渠成的故事。哭呢,悲惨的事情也都没有发生在我的身上,我也没有哭的必要。当我觉得又得过且过了一年的时候,我还是想回顾一下,就翻看了年度相册作为事件源,发现能写的东西还是非常多的。个中甘甜与辛酸,均冷暖自知。 个人篇 我现在很胖了,体重直线往上升,160 斤的体重,看得我瑟瑟发抖。今年工作压力很大很大,烟酒没有停过,饿了么外卖的年度账单里面搞到小 5K,大部分都是夜宵了。 我 2020 年 9 月进入这个集中营后,试用期 3 个月,就开始了正式工的一年,这一年恰好都是在 2021 年经历的。工作不如意十有八九,晚上回到家就要吃东西喝酒减压,第二天继续集中营的生活。就这样,体重从 130 升到了 160,还没有停下来的意思。 今年我做了一个重大的外观上的改变,就是留长发了。之前也都是短长发,这一次我扎了辫子,我很喜欢。除了老婆,也没有人支持我,所幸在自己的坚持下,没有放弃。 今年依旧没有驾照没有车,感觉后面几年也都不会有。其实我心里很想骑机车,十一回老家打算搞一个摩托车驾照的,家里弄这个驾照很方便,但是老婆担心骑车危险,没让搞。C 照嘛,我和老婆都没有开车的兴趣,也都没有学。每次外出都是骑着电动车带着老婆孩子,天冷的时候挺受苦的。有一次带着口罩骑车的场景很像猩猩,老婆孩子笑了好久。 今年身心感觉不是很健康,和老婆说过想找个心理医生,后来也作罢了。公司也有免费的心里医生可以咨询,其实想的话完全可以去咨询。只是中国人的通病吧,感觉心里问题羞于表达和外漏,也不愿意和其他人诉说。 烟酒和夜宵的刺激,我现在身体差的厉害,自己也感觉到了。想着戒烟戒不掉,现在也开始习惯电子烟了。家人都反感我抽烟,尤其是爸爸,说世代没有抽烟的,出了我这么一个烟鬼和酒鬼。我听的长了,也就不感冒了,我知道烟酒也都是上瘾,和毒品类似。但是每当有烦心事的时候,我真不知道除了烟酒还有啥可以帮我减压,当然这也是我这个烟酒为自己的狡辩吧,毕竟不抽烟不喝酒的人那么多。 脱口秀看了一段时间,最后没有养成习惯。脱口秀里面都是段子,可是那些段子和抖音、视频一样,看了就忘,没有知识性,几乎没有价值。有几个晚上,我就刷着社交软件里面的短视频无限 feeds 流刷到大半夜,过后又发现毫无价值。带来了瞬间的笑点,可是过后的空虚和对于时间流逝的懊恼远远大于那个笑点带来的开心。那些不是愉悦,因为愉悦肯定是持久的,至少也会持久那么几个小时或者几天,不会像脱口秀和抖音那样。 《剑来》断断续续的看了不少,后面换成 kindle 看了。kindle 看书的确很爽,剑来写的非常不错。目前觉得好看的小说,屈指可数。有诛仙、哈利波特、平凡世界、鬼吹灯、三体这些,这一次到剑来了。这些小说,都不应该拍成影视,否者不仅仅侮辱了小说,也侮辱了看节目的人。可是总有很多资本,想着把那只欲望的手伸到这伟大的故事当中,然后一顿掀桌子。中国影视真的不能再差了,再差下去,就改叫媒体吧。 家庭篇 连续两年没有回家过年了,疫情给我们带来的影响还是很大的。 这一年也没有啥地方玩,远的不方便走动,近的玩了一遍又一遍。2020 年过年期间西溪湿地免费开放了,我带孩子去转了一圈,不好玩,2021 年不免费了,也就更没去了。其他游玩的地方,屈指可数,宝寿山、湿地博物馆、福堤、青山湖、南湖、西溪旺角这些。 吃的话,还是找到了好去处,就是菲吃布渴,我们吃了很多次,每次去都吃很多螃蟹,我非常喜欢那边的刺身和白葡萄酒,我现在超爱刺身了。其他的就是简单吃一些了,有羊老三、大骨头、汉堡王这些。大渔铁板烧是挺让人失望的,我去过几次,因为团队聚餐。我带老婆去过一次,老婆也是很不喜欢,还死贵,和菲吃布渴比起来差距太大。 今年还有一个比较重要的事情就是房子,房子交付了,年底前也全部装修好了,打算 2022 年中搬过去。装修找的远亲,也给了我们很多优惠,装的是很不错的,加上家电等杂七杂八的,目前总费用在 16W,还有一些家电没有买,打算入住的时候购置。房子整体是简洁风,客厅留了很大的空给孩子玩耍,装了一个大桌子用作吃饭和办公,加了一个墙柜用来放书和手办,整个客厅的家具目前都是木质的。对电视没有大的渴望,也就没有装,但也预留了投影仪的空。主卧没打算放床,单独买了金可儿的 2mx2m 床垫放在地板上,这一点的确没有一个人同意我,但是我还是坚持了下来。次卧软装还没有弄,打算搬过去后,让孩子选择她自己喜欢的装修风格。整个装修过程还是挺麻烦的,我 sketch 一遍一遍出图,计算沙发、大桌子、墙柜等等的位置,就怕弄错了导致杂七杂八的一些后遗症。 房子交付了,老婆也把她的户口迁过来了,主要还是为了孩子上学方便。迁户口迁了好几次,折腾了不少时间,因为社保原因花了 4500 左右。 今年整体来说,和老婆关系还是很和谐的,没有上一年吵架次数多。和老婆结婚 4 年多了,大家也都相互了解和理解了,更多的是我们相互都是尊重对方的,这一点难能可贵。老婆主要是教育孩子,孩子的成长的确很不错,至少比我们小时候好多了,一代比一代好吧。老婆总是埋怨性生活质量太低,一来这个出租房的床真不好,再者我也发觉当压力大的时候,对性的渴望也会降低。这一年我的压力是不少,来自很多方面。 闹闹已经陪伴我们有 6 年了,它给我们带来了很多快乐。因为闹闹比孩子大,现在孩子也会开玩笑的叫它姐姐了。经常我们出去吃饭,有骨头也会带回来给它。每次去菲吃布渴,我都会带两块羊排回来,它吃的可开心了。现在闹闹拉屎习惯是养成了,出去遛狗得拉 2-4 次屎,最后一坨呢,我们不睡觉它不拉屎,灯关了趟被窝了,阳台就飘来一坨臭屎味,可厌烦人了。后面搬到新家了,得给它弄个好窝。 我和父母没有常见面,更少有聊天,这一年尤其明显。他们经常开视频过来看孩子,也都是我老婆和孩子与父母聊长聊短。一来每次视频我都不在家,其次也是我刻意躲避他们。爸爸很厌烦我抽烟的,10 月份还给了我最后通牒。每次说我就有些烦,可是我又戒不了烟,也不敢面对他们。因为当年买房子花光了积蓄,当时给双方父母都购置了保险,又交了一年。 孩子篇 孩子明显长大了好多,每次到电梯,她都说自己能碰到哪一层的按键了,之前都不行。以前我都是叫她 “汤圆”,现在已经不自觉的在 “汤圆” 和 “安歌” 之间轮流喊了,尤其是生气的时候。今年是孩子成长最快的一年,出乎想象。 孩子上学了,中班,上了半个学期了。就这个幼儿园,孩子哭了差不多两个月才适应下来。孩子不想上学,经常和老婆说 “我不想上学了”,一到周末就很开心,到周一就很沮丧,像极了我这个上班族。 给孩子报了跳舞的班,孩子很喜欢,很多次给我们表演劈叉。当时报班,只是想让孩子从另一个方面了解规则,以熟悉和适应学校规则,因为她非常不喜欢上学,在学校也很不老实,到没想到她喜欢上了跳舞。有次老师给老婆发消息,说安歌不好好上课,还挑唆班里一个同学说要上卫生间,结果到卫生间两个人聊天去了。 孩子还学会了我到现在都不会的技能,一个是轮滑,一个是跳绳。这都让我有些惊讶,她现在轮滑可厉害了,至少我是这么感觉。 令我吃惊的是,孩子现在已经很安稳的骑车了。我教了她半小时左右,两轮子的车她完全上手了。在看到她骑车的时候,已经非常熟练了。想当年,我还是在 3 年级左右学会的那种超大的横杠自行车,有横梁的那种,就一只脚斜跨进三角形的横梁区域然后半蹬半蹬的那种。 孩子牙齿还是很不好,前段时间吃糖就粘掉了一颗牙。现在满口牙,除了 6 岁左右会换牙的那些,其他的都做了补牙或者根管治疗。生病次数倒没有多少,但滨江的儿童医院也跑了几次。 工作 & 学习篇 2021 年是我在公司真真实实呆着的一年,有太多的辛酸,索幸是坚持了下来。这一年绩效没咋好过,两次都是 3.5。整体来说,就是瞎忙活,做些无足轻重的业务,可有可无,无关痛痒。 工作本身并没有给我带来过多的惊喜,除了内网的一些技术文章挺有意思,其他的我能接触到的范围内并没有多少让我心动,这也是我经常有跳槽想法的原因。每个人只要按部就班的完成自己的工作,就已经可以保障工作的稳定和体面。可这种非必要无激情的工作,多少让人有些乏味。 我在公司感触颇深的是人,工作上与同事的交往和对大多同事的心里评价尤为有意思。 同事很多是自私主义者,不管六脉神剑的企业文化如何熏陶,这是无法改变的事实。这也是很多同事工作困难的原因。 擅长甩锅和说半句话,甩锅不用细说,就是把那些自身的责任,想一万个方法强加到别人身上,比如工作内容和故障。这里着重说一下工作内容的甩锅,就是一项工作明明一位同学做比较合适,但那位同学就一万个理由不做,让别人来搞,我经历的一位就是说业务价值不高不想做,最后升到主管那位同学才接下来。 说半句话就有些微妙了,你能想象你和别人说话,对方说半话,剩下的不说了?让人云里雾里的谈话,十分让人迷惑。好像什么都说了,但又摸不着头脑。但是环境对一个人的影响很大很大,现在有些时候我说话也会说半句,因为这种行为,一来不给自己惹麻烦,二来对自己工作干扰量少,当然不到重要时刻,我不会这样,我感觉这样不违法但缺德。说半句话还有一个表现,是打错字。这一点我是深恶痛绝,总有人经常打错字,文字这种东西,不同的表达完全就是两个意思了,打字不分段还有谐音字,有时候完全不知道说的啥。 当然了,我不是站在吹毛求疵或者批评的立场来工作和学习,公司给我带来的成长是这些年最大的,真是一年更比六年强。在这里体验到了体系化结构化的工作方式,学会了远程办公的复杂多样性,熟悉了技术和规则同时保障项目稳定迭代的重要性,这些都是非常重要的宝贵经历,不愧是中国科技圈的黄埔军校。所以刚毕业的大学生能进一家对的公司,对人生有举足轻重的作用。 再说到这一年的学习情况,应该是这些年进步最大的。我生性愚笨且自知,所以学东西也不讲快,只能退而求其次做到熟知和理解。前几年打下了计算机基础知识的入门锤炼,今年有所收获,很多知识都像血液一样在我的身体里逐渐清晰明朗起来。比较重要的板块有 CPU、汇编、编译、操作系统、网络、算法等。书读了很多,对计算机的世界也越发了解和熟悉了。说到这些基础知识有什么用?其实用处真的很大,现在看不同的语言,都有一个客观的运行足迹在大脑中奔腾,不管是 C、C++、OC 此类编译语言,还是 js、java 此类解释语言,以及 java 和 js 为了进一步提高效率依靠 v8 和 jit 实现的运行时编译,我能想到他们这些语言本身,在历史发展过程中,在运行高效和开发高效之间的权衡,就像算法在空间和时间之间的不断权衡一样。 前段时间看到 C++ 的模板,有编译期间显示化。即下面这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // templatedemo.hpp template <typename T> void Swap(T &a, T &b) { T temp = a; a = b; b = temp; } // test.cpp #include "templatedemo.hpp" void test_m_p_test() { int a = 3,b = 4; float c = 5,d = 6; Swap(a, b); Swap(c, d); } // main.cpp #include "templatedemo.hpp" int main(int argc, const char * argv[]) { int a = 3,b = 4; Swap(a, b); return 0; } 上面在 templatedemo 头文件中编写了模板,在 test 目标文件编译期同时生成了 Swap (int,int) 和 Swap (float,float) 类型的函数,又在 main 目标文件编译期生成了 Swap (int,int) 函数。我当时就有疑问,这难道不会函数命名冲突吗?我打消了函数签名不一样所以共存的想法,因为书上的确说最后只会生成一个 Swap (int,int) 函数。于是我开始通过汇编查看细节,发现这两个目标文件中函数签名的确是完全一样的,最后生成的可执行文件真的也只有一份,另一份无缘无故消失了。简单验证后发现,C++ 编译器在链接阶段,完全根据函数签名仅仅保留了第一个函数,把后面同名的函数都 pass 了,所以在这种场景下,如果两个函数的具体实现不一样,也只会保留一个,莫名的 bug 就这样产生了。我当时就顿悟了。 上面问题,我从产生疑问到最后顿悟,总耗时在半小时以内。如果对于编译流程和汇编不熟悉,一来无法短时间知道真相,二来也无法理解彻底。 前段时间捡起 java 语言看了看,也很快就走马观花般停止了。不是难以理解不看了,毕竟我大学写了一年多 java 有些印象只是现在淡忘了。而是发现 java 也非常容易理解,和大学时期学习 java 时候的苦恼简直天壤之别。万般语言均是为了业务服务,说到底还是在特定场景下有各自的生存空间。而这些语言在各自领域有了一定影响力之后,又开始做大做强压榨 CPU 和内存。 高级语言真的没啥贵贱之分,只是在特定业务场景下的产物,最后又约束在特定的语言范式下,说到底 C 和汇编才是中流砥柱。因为硬件资源组合是多样的,不同资源的速度又是有差异的。所以才会在不同场景抓住不同的资源有的放矢。所有语言都是在安全性、可移植性、高性能三者之间做权衡,性能这块又有抓 cpu 性能和内存、硬盘、网络资源性能的。 这一年看了不少书,还有两个在线平台,他们给了我很大帮助。不少书和文章都是看了一遍又一遍,比如《程序员的自我修养 - 编译链接 & 库》。极客时间和 C 语言中文网,做的都是非常不错的。这几年在知识上的资金投入还是不少的,也是值得的。随着了解知识的增多,也越发感觉到焦虑,越感觉到人生的渺小。那一眼望不到头的专业领域,就如盲人摸象一般,进不去,也无法探知。或许到了一定年龄,就得想着抛弃了,抛弃那些自己明确要抛弃的,抓住那些自己明确要抓住的。 对自己失望的是,2021 年一篇技术文章都没有产出。来年得补上。 心境 & 认知篇 多重人格,我承认我是失败了。因为我不愿意在多个人格之间切换,我还是想做我自己,何必苟且呢。欣喜的是我发现我其实并没有社交恐惧症,或许有那么一丢丢,但我还是挺喜欢和各种人交往的。我更大的主观驱动性是喜欢独处,我内心渴望有一个安静的只有我的空间,在那个空间里,或许我什么都不用干,也可以看书或者打游戏。 一年里心境有些破碎,关于未来和家庭。我很恐惧未来的路该往何处,也担心孩子和家庭以后的生活走下坡路。我想做一些事情来弥补,就一直看书学习,以此来打磨心境。因为看书学习的时候,会不自觉的专心起来,就忘记那些杂七杂八的担忧。一觉醒来,就又上班了,上班也没有时间考虑那些了。今年,希望能找到出路,在这条路上有稳定的收入,但何其难哦。 我在碎碎念中多次提到这是一个糟糕的时代,我并不是站在自己的角度来考量这个问题。即便能看到我这个文章的你,生活都不会差。因为那些年收入在 10W 及以下的,可能真不会翻到这篇文章,而 10W 年收入,就能保障在这个时代不会混不下去了。你或许会认为自己过的不如意,但是你已经活的很好了。这是一个糟糕的时代,有太多太多的人每天行走在崩溃之间,他们在绝望之中找不到生命意义。也有太多太多的人,从来没有考虑过生命意义,他们仅仅如行尸走肉般的生活工作着,他们缺少一盏思考的灯。国家并没有多富强,社会也没有那么包容,家庭并没有那么温馨,个人也没有很多尊严,这才是大家应该看到的真相。黑客帝国是每个中国人都应该观看和思考的一部电影,我们每个人都如那些肉体存放培养皿而思想被禁锢在虚拟网络。有人觉醒了,觉醒的人需要被消灭,有人逃出了,锡安也要被一锅端。我对于媒体和政府,就是希望能说真话,能解决问题,但显然已经一年不如一年,越来越猖狂和明目张胆,法律已经在荷枪实弹的保护下走出笼子,面向世人张牙舞爪。 我以前有一个愿望,是希望将来有一天,能够拿出一部分钱,组织一批有才能的人定期向偏远地区的孩子们说教。给这些孩子说城市的模样,给他们说人生的理想和意义,给他们说个人的责任。近期我这个想法动摇了,一来源于我对自己未来的担忧,还有就是我感觉到自己的无能为力。这根本不是一个小工程,我只是在杯水车薪。其实但凡国家层面能稍微放开一点,这些孩子的心性都会变得不一样,可惜那个层面是如此的牢不可破,万般皆蝼蚁。诛仙中有一句话:天地不仁,以万物为刍狗。 所幸的是,我个人没有丝毫影响力目前,但有很多有影响力的自媒体们,虽然赚钱是一部分,但还是在努力的奋斗着。他们深爱着这片土地和人们,也有一股倔强的心境。他们在通过各种委婉的方式,在唤醒那些 “肉体存放培养皿而思想被禁锢在虚拟网络” 中的人。 今年政府出台了很多政策,让各个公司存活的极为困难。我个人觉得这是不合理的,这是通过权利影响经济,而经济是所有文明发展的首要条件。中国的监管,像胡闹一样,该监管的不监管,不该监管的一头劲。我是一个愚人,都知道自由经济和改革开放才是时代发展的命脉,现在的监管更多的感觉是在示威而不是进步。 比如那教育行业,孩子是最重要的财富,教育是必不可少的。孩子的压力不是来自教育,而是来自环境。因为环境让大人焦虑,大人才通过教育希望孩子的未来上一个台阶。现在教育没有了,孩子的未来又在哪里?而大人的焦虑有哪怕一点降低吗?不降反增!这种已经明确先有问题A 然后引来灾难B 的逻辑问题,现在不解决 A,把 B 一刀切,这就是从源头斩断了那些未来有可能跨阶级升迁的孩子们的路。再者,教育难道不应该是国家层面出力吗?良好的师资力量、良好的教育环境、保障孩子的学校饮食健康和营养、保障孩子思想的开放等等,哪一个不应该是应该大力发展的方向?可是呢?有些孩子求爹告奶都不能有一个学校接纳孩子,最基本的教育入口都不开放,这才是现状。我的孩子 2022 年要上大班了,因为搬家肯定是要插班的,可是那边之前中班都不让我插,我把那边的幼儿园都跑遍了,那些老师的口气可严肃了,我感觉不到为人师表教书育人的影子,我也对几个月后孩子能否继续上学感到怀疑。这就是我正在经历的现状,那些一口一句 “来生还做中国人” 的同胞们可以换个角度思考一下,“满足一定条件才能入学” 这样的规则,真的合适吗?满足了一定条件就能入学吗?人多资源少是老生常谈的原因,你有想过资源到底有多少?或者资源是不是没有那么少?什么事都怕问一个问什么。中国的教育,埋葬了太多的学生。而思想解放更是社会进度国家强大的第一的生产力,可这堵墙偏偏是牢不可破的,就更不用提了。 还有那游戏行业,中国已经好久没有通过游戏审批号了。就是说国内,很长一段时间没有任何一款游戏能够上架。估计有近一年了吧,而这还会持续下去。即使在之前有通过版权号,能通过的也是寥寥。相信 “影响孩子身心健康” 作为理由人们,你们想想如果因为孩子就把整个游戏行业打压,那孩子的地位得是多少的举足轻重啊,可现实呢?孩子和妇女的地位,真是草菅人命不为过。 这里还想多嘴一句,很多人说女性现在翻身做主人了。这些翻身做主人的女性大多有中专以上教育,他们是靠自身的知识底蕴找到了不低声下气的勇气,他们在一二三线城市生活着。中国面积太大了,太多太多的女性还在水深火热当中,没人报道,无处伸冤。很多人觉得都什么年代了,还有家暴?还有欺凌?但凡媒体能够稍微公正一点,都会颠覆很多人的认知。即使在法律上,买卖妇女的代价都很低,妇女不如狗。法律当然也在不断完善,可这都什么年代了…… 这基本的人权还没完善好?良好的社会肯定是男女平等的,只有那一天,你生了男孩不担心以后媳妇欺负他,你生了女孩不担心以后丈夫孽待她,你没有子女婚姻的安全层面顾虑,那才是男女平等了。因为这样至少可以说明,法律可以让作出欺凌的一方接受到惩罚。能够看到我这篇文章的路人们,我就默认你一定接受了足够的教育和一定的生存技能,相信我,只要你有了孩子,尤其是女孩,你一定会担心她以后的婚姻安全问题。你甚至不担心她婚后会不会生活受苦,因为你想着真不行还可以帮他们些,但你一定会担心她被家暴。 这是一个混乱的时代,国家和人民都挺疯狂的。我一两年前是不相信很多言论是网上的键盘侠们发出的,我以为那是水军。我甚至还会嘲笑朋友说 “你们不知道,那不是真实用户写的,那都是水军发出来的”。但是当我发现那是真真实实的用户敲打出来的言论后,我就内心崩塌了。疯狂的不仅仅是国家层面,还有很多人民,一个愿打一个愿挨。鉴于那些人民也都不会看到我的文章,我也假装不担心他们会来骂我。我希望有那么一天,思想能够开放,让人做人,让人们富足,让社会和谐,让国家强大。 有一天,我带孩子去公司玩,公司有大绿人雕像,我让孩子在雕像脚下拍了照。 这个照片我发了朋友圈,老婆说雕像有私部暴漏,影响不好,让我删掉。老婆的思想其实挺开放了,我们甚至都带着孩子举办的婚礼,在很多方面并不受思想的约束。但这次的雕像照片还是觉得有忌讳。我就觉得我应该说些本应该被大众接受的思想,性就是一个方面。 性,是美好生活的代名词,这世间还有什么是比性更伟大的吗?没有性就没有人类的繁衍和文明的传承,你说伟大不伟大?性不应该羞于表达,性教育是从孩子时期就应该教育起来的。 我在初中对性的感知是见到女同学会害羞,高中知道精子卵子结合方式,大学知道自慰和性释放,结婚前和老婆有了性行为。我感觉这很落后了,周围所有人都对性绝口不提,认为那是不入耳目,认为那是洪堤野兽。爸妈可能以为我知道了,大部分朋友以为我是懂装不懂,可我真的是不知道。 很多人说性不能大势宣扬,那会引发很多人的罪恶。那些由性引发的邪恶,应该被法律制裁。如果有人胆敢跨越法律,就需要承担后果。所以现在就有一个问题,就是法律不触达,那么就把性关押起来,不让说不让提让其变成羞耻。这难道不应该由法律来管辖吗?法律你不干好自己的事情是为啥?要你法律何用? 你有勇气和朋友说你的性生活一周几次,每次多长时间吗?你觉得这是个人隐私不说完全没有问题,但当你想说又因为害羞、不好意思等原因刻意压制不说,就像羞于看心理医生一样,这就是问题。性这样伟大的事物,应该和大米、吃饭、长河一样的地位存在,没有性,就没有人类。 如果哪一天,性能够在日常街道上被自由讨论,那一定是文明的时代。 杂项 2021 年,我自身和周围环境也有一些变化,有些值得一提。 好朋友离婚了,离婚原因也很复杂,但离婚后的烂摊子,感觉好朋友前半生是毁了。孩子和母亲生活,好朋友虚无缥缈的只剩下无处安放的房产,心境几近崩塌。他想着挽回婚姻现在看来是没有希望,他为了挽回婚姻考了很多次编制目前也没有结果。好朋友的壮年时期是悲喜交加,如今孤独在城市里生活。 好朋友结婚了,今年参加的唯一一场婚礼。她是一个性格要强的女孩子如今也趋于平静,初中高中都很刻苦,但学习一直算不上优秀。同龄人也都结婚生子了,我们这一辈,都奔 30 了。 对春晚的态度,发生了翻天覆地的变化。小时候,我妈妈会看春晚的前半场,我和爸爸哥哥一定会看完整场春晚,到那个难忘今宵。最近几年越发破了家庭规矩,对春晚有些冷漠,今年的春晚,就一眼都没看了,甚至目前已经过去快一周了,一个场景画面都没有去查看。也不知道谁主持也不知道谁表演,随它去吧。我实在是厌烦了那份太平粉饰一片安好,也实在厌烦了朱门酒肉臭路有冻死骨。小时候我父亲每晚一定要守在新闻联播前观看,有时候我会闹着换台,父亲就会生气。好久不在父亲身边了,也不知道他是否还有这个习惯。难说,家里一直很忙,而且父亲有了手机后网络资讯也丰富了,不定还会看新闻联播。希望他不看了吧,父亲是聪慧的明白人,可有些事情他没法知道和想象。 还有大年初一 1:3 败给越南的男足吧。这有什么好说的?其实也没啥,人民早已经没有底线了。中国一直在弱势项目中发力,国际运动会上,啥跳水、射箭等运动,就经常看到中国身影。可是强势运动,比如篮球、足球这些,中国就草草收场了。这也是国家的战略吧,只是当运动不能全民,当运动变成利益只为了面子表现,当运动失去它本来的意义,运动还有它存在的意义吗?男足是个例外,纳税人的钱是真花了,效果在哪里?真要比拼努力,那么努力一定是有收获的。比上不足比下有余嘛,这点我还是有体会的。所以男足这个怪圈,运动员自身的问题或许并不大。 来年想法 新的一年,也有一些期待。自己对未来的迷茫,也希望能够稍微找到一个入口。列一下吧: 个人: 体重到 150kg 适当锻炼 烟少抽些 稍微找到离开迷茫的入口 家庭: 安稳搬到新家,家具等软装购置完成,次卧和书房安装完毕 和老婆吵架不要太剧烈,不上升到父母(老婆上一次和我争吵说是故意的,因为不吵架有些事情不好解决,不吵架生活没激情,,,) 孩子; 买一个 6-8 岁的山地自行车 顺利进入大班插班 练习跆拳道 学习: 写作能力提升(产出 5 + 文章) 英语能力稍微提升 设计模式 编程范式

2022/2/2
articleCard.readMore

碎碎念 - 2022

更新:2023.10.08 已经迁移至 X (Twitter),。欢迎访问和 follow。 2022 年 8 月 6 日 00:36|有些人,道理呢是说不通的。动手,虽然不好听,可能够解决问题。 2022 年 8 月 4 日 00:46|取关了差评的公众号。本来作为科普公众号挺好的,非要瞎几把含沙射影讲了几年政治。基本不主动取关,现在啊,实在是恶心的撑不住了。差评,之前还挺好的,我之前工作在他们办公室旁边,还去热过饭,一群有激情的少年员工。直到有一天我发现它们不仅瞎扯淡还控评,之后也没有调整。直到现在的不喜取关。 2022 年 8 月 3 日 02:32|看着各大搜索引擎的热搜,笑死了。都挺好,这些年,没啥进步但也没退步。 2022 年 8 月 3 日 02:25|挺好,这个访台事件让我清了好几位朋友圈人士。傻逼脑残,不遇到这样的事都找不到机会。 2022 年 8 月 1 日 10:00|工作上,总要和某些同事打招呼,说 “大佬,这个…”。加上大佬,大哥,老哥这些前称,对我们这些底层的人来说,比较好办事。其实也不是他们喜欢听,他们并不喜欢。只是当你说了这些前缀,他们不看消息不回消息,也有些不好意思了。仅此而已。 2022 年 7 月 9 日 19:52|碰到了就以为是机缘,其实大可不必。相同的机缘有很多,这只是碰巧碰到了,仅此而已。 2022 年 7 月 9 日 00:13|社死,不再单纯指性格上的一种。那些社会上发不出声音的,都是社死。 2022 年 7 月 6 日 13:32|有时候我也能感觉到自己的成长,虽然很慢。我不是一个聪明人,甚至普通人的聪明度也达不到,目前还能在思维上成长我已经很开心了,即使慢一些。后面不知道还能保持多久。但有一根弦时刻作为我的判断准绳,还是相当有效的。这跟准绳使得即使我不知道如何判断,最后也能大概找到一个方向。这个准绳是:见到的都不是真实的,听到的更不如看到的,真实的大概率找不到真相,真相也会和所听所言差距非常大。这套准绳在生活工作的各个方面都很有效。 2022 年 7 月 4 日 01:51|有些话,挺想找个人或者地方说说的。可是人找不到,地方也不敢说。真憋屈。 2022 年 7 月 1 日 13:01|工作太难,自身并不是主要原因。因为自身条件达不到也不会接触对应的工作。工作难根本原因在于合作,大家的方向和信息差一致,问题就不大。最怕有人刻意隐藏些什么,使得工作迟迟无法推进。 2022 年 6 月 27 日 03:02|如果不购买 a 公司的产品是因为 a 公司的公关、营销、言论等原因,其实我想说大可不必。虽然我也经常这样,但我明白一点,基本上公司都有见不得人的丑事,如果根据一个底线不使用 a 公司的产品或者服务,那么可能没法生活或者生存了,这就是事实,他们的丑事可能颠覆你的想象。但我还是根深地固的不愿意使用某些产品,所以我也没法刻守自己的底线。还有一个细节点,我会尽量购买非国产的,因为我还明白一点,从概率上来说,这会让我心安很多,其中更多的是从产品或者服务本身来说,当然公司的丑事也占一小部分,我知道肯定有很多丑事的,我都明白,我只能倾向于没有那么丑,毕竟对比来说有些公司跌破底线到无法接受。 2022 年 6 月 26 日 13:19|我近乎失业了,当然不开心了。我这和大环境也有很大关系,可谁为这个大环境买单呢?现在看起来我是代价的一部分。现在我工作内容也多,还要准备着找工作。很多公司又不想去,难过哦 2022 年 6 月 24 日 08:52|又是一年志愿季,选专业好文推荐,有需要可关注下。 几百字,虽然没有针对性的方向说明,但提供了深度的避坑指北。配合评论食用更佳。 选专业的重要性应该和结婚一样慎重,影响非常大。很多非一线城市的学生,受家庭及环境影响,对这些懵懵懂懂,根据分数线和亲属建议或者跟风,就误了大学四年甚至工作后的很多年。真应该有专门的公司提供这些咨询服务,不过一对一的服务就注定价格高,只能满足富裕些的家庭。 比如我当年就根据分数线正好压到哪个学校,然后学校内部提供的专业再做下优先级排序这样子。我选的电子信息工程,但我不知道这是干啥的,那谁知道这专业后面是要搞电路板和软件呢?所幸我也挺喜欢这专业。我老婆就有一点离谱,一本的给水排水土木工程。 大学教育并不一定让人有啥突破,但一定会改变一个学生对待事物的思维方式,思考问题不再直来直往一根筋,还能尽量满足逻辑闭环。拿推导来说,如果 a 是 b 是必要条件,那么 a 不满足,b 也无法成立。把 a 和 b 换算到生活中,就有人认定 b 现象存在且合理,即使前置事项 a 不满足。还有 ab 无必要相关性,就有人认定因为 a 所以 b,比如因为嫖娼,所以全部下架。 https://mp.weixin.qq.com/s/j-pZFcr1qXABAJFeV2-__w 2022 年 6 月 21 日 13:43|我呢,明白自己不是一个自信的人。我不自信的原因是感觉到自己的渺小和微不足道,我认为优秀的人太多,那么有他们在前面铺路,我这个低微到尘埃里的人,就只要沿着他们的先河走就好了。 可我又处在一个矛盾体里面,谁是优秀的人呢?我肯定不是,也肯定有人是,可谁是呢?如果有一个排名,把优秀和不优秀的人分成两类,那我在不优秀的一类里面也就可以安稳按照上面说的办事了,因为我知道优秀的人就在另一个圈子里。可显然这样的分类并不存在,而我也并不能找到那些帮我铺路的为人类窃篝火的先人。 进一步想了想,如果优秀的人里面进一步做排名,那第二名会不会和我一样等着第一名? 于是呢,我就发现,优秀没有啥绝对的标准,优秀本身就是宽泛的概念。就是上学也会这学期成绩优秀下学期不优秀这样子。而我自认为低微产生的自卑,其实更毫无意义,我不是在等优秀的人抄作业,这是等不到的,我仅仅是在逃避,逃避别人的讨论和眼光。 那么,当别人的讨论和眼光对我没有过大的坏处甚至是有很多好处,我为什么还有逃避呢?当然,如果要进局子这些有实质伤害的,我理解这是要注意,也不能太傻不是。 有想法、意见、评论、行为等等,只要因为不自信想要逃避的,我想那就不要逃避了吧,这些思维或者行动的火花,都弥足珍贵。 2022 年 6 月 21 日 01:03|我们常说注意安全,但往往都是危险自己找上门来。 2022 年 6 月 21 日 00:13|有自己的判断能力,是多么的弥足珍贵。 2022 年 6 月 20 日 12:29|昨天父亲节,菲住不可的自助餐,没啥人吃饭。细思极恐。 2022 年 6 月 19 日 18:00|早十分钟,省的就大于十分钟。 Jun 7, 2022 at 01:10|李佳琦,已经被社会和政府驱动到那个位置,是上海人大代表又是公司老板又是直播一哥。尤其直播一哥,基本上每次直播都有人神经紧张的掐着网线,这次这么快被封就能看出来。这样的氛围,想不了解政治都难,而且旁边会有一群给意见的人,不可能不清楚一些历史风波。但这次被封,看起来真的不清楚,这很奇怪也很矛盾,但这看起来真的是事实。 Jun 7, 2022 at 00:57|我今天发现一个事情,人生的道路,做出了一个岔路口的选择后,总幻想还有下一个路口可以挽回上一个路口的不良选择。其实已经没有选择了,上一个路口的选择是演进了下一个路口的可供选择,这是有依赖关系的。我私把这种抉择叫做人生单向不可逆抉择。以后每当我后悔、烦躁的时候,我就想想这个吧,都是前面的选择结的后面的果,不管上一次选择是幼稚、环境、情绪还是其他原因导致的,但已经做抉择了,以后呢,看似还有选择,其实没得选择。 Jun 5, 2022 at 17:59|其实家长并不清楚怎么更好的教育孩子,这是事实。人各有专长,如果指责孩子没有家教等家庭教育方面的问题,其实有些自以为是。如果家长在生育前接受高层级的孩子教育,不通过就不允许生孩子,我认为这是解决办法,但显然这样的父母教育无法普及和落地,主要是成人的学习时间没有普适性。所以呢,老师,才是孩子学习路上的中流砥柱。老师有办法可以用不同的方式对各个孩子做差异性培养,这是老师和政府的职责。显然这一块也无法落地,老师现在可都把孩子教育问题给到了父母,孩子不行就是父母教育不行。那孩子怎么办?父母还是要多用心,把本该政府做的事拉过来做,做得好做不好就看父母如何表现了。其实绝大部分都做不好,这是事实。孩子教育可是系统化的大学问,父母哪懂这些啊! Jun 5, 2022 at 22:30|我又不禁想起了那场轰动全国的阿里新月陪酒事件。因为过程的不堪使得所有人愤怒,但当真相和故事南辕北辙的时候,已经没有人敢站出来批判故事的离奇,只能掩盖起真相了。真可笑额。新月没多久也被公司开除了,老鼎还向新月发起了诉讼要求澄清名誉权。这场故事里没有政府介入,不需要避开啥权利影射,仅仅是因为前后反差太大,警方就不敢翻案了,使得很多人蒙受冤屈,有个哥们应该现在还在蹲着呢吧。古代冤假错案使现代人愤怒,现代的冤假错案又何曾少过?后代翻看历史,也想现代看古代一样。 Jun 5, 2022 at 13:26|一个人的价值,是综合性的。从综合性的角度来评估,每个人的价值其实相差不大。但如果有了偏向性,那么人的价值就会被单方面放大,一俊遮百丑。社会呢,在人才、教育、发展等集体面前,还是应该倾向于偏向性考虑人的价值,才能够做到人尽其职,效率最大化。而独立性的环境如相亲社交等,更应该从综合性来考量人的价值,才不会有歧视。 Jun 5, 2022 at 12:24|有一天,我的网络也会被中断掉,只是时间问题。而那些真真实实的往事,都将随着一代人的离去和消息的封杀,变得无人记忆。现在的老师、学生、艺人,都已经不知道三年灾害、十年文革、89 民运了。比如李嘉琪,并不知道他的直播为啥封了。 Jun 4, 2022 at 20:52|我真是烦死了这套健康码机制。能疯。太耽误事儿 Jun 4, 2022 at 11:46|以后,我该如何和孩子说这次上海人的经历?我感觉不管怎么描述,都说不清楚这里面的荒诞。因为大环境不一样了,这是和平年代里的世界级繁华的上海,可上海人的遭遇是严重的格格不入。没法描述,因为描述的内容一听,就是假的。没饭吃、垃圾桶翻菜、隔壁居委会房间里面满是食物、各地的食物捐赠被政府当作不安全丢弃、老少亲人在自己面前死亡,等等等等,这些怎么可能发生?这些听着就是荒诞。 May 31, 2022 at 02:49|我是一个心里放不下事情的人,有事情就寝食难安。注定成不了大气候。却又心比天高。 May 31, 2022 at 01:41|人呢,还是需要接受孤独。越成长,对环境了解的也越深。越思考,对问题也越发清晰。最后呢,还是需要孤独的面对一些不可预期,没人可以帮忙。每个人都需要孤独的坦然面对很多事,这样说来,每个人也都不是孤独的。 May 29, 2022 at 22:02|遇事不拖拉,还是挺香的。这个准则要成为我的思想钢印,不拖拉,心里负担会小很多。 May 29, 2022 at 13:52|有一说一,华春莹、胡锡进、赵立坚这些人的脸皮,那真的不是一般的皮。真脸皮哪能这么坚硬。 2022 年 05 月 02 日 我和两类人是说不上太多有见解的话也成不了朋友的。一类是昵称后面飘国旗,一类是车子前面插国旗。我深信一点,当一个公众人物公然赞美祖国,那么他不是在犯罪,就是在犯罪的路上。这个观点可以细品。祖国是一个免死金牌,党员是如此,公众人物也是如此。只有平民百姓,不仅没有免死金牌,还可能含冤入狱。 2022 年 05 月 01 日 博客增加了阅读记数。我这渺小的博客,加不加意义不大。先看个一两年的效果吧。 2022 年 04 月 30 日 除了上海,强制封控的还有很多,比如瑞丽,天津,西安,哈尔滨等等。别看上海很残酷,感觉上海的整体表现对比下来很好的,毕竟是世界级的经济中心。 瑞丽可就有些惨了,去年 3 月开始就在不间断的封,到目前断断续续被封时长应该达到 200 日。瑞丽前副市长发文求助还被市媒体指责家丑外扬来着,果然退休就不被待见。感觉去年 11 月份左右,瑞丽的网络就变成市域网了。 恭喜瑞丽人民解封,但也不能掉以轻心,毕竟已经在封控和解封之间循环好几波了。就是不怕病毒了,也该记得隔离的痛。 不怕困难,敢于承担,这是底层人民的善良和担当。大家要相信政府相信经济,在中央和总书记的领导指挥下,今年的 GDP,应该还是会往上跑的 :) https://m.weibo.cn/status/4763210085829235?wm=3333_2001&from=10C4193010&sourcetype=weixin 2022 年 04 月 23 日 今日朋友圈,很震撼,最黑人间四月天。 上海毕竟是上海,不愧是精英魔都,大家都是清醒的。 让很多人知道了什么是 404,什么是内容审查。 2022 年 04 月 14 日 !!!碎碎念一直在订阅号里面发,方便很多。因为 4.14 的文字,订阅号被永久封了。没啥想特别说的,我也打算做一些改变。不是对外部的改变,而是自身的。后面写篇文章吧。 2022 年 04 月 14 日 上海这座城市里面精英远远大于其它疫情封控城市,所以疫情封控的残暴让更多人知晓。可惜唤不醒更多城市的同胞,觉醒的人早己经觉醒,不觉醒的很难觉醒。如果这都不能让人觉醒,真不知道怎么才能让他们悟了。 这残酷程度超过俄乌战争了吧。 2022 年 04 月 12 日 -> 三体中有这么一个章节,是威慑纪元篇,智子要求所有地球人迁移到澳大利亚,违背者会被清除。 为了完成这项任务,智子招募了 500 万地球治安军,给予治安军权利和武力支持,以完成 51 亿人全部全部迁移到澳大利亚。 开始的时候,治安军还很文明,只是督促各国完成人口迁移并保障基础设施稳定。 随着人口迁移越来越困难后,治安军开始动用武力强迫迁移。 最后对于没有迁移到澳大利亚的人民,治安军举起了激光狙击枪,疯狂屠杀。 地球治安军,因为做的事违背人类道义,自己也知道不光彩。所以他们这样安慰自己,文中这样说:“共计 10 多亿人申请,2000 万人参加面试,500 万人通过。通过的人并不在意别人的唾沫和鄙夷的目光,因为他们知道,那些吐唾沫的人中相当一部分都是提交过申请的”。最后,治安军就无意识的在大脑里刻上 “我的权利使得我可以这么做” 的思想钢印。 随后进入广播纪元,黑暗森林法则被验证后,三体星系的宇宙坐标被万有引力号广播到了宇宙中,地球和三体都会在某一天被更高等的文明摧毁。智子也开始放弃地球并进行撤离,人们开始从澳大利亚回到自己的家乡。 对那批地球治安军也开始了审判,他们的结局很不好。 《三体》是长篇科幻小说,获得首个亚洲雨果奖。作者刘慈欣被称为 “凭一己之力将中国科幻小说推向世界级巅峰”。 智子赋予了地球治安军维持秩序的权利,治安军三个阶段的变化被刘慈欣表现的很生动形象。 借用 “庸医治驼” 里面的医生一句话:我业治 (qing) 驼 (ling),但管人 (qing) 直 (ling),哪管人死。 2022 年 04 月 11 日 你们这么折磨上海人民,可真行啊! 2022 年 04 月 07 日 对很多事物,有很多想说的。最后都化为一句心里话:就这样吧,说了也徒增烦恼自讨没趣。 不是每个人都有觉醒的机会,但大多数人在生命终结之前,都会在某个时刻觉醒。只是觉醒还是越早越好。 觉醒,是对这个世界的认知,是对人生的把握,是对未知的勇气,也是对价值的衡量。 每个人的一生都是在觉醒,往横向和纵向的觉醒旅程中,有人拉一把会很快。知识永远是进步的阶梯,日拱一卒的不懈,觉醒的大门也很快就会打开了。 2022 年 03 月 30 日 上海的伟大在于,它和其他城市走了不一样的路,虽然说国民眼中失败了,但失败不在于上海城市,而在于清零防控。 出现疫情就封城,是权贵们最保险的避险符。封城,代价多么的巨大,仅仅是坐办公室是体会不到的,可偏偏一大波比例的人都不是做办公室的,他们都为了生计在四处奔波,封城他们就没法奔波。 上海如果能够把与病毒共存这条路走下去,和国际化大都市这个招牌一样走群体免疫路线,那一定是最有益于国家和人民的,但对于权贵们,是打脸和断财。上海走不下去的原因是疫苗的邋遢和人民对病毒的恐惧。没多少人能客观的认识奥密克我的伤害性,大家都活在故事和恐惧里,不知道除了这片土地,其他地方已经和病毒共存了。 媒体没法有作为啊! 2022 年 03 月 27 日 和泳馆的小哥闲聊,说因为疫情生意不好。 聊着聊着,就说病毒是美国搞出来的,媒体证实了。我也就聊不下去了,就走了。 目前,病毒从哪来的,依旧没有定论,我觉得以后也没有定论了。各种客观的不客观的分析都有,说美国也有,说中国也有,说大自然也有,我也听到比如美国资助各国做病毒学研究,其中包括中国并且刚好泄漏了等等,都很多。 但上面都只是猜测,或许真相就是上面的一种也未可知。但是,这些都不是证实的消息,只要说证实,那就是假消息。 这个病毒的来源,到现在已经过去快三年了,还没有定论,以后应该也没有定论了。 希望以后不要有媒体说怎么来的了,因为不会有真相,即使原因就是上面的某一种。 2022 年 03 月 25 日 能力和权利越大,破坏力也就越大,比如希特勒、拜登、普京这些恶魔。权利大于能力比如金三胖,只能威胁到内部。能力大于权利,看着挺不错的,这个应该是友善的生存环境,这种环境下会存在监督。乌克兰的人民要因为这场战争受苦了,兴百姓苦,亡百姓苦。 2022 年 03 月 25 日 看内网的女性相亲帖,就感觉不在一个平行宇宙。她们都是海龟硕士啥的,还各地有车有房。那找对象的困难在哪呢?也不是说内部的同学能达到你们的要求,虽然几率大一些这个得承认,但你们都近三十了,怎么会没有自己的交际圈呢? 他们一般对对方要求还挺高,至少得有房的。那你们自己都有房了,还要男方有房,是啥意思哦。或许这是做资金筛选的最快的方式吧。 2022 年 03 月 23 日 清零式疫情防控,对于经济和人民的影响,已经苦不堪言到无法接受了。这里是一座孤岛,哪个媒体敢于打第一枪呢? 媒体已经不存在了。也包括自媒体。自媒体更应该叫自渠道,不应该称为媒体。与病毒共存,是肯定的,这是对经济和国人最好的交代,就看什么时候执行了。疫苗是最坚强的后盾,早期排除困难走进口也行, 目前看只能等国内的好疫苗出厂了。 最后一句话很搞笑,国内疫苗覆盖率应该到 6-8 亿了吧,为什么还没法走群体免疫? 人贵自知。 2022 年 03 月 22 日 飞机昨天坠毀了。这很值得伤痛。 我刚听到这个消息的时候,心里肯定有波澜的,但前有地震、疫情、战争、女权等各种离奇的事情发生,我认为即使一百多人生还可能性极低,但这件事在国内乃至国外应该不会有太大波澜。 出乎我的意料,各国都知道了。很多国外人士都发了慰问推送,这其实是让我感到惊讶的。原来这不是小事。可是这又真的不是小事吗?和近期的很多大事相比,是算小事的。 我思考了一下,世界还是很小的。好事坏事都很容易传遍世界。新鲜事时有发生,并不是只有罕见的新闻才会大范围传播。 2022 年 03 月 21 日 成长的慢,无形中也会被淘汰。 2022 年 03 月 20 日 对于一些事物,特定的人群就不应该考虑价值然后行动,而应该有思想钢印,即这件事非做不可。比如算法对于程序员。 2022 年 03 月 17 日 每一个赞的打分,都应该给一个不赞的选项。基操。 2022 年 03 月 08 日 对于工作上不会编这个事,对我的苦恼有些大。 2022 年 03 月 07 日 我真的是不聪明,很多事情都在懊恼中。脑子笨,好多事情搞不好,我都为自己感觉到十分的羞愧。哎,一声叹息。 2022 年 03 月 01 日 我真的是不聪明,很多事情都在懊恼中。脑子笨,好多事情搞不好,我都为自己感觉到十分的羞愧。哎,一声叹息。 2022 年 03 月 01 日 关于俄乌战争,我打心眼里仅仅反对战争,而对于国际,我没有啥想说的,国际间的关系太复杂,我拎不清也看不清,能力有限。 因为战争是给人民带来了灾难,而人民最无辜,不管哪个国家或者哪个朝代,没有人民渴望战争因为他们知道自己永远是受害者。 所以谁主动发起了战争,我就反对谁,不管是希特勒、小布什、拜登或普京,战争是人民和人类的底线,你不管因为什么原因突破了底线,我就要反对你,这个充满血泪和痛苦的底线,一定不能被突破。我不知道是不是普京年龄大了等不及了还是其他重要原因,但发起战争就是错了。 那些崇拜普京睿智勇敢有魅力的人,就是不说战争发起者这个事,我也希望你慎重。这和追星一样,周杰伦已经是底线了,细品 2022 年 02 月 28 日 普京开始核威胁了,我的天,你们不怕我怕,我一家人的命呢,虽然低贱,但想活着。你们在不明真相下崇拜俄爹普京,我能理解。但是祭出核威胁这样的言论,你们还拍掌欢快,我是不理解你们了,你们命比我的还贱,我以为我已经到底了。 2022 年 02 月 27 日 给孩子买铅笔橡皮了,孩子要写作业了。时间过得真快。 2022 年 02 月 26 日 有些事情,没有见到也就罢了,但见到之后还要装作没看见,良心会受到谴责。 我好久没发朋友圈了,因为不值得。最近发了两个,分别是江苏女和乌克兰。突破了我的底线,就一定要说说。那些说普京帅气勇敢又魅力的,随你们吧,不怪你,怪的是这个荒诞的年代。只是有一天,后代发现你们是帮凶,脸面放哪? 我的朋友圈,都是工作和学习的朋友。第一次上海的老板问我文字谁写的,第二次高中同学说不能这么评价普京,普京睿智勇敢有魅力。两条朋友圈,失去了太多交情,能有啥办法?不说些什么良心过不去,说一些交情少一些,这个可怕的社会,说话死。 2022 年 02 月 25 日 坏人嘛,能力越大,破坏力自然也会越大,在所难免,比如,普京。 2022 年 02 月 24 日 战争,受苦的人民太多了,不管是士兵还是普通民众,他们是受伤最痛的。战争为什么会存在呢?经济全球化下,人人都安居一隅可行吗?不理解战争,也很难过,非常心痛,为那些失去太多的百姓。活着真好。- 乌克兰 2022 年 02 月 23 日 女性被买卖,不管会所、阁楼、还是农村媳妇,是重罪。国家应该做监管。几把我不想写了。 只希望保护好能保护的后代吧,人生啊。。 2022 年 02 月 22 日 我老婆的思想觉悟已经有些高了,我问老婆江苏女事件怎么看,老婆说不管我们的事情。我才明白这件事已经没有啥余地了,这次表达是非观点的人已经很多了,可有什么用? 原来大家都是这样的思想啊,国人终究不理解该怎么生活。我很悲伤,非常悲伤。 2022 年 02 月 21 日 很多人对于自己的不幸,归结于自身的不够努力和社会的残酷,但从不考虑到自己走到这一步国家应该承担哪些责任。有些人自始至终都想不明白,国家在国民生活中本应该处于什么位置和立场,更不明白自己和国家之间的关系。 我认为理想的国度,应该是保障 20% 的人为他们的理想奋斗,这类人集中在 25-50 岁之间,他们是国家的发展和经济支撑。其他人,要么是婴儿孩子重点培养他们的兴趣爱好,要么是老人保障他们的生活自由,要么是躺平年轻人中年人保障他们的生活和爱好的维持。一个国家的繁荣昌盛,肯定不是全民运动,那么一小戳有理想有报复有能力的人就够了,更多的人要么在路上,要么在归途,要么在躺平,都应该好好善待,这才是国家应该做的 2022 年 02 月 17 日 -> 在老一辈人眼里,江苏女这事就不是大事。农村或多或少都有一个买来的媳妇。有的孩子也是买的,但没妇女多。 至于为啥舆论消不下去,那肯定不是网警过年放假了。有很多高技术数据研发人员,早就做到审查自动化了。很多人都不知道现在监控技术有多发达,但一直都被这些高科技服务着。监控技术发达是因为技术研究都被用在了数据分析和监控上,举全国之力。 江苏上峰一次次的发布公告,虽然背后是调戏了韭菜,但他们在发布公告之初是认为这公告代表的权威就是铁律,一小戳异议韭菜被和谐根本无足轻重。没人知道为啥这次舆论审查怎么不严谨了,上峰也不知道原因。 天网这么浩大的工程,还没发挥出重大价值。人身监控、肇事逃逸这些重大场景已经有实打实的落地,但那些小场面也应该介入进去,比如丢了电动车丢了妇女孩子什么的,可每遇到这些小场面的时候,摄像头总是坏掉。几年前我去警局看监控找电动车,警察小哥说 “你看,被挡住了”。我还看过这样的言论,说警察数量有限,管不了那么多琐事,脑回路就挺弯曲。人民日报批评星巴克驱赶警察那个,也很玄,真不知道哪个群体敢惹警察,尤其还是企业单位。 江苏女这事虽然棺材板没压住,过两天肯定就被解决了,在上峰眼里也不是啥大事。几百年后的后代们或许会惊讶,这事,就这么过去了?也或许他们会唱赞歌,赞美一位江苏兄弟给一流浪女长期提供食物和生活补助,写故事的人可不都天马行空么,尤其是无法考证的历史故事。 读书认字接受教育不为了听谎言,也不是反抗啥,至少保持独立分析能力。虽然被活在小世界里,但得知道还有天外天。 以前以为很多文章下面的评论肯定是水军,有一天发现那些评论都是真真实实的有学问的键盘侠打出来的,也惊呆了。 最后呢,希望这次江苏女事件,上峰最后能给个不糊弄版本的交代。毕竟当年蜘蛛网查封了食指的阳台的时候,食指还是坚定的向我们呼喊着:相信未来,热爱生命。 2022 年 02 月 17 日 有些人脑子是不怎么好使。文字狱,是某些人的利器。 2022 年 02 月 16 日 我的朋友圈不敢发啥东西,因为没几个脑子正常的人。开始我认为有了一定阅历或者能力的人,能看的更远些更清些,然而我发现我是错的,大家不过也如一般人一样行尸走肉人云亦云颠倒是非。不管高知还是低知,在思想上,还是一样的水平。 2022 年 02 月 14 日 真是被网上这批狗子们寒颤死了。一个 MESI 和内存屏障,那么多博客写,没几个分析出精髓的。垃圾。还是极客时间的作者认真些的。 2022 年 02 月 09 日 看到政府发的图片,以后要限制流产。日狗了,流产是自由好吧。有人说只是要限制流产,其实这就是不让你流产,得理解老板的意思。想着我出生的时候,那时候还是计划生育呢,是被迫流产。(2022.04.29 政府说父母不能干预孩子婚姻,否则会触发法律。一切为了生育率,为了更多的韭菜啊。) 2022 年 02 月 06 日 我想起那年年后上班,老板和我们说公司解散换产品。我还在,其他同事走的太多了。我想老板心里还是难过的,为了一大伙人过好年,解散的消息也要放到年后说。如果我做老板,我有这份心境承担这份压力吗? 2022 年 02 月 02 日 我说这是一个神奇的国度,99 的人应该都不相信。对于很多事情,温水中的人,底线很低很低…… 低到令人发指。而这些,都是一个国度带来的。 2022 年 01 月 31 日 新年好,,, 2022 年 01 月 28 日 平地一声雷,滨江带动杭州成为全国的敌人了。好残酷,大家都回不了家了。 2022 年 01 月 25 日 黑客帝国是元宇宙的开局,更是几十亿人的良药。奈何药太低微,救不了多少人。- 满心悲痛遥送刘学州少年 2022 年 01 月 19 日 编程也是一个很混乱的职业,有人还没出校门,就深入理解底层原理如编译、链接、内核、cpu、网络、算法、范式等其中的一部分,有人工作十余年还不理解程序执行原理。但他们都可以完成本质工作,薪水也可能无过大差异。 究其原因,这是一个教育基础严重薄弱的时代,也是一个只需要干活无需往深处探索的年代。这浮躁的社会,让大家都不知道哪样才是人生的意义,而活着或者更好的活着,就是当前的目标。 2022 年 01 月 08 日 我和老婆说,这是一个奇葩的年代。说这句话的时候,我一时间不知道该列举哪些奇葩事,只在心底有个概念,全都是奇葩。 2022 年 01 月 06 日 一粒沙,就要当好一粒沙的角色。首先是自己,然后是家庭,其次是公司和社会等等。所以,如果有和自己和家庭相违背的,当然是做好一粒沙的角色了。 2022 年 01 月 02 日 在这慌乱的年代,可不就如 zion 和 matrix 一般么。

2022/2/2
articleCard.readMore

碎碎念 - 2021

更新:2023.10.08 已经迁移至 X (Twitter),。欢迎访问和 follow。 2021 年 12 月 30 日 公司内部,先把一件事情弄大,再小的事情都会被处理掉。不会闹事没有饭吃。 2021 年 12 月 26 日 今天和两位朋友要了欠款,也都很爽快。不禁想起那一年,我和高中同桌讨要 3000 借款的场景。 讨要了一年,期间说现在没钱、刚分手、现在手里真没钱、等等。开始的时候,我是真没放心上,当年都是好朋友,只是毕业好久没见面了,只当是困难。后面偶然从另一个好朋友那里知道也有借款并及时要回止损了,我才知道,这朋友心变了,变得喝人血馒头了。 最后他就是死马当活马医,不接电话不回信息。我就留言说法院投诉了,等传票。 他马上还了钱,然后删除好友关系,然后退出同学群。这一串操作把我打懵了,可能他面子上也觉得这很丢人吧。至于良心,我倒是不觉得他还有了,毕竟当时可是高中同桌好友,要是有哪能干出这事。 2021 年 12 月 25 日 房子,终于是告一段落了。基本好了,还有冰箱洗衣机,等入住的时候再买。其他基本 OK 了。 2021 年 12 月 23 日 人微言轻的时候,靠的就是不要脸了。别人就是看不起,只有不要脸可以更好的沟通。 2021 年 12 月 22 日 今天部门开圆桌会,发现同事和之前经历的公司一样,都不做出头鸟,也没有啥醍醐灌顶的言论。但是那一种不向权贵膜拜低头的自信气质,倒是走过的公司很少见的。走过的公司,有小厂,大家看老板脸色混饭吃。有管理上近乎国企的公司,大家严肃对待上 1 级和上 n 级领导的言论甚至抄录下来防止忘记。这里有一种你是我领导但我不认为你比我强的心里。所以呢,总体来说,很多人都在社会和公司噤若寒蝉,只有在家的时候才放开约束。大公司的员工,虽然除了工作表现上会有突出,其他方面也和大众一样,是自私、有歪心、思维更活跃的普通人。应该按照自己想法好好打扮自己的家,对所有人那都是可以放纵的地方。 2021 年 12 月 21 日 都美竹是吴亦凡的破壁人,彭 s hua i、李靓蕾也都在微博上作为破壁人,揭穿了面壁人。而李云迪是他自己的破壁人。 短期这么多面壁人被揭穿,到年底了,想着应该告一段落了。 可真没想到,薇娅,罚款 13 亿。这上一次听到这个数,还是人口普查的时候。 其实呢,都是笑话。没有人也没有机构反思为什么会出现这些事件。更甚,某些事件就是刻意发生的,所以不需要反思,需要的是分红。 可千万别说复盘,复盘这个词,是说先用心做了,不管结果好不好,才有复盘。前提是先用心做了。 2021 年 12 月 17 日 上一次哭不知道什么时候了,记忆中还是大学和女友分手的时候吧应该。只所以说应该,是我不记得当时哭了没有,也都近十年了。 今天看到一向搞笑的六点半,刷了第一遍发现没有反转,到会议室刷了第二遍,就没忍住了。 视频里面说的是一个中年男子失业后的心酸和家人的支持。 不过我也有些其他话要说,我想很多中年人遇到这种场景都会痛恨自己没有年轻时候努力。其实呢,为什么你要不停的工作努力和拼命,才能艰难维持生活呢?有没有可能,在一个不那么严酷的国度,那里的人的一生,完全和我们的不一样?他们不奋斗也可以轻松过一生? https://mp.weixin.qq.com/s/hI0JdNLVQRPjOqoAuRhm4A 2021 年 12 月 16 日 one day day,like dog 2021 年 12 月 14 日 心憔悴的时候,啥都不想做。 2021 年 12 月 13 日 计算机这条路,感觉磕不下去了。每时每刻都感觉在入门,知识点好多,有些力不从心了。 2021 年 12 月 10 日 大厂的员工,都很自私及不勇于承担。自我感觉没有中小厂的员工有担当。这一点在文字交流中非常容易表现。比如打错字和模棱两可的话。你不理解他意思,他也懒得改正或者故意就那样。意思呢,就是:该说的我已经说了,该提醒的我提醒了,最后咋样,和我没有关系了。 回车删除修正那几个字都是浪费时间,毕竟自己很忙。搞笑的要死。 这里我就要推荐双拼了,打字快,不容易错。 2021 年 12 月 07 日 聪明的人都很自大,长久不好。 不聪明的人大多思维反应慢,一辈子不好。 相比来说,聪明人优势大于不聪明人。但最后收益来说,很可能不聪明人占据优势。因为自大,可能一下子就毁了,而思维慢,却是温水煮青蛙,最后不定被煮死。 2021 年 12 月 06 日 一句理所当然脱口而出的问候:这么晚还卷啊。 2021 年 12 月 02 日 社交恐惧症越来越严重了,好像病情加重了。难怪抑郁症让人痛苦,能感受到一些了。 2021 年 12 月 01 日 不开心 2021 年 11 月 27 日 很多选择,只能选一次。要慎重。如果选错了,也别刻意想重选,蒙着头走下去吧。 2021 年 11 月 19 日 去年公司家书,给老婆寄了。今年,开始想到爸妈,不合适,然后想到孩子,还是算了,最后想到老婆,也没必要。有很多想说的话,可是太亲了,尽然无法表达。这和从小的教育有影响,希望我的孩子以后不要这样。 2021 年 11 月 16 日 迷茫的人生迷茫的思想,苟且的命运不甘的心。 2021 年 11 月 14 日 无聊的周末,作死的我。浪费大好时间就是在自杀啊 2021 年 11 月 10 日 加班费,扣的快没了…… 人生啊,我在为谁加班? 2021 年 11 月 07 日 身边的,摸的见看得着的,永远都是最好的。珍惜那些距离短的事物和人物,没有啥一定属于你,但是距离短的,一定是更值得付出的。距离变远了,就要后悔了。 2021 年 11 月 06 日 【吉利】今天,我 35 岁啦!感谢你我美好的遇见,吉利因你而更出色。在未来追梦的路上,我们继续一起奋进。快乐人生,吉利相伴!(离开东家一年多了,那不是一个值得尊敬的东家。而值得尊敬的,其实没多少,至少我没经历过。) 2021 年 11 月 05 日 可与语人无二三 2021 年 11 月 04 日 心在滴血,一般就是说钱的事了。克金玩游戏,其实也没啥。 2021 年 11 月 02 日 三体系列,几多年后,可能会成为禁书。 文洁邀请外部文明重造家园。 不要回来,不要回来,不要回来,这里不是家。 当然,什么书好,什么书坏,非人决定,而由趋势界定。 2021 年 11 月 01 日 rui & jin ,谁能想到呢?上一次几亿人眼皮下杀人,这一次几亿人眼皮下剥人,挺身而出的被反杀,也没多少人能出头了,都已经麻木了。这是什么时代呢?不知道,这世道,都是戏子。 2021 年 10 月 30 日 看了几个产品,把微信公众号印在了产品上。他们不敢想象有一天,他们的公众号会不再存在么? 2021 年 10 月 29 日 感冒了,真难受。头也疼,腰、背、大腿也都疼且无力。依旧在工作一线,这就是艰苦奋斗的命!没有假期没有旅游,都习惯了,也就不渴望了。那些经历过的,应该是承受不了。所以啊,当体验过了,就知道那个东西了。但是没有体验过甚至都没听过,那么连想法或者渴望也都不会有。牛,也就是这样的奋斗一生。 2021 年 10 月 27 日 欧 jin z 带来的影响,将是长远且深刻的。会有各类人模仿,也让各类人焦虑担心。有好有坏,也发人深省,也醍醐灌顶,也捕风捉影,也无病乱投医。 2021 年 10 月 26 日 想要离开了,开始准备准备吧。这里没有发展机会,也没有前途。 2021 年 10 月 24 日 li yun di 这样的事,发生在他身上,不可避免就是大事。制度下的性教育不完善和个人崇拜,是导致其从高处陨落的原因。一个两个甚至三个四个这种级别的陨落,都还可以说是个例,但如果这种个例已经成为饭后谈资和故事一样普遍,或许所有人都应该思考制度的健全性。更甚者,或许每次这种级别的谈资出现,就代表者另一件大事发生吧。 2021 年 10 月 22 日 婚后,我才发现对很多事情的无力。哪些改变不了的现状,对一个人是持续的针扎。避不开,也面对不了。 2021 年 10 月 21 日 什么可以让一群人不懈的努力和奋斗?是贫穷和温饱。当全部人都在奋斗和生存的边缘徘徊,奋不顾身,不论年龄和性别的无差别努力,那么一个大环境下渴望的只有温饱。艰苦奋斗是没有底线的,如果奋斗的收入不被自己获取,那么自己、下一代、下下一代,都会被无穷尽的奋斗压迫。 2021 年 10 月 20 日 2021 年 10 月 17 日 沃尔玛、东芝,接连撤出,后面撤出的会更多。坑货们以为他们是来抢夺的,其实是相互依靠。他们是赚了钱,但带动的趋势、就业形势、经济发展、技术发展,哪些不是实打实的?相比他们赚的钱,这些才是更重要的。别说什么经济自运行了,是人都知道行不通,除非不是人或者胎里坏。这群夜郎自大的 bug,要让大家一起苦喽。 2021 年 10 月 16 日 表里不一,真实低俗的通病。如果每个人都如此,那一定是因为环境而后天形成的。表里不一是欺骗,带来的影响有很多,比如:欺压、使坏、嫉妒、80% 的贬义词 2021 年 10 月 14 日 我能感受到很多很多人的不如意,因为我所处的生活和工作环境能满足家庭开支和个人成长,我本该是满意的,但是我并不快乐,想来很多人应该是比我更加艰苦和煎熬了。 工作和生活上其实是有很多很多不如意,步履蹒跚,举步维艰。可是能和谁说呢?想了一圈,发现空无一人。和父母离得很远,长大了也有了隔阂,不愿多说。和老婆也说不起来,老婆并不愿意倾听,说多了反而让对方感觉是傻逼,毕竟对方拍拍屁股走了,就没家庭了。孩子也没法说,孩子太小,还得忍着心情陪孩子玩。朋友更是聚少离多,根本谈及不了这些琐事。 长久了,也就习惯这样的闭口不言了。可很多时候,还是忍不住心里念及着,假装在和世界诉说。 2021 年 10 月 13 日 自由不是想做什么就做什么,而是不想做什么就不做什么。 2021 年 10 月 12 日 我的苹果机用了一年了,装了 97 个应用,用了 33G。给老婆买的 64g 的 iPhone12,死活都是嫌弃空间太小,然后要给丈母娘了,因为又买了 128g 的 iPhone13。一个人总是对已经拥有的不珍惜,尤其发现一些缺点后,就愈发的嫌弃,但其实拥有的已经是很好很好的了。这句总结也送给自己吧。 2021 年 10 月 11 日 伟大的政府不会看着不管的,投诉吧投诉吧。 2021 年 10 月 10 日 书和命运之间,关系没有多深。但书还是要读的,命运这样的玄学物质,看命或者看运吧。 2021 年 10 月 09 日 一周中最喜欢的是放假前一天的下午,最不喜欢的是和别人一起踢皮球。 2021 年 10 月 08 日 我今天知道还有一种书叫 “不发声承诺书” 2021 年 10 月 07 日 如果一件事情比较重要,千万别一股脑的交给任何人做,至少也要参与一下,不管是生活还是工作。 不要信任 & 相信任何人,包括至亲,这是一个铁律。 2021 年 10 月 06 日 饿的时候不会考虑商家做菜是否没洗手,脏的时候不会考虑上游是否有死尸。所谓的底线不过是需要一个身不由己来打破。 2021 年 10 月 05 日 教育好,少年才能强。现在的教育,很不好! 2021 年 10 月 01 日 胃又开始涨了,身体越来越差。我开始担心死亡了。还没过 30 呢,心里素质却差的不行。 2021 年 09 月 30 日 如果我哪天出版《我的一生》,狗的都不愿看吧。 2021 年 09 月 29 日 一个技术本身很好。总有人打着创新的名义做二次开拓,给很多人留下烂摊子。 2021 年 09 月 28 日 不禁再次感慨一句,每个人都是自己人生中的王。欲戴王冠,必承其重。 2021 年 09 月 26 日 孟晚舟这个事呢,是很复杂的。不能多说又不想少说,最后一句话也不说了。 2021 年 09 月 23 日 自古英雄多才俊,是有原因的。年过 30 后发家的概率就少很多了。阶级提升靠的是几代人的努力,因为起点有太大悬殊。公平永远是上层人给下层人说的话,而下层人也似乎习惯了上层人说的那些跨越者的事迹。可幸运儿能有几个呢?这些幸运儿中,又有多少是拉扯上去的?哪有开挂的人生,不过是贵人相助。 2021 年 09 月 21 日 我给所有中国家庭的父母一句劝:孩子,不管多么辛苦,一定还是要在自己身边带着,别一脱手给了爷爷奶奶。原因很多,不胜枚举,没啥恶意,主要就是合适不合适 & 孩子成长问题。 2021 年 09 月 17 日 不要脸得以生存,大公司生存法则 top1。 2021 年 09 月 16 日 心里有疾病的人,都不知道自己有病。 2021 年 09 月 13 日 微信朋友圈我现在一两三天看一次,匆匆的。我不喜欢朋友圈不是因为朋友圈本身,而是因为我的朋友圈的内容。每次看朋友圈,我都感叹,为什么我的朋友圈的内容这么傻逼、这么没脑子。慢慢的,朋友圈也开始离我远去了。 禁看了一批朋友后,朋友圈有些起色了,但还是不想看了,因为朋友圈的内容又显得乏味了。之前能够让我颠簸潮涌,现在像嚼蜡一样,无味又无聊。 微信,终于也只是一个社交工具,在管辖之下,无法更大了。而其实,微信这样的赛道,本应该有无限个可能。 微信是一个值得骄傲的大叔,本就达到了年轻人的巅峰,可这个巅峰,是沉默守旧的巅峰,毫无激情和创新可言。不死不灭,或许是微信最大的悲哀。 2021 年 09 月 11 日 有时候我对很多重复洗脑的事情感到十分迷惑和恐惧。 比如,一个人持续长久不断的和你说,你是狗,最后你也不会认同这个观点。因为你十分确认自己是个人,因为你见过狗,因为你知道这是天方夜谭的事情,不值得信。 但如果对方不是和你说你是狗,而是不断的说你是坏人,说你的心很黑暗,说你给团体 / 同胞带来了无法挽回的灾难,或许过不了多久你也会忏悔,认为自己的确是个垃圾。对于还不认同的那些人,就开始举例子、做舆论,最后施以威压。 最后呢,讲道理的人和听道理的人,都认同同一个观点了。听的人变成了讲的人,讲的人变成布道者。 通过不间断的重复一个东西,就像皇帝的新装一样,原来黑白可以颠倒,秩序不在井然,善恶也不在有界限。 2021 年 09 月 09 日 我到阿里一年了,没来之前,我充满渴望。这一年接触发生了很多事。对很多事物,这里的同学也和芸芸众生一样的无知与愚昧。 但有一点,我能深刻感受到阿里同学的逻辑思维能力很强,各个岗位的同学这方面能力都特别优秀,相形见绌。值得尊敬的人,很多都是散落在各个楼宇或者尘埃市井之中。聚光灯下的大人,往往只是有大人这个称呼,德不配位的权和利。这在职场、商场、政务等环境下都适用。 一年有感。 2021 年 09 月 08 日 一直没能找到词描述这么一群人,就是说话说一半、言语表达模糊、回答和问题差距过大、不合实际、藏着掖着的表达,最近找到词了,这群人叫老政客。这种人很讨厌,给人云里雾里的感觉,和他们交往很累。但是这群人也很容易成功,因为成功定义比较容易选定的方向是企业和政治,老政客们很受这两个方向喜欢,精通踢皮球、甩锅、偷懒、压榨等。 2021 年 09 月 07 日 限制网络速度的,是什么?材料?成本?还是技术? 网络给我饭碗,我是网络的忠实用户。我依靠网络生活,但是我这么多年都没能接触到 200M 以上的带宽。 我能过接触到最新版的 iPhone,也知道很潮流的最新科技。但是我却没能接触到高带宽。在互联网已经如此普遍的环境下,我依旧没体验过秒下电影的感觉。 非互联网行业从业者使用 100M 以上带宽的,没有多少人。更多人都在不自觉的看着 loading 动画消磨时间。本来一瞬间的事情(100ms),他们需要 20 倍的时间(2000ms)。看个视频不停的卡卡卡,刷个网页一直在加载,场景数不胜数… 但网络数据包,可是光信号传播的。 低速的网络,每时每刻都在谋财害命。 2021 年 09 月 06 日 c 语言中理解数组,就理解数组是一种数据类型,和 int、char 一样的数据类型,这样可以清晰明白很多层含义。如数组名和指针、数组 size、内存大小、数组指针等。 2021 年 09 月 03 日 我当了好几年程序员,毫无建树,也越发感觉到疲惫。回头看看,发现这个职业有两个点是让我深有体会的。一个是保持着人的初心,在这个复杂焦虑愤怒不甘痛苦的大社会环境背景下,coder 还是当年那个少年,这在同学聚会等场合很容易看出来,coder 虽然也经历了很多,但内心还是挺干净的。另一个点是与人沟通的能力很弱,这或许是程序员的通病吧,除了自嘲,说的话鲜有亮点。整体上说,程序这个行业的工作者,是挺悲哀的,飞起来的是坐骑,飞不起来的是狗奴。 2021 年 09 月 02 日 每个人都是自己小世界的王,是自己人生的中心 & 主角。虽然你的所作所为在外人眼里微不足道,甚至在亲戚 / 家人眼中也一样。但还是要时刻记得,其实在他们眼中的世界里,他们也是王,你只是臣子。而反过来,其实你也是这么看他们的,甚至至亲,难道不是吗?每个人都是自私的,伟大是包装出来的故事,自私才是人的本性。这无非对错,反而这才是最真实,每个人都应该先满足自己再充裕他人。因为每个人都只认自己一个王,其他都是冒牌的。 2021 年 09 月 01 日 感觉压力大了生活工作中处处有困难,那是在经历事情。而以往的顺风顺水无乱心事只是温水青蛙的表象。有所成长,一定是在经历中磨练的。 2021 年 08 月 30 日 斗罗大陆动画版听说在学生一代影响非常大。的确,情节是波澜壮阔的,和很多小说一样,抓住遇神杀佛的无敌情节,最容易让中低层人民疯狂和陷入。but,斗罗大陆动画版有一个非常大的不好,就是缺少逻辑性,这对青少年影响太大了。很多很多成年人都感觉不到逻辑性的重要,因为他们逻辑性也很弱。 2021 年 08 月 27 日 感情是小说、电视剧、电影、小故事、生活、工作、社交等等场景下不可缺少的主线情节。但是感情又是最虚无缥缈的,没有实体也无法用价值衡量。所以感情最普通也最特殊,最低微也最强大。真没什么东西能有感情的范围广、强度大、意义深远。空气或许和它有的一比。但是有多少人会在意空气质量?但很多人都会拿感情做文章。 2021 年 08 月 25 日 一个人对于自己内心的渴望,好难控制。 2021 年 08 月 09 日 公司存在的意义是盈利和为部分人实现价值,不管从什么角度解读一家公司的策略或方案,如客服、监督委员会、内部督查组、食堂、班车、加班福利等。一定不要把好话说的过满,因为大多和盈利相违背的,会有打脸的一天。没有一家公司是用户贴心的棉袄,任何一家公司的底线都无限接近当地市场环境的下限,甚至更低。但在历史年轮的前进道路上,公司也都是伟大且必要的。公司大了,公关是非常有必要的,但一定要限制底线和权利。文字的力量很强大,写着写着,还没给别人看呢自己就能感动哭。(对最近诸多活跃公司如华为、滴滴、鸿星尔克、苹果、特斯拉、阿里、腾讯、媒体 & 政府部门等有感) 2021 年 08 月 09 日 多少公司依靠性打插边球上市,又有多少人因为性成为阶下囚。性从古至今都是欲望的毒,但它也是最纯洁伟大的。性教育应该加强,知彼知己才能遇事不慌。 2021 年 08 月 05 日 谣言应该被传开,而不是禁传。游戏 & 影视剧也不应该被严查,网络更应该开放。

2021/8/7
articleCard.readMore

打算记录一些碎碎念

更新:2023.10.08 已经迁移至 X (Twitter),。欢迎访问和 follow。 这一篇是碎碎念的扉页,作为碎碎念分类的开篇。 过去的这一年,我又经历了一场无法言说的个人荒诞。有严肃的,有开心的,有奇葩的,有诡异的,更有无法言说的。 过去一年,对我的影响很大,客观评价来说,我很受益,也有伤害。虽然我的这些经历,对很多人来说,每天都在经历,或者就像每日的吃饭一样正常,但我回首思考,对我或者很多很多人而言,如果有这样一场经历,人生的见识的确更丰满一些。 我 2020 年 9 月换了新工作,这里开会都是远程的,名词比黑话更多更可怕,技术栈是多样的,说话都是说一半的,同事之间并不好相处,信息也都是孤岛的。这是一场暴风雨,淋了我一身,也让我成长。在新公司的试用期答辩上,我说,试用期的 3 个月平均下来,我差不多用了 1 个月适应新公司的工作方式,又用了 1 个月熟悉复杂的业务和技术栈,再用了一个月,投入自己的工作完成试用期的绩效。工作让我身心疲惫和无限焦虑,也让我快速成长,幅度最大的是思维的提升,思维提升又反过来作用于我的工作和生活,这样的良性循环,挺好的。后面把一些感想记录在碎碎念里。 生活上,自认为过的不如意。近一年时间里,喝了很多酒,抽了很多烟,点了不少外卖,胖了 30 斤。生活质量变得好了很多,焦虑感却越来越强。有时间都陪了孩子,实际上是用陪孩子来麻痹自己的焦虑心。对老婆和自身的关心度急剧下降,每每想到这一点就深感内疚。我能感觉到,生活上的变化是因为工作上的焦虑导致的,我没能处理好工作和生活的关系。找到病源,病应该是好解决的,我想着半年内应该能处理好,以 2021 年底为 timeline 吧。后面把生活上的念念碎,也记录下来。 日子一天天的在走,我有时候晚上给自己复盘,发现其实每天都有很多新鲜事。有些事是能说给别人听或者记录下来方便后面看,有些事是只愿意埋在记忆里让它慢慢被遗忘,也有些事是希望马上忘记不希望被任何人提起或者发现。一句老话:“不如意事常八九,可与语人无二三”。

2021/7/25
articleCard.readMore

多重性格就是逆天改命

我在一周前发现了一个惊人的秘密。我尝试去了解和熟悉这个秘密,发现需要很大的勇气和一个不那么友好的过程。于是我打算把战线拉长,我希望经过一段时间的自我训练后,拥有这个逆天改命的技巧与能力。 秘密就是多重性格。多重性格不是 “多重人格”,多重人格是疾病,多个人格在一个主体上不互斥的体现出来,多个人格之间相互独立。 相反,多重性格就是一个主体,可以控制多个性格,在不同的场景下,展现不同的性格形态。但是这多个性格之间相互影响、相互约束,只在最合适的场景、使得主体舒适度最大这样的条件下,主体刻意展现其中的一种性格。 很多人都对别人劝诫,说性格不好,应该改改脾气云云。说这话的人都是傻逼,因为我也说过。 说这话的人会认为自己在某方面的性格是优秀的,因为没有尝试过改改脾气这个过程,才会顺口说出这样的劝诫。 但什么样的性格才是优秀的?这本身就不像 1+1=2 这样,有一个绝对的标准。 改改脾气是否是那么容易的?我下面会阐述,基本改不了。 很多女性都喜欢温文尔雅的男士,那让这个男士去做将军去砍头杀人,拿着刀他都手抖。我这是举个例子,我写过一个 [打比方][1] 的文章有说明,例子可以更好的阐述观点,但例子可能会误导人。我就想引申出一个重心:性格是多样的,但各有千秋。好看的外表千篇一律,有趣的灵魂也绝非万里挑一。 多样又花开 N 朵的性格之间,本身是没有可比性的,谁也不能说谁的性格优异与他人。但性格对于其拥有者,利弊都是潜移默化的,但大多也都是一条路走到黑,可能默默无闻,可能黄袍加身。所以,但凡我们能够窃取一点天机,了解到人各不同这一本质,就可以减少很多人际复杂关系和调节自身心情。如若能够参透些许,或许真的可以做到逆天改命。 性格的确定性 我们常说性格养成,而很少说性格练就,就是因为性格和专业技能等不同。专业技能是可以短时间内积聚起来,然后运用于工作及生活中。而性格不是一触而就的,它是一个养成的过程。 可惜的是,当一个成年人知道性格养成含义的时候,为时已晚。因为这个养成,是幼年时期的养成,即 0-6 岁之间。婴儿和幼年阶段,是一个人的性格养成阶段,过了这个阶段,是什么性格,都会跟随主体一生,也就是说,这个性格,一经确定基本上不可改变了。 性格由两个比较大的约束制约,一个是基因,一个是环境。环境给一个幼年儿童带来的影响非常大,说环境会影响小孩子的一生,我认为说的就是性格养成。一个生活在家庭温馨下的孩子,或许不会懦弱和偏激。而一个家庭时不时的对孩子大吵大骂,这孩子大差不差不会勇敢甚至会感觉胆怯和敏感。具体环境如何影响性格,肯定是一个非常大的话题,我当然没有这个能力讨论。但是我心里清楚,一个好的环境,一定可以减少极端性格养成的概率,也一定可以增加健康性格养成的概率。极端性格如暴力倾向等,虽然也属于性格多样化的一种,但显然是不应被采纳的。 这也就是我们社会上看到的种种人与人之间不同的起源。因为幼年所处环境的不同和本身基因的不同,会有形形色色的性格在自己的周围飘荡。有些与自己合得来的性格,自己就会放松警惕感觉轻松愉快,有些与自己合不来的,自己就会增加警觉度感到压抑。也没有什么性格属于万能性格,比较惹人喜欢的热心肠爱助人的性格,在一些敏感人面前,也会变成一种嘲讽和压抑。人总是趋向于安全的集体,所以一个人的周围,大差不差,都是和其性格差别不远的一群人的聚集。这也就是物以类聚,人以群分的原因。 每个人,在童年青年壮年时期,都会和很多人产生交往,会很自然的发现,周围人有和自己不一样的性格表现。比如同样对待一件事物,有人看到的是风险危机而浅意识想着逃离,有人看到的是机会机遇而浅意识想着奔向,两种人从自己的角度分析,都是合情合理的,但两种人相互从对方的角度分析,却都感觉对方是傻逼。这是人的天性:每个人都会浅意识认为自己的性格是优秀的,因为人天性需要保护自己。所以在抉择时刻,第一想法永远是性格驱动的,而不是抉择时刻的环境驱动。 也正因为这种自我保护的驱动下,一个人,即使发现周围人有和自己不一样的性格表现,也会浅意识偏向自己一边而不倾向于改变。 具体来说就是,如果没有发现更好的性格,那自己也找不到对标对象,不会意识到不同性格之间的差异,更不会有性格改变意图。 如果受到外界大的环境打击,比如一个性格软弱的人,在工作上被强势的同事长期挤压,这种环境下会明显发现自己的性格在工作场景下吃亏了。但这时候,人也会改变自己的想法,潜意识告诉自己,自己的性格是保护型而同事的性格是攻击型,并潜意识告诉自己,同事的性格不可取。所以自己会选择更换工作以获取更大安全性。这种情况下,也少有人会有改变性格以适应环境的意图,更多的是采取躲避以获取安全感。(显然,工作这个例子,只是一个比方。也有人会试图做改变,比如,杜拉拉。) 这基本上是很多人的整体倾向了。即幼年时期养成的性格,大差不差要伴随自己一生,直到入土。如果幼年时期养成的性格整体上比较优秀,那么一生或许开开心心的,即使穷困潦倒,也能苦中作乐。如果幼年时期养成的性格整体上比较悲观,那么一生或许郁郁寡欢,即使吃喝不愁,也能整出让自己不开心的事来。 所以,在经过幼年性格养成后,很多人的性格就会跟随主体一辈子了。即使家境殷实,但郁郁寡欢直至终老的人,一定不在少数,因为时时刻刻,他都能整出让自己不开心的事来。 的确,有人会改变性格,在突如其来的外界打击,或者内心的强烈驱动,都会使得一个人的性格发生大的变化。遗憾的是,幼年时期养成的性格,依旧伴随一生,想抹杀是不可能的。 多重性格的重要性 正如上面说到的,有人会因为各种原因,性格会发生改变。 因为一个人的一生很长,需要在各个地区、各种环境、多样的社交群里中度过,而这些,都是和幼年时期不同的。所以幼年时期养成的性格,与之后的大部分岁月,都是不匹配的。如果一个见到女孩子都要跑很远绕路走的腼腆小哥,找了一份推销员的工作,我直觉告诉我,他的心情一定比他的工作波动性要大。 腼腆小哥做推销员,没有任何问题,因为这符合生存规律,是天经地义的。 腼腆小哥做推销员,并且成绩优秀业绩感人,这是个人和公司都非常开心的事情,个人收入和稳定性都增加了,公司产品销量更好了,这非常棒。 那腼腆小哥的个人心情和感受,谁能顾及到呢?小哥虽然有高收入有过人实力,但小哥会不会内心对这份工作有 100 分的抵触呢?小哥真实想法会不会是做一个技术工种的埋头苦干的角色,而不是整天和客户抛头露面这样的角色呢?公司显然不会照顾到他的心情,客户也不会,小哥的父母、孩子、妻子,甚至也都无法顾及。因为环境的约束,甚至没有人能够照顾好小哥的情绪,除了小哥自己。 小哥因为性格原因,本身可能十分不愿意和人打交道,尤其还要追着厌烦自己的客户介绍自己的产品。但是他也不能离开这个环境,因为他在这个环境或许已经呆了很久并有了积累,换个环境自己一无是处,而现实也不允许他出现转行这样大的工作变动。显然,小哥的心情一定非常颠簸和劳累,虽然他表现光鲜。 中国快速发展的这些年,尤其农村地区,幼年时期的性格养成基本是完全自主完成的,即没有外界的刻意辅助。没有父母在身边,爷爷奶奶对教育概念空白,周围都是一群放荡不羁的伙伴,六年的无约束环境,会使得性格的极端性非常高,即性格出众,特别自私、特别害羞、特别健谈甚至特别大胆。幼年时期生活的环境,在后面的 10 年左右还是稳定的,因为要在家上学,这 10 年近一步稳固了幼年性格,会生根发芽无法磨灭。倒霉的是,再往后的余生,就要前往城市务工,这环境的变化是巨大的,非常显著的影响就是性格和环境的巨大出入会导致自己和群体格格不入。 显然,上面说到的性格适用于所有人而绝非从务工人员,因为环境的差异性太大了,在环境面前,所有人都有可能面临性格和环境不匹配。 比如,和你工作比较近的两位同事,性格都是内向实干的。你和他们说话,他们爱理不理有一句没一句的,你邀请他们去吃饭,他们千万个不愿意找理由塘塞的。你当然可以找其他同事去玩儿,但和你朝夕相处最紧密的两个同事,就是他俩。你会时刻感觉到合作的困难而心生怨气。 这显然还都是小事,不会给你带来多大的麻烦。但如果你又是天生的胆小怕事畏畏缩缩,现在你空有一身本领,但一面试你就无话可说,大脑空白,焦虑手抖乃至胡言乱语,那好工作也与你无缘。 所以随着环境的多样和场景的变迁,一条性格走到黑,会时刻出幺儿子。最直观的来说,人是靠大脑驱动的,我们下一秒是吃饭睡觉还是去撸串,都是大脑发出指令,肉体产生实际行动。而大脑思考问题,却是受到环境影响的,不同环境下大脑会确定不同的方案让肉体执行。如果性格不能切入环境场景中去,那么思维方式就会受限,大脑的活跃度就停留在稳定状态,这样,好方案出不来,坏方案一大堆。伤害最大的,还是自己心里上会非常疲惫,自己当前性格和当前场景的格格不入,会使得厌恶指数级增长,心疾不好治。 我之所以在一周前偶然的机会发现这个多重性格的秘密后激动万分,就是因为我发现,上面说到的性格和环境的不匹配,是有解药的。 如果你活了大半辈子,突然有一天,医生告你说你患了一个听都没听过的 X 病,只有半年活头,你一定会伤心到极点。如果是知名的癌症,自己也就释怀了,毕竟耳睹目汝这么多年,我们都知道癌症不好治。但这听都没听过的 X 病,是啥玩意就这么厉害?这就是我很久之前发现的自己的性格和环境有落差后,我的感觉。因为我之前完全没有意识到我很多的不开心不顺利,是性格和环境有出入带来的。 但是医生过了两个月打电话给你,说有治了,马上就能治好,你想想你多开心。这就是一周前的晚上,我在墙角抽烟时候的喜悦,我自己都笑出了花。因为我发现了多重性格这个秘密。 具体来说,每个人的大脑,虽然都比较倔强,比如性格这一块,幼年成型后,就改不了了。但是吧,大脑可以被欺骗。如果我们刻意的融入一个性格,而这个性格和当前环境比较融洽,那么,大脑就会依靠这个性格给予我们相应的满足感的反馈。最重要的是,这种满足感,是可以被记忆的。很久之后,你回忆起那一刻的场景,依旧会心情愉悦。 我自己的性格,自我感觉是非常糟糕的,连我自己都厌恶,胆小、怕事、懦弱。运气比较好,我在十年前,2010 年上大学后,和其他城市、其他省份的同学交往后,发现了这一点。于是,当时我试着改变了自己的性格。那时候,我天真以为,我会改变成另外一个人了。我当时专门看了心理学书籍,试着从与人交流的语气表达、自己的行为姿势等方面,对自己进行改变。效果当然是显著的,不然我肯定不会十年后才找到解药。因为我以为性格切换成功了,后面虽然有过一些冲突,但因为我上面说到的人天性的自我保护影响,我天然以为自己是正确的。 这十年的经历,实在是有些长了,有很多浪费,但也给我上了很好的一课。那就是,幼年时期的主性格,是永不可灭的。它是什么样,永远都在那里,在你无防备的时候,悄无声息的就回到了你的主体,占据主要位置。 我十年之前沉默寡言,喜欢躲在角落里自己玩。十年前我改变自己的性格,刻意找人聊天,刻意开会的时候坐到最前面。但是在漫漫长夜,没有任何危险的环境里,我依旧回到 10 年前的那个自己,我自己都厌恶的自己,我开始胡思乱想,所有的压抑又会占据我所有的身体。这种情绪有时候会延续很长时间而无法缓解。这十年,我一直备受心情波动上的困扰,近几年也给老婆和家人非常大的压力,我在老家和老父亲因为孩子问题吵架,老婆也不知道什么原因我突然就不在理她。 所以,我基本上用我十年的经历,确定了一个主题:性格是不可能改变的,幼年时期的性格,会永远的伴随一生。而改变性格,一来根本改不了,二来要走科学方法。像我十年前那样,看看心理学书籍,刻意改变自己,就认为改性格成功了,简直可笑。 科学方法就是多重性格。既不回避主性格,因为它回避不了,也不改变性格,因为它改不了。尊重大自然的客观规律,切换性格,切换到舒适度最大的性格。 自己的主性格既然不可能去除,那一定不要逆天行事。相反,一定要善待它。因为它是你幼年时期艰苦养成的,它给你带来的安全感,比你亲爹亲妈都多。你只有在主性格里面,才能做真实的自己,也才有最大的安全感。所有不要抵触它,一定要和它和睦相处。毕竟,你干不倒它,它却时刻控制着你的大脑和情绪。 在主性格外,要有 1-3 个辅助性格做即时场景的切换。一定不要过多,即使 1-3 个辅助性格都已经非常难了,不要自讨苦吃,而且,经过我最近几天的发现,3 个辅助性格已经完全够用了。在特定的场景里,比如和陌生人交流,比如和老板开会,这时候自己切换到 3 个辅助性格里面最合适的那个性格,切换到能给自己带来最大舒适感和效率的性格,然后尽情的在这个性格里面发挥。 多重性格如何培养和切换 这 1-3 个辅助性格该如何培养,这是我当时遇到的第一个问题。不过因为我这些年写代码的缘故,越来越会偷懒了,不在像十年前一样去看心理书或者买一大堆书来研究了。我如何不踩在巨人的肩膀上,顺势而为呢? 我把我近些年遇到的人,按照地区大概过了一遍,马上就确定了三个合适人选。而他们的性格,其实就是我非常羡慕和需要的。我试着把自己的生活和工作场景,和这三个候选性格做匹配,惊奇的发现,大差不差,都能够匹配上。 所以,像我们这些不是专业领域的人,就不要去研究详细了,抓几个自己喜欢的,先试着运营一段时间,不要把时间浪费在不必要的地方,时间真的不多了。像我十年前,真是傻逼不是,看了那些心理学书,现在还能记得啥?难不成代码上要写一些场景化的注释以体贴到观看代码人员的情绪?当年我真傻。 就这样,我找到了学习的标杆。其中一个是机缘巧合在无锡遇到的,他的性格整体是谈笑风生豪放派,我和他接触很短,吃过饭,算下来时间不超过两天。他的性格是我这些年遇到的独一无二却又印象深刻的,当过兵,豪放又心思缜密。当时他几十万钞票就放在床底下,让我很吃惊。另外两个是上海遇到的,都有长期的接触,前两天还和一位吃饭吹牛逼。就这样,计划已经准备的差不多了。 近一周,我都在尝试多重性格的转场切换,先下三个结论,有坑后面慢慢补: 真好用。我在一个之前压力非常大的场景下,开始尝试切换。之前我偶尔有发挥良好的时候,但事后回忆都会感觉到不足。但这次,我明显感觉到现场发挥良好,从语言表达和自信度上,都感觉非常棒,以至于我惊喜了很久。 需要勇气,会心态崩溃。有时候自己会很累,回到主性格的周期和时间,大于之前。之前在一个人安静的时候,会自然会到主性格,现在动不动就回到主性格。然后尝试切换的时候,会心里感觉劳累。 需要时间。对接第二点,感觉后面好了很多,自己操控变得顺手起来。看来第二点是必经的路,这个时间长短应该是看人的。我给自己设定两个阶段,分别是 1 个月和 3 个月。 我开始时候对这个多重性格的切换两样观点,一个观点是很简单,就是届时选一个切换。另一个观点是比较难,需要违背主性格。 现在来看,还好,没有那么简单,但也没有那么难。 也就是我上面发现的:切换性格并在场景里投入后,会欺骗大脑,大脑会给你当前场景下的反馈,而这个反馈,肯定比主性格要好,因为我们已经确定当前切换的性格是最优的,能够带来最大的舒适度和效率。这个反馈有记忆,我过了好几天回味当时的场景,还是感觉开心的。 我感激这两次改变。一次十年前,我变得自信和勇敢了,但那次不彻底。一次是现在,我发现了新天地,我希望我能和我厌恶的主性格和辅助性格,和睦相处。一成不变的人生多有乏味,多重性格给我带来了很大的刺激。 在 IT Code 界,多重人格叫做进程,多重性格叫做线程。 [1]:https://www.yigegongjiang.com/2020 / 打比方 /

2020/6/13
articleCard.readMore

从 Core Foundation 看更大世界

Core Foundation 是被 iOSer 忽略的一个重要框架。说重要,因为 Core Foundation 提供了丰富的组件库,这些组件库可以很好的用于开发工作。 但之所以被忽略,因为很多开发工作,可以用更友好的 Foundation 框架替代。 Core Foundation 有 Foundation 没有的功能,比如 CFDictionary 的 Key 元素无需实现 NSCoping 协议、CFArray 可以不进行对象引用计数等。反过来,Foundation 也有 Core Foundation 无法胜任的工作,最大的来说就是自动引用计数功能。 在 iOS 项目开发过程中,我们可以使用基于 C 语言的 Core Foundation 框架写一些业务功能逻辑,甚至有时候非用 Core Foundation 不可,因为它有 Foundation 没有的功能。 Foundation 是用 Objective-C 语言写的,Core Foundation 使用 C 和 C++ 语言写的。我们都知道 Objective-C 是 C 的超集,所以认为 Objective-C 和 C、C++ 混编是正常的。 那么,什么是超集?Objective-C 是动态的面向对象的,C 是静态的面向过程的,如何实现这个超集? 既然 Objective-C 可以和 C、C++ 一起使用,那么 Golang 呢?我们可不可以用 Go 来做混合开发? 通过 Core Foundation,可以有更大的认知空间。 比如各类高级语言在计算机中是如何运行的?Dart (flutter) 可以做混合开发,原理是什么?Lua 做热更新,它不是 C 语言也不是 Objective-C 语言,是怎么被计算机调用执行的?用 Node.js 写 iOS 代码,到底行不行? 各种风马牛不相及的高级编程语言,是否有各自的边界? iOS 开发框架有哪些 框架是库的更高一层描述,这里的库一般指的是动态库,但说静态库,也完全没有问题。 比如,我们要写一个语音识别功能,我们写了 1-n 个动态库 (a.framework、b.framework…) 来完成这个功能。在项目最后,客户说只需要一个动态库,我们就把这 1-n 个动态库组合成 1 个动态库并命名 GJAudioKit.framework,就可以叫 GJAudioKit 为语音识别框架了。 所以框架这个专有名字该怎么解释说明? 一来是众多库组合起来的意思。为了一个功能,需要写 1-n 个库。最后将 1-n 个库组合成 1 个库,这个库就叫一个框架。 二来框架是一个抽象,是比库更高层级的抽象。不管静态库还是动态库,都是目标文件 (.o) 的合集。而框架比库的抽象层级更高,表示一个功能、一个业务或者一个模块,比如语音识别框架。 iOS 系统提供了很多框架给我们使用,详见下图: 我们经常使用的框架都正在图中的分层结构中找到对应的影子。 也可以从 Xcode 的资源文件中查看,如下图: 刚才说到框架有 1-n 个库组成,上图中的 SwiftUI.framework 框架里面只有一个 SwiftUI.h 头文件,而 Foundation.framework 框架里面有近 130 个头文件。我们可以理解为一个头文件即可单独生成一个动态库。 框架的理解就到这,值得一提的是,上面列出的框架,都是系统提供的,也都是 C、C++、Objective-C 写的。我们自己当然也可以开发需要的库或者框架,那么,我们必须要用 C 族语言开发吗? 如果用 Go 来写 iOS 框架会怎么样 手机和 PC 有很多不同,比如 PC 的硬件都是可以拆卸的,而手机一般都不会拆卸,所有硬件都是集成到一个主板焊死的,我们不能随便更换存储和内存。CPU 也不一样,PC 有很多种类的 CPU 支持,可以根据用户是美术生或者喜欢玩游戏而选择不同的产品型号,比如撕裂者等。而手机为了省电,都用的 ARM 架构 CPU。 但不管手机和 PC 如何不同,有一个共同点是计算机发展几十年来不曾改变的,乃至手机和计算器都是一样的原理,那就是他们都依托发展于冯诺伊曼机。 程序需要执行,不能执行没有任何意义的程序,所以输入输出是必须的。而冯诺伊曼机的程序执行就是执行二进制。 所以从这点上看,高级语言再怎么变化,最终也跑不了 CPU 指令集二进制执行这个宿命。 PC 上我们可以跑上千种语言,因为这些语言最终都是二进制。只要语言被编译汇编成能够被执行的指令集,那么这些语言就有被编写和执行的意义,不管是在 PC 上执行,或者手机或者树莓派上执行。 所以,Go 当然可以在 iOS 手机上运行,不仅 Go,Java、Ruby、Lua、Node.js 都可以。那,怎么才能将 Go 写到 iOS 程序里面呢? 这就要分析 C 是如何被 iOS 系统执行的。因为他们都属于高级语言,如果 C 能够依靠一个逻辑被执行,那么 Go 按照这个逻辑也就可以执行。 C 语言是如何被操作系统执行的 系统调用和运行时 很久很久之前,是用纸带打洞进行编程。那时候 CPU 直接执行二进制。 现在就不行了,因为有操作系统存在了。操作系统是一个大管家,管理着所有的应用程序,通过合理的管理应用程序的内存,进行系统级别的控制。 操作系统该如何操作,才能管理应用程序呢?显然,控制了代码,就控制了所有。如果实际运行的代码,都在操作系统的管辖范围内,那么操作系统就想怎么控制就怎么控制了。 这个时候,我们就无法绕过操作系统直接让 CPU 执行我们的二进制了,而是需要让操作系统在中间做一个中间者,我们调用操作系统的接口,操作系统进而让 CPU 执行进入内核态 (内核塌陷),这个时候我们的代码才算被执行。这个操作系统的接口,就是系统调用。 操作系统提供了完善的服务,人们都装了主流的几大操作系统。因为我们的代码需要被用户拿去执行才有意义和价值,所以,我们的代码全都需要接受操作系统的控制。 所以现在,我们的 C 语言代码从开发阶段到被执行,是下面这样的: 系统调用,都是汇编实现的,并实现了 C 的接口供用户调用。这里需要说明一点,几大操作系统都是用 C 语言提供的系统调用接口。不管用户态是什么类型的高级语言,系统调用提供的仅仅是 C 接口。 下面是部分系统调用接口, 系统调用接口并不是很多,都是操作系统提供给外界的刚需接口,大约 350 个左右(不同系统的接口数量不同)。这个时候,C 开发人员开发的时候都是调用 open 用来打开文件,调用 brk 来申请内存。显然和现实不太一样,我们开发的时候,都用的 fopen 打开文件,用 malloc 申请内存。这是为什么呢? 原因就是直接使用系统调用,非常困难。具体分两点来说: 系统调用提供的接口都是基础接口,比较生硬且基础。程序员需要的一个很基础功能,可能需要调用好多个系统调用接口才能完成。 系统调用是操作系统提供的。如果用户用 Linux 系统的系统调用接口开发了程序 A,那么如果想让程序 A 在 Windows 系统上运行,那是不可能的,因为两个系统的系统调用接口完全不一样。 显然,C 语言开发者直接进行系统调用,遇到了困难。而中间件可以解决所有困难,如果解决不了,那就再加一个中间件。 下面是添加了 C Runtime Library(运行时)后的调用流程: 运行时是一个中间层,用户写的代码,最终调用的都是运行时接口。这个接口可以专门为 C 语言提供非常丰富的接口调用,有下面四种情况: 1 个系统调用接口可以为 n 个运行时接口提供服务,比如 malloc 和 free 都使用了 brk。 1 个运行时接口可以调用 n 个系统调用接口,比如 w 接口,需要 x、y、z 接口同时提供服务。 1 个运行时接口可以仅调用 1 个系统调用接口,这个时候是 1-1 关系。 1 个运行时接口可以不调用系统调用接口,如 strcpy,专门的 C 语言字符串处理函数。 而且,不同系统提供的系统调用是不同的,但只要改变运行时,不需要修改用户代码,即可适配多平台。而每个系统都需要维护一套语言级别的运行时,这是必要且可行的。 这样,C 运行时作为中间层,极大的提高了开发人员生产力。 跨过运行时直接系统调用 这里有一点需要说明,C 运行时虽然作为高级语言和系统调用的中间层,但也不一定非要过这个中间层不可。因为操作系统只管理系统调用,而上层如何调用系统调用,是不受约束的。如果有一门语言,完全没有运行时概念,用户代码直接对接系统调用完全没有问题,就是上面说的系统调用生硬且基础和不能跨平台两个缺点。所以 C 语言真实调用逻辑如下: 开发人员在编写代码的时候,可以调用 C 运行时库接口,也可以直接进行系统调用。但还是那句话,这样用的人肯定不多,项目里面可能个别代码会如此实现,但一定是有足够把握才会这么做。 Windows API 的存在 还有一点需要说明,Windows 和 Linux 还有些不一样。Linux 的 CRT 直接进行系统调用,而 Windows 又加了一层中间层,名叫 Window API。这个中间层夹在系统调用和 MCRT 之间,如下图: Window API 对微软意义重大,作为最出名对商业软件,Window API 更好的保障了用户升级带来对兼容性,所以中间层真的很好用。 C 运行时和 C 标准库的关系 下面额外补充一点,C 语言规范中,出了标准,没有出实现。所以 C 语言的编译器和相关库版本非常多。 因为不同操作系统之间,有相同特性也有不同特性,所以不同操作系统的运行时接口有相同的也有不同的。 比如,内存分配,提供的 api 都是 malloc。而 windows 有图形界面的绘图 api,对应的 linux 就没有体现。 于是就把默认提供的 api 如 malloc 或者 printf 等叫做 C 标准库。其他各自独有的也并入 C 运行时库。详见下图: C 的运行时比较特别,主要因为 C 出了标准,却没有给实现,于是各家为政。所以 C 的运行时库里面包含了 C 标准库,还有其他接口如启动函数等。而对于其他高级语言,一般就没有运行时库包含标准库的概念,因为标准库和运行时库也是独立的。比如,Objective-C 里面,系统提供了非常多的标准库即框架,这些框架都是动态库形式,但他们不是运行时,有单独的运行时库负责系统调用 (OC 比较特别,实际为对接 C 运行时而非系统调用,下面会详细说明)。 C 语言运行总结 到这里,不知道大家有没有发现秘密,C 语言能够被执行,有两个要点: 系统调用。对外界提供统一的内核塌陷。 C 运行时。提供 C 程序员开发的接口。 所以做为高级语言的 C 语言,能够在计算机和手机甚至嵌入式系统执行,核心就在于系统调用。我们编写的代码,在编译汇编链接后,都变成了对运行时的调用,而运行时对系统调用 Api 进行调用。系统调用由操作系统控制,所以操作系统才能严丝合缝的对我们编写的代码进行控制和管理。当然最终执行还是 CPU 执行指令,只是优先级、内存分配、线程调度等,都是操作系统控制了。 所以,只要一门高级语言,最终能够通过 L Runtime Library (语言运行时) 进行系统调用,那么,该语言就可以在操作系统的控制下,完成指令集的运行。 Objective-C 是如何被操作系统执行的 上面我们了解了高级语言 C 语言运行的原理,那么我们紧接着看下 Objective-C 是如何被运行的。 我们都知道,Objective-C 是 C 语言的超集,这个超集该如何理解呢? C 是面向过程的静态的,C 不是动态化语言(不是完全动态化),因为函数调用在编译时候已经确定。 但是基于 C 语言的 Objective-C 语言,却是完完全全的动态化语言。Objective-C 是面向对象的编译型语言,所有函数的执行都是在运行过程中确定,超集是如何做到这些功能的呢? C 的代码执行我们已经知道,我们写的代码,在编译后都变成 C 运行时函数调用的二进制。 这个 C 运行时作为中间层,干了一件大事,就是完成对系统调用的隔离和封装。 超集从字面可以理解,是在 C 的基础上做一些事情。 所以,我们可以想一下,如果在一个中间层上面再加一个中间层,在 C 运行时上面再加一个 OC 运行时,那么 OC 运行时是不是就可以做更多的事情? 比如,在 Objective-C 中,代码调用 A 类的 a 对象的 method_1 () 函数,那么在运行时,我们希望调用 method_2 () 函数。那么这么一个函数调用的变化,肯定不能依靠系统调用来做,它管内核状态,不管应用层事情。也不能 C 运行时来做,因为 C 运行时是 C 语言特有的功能,不会单独为高级语言 Objective-C 来做这个事,本身它也做不了,因为它是静态语言,自身都没有动态性。so,肯定有一层单独为 Objective-C 做了这个事情,这一层,就是 OC 运行时。 我们通过 OC 运行时源码分析一下对象创建的过程。 如果我们有一个 OC_Person 类,如下: 1 2 3 4 5 @interface OC_Person : NSObject @property (nonatomic, copy) NSString *name; @end @implementation OC_Person @end 现在,我们要创建一个对象 person,即: 1 OC_Person *person = [[OC_Person alloc] init]; 详细调用流程图如下: 从上面的函数调用链,我们发现有两个关键点: Objective-C 进行对象内存初始化的时候,通过 Objective-C 的函数调用,最终调用到 C 语言的 calloc () 函数调用。 内存分配的大小,是通过”->” 结构体从一个 struct 拿到的。即调用一个结构体的 instanceSize 函数。(C++) 所以,我们在 Objective-C 里面创建一个对象并进行内存分配,开始的时候调用的是 OC 运行时,最终是调用了 C 运行时。我们创建的 Objective-C 对象本身,在运行时阶段,都是通过 struct 结构体获取值,所以对象在项目 Build 后,都转化成 C/C++ 结构代码了。 我们在看下下面代码执行流程: 1 [person setName:@"x"]; 详细调用过程如下: [person setName:@"x"]->objc_msgSend(person,SEL(setName),@"x")-> 汇编代码在运行时阶段查找 struct OC_Person{...} 结构体中的 setName 函数的地址 p_setName->call Oxab435c2(p_setName)。 上面函数调用过程分析如下: Objective-C 的函数调用,是通过汇编语言编写的 objc_msgSend 进行的中转。其实 objc_msgSend 本身就是一个中间层,是动态转发的入口,将函数调用中转到运行时阶段。 OC 运行时阶段进行函数地址的查找,在找到对应的函数地址后,进行地址调用 (函数执行)。 从上面对 OC 运行时的分析,我们可以看出,说 Objective-C 是 C 的超集,其实应该这样理解: Objective-C 是高级语言,在代码编译后,会调用 OC 运行时接口,进行相关操作如对象创建,方法查找等。而 OC 运行时接口的具体实现,则是依托 C 运行时实现的。 比如,我们创建的 Objective-C 对象,在编译后,都会转换成 struct 结构体的形式进行 OC 运行时调用。再比如,我们创建对象调用 OC 运行时的 alloc 接口,在内部,却是调用的 C 运行时的 calloc 接口 (显然,calloc 调用的是系统调用的 brk 接口)。 面向对象是面向开发人员的,OC 运行时负责面向对象的 Objective-C 代码和 C 运行时之间的沟通。 Objective-C 的标准库和 C 就不一样了。C 的标准库上面说到,是 C 运行时的一部分。对于其他高级语言来说,标准库就是单纯为应用层封装的动态库,不属于运行时的一部分了。 下面是具体的流程图: 我在之前文章中,也有一个 Objective-C 和运行时库的说明:Objective-C 和 Runtime。 Go 该如何被 iOS 系统执行 我们已经分析了 C 和 Objective-C 在 iOS 操作系统上运行的原理。 我们可以确定,我们写的代码,只要最终能够对接系统调用并编译成二进制交由操作系统运行,那么我们的代码就能运行。 我们写的代码都是高级语言代码,比如我们写的高级语言的函数调用:[OC_Person alloc],这个函数在编译后我们假设为 call Oxa1b2c3,其中 Oxa1b2c3 是 OC 运行时的_objc_rootAlloc 函数的虚拟地址。 在_objc_rootAlloc 内部会调用 calloc() 进行 C 运行时函数调用,我们假设为 call Oxd4e5f6。到此为止,我们自己写的 [OC_person alloc] 代码,我们知道在代码区里面,那么 Oxa1b2c3 和 Oxd4e5f6 这两个运行时的函数在哪里呢? 运行时说到底,也是代码。运行时有两种存在形式,一个是动态库,一个是静态库。 我们的操作系统都默认有 C 的动态库运行时。所以我们在 Linux A 电脑编译出来的 ELF 执行文件,在 Linux B 电脑上是可以直接运行的,就是因为 C 运行时库是用动态库的形式在执行文件启动后进行链接的。不仅仅操作系统,只要是计算机,大差不差都有 C 的动态库运行时,所以 C 语言才如此通用。因为只要写了 C 语言,不出意外到哪里都可以跑起来,除非用了特定系统的 api。 那如果有一个操作系统,真的没有 C 的动态库运行时,是不是就不能支持可执行文件了呢? 运行时存在两种形式,一种动态库,还有一种静态库。我们在编译可执行文件的时候,可以把运行时打到可执行文件中,这样刚才说到的 Oxa1b2c3 和 Oxd4e5f6 运行时函数就打到可执行文件中了,即代码区。这样,及时操作系统没有安装 C 的动态库运行时,可执行文件一样可以跑起来。只不过,这样的化,可执行文件就会变大,因为包含了静态库运行时的大小。 所以 Objective-C 能够在 iOS 和 Mac 上运行,就是因为这两个系统里面,有动态库 OC 运行时。 那 Objective-C 能不能在安卓手机或者树莓派上面运行呢?因为 Objective-C 不支持运行时的静态库链接,而安卓和树莓派上没有动态库 OC 运行时,所以就运行不了 Objective-C App 了,因为找不到对应的函数调用,即上面说到的 Oxa1b2c3 和 Oxd4e5f6。 Go 静态库运行时的必要性 所以,Golang 该如何在 iOS 系统上执行?Golang 本身是高级语言,肯定有运行时库,分别有动态库和静态库两个版本。因为 iOS 操作系统本身没有 Golang 运行时,那么在编写 Golang 的代码后,在编译链接的时候,把 Golang 的静态库链接到最终的执行文件中 (静态库或者动态库,或者叫框架),那么这串 Golang 编写的代码,就能够在 iOS 系统上完美的运行起来。 这个时候,Golang 运行时需要做那些事情呢? Golang 需要做一个静态库运行时,链接到执行文件中。因为 iOS 系统本身只有 Objective-C 运行时、C 运行时、C++ 运行时,没有 Golang 运行时。 Golang 原本肯定没有考虑运行在 iOS 上,所以 Golang 的运行时对接了 Windows 和 Linux 的系统调用。那么现在,Golang 的静态库运行时就需要对接 iOS 的系统调用。 当上面两个步骤完成,我们就可以通过 Golang 编写代码并导出 framework 库,在 iOS 系统上被执行。 我们写出 Golang 库,肯定还是希望被 Objective-C 调用,因为 Objective-C 和 C 支持混编,而 Go 有一个库 CGo,可以让 Go 和 C 连通,所以 Objective-C 这个时候就可以放心的调用 Golang 开发的库 / 框架了。 下面是 Golang 在 iOS 系统上的运行流程图: 幸运的是,Go 开发 iOS 所需要的库 / 框架,目前已经有发行版了,即 Go Mobile。我测试一下,简单一行代码,在编译后的嵌入动态库中,也有 1.5M,原因就是这个动态库中,有 Go 的静态运行时和 CGO 静态库。 其他高级语言如何编写 iOS 需要的动态库 其实不止 Go,Node.js 也一样可以用来开发 iOS 的动态库,有这个 Node.js for Mobile Apps。 Go 和 Node.js 能够写 iOS 动态库,那么按照同样的逻辑,其他高级语言也一样可以做这件事,比如,游戏开发中,很多就用 Lua 来做热更新,Lua 代码要被执行,也需要一个 Lua 的静态运行时嵌入到库中。 上面说到的,还都是业务功能库,不是 UI。那 Flutter 就是完全依靠 Dart 语言来做跨平台的混合开发方案。Flutter 的库会让 iOS App 的包体积增加 15-25M,就是因为里面有一系列的运行时和相关 UI 组件库存在。 我们通过上面 Go 的大小可以发现,运行时库本身没有多大,Flutter 库比较大的原因,是 Flutter 通过 Dart 完全重新实现了自己一套 UI 框架,所以代码量肯定是巨大的,框架体积自然就增大了。 限制高级语言的枷锁是什么 所以我们可以发现,至少在 iOS 上面,是没有高级语言限制的,只要高级语言有这个运行在 iOS 系统上的需求,都能实现。 而 Go 如果后期希望像 Flutter 一样实现 UI 框架,也一样没有问题。限制它们的,仅仅是业务需求罢了(实际上,对于 Go 和 Node.js 的 iOS 动态库开发,需求不大,所以都是试探性发展,因为 C 和 C++ 已经足够优秀,用 Objective-C 来开发本身也够用了)。举例而言,C++ 是编写稳定后台服务的热门语言,而基于 C++ 的 Qt,就可以用来做跨平台的 GUI。而 Swift 初期被用来开发 iOS/Mac App,现在也一样可以用作服务器开发。甚至 Javascript 只是浏览器端的脚步语言,引入 V8 引擎后,JS 已经花开两朵,前端和 Node.js 后台发展的都非常棒。 我们也可以认知到,高级语言的存在,只是特定场景的需求。如果当年苹果不开发 Objective-C,用 Java 来开发 iOS App,也完全可以的,只是苹果需要一套自己的能够被私有控制的开发体系。 语言是用来完成特定场景的工作任务,如果用 Objective-C 来写服务器的 I/O 多并发,显然没有 Go 和 Node.js 的事件驱动来的吞吐量大。而 Objective-C 后期能不能实现协程、多进程等特性?当然可以,就是需要不需要而已。 限制语言功能及发展的,仅仅是它的业务场景,而不在于语言本身或者操作系统。 Core Foundation 和 Foundation 的区别 我们在上面已经研究了语言和框架。框架和库的关系,在文章开头也已经说明。 这里就来研究一下 Core Foundation 和 Foundation 两个框架的区别和联系。 Core Foundation 是基于 C 开发的,Foundation 是基于 Objective-C 开发的。但是有一点,Foundation 是基于 Core Foundation 封装并实现了 Core Foundation 所没有的部分。 我们可以用下图来表示 Core Foundation 和 Foundation 的关系: Foundation 用 Objective-C 封装了 Core Foundation 的 C 组件,并实现了额外了组件供开发人员使用。而 Core Foundation 也有一些 Foundation 没能彻底封装的功能,这些功能是 Core Foundation 特有的。 下面可以看一下 Foundation 和 Core Foundation 的组件库都有哪些: 从图中,我们可以看到,Foundation 的组件是多于 Core Foundation 的,比如 NSBundle 在 Core Foundation 就没有体现。而 NSArray 就和 Core Foundation 的 CFArray 是对应的。反过来,Core Foundation 的 CFTree 和 CFBitVector 在 Foundation 里也没有体现,或许是在其他组件中使用到了这两个算法库。 因为 Core Foundation 是 C 实现的,虽然 Objective-C 能够兼容并调用 C,但是和 C 相互通信并转换,就不那么容易了。 其实 Objective-C 和 C 直接通信就像 Go 和 C 直接通信一样,是高级语言之间的通信。Go 有 CGO 库完成了这个中间层,Objective-C 虽然基于 C,有得天独厚的优势,但是如果没有官方实现,那还是会出现高级语言之间的代沟。 举例来说,现在有下面两个 C 和 Objective-C 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 C: typedef struct person{ int age; char *name; } Person; --- Objective-C: @interface Person : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSUInteger age; @end @implementation Person @end 我们现在创建一个 C 语言的 p 变量和 Objective-C 的 pp 对象,尝试将他们互通,代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 C: Person p = (Person *)malloc(sizeof(Person)); p->age = 10; p->name = "GJ"; Objective-C: Person pp = Person.new; pp.age = 100; pp.name = @"GJ2"; 开始通信1: pp = <Conver>p; NSLog(@"pp name is %@", pp.name); 开始通信2: p = <Conver>pp; printf("p name is %s\n", p->name); C 是面向过程的,Objective-C 是面向对象的。上面的 p 和 pp 的格式转化,目前来看的确是没有办法完成的。也就是说,缺少 <Conver> 这个环节。 Objective-C 本身是可以直接使用 C 代码的,虽然转化比较困难,但可以在不转化的前提下,直接调用 C 结构体变量进行使用。但是 C 却没有办法直接调用 Objective-C 对象了,所以这个时候可以写一个转换层,来完成这个工作: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Objective-C to C: C_Person * conver(Person *p) { C_Person *c_p = (C_Person *)malloc(sizeof(C_Person)); c_p->age = (int)p.age; c_p->name = [p.name cStringUsingEncoding:NSUTF8StringEncoding]; return c_p; } --- 使用: p = conver(pp); printf("p name is %s\n", p->name); free(p); 我们可以看到,通过这样中转的方式,我们可以将 C 和 Objective-C 相互转换并通信。 显然,大家也发现有些费事。虽然这些转换如果真的要写 C 代码,那么就必不可少。但是如果使用 Core Foundation,那会方便很多。 我们刚才说过,Foundation 是封装的 Core Foundation,苹果开发了一个强大的功能,即桥接(Bridge)。通过桥接,可以非常方便的实现 C 和 Objective-C 的数据转换,比如下面: 1 2 3 CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); NSMutableDictionary *oc_dic = (__bridge_transfer NSMutableDictionary *)(cf_mu_dic); // CFRelease(cf_mu_dic);// 注意,这里因为__bridge_transfer缘故,就可以不用执行CFRelease释放内存了。 对于 Core Foundation 使用过程中产生的变量,都可以通过桥接的方式,变成 Foundation 对象。桥接帮我们做了格式转换的同时,也帮我们做了 ARC。 刚才,我们在执行 p = conver(pp) 的时候,大家注意,后面使用了 C 语言的内存释放,即 free(p)。Core Foundation 本身也有引用计数,但是没有自动计数即 ARC。所以 Core Foundation 的对象释放的时候,需要调用 CFRelease,那么在桥接到 Foundation 后,就可以使用 Objective-C 的 ARC 了,非常方便。 桥接中的__bridge/__bridge_transfer/__bridge_retain 可以很方便的帮我们做对象管理转移操作,我们就不需要手动去释放内存了。 这里还是要补充一点,基于 C 语言的 Core Foundation 之所以能作为 Objective-C 开发框架,就是上面提到的,只要是高级语言,只要有相关运行时,就可以用来开发组件 / 库 / 框架。 Core Foundation 和 C 与 Objective-C 的转换 桥接(Bridge) 我们从上面 Core Foundation 和 Foundation 之间了解到,通过桥接,可以很好的转换 Core Foundation 和 Foundation 对象。 桥接做了两件事,一个是自动引用计数,一个是格式转换。 我们先说一下格式转换,因为桥接的转换局限性很大。 我们上面把 C 的 struct person 结构变量转换成 Objective-C 的 Class Person,需要自己写类似于 C_Person * conver(Person *p) 的转换函数。说明 C 和 Objective-C 之间转换本身是不能直接进行的。 但是 Core Foundation 的 CFArray、CFString 等和 Foundation 的 NSArray 和 NSString 等转换,通过桥接就可以直接转换。这是因为 Core Foundation 比较特别。Core Foundation 是苹果自己写的 C 代码,所以在桥接的时候,苹果拥有 Core Foundation 的数据结构和 Foundation 的对象细节,所以桥接可以自动完成转换工作。 而我们自己写的 C 结构体变量,和 Objective-C 对象之间,就不能很好转换了,如果我们写了 C 代码需要和 Objective-C 进行转换,就必须自己写一个中间层了。 这就是桥接对于数据格式转换的局限性,准确来说,桥接对数据格式转换,的确只在 Core Foundation 里面才有体现,毕竟如上所说,苹果自己知道 Core Foundation 和 Foundation 之间的所有细节。 这对于我们来说,其实已经完全够用了,因为我们真实业务开发场景,如果需要避免 Objective-C 的运行时带来的消耗,的确可以通过 Core Foundation 来编写代码。 下面再说桥接的另一个大杀器,那就是自动引用计数。 Core Foundation 在和 Foundation 进行转换的时候,可以通过__bridge/__bridge_transfer/__bridge_retain 进行自动引用计数控制,这个不在细说。 这里介绍 C 和 Objective-C 之间通过桥接进行引用计数控制。引用计数是针对 Objective-C 对象来说的,我们看一下 Objective-C 对象和 C 之间的转换: 1 2 3 4 5 6 7 8 9 10 11 12 Person *oc_p1 = Person.new; oc_p1.name = @"GJ"; oc_p1.age = 10; // 1. 假设这里因为业务代码需要,我们将OC对象转成C指针进行传递 void *c_p = (__bridge void *)(oc_p1);// c_p = 0x0000600003977500 // 2. 程序执行过程中,有各种原因可能会导致oc_p1对象计数-1,比如离开块区域等。这里我们通过置nil进行模拟 oc_p1 = nil; // 3. 这里需要把之前由C指针存储的指针还原成Objective-C对象 __weak Person *oc_p2 = (__bridge Person *)(c_p);// 这里有"__weak"避免计数影响。因为默认是"__strong",计数会+1 // 4. 这里使用还原后的Objective-C对象 NSLog(@"%@", oc_p2.name); 上面我们使用 **__bridge** 进行 Objective-C 和 C 的强制指针转换,表示不对计数进行任何改变。 在代码执行到第 3 步的时候,就会奔溃。 因为第 1 步前,堆对象的计数为 1,第 1 步没有改变计数,堆对象计数还是 1。 经过第 2 步,堆对象不在有引用计数了,所以堆对象就被释放了。 在第 3 步,想要使用 C 的 c_p 指针的时候,这个指针所存储的 0x0000600003977500 堆地址,已经变成野指针了,使用的时候直接会崩溃。 下面看下 **__bridge_retained 和__bridge_transfer** 的作用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Person *oc_p1 = Person.new; oc_p1.name = @"GJ"; oc_p1.age = 10; // 1. 假设这里因为业务代码需要,我们将OC对象转成C指针进行传递 void *c_p = (__bridge_retained void *)(oc_p1);// c_p = 0x0000600003977500 // 2. 程序执行过程中,有各种原因可能会导致oc_p1对象计数-1,比如离开块区域等。这里我们通过置nil进行模拟 oc_p1 = nil; // 3. 这里需要把之前由C指针存储的指针还原成Objective-C对象 __weak Person *oc_p2 = (__bridge_transfer Person *)(c_p);// 这里有"__weak"避免计数影响。因为默认是"__strong",计数会+1 // 4. 这里使用还原后的Objective-C对象 NSLog(@"%@", oc_p2.name); // 5. 这里继续将C指针存储的指针还原成Objective-C对象 __weak Person *oc_p3 = (__bridge_transfer Person *)(c_p); 这里代码运行情况分析如下: 第 1 步之前,堆对象计数为 1。经过第 1 步后,**__bridge_retained 会使得计数 + 1,堆对象计数变成 2。 经过第 2 步,堆对象计数变成了 1。 第 3 步__bridge_transfer** 会使得计数 - 1,堆对象计数变成 0,堆对象被释放。 第 4 步会打印 null,因为 oc_p2 本身为 null。 第 5 步,程序崩溃,因为 c_p 指针存储的堆对象已经释放,指针此时为野指针。 从上面两个例子,我们可以看到,在和 C 进行赋值的过程中,桥接帮我们做了引用计数的工作。和 Core Foundation 的转换过程中的计数规则是一样的。 我们在赋值过程中,使用 **__bridge_retained 和__bridge_transfer 可以有效的降低崩溃风险,因为这两种 bridge 方式,帮我们做了引用计数的加和减。 单独进行__bridge 赋值的时候,引用计数没有改变,相当于同一时间,有多个指针指向堆对象,但是对象的计数却和指向指针的个数不一致。如果对象被释放,很可能还有指针在指向,这个时候使用就会发生野指针。 通过 retained 和 transfer**,在赋值过程中,加 1 和减 1 是同步的,这样可以有效降低对象计数和指向指针个数不一致的野指针风险。 通过桥接给 C 和 Objective-C 赋值的风险 通过上面两个例子,或者写更多其他 Objective-C 和 C 指针赋值的代码后,就会发现这样写代码的风险非常大。最大的风险就是野指针和内存不释放。 如果完全写 Objective-C 的代码,OC 运行时已经帮我们处理了引用计数和对象释放后指针自动变 nil 问题,所以我们大概率不会出现野指针和内存不释放情况(OC 运行时的 Weak 表帮我们处理了对象释放后指针自动变 nil。而 Objective-C 的引用计数的内存管理方式,也容易因为循环引用导致内存不释放,这是引用计数管理内存的天然缺陷)。 但是在 C 赋值嵌入进来后,即使通过桥接进行计数管理,也依旧摆脱不了随时崩溃的风险。原因就是因为对象被释放导致野指针随时可能会发生,或者对象无法释放导致内存泄漏。 对于经常写 C 代码的程序员来说,应该不会担心这些问题,因为他们已经习惯内存需要手动管理。被拥有自动内存释放机制娇生惯养的程序员们,就需要注意这个风险了。 比如下面代码,就很发生内存泄漏: 1 2 3 4 5 6 7 8 9 10 11 12 13 void func { int number = 10; char *c_chars = (char *)malloc(sizeof(char) * number); memset(c_chars, 0, number); int i = 0; for (; i < number - 1; ++i) { *(c_chars + i) = 'a' + i; } NSString *oc_str = [NSString stringWithCString:c_chars encoding:NSUTF8StringEncoding]; free(c_chars); NSLog(@"%@", oc_str);// 打印“abcdefghi” } 上面代码中,前面用 C 语言申请了 10 个字节的堆空间,然后开始赋值被转成 Objective-C 的 NSString 对象。 oc_str 是 ARC 控制的,出了 func 函数作用域,内存就会被释放。可是如果忘记写 free(c_chars); 这行代码,就会导致 10 字节的内存泄漏。像这样的内存细节,防不胜防的同时又会慢慢耗尽内存空间。 所以,如果需要避免 Objective-C 的运行时带来的消耗而想采用 C 写业务,最好使用 Core Foundation,它和 Foundation 之间的桥接非常完美,一般不会出问题。而自己写 C 进行混用,野指针和内存不释放是挥之不去的地雷。 Core Foundation 的使用 Core Foundation 只是一个非常优秀的框架,但是苹果用 C 写的 Core Foundation 框架和 Objective-C 写的 Foundation 框架,不是 iOS 框架的全部。框架是库的抽象,用 Golang 等其他高级语言,一样可以写出优秀的框架。Dart 就是举足轻重的例子。 上文的截图中,给出了 Core Foundation 框架里面都有哪些好用的组件,比如 CFString、CFDate 等。下面的一些示例,是用 Foundation 不好实现的。 CFRunloop 介绍 iOS 的 Runloop 水还是很深的。我也写了 Runloop 的一篇文章,一直在草稿中未能发布。因为牵涉面太广,如事件驱动、线程休眠、自动释放池、UI 刷新等。通过 Runloop 能够更加清楚明白的理解 App 运行的原理,也可以做非常多有用的东西,如主线程卡顿监控、线程保活等。 Foundation 提供了 NSRunloop 供我们开发人员使用,但是 NSRunloop 有一个大坑,对于不了解 Runloop 的开发人员来说,很容易陷进去。 网上有很多 Runloop 的介绍,在介绍让线程执行一段时间的时候,会使用 [[NSRunLoop currentRunLoop]run]。我揣摩本意,发现他们并不是想要让线程永久长活,但是却使用了 run 函数。这样会使得当前线程永远无法释放,是永远。因为 NSRunloop 里面 run 函数是对 CFRunLoopRun() 函数的 true 循环封装,当结束一次循环后,NSRunloop 会立刻再次调用 CFRunLoopRun() 函数,没有任何办法可以销毁当前线程的 Runloop。这样,项目里面就永远的多出来一条可能已经不再需要的线程。主线程就使用的这个逻辑。 在 CFRunnloop 里面,仅有两种方式安全启动线程的 runloop,分别为 CFRunLoopRun() 和 CFRunLoopRunInMode,其中 CFRunloopRun 还是语法糖。这两种启动方式,都是一次循环,客户端可以自行控制啥时取消 Runloop,有效的降低 Runloop 未知风险。相关源码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CFRunloop.c // RunLoop 运行循环 void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { // 调用RunLoop执行函数(默认运行default Mode) result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); } // 切换并运行到对应的mode(运行modeName参数对应的mode) SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } iOS 的 runloop,就是通过调用 CFRunLoopRunSpecific()->__CFRunLoopRun() 实现的。其中,NSRunloop 的 run 函数,相当于下面代码: 1 2 3 4 5 - (void)run { do { CFRunloopRun(); } while(true); } 所以,线程永远也无法销毁。因为 CFRunloopRun () 函数会在 Mode 切换或者手动调用 CFRunLoopStop() 等情况下执行完毕,但是外部的 do-while true 循环,永远结束不掉。 这里,如果需要写 Runloop 相关的代码,我强烈建议使用 CFRunloop,而不要使用 NSRunloop。相比来说,CFRunloop 提供了比 NSRunloop 更加细致化的 Api,相比之下,NSRunloop 就寥寥无几了。 下面是我写的一些 CFRunloop 测试代码,因为 Core Foundation 是 C 语言写的,所以里面的组件都是面向过程的调用方式,和面向对象有些不同: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createThread) object:nil]; [self.thread setName:@"test thread 1"]; [self.thread start]; - (void)createThread { NSLog(@"the thread is [%@]", [NSThread currentThread]); // NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // self.port = NSMachPort.new; // self.port.delegate = self; // [runloop addPort:self.port forMode:NSRunLoopCommonModes]; // [self addObserver]; CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), (__bridge CFStringRef)@"dadada"); CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{ printf("abc"); }); NSLog(@"the runloop is [%@]", [NSRunLoop currentRunLoop]); [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]]; // CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, NO); NSLog(@"---end"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 主线程下调用"addObserver",可以实时查看主线程的Runloop状态 - (void)addObserver { CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreateWithHandler( kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"--1 即将进入loop kCFRunLoopEntry--"); break; case kCFRunLoopBeforeTimers: NSLog(@"--2 即将处理timer kCFRunLoopBeforeTimers--"); break; case kCFRunLoopBeforeSources: NSLog(@"--3 即将处理source kCFRunLoopBeforeSources--"); break; case kCFRunLoopBeforeWaiting: NSLog(@"--4 即将休眠 kCFRunLoopBeforeWaiting--"); break; case kCFRunLoopAfterWaiting: NSLog(@"--5 即将从休眠唤醒 kCFRunLoopAfterWaiting--"); break; case kCFRunLoopExit: NSLog(@"--6 即将退出loop kCFRunLoopExit--"); break; default: break; } }); CFRunLoopAddObserver(CFRunLoopGetCurrent(), runloopObserver, kCFRunLoopCommonModes); } CFDictionary 介绍 Foundation 里面有 NSDictionary 与之对应,如果我们希望用我们自定义的对象为 key,存储与 NS 字典中,直接存储是不行的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 定义Person类 @interface Person : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSUInteger age; @end @implementation Person - (NSString *)description { return [NSString stringWithFormat:@"name:%@, age:%lu", self.name, (unsigned long)self.age]; } - (void)dealloc { printf("Person Dealloc.\n"); } @end - (void)func { Person *p = Person.new; NSMutableDictionary *oc_dic = NSMutableDictionary.new; [oc_dic setObject:@"" forKey:p];// 这里会崩溃 } 上面代码中,如果我们把自定义 Person 类的对象 p 作为 key 存储到 NSMutableDictionary 中,运行时是会崩溃的。 因为 Foundation 规定,字典的 key 必须要实现 NSCoping 协议,字典在添加属性的时候,是调用 [key copy] 作为字典 key 的。 改写如下: 1 2 3 4 5 6 7 8 9 10 11 12 @interface Person : NSObject<NSCopying> @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSUInteger age; @end @implementation Person - (id)copyWithZone:(nullable NSZone *)zone { Person *p = Person.new; p.name = self.name; p.age = self.age; return p; } @end 如果我们使用 Core Foundation,就可以避开这个限制,即 Person 类不需要实现 NSCoping 协议,如下: 1 2 3 4 5 Person *p = Person.new; CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFStringRef cf_str_value = CFSTR("value"); CFDictionaryAddValue(cf_mu_dic, (__bridge const void *)(p), cf_str_value); CFDictionary 默认会对 Key 和 Value 做 retain,所以我们使用 **__bridge 即可。当 p 被当作 key 加入 cf_mu_dic 后,p 的引用计数已经变成 2 了。 如果我们使用__bridge_retained**,如下: 1 2 3 4 5 6 7 8 9 Person *p = Person.new; CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFStringRef cf_str_value = CFSTR("value"); CFDictionaryAddValue(cf_mu_dic, (__bridge_retained const void *)(p), cf_str_value); p = nil; CFDictionaryRemoveAllValues(cf_mu_dic); // 这里因为"__bridge_retained"缘故,p置空和移除CFDictionary所有元素后,对象的引用计数还是1,所以内存泄漏。 这里因为”__bridge_retained” 缘故,p 置空和移除 CFDictionary 所有元素后,对象的引用计数还是 1,所以内存泄漏。 CFDictionary 的 key 不需要实现 NSCoping 协议这一特性,YYModel 就有使用,这也是 YYModel 使用 CFDictionary 的最终原因: 1 2 3 4 5 6 7 8 9 YYMemoryCache.m _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key)); CFDictionaryGetCount(_dic); _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); CFRelease(_dic); YYModel 使用 CFDictionary,就是因为缓存对象是各式各样的,极大可能都是没有实现 NSCoping 协议的。 因为 YYModel 通过一个__unsafe_unretained 类型的双向链表来保存对象,所以 YYModel 需要一个容器来持有缓存对象防止被提前释放。 为了加快查询对象的速度,使用查找复杂度为 1 的 hash map 结构即字典 (CFDictionary),而非数组 (CFArray)。 CFDictionary 还有一个巨大特性,是可以吊打 NSDictionary 的,那就是可以自行控制引用计数。下图表示 CFDictionary 的创建函数及相关函数调用: 我们举例一下, 1 _dic = CFDictionaryCreate(CFAllocatorGetDefault(), keys, values, n, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 这里创建一个_dic 变量,这里每个 key 都会经过 kCFTypeDictionaryKeyCallBacks 结构体获取到 retain/release/copyDescription/equal/hash 进行函数调用。 比如,如果两个 key 一样,那么 equal 就会比对出 true,第二个 key 元素就会被过滤。注意,这里和 NSDictionary 不一样,NSDictionary 是完全 hash map table,两个元素如果一样,就会通过拉链法或者开放寻址法进行存储。但是在 CFDictionary 里面,如果两个元素 equal 为 true,则会过滤另一个。 然后,一个 key 被存储的时候,会调用 retain 函数进行引用计数 + 1。这里调用的是系统默认的,如图中所示,如果我们用自己的 retain 函数代替系统的,就可以实现引用计数的多变性: 1 2 3 void * Custom_CFDictionaryRetainCallBack(CFAllocatorRef allocator, const void *value) { return value;// 系统默认为return CFRetain(value); } 我们通过改写一个 key retain 函数,就可以改变 CFDictionary 的 key 在 retain 时候的计数是否 + 1。 如果执行 CFDictionaryRemoveAllValues(cf_mu_dic);,则字典中所有元素都会被移除,这个时候每个 key 都会被调用 release 函数执行引用计数 - 1 操作,我们也可以重写: 1 2 3 void Custom_CFDictionaryReleaseCallBack(CFAllocatorRef allocator, const void *value) { // 系统默认为CFRelease(value),现在啥都不做 } 我们改写 key release 函数,就可以使得 key 被移除释放的时候,引用计数不在 - 1。 这里,我们的操作性非常强,我们可以提供自己的函数地址,就可以实现多样化的 CFDictionary 引用计数逻辑,详细代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const void * custom_dictionary_key_retain(CFAllocatorRef allocator, const void *value) { return value; } void custom_dictionary_key_release(CFAllocatorRef allocator, const void *value) { } const void * custom_dictionary_value_retain(CFAllocatorRef allocator, const void *value) { return value; } void custom_dictionary_value_release(CFAllocatorRef allocator, const void *value) { } - (void)func { CFDictionaryKeyCallBacks custom_dictionary_key_call_backs = { 0, custom_dictionary_key_retain, custom_dictionary_key_release, CFCopyDescription, CFEqual, CFHash, }; CFDictionaryValueCallBacks custom_dictionary_value_call_backs = { 0, custom_dictionary_value_retain, custom_dictionary_value_release, CFCopyDescription, CFEqual, }; Person *p = Person.new; CFMutableDictionaryRef cf_mu_dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 10, custom_dictionary_key_call_backs, custom_dictionary_value_call_backs); // 这里的cf_mu_dic对于key和value的引用计数完全改变了,key和value在加入和移除的时候,引用计数都不会被CFDictionary改变 CFDictionaryAddValue(cf_mu_dic, (__bridge const void *)(p), (__bridge const void *)(p)); } 这里,我们描述了很多 CFDictionary 相比于 NSDictionary 的不同,有如下: CFDictionary 的 key 不需要实现 NSCoping 协议,NSDictionary 的 key 如果没有实现 NSCoping 协议,则会运行时崩溃。YYModel 等开源库主要就是使用了这个特性。 CFDictionary 的 key 如果相等,在元素不会被插入。NSDictionary 则会通过拉链法和开放寻址法进行数据存储。 CFDictionary 的 key 和 value 的引用计数,都可以自行控制。NSDictionary 的 key 用的 [key copy],value 用的 retain。 所以,CFDictionary 相比 NSDictionary 来说,扩展性也更强。 最近一直在喝 Luckin Coffee,最近因为收入造假,快要被纳斯达克下市了。

2020/5/23
articleCard.readMore

TCP 数据传输过程分析

最近在研究发送 / 接收缓冲区对 Socket 性能对影响,朋友发来两篇质量非常高的研究性报告让我看。 Socket 缓存究竟如何影响 TCP 的性能 TCP 性能和发送接收窗口、Buffer 的关系 做研究的,都少不了数据。上面文章中 “缓冲区对 Socket 性能分析” 的数据非常严谨,我非常佩服。 于是我用 WireShark 分析验证 TCP 的数据传输流程,过程包含三次握手、滑动窗口数据传输、四次挥手,收益很多。 头部字段说明 主要分析 TCP 的头字段,有序号、确认序号、ACK、SYN、Option、窗口大小等。详见下图 TCP 头字段说明: 部分内容分析了 IP 的头字段,如包大小等。详见下图 IP 头字段说明: 数据包长度的问题 一个网络包的最大长度,为 65535 字节,为什么是这个大小呢?因为在 IP 网络层,用于标记一个包大小的位数是 16 位,而 16 位能够标记的大小就是 2 的 16 次方,即 65535 字节 (-1)。 上图 IP 头字段中有一个 “总长度(16 位)” 字段,即包大小字段。具体抓包如下: 图中 “Total Length:64” 即当前包大小,通过下部分的绿色 bit 标记显示,可以看出,共有 16 个比特位用来标记 “Total Length” 大小,当前为 64 个字节,即 “00000000 01000000”。 所以,65535 字节大小的包,是 IP 网络层能够从上层接收的最大包大小。 那么 65535 字节的包,是否可以直接发送呢?显然是不行的,因为还有 MTU 的限制。 链路层还有大小限制,为 1500 大小,所以网络层对于大于 1500 字节的包,需要进行分片。 而且,MTU 不是一个定值,一个 1500 字节的包,在网络路由中,可能一个路由仅仅支持 500 大小的包,那么这个包就需要被这个路由分片。 所有分片的包,都需要网络层被重组,然后才能够上传到传输层(如果丢包,传输层会做校验,校验不通过会被丢弃)。而重组,也不一定是服务器完成。比如防火墙需要把包重组后才能做安全防护,所以重组在防火墙这里就可以发生了,当然,服务器的网络传输层也会做重组操作。 以上说明中,提到的字节大小,是包含发送的数据和每一层头字段的总和,所以实际数据会偏小 10-100 字节。 分片,是针对网络层来说的,因为网络层对接链路层,如果发现大于 MTU,就会分片后,交由链路层发送出去。 所以对于 UDP 来说,一个 2000 字节的包,IP 层可能会分成两片后发送出去。 IP 分片后,每个分片后的包单独发送,每个包都有可能丢包。对于 UDP 来说,一个包丢了,整个 UDP 的包都算丢失了。因为对方进行分片重组后,交由 UDP,UDP 会进行数据校验(UDP 的头部有 16 位大小的” 校验和” 字段),发现数据不完整,就会丢弃。所以这个时候这个包就算丢了。 而 TCP 显然不想和 UDP 一样,TCP 已经做分段了,TCP 自己维护了一套数据包的稳定传输,当然不希望 IP 层分片,这样会导致 TCP 的一个包,经过 IP 的分片不稳定传输后,更加不稳定,增加丢包概率。 所以 TCP 就自己维护分段逻辑,在三次握手的时候,确定一个合适的包大小,后续所有的包,都按照这个大小进行传输。 这个大小尽量能直接通过 IP 层而不会被分片(实际上,是否被分片,还由中间路由控制,因为不能绝对不分片),这样,通过滑动窗口,TCP 就很好的控制了包的传输。这里 TCP 确定的一个合适的包大小,就是三次握手的时候确定的。在第一次和第二次握手包里面,双方都会发送自己最大的 MSS,然后双方就有自己和对方的 MSS,取最小值,作为包大小。这个 MSS 存储于第一次和第二次握手包里面的 Option 里面,名为 TCP Option Maximum segment size。详见下图: 图中是抓互联网包中第二次握手的数据,可以看到绿色部分 TCP Option Maximum segment size:1460 bytes。这里第一次握手终端发给服务器的也是 1460 大小,我没有把图截出来。如果双方大小不一致,协商后会按照小的一方来。 这里可以发现,TCP 的包大小,是小于 1500 的。而后面进行正式数据传输的时候,都会以此大小为标准。 但是 TCP 包大小也不是说一定小于 1500,比如下图: 这个图里面,红色框中,Maximum segment size 已经达到 16344 字节,远远大于 1500。 其实这个图是本机服务器的场景,网络包都没有经过网关,是我在本机开了一个 http 服务抓的包。所以在第一次和第二次握手的时候,传输层会考虑当前网络环境,给出一个合适的大小值。 图中可以看到,红色框里面有很多 16388 大小的包传输,这是握手之后正式传输的数据包,表示每个包的大小有 16388 字节。之所以不是 16344,就是上面说到的,网络各层会添加各自的头部,这个也是需要一定空间的,数据包大小是发送的数据和层头部字段的总和,所以会有 10-100 字节的偏差。 三次握手过程 三次握手,首先能想到的就是 SYN 和 ACK。但 SYN 和 ACK 并不是虚无缥缈的东西,它们是实实在在的用头部字段表示的。具体可以看文章顶部 TCP 的头部字段,在中间部分有 SYN 和 ACK 字段,位于 Flag 标志位中,他们分别占据 1 位,是个 bool 值。 ACK 和 SYN SYN 是用来请求建立连接(建立套接字)的。第一次和第二次握手,在 C 和 S 端均发送了 SYN,表示双方均希望建立连接。 而 ACK 是 SYN 的答复,即” 请求建立连接回复”(同意建立套接字)。第二次和第三次握手,在 S 和 C 端均发送了 ACK,表示双方均回执了对方的建立连接请求。 当三次握手完成后,后面传输的所有数据包,ACK 的位都必须是 1,表示当前套接字已经建立。 详细可以看下图: 其实,三次握手的过程,完成了很多事情,远远不止建立稳定套接字这么简单。下面说几个我分析到的: 序列号 三次握手的时候会确定序列号。每个包传输的时候,都会带一个唯一的序列号。这个序列号在滑动窗口的时候用来做确认标记,当然还有其他用途,比较包超时等。 序列号是一个比较大的数,会以时间戳为依据,每 4ms 会加 1,这样可以防止超时的包最后又正确传输到接收方的过滤操作。 有一个需要注意的是,序列号和确认号是相对的。 序列号相对自己的应用层发送的数据包大小递增,只要应用层需要发送数据,那么在传输层每个包的序列号,都是上一个包的序列号加上上一个包的数据大小。如果是回执包,是不会增加序列号的,因为回执包,是 TCP 传输层维护数据完整性用的,都不会上传到应用层,所以不牵涉到应用层的发送数据。对于 TCP 层自己发送的用于维持数据完整性的数据包,不会增加序列号。(三次握手和四次挥手的序列号有些特别,下面会讨论。) 而确认号按照接收的数据包大小进行回执,告知对方自己接收了哪些包。因为滑动窗口的缘故,并不会每个数据包都给予确认,而是批量给予确认,所以确认号有可能会跳跃好几个接收的序列号。 序列号详见下图 (序列号是随机的一个比较大的数,在 wireshark 中默认显示相对序号,真实序号为 Sequence number (raw) 字段): 确认号详见下图: 奇怪的握手和挥手序列号 下面重点说一下三次握手和四次挥手的序列号问题。因为这两个阶段序列号比较奇葩,也就是为什么三次握手的时候,第二次的 ACK 要加 1,第三次的 Seq 要加 1。 准确来说,如果一个数据包需要重传,那么这个数据包一定非常重要。所以我们应用层的数据包都是需要重传的。而有些包,或许没有那么重要,我们举例来说,那就是第三次握手。 握手一定需要三次,这是我们都知道的,那么如果第三次握手就是没有发到服务端,难道后面的数据就不能传了吗? 比如说,第一第二次握手都很快完成了,但是第三次握手迟迟没有完成,乃至于一直在超时重发,那么是不是客户端就一定要等第三次消息包确认接收后,才能发送应用层的数据呢? 实际上,并不是。其实客户端在发送第三次握手后,马上就开始发送应用层数据了。如果第三次握手迟迟没有到服务端,但是服务端接收到了客户端发过来的后续数据,那么也认为客户端已经成功接收到了第二次握手的包,所以服务端也同样会建立套接字。 这个也叫做抢跑。 所以,对于那些不是非常重要的数据包,这些数据包的序列号是不需要增加的。而序列号增加的最终目的,就是为了做包的整合和过滤。所以细心的朋友看上面图的时候,客户端的回执包的 Seq 一直都是 631,因为回执包如果丢了,还有其他的回执包用于滑动窗口的验证,一个包丢了,问题不大。 第一次握手,客户端的相对序列号为 0,tcp 数据大小(TCP segment length)为 0。按照上面的序列号规则,那么对方的回执号也应该是 0,第三次握手发送的序列号也应该是 0(0+0=0 规则)。 但事实不是这样,服务端的回执是 1,第三次握手发送的序列号也变成了 1。 所以第一次握手,TCP 认为该包一定需要认真对待,如果丢了,一定需要重传,不然握手就没法建立了。所以 TCP 为这个包,默认做了序列号增加的操作。具体应该增加几,TCP 默认增加 1。 详见下图: 第二次握手和第一次握手一样,都非常重要,所以服务端的序列号也做了加 1 的操作,不在说明。 而第三次握手的序列号就没有加 1 了,如下图: 四次挥手也和三次握手一样的逻辑,在第一次和第三次挥手的时候,序列号都做了加 1,而第二次和第四次,就没有做加 1 了。 Window Size(Scale) 在第一次和第二次握手阶段,还确定了一个非常重要的东西,就是窗口大小。我们都知道 TCP 是基于滑动窗口来实现流量控制、顺序传输、丢包重传这三个重要机制的。但很多人不知道,窗口的大小,其实在握手阶段就已经最初确定。这里说最初两个字,是因为窗口大小在数据传输过程中,还会变化。 握手中的窗口大小 在第一和第二次握手的过程中,双方会互相发送 window size 头字段,表示窗口大小。最终会取最小值参与窗口大小的计算。 之所以说参与窗口大小的计算,而不是确定,是因为第一第二次握手确定的窗口大小,并不是最终大小。还有一个 scale 字段,最终窗口大小的值是 scale 和 window size 相乘的值,即 scale * window size。 scale 字段,在第一次和第二次握手的时候,是在 Option 字段里面存储的,占据 3 个字节大小,存储的是位移运算的偏移。如果 scale 值为 6,则为 64,即 1 进行左移 6 位,如果值为 7,则为 128。 这个 scale 说来还有一个特别,在握手阶段确定之后,就不会改变了。 在第一次和第二次握手里面,scale 已经协商并确定,但是最终窗口大小的值确是由第三次握手确认的。说起来有点绕,我举例说一下。 首先说一下为什么需要 scale。在文章首部,TCP 头字段里面,有一个窗口大小的标记位,共 16 位。也就是说,TCP 默认支持的滑动窗口大小,最大为 2 的 16 次方,即 65535 字节。 但是随着互联网的快速发展,网络越来越好,带宽越来越大,65535 字节的窗口大小,已经不能满足客观的互联网需求,就是说窗口太小了。 所以这就弄出来一个 scale,用 TCP 头字段的 window size * (1<<scale),来标记最终窗口大小。 这里举个例子,如果客户端 Window size = 1000,scale = 6,服务器 Window size = 2000,scale = 7,那么最终窗口大小就是 1000 * (1<<6),即 64000 字节大小。 但是有时候呢,客户端和服务器的资源可能都非常好,比较内网或者本机环境,这个时候客户端 Window size = 65535(满了),scale = 6, 服务器 Window size = 65535(也满了),scale = 6。 这个时候,如果计算滑动窗口,65535*(1<<6)=4194240字节=4M。这个时候滑动窗口又太大了,所以这个时候就需要在第三次握手的时候,重新计算 Window size。第三次握手的时候,TCP 的头部 Window size 可能会变成 6739,这个时候窗口大小为 6739*(1<<6)=408256字节。 具体流程详见下图: 上面说到,scale 在第一次和第二次握手的时候,就已经确定,并且后面不会更改。我在抓包的时候,的确没有在其他数据包里面发现 scale 字段。 TCP 毕竟只是传输层协议,它不管数据是啥,只管传输。那么后续如何根据 scale 来计算窗口大小的呢? 我猜测是把这个字段放到套接字里面了。原因有两个,一:我的确没有找到其他数据包里面有 scale 标记,二:scale 是因为互联网快速发展起来后才加上的,这个时候 TCP 头字段已经确定,并且写死到计算机内核中无法修改。所以只能放在其他位置,比如套接字。 传输过程中的窗口大小调整 因为滑动窗口的机制,如果发送方发送的数据,接收方能够及时消化掉,那么滑动窗口就保持不变。 如果接收方来不及处理发送方发过来的数据,会导致接收缓冲区满,这个时候发送方继续发送数据,接收方也无法读取了。 所以发送方就选择暂时不发送数据。发送方如何知道接收方来不及处理这么一个状态呢?那就需要接收方通过滑动窗口告知到它了。 所以发送和接收方,在数据传输的过程中,会及时的将双方的窗口状态发送到对方。这里对方指的是客户端或者服务端。因为客户端和服务端都可能成为发送方或者接收方。 在数据传输这个过程中,发送和接收方的窗口,是不一样大的。因为这是客户端和服务端两个端的状态,发送方发出的数据,如果接收方一直没有响应,那么发送方的数据只能继续呆在发送缓冲区,这个时候发送方的窗口可能还比较大,而接收方因为无法处理更多数据,可能窗口都已经关闭了。 具体抓包如下: 传输过程中,窗口变化情况很多,上面也只举例说了一部分。 四次挥手 四次挥手,抓包上看过程和三次挥手很相似。主要有下面几点需要注意: 双方都可以发送断开连接请求。即 C 和 S 均可以主动发起第一次挥手。 FIN 断开连接(销毁套接字)。第一次和第三次挥手,会发送这个 FIN。 ACK 断开连接回复(同意断开连接)。第二次、第三次、第四次挥手,会发送这个 ACK。 第三次挥手后,消息接收方会发送第四次握手,并立刻处于 TIME_WAIT(等待)状态。这个状态会等待 2MSL 时间后,才断开连接。这里需要等待的时间足够长(2MSL),因为第四次挥手可能会丢包,所以需要重发,所以不能立刻断开。如果立刻断开,另一端收不到第四次挥手,可能认为对方还不想断开连接,那么套接字就会一直存在,消耗资源。还有,MSL 是包在网络上的最长时间,超过这个时间,包就认为需要被丢弃,会回执 ICMP 的错误回执包。MSL 的值不等,一般为 30s/60s/120s。 还有一点,上面说到一端收不到第四次挥手,可能会一直保留套接字,导致消耗资源。其实当它发出第三次挥手,并迟迟等不到第四次握手,相当于它认为它发出的第三次挥手包丢了,所以会重发第三次挥手包。这个时候,如果另一端已经过了 2MSL,会自动销毁,然后重发的第三次挥手包,会没有接收方,ICMP 会告知错误,这个时候它也可以判断另一端已经走了,自己可以销毁资源了。 抓包如下: TCP Keep-Alive TCP 会自行保持 TCP 通道的稳定性,这个和 HTTP 的 keep-alive 不一样。TCP 的 Keep-Alive 是纯通道层的心跳,用于验证当前双方的连接(套接字)是否稳定。 具体发送时机和失败后的发送次数和间隔,操作系统有默认参数,也可以手动调整。默认为 7200s 后发送第一个检测包,之后每隔 75s 发送一次。如果没有收到回执包,则连续重试 9 次,每次重试时间翻倍。 具体发送信息为:[TCP Keep-Alive] 和 [TCP Keep-Alive ACK] 详见下图: 滑动窗口 上面在介绍窗口大小的时候,已经说明过滑动窗口的抓包数据分析。 具体来说,在数据传输过程中,应用层可能不会立刻读取缓冲区数据,所以有一部分已经接收的数据依旧存储在缓冲区中,但他们还没有被读取,而且它们占据了一部分缓冲区大小。这个时候窗口大小就需要调整了,因为发送方如果继续按照之前的窗口约定一直发送数据,缓冲区已经不能接收更多数据了,所以这些包都会被丢弃,而发送方一直会收不到包接收成功的回执包。所以通信双方一定要实时约定窗口大小。 滑动窗口,主要是为了不把接收方的缓冲区塞满,这样就实现了流量控制。也同时实现了顺序整理,丢包重传策略。 在滑动窗口大小标记为 0 后,双方会间隔性发送探测包,询问当前是否已经有合适的窗口继续发送数据。如果拥堵方缓冲区数据被读取了,这个时候窗口大小充足后,会立刻发送 [TCP Window Update] 消息包,告知另一端,可以继续发送数据了。 丢包重传优化 在上面说 [TCP Window Full] 的时候,我们遇到了一个重传包的情况,即 [TCP Retransmission]。当时接收方缓冲区非常紧张,导致不能及时的消化发送方的数据,所以 170ms 内没有给发送方发送回执包。发送方以为包丢了,所以重传了。 每一个包,如果在规定时间里面没有收到回执包,即认为丢包。丢包后,会根据指数级的延时进行重发。这里就会有一个问题,如果一个包在前几次都发送失败了,后面会效率很低,因为要很久才能够重发。在滑动窗口的时候,会有一个冗余回执的优化,即按照 1-9 这 9 个包排序,其中 5 包丢失了,后面接收方在收到 7、8、9 包后,会发送 4 包的回执,表示 5 包没有收到。这样发送方在收到 3 次 5 包的冗余回执后,会立刻进入快速重传,避免了超时周期过长的问题。 太累了,累哭了,写技术文章太累了。 写字加画图,喝了两大杯咖啡和一罐红牛,真费钱。 远远没有牛逼好吹。 如果有人说牛逼难吹,那一定是书看少了知识不够渊博。 我就很佩服牛逼吹的又响又脱俗的人。

2020/5/13
articleCard.readMore

iOS main 文件中的 @autoreleasepool 的真实用途

这个问题,在很多年前其实是经历过血腥风雨的。 就是下面的代码,Xcode 创建项目的时候,会生成下面的 main.mm 文件: 1 2 3 4 5 int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } 问题就是,main 里面的 autoreleasepool 是做什么用的? 前几年是 iOS 炙手可热的疯狂年代,面试官们会经常问到这个问题,后来,这个问题后来越来越淡了,因为没有人能回答的好它。 甚至于,能回答这个问题的人,都是瞎扯淡,我可以这样确认。 这个问题,我曾经研究过很久,我把可能的牵强逻辑都灌进去,也无法得出结论。那时我给自己的答案是: 我确认这个 autoreleasepool 没有任何用处,不管别人怎么说,除非拿出实锤,否则我确定它没任何用途。 苹果是一家特别的公司,不会恶意强奸用户,所以我相信它有特别的用处,但是我不知道是啥。 显然,这是一个两面派的答案。一来我相信自己的逻辑判断,二来我相信苹果一定别有用途。 我本以为这个问题,至少有经验的 iOS 开发一定不会问,因为问就是自讨没趣,这是一个没有答案的问题。 如果有人能给出答案,那就像回答” 鲁迅写的那个字妙在哪,体现了他当时什么心情 “一样的天马行空,我可以确认能回答出来的答案绝对是天马行空的。 显然,我写这个文章的原因,是有人问了。 我甚至听到这个问题的时候,以为这个已经被淡忘好多年的经历过血腥风雨的问题又要卷土重来一遍。 我按捺不住自己的复杂心情,又打开了 google,想看看最近是不是又有什么新的傻逼讲解出来了。 显然,我依旧一无所获。 那种不死心,很多时候是天生的,我在 5 月 5 号,写下文章标题,用来激励自己,这次,一定要找到答案。 中间断断续续的,把相关书籍的部分章节重点看看,把 OC 相关源码重点看看,和同事聊聊聊,一直到今天,5 月 9 号。 我想看看 Xcode 创建 Mac 项目的 main 文件长啥样,如下: 1 2 3 4 5 6 7 // Mac app int main(int argc, const char * argv[]) { @autoreleasepool { // Setup code that might create autoreleased objects goes here. } return NSApplicationMain(argc, argv); } 反正我立刻就惊了,Mac 和 iOS 的 main 文件竟然是不一样的。我立刻创建一个 iOS 项目,如下: 1 2 3 4 5 6 7 8 9 // iOS app int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } 我当场就笑了。 不知道 Xcode 的哪个版本开始就改变了项目的 main 初始代码,至少,我一直没有注意它。 这个能够引发血腥风雨和奇思淫计的问题,总算可以成为历史了。 现在如果有人问 “main 文件中的 @autoreleasepool 的用途”,就是光明正大的问自动释放池的释放原理了。

2020/5/5
articleCard.readMore

多线程的难点在哪里

不管在哪门开发语言中,多线程都是绕不过去的开发方式。多线程本身也不是开发语言的一部分,只是开发语言会对多线程进行语言级别的包装。 多线程属于计算机原理的一部分。每个应用都需要操作系统分配一个进程后才能在进程中执行,为了实现并发效果,才有了多进程方案。而进程切换开销太大,这才有了多线程方案。乃至于多线程也是有不小的内存和 CPU 开销,后面的协程才开始起家。 但是协程已经不属于计算机的范畴,协程是单线程的,需要语言级别实现的。有些语言并没有实现协程,也只能使用多线程的方案对 CPU 资源进行深一步的榨干。 像 iOS 开发里面,每个应用都是独立的沙盒,官方并没有提供多进程的方案 (操作系统级别肯定要支持),也没有提供协程方案,如果想更有效的利用 CPU 资源,只能从多线程上面入手。 iOS 里面多线程虽然理解起来有点绕,尤其任务和队列嵌套的时候。但只要多使用几次并加以过程分析,多线程的使用也就能很好的过关了。因为 GCD 本身已经对多线程有了非常棒对封装,只要不自己作死,串型队列和同步任务一起处理的时候小心一些,就不会出现队列等待导致死锁。即使死锁了,像这样的系统中断性问题,也很容易排查和处理。 那多线程,难点在哪里呢?显然不是 Api 级别的多线程接口调用。 移动端界面开发,要保障不出现重大事故,很大程度上不在于 UI。因为 UI 是寄托于数据展示的,只要数据不出现大问题,UI 都是写好的机器代码,基本不会出问题。 但如果数据有间歇性的不稳定,那么对于 App 来说就是一个隐藏的地雷,因为谁也不知道哪个时刻会发生数组越界,或者数据为空。 所以,数据的稳定性和完整性,非常重要。 在多线程情况下,我们会操作 UI 吗?显然不会,多线程更多处理的都是数据。所以多线程难点可以从数据下手。如果多线程情况下数据的可靠性无法保证,宁愿放弃多线程 (FMDB 就是一个例子) 。 多线程情况下,数据的稳定性和完整性的保障,很复杂吗?其实也并不复杂,因为就两个要素点:原子性粒度的大小和锁。 原子性粒度的大小,不会太难控,有些编程经验,不会处理的太差。 而锁机制翻来覆去就那些,系统能够支持的甚至更少。在 Java 里面就有很多不同的锁,在 iOS 里面算来算去就那么几个,还是新瓶装旧酒。 那多线程,难点主要在哪里呢?其实还是数据。难点在于数据的处理!复杂度上的消耗,计算机时间片的消耗。 多线程编程,最终考量的其实是数据结构和算法! 对于上面的诸多观点,下面一一进行分析: GCD 线程操作的理解 iOS 下面,官方提供的多线程方案有 Pthreads、NSThread、GCD、NSOperation。 Pthreads 是完全 C 开发的,相关函数调用感觉会非常小巧,感受一下,如 pthread_create(x ...),pthread_exit(NULL) 这样。我一直很喜欢这种面向过程式的函数调用开发方式,显然这样的函数也是和面向对象格格不入的。 NSThread 是对 Pthreads 的 OC 封装,方便使用了一些。但是他们两个都需要自行管理线程生命周期。既然已经面向对象开发,连内存都可以自动释放了,我们还是应该使用更加一体化的多线程方式,那就是 GCD 和 NSOperation。 NSOperation 将多线程的面向对象更加具体化,在处理比较复杂和大型的多线程场景下,非常适用,因为代码理解性和可读性非常高。 其实,iOS 开发人员使用的基本都是 GCD,无出其右。因为工作场景下,GCD 完全可以完成多线程任务了,性能也足够好,使用又方便,代码简短易懂。如果不是复杂和大型的多线程场景,基本也不会去用 NSOperation。 而且 GCD 还有一个大杀器,那就是线程安全锁。GCD 将多线程操作和线程安全都涵盖了,我们可以很方便的使用 dispatch_barrier_async 写出读写锁,也可以使用 dispatch_semaphore 写出二元和多元信号量锁。 可以说,使用 GCD,把多线程开发的大部分问题一套带走了。当然还有一个没有带走的,就是上面提到的 “数据结构和算法”。 很多人对 GCD 理解困难,其实是被三个方面困住了。一个是不理解队列这种数据结构,一个是不理解任务这种执行方式,一个是不写代码进行测试和分析执行过程。 同步任务和异步任务 一个任务是同步还是异步,是依靠线程的。因为我们函数执行过程中,只可能在一个线程里面执行。如果是同步任务,那么不可能换线程,如果是异步任务,那么必须要换线程。 函数执行是一个函数调用执行栈空间,通过 rbp 和 rsp 两个寄存器不断上下移动栈指针位置来实现的。而一个函数调用执行栈就专属于一个线程。 如果是同步任务,只能在当前函数调用栈执行,所以开启不了新线程。 如果是异步任务,必须要脱离当前函数调用栈,必须要开启新线程。(不开启新线程也可以,就是协程方案。但是 OC 不支持协程,所以只能开启新线程。) 同步任务要点: 同步任务立刻被放入队尾(但是不一定立刻执行,因为队列里面可能已经有任务 X 和 Y,则必须等 X 和 Y 出队 [如果是同步任务还必须执行完] 后才能执行被放入队尾的同步任务)。 同步任务一定要被执行完后才能继续后面的代码执行。 不具备开启新线程能力。同步任务被调用的时候在 A 线程,执行也一定在 A 线程。 异步任务要点: 异步任务立刻被放入队尾(但是不一定立刻执行,因为队列里面可能已经有任务 X 和 Y,则必须等 X 和 Y 出队后才能执行被放入队尾的异步任务)。 异步任务因为肯定会开启新线程,所以后续代码立刻执行。 具备开启新线程能力,而且一定要开启。但是开启线程数量由队列决定。异步任务被调用的时候在 A 线程,执行一定不在 A 线程。 串行队列和并发队列 队列 (Queue) 是非常基本的数据结构,基于数组或者链表这两种物理结构实现。队列它的特点就是:外部数据从队尾入队,内部数据从队头出队。 比如这个队列:队尾->A->B->C->队头,如果现在加入外部数据 D,那么 D 只能添加在 A 的后面,想插队添加到指定 index 是不可能的。而如果内部数据想被删除,只能先删除 C,然后才能继续删除 B 和 A。想插队删除元素,也不可能。 串行队列要点: 允许开启线程,最多开启 1 个线程,是否开启线程由任务决定。 所有任务必须依次出队,必须上一个出队的任务处理完,下一个任务才允许出队并执行。 并发队列要点: 允许开启线程,可以开启多个线程 (100 以上都有可能,依靠系统调度),是否开启线程由任务决定。 所有任务必须依次出队,但是下一个任务出队不需要上一个任务执行完。 用上面 ABC 队列举例,如果 A、B、C 任务分别需要执行 10s。 相关伪代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 被外界业务调用的函数 - (void)test { /* ... 这里有业务代码执行,标记这块区域为X,此时X执行代码所在线程我们假设为thread_1。(thread_1也就是当前test函数被执行所在的线程。) ... */ dispatch_queue_t queue = 队列; 任务C(queue, ^{ [NSThread sleepForTimeInterval:10]; }); 任务B(queue, ^{ [NSThread sleepForTimeInterval:2]; }); 任务A(queue, ^{ [NSThread sleepForTimeInterval:2]; }); /* ... 这里有业务代码执行,标记这块区域为Y,此时Y执行代码一定也在thread_1线程 ... */ } 在串行队列下有下面可能: X 为 thread1,任务为 thread1,执行过程为:X_thread1->(C->10s 后 ->B->10s 后 ->A->10s 后)_thread1->Y_thread1(A、B、C 均为同步任务) X 为 thread1,任务为 thread2,执行过程为:X_thread1->(C->10s 后 ->B->10s 后 ->A->10s 后)_thread2->Y_thread1(A、B、C 均为异步任务) X 为 thread1,任务为 thread1 和 thread2,执行过程为:X_thread1->(C->10s 后)_thread1->(B)_thread2->(A->10s 后)_thread1->Y_thread1(A、C 均为同步任务,B 异步) 分析一下:因为 C 是同步任务,所以必须 C 执行 10s 后,后面的任务才能出队。因为 B 是异步任务,所以 C 执行完后,B 先出队,但 A 不用等 B 执行完即可出队执行。所以 B 和 A 可以说是并发执行的。 A 出队后,只有当 A 被执行完成,后面的业务代码才能继续执行。 在并发队列下有下面可能: X 为 thread1,任务为 thread1,执行过程为:X_thread1->(C->10s 后 ->B->10s 后 ->A->10s 后)_thread1->Y_thread1(A、B、C 均为同步任务) X 为 thread1,任务为 threadx,执行过程为:X_thread1->Y_thread1->(C)_threadx->(B)_threadx->(A)_threadx(A、B、C 均为异步任务) 分析一下:首先 ABC 都是异步任务,在并发队列里面都会开启新线程,所以 X 执行完后把 ABC 添加到队列后,不会等 ABC 的执行过程,直接就会执行 Y 了。为什么呢?因为 ABC 在其他线程,由其他线程负责执行,ABC 的代码执行调用栈都不在 thread1 上面,而是在他们各自对应的线程。 其次,ABC 三个任务,C 会先出队,然后是 B,然后是 A。他们出队顺序是固定的,但是因为他们各自在各自的执行线程,所以执行的先后顺序是不确定的。 ABC 是否开启多个线程有系统决定。如果系统开启 3 个线程,那么 ABC 会各自在自己的线程执行,没有先后顺序。如果系统仅仅开启 2 个线程,那么 A 会被分配到 C 或者 B 的执行线程,这个时候 A 就必须要等 C 或者 B 执行完才能执行(代码执行依靠调用栈,当前在执行 C 或者 B,就不可能执行 A,只能等 C 或者 B 执行完,当前调用栈结束,才能继续执行 A)。 X 为 thread1,任务为 thread1 和 thread2,执行过程为:X_thread1->(C->10s 后)_thread1->(B)_thread2->(A->10s 后)_thread1->Y_thread1(A、C 均为同步任务,B 异步) 分析过程和串行队列一致 上面的结果都只是一小部分。因为任务可能会有多个同步和异步穿插,所以整体执行过程会更复杂 (如 A 同步 + B 异步 + C 同步 + D 异步等)。但只要记住上面同步异步及串行并发的要点部分,整体抽丝剥茧来分析,过程并不难理解。 多写测试代码多分析 多写一些测试代码并分析过程,GCD 的内容很快就能理解。 多写一些串行队列嵌套同步任务,很容易出现死锁,很快就能根据上面的几个要点分析出来死锁原因。 所以死锁和主队列没多大关系,只要是串行队列嵌套同步任务,都可能出现死锁。 主队列就是串行队列的特殊形式,因为主队列比串行队列更严苛,主队列不能开启新线程,只能在主线程运行,这就是主队列的要点,其他和串行队列一致。 比如下面这个多线程代码分析: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 被外界业务调用的函数 - (void)test { /* ... 这里有业务代码执行,标记这块区域为X,此时X执行代码所在线程我们假设为thread_1。(thread_1也就是当前test函数被执行所在的线程。) ... */ dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL); dispatch_async(dispatch_get_global_queue(0, 0), ^{ // C dispatch_sync(queue, ^{ [NSThread sleepForTimeInterval:1]; }); }); // B dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:2]; }); // A dispatch_sync(queue, ^{ [NSThread sleepForTimeInterval:2]; }); /* ... 这里有业务代码执行,标记这块区域为Y,此时Y执行代码一定也在thread_1线程 ... */ } 它的执行流程是:X_thread1->(C->1s)_thread1->(B->2s->C->2s)_thread1->Y_thread1。 分析:C 被异步加入到串行队列里面,所以这个时候队列里面已经有 C。因为 C 是异步加入的,所以 B 代码也会立刻执行并被立刻加入串行队列。但是 B 没有办法执行,因为 C 需要 1s 才能执行完成。当 1s 过后,B 才能出队并执行。所以代码会在 B 处停 3s 然后才能将 A 加入队列并执行。 多线程数据处理之原子性粒度 如果上面 GCD 你已经能够熟练的分析并使用了,可能会有种大悟的感觉,原来多线程也不过如此。 可千万不要认为,上面 GCD 的使用,就是多线程的全部,相关多线程的坑来说,上面都是皮毛,多线程的坑完全不是串行队列死锁那么简单。 我们看最简单的一种情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @interface VC () { int _number; } @end @implementation VC - (void)test { _number = 0; dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); for (int i = 0; i < 10000; ++i) { dispatch_group_async(group, queue, ^{ self->_number = self->_number + 1; }); } for (int i = 0; i < 10000; ++i) { dispatch_group_async(group, queue, ^{ self->_number = self->_number - 1; }); } dispatch_group_notify(group, queue, ^{ printf("the _number is %d\n", self->_number);// 很少会打印0,有可能是-12,有可能是10,反正很少为0 }); } @end 上面,我们多个异步线程操作_number 成员变量,分别进行 10000 次增减 1,最后结果不是 0。这就是多线程的一个坑。 出现上面的原因就是,self->_number = self->_number + 1; 这行代码不是原子的。 你可能会觉得,那这样的等号赋值可能有问题,那 ++self->_number; 这样的方式进行增 1 操作会不会正常? 结果就是,一样不正常。不正常的原因,依旧因为 ++self->_number; 这行代码也不是原子的。 那什么是原子的操作? 一条 CPU 执行的一个单指令,就是原子的。++、-- 这样的高级语言,在汇编后都会被编译成好几个操作指令。当计算机把操作指令执行了一半的时候,另一个线程也会开始执行,这个时候前一个线程就会被系统调度打断,去执行下一个线程的指令。所以,数据这个时候就产生了紊乱,导致 ++、-- 操作完全乱套了。 问题已经讲述清楚了,那该怎么解决呢?就是要手动制造原子性。一个操作指令是原子的,但是我们不可能把高级语言写出操作指令的形式,那样就回到汇编时代了。所以我们需要在外部制造更大的原子性区域,在这个区域里面,同一时间只能有一个线程操作。这样,就不会出现区域里面的代码执行一半转而另一个线程闯进来了。 原子性是有粒度大小的,如果粒度过大,则多线程间接变成单线程。如果粒度过小,则可能不足以保障原子性。 下面举例分析: 粒度过大: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 - (void)test { _number = 0; dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); for (int i = 0; i < 10000; ++i) { dispatch_group_async(group, queue, ^{ @synchronized (self) {// 这里为了测试方便,使用了self,开发过程中不要这么用,应该维持一个私有对象用来做标记 [self add]; } }); } for (int i = 0; i < 10000; ++i) { dispatch_group_async(group, queue, ^{ @synchronized (self) { [self sub]; } }); } dispatch_group_notify(group, queue, ^{ printf("the _number is %d\n", self->_number); }); } - (void)add { /* ... 这里有很多代码逻辑,预计1000行 ... */ ++_number; /* ... 这里有很多代码逻辑,预计1000行 ... */ } - (void)sub { /* ... 这里有很多代码逻辑,预计1000行 ... */ --_number; /* ... 这里有很多代码逻辑,预计1000行 ... */ } 上面我们的多线程操作主要是 add 和 sub 两个函数,业务处理也都在这两个函数里面。但是我们在调用 add 和 sub 函数的时候,通过 synchronized 锁临时添加了原子性区域,这就导致 add 和 sub 里面的 2000 多行代码,同一时间只能一条线程执行,变成了单线程操作。多线程形同实亡。 粒度过小: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @property (atomic) int num; - (void)test { self.num = 0; dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); for (int i = 0; i < 10000; ++i) { dispatch_group_async(group, queue, ^{ ++self.num; }); } for (int i = 0; i < 10000; ++i) { dispatch_group_async(group, queue, ^{ --self.num; }); } dispatch_group_notify(group, queue, ^{ printf("the _number is %d\n", self.num);// 依旧不为0 }); } 我们定义了一个原子性 (atomic) 的 num 变量,但是最后打印的 num 依旧不为 0。原因就是 num 虽然已经在 set 和 get 方法里面添加了 synchronized 锁,但这个锁只能保障 num 变量在读和取的时候是原子性的。如果两个线程同时读,这个时候两个线程获取到的值是一样的,但是一个线程增 1,一个线程减 1,最后两个线程原子性调用 set 方法赋值。显然,num 的值这个时候就以最后调用 set 方法的线程值为准,不在准确了。 这个时候就是原子性粒度过小,导致虽然添加了锁,但依旧值不准确。 所以为了多线程处理能力最大化 (足够榨干 CPU 资源),也为了数据依旧稳定和准确,原子性的粒度需要考量一个合适的区域。 多线程数据处理之锁 原子性粒度考量完成后,下面就是如何保障这个原子性区域的问题了。上面我们简化说明都使用了 synchronized 锁,但是多线程本身还有其他各种锁,synchronized 只是其中使用最方便但是效率也最低的一种。 iOS 下锁比较好理解,因为不像 JAVA,iOS 下锁就那么多,下面根据多线程锁机制来分析。 《程序员的自我修养 - 链接、装载与库》一书中,在说到线程安全锁机制的时候,概括说了 5 种锁,而 iOS 里面所有锁就是基于其中 4 种来的。 5 种线程锁分别是:信号量、互斥量、临界区、读写锁、条件变量。 信号量:是线程级别的,任何一个线程均可以自行加锁和解锁,任何一个线程也可以对另一个线程已加的锁进行解锁。 互斥量:也是线程级别的,但是比信号量严苛一些。任何一个线程均可以自行加锁和解锁,但是 A 线程不能对 B 线程已加的锁进行解锁,必须有 B 线程自己解锁。(iOS 里面,A 也可以解 B 加的锁,没有报错。) 临界区:进程级别的。比互斥量严苛了一些。加锁解锁被称作进入临界区、离开临界区。A 进程创建的临界区,只有 A 进程可以进出,其他进程不能操作。因为 iOS 是沙盒机制,对于单个 App 来说,不存在多进程,所以临界区在 iOS 开发里面用不到。不过系统肯定是需要临界区的,不过那是操作系统的事情了。 读写锁:读的时候数据不需要保障稳定性,所以可以并发读,但是写一定要独立,写的时候只能一个一个写,而且写的时候不能有读操作。也叫共享-独占锁。 条件变量:在达到某个特定的条件下,线程才能加锁和解锁。条件可以预先设置好,后面的加锁和解锁就根据条件来触发。 iOS 的锁有二十种左右,但更多都是对几个特定锁对封装。举例来说: OSSpinLock 自旋锁(特别说明,虽然性能非常高,但是已经被废弃)。本质是互斥锁,ABC 同时访问对时候,C 先进去并加锁,然后 AB 不断循环访问是否解锁,如果解锁,立刻进入并加锁。所以被挡在锁外面对线程没有休息,而是不停对查询。 CPU 消耗很大,因为挡在锁外面对线程一直在不停查询。 因为优先级反转原因,该锁已经被苹果弃用。比如优先级为:A>B>C。这个时候 C 提前进入加锁并执行代码,但是 A 优先级太高,导致 A 不停查询并占用了非常多对时间片,最后 C 用了很久才执行完并解锁。这个过程中,有太多时间片都浪费在了查询上。 os_unfair_lock 互斥锁的一种,OSSpinLock 的替代品。自旋锁会不停的查询并忙等,os_unfair_lock 会在加锁的情况下,对线程进行休眠。当解锁后继续执行。 只要是锁,优先级反转都会出现。但是不同于自旋锁,互斥锁会让挡在外面对线程处于休眠状态,在解锁后激活并执行。这样对 CPU 的消耗会很低。 dispatch_semaphore 信号量。可以实现二元信号量和多元信号量。通过信号量还可以做限制并发操作。 pthread_mutex 互斥锁。mutex 是互斥锁的完整体现。基于 mutex 可以实现互斥锁、递归锁、条件锁。效率非常高。 普通互斥锁:性能很高的锁,挡在锁外面的线程会休眠,不会出现优先级反转后时间片浪费情况。 递归锁:当 A 线程因为递归等原因,在没有释放锁的情况下,又重新加锁。这个时候互斥锁是不能加锁的,因为之前已经加过锁了。递归锁可以解决这个问题,在递归锁下,同一个线程可以一次加锁,然后一次解锁。 条件锁:mutex 实现的条件锁,不能根据条件自动加锁解锁。需要动手激活指定条件然后加锁或解锁。 NSLock 互斥锁。对 mutex 普通互斥锁的封装,面向对象。 NSRecursiveLock 递归锁。对 mutex 递归锁的封装,面向对象。 NSCondition 条件锁。对 mutex 条件锁的封装,面向对象。可以预设条件,在条件到达后,自行加锁解锁。相关 mutex 自行实现,代码过程更加自动化。 synchronized 递归锁。对 mutex 递归锁的封装,使用最方便,不需要手动加锁和解锁。但是性能也是所有锁里面最低的。 dispatch_rwlock 读写锁。可以保障写操作的互斥独立,读操作是重入可并发的。 dispatch_barrier_async 栅栏锁。可以分割一段任务队列(警告:必须使用自定义队列,不能使用主队列和全部队列)。也可以用来模拟读写锁,iOS 的属性修饰符 atomic 完全可以通过 dispatch_barrier_async 来实现。 dispatch_group_t 栅栏锁。和 barrier 类似,功能方向略有差异,group 栅栏锁可以实现组的操作。 还有其他一些锁,但可以发现,大差不差,都是对多线程 5 种锁的实现和封装,这 5 种锁分别是:信号量、互斥量、临界区(iOS 开发层面没必要使用)、读写锁、条件变量。 在 iOS 开发里面具体使用哪些锁问题已经不大,只要不使用 OSSpinLock,其他锁都可以试一试。目前来看,业务层面开发,NSLock 和 synchronized 用的较多。组件库方面,os_unfair_lock 和 mutex 使用的较多。 多线程数据安全总结 Runtime 源码里面,对于 SideTable 中 weak 表的实现,就是原子性粒度和锁的解释说明。 Runtime 对 weak 表的整个实现,都标记为线程不安全,并且在外部 SizeTable 中定义了 spinlock 自旋锁来限制原子性区域。 所以 weak 表的实现里面,原子性区域还是比较大的,整个 weak 表内部的数据的处理都处于不安全状态,通过最外界的函数调用处,给予自旋锁来保障线程安全。 相关代码如下所示: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 struct SideTable { // 锁对象 => 自旋锁,用于上锁/解锁 SideTable // spinlock_t的最终定义实际上是一个uint32_t类型的非公平的自旋锁。所谓非公平,就是说获得锁的顺序和申请锁的顺序无关,也就是说,第一个申请锁的线程有可能会是最后一个获得到该锁,或者是刚获得锁的线程会再次立刻获得到该锁,造成饥饿等待。 同时,在OC中,_os_unfair_lock_opaque也记录了获取它的线程信息,只有获得该锁的线程才能够解开这把锁。 spinlock_t slock; // 索引哈希表(稠密哈希) => 对象引用计数map,用来存储OC对象的引用计数(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)。 // 是一个以objc_object为key的hash表,其vaule就是OC对象的引用计数。同时,当OC对象的引用计数变为0时,会自动将相关的信息从hash表中剔除。 RefcountMap refcnts; // weak表(核心实现) => 对象弱引用map weak_table_t weak_table; // 构造函数 SideTable() { memset(&weak_table, 0, sizeof(weak_table)); } // 析构函数(看看函数体,苹果设计的SideTable其实不希望被析构,不然会引起fatal 错误) ~SideTable() { _objc_fatal("Do not delete SideTable."); } // 锁操作 符合StripedMap对T的定义 void lock() { slock.lock(); } void unlock() { slock.unlock(); } void forceReset() { slock.forceReset(); } // Address-ordered lock discipline for a pair of side tables. template<HaveOld, HaveNew> static void lockTwo(SideTable *lock1, SideTable *lock2); template<HaveOld, HaveNew> static void unlockTwo(SideTable *lock1, SideTable *lock2); }; 还有 Runtime 里面_read_images 函数里面操作 SEL 的代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 将所有SEL都注册到哈希表中,是另外一张哈希表 static size_t UnfixedSelectors; sel_lock(); for (EACH_HEADER) { if (hi->isPreoptimized()) continue; bool isBundle = hi->isBundle(); // 取出的是字符串数组,例如首地址是"class" SEL *sels = _getObjc2SelectorRefs(hi, &count); UnfixedSelectors += count; for (i = 0; i < count; i++) { // sel_cname函数内部就是将SEL强转为常量字符串 const char *name = sel_cname(sels[i]); // 注册SEL的操作 sels[i] = sel_registerNameNoLock(name, isBundle); } } sel_unlock(); --- void sel_lock(void) { selLock.write(); } void sel_unlock(void) { selLock.unlockWrite(); } --- rwlock_t selLock; 这里对原子性粒度把控的就很细,用的读写锁。 多线程下数据结构和算法的重要性 到这里,多线程下数据安全基本已经可以告一段落了。但是,还没有结束。 我们之前提过,多线程下数据安全通过原子性粒度的把控和锁机制,已经可以比较好的实现。而粒度控制小心一些,锁就那么多,用的恰当一些,数据安全就没有问题了。 后面的问题,就是原子性区域内部的代码执行效率的问题了。因为原子性区域内部都是单个线程在执行,所以执行效率一定是要很高的。 我们举个例子,如果原子性区域里面,代码耗时需要 1s,那么多线程操作下,是不是有很多线程都会被原地休眠?整个执行效率肯定低下的要死。 所以,这个时候,就要使用合适的数据结构和算法,来提高代码执行的效率。 我们用 YYCache 的内存缓存举例,YYCache 使用 pthread_mutex 互斥锁,原子性粒度控制的很小,在对数据进行操作的时候才开始加锁和解锁。 缓存使用来 LRU 算法,通过双向链表来实现数据对增和删的复杂度为 O (1)。 但是链表的查询复杂度是比较高的,因为链表无法做随机寻址,也没法用数组的空间局部性缓存加速。 所以作者通过空间换时间的方式,引入了 hash map,将缓存数据存入 hash map 中实现查询复杂度为 O (1)。 这样,整体内存缓存的数据的操作复杂度都将为 O (1)。 之前也写过文章说明数组和链表的优缺点,其中重要一点就是链表的增删复杂度为 1,数组的查复杂度为 1。为了更好的使用数组和链表的双方优点,所以 hash map 和链表常一块使用。 Runtime 里面也是各种 hash table 和 hash map table 的使用,甚至 hash 函数都为了高效,尽可能使用位操作来计算索引值。 所以多线程编程,数据安全能通过锁保障的也都能很好保障(像 FMDB 和 iOS UI 主线程,知道多线程数据安全的处理风险太大,索性就不支持多线程了),唯有算法这个环节,诡异且多变,最能体现价值。 今天是五四青年节日,我查了青年的年龄标准,是 14-28 周岁。突然很开心,我明年还能在过一次五四青年节。 历史前进的车轮肯定不会停下来,不管是文明,经济,抑或是网络,甚至自由。后浪必定比前浪更加优秀,这是毋庸置疑的,否则不符合历史规律。 每一波新一代,都拥有更棒的环境,更好的认知,更方便的学习方式。所以后浪们必定更加优秀和杰出,这是必然。 社会这个大团体,也一定会在后浪的推动下,一直向前,稳步向前。 但有一个重点我也想表达,随着历史长河的流逝,社会必然会进步。如果要进步的更快,那思想独立和思想解放必定占据非常大的比重,中国的新一代在这方面有很大短板。 我已经在职场 7 年了,虽然还是青年,但已经没有了青年的气质和气息。即使假装青年的疯癫,但眼角的复杂情绪却无法掩藏,也无法欺骗自己。 我显然不是青年了,很多时候我会无知于自己的未来,也对未来充满恐惧、失措、无助,而青年人不应该有这些拘束思想,他们应该是奔放的,激情四射的。 我想了一下,我脱离青年身份,应该是 3 年前,那年我 24 周岁,本命年。那年,我孩子出生。

2020/5/4
articleCard.readMore

Objective-C 和 Runtime

运行时 (Runtime) 本身是一个非常普通的概念,每个编程语言都会有运行时,非 iOS Objective-C 特有。 概念上理解,运行时,就是程序执行的过程,每个编程语言,只有运行后执行特定的任务才有价值,所以每个编程语言,都有运行时。 而 Objective-C 的 Runtime 显然不仅仅是程序执行过程这么简单,它一举将基于面向过程的语言 C 而实现的面向对象的语言 Objective-C 变成了动态语言。 为什么 Objective-C 的 Runtime 有些难理解,因为运行时从概念上理解非常简单,但是 Java、Python、C 它们的运行时都是不一样的,运行时期间可以做很多事情,所以运行时理解起来还是比较抽象和高阶。 如果把 Runtime 改名为 OCDR(Objective-C Dynamic Resolution,即 Objective-C 动态决议),剥离运行时和运行时库的概念,那么很多人都会轻松掌握 Objective-C Runtime 的核心。而 Objective-C Runtime 本质上的确就在践行动态决议这么一个过程。 Objective-C Runtime 使得函数调用变成了动态消息传递,并且可以在执行过程中对 Object 进行增删改查,所以更准确的说,Runtime 使得 Objective-C 变成了动态语言。 动态语言分析 首先理解一下各类语言的一个区分分界,那就是动态语言和静态语言的区分(非动态类型语言和静态类型语言)。 关于编译型语言 & 解释型语言,可以查看 Shell 和进程。 静态语言 静态语言比较容易理解,我们通过高级语言写好的代码,经过预处理、编译、汇编、链接后,就形成了机器码(二进制文件)。这个时候,函数名、变量名都保存在符号表中,并拥有对应的虚拟地址。而我们调用一个函数,就是通过机器码调用的,汇编示例如:’call 0X12345678’,这里 call 代表函数调用,0X12345678 代表函数的虚拟地址。 这个机器码执行的过程中,PC 指令寄存器会不断的记录下一行将要执行的指令,然后不停的执行下去 (延伸一个知识点,如果下一行指令不再内存中,操作系统会发现页缺失,然后从硬盘中将对应缺失页的指令拷贝到对应的真实内存中,然后继续执行)。 这里我们可以发现,我们通过高级语言写好的函数,在编译后,就已经不可改变了。程序执行后,CPU 内部的运算单元、控制单元、数据单元,就有条不紊的按照机器码执行就好了。 那么如果我们想改变一个函数的实现呢?比如本身调用的 A 函数,运行期间想调用 B 函数,有办法实现吗?对于静态语言来说,不行! 那么如果我们想改变一个对象的结构体呢?对于静态语言来说,不行! 所以静态语言,对于函数的定义,在编译期就必须是明确的,如果找不到函数的实现,就会编译错误。 举例来说,如 C 语言。不过 C 语言也有一个动态化的能力,就是 hook,可以做一定程度的动态能力,不过这并不是语言级别支持的,只是上层业务的补救。 动态语言 动态语言相比静态语言,也比较容易理解了,在运行时代码可以根据某些条件改变自身结构,如函数的调用,自定义类的生成和对象的创建等。那这样的语言就是动态语言。 举例来说,如 Objective-C 语言。 分析 C 是静态的,Objective-C 是动态的,而 Objective-C 是基于 C 实现的。那 Objective-C 是如何基于 C 实现动态特性的呢? C 有它的运行时,不过程序执行过程中,CPU 完全掌控机器码执行流程,所以 C 的运行时不能多样化,基本上就是按照程序员写的代码执行顺序执行。这也是 C 快的原因,因为 CPU 直接执行,不用考虑那么多复杂的情况。 那 Python 如何实现动态的呢?Python 也有它的运行时,不过说来巧妙,它不是编译性语言,不会像 C 一样打包成机器码直接执行。Python 是解释性语言,代码执行到哪一行,就即时编译该行形成机器码执行,所以 Python 非常容易实现动态,只要代码运行过程中适当的添加 if 语句对将要执行的对象进行替换,就能很好的实现动态,鸭子 Duck 模型也就来源于此。 那 Objective-C 呢?Objective-C 和 C 一样是编译性语言,项目打包后会经过编译链接等处理变成机器码。 Objective-C 实现动态就要运行时了,因为在运行时做了很多操作,以至于很多人叫它 “运行时系统”。 之所以前面说 Objective-C 的 Runtime 应该改名为”Objective-C Dynamic Resolution,Objective-C 动态决议”,就是因为 Objective-C 的 Runtime 主要实现的两个点都更加符合 “动态决议” 这个命名: 通过 objc_msgsend (a_object,a_object_method,xx) 这个中间者函数,动态解析被调用的函数。 通过指针、数组的指针、链表指针、全局数据,间接操作函数列表、属性列表等,动态的对 Object 进行增删改查。 Objective-C Runtime 函数调用动态性 之所以很多人说 Objective-C 的函数调用并非函数调用,而是消息传递,其原因就是 Objective-C 的函数调用很不一样。 在 C 中,我们调用一个函数,会这样写: 1 2 3 Node *head = NULL; ...生成链表... reverseLinkList(&head);// 反转链表 这里,reverseLinkList 函数的调用,在汇编里面为:call 0x12345678,其中 0x12345678 就是 reverseLinkList 这个符号名的虚拟地址。 这里我们可以发现,reverseLinkList 函数的虚拟地址在编译的时候就已经定下来了,后期无法做到动态性。 再来看一下 Objective-C 的实现,因为 Objective-C 是面向对象的语言,所以我们调用对象的一个方法: 1 2 Person p = Person.new; [p realAge]; 这里,[p realAge] 这个函数的调用,p 有自己的虚拟地址,realAge 函数也有自己的虚拟地址,但是在汇编之前,该函数已经被编译成了这样: objc_msgsend(p, realAge)。 如此,Objective-C 通过 objc_msgsend 这个完全汇编写成的中间函数,在函数内部通过运行时库对 p 进行 superclass 和 isa 的访问,乃至最后实现三次动态函数转发操作。 所以 Objective-C 实现消息传递,就是依靠 objc_msgsend 这个中间函数来实现,如果 objc_msgsend 的虚拟地址为 0x87654321,那么 [p realAge] 函数被执行的时候,实际汇编为:call 0x87654321。 上面说完了 objc_msgsend 这个中间函数完成动态解析的操作,那它是如何进行 superclass 和 isa 的访问,又是如何访问属性和方法列表的呢? Objective-C 面向对象的本质:C 的 struct(结构体) 通过 Runtime 源码,我们可以发现,Objective-C 里面一切皆对象,乃至 UILabel、NSTimer 等。而这些对象是以什么样的数据结构存在呢? 都是 struct(结构体)! 我们的成员变量,成员属性,方法等,都保存在结构体里面。 所以这里我们可以深刻理解一下,为什么说 Objective-C 是 C 的超集,因为面向对象的 Objective-C 语言完全就是依靠面向过程的 C 语言发展起来的,乃至于我们写的面向对象的代码,最后都会变成面向过程的执行流程。 我们可以查看苹果公开的 runtime.h 文件,都是面向过程的函数调用。如: objc_setAssObjective-CiatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssObjective-CiationPolicy policy) 你看,这完全就是面向过程的编程。 因为 struct 是我们对象的存储结构,所以我们可以通过 * class_getProperty (Class _Nullable cls, const char * _Nonnull name)* 等库函数直接对 struct 内部保存的数据进行访问。 比如我们要访问 a_object 的 a_object_method 方法,那么就要通过 a_object_struct,根据 superclass 找到类 A_struct,然后遍历 A_struct 里面的 methods 数组,找到 a_object_method 方法并执行。 struct 结构体在编译后就已经确定,那如何做到动态性如增加方法呢?struct 的实现是 C 和 C++ 混编的,但大体逻辑不变,都是通过指针实现的。 比如说,struct 里面有一个方法的数组指针,那么我们动态添加方法的时候,只要通过这个数组指针就可以添加到对应的方法数组中,这样就完全不会改变 struct 的结构 (甚至如果数组需要扩容,也和 struct 没有任何关系)。 因为 struct 在编译后就已经确定,所以能做动态性的方案不外乎这几点: 通过数组指针,如方法列表等 通过链表指针,如 NSNotification 等 通过全局对象,如关联对象等 所以指针才是 C 和 Objective-C 最终执行的终极形态,不断通过指针间接的对数据区进行增删改查。 而 Objective-C 的 Runtime 就是不断通过指针对 struct 结构体进行增删改查数据处理来实现动态特性。 Objective-C Runtime 总结 Objective-C Runtime 用的最多的是 objc_msgsend,所有的函数调用都会通过这个消息传递完成。 编译成机器码的函数调用,本身无法实现动态性,通过 objc_msgsend 这个中间层巧妙的实现了。 而 objc_msgsend 的汇编执行又得反过来操作运行时库,又回到了对象的 struct 结构体上面。 Objective-C Runtime 本质就是通过指针操作 struct 的增删改查来实现 Objective-C 的动态特性。 C 语言中一切皆指针,基于 C 发展起来的 Objective-C,也是一样。 而指针也是中间层思想的体现。本身无法直接操作的数据,通过指针这个中间层,间接的对数据进行操作。 延伸 神话 Runtime 的结果就是越发觉得自己的无知和不知所以。 Runtime 是啥?从编译上来看,Runtime 不是和我们项目一起打包成 ipa 包的,那 Runtime 还能是啥? Runtime 说的直白点,仅仅是个动态库而已,也叫运行时库。 它和我们项目里面使用的自行编译或者三方编译的动态库一样,也需要在应用启动后加载到进程中(Runtime 和系统 framework 一样,属于系统动态库)。 iOS 和 Mac 使用的可执行文件是 Mach-o,与 Linux 的 ELF 和 Window 的 PE 一样,都是基于 COFF 格式扩展的。 所以 Mach-o 可执行文件的运行流程和 ELF 等都大差不差,当然具体到细节有一些差异。但总体都是差不多的,比如 segment 和 section、section head、内存分区等,都一样。 应用启动后,首先操作系统通过 fork 开辟进程,开启用户态后,然后加载 Mach-o head。head 里面有很多重要信息,如 Magic Number (Mach-o 通过 0xfeedface/0xfeedfacf 表示 32/64 位,ELF 则有’E’’L’’F’标记、32/64 位、版本、大小端等)、版本、支持 CPU、文件类型等。 然后找到 Load Commands 区,这里面有几个重要的信息,其中有三个为 Segment (通过 Segment 将相同读取及可执行权限的 section 汇总到一起),Dyld,Dylib。 通过 Segment 可以找到具体执行的指令和数据的 Section,和 ELF 里面的 section head (段表) 类似。应用启动的时候还没有到执行 Segment 那一步,还有一些重要的信息没有完成,其中一个就是动态库的符号解析和重定位。 PC 指令寄存器首先指向了 Dyld (动态链接器),将 Dyld 运行起来后,开始将所有动态库复制到内存(已经在内存的无需重复加入,如系统库)。 所有项目里面所有用到的动态库,都会在这个时候拷贝到内存里面,一个都不会少。像一般项目,系统库都会有 200-500 个动态库要添加,因为已经在内存,所以不会耗费时间(手机重启后打开的第一个应用,会比较耗时,就是因为很多系统库都不在内存中)。 Dyld 将所有需要的动态库拷贝到内存后,Runtime 库也就拷贝好了。所以这是 Runtime 第一次展示的时机。而它第二次展示,就要等很多准备操作之后了。 因为 iOS 在 4.3 系统之后,升级了虚拟地址方案,采用了动态虚拟地址,就是每个应用程序启动后,起始地址都是动态的。那么已经编译好的 Mach-0 就需要重新修正指令地址了。 这里的指令地址修复,分为两个环节,一个是本身 Mach-0 内部的地址修复,如函数调用、数据使用。这个就是 Rebase 阶段,总体是 I/O 耗时,但没有太多复杂的。 第二个环节是对动态库对引用修正。因为对动态库的使用本身就是地址为 0,需要在应用启动后重定位的,所以动态虚拟地址和动态库重定位在第二个环节就一套带走了。 这里有个特殊点,就是动态库也是程序,同样包含指令和数据。所有应用程序对动态库的指令都是共享的,如函数调用,但是对数据都是私有的,需要应用程序自己处理。 第二个环节里面,会把所有需要修正的数据全部修正掉,而函数调用,则是真正运行到这行指令的时候,才能修正。没有运行之前,仅仅做了一层映射,并没有真正处理。 这个映射就是动态库的数据和指令的映射,是一个 section 保存在 Data 区,因为这个区的数据可读写。这个映射就是 got section。 而第二个环节,也就是 Bind 阶段,更多的是 CPU 操作。 Rebase 和 Bind 过后,又到了 Runtime 大显身手的时候了,PC 指令寄存器指向了 Runtime 的指令位置,开始进行 class 的遍历、method 的遍历、load 的执行等等操作。 过后,才是项目的 main 函数的执行,然后是 application 的执行。当然,项目运行起来后的所有 objc_msgsend,都是要走一遍 Runtime 的。也就是 struct 那一套了。 所以 Runtime 是一个动态库,也叫运行时库。这也是我们项目里面要使用 Runtime 的时候,都要先#import <objc/runtime.h> 的原因。

2020/4/8
articleCard.readMore

早起打卡计划 ing

2020.01.28,大年初四,疫情已经四处开花了。那天,我坐在出租屋里和孩子玩。我突然想到,2019 年的年度总结我已经写出来了,但是 2020 年都过去两个月了,可是 Flag 呢?好像是丢了。 像我这样到人,除了抽烟一直没能戒掉,做其他事还是计划感挺强的。我觉得 2020 年,应该立个 Flag。 想到这次疫情,不知道让多少人的季度 Flag 都没完成甚至直接泡汤了。Flag 多立也无意,立了牌坊挂不起来也百搭。对我目前而言,很多东西根本就不是 Flag,而是 Target 了。 那天,我想了一些计划,但没有哪个计划感觉到鹤立鸡群。突然想到自己很多年前有的一个公众号,也不知道还在不在,就登上去看看,顺手从 Telegram 上面复制了一段疫情动态发了上去。 2020.01.31,大年初六,我确定了 Flag,那就是坚持做一件早起的事。如果这件事短期没能撑下来,那我就找个借口自我安慰,年龄大了后想懒床还懒不了呢。 但如果撑下来了,那就太有意义的一件事了。因为我曾今尝试过,早起真能做很多事情,鸡汤文诚不欺我。 于是,我决定坚持每天早上 6 点发表一个文章到公众号上,看看自己能坚持多久。 想着发什么内容好的时候,正好现在是疫情期,那就用新冠疫情是否结束来做标题,附带丁香园的实时数据动态截图。(我想着疫情很快也就结束了,后面想着通过其他波动性的内容,如股价等来做文章内容。但是最近有关专家说,要和病毒做长期共存的准备,泪奔了。) 于是我准备了疫情的 3 个状态图,分别表示正在爆发、正在好转、已经结束。然后用手机把丁香园实时数据截图并剪裁,最后通过 “订阅号助手” App 直接发文。 下面是我准备的 3 张图片: 正在爆发 正在好转 已经结束 图是我用 Sketch 做的,开始想着用 PS,发现已经卸载了。重装一个 PS 包太大了,Sketch 下载包小很多。这样前前后后找图标,压缩图片,也折腾了一个小时左右。 我本来想着十几分钟就能完成的,所以做事还是要留几分冗余。地图上面搜距离 A 地有 5 公里,那红绿灯或者上下地铁站的时间也应该需要考虑进去。 2020.02.01 06:23,第一条推文发送成功。 2020.02.04 06:02,第一条距离 6 点最近的推文发送成功。 后面渐渐稳定在 6 点多一些,超过 7 点以后就不发了。 距离今日,总共过去 37 日,发表推文 28 篇,坚持率 76%。 不是很好。有点超出我的预期,但没有达到我期待。但还是很感动,这特么比戒烟容易多了。 公众号名称:一个工匠 已经被腾讯给封了。– by 2023.08.06

2020/3/9
articleCard.readMore

打比方

很多人都喜欢打比方来阐述一件事情,比如说 “xxx 就是一个例子”、“有 xxx 一个例子”、“我和你打一个这样的比方 xxx”,比方说完之后,就开始阐述自己的观点。 打比方是一个久经沙场,百试不爽的论证妙计。怎么让对方跟着你的节奏走?当然是打个比方。比方一般都比较有趣,尤其带有故事性,很容易带节奏。而观点但凡和比方有一点相合,那比方就是绝佳的药引子。 但事实是:观点可能是错的,比方可能是不存在的,观点和比方的相合度可能是 1% 的交集,但观点却被采纳了! 比方本身是个很好的东西,我觉得比方诞生的本意,是用来更加形象和具体化观点。因为本身观点的抽象性,如果通过一个比方一个例子来讲述,这样观点会更加突出易理解。 可是后期比方显然变味了,比方变成了让大众接纳一个观点的药引子。这在很多自媒体、上下级谈话、朋友聊天甚至诈骗中,比方尤其展现了药引子的用途。 如果谈论变味后的比方,尤其要特别说明谁提出了这个比方。人都是利益和目标的趋向体,所以人总会用某些途径来达到某些目的。比方很多时候都是策划过的药引子,所以它具有专项性。 所以比方对于提出人是有利的。当然这也是说服人的过程中,或者阐述个人观点的过程中,非常有效的表达方式。当我们要让别人同意某个观点的时候,可以从低的细节纬度和高的泛纬度,打不同的比方,高低纬度的比方都很恰当合适,那观点就已经成型了。但我们还是重复说明一下,这里的比方很恰当,但这个比方可能不是正确的。所以对于观点的接收者,要适当的辨别,不要被比方给误导了。 识别比方的有效方法就是逻辑闭环。比方不管说的怎么好,如果逻辑不闭环,那就是有问题的。有人打比方 xxx 因为做了 xxx 事,然后取得了成功。这在成功学里面经常出现。可是 xxx 是 xxx 成功的充分必要条件吗?显示不是吧。成功的前提那么多,机遇很多时候也是可遇不可求,哪能以偏概全呢。我很反感成功学,这玩意很虚很假不合逻辑。你学着做,你也成不了大师,还会感到迷茫。 我们无需排斥比方,毕竟很多时候比方都是一件有趣的事情,比如一场经历,一个故事。但我们还是需要剥离比方,正视比方后面的观点。 再退一步,从私人角度警告一下,比方是我朋友,你为什么要打他? 病毒和细菌,才是地球的主宰。而无所不能的人类,在未知面前,不过是个幼婴。

2020/3/3
articleCard.readMore

IM 和 Socket 的关系及 Heart 的必要性

很多互联网服务都会开通 IM 功能,如好友通讯,用户反馈,在线推送等。在微信未盛行,QQ 大行其道的年代,IM 还属于高深技术,目前已经进入寻常百姓家了。 其实 IM 本身整个技术流程从终端角度来看不是一个难以理解的问题。但因为诸多人对网络协议不了解,所以认为 IM 难以入门。 首先要说 IM 是一个什么样的形态,它和 HTTP/S 有什么不同。 HTTP 解决了端到端通讯的问题,在互联网大放异彩的原因是浏览器的盛行。冲浪需要网络获取服务端资源,HTTP 可以使得终端发起网络请求服务端返回资源结果实现资源互通。 我们的感知里面,终端发起网络请求,服务端回复网络请求,HTTP 使命就完成了。所以很多人对于 HTTP 的认知是:一次性通讯,每次需要获取信息都要重新发起请求。 其实有这样的认知是完全正确的,因为 HTTP 的确就是这样设计的。这里要着重说到设计这个词,因为 HTTP 就是为了完成一次性通讯这么一个任务。而像 HTTP 这样的应用层协议还有非常多,比如 FTP、RTMP 等等。他们都是为了完成特定任务而实现的协议。所有应用层协议都是被设计过的,为了完成特定的使命。 但是随着社交关系的复杂,如何实现即时通讯又是摆在眼前的问题。HTTP 是一次性通讯,请求返回后,终端和服务端两不相欠,也感知不到对方的存在。 如果我们想通过 HTTP 实现即时通讯,那就需要客户端每隔 N 秒 (比如 2s) 向服务器发送一个请求,然后服务器返回新消息。这虽然很简陋,但是我就经历过这样的方案设计,我认为,在特定场景、功能、需求的前提下,这样的技术方案设计是完全合理的。 HTTP 显然不适合大量用户下的即时通讯场景。在社交网络中,服务端需要在线数据统计,需要消息及时在线推送到终端,需要即使感知到终端连接是否中断好清理服务器资源,而这些,显然 HTTP 都无能为力。 那如何实现真正的即时通讯呢?真正的即时通讯和 HTTP 通讯在技术原理层面有哪些区别呢? Socket 是什么 很多人都认为 Socket 是一套协议。其实根本就没有 Socket 协议存在。但是话又说回来,Socket 本身的确又有其他两个身份。如果有人非要说 Socket 是协议,那 Socket 就有三个身份了。 Socket 库 互联网本来就是数据的传输。传输层是有专门的协议负责数据传输,就是 TCP 它们。 可是应用层 HTTP 怎么和传输层对接呢?就需要很多个接口。而这些接口处于一个库中,没错,这个库就叫做 Socket 库。 Socket 库提供了 write、read 接口用来从缓冲区读取数据,提供了 connect 接口用来建立连接,此外,Scoket 还有很多其他接口如 close、gethostbyname 等。 1 2 3 这里要特别说明一下gethostbyname接口,因为它太重要了。gethostbyname是用来做域名解析用的,没错,域名转成IP是Scoket库提供的接口完成的。 所有网络请求,不管是局域网还是广域网,都需要知道对方的IP才能够发送消息,这是七层网络模型决定的。但是我们一般都只知道域名,在网络请求的时候,就需要做一次域名和IP的转换。 这个域名解析是操作系统来实现的,操作系统发现请求是域名,就调用gethostbyname接口进行系统中断,然后Socket库经过一系列的递归发送UDP数据报来查询域名对应的IP,最后操作系统返回该IP,应用层恢复系统中断,开始继续执行。 所以 Socket 库,就是一个 Api 接口的实现合集,它连接了应用层和传输层。应用层调用 Socket 库接口,把数据给到传输层,后面数据才能够发出去。 Socket 套接字 互联网上两个终端 (比如客户端和服务器) 如果需要通讯,就需要知道对方的 IP 和 Port (端口)。这是由网络七层模型决定的。 IP 通过 gethostbyname 已经拿到了,Port 应用层在请求的时候已经知道,比如 HTTP 默认 80,HTTPS 默认 443,还有自定义 8889 等。 那知道对方的 IP 和 Port 后存储在哪里呢?就是一个文件里面,更接地气的说,叫套接字,Socket 翻译过来也就是套接字。 终端要发送请求到服务端,首先需要在终端建立自己的套接字,存储服务端的 IP 和 Port。服务端要接收终端的消息,也要在服务端建立一个套接字,存储终端的 IP 和 Port。 终端发送 HTTP 消息给服务端后,服务端经过数据库查询等处理,要把数据返回给终端,就需要读取服务端的套接字拿到终端的 IP 和 Port,数据通过七层模型,才能把消息回复给终端。 套接字除了找到对方的 IP 和 Port,还有其他用途吗?很多用途。套接字主要做信息存储,这也合符文件的本质。所以套接字里面存储了很多东西,通讯过程中需要的信息都会存储在套接字中。下面举两个例子: 一个例子是 TCP 三次握手。TCP 本来就是一个传输通道层协议,在调用 Socket 库里面的 connect 接口的时候,开始进行三次握手协商。但是 TCP 仅仅是一个协议,握手握了几次肯定是需要内存或者磁盘存储的,这里就是把三次握手状态 (如连接中,连接完成) 存储到了套接字中。 更确切的说,是 connect 接口把三次握手状态存储到了套接字中。 因为 TCP 仅仅是一个传输层协议,用来传输数据,它的工作仅仅是更高效率的传输数据而已。而数据所表达的含义,TCP 是不管的。握手状态明显是数据含义,相对 TCP 而言处理数据是业务层的工作,而 connect 这时就是 TCP 的业务方。 三次握手的状态写到套接字后,所有的数据传输,都会先从套接字中看一下当前握手状态,发现已经连接完成,那么就可以放心的发送数据了。 还有一个例子是 TCP 的窗口信息。TCP 在稳定连接、顺序传输、丢包控制、流量控制、拥塞控制方面是碾压 UDP 的。上面的三次握手就是在做稳定连接的工作,而其他四个方面就是通过滑动窗口和拥塞窗口实现的。在这两个窗口的实现里面,需要记录已经发送包的序号、对方已接收的包序号、当前还能发送包的量等等各种信息,通过这些信息,TCP 实时更新自己的发送策略,做到了稳定性数据传输。 显然,套接字里面能存储很多东西,而且,套接字主要的用途,是为数据传输提供信息判断的能力。 Socket 到底是什么? 其实上面 Socket 库、Socket 套接字这两个身份,都不常被人提起,因为这两个身份比较基础,很多人不愿意去理解他们。 但是千万不要理解 Socket 是一个协议,这是无稽之谈。因为这两个身份很多人不愿意去理解,Socket 协议又不存在,所以如果要向别人介绍什么是 Socket,我更愿意叫它 “Socket 中间件”。这是我自己瞎起的名字,但是不会像 Socket 协议那样让人误解。 在 Socket 库身份的时候,它是连接应用层和传输层的纽带,是一个中间件。 在 Socket 套接字的时候,它保存了数据传输过程中的各种有效参数,也是一个中间件。 HTTP 和 Socket 的关系 说 HTTP 和 Socket 的关系,其实说的就是 HTTP 和 Socket 库的关系。 和 HTTP 直接关联的是 Socket 库,HTTP 要进行域名解析,数据传输,数据接收,断开连接的时候,都需要通过调用 Socket 库的相关接口才能操作下去。 这样说来,其实 HTTP 和 TCP 关联就不大了。为什么这么说呢?如果哪一天,HTTP 说要用 UDP 协议,那么在调用 Socket 库相关接口的时候,传递一个参数告知使用 UDP 就可以了。 实际上,HTTP 使用 TCP 协议的确是这么实现的,HTTP 调用 Socket 库的时候,传递了一个参数告知使用 TCP,然后才会建立 TCP 稳定连接。 TCP、UDP 和 Socket 的关系 说 TCP、UDP 和 Socket 的关系,其实说的就是它们和 Socket 套接字的关系。 因为 Socket 库把数据传递到传输层后,就不管了,后面都是传输层的事情了。传输层在进行三次握手、窗口策略等过程中,需要不断和 Socket 套接字进行存储和读取。 TCP 和 UDP 都和 Socket 有一个共同的关系,那就是连接套接字的建立 (UDP 可以不建立,下面细说)。 当 TCP 需要传输数据的时候,就需要通过 connect 建立一个套接字,套接字里面有 IP 和 Port,然后进行三次握手在套接字里面记录状态。 总体流程为:建立套接字 -> 三次握手记录状态 -> 发送数据流 A-> 接收数据流 A’-> 发送数据流 B-> 接收数据流 B’-> 销毁套接字,释放资源。 当 UDP 需要传输数据的时候,可以通过 connect 建立一个套接字,套接字里面有 IP 和 Port,然后直接发送数据。 流程为:Socket ()->connect->send (A)->recv (A’)->send (B)->recv (B’)->close。 但是 UDP 也可以不 connect,下面是区别: 流程为:Socket ()->sendto (A)->recvfrom (A’)->close->Socket ()->sendto (B)->recvfrom (B’)->close。 UDP 在 connect 后,可以建立套接字,这样不用每次都建立连接,发起请求的时候直接去套接字里面拿端口和 IP。如果不 connect,只能发送消息的时候,自行附带 IP,使用 sendto () 接口。 即时通讯 IM 到底是啥? 所以即时通讯,就是编写业务层代码 (自己写或者使用 WebSocket 等应用层协议),使用 UDP 或者 TCP 这样的传输层协议,然后通过 Socket 套接字确定对方的 IP 和 Port,最后通过 Socket 库接口双向发送消息。 这也就是 IM 的本质。 HTTP 一直都是单向发送消息,比如客户端登陆或者调服务端 Api。这都是单向的。 其实这个单向是宏观上看的,微观上,客户端请求,服务端返回,这其实也是双向的。 但是 HTTP 因为本身的业务关系,在收到请求后,发起请求的一方就主动关闭了通道,即销毁了双方的 Socket 套接字。所以服务端就没法再发消息给终端了,只能等着下次终端再次发起强求,才能再次返回数据给终端,如此反复。 所以 HTTP 不能做到双向通讯,是由 HTTP 本身协议特性决定的。如果 HTTP 不主动断开,那 HTTP 也能实现即时通讯实现双向发送消息。 1 2 3 4 !!!HTTP1.0,消息往返后request主动关闭并销毁双方套接字,通道关闭。 !!!HTTP1.1,可以通过开启connection:keepalive,用于保持双方套接字。这个时候,因为通道还在,是可以做即时通讯用途的。 !!!HTTP2.0,在1.1的基础上,在传输层做更大程度优化,如多路复用、数据压缩等。这个时候,还是没有关闭销毁套接字,所以也可以做即时通讯使用。 !!!一般没有人使用HTTP来做即时通讯。但是实际上是可以通过HTTP来实现即时通讯的。 实际上,WebSocket 就是这样实现的。WebSocket 和 HTTP 一样属于应用层协议,WebSocket 通过 HTTP 建立连接,然后不断开通道,继续使用 TCP 进行双向发送消息。 话再说回头,WebSocket 能作为 IM 协议使用,其实是 WebSocket 协议帮我们做了业务层处理操作,如封包、粘包、调用 Socket 库 Api (send ()/recv ()/connect () 等)。 如果我们不使用 WebSocket,我们就需要自行处理 Socket 库调用,数据收发,和封包粘包的处理等工作。 好在各个平台开发方,都有比较出色等 Socket 三方库使用,它们就是封装了和 Socket 库交互的部分,但是封包粘包还是需要我们处理,因为这个时候没有 HTTP 或者 WebSocket 这样的业务方了,我们自己就是业务方,所以需要我们自己处理这些业务。 Heart 要解决什么问题 说到 IM,就不能不提心跳了。不管使用 TCP、UDP 还是 WebSocket,心跳都是必不可少的。 那么心跳是要解决什么问题呢? 其实心跳总结来看,就是要解决 IM 的一个本质问题:维系长链接,保持高可用。 要分析这个本质,就要先说明 IM 目前遇到了什么问题。总结来看,如下 3 点: 终端网络波动太大。尤其移动端特别明显,进出电梯时候的网络屏蔽、网络供应商切换时候的 IP 更换、高速移动过程中基站的变更等等。这些都会导致套接字失效。前面说过套接字是即时通讯通道能够稳定连接的核心。网络波动会导致套接字中 IP 更换,服务端存储的套接字就失效了,需要销毁老的套接字,并创建新的套接字。 NAT 超时后映射会销毁,中断通路。官方已经发布公告,IPv4 已经用完。本身 IPv4 在大陆就很紧缺。所以终端用户上网都会分配局域网 IP,通过路由器 NAT 中转到公网去。此时路由器内部会记录一个 IP 和 Port 的映射表。但是运营商为了节省资源和降低网关压力,对一段时间没有数据收发的连接,会清理掉。这时候终端和服务端都是完好的,但是中间的通路却断了。 企业等防火墙会在一段时间后关闭网络包传输。这个和 NAT 超时类似,防火墙会定时对一段时间没有数据收发对连接进行关闭,如 5 分钟。 以上 3 个问题,会导致终端和服务端无法正常通讯,导致连接不可维系。 所以心跳,就为了处理上面情况带来的影响,使得终端和服务端可以及时做如下处理: 让客户端尽快重连,保持通路。这个特别重要,如果发现中断,一定要及时重连,不然就收不到消息了。 可以让服务端尽快感知到连接的变化,清理 Socket 资源。这个是保持服务端高可用的有效途径。 告知中间设备,目前连接活跃。因为有心跳在,就有数据包通过,那么 NAT 就不会被清理,防火墙也不会屏蔽。 具体心跳的实现方式也是多样的,有很多种,比如下面这些: 应用层发送定时心跳。定时 30s、1 分钟、2 分钟、4 分钟这样的固定心率或者动态心率都有。服务器判断长时间没有心跳则销毁 Socket 等资源 开启传输层 TCP Keepalive。TCP 本身自带 keep alive,默认关闭状态。keep alive 默认每隔 2 小时发送一次传输层心跳,失败重试 9 次,超时时间为 75s,当然 TCP 的心跳可以调整。但是 TCP keep alive 有一个致命点,因为不需要应用层参与,数据包到达传输层即返回,和 ping 有些类似 (ping 都不过传输层),所以只能判断服务端内核层是否正常响应,但业务层是否崩溃不可知。如果服务器因为大批量任务处理导致拒绝服务了,TCP keep alive 是检测不到的。但是 TCP keep alive 对于感知网络是否正常非常有用,如上面的 NAT 和防火墙等。 TCP Keepalive 和应用层心跳配合使用,一个用来判断网络,一个用来判断业务服务器。这样可以对 Socket 中断进行 debug,以判断是否是中间通道被关闭导致的连接中断。 智能心跳。频繁的发起心跳会导致电量、流量等浪费,智能心跳可以平衡 NAT 超时和资源节约。智能心跳根据网络环境自动调整心跳方案如动态心率、通过使用二分法等算法方案逐步逼近 NAT 超时临界点等。 所以心跳,就是为了解决网络波动、中间设备关闭通道、网线插拔、机器断电等等意外导致的链路中断。 性话题,是对人性非常有考验的话题。 产品依靠性话题可以快速提升流量,朋友因为性话题可以快速拉近距离。 经常谈性的人大部分不色,闭口不言的人却又心怀鬼胎。

2020/2/4
articleCard.readMore

如果我的人生突然只剩下 8 小时...

如果不是 2019-nCoV,我都不会有这个想法。我一直觉得自己还年轻,如果不是车祸等天灾,怎么也轮不到我来思考死亡。但是风水转动的就是如此的快,每个大陆人都有感染的风险,感染后会有死亡的可能。 当然如果自己不作,做好防护工作,那肯定是相对安全的,当然这也是概率问题,概率和幸福指数直接相关,可惜我还不能理解概率。 这次疫情距离很多人都很近,尤其是一二线城市生活的人。我相信自己不会太作到去碰触病毒,但是我也认识到,不管天灾人祸还是自身疾病,不同年龄段的人都有可能突然离开。或许因为车祸,或许因为疾病,人类在未知和概率面前,还是太渺小。 而如果我的人生,突然只剩下 8 小时,我会做些什么? 只剩下 8 小时,我会做些什么呢?旅游等肉体上的快乐我并不是很在乎,看书、钓鱼等精神上的建设我在这个时候肯定也没多大兴趣。我目前能想到的,就是和我的女儿、老婆、父母说些话。 人果然是自私的利益体,在重要时刻,思考的永远还是自己能快速触及到的地域。 首先,我会和女儿说说话。为什么第一个说话对象选择女儿呢?因为我发现我太爱她了,超过了其他人,也超过了自己。在成为父母后,我才体会到父母对孩子的关爱是无时无刻无微不至的,总会不知觉的偏爱。 我想和女儿说一些话,希望这些话以后能给女儿一些帮助。女儿还小,能听我说完一段话中途不打岔问为什么我就谢天谢地了。 我想对女儿说,你一定要思想独立,人是靠着思维活动的,思想不独立,人生处处感到碰壁和绝望。而且,在思想独立的基础上,自己分析问题,一定要有逻辑性。逻辑感需要培养,我希望你能学些编程技能,对培养你的逻辑闭环非常用帮助。 女性有一个通病,就是心胸不开阔。这在很大程度上,对自己和周围人都有很大的弊端。比如 4 个室友却有 3 个微信群,猜忌横生。女儿,我希望你学会大度,心胸开阔,当你的心胸宽广到一定程度,在你眼前的就是一片银河。 我还希望你能够培养出来多重性格,显然不是多重人格病,而是希望你能够在不同场合下拥有不同的行为举止。一套性格走天下,你会感觉到疲惫,也会将你的圈子缩小。如果能将多重性格玩转股掌之间,那对你的人生一定是逆天改命的影响。我希望其中一个性格是独立性格,独立性格可以培养你的独立观念,如生活独立、经济独立等。独立性格还可以让你理解孤独,学会自我疗伤,人生一定不是顺利的,你一定要学会自己和自己和解。 有一个很重要的点,你一定要理解金钱的重要性,在你幼年快乐成长期度过后,金钱观念越早建立越好。你需要理解金钱和你人生的相关性,他们绝对不是对等的,但绝对又有千丝万缕的联系。如果你把金钱看的太重,你会感觉四处碰壁遥遥无期畏畏缩缩。如果你把金钱看的太轻,你回头后会发现自己在虚伪的欺骗自己,会感觉浑浑噩噩无聊空虚。我希望你把金钱看作你的激励,它不是你的目标,而是你的同行伙伴,你和你的伙伴一起成长,遇到困难相互扶持一下。你一定会有很长一段金钱匮乏的时期,不要迷茫,你的伙伴会和你一起成长的。 还有,如果有机会,你一定要去一个自由的国度。你一定会很长时间不理解为什么要去自由的国度,甚至于难道自己目前不自由?张无忌妈妈殷素素死前对张无忌说:” 孩儿,你长大了之后,要提防女人骗你,越是好看的女人越是会骗人。” 女儿,你一定要去一个自由的国度,这其实也是我努力赚钱的目的。 哦,差点忘了,还有,你要勇敢,要坚强,这在你思想和逻辑完备、拥有独立性格后,会是比较轻松的事情。勇敢坚强是你的气质,走到哪都是你的名片和自我约束,在生活和工作等各个方面,都大有益处。你要学会不要脸,这并不丢人,面子是你的影子,晚上到了,它会离开一段时间,但是白天,它还会再回来的。你要经历你的人生,有意义的生活,而不是得过且过,这一定是需要勇气的。 女儿,和你说的这些,我都是做不到的,如果你能做到,你一定会非常非常的优秀。 其次,我想和老婆说说话。 说到这几十年要感谢的人,父母把我养大教育大,是第一要感谢的。其次肯定就是老婆了,老婆给了我一个美好的家和一个美好的未来。 我首先要感谢的是老婆的勇敢和独立。在我大学毕业后无积蓄无资产的环境下,一直跟着我跑东跑西。我们相处的恋爱时间并不长,半年左右吧,后面有了孩子和婚姻。所以我谢谢老婆你对我的信任,也谢谢你的勇敢和独立。有所愧疚的就是恋爱期太短了,到目前的 3 年育儿和恋爱相比显然太不甜蜜。 我们都是农村出来的。我非常佩服你的通情达理,这是很多农村妇女所不能达到的。我想这和你受到高等教育很有关联。你对我的亲人,真是很用心了。对我和我亲人的包容与照顾,都让我心生感激。目前这纷争的社会,通情达理总归会有好运,也实属难能可贵。 上面的几点,都是很多人望尘莫及的,而你可值得推敲的优点还有一大堆。我很多时候能感觉到你对自己的不自信,甚至贬低自己,这其实完全没有必要,放眼望去,你已经非常优秀了。 有一些需要改善的地方,你也一定不能不放在心上。 你一向是个很果断的人,做决定的时候很麻溜。果断一定是好事,相比犹豫不决,果断可以减少非常多的麻烦。不过果断很多时候带来的是选择题,如果果断又能每次都选择对,那实在是天人所希望的了。老婆你一定要思考一下选择题,尽可能选择对的方向,不要急躁,果断而不武断。有些场景,如拒绝某个提案,言语也需要打磨好不至于太僵硬。 有一个让我不放心的是,你在大学毕业后,不再怎么喜欢学习了。学习是一门需要终生持续的课程,不仅仅是学校,一定不要把已经持续近 20 年的手艺活给怠慢了。学习的面很广,也不止插花或者做饭,感兴趣的事情,都应该多学习,学习可以持续的给你充电,让生活充满乐趣。还有,认识的多了,也会改变你的一些想法。比如你会学会变通,没有一成不变的东西,更加没有绝对的东西,妇女还不是在改革开放的浪潮中翻身把歌唱了?这样你看到那些明星的离婚或许会释怀很多。 最后,我想和父母亲说说话。虽说父母大于天,尤其我成为父亲之后,尤为体会父母的伟大。但是我只能把父母亲放在最后,因为前面三位,从我的角度来看,都大于父母。说孩子长大了自然要脱离父母,脱离父母的岂止肉体,还有心吧。 相比给予的不用担心温饱的日子,我更感激的是你们坚定教育的决心。你们一心让我和哥哥接收完整的教育一直到大学,教育完全改变了一个人的性格、生活、态度和人生观。如果没有教育基础,我也没有办法看到和接触更外一层的社会和知识。接触了完整的体系化教育带来的改变,让我深刻认识到农村缺少教育人群的认知短缺。就像有钱了才知道有钱的好一样,如果没钱,不仅体会不了,连想象的空间都显得是空中楼阁。我很感激你们坚持不懈的教育决心,这是温饱和金钱无法相比较的。 当我自己成家成了父母后,越发感觉到你们的不容易,我才感悟 3 年,而你们已经经历 20 多年了,其中的包容和支持,这样良好的教育品质,一定会延续下去。 不同年龄阶段对父母的理解是不一样的,曲线像这样:幼年的依赖,青年的违背,壮年的寡淡,中年的感激,中老年的理解和怀念。我现在正好处在壮年期,以至于对你们的感情多少显得寡淡空虚。有很多话想说,却又有所避讳而不能无话不谈,也不知从何说起,也不知如何收尾。 如果我的人生突然只剩下 8 小时,发现至少对于至亲,就有很多未完事,何况花花世界,那么多事物还没来得及体会呢。人间太美好,想做的事基本还没做,想体验的人生还没有体验。 人生不会突然只剩下 8 小时,这当然只是一个假设。如果遇到重大车祸之类,人生突然就只剩下几秒。如果遇到不可治愈的疾病,人生还有几个月或者几年可以渡过。 谁也不知道明天和意外哪一个会先来临,像这样的违背自然规律的意外事件,根据风险和概率,我对自己的遭殃可能性有如下的排名: 交通事故。这个是每天都有案发现场,概率非常大,出行一定要安全第一的。 遇人不淑。不管是工作还是生活,人都是群居动物,总归会遇到各类人,可能是闺蜜,也可能是邻居。遇人不淑的影响不像交通事故那么短暂,可能是长达数年或者数十年的。 不治之症。保险,老生常谈的话题,一定要保障起来。上下三代都需要保障,紧急时候这是唯一的稻草。 水火电及自然灾害。家庭用电、用水、用火,都要注意安全。水火无情,电是老虎。而疫情、洪灾、地震这样不可预测的自然灾害, 时间真的不多了。 人生短短几十载,能奋斗的也就那么一二十年。 如何珍惜并且压缩时间,就是自发的给予心里上督促,烙下 “时间不多了” 思想钢印。

2020/1/31
articleCard.readMore

胡说八道之雪粉

雪粉已经是大众熟知的美食,不仅国内家家户户必备,国外也是一抢而空。但很少有人知道雪粉的制作过程,而我的家乡,却是雪粉生产大本营。我对雪粉,一来情有独钟,它对家乡的经济发展有巨大的推动力,二来感到惋惜,除了我的家乡,很少有人知道雪粉的制作流程,国外也只能依靠进口才能品尝一二。 雪粉这样的美食原材料,本应该被更多人了解。我网上查阅了很多资料,都没有详细介绍。我想,我可以把雪粉的制作过程写出来,也算给了雪粉一个互联网的名份。 挑剔的雪粉原材料 雪粉并不是通过化学物质合成的,而是用真真实实的雪花制作的。在我老家,是四季分明的,也只有冬季,才可能有足够的降雪用来制作雪粉。 雪粉的原材料要求比较严格,一定得是中原地区冬季降下的厚雪。 中国北部大兴安岭等地的雪均不可作为原材料,因为那里的雪阳气太重,做出的雪粉不仅材质硬,而且食而无味。主要原因也就是阳气太重,雪花中参杂的杂质会更多。杂质一定是要清理的,牵涉到食品安全问题。可雪本就是什锦糖体质,杂质非常不宜清理,所以中国以北的地区均无法作为雪粉原材料。 而中国南部基本告别冬天了,那零星的雪花,无法形成厚雪。 雪花一定要和空气进行充分的氧化,最后隔绝大部分空气进行发酵。这样的雪花才是绵柔的。但对于大批量制作雪粉的供应商,氧化和发酵这样的前提条件一定不能在仓库中进行,几千上万吨的雪根本没有仓库可以发酵,所以只能依靠大自然。也只有中国中部地区,在四季分明情况下,降雪时期气温一般在零下 2-8 度,雪花阳气和阴气都不会过重,部分地区雪花参杂的杂质可以经过机器直接过滤。雪花层层飘落,氧化非常充足,最后堆叠发酵。 目前雪粉制作大本营也就是在我的老家安徽,其中以淮南一带最为集中。而周边也有不少厂商生产,但大多不是名厂,货物只能在偏远地区销售。周边省份因为地理位置比较靠近,天气适宜,也会有零星厂家生产,但都是边角料,没有大的经营体。 超低温脱水干化 东北是雪花大区,但无奈材质不达标,所以输出基本为 0,都只能在东北融化。雪粉原材料的获取本就是客观上的高难度,中部地区一年中仅有一个月的时间有批量降雪。 市场上雪粉需求量太大,基本上和烟草供应属于一个级别。但是烟草可以在全国各地进行全年种植,而雪粉相比之下时间周期就非常短了。所以家里的厂商都是批量大量制作雪粉。量到什么级别呢?一般都是以万吨为单位,小些的厂家也是 5000 吨以上。 制作雪粉第一步就是原材料脱水。雪花的含水量非常大,比水母的水含量还大很多。单片雪花太过渺小,不适合人工直接操作,一般都是通过冷冻干燥来脱水。 冷冻干燥是非常有效的办法,像咖啡制作的时候也是通过冷冻干燥来实现的。原理也非常简单,超低温! 在低温下,不管是坚硬的物质,还是柔软的物质,都会被粉末硬化。著名的科幻小说三体中就有描述,“三颗飞星意味着漫长的严寒岁月,冷得能把石头冻成粉末。” 首先就是制冷,大量冷气被缓慢冲入冷气房中,一般持续一天左右。 然后冷气房的温度会低于 50 摄氏度,有些厂家为了将雪花干燥更加彻底,会加大成本将冷气房温度调低到 60 度。 在低温下,人基本上是不能正常行动的。在零下 20 度左右,人走一小圈,衣服就会和身体紧密贴合,身体表面会有衣服印记。零下 40 度左右,开会泼出去就会冰化。所以雪粉的冷气房里,人是需要穿定制的升温服的。即使这样,人也只能在里面呆半小时左右,就必须出来。 当上万吨的雪花在冷气房中待够 5 小时后,就可以进行粉末萃取了。厂里通过螺旋式粉碎机,不间断的将已经脱水的固化雪块切割成粉末。粉碎机也是直接在冷气房里面直接工作的,雪块不需要搬迁,直接冷气房粉碎后传输到外面的制作车间。 脱水后的雪花,物理成分就不会太活跃了,可以进行多种包装方式进行存储。一般销售外部市场的都是经过包装袋封口包装一下,而我们本地人就是现吃现装。 雪粉多样化 我父母亲对雪粉的质量要求更高,会进行日晒。父母说,这样的雪粉粘度会更大,口感顺滑。 我老婆非常喜欢吃雪粉,她老家也属于地区中部,但都是雪粉边角料厂商,所以没能经常吃。嫁到我家后,不图富贵不图高雅,但唯独对雪粉情有独钟。每年冬季雪粉制作时期到了,都会和爸妈一起晒雪粉。外面吃的雪粉都是包装袋包装的,和经过冬日晒过的雪粉相比,差别不仅仅是口感,仪式感上的落差也是无法估量的。 很多时候,我们也有一些副产。毕竟雪粉对于事物来说也只能算原材料。村里想想发家致富靠着原材料终究还是不行,所以直接作出成品卖更有利可图。雪粉可以做很多事物,如面团,高精粉等。其中以汤圆和鸡蛋最为有名。 制作汤圆的时候,雪粉都不会出库,在经过粉碎机粉碎后,直接进入高压搅拌器。在高压下,雪粉会进一步发生物理变化,变成半固态状。 然后通过机器压模。 市场上的汤圆就做好了。因为全程没有加入水等液态,所以汤圆外部会有裂缝。但这也是最原生的。相比之下,有些厂商会加入适量鸡蛋液,里面还有包一些芝麻等调味料。 有些时候家里红白喜事的,直接通知厂里送一些汤圆。厂里就直接把做好的汤圆过水升温,熟食做好就直接送到客户手里了。 还有就是鸡蛋了。这当然不是市场上的生鸡蛋,而是雪粉鸡蛋。雪粉鸡蛋因为价格便宜,口感 Q 弹,已经在各种酒店和国外旅游市场占据非常大的份额。 因为都是直接副产,和汤圆生产一样,没啥差异。 有时候厂家在包装好有就直接销售了,因为是熟食,所以会加一些防腐剂。 但有时候也会进行调味然后真空包装。进入市场后就是各类小零食了。 通过挑选过的文字和图片,串联适当的逻辑,荒谬的事情就变得有理有据,变得合理了起来。 如果再把感情渲染这张王牌打出来,天马行空的荒谬也能直接变成思想钢印。 这就是当前媒体、自媒体、多媒体的怪象。

2020/1/19
articleCard.readMore

Array 和 LinkedList 的深刻认知

谈数据结构的时候,到底在谈论什么?本质上就是谈论两个东西,分别是 ** 数组 (Array) 和链表 (Linked List)**。 数据物理结构和数据逻辑结构 数组和链表,是数据存储计算机的物理结构,所以叫做数据物理结构。而队列、散列表、堆、树、图等,已经超出了物理结构,是方便我们使用的应用层结构,根据数据的 1-1、1-n、n-1、n-n 逻辑关系,引申出了数据逻辑结构。分别为线性结构和非线性结构,相见下图: 因为我们要使用数据了,所以数据的逻辑形态非常多,这就像应用层协议一样,为了方便我们使用互联网进行数据传输,会有非常多的协议准则。但是数据传输最终只能形成二进制携带 ip 和 port 通过信号传输,所以我们最终处理数据逻辑结构的物理结构只有两个,那就是数组和链表,而很多逻辑结构如完全二叉树、队列等,也都可以同时用数组和链表同时表示。 花开两朵,各表一枝 数组和链表的千差万别又独宠万千的根本原因是什么呢? 其实说到头就和 TCP 和 UDP 的关系一样,他们有各自的优缺点。TCP 和 UDP 磨磨杀杀几十年,谁也没有打败谁,反而都成了互联网传输层协议的中梁砥柱。 数组和链表的关系,也的确成为了花开两朵,各表一枝的比翼鸟。 时间复杂度 数组和链表,如果从直观上来说明不同,那从时间复杂度上来说明再形象不过了。对于数据的增删改查(CRUD),数组和链表的表现是不同的。 因为数据进行增删之前,都需要查找,所以查和增删一定要分开讨论,避免查的时间复杂度影响到增删。 而查到数据后的修改操作,本身不牵涉到时间复杂度,所以改我们可以不用考虑。 我们先看增删两种情况下数组和链表的区别: 数组 -> 增 -> O (n):每次增加一个元素到某个位置,都需要调整其他元素在数组中的位置,进行整体向后 偏移。 数组 -> 删 -> O (n):每次删除某个位置的一个元素,都需要调整其他元素在数组中的位置,进行整体向前 偏移。 ** 链表 -> 增 -> O (1)**:加入一个节点,直接更新前后连续 3 个节点的前后驱指针就好。 ** 链表 -> 删 -> O (1)**:删除一个节点,直接更新前后连续 3 个节点的前后驱指针就好。 从数据的增删来看,链表是可以秒杀数组的。即使数组对于删的操作进行优化,如假删除,可整体依旧没有链表快。 我们再看查情况下数组和链表的表现: 先说链表,因为链表所有节点的内存地址是不固定的,所以都需要进行一次查找操作。链表的查找只能通过一个节点的前后驱指针遍历查找,所以时间复杂度为 O (n)。 数组数据我们在不生成新数据逻辑结构的前提下,也只能逐个匹配查询,时间复杂度也是 O (n)。 如果数据本身是有序的,直接二分查找,时间复杂度为 O (logn) 因为我们要获取数据在数组里面的位置以进行增删操作,如果换成二叉搜索树等结构,查询到了数据也没有意义,因为拿不到相对于原数组的位置偏移。 从这里看,查方面,链表和数组打成平手。总体来看,数组是已经输给链表了,毕竟链表的增删复杂度只有 1。 But,这里有个但是,因为数组内存地址连续,所以天然支持随机访问,也就是 array[1] 这样的操作。 我们只要给予数组第一个数据内存地址的相对偏移,就可以根据这个偏移,通过随机访问获取到数据。相比来说,链表的则是顺序访问。 这个大杀器,使得数组的查时间复杂度变成 O (1)。 从这里看,从时间复杂度上面分析,数组和链接各有优势。 内存结构 从上面分析,数组本身已经败落,最后力挽狂澜的优势就是内存地址连续带来的随机访问。 这里就要先说一下数组和链表在内存存储上的差异。这是他们诸多不同的导火索。 数组的内存大小是连续且固定的。不管高级语言怎么做动态数组的优化,这个计算机硬件层面的约束总是跨不过去的。所有数组,都是固定大小的。 也就是我们在申明一个数组空间的时候,内部的元素个数和每个元素的 Size,都已经确定了,那么总大小也是确定的了。 而且,这个空间是连续的空间,即内存地址开始到结束这段区间,都属于该数组,也只能该数组使用。这段区间,可能有 10KB,也可能 100M,根据使用者申请的大小而定。 链表则有很大不同。使用者需要用到节点 (元素) 的时候,才会向系统申请内存空间。而系统分配空间的时候,也仅仅分配申请大小的空间,但这个空间在什么位置,系统不管。 所以使用者需要自己记录每个空间的地址,通过这个地址进行数据的存储。 所以链表的节点空间完全是混乱的,就像一团麻花一样,这一点和数组非常不同。 数组因为空间连续,所以知道开始地址,就能根据元素相对偏移定位到其他地址。而链表的节点空间完全混乱,怎么把节点联系起来呢? 所以链表就走了一个骚操作,每个节点都存储下一个节点的内存地址。这样所有节点形成链结构,就把所有数据串起来了。 所以数组和链表的内存结构十分不同,这样各自有什么好处吗? 内存空间分配 数组因为是连续空间,那么如果没有申请者需要的那么大的连续空间,就分配不了数组空间了。 如果申请者需要申请 10000 个 100KB 元素大小的空间,则需要近 1G 的连续内存空间。如果系统本身还有 2G 内存可供使用,但偏偏没有 1G 连续的空间,那系统因为没法处理,最后只能返回空地址。 链表就不需要连续了,申请者告诉系统需要 10000 个 100KB 的凌乱空间,系统就东凑西凑的一个一个返回给申请者。申请者自己把每个空间地址绑定到需要的节点上,就能使用了。 从这里看,链表比数组的灵活性更大。 细心的你一定发现了,系统返回的空间地址,总归需要存储起来,那每个节点都需要额外 4 个字节用来存储下游节点的内存地址。 那么对于刚才 10000 个元素来说,需要额外的近 40KB 的内存空间才行。所以每个节点,都需要多申请 4 字节的内存。如果对于复杂点的双向链表,就需要多 8 个字节了。 这里也发现,虽然链表与数组相比,虽然申请内存上灵活很多,但是也更费内存一些。 内存空间数量 因为数组的内存空间是固定的,那么在数据操作数组开始申请的大小后,就需要扩容。这个扩容代价还是非常大的。 首先,数组需要申请一块更大的内存空间,然后把之前的内存数据迁移过来,然后才能销毁之前的内存空间。 举上面的例子,如果 10000 个 100KB 的内存空间存满了,当第 10001 个元素进来的时候,就需要再申请 1.5G 的连续内存空间,然后把之前 1G 的数据全部拷贝过来。 所以数组对于扩容有天然的局限性,要处理的事情很多。 链表因为内存空间不连续,就天然免疫了。多一个节点,就向系统再申请 100KB 内存空间,然后串连到链表里面就好。 这里可以看出,链表相比数组,有节点数量上的优势。 内存碎片化 说起内存碎片化,数组和链表都有责任。因为他们都会导致内存碎片。 比如刚才 1G 内存的数组,当数组被清理后,就出现了一个 1G 内存的空洞,这个空洞需要留给其他人用。 如果是 100M,那就是 100M 的空洞,如果是 1M,那就是 1M 的空洞。 空洞大一些,我们感觉不到,因为可分配的余地很大。但是很多这样 1M 或者 10M 的内存空洞,一来不方便利用,二来内存空间里遍地都是,就形成内存碎片了。 那链表呢?也是一样的。因为链表需要频繁的申请内存空间,虽然不知道系统从哪里找来的空间,但链表被销毁后,这些内存空间依旧形成了大大小小的碎片。 所以对于内存碎片,数组和链表都会生成。但有一点,链表处理内存碎片的能力也很强。 链表需要空间的时候,都是一个节点一个节点去讨要的。虽然不知道系统怎么找来的空间,但系统肯定捡碎片来用比较方便,这样可以有效的使用内存空间。 数组因为批量一次性申请,系统很多时候也试着去找碎片空间,但是碎片很多都不符合数组要求。 所以链表相比数组更懂得报恩吧。 数据搬家 数组里的元素想要移动位置就比较麻烦了。因为内存总共就那么大,所有元素都是紧挨着排列的。 那么删除一个元素,就需要把其他元素补上来,增加一个元素,就需要把其他元素踢下去。所以每次元素新增和删除的时候,都挺费事的。这也是为什么数组插入和删除的时间复杂度都为 O (n) 的原因。 删除元素,我们可以通过假删除来优化。即不是物理删除,而是标记删除,这样查找数据的时候发现这个数据已经标记删除,不出处理就好。 那新增元素可就没有这么爽了。一来没有假增加一说,而来如果遇到扩容问题,复杂度又增加许多。 链表还是那么一句老话走天下,因为空间地址不连续,删除新增一个节点的时候,就把上下驱地址变更就好,方便的很。 所以,链表中的节点,还是比数组里的元素待遇好很多,不像数组那么折腾,动不动就要和邻居一起搬家,甚至迁徙风险还很高 (扩容) 随机访问和局部性原理带来的速度! 从上面看,链表相比数组,还是有不少优势的,甚至感觉数组有些多余。 可数组就有一个特性,是链表怎么也无法匹敌的,那就是速度! 对于 CPU 来说,只要给它地址,它就能帮你搞到数据。 因为数组的连续内存空间,所以只要给予相对地址的偏移,就能根据相对地址和偏移,找到偏移数据,这是随机访问带来的好处。 但是链表就没有那么好运了,节点空间都是系统随便给的,根本没有逻辑性,所以访问数据只能一个一个遍历找到对应节点的地址,这也就是顺序访问。 本来随机访问已经比顺序访问带来了很大的速度优势,数组还利用了计算机的高速缓存这趟快车进一步提升了自己的速度,数组真是鬼才。 比较数据获取速度,我们都知道固态硬盘(SSD)大于机械硬盘(HDD),内存条(Memory)大于固态硬盘,三级缓存(L3 Cache)大于内存条。现代计算机都有一级缓存(L1)、二级缓存(L2)、三级缓存(L3),其实一级缓存的速度已经非常非常快了,随机访问延时能达到 1ns,相比来说,内存延时有 100ns。 数组和链表数据都是在内存里的,如果访问数据的时候,能够顺带多访问一点数据并放置到 L1 L2 L3 缓存中,后期直接访问多级缓存,而不再去内存拿数据,那对于 CPU 来说可就太棒了,终于不用去等慢的要死的内存条了。 前辈们探索发现了存储器中的数据局部性原理。 当一个数据被访问,那么它短时间内很可能再次被访问,这是时间局部性。 当一个数据被访问,那么它相邻的数据很可能很快被访问,这是空间局部性。 当然,局部性原理里面说到的数据,不仅仅是数组里面的数据,还包括指令。但这已经完全够数组消化的了。 数组中某个元素被访问后,CPU 会顺带预读相邻的一些数据,一起从内存中读走并放置到高速缓存 (一级、二级、三级) 中。后面读取其他元素的时候,直接从高速缓存中读取了,都不过内存了。 数组利用了空间局部性提升自己的速度,比如遍历 100W 和 150W 数组数据的时候,一个耗时 100ms,一个耗时 110-120ms,耗时并不根据数组数量线性变化,就是这个原理。 链表没有搭上随机访问这趟列车,也搭不上高速缓存这趟快车,速度上已经落后数组不是一点点了。 总结 链表很多方面的确优与数组,但速度上就落后数组很多了。在速度和时间就是一切的大前提下,数组成功扳回了举足轻重的一局。 其实数组和链表还有很多细节可以讨论,比如数组在数学运算上更胜一筹,尤其二维数组做矩阵运算效率极高。而散列表和链表一起使用,就是规避数组和链表的各自不足。 数据逻辑结构,最后底层都会通过数组或链表实现,有些只能通过数组或者链表单一实现,有些则两者均可。还会根据机器配置等客观因素进行考量。 数组和链表各自开了两朵美丽的花,一起为算法撑起来物理基础。 算法串联数据,如脊贯肉; 数据实化算法,如肉附脊。 — 郑晖《冒号课堂》

2020/1/12
articleCard.readMore

我的 2019

参加工作后的这几年,评价哪一年比较平庸或许都不好,毕竟都是自己亲身经历过的时光,每一年都是像孩子一样善待过,不忍心批评。 但就像每人心里都有一个钟意的水杯,我还是很想投 2019 一票,因为这一年,我的确有不少变化。 这一年,我在技能成长、认知提升、工作、家庭生活、业余生活上都有很大变化。优秀肯定无从谈起,人一定要对自己有自知,但我一定是在向前走的,这让我很高兴。 技能成长 先从 2019 年的技能成长说起,因为它给我的温差最大,有时候我激动万分,有时候也在半夜垂头懊恼。 这一年,我把程序员的基础能力,能复习的复习了多遍,有需要深入研究的,也研究了多遍,收获颇多。 当把很多知识如编译、内存、网络等串联起来后,发现自己不是在使用代码,而是在尝试了解它,懂它,它不再是冷冰冰的工具了。 不过下半年开始我越发感觉自己的渺小和知识海洋的无边界,自己就是那沧海一栗的孤舟。对未知的领域保持敬畏之心,我亦是行人。 我总结一下技术学习的技巧,一定是学习资料的连贯性,资料一定要有章节,能够形成知识闭环。否则就是梦里开花盲人摸象,以为明白了,终究只是瞟客。 这里我着重感谢几个平台。一个是池老师的极客时间,上面的课程都非常优秀。一个是当当,我在上面买了很多书。一个是小众的网站 “C 语言中文网”,我向站长发了感谢邮件。 有两个心得也总结一下,一是知识一定是付费的但不贵,走捷径不值得。二是有价值的书读第二遍才能有收获,有些需要读三遍以上。 认知提升 2019 年,认知提升也非常值的说一说。 最感慨的,莫过于对时间的珍惜,时间真的不多了。人的一生太短了,一转眼又过去了一年,可一生又能有几个奋斗的年头呢?人的一生应该做的、想做的事情真的太多了,如果可以,都应该尝试一遍。可时间是绝对可以度量的,如何在有效的时间里做更多的事情,真是一门深学问。 其次是辨别问题多样性的能力提高了不少。古人云:耳听有虚眼见为实,当代互联网盛行,眼睛也不再可靠,比如剧情中的一帧和整个剧情分别表达的思想可能完全是相左的,但有人就是通过那一帧告诉你整个剧情。获取到的信息,我会根据常识和逻辑,多思考一下信息有效性。但对于精心策划过的信息,真不好辨别,因为信息本身经过一系列的逻辑推导和糖衣包装,常见于引导舆论的自媒体。 还有心态。这一年心态总体比较平稳,控制住情绪避免大起大落。常言 “不如意事常八九”,处人处事本身就是不顺心的过程,求同存异显得额外重要。对人而言所处的环境、认知、格局都不同,想法和实践都会有差异。对事而言没有一件事能够完美讨好所有人欢心,事物也都存在两面性。对与错没有明显的分割线,一定是求同存异而非一票否定。就像吵架双方都感觉自己是对的而对方是错的,不然不会吵起来,那到底谁对谁错呢? 其他杂七杂八的认知也有一些可圈可点,比如时间四象限法和心不二用,在工作和生活中尤其有用。还有个人隐私的保护。 而且这一年,我喜欢上写 Log 了。我发现记性这东西,非常不靠谱,大脑是不可靠的。我通过 Things 来做日常记录,一天 3-5 条 10 字以内的事项,写起来又快又方便。有时候文字里会带些偏见,但绝大部分文字都是客观中立的描述,毕竟不是日记,没有那么多心情波动需要展示。 工作 2019 年的工作,说好不好,说坏不坏,是变动的一年。 “2019 年是往年最坏的一年,以后最好的一年。” 2019 年的的确确带给了很多个人和公司挥之不去的噩梦。我和近 30 名同事年初从集团总部调到新成立的子公司,负责内部创业。但 2019 年是创业方向迷茫的一年,我们忙忙活活大半年,先后做了思维导图、资源共享、通讯软件等,却迟迟等不到领导层的精确创业方向。下半年确定了两个大方向,分别是企业级内部通讯私有化部署和企业级音视频会议,目前看是行之有效的而且已经有了一定的成果。 工作从来不是一件容易的事情,如果谁说工作容易,那一定是有问题的。工作是一个高技能活,需要把自己掌握的交流、合作、潜力等技能有效的联系起来,还要不断学习进步提高,才能将工作玩的好,也才能有格局看见更多的机会。 工作上有个小插曲,让我恐慌小半天。代码跑着跑着,一个不小心,删除了几十个 G 的文件。开始没注意,就看文件夹抖动,后来才发现本该运行结束的程序却还在执行中。原来删除文件代码写错了,可是又无法找回那已经丢失的文件。最后通过 “Time Machine” 恢复了,幸好我有隔段时间备份一下 “Time Machine” 的习惯。使用 Mac 电脑的用户,一定要经常通过时间机器及时备份系统,数据永远是无价的。制作成本也非常便宜,500G 的机械硬盘和 30 元的移动硬盘外壳,就可以 DIY 一个 “Time Machine” 备份盘了。 家庭生活 2019 年的家庭生活,总体是温馨和成长的。 婚姻是毕业人士工作后的第一座大山。没有一帆风顺的婚姻,我和老婆今年也吵架了几次,但老婆是开明的,我们的争吵没有延续多久,都是在和平谈判中和好。我一开始对于婚姻中的不满是沉默的,后来在老婆的指导下,我们开始开诚布公,有事说事。我也从单方面埋怨到后来自我错误修正。我老婆是很明智的,沟通是解决问题的唯一途径,内心单方面抱怨是没有价值且浪费时间的,一来对方不会知道啥原因,二来怨言可能毫无依据。 今年我和老婆也确定了家庭资金独立观念,不接受双方父母亲金钱上的刻意帮助。这是少有的在我的牵头下完成的家庭决定,我还是挺自豪的。生活一定要经历一些风雨,不然根基会不稳,而风雨的来源十之八九是金钱。婚后几年没有大花销,生活上也没有多少克制,对金钱观念也毕竟淡薄。金钱观念一定要培养起来,因为它是所有混乱、争吵、矛盾的源头。 这两年,尤其今年,我和老婆厨艺上升是明显的。开始时候,我们做的饭吃不太下。现在老婆做的饭还是非常优秀的,我也有很大的长进,尤其是做螃蟹。我们之所以减少外出吃饭,因为感觉外面吃饭不卫生。餐饮是生意,生意一定会有利益,在利益面前人都是会冒险,不管是食材还是卫生,都无法给予保障。 今年我和老婆一起购置了一套房产。考虑到各个方面,我们还是决定购置一套。购房过程还是比较顺利的,毕竟是买卖就会有服务,谁也不会和利益过不去。 婚姻和孩子相比,婚姻可简单多了。孩子真是大魔王,各种事需要操心,其中我老婆负担了绝大多数,相比来说,我是失职的。我和老婆已经刻意不溺爱孩子,但还是会忍不住的偏爱。现在孩子已经快三岁了,我每周有一天陪孩子玩耍,有时候我们在家,有时候我们去公园,有时候去游乐场,眼看着孩子一天天长大。今年,孩子成长的特别明显,会从 1 数到 10,会从 One 数到 Ten,会说一些故事给我们听,还会唱几首歌了。孩子喜欢和我玩捉迷藏,有一次我假装找不到孩子了,孩子跑到我的面前说 “我在这里我在这里”,我依旧装作找不到,最后孩子哭的一塌糊涂,对妈妈说爸爸找不到我了,我实在是笑坏了。我和孩子一起还画了一些漫无天际的画,回头看都会笑起来。孩子还会冒不然的说一些让我们惊奇的话,比如 “爸爸你真牛逼”,“我太累了” 等。最让我们好笑的是孩子的行为动作,有时候扭屁股,有时候学螃蟹走路,有时候扮鬼脸,有时候自顾自的跳舞,每次都能让我和老婆笑到不止。 孩子的家庭教育,对孩子的性格和日后生活都是潜移默化的。中国的孩子家庭教育普遍缺失严重,很多成年人都是有性格缺陷的。我希望给到孩子的不是金钱和生活,那是孩子成年后需要自己努力的,我希望给到孩子一个完整的生命体,孩子在这个完整的没有大偏差的生命体之上发展和长大。 还有两个能力希望孩子可以接受到培养,一个是抽象能力,一个是逻辑能力。现在孩子会把一个公仔玩偶拿着和我们说 “这是蛋糕”,我希望孩子能够把天马行空的抽象能力保持下去。逻辑能力也很重要,靠天生和学习。思维决定行动,逻辑可以影响思维从而约束行动的严密性。这两个能力对孩子的影响都是巨大的。 今年,明显感觉我的父母亲老了很多,面部皱纹增多了。我父亲会用微信的视频聊天功能了,每周最少聊天一次。记得小时候,父亲对我和哥哥说,科技真先进,已经有远程视频聊天了。现在,视频聊天已经进入寻常百姓家了,互联网这趟快车,真的所向披靡,而父亲他们在农村生活的老一辈,显然已经脱离了这趟快车。再过几十年,我们在孩子面前,肯定也是同样的脱节,科技和时间,是不会停下来的。 健康也已经成为今年焦虑的中心点,尤其购置房产之后。我体检结果还是很好,但上有老下有小,真担心出现一些不可预期的问题。双方父母和家人目前都购买了保险,也和老婆一起去洗牙了两次,得空会去公司健身房锻炼身体,饭后也会走走路消化一下。 业余生活 2019 年,还有很多可圈可点的业余生活值的一说。 首先我体验了近一个月的电子烟,才发现这玩意根本无法戒烟。我也是没有根据常识逻辑进行判断,电子烟和尼古丁瘾之间有互斥关系吗?显然没有。 有两类视频节目让我印象深刻。一个是 “耿哥出品 必属废品” 耿哥系列视频,我惊奇与他的工作方向,没有大的风险却可以有稳定的资金流入,只要耿哥自己不作,他的系列视频应该可以更新到他退休吧。还有一个是 ” 笑匠俗哥 “ 的七猫广告视频,简短不废话,扣人心悬啊。 王朗和诸葛亮的对话,没事时候也会回味一下。“明日在两军阵前,老夫只须一席话语,断叫诸葛亮拱手而降,蜀兵不战自退!” … “神器易更,而归有德之人” … 还有一些笑话,每次想到都会笑一番,如 “大家好,我是不紧张,我今天给大家讲的故事叫卖女孩的小火柴”。笑话真的可以有效降低压力。 以上,就是我的 2019 回顾和总结。 2019 年忙的很疲惫,希望 2020 年忙的有价值。

2020/1/7
articleCard.readMore

Swift struct (值类型) 与 class (引用类型) 区别及三种传参机制深刻关系和使用

在 Swift 中,struct(值类型)和 class(引用类型)的区别,不仅仅在于对象复制时候表现出来的差异,也不仅仅是构造器和继承的异同,本质上却是数据抽象层级的高低。 如果不能把值传参、地址传参和引用传参与类对象联系起来,也无法理解不同传参下对象的使用和 struct、class 的应用场景。 因为 struct 和 class 表现出来的是语法层面的差异,而项目使用中体现的是语义层级的差异。比如,Objective-C 里面的 NSString,它是引用类型,但是我们却在使用它的值语义。 1 2 3 4 5 6 7 NSMutableString *oldname = @"hello".mutableCopy; NSMutableString *newname = oldname; newname = @"word".mutableCopy; NSLog(@"\n oldName:%@ \n newName:%@", oldname, newname); > oldName:hello > newName:word 我们使用了引用类型的 NSMutableString,但是 newname 并没有引用复制 oldname,仅仅是指针地址复制了 oldname。这样导致了 newname 并不是 oldname 的别名 (alias)。 如果 newname 是 oldname 的别名,那么对 newname 的所有 all 操作,都会同步到 oldname。 这里,newname 如果改变了对象数据是可以同步到 oldname,但是却不能改变 oldname 变量的值(oldname 的存储值,即 “hello” 的指针地址)。 所以,这里的 NSMutableString 虽然是引用类型,却具有值语义。 因为编程语言概念上的模糊,下面首先介绍 struct 和值类型的关系。 然后重点说明值类型和引用类型的区别,这是重点,直接解释了 struct 和 class 的根本区别。 最后加一点小彩蛋,介绍 Swift 里面 struct 特性。 0X00 struct 是什么?好像在哪里见过!struct 和值类型是什么关系? 好久好久之前,是没有 class 什么事的。那时候 C 语言活跃于各种应用场景,在上层抽象了汇编的实现,C 语言可以用于嵌入式、服务器、驱动等。为什么没有 class 什么事情?class 是类,是 OOP(面向对象编程 / 对象导向编程)的专属。而 C 语言是过程式语言,没有对象概念,也就没有 class 什么事情。 那时候,都用结构体,也就是 struct。如下: 1 2 3 4 5 6 // C语言版本 // 人员结构体,具有姓名和年龄 struct Person { char *name; int age; }; 那时候,这些数据都叫做值类型,而 struct 就是值类型的。值类型是变量直接包含值统称,其他的还有基础数据类型,如 Int,long,char,bool,以及自定义 struct(如上面的 Person)。 后面大家就知道了,出现了 C++/Java/C# 等 OOP 语言,class 开始活跃于千万家。更甚之,在某些语言上,值类型已经被淡化了。 1 2 3 4 5 // Objective-C版本 .h @interface Person : NSObject @property(nonatomic,copy) NSString *name; @property(nonatomic,assign) NSInteger age; @end 以上为 OC 的版本,我们会这么描述它:我们创建了一个 Person 类。他具有名称属性和年龄属性。 而且,我们也确定,如果我们新建一个 Person 对象,如 Person *p = Person.new;,那么这个对象一定存放在堆内存。那我们为什么不能把对象创建在栈内存呢? 我们可以知道,上面 OC 创建的是 Class,对象存在于堆中。那我们可不可以把对象放在栈上?至少在 Objective-C 开发过程中,肯定不会想到这个,因为 struct / 结构体 / 值类型被淡化了。(Java 也是一样,号称纯 OOP 语言) 其实我们可以创建栈上的数据的,如局部变量中的 Int、long 等,都是在栈上的。说一个少见的,CGRect、CGSize 也是在栈上的。源码实现如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /* Points. */ struct CGPoint { CGFloat x; CGFloat y; }; typedef struct CG_BOXABLE CGPoint CGPoint; /* Sizes. */ struct CGSize { CGFloat width; CGFloat height; }; typedef struct CG_BOXABLE CGSize CGSize; /* Rectangles. */ struct CGRect { CGPoint origin; CGSize size; }; typedef struct CG_BOXABLE CGRect CGRect; 其实 OC 中使用的 CGRect 等,也就是 struct 值类型数据。只是我们很少顾及值类型,虽然一直用,但是感知不到他们的存在。 直到有一天,Swift 也成了苹果开发的官方语言,引入了 struct,一大堆开发人员开始迷惑,发生了什么事?我该怎么办? 1 2 3 4 5 6 7 8 9 10 11 // Swift struct版本 struct Person { var name: String var age: UInt8 } // Swift class版本 class Person { var name: String var age: UInt8 } 同样的 Person,一个是 struct,一个是 class。一个复制的时候是全部数据复制,一个复制的时候是指针复制。一个有可能存放在栈上,一个只能存放在堆上。 放心了很多,原来 struct 和 Int、BOOL 等是一样的。他们本身不可以改变,只能被复制。struct 拥有和 Int 一样的外观。 等等,Swift 的 struct 可以引入方法,可以实现构造器,甚至可以通过 mutating 来改变自己。俨然已经上升可以和 class 平起平坐了(其实没有高低之分)。这怎么和 Int 又不太一样了?struct 到底是啥? 其实 struct 一直都没有消失。它一直在我们周边。仅仅是因为我们在个别面向对象语言的冲击下,淡化了值类型。 而现在,在 Swift 中,我们必须捡起来,因为值类型在 Swift 中不可或缺了。甚至 Swift 里面的 Int,String 等,都是通过 struct 直接实现的。 1 2 3 4 5 6 7 8 9 10 11 /// A signed integer value type. /// /// On 32-bit platforms, `Int` is the same size as `Int32`, and /// on 64-bit platforms, `Int` is the same size as `Int64`. public struct Int : FixedWidthInteger, SignedInteger { /// A type that represents an integer literal. public typealias IntegerLiteralType = Int ... ... } 相比之下,C++ 一直存在值类型,而且 C++ 里面的自定义的 struct 还可以继承和多态,完全面向对象化了。 0X01 值类型和引用类型的内存差异 值类型和引用类型,从语法上还是毕竟容易理解的。如果牵涉到语义,就比较复杂,因为上面说到的,引用类型就牵涉到了值语义。 我们先从语法上来理解值类型和引用类型的内存差异。 引用类型内存图 首先,class 是引用类型,一定是存放在堆内存上的。如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // Swift class版本 class Person { var name: String? var age: UInt8? } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" var someOne = person someOne.name = "Robot" > person.name:Robot > someOne.name:Robot 那么我们定义一个 Person 对象时候的内存分布如下图: person 这个引用变量,我们使用的时候,可能存储在栈中的(也可能在堆中,但不是重点)。但是其指向的对象,却一定是在堆中的。 这里我们有两个名词认知不要弄混了,一个是 person 变量,一个是 person 对象。变量只是符号,编译的时候存储于符号表中的一个标记,对象才是我们使用的数据实体,变量用于找到对象。someOne 也是同理。下文中的变量和对象都是同理,后面不再做强调。 上面操作完成后,person 和 someOne 的 name 都变成 Robot 了,这是合情合理的,我们都司空见惯了,不做多描述。 值类型内存图 下面看看值类型, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // Swift struct版本 struct Person { var name: String var age: UInt8 } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" var someOne = person someOne.name = "Robot" > person.name:Hello > someOne.name:Robot 内存分布如下图: 我们可以发现,值类型相比引用类型,person 和 someOne 这两个值变量,指向特征不那么明显了,更多的是复制。我们的 someOne 并没有指向 person,而是把 person 的数据完全复制了一份成为自己的。 更重要的,person 和 someOne 存储的,不再是对象的指针,而是真真实实的数据了。其实对象依旧还是对象,变量依旧是变量,如上面说的那样。但是,值变量,直接包含数据 (值类型的定义) 了,不再通过指针指向数据了。 当然,我们从图上看到的,值对象是在栈里面。当然值对象也会在堆里面,场景不一样,存储位置也会不一样。但是和引用对象不同的一点,值对象是可以存储在栈里面的。 我们经常使用的 Int 等,其实很多时候都是存储在栈里面的。上面 Person 引用类,其实也有 age 属性,这个 age 属性就是值对象存储在堆上,因为 person 对象是在堆上的。后面说到 struct 和 class 联动内嵌的时候,会详细说明 值类型和引用类型内存比较 显而易见,内存方式的不同,带来的优缺点也是迥异的。 最直接的,栈肯定是比堆快的。下面我们默认值对象存储在栈中,引用对象存储在堆中进行分析。 一来,值类型通过值变量直接定位数据,而引用类型需要通过引用变量的指针间接获取,在寻址方面就会出现时间劣势。 再者,栈通过 CPU 提供的指令寄存器操作数据,而堆通过操作系统支持。堆空间由程序员自行控制,包括垃圾回收等,CPU 不会干预过多。 其次,我们在栈上分配内存,是直接分配。而对于引用类型,只在栈上分配变量地址,对象需要另外分配堆内存空间。(可能会出现这种情况,需要 100 个对象,栈类型会在栈内存中直接分配完毕,而引用类型会在栈上一次性分配 100 个变量内存,然后在堆中需要进行 100 次对象内存分配。) 而且,由于堆内存是空间不连续性的(操作系统分配堆内存池供开发使用,如果一个对象销毁了,就会产生内存碎片),不连续堆空间会违背局部性原理,会增加高速缓存错失的几率(命中不了)。堆空间的高速缓存指的是二级缓存,而栈是依据 “LIFO” 的存储机制,不会出现内存碎片,天然增加了一级缓存的命中率。 特别,各种语言都会着重优化值类型,以达到更快的速度。毕竟栈处理速度的快慢,直接影响到程序的快慢。因为我们的代码运行根本依靠函数,而函数就是在栈中执行,如 main 函数和自定义函数。所以才会加入寄存器这样的快速单元,而堆里的数据,是可以通过异步来完成存储的。 因为值对象也是会存储在堆中,所以我们可以这样说:值类型有可能利用栈的优势,进一步提高程序的性能。 内存比较上,相比来说,值类型完胜引用类型。 引用类型相应的优点, 一来,发生在拷贝的过程中。值类型拷贝是完全拷贝,所有数据都会拷贝一遍,而引用类型,拷贝的仅仅是指针,从而提交效率。 二来,栈内存空间有有限的,相比堆内存空间,简直太小来,如著名的网站 “Stack Overflow” 的名字一样,动不动就会栈溢出。而堆内存,就是普通点的服务器,4G 容量不是问题的,高级点的服务器都是几十几百 G 容量。 我们可以看出,栈和堆相比,强于速度,弱于空间。虽然速度完胜,但除了空间,还有其他方面,引用类型却又是完胜值类型的。从 Java,Objective-C 这些语言上不难看出,引用类型的确是完胜值类型的。下面我们一点点来分析他们之间的不同和场景应用。 0X02 值类型和引用类型的相等性差异 值类型相等性比较 我们回顾一些最基础的值类型 Int 的相等性比较。 1 2 3 4 5 var num1 = 100 var num2 = 100 assert(num1 == num2) var num3 = 200 assert(num1 != num3) 上面代码编译运行,断言是可以通过的。值类型的比较,特别简单,就是比较数据是否一样。 因为值类型,不管存储在哪里,不变的一点是,值变量直接包含值对象。我们自定义一个值类型来看一下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Swift struct版本 struct Person: Equatable { var name: String var age: UInt8 } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" var someOne = person someOne.name = "Hello" // someOne.name = "Robot" assert(person == someOne) // assert(person != someOne) 它们的内存图如下: 当两个值对象的 name 都是 Hello 的时候,两个值对象是相等的。如果 someOne 的 name 变成 Robot,两个值对象就是不想等的。(如果我们把注释的代码打开,相应注释位置上一行代码删除,会发现两个断言都是可以通过的。) 值类型的相等性比较真的非常简单,就是匹配字节码是否一致。只要值对象的所有字节码是一致的,那两个值对象就是相等的。字节码从哪里来?Hello 和 Robot,计算机不认识的,他们都会变成对应的码值然后转化成二进制存储内存中。所以值类型相等性判断就是查看二进制是否一样。 引用类型相等性比较 因为引用变量存储的是引用对象的内存地址。同样一个对象,可能有两个引用变量存储着其地址。 所以引用类型的比较,有两个方面,一个是比较存储的内存地址是否一致,另一个是比较内存地址对应的数据是否一致。 因为字符串在 Swift 里面是值类型的,我们用 Objective-C 里面的 NSString 来分析。 1 2 3 4 5 NSString *str1 = [NSString stringWithFormat:@"%@", @"Hello"]; NSString *str2 = @"Hello"; assert(str1 == str2); // ERROR assert([str1 isEqualToString:str2]); // OK 相应的内存图如下: 因为 Objective-C 对于字符串的生成比较考究(Objective-C 里面,字符串根据创建的形式不同和存储中英文的不同,有常量区、栈区、堆区不同表现形式),我们用上面方式建立两个不同地址的 Hello 字符串。其中比较相等性,一个是通过 **==,一个是通过 equal**。 引用类型的相等性比较,直接通过值类型的 == 比较的化,比较的是内存地址,显然 str1 和 str2,他们的内存地址不可能一样,所以他们并不相等。 而通过 equal 来比较,就变成了上面的值类型的字节码比较,Hello 的二进制存储都是一样的,他们就相等了。 0X03 值类型和引用类型在花式传参过程中的异同 如果不能把值传参、地址传参和引用传参与类对象联系起来,也无法理解不同传参下对象的使用和 struct、class 的应用场景。 为什么三种传参方式对 struct 和 class 的理解如此重要?其实他们本不重要,只是附带品。但是因为很多很多人对他们认知是错误的,所以才变得重要起来。 毕竟,错误的理论,总不能推导出来正确的知识。 首先,我们需要确定一个知识点,值传参、引用传参、地址传参和值类型、引用类型相比,虽然也有值和引用的区分,但他们不一一对应的关系,即不是值类型对应值传参。 值类型和引用类型是数据存在的方式,三种传参方式,是数据传递的方式。他们是对数据两个层面的操作控制。 所以,我们总共有 6 种情况需要分析:值类型下的值传参、引用传参、地址传参,引用类型下的值传参、引用传参、地址传参。 说明:因为引用传参很多语言默认都没有实现,如 Java、Objective-C、Swift 等等,所以需要通过 C++ 模拟。 值类型下的值传参、引用传参、地址传参 示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 // 值类型的值传参 // Swift struct版本 struct Person: Equatable { var name: String var age: UInt8 } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" var someOne = person someOne.name = "Robot" someOne = Person(name: "SomeOne", age: 18) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 值类型的地址传参 // Swift struct版本 struct Person { var name: String var age: UInt8 } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" func change(someOne: inout Person) { someOne.name = "Robot" someOne = Person(name: "SomeOne", age: 18) } change(someOne: &person) 1 2 3 4 5 6 7 8 9 // 值类型的引用传参 // C++ 版本 void change(int & param2){ param2++; param2 = 100; } int param1 = 20; change(param1); 上面的内存分布如图: 我们分析一下值类型下的三种传参, 值传参,就是行参对象原封不动的获取了实参对象的数据拷贝。两个对象之间不再有任何关联,对行参对象的内部修改 (someOne.name = “Robot”) 和自身修改 (someOne = Person (name: “SomeOne”, age: 18)),都不会更改另一个对象的任何数据。如上图所示,person 对象没有任何修改。 地址传参,就是行参对象原封不动的获取了实参对象的地址拷贝。所以行参实际存储的是实参的内存地址拷贝。那么通过内存地址对数据的内部修改 (someOne.name = “Robot”) 都会影响到原对象,毕竟对象只有一个。但是行参自身修改 (someOne = Person (name: “SomeOne”, age: 18)),却不会影响到原对象,因为行参是实参的地址拷贝,自身数据的改变就是改变了拷贝的那份内存地址,不会影响到实参。所以地址传参,本质还是值传参,因为拷贝了实参的内存地址(而非整个对象数据)。而通过内存地址修改原数据,这是一种途径,和传参无关,因为内存地址本身就是用来获取和修改数据的。 引用传参,分析起来其实最简单的。那就是,行参仅仅是实参的一个别名 (alias)。行参存储的依旧是实参的内存地址拷贝,但是对行参所有的操作,都会通过间接寻址的方式直接操作实参。注意,是所有操作,因为行参仅仅是实参的别名。所以我们看到,引用传参里面,行参不仅可以修改原对象数据,还可以更换原对象。(这个更换原对象,有两种方式,上图中是其中一种,即在原对象内部修改数据,没有创建新对象。还有一种方式是创建一个全新的对象,然后实参指向新对象,是以新换旧的思想。但是原理都不变,都是行参操作实参直接改变数据。) 通过上面值类型的三种传参,大家可能已经发现,在 Objective-C、Java、Swift 里面进行值类型的实参行参赋值的时候,其实都只是值传参和地址传参,并没有引用传参。只有个别语言如 C++ 里面才支持引用传参。 上面说明的是值类型,那么引用类型的三种传参是否会有一些不同呢? 引用类型下的值传参、引用传参、地址传参 示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 引用类型的值传参 // Swift class版本 class Person { var name: String var age: UInt8 init(name: String, age: UInt8) { self.name = name self.age = age } func copy() -> Person { return Person(name: name, age: age) } } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" var someOne = person.copy(); person.name = "Robot" someOne.name = "SomeOne" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 引用类型下的地址传参 // Swift class版本 class Person { var name: String var age: UInt8 init(name: String, age: UInt8) { self.name = name self.age = age } } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" var someOne = person; person.name = "Robot" someOne = Person(name: "SomeOne", age: 18) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 引用类型下的引用传参 // C++ 版本 class Person { public: char *name; int age; Person(char *temp, int flag); }; Person::Person(char *temp, int flag) { name = new char[strlen(temp) + 1]; strcpy(name, temp); age = flag; } void change(Person &someOne) { someOne.name = "Robot"; someOne = Person("SomeOne", 18); } int main(int argc, const char * argv[]) { Person person("Hello", 20); change(person); return 0; } 上面的内存分布如图: 我们分析一下引用类型下的三种传参, 值传参,因为引用类型不支持值传参,所以我们使用 copy,模拟了值传参。这也是深拷贝的实现方式。我们通过自定义 copy 来实现引用类型的值传参。 地址传参,和值类型的地址传参没有区别。 引用传参,和值类型的引用传参没有区别。 我们发现,值类型的直接赋值,使用的是值传参。引用类型的直接赋值,使用的是地址传参。而在值类型下的三种传参分析中,我们分析到地址传参就是特殊的值传参。 所以我们可以得出结论,值类型和引用类型的直接赋值,都是值传参的形式。而引用传参,需要语言级别的实现,目前 C++ 有实现,Java、Objective-C、Swift 等语言中皆没有实现。 花式传参总结 值传参、地址传参、引用传参,是数据赋值过程中的不同表现形式。而地址传参,本质还是值传参。所以我们可以说只有两种传参方式,即值传参和引用传参。 而值传参和引用传参,和值类型与引用类型不是一一对应的关系,具体不同是下文说到的语义的不同,即: 根据我们使用的场景,我们可以通过将值传参和引用传参作用于值类型对象上。 根据我们使用的场景,我们也可以通过将值传参和引用传参作用于引用类型对象上。 如果我们需要改变实参,则我们需要引用传参。(很多语言不支持) 如果我们不需要改变实参,则我们需要值传参。如果想进一步改变原对象的部分值,则需要地址传参通过指针来实现。 0X04 值语义和引用语义的联动性 直观来说,值类型语法上是静态的,变量直接包含并操作数据。引用类型语法上是动态的,通过对象的引用操作数据。从值类型和引用类型的内存结构图可以分析出来。 所以,我们引申出值语义,即数据是静态的,也就是值传参的逻辑,是数据拷贝,是一份全新的数据。 我们也引申出引用语义,即数据是动态的,也就是引用传参的逻辑,数据可以通过别名操作,数据本身没有进行任何复制。 所以我们可以这样理解,值类型默认应该是值语义的,引用类型默认应该是引用语义的。 如下所示: 1 2 3 4 5 6 7 // 值变量v1与v2互相独立 ValueType v1 = someValue; ValueType v2 = v1; // 引用变量r1与r2互相关联 ReferenceType r1 = someObject; ReferenceType r2 = r1; 上面 v1 和 v2 都是值类型 ValueType 的对象,其中 v1 和 v2 没有任何关联,默认的值传参。 上面 r1 和 r2 都是引用类型 ReferenceType 的对象,其中 r2 是 r1 的别名,操作 r2 和操作 r1 完全一样。 但是事与愿违,至少引用类型的默认引用传参,就是很多语言所不支持的。我们的 Swift 就没有办法实现引用类型的引用传参。即上面的 r1 和 r2 如果在 Swift 中定义,那么 r2 不是 r1 的别名,仅仅是 r1 存储的 someObject 地址的一份拷贝。 所以,严格按照引申出来的值语义和引用语义的定义,那么很多语言都没有引用语义了。这显然是片面的,因为我们的地址传参,也可以通过指针操作数据来实现动态性。 所以严格的定义如下: 值语义的对象是独立的, 引用语义的对象却是允许共享的。 理解下来,就是: 值传参具有值语义,因为值传参后行参实参两个对象完全独立。 地址传参和引用传参,具有引用语义,因为行参可以通过指针或者别名(实质还是指针,间接寻址方式)来操作实参对象。 我们在 “花式传参” 中说到有 6 种传参方式,因为值类型对应三种传参,引用类型也对应三种传参。 所以我们可以发现: 值类型通过地址传参和引用传参,可以实现引用语义。 引用类型通过值传参,可以实现值语义。 代码示例如下: 1 2 3 4 5 6 7 8 // 值类型的 ValueType具有引用语义(C++) ValueType v1 = someValue; ValueType& v2 = v1; // 方法1:通过引用让v2成为v1的别名 ValueType* v3 = &v1; // 方法2:通过指针让v3指向v1 // 引用类型的ReferenceType具有值语义 ReferenceType r1 = someObject; ReferenceType r2 = r1.copy(); // r1与r2是两个完全独立的对象 而相应的语言如 Objective-C 就有很多语义层级的类型处理,将值类型处理成引用语义,将引用类型处理成值语义。如下: 1 2 3 4 5 6 7 8 9 10 11 // int值类型具有引用语义 int i1 = 8; int *i2 = &i1; (*i2)++; NSLog(@"i:%d,ii:%d", i1, *i2);// i1:9,i2:9 // NSString引用类型具有值语义 NSString *s1 = @"s1 value"; NSString *s2 = s1; s2 = @"s2 value"; NSLog(@"s1:%@,s2:%@", s1, s2);// s1:s1 value,s2:s2 value 所以, 语法上的值类型可能是语义上的引用类型, 语法上的引用类型可能是语义上的值类型。 0X05 值类型和引用类型的抽象层级差异 在文章开头提到的值类型和引用类型的内存模型中,已经明确值类型是直接包含数据,而引用类型是通过内存地址间接操作数据。 从内存分布上,我们可以模糊的发现: 值类型,重在数据,是静态的。如果一个数据是通过值类型来展示,那么这个数据重在数据的价值。这个数据一定非常重要,而我们可以通过值变量直接获取。 引用类型,重在如何使用,是动态的。如果一个数据是通过引用类型来展示,那么这个数据重在如何被使用。这个数据当然也重要,但是我们可以通过指针和引用 (别名 / 间接寻址) 变相获取,而指针可以赋值给其他变量,最后可以通过各个变量来获取。 我们可以创建各种面向对象的引用类型 (class),然后实现继承和多态,这是引用类型带来的益处。因为我们可以通过指针和引用轻松的实现多态特征。 而对于值类型 (struct),Swift 文档里直接说明,struct 不允许继承和重载。值类型在编译期间具体类型就已经确定,多态绑定也是不可能,因为其空间大小已经确定,没有空余空间容纳子类型。 所以从抽象层级上来观看,引用类型相比值类型,抽象层级更高。 因为值类型在编译期间空间大小和具体类型已经确定,所以值类型完全就是不依赖内存地址的,这也是为什么值变量直接包含值对象的原因,所以值类型是具有空间无关性的。而引用类型存储于堆中,必须通过指针进行访问,显然引用类型和空间是强关联的。 而且,值类型的数据在值对象生命周期内是固定的,体现了不可变性,具有时间无关性。即使通过地址传参和引用传参,我们可以改变值对象的数据,那也是在原来的存储空间中,用新数据覆盖旧数据,使用了旧数据的存储空间而已。引用类型的改变是一种自我更新,对象上发生状态迁移和属性改变。 因为值类型的时间无关性和空间无关性,所以值类型天然具有重数据,对象意识淡薄,更多的体现在属性而非实体。比如我们描述一个 Person 的 struct,更多的体现在用 age,sex 等抽象属性来描述 “20”“男” 这样的具体数据(“20”、“男” 本身是具体的,age、sex 属性则是面向对象层级的抽象描述)。 而引用类型与值类型相比是对立面的抽象表达,更多的体现一个实体对象。比如我们描述一个 Person 的 class,更多的体现在男人、女人、好人、坏人这样的实体对象。 总结来看,值类型是引用类型的基础,值类型在内存和速度的使用上,拥有更快的速度。引用类型在值类型之上进行了更多的抽象。 引用类型依靠间接性和抽象性,相比值类型拥有来更大的灵活性,小方面来说在赋值上通过地址传参和引用传参节省了时间损耗,大方面来说通过间接性和抽象性,直接成为实现多态的必要条件。 这也从侧面反映了 class 可以继承和多态,看起来繁荣昌盛,而 struct 则显得不起眼,为居一隅却撑起来整个面向对象大厦。 0X06 struct 和 class 联动内嵌下的认知 单个分析值类型和引用类型还比较容易理解,如果值类型和引用类型相互嵌套,虽然并不是复杂的逻辑,但如果不画图理解一下,很容易脑回路阻塞。 下面通过示例代码和内存图清晰了解一下: 值类型嵌套值类型 1 2 3 4 5 6 7 // Swift struct版本 struct Person { var name: String var age: UInt8 } var person = Person(name: "Gongjiang", age: 20) person.name = "Hello" 值类型嵌套引用类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // Swift class版本 class Dog { var name: String init(name: String) { self.name = name } } // Swift struct版本 struct Person { var name: String var age: UInt8 var dog: Dog // 引用类型 } var dog = Dog(name: "Wa") var person = Person(name: "Gongjiang", age: 20, dog: dog) person.name = "Hello" 引用类型嵌套值类型 1 2 3 4 5 6 7 8 // Swift class版本 class Dog { var name: String init(name: String) { self.name = name } } var dog = Dog(name: "Wa") 引用类型嵌套引用类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // Swift class版本 class Dog { var name: String init(name: String) { self.name = name } } // Swift class版本 class Person { var name: String var age: UInt8 var dog: Dog // 引用类型 init(name: String, age: UInt8, dog: Dog) { self.name = name self.age = age self.dog = dog } } var dog = Dog(name: "Wa") var person = Person(name: "Gongjiang", age: 20, dog: dog) person.name = "Hello" 0X07 Swift 中的 struct 为什么很特别?struct 能给我们带来哪些认知? 本身 struct 不是很特别,但是在目前很流行的语言(Objective-C、Java)均不支持自定义 struct 的前提下,Swift 开始支持了。而且 Swift 本身如 String、Int、Bool 等均为 struct。Swift 把已经被很多开发人员忽视的值类型提到了非常高的高度,这就非常值的重视了。 值对象本身是比引用对象安全的。通过指针可以直接操作数据,如果代码逻辑比较复杂,那么在一个不起眼的位置,很可能修改了一个共享的重要参数,非常有利于 bug 的滋生和增加 bug 排查复杂度。相比来说,值对象具有空间无关性,代码层面上可以有效的抵制指针带来的负面影响。所以值对象是更安全的。 struct 没有引用计数,本身是自动线程安全的。 在上面值类型和引用类型的内存差异中,我们说到,值类型相比引用类型,有速度上的天然优势。 struct 没有继承,相对来说更安全,而且 struct 可以实现协议,可以很好的实现面向协议编程。Swift 是一门多范式编程的语言,其中对于面向协议编程尤其重视。 OX08 Swift 下 String 的搅局误区 在讨论 Swift 的值类型中,多次提到 String 类型是值类型的。我相信很多朋友都疑惑,String 作为值类型是如何实现的。 毕竟,下面的代码中: 1 2 3 4 5 6 7 8 9 10 11 var str = "Hello" withUnsafePointer(to: &str) { (value) in print(value) // 0x0000000100145060 } str.append("Hello...Hello") // 有1000个Hello withUnsafePointer(to: &str) { (value) in print(value) // 0x0000000100145060 } 我们定义了一个可变变量 str 为值类型的字符串变量,按照内存图逻辑,在 str 拼接了 1000 个 Hello 后,再怎么说,str 的空间存储也不够了。 如果 str 的内存地址变化了,那么就说明 str 不是值类型了(上面我们说过值类型的内存变化)。 如果 str 的内存地址没有变,那 str 再怎么也存储不了无限多个字符啊! 我们拿 Int 来做对比: 1 2 var i: Int8 = 10 i = 10000000 // Error 这里如果对 Int8 值类型的变量赋值过大,首先编译就过不了了,超过 Int8 的存储空间了嘛。 值类型的空间大小,在编译时就已经确定,这是毋庸置疑的。 那 String 是如何做到的呢? 打印一下上面代码中的 str 大小,看看到底是多大: 1 2 3 4 5 6 7 var str = "Hello" print(MemoryLayout.stride(ofValue: str)) // 16 str.append("Hello...Hello") // 有1000个Hello print(MemoryLayout.stride(ofValue: str)) // 16 可以发现,str 的内存大小,没有变化过… 这也说明了,String 的确是值类型的,它真的可以存储非常非常多的字符。 我在查看了 “M 了个 J” 大神的博客后,依然无解,因为他把答案公布在了两小时的汇编视频中,而我没有去看。 这里把大神的博客放下,希望有心人能去窥探一下。 https://www.cnblogs.com/mjios/p/11799136.html 朋友对我说,抽象是一名程序员核心的能力。很多时候,我越发觉得这句话说的对!

2019/12/20
articleCard.readMore

逃离信用卡和花呗

我刚刚关闭了自己的花呗。紧接着开始逐渐关闭一些信用卡的绑定。 这种温水煮青蛙的花钱方式,我经历了 6 年了。现在,因为穷,我有些适应不下去了。 我不知道为什么每个月都有这么多的钱要还,我更不知道为什么我要办理 4 张信用卡和花呗一起使用。 我就知道,我已经很久没有带现金外出过了,我家里也很少有现金。 但是,一点点的无知觉消费,使得每个月都有很多钱要还。计算机 / 服务器永远不会自己计算错,说明还的钱都是有依据的。无感知的消费,使得每个月消费金额过多。 相信我,远离信用卡和花呗。过段时间,我会贴出重回现金生活的感受。我想,一定非常棒。

2019/12/9
articleCard.readMore

Swift_let_var_struct_class

0x01 问题描述和表现 对于 let 和 var,struct 和 class,分成两类来说,很多人比较容易理解。let:不可变,var:可变,struct:栈空间,class:堆空间。 当我想要确认他们的原理的时候,截止发文为止,我没有在中文互联网上搜索到相关信息。所以我把他们的原理写在下面。 下面的示例,你能够理清楚多少? 1 2 3 4 5 6 7 8 struct Person1 { let name1: String = "name1" var name2: String = "name2" } class Person2 { let name1: String = "name1" var name2: String = "name2" } 上面定义了一个 struct,一个 class,我们下面的代码会有什么结果? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 let test1 = Person1() test1.name1 = "t" test1.name2 = "t" test1 = Person1() var test2 = Person1() test2.name1 = "t" test2.name2 = "t" test2 = Person1() let test3 = Person2() test3.name1 = "t" test3.name2 = "t" test3 = Person2() var test4 = Person2() test4.name1 = "t" test4.name2 = "t" test4 = Person2() 这里我们定义了四个对象变量,两个 let,两个 var。两个 let 里面一个是 struct 对象,一个是 class 对象。两个 var 也是一样。然后 struct 里面有一个 let 和一个 var 属性,class 也是一样。 上面的代码会报错,你知道的。但是你知道会是什么样的报错吗? 如上图所示,同样的let对象变量(test1/test3),struct和class有不同的错误。同样的struct,let和var也有不同的错误。 重点来说, 如果我们用 let 声明了一个 struct 对象,那么基本可以确定内部属性一个都不能改变了(不管内部属性是用 let 或者 var 修饰)。如果我们用 let 声明一个 class 的对象,那么内部属于 var 的属性还是可以改变的。 如果我们用 var 声明一个 struct 对象,那么表现同 class 对象一样。 通过上面两点,我们可以发现,struct 和 class,他们之间的差别,肯定不仅仅是栈和堆内存的关系,不是吗? 实际上,struct 比 class 有更严格的约束。而 let 和 var 的可变和不可变也有更深的原理。 0x02 let 和 var 到底是什么? 如果理解 C 里面的指针和 const 不变量,那么非常容易理解了。 我们在 C 里面定义下面四个变量, 1 2 (1) const int age1 = 21; 1 2 3 (2) int temp = 22; const int *age2 = &temp; 1 2 3 (3) int temp = 23; int * const age3 = &temp; 1 2 3 (4) int temp = 24; const int * const age4 = &temp; 这四个常量 age 的存储,如下图所示: 对于 age1,我们就不能通过 age1 = 0; 这样的语句修改值了。 对于 age2,因为 const 标记的是值,所以 *age2 = 0; 是不行的,会报错。但是我们可以通过 age2 = &other; 这样的语句修改 age2 对应的指针值。 对于 age3,因为 const 标记的是指针,所以 age3 = &other;* 是不行的,会报错。但是我们可以通过 age3 = 0;** 这样的语句修改 age3 对应的数值。 对于 age4,因为数值和指针都是只读的,都是常量,什么也修改不了。 我们发现,其实 Swift 里的 let,就是 C 里的 const。但是在 struct 和 class 中表现是不一致的。struct 类型的 let 变量就是 “(4)int temp = 24;const int * const age4 = &temp;” 类型,什么都修改不了。而 class 类型的 let 变量,就是 “(3)int temp = 23;int * const age3 = &temp;” 类型,本身对象指针无法修改,但是内部属性在修饰符允许的情况下是可以修改的。 我们通过上面的报错信息验证一下刚才的观点。 在代码的第 15 行,test1.name2 = “t” 报错为 Cannot assign to property: ‘test1’ is a ‘let’ constant,代码的第 16 行,test1 = Person1() 报错为 Cannot assign to value: ‘test1’ is a ‘let’ constant。他们的报错原因完全相同,而 name2 本身是 var 属性。所以 15 行和 16 行的报错不是 struct 内部决定的,而是由 let test1 这个 let 属性决定的。 同样的,我们看到代码第 25 行,test3.name2 = “t” 没有报错,和第 15 行出现报错对比,可以确定同样的 let 类型的对象变量,在修饰 struct 和 class 的时候,他们的行为却是不一致的。 (我相信有不少朋友,这里会饶进入一个洞里,那就是 C 里面的 const 修饰的是 Int 基础类型,而这里 let 修饰的是 struct 对象,struct 可不是 Int 或者 String,struct 可是对象啊!他们是否有可比性?如果没有可比性,那么上面的结论是否还能成立?这里做说明:Int、Char 是啥?在内存里面就是一块存储区域。struct 呢?也是一块存储区域。对象是啥?是方便我们开发所汇总出来的一套编程思想,即面向对象编程。过了 IDE,解析器和编译器还会管你是 Int 基础类型或者对象类型?不会管的。在广东人面前,什么都是肉,即使你是福建人。退一万步来说,Swift 里面,Int、String 也是通过 struct 来实现的) 那我们在看看 var。test2 和 test4 两个 var 变量,虽然一个是 struct 对象,一个是 class 对象,但是他们的表现却是一致的。其实 var 就是非 let 修饰符。let 修饰符和 const 类似,那么 var 其实就是省略了 const 的修饰符。我们举下面例子 1 2 (1) const int age = 10; 1 2 (2) not const int age = 10; 1 2 (3) int age = 10; 上面例子中,not const 修饰符是我杜撰的。意思就是非 const。那么我们可以发现,not const 就是 var。 我们还可以发现,(2)和(3)其实是等价的。而(3)仅仅是省略了 const 修饰符,所以苹果一开始发布 Swift 语言的时候,开始完全可以指定下面的规则: Swift 文档官方指南: 在 Swift 中,let 表示不可变。如果什么都不写,那么默认可变。如: const age = 10 和 age = 10 编译器会根据语意自动识别 age 为 Int 类型,所以您无需显示添加 Int,如:const age: Int = 10 及 age: Int = 10。 苹果没有这么做,或许是 var 和 let 一起写出来的代码,一来易于识别,二来看起来又有美感。而且天下语言基础语法一大抄,JS 也是同时有 let 和 var。而 Dart 就是有 var 和 const。 0x03 struct 和 class 的区别到底是什么? 网上已经能够看到很多两者的区别,如栈,堆,继承等。 这里,我想从属性约束上说一点,这点网上说的不多。 对于 class,我们都是非常熟悉的了,毕竟面向对象编程这么久,也无需多言。属性是 var,那么就是完全可变的。如果是 let,那么回到上一个论题。 对于 struct,我们看下面的代码截图: 我们发现,在 struct 里面,不管属性是 let 还是 var,方法(内部函数)都是无法直接修改该属性的。如代码行 10 和 13 行所示。 如果我们需要修改内部属性,需要在方法前面加上 mutating 关键字。这在 class 里面是不存在的,说明 struct 相比 class,属性约束要强一些。 当然,我们看代码 16 行,对于 let 属性,mutating 也无能为力,这个和 class 倒是一样的。毕竟 let 本身就是完全不可变的。 所以,我们得出一个结论,struct 相比 class 来说,在属性约束上,是要强一些的。 0x04 一个蹊跷点 这里给大家看一个蹊跷的地方,如下图所示: 上面的代码,编译和运行都不会有问题。 我们来分析一下问题点: 对于 Person1 内部来说,var 修饰的 name2 属性,是不能随便改变的。如果要改变,必须要通过 func 前面添加 mutating 来实现。本质上这是一种内部约束。 对于 var 修饰的 test2 对象来说,可以直接通过变量修改 name2 属性。就是说,可以越过 struct 内部约束直接修改变量值。 这里的蹊跷点就在于,外部约束大于内部约束了。我感觉不可思议,但这的确是一个事实。 0x05 总结 Swift 里的 let,就是 C 里的 const。但是在 struct 和 class 中表现是不一致的。struct 类型的 let 变量就是 “(4)int temp = 24;const int * const age4 = &temp;” 类型,什么都修改不了。而 class 类型的 let 变量,就是 “(3)int temp = 23;int * const age3 = &temp;” 类型,本身对象指针无法修改,但是内部属性在修饰符允许的情况下是可以修改的。 struct 相比 class,内部属性约束会更强一些。 下面给出我这边完整测试代码(可通过代码右上方复制按钮直接复制): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import Foundation struct Person1 { let name1: String = "name1" var name2: String = "name2" let arr1: [Int] = [] var arr2: [Int] = [] func change1() { name1 = "test" } func change2() { name2 = "test" } mutating func change3() { name1 = "test" } mutating func change4() { name2 = "test" } } class Person2 { let name1: String = "name1" var name2: String = "name2" let arr1: [Int] = [] var arr2: [Int] = [] func change1() { name1 = "test" } func change2() { name2 = "test" } mutating func change3() { name1 = "test" } mutating func change4() { name2 = "test" } } let test1 = Person1() test1.name1 = "t" test1.name2 = "t" test1.arr1.append(1) test1.arr2.append(1) test1 = Person1() var test2 = Person1() test2.name1 = "t" test2.name2 = "t" test2.arr1.append(1) test2.arr2.append(1) test2 = Person1() let test3 = Person2() test3.name1 = "t" test3.name2 = "t" test3.arr1.append(1) test3.arr2.append(1) test3 = Person2() var test4 = Person2() test4.name1 = "t" test4.name2 = "t" test4.arr1.append(1) test4.arr2.append(1) test4 = Person2() 大千世界,真的无奇不有。很多自然科学无法解答的问题,都是奇闻逸事。 如果有人说自己观察到尸体动了一下,但是本该绝对不可能动。这怎么用科学来解释?说这个人撒谎?但是这是一个正直刚正的人,又绝对不可能撒谎! 我老家的老人们说自己看到了已经离开了很久的村里人在路上走路,这又该如何解释?他们可能撒谎了,也可能没有撒谎。 我们可以用压力过大、悲伤过度、思念强烈等客观原因来分析,但也没有科学依据来证实这些客观原因能够导致上面的现象。 世界本就是无奇不有,对我们有恶意的,我们不要贸然进入。对我们没有恶意的,我们也无需害怕和担忧。 海底的生物有多奇特,人类不知道。地下深处结构如何,人类也未可知。 很多人认为宇宙飞船都上天了,在这么发达的社会,无法用科学来解释的问题,都是恐怖的。 但是科学才触及到真理的哪个边界啊!或许科学连真理的边缘还没有触及到呢。 当前人类最应该害怕的,难道不应该是人类本身么?

2019/12/5
articleCard.readMore

网聚人的力量 - 网易

网易这家公司挺邪乎的(自然科学解释不了),相比来说 x 浪 x 度也只能叫奇葩。 他家产品基本都是 “用不久但长活” 系列。 产品有猪肉、邮箱、游戏、新闻、音乐、课堂、阅读、购物等。 挑几个重点来说,邮箱当年我可是力挺的,同事朋友要找工作,我那给的第一个建议就是:” 不要用 QQ 邮箱,一定要用 126 邮箱。“但是几年前,我已经转到 Gmail,头都没有回一下。 还有新闻,那些年热血沸腾,新闻可是每天都刷,觉得可以了解社会。后来明白,永远不可能通过新闻了解社会,也就不用任何新闻产品了。英语又不好,新闻产品的阅读就很有限了。索性都不看了。后来想想网易的新闻,不管从新闻来源,还是观点的不中立,还是一水的评论,都挺有意思的,或许说好笑,更贴切一点。 还有那个音乐,我是他们老用户了,出来没多久,我就一直在用。后来也不知道哪一天,突然就不用了。为什么呢?它的开屏广告实在是太妖了。我接入互联网的时候,互联网已经盛行很久,但是我心里总是感觉,云音乐才是开屏广告的鼻祖,心里阴影还是很大的。 至于游戏,怎么说呢,网易旗下或者代理的游戏,基本上都是,下载一小时不到肯定被卸。或许不怪网易,仅仅是我没兴趣吧,比较网易还是有很大一批游戏用户的。 他家的 “酒友” 应用四年不更新但也不下架。 作为门户网站的鼻祖和中文互联网长河的见证者,网易,显然疲惫了。 网易给社会的贡献毋庸置疑,可以说非常大。比较风风火火这么些年,带动的技术进步、经济发展,都是非常巨大的。 但恰巧,或许因为和阿里门对门,风头被盖了太多,很多人对于网易的认知,并不多,就比如我,仅仅是门外汉般的了解。 但我每次上班都从网易楼下路过,有三年了。我心里感觉不是太好,那几排树有些妖,把网易大楼围在里面,花枝招展的。 所以你看,没啥过错,就是有,也是轻轻点点的。但就不是很喜欢,挺邪乎的。 直到,直到 2019.11.23,持续了数月之久的内部员工重病被暴力裁员,暴雷了 ==> 链接三连 https://mp.weixin.qq.com/s/FW7uR5t6UMMxgkCcAvk-MA https://mp.weixin.qq.com/s/pxJVSf7e6IdMByomPuXWdA https://mp.weixin.qq.com/s/dw0eM1tRVVdaJfiAlfC9Sw 口诛笔伐一大片,更多人碍于朋友圈里的领导和老板,仅仅点了赞,来表示,自己已经知道这件事并希望继续关注这件事。 百足之虫死而不僵,瘦死的骆驼也比马大。网易,2019 年,裁员裁了一年了,望好。 请一定要买保险,对所有朋友的劝告。如果可以,父亲、母亲、丈母娘、老丈人的保险,都买齐。 还有,做人,不管因为什么压力,底线一定要守住。比如网易的高管和 HR,他们在很多很多场景下,肯定都是好人,但是在一些莫名的压力下,就没人性了。 2019.11.30 更 从月光博客那里看到,华为 13 年老员工因为公司恶意诬陷被关押 251 天,真是大瓜,毕竟国民品牌,相关资料也删除的很快。 传华为 13 年老员工离职索要赔偿被关押 251 天 新浪财经 <已被删> 悄无声息的夜,谁来补偿该同胞正值壮年的无法奋斗。 2019.12.01 更 总部在安徽合肥的呆萝卜在杭州的研发总部也爆出大瓜,瓜还真不小,可惜因为总部合肥,裁员杭州,地理原因,没有过多媒体参与进来。 年底 300 人被裁 生鲜电商 “呆萝卜” 出事了 不知道有多少人,开开心心的要到了号,或者买房正在审核期,偏偏因为恶意断缴的公积金,泡汤了。还有那很可能拿不回来的两个月薪资。 我老婆曾在合肥工作做,老板 5 个月薪资没有发。她们几个人区劳动局投诉,劳动局说他们无法强制老板发送工资,但可以口头警告其老板。 4 年了,我老婆当年的 5 个月薪资,现在还没有拿到:)

2019/11/25
articleCard.readMore

绿皮火车

因为要参加亲戚婚礼,我在杭州前往马鞍山的绿皮火车上。许久没有坐过绿皮火车了,入站前我还有了丝丝害怕。害怕火车站是否有危险。 普通火车站只能是普通打工者最廉价的出行方式了。一路上的感慨。即使是杭州,这样的一线城市,杭州火车站也显的太过落寞。路上经过一个小巷,有浓浓的尿骚味。火车站也落魄的很,明显已经完全不在受到重视。 这样说,近几年国家经济发展迅速,尤其杭州为代表的一线城市,不仅在经济还是科技都属于一流水平。但是火车站,还是和几年前见到的一样,没有丝毫的改变,至少我没有看到。 我通过水泥地的走道进入了候车室,就像 80 年代一样的怀旧感。在候车室有两个人尬聊,一人说到,自己是个做菜的,算不上厨师。在杭州也就是混口饭吃,因为没有立足点,感觉到与杭州人群的格格不入。说到自己一个月 3000-4000 左右。 绿皮车厢里面呢,有不少抽烟者和随地而睡的人。 车厢很闷。根据我坐绿皮车的经验,车厢里面不是有点冷就是有点热。这经验也是我到车上后回忆起来的。我记性很差,努力回忆一下,之前的经验就如同刚才所说。这一次是有点热。 车厢的气味都没有变化,还是那种幽幽的,不想闻但一定会吸入的怀旧气息。 我是有些不习惯了,因为这些年一直都是做高铁,的确舒服很多。 工作生活中也遇不到绿皮火车和车上的人,隔阂的确越来越远,以至于像两个平行的时间线。 终究离发达国家还是有不少差距,大家都还是生活在困苦之中。 勇敢面对。

2019/11/1
articleCard.readMore

喝酒十之八九会误事

没事不要乱喝酒。没事不要多喝酒。喝酒前一定明确自己当前的处境。我多年喝酒善意的总结。 酒精对人的刺激,非常明显。少量酒精会像兴奋剂一样使人高亢。少量酒精还真不会有什么出格的事情发生,喝点就喝点嘛,谁都管不着,还真的不用怎么担心。但是一定要注意,超出预期的酒精也是从少量酒精过度过去的。 过量酒精对人刺激超大,甚至酒壮怂人胆。为什么一个很怂的人,会变得不怂了?要知道,怂这个东西可是骨子里的东西,就像有人天生害怕蛇一样,不能看到甚至听到蛇,连照片都不行。所以怂变得不怂,很蹊跷,哪有一蹴而就的东西。 过量酒精立刻使得人思维紊乱。人思维都紊乱了,那还得了?后面会出现啥事啊?我也不知道,谁都不知道。因为我不知道还有什么事情是可以脱离思维来完成的,如果要说,那可能就是敲打膝盖的神经反射吧。 怂人为什么不怂了?酒精让他思维紊乱了呗。以前做一件事情会想到八十件后果,现在只能想到十件后果,发现这十件后果都是自己能承受的,那就干吧。 为什么少量酒精也能壮胆啊?只喝了一口酒,还可能是假喝的,但胆子就变大了。哪有无缘无故的爱啊,只是你很坚强,你强迫自己迈出了人生中不可或缺的那一步。为坚强的你比心:) 严重过量酒精会让人产生幻觉。最明显对幻觉就是:我… 我还没醉,我… 我没喝酒,我… 我来开车。 这个时候,醉酒的人就是在裸奔了。醉酒人有美好的幻觉,旁人就只要搬个小板凳,一起欣赏美景了。无聊的生活中,难得有一些莫名的惊喜。能不能吃到甜瓜,那还得看幻觉是不是美妙的。 喝酒还有个奇幻的过度,那就是: 喝少量酒的时候,毛事没有,吹吹牛反正自己也不信。 吹着吹着,就不在线了,马上就喝多。然后吹的自己也信了。 再然后呢?严重过量了,开始用行动来实现自己吹的内容。 之所以说这个过度很奇幻,因为就像温水煮青蛙一样,你用最高清最贵的机子全程录像,回头来找三个阶段的分割时间点,硬是找不到。 一个东西就在那里,但是怎么都找不到。我们就要对它心生恐惧才好。因为那是未知的领域,不是我们擅长的,一定要谨慎,不要一不小心落水了。 还有一个小插曲,和你聊天的人,如果说自己没有喝醉过。你就不要信就好。这里不带其他分析,你就是不要相信 “他没有喝醉过” 这么一个事就好。 我相信没说这话的人可能真没醉过,但是说过这话的人,肯定醉过。 为什么啊?因为一个没有醉过的人,是不知道醉的感觉的。一个人说自己没有醉过,从心里上来分析,显然已经知道醉的感觉是什么样的。 就像一个人聊天中,冒出来一句 “小王啊,说实话,XXXX”,那么 “XXXX” 的内容很可能都不是实话。为啥啊?“说实话” 这三个字一出,从心里上分析,就是开始自我安慰了。安慰啥啊?安慰自己相信 “XXXX” 的内容呗。毕竟人都有共性,想要别人相信,首先自己要打磨好,最好让自己也相信了。 最后有两个最善意的话语送给各位,一定记牢: 喝车不开酒,开酒不喝车。 思维下线后出门 = 裸奔小丑 2019 年 10 月 5 日凌晨,HK《禁止蒙面规例》通过紧急法全面生效。紧急法全名《紧急情况规例条例》,该法赋予香港特首权利,在情况紧急之下,可绕过立法会,订立紧急法律。 HK 部分人士在马鞍山商场建立临时政府,并发布七条宣言。以示反对。 我想起了一句话:当一个人变成了坏人形象时,他做的每一件事,都是坏事。再往后,每一件坏事,都会被认为是该人做的。 不管戏里戏外,比如和珅之于王刚,比如容嬷嬷之于李明启。有故事说有商人不卖东西给王刚和李明启,说他们是贪官是坏人,哈哈。 那到底谁是坏人?是不是真的是坏人?这特么恐怕是要和 “皇帝的新衣” 一样了。皇帝到底有没有穿衣服?真是个迷。

2019/10/5
articleCard.readMore

HKong

HK,我没有去过,但是我觉得这个地方很神奇。 为什么呢? 1. 美国棱镜门在 2013 年 6 月被曝光,美国政府遭遇百年一遇的全球信任危机,曝光人斯诺登选择在 HK 将相关资料公众于世。 2. 朝鲜目前最高领导人金正恩的弟弟金正男,于 2017 年 2 月被刺杀。之前呆过比较久的地方,一个是前苏联,一个是 HK。 3. HK 的香港大学世界闻名,比肩国内的清华与北大。而 HK 世界闻名的大学达到数十个。 4. 中国公司上市地点有三个,HK、深圳、上海。HK 这么小的面积却拥有大量的公司前往,中国众多巨头纷纷前往。 5. HK 首富李嘉诚的身价,在国内也没有几个人能够比得上。 还有很多其他的经得起推敲的新闻点,很多新闻都表明,HK 是一个在政治、经历、教育等方面在全球都非常有影响力。前些年经常可以在大陆媒体上看到 HK 的发展和一国两制的自豪。这两年逐渐听到的少了,也或许和我不再看新闻有关。不过在代购上,经常看到 HK 的字眼。 HK 近期成为全球人的聚焦点,有人骂有人赞更多是在观看。时间不会停下来,什么事情一定会有一个结局,或许几多年后,我们可以叫这次事件为 “反送中案件”。 事件持续了很长时间了,但也不会更长了。因为过不了几天,9 月 1 号就是开学日了,10 月 1 号更是中国 70 周年大国庆,甚至大陆装甲车已经在深圳集合了。事件从开始的游行示威,到最后暴力游行,再到最后甚至都不算暴力游行,因为活动是没有到警局备案的(未备案原因未知,可能没有去备案,也可能没通过,也可能其他)。 我非常关心 HK 事件,更多的因果其实并不是我们这些小民所能认知的,我关心的是流血,因为是同胞。每一个中国人都应该理解,流血和战争,是一脉相连的,谁都不希望火炮再一次在中国燃烧。 近期,国内的很多舆论与媒体与个人言论,已经在恐怖分子、武力清场这些极端言论之间徘徊很久了。这真的不好,刚看到听到这些,我心里很震惊。有多少人对 HK 理解多少?对事情的原委有多大认知?很多都是没有,总感觉跟着大众走,那肯定不会错。其实对于这些敏感话题,你什么都不做,有些时候也更安全。何必要放出侮辱同胞的言论呢?何况有些侮辱是经不起推敲。 当然,我对事件原委了解也不多,也不多言,但是有两个结果却是所有人都能想到的。 1. 游行诉求中要求 HK 最高领导人下台,是不可能的。 2. 暴力事件中的港独,也是不可能的。 而其他诉求是否可以达成,鉴于目前已经达到装甲车集合的情行,其他诉求可能性不大的。 所以这次的 HK 动乱,如果开始时候能够停止,是最好的。而现在,HK 人在大陆这边是不会有他们自以为高高在上的感觉了,以后或许也不会有了,更严重的,香港的经济和体系,都会收到重创乃至倒退。而对于大陆,在外面也有了负面的声音,对台湾省和周边小国都会产生压力。 香港是一个很神奇的地方,至少和大陆有很大的区别,这是毋庸置疑的。因为不同,所以更需要思考,很多问题不是纸面上的那么简单,一个普通人就能够把香港了解清楚?不可能的。 很多香港商人平民和国外知名人士均发表言论或者默默支持游行,但是他们自己又没有在街头游行。他们的支持对最终不可控的结局是潜移默化的,因为很多人都会向他们看齐。 发表言论没啥错,言论是观点。但是我们一定要学会思考,思维上不要被牵着走,脑回路才会顺畅,远离巨婴。 而大陆把同胞的暴力游行比作恐怖主义,为后续武力提供舆论依据,也影响了舆论。那可是同胞啊。 总的来说,在战火中追寻自由和民主,虽然不比那岁月静好的普通生活,但也值得尊敬吧,骨子里的气简称骨气。 希望这次香港暴乱能够停下来,也希望相互之间的诉求能够达成和解,更希望同胞们不会出现流血。 媒体, 以不持立场而神圣, 以不下定义得自由。 如果媒体不能保持新闻的中立性,那一定会变成利益的工具。

2019/8/15
articleCard.readMore

胡说八道之云服务

杭州的云服务建设的非常快。 下图中白云的后面就有非常强大的软硬件储备,撑起了几乎整个中国互联网的基础建设。 与微软亚马逊等科技公司不同,国外巨头一般把服务器存放在大海里,方便散热降低能耗。 而在杭州,大家使用的云服务就都存储在图片中的那些云后面。 那里因为高度和湿度原因,温度极低,甚至在那里运行着自主研发的传说中的 10GHZ 的高密度 CPU 芯片,因为降温迅速,晶体管的散热在大自然面前显得太无力。 在下图中,大家能看到有一架飞机飘过,它刚刚将大量的硬盘运送到了云上。

2019/8/5
articleCard.readMore

互联网网络传输协议理解坑点

DNS 协议的用途和解析方式是什么样的? 所有应用层层面的数据,如果需要进行网络传输,必定需要 ip 地址这个核心参数。没有 ip 地址的包,是不可能发出去的,都过不了网卡。因为网卡不知道要把这个包传到哪里去。 而 ip 地址人类很难记忆,所以很多服务都是通过域名进行访问。域名是比较方便记忆,但是网络又不认,因为网络无法根据域名进行数据传输,即域名没有定位功能。 所以所有的应用层的域名访问,都需要 DNS 协议解析成 ip 地址后才能够封包发送,ip 有定位功能。如果一个应用层服务直接通过 ip 地址访问另一个服务,那么是完全不用 DNS 做一次 ip 地址解析的。 我们的域名是需要从特定的服务商进行购买,在服务商处会进行 ip 地址的配置。而 DNS 协议的根据就是层层服务商解析,问根域,返回顶级域,问顶级域,返回权威域,这样通过.com、.cn 等不同服务商的判断,最终落实并返回一个确定的 ip 地址。 DNS 协议是系统层级的操作,应用层无需关心,系统通过 DNS 协议拿到 ip 地址后会给到应用层,然后应用层将 ip 地址传递到传输层进行网络数据封包。 为了效率问题,系统会进行 DNS 的缓存以加快解析速度,并且还会设置有效期以较少误差。 比较例外的是 hosts 文件。在这里配置的信息具有第一优先级,系统如果在 hosts 中找到了对应的域名和 ip 地址的映射,则不再进行 DNS 请求而直接返回映射数据。 DNS 会有劫持问题,在移动端会使用 HTTPDNS 技术。DNS 有一个非常重要的大杀器,就是做负载均衡。 ping 和 ICMP 是怎么样的关系? ICMP 是网络层协议,ping 是对 ICMP 的应用层实现。 ICMP 和 IP 协议属于同一层。ICMP 工作是判断网络环境,通过返回包的时间、状态等确定网络复杂度。ping 使用了 ICMP 的 “查询报文” 功能,根据返回包的时间和状态等,判断当前主机是否可以访问及延时等信息。 ICMP 还有一个功能是 “差错报文”,Traceroute 是对该功能的应用。 ping 出去的包,是不过传输层的,即直接过滤 TCP 和 UDP,到达网络层封包然后通过下层协议及网卡将数据发送出去。 之所以不过传输层,是因为过它们没有实际用途,他们还会影响到 IMCP 的核心功能即网络判断。如:如果过 TCP,那么有三次握手等,本身判断网络包状态和三次握手没有关系,TCP 的稳定传输会导致所有包都能有顺序的抵达终点,完全背离了 ICMP 的初衷。应答方,在 IMCP 层就会返回应答数据,也不会在上报到传输层。所以传输层全程都不干涉到。 DHCP 用途是什么?是怎么实现的? DHCP 即动态主机配置协议,就是用来动态配置 IP 的。要想能够联网,不管局域网还是外网,都需要 IP。 而电脑或者手机进行网络配置的时候,不仅仅需要配置 IP,还需要配置子网掩码和网关。有一个不对,网络就白配了。 为了方便,可以通过 DHCP 协议,动态的获取 IP、子网掩码、网关等重要信息。 当我们连接到一个新的 WIFI 后,设备首先广播信息,将自己的 mac 地址及默认 ip 0.0.0.0 还有 UDP 端口发送出去,因为这个时候设备还没有 ip 地址,所以不知道发给谁,只能广播。 然后网络服务也会通过广播的方式将 ip 等信息广播出来,广播信息里面有设备的 mac 地址(牵涉到 mac 地址的用途问题了)。 设备拿到这个信息后,依旧通过 ip 0.0.0.0 回复给网络服务,说明该 ip 及相关配置信息没有问题,自己确认使用。 然后网络服务给予 ack 回执,说明确认信息欢迎加入该网络。 后面设备均使用网络服务分配的 ip 地址进行网络数据传输了。 DHCP 还会设置有效期,设备需要主动联系网络服务进行有效期的续签。比如 10 个小时的有效期,那么在有效期过去一半(50%),即 5 个小时的时候,设备会主动给网络服务发送续签的消息包,根据收到的 ack 回执里的信息更新本地配置,如新的有效期等。 一个数据包,没有端口,还能够通过网卡发出去吗? 端口是传输层所使用的,一个数据包没有端口,一样是可以发出去的。但是不能没有 ip 地址和 mac 地址。这两个缺少了,网卡那边就过不去。 TCP 的三次握手,是怎么进行的?和 Socket 有什么关系? TCP 三次消息通话的详细流程一定要清晰的,如 bind、connect 等等,并且知道客户端在第二次握手的时候已经确定连接并且可以发送数据(也叫抢跑)。TCP 的三次握手,根本目的是为了建立一个稳定的连接。这个连接并不是客户端和服务器之间有一条通道,而是客户端和服务器端建立稳定的套接字。每个连接,客户端都会有一个套接字对应这个连接,服务器端也是一样。因为服务器端一对多的关系,所以服务器端可以有成千上万甚至数十万个套接字(理论上限很高,因为 Socket 也是文件,文件打开需要耗费资源,所以实际上限远远低于理论上限)。每个套接字里面包含客户端 ip、客户端 port、服务器端 ip、服务器端 port 四个重要信息。只有一方套接字还在,一方就可以通过这个套接字给另一方发送消息,即使对方可能收不到消息了(对方关闭服务等原因导致对方已经不存在等)。所以 TCP 的三次握手,目的是为了建立一套稳定的连接,而该稳定连接的建立,是建立在 Socket 套接字的基础上。如果三次握手还没有成功,这个套接字就不是完备的套接字,即连接中状态。通过三次握手的套接字是连接状态,即 established 状态。 TCP 三次握手的时候,有没有网络层参与? 如果对网络分层模糊,很难理解该问题的意思,会感到模糊并思考该问题表达什么意思。TCP 三次握手,是传输层的问题。那么传输层在进行数据握手数据传输的时候,一定要进行网络包的传送和接收。而网络包的传输,一定需要经过网络层、链路层、物理层,没有这些层,只有传输层的端口,这个包是发不出去的。所以 TCP 三次握手还好,四次挥手也好,HTTPS 的 SSL 安全建立也好,一定都是需要有下层才能够发包的。即网络数据包可以有下层没有上层,但不能有上层没有下层。 TCP 和 UDP 的不同? 这真是一个可大可小的问题。但是如果简单的回答 TCP 比 UDP 多了稳定的连接状态,应该不会让人满意的。 下面我说出我对 TCP 和 UDP 的理解。 首先,我们一个 TCP 或者 UDP 的网络请求包,在经过传输层封包后,后面就全部都是一样的了。 一样经过网络层,经过链路层,通过网卡把包发送出去,到集线器进行以太网数据传输,通过交换机,再通过路由器,通过中继器加强信号,通过防火墙… 大家可以看到,TCP 和 UDP 仅仅是数据包传输过程中的一角,根本不是大的角色。而这两个两角色,对外面大的世界是产生不了多大影响的,或者说没有影响。TCP 的包,还是 UDP 的包,其他层都是不管的,对于其他层来说,都是包。 我们在乎的问题是数据传输,如果一个数据,可以 biu 的一下子就可以传递到另一方,那还需不需要有 TCP 呢?比如,我们一张图片,1M 大小,如果这 1M 大小,可以通过一个包传递到云存储去,那还需要 TCP 干嘛呢?毕竟 UDP 我们也可以根据是否有回执来判断这个图片是否发送成功了。如果回执一段时间没有回来,那我们就重发嘛。 而我们之所以需要 TCP,根本原因不还是这个 1M 的图片,不能一个包直接发送过去,因为包定义最大 1500 个字节,其中还有一些是头字段。因为一个数据需要拆分成很多个包发送,所以需要关注顺序和丢失问题,这才引入了 TCP 的概念。 UDP 做了什么事情?把数据封包后通过下层再次封包之后发出去。发成功了吗?不管,如果应用层要管请自行根据超时判断。发失败了会重发吗?不管,如果应用层要管的话请自行重发。 所以你发现了没有?如果我们把 1M 的图片二进制流,分割成 1000 字节大小,共计约 1000 个包,我们应用层主动处理这 1000 个包,一定可以成功的把包发到另一方。可是这样完美吗?不完美。应用层需要大量的处理逻辑,难度极大,而且性能不佳,比如当前网络真的比较卡,应用层很多次重发都将是无效的。比如服务器网卡缓冲区因为应用层没有及时消化数据,已经满了,这个时候发再多数据根本就是失败的,服务器的网卡缓冲区装不下更多数据了啊。 So,这个时候才是伟大的 TCP 出场的时候。TCP 出场,一口气做了这五件大事:通道连接、丢包控制、流量控制、顺序控制、拥堵控制。大家对照上面的说明可以理解,TCP 是啥?不就是另一个对 UDP 补充的升级版协议嘛。TCP 把 UDP 不足的地方,通过另一个协议,全部解决了。 本身,我们需要应用层为了 UDP 的不足做全量维护,而 TCP,通过协议层面,直接帮我们做了这些事情。我们再也不用管数据发送成功不成功的问题了,因为 TCP 的重发可以帮我们处理的很好。 So,TCP 和 UDP 有啥不同? 他们只是在数据传输链里面很小的一环,他们影响不了传输过程。 UDP 的不足就是 TCP 的补充。他们之间没有关系,是两套协议,但是,TCP 却完美补充了 UDP 的先天不足。 TCP 是万能的而 UDP 一文不值?瞎说!很多场景,根本不需要 TCP 的高稳定性,因为高稳定性带来的代价很大。而 UDP 轻量,性能很高。比如流媒体直播场景,丢一桢没啥所谓,但是因为丢了一桢而不断重发堵塞剩下的桢和网络通道,罪过可就大了。 高稳定性用 TCP,高性能用 UDP,善长不同。 但是最终我们一定要能够理解,产生 TCP 和 UDP 两个不同协议的根本原因,在于网络包大小只能小于 1500 字节。如果一个包能够有 1M 大小,那还要 TCP 这么复杂的处理流程干嘛呢?而 TCP 和 UDP 的表现不同,只是两个协议的维护方式不同。 传输层、网络层、Mac 层(链路层)各自填补了哪些重要信息? 传输层填补了端口号。网络层填补了 IP 地址。链路层填补了 Mac 地址。 为什么需要端口号?因为操作系统需要根据端口号,将网卡缓冲区里接收到的数据通知到应用层。 为什么需要 IP 地址?因为 IP 地址才有定位功能,有了 IP 地址,数据包才能够发到指定的一台公网计算机上。 为什么需要 Mac 地址?因为 IP 地址具有定位功能,能够定位到一台公网主机上。但是在定位过程中,不全是公网,过路由器的时候,因为 IP 地址个数限制,很多时候都是一个网段的内网,而内网就需要 Mac 地址来进行通信了。 这里需要特别注明一点,IP 地址和 Mac 地址,在数据传输过程中是必不可少的,但是端口号却不是必须的。毕竟端口牵涉到传输层,而不是所有的数据包都过传输层。比如 ping 使用的 ICMP。 HTTP 和 TCP 的关系是怎么样的?啥是 Socket 套接字? 你知道巴基斯坦和卡巴斯基是什么关系吗?没错,HTTP 和 TCP 没有啥关系啊。HTTP 是应用层协议,TCP 是传输层协议,一个是客户端直接操作用于数据封包解析展示的,一个用于网络数据传输的。 But,HTTP 使用了 TCP,因为 HTTP 的包在应用层封包后,紧接着就跑到网络层继续封包。但是 HTTP 直接操作的 TCP 吗?根本没有,HTTP 直接操作的是 Socket Api。啥是 Socket Api?首先 TCP 或者 UDP 是传输层协议,他们是不能直接供外部调用的,在他们之上,还有一个叫做套接字的东西。 套接字是啥?套接字,就是操作系统用来判断数据归属哪一个程序的判断标准,套接字中有三个参数:ip 地址、传输层协议、端口号。通过这三个参数的结合,就可以实现数据的并发传输。 Socket Api 是啥?为了将这个套接字提供给应用层使用,套接字开发了一套 Api,如 bind、connect 等。应用层通过这些 Api 就可以创建稳定的套接字。 所以 Socket 是 HTTP 和 TCP 直接的纽扣,也是应用层和传输层之间的纽扣。承上启下的作用。 HTTPS 和 HTTP 的不同,HTTPS 的实现原理是怎么样的(详细说明 SSL/TLS 协议的流程)? 在协议上,最直接的区别是:HTTP 是明文传输,HTTPS 是密文传输(这是在协议上,我们也可以自己做数据加密,但是牵涉到密钥泄漏问题)。 HTTP 和 HTTPS 相比,少了一个 S,S 代表 SSL/TLS 协议,在 OSI 七层模型里面属于会话层。正是因为这一层,导致 HTTPS 会 HTTP 复杂了不是一点点,但是也安全了不是一点点。 那 HTTP 和 HTTPS 相比,除了这个 SSL/TLS 之外,还有没有其他的不同?其实还真没有。两者都是应用层协议,下层数据封包都是一致的。如果真要说还有什么不同,我们就再加上端口号不同吧。HTTP 是 80 端口,HTTPS 是 443 端口。 那么重点来了,这个 SSL/TLS 就是两者差别的全部,分析完 SSL/TLS,也就理解了两者之间的根本不同。而 SSL/TLS 也是 HTTPS 协议的安全根本所在。 SSL/TLS 几乎就是为了安全而生的,它一生都在围绕安全做各种处理。怎么才叫安全?网络数据安全有四个决定性要素:身份认证、不可否认、完备性、保密性。后面我们来验证 SSL/TLS 是如何确定四要素的。 First of all,为什么叫 SSL/TLS 协议?SSL 是什么?TLS 是什么?其实是一个东西,SSL 从 V1 发展到 V2 然后发展到 V3 后,才正式成熟,在成熟后,SSL V3.1 被直接改名为 TLS 1.0。而后 TLS 继续开始发展,先后出了版本 1.1(2006 年)、1.2(2008 年)、1.3(2018 年)。目前使用最广泛的是 TLS 1.2,之前版本都被验证为不安全的,已经被弃用。而 1.3 比 1.2 更加安全,目前也在大范围推广。而 1.3 和 1.2 却又有很大不同(新特性)和关联,所以了解了 1.2 和 1.3,才能理解安全四要素。 安全四要素身份认证、不可否认、完备性、保密性,该如何保障?大家肯定都知道了,文字描述起来复杂不宜懂,但是我实在不想放图片。大家既然都知道了,那就直接入正题(公私钥这块复杂不容易理解的,一定要 Google 理解清楚)。 好了,那我们总结一些,RSA 等非对称加密方式,保障了保密性。签名保障了完备性。证书保障了身份认证和不可否认。似乎都齐了,我们需要有一套证书传递逻辑,然后将数据加密,就可以完美的将数据从一端传输到另一端。 这里我们还是要提一点,加密分对称加密和非对称加密。两者耗时不在一个数量级,其中非对称加密耗时非常厉害的。所以 TLS 使用了非对称加密的方式来传输并生成对称密钥,然后用对称密钥进行数据加密传输的。而 HTTPS 用了 TLS,所以 HTTPS 的非对称加密,只是在连接开始的时候用到了,拿到对称密钥后,就不再使用非对称加密了。 总体核心流程是什么样的呢?简单描述下: 1. C——TLS 协议支持列表、C-RandomNum、RSA 密码套件 ———->S 2. S——TLS 协议确认版本、S-RandomNum、RSA 密码套件 ———->C 3. S—— 服务器证书 ———->C 4. C 验证服务器证书的有效性 5. C 生成随机数 RandomNum-Secret 随机数 6. C—— 用服务器证书里的公钥加密 RandomNum-Secret 随机数 ———->S 7. 现在,服务器和客户端,均有三个随机数了,分别是:C-RandomNum、S-RandomNum、RandomNum-Secret。两端根据同样的一套运算,计算出对称密钥:Master-Secret 8. 对称密钥已经生成完成,后面通过对称密钥通信。所有数据都会通过对称密钥加密后,才能发到另一方。另一方也通过对称密钥解密后,拿到原数据。 大家可以看到,通过上面 8 个环节,就可以建立一套对称加密的安全通道。这里需要提醒一下,生成对称密钥,共需 3 个随机数,其中前两个是明文传输的,第三个才是加密传输的。因为第三个通过公钥加密,中间人无法破解。所以对称密钥是安全的。 那上面是不是就是 TLS 安全认证的全部了呢?很抱歉,上面的通道建立方式是大家比较熟知的,也认为一直在使用的,但并不是流行的。上面走的是 RSA 的公私钥认证逻辑,但是 RSA 在一些情况下也被认为不安全(具体就要说到 TLS 1.3 了)。而非对称加密还有一个更高效更安全的方案,那就是 ECC(ECDSA、ECDHE)方案。而 ECC 才是使用的最为广泛的,甚至 RSA 已经在 TLS1.3 里面被废弃了。 所以上面流程我们先忘记,来个全新却几乎一样的新流程: 1. C——TLS 协议支持列表、C-RandomNum、ECDHE 密码套件 ———->S 2. S——TLS 协议确认版本、S-RandomNum、ECDHE 密码套件 ———->C 3. S—— 服务器证书 ———->C 4. S—— 用服务器私钥加密 ECDHE Param1———->C 5. C 验证服务器证书的有效性 6. C—— 用服务器证书里的公钥加密 ECDHE Param2———->S 7. C 根据 Param1 和 Param2 作为 ECDHE 入参,生成 Param-Secret 8. S 根据 Param1 和 Param2 作为 ECDHE 入参,生成 Param-Secret 9. C 根据 C-RandomNum 和 S-RandomNum 和 Param-Secret,生成 Master-Secret 10. S 根据 C-RandomNum 和 S-RandomNum 和 Param-Secret,生成 Master-Secret 11. 现在,服务器和客户端,已经计算出对称密钥:Master-Secret。对称密钥已经生成完成,后面通过对称密钥通信。所有数据都会通过对称密钥加密后,才能发到另一方。另一方也通过对称密钥解密后,拿到原数据。 上面才是目前市场上最普遍使用的 TLS 安全流程。因为使用的是 ECC,而 ECC 需要两个参数计算最后值,所以使用 ECC 密码套件的流程比 RSA 密码套件的流程,多了 ECDHE Param1 和 ECDHE Param2。 还有一个巧妙的,在 RSA 中,第三个参数一定要一方发给另一方,最后才能发送第一条加密数据用作验证。而 ECC 方案里面,在一方还没有确认通信的时候,另一方就可以发送第一条加密数据用作验证了。和 TCP 三次握手里面一样,在第二次握手完成,C 端就可以向 S 端发送数据包了。这个 “抢跑”,不失为 TLS ECC 密码套件和 TCP 三次握手的亮点。

2019/5/25
articleCard.readMore

CPU 的制作

对 CPU 的一点简要说明。 如果你是对 CPU 的制作有一些模糊,或者希望通过其他博文来验证你的想法,那么下面一些认知或许对你有一些帮助。 大到宇宙飞船,小到 PC、手机、冰箱,无一没有芯片的影子。 各位一定都对芯片有很多认知了,我们不需要多做说明,没有芯片,就没有新时代。从真空三极管到锗晶体管到硅晶体管,每一步都是一次跃迁。 集成电路的制作 晶圆">硅 => 晶圆 原材料首当其冲的是高纯度硅。通过把高纯度的硅融化,用一个引子伸入容器,不断的让硅附着生长在引子上面。我们可以想象明矾的制作过程。 引子不断的往上提,最后一个很重的圆柱形硅淀就形成了。 这个圆柱形直径有 10 厘米 +。 然后通过机器切割,从上往下,切割一个豁口或者一个边,这个豁口或者边,是为了客户进行晶圆制作的时候辨认方向用的。因为最终晶圆不是 100% 利用的,这个豁口或者边,是肯定不会用到的。 然后,对整个圆柱形硅切割成晶圆。每个晶圆对直径也还是 10 厘米 +,但是厚度只有 3 毫米左右。这样的一片片晶圆,就是后面集成电路的原材料了。 所以,晶圆其实是很大的一个圆盘,比普通人的一张脸,还是要大一些的。 晶圆光刻腐蚀 这个比较复杂,网上都有详细的说明。Intel 和 AMD 也有公开视频说到一系列的复杂过程。 我不懂,我不做更多说明。大家可以自行查看。网上都是图,还是很方便理解和查看的。 不过这里也是技术难度最大的一环,是技术的巅峰。光刻机,不是一个国家自主创新能够完成的。有时候还是应该想着合作共赢,而不是喊口号和战狼外交。 晶体管数量 很多人看了光刻腐蚀的图文甚至视频,认同 CPU 里面真的好复杂。为什么呢?很多人想,这样的操作,里面有好多好多个晶体管了吧。 其实,很多人都不敢放大自己的想象。那不是好多好多个,也不是几百万或者几千万甚至几亿个,而是几百亿个。 现在的技术,已经达到 300 亿个晶体管,在一个指甲盖大小的 CPU 里面了。 这些晶体管,就组合成了集成电路,不同数量的晶体管,组合成不同用途的寄存器等。 晶圆良率 请大家一定理解,一个晶圆,可不仅仅只能光刻腐蚀出来一个 Die,而是很多个 Die。不然这个芯片得多大啊。 我有个同事有次和我说,一个晶圆只能制作一个芯片,这个晶圆利用率为什么不能提高? 其实,我同事就没有理解晶圆的大小。一个比脸还大的盘子,怎么可能只能做一个指甲盖大小的芯片啊! 所以,一个芯片,可以做很多个 Die,每个 Die,经过测试包装后,就成了 CPU。 所以,这里有一个良率的问题。为什么会有良率?因为几百亿个晶体管里面,有一部分的晶体管是不能使用的,而不是全部能正常工作。 很少有晶圆,上面光刻腐蚀的几十个 Die 都是完好的。 良率有一个现象,越靠近中心的位置,Die 的良率越高。同时,越偏离中心,Die 的良率越低。(中心,即晶圆的中心) 那每个晶圆里面,肯定有一些 Die 里面的晶体管是损坏的。是不是这些 Die 就不能用了呢? 不是的。同一批次的 Die,那些晶体管损坏的产品,就相应的把这些损坏晶体管封死,变成低配的 CPU。 比如,同一批次的 I7 处理器,可能有些 Die 里面有两个 Core(核心)损坏了。那么在后面测试的时候,就封死这两个核,变成 I5 处理器。 商人都是无利不起早的,他们为什么要这么做?因为这样做一来商家可以赚更多钱,不然那么多 Die 不都是扔了嘛。二来降低了用户的价格,东西多一些价格总归会将一些的。 网上有人提到过,那么有没有一个晶圆只做一个 Die 的呢? 有的。只是这个成本太高的。因为良率的问题,这个 Die 得多么精密的仪器才能够保障其晶体管不受损坏。因为这个 Die 占据一整个晶圆,面积大了,坏点几率也相应增加了无数倍。几乎 100%。 哪些情况下会使用这样的超大 Die?宇宙飞船啊。高级东西当然需要高级的配件。 晶体管数量为什么目前保持 300 亿多一些,再也上不去了 晶体管数量越多,计算能力也就越强,电脑的处理速度也就越快。那晶体管还能不能提升到上千亿? 不是技术上上不去了,技术上一直在突破。 而是散热上不去了。晶体管需要通电才能工作,能量总归是守恒的。CPU 内部几百亿个晶体管,总归要散热的。 因为散热真的不能那么有效,晶体管数量再多下去,就不能工作了。晶体管的工作环境温度太高了。 散热有很多计算方式,比如降低电压等方案都可以有效的降低散热量。但是目前的确已经到瓶颈了,好多年没有前进了。都在往多核、超流水线等方向发力。 内存是不是也有很多晶体管? 不仅仅内存有,显卡啥的都有。 一个高速缓存芯片的一比特需要 6-8 个晶体管,而内存容量比较大,一个比特只需要 1 个晶体管,但是还需要外部元件来支持,比如需要一个电容,因为晶体管会持续放电,数据会丢失,需要电容来补电。 显卡里面有 GPU。GPU 和 CPU 本质上都是一样的,就是里面的运算处理单元因为工作模式不同,量级不同而已。 所以,他们里面都是有晶体管存在。 手机都比电脑还强劲了感觉,手机里面有 CPU 吗? 手机里面岂止有 CPU,手机里面有和 PC 配件一样的东西。什么 CPU、GPU、内存、磁盘、驱动等等,都是齐的。 手机就是缩小版的 PC。 手机里面的 CPU 呢,还不是单独存在的。手机厂商,会把内存、显卡、驱动、CPU 等等,全部封装在一个大的芯片里面,叫贴片 SOC。他们是非常集中的一体,不像 PC 上面想换内存条了,再买一个就好了。 一个人的思维,决定了一个人的高度。 一举一动,笑或者哭,走或者跳,都是通过大脑发出指令,身体才能够执行。 哪些主观行动,是不通过人的思维控制的呢?神经反射之类的我们除外,它不属于主观行动。 一个人如何学习,如何赚钱,如何理财,如何钱生钱命转命。都是他的思维驱动他的行动的。 所以,努力的提升思维吧! 很多人说的,努力比不上选择,其实努力很廉价的。人只要主观愿意,随时可以很努力。你努力了,更多的人可以比你更努力。努力决定的仅仅是下限! 思维的提升,才能有效的不让努力白费,才能更好的实现人生价值。 都是行人。

2019/4/11
articleCard.readMore

我心里一直有一个念想

我有一个梦想,因为这个梦想,我也产生过心里的孤独。 今天我索性大胆的把自己的念想写出来,也把自己孤独的过程表达出来。 如果屏幕前的你看完心里也会产生一些奇思妙想,那我们或许可能很好的沟通下去。如果有共鸣,如果旅途方向再一直,陌生人我也愿意为你背负行囊。 我这个人呢,代码写久了,很多时候开始怀疑人生了。 代码总是有规律可循的,可是人生总是计划赶不上变化,很多时候我找不到人生的意义。 有一段时间,我一直在听一首歌,“黑暗中最亮的星”。我这人记不住歌词,前两年我很喜欢听歌,也是网易云音乐的第一批用户。有一首歌网易云音乐年度总结里面说,我听了 1000 多遍,但是我连歌词都记不住,我听的更多的是旋律。很多歌,歌词改变,依旧会很好听。“黑暗中最亮的星” 也是一样,更多听的还是调子,但是里面有一段歌词,我念念叨叨很久,“每当我找不到存在的意义,每当我迷失在黑夜里,夜空中最亮的星,请指引我靠近你”。 人生苦短,绝对不是瞎说。时代总是年轻人的,这个大家都是心知肚明。就像你街上看到一个 50-70 岁的中老人,和 30-40 岁的年轻人中年人对比,你的内心更愿意交往哪一个?或者直接点你认为哪个更有能力?(人都是利益的个体,更倾向交往对自己利益价值更大的一方。) 年龄大了,能力再大,也不过是在消耗仅剩的几年可消耗时光。后面自然有杂七杂八的老年琐事满足生活上的空虚。 我很多时候在想,时间真的不多了,而我,能产生什么有价值的人生意义呢?对自己,对家人,对朋友,对更多的陌生人。 我这人吧,对于生活质量没有想象中的那么高,比如对于车,我没有很高的渴望度,甚至有点排斥。不过我很喜欢摩托车,骑起来带风的那种,但是城市里禁摩托车。我对衣服啊,吃喝啊,甚至房子啊,都没有太多的渴望。 但是我喜欢钱。这些年一直都喜欢。和有些朋友聊天,他们说钱财挣不完,说开心生活最重要,说要那么多钱干嘛,说够花就好。我相信你身边也有很多这样的朋友。我不认同这些朋友的观点,我也会提出自己的认知,但是也不刻意要求改变谁。我认为很多人,都大大小小的认识到了钱难挣这么一个事实,因为过程中的辛苦,都潜意识的把头埋在了沙子里。谁会不喜欢钱呢?就事论事客观说,没有人不喜欢钱吧,在这个货币通行的时代。很多人说自己渴望平静的生活,说很多著名的人在很多场合也这样公开的表达过。可是人家已经经历了人生的风风雨雨,经历了辉煌。经历不一样,千万不要乱套用。 我渴望有一定的资金,可以满足自己的生活开销,可以让我的家人有好的生活质量,可以让家人不因为普通的中大型疾病而感到绝望。 这就是我的初期念想。 直白来说,满足家人的生活开销,提高家人的生活质量,让家人有吃有住有养老,让家人不因为常见疾病而愁眉苦脸。这或许不是我个人的想法吧,这也是每个人的基本想法吧?但很多人在生命的旅途中,都达不到初步的财务自由,我也只是希望。 我希望有一天,我可以每月拿出 5W 元,以慈善的名义,和三三两两个大学生,去偏远地区说道,讲述人生的意义,让很多没有机会接触更大世界的人,更好的认识自己,认识这个世界,增强自己思维的高度。 这是我的终极念想。 我之所以有这个念想,因为从自己的实际情况分析,我觉得应该有很多很多非常优秀非常厉害的人,他们因为家庭地域等客观原因,没有办法成为本应该成为的样子。这些人,不管对于他个人,还是社会这么一个大的种族部落,都是可惜的。 我自己生活在安徽一个并不怎么偏僻的乡村。但是在 18 岁之前,我对这个世界知道的寥寥。在上大学后,我才知道城市里面有公交车这个东西,很方便城市居民的位置流动。上大学很久后,我才有机会做过另一个城市的地铁,地下原来还有这么神奇的东西。甚至后面的高铁等等。计算机是改变我人生的硬件和软件,我目前也依靠这玩意为生。我那个并不怎么偏僻的乡村,我却对世界认知寥寥。这里面有很多原因。但很多都不是主观原因。比如说:家庭条件不允许,父母见识面不宽,家庭教育方向有偏离等等,但是对于一个还没有形成自主意识形态的未成年人来说,他们在很长一段时间并不能决定自己的命运。那成年后呢?会改变吗?我感谢我的父母,让我有机会接触大学教育。每每想到这个,我就觉得我的父母在应试教育这条路上走了对的也是唯一的一条路。教育可能是低级阶层向上跨步的最快方式了,寒门再难出贵子,这已经是定律,但是每一代人或许都可以向上跨步 1-2 个阶层,再多就难了。 一个人生活幸福度的高低,可以说绝大部分取决与思维的高度。我不敢说满,因为有很多土豪依靠巨额家产的确有优质的幸福度,幸福不幸福,很大方面取决与个人的想法和态度。但是,阶层的高低,就是思维层度的高低,这个是毋庸置疑的。想法不一样,态度不一样,导致工作方式和生活方式也不一样,反过来又互相推进。所以很多时候,一个人的想法是片面的有局限的,就是因为人和人之间,阶层和阶层之间,思维的高度都不一样,看待问题的角度更加不一样。 我很多时候会产生人生价值意义的思考,我觉得这个终极念想,或许是我的出路。我希望更多本有能力改变自己和周围更多人的那些陌生朋友,希望他们能够认识这个世界,发散人生意义的思考,知道这个世界还有不同的玩法和活法。 不是我喜欢网易云音乐,这玩意也不怎么讨人喜欢。我卸载了很多次。但是人生很多时候总是在一些时间需要一些音乐假装一下情调,没有音乐完全没有问题,但是就像演唱会没有主持人一样,少点东西。听音乐总要有个音乐库吧,我前前后后选了很多个,最后也只有网易云音乐自己相对来说喜欢一些。 邓丽君、王菲、张学友、香港几大歌星、周杰伦等等,他们也都过了当前年代了,新歌很少出了。我自己私下评价,他们是音乐的开拓者和推广者,现在的音乐人,要么在摸鱼,要么没办法出道。社会总是对有理想的人是残酷的。 他们过后,再没能改变时代的音乐人了,但新歌倒是连绵不绝,有高有低。偶然有几个爆款出来,也没有后续了。

2019/3/28
articleCard.readMore

网络基础协议

互联网规则 互联网本质,就是数据在一定的协议基础上,在多台主机之间,进行数据的流动共享。 网络传输协议非常多,不是简简单单的 HTTP/HTTPS 协议,我们 App 看的直播就有 RTMP、私有 UDP 协议、DNS (+CDN 加速) 等等。 数据在网络上基于二进制包进行传输,传输规则基于 7 层网络协议,4/5 层网络协议便于理解 数据包在传输过程中几个关键不可缺少的字段:端口、MAC 地址、IP 地址。可以没有应用层的 HTTP 等协议,但是绝对不可能没有网络层、Mac 层和物理层。没有这三层,数据是不可能找到对应接收方的,甚至这个数据包都出不了你的电脑端口。可以没有应用层等,如 ping 一个主机使用的 ICMP 就是网络层协议,就没有应用层。 HTTP/HTTPS 规则 HTTP 协议是无状态的协议,所以需要 Session、Cookie HTTP 没有三次握手,握手的是 TCP。应用层只要通过 TCP 必定会有三次握手。握手并不是 C-S 之间有一条网络管道进行连接,而是两端各自维护相应的状态,当双方状态都处于 runing 的时候 (双方套接字处于完成状态,本质是 Socket 套接字,UDP 也适用该规则),代表双方连接建立 HTTPS 的公私钥认证,很多情况下只发生一次,公私钥认证的用途仅仅是为 C-S 之间的后续通讯建立对称密钥。后续的网络请求不出问题是不会重新公私钥认证的。因为服务器会在第一次公私钥认证的时候,生成 Session ID,该 Session ID 指向对称密钥并保存。客户端一般也会保存这个 Session ID 和对称密钥,后面客户端提交 Session ID 到服务器就可以建立起来安全通信。HTTP1.0 就可以支持 keep alive,多个网络请求可以复用建立的连接,这个时候更加不需要公私钥认证了。 HTTPS 的公私钥认证,生成的对称密钥是由 C 生成一个随机数、S 生成一个随机数、C 再生成一个随机数这三个数完成的。公私钥认证的开始,是没有加密的,因为客户端还没有拿到公钥。所以前两个随机数是可以抓包拿到的,但是第三个随机数是 C 通过公钥加密传输的,所以第三个随机数的安全传输才是整个安全机制的重点。(前两个随机数被串改了也没关系,因为 C 和 S 的随机数不一样了,生成的对称密钥也不一样,后面的数据传输加解密过程中,就无法完成校验了)有个重点是,为什么需要 3 个随机数?而不能直接传输上面的第三次随机数?因为随机数为了确保随机性,而随机性不能完全依靠一方来确定,因为很可能不随机。而 3 个随机数,已经可以很好的保障最后生成的对称密钥的随机性了。 网络分层 五层分 物理层 Mac 层(链路层) 网络层 传输层 应用层 四层分 网络接口层 网络层 传输层 应用层 七层分 物理层 Mac 层(链路层) 网络层 传输层 会话层 表达层 应用层 相关分层协议 物理层 网线、光纤、交叉线、集线器(HUB,就是现在电商网站上买的那些 USB 扩展口) Mac 层(链路层) 交换机、ARP <通过 IP 找主机 Mac 地址>、RARP < 通过主机 Mac 地址找 IP>、VLAN 网络层 路由器、ICMP<ping 使用的主协议>,IP 传输层 TCP、UDP、其他 会话层 SSL/TLS <部分> 表达层 SSL/TLS <部分> 应用层 HTTP、HTTPS、FTP、RTMP、DNS (HTTPDNS)、DHCP(自动获取 ip 地址协议) 大纲如下 逐步更新,更新时间未知 数据是怎么通过协议进行传输的 等待更新 等待更新 网络分层在数据传输过程中的具体体现 等待更新 等待更新 数据是怎么在协议的基础上保持安全的 等待更新 等待更新 我们如何访问到 Baidu.com 的 等待更新 等待更新 直播是如何贴近我们生活的 等待更新 等待更新 网络资源加速是如何实现的 等待更新 等待更新 移动端 HTTPS 网络请求的优化方案 等待更新 等待更新 结语

2019/3/1
articleCard.readMore

为什么 32 位操作系统只能用 4G 内存空间

字节与比特 比特是计算机存储的最小存储单元。我们认知到的数字 3,在计算机的存储里(硬盘或者内存)的结构是这样的:00000011,也就是我们理解的二进制。 所以这个数字 3 是由 8 位组成的。每位有 0 和 1 两种变化。 比特存储,是计算机的基石。我们在互联网上通行的一切,如图片、音视频、文字,甚至各位的博客、App、电子书等等,能想到的能通过互联网传输的一切,都是比特存储。 举个例子,我们看的一张图片,在磁盘上的存储,或许就是这样子:0101011101010101010111101011110101010101100***(省略100000000个)***1010100111010101。 1 字节 (byte)=8 比特 (bit) 我们刚才说到的数字 3,就是一个字节,在磁盘上就是 00000011。(为了便于理解,实际可能是 00000000 00000011,或者 00000000 00000000 00000000 00000011)。 1KB = 1024B(2 的 10 次方) 1MB = 1024KB(2 的 10 次方) 1GB = 1024MB(2 的 10 次方) 这些大小的计算都是定死的规则。规则很重要,有了规则才能合作。 内存条 实实在在的东西,有 4GB、8GB、16GB 等大小,长条形的带黑边的玩意,实体店网店都能买到,笔记本的小一些,台式机的大一些,有各种型号。手机都是用的贴片系统 SOC,内存条都集成到了主板上。 如果电脑开机的时候出现滴滴滴的声音,就把内存条拔出来檫檫,有很大概率能好,这是台式机时候的老梗。 内存条根本性决定了我们可用的最大内存。如果内存只有一个 G,不管 CPU 和操作系统怎么高配,也只有一个 G 的内存能用。 内存条是硬件存储设备。很容易能支持 8GB 的数据存储。8GB = 8G * 1024M * 1024K * 1024Byte,也就是 8589934592Byte (字节),8589934592 * 8 即 68719476736Bit(比特)。 CPU CPU 一定程度上影响了最大内存。 我们经常说的 32 位操作系统或者 64 位操作系统,其实说的就是 CPU 里面核心的寄存器是 32 位还是 64 位! 寄存器是由多个集成电路元件 IC 组合成的。CPU 上有数据总线的针脚,可以和内存条一一对应,这里就有数据总线、地址总线、控制总线三大总线了。 如果 CPU 的数据总线有 32 个针脚,并且和内存条上面的 32 个数据总线针脚一一对应连接,那么就可以一次进行 32 比特的数据传输,也就是一次 4 个字节。 数据在 CPU 和内存双向流动的帮手就是寄存器。所以寄存器也要有一次处理 32 比特数据的能力,所以这个时候寄存器也是 32 位的。 CPU 中的寄存器决定我们的操作系统是 32位还是 64位(甚至 8 位,16 位等)。这里的 32位和 64位,指的是寄存器的位数,而非内存地址的长度。寄存器位数越大,那么一次处理数据的能力越强。比如 32 位寄存器一次可以处理 4 字节数据,64 位寄存器一次可以处理 8 字节数据。 主板 - 地址总线 上面我们说的 CPU 一定程度上影响最大内存,但不是根本原因。 地址总线影响的是内存空间寻址范围。地址总线硬件决定了我们可用的最大内存。 在 CPU 和内存条上,有两个重要的线,一个是数据总线,一个是地址总线。因为 CPU 和内存条本质也不是互通的,他们都需要插在主板上面,所以主板是他们沟通的桥梁。 上面我们说到 8G 的内存条,有 68719476736 比特的数据,逆过来分析一下: 这么大的比特数据,在内存条是按照 8Bit 一组排列的。 每 8Bit 形成一个小房间。所以 8GB 的内存条里面,有 68719476736 / 8 个房间。 计算机操作系统可以通过定向寻址的方案,直接找到每个房间。形象来说,就是每个房间都有一个内存地址 , 系统通过内存地址就可以瞬间找到那个房间,房间里面有 8 个小人 (8Bit)。 内存地址是关键,就由地址总线来指向和控制。如果地址总线有 32 条,那么内存地址最小为:00000000 00000000 00000000 00000000,最大为:11111111 11111111 11111111 11111111。64 条就 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 和 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111。 对于 32 条地址总线来说,这最小和最大之间,有多少个呢?因为每个 Bit 有 0 和 1 两个选择,所以总共有 2 的 32 次方个。每一个都一一对应内存条里面的一个房间,所以最多可以对应 2*2*2*2(32个2相乘)=4294967296 个房间。 每个房间是 8Bit 即 1Byte (字节),也就是总房间大小为:4294967296Byte,也就是 4294967296Byte/1024K/1024M/1024G = 4GB。 对于 32 条地址总线,最大的内存地址也就是:11111111 11111111 11111111 11111111。也就是最多只能找到 2*2*2*2(32个2相乘)=4294967296 个房间。也就是最大内存查找范围只有 4294967296Byte/1024K/1024M/1024G = 4GB。 数据总线和地址总线的关联 上面说了数据总线和地址总线,我们知道了数据总线用来传输数据,地址总线用来定位地址。 数据总线和地址总线的数量是不固定的,比如 window 操作系统,32 位,那么数据总线是 32 条,而地址总线可能是 32 条,也可能是 36 条。 如果地址总线是 32 条,那么最大内存就是 4G,如果有 36 条,那么最大内存就是 64G 了。 如果 32 位数据总线,配 36 位地址总线,那么一次只能传输 32 位数据,数据对应的地址却是 36 位,寄存器这样看存不下来嘛,因为寄存器少了 4 位。这个时候可以通过位移动,将两个 32 位的地址变成 36 位。 如果 64 位数据总线,配 36 位地址总线,那么一次能传输 64 位数据,而数据对应的地址却只有 36 位。这样只要把高位补 0 就可以了,这也是为什么 64 位操作系统能够兼容 32 位软件的原因。虽然浪费了空间,但是能兼容。 64 位数据总线配 64 位地址总线?2 的 64 次数,实在太大了,就是 ipv6 的数量。所以根本配不了 64 条地址总线,就是配了,也没有这么大的内存可以叠加啊。所以一般都配 40-50 条地址总线就差不多了。 为什么 32 位操作系统只能用 4G 内存空间 当年 win7 系统,明明买了 8G 的内存条,到我到电脑里面一看,只有 4G 不到。其余到 4G 不见了。 其实不是不见了,它还在那里,只是地址总线只有 32 条,导致读取不了更多到内存空间了。 C 绝对是计算机语言的开荒基石。 熟练 C 的程序员,大脑里就有一片星辰大海。

2019/2/26
articleCard.readMore

努力和天赋

今天,在公司卫生间里面,看到了这个 “每天努力 0.01” 和 “每天懈怠 0.01” 一年后的差距,颇为感触。 不过我不是因为这两个差距的比较产生心里的鸡汤,而是引发了一些关于努力的思考。 什么算是努力?好人一定要好报吗? 首先,我产生了下面几点思考: 每天的 0.01,在 365 年后的差距计算里面,为什么是累乘?而不是累加或者其他计算方式? 比较两者的差距,是否应该用除法表示他们之间的倍数?这样对于比较差距是否合理? 什么才算是努力?努力必然和方式、时间、付出、天赋等因素相互参杂,而努力后的结果是否可以评判一个人的努力程度? 一个人在某方面的成功,和努力的关联有多大? 不要被鸡汤迷失了方向 努力,可以上升到一个民族的方向。中国民族努力奋斗这么多年,在世界上有声望,没有入侵和战乱,大大部分国民吃喝满足。这是几代中国人民共同努力的结果。 努力,也是每个人应该准守的人生基本原则。不努力的人才,苟活有何意义?很多人虚度时光,你也无需指责,他的人生里没有你,你何须指手画脚? 但是图中的努力,有些误导人。为什么 365 天的 0.01 努力,不是加法,而是乘法? 为了鸡汤而可以制造的假象就迷惑人了。1.01*365/0.99*365,也就是 1.02020202 这样子,远远没有 1480 这么大。 所以我想说,这个鸡汤太过分了,努力固然重要,但是绝没有天地距离这么夸张。努力和结果,有 “方式、时间、付出、天赋” 等条件约束,不是简单的两个字。 何为努力? 万事皆有方式,如学习方式、工作方式。如果你在努力的过程中脑子里昏昏的始终没有清晰过,那你的努力只能是虚度时光并假装自己在辛苦,你欺骗了别人,也欺骗了自己。 这样的努力是廉价可耻的,当然我们局外人没有权利指责,那或许是你刻意制造的生活呢? 努力,需要方法。找到一个适合自己的有效果的方法,就已经是走了大捷径,跑了快速道。而且这个方法可以便随你各个方面,时间上或许就是一生。 没有方法在努力的人,始终在埋冤,在怨天尤人,说自己累,不相信技巧。 有方法在努力的人,都说这个方法好,努力原来有捷径可走。 方法不是固定的,每个人都会有自己的方法,我们都要找到它。 比如:与人沟通会增加见识产生灵感获得额外消息,站在巨人的肩膀上会走的更快更高,寻找合适的资源工具可以节省大量的时间等。 努力一定有好的回报? 是的。努力一定有好的回报。 你的努力,一定会在潜移默化里改变你一些什么,最直接等影响或许就是你成功了,财富自由,家庭辛福。 但是努力,绝对不会让你登上人生巅峰,这个是一定要摆正好的心态问题。 人,努力到一个点后,就开始怀疑自己了。为什么我的努力没有达到我的预期?我耗费了自己这么多的精力,却没有达到我想要的,我死了算了。 这个时候,一定要摆正心态,是不是你的目标太高了,以至于你的努力弥补不了这个高度的空白,需要天赋来支撑了。 天赋这玩意就很奇妙了,不管你同意不同意这个观点,它就摆在那里,甚至是真真实实的摆在那里。 某个方向的天赋可以让你脑壳在这个方向上高速运转,得天独厚的优越感。 从小时候和你一起学习的同学,到长大后和你一起工作的同事,甚至很多你认识的创业老板们,我们大家都会见过或者听过有一些有天赋的朋友。 如果你把那些人的卓越,当作努力,那不是对自己付出的努力的唾弃和践踏吗?不要不尊重自己,那就是人家的天赋,可惜不在你身上而已。 摆正好心态,努力过好自己的一生,就是对自己的不辜负,对自己生命的尊重。 如果你是某个方向的天赋,请一定继续努力,用这个天赋,帮助更多需要帮助的人。 如果你没有那个天赋,请一定继续努力,在这个方向上超过更多人,首先让自己和家人幸福,有条件了一定帮助更多需要帮助的人。 努力 PK 天赋 某个方向上的天赋人才,没有想象的那么多,金字塔的顶端就那么大。 你自认为有天赋的那些同学或者朋友,或许仅仅是努力的方式比你好一些,努力的时间比你多一些。人家比你更努力。 不要低估了自己的努力,也不要高估了周围人的天赋。 天赋决定了你的上限, 努力决定了你的下限。 最近听到一个可怕的消息, 你的努力方式,也是由你的基因决定的。 从你出生的那刻起,95% 的命运已经确定了。 我不看鸡汤就是因为幸存者法则, 但是没想到命是这么的严酷。 加油努力吧,不然就是一个废人了!

2019/2/24
articleCard.readMore

洗牙

1 2 3 4 5 6 update 2023.09.21 近一年没有洗牙了,几天前再次去洗了一次。 现在我学的聪明了,不去医院洗了,去诊所,价格便宜很多。两个人 100 元左右。 这几年牙齿护理的比较好,牙结石比较少,也没有什么烟渍。出了一些血,问题不大。 老婆发现有 4 颗蛀牙,打算本周去补牙。 洗完牙后,很舒服的。值得推荐。 今天去社区一个牙科诊所洗牙了。 现在已经洗牙归来,本来不是什么天大的个人事情需要在博客里面说一遍。 但是我忍不住自己的喜悦,所以一定想要推荐没有洗牙习惯的朋友,一定要去一次。 这是脱胎换骨的体验。 我长这么大,一直排次医院。要不是老婆把洗牙列为 18 年的目标并且现在到了 19 年,我可能还要推迟去医院到时间。 我长这么大,第一次去洗牙。 因为有抽烟习惯,这满口腔的牙结石我都感到恶心。我也不知道老婆怎么忍受的了。 我不敢照镜子看口腔,因为会看到那恶心的牙结石。我一直把头埋在沙子里。 我和老婆。双人套餐,总共 218,加上周末每人加 20,共计 258 元人民币。我用了 30 分钟洗牙,老婆大约 20 分钟。全程无痛(有一点可忽略的牙龈痛)。 现在的我,回到了我没有抽烟时候的口腔感觉,那是近 10 年前的感觉了。 舌头转一圈,那种凹凸感,不在是顺滑的了。牙结石已经把我的压床填的满满的,因为严丝合缝,所以滑滑的。 朋友,如果你没有洗过牙,或者没有经常洗牙习惯,一定要去。这是付出和回报严重不成正比的投资。 为了健康,也为了你的家人。 前两天拿了一个电动超声波牙刷。 我朋友说,用了电动牙刷,就再也回不到之前的手动刷牙生活了。 我想试一下,这个神奇的生活质量提升工具。

2019/2/23
articleCard.readMore

RAC-Api-Summer

From Internet. 方便自己查阅使用,侵权删。 一、常见类 1、RACSiganl 信号类。 RACEmptySignal :空信号,用来实现 RACSignal 的 +empty 方法; RACReturnSignal :一元信号,用来实现 RACSignal 的 +return: 方法; RACDynamicSignal :动态信号,使用一个 block - 来实现订阅行为,我们在使用 RACSignal 的 +createSignal: 方法时创建的就是该类的实例; RACErrorSignal :错误信号,用来实现 RACSignal 的 +error: 方法; RACChannelTerminal :通道终端,代表 RACChannel 的一个终端,用来实现双向绑定。 2、RACSubscriber 订阅者 3、RACDisposable 用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。 RACSerialDisposable :作为 disposable 的容器使用,可以包含一个 disposable 对象,并且允许将这个 disposable 对象通过原子操作交换出来; RACKVOTrampoline :代表一次 KVO 观察,并且可以用来停止观察; RACCompoundDisposable :它可以包含多个 disposable 对象,并且支持手动添加和移除 disposable 对象 RACScopedDisposable :当它被 dealloc 的时候调用本身的 -dispose 方法。 4、RACSubject 信号提供者,自己可以充当信号,又能发送信号。 RACGroupedSignal :分组信号,用来实现 RACSignal 的分组功能; RACBehaviorSubject :重演最后值的信号,当被订阅时,会向订阅者发送它最后接收到的值; RACReplaySubject :重演信号,保存发送过的值,当被订阅时,会向订阅者重新发送这些值。 5、RACTuple 元组类,类似 NSArray, 用来包装值. 6、RACSequence RAC 中的集合类 7、RACCommand RAC 中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。 8、RACMulticastConnection 用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的 block,造成副作用,可以使用这个类处理。 9、RACScheduler RAC 中的队列,用 GCD 封装的。 RACImmediateScheduler :立即执行调度的任务,这是唯一一个支持同步执行的调度器; RACQueueScheduler :一个抽象的队列调度器,在一个 GCD 串行列队中异步调度所有任务; RACTargetQueueScheduler :继承自 RACQueueScheduler ,在一个以一个任意的 GCD 队列为 target 的串行队列中异步调度所有任务; RACSubscriptionScheduler :一个只用来调度订阅的调度器。 二、常见用法 rac_signalForSelector : 代替代理 rac_valuesAndChangesForKeyPath: KVO rac_signalForControlEvents: 监听事件 rac_addObserverForName 代替通知 rac_textSignal:监听文本框文字改变 rac_liftSelector:withSignalsFromArray:Signals: 当传入的 Signals (信号数组),每一个 signal 都至少 sendNext 过一次,就会去触发第一个 selector 参数的方法。 三、常见宏 RAC (TARGET, [KEYPATH, [NIL_VALUE]]):用于给某个对象的某个属性绑定 RACObserve (self, name) :监听某个对象的某个属性,返回的是信号。 @weakify (Obj) 和 @strongify (Obj) RACTuplePack :把数据包装成 RACTuple(元组类) RACTupleUnpack:把 RACTuple(元组类)解包成对应的数据 RACChannelTo 用于双向绑定的一个终端 四、常用操作方法 flattenMap map 用于把源信号内容映射成新的内容。 concat 组合 按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号 then 用于连接两个信号,当第一个信号完成,才会连接 then 返回的信号。 merge 把多个信号合并为一个信号,任何一个信号有新值的时候就会调用 zipWith 把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的 next 事件。 combineLatest: 将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的 signal 至少都有过一次 sendNext,才会触发合并的信号。 reduce 聚合:用于信号发出的内容是元组,把信号发出元组的值聚合成一个值 filter: 过滤信号,使用它可以获取满足条件的信号. ignore: 忽略完某些值的信号. distinctUntilChanged: 当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。 take: 从开始一共取 N 次的信号 takeLast: 取最后 N 次的信号,前提条件,订阅者必须调用完成,因为只有完成,就知道总共有多少信号. takeUntil:(RACSignal *): 获取信号直到某个信号执行完成 skip:(NSUInteger): 跳过几个信号,不接受。 switchToLatest: 用于 signalOfSignals(信号的信号),有时候信号也会发出信号,会在 signalOfSignals 中,获取 signalOfSignals 发送的最新信号。 doNext: 执行 Next 之前,会先执行这个 Block doCompleted: 执行 sendCompleted 之前,会先执行这个 Block timeout:超时,可以让一个信号在一定的时间后,自动报错。 interval 定时:每隔一段时间发出信号 delay 延迟发送 next。 retry 重试 :只要失败,就会重新执行创建信号中的 block, 直到成功. replay 重放:当一个信号被多次订阅,反复播放内容 throttle 节流:当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出。 五、UI - Category(常用汇总) 1、rac_prepareForReuseSignal: 需要复用时用 相关 UI: MKAnnotationView、UICollectionReusableView、UITableViewCell、UITableViewHeaderFooterView 2、rac_buttonClickedSignal:点击事件触发信号 相关 UI:UIActionSheet、UIAlertView 3、rac_command:button 类、刷新类相关命令替换 相关 UI:UIBarButtonItem、UIButton、UIRefreshControl 4、rac_signalForControlEvents: control event 触发 相关 UI:UIControl 5、rac_gestureSignal UIGestureRecognizer 事件处理信号 相关 UI:UIGestureRecognizer 6、rac_imageSelectedSignal 选择图片的信号 相关 UI:UIImagePickerController 7、rac_textSignal 相关 UI:UITextField、UITextView 8、可实现双向绑定的相关 API rac_channelForControlEvents: key: nilValue: 相关 UI:UIControl 类 rac_newDateChannelWithNilValue: 相关 UI:UIDatePicker rac_newSelectedSegmentIndexChannelWithNilValue: 相关 UI:UISegmentedControl rac_newValueChannelWithNilValue: 相关 UI:UISlider、UIStepper rac_newOnChannel 相关 UI:UISwitch rac_newTextChannel 相关 UI:UITextField 六、Foundation - Category (常用汇总) 1、NSArray rac_sequence 信号集合 2、NSData rac_readContentsOfURL: options: scheduler: 比 oc 多出线程设置 3、NSDictionary rac_sequence 不解释 rac_keySequence key 集合 rac_valueSequence value 集合 4、NSEnumerator rac_sequence 不解释 5、NSFileHandle rac_readInBackground 见名知意 6、NSIndexSet rac_sequence 不解释 7、NSInvocation rac_setArgument: atIndex: 设置参数 rac_argumentAtIndex 取某个参数 rac_returnValue 所关联方法的返回值 8、NSNotificationCenter rac_addObserverForName: object: 注册通知 9、NSObject rac_willDeallocSignal 对象销毁时发动的信号 rac_description debug 用 rac_observeKeyPath: options: observer: block: 监听某个事件 rac_liftSelector: withSignals: 全部信号都 next 在执行 rac_signalForSelector: 代替某个方法 rac_signalForSelector:(SEL) selector fromProtocol: 代替代理 10、NSOrderedSet rac_sequence 不解释 11、NSSet rac_sequence 不解释 12、NSString rac_keyPathComponents 获取一个路径所有的部分 rac_keyPathByDeletingLastKeyPathComponent 删除路径最后一部分 rac_keyPathByDeletingFirstKeyPathComponent 删除路径第一部分 rac_sequence 不解释 (character) rac_readContentsOfURL: usedEncoding: scheduler: 比之 OC 多线程调用 13、NSURLConnection rac_sendAsynchronousRequest 发起异步请求 14、NSUserDefaults rac_channelTerminalForKey 用于双向绑定,此乃一

2019/1/15
articleCard.readMore

iOS 内存原理

2023.02.18 更 引用计数是 iOS 内存管理的核心,strong 是对其直接应用,weak / 自动释放池 是对其间接应用。要理解 weak 和自动释放池,最有效的办法就是看 runtime 源码,理解 hashTable 和 hashMap 这两张数据结构表。其中 自动释放池 还和 runtime 有很大关系,这点需要串联下知识点。 没有经历过 MRC 年代,对 iOS 的内存管理的理解就不会那么顺畅。 MRC 年代的内存总是不好管理,所以 ARC 帮我们做了很多事情。ARC 做了很多事情让内存管理更加精准优秀外,也隐藏了很多内存管理的细节,也让这块知识点不容易啃食。 真正的内存管理,一定需要回到 MRC 下面去理解,根本思想是:谁创建、谁释放、谁引用、谁管理。 内存释放的唯一途径是:引用计数 = 0 其中自动释放池做了 “谁创建谁释放” 里面的一部分。 ARC 帮我们做了 “谁创建、谁释放、谁引用、谁管理” 四个部分。 ARC 帮我们写了很多管理内存的代码,包括 autolease、retain、release 等。如果不理解 MRC 下面他们的含义,是不可能理解 iOS 内存管理的。 对于 autolease、autoleasepool、autoleasepoolpage 这些,是自动释放池部分,是 iOS 内存管理的一个面。 在 ARC 下,我们虽然不需要写 retain 和 release,不代表他们不存在了。只是编译器帮我们自动添加了,并且在合适的时间添加的。只有编译器也不行,在运行时也会进行内存的控制。在编译和运行时两方的协调控制下,才做到了引用计数及时 = 0,也只有计数 = 0,内存才正确释放。 ARC 内存不是绝对安全释放的,还牵涉到内存区,如果字符串定义到了堆区,释放是及时的,定义到了栈区和常量区,就不那么及时了(虽然引用计数 = 0,代码也不能在调用,但是真实内存还在)。 而且很可能还会因为代码原因导致引用计数永远不可能为 0,常见的就是循环引用,如 Block 的双向强引用,NSTimer 的双向强引用等等,这里都需要特别的破环。解决双向引用的问题,核心在于破环,只要有一个缺口,内存不可能不释放。 其中循环引用的环的查找,也有不少技巧。核心还是在于通过 runtime 来判断是否是强引用,然后通过广度遍历,来确定环的存在。 深入理解自己生成的对象,自己持有、非自己生成的对象,自己也能持有、不再需要自己持有的对象需要释放、非自己持有的对象无法释放,就能深入理解 iOS 的内存管理。 推荐《Objective-C 高级编程》,更推荐苹果开源的 runtime 源码。

2019/1/6
articleCard.readMore

游戏人生

最近一年,试玩了不少几款游戏。 有下载需要付费的,有内购的,也有免费的。有本机的,也有联网的。有养成类的,也有公平平台的。 游戏过程是下载了,试玩了,玩了,卸载了。最后连游戏名字也忘了。 今天,也是在 2018 年的最后一天,开始卸载最后一批游戏。游戏这段旅程,在我的生命里,初步结束了。 昨晚,我刚在一个游戏里面付费了 30 元。接着,我杀死了应用,想着这段时间我的游戏生涯。 我对于游戏,始终有一条清晰的线,沿着这条线,不迷失。也是这条线,让我知道游戏的本质,看清很 low 的游戏也日流水过百万的简单操作下的华丽外衣。 我所认知的这条线,是游戏制作人的线。 你玩不过制作人,这是我对游戏的总结。制作人所关心的,是对你神经敏感度的把控。任何一款游戏,你在其中的每一个杀怪,每一次升级,乃至每一个按钮的操作,都在可预估可预期的范围内。如果你在一款游戏里面的活动,超过了制作人的预期,那么这可以说是制作人的失职了。我在玩游戏的过程中,很多次都在想,制作人有没有考虑到我这样的玩家?有没有想过我会点这个按钮而不是那个按钮?最后我发现,在游戏里,你的等级再高,你的操作再溜,不过都是制作人给你的。你就是那个狂妄的孙悟空,始终没有脱离佛祖的五指山。我一直惦记着制作人的线,我在玩过的游戏中,都觉得自己不是透明的,我在被监控。这让我很焦虑也很恶心。我不想被控制,更不想自己的每一个操作和决定,都在别人的电脑显示屏下显示。我不想没有隐私。 如果真的有一款游戏,或者三体里面的那个游戏,你为你的操作负责,没有版本线,你在游戏里的一切,就和你的真实生活一样。每一个决定,都会有不一样的没有被安排的人生。那么,我觉得我会喜欢上这个游戏。虽然我不知道,下一步决定,是不是会导致 GameOver。但是我知道,这样的游戏肯定是不会存在的。一个制作人没有预期到的游戏情节,又该如何生成画面呢?毕竟游戏画面,是代码写出来并运行在服务器上的,不可能由玩游戏的人去生成。 游戏的本质和华丽外衣 游戏的本质,是对瞬间肾上腺素的欺骗与压榨。 游戏本身很美丽,华丽的场景,华丽的服装,华丽的技能,华丽的人设。 这些外衣,让你觉得你让你手里的人设如此华丽。 因为你的努力,人设升级无限快,因为你的操作,一个个技能才夺目绚烂。 如果抛开这些后期的引擎渲染,你的人设,其实就是一根竖线。你的技能操作,就是竖线在那左边移动一下,右边移动一下。甚至于,你的人设,就是(x,y)这样的坐标。 你付出了自己的时间,制片人给你的努力包装了外衣,让你觉得你的付出是值得的。而你在赢得一场比赛后,制片人就开始收割你的肾上腺素,让你激动,让你产生快感。而你如果输了一场比赛,肾上腺素同样也收割你的情绪,让你癫狂。 制片人要制作的,就是一个个瞬间肾上腺素突涌的点。在这个点,你会产生快感,会癫狂,会进行自认为公平的等价交换。最恐怖的莫过于这个实质上并不等价的交换,在这个交换里,你会通过现金,通过时间,通过自认为能够交换的一切,来和制片人做交易。 为了这肾上腺素突涌的点,制片人大费脑经。所以,中级的游戏,是在游戏里多留你一会是一会,多一些时间,突涌的点就会多一些,你进行交换的概率也大一些。高级的游戏,是让你主动找它,主动找那些突涌的点,时时刻刻牵着你在游戏中找满足。 你觉得的快乐,并不是快乐,只能称作临时的心里满足。而对于你肾上腺素的压榨,更深切来说,可能不仅是压榨,更多的是欺骗。毕竟一次次的交易,谁又能为那个虚拟的戒指进行定价?朋友,制片人欺骗了你,你的肾上腺素,不止那个价。 人生不是游戏。游戏有场景,人生却无制片人。 我写下这篇关于游戏的文章,是我 2018 年的人生感悟之一。 对于没怎么玩过游戏的朋友来说,我建议你玩玩游戏。游戏本没有对错,它就像食材、电影、戏剧一样可以丰富你的生活。如果在你的人生中没有经历过几场游戏,那么人生里总会缺少点什么。如果非要说这缺少的是什么,我们可以把它理解为你和朋友饭后的谈资。 对于成谜游戏的朋友,我希望你能够了解到,为了你和一群像你一样的朋友,制片人没日没夜的付出,最后他们很可能还要限制你们的游戏时间,虽然对他们来说,这很矛盾。我更希望你能认知到虚拟和现实的不同,认识到捏在手里的一百块人民币和游戏里的一百万金币的区别。每次击杀 Boss 和英雄升级的激动,那不是快乐,只是用自己的时间不等价交换过来的临时满足。而你的人生中,用来换取临时满足感的肾上腺素并没有那么多,有那么几次,就够了。 游戏内无对错,游戏外有成败。 游戏带来的临时满足并不是快乐,快乐一定来源于真实场景过度。快乐一定是舒缓的持久的心情愉悦的。我抽烟恶习好多年了,每支烟的尼古丁带来的满足感有效期不过 2 小时。游戏带来的临时满足,或许连百分之一支烟带来的有效期还要少。 我们的人生之所以精妙,就是因为,人生,没有制片人。 自从移动互联网发展起来的这几年,每年的这个时候都是资本寒冬。 今年也和去年、前年、大前年一样,各种公司倒闭,部门裁员。 不过也许是我又大了一岁,今年的杭州感觉真的冷啊。 嗯,感觉是比去年冷一些。 就拿下雪来说,还没有跨年,这都两场雪了。

2018/12/31
articleCard.readMore

Block 闭包简查记录

像 Block 这种闭包,就是怎么用怎么爽,怎么用怎么喜欢的编码方式了。 闭包很伟大,也是各个语言都实现了的基础语法。我这边对闭包的理解,就是:内部函数持有外部变量。 但是各个语言都有需要注意的点,如 iOS 里面的循环引用,Swift 里面的逃逸闭包,Python 里面的闭包变量延迟定义等。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 As a local variable: returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...}; As a property: @property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes); As a method parameter: - (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName; As an argument to a method call: [someObject someMethodThatTakesABlock:^returnType (parameters) {...}]; As a typedef: typedef returnType (^TypeName)(parameterTypes); TypeName blockName = ^returnType(parameters) {...};

2018/11/24
articleCard.readMore

《白夜行》

雪穗和亮司大约 10 岁在图书馆相识 因为家庭及环境等感同身受两人很默契 雪穗早年丧父贫穷 亮司家为典当行但家庭不和谐 雪穗母亲把雪穗卖给有恋童癖的亮司父亲 亮司躲在荒楼里看到父亲强暴雪穗 亮司杀死了父亲并制造了密室杀人的现场 雪穗杀死母亲并制造煤气自杀现场 两个小孩在年龄的掩护下没被警方怀疑 亮司受不了母亲和管家有染后离家出走 雪穗认亲戚做养母到另一个地方学习生活 亮司未满十八开始了自己的黑夜生活 亮司做色情交易赚中间费 亮司同学和 40 岁妇女脱离亮司管理私下交易过程中妇女过于激动死亡于酒店 亮司认为电脑终将改变世界并带来财富 亮司经过努力制造了会写游戏代码的同学不在场证明并成功捕获其同学忠心 雪穗偷盗私教的游戏大部分代码后给了亮司 亮司伙同同学补全其代码靠卖盗版游戏生活 雪穗因幼年生活被其女同学察知并嘲笑后让亮司强暴了该女同学成功掩藏过去 亮司察觉磁卡是漏洞后制造假银行磁卡并盗取原卡现金 雪穗让亮司强暴了大学好友拿到大学舞蹈社的工费卡后交由亮司并套现 后面事件很多,也更复杂和离奇,写不下去了,因为写出来就感觉是雪穗和亮司的错,但全文都体会不到他们有什么错。 事件和故事终将不一样。

2018/11/11
articleCard.readMore

谈谈事件驱动

事件驱动这个技术方案,可以说实实在在影响了这些年编程界的技术方向。最实际的受用者应该就是异步编程了,如 I/O。 我所认知到的语言,都是事件驱动的使用者,受益者,推动者。 很多朋友可能更多的停留在精通官方 API 阶段,还没有更深层次的认知计算机原理,不晓得代码是怎么工作的。我不精通官方 API,计算机原理也认知浅薄,但在自己认知的语言范围内了解了一些。我感到庆幸,希望和大家分享。 事件驱动的形象描述 A 和家人去外婆家取了个号。A 一直在门口等着,是阻塞。A 出去玩一会,门分钟去外婆家门口看看到了自己号没有,是监听。A 去一个衣服店看衣服去了,到了自己号的时候,收到外婆家发的短信消息后直接去外婆家,是回调。 上面这个例子,变种极多也千篇一律。串行并行、同步异步、阻塞非阻塞都可以用。我们用这个例子来说事件驱动。 这里分析第三种情况,A 去看衣服去了,这个时候外婆家的号叫到自己,并短信通知了自己,这个行为的分析。 A 查看短信的行为和外婆家发出短信的行为,就是我们分析的重点。 外婆家为了发出这个短信,需要耗时,甚至外婆家也不知道耗时多久。所以外婆家需要做的是,一定要在 A 的号到了的时候,准确及时的发出短信,这是一个事件的发出。 A 虽然有手机,但是看不看短信是 A 的事情。所以 A 一定要在看到短信之后立即作出处理判断,这是一个事件的反馈。 但是 A 为了能够尽快吃到饭,多做了几手准备,他把西贝、海底捞几家店的号都拿了,打算谁先叫到自己,就去谁家。 所以 A 接受到的事件是多个并且不确定的。 Life is thread,我们把 A 比作一个线程。A 能够及时响应各家店面发来的消息,原因就是线程里面有一个 while (true):{pass;} 这样的循环。依靠 CPU 这个超强大脑控制器,只要有事件需要通知到线程,线程里面的这个 while 循环就会获取到并及时处理。 所以事件驱动的本质是:一方及时发出事件,通过 CPU 时间片实时轮转事件循环队列并告知到 while 循环以通知到另一方,另一方及时响应事件。 iOS 中的使用 Runloop Runloop 可以说把事件驱动利用到了极致。你能想象,如果没有 Runloop,你就真的不能使用 iPhone 手机了! Runloop 依托于线程。我们手势点击一个按钮,就是在操作主线程里的 Runloop。 Runloop 通过事件驱动在以下 8 种状态下实时循环切换,用于省电的同时又能够及时处理用户界面反馈。 1 2 3 4 5 6 7 8 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop }; 闭包 我们通过 IOS 里的 Block 闭包,可以在异步执行一串功能逻辑代码后接着处理闭包里的活。 1 - (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName; 1 [someObject someMethodThatTakesABlock:^returnType (parameters) {...}]; 还有其他非常多的使用,如通知等 Python 中的使用 Python 近些年才完全开发完优秀的协程并开放使用。使得 Python 上多并发成为事实上的可能。这里的多,不是之前的几十几百,是上十万。 Event_loop+ 协程 Python 里面使用多线程其实并不怎么爽,本身就是耗资源的语言,多线程切换更加雪上加霜了。通过协程,妥妥的解决了上十万的并发。 1 2 3 4 5 6 7 8 9 import asyncio async def test(i): print("test_1",i) await asyncio.sleep(1) print("test_2",i) loop=asyncio.get_event_loop() tasks=[test(i) for i in range(5)] loop.run_until_complete(asyncio.wait(tasks)) loop.close() aiohttp 网络访问上的多并发,对协程的进一步封装。 Node.js 中的使用 Event_loop 事件循环 这个就没得说了,彻彻底底依靠事件驱动起家的语言。基于 javascript 和 V8 引擎起来的 Node.js,就是完完全全的单线程语言。 可是要知道,Node.js 就是以单线程中使用事件驱动处理高 IO 闻名世界的。 你可以想象一下,一个 Node.js 搭建的后台,每秒上千上万的并发,都是单线程在处理吗? Java 就是一个用户一个请求一个线程(又名线程驱动),服务器资源耗费真的大。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var fs = require("fs"); var debug = require('debug')('example1'); debug("begin"); fs.readFile('package.json','utf-8',function(err,data){ if(err) debug(err); else debug("get file content"); }); setTimeout(function(){ debug("timeout2"); }); debug('end');

2018/11/8
articleCard.readMore

折腾

这次又折腾了一把。原因是我想有一个能够宣传出去的名字了。我希望以后,这个名字所生产的内容,可以指导或者引导一些人,也希望这个名字可以成就自己。 这个名字,其实想了很久了,但是一直没有想好。因为一开始,我想找一个英文名。找了许久许久,找到了几个试用了一段时间,发现并不合我心。终于几天前,我想到,或许那些写小说的人,他们的笔名就很有意思呢?比如南派三叔,唐家三少啥的。没有必要局限在英文名字里面走不出去。中国的文字可真是博大精深,不多久,我确定了响当当的 **” 一个工匠”**。 一个工匠。跋山涉水,走心为匠。 我觉得很合适我,就 gmail 邮箱也申请了。申请后,也越发喜欢,以后我就自叫 “一个工匠” 了。 这两天在切账号。首先把国外的一些账号都切到这个了,其他的还没有动手。实不相瞒,这真是一个累活。 顺带发现了 facebook 的一个想不到的恶心。我注册了一个新脸谱账号,注册成功后故意引导我去输入手机号和自己的照片。照片上传后,就是一直审核了。本来以为几分钟就好了,可这一个审核,用了一两天,期间无法登陆账号。简直无法想象。后来我注册了一个新的,避开了手机号上传和照片上传,使用没有任何影响。好恶心! 因为寻找 “一个工匠” 这个名字,我总耗时应该在 2 天。经历各种名字后,试用并通过。说来也好笑,之前选定的英文名,试用了几个月,最后还是无疾而终。这个名字,试用了几天,就落锤了。所以说,喜欢一个人并有幸在一起,那真的有无限的未来。而不喜欢并强加在一起,那就是一场悲戏。 为了一个名字,我自觉用时过多了。这也是我的过失,没有在学习生活中巧妙的寻找发现。但是我还是将自己的这段寻找合适名字的经历,归类为折腾。 爱折腾的人生,总归会和普通的人生不一样。但哪样才是普通的人生,哪样才是不一样的人生,终归没有一个固定的公式用来计算。人各不同,不敢妄语。但是每个人,都应该做到对自己十分了解清楚。清楚自己的人,也会爱惜自己的宝贵一生。但说起来又可能是个笑话,对自己还能不了解?实然,就是如此。很多人对自己都是一个黑盒呢!我对自己也是一脸懵,比如 “我的一生应该是什么样的” 与 “我的一生会是什么样的”。这 “应该” 和 “会” 之间,就有一万种可能。所以我不是大师。 一直关注 ofo 最近风声不断感觉最后的一场风雪也要到来了 于是安装 app 想看看里面余额几多 尽然发现自己也在退款人员行列 本是火中鸟 以为隔岸猴

2018/10/31
articleCard.readMore

老博新开 祭首

好久没有写博客了,猛的一下打字,久旱逢甘霖,突破九重天。 开始的 CSDN 博,到后面私有博,现在的静态博。虽然都挺方便的,但是变迁中感觉自己原来越懒了,要啥维护,就静态的! 最近几年,养精蓄锐全是瞎扯,不过在我身上发生的一些事情,倒是值得简要回味一下。 女儿现在快两岁了,很可爱。我和老婆想了好久都没有起好她的名字,最后朋友来看孩子,帮我们从诗经《九歌》中取了一段扬枹兮拊鼓,疏缓节兮安歌。我们给孩子取名杨安歌。她走路说话都比较晚,现在可以学着我们咿呀两句了,跑的也比较快,挺闹人,但也开心。 老婆全职带孩子,我们也闹了好几次,最后都回归了现实。老婆想上班,也有大想法。希望我们以后能够实现。和老婆的婚礼是在老家进行的,比较简单。孩子也参加了我们的婚礼。后面打算补上蜜月。 经历了两次稅改,上次多少年前不想去查了,这次就在今年。可是没觉得变好了,什么都在升值,买菜买衣全是稅。想到了捕蛇者说。 房子价格这两年可真的是疯了。如果让我形容,那就像大跃进一样,举国性,大跨度。我在一年前结婚了,现在还没有一个房子。不过我老家的房子倒是挺大的,我也不知道还能不能习惯老家的生活。 经历了一些工作、生活上的细节,对人生也开放乐观老练了很多。比如家庭,朋友,工作,生活。这些可以简单聊聊,放在其他博文里面说,这里不写了,值得回味。 我的记性本来很差,最近感觉越来越差了。我真怕老了一些后,会老年痴呆。知道我记性差的,也就老婆,因为其他人,和他们说,他们都不信。 最近三年吧,工作虽无建树,倒是有了妻儿,思想上也有了很大程度的提升。现在孩子大一些了,想发力了。在思考一下,看看什么时候是出路。 对于工作上,进步最大的一定是处人处事。这玩意,俗称情商。智商不够情商补,这句话本身虽然有点骂人,但绝对是真理。不过最后我思考了这玩意,情商依托的是环境。这里的环境,一是有人指导,二是有练习场所。虽然我理解的很迟,但也在弥补二十年的空白。我也会关注我的孩子。所以说,一代又一代,生活的方式虽然不同,但年轮至少是往前的。 昨晚老婆说,除了和你一起出来转转看看这霓虹大灯,哪里都没有去过了。我说我也是,最近几年,大门不出二门不迈的。 电影院没有去过,KTV 没有去过,更别说旅游了,就是很近的景点,都没怎么去过。 我们一致把矛头指向了孩子,这坑货,毁了我和老婆的热恋期、蜜月期、独处期。一把屎一把尿的,我虽然没有太出力,倒是辛苦了老婆。 我和老婆,带着孩子,还有家里的一条狗名叫闹闹,简单生活好几年。 Google 的 “蜻蜓”,不出意外是肯定要在 Google 离开中国 8 年后重新入华了。 开心也不开心。 开心大陆人有机会摆脱百度这个坑爹玩意了。 不开心信息的剪裁与封锁。 毕竟,媒体的力量,非神力,无法抗拒。 2018.12.18 13:04:00更新 几分钟前收到消息,“蜻蜓” 项目终止了。 感慨啊,Google 员工的力量的确不小啊。 虽然该项目的终止肯定不是完全因为员工的反对,但员工的反对肯定引导了项目的终止。 外界声音:Google 你不能这样,“蜻蜓” 带来的危害会很大,被人为干预的搜索,肯定会造成使用者信息的不透明和方向的缺失。

2018/10/15
articleCard.readMore