+
@@ -64,7 +64,7 @@ Clang 相较于 GCC,具备下面优点:
- 设计清晰简单,容易理解,易于扩展增强
-
+
@@ -92,7 +92,7 @@ clang -ccc-print-phases main.m
展示如下:
-
+
@@ -104,7 +104,7 @@ clang -ccc-print-phases main.m
查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import)、宏定义替换等。展示如下:
-
+
@@ -112,7 +112,7 @@ clang -ccc-print-phases main.m
词法分析阶段,主要生成 Token。使用指令 `clang -fmodules -E -Xclang -dump-tokens main.m` 查看具体做了什么
-
+
@@ -120,7 +120,7 @@ clang -ccc-print-phases main.m
语法分析阶段,生成语法树(AST,Abstract Syntax Tree)。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看
-
+
对 main.m 的代码进行改造
@@ -142,7 +142,7 @@ void test(int a, int b) {
再次查看 AST 可以加深理解
-
+
其中:
@@ -154,7 +154,7 @@ void test(int a, int b) {
也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。
-
+
@@ -171,7 +171,7 @@ LLVM IR 有3种表示格式:
- text:便于阅读的文本格式,类似于汇编语言,推展名为 `.ll`。使用指令 `clang -S -emit-llvm main.m` 进行转换
-
+
学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp`
@@ -313,15 +313,15 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
```shell
mkdir llvm_xcode_build
cd llvm_xcode_build
- cmake -S .https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/llvm-project/llvm -B ./ -G Xcode -DLLVM_ENABLE_PROJECTS="clang"
+ cmake -S ../../llvm-project/llvm -B ./ -G Xcode -DLLVM_ENABLE_PROJECTS="clang"
```
因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功
-
+
-
+
@@ -349,11 +349,11 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
- 先创建一个插件文件夹 `code-style-validate-plugin`
-
+
- 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)`
-
+
@@ -382,17 +382,17 @@ Xcode 打开项目,选择自动创建 Schemes
-
+
选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑
-
+
初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
-
+
@@ -402,7 +402,7 @@ Xcode 打开项目,选择自动创建 Schemes
此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话,Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。
-
+
@@ -421,7 +421,7 @@ Xcode 打开项目,选择自动创建 Schemes
- `-Xclang`
- 插件名称
-
+
@@ -429,7 +429,7 @@ Xcode 打开项目,选择自动创建 Schemes
在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
-
+
@@ -441,17 +441,17 @@ Xcode 打开项目,选择自动创建 Schemes
如下所示:
-
+
继续编译还是会报错,报错如下:
-
+
解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality` 的 ` Default` 改为 `NO`。
-
+
@@ -463,7 +463,7 @@ Tips: 由于重新修改了插件的源码,所以每次 Build 构建完 FANP
编译成功,可以看到在日志中输出了我们编写的日志信息。
-
+
@@ -496,7 +496,7 @@ NS_ASSUME_NONNULL_END
利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m`
-
+
核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。
@@ -786,7 +786,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见
- 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响,继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错,遇到1个则终止编译,请注意该区别,按需编写自己的插件逻辑。
-
+
@@ -834,7 +834,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
-
+
@@ -872,7 +872,7 @@ LLVM 项目中 clang 内置了一堆 checker,用于实现 lint。
- 自动调用 `[super dealloc]`
-
+
- 为每个拥有 ivar 的 Class 合成` .cxx_destructor` 方法来自动释放类的成员变量,代替 MRC 时代的 `self.xxx = nil`
diff --git a/Chapter1 - iOS/1.105.md b/Chapter1 - iOS/1.105.md
index 247abd3..60da1dc 100644
--- a/Chapter1 - iOS/1.105.md
+++ b/Chapter1 - iOS/1.105.md
@@ -18,9 +18,149 @@ UIView 绘制流程。
当调用 UIView `[UIView setNeedsDisplay]` 方法时,系统会立刻调用其 Layer 的同名方法 `[view.layer setNeedsDisplay]` 方法,之后相当于给当前 Layer 打上一个脏标记,之后会在当前 RunLoop 快要结束的时候才会调用 Layer 的 `[CALayer display]` 方法。然后进入当前 UIView 真正的绘制流程中。
-其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法,如果没有实现,则进入系统的绘制流程中;如果实现了,则可能是异步绘制或者自定义渲染的实现。
+其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法
+
+- 如果没有实现,则进入系统的绘制流程:比如:创建绘制上下文、调用 `drawInContext:`、生成内容并赋值给 `contents`
+- 如果实现了,则可能是异步绘制或者自定义渲染的实现。是**代理自定义绘制的入口**。代理可以在这个方法里直接设置`layer.contents`(比如异步绘制生成 UIImage 后赋值给`contents`),完全接管 layer 的内容渲染
+
+
+
+Demo1:
+
+自定义 View,不实现 `displayInContext` 方法
+
+```objective-c
+#import
+
+说明:类似一个 CS 架构。
+
+- **`lldb-mi` 的角色:** 它是一个**协议适配器**,专注于将 **GDB/MI 协议** 翻译成 **LLDB API 调用**。
+- **`API` 是关键桥梁:** `lldb-mi` 几乎所有的功能都通过调用 **LLDB Public API** 来实现。
+- **Core 下的模块是 LLDB 的能力:** 图中 `Core` 下列出的 `Object Files`, `Symbols`, `Process` 等,代表了 **LLDB 调试器引擎本身提供的强大核心功能**。`lldb-mi` 依赖 API 来利用这些功能。
+- **依赖关系:** `lldb-mi` -> `API` -> (LLDB 核心引擎的) `Object Files`, `Mach-O/ELF`, `Symbols`, `DWARF`, `Disassembly`, `LLVM`, `Process`, `GDB Remote`。
+- **目的:** 这张图说明了 `lldb-mi` 是如何构建在 LLDB 强大的核心调试引擎之上,通过标准的 API 调用其功能,从而为支持 GDB/MI 协议的客户端提供调试服务。
+
+
+
+
+
+## LLDB Workflow
+
+
+
+说明:
+
+- **lldb-server**:调试服务端,`lldb-server` 作为“调试代理”,直接操控目标程序。分为两种部署方式:
+
+ - **Host 端**:调试本地程序(如 macOS 进程)。
+
+ - **Remote 端**:部署到目标设备(如手机/嵌入式设备),直接控制被调试程序
+
+- 通信层:TCP + GDB RSP 协议
+
+ - **TCP Socket**:物理传输通道,连接 Host 的 LLDB 和 Remote 的 lldb-server
+ - **GDB RSP (Remote Serial Protocol)**
+ - **基于 ASCII 的调试协议**(明文消息,如 `$m
+此外,`mobileprovision` 文件是不可读的。可以通过 **security cms -D -i 195103db-6d6f-4da1-bd0e-66d5db88176f.mobileprovision -o profil
+e.plist** 指令,dump 成为一个 plist 格式的文件。如下图所示:
+
+
+指令解读:
+- security:是 macOS 自带的安全相关的命令行工具,用于处理证书、配置文件、密钥等的处理
+- cms:表示使用 CMD(Cryptographic Message Syntax,密码消息语法)相关功能,用于处理消息签名和加密消息
+- -i:指定输入文件
+- -o:指定输出文件
+
+重要信息:
+- DeveloperCertificates: 允许使用的开发者证书
+- Entitlements:允许使用的权限列表
+- ProvisionedDevices:允许安装的设备列表(ProvisionsAllDevices,代表授权任意设备)
+
+
+
+### 3. 授权文件(Entitlements)
+声明了 App 所需的权限
+
+
+
+### 4. 签名 codesign
+
+#### 1. 基础签名
+
+- **-s**:--sign identity 指定签名所用的证书(- 代表 ad-hoc 签名)
+- **--entitlements**:entitlements_file 指定签名所需要的 entitlements 文件
+- **-f**: --force 强制替换现有签名
+- **-preserve-metadata=identifier,entitlements**: 重用就签名的一些信息
+- **-deep**: 递归对该 bundle 内包含的其他文件签名
+
+
+注意:为什么不推荐使用 `--deep`?
+
+在没有 `--deep` 的情况下,codesign 命令只会对指定的主目标(main target)(例如 .app 包或 .framework)进行签名。它不会自动递归地签名的 .app 包内部的任何嵌套的组件(如嵌入的 .framework 或 .dylib)。
+
+--deep 选项的设计初衷是试图递归地签名一个 bundle 内部的所有嵌套代码。例如,如果你的 MyApp.app 内部嵌入了 ThirdParty.framework,使用 `codesign --deep -f -s "Your Identity" MyApp`.app 会同时签名 MyApp.app 和其内部的 ThirdParty.framework。
+
+错误的签名顺序(主要问题):
+- 代码签名不仅是对二进制文件盖章,它还计算并存储每个组件的哈希值到其 _CodeSignature/CodeResources 文件中。
+- 当你签名一个 .app 时,它也会计算其内部所有文件(包括嵌套的 .framework)的哈希值。如果这些嵌套的组件在 .app 被签名之后又被修改了,其哈希值就会改变,导致签名无效。
+- `--deep` 的工作方式是:先递归地签名最内层的组件(如 ThirdParty.framework),然后签名外层的容器(MyApp.app)。
+
+这看起来是正确的,对吗? 但实际上,在签名外层 .app 时,它记录的仍然是内层组件签名前的哈希值。而内层组件在签名后其内容(因为附加了签名信息)已经发生了变化,这就导致了内外记录不一致,从而使整个签名变得无效且不可靠。
+
+与现代构建系统不兼容:
+- Xcode 和标准的构建流程(如 xcodebuild)的正确做法是:先单独签名每一个嵌套的组件(Embedded Framework),最后再签名主应用。这确保了每个组件都有自己独立的有效签名,并且主应用在签名时记录的是所有嵌套组件的最终状态。
+- `--deep` 破坏了这种明确的、分阶段的签名流程,试图用一步代替多步,反而引入了混乱。
+
+
+
+#### 2. 动态库签名
+
+动态库可以上架,是因为对动态库可以进行签名。来一个 Demo 工程。一个 iOS 工程,以动态库的形式使用 SDWebImage
+
+##### 1. Demo1
+
+1个 iOS App 工程,使用动态库的方式,依赖了 AFNetworking。选择模拟器进行编译,查看日志:
+
+
+
+日志分析:
+1. 日志中的 `--sign` 后一般接的是证书的名称,但是当前日志中是 `-`,代表使用自动签名模式(Automatic Signing)。
+ - **--sign 参数**:这是 codesign 命令的核心参数,用于指定用于签名的身份(Identity)。这个身份通常对应着钥匙串(Keychain)中的一个证书(Certificate)及其关联的私钥。
+ - **-**:这是一个特殊的标识符,在这里它不代表一个具体的证书名称。它的含义是:“使用临时生成的、匿名的 Ad-Hoc 签名身份来进行签名,而不需要指定一个具体的、来自苹果开发者账户的证书。”
+ 在模拟器(Simulator)上运行(正如你的日志中 Debug-iphonesimulator 所示):
+ 根本原因:iOS 模拟器不像真机设备那样需要验证苹果官方的代码签名证书来确保安全。模拟器的运行环境更加宽松,其主要目的是为了快速调试。为了方便:使用 Ad-Hoc 签名可以省去为模拟器编译时配置和选择开发证书的步骤,极大地加快了编译和调试的速度。Xcode 默认就会为模拟器构建采用这种方式。
+
+2. 日志中的 `--entitlements /Users/unix_kernel/Library/Developer/Xcode/DerivedData/InstallDyanmicAndStaticFramework-bmcbqvmynpdalkdhzqirbithtfwp/Build/Intermediates.noindex/InstallDyanmicAndStaticFramework.build/Debug-iphonesimulator/InstallDyanmicAndStaticFramework.build/InstallDyanmicAndStaticFramework.app.xcent` 代表 Xcode 开启的 App 能力信息。`--entitlements` 参数后面跟随的是一个 `.xcent` 文件的路径,这个文件包含了应用程序的权限(Entitlements)配置信息,也就是 “App 能力信息”
+
+对其查看,内容如下:
+
+
+`security find-identity -v -p codesigning` 指令用于 macOS 系统中用于查看可用代码签名证书的命令,主要用于开发者在进行代码签名操作前确认可用的证书信息
+
+##### 2. Demo2
+
+1个 iOS App 工程,使用动态库的方式,依赖了 AFNetworking。选择真机进行编译,查看日志:
+
+日志分析:
+1. 日志中的 `Signing Identity: "Apple Development: FantasticLBP@github.com (953PZFXZFR)"` 变成了开发者证书。多了一个配置文件。
+2. 使用的动态库 AFNetworking 是如何签名的?
+选择 Pods 的 Product 里面的 AFNetworking 动态库,右击 “show in finder”。看到并没有一个 **`_CodeSignature`** 的文件夹,也就是没有签名信息。然后用指令 ` objdump --macho --private-headers /Users/unix_kernel/Library/Developer/Xcode/DerivedData/InstallDyanmicAndStaticFramework-bmcbqvmynpdalkdhzqirbithtfwp/Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/AFNetworking` 进行查看,发现也不存在 **`LC_CODE_SIGNATURE`** 存储签名信息的 load command。
+
+
+可能会问,如何确定是动态库?使用 `file AFNetworking` 指令即可验证
+
+
+问题:动态库 AFNetworking 没有经过签名,为什么拷贝到 App 里面后,可以上架?
+其实 Cocoapods 自动生成了脚本,在主工程的 `Build Phases -> Embed Pods Frameworks` 下。且 `Input File Lists` 配置的文件内容,就是所依赖库的文件路径。会被当作参数传递给 `Embed Pods Frameworks` 脚本。
+
+
+观察编译日志,会发现 「Run custom shell script '[CP] Embed Pods Frameworks'」这里,先对 Frameworks 目录进行了创建和拷贝。然后对 AFNetworking 进行签名。
+在主工程的 Products 目录下,选择 App,show in finder,然后显示包内容。查看 `Frameworks` 文件夹下的 `AFNetworking.framework` 已经存在了 `_CodeSignature` 文件夹,也就是已经签名完成。继续查看 Load Command,发现也存在了 **LC_CODE_SIGNATURE** Load Command。
+
+
+同时,也可以看到是先对动态库签名,再对 App 签名。
+
+#### 3. 如何查看签名信息
+
+[jtool2](https://github.com/excitedplus1s/jtool2) 工具可以方便的查看签名信息。jtool2 类似于 otool,但添加了许多 Mach-O 相关的命令,功能更完善。它支持多种运行平台
+
+安装方式为:`brew install --no-quarantine excitedplus1s/repo/jtool2`
+使用方式为:**`jtool2 --sig -vv ${MachOFile}`**
+
+
+
+
+
+### 5. fastlane 相关概念
+
+
+- fastlane 本质就是一套命令行工具,专为用来简化并实现我们与 Apple 交互时的自动化
+- fastlane 的每一个单独工具都是为了解决常见的 App Store 或其他问题而设计的
+- fastlane 通过脚本方式集合了一系列常见的行为,叫做 lane。也就意味着可以通过 lane 来对自己的 App 做一些量身定制的需求
+- fastlane 包含大量的 action
+- action 表示特定的应用商店或者其他开发者工作流任务
+- lane 表示工作流程
+
+- Appfile: 存储有关开发者账号相关信息
+- Fastfile:核心文件,主要用于命令行调用和处理具体的流程,lane 相对于一个方法或者函数。
+
+#### 1. action
+
+- cert: 创建和维护签名证书
+- sigh:配置文件
+- gym:构建打包应用程序
+- deliver:上传应用程序和屏幕截图到 App Store Connect
+- pilot:为 TestFlight 上传构建并处理其管理
+- scan:自动化测试
+- match:团队中同步证书和配置文件
+- boarding:测试邀请
+- pem:管理推送配置文件
+- produce:在 App Store Connect 创建新的 App
+
+
+使用方式有3种:
+- 第一种:命令行方式。比如
+ ```
+ fastlane scane --workspace "fastlaneDemo.xcworkspace" --scheme "fastlaneDemo" --device "iPhone 8" --clean
+ ```
+- 第二种:Fastfile 方式。采用 FastLane 约定的格式去编写逻辑。比如
+ ```
+ default_platform(:ios)
+
+ platform :ios do
+
+ lane :builds do
+ # 单元测试
+ scan(
+ workspace: "fastlaneDemo.xcworkspace",
+ scheme: "fastlaneDemo",
+ devices: ["iPhone 14 Pro Max", "iPhone 14", "iPhone SE (3rd generation)"],
+ clean: true,
+ code_coverage: true,
+ output_types: "html,junit",
+ output_directory: "./fastlane/test_output"
+ )
+ # 证书
+ cert
+ # 配置文件
+ sigh
+ # 打包
+ gym(
+ workspace: "fastlaneDemo.xcworkspace",
+ scheme: "fastlaneDemo",
+ clean: true,
+ output_directory: "./fastlane/build_output",
+ output_name: "fastlaneDemo.ipa",
+ silent: false,
+ include_symbols: true,
+ include_bitcode: true
+ )
+ end
+ end
+ ```
+- 第三种:使用特定的 Fastlane 文件。比如使用脚本 `Fastlane scan init` 会生成关于 scan 相关逻辑的脚本文件 `Scanfile`
+
+
+建议使用方式二、三,不建议使用方式一。关于 fastlane 脚本编写文档查看 [fastlane docs](http://docs.fastlane.tools)
+
+使用方式:
+Scanfile 是 专门用于配置 scan 动作(即运行测试) 的配置文件。它的唯一目的是为 scan 提供默认参数。你可以在里面设置 scheme、output_directory、code_coverage 等测试相关的选项。当你在命令行直接运行 fastlane scan 或在 Fastfile 的 lane 中调用 scan 时,它会自动读取 Scanfile 中的配置。
+
+
+
+| 文件 | 角色 | 用途 | 示例内容 |
+| :-------------- | :---------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
+| **`Fastfile`** | ****指挥官\**/\**剧本** | **定义 lanes(工作流)**。这是 Fastlane 的核心,你在这里组合不同的动作来创建自动化流程。 | `lane :test { scan }` `lane :beta { cert; sigh; gym; upload_to_testflight }` |
+| **`Scanfile`** | **专项配置员** | 只为 **`scan`** 动作提供默认配置参数。 | `scheme("MyApp")` `clean(true)` `output_directory("./test_results")` |
+| **`Gymfile`** | **专项配置员** | 只为 **`gym`** 动作(构建 IPA)提供默认配置参数。 | `workspace("MyApp.xcworkspace")` `export_method("app-store")` `output_directory("./builds")` |
+| **`Matchfile`** | **专项配置员** | 只为 **`match`** 动作(证书管理)提供默认配置参数。 | git_url("https://github.com/.../certs")` `app_identifier("com.yourapp") |
+
+总结:
+
+- **`Fastfile`** 就像是一个**总剧本**,里面写着:第一场戏(`lane :test`)是跑步测试,第二场戏(`lane :beta`)是打包上传。
+- **`Scanfile`**、**`Gymfile`** 等就像是每个**演员(scan, gym 动作)的个人小抄**,上面写着他们的默认表情、站位等细节。
+- 当“总剧本”喊到某个演员时,演员就会按照自己“小抄”上的默认设置来表演,除非剧本特意指定了另一种表演方式。
+
+
+
+QA:按照上面的思路和角色分工、能力边界,将下面的代码优化
+
+```ruby
+platform :ios do
+
+ lane :builds do
+ # 单元测试
+ scan(
+ workspace: "fastlaneDemo.xcworkspace",
+ scheme: "fastlaneDemo",
+ devices: ["iPhone 14 Pro Max", "iPhone 14", "iPhone SE (3rd generation)"],
+ clean: true,
+ code_coverage: true,
+ output_types: "html,junit",
+ output_directory: "./fastlane/test_output"
+ )
+ # 证书
+ cert
+ # 配置文件
+ sigh
+ # 打包
+ gym(
+ workspace: "fastlaneDemo.xcworkspace",
+ scheme: "fastlaneDemo",
+ clean: true,
+ output_directory: "./fastlane/build_output",
+ output_name: "fastlaneDemo.ipa",
+ silent: false,
+ include_symbols: true,
+ include_bitcode: true
+ )
+ end
+end
+```
+
+优化:
+
+- 文件一:`./fastlane/Scanfile` 只存放测试相关的配置
+
+ ```ruby
+ # Scanfile 专用配置
+ workspace("fastlaneDemo.xcworkspace")
+ scheme("fastlaneDemo")
+ devices(["iPhone 14 Pro Max", "iPhone 14", "iPhone SE (3rd generation)"])
+ clean(true)
+ code_coverage(true)
+ output_types("html,junit")
+ output_directory("./fastlane/test_output")
+ skip_build(true)
+ ```
+
+- 文件二:`./fastlane/Gymfile` 存放构建相关的配置
+
+ ```ruby
+ # Gymfile 专用配置
+ workspace("fastlaneDemo.xcworkspace")
+ scheme("fastlaneDemo")
+ clean(true)
+ output_directory("./fastlane/build_output")
+ include_symbols(true)
+ include_bitcode(true)
+ ```
+
+- 文件三:`./fastlane/Fastfile` 存放 lane 相关逻辑
+
+ ```ruby
+ # Fastfile - 定义 lanes
+ default_platform(:ios)
+
+ platform :ios do
+ # 定义一个名为 builds 的 lane
+ lane :builds do
+ # 运行测试,所有配置会自动从 Scanfile 读取
+ scan
+ # 管理证书
+ cert
+ sigh
+ # 构建 ipa,所有配置会自动从 Gymfile 读取
+ gym
+ end
+ end
+ ```
+
+分析:
+
+1. **清晰与模块化**:每个文件职责单一,易于维护。
+
+2. **复用性**:你可以在 `Fastfile` 中创建多个不同的 lane(如 `lane :tests`、`lane :adhoc`),它们都可以共享 `Scanfile` 和 `Gymfile` 中的通用配置,无需重复代码。
+
+3. **可覆盖性**:在 `Fastfile` 的 lane 中调用 `scan` 或 `gym` 时,你可以随时覆盖对应 `*.file` 中的默认设置。例如:
+
+ ```ruby
+ lane :special_build do
+ gym(
+ output_name: "SpecialRelease.ipa" # 覆盖 Gymfile 中的默认命名规则
+ )
+ end
+ ```
+
+
+
+#### 2. Matchfile
+
+终端使用 `fastlane match init` 指令创建 Matchfile,同时根据提示选择一些模版和输入信息。
+
+
+
+
+
+- `git_url`:
+
+ 是 `match` 最核心的配置,指定了存储加密证书和配置文件的 Git 仓库地址。`match` 不会将敏感的证书文件(`.cer`)、私钥文件(`.p12`)和配置文件(`.mobileprovision`)保存在本地或只留在苹果开发者门户。
+
+ 相反,它会将这些文件**加密后**推送到这个指定的 Git 仓库(`https://github.com/FantasticLBP/knowledge-kit`)中。
+
+ 当任何开发者或 CI/CD 系统需要这些文件时,`match` 会从这个仓库克隆或拉取最新的加密文件,然后在本地解密并安装到钥匙串和 Xcode 中。
+
+ 这个仓库应该是**私有的(Private)**,因为里面存储的是你应用的敏感安全凭证
+
+- `storage_mode("git")`:
+
+ 明确指定 `match` 使用 Git 作为存储后端。
+
+ match` 支持多种存储方式,`git` 是**默认且最常用**的一种。
+
+ 其他可选模式包括 `s3`(Amazon S3)和 `google_cloud`(Google Cloud Storage)。这些通常在更复杂或企业级的 CI/CD 环境中使用,以提高大文件的下载速度
+
+- `type("development")`
+
+ 设置 `match` 的**默认操作类型**为 `development`(开发证书和配置文件)
+
+ 这个参数定义了你要管理哪类证书。iOS 开发中有几种主要类型:
+
+ - `"development"`:用于开发阶段,可在真机上调试应用。
+ - `"appstore"`:用于提交到 App Store 或 TestFlight。
+ - `"adhoc"`:用于分发给有限数量的测试设备(最多 100 台)。
+ - `"enterprise"`:用于企业账号的内部分发。
+
+ 在这里设置为 `"development"` 意味着:
+
+ - 当你直接运行 `fastlane match`(而不指定类型)时,它会默认操作开发证书。
+ - 当你运行 `fastlane match development` 时,它也会使用这个配置(尽管命令行参数已经指定了类型,但其他相关配置会从这里读取)。
+
+这个 `Matchfile` 配置告诉我们:
+
+1. **存储位置**:所有加密的证书和配置文件都将被同步到 `https://github.com/FantasticLBP/knowledge-kit` 这个 Git 仓库中。
+2. **存储方式**:使用 Git 进行版本管理和同步(这是标准做法)。
+3. **默认环境**:默认情况下,操作的是用于**开发环境**的证书和配置文件。
+
+**一个典型的工作流程:**
+
+1. 团队成员 A 首次运行 `fastlane match development`。
+2. `match` 会检查指定的 Git 仓库中是否已有开发证书。
+ - 如果**没有**,它会连接到苹果开发者门户,创建新的开发证书和配置文件,加密后推送到 Git 仓库。
+ - 如果**已有**,它会将加密文件拉取到本地,解密后安装到 Xcode 和钥匙串中。
+3. 团队成员 B 加入项目,同样运行 `fastlane match development`。
+4. `match` 从同一个 Git 仓库拉取**完全相同的**证书和配置文件,确保团队环境一致。
+
+
+
+## 三、持续集成
+
+Cotinuous Integration,持续集成意味着每次代码的变更都在构建服务器上运行测试,并在指定场景下触发。这样如果开发者将测试失败的代码推送到代码库,也称为破坏构建,CI 会触发警告。
+
+- 主动式 CI:CI 提供商
+- 托管式 CI:github action
+- 手动式 CI:自己管理,Travis CI、Jenkins
+
+
+
+- Travis CI:小型开源项目,付费、简单、方便、
+- Jenkins:大型企业、丰富的自定义选项、定制化,不能做到开箱即用,免费
+
+### 1. docker
+
+> 问:为什么一定要 docker?不能在服务器上按照本地的配置安装所需的各个软件吗
+>
+> 答:在一个团队中,每个开发者的本地环境都可能略有不同(macOS 版本、Xcode 通过 App Store 安装还是手动安装、Homebrew 的使用方式等)。
+>
+> - **问题**:新同事加入,需要花费**一整天甚至更长时间**来按照文档一步步配置环境,任何一步的疏漏都会导致环境配置失败。
+> - **后果**: onboarding 成本极高,而且无法保证所有人的环境真正一致,为后续的协作埋下了隐患。
+>
+> 为什么“手动安装一下”不是最优解?它的成功依赖于:
+>
+> - **人的记忆和文档**:需要有人(或文档)准确地记录下所有依赖的**精确版本**(不仅仅是 `fastlane`,还包括它的依赖,以及依赖的依赖)。这份文档需要随着项目的每一次依赖变更而**实时更新**,这几乎是不可能的任务。
+> - **手动操作的准确性**:需要操作人员完全正确地执行安装步骤,不能有任何错漏。这是一个枯燥且容易出错的过程
+
+#### 1. 定义
+
+**Docker 概述**: Docker 是一种成熟高效的软件部署技术,利用容器化技术为应用程序封装独立的运行环境。每个运行环境即为一个**容器**,承载容器运行的计算机称为**宿主机**。
+
+容器。容器虚拟化指的是操作系统而不是硬件,容器之间是共享同一套操作系统资源的。
+
+- 镜像(image):提供容器运行时所需的程序、库、资源、配置等文件,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)
+- 容器(Container):镜像运行时的实体,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务
+- 仓库(Repository):镜像构建完成后,可以很容易的在当前容器上运行
+
+虚拟机:虚拟出一套硬件后,在其上运行一个完整的操作系统。
+
+
+
+**容器与虚拟机的区别**:
+
+* **Docker容器**: 多个容器共享同一个系统内核
+* **虚拟机**: 每个虚拟机包含一个操作系统的完整内核
+* **优势**: 所以 Docker 容器比虚拟机更轻量、占用空间更小、启动速度更快
+
+**镜像 (Image)**:
+
+* **定义**: 镜像是容器的模板,可类比为软件安装包
+* **类比**: 类似于制作糕点的模具,可用于创建多个糕点(容器),并可分享给他人
+
+**容器 (Container)**:
+
+- **定义**: 容器是基于镜像运行的应用程序实例,可类比为安装好的软件。
+- **类比**: 类似于模具制作出的糕点。
+
+**Docker仓库 (Registry)**:
+
+* **定义**: 用于存放和分享Docker镜像的场所
+* **Docker Hub**: Docker的官方公共仓库,存储了大量用户分享的Docker镜像。
+
+
+
+Docker 理解为:标准化、一摸一样的工具箱和工作环境。保证无论哪个工人来,用的工具和环境都完全一样,避免“在我这儿是好的”问题。
+
+- 环境标准化:创建一个包含特定版本 Ruby、Fastlane、Xcode 命令行工具、Cocoapods 的镜像
+- 隔离一致性:确保开发、测试、生产环境的构建完全一致,消除“环境依赖”问题
+- 快速搭建与清理:轻松为 Jenkins 提供纯净、可随时销毁和重建的构建环境。
+
+**价值:开发者需要花费大量时间排查环境差异,而不是专注于修复真正的业务逻辑 Bug,严重拖慢开发效率。**
+
+
+
+#### 2. docker 的核心角色与能力
+
+docker 在 iOS CI/CD 管道中(通常与 Jenkins、Gitlab CI、Github Actions 等工具配合)中主要负责“管理阶段”和“环境保障”
+
+1. 环境标准化与一致性
+
+ - 问题:传统的 CI 机器(无论是物理机还是虚拟机)环境复杂,不同项目所依赖的 Ruby、NodeJS、Python、Fastlane、Cocoapods 等工具版本可能冲突。造成“在我本地是好的,在 CI 机器上发布后就失败了”。(CI 机器上并不一定只部署一个 iOS 的 CI 项目,可能其他项目也部署了,可能由于 Ruby 安装了2.0,但是 iOS 的 CI 服务需要 Ruby 3.0+,这样就造成了本地 CI 服务正常,部署到 CI 服务器之后就有了问题)
+ - Docker 解决方案:构建一个专门的 Docker 镜像,预安装项目所需的所有命令行工具和依赖的特定版本,专门负责 CI 逻辑(如 Ruby 3.1.2、Fastlane 2.2.1、Cocoapods 1.22.1、Xcode 16)
+ - 结果:每一次 CI 构建都会从一个纯净、一致、已知状态的镜像环境启动,彻底消除了环境差异导致的构建失败,保证了构建结果的可靠性和可重现性。
+
+2. 依赖隔离
+
+ 多个 iOS 项目可能在同一台 CI 服务器上运行。使用 Docker 可以将每个项目需要的构建环境相互隔离。避免了项目之间的依赖冲突(例如,项目 A 需要 Cocoapods 1.10,项目 B 需要 Cocoapods 1.11)
+
+3. 快速的构建环境准备:
+
+ 相比启动一个完整的虚拟机,启动一个 Docker 容器是秒级的。这大大减少了 CI 流水线等待构建环境就绪的时间,加快了整个构建流程的反馈循环
+
+4. 版本控制与可追溯性:
+
+ DockerFile 是文本文件,可以放入 git 仓库进行版本管理。对构建环境的任何更改(例如升级 Fastlane 版本)都像代码更改一样,可以通过 Pull Request 进行审查、测试和记录,实现了基础设施即代码 (IaC)。
+
+5. 作为轻量级的“任务运行器”
+
+ 在 CI 流程中,Docker 容器通常被当做一个一次性的、执行特定任务的“沙盒”,比如:执行 pod install、执行 lint 或代码分析、运行 fastlane。
+
+
+
+#### 3. docker 的能力边界
+
+尽管 docker 很强大,但在 iOS 开发领域,它有非常明确且无法跨越的边界:
+
+1. 无法直接构建 iOS 应用(最关键的边界)
+
+ - 根本原因:iOS 最终编译、链接和打包必须依赖 MacOS 内核和 Xcode、Clang、SwiftCli
+ - Docker 局限性:标准的 Docker 容器基于 Linux 内核。它无法运行 MacOS 系统或 Xcode 这样的 GUI 应用(所以无法执行 xcodebuild 指令)
+ - 结论:无法在 linux docker 容器内编译出 `.ipa` 文件
+
+2. 无法运行 MacOS 镜像
+
+ Apple 的许可协议和硬件限制,导致不存在官方的 MacOS Docker 镜像。虽然有类似 LinuxKit 这样的非官方项目,但不够稳定,不能用于生成环境,风险较大
+
+
+
+#### 4. 最佳搭配
+
+明确了 docker 的作用和能力边界后,一个典型的 iOS CI/CD 架构就清晰了:**“在 Linux Docker 容器中准备环境和运行脚本,在 MacOS 主机/节点上进行最终编译”**
+
+- CI Server(Jenkins Controller):可以运行在任何系统上,负责任务调度
+- MacOS Build Agent/Node:一台或多台安装了 Xcode 的 MacOS 系统的电脑,它被注册到 CI Server 上,专门用于执行需要的 xcodebuild 任何
+- Docker 的使用:
+ - CI 流水线启动后,CI Server 会现在 Linux Docker 容器中完成所有它能做的工作:代码拉取、依赖安装(pod install)、运行单元测试(如果测试不依赖 MacOS 框架)、执行静态分析等
+ - 当需要进行 Xcode 的步骤(如编译、打包、签名)时,CI Server 会将工作委托给一台 MacOS Build Agent
+ - MacOS Agent:接收任务,它可能本身会通过一个 Docker 来获取一个一致的环境(用于运行 fastlane 等工具),或者直接使用本地安装的工具,然后调用 xcodebuild 和 fastlane 完成最终的构建和打包
+ -
+
+
+
+
+
+### 2. fastlane
+
+fastlane 可以理解为**专业高效的“工人”**。它精通所有 iOS 打包、签名、测试、上传的具体细节,干活又快又好。
+
+是一个自动化命令工具集。
+
+### 3. jenkins
+
+统筹全局的“项目经理”,它不亲手干活,也不提供工具,但负责安排任务、监控进度、触发流程(比如代码一来就让工人工作),并向大家汇报结果。
+
+- 调度与触发:监听 git 代码推送,定时或者其他事件触发整个流水线
+- 流程编排:定义 pipeline 流水线,决定先做什么后做什么
+- 资源管理:分配和管理执行任务的服务器(成为 Agent/Node)
+- 状态监控与报告:集中展示构建结果、测试报告、日志、并发送通知
+
+
+
+### 4. 黄金搭档
+
+```mermaid
+flowchart TD
+A[开发者推送代码] --> B[Jenkins 监听到推送事件]
+
+subgraph Jenkins_Agent_Node[Jenkins 代理节点]
+ direction TB
+ B --> C[Jenkins 触发 Pipeline]
+ C --> D[Pipeline 步骤: 准备环境]
+ D --> E["执行 Docker Run项目 ${env.JOB_NAME} 的测试已通过。
+ +构建详情:
+测试覆盖率:
+${coverageReport}
+
+ 此MR已获得足够审批,可以合并。
+ """, + to: "${changeAuthor},${reviewers}", + mimeType: 'text/html' + ) +} + +// 发送构建成功通知 +def sendBuildSuccessNotification() { + emailext ( + subject: "✅ 构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}", + body: """ +项目 ${env.JOB_NAME} 的构建已成功完成。
+构建详情:
+编译检查通过,可以继续代码审查流程。
+ """, + to: env.CHANGE_AUTHOR ?: 'team@example.com', + mimeType: 'text/html' + ) +} + +// 发送失败通知 +def sendFailureNotification() { + def recipients = env.CHANGE_AUTHOR ? "${env.CHANGE_AUTHOR},team@example.com" : 'team@example.com' + + emailext ( + subject: "❌ 构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}", + body: """ +项目 ${env.JOB_NAME} 的构建失败。
+构建详情:
+
+
+效果:线上低概率异常的根源被提前消灭,问题无法逃逸到现线上,业务层几乎无需处理这类异常(因为代码本身就没有漏洞)
+
+关于如何编写 LLVM 插件,做到在 Xcode 中实时展示代码中存在的问题,可以查看:[LLVM 插件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md) 这篇文章。
+
+
+
+### 方案 4:轻量级熔断 / 降级(适合核心业务场景)
+
+对于 “万分之一” 但影响大的异常(如支付金额计算、商品价格),采用「熔断策略」:
+第一次出现异常:上报 + 降级为默认值(如价格显示 “--”);
+短时间内多次出现(如 1 分钟内 > 3 次):触发熔断,建议:展示兜底 UI,提示安抚用户,稍等片刻后尝试(起码不会发生稳定的 crash 或者业务异常)
+熔断状态上报 APM,研发收到告警后优先修复。
+
+### 方案 5:语义化默认值 + 业务无感知适配(美团 / 饿了么实践)
+
+返回**业务可识别的语义化空值**,并在 UI 层做统一适配:
+
+| 异常场景 | 兜底值 | UI 层统一处理 | 业务层感知 |
+| ---------------- | -------- | --------------- | ---------- |
+| 数组越界 | NSNull | 显示 “--” | 无 |
+| 字典 key 为 nil | NSNull | 显示 “暂无数据” | 无 |
+| 分母为 0 | INFINITY | 显示 “计算异常” | 无 |
+| 字符串转数字失败 | NSNull | 显示 “--” | 无 |
+
+**实现方式**:
+
+- 封装全局 UI 工具类(如`WXUILabel+Safe.h`),重写`setText:`方法:
+
+```objective-c
+- (void) safe_setText:(id)text {
+ if (text == [NSNull null] || text == nil) {
+ self.text = @"--";
+ } else if ([text isKindOfClass:[NSNumber class]] && isinf([text doubleValue])) {
+ self.text = @"计算异常";
+ } else {
+ self.text = [text description];
+ }
+}
+```
+
+业务层只需使用`safe_setText:`,无需为每个异常场景写判断逻辑。
+
+
+
+## 四、最佳实践
+
+1. **开发环境零容忍**:通过静态分析、自定义 LLVM 插件 + 运行时 Hook,让问题在测试阶段暴露,比如异常发生时,通过日志打印案发现场数据,或者在 Xcode 面板上可视化的在有问题的代码地方显示 error,及时 crash 掉,阻塞正常流程,以防止开发偷懒不去及时修复,产生技术债,同时也防止问题逃逸到线上。
+2. **生产环境软兜底**:不要一刀切,做到可配置。业务方在配置平台可以针对不同业务域的配置,决定是否开启兜底展示(某些业务域的架构师可能更严格,需要开发在 debug 阶段解决这些问题,线上如果出现就统计为 crash)。如果开启,那么安全气垫就返回语义化默认值(而非无意义的 0),全局 UI 统一处理,业务层零侵入;
+3. **可观测驱动修复**:APM 平台统计异常频率 / 影响度,优先处理高频高影响的异常;
+4. **核心场景按需关注**:仅对价格、支付等核心场景注册回调,非核心场景无需处理。
+
+虽然机制和策略已经制定了,但是执行还是靠人,难以保证100%解决问题。所以开发解决通过 lint 和 crash 可以拦截到大部分问题,但是逃逸到线上的部分问题,还是需要安全气垫去 cover,线下无限毕竟去清空问题 + 少数逃逸到线上的问题通过安全气垫去 cover 就可以保证相对全面的安全守护。
+
+
+
+## 五、平台做些什么
+
+获取到 问题数据是第一步,获取之后问题需要及时上报,需要有一个数据上报系统,数据及时准确、高效的上传到服务端(这就是另一个命题,可以查看这篇文章:[打造一个通用、可配置、多句柄的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)),到 APM 后台。后台产生工单,根据业务模块分配到对应的责任人、群组。根据问题的紧急程度,报警策略和提醒频次也不一样,比如**波动报警、自定义报警策略等(这也是另一个命题:智能报警策略、波动报警策略、业务线自定义可配置责任人等)**。责任人及时解决问题、修改工单状态、热修复或者发布新版本,问题闭环。
+
+线上异常上报后,APM 平台需提供:
+
+- 异常频率(如 “数组越界” 仅 1/10000);
+- 影响用户数(如仅影响 10 个用户);
+- 业务上下文(如 “商品详情页 - 价格数组”);
+- 调用栈 + 设备信息。
+
+**核心逻辑**:
+
+- 低频率、低影响的异常(如 1/10000,影响 10 用户):暂时无需业务层处理,研发排期修复即可;
+- 高频率、高影响的异常(如 1/100,影响 1000 用户):触发告警,研发紧急修复,业务层临时加兜底。
+
+这样业务开发无需为 “万分之一” 的异常写逻辑,仅需处理 “高频高影响” 的异常。
+
+
+
+## 总结
+
+“最佳实践”部分选择的方案既避免了 “安全气垫返回错误值导致业务异常”,又解决了 “业务开发为低概率异常写冗余逻辑” 的问题,是网易、腾讯、阿里等大厂的通用实践 ——**优雅的方案不是 “兜底所有异常”,而是 “让异常可被提前发现、可被观测、最小化影响”**,即使少数逃逸到线上的,再由安全气垫去兜底解决(解决了展示问题、异常上报和案发现场还原问题),一个好的 APM 平台不光是告诉你又问题,还会告诉你哪里有问题。
+
diff --git a/Chapter1 - iOS/1.38.md b/Chapter1 - iOS/1.38.md
index b187121..46fb552 100644
--- a/Chapter1 - iOS/1.38.md
+++ b/Chapter1 - iOS/1.38.md
@@ -35,7 +35,7 @@
先附上一张总结的非常棒的RunLoop图
-
+
@@ -138,7 +138,7 @@ struct __CFRunLoopMode {
Demo:
-
+
@@ -178,11 +178,11 @@ Source0:
Demo1:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
-
+
Demo2: `- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait` 该 API 的底层就是:向目标线程的 RunLoop 添加一个 Source0 事件,标记后唤醒线程执行对应的事件。
-
+
Source1:
@@ -221,7 +221,7 @@ CFRunLoopSourceRef 事件源(输入源)
### 一对多的关系
-
+
@@ -434,7 +434,7 @@ CFRelease(obersver);
*/
```
-
+
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
@@ -510,7 +510,7 @@ CFRelease(obersver);
### 运行原概要
-
+
- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
- Source0:非基于 port 的,用户主动触发的事件。
@@ -527,7 +527,7 @@ CFRelease(obersver);
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
-
+
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
@@ -1137,7 +1137,7 @@ RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopSer
上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。
-
+
Demo:
@@ -1151,7 +1151,7 @@ Demo:
2. 上面8>2,Runloop 处理 GCD Async To Main Quque
-
+
@@ -1165,7 +1165,7 @@ Demo:
可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
-
+
@@ -1647,7 +1647,7 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
当界面的 Frame 改变,或者更改 UIView、CALayer 的层次时,或者调用了 UIView、CALayer 的 setNeedsLayout、setNeedsDisplay 方法后,这个 UIView、CALayer 会被标记为待处理(类比前端的 Virtual Dom Diff,标记为 dirty),并被提交到一个全局容器中。
-苹果设计 UI 更新也是 RunLoop 的业务方,所以会注册一个 Obserger 监控 `kCFRunLoopBeforeWaiting`(将要休眠)和 `kCFRunLoopExit` (即将退出 RunLoop)状态,然后会执行 `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()` 回调。内部会遍历所有待处理的 UIView、CALayer 以执行实际的绘制和渲染,更新 UI
+苹果设计 UI 更新也是 RunLoop 的业务方,所以会注册一个 Observer 监控 `kCFRunLoopBeforeWaiting`(将要休眠)和 `kCFRunLoopExit` (即将退出 RunLoop)状态,然后会执行 `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()` 回调。内部会遍历所有待处理的 UIView、CALayer 以执行实际的绘制和渲染,更新 UI
@@ -1887,7 +1887,7 @@ main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
-
+
改进代码如下
@@ -1995,7 +1995,7 @@ self.thread = [[LifeThread alloc] initWithBlock:^{
效果如下:
-
+
diff --git a/Chapter1 - iOS/1.39.md b/Chapter1 - iOS/1.39.md
index 33ad89e..c9cb0fb 100644
--- a/Chapter1 - iOS/1.39.md
+++ b/Chapter1 - iOS/1.39.md
@@ -1079,6 +1079,169 @@ QA:互斥递归锁,可以在不同线程中加锁吗?
+#### 使用注意事项
+##### 加解锁必须成对
+加解锁必须成对出现,否则容易出现多线程性能问题。提供一种思路,利用 `@try-finally` 来保证加解锁必须成对存在,这个写法也是 Weex 官方的实现。
+Weex 中的 WXThreadSafeMutableDictionary 提供了一个线程安全的字典,其本质是通过加 pthread_muext_t 锁来维护内部的一个字典的。
+比如下面的代码
+
+初始化锁相关的配置
+```Objective-C
+@interface WXThreadSafeMutableDictionary ()
+{
+ NSMutableDictionary* _dict;
+ pthread_mutex_t _safeThreadDictionaryMutex;
+ pthread_mutexattr_t _safeThreadDictionaryMutexAttr;
+}
+
+@end
+
+@implementation WXThreadSafeMutableDictionary
+
+- (instancetype)initCommon
+{
+ self = [super init];
+ if (self) {
+ pthread_mutexattr_init(&(_safeThreadDictionaryMutexAttr));
+ pthread_mutexattr_settype(&(_safeThreadDictionaryMutexAttr), PTHREAD_MUTEX_RECURSIVE); // must use recursive lock
+ pthread_mutex_init(&(_safeThreadDictionaryMutex), &(_safeThreadDictionaryMutexAttr));
+ }
+ return self;
+}
+
+- (instancetype)init
+{
+ self = [self initCommon];
+ if (self) {
+ _dict = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+```
+在字典操作的地方使用锁
+```Objective-C
+- (void)setObject:(id)anObject forKey:(id
+
+```
+source 'https://rubygems.org' gem 'rails', '4.1.0.rc2'
+gem ‘rack-cache'
+gem 'nokogiri', '~> 1.6.1'
+```
+
+- 读取 Gemfile:Bundler 首先会读取当前目录下的 Gemfile 文件,解析其中声明的所有依赖项及其版本约束
+- 解析依赖关系:
+ - 分析每个 gem 的版本要求,确定满足所有约束的最佳版本组合
+ - 处理依赖的依赖(传递依赖),确保整个依赖树的兼容性
+- 检查本地缓存:
+ - 查看本地是否已缓存所需版本的 gem
+ - 如果有,直接使用本地缓存,跳过下载步骤
+- 从源下载 gem:
+ - 对于本地没有的 gem,从 source 'https://rubygems.org' 指定的源下载
+ - 默认源是 RubyGems 官方仓库,国内用户可能需要切换到镜像源以提高速度
+- 安装 gem 到项目目录:
+ - 默认情况下,gem 会被安装到项目根目录下的 vendor/bundle 目录
+ - 这种方式可以避免污染系统级的 gem 安装,实现项目间的依赖隔离
+- 生成 Gemfile.lock:
+ - 安装完成后,Bundler 会生成 Gemfile.lock 文件
+ - 该文件记录了实际安装的每个 gem 的精确版本,确保团队协作或部署时使用完全相同的依赖版本
+
+思考:怎么样,是不是发现 Ruby Bundler 工作流程和 iOS Cocoapods 的工作过程一致?是的,iOS Cocoapods 的设计就是参考 Ruby Bundler 的设计。甚至连 pod search、install、list API 设计也和 gem 一致
+
+
+
+## 四. Cocoapods
+
+### 1. cocoapods-binary
+- cocoapods-binary 通过开关,在 pod insatll 的过程中进行 library 的预编译,生成 framework,并自动集成到项目中。
+- 整个预编译工作分成了三个阶段来完成:
+ - binary pod 的安装
+ - binary pod 的预编译
+ - binary pod 的集成
+
+### 2. Hook
+- pre_install:Pod 下载之后,但在安装之前可以有时机对 Pod 进行任何更改
+- post_install:当所需依赖安装完成,但此时生成的 XcodeProj 项目在写入磁盘前,我们可以对其进行最后的更改
+
+
+### 3. Cocoapods 工程拆解
+#### 1. cocoapods.gemspec 文件
+```Ruby
+# encoding: UTF-8
+require File.expand_path('../lib/cocoapods/gem_version', __FILE__)
+require 'date'
+
+Gem::Specification.new do |s|
+ s.name = "cocoapods"
+ s.version = Pod::VERSION
+ s.date = Date.today
+ s.license = "MIT"
+ s.email = ["eloy.de.enige@gmail.com", "fabiopelosin@gmail.com", "kyle@fuller.li", "segiddins@segiddins.me"]
+ s.homepage = "https://github.com/CocoaPods/CocoaPods"
+ s.authors = ["Eloy Duran", "Fabio Pelosin", "Kyle Fuller", "Samuel Giddins"]
+
+ s.summary = "The Cocoa library package manager."
+ s.description = "CocoaPods manages library dependencies for your Xcode project.\n\n" \
+ "You specify the dependencies for your project in one easy text file. " \
+ "CocoaPods resolves dependencies between libraries, fetches source " \
+ "code for the dependencies, and creates and maintains an Xcode " \
+ "workspace to build your project.\n\n" \
+ "Ultimately, the goal is to improve discoverability of, and engagement " \
+ "in, third party open-source libraries, by creating a more centralized " \
+ "ecosystem."
+
+ s.files = Dir["lib/**/*.rb"] + %w{ bin/pod bin/sandbox-pod README.md LICENSE CHANGELOG.md }
+
+ s.executables = %w{ pod sandbox-pod }
+ s.require_paths = %w{ lib }
+
+ # Link with the version of CocoaPods-Core
+ s.add_runtime_dependency 'cocoapods-core', "= #{Pod::VERSION}"
+
+ s.add_runtime_dependency 'claide', '>= 1.0.2', '< 2.0'
+ s.add_runtime_dependency 'cocoapods-deintegrate', '>= 1.0.3', '< 2.0'
+ s.add_runtime_dependency 'cocoapods-downloader', '>= 2.1', '< 3.0'
+ s.add_runtime_dependency 'cocoapods-plugins', '>= 1.0.0', '< 2.0'
+ s.add_runtime_dependency 'cocoapods-search', '>= 1.0.0', '< 2.0'
+ s.add_runtime_dependency 'cocoapods-trunk', '>= 1.6.0', '< 2.0'
+ s.add_runtime_dependency 'cocoapods-try', '>= 1.1.0', '< 2.0'
+ s.add_runtime_dependency 'molinillo', '~> 0.8.0'
+ s.add_runtime_dependency 'xcodeproj', '>= 1.27.0', '< 2.0'
+
+ s.add_runtime_dependency 'colored2', '~> 3.1'
+ s.add_runtime_dependency 'escape', '~> 0.0.4'
+ s.add_runtime_dependency 'fourflusher', '>= 2.3.0', '< 3.0'
+ s.add_runtime_dependency 'gh_inspector', '~> 1.0'
+ s.add_runtime_dependency 'nap', '~> 1.0'
+ s.add_runtime_dependency 'ruby-macho', '~> 4.1.0'
+
+ s.add_runtime_dependency 'addressable', '~> 2.8'
+
+ s.add_development_dependency 'bacon', '~> 1.1'
+ s.add_development_dependency 'bundler', '~> 2.0'
+ s.add_development_dependency 'rake', '~> 12.3'
+
+ s.required_ruby_version = '>= 2.6'
+end
+
+```
+
+`cocoapods.gemspec` 作为 Cocoapods 工程的配置文件,类似 iOS 组件库的 Podspec 文件一样。
+Cocospods 工程本身就是一个 Ruby gem,所以 `cocoapods.gemspec` 用于描述这个 gem 包的元数据,包括作者、版本、描述信息,包括一些导入的文件。
+
+也声明了该 gem 包含的源代码文件、资源文件,以及它所依赖的其他 Ruby gem(比如 xcodeProj 等)和版本要求,确保安装时能正确解析依赖关系。
+
+
+
+#### 2. cocoapods-core
+
+1. CocoaPods 核心模块,用来支持:
+
+ - Pod::specification(podspec)
+
+ - Pod::Podfile(Podfile)
+
+ - Pod::Source(Spec repo)
+
+2. cocoapods-deintergrate: 用于从项目中删除和取消集成 CocoaPods,指令为 `pod deintegrate`
+
+3. Xcodeproj:
+
+ 来操作 Xcode 项目的创建和编辑等。同时支持 Xcode 项目的脚本管理和 libraries 构建,以及 Xcode 工作空间(.xcworkspace) 和配置文件 .xcconfig 的管理
+
+4. cocospods-downloader:用于下载和管理引入的源码
+
+5. cocoapods-plugins: 插件管理功能
+
+6. cocoapods-try:可以快速体验该 pod 的 Demo 项目
+
+7. CLAide:命令行解释器
+
+8. ruby-macho:一个用于检查和修改 Mach-O 文件的 Ruby 库
+
+
+
+#### 3. Podfile
+
+Podfile 是一个文件,以 DSL 来描述依赖关系,用于描述项目所需要的第三方库。
+
+
+
+### 4. VSCode 调试 Cocoapods
+
+1. 新创建文件夹 `RubyDemos`
+2. 从 git clone Cocoapods 源码到本地目录
+3. 进入到 Cocoapods 文件夹,将分支切换到和本机安全的 pod 版本一致的分支,指令为: **git checkout `pod --version`**
+4. `RubyDemos` 根目录下创建一个 Xcode iOS 工程,并为其编写 Podfile 文件。目的是为了调试 Cocoapods
+5. `RubyDemos` 根目录下创建一个 **Gemfile** 文件。内容如下:
+ ```Ruby
+ source 'https://rubygems.org'
+
+ gem 'cocoapods', path: './Cocoapods' # 指向本地源码
+ gem 'debug', '~> 1.9.0' # 调试用
+ ```
+6. 终端执行 `bundle install` 指令
+7. 此时,项目文件夹为:
+ ```
+ .
+ ├── CocoaPods
+ ├── Demos
+ ├── Gemfile
+ ├── Gemfile.lock
+ └── StaticLibConflictsDemo
+ ```
+8. 用 VSCode 打开工程。进入 Run and Debug 面板 → 点击 create a launch.json file → 选择 Ruby → 选 Debug Local File。
+9. 修改 launch.json 内容。
+ ```json
+ {
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug CocoaPods",
+ "type": "rdbg", // 必须为 rdbg(debug 工具类型)
+ "request": "launch",
+ "script": "${workspaceFolder}/CocoaPods/bin/pod", // 源码中的 pod 入口
+ "args": ["install", "--verbose"], // 执行 pod install
+ "cwd": "${workspaceFolder}/StaticLibConflictsDemo", // 测试工程目录(Podfile 所在目录)
+ "useBundler": true, // 强制通过 bundle 执行
+ "askParameters": false // 关闭参数询问(避免干扰)
+ }
+ ]
+ }
+ ```
+10. VSCode 插件市场安装:VSCode rdbg Ruby Debugger、Ruby LSP
+11. VSCode 面板中,点击左侧的调试按钮。便可调试。要是看到下面的图,说明可以正常 Debug 了
+
+
+接下来就可以愉快的调试了。
+说明:
+- pod 的每个指令,分别对应 Cocoapods 工程中一个代码文件
+
+- 同时根据观察,发现 `target do` 的代码比 pre_install、post_install 执行更早。所以我们可以做一些脚本化的操作。
+比如下面,增加了一段自定义的脚本
+
+
+
+
+## 五、体验核心依赖库能力
+
+### 1. ruby-macho
+
+#### 1. 操作 Mach-O 文件
+
+读取 Mach-O 文件,并用操作对象的方式去读区、增加、删除信息。
+
+分别对 Mach-O 增加了一个 `LC_RPATH` 类型的 Load Command
+
+```ruby
+lc_rpath = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, 'test_rpath')
+file_exec.add_command lc_rpath
+```
+
+
+
+对 Mach-O 文件中删除了类型为 `LC_LINKER_OPTION` 的 Load Command
+
+
+
+#### 2. 操作动态库
+
+`ruby-macho` 还可以读取动态库信息,下面演示几个能力:
+
+- 读取动态库所依赖的动态库信息
+
+ ```ruby
+ # 打印出当前 Mach-O 文件使用的所有动态库
+ macho_dylibs = MachO::Tools.dylibs(macho_filepath)
+ macho_dylibs.each do | dylib |
+ puts dylib
+ end
+ ```
+
+- 修改动态库的 id
+
+ ```ruby
+ # 修改动态库的 id
+ MachO::Tools.change_dylib_id(macho_copy_filepath, 'test_selfdefined_dylib')
+ ```
+
+ 修改后在终端用指令 **objdump --macho --private-headers ./macho/libAFNetworking_copy.dylib | grep 'LC_ID_DYLIB' -A 5** 验证效果,如下图所示:
+
+
+
+ 也可以直接查看动态库的 id
+
+ ```ruby
+ original_dylib_id = MachO::MachOFile.new(macho_filepath)
+ copyed_dylib_id = MachO::MachOFile.new(macho_copy_filepath)
+
+ # 也可以直接查看动态库的 id
+ puts "before change dylib id: #{original_dylib_id.dylib_id}"
+ puts "after change dylib id: #{copyed_dylib_id.dylib_id}"
+ ```
+
+- 修改动态库 rpath
+
+ ```ruby
+ MachO::Tools.change_rpath(macho_copy_filepath, '@loader_path/Frameworks', '@loader_path/Frameworks/FantasicLBP')
+ ```
+
+
+
+#### 3. 合并动态库到胖二进制
+
+ruby-macho 有很多丰富的 API,基本上开发阶段所遇到的问题,都有现成的 API 解决。比如二进制指令集的合并
+
+```ruby
+filenames = [dylib_merged_filepath, dylib_arm_filepath]
+# # 第一个参数为合并之后的动态库名称,第二个参数为需要合并的一堆动态库
+MachO::Tools.merge_machos(dylib_merged_filepath, *filenames)
+```
+
+合并后用 `otool -f ./macho/libAFNetworking_merged.dylib` 指令查看指令集
+
+
+
+
+
+### 2. Xcodeproj
+
+通过 xcodeproject 路径构建 xcodeproj 对象 `app_project = Xcodeproj::Project.new(app_project_path)`
+
+通过 xcworkspace 路径构建 xcworkspace 对象 `app_workspace = Xcodeproj::Workspace.new_from_xcworkspace(app_workspace_project_path)`
+
+然后通过对象的方式访问 xcworkspace 的信息。比如 schemes
+
+```ruby
+app_workspace.schemes.each do | scheme |
+ puts scheme
+end
+```
+
+
+
+也可以针对特定的 target 修改 xcconfig
+
+```ruby
+# 修改 targets 中第一个对应 configurations 为指定的 xcconfig 文件
+configuration_path = File.dirname(__FILE__) + '/xcodeproject/AFNetworkingMock/xcodeproj-testing.debug.xcconfig'
+# 转换为 xcode 的 file
+xc_file = app_project.new_file(configuration_path)
+
+app_project.targets.first.build_configurations.first.base_configuration_reference = xc_file
+```
+
+效果如下:
+
+
+
+也可以对特定的 target 修改 buildSetting 中的信息,比如 bundle id
+
+```ruby
+# 修改 target 的 bundle id
+app_project.targets.each do | target |
+ target.build_configurations.each do | config |
+ config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.github.FantasticLBP.AFNetworkingMock' if config.name == 'Debug'
+ end
+end
+```
+
+
+
+
+
+## 六、自定义 cocoapods 插件
+
+### 1. 自定义 gem 库
+
+
+
+#### 1. 初始化创建
+
+输入指令 `bundle gem cocoapods-hmap` 自定义一个名为 `cocoapods-hmap` 的 gem 库
+
+#### 2. 工程结构说明
+
+得到的工程结构是:
+
+```shell
+.
+├── CHANGELOG.md
+├── CODE_OF_CONDUCT.md
+├── Gemfile
+├── LICENSE.txt
+├── README.md
+├── Rakefile
+├── bin
+│ ├── console
+│ └── setup
+├── cocoapods-hmap.gemspec
+├── lib
+│ └── cocoapods
+│ ├── hmap
+│ │ └── version.rb
+│ └── hmap.rb
+└── sig
+ └── cocoapods
+ └── hmap.rbs
+```
+
+说明:
+
+- **cocoapods-hmap.gemspec**:
+
+ - gem 的核心配置文件,定义了 gem 的名称、版本、作者、依赖、描述、文件包含规则等关键信息。
+ - 用于打包和发布 gem 到 RubyGems 仓库,是 gem 工程的 “身份证
+
+- **Rakefile**:
+
+ - 定义自动化任务(如测试、打包、发布等),通过 `rake <任务名>` 执行(类似 `Makefile`)。
+ - 常见任务:`rake spec`(运行测试)、`rake build`(打包 gem)、`rake release`(发布到 RubyGems)
+
+- **Gemfile**:
+
+ - 定义 gem 开发 / 测试阶段的依赖(如测试框架 `rspec`、打包工具等),类似前端的 `package.json`。
+ - 通过 `bundle install` 安装依赖,依赖版本由 `Gemfile.lock`(自动生成)锁定
+
+- **源代码目录:lib/ **
+
+ - gem 的核心功能代码存放目录,Ruby 会自动加载该目录下的文件
+
+ - `lib/cocoapods/hmap.rb`:gem 的主入口文件,定义了 `CocoaPods::Hmap` 模块的核心逻辑,是功能实现的主要载体(如与 CocoaPods 集成的逻辑、头文件映射相关功能等)
+ - `lib/cocoapods/hmap/version.rb`: 单独存放版本号的文件,通常定义 `CocoaPods::Hmap::VERSION` 常量,便于统一管理版本(在 `gemspec`中会引用该常量)。
+
+
+
+#### 3. 改造并 run 起来
+
+- 修改 gemspec 文件中带有 url、uri 的字段,开发阶段可以修改为任何一个 url 字符串
+
+- 修改自带的工程结构,比如 `cocoapods/hmap` 改为 `cocoapods-hmap`,将对应的文件也移动位置
+
+- 我们预期的效果是:在终端输入 **pod hmap** 就可以将工程中的静态库 Header Search Path 传统查找模式改为 hmap 文件配置模式。所以需要的的步骤就是在 **bin 目录下创建 hmap.rb**,然后通过 **bin 目录下的代码调用 lib 目录下的 HMap.rb** 能力。
+
+- 为此,需要:
+
+ - 在 lib 目录下创建 HMap.rb 文件
+
+ ```ruby
+ require_relative "version"
+
+ module CocoapodsHmap
+ class HMap
+ def initialize
+ puts "Cocoapods HMap initialized"
+ end
+
+ def self.run
+ puts "Running Cocoapods HMap..."
+ end
+
+ end
+ end
+
+ ```
+
+ - 在 bin 目录下创建 hmap.rb 文件
+
+ ```ruby
+ #!/usr/bin/env ruby
+ require "bundler/setup"
+ require "cocoapods-hmap/hmap"
+
+ # 打印携带的参数
+ puts ARGV
+
+ CocoapodsHmap::HMap.run
+ ```
+
+- 为了在 VSCode 中测试,需要在 Gemfile 中添加一行 **gem 'debug', '~> 1.9.0' # 调试用**
+
+- 工程根目录创建 `.vscode` 文件夹,创建 launch.json 文件。内容如下:
+
+ ```json
+ {
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "rdbg",
+ "name": "Debug current file with rdbg",
+ "request": "launch",
+ "script": "${workspaceFolder}/bin/hmap",
+ "args": ["hmap"], // pod 命令的参数
+ "askParameters": true,
+ "cwd": "${workspaceFolder}", // pod 执行命令的路径
+ },
+ {
+ "type": "rdbg",
+ "name": "Attach with rdbg",
+ "request": "attach"
+ }
+ ]
+ }
+ ```
+
+VSCode 中运行效果如下:
+
+
+
+#### 4. 如何将自定义的指令加入到 cocoapods 中
+
+- 在 Command 目录下创建 `hmap` 文件,不带任何拓展名。rake 处理后,最后会变为 `/Users/unix_kernel/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/cocoapods-hmap-0.1.0/bin/hmap: Ruby script text executable, ASCII text`
+
+- 在 lib 目录下,创建 `cocoapods-hmap` 文件夹。
+ - 在 `cocoapods-hmap` 文件夹内创建 `command` 文件夹,在 `command` 文件夹内创建 `hmap.rb` 文件
+ - class 继承自 Pod::Command,并实现相关方法。比如 initialize、validate!、run
+- 在 lib 目录下,创建 `cocoapods-plugin.rb` 文件
+ - 暴露继承自 Pod::Command 的 hmap command
+
+调试运行后的效果如下:
+
+
+
+类名小写,和类文件关联起来
+
+比如 install.rb 中,类名为 `class Install` ,内部会记录为 **{"install": "install.rb"}**
+
+
+
+#### 5. 打包安装到本地
+
+在终端项目目录下,执行指令 **rake install:local** ,主要用于**在本地构建并安装当前开发的 gem 包**,方便开发者进行本地测试和调试。
+
+一开始有报错,如下图所示。按照提示修改 `cocoapods-hmap.gemspec` 中的配置,然后就可以成功安装了。然后输入 **gem list** 查看:
+
+
+
+输入: **gem info cocoapods-hmap** 查看安装信息
+
+
+
+
+
+就目前的功能进行测试:
+
+| 条件 | 预期 | 结果 |
+| ----------------------------------- | --------------------------------------- | -------- |
+| 随便一个工程目录,没有 Podfile 文件 | 执行 `pod hmap` 会报错 | 符合预期 |
+| 存在 Podfile 文件的目录 | 正常执行 run 方法里面的逻辑(打印逻辑) | 符合预期 |
+
+
+
+
+
+#### 6. Hook 能力
+
+##### 1. post_install
+
+- 修改插件入口文件 (cocoapods_plugin.rb)
+
+ 先在 `lib/cocoapods-plugin.rb` 中注册插件和对应的 hook 能力。利用 API:**Pod::HooksManager.register('cocoapods-hmap', :post_install)**,其文档说明如下:
+
+ >register(plugin_name, hook_name, &block)
+ >
+ >**Definitions**: [hooks_manager.rb](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
+ >
+ >Registers a block for the hook with the given name.
+ >
+ >@param [String] plugin_name The name of the plugin the hook comes from.
+ >
+ >@param [Symbol] hook_name The name of the notification.
+ >
+ >@param [Proc] block The block.
+
+ ```ruby
+ Pod::HooksManager.register('cocoapods-hmap', :post_install) do |context, options|
+ argv = CLAide::ARGV.new([]) # 创建一个空的参数数组
+ command = Pod::Command::HMap.new(argv)
+ command.run_post_install(context, options)
+ end
+ ```
+
+- 完善 HMap 命令类
+
+ 修改 `lib/cocoapods-hmap/command/hmap.rb`,增加 `run_post_install` 方法
+
+ ```ruby
+ module Pod
+ class Command
+ class HMap < Command
+ // ...
+
+ # Post-install 钩子执行的方法
+ def run_post_install(context, options = {})
+ puts "[Cocoapods hmap] Running HMap command in post_install hook..."
+ end
+ end
+ end
+ end
+ ```
+
+- 测试配置
+
+ 修改测试项目的 Podfile 文件,声明 **plugin 'cocoapods-hmap'**
+
+ ```ruby
+ platform :ios, '9.0'
+ plugin 'cocoapods-hmap'
+
+ post_install do | installer |
+ puts "Self defined post_install hook"
+ end
+
+ target 'StaticLibConflictsDemo' do
+ # Comment the next line if you don't want to use dynamic frameworks
+ # AFNetworking 以静态库的形式被依赖
+ pod 'AFNetworking'
+ # 脚本化
+ script_phase :name => 'Run Self-defined Script',
+ :script => "echo 'This is a self-defined script phase'",
+ :input_files => [],
+ :execution_position => :after_compile
+ end
+ ```
+
+- 修改 `cocoapods-hmap` 工程的 VSCode 的 launch.json 文件
+
+ 因为 cocoapods-hmap 工程和 iOS Pods 工程不在一个目录,所以可以在 args 的第二个参数设置为测试工程路径
+
+ ```json
+ {
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "rdbg",
+ "name": "Debug current file with rdbg",
+ "request": "launch",
+ // "script": "${workspaceFolder}/bin/hmap",
+ "script": " /Users/unix_kernel/.rbenv/versions/3.2.2/bin/pod",
+ "args": [
+ "install",
+ "--project-directory=${workspaceFolder}/../StaticLibConflictsDemo"
+ ], // pod 命令的参数
+ "askParameters": true,
+ "cwd": "${workspaceFolder}", // pod 执行命令的路径
+ },
+ {
+ "type": "rdbg",
+ "name": "Attach with rdbg",
+ "request": "attach"
+ }
+ ]
+ }
+ ```
+
+- 测试:存在2种方法
+
+ - 第一种:在 cocospods-hmap 工程中测试,如下图
+
+
+
+ - 第二种:
+
+ - 在终端 cocoapods-hmap 目录下执行 **rake install:local** ,将插件安装到本地
+ - 然后切换到 iOS 被测工程目录下,执行 `pod install`
+
+ 效果如下:
+
+
+
+
+
+##### 2. pre_install
+
+
+
+
+
+
+
+### 2. CocoaPods 插件系统设计
+
+CocoaPods 通过严格的目录结构约定来加载插件:
+
+```shell
+lib/
+├── cocoapods-plugin.rb # 插件主入口文件(必需)
+└── cocoapods-hmap/ # 插件命名空间目录
+ └── command/ # 命令目录
+ └── hmap.rb # 命令实现文件(必需)
+```
+
+#### 1. 自动加载机制
+
+CocoaPods 启动时会自动执行以下操作:
+
+1. 扫描已安装的 gem
+2. 查找所有以 `cocoapods-` 为前缀的 gem
+3. 加载这些 gem 中的 `lib/cocoapods-plugin.rb` 文件
+4. 通过该文件加载插件功能
+
+#### 2. 关键文件
+
+- 插件入口文件 (`lib/cocoapods-plugin.rb`)
+
+ ```ruby
+ require 'cocoapods-hmap/command/hmap'
+ ```
+
+- 命令实现文件 (lib/cocoapods-hmap/command/hmap.rb)
+
+-
+
+
+
+
-- block 最后一行是返回值,不需要指定 return
-- ruby 脚本语言在编写好代码后也想像其他 GPL 语言一样断点可以,需要安装依赖和相应的代码调整
- - `gem install pry`
- - 文件引入 `require 'pry'`
- - 需要 debug 的地方加上 `binding.pry`
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md
index 0730290..98d39c4 100644
--- a/Chapter1 - iOS/1.74.md
+++ b/Chapter1 - iOS/1.74.md
@@ -1,8 +1,8 @@
## 带你打造一套 APM 监控系统
-> APM 是 Application Performance Monitoring 的缩写,监视和管理软件应用程序的性能和可用性。应用性能管理对一个应用的持续稳定运行至关重要。所以这篇文章就从一个 iOS App 的性能管理的纬度谈谈如何精确监控以及数据如何上报等技术点
+> APM 是 Application Performance Monitoring 的缩写,监视和管理软件应用程序的性能和可用性。应用性能管理对一个应用的持续稳定运行至关重要。所以这篇文章就从一个 iOS App 的性能管理的维度谈谈如何精确监控以及数据如何上报等技术点
-App 的性能问题是影响用户体验的重要因素之一。性能问题主要包含:Crash、网络请求错误或者超时、UI 响应速度慢、主线程卡顿、CPU 和内存使用率高、耗电量大等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。
+App 的性能问题是影响用户体验的重要因素之一。性能问题主要包含:卡顿、Crash、网络请求错误或者超时、UI 响应速度慢、主线程卡顿、CPU 和内存使用率高、耗电量大、Weex/RN/Flutter 页面白屏等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。
本篇文章着重总结了 APM 的原因以及如何收集数据。APM 数据收集后结合数据上报机制,按照一定策略上传数据到服务端。服务端消费这些信息并产出报告。请结合[姊妹篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md), 总结了如何打造一款灵活可配置、功能强大的数据上报组件。
@@ -8094,6 +8094,52 @@ runZoned
-思考:一个工程中,只要写好了测试代码,是不是加载这几个动态库就可以运行测试用例了?
+思考:一个非 UI 测试工程(正常的 App 工程),是不是加载这几个测试相关的动态库,写好测试代码,就可以运行测试用例了?
+
+细节不贴了。Demo App 引入 `XCTest.framework` 后业务代码里即可引入 `#import
+
精准测试助力业务,质量更加稳定。
@@ -1394,3 +1611,5 @@ UITesting 还是建议在核心逻辑且长时间没有改动的情况下去做

WWDC 这张图也很清楚,UI 其实需要的占比较小,还是要靠单测驱动。
+
+##
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.89.md b/Chapter1 - iOS/1.89.md
index dfb663e..dbd4224 100644
--- a/Chapter1 - iOS/1.89.md
+++ b/Chapter1 - iOS/1.89.md
@@ -11,9 +11,9 @@
-## block 本质探索
+## 一、block 本质探索
-### 实验探索
+### 1. 实验探索
Demo
@@ -244,7 +244,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
-### 结论
+### 2. 结论
通过探索发现:
@@ -260,9 +260,9 @@ struct __ViewController__viewDidLoad_block_desc_0 {
-## block 变量捕获
+## 二、block 变量捕获
-### auto 变量捕获
+### 1. auto 变量捕获
> 在 C 和 Objective-C 编程语言中,`auto` 关键字用于声明自动存储期的变量。自动存储期的变量会在定义它们的块(block)或作用域(scope)中自动创建,并在退出该作用域时自动销毁。这是变量存储期的默认行为,因此 `auto` 关键字实际上是可选的,但有时候为了清晰起见,开发者可能会显式使用它。
@@ -322,7 +322,7 @@ c++ 中,在函数内部定义的变量,默认用 **auto** 修饰,叫做自
-### static 变量捕获
+### 2. static 变量捕获
```objective-c
auto NSInteger age = 27;
@@ -354,7 +354,7 @@ printInfoBlock();
-### 全局变量捕获
+### 3. 全局变量捕获
```objective-c
NSInteger age = 27;
@@ -405,7 +405,7 @@ QA:为什么局部变量存在捕获,全局变量不需要捕获?
-### 变量捕获总结
+### 4. 变量捕获总结
block 截获变量可以分为:
@@ -576,9 +576,9 @@ static void __Person__testBlockCapture_block_func_0(struct __Person__testBlockCa
-## block 类型
+## 三、block 类型
-### 类型划分
+### 1. 类型划分
我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下
@@ -619,7 +619,7 @@ block 的类型可以通过 isa 或者 class 方法查看,最终都是继承
-### 如何判断 block 属于什么类型
+### 2. 如何判断 block 属于什么类型
Demo:
@@ -651,7 +651,7 @@ Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 `
-### 总结
+### 3. 总结
| block 类型 | 环境 |
| ------------------- | ------------------------------ |
@@ -691,9 +691,9 @@ int main(int argc, const char * argv[]) {
-## 内存管理
+## 四、内存管理
-### ARC 针对 block 的优化
+### 1. ARC 针对 block 的优化
#### 1. block 作为函数返回值,并且捕获了 auto 变量
@@ -905,7 +905,7 @@ static struct __main_block_desc_0 {
-### block 的 copy、dispose
+### 2. block 的 copy、dispose
```c++
static struct __main_block_desc_0 {
@@ -1004,9 +1004,319 @@ NSLog(@"-touchesBegan:withEvent:");
2024-08-13 20:59:54.313367+0800 BlockExplore[29889:968688] (null)
```
-### block 如何修改变量
+### 3. _Block_release 和 _Block_copy
-#### __block 修饰基本数据类型
+Demo1
+
+```objective-c
+void test(void) {
+
+ int a = 0;
+ void(^__weak weakBlock)(void) = nil;
+ {
+ int b = 2;
+ void(^ __weak weakInnerBlock)(void) = ^{
+ NSLog(@"%d", b);
+ };
+ a = b;
+ weakBlock = weakInnerBlock;
+ }
+ weakBlock();
+}
+```
+
+- 定义了一个局部变量 weakBlock
+
+- 在 `{}` 内定义了一个栈上的 block **__NSStackBlock__** ,block 内访问了定义在 block 外部的 b
+
+- 将 weakInnerBlock 赋值给 weakBlock
+
+- 出了 `{}`,也意味着栈上的 block 作用域结束了。block 会调用 block_release 方法
+
+- 由于出了局部作用域,栈上的 block 被释放了。所以在 `{}` 外调用 weakBlock 则存在野指针风险。编译器也会报警告:`Assigning block literal to a weak variable; object will be released after assignment` 。但可能不崩溃:栈内存虽回收但未被新数据覆盖
+
+
+
+我们来看看 **block_release** 源码
+
+```c++
+void
+_Block_release(const void *src)
+{
+ struct StackBlockClass *self = (struct StackBlockClass *)src;
+ extern const void _NSConcreteStackBlock;
+
+ if (self->isa == &_NSConcreteStackBlock // 必须是栈块类型
+ // A Global block doesn't need to be released
+ && self->flags & BLOCK_HAS_DESCRIPTOR // 必须有描述符结构
+ // Should always be true...
+ && self->reserved > 0) // 引用计数大于0
+ // If false, then it's not allocated on the heap, we won't release auto memory !
+ {
+ self->reserved--;
+ if (self->reserved == 0)
+ {
+ if (self->flags & BLOCK_HAS_COPY_DISPOSE)
+ self->descriptor->dispose_helper(self);
+ free(self);
+ }
+ }
+}
+```
+
+说明:
+
+- `StackBlockClass` 表示栈 block 结构,包含:
+ - isa:指向块类型的指针(栈块为 `__NSConcreteStackBlock`)
+ - flag: 标志位( BLOCK_HAS_DESCRIPTOR 表示有描述符)
+ - reserved:引用计数器
+ - descriptor:包含析构函数 dispose_helper。当 block 捕获了 OC、C++ 对象后,编译器会自动设置此标志
+- 释放条件:
+ - 排除全局 block 和堆 block:全局block `__NSConcreteGlobalBlock` 和堆上的 block 无需释放。
+ - **栈块**:
+ 存储在栈内存中,生命周期与函数作用域绑定。
+ **需要手动管理**:通过 `_Block_copy()` 复制到堆时,需用 `_Block_release()` 平衡引用计数。
+ - **堆块**:
+ 由 `_Block_copy()` 动态分配在堆内存,受 ARC 管理。
+ **自动管理**:ARC 会自动插入 `objc_release()` 调用,使用标准 Objective-C 对象释放机制。
+ - 有效性检验:确保块结构完整且引用计数有效
+- 引用计数管理:
+ - `self->reserved--` 引用计数减 1
+ - 引用计数归0的时候,调用 dispose_helper 方法。释放 block 捕获的对象、内存
+ - 释放 block 结构体内存
+- 一般来说 block 由栈管理,但是被 copy、strong 等强引用作为属性或者参数,则会调用 **_Block_copy** 拷贝到堆上。本函数管理堆化后栈 block 的引用计数
+
+
+
+Demo2
+
+```c++
+void test2(void) {
+
+ int a = 0;
+ void(^__weak weakBlock)(void) = nil;
+ {
+ int b = 2;
+ // 栈 block 赋值给一个 strong或copy 修饰的强引用变量,则会调用 _Block_copy 方法拷贝到堆上,变成堆 block
+ void(^ __strong strongInnerBlock)(void) = ^{
+ NSLog(@"%d", b);
+ };
+ a = b;
+ // 结构体赋值,也就是此时 a 是一个堆上的 block
+ weakBlock = strongInnerBlock;
+ } // 离开作用域,堆上的 block 会自动调用 _Block_release 方法,内部会 free 掉,此时再去调用释放的内存,系统机制会为已经释放的内存填充 0xDEADBEEF 标记,MMU 也会触发缺页异常,发送 crash
+ weakBlock();
+}
+```
+
+现象:上面的代码会稳定 crash。报错:`Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)`
+
+分析:
+
+- 在 `{}` 内定义初始化了一个栈 block
+- 赋值给一个由强指针指向的 strongInnerBlock 变量时,自动触发 `_Block_copy` 方法,复制到堆上,变为堆 block **__NSConcreteMallocBlock**
+- 然后通过结构体赋值的方式,赋值给 `weakBlock`
+- 要出 `{}` 作用域时,则会调用 `_block_release` 方法。堆块的引用计数清零。释放后,堆内存表记为不可访问
+- 此时调用 `weakBlock()` 则会 crash。访问已经释放的堆内存。系统在释放的堆内存上添加保护(如 `EXC_BAD_ACCESS`)
+
+test 和 test2 的本质区别
+
+| 函数 | 内存类型 | 内存回收机制 | 崩溃原因 |
+| :------ | :------- | :---------------------- | :--------------- |
+| `test` | 栈内存 | 系统自动回收(无保护) | 内存可能未被覆盖 |
+| `test2` | 堆内存 | `free()` + 内存保护标记 | 强制触发访问异常 |
+
+
+
+Demo3
+
+
+
+为什么上面的代码会 crash?
+
+- weakBlock 是一个栈 block(__NSConcreteStackBlock)
+- GCD 代码以 block 的形式将延迟任务添加到主队列中
+- `dispatch_block_t` 内部捕获了 block,但捕获的是原始指针,未复制 block
+- GCD 不会阻塞,函数会结束,同时栈 block 会被回收,栈帧销毁。此时 `dispatch_block_t` 内持有其悬挂指针。
+- 3s 后开始执行 dispatch_block_t 所指向的 weakBlock
+- 发现 weakBlock 所指向的栈内存被回收,调用无效指针导致 **EXC_BAD_ACCESS** 崩溃(访问野指针)。
+
+
+
+解决方案:
+
+1. 将栈 block 改为堆 block。block 修饰改为 strong
+
+ ````objective-c
+ void test3(void) {
+ int a = 10;
+ void(^__strong block)(void) = ^ {
+ NSLog(@"%d", a);
+ };
+
+ dispatch_block_t dispatch_block = ^{
+ block();
+ };
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
+ }
+ ````
+
+2. 复制 block 到堆
+
+ ```objective-c
+ void test3(void) {
+ int a = 10;
+ void(^__weak weakBlock)(void) = ^ {
+ NSLog(@"%d", a);
+ };
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), [weakBlock copy]);
+ }
+ ```
+
+3. 栈帧不要回收。开启一个 RunLoop 晚退出3秒
+
+ ```objective-c
+ void test3(void) {
+ int a = 10;
+ void(^__weak weakBlock)(void) = ^ {
+ NSLog(@"%d", a);
+ };
+
+ dispatch_block_t dispatch_block = ^{
+ weakBlock();
+ };
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
+ }
+ ```
+
+
+
+Demo4
+
+
+
+分析:
+
+- block 本质就是结构体
+
+- `void (^__strong strongBlock)(void) = weakBlock;` 看上去左侧是一个 `__strong` 修饰,其实就是结构体,所以不会像对象一样,底层调用 setter 方法。所以 block 的赋值本就是按照结构体的成员变量一个个字段简单赋值而已。
+
+ ```c++
+ struct __block_impl {
+ void *isa; // 指向 Block 类 (栈上)
+ int flags; // 标志位 (栈上)
+ int reserved; // 保留字段 (栈上)
+ void (*invoke)(void); // 函数指针 (代码段)
+ struct __block_descriptor *descriptor; // 描述符指针 (栈上)
+ };
+
+ struct __block_descriptor {
+ unsigned long reserved; // 保留字段 (栈上)
+ unsigned long size; // Block 大小 (栈上)
+ void (*copy)(void); // copy 辅助函数 (代码段)
+ void (*dispose)(void); // dispose 辅助函数 (代码段)
+ };
+ ```
+
+- 当把结构体 block 的 invoke 设为 nil,由于是简单赋值,所以原来的 weakblock 的 invoke 也为 nil
+
+- 所以 strongblock 的 invoke 也为 nil,所以执行 block 底层就是先判断 block 的 isa、然后根据 invoke 指针,找到代码段的函数去执行。此时已经为 nil,再去执行就会 crash
+
+解决方案:将 weakblock copy 一下,即 `void (^__strong strongBlock)(void) = [weakBlock copy];`
+
+
+
+### 4. 栈内存、堆内存保护机制
+
+通过上面 test 和 test2 知道,栈内存、堆内存在释放后继续访问存在不同的表现
+
+#### 1. 栈内存
+
+##### 1. 栈内存的本质上是:
+
+- **线性结构**:栈是连续的内存区域,通过栈指针(SP)管理
+- **自动回收**:函数退出时,只需移动栈指针即可"释放"内存
+- **物理内存不变**:移动栈指针不会立即清除数据,原内存内容保持不变
+
+##### 2. 访问已释放的内存为何可能不崩溃?
+
+- 无硬件保护:CPU 和 MMU(内存管理单元)不跟踪栈帧生命周期
+- 内存数据保留:除非被新的栈帧覆盖,否则数据仍可被读取
+
+##### 3. 不崩溃的深层原因:
+
+- 无页表标记:栈内存页始终标记为:可读写 (PROT_READ|PORT_WRITE)
+- 无隔离机制:不同函数的栈帧共享同一内存页
+- 延迟覆盖:新栈帧写入前,元数据保持有效
+
+
+
+
+
+#### 2. 堆内存
+
+##### 1. 堆内存管理的核心机制:
+
+- malloc -> 向内核申请内存页 -> 设置页表属性 -> 分配内存块
+- free -> 标记内存页属性 -> 加入空闲链表 -> 触发内存页回收
+
+##### 2. free 后的关键操作
+
+内存标记
+
+````objective-c
+// 典型内存分配器实现
+void free(void *ptr) {
+ // 1. 在内存块头部写入魔数(如0xDEADBEEF)
+ *(uint32_t*)(ptr - 4) = 0xDEADBEEF;
+
+ // 2. 通过 mprotect 设置内存页不可访问
+ mprotect(ALIGN_PAGE(ptr), PAGE_SIZE, PROT_NONE);
+}
+````
+
+页表更新
+
+| 状态 | 页表项标志位 | 访问后果 |
+| :--- | :-------------- | :----------- |
+| 正常 | PRESENT+RW+USER | 允许访问 |
+| 释放 | ~PRESENT | 触发缺页异常 |
+
+稳定崩溃的硬件基础:
+
+MMU 介入:当访问 `PROT_NONE` 内存页时:
+
+1. 触发缺页一场(Page Fault)
+2. 内核检查异常地址
+3. 发送 `SIGSEGV` 信号
+4. 进程终止(崩溃)
+
+#### 3. iOS/Macos 优化
+
+##### 1. 堆内存保护优化
+
+- Malloc Scribble:释放后填充`0x55`(调试模式默认启用)
+
+- Zone-based Protection:
+
+ ```objective-c
+ malloc_zone_t *zone = malloc_create_zone(0, 0);
+ malloc_set_zone_name(zone, "Protected Zone");
+ malloc_zone_protect(zone, 1); // 启用保护
+ ```
+
+##### 2. 栈内存的刻意放松
+
+- **性能优先**:避免栈操作时的权限检查
+- **安全边界**:仅防止栈溢出,不保护栈帧间访问
+
+
+
+### 5. block 如何修改变量
+
+#### 1. __block 修饰基本数据类型
```objectivec
typedef void(^MyBlock)(void);
@@ -1178,7 +1488,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
-#### __block 修饰对象
+#### 2. __block 修饰对象
对` __block` 修饰的对象,clang 转换为 c++ 后如下:
@@ -1307,13 +1617,13 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
-#### 什么情况下需要 __block
+#### 3. 什么情况下需要 __block
局部变量:基本数据类型、对象数据类型
-#### 什么情况下不需要 __block
+#### 4. 什么情况下不需要 __block
- 全局变量(不截获)
- 静态全局变量(不截获)
@@ -1323,7 +1633,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
-## `__forwarding` 的设计
+## 五、`__forwarding` 的设计
Demo1
@@ -1587,7 +1897,7 @@ Tips:实现 block 拷贝及其捕获对象的函数是 `_Block_copy`,工作
-## Block 内存引用
+## 六、Block 内存引用
对于` __block` 修饰的变量进行研究
@@ -1803,7 +2113,7 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
-## 循环引用
+## 七. 循环引用
self 是一个局部变量,block 访问 self,即存在捕获变量的效果。
@@ -1817,7 +2127,7 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
可以看到 Person 对象的 dealloc 方法没有执行,里面的打印信息没有输出。
-### ARC 下
+### 1. ARC 下
`__weak`、`__unsafe_unretained` 修饰 `__block` 所修饰的变量。区别在于:
@@ -1880,7 +2190,7 @@ p.block();
-### MRC 下
+### 2. MRC 下
方法1: `__unsafe_retained` 修饰。`__unsafe_unretained typeof(self) weakself = self;`
@@ -1888,7 +2198,7 @@ p.block();
-## 为什么加 weakself、strongself
+## 八、为什么加 weakself、strongself
weakSelf 是为了使 block 不持有 self,避免 Retain Circle 循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。
@@ -1922,7 +2232,9 @@ block 执行完后这个 strongSelf 会自动释放,没有不会存在循环
});
```
-## 总结
+
+
+## 九、总结
1. block 本质是什么?封装了函数调用及其调用环境的 OC 对象。本质实现是一个结构体。
2. `__block` 的作用是什么?可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如`__weak`、`__unsafe_unretained`、`__block`
diff --git a/Chapter1 - iOS/1.91.md b/Chapter1 - iOS/1.91.md
index aef2391..2d9a590 100644
--- a/Chapter1 - iOS/1.91.md
+++ b/Chapter1 - iOS/1.91.md
@@ -1,10 +1,10 @@
# DYLD 及 Mach-O
-> 动态库还是静态库对包体积影响大?
+> 链接动态库还是静态库对 App l包体积影响大?
>
> 动态库、静态库编译链接都做了哪些事情?链接的要素是什么?
>
-> Moudle 是什么?
+> Module 是什么?
>
> 如果这些问题不是很清楚,可以带着问题看看本文。
@@ -1149,6 +1149,44 @@ clang 编译、链接的参数解释如下:
-framework
+
+
+现象:工程编译成功。编译链接后,运行输出打印信息。
+问题:为什么2个同样的静态库,没有链接失败?
+分析:
+- 直接在 Xcode 中手动拖入一个静态库/动态库,本质是将库的路径写到 OTHER_LDFLAGS 后面。Xcode 会按照指定的路径去加载静态库/动态库。
+ ```shell
+ OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking" "/Users/unix_kernel/Desktop/编译链接/LDAndFramework/StaticLibConflictsSolution/StaticLibConflictsDemo/AFNetworking"`
+ ```
+- 但链接器很聪明,当发现同名的库的时候,会优先链接找到的第一个库库,第二个库不会被链接。
+- 即使将 `OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking"` 中的 `-ObjC` 改为 `-all_load` 也不会存在链接问题。本质是链接器对于同名的库,找到第一个后,后面的就停止查找,不会被链接了,所以不存在符号重复相关的问题
+
+
+
+##### 2. Demo2
+
+一个 cocoapods 组织的工程,以 pod 的形式引入了 AFNetworking 库,再将另一个 `libAFNetworking.a` 静态库改名为 `libAFNetworkingCopy.a`,然后引入 Xcode 工程。
+
+
+
+
+现象:工程链接失败。报错:`Issues223 duplicate symbols for architecture x86_64`
+问题:为什么上次的链接成功,这次却链接失败了,报符号重复的错误?
+分析:
+- 不同名称的静态库,就不存在「链接器对于同名的库,找到第一个后,后面的就停止查找,不会被链接了,所以不存在符号重复相关的问题」,也就是会发生冲突
+
+
+
+##### 3. Demo3
+
+- 一个 cocoapods 组织的工程,以 pod 的形式引入了 AFNetworking 库
+- 然后 Xcode 新创建一个静态库,选择 `Static Library`
+- 新创建的静态库就1个 `AFNetworkingMock` 文件。不过类里面还存在一个类 `AFURLSessionManager` 和一个全局函数 `global_function`
+
+现象:工程链接失败。报错:`error: duplicate interface definition for class 'AFURLSessionManager'`
+
+
+问题:为什么上次的链接成功,这次却链接失败了,报符号重复的错误?
+分析:
+
+- 不同名称的静态库,就不存在「链接器对于同名的库,找到第一个后,后面的就停止查找,不会被链接了,所以不存在符号重复相关的问题」,也就是会发生冲突
+
+
+
+#### 2. 解决方案
+
+2个静态库有符号冲突怎么办?且一个静态库没有源码。[llvm-objcopy](https://llvm.org/docs/CommandGuide/llvm-objcopy.html) 这个工具刚好有能力在静态库基础上直接修改符号名称。
+
+##### 1. 收集需要重命名的符号
+通过 **objdump --macho -t ${MACH_PATH}** 就可以输出符号信息。Demo 中需要重命名的符号如下:
+
+
+
+
+
+
+##### 2. 编译 llvm-objcopy 产出工具
+
+关于 LLVM 如何编译运行,或者开发 pass 可以查看 [LLVM 编译运行](./1.102.md#编写-xcode-插件)
+
+打开 LLVM 工程,Scheme 切换为 `llvm-objcopy` 然后编译运行,从 Products 目录下将产物拷贝到个人电脑的 CustomTools 文件夹,后续就可以在终端访问 llvm-objcopy 指令。(因为在 `.zshrc` 文件里配置过 `export PATH=~/CustomTools:$PATH`)
+
+这样就说明已经成功了
+
+
+
+然后改造 Demo 工程静态库的脚本为 **llvm-objcopy --prefix-symbols=FantasticLBP_ ${MACH_PATH}** 去给冲突的符号加前缀 `FantasticLBP_`(当然这个前缀名字可以是 SDK 名称简写、业务线简写,我这里是 Github ID ,验证问题而已)。
+
+结果:但发现运行报错,提示 `llvm-objcopy: command not found`
+
+
+改进:将 `llvm-objcopy` 移动到源码根目录下,现在 `Command not found` 的问题解决了,但是报错: **error: option is not supported for MachO**
+
+说明 `--prefix-symbols` 并不支持 MachO 格式。而 `--redefine-sym` 符合要求。
+
+打开 llvm 源码进行调试,配置启动参数。
+
+发现修改成功
+
+
+上面演示了单个修改符号名称的过程。llvm-objcopy 可以批量修改。
+- 指令改为 `--redefine-syms`。
+- 用一个文件,格式为空格和换行来区别。
+ ```
+ _OBJC_CLASS_$_AFURLSessionManager FantasticLBP__OBJC_CLASS_$_AFURLSessionManager
+ _OBJC_METACLASS_$_AFURLSessionManager FantasticLBP__OBJC_METACLASS_$_AFURLSessionManager
+ _OBJC_IVAR_$_AFURLSessionManager._session FantasticLBP__OBJC_IVAR_$_AFURLSessionManager._session
+ ```
+ 修改后运行,发现批量符号修改成功。
+
+
+
+
+##### 3. 使用工具修改符号名称
+
+利用 llvm-objcopy 的能力,修改重名的符号后测试下。
+
+
+可以看到:
+
+- 符号冲突的问题解决了
+- 但同一个符号存在2处实现,当两个同名类被加载时,运行时随机选择一个
+
+
+
+##### 4. 为什么存在2处类定义?
+
+```mermaid
+graph LR
+A[AFURLSessionManager] --> B[_OBJC_CLASS_$_AFURLSessionManager]
+A --> C[_OBJC_METACLASS_$_AFURLSessionManager]
+A --> D[“__objc_classname” 段中的字符串]
+```
+
+**通过 llvm-objcopy 只是修改了 `_OBJC_CLASS_$` 符号而忽略 `_OBJC_METACLASS_$` 和类名字符串,运行时仍会看到两个类定义,导致冲突未解决**
+而且和运行时系统相关的所有功能都会受影响,比如:Category、KVC/KVO 内部使用类名字符串查找、Archive/Unarchive 崩溃等。
+
+所以这种方式存在巨大风险,这里只是从技术研究角度出发,证明可以这么做,但是影响面太大。
+另外一种方式:2个不同名的静态库,但存在相同符号。可以通过 cocoapods 的配置,将某个静态库改为动态库。因为动态库存在二级命名空间,就可以避免符号重复的问题。
+关于动态库二级命名空间的细节,可以查看[这里](#twoLevelNamespace)
+
+方案 1:单个库转为动态库
+```ruby
+# Podfile
+use_frameworks! # 启用框架支持
+
+target 'YourApp' do
+ pod 'AFNetworking', '~> 4.0' # 默认静态库
+
+ # 将冲突库转为动态框架
+ pod 'ConflictingLib', :modular_headers => true, :linkage => :dynamic
+end
+```
+方案 2:指定模块为动态框架
+```ruby
+dynamic_frameworks = ['ConflictingLib']
+
+target 'YourApp' do
+ # 先声明所有库
+ pods = [
+ 'AFNetworking',
+ 'ConflictingLib',
+ 'OtherLib'
+ ]
+
+ # 动态处理
+ pods.each do |pod|
+ if dynamic_frameworks.include?(pod)
+ pod pod, :modular_headers => true, :linkage => :dynamic
+ else
+ pod pod
+ end
+ end
+end
+```
+实验:我的电脑 cocoapods 版本较低,采用下面写法
+```ruby
+platform :ios, '9.0'
+use_frameworks! # 确保启用框架支持
+
+target 'StaticLibConflictsDemo' do
+ # 动态链接 AFNetworking(兼容所有版本)
+ pod 'AFNetworking', :modular_headers => true
+
+ # 添加后安装钩子脚本
+ post_install do |installer|
+ # 将 AFNetworking 设为动态框架
+ installer.pods_project.targets.each do |target|
+ if target.name == 'AFNetworking'
+ # 设置 Mach-O 类型为动态库
+ target.build_configurations.each do |config|
+ config.build_settings['MACH_O_TYPE'] = 'mh_dylib'
+ config.build_settings['DYLIB_INSTALL_NAME_BASE'] = '@rpath'
+ config.build_settings['LD_RUNPATH_SEARCH_PATHS'] = ['$(FRAMEWORK_SEARCH_PATHS)']
+ end
+ end
+ end
+
+ # 确保框架被正确嵌入
+ installer.pods_project.build_configurations.each do |config|
+ config.build_settings['EMBEDDED_CONTENT_CONTAINS_SWIFT'] = 'YES'
+ end
+ end
+end
+```
+效果:
+
+可以看到不光是解决了符号重复的问题,也解决了同一个类,存在2处实现的问题。
+
+
+```mermaid
+graph LR
+A[主可执行文件] --> B[静态库A]
+A --> C[动态库B]
+C --> D[私有符号空间]
+```
+
+- 静态库:符号直接合并到主可执行文件的全局符号表
+- 动态库:拥有独立的符号空间(LC_ID_DYLIB 标识)
+ - 动态库内部符号相互可见
+ - 外部通过导出符号表(LC_DYSYMTAB)访问
+
+
+##### 5. 符号冲突总结
+###### 1. 最佳实践方案:动态库隔离
+
+利用动态库的二级命名空间特性,将冲突的静态库转换为动态库,实现符号隔离
+
+```ruby
+# Podfile
+use_frameworks! # 关键:启用框架支持
+
+target 'StaticLibConflictsDemo' do
+ # 将冲突库转为动态 framework
+ pod 'ConflictingLib', :modular_headers => true, :linkage => :dynamic
+
+ # 其他静态库
+ pod 'NonConflictingLib'
+end
+```
+
+静态库的 xcconfig 为
+
+
+
+动态库的 xcconfig 为
+
+
+
+###### 2. 源码级解决方案:添加类前缀
+
+```objective-c
+// 原始类名
+@interface AFURLSessionManager : NSObject
+
+// 修改后
+@interface ABC_AFURLSessionManager : NSObject
+```
+
+###### 3. 构建系统级解决方案:Bazel 命名空间
+
+```shell
+objc_library(
+ name = "AFNetworking",
+ namespace = "ABC", # 自动添加前缀
+ srcs = glob(["*.m"]),
+ hdrs = glob(["*.h"]),
+)
+```
+
+
+
+## 六、Strip 流程
静态库/ .o 文件 Strip 流程: 处理 `_DWARF` 段
-
+
Strip 的过程,就是在修改 Mach-O 文件中的内容。
-
-
动态库
-
+
All Symbols
-
+
@@ -1295,7 +1591,7 @@ Non-Global Symbols(非全局符号):
-
+
@@ -1400,7 +1696,7 @@ test.o -o test
Mach header 包括一些基础信息,所以 Mach header 存在冗余(大小端序、CPU 类型等),这也就是为什么静态库比动态库体积大的原因之一。
-
+
```shell
Unix_Kernel ~/Desktop/OCExplore/OCExplore file Person.o
@@ -1416,7 +1712,7 @@ Mach header
动态库在 Mach header 这里有改进,AFNetworking 动态库格式如下:
-
+
将公共的信息放到一起,公用一个 Mach header。
@@ -2258,6 +2554,141 @@ otool -l Person | grep 'ID' -A 5
+
+### 6. 二级命名空间
+在 iOS 系统中,动态库的二级命名空间是一种关键的符号管理机制,它解决了不同库之间可能发生的符号冲突问题。这个机制是 macOS 和 iOS 平台动态链接的核心特性。
+
+
+#### 1. 核心原理
+二级命名空间通过2个维度来唯一标识符号:
+- 符号名称(Symbol Name)
+- 动态库名称(Dylib Identity)
+```
+graph TD
+ A[符号解析] --> B{是否在二级命名空间?}
+ B -->|是| C[符号名称 + 库标识符]
+ B -->|否| D[仅符号名称]
+ C --> E[唯一符号]
+ D --> F[可能冲突]
+```
+
+##### 1. 库标识符(Dylib Identity)
+每个动态库都有一个唯一标识符,通常包含:
+- 安装路径(Install Name)
+- 当前版本号(Current Version)
+- 兼容版本号(Compatibility Version)
+```shell
+# 查看库标识符
+otool -l libExample.dylib | grep -A 3 LC_ID_DYLIB
+
+# 输出示例:
+# cmd LC_ID_DYLIB
+# cmdsize 56
+# name @rpath/libExample.dylib (offset 24)
+# time stamp 1 Wed Dec 31 19:00:01 1969
+# current version 1.0.0
+# compatibility version 1.0.0
+```
+
+##### 2. 使用过程
+当动态链接器(dyld)加载符号的时候:
+- 解析符号引用
+- 确定定义该符号的动态库
+- 使用(符号名、动态库ID)作为唯一键
+- 在符号表中查找唯一匹配
+```mermaid
+sequenceDiagram
+ participant App as 应用程序
+ participant dyld as 动态链接器
+ participant LibA as 库A
+ participant LibB as 库B
+
+ App->>dyld: 请求符号 foo
+ dyld->>LibA: 检查 foo@LibA
+ dyld->>LibB: 检查 foo@LibB
+ dyld-->>App: 返回 foo@LibA
+```
+
+##### 3. 符号表结构
+在二级命名空间下,符号表使用分层结构
+```shell
+Symbol Table
+├── LibA Symbols
+│ ├── foo
+│ ├── bar
+│ └── baz
+├── LibB Symbols
+│ ├── foo
+│ ├── xyz
+│ └── abc
+└── ...
+```
+
+#### 2. 应用场景
+##### 1. 解决符号冲突
+当两个库定义同名符号时:
+```shell
+// libNetwork.dylib
+void process_data() {
+ // 网络处理实现
+}
+
+// libAudio.dylib
+void process_data() {
+ // 音频处理实现
+}
+```
+在二级命名空间作用下:
+- process_data@libNetwork
+- process_data@libAudio
+所以,可以安全的使用2个库,而不会发生符号冲突问题
+
+
+
+##### 2. Xcode 配置
+
+- 启用二级命名空间(默认):`OTHER_LDFLAGS = $(inherited) -twolevel_namespace`
+- 禁用二级命名空间:`OTHER_LDFLAGS = $(inherited) -flat_namespace`
+查看命名空间状态:
+```shell
+# 检查二进制是否使用二级命名空间
+otool -lv YourApp | grep TWOLEVEL
+
+# 输出示例:
+# flags TWOLEVEL
+```
+
+
+
+#### 3. 动态库开发 tips
+
+##### 1. 动态库设计原则
+- 使用唯一前缀命名符号
+ ```c++
+ // 良好的命名实践
+ void TaoBaoNetwork_processData() {
+ // 实现
+ }
+ ```
+- 限制导出符号数量
+- 明确声明依赖关系
+- 符号可见性控制
+ ```c++
+ // 只导出必要的符号
+ __attribute__((visibility("default")))
+ void public_api();
+
+ // 隐藏内部实现
+ __attribute__((visibility("hidden")))
+ void internal_function();
+ ```
+
+##### 2. 运行时路径管理
+使用适当的路径变量:
+- `@executable_path`: 可执行文件所在目录
+- `@loader_path`: 当前加载模块所在目录
+- `@rpath`: 运行时搜索路径
+
## 九、xcframework
@@ -2787,15 +3218,103 @@ end
### 1. 定义
+在 iOS/macOS 开发领域的编译链接体系中,Module 是一项由 Clang/LLVM 提供的**语义化头文件封装**技术,其核心目标是**通过强隔离的预编译单元解决传统 `#include` 机制的头文件污染与低效问题**
+
+编译器实现:
+
+```mermaid
+graph LR
+ A[Module] --> B[Clang Module]
+ A --> C[Swift Module]
+ B --> D[.modulemap 描述文件]
+ B --> E[.pcm 预编译二进制]
+ C --> F[.swiftmodule 接口文件]
+```
+
+特征:
+
+- **原子性**:头文件集合的编译边界(不可部分导入)
+- **自包含性**:显式声明导出符号(`export`)与依赖(`requires`)
+- **隔离性**:宏定义(`#define`)不泄漏到导入方
+- **预编译能力**:可序列化为 `.pcm`(Precompiled Module)加速编译
+
+
+
一个 Module 是机器代码和数据的最小单位,可以独立于其他代码单元进行链接。
通常,Module 是通过编译单个源文件生成的目标文件。例如,当前的 `test.m` 被编译成目标文件 `test.o` 时,当前的目标文件就代表了一个 Module 。
-但是,Module 在调用的时候会产生**开销**,比如我们在使用一个静态库的时候。(这个开销是什么接下去会讲)
+但是,Module 在调用的时候会产生**开销**,比如我们在使用一个**静态库**的时候。
+
+
+
+### 2. .pcm 文件
+
+#### 1. 结构
+
+Module 生成的 `.pcm`(Precompiled Module)文件是 Clang 对 C++ Module 的**预编译二进制表示**,其内容远非简单的头文件文本封装,而是包含编译器所需的完整语义信息。
+
+`.pcm` 文件包含:
+
+- 模块元数据
+
+ - **模块标识**:模块名、导出符号列表(`export` 声明的函数/类)
+
+ - **编译环境指纹**:
+
+ ```shell
+ Target: x86_64-apple-darwin22.4.0
+ SDK: MacOSX13.3.sdk
+ Language: C++17
+ ```
+
+ - **时间戳与版本号**:确保与源码版本一致
+
+- AST 抽象语法树:**完整语法树序列化**:将头文件解析后的 AST 以二进制形式存储,通常占 `.pcm` 文件的 **60-70%**(主要膨胀源)
+
+- 语义信息:
+
+ - 符号可见性
+ - 内联函数代码生成
+ - ...
+
+- 依赖关系图
+
+ - **显式依赖**:通过 `import` 直接导入的其他模块(如 `import std.core;`)
+ - **隐式依赖**:递归包含的所有头文件(如 `
-如何做到该工具做到像系统自带的 `objdump` 一样,在终端命令行使用:
+#### 2. 变成终端可使用的能力
+
+QA:如何做到该工具做到像系统自带的 `objdump` 一样,在终端命令行使用:
- 在电脑根目录新建 `CustomTools` 文件夹
- 将上面编译后的产物复制进去
- 在 `.zshrc` 文件里将路径添加进去。`export PATH=~/CustomTools:$PATH`
- 编辑 `.zshrc` 文件后,在终端执行 `souce .zshrc`
-- 即可使用
+- 即可使用,在终端输入: **hmapdump ${hmapFileFullPath}**
效果如下:
@@ -3530,29 +4197,100 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成
### 6. hmap 助力提升 iOS 项目编译速度
-已经编写好生成 `.hmap` 文件的能力了,那如何使用生成的 `.hmap` 文件?
+本节讨论:**如何为静态库自定义 hmap 开启编译加速**?
-- Xcode `Build Setting` 中 `Use Header Maps` 为 NO
-- `Header Search Path` 设置生成的 hmap 文件路径
+为了方便观察,开启2组对照实验。
-实践操作下。把上面静态库无法走 Header Maps 带来的问题解决掉。
+#### 1. 实验一:静态库 + Header Search Paths + App 编译
-第一步,编写 hmap 所需要的 json 信息。
+##### 1. 静态库设置
-第二步,利用 `HMapWriter` 能力,根据 json 生成静态库所需要的 `StaticLibUsage.hmap`
+- 第一步:Xcode 新建项目,选择 **Static Library **。为了方便对照,命名为:**HMapStaticLib-HeaderSearchPath**
+- 第二步:根目录下创建 **Sources** 文件夹,多创建几个存在继承关系的类
+- 第三步:把这几个类的头文件导入到静态库头文件中
+- 第四步:**Build Phases - Headers** 中设置静态库头文件为 **Public**,允许外部直接访问。其他几个头文件设为 **Private**
+- 第五步:为了方便操作,不用每次创建文件夹,手动拖 **.a** 文件和 **.h** 文件很低效。编写 shell 脚本处理。指定到 **Build Phases - Run Script** 中
-第三步,在静态库 Xcode 项目中,关闭 `Use Header Maps`(设置为 NO),同时修改 `Header Search Paths` 为生成 `StaticLibUsage.hmap` 路径。
+整体目录结构和设置如下:
-第四步,编译使用静态库的 App 工程。最后比较静态库走自定义 `.hmap` 前后的编译耗时。
-
-
-
-可以看到静态库使用了自定义的 Header Maps 文件后,使用静态的 App 前后,编译耗时减少了1.1s,节省了57%。
+
-Demo 及其演示代码见 [HMapStaticLibApp](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapStaticLibApp) 和 [HMapStaticLib](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapStaticLib)。
+##### 2. App 以 Header Search Paths 方式编译链接静态库
+- 新建 App 工程。在工程目录下创建 **StaticLib** 文件夹
+- 将上面脚本得到静态库产物 **HMapStaticLib-HeaderSearchPath** 拷贝到 **StaticLib** 文件夹下
+- App 工程打开,Build Settings - Header Search Paths** 中配置:`${SRCROOT}/HMapBenchMark-HeaderSearchPath/StaticLib/HMapStaticLib-HeaderSearchPath/Headers`
+- App 工程的 `ViewController.m` 中,`#import "HMapStaticLib_HeaderSearchPath.h"` 引入静态库的公共头文件,然后使用里面的类
+- 编译运行,导出编译日志。查看编译时间.
+
+注意:因为我的 Demo 都是在一个 Xcode 工程,不同的 **TARGETS**,所以即使打包好的静态库产物,复制到 App 工程 **StaticLib** 文件夹下,系统在编译的时候还是会以源码的形式编译。
+会看到以下的编译日志。
+
+
+解决办法:
+- 方法1: 创建不同的 Xcode 工程
+- 方法2: 还是同一个 Project,不同的 Targets,选中当前的 Target,**Edit Scheme**,在 **Build** 下取消勾选 **Find Implicit Depedencies**
+
+
+问题修复完,最终编译链接效果如下:
+
+编译日志里时间先放着。等以 HeaderMap 编译链接实现结束一起对比看看时间节省了多少。
+
+
+
+
+#### 2. 实验二:静态库 + HeaderMap + App 编译
+
+##### 1. 静态库设置
+
+- 第一步:Xcode 新建项目,选择 **Static Library **。为了方便对照,命名为:**HMapStaticLib-HeaderMap**
+- 第二步:根目录下创建 **Sources** 文件夹,多创建几个存在继承关系的类
+- 第三步:把这几个类的头文件导入到静态库头文件中
+- 第四步:**Build Phases - Headers** 中设置静态库头文件为 **Public**,允许外部直接访问。其他几个头文件设为 **Private**
+- 第五步:根据源码中头文件目录,创建一个 **HMapFile.json** 文件。以数组的形式写入信息,3个元素,分别为:Key、Prefix、Suffix。利用 LLVM 开发的 hmapmaker 能力,将 json 转换为 hmap 文件
+
+- 第六步:**Build Settings - Use Header Maps** 中设为 **NO**,**Header Search Paths** 设为:`${SRCROOT}/HMapStaticLib-HeaderMap/hmap`(hmap 文件所在目录)
+- 第七步:**Build Phases - Headers** 中设置静态库头文件为 **Public**,允许外部直接访问。其他几个头文件设为 **Private**
+- 第八步:为了方便操作,不用每次创建文件夹,手动拖 **.a** 文件和 **.h** 文件很低效。编写 shell 脚本处理。指定到 **Build Phases - Run Script** 中
+
+整体目录结构和设置如下:
+
+
+
+
+
+##### 2. App 以 HeaderMap 方式编译链接静态库
+
+- 新建 App 工程。在工程目录下创建 **StaticLib** 文件夹
+- 将上面脚本得到静态库产物 **HMapStaticLib-HeaderMap** 拷贝到 **StaticLib** 文件夹下
+- App 工程打开,Build Settings - Header Search Paths** 中配置:`${SRCROOT}/HMapBenchMark-HeaderMap/StaticLib/HMapStaticLib-HeaderMap/Headers`
+- App 工程的 `ViewController.m` 中,`#import "HMapStaticLib_HeaderMap.h"` 引入静态库的公共头文件,然后使用里面的类
+- 编译运行,导出编译日志。查看编译时间.
+
+注意:因为我的 Demo 都是在一个 Xcode 工程,不同的 **TARGETS**,所以即使打包好的静态库产物,复制到 App 工程 **StaticLib** 文件夹下,系统在编译的时候还是会以源码的形式编译。
+会看到以下的编译日志。
+
+
+解决办法:同一个 Project,不同的 Targets,选中当前的 Target,**Edit Scheme**,在 **Build** 下取消勾选 **Find Implicit Depedencies**
+
+
+最终编译链接效果如下:
+
+
+
+
+#### 3. 数据分析及结论
+
+**可以看到静态库先用 Header Search Paths 的方式,App 编译耗时6.5s。使用了自定义的 Header Map 文件后,App 编译耗时6.1s。编译耗时减少了0.4s,节省了6.6%。这是只有1个静态库且只有7个头文件的情况下测试得到的数据。真实项目中,如果静态库数量越多、项目文件目录长且嵌套复杂、头文件数量越多,以自定义 hmap 的方式将会在编译阶段节省更多的时间**
+
+
+
+源码查看:
+- 静态库 + HeaderSearchPath:[HMapStaticLib-HeaderSearchPath](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapStaticLib-HeaderSearchPath) 和 [HMapBenchMark-HeaderSearchPath](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapBenchMark-HeaderSearchPath)
+
+- 静态库 + hmap:[HMapStaticLib-HeaderMap](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapStaticLib-HeaderMap) 和 [HMapBenchMark-HeaderMap](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapBenchMark-HeaderMap)
### 7. 工程化问题
@@ -3562,6 +4300,18 @@ Demo 及其演示代码见 [HMapStaticLibApp](https://github.com/FantasticLBP/Bl
- 生成后的 hmap 文件如何使用
如果每个工程项目都这么做,效率有点低,所以需要站在工程化的角度去设计,如何优化?
+完整流程如下:
+```mermaid
+graph TD
+ A[Podfile] --> B[post_install Hook]
+ B --> C[遍历所有 Pod]
+ C --> D[收集头文件映射]
+ D --> E[生成 hmap 文件]
+ E --> F[修改 xcconfig]
+ F --> G[关闭默认 hmap]
+ G --> H[App 编译加速]
+```
+
Cocoapods 提供了很多钩子,可以自定义编写 Ruby 脚本。
- HooksManager 注册 cocoapods 的 `post_install` 钩子
- 通过 `header_mappings_by_file_accessor` 遍历所有头文件和 `header_dir`,由header_dir/header.h和header.h为key,以头文件搜索路径为value,组装成一个Hash
+
#### 2. 如何找到全局变量地址
第一步,先用指令查看全局变量的地址值。 `objdump --macho -s main`,可以看到地址值为 `0x100008000`
-
+
diff --git a/Chapter2 - Web FrontEnd/2.32.md b/Chapter2 - Web FrontEnd/2.32.md
index aa36ba6..8911ee2 100644
--- a/Chapter2 - Web FrontEnd/2.32.md
+++ b/Chapter2 - Web FrontEnd/2.32.md
@@ -37,13 +37,13 @@ render () {
## 生命周期
-
+
## 状态管理
Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍了。解决了各个组件之间数据传递的复杂问题。先看看 Redux 进行状态管理的一个流程吧。
-
+
### 开发步骤
@@ -109,7 +109,7 @@ Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍
Redux-thunk 是 redux 里面常用的一个中间件。中间件?针对谁和谁的中间?对 action 和 store 的中间件。本来 action 只可以返回一个对象,灵活性较低,但是采用了 redux-thunk 之后,action 不仅可以传递对象,还可以传递函数。 action 通过 dispatch 传递给 store。 dispatch 判断 action 的类型,如果是对象则直接传递;如果是函数则直接执行。
-
+
- 异步函数不应该放在组件的生命周期函数里面。复杂的业务逻辑和异步函数适合拆分。目前主流的解决方案有2种中间件:redux-thunk、redux-saga。采用不同的策略
@@ -237,4 +237,1481 @@ class Welcome extends React.Component {
- Vue 设计思想:How easy it can be。React:How corrct it can be 和 all in js(css写法也在用 js 控制,比如 styled-component)
-在 React、React Native、Vue、Weex、Flutter 等声明式开发思想的框架下,UI = F(state)。一个状态唯一对应一个 UI(但一个 UI 不一定对应一个 state),关心 state 即可
\ No newline at end of file
+在 React、React Native、Vue、Weex、Flutter 等声明式开发思想的框架下,UI = F(state)。一个状态唯一对应一个 UI(但一个 UI 不一定对应一个 state),关心 state 即可
+
+
+
+
+
+## React 核心渲染流程 Fiber 架构
+
+React 在 16 之后引入了 Fiber 架构,旨在解决长列表的渲染卡顿问题。几个关键问题
+
+### hostRenderFiber 里的 updateQueue.shared.pending 怎么理解?为什么需要 shared 属性?
+
+
+在 React 的 Fiber 架构中,`hostRenderFiber.updateQueue.shared.pending` 涉及更新队列的核心设计,尤其是为了支持并发渲染和多优先级更新。我们可以分两部分理解:
+
+
+1. 先拆解概念:`hostRenderFiber.updateQueue.shared.pending`
+- **`hostRenderFiber`**:指「宿主环境渲染 Fiber 节点」(如对应 DOM 元素的 Fiber),负责管理该节点的渲染和更新状态。
+- **`updateQueue`**:Fiber 节点上的「更新队列」,用于存储待处理的更新(如 `setState`、`useState` 产生的更新)。React 通过更新队列实现更新的合并、优先级排序和批量处理。
+- **`shared`**:更新队列中「共享部分」的容器,用于在多场景下共享更新数据(核心是支持并发模式)。
+- **`pending`**:`shared` 中存储「待处理更新」的链表(环形链表),指向当前等待被处理的更新队列尾部。
+
+
+2. `pending` 的作用
+`pending` 是更新队列中**未被处理的更新链表**,本质是一个「环形单向链表」。其设计目的是高效地收集和处理更新:
+- 当产生新更新(如调用 `setState`)时,React 会创建一个 `Update` 对象,并将其添加到 `pending` 链表的尾部(通过指针调整形成环形)。
+- 在「协调阶段(Reconciliation)」,React 会从 `pending` 中取出所有待处理更新,进行合并(如多个相同状态的更新合并为一个),最终计算出最新的状态。
+
+环形链表的设计能让更新的添加和取出操作更高效(时间复杂度 O(1)),避免频繁的数组操作开销。
+
+
+3. 为什么需要 `shared` 属性?
+`shared`(共享)是为了支持 React 的**并发渲染机制**而设计的,核心解决「多场景下更新队列的共享与同步」问题:
+
+在并发模式中,React 可能同时存在多个「更新任务」(如高优先级的用户输入更新和低优先级的列表渲染更新),且这些任务可能在不同的「调度阶段」(如主线程、后台任务)中被处理。此时需要一个「共享的更新容器」,让不同的调度逻辑、优先级任务能访问到同一份待处理更新,避免更新丢失或冲突。
+
+
+具体来说,`shared` 的核心作用是:
+- **跨优先级共享更新**:高优先级更新和低优先级更新可以共用同一个 `pending` 队列,确保低优先级更新不会被高优先级任务覆盖。
+- **跨 Fiber 共享更新**:在 `current Fiber`(当前渲染树的 Fiber)和 `workInProgress Fiber`(正在构建的新 Fiber)交替工作时,`shared` 确保两者能访问到相同的待处理更新,避免状态不一致。
+- **支持中断与恢复**:并发渲染中,低优先级任务可能被高优先级任务中断,`shared.pending` 中的更新不会因中断而丢失,恢复时可继续处理。
+
+
+总结
+- `hostRenderFiber.updateQueue.shared.pending` 是宿主 Fiber 节点上「共享更新队列中待处理的更新链表」,用于收集和暂存未处理的更新。
+- `shared` 属性是为了适配并发渲染的多优先级、多阶段调度需求,确保更新队列能在不同场景(如不同优先级任务、不同 Fiber 树)中被安全共享和同步,避免更新丢失或冲突。
+
+
+注意:「在并发模式中,React 可能同时存在多个更新任务」并不是说明 React 在渲染的时候存在多个线程,**JS 是单线程模型**这一客观事实没有改变,这里说的是更新任务,是任务队列,并不是任务线程。
+
+- JS 单线程的限制是 “同一时间只能执行一个任务”,但可以通过任务队列(如宏任务、微任务)控制任务的执行时机。
+- React 并发模式下,不同更新任务(如用户输入、列表渲染)会被标记不同的优先级(通过 Scheduler 包实现)。高优先级任务(如点击事件)可以打断低优先级任务(如长列表渲染)的执行 —— 此时低优先级任务的中间状态不会提交到 DOM,而是被暂停,等高优先级任务完成后,低优先级任务再从暂停处恢复执行。
+
+
+这种 “中断 - 恢复” 机制让开发者感觉 “多个任务在同时处理”,但本质上仍是主线程按优先级依次执行,没有多线程参与。而 shared.pending 正是为了在这种 “中断 - 恢复” 中,确保低优先级任务被打断时,其未处理的更新不会丢失(因为更新存在共享队列中,恢复时可继续读取)。
+
+
+
+### shared 与双缓冲机制(current/WIP 切换)的关系
+shared(共享)是为了支持 React 的并发渲染机制而设计的」可以理解为 React Fiber 架构为了解决长列表渲染问题,借鉴 iOS、Android 设计了双缓冲机制,系统在 current 和 WIP 之间切换,从而避免长时间阻塞主线程,提升用户体验吗?
+
+双缓冲机制(current 与 workInProgress Fiber 树):这是 React Fiber 架构的核心设计之一,类似 iOS/Android 的双缓冲(前台显示缓冲区、后台绘制缓冲区)。
+- current Fiber:当前已渲染到 DOM 的 Fiber 树,代表 “当前屏幕上的 UI”。
+- workInProgress Fiber(WIP):正在内存中构建的新 Fiber 树,用于计算更新后的 UI。
+当 WIP 树构建完成后,React 会通过切换 current 指针(指向 WIP 树)完成更新,避免直接修改 current 树导致的 UI 闪烁或不完整(因为构建过程可能被中断)。
+这一机制的核心是避免渲染过程中的中间状态暴露给用户,解决 “长时间阻塞主线程导致的 UI 卡顿”。
+
+shared 的作用:支持双缓冲中的更新共享双缓冲机制中,current 和 WIP 是两棵独立的 Fiber 树,但它们需要共享同一份更新队列—— 否则如果更新只存在于 current 树,WIP 树构建时会丢失更新;反之如果只存在于 WIP 树,若 WIP 树被中断丢弃(高优先级任务抢占后可能重新构建),更新也会丢失。
+- shared 正是为这两棵树提供了一个 “共享的更新容器”:
+- 无论更新是在 current 树还是 WIP 树中产生,都会被添加到 shared.pending 队列。
+- 当需要构建 WIP 树时,会从 shared.pending 中读取所有待处理更新,确保不遗漏。
+即使 WIP 树被中断重建,新的 WIP 树仍能从 shared.pending 中获取最新更新。
+简单说:双缓冲机制解决 “渲染过程不阻塞 UI”,shared 解决 “双缓冲切换时更新不丢失”,二者配合支撑了并发渲染的可靠性。
+
+### 「无论更新是在 current 树还是 WIP 树中产生,都会被添加到 shared.pending 队列」那么是不是 update 任务会有个标记用于区分当前任务是 current Fiber 树的任务还是 WIP Fiber 树的任务?请分别列举,WIP和 Current 不同的任务提交后,后续是如何执行的
+
+在 React 中,Update 任务(即更新对象)并没有专门的标记区分其来自 current Fiber 树还是 workInProgress Fiber 树。这是因为 shared.pending 队列的设计目标就是 “消除来源差异”—— 无论更新来自哪个 Fiber 树,最终都需要被统一处理,确保状态的一致性。一言以蔽之就是:**update 任务不管是 WIP Fiber tree 产生的,还是 Current Fiber Tree 产生的,只用于服务于下一次(也就是 WIP Fiber Tree),类似双缓冲机制,用于构建下一次需要显示的内容**
+
+- current 树是 “只读的”:它代表当前屏幕上的 UI,任何更新都不会直接修改 current 树(否则会导致用户看到中间态、闪烁或不一致)。
+- WIP 树是 “可写的构建区”:所有更新(无论来自 current 树的用户交互,还是 WIP 树构建中的嵌套逻辑)最终都会被收集到 shared.pending 队列,然后在 WIP 树的构建过程中被统一处理(计算新状态、生成新的 Fiber 节点)
+
+### “挂载”和“插入”
+
+React 中经常会看到挂载和插入2个术语。2者有啥区别呢?
+
+| 概念 | 含义 | 发生阶段 | 操作对象 | 是否触发浏览器重排 |
+| :--------------------- | :-------------------------------- | :--------- | :----------------------------------------------------------- | ------------------ |
+| **挂载(mounting)** | 在内存中构建DOM节点间的父子关系 | Render阶段 | React Element 对象定时器间隔:{delay}ms
+ setDelay(Number(e.target.value))} + /> +修改输入框可改变间隔,观察计数变化
+
+
+git 存在3个区域:
+
+| **区域** | **别名** | **存储位置** | **核心功能** | **操作命令** |
+| :--------- | :---------- | :---------------- | :----------------- | :----------- |
+| **工作区** | Workspace | 本地目录 | 开发者直接编辑文件 | 手动编辑文件 |
+| **暂存区** | Index/Stage | `.git/index` 文件 | 准备下次提交的变更 | `git add` |
+| **版本库** | Repository | `.git/objects` | 永久存储的提交历史 | `git commit` |
+
+代码回滚用的是 git reset 指令。区别在于参数:
+
+- git reset --hard commitId:将代码回滚到工作区,本次代码(文件)的变动都会被舍弃。
+- git reset --soft commitId:将代码回滚到暂存区,本次代码(文件)的变动不会被舍弃,也就是相当于执行了 `git add` 的操作
+
+一般而言,为了安全和灵活,都采用 `git reset --soft commitId` 指令。
+
+
+
+HEAD:用来指向分支的最后一次提交对象。
+
+如果想要回到上一步,可以用 `HEAD~1` 代替具体的 commitID。
+
+如果想要丢弃还需要进一步处理:
+
+- git reset HEAD .`:则将暂存区的提交回滚掉。相当于没有执行 `git add`(只有本地新改动的数据)
+- `git checkout -- .`: 将本地新改动的数据丢弃掉。
+
+
+
+git checkout:
+
+- 切换分支,例如:`git checkout master`
+- 重新存储工作区文件。例如:`git checkout -- .`
+
+
+
+## git 原理
+
+- git 以 key、value 的形式存储。
+- 二进制仓库的底层数据结构为树
+- 本次修改文件的 hash 值为 key
+- 本次修改文件的压缩版本为 value
+
+
+
+### 1. git 对象存储
+
+git 将存储对象的40位 HASH 分为2部分:
+
+- 头2位作为文件夹
+
+- 后38位作为对象文件名。结构为:`.git/objects/hash[0:2]/hash[2:40]`
+
+
+
+ 比如:`gitDemo/.git/objects/22/13d05bf4b8cfc7ee323af3ac427ad2fa14da88`
+
+
+
+QA: 为什么要设计这样的目录结构(Hash 值总40位,前2位为文件夹名称,后38为文件名称),而不直接用40位 hash 值作为文件名?
+
+- 部分文件系统对目录下的文件数量有限制。例如,FAT32 限制单目录下的最大文件数量位 65535 个
+- 部分文件系统查找文件属于线性查找,目录下的文件越多,查找越慢
+
+
+
+### 2. git add 的本质
+
+
+
+`git ls-files -s` 指令查看暂存区文件 。
+
+git add 的本质就是内容哈希化。
+
+- 输入:文件内容(二进制流)
+- 处理:
+ - 添加头部信息:**"blob " + 内容字节数 + "\0"**
+ - 计算 SHA-1: **SHA1(header + content)**
+- 输出:40位十六进制的哈希值
+
+比如对 “Hello” 进行哈希计算:
+
+1. 原始内容:`b"Hello"`
+2. 添加头部
+ - 内容长度:5字节
+ - 构造头部:`b"blob 5\0"`
+3. 完整数据:`b"blob 5\0Hello"`
+4. SHA-1 计算。`HashUtils.sha1(data).hexdigest()`
+
+### 3. 如何计算 git 哈希
+
+使用指令 **git hash-object -w ./index.txt** 即可。
+
+将得到的40位长度的哈希,拆为2部分,前2位为文件夹名称,后38位为文件名。
+
+
+
+Demo1:
+
+对 `index.txt` 文件使用 `git hash-object -w index.txt` 指令计算哈希。
+
+
+
+Demo2:
+
+ `index.txt`文件内容没有改变, 继续计算哈希。发现哈希值一致
+
+结论:
+
+- **只要文件内容不变,hash 值不变。**
+- **每次计算一次哈希,都会在 `.git/objects/` 文件夹下多出一个子文件夹。**
+
+Demo3:
+
+对 `index.txt` 文件内容进行调整,`git hash-object -w index.txt` 指令计算哈希
+
+
+
+结论:
+
+- **文件内容改变了,hash 值变了。**
+- **文件内容改变后,生成新的 hash 值,同时会在 `.git/objects/` 文件夹下多出一个新的文件夹**。文件夹名称为新的哈希值的前2位,文件夹内文件名称为新的哈希值的后38位。
+
+
+
+### 4. 模拟 git add
+
+利用指令 **`git update-index --add --cacheinfo 100644 {FileHashValue} index.txt`** 将工作区的文件添加到缓存区
+
+- `--add`: 强制将指定文件添加到暂存区(即使文件不存在于工作目录中)
+- `--cacheinfo`: 手动指定文件的「模式(mode)+ 哈希值(hash)+ 路径(path)」来更新暂存区,用于添加那些不在工作目录中的文件
+- `100644`: 文件模式(file mode),表示这是一个普通文件(非执行文件、非符号链接等)。Git 中常见模式:`100644`(普通文件)、`100755`(可执行文件)、`120000`(符号链接)等
+- `{FileHashValue}`: 文件内容的 SHA-1 哈希值(40 位字符串),对应 Git 数据库(`.git/objects`)中存储的文件内容
+- `index.txt`:最终在暂存区中记录的文件名(路径)
+
+Demo:
+
+- 为了模拟 `git add` 的效果,先把仓库中的 `.git` 文件夹删掉
+- 利用指令 `git hash-object -w index.txt` 计算出 index.txt 的哈希值
+- 利用指令 `git update-index --add --cacheinfo 100644 2213d05bf4b8cfc7ee323af3ac427ad2fa14da88 index.txt` 将 `index.txt` 和计算出来的哈希值写入到暂存区
+- 利用指令 `git status` 查看是否成功写入到暂存区
+- 继续按照上述2个流程(git hash-object 和 git update-index)将剩余2个文件进行模拟添加到暂存区
+
+整体效果如下图:
+
+
+
+
+
+### 5. git 只有文件,没有目录
+
+为什么进一步强调 **git 只有文件,没有目录**的概念,做一个实验。
+
+第一步:上述步骤生成了3份 git hash,分别保存在 `.git/objects/` 目录下。
+
+
+
+第二步:接下去将暂存区的文件生成一颗树。使用指令 **git write-tree**
+
+第三步:查看生成的树信息。使用指令 `git cat-file -p {TreeHash}`
+
+
+
+第四步:使用指令 **`git read-tree --prefix=FantasticLBP/ 7f7bbe6285c9c767aeaa1aedba6dcf5324774bc8`** 将指定的 tree 对象内容读取到当前索引中,并将其所有文件放在名为 `FantasticLBP/` 的目录下。
+
+此时文件目录为:
+
+
+
+第五步:此时的效果为,暂存区里面存在另一份目录名为 `FantasticLBP/` 的暂存区信息。但是此时实体文件夹下并不存在。使用指令 `git checkout -- .` 便可以从暂存区恢复。
+
+恢复后的目录结构为:
+
+
+
+
+
+结论:
+
+- 通过在暂存区里面重新构建一颗树,便可以使用 checkout 恢复出来
+- 所以在使用代码重置功能的时候,最好使用 `git reset --soft head~1 ` 的方式进行,因为更加灵活。使用 `--hard` 就没有暂存区的记录了。
+
+### 6. git commit 的本质
+
+上述步骤:
+
+- 生成了3份文件的 git hash
+- 同时又将3份文件写入到暂存区,得到一个暂存区 tree 的哈希: `ce8045cf527ef892b8ad19c7b0bbac889fc44c59`。
+
+接下去为了探索 git commit 的本质,我们用 **`echo 'init commit' | git commit-tree ce8045cf527ef892b8ad19c7b0bbac889fc44c59`** 来完成提交。
+
+**`git commit-tree`**:Git 底层命令,用于创建一个新的提交对象(commit object)。它需要一个 tree 对象作为基础,并通过标准输入接收提交信息。命令执行成功后,会输出新创建的提交对象的 SHA-1 哈希值。
+
+
+
+可以看到:
+
+git commit 提交后也是会生成一个新的提交对象(commit object),新的提交对象也会在 `.git/objects/` 目录下存在。
+
+
+
+### 7. git rebase
+
+假如当前在分支 featureA 上,执行 `git rebase master` 后的效果,等价于将寻找到 featureA 和 master 的最近公共祖先节点,然后从 最近公共祖先节点到 featureA 的最新节点之间的 commit 都会被拆开,拼接到 master 分支最后面的 commit 上,重新组合成一个新的 commit。
+
+也就是搞清楚2个对象:
+
+- 当前在什么分之上?featureA
+- git rebase 指令后跟什么分之?master
+
+执行完的效果就是:master + featureA(从 master 和 featureA 的最近公共祖先处截断、拼接在后面)
+
+```shell
+C0 <--- C1 <--- M1 <--- M2 [master]
+ \
+ \
+ A1 <--- A2 <--- A3 [featureA, HEAD]
+```
+
+- 寻找公共祖先
+
+ ```shell
+ LCA = git merge-base featureA maste` # 返回 C1
+ ```
+
+- 提取提交差异
+
+ ```shell
+ commits = git log C1..featureA --pretty=format:"%H` # 获取从 C1 到 featureA 的 commit 差异,得到: [A1, A2, A3]
+ ```
+
+- 重置到目标基点
+
+ ```shell
+ git reset --hard M2 # featureA分支指针临时指向M2
+ ```
+
+- 按顺序重新重置提交
+
+ ```shell
+ git cherry-pick A1 # 创建新提交A1' (父提交=C1)
+ git cherry-pick A2 # 创建新提交A2' (父提交=A1)
+ git cherry-pick A3 # 创建新提交A3' (父提交=A2)
+ ```
+
+最终结果
+
+```shell
+C0 <--- C1 <--- M1 <--- M2 [master]
+ \
+ \
+ A1' <--- A2' <--- A3' [featureA, HEAD]
+```
+
+
+
+git rebase 还可以用于一些提交信息的处理,比如 `git rebase -i {CommitHash}`
+
+
+
+git rebase 还可以将多次 commit 合并为1次,也可以修改某次 commit 的信息等等,具体的可以看指令后面的注释。
+
+
+
+### 8. 冲突展示
+
+#### 1. 双路展示策略
+
+git 在 merge 或者 rebase 都会存在冲突的可能,关于文件内容冲突默认是按照双路合并的形式进行展示的。这样子信息有限
+
+可以通过下面指令,查看当前的冲突展示规则:
+
+```shell
+git config merge.conflictstyle
+```
+
+如果没有任何输出,则说明没有任何设置,默认就是**双路展示策略**。
+
+冲突标记仅显示**当前分支**(`<<<<<<< HEAD`)和**合并分支**(`>>>>>>>
+
+当前 case 下,我需要选择左侧为结果,所以点击了 "Choose left"。下面区域为合并后的结果。在终端还有来一次确认过程。做到 double check。
+
+
+
+属于 y 后,相当于选择了结果,接下去再进行 add、commit 等操作。
+
+
+
+### 9. git log -p
+
+#### 1. 基础指令
+
+**`git log -p commitId`** 是一个组合命令,用于**详细展示特定提交及其之前的提交历史,并显示每个提交的代码差异**
+
+说明:从指定的 `commitId` **开始回溯**,按时间倒序列出其**祖先提交**
+
+示例如下
+
+
+
+
+
+#### 2. 输出信息结构
+
+每个提交显示2块信息:
+
+- 提交元信息
+
+ 比如
+
+ ```shell
+ commit 0d614db5d3ba7ae2606de696a3627819b2368378 # 提交哈希
+ Author: FantasticLBP
+
+ - 使用 `git stash show -p stash@{0}` 查看**存储修改内容**。其中 `-p` 是 `--patch` 的缩写,显示完整差异(补丁格式),`stash{0}` 指定要查看的储藏引用(0 表示最近一次储藏)
+
+
+
+
+
+ - 也可以使用 `git stash` 指令,会自动生成默认消息,包含清晰的 WIP 日志。也是标准推荐做法。
+
+ ```shell
+ # 标准命令
+ git stash
+ # 等价于
+ git stash push "WIP on $(git branch --show-current): $(git log -1 --format=%s)"
+ ```
+
+ - 使用 `git stash apply stash@{1}` 来应用某次具体的 stash 栈里的信息。注意:新 stash 的 序号更早,早期的由近到远依次+1
+
+
+
+ - 如果从 stash 里取出的代码不满意,如何恢复到 之前的状态?使用组合命令 **git stash show -p stash@{1} | git apply -R`** 来达到反向应用补丁的效果。
+
+
+
+ 管道符 `|` ,将前一个命令的输出作为后一个命令的输入。比如:
+
+ ```shell
+ # 应用了错误的存储
+ git stash apply stash@{1}
+
+ # 发现需要撤销
+ git stash show -p stash@{1} | git apply -R
+ ```
+
+ - stash 由于是栈,所以有 push、pop 能力。push 就是往栈里加,pop 就是移除。完整指令为 **`git stash pop stash@{0}`**
+
+
+
+### 11. git ignore
+
+在开发中经常会遇到有些类型的文件不需要提交到远端进行多人协作,比如 iOS 开发中的 cocoapods install 后的 pods 目录,或者 NodeJS 开发中的 node_modules 目录,git 也设计了该口子,允许使用 **.gitignore ** 文件用于指定 Git 版本控制系统应忽略的文件和目录,避免将临时文件、编译产物或敏感信息纳入版本控制。
+
+#### 1. 核心规则
+
+- 每行一个规则,支持通配符:
+ - `*` 匹配任意字符(除路径分隔符外)
+ - `**` 匹配任意层级目录
+ - `?` 匹配单个字符
+ - `[abc]` 匹配指定字符
+- 路径规则:
+ - 以 `/` 开头:仅匹配项目根目录(如 `/temp.log`)
+ - 以 `/` 结尾:仅匹配目录(如 `build/`)
+- 取反规则:用 `!` 取消忽略(如 `!src/important.log`
+
+
+
+#### 2. 最佳实践
+
+- 在项目根目录创建 ``.gitignore` 文件
+
+- 根据项目和语言类型,搜索一个标准的 `.gitignore` 模版,将内容复制进去
+
+- 如果项目初始化的时候没有添加完全部的 `.gitignore` 文件内容。再到后来开发的过程中,给 `.gitignore` 里面添加了内容,但后来添加的 `.gitignore` 已经比较晚了,当前提示不允许追踪的文件,还是会被 git 追踪。并且已经提交到暂存区了。
+
+ 因为都存在于本地暂存区了,所以可以使用指令 **`git update-index --assume-unchanged {FileName}`** 来告诉 git,不要追踪暂存区的 FileName 该文件
+
+
+
+- 顾名思义,pre-push、post-push 分别是在 push 之前、push 之后触发的钩子。所以其他几个钩子类似
+
+- 本次演示继续在之前的 Demo 上演示,使用 Pre-Commit 钩子,去掉拓展名。注释掉其他代码,添加一句打印输出
+
+
+
+注意:这里可以使用 shell、也可以使用 python、JS、Ruby 等脚本
+
+
+
+
+
+#### 1. 代码格式与静态检查
+
+- 钩子:`pre-commit`
+
+- 作用:提交前自动运行 ESLint、Prettier、Flake8 等工具,检查语法错误、代码风格或安全漏洞。若检查失败,则阻止提交
+
+ ```shell
+ # pre-commit 脚本片段
+ eslint --fix --ext .js,.ts src/ # 自动修复并检查JS/TS文件
+ ```
+
+
+
+#### 2. git 日志规范化
+
+- 钩子:`commit-msg`
+
+- 作用:检查提交信息是否符合约定格式(如必须包含前缀 `[Feat]`、`[Fix]` 或关联问题编号)。违规时终止提交
+
+ ```shell
+ # commit-msg 脚本片段
+ if ! grep -qE "^(FEAT|FIX|DOC):" "$1"; then
+ echo "提交信息必须以 FEAT/FIX/DOC 开头!"
+ exit 1
+ fi
+ ```
+
+#### 3. 单元测试与代码覆盖率
+
+- 钩子:`pre-push`
+- 作用:推送前运行测试套件(不管是单元测试也好,还是精准测试也好),必须保证全部的测试 case 通过,且代码覆盖率达到95%以上才可以合并。确保新代码不破坏现有功能。测试失败则阻止推送。
+
+
+
+#### 4. 自动部署测试环境
+
+- 钩子:`post-receive`(服务端)
+- 作用:代码推送到远程仓库后,自动将代码同步至服务器目录,触发部署流程(如更新网站文件)
+
+
+
+#### 5. CI/CD 流程
+
+- 钩子:`post-receive`(服务端)
+- 作用:推送完成后通知 CI 工具(如 Jenkins、GitLab CI)执行流水线任务(构建、测试、部署)
+
+
+
+### 15. 本地仓库对应远端仓库是1对多的关系
+
+为了模拟本地和远端是1对多的关系:
+
+- 将之前的 Demo 的 `.git` 文件夹删掉。模拟一个干净的文件夹,没有 git 信息
+- 创建2个文件夹,模拟 git 远端服务器的 repo 信息。分别为 gitDemoServer1、gitDemoServer2
+- 分别在 gitDemoServer1、gitDemoServer2 文件夹执行 `git init --bare` ,**在服务器文件夹初始化裸仓库**
+- 在 gitDemo 终端路径下,执行 `git init` 初始化本地仓库
+- 继续在 gitDemo 终端路径下分别执行 **`git remote add origin /Users/unix_kernel/Desktop/gitDemoServer1` **和** `git remote add origin2 /Users/unix_kernel/Desktop/gitDemoServer2`** ,为了给当前的 repo 配置多个远端仓库
+- 最后执行 `git remote -v` 指令查看当前 repo 的 remote 信息
+
+
+
+说明:
+
+- 裸仓库特点:
+
+ - 没有工作目录(不能直接编辑文件)
+ - 目录内容直接包含 Git 内部文件(无 `.git` 隐藏文件夹)
+
+- `git remote add origin /Users/unix_kernel/Desktop/gitDemoServer1`
+
+ - `origin` 和 `origin2` 是远程仓库的别名
+ - 路径可以是绝对路径(推荐)或相对路径
+
+- 本地只有1个分支,远端分支名也不一定相同。所以需要告诉 git 本地分支如何与远端进行关联
+
+ ````shell
+ git remote add -t main origin https://github.com/FantasticLBP/GitDemo
+ ````
+
+ - `git remote add` :想本地仓库添加要 track 的远程仓库
+ - `-t main` : 指定本地要 track 远程仓库中的哪个分支
+ - `origin`: 远程仓库的名字
+ - `https://github.com/FantasticLBP/GitDemo`: 远程仓库的地址
+
+
diff --git a/Chapter8 - Algorithm/8.1.md b/Chapter8 - Algorithm/8.1.md
new file mode 100644
index 0000000..c8be63f
--- /dev/null
+++ b/Chapter8 - Algorithm/8.1.md
@@ -0,0 +1,190 @@
+## leetcode 968. 监控二叉树
+
+## 题目描述
+给定一个二叉树,我们在树的节点上安装监控。
+
+节点上的每个摄影头都可以监视其父对象、自身及其直接子对象
+
+计算监控树的所有节点所需的最小监控数量。
+
+2个例子:
+
+case1:
+
+``` shell
+ 1
+ |
+ 2 监控
+ / \
+ 3 4
+
+```
+
+Case2:
+
+```shell
+ 1
+ /
+ 2 监控
+ /
+ 3
+ /
+ 4 监控
+ \
+ 5
+```
+
+## 分析
+
+- 一颗监控可以覆盖:当前节点、当前节点的父节点、当前节点的所有子节点3层
+
+- 本题目要求使用最小数量的监控解决问题。那么也就是贪心思维的体现,那么问题来了,什么策略才可以使用最小数量的监控?
+
+ - 一棵二叉树中,叶子节点数量肯定是最多的。所以要想最小数量安装监控,优先选择非叶子节点上安装监控。这也是本题中贪心思维的体现。
+ - 如果最后一层叶子节点不安装监控,那么肯定是叶子节点的父节点安装监控,同理叶子节点的父节点的父节点,也会被监控覆盖到,所以叶子节点的父节点的父节点不用安装监控。于是
+ 叶子节点的父节点的父节点的父节点就必须安装监控。
+
+
+
+- 对于二叉树一定是采用递归法或者迭代法解决,本题选择递归法。
+
+- 因为要根据叶子节点的状态来反推父节点的状态,所以采用后续遍历。
+
+- 另外需要根据左右子树的返回值来判断,所以递归函数需要返回值
+
+- 为了方便定义3个状态。
+
+ - 未设置监控 NotCovered = 0
+ - 被监控覆盖 IsCoverd = 1
+ - 设置监控 SetCamera = 2
+
+- 如何定义递归函数?
+
+ - 返回值就是数字,存在3种情况,也就是上面定义的3种状态
+ - 递归函数的终止条件是什么?遇到叶子节点的左右空子树的情况下,该选用什么状态?思考下:共3种情况
+ - 空节点选用 “未覆盖 NotCovered”?
+ ❌ 问题 :叶子节点必须安装监控来覆盖空节点
+ 如果空节点是 NotCovered,那么空节点的父节点,也就是叶子节点,必须设置监控,才可以保证叶子节点的左右子节点才可以被监控覆盖到。
+ ❌ 结果:摄像头数量过多,不符合最小化原则
+ 这和题目要求的最小监控数量不契合
+ - 空节点选用 “安装监控 SetCamera”?
+ ❌ 问题:叶子节点自动变为被覆盖状态 (IsCovered = 1)
+ 如果空节点是 SetCamera,那么空节点的父节点,也就是叶子节点,状态一定是被覆盖 IsCoverd,如果叶子节点是 IsCoverd,
+ ❌ 结果:叶子节点的父节点不需要安装监控,破坏了"叶子节点的父节点安装监控"的最优策略
+ 反推叶子节点的父节点就不需要安装监控了(因为监控必须间隔设置,覆盖 - 监控 - 覆盖 - 监控 这样的形式),那这个情况也和题目的预设条件不满足。
+ - 所以空节点应该选用 “被监控覆盖 IsCoverd” 这个状态
+ ✅ 正确:叶子节点为未覆盖状态 (NotCovered = 0)
+ 空节点被监控覆盖 IsCoverd,那么空节点的父节点,也就是叶子节点就不需要设置监控,
+ ✅ 结果:叶子节点的父节点必须安装监控,符合贪心策略
+ 也就是未设置监控 NotCovered = 0,那么叶子节点的父节点才需要设置监控 SetCamera
+
+## 状态转换情况
+说明:
+- 未设置监控 NotCovered = 0
+- 被监控覆盖 IsCoverd = 1
+- 设置监控 SetCamera = 2
+
+| leftChild | rightChild | root | count=0 | 说明 |
+| --------- | ---------- | ---- | ------- | ------------------------------------------------------------ |
+| 1 | 1 | 0 | +0 | 空节点(叶子节点的子节点)必须同时处于被覆盖状态 |
+| | | | | |
+| 0 | 0 | 2 | +1 | 普通节点不管 left、right 只要有1个处于未覆盖状态,那么父节点一定要设置监控才可以“罩着”下面的子节点 |
+| 0 | 1 | 2 | +1 | |
+| 1 | 0 | 2 | +1 | |
+| 2 | 0 | 2 | +1 | |
+| 0 | 2 | 2 | +1 | |
+| | | | | |
+| 2 | 2 | 1 | +0 | 其他情况,父节点都是处于被监控覆盖的状态,不需要增加监控 |
+| 2 | 1 | 0 | +0 | |
+| 1 | 2 | 1 | +0 | |
+| 2 | 1 | 1 | +0 | |
+
+
+
+## 贪心思想
+
+本题目贪心体现在(监控数量 count = 0):
+- 空节点(叶子节点的子节点):处于被监控状态(IsCovered 状态),没有安装监控。count 不变
+- 叶子节点:不设置监控,处于 NotCovered 状态。需要父节点罩着。count 不变
+- 叶子节点的父节点:安装监控,处于 SetCamera 状态,count++
+- 叶子节点的爷爷节点:处于被监控状态(IsCovered 状态),没有安装监控。count 不变
+- ♻️ 循环往复
+
+
+
+## 代码实现(JS 为例)
+
+```js
+/**
+ * @param {TreeNode} root
+ * @return {number}
+ */
+var minCameraCover = function(root) {
+ let count = 0
+ const Mode_NotCovered = 0 // 未设置监控
+ const Mode_IsCovered = 1 // 被监控覆盖
+ const Mode_SetCamera = 2 // 设置监控
+
+ const traverse = (node) => {
+ // 空节点视为已覆盖(推到过程见上面注释部分)
+ if (node === null) return Mode_IsCovered
+
+ // 后序遍历
+ let left = traverse(node.left)
+ let right = traverse(node.right)
+
+ // 如果左右孩子有一个未被覆盖,当前节点需要安装摄像头
+ if (left === Mode_NotCovered || right === Mode_NotCovered) {
+ count++
+ return Mode_SetCamera
+ }
+ // 如果左孩子和右孩子都是覆盖状态,那么父节点处于非覆盖状态
+ if (left === Mode_IsCovered && right === Mode_IsCovered) {
+ return Mode_NotCovered
+ }
+ // 如果左孩子或者右孩子是设置监控状态,那么父节点处于监控覆盖状态
+ if (left === Mode_SetCamera || right === Mode_SetCamera) {
+ return Mode_IsCovered
+ }
+ return Mode_NotCovered
+ }
+
+ let rootResult = traverse(root)
+ // 检查根节点状态,如果未被覆盖则需要增加一个摄像头
+ if (rootResult === Mode_NotCovered) count++
+ return count
+};
+```
+提交后发现空间复杂度一般,去掉定义的状态和更加清楚的 if 分支,代码如下
+
+```js
+var minCameraCover = function(root) {
+ let count = 0
+
+ const traverse = (node) => {
+ // 空节点视为已覆盖(推到过程见上面注释部分)
+ if (node === null) return 1
+
+ // 后序遍历
+ let left = traverse(node.left)
+ let right = traverse(node.right)
+
+ // 如果左右孩子有一个未被覆盖,当前节点需要安装摄像头
+ if (left === 0 || right === 0) {
+ count++
+ return 2
+ }
+ // 如果左孩子或者右孩子是设置监控状态,那么父节点处于监控覆盖状态
+ if (left === 2 || right === 2) {
+ return 1
+ }
+ return 0
+ }
+
+ let rootResult = traverse(root)
+ // 检查根节点状态,如果未被覆盖则需要增加一个摄像头
+ if (rootResult === 0) count++
+ return count
+};
+```
+
diff --git a/Chapter8 - Algorithm/8.2.md b/Chapter8 - Algorithm/8.2.md
new file mode 100644
index 0000000..c9c13f4
--- /dev/null
+++ b/Chapter8 - Algorithm/8.2.md
@@ -0,0 +1,161 @@
+# 《剑指 Offer》字符串“左旋”、“右旋”里的数学秘密
+
+> 为什么要写本篇文章?看上去这是 easy 级别的题目。但“点是面的缩影,面是点的抽象”,单独一道题似乎很简单,我们可以比较轻松做出来。但是这一类题目的本质是什么?不要处于混沌的状态解决了题目,但下次遇到类似的,还是要迟疑思考一会儿。本篇文章带你吃透问题的本质和背后的数学推导。
+
+
+
+## 题目描述
+字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
+
+请定义一个函数实现字符串左旋转操作的功能。比如:
+- 输入字符串 "abcdefg" 和数字 2
+- 该函数将返回左旋转 2 位后的结果 "cdefgab"
+
+请实现该函数
+
+## 结论
+只要是字符串的左旋、右旋,都用整体逆序 + 部分逆序的方法,也可以是部分逆序 + 整体逆序。
+
+## 分析
+关于反转(也就是逆序),有2个 feature:
+- 反转的可逆性:反转(反转(x)) = x
+ - 类似负负得正。比如:'123' 经过一次反转后为 '321', '321' 再经过一次反转为 '123'
+- 反转的可组合性:反转 (A + B) = 反转(B) + 反转(A)
+ - '123456' 按照长度为3进行拆分为2部分,s1 + s2。s1 = '123', s2 = '456
+ - 先对后面的 s2,也就是'456' 反转得到 '654',即 s2' = '654'
+ - 再对前面的 s1,也就是'123' 反转得到 '321',即 s1' = '321'
+ - 再对 s1' 和 s2' 进行拼接, s1' + s2' = '654321'
+ - 观察发现 s1' + s2' 就等于对整体 s1 + s2 逆序后的结果。
+
+再来观察看看:左旋、右旋题目要求的是什么?
+
+前提:假设一个逆序函数,可以将 x 作为输入,输出是 x'。这个共识、前提成立,我们再进行后续的推导:
+1. 原始字符串通过 k 为分割,可以拆分为: A + B。题目求的是什么? B + A
+2. 思考:根据上面的2个特性,通过什么变化可以从 A + B,得到 B + A 呢?
+不难得出结论,有2个方案:
+- 先整体逆序,`逆序 (A + B) = 逆序(B) + 逆序(A) = B' + A'`
+- 再局部逆序,`逆序(B') + 逆序(A') = B + A`
+
+结论:我们发现这时候的结果刚好满足题目要求。所以这些方法都是有迹可循的,符合数学群论中的 **“逆运算”** 和 **“运算律”** 的思想
+
+
+## 方法1:先整体逆序,再局部逆序
+1. 原始字符串通过 k 拆分为: A + B 的结构,左旋后变为 B + A
+2. 先整体逆序。`逆序(A) = A' 逆序(B) = B'。大的结构还是逆序(A + B) = B' + A'`
+3. 再局部逆序:`逆序(B') + 逆序(A') = B + A`
+
+结论:我们发现这时候的结果刚好满足题目要求,不管是先局部再整体,还是先整体再局部,效果是等价的。
+
+```javascript
+ // 方法1: 先整体,再部分
+ const rotateLeft = (message, k) => {
+ const length = message.length
+ let datasource = Array.from(message)
+
+ const reverse = (datasource, fromIndex, toIndex) => {
+ for (; fromIndex < toIndex; fromIndex++, toIndex--) {
+ let temp = datasource[fromIndex]
+ datasource[fromIndex] = datasource[toIndex]
+ datasource[toIndex] = temp
+ }
+ }
+
+ // 1. 先整体逆序
+ reverse(datasource, 0, length - 1)
+
+ // 2. 再局部逆序
+ // 先对左半部分逆序
+ /*
+ 已知:leftTo = k,leftLength = fullLength - k,求 leftTo?
+ 注意:此时的 length 不等于 k,因为左旋的前半段为 k,剩余的后半段 length 为完整的 length - k
+ leftTo - leftFrom + 1 = leftLength
+ leftTo = leftLength + leftFrom - 1
+ 代入得到:
+ leftTo = (fullLength - k) + 0 - 1 = length - k - 1
+ */
+ reverse(datasource, 0, length - k - 1)
+ // 再对右半部分逆序
+ /*
+ 已知:rightTo = fullLength - 1,rightLength = k,求 rightFrom?
+ rightTo - rightFrom + 1 = rightLength
+ rightFrom = rightTo - rightLength + 1
+ 代入得到:
+ rightFrom = (length - 1) - k + 1 = length - k
+ */
+ reverse(datasource, length - k, length - 1)
+
+ // 3. 字符串数组拼接为结果
+ return datasource.join('')
+ }
+```
+
+
+## 方法2:先部分逆序,再整体逆序
+思考:能不能先局部逆序,再整体逆序?
+分析:继续用上面的思路推导下
+1. 原始字符串通过 k 拆分为: A + B 的结构,左旋后变为 B + A
+2. 先局部逆序:`逆序(A) + 逆序(B) = A' + B'`
+3. 再整体逆序。`逆序(A + B) = 逆序(B) + 逆序(A)`。但是此刻我们的输入为: A' + B',
+
+所以等价于:`逆序(A' + B') = 逆序(B') + 逆序(A') = B + A`
+
+结论:我们发现这时候的结果刚好满足题目要求,不管是先局部再整体,还是先整体再局部,效果是等价的。
+
+```javascript
+ // 方法2: 先部分,再整体
+ const rotateLeft1 = (message, k) => {
+ const length = message.length
+ let datasource = Array.from(message)
+
+
+ const reverse = (datasource, fromIndex, toIndex) => {
+ for (; fromIndex < toIndex; fromIndex++, toIndex--) {
+ let temp = datasource[fromIndex]
+ datasource[fromIndex] = datasource[toIndex]
+ datasource[toIndex] = temp
+ }
+ }
+
+ // 1. 再局部逆序
+
+ // abcdefg -> gfedc ba ->
+ // 先对左半部分逆序
+ /*
+ 已知:leftTo = k,leftLength = fullLength - k,求 leftTo?
+ 注意:此时的 length 不等于 k,因为左旋的前半段为 k,剩余的后半段 length 为完整的 length - k
+ leftTo - leftFrom + 1 = leftLength
+ leftTo = leftLength + leftFrom - 1
+ 代入得到:
+ leftTo = (fullLength - k) + 0 - 1 = length - k - 1
+ */
+ reverse(datasource, 0, k - 1)
+ // 再对右半部分逆序
+ /*
+ 已知:rightTo = fullLength - 1,rightLength = k,求 rightFrom?
+ rightTo - rightFrom + 1 = rightLength
+ rightFrom = rightTo - rightLength + 1
+ 代入得到:
+ rightFrom = (length - 1) - k + 1 = length - k
+ */
+ reverse(datasource, k, length - 1)
+
+ // 2. 先整体逆序
+ reverse(datasource, 0, length - 1)
+
+ // 3. 字符串数组拼接为结果
+ return datasource.join('')
+ }
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter8 - Algorithm/chapter8.md b/Chapter8 - Algorithm/chapter8.md
new file mode 100644
index 0000000..e69de29
diff --git a/SUMMARY.md b/SUMMARY.md
index cd36511..d74a057 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -117,6 +117,8 @@
* [112、Swift 枚举值内存布局](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.112.md)
* [113、Swift 结构体和类的内存布局](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.113.md)
* [114、Swift 优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.114.md)
+ * [115、AI 对端上的赋能](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.115.md)
+ * [147. Rust 在移动端可以做什么](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.147.md)
* [Chapter2 - Web FrontEnd](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/chapter2.md)
* [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md)
* [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md)
@@ -162,6 +164,9 @@
* [42、JS原型链与Objective-C内存布局不能说的秘密](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.43.md)
* [43、Vue 核心原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.44.md)
* [44、浏览器渲染原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.45.md)
+ * [45、内存管理之垃圾回收与内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.46.md)
+
+
* [Chapter3 - Server](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/chapter3.md)
* [1、利用分页和模糊查询技术实现一个App接口](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.1.md)
* [2、网页端扫码登录实现原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.2.md)
@@ -246,4 +251,6 @@
* [24、短视频刷多了会变笨吗?怎么样提升我们的表达和思辨能力](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.25.md)
* [25、对于”文件“的新认识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.26.md)
* [26、敏捷软件开发和 Scurm Master](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.27.md)
- * [27、工作感悟和职场思考](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.28.md)
\ No newline at end of file
+ * [27、工作感悟和职场思考](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.28.md)
+* [Chapter8 - Algorithm](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/chapter8.md)
+ * [1、Leetcode968. 监控二叉树](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/8.1.md)
\ No newline at end of file
diff --git a/assets/AFNetworkingIsDynamicLib.png b/assets/AFNetworkingIsDynamicLib.png
new file mode 100644
index 0000000..8653c33
Binary files /dev/null and b/assets/AFNetworkingIsDynamicLib.png differ
diff --git a/assets/APM-CoreAnimationPipeline.png b/assets/APM-CoreAnimationPipeline.png
new file mode 100644
index 0000000..b36eed4
Binary files /dev/null and b/assets/APM-CoreAnimationPipeline.png differ
diff --git a/assets/AddStaticLibIntoApp.png b/assets/AddStaticLibIntoApp.png
new file mode 100644
index 0000000..0dd8197
Binary files /dev/null and b/assets/AddStaticLibIntoApp.png differ
diff --git a/assets/AppCompileStaticLibWithHeaderMap.png b/assets/AppCompileStaticLibWithHeaderMap.png
new file mode 100644
index 0000000..155a6f2
Binary files /dev/null and b/assets/AppCompileStaticLibWithHeaderMap.png differ
diff --git a/assets/AppCompileStaticLibWithHeaderSearchPath.png b/assets/AppCompileStaticLibWithHeaderSearchPath.png
new file mode 100644
index 0000000..393d7b2
Binary files /dev/null and b/assets/AppCompileStaticLibWithHeaderSearchPath.png differ
diff --git a/assets/AppLinkDynamicLibNotSign.png b/assets/AppLinkDynamicLibNotSign.png
new file mode 100644
index 0000000..c5f147f
Binary files /dev/null and b/assets/AppLinkDynamicLibNotSign.png differ
diff --git a/assets/BlockAssignIsValueAssign.png b/assets/BlockAssignIsValueAssign.png
new file mode 100644
index 0000000..50a9f77
Binary files /dev/null and b/assets/BlockAssignIsValueAssign.png differ
diff --git a/assets/CocdSignIphoneLog.png b/assets/CocdSignIphoneLog.png
new file mode 100644
index 0000000..ac4dce3
Binary files /dev/null and b/assets/CocdSignIphoneLog.png differ
diff --git a/assets/CocoapodsCommandWithSciptFile.png b/assets/CocoapodsCommandWithSciptFile.png
new file mode 100644
index 0000000..3d5ea65
Binary files /dev/null and b/assets/CocoapodsCommandWithSciptFile.png differ
diff --git a/assets/CocoapodsHMapInfo.png b/assets/CocoapodsHMapInfo.png
new file mode 100644
index 0000000..8132397
Binary files /dev/null and b/assets/CocoapodsHMapInfo.png differ
diff --git a/assets/CocoapodsHMapLocalInstall.png b/assets/CocoapodsHMapLocalInstall.png
new file mode 100644
index 0000000..479bf21
Binary files /dev/null and b/assets/CocoapodsHMapLocalInstall.png differ
diff --git a/assets/CocoapodsHMapPostInstall.png b/assets/CocoapodsHMapPostInstall.png
new file mode 100644
index 0000000..349dbf4
Binary files /dev/null and b/assets/CocoapodsHMapPostInstall.png differ
diff --git a/assets/CocoapodsHMapPostInstall2.png b/assets/CocoapodsHMapPostInstall2.png
new file mode 100644
index 0000000..3cab6fe
Binary files /dev/null and b/assets/CocoapodsHMapPostInstall2.png differ
diff --git a/assets/CocoapodsHMapV1.png b/assets/CocoapodsHMapV1.png
new file mode 100644
index 0000000..5c73744
Binary files /dev/null and b/assets/CocoapodsHMapV1.png differ
diff --git a/assets/CocoapodsHMapV2.png b/assets/CocoapodsHMapV2.png
new file mode 100644
index 0000000..5b25a8f
Binary files /dev/null and b/assets/CocoapodsHMapV2.png differ
diff --git a/assets/CocoapodsHMapV2Test.png b/assets/CocoapodsHMapV2Test.png
new file mode 100644
index 0000000..f808bfd
Binary files /dev/null and b/assets/CocoapodsHMapV2Test.png differ
diff --git a/assets/CocoapodsScriptCodeSignDetails.png b/assets/CocoapodsScriptCodeSignDetails.png
new file mode 100644
index 0000000..ff7fbd0
Binary files /dev/null and b/assets/CocoapodsScriptCodeSignDetails.png differ
diff --git a/assets/CocoapodsSelfDefinedScript.png b/assets/CocoapodsSelfDefinedScript.png
new file mode 100644
index 0000000..e9cf916
Binary files /dev/null and b/assets/CocoapodsSelfDefinedScript.png differ
diff --git a/assets/CodeSignAppWithDynamicLibLog.png b/assets/CodeSignAppWithDynamicLibLog.png
new file mode 100644
index 0000000..d91d7c2
Binary files /dev/null and b/assets/CodeSignAppWithDynamicLibLog.png differ
diff --git a/assets/CodeSignByCocoapodsScripts.png b/assets/CodeSignByCocoapodsScripts.png
new file mode 100644
index 0000000..c782400
Binary files /dev/null and b/assets/CodeSignByCocoapodsScripts.png differ
diff --git a/assets/CodeSignEntitlements.png b/assets/CodeSignEntitlements.png
new file mode 100644
index 0000000..1341ecb
Binary files /dev/null and b/assets/CodeSignEntitlements.png differ
diff --git a/assets/DumpProvisionProfile.png b/assets/DumpProvisionProfile.png
new file mode 100644
index 0000000..a84866a
Binary files /dev/null and b/assets/DumpProvisionProfile.png differ
diff --git a/assets/DuplicateSymbolWhenLinkTwoStaticLib.png b/assets/DuplicateSymbolWhenLinkTwoStaticLib.png
new file mode 100644
index 0000000..06c65db
Binary files /dev/null and b/assets/DuplicateSymbolWhenLinkTwoStaticLib.png differ
diff --git a/assets/FastlaneMatchfile.png b/assets/FastlaneMatchfile.png
new file mode 100644
index 0000000..e8eab68
Binary files /dev/null and b/assets/FastlaneMatchfile.png differ
diff --git a/assets/FastlaneScriptCodeGen.png b/assets/FastlaneScriptCodeGen.png
new file mode 100644
index 0000000..5380457
Binary files /dev/null and b/assets/FastlaneScriptCodeGen.png differ
diff --git a/assets/FastlaneStructure.png b/assets/FastlaneStructure.png
new file mode 100644
index 0000000..04d5192
Binary files /dev/null and b/assets/FastlaneStructure.png differ
diff --git a/assets/GitHash.png b/assets/GitHash.png
new file mode 100644
index 0000000..7557ced
Binary files /dev/null and b/assets/GitHash.png differ
diff --git a/assets/GitHooksTemplate.png b/assets/GitHooksTemplate.png
new file mode 100644
index 0000000..33a093c
Binary files /dev/null and b/assets/GitHooksTemplate.png differ
diff --git a/assets/GitLogPCommand.png b/assets/GitLogPCommand.png
new file mode 100644
index 0000000..67ec5ac
Binary files /dev/null and b/assets/GitLogPCommand.png differ
diff --git a/assets/GitOpenDiffPanel.png b/assets/GitOpenDiffPanel.png
new file mode 100644
index 0000000..fe4e4da
Binary files /dev/null and b/assets/GitOpenDiffPanel.png differ
diff --git a/assets/GitOpenDiffResult.png b/assets/GitOpenDiffResult.png
new file mode 100644
index 0000000..1f35373
Binary files /dev/null and b/assets/GitOpenDiffResult.png differ
diff --git a/assets/GitPreCommitHook.png b/assets/GitPreCommitHook.png
new file mode 100644
index 0000000..bfbe0c0
Binary files /dev/null and b/assets/GitPreCommitHook.png differ
diff --git a/assets/GitRebaseCommand.png b/assets/GitRebaseCommand.png
new file mode 100644
index 0000000..cc6ba7e
Binary files /dev/null and b/assets/GitRebaseCommand.png differ
diff --git a/assets/GitRemoveTrackingWithIgnore.png b/assets/GitRemoveTrackingWithIgnore.png
new file mode 100644
index 0000000..85c7037
Binary files /dev/null and b/assets/GitRemoveTrackingWithIgnore.png differ
diff --git a/assets/GitStashApply.png b/assets/GitStashApply.png
new file mode 100644
index 0000000..06bd2bb
Binary files /dev/null and b/assets/GitStashApply.png differ
diff --git a/assets/GitStashApplyRevert.png b/assets/GitStashApplyRevert.png
new file mode 100644
index 0000000..aad4b24
Binary files /dev/null and b/assets/GitStashApplyRevert.png differ
diff --git a/assets/GitStashCommand.png b/assets/GitStashCommand.png
new file mode 100644
index 0000000..cabb549
Binary files /dev/null and b/assets/GitStashCommand.png differ
diff --git a/assets/GitStashPCommand.png b/assets/GitStashPCommand.png
new file mode 100644
index 0000000..5b157b9
Binary files /dev/null and b/assets/GitStashPCommand.png differ
diff --git a/assets/GitWorkFlow.png b/assets/GitWorkFlow.png
new file mode 100644
index 0000000..b7ddd52
Binary files /dev/null and b/assets/GitWorkFlow.png differ
diff --git a/assets/HMapDumpRes.png b/assets/HMapDumpRes.png
index 89807d3..6fb2ae8 100644
Binary files a/assets/HMapDumpRes.png and b/assets/HMapDumpRes.png differ
diff --git a/assets/HMapDumpSystemTools.png b/assets/HMapDumpSystemTools.png
index 29e0b3a..34b6f5f 100644
Binary files a/assets/HMapDumpSystemTools.png and b/assets/HMapDumpSystemTools.png differ
diff --git a/assets/HMapDumpXcodeConfig.png b/assets/HMapDumpXcodeConfig.png
index 8c50a0a..36ddbeb 100644
Binary files a/assets/HMapDumpXcodeConfig.png and b/assets/HMapDumpXcodeConfig.png differ
diff --git a/assets/LLDBArchitecture.png b/assets/LLDBArchitecture.png
new file mode 100644
index 0000000..6830834
Binary files /dev/null and b/assets/LLDBArchitecture.png differ
diff --git a/assets/LLDBWorkflow.png b/assets/LLDBWorkflow.png
new file mode 100644
index 0000000..270597f
Binary files /dev/null and b/assets/LLDBWorkflow.png differ
diff --git a/assets/LLVMObjCopyBuildSuccess.png b/assets/LLVMObjCopyBuildSuccess.png
new file mode 100644
index 0000000..4e09abb
Binary files /dev/null and b/assets/LLVMObjCopyBuildSuccess.png differ
diff --git a/assets/LLVMObjCopyCommandNotFound.png b/assets/LLVMObjCopyCommandNotFound.png
new file mode 100644
index 0000000..b07b762
Binary files /dev/null and b/assets/LLVMObjCopyCommandNotFound.png differ
diff --git a/assets/LLVMObjCopyHelp.png b/assets/LLVMObjCopyHelp.png
new file mode 100644
index 0000000..e3d3eef
Binary files /dev/null and b/assets/LLVMObjCopyHelp.png differ
diff --git a/assets/LLVMObjcCopySuccess.png b/assets/LLVMObjcCopySuccess.png
new file mode 100644
index 0000000..2fe4ce0
Binary files /dev/null and b/assets/LLVMObjcCopySuccess.png differ
diff --git a/assets/LVMObjCopyBatchSuccess.png b/assets/LVMObjCopyBatchSuccess.png
new file mode 100644
index 0000000..a944963
Binary files /dev/null and b/assets/LVMObjCopyBatchSuccess.png differ
diff --git a/assets/LVMObjCopyLaunchOptions.png b/assets/LVMObjCopyLaunchOptions.png
new file mode 100644
index 0000000..85a235e
Binary files /dev/null and b/assets/LVMObjCopyLaunchOptions.png differ
diff --git a/assets/LVMObjCopyOptionNotSupportIssue.png b/assets/LVMObjCopyOptionNotSupportIssue.png
new file mode 100644
index 0000000..294d141
Binary files /dev/null and b/assets/LVMObjCopyOptionNotSupportIssue.png differ
diff --git a/assets/LinkFailedWhenLinkTwoStaticLib.png b/assets/LinkFailedWhenLinkTwoStaticLib.png
new file mode 100644
index 0000000..a42e60f
Binary files /dev/null and b/assets/LinkFailedWhenLinkTwoStaticLib.png differ
diff --git a/assets/LinkFailedWhenLinkTwoStaticLib2.png b/assets/LinkFailedWhenLinkTwoStaticLib2.png
new file mode 100644
index 0000000..b91fad8
Binary files /dev/null and b/assets/LinkFailedWhenLinkTwoStaticLib2.png differ
diff --git a/assets/LinkSuccessWhenLinkTwoStaticLib.png b/assets/LinkSuccessWhenLinkTwoStaticLib.png
new file mode 100644
index 0000000..018a9da
Binary files /dev/null and b/assets/LinkSuccessWhenLinkTwoStaticLib.png differ
diff --git a/assets/LinkTwoStaticLibBefore.png b/assets/LinkTwoStaticLibBefore.png
new file mode 100644
index 0000000..55dfb60
Binary files /dev/null and b/assets/LinkTwoStaticLibBefore.png differ
diff --git a/assets/MobileDeviceAI-AlgorithmModel.png b/assets/MobileDeviceAI-AlgorithmModel.png
new file mode 100644
index 0000000..56906c7
Binary files /dev/null and b/assets/MobileDeviceAI-AlgorithmModel.png differ
diff --git a/assets/MobileDeviceAI-ComputeContainer.png b/assets/MobileDeviceAI-ComputeContainer.png
new file mode 100644
index 0000000..3f9bfe1
Binary files /dev/null and b/assets/MobileDeviceAI-ComputeContainer.png differ
diff --git a/assets/MobileDeviceAI-DataCapture.png b/assets/MobileDeviceAI-DataCapture.png
new file mode 100644
index 0000000..7ca9aea
Binary files /dev/null and b/assets/MobileDeviceAI-DataCapture.png differ
diff --git a/assets/MobileDeviceAI-DataGraphIndex.png b/assets/MobileDeviceAI-DataGraphIndex.png
new file mode 100644
index 0000000..9f18f57
Binary files /dev/null and b/assets/MobileDeviceAI-DataGraphIndex.png differ
diff --git a/assets/MobileDeviceAI-DataLevel.png b/assets/MobileDeviceAI-DataLevel.png
new file mode 100644
index 0000000..7c54c74
Binary files /dev/null and b/assets/MobileDeviceAI-DataLevel.png differ
diff --git a/assets/MobileDeviceAI-Disadvantage.png b/assets/MobileDeviceAI-Disadvantage.png
new file mode 100644
index 0000000..f661aa4
Binary files /dev/null and b/assets/MobileDeviceAI-Disadvantage.png differ
diff --git a/assets/MobileDeviceAI-JudgeCenter.png b/assets/MobileDeviceAI-JudgeCenter.png
new file mode 100644
index 0000000..b1b0960
Binary files /dev/null and b/assets/MobileDeviceAI-JudgeCenter.png differ
diff --git a/assets/MobileDeviceAI-LocalAndServerDiff.png b/assets/MobileDeviceAI-LocalAndServerDiff.png
new file mode 100644
index 0000000..5f7771c
Binary files /dev/null and b/assets/MobileDeviceAI-LocalAndServerDiff.png differ
diff --git a/assets/MobileDeviceAI-LocalAndServerDiff2.png b/assets/MobileDeviceAI-LocalAndServerDiff2.png
new file mode 100644
index 0000000..ae65f7c
Binary files /dev/null and b/assets/MobileDeviceAI-LocalAndServerDiff2.png differ
diff --git a/assets/MobileDeviceAI-PageBackRecommended.png b/assets/MobileDeviceAI-PageBackRecommended.png
new file mode 100644
index 0000000..e991b3b
Binary files /dev/null and b/assets/MobileDeviceAI-PageBackRecommended.png differ
diff --git a/assets/MobileDeviceAI-PageBackRecommendedIssue.png b/assets/MobileDeviceAI-PageBackRecommendedIssue.png
new file mode 100644
index 0000000..b3a3b96
Binary files /dev/null and b/assets/MobileDeviceAI-PageBackRecommendedIssue.png differ
diff --git a/assets/MobileDeviceAI-Push.png b/assets/MobileDeviceAI-Push.png
new file mode 100644
index 0000000..f40cbd0
Binary files /dev/null and b/assets/MobileDeviceAI-Push.png differ
diff --git a/assets/MobileDeviceAI-UserDataFlow.png b/assets/MobileDeviceAI-UserDataFlow.png
new file mode 100644
index 0000000..dd1faf7
Binary files /dev/null and b/assets/MobileDeviceAI-UserDataFlow.png differ
diff --git a/assets/MobileDeviceAI-reLayout.png b/assets/MobileDeviceAI-reLayout.png
new file mode 100644
index 0000000..19b5e6d
Binary files /dev/null and b/assets/MobileDeviceAI-reLayout.png differ
diff --git a/assets/MobileDeviceAI-reLayoutIssue.png b/assets/MobileDeviceAI-reLayoutIssue.png
new file mode 100644
index 0000000..20a9366
Binary files /dev/null and b/assets/MobileDeviceAI-reLayoutIssue.png differ
diff --git a/assets/MobileDeviceAIArch.png b/assets/MobileDeviceAIArch.png
new file mode 100644
index 0000000..08822dc
Binary files /dev/null and b/assets/MobileDeviceAIArch.png differ
diff --git a/assets/RubyGemProcess.png b/assets/RubyGemProcess.png
new file mode 100644
index 0000000..af25cb6
Binary files /dev/null and b/assets/RubyGemProcess.png differ
diff --git a/assets/RubyMachoAddLoadCommand.png b/assets/RubyMachoAddLoadCommand.png
new file mode 100644
index 0000000..9817a91
Binary files /dev/null and b/assets/RubyMachoAddLoadCommand.png differ
diff --git a/assets/RubyMachoChangeIDOnDylib.png b/assets/RubyMachoChangeIDOnDylib.png
new file mode 100644
index 0000000..763707c
Binary files /dev/null and b/assets/RubyMachoChangeIDOnDylib.png differ
diff --git a/assets/RubyMachoChangeRPath.png b/assets/RubyMachoChangeRPath.png
new file mode 100644
index 0000000..da5c254
Binary files /dev/null and b/assets/RubyMachoChangeRPath.png differ
diff --git a/assets/RubyMachoDeleteLoadCommand.png b/assets/RubyMachoDeleteLoadCommand.png
new file mode 100644
index 0000000..1343391
Binary files /dev/null and b/assets/RubyMachoDeleteLoadCommand.png differ
diff --git a/assets/RubyMachoMergeMach.png b/assets/RubyMachoMergeMach.png
new file mode 100644
index 0000000..c3d95ef
Binary files /dev/null and b/assets/RubyMachoMergeMach.png differ
diff --git a/assets/StaticConflictsframework.png b/assets/StaticConflictsframework.png
new file mode 100644
index 0000000..612126c
Binary files /dev/null and b/assets/StaticConflictsframework.png differ
diff --git a/assets/StaticConflictslibrary.png b/assets/StaticConflictslibrary.png
new file mode 100644
index 0000000..f548039
Binary files /dev/null and b/assets/StaticConflictslibrary.png differ
diff --git a/assets/StaticLibCompileTimeCompare.png b/assets/StaticLibCompileTimeCompare.png
new file mode 100644
index 0000000..e992395
Binary files /dev/null and b/assets/StaticLibCompileTimeCompare.png differ
diff --git a/assets/StaticLibConflictsSolvedByDynamicLibNamespace.png b/assets/StaticLibConflictsSolvedByDynamicLibNamespace.png
new file mode 100644
index 0000000..c4f30fb
Binary files /dev/null and b/assets/StaticLibConflictsSolvedByDynamicLibNamespace.png differ
diff --git a/assets/StaticLibHMapGenerate.png b/assets/StaticLibHMapGenerate.png
new file mode 100644
index 0000000..029d6d9
Binary files /dev/null and b/assets/StaticLibHMapGenerate.png differ
diff --git a/assets/StaticLibLinkErrorWhenSameSymbol.png b/assets/StaticLibLinkErrorWhenSameSymbol.png
new file mode 100644
index 0000000..f21ea92
Binary files /dev/null and b/assets/StaticLibLinkErrorWhenSameSymbol.png differ
diff --git a/assets/StaticLibNeedRenamedSymbol.png b/assets/StaticLibNeedRenamedSymbol.png
new file mode 100644
index 0000000..6beb6d7
Binary files /dev/null and b/assets/StaticLibNeedRenamedSymbol.png differ
diff --git a/assets/StaticLibraryHeaderMapSettings.png b/assets/StaticLibraryHeaderMapSettings.png
new file mode 100644
index 0000000..78aa292
Binary files /dev/null and b/assets/StaticLibraryHeaderMapSettings.png differ
diff --git a/assets/StaticLibraryHeaderSearchPathSettings.png b/assets/StaticLibraryHeaderSearchPathSettings.png
new file mode 100644
index 0000000..c7710f0
Binary files /dev/null and b/assets/StaticLibraryHeaderSearchPathSettings.png differ
diff --git a/assets/VSCodeDebugCocoapods.png b/assets/VSCodeDebugCocoapods.png
new file mode 100644
index 0000000..49285ad
Binary files /dev/null and b/assets/VSCodeDebugCocoapods.png differ
diff --git a/assets/VegetablesGoodsAIDataUpload.png b/assets/VegetablesGoodsAIDataUpload.png
new file mode 100644
index 0000000..6e0a92a
Binary files /dev/null and b/assets/VegetablesGoodsAIDataUpload.png differ
diff --git a/assets/VegetablesPurchaseViaAIArch.png b/assets/VegetablesPurchaseViaAIArch.png
new file mode 100644
index 0000000..bc6ae0b
Binary files /dev/null and b/assets/VegetablesPurchaseViaAIArch.png differ
diff --git a/assets/VegetablesPurchaseWorkflowViaAI.png b/assets/VegetablesPurchaseWorkflowViaAI.png
new file mode 100644
index 0000000..c2da339
Binary files /dev/null and b/assets/VegetablesPurchaseWorkflowViaAI.png differ
diff --git a/assets/VisitReleasedStackBlockWillCrash.png b/assets/VisitReleasedStackBlockWillCrash.png
new file mode 100644
index 0000000..789cea0
Binary files /dev/null and b/assets/VisitReleasedStackBlockWillCrash.png differ
diff --git a/assets/Weex-ConcurrencyEnhancement.png b/assets/Weex-ConcurrencyEnhancement.png
new file mode 100644
index 0000000..df5cbfb
Binary files /dev/null and b/assets/Weex-ConcurrencyEnhancement.png differ
diff --git a/assets/WeexCallModuleMethodNameMismatch.png b/assets/WeexCallModuleMethodNameMismatch.png
new file mode 100644
index 0000000..c5f0f87
Binary files /dev/null and b/assets/WeexCallModuleMethodNameMismatch.png differ
diff --git a/assets/WeexCallNativeModuleParamsError.png b/assets/WeexCallNativeModuleParamsError.png
new file mode 100644
index 0000000..a1f743e
Binary files /dev/null and b/assets/WeexCallNativeModuleParamsError.png differ
diff --git a/assets/WeexCaptureVueError.png b/assets/WeexCaptureVueError.png
new file mode 100644
index 0000000..fa50bc9
Binary files /dev/null and b/assets/WeexCaptureVueError.png differ
diff --git a/assets/WeexComponentBuildFlow.png b/assets/WeexComponentBuildFlow.png
new file mode 100644
index 0000000..dc68ca6
Binary files /dev/null and b/assets/WeexComponentBuildFlow.png differ
diff --git a/assets/WeexComponentClickLogic.png b/assets/WeexComponentClickLogic.png
new file mode 100644
index 0000000..1d9871e
Binary files /dev/null and b/assets/WeexComponentClickLogic.png differ
diff --git a/assets/WeexComponentRegisterError.png b/assets/WeexComponentRegisterError.png
new file mode 100644
index 0000000..0da7907
Binary files /dev/null and b/assets/WeexComponentRegisterError.png differ
diff --git a/assets/WeexJSBundleDownloadFailed.png b/assets/WeexJSBundleDownloadFailed.png
new file mode 100644
index 0000000..cfa756e
Binary files /dev/null and b/assets/WeexJSBundleDownloadFailed.png differ
diff --git a/assets/WeexJSBundleDownloadFailedAPM.png b/assets/WeexJSBundleDownloadFailedAPM.png
new file mode 100644
index 0000000..45aae06
Binary files /dev/null and b/assets/WeexJSBundleDownloadFailedAPM.png differ
diff --git a/assets/WeexJSBundleEncodingAPM.png b/assets/WeexJSBundleEncodingAPM.png
new file mode 100644
index 0000000..47aa961
Binary files /dev/null and b/assets/WeexJSBundleEncodingAPM.png differ
diff --git a/assets/WeexJSBundleParseErrorAPM.png b/assets/WeexJSBundleParseErrorAPM.png
new file mode 100644
index 0000000..ed24ebe
Binary files /dev/null and b/assets/WeexJSBundleParseErrorAPM.png differ
diff --git a/assets/WeexJSBundleParseFailed.png b/assets/WeexJSBundleParseFailed.png
new file mode 100644
index 0000000..6f6a2ac
Binary files /dev/null and b/assets/WeexJSBundleParseFailed.png differ
diff --git a/assets/WeexJSbundleEncodingError.png b/assets/WeexJSbundleEncodingError.png
new file mode 100644
index 0000000..04e2a06
Binary files /dev/null and b/assets/WeexJSbundleEncodingError.png differ
diff --git a/assets/WeexMockVueAPM.png b/assets/WeexMockVueAPM.png
new file mode 100644
index 0000000..656ed6e
Binary files /dev/null and b/assets/WeexMockVueAPM.png differ
diff --git a/assets/WeexModuleRequireError.png b/assets/WeexModuleRequireError.png
new file mode 100644
index 0000000..d87b69c
Binary files /dev/null and b/assets/WeexModuleRequireError.png differ
diff --git a/assets/WeexRequireModuleError.png b/assets/WeexRequireModuleError.png
new file mode 100644
index 0000000..ae99068
Binary files /dev/null and b/assets/WeexRequireModuleError.png differ
diff --git a/assets/WeexRequireModuleErrorAPM.png b/assets/WeexRequireModuleErrorAPM.png
new file mode 100644
index 0000000..66ff520
Binary files /dev/null and b/assets/WeexRequireModuleErrorAPM.png differ
diff --git a/assets/XcodeProvisionProfile.png b/assets/XcodeProvisionProfile.png
new file mode 100644
index 0000000..89231f1
Binary files /dev/null and b/assets/XcodeProvisionProfile.png differ
diff --git a/assets/XcodeWillCompileStaticLibUseSourceCodeWhenInSameProject.png b/assets/XcodeWillCompileStaticLibUseSourceCodeWhenInSameProject.png
new file mode 100644
index 0000000..8fb3864
Binary files /dev/null and b/assets/XcodeWillCompileStaticLibUseSourceCodeWhenInSameProject.png differ
diff --git a/assets/XcodeWillCompileStaticLibUseSourceCodeWhenInSameProjectIssue.png b/assets/XcodeWillCompileStaticLibUseSourceCodeWhenInSameProjectIssue.png
new file mode 100644
index 0000000..debfba6
Binary files /dev/null and b/assets/XcodeWillCompileStaticLibUseSourceCodeWhenInSameProjectIssue.png differ
diff --git a/assets/Youzan-VegetablesGoodsWorkflow.png b/assets/Youzan-VegetablesGoodsWorkflow.png
new file mode 100644
index 0000000..46eaf17
Binary files /dev/null and b/assets/Youzan-VegetablesGoodsWorkflow.png differ
diff --git a/assets/fastlaneScreentshotConfig.png b/assets/fastlaneScreentshotConfig.png
new file mode 100644
index 0000000..e5e4ae6
Binary files /dev/null and b/assets/fastlaneScreentshotConfig.png differ
diff --git a/assets/gitAddMocking.png b/assets/gitAddMocking.png
new file mode 100644
index 0000000..90c046d
Binary files /dev/null and b/assets/gitAddMocking.png differ
diff --git a/assets/gitCommitTree.png b/assets/gitCommitTree.png
new file mode 100644
index 0000000..bdb5d3f
Binary files /dev/null and b/assets/gitCommitTree.png differ
diff --git a/assets/gitFileTreeAfter.png b/assets/gitFileTreeAfter.png
new file mode 100644
index 0000000..7db5ab1
Binary files /dev/null and b/assets/gitFileTreeAfter.png differ
diff --git a/assets/gitFileTreeBefore.png b/assets/gitFileTreeBefore.png
new file mode 100644
index 0000000..ab0ab49
Binary files /dev/null and b/assets/gitFileTreeBefore.png differ
diff --git a/assets/gitRemoteAdd.png b/assets/gitRemoteAdd.png
new file mode 100644
index 0000000..88c4c67
Binary files /dev/null and b/assets/gitRemoteAdd.png differ
diff --git a/assets/gitTreeRecoverAfter.png b/assets/gitTreeRecoverAfter.png
new file mode 100644
index 0000000..0f08f22
Binary files /dev/null and b/assets/gitTreeRecoverAfter.png differ
diff --git a/assets/gitTreeRecoverBefore.png b/assets/gitTreeRecoverBefore.png
new file mode 100644
index 0000000..fddb97d
Binary files /dev/null and b/assets/gitTreeRecoverBefore.png differ
diff --git a/assets/githashChange.png b/assets/githashChange.png
new file mode 100644
index 0000000..db2372e
Binary files /dev/null and b/assets/githashChange.png differ
diff --git a/assets/githashCommand.png b/assets/githashCommand.png
new file mode 100644
index 0000000..860e056
Binary files /dev/null and b/assets/githashCommand.png differ
diff --git a/assets/githashDemo.png b/assets/githashDemo.png
new file mode 100644
index 0000000..e7dc1a0
Binary files /dev/null and b/assets/githashDemo.png differ
diff --git a/assets/jtool2WatchSignature.png b/assets/jtool2WatchSignature.png
new file mode 100644
index 0000000..4776b2b
Binary files /dev/null and b/assets/jtool2WatchSignature.png differ
diff --git a/assets/wechat_2025-08-20_235658_762.png b/assets/wechat_2025-08-20_235658_762.png
new file mode 100644
index 0000000..bee616c
Binary files /dev/null and b/assets/wechat_2025-08-20_235658_762.png differ
diff --git a/assets/xcodeprojChangeBuildSettings.png b/assets/xcodeprojChangeBuildSettings.png
new file mode 100644
index 0000000..71738dc
Binary files /dev/null and b/assets/xcodeprojChangeBuildSettings.png differ
diff --git a/assets/xcodeprojSetXcconfig.png b/assets/xcodeprojSetXcconfig.png
new file mode 100644
index 0000000..66b9c48
Binary files /dev/null and b/assets/xcodeprojSetXcconfig.png differ
diff --git a/assets/xcodeprojVisitScheme.png b/assets/xcodeprojVisitScheme.png
new file mode 100644
index 0000000..4aace85
Binary files /dev/null and b/assets/xcodeprojVisitScheme.png differ