diff --git a/.DS_Store b/.DS_Store
index 05106d4..f26a6fa 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/Chapter1 - iOS/.DS_Store b/Chapter1 - iOS/.DS_Store
new file mode 100644
index 0000000..5008ddf
Binary files /dev/null and b/Chapter1 - iOS/.DS_Store differ
diff --git a/Chapter1 - iOS/1.102.md b/Chapter1 - iOS/1.102.md
index 926d297..d2e0b4d 100644
--- a/Chapter1 - iOS/1.102.md
+++ b/Chapter1 - iOS/1.102.md
@@ -197,6 +197,21 @@ LLVM IR 有3种表示格式:
+## 调试 LLVM
+选择 Edit Scheme.
+
+
+
+
+
+
+最后就可以加断点进行 Debug 了。但为了让调试更有意义,类似 `nm -a /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LDExploreDemo-ehvvtxafpkdkubgrswvvsudzhqbb/Build/Products/Debug-iphonesimulator/LDExploreDemo.app/LDExploreDemo` 一样可以查看到更有意义的信息,可以在 Edit Scheme 面板中 `Run -> Arguments -> Arguments Passed On Launch` section 中的 **+** 点击,添加一些参数,如下图:
+
+
+
+最后允许测试。注意:LLVM 项目较大,可以选择顶部 "Product -> Perform Action -> Run Without Building".
+
+
## 用途
LLVM 的一些插件,比如 libclang、libTooling,可以查看官方文档:https://clang.llvm.org/docs/Tooling.html,可以做一些**语法树解**
diff --git a/Chapter1 - iOS/Untitled-1 b/Chapter1 - iOS/1.11
similarity index 100%
rename from Chapter1 - iOS/Untitled-1
rename to Chapter1 - iOS/1.11
diff --git a/Chapter1 - iOS/1.111.md b/Chapter1 - iOS/1.111.md
index 8cb5dfd..5479def 100644
--- a/Chapter1 - iOS/1.111.md
+++ b/Chapter1 - iOS/1.111.md
@@ -1,7 +1,7 @@
## 写给 iOSer 的鸿蒙开发 tips
## 下载问题
-
+
The other possible cause is that the system language of the PC is English and the region code is US. You could try to perform the following operations to change the region code to CN. Before changing the region code, close DevEco Studio.
@@ -15,4 +15,350 @@ For Mac OS: ~/Library/Application Support/Huawei/DevEcoStudio3.0/options/country
-```
\ No newline at end of file
+```
+
+
+
+## 鸿蒙开发任务拆分
+
+### 依赖梳理
+
+- 梳理依赖的二方、三方 SDK,根据对应 SDK 的鸿蒙化排期调整整体项目的排期
+- 暂时不能鸿蒙化的 SDK 功能降级
+
+### 优先级对齐
+
+- 根据依赖梳理的排期,倒推二方 SDK 的优先级
+- 梳理各个团队的 P1、P2、P3 任务
+
+### 功能定版
+
+- 考虑到部分基础能力无法实现和人力资源排期的问题,无法完全对齐 Android 版本
+- 根据前期梳理,确定每个节点的功能范围
+
+### 架构设计
+
+- App 模块架构基本对齐 Android
+- 鸿蒙特性部分微调
+- 基础能力下沉到 C++,提升复用
+
+## 规范
+
+### 代码规范制定
+
+除了常规流程的 MR,但对于鸿蒙,大家都属于摸着石头过河的状态。开发也是摸着石头过河,所以会变开发,定期制定、更新代码规范文档,并定期 review 项目代码,完善代码规范,避免糟糕代码野蛮生长
+
+### 踩坑分享
+
+及时收集整理开发过程中的踩坑点,定期在团队内分享,减少或避免相同问题的再次发生
+
+### 最佳实践分享
+
+定期梳理最佳实践,鼓励分享。快速提高、拉齐大家的鸿蒙水平
+
+
+
+## 鸿蒙背景下的跨端方案
+
+业务使用 TS 开发,公用一套渲染引擎
+
+
+
+### 引擎选择
+
+| 引擎 | | 优点 | 缺点 |
+| -------------- | --------------- | --------------------------------- | ----------------------------- |
+| JavaScriptCore | Apple | 在 iOS 平台上有非常明显的主场优势 | 在 Android 缺乏优化,适配不好 |
+| V8 | Google | 性能优异,支持 JIT,支持调试 | 体积大、内存占用大 |
+| quickJS | Fabrice Bellard | 体积小、内存小 | 无法进行 JIT,不支持调试 |
+| panda 熊猫 | 鸿蒙 | 内置 JS 引擎,支持 JIT | 没有太多关于引擎的介绍 |
+
+所以 iOS 侧旋 JavascriptCore 引擎,Android 选 quickJS 作为 JS 引擎,后续增加 V8 作为本地开发调试的引擎。
+
+
+
+## 公共能力
+
+### 公共桥能力
+
+业务方可以通过公共桥直接使用 Native 侧的能力:
+
+- modal 弹窗:Modal 桥主要用来展示 toast、dialog、window 等一些原生系统弹窗能力
+- keyboard:keyboard 桥主要用来控制键盘相关的操作
+- Navigator:Navigator 桥主要用来负责导航以及对导航栏的定制操作
+- Network:Network 桥主要用于网络相关的操作
+- Storage:Storage 桥主要用来持久化存储和获取数据
+- Broadcast:Broadcast 桥主要用于接收和发送通知
+
+### 自定义桥能力开发
+
+- 定义桥协议:定义需要实现的功能、确定桥所属模块与方法名、参数等
+- TS 侧开发:在 TS 侧实现桥协议的方法,确定以同步还是异步的方式回调
+- Native 侧开发:通过公共通信层解析 TS 侧透传的方法与参数,实现 Native 侧功能与回调
+- 桥注册:桥开发完毕后,需要在 Native 侧通过 registerCustomModule 方法,注册后才可以使用
+- 桥使用:TS 侧业务代码通过调用桥协议,来使用自定义桥功能
+
+### 渲染
+
+将 js 引擎返回的 UI 数据通过解析进行渲染,根布局为 stack,子组件通过 offset 确定位置、size 确定大小、type 确定组件类型。会有重叠、内嵌的情况,则递归循环渲染即可
+
+参考鸿蒙 RN 团队在1月份的方案,使用 stack 组件内部,forEach 循环渲染子组件。适配初期,在嵌套不深的页面没有发现问题,整体上打通了从 JS 代码到引擎渲染的核心流程。
+
+
+
+左侧的 UI 树和右侧的 Model 树,通过 `@ObjectLink` 、`@Observed` 来进行数据渲染和刷新。
+
+
+
+
+
+## 鸿蒙APO 探索之路
+
+AOP Aspect Oriented Programming 是一种编程范式,被允许开发者将关注点与业务逻辑中分离出来。
+
+AOP 优势:解耦、复用、模块化
+
+AOP 应用场景:日志、埋点、监控
+
+### AOP 的实现方案
+
+- 编译时:AspectJ
+- 类加载时:AspectJ
+- 链接时:fishhook
+- 运行时:Epic
+
+### 为什么使用 AOP?
+
+日志、网络、性能监控、埋点等多个 AOP 使用场景。
+
+### 鸿蒙 AOP 方案探索
+
+#### 痛点
+
+- 匿名函数、箭头函数:`Button.onClick(ClickEvent)`
+- 函数局部类: `HttpClient#Builder()#build()`
+
+```ts
+View().
+onClick(() => {
+ // handle business logic
+})
+```
+
+- 属性不可修改场景:`router.pushUrl`
+
+ `Object.defineProperty()` 当 writeable 特性设置为 false 的时候,该属性是不可修改的。尝试对一个不可修改的属性进行写入时不会改变它。在严格模式下还会报错。
+
+ 鸿蒙也是基于 TS 的,所以也可以调用 `Object.freeze()` 冻结属性。比如鸿蒙早期路由实现,是基于 Router 的,很多 API 的参数,writeable 属性都是 false。
+
+解决方案
+
+无统一修改点场景一:箭头参数函数 `Button.onClick(ClickEvent)`
+
+构造一个第一个参数为函数的 wrappFn 函数,持有目标参数函数。就跟 Native 侧的 hook 一样。构造一个一样的 hook 函数。
+
+```ts
+function hookMethod(traget, action, beforeFn?, afterFn?) {
+ wrapMethod(target, action, (originalMethod) => function(callback) {
+ const wrappedCallback = (...args) => {
+ beforeFn?.apply(this, args)
+ callback.apply(this, args)
+ afterFn?.apply(this, args)
+ }
+ originalMethod.call(this, wrappedCallback)
+ });
+}
+```
+
+使用
+
+```ts
+Aspect.addBefore(Button, 'onClick', () => {
+ router.pushUrl({url: 'pages/Index' })
+ logger.w(TAG, '1.Aspect add before --- Button#onClick()#action, do your business...');
+}, true)
+```
+
+无统一修改点场景二:局部类 `HttpClient#Builder()#build()`
+
+AOP 的本质是关注点分离,面对这种情况,每次通过 HttpClient 获取 Budiler() 就会产生不同的对象,所以传统的通过 hook 某个类的方法形式,已经不再适用了。所以需要通过更高层的 hook,即 `Object.defineProperty`
+
+通过属性定义拦截 HttpClient 的 Builder() 属性的获取,builder 的获取,都会被收口拦截。
+
+```ts
+function hookMethod(target: any, propertyName: string, methodName: string, beforeFn?: (context: any, args: any[]) => void, afterFn?: (context: any, args: any[], result: any) => void) {
+
+ const propertyDescriptor = Object.getOwnPropertyDescriptor(target, propertyName)
+ if (propertyDescriptor && propertyDescriptor.get) {
+ Object.defineProperty(target, propertyName, {
+ get() {
+ const originalTarget = propertyDescriptor.get!.call(this)
+ const originalMethod = originalTarget.prototype[methodName]
+ originalTarget.prototype[methodName] = function (...args: any[]) {
+ beforeFn?.call(this, this, args)
+ let result = originalMethod.apply(this, args)
+ afterFn?.call(this, this, args, result)
+ return result
+ }
+ return originalTarget
+ }
+ })
+ }
+}
+```
+
+使用
+
+```ts
+Apsect.hookMethod({
+ target: HttpClient,
+ methodNameOrProperty: 'Builder',
+ beforeFn: (context: args) => {
+ const builderContext = context as InstanceType;
+ builderContext._eventListener = new MyEnevtListener()
+ builderContext.addInterceptor(new MyEnevtListener());
+ },
+ propertyMethodNameOrType: 'build'
+})
+```
+
+无统一修改点场景三:`router.pushUrl`,编译时 + 运行时组合实现偷梁换柱。
+
+
+
+如何实现编译时替换?
+
+鸿蒙提供了 Hvigor Plugin 编译时自定义插件,用于实现定制化构建。思路:直接利用 Hvigor Plugin hook 某个编译 task
+
+
+
+那到底 hook 哪个编译 task?
+
+
+
+问题:
+
+- output 修改无效
+
+
+
+
+
+ ArkTS 编译之后会产生临时目录,将 ets 编译为 ts。那是不是可以直接修改产物,看看最后能不能影响方舟字节码。
+
+ 发现修改了 index.protoBin 、ts 文件,发现最终无法影响编译产物 `*.abc` 文件
+
+ 联系了鸿蒙团队的工程师,验证了说是2条并行链路。并不是先编译产生临时文件,再通过临时文件产生 `*.abc` 文件。事实上是2个并行过程。所以此路不通
+
+- input 无法 hook,Hvigor plugin 暂未开放相关能力
+
+
+
+ Hvigor 目前开放的能力有限,仅支持在默认的 task 前后加一些钩子函数。但无法修改 input。此路不通
+
+- 既然没法直接修改默认 task,怎么实现?
+
+ 思路:copy -> modify -> revert
+
+
+
+
+
+ 插桩只能影响产物,不能影响源码。所以先对源码进行备份。
+
+ ```ts
+ export function HllEntreyPlugin(): HvigorPlugin {
+ return {
+ pluginId: 'HllEntryPlugin',
+ apply: (node: HvigorNode) => {
+ node.registerTask({
+ name: 'HllEntreyPluginInjectTask',
+ run: () => {
+ dispatcherToPlugins(node) // copy & modify
+ }
+ });
+ node.registerTask({
+ name: 'HllEntryPluginResetTask',
+ run: () => {
+ resetPluginCodes(originalBackUpFiles) // revert
+ },
+ dependcies: ['default@CompileArkTS'],
+ postDependencies: ['assembleHap']
+ })
+ }
+ }
+ }
+ ```
+
+ 如何修改 AST?
+
+ 鸿蒙目前没有提供修改 AST 的能力。如何做?
+
+ ArkTS 虽然没有提供 AST 相关 API。但从 CompileArkTS Task 产物来看,ArkTS 最终会编译成 TS。
+
+ 且 TS 提供 AST 相关 API。TS 是开源的,TS 开源代码中有关于 AST 的模块。[TS Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)
+
+ 可以基于源码中 AST 相关的 API 可以抽取封装一下。
+
+ 于是,整个流程就变成了
+
+
+
+ 可以利用 [Typescript AST Viewer](https://github.com/dsherret/ts-ast-viewer) 来查看 AST 抽象语法树信息。可以在线查看 AST,官网地址为:[Typescript AST Viewer](https://ts-ast-viewer.com)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#### Aspect(运行时)
+
+官方在 API 11 开始提供的方案,可快速实现对类方法前后进行插桩或替换。
+
+关键点一:属于可修改-即 addBefore、addAfter、replace 接口的原理,基于 class
+
+
+
+#### AspectPro V1(编译时<正则> + 运行时)
+
+#### AspectPro V2(编译时 + 运行时)
+
diff --git a/Chapter1 - iOS/1.112.md b/Chapter1 - iOS/1.112.md
index 9658939..847f123 100644
--- a/Chapter1 - iOS/1.112.md
+++ b/Chapter1 - iOS/1.112.md
@@ -1,8 +1,12 @@
# Swift 枚举值内存布局
-> enum 使用很简单,那大家有没有思考过系统针对枚举的实现是怎么样的?接下去会针对不同情况的枚举,结合汇编来窥探下系统实现原理。
+> enum 使用很简单,那大家有没有思考过系统针对枚举的实现是怎么样的?
+>
+> 不同类型的枚举占用多大内存空间?下面结合汇编来窥探下系统实现原理
-### 基础枚举
+
+
+## 基础枚举(不带关联值、不带原始值)
```swift
enum Season {
@@ -21,22 +25,31 @@ print("over")
- `var season: Season = Season.spring` 基础枚举,默认值是0。
-
+
- `season = Season.summer`,此时可以看到第一个字节的位置是1.
-
+
- `season = Season.antumn` ,此时可以看到第一个字节的位置是2
-
+
-结论:查看内存信息,可以看到基础枚举,只占1个字节大小空间,且值为默认值。
+结论:查看内存信息,可以看到**不带关联值、不带原始值的基础枚举,只占1个字节大小空间,且值为默认值**
-### 只有原始值
+延伸:对于无关联值、无原始值的简单枚举,Swift 编译器会进行内存优化:
+
+- 当枚举 case ≤ 256 个时,使用 1 字节(UInt8)
+- 当 case ≤ 65536 时,使用 2 字节(UInt16)
+
+
+
+## 只有原始值的枚举
+
+不带关联值、只有原始值的枚举
```swift
-enum Season:Int {
+enum Season : Int {
case spring = 1
case summer = 2
case antumn = 3
@@ -56,17 +69,17 @@ print("over")
- `var season: Season = Season.spring` 基础枚举,变量默认值,可以看到第一个字节的位置是0
-
+
- `season = .winter` 基础枚举,当赋值为 winter 的时候,可以看到第一个字节的位置是3
-
+
-结论:带有原始值的枚举,同样只占用1个字节,该字节的值为枚举的位置(比如 case1 case2)
+结论:**只带有原始值的枚举,同样只占用1个字节,该字节的值为枚举的位置索引(比如1、2),而非原始值。原始值不占用枚举的内存**
-### 带有关联值的枚举
+## 只带有关联值的枚举
```swift
enum Season {
@@ -90,9 +103,11 @@ season = Season.unknown
print("over")
```
-- `var season: Season = Season.spring(1, 2, 3)` 带有关联值的枚举,`.spring` 有3个 Int,单个 Int 占8个字节空间,所以红色框代表 spring 的1,蓝色框代表 spring 的2,绿色框代表 spring 的3,黄色框代表枚举的第1个 case,剩余7个字节,为空。
+- `var season: Season = Season.spring(1, 2, 3)` 带有关联值的枚举:
-
+ `.spring` 有3个 Int,单个 Int 占8个字节空间,所以红色框代表 spring 的1,蓝色框代表 spring 的2,绿色框代表 spring 的3,黄色框代表枚举的第1个 case,剩余7个字节,为空。后续的7个字节是为了内存对齐而补齐的内存。
+
+
其内存信息如下(8字节为1组,对应上图)
@@ -106,79 +121,88 @@ print("over")
这段内存信息怎么看?我划分了下
- ```
- 关联值: 01 00 00 00 00 00 00 00
- 关联值: 02 00 00 00 00 00 00 00
- 关联值: 03 00 00 00 00 00 00 00
- 位置值: 00
- 内存对齐占用:00 00 00 00 00 00 00
+ ```shell
+ case spring 的关联值的第1个 Int: 01 00 00 00 00 00 00 00
+ case spring 的关联值的第2个 Int: 02 00 00 00 00 00 00 00
+ case spring 的关联值的第3个 Int: 03 00 00 00 00 00 00 00
+ 表明哪个 case 的索引值: 00
+ 内存对齐占用: 00 00 00 00 00 00 00
```
下面的几组一样
-- `season = Season.summer(4, 5)` 带有关联值的枚举,`.summer` 有2个 Int,单个 Int 占8个字节空间,所以红色框代表 summer 的4,蓝色框代表 summer 的5,绿色框为空,黄色框代表枚举的第2个 case,剩余7个字节,为空。
+- `season = Season.summer(4, 5)` 带有关联值的枚举,`.summer` 这个枚举关联值有2个 Int,单个 Int 占8个字节空间,所以红色框代表 summer 第一个关联值 4,蓝色框代表 summer 第二个关联值 5,绿色框为空,黄色框代表枚举的第2个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
```shell
- 04 00 00 00 00 00 00 00
- 05 00 00 00 00 00 00 00
- 00 00 00 00 00 00 00 00
- 01
- 00 00 00 00 00 00 00
+ case summer 的关联值的第1个 Int: 04 00 00 00 00 00 00 00
+ case summer 的关联值的第2个 Int: 05 00 00 00 00 00 00 00
+ 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
+ 表明哪个 case 的索引值: 01
+ 内存对齐占用: 00 00 00 00 00 00 00
```
-- `season = Season.antumn(6) 带有关联值的枚举,`. `antumn` 有1个 Int,单个 Int 占8个字节空间,所以红色框代表 antumn 的6,蓝色框为空,绿色框为空,黄色框代表枚举的第3个 case,剩余7个字节,为空。
+- `season = Season.antumn(6) 带有关联值的枚举,`. `antumn` 存在关联值 1个 Int,单个 Int 占8个字节空间,所以红色框代表 antumn 的6,蓝色框为空,绿色框为空,黄色框代表枚举的第3个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
```shell
- 06 00 00 00 00 00 00 00
- 00 00 00 00 00 00 00 00
- 00 00 00 00 00 00 00 00
- 02
- 00 00 00 00 00 00 00
+ case autumn 的关联值的第1个 Int: 06 00 00 00 00 00 00 00
+ 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
+ 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
+ 表明哪个 case 的索引值: 02
+ 内存对齐占用: 00 00 00 00 00 00 00
```
-- `season = Season.winter(true)` 带有关联值的枚举,`. `winter` 有1个 Bool,单个 Int 占1个字节空间,所以红色框代表 winter 的 true,蓝色框为空,绿色框为空,黄色框代表枚举的第4个 case,剩余7个字节,为空。
+- `season = Season.winter(true)` 带有关联值的枚举,`. `winter` 关联值是 1个 Bool,单个 Bool 占1个字节空间,所以红色框代表 winter 的 true,蓝色框为空,绿色框为空,黄色框代表枚举的第4个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
```shell
- 01 00 00 00 00 00 00 00
- 00 00 00 00 00 00 00 00
- 00 00 00 00 00 00 00 00
- 03
- 00 00 00 00 00 00 00
+ case winter 的关联值的第1个 Int: 01 00 00 00 00 00 00 00
+ 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
+ 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
+ 表明哪个 case 的索引值: 03
+ 内存对齐占用: 00 00 00 00 00 00 00
```
- `season = Season.unknown` 带有关联值的枚举,`unknown` 没有关联值,所以红色框为空,蓝色框为空,绿色框为空,黄色框代表枚举的第5个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
```shell
- 00 00 00 00 00 00 00 00
- 00 00 00 00 00 00 00 00
- 00 00 00 00 00 00 00 00
- 04
- 00 00 00 00 00 00 00
+ case unknown 关联值的第1个Int: 00 00 00 00 00 00 00 00
+ 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
+ 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
+ 表明哪个 case 的索引值: 04
+ 内存对齐占用: 00 00 00 00 00 00 00
```
- `MemoryLayout.size` :3个 Int 最大为3*8,1个字节用来表达位置信息,`3*8 + 1 = 25`
+
- `MemoryLayout.stride` :获取系统分配给数据类型的内存大小,也就是实际内存大小(对齐后的)
+
- `MemoryLayout.alignment` 内存对齐系数,以8 Byte 为单位,对象分配的内存必须是该值的整数倍
+结论:
+
+- **对于有不同枚举值有不同关联结构的枚举,其内存由关联值最大的 case 的内存决定(其余的 case 沿用该结构。所有 case 共享同一块内存区域。该区域大小为最大关联值所需内存)**
+- **除了最大内存的 case 外,还需要1个字节存储所属哪个 case**(通常 1 字节,具体取决于 case 数量,1字节能表示的 case 数量为256个)
+- **枚举总内存实际大小 = 1个字节存储所属哪个 case + 关联值所占内存最大的 case 的内存大小**
+- **另外,枚举所占内存还需要考虑内存对齐的情况。比如本例中实际内存为25,内存对齐为8字节,所以最终分配了32字节的内存**
-### 只有一个 case 的枚举
+
+## 只有一个 case 的枚举
```swift
enum SimpleEnum {
@@ -205,7 +229,9 @@ print(MemoryLayout.alignment) // 1
现在好理解,2个 case 需要存储1个 Byte 的值来区分是哪个 case,1 Byte 可以代表最多256个 case
-### 只有1个 case 且带关联值的枚举
+
+
+## 只有1个 case 且带关联值的枚举
```swift
enum SimpleEnum {
@@ -217,7 +243,10 @@ print(MemoryLayout.stride) // 8
print(MemoryLayout.alignment) // 8
```
-带有关联值且只有1个 case 的枚举,因为有1个 Int 的关联值,但只有1个 case,所以只需要8 Byte 存储关联值即可。
+- 带有关联值且只有1个 case 的枚举,因为有1个 Int 的关联值,需要8 Byte 存储关联值
+- 但只有1个 case,不需要额外空间来判断所属哪个枚举值,所以不需要额外空间
+
+
请看下面的对照实验
@@ -232,13 +261,55 @@ print(MemoryLayout.stride) // 16
print(MemoryLayout.alignment) // 8
```
-2个 case,其中一个 case 有关联值 Int,所以需要8 Byte 存 Int 值,1 Byte 区分是哪个 case,实际需要占用 8 + 1 = 9 Byte,内存对齐单位是8,9向上为16.
+2个 case,其中一个 case 有关联值 Int,所以需要8 Byte 存 Int 值,1 Byte 区分是哪个 case,实际需要占用 8 + 1 = 9 Byte,内存对齐单位是8,9向上为16。
-### 用汇编验证下内存
+### 为什么不能复用关联值的空间
-```
+case1 占用8个字节,case2 占用1个字节,能用 case1 的8个字节的最开始的位置存储 case2 的信息吗?这样的话节省内存
+
+- **内存布局的确定性**:Swift 要求枚举实例的内存布局在编译时确定。若允许不同 case 复用同一块内存,会导致运行时动态解析内存布局,降低性能和安全性。
+
+- **所有 case 共享同一块内存**:枚举实例的内存大小由**最大关联值的大小 + 标签所需空间**决定。
+
+- **标签(Discriminant)**:用于区分不同 case,通常占用 1 字节(但具体由 case 数量决定)。
+
+ **标签的占用大小**由枚举的 `case` 数量决定:
+
+ - **1 个 `case`**:不需要标签(因为没有其他可能性)。
+ - **2~256 个 `case`**:标签通常占用 **1 字节**(`UInt8`,可以表示 256 种可能)。
+ - **257~65536 个 `case`**:标签占用 **2 字节**(`UInt16`)。
+ - 更大数量依此类推(但实际中几乎不会用到如此多的 `case`)。
+
+ - **内存对齐约束**:即使标签的逻辑占用小于 1 字节(例如只有 2 个 `case`,理论上只需 1 位),实际仍会占用 **至少 1 字节**(因为内存按字节寻址)。
+
+- **内存对齐**:总大小会按对齐要求(如 8 字节)向上取整到最近的倍数(即 `stride`)。
+
+
+
+以示例中的 `SimpleEnum` 为例:
+
+- `case one(Int)`:需要 8 字节存储 `Int` + 1 字节标签 → 共 9 字节。
+- `case two`:仅需 1 字节标签 → 但内存仍需按最大 case 分配(即 8 字节关联值空间 + 1 字节标签 → 总 9 字节)。
+
+因此,无论当前是哪个 case,枚举实例始终占用 **9 字节**(对齐后 `stride` 为 16 字节)。
+
+
+
+更改验证标签对于枚举占用内存大小的影响
+
+
+
+可以看到 enum 只有1个 case 的时候,内存大小只和最大关联值大小有关,1个 case 的情况下不需要额外的空间来判断所属哪个 case。
+
+因此此时枚举的内存大小 = 最大关联值的内存大小 = 8
+
+
+
+## 用汇编验证下内存
+
+```swift
enum Season {
case spring(Int, Int, Int)
case summer(Int, Int)
@@ -258,7 +329,7 @@ print("over")
断点停到 `var season: Season = Season.spring(1, 2, 3)` 位置
-
+
将断点处的汇编单独摘出来研究
@@ -294,4 +365,78 @@ print("over")
- n个字节用来存储关联值(n取占用内存最大的关联值),任何一个 case 的关联值都共用这 n 个字节
- 且存在内存对齐,所以占用大小为 n 和 1 的最大值,再结合内存对齐。
- 如果枚举的定义非常简单,系统会用1个字节来存放值,最大范围是256个 case。
-- 枚举定义如果有原始值,也不会影响内存布局。
\ No newline at end of file
+- 枚举定义如果有原始值,也不会影响内存布局。
+
+
+
+## switch 的工作原理
+
+```swift
+enum Season {
+ case spring(Int, Int, Int)
+ case summer(Int, Int)
+ case antumn(Int)
+ case winter(Bool)
+ case unknown
+}
+
+var season: Season = Season.spring(1, 2, 3)
+switch season {
+ case let .spring(v1, v2, v3):
+ print(".spring", v1, v2, v3)
+ case let .summer(v1, v2):
+ print(".summer", v1, v2)
+ case let .antumn(v1):
+ print(".antumn", v1)
+ case let .winter(v1):
+ print(".winter", v1)
+ case .unknown:
+ print(".unkown")
+}
+```
+
+- Swift 先判断 season 的成员值,判断属于哪个 case。
+ - 如果发现成员值为0,则走第1个 case,将 season 的前24个字节,分别赋值给第1个 case 的 v1、v2、v3
+ - 如果发现成员值为1,则走第2个 case,将 season 的前16个字节,分别赋值给第2个 case 的 v1、v2
+ - 如果发现成员值为2,则走第3个 case,将 season 的前8个字节,赋值给第3个 case 的 v1
+ - 如果发现成员值为3,则走第4个 case,将 season 的第1个字节,赋值给第4个 case 的 v1
+ - 如果发现成员值为4,则走第5个 case,则执行第5个 case 的打印逻辑
+
+
+
+## Swift 枚举的本质
+
+从表象来看,枚举存在以下情况:
+
+- **不带关联值、不带原始值的基础枚举,只占1个字节大小空间,且值为默认值**
+- **只带有原始值的枚举,同样只占用1个字节,该字节的值为枚举的位置索引(比如1、2),而非原始值**
+- **带有关联值的枚举内存大小:**
+ - **对于有不同枚举值有不同关联结构的枚举,其内存由关联值最大的 case 的内存决定(其余的 case 沿用该结构。所有 case 共享同一块内存区域。该区域大小为最大关联值所需内存)**
+ - **除了最大内存的 case 外,还需要1个字节存储所属哪个 case**(通常 1 字节,具体取决于 case 数量,1字节能表示的 case 数量为256个)
+ - **枚举总内存实际大小 = 1个字节存储所属哪个 case + 关联值所占内存最大的 case 的内存大小**
+ - **另外,枚举所占内存还需要考虑内存对齐的情况。比如本例中实际内存为25,内存对齐为8字节,所以最终分配了32字节的内存**
+
+但从本质来讲:
+
+- 对于无关联值、无原始值的简单枚举,Swift 编译器会进行内存优化:
+
+ - 当枚举 case ≤ 256 个时,使用 1 字节(UInt8)
+
+ - 当 case ≤ 65536 时,使用 2 字节(UInt16)
+
+- **内存布局的确定性**:Swift 要求枚举实例的内存布局在编译时确定。若允许不同 case 复用同一块内存,会导致运行时动态解析内存布局,降低性能和安全性。
+
+- **所有 case 共享同一块内存**:枚举实例的内存大小由**最大关联值的大小 + 标签所需空间**决定。
+
+- **标签(Discriminant)**:用于区分不同 case,通常占用 1 字节(但具体由 case 数量决定)。
+
+ **标签的占用大小**由枚举的 `case` 数量决定:
+
+ - **1 个 `case`**:不需要标签(因为没有其他可能性)。
+ - **2~256 个 `case`**:标签通常占用 **1 字节**(`UInt8`,可以表示 256 种可能)。
+ - **257~65536 个 `case`**:标签占用 **2 字节**(`UInt16`)。
+ - 更大数量依此类推(但实际中几乎不会用到如此多的 `case`)。
+
+ - **内存对齐约束**:即使标签的逻辑占用小于 1 字节(例如只有 2 个 `case`,理论上只需 1 位),实际仍会占用 **至少 1 字节**(因为内存按字节寻址)。
+
+- **内存对齐**:总大小会按对齐要求(如 8 字节)向上取整到最近的倍数(即 `stride`)。
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.113.md b/Chapter1 - iOS/1.113.md
index 9556d7c..4039155 100644
--- a/Chapter1 - iOS/1.113.md
+++ b/Chapter1 - iOS/1.113.md
@@ -1,6 +1,6 @@
# Swift 结构体和类的内存布局
-## 结构体内存布局
+## 结构体初始化器
实验1:在 struct 内部自己实现 init
@@ -18,7 +18,7 @@ var point = Point()
在`init` 方法内第一行处加 断点,如下所示
-
+
实验2:struct 内不自己加 init
@@ -32,11 +32,15 @@ var point = Point()
在`var point = Point()`处加 断点,如下所示
-
+
-结论:结构体会有一个编译器自动生成的初始化编译器。目的是保证所有成员都有初始值。
+现象:可以看到加不加自定义初始化器的汇编代码基本相同。
-实验3:
+结论:**如果没有为结构体声明初始化器,编译器会自动生成1个初始化器。目的是保证所有成员都有初始值。**
+
+
+
+## 结构体内存布局
```swift
struct CustomDate {
@@ -62,6 +66,7 @@ Int 占8 Byte,Bool 占1 Byte,共 2*8 + 1 = 17 Byte,由于存在内存对
- 与类的比较:与 `class`(类)不同,`struct` 不需要额外的内存来存储类型信息、引用计数或其他元数据。这使得 `struct`通常比 `class` 更轻量级,并且在某些情况下具有更好的性能。
+
## 类的内存布局
类和结构体类似,但是编译器不会为类自动生成可以传入成员值的初始化器。
@@ -124,13 +129,13 @@ test()
断点打在 `var point1 = Point(x: 10, y: 20)` 处,查看汇编
-
+
乍一看如果不认识的话,先找字面量(立即数),比如红色框内的 `0xa`,就是 10,`0x14` 就是20。[之前](./109.md)学过寄存器的设计,64位寄存器是兼容32位寄存器的。红色框内将 `0xa`,也就是 10 保存到 `%edi ` 寄存器内部,也就是保存到 `%rdi` 中,将 `0x14` 也就是20,保存到 `%esi` 也就是保存到 `%rsi` 寄存器中。
LLDB 模式下输入 `si` 进入 init 方法内部。
-
+
可以查看到将 `%rsi` 里的10 保存到 `%rdx` 中了;将 `%rdi` 里的20 保存到 `%rax` 中了。这也就是 struct `init` 方法做的事情。
@@ -155,11 +160,69 @@ LLDB 模式下输入 `finish` 结束 init 方法。
-**Swift 标准库中,为了提升性能,String、Array、Dictionary、Set 采取了 Copy On Write 技术**
+### COW 机制
-比如仅当有“写”操作时,才会真正执行拷贝操作
+**值类型的赋值操作:Swift 标准库中的 String、Array、Dictionary 和 Set 确实采用了 Copy-On-Write(COW,写时复制) 技术,这是一种内存优化策略,旨在提升性能并减少不必要的内存复制**
-对于标准库值类型的赋值操作,Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值。
+核心思想:
+
+- **延迟复制**:当多个变量引用同一份数据时,它们共享底层存储,直到某个变量尝试修改数据时,才会真正复制一份独立的副本。
+- **节省资源**:避免对不可变数据进行冗余复制,减少内存占用和计算开销
+
+仅当有“写”操作时,才会真正执行拷贝操作:
+
+- 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
+- 自定义的值类型,比如结构体,在赋值的时候,就会立马发生深拷贝
+
+举个例子
+
+```swift
+var array1 = [1, 2, 3]
+var array2 = array1 // 此时共享底层存储
+
+array2.append(4) // 触发 COW:array1 和 array2 的存储分离
+```
+
+工作过程:
+
+- 赋值时:`array2` 与 `array1` 共享同一块内存
+- 修改时:当 `array2` 被修改时,检查引用计数。如果引用计数 > 1(即存在多个所有者),则复制底层存储,确保修改不影响其他变量
+
+写操作触发检查机制:
+
+- **修改前检查**:执行写操作(删除、添加、修改)时,检查缓冲区的引用计数
+
+- **唯一性检查**:若引用计数为1,则直接修改缓冲区;否则,复制缓冲区并修改新副本
+
+ 伪代码
+
+ ```swift
+ // 伪代码逻辑
+ mutating func append(_ element: Element) {
+ if !isUniquelyReferenced(&buffer) {
+ buffer = buffer.copy() // 复制缓冲区
+ }
+ buffer.append(element) // 修改新副本
+ }
+ ```
+
+#### 什么是缓冲区
+
+ Array 结构体(值类型)
+ +-------------------+
+ | 指向缓冲区的指针 |-----→ Buffer 类(引用类型)
+ | | +----------------+
+ | 其他元数据(长度、容量) | | 存储元素的内存块 |
+ +-------------------+ | [1, 2, 3, ...] |
+ +----------------+
+
+1. **结构体轻量级**:
+ `Array` 结构体本身只包含一个指针和少量元数据(如长度、容量),占用固定大小(如 8 字节指针 + 8 字节长度 + 8 字节容量 = 24 字节)。
+2. **缓冲区动态分配**:
+ 实际存储元素的连续内存块由缓冲区动态分配在堆上,容量可扩展。
+3. **共享与复制**:
+ - **赋值时**:仅复制结构体的指针(浅拷贝),多个数组共享同一缓冲区。
+ - **修改时**:通过 COW(写时复制)机制,仅在需要时复制缓冲区。
@@ -188,11 +251,11 @@ testReferenceType()
下断点,可以看到下面的汇编:
-
+
在调用(汇编的 call)完 `allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
-
+
红色框代表类信息的地址,蓝色框代表引用计数,绿色框代表10,黄色框代表20.
diff --git a/Chapter1 - iOS/1.114.md b/Chapter1 - iOS/1.114.md
index 1dbdd6e..19a10e6 100644
--- a/Chapter1 - iOS/1.114.md
+++ b/Chapter1 - iOS/1.114.md
@@ -43,9 +43,9 @@ print("全局变量", Mems.ptr(ofVal: &p))
print("堆空间", Mems.ptr(ofRef: p))
```
-
+
-
+
代码段:Person.sayHi 0x1000034d0
@@ -183,7 +183,7 @@ print(fn(2)) // 2
print(fn(3)) // 3
```
-
+
@@ -207,7 +207,7 @@ print(fn(3)) // 6
-
+
可以看到上面有 `allocObject` 方法调用,说明产生了堆空间对象,用于存放 `num` 。是由 `var fn = getFn()` 造成的,调用1次 `getFn` 则产生1次堆空间分配,用于保存 num。
@@ -235,31 +235,31 @@ print(fn3(3)) // 4
我们在汇编 `swift_allocObject` 下面下个断点
-
+
-第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x0000600000210000`。此时还没值。
+第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x0000600000210000`。此时还没值。
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
+
可以看到内存数据发生了改变。绿色框内有了值1。
-第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x00006000002042c0`。此时还没值。
+第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x00006000002042c0`。此时还没值。
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
+
-第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x000060000020d460`。此时还没值。
+第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x000060000020d460`。此时还没值。
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
+
打印结果也说明了问题,因为调用3次 `getFn()` 所以会在堆上 alloc 3块内存,用于保存捕获的变量。所以调用 fn1 得到 2,调用 fn2 得到 3,调用 fn3 得到 4。
@@ -279,7 +279,7 @@ print(fn(1, 2)) // 3
在 `var fn = sum` 处下断点,可以看到下面汇编
-
+
我们知道 `leaq` 是从 `%rip + 0x10f` 算出来的地址,赋值给 `%rax`。所以大概可以推测 `%rip + 0x10f` 就是 `sum` 函数地址。LLDM p 打印 `sum` 地址为 `0x0000000100003a30`。
@@ -297,15 +297,15 @@ print(fn(1, 2)) // 3
直奔主题,研究闭包内存
-
+
可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么,LLDB 输入 `si`
-
+
可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算,2个 `ecx` 异或,结果为0,写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx`,`edx` 也就是 `rdx`。走完第6行的汇编,继续看第7、8行
-
+
将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 `, 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。
@@ -315,7 +315,7 @@ print(fn(1, 2)) // 3
继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包)
-
+
基本可以断定:函数会返回一个长度为16 Byte 的内存。分别保存在 `rax` 和 `rdx`上。所以针对性的研究 `rdx` 和 `rax`
@@ -323,7 +323,7 @@ print(fn(1, 2)) // 3
20行的 `rax` 保存了一个看似是 `plus` 的函数地址。具体是:`rip + 0x167 = 0x100003969 + 0x167= 0x100003C37 `
-
+
继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。
@@ -340,13 +340,13 @@ Tips:由于地址是动态生成的,所以真正去调用 plus 的时候一
顺着思路,分析下汇编:
-
+
我们看到25行 `callq *%rax` 存在动态调用,所以需要找到 `%rax` 里的值是哪里来的。然后顺着向上找,找到23行 `movq -0x30(%rbp), %rax`。然后继续向上看到16行 `movq %rax, -0x30(%rbp)`。继续向上看到15行 `movq 0x88db(%rip), %rax`。相当于就是在 `rip + 0x88db ` 处取出8个字节出来,当作函数地址调用(汇编代码的右边写了,`fn1` ),地址为:`0x88db(%rip) = rip + 0x88db = 0x10000392d + 0x88db = 0x10000C208` 。
断点继续放开,在汇编25行处加断点 `callq *%rax`
-
+
可以看到在方法内部,第6行汇编处,直接调用一个代码段的函数地址 `jmp 0x1000039f0 `
@@ -357,13 +357,13 @@ Tips:由于地址是动态生成的,所以真正去调用 plus 的时候一
LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
-
+
`fn1` 函数调用的时候,参数如何传递?
-
+
汇编17行 `movq 0x88d8(%rip), %r13 ; SwiftDemo.fn1 : (Swift.Int) -> Swift.Int + 8`可以看到将返回的 fn1 本身16字节的后8个字节,也就是堆地址空间值,保存到寄存器 `edi` 也就是寄存器 `rdi` 上了。
@@ -371,7 +371,7 @@ LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
然后 LLDB 输入 `si` 去分析 callq 内部
-
+
@@ -379,7 +379,7 @@ LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
继续输入 `si`
-
+
可以看到汇编的第5行 `movq %rsi, -0x50(%rbp)` 将 `rsi` 里的1,保存到 `rbp - 0x50` 处。第6行汇编 `movq %rdi, -0x58(%rbp)` 将 `rdi` 堆地址值保存到 `rbp - 0x58` 处。
@@ -387,16 +387,16 @@ LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
然后真正做 `plus` 加法运算的就是28行的 `addq 0x10(%rsi), %rdi`, 从 `rsi` 堆地址值的第16个字节的地方取出8个字节的值,也就是捕获的外部变量 `num` 再和参数1相加。
-
+
可以看到第6行堆地址空间的值写入到 `rbp -0x58` ,第26行又将 `rbp -0x58` 写入到 `rdi`,29行将 `rdi` 的值,写入到 `rbp - 0x48`,34行将 `rbp - 0x48` 写入到 `rcx`,35行将 `rcx` 的地址值,写入到 `rax` 对应的存储空间。也就是堆上的 `num` 值已经修改,被覆盖了。
-总结:当 `getFn` 内部没有发生闭包的时候,fn1 的地址就是16 Byte,前8 Byte就是 `plus` 的函数地址。当发生函数闭包的时候,`fn1` 的16 Byte,前8 Byte 存储间接调用 `plus` 函数的中转函数,后8 Byte 存储着捕获的且在堆上分配内存的地址值。真正调用 `plus` 的时候会通过寄存器传递2个参数:1个是 fn1 的参数,1个是堆空间的地址值。
+总结:当 `getFn` 内部没有发生闭包的时候,fn1 的地址就是16 Byte,前8 Byte就是 `plus` 的函数地址。当发生函数闭包的时候,`fn1` 的16 Byte,前8 Byte 存储间接调用 `plus` 函数的中转函数,后8 Byte 存储着捕获的且在堆上分配内存的地址值。真正调用 `plus` 的时候会通过寄存器传递2个参数:1个是 fn1 函数的参数,1个是堆空间的地址值。
```swift
- var fn1 = getFn()
+var fn1 = getFn()
fn1(1) // 2
fn1(3) //4
```
@@ -414,11 +414,32 @@ fn2(4) // 5
因为调用了2次 `getFn` 所以堆内存分配了2个被捕获的变量地址,调用过1次 fn1 后,堆地址所指向的内存上的数被修改了。当第二次调用的时候操作的还是同一块堆内存地址,也就是同一个被捕获的堆地址所指向的变量。当调用 fn2 的时候操作的是被捕获的新的一个堆地址空间所指向的变量。
+且 fn1 所占用的16个字节,fn2 所占用16个字节。2者的前8字节内容相同,都是包装了 `plus` 函数的一个地址。
+
+
+
+### “闭包就是对象”
+
+捕获了外部变量的闭包类似于一个类,里面存在存储属性和方法
+
+```swift
+class {
+ var num: Int
+ func fn(_ i: Int) -> Int {
+ return i + num
+ }
+}
+```
+
+
+
+
+
## 自动闭包
-自动闭包是一种自动创建的,用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值
+**自动闭包是一种自动创建的,用来把作为实际参数传递给函数的表达式打包的闭包**。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值
这个语法的好处在于通过写普通表达式代替显示闭包,而使你省略包围函数的形式参数的括号.
@@ -481,7 +502,7 @@ serve(customer: group.remove(at: 0))
但如果函数参数没有加 `@autoclosure`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的
-
+
正确的做法是:要么加 `@autoclosure` 要么在函数调用的时候加 `{}`
@@ -507,4 +528,63 @@ func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> S
}
collectCustomerProviders(group.remove(at: 0))
```
+
+
+
+## 闭包和闭包表达式的区别
+
+### 闭包
+
+定义:闭包是自包含的功能代码块,可以在代码中被传递和使用。它能捕获并存储其所在上下文的常量和变量(即“闭合”环境)
+
+种类:
+
+- 全局函数(有名称,不捕获任何值)
+- 嵌套函数(有名称,可捕获外曾函数的变量)
+- 闭包表达式(匿名,轻量语法,可以捕获上下文变量)
+
+闭包强调的是:捕获上下文的能力。不管是函数还是闭包表达式
+
+### 闭包表达式
+
+定义:是 Swift 中一种简洁的语法格式,用于编写匿名闭包。是闭包的一种实现方式之一(函数和闭包表达式都可以实现闭包)
+
+特点:
+
+- 没有函数名
+- 语法清凉,支持参数类型推断、隐式返回、简写参数名(如 $0、$1)等特性
+- 通常用于作为函数的参数传递
+
+
+
+### 总结
+
+- **闭包**是广义概念,包含所有能捕获上下文的代码块(如函数、闭包表达式)。
+- **闭包表达式**是闭包的一种具体实现方式,专指用轻量语法编写的匿名闭包。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter1 - iOS/1.115.md b/Chapter1 - iOS/1.115.md
index f788965..9d41edd 100644
--- a/Chapter1 - iOS/1.115.md
+++ b/Chapter1 - iOS/1.115.md
@@ -1,13 +1,95 @@
# 属性
+## 实例相关属性分类
+
+### 存储属性
+
+英文叫 Stored Property
+
+- 类似于成员变量的概念
+- 为什么叫存储属性?属性的内存直接存储在实例的内存中
+- 结构体、类,都有存储属性
+- **枚举不可以定义存储属性**
-计算属性的本质是方法。
+
+### 为什么 enum 不可以定义存储属性?
+
+最基础的枚举,内存占用1个字节,只用来存储哪个 case 的索引值
+
+```swift
+enum Season {
+ case spring
+ case summer
+ case antumn
+ case winter
+}
+```
+
+带有关联值的枚举
+
+```swift
+enum Season {
+ case spring(Int, Int, Int)
+ case summer(Int, Int)
+ case antumn(Int)
+ case winter(Bool)
+ case unknown
+}
+var season: Season = Season.spring(1, 2, 3)
+print(MemoryLayout.size) // 3*8 + 1
+print(MemoryLayout.stride(ofValue: season)) // 32
+```
+
+`.spring` 有3个 Int,单个 Int 占8个字节空间,所以红色框代表 spring 的1,蓝色框代表 spring 的2,绿色框代表 spring 的3,黄色框代表枚举的第1个 case,剩余7个字节,为空。后续的7个字节是为了内存对齐而补齐的内存。
+
+1个字节用来表达位置信息。共 `3*8 + 1 = 25`
+
+内存对齐系数,以8 Byte 为单位,对象分配的内存必须是该值的整数倍,所以实际分配后的内存为32字节
+
+
+
+带有原始值的枚举
+
+```swift
+enum Season : Int {
+ case spring = 1
+ case summer = 2
+ case antumn = 3
+ case winter = 4
+}
+
+print(MemoryLayout.size) // 1
+print(MemoryLayout.stride) // 1
+print(MemoryLayout.alignment) // 1
+```
+
+只带有原始值的枚举,同样只占用1个字节,该字节的值为枚举的位置索引(比如1、2),而非原始值。原始值不占用枚举的内存
+
+
+
+总结:
+
+- Swift 的枚举是一种**值类型**,核心目的是表示一组**互斥的、有限的可能性**
+- 允许存储属性,当枚举实例处于不同 case 时,这些属性的存在性和内存占用会变得不可预测
+
+
+
+### 计算属性
+
+英文名为:Computed Property
+
+- 计算属性的本质是方法
+- 不占用实例的内存
+- 计算属性只能用 var,不能用 let
+- 有了 setter,必须有 getter
+- 可以只有 getter,没有 setter
+- **枚举、结构体、 类都可以定义计算属性**
```swift
struct Circle {
- var radius: Int
- var diameter: Int {
+ var radius: Int // 存储属性
+ var diameter: Int { // 计算属性
set {
radius = newValue/2
}
@@ -21,6 +103,10 @@ var circle = Circle(radius: 10)
circle.diameter = 24
// print(circle.radius) // 12
let diameter = circle.diameter
+
+print(MemoryLayout.size) // 8
+print(MemoryLayout.stride) // 8
+print(MemoryLayout.alignment) // 8
```
计算属性 `y` 等价于下面的代码:
@@ -36,11 +122,94 @@ getDiameter () {
然后通过汇编来窥探下,在 `circle.diameter = 22` 处加断点,可以看到本质上调用的就是 `setter` 方法,`setter` 内部的实现就不一一窥探了
-
+
然后断点继续,在 `let diameter = circle.diameter` 处加断点,可以看到调用了 getter 方法
-
+
+
+计算属性的本质就是方法,看上去是属性,但是不占用结构体的内存。而是独立在代码段中,所以只占用1个 Int 即8个字节的大小。
+
+
+
+- **计算属性可以有只读计算属性**
+
+ 也就是只有 getter,没有 setter 方法
+
+ ```swift
+ struct Circle {
+ var radius: Int // 存储属性
+ var diameter: Int { radius*2 }
+ }
+ ```
+
+
+
+### 延迟存储属性
+
+常规写法
+
+```swift
+class Car {
+ init () {
+ print("Car init")
+ }
+
+ func run () {
+ print("Car is running")
+ }
+}
+
+class Person {
+ let car:Car = Car()
+ init () {
+ print("Person init")
+ }
+
+ func goOut() {
+ car.run()
+ }
+}
+
+
+let p = Person()
+print("---")
+p.goOut()
+
+// console
+Car init
+Person init
+---
+Car is running
+```
+
+但是想实现一个需求,就是在 Person 初始化的时候先不初始化 Car,当调用 Person 对象的 goOut 方法的时候再初始化,该怎么办?
+
+**使用 lazy 可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化**
+
+对 Person 改造如下
+
+```swift
+class Person {
+ lazy var car:Car = Car()
+ init () {
+ print("Person init")
+ }
+
+ func goOut() {
+ car.run()
+ }
+}
+// console
+Person init
+---
+Car init
+Car is running
+```
+
+注意:延迟属性 lazy 必须和 var 搭配使用,不能是 let
+
+
## 异同点
@@ -131,11 +300,40 @@ let season = Season.summer
print(season.rawValue)
```
-
+
-通过汇编可以可以看到,在调用 `enum` 的 `rawvalue` 的时候本质是通过计算属性调用 `getter` 来实现的。
+通过汇编 `SwiftDemo.Season.rawValue.getter` 可以看到,在调用 **`enum` 的 `rawvalue` 的时候本质是通过计算属性调用 `getter` 来实现的**。
+类似于:
+```swift
+enum Season: Int {
+ case spring = 10
+ case summer = 20
+ case autumn = 30
+ case winter = 40
+
+ var rawValue: Int {
+ get {
+ switch self {
+ case .spring:
+ return 10
+ case .summer:
+ return 20
+ case .autumn:
+ return 30
+ case .winter:
+ return 40
+ }
+ }
+ }
+}
+
+let season = Season.summer
+print(season.rawValue)
+```
+
+也侧面证明了 rawValue 不占用枚举的内存空间(是方法,存储在代码段)
@@ -143,6 +341,8 @@ print(season.rawValue)
- 可以为非 `lazy` 的 `var` 存储属性设置属性观察期
+- 计算属性由于有 set 和 get,因此不能有属性观察器 willSet 和 didSet
+
- 在初始化器中设置属性值不会触发 `willSet`、`didSet`
- 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上
@@ -231,11 +431,11 @@ width is 20, side is 4, girth is 80
-
+
然后看到17行的关键代码,LLDB 输入 `si`,可以看到在第6行 `movq $0x14, (%rdi)`,将16进制的 `0x14` 也就是20,移动到指定的内存地址 `rdi` 上
-
+
因为 `struct` 结构体内存布局中,成员变量在内存中是连续存储的。所以 `struct `的地址也就是 `struct` 中第一个成员变量 `width` 的地址。
@@ -262,13 +462,13 @@ width is 5, side is 4, girth is 20
在 `changeValue(&shape.girth)` 处下断点,查看汇编
-
+
核心思路:方法参数用 `inout`修饰,则传递的是引用(内存地址)。
- 汇编19行 `callq 0x1000030e0 ; SwiftDemo.Shape.girth.getter : Swift.Int at main.swift:16` 调用了 `girth` 计算属性的 `getter`,`getter` 的返回值存放在寄存器 `rax` 上
-- 20行将 `movq %rax, -0x28(%rbp)` 函数返回值 `rax` 存放在 `main` 函数的栈空间上(虽然没有写调用函数,但是代码主入口就是 `main` 函数),且 `rbp` 到 `rsp` 之间都是函数的栈空间
+- 20行将 `movq %rax, -0x28(%rbp)` 函数返回值 `rax` 存放在 `main` 函数的栈空间上(虽然没有写调用函数,但是代码主入口就是 `main` 函数),且 `rbp` 到 `rsp` 之间都是函数的栈空间。也就是一个局部变量
- 21行 `leaq -0x28(%rbp), %rdi` 将栈空间上 `-0x28(%rbp)` 的地址值赋值给 `rdi` 寄存器
@@ -276,7 +476,7 @@ width is 5, side is 4, girth is 20
- LLDB 输入 `si` 查看 changeValue 内部。可以看到第6行 `movq $0x14, (%rdi)` 直接将20赋值给 `rdi`
-
+
@@ -311,7 +511,7 @@ width is 10, side is 20, girth is 200
在 `changeValue(&shape.side)` 处添加断点,查看汇编
-
+
分析:
@@ -329,11 +529,15 @@ width is 10, side is 20, girth is 200
- LLDB 输入 `si`,可以看到 `setter` 方法内部,调用了 `willSet`、`didSet`
-
+
-总结:带有属性观察器的存储属性,如果调用的方法参数是 `inout` 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很多简单,满足了修改值的需求。但是属性观察器的 `willSet`、`didSet` 就没办法触发了。所以为了触发属性观察器系统的设计是:
+总结:带有属性观察器的存储属性,如果调用的方法参数是 `inout` 类型,系统真正的实现,并不是直接将 inout 参数的地址传进去,因为传进去很简单,满足了修改值的需求,但属性观察器的 `willSet`、`didSet` 就没办法触发了。
+
+问题症结是:**直接传递 inout 参数的地址,可以满足直接修改值的需求,但直接修改没办法触发属性观察器的 willSet 和 didSet**
+
+所以为了触发属性观察器系统的设计是:
- 第一步:先将传递进去的地址,保存在函数的栈地址空间内的某个内存上
- 第二步:然后调用带有 `inout` 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
@@ -348,11 +552,10 @@ width is 10, side is 20, girth is 200
### 总结
1. 如果实参有内存地址,且没有设置属性观察器和计算属性,实现是直接将实参的内存地址传入带 `inout` 函数
-
-2. 如果实参是计算属性或者设置了属性观察器:系统采用了 Copy In Copy Out 的策略。
- 1. 调用带 `inout` 函数时,先复制实参的值,产生副本 (getter,栈空间上的局部变量)
- 2. 将副本的内存地址传入带 `inout` 函数(副本进行引用传递),在函数内部修改副本的值
- 3. 函数返回后,再将副本的值覆盖实参的值(setter,willSet、set)
+2. 如果实参是计算属性或者设置了属性观察器:系统采用了 **Copy In Copy Out** 的策略
+ - 调用带 `inout` 函数时,先复制实参的值,产生副本 (get。栈空间上的局部变量 rbx + offset)
+ - 将副本的内存地址传入带 `inout` 函数(副本进行引用传递),在函数内部修改副本的值
+ - 函数返回后,再将副本的值覆盖实参的值(set。willSet、didSet)
@@ -368,9 +571,9 @@ width is 10, side is 20, girth is 200
- 存储类型属性可以是 `let`
-- 存储属性可以用 `class`、`static` 修饰
+- 存储属性可以用 `static` 修饰
-- 枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性
+- **枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性**
```swift
enum Season {
@@ -398,9 +601,11 @@ width is 10, side is 20, girth is 200
+### 内存角度分析:类型属性存储在哪
+
Demo1:
-
+
`movq $0xa, 0x86d1(%rip) ` num1 的地址为: `rip + 0x86d1 = 0x100003b0f + 0x86d1 = 0x10000C1E0 `
@@ -414,7 +619,7 @@ Demo1:
Demo2
-
+
`movq $0xa, 0x8b65(%rip)` num1 的内存为 `rip + 0x8b65 = 0x1000037c3 + 0x8b65 = 0x10000C328 `
@@ -422,12 +627,47 @@ Demo2
`movq $0xc, 0x8b38(%rip) ` num1 的内存为 `rip + 0x8b38 = 0x100003800 + 0x8b38 = 0x10000C338 `
-可以看到 `0x10000C328` `0x10000c330` `0x10000C338` 也是内存连续的。所以类型属性就是带有访问控制(必须通过类来访问)的全局变量
+可以看到 `0x10000C328` `0x10000c330` `0x10000C338` 也是内存连续的。所以**类型属性就是带有访问控制(必须通过类来访问)的全局变量**
-Demo3
+### 类型属性是线程安全的
-类型属性如何保证线程安全的?如何保证只会初始化一次。
+看个 Demo
-底层会调用 `swift_once` 进而调用 `dispatch_once_t`,`dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
+```swift
+class Manager {
+ static var count = Int.random(in: 1...100)
+}
+
+Manager.count = 10
+Manager.count = 11
+```
+
+下断点可以看到,调用了**地址访问器函数**。为什么需要地址访问器函数:
+
+- 线程安全初始化首次访问时触发初始化(可能包含 `dispatch_once` 逻辑)
+- 抽象内存访问隐藏实际存储位置(可能位于不同段或延迟分配)
+- 支持属性观察(didSet)通过封装访问点插入回调逻辑
+
+
+
+lldb 输入 si 查看具体实现
+
+
+
+可以看到底层调用了 `swift_once` 函数,函数传递了2个参数, rsi 存储 dispatch_once 的 block 参数,rdi 存储了 onceToken
+
+继续敲 si 可以看到底层调用的就是 GCD 的 `dispatch_once` 函数。
+
+
+
+
+
+类型属性如何保证线程安全的?如何保证只会初始化一次
+
+底层会调用 `swift_once` 进而调用 `dispatch_once_f`,`dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
+
+所以类型存储属性底层通过 dispatch_once 保证了只会初始化1次,线程安全。
+
+
diff --git a/Chapter1 - iOS/1.116.md b/Chapter1 - iOS/1.116.md
index 34450d0..1ecb47f 100644
--- a/Chapter1 - iOS/1.116.md
+++ b/Chapter1 - iOS/1.116.md
@@ -42,7 +42,7 @@ print(Mems.memStr(ofRef: worker))
```
- 内存对齐都是16 Byte 的整数倍
-- 一个类内存中,至少占16字节的内存。前8位是类信息、其次的8位是引用计数信息,最后跟属性内存
+- 一个类内存中,至少占16字节的内存。前8位是类信息、其次的8位是引用计数信息,接着是属性内存区域
- 由于类存在继承,所以子类中,前16字节存储类信息和引用计数信息,其次是属性内存,存在继承的话,前面的属性是父类的属性,后面才是自己的属性。
所以:
@@ -159,7 +159,13 @@ Dog.speak() // Animal speak dog is bark
但如果将 `Animal` 方法的 `class` 改为 `static`,就无法 `override` 了
-
+
+
+
+
+
+- 如果父类的方法是被 class 修饰的,子类继承后重写时,可以将 class 改为 static。
+- 虽然子类可以将父类方法的 class 改为 static。但影响的是当前子类的子类,无法再重写方法了。
@@ -222,15 +228,27 @@ Circle did set radius 1 2
- 被 final 修饰的类,禁止被继承
+## Swift 协议(Protocol)中声明的属性必须使用 var 关键字
+
+协议的核心目标:定义“能力”而非“实现”
+协议是描述类型应该具备什么能力的抽象蓝图,而不是具体实现。
+属性在协议中本质上定义的是对外的访问接口(读、写),而不是存储方式(常量或变量)。
+因此,**协议中的属性声明必须明确其访问权限({ get } 或 { get set }),而 var 是唯一能表达这种动态性的关键字**。
+
+- 协议中的属性用 var:统一表示“访问接口”,支持动态约束({ get } 或 { get set })。
+- 遵循类型可用 let 或 var:只要满足协议的访问权限要求即可。
+- let 无法用于协议:因其无法表达可写性,违背协议动态描述能力的初衷。
+
+
+
## 多态的实现原理
-OC: Runtime
-
-C++:虚表
-
-Swift:没有 Runtime,所以多态的实现类似 C++
+- OC: Runtime
+- C++:虚函数表
+- Swift:没有 Runtime,所以多态的实现类似 C++
+来个 Demo
```swift
class Animal {
func speak () {
@@ -276,9 +294,9 @@ Animal sleep
在 `animal.speak()` 处加断点,可以看到
-
+
-
+
解释:
@@ -292,20 +310,43 @@ Animal sleep
画了张图,也就是说 `rax` 中存放了 Dog 对象内存中的前8个字节,也就是下图的最右侧
-
-
-
+
核心是上面的内存布局图。结合汇编就知道多态是如何实现的。
+1. Swift 多态的实现原理
+
+Swift 的多态通过 虚函数表(vtable) 实现,这是一种 编译时确定的动态派发机制。其核心逻辑是:
+
+- 每个类类型在编译时会生成一个 虚函数表,表中存储了类的方法实现指针
+- 子类继承父类时,会复制父类的虚函数表,并替换重写方法的指针为自己的实现
+- 在运行时,通过对象的 类型元数据指针 找到对应的虚函数表,从而调用正确的方法
+
+动态派发与静态派发的区别:
+- 动态派发:通过虚函数表实现(例如普通类方法),允许子类重写。
+- 静态派发:编译时直接绑定方法地址(例如 final 方法、static 方法、结构体和枚举的方法),性能更高
+
+2. 虚函数表(vtable)的作用
+
+虚函数表的核心作用是为 动态派发 提供支持:
+- 方法重写:子类通过覆盖虚函数表中的方法指针,实现多态。
+- 运行时方法查找:对象调用方法时,通过虚函数表找到实际的方法实现。
+- 类型安全性:保证方法调用的正确性,即使对象被向上转型(例如 父类引用 = 子类对象)。
+
+
总结: **虚函数表**(vtable)是一种用于实现动态多态性的机制,通常用于面向对象的编程语言中(C++ 也是一样)。在 Swift 中,虚函数表用于存储类或协议中方法的地址,以便在运行时进行动态分派。
在 Swift 中,虚函数表的作用是为每个类或协议创建一个表,其中包含了对应方法的地址。当调用对象的方法时,运行时系统会根据对象的实际类型查找对应的虚函数表,然后调用表中存储的方法地址,从而触发特定的实现。
虚函数表在 Swift 中的作用是实现动态分派,使得在运行时根据对象的实际类型确定调用的具体实现。这为 Swift 中的多态性提供了基础,允许相同的方法名称根据对象的类型触发不同的实现,从而实现灵活的对象行为。
+最小内存占用:一个没有属性的类对象至少占用 16 字节(类型元数据指针 8 字节 + 引用计数 8 字节)。
+属性存储:属性从第 17 字节开始存储
+引用计数细节:
+- 默认情况下,引用计数直接存储在对象头部。
+- 当引用计数溢出时,Swift 会使用 Side Table 扩展存储,此时对象头部的引用计数字段会指向 Side Table。
## 类的类型信息存储在哪
@@ -323,6 +364,15 @@ var dog2 = Dog()
## 初始化器
+### 初始化器可以继承
+- convenience 便捷初始化器只可以横向调用,不可以纵向调用(比如子类继承父类后,子类重写指定初始化器的时候,必须加 override 且子类中只能调用父类的指定初始化器,不能调用便捷初始化器)
+- 便捷初始化器是不能被子类调用的
+
+
+### 自动继承
+- 如果子类没有自定义任何指定初始化器,则会自动继承父类所有的指定初始化器
+
+
### require
- 用 required 修饰的指定初始化器,表明其所有的子类都必须实现该初始化器(通过继承或者重写来实现)
@@ -330,7 +380,7 @@ var dog2 = Dog()
-## 可失败初始化器
+### 可失败初始化器
类、结构体、枚举都可以使用 `init?` 定义可失败初始化器,也可以用 `init!` 来定义可失败初始化器。区别下面会讲
@@ -365,7 +415,7 @@ print(num) // Optional(12)
1. 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器。因为在外部调用的时候,不知道到底是使用哪个初始化方法。编译器会报错 `Invalid redeclaration of 'init(_:)'`
-
+
2. 可以用 `init!` 来定义隐式解包的可失败初始化器
@@ -403,7 +453,7 @@ print(num) // Optional(12)
}
```
-
+
且前面的写法比较危险,假设第一个 `init?` 返回 `nil`,第二个 `convenience init()` 去对 nil 强制解包,则会 crash
@@ -441,7 +491,85 @@ print(num) // Optional(12)
+### OC alloc init,为什么 Swift 只需要 init?
+1. 语言设计哲学的分歧
+
+ OC 显示控制与动态性。OC 是 C 的超集,继承了对底层内存管理的直接控制。`alloc` 和 `init` 的分离体现了**职责分离**原则:
+
+ - **`alloc`**:类方法(`+alloc`),负责**内存分配**(计算对象大小、向系统申请内存空间,返回一个“空白”实例)。
+ - **`init`**:实例方法(`-init`),负责**状态初始化**(设置属性默认值、建立对象依赖关系等)。
+ - 这种分离允许开发者灵活干预内存分配(例如自定义 `+allocWithZone:`)或初始化过程(例如工厂方法 `+new`)。
+
+ Swift 简洁性与安全性
+
+ - Swift 作为现代语言,追求代码简洁和安全性。`Person()` 的语法**隐藏了内存分配细节**,开发者只需关注初始化逻辑。编译器会自动插入内存分配代码(类似 `__allocating_init`)并调用初始化方法。类似 `let person = Person.__allocating_init()`
+ - Swift 强制在初始化完成前为所有存储属性赋值,并通过两段式初始化(Phase 1: 分配内存并设置默认值;Phase 2: 自定义初始化)避免未定义状态
+
+2. 编译器与运行时的工作
+
+ OC:运行时开放性
+
+ Objective-C 的 `+alloc` 方法由运行时动态处理。开发者可以重写 `+alloc` 或 `+allocWithZone:` 实现自定义内存分配策略(例如对象池、单例)。为了实现这种灵活性,更需要显式调用 alloc
+
+ ```objective-c
+ // 自定义 alloc 方法
+ + (instancetype)alloc {
+ if (单例条件) {
+ return sharedInstance;
+ }
+ return [super alloc];
+ }
+ ```
+
+ Swift: 编译时的静态优化
+
+ - 内存分配的编译时确定:Swift 的对象大小和内存布局在编译时即可确定(值类型更是完全静态)。编译器直接生成内存分配指令,无需运行时动态计算。
+ - 初始化器的静态派发:Swift 的初始化方法通过静态派发(或虚表派发)调用,无需 Objective-C 的消息转发开销。编译器能安全地合并内存分配和初始化步骤。
+
+为什么 Swift 可以省略 `alloc`?
+
+1. **编译器自动化**:内存分配由编译器隐式插入代码处理,无需开发者参与。
+2. **类型安全性**:严格的初始化规则确保对象在初始化完成后处于合法状态。
+3. **现代语法设计**:隐藏底层细节,提升代码可读性和编写效率。
+4. **静态优化**:编译时确定对象内存布局,无需运行时动态分配逻辑。
+
+而 Objective-C 保留 `alloc` 和 `init` 的分离,既是对历史的兼容,也为需要精细控制内存或动态行为的场景保留了灵活性。
+
+
+
+### deinit
+
+deinit 也叫反初始化器,类似于 C++ 的析构函数、OC 中的 dealloc 方法
+
+当类的实例对象被释放内存时,就会调用实例对象的 deinit 方法
+
+```swift
+class Person {
+ deinit {
+ print("Person deinit")
+ }
+}
+
+class Student: Person {
+ deinit {
+ super.deinit() // Deinitializers cannot be accessed
+ print("Student deinit")
+ }
+}
+
+func test() {
+ let st = Student()
+}
+test()
+```
+
+上述代码编译报错:Deinitializers cannot be accessed
+
+deinit 的基本规则:
+
+- **不可继承性**:`deinit` 本身不会被继承。每个类必须定义自己的 `deinit` 方法(显式或隐式)。
+- **自动链式调用**:无论子类是否重写 `deinit`,父类的 `deinit` 方法总会在子类析构完成后被自动调用,无需手动调用 `super.deinit()`。
## 可选链
@@ -465,6 +593,128 @@ print(result!)
+## 可选项 Optional 的本质
+
+可选项的本质是 **enum 类型 + 泛型**
+
+```swift
+@frozen public enum Optional : ExpressibleByNilLiteral {
+
+ /// The absence of a value.
+ ///
+ /// In code, the absence of a value is typically written using the `nil`
+ /// literal rather than the explicit `.none` enumeration case.
+ case none
+
+ /// The presence of a value, stored as `Wrapped`.
+ case some(Wrapped)
+
+ /// Creates an instance that stores the given value.
+ public init(_ some: Wrapped)
+}
+```
+
+`var age:Intt? = 20` 是语法糖,本质是 `var age:Optional = .some(20)` 所以下面写法是一样的
+
+```swift
+// 写法1
+var age1: Int? = 30
+age1 = 20
+age1 = nil
+
+// 写法2
+let age2: Optional = .some(30)
+age2 = 20
+age2 = .none
+```
+
+一些不合格的写法:
+
+Optional 是 enum + 泛型,所以必须要设置泛型类型
+
+```swift
+var age = Optional.none // Generic parameter 'Wrapped' could not be inferred
+```
+
+`if let` 是专门用于 Optional 解包的语法糖。
+```swift
+var age: Int? = .none
+
+if let a = age {
+ print(a)
+} else {
+ print("nil")
+}
+```
+等价于
+```swift
+if age != nil {
+ let a = age!
+ print(a)
+} else {
+ print("nil")
+}
+```
+
+- 只有非 nil 时,才会进入 if 分支,并将解包后的值绑定到 a
+- nil 时,直接进入 else 分支
+
+`switch case` 是通用模式匹配,不针对 Optional 做特殊处理。
+```swift
+switch age {
+ case let a:
+ print("age is ", a)
+ case nil:
+ print("nil")
+}
+```
+- 第一个 case let a 会匹配所有可能的值(包括 .some(30) 和 .none,即 nil),因为 a 的类型是 Int?。
+- 一旦匹配到第一个 case,后续的 case nil 会被跳过。除了第一个之外的 case 都无法执行
+**要在 switch 中正确处理 Optional,需明确匹配 .some 和 .none,需要用 `case let a?`**
+```swift
+switch age {
+ case let a?:
+ print("age is ", a)
+ case nil:
+ print("nil")
+}
+```
+下面写法效果等价于
+```swift
+var age: Int? = .none
+age = nil
+
+if let a = age {
+ print(a)
+} else {
+ print("nil")
+}
+
+switch age {
+ case let a?:
+ print("age is ", a)
+ case nil:
+ print("nil")
+}
+
+switch age {
+ case let .some(a):
+ print("age is ", a)
+ case nil:
+ print("nil")
+}
+```
+双层嵌套可选型:
+```swift
+var age1 = Optional.some(Optional.some(30))
+var age2: Int?? = 30
+var age3: Optional = .some(.some(30))
+var age4: Optional = .some(30)
+print(age1!!)
+print(age2!!)
+print(age3!!)
+print(age4!!)
+```
## X.self , X.Type, AnyClass
@@ -472,8 +722,6 @@ print(result!)
- `X.self` 是一个元类型(metadata)的指针,metadata 存放着类型相关信息
- `X.self` 属于 `X.type` 类型
-
-
通过汇编探究下背后细节
```swift
@@ -484,21 +732,25 @@ var personType: Person.Type = Person.self
-
+
在第二行代码下断点,可以看到关键的汇编是第8行和第12行:
- 第14行可以看到 `rip + 0x89ae = 0x10000396a + 0x89ae = 0x10000C318 `,明显是一个堆地址空间,也就是全局变量 `person`
+
- 第15行可以看到 `rip + 0x89af = 0x100003971 + 0x89af = 0x10000C320 `,明显是一个堆地址空间,也就是全局变量 `personType`
+
- 顺着关键代码找上去,看看 `rax`、`rcx` 的值是哪来的
- 第8行调用函数后可以看到 Xcode 的说明,获取 `metadata`,函数返回值保存到 `rax`,LLDB 打印出为 `0x000000010000c248`
- 第11行初始化堆内存后,将地址保存到寄存器 `rax`,LLDB 打印出地址为 `0x0000600000004010`,然后查看 `0x0000600000004010` 对应的对象信息,可以看到内存的前8个字节的值,就是上面得到的 `metadata` 对象的地址值
+- person 对象的内存布局中,前8个字节就是 personType 的地址。
+
- `metadata` 结构类似下图右侧
-
+
@@ -510,9 +762,29 @@ var person: Person = Person()
print(Person.self == type(of: person)) // true
```
+`AnyObject.Type` 的用法
+
+```swift
+class Person {
+
+}
+class Student: Person {
+
+}
+
+var anyType: AnyObject.Type = Person.self
+anyType = Student.self
+
+public typealias AnyClass = AnyObject.Type
+
+var anyType2: AnyClass = Person.self
+anyType2 = Student.self
+
+```
-## 元类型的应用
+
+### 元类型的应用
```swift
class Person {
@@ -535,14 +807,95 @@ var people: Array = createInstance([studentType, workerType])
print(people) // [SwiftDemo.Student, SwiftDemo.Worker]
```
+注意:为了保证子类一定有 `init(){ }` 方法,在基类中需要声明为 `required init() {}`
+
+
+
+## Swift 继承和基类
+
+```swift
+import Foundation
+class Person {
+ var age:Int = 0
+}
+
+class Student: Person {
+ var no:Int = 0
+}
+
+print(class_getInstanceSize(Student.self)) // 32
+print(class_getSuperclass(Student.self)!) // Person
+print(class_getSuperclass(Person.self)!) //_TtCs12_SwiftObject
+
+```
+
+分析:
+
+- Student 类继承自 Person 类,类的内存布局中:
+
+ - isa:前8个字节是 isa 指针,指向类的元数据(AnyObject.Type),包含类型信息、方法表。 虚函数表(vtable)存储在类的元数据中,虚函数表并不直接存储在实例内存中,而是通过 isa 指向的类元数据(ClassMetadata)中。
+
+ 调用方法时候,运行时通过 isa 找到类元数据,再从元数据中读取 vtable 地址,最终定位到具体方法实现地址
+
+ - 引用计数:紧接着的8个字节存储引用计数信息
+
+ - 紧接着是从 Person 继承来的 age 属性,占8个字节。然后是自己的 no 属性,也占8个字节。
+
+- Student 类的父类是 Person 类,打印没问题
+
+- Swift 类的隐式根类
+
+ - Swift 有个隐藏基类:`Swift._SwiftObject`
+ - Person 类没有显式继承其他类,它默认会隐式继承自 Swift 的内部根类 `SwiftObject`。这个类是 Swift 运行时的基础,类似于 Objective-C 的 `NSObject`,但独立存在。蕾丝
+ - `_TtCs12_SwiftObject` 是 `SwiftObject` 类在 Objective-C 运行时中的**符号化名称**(mangled name)
+ - `_TtC`:Swift 类的固定前缀。
+ - `s12`:模块名或类名的编码长度。
+ - `SwiftObject`:实际类名
+
+- 与 Objective-C 运行时的交互
+
+ - **`class_getSuperclass` 的局限性**
+ `class_getSuperclass` 是 Objective-C 运行时函数,返回的是 Objective-C 运行时能识别的父类。由于 `SwiftObject` 是 Swift 内部类,Objective-C 运行时无法直接理解它,因此返回其符号化名称。
+ - **Foundation 的影响**
+ 导入 `Foundation` 会引入 Objective-C 运行时,但不会改变 Swift 类的默认根类。只有显式继承 `NSObject` 的 Swift 类才会在 Objective-C 运行时中以 `NSObject` 为根类。
+
+
+
+`Swift._SwiftObject` 的作用:
+
+- **纯 Swift 类的默认父类**
+ 当 Swift 类不显式继承 `NSObject` 或其他类时,默认隐式继承自 `Swift._SwiftObject`。
+- **提供基础能力**
+ 类似于 Objective-C 的 `NSObject`,`Swift._SwiftObject` 提供了:
+ - 内存管理:引用计数(通过 `swift_retain`/`swift_release`)。
+ - 类型元数据:存储类的方法表、属性信息等。
+ - 动态派发:支持方法重写和协议扩展。
+
+通过源码查看 Swift 类的内存布局
+
+```swift
+struct HeapObject {
+ HeapMetadata const *metadata; // 包含 isa 和引用计数
+
+ SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
+ ...
+}
+
+#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \
+ InlineRefCounts refCounts
+```
+
+`HeapObject` 是 Swift 对象的基础结构,包含 `isa` 和引用计数字段
+
## Self
-Self 一般用作返回值类型,限定返回值跟方法调用者必须是同一类型(也可以当作参数类型)
+**`Self` 是动态类型,会随着子类调用而改变**。Self 一般用作返回值类型,限定返回值跟方法调用者必须是同一类型(也可以当作参数类型)
```swift
protocol Runable {
+ init()
func copy() -> Self
}
@@ -561,6 +914,19 @@ var student = Student()
print(student.copy()) // Student
```
+QA:上面的 Person 类在遵循 Runable 协议,实现 copy 方法,方法里能返回 `Person()` 吗?
+
+```swift
+class Person: Runable {
+ required init() {}
+ func copy() -> Self {
+ Person()
+ }
+}
+```
+
+答:不行。因为 Person 类可以被继承,如果 copy 方法里写死返回 Person 实例。Student 继承 Person 后,copy 方法会也会返回 Person 对象。但协议要求的是返回当前类的对象,这明显违法了协议“契约”
+
@@ -585,7 +951,7 @@ print(student.copy()) // Student
就像上面[多态实现的原理](#target-anchor)这里讲到的一样
-
+
查表是一种简单、易实现、性能可预知的方式。然而,这种派发方式比起直接派发来说,还是慢了一点(从字节码的角度来看,多了两次读和一次跳转。由此带来了性能损耗)。另一个慢的原因在于编译器可能会由于函数内执行的任务,导致无法优化(如果函数带有副作用的话)
diff --git a/Chapter1 - iOS/1.117.md b/Chapter1 - iOS/1.117.md
index d52cc2a..a02889d 100644
--- a/Chapter1 - iOS/1.117.md
+++ b/Chapter1 - iOS/1.117.md
@@ -197,7 +197,7 @@ class Stack: Stackable {
-## Swift 范型本质
+## Swift 泛型本质
```swift
func swapValue(_ value1: inout T, _ value2: inout T) {
@@ -223,13 +223,13 @@ swapValue(&s1, &s2)
- 然后调用 `swapValue` 方法
- 后续的 `String` 的 `SwapValue` 过程类似
-所以编译器最后在执行的时候,会将范型真正的类型对应的 metadata 信息当作函数参数,传递进去,再去执行函数
+所以编译器最后在执行的时候,会将泛型真正的类型对应的 metadata 信息当作函数参数,传递进去,再去执行函数
-## 范型类型约束
+## 泛型类型约束
-范型必须遵循协议,可以在方法后加 `<>`,在 `<>` 内写范型 `T: 需要继承的类 & 协议` (也可以是其他名字,大多数语言都写 T),
+泛型必须遵循协议,可以在方法后加 `<>`,在 `<>` 内写泛型 `T: 需要继承的类 & 协议` (也可以是其他名字,大多数语言都写 T),
```swift
protocol Runable {}
@@ -239,7 +239,7 @@ func swapValue(_ a: inout T, _ b: inout T) {
}
```
-另一种场景是在方法参数是某个范型且遵循协议后,对其范型有更多限制,则用 `where` 去处理。如下例子
+另一种场景是在方法参数是某个泛型且遵循协议后,对其泛型有更多限制,则用 `where` 去处理。如下例子
```swift
protocol Stackable {
diff --git a/Chapter1 - iOS/1.119.md b/Chapter1 - iOS/1.119.md
index 0d92c83..5efabe8 100644
--- a/Chapter1 - iOS/1.119.md
+++ b/Chapter1 - iOS/1.119.md
@@ -1,15 +1,13 @@
# 剖析 Swift String
-
-
-带着问题研究下 Swift 中的 String
+ 带着问题研究下 Swift 中的 String
- 1个 String 变量占用多少内存?
- String 存放在什么位置?
-
+
@@ -21,7 +19,7 @@ var str1: String = "0123456789"
实验很简单,就一行代码。来窥探下 String 的初始化和内存结构。出发断点看到下面汇编:
-
+
简单分析下:
@@ -47,7 +45,7 @@ QA:这个10是什么东西?1是什么东西?
var str1: String = "01234"
```
-
+
可以看到其他和上面的没变化,唯一不同的是 `movl $0x5, %esi` 将5赋值给寄存器 `esi`,即寄存器 `rsi`。实验的字符串 "01234" UTF8 字符个数为5
@@ -57,7 +55,7 @@ var str1: String = "01234"
var str1: String = "01234😄"
```
-
+
可以看到将9赋值给寄存器 `esi` 即 `rsi`,字符串赋值给寄存器 `rdi`,`xorl %edx, %edx` 异或运算结果 0 赋值给寄存器 `edx` 即 `rdx`,也就是不纯粹为
@@ -95,7 +93,7 @@ extension String: _ExpressibleByBuiltinStringLiteral {
继续探索:
-
+
可以看到 `str1` 地址为 `rip + 0x8853 = 0x1000039a5 + 0x8853 = 0x10000C1F8 `,然后读取对应堆上的内容为:
@@ -130,7 +128,7 @@ var str1: String = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1))
```
-
+
分析下:
@@ -142,15 +140,21 @@ print(Mems.memStr(ofVal: &str1))
- 第20行 `movabsq $0x7fffffffffffffe0, %rdx` 则会把立即数 `0x7fffffffffffffe0` 移动到寄存器 `rdx`
-- 第21行 `addq %rdx, %rdi` 将 `rdx` + ` rdi` 的值赋值给 `rdi`,也就是 `rdi` 存放了: 字符串的真实地址 + `0x7fffffffffffffe0。`所以字符串真实地址 = `rdx 的地址` - `0x7fffffffffffffe0`。寄存器 `rdi` 读取出地址为 `0x8000000100007800` 。所以字符串真实地址为:`0x8000000100007800` - `0x7fffffffffffffe0` = `0x100007820`,LLDB 读取下 `x 0x100007820 ` 看到 30、31...46刚好是字符串 `0123456789ABCDEF` 的 ASCII 值。
+- 第21行 `addq %rdx, %rdi` 将 `rdx` + ` rdi` 的值赋值给 `rdi`,也就是 `rdi` 存放了: 字符串的真实地址 + `0x7fffffffffffffe0 相加后的值。`
- 所以 `字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
+ 所以字符串真实地址 = `rdx 的地址` - `0x7fffffffffffffe0`。
+
+ 寄存器 `rdi` 读取出地址为 `0x8000000100007800` 。所以字符串真实地址为:`0x8000000100007800` - `0x7fffffffffffffe0` = `0x100007820`。
+
+ LLDB 读取下 `x 0x100007820 ` 看到 30、31...46刚好是字符串 `0123456789ABCDEF` 的 ASCII 值。
+
+ 所以 **`字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`**,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
- 经过23行后 `orq %rdi, %rdx` 可以看到 `rdx` 、`rdi` 里面存储的都是:`字符串真实地址` + `0x7fffffffffffffe0`
- LLDB 输入 finish 结束函数细节,外部可以看到第10行 `movq %rdx, 0x8864(%rip) ` 将 `rdx` 寄存器里的值(也就是:`字符串真实地址` + `0x7fffffffffffffe0` )赋值给 `str1` 指针的后8个字节
-
+
@@ -168,13 +172,13 @@ var str1: String = "0123456789ABCDEF"
字符串地址为 ` 0x10000397f + 0x3ea1 = 0x100007820`,看着像全局变量、也有可能是常量区?字符串内容写死的情况下,编译器编译后内存地址应该可以确定,那到底在什么地方呢?利用 `MachOView` 窥探下 `MachO` 文件吧
-
+
利用 MachOView 打开如下
-
+
@@ -184,12 +188,23 @@ X86_64 架构下,我们在 MachOView 看到的地址,真实对应地址为
在 macOS 和 iOS 的二进制文件(如 Mach-O 格式的可执行文件或动态库)中,`Section64` 是一个结构体,用于描述二进制文件中的一个段(section)。每个段包含特定类型的数据或代码,并且具有特定的属性,比如是否可写、是否可执行等。
- `Section64(__TEXT__,__cstring)` 中:
+ `Section64(__TEXT,__cstring)` 中:
-- `__TEXT__` 是段的段名(segment name),它通常包含代码(`__text`)和常量数据(如 `__cstring`、`__const` 等)。
-- `__cstring` 是节的节名(section name),它通常包含 C 字符串字面量。这些字符串字面量在编译时被存储在只读数据段中。
+- `__TEXT` 是段的段名(segment name),存储**只读且可执行**的内容,包括代码和只读数据。**典型节(Sections)**:
+ - `__text`:存放机器指令(代码段)。
+ - `__cstring`:存放字符串常量。
+ - `__const`:存放其他常量数据。
-因此,`Section64(__TEXT__,__cstring)` 描述的是二进制文件中存储 C 字符串字面量的一个节。这个节位于 `__TEXT__` 段中,并且包含的是只读数据。
+- **`__DATA` 段**:存储**可读写**的数据(如全局变量、静态变量)。
+- `__cstring` 节:
+ - **功能**:`_cstring` 专门存储硬编码的字符串常量(如 `"Hello, World!"`)。
+ - **内存权限**:映射到内存时,`__TEXT` 段整体为**只读**(`r--` 或 `r-x`),但 `_cstring` 本身**不可执行**,仅用于数据存储
+ - **所属区域**:
+ - 逻辑上属于**常量区**(类似 ELF 格式的 `.rodata`)。
+ - 物理上可能与代码段(`__text`)同属 `__TEXT` 段,但用途和权限不同。
+
+
+因此,`Section64(__TEXT,__cstring)` 描述的是二进制文件中存储 C 字符串字面量的一个节。这个节位于 `__TEXT__` 段中,并且包含的是只读数据。
@@ -202,7 +217,7 @@ print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x8000000100007800
print(Mems.memStr(ofVal: &str2)) // 0x0000353433323130 0xe600000000000000
```
-
+
可以看到:
@@ -224,6 +239,52 @@ print(Mems.memStr(ofVal: &str3)) // 0xd000000000000015 0x8000000100007800
+### Swift 字符串存储本质
+
+Swift 字符串存储的两种模式:
+
+- **内联存储(Small String Optimization,SSO)**:
+ - **条件**:字符串长度 ≤15 个 **ASCII 字符**(或 ≤7 个 **UTF-16 字符**)。
+ - **特点**:字符串内容直接存储在 `StringObject` 的 16 字节内存中,无需堆分配。有点类似 Objective-C 的 **Tagged Pointer**
+- **堆存储(Heap-Allocated)**:
+ - **条件**:字符串长度超过上述限制(字符串长度 > 15 个 **ASCII 字符** 或 > 7 个 **UTF-16 字符**)
+ - **特点**:字符串内容存储在堆内存。其指针结构是一个 16 字节的 `StringObject`,`StringObject` 存储堆地址和元数据
+
+
+
+#### 内联存储(SSO)的具体实现
+
+内存布局:前8个字节(元数据 + 部分字符) + 后8个字节(剩余字符 + 填充)
+
+元数据编码:
+
+最低有效位(LSB)用于标识存储模式:
+
+- **0**:内联存储
+- **1**:堆存储
+
+其余位存储字符串长度和编码信息(ASCII 或 UTF-16)
+
+Demo
+
+````Swift
+let str = "Hello" // 5 个 ASCII 字符
+内存布局如下:
+0x0000000000000a05 // 元数据(长度=5, ASCII, 内联标志位=0)
+0x48656c6c6f000000 // ASCII 字符 "Hello" 的十六进制表示 + 填充
+````
+
+与 Objective-C Tagged Pointer 的区别
+
+| **特性** | **Swift 内联存储 (SSO)** | **Objective-C Tagged Pointer** |
+| :----------- | :------------------------- | :------------------------------ |
+| **存储位置** | 字符串对象的 16 字节内存中 | 指针值本身(64 位) |
+| **标识方式** | 元数据的最低有效位 (LSB) | 指针的最高有效位 (MSB) |
+| **兼容性** | 需考虑 Unicode 编码复杂性 | 仅支持有限类型(如短 NSString) |
+| **内存安全** | 完全由编译器管理 | 需运行时特殊处理 |
+
+
+
@@ -257,12 +318,12 @@ print("explore")
可以看到:长度为16的字符串拼接后
-- 内存的前8个字节,从 `0xd000000000000010` 变到了 `0xf000000000000011`,最后2位代表字符串长度,从16位变成17位。
+- 内存的前8个字节,从 `0xd000000000000010` 变到了 `0xf000000000000011`,最后2位代表字符串长度,16进制的10就是16。从16位变成17位。
- 内存的后8个字节,字符串的地址改变了
上面可知, `字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
-
+
字符串真实地址:`0x0000600001700440 + 0x20 = 0x0000600001700460`,LLDB `x 0x0000600001700460 ` 可以看到字符串拼接后的结果。看上去是存储在堆上。如何验证?
@@ -270,11 +331,11 @@ print("explore")
我们知道堆上的内存初始化在 Swift 侧,关键方法为 `swift_allocObject ` -> `swift_slowAlloc` -> `malloc `。给 malloc 下断点,然后在断点出查看调用堆栈,可以看到在 `string.append()` 后有堆分配内存
-
+
结束当前函数调用,在外层可以看到 str2 地址值的后8个字节为 `0x000060000170410`,再 + `0x20` 就是字符串真实地址,打印如下。
-
+
0x20 是什么?这32个字节存放了什么信息?存储字符串的描述信息,比如:引用计数、字符串长度等信息。
@@ -292,17 +353,28 @@ print("explore")
`__TEXT` 是 Mach-O 文件中通常包含代码和只读数据的段。这个段包含了程序的主要执行代码,以及常量字符串、符号表等。
-`dyld_stub_binder` 是一个由动态链接器(dyld)在运行时生成和使用的函数。它的主要目的是在首次调用某个动态链接的符号(如函数或方法)时,将该符号的实际地址绑定到调用点。
+`dyld_stub_binder` 是一个由动态链接器(dyld)在运行时生成和使用的函数。它的主要目的是在首次调用某个动态链接的符号(如函数或方法)时,将该符号的实际地址绑定到调用点
+
+Swift 中 `String` 类型的初始化方法(`init`)的地址是否采用延迟绑定(Lazy Binding),取决于 **编译环境、优化级别和具体方法实现**
+
+### 延迟绑定的基本原理
在编译时,对于动态链接的符号,编译器会生成一个桩(stub),而不是直接调用该符号。桩是一个小段的代码,当被首次执行时,它会触发 `dyld_stub_binder` 的调用。`dyld_stub_binder` 的任务就是找到该符号的实际地址,并将其写入桩中,从而替换桩的原始代,这样,下一次调用该符号时,就可以直接跳转到实际的地址,而无需再次通过桩和 `dyld_stub_binder`。
+延迟绑定(Lazy Binding)是动态链接的机制,用于推迟符号(如函数、方法)地址的解析到首次调用时。其核心步骤为:
+
+1. **编译阶段**:生成存根(Stub),指向符号占位地址。
+2. **启动阶段**:存根指向动态链接器(如 `dyld`)的解析函数(如 `dyld_stub_binder`)。
+3. **首次调用**:触发符号解析,动态链接器填充真实地址到存根。
+4. **后续调用**:直接跳转到已解析的地址。
+
替换桩,位于 `__DATA,__la_symbol_ptr` 数据段可读可写,所以可以修改。
-
+
`__stub_helper` 节是 `__TEXT` 段中的一个特定节,它包含了用于处理符号懒加载的辅助函数和代码。当动态链接的符号首次被调用时,这些辅助函数会被触发,以解析符号并将其地址绑定到调用点。
diff --git a/Chapter1 - iOS/1.120.md b/Chapter1 - iOS/1.120.md
index 2d24896..6f810e9 100644
--- a/Chapter1 - iOS/1.120.md
+++ b/Chapter1 - iOS/1.120.md
@@ -7,7 +7,7 @@
- open:允许在定义实体的模块、其他模块中访问,允许其他模块进行继承、重写(open只能用在类、类成员上)
- public:允许在定义实体的模块、其他模块中访问,不允许其他模块进行继承、重写
- internal:只允许在定义实体的模块中访问,不允许在其他模块中访问
-- fileprivate:只允许在定义实体的源文件中访问
+- fileprivate:只允许在定义实体的源文件中访问
- private:只允许在定义实体的封闭声明中访问
绝大部分实现默认都是 internal
@@ -37,9 +37,9 @@
- 参数类型、返回值类型 >= 函数
- 父类 >= 子类
-
+ 因为定义的子类如果是 internal,而父类是 fileprivate,则可以访问到子类的地方,访问不到父类,这是不合理的。
- 父协议 >= 子协议
-
+ 协议也可以继承,父协议里的一些属性如果访问级别较低,遵循子协议,实现子协议的类,是无法访问到父协议中的属性的。
- 原类型 >= typealias
```swift
@@ -83,43 +83,50 @@
fileprivate var data1:(Dog, Person)
private var data2:(Dog, Person)
```
+- 泛型类型:泛型类型的访问级别由:类型的访问级别以及所有泛型类型参数的访问级别最低的那个决定
+ Demo1: 编译器报错:`Variable must be declared private or fileprivate because its type uses a fileprivate type`
+ 分析:
+ - 泛型的访问级别由最低的类别的访问级别决定。Car 为 internal,Dog 为 fileprivate,所以最低的是 fileprivate,所以 Person 类型的访问级别为 fileprivate。因此 Person 类型变量的访问级别为 fileprivate 或者比 fileprivate 更低的访问级别。
+ - Swift 默认所有的变量、方法、类的默认访问级别为 internal。internal 访问级别高于 fileprivate,所以报错。
-看似规则比较多,实际上就是对「一个实体不可以被更低访问级别的实体定义」的解释。
+ ```swift
+ internal class Car { }
+ fileprivate class Dog { }
+ public class Person { }
+
+ var person: Person = Person() // compile error: Variable must be declared private or fileprivate because its type uses a fileprivate type
+ ```
+
+ 改进下: `fileprivate var person: Person = Person()` 和 `private var person: Person = Person()` 都不会报错。
-类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别
+- 类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别:
+ - 一般情况下,类型为 fileprivate、private,那么成员、嵌套类型默认也是 private、fileprivate
+ - 一般情况下,类型为 internal、public,那么成员、嵌套类型默认也是 internal
-- 一般情况下,类型为 fileprivate、private,那么成员、嵌套类型默认也是 private、fileprivate
-- 一般情况下,类型为 internal、public,那么成员、嵌套类型默认也是 internal
+ 测试:
+ ```swift
+ // 编译不通过
+ class TestClass {
+ private class Person {}
+ fileprivate class Student : Person {} // Class cannot be declared fileprivate because its superclass is private
+ }
-
-测试:
-
-```swift
-// 编译不通过
-class TestClass {
+ // 编译通过
private class Person {}
- fileprivate class Student : Person {} // Class cannot be declared fileprivate because its superclass is private
-}
-
-// 编译通过
-private class Person {}
-fileprivate class Student : Person {}
-```
-
-当 fileprivate、private 都写在文件的全局作用域时,访问权限是一样的。
-
+ fileprivate class Student : Person {}
+ ```
+- 当 fileprivate、private 都写在文件的全局作用域时,访问权限是一样的。
+总结:看似规则比较多,实际上就是对「一个实体不可以被更低访问级别的实体定义」的解释。
## getter、setter
-
-getter、setter 默认自动接收他们所属换的访问级别
-
-可以给 setter 单独设置一个比 getter 权限更低的访问级别,用以限制写权限
+- getter、setter 默认自动接收他们所属换的访问级别
+- **可以给 setter 单独设置一个比 getter 权限更低的访问级别,用以限制写权限**
```swift
private(set) var num = 10
@@ -146,8 +153,7 @@ print(num)
## 初始化器
-
-- 如果一个 public 类,想在另一个模块调用编译生成的默认无参初始化器,必须显示提供 public 的无参初始化器(因为 public 类的默认初始化器是 internal 级别的)
+- 如果一个 public 类,想在另一个模块调用编译生成的默认无参初始化器,必须显示提供 public 的无参初始化器(因为 public class,编译器生成的 init 初始化器是 internal。另一个模块是无法访问的)
```swift
// APM.dylib
@@ -164,20 +170,13 @@ print(num)
## 枚举类型
-
-不能给 enum 的 case 单独设置访问级别,每个 case 自动对齐 enum 的访问级别
-
-- public 的 enum,各个 case 也是 public
+不能给 enum 的 case 单独设置访问级别,每个 case 自动对齐 enum 的访问级别(比如:public 的 enum,各个 case 也是 public)
## 协议
-
-协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别
-
-- public 定义的协议,各个属性、方法也是 public 级别
-
-协议实现的访问级别 >= 类型的访问级别(协议的访问级别)
+协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别(比如:public 定义的协议,各个属性、方法也是 public 级别)
+**协议实现的访问级别 >= 类型的访问级别(协议的访问级别)**
```swift
// 编译通过
diff --git a/Chapter1 - iOS/1.121.md b/Chapter1 - iOS/1.121.md
index c1e10cd..0337eae 100644
--- a/Chapter1 - iOS/1.121.md
+++ b/Chapter1 - iOS/1.121.md
@@ -1,6 +1,188 @@
# 内存管理
+## 弱引用
+Swift 和 OC 都是通过引用计数方式来管理内存的。
+
+Swift 的 ARC 存在3种情况:
+
+- 强引用(strong reference)。默认情况下,都是强引用。
+ 当一个强指针离开作用域后,会自动释放对象,调用 deinit 方法。
+ ```swift
+ class Person {
+ deinit {
+ print("Person deinit")
+ }
+ }
+
+ func test () {
+ let p: Person = Person()
+ }
+
+ print("1")
+ test()
+ print("2")
+ // console
+ 1
+ Person deinit
+ 2
+ ```
+
+- 弱引用(weak reference)。通过 weak 定义弱引用。**必须是可选类型,因为实例销毁后,ARC 会自动将弱引用设置为 nil**。
+ ```swift
+ weak var p: Person? = Person()
+ ```
+ - 弱引用如果被设置为 nil,是不会触发属性观察器的 willSet、didSet 方法的
+ ```swift
+ class Dog {
+ deinit {
+ print("Dog deinit")
+ }
+ }
+
+ class Person {
+ weak var dog: Dog? {
+ willSet {
+ print("willSet")
+ }
+ didSet {
+ print("didSet")
+ }
+ }
+ deinit {
+ print("Person deinit")
+ }
+ }
+
+ func test () {
+ let p: Person = Person()
+ p.dog = Dog()
+ print(p)
+ }
+
+
+ print("1")
+ test()
+ print("2")
+ // console
+ 1
+ willSet // 这里的触发是 test 方法里,给 person 对象设置了 dog 属性时触发的。但是 weak 指针设置为 nil 的时候没有触发属性观察器
+ didSet
+ Dog deinit
+ SwiftDemo.Person
+ Person deinit
+ 2
+ ```
+ 换一种写法。可以发现在 init 方法里面,属性观察器 willSet、didSet 是不会触发的。
+ ```swift
+ class Dog {
+ deinit {
+ print("Dog deinit")
+ }
+ }
+
+ class Person {
+
+ weak var dog: Dog? {
+ willSet {
+ print("willSet")
+ }
+ didSet {
+ print("didSet")
+ }
+ }
+
+ init (dog: Dog?) {
+ self.dog = dog
+ }
+ deinit {
+ print("Person deinit")
+ }
+ }
+
+ func test () {
+ let p: Person = Person(dog: Dog())
+ print(p)
+ }
+
+
+ print("1")
+ test()
+ print("2")
+ // console
+ 1
+ Dog deinit
+ SwiftDemo.Person
+ Person deinit
+ 2
+ ```
+- 无主引用(unowned reference)。通过 unowned 定义无主引用
+ - 不会产生强引用,非可选类型。实例销毁后仍然存储着实例的内存地址,类似 OC 的 `unsafe_retained`
+ - 如果在实例销毁后访问无主引用,会产生野指针错误
+
+
+weak、unowned 只能用在类实例上。比如:
+```swift
+protocol Liveavle: AnyObject { }
+class Person { }
+
+weak var p1: Person?
+weak var p2: AnyObject?
+weak var p3: Liveavle?
+
+unowned var p4: Person?
+unowned var p5: AnyObject?
+unowned var p6: Liveavle?
+```
+
+
+## 循环引用
+weak、unowned 都能解决循环引用问题。但是 weak 由于当对象释放后,会把指针设置为 nil。所以 unowned 会比 weak 的性能更好。
+- 在生命周期中对象可能会变为 nil,推荐使用 weak
+- 初始化赋值后再也不会变为 nil 的对象,推荐使用 unowned
+
+## 闭包的循环引用
+
+上面的代码会发生循环引用,会导致局部变量的 p 无法释放(看不到 Person 的 deinit 方法调用)
+
+解法:
+- **在闭包表达式的捕获列表声明 weak 或者 unowned 引用,解决循环引用的问题**
+因为在闭包里,声明的捕获列表中将 p 用 weak 修饰,所以可以为 nil。p 使用到的地方必须用 `p?.run()`
+```swift
+class Person {
+ var fn:(() -> ())?
+ func run () { print("run") }
+ deinit { print("deinit") }
+}
+
+func test () {
+ let p = Person()
+ p.fn = {
+ [weak p] in
+ p?.run()
+ }
+}
+
+test()
+// deinit
+```
+另一种写法
+```swift
+p.fn = {
+ [unowned p] in
+ p.run()
+}
+```
+-
+```swift
+class Person {
+ lazy var fn:() -> () = {
+ self.run()
+ }
+ func run () { print("run") }
+ deinit { print("deinit") }
+}
+```
## @escaping
@@ -169,7 +351,7 @@ print(age)
- `changeValue1` 的参数是不可变的指针,所以方法内部去修改值,编译器会报错
- `changeValue2` 的参数是可变的指针,所以方法内部去修改值,编译没问题
-- 指针加了范型,访问真实的值可以通过 `指针.pointee` 去访问
+- 指针加了泛型,访问真实的值可以通过 `指针.pointee` 去访问
diff --git a/Chapter1 - iOS/1.124.md b/Chapter1 - iOS/1.124.md
index 27c8763..0b6da33 100644
--- a/Chapter1 - iOS/1.124.md
+++ b/Chapter1 - iOS/1.124.md
@@ -180,14 +180,15 @@ print(Mems.memStr(ofRef: p))
// console
0x011d8001000104e9 0x000000000000001c 0x00000000000000af 0x0000000000000000
```
-
+
可以看到当 Swift 类继承自 NSObject 后,前8个字节存放的是 isa 指针,其次的16个字节存放存储属性信息,最后的8个字节用来内存对齐。
## 混编
-### Swift 类如何在 OC 中使用
+### OC 调用 Swift
OC 项目,使用 Swift 开发默认会创建 `项目名-Swift.swift` 文件。Swift 类都可以在该文件中找到
-默认情况下生成的 Swift class 是不可以直接在 OC 中使用的,如果需要访问需要在 class 前加 `@objc`,编译器生成的代码如下:
+
+默认情况下生成的 Swift class 是不可以直接在 OC 中使用的,如果需要**访问需要在 class 前加 `@objc` 且继承自 NSObject**,编译器生成的代码如下:
class 不仅需要创建对象,还需要访问属性和方法,可以在属性或者方法前加 `@objc`,效果如下
@@ -196,9 +197,41 @@ class 不仅需要创建对象,还需要访问属性和方法,可以在属
但这样很麻烦,需要给每个属性、方法添加 `@objc`。有简便方法,可以直接在 class 前加 `@objcMembers`,这样该 class 所有的属性、方法都可以在 OC 中访问
+**Swift 写的 extension,在`项目名-Swift.swift` 文件中可以看到,是被编译器编译为 OC 的分类 Category**。
+```swift
+@objcMembers class Car : NSObject {
+ var price: Double
+ var band: String
+ init(price: Double, band: String) {
+ self.price = price
+ self.band = band
+ }
+}
+
+extension Car {
+ func test() {
+ print("Car test")
+ }
+}
+```
+
+```objective-c
+@interface Car : NSObject
+@property (nonatomic) double price;
+@property (nonatomic, copy) NSString * _Nonnull band;
+- (nonnull instancetype)initWithPrice:(double)price band:(NSString * _Nonnull)band OBJC_DESIGNATED_INITIALIZER;
+@end
+
+@interface Car(SWIFT_EXTENSION(TestSwift))
+- (void)test
+@end
+
+```
+
+可以通过 `@objc(name)` 重命名 Swift 暴露给 OC 的符号名(类名、属性名、函数名等)
-### OC 类、对象方法如何在 Swift 中访问
+### Swift 中访问 OC 的对象、方法
要在 Swift 中访问 OC 类,需要创建桥接文件,OC 工程首次创建 Swift 文件时,Xcode 默认创建桥接文件 `项目名-Bridging-Header.h`。如果是手动创建的,则需要配置(在项目的 Build Settings 中,找到 Objective-C Bridging Header 设置项,并指定桥接头文件的路径。确保桥接头文件的路径正确无误,并且文件名和扩展名都正确)。
在桥接文件中(`项目名-Bridging-Header.h`) 写好需要在 Swift 中使用的 objective-C 类。
@@ -207,4 +240,351 @@ Swift 中不允许访问 objective-c 的方法或者需要换个方法名去调
`- (void)showPower NS_SWIFT_NAME(diaplayPower());` oc 对象方法名,在 Swift 中使用时,想换个名字,可以用 `NS_SWIFT_NAME(新的方法名())`
-`- (void)displayPower NS_SWIFT_UNAVAILABLE("请使用 showPower");` oc 对象方法名,不想在 Swift 使用时,可以加 `NS_SWIFT_UNAVAILABLE(原因)`
\ No newline at end of file
+`- (void)displayPower NS_SWIFT_UNAVAILABLE("请使用 showPower");` oc 对象方法名,不想在 Swift 使用时,可以加 `NS_SWIFT_UNAVAILABLE(原因)`
+
+### 符号名映射
+`@_silgen_name` 是 Swift 中用于底层符号控制的工具,适合需要直接操作函数符号的场景
+
+- 符号名称映射
+ 将 Swift 函数直接映射到指定的 C 函数名(或其他语言符号),绕过 Swift 默认的名称修饰(name mangling)
+
+ 声明 `@_silgen_name("my_c_function") func mySwiftFunction()` 后,意味着在 Swift 代码中调用 `mySwiftFunction` 会直接链接到 C 函数 `my_c_function` 中
+- 与系统 API 或 C 函数交互
+ 直接调用系统库函数或 C 函数,无需通过 Swift 的桥接机制(如 @_cdecl 或 Objective-C 兼容层)。
+ 适用于需要精确控制符号名称的场景(如调用 libc 函数、系统调用等
+- 导出 Swift 函数供外部使用
+ 强制 Swift 函数在编译后使用特定名称导出,方便其他语言(如 C、Python)通过动态链接调用。
+
+
+## QA
+### 为什么 Swift 暴露给 OC 的类,最终都要继承自 NSObject?
+什么时候会用到一个类?肯定是抽象一个问题为类吧,那么也一定会访问该类的属性或者方法吧。但在 OC 的世界中,一切皆对象,也遵循 NSObject 的内部布局,也会走 Runtime 的标准流程。
+
+1. OC 运行时依赖
+- 必须是 OC 对象:Objective-C 的 id 类型指向的对象,本质是 objc_object 结构体,其核心是通过 isa 指针关联到类(objc_class)
+- 必须支持运行时、消息系统的:Objective-C 的方法调用依赖运行时动态查找方法实现(通过 `objc_msgSend`),而这一机制需要类继承自 NSObject
+
+如果 Swift 类不继承 NSObject,则:
+- 它的实例在内存中缺少 isa 指针,无法被 Objective-C 运行时识别为有效对象。
+- Objective-C 代码无法通过 id 类型接收该对象,也无法调用其方法。
+
+2. NSObject 基类提供的基础能力
+NSObject 是 Objective-C 的根类,定义了对象的基本行为:
+- 内存管理:实现引用计数(retain/release)和 weak 指针、 dealloc 方法
+- 运行时元数据:提供 class、respondsToSelector: 等反射方法
+- 协议支持:实现 NSObjectProtocol(如 isEqual:、hash、description)。
+若 Swift 类不继承 NSObject,则无法直接使用这些基础功能,导致与 Objective-C 交互时出现兼容性问题。
+
+
+3. 互操作性的桥梁
+- 当 Swift 类继承 NSObject 时,编译器会生成一个 Objective-C 兼容的类结构(包括 isa 指针和元数据)
+- 若使用 @objc 修饰非 NSObject 子类,编译器会报错
+
+
+### `p.run()` 底层是怎么调用的?
+Demo1: Swift 调用 Swift 对象方法
+
+
+
+纯 Swift 环境中,调用对象的方法,走的是虚表的逻辑。最终底层会调用 `callq *0x78(%rax)`
+
+可以看到:Swift 调用 Swift 对象和方法,断点处显示直接调用方法地址,lldb 模式下输入 `si` 可以看到汇编代码停在了 **SwiftDemo`Cat.sayHi():** 的地方。所以走的是虚函数表逻辑。
+
+Demo2: OC 调用 Swift 对象方法
+1. Swift 类继承自 NSObject,在前面加 `@objcMembers` 暴露给 OC 环境
+2. Swift 环境调用 OC 对象和方法
+3. OC 方法中调用 Swift 对象和方法
+4. 给 OC 环境中,调用 Swift 对象方法的地方下个断点,查看走的是 OC 的 Runtime 还是 Swift 的虚函数表的逻辑
+断点截图如下:
+
+
+可以看到在 OC 环境中,调用 Swift 对象的方法,本质上走的是 Runtime 的流程,汇编可以看到走的是 `objc_msgSend` 流程,效果类似 `objc_msgSend(p, @selector(run))`
+
+结论:OC 类暴露给 Swift 环境后,调用 OC 对象的方法,本质走的是 Runtime 流程。
+
+### 被 @objcMembers 修饰的 Swift 对象,在 Swift 中调用
+
+Demo3: 暴露给 OC 的 Swift 对象,被 Swift 环境调用
+1. 继承自 NSObject 的 Swift 类
+2. 被 `@objcMembers` 修饰
+3. 在 Swift 环境中调用暴露给 OC 的 Swift 对象方法
+4. 断点查看方法调用的本质
+
+
+可以看到,在 Swift 环境中,即使某个 Swift 类暴露给了 OC,调用其对象方法的本质,依旧是走虚函数表。因为此时用不到 Runtime 的能力
+
+Demo4: 在 Demo3 的基础上,swift 方法内部调用 OC 对象方法
+
+也就是说:
+1. Cat 类继承自 NSObject,被 `@objcMembers` 修饰
+2. 在 Swift 中调用 Cat 对象的 sayHi 方法
+3. 在 sayHi 方法内部,调用 OC Person 对象的 run 方法
+
+下断点可以看到:
+
+
+
+分为2个阶段:
+1. 第一阶段:在 Swift 环境调用虽然暴露给 OC 的 Swift 对象方法,但因为没有和 OC 直接交互,所以走的是 Swift 虚函数表逻辑
+2. 第二阶段:在 Swift 环境调用 OC 对象方法,因为底层是 OC 方法调用,所以走的是 OC Runtime 逻辑
+
+思考:想让 Swift 方法也走 OC 的 Runtime,可以利用 **`dymanic`** 关键词修饰方法。如下:
+
+
+
+
+## dynamic 的作用
+在 Swift 中,dynamic 关键字用于强制方法或属性通过 Objective-C 运行时(Runtime)进行动态派发,即使该方法或属性在纯 Swift 代码中被调用。它的核心应用场景与 Objective-C 运行时的动态特性(如 KVO、方法交换、动态解析等)紧密相关
+
+核心作用:绕过 Swift 的静态优化
+Swift 默认会尝试优化方法派发(如使用虚函数表或直接派发),而 dynamic 会强制方法或属性始终通过 Objective-C 的 objc_msgSend 机制调用,确保动态性。
+启用 Objective-C 运行时特性
+
+若需要实现以下功能,必须使用 dynamic:
+- 键值观察(KVO):标记为 dynamic 的属性会自动支持 KVO。
+- 方法交换(Method Swizzling):运行时替换方法实现。
+- 动态方法解析:通过 resolveInstanceMethod: 动态添加方法实现。
+- 消息转发:通过 forwardingTargetForSelector: 或 forwardInvocation: 处理未实现的方法。
+
+
+### 支持 KVO
+Swift 中默认的存储属性不支持自动 KVO 通知,但通过 dynamic 标记属性后,属性访问会通过 Objective-C 运行时,从而触发 KVO 机制。
+```swift
+@objcMembers class Cat: NSObject {
+ dynamic var name: String // 支持 KVO
+ init(name: String) { self.name = name }
+}
+```
+在 OC 中监听 name 变化
+```Objective-c
+Cat *cat = [[Cat alloc] initWithName:@"PiPi"];
+[cat addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
+```
+
+### 方法交换
+若要在运行时替换方法实现(如 AOP 编程、调试 Hook),必须确保目标方法是动态派发的。
+
+```swift
+@objcMembers class Cat: NSObject {
+ dynamic func sayHi() { print("Original") }
+}
+```
+在 Objective-C 中交换方法实现
+```Objective-c
+Method originalMethod = class_getInstanceMethod([Cat class], @selector(sayHi));
+Method swizzledMethod = class_getInstanceMethod([Cat class], @selector(swizzled_sayHi));
+method_exchangeImplementations(originalMethod, swizzledMethod);
+```
+
+### 动态解析未实现的方法
+当调用一个未实现的方法时,可通过 resolveInstanceMethod: 动态添加实现。
+```swift
+@objcMembers class Cat: NSObject {
+ dynamic func sayHi() { print("Hello") } // 假设此方法未实现,运行时动态添加
+}
+```
+Objective-C 运行时动态解析
+```Objective-c
++ (BOOL)resolveInstanceMethod:(SEL)sel {
+ if (sel == @selector(sayHi)) {
+ class_addMethod([self class], sel, (IMP)dynamicSayHi, "v@:");
+ return YES;
+ }
+ return [super resolveInstanceMethod:sel];
+}
+
+void dynamicSayHi(id self, SEL _cmd) {
+ NSLog(@"Dynamic Hello");
+}
+```
+
+### 动态调用
+当 Swift 类的方法需要被 Objective-C 或其他动态语言(如通过 performSelector:)调用时,若方法未被标记为 dynamic,可能因编译器优化导致动态调用失败。
+```swift
+@objcMembers class Cat: NSObject {
+ dynamic func sayHi() { print("Hello") }
+}
+```
+Objective-C 中动态调用
+```Objective-c
+Cat *cat = [[Cat alloc] init];
+[cat performSelector:@selector(sayHi)]; // 需 dynamic 支持
+```
+
+
+## 数据类型转换
+在 Swift 和 Objective-C 的类型桥接机制中,String 与 NSString 可以互相转换,而 String 不能直接与 NSMutableString 互相转换,但 NSMutableString 可以转为 String。类似的情况也出现在 Array/NSArray/NSMutableArray 和 Dictionary/NSDictionary/NSMutableDictionary 之间
+
+### 核心原因
+可变性的语义差异:
+- Swift 的值类型(String、Array、Dictionary) 被设计为不可变的值语义。每次修改会产生新实例(Copy on Write)。比如 `var str = "A"; str += "B"`
+会创建新字符串 'AB',而非修改原内存
+- OC 的类型 NSString 是不可变的引用类型,NSMutableString 是可变的引用类型,允许直接修改内容
+这种差异导致桥接时需要严格处理可变性,确保类型安全和语义一致。
+
+
+### Swift string 与 OC NSString
+双向隐式桥接。因为两者都是不可变的,语义一致,没有副作用,都可以直接互相转换。
+
+```swift
+// Swift -> OC
+let swiftStr1: String = "Hello"
+let nsStr1: NSString = swiftStr1 as NSString
+// OC -> Swift
+let nsStr12: NSString = "World"
+let swiftStr2: String = nsStr12 as String
+```
+### Swift string 与 OC NSMutableString
+
+Swift string 与 OC NSMutableString 是单向桥接的。OC NSMutableString 可以转为 Swift String。但 Swift String 不能转为 OC NSMutableString
+
+原因:OC NSMutableString 是可变类型,转为 Swift String 时会创建一份不可变的副本,避免被 Swift String 修改造成意外修改。
+若允许 Swift String 直接转换为 OC NSMutableString,则可能通过 OC 代码修改 String 的值,破坏 Swift 值语义。
+```swift
+// Objective-C → Swift(允许)
+let mutableStr: NSMutableString = "Hello".mutableCopy() as! NSMutableString
+let swiftStr: String = mutableStr as String // 隐式桥接,生成不可变副本
+
+// Swift → Objective-C(禁止隐式桥接)
+let swiftStr = "Hello"
+let mutableStr = swiftStr as NSMutableString // ❌ 编译错误
+```
+
+### 类似情况
+- Swift Array 与 OC NSArray 可以互相转换。
+- OC 的 NSMutableArray 可以转换为 Swift Array。但是 Swift Array 不能转换为 OC NSMutableArray
+
+### 底层原理
+1. 类型桥接的实现方式:Swift 编译器通过 `_ObjectiveCBridgeable` 协议实现与 OC 类型的桥接。
+例如 String 实现了 `_ObjectiveCBridgeable`,使其能与 NSString 自动转换
+
+2. 可变类型桥接限制
+Swift String:
+- 值类型:Swift String 是结构体,遵循值语义。每次赋值或者修改都会生成新的独立副本,确保数据不可变性和线程安全
+- 不可变:即使用 var 声明,修改 String 也会通过创建新实例实现,而非直接修改内存
+Objective-C 的 NSMutableString
+- 引用类型(Reference Type):NSMutableString 是类(class),遵循引用语义。变量持有的是指向内存地址的指针。
+- 可变(Mutable):允许直接修改内存中的内容(如追加、删除字符),所有持有该引用的代码都会感知到变化。
+
+3. 为什么 String 不能直接桥接为 NSMutableString?
+
+- 原因 1:值语义与引用语义的冲突
+若允许将 Swift 的 String 直接桥接为 NSMutableString,则相当于将一个值类型强制转换为可变的引用类型。
+风险示例:
+```swift
+let swiftStr = "Hello"
+let mutableStr = unsafeBridgeToNSMutableString(swiftStr) // 假设存在这种桥接
+mutableStr.append("!") // 修改 mutableStr
+print(swiftStr) // 若桥接是共享内存,此处会输出 "Hello!",违背值语义!
+```
+
+这会导致 Swift 的 String 失去其不可变性保证,破坏类型安全。
+
+- 原因 2:内存管理的不兼容
+Swift 的 String 可能存储在栈内存或静态区(尤其是短字符串),而 NSMutableString 必须分配在堆内存。
+直接桥接可能导致内存访问错误(如悬垂指针)。
+
+- 原因 3:设计哲学的保护
+Swift 强调安全性,禁止隐式共享可变状态。若允许直接桥接,开发者可能无意中在多个地方修改同一块内存,导致难以调试的问题。
+
+QA: 如何显式实现 String → NSMutableString?
+若需要将 Swift String 转为 NSMutableString,必须显式创建新对象,而非直接桥接:
+```swift
+let swiftStr = "Hello"
+let mutableStr = NSMutableString(string: swiftStr) // 显式拷贝
+mutableStr.append("!") // 安全修改
+```
+
+在 Swift 和 Objective-C 的互操作中,`String` 和 `NSMutableString` 之间的转换规则是由两者的**类型语义**和**内存管理机制**共同决定的。以下是具体原因和底层逻辑:
+
+---
+
+### **1. 类型语义的根本差异**
+#### **Swift 的 `String`**
+- **值类型(Value Type)**:
+ Swift 的 `String` 是结构体(`struct`),遵循值语义。每次赋值或修改都会生成新的独立副本,确保数据不可变性和线程安全。
+- **不可变(Immutable)**:
+ 即使使用 `var` 声明,修改 `String` 也会通过创建新实例实现,而非直接修改内存。
+
+#### **Objective-C 的 `NSMutableString`**
+- **引用类型(Reference Type)**:
+ `NSMutableString` 是类(`class`),遵循引用语义。变量持有的是指向内存地址的指针。
+- **可变(Mutable)**:
+ 允许直接修改内存中的内容(如追加、删除字符),所有持有该引用的代码都会感知到变化。
+
+---
+
+### **2. 为什么 `String` 不能直接桥接为 `NSMutableString`?**
+#### **原因 1:值语义与引用语义的冲突**
+- 若允许将 Swift 的 `String` 直接桥接为 `NSMutableString`,则相当于将一个值类型强制转换为可变的引用类型。
+ **风险示例**:
+ ```swift
+ let swiftStr = "Hello"
+ let mutableStr = unsafeBridgeToNSMutableString(swiftStr) // 假设存在这种桥接
+ mutableStr.append("!") // 修改 mutableStr
+ print(swiftStr) // 若桥接是共享内存,此处会输出 "Hello!",违背值语义!
+ ```
+ 这会导致 Swift 的 `String` 失去其不可变性保证,破坏类型安全。
+
+#### **原因 2:内存管理的不兼容**
+- Swift 的 `String` 可能存储在栈内存或静态区(尤其是短字符串),而 `NSMutableString` 必须分配在堆内存。
+ 直接桥接可能导致内存访问错误(如悬垂指针)。
+
+#### **原因 3:设计哲学的保护**
+- Swift 强调安全性,禁止隐式共享可变状态。若允许直接桥接,开发者可能无意中在多个地方修改同一块内存,导致难以调试的问题。
+
+---
+
+### **3. 为什么 `NSMutableString` 可以转为 `String`?**
+当 `NSMutableString` 桥接到 Swift 时,**会生成一个不可变的副本**,切断与原对象的关联:
+```swift
+let mutableStr: NSMutableString = "Hello".mutableCopy() as! NSMutableString
+mutableStr.append("!") // 修改原对象
+
+let swiftStr = mutableStr as String // 桥接生成新副本
+print(swiftStr) // "Hello!"
+mutableStr.append("?") // 继续修改原对象
+print(swiftStr) // 仍然是 "Hello!",不受影响
+```
+- **行为安全**:生成的 `String` 是独立的不可变副本,与原 `NSMutableString` 解耦。
+- **符合语义**:Swift 的 `String` 仍然是值类型,后续修改不会影响副本。
+
+---
+
+### **4. 如何显式实现 `String` → `NSMutableString`?**
+若需要将 Swift `String` 转为 `NSMutableString`,必须**显式创建新对象**,而非直接桥接:
+```swift
+let swiftStr = "Hello"
+let mutableStr = NSMutableString(string: swiftStr) // 显式拷贝
+mutableStr.append("!") // 安全修改
+```
+- **显式拷贝**:通过 `NSMutableString` 的构造器生成独立可变对象,避免共享内存。
+
+---
+
+### **5. 类似场景:`Array` ↔ `NSMutableArray`**
+同样的规则适用于集合类型:
+- **Swift `Array`**:
+ 值类型,桥接到 `NSArray`(不可变),但无法直接桥接为 `NSMutableArray`。
+- **`NSMutableArray` → `Array`**:
+ 生成不可变副本,与原对象解耦。
+
+```swift
+let swiftArray = [1, 2, 3]
+let mutableArray = NSMutableArray(array: swiftArray) // 显式拷贝
+mutableArray.add(4) // 安全修改
+```
+
+Swift 通过严格的类型桥接规则,确保值类型的不可变性和引用类型的可控性。这种设计虽然牺牲了部分灵活性,但从根本上避免了数据竞争、意外修改等风险,符合其安全至上的哲学。
+
+
+
+| Swift 数据类型 | 单向转换 or 双向转换 | OC 数据类型 |
+| -------------- | -------------------- | ------------------- |
+| String | <--------> | NSString |
+| String | <-------- | NSMutableString |
+| Array | <--------> | NSArray |
+| Array | <-------- | NSMutableArray |
+| Dictionary | <--------> | NSDictionary |
+| Dictionary | <-------- | NSMutableDictionary |
+
diff --git a/Chapter1 - iOS/1.125.md b/Chapter1 - iOS/1.125.md
index 1aebdf6..b086ada 100644
--- a/Chapter1 - iOS/1.125.md
+++ b/Chapter1 - iOS/1.125.md
@@ -1,4 +1,4 @@
-# Swift 函数式编程
+# Swift 函数式编程
## 定义
@@ -46,7 +46,7 @@ func multiple( _ v: Int) -> (Int) -> Int { { $0 * v }}
func divide(_ v: Int) -> (Int) -> Int { { $0 / v } }
func mod(_ v: Int) -> (Int) -> Int { { $0 % v }}
-// 函数合成,范型
+// 函数合成,泛型
infix operator >>> : AdditionPrecedence
func >>>(_ f1: @escaping (A) -> B,
_ f2: @escaping (B) -> C)
@@ -87,43 +87,73 @@ print(rs)
Demo2:将一个三个参数的函数变成柯里化
```swift
-func add(_ v1: Int, _ v2: Int) -> Int { v1 + v2 }
-func multipleAdd(_ v1: Int, _ v2: Int, _ v3: Int) -> Int { v1 + v2 + v3 }
-func currying(_ fn: @escaping (A, B) -> C) -> ((A) -> ((B) -> C)) {
- return { a in
- return { b in
- return fn(a, b)
+func addThree(_ v1: Int, _ v2: Int, _ v3: Int) -> Int { v1 + v2 + v3 }
+func addThree(_ v1: Int) -> ((Int) -> (Int) -> Int) {
+ return { v2 in
+ return { v3 in
+ return v1 + v2 + v3
}
-
}
}
-func currying(_ fn: @escaping (A, B, C) -> D) -> ((A) -> ((B) -> ((C) -> D ))) {
- return { a in
- return { b in
- return { c in
- return fn(a, b, c)
+func currying(_ fn: @escaping (A, B, C) -> D) -> ((A) -> ((B) -> ((C) -> D))) {
+ return { v1 in
+ return { v2 in
+ return { v3 in
+ fn(v1, v2, v3)
}
}
}
}
-let rs1 = currying(add)(10)(20)
-let rs2 = currying(multipleAdd)(10)(20)(30)
-print(rs1, rs2) // 30 60
+print(addThree(1, 2, 3)) // 6
+print(addThree(1)(2)(3)) // 6
+print(currying(addThree)(1)(2)(3)) // 6
```
## 函子
- 像 Array、Optional 这样支持 map 运算的类型,称为函子(Functor)
+### 概念
+
+在函数式编程中,**函子(Functor)** 是一个核心概念,它表示一种可以**被映射**的容器或结构。简单来说,函子能够接受一个函数,将该函数应用到其内部的值上,并返回一个**保持原有结构**的新函子。
+
+函子存在3个特性:
+
+- **容器性**:函子是一个包装值的容器(`List` 等)
+- **可映射性**:通过 `map` 方法将函数应用到容器内的值,例如 `var array2 = [1, 2, 3].map { $0 + 1 }`
+- **结构不变性**:映射过程不会改变容器的结构,例如数组的 `map` 方法返回新数组而非其他类型
+
+函子提供了一种**安全操作上下文中的值**的方式,是函数式编程中组合和抽象的基础工具。它通过 `map` 方法解耦了“值”和“上下文”,使得代码更模块化、可复用
+
+
+
+### 函子定律
+
+合法的函子必须满足以下规则:
+
+1. **恒等律**:`fmap id = id`(映射恒等函数后,容器不变)。
+2. **组合律**:`fmap (f . g) = fmap f . fmap g`(函数组合的映射等价于分别映射)
+
+
+
+### 总结
+
+像 Array、Optional 这样支持 map 运算的类型,称为函子(Functor)
+
```swift
- // Array
- public func map(_ transform: (Element) -> T) -> Array
+// Array
+@inlinable public func map(_ transform: (Element) throws -> T) rethrows -> [T]
// Optional
-public func map(_ transform: (Wrapped) -> U) -> Optional
+@inlinable public func map(_ transform: (Wrapped) throws -> U) rethrows -> U?
+```
+
+跳出语言来看,符合函数式编程的语言都存在**函子** 这一概念,符合下面的形式,都可以叫函子
+
+```swift
+func map(_ fn: (Element) -> T) -> Type
```
diff --git a/Chapter1 - iOS/1.126.md b/Chapter1 - iOS/1.126.md
index a10dfbb..90a5e8a 100644
--- a/Chapter1 - iOS/1.126.md
+++ b/Chapter1 - iOS/1.126.md
@@ -1,4 +1,4 @@
-# Swift 面向协议编程
+ # Swift 面向协议编程
## 概念
@@ -125,8 +125,13 @@ extension MyCompitable {
get{ MY.self }
}
}
+```
+具体使用的地方
+```swift
+// 第一步:让 String 拥有 my 前缀属性
extension String: MyCompitable { }
+// 第二步:给 String.my、String().my 前缀拓展功能(因为协议的 extension 中有 my 计算属性,也有个 my 静态计算属性,也就是一个方法)
extension MY where Base == String {
func countOfNumber() -> Int {
var count: Int = 0
@@ -156,4 +161,65 @@ I am a mutating method
要拓展系统提供的类型,可以按照上述模版进行修改。
- 加前缀 `my` 的目的是防止重复,系统实现是黑盒,如果自己直接提供类似 `testString.countOfNumber` 怕后续系统也提供 `countOfNumber` 方法。所以加前缀 `testString.my.countOfNumber`
--
+
+QA:如果是 NSString、NSMutableString 可以满足需求吗?
+答案是不行的。但是可以按照上述方式进行修改调整。怎么修改?按照 `extension MY where Base == String` 和 `extension MY where Base == NSString` 的方式写2遍?
+
+当然不,找共性,**String、NSString、NSMutableString 都遵循 ExpressibleByStringLiteral 协议**。所以实现 `extension MY where Base: ExpressibleByStringLiteral` 即可。
+
+```swift
+// 第一步:让 String 拥有 my 前缀属性
+extension String: MyCompitable { }
+extension NSString: MyCompitable { }
+
+// 第二步:给 String.my、String().my 前缀拓展功能(因为协议的 extension 中有 my 计算属性,也有个 my 静态计算属性,也就是一个方法)
+extension MY where Base: ExpressibleByStringLiteral {
+ func countOfNumber() -> Int {
+ var count: Int = 0
+ for c in (base as! String) where ("0"..."9").contains(c) {
+ count += 1
+ }
+ return count
+ }
+
+ static func test() {
+ print("I am a static method")
+ }
+
+ mutating func modify() {
+ print("I am a mutating method")
+ }
+}
+
+
+var str:String = "123"
+print(str.my.countOfNumber())
+String.my.test()
+str.my.modify()
+print("")
+
+var ocStr: NSString = "456" as NSString
+print(ocStr.my.countOfNumber())
+String.my.test()
+ocStr.my.modify()
+print("")
+
+var ocMutableStr: NSString = "456" as NSMutableString
+print(ocMutableStr.my.countOfNumber())
+String.my.test()
+ocMutableStr.my.modify()
+```
+
+
+这种设计在 SnapKit 中也存在:
+```swift
+// 实例属性用法
+view.my.makeConstraints { ... }
+// 静态属性用法
+UIView.my.registerDefaultConfig()
+```
+在 RxSwift 中也存在:
+```swift
+let observable = Observable.timer(.second(2), period: .second(1), scheduler: MainScheduler.instance)
+observable.map{ "\($0)" }.bind(to: priceLabel.rx.text)
+```
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.127.md b/Chapter1 - iOS/1.127.md
index 90507bd..b0273a0 100644
--- a/Chapter1 - iOS/1.127.md
+++ b/Chapter1 - iOS/1.127.md
@@ -1,4 +1,4 @@
-# 响应式编程
+ # 响应式编程
diff --git a/Chapter1 - iOS/1.136.md b/Chapter1 - iOS/1.136.md
index 062dcd8..14f7d64 100644
--- a/Chapter1 - iOS/1.136.md
+++ b/Chapter1 - iOS/1.136.md
@@ -1,116 +1,166 @@
-
+# 工程化
## 多环境配置
+Project、Target、Scheme 主要管理什么?
+- Project:包含了项目所有的代码、资源文件,所有信息
+- Scheme:对于指定 Target 的环境配置
+- Target:对于指定代码和资源文件的具体构建方式
+
多环境配置的3种方式:
- 多 target 配置
- Scheme 多 target 进行环境配置
- xconfig 文件配置
-QA:Target、Scheme 的关系是什么?
-- Project:包含了项目所有的代码、资源文件,所有信息
-- Scheme:对于指定 Target 的环境配置
-- Target:对于指定代码和资源文件的具体构建方式
-
## 多环境配置的不同方式
### 多 Target 的方式
-针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。搭配不同的宏定义,来实现控制逻辑的效果。
+#### 方案
-注意:duplicate 之后,target 虽然多了一份,但是代码和资源不变,所以
+针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。 **多 Target(Targets)** 是管理不同应用变体(如免费版/付费版、测试版/生产版、多客户定制版)的高效方式。
-
+所以,**为了区分不同的环境,做一些逻辑的控制。所以需要搭配不同的宏定义,来实现控制逻辑的效果。**
+
+注意:duplicate 之后,target 虽然多了一份,但是代码和资源不变
-
+#### 关键步骤
+
+
+
+
+##### 管理配置文件
+
+1. **独立的 Info.plist**
+
+ - 复制原 `Info.plist` 并重命名(如 `Pro-Info.plist`)
+
+ 当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
+
+
+
+ - 在新 Target 的 `Build Settings` → `Packaging` → `Info.plist File` 指定新路径
+
+2. **环境配置分离**
+
+ - 创建 `Config-Pro.xcconfig` 文件定义专属配置:
+
+ ```shell
+ // Config-Pro.xcconfig
+ API_URL = https://api.pro.com
+ APP_NAME = Pro App
+ ```
+
+ - 在 Target 的 `Build Settings` → `Base Configuration` 指定配置文件
+
+
+
+##### 宏定义
+
- OC:Build Settings -> Preprocessor Macros 里面的 Debug/Release 模式下添加自定义宏。比如在 debug 模式下 `IsOCDebug = 1`
- Swift:Build Settings -> Other Swift Flags 里的 Debug/Release 模式下添加自宏定义。注意命名有格式要求:`-D + 宏名称`
+#### 思考
+该方式还是存在弊端:
-当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
-
-
-
-
-
-
-
-思考:该方式还是存在的问题:多个 info.plist、配置比较乱
+- 工程存在多份 info.plist(实际上 plist 文件很少改动,所以没有这种需求)
+- 配置比较零散、比较乱
### 多 Scheme 的方式
-针对一个 Target 可以添加多个 Scheme,步骤如下
+#### 方案
-
+针对多 Target 方案存在的问题,可以用**「多 Scheme + 多 Configuration 」**的方式解决。
-这样的创建好之后,该 Target 存在3个 Scheme 了。有了 Scheme 有什么作用呢?设置宏定义的时候可以针对不同的 Scheme 进行设置。
+#### 关键步骤
-
+##### 创建 Configuration
+
+针对一个 Target 可以添加多个 **Configuration**,步骤如下:
+
+先选中 Project,然后在右侧选择 Info 选项卡,在 Configurations Section ,点击 "+" ,即可创建新的 Configuration。
+
+
+
+
+
+创建好之后,该 Target 存在3份 Configuration 了。不同的 Configuration 有什么作用呢?设置宏定义的时候可以针对不同的 Configuration 进行设置。
+
+
针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
-
+
-点击 Edit Scheme,在 Run 里面选择对应的 Scheme。
+点击 Edit Scheme,在 Run 里面选择对应的 Configuration。
-但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Scheme。有没有什么办法解决切换问题呢。
+但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Configuration。有没有什么办法解决切换问题呢。
-创建实体 Scheme
+##### 创建实体 Scheme
创建 Scheme 步骤:Xcode -> New Scheme,再弹出的方框内,选择对应的 Target,然后输入需要创建的 Scheme 名称。此次我们创建了:Debug、Beta 2个新的 Scheme。
-
+
-创建好之后,可以看到实体 Scheme 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme,然后在 Run 里面 “Build Configuration” 里面选择对应的 Scheme 与之对应。
+创建好之后,可以看到实体 Configuration 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme,然后在 Run 里面 “Build Configuration” 里面选择对应的 Configuration 与之对应。
-
+
-创建之后就可以根据 Scheme 设置值,在 `Build Settings -> User-Defined` 下添加自定义的字段,同时可以根据 Scheme 设置不同的值。
+##### plist 暴露自定义字段
-设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用。
+1. 创建之后就可以根据 Configuration 设置值,在 `Build Settings -> User-Defined` 下添加自定义的字段,同时可以根据 Configuration 设置不同的值。
+2. 设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用。
完整如下图:
-
-
-
+
切换不同的 Scheme,可以运行不同的效果,当前 case 下,选择 Debug Scheme,输出不同结果 `HOST_URL: http://www.debug.baidu.com`
-思考:目前的方案已经优雅不少,但是还是存在,自定义宏的时候需要选择不同的 Scheme,过程繁琐。
+#### 思考
+
+目前的方案已经优雅不少,该方式还是存在弊端:自定义宏的时候需要选择不同的 Scheme,过程繁琐
### Xcconfig
+#### 方案
+
+使用过 CocoaPods 的都会留意到工程存在 `*.Pro.xcconfig` 文件。里面是一些工程相关的配置。所以我们也可以用该方式处理工程问题。
+
+
+
+#### 关键步骤
+
Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,还可以修改 Build Settings 里面的选项。
-创建步骤如下:
+第一:创建步骤如下:
-
+
文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig`
@@ -118,15 +168,15 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
-修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations,选择对应的 Scheme,然后选择右边对应的 Xcconfig 文件。如下图
+第二:修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations,选择对应的 Target,然后选择右边对应的 Xcconfig 文件。如下图
-
+
我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`,Xcode 切换到 debug scheme 下,然后 Command + B 编译。
-
+
验证结果:
@@ -139,4 +189,192 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
说明:在 Xcode Build Settings 手动配置的信息,和通过 Xcconfig 方式编写的信息,不会冲突。
-对于 xcconfig 文件,我们其实并不陌生、因为在使用 Cocoapods 的时候就已经在使用这个文件了,只是很多人不知道其中变量的含义。
\ No newline at end of file
+对于 xcconfig 文件,我们其实并不陌生、因为在使用 Cocoapods 的时候就已经在使用这个文件了,只是很多人不知道其中变量的含义。
+
+
+
+#### 注意
+
+在 `.xcconfig` 里添加的内容,根据使用场景的不同,细节存在差异:
+
+1. 仅编译时使用(无需 plist 声明):**不需要在 plist 中声明 `HOST_URL`,值会直接注入编译环境**
+
+2. 运行时通过 Info.plist 访问(需 plist 声明):
+
+ - 在 `.xcconfig` 文件里添加了:`HOST_URL=127.0.0.1`
+ - 在 plist 中需要加一栏:key 为 `HOST_URL`,value为 `${HOST_URL}`
+
+
+
+ - 代码中使用
+
+ ```swift
+ let host = Bundle.main.object(forInfoDictionaryKey: "ServerHost") as! String
+ let apiKey = Bundle.main.object(forInfoDictionaryKey: "ApiSecretKey") as! String
+ print("Host: \(host), API Key: \(apiKey)")
+ ```
+
+
+
+QA:思考一个问题:为什么在 `xcconfig` 文件中设置的值,最后会显示在 Xcode 的 Build Settings 的 GUI 面板上?
+
+这便是接下去的内容:「Xcode 配置的层级机制」
+
+
+
+## Xcode 配置的层级机制
+
+### 层级机制
+
+Xcode 的 Build Settings 是一个**多层叠加系统**,优先级从高到低如下:
+
+**Target 设置 > Project 设置 > .xcconfig 文件 > Xcode 默认值**
+
+Apple 在 [Build Settings Reference](https://help.apple.com/xcode/mac/current/#/itcaec37c2a6) 中明确说明:
+
+> **继承规则**:
+>
+> - Target 设置继承 Project 设置,Project 设置继承底层默认值。
+> - 若 Target 显式定义某配置项,则覆盖 Project 中的相同项16。
+
+**关键推论**:
+
+> Target 作为具体构建目标,其配置需独立于 Project 的通用设置。若二者冲突,**Target 优先级更高**。
+
+根据 [Xcode Build System Guide](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/XcodeBuildSystem/):
+
+> - `.xcconfig` 是**基础配置层**,通过 `baseConfigurationReference` 字段被 Project/Target 引用23。
+> - 若 Project 或 Target 显式设置了某值(即使为空),**将覆盖 .xcconfig 中的定义**24。
+
+
+
+典型案例:当 `project.pbxproj` 中定义 `OTHER_LDFLAGS = ""`(空字符串)时,它会覆盖 `.xcconfig` 中的非空值,导致链接标志失效
+
+说明: xcconfig 优先级低于前2者。
+
+结论:配置的优先级顺序为:**Target 设置 > Project 设置 > .xcconfig 文件 > Xcode 默认值**
+
+
+
+### 为什么 xcconfig 的结果会显示在 Build Settings 中
+
+1. **配置文件的显式声明**
+ `.xcconfig` 文件是 Build Settings 的合法数据源。通过以下方式关联:
+
+ ```
+ OTHER_LDFLAGS = -framework "AFNetworking"
+ ```
+
+ Xcode 会将其视为项目配置的一部分,并在 GUI 中显示。
+
+2. **Build Settings 的“继承”特性**
+ Xcode 的 Build Settings 界面本质是一个**实时计算的合并视图**,它会展示:
+
+ - 所有直接通过 GUI 设置的值
+ - 从 `.xcconfig` 导入的值
+ - 继承的默认值(如 `$(inherited)`)
+
+3. 如果使用 CocoaPods 安装的依赖,则会生成2个 CocoaPods 生成的 `.xcconfig` 文件。如果开发者自己再创建 `.xcconfig` 则需要处理2者的逻辑,因为 Xcode 一个工程只可以选择一个 `.xcconfig` 文件
+
+
+
+### Xcode Project Management Guide
+
+这份[文档](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/XcodeBuildSystem/000-Introduction/Introduction.html#//apple_ref/doc/uid/TP40006904-CH1-SW1)深入讲解了 Xcode 项目结构、Build Settings 的继承关系、环境变量(如 `$(SRCROOT)`)等,适合想系统理解设置机制的人
+
+聊起工程化,不得不查看 CocoaPods 的 [Podfile 配置指南](https://guides.cocoapods.org/syntax/podfile.html)
+
+
+
+### 实践验证
+
+#### Demo1
+
+第一步:新建 Xcode iOS 工程。
+
+第二步:新建的工程配置了一份基础的 `Base.xcconfig` 来配置基础的编译信息。`Dev.xcconfig` 包含 `Base.xcconfig` 信息,在此基础上增加了一些编译参数。
+
+`Base.xcconfig` 配置如下:
+
+```shell
+BASE_LD_CONFIG = -framework "WantUIKit"
+OTHER_LDFLAGS = $(BASE_LD_CONFIG)
+```
+
+`Dev.xcconfig` 配置如下:
+
+```shell
+#include "Base.xcconfig"
+TEMP_LDFLAGS = $(BASE_LDFLAGS) -framework "AFNetworking" -framework "SDWebImage" -framework "PrismClient"
+OTHER_LDFLAGS = $(TEMP_LDFLAGS)
+```
+
+
+
+第三步:
+
+- 当前 xcconfig 是为 Dev 模式下设置的。所以项目的 scheme 选择 `Debug` 模式。
+- 选中 `PROJECT`,然后在 `Configurations` 下给 `Debug` 配置 `Dev.xcconfig` 文件。
+
+
+
+结果:编译工程,可以看到报错了。符合预期
+
+
+
+原因:本 Demo 的目的就是通过 `xcconfig` 文件和继承关系来验证对 Xcode Build Settings 中的 `Other Linker Flags` GUI 面板来验证 xcconfig 及其层级关系会正确影响到最终的编译参数上。
+
+注意:为什么不用 `$(inherited)`?
+
+`$(inherited)` 的作用范围:
+
+- 仅继承来自 **Xcode 构建系统层级**的值(Target 设置 → Project 设置)
+- 不继承 **同一配置文件链** 中通过 `#include` 引入的值
+
+所以此时用**中间变量**的方法。
+
+
+
+#### Demo2
+
+验证 `$(inherited)` 的继承效果。
+
+第一步:创建工程,配置 Podfile 文件。Podfile 文件内容如下
+
+```shell
+source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' # 清华源
+
+platform :ios, '16.2'
+inhibit_all_warnings! # 屏蔽第三方库警告
+
+target 'LDExploreDemo' do
+ # Pods for InstallDyanmicAndStaticFramework
+ pod 'SDWebImage'
+ pod 'AFNetworking'
+end
+```
+
+第二步:`pod install` 后,可以看到自动生成的 xcconfig 文件内容如下。
+
+为了测试 xcconfig 配置信息的继承,故意把生成的原始信息注释掉。去掉了 `-framework "ImageIO"`
+
+
+
+第三步:创建 `Base.xcconfig` 文件。引入 Cocoapods 自动生成的 `Pods-LDExploreDemo.debug.xcconfig` 然后声明 `OTHER_LDFLAGS = $(inherited) -framework "ImageIO"`由2部分组成,一部分是 `$(inherited)` 一部分是新加的 `-framework "ImageIO"`
+
+第三步:创建 `Dev.xcconfig` 文件。引入第三步创建的 `Base.xcconfig` 文件。声明 `OTHER_LDFLAGS = $(inherited)` 为继承来的配置。
+
+第四步:项目的 `AppDelete.m` 中引入 `#import `,然后创建对象并验证证
+
+ ````objective-c
+ AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];
+ NSLog(@"%@", policy);
+ ````
+
+
+
+说明:
+
+- Cocoapods install 后,自动创建链接器所需参数。都在 `Pods-项目名.debug.xcconfig` 配置文件中
+- 我们可以自己创建的 `*.xcconfig` 是可以引入自动生成的配置文件的。并在此基础上可以修改。然后在 Xcode Project Configuration 里可以指定为新创建的 xcconfig 文件
+- 并且是可以生效的
diff --git a/Chapter1 - iOS/1.30.md b/Chapter1 - iOS/1.30.md
index 637f280..8f103a5 100644
--- a/Chapter1 - iOS/1.30.md
+++ b/Chapter1 - iOS/1.30.md
@@ -113,4 +113,23 @@
13. Xcode 运行项目,模拟器启动失败。报错 `Failed to start launchd_sim: could not bind to session, launchd_sim may have crashed or quit respond`
- 关闭Xcode,在终端中键入以下命令:sudo chmod 1777 /tmp
- 清理此路径中的dyld文件夹:/Library/Developer/CoreSimulator/Caches
- - 重新启动Xcode,完成!
\ No newline at end of file
+ - 重新启动Xcode,完成!
+
+ 14. Xcode 自动设置 __nonnull.
+ 升级到 Xcode 10 , 新建类的时候发现头文件中多了2个宏:
+
+ NS_ASSUME_NONNULL_BEGIN
+ NS_ASSUME_NONNULL_END
+
+ 作用
+ 这两个东西是Nonnull区域设置(Audited Regions) 。
+ 这两个宏之间的代码里的所有简单指针对象都被默认为 ___nonnull,我们只需要去指定 __nullable 的指针。
+
+ 2014 年的 Apple WWDC 发布了强语言 swift ,必须要指定一个对象是否为空。为了迎合swift,OC中增加了 __nullable 和 ___nonnull 用于指定对象是否为空。
+ 每个属性、方法都指定 ___nonnull 和 __nullable 是一件非常繁琐的事。为了减轻开发工作量,苹果提供了两个宏:NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 。这两个宏之间的代码里的所有简单指针对象都被默认为 ___nonnull,我们只需要去指定 __nullable 的指针。
+
+ 解决的问题
+
+ - 减少冗余代码:避免在每个属性、方法参数或返回值前手动添加 nonnull,提高代码简洁性。
+ - 提升类型安全性:编译器会对默认的 nonnull 指针进行静态检查,传递 nil 时会触发警告。
+ - 改善 Swift 互操作性:Swift 能识别这些注解,将 nonnull 指针转换为非可选类型(如 String),将 nullable 指针转换为可选类型(如 String?),使接口更清晰。
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.38.md b/Chapter1 - iOS/1.38.md
index 1076a5e..b187121 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:
-
+
@@ -170,24 +170,24 @@ RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 So
Source0:
-- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
-
-- `performSelector:onThread:`
-
-- 数组
-
-Demo:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
-
-
+- 处理开发者主动提交的任务或应用内部逻辑。
+ - `performSelector:onThread:`
+ - `dispatch_async`到主线程的任务(最终封装为Source0)。
+ - 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源。UIKit控件的事件处理(如按钮点击后的回调)。
+Demo1:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
+
+
+
+Demo2: `- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait` 该 API 的底层就是:向目标线程的 RunLoop 添加一个 Source0 事件,标记后唤醒线程执行对应的事件。
+
+
Source1:
- 基于 Port 的线程间通信,可以主动唤醒 RunLoop
-
-- 系统事件捕捉(比如屏幕触摸事件,Source1捕捉后,派发给 Source0 处理)
-
+- 用户触摸屏幕时,系统通过 Mach Port 将事件传递到应用主线程的 RunLoop。RunLoop 被 Source1 唤醒后,将事件分发给 Source0处理具体的 UI 响应逻辑(如 `hitTest:withEvent:` 和响应链)。
- 字典。`{machport : 1}`
Timers:
@@ -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 堆栈存在系统调用
-
+
@@ -1887,10 +1887,72 @@ main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
-
+
改进代码如下
+```objective-c
+__weak ViewController *weakself = self;
+self.thread = [[LifeThread alloc] initWithBlock:^{
+ NSLog(@"RunLoop Start");
+ [[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode];
+ // tips2:
+ while (!weakself.needStopThread) {
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
+ }
+ NSLog(@"RunLoop Stop");
+}];
+[self.thread start];
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ if(!self.thread) return;
+ [self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:NO];
+}
+#pragma mark - 线程相关
+- (void)threadTask {
+ NSLog(@"线程任务 %@", [NSThread currentThread]);
+}
+
+- (void)stopThread {
+ if(!self.thread) return;
+ // tips1:
+ [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
+}
+
+- (void)stop {
+ self.needStopThread = YES;
+ CFRunLoopStop(CFRunLoopGetCurrent());
+ self.thread = nil;
+}
+
+- (void)dealloc {
+ NSLog(@"%s", __func__);
+ [self stopThread];
+}
+
+@end
+```
+
+但上面的代码还是存在问题:
+
+- 如果 `stop` 方法内部的 **`waitUntilDone` 为 NO,则可能会出现 Crash**。因为该参数代表后续逻辑代表会不会等该 selector 执行完毕。因为为 NO,所以 ViewController 执行 dealloc 了,所以 `dealloc` 方法和 Thread 内部的 block 同时进行,不能确保在 block 内部执行的时候 dealloc 有没有执行完,访问 weakSelf.isStoped 可能会出现坏内存访问,则会 crash
+
+ 解决方案:把 waitUntilDone 改为 YES
+
+- 但发现还存在问题:页面返回后,线程还是在执行打印任务。
+
+ 断点发现,在 NSThread 的 block 里,while 条件中 weakself 已经为 nil 了。但 self.thread 还存在,且 block 里的逻辑,while 条件取反后条件成立,则继续调用 `[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];` RunLoop 持续运行
+
+ 解决方案:不能在 block 里添加 `__strong typeof(weakself) strongSelf = weakself;` 否则造成循环引用(`VC -> thread -> block -> self(VC)`),页面消失,thread 无法释放。
+
+ 正确的做法是在 while 条件中增加 weakself 是否为 nil 的判断。
+
+ 可能有些人会产生疑问:为什么 waitUntilDone 改为 YES,dealloc 也就没执行完毕,为什么 weak 指针指向的 weakself 就为 nil 了?
+
+ `weak` 指针的特性是:**当对象开始销毁时(即 `dealloc` 被调用时),所有指向它的 `weak` 指针会立即被置为 `nil`**。这一行为发生在 `dealloc` 方法执行之前,而不是之后。
+
+继续优化版本:
+
```objective-c
__weak ViewController *weakself = self;
self.thread = [[LifeThread alloc] initWithBlock:^{
@@ -1933,20 +1995,18 @@ self.thread = [[LifeThread alloc] initWithBlock:^{
效果如下:
-
+
注意:
-- 如果 `stop` 方法内部的 `waitUntilDone` 为 NO,则会出现 Crash。因为该参数代表后续代表会不会等该 selector 执行完毕。因为为 NO,所以 ViewController 执行 dealloc 了,所以 `dealloc` 方法和 Thread 内部的 block 同时进行,不能确保在 block 内部执行的时候 dealloc 有没有执行完,访问 weakSelf.isStoped 可能会 crash
-
- 线程的 RunLoop 结束了,线程也无法执行任务了,所以需要给线程对象设置为 nil。同时任务派发的地方也需要判断线程是否存在,否则会 crash
- NSRunLoop 常驻线程可以运行在 NSRunLoopCommonModes 下吗?
-
+
不可以。因为在跑的时候如果 modeName 等于 kCFRunLoopCommonModes 则直接 kCFRunLoopRunFinished,则 RunLoop 的 while 循环条件失败
-
+
```objective-c
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
diff --git a/Chapter1 - iOS/1.39.md b/Chapter1 - iOS/1.39.md
index 23b94d4..33ad89e 100644
--- a/Chapter1 - iOS/1.39.md
+++ b/Chapter1 - iOS/1.39.md
@@ -12,7 +12,7 @@
-## 多线程方案
+## 一、多线程方案
| 技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
| ----------- | -------------------------------------------------------------- | --- | ------- | ---------- |
@@ -28,7 +28,7 @@
-## 多线程死锁
+## 二、多线程死锁
什么是死锁?
@@ -44,12 +44,12 @@ Demo0
// 死锁
- (void)viewDidLoad {
[super viewDidLoad];
- dispatch_sync(dispatch_get_main_queue(), ^{
+ dispatch_sync(dispatch_get_main_queue(), ^{ // Crash. Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
NSLog(@"task");
});
}
-// 死锁
+// 不死锁
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
@@ -68,15 +68,18 @@ task
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
- dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
- dispatch_sync(serialQueue, ^{
- NSLog(@"task");
+ dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
- NSLog(@"3");
+ NSLog(@"task");
+ dispatch_sync(serialQueue, ^{ // Crash.Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
+ NSLog(@"3");
+ });
});
- });
- NSLog(@"2");
+ NSLog(@"2");
}
+// console
+1
+task
```
为什么一个死锁了,一个没有死锁?
@@ -168,11 +171,19 @@ NSLog(@"执行任务5");
// 1 5 2 3 4
```
+分析:为什么不会死锁?
+
+- 先打印1
+- 然后给并发队列派发了异步任务,所以不会阻塞,开启了子线程,在子线程中打印了5
+- 并发队列里存在任务2,然后先打印2
+- 然后用同步的方式给并发队列里添加了任务3,同时里面还存在任务4
+- 是不是产生了一种假设:任务4要执行必须等前面的任务3执行完毕,任务3的执行也必须等任务4执行完毕,造成互相等待死锁?
+- 但别忘记这是并发队列,并发队列里的不同 task 可以同时执行,并不会互相等待。上一行的分析适用于串行队列。
+
总结:
-- 队列决定了任务执行完是否需要等待。任务决定是否可以产生新线程
-
-- 死锁:使用 `sync` 函数给当前串行队列派发任务,则会卡住当前串行队列,产生死锁
+- **队列决定了任务执行完是否需要等待。任务决定是否可以产生新线程**
+- 使用 `sync` 函数给当前串行队列派发同步任务,则会卡住当前串行队列,产生死锁
Demo6
@@ -258,29 +269,17 @@ dispatch_sync(dispatch_get_main_queue(), nil);
-### 总结
+### 死锁总结
-只要是同步提交任务 `dispatch_sync()` 不管是提交到串行队列还是并发队列,都是在当前线程执行。
+在当前串行队列上,使用 `sync` 函数给当前串行队列派发同步任务,则会卡住当前串行队列,产生死锁
+
+( 当前队列是串行队列 A,且以同步的形势,派发了一个任务到同一个串行队列 A 上去)。
-## 一些经典 Demo
-
-
-
-会输出什么?
-
-打印结果,电脑速度快的话,会有很多次打印出5.慢的话,打印出大于5的几次。
-
-分析:因为在循环内部,是全局并发队列。多线程的情况下,执行异步任务,任务的先后顺序没办法保证。可能线程1,拿到a=0,然后内部加了1.线程2一开始拿到a=0,但是代码还没执行到a++,在线程1里面,a就已经变为2,因为是 __block 修饰的。所以线程2里面拿到的a变成了a,然后内部a++后,a就是3.其他线程执行情况类似。
-
-NSLog 属于 IO 流,比普通运算耗时。所以当能执行 NSLog 的时候,a 一定是大于等于5的。某条线程 a 大于等于5之后,就立马结束 while 循环,开始执行最后的 NSLog。
-
-所以电脑越快,打印5的次数更多。电脑慢的情况下,可能会存在几次输出大于5的情况。
-
-## performSelector...withObject 研究
+## 三、performSelector...withObject 底层原理剖析
### performSelector...withObject
@@ -312,23 +311,32 @@ NSLog 属于 IO 流,比普通运算耗时。所以当能执行 NSLog 的时候
-### performSelector...withObject...afterDelay
+### performSelector...withObject...afterDelay 剖析以及经典问题
Demo1
-
+
-QA:为什么先打印1、3再打印2?因为 `performSelector...withObject...afterDelay` 相当于给 RunLoop 添加了一个 Timer,Timer 运行需要 RunLoop 配合。RunLoop 在被唤醒的时候会处理定时器。
+QA:为什么先打印1、3再打印2?
+
+- 该方法会将 `showLog` 方法的调用封装成一个 NSTimer 定时器事件,并添加到当前线程的 RunLoop 中
+- 不会阻塞当前线程:调用后立即返回,继续执行后续代码,打印 2
+- NSTimer 的执行依赖 RunLoop 的运行:定时器事件需要 RunLoop 处于运行状态才能触发。由于主线程的 RunLoop 默认是开启的,因此无需手动启动
+- 即使延迟时间为 0,任务也不会立即执行,而是等待当前代码执行完毕,RunLoop 进入下一次循环时才触发
+
+可以理解为本轮 RunLoop 在唤醒状态下优先处理屏幕点击事件(包括打印1、3,同时内部给 RunLoop 提交了一个 NSTimer),提交的 NSTimer 等本轮结束后下次 RunLoop 唤醒才执行。
+
+所以先打印1、再打印3,最后打印2
Demo2:
-
+
-QA:为什么 showLog 里的2没有打印?
+QA:为什么 test 里的2没有打印?
查看源码分析,如何查看 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 源码。
@@ -356,51 +364,177 @@ QA:为什么 showLog 里的2没有打印?
}
```
-答:通过源码分析可以看到,`performSelector...withObject...afterDelay...` 本质是开启一个定时器,并添加到 RunLoop。但没有启动 RunLoop。打印1、2是由于他们不需要 RunLoop 的配合,而定时器需要 RunLoop 的配合。
+通过源码分析可以看到:
+
+- `performSelector...withObject...afterDelay...` 本质是开启一个定时器,并添加到 RunLoop但没有启动 RunLoop
+- 打印1、2是由于他们不需要 RunLoop 的配合
+- 点击事件里通过 GCD 开启一个子线程,子线程默认没有 RunLoop。所以定时器里的逻辑没办法执行
所以代码改下就可运行。
-```objectivec
-- (void)test{
- NSLog(@"2");
-}
-- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
- dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
- dispatch_async(queue, ^{
- NSLog(@"1");
- [self performSelector:@selector(test) withObject:nil afterDelay:3];
- [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- NSLog(@"3");
- });
-}
-// 1 2 3
-```
+
+
+
+
+注意:可能有一部分人会这么在子线程中添加 RunLoop,会存在3无法打印的问题。为什么?
+
+
+
+`[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];` 本质上让 RunLoop 在指定模式下运行,直到发生以下情况之一:
+
+- 有事件到达并被处理
+- 到达指定的超时时间(`beforeDate`参数)
+
+- 如果 beforeDate 设置为 `[NSDate distantFuture]`,RunLoop会无限期等待事件,不会主动超时
+
+所以改法也很简单,有3种:
+
+第一种:在子线程方法中,手动关闭子线程中的 RunLoop
+
+
+
+第二种:不要用 `[NSDate distantFuture]`,设置个0秒也可以,改为 `[NSDate dateWithTimeIntervalSinceNow:0]]`
+
+
+
+第三种:不要用 `[NSDate distantFuture]`,设置个0秒也可以,改为 `[NSDate dateWithTimeIntervalSinceNow:0]]`。另外不要加 Port,直接在子线程中先获取一次 RunLoop 就好,因为 ``performSelector...withObject...afterDelay...` ` 已经给当前的 RunLoop 添加了 NSTimer,只是没有开启。 分析 RunLoop 源码分析后会发现,在子线程中获取一次 RunLoop,会默认创建一个 RunLoop。
+
+
所以要研究 iOS 底层的同学,看看 **GUNStep 代码吧,这是宝藏**
-Demo3:
-
+
+### pthread 线程原理
+
+Demo1:
+
+
同理,GCD 虽然开启了子线程,但是 Block 结束后,线程也就结束了。所以线程任务中的1秒后的任务肯定也结束了。
+Demo2:
-
-Demo4:
-
-
+
可以看到 NSThread 里的 block 执行结束后,thread 结束了。后面的 performSelector 想在线程里执行任务,就会 crash。
-解决办法也是在线程的 block 里面加 RunLoop,让它保活
+分析:
-
+- 屏幕点击事件里创建了一个 NSThread,用 block 的形势添加了一个任务。
+- 调用 start,立马执行。执行完 block 里面的代码后,thread 内已经没有任务了,则 thread 立马销毁(注意:并不是在 131 行打断点发现 thread 内存还存在就没问题,因为此时 block 任务还没执行)。
+- 然后 performSelector 向已退出的线程提交任务发生 crash
+
+查看源码分析:
+
+GUN NSThread.m 文件
+
+````objective-c
+- (void) start
+{
+ // ...
+ pthread_attr_t attr;
+ // ...
+ pthread_attr_init(&attr);
+ /* Create this thread detached, because we never use the return state from
+ * threads.
+ */
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ /* Set the stack size when the thread is created. Unlike the old setrlimit
+ * code, this actually works.
+ */
+ if (_stackSize > 0)
+ {
+ pthread_attr_setstacksize(&attr, _stackSize);
+ }
+ if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
+ {
+ DESTROY(self);
+ [NSException raise: NSInternalInconsistencyException
+ format: @"Unable to detach thread (last error %@)",
+ [NSError _last]];
+ }
+}
+
+
+/**
+ * Trampoline function called to launch the thread
+ */
+static void *nsthreadLauncher(void *thread) {
+ NSThread *t = (NSThread*)thread;
+ setThreadForCurrentThread(t);
+
+ /*
+ * Let observers know a new thread is starting.
+ */
+ if (nc == nil) {
+ nc = RETAIN([NSNotificationCenter defaultCenter]);
+ }
+ [nc postNotificationName: NSThreadDidStartNotification
+ object: t
+ userInfo: nil];
+
+ [t _setName: [t name]];
+ [t main];
+ // 执行完毕后退出,销毁线程
+ [NSThread exit];
+ // Not reached
+ return NULL;
+}
+
+
+- (void) main {
+ if (_active == NO) {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on inactive thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+ // 执行线程中的方法
+ [_target performSelector: _selector withObject: _arg];
+}
+
++ (void) exit{
+ NSThread *t;
+
+ t = GSCurrentThread();
+ if (t->_active == YES) {
+ unregisterActiveThread(t);
+
+ if (t == defaultThread || defaultThread == nil) {
+ /* For the default thread, we exit the process.
+ */
+ exit(0);
+ } else{
+ pthread_exit(NULL);
+ }
+ }
+}
+````
+
+分析:
+
+- 线程入口函数的执行流程
+
+ - 线程启动后,执行 `main` 函数
+ - `performSelector:` 执行用户定义的任务
+ - 任务执行完毕后,标记线程状态为 `finished`
+ - 线程入口函数返回,触发底层 `pthread_exit` 线程终止,操作系统回收线程资源
+
+线程销毁的直接原因
+
+- **POSIX 线程(`pthread`)的特性**:当线程的入口函数返回时,线程会立即终止,内核自动回收其资源(栈、寄存器状态等)
+- `NSThread` 对象虽然可能未被立即释放,但底层线程已销毁,无法再执行任务
+
+所以解决办法也是在线程的 block 里面加 RunLoop,让它保活
+
+
-## 队列组
+## 四、GCD API - 队列组
- 实现异步并发执行任务1、任务2
@@ -419,6 +553,7 @@ dispatch_group_async(group, queue, ^{
NSLog(@"Task2: %@ - index:%zd", [NSThread currentThread], index);
}
});
+
dispatch_group_notify(group, queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
for (NSInteger index = 0; index< 5; index++) {
@@ -426,18 +561,76 @@ dispatch_group_notify(group, queue, ^{
}
});
});
+// 等价于
+dispatch_group_notify(group, dispatch_get_main_queue(), ^{
+ for (NSInteger index = 0; index< 5; index++) {
+ NSLog(@"Task3: %@ - index:%zd", [NSThread currentThread], index);
+ }
+});
+```
+
+
+
+
+
+## 五、多线程安全问题(资源访问)- 加锁
+
+### 经典 Demo
+
+
+
+会输出什么?
+
+打印结果,电脑速度快的话,会有很多次打印出5.慢的话,打印出大于5的几次。
+
+分析:因为在循环内部,是全局并发队列。多线程的情况下,执行异步任务,任务的先后顺序没办法保证。可能线程1,拿到a=0,然后内部加了1.线程2一开始拿到a=0,但是代码还没执行到a++,在线程1里面,a就已经变为2,因为是 __block 修饰的。所以线程2里面拿到的a变成了a,然后内部a++后,a就是3.其他线程执行情况类似。
+
+NSLog 属于 IO 流,比普通运算耗时。所以当能执行 NSLog 的时候,a 一定是大于等于5的。某条线程 a 大于等于5之后,就立马结束 while 循环,开始执行最后的 NSLog。
+
+所以电脑越快,打印5的次数更多。电脑慢的情况下,可能会存在几次输出大于5的情况。
+
+
+
+### 为什么需要锁?
+
+多线程存在资源共享问题。比如多个线程对同一块内存,同时读或者写,导致不一致,很容易引发数据错乱和数据安全问题。典型的生产者消费者问题。比如多个线程访问同一个对象、同一个变量、同一个文件。计算机中看上去一个很简单的操作,背后往往是多个指令的操作,所以很容易发生多线程资源访问的问题。
+
+比如,`self.ticketCount++` 看似是原子操作,实际在底层会分解为多个步骤,涉及读取、计算和写入操作
+
+拆解为:
+
+```objective-c
+// 1. 读取当前值到寄存器
+int current = [self ticketCount];
+
+// 2. 执行自增计算
+int newValue = current + 1;
+
+// 3. 将新值写回内存
+[self setTicketCount:newValue];
+```
+
+X86 汇编为
+
+```assembly
+; 读取 ticketCount 到 eax 寄存器
+mov eax, [self.ticketCount]
+
+; 自增 eax 寄存器
+inc eax
+
+; 将 eax 写回 ticketCount
+mov [self.ticketCount], eax
```
-## 多线程安全问题-锁
-
-多线程存在资源共享问题。比如1块内存可能会被多个线程共享,同时读或者写,导致不一致,很容易引发数据错乱和数据安全问题。典型的生产者消费者问题。比如多个线程访问同一个对象、同一个变量、同一个文件
-
解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是:加锁。
+### iOS 锁种类
+
常见的锁有:
- OSSpinLock
@@ -519,6 +712,8 @@ Demo:
@end
```
+注意:多线程加锁必须是同一把锁,也就是第一次创建锁的时候,应该保存起来,后续其他线程访问的时候,继续使用同一把锁,否则每次访问都创建锁,则多线程锁对资源的保护效果就达不到。
+
#### 存在问题
@@ -529,29 +724,33 @@ Demo:
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
-QA:优先级反转是什么?
+#### 优先级反转问题
线程本质上就是 CPU 高速切换,系统分配很少的时间段分别给不同的线程,导致用户看上去是同时在做多个线程内的事情。操作系统会使用基于优先级抢占式调度算法。高优先级的线程始终在低优先级线程前执行。
+高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。从现象上来看,好像是中优先级的任务比高优先级任务具有更高的优先权。
-举个例子:优先级:线程 A < 线程 B < 线程 C,线程A、C 都会使用共享资源 R,该资源由信号量控制进行互斥访问。
-
-线程 A 在 T1 时刻使用资源 R 并拿到锁。开始执行线程内的逻辑。
-线程 C 在 T2 时刻被唤醒,但是此时锁被线程 A 使用,线程 C 尝试访问资源 R,但由于自然 R 被 线程 A 所占有,所以线程 C 放弃 CPU 进入阻塞(等待)状态,而线程 A 继续占据 CPU,执行任务
+操作系统通常采用**抢占式调度**策略,规则如下:
-目前来看一切正常。
+- **高优先级任务优先**:只要高优先级任务处于就绪状态(未阻塞),它总能抢占低优先级任务的 CPU 时间。
+- **锁的阻塞行为**:操作系统调度器的核心逻辑是:**仅从就绪队列(Ready Queue)中选择任务执行**。所以当任务因等待锁而阻塞时,它的优先级对调度不再产生影响,直到锁被释放
-但是在 T3 时刻,线程 B 被唤醒,由于优先级比较高,所以会立即抢占 CPU,此时线程 A 被迫进入 READY 状态等待(导致线程 A 无法及时释放资源 R)。
+举个例子:假设存在三个任务,优先级为 **H > M > L**,且 L 持有某个锁:
-T4 时刻,线程 B 放弃 CPU,此时线程 A (优先级10)是唯一处于 READY 状态的线程,所以再次占据 CPU 去执行任务,在 T5 时刻释放锁和资源 R。
-
-在 T5 时刻,线程 A 解锁瞬间,线程 C 立即获取锁,并在优先级20上等待 CPU,因为优先级比较高,所以系统会立刻调度线程 C 的任务执行。此时线程 A 进入 READY 状态。
-
-线程 B 从 T3 到 T4 这个时间段占据 CPU 资源的行为叫做优先级反转。一个优先级 15 的线程B,通过压制优线级10的线程 A,而事实上导致高优先级线程 C 无法正确得到 CPU。这段时间是不可控的,因为线程 B 可以长时间占据 CPU(即使轮转时间片到时,线程 A 和 B 都处于可执行态,但是因为B的优先级高,它依然可以占据 CPU),其结果就是高优先级线程 C 可能长时间无法得到 CPU。
+1. 初始状态:
+ - L 持有锁,并在 CPU 上运行,因为此时没有更高优先级的任务需要执行
+ - 过了一会儿,H 请求锁,但锁已被 L 持有,因此 H 被阻塞,忙等
+ - 再过了一会儿,M 处于就绪状态,但不需要锁
+2. M 抢占 CPU:
+ - 出于时间片轮转算法,当 L 的时间片用完或被其他原因中断时,调度器会选择下一个最高优先级的就绪任务执行
+ - 此时 H 因等待锁被阻塞(即使优先级最高,但出于等待锁的状态下,H 的状态变为 `Blocked`,会被移出就绪队列。调度器不再将 H 视为候选任务),M 的优先级高于 L,因此 M 抢占 CPU 并开始执行
+3. L 无法释放锁:
+ - M 的执行导致 L 无法继续运行,因此 L 无法完成工作并释放锁
+ - H 继续被阻塞。所以产生高优先级的 H 一直在等待,中等优先级的 M 被执行的优先级反转现象
当高优先级任务正等待信号量(此信号量被一个低优先级任务拥有着)的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行类似死锁]的情形发生
@@ -585,7 +784,7 @@ T4 时刻,线程 B 放弃 CPU,此时线程 A (优先级10)是唯一处
}
```
-#### 汇编窥探原理
+#### 汇编剖析实现原理
自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。
@@ -603,21 +802,21 @@ si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看
第一步:当第二次调用 saveMoney 方法,开启汇编调试
-
+
看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock` 方法内部调试
第二步:继续输入 si,敲回车
-
+
第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。
-
+
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
-
+
发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,“太浪费性能了”(如果使用锁资源的线程任务很简单,那自旋也是高效的,可以快速获取锁。)
@@ -625,6 +824,26 @@ si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看
+#### 思考
+
+OSSpinLock 效率这么低,那使用场景是什么?
+
+- 短临界区与多核优化
+
+ 自旋锁的核心优势在于 **避免线程上下文切换的开销**。在以下场景中,OSSpinLock 的性能可能优于传统互斥锁(如 `pthread_mutex`):
+
+ - 锁持有时间极短(如几纳秒到微秒级):忙等的 CPU 消耗低于线程休眠与唤醒的开销
+ - 多核 CPU 环境:当线程在另一个核心上即将释放锁时,忙等线程可以立即获取锁,无需等待调度器介入
+
+- 实现简单且无系统调用
+
+ - 用户态实现:OSSpinLock 完全在用户空间运行,无需陷入内核态,减少了系统调用(syscall)的开销。
+ - 低延迟:对于高频、轻量级的锁操作(如计数器自增),自旋锁的响应速度更快
+
+虽然它有合适的使用场景,但 Apple 已经标记为废弃了,所以最好别用,否则某个版本出现什么不符合预期的行为,就有苦说不出了。
+
+
+
### os_unfair_lock
#### 使用
@@ -696,31 +915,33 @@ int cursorr = 1;
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
-
+
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
-
+
-#### 汇编窥探原理
+#### 汇编剖析实现原理
同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下
-
-
-
-
-
+
+
+
+
+
-
+
-结论:可以看到 `os_unfair_lock` 在锁等待的时候,底层调用的是 `sysCall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等,性能也好。
+结论:可以看到 `os_unfair_lock` 在锁等待的时候,底层调用的是 `sysCall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等的实现,所以性能好。
系统对其描述是:`Low-level lock that allows waiters to block efficiently on contention.`,即低级锁,低级锁的特点是等不到锁就休眠。
+在并发编程中,设计一种**低级别锁**,能够使等待线程在竞争时**高效阻塞**(而非忙等),通常需要从用户态切换到内核态这样的协作机制
+
### pthread_mutex
@@ -764,9 +985,9 @@ pthread_mutex_destroy(&_moneyLock);
使用如下
-
+
-#### 递归锁
+#### 化身递归锁
如果在某个方法内部递归调用自身怎么实现,好像挺简单的,直接内部调用即可。
@@ -789,28 +1010,30 @@ pthread_mutex_destroy(&_moneyLock);
-[PThreadMutexRecursiveLockTester otherTest]
```
-只打印了 1。为什么?因为第一次调用正常加锁,然后递归调用自身,第二次调用的时候尝试加锁,但是这时候第一次调用时候锁还没释放。
+只打印了 1。为什么?因为第一次调用正常加锁,然后递归调用自身,第二次调用的时候尝试加锁,但是这时候第一次调用时候所占用的锁还没释放,会发生死锁。
-先加锁,然后递归调用,再继续加锁,再调用再加锁,最后一次函数执行完则解锁,出栈后继续解锁,再解锁。效果等价于
+我们的实际编程中,存在递归函数的情况。上面学完的锁,都不能满足该情况。执行函数 test,然后加锁,然后继续调用 test,要加锁,发现锁被占用了,则会死锁。所以引进了递归锁。
+
+递归锁的工作流程:先加锁,然后递归调用,再继续加锁,再调用再加锁,最后一次函数执行完则解锁,出栈后继续解锁,再解锁。类似于 NodeJS 的洋葱模型,效果等价于
```shell
+ 代表加锁;- 代表解锁
-线程1: otherTest(+ -)
- otherTest(+ -)
- otherTest(+ -)
+线程1: otherTest in: +
+ otherTest in: +
+ otherTest in: +
+----------------------------------
+ otherTest out:-
+ otherTest out:-
+ otherTest out:-
```
+巧妙的是:互斥锁 pthread_mutex_lock 提供实现该功能的 API。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);` 就可以实现递归锁的效果了。
-
-互斥锁提供 API 实现该功能。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);`
-
-在**同一个线程中可以多次获取同一把锁。并且不会死锁**。
-
-
+即:在**同一个线程中可以多次获取同一把锁。并且不会死锁**。
改进后的效果如下
-
+
@@ -820,35 +1043,35 @@ QA:互斥递归锁,可以在不同线程中加锁吗?
-#### 汇编窥探原理
+#### 汇编剖析实现原理
-
+
输入 si 继续跟进,可以看到还是在执行我们自己的代码,LockExplore image 的 `pthread_mutex_lock` 方法
-
+
继续输入 si 跟进
-
+
可以看到此时调用到系统 `libsystem_pthread.dylib` 库的 `pthread_mutex_lock` 方法了。
第41行看到关键函数,继续输入 si 进去看看
-
+
可以看到内部第62行关键函数调用了 `_pthread_mutex_firstfit_lock_wait` 方法。此时继续输入 si 跟踪看看
-
+
可以看到内部第25行调用了关键函数 `__psynch_mutexwait`,继续输入 si 看看
-
+
可以看到内部继续调用了系统 `libsystem_pthread.dylib` 库的 `__psynch_mutexwait` 方法。继续输入 si
-
+
可以看到内部第4行发生了系统调用 `sysCall`,执行完第四句指令,线程立马就结束了。
@@ -856,7 +1079,9 @@ QA:互斥递归锁,可以在不同线程中加锁吗?
-### 互斥条件锁 pthread_cond_t
+#### 互斥锁的条件变量 pthread_cond_t
+
+多线程环境下,很多时候没办法确保先有数据再消费,比如生产者-消费者问题,这时候就有互斥锁的另一个 API 了,即条件变量`pthread_cond_t`
#### 使用
@@ -868,7 +1093,7 @@ QA:互斥递归锁,可以在不同线程中加锁吗?
激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)`
-
+
可以看到同时调用 remove、add 方法
@@ -970,13 +1195,13 @@ NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多
Demo
-
+
NSLock 死锁
-
+
会发生死锁,后续代码无法执行,App 表现就是 ANR。重复对 NSLock 进行加锁可能导致死锁问题,同时也可能引发数据竞争和性能下降等并发相关隐患
@@ -986,7 +1211,7 @@ NSLock 死锁
### NSCondition
-NSCondition 是对 mutex 和 cond 的封装。
+NSCondition 是对 `pthread_mutex_t` 和 `pthread_cond_t 的封装。
API
@@ -1026,23 +1251,31 @@ API
Demo:
-
+
+观察本次打印顺序,可以看到:
+
+- 程序先执行 `_remove` 方法,先加锁,然后遇到 if 条件满足了,则执行 `wait` 。wait 干的事情是先解锁,然后等待另一个地方发送 `signal`
+- 然后 `_add` 方法得到了锁,加锁。开始执行 addObject 方法。然后立马调用 `signal` 方法
+- 可能看上去很快,感觉同一时刻在 `_remove` 方法中又得到了锁资源,然后删除了元素,最后释放了锁资源
+
+疑问:调用 signal 方法后,另一个等待锁的地方会立马得到锁资源吗?可以做个实验,给 signal 后 sleep 2秒,再调用 unlock
+
+
+
+观察打印信息可以看到:
+
+- 程序先执行 `_remove` 方法,先加锁,然后遇到 if 条件满足了,则执行 `wait` 。wait 干的事情是先解锁,然后等待另一个地方发送 `signal`
+- 然后 `_add` 方法得到了锁,加锁。开始执行 addObject 方法。然后立马调用 `signal` 方法
+- 但此时 `_remove` 方法内的逻辑还没执行,在2s后才执行。说明2s后等 `_add` 方法调用 unlock 方法后,`_remove` wait 才得到锁资源
+
+结论:如果逻辑很简单,**NSCondition unlock 和 signal 的顺序没有要求。但要意识到只发送 signal,没有 unlock 的话,wait 是不能立马得到锁的,需要等 unlock 后才可以执行后续逻辑。具体顺序看业务场景**
-使用 NSCondition 的时候 unlock 和 signal 的顺序可能会对结果造成影响。举个例子
-
-
-可以看到在这种情况下,由于 NSCondition 另一个地方 wait,wait 也需要释放锁,但是另一个发 signal 的地方,还没释放锁。所以会等待2s。
-
-针对这个情况,可以将 unlock 和 signal 的顺序进行调整。
-
-
-
-先解锁,然后发送 singal,后续其他的业务逻辑也不影响。当然这个需要针对实际代码进行设计。
+
@@ -1083,7 +1316,7 @@ API 如下:
Demo
-
+
分析:虽然通过3个线程,设置了线程的先后顺序,但是多线程任务执行的时候到底谁先执行,是没办法控制的。但是通过 `NSConditionLock lockWhenCondition:*` 的能力,可以控制线程的执行顺序。
@@ -1093,13 +1326,13 @@ Demo
-### dispatch_queue
+### dispatch_queue(DISPATCH_QUEUE_SERIAL)
使用 GCD 的串行队列,也是可以实现线程同步。
线程同步的本质就是多线程的任务是顺序执行
-
+
@@ -1113,23 +1346,25 @@ semaphore 叫做”信号量”
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
-
+
可以看到打印了20个线程,但是我们控制线程最大数量怎么办呢?可以用信号量实现。效果如下:
-
+
-`dispatch_semaphore_wait` 函数的本质
+#### dispatch_semaphore_wait 原理
+
+执行 `dispatch_semaphore_wait` 方法时,
- 如果信号量的值 > 0,则会让信号量的值 -1,然后继续向下执行代码
-- 如果信号量的值 <= 0,则线程休眠等待。等待多久取决于第二个参数。等到信号量的值 > 0(直到其他的线程,任务执行完毕,利用 `dispatch_semaphore_signal`API 让信号量的值+1),此时继续会让信号量的值 -1,然后继续向下执行代码
+- 如果信号量的值 <= 0,则线程休眠等待(等待多久取决于第二个参数),直到信号量的值 > 0(直到其他的线程,任务执行完毕,利用 `dispatch_semaphore_signal`API 让信号量的值+1),此时继续会让信号量的值 -1,然后继续向下执行代码
-`dispatch_semaphore_signal` 函数的本质:让信号量的值 + 1
+`dispatch_semaphore_signal` 函数的作用:让信号量的值 + 1
所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行。代码如下
-
+
@@ -1154,7 +1389,7 @@ void _dispatch_semaphore_dispose(dispatch_object_t dou,
}
```
-#### 封装
+#### 使用封装
有的时候我们需要在方法内部创建 semaphore ,则可以创建宏
@@ -1189,15 +1424,17 @@ dispatch_semaphore_signal(semaphore);
`@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装。Demo 如下
-
+
+#### 源码剖析
+
为了探究下实现,开启汇编调试
-
+
-
+
通过汇编可以看到 `@synchronized` 底层调用了 `objc_sync_enter` 方法,其中又调用了 `id2data` 和 `os_unfair_recursive_lock_lock_with_options` 方法。 可以查看 objc4 的源码(笔者的 objc 版本为 objc4-objc4-912.3),查找 `objc_sync_enter`
@@ -1257,7 +1494,7 @@ using recursive_mutex_t = objc_recursive_lock_t;
可以发现,如果 `@synchronized` 参数为`nil`,`@synchronized(nil) `调用 `objc_sync_nil()`,最终什么也不执行。
-```
+```objective-c
static SyncData* id2data(id object, SyncKind kind, enum usage why)
{
ASSERT(kind != SyncKind::invalid);
@@ -1441,7 +1678,11 @@ static SyncData* id2data(id object, SyncKind kind, enum usage why)
### 各种锁性能对比
-性能从高到低:`os_unfair_lock > OSSpinLock > dispatch_semaphore > pthread_mutex > dispatch_queue(DISPATCH_QUEUE_SERIAL) >NSLock > NSCondition > pthread_mutex(recursive) > NSRecursiveLock > @synchronized`
+性能从高到低:
+
+````shell
+os_unfair_lock > OSSpinLock > dispatch_semaphore > pthread_mutex > dispatch_queue(DISPATCH_QUEUE_SERIAL) > NSLock > NSCondition > pthread_mutex(recursive) > NSRecursiveLock > @synchronized
+````
@@ -1461,7 +1702,7 @@ static SyncData* id2data(id object, SyncKind kind, enum usage why)
- 预计线程等待锁的时间较长
-- 单核处理器
+- 单核处理器(一旦使用自旋锁,CPU 就很忙了,很少有资源去处理其他逻辑,会卡顿)
- 临界区有IO操作(IO 操作一般占用 CPU 资源较多,互斥锁本身就占用 CPU,所以不适合)
@@ -1473,7 +1714,7 @@ static SyncData* id2data(id object, SyncKind kind, enum usage why)
### atomic
-### 源码探究
+#### 源码探究
`atomic` 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。
@@ -1538,6 +1779,14 @@ id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
属性赋值逻辑:
```c
+
+void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
+{
+ bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
+ bool mutableCopy = (shouldCopy == MUTABLE_COPY);
+ reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
+}
+
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
@@ -1562,7 +1811,7 @@ static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t o
oldValue = *slot;
*slot = newValue;
} else {
- // atomic,加锁
+ // atomic,加自旋锁
spinlock_t& slotlock = PropertyLocks.get()[slot];
slotlock.lock();
oldValue = *slot;
@@ -1573,13 +1822,6 @@ static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t o
objc_release(oldValue);
}
-void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
-{
- bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
- bool mutableCopy = (shouldCopy == MUTABLE_COPY);
- reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
-}
-
void lock() {
lockdebug_mutex_lock(this);
//
@@ -1601,9 +1843,7 @@ QA:为什么在 iOS 上几乎没有使用?
因为属性 getter、setter 使用太高频了,另外 atomic 内部实现是自旋锁,自旋锁是忙等,针对移动设备上那寸土寸金的 CPU,太奢侈了,太耗费性能了。
-
-
-### atomic 并不能保证使用属性的过程是线程安全的
+#### atomic 并不能保证使用属性的过程是线程安全的
```objectivec
@property (atomic,copy) NSString *name;
@@ -1628,21 +1868,51 @@ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
预期:线程 A 打印出来一定是杭城小刘,线程 B 打印出来是魅影。但事实上可能存在乱序。
-atomic 是原子属性,它内部实现是针对属性的 setter、getter 进行加锁(早期实现是自旋旋,因为存在问题,后续替换为了 os_unfair_lock)。但是事实上在进行多线程编程的时候,我们针对数据的操作并不是修改指针本身(思考 NSString 的 getter、setter),而是操作类似 NSArray、NSDictionary 这样的 case。比如 `@property(atomic, strong)NSMutableArray *hobbies;` 如果在多线程情况下进行处理,一边生产者添加数据,一边消费者消费数据,则会产生内存问题。
+**`atomic` 仅保证单次读/写的原子性**
-所以多线程并发编程来说,推荐使用锁是一个合理的方案。此外自旋锁不推荐使用,互斥锁中 pthread_mutex 等性能高一些的锁推荐使用。
+```objective-c
+// 即使属性是 atomic,以下代码仍然线程不安全
+if (self.atomicValue > 10) { // 步骤1:读取
+ self.atomicValue = 0; // 步骤2:写入
+}
+// 线程 A 可能在步骤1后,线程 B 修改了 atomicValue,导致逻辑错误
+```
+
+**无法保护对象内部状态**
+
+**指针与内容的区别**:`atomic` 仅保证指针本身的原子性(如 `NSArray *` 的赋值),但对象内部的状态(如数组的元素)不受保护。即使属性声明为 `atomic`,对可变集合的操作仍可能崩溃。
+
+```objective-c
+// 线程A
+NSMutableArray *array = self.atomicArray; // 原子性读取指针
+[array addObject:@"A"]; // 非原子操作,可能与其他线程冲突
+
+// 线程B
+NSMutableArray *array = self.atomicArray; // 原子性读取指针
+[array removeAllObjects]; // 导致线程A的 addObject: 崩溃
+```
-## 多线程读写安全
+总结:atomic 是原子属性,它内部实现是针对属性的 setter、getter 进行加锁(早期实现是自旋旋,因为存在问题,后续替换为了 os_unfair_lock)。但是事实上在进行多线程编程的时候,我们针对数据的操作并不是修改指针本身(思考 NSString 的 getter、setter),而是操作类似 NSMutableArray、NSDictionary 这样的 case。比如 `@property (atomic, strong) NSMutableArray *hobbies;` 如果在多线程情况下进行处理,一边生产者添加数据,一边消费者消费数据,则会产生多线程问题。
-读写的特点:
+所以多线程并发编程来说,线程安全是一个系统性问题,无法仅靠声明 `atomic` 解决。推荐使用锁是一个合理的方案。此外自旋锁不推荐使用,互斥锁中 pthread_mutex 等性能高一些的锁推荐使用。
+
+
+
+### 多线程读写锁
+
+#### 读写的特点
- 同一时间,只能有1个线程进行写的操作(只能有1个写)
- 同一时间,允许有多个线程进行读的操作(可以同时读)
- 同一时间,不允许既有写的操作,又有读的操作(读写不能同时进行)
-“多读单写”问题,经常用于文件、数据的读写操作。iOS 主流方案有:
+**允许多个线程同时读,但仅允许一个线程写,读写分开的场景,提高读多写少场景的性能**。
+
+“多读单写”问题,经常用于文件、数据的,**频繁读取但写入较少**的共享资源(如缓存数据)
+
+iOS 主流方案有:
- pthread_rwlock:读写锁
@@ -1673,13 +1943,13 @@ pthread_rwlock_init(&_lock, NULL)
Demo
-
+
### dispatch_barrier_async
-多读单写,
+多读单写
```objectivec
// 初始化队列
@@ -1696,12 +1966,12 @@ dispatch_barrier_async(self.queue, ^{
注意:
-- 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
-- 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
+- **`dispatch_barrier_async` 函数传入的并发队列必须是自己通过 `dispatch_queue_cretate` 创建的**
+- **如果传入的是一个串行队列或全局并发队列,那 `dispatch_barrier_async` 函数便等同于 `dispatch_async` 函数的效果**
上 Demo
-
+
@@ -1730,7 +2000,7 @@ Demo
}
```
-
+
@@ -1753,12 +2023,195 @@ Demo
}
```
-
+
-结论:可以发现 GCD 的栅栏函数,拦不住全局队列,却可以拦住普通的队列。这是为什么?
+结论:可以发现 GCD `dispatch_barrier_async` 栅栏函数,拦不住全局队列,却可以拦住自己创建的普通队列。这是为什么?
全局队列的业务方不只是当前 App 进程,还有一些系统任务(全局并发队列中不仅有开发者的任务,还有系统的任务),如果我们用我们的任务去栏住系统的任务,可能会导致一些未知的错误。栅栏函数对全局并发队列无效,所以我们在开发的时候一定要注意
+
+
+#### 为什么 dispatch_barrier_async 拦不住全局并发队列
+
+##### 官方文档证明
+
+[Apple 官方文档](https://developer.apple.com/documentation/dispatch/dispatch_barrier_async?language=objc) `dispatch_barrier_async` 条目中也明确指出:
+
+> The queue you specify should be a concurrent queue that you create yourself using the [`dispatch_queue_create`](https://developer.apple.com/documentation/dispatch/dispatch_queue_create?language=objc) function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the [`dispatch_async`](https://developer.apple.com/documentation/dispatch/dispatch_async?language=objc) function.
+
+栅栏块仅对通过 `DISPATCH_QUEUE_CONCURRENT` 创建的并发队列有效。在串行队列或全局并发队列中,其行为与普通异步提交的任务相同
+
+
+
+##### 系统设计角度
+
+- **全局并发队列的共享性与系统任务**
+
+ - 全局并发队列是系统提供的共享并发队列,被整个进程(甚至系统)的多个模块共同使用。这些队列不仅执行当前应用提交的任务,还可能运行系统级别的后台任务(如日志、框架内部操作等)
+ - 如果开发者用栅栏函数拦截全局队列,可能会**阻塞系统关键任务**,导致不可预见的后果(如死锁、性能下降或功能异常)。因此,苹果从设计上禁用了栅栏对全局队列的支持,避免干扰系统行为
+
+- **自定义队列的私有性**
+
+ 自定义并发队列由开发者显式创建,完全由应用控制。所有提交到该队列的任务都是开发者显式添加的,没有外部任务干扰
+
+
+
+##### GCD 源码角度
+
+`libdispatch` repo 中和 `dispatch_barrier_async` 有关的2个函数为:`_dispatch_lane_wakeup`、`_dispatch_lane_barrier_complete`
+
+- `_dispatch_lane_wakeup`:处理自定义并发队列的任务唤醒逻辑。当检测到 `DISPATCH_WAKEUP_BARRIER_COMPLETE` 标志时,会调用 `_dispatch_lane_barrier_complete`,确保栅栏前的任务全部执行完毕后再执行栅栏任务
+- `_dispatch_lane_barrier_complete`: 具体处理栅栏同步逻辑,括等待队列中现有任务完成、执行栅栏任务、释放后续任务
+
+查看源码:`queue.c`(队列核心逻辑)、`queue_internal.h`(队列内部结构定义)、`source.c`(任务调度逻辑)
+
+下面的代码进行了简化
+
+队列结构
+
+```c++
+// 队列结构
+struct dispatch_queue_s {
+ const struct dispatch_queue_vtable_s *do_vtable; // 虚表指针(定义队列操作函数)
+ uint32_t dq_atomic_flags; // 队列状态标记(如是否并发、是否被栅栏阻塞)
+ // ... 其他字段(如任务链表、线程池引用等)
+};
+
+// 全局队列和自定义队列的虚表不同
+static const struct dispatch_queue_vtable_s _dispatch_queue_global_vtable = { /* 全局队列操作函数 */ };
+static const struct dispatch_queue_vtable_s _dispatch_queue_concurrent_vtable = { /* 自定义并发队列操作函数 */ };
+```
+
+调用 `dispatch_barrier_async` 任务提交:
+
+- 将 `block` 封装为 `dispatch_continuation_t` 对象,并标记 `DC_FLAG_BARRIER`
+
+ ```c++
+ dispatch_barrier_async_f(dispatch_queue_t dq, void *ctxt,
+ dispatch_function_t func) {
+ dispatch_continuation_t dc = _dispatch_continuation_alloc_cacheonly();
+ uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER; // 标记为栅栏函数
+ dispatch_qos_t qos;
+
+ if (likely(!dc)) {
+ return _dispatch_async_f_slow(dq, ctxt, func, 0, dc_flags);
+ }
+
+ qos = _dispatch_continuation_init_f(dc, dq, ctxt, func, 0, dc_flags);
+ _dispatch_continuation_async(dq, dc, qos, dc_flags);
+ }
+ ```
+
+- 将任务加入到队列。通过队列的 `dq_push` 方法将任务加入到队列任务链表里
+
+ ```c++
+ static inline void
+ _dispatch_continuation_async(dispatch_queue_class_t dqu,
+ dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
+ {
+ #if DISPATCH_INTROSPECTION
+ if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
+ _dispatch_trace_item_push(dqu, dc);
+ }
+ #else
+ (void)dc_flags;
+ #endif
+ return dx_push(dqu._dq, dc, qos); // 调用队列的入队函数
+ }
+
+ ```
+
+- **自定义并发队列** 的 `dx_push` 指向 `_dispatch_lane_push`,将任务插入队列的任务链表尾部
+
+- **全局队列** 的 `dx_push` 指向 `_dispatch_root_queue_push`,直接忽略 `DC_FLAG_BARRIER` 标记
+
+队列唤醒与任务调度
+
+当队列需要执行任务时,GCD 会调用队列的 `dx_wakeup` 方法。不同队列的唤醒逻辑存在差异:
+
+- 自定义并发队列的唤醒逻辑。自定义队列的 `dx_wakeup` 指向 `_dispatch_lane_wakeup`,其关键逻辑如下
+
+ ```c++
+ // 自定义队列唤醒函数(queue.c)
+ static void _dispatch_lane_wakeup(dispatch_lane_class_t dq, ...) {
+ // 检查队列状态和栅栏标记
+ if (dq->dq_atomic_flags & DC_FLAG_BARRIER) {
+ // 进入栅栏同步逻辑
+ _dispatch_lane_barrier_complete(dq);
+ } else {
+ // 普通任务调度
+ _dispatch_lane_drain(dq);
+ }
+ }
+
+ static void _dispatch_lane_barrier_complete(dispatch_lane_class_t dq, ...) {
+ // 1. 等待前置任务完成
+ while (存在未完成的非栅栏任务) {
+ _dispatch_lane_drain_non_barriers(dq); // 执行所有非栅栏任务
+ }
+
+ // 2. 执行栅栏任务
+ dispatch_continuation_t barrier_dc = 从队列中取出栅栏任务;
+ _dispatch_client_callout(barrier_dc->dc_func); // 执行栅栏块
+
+ // 3. 释放后续任务
+ _dispatch_lane_class_barrier_complete(dq); // 清除栅栏标记,唤醒后续任务
+ _dispatch_lane_drain(dq); // 继续执行后续任务
+ }
+ ```
+
+- 全队队列的唤醒逻辑
+
+ 全局队列的 `dx_wakeup` 指向 `_dispatch_root_queue_wakeup`,其逻辑完全忽略栅栏标记
+
+ ```c++
+ DISPATCH_OPTIONS(dispatch_wakeup_flags, uint32_t,
+ // The caller of dx_wakeup owns two internal refcounts on the object being
+ // woken up. Two are needed for WLH wakeups where two threads need
+ // the object to remain valid in a non-coordinated way
+ // - the thread doing the poke for the duration of the poke
+ // - drainers for the duration of their drain
+ DISPATCH_WAKEUP_CONSUME_2 = 0x00000001,
+
+ // Some change to the object needs to be published to drainers.
+ // If the drainer isn't the same thread, some scheme such as the dispatch
+ // queue DIRTY bit must be used and a release barrier likely has to be
+ // involved before dx_wakeup returns
+ DISPATCH_WAKEUP_MAKE_DIRTY = 0x00000002,
+
+ // This wakeup is made by a sync owner that still holds the drain lock
+ DISPATCH_WAKEUP_BARRIER_COMPLETE = 0x00000004,
+
+ // This wakeup is caused by a dispatch_block_wait()
+ DISPATCH_WAKEUP_BLOCK_WAIT = 0x00000008,
+
+ // This wakeup may cause the source to leave its DSF_NEEDS_EVENT state
+ DISPATCH_WAKEUP_EVENT = 0x00000010,
+
+ // This wakeup is allowed to clear the ACTIVATING state of the object
+ DISPATCH_WAKEUP_CLEAR_ACTIVATING = 0x00000020,
+ );
+
+
+ void _dispatch_root_queue_wakeup(dispatch_queue_global_t dq,
+ DISPATCH_UNUSED dispatch_qos_t qos, dispatch_wakeup_flags_t flags)
+ {
+ if (!(flags & DISPATCH_WAKEUP_BLOCK_WAIT)) {
+ DISPATCH_INTERNAL_CRASH(dq->dq_priority,
+ "Don't try to wake up or override a root queue");
+ }
+ if (flags & DISPATCH_WAKEUP_CONSUME_2) { // 只处理 DISPATCH_WAKEUP_CONSUME_2 类型,忽略 DISPATCH_WAKEUP_BARRIER_COMPLETE
+ return _dispatch_release_2_tailcall(dq);
+ }
+ }
+ ```
+
+可以看到:`_dispatch_root_queue_wakeup`:全局队列的唤醒函数。此函数未处理 `DC_FLAG_BARRIER` 标记,直接忽略栅栏逻辑,按默认并发方式执行任务。因此,全局队列无法支持栅栏功能
+
+**总结**:`dispatch_barrier_async` 的设计初衷是为**开发者控制的私有并发队列**提供同步机制,避免全局队列的共享性引入风险。因此,务必仅将**栅栏用于自定义的并发队列**。
+
+
+
### dispatch_group_async
如何实现 A、B、C 三个任务并发执行完,再去执行任务 D ?假设需求是根据省市区下载 json,然后根据 json 数据,选中地址 picker view。
@@ -1780,7 +2233,7 @@ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
-## NSOperation
+## 六、NSOperation
需要和 NSOperationQueue 配合使用。优点:
@@ -1909,7 +2362,7 @@ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
-## NSThread
+## 七、NSThread
NSThread 的一个工作流程如下:
@@ -2033,7 +2486,7 @@ main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始
-## 其他常见的多线程编程模式
+## 八、其他常见的多线程编程模式
### Promise
diff --git a/Chapter1 - iOS/1.40.md b/Chapter1 - iOS/1.40.md
index 88db707..aa945a1 100644
--- a/Chapter1 - iOS/1.40.md
+++ b/Chapter1 - iOS/1.40.md
@@ -21,7 +21,7 @@
假设我们计算有`128MB`内存,程序A需要`10MB`,程序B需要`100MB`,程序C需要`20MB`。如果我们需要同时运行程序A和B,那么比较直接的做法是将内存的`前10MB`分配给程序A,`10MB~110MB`分配给B。
-
+
但存在以下问题:
@@ -51,7 +51,7 @@
比如A需要`10M`,就假设有`0x00000000` 到`0x00A00000`大小的虚拟空间,然后从物理内存分配一个相同大小的空间,比如是`0x00100000`到`0x00B00000`。操作系统来设置这个映射函数,实际的地址转换由硬件完成。如果越界,硬件就会判断这是一个非法访问,拒绝这个地址请求,并上报操作系统或监控程序。
-
+
这样一来利用**分段**的方式可以解决之前的**地址空间不隔离**和**程序运行地址不确定**
@@ -78,13 +78,13 @@
-
+
保护页也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问,而且只有操作系统有权修改这些属性,那么操作系统就可以做到保护自己和保护进程。
虚拟存储的实现需要硬件支持,几乎所有CPU都采用称为**MMU的部件来进行页的映射**
-
+
在页映射模式下,`CPU`发出的是`Virtual Address`,即我们程序看到的是`虚拟地址`。经过`MMU`转换以后就变成了`Physical Address`。一般`MMU`集成在`CPU`内部,不会以独立的部件存在。
@@ -102,7 +102,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
栈、堆、BSS、数据段、代码段
-
+
@@ -131,7 +131,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
代码段(code segment):编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。
-
+
上 Demo 验证
@@ -210,7 +210,7 @@ Tagged Pointer 格式下,指针值不再是有效抵制,而是表示值。
当对 TaggedPointer 数据调用方法的时候,objc_msgSend 能识别出如果是 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。所以使用了 TaggedPointer 技术不仅节约内存空间,又能提高方法查找速度。
-
+
@@ -311,7 +311,7 @@ Tagged Pointer 也就是一个伪指针,对象的指针中存储的数据变
Demo
-
+
在 64 位的 cpu 下,如果使用 Tagged Pointer 技术的话,则 NSNumber 对象的值直接存储在了指针中,系统不会为其在堆上分配内存,可以节省很多内存开销。此时,NSNumber 对象的指针中存储的数据变成了 Tag + Data 的形式(Tag 为特殊标记,用于区分NSNumber、NSDate、NSString 等小内存对象的类型;Data 为具体的值)。这样使用一个 NSNumber 对象只需要在栈中开辟 8 个字节的指针内存。当栈中 8 个字节的指针内存不够存储数据时,才会再将 NSNumber 对象存储到堆中
@@ -331,7 +331,7 @@ Demo
路径:Xcode - Edit Scheme - Run - Arguments - Environment Variables - 添加环境变量 `OBJC_DISABLE_TAG_OBFUSCATION` 设置为 YES 即可。
-
+
@@ -500,7 +500,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u
}
```
-
+
@@ -508,7 +508,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u
#### Tagged Pointer 与 isa
-
+
通过参考 objc 源码,针对对象指针进行解密后发现:
@@ -539,7 +539,7 @@ b 也就是11,二进制为 `1011`,Tagged Pointer 中,iOS 侧第一位是 T
3 区分数据类型。具体是什么数据类型,继续做个实验看看
-
+
@@ -560,7 +560,7 @@ b 也就是11,二进制为 `1011`,Tagged Pointer 中,iOS 侧第一位是 T
Objc 源码中,NSInteger、NSUInteger 都是别名。初始化 NSNumber 的时候用的是 `NSNumber numberWithInteger:<#(NSInteger)#>`
-
+
@@ -638,7 +638,7 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum
验证下
-
+
可以看到:
@@ -651,11 +651,11 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum
下面是针对 double 包装成 NSNumber 的 Tagged Pointer 指针结构拆分:
-
+
-
+
@@ -741,8 +741,6 @@ _objc_isTaggedPointer(const void * _Nullable ptr)
- 非 iOS: 最低有效位是1`1UL`。也就是 `0000...1`,共63个0,最后一位是1。
-
-
所以,判断是不是 TaggedPointer 可以用下面代码判断
```c++
@@ -761,6 +759,21 @@ static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
+#### 为什么 ARM64 使用 `(1ULL << 63)` 判断
+
+ARM64 地址空间设计:
+
+- 用户空间地址范围:
+ ARM64 的用户态程序地址通常为 `0x0000000000000000 ~ 0x0000FFFFFFFFFFFF`,最高位(第 63 位)始终为 `0`。
+- 内核空间地址:
+ 内核空间地址最高位为 `1`(如 `0xFFFF000000000000`),但用户态程序无法访问。
+
+因此,苹果**将最高位用作 Tagged Pointer 标志**,天然避免与普通指针冲突。
+
+其他几种情况就不举例子了。
+
+
+
### NONPOINTER_ISA
在64位架构下,ISA 占64位空间,但实际上用不了那么多,实际上有32位或者40位就够用了,剩余的比较浪费。iOS 为了提高利用率,在剩余的位上存储了一些内存管理相关的信息。所以是不纯粹的指针。叫 NONPOINTER_ISA。
@@ -848,7 +861,7 @@ Demo1
运行该代码会 Crash,报错信息如下
-
+
@@ -859,12 +872,10 @@ Demo1
不仔细想可能发现不了问题,看到 `objc_release` 就会想到是在多线程情况下 NSString 的 setter 方法内,ARC 代码经过编译器最后会按照 MRC 去运行。所以 Setter 类似下面代码。
```objectivec
--(void)setName:(NSString *)name
-{
- if (_name!=name) {
- [name retain];
- [_name release];
- _name = name;
+-(void)setName:(NSString *)name {
+ if (_name!=name) { // 避免对同一个对象多次复制
+ [_name release]; // 释放旧值
+ _name = [name copy]; // 拷贝并持有新值
}
}
```
@@ -873,11 +884,11 @@ Demo1
改法1:将 property 改为 **atomic** 修饰的。
-
+
改法2:对 name 加锁
-
+
@@ -885,15 +896,24 @@ Demo1
Demo2
-
+
同样的代码字符串变短居然不 crash 了?因为命中 Tagged Pointer 逻辑了,查看类型是 `NSTaggedPointerString`
-本问题本质是
+本问题本质是:
-- ARC 代码在编译后真正运行阶段是走 MRC 的,strong、copy 内部都会 release 旧的,copy/retain 新的
+- ARC 代码在编译后真正运行阶段是走 MRC 的,strong、copy 修饰的属性,内部的 setter 实现都会 release 旧的值,copy/retain 新的
+
+ ```objective-c
+ - (void)setName:(name) {
+ if (!_name != name) {
+ [_name release];
+ _name = [name copy];
+ }
+ }
+ ```
- 多线程情况下访问 setter 需要加锁
@@ -932,7 +952,7 @@ NSString、NSMutableString 继承关系如下:
-
+
通过 `[[NSString alloc] initWithString:@"**"]` 方式创建的 NSString 字符串
@@ -996,31 +1016,33 @@ alloc 后的对象,在引用计数表中是没有对应的 key、value 信息
### 引用计数和 getter、setter
+为了研究内存管理,使用 MRC 环境。
+
iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC 对象引用计数默认是1,当引用计数减为 0,OC 对象就会销毁,释放其占用的内存空间
调用 retain/copy 会让 OC 对象的引用计数 +1,调用 release 会让 OC 对象的引用计数 -1。
-
+
可以看到,如果我们提前将 cat 释放了,那后续赋值给 person 的 _cat 成员变量就没法使用了,因为已经释放了,否则就会造成 `EXC_BAD_ACCESS`。这样子太不灵活了。需要改进下:
调用 setCat 的时候,对传入的 cat 进行 retain,引用计数 +1,谁用谁管理,同样的最后在 Person 对象释放的时候对 cat 进行 release,引用计数 -1.
-
+
但上面的代码不完美,还是存在问题。假设 cat1、cat2 2个对象,当作参数调用2次 setCat 方法,如果 setCat 方法内部不做处理,会导致第2次调用 setCat 后,之前调用时传入的 cat1 会无法释放。
-
+
修改下。调用 setCat 方法时,对之前的 _cat 调用 release,对旧的引用计数-1,再对新传入的对象调用 retain,让引用计数+1,然后赋值
-
+
上面的代码还是存在问题,会造成僵尸对象问题
-
+
分析下 cat 的引用计数情况:
@@ -1032,13 +1054,13 @@ iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC
改进
-
+
-内存管理的经验总结
+### 内存管理的经验总结
- 当调用 alloc、new、copy、mutableCopy 方法返回了一个对象,在不需要这个对象时,要调用 release 或者 autorelease 来释放它
@@ -1082,8 +1104,7 @@ Cat *cat = [[Cat alloc] init]; // 1
改进
```objectivec
-- (void)setCat:(Cat *)cat
-{
+- (void)setCat:(Cat *)cat {
if (_cat != cat) {
[_cat release];
_cat = [cat retain];
@@ -1129,13 +1150,13 @@ ARC 其实是 LLVM + Runtime 共同作用的结果。LLVM 编译器自动插入
OC 有2个拷贝方法
- copy 不可变拷贝,产生新不可变对象
- - 针对不可变类型,调用 copy 方法,效果是产生一个新的引用。因为本身不可变,所以一个引用就好,可以实现“产生不可变对象”的目的。
+ - **针对不可变类型,调用 copy 方法,效果是产生一个新的引用。因为本身不可变,所以一个引用就好,可以实现“产生不可变对象”的目的**。
- - 针对可变类型,调用 copy 方法,效果是产生一个新的对象,并且将内容拷贝到新对象里面。产生1个新的不可变对象
+ - **针对可变类型,调用 copy 方法,效果是产生一个新的对象,并且将内容拷贝到新对象里面。产生1个新的不可变对象**
- mutableCopy 可变拷贝,产生新可变对象
- 针对不可变类型,调用 mutablecopy 方法,需要产生一个可变对象,但是需要互不影响的新的可变对象
- - 针对可变类型,调用 mutablecopy 方法,需要产生一个新的可变对象。
+ - **针对可变类型,调用 mutablecopy 方法,需要产生一个新的可变对象**。
上个 Demo1
@@ -1192,12 +1213,30 @@ NSLog(@"array2 --- %zd", array2.retainCount);
此外 mutableCopy 是 Foundation 针对集合类提供的。如果自定义对象需要支持 copy 方法,需遵循对应的`NSCopyint` 协议,实现协议方法 `-(id)copyWithZone:(NSZone *)zone`
+
+
+Demo3
+
+
+
+会发现发生了 crash。问题是因为
+
+- `@property (nonatomic, copy) NSMutableArray *data;` 对 NSMutableArray 用了 copy 修饰词。在 setter 方法里的实现就是 ARC 编译器会做的事情。对 NSMutableArray 调用 copy 方法,得到一个不可变对象 NSArray
+- NSArray 不存在 `addObject` 方法。调用不存在方法自然会报 `unrecognized selector sent to instance ...` 的
+
总结:
-| | NSString | NSMutableString | NSArray | NSMutableArray | NSDictionary | NSMutableDictionary |
-| ----------- | ------------------- | ------------------- | ------------------ | ------------------ | ----------------------- | ----------------------- |
-| copy | NSString 浅拷贝 | NSString 深拷贝 | NSArray 浅拷贝 | NSArray 深拷贝 | NSDictionary 浅拷贝 | NSDictionary 深拷贝 |
-| mutableCopy | NSMutableString 深拷贝 | NSMutableString 深拷贝 | NSMutableArray 深拷贝 | NSMutableArray 深拷贝 | NSMutableDictionary 深拷贝 | NSMutableDictionary 深拷贝 |
+- **不可变对象,调用 copy 方法,得到一个不可变的副本,就是浅拷贝**,其余都是深拷贝
+- 若可变类型(NSMutableArray、NSMutableString、NSMutableDictionary)属性需要 **可变性**,应使用 `strong` 配合
+
+| 数据类型 | - 调用 copy 方法得到
- 深 or 浅拷贝 | - 调用 mutablecopy 方法得到
- 深 or 浅拷贝 |
+| ------------------- | -------------------------------------- | --------------------------------------------- |
+| NSString | NSString
浅拷贝 | NSMutableString
深拷贝 |
+| NSMutableString | NSArray
深拷贝 | NSMutableString
深拷贝 |
+| NSArray | NSArray
浅拷贝 | NSMutableArray
深拷贝 |
+| NSMutableArray | NSArray
深拷贝 | NSMutableArray
深拷贝 |
+| NSDictionary | NSDictionary
浅拷贝 | NSMutableDictionary
深拷贝 |
+| NSMutableDictionary | NSDictionary
深拷贝 | NSMutableDictionary
深拷贝 |
深拷贝和浅拷贝的区别?
- 深拷贝不会影响对的引用计数
@@ -1207,6 +1246,81 @@ NSLog(@"array2 --- %zd", array2.retainCount);
## 引用计数及weak指针
+### weak 指针
+
+Case1
+
+```objective-c
+__strong Person *p1;
+__weak Person *p2;
+__unsafe_unretained Person *p3;
+{
+ Person *p = [[Person alloc] init];
+}
+```
+
+大括号结束,则立马调用了 Person 的 dealloc 方法
+
+Case2
+
+```objective-c
+{
+ Person *p = [[Person alloc] init];
+ p1 = p
+}
+```
+
+有强指针指向,大括号结束,引用计数位1,则不会执行 dealloc 方法
+
+Case3
+
+```objective-c
+{
+ Person *p = [[Person alloc] init];
+ p2 = p
+}
+NSLog(@"p2:%@", p2);
+```
+
+弱指针指向则不改变引用计数,大括号结束,则不执行 dealloc 方法
+
+Case4
+
+```objective-c
+{
+ Person *p = [[Person alloc] init];
+ p3 = p
+}
+NSLog(@"p3:%@", p3);
+```
+
+用 `__unsafe_unretained` 指向的指针,当对象释放后,则会 crash `Thread 1: EXC_BAD_ACCESS (code=1, address=0x3eadde6d8408)`
+
+原因在于:
+
+1. **对象释放后的行为**:
+ - `__weak`:当对象被释放时,指针会自动设置为 nil(空指针)
+ - `__unsafe_unretained`:当对象被释放时,指针保持不变,成为"悬垂指针"(dangling pointer)
+2. **安全性**:
+ - `__weak` 是安全的,因为访问 nil 指针不会导致崩溃
+ - `__unsafe_unretained` 是不安全的,因为访问已释放对象会导致崩溃
+
+为什么会有这种差异?
+
+- **`__weak` 的实现**:
+ - 运行时系统维护了一个弱引用表
+ - 当对象被释放时,运行时系统会遍历所有指向该对象的弱引用,并将它们置为 nil
+ - 这个过程是自动的,由 ARC 管理
+- **`__unsafe_unretained` 的实现**:
+ - 完全不参与引用计数
+ - 运行时系统不会跟踪这些指针
+ - 当对象被释放时,指针仍然指向原来的内存地址
+ - 如果访问这个指针,实际上是在访问已释放的内存,导致崩溃
+
+
+
+### 引用计数信息
+
```objectivec
union isa_t {
Class cls;
@@ -1233,6 +1347,25 @@ iOS 从 64 位开始开始,对 isa 进行了优化,信息存放于 union 结
也就是说,iOS 从64位开始,引用计数存放于 isa 结构体的一个 union 中,字段为 `extra_rc`,值为对象引用计数值 。当引用计数过大无法存放的时候, union 中 `has_sidetable_rc `为 1,则引用计数存放于 SideTable 结构体中。
+QA:不知道你是否会有这样的疑问:extra_rc 字段都19位了,还担心不够,居然设计了一个 has_sidetable_rc字段?按理说19位可以存储非常大的数字了,什么对象会有那么大的引用计数?
+
+- **循环引用或泄漏**:若代码存在循环引用或内存泄漏,引用计数可能无限增长。虽然 19 位理论上能存储 50 万+的引用,但设计上需要防止溢出导致未定义行为。
+
+- **框架与系统级对象**:某些系统级对象(如单例、缓存池)可能被大量持有。例如:
+
+ ```objective-c
+ // 假设某个缓存池错误地持有了大量对象
+ for (int i = 0; i < 1000000; i++) {
+ [cache addObject:someObject]; // someObject 的引用计数暴增
+ }
+ ```
+
+所以:
+
+- 19 位 `extra_rc` :覆盖了绝大多数场景,同时避免了频繁访问 Side Table 的性能损失。
+- **`has_sidetable_rc`** 作为安全网,处理极端情况(如泄漏、系统级对象),同时支持 Side Table 的多功能用途(弱引用、关联对象等)。
+- 这种设计体现了 **“优化常态,防御极端”** 的工程哲学,在内存效率、性能和健壮性之间取得平衡
+
### 散列表
@@ -1247,9 +1380,20 @@ struct SideTable {
};
```
-其中 refcnts 是一个存放着对象引用计数的散列表。
+其中 refcnts 是一个存放着对象引用计数的散列表。其实 `RefcountMap` 是一个 `objc::DenseMap` ,是高性能哈希表,专为密集内存布局优化
+```c++
+// RefcountMap disguises its pointers because we
+// don't want the table to act as a root for `leaks`.
+typedef objc::DenseMap,size_t,RefcountMapValuePurgeable> RefcountMap;
+```
+其中:
+
+- 键类型:`DisguisedPtr`:用于伪装指针,对原始对象指针进行编码,使其不直接暴露内存地址。同时**避免内存泄漏误报**:防止 `leaks` 等工具将哈希表中的指针误判为活动根节点(Root)。若存储原始指针,工具可能认为这些是有效引用,导致泄漏检测失效。
+- 值类型:`size_t`:对象的引用计数值,可能包含额外标志位
+- 策略类:`RefcountMapValuePurgeable` :管理哈希表值的生命周期
+-
查看 objc4 源码,看看如何获取引用计数
@@ -1265,15 +1409,15 @@ _objc_rootRetainCount(id obj)
inline uintptr_t
objc_object::rootRetainCount()
{
- if (isTaggedPointer()) return (uintptr_t)this;//如果是采用isTaggedPointer直接返回this本身
+ if (isTaggedPointer()) return (uintptr_t)this; //如果是采用 isTaggedPointer 直接返回 this 本身
sidetable_lock();
- isa_t bits = LoadExclusive(&isa.bits);//取出isa_t
+ isa_t bits = LoadExclusive(&isa.bits); // 取出isa_t
ClearExclusive(&isa.bits);
- if (bits.nonpointer) { //如果是优化的指针
+ if (bits.nonpointer) { // 如果是优化的指针
uintptr_t rc = 1 + bits.extra_rc; // 引用计数值
- if (bits.has_sidetable_rc) {// 如果 has_sidetable_rc 为1,则说明引用计数过大无法存贮在 isa 中,需要去 SideTable 中获取
- rc += sidetable_getExtraRC_nolock();//去sidetable中去拿取计数
+ if (bits.has_sidetable_rc) { // 如果 has_sidetable_rc 为1,则说明引用计数过大无法存贮在 isa 中,需要去 SideTable 中获取
+ rc += sidetable_getExtraRC_nolock(); // 去 sidetable 中去拿取计数
}
sidetable_unlock();
return rc;
@@ -1287,65 +1431,32 @@ objc_object::sidetable_getExtraRC_nolock()
{
assert(isa.nonpointer);
SideTable& table = SideTables()[this]; // SideTables 重载 [] 运算符,本质上就是调用 indexForPointer 方法
- RefcountMap::iterator it = table.refcnts.find(this);
+ RefcountMap::iterator it = table.refcnts.find(this); // 从 refcnts 哈希表中根据 this 指针地址,经过哈希计算,得到结果
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
-//没有优化过的isa去sidetable中拿计数
+// 没有优化过的 isa,则去 sidetable 中拿计数
uintptr_t
objc_object::sidetable_retainCount()
{
- SideTable& table = SideTables()[this];//根据地址拿到SideTable
+ SideTable& table = SideTables()[this]; // 根据地址拿到 SideTable
size_t refcnt_result = 1;
table.lock();
- RefcountMap::iterator it = table.refcnts.find(this);//从SideTable中根据地址拿取RefcountMap
+ RefcountMap::iterator it = table.refcnts.find(this); // 从 SideTable 中根据地址拿取 RefcountMap
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
- return refcnt_result;//返回RefcountMap中的计数
+ return refcnt_result; // 返回 RefcountMap 中的计数
}
```
-`__unsafe_unretained` 不安全
-
-如何体现?上 Demo
-
-```objectivec
-__weak Person *p2;
-__unsafe_unretained Person *p3;
-{
- Person *p = [[Person alloc] init];
- p2 = p;
-}
-NSLog(@"%@", p2);
-2022-04-12 21:39:30.308917+0800 Main[5307:98296] -[Person dealloc]
-2022-04-12 21:39:30.309413+0800 Main[5307:98296] (null)
-```
-
-可以看到出了代码块,之后 p2 虽然指向 p,但是 p 没有强指针指向,所以回收了,此时打印 p2,是 null。
-
-```objectivec
-__unsafe_unretained Person *p3;
-{
- Person *p = [[Person alloc] init];
- p3 = p;
-}
-NSLog(@"%@", p3);
-2022-04-12 21:40:47.558581+0800 Main[5342:99598] -[Person dealloc]
-2022-04-12 21:40:47.559330+0800 Main[5342:99598]
-```
-
-当对象用 `__unsafe_unretained` 修饰后,对象虽然被释放了,但是内存还没回收,这时候去使用,很容易出错,报 `EXC_BAD_ACCESS` 。
-
-
-
### 分离锁
为什么不是一个 SideTable?而是 SideTables
@@ -1371,9 +1482,9 @@ NSLog(@"%@", p3);
SideTables 的本质是一张 **Hash 表**。
-也就是根据对象地址,如何快速知道,属于哪一张 SideTable?哈希函数。
+快速分流的目的,就是根据对象地址,如何快速计算出属于哪一张 SideTable?这个就是哈希函数要做的事情。
-输入:ptr -> 经过:f(ptr) -> 计算出 index。`f(ptr) = (uintptr_t)ptr % array.count`
+输入:ptr -> 经过:f(ptr) -> 计算出 index。即 `f(ptr) = (uintptr_t)ptr % array.count`
使用哈希查找就是为了提高查找效率。
@@ -1481,22 +1592,28 @@ class StripedMap {
```
- iOS 侧 StripeCount 为8
+
- `indexForPointer` 方法根据传入的指针,将指针地址转换为 uintptr_t 类型,然后将地址右移4位和右移9位的结果进行异或运算,然后将结果取模 StripeCount(iOS 侧为8),用于确定索引的范围(范围在:[0, stripeCount -1] )
-- Operator 重写了运算符 [],底层调用 `indexForPointer` 方法,使之使用起来更像一个数组。
+
+- Operator 重写了运算符 [],底层调用 `indexForPointer` 方法,使之使用起来更像一个数组
+
+- 位移和异或操作在 CPU 指令级别极快,适合高频调用场景(如对象内存管理)。
+
+
### 引用计数表
-
+
-### 弱引用表 weak 指针原理
+### weak 指针原理
weak_table_t 结构如下:
-
+
```c++
#define WEAK_INLINE_COUNT 4
@@ -1511,7 +1628,7 @@ struct weak_entry_t {
weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
- uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
+ uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};
struct {
@@ -1519,7 +1636,7 @@ struct weak_entry_t {
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
-
+ // 判断当前是否使用动态哈希表模式
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
@@ -1542,10 +1659,26 @@ struct weak_entry_t {
可以看到
-- 在 `weak_entry_t ` 的结构中有联合体,在联合体的内部有定长数组 `inline_referrers[WEAK_INLINE_COUNT]` 和动态数组`weak_referrer_t *referrers `两种方式来存储弱引用对象的指针地址。
-- 通过 `out_of_line()` 这样一个函数方法来判断采用哪种存储方式
- - 当弱引用该对象的指针数目小于等于 `WEAK_INLINE_COUNT`时,使用定长数组
- - 当超过`WEAK_INLINE_COUNT`时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储
+- 在 `weak_entry_t ` 的结构中有联合体,联合体用于高效存储弱引用指针的地址,分为两种模式:定长数组 `inline_referrers[WEAK_INLINE_COUNT]` 和动态数组`weak_referrer_t *referrers `两种方式来存储弱引用对象的指针地址
+
+ - 内联数组模式(`inline_referrers`):直接存储少量弱引用指针地址
+ - 动态哈希表模式(`referrers`):存储大量弱引用指针地址,通过哈希表管理
+
+- 通过 `out_of_line()` 这样一个函数方法来判断采用哪种存储方式:
+
+ - 内联数组(`inline_referrers`)
+ - 当弱引用该对象的指针数目小于等于 `WEAK_INLINE_COUNT`(即4)时,使用定长数组直接存储前4个弱引用指针地址
+ - 优势:避免动态内存分配,提升对小规模弱引用的处理效率
+
+
+ - 动态哈希表(`referrers`)
+ - 当超过`WEAK_INLINE_COUNT`时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储
+ - `weak_referrer_t *referrers`:指向动态分配的哈希表数组。
+ - `out_of_line_ness`(2位):标记当前是否为动态模式(值`REFERRERS_OUT_OF_LINE`)。
+ - `num_refs`:当前存储的弱引用数量。
+ - `mask`:哈希表容量(总槽位数,值为`2^n - 1`)。
+ - `max_hash_displacement`:最大哈希冲突步长,用于优化查找
+ - 优势:支持大规模弱引用的高效插入、查找和删除。
@@ -1575,7 +1708,7 @@ objc_destoryWeak(&obj);
上 Demo
-
+
可以看到当一个 weak 指针被赋值的时候,底层调用了 `objc_initWeak`,跟踪查看 objc 源码
@@ -1634,7 +1767,7 @@ static id storeWeak(id *location, objc_object *newObj)
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
- !((objc_class *)cls)->isInitialized()) // 如果 cla 还没有初始化,则先初始化,再尝试设置 weak
+ !((objc_class *)cls)->isInitialized()) // 如果 cls 还没有初始化,则先初始化,再尝试设置 weak
{
SideTable::unlockTwo(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
@@ -1985,6 +2118,7 @@ objc_object::sidetable_clearDeallocating()
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
+ // 清理引用计数信息
table.refcnts.erase(it);
}
table.unlock();
@@ -2007,14 +2141,17 @@ weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
size_t count;
// 判断使用动态数组还是定长数组,来找出 referrers 的数组长度和数组地址
if (entry->out_of_line()) {
+ // 进入这个 if 则说明是动态哈希表模式
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
+ // 使用内联数组模式
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
+ // 抹平差异,无差别处理是内联数组还是动态哈希表,根据 count 遍历 referrers,依次设置为 nil
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i]; // 取出每个 weak 指针地址
if (referrer) {
@@ -2036,10 +2173,47 @@ weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
}
```
-总结:对象释放的时候,调用 `clearDeallocating` 根据对象地址,获取 weak 指针地址的数组,然后遍历,依次设置为 nil,最后从 weak_table_t 中移除 weak_entry_t,完成了对象释放后,所有指向该对象的 weak 指针都被设置为 nil 这个效果
+总结:当对象的引用计数为0的时候,会先调用 dealloc 方法,然后内部调用 `clearDeallocating` 方法,该方法清理与对象相关的弱引用。
+
+- 会根据对象地址,经过哈希计算算,从全局的 SideTables 中找到对应的 SideTable
+- 然后访问成员变量 `weak_table_t weak_table`,其中存储着当前对象所有的弱引用。
+- 再次根据哈希计算,从 weak_entries 中找到 `weak_entry_t`
+- `weak_entry_t` 根据 `out_of_line` 方法,判断采用的是动态哈希表还是内联数组。用2个变量(referrers、count)抹平差异,记录个数和数据源,然后依次遍历,设置为 nil
+- 最后从 weak_table_t 中移除 weak_entry_t,完成了对象释放后,所有指向该对象的 weak 指针都被设置为 nil 这个效果
+### `__unsafe_unretained` 不安全
+
+如何体现?上 Demo
+
+```objectivec
+__weak Person *p2;
+__unsafe_unretained Person *p3;
+{
+ Person *p = [[Person alloc] init];
+ p2 = p;
+}
+NSLog(@"%@", p2);
+2022-04-12 21:39:30.308917+0800 Main[5307:98296] -[Person dealloc]
+2022-04-12 21:39:30.309413+0800 Main[5307:98296] (null)
+```
+
+可以看到出了代码块,之后 p2 虽然指向 p,但是 p 没有强指针指向,所以回收了,此时打印 p2,是 null。
+
+```objectivec
+__unsafe_unretained Person *p3;
+{
+ Person *p = [[Person alloc] init];
+ p3 = p;
+}
+NSLog(@"%@", p3);
+2022-04-12 21:40:47.558581+0800 Main[5342:99598] -[Person dealloc]
+2022-04-12 21:40:47.559330+0800 Main[5342:99598]
+```
+
+当对象用 `__unsafe_unretained` 修饰后,对象虽然被释放了,但是内存还没回收,这时候去使用,很容易出错,报 `EXC_BAD_ACCESS`
+
### 总结
在 OC 中,每个对象对应一个 SideTable,而一个 SideTable 对应多个对象。StrippedMap 是一种数据结构,用于实现高效的并发访问和锁分离。在 StrippedMap 中,有多个 SideTable 实例(iOS 真机,是8个),每个 SideTable 包含一个 `weak_table_t` 和一个 `spinlock_t` ,以实现对弱引用表和引用计数的线程安全访问。这种设计通过锁分离和分区的方式,提高了系统的并发性能,避免了全局锁带来的性能瓶颈,从而实现了高效的对象管理和引用计数处理。(但也有缺点,哈希表越满,哈希冲突会多,性能越差.)
@@ -2375,7 +2549,7 @@ void sel_init(size_t selrefCount){
在 gone 处加断点,利用 runtime 查看类中的方法信息
-
+
发现存在 `.cxx_destruct` 方法。
@@ -2410,7 +2584,7 @@ void sel_init(size_t selrefCount){
@end
```
-
+
Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString,则叫实例变量。
@@ -2424,7 +2598,7 @@ Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint,当变量被修改时会触发断点,可以看出从某个值变为 0x0,也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil.
-
+
@@ -2530,6 +2704,8 @@ id objc_storeStrong(id *object, id value) {
在 `.cxx_destruct` 方法内部会对所有的实例变量调用 `objc_storeStrong(&ivar, null)` ,实例变量就会 release 。
+类的 `isa` 指针指向的类结构体中包含 `CLS_HAS_CXX_STRUCTORS` 标志位时,`object_cxxDestruct`才会被调用。该标志在编译时由 Clang 自动生成,若类包含 C++ 成员变量或显式定义析构函数58。
+
### 自动调用 [super dealloc] 的原理
@@ -2667,7 +2843,7 @@ Person *person2 = [Person personWithName:@"FantasticLBP"];
隐式调用工厂方法
-
+
@@ -2677,7 +2853,7 @@ Person *person2 = [Person personWithName:@"FantasticLBP"];
如何修改?加一个 bridge 即可。
-
+
由于 ARC 没有加 retain。所以 `person = (__bridge id)result;` 这里完成了对象的 retain。ARC 在退出方法的作用域时给对象加上release。前后对应,内存正确。
@@ -2788,7 +2964,7 @@ class AutoreleasePoolPage {
- 每个 AutoreleasePoolPage 对象占用 4096 (16的3次方,0x2000)字节内存,除了用来存放它内部的成员变量(内部成员固定有7个,56个字节,即 `0x18`, `0x1000 + 0x38 = 0x1038` ),剩下的空间用来存放 autorelease 对象的地址
- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个 AutoreleasePoolPage 对象,parent 指向上一个 AutoreleasePoolPage 对象
-
+
```objectivec
id * begin() {
@@ -2813,9 +2989,7 @@ id * end() {
- end 方法返回 autoreleasePoolPage 对象中结束存储 autorelease 对象的开始地址。该怎么算?`end 地址 = 自己对象的开始地址 + 4096字节`。其中 `SIZE` 是一个宏计算结果,也就是 4096。
-
-
-#### 源码分析
+### 源码分析
1.源码分析 push 方法
@@ -3114,7 +3288,7 @@ class AutoreleasePoolPage : private AutoreleasePoolPageData {
举个例子,for 循环创建1000个 Person 对象,用 autorelease 修饰,如何工作?
-
+
分析:
@@ -3153,7 +3327,7 @@ int main(int argc, const char * argv[]) {
main 方法内部3个 autoreleasepool 底层怎么样工作的?
-
+
分析:
@@ -3944,115 +4118,9 @@ static inline id *autoreleaseFast(id obj) {
-### ARC 时代会自动加 autorelease
-
-系统容器类,在使用 block 枚举器的时候,内部会自动创建 AutoreleasePool
-
-```objectivec
-[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
- @autoreleasepool {
- <#statements#>
- }
-}];
-```
-
-所以,我们老老实实写的 for、while 循环中需要手加局部 AutoreleasePool。推荐使用系统提供的容器类的 block 枚举器。
-
-
-
-Cocoa 框架中,很多类方法用于返回 autorelease 对象。
-
-```objective-c
-// NSArray.m
-+ (id) arrayWithCapacity: (NSUInteger)numItems
-{
- return AUTORELEASE([[self allocWithZone: NSDefaultMallocZone()]
- initWithCapacity: numItems]);
-}
-
-// NSDictionary.m
-+ (id) dictionary
-{
- return AUTORELEASE([[self allocWithZone: NSDefaultMallocZone()] init]);
-}
-```
-
-
-
-### autorelease 对象什么时候调用 release 方法
-
-iOS 项目中,`viewDidLoad` 方法内的创建的对象什么时候释放?
-
-```objective-c
-@implementation ViewController
-- (void)viewDidLoad {
- [super viewDidLoad];
- Person *p = [[Person alloc] init];
- NSLog(@"%s", __func__);
-}
-- (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated];
- NSLog(@"%s", __func__);
-}
-- (void)viewDidAppear:(BOOL)animated {
- [super viewDidAppear:animated];
- NSLog(@"%s", __func__);
-}
-@end
-// console
-- [ViewController viewDidLoad]
-- [ViewController viewWillAppear]
-- [Person dealloc]
-- [ViewController viewDidAppear]
-```
-
-看上去这个释放时机不确定?
-
-在 `viewDidLoad` 方法中,打印 runloop。可以发现:
-
-iOS 在主线程的 Runloop 中注册了2个 Observer
-
-- 第1个 Observer 监听了 `kCFRunLoopEntry` 事件,会调用`objc_autoreleasePoolPush()`
-- 第2个 Observer 监听了2个事件:
- - `kCFRunLoopBeforeWaiting` 事件,会调用`objc_autoreleasePoolPop()` 同时也会调用 `objc_autoreleasePoolPush()`
- - `kCFRunLoopBeforeExit`事件,会调用 `objc_autoreleasePoolPop()`
-
-
-结合 RunLoop 运行图
-
-
-
-- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
-
-- 做一堆其他事情
-
-- 07 在将要休眠的时候先调用 `objc_autoreleasePoolPop`,再调用 `objc_autoreleasePoolPush`
-
-- 等待唤醒做一堆其他事情,回到第二步
-
-- 07 又开始休眠,先调用 `objc_autoreleasePoolPop`,再调用 `objc_autoreleasePoolPush`
-
-- 11 没任务将要休眠,调用 `objc_autoreleasePoolPop`
-
-可以看到 `objc_autoreleasePoolPush`、`objc_autoreleasePoolPop` 成对调用,贯穿 RunLoop
-
-AutoreleasePool 也是 RunLoop 的业务方。iOS GUI 系统下,很多动画、视图渲染、对象的销毁都依赖 RunLoop。
-
-可以回答上面的问题了:系统对 Runloop 添加了观察者,监听 RunLoop 的状态,当将要休眠的时候会触发回调,系统会执行 AutoreleasePool 的 pop 方法,来释放当前一轮 RunLoop 中需要释放的对象。
-
-RunLoop 和 AutoreleasePool 存在几种可能:
-
-- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待没任务的时候,RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。同时执行一次 `objc_autoreleasePoolPush` 方法,然后去休眠。休眠唤醒后继续执行 RunLoop 各个阶段的事情。等没事(Timer、Source)处理的时候,继续 RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。不断循环,依次类推,push 和 pop 成对存在
-- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待需要退出的时候(`kCFRunLoopBeforeExit`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。push 和 pop 成对存在
-- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待没任务的时候,RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。同时执行一次 `objc_autoreleasePoolPush` 方法,然后去休眠。休眠唤醒后继续执行 RunLoop 各个阶段的事情。等没事(Timer、Source)处理的时候,继续 RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。不断循环,最后没事情处理了,等待需要退出的时候(`kCFRunLoopBeforeExit`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。push 和 pop 成对存在在
-
-
-
-
-
### autorelease 实现原理
-`[obj autoreleaae] ` 底层调用 NSObject 的 autorelease 实例方法。
+`[obj autoreleaae]` 底层调用 NSObject 的 autorelease 实例方法。
```objective-c
// NSObject.m
@@ -4105,6 +4173,193 @@ IMP Caching 比其他方法快2倍。
+### autorelease 对象释放时机
+
+> 也就是在聊:autorelease 对象 什么时候调用 release 方法?
+
+Demo1: iOS 项目中,MRC 环境,`viewDidLoad` 方法内的创建的对象什么时候释放?
+
+```objective-c
+@implementation ViewController
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ Person *p = [[Person alloc] init];
+ NSLog(@"%s", __func__);
+}
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:animated];
+ NSLog(@"%s", __func__);
+}
+- (void)viewDidAppear:(BOOL)animated {
+ [super viewDidAppear:animated];
+ NSLog(@"%s", __func__);
+}
+@end
+// console
+- [ViewController viewDidLoad]
+- [ViewController viewWillAppear]
+- [Person dealloc]
+- [ViewController viewDidAppear]
+```
+
+看上去这个释放时机不确定?
+
+在 `viewDidLoad` 方法中,打印 runloop。可以发现:
+
+iOS 在主线程的 Runloop **通用模式(Common Modes)** 中注册 **1 个观察者**,**该观察者的优先级为最高(`order = -2147483647`),确保 AutoreleasePool 操作先于其他回调执行。**其监听以下三个阶段:
+
+- 监听了 `kCFRunLoopEntry`(进入 RunLoop) 事件,会调用`objc_autoreleasePoolPush()`
+- `kCFRunLoopBeforeWaiting`(即将休眠),会调用`objc_autoreleasePoolPop()` 同时也会调用 `objc_autoreleasePoolPush()`
+- `kCFRunLoopExit`(退出 RunLoop),会调用 `objc_autoreleasePoolPop()`
+
+另外,在 `viewDidLoad` 中打印 RunLoop 会发现系统给 RunLoop 昨天添加了2个 Observer,用来在 AutoreleasePool 的场景下使用。
+
+```shell
+//activities=0x1,kCFRunLoopEntry
+{valid=Yes,activities=0x1,repeats=Yes,order=-2147483647,callout=_wrapRunLoopWithAutoreleasePoolHandler(***)}
+//activities=0xa0,kCFRunLoopBeforeWaiting|kCFRunLoopExit
+{valid=Yes,activities=0xa0,repeats=Yes,order=2147483647,callout=_wrapRunLoopWithAutoreleasePoolHandler(***)}
+
+```
+
+看上去这个释放时机不确定?
+
+结合 RunLoop 运行图
+
+
+
+- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
+
+- 做一堆其他事情
+
+- 07 在将要休眠的时候先调用 `objc_autoreleasePoolPop`,再调用 `objc_autoreleasePoolPush`
+
+- 等待唤醒做一堆其他事情,回到第二步
+
+- 07 又开始休眠,先调用 `objc_autoreleasePoolPop`,再调用 `objc_autoreleasePoolPush`
+
+- 11 没任务将要休眠,调用 `objc_autoreleasePoolPop`
+
+可以看到 `objc_autoreleasePoolPush`、`objc_autoreleasePoolPop` 成对调用,贯穿 RunLoop
+
+AutoreleasePool 也是 RunLoop 的业务方。iOS GUI 系统下,很多动画、视图渲染、对象的销毁都依赖 RunLoop。
+
+可以回答上面的问题了:系统对 Runloop 添加了观察者,监听 RunLoop 的状态,当将要休眠的时候会触发回调,系统会执行 AutoreleasePool 的 pop 方法,来释放当前一轮 RunLoop 中需要释放的对象。
+
+RunLoop 和 AutoreleasePool 存在几种可能:
+
+- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待没任务的时候,RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。同时执行一次 `objc_autoreleasePoolPush` 方法,然后去休眠。休眠唤醒后继续执行 RunLoop 各个阶段的事情。等没事(Timer、Source)处理的时候,继续 RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。不断循环,依次类推,push 和 pop 成对存在
+- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待需要退出的时候(`kCFRunLoopBeforeExit`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。push 和 pop 成对存在
+- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待没任务的时候,RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。同时执行一次 `objc_autoreleasePoolPush` 方法,然后去休眠。休眠唤醒后继续执行 RunLoop 各个阶段的事情。等没事(Timer、Source)处理的时候,继续 RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。不断循环,最后没事情处理了,等待需要退出的时候(`kCFRunLoopBeforeExit`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。push 和 pop 成对存在在
+
+
+
+Demo2: iOS 项目中,ARC 环境,`viewDidLoad` 方法内的创建的对象什么时候释放?
+
+```objective-c
+@implementation ViewController
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ Person *p = [[Person alloc] init];
+ NSLog(@"%s", __func__);
+}
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:animated];
+ NSLog(@"%s", __func__);
+}
+- (void)viewDidAppear:(BOOL)animated {
+ [super viewDidAppear:animated];
+ NSLog(@"%s", __func__);
+}
+@end
+// console
+- [ViewController viewDidLoad]
+- [Person dealloc]
+- [ViewController viewWillAppear]
+- [ViewController viewDidAppear]
+```
+
+可以看到 ARC 下 p 对象被立马释放了。也就是说方法内部, ARC 下 LLVM 会自动给生成的对象做了:
+
+- **所有权规则**:通过 `alloc`/`new`/`copy`/`mutableCopy` 创建的对象,调用者默认持有其 **强引用**(即引用计数为 1)。
+- **作用域结束时释放**:若对象未被其他强引用持有,编译器会在作用域结束时插入 `release`,触发引用计数归零,对象被销毁。
+
+
+
+系统回答下:
+
+区分3种情况:
+
+- 含全局 AutoreleasePool 和 RunLoop
+
+ iOS 项目(Mac 项目):`main` 函数中的 `@autoreleasepool` 是全局池,包裹了应用的启动代码(如 `UIApplicationMain`)
+
+ - iOS/Mac 的 **主 RunLoop** 添加了1个观察者用于监听 RunLoop 的状态
+ - 从 `kCFRunLoopEntry` 就会调用 `objc_autoreleasePoolPush` 方法
+ - 在 `kCFRunLoopBeforeWaiting` 事件,会调用`objc_autoreleasePoolPop()` 同时也会调用 `objc_autoreleasePoolPush()`
+ - `kCFRunLoopExit `事件,会调用 `objc_autoreleasePoolPop()`
+
+ ```objc
+ int main(int argc, char * argv[]) {
+ NSString * appDelegateClassName;
+ @autoreleasepool {
+ // Setup code that might create autoreleased objects goes here.
+ appDelegateClassName = NSStringFromClass([AppDelegate class]);
+ }
+ return UIApplicationMain(argc, argv, nil, appDelegateClassName);
+ }
+ ```
+
+- (Mac 终端项目)没有全局 autorelasepool 对象和 RunLoop 处理的,也就是局部 `@autoreleasepool`
+
+ - `@autoreleasepool` 会被 LLVM 编译器转换为 `__AtAutoreleasePool` 结构体
+ - 每个线程维护一个 `AutoreleasePoolPage` 链表,存储 `autorelease` 对象指针。
+ - 开始会自动调用结构体构造方法,调用 `autoreleasepoolPush` 方法,会插入一个 哨兵对象 `POOL_BOUNDARY`, 返回哨兵对象的地址,用 obj 承接
+ - 大括号结束要自动调用 `autoreleasepoolPop(obj)` 方法。其内部会从当前 AutoreleasePoolPage 存储 autorelease 对象的顶部,不断调用 release 方法,直到遇到 obj 地址
+
+- ARC 下
+
+ - 在 ARC 下,编译器会尽可能避免使用 `autorelease`。对于通过 `alloc`/`new`/`copy`/`mutableCopy` 创建的对象,若未被返回或赋值给强引用,编译器会直接插入 `release` 而非 `autorelease`。
+
+
+
+### ARC 时代会自动加 autorelease
+
+> 方法里面的对象,出了方法会立马释放吗?
+
+系统容器类,在使用 block 枚举器的时候,内部会自动创建 AutoreleasePool
+
+```objectivec
+[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ @autoreleasepool {
+ <#statements#>
+ }
+}];
+```
+
+所以,我们老老实实写的 for、while 循环中需要手加局部 AutoreleasePool。推荐使用系统提供的容器类的 block 枚举器。
+
+
+
+Cocoa 框架中,很多类方法用于返回 autorelease 对象。
+
+```objective-c
+// NSArray.m
++ (id) arrayWithCapacity: (NSUInteger)numItems
+{
+ return AUTORELEASE([[self allocWithZone: NSDefaultMallocZone()]
+ initWithCapacity: numItems]);
+}
+
+// NSDictionary.m
++ (id) dictionary
+{
+ return AUTORELEASE([[self allocWithZone: NSDefaultMallocZone()] init]);
+}
+```
+
+
+
### 思考
- 在当次 runloop 将要结束的时候调用 AutoreleasePoolPage::pop()
@@ -4115,6 +4370,618 @@ IMP Caching 比其他方法快2倍。
## 典型的内存问题
+### NSTimer、CSDisplayLink 中的内存泄露
+#### CADisplayLink 内存泄漏
+
+
+
+可以看到 CADisplayLink 和 VC,VC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
+
+
+
+#### NSTimer 内存泄漏
+
+对比实验
+
+NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
+
+Demo 如下:
+
+
+
+但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
+
+
+
+
+
+#### 源码分析
+
+查看 gnu 源码发现
+
+```objective-c
+// NSTimer.m
++ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
+ target: (id)object
+ selector: (SEL)selector
+ userInfo: (id)info
+ repeats: (BOOL)f
+{
+ return AUTORELEASE([[self alloc] initWithFireDate: nil
+ interval: ti
+ target: object
+ selector: selector
+ userInfo: info
+ repeats: f]);
+}
+```
+
+内部调用下面的函数
+
+```objective-c
+- (id) initWithFireDate: (NSDate*)fd
+ interval: (NSTimeInterval)ti
+ target: (id)object
+ selector: (SEL)selector
+ userInfo: (id)info
+ repeats: (BOOL)f
+{
+ if (ti <= 0.0)
+ {
+ ti = 0.0001;
+ }
+ if (fd == nil)
+ {
+ _date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
+ initWithTimeIntervalSinceNow: ti];
+ }
+ else
+ {
+ _date = [fd copyWithZone: NSDefaultMallocZone()];
+ }
+ _target = RETAIN(object);
+ _selector = selector;
+ _info = RETAIN(info);
+ if (f == YES)
+ {
+ _repeats = YES;
+ _interval = ti;
+ }
+ else
+ {
+ _repeats = NO;
+ _interval = 0.0;
+ }
+ return self;
+}
+```
+
+外面的 repeat 根据传递的布尔值,内部赋值给 _repeats 参数。
+
+内部会自动调用 fire
+
+```objective-c
+- (void) fire
+{
+ /* We check that we have not been invalidated before we fire.
+ */
+ if (NO == _invalidated) {
+ if ((id)_block != nil) {
+ CALL_BLOCK(_block, self);
+ } else {
+ id target;
+
+ /* We retain the target so it won't be deallocated while we are using
+ * it (if this timer gets invalidated while we are firing).
+ */
+ target = RETAIN(_target);
+
+ if (_selector == 0) {
+ NS_DURING {
+ [(NSInvocation*)target invoke];
+ }
+ NS_HANDLER {
+ NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
+ @"raised during posting of timer with target %s(%s) "
+ @"and selector '%@'",
+ [localException name], [localException reason],
+ GSClassNameFromObject(target),
+ GSObjCIsInstance(target) ? "instance" : "class",
+ NSStringFromSelector([target selector]));
+ }
+ NS_ENDHANDLER
+ } else {
+ NS_DURING {
+ [target performSelector: _selector withObject: self];
+ }
+ NS_HANDLER {
+ NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
+ @"raised during posting of timer with target %p and "
+ @"selector '%@'",
+ [localException name], [localException reason], target,
+ NSStringFromSelector(_selector));
+ }
+ NS_ENDHANDLER
+ }
+ RELEASE(target);
+ }
+
+ if (_repeats == NO) {
+ [self invalidate];
+ }
+ }
+}
+```
+
+可以看到如果 repeat 为 NO ,则会执行 `[target performSelector: _selector withObject: self];` 调用1次方法,然后会执行 `invalidate` 函数,`invalidate` 实现如下
+
+```objective-c
+- (void) invalidate
+{
+ /* OPENSTEP allows this method to be called multiple times. */
+ _invalidated = YES;
+ if (_target != nil)
+ {
+ DESTROY(_target);
+ }
+ if (_info != nil)
+ {
+ DESTROY(_info);
+ }
+}
+```
+
+可以看到当 target 和 info 存在的时候,都会在 `invalidate` 方法中被 destory,也就是释放。
+
+```
+#define DESTROY(object) ({ \
+ void *__o = (void*)object; \
+ object = nil; \
+ [(id)__o release]; \
+})
+#endif
+```
+
+结论:通过 gnu 可以看到,NSTimer 会对传入的 target、info 对象进行持有强引用,当 repeat 参数为 NO 的时候,则会立马通过 performSelector 的方式执行定时器任务,然后执行 invalidate 方法,对其内部引用的 object、info 进行释放。
+
+
+
+上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。
+
+能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时,NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。
+
+当前的 VC 和 定时器互相引用,造成循环引用。所以思路如下:
+
+如果能在合适的时机打破循环引用就不会有问题了
+
+1. 控制器不再强引用定时器
+2. 定时器不再保留当前的控制器
+
+
+
+#### 解决方案
+
+##### 改用 block 的方式替换 API,不再持有 target
+
+
+
+该种方式,控制器 (self)强引用 timer,timer 强引用 block,block 弱引用 self,3者没有形成环。
+
+
+
+##### 采用系统 NSProxy 代替自定义的中间类
+
+发生循环引用的原因是:
+
+```shell
+VC.timer -> NSTimer
+NSTimer.target -> VC
+```
+
+形成循环引用。所以引入第三者,weak 指针指向 target。效果为:
+
+```shell
+VC.timer -> NSTimer
+NSTimer.target -> TimerTarget
+TimerTarget.target(weak) -> VC
+```
+
+但如果自定义继承自 NSObject 的对象,则不优雅。
+
+```objective-c
+@interface TimerTarget : NSObject
++ (instancetype)proxyWithTarget:(id)target;
+- (void)timerTask;
+@end
+
+
+@interface TimerTarget()
+@property (nonatomic, weak) id target;
+@end
+
+@implementation TimerTarget
+
++ (instancetype)proxyWithTarget:(id)target {
+ TimerTarget *proxy = [[TimerTarget alloc] init];
+ proxy.target = target;
+ return proxy;
+}
+
+- (void)timerTask {
+ [self.target performSelector:@selector(timerTask)];
+}
+@end
+```
+
+存在问题:虽然解决了循环引用的问题,但每次都要创建类,并且实现和定时器方法签名一样的对象方法。
+
+解决方案1:打破循环引用,NSTimer target 自定义。不去实现对象方法,因为最终会走到消息转发流程,调用 `forwardingTargetForSelector` 方法。统一解决未实现的方法。
+
+```objective-c
+- (id)forwardingTargetForSelector:(SEL)aSelector {
+ return self.target;
+}
+```
+
+
+
+解决方案2:使用专门处理消息转发的 NSProxy 类
+
+
+
+##### NSProxy 闪亮登场
+
+
+
+可以看到使用 NSProxy 也可以解决 NSTimer 和 VC 循环引用的问题。但注意:继承自 NSProxy 的类,不能 init。
+
+QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?看上去反而是自定义的 NSObject 使用更简单呀?
+
+答:**NSProxy 效率更高**。NSProxy 的主要作用是为消息转发提供一个通用的接口,是一个继承自 NSObject 的对象,虽然看上去 API 更简单,写法简单,但内部运行的时候还是基于 isa 去查找类对象、元类对象的 cache 中查找,找不到再去 class_rw_t 中查找,找不到再从 superclass 找父类的类对象、元类对象...流程,最后还是找不到,则走 runtime 的动态方法解析、消息转发阶段。
+
+
+
+
+
+看一段神奇的代码
+
+
+
+为什么打印出 `0 1`?
+
+分析:
+
+- p1 是 `TimerProxy` 类,继承于 NSObject 所以就不是 UIViewController 类型。
+
+- p2 是 `MethodProxy` 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 VC 对象,所以上面的 `[p2 isKindOfClass:[self class]]` 等价于 `[self isKindOfClass:[self.class]]`,所以为 1。
+
+也就是说继承自 NSProxy 类的对象,调用方法的时候,会自动走消息转发的流程。
+
+这一点可以查看 GUN 查看下源码印证,代码位于 `NSProxy.m` 中
+
+```objectivec
+- (BOOL) isKindOfClass: (Class)aClass {
+ NSMethodSignature *sig;
+ NSInvocation *inv;
+ BOOL ret;
+ sig = [self methodSignatureForSelector: _cmd];
+ inv = [NSInvocation invocationWithMethodSignature: sig];
+ [inv setSelector: _cmd];
+ [inv setArgument: &aClass atIndex: 2];
+ [self forwardInvocation: inv];
+ [inv getReturnValue: &ret];
+ return ret;
+}
+```
+
+可以看到内部直接调用了消息转发。
+
+
+
+#### GCD Timer
+
+**CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。**
+
+
+
+
+
+假设一个 NSTimer 被加到 RunLoop 开头,NSTimer 执行周期为1s,RunLoop 前面任务繁重,第一次走完一个完整的 RunLoop 需要0.4s,然后从头检测 NSTimer 有没有到时间,发现还没到继续执行 RunLoop 后续逻辑。后面遇到卡顿任务了,第二次 RunLoop 用了0.5s,然后从头检测 NSTimer 有没有到时间,0.4+0.5还不到时间,继续跑,第三次 RunLoop 比较轻松,耗时0.2s,再判断定时器时间有没有到,则此次已经0.4+0.5+0.2=1.1s了,此时 NSTimer 的事件被执行,此时精确度已经不够了(每次 RunLoop 的执行时间不固定)
+
+如果 NSTimer 被添加到了一个特定的模式,当滚动视图时, RunLoop 会切换到 `UITrackingRunLoopMode`,如果 NSTimer 没有被添加到这个模式,它就不会触发。
+
+当 RunLoop 没有事件可处理时,它会进入休眠状态。这意味着即使定时器的时间间隔到了,但 `RunLoop` 可能还在休眠中,因此定时器不会立即触发。
+
+
+
+网上有些针对 FPS 帧率的检测是基于 CADisplayLink 计算的,所以这种方案不准确。具体可以查看文章:[带你打造一套 APM 监控系统](./1.74.md)
+
+
+
+GCD 的 timer 会更加准时,底层依赖系统内核,不依赖 RunLoop。
+
+```objectivec
+@property (nonatomic, strong) dispatch_source_t timer;
+// 创建队列
+dispatch_queue_t queue = dispatch_get_main_queue();
+// 创建 GCD 定时器
+dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
+uint64_t start = 2.0;
+uint64_t interval = 1.0;
+// 设置定时器周期
+dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
+// 设置定时器任务
+dispatch_source_set_event_handler(timerSource, ^{
+ NSLog(@"tick tock");
+});
+// 启动定时器
+dispatch_resume(timerSource);
+self.timer = timerSource;
+```
+
+为什么 GCD timer 会更准确?因为普通定时器运行依赖 RunLoop,RunLoop 一个运行周期内的任务繁忙程度是不确定的。当某次任务繁重,那么定时器调度就不准时。
+
+GCD timer 不依赖 RunLoop,系统底层驱动,所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动,RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
+
+
+
+#### 高精度定时器封装
+
+项目中经常使用定时器,普通定时器存在精度丢失的问题、循环引用的问题,为了使用方法我们封装一个定时器
+
+```objectivec
+#import
+
+@interface PreciousTimer : NSObject
+
++ (NSString *)execTask:(void(^)(void))task
+ start:(NSTimeInterval)start
+ interval:(NSTimeInterval)interval
+ repeats:(BOOL)repeats
+ async:(BOOL)async;
+
++ (NSString *)execTask:(id)target
+ selector:(SEL)selector
+ start:(NSTimeInterval)start
+ interval:(NSTimeInterval)interval
+ repeats:(BOOL)repeats
+ async:(BOOL)async;
+
++ (void)cancelTask:(NSString *)name;
+
+@end
+
+#import "PreciousTimer.h"
+
+@implementation PreciousTimer
+
+static NSMutableDictionary *timers_;
+dispatch_semaphore_t semaphore_;
++ (void)initialize
+{
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ timers_ = [NSMutableDictionary dictionary];
+ semaphore_ = dispatch_semaphore_create(1);
+ });
+}
+
++ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
+{
+ if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
+
+ // 队列
+ dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
+
+ // 创建定时器
+ dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
+
+ // 设置时间
+ dispatch_source_set_timer(timer,
+ dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
+ interval * NSEC_PER_SEC, 0);
+
+
+ dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
+ // 定时器的唯一标识
+ NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
+ // 存放到字典中
+ timers_[name] = timer;
+ dispatch_semaphore_signal(semaphore_);
+
+ // 设置回调
+ dispatch_source_set_event_handler(timer, ^{
+ task();
+
+ if (!repeats) { // 不重复的任务
+ [self cancelTask:name];
+ }
+ });
+
+ // 启动定时器
+ dispatch_resume(timer);
+
+ return name;
+}
+
++ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
+{
+ if (!target || !selector) return nil;
+
+ return [self execTask:^{
+ if ([target respondsToSelector:selector]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [target performSelector:selector];
+#pragma clang diagnostic pop
+ }
+ } start:start interval:interval repeats:repeats async:async];
+}
+
++ (void)cancelTask:(NSString *)name
+{
+ if (name.length == 0) return;
+
+ dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
+
+ dispatch_source_t timer = timers_[name];
+ if (timer) {
+ dispatch_source_cancel(timer);
+ [timers_ removeObjectForKey:name];
+ }
+
+ dispatch_semaphore_signal(semaphore_);
+}
+
+@end
+```
+
+使用 Demo
+
+```objectivec
+- (void)viewDidLoad{
+ [super viewDidLoad];
+ NSLog(@"now");
+ self.timerId = [PreciousTimer execTask:^{
+ NSLog(@"tick tock %@", [NSThread currentThread]);
+ } start:2 interval:1 repeats:YES async:YES];
+}
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
+{
+ [PreciousTimer cancelTask:self.timerId];
+}
+```
+
+说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
+
+
+
+
+
+#### 采用 Block 的形式为 NSTimer 增加分类
+
+```objectivec
+//.h文件
+#import
+
+@interface NSTimer (UnRetain)
++ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
+ repeats:(BOOL)repeats
+ block:(void(^)(NSTimer *timer))block;
+@end
+
+//.m文件
+#import "NSTimer+SGLUnRetain.h"
+
+@implementation NSTimer (SGLUnRetain)
+
++ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
+
+ return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(lbp_blcokInvoke:) userInfo:[block copy] repeats:repeats];
+}
+
++ (void)lbp_blcokInvoke:(NSTimer *)timer {
+
+ void (^block)(NSTimer *timer) = timer.userInfo;
+
+ if (block) {
+ block(timer);
+ }
+}
+@end
+
+//控制器.m
+
+#import "ViewController.h"
+#import "NSTimer+UnRetain.h"
+
+//定义了一个__weak的self_weak_变量
+#define weakifySelf \
+__weak __typeof(&*self)weakSelf = self;
+
+//局域定义了一个__strong的self指针指向self_weak
+#define strongifySelf \
+__strong __typeof(&*weakSelf)self = weakSelf;
+
+@interface ViewController ()
+
+@property(nonatomic, strong) NSTimer *timer;
+
+@end
+
+@implementation ViewController
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ __block NSInteger i = 0;
+ weakifySelf
+ self.timer = [NSTimer lbp_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
+ strongifySelf
+ [self p_doSomething];
+ NSLog(@"----------------");
+ if (i++ > 10) {
+ [timer invalidate];
+ }
+ }];
+}
+
+- (void)p_doSomething {
+
+}
+
+- (void)dealloc {
+ // 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档)
+ [self.timer invalidate];
+}
+@end
+```
+
+上面的方法之所以能解决内存泄漏的问题,关键在于把保留转移到了定时器的类对象身上,这样就避免了实例对象被保留。
+
+当我们谈到循环引用时,其实是指实例对象间的引用关系。类对象在 App 杀死时才会释放,在实际开发中几乎不用关注类对象的内存管理。下面的代码摘自苹果开源的 NSObject.mm 文件,从中可以看出,对于类对象,并不需要像实例对象那样进行内存管理。
+
+```objective-c
++ (id)retain {
+ return (id)self;
+}
+
+// Replaced by ObjectAlloc
+- (id)retain {
+ return ((id)self)->rootRetain();
+}
+
++ (oneway void)release {
+}
+
+// Replaced by ObjectAlloc
+- (oneway void)release {
+ ((id)self)->rootRelease();
+}
+
++ (id)autorelease {
+ return (id)self;
+}
+
+// Replaced by ObjectAlloc
+- (id)autorelease {
+ return ((id)self)->rootAutorelease();
+}
+
++ (NSUInteger)retainCount {
+ return ULONG_MAX;
+}
+
+- (NSUInteger)retainCount {
+ return ((id)self)->rootRetainCount();
+}
+```
+
+iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
+
+## 检测
+
+根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)
+
### 使用NSArray 保存weak对象,会有什么问题?
Foundation 中数组在元素被添加的时候(这里的数组 指平常使用的 NSArray 和 NSMutableArray )会强引用持有,就算使用 `__weak` 修饰也没有用,导致一些奇特的内存泄漏和循环引用问题。
@@ -4168,11 +5035,11 @@ NSHashTable *hashTable = [NSHashTable weakObjectsHashTable];
再来2个实验:
-
+
分析:可以看到 p1 的地址,在刚初始化后,和当作 key 加入到 NSDictionary 后,地址发生了变化。对 p1 执行了 copy 操作。
-
+
分析:
@@ -4223,7 +5090,7 @@ NSHashTable *hashTable = [NSHashTable weakObjectsHashTable];
这段代码运行会 crash,信息如下
-
+
原因是 NSError 构造方法内部会加 autorelease。源码如下
@@ -4286,7 +5153,7 @@ MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing
我写了个僵尸对象检测工具,效果如下
-
+
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)
diff --git a/Chapter1 - iOS/1.45.md b/Chapter1 - iOS/1.45.md
index 94fce65..e69de29 100644
--- a/Chapter1 - iOS/1.45.md
+++ b/Chapter1 - iOS/1.45.md
@@ -1,571 +0,0 @@
-# NSTimer、CSDisplayLink 中的内存泄露
-
-
-
-## CADisplayLink 内存泄漏
-
-
-
-可以看到 CADisplayLink 和 VC,VC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
-
-
-
-## NSTimer 内存泄漏
-
-### 对比实验
-
-NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
-
-Demo 如下:
-
-
-
-但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
-
-
-
-
-
-### 源码分析
-
-查看 gnu 源码发现
-
-```objective-c
-// NSTimer.m
-+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
- target: (id)object
- selector: (SEL)selector
- userInfo: (id)info
- repeats: (BOOL)f
-{
- return AUTORELEASE([[self alloc] initWithFireDate: nil
- interval: ti
- target: object
- selector: selector
- userInfo: info
- repeats: f]);
-}
-```
-
-内部调用下面的函数
-
-```objective-c
-- (id) initWithFireDate: (NSDate*)fd
- interval: (NSTimeInterval)ti
- target: (id)object
- selector: (SEL)selector
- userInfo: (id)info
- repeats: (BOOL)f
-{
- if (ti <= 0.0)
- {
- ti = 0.0001;
- }
- if (fd == nil)
- {
- _date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
- initWithTimeIntervalSinceNow: ti];
- }
- else
- {
- _date = [fd copyWithZone: NSDefaultMallocZone()];
- }
- _target = RETAIN(object);
- _selector = selector;
- _info = RETAIN(info);
- if (f == YES)
- {
- _repeats = YES;
- _interval = ti;
- }
- else
- {
- _repeats = NO;
- _interval = 0.0;
- }
- return self;
-}
-```
-
-外面的 repeat 根据传递的布尔值,内部赋值给 _repeats 参数。
-
-内部会自动调用 fire
-
-```objective-c
-- (void) fire
-{
- /* We check that we have not been invalidated before we fire.
- */
- if (NO == _invalidated) {
- if ((id)_block != nil) {
- CALL_BLOCK(_block, self);
- } else {
- id target;
-
- /* We retain the target so it won't be deallocated while we are using
- * it (if this timer gets invalidated while we are firing).
- */
- target = RETAIN(_target);
-
- if (_selector == 0) {
- NS_DURING {
- [(NSInvocation*)target invoke];
- }
- NS_HANDLER {
- NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
- @"raised during posting of timer with target %s(%s) "
- @"and selector '%@'",
- [localException name], [localException reason],
- GSClassNameFromObject(target),
- GSObjCIsInstance(target) ? "instance" : "class",
- NSStringFromSelector([target selector]));
- }
- NS_ENDHANDLER
- } else {
- NS_DURING {
- [target performSelector: _selector withObject: self];
- }
- NS_HANDLER {
- NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
- @"raised during posting of timer with target %p and "
- @"selector '%@'",
- [localException name], [localException reason], target,
- NSStringFromSelector(_selector));
- }
- NS_ENDHANDLER
- }
- RELEASE(target);
- }
-
- if (_repeats == NO) {
- [self invalidate];
- }
- }
-}
-```
-
-可以看到如果 repeat 为 NO ,则会执行 `[target performSelector: _selector withObject: self];` 调用1次方法,然后会执行 `invalidate` 函数,`invalidate` 实现如下
-
-```objective-c
-- (void) invalidate
-{
- /* OPENSTEP allows this method to be called multiple times. */
- _invalidated = YES;
- if (_target != nil)
- {
- DESTROY(_target);
- }
- if (_info != nil)
- {
- DESTROY(_info);
- }
-}
-```
-
-可以看到当 target 和 info 存在的时候,都会在 `invalidate` 方法中被 destory,也就是释放。
-
-```
-#define DESTROY(object) ({ \
- void *__o = (void*)object; \
- object = nil; \
- [(id)__o release]; \
-})
-#endif
-```
-
-结论:通过 gnu 可以看到,NSTimer 会对传入的 target、info 对象进行持有强引用,当 repeat 参数为 NO 的时候,则会立马通过 performSelector 的方式执行定时器任务,然后执行 invalidate 方法,对其内部引用的 object、info 进行释放。
-
-
-
-上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。
-
-能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时,NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。
-
-当前的 VC 和 定时器互相引用,造成循环引用。所以思路如下:
-
-如果能在合适的时机打破循环引用就不会有问题了
-
-1. 控制器不再强引用定时器
-2. 定时器不再保留当前的控制器
-
-
-
-## 解决方案
-
-### 改用 block 的方式替换 API,不再持有 target
-
-
-
-该种方式,控制器 (self)强引用 timer,timer 强引用 block,block 弱引用 self,3者没有形成环。
-
-
-
-### 采用系统 NSProxy 代替自定义的中间类
-
-
-
-注意:继承自 NSProxy 的类,不能 init。
-
-
-
-QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?看上去反而是自定义的 NSObject 使用更简单呀?
-
-答:**NSProxy 效率更高**。NSProxy 的主要作用是为消息转发提供一个通用的接口,是一个继承自 NSObject 的对象,虽然看上去 API 更简单,写法简单,但内部运行的时候还是基于 isa 去查找类对象、元类对象的 cache 中查找,找不到再去 class_rw_t 中查找,找不到再从 superclass 找父类的类对象、元类对象...流程,最后还是找不到,则走 runtime 的动态方法解析、消息转发阶段。
-
-
-
-
-
-看一段神奇的代码
-
-
-
-为什么打印出 `0 1`?
-
-分析:
-
-- p1 是 `TimerProxy` 类,继承于 NSObject 所以就不是 UIViewController 类型。
-
-- p2 是 `MethodProxy` 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 self,所以上面的 `[p2 isKindOfClass:[self class]]` 等价于 `[self isKindOfClass:[self.class]]`,所以为 1。
-
-也就是说继承自 NSProxy 类的对象,调用方法的时候,会自动走消息转发的流程。
-
-这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
-
-```objectivec
-- (BOOL) isKindOfClass: (Class)aClass
-{
- NSMethodSignature *sig;
- NSInvocation *inv;
- BOOL ret;
- sig = [self methodSignatureForSelector: _cmd];
- inv = [NSInvocation invocationWithMethodSignature: sig];
- [inv setSelector: _cmd];
- [inv setArgument: &aClass atIndex: 2];
- [self forwardInvocation: inv];
- [inv getReturnValue: &ret];
- return ret;
-}
-```
-
-可以看到内部直接调用了消息转发。
-
-
-
-
-
-### GCD Timer
-
-**CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。**
-
-
-
-
-
-假设一个 NSTimer 被加到 RunLoop 开头,NSTimer 执行周期为1s,RunLoop 前面任务繁重,第一次走完一个完整的 RunLoop 需要0.4s,然后从头检测 NSTimer 有没有到时间,发现还没到继续执行 RunLoop 后续逻辑。后面遇到卡顿任务了,第二次 RunLoop 用了0.5s,然后从头检测 NSTimer 有没有到时间,0.4+0.5还不到时间,继续跑,第三次 RunLoop 比较轻松,耗时0.2s,再判断定时器时间有没有到,则此次已经0.4+0.5+0.2=1.1s了,此时 NSTimer 的事件被执行,此时精确度已经不够了(每次 RunLoop 的执行时间不固定)
-
-如果 NSTimer 被添加到了一个特定的模式,当滚动视图时, RunLoop 会切换到 `UITrackingRunLoopMode`,如果 NSTimer 没有被添加到这个模式,它就不会触发。
-
-当 RunLoop 没有事件可处理时,它会进入休眠状态。这意味着即使定时器的时间间隔到了,但 `RunLoop` 可能还在休眠中,因此定时器不会立即触发。
-
-
-
-网上有些针对 FPS 帧率的检测是基于 CADisplayLink 计算的,所以这种方案不准确。具体可以查看文章:[带你打造一套 APM 监控系统](./1.74.md)
-
-
-
-GCD 的 timer 会更加准时,底层依赖系统内核,不依赖 RunLoop。
-
-```objectivec
-@property (nonatomic, strong) dispatch_source_t timer;
-// 创建队列
-dispatch_queue_t queue = dispatch_get_main_queue();
-// 创建 GCD 定时器
-dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
-uint64_t start = 2.0;
-uint64_t interval = 1.0;
-// 设置定时器周期
-dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
-// 设置定时器任务
-dispatch_source_set_event_handler(timerSource, ^{
- NSLog(@"tick tock");
-});
-// 启动定时器
-dispatch_resume(timerSource);
-self.timer = timerSource;
-```
-
-为什么 GCD timer 会更准确?因为普通定时器运行依赖 RunLoop,RunLoop 一个运行周期内的任务繁忙程度是不确定的。当某次任务繁重,那么定时器调度就不准时。
-
-GCD timer 不依赖 RunLoop,系统底层驱动,所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动,RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
-
-
-
-### 打破循环引用,NSTimer target 自定义
-
-
-
-
-
-
-
-
-
-### 高精度定时器封装
-
-项目中经常使用定时器,普通定时器存在精度丢失的问题、循环引用的问题,为了使用方法我们封装一个定时器
-
-```objectivec
-#import
-
-@interface PreciousTimer : NSObject
-
-+ (NSString *)execTask:(void(^)(void))task
- start:(NSTimeInterval)start
- interval:(NSTimeInterval)interval
- repeats:(BOOL)repeats
- async:(BOOL)async;
-
-+ (NSString *)execTask:(id)target
- selector:(SEL)selector
- start:(NSTimeInterval)start
- interval:(NSTimeInterval)interval
- repeats:(BOOL)repeats
- async:(BOOL)async;
-
-+ (void)cancelTask:(NSString *)name;
-
-@end
-
-#import "PreciousTimer.h"
-
-@implementation PreciousTimer
-
-static NSMutableDictionary *timers_;
-dispatch_semaphore_t semaphore_;
-+ (void)initialize
-{
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- timers_ = [NSMutableDictionary dictionary];
- semaphore_ = dispatch_semaphore_create(1);
- });
-}
-
-+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
-{
- if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
-
- // 队列
- dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
-
- // 创建定时器
- dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
-
- // 设置时间
- dispatch_source_set_timer(timer,
- dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
- interval * NSEC_PER_SEC, 0);
-
-
- dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
- // 定时器的唯一标识
- NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
- // 存放到字典中
- timers_[name] = timer;
- dispatch_semaphore_signal(semaphore_);
-
- // 设置回调
- dispatch_source_set_event_handler(timer, ^{
- task();
-
- if (!repeats) { // 不重复的任务
- [self cancelTask:name];
- }
- });
-
- // 启动定时器
- dispatch_resume(timer);
-
- return name;
-}
-
-+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
-{
- if (!target || !selector) return nil;
-
- return [self execTask:^{
- if ([target respondsToSelector:selector]) {
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
- [target performSelector:selector];
-#pragma clang diagnostic pop
- }
- } start:start interval:interval repeats:repeats async:async];
-}
-
-+ (void)cancelTask:(NSString *)name
-{
- if (name.length == 0) return;
-
- dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
-
- dispatch_source_t timer = timers_[name];
- if (timer) {
- dispatch_source_cancel(timer);
- [timers_ removeObjectForKey:name];
- }
-
- dispatch_semaphore_signal(semaphore_);
-}
-
-@end
-```
-
-使用 Demo
-
-```objectivec
-- (void)viewDidLoad{
- [super viewDidLoad];
- NSLog(@"now");
- self.timerId = [PreciousTimer execTask:^{
- NSLog(@"tick tock %@", [NSThread currentThread]);
- } start:2 interval:1 repeats:YES async:YES];
-}
-- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
-{
- [PreciousTimer cancelTask:self.timerId];
-}
-```
-
-说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
-
-
-
-
-
-### 采用 Block 的形式为 NSTimer 增加分类
-
-```objectivec
-//.h文件
-#import
-
-@interface NSTimer (UnRetain)
-+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
- repeats:(BOOL)repeats
- block:(void(^)(NSTimer *timer))block;
-@end
-
-//.m文件
-#import "NSTimer+SGLUnRetain.h"
-
-@implementation NSTimer (SGLUnRetain)
-
-+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
-
- return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(lbp_blcokInvoke:) userInfo:[block copy] repeats:repeats];
-}
-
-+ (void)lbp_blcokInvoke:(NSTimer *)timer {
-
- void (^block)(NSTimer *timer) = timer.userInfo;
-
- if (block) {
- block(timer);
- }
-}
-@end
-
-//控制器.m
-
-#import "ViewController.h"
-#import "NSTimer+UnRetain.h"
-
-//定义了一个__weak的self_weak_变量
-#define weakifySelf \
-__weak __typeof(&*self)weakSelf = self;
-
-//局域定义了一个__strong的self指针指向self_weak
-#define strongifySelf \
-__strong __typeof(&*weakSelf)self = weakSelf;
-
-@interface ViewController ()
-
-@property(nonatomic, strong) NSTimer *timer;
-
-@end
-
-@implementation ViewController
-- (void)viewDidLoad {
- [super viewDidLoad];
-
- __block NSInteger i = 0;
- weakifySelf
- self.timer = [NSTimer lbp_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
- strongifySelf
- [self p_doSomething];
- NSLog(@"----------------");
- if (i++ > 10) {
- [timer invalidate];
- }
- }];
-}
-
-- (void)p_doSomething {
-
-}
-
-- (void)dealloc {
- // 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档)
- [self.timer invalidate];
-}
-@end
-```
-
-上面的方法之所以能解决内存泄漏的问题,关键在于把保留转移到了定时器的类对象身上,这样就避免了实例对象被保留。
-
-当我们谈到循环引用时,其实是指实例对象间的引用关系。类对象在 App 杀死时才会释放,在实际开发中几乎不用关注类对象的内存管理。下面的代码摘自苹果开源的 NSObject.mm 文件,从中可以看出,对于类对象,并不需要像实例对象那样进行内存管理。
-
-```objective-c
-+ (id)retain {
- return (id)self;
-}
-
-// Replaced by ObjectAlloc
-- (id)retain {
- return ((id)self)->rootRetain();
-}
-
-+ (oneway void)release {
-}
-
-// Replaced by ObjectAlloc
-- (oneway void)release {
- ((id)self)->rootRelease();
-}
-
-+ (id)autorelease {
- return (id)self;
-}
-
-// Replaced by ObjectAlloc
-- (id)autorelease {
- return ((id)self)->rootAutorelease();
-}
-
-+ (NSUInteger)retainCount {
- return ULONG_MAX;
-}
-
-- (NSUInteger)retainCount {
- return ((id)self)->rootRetainCount();
-}
-```
-
-iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
-
-
-
-## 检测
-
-根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.46.md b/Chapter1 - iOS/1.46.md
index 3ec368f..68b19b0 100644
--- a/Chapter1 - iOS/1.46.md
+++ b/Chapter1 - iOS/1.46.md
@@ -2,16 +2,464 @@
> KVO 的实现原理是什么?如何手动触发 KVO?本文来探索下 iOS 中 KVO 底层细节
+## 一、KVO 的高级用法
+
+### 1. KVO 居然还有触发模式的说法?
+
+#### 触发模式
+
+**KVO 的触发分为`自动触发模式`和`手动触发模式`2种**。通常我们使用的都是自动通知,注册观察者之后,当条件触发的时候会自动调用 `-(void)observeValueForKeyPath`。
+
+如果需要实现手动通知,我们需要使用 `+automaticallyNotifiesObserversForKey` 方法返回 NO 即可。此时即使被观察对象的属性值发生了变化,也不会出发观察者的 `- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context` 方法。
+
+```Objective-c
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ return NO;
+}
+```
+
+如何手动触发(设置了 `automaticallyNotifiesObserversForKey` 返回 NO 的前提下)KVO ?
+
+**手动调用 `willChangeValueForKey`、 `didChangeValueForKey`**
+
+在需要触发 KVO 的地方,先调用 `willChangeValueForKey`,然后更改被观察对象的属性值,最后调用 `didChangeValueForKey` 方法。
+
+
-## 底层实现分析
-Demo1:创建 Person 类,点击事件里触发属性值的改变。
+QA:在需要触发 KVO 的地方,只调用`willChangeValueForKey`、 `didChangeValueForKey`,不修改被观察对象的属性值,KVO 会触发吗?
-
+会调用,只是值没有变化。
+
+系统默认行为:当没有显式设置新值时,KVO 会尝试通过属性的访问器方法(`getter`)获取当前值作为 `newValue`。此时 `age` 未初始化,默认为 `0`
+
+说明,属性值改不改变不影响 KVO 的触发,只与 `willChangeValueForKey`、 `didChangeValueForKey` 的调用与否有关。
+
+- **禁用自动 KVO 通知**:`+automaticallyNotifiesObserversForKey:返回 NO` 仅禁止属性赋值自动触发 KVO,**手动调用 `willChange/didChange` 仍会触发回调**。
+- **`change` 字典内容**:依赖新旧值的显式记录,若未正确设置属性值,KVO 会通过 `valueForKey:` 获取当前值。
+- **最佳实践**:在手动触发 KVO 时,始终在 `willChange` 和 `didChange` 之间修改属性值,并确保新旧值能被正确捕获
+#### 使用场景
+
+##### 批量属性修改后一次性通知
+
+当需要同时修改多个关联属性时,自动触发 KVO 会导致多次回调,而手动触发可以合并为一次通知,提升性能。
+
+Demo: 图形对象的 `frame` 更新
+
+假设一个 `Rectangle` 对象有 `x`、`y`、`width`、`height` 四个属性,修改 `frame` 时需要同时更新这四个属性:
+
+```objective-c
+// Rectangle.m
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ // 禁用自动触发
+ if ([key isEqualToString:@"x"] ||
+ [key isEqualToString:@"y"] ||
+ [key isEqualToString:@"width"] ||
+ [key isEqualToString:@"height"]) {
+ return NO;
+ }
+ return [super automaticallyNotifiesObserversForKey:key];
+}
+
+- (void)setFrameWithX:(CGFloat)x y:(CGFloat)y width:(CGFloat)width height:(CGFloat)height {
+ // 手动触发 KVO
+ [self willChangeValueForKey:@"x"];
+ [self willChangeValueForKey:@"y"];
+ [self willChangeValueForKey:@"width"];
+ [self willChangeValueForKey:@"height"];
+
+ _x = x;
+ _y = y;
+ _width = width;
+ _height = height;
+
+ [self didChangeValueForKey:@"x"];
+ [self didChangeValueForKey:@"y"];
+ [self didChangeValueForKey:@"width"];
+ [self didChangeValueForKey:@"height"];
+}
+```
+
+##### **属性之间存在依赖关系**
+
+当一个属性的值依赖于其他属性时,需要手动触发其 KVO 通知。类似于 Vue 的计算属性一样。
+
+Demo: `fullName` 依赖 `firstName` 和 `lastName`
+
+```objective-c
+// Person.m
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ // 禁用自动触发
+ if ([key isEqualToString:@"fullName"]) {
+ return NO;
+ }
+ return [super automaticallyNotifiesObserversForKey:key];
+}
+
+- (void)setFirstName:(NSString *)firstName {
+ [self willChangeValueForKey:@"fullName"];
+ _firstName = firstName;
+ [self didChangeValueForKey:@"fullName"];
+}
+
+- (void)setLastName:(NSString *)lastName {
+ [self willChangeValueForKey:@"fullName"];
+ _lastName = lastName;
+ [self didChangeValueForKey:@"fullName"];
+}
+
+- (NSString *)fullName {
+ return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
+}
+```
+
+##### **属性变更需要条件触发**
+
+当属性变更需要满足特定条件时才触发通知。
+
+Demo: 数值范围校验
+
+```objective-c
+// TemperatureSensor.m
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ if ([key isEqualToString:@"temperature"]) {
+ return NO; // 禁用自动触发
+ }
+ return [super automaticallyNotifiesObserversForKey:key];
+}
+
+- (void)setTemperature:(CGFloat)temperature {
+ // 温度变化超过 0.5 度才触发通知
+ if (fabs(temperature - _temperature) > 0.5) {
+ [self willChangeValueForKey:@"temperature"];
+ _temperature = temperature;
+ [self didChangeValueForKey:@"temperature"];
+ }
+}
+```
+
+##### 避免循环触发
+
+当属性 A 的变更会触发属性 B 的变更,而属性 B 的变更又可能触发属性 A 的变更时,需要手动控制。
+
+Demo: 双向关联对象
+
+```objective-c
+// Node.m (双向链表节点)
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ if ([key isEqualToString:@"next"]) {
+ return NO; // 禁用自动触发
+ }
+ return [super automaticallyNotifiesObserversForKey:key];
+}
+
+- (void)setNext:(Node *)next {
+ // 手动解除旧节点的反向关联
+ if (_next != next) {
+ [self willChangeValueForKey:@"next"];
+ _next.previous = nil; // 可能触发 previous 的 KVO
+ _next = next;
+ _next.previous = self; // 可能触发 next 的 KVO
+ [self didChangeValueForKey:@"next"];
+ }
+}
+```
+
+##### **性能优化**
+
+当属性频繁变更但无需立即通知观察者时,手动触发可以合并多次变更为一次通知。
+
+Demo: 实时数据流处理
+
+```objective-c
+// DataStreamProcessor.m
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ if ([key isEqualToString:@"buffer"]) {
+ return NO; // 禁用自动触发
+ }
+ return [super automaticallyNotifiesObserversForKey:key];
+}
+
+- (void)appendData:(NSData *)data {
+ // 数据积累到阈值后一次性触发通知
+ [self.internalBuffer appendData:data];
+ if (self.internalBuffer.length >= 1024) {
+ [self willChangeValueForKey:@"buffer"];
+ _buffer = [self.internalBuffer copy];
+ [self.internalBuffer setLength:0];
+ [self didChangeValueForKey:@"buffer"];
+ }
+}
+```
+
+注意:
+
+1. **成对调用**:`willChange` 和 `didChange` 必须成对出现。
+2. **线程安全**:确保 KVO 方法在同一线程调用。
+3. **性能权衡**:手动触发会增加代码复杂度,需根据场景选择。
+
+
+
+### 2. KVO 如何优雅监听 property 嵌套的情况
+
+假设 Person 对象有一个 dog 对象作为属性。 Dog 对象拥有 age、name 2个属性。现在想实现对 person 对象的 dog 监听,当 dog 对象的 name、age 任何一个属性改变时,都可以监听到改变,该怎么实现呢?
+
+版本1:不优雅的方案。手动依次将 dog 的每个属性,都被 Person 的观察者监听。
+
+`[_p1 addObserver:self forKeyPath:@"_dog.age" options:(NSKeyValueObservingOptionNew) context:nil];`
+
+代码如下
+
+
+
+看上去很麻烦,有没有优雅点的方案?
+
+版本2: 利用系统 API `+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0))`
+
+`+keyPathsForValuesAffectingValueForKey:` 是 KVO(Key-Value Observing)中用于 **声明属性依赖关系** 的方法,它允许开发者显式指定某个属性的值变化依赖于其他属性(或其键路径)。通过该方法,系统会自动监听依赖属性的变化,并在这些依赖属性变化时触发目标属性的 KVO 通知。
+
+实现如下:
+
+
+
+注意:在对 Person 对象的 `dog` 属性进行监听后,Person 内部需要实现 `+`keyPathsForValuesAffectingValueForKey 方法,判断 key 为 `@"dog"` 后,想监听 dog 的哪个属性变化就通知 Person 对象的观察者收到响应的话就写上去。但注意绿色框里面,set 添加的内容,必须换个名字,比如 `@"_dog.name"`,不能是 `@"dog.name"`。这会导致循环依赖或逻辑矛盾
+
+
+
+#### 使用场景
+
+##### 计算属性
+
+类似 Vue 的 Computed Property,当一个属性的值是基于其他属性计算得出时,可通过该方法声明依赖关系,确保计算属性的 KVO 通知自动触发。
+
+Demo:`fullName` 依赖 `firstName` 和 `lastName`
+
+```objective-c
+// Person.h
+@property (nonatomic, copy) NSString *firstName;
+@property (nonatomic, copy) NSString *lastName;
+@property (nonatomic, readonly) NSString *fullName; // 计算属性
+
+// Person.m
++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
+ NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
+ if ([key isEqualToString:@"fullName"]) {
+ // 声明 fullName 依赖 firstName 和 lastName
+ keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"firstName", @"lastName"]];
+ }
+ return keyPaths;
+}
+
+- (NSString *)fullName {
+ return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
+}
+```
+
+##### **聚合属性(Aggregated Property)**
+
+当某个属性是多个子属性的聚合结果(如总和、平均值),可通过该方法声明依赖关系。
+
+Demo:`totalPrice` 依赖多个商品项的 `price` 和 `quantity`
+
+```objective-c
+// Order.h
+@property (nonatomic, strong) NSArray *items;
+@property (nonatomic, readonly) CGFloat totalPrice; // 聚合属性
+
+// Order.m
++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
+ NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
+ if ([key isEqualToString:@"totalPrice"]) {
+ // 声明 totalPrice 依赖所有 items 的 price 和 quantity
+ NSMutableArray *dependencies = [NSMutableArray array];
+ for (OrderItem *item in self.items) {
+ [dependencies addObject:[NSString stringWithFormat:@"items.%@.price", item.itemId]];
+ [dependencies addObject:[NSString stringWithFormat:@"items.%@.quantity", item.itemId]];
+ }
+ keyPaths = [keyPaths setByAddingObjectsFromArray:dependencies];
+ }
+ return keyPaths;
+}
+
+- (CGFloat)totalPrice {
+ CGFloat sum = 0;
+ for (OrderItem *item in self.items) {
+ sum += item.price * item.quantity;
+ }
+ return sum;
+}
+```
+
+
+
+##### **跨对象依赖(Cross-Object Dependency)**
+
+当属性依赖于其他对象的属性时,可通过键路径声明跨对象依赖。
+
+Demo:Person 的 dog 属性本身就是一个对象,对象的值改变后通知观察者
+
+```objective-c
+// Person.h
+@property (nonatomic, strong) Dog *dog;
+// Person.m
++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
+ NSSet *keyPathSet = [super keyPathsForValuesAffectingValueForKey:key];
+ if ([key isEqualToString:@"dog"]) {
+ keyPathSet = [[NSSet alloc] initWithObjects:@"_dog.name", @"_dog.age", nil];
+ }
+ return keyPathSet;
+}
+
+// VC
+[_p1 addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
+self.p1.dog.age = 1;
+self.p1.dog.name = @"Lucy";
+```
+
+
+
+### 3. KVO 如何对容器类进行监听
+
+可以在下面的 Demo1 中可以看到 KVO 无法直接对数组进行 KVO 监听。但系统为了方便,也提供了容器类的接口,比如针对 `NSMutableArray` 系统就提供了 `mutableArrayValueForKey` 接口。
+
+
+
+那么虽然功能实现了,我们可以想想,这个接口背后做了哪些事?
+
+- NSMutableArray 实例调用 `mutableArrayValueForKey` 方法,即利用 Runtime 创建一个继承自 NSMutableArray 的子类
+
+- 因为需要对 NSMutableArray 容器类进行监听,所以 NSMutableArray 方法调用都需要可以观察到。所以需要对这些方法进行重写,内部需要:先调用 `willChangeValueForKey` -> 再调用父类方法实现 -> 最后调用 `didChangeValueForKey`
+
+ ```objective-c
+ - (void)addObject:(ObjectType)anObject;
+ - (void)insertObject:(ObjectType)anObject atIndex:(NSUInteger)index;
+ - (void)removeLastObject;
+ - (void)removeObjectAtIndex:(NSUInteger)index;
+ - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(ObjectType)anObject;
+ ```
+
+- OC 调用方法的本质就是利用对象的 isa 找到类对象,然后查找方法列表中的实现。为了调用方法可以走到重写的方法里,所以需要修改当前的 NSMutableArray 的实例对象的 isa 为新创建的类
+
+- 如何触发?为 NSObject 添加分类即可。只要对集合类进行观察,就生成子类,修改 isa。拦截容器类的方法。
+
+
+
+### 4. KVO 的问题与改进
+
+#### 问题
+
+KVO 存在一些问题,让我们用起来很不爽。
+
+##### 野指针崩溃
+
+**在调用 addObserver 后,KVO 并不会对观察者进行强引用。**
+
+问:`self.person1` 有没有强引用 `self` ?
+
+答案是否。因为当前控制器用 strong 指针拥有一个 person1 属性,此时如果 self.person1 也强引用了 self,则形成环,造成内存泄漏。系统不会这么设计,所以答案是否。
+
+所以要注意观察者的生命周期,否则会导致观察者被释放带来的 Crash 问题。如果**观察者对象被释放,若未移除观察,则被观察对象的属性变化时,仍然会调用观察者的 `- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context` 方法。此时,观察者指向的内存可能已被回收或分配给其他对象,导致访问无效内存(`EXC_BAD_ACCESS` 崩溃)**。
+
+所以 addObserver 和 removeObserver 需要成对存在
+
+```objective-c
+// Observer 被释放后未移除观察
+- (void)dealloc {
+ // 未调用 [object removeObserver:self forKeyPath:@"keyPath"];
+}
+```
+
+##### 内存泄漏
+
+虽然 **KVO 不会强引用观察者**(Observer 是弱引用),但以下情况可能引发内存问题:
+
+- **多次添加观察者**:重复调用 `addObserver` 会导致多次监听同一属性,但未正确移除时,被观察对象会保留多余的观察记录。
+- **被观察对象生命周期长于观察者**:若被观察对象存活时间更长,未移除观察会导致观察者无法释放(如单例对象监听某个属性)。
+
+Bad case
+
+```objective-c
+// 多次添加观察者而未移除
+[self.object addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];
+[self.object addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];
+// 移除一次后仍残留一次观察
+[self.object removeObserver:self forKeyPath:@"value"];
+```
+
+官方文档说明:
+
+> *Prior to iOS 9, if an object is observing a property when it is deallocated, the system will throw an exception. In iOS 9 and later, the system automatically removes the observer when it is deallocated.*
+> —— [Apple Developer Documentation](https://developer.apple.com/documentation/objectivec/nsobject/1415099-removeobserver)
+
+iOS 9+ 的改进与兼容性
+
+- iOS 9 及以上:当观察者或被观察对象被释放时,系统会自动清理 KVO 注册信息,减少崩溃风险。
+- iOS 8 及以下:仍需手动调用 `removeObserver`,否则可能导致崩溃。
+
+
+
+#### 改进
+
+- 添加 observer 后需要在 dealloc 方法中移除
+- 不能同同一属性,多次添加 observer
+- 也不能注册1次(addObserver),2次移除(removeObserver)。必须严格对应
+- 在某个地方添加监听,在另一处 `observeValueForKeyPath` 方法里拿到变化。导致 VC 的代码量变大且很分散
+
+可以用 RAC 或者 FBKVO
+
+FBKVO 优势:
+
+- **语法简洁易用** :相比苹果原生的 KVO API,FBKVO 的语法更加简洁直观,减少了大量的模板代码和繁琐的步骤,使得开发者可以更快速、更方便地实现对对象属性变化的观察,降低了代码的复杂度和出错的概率。
+- **自动管理观察者生命周期** :在原生 KVO 中,需要手动在合适的地方添加和移除观察者,容易出现忘记移除观察者而导致的崩溃问题。而 FBK VO够自动管理观察者的生命周期,当观察的对象被销毁或者观察者本身被销毁时,会自动移除相应的观察,有效避免了因观察者未及时移除而引发的异常,提高了代码的健壮性。
+- **支持 block 回调** :提供了基于 block 的观察回调方式,使得代码更加简洁明了,逻辑更加集中,便于阅读和维护,也更符合现代 iOS 开发中使用 block 的编程习惯。
+- **线程安全** :在多线程环境下,FBKVO 能够保证观察者注册、移除以及回调等操作的线程安全,避免了因线程问题导致的数据不一致和程序崩溃等风险,让开发者在处理多线程场景下的 KVO 操作时更加放心。
+- **丰富的观察选项** :除了支持原生 KVO 的观察选项外,还提供了一些额外的选项和功能,如可以方便地观察 NSArray 和 NSDictionary 等集合类的变化,以及对观察属性的变化进行更细致的控制和过滤等,满足了开发者在不同场景下的多样化需求。
+
+FBKVO 工作原理:
+
+- **基于原生 KVO 进行封装** :FBKVO 在内部封装了苹果原生的 KVO 机制,通过创建一个中间类来桥接观察者和被观察对象,替开发者处理了原生 KVO 中繁琐的细节操作,如 addObserver:forKeyPath:options:context: 和 removeObserver:forKeyPath: 等方法的调用,以及 context 参数的管理等。
+- **利用关联对象存储观察信息** :由于在 Objective - C 中,类别(category)无法直接添加属性,FBKVO 使用关联对象技术将观察的相关信息(如观察的键路径、回调 block 等)存储在被观察对象上,从而实现对被观察对象的扩展,使其能够记录和管理自身的观察者信息。
+- **实现自动移除观察者机制** :FBKVO 通过在被观察对象的 dealloc 方法中插入代码,或者利用 NSObject 的 KVO 通知机制,在观察的对象或者观察者即将被销毁时,自动调用 removeObserver:forKeyPath: 等方法移除相应的观察者,确保了观察者与被观察对象之间的正确解绑,避免了野指针等潜在问题。
+- **block 转换为对象方法调用** :在原生 KVO 中,观察者的回调是通过对象的方法来实现的。FBKVO 将开发者提供的 block 回调封装成一个符合原生 KVO 回调格式的对象方法,当被观察对象的属性发生变化时,先调用原生的 KVO 回调方法,再将事件传递给对应的 block 回调,从而实现了 block 回调的机制,使代码更加简洁和灵活。
+
+
+
+## KVO 实现机制
+
+## KVO 底层实现分析
+
+### Demo1
+
+
+
+可以发现对成员变量添加观察者的时候,成员变量的值变化了,KVO 也是监听不到的
+
+结论:**KVO 无法观察成员变量的变化**
+
+### Demo2
+
+
+
+可以看到对 NSMutableArray 类型的属性添加了 KVO。然后点击屏幕,NSMutableArray 里添加了元素,但是观察方法没有触发。
+
+对实验进行改进下
+
+
+
+结论: **KVO 只可以对属性的 setter 方法起作用**。
+
+通过 Demo1 和 Demo2 可以发现:**触发 KVO 的本质是必须要有属性的 setter,且触发属性的 setter。直接修改成员变量,是不会触发 setter** 的。
+
+### Demo3
+
+创建 Person 类,点击事件里触发属性值的改变
+
+
+
分析:
- 添加过 KVO 的 person1,isa 为系统利用 Runtime 技术动态创建的类,名字为 `NSKVONotifying_Person`
@@ -20,7 +468,7 @@ Demo1:创建 Person 类,点击事件里触发属性值的改变。
在内存中的结构如下图
-
+
整个流程分析下:
@@ -30,13 +478,11 @@ Demo1:创建 Person 类,点击事件里触发属性值的改变。
当我们按照 KVO 后动态生成的类名去创建一个新的类的时候,Xcode 会报错:`[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class`。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效
-
+
+### Demo4
-
-Demo2:
-
-
+
分析:
@@ -46,9 +492,9 @@ Demo2:
-Demo3:
+### Demo5
-
+
可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify`
@@ -58,11 +504,11 @@ Demo3:
### NSSet**ValueAndNotify 的内部实现
-
+
来对 Person 类增加一些打印方法
-
+
@@ -72,7 +518,12 @@ Demo3:
- 调用原本的 setter
-- 调用 `didChangeValueForKey`。在 `didChangeValueForKey` 内部会调用 KVO 的 `- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context` 方法
+- 调用 `didChangeValueForKey`。
+
+ - `didChangeValueForKey:` 会检查属性值是否实际变化,避免冗余通知
+ - 通知的发送是线程安全的,但观察者的回调执行线程取决于注册时的上下文
+
+- 在 `didChangeValueForKey` 内部会调用 KVO 的 `- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context` 方法
@@ -110,6 +561,15 @@ Demo3:
return [Person class];
}
+-(BOOL)_isKVOA {
+ return YES;
+}
+
+
+- (void)dealloc {
+ // 收尾工作
+}
+
@end
```
@@ -117,11 +577,11 @@ Demo3:
### 重写 class 方法
-
+
可以看到利用 runtime api,在添加 KVO 之后,类对象为 `NSKVONotifying_Person`
-但是利用 class 方法,添加 KVO 之后,获取类对象依旧为 Person。
+但是利用 class 方法,添加 KVO 之后,获取类对象依旧为 Person。所以推测系统在为某个对象添加 KVO 监听后,先利用 Runtime 生成一个继承自被监听类的基类。调用 `- (Class)class` 方法的本质就是先利用 isa 指针找到 `NSKVONotifying_Person` 类对象,然后在类对象的对象方法列表中查看有没有 `class` 方法的实现,系统应该是重写了 `-(Class)class` 方法,用于屏蔽底层实现。
好处是:屏蔽了 KVO 底层内部实现,隐藏了 `NSKVONotifying_Person` 的存在,通过 `-(Class)class` 方法告诉开发者添加 KVO 之后的类,依旧是 Person,本质上是继承自 Person 的类对象,能力没有改变。
@@ -129,38 +589,24 @@ Demo3:
### KVO 类的所有方法
-
+
利用 runtime api,打印添加 KVO 后,动态创建的 NSKVONotifying_Person 都存在什么方法?
+```objective-c
- setHeight:
- class
- dealloc
-- _isKVOA
+_isKVOA
+```
+
+
QA:为什么新创建的类没有 getter?
-因为新创建的类是子类,父类中存在 getter。子类中增加的方法只是为了触发 KVO,getter 不影响。
-
-
-
-### 修改成员变量的值可以触发 KVO 吗
-
-
-
-我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。
-
-也就是说,触发 KVO 的本质是必须要有 setter,且触发 setter。直接修改成员变量,是不会触发 setter 的。
-
-QA:如何手动触发?
-
-手动调用 `willChangeValueForKey`、 `didChangeValueForKey`
-
-
-
-
+因为新创建的类是子类,父类中存在 getter。子类中增加的 setter 方法只是为了触发 KVO,getter 不影响。
@@ -170,244 +616,36 @@ QA:请描述系统如何实现一个对象的 KVO?KVO 的本质是什么
- 当修改 instance 对象的属性时,会调用 Foundation 框架的 `_NSSet***ValueAndNotify` c 函数
- 然后调用:
- `willChangeValueForKey`
- - 原来的 setter
- - `didChangeValueForKey`,且 didChangeValueForKey 内部会触发监听器(Observer)的监听方法(`- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context`)。也就是发消息
-
-QA:如何手动触发 KVO?
-
-上面剖析了 KVO 的系统实现,所以手动触发 KVO 就需要调用 `willChangeValueForKey` 和 `didChangeValueForKey`
+ - 原来的 setter
+ - `didChangeValueForKey`,且 didChangeValueForKey 内部会触发监听器(Observer)的监听方法(`- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context`)。也就是发消息
-### 当没有observer观察任何一个property时,删除动态创建的子类
+### 当没有 observer 观察任何一个 property 时,删除动态创建的子类
`[self.person removeObserver:self forKeyPath:@"height"];` 该代码调用后,会删除动态创建的子类。
-## KVC
+### 为什么要选择是继承的子类而不是分类呢?
-`setValueForKey` 用来设置对象的一层属性值修改。
+对某个类的属性添加 KVO 观察后,系统会创建一个继承自该类的子类,子类的类对象没有 setter 方法,但是可以调用 setter 方法(子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃)。但是为了可以触发 KVO,所以需要在保证原来业务逻辑的基础上,在业务逻辑之前调用 `willChangeValueForKey` ,在业务逻辑之后调用 `didChangeValueForKey`.
-`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
-
-
-
-### KVC 设值原理
-
-KVC 之后会触发 KVO 吗?
-
-
-
-发现 KVC 触发了 KVO。
-
-问题来了:为什么 KVC 会触发 KVO?探究下 `setValueForKey`
-
-整个流程如下
-
-
-
-`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException`
-
-```
-@implementation Person
-- (void)setAge:(int)age
-{
- _age = age;
-}
-
-- (void)_setAge:(int)age
-{
- _age = age;
-}
-
-+ (BOOL)accessInstanceVariableDirectlt
-{
- return YES;
-}
-
-@end
-```
-
-
-
-### 直接修改成员变量会触发 KVO 吗?
-
-不会。KVO 的实现原理就是在 setter 方法内,调用 `willChangeValueForKey`、`didChangeValueForKey` ,所以直接修改成员变量不会触发 KVO。
-
-想要触发,可以手动调用上面2个 API。
+为了实现这一诉求就必须使用继承,因为如果用分类实现了,则知道分类的方法会在 App 启动后经过 Runtime 进行方法整合后,会排在类对象的方法列表最前面,也就是会覆盖被监听类的属性 setter 方法。万一 setter 里面不只是简单的 `_name = name;` 而是有加锁、或者其他的业务逻辑,**使用 Category 会造成业务逻辑丢失的情况。所以必须使用继承实现**,内部再调用 `[super setName:value]` 即可。
```objective-c
-- (void)setName:(NSString *)name {
- [self willChangeValueForKey:@"name"];
- _name = name;
- [self didChangeValueForKey:@"name"];
+- setter {
+ [self willChangeValueForKey:key]
+ [super setter:key]
+ [self didChangeValueForKey:key]
}
```
-
-
-### KVC 取值原理
-
-`valueForKey` 原理
-
-- 按照 getKey、key、isKey、_key 的顺序寻找方法实现,找到则直接调用方法,返回值
-- 如果没找到则调用 `+(BOOL)accessInstanceVariableDirectly` 方法,询问是否可以访问成员变量
- - 为 NO 则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
- - 为 YES 则按照 ` __key`、`_isKey`、`key`、`isKey` 的顺序访问成员变量。找到哪个则返回值
-
-- 都没找到则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
+关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
-
-
-
-
-### KVC 会破坏面向对象的原则吗?
-
-KVC 有违背面向对象编程思想吗?如果一个类的成员变量是私有的,也没有在 `.h` 中公开一些方法去设置、修改成员变量,那么外部直接通过 KVC 去修改值,是有违背面向对象编程思想的。
-
-KVC 提供了对应的能力,去保护或者说支持面向对象的原则。
-
-
-
-
-
-
-
-## 基本用法-字典快速赋值
-
-KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setValuesForKeysWithDictionary`。
-
-```objective-c
-//前提:model 中的各个 property 必须和 NSDictionary 中的属性一致
-- (instancetype)initWithDic:(NSDictionary *)dic{
- BannerModel *model = [BannerModel new];
- [model setValuesForKeysWithDictionary:dic];
- return model;
-}
-```
-
-但是这里会有2种特殊情况。
-
-- 情况一:在 model 里面有 property 但是在 NSDictionary 里面没有这个值
-
-运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null
-
-- 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值
-
-运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分
-
-```
-- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
-
-}
-```
-
-- 情况三:如果 Dictionary 和 Model 中的 property 不同名
-
-我们照样可以利用 **setValue:forUndefinedKey:** 去处理
-
-```objective-c
-//model
-@property (nonatomic,copy)NSString *name;
-@property (nonatomic,copy)NSString *sex;
-@property (nonatomic,copy) NSString* age;
-//NSDictionary
-NSDictionary *dic = @{@"username":@"张三",@"sex":@"男",@"id":@"22"};
-
--(void)setValue:(id)value forUndefinedKey:(NSString *)key{
- if([key isEqualToString:@"id"]){
- self.age=value;
- }
- if([key isEqualToString:@"username"]){
- self.name=value;
- }
-}
-```
-
-- 情况四:如果我们观察对象的属性是数组,我们经常会观察不到变化,因为 KVO 是观察 setter 方法。我们可以用 `mutableArrayValueForKeyPath` 进行属性的操作
-
-```
-NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
-[hobbies addObject:@"Web"];
-```
-
-- 情况五: 注册依赖键.
-
-KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类,Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是,Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”
-
-```
-[self.person addObserver:self
- forKeyPath:NSStringFromSelector(@selector(dog))
- options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
- context:ContextMark];
-
-self.person.dog.name = @"啸天犬";
-self.person.dog.weight = 50;
-
-
-// Person.m
-+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
- NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
-
- if ([key isEqualToString:@"dog"]) {
- NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
- keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
- }
- return keyPaths;
-}
-```
-
-
-
-
-
-## 几个基本的知识点
-
-1. KVO 观察者和属性被观察的对象之间不是强引用的关系
-
-2. KVO 的触发分为`自动触发模式`和`手动触发模式`2种。通常我们使用的都是自动通知,注册观察者之后,当条件触发的时候会自动调用 `-(void)observeValueForKeyPath`。如果需要实现手动通知,我们需要使用下面的方法
-
-```Objective-c
-+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
- return NO;
-}
-```
-
-3. 若类有实例变量 NSString *_foo, 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key ?
-
-都可以
-
-4. KVC 的 keyPath 中的集合运算符如何使用
-- 必须用在 **集合对象** 或者 **普通对象的集合属性** 上
-
--简单的集合运算符有 @avg、@count、@max、@min、@sum
-
-5. KVO 和 KVC 的 keyPath 一定是属性吗?
- 可以是成员变量
-
-6. KVO 中 派生类的 setter 方法内部实现调用了 Foundation 框架中的 `_NSSetIntValueAndNotify`.
-
-7. 直接修改对象的成员变量会触发 KVO 吗?
-
- 不会。因为成员变量没有 setter.
-
- ```
- @interface Person: NSObject
- {
- @public:
- int age;
- }
- @end
- ```
-
-
-
-
-
-## 实现机制
+##
> Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
@@ -425,17 +663,10 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
-
-
-为什么要选择是继承的子类而不是分类呢?
-子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
-
-关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
+
-## QA
-
iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么)
- 当对一个对象使用了 KVO 监听,系统会修改这个对象的 isa 指针,改为一个指向通过 Runtime 动态创建的子类
@@ -453,15 +684,21 @@ iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么)
-## 模拟实现系统的 KVO
+### 模拟实现系统的 KVO
1. 创建被观察对象的子类
+
2. 重写观察对象属性的 set 方法,同时调用 `willChangeValueForKey、didChangeValueForKey`
+
+ 因为子类继承自父类,所以子类调用 setter 的时候,会调用成功,不过方法调用的时候是通过子类的 isa 找到子类对象的类对象,然后从类对象的方法列表里查看 setter,发现没有则通过子类的 superclass 找到父类,然后找到父类的类对象方法列表,成功找到了 setter 方法实现。
+
+ 但 KVO 的 setter 里需要在原来基础上做一些额外逻辑,所以需要重写 setter。在子类里重写 setter,本质就是为元类对象里添加一个新的 setter 方法。
+
3. 外界改变 isa 指针(class方法重写)
我们用自己的类模拟系统的 KVO。
-```
+```objective-c
//NSObject+LBPKVO.h
#import
@@ -552,11 +789,227 @@ void setName (id self, SEL _cmd, NSString *name) {
KVO 的缺陷:
-KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block 没门儿。感觉如果加入 block 就更棒了。
+KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block 没门儿。感觉如果加入 block 就更棒了。
KVO 的改装:
-看到官方的做法并不是很方便使用,我们看到无数的优秀框架都支持 block 特性,比如 AFNetworking ,所以我们可以将系统的 KVO 改装成支持 block。
+看到官方的做法并不是很方便使用,我们看到无数的优秀框架都支持 block 特性,比如 AFNetworking ,所以我们可以将系统的 KVO 改装成支持 block。
+
+
+
+## KVC
+
+`setValueForKey` 用来设置对象的一层属性值修改。
+
+`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
+
+
+
+### KVC 设值原理
+
+KVC 之后会触发 KVO 吗?
+
+
+
+发现 KVC 触发了 KVO。
+
+问题来了:为什么 KVC 会触发 KVO?探究下 `setValueForKey`
+
+整个流程如下
+
+
+
+`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException`
+
+```
+@implementation Person
+- (void)setAge:(int)age
+{
+ _age = age;
+}
+
+- (void)_setAge:(int)age
+{
+ _age = age;
+}
+
++ (BOOL)accessInstanceVariableDirectlt
+{
+ return YES;
+}
+
+@end
+```
+
+
+
+### 直接修改成员变量会触发 KVO 吗?
+
+比如在屏幕触目事件里修改 Person 对象的成员变量 `_age`。比如 `self.p1->_age = 30;` 这样是无法触发 KVO 的。
+
+因为 KVO 的实现原理就是在 Runtime 动态生成类里拦截 setter 方法。在 setter 内部调用 `willChangeValueForKey`、`didChangeValueForKey` ,所以直接修改成员变量不会触发 KVO。
+
+如果要在修改成员变量的基础上触发 KVO,则必须手动调用上面2个 API。比如下面的代码
+
+```objective-c
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ [self.p1 willChangeValueForKey:@"age"];
+ self.p1->_age = 30;
+ [self.p1 didChangeValueForKey:@"age"];
+}
+```
+
+
+
+### KVC 取值原理
+
+`valueForKey` 原理
+
+- 按照 getKey、key、isKey、_key 的顺序寻找方法实现,找到则直接调用方法,返回值
+- 如果没找到则调用 `+(BOOL)accessInstanceVariableDirectly` 方法,询问是否可以访问成员变量
+ - 为 NO 则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
+ - 为 YES 则按照 ` __key`、`_isKey`、`key`、`isKey` 的顺序访问成员变量。找到哪个则返回值
+
+- 都没找到则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
+
+
+
+
+
+
+
+### KVC 会破坏面向对象的原则吗?
+
+KVC 有违背面向对象编程思想吗?如果一个类的成员变量是私有的,也没有在 `.h` 中公开一些方法去设置、修改成员变量,那么外部直接通过 KVC 去修改值,是有违背面向对象编程思想的。
+
+KVC 提供了对应的能力,去保护或者说支持面向对象的原则。
+
+
+
+## 基本用法-字典快速赋值
+
+KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setValuesForKeysWithDictionary`。
+
+```objective-c
+//前提:model 中的各个 property 必须和 NSDictionary 中的属性一致
+- (instancetype)initWithDic:(NSDictionary *)dic{
+ BannerModel *model = [BannerModel new];
+ [model setValuesForKeysWithDictionary:dic];
+ return model;
+}
+```
+
+但是这里会有2种特殊情况。
+
+- 情况一:在 model 里面有 property 但是在 NSDictionary 里面没有这个值
+
+运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null
+
+- 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值
+
+运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分
+
+```
+- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
+
+}
+```
+
+- 情况三:如果 Dictionary 和 Model 中的 property 不同名
+
+我们照样可以利用 **setValue:forUndefinedKey:** 去处理
+
+```objective-c
+//model
+@property (nonatomic,copy)NSString *name;
+@property (nonatomic,copy)NSString *sex;
+@property (nonatomic,copy) NSString* age;
+//NSDictionary
+NSDictionary *dic = @{@"username":@"张三",@"sex":@"男",@"id":@"22"};
+
+-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
+ if([key isEqualToString:@"id"]){
+ self.age=value;
+ }
+ if([key isEqualToString:@"username"]){
+ self.name=value;
+ }
+}
+```
+
+- 情况四:如果我们观察对象的属性是数组,我们经常会观察不到变化,因为 KVO 是观察 setter 方法。我们可以用 `mutableArrayValueForKeyPath` 进行属性的操作
+
+```
+NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
+[hobbies addObject:@"Web"];
+```
+
+- 情况五: 注册依赖键.
+
+KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类,Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是,Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”
+
+```
+[self.person addObserver:self
+ forKeyPath:NSStringFromSelector(@selector(dog))
+ options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
+ context:ContextMark];
+
+self.person.dog.name = @"啸天犬";
+self.person.dog.weight = 50;
+
+
+// Person.m
++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
+ NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
+
+ if ([key isEqualToString:@"dog"]) {
+ NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
+ keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
+ }
+ return keyPaths;
+}
+```
+
+
+
+## 几个基本的知识点
+
+1. KVO 观察者和属性被观察的对象之间不是强引用的关系
+
+2. KVO 的触发分为`自动触发模式`和`手动触发模式`2种。通常我们使用的都是自动通知,注册观察者之后,当条件触发的时候会自动调用 `-(void)observeValueForKeyPath`。如果需要实现手动通知,我们需要使用下面的方法
+
+```Objective-c
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ return NO;
+}
+```
+
+3. 若类有实例变量 NSString *_foo, 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key ?
+
+都可以
+
+4. KVC 的 keyPath 中的集合运算符如何使用
+- 必须用在 **集合对象** 或者 **普通对象的集合属性** 上
+
+-简单的集合运算符有 @avg、@count、@max、@min、@sum
+
+5. KVO 和 KVC 的 keyPath 一定是属性吗?
+ 可以是成员变量
+
+6. KVO 中 派生类的 setter 方法内部实现调用了 Foundation 框架中的 `_NSSetIntValueAndNotify`.
+
+7. 直接修改对象的成员变量会触发 KVO 吗?
+
+ 不会。因为成员变量没有 setter.
+
+ ```
+ @interface Person: NSObject
+ {
+ @public:
+ int age;
+ }
+ @end
+ ```
diff --git a/Chapter1 - iOS/1.48.md b/Chapter1 - iOS/1.48.md
index fc9cd30..7c305d8 100644
--- a/Chapter1 - iOS/1.48.md
+++ b/Chapter1 - iOS/1.48.md
@@ -1177,13 +1177,13 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
过程如下
-
+
结果就是类方法列表中,最前面的就是所有分类的方法列表,最后是类自身的方法列表。
-效果为 `[[PersonWork Category 方法列表], [PersonStudy Category 方法列表], [Person类自身对象方法列表]]`
+效果为 `[PersonWork Category 方法列表, PersonStudy Category 方法列表, Person类自身对象方法列表]`
-
+假设 Person 有 m1、m2 对象方法,Person+Study 分类有 m3、m4方法,Person+Sleep 分类有 m5、m6方法,(Person+Sleep 先编译)合并后的方法列表是 [m5, m6, m3, m4, m1, m2]
总结:
@@ -1193,6 +1193,62 @@ Category 编译之后 底层结构为 struct category_t,里面存储着分类
+QA:
+1. Runtime 在将 category 方法和类方法合并的时候,以前版本是
+
+```objective-c
+memmove(array()->lists + addedCount, array()->lists,
+ oldCount * sizeof(array()->lists[0]));
+memcpy(array()->lists, addedLists,
+ addedCount * sizeof(array()->lists[0]));
+```
+
+而新版本为
+
+```objective-c
+for (int i = oldCount - 1; i >= 0; i--)
+ newArray->lists[i + addedCount] = array()->lists[i];
+for (unsigned i = 0; i < addedCount; i++)
+ newArray->lists[i] = addedLists[I]
+```
+
+为什么要改?给出专业和权威的回答,尽量参考官方文档
+
+- 线程安全与原子性
+旧方案的潜在问题:旧版本直接在原内存区域操作(memmove 后接 memcpy),若此时其他线程访问方法列表,可能读到不一致的中间状态(如部分旧数据被覆盖、部分新数据未写入),导致崩溃或逻辑错误
+新方案的改进:新版本通过 分配新内存、完整构建新数组,最后一次性更新指针(如 array()->count 和 array()->lists),使得操作具备原子性。其他线程要么看到完整的旧数组,要么看到完整的新数组,避免了中间状态的不一致性
+
+- 内存拓展的可靠性
+`relloc` 的结果不可预测,可能失败或返回新地址,导致原指针失效。若运行时其他模块持有旧指针的引用,会引发野指针问题
+新版本显式分配新内存(malloc),将旧数据迁移到新内存后替换指针。这种方式更可控,避免了 `realloc` 的不确定性,同时允许旧内存的安全释放
+
+- 内存重叠与顺序控制
+虽 `memmove` 允许源和目标内存重叠,但在原数组基础上直接扩展时,若内存无法原地扩展(如后续内存被占用),`memmove` 可能覆盖有效数据或越界,导致不符合预期的行为
+
+- 手动复制的灵活性
+面向未来,方便插入一些其他逻辑。
+
+
+2. 将 Category 信息和类自身信息合并放到运行时有什么好处?为什么不放到编译时搞定?
+- 动态性。
+ - 延迟绑定:Objective-C 的方法调用基于消息转发(Messaging),方法实现可以在运行时动态绑定。Category 的方法在启动时被合并到类的方法列表中,使得开发者可以在不修改原始类代码的情况下,动态扩展或替换方法。
+ - 支持热更新与插件化:通过 dlopen 或动态库加载,可以在程序运行时加载新的 Category(如插件功能),实现功能扩展,而无需重新编译主程序。
+- 解决多 Category 方法冲突的灵活性
+ 当多个 Category 定义了同名方法时,最后被加载的 Category 方法会覆盖之前的方法。这种策略虽然简单,但需要运行时动态合并:
+ 编译时无法确定 Category 的加载顺序(例如依赖动态库的加载顺序)。
+ 运行时可以通过调整加载顺序实现方法覆盖的灵活控制。
+
+如果放到编译时处理的潜在优势与取舍
+如果选择编译时合并 Category,可能带来:
+- 性能提升:方法查找可能更快(无需运行时遍历 Category 列表)。
+- 更强的类型安全:编译时能检查所有方法冲突。
+但代价是:
+- 丧失动态能力:如热修复、Method Swizzling、插件化等场景将无法实现。
+- 代码僵化:所有扩展必须在编译时确定,无法适应动态环境(如大型应用的模块化延迟加载)
+
+Objective-C 将 Category 合并推迟到运行时,是为了最大化语言的动态性和灵活性,支持热更新、AOP、插件化等高级场景。这种设计符合其“动态消息语言”的定位,但牺牲了部分编译时优化机会。选择编译时或运行时处理,本质是灵活性与性能/安全性的权衡,而 Objective-C 明确选择了前者。
+
+
### QA
#### 为什么二维数组打了引号?
@@ -1278,7 +1334,7 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
}
```
-
+
可以看到 sayHi 方法存在多个,但是由于 Category 同名的方法在方法列表的前面,所以类自身的方法实现”被覆盖了“(根据 isa 查找方法实现的时候,优先查找到 Category 的方法实现,则停止查找了)
@@ -1291,17 +1347,17 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具有不同实现。探索编译顺序决定方法实现
-
+
2个对比实验:
让 `Person+Study` 参与后编译
-
+
让 `Person+Learn` 参与后编译
-
+
@@ -1882,10 +1938,11 @@ static bool call_category_loads(void)
}
```
-阅读源码发现:
+阅读源码可以得出2个结论:
-1. 在调用 `+load` 方法的时候,系统会先调用(可加载)类的 `+load` 方法,再调用分类的 `+load` 方法
-2. `call_class_loads`、`call_category_loads` 方法内部实现,是通过 `loadable_class` `loadable_category` 结构体的 method 成员值 ,通过 `load_method_t load_method = (load_method_t)classes[i].method` 找到 `+ load` 方法地址。最后直接调用 `(*load_method)(cls, SEL_load)` 方法本身,没有采用消息机制。
+1. `+load` 方法是系统启动后,系统主动调用的;
+2. 在调用 `+load` 方法的时候,系统会先调用(可加载)类的 `+load` 方法,再调用分类的 `+load` 方法(先调用 `call_class_loads` 再调用 `call_category_loads`)
+3. `call_class_loads`、`call_category_loads` 方法内部实现,是通过 `loadable_class` `loadable_category` 结构体的 method 成员值 ,通过 `load_method_t load_method = (load_method_t)classes[i].method` 找到 `+ load` 方法地址。最后直接调用 `(*load_method)(cls, SEL_load)` 方法本身,没有采用消息机制。
@@ -2004,6 +2061,33 @@ static void schedule_class_load(Class cls)
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
+
+void add_class_to_loadable_list(Class cls)
+{
+ IMP method;
+
+ loadMethodLock.assertLocked();
+
+ method = cls->getLoadMethod();
+ if (!method) return; // Don't bother if cls has no +load method
+
+ if (PrintLoading) {
+ _objc_inform("LOAD: class '%s' scheduled for +load",
+ cls->nameForLogging());
+ }
+
+ if (loadable_classes_used == loadable_classes_allocated) {
+ loadable_classes_allocated = loadable_classes_allocated*2 + 16;
+ loadable_classes = (struct loadable_class *)
+ realloc(loadable_classes,
+ loadable_classes_allocated *
+ sizeof(struct loadable_class));
+ }
+ // 将
+ loadable_classes[loadable_classes_used].cls = cls;
+ loadable_classes[loadable_classes_used].method = method;
+ loadable_classes_used++;
+}
```
通过源码,我们可以看出:
@@ -2079,18 +2163,80 @@ void add_category_to_loadable_list(Category cat)
可以看到通过 `schedule_class_load` 处理完类之后,分类是直接通过 `_getObjc2NonlazyCategoryList(mhdr, &count)` 获取的,之后也是直接添加到 `loadable_categories` 中去。
+举个例子:
+
+存在下面 4个类
+
+```objective-c
+Class Person : NSObject
+Class Person+Good : NSObject
+Class Person+Bad : NSObject
+Class Student : Person
+Class Student+Good : Person
+Class Student+Bad : Person
+Class Cat: NSObject
+Class Dog: NSObject
+```
+
+如果在 Xcode 的 Build Phases 中,顺序为
+
+```objective-c
+Cat.m
+Dog.m
+Student+Good.m
+Student+Bad.m
+Student.m
+Person+Good.m
+Person+Bad.m
+Person.m
+
+运行后打印为:
+- Cat load
+- Dog load
+- Person load
+- Student load
+- Student+Good + load
+- Student+Bad + load
+- Person+Good load
+- Person+Bad load
+```
+
+如果在 Xcode 的 Build Phases 中,顺序为
+
+```objective-c
+Cat.m
+Student+Good.m
+Student+Bad.m
+Student.m
+Person+Good.m
+Person+Bad.m
+Person.m
+Dog.m
+
+运行后打印为:
+- Cat load
+- Person load
+- Student load
+- Student+Good + load
+- Student+Bad + load
+- Person+Good load
+- Person+Bad load
+- Dog load
+```
+
### +load 总结
-- `+load` 方法是系统通过 runtime 在加载类、分类的时候调用的
+- `+load` 方法是系统通过 Runtime 在加载类、分类的时候调用的
- 每个类、分类的 `+load` 在程序运行过程中只调用1次
- 调用顺序方面:
- 先调用类的 `+load` 方法
- - 存在继承关系的话,调用子类的 `+load` 之前会调用父类的 `+load` 方法(runtime 会保证好,先调用父类的 `+load` ,再调用子类的 `+load` )
- - 不存在继承关系的话,会按照编译顺序调用 `+load`(先编译的先调用)
+ - 存在继承关系的话,调用子类的 `+load` 之前会调用父类的 `+load` 方法(Runtime 会保证好,先调用父类的 `+load` ,再调用子类的 `+load` )
+ - 不存在继承关系的话,会按照编译顺序调用 `+load`(先编译的先调用,编译顺序参考 Xcode 的 Build Phases -> Compile Sources 中的顺序)
- 再调用分类的 `+load` 方法
- 按照编译顺序调用 `+load`(先编译的先调用)
+ - 全局数组 `loadable_classes`,确保后续运行时能按正确顺序(父类 → 子类 → 分类)调用这些方法
@@ -2124,9 +2270,7 @@ Person +load
可以看到,我们主动调用 `[Student load]` 由于 Student 自身没有实现 `+load`,由于存在继承,所以调用了 Person 的 `+load` 。手动调用 `+load` 本质上就是给 runtime 消息机制,等价于 `objc_msgSend([Student class], @selector(load))` ,通过 Student 类对象的 isa,找到 Student 元类对象,判断有没有类方法 `+load`,发现没有,则根据 Student 元类对象的 `superclass` 找到父类的元类对象,也就是 Person 的元类对象,发现有 `+load` 实现,即调用了 `+load` ,打印 `Person +load`
-
-
-2.为什么 load 方法打印顺序是这样的?
+2.为什么 load 方法打印顺序是这样的?
因为调用 student alloc,相当于发送了消息。则肯定先执行 load 方法。类在 Runtime 启动阶段会调用 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面。load 方法在本质上是执行 `call_load_methods`,方法地址是确定的(查看下面的源代码可以发现 `load` 方法是在编译期就可以确定的)。不走 `objc_msgSend` 这套流程。所以先打印父类 load、再打印子类 load、最后打印分类 load。如果存在多个分类,则按照编译顺序打印 load。
@@ -2155,16 +2299,16 @@ Person +load
```objectivec
+[Person initialize]
-+[Student(Good) initialize]
++[Student(Go od) initialize]
```
查看分类在 Runtime 加载类信息时候的调用原理可以知道,分类中的类方法、对象方法都会被加载原始类的前面去(initialize 是类方法)如下图:
-
+
### 为什么给子类发消息,父类和子类的 +initialize 都会被调用?且父类的先调用
-梳理下目前掌握的信息:当类第一次接收消息,也就是第一次调用对象方法的时候,该类的 `+initialize` 方法会被调用。所以需要从 objc_msgSend 或者 获取对象方法、的角度去查看源码。聚焦下,以 `class_getInstanceMethod` 方法为入口,分析源码
+梳理下目前掌握的信息:当类第一次接收消息,也就是第一次调用对象方法的时候,该类的 `+initialize` 方法会被调用。所以需要从 objc_msgSend 或者获取对象方法的角度去查看源码。聚焦下,以 `class_getInstanceMethod` 方法为入口,分析源码
```c++
/***********************************************************************
@@ -2353,7 +2497,7 @@ realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
`realizeAndInitializeIfNeeded_locked` 内部判断,当 class 需要被初始化且没有初始化过的时候则执行 `initializeAndLeaveLocked`
-```
+```c++
// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
@@ -2554,6 +2698,56 @@ void callInitialize(Class cls)
至此,可以解释 Demo 中的现象了。
+再举个例子
+
+```objective-c
+class Student: Person
+class Person: NSObject
+class Person+Good: NSObject
+class Person+Bad: NSObject
+class Teacher: Person
+```
+
+其中 Student、Teacher 都没有实现 initialize 方法。只有 Person 实现了、Person+Good 和 Person+Bad(编译顺序较后) 也实现了 initialize 方法.
+
+调用
+
+```objective-c
+[Student alloc]
+[Teacher alloc]
+```
+
+打印输出
+
+```objective-c
+- Person+Bad initialize
+- Person+Bad initialize
+- Person+Bad initialize
+```
+
+为什么输出这样的信息?
+
+`[Student alloc]` 等价于 `objc_msgSend([Student Class], @sel(initialize))` 然后 `objc_msgSend` 汇编实现里会调用 `lookUpImpOrForward`,`lookUpImpOrForward` 内部会调用 `initializeNonMetaClass`,其内部实现会判断是否存在父类,存在父类且父类没有调用过 `initialize`,则会递归先调用当前类的父类的 `initialize` 方法。递归结束则调用 `callInitialize` 方法去完成当前类的 `initialize`
+
+所以上述代码底层类似于
+```Objective-c
+if (Student 类没有初始化过) {
+ if (Student 父类(Person 父类存在,但存在分类,也就是 Person+Bad)存在 && 父类没有初始化) {
+ objc_msgSend(Person + Bad,@selector(initializ)) // Person+Bad initialize
+ }
+ objc_msgSend([Student class],@selector(initializ)) // Student 没有 initialize 方法,则根据 isa 查找父类方法。打印 Person+Bad initialize
+}
+
+
+if (Teacher 类没有初始化过) {
+ // Person 已经 initialize 过,if 里的逻辑进不去
+ if (Teacher 父类(Person 父类存在,但存在分类,也就是 Person+Bad)存在 && 父类没有初始化) {
+ objc_msgSend(Person + Bad,@selector(initializ))
+ }
+ objc_msgSend([Teacher class],@selector(initializ)) // Teacher 没有 initialize 方法,则根据 isa 查找父类方法。打印 Person+Bad initialize
+}
+```
+
### 为什么 `initialize`方法存在“覆盖”的情况?
@@ -2725,7 +2919,52 @@ NS_ASSUME_NONNULL_END
@end
```
+QA:
+1. 为什么 `category_t` 结构体里的属性命名为 `instanceProperties`? `classProperties` 何时使用?
+普通的 property 就是 instanceProperties,而用 `@property (class, nonatomic, copy) NSString *name;` class 修饰的就是 `classProperties`。
+类属性是 Objective-C 在 Xcode 8 / LLVM 3.0 后引入的特性,旨在提供一种更简洁的方式声明与类相关的全局状态或行为。
+属性描述中有 `class` 表明这是类属性,通过类名直接访问(如 ClassName.defaultName),而非实例属性。
+类属性存储在类的元类(metaclass)中,与实例属性完全隔离。
+尽管声明方式类似实例属性,类属性不会自动生成存取方法或存储,需开发者手动实现。
+
+```
+// Person.h
+@interface Person : NSObject
+@property (class, nonatomic, copy) NSString *defaultName;
+@end
+
+// Person.m
+@implementation Person
+static NSString *_defaultName = nil;
+
++ (NSString *)defaultName {
+ return _defaultName;
+}
+
++ (void)setDefaultName:(NSString *)defaultName {
+ _defaultName = [defaultName copy]; // 执行 copy 操作
+}
+@end
+
+// 使用
+// 设置类属性
+Person.defaultName = @"Anonymous";
+// 获取类属性
+NSString *name = Person.defaultName;
+```
+
+类属性的使用场景:
+- 为类定义全局可访问的默认值,例如应用的主题颜色、默认语言等。`@property (class, nonatomic, strong) UIColor *appThemeColor;`
+- 单例模式的替代方案.若只需一个简单的共享实例,类属性可以替代传统单例模式。
+
+
+2. 为什么要给 Category 添加的属性只有属性的 setter、getter,却没有成员变量和对应的 setter、getter 实现,为什么这么设计?存在什么优点
+为什么不能直接添加成员变量?运行时限制,Runtime 在程序加载时就确定了类的内存布局,包括成员变量的存储结构,如果允许在运行时动态添加成员变量,可能会破坏类的内存布局,导致兼容性问题
+编译时的静态性:成员变量的存储结构需要在编译时确定,而 Category 是一种运行时机制,无法在编译时修改类的结构
+设计哲学:OC 的设计哲学强调动态性,而动态性体现在方法上,而不是成员变量上。成员变量的静态性有助于保持类的稳定性和可预测性。
+
+Objective-C 的 Category 设计允许动态添加方法,但不允许直接添加成员变量,这是基于运行时机制的限制和语言设计哲学的考虑。这种设计的优点在于灵活性和扩展性,同时避免了对类内存布局的破坏。通过关联对象等机制,开发者可以在 Category 中实现类似成员变量的功能,从而充分利用运行时的动态性。
## 关联对象的底层实现
@@ -2820,24 +3059,26 @@ void _object_set_associative_reference(id object, void *key, id value, uintptr_t
梳理后,如下图所示:
-
+
-AssociationsManager 管理的 AssociationHashMap 结构如下:
+AssociationsManager 管理的 AssociationsHashMap 结构如下:
```objective-c
{
+ // Person 实例对象的地址
"0x4927298732": {
- "@selector(studyNumber)" : {
+ "@selector(studyNumber)的地址" : {
"value": "2022122201",
"policy": "retain"
},
- "@selector(title)": {
+ "@selector(title)的地址": {
"value": "Hello category",
"policy": "retain"
}
},
+ // UIView 实例对象的地址
"0x3666444222": {
- "@selector(backgroundColor)" : {
+ "@selector(backgroundColor)"的地址 : {
"value": "0xff0021",
"policy": "retain"
}
@@ -2937,7 +3178,7 @@ NS_ASSUME_NONNULL_END
### 声明私有方法
-
+
diff --git a/Chapter1 - iOS/1.49.md b/Chapter1 - iOS/1.49.md
index a56066d..6d85086 100644
--- a/Chapter1 - iOS/1.49.md
+++ b/Chapter1 - iOS/1.49.md
@@ -1,10 +1,12 @@
# MVC、MVP、MVVM
-## MVC
+## MVC 架构
MVC 模式下,软件被划分为视图(View:用户界面)、控制器(Controller:业务逻辑)、模型(Model:数据保存)
-
+
+
+
1. 用户操作 View,在 View 上面的事件都将被传递到 Controller 处理
2. Controller 处理事件、请求网络,操作 Model 更新状态
@@ -14,44 +16,667 @@ MVC 模式下,软件被划分为视图(View:用户界面)、控制器(
+### Apple MVC 架构
-## MVP
+效果等价于:
+
+
+
+最典型的就是 iOS 侧的 UITableView 的设计:
+
+- Controller 感知 View:UIViewController 通过 View 的形式持有 UITableView
+- View 事件代理到 Controller:而 View 上的 UI 事件自身不处理,通过代理的形式交给 UIViewController 处理
+- Controller 感知 Model:UIViewController 负责从网络或者数据库中拉取数据,持有 Model。在合适的时机上,UIViewController 负责将 Model 数据交给 UIView 去展示(UITableView 的 cellforRow 代理方法中,cell.titleLabel.text = model.title 的形式展示上去) 。也就是说 View 无法感知 Model 的存在。
+- Model 的变化通过 Controller 传递到 View 上:View 的点击事件通过代理交给 Controller 处理,往往会涉及 Model 的变化。Model 变化后,还是不直接操作 View。依旧通过 Controller 操作 View 的接口,更新数据
+
+优点:
+
+- View 可以重用。因为 View 不用感知 Model 的存在,View 展示的东西,外部通过访问修改 UI 控件属性的形式去实现。
+
+ ```objective-c
+ @interface GoodCell
+ @property (nonatomic, strong, readonly) UIImageView *goodsImageView;
+ @property (nonatomic, strong, readonly) UILabel *goodsNameLabel;
+ @end
+
+ // GoodsListViewController
+ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
+ GoodsModel *model = self.goods[indexPath.row];
+ cell.goodsImageView.image = [UIImage imageNamed:model.iamge];
+ cell.goodsNameLabel.text = model.name;
+ return cell;
+ }
+ ```
+
+
+
+- Model 可以重用。Model 也不用感知 View 的存在。
+
+缺点:
+
+- Controller 过于臃肿。因为 View 和 Model 彼此独立,所以读取 Model 然后拼装数据,外部通过访问修改 UI 控件属性,负责展示 UI 的逻辑都放在 Controller 中,所以 Controller 就会很臃肿。
+
+
+
+### MVC 架构变种
+
+
+
+改变:
+
+- View 可以拥有 Model,感知 Model。一部分之前放在 Controller 里的逻辑,拆分到 View 里面了
+
+优点:
+
+- 对 Controller 进行了瘦身,这符合“单一职责原则”,使 Controller 更专注于协调和业务逻辑。
+
+ 将 UI 配置逻辑内聚到 View 中,避免了 Controller 中冗长的控件操作代码(如 `cell.imageView.image = model.image;`)
+
+- 将 View 内部的细节进行了隐藏,更具备封装性。外界不知道 View 内部具体的实现(Apple MVC 会暴露 UI 控件,现在不用暴露了),更具封装性
+
+ ```objective-c
+ // GoodCell.h
+ @interface GoodCell
+ @property (nonatomic, strong) GoodsModel *model;
+ @end
+
+ // GoodCell.m
+ @interface GoodCell()
+ @property (nonatomic, strong, readonly) UIImageView *goodsImageView;
+ @property (nonatomic, strong, readonly) UILabel *goodsNameLabel;
+ @end
+
+ -(void)setModel:(GoodsModel *)model {
+ _model = model;
+ self.goodsImageView.image = [UIImage imageNamed:model.iamge];
+ self.goodsNameLabel.text = model.name;
+ }
+ // GoodsListViewController
+ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
+ cell.model = self.goods[indexPath.row];
+ return cell;
+ }
+ ```
+
+缺点:
+
+- View 强依赖于 Model。
+
+### 思考:View 强依赖于 Model 真的是缺点吗?
+
+这个缺点就真的是缺点吗?我一个商品的 GoodsView 展示,就需要展示商品图片、商品名称、商品原价、商品折扣价、销量等信息。单独暴露 UI 控件,然后访问的话很灵活也很独立,但工作量很大,为了 UI 的展示,让 View 持有 Model 真的是缺点吗?
+
+
+
+要回答是不是缺点,需要回答2个问题:
+
+1. 何时是合理的设计?
+
+ - View 与 Model 高度绑定
+
+ 如果 GoodsCell 仅用于展示 GoodsModel,且2者生命周期一致,这种直接依赖反而合理,此时 View 与 Model 的耦合是**高内聚** 的体现
+
+ - 避免过度抽象
+
+ 强行解耦,为 View 抽象设计出一个通用接口,可能引入不必要的复杂度。比如为 GoodsCell 设计一个 `id` 协议,在仅有一个 Model 的场景下属于过度设计了
+
+2. 何时可能成为问题?
+
+ - 复用 View 展示不同 Model
+
+ 如果希望 GoodsCell 复用展示其他数据(如 `DiscountGoodsModel`),之前那种 GoodsCell 依赖 GoodsModel 的设计,会导致导致代码难以拓展。此时可以考虑 `ViewModel` 模式或者**协议抽象**解耦
+
+ - Model 频繁变更
+
+ 如果 GoodsModel 的字段,比如 image 频繁变更,所有直接依赖它的 View 都需要同步修改,此时可以将 Model 到 View 的映射逻辑,迁移到独立的转换层,比如 `GoodsModelConverter`
+
+怎么样优化?
+
+1. 可将 Model 到 View 的映射逻辑移到 **独立的转换层**,隔离变化
+
+ 1. `GoodsModel` 结构如下:
+
+ ```objective-c
+ // GoodsModel.h
+ @interface GoodsModel : NSObject
+ @property (nonatomic, copy) NSString *imageName;
+ @property (nonatomic, copy) NSString *name;
+ @property (nonatomic, assign) CGFloat price;
+ @end
+ ```
+
+ 现在需要将 `GoodsModel` 转换为 View 可直接使用的数据(如处理价格格式化、图片加载):
+
+ 实现 GoodsModelConverter
+
+ ```objective-c
+ // GoodsModelConverter.h
+ @interface GoodsModelConverter : NSObject
+
+ // 将 GoodsModel 转换为字典(Key-Value 形式)
+ + (NSDictionary *)convertToDisplayData:(GoodsModel *)model;
+
+ @end
+
+ // GoodsModelConverter.m
+ @implementation GoodsModelConverter
+
+ + (NSDictionary *)convertToDisplayData:(GoodsModel *)model {
+ // 处理价格格式化
+ NSString *priceText = [NSString stringWithFormat:@"¥%.2f", model.price];
+
+ // 加载图片(假设 imageName 是本地资源名)
+ UIImage *image = [UIImage imageNamed:model.imageName];
+
+ return @{
+ @"name": model.name,
+ @"price": priceText,
+ @"image": image
+ };
+ }
+
+ @end
+ ```
+
+ 修改 View 使用转换后的数据
+
+ ```objective-c
+ // GoodsCell.h
+ @interface GoodsCell : UITableViewCell
+ // 不再直接依赖 GoodsModel,而是通过字典传递数据
+ - (void)configureWithDisplayData:(NSDictionary *)displayData;
+ @end
+
+ // GoodsCell.m
+ @implementation GoodsCell {
+ UIImageView *_goodsImageView;
+ UILabel *_nameLabel;
+ UILabel *_priceLabel;
+ }
+
+ - (void)configureWithDisplayData:(NSDictionary *)displayData {
+ _goodsImageView.image = displayData[@"image"];
+ _nameLabel.text = displayData[@"name"];
+ _priceLabel.text = displayData[@"price"];
+ }
+
+ @end
+ ```
+
+ Controller 调用
+
+ ```objective-c
+ // GoodsListViewController.m
+ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
+ GoodsModel *model = self.goods[indexPath.row];
+
+ // 通过 Converter 转换数据
+ NSDictionary *displayData = [GoodsModelConverter convertToDisplayData:model];
+
+ [cell configureWithDisplayData:displayData];
+ return cell;
+ }
+ ```
+
+ 如何应对 Model 的变化?
+
+ 假设 GoodsModel 的 image 字段变为 remoteImageURL,则不需要每处修改 View 使用的地方,统一在 GoodsModelConverter 即可
+
+ ```objective-c
+ // GoodsModelConverter.m
+ + (NSDictionary *)convertToDisplayData:(GoodsModel *)model {
+ // 新增网络图片加载逻辑(伪代码)
+ UIImage *placeholderImage = [UIImage imageNamed:@"placeholder"];
+ UIImage *image = placeholderImage;
+
+ return @{
+ @"name": model.name,
+ @"price": priceText,
+ @"image": image
+ };
+ }
+ ```
+
+ View 和 Controller 无需任何修改。
+
+ - GoodsCell 仍然接收 displayData 字典,不用关心具体来源
+ - Controller 仍然调用 convertToDisplayData 方法
+
+ 更优雅的方案是引入 ViewModel 对象。
+
+2. 使用 ViewModel 模式
+
+ 将 Model 转换为 View 专用的数据结构,View 仅依赖 ViewModel:
+
+ ```objective-c
+ // GoodsCellViewModel.h
+ @interface GoodsCellViewModel
+ @property (nonatomic, readonly) UIImage *image;
+ @property (nonatomic, readonly) NSString *name;
+ - (instancetype)initWithGoods:(GoodsModel *)goods;
+ @end
+
+ // GoodsCell.h
+ @interface GoodsCell
+ @property (nonatomic, strong) GoodsCellViewModel *viewModel;
+ @end
+ ```
+
+ 优点:
+
+ - View 与 Model 解耦,便于复用
+ - ViewModel 可以封装数据转换逻辑(比如多语言本地化、图片加载、字符串格式化拼接)
+
+3. 通过抽象协议依赖
+
+ 定义 View 所需数据的协议,Model 实现该协议。
+
+ 定义 GoodsDisplayable 协议
+
+ ```objective-c
+ // GoodsDisplayable.h
+ @protocol GoodsDisplayable
+ @property (nonatomic, readonly) NSString *goodsImageName;
+ @property (nonatomic, readonly) NSString *goodsName;
+ @end
+
+ // GoodsModel.h
+ @interface GoodsModel : NSObject
+ @end
+
+ // GoodsCell.h
+ @interface GoodsCell
+ @property (nonatomic, strong) id displayData;
+ @end
+ ```
+
+ 在正常商品列表页面,Controller 逻辑为:
+
+ ```objective-c
+ // GoodsListViewController.m
+ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
+ cell.displayData = self.goods[indexPath.row];
+ return cell;
+ }
+ ```
+
+ 在打折商品列表页面,Controller 逻辑为:
+
+ ```objective-c
+ // DiscountGoodsListViewController.m
+ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
+ cell.displayData = self.goods[indexPath.row];
+ return cell;
+ }
+ ```
+
+现在我们可以尝试概括性回答该问题了:
+
+**View 持有 Model 是否是缺点,取决于具体场景:**
+
+- **单一用途、无复用需求**:直接依赖 Model 是合理选择,简化代码且无过度设计。
+- **需复用或 Model 不稳定**:通过 ViewModel 或协议解耦,提高灵活性。
+
+最终,**没有绝对的最佳实践,只有适合场景的权衡**。Apple 的 MVC 变种在简单场景下有效,但在复杂场景需结合其他模式优化。
+
+
+
+
+## MVP 架构
MVP 模式将 Controller 改名为 Presenter,通信改变了通信方向
-
+
1. 各部分之间的通信都是双向的
2. Model 与 View 不发生联系,都通过 Presenter 传递
-3. View 层非常薄。不部署任何业务逻辑,称为“被动视图(Passive View)”,即没有任何主动性,而 Presenter 非常厚,所有的逻辑都部署在这层
-
-如果 Presenter 这一层很厚的话,可以继续拆,比如再增加一个 Interactor 的角色,专门处理处理 View 相关响应事件。这样子角色更多,职责也更清晰。维护也方便。
+3. View 层非常薄。不部署任何业务逻辑,称为“被动视图(Passive View)”,即没有任何主动性
+4. 而 Presenter 非常厚,所有的逻辑都部署在这层。比如在 Presenter 里组装 View 展示到 Controller 的 View 上,加载数据,展示到 View 上。
+5. 如果 Presenter 这一层很厚的话,可以继续拆,比如再增加一个 Interactor 的角色,专门处理处理 View UI 响应事件。这样子角色更多,职责也更清晰。维护也方便。
-## MVVM
+### 错误实现
+
+举个例子:
+
+App 只有一个个人中心 ViewController,其 View 上只有1个展示个人信息的子 View,子 View 上只有1个展示头像 UIImageView 和 1个展示昵称的 UILabel。用 MVP 的思想实现如下
+
+模型为
+
+```objective-c
+@interface PersonalInfoModel : NSObject
+@property (copy, nonatomic) NSString *name;
+@property (copy, nonatomic) NSString *image;
+@end
+
+```
+
+关键就是要增加一个 `Presenter` 的角色,负责组装 View 展示到 Controller 的 View 上,加载数据,展示到 View 上。
+
+```objective-c
+// PersonInfoPresenter.h
+@interface PersonInfoPresenter : NSObject
+- (instancetype)initWithController:(UIViewController *)controller;
+@end
+
+// PersonInfoPresenter.m
+@interface PersonInfoPresenter()
+@property (weak, nonatomic) UIViewController *controller;
+@end
+
+@implementation PersonInfoPresenter
+
+- (instancetype)initWithController:(UIViewController *)controller
+{
+ if (self = [super init]) {
+ self.controller = controller;
+
+ // 创建View
+ PersonalInfoView *personalInfoView = [[PersonalInfoView alloc] init];
+ personalInfoView.frame = CGRectMake(100, 100, 100, 150);
+ personalInfoView.delegate = self;
+ [controller.view addSubview:personalInfoView];
+
+ // 加载模型数据
+ PersonalInfoModel *model = [[PersonalInfoModel alloc] init];
+ model.name = @"杭城小刘";
+ model.image = @"UnixKernel";
+
+ // 赋值数据
+ [personalInfoView setName:app.name andImage:app.image];
+ }
+ return self;
+}
+
+#pragma mark - MJAppViewDelegate
+- (void)personalInfoViewDidClick:(PersonalInfoView *)personalInfoView
+{
+ NSLog(@"presenter 监听了个人信息 View 的点击事件");
+}
+```
+
+PersonalInfoView
+
+```objective-c
+#import
+
+@class PersonalInfoView;
+
+@protocol PersonalInfoViewDelegate
+@optional
+- (void)personalInfoViewDidClick:(PersonalInfoView *)personalInfoView;
+@end
+
+@interface PersonalInfoView : UIView
+- (void)setName:(NSString *)name andImage:(NSString *)image;
+@property (weak, nonatomic) id delegate;
+@end
+
+
+@implementation MJAppView
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ UIImageView *iconView = [[UIImageView alloc] init];
+ iconView.frame = CGRectMake(0, 0, 100, 100);
+ [self addSubview:iconView];
+ _iconView = iconView;
+
+ UILabel *nameLabel = [[UILabel alloc] init];
+ nameLabel.frame = CGRectMake(0, 100, 100, 30);
+ nameLabel.textAlignment = NSTextAlignmentCenter;
+ [self addSubview:nameLabel];
+ _nameLabel = nameLabel;
+ }
+ return self;
+}
+
+- (void)setName:(NSString *)name andImage:(NSString *)image {
+ _iconView.image = [UIImage imageNamed:image];
+ _nameLabel.text = name;
+}
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ if (self.delegate && [self.delegate respondsToSelector:@selector(personalInfoViewDidClick:)]) {
+ [self.delegate personalInfoViewDidClick:self];
+ }
+}
+
+@end
+```
+
+在 ViewController 的使用
+
+```objective-c
+@interface ViewController ()
+@property (strong, nonatomic) PersonInfoPresenter *presenter;
+@end
+
+@implementation ViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ self.presenter = [[PersonInfoPresenter alloc] initWithController:self];
+}
+
+@end
+```
+
+目前实现的效果是:
+
+- Presenter 负责了 View 的创建与展示,同时调用 Model 的能力,从数据库或者网络加载业务数据,然后更新到 View 上。使得 Controller 逻辑更加单一
+- 将大部分 View 的组装逻辑从 Controller 中抽离到 Presenter 中,实现了 Controller 的瘦身
+- Model 和 View 无法感知对方,也无法直接访问,保证了 Model 和 View 的复用能力
+
+
+
+### 正确实现
+
+目前的实现中,Presenter 中拥有了 View,对于 Presenter 的单元测试不好展开,该如何修改?
+
+- View 还是放在 Controller 中创造
+- 为了解耦,将 View UI 事件和数据获取抽成对应的协议
+- Model 依旧是薄薄的一层,数据获取放在 Service 里进行
+
+定义 View 刷新协议
+
+```objective-c
+// PersonalInfoViewProtocol.h
+@protocol PersonalInfoViewProtocol
+- (void)displayName:(NSString *)name image:(NSString *)image;
+@end
+```
+
+View 实现协议
+
+```objective-c
+// PersonalInfoView.h
+@interface PersonalInfoView : UIView
+@property (nonatomic, weak) id delegate;
+@end
+
+// PersonalInfoView.m
+@implementation PersonalInfoView
+// 原有代码不变,但实现 displayName:image: 方法
+- (void)displayName:(NSString *)name image:(NSString *)image {
+ _iconView.image = [UIImage imageNamed:image];
+ _nameLabel.text = name;
+}
+
+- (void)didClickProfileView {
+ if (self.delegate && [self.delegate respondsToSelector:@selector(personalInfoViewDidClick:)]) {
+ [self.delegate personalInfoViewDidClick:self];
+ }
+}
+@en
+```
+
+定义数据拉取协议
+
+```objective-c
+// PersonalInfoServiceProtocol.h
+@protocol PersonalInfoServiceProtocol
+- (void)fetchPersonalInfoWithCompletion:(void (^)(PersonalInfoModel *model))completion;
+@end
+```
+
+定义数据拉取 Services
+
+```objective-c
+// PersonalInfoService.h
+@interface PersonalInfoService : NSObject
+@end
+
+// PersonalInfoService.m
+@implementation PersonalInfoService
+- (void)fetchPersonalInfoWithCompletion:(void (^)(PersonalInfoModel *))completion {
+ // 模拟网络请求
+ dispatch_async(dispatch_get_global_queue(0, 0), ^{
+ PersonalInfoModel *model = [[PersonalInfoModel alloc] init];
+ model.name = @"杭城小刘";
+ model.image = @"UnixKernel";
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(model);
+ });
+ });
+}
+@end
+```
+
+重构 Presenter。loadData 数据加载成功后,调用其 View 的代理方法,刷新 UI
+
+```objective-c
+// PersonInfoPresenter.h
+@interface PersonInfoPresenter : NSObject
+- (instancetype)initWithView:(id)view
+ service:(id)service;
+- (void)loadData;
+@end
+
+// PersonInfoPresenter.m
+@interface PersonInfoPresenter()
+// 重要地方:遵循相应协议的 View 对象
+@property (nonatomic, weak) id view;
+// 重要地方:遵循相应协议的 Services 对象
+@property (nonatomic, strong) id service;
+@end
+
+@implementation PersonInfoPresenter
+
+- (instancetype)initWithView:(id)view
+ service:(id)service {
+ if (self = [super init]) {
+ _view = view;
+ _service = service;
+ }
+ return self;
+}
+
+- (void)loadData {
+ [self.service fetchPersonalInfoWithCompletion:^(PersonalInfoModel *model) {
+ [self.view displayName:model.name image:model.image];
+ }];
+}
+
+#pragma mark - 点击事件处理(可选)
+- (void)handleViewClick {
+ NSLog(@"Presenter 处理点击事件");
+}
+@end
+```
+
+Controller 里组装和创建 Presenter,在 `viewDidLoad` 里面调用 presenter 的 loadData 方法,让其内部的 Services 加载网络数据。
+
+```objc
+// ViewController.m
+@interface ViewController ()
+@property (nonatomic, strong) PersonInfoPresenter *presenter;
+@property (nonatomic, strong) PersonalInfoView *infoView;
+@end
+
+@implementation ViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ // 创建 View
+ self.infoView = [[PersonalInfoView alloc] initWithFrame:CGRectMake(100, 100, 100, 150)];
+ self.infoView.delegate = self;
+ [self.view addSubview:self.infoView];
+
+ // 注入依赖
+ id service = [[PersonalInfoService alloc] init];
+ self.presenter = [[PersonInfoPresenter alloc] initWithView:self.infoView service:service];
+
+ // 触发数据加载
+ [self.presenter loadData];
+}
+
+#pragma mark - PersonalInfoViewDelegate
+- (void)personalInfoViewDidClick:(PersonalInfoView *)view {
+ [self.presenter handleViewClick];
+}
+@end
+```
+
+
+
+关键改动点:
+
+1. **Presenter 不再持有 View**:通过协议与 View 通信,避免直接依赖具体类。
+2. **数据加载抽象为 Service**:Presenter 通过协议调用 Service,便于模拟不同场景。
+3. **布局职责回归 View**:Presenter 不再设置 `frame`,保证单一职责。
+4. **依赖注入**:Presenter 的 Service 和 View 通过初始化注入,测试时可替换为 Mock。
+
+
+
+思考:如何一个复杂的购物车页面包含很多子 View,按照 MVP 模式,该怎么处理?
+
+- **模块化拆分**:每个功能单元独立为 View-Presenter-Service 组合
+
+ 可以把复杂的购物车 view 拆分为独立的几个 View。比如:
+
+ - 商品列表 GoodsListView、商品列表 GoodsListPresenter、商品列表 GoodsListServices
+ - 商品统计信息 GoodsSummaryInfoView、商品统计信息 GoodsSummaryInfoPresenter、商品统计信息 GoodsSummaryInfoServices
+ - 营销活动展示 GoodsPromoptionView、营销活动展示 GoodsPromoptionPresenter、营销活动展示 GoodsPromoptionServices
+ - 每个 View 设计自己对应的 Presenter
+
+- Controller 依旧复杂 View 的组装,和各个 Presenter 的创建工作
+
+- **协调机制**:使用 Coordinator 或响应式编程管理跨模块事件
+
+- 其他流程和单个 View、单个 Presenter 没啥不同。区别在于复杂的业务下,Controller 会存在多个 View、多个 Presenter
+
+
+
+## MVVM 架构
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
-
+
区别在于:采用双向绑定的模式。View 的改变,自动反应在 ViewModel 上。 ViewModel 的改变会发应在 View 上。
MVC 等到业务逻辑很复杂的时候被称为 Massive View Controller (重量级视图控制器)。这样的 Controller 后期维护看着阅读成本很高、不易于测试、维护。当时写这个代码的人离职后,几千行规模的代码在后期添加新功能或者修改 Bug 都是一件折磨人的事情。
-
+
-看到 View 和 ViewController 是不同的技术组件,但是日常开发中它们总是成对的存在,为什么不考虑将他们的连接正规化呢?
+看到 View 和 ViewController 是不同的技术组件,但是日常开发中它们总是成对的存在,为什么不考虑将他们的连接正规化呢?
-
+
典型的 MVC 存在弊端就是 Controller 层非常复杂,很多逻辑都在里面,包括一些不是逻辑的“表示逻辑”(presentation logic)。用 MVVM 术语来说就是将那些 Model 数据转换为 View 可以呈现的东西。例如将一个 NSDate 格式化为一个“2018-11-17” 这样的 NSString。
上图中缺少一个环节,一个专门用来处理所有的表示逻辑。称为 “ViewModel”。位于 ViewController 与 Model 之间。
-
+
MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel。iOS 使用 MVVM 可以降低 ViewController 的复杂性并使得表示逻辑易于测试。
@@ -62,6 +687,169 @@ MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel
+### **MVVM 核心改造点**
+
+1. **引入 ViewModel**:负责数据转换、业务逻辑,通过数据流驱动 UI 更新
+2. **数据双向绑定**:使用 ReactiveCocoa(RAC)建立 View 和 ViewModel 的绑定关系
+3. **事件命令化**:用户交互事件封装为 Command,由 ViewModel 处理
+
+
+
+改造如下:
+
+定义 ViewModel(核心)
+
+```objc
+// PersonalInfoViewModel.h
+#import
+
+@class PersonalInfoModel;
+
+@interface PersonalInfoViewModel : NSObject
+
+// 输出属性(供 View 绑定)
+@property (nonatomic, strong, readonly) RACSignal *nameSignal;
+@property (nonatomic, strong, readonly) RACSignal *imageSignal;
+
+// 输入命令(处理 View 事件)
+@property (nonatomic, strong, readonly) RACCommand *viewDidClickCommand;
+
+// 初始化方法(依赖注入)
+- (instancetype)initWithService:(id)service;
+
+@end
+
+// PersonalInfoViewModel.m
+@interface PersonalInfoViewModel()
+
+@property (nonatomic, strong) id service;
+@property (nonatomic, strong) RACSubject *nameSubject;
+@property (nonatomic, strong) RACSubject *imageSubject;
+
+@end
+
+@implementation PersonalInfoViewModel
+
+- (instancetype)initWithService:(id)service {
+ if (self = [super init]) {
+ _service = service;
+ _nameSubject = [RACSubject subject];
+ _imageSubject = [RACSubject subject];
+
+ // 暴露信号
+ _nameSignal = _nameSubject;
+ _imageSignal = _imageSubject;
+
+ // 初始化命令
+ [self setupCommands];
+ [self loadData];
+ }
+ return self;
+}
+
+#pragma mark - 初始化命令
+- (void)setupCommands {
+ @weakify(self);
+ _viewDidClickCommand = [[RACCommand alloc]
+ initWithSignalBlock:^RACSignal *(id input) {
+ @strongify(self);
+ NSLog(@"ViewModel 处理点击事件");
+ return [RACSignal empty];
+ }];
+}
+
+#pragma mark - 数据加载
+- (void)loadData {
+ @weakify(self);
+ [self.service fetchPersonalInfoWithCompletion:^(PersonalInfoModel *model) {
+ @strongify(self);
+ // 更新数据流
+ [self.nameSubject sendNext:model.name];
+ [self.imageSubject sendNext:[UIImage imageNamed:model.image]];
+ }];
+}
+
+@end
+```
+
+改造 View
+
+```objective-c
+// PersonalInfoView.h(协议不再需要)
+@interface PersonalInfoView : UIView
+
+// 暴露 UI 组件用于绑定
+@property (nonatomic, strong) UIImageView *iconView;
+@property (nonatomic, strong) UILabel *nameLabel;
+
+// 绑定 ViewModel
+- (void)bindViewModel:(PersonalInfoViewModel *)viewModel;
+
+@end
+
+// PersonalInfoView.m
+#import
+
+@implementation PersonalInfoView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ // ... 原有初始化代码不变 ...
+}
+
+- (void)bindViewModel:(PersonalInfoViewModel *)viewModel {
+ // 绑定数据到 UI
+ RAC(self.nameLabel, text) = [viewModel.nameSignal deliverOnMainThread];
+ RAC(self.iconView, image) = [viewModel.imageSignal deliverOnMainThread];
+
+ // 绑定点击事件到 Command
+ @weakify(viewModel);
+ [self addGestureRecognizer:[[UITapGestureRecognizer alloc]
+ initWithTarget:self action:@selector(handleTap)]];
+ [[self rac_signalForSelector:@selector(handleTap)]
+ subscribeNext:^(id x) {
+ @strongify(viewModel);
+ [viewModel.viewDidClickCommand execute:nil];
+ }];
+}
+
+- (void)handleTap {
+ // 空方法,仅用于触发 RAC 信号
+}
+
+@end
+```
+
+改造 ViewController(胶水、组装)
+
+```objective-c
+// ViewController.m
+@interface ViewController ()
+@property (nonatomic, strong) PersonalInfoView *infoView;
+@property (nonatomic, strong) PersonalInfoViewModel *viewModel;
+@end
+
+@implementation ViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ // 创建 View
+ self.infoView = [[PersonalInfoView alloc] initWithFrame:CGRectMake(100, 100, 100, 150)];
+ [self.view addSubview:self.infoView];
+
+ // 创建 Service 和 ViewModel
+ id service = [[PersonalInfoService alloc] init];
+ self.viewModel = [[PersonalInfoViewModel alloc] initWithService:service];
+
+ // 绑定 ViewModel
+ [self.infoView bindViewModel:self.viewModel];
+}
+
+@end
+```
+
+
+
## 一个简单的例子
PersonModel
@@ -168,3 +956,24 @@ SpecBegin(Person)
SpecEnd
```
+
+
+## VIPER 架构
+
+`View-Interactor-Presenter-Entity-Routing` ,一种基于单一职责原则和清晰的模块界限的架构模式,包含五个主要组成部分:
+
+- View(视图层):负责显示用户界面和接收用户输入。它将用户输入传递给 Presenter
+- Presenter(演示者):将从 View 层获取指令转到 Interactor 处理并接收处理后的数据,并将其格式化为适合 View 显示的格式,然后将这些数据传递给 View 去显示更新
+- Interactor(交互器):负责处理具体的业务逻辑。它获取数据(可能来自网络、数据库等),处理业务规则和数据,并将结果传递给Presenter
+- Entity(实体层):应用的基本数据对象,类似 MVC 架构中的 Model 层,例如数据库对象或网络请求的 JSON 对象等
+- Routing(路由层):负责页面之间的导航逻辑跳转
+
+VIPER 架构的优点在于模块职责划分清晰,模块间的耦合度低,特别适合大项目的开发, 其主要缺点是由于层的划分较多,增加了代码复杂性,对于小型项目而言可能会显得过度设计
+
+这么看来 VIPER 很像前端中 Redux 的设计:
+
+- VIPER 相比 Redux 简化了 UI 事件 Action 和 ActionCreator 这个角色
+- Interactor 做的事情类似 Reducer ,都会根据对应的事件类型,判断如何处理数据。区别在于前端约定 Reducer 的实现必须是纯函数
+
+除了角色不同外,VIPER 和 Redux 对于 UI 展示和事件响应、处理的整个流程很类似。所以可以理解为「 VIPER 是 Redux 在客户端的简易实现」。
+
diff --git a/Chapter1 - iOS/1.60.md b/Chapter1 - iOS/1.60.md
index 7fb5d6b..6c5f090 100644
--- a/Chapter1 - iOS/1.60.md
+++ b/Chapter1 - iOS/1.60.md
@@ -10,7 +10,7 @@ App 的包大小做优化的目的就是为了节省用户流量,提高用户
-App 瘦身一般指的是安装包(IPA),主要由可执行文件、资源组成。
+App 瘦身一般指的是安装包(IPA),主要由**可执行文件、资源组成**。
对于产物的分析,可以查看可执行文件的具体组成。
@@ -32,7 +32,7 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术
### 1.1 Slicing
-
+
当向 App Store Connect 上传 .ipa 后,App Store Connect 构建过程中,会自动分割该 App,创建特定的变体(variant)以适配不同设备。然后用户从 App Store 中下载到的安装包,即这个特定的变体,这一过程叫做 Slicing。
@@ -42,7 +42,7 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术
其中,2x 和 3x 的细分,要求图片在 **Assets** 中管理。Bundle 内的则会同时包含。
-
+
### 1.2 Bitcode
@@ -66,9 +66,9 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
在上传到 App Store 时需勾选“Includ app symbols for your application...”。勾选之后 Apple 会自动生成对应的 dYSM,然后可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下载对应的 dYSM 来进行符号化
-
+
-
+
那么 Bitcode 会对 App Thining 有什么作用?
@@ -83,7 +83,7 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
-
+
应用场景:相机应用的贴纸或者滤镜、关卡游戏等
@@ -101,7 +101,7 @@ on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,
包体积,评判标准是以 App Store 上看到的为准。但是上传到 App Store Connect 处理完后,会自动帮我们生成具体设备上看到的大小。如下:
-
+
这其中:又可以分为2类: Universal 和具体设备
Universal 指通用设备,即未应用 App slicing 优化,同时包含了所有架构、资源。所以包体积会比较大
@@ -245,13 +245,13 @@ self.imageView.image = images.lastObject;
Timeprofile-imageNamedFromAssets
-
+
TimeProfile-imageWithContentsOfFile
-
+
Timeprofile-UIImageNamedFromFolder
-
+
Images.xcassets :
@@ -276,7 +276,7 @@ CocoPods 中两种资源引用方式介绍下:
说说项目中的情况吧:在工程中之前是通过 resource_bundles 引用资源的。资源是放在 Resources 目录下的图片引用。查询资料后说「如果图片资源放到 .xcasset 里面 Xcode 会帮我们自动优化、可以使用 Slicing 等(这里不仅仅指的是 resource_bundle 下的 xcassets」。所以动手将各个 Pod 库里面的图片全都通过 Assets Catalog 的方式进行处理。
-
+
步骤:
@@ -433,14 +433,51 @@ Symbols Hidden by Default 会把所有符号都定义成”private extern”,
在 iOS微信安装包瘦身 一文中,有提到:
-> 去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,并且Other C Flags添加-fno-exceptions,可执行文件减少了27M,其中__gcc_except_tab段减少了17.3M,__text减少了9.7M,效果特别明显。可以对某些文件单独支持异常,编译选项加上-fexceptions即可。但有个问题,假如ABC三个文件,AC文件支持了异常,B不支持,如果C抛了异常,在模拟器下A还是能捕获异常不至于Crash,但真机下捕获不了(有知道原因可以在下面留言:)。去掉异常后,Appstore 后续几个版本 Crash 率没有明显上升。
+> 去掉异常支持,Enable C++ Exceptions 和 Enable Objective-C Exceptions 设为 NO,并且 Other C Flags 添加 `-fno-exceptions`,可执行文件减少了27M,其中 __gcc_except_tab 段减少了17.3M,__text 减少了 9.7M,效果特别明显。可以对某些文件单独支持异常,编译选项加上 `-fexceptions` 即可。但有个问题,假如 ABC 三个文件,AC 文件支持了异常,B 不支持,如果 C 抛了异常,在模拟器下 A 还是能捕获异常不至于 Crash,但真机下捕获不了。去掉异常后,Appstore 后续几个版本 Crash 率没有明显上升。
-个人认为关键路径支持异常处理就好,像启动时NSCoder读取setting配置文件得要支持捕获异常,等等
+个人认为关键路径支持异常处理就好,像启动时 NSCoder 读取 setting 配置文件得要支持捕获异常,等等
看这个优化效果,感觉发现了新大陆。关闭后验证.. 毫无感知,基本没什么变化。
可能和项目中用到比较少有关系。故保持开启状态。
+
+
+潜在问题与解决方案
+
+问题 1:依赖异常的标准库组件失效
+
+- 现象:如 `std::vector` 在内存不足时直接崩溃。
+- 解决:
+ - 使用 `std::nothrow` 分配内存。
+ - 替换为无异常的容器实现(如自定义或第三方库)。
+
+问题 2:第三方库依赖异常
+
+- 现象:链接时报错(如库中未定义异常相关符号)。
+- 解决:
+ - 重新编译第三方库,确保其也启用 `-fno-exceptions`。
+ - 隔离异常代码,通过 C 接口封装调用。
+
+问题 3:代码中残留 `try`/`catch`
+
+- 现象:编译错误 `error: exception handling disabled`。
+- 解决:
+ - 全局搜索并删除所有异常处理代码。
+ - 使用宏或条件编译隔离异常代码(不推荐)。
+
+
+
+替代方案:若需保留部分异常逻辑但优化性能,可考虑**局部禁用异常**:通过 `#pragma clang exception_behavior disable` 或函数级属性控制。
+
+```c++
+#pragma clang exception_behavior disable
+void criticalFunction() {
+ // 此函数内禁用异常处理
+}
+#pragma clang exception_behavior enable
+```
+
#### 3.1.8 Link-Time Optimization
Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码;这个优化也是高效的,因为可以在全局视角下优化代码。
@@ -464,6 +501,8 @@ Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间
代码的优化,即通过删除无用类、无用方法、重复方法等,来达到可执行文件大小的减小。
而如何筛选出符合条件的无用类、方法,则需要通过一些工具来完成(fui)
+**编写 LLVM 插件检测处重复代码,未被调用的代码。**
+
扫描无用代码的基本思路都是查找已经使用的方法/类和所有的类/方法,然后从所有的类/方法当中剔除已经使用的方法/类剩下的基本都是无用的类/方法,但是由于 Objective-C 是动态语言,可以使用字符串来调用类和方法,所以检查结果一般都不是特别准确,需要二次确认。目前市面上的扫描的思路大致可以分为 3 种:
- 基于 Clang 扫描
@@ -480,7 +519,8 @@ Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间
- 工程师确认后,删除即可
LinkMap 文件分为3部分:Object File、Section、Symbols。
-
+
+
- Object File:包含了代码工程的所有文件
- Section:描述了代码段在生成的 Mach-O 里的偏移位置和大小
@@ -489,7 +529,9 @@ LinkMap 文件分为3部分:Object File、Section、Symbols。
先说说如何快速找到方法和类的全集?
我们可以通过 **LinkMap** 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里面的 **Write Link Map File** 设置为 YES,然后指定 **Path to Link Map File** 的路径就可以得到每次编译后的 LinkMap 文件了。
-
+
+
+产出的 LinkMap 阅读起来比较累,github 有个[可视化项目](https://github.com/jayden320/LinkMap) 用来查看 LinkMap 文件。
#### 3.2.1 基于 clang 扫描
@@ -499,11 +541,11 @@ LinkMap 文件分为3部分:Object File、Section、Symbols。
上面我们得知可以通过 LinkMap 统计出所有的类和方法,还可以清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
-
+
-
+
-
+
得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
@@ -520,7 +562,7 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
前置条件:先运行项目,在生成的 Products 目录下的 BridgeLabiPhone.app 解压,取出对应的和工程同名的 BridgeLabiPhone。然后运行上面的 Github 项目。可以看到运行了一个 Mac App。点击顶部的菜单栏里面的 File->Open。选择电脑上的 BridgeLabiPhone.app 选择里面的 BridgeLabiPhone。见下图
-
+
由于 Objective-C 是一门动态语言,所以检测出的结果仍旧需要我们2次确认。
@@ -534,7 +576,7 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。
-
+
说明:AppCode检测出了实际上需要的大部分场景的问题,但是由于 Objective-C 是一门动态性语言,所以 AppCode 检测出无用的方法等都需要工程师自己再次确认后删除。(在我们的工程中有一些和 H5 交互的桥接方法,因此 AppCode 视为 Unused Method,但是你删除的话,那就自己哭去吧 😭)。实际经验告诉我,使用 AppCode 的时候如果工程比较大,则整个 code inspect 会非常耗时(给你打个预防针哦,笔芯)
@@ -642,7 +684,7 @@ lipo create libWeiboSDK-armv7.a libWeiboSDK-arm64.a -output libWeiboSDK.device.a
DebuggingWith Arbitrary Record Formats 是 ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式,.dSYM 文件中真正保存符号表数据的是 DWARF 文件。DWARF 文件中不同的数据都保存在相应的 section 中。
最后的一个对比效果图:
-
+
总结:瘦身技术常见操作就这些,但是维持应用包体积的瘦身却是一个观念,从日常开发到线上发布都需要有这个意识。这样当你在写代码的时候就会考虑同样一个效果,你的具体实现手段是怎么样的。比如为了一个稍微炫酷的效果就要引入一个很大的三方库,有了“瘦身”的意识,你很大可能就是自己动手撸一个代码。比如一些无用资源的管理方式、有用的图片资源的高效管理方式等等。有了意识,行动自然会往这个方面去靠。(😂大道理一套一套的。我也不想的,毕竟是playboy)
diff --git a/Chapter1 - iOS/1.7.md b/Chapter1 - iOS/1.7.md
index 4afe5cd..f652e80 100644
--- a/Chapter1 - iOS/1.7.md
+++ b/Chapter1 - iOS/1.7.md
@@ -12,7 +12,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。
-
+
@@ -58,7 +58,7 @@ struct NSObject_IMPL {
因此可以知道,OC 的类底层是由 c/c++ 的继承实现的。
-
+
由于 obj 对象没有任何属性和方法,只有一个 isa 指针,且类的本质就是结构体,所以当结构体只有1个成员时,该成员的地址值,就是该结构体的地址。
@@ -118,7 +118,7 @@ struct Student_IMPL {
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
-
+
@@ -126,7 +126,7 @@ struct Student_IMPL {
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
-
+
发现是可以正确访问的。
@@ -193,7 +193,7 @@ struct Student_IMPL {
为什么 `class_getInstanceSize([Person class])` 也是16,不是8+4吗?因为存在内存对齐,结构体的大小必须是最大成员大小的倍数(Person 中也就是8的倍数)
-
+
@@ -330,12 +330,12 @@ int main(int argc, const char * argv[]) {
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
-
+
**结论**
-
-
+
+
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
@@ -530,13 +530,64 @@ size_t instanceSize(size_t extraBytes) {
- `class_getInstanceSize([NSObject class])` :8,返回实例对象内存对齐后的的成员变量所占用的内存大小(即代码注释的 `Class's ivar size rounded up to a pointer-size boundary.` ),一个空对象,只有 isa 指针,所以只有8字节。可以理解为 **创建一个对象,至少需要多少内存**
- `malloc_size((__bridge const void *)obj)` :16,Apple 规定,对象至少16个字节。但是只有一个 isa,所以只占用8个字节。
- 内存对齐:结构体的最终大小必须是最大成员的倍数。可以理解为**创建一个对象,实际上分配了多少内存**
+
+- 内存对齐:结构体的最终大小必须是最大成员的倍数。可以理解为**创建一个对象,实际上分配了多少内存**
- 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
- 但 NSObject 对象内部只使用了8个字节的空间(64位环境下,通过 class_getInstanceSize 函数获得)
+
+
+```objective-c
+@interface Person : NSObject
+@end
+
+Person *person = [[Person alloc] init];
+NSLog(@"%zd", class_getInstanceSize([person class])); // 8
+NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16
+```
+
+
+
+为对象增加2个属性呢
+
+```objective-c
+@interface Person : NSObject
+@property (nonatomic, assign) int age;
+@property (nonatomic, assign) int height;
+@end
+
+Person *person = [[Person alloc] init];
+NSLog(@"%zd", class_getInstanceSize([person class])); // 16
+NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16
+
+```
+
+
+
+创建一个实例对象,至少需要多少内存?
+
+```objective-c
+#import
+class_getInstanceSize([Person class])
+sizeOf()
+```
+
+创建一个实例对象,实际上分配了多少内存?
+
+```objective-c
+#import
+malloc_size((__bridge const Person *)obj)
+```
+
+占用内存,只需要遵循结构体内存对齐规则即可:即结构体成员变量内存最大的整数倍数
+
+实际内存,除了考虑结构体内存对齐规则以外,还需要考虑系统为了内存访问速度而设计的内存分配 buckets 大小。
+
+
+
## 五、属性和方法
```objective-c
@@ -563,7 +614,17 @@ struct Person_IMPL {
};
```
-我们知道 `@property` 的本质是生成一个带下划线的 ivar,即 `_height`,还有 height 的 getter、setter 方法。为什么在结构体里没有看到方法?因为对象可以存在多个,这些方法的实现需要公用,没必要每个对象里都保存一份。
+我们知道 `@property` 的本质是生成一个带下划线的 ivar,即 `_height`,还有 height 的 getter、setter 方法。
+
+为什么在结构体里没有看到方法?
+
+1. 内存优化:
+ - 因为对象可以存在多个,这些方法的实现需要公用,没必要每个对象里都保存一份。如果方法存储在实例中,1000 个实例会有 1000 份相同的方法代码,导致内存浪费。
+ - **共享方法实现是面向对象语言的通用设计**(如 C++、Java)
+2. **动态性支持**:
+ - 方法存储在类对象中,使得 Objective-C 的**运行时方法替换**(Method Swizzling)成为可能。例如,可以通过 `class_replaceMethod` 动态修改类的方法实现,所有实例立即生效。
+3. **继承与多态**:
+ - 子类可以重写父类方法,方法查找会沿着类继承链进行,这种机制依赖于方法存储在类对象中。
@@ -707,7 +768,7 @@ iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内
GUN 都存在内存对齐这个概念。
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
-
+
@@ -718,21 +779,21 @@ GUN 都存在内存对齐这个概念。
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
-
+
instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用
class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。
-
+
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。
-
+
当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。
-
+
```objectivec
@interface Student : NSObject
@@ -753,7 +814,7 @@ class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 is
@end
```
-奇怪的是,我们给 Student 类对象调用 test 方法,`[Student test]` 则调用成功。是不是很奇怪?站在面向对象的角度出发,Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类对象,所以根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 `-(void)test` 对象方法?
+奇怪的是,我们给 Student 类对象调用 test 方法,`[Student test]` 则调用成功。是不是很奇怪?站在面向对象的角度出发,Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类方法,所以再继续根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 `-(void)test` 对象方法?
因为NSObject 元类对象的 superClass 继承自 NSObject 的类对象,类对象是存储对象方法的,所以定义在 NSObject 分类中的 `-(void)test` 最终会被调用。
@@ -778,7 +839,7 @@ struct mock_object_class *student = (__bridge mock_object_class *)[[Student allo
```
如何查看类真正的结构?在 Xcode 中打印出来
-思路:查看 Class 内部的数据,发现是 struct,所以我们自己定义一个 struct,去承接类对象的元类对象信息
+思路:查看 Class 内部的数据,发现是 struct,所以我们查看源码,自己定义一个 struct,去承接类对象的元类对象信息
```c
#import
@@ -916,7 +977,11 @@ public:
};
#endif /* MockClassInfo_h */
+```
+使用用
+
+```objective-c
Student *stu = [[Student alloc] init];
stu->_weight = 10;
@@ -930,6 +995,10 @@ class_rw_t *studentMetaClassData = studentClass->metaClass()->data();
class_rw_t *personMetaClassData = personClass->metaClass()->data();
```
+可以看到 Xcode 报错了,因为是 `main.m` 引入了 `MockClassInfo.h` ,会当作 OC 去编译。为了解决编译报错,将 `main.m` 改为 `main.mm` ,变成为 objective-c++ 文件,支持 OC 和 C++ 混编。
+
+
+
## 六、 内存对齐
@@ -937,7 +1006,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data();
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
1. 提高访问速度:内存对齐可以使数据在内存中的存储更加高效,因为大部分计算机体系结构都要求数据按照特定的边界对齐,这样可以减少内存访问的次数,提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
-
+
2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问,只能访问对齐后的内存地址,否则会报异常。
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARM,MIPS,和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。
@@ -990,9 +1059,26 @@ NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32
```
-`isa 指针8字节` + `int _age 4字节` + `_height 4字节` + `_no 4 字节` = 20 字节,因为存在内存对齐,因为结构体本身对齐内存对齐,必须为8的倍数,所以占据24个字节的内存。结构体成员变量,内存对齐时,对齐基数必须是各个成员变量中最大字节数的一个。
+QA:`isa 指针8字节` + `int _age 4字节` + `_height 4字节` + `_no 4 字节` = 20 字节。
-结构体占据24字节,为什么运行起来后通过 `malloc_size` 得到32个字节?这个涉及到运行时内存对齐。规定 **iOS 中内存对齐以 16 的倍数为准**。
+结构体内存对齐规则:结构体的总大小必须是**最大成员对齐基数**的倍数。也就是 isa 8个字节的整数倍。所以占据24个字节的内存。**结构体成员变量,内存对齐时,对齐基数必须是成员变量最大一个的字节数的倍数**。
+
+比如 `Person_IMPL` 结构体的最大成员变量是 isa,大小为8,所以内存对齐时,结构体占用内存必须是8的倍数。内存占用为8的倍数,实际大小为20,但为了内存对齐,则为24
+
+QA:结构体占据24字节,为什么运行起来后通过 `malloc_size` 得到32个字节?
+
+这个涉及到运行时内存对齐。规定 **iOS 中内存对齐以 16 的倍数为准**。内存分配器的底层策略:
+
+大多数系统的内存分配器(如 macOS/iOS 的 `malloc`)会按 **特定大小的块(Bucket)** 分配内存,以提高性能和减少碎片。例如:
+
+- 如果请求的内存大小在 **16~32 字节** 之间,分配器可能直接分配 **32 字节**。
+- 这种策略称为 **Size Class**,目的是减少内存碎片和管理
+
+系统原理:
+
+- malloc 的实现:macOS/iOS 使用 `libmalloc`,其内存块按 **16 字节粒度** 分配。
+- **`malloc_size` 的行为**:返回系统实际分配的大小,而非用户请求的大小。
+- **内存对齐的硬件原因**:CPU 访问对齐的内存地址效率更高,未对齐可能导致性能损失或崩溃。
@@ -1017,10 +1103,11 @@ NSLog(@"%zd", malloc_size(temp));
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看
-
+
可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT`
在 i386 里面是16,在非 i386 里面有个判断
+
```c
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 * SIZE_SZ)
@@ -1031,7 +1118,7 @@ NSLog(@"%zd", malloc_size(temp));
# define INTERNAL_SIZE_T size_t
```
在 Xcode 打印输出, `__alignof__ (long double)` 为16,`sizeof(size_t)` 为8,即 `2 * SIZE_SZ` = 16,所以不管怎么看,在 GUN 里面内存对齐一定都是16.
-
+
Todo: 研究探索 libmalloc 源码
@@ -1055,6 +1142,24 @@ NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5); // 0x7ff85ca74270 0x7ff8
- cls1...cls5 都是 NSObject 的 class 对象,也就是类对象。
- 它都是同一个对象,每个类在内存中有且只有一个 class 对象
+```objective-c
+Person *person = [[Person alloc] init];
+NSLog(@"%zd", class_getInstanceSize([person class])); // 24
+NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32
+
+// 类对象
+Person *p = objc_getClass("Person");
+Class pCls0 = [person class];
+Class pCls1 = [Person class];
+Class pCls11 = [[[Person class] class] class];
+// 元类对象
+Class pcls2 = object_getClass([Person class]);
+Class pcls22 = object_getClass(object_getClass(person));
+
+NSLog(@"%p %p %p %p %p %p", p, pCls0, pCls1, pCls11, pcls2, pcls22);
+// 0x100008268 0x100008268 0x100008268 0x100008268 0x100008240 0x100008240
+```
+
Class 对象在内存中存储的信息主要包括:
@@ -1176,13 +1281,13 @@ Class objc_getClass(const char *aClassName)
struct NSObject_IMPL {
class isa;
}
-
+
struct Person_IMPL {
struct NSObject_IMPL NSObject_IAVRS;
int _age;
int _height;
}
-
+
//
struct Person_IMPL {
class isa;
@@ -1193,3 +1298,50 @@ Class objc_getClass(const char *aClassName)
- Class 对象:属性信息、对象方法信息、成员变量信息、协议信息、superclass、isa 存放在类对象中。
- Meta-Class 对象:类方法信息,存放在元类对象中。
+
+
+## 总结
+
+
+
+1. 实例对象的 isa 指针指向类对象
+2. 类对象的 isa 指针指向元类对象
+3. 元类对象的 isa 指针指向基类对象的元类对象
+4. 基类的元类对象的 isa 指针指向基类的类对象
+5. 同一个类的多个实例对象只有1个类对象
+6. 同一个类的多个实例对象只有1个元类对象
+7. 如果一个类美元父类,比如 NSObject 类,则 superclass 指针为 nil
+8. 实例对象的 isa 地址,按位与 `ISA_MASK` 得到类对象的地址
+9. 类对象的 isa 地址,按位与 `ISA_MASK` 得到元类对象的地址
+
+对象的 isa 指针指向哪里?
+
+- 实例对象(instance)的 isa 指针指向类对象(class 对象)
+- 类对象(class 对象) 的 isa 指针指向元类对象(meta-class 对象)
+- 元类对象(meta-class 对象) 的 isa 指针指向基类的元类对象(meta-class 对象)
+
+OC 的类信息存放在哪里?
+
+```objective-c
+@interface Student : Person {
+ int _no;
+}
+@property (nonatomic, assign) int score;
+
+
+- (void)study;
+
++ (void)live;
+
+@end
+```
+
+- 对象方法、属性、成员变量、协议信息存储在类对象(class 对象)中
+
+ 比如 `-(void)study` 方法、score 属性、`_no` 成员变量,`NSCopying` 协议
+
+- 类方法,存储在元类对象(meta-class 对象)中
+
+ 比如 `+(void)live` 方法
+
+- 成员变量的具体值,存放在实例对象的内存中。比如 student1 的 score 为30,student2 的 score 为 90
diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md
index 3f92775..0730290 100644
--- a/Chapter1 - iOS/1.74.md
+++ b/Chapter1 - iOS/1.74.md
@@ -22,11 +22,11 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 1. 屏幕绘制原理
-
+
讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式,从上到下一行行扫描,扫面完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;当一帧画面绘制完成后,电子枪恢复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical synchronization),简称 VSync。显示器通常以固定的频率进行刷新,这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
-
+
通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要显实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPU,GPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
@@ -36,7 +36,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
为了解决这个问题,GPU 通常有一个机制叫垂直同步信号(V-Sync),当开启垂直同步信号后,GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源
-
+
答疑
@@ -48,7 +48,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
揭秘。请看下图
-
+
当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
@@ -56,7 +56,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 2. 卡顿产生的原因
-
+
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
@@ -75,7 +75,7 @@ RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、
RunLoop 状态如下图
-
+
第一步:通知 Observers,RunLoop 要开始进入 loop,紧接着进入 loop
@@ -251,9 +251,9 @@ if (sourceHandledThisLoop && stopAfterHandle) {
}
```
-完整且带有注释的 RunLoop 代码见[此处](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
+完整且带有注释的 RunLoop 代码见[此处](./../assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
-
+
RunLoop 6 个状态
```Objective-C
@@ -286,13 +286,13 @@ WatchDog 在不同状态下具有不同的值。
通过 `long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)` 方法判断是否阻塞主线程,`Returns zero on success, or non-zero if the timeout occurred.` 返回非 0 则代表超时阻塞了主线程。
-
+
可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting?因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
Runloop 检测卡顿流程图如下:
-
+
关键代码如下:
@@ -371,7 +371,7 @@ while (self.isCancelled == NO) {
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
维基百科搜索到 “Call Stack” 的一张图和例子,如下
-
+
上图表示为一个栈。分为若干个栈帧(Frame),每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B,调用函数 B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。
@@ -464,11 +464,11 @@ static mach_port_t main_thread_id;
这里有个策略,卡顿是由于主线程某个或某组方法执行过长而造成的卡顿,所以卡顿堆栈只需要分析主线程即可,但是此时是未符号化的方法(完成流程如下)
-
+
测试过,单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。
-
+
按照栈顶函数地址和当前堆栈的深度作为判断依据,如果某个堆栈栈顶地址和深度相同,则出现次数最多的一个堆栈,可以认为是一个卡顿堆栈,因为认为当发生卡顿的时候,函数堆栈大概率不会变化。
@@ -505,7 +505,7 @@ static mach_port_t main_thread_id;
上传这些信息到服务端后,APM 后端调用符号化服务的能力,符号化机器找到对应的 DSYM 文件,调用 atos 的指令进行符号化,如下效果。
-
+
系统堆栈符号化的问题,也就是获取系统符号文件的问题。可以通过 ipsw,也就是刷机所需要的固件信息。
@@ -517,7 +517,7 @@ static mach_port_t main_thread_id;
服务端聚合策略
-
+
找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 key。
@@ -545,13 +545,94 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod
方法调用的深度约定为100,现在方法调用层级不会那么深,如果真有那么多节点都是很小的子节点,排查意义不大,所以阈值没必要设置太大。
+### 7. 卡顿优化
+
+#### CPU
+
+- 尽量使用轻量级的对象,比如用不到事件处理的地方,就考虑用 CALayer 取代 UIView
+
+- 不要频繁的调用 UIView 相关属性,比如 frame、bounds、transform 等属性,尽量减少不必要的修改
+
+- 尽量提前计算好布局,在有需要的时候一次性调整对应的属性,不要多次修改属性
+
+- Autolayout 会比直接设置 frame 消耗更多的 CPU 资源
+
+ AutoLayout 的机制与开销
+
+ - **约束解析**:AutoLayout 基于约束(Constraints)构建线性方程组,通过 **Cassowary 算法** 求解视图的最终布局。这个过程涉及较多的数学计算(尤其是视图层级复杂时)。
+ - **布局传递**:当视图层级变化时,AutoLayout 会触发 **布局传递(Layout Pass)**,包括 `updateConstraints`、`layoutSubviews`、`drawRect` 等阶段,可能多次递归调用。
+ - **性能瓶颈**:
+ - 复杂层级:视图嵌套层级越深,约束数量越多,计算量呈指数级增长。
+ - 动态修改约束:频繁添加/删除约束(如动画中)会导致重复布局计算。
+
+ 苹果对 AutoLayout 的性能进行了持续优化(如 iOS 12 引入的 **Cassandra 引擎**),显著减少了约束解析的开销
+
+ 所以,可以创建 DataModel.frameModel 中计算控件布局信息。
+
+ 再比如 `FDTemplateLayoutCell` 库。
+
+- 图片的 size 最好跟 UIImageView 的 size 一致,否则会拉伸或者压缩
+
+- 控制线程的最大并发数量。
+
+- 尽量把耗时操作放到子线程中,完成后在主线程更新 UI。比如 Texture 框架
+
+ - 文本处理(尺寸计算、绘制)
+ - 图片处理(解码、绘制 )
+
+#### GPU
+
+- 尽量减少视图的层次和数量
+
+- 尽量避免短时间内大量图片的显示,尽可能将多张图片合并成一张进行展示
+
+- GPU 能处理的最大纹理尺寸是 4096*4096,一旦超过这个尺寸,就会占用 CPU 资源进行处理,所以纹理尽量不要超过这个尺寸
+
+- 减少透明的视图(alpha < 1),不透明就设置 opaque 为 YES
+
+ - 因为 alpha 透明度导致 GPU 需要实时混合多个图层,增加计算负担
+
+ Demo:两个透明视图叠加的混合计算
+
+ 假设有一个蓝色半透明视图(`alpha = 0.5`)覆盖在一个红色视图上:
+
+ 1. 底层红色视图:颜色值为 `RGB(1.0, 0, 0)`。
+ 2. 上层蓝色视图:颜色值为 `RGB(0, 0, 1.0)`,`alpha = 0.5`。
+ 3. 混合计算:
+ - **最终颜色 = 上层颜色 × alpha + 下层颜色 × (1 - alpha)**
+ - 结果 = `(0, 0, 1.0) * 0.5 + (1.0, 0, 0) * 0.5 = RGB(0.5, 0, 0.5)`。
+
+ **关键点**:每个像素都需要实时计算,这对 GPU 来说是额外的负担。
+
+ - 在视觉效果和性能之间权衡,优先保证用户交互流畅性。比如
+
+ - 优先使用不透明颜色替代透明度:用 `RGB(0.9, 0.9, 0.9)` 代替 `white.withAlphaComponent(0.9)`。让设计师提供不透明的近似色值
+ - 如果视图完全不透明,设置 `view.isOpaque = true`,这会提示系统跳过混合计算。
+ - 合并视图层级,避免多层透明视图嵌套。用单个 `UILabel` 代替 `UIView(背景半透明) + UILabel`
+
+- 尽量避免出现离屏渲染
+
+ - 需要创建新的缓冲区
+ - 离屏渲染的整个过程,需要多次切换上下文环境,先从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束后,再将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
+ - 离屏渲染的代价是普通渲染的 2 倍以上(读写两次缓冲区)。
+
+
+
+
+
## 二、 App 启动时间监控
+### 0. Xcode 查看简易启动时间
+
+Xcode 增加环境变量,添加 `DYLD_PRINT_STATISTICS` 设为1,来查看 pre-main 各个阶段的耗时情况
+
+如果需要更详细的信息,可以增加 `DYLD_PRINT_STATISTICS_DETAILS` 为1来查看数据。
+
### 1. 简易版App 启动时间的监控
应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。
-
+
冷启动:App 尚未运行,必须加载并构建整个应用。完成应用的初始化。冷启动存在较大优化空间。冷启动时间从 `application: didFinishLaunchingWithOptions:` 方法开始计算,App 一般在这里进行各种 SDK 和 App 的基础初始化工作。
@@ -573,6 +654,8 @@ uint64_t timelapse = mach_absolute_time() - g_apmmLoadTime;
double timeSpan = (timelapse * g_apmmStartupMonitorTimebaseInfoData.numer) / (g_apmmStartupMonitorTimebaseInfoData.denom * 1e9);
```
+
+
### 2. 线上监控启动时间就好,但是在开发阶段需要对启动时间做优化。
要优化启动时间,就先得知道在启动阶段到底做了什么事情,针对现状作出方案。
@@ -586,10 +669,10 @@ App 启动过程:
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching();
Pre-Main 阶段
-
+
Main 阶段
-
+
#### 2.1 加载 Dylib
@@ -620,8 +703,10 @@ Main 阶段
优化:
-- 使用 `+initialize` 代替 `+load`
-- 不要使用过 attribute\*((constructor)) 将方法显示标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_one、pthread_once() 或 std::once()。也就是第一次使用时才初始化,推迟了一部分工作耗时也尽量不要使用 c++ 的静态对象
+- 用 `+initialize` 方法和 `dispatch_once` 取代所有的 `attribute *((constructor))`、c++ 静态构造器、Objc 的 `+load` 方法
+- 不要使用过 `attribute *((constructor))` 将方法显示标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_one、pthread_once() 或 std::once()。也就是第一次使用时才初始化,推迟了一部分工作耗时也尽量不要使用 c++ 的静态对象
+
+QA:为什么 `+ initialize` 需要搭配 `dispatch_once`?因为 `+initialize` 可能执行多次
#### 2.4 pre-main 阶段影响因素
@@ -650,6 +735,8 @@ Main 阶段
#### 2.5 main 阶段优化
+在不影响用户体验的情况下,尽可能将一些逻辑延迟,不要全部放在 `finishLaunching` 方法中。
+
- 减少启动初始化的流程。能懒加载就懒加载,能放后台初始化就放后台初始化,能延迟初始化的就延迟初始化,不要卡主线程的启动时间,已经下线的业务代码直接删除
- 优化代码逻辑。去除一些非必要的逻辑和代码,减小每个流程所消耗的时间
- 启动阶段使用多线程来进行初始化,把 CPU 性能发挥最大
@@ -673,9 +760,9 @@ Main 阶段
### 4. 精确版启动时间监控
-
+
-
+
进程创建:通过 sysctl 可以拿到
@@ -685,7 +772,7 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到
首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。
-
+
对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree,通过 IPC 发送给 Render Server。
@@ -697,21 +784,21 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到
- Layout 布局:调用 `layout` 等与布局相关的 API
-
+
-
+
- Display 绘制:调用 `drawRect` 等与绘制相关方法
-
+
- Prepare:图片解码
- Commit:递归打包 Render Tree,通过 IPC 计数发送给 Render Server
-
-
-
+
+
+
断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。
@@ -919,14 +1006,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使
Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。
-
+
- Clean Memory
Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework(每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling,那么变为 dirty)。
一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件,也是只读的、clean page。
- 
+ 
- Dirty Memory
@@ -934,7 +1021,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
在使用 framework 的过程中会产生 Dirty memory,使用单例或者全局初始化方法有助于帮助减少 Dirty memory(因为单例一旦创建就不销毁,一直在内存中,系统不认为是 Dirty memory)。
- 
+ 
- Compressed Memory
@@ -945,7 +1032,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize
设备不同,内存占用上限不同,App 上限较高,extension 上限较低,超过上限 crash 到 `EXC_RESOURCE_EXCEPTION`。
-
+
接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。
@@ -1740,7 +1827,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
FacekBook 提出排除法监控 OOM。
-
+
- App 更新了版本
@@ -2145,7 +2232,7 @@ App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试
#### 野指针可能存在的问题
-
+
### 2. Zombie Object
@@ -2192,11 +2279,11 @@ self:_NSZombie_Person - superClass:nil
利用 Instruments 下的 Zombies 工具检测,看看野指针的情况,系统是怎么捕获的。
-
+
切换到 `Call Trees` 模式下可以看到系调用了 `_dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
-
+
通过符号名称大概可以猜系统会在调用 dealloc 方法时,类似 KVO,派生出一个新的僵尸类,将当前类的 isa 指针指向僵尸类。
@@ -2903,7 +2990,7 @@ bool init_safe_free(void)
注意:ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC,加 `-fno-objc-arc`
-
+
### 6. 方案对比
@@ -2918,7 +3005,7 @@ bool init_safe_free(void)
### 1. App 网络请求过程
-
+
App 发送一次网络请求一般会经历下面几个关键步骤:
@@ -2956,7 +3043,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
iOS 网络框架层级关系如下:
-
+
iOS 网络现状是由 4 层组成的:最底层的 BSD Sockets、SecureTransport;次级底层是 CFNetwork、NSURLSession、NSURLConnection、WebView 是用 Objective-C 实现的,且调用 CFNetwork;应用层框架 AFNetworking 基于 NSURLSession、NSURLConnection 实现。
@@ -3544,11 +3631,11 @@ iOS 中 hook 技术有 2 类,一种是 NSProxy,一种是 method swizzling(
但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案 3,对于网络监控需要做如下的处理
-
+
可能对于 CFNetwork 比较陌生,可以看一下 CFNetwork 的层级和简单用法
-
+
CFNetwork 的基础是 CFSocket 和 CFStream。
@@ -3645,9 +3732,9 @@ void printResponseData (CFDataRef responseData) {
NSURLSession、NSURLConnection hook 如下。
-
+
-
+
业界有 APM 针对 CFNetwork 的方案,整理描述下:
@@ -3922,7 +4009,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
};
```
- 
+ 
method swizzling 改进版如下
@@ -3949,7 +4036,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
typedef struct objc_object *id;
```
- 
+ 
我们来分析一下为什么修改 `isa` 可以实现目的呢?
@@ -4107,11 +4194,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
HTTP 请求报文结构
-
+
响应报文的结构
-
+
1. HTTP 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。
2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符)
@@ -4138,11 +4225,11 @@ HTTP 请求报文结构
下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。
-
+
下图是在终端使用 `curl` 查看一个完整的请求和响应数据
-
+
我们都知道在 HTTP 通信中,响应数据会使用 gzip 或其他压缩方式压缩,用 NSURLProtocol 等方案监听,用 NSData 类型去计算分析流量等会造成数据的不精确,因为正常一个 HTTP 响应体的内容是使用 gzip 或其他压缩方式压缩的,所以使用 NSData 会偏大。
@@ -4649,33 +4736,117 @@ XMLHttpRequest.prototype.open(function(...args) {
### 3. 开发阶段针对电量消耗我们能做什么
-CPU 密集运算是耗电量主要原因。所以我们对 CPU 的使用需要精打细算。尽量避免让 CPU 做无用功。对于大量数据的复杂运算,可以借助服务器的能力、GPU 的能力。如果方案设计必须是在 CPU 上完成数据的运算,则可以利用 GCD 技术,使用 `dispatch_block_create_with_qos_class(<#dispatch_block_flags_t flags#>, dispatch_qos_class_t qos_class, <#int relative_priority#>, <#^(void)block#>)()` 并指定 队列的 qos 为 `QOS_CLASS_UTILITY`。将任务提交到这个队列的 block 中,在 QOS_CLASS_UTILITY 模式下,系统针对大量数据的计算,做了电量优化
+耗电的主要来源:
-除了 CPU 大量运算,I/O 操作也是耗电主要原因。业界常见方案都是将「碎片化的数据写入磁盘存储」这个操作延后,先在内存中聚合吗,然后再进行磁盘存储。碎片化数据先聚合,在内存中进行存储的机制,iOS 提供 `NSCache` 这个对象。
+- CPU 处理,Processing
+- 网络,Networking
+- 定位,Location
+- 图像,Graphics
-NSCache 是线程安全的,NSCache 会在达到达预设的缓存空间的条件时清理缓存,此时会触发 `- (**void**)cache:(NSCache *)cache willEvictObject:(**id**)obj;` 方法回调,在该方法内部对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 次数少了,对电量的消耗也就减少了。
+优化:
-NSCache 的使用可以查看 SDWebImage 这个图片加载框架。在图片读取缓存处理时,没直接读取硬盘文件(I/O),而是使用系统的 NSCache。
+- 尽可能降低 CPU、GPU 的功耗
-```objective-c
-- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
- return [self.memoryCache objectForKey:key];
-}
+ CPU 密集运算是耗电量主要原因。所以我们对 CPU 的使用需要精打细算。尽量避免让 CPU 做无用功。
-- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
- UIImage *diskImage = [self diskImageForKey:key];
- if (diskImage && self.config.shouldCacheImagesInMemory) {
- NSUInteger cost = diskImage.sd_memoryCost;
- [self.memoryCache setObject:diskImage forKey:key cost:cost];
- }
+ 对于大量数据的复杂运算,可以借助服务器的能力、GPU 的能力。如果方案设计必须是在 CPU 上完成数据的运算,则可以利用 GCD 技术,使用 `dispatch_block_create_with_qos_class(<#dispatch_block_flags_t flags#>, dispatch_qos_class_t qos_class, <#int relative_priority#>, <#^(void)block#>)()` 并指定 队列的 qos 为 `QOS_CLASS_UTILITY`。将任务提交到这个队列的 block 中,在 QOS_CLASS_UTILITY 模式下,系统针对大量数据的计算,做了电量优化
- return diskImage;
-}
-```
+
+
+ 除了 CPU 大量运算,I/O 操作也是耗电主要原因。业界常见方案都是将「碎片化的数据写入磁盘存储」这个操作延后,先在内存中聚合吗,然后再进行磁盘存储。碎片化数据先聚合,在内存中进行存储的机制,iOS 提供 `NSCache` 这个对象。
+
+
+
+ NSCache 是线程安全的,NSCache 会在达到达预设的缓存空间的条件时清理缓存,此时会触发 `- (**void**)cache:(NSCache *)cache willEvictObject:(**id**)obj;` 方法回调,在该方法内部对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 次数少了,对电量的消耗也就减少了。
+
+ NSCache 的使用可以查看 SDWebImage 这个图片加载框架。在图片读取缓存处理时,没直接读取硬盘文件(I/O),而是使用系统的 NSCache。
+
+ ```objective-c
+ - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
+ return [self.memoryCache objectForKey:key];
+ }
+
+ - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
+ UIImage *diskImage = [self diskImageForKey:key];
+ if (diskImage && self.config.shouldCacheImagesInMemory) {
+ NSUInteger cost = diskImage.sd_memoryCost;
+ [self.memoryCache setObject:diskImage forKey:key cost:cost];
+ }
+
+ return diskImage;
+ }
+ ```
+
+ 可以看到主要逻辑是先从磁盘中读取图片,如果配置允许开启内存缓存,则将图片保存到 NSCache 中,使用的时候也是从 NSCache 中读取图片。NSCache 的 `totalCostLimit、countLimit` 属性,
+
+ `- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;` 方法用来设置缓存条件。所以我们写磁盘、内存的文件操作时可以借鉴该策略,以优化耗电量。
+
+
+
+- 少用定时器
+
+- 优化 I/O 的操作
+
+ - 尽量不要频繁写入小数据,最好批量一次性写入
+ - 读写大量重要数据的时候,考虑用 `dispatch_io`,其提供了基于 GCD 的异步操作文件的 I/O 的 API,用 dispatch_io 系统会优化磁盘访问
+ - 数据量较大的时候,建议使用数据库技术(SQLite、CoreData)
+
+- 网络优化
+
+ - 减少、压缩网络数据
+ - 如果多次请求的结果是相同的,尽量使用缓存
+ - 断点续传,否则网络不稳定的时候,可能多次传输相同内容
+ - 网络不可用时候,不要尝试执行网络请求
+ - 让用户可以取消长时间运行或者速度很慢的网络请求,设置合适的超时时间
+ - 批量下载。比如下载电子邮件的时候,一次下载多份,不要一份一份的下载,会开启多个网络请求。
+
+- 耗电优化
+
+ - 定位优化
+
+ - 如果只是需要快速确定用户位置信息,最好用 `CLLocationManager requestLocation` 方法,定位完成后,则会让定位硬件自动断电
+
+ - 如果不是导航应用,尽量不要实时更新位置,定位关闭就关掉定位服务
+
+ - 尽量降低定位精度,比如尽量不要使用精度最高的 `KCLLocationAccuracyBest`
+
+ - 需要后台定位的时候,尽量设置 `manager.pausesLocationUpdatesAutomatically = YES` 。如果用户不太可能移动的时候,系统会自动暂停位置更新
+
+ - 尽量不要使用 `manager.startMonitoringSignificantLocationChanges`,优先考虑 `startMonitoringForRegion:`
+
+ - **理围栏(`startMonitoringForRegion:`)**
+ 仅在设备**进入或离开预设的特定区域边界时触发**。系统通过低功耗传感器(如蜂窝基站、Wi-Fi)监测位置,仅在跨越边界时唤醒应用,触发频率极低。
+
+ 地理围栏通过精准的边界条件限制触发次数,最大程度减少位置更新频率,显著降低功耗
+
+ 需在特定地点(如商店、家)触发行为的应用(如推送通知、签到功能)。例如:“当用户接近咖啡店时发送优惠券”。
+
+ - **显著位置变化(`startMonitoringSignificantLocationChanges`)**
+ 在设备移动约500米或切换蜂窝基站时触发,**无论是否涉及特定区域**。虽然比持续GPS追踪省电,但触发频率较高(尤其在移动频繁时),导致更多后台唤醒。
+
+ 显著位置变化依赖大范围位置变动,可能在用户移动较多时频繁触发,增加电池消耗。
+
+ 需持续记录大致位置轨迹的应用(如运动追踪、天气更新)。但此类场景较少,且应谨慎使用以避免过度耗电。
+
+ - 硬件检测优化
+
+ - 用户移动、摇晃、倾斜设备时,都会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测,在不需要检测的场合,应该及时关闭这些硬件。
+
+ 硬件检测优化的核心是**精准控制传感器的启用时机**,结合生命周期管理与需求分析,避免后台或非必要场景下的持续耗电。通过合理使用Core Motion API、优化采样频率及严格测试,可显著提升应用的能效表现。
+
+ 比如:
+
+ - 绑定生命周期管理。
+
+ - 视图控制器:在`viewDidDisappear`或`deinit`中停止传感器。
+
+ - 应用状态:监听`UIApplication.didEnterBackgroundNotification`,在后台时暂停传感器
+
+ - 优化采样频率
+
+ - 根据需求选择最低有效频率(如`0.1秒`代替`0.01秒`),减少数据处理的能耗
-可以看到主要逻辑是先从磁盘中读取图片,如果配置允许开启内存缓存,则将图片保存到 NSCache 中,使用的时候也是从 NSCache 中读取图片。NSCache 的 `totalCostLimit、countLimit` 属性,
-`- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;` 方法用来设置缓存条件。所以我们写磁盘、内存的文件操作时可以借鉴该策略,以优化耗电量。
## 八、 Crash 监控
@@ -4704,7 +4875,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。
-
+
### 2. Crash 收集方式
@@ -4768,7 +4939,7 @@ KSCrash 功能齐全,可以捕获如下类型的 Crash
流程图如下:
-
+
对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
@@ -5079,7 +5250,7 @@ static void restoreExceptionPorts(void)
KSCrash 在这里的处理逻辑如下图:
-
+
看一下关键代码:
@@ -5441,9 +5612,9 @@ static void setEnabled(bool isEnabled)
阅读下源码,看看为什么 `NSUncaughtExceptionHandler` 可以收集 crash 信息。查看 objc 源码
-
+
-
+
发现入口函数 `_objc_init` 中调用可设置异常处理的 `exception_init` 方法。内部通过底层 API `std::set_terminate(&_objc_terminate)` 来设置异常处理的 handler。
@@ -5545,7 +5716,7 @@ static void setEnabled(bool isEnabled)
其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。
-
+
```c
/** Start general exception processing.
@@ -6046,7 +6217,7 @@ static int64_t getReportIDFromFilename(const char* filename)
}
```
-
+
#### 2.7 前端 js 相关的 Crash 的监控
@@ -6070,7 +6241,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
};
```
-
+
##### 2.7.3 React Native 异常监控
@@ -6093,7 +6264,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
模拟器点击 `command + d` 调出面板,选择 Debug,打开 Chrome 浏览器, Mac 下快捷键 `Command + Option + J` 打开调试面板,就可以像调试 React 一样调试 RN 代码了。
-
+
查看到 crash stack 后点击可以跳转到 sourceMap 的地方。
@@ -6117,7 +6288,7 @@ Tips:RN 项目打 Release 包
现象:iOS 项目奔溃。截图以及日志如下
-
+
```shell
2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({
@@ -6211,7 +6382,7 @@ global.ErrorUtils.setGlobalHandler((e) => {
现象:iOS 项目不奔溃。日志信息如下,对比 bundle 包中的 js。
-
+
结论:
@@ -6592,7 +6763,7 @@ parseJSError(line, column);
5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
-
+
##### 2.7.5 SourceMap 解析系统设计
@@ -6884,7 +7055,7 @@ parseJSError(line, column);
`.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下:
-
+
#### 4.2 DWARF 文件
@@ -7416,7 +7587,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/
```
-
+
### 5. 服务端处理
@@ -7426,7 +7597,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis,然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。
-
+
上图展示了一个 ELK 的日志架构图。简单说明下:
@@ -7436,13 +7607,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。
-
+
##### 5.2 服务侧
Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化处理,以方便定位问题、crash 产生报表和后续处理。
-
+
所以整个流程就是:客户端 APM SDK 收集 crash log -> Kafka 存储 -> Mac 机执行定时任务符号化 -> 数据回传 Kafka -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
@@ -7454,7 +7625,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下
-
+
说明:
@@ -7466,19 +7637,19 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
-
+
其中符号化服务是大前端背景下大前端团队的产物,所以是 NodeJS 实现的(单线程,所以为了提高机器利用率,就要开启多进程能力)。iOS 的符号化机器是 双核的 Mac mini,这就需要做实验测评到底需要开启几个 worker 进程做符号化服务。结果是双进程处理 crash log,比单进程效率高近一倍,而四进程比双进程效率提升不明显,符合双核 mac mini 的特点。所以开启两个 worker 进程做符号化处理。
下图是完整设计图
-
+
简单说明下,符号化流程是一个主从模式,一台 master 机,多个 slave 机,master 机读取 .DSYM 和 crash 结果的 cache。「数据处理和任务调度框架」调度符号化服务(内部 2 个 symbolocate worker)同时从七牛云上获取 .DSYM 文件。
系统架构图如下
-
+
## 九、Weex、Flutter 异常监控
@@ -7774,7 +7945,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分,没法实际统计加载页面时有多少JS获取失败的情况。
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache,并且会做大量的预加载,但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载,可能还会造成 OOM 问题
- 
+ 
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单)
@@ -7928,17 +8099,17 @@ runZoned>(() async {
可能有些人一直没有遇到过因为在子线程操作 UI,导致在开发阶段 Xcode console 输出了一堆日志,大体如下
-
+
本来常见的开发都会规避这些写法,没机会看到子线程操作 UI 的问题,但是 Weex 的业务代码,检测出存在子线程操作 UI 的问题,所以还是有必要增加这个能力的。
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` ,type 选择 `Main Thread Checker`, 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
-
+
效果如下
-
+
### 2. 问题及解决方案
@@ -7952,7 +8123,7 @@ runZoned>(() async {
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
-
+
另外该功能可以在线下 debug 阶段开启,判断是否是在 Xcode debug 状态,可以通过苹果提供的[官方判断方法](https://developer.apple.com/library/archive/qa/qa1361/_index.html#//apple_ref/doc/uid/DTS10003368)实现。
@@ -7966,7 +8137,7 @@ runZoned>(() async {
好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示
-
+
页面作为承载用户交互的具体战场,我们需要对页面的性能有个直观的指标。业界一般有2个指标:**页面渲染时长、页面可交互时长**。
@@ -8078,7 +8249,7 @@ runZoned>(() async {
6. 整个 APM 的架构图如下
- 
+ 
说明:
diff --git a/Chapter1 - iOS/1.82.md b/Chapter1 - iOS/1.82.md
index 9862294..4e94a3a 100644
--- a/Chapter1 - iOS/1.82.md
+++ b/Chapter1 - iOS/1.82.md
@@ -1,6 +1,6 @@
# Runtime
-> 做很多技术项目或者业务项目、再或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写、结合一些场景和源码分析来系统化学习。
+> 做很多技术项目或者业务项目、再或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就结合一些场景和源码分析来系统化学习。
>
> 2个问题:
>
@@ -75,7 +75,7 @@ Person 类存在3个 BOOL 属性:
上 Demo
-
+
@@ -85,7 +85,7 @@ Person 类存在3个 BOOL 属性:
新方案采用:**结构体的位域能力**,限定单个成员变量所占用的内存。代码如下:
-
+
@@ -97,7 +97,7 @@ Person 类存在3个 BOOL 属性:
虽然上述方式都可以实现存储 Person 类3个属性的目的,但是还有第三种方案,参考 iOS 系统设计,采用 Union 实现。代码如下
-
+
分析:
@@ -122,7 +122,20 @@ union {
但是增加下面的结构体,是为了增加代码的可读性,内存无负向影响。
+说明:
+- 联合体的所有成员 **共享同一块内存空间**,其大小由最大的成员决定。
+
+- char 类型的 `bits` 和匿名结构体 **共享同一块内存**(1个字节大小的空间)
+
+- struct 里面的 tall 第 0 位,rich 第 1 位,handsome 第 2 位。这样,3 个成员总共只占用 3 位,而 1 字节有 8 位,因此可以完全容纳
+
+- **本质是共享内存**:联合体的 `bits` 和结构体是同一块内存的不同“解释方式”。
+
+ - 通过 `bits`,你可以直接读写整个字节。
+ - 通过结构体位域,你可以单独操作某一位。
+
+ **位域的紧凑存储**:3 个 `char :1` 成员仅占用 3 位,而 1 字节有 8 位,因此足够存储。
### 位运算设计 API
@@ -164,11 +177,16 @@ union {
0b0000 0000
```
-我们发现上面3个数**按位或之后的数字,分别与每个数按位与,得到的结果就是数据本身**。
+我们发现上面:3个数**按位或之后的结果,再分别与每个数按位与,得到的结果就可以还原数据本身**。
+
+也就是:
+
+- 方法参数可以传递各个枚举值按位或的结果
+- 方法内部再拿这个参数,分别与各个枚举值按位与,得到的结果如果是 YES,则说明参数中传递了该枚举值
与一个不是3个数之一的数按位与,得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值
-
+
有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。
@@ -178,6 +196,37 @@ union {
## Runtime 源码阅读
+### id 的本质
+
+`id` 在 Objective-C 的运行时头文件(``)中被定义为指向 `struct objc_object` 的指针:
+
+```objective-c
+typedef struct objc_object *id;
+```
+
+那 `objc_object` 的核心结构是什么?
+
+```objective-c
+struct objc_object {
+private:
+ isa_t isa;
+
+public:
+ // ISA() assumes this is NOT a tagged pointer object
+ Class ISA();
+
+ // getIsa() allows this to be a tagged pointer object
+ Class getIsa();
+ // ...
+}
+```
+
+`isa` 指针:所有 Objective-C 对象的内存起始位置都是一个 `isa` 指针,指向该对象的类(`Class`)。
+
+注意:Arm64 之后,isa 变成了 union isa_t,里面存储了更多信息
+
+
+
### isa 本质
- 在 arm64 架构之前,isa 就是一个普通的指针,存储着 Class 或 Meta-Class 对象的内存地址。
@@ -201,7 +250,7 @@ union isa_t {
union isa_t {
uintptr_t bits;
struct {
- uintptr_t nonpointer : 1;
+ uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
@@ -221,6 +270,9 @@ struct 内部的成员变量可以指定占用内存位数, `uintptr_t nonpoin
- has_assoc:是否有设置过关联对象,如果没有,释放时会更快
- has_cxx_dtor:是否有 c++ 的析构函数(`.cxx_destruct`),如果没有,释放时会更快
+ - 当 `has_cxx_dtor = 1` 时,表示该 Objective-C 对象 **持有 C++ 成员变量或继承自 C++ 类**,需要在对象释放时调用 C++ 析构函数
+
+ - Objective-C 的 `dealloc` 方法在释放对象内存前,会检查 `has_cxx_dtor` 标志位。若为 `1`,则调用 `object_cxxDestruct` 函数执行 C++ 析构逻辑
- shiftcls:存储着 class、meta-class 对象的内存地址信息
@@ -236,7 +288,7 @@ struct 内部的成员变量可以指定占用内存位数, `uintptr_t nonpoin
-上面说的更快,是如何得出结论的?
+上面说的「如果没有,释放时会更快」,是如何得出结论的?
查看 objc4 源代码看到对象执行销毁函数的时候会判断对象是否有关联对象、析构函数,有的话分别调用析构函数、移除关联对象等逻辑。
@@ -273,7 +325,7 @@ isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类
`0x0000000ffffffff8ULL` 用程序员模式打开计算器
-
+
@@ -283,7 +335,7 @@ extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcl
知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象)
-
+
@@ -299,7 +351,7 @@ extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcl
0b0010 1000
```
-结论:**根据按位与的效果。`ISA_MASK` 的后3位都是0,所以我们找到的类地址二进制表示时后3位一定为0**
+结论:**根据按位与的效果。`ISA_MASK` 的后3位都是0,所以经过 isa union 位运算后得到的类对象地址或者元类对象地址,用二进制表示时,最后3位一定为0**
我们可以验证下
@@ -311,7 +363,7 @@ NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0
NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28
```
-为什么有的结尾是8?16进制的8转为二进制,`0x1000`
+为什么有的结尾是8?因为 `0x` 是16进制。16进制的8转为二进制,`0x1000`
关于这部分的调试,需要在真机上运行,真机上 arm64,拷贝对象地址到系统自带的运算器(程序员模式),查看64位地址。按照下面的顺序一一查看
@@ -343,9 +395,17 @@ isa 指针,在 arm64 之前,isa 指针就是普通的对象,存储着类
```c
struct objc_object {
+private:
isa_t isa;
-}
+public:
+ // ISA() assumes this is NOT a tagged pointer object
+ Class ISA();
+
+ // getIsa() allows this to be a tagged pointer object
+ Class getIsa();
+ // ...
+}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
@@ -423,7 +483,7 @@ struct class_ro_t {
具体关系整理如下图
-
+
@@ -431,11 +491,11 @@ struct class_ro_t {
- 元类对象可以看成是特殊的类对象,数据类型都是 `Class`,所以大部分数据结构都一样,区别在于某些值是否有值
-- `class_rw_t`:里面的 methods、properties、protocols 是数组(`method_array_t` -> `method_list_t` -> `method_t`),是可读可写的,包含了类的初始内容、分类的内容。
+- `class_rw_t`:里面的 methods、properties、protocols 是二维数组(其实不能叫二维数组,因为每个子列表 `method_list_t` 的长度可以不同,应该叫 **方法列表的列表**)其结构类似:`method_array_t` -> `method_list_t` -> `method_t`,是可读可写的,包含了类的初始内容、分类的内容。
-
+
比如访问 method 的过程
@@ -453,11 +513,142 @@ struct class_ro_t {
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
-
+
+
+- 当系统运行 Runtime 会把类自身的信息(class_ro_t) 中的信息和 Category 中的信息合并起来,放到 class_rw_t 中的信息去
+ 源码 `objc-runtime-new.mm` 如下
+ ```c++
+ static Class realizeClassWithoutSwift(Class cls, Class previously)
+ {
+ // ...
+ class_rw_t *rw;
+ Class supercls;
+ Class metacls;
+ // ...
+
+ // fixme verify class is not in an un-dlopened part of the shared cache?
+
+ auto ro = cls->safe_ro(); // 获取编译期确定的只读元数据(class_ro_t)
+ auto isMeta = ro->flags & RO_META; // 判断是否为元类
+ if (ro->flags & RO_FUTURE) {
+ // This was a future class. rw data is already allocated.
+ // Future Class 已预分配 rw 数据。Future Class:共享缓存(shared cache)中预生成但未完全实现的类。
+ rw = cls->data(); // 直接获取已分配的 rw
+ ro = cls->data()->ro(); // 更新 ro 指针
+ ASSERT(!isMeta);
+ cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE); // 更新标志位:从 RW_FUTURE 转换为 RW_REALIZED|RW_REALIZING。
+ } else {
+ // Normal class. Allocate writeable class data.
+ // 为普通类分配可读写数据
+ rw = objc::zalloc(); // 分配归零内存
+ rw->set_ro(ro); // 绑定只读数据到 rw
+ rw->flags = RW_REALIZED|RW_REALIZING|isMeta; // 设置状态标志
+ cls->setData(rw); // 将 rw 关联到类
+ }
+ // ...
+ return cls;
+ }
+ ```
+ 总结:
+ - 系统刚运行起来的时候 `objc_class` 结构体的 `class_data_bits_t bits` 的 data 指向的是 class_ro_t 的
+ - 经过 Runtime 会把类的原始信息和 Category 信息(Property、method、protocol)进行整合,创建 class_rw_t 结构体,把 class_ro_t 复制给 class_rw_t 的 `ro` 成员变量。
+ - 最后把 `objc_class` 结构体的 `class_data_bits_t bits` 的 data 指向的是 class_rw_t
+
+ ```c++
+ // 编译期
+ objc_class.bits.data() → class_ro_t
+
+ // 运行时初始化后
+ objc_class.bits.data() → class_rw_t
+ class_rw_t.ro → class_ro_t
+ ```
+
+
+
+ 阐述下类的数据存储演进流程
+
+ 阶段 1:编译期(`class_ro_t`)
+
+ - **`class_ro_t`(Read Only)**:
+ - **内容**:由编译器生成,包含类名、父类指针、实例大小、基础方法列表(`baseMethodList`)、属性列表(`baseProperties`)、协议列表(`baseProtocols`)等。
+ - **内存位置**:存储在 Mach-O 文件的 `__DATA __objc_const` 段中。
+ - **访问方式**:通过 `objc_class->bits.data()` 直接访问。
+
+ 阶段 2:运行时初始化(`class_rw_t`)
+
+ - **触发时机**:
+
+ - 首次调用 `[MyClass alloc]` 或 `objc_getClass("MyClass")` 时触发类的 `realize`。
+ - 运行时通过 `realizeClassWithoutSwift()` 函数完成初始化。
+
+ - **核心操作**:
+
+ 1. **分配 `class_rw_t`**:动态创建可读写结构体。
+ 2. **绑定 `class_ro_t`**:将 `rw->ro` 指向编译期的 `class_ro_t`。
+ 3. **更新指针**:将 `objc_class->bits.data()` 指向 `class_rw_t`。
+ 4. **合并 Category**:附加所有分类(Category)的方法、属性、协议到 `class_rw_t`。
+
+ - **内存变化**:
+
+ ```
+ // 编译期
+ objc_class.bits.data() → class_ro_t
+
+ // 运行时初始化后
+ objc_class.bits.data() → class_rw_t
+ class_rw_t.ro → class_ro_t
+ ```
+
+ `class_rw_t` 与 `class_ro_t` 的核心区别
+
+ | 特性 | `class_ro_t`(只读) | `class_rw_t`(读写) |
+ | :------------- | :------------------- | :--------------------------------- |
+ | **生成时机** | 编译期生成 | 运行时动态分配 |
+ | **内存位置** | Mach-O 文件数据段 | 堆内存 |
+ | **内容可变性** | 不可修改 | 可动态添加方法、属性、协议 |
+ | **包含数据** | 基础方法、属性、协议 | 基础数据 + 分类数据 + 动态扩展数据 |
+ | **性能优化** | 无锁访问 | 需要线程安全措施(如锁) |
+
+ Category 的合并逻辑
+
+ - **附加到 `class_rw_t`**:
+
+ - **方法**:分类的方法会被插入到 `class_rw_t.methods` 数组的**头部**,因此分类方法优先于原类方法被调用。
+ - **属性**:分类的属性通过关联对象(Associated Object)实现,不会真正添加到 `class_rw_t.properties`。
+ - **协议**:分类的协议会被合并到 `class_rw_t.protocols` 列表中。
+
+ - **源码关键路径**:
+
+ ```
+ // objc-runtime-new.mm
+ static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count) {
+ // 遍历所有分类,合并方法、属性、协议到 class_rw_t
+ for (uint32_t i = 0; i < cats_count; i++) {
+ auto& entry = cats_list[i];
+ method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
+ if (mlist) {
+ rw->methods.attachLists(&mlist, 1); // 方法合并
+ }
+ // 类似处理属性和协议
+ }
+ }
+ ```
+
+ 设计意义
+
+ - **内存优化**:
+ - 未初始化的类保持 `class_ro_t` 轻量级存储,减少内存占用。
+ - 按需分配 `class_rw_t`,仅在类首次使用时初始化。
+ - **动态性支持**:
+ - 运行时可通过 `class_addMethod()` 或分类动态扩展方法。
+ - 支持方法交换(Method Swizzling)等高级特性。
+ - **性能权衡**:
+ - 方法查找需遍历 `class_rw_t.methods` 的多个方法列表(原类 + 所有分类)。
+ - 引入 `class_rw_ext_t` 进一步优化(将常用数据如方法缓存分离)
QA:
@@ -1240,7 +1431,7 @@ v
可以对照下面的表格进行查看:
-
+
```objectivec
- (int)calcuate:(int)baseHeight heigith:(float)height;
@@ -1287,13 +1478,62 @@ struct objc_class : objc_object {
}
```
-调用方法的本质,比如说对象方法,先根据对象的 isa 找到类对象,在类对象的 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 methods 方法数组(Array 中的元素是方法 Array),假设某个类的方法调用 `[Worker live]`,需要通过 superclass 查找8次,每次都是在方法的“二维数组”里遍历。某个逻辑就需要调用 `[Worker live]`6次,每次需要8次二维数组的遍历,可想而知,效率低下。
+调用方法的本质:
+
+- 比如说对象方法,先根据对象的 isa 找到类对象,在 arm64 下,isa 是非指针的 union,利用64位中的不同位存储不同信息,比如用33位来存储类对象地址、元类对象地址信。用 `ISA_MASK` 来提取类对象的真实地址
+- 在类对象的 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法
+- 找不到则调用 superclass 查找父类的 methods 方法数组(Array 中的元素是方法 Array),看是否存在方法。找不到则继续在当前类的 superclass 继续向上查找...
+- 假设某个类的方法调用 `[Worker live]`,需要通过 superclass 查找8次,每次都是在方法的“二维数组”里遍历。某个逻辑就需要调用 `[Worker live]`6次,每次需要8次二维数组的遍历,6*8= 48次,可想而知,效率低下。
所以为了方便,给类设置了**方法缓存**。比如调用 Worker 对象的 live 方法,通过 isa 找到元类对象,元类对象中不存在,则通过 superclass 找父类的元类对象, 不断找,假设经历了8次 superclass,最后在 Person 类中找到了,则将 Person 类中的 eat 方法缓存在 Worker 的 `cache_t` 类型的 cache 中。
-所以完整结构为:先根据对象的 isa 找到类对象,在类对象的 cache 列表中查找方法实现,如果找不到,则去 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 cache 中查找,找到则调用方法,同时将父类 cache 缓存中的方法,在子类的 cache 中缓存一边。父类 cache 没找到,则在 methods 方法数组(Array 中的元素是方法 Array)查找,找到则调用,同时在子类 cache 中缓存一份。父类 methods 方法数组(Array 中的元素是方法 Array)没找到则继续调用 superclass,依次类推
+#### Cache 完整流程
+
+1. 通过实例的 `isa` 指针找到类对象
+
+ 实例方法存储在类对象中,类方法存储在元类中。方法调用时,首先通过实例的 `isa` 指针定位到类对象。
+
+2. 找类对象的 `cache`
+
+ - 检查类对象的方法缓存 `cache`(哈希表结构),若找到方法实现(IMP),直接调用并结束流程。
+
+ - **缓存目的**:哈希表查找时间复杂度为 O(1),避免频繁方法查找的性能损耗。
+
+3. 查找类对象的 `method_list_t` 方法列表
+
+ - 若 `cache` 未命中,遍历类对象的 `method_list_t`(方法列表)。
+
+ - **方法列表结构**:
+ - 由多个方法数组构成,顺序为:**后加载的 Category 方法在前,类自身方法在后**(例如:Category2 → Category1 → 类原始方法)。
+ - 这是 Runtime 在启动时合并 `class_ro_t`(只读数据)到 `class_rw_t`(可读写数据)时确定的,后编译的 Category 方法会插入到列表前端,覆盖同名方法。
+
+ - **命中缓存**:若在方法列表中找到方法,将其**缓存到当前类对象的 `cache`** 中,随后调用方法。
+
+4. 沿继承链向父类逐级查找
+
+ 若当前类未找到方法,通过 `superclass` 指针查找父类,重复以下步骤:
+
+ - 查找父类 `cache`:若父类 `cache` 命中,将方法**缓存到最初类对象的 `cache`**(即子类的 `cache`),调用方法
+ - 如果 cache 中 没有找到,则查找父类 `method_list_t`:若父类方法列表命中,同样**缓存到子类的 `cache`**,调用方法
+ - 递归查找:若未找到,继续向更上层父类查找...
+ - 直到根类(`NSObject`),如果 NSObject 的 cache 中找到方法缓存则执行方法,且把方法在当前的子类的 cache 中缓存一份。如果没找到则继续在 method_list_t 中查找,找到也继续在当前的子类 cache 中缓存一份
+
+5. 若当前类未找到方法,通过 `superclass` 指针查找父类,重复上述步骤:,未找到方法时触发消息转发若继承链全部查找未果,进入动态消息解析流程:
+
+ - 动态方法解析(`+resolveInstanceMethod:` 或 `+resolveClassMethod:`):
+ - 快速消息转发(`-forwardingTargetForSelector:`):将消息转发给其他对象处理。
+ - 慢速消息转发(`-methodSignatureForSelector:` 和 `-forwardInvocation:`):创建 `NSInvocation` 对象,可修改参数、目标、方法等
+ - 如果还没处理,则报标准的 “unrecognized selector sent to instance” 错误,导致程序崩溃。
+
+
+
+#### 源码剖析
+
+为什么说空间换时间?
+
+假设 Person 类存在 test1、test2 方法。经过运算后 test1 需要存储在 bucket_t 数组的第8个位置,test2 存储在第5个位置,其他的位置都是 NULL。也就是预留了一些闲置的空间。存储的时候都会经过 hash 运算。效率会很高。空间换时间的实现。
```c
struct cache_t {
@@ -1310,58 +1550,134 @@ struct bucket_t {
}
```
-方法缓存查找原理,就是利用散列表(哈希表)查找。涉及:空间换时间,哈希表拓容策略,哈希碰撞算法。
-
-
+方法缓存查找原理,就是利用散列表(哈希表)查找。涉及:空间换时间,哈希表拓容策略,哈希碰撞算法
objc4 源码 `objc-cache.mm`
```c++
typedef uintptr_t cache_key_t;
-// 根据 SEL 计算方法缓存 key 就说根据 SEL(方法名 C 字符串的指针)转换为一个用于存储指针的整数值(这个类型是一个无符号整数类型,uintptr_t 提供了一种方式来将指针转换为一个整数,以及将整数转换回指针)
+// 根据 SEL 计算方法缓存 key 就是根据 SEL(方法名 C 字符串的指针)转换为一个用于存储指针的整数值(这个类型是一个无符号整数类型,uintptr_t 提供了一种方式来将指针转换为一个整数,以及将整数转换回指针)
cache_key_t getKey(SEL sel)
{
assert(sel);
return (cache_key_t)sel;
}
-static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
+void cache_t::insert(SEL sel, IMP imp, id receiver)
{
- cacheUpdateLock.assertLocked();
-
+ runtimeLock.assertLocked();
+
+ // 避免在类完成初始化之前,修改方法缓存
// Never cache before +initialize is done
- if (!cls->isInitialized()) return;
+ if (slowpath(!cls()->isInitialized())) {
+ return;
+ }
- // Make sure the entry wasn't added to the cache by some other thread
- // before we grabbed the cacheUpdateLock.
- if (cache_getImp(cls, sel)) return;
- // 获取方法缓存 cache_t
- cache_t *cache = getCache(cls);
- // 根据 SEL 计算方法缓存 key
- cache_key_t key = getKey(sel);
+ if (isConstantOptimizedCache()) {
+ _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
+ cls()->nameForLogging());
+ }
- // Use the cache as-is if it is less than 3/4 full
- mask_t newOccupied = cache->occupied() + 1;
- mask_t capacity = cache->capacity();
- if (cache->isConstantEmptyCache()) {
+#if DEBUG_TASK_THREADS
+ return _collecting_in_critical();
+#else
+#if CONFIG_USE_CACHE_LOCK
+ mutex_locker_t lock(cacheUpdateLock);
+#endif
+
+ ASSERT(sel != 0 && cls()->isInitialized());
+
+ // Use the cache as-is if until we exceed our expected fill ratio.
+ mask_t newOccupied = occupied() + 1;
+ unsigned oldCapacity = capacity(), capacity = oldCapacity;
+ if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
- cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
+ if (!capacity) capacity = INIT_CACHE_SIZE;
+ reallocate(oldCapacity, capacity, /* freeOld */false);
}
- else if (newOccupied <= capacity / 4 * 3) {
- // Cache is less than 3/4 full. Use it as-is.
+ else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
+ // Cache is less than 3/4 or 7/8 full. Use it as-is.
}
+#if CACHE_ALLOW_FULL_UTILIZATION
+ else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
+ // Allow 100% cache utilization for small buckets. Use it as-is.
+ }
+#endif
else {
- // Cache is too full. Expand it.
- cache->expand();
+ capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
+ if (capacity > MAX_CACHE_SIZE) {
+ capacity = MAX_CACHE_SIZE;
+ }
+ reallocate(oldCapacity, capacity, true);
}
+ bucket_t *b = buckets();
+ mask_t m = capacity - 1;
+ mask_t begin = cache_hash(sel, m);
+ mask_t i = begin;
+
// Scan for the first unused slot and insert there.
- // There is guaranteed to be an empty slot because the
- // minimum size is 4 and we resized at 3/4 full.
- bucket_t *bucket = cache->find(key, receiver);
- if (bucket->key() == 0) cache->incrementOccupied();
- bucket->set(key, imp);
+ // There is guaranteed to be an empty slot.
+ do {
+ if (fastpath(b[i].sel() == 0)) {
+ // 找到合适的位置,插入新的 method_t
+ incrementOccupied();
+ b[i].set(b, sel, imp, cls());
+ return;
+ }
+ if (b[i].sel() == sel) {
+ // SEL 已经存在,被其他线程加入
+ // The entry was added to the cache by some other thread
+ // before we grabbed the cacheUpdateLock.
+ return;
+ }
+ // while 条件满足一次,则尝试插入一次。没有插入成功,则继续尝试 while,意味着发生了哈希冲突,开放定址法,线性尝试 index
+ } while (fastpath((i = cache_next(i, m)) != begin));
+
+ bad_cache(receiver, (SEL)sel);
+#endif // !DEBUG_TASK_THREADS
+}
+
+void cache_t::copyCacheNolock(objc_imp_cache_entry *buffer, int len)
+{
+#if CONFIG_USE_CACHE_LOCK
+ cacheUpdateLock.assertLocked();
+#else
+ runtimeLock.assertLocked();
+#endif
+ int wpos = 0;
+
+#if CONFIG_USE_PREOPT_CACHES
+ if (isConstantOptimizedCache()) {
+ auto cache = preopt_cache();
+ auto mask = cache->mask;
+ uintptr_t sel_base = objc_opt_offsets[OBJC_OPT_METHODNAME_START];
+ uintptr_t imp_base = (uintptr_t)&cache->entries;
+
+ for (uintptr_t index = 0; index <= mask && wpos < len; index++) {
+ auto &ent = cache->entries[index];
+ if (~ent.sel_offs) {
+ buffer[wpos].sel = (SEL)(sel_base + ent.sel_offs);
+ buffer[wpos].imp = (IMP)(imp_base - ent.imp_offset());
+ wpos++;
+ }
+ }
+ return;
+ }
+#endif
+ {
+ bucket_t *buckets = this->buckets();
+ uintptr_t count = capacity();
+
+ for (uintptr_t index = 0; index < count && wpos < len; index++) {
+ if (buckets[index].sel()) {
+ buffer[wpos].imp = buckets[index].imp(buckets, cls());
+ buffer[wpos].sel = buckets[index].sel();
+ wpos++;
+ }
+ }
+ }
}
bucket_t * cache_t::find(cache_key_t k, id receiver)
@@ -1370,12 +1686,14 @@ bucket_t * cache_t::find(cache_key_t k, id receiver)
bucket_t *b = buckets();
mask_t m = mask();
- mask_t begin = cache_hash(k, m); // 该方法实现就是 k & mask,按位与
+ mask_t begin = cache_hash(k, m); // 该方法实现就是 `key & mask`,按位与来计算哈希索引
mask_t i = begin;
do {
+ // 从 buckets 里的 beign 位置开始寻找方法缓存,如果找到则返回 buckets[i]
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
+ // while 的一个为true则代表初始化找到的哈希索引位置没有找到,意味着发生了哈希冲突,则用开放定址法去查找下一个可能的位置。调用的方法是 cache_next
} while ((i = cache_next(i, m)) != begin);
// hack
@@ -1383,9 +1701,17 @@ bucket_t * cache_t::find(cache_key_t k, id receiver)
cache_t::bad_cache(receiver, (SEL)k, cls);
}
+
+// 哈希冲突的时候,调用 cache_next 方法来寻找合适的 index。是标准的开放定址法
+#if CACHE_END_MARKER
+static inline mask_t cache_next(mask_t i, mask_t mask) {
+ return (i+1) & mask;
+}
+#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
+#else
```
可以看到在查找方法缓存的时候:
@@ -1393,11 +1719,35 @@ static inline mask_t cache_next(mask_t i, mask_t mask) {
- 首先根据缓存 key,利用 `cache_hash` 方法计算出一个 begin,赋值给 i
- 然后利用 i 在方法缓存 `cache_t` 的 `buckets` 中查找,假设 key 相等,则直接返回对应的方法
- 如果没有找到则执行 `cache_next` 方法,该方法则会判断 i 是不是等于0,不等于0则自减1,等于0则设置为 mask 的值
-- mask 值,在设计上等于 `buckets` 散列表的长度减1
+- mask 值,在设计上等于 `buckets` 散列表的长度减1。
+ 1. 任何一个值,与一个 mask 按位与之后的结果,一定小于等于 mask 的值。比如:
+ ```shell
+ 1. 按位与之后,最大的等于自己的值本身
+ 0x 1000 1001
+ & 0x 1111 1111
+ -----------------
+ 0x 1000 10001
+
+
+ 2. 按位与之后,结果小于自己的值本身
+ 0x 1111 0001
+ & 0x 0000 0001
+ -----------------
+ 0x 0000 0001
+
+ 0x 1111 0001
+ & 0x 0000 1111
+ -----------------
+ 0x 0000 1111
+ ```
-散列表长度不够了,则会哈希拓容,此时之前存储的方法缓存则会被释放,执行 `cache_collect_free`
+ 2. 所以哈希函数设计位:方法选择器 `SEL` 的指针地址(`uintptr_t` 类型)作为 key,与 `_mask` 按位与的结果。其中 `_mask` 为当前散列表的长度减1
+
+
+
+散列表长度不够了,则会哈希拓容(),此时之前存储的方法缓存则会被释放,执行 `cache_collect_free`
```c
void cache_t::expand()
@@ -1444,7 +1794,153 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask)
空间换时间的一个实现。且按位与的特点,`(key & mask)` 的结果一定比 mask 值小。
-查找类的方法缓存 Demo
+
+
+#### 模拟方法缓存工作流程
+
+举个例子,来直观了解下 cache 的工作流程:
+
+假设初始缓存容量为 **4**(`_mask = 3`),逐步插入 `test1`、`test2`、`test3`、`test4` 方法后触发扩容,最终容量变为 **8**(`_mask = 7`)。
+
+1. 初始状态,散列表容量为4,`_mask` =3,_occupied = 0,目前没有方法写入缓存。散列桶数组为空
+
+ ```shell
+ buckets[0]: null
+ buckets[1]: null
+ buckets[2]: null
+ buckets[3]: null
+ ```
+
+2. 不断插入方法:
+
+ - 假设 test1 的 sel 的 uintptr_t 为5,计算哈希索引 `5 & 3 = 1`
+
+ ```shell
+ 0b 0000 0101
+ & 0b 0000 0011
+ ---------------
+ 0b 0000 0001
+ ```
+
+ 所以插入索引为1的位置。buckets[1] = test1, _occupied= 1
+
+ ```shell
+ buckets[0]: null
+ buckets[1]: test1
+ buckets[2]: null
+ buckets[3]: null
+ ```
+
+ - 假设 test2 的 sel 的 uintptr_t 为7,计算哈希索引 `7 & 3 = 3`。所以插入索引为3的位置。buckets[3] = test2, _occupied= 2
+
+ ```
+ buckets[0]: null
+ buckets[1]: test1
+ buckets[2]: null
+ buckets[3]: test2
+ ```
+
+ - 假设 test3 的 sel 的 uintptr_t 为9,计算哈希索引 `9 & 3 = 1`,发现位置1被占用了,此时采用开放定址法寻找合适的位置(也就是哈希冲突的解法)。比如先从1开始尝试,index = 2,发现没有被占用,则开放定址的过程终止,则将 test3 放到位置2上
+
+ ```
+ buckets[0]: null
+ buckets[1]: test1
+ buckets[2]: test3
+ buckets[3]: test2
+ ```
+
+ 假设哈希函数为: `index = Hash(key) mod _mask`
+
+ 补充说明:**针对哈希冲突的开放定址法一般有:线性探测法、二次探测法、伪随机探测法**
+
+ - 线性探测法就是:`index = (Hash(key) + di) mod _mask`,其中 di 为1, 2, 3, ... _mask - 1 构成的线性序列
+ - 二次探测法就是:`index = (Hash(key) + di) mod _mask`,其中 di 为1^2, 2^2, 3^2, ...直到平方数小于等于 _mask - 1 的数,构成的二次序列
+ - 伪随机数测法就是:`index = (Hash(key) + di) mod _mask`,其中 di 为伪随机数,构成的二次序列
+
+ 所以插入索引为3的位置。buckets[3] = test2, _occupied= 2
+
+ - 假设 test4 的 sel uintptr_t 为11,计算哈希索引 `11 & 3 = 3`,发现位置3被占用了,此时采用开放定址法寻找合适的位置(也就是哈希冲突的解法)。比如先从1开始尝试,index = 0,发现没有被占用,则将 test4 放到位置0上
+
+ ```
+ buckets[0]: test4
+ buckets[1]: test1
+ buckets[2]: test3
+ buckets[3]: test2
+ ```
+
+ 此时,`_occupied = 4`,此时 `_occupied > 3/4 * capacity`(`3/4 * 4 = 3`),**触发扩容**。
+
+3. 扩容与哈希重建
+
+ 新容量:capacity = 8, _mask = 7
+
+ 创建新桶
+
+ ```shell
+ buckets[0]: null
+ buckets[1]: null
+ buckets[2]: null
+ buckets[3]: null
+ buckets[4]: null
+ buckets[5]: null
+ buckets[6]: null
+ buckets[7]: null
+ ```
+
+ 安装上述哈希方法,重新计算位置并保存到桶中
+
+ | 方法 | 旧索引 | 新索引计算(哈希值 & 7) |
+ | ----- | ------ | ------------------------ |
+ | test1 | 1 | 5&7 = 5 |
+ | test2 | 3 | 7&7 =7 |
+ | test3 | 2 | 9 & 7 = 1 |
+ | Test4 | 0 | 11 & 7 = 3 |
+
+ 桶的最新状态
+
+ ```
+ buckets[0]: null
+ buckets[1]: test3
+ buckets[2]: null
+ buckets[3]: test4
+ buckets[4]: null
+ buckets[5]: test1
+ buckets[6]: null
+ buckets[7]: test2
+ ```
+
+ 更新缓存结构:
+
+ - _buckets 指向新的桶数组
+ - _mask = 7
+ - _occupied = 4
+
+4. 读取方法流程
+
+ - 从缓存中查找 test1 方法,假设 test1 的 sel 的 uintptr_t 为5,计算哈希索引 `5 & 7 = 5`,则返回桶中 buckets[5] 的 method_t
+ - 从缓存中查找 test2 方法,假设 test2 的 sel 的 uintptr_t 为7,计算哈希索引 `7 & 7 = 7`,则返回桶中 buckets[7] 的 method_t
+ - 从缓存中查找 test3 方法。假设 test3 的 sel 的 uintptr_t 为9,计算哈希索引 `9 & 7 = 1`,则返回桶中 buckets[1] 的 method_t
+ - 从缓存中查找 test4 方法,假设 test4 的 sel 的 uintptr_t 为11,计算哈希索引 `11 & 7 = 3`,则返回桶中 buckets[3] 的 method_t
+
+结论:
+
+1. 存储位置动态变化:扩容后方法的存储位置会发生变化(如 `test3` 从旧索引 2 → 新索引 1),但通过 **重新哈希**,所有方法被重新分配到新容量的正确位置。
+2. 查找逻辑一致性:无论缓存是否扩容,查找时始终使用 **当前的 `_mask`** 计算索引,确保能正确命中方法。
+3. 性能与空间的平衡:空间换时间
+ - **扩容代价**:重新哈希需要遍历旧桶,时间复杂度为 O(n),但扩容频率随容量指数级降低。
+ - **查找效率**:平均时间复杂度接近 O(1),即使位置变化也不影响性能。
+
+设计哲学:
+
+- 以空间换时间:通过扩容减少哈希冲突,牺牲内存换取更快的查找速度。
+- 惰性扩容:仅在负载因子超过阈值时扩容,避免频繁内存分配(标准做法,没有哪个成熟方案是随便扩容的)
+- 幂等性保证:无论扩容多少次,方法的最终存储位置始终由 `SEL & _mask` 决定,确保逻辑正确。
+
+通过这种设计,Objective-C 的方法缓存在高频调用场景下依然能保持极速响应,同时动态适应方法数量的增长
+
+
+
+实验:查找类的方法缓存 Demo
```objective-c
GoodStudent *goodStudent = [[GoodStudent alloc] init];
@@ -1496,6 +1992,10 @@ void cache_t::expand()
#### 如何根据方法散列表查找某个方法
+所有的方法都在 buckets 数组里。所以问题转换为:如何根据方法计算出在 bucktes 中的索引?
+
+本质上就是考察对于核心原理和源码的理解程度: `bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache._mask]`
+
```objectivec
GoodStudent *goodStudent = [[GoodStudent alloc] init];
mock_objc_class *personClass = (__bridge mock_objc_class *)[GoodStudent class];
@@ -1513,7 +2013,7 @@ bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache.
NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp);
```
-
+
@@ -1528,6 +2028,26 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask)
}
```
+注意:上述的模拟只是实现了系统哈希计算后查找 cache 的一半流程。还剩一半流程是:当哈希冲突的时候,计算出的 index 里存储的是另一个方法信息。此时就需要利用开放定植法去尝试合适的索引。
+
+系统代码如下:
+
+```c++
+mask_t begin = cache_hash(k, m); // 该方法实现就是 `key & mask`,按位与来计算哈希索引
+mask_t i = begin; // 初始先从该位置获取 cache 里的方法,做比较
+
+// 如果不满足,则利用 do...while 实现的开放定址法寻找方法缓存
+do {
+ // 从 buckets 里的 beign 位置开始寻找方法缓存,如果找到则返回 buckets[i]
+ if (b[i].key() == 0 || b[i].key() == k) {
+ return &b[i];
+ }
+ // while 的一个为true则代表初始化找到的哈希索引位置没有找到,意味着发生了哈希冲突,则用开放定址法去查找下一个可能的位置。调用的方法是 cache_next
+} while ((i = cache_next(i, m)) != begin);
+```
+
+
+
`[student live]`当调用对象方法的时候。会根据对象的 isa 指针,找到类对象,然后在类对象的方法缓存中查找有没有方法实现
- 如果找到了则立马执行
@@ -1542,9 +2062,32 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask)
-## Runtime - objc_msgSend
+#### 散列表的设计哲学
-
+阅读了方法缓存相关的源码,可以看到 Cache 的实现就是一个散列表。因为底层源码需要保证高性能,所以采用时间换空间的策略。
+
+利用散列表的散列函数,来实现快速计算存储和访问所需的 index。但是带来的一个问题是散列表可能会有一些闲置空间;且散列表计算出来的位置可能会冲突,所以需要哈希碰撞策略(比如开放定址法和拉链法);另外散列表容量快满的时候则需要哈希扩容。
+
+有个压力位的设计:例如 Apple OC 对于方法缓存的压力位就是 `_capacity * 3 / 4`。
+
+- 当方法缓存的 `_occupied`(已占用的槽位数)大于等于 `_capacity * 3 / 4` 时,就会触发扩容操作。
+- 种设计是为了在缓存的查找效率和存储空间之间取得平衡。当缓存的占用率达到 3/4 时,意味着缓存的使用较为频繁,并且继续添加新缓存可能会导致较多的哈希冲突,从而降低查找效率。此时进行扩容可以有效地减少哈希冲突,提高后续方法查找的速度。
+
+
+
+**针对哈希冲突的开放定址法一般有:线性探测法、二次探测法、伪随机探测法**
+
+- 线性探测法就是:`index = (Hash(key) + di) mod _mask`,其中 di 为1, 2, 3, ... _mask - 1 构成的线性序列
+- 二次探测法就是:`index = (Hash(key) + di) mod _mask`,其中 di 为1^2, 2^2, 3^2, ...直到平方数小于等于 _mask - 1 的数,构成的二次序列
+- 伪随机数测法就是:`index = (Hash(key) + di) mod _mask`,其中 di 为伪随机数,构成的二次序列
+
+
+
+## Runtime - objc_msgSen
+
+
+
+
```c++
Person *p = [[Person alloc] init];
@@ -1555,9 +2098,7 @@ objc_msgSend(p, sel_registerName("eat"));
objc_msgSend(object_getClass("Person"), sel_registerName("sayHi"));
```
-oc 方法(对象方法、类方法)调用本质就是 `objc_msgSend`
-
-`objc_msgSend` 可以分为3个阶段:
+oc 方法(对象方法、类方法)调用本质就是 `objc_msgSend` 方法。`objc_msgSend` 可以分为3个阶段:
- 消息发送
@@ -1980,7 +2521,7 @@ class_rw_t *data() {
- 没排序,则线性查找 (Linear search of unsorted method list)
-`getMethodNoSuper_nolock` 执行完则会将方法写入到当前类对象的缓存中。
+`getMethodNoSuper_nolock` 执行完则会将方法写入到当前类对象的缓存中,调用 `log_and_fill_cache` 方法
```c
static void
@@ -2117,7 +2658,7 @@ if (imp) goto done;
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
-
+
@@ -2125,8 +2666,12 @@ if (imp) goto done;
注意:
+当找到方法后,缓存只保存到当前消息调用者,也就是子类的方法 Cache 中去。
+
方法缓存,也叫快速映射表(fast map),即使是快速执行路径(fast path),还是不如“静态绑定的函数调用操作”(statically bound function call)那样迅速,但大多数情况下这不是性能瓶颈。如果真的很在意,可以用纯 c 函数去实现。
+上述消息发送阶段结束,方法依旧没有找到并执行,则开始进入动态方法解析阶段。
+
### 动态方法解析阶段
@@ -2262,7 +2807,7 @@ SEL_resolveClassMethod, sel);`
完整流程如下
-
+
@@ -2286,7 +2831,7 @@ QA:
上述动态消息解析后,会缓存起来吗?
-第一次消息动态解析后,只是将方法增加到 class 或者 meta-class 的 class_rw_t 中。然后会继续执行 `goto retry`,在 `gto retry` 的流程中,会先在 class 的方法缓存中查找有没有缓存,如果没有缓存,则会在 class 的 class_rw_t 中查找方法,找到并执行,同时还会吧方法保存到方法缓存中去。以便后续再次调用方法的时候更加高效。
+第一次消息动态解析后,只是将方法增加到 class 或者 meta-class 的 class_rw_t 中。然后会继续执行 `goto retry`,在 `gto retry` 的流程中,会先在 class 的方法缓存中查找有没有缓存,如果没有缓存,则会在 class 的 class_rw_t 中查找方法,找到并执行,同时还会把方法保存到方法缓存中去。以便后续再次调用方法的时候更加高效。
所以这个“会”缓存起来,只不过缓存的时机不是同步的,而是再次调用 `goto retry` 的时候,发现没有缓存,则在 class_rw_t 找查找到后,再缓存起来
@@ -2307,7 +2852,7 @@ Person *person = [[Person alloc] init];
知道 `objc_msgSend` 的流程,我们尝试给它修正下
-
+
方法1,增加一个兜底方法,然后利用 `class_addMethod` 动态增加方法实现
@@ -2341,7 +2886,7 @@ class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncod
方法3,也可以添加 c 语言方法
-
+
c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。
@@ -2454,7 +2999,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
-
+
```c
int __forwarding__(void *frameStackPointer, int isStret) {
@@ -2498,7 +3043,7 @@ int __forwarding__(void *frameStackPointer, int isStret) {
完整流程如下
-
+
@@ -2532,13 +3077,13 @@ int __forwarding__(void *frameStackPointer, int isStret) {
Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
-
+
调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,`resolveInstanceMethod` 没有处理对象方法,所以会报错
方法1:因为动态消息解析没有处理,则会开始走消息转发阶段。消息转发首先会调用 `- (id)forwardingTargetForSelector:(SEL)aSelector` 方法。(如果是对象方法则调用 `- (id)forwardingTargetForSelector:(SEL)aSelector`,如果是类方法则调用 `+ (id)forwardingTargetForSelector:(SEL)aSelector`)
-
+
方法2:如果消息转发里,`forwardingTargetForSelector` 返回了 nil,则开始调用方法签名 `methodSignatureForSelector` 方法和 `forwardInvocation` 方法
@@ -2566,15 +3111,20 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
}
```
-注意:`methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行
+注意:
-
+1. `methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行
+2. 方法签名 `methodSignatureForSelector` 必须正确,否则会获取参数 crash,报错 `Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSInvocation getArgument:atIndex:]: index (2) out of bounds [-1, 1]'` 的错误
+
+
上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。
-
+通过 `NSInvocation` 获取参数一般是从2开始,因为第一个是 self,第二个是 _cmd,第三个是 cost
+
+
@@ -2601,13 +3151,15 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
}
```
-
+
-
+
+注意:**实例对象有消息转发,类方法也有消息转发机制**。但是在 Xcode 中只可以提示 `-(id)forwardingTargetForSelector:(SEL)aSelector`
+不提示 `+(id)forwardingTargetForSelector:(SEL)aSelector` 所以有人文章说 OC 不支持类方法的动态方法解析和转发,这是错误的
@@ -2615,7 +3167,7 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 receiver(方法调用者)发消息(selector 方法名),就是 `objc_msgSend` 的工作流程,具体分为3个阶段:
-- 消息发送:主要是从当前类、当前类的父类...中查找
+- 消息发送:objc_msgSend 的完整流程
- 动态方法解析
- 消息转发
@@ -2632,7 +3184,7 @@ OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 rece
3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找
-
+
先根据 superclass 找到父类,然后在父类中也按照上面3点进行递归查找,即:
@@ -2747,17 +3299,19 @@ objc_msgSendSuper(arg, sel_registerName("class"))
其实2个方法本质上消息 receiver 都是 self,也就是当前的 Student,所以打印都是 Student
+
+
结论:`[super message]` 有2个特征
- super 消息的调用者还是 self
-- 方法查找是根据当前 self 的父类开始查找
+- 方法查找是根据当前 self 的父类的类对象方法列表开始查找
通过将代码转为 c++ 发现,super 调用本质就是 `objc_msgSendSuper`,实际不然
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
-
+
查看 objc4 源代码发现是一段汇编实现。
@@ -2845,7 +3399,7 @@ call - 调用函数
也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"`
-
+
@@ -2871,11 +3425,11 @@ call - 调用函数
Demo1
-
+
-上面的打印有没有有疑惑的?2个判断都是调用对象方法的 `isMemberOfClass` 、`isKindOfClass`
+上面的打印有没有有疑惑?2个判断都是调用对象方法的 `isMemberOfClass` 、`isKindOfClass`
由于 objc4 是开源的,查看 `object.mm`
@@ -2891,15 +3445,15 @@ Demo1
}
```
-`isMemberOfClass` 判断当前对象是不是传递进来的对象
+- `isMemberOfClass` 判断当前对象是不是传递进来的对象
-`isKindOfClass` 内部是一个 for 循环,第一次循环先拿当前类的类对象,判断是不是和传递进来的对象一样,一样则 return YES,否则先给 tlcs 赋值当前类的父类,然后走第二次判断,直到 cls 不存在(NSObject 的父类为 nil)。所以 `isKindOfClass` 其实判断的是当前类是传递进来的类,或者传递进来类的子类
+- `isKindOfClass` 内部是一个 for 循环,第一次循环先拿当前类的类对象,判断是不是和传递进来的对象一样,一样则 return YES,不一样则先给 tlcs 赋值当前类的父类,然后走第二次判断,直到 cls 不存在(NSObject 的父类为 nil)。所以 `isKindOfClass` 其实判断的是当前类是传递进来的类或子类
Demo2
-
+
下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass`
@@ -2915,29 +3469,30 @@ Demo2
}
```
-可以看到 `+(BOOL)isMemberOfClass:(Class)cls` 方法内部就是对当前类获取类对象,然后与传递进来的 cls 判断是否相等。由于是 `[Student isMemberOfClass:[Student class]])` `Student` 类调用类方法 `+isMemberOfClass` 所以类对象的类对象也就是元类对象,cls 参数也就是 `[Student class]` 是一个类对象,元类对象等于类对象吗?显然不是
+分析:
+
+第一类:`isMemberOfClass`
+
+可以看到 `+(BOOL)isMemberOfClass:(Class)cls` 方法内部就是对当前 receiver 类获取类对象,然后与传递进来的 cls 判断是否相等。因为本身就是类方法,方法 receiver 就是一个类对象,对类对象调用 `object_getClass` 方法,获取到的就是 receiver 类的元类对象
+
+- `[Student isMemberOfClass:[Student class]]` 等价于:先对 Student 类对象调用 `object_getClass` 方法,得到 Student 类的元类对象,等号左侧是 Student 的元类对象,等号右侧是 Student 类对象(cls 参数也就是 `[Student class]` 是一个类对象)元类对象等于类对象吗?显然不是显然不成立,输出0
+- `[Student isMemberOfClass:[NSObject class]]` 等价于:先对 Student 类对象调用 `object_getClass` 方法,得到 Student 类的元类对象,等号左侧是 Student 元类对象,等号右侧是 NSObject 类对象(cls 参数也就是 `[NSObject class]` 是一个类对象)元类对象等于类对象吗。显然不成立,输出0
想让判断成立,可以改为 `[Student isMemberOfClass:object_getClass([Student class])]` 或者 `[[Student class] isMemberOfClass:object_getClass([Student class])]`
-`+(BOOL)isKindOfClass:(Class)cls` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。
+第二类:`+isKindOfClass`
+可以看到 `+(BOOL)isKindOfClass:(Class)cls` 方法内部就是对当前 receiver 调用 `object_getClass`,由于自身就是类方法,所以receiver 就是类对象,调用方法后返回元类对象。for 循环内判断,是否是右边传入对象的元类或者元类的子类。
+- `[Student isKindOfClass:[Student class]]); ` 为什么会输出0?因为 `isKindOfClass` 底层是 for 循环对传入的 self 使用 `object_getClass((id)self)` 获取类对象,因为传入的已经是类对象,所以 `object_getClass((id)self)` 内部得到的是元类对象。右边是传入的类对象。等号左边的 Student 的元类对象,不等于等号右边的类对象。所以为 false,输出 0.
-QA:
+ 如何更改使得输出1?`[Student isKindOfClass:object_getClass([Student class])]`。右边也是元类对象,则为 True 输出1.
-1. `NSLog(@"%d", [Student isKindOfClass:[Student class]]); ` 为什么会输出0?
+- `NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); ` 为什么输出1?看右边的部分,调用 `isKindOfClass` 方法,本质上就是 Student 类的类对象,也就是 Student 元类,和传入的右边 `[NSObject class]` 判断是否想通过。
- 因为 `isKindOfClass` 底层是 for 循环对传入的 self 使用 `object_getClass((id)self)` 获取类对象,因为传入的已经是类对象,所以 `object_getClass((id)self)` 内部得到的是元类对象。右边是传入的类对象。等号左边的 Student 的元类对象,不等于等号右边的类对象。所以为 false,输出 0.
+ 第一次 for 循环当然不同,所以不能 return,会将 `tcls ` 走步长改变逻辑 `tcls = tcls->superclass`,也就是找到当前 Student 元类对象的父类。
- 如何更改使得输出1?`[Student isKindOfClass:object_getClass([Student class])]`。右边也是元类对象,则为 True 输出1.
-
-2. `NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); ` 为什么输出1?
-
- 看右边的部分,调用 `isKindOfClass` 方法,本质上就是 Student 类的类对象,也就是 Student 元类,和传入的右边 `[NSObject class]` 判断是否想通过
-
- 第一次 for 循环当然不同,所以不能 return,会将 `tcls ` 走步长改变逻辑 `tcls = tcls->superclass`,也就是找到当前 Student 元类对象的父类。
-
- 第二次 for 循环也一样不相等,Person 元类不等于 `[NSObject class]` 继续向上,直到 tcls = NSObject。此时还是不等,这时候 tcls 走步长改变逻辑,`tcls = tcls->superclass` NSObject 元类的 superclass 还是 NSObject。所以 for 循环内部的判断编委 ` [NSObject class] == [NSObject class]`,return YES。
+ 第二次 for 循环也一样不相等,Person 元类不等于 `[NSObject class]` 继续向上,直到 tcls = NSObject。此时还是不等,这时候 tcls 走步长改变逻辑,`tcls = tcls->superclass` NSObject 元类的 superclass 还是 NSObject。所以 for 循环内部的判断编委 ` [NSObject class] == [NSObject class]`,return YES。
@@ -2952,13 +3507,23 @@ QA:
}
```
-
+QA:`NSLog(@"%d", [Person isKindOfClass:[NSObject class]]);` 为什么输出1?
+其内部流程:
+- 先对 receiver 调用 `objcet_getClass` 方法,得到 Person 的元类对象 tcls
+- 开启 for 循环,判断 tcls 是否等于方法参数(也就是 NSObject 的类对象)
+- for 循环一开始不满足,则不断进行,令 tcls = tcls->superClass。for 循环结束的条件是 tcls 为 nil,所以最后找到 NSObject 的元类的时候,继续往上找,NSObject 元类对象的父类是 NSObject 的类对象,此时 tcls 就是 NSObject 的类对象,cls 也是 NSObject 类的类对象,相等,输出1.
+
+
+
+同理 `[NSObject isKindOfClass:[NSObject class]]` 也为 YES,工作流程和上面的类似。也就是 `[继承自 NSObject 及其继承自任何子类 isKindOfClass:[NSObject class]]` 都为 YES
综合练习:
-
+
+
+
@@ -2995,10 +3560,12 @@ QA:
程序运行什么结果?
-
+
为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"。我们来分析下:
+`[Person class]` 类对象是全局唯一的,存在于全局区。
+
第一、**方法调用本质就是寻找 isa 进行消息发送**
```objective-c
@@ -3008,6 +3575,10 @@ Person *person = [[Person alloc] init];
`[[Person alloc] init]` 在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。
+`[p sayHi]` 编译后 `objc_msgSend(p, @selector(sayHi))`
+
+
+
第二、**栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。**
这个流程其实和上面的代码一样的。所以可以正常调用
@@ -3023,9 +3594,34 @@ void test () {
方法内的变量存储在栈上,堆向上增长,栈向下增长。
-
+
-第三、**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程,本质就是对象地址 + Offset。此时就是 isa 地址 + 8字节偏移量)**
+第三,id 的本质是
+
+```objective-c
+typedef struct objc_object *id;
+```
+
+那 `objc_object` 的核心结构是什么?
+
+```objective-c
+struct objc_object {
+private:
+ isa_t isa;
+
+public:
+ // ISA() assumes this is NOT a tagged pointer object
+ Class ISA();
+
+ // getIsa() allows this to be a tagged pointer object
+ Class getIsa();
+ // ...
+}
+```
+
+所以可以看到找到对象 id,也就可以找到 isa 信息,也就可以获取到类对象信息,进而查找方法列表。
+
+第四、**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程,属性的地址 = 对象地址基础地址 + 属性的Offset。此时就是 isa 地址 + 8字节偏移量)**
上面代码可以类比类调用方法的流程。 obj 指针指向 Person 这块内存,给类对象发送 `sayHi` 消息也就是通过 obj 指针找到 isa,恰好 obj 指针指向的地址就是类对象的类结构体的地址,结构体成员变量第一个就是 isa 指针,结构体的其他成员变量就是类的其他属性,这里也就是 `_name`,所以我们给自定义的指针 `void *p` 调用 sayHi 方法,系统 runtime 在打印 name 的时候,会在 p 附近(下8个字节,因为 isa 是指针,长度为8)找 `_name` 属性,此时也就找到了 temp 字符串。
@@ -3036,11 +3632,20 @@ struct Person_IMPL {
}
```
+整体流程是:
+
+- 调用 `[p sayHi]` 方法的的流程是,编译后 `objc_msgSend(p, @selector(print))`
+- Runtime 会认为 p 是对象,然后寻找 p 的 isa 信息,即使 isa 是 union 类型,但系统根据 isa 寻找类对象的时候,是调用 `p 地址 & ISA_MASK` 获取类对象地址的
+- 也就是获取到了 cls 指向的 [Person class] 类对象地址。然后 Person 内存在对象方法 print,发起调用
+- 但内部访问的 self.name 本质就是:「知道对象的地址,如何访问属性值」。属性值 = 实例对象 baseAddress + 属性 offsetAddress,对于 name 也就是基地址偏移8位,找到 name
+- 由于方法内就是栈,由高地址向低地址增长(方法内从上到下,变量的地址依次降低)
+- 因为 obj 本身占8位,然后找 name 也就是获取到上一个8位的对象值,此时拿到了 temp,所以 Runtime 会认为 temp 就是所需要的 self.name
+
再看一个变体1
-
+
打印输出是因为 `*p` 类似 isa 指针。本身占用8字节空间,然后访问 `self->_name` 就是 `base + 8 = isa地址 + 8 ` 出的内存就是 name,`*p` 是在栈中,加8,就是向上声明的变量,当前情况下 `Address(*p) + 8` 就是 temp 变量。所以输出 ``
@@ -3048,7 +3653,7 @@ struct Person_IMPL {
再看一个变体2
-
+
分析: 搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。
@@ -3067,17 +3672,26 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
-
+
可能会疑问,知道了 super 调用本质,知道了会产生一个局部变量的结构体,但是结构体里面2个成员变量,找属性的时候,isa 下的8个字节会命中哪个?
-结构体 `objc_super` 存在2个成员变量,self 是第一个,`class_getSuperclass(objc_getClass("ViewController"))` 是第二个,self 地址更低。
+```c++
+struct objc_super {
+ __unsafe_unretained _Nonnull id receiver;
+ __unsafe_unretained _Nonnull Class super_class;
+};
+```
+
+结构体 `objc_super` 存在2个成员变量,self 是第一个,`class_getSuperclass(objc_getClass("ViewController"))` 是第二个,self 地址更低,会被认为是 Person 的 name 属性值(虽然栈内变量地址由高到低,但是结构体 objc_super 的2个成员变量顺序并不会改变,也就是相对位置不变。假设结构体地址为 structBase = 0x00007ff7bf961c28,也就是第一个成员变量 receiver 地址为 0x00007ff7bf961c28,由于 id 类型长度为8位,所以第二个成员变量 super_class 地址为 = 0x00007ff7bf961c28 + 8 = 0x7FF7BF961C30 )。
+
+假设 obj 地址为 0x7FF7BF961C20,所以 name 地址为 0x7FF7BF961C20 + 8 = 0x7FF7BF961C28,命中结构体的地址,按照 name 自身长度为8,也就取到了结构体第一个成员变量 self 的值。此时为 ViewController 对象。
画图如下,有助于理解
-
+
@@ -3092,7 +3706,7 @@ id rs = [NSObject valueForKey:@"isa"];
NSLog(@"%@", rs);
```
-
+
不会 Crash。输出的是 NSObject 的元类对象。因为 NSObject 调用的是类方法,先去元类对象的方法列表中查找,发现没有 `valueForKey` 方法。则继续从 NSObject 元类对象的父类对象,也就是 NSObject 的类对象上查找 `valueForKey` 方法。
@@ -3102,7 +3716,7 @@ NSLog(@"%@", rs);
查看了 objc 源码,会发现很多 NSObject 的基础方法:`+ (id)init`、`- (id)init` 等均有 `+` 、`-` 方法。
-
+
为什么这么设计?猜测是为了代码的健壮。
@@ -3119,7 +3733,7 @@ NSLog(@"%@", rs);
### 统计 App 中未响应的方法。给 NSObject 添加分类 `NSObject+ExceptionHunter.m`,利用 NSProxy 实现
-```objectivec
+```objective-c
#import "NSObject+ExceptionHunter.h"
#import
@@ -3133,7 +3747,9 @@ NSLog(@"%@", rs);
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
- // 收集上报
+ id receiver = anInvocation.target;
+ NSString *methodName = NSStringFromSelector(anInvocation.selector);
+ // 收集上报...
NSLog(@"%@方法未调用成功", NSStringFromSelector(anInvocation.selector));
}
@@ -3151,13 +3767,13 @@ Person *p = [Person new];
object_setClass(p, [Student class]);
```
-
+
### 动态创建类
-`objc_allocateClassPair`、`objc_registerClassPair` 成对存在
+必须先创建类:`objc_allocateClassPair`、再注册类(用于向运行时系统注册一个新创建的类及其元类):`objc_registerClassPair` 成对存在
动态创建类、添加属性、方法
@@ -3180,9 +3796,12 @@ void createClass (void) {
}
```
-
+
-runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`
+注意:
+
+- 运行时注册的类会在程序生命周期内持久存在(调用 copy、create 等出来的内存),不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`
+- 能否动态添加实例变量到已注册的类?**不能**。注册后类的内存布局已固定,修改会导致崩溃。
@@ -3208,7 +3827,9 @@ KVC 可以根据具体的值,去取出 NSNumber ,然后调用 intValue
-### 访问对象的所有成员变量信息
+### 访问对象的所有成员变量信息-字典转模型
+
+用到的 API 是 class_copyIvarList`
```objectivec
@property (nonatomic, strong) NSString *name;
@@ -3223,10 +3844,14 @@ for (int i =0 ; i
+
不够健壮体现在:
-- 没有考虑对象继承的情况,比如 Student 继承自 Person,那么 `[Student modelWithJSON:dict]` 转换就存在问题
-- 假设服务器返回的 json 有9个字段,本地对象有8个字段,怎么处理?
+- 假设服务器返回的 json 有9个字段,本地对象有8个字段,怎么处理?可能报错 `[ setNilValueForKey:]`
- 服务器给的有名字为 id 的数据,OC 对象的属性名不能叫 id,如何处理?
+- Person 中嵌套 Cat 呢?Person 对象有个 Cat 类型的属性
具体可以参考 YYModel
@@ -3323,10 +3970,14 @@ runtime 方法交换的本质,就是交换类对象、元类对象的 class_rw
```objectivec
@implementation UIControl (Monitor)
+ (void)load {
- Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
- Method method2 = class_getInstanceMethod(self, @selector(lbp_sendAction:to:forEvent:));
- method_exchangeImplementations(method1, method2);
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
+ Method method2 = class_getInstanceMethod(self, @selector(lbp_sendAction:to:forEvent:));
+ method_exchangeImplementations(method1, method2);
+ });
}
+
- (void)lbp_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
// 调用系统原来的实现
@@ -3380,6 +4031,26 @@ static void flushCaches(Class cls)
安全气垫就是对代码运行过程中出错的方法进行兜住,比如数组越界等。ROI 和带来的一些业务异常问题就见仁见智了。实现手段就是 runtime hook 然后更改方法实现。做一些安全判断。和网易大白的作者交流过,发现安全气垫的副作用,比如一个商品价格运算后异常,会 crash 这时候起码 crash 不能交易下单了。但是用了安全气垫,好处是不会 crash,缺点是给一个有问题的值,要么价格为0,要么为空。用户可以正常下单,这时候产生资损了。电商业务虾带来的业务异常问题比稳定性异常问题更严重。电商公司一般会给商家有资损保障,赔付率。
+不写代码是因为没有什么难的地方,难点在于策略的选择。
+
+
+
+### 单元测试的 mock
+
+单元测试框架很多都依赖了 Runtime 的能力,来 mock 方法返回值。比如 [OCMock](https://github.com/erikdoe/ocmock) 库。
+
+
+
+### 热修复
+
+动态替换方法实现修复线上 Bug。具体的可以看开源库 [JSPatch](https://github.com/bang590/JSPatch)
+
+
+
+### hook 库
+
+比如 [Aspects](https://github.com/steipete/Aspects) 的实现。
+
## 总结
diff --git a/Chapter1 - iOS/1.87.md b/Chapter1 - iOS/1.87.md
index 90ef615..c99a044 100644
--- a/Chapter1 - iOS/1.87.md
+++ b/Chapter1 - iOS/1.87.md
@@ -49,7 +49,7 @@
- 当某个类继承自 NSObject 的时候,如果没有其他属性,则这个类占据16个字节。`class_getInstanceSize` 占据 8 , `malloc_size` 占据 16
- 当某个类继承自 NSObject 的时候,如果有其他属性,则这个类占据16个字节。`class_getInstanceSize` 占据 16 , `malloc_size` 占据 16
- 方法二: 从源代码角度出发验证(从上大下)
+ 方法二: 从源代码角度出发验证(从上到下)
```c++
// NSObject.mm
diff --git a/Chapter1 - iOS/1.89.md b/Chapter1 - iOS/1.89.md
index 0ef8048..dfb663e 100644
--- a/Chapter1 - iOS/1.89.md
+++ b/Chapter1 - iOS/1.89.md
@@ -28,7 +28,7 @@ block(1, 2);
用指令`xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转为 c++
-
+
`ViewController.m` 的 `viewDidLoad` 函数为 `_I_ViewController_viewDidLoad`
@@ -111,6 +111,12 @@ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__vi
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
```
+简化为
+
+```c++
+block->FuncPtr(block, 1, 2);
+```
+
最后在 viewDidLoad 函数中通过结构体 impl 的成员 FuncPtr,调用了函数。
第二个参数:
@@ -132,7 +138,41 @@ QA:
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
```
-为什么 `block->FuncPtr` 可以直接访问,而不是 block 先访问 impl,再访问 FuncPtr?因为 __block_impl 就是 __main_block_impl_0 这个结构体的第一个变量地址(结构体特性)
+为什么 `block->FuncPtr` 可以直接访问,而不是通过 block 先访问 impl,再访问 FuncPtr?因为 `__block_impl` 就是 `__main_block_impl_0` 这个结构体的第一个变量地址(结构体特性),然后访问第一个结构体变量内的成员变量,等价于,直接访问结构体第一个成员变量的结构体变量
+
+举个例子
+
+```c++
+struct B { int x; };
+struct A { struct B b; int y; };
+
+struct A a;
+// 以下两个指针指向同一地址:
+struct B *b_ptr1 = &(a.b);
+struct B *b_ptr2 = (struct B *)&a; // 强制类型转换
+```
+
+`b_ptr1` 和 `b_ptr2` 是等价的,因为 `a` 的起始地址就是 `a.b` 的起始地址。
+
+因为 `__block_impl` 是 `__ViewController__viewDidLoad_block_impl_0` 结构起的第一个成员变量,所以 `block->FuncPtr` 会被转换为 `block->imp.FuncPtr`
+
+结构体访问成员变量的原理就是:**基地址 + 偏移量**(baseAddress + Offset)
+
+- `__block_impl` 的 `FuncPtr` 成员在其内部的偏移量是固定的(例如,假设 `isa` 占 8 字节,`Flags` 和 `Reserved` 各占 4 字节,则 `FuncPtr` 的偏移量为 `8 + 4 + 4 = 16` 字节)。
+- 无论通过 `__main_block_impl_0` 还是 `__block_impl` 的指针访问 `FuncPtr`,最终都是基于同一基地址(`block` 的起始地址)加上相同的偏移量(16 字节)来获取值。
+
+```c++
+struct block { // baseAddress
+ struct first {
+ isa;
+ Flags;
+ FuncPtr; // offset
+ Desc;
+ }
+ struct second;
+ // ...
+}
+```
上述的代码,等价于 `block->impl.FuncPtr`
@@ -200,7 +240,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
}
```
-
+
@@ -214,7 +254,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
-
+
@@ -239,11 +279,11 @@ printBlock();
用 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
-
+
概括如下:
-
+
@@ -261,7 +301,7 @@ printAgeBlock();
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
-
+
代码分析:
@@ -298,7 +338,7 @@ printInfoBlock();
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
-
+
@@ -339,7 +379,7 @@ age is 28, height is 176
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
-
+
@@ -420,7 +460,7 @@ block 截获变量可以分为:
用指令 `xcrun --sdk iphoneos clang -arch arm64 Person.m -rewrite-objc -o Person-arm64.cpp` 转换为 c++ 代码
-
+
@@ -439,12 +479,101 @@ block 截获变量可以分为:
}
```
+- `play` 方法等价于
+
+ ```c++
+ - (void)play:(id)self selector:(SEL)_cmd {
+ //...
+ }
+ ```
+
+ 所以 self 是局部变量,block 为了访问局部变量,会发生变量捕获。
+
- block 为了局部变量 self 的将来访问,结构体内部也增加了一个 Person 类型的 self,所以存在 self 的变量捕获。
-
-
所以,答案是会,因为 self 就是局部变量。 一个 oc 方法转换为 `void test(Person *self, SEL _cmd)` 形式,所以 self 也是局部变量,会被捕获。
+Tips:判断会不会捕获的本质就是判断某个对象、变量是不是局部变量,是局部变量则会捕获,否则不会捕获。
+
+举个例子:
+
+```objective-c
+// Person.h
+@interface Person : NSObject {
+ @public
+ NSString *_hobby;
+}
+- (void)testBlockCapture;
+@end
+
+@implementation Person
+ - (void)testBlockCapture {
+ void (^test)(void) = ^(void) {
+// NSLog(@"%@", _hobby);
+ };
+ test();
+}
+@end
+```
+
+利用 clang 转为 c++ `xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp`
+
+```c++
+struct __Person__testBlockCapture_block_impl_0 {
+ struct __block_impl impl;
+ struct __Person__testBlockCapture_block_desc_0* Desc;
+ __Person__testBlockCapture_block_impl_0(void *fp, struct __Person__testBlockCapture_block_desc_0 *desc, int flags=0) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+
+
+static void __Person__testBlockCapture_block_func_0(struct __Person__testBlockCapture_block_impl_0 *__cself) {
+
+}
+```
+
+可以看到没有捕获任何变量。但对上述代码进行调整
+
+```objective-c
+// Person.m
+- (void)testBlockCapture {
+ void (^test)(void) = ^(void) {
+ NSLog(@"%@", _hobby);
+ };
+ test();
+}
+
+```
+
+继续调为 C++
+
+```c++
+struct __Person__testBlockCapture_block_impl_0 {
+ struct __block_impl impl;
+ struct __Person__testBlockCapture_block_desc_0* Desc;
+ Person *self;
+ __Person__testBlockCapture_block_impl_0(void *fp, struct __Person__testBlockCapture_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+
+
+static void __Person__testBlockCapture_block_func_0(struct __Person__testBlockCapture_block_impl_0 *__cself) {
+ Person *self = __cself->self; // bound by copy
+
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_vf_05htlrfx42qck4yh6bsnh3yr0000gn_T_Person_28fd90_mi_4, (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_hobby)));
+ }
+```
+
+观察可以看到 Person 的 self 被捕获进去了,本质上是因为 `testBlockCapture` 方法内的 block 访问了成员变量 `_hobby`,本质上还是访问了 `self->hobby` ,也就是对局部变量 self 进行了访问,所以存在变量捕获。
+
## block 类型
@@ -453,13 +582,13 @@ block 截获变量可以分为:
我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下
-
+
也就是说: `__NSGlobalBlock__` -> `NSBlock` -> `NSObject`。
继续验证,Demo2
-
+
同时利用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
@@ -484,7 +613,7 @@ block 的类型可以通过 isa 或者 class 方法查看,最终都是继承
这3种 block 在内存中的排布如下图:
-
+
@@ -494,21 +623,27 @@ block 的类型可以通过 isa 或者 class 方法查看,最终都是继承
Demo:
-由于 ARC 默认会做一些优化,我们将工程的 ARC 关掉(Build Setting 里 Automatic Reference Counting 设置为 No)
+由于 ARC 默认会做一些优化,为了彻底的研究 block,我们将工程的 ARC 关掉(Build Setting 里 Automatic Reference Counting 设置为 No)
-
+
分析:
- block1 是 `__NSGlobalBlock__` ,此**类型的 block 用结构体实例的内容不依赖于执行时的状态,所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。**
+
- block2 是 `__NSStackBlock__`.
-- 为什么执行 block2 的时候发生了 crash?猜测由于在 test 方法内给 block2 赋值,也就是在栈上定义和捕获了栈上的变量 age,test 方法结束,可能栈上的数据消失或者乱了,所以这个情况下调用 block2 会 crash。
-针对 block2 的问题该怎么处理?
+- 但为什么执行 block2 的时候发生了 crash?
-当 `__NSStackBlock__` 调用 copy 方法后会变为 `__NSMallocBlock__`。如下图:
+ 猜测由于在 test 方法内给 block2 赋值,也就是在栈上定义和捕获了栈上的变量 age。等 test 方法结束,可能栈上的数据消失或者内存指向某个不可预知的地方,所以这个情况下调用 block2 会 crash。
-
+如何解决该问题?
+
+这类问题概括就是:`__NSStackBlock__` 栈上的 block 及其捕获的数据出了栈后,内存不稳定,将来某个时间调用 block,可能会发生 crash。需要把数据和内存稳定下来,不要交由栈自动处理。想办法把栈上的数据移动到堆上,由程序员自己管理内存。
+
+当 `__NSStackBlock__` 调用 `copy` 方法后会变为 `__NSMallocBlock__`。如下图:
+
+
@@ -524,7 +659,7 @@ Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 `
| `__NSStackBlock__` | 访问了 auto 变量 |
| `__NSMallocBlock__` | `__NSStackBlock__` 调用了 copy 方法 |
-调用 copy 方法
+调用 `copy` 方法
| Block 类 | 原本位置 | 复制效果 |
| --------------------------- | ------------ | ---------- |
@@ -532,17 +667,39 @@ Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 `
| `__NSConcreteGlobalBlock__` | 程序的数据段 | 什么也不做 |
| `__NSConcreteMallocBlock__` | 堆 | 引用计数+1 |
+Demo
+
+```objective-c
+int age = 30;
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ int height = 175;
+ NSLog(@"数据段:%p", &age); // 数据段:0x100008938
+ NSLog(@"栈区:%p", &height); // 栈区:0x7ff7bfeff400
+ NSLog(@"堆区:%p", [[NSObject alloc] init]); // 0x600000014000
+ NSLog(@"数据段:%p", [Person class]); // 0x1000088c8
+ }
+}
+```
+
+可以看到:
+
+- age 是全局变量,在数据段区
+- height 是局部变量,在栈区
+- `[[NSObject alloc] init]` 是 alloc 的对象,在堆区
+- `[Person class]` 看到地址接近数据段,所以也在数据段区
+
## 内存管理
### ARC 针对 block 的优化
-#### block 作为函数返回值,并且捕获了 auto 变量
+#### 1. block 作为函数返回值,并且捕获了 auto 变量
MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也就是栈上定义的 block,函数调用结束后可能一些相关数据就释放了,存在潜在风险。
-
+
@@ -550,24 +707,51 @@ MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也
MRC 下如果函数返回值是 block,且 block 内做了 auto 变量捕获的逻辑,编译器会报错:`Returning block that lives on the local stack`。此时 block 应该为 `__NSStackBlock__`
-
+
+
+即使编译器不报错,也存在很大风险,因为 block 创建在栈上,函数返回后栈内存可能被回收,导致后续访问野指针。
+
+需要:
+
+- 复制到堆:使用 `copy` 方法将栈 block 复制到堆,延长生命周期
+- 自动释放:返回堆 block 时,通常用 `autorelease` 标记,遵循 MRC 的命名规则(非 `alloc/new/copy` 方法返回自动释放对象)
+
+改正:
+
+```objective-c
+HelloBlock generateBlock(void) {
+ int age = 28;
+ HelloBlock block = ^(void) {
+ NSLog(@"Hello block, age is %d", age);
+ };
+ return [[block copy] autorelease];
+}
+```
+
+内存管理总结:
+
+- 返回 block 前:务必使用 `[[block copy] autorelease]`
+- 调用者:若需长期持有返回的 block,应手动 `retain` 并在不再使用时 `release`
+- 命名规范:若方法名包含 `alloc/new/copy`,返回对象由调用者释放;否则返回 `autorelease` 对象。
-改为 ARC,看看
+另外的改法是,在 Build Setting 中改为 ARC,看看
-
+
也就是说,ARC 模式下,当 block 捕获了 auto 变量,并且作为函数返回值的时候,ARC 会自动调用 copy 方法,将 `__NSStackBlock__` 变为 `__NSMallocBlock__`
+
+
Demo1:
-```objectivec
+```objective-c
MyBlock block;
{
- Person *person = [[Personalloc] init];
+ Person *person = [[Person alloc] init];
block = ^{
NSLog(@"block called");
};
@@ -631,15 +815,15 @@ ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBloc
-#### ARC 针对强指针指向的 block 会调用 copy
+#### 2. 将 block 赋值给强指针的时候会调用 copy(ARC 针对强指针指向的 block 会调用 copy)
MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__`
-
+
改为 ARC
-
+
@@ -648,50 +832,61 @@ MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__`
- 捕获了 auto 变量的 `__NSStackBlock__`,ARC 下调用 copy 会变为 `__NSMallocBlock__`
- 没有捕获变量的 `__NSGlobalBlock__`,ARC 下调用 copy 依旧为 `__NSGlobalBlock__`
+#### 3. block 作为 Cocoa API 中的方法名含有 usingBlock 的方法参数时
+
+```objective-c
+NSArray *array = @[];
+[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+
+}];
+```
+
+#### 4. block 作为 GCD API 的方法参数时
+
+```objective-c
+dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+
+});
+```
+
#### 总结
-在 ARC 下,编译器会根据情况,自动将战上的 block 复制到对上,比如:
+在 ARC 下,编译器会根据情况,自动将栈上的 block 复制到堆上,比如:
-- block作为函数返回值时
+- block 作为函数返回值时
- 将 block 赋值给 `__strong` 指针时
- block 传递给 Cocoa API 中名字含有 usingBlock 的方法参数时
- block 传递给 GCD 的方法参数时
-
-
-
-
-
-
ARC 下:block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,捕获的对象将会在 block 销毁后销毁
-
+
MRC 下:block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,因为是 MRC,所以需要手动管理内存。会发现对象将在离开作用域后立马销毁,不会被 block 所捕获。
-
+
MRC,对 block 加 copy,变为 `__NSMallocBlock__` 呢?
-
+
ARC 下对 block 引用的对象加 `__weak` 修饰呢?
-
+
用指令 `xcrun --sdk iphoneos clang -arch arm64 main.m -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 -o main-arm64.cpp` 转换为 c++ 进行分析看看。注意,因为 weak 涉及运行时,需要在 clang 后添加 runtime 参数
-
+
如果对 Person 不加 `__weak` 修饰,block 结构体内部将会是`__strong`。
-
+
@@ -735,27 +930,33 @@ static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_objec
Demo1:
-
+
+
+说明:ARC 环境下,GCD 的 block 会自动被拷贝到堆上 `__NSMallocBlock__`,堆上的 block 会对使用的对象进行 copy,所以 person 引用计数+1,则在 GCD block 执行完毕后才 release
Demo2
-
-
+
+- 栈空间上的 block 是不会对对象进行保命的(不管是 ARC 还是 MRC,都不调用 retain、copy 方法)。
+- ARC 下, block 如果访问了 auto, static 变量,则属于 `__NSStackBlock__`,ARC 下用强指针指向,则会变为 `__NSMallocBlock__ 堆上的 block, 是会对对象进行保命的。GCD 的 block 会自动拷贝到堆上,属于 `\__NSMallocBlock__`,也会对对象进 行 copy 保命。
+- 栈空间的 block 调用 copy 方法会变为堆空间的 block,会对 block 内使用的对象调用 retain、copy 方法进行保命。
马上执行了 Person 的 dealloc 方法。因为 `__weak` 修饰,block 内部的 `_Block_object_assign` 会根据 `__strong` 为对象引用计数 +1,`__weak` 则引用计数不变。所以是 `__weak` 修饰,出离作用域则立马会释放 Person 对象。
`_Block_object_assign` 会根据内存修饰符来对内存进行操作。
+- `_block_object_assign` 函数会根据 auto 变量的修饰符(`__strong`、`__weak`、`__unsafe_unretained`)做出相应的操作,类似 retain(形成强、弱引用)
+
Demo3
-
+
因为 GCD 是给 MainQueue 添加任务的,所以是串行,2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后,Person 才释放。
Demo4
-
+
@@ -880,7 +1081,7 @@ MyBlock block = ^{
转为 C++
-
+
```c++
struct __Block_byref_age_0 {
@@ -924,7 +1125,7 @@ block 内部的函数在修改 age 的时候其实就是通过 `__main_block_imp
-
+
@@ -936,7 +1137,7 @@ QA:为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block
看个有趣的例子,验证下 __block 的效果
-
+
转换成 c++ 可以看到
@@ -981,7 +1182,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
对` __block` 修饰的对象,clang 转换为 c++ 后如下:
-
+
@@ -994,7 +1195,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
注意:
-
+
@@ -1006,7 +1207,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
-Demo:知道 `__block` 的本之后,下面打印的 age 的地址是 struct 里面哪个的值?
+Demo:知道 `__block` 的本质之后,下面打印的 age 的地址是 struct 里面哪个的值?
```objectivec
__block int age = 27;
@@ -1064,7 +1265,7 @@ int main(int argc, const char * argv[]) {
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
-
+
```c
// 0x0000000105231f70
@@ -1102,7 +1303,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
- dispose 函数会调用 `_Block_object_dispose` 函数
-- `_Block_object_dispose` 函数会自动释放 `__block` 修饰的变量(release)
+- `_Block_object_dispose` 函数会自动释放引用的 auto 变量,类似于 release
@@ -1124,7 +1325,95 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
## `__forwarding` 的设计
-看一个例子,`__block` 如何修改外部变量
+Demo1
+
+```objective-c
+__block int age = 27;
+NSLog(@"1: age is %d, address is %p", age, &age);
+
+age = 30;
+NSLog(@"4: age is %d, address is %p", age, &age);
+// console
+1: age is 27, address is 0x7ff7bb181c28
+4: age is 30, address is 0x7ff7bb181c28
+```
+
+利用 `xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m -o ViewController-arm64.cpp` 转成 c++ 研究下
+
+```c++
+struct __Block_byref_age_0 {
+ void *__isa;
+ __Block_byref_age_0 *__forwarding;
+ int __flags;
+ int __size;
+ int age;
+};
+
+static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
+ ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
+ int hegith = 175;
+ __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_vf_05htlrfx42qck4yh6bsnh3yr0000gn_T_ViewController_450701_mi_0, (age.__forwarding->age), &(age.__forwarding->age));
+ (age.__forwarding->age) = 30;
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_vf_05htlrfx42qck4yh6bsnh3yr0000gn_T_ViewController_450701_mi_1, (age.__forwarding->age), &(age.__forwarding->age));
+}
+```
+
+可以看到:
+
+- age 没有在任何一个 block 中访问(也就不存在变量捕获的问题),仅仅是被 `__block` 修饰,编译器也会转为结构体 `struct __Block_byref_age_0`
+
+- 通过 `__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};` 可以看出此时 `__Block_byref_age_0` 结构体中的 `__forwarding` 成员变量指针指向栈空间上 age 的地址(也就是结构体 `__Block_byref_age_0` 自身的地址)
+
+ 大概如下
+
+ ```c++
+ // 栈上创建 __Block_byref_age_0 结构体实例
+ __Block_byref_age_0 age = {
+ .__isa = NULL,
+ .__forwarding = &age, // 指向自己(栈地址)
+ .__flags = 0,
+ .__size = sizeof(__Block_byref_age_0),
+ .age = 27
+ }
+ ```
+
+- NSLog 打印 age 的值和地址的时候,都是 `(age.__forwarding->age), &(age.__forwarding->age))`通过结构体变量的成员变量 `__forwarding` 指针指向的自身,再去访问成员变量的
+- `(age.__forwarding->age) = 30;` 赋值的时候也是通过结构体变量的成员变量 `__forwarding` 指针指向的自身去赋值的
+
+Tips:
+
+在 C 和 Objective-C 中,结构体位于栈还是堆上,取决于声明方式。
+
+- 在栈上创建:当结构体作为局部变量在函数内部声明时,它会直接分配在栈上
+- 在堆上创建:当使用动态内存分配函数(如`malloc`、`calloc`或Objective-C的`alloc`)时,结构体会在堆上分配
+
+``` objective-c
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ // 栈上
+ struct Person {
+ char name[20];
+ int age;
+ };
+ struct Person p1; // p1 分配在栈上
+ p1.age = 25;
+ NSLog(@"栈地址:%p", &p1); // 输出栈地址
+
+ // 堆上
+ struct Student {
+ char name[20];
+ int age;
+ };
+
+ struct Student *st = malloc(sizeof(struct Student)); // p2 指向堆内存
+ st->age = 30;
+ NSLog(@"堆地址:%p", st); // 输出堆地址
+ free(st); // 需要手动释放
+}
+```
+
+Demo2 : `__block` 如何修改外部变量
```objective-c
- (void)viewDidLoad {
@@ -1151,16 +1440,16 @@ in block: age = 28, address is 0x600000464938
-
+
分析:
-- `__block` 修饰的外部变量将会被封装为一个结构体对象,该结构体对象内有一个 `__forwarding` 成员变量
+- 被 `__block` 修饰的 int age 将会被封装为一个结构体,该结构体内有一个 `__forwarding` 成员变量
```c++
struct __Block_byref_age_0 {
void *__isa;
- __Block_byref_age_0 *__forwarding;
+ __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
@@ -1173,6 +1462,12 @@ in block: age = 28, address is 0x600000464938
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
````
+- block 通过指针持有栈上的 `__Block_byref_age_0` 结构体
+
+- 当发现 block 是函数返回值或者被一个强指针指向的时候,编译器生成 `_Block_copy()` 函数调用,将 block 从栈复制到堆。
+
+- 如果 block 捕获了 `__block` 变量,编译器会调用 `_Block_object_assign()`,将 `__Block_byref_age_0` 结构体从栈复制到堆。同时修改 `__Block_byref_age_0` 结构体的 `__forwarding` 指针,指向堆上的结构体地址。
+
- block 内部代码,将被封装为一个新的函数 `__ViewController__viewDidLoad_block_func_0`,其内部通过结构体指针` _cself` 的 age 成员变量,获取到 `__Block_byref_age_0` 指针,该指针命名为 age。然后通过 age 指针访问到结构体的 `__forwarding` 成员变量,该成员变量指向的是结构体自己,然后再访问 age 拿到真正的 age 进行修改。
```c++
@@ -1185,7 +1480,7 @@ in block: age = 28, address is 0x600000464938
- 第一行输出 `1: age = 27, address is 0x7ff7b0faebf8` 是因为此时 age 在栈上,高地址 `0x7ff7b0faebf8`
-- 第二行输出 `2: age = 27, address is 0x600000464938` 是因为 block 被拷贝到此对上,内部对于使用到的` __block` 变量也会拷贝到堆上,是通过一个结构体对象来实现的。由于在栈上,地址变为 `0x600000464938`,相较于栈上的地址,地址变低了。
+- 第二行输出 `2: age = 27, address is 0x600000464938` 是因为 block 被拷贝到堆上,内部对于使用到的` __block` 变量也会拷贝到堆上,是通过一个结构体对象来实现的。由于在栈上,地址变为 `0x600000464938`,相较于栈上的地址,地址变低了。
- 将 block 从栈拷贝到堆上时,block 所捕获的 `__block` 变量也会从栈拷贝到堆上,但是此时我们在该函数的作用域内(即 block 外)仍然是可以对 age 变量进行修改的
@@ -1197,46 +1492,261 @@ in block: age = 28, address is 0x600000464938
- 第五行输出 `4: age = 29, address is 0x600000464938` 是因为此时通过栈上的 age 结构体,通过成员变量 `__forwarding` 指向对上的结构体地址,然后再通过指向堆上的结构体的 age 成员变量已经被修改为 29 了
-总结下:
+通过 Demo1 和 Demo2 总结下:` __forwarding` 的作用是什么?为什么这么设计
-那么` __forwarding` 的作用是什么?为什么这么设计
-
-
+
-- 当 block在栈中时,`__Block_byref_age_0` 结构体内的 `__forwarding` 指针指向栈上的结构体自己
+- 当 block在栈中时,栈上的 `__Block_byref_age_0` 结构体内的 `__forwarding` 指针指向栈上的结构体自己
-- 而当 block 被复制到堆中时,栈中的`__Block_byref_age_0` 结构体也会被复制到堆中一份,而此时栈中的 `__Block_byref_age_0` 结构体的成员变量 `__forwarding` 指针指向的就是堆中的 `__Block_byref_age_0`结构体,堆中 `__Block_byref_age_0`结构体内的 `__forwarding` 指针依然指向自己,此时再访问成员变量 age 就可以修改堆上的值
+- 当 block 被复制到堆中时,栈中的`__Block_byref_age_0` 结构体也会被复制到堆中一份,而此时栈中的 `__Block_byref_age_0` 结构体的成员变量 `__forwarding` 指针指向的就是堆中的 `__Block_byref_age_0`结构体,堆中 `__Block_byref_age_0`结构体内的 `__forwarding` 指针依然指向自己,此时再访问成员变量 age 就可以修改堆上的值
+
+
+
+完整流程:
+
+- 初始阶段:`__block` 变量(结构体)在栈上,`__forwarding` 指向栈地址
+- block 复制到堆时:`__block` 结构体被复制到堆,原来栈上结构体变量 `__forwarding` 指针被重定向到堆副本(指向堆上的地址)
+- 最终结果:所有代码(无论 block 内外)通过 `__forwarding` 访问堆上的副本,因此地址看似“不变”,实际是 `__forwarding` 指针在幕后重定向
+
+这种设计既保证了性能(避免不必要的堆分配),又确保了 Block 内外对 `__block` 变量修改的同步性。
+
+所以存在2方面的优点:
+
+- 性能优化:如果 Block 未被复制到堆(例如仅在栈上使用),则无需为 `__block` 变量分配堆内存。`__forwarding` 指针的初始设计允许“按需复制”。
+- 统一访问逻辑:无论 `__block` 变量在栈还是堆,代码中访问 `age` 时,始终通过 `age.__forwarding->age`,编译器隐藏了实际存储位置的差异
+
+`__forwarding` 指针的设计是为了解决 **Block 在栈(Stack)和堆(Heap)之间迁移时的内存访问一致性问题**,并确保对 Block 及其捕获变量的操作始终安全可靠
**一言以蔽之,`__forwarding` 指针是为了在 `__block` 变量从栈复制到堆上后,在 block 外对 `__block` 变量的修改也可以同步到堆上实际存储的 `__block` 变量的结构体上。也就是抹平栈、堆上对变量操作的差异。**
+Tips:实现 block 拷贝及其捕获对象的函数是 `_Block_copy`,工作流程如下:
+
+- 检查 block 类型
+
+ 首先通过 Block 的 `flags` 字段判断其当前存储位置:
+
+ - 全局 Block(`BLOCK_IS_GLOBAL`):直接返回原 Block,无需复制(全局 Block 存储在数据区,生命周期与程序一致)。
+ - 堆 Block(`BLOCK_NEEDS_FREE`):增加引用计数后返回原 Block(堆 Block 已由 ARC 管理)。
+ - 栈 Block:需要复制到堆上,进入下一步流程。
+
+ ```c++
+ void *_Block_copy(const void *arg) {
+ struct Block_layout *src = (struct Block_layout *)arg;
+ if (src == NULL) return NULL;
+
+ // 全局 Block 直接返回
+ if (src->flags & BLOCK_IS_GLOBAL) {
+ return src;
+ }
+
+ // 堆 Block 增加引用计数
+ if (src->flags & BLOCK_NEEDS_FREE) {
+ src->flags |= BLOCK_REFCOUNT_MASK;
+ return src;
+ }
+
+ // 栈 Block 复制到堆
+ // ...
+ }
+ ```
+
+- 分配堆内存并复制结构体值
+
+ 为栈 Block 分配堆内存,并复制其内容:
+
+ - 内存分配:使用 malloc 分配与栈上结构体一样大的堆空间
+ - 结构体复制:将栈上的 block_layout 包含函数指针等都复制到堆上
+
+ ```c++
+ struct Block_layout *dest = malloc(src->descriptor->size);
+ memmove(dest, src, src->descriptor->size); // 复制结构体
+ ```
+
+- 更新 block 标志位
+
+ 复制后设置 block 的标志位:
+
+ - 标记为堆 block:`BLOCK_NEEDS_FREE` 表示需要手动释放
+ - 初始化引用计数:引用计数设为 1(后续通过 `_Block_release` 管理)
+
+ ```c++
+ dest->flags &= ~BLOCK_REFCOUNT_MASK; // 清除原有引用计数
+ dest->flags |= BLOCK_NEEDS_FREE | 1; // 标记为堆 Block,引用计数=1
+ ```
+
+- 处理捕获的变量。调用 `descriptor` 中的 `copy` 助手函数,处理捕获的变量
+
+ - 对象类型变量:对 `__strong` 修饰的对象调用 `retain`(符合 ARC 规则)
+ - `__block` 变量:将栈上的 `__Block_byref_xxx` 结构体复制到堆,并更新 `__forwarding` 指针
+
+- 返回堆上的 block:最终返回堆上的 block,供后续使用
+
+
+
## Block 内存引用
对于` __block` 修饰的变量进行研究
+Demo0
+
+
+
+可以看到:
+
+- block 访问了外部的变量,则会在 block 的底层实现即 `__ViewController__viewDidLoad_block_impl_0` 内增加一个 `__Block_byref_age_0 *age` 成员变量
+- 被 `__block` 修饰的基础数据类型 int 会被编译器自动创建为一个结构体 `__Block_byref_age_0` ,其内部的成员变量 age 存储真实的值
+
Demo1
-
+
-
+
Demo2
-
+
-
+
-分析:
+Test0:
-- block 结构体里面的针对变量生成的结构体新对象,都是 strong 指针
-- block 所捕获的对象是` __weak` 还是` __strong` 决定的是新生成结构体对象里面的对象内存访问修饰符。
+```c++
+int age = 20;
+void (^block)(void) = ^(void) {
+ NSLog(@"%p", &age);
+};
+
+
+struct __ViewController__viewDidLoad_block_impl_0 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ int age;
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+```
+
+Test1:
+
+```c++
+__strong NSObject *obj = [[NSObject alloc] init];
+NSLog(@"1 %p", obj);
+void (^block)(void) = ^(void) {
+ NSLog(@"%p", obj);
+};
+block();
+
+
+struct __ViewController__viewDidLoad_block_impl_0 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ NSObject *__strong obj;
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+```
+
+Test2:
+
+```c++
+_block NSObject *obj = [[NSObject alloc] init];
+__weak NSObject *weakObj = obj;
+NSLog(@"1 %p", weakObj);
+void (^block)(void) = ^(void) {
+ NSLog(@"%p", weakObj);
+};
+block();
+
+
+struct __Block_byref_obj_0 {
+ void *__isa;
+__Block_byref_obj_0 *__forwarding;
+ int __flags;
+ int __size;
+ void (*__Block_byref_id_object_copy)(void*, void*);
+ void (*__Block_byref_id_object_dispose)(void*);
+ NSObject *__strong obj;
+};
+
+struct __ViewController__viewDidLoad_block_impl_0 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ NSObject *__weak weakObj;
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSObject *__weak _weakObj, int flags=0) : weakObj(_weakObj) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+```
+
+Test3:
+
+```objective-c
+NSObject *obj = [[NSObject alloc] init];
+__block __weak NSObject *weakObj = obj;
+
+NSLog(@"1 %p", weakObj);
+void (^block)(void) = ^(void) {
+ NSLog(@"%p", weakObj);
+};
+
+struct __Block_byref_weakObj_0 {
+ void *__isa;
+__Block_byref_weakObj_0 *__forwarding;
+ int __flags;
+ int __size;
+ void (*__Block_byref_id_object_copy)(void*, void*);
+ void (*__Block_byref_id_object_dispose)(void*);
+ NSObject *__weak weakObj;
+};
+
+struct __ViewController__viewDidLoad_block_impl_0 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ __Block_byref_weakObj_0 *weakObj; // by ref
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_weakObj_0 *_weakObj, int flags=0) : weakObj(_weakObj->__forwarding) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+```
+
+可以看出:
+
+- Test0:如果基础数据类型,没有用 `__block` 修饰,则 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,只会增加一个基础成员变量 `int age`
+
+- Test1:如果是一个 strong 指针指向的对象,则会在 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,只会增加一个 strong 对象类的成员变量 `NSObject *__strong obj;`
+
+- Test2: 如果一个对象被 `__block` 修饰,但是是强指针类型的,那么生成的被 `__block` 修饰的结构体 `__Block_byref_obj_0` 内只会有一个强指针指向的成员变量;如果一个仅仅是 weak 指针的对象(没有被 `__block` 修饰),那么在 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,只会存在一个 weak 指针的成员变量 ` NSObject *__weak weakObj;`
+
+- Test3: 如果一个对象被 `__block` 修饰,同时是弱指针类型的,则在 block 底层的结构体 `__ViewController__viewDidLoad_block_impl_0` 内,仅仅会存在一个强指针指向的成员变量 ` __Block_byref_weakObj_0 *weakObj` ,指向被 `__block` 底层生成的结构体 `__Block_byref_weakObj_0`, `__Block_byref_weakObj_0` 结构体内会存在一个 weak 指针指向的成员变量,指向堆上的 weakObj
+
+
+
+通过上面例子的源码可以看到:
+
+- block 结构体里面的针对变量生成的结构体新对象,都是用 strong 指针指向的
+- block 所捕获的对象是` __weak` 还是` __strong` 决定了新生成结构体对象里面的对象内存访问修饰符。
```c
int main(int argc, const char * argv[]) {
@@ -1299,7 +1809,13 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
为什么会存在循环引用?block 会对截获的变量是对象类型,会把所有权也进行捕获。为什么 strong 类型的对象,会造成对象和 block 的循环引用
+看个 Demo
+
+
+可以看到 block 放在堆上的时候(被抢指针指向、作为返回值)的时候,如果 block 内部访问了强指针指向的对象,则会发生循环引用。
+
+可以看到 Person 对象的 dealloc 方法没有执行,里面的打印信息没有输出。
### ARC 下
@@ -1336,6 +1852,12 @@ Person *p = [[Person alloc] init];
[p test];
```
+
+
+
+
+
+
方法1: `__weak` 修饰。`__weak typeof(self) weakself = self;`
方法2: `__unsafe_retained` 修饰。`__unsafe_unretained typeof(self) weakself = self;`
@@ -1354,7 +1876,7 @@ p.block();
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak`
-
+
@@ -1374,7 +1896,31 @@ strongSelf 的目的是因为一旦进入 block 执行,假设不允许 self
block 执行完后这个 strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。
+1. **防止对象在 block 执行过程中被释**
+ - 弱引用的风险:`weakself`是对`self`的弱引用,不会增加其引用计数。如果在Block开始执行后,`self`被其他部分释放,后续对`weakself`的访问将指向无效内存,导致崩溃。
+ - 强引用的保障:通过`__strong`修饰的`strongSelf`,在 block 内部临时强引用`self`,确保在 block 执行期间`self`不会被释放。即使外部已经不再持有`self`,只要 block 在执行,`strongSelf`会保持`self`存活。
+
+2. ### **避免悬垂指针(Dangling Pointer)**
+
+ ```objective-c
+ // bad
+ __weak typeof(self) weakself = self;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // 假设此时 weakself 已被释放
+ [weakself doSomething]; // 访问已释放对象,崩溃!
+ });
+
+ // good
+ __weak typeof(self) weakself = self;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // 假设此时 weakself 已被释放
+ __strong typeof(self) strongSelf = weakself;
+ if (strongSelf) {
+ [strongSelf doSomething]; // 访问已释放对象,崩溃!
+ }
+ });
+ ```
## 总结
diff --git a/Chapter1 - iOS/1.91.md b/Chapter1 - iOS/1.91.md
index 86ffaa4..aef2391 100644
--- a/Chapter1 - iOS/1.91.md
+++ b/Chapter1 - iOS/1.91.md
@@ -1,17 +1,709 @@
# DYLD 及 Mach-O
+> 动态库还是静态库对包体积影响大?
+>
+> 动态库、静态库编译链接都做了哪些事情?链接的要素是什么?
+>
+> Moudle 是什么?
+>
+> 如果这些问题不是很清楚,可以带着问题看看本文。
+
+
+
什么是 DYLD?dynamic loader,动态加载器。在 MacOS/iOS 中,是使用 `/usr/lib/dyld` 程序来加载动态库的。
-## 从源文件到可执行文件做了什么?
+## 一、工程多环境配置
-问:源文件 -> 可执行文件,是不是只需要经过编译就够了?
+- Project:包含了项目的所有代码、资源文件、所有信息
+
+- Target:对指定代码和资源文件的具体(特定)构建方式
+
+- Scheme:对指定 Target 的环境配置
+
+- xcconfig:便捷化的方式管理编译、链接等配置
+
+传统的在 GUI 面板上操作不在本文研究范畴。对于 Build Settings 各个 key 不知道是什么作用的,可以查看 [Xcode Build Settings Reference](https://help.apple.com/xcode/mac/current/#/itcaec37c2a6) 和 [Xcode Project Management Guide](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/XcodeBuildSystem/)
+
+
+
+### 1. 多 Target 的方式
+
+举个例子,我们经常会遇到在不同环境下,根据环境的不同,网络接口请求不同的 url。常见的一幕是:给 QA 测试不同环境的 App,手动更改 baseUrl 地址,然后打包让下载。太低效了。
+
+所以需要做:
+
+1. 在 Xcode 中创建多个 Target。选择 TARGETS 模块,选择原有的 TARGET,点击 **Duplicate**,即可创建新的 TARGET 和对应的 plist 文件
+
+2. 为了区分不同环境,还需要设置 OC 和 Swift 各自的编译宏。
+
+ - OC:需要在 `Build Settings` 中的 `Preprocessor Macros` 中添加,可以区分不同的 **Configuration**(工程自带的是 Debug、Release。可以在 PROJECT 中添加新的 Configuration。假设在 Configuration 里再增加一个 Beta。那就可以在这里设置)。
+
+ OC 自带 `DEBUG` 宏定义来区分是否是 Debug 环境。只有在 Beta 下 BETA 宏为1,其他为0。
+
+ 要增加新的参数,选中对应的 Configuration,双击后在弹出的面板后,按照 **DEBUG=1** 的格式添加
+
+
+
+ - 对于 Swift 则有不同的编译器。在 Build Settings 中的 **Other Swift Flags** 里选择对应的 Configuration,按照 **-DDEV** 的格式添加。**-D** 是必须要存在的,后面要加的 Flag,就紧跟在后面
+
+
+
+
+
+
+
+操作了一番下来,我们可以发现一些缺点:
+
+1. 操作繁琐。需要添加 OC、Swift 的宏定义。
+2. 资源重复,存在多份 plist 文件
+
+
+
+### 2. 多 Scheme 方式
+
+要在不同的环境设置不同的 BASE_URL 在多 Scheme 方案下如何实现?
+
+1. 多 Scheme 可以选中 PROJECT,然后点击左下方的 **+**,点击「Duplicate "Debug" Configuration」,增加一列后,重命名为 Beta
+
+
+
+2. 选择 TARGETS,点击左上角的 **+** 号,在弹出面板中选择 "Add User-Defined Setting"。然后区分不同的 Configuration,设置不同的值
+
+3. 然后新添加的值要在项目中使用,需要添加到 plist 中。
+
+4. 添加后便可以按照正常使用 plist 的方式进行使用
+
+5. Xcode 选择项目进行 Manage Scheme。添加了 `LDExploreDemoBeta`、`LDExploreDemoRelease` 2个 **Scheme**。
+
+6. 分别选择不同的 Scheme,点击 Edit Scheme。**让不同的 Scheme 对应不同的 Configuration**
+
+
+
+7. 选择 Debug Scheme,Debug Scheme 的名称就是 `LDExploreDemo`,点击 Run 验证
+
+
+
+
+
+优点:不用像方式1一样,要配置某个值,需要切换不同的 TARGETS。通过多 Scheme 的方式,可以在同一个 Build Settings 中对不同的值进行配置,不会很分散。到时候只需要切换不同的 Scheme 即可。
+
+
+
+### 3. .xcconfig 方式
+
+#### 1. 基础使用
+
+ Cocoapods 管理的项目在 install 之后便可以看到2份 `.xcconfig` 文件。命名格式为:`Pods-{ProjectName}.debug.xcconfig` 和 `Pods-{ProjectName}.release.xcconfig`
+
+所以我们也可以使用 `.xcconfig` 的方式管理工程。命名格式为:`Pods-{ProjectName}.${ConfigurationName}.xcconfig`
+
+- ProjectName: 就是工程项目名
+- ConfigurationName:工程设置的 Configuration 名称。比如 Debug、Beta、Release
+
+另外 `.xcconfig` 可以给 PROJECT 设置,也可以给 TARGETS 设置。
+
+
+
+需求:设置不同环境对应不同的 BASE_URL 的值,在项目中正确读取。
+
+1. 在 PROJECT 中创建不同的 Configuration,比如:Debug、Beta、Release
+
+2. Manage Scheme。创建不同的 Scheme。比如:`${ProjectName}Debug` 、`${ProjectName}Beta` 、`${ProjectName}Release
+
+3. Edit Scheme。让不同的 Scheme 选择对应的 Build Configuration
+
+
+
+4. 创建不同的 `.xcconfig` 文件,命名格式为:`Pods-{ProjectName}.${ConfigurationName}.xcconfig`。比如:Config-LDExploreDemo.Debug.xcconfig、Config-LDExploreDemo.Beta.xcconfig、Config-LDExploreDemo.Release.xcconfig。并编辑里面的内容
+
+5. 将 xcconfig 文件中的变量,在 plist 文件中声明一下
+
+6. 业务代码中按照读取 plist 文件的逻辑,正常编写业务代码
+
+7. Xccode 选中工程的不同 Scheme 运行即可
+
+效果如下:
+
+
+
+同样 Cocoapods 管理的工程,也会根据 Configuration 生成不同的 xcconfig 文件,我们可以修改或者创新新的 xcconfig 文件,但引入使用。
+
+
+
+#### 2. xcconfig 编写规范
+
+xcconfig (Xcode Configuration Settings File) 文件是用于管理 Xcode 项目构建设置的纯文本配置文件。遵循正确的编写规范可以确保项目配置的可维护性和一致性。
+
+1. 键值对声明
+
+ ```shell
+ // 基本键值对
+ BUILD_SETTING_NAME = value
+
+ // 带引号的值(包含空格时必需)
+ OTHER_LDFLAGS = -ObjC -all_load
+ FRAMEWORK_SEARCH_PATHS = "$(inherited)" "$(PROJECT_DIR)/Frameworks"
+ ```
+
+2. 包含其他文件
+
+ ```shell
+ // 包含其他配置文件
+ #include "Base.xcconfig"
+ #include "../Configurations/SharedSettings.xcconfig"
+ ```
+
+3. 注释规范
+
+ ```shell
+ // 单行注释
+ /*
+ 多行注释
+ 用于详细说明
+ */
+
+ // 设置分组标题
+ ////////////////////////////
+ // MARK: - 路径设置
+ ////////////////////////////
+ ```
+
+4. 条件设置
+
+ SDK 条件
+
+ ```shell
+ // iOS 真机配置
+ LIBRARY_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) "$(SRCROOT)/iOS/Libs"
+
+ // iOS 模拟器配置
+ LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) "$(SRCROOT)/Simulator/Libs"
+
+ // macOS 配置
+ OTHER_CFLAGS[sdk=macosx*] = -DMAC_ENV
+ ```
+
+ 架构条件
+
+ ```shell
+ // ARM64 架构
+ GCC_PREPROCESSOR_DEFINITIONS[arch=arm64] = ARM_OPTIMIZED=1
+
+ // x86_64 架构
+ SWIFT_COMPILATION_MODE[arch=x86_64] = wholemodule
+ ```
+
+ 构建配置条件
+
+ ```shell
+ // Debug 配置
+ OTHER_SWIFT_FLAGS[config=Debug] = -D DEBUG -enable-testing
+
+ // Release 配置
+ CODE_SIGN_IDENTITY[config=Release] = "Apple Distribution"
+ ```
+
+ 组合条件(上述条件可以自由组合)
+
+ ```shell
+ // 仅对 iOS 模拟器的 Debug 配置生效
+ ENABLE_UI_TESTS[sdk=iphonesimulator*][config=Debug] = YES
+ ```
+
+ 设置条件后,如果在不符合的条件下,编译会报错
+
+
+
+5. 值引用和继承
+
+ ```shell
+ // 引用其他设置的值
+ NEW_SETTING = $(EXISTING_SETTING)/subpath
+
+ // 继承上级设置(重要!)
+ OTHER_LDFLAGS = $(inherited) -framework CryptoKit
+ ```
+
+6. 文件组织结构
+
+ ```shell
+ Project/
+ ├── Configurations/
+ │ ├── Base.xcconfig
+ │ ├── Debug.xcconfig
+ │ ├── Release.xcconfig
+ │ ├── Development.xcconfig
+ │ └── Production.xcconfig
+ ```
+
+7. 分层配置
+
+ ```shell
+ // Base.xcconfig - 通用基础设置
+ SDKROOT = iphoneos
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0
+
+ // Debug.xcconfig
+ #include "Base.xcconfig"
+ GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 COCOAPODS=1
+ SWIFT_OPTIMIZATION_LEVEL = -Onone
+
+ // Release.xcconfig
+ #include "Base.xcconfig"
+ SWIFT_OPTIMIZATION_LEVEL = -O
+ GCC_OPTIMIZATION_LEVEL = 3
+ ```
+
+8. 路径管理规范
+
+ ```shell
+ // 使用 PROJECT_DIR 作为根目录
+ PROJECT_DIR = $(SRCROOT)
+
+ // 框架搜索路径
+ FRAMEWORK_SEARCH_PATHS = $(inherited) \
+ "$(PROJECT_DIR)/ThirdParty" \
+ "$(PROJECT_DIR)/Frameworks"
+
+ // 头文件搜索路径
+ HEADER_SEARCH_PATHS = $(inherited) \
+ "$(PROJECT_DIR)/Includes" \
+ "$(PROJECT_DIR)/Generated"
+ ```
+
+9. 多平台支持
+
+ ```shell
+ // 通用设置
+ COMMON_SETTINGS = -DCOMMON_FEATURE
+
+ // iOS 特定
+ BUILD_SETTING[sdk=iphoneos*] = $(COMMON_SETTINGS) -DIOS
+ BUILD_SETTING[sdk=iphonesimulator*] = $(COMMON_SETTINGS) -DIOS_SIMULATOR
+
+ // macOS 特定
+ BUILD_SETTING[sdk=macosx*] = $(COMMON_SETTINGS) -DMACOS
+ ```
+
+10. 条件包含
+
+ ```shell
+ // 根据配置包含不同文件
+ #if $(CONFIGURATION) == Debug
+ #include "DebugOverrides.xcconfig"
+ #elif $(CONFIGURATION) == Release
+ #include "ReleaseOverrides.xcconfig"
+ #endif
+ ```
+
+11. 命令行验证
+
+ ```shell
+ # 检查语法
+ plutil -lint Config.xcconfig
+
+ # 查看最终设置
+ xcodebuild -project YourProject.xcodeproj -showBuildSettings
+ ```
+
+#### 3. 编译链接日志输出重定向
+
+
+
+终端输入 `tty` 敲回车,然后在 Xcode 脚本中,就可以将 echo 输出的结果重定向到终端 `echo "message" > /dev/ttys000`
+
+因为要编写脚本,研究 MachO 文件,但每次编译后的 MachO 还要 show in Finder,之后再切换到终端,然后执行脚本很繁琐。所以可以直接在 Xcode 的 Build Phases 中,增加脚本解决该问题。
+
+但 Build Phases 中的 Run Script 长度有限,不能写很多脚本,所以还是需要结合 Xcconfig。
+
+Xcconfig 中定义的变量在 Run Script 中是可以访问的到。
+
+Demo 验证如下:
+
+
+
+所以编写了一个脚本,进行输出重定向 [xcode_run_cmd](./../assets/xcode_run_cmd.sh)
+
+使用的时候,需要搭配 `.xcconfig` 文件,需要设置3个参数:
+
+- CMD:运行到命令
+- CMD_FLAG :运行到命令参数
+- TTY:需要打开终端,查看终端编号配置进去。
+
+
+
+
+
+#### 4. LLVM Strip 调试
+
+符号的处理都是利用 LLVM Strip 的能力,对这部分好奇的,可以在源码中对 Strip 进行调试。
+
+调试的时候要想更有意义,类似:真的在 strip 一个项目的符号,可以把某个可执行文件的路径配置到 LLVM-Strip 的启动项中
+
+步骤:Edit Scheme - Arguments - 添加2个参数:-pa 和 需要脱符号的路径
+
+
+
+关于 LLVM 工程如何编译、运行的具体步骤,可以查看 [LLVM](./1.102.md)
+
+
+
+
+
+## 二、Xcode 配置层级机制
+
+上面已经看到了 Xcconfig 文件的身影。在 Xcode 中使用 `.xcconfig` 文件设置 `OTHER_LDFLAGS = -framework "AFNetworking"` 后,该值会出现在项目的 **Build Settings** 界面中,这是由 Xcode 配置系统的设计机制决定的
+
+### 1. Xcode 配置层级机制
+
+Xcode 的 Build Settings 是一个**多层叠加系统**,优先级从高到低如下:
+
+```mermaid
+graph LR
+A[Xcode 默认值] --> B[.xcconfig 文件]
+B --> C[Project 设置]
+C --> D[Target 设置]
+D --> E[最终生效值]
+```
+
+这是因为:
+
+- `.xcconfig` 配置是比较通用的,比较基础的
+- **Project 是通用容器**,存放多个 Target 的共享配置。只能选择 Configuration 下的某个 Configuration Set。并指定对应的 `.xcconfig`
+- **Target 是具体构建目标**(如 App、Framework),可以在拥有从 Project 继承而来的配置后,进行独立配置。比如管理 Project 中的某几个文件、资源的编译、链接配置方式。
+- Xcode 默认值:优先级最低。啥都不设置才是默认值
+
+最终生效的意思是:Xcode 的 Build Settings 界面本质是一个**实时计算的合并视图**,它会展示:
+
+- 所有直接通过 GUI 设置的值
+- 从 `.xcconfig` 导入的值
+- 继承的默认值(如 `$(inherited)`)
+
+举个例子:
+
+假设在以下位置设置 `OTHER_LDFLAGS`:
+
+| 配置位置 | 设置的值 |
+| :------------- | :-------------------------- |
+| Project 层 | `-framework "CoreData"` |
+| Target 层 | `-framework "AFNetworking"` |
+| .xcconfig 文件 | `-framework "OpenSSL"` |
+
+最终生效值将是:`OTHER_LDFLAGS = -framework "AFNetworking"`
+
+
+
+### 2. 验证 Xcode 的配置层级系统
+
+1. 新创建一个 iOS 项目,不引入任何库
+
+2. 创建2个 `.xcconfig` 文件
+
+ - `Base.xcconfig`:只链接一个 UI 基础库 WantUIKit
+ - `Dev.xcconfig`:引入 `Base.xcconfig` 文件,同时在此基础上,引入:SDWebImage、AFNetworking、PrismClient 3个库
+
+
+
+3. 配置工程的 PROJECT 对应的 Configuration。让 Debug 模式,选择 `Dev.xcconfig`
+
+编译后,查看如下:
+
+
+
+所以可以看到:Xcode 的配置是遵循层级关系和继承的。
+
+当配置完 Xcconfig 和 Xcode GUI 面板上操作完后,可以用 **xcodebuild -showBuildSettings -configuration Debug** 查看最终的结果是否符合预期
+
+
+
+
+
+## 三、符号的导入与导出
+
+### 1. 从源文件到可执行文件做了什么?
+
+iOS 应用的构建过程本质上是将高级语言代码转化为设备可执行的 Mach-O 文件的过程,其中**编译阶段**和**链接阶段**是核心技术环节
+
+
+
+#### 1. 编译阶段(Compilation) - 将源代码转化为机器码的中间形态
+
+前端编译:
+
+1. 词法分析(Lexical Analysis):将源代码拆分为 token 流(关键字、标识符、运算符等)
+
+ ```mermaid
+ graph LR
+ Source["int a = 10;"] --> Lexer
+ Lexer --> Tokens[["'int'
'a'
'='
'10'
';'"]]
+ ```
+
+2. 语法分析(**Syntax Analysis**):生成抽象语法树(AST)
+
+ ````c
+ int main() {
+ return add(2, 3);
+ }
+
+ ->
+ FunctionDecl: main
+ └─ CompoundStmt
+ └─ ReturnStmt
+ └─ CallExpr: add
+ ├─ IntegerLiteral: 2
+ └─ IntegerLiteral: 3
+ ````
+
+3. 语义分析(Semantic Analysis):
+
+ - 类型检查(确保 `add` 函数存在且参数匹配)
+ - 作用域验证(变量声明周期检查)
+ - 生成带类型信息的 AST
+
+ 中间代码的生成与优化:
+
+1. LLVM IR 的生成:平台无关的中间表示(Objective-C 通过 Clang,Swift 通过 SILGen)
+
+ ```ruby
+ ; add 函数的 IR 表示
+ define i32 @add(i32 %a, i32 %b) {
+ %1 = add i32 %a, %b
+ ret i32 %1
+ }
+ ```
+
+2. 机器无关的优化。经历一系列 Pass。关键方面
+
+ - 常量传播(Constant Propagation)
+ - 死代码消除(DCE)
+ - 函数内联(Function Inlining)
+ - 循环展开(Loop Unrolling)
+
+ ```assembly
+ - 优化前:
+ %1 = mul i32 %a, 1 ; a*1
+ %2 = add i32 %1, 0 ; +0
+
+ + 优化后:
+ %result = add i32 %a, %b
+ ```
+
+3. 目标代码生成:包含机器码 + 符号表 + 重定位信息
+
+ - 汇编代码生成:将 IR 转换为目标架构的汇编代码(.s 文件)
+ - **指令选择**:映射 IR 到机器指令
+ - **寄存器分配**:管理有限寄存器资源
+ - **指令调度**:优化指令顺序
+ - **代码发射**:生成汇编文本
+ - 汇编器阶段:将汇编文本转为机器码。输入 `.s` 文件,输出 `.o` 文件
+
+
+
+
+
+
+
+
+
+
+
+
+
+#### 2. 链接阶段(Linking)- 构建完整可执行文件
+
+ 1. 符号解析与重定位
+
+
+
+ ```c++
+ // main.c
+ extern int add(int, int); // 外部符号声明
+
+ int main() {
+ return add(2, 3);
+ }
+ ```
+
+ 符号解析过程:
+
+ ```mermaid
+ graph LR
+ A[main.o] -- 寻找 add 符号 --> B[math.o]
+ B -- 返回符号地址 --> A
+ ```
+
+ 重定位操作:
+
+ ```assembly
+ - 链接前: call 0x00000000 ; 未知地址
+ + 链接后: call 0x1000F2A0 ; 解析后的add函数地址
+ ```
+
+ 2. 静态链接处理
+
+ | 文件类型 | 处理方式 | 示例 |
+ | :----------- | :----------- | :-------------------- |
+ | 目标文件(.o) | 直接合并 | main.o + utils.o |
+ | 静态库(.a) | 按需提取 | libMath.a 中的 add.o |
+ | Swift 模块 | 消除重复符号 | __TEXT.__swift5_types |
+
+ 合并过程:
+
+ ```assembly
+ 0x1000: _main (来自 main.o)
+ 0x2000: _add (来自 math.o)
+ ```
+
+ 3. 动态链接处理
+
+ ```mermaid
+ sequenceDiagram
+ participant App as 应用程序
+ participant PLT as PLT(NSLog桩代码)
+ participant GOT as GOT(NSLog指针)
+ participant Binder as dyld_stub_binder
+ participant Dyld as dyld(动态链接器)
+ participant Lib as libSystem (NSLog实现)
+
+ App->>+PLT: 首次调用 NSLog
+ PLT->>+GOT: 读取NSLog指针
+ Note right of GOT: 初始指向绑定器
+ GOT-->>-PLT: 返回dyld_stub_binder地址
+ PLT->>+Binder: 跳转到绑定器
+ Binder->>+Dyld: 请求解析NSLog
+ Dyld->>+Lib: 查找NSLog实现
+ Lib-->>-Dyld: 返回0x7FFFE4567890
+ Dyld-->>-Binder: 返回真实地址
+ Binder->>+GOT: 更新指针地址
+ Note right of GOT: 指向真实NSLog实现
+ Binder->>+Lib: 跳转到NSLog
+ Lib-->>-App: 执行NSLog功能
+
+ Note over App,Lib: 后续调用
+ App->>PLT: 再次调用NSLog
+ PLT->>GOT: 读取指针
+ GOT-->>PLT: 返回真实地址(0x7FFFE4567890)
+ PLT->>Lib: 直接跳转
+ Lib-->>App: 执行NSLog
+ ```
+
+ 当 App 编译完成的时候,Mach-O 文件中会存在一些关键结构:
+
+ - `__TEXT.__stubs` section(PLT 桩代码)
+
+ ```assembly
+ ; NSLog 的桩代码
+ _NSLog_stub:
+ jmp qword ptr [rip + _NSLog_ptr] ; 跳转到GOT中的指针
+ nop
+ ```
+
+ - `__DATA._la_symbol_ptr` section(GOT 懒符号指针)
+
+ ```assembly
+ _NSLog_ptr:
+ .quad _dyld_stub_binder ; 初始指向绑定器
+ ```
+
+ - `__DATA._nl_symbol_ptr` section(非懒加载指针)
+
+ ````assembly
+ ; 启动时立即绑定的符号
+ _malloc_ptr: .quad _malloc_real
+ ````
+
+ 首次调用 NSLog 过程
+
+ - App 调用桩代码 `NSLog(@"hello world");` 编译后调用的是 `_NSLog_stub`
+
+ - 桩代码读取 GOT 指针
+
+ ```assembly
+ jmp qword ptr [rip + _NSLog_ptr]
+ ```
+
+ 此时 `_NSLog_ptr` 指向 `dyld_stub_binder`
+
+ - 执行动态绑定器
+
+ dyld_stub_binder 工作流程:
+
+ - 通过栈帧找到调用者信息
+ - 计算目标符号在重定位表中的偏移量
+ - 调用 dyld 的 bindLazySymbol 函数
+
+ - dyld 解析符号地址
+
+ dyld 执行下面步骤:
+
+ - 遍历加载的镜像(libSystem.dylib、Foundation.framework 等)
+
+ - 在导出符号表中查找 `_NSLog`
+
+ - 计算 ASLR 偏移后的实际地址。
+
+ ```shell
+ 基地址: 0x7FFFE4500000
+ 偏移: 0x0000000000004a30
+ 实际地址: 0x7FFFE4504a30
+ ```
+
+ - 更新 GOT 并跳转
+
+ ```assembly
+ ; 更新GOT指针
+ mov [rip + _NSLog_ptr], rax ; rax=0x7FFFE4504a30
+
+ ; 跳转到真实实现
+ jmp rax
+ ```
+
+ 后续调用 NSLog 流程
+
+ ```mermaid
+ graph LR
+ A[App调用NSLog] --> B[桩代码]
+ B --> C[读取GOT指针]
+ C --> D{指针已绑定?}
+ D -->|是| E[直接跳转实现]
+ D -->|否| F[触发绑定流程]
+ ```
+
+
+
+ 4. 生成 Mach-O 可执行文件
+
+ ```shell
+ Mach-O Header
+ ┌───────────────────────┐
+ │ Load Commands │ → 描述段信息
+ ├───────────────────────┤
+ │ __TEXT (代码段) │ → 只读机器码
+ │ __text: 主程序代码 │
+ │ __stubs: PLT存根 │
+ ├───────────────────────┤
+ │ __DATA (数据段) │ → 可读写数据
+ │ __data: 全局变量 │
+ │ __la_symbol_ptr: │ → 延迟绑定指针
+ ├───────────────────────┤
+ │ Dynamic Loader Info │ → dyld 所需信息
+ └───────────────────────┘
+ ```
+
+
+
+ 编译、链接的对比
+
+ | 阶段 | 核心任务 | 关键技术 | 输出 |
+ | :------- | :------- | :------------------------ | :------------ |
+ | **编译** | 代码转换 | AST 生成 IR 优化 指令选择 | `.o` 目标文件 |
+ | **链接** | 模块集成 | 符号解析 地址绑定 重定位 | Mach-O 文件 |
+
+
-不是的。从源文件到可执行文件,需要经过编译、链接2个步骤:
-- 编译:将源代码转为了二进制机器指令。保存这些二进制机器指令的文件,叫做目标文件 `.o` 文件。每个源文件对应一个目标文件。
-- 链接:得到目标文件怎么变成可执行程序呢?链接器将这些目标文件打包成最终可执行程序。除了打包目标文件外,链接器还会打包一个非常重要的东西,就是标准库。可执行程序 = 程序员写的代码 + 使用到的标准库(动态库/静态库)。
那看上去链接器做的事情很简单,不就是个打包工具?**链接器最重要的工作就是决定符号(变量名、函数名)的定义**
@@ -32,7 +724,7 @@ int main() {
-一步步验证下:
+通过一个例子一步步验证下上面的过程:
第一步,将 main.m 编译为 main.o 文件。指令如下
@@ -45,13 +737,13 @@ clang -target x86_64-apple-macos13.1 \
第二步,已经得到了 main.o 目标文件,也就是二进制文件,利用指令 `objdump -r main.o` 查看目标文件中的内容
-
+
可以看到 main 函数中,callq 就是调用 NSLog 函数。后面的地址写为了 0,这里的0会在后面链接的过程中被修正。
第三步,另外为了能让链接器能够定位到这些需要被修正的地址,在代码块中可以看到一个重定位表。指令为 `objdump -r main.o`
-
+
NSLog 位于偏移量为19的位置,
@@ -68,70 +760,11 @@ clang -target x86_64-apple-macos13.1 \
-## 输出重定向
+### 2. 符号
-
+符号可以理解为程序中各种元素的抽象表示。它就像是一个标识符,用来代表函数、变量、类等编程元素
-终端输入 `tty` 敲回车,然后在 Xcode 脚本中,就可以将 echo 输出的结果重定向到终端 `echo "message" > /dev/ttys000`
-
-因为要编写脚本,研究 MachO 文件,但每次编译后的 MachO 还要 show in Finder,之后再切换到终端,然后执行脚本很繁琐。所以可以直接在 Xcode 的 Build Phases 中,增加脚本解决该问题。
-
-但 Build Phases 中的 Run Script 长度有限,不能写很多脚本,所以还是需要结合 Xcconfig。
-
-Xcconfig 中定义的变量在 Run Script 中是可以访问的到。
-
-Demo 验证如下:
-
-
-
-
-
-## 符号可见性
-
-按照先后顺序
-
-- 生成目标文件阶段:`-O1、O2、O3、Os、Oz`
-- 链接:死代码剥离 dead code strip
-- 编译后的产物 mach-o:strip 剥离符号
-
-
-
-## MachO 可读可写
-
-Apple 的机制,只要文件可以正确签名,Apple 就认,可以修改。
-
-
-
-## 编译阶段做了什么
-
-- 汇编
-- 将符号归类:
- - 数据,放在数据段
- - 可以获取到地址的符号,变成地址
- - 类似 NSLog 这种只有在链接的时候才可以确定一些东西,那这种暂时无法确定地址的符号,都统一暂存起来。叫做“重定位符号表”。fishhook 就是基于此来实现系统符号的 hook
-- 链接。链接器通过链接将重定位符号表和符号表合并到一张表中,目标文件(`.o` 文件)和符号表,合并到一起,
-
-如何找出需要重定位的符号?
-
-```shell
-objdump --macho --reloc MachOAndSymbol.o
-```
-
-
-
-two_levelnamespace & flat_namespace:
-
-⼆级命名空间与⼀级命名空间。链接器默认采⽤⼆级命名空间,也就是除了会记录符号名称,还会记录符号属于哪个Mach-O的,⽐如会记录下来_NSLog来⾃Foundation。
-
-
-
-## 符号
-
-- 全局符号对整个项目可见,对使用的地方可见,整个符号表都可见。
-
-- Static 只对定义所在的文件可见。
-
-符号的种类
+#### 1. 符号的种类
| Symbol Type | **说明** |
| ----------- | ------------------------------------------------------------ |
@@ -144,7 +777,7 @@ two_levelnamespace & flat_namespace:
| **-** | debugger symbol table |
| **S** | 除了上⾯所述的,存放在其他`section`的内容,例如未初始化的全局变量存放在(__DATA,__common)中 |
| **I** | indirect symbol(符号信息相同,代表同⼀符号) |
-| **U** | 动态共享库中的⼩写u表示⼀个未定义引⽤对同⼀库中另⼀个模块中私有外部符号 |
+| **u** | 动态共享库中的⼩写u表示⼀个未定义引⽤对同⼀库中另⼀个模块中私有外部符号 |
编译 `main.m` 到 `main.o`
@@ -157,35 +790,147 @@ clang -target x86_64-apple-macos13.1 \
使用 `nm -pa .o文件路径` 命令来查看符号
-
+
+
+#### 2. 符号的大分类
+
+##### 1. 全局符号(Global Symbols)
+
+定义:在目标文件中显式导出的符号,可被其他模块引用。
+
+特点:
+
+- 对其他目标文件或库**可见**
+- 链接时若存在多个同名全局符号,会引发 **`duplicate symbol` 错误**(除非使用弱符号)。
+
+```c++
+// C/C++/Objective-C 中未加 static 的函数/全局变量
+int globalVar = 10;
+void publicFunction() { ... }
+```
+
+Mach-O 标记:`N_EXT`(外部符号)。
+
+针对全局符号:
+
+- 当存在2个同名的全局符号,一个初始化了,一个未初始化,不会报错。在编译链接阶段,当找到定义之后,未定义的会被删掉。
+- 链接器默认会把未初始化的全局符号,给强制初始化掉。比如 `int global_age;` 初始化为 `int global_age = 0;`
+
+##### Common Symbol
+
+Common Symbol:**Common Symbol 是全局符号的“临时状态”**
+
+编译器将未初始化的全局变量(如 `int x;`)暂存为 Common Symbol,**链接时**再决定其最终形态:
+
+- 若全局唯一 → 转为标准全局符号(位于 `.bss` 段)
+- 若多个同名 → 合并为一个全局符号
+- 若存在强定义 → 被强定义覆盖
+
+Common Symbol(公共符号)和全局符号(Global Symbol)本质上是**同一符号在不同编译阶段的表现形式**,二者既有紧密联系又有关键区别
+
+编译期与链接期的形态转换
+
+| 阶段 | Common Symbol | 全局符号 (Global Symbol) |
+| :--------- | :----------------------------------------- | :------------------------- |
+| **编译期** | ✅ 未初始化的全局变量被标记为 Common Symbol | ❌ 此时未形成强定义 |
+| **链接期** | ❌ 被转化或合并 | ✅ 最终成为强定义的全局符号 |
+链接器设置:
+
+- -d:强制定义 Common Symbol
+- -commons:指定对待 Common Symbol 如何响应
+
+有趣的 feature:
+
+```c++
+int global_int_age = 28;
+int global_int_age;
+
+void main() {
+ print("Hello world");
+}
+```
+
+上面的代码不会编译报错。当存在2个同名的全局符号,一个初始化了,一个未初始化,不会报错。在编译链接阶段,当找到定义之后,未定义的会被删掉。
+
+##### 2. 本地符号(Local Symbols / Static Symbols)
+
+定义:仅在当前目标文件中可见,不会暴露给其他模块
+
+特点:
+
+- 链接时不会被其他目标文件引用,避免了命名冲突
+- 通常用于内部工具函数或私有状态
+- Static 修饰的符号,只对定义所在的文件可见。
+
+```c++
+static int localVar = 5; // 静态全局变量
+static void privateFunc() { ... } // 静态函数
+```
+
+Mach-O 标志:无 `N_EXT` 标志
+
+##### 3. 未定义符号(Undefined Symbol)
+
+定义:当前目标文件声明但未定义的符号,需由链接器在其他目标文件或库中解析。
+
+特点:链接时若找不到符号定义,则会报错:Undefined Symbol
+
+```c++
+extern int externalVar; // 声明外部变量
+void undefinedFunc(); // 声明未实现的函数
+```
+
+##### 4. Weak Symbol
+
+**Weak Reference Symbol**:表示此 未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将该符号设置弱链接标志
+
+**Weak definition Symbol**:表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则该弱定义将被忽略。只能将合并部分中的符号标记为弱定义
+
+
+
+当把其中一个符号通过 **\__attribute__((weak))** 改为 weak symbol 的时候,再次编译,发现没有问题。
+
+
+
+- 当用 **\__attribute__((weak, visibility("hidden")))** 把弱定义的全局符号设置为隐藏的时候,本来是全局符号的弱定义符号,就会变成局部弱定义符号
+
+- 弱引用符号。`__attribute__((weak_import))` 是 Clang/GCC 编译器的一个属性,用于在 iOS/macOS 开发中实现**弱链接(Weak Linking)**。它的核心作用是:**允许代码引用高版本 SDK 中的符号(函数/变量/类),同时在低版本系统中运行时安全地处理这些符号的缺失**,避免崩溃
+
+ 主要作用:
+
+ 1. 向后兼容性
+
+ - 当您的 App **部署目标(Deployment Target)设置为较低版本(如 iOS 12)**,但**编译时使用的高版本 SDK(如 iOS 13 SDK)** 时,您可以在代码中使用高版本新增的 API(如 iOS 13 新增的类),但必须通过弱导入确保在低版本系统上运行时不会崩溃
+ - 编译器不会阻止你使用高版本符号,但运行时如果符号不存在,其地址会被设为 `NULL`。
+
+ 2. 运行时安全检查。使用前需显式检查符号是否可用:
+
+ ```objective-c
+ if (&NewFunction != NULL) { // 检查弱导入函数指针
+ NewFunction(); // 安全调用
+ }
+ if ([NewClass class] != nil) { // 检查弱导入类
+ // 安全使用 NewClass
+ }
+ ```
+
+ 3. 避免链接错误
+
+ - 未使用 `weak_import` 时:低版本系统因找不到符号会直接崩溃
+ - 使用后:**\__attribute__((weak_import))** 系统动态加载器(dyld)会将缺失符号置为 `NULL`,由开发者处理
+
+ 实验不方便模拟动态库和 App 的情况。就看同一个 Mach-O 中,只有函数声明,没有实现的情况
+
+
+
+ 但告诉编译器该符号是弱引用符号,就可以编译链接成功
+
+
-### 符号的分类
-
-- Common Symbol:在定义时,未初始化的全局符号。
-
- 有趣的 feature:
-
- ```c++
- int global_int_age = 28;
- int global_int_age;
-
- void main() {
- print("Hello world");
- }
- ```
-
- 上面的代码不会编译报错。当存在2个同名的全局符号,一个初始化了,一个未初始化,不会报错。在编译链接阶段,当找到定义之后,未定义的会被删掉。
-
- 针对全局符号:
-
- - 当存在2个同名的全局符号,一个初始化了,一个未初始化,不会报错。在编译链接阶段,当找到定义之后,未定义的会被删掉。
- - 链接器默认会把未初始化的全局符号,给强制初始化掉。比如 `int global_age;` 初始化为 `int global_age = 0;`
-
--
@@ -193,21 +938,142 @@ clang -target x86_64-apple-macos13.1 \
-## vim 快速查找 API 功能
+### 3. 脱符号
-`man nm`
+符号在 Mach-O 中占一定的体积,所以需要脱符号。那么哪些符号不能脱?
+
+动态库的全局符号不做处理,默认就是导出符号。链接之后,这些导出符号可能就是间接符号表中的后续经由 dyld_stub_binder 去查找所需的符号。所以动态库的全局符号不能 strip 掉。
+
+看个 Demo:OC 对象默认就是全局符号。
+
+
+
+链接器也提供了能力,将导出符号变为不导出的符号:**-Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_Person**
+
+
+
+如果有一批符号需要隐藏,链接器提供了更方便的参数: **-Xlinker -unexported_symbols_list -Xlinker ${PROJECT_DIR}/LDExploreDemo/hidden_symbols.list**
+
-
+
+### 4. 符号可见性
+
+按照先后顺序
+
+- 生成目标文件阶段:`-O1、O2、O3、Os、Oz`
+- 链接:死代码剥离 dead code strip
+- 编译后的产物 mach-o:strip 剥离符号
+
+
+
+## 四、编译阶段做了什么
+
+### 1. 预处理
+
+- **输入:** 源代码文件(`.m`, `.mm`, `.c`, `.cpp`, `.swift` 等)。
+- **处理:** 由预处理器执行。
+- **操作:**
+ - **宏展开:** 替换所有 `#define` 定义的宏。
+ - **头文件包含:** 将 `#import` 或 `#include` 指令替换为对应头文件的内容(递归处理嵌套包含)。
+ - **条件编译:** 根据 `#if`, `#ifdef`, `#ifndef`, `#else`, `#elif`, `#endif` 等指令决定哪些代码块被包含或排除(常用于区分 Debug/Release 版本、不同平台、功能开关)。
+ - **特殊指令:** 处理 `#pragma` 等特殊指令。
+- **输出:** 一个“纯净”的、宏已展开、头文件已包含、条件编译已处理完毕的“翻译单元”文本文件。这个文件是后续编译步骤的输入
+
+### 2. 词法分析
+
+- **输入:** 预处理后的翻译单元。
+- **处理:** 由编译器前端执行。
+- **操作:** 将源代码字符流分解成一系列有意义的 **词素**。例如,将 `int result = a + b;` 分解成 `int` (关键字), `result` (标识符), `=` (运算符), `a` (标识符), `+` (运算符), `b` (标识符), `;` (符号)。
+- **输出:** 一个 **Token 序列**
+
+### 3. 语法分析
+
+- **输入:** Token 序列。
+- **处理:** 由编译器前端执行。
+- **操作:** 根据语言的语法规则,将 Token 序列组织成具有层次结构的 **抽象语法树**。AST 代表了代码的结构(函数、语句、表达式、操作符、操作数等),但不包含语义信息(如变量类型)。
+- **输出:** **抽象语法树**
+
+### 4. 语义分析
+
+- **输入:** AST。
+- **处理:** 由编译器前端执行。
+- **操作:**
+ - **符号表管理:** 收集标识符(变量名、函数名、类名等)及其属性(类型、作用域、存储类别等),建立符号表。
+ - **类型检查:** 验证表达式和操作的类型是否兼容(如 `int` + `float` 是允许的,`int` + `NSString*` 是不允许的)。
+ - **类型推导:** (特别是 Swift) 推断未明确声明类型的变量或表达式的类型。
+ - **常量表达式求值:** 计算编译时可确定的常量表达式(如 `const int size = 10 * 20;`)。
+ - **检查语言规则:** 如变量是否声明后再使用、函数调用参数个数和类型是否匹配、类是否实现了协议要求的方法等。
+- **输出:** 经过语义验证和修饰的 **AST**(节点上附加了类型等语义信息),以及符号表。如果发现错误(类型不匹配、未定义符号等),会在此阶段报告
+
+### 5. 生成中间代码
+
+- **输入:** 经过语义分析的 AST。
+- **处理:** 由编译器前端执行。
+- **操作:** 将 AST 转换成一种与具体硬件架构无关的、更低级的表示形式,称为 **中间代码**。LLVM 使用的中间代码是 **LLVM IR**。
+ - **LLVM IR (Intermediate Representation):** 一种类似 RISC 指令集的低级语言,具有强类型化、静态单赋值形式等特点。它是编译流程的核心枢纽。
+- **输出:** **LLVM IR 代码**(通常存储在 `.ll` 文本文件或 `.bc` 二进制文件中)。这是**优化发生的主要阶段**
+
+### 6. IR 优化
+
+- **输入:** LLVM IR。
+- **处理:** 由 **LLVM 优化器**执行。
+- **操作:** 应用一系列优化通道对 LLVM IR 进行转换,目标是在不改变程序行为的前提下,提升性能、减小代码体积。常见优化包括:
+ - **死代码消除:** 删除永远不会被执行的代码。
+ - **常量传播:** 将使用常量值的变量直接替换为常量。
+ - **循环优化:** 展开、不变代码外提、归纳变量优化等。
+ - **内联:** 将小函数调用直接替换为函数体,减少调用开销(特别对于 Swift 的泛型函数和 `@inlinable` 函数)。
+ - **公共子表达式消除:** 避免重复计算相同的表达式。
+ - **内存优化:** 如提升堆栈分配、优化访问模式。
+ - **Tail Call Optimization:** 优化尾递归调用。
+ - **特定于 Objective-C/Swift 的优化:** 如 ARC 优化(消除不必要的 `retain`/`release`)、Swift 的泛型特化等。
+- **输出:** 优化后的 **LLVM IR**。Xcode 的编译设置(如 `-O0`/`-Onone` - 无优化, `-O1` - 基础优化, `-O2`/`-O` - 常用优化, `-O3` - 激进优化, `-Os`/`-Osize` - 优化大小)控制优化的级别和类型
+
+### 7. 生成目标汇编代码
+
+- **输入:** 优化后的 LLVM IR。
+- **处理:** 由 **LLVM 后端**执行。
+- **操作:** 将平台无关的 LLVM IR 代码转换成特定目标 CPU 架构(iOS 设备主要是 **ARM64**)的 **汇编语言**。
+- **输出:** **目标架构的汇编代码文件**(`.s` 文件)。
+
+### 8. 生成 .o 目标文件
+
+- **输入:** 目标架构的汇编代码文件(`.s`)。
+- **处理:** 由 **汇编器**执行。
+- **操作:** 将人类可读的汇编语言代码转换成机器可以直接执行的 **目标文件**。目标文件包含机器指令、数据以及符号表、重定位信息等元数据(通常是 **Mach-O 格式**的 `.o` 文件)。
+- **输出:** **目标文件**(`.o` 文件)。
+
+### 9. 链接
+
+- **输入:**
+ - 编译生成的所有目标文件(`.o`)。
+ - 项目引用的静态库(`.a` 文件,本质是 `.o` 文件的集合)。
+ - iOS SDK 提供的动态库框架(如 `UIKit.framework`, `Foundation.framework`, `libSystem.dylib` 等)的导入信息(头文件声明和链接库 stub)。
+ - 链接器脚本(通常由 Xcode/LLD 管理)。
+- **处理:** 由 **链接器**执行(iOS 主要使用 `ld64`)。
+- **操作:**
+ - **符号解析:** 将所有目标文件和库中的符号引用与符号定义关联起来。确保每个被引用的函数或变量都有唯一且明确的定义。
+ - **地址和空间分配:** 给程序中的各个段(如代码段 `__TEXT`, 数据段 `__DATA`)以及符号分配最终的内存地址(虚拟地址)。此时地址是相对的或基于段的起始地址。
+ - **重定位:** 修改目标文件和库中的代码和数据引用,将符号引用替换为链接器分配的最终地址(或地址偏移量)。这是链接器最核心的工作之一。
+ - **合并段:** 将所有输入目标文件的相同段(如 `.text`, `.data`, `.rodata`, `.bss`)合并到输出文件的对应段中。
+ - **处理静态库:** 链接器从静态库中只提取那些被目标文件引用了符号所在的 `.o` 文件,避免包含未使用的代码。
+ - **处理动态库:** 记录程序依赖的动态库(如 `UIKit`)及其版本信息(`LC_LOAD_DYLIB` 加载命令),并设置符号绑定信息(延迟绑定通过 `__DATA,__la_symbol_ptr`),但不将动态库代码复制到最终可执行文件中。动态库在运行时由 `dyld` 加载。
+ - **生成入口点:** 设置程序的入口点(通常是 `start` 函数,最终调用 `main` 或 `UIApplicationMain`)。
+ - **生成 Mach-O 头部和加载命令:** 构建最终的 **Mach-O 文件**结构,包含描述文件类型、目标架构、入口点、段信息、符号表、动态库依赖等信息的头部和一系列加载命令。
+- **输出:** **可执行的 Mach-O 文件**(通常是应用程序的二进制文件,如 `YourApp`)和/或 **动态库**(`.dylib`)、**Bundle**(`.bundle`, `.framework` 内部)
+
+Tips: 后续有很多终端使用指令的场景,为了查找方便高效,分享一个技巧
+
+**man 指令**,比如 `man nm`:
+
+
进入 vim 模式了,看到左下角有 `:` 光标,如果想查看当前 nm 命令的参数,可以快速查找,输入 `/ + 具体参数`,敲回车即可跳转到要匹配到的位置,如果有多个结果,且当前自动跳转到的不是正确的位置,vim 模式下可以输入 `n` 跳转到下一个匹配到的位置(n 即 next),输入 `N` 则跳转到上一个匹配到的位置。
-
-
比如查找 `-p`,则输入 `/-p`,敲回车的效果如下
-
+
@@ -251,60 +1117,23 @@ QA:从符号角度出发,动态库还是静态库对于 App 瘦身较好(
-## Strip
+## 五、静态库
-静态库/ .o 文件 Strip 流程: 处理 `_DWARF` 段
+写在前面:
-
+`.a` 静态库,`.dylib` 动态库使用有问题,一般是:
-Strip 的过程,就是在修改 Mach-O 文件中的内容。
+- header search path
+- library search path
+- other link flags
+
+这3个的一个或者多个造成的问题,着手去排查问题即可。
+### 1. clang 指令
-
-动态库
-
-
-
-All Symbols
-
-
-
-
-
-Non-Global Symbols(非全局符号):
-
-
-
-
-
-
-
-
-
-## 断点
-
-- 终端 LLDB 模式下通过命令添加的断点是通用的,是符号断点。通过 `br write -f 文件路径` 可以将断点导出,共享给其他人
-- Xcode GUI 添加的断点是带有绝对路径的,通过 `br write -f 文件路径` 导出的断点信息在带有文件的绝对路径,不方便共享。要做的就是文件路径的替换。
-
-
-
-## 链接
-
-源代码编译成目标文件。
-
-```shell
-clang -x objective-c \
--target x86_64-apple-macos11.1 \
--fobjc-arc \
--isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
--c main.m -o main.o
-```
-
-
-
-从 `.o` 目标文件链接为可执行文件
+clang 编译、链接的参数解释如下:
```shell
clang命令参数:
@@ -322,42 +1151,21 @@ clang -x objective-c \
-```shell
-clang -target x86_64-apple-macos13.1 \
--fobjc-arc \
--isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MaxOSX.platform/Developer/SDKs/MacOSX11.sdk \
--L./AFNetworking \
--lAFNetworking \
-test.o -o test
-```
-
-
-
-## 探索「静态库就是 .o 文件的合集」
-
-`.a` 静态库,`.dylib` 动态库使用有问题,一般是:
-
-- header search path
-- library search path
-- other link flags
-
-这3个的一个或者多个造成的问题,着手去排查问题即可。
-
-
+### 2. 静态库就是 .o 文件的合集
做个实验,验证下静态库其实就是 `.o` 文件的合集。
第一步,编写 oc 代码,就一个 Person 类,写一个类方法,编译为静态库。`Person.m` 编译为 `Person.o`
-第二步,利用 clang 将 `person.o` 转换为静态库。
+
-
+第二步,将 `Person.o` 重命名为 `Person.dylib`
其实,这里就已经可以验证「静态库就是 .o 文件的合集」。
利用 `objdump --macho --private-header Person.dylib` 查看静态库依旧是 `Object File`
-
+
第三步,编写代码 `main.m` 代码,导入静态库 ``
@@ -403,17 +1211,101 @@ clang -target x86_64-apple-macos13.1 \
- 成功,则说明 静态库就是`.o` 文件的集合,单个 `main.o` 文件,修改拓展名就可以变为静态库
- 不成功,则相反
-
+
-这一部分相关的脚本和源码,可以查看
+### 3. 静态库的合并
+
+**静态库本质就是一堆 `.o` 的合集**,那么2个或者2个以上的静态库是可以合并的。
+
+1. 创建2个类:Person、Cat 用于创建2个静态库
+
+2. 利用 Clang 指令将 .m 编译为 .o 目标文件
+
+ ```shell
+ clang -x objective-c \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./CatStaticLibray \
+ -c ./CatStaticLibrary/Cat.m -o Cat.o
+ ```
+
+3. 再利用 clang 指令将 .o 目标文件,链接为静态库
+
+
+
+4. 利用 libtool 合并2个静态库
+
+ ```shell
+ libtool -static \
+ -o libPersonCat \
+ libPerson libCat
+ ```
+
+5. 用 **ar -t libPersonCat** 指令,验证合并后的静态库所包含的 .o 目标文件
+
+
+
+### 4. Auto-Link
+
+链接器有一个特性,Auto-Link,启动这个特性后,当我们 **import <模块>** 不需要我们再去往链接器配置这个链接参数。比如 `import ` 我们在代码里使用的是 *.framework 这个目标文件时,就**自动在目标文件的 `Mach-O` 中插入一个 Load Command 格式是 `LC_LINKER_OPTION`,存储这样一个链接器参数 `-framework FrameworkName`**
-## Framework
+### 5. duplicate symbol
-Mac OS/iOS 平台还可以使用 Framework,Framework 实际上是一种打包方式,将库的:二进制文件、头文件、和有关资源打包到一起,方便管理和分发。
+静态库(`.a` 文件)在链接阶段出现 **`duplicate symbol`** 错误,本质是链接器在合并多个目标文件时发现了重复的全局符号定义
+
+静态库的本质是**目标文件(`.o`)的集合**。当链接器将主工程和静态库的代码合并时,如果发现:
+
+1. 同一个符号(函数/变量)在多个地方被定义
+2. 链接器无法确定使用哪个定义
+
+就会抛出 `duplicate symbol` 错误
+
+其实上述存在前提,链接的时候需要指定为 `all_load\-Objc` 时才会存在
+
+
+
+## 六、 Strip 流程
+
+静态库/ .o 文件 Strip 流程: 处理 `_DWARF` 段
+
+
+
+Strip 的过程,就是在修改 Mach-O 文件中的内容。
+
+
+
+
+
+动态库
+
+
+
+All Symbols
+
+
+
+
+
+Non-Global Symbols(非全局符号):
+
+
+
+
+
+
+
+
+
+## 七、Framework
+
+### 1. 定义
+
+**Mac OS/iOS 平台还可以使用 Framework,Framework 实际上是一种打包方式,将库的:二进制文件、头文件、和有关资源打包到一起,方便管理和分发**。
Framework 和系统 UIKit.Framework 还是有很大区别的。
@@ -436,16 +1328,69 @@ Framework 和系统 UIKit.Framework 还是有很大区别的。
-Framework:
+### 2. Framework 2种结构
-- 动态库:Header + `.dylib` + 签名 + 资源文件
-- 静态库:Header + `.a` + 签名 + 资源文件
+- **动态库:Header + `.dylib` + 签名 + 资源文件**
+- **静态库:Header + `.a` + 签名 + 资源文件**
+### 3. 静态库转 Framework
+
+继续做个实验,验证下 Framework 的结构(上面做了静态库),所以我们可以沿用上面的成果,将静态库包装成 Framework
+
+第一步,新建一个文件夹 `Framworks`,下面创建一个 `Person.Framework` 文件夹,把之前得到的静态库 `Person` 移动到该目录下。并创建一个 `test.m` 移动过去。
+
+```objective-c
+#import
+#import "Person.h"
+
+int main () {
+ Person *person = [[Person alloc] init];
+ NSLog(@"Person is %@", person);
+}
+```
+
+第二步,模仿 Framwork 文件结构目录。因为 Framework 的结构里有 Header 信息,所以创建 `Headers` 文件夹,把 `Person.h` 文件放进去。Headers 同层目录,把 Person 静态库放进去。
+
+第三步,根据 `test.m` 和 framework 信息,编译成 `test.o`
+
+```shell
+clang -x objective-c \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I ./Frameworks/Person.framework/Headers \
+ -c test.m -o test.o
+```
+
+第四步,再根据 `test.o` 和 framework 去完成链接。链接三要素:库的头文件、库所在目录、库的名称。只不过在处理 Framework 的时候,参数不一样
+
+```shell
+clang \
+-target x86_64-apple-macos13.1 \
+-fobjc-arc \
+-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -F ./Frameworks \
+-framework Person \
+test.o -o test
+```
+
+说明:test.o 链接 Person.framework 生成 main 可执行文件
+
+- `-F./Frameworks` 在当前目录的子目录 Frameworks 查找需要的库文件。类似 Xcode 里的 **Framwork search path**
+
+- `-framework Person ` 链接的名称为 `Person.framework` 的动态库或者静态库。类似 **Other linker flags -framework**
+
+ 查找规则:先找 `Person.framework` 的动态库,找不到,再去找 `Person.framework` 的静态库,还找不到,就报错
+
+第五步:成功得到了 test 可执行文件,说明模仿 Framwork 搭建的静态库 Framwork 成功了。然后测试下可执行文件运行结果。进行 double check
+
+
-QA:通常情况下,**同一份代码,一个库制作成动态库体积会比静态库小**。为什么?
+
+### 4. QA:通常情况下,**同一份代码,一个库制作成动态库体积会比静态库小**。为什么?
静态库是一堆 `.o` 文件的集合。假设 AFNetworking 有15个 `.m` 文件,编译后产生 15个 `.o` 文件。每个 `.o` 文件的都存在下面3部分:
@@ -455,7 +1400,7 @@ QA:通常情况下,**同一份代码,一个库制作成动态库体积会
Mach header 包括一些基础信息,所以 Mach header 存在冗余(大小端序、CPU 类型等),这也就是为什么静态库比动态库体积大的原因之一。
-
+
```shell
Unix_Kernel ~/Desktop/OCExplore/OCExplore file Person.o
@@ -471,7 +1416,7 @@ Mach header
动态库在 Mach header 这里有改进,AFNetworking 动态库格式如下:
-
+
将公共的信息放到一起,公用一个 Mach header。
@@ -490,59 +1435,90 @@ Mach header
+### 5. dead strip
+dead strip 触发条件:
-继续做个实验,验证下 Framework 的结构(上面做了静态库),所以我们可以沿用上面的成果,将静态库包装成 Framework
+- 没有被入口点使用 -> 脱掉
+- 没有被导出符号使用 -> 脱掉
-第一步,新建一个文件夹 `Framworks`,下面创建一个 `Person.Framework` 文件夹,把之前得到的静态库 `Person` 移动到该目录下。并把 `main.m` 移动过去。
+由于 OC 是动态性语言,比如 Category 是在运行时创建的,所以即使开启了 dead strip 也没办法脱掉 OC 符号。
-
+链接器很方便,提供了 **-why_live** 参数,来查看为什么某个符号没有被脱掉。用法:**-Xlinker -why_live -Xlinker _global_Function **
-第二步,因为 Framework 的结构里有 Header 信息,所以创建 `Headers` 文件夹,把 `Person.h` 文件放进去。
-
-第三步,根据 `main.m` 和 framework 信息,编译成 `main.o`
-
-```shell
-clang -x objective-c \
- -target x86_64-apple-macos13.1 \
- -fobjc-arc \
- -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
- -I./Frameworks/Person.framework/Headers \
- -c main.m -o main.o
-```
-
-第四步,再根据 `main.o` 和 framework 去完成链接。链接三要素:库的头文件、库所在目录、库的名称。只不过在处理 Framework 的时候,参数不一样
-
-```shell
+``` shell
clang -target x86_64-apple-macos13.1 \
- -fobjc-arc \
- -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
- -F./Frameworks \
- -framework Person \
- main.o -o main
+-fobjc-arc \
+-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+-Xlinker -dead_strip \
+-Xlinker -all_load \
+-Xlinker -why_live -Xlinker _global_Function \
+-L ./StaticLibrary \
+-l Person \
+test.o -o test
```
-说明:main.o 链接 Person.framework 生成 main 可执行文件
+dead code strip 和 xlinker 提供的4个参数:
-- `-F./Frameworks` 在当前目录的子目录 Frameworks 查找需要的库文件
+- `-noall_load`: 完全不加载、直接去优化。`OTHER_LDFLAGS=-Xlinker -noall_load`
-- `-framework Person ` 链接的名称为 `Person.framework` 的动态库或者静态库
-
- 查找规则:先找 `Person.framework` 的动态库,找不到,再去找 `Person.framework` 的静态库,还找不到,就报错
+- `-all_load`: 完全加载、不要去优化掉。`OTHER_LDFLAGS=-Xlinker -all_load`
+- `-ObjC` : 排除ObjC的代码、其他的都优化掉。`OTHER_LDFLAGS=-Xlinker -ObjC `
+- `-force_load` : 指定哪些静态库不要优化掉;
+## 八、动态库
+### 1. tbd
-## 动态库
+tbd 全称是 text-based stub libraries,本质就是一个 YAML 描述的文本文件。
-### 直接链接动态库
+作用:用于描述动态库的链接信息,包括导出的符号、动态库的架构信息、动态库的依赖信息
+
+用于避免在真机开发过程中直接使用传统的 dylib。
+
+对于真机来说,由于动态库都是在设备上的,在 Xcode 上使用基于 tbd 格式的伪 framework,可以大大减少 Xcode 的大小
+
+一个典型的 TBD 文件(如 `UIKit.tbd`)包含以下部分:
+
+```yaml
+--- !tapi-tbd-v3
+archs: [ arm64, arm64e ]
+platform: ios
+install-name: /System/Library/Frameworks/UIKit.framework/UIKit
+current-version: 61000
+compatibility-version: 1.0
+exports:
+ - archs: [ arm64, arm64e ]
+ symbols: [ _UIApplicationMain, _UIViewSetFrame ]
+ weak-symbols: [ _UISomeOptionalAPI ]
+ objc-classes: [ UIViewController, UIView ]
+ objc-ivars: [ UIViewController.view ]
+...
+```
+
+| 字段 | 描述 |
+| :---------------------- | :----------------------------------- |
+| `archs` | 支持的架构 (arm64, armv7, x86_64 等) |
+| `platform` | 目标平台 (ios, ios-simulator, macos) |
+| `install-name` | 运行时加载路径 |
+| `current-version` | 库的当前版本 |
+| `compatibility-version` | 兼容版本 |
+| `exports` | 导出的符号信息 |
+| `symbols` | 导出的 C 函数和全局变量 |
+| `objc-classes` | 导出的 Objective-C 类 |
+| `objc-ivars` | 导出的实例变量 |
+| `objc-eh-types` | Objective-C 异常类型 |
+| `re-exports` | 重新导出的其他库 |
+
+### 2. 直接链接动态库
继续通过小实验来研究动态库的创建与使用
第一步:创建 dylib 文件夹,下面创建 `Person.h` `Person.m` 类。在 dylib 同层目录创建 main.m 文件。代码如下
-
+
第二步:对 main.m 编译成 main.o 文件,指令为
@@ -628,21 +1604,19 @@ echo "---------------- Done --------------"
结果如下:
-
+
第六步:对生成的 main 可执行文件进行调试运行,使用 lldb 指令 `lldb -file 可执行文件`,然后输入 r 进行运行:
-
-
-
+
咦,为什么我用动态库链接后还是无法使用???带着问题研究下
-### 静态库链接成动态库
+### 3. 静态库链接成动态库
因为:
@@ -713,11 +1687,11 @@ echo "---------------- Done --------------"
执行完脚本,又出现了奇怪的现象:
-
+
-发现,在利用动态库链接成可执行文件时报错了 `undefined symbols for **`,然后利用 `objdump --macho --exports-trie libPerson.dylib` 查看动态库的导出符号,居然是空。为什么?
+发现,在**利用动态库链接成可执行文件时报错了 `undefined symbols for **`,然后利用 `objdump --macho --exports-trie libPerson.dylib` 查看动态库的导出符号,居然是空**。为什么?
链接器在链接阶段,默认使用 `-noall_load` 参数。共4个参数:
@@ -739,69 +1713,65 @@ ld -dylib -arch x86_64 \
再次运行 build 脚本,然后对可执行文件执行,还是报错 😂
-
+
通过上面的实验可以得出结论:
-- 静态库是 `.o` 文件的合集
-- 动态库是 `.o` 文件链接后的产物
-- 静态库可以链接成动态库
-- 动态库是最终链接产物。动态库比静态库多走一次链接的过程
+- **静态库是 `.o` 文件的合集**
+- **动态库是 `.o` 文件链接后的产物**
+- **静态库可以链接成动态库**
+- **动态库是最终链接产物。动态库比静态库多走一次链接的过程**
-### 动态库 Library not loaded?
+### 4. 动态库 Library not loaded?
为什么动态库链接后的可执行文件运行,会报 `Library not loaded: 'libPerson.dylib'` 错误?
不得不聊聊动态库加载原理
-
+
-也就是说当 dyld 加载一个可执行文件(main) 的时候,在 Mach-O 中有一个名字叫 `LC_LOAD_DYLIB` 的 Load Command,里面保存了所使用到的动态库的路径。可执行文件的动态库就是靠 dyld 根据动态库路径进行加载的。
-
-用 MachOView 打开另一个 App 看看
-
-
+也就是说:**当 dyld 加载一个可执行文件(main) 的时候,在 Mach-O 中有一些名字叫 `LC_LOAD_DYLIB` 的 Load Command,里面保存了所使用到的动态库的路径。可执行文件所依赖的动态库,就是靠 dyld 根据动态库路径进行加载的**。
+用 MachOView 打开另一个 App **看看**
+
对于我们自己链接的可执行文件 main 进行查看,利用 `otool -l main | grep 'DYLIB' -A 5` 指令
-
+
可以发现:
-- name 好像就是动态库的路径
+- **name 好像就是动态库的路径**
- 链接的其他几个动态库的路径都没问题,就是 LibPerson.dylib 路径有问题。
如何解决?
-需要在编译链接生成动态库的时候,有个东西保存动态库路径,这个就是 Mach-O 文件中的另一个 Load Command,即 `LC_ID_DYLIB`。
+**需要在编译链接生成动态库的时候,有个东西保存动态库路径,这个就是 Mach-O 文件中的另一个 Load Command,即 `LC_ID_DYLIB`**。
#### 方式一:通过 `install_name_tool` 指令
-
+
-通过改变动态库 name 来修改动态库的路径。具体指令为 `install_name_tool -id /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/dylib/libPerson.dylib libPerson.dylib`
+通过改变动态库 name 来修改动态库的路径。具体指令为: `install_name_tool -id 动态库路径 动态库名称`。即:`install_name_tool -id /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/dylib/libPerson.dylib libPerson.dylib`
修改动态库的 name 之后,再次 `otool -l libPerson.dylib | grep 'DYLIB' -A 5` 查看路径信息
-
+
动态库有了正确的 name 后,再重新链接生成可执行文件。可执行文件可以正确运行,查看所以来的动态库路径,均正确加载。
-
+
-
-
-上面的方案有点缺点,因为路径是绝对路径,因此没办法迁移。
+方式一有明显**缺点,因为路径是绝对路径,因此没办法迁移**。
@@ -811,6 +1781,13 @@ ld -dylib -arch x86_64 \
`@rpath` 保存一个或多个路径的变量。
+关键步骤:**谁链接动态库,谁来提供 rpath**
+
+- 动态库需要 rpath 信息,用来加载访问动态库真正的实现
+- 宿主(App)提供 rpath 信息,用于提供路径信息给动态库
+
+
+
前提说明:模拟下 App 真实环境。创建一个文件夹 `Frameworks`,内部继续创建 `Person.framework` 文件夹,其内部继续创建 `Headers`
@@ -855,7 +1832,7 @@ clang -dynamiclib \
Person.o -o Person
```
-第三步,给动态库利用 `install_name_tool` 修改 id,id 指定 `@rpath` 信息
+第三步,给动态库利用 `install_name_tool` 修改 id,id 指定 `@rpath` 信息(在做这么一件事:动态库需要 rpath 信息,用来加载访问动态库真正的实现)
```shell
install_name_tool -id @rpath/Frameworks/Person.framework/Person /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/Frameworks/Person.framework/Person
@@ -889,7 +1866,7 @@ clang -target x86_64-apple-macos13.1 \
此时的可执行文件虽然链接成功了,但是可执行文件需要用到动态库的功能,直接运行会报错 `Library not loaded: '@rpath/Frameworks/Person.framework/Person'`。所以需要给可执行文件添加 `rpath` 信息
-第七步,给可执行文件 `main` 添加 `rpath` 信息
+第七步,给可执行文件 `main` 添加 `rpath` 信息(在做这么一件事:宿主(App)提供 rpath 信息,用于提供路径信息给动态库)
```shell
install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary main
@@ -903,7 +1880,7 @@ install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLib
下面是上面全部步骤的截图说明。
-
+
@@ -916,7 +1893,7 @@ install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLib
- 可执行文件中 `Load Command ` 中存在 `LC_RPATH` ,值为 `/usr/meiying/desktop/DynamicExplore/Person.framework`
- 动态库 Mach-O 中也存在 值为 `@rpath/Frameworks/Person.framework/Person`
-反思:上面的方案还是有缺点的,应为可执行文件提供的 `rpath` 还是一个绝对路径。
+反思:上面的方案还是有缺点的,因为可执行文件提供的 `rpath` 还是一个绝对路径。
@@ -926,13 +1903,13 @@ install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLib
`@loader_path`:表示被加载的`Mach-O` 所在的⽬录,每次加载时,都可能被设置为不同的路径,由上层指定
-可以将可执行文件的 rpath 修改为灵活的,而不是写死的路径,指令为
+可以将可执行文件的 **rpath 修改为灵活的,而不是写死的路径**,指令格式为:**install_name_tool -rpath oldPath newPath**
```shell
install_name_tool -rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary @executable_path main
```
-
+
这个可不是花里胡哨的烧操作,Cocoapods 也是这么干的
@@ -940,7 +1917,7 @@ install_name_tool -rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibrary
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
```
-
+
@@ -956,7 +1933,7 @@ LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_pa
这样一个场景,代码模拟下,文件目录如下
-
+
1. 在 Cat.framework 文件夹下运行 build.sh
2. 在 Person.framework 文件夹下运行 build.sh
@@ -1078,7 +2055,7 @@ echo "---------------- Done --------------"
运行报错如下:
-
+
为什么还错误了?都已经给可执行文件添加了 `@executable_path`,给2个动态库都添加了 `@rpath`,怎么办?
@@ -1120,7 +2097,7 @@ otool -l Person | grep 'ID' -A 5
在 Person.framework运行下 build.sh,然后在根目录下运行 build.sh,得到新的可执行文件,然后可以成功运行
-
+
反思:可执行文件依赖动态库 A,动态库 A 依赖动态库 B,上面的配置很繁琐:
@@ -1136,13 +2113,13 @@ otool -l Person | grep 'ID' -A 5
注:为了方便看清楚脚本执行情况和可执行文件执行结果,这次运行注释了 otool 的打印脚本。
-
+
`loader_path` 是标准解决方案。随便打开 AFNetworking 工程看看
-
+
@@ -1158,7 +2135,7 @@ sudo codesign --force --deep --sign - (应用路径)
-### 动态库如何导出所引用的动态库的符号
+### 5. 动态库如何导出所引用的动态库的符号
- 主工程 -> Person 动态库
- Person 动态库 -> Cat 动态库
@@ -1167,7 +2144,7 @@ sudo codesign --force --deep --sign - (应用路径)
正常写代码肯定可以,但是从链接器角度分析下,如何实现
-调用的本质就是符号的发现。也就是 Cat 的符号有没有导出?可执行文件 mian 使用的能力就是动态库导出后,自己导入的。
+**调用的本质就是符号的发现。也就是 Cat 的符号有没有导出?可执行文件 mian 使用的能力就是动态库导出后,自己导入的**。
因为 main 引入了 `import ` ,查看下 Person 动态库的导出符号,使用指令 `objdump --macho --exports-trie Person`
@@ -1175,19 +2152,17 @@ sudo codesign --force --deep --sign - (应用路径)
发现 Person 没有导出 Cat 的符号。那在可执行文件中调用不了 Cat 的能力了。
-
-
-
+
怎么办呢?链接器 LD 已经是很成熟的东西了,对于处理动态库依赖了动态库,且需要将被依赖动态库的符号导出,这样的需求早已满足了。具体是什么参数?终端输入 `man ld` 查看下指令
-
+
-其中,我们需要用的是 `-reexport_framework` 这个参数。指令为 ` -Xlinker -reexport_framework -Xlinker Cat`
+其中,我们需要用的是 **-reexport_framework** 这个参数。指令为 ` -Xlinker -reexport_framework -Xlinker Cat`
对此,修改 Person.framework 的 build.sh 脚本
-```
+```shell
echo "1:编译 Person.m 为 Person.o 可执行文件"
clang -target x86_64-apple-macos13.1 \
@@ -1222,11 +2197,11 @@ otool -l Person | grep 'ID' -A 5
执行脚本输出如下:
-
+
结构如下:
-
+
理论上来讲,Person.framework 把 Cat.framework 导出了,实现方式是通过给 Mach-O 的一个叫做 `LC_REEXPORT_DYLIB` 的 Load Command。也就是可执行文件,通过 Person.framework 的 `LC_REEXPORT_DYLIB` load Command 可以实现访问 Cat.framework 的符号。
@@ -1248,7 +2223,7 @@ otool -l Person | grep 'ID' -A 5
}
```
-- 修改 main.m 的 build.sh 脚本,因为 Person.framework 已经暴露了 Cat.framework 的能力。LD 链接指令需要加 Cat.framework 头文件的参数
+- 修改 main.m 的 build.sh 脚本,因为 Person.framework 已经暴露了 Cat.framework 的能力, `mian.m` 中引入了 `Cat.Framework` 的符号,LD 链接指令需要加 Cat.framework 头文件的参数
```shell
echo "---------------- start --------------"
@@ -1281,29 +2256,30 @@ otool -l Person | grep 'ID' -A 5
修改完从里到外一次性执行 build.sh,得到 main 可执行文件。一切顺利,输出如下:
-
+
+## 九、xcframework
-
-## xcframework
-
-### 诞生背景
+### 1. 诞生背景
XCFramework:是苹果官⽅推荐的、⽀持的,可以更⽅便的表示⼀个多平台和架构的分发⼆进制库的格式。专⻔在 2019 年提出的framework 的另⼀种先进格式。
需要 Xcode11 以上⽀持。是为了更好的⽀持 Mac Catalyst 机制和 ARM 芯⽚的 macOS。
-
-
-### 优势
+### 2. 优势
胖二进制:Fat Binary,通用二进制格式(Universal Binary)。通用二进制文件实际上就是将支持不同架构的二进制文件打包成一个文件,系统在加载运行时,会根据通用二进制文件中提供的架构,选择和当前系统匹配的二进制文件。
动态库是可以合并的。前提是不同的 CPU 架构。合并之后还是多个不同的动态库,只不过 mach-header 是挨在一起的,所有的库文件也是挨着的。可以理解成是“压缩”。
-lipo 指令的缺点:不能合并相同架构。对不同的库处理后,还要处理 `dSYM` 文件,如果库开启了 Bitcode,还会生成 `BCSymbolMaps`
+lipo 指令的缺点:
+
+- 不能合并相同架构
+- 需要手动处理头文件、资源文件的内容
+- 设计优秀的 SDK 还需对外提供 DSYM文件,用于后续卡顿、Crash 的堆栈还原。对不同的库处理后,还要处理 `dSYM` 文件
+- 如果库开启了 bitcode,还会生成 `BCSymbolMaps`
文件。所以使用 lipo 处理动态库的合并、拆分,都需要管理 `dSYM`、`BCSymbolMaps`、`库文件`,较为繁琐。
@@ -1311,15 +2287,15 @@ lipo 指令的缺点:不能合并相同架构。对不同的库处理后,还
和传统的 framework 相⽐:
-- 可以⽤单个`.xcframework` ⽂件提供多个平台的分发⼆进制⽂件
+- **可以⽤单个`.xcframework` ⽂件提供多个平台的分发⼆进制⽂件**
-- 与 `Fat Header` 相⽐,可以按照平台划分,可以包含相同架构的不同平台的⽂件
+- **与 `Fat Header` 相⽐,可以按照平台划分,可以包含相同架构的不同平台的⽂件**
-- 在使⽤时,不需要再通过脚本去剥离不需要的架构体系(比如默认包含3种架构,armv7、arm64、x86_64 上架前为了包大小,还会用 lipo 指令剔除不需要的架构)
+- **在使⽤时,不需要再通过脚本去剥离不需要的架构体系(比如默认包含3种架构,armv7、arm64、x86_64 上架前为了包大小,还会用 lipo 指令剔除不需要的架构)**
-### 如何制作 xcframework
+### 3. 如何制作 xcframework
第一步:先创建一个动态库 `pod lib create Person`。里面就一个 Person 类, 包含2个方法。
@@ -1351,17 +2327,17 @@ xcodebuild -workspace Person.xcworkspace \
打包成功的输出如下:
-
+
实体目录如下:
-
+
-注意:我们打败归档后生成的符号表只有 `.dSYM` 文件,没有 `BCSymbolMaps` 文件,是因为工程没有开启 BitCode,当开启 BitCode 后的,打包会生成 `BCSymbolMaps` 文件,作用也是用于 Crash 后解析堆栈用的。
+注意:我们打包归档后生成的符号表只有 `.dSYM` 文件,没有 `BCSymbolMaps` 文件,是因为工程没有开启 bitcode,当开启 bitcode 后的,打包会生成 `BCSymbolMaps` 文件,作用也是用于 Crash 后解析堆栈用的。
-因为 `.dSYM` 文件是默认生成的,但是 `Bitcode` 需要分别开启(Pod 和 Demo 工程)。开启 Bitcode 后重新打包,看看生成的产物里有没有 `BCSymbolMaps` 和相关的 `.bcsymbolmap` 文件
+因为 `.dSYM` 文件是默认生成的,但是 `bitcode` 需要分别开启(Pod 和 Demo 工程)。开启 bitcode 后重新打包,看看生成的产物里有没有 `BCSymbolMaps` 和相关的 `.bcsymbolmap` 文件
-
+
@@ -1376,7 +2352,7 @@ xcodebuild -create-xcframework \
结果如下:
-
+
可以看到打包后的 xcframework 自动处理了文件夹。但是缺点是我们提供的 framework,别人使用可能会 crash,所以为了一些使用场景需要在 xcframework 中提供 `.dSYM` 文件或者 `.bcsymbolmap` 文件。
@@ -1395,7 +2371,7 @@ xcodebuild -create-xcframework \
结果如下
-
+
可以看到 xcframework 里面不只有不同的动态库,还携带了对应的 `.dSYM` 和 `bcsymbolmap` 文件,用于堆栈、符号还原。
@@ -1409,15 +2385,15 @@ xcodebuild -create-xcframework \
- import 头文件并使用
-
+
- 编译运行,查看 products 下面的产物,因为选择的是模拟器运行,所以验证 `Person.framework` 里面的动态库文件大小,是否和 `Person.xcframework` 里面模拟器目录下 Person 动态库的大小一致
-
+
可以看到我们打包的 `Person.xcframework` 可以正常使用,除此之外,`Person.xcframework` 包含了模拟器和真机的动态库文件和对应的 `.dSYM` 和 `.bcsymbolmap` 文件,当导入到项目中的时候,Xcode 会根据当前编译的架构,自动从里面选择合适的架构文件。
-好处有3:
+好处有三:
- 不需要处理头文件
- 我们不需要关心上线前处理,重复架构(lipo 剔除)
@@ -1425,9 +2401,7 @@ xcodebuild -create-xcframework \
注意:该部分代码,在 `LDAndFramework/XCFramework` 目录下。
-
-
-## Weak Import
+### 4. 弱链接(weakly linked)
先做个小实验
@@ -1437,15 +2411,15 @@ xcodebuild -create-xcframework \
第三步:编译运行。
-
+
结论:编译正常,但是运行会报错 `Library not loaded: @rpath/Person.framework/Person`
第一种解决方案是给 xcconfig 添加 rpath 的具体路径。
-
+
-第二种解决方案是将库声明为“弱引用”。输入 `man ld` 查看具体的参数和说明:
+**第二种解决方案是将库声明为“弱链接”**。输入 `man ld` 查看具体的参数和说明:
```shell
-weak_framework name[,suffix]
@@ -1453,6 +2427,25 @@ xcodebuild -create-xcframework \
clang optimizations, if functions are not marked weak, the compiler will optimize out any checks if the function address is NULL.
```
+运行时行为:
+
+1. **dyld 加载**:
+ - 弱链接框架不作为启动依赖
+ - 不检查框架是否存在
+2. **符号解析**:
+ - 首次访问符号时尝试解析
+ - 失败则置为 `NULL`
+3. **错误处理**:
+ - 不触发崩溃
+ - 无异常抛出
+
+主要功能:
+
+1. **运行时可选依赖**:允许程序在框架不存在时仍能运行
+2. **版本兼容**:支持调用新版框架功能,同时保持旧系统兼容
+3. **避免崩溃**:框架缺失时不会导致 `EXC_BAD_ACCESS`
+4. **动态功能检测**:可安全检查框架/API 可用性
+
修改 xcconfig 文件为
```shell
@@ -1471,9 +2464,9 @@ OTHER_LDFLAGS = $(inherited) -Xlinker -weak_framework -Xlinker "Person"
我们对添加了 `_weak-framework` 这个链接器参数的可执行文件查看下,指令为 `otool -l WeakImportDemo`
-
+
-查看 Mach-O 发现,从 `LC_LOAD_DYLIB` 变为 `LC_LOAD_WEAK_DYLIB`
+查看 Mach-O 发现,**被 `-weak-framework` 声明后,cmd 从 `LC_LOAD_DYLIB` 变为 `LC_LOAD_WEAK_DYLIB`**
@@ -1483,11 +2476,11 @@ OTHER_LDFLAGS = $(inherited) -Xlinker -weak_framework -Xlinker "Person"
这样,应用程序在运行时可以优雅地处理缺少的符号,例如,某个特性可能因为缺少实现而不可用,但应用程序的其他部分仍然可以正常工作。
-当一个动态库,在运行时,不能保证
-## 静态库符号冲突原因及其解法
+
+### 5. 静态库符号冲突原因及其解法
探索下静态库符号冲突的情况下,怎么解决?
@@ -1510,17 +2503,17 @@ OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2"
第四步:尝试编译,编译成功。
-
+
-QA:为什么链接2个同名同符号的静态库,编译不报错?
+QA:**为什么链接2个同名同符号的静态库,编译不报错?**
会有二级命名空间吗?不是的。静态库本质就是一堆 `.o` 文件,所有的 `.o` 最后都会和主工程的可执行文件进行合并,所以不存在二级命名空间的问题。
-核心原因是因为,链接器针对静态库,在 deac code strip 专门为静态库,设置为 `-noall_load`,意思是:完全不加载、直接去优化
+核心原因是因为,**链接器针对静态库,在 deac code strip 专门为静态库,设置为 `-noall_load`,意思是:完全不加载、直接去优化**
-可以验证下,在链接的时候修改链接参数为 `-all_load` 就会报错
+**可以验证下,在链接的时候修改链接参数为 `-all_load` 就会报错**
-
+
@@ -1537,16 +2530,24 @@ LD 提供了 ` -load_hidden` 参数。
修改 LD 链接参数
```shell
-OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" -Xlinker -force_load -Xlinker "${SRCROOT}/AFNetworking/libAFNetworking.a" -Xlinker -load_hidden -Xlinker "${SRCROOT}/AFNetworking2/libAFNetworking2.a"
+OTHER_LDFLAGS = $(inherited)
+ -l"AFNetworking"
+ -l"AFNetworking2"
+ -Xlinker -force_load // 强制加载静态库中所有目标文件(即使未使用)
+ -Xlinker "${SRCROOT}/AFNetworking/libAFNetworking.a"
+ -Xlinker -load_hidden // 隐藏静态库符号
+ -Xlinker "${SRCROOT}/AFNetworking2/libAFNetworking2.a"
```
-
+
具体代码见: `LDAndFramework/StaticLibConflictsSolution/StaticLibConflictsDemo`
-## 动动
+## 十、动态库/静态库的搭配使用
+
+### 1. 动动
第一步,创建一个名字叫 `NetworkManager` 的 Framework,勾选测试。里面就添加一个 `NetworkDetector` 类,有1个类方法 `+sharedManager`
@@ -1559,7 +2560,7 @@ OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" -Xlinker -force_
发现编译通过,运行报错。
-
+
为什么?原因
@@ -1569,7 +2570,7 @@ OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" -Xlinker -force_
使用的 AFNetworking 动态库,所以`NetworkManager.Framework` 的 Load Command `LC_LOAD_DYLIB` 中的 name 就告诉外部,AFNetworking 将会在 `@rpath/AFNetworking.framework/AFNetworking` 下面查找。
-
+
遵循原则是谁链接库,谁就提供库所需要的 rpath 信息。所以 `NetworkManager.Framework` 提供 rpath 信息。xcconfig 文件的 `LD_RUNPATH_SEARCH_PATHS` 是 Xcode 项目中的一个设置,它在编译时告诉链接器在生成的可执行文件的运行时路径(rpath)中包含特定的目录( `install_name_tool -add_rpath` 是在二进制文件生成后对其进行修改)
@@ -1583,19 +2584,19 @@ dyld 在运行起来后,会根据 `LD_RUNPATH_SEARCH_PATHS` 提供的 rpath
方式一:low 一点,直接给测试工程也 `pod AFNetworking`。这是在探究原理,简单解决问题可以这么做。
-
+
方式二:不是找不到 AFNetworking 吗?因为 xcconfig 提供的路径找不到,那直接给 `LD_RUNPATH_SEARCH_PATHS` 配置一个可以找到的地方。然后运行成功。这只是为了研究定位问题后,简单解决问题的方案。
-
+
方式三:观察标准做法,比如一个 App 使用了 AFNetworking,这个 case Cocoapods 是怎么处理的?
-
+
是通过 shell 脚本来处理的。该脚本肯定是 Cocoapods 生成的。属于工程化范畴。核心代码如下
-
+
具体怎么做呢?编写 shell 脚本,编译 Person 动态库、AFNetworking 动态库,然后将产物复制到 Frameworks 文件夹下。
@@ -1611,13 +2612,13 @@ Tips
那怎么做呢?
-第一步:在 App(我们的实验中就是测试工程)创建 NetworkObject 类
+第一步:在 App(我们的实验中就是测试工程)创建 NetworkObject 类(OC 类默认就是全局符号,反向依赖后,可供外部使用)
第二步:在 framework 的 HEADER_SEARCH_PATHS 中增加 App 的头文件查找路径
第三步:framework 中增加实现代码,使用 App 中的符号
-
+
@@ -1630,53 +2631,61 @@ Tips
LD 链接器支持符号的处理。
-方法一:修改动态库 xcconfig 添加未定义的符号为动态查找。但风险较大,所有未定义的都不会报错误伤较大。
+方法一:**`-Xlinker -undefined -Xlinker dynamic_lookup` 修改动态库 xcconfig 添加未定义的符号为动态查找。但风险较大,所有未定义的都不会报错误伤较大**(比如随便敲的符号,也检测不出问题)
```shell
OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -undefined -Xlinker dynamic_lookup
```
-方法二:只对特定的未定义的符号采用动态查找
+```mermaid
+graph LR
+ A[链接器 ld] --> B{遇到未定义符号}
+ B -->|默认行为| C[立即报错]
+ B -->|dynamic_lookup| D[延迟到运行时]
+ D --> E[dyld 动态解析]
+ E -->|成功| F[正常执行]
+ E -->|失败| G[运行时崩溃]
+```
+
+
+
+方法二:**`-Xlinker -U -Xlinker _OBJC_CLASS_$_NetworkObject` 只对特定的未定义的符号采用动态查找**
```shell
OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -U -Xlinker _OBJC_CLASS_$_NetworkObject
```
-
+
+### 2. 动静
+模拟:App -> NetworkManager 动态库 -> AFNetworking 静态库。将上面的工程中,NetworkManager 中的 podfile `# use_frameworks!` 注释掉,就会以静态库的形式链接 AFNetworking。
-## 动静
-
-模拟:App -> NetworkManager 动态库 -> AFNetworking 静态库。将上面的工程中,NetworkManager 中的 podfile `# use_frameworks!` 注释掉,就会以静态库的形式链接 AF。
-
-动态库依赖静态库,则会把静态库中所有代码链接到动态库中。所以工程可以正常编译、链接。
+动态库依赖静态库(动态库作为宿主),则会把静态库中所有导出符号链接到动态库中。同时动态库的符号都被保留了(静态库被引用到的符号 + 动态库的符号,存放于动态库的导出符号表中)所以工程可以正常编译、链接。
如何在 App 中引入静态库 AFNetworking 中的符号?因为链接后,动态库中已经包含了静态库中的符号,所以只需要让 Xcode 编译通过即可。符号查找无需关心。
打开 NetworkManager 动态库的 Mach-O 查看下符号,可以看到动态库 NetworkManager 中已经包含了静态库 AFNetworking 的符号。
-
+
如何成功编译?告诉链接器 HEADER_SEARCH_PATH 信息即可。
-
+
思考:动态库链接静态库后,静态库中暴露的导出符号,在动态库中也是导出符号。假设动态库不想把静态库的符号暴露出来,该怎么做?
-LD 链接器提供了能力,将静态库的符号不暴露出来。
+LD 链接器提供了能力,将静态库的符号不暴露出来。指令为: **-Xlinker -hidden-l"AFNetworking"**
```shell
OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking"
```
-
+
-
-
-## 静静
+### 3. 静静
模拟:App -> NetworkManager 静态库 -> AFNetworking 静态库
@@ -1685,64 +2694,65 @@ OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking"
之后编译报错
-
+
-此时 NetworkManager 静态库中只有自己的符号,没有所依赖的 AFNetworking 中的符号。
+静态库链接的本质是:链接器只提取被直接引用的目标文件。此时 NetworkManager 静态库中只有自己的符号,没有所依赖的 AFNetworking 中的符号。
App 链接 NetworkManager 静态库,需要告诉链接器:头文件、库名称、库所在位置。
-但 NetworkManager 静态库链接 AFNetworking 静态库,没有配置信息告诉 App。所以需要额外配置。App 直接依赖的静态库,静态库所以来的静态库没有对 App 可见。
+但 NetworkManager 静态库链接 AFNetworking 静态库,没有配置信息告诉 App。所以需要额外配置。App 直接依赖的静态库,静态库所依赖的静态库没有对 App 可见。
-方法一:Build Settings -> 查找 Other Linker Flags,添加 `-lAFBetworking` 指明链接哪个库;查找 Libarary Search Path,设置库的查找路径 `${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AFNetworking`
+方法一:Build Settings -> 查找 Other Linker Flags,添加 `-lAFNetworking` 指明链接哪个库;查找 Libarary Search Path,设置库的查找路径 `${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AFNetworking`
-
+
-方法二:直接给 App install 静态库。
+方法二:直接给 App install 静态库。Cocoapods 处理这些依赖关系。
具体代码见:`LDAndFramework/StaticLibUseStaticLib`
-## 静动
+### 4. 静动
-模拟:App -> NetworkManager 静态库 -> AFNetworking 动态库。效果等价于, (App + 静态库)+ 动态库。
+模拟:App -> NetworkManager 静态库 -> AFNetworking 动态库。效果等价于:App + 动态库
- Xcode 中将 NetworkManager 项目的 Build Settings 中的 Mach-O Type 设置为 `Static Library`
- Podfile 中将 `use_frameworks!` 注释打开
编译报错,符号未定义 。
-
+
-所以症结所在:就是把动态库的代码也放到 App 里面,App 才可以使用动态库里面的符号。上面代码运行,NetworkManager 静态库调用 AFNetworking 动态库的能力,就相当于 App 直接访问 AFNetworking 动态库的符号一样。
+App 调用静态库,静态库中被 App 引用的符号最终会被链接到 App 里。所以问题演变为:App 如何使用动态库里面的符号?
-App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AF。
+上面代码运行,NetworkManager 静态库调用 AFNetworking 动态库的能力,就相当于 App 直接访问 AFNetworking 动态库的符号一样。
+
+App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AFNetworking。
- AutoLink:当在代码中 `import ` 会自动在目标文件的 Mach-O 中 `OTHER_LDFLAGS` 拼接 `-framework` 参数
- 所以只需要告诉 App framework search path 即可。参考debug.xcconfig 中的 `FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking"`,展开为 `"${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AFNetworking"`。将其设置到 Build Settings 的 framework search path 中。
-修改后编译没问题,运行报错,因为编译后的 AFNetworking 没有拷贝到 App 所需目录下.
+修改后编译没问题,运行报错 `image not found`,因为编译后的 AFNetworking 没有拷贝到 App 所需目录下。(通过配置,链接器会在 `build/Debug-iphoneos/AFNetworking` 的位置查找 AFNetworking,目前找不到)
-
+
怎么处理?
-- 参考其他使用 AFNetworking 的项目,Cocoapods 工程化模版写好了脚本,直接拷贝一份到工程根目录下,重命名为 `handle-frameworks.sh`
-- Xcode Targets 选择 “NetworkManagerTests” -> Build Phases,添加 “Run script”,内容为 `"${SRCROOT}/handle-frameworks.sh"`
+- 参考其他使用 AFNetworking 的项目,Cocoapods 工程化模版写好了脚本。
+ - 工程根目录下新建 `Scripts` 文件夹
+ - 直接拷贝一份到 `Scripts` 目录下,重命名为 `handle-frameworks.sh`
-运行成功。
-
-
+- Xcode Targets 选择 “NetworkManagerTests” -> Build Phases,添加 “Run script”,内容为 `"${SRCROOT}/Scripts/handle-frameworks.sh"`
+运行成功。同时查看测试过程的 Product 里 framework 目录下确实存在了 AFNetworking
+
具体代码见:`LDAndFramework/StaticLibUseDynamicLib`
-
-
-## 同时依赖静态库、动态库
+### 5. 同时依赖静态库、动态库
```shell
platform :ios, '14.1'
@@ -1773,10 +2783,9 @@ end
+## 十一、Module
-## module
-
-### 定义
+### 1. 定义
一个 Module 是机器代码和数据的最小单位,可以独立于其他代码单元进行链接。
@@ -1784,9 +2793,9 @@ end
但是,Module 在调用的时候会产生**开销**,比如我们在使用一个静态库的时候。(这个开销是什么接下去会讲)
+
-
-### 场景
+### 2. 场景
说人话,平时什么情况下使用的最多?导入库文件的时候,就是 module 的主战场。
@@ -1796,17 +2805,21 @@ end
- `include`:
- 假设没有开启 module 能力,当使用 `#include <*.h>` 的时候, 头文件 A、B 在 `use.c ` `another-use.c` 中,include 几次就要编译几次。
+ 假设没有开启 module 能力,当使用 `#include "*.h"` 的时候, 头文件 A、B 在 `use.c ` `another-use.c` 中,include 几次就要编译几次。
-
+
- 编译 use.c 就要编译 include 进来的 A、B。编译 another-use.c 同样要编译 A、B。效率低下。
+ 当编译 `use.c` 的时候,就要编译 include 进来的 `A.h`、`B.h`。编译 `another-use.c` 同样要再编译一次 `A.h`、`B.h`。被 include 了几次,就要编译几次,重复编译,效率低下。
+
+ - `#include` 是纯粹的文本替换指令
+ - 预处理器将头文件内容原样复制到包含位置
+ - 每个 `.c` 文件都获得头文件的完整副本
- `import`:与 `include` 相对应,`import ` 语句用于导入模块,而不是简单的文本包含。使用模块可以减少编译时间,因为编译器只需要编译模块的接口而不是整个模块的实现
-
+
-`clang -fmodules -fmodule-map-file=mo dule.modulemap -fmodules-cache-path=./moduleCache -c use.c -o use.o` :
+**`clang -fmodules -fmodule-map-file=mo dule.modulemap -fmodules-cache-path=./moduleCache -c use.c -o use.o` **:
- `-fmodules` 用于告诉 clang 启用 module 编译
- 编译后 module 缓存保存到 `-fmodules-cache-path` 后面的路径中可以看到 A、B2个头文件,编译缓存也存在2个,分别以 A、B 开头
@@ -1814,9 +2827,22 @@ end
module 是 clang 提供的能力。可以把头文件编译成二进制文件,缓存到系统目录中。这样的好处是,在使用某个 `.h` 的多个 `.m` 中,就不会因为多处引入 `.h` 而编译多次 `.h`
+当开启 module 能力后,下面3种写法都会被转换为 moudle 的写法
+
+```c
+#import
+#include "AFNetworking/AFNetworking.h"
+@import AFNetworking.AFNetworking;
+// 转换为
+@import AFNetworking.AFNetworking;
+```
-### modulemap 规范和实例
+
+### 3. modulemap 编写规范
+
+- 开启 module 能力:Xcode 默认开启了 module 能力,开启或者关闭步骤:选择项目中的 target,进入 Build Settings 页面,搜索 “Enable Modules (C and Objective-C)”,将其设置为 NO,即可关闭 Module
+- 配置 Module Map File 路径:Xcode -> Build Settings -> Module Map File 中配置 moduleMapFile 路径。
@@ -1857,7 +2883,7 @@ AFNetworking 的 [Framework/module.modulemap](https://github.com/AFNetworking/AF
```json
framework module AFNetworking { // 定义一个名为 AFNetworking 的框架模块
- umbrella header "AFNetworking.h" // 指定了框架的伞头文件,这个文件是包含了所有公共头文件的文件,方便外部嗲欧哦那个
+ umbrella header "AFNetworking.h" // 指定了框架的伞头文件,这个文件是包含了所有公共头文件的文件,方便外部调用
export * // 表示框架中的所有公共接口(类、结构体、枚举、协议等)都被导出,
module * { export * } // 意味着框架内部的所有子模块(即 AFNetworking.h 中头文件代表的子模块都会被匹配到),子模块的名称就是 .h 文件的名称,都将被导出,可以被外部所访问
}
@@ -1876,9 +2902,9 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
export *
}
- explicit module ASControlNode_Subclasses { // 显示指名,定义了一个名为 ASControlNode_Subclasses 的子模块,它包含了与 ASControlNode 相关的子类
- header "ASControlNode+Subclasses.h" // 指定了子模块的头文件,这个头文件包含了子类的定义
- export *
+ explicit module ASControlNode_Subclasses { // 如果需要显示指名,则必须使用 explicit。本行定义了一个名为 ASControlNode_Subclasses 的子模块,它包含了与 ASControlNode 相关的子类
+ header "ASControlNode+Subclasses.h" // 指定了子模块的头文件,这个头文件包含了该子模块中所有需要对外暴露的头文件的定义
+ export * // 将 ASControlNode+Subclasses.h 中的 .h 头文件,名称不变,导出出去
}
explicit module ASDisplayNode_Subclasses { // 定义了一个名为 ASDisplayNode_Subclasses 的子模块,它包含了所有与 ASDisplayNode 相关的子类
@@ -2006,11 +3032,19 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
使用:子模块可以通过大模块访问到,比如 `@AsyncDisplayKit.ASEventLog;`
+注意: umbrella 有2种用法:
+
+- umbrella header "Header.h":用于指定一个头文件作为模块的伞头文件,该头文件会包含模块中所有需要暴露的头文件。
+
+- umbrella "文件夹目录":用于将指定目录中的所有头文件都包含在模块中,这种方式适用于目录中有很多头文件需要暴露的情况。简化了模块中多个头文件的管理和导入,不需要在 `modulemap` 文件中逐个列出文件名,提高了开发的灵活性和效率。
+
+
+
更多关于 Module 的信息,可以查看 [Clang::Modules](https://clang.llvm.org/docs/Modules.html)
-### 实战:Framework 使用 modulemap
+### 4. 实战:Framework 使用 modulemap
第一步:创建动态库 `PersonFramework`,创建一个 iOS App Demo 工程。
@@ -2018,7 +3052,7 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
第三步:给主工程、动态库设置在同一个 Xcode 项目中编译调试。
-
+
@@ -2026,7 +3060,7 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
- 工程中 `PersonFramework.modulemap` 命名的 modulemap,在编译后被 Xcode 重命名为 `module.modulemap`,系统只认这个
-- `.modulemap` 文件有多种写法,就拿上面的举例,等价于3种写法:
+- `.modulemap` 文件有多种写法,就拿上面的举例,存在3种写法:
- 写法1
@@ -2077,7 +3111,7 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
-### Swift Framework modulemap
+### 5. Swift Framework modulemap
背景:探索 Swift Framework 中,没有桥接文件,Swift 如何访问 OC?如何处理 modulemap 导出文件?
@@ -2085,11 +3119,11 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
利用 modulemap 解决。
-
+
但是,上面的做法有副作用。虽然 Framework 内部 Swift 可以访问 OC 了,但是使用 Framework 的地方,也可以访问到 OCStudent 类。如何解决该问题?
-
+
@@ -2097,13 +3131,13 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
module 提供 private modle 的能力。
-
+
说明:
-- private module 文件必须命名为 `PersonSwiftFramework.private.modulemap` 的 `private.modulemap` 格式
+- **private module 文件必须命名为 `PersonSwiftFramework.private.modulemap` 的 `private.modulemap` 格式**
-- `private.modulemap` 模块名必须为 `PersonSwiftFramework_Private`
+- **`private.modulemap` 模块名必须为 `PersonSwiftFramework_Private`**
```json
/* module.modulemap */
@@ -2125,11 +3159,9 @@ module 提供 private modle 的能力。
+### 6. Swift module - Swift 静态库的合并
-
-### Swift 静态库的合并
-
-#### 概念
+#### 1. Swift module 概念
Xcode 9 之后,Swift 开始支持静态库。
@@ -2137,21 +3169,17 @@ Swift 没有头文件的概念,Swift 要用 public 修饰的类和函数怎么
Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过的 AST,也包含 SIL(Swift Intermediate Language,Swift 中间语言)
-
-
-#### 实战:Swift 静态库合并
+#### 2. 实战:Swift 静态库合并
第一步:准备2个 Swift 静态库。其中 FLSwiftWorker 2个类完全一样,FLSwiftA、FLSwiftB 同名方法,方法实现不一样。
编写脚本: `cp -Rv -- "${BUILT_PRODUCTS_DIR}/" "${SOURCE_ROOT}/../Products"`,把产物拷贝到 products 目录下
-
+
第二步,打算用 libtool 指令 `libtool -static FLSwiftLibA.framework/FLSwiftLibA FLSwiftLibB.framework/FLSwiftLibB -o libFLSwiftC.a` 进行合并,发现报错
-
-
-
+
因为存在同名符号,但是又不存在2级命名空间,所以如何处理符号问题??
@@ -2192,35 +3220,49 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过
HEADER_SEARCH_PATHS = $(inherited) "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Headers" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Headers"
```
- 1.为什么 App 工程中的 OC 类中导入头文件编译成功,但还是无法访问里面的类?
+ **为什么 App 工程中的 OC 类中导入头文件编译成功,但还是无法访问里面的类?**
-
+
同样,需要一个新的参数,告诉 LD modulemap 的信息,然后系统根据 module.modulemap 去关联查找 `FLSwiftLibA.swiftmodule` 里面的 `x86_64-apple-ios-simulator.swiftmodule` swiftmodule 文件信息。
这个时候就可以在 App oc 代码中访问静态库里 Swift 类了。
- 2.为什么 App 工程中的 Swift 类中导入头文件报错?
+
+
+ **为什么 App 工程中的 Swift 类中导入头文件报错?**
因为上面的配置是 Swift 编译后产生的 modulemap 和 swiftmodule 是配置 `OTHER_CFLAGS`,其实就是配置 c、oc 编译器也就是 clang 的关于 swift 的信息。
而 Swift 编译器是 swiftc,需要额外配置。`SWIFT_INCLUDE_PATHS = "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Modules/" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Modules/"`
-
-
- 但是配置后还是报错,因为看上去文件路径是对的,Frmaework 生成的时候,就是这么划分文件夹的。但实际编译的时候 Headers和 `module.modulemap` 、`.swiftmodule` 文件夹在同一层目录。所以我们手动编译静态库之后,需要处理静态库产物的文件目录信息。
+
+ 但是配置后还是报错,因为看上去文件路径是对的,Framework 生成的时候,就是这么划分文件夹的。但实际编译的时候 Headers和 `module.modulemap` 、`.swiftmodule` 文件夹在同一层目录。所以我们手动编译静态库之后,需要处理静态库产物的文件目录信息。
+
-
+
+
+#### 3. 为什么 Swift 静态库在链接的时候需要配置2个信息?
+
+- Header Search Path
+- Swift module 相关信息
+
+**Swift 静态库编译后确实会生成两个核心目录:`Headers` (包含 Objective-C/C 头文件) 和 `.swiftmodule` (包含 Swift 的模块接口信息)。** 要让使用方(宿主 App 或其他模块)正确链接和使用这个静态库,**必须同时处理好这两个部分的路径配置**
+
+- 配置 Header Search Path:告诉编译器(主要是 Clang 前端,处理 Objective-C/C 和 Swift 与 C 的交互)在哪里可以找到静态库暴露的 **Objective-C/C 头文件** (通常位于 `Headers/` 目录下)
+- 配置 Swift Module 信息:
+ - -fmodule-map-file: `OTHER_CFLAGS = "-fmodule-map-file={path}"`
+ - Swift Import Paths:对应 xcconfig 文件中,就是 SWIFT_INCLUDE_PATHS = {path}
-## hmap文件与 Header Maps
+## 十二、hmap文件与 Header Maps
-### 头文件导入技术发展历史
+### 1. 头文件导入技术发展历史
-#### 思考
+#### 1. 思考
引入头文件的方式:
@@ -2245,7 +3287,7 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过
-#### 早期的 import
+#### 2. 早期的 import
最早期是 include,再到后来的 import,区别是什么?
@@ -2327,7 +3369,7 @@ import 举个例子吧。在 `Person.m`
-#### PCH(PreCompiled Header)
+#### 3. PCH(PreCompiled Header)
为了优化前面提到的问题,一种折中的技术方案诞生了,它就是 `PreCompiled Header`。早期做 iOS 开发的都看过 pch 文件。
@@ -2344,7 +3386,7 @@ import 举个例子吧。在 `Person.m`
-#### clang module
+#### 4. clang module
为了解决上面的问题,Clang Module 技术诞生了。
@@ -2360,17 +3402,17 @@ import 举个例子吧。在 `Person.m`
-### 诞生背景
+### 2. 诞生背景
但存在一个问题,如果同时存在多个 `header search path` 的时候,假设一个工作项目有10000个文件,一个类使用了10个头文件,要去10000个文件中分别查找10个头文件具体位置,这个查找过程是发生在编译阶段的。无疑会增加编译耗时。
基于此,诞生了 Header Maps 技术,通过 hmap 文件,让 Xcode 在查找头文件的时候更快速。
-### hmap 结构
+### 3. hmap 结构
创建一个 iOS App,默认模版的基础上添加一个 Person 类,一个继承自 Person 的 Student 类。然后编译
-
+
@@ -2382,7 +3424,7 @@ import 举个例子吧。在 `Person.m`
`hmap` 文件不可读,必须用对应的工具解析,按照指定的格式解析。因为属于编译器的 scope,所以查看 LLVM 源码窥探下。
-
+
可以看到 hmap 结构类似 Mach-O ,顶部 HMapHeader 告诉系统,当前有几个 Bucket,下面是 Bucket 信息。然后最下方是 string 区域。
@@ -2395,7 +3437,7 @@ import 举个例子吧。在 `Person.m`
-### 编写工具分析 hmap 文件
+### 4. 编写工具分析 hmap 文件
其结构、工作原理都类似 Mach-O 文件。参考 Mach-O 文件结构和 [LLVM:HeaderMap.cpp](https://github.com/llvm/llvm-project/blob/main/clang/lib/Lex/HeaderMap.cpp) 的实现,编写一个读取 hmap 文件的代码。
@@ -2403,9 +3445,9 @@ import 举个例子吧。在 `Person.m`
运行调试的时候,把需要读取的 HMapFile 拷贝到项目根目录。然后 Edit Scheme - Run - Arguments Passed On Launch
-
+
-
+
如何做到该工具做到像系统自带的 `objdump` 一样,在终端命令行使用:
@@ -2417,15 +3459,15 @@ import 举个例子吧。在 `Person.m`
效果如下:
-
+
-### 编写工具生成 hmap 文件
+### 5. 编写工具生成 hmap 文件
-#### 为什么要生成 hmap 文件
+#### 1. 为什么要生成 hmap 文件
如果2个 `.m` 文件有相同的头文件代码,造成编译浪费。
@@ -2456,7 +3498,7 @@ Xcode 会主动生成 `.hmap` 文件,那为什么还需要研究生成 `hmap`
然后查看编译日志中的 ` HMapStaticLibApp-project-headers.hmap` 文件。利用上面制作的 `HMapDump` 工具。
-
+
分析:在 App 使用 Static Library 的情况下,假设开启了 `Use Header Map`,静态库中所有头文件类型为 `Project`(只有 Project、Private、Public 3种类型,public 就是字面意思的公开,private 则代表 In Progress, project 才是通常意义上的 Private 含义)的情况,最终生成的 `.hmap` 文件中只会包含类似 `#import "Student.h"` 的键值引用。也就是说使用的地方,只有 `#import "Student.h"` 的这种方式才会走 hmap 策略,否则还是走 `Header Search Path` 来寻找头文件路径。
@@ -2477,7 +3519,7 @@ Xcode 会主动生成 `.hmap` 文件,那为什么还需要研究生成 `hmap`
-#### 如何生成 HMap 文件
+#### 2. 如何生成 HMap 文件
LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成白盒,有迹可循,感兴趣的可以去查看源码并带着一些关键词来搜索源码进行查看,可能会发现一些平时了解不到的细节。
可以查看 [HeaderMapTest.cpp](https://github.com/llvm/llvm-project/blob/main/clang/unittests/Lex/HeaderMapTest.cpp) 和 [HeaderMapTestUtils.h](https://github.com/llvm/llvm-project/blob/main/clang/unittests/Lex/HeaderMapTestUtils.h) 编写 hmap 的生成代码。编写后的具体代码可以查看 [BlogDemos:HMapWritor](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapWritor)
@@ -2486,7 +3528,7 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成
-### hmap 助力提升 iOS 项目编译速度
+### 6. hmap 助力提升 iOS 项目编译速度
已经编写好生成 `.hmap` 文件的能力了,那如何使用生成的 `.hmap` 文件?
@@ -2503,7 +3545,7 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成
第四步,编译使用静态库的 App 工程。最后比较静态库走自定义 `.hmap` 前后的编译耗时。
-
+
可以看到静态库使用了自定义的 Header Maps 文件后,使用静态的 App 前后,编译耗时减少了1.1s,节省了57%。
@@ -2513,7 +3555,7 @@ Demo 及其演示代码见 [HMapStaticLibApp](https://github.com/FantasticLBP/Bl
-### 工程化问题
+### 7. 工程化问题
上面是理论上分析并思考了2个问题:
- 为什么需要介入自己生成 hmap 文件
@@ -2528,24 +3570,9 @@ Cocoapods 提供了很多钩子,可以自定义编写 Ruby 脚本。
-## 死代码剥离
+## 十一、dyld 及其工作流程
-死代码剥离的条件:
-
-- 没有被入口点使用则会被干掉
-- 没有被导出符号所使用,则会被干掉
-
-dead code strip 和 xlinker 提供的4个参数:
-
-- `-noall_load`: 完全不加载、直接去优化。`OTHER_LDFLAGS=-Xlinker -noall_load`
-
-- `-all_load`: 完全加载、不要去优化掉。`OTHER_LDFLAGS=-Xlinker -all_load`
-- `-ObjC` : 排除ObjC的代码、其他的都优化掉。`OTHER_LDFLAGS=-Xlinker -ObjC `
-- `-force_load` : 指定哪些静态库不要优化掉;
-
-
-
-## DYLD(dyld shared cache) 动态库共享缓存
+### 1. DYLD(dyld shared cache) 动态库共享缓存
UIKit、CoreGraphics 等。从 iOS13 开始,为了提高性能,绝大部分的系统动态库文件都被打包存放到了一个缓存文件中(dyld shared cache)中。
存放路径为:`/System/Libarey/Caches/com.apple.dyld/dyld_shared_cache_armX`
@@ -2561,11 +3588,123 @@ ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheI
某个 App 被第一次打开时候, dyld 根据动态库的依赖,循环加载动态库到动态库共享缓存中(内存),后续其他 App 第一次打开发现使用了动态库A已经在动态库共享缓存中存在,则不需要加载。这一步调用方法为 `findInSharedCacheImage`
+### 2. ASLR
+
+#### 1. 虚拟内存、物理内存、内存分页、ASLR
+
+早期:应用程序的可执行文件是存储在磁盘上的,启动应用,则需要将可执行文件加载进内存,早期计算机是没有虚拟内存的,一旦加载就会全部加载到内存中,并且进程都是按照顺序排列的,这样存在两个问题:
+
+- 当前进程只需要在合适的地址基础上,加减一些地址,就能访问到下一个进程所使用的内存,安全性很低
+
+- 当前软件越来越大,开启多个软件时会直接全部加载到内存中,导致内存不够用。而且使用软件的时候大多数情况只使用部分功能。如果软件一打开就全部加载到内存中,会造成内存的严重浪费。
+
+基于上述2个问题,诞生了虚拟内存技术。App 进程通过内存管理单元(Memory Management Unit, MMU)来管理的。MMU 使用一张特殊的表来跟踪虚拟内存地址和物理内存地址之间的映射关系。这张表通常被称为页表(Page Table)
+
+内存分页:
+
+- 页表结构:页表包含了一系列的表项,每个表项对应虚拟内存中的一个页(通常是 4KB)。每个表项包含了该虚拟页对应的物理页的信息,包括物理页的起始地址和一些状态信息。
+- 虚拟地址到物理地址的转换:当程序访问一个虚拟地址时,MMU 查看页表来找到对应的表项,然后根据表项中的信息将虚拟地址转换为物理地址。
+- 页表缓存(TLB - Translation Lookaside Buffer):为了提高地址转换的速度,MMU 通常会有一个 TLB,它缓存了最近访问的页表项。这样,对于频繁访问的地址,转换过程可以更快地完成
+- 页错误处理:如果一个虚拟地址在页表中没有对应的条目,就会发生页错误。操作系统的页错误处理程序会被调用,它负责加载缺失的页到物理内存中,并更新页表。
+- 内存分配:当应用程序请求内存时,操作系统会分配虚拟地址空间,并在页表中创建相应的条目。如果需要,操作系统还会分配物理内存页,并将其与虚拟页关联起来。
+
+当物理内存满的时候,会发生覆盖。用户使用的活跃的数据,覆盖内存中最不活跃的数据那一页。对应现实的表现就是:iPhone 上永远可以较好的打开一个 App,比如打开了 App1、App2、App3...,等打开使用了一阵子后,内存紧张,我们尝试切换打开最早的 App1,会发现 App1 重新启动了,之前用的功能 A 的页面1,已经不见了,经历一个新的启动流程。
-## dyld 及其工作流程
-### 概念
+但虚拟内存方案带来一个问题。比如黑客不断探索发现,某个重要的功能位于第3页,是不是完全可以通过固定的地址去访问??
+
+因为早期物理内存方案下,App 启动后位于什么地址是不确定的。有了虚拟内存后,App 内符号的地址都是从0到4G,都是相对地址。
+
+为了解决该问题,Apple 又推出了 ASLR 技术。核心就是为了解决虚拟内存地址固定不变的问题。就不能通过固定的地址访问内存或者数据了。
+
+#### 2. 未使用 ASLR 的问题
+
+- 函数代码存放在 `__TEXT` 段
+
+- 全局变量存放在 `__DATA` 段
+
+- 可执行文件的内存地址为 `0x0`
+
+- 可执行文件 Header 的内存地址,就是 `LC_SEGMENT(__TEXT)` 中的 VM Address
+
+ - arm64 :`0x100000000`
+
+ - 非 arm64:`0x4000`
+
+也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
+
+
+
+利用 MachOView 查看如下:
+
+
+
+
+
+
+
+- _PAGEZERO
+ - VM Address:0x0
+ - VM Size:0x100000000
+- _TEXT
+ - VM Address:0x100000000
+ - VM Size:0x4000
+- _DATA_CONST:
+ - VM Address:0x10004000
+ - VM Size:0x4000
+- _DATA
+ - VM Address:0x10008000
+ - VM Size:0x4000
+- _LINKEDIT
+ - VM Address:0x1000C000
+ - VM Size:0x8000
+
+
+
+
+
+
+
+
+
+File Offset:在 Mach-O 文件中的位置
+
+File Size:在 Mach-O 文件中的占据的大小
+
+从下面的图可以看出,`_PAGEZERO` 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。
+
+
+
+
+
+
+
+我们会发现根据 Mach-O 文件中的信息,File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全
+
+#### 3. ASLR 诞生
+
+Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
+
+
+
+
+
+- LC_SEGMENT (__TEXT) 的 VM Address `0x10005000`
+
+- ASLR 随机偏移 0x5000,也就是可执行文件的内存地址
+
+在有 ASLR 的时候:__TEXT 代码段地址 = __`PAGEZEROR 地址`
+
+在 Mach-O 文件中的地址是原始地址
+
+```shell
+代码运行起来函数真实地址 = ASLR-Offset + __PAGEZERO(arm64:0x100000000,其他:0x4000)+ 函数基于 Mach-O 的地址
+```
+
+
+
+### 3. 概念
dyld 是一个动态链接程序。是 iOS 和 macOS 系统中的**动态链接器**(Dynamic Loader)。它负责在程序运行时加载和链接动态库。简单来说,dyld 就如同一个「中间人」,将你的程序代码和它所依赖的所有动态库整合在一起,最终让你的程序能够正常运行。
@@ -2579,7 +3718,7 @@ dyld 是一个动态链接程序。是 iOS 和 macOS 系统中的**动态链接
objdump --macho --private-headers DSYMDemo
```
-
+
- **启动阶段**:应用程序启动时,iOS 系统首先解析 `Info.plist` 文件,加载相关信息,例如启动画面等,并建立沙盒环境以及进行权限检查
@@ -2640,7 +3779,7 @@ objdump --macho --private-headers DSYMDemo
-### dyld 到底做了什么
+### 4. dyld 到底做了什么
1. 执行自身初始化配置加载环境。 `LC_DYLD_INFO_ONLY`
@@ -2672,11 +3811,102 @@ objdump --macho --private-headers DSYMDemo
compatibility version 1.0.0 // 这是动态库的兼容版本号,表明应用程序至少需要这个版本的库才能正常运行
```
+ 通过下面指令便可以获取到 Load Command 为 LC_LOAD_DYLIB 的记录,也就是可以看到所需要的全部动态库
+
+ ```shell
+ objdump --macho --private-headers /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LLDBWithDSYM-bhspsnzkxcmhjjcekslnvrepukag/Build/Products/Debug-iphoneos/LLDBWithDSYM.app/LLDBWithDSYM | grep -A 7 'LC_LOAD_DYLIB'
+ ```
+
3. 搜索所有动态库,绑定需要在调用程序之前用的符号(非懒加载符号)。`LC_DYSYMTAB`
+ 使用下面指令便可以查看 LC_DYSYMTAB 相关信息
+
+ ```shell
+ objdump --macho --private-headers /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LLDBWithDSYM-bhspsnzkxcmhjjcekslnvrepukag/Build/Products/Debug-iphoneos/LLDBWithDSYM.app/LLDBWithDSYM | grep -A 30 'LC_DYSYMTAB'
+ ```
+
+ `LC_DYSYMTAB` 对于动态链接非常重要,因为:
+
+ - 定义了符号的分类(本地/外部定义/未定义)
+
+ - 定位了间接符号表(用于方法调用和动态绑定)
+
+ 如何定位间接符号表?
+
+ ```shell
+ indirectsymoff 69744 # 间接符号表在文件中的偏移地址
+ nindirectsyms 25 # 间接符号表包含25个条目
+ ```
+
+ - 间接符号表起始位置 = **文件起始位置 + 69744 字节**
+ - 每个条目大小 = **4 字节** (32 位无符号整数)
+ - 总表大小 = 25 条目 × 4 字节/条目 = **100 字节**
+ - 结束位置 = 69744 + 100 = **69844 字节**
+
+ 结构类似于:
+
+ ```shell
+ 文件布局:
+ 0x0000 ┌───────────────────┐
+ │ Mach-O 头部 │
+ ├───────────────────┤
+ │ 加载命令区域 │
+ │ (包含LC_DYSYMTAB) │
+ ├───────────────────┤
+ │ __TEXT段 │
+ ├───────────────────┤
+ │ __DATA段 │
+ ├───────────────────┤
+ 0x69744├───────────────────┤ ← 间接符号表开始位置
+ │ 条目0: 4字节 │
+ │ 条目1: 4字节 │
+ │ ... │
+ │ 条目24: 4字节 │
+ 0x69844├───────────────────┤ ← 间接符号表结束位置
+ │ 其他数据 │
+ └───────────────────┘
+ ```
+
+ - 提供了重定位信息(地址修正数据)
+
+ ```shell
+ extreloff 0 # 外部重定位表偏移=0 → 不存在
+ nextrel 0 # 外部重定位条目数=0
+ locreloff 0 # 本地重定位表偏移=0 → 不存在
+ nlocrel 0 # 本地重定位条目数=0
+ ```
+
+ - 包含模块表(动态链接遗留信息)
+
+ ```shell
+ ilocalsym 0 # 本地符号起始索引=0
+ nlocalsym 173 # 本地符号数量=173
+ iextdefsym 173 # 外部定义符号起始索引=173
+ nextdefsym 8 # 外部定义符号数量=8
+ iundefsym 181 # 未定义符号起始索引=181
+ nundefsym 22 # 未定义符号数量=22
+ ```
+
4. 在 indirect symbol table 中,将需要绑定的导入符号真实地址替换。`LC_DYSYMTAB`
- Todo:fishhook 原理
+ 工作流程:
+
+ 动态库、静态库在使用类似 NSLog 等系统动态库符号的时候,由于系统动态库是是共享缓存技术,所以编译阶段没办法确定这些系统符号的地址。源码调用 NSLog,编译器会生成 `call _NSLog`,标记为未定义符号(在 Mach-O 中体现为 `n_type` 的 `N_UNDF` 标志)。
+
+ 这时候间接符号表就登场了,存储了在符号表中的索引,符号的真实地址存储在 `_DATA` 段的 **_la_symbol_ptr**,一开始 **_la_symbol_ptr** 中的地址,指向 **dyld_stub_binder**
+
+
+
+ 链接阶段同样无法确认系统符号地址。
+
+ - 对于静态库来说,当和 App 链接的时候,由于启用 dead code strip,没有被 App 使用的符号全被 Strip 掉,使用的符号和 App 合并了,静态库的间接符号表一样合并到了 App 的间接符号表中。
+ - 对于动态库来说,动态库无法合并和裁剪,所以导出符号表和全部的符号都被保留了,但也不能和 App 合并。
+
+
+
+ 所以总的来说,编译链接后,App 启动的时候,第一次调用了 NSLog,汇编层面会发生类似调用 `call _NSLog` 去实现,然后 dyld 通过 dyld_stub_binder 去扫描加载的动态库的导出符号表信息,查询真正地址后发生 NSLog 调用,并把真实地址写入懒加载符号表(_la_symbol_prt)中。第二次及以后调用的时候,便可以从懒加载符号表中直接获取到真实地址了。
+
+ 这也是 Fishhook 可以正常 work 的基础。
5. 向程序提供 Runtime 运行时使用 dyld 的接口(存在于 libdyld .dylib 中,由 `LC_LOAD_DYLIB` 提供)
@@ -2692,21 +3922,21 @@ objdump --macho --private-headers DSYMDemo
stacksize 0 // 这个字段指定了为程序的主线程初始栈分配的大小。在这个例子中,栈大小被设置为0,这可能意味着栈的大小将使用默认值,或者在其他地方指定(例如,通过链接器的其他设置或命令行选项)
```
-
+
-### main 函数
+### 5. main 函数
实验一:
-
+3.
编写2个函数,main 函数和 test 函数,除了方法名不同,在汇编侧是一样的。
实验二:
-
+
可以看到创建的 `test.c` 文件中,只有一个 `test` 方法。没有 `main` 方法,然后用 gcc 发现链接报错。
@@ -2727,59 +3957,117 @@ iOS 侧,dyld 默认以 main 函数作为函数起点。
-### dyld 加载过程
+### 6. dyld 加载过程
-
+
- 调⽤ `fork `函数,创建⼀个进程
-
- 调⽤ `execve` 或其衍⽣函数,在该进程上加载,执⾏ `Mach-O`⽂件
-
- 将 `Mach-O` ⽂件加载到内存
-
- 开始分析 `Mach-O` 中的 `mach_header`,以确认它是有效的 `Mach-O` ⽂件
-
- 验证通过,根据 `mach_header `解析 `load commands`。根据解析结果,将程序各个部分加载到指定的地址空间,同时设置保护标记
-
- 从 `LC_LOAD_DYLINKEN`中加载 `dyld`
-
- `dyld`开始⼯作
-
- 调用 `__dyld_start()` 函数,通知 `dyld` 开始工作
-
- 调用 `dyldbootstrap::start` 函数,使 `dyld` ⾃身进⼊可运⾏状态
-
- 调用 `dyld::_main` 函数,`dyld `的入口函数
-
- 检查共享缓存中的缓存,如果找到直接返回(红线逻辑),否则继续后面的流程
-
- 共享缓存中没有找到,则继续下面流程
-
- 加载所有手动插入的动态库
-
- 链接程序需要的动态库
-
- 链接插入的库
-
- 应用插入函数
-
- 绑定符号
-
- 调用 `instantiateMainExecutable` ,为主可执行文件创建镜像
-
- 调用当前程序与动态库的初始化构造函数(`__attribute__((constructor))`)
-
- 通过 `LC_MAIN` 查找设置程序⼊⼝函数,将胶⽔地址设置成⼊⼝函数地址,否则胶⽔地址为0
-
- 提供胶水地址,返回到 `dyld::_main` 函数中继续执行
-
- 通过 `dyld::_main`→`dyldbootstrap::start`→`__dyld_start()`,dyld 配置完成,把控制权交给可执⾏⽂件的⼊⼝函数`main()`,继续后面的流程
-
-### dyld 应用
+
+QA:有没有这样一个疑问:dyld 的工作流程有一部分是:加载所有手动插入的动态库、链接程序需要的动态库、链接插入的库、应用插入的函数。为什么 dyld 的工作流程为什么和链接器的有些重合?链接动态库为什么是 dyld 在做的事情
+
+链接器(Linker)和动态链接器(Dynamic Linker)在程序构建和运行过程中的不同角色,它们的工作看似有重合,实则处于不同阶段且分工明确
+
+```mermaid
+graph LR
+A[源代码] --> B[编译阶段]
+B --> C[目标文件 .o]
+C --> D[静态链接阶段]
+D --> E[可执行文件]
+E --> F[运行时]
+F --> G[dyld]
+
+subgraph 静态链接器 ld
+D[符号解析
重定位
静态库合并
生成加载命令]
+end
+
+subgraph 动态链接器 dyld
+G[加载动态库
地址空间分配
符号绑定
运行初始化]
+end
+
+```
+
+1. **时间点不同**:
+
+ - **链接器**:在**编译时**工作(构建阶段)
+ - **dyld**:在**运行时**工作(程序启动后)
+
+2. **地址确定性**:
+
+ - 动态库加载地址在编译时**无法确定**(受 ASLR 影响)
+ - 系统库位置由共享缓存决定(每次启动可能不同)
+
+3. **工作内容本质差异**:
+
+ | 功能 | 链接器 (ld) | dyld |
+ | :----------------- | :------------------- | :----------------- |
+ | **静态库处理** | ✅ 合并到可执行文件 | ❌ 不处理 |
+ | **动态库依赖记录** | ✅ 写入 LC_LOAD_DYLIB | ❌ 只读取 |
+ | **地址分配** | ❌ 仅预留空间 | ✅ 实际分配内存地址 |
+ | **符号绑定** | ❌ 只设置占位符 | ✅ 实际解析地址 |
+ | **共享缓存** | ❌ 无法访问 | ✅ 直接映射使用 |
+
+dyld 完整流程
+
+```mermaid
+sequenceDiagram
+ participant K as 内核
+ participant D as dyld
+ participant M as 主程序
+ participant S as 共享缓存
+ participant L as 动态库
+
+ K->>D: 1. 传递控制权
+ D->>M: 2. 解析 Mach-O 头
+ D->>D: 3. 递归加载依赖库
+ loop 每个 LC_LOAD_DYLIB
+ D->>S: 4a. 检查共享缓存
+ S-->>D: 返回缓存地址
+ D->>L: 4b. 加载磁盘动态库
+ L-->>D: 返回加载地址
+ end
+ D->>D: 5. 重定位符号
+ D->>M: 6. 绑定非懒加载符号
+ D->>M: 7. 初始化程序
+ D->>M: 8. 调用 main()
+```
+
+由于 ASLR 的存在
+
+1. **静态链接器**:
+ - 专注**可执行文件结构构建**
+ - 处理**确定性的链接操作**
+ - 生成**可重定位的 Mach-O 文件**
+2. **动态链接器**:
+ - 处理**环境相关**的地址分配
+ - 管理**运行时动态行为**
+ - 实现**资源共享和安全特性**
+
+### 7. dyld 应用
窥探系统库底层实现的时候可能需要从动态库共享缓存中提取出某个 Framework,比如 UIKit。这时候要么用第三方工具,要么用 dyld 的能力。
@@ -2828,13 +4116,13 @@ int main(int argc, const char* argv[])
dyld 本身就是一种 MachO 文件,MachO 文件类型为7,是一种包含多种架构的结构,如下图:
-
+
可以加载以下类型的 Mach-O 文件:MH_EXECUTE、MH_DYLIB、MH_BUNDLE
-#### 插入动态库
+#### 1. 插入动态库
工作原理:使用 `__attribute__((constructor))` 是 GCC 和 Clang 编译器支持的一个属性,用于标记一个函数为构造函数,该函数会在程序加载动态库时自动执行。
@@ -2857,21 +4145,47 @@ static void customConstructor(int argc, const char **argv) {
第三步:Edit scheme -> Run -> Environment Variables。Name 为 `DYLD_INSERT_LIBRARIES`,value 为 InjectFunction 动态库路径,`${SRCROOT}/Inject`。
-
+
第四步:运行输出如下
-
+
具体 [Demo]()
+不只是这一种方式可以插入动态库,图上是 GUI 的方式,给 Xcode Enviornment Variables 中加入 `DYLD_INSERT_LIBRARIES` 的方式实现。还可通过 **dlopen("/path/to/your_lib.dylib", RTLD_NOW)** 实现。
-#### 函数替换
+
+#### 2. 函数替换
工作原理:
-在 iOS 中,`__DATA, __interpose` 是一个特殊的 Mach-O 段,用于实现函数插桩 (Function Interposing)。它允许你在不修改原始库代码的情况下,拦截并替换库中的某个函数
+**在 iOS 中,`__DATA,__interpose` 是一个特殊的 Mach-O 段,用于实现函数插桩 (Function Interposing)。它允许你在不修改原始库代码的情况下,拦截并替换库中的某个函数**
+
+```shell
+Mach-O 文件结构
+├── Header
+├── Load Commands
+└── Segments (每个段包含多个节)
+ ├── __TEXT (代码段)
+ │ ├── __text
+ │ └── __cstring
+ └── __DATA (数据段) <-- __interpose 所在位置
+ ├── __data
+ ├── __bss
+ └── __interpose <-- 拦截功能的核心
+```
+
+`__interpose` 通过在 `__DATA` 段中声明特殊结构,通知 dyld(动态链接器)进行函数重定向
+
+```c
+// 拦截结构体定义
+struct interpose_t {
+ void* replacement; // 替换函数地址
+ void* original; // 原始函数地址
+};
+```
**`__attribute__((used)) static struct { ... } ... __attribute__ ((section("__DATA, __interpose"))) = { ... };`** 定义一个结构体,它包含两个指向函数的指针:`replacement` 和 `replacee`。结构体使用 `__attribute__((used))` 属性标记,以避免编译器将其优化掉。同时,它被放置在 `__DATA, __interpose` 段,这个段是专门为函数插桩而设计的。
@@ -2904,7 +4218,7 @@ INTERPOSE(my_NSLog, NSLog);
第四步:运行输出。发现 NSLog 确实被 hook 做了替换。
-
+
@@ -2912,36 +4226,62 @@ INTERPOSE(my_NSLog, NSLog);
-### 调试 dyld
+### 8. 调试 dyld
-一种是替换系统的 dyld,风险大,需要感知源码。推荐使用 dyld 提供的环境变量来控制 dyld 在运行过程中输出感兴趣的信息。
+第一种是替换系统的 dyld,风险大,需要感知源码。需要准备带调试信息的 `dyld/libdyld.dylib/libclosured.dylib` ,与系统做替换,风险较大。
+
+第二种方式是**通过 lldb 调试 dyld**。dyld 提供了一些环境变量,用于控制 dyld 在运行过程中输出感兴趣的信息。
+
+lldb 保留了一个库列表,避免在按名称设置断点时出现问题,而 dyld 与 libdyld.dyllib 就在该列表上。
+
+有2种方式可以强制在 dyld 上设置断点:
+
+- **br set -n dyldbootstrap::start -s dyld**
+ - `br set`:`breakpoint set` 的简写,表示设置断点
+ - `-n dyldbootstrap::start`:`-n` 指定函数名称,`dyldbootstrap::start` 是 macOS/iOS 系统动态链接器 **dyld** 的入口函数。当程序启动时,系统会先执行此函数完成动态库加载和程序初始化
+ - `-s dyld`:`-s` 限定共享库范围,`dyld` 是 macOS/iOS 的核心动态链接器(路径通常为 `/usr/lib/dyld`),所有用户进程的启动都依赖它
+- **set target.breakoints-use-platform-avoid-list 0**:禁用 LLDB 的系统断点保护机制
+ - 默认值为1,LLDB 会遵守操作系统提供的**断点黑名单**(如 macOS 的 `dyld`、系统框架关键函数)。禁止在这些敏感位置设置断点,防止系统崩溃。
+ - **设置为 `0` (禁用),强制允许在任何地址设置断点**,包括:
+ - 操作系统内核函数
+ - 动态链接器 (`dyld`) 内部
+ - 系统框架关键路径(如 `libobjc` 的运行时函数)
+
+推荐使用 dyld 提供的环境变量来控制 dyld 在运行过程中输出感兴趣的信息。格式为:**DYLD_PRINT_APIS=1 ${MachOPath}**
+
+- `DYLD_PRINT_APIS`: 打印 dyld 内几乎所有发生的调用。格式为:**DYLD_PRINT_APIS=1 ${MachOPath}**
+
+ ```shell
+ DYLD_PRINT_APIS=1 Users/unix_kernel/Library/Developer/Xcode/DerivedData/LLDBWithDSYM-bhspsnzkxcmhjjcekslnvrepukag/Build/Products/Debug-iphoneos/LLDBWithDSYM.app/LLDBWithDSYM
+ ```
-- `DYLD_PRINT_APIS`: 打印 dyld 内几乎所有发生的调用
- `DYLD_PRINT_LIBRARIES` :打印在应用程序启动期间正在加载的所有动态库
+
- `DYLD_PRINT_WARNINGS`:打印 dyld 运行过程中的辅助信息
+
- `DYLD_PATH`:显示 dyld 搜索动态库的目录顺序
+
- `DYLD_PRINT_ENV`:显示 dyld 初始化的环境变量
+
- `DLYD_PRINT_SEGMENTS`:打印当前程序的 segment 信息
+
- `DYLD_PRINT_STATISTICS`:打印 pre-main 耗时
+
- `DYLD_PRINT_INITIALIZERS`:会在执行每个镜像(image)的初始化器(initializer)时打印出一行信息。这些初始化器包括 C++ 的静态构造函数以及使用 `__attribute__((constructor))` 标记的函数。这个环境变量对于调试和分析程序启动时的初始化顺序和行为非常有用。
怎么用?
-`环境变量=1 ./可执行文件`
-
-
-
-
+
另一种方式是利用 Xcode -> Edit Scheme,增加或修改 dyld 环境变量
-
+
-## Mach-O
+## 十二、Mach-O
Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式。
@@ -2971,7 +4311,7 @@ Mach-O 格式⽤来替代 BSD 系统的 `a.out` 格式。Mach-O ⽂件格式保
-### 常见的 Mach-O 文件类型
+### 1. 常见的 Mach-O 文件类型
- MH_OBJECT
- 目标文件(.o)
@@ -2987,7 +4327,7 @@ Mach-O 格式⽤来替代 BSD 系统的 `a.out` 格式。Mach-O ⽂件格式保
Xcode 中也可以查看 Mach-O 文件类型
-
+
@@ -2995,13 +4335,13 @@ Xcode 中也可以查看 Mach-O 文件类型
Tips:`file` 命令可以查看文件类型。
-
+
`find . -name "*.c"` 比如在当前路径查找 .c 文件
-### Universal Binary
+### 2. Universal Binary
通用二进制文件,可以同时适用于多种架构的二进制文件,包含多种不同架构的独立的二进制文件。
@@ -3026,9 +4366,9 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
-### Mach-O 结构
+### 3. Mach-O 结构
-
+
一个 Mach-O 文件包含3块
@@ -3040,17 +4380,17 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
可以用 [MachOView:](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
-
+
比如我用 otool 查看我编写的一个 SwiftUIDemo 所依赖的共享缓存库
-
+
用 MachOView 查看 DDD Mach-O 文件
-
+
-
+
可以看到在 Mach-O 文件上,`__PAGEZERO` 的 `VM Size` 有值,但是 File Size 为0,也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0(如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0)。
@@ -3058,7 +4398,7 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
-## Mach-O 文件如何查找地址
+### 4. Mach-O 文件如何查找地址
第一步,编写一个名为 `main.m` 的代码
@@ -3083,7 +4423,7 @@ int main () {
第五步,查看目标文件指令信息 `objdump --macho -d main.o`
-
+
分析下指令信息:
@@ -3096,16 +4436,14 @@ int main () {
- test1、test2 2个函数都存在,地址不一样,为什么都是 `00 00 00 00 ` ?那系统如何确定函数真实地址?需要找个地方将这些符号存起来,然后再找个时机去把真实的地址写进去。需要**重定位符号表**,告诉链接器在链接阶段需要重定位
-
+
- 使用 `objdump --macho --reloc main.o` 指令查看 main.o 的重定位符号表。
- 符号表中 `test1` 的地址就是 `0000004a` 对应的数据。`49` 后面就是 `4a`
- 符号表中 `test2` 的地址就是 `0000004f` 对应的数据。`4e` 后面就是 `4f`
- 符号表中 `globalValue` 的地址就是 `0000003f` 对应的数据,`3c 3d 3e 3f`,经过 `48 8b 05` 到 `3f`
-
-
-### 如何找到函数的真实地址
+#### 1. 如何找到函数的真实地址
以 test1 为例。
@@ -3126,15 +4464,13 @@ iOS 是小端序,从右向左看,`b2 ff ff ff` 可以看到后4位是 ff,
完整的
-
+
-
-
-### 如何找到全局变量地址
+#### 2. 如何找到全局变量地址
第一步,先用指令查看全局变量的地址值。 `objdump --macho -s main`,可以看到地址值为 `0x100008000`
-
+
@@ -3161,9 +4497,9 @@ iOS 是小端序,从右向左看,`b2 ff ff ff` 可以看到后4位是 ff,
-## .dSYM 文件
+## 十三、.dSYM 文件
-### 定义
+### 1. 定义
`.dSYM` 文件就是保存 DWARF 格式的调试信息的文件。
@@ -3173,38 +4509,42 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调
-### 探索
+### 2. 探索
-#### 如何生成 `.dSYM` 文件
+#### 1. 如何生成 `.dSYM` 文件
第一步,新建 `main.m` 文件
-第二步:Xcode 每次编译运行都会生成新的 `.DSYM` 文件。使用 clang -g 参数也可以生成,指令为 `clang -g -c main.m -o main.o`
+第二步:Xcode 每次编译运行都会生成新的 `.DSYM` 文件。使用 clang -g 参数也可以生成 DSYM 文件。指令为: **`clang -g -c main.m -o main.o`**
-第三步:查看 Mach-O 中,包含 `__DWARF` 的段,使用指令 `objdump --macho --private-headers main.o`
+第三步:查看 Mach-O 中,包含 `__DWARF` 的段,使用指令查看:**`objdump --macho --private-headers main.o`**
-
+
+
+可以看到:**编译阶段,编译器会把调试信息放到一个单独的段中,该段名为 `__DWARF`**
+
+我们知道:**链接阶段,Strip 会把调试信息从 .o 文件中剥离出来,单独放在符号表中**
+
+继续向下窥探验证
第四步:编译成可执行文件,指令为 `clang -g main.m -o main`
第五步:查看可执行文件中是否包含 `__DWARF` 段。`objdump --macho --private-headers main`
-
+
第六步:查看可执行文件的符号表。指令为:`nm -pa main`。红色区域代表调试符号。
-
+
+可以看到:链接完成后,所有的调试符号、使用的符号,都放在符号表里了。
+看到了调试符号和 DWARF 信息,那如何生成 .dSYM 文件?因为 DWARF 是一种调试格式,所以知道调试符号,只要按照 DWARF 格式组织,写入一个文件就可以了。
+- 指令 **`clang -g1 main.m -o main`**,**参数 `-g1` 用于生成 `.dSYM` 文件**。
+- 指令 **`dwarfdump main.dSYM` 用于查看 `.dSYM` 文件**
-
-看到了调试符号和 DWARF 信息,那如何生成 .dSYM 文件?
-
-- 指令 `clang -g1 main.m -o main`,参数 `-g1` 用于生成 `.dSYM` 文件。
-- 指令 `dwarfdump main.dSYM` 用于查看 `.dSYM` 文件
-
-
+
@@ -3225,228 +4565,1013 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调
-#### .dSYM 符号化
+#### 2. Xcode Build Settings 中的 `DWARF` 和 `DWARF with dSYM File` 有啥区别?
+
+##### 1. DWARF
+
+```mermaid
+graph LR
+A[源代码] --> B[编译]
+B --> C[生成Mach-O文件]
+C --> D[包含DWARF调试信息]
+D --> E[可执行文件体积膨胀]
+```
+
+调试信息位置:直接嵌入在 `.o` 目标文件和最终可执行文件中
+
+文件结构:
+
+```shell
+App (Mach-O)
+├── __TEXT (代码段)
+├── __DATA (数据段)
+└── __DWARF (调试段) // 包含所有调试信息
+```
+
+调试过程:LLDB 直接从可执行文件中读取调试信息
+
+优点:调试速度快(无需加载额外文件)
+
+缺点:增加30%~50%的 App 体积,源码信息泄漏
+
+##### 2. DWARF with dSYM File
+
+```mermaid
+graph LR
+A[源代码] --> B[编译]
+B --> C[生成Mach-O文件]
+C --> D[dsymutil工具]
+D --> E[剥离调试信息]
+E --> F[精简的可执行文件]
+D --> G[生成.dSYM文件]
+```
+
+文件结构:
+
+```shell
+App (精简Mach-O) // 无调试信息
+App.dSYM // 独立调试信息包
+ └── Contents
+ └── Resources
+ └── DWARF
+ └── App // 实际DWARF数据
+```
+
+调试过程:
+
+- 调试器从 .dSYM 加载符号信息
+- 崩溃服务使用 .dSYM 符号化堆栈
+
+| **特性** | **DWARF** | **DWARF with dSYM File** |
+| :--------------- | :----------------- | :----------------------- |
+| **调试信息位置** | 嵌入在可执行文件中 | 分离到独立的.dSYM文件 |
+| **文件大小** | 可执行文件体积大 | 可执行文件体积小 |
+| **调试速度** | 调试启动快 | 调试启动稍慢 |
+| **符号化能力** | 需要完整可执行文件 | 使用.dSYM文件即可 |
+| **适用场景** | 开发调试阶段 | 发布/测试阶段 |
+| **安全隐私** | 源码信息易暴露 | 保护源码信息 |
+| **Bitcode支持** | 不支持 | 支持 |
+| **默认配置** | Debug模式默认 | Release模式默认 |
+
+
+
+#### 3. 深入理解程序技术器
+
+在崩溃日志中 `4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25)` 的 `+55` 表示**程序计数器(PC)距离函数入口点的字节偏移**
+
+##### 1. 程序执行流程
+
+```mermaid
+graph LR
+ A[函数入口] --> B[指令1]
+ B --> C[指令2]
+ C --> D[...]
+ D --> E[崩溃点]
+ E --> F[后续指令]
+
+ style A fill:#4CAF50,stroke:#388E3C
+ style E fill:#F44336,stroke:#D32F2F
+```
+- 偏移量:0x37(十进制 55)
+- 函数入口地址:0x103b84f17 - 0x37 = 0x103B84EE0(符号 -[ViewController visitArray] 的起始地址)
+- 奔溃点地址:0x103b84f17
+
+##### 2. 为什么需要这个偏移量?
+###### 1. 精确代码定位
+- 函数内部位置:程序员写的一个函数,最后转换为汇编,内部可能存在好几十条指令。偏移量精确定位到具体指令
+- 多行代码映射:一个函数可能对应的多行源代码,偏移量确定具体行号
+
+###### 2. 优化代码分析
+- 内联函数识别:当函数被内联时,偏移量帮助定位到函数原始地址
+- 尾部调用优化:编译器优化后,返回地址可能不在函数末尾
+
+
+#### 4. 剖析 .dSYM 符号化
第一步:新建 iOS 项目,Buidl Setting 切换为 `DWARF with dSYM File ` 。设置模拟 crash(数组越界)
第二步:Mac 自带 `console` App 查看崩溃报告,因为有 `.dSYM` 文件,所以可以看到方法信息
-
-
-
+
思考:假设线上 crash 了,如何根据 crash 堆栈中没有符号化的地址,找到符号的真实地址?
```shell
+-------------------------------------
+Translated Report (Full Report Below)
+-------------------------------------
+
+Incident Identifier: 2AA5A2DD-78A3-4FCC-807D-4523F88DBD0E
+CrashReporter Key: B8015533-4AEE-8714-C435-0ABAB8898AE5
+Hardware Model: MacBookPro11,4
+Process: ExploreDSYM [23538]
+Path: /Users/USER/Library/Developer/CoreSimulator/Devices/C099153B-7736-4241-A734-2514070F8E90/data/Containers/Bundle/Application/E85716C6-245A-4B16-BA20-1A030036DE46/ExploreDSYM.app/ExploreDSYM
+Identifier: com.unix.kernel.ExploreDSYM
+Version: 1.0 (1)
+Code Type: X86-64 (Native)
+Role: Foreground
+Parent Process: launchd_sim [18357]
+Coalition: com.apple.CoreSimulator.SimDevice.C099153B-7736-4241-A734-2514070F8E90 [8102]
+Responsible Process: SimulatorTrampoline [624]
+
+Date/Time: 2025-06-19 17:15:50.0523 +0800
+Launch Time: 2025-06-19 17:15:47.7752 +0800
+OS Version: macOS 12.7.6 (21H1320)
+Release Type: User
+Report Version: 104
+
+Exception Type: EXC_CRASH (SIGABRT)
+Exception Codes: 0x0000000000000000, 0x0000000000000000
+Exception Note: EXC_CORPSE_NOTIFY
+Triggered by Thread: 0
+
Last Exception Backtrace:
0 CoreFoundation 0x7ff80042889b __exceptionPreprocess + 226
1 libobjc.A.dylib 0x7ff80004dba3 objc_exception_throw + 48
-2 CoreFoundation 0x7ff800304c83 -[__NSArray0 objectEnumerator] + 0
-3 DSYMDemo 0x1003f4f16 -[ViewController visitElement] + 54 (ViewController.m:26)
-4 DSYMDemo 0x1003f4e64 __29-[ViewController viewDidLoad]_block_invoke + 36 (ViewController.m:20)
-5 libdispatch.dylib 0x7ff80013ca3a _dispatch_client_callout + 8
-6 libdispatch.dylib 0x7ff80013fe87 _dispatch_continuation_pop + 715
-7 libdispatch.dylib 0x7ff80015534a _dispatch_source_invoke + 2046
-8 libdispatch.dylib 0x7ff80014c1e9 _dispatch_main_queue_drain + 1015
-9 libdispatch.dylib 0x7ff80014bde4 _dispatch_main_queue_callback_4CF + 31
-10 CoreFoundation 0x7ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
-11 CoreFoundation 0x7ff800382436 __CFRunLoopRun + 2482
-12 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
-13 GraphicsServices 0x7ff809cb128a GSEventRunModal + 139
-14 UIKitCore 0x107850ad3 -[UIApplication _run] + 994
-15 UIKitCore 0x1078559ef UIApplicationMain + 123
-16 DSYMDemo 0x1003f51be main + 110 (main.m:17)
-17 dyld_sim 0x1006312bf start_sim + 10
-18 dyld 0x10deea52e start + 462
-// ...
+2 CoreFoundation 0x7ff8004b0019 -[__NSCFString characterAtIndex:].cold.1 + 0
+3 CoreFoundation 0x7ff80035282d +[NSConstantArray automaticallyNotifiesObserversForKey:] + 0
+4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25)
+5 ExploreDSYM 0x103b84e64 __29-[ViewController viewDidLoad]_block_invoke + 36 (ViewController.m:20)
+6 libdispatch.dylib 0x7ff80013ca3a _dispatch_client_callout + 8
+7 libdispatch.dylib 0x7ff80013fe87 _dispatch_continuation_pop + 715
+8 libdispatch.dylib 0x7ff80015534a _dispatch_source_invoke + 2046
+9 libdispatch.dylib 0x7ff80014c1e9 _dispatch_main_queue_drain + 1015
+10 libdispatch.dylib 0x7ff80014bde4 _dispatch_main_queue_callback_4CF + 31
+11 CoreFoundation 0x7ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
+12 CoreFoundation 0x7ff800382436 __CFRunLoopRun + 2482
+13 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
+14 GraphicsServices 0x7ff809cb128a GSEventRunModal + 139
+15 UIKitCore 0x10c80aad3 -[UIApplication _run] + 994
+16 UIKitCore 0x10c80f9ef UIApplicationMain + 123
+17 ExploreDSYM 0x103b851be main + 110 (main.m:17)
+18 dyld_sim 0x103dc12bf start_sim + 10
+19 dyld 0x1055ce52e start + 462
+
+Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
+0 libsystem_kernel.dylib 0x7ff83611cfce __pthread_kill + 10
+1 libsystem_pthread.dylib 0x7ff8361741ff pthread_kill + 263
+2 libsystem_c.dylib 0x7ff800132fe0 abort + 130
+3 libc++abi.dylib 0x7ff800258742 abort_message + 241
+4 libc++abi.dylib 0x7ff80024995d demangling_terminate_handler() + 266
+5 libobjc.A.dylib 0x7ff800032082 _objc_terminate() + 96
+6 libc++abi.dylib 0x7ff800257b65 std::__terminate(void (*)()) + 8
+7 libc++abi.dylib 0x7ff800257b16 std::terminate() + 54
+8 libdispatch.dylib 0x7ff80013ca4e _dispatch_client_callout + 28
+9 libdispatch.dylib 0x7ff80013fe87 _dispatch_continuation_pop + 715
+10 libdispatch.dylib 0x7ff80015534a _dispatch_source_invoke + 2046
+11 libdispatch.dylib 0x7ff80014c1e9 _dispatch_main_queue_drain + 1015
+12 libdispatch.dylib 0x7ff80014bde4 _dispatch_main_queue_callback_4CF + 31
+13 CoreFoundation 0x7ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
+14 CoreFoundation 0x7ff800382436 __CFRunLoopRun + 2482
+15 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
+16 GraphicsServices 0x7ff809cb128a GSEventRunModal + 139
+17 UIKitCore 0x10c80aad3 -[UIApplication _run] + 994
+18 UIKitCore 0x10c80f9ef UIApplicationMain + 123
+19 ExploreDSYM 0x103b851be main + 110 (main.m:17)
+20 dyld_sim 0x103dc12bf start_sim + 10
+21 dyld 0x1055ce52e start + 462
+
+Thread 1:
+0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
+
+Thread 2:
+0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
+
+Thread 3:
+0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
+
+Thread 4:: com.apple.uikit.eventfetch-thread
+0 libsystem_kernel.dylib 0x7ff83611693a mach_msg_trap + 10
+1 libsystem_kernel.dylib 0x7ff836116ca8 mach_msg + 56
+2 CoreFoundation 0x7ff80038788e __CFRunLoopServiceMachPort + 145
+3 CoreFoundation 0x7ff800381fdf __CFRunLoopRun + 1371
+4 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
+5 Foundation 0x7ff800c568b4 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 213
+6 Foundation 0x7ff800c56b2d -[NSRunLoop(NSRunLoop) runUntilDate:] + 72
+7 UIKitCore 0x10c8e0286 -[UIEventFetcher threadMain] + 535
+8 Foundation 0x7ff800c8011b __NSThread__start__ + 1009
+9 libsystem_pthread.dylib 0x7ff8361744e1 _pthread_start + 125
+10 libsystem_pthread.dylib 0x7ff83616ff6b thread_start + 15
+
+Thread 5:
+0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
+
+Thread 6:
+0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
+
+Thread 7:
+0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
+
+
+Thread 0 crashed with X86 Thread State (64-bit):
+ rax: 0x0000000000000000 rbx: 0x0000000105649600 rcx: 0x00007ff7bc379b98 rdx: 0x0000000000000000
+ rdi: 0x0000000000000103 rsi: 0x0000000000000006 rbp: 0x00007ff7bc379bc0 rsp: 0x00007ff7bc379b98
+ r8: 0x00007ff7bc379a60 r9: 0x00007ff7bc379cc0 r10: 0x0000000000000000 r11: 0x0000000000000246
+ r12: 0x0000000000000103 r13: 0x0000003000000008 r14: 0x0000000000000006 r15: 0x0000000000000016
+ rip: 0x00007ff83611cfce rfl: 0x0000000000000246 cr2: 0x0000000000000000
+
+Logical CPU: 0
+Error Code: 0x02000148
+Trap Number: 133
+
Binary Images:
- 0x7ff836115000 - 0x7ff83614cfff libsystem_kernel.dylib (*) /usr/lib/system/libsystem_kernel.dylib
- 0x7ff83616e000 - 0x7ff836179ff7 libsystem_pthread.dylib (*) /usr/lib/system/libsystem_pthread.dylib
+ 0x7ff836115000 - 0x7ff83614cfff libsystem_kernel.dylib (*) <2fe67e94-4a5e-3506-9e02-502f7270f7ef> /usr/lib/system/libsystem_kernel.dylib
+ 0x7ff83616e000 - 0x7ff836179ff7 libsystem_pthread.dylib (*) <5a5f7316-85b7-315e-baf3-76211ee65604> /usr/lib/system/libsystem_pthread.dylib
0x7ff8000b5000 - 0x7ff800139ff7 libsystem_c.dylib (*) <8a60f5c1-ea1f-352b-b778-967be44e3677> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libsystem_c.dylib
0x7ff800248000 - 0x7ff80025dffb libc++abi.dylib (*) /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libc++abi.dylib
0x7ff80002c000 - 0x7ff80005ffe9 libobjc.A.dylib (*) <2a7a213a-fdb2-311c-81d7-efdfd9ddf25a> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libobjc.A.dylib
0x7ff80013a000 - 0x7ff800185ff3 libdispatch.dylib (*) <59be51c1-e9f3-3a60-8108-cd70ae082897> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libdispatch.dylib
0x7ff800303000 - 0x7ff80068bffc com.apple.CoreFoundation (6.9) <2be0f79f-8b25-3614-9e7e-dbac565f72dd> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
0x7ff809cae000 - 0x7ff809cb5ff2 com.apple.GraphicsServices (1.0) <16365e42-1d5c-363d-84d1-3bb290a43253> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/GraphicsServices.framework/GraphicsServices
- 0x106a0f000 - 0x1084dafff com.apple.UIKitCore (1.0) /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
- 0x1003f3000 - 0x1003f6fff com.unix.kernel.DSYMDemo (1.0) /Users/USER/Library/Developer/CoreSimulator/Devices/5E0D0385-812D-4FE6-83F6-FFE74E964106/data/Containers/Bundle/Application/474123CE-45D0-45A4-A4B1-EC7588A849AB/DSYMDemo.app/DSYMDemo
- 0x10062f000 - 0x10068efff dyld_sim (*) <6fb74554-3370-3677-93d4-7f7a01ea6a80> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
- 0x10dee5000 - 0x10df50fff dyld (*) <499010ac-3054-326e-a050-fefffb5ce89a> /usr/lib/dyld
+ 0x10b9c9000 - 0x10d494fff com.apple.UIKitCore (1.0) /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
+ 0x103b83000 - 0x103b86fff com.unix.kernel.ExploreDSYM (1.0) /Users/USER/Library/Developer/CoreSimulator/Devices/C099153B-7736-4241-A734-2514070F8E90/data/Containers/Bundle/Application/E85716C6-245A-4B16-BA20-1A030036DE46/ExploreDSYM.app/ExploreDSYM
+ 0x103dbf000 - 0x103e1efff dyld_sim (*) <6fb74554-3370-3677-93d4-7f7a01ea6a80> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
+ 0x1055c9000 - 0x105634fff dyld (*) /usr/lib/dyld
0x7ff8006fe000 - 0x7ff80102eff4 com.apple.Foundation (6.9) <86cd050d-44fc-3045-a1f3-8ad5047b329e> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation
+
+EOF
+
+-----------
+Full Report
+-----------
+
+{"app_name":"ExploreDSYM","timestamp":"2025-06-19 17:15:55.00 +0800","app_version":"1.0","slice_uuid":"be3ba671-c69f-3a68-9d76-310c4e150256","build_version":"1","platform":7,"bundleID":"com.unix.kernel.ExploreDSYM","share_with_app_devs":0,"is_first_party":0,"bug_type":"309","os_version":"macOS 12.7.6 (21H1320)","incident_id":"2AA5A2DD-78A3-4FCC-807D-4523F88DBD0E","name":"ExploreDSYM"}
+{
+ "uptime" : 110000,
+ "procLaunch" : "2025-06-19 17:15:47.7752 +0800",
+ "procRole" : "Foreground",
+ "version" : 2,
+ "userID" : 501,
+ "deployVersion" : 210,
+ "modelCode" : "MacBookPro11,4",
+ "procStartAbsTime" : 110816195256181,
+ "coalitionID" : 8102,
+ "osVersion" : {
+ "train" : "macOS 12.7.6",
+ "build" : "21H1320",
+ "releaseType" : "User"
+ },
+ "captureTime" : "2025-06-19 17:15:50.0523 +0800",
+ "incident" : "2AA5A2DD-78A3-4FCC-807D-4523F88DBD0E",
+ "bug_type" : "309",
+ "pid" : 23538,
+ "procExitAbsTime" : 110818470471618,
+ "cpuType" : "X86-64",
+ "procName" : "ExploreDSYM",
+ "procPath" : "\/Users\/USER\/Library\/Developer\/CoreSimulator\/Devices\/C099153B-7736-4241-A734-2514070F8E90\/data\/Containers\/Bundle\/Application\/E85716C6-245A-4B16-BA20-1A030036DE46\/ExploreDSYM.app\/ExploreDSYM",
+ "bundleInfo" : {"CFBundleShortVersionString":"1.0","CFBundleVersion":"1","CFBundleIdentifier":"com.unix.kernel.ExploreDSYM"},
+ "storeInfo" : {"deviceIdentifierForVendor":"9BE90473-1F72-514D-A6F8-2A4F5F1C42D7","thirdParty":true},
+ "parentProc" : "launchd_sim",
+ "parentPid" : 18357,
+ "coalitionName" : "com.apple.CoreSimulator.SimDevice.C099153B-7736-4241-A734-2514070F8E90",
+ "crashReporterKey" : "B8015533-4AEE-8714-C435-0ABAB8898AE5",
+ "responsiblePid" : 624,
+ "responsibleProc" : "SimulatorTrampoline",
+ "wakeTime" : 27225,
+ "sleepWakeUUID" : "231A480C-858C-466F-AA42-E97E3B5B94AB",
+ "sip" : "enabled",
+ "isCorpse" : 1,
+ "exception" : {"codes":"0x0000000000000000, 0x0000000000000000","rawCodes":[0,0],"type":"EXC_CRASH","signal":"SIGABRT"},
+ "asiBacktraces" : ["0 CoreFoundation 0x00007ff8004288ab __exceptionPreprocess + 242\n1 libobjc.A.dylib 0x00007ff80004dba3 objc_exception_throw + 48\n2 CoreFoundation 0x00007ff8004b0019 -[__NSCFString characterAtIndex:].cold.1 + 0\n3 CoreFoundation 0x00007ff80035282d +[NSConstantArray automaticallyNotifiesObserversForKey:] + 0\n4 ExploreDSYM 0x0000000103b84f17 -[ViewController visitArray] + 55\n5 ExploreDSYM 0x0000000103b84e64 __29-[ViewController viewDidLoad]_block_invoke + 36\n6 libdispatch.dylib 0x00007ff80013ca3a _dispatch_client_callout + 8\n7 libdispatch.dylib 0x00007ff80013fe87 _dispatch_continuation_pop + 715\n8 libdispatch.dylib 0x00007ff80015534a _dispatch_source_invoke + 2046\n9 libdispatch.dylib 0x00007ff80014c1e9 _dispatch_main_queue_drain + 1015\n10 libdispatch.dylib 0x00007ff80014bde4 _dispatch_main_queue_callback_4CF + 31\n11 CoreFoundation 0x00007ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9\n12 CoreFoundation 0x00007ff800382436 __CFRunLoopRun + 2482\n13 CoreFoundation 0x00007ff8003816a7 CFRunLoopRunSpecific + 560\n14 GraphicsServices 0x00007ff809cb128a GSEventRunModal + 139\n15 UIKitCore 0x000000010c80aad3 -[UIApplication _run] + 994\n16 UIKitCore 0x000000010c80f9ef UIApplicationMain + 123\n17 ExploreDSYM 0x0000000103b851be main + 110\n18 dyld 0x0000000103dc12bf start_sim + 10\n19 ??? 0x00000001055ce52e 0x0 + 4384941358"],
+ "extMods" : {"caller":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"system":{"thread_create":0,"thread_set_state":627,"task_for_pid":9},"targeted":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"warnings":0},
+ "lastExceptionBacktrace" : [{"imageOffset":1202331,"symbol":"__exceptionPreprocess","symbolLocation":226,"imageIndex":6},{"imageOffset":138147,"symbol":"objc_exception_throw","symbolLocation":48,"imageIndex":4},{"imageOffset":1757209,"symbol":"-[__NSCFString characterAtIndex:].cold.1","symbolLocation":0,"imageIndex":6},{"imageOffset":325677,"symbol":"+[NSConstantArray automaticallyNotifiesObserversForKey:]","symbolLocation":0,"imageIndex":6},{"imageOffset":7959,"sourceLine":25,"sourceFile":"ViewController.m","symbol":"-[ViewController visitArray]","imageIndex":9,"symbolLocation":55},{"imageOffset":7780,"sourceLine":20,"sourceFile":"ViewController.m","symbol":"__29-[ViewController viewDidLoad]_block_invoke","imageIndex":9,"symbolLocation":36},{"imageOffset":10810,"symbol":"_dispatch_client_callout","symbolLocation":8,"imageIndex":5},{"imageOffset":24199,"symbol":"_dispatch_continuation_pop","symbolLocation":715,"imageIndex":5},{"imageOffset":111434,"symbol":"_dispatch_source_invoke","symbolLocation":2046,"imageIndex":5},{"imageOffset":74217,"symbol":"_dispatch_main_queue_drain","symbolLocation":1015,"imageIndex":5},{"imageOffset":73188,"symbol":"_dispatch_main_queue_callback_4CF","symbolLocation":31,"imageIndex":5},{"imageOffset":543519,"symbol":"__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__","symbolLocation":9,"imageIndex":6},{"imageOffset":521270,"symbol":"__CFRunLoopRun","symbolLocation":2482,"imageIndex":6},{"imageOffset":517799,"symbol":"CFRunLoopRunSpecific","symbolLocation":560,"imageIndex":6},{"imageOffset":12938,"symbol":"GSEventRunModal","symbolLocation":139,"imageIndex":7},{"imageOffset":14949075,"symbol":"-[UIApplication _run]","symbolLocation":994,"imageIndex":8},{"imageOffset":14969327,"symbol":"UIApplicationMain","symbolLocation":123,"imageIndex":8},{"imageOffset":8638,"sourceLine":17,"sourceFile":"main.m","symbol":"main","imageIndex":9,"symbolLocation":110},{"imageOffset":8895,"symbol":"start_sim","symbolLocation":10,"imageIndex":10},{"imageOffset":21806,"symbol":"start","symbolLocation":462,"imageIndex":11}],
+ "faultingThread" : 0,
+ "threads" : [{"triggered":true,"id":1036283,"threadState":{"r13":{"value":206158430216},"rax":{"value":0},"rflags":{"value":582},"cpu":{"value":0},"r14":{"value":6},"rsi":{"value":6},"r8":{"value":140701991410272},"cr2":{"value":0},"rdx":{"value":0},"r10":{"value":0},"r9":{"value":140701991410880},"r15":{"value":22},"rbx":{"value":4385445376,"symbolLocation":0,"symbol":"_main_thread"},"trap":{"value":133},"err":{"value":33554760},"r11":{"value":582},"rip":{"value":140704035753934,"matchesCrashFrame":1},"rbp":{"value":140701991410624},"rsp":{"value":140701991410584},"r12":{"value":259},"rcx":{"value":140701991410584},"flavor":"x86_THREAD_STATE","rdi":{"value":259}},"queue":"com.apple.main-thread","frames":[{"imageOffset":32718,"symbol":"__pthread_kill","symbolLocation":10,"imageIndex":0},{"imageOffset":25087,"symbol":"pthread_kill","symbolLocation":263,"imageIndex":1},{"imageOffset":516064,"symbol":"abort","symbolLocation":130,"imageIndex":2},{"imageOffset":67394,"symbol":"abort_message","symbolLocation":241,"imageIndex":3},{"imageOffset":6493,"symbol":"demangling_terminate_handler()","symbolLocation":266,"imageIndex":3},{"imageOffset":24706,"symbol":"_objc_terminate()","symbolLocation":96,"imageIndex":4},{"imageOffset":64357,"symbol":"std::__terminate(void (*)())","symbolLocation":8,"imageIndex":3},{"imageOffset":64278,"symbol":"std::terminate()","symbolLocation":54,"imageIndex":3},{"imageOffset":10830,"symbol":"_dispatch_client_callout","symbolLocation":28,"imageIndex":5},{"imageOffset":24199,"symbol":"_dispatch_continuation_pop","symbolLocation":715,"imageIndex":5},{"imageOffset":111434,"symbol":"_dispatch_source_invoke","symbolLocation":2046,"imageIndex":5},{"imageOffset":74217,"symbol":"_dispatch_main_queue_drain","symbolLocation":1015,"imageIndex":5},{"imageOffset":73188,"symbol":"_dispatch_main_queue_callback_4CF","symbolLocation":31,"imageIndex":5},{"imageOffset":543519,"symbol":"__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__","symbolLocation":9,"imageIndex":6},{"imageOffset":521270,"symbol":"__CFRunLoopRun","symbolLocation":2482,"imageIndex":6},{"imageOffset":517799,"symbol":"CFRunLoopRunSpecific","symbolLocation":560,"imageIndex":6},{"imageOffset":12938,"symbol":"GSEventRunModal","symbolLocation":139,"imageIndex":7},{"imageOffset":14949075,"symbol":"-[UIApplication _run]","symbolLocation":994,"imageIndex":8},{"imageOffset":14969327,"symbol":"UIApplicationMain","symbolLocation":123,"imageIndex":8},{"imageOffset":8638,"sourceLine":17,"sourceFile":"main.m","symbol":"main","imageIndex":9,"symbolLocation":110},{"imageOffset":8895,"symbol":"start_sim","symbolLocation":10,"imageIndex":10},{"imageOffset":21806,"symbol":"start","symbolLocation":462,"imageIndex":11}]},{"id":1036302,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036303,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036304,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036305,"name":"com.apple.uikit.eventfetch-thread","frames":[{"imageOffset":6458,"symbol":"mach_msg_trap","symbolLocation":10,"imageIndex":0},{"imageOffset":7336,"symbol":"mach_msg","symbolLocation":56,"imageIndex":0},{"imageOffset":542862,"symbol":"__CFRunLoopServiceMachPort","symbolLocation":145,"imageIndex":6},{"imageOffset":520159,"symbol":"__CFRunLoopRun","symbolLocation":1371,"imageIndex":6},{"imageOffset":517799,"symbol":"CFRunLoopRunSpecific","symbolLocation":560,"imageIndex":6},{"imageOffset":5605556,"symbol":"-[NSRunLoop(NSRunLoop) runMode:beforeDate:]","symbolLocation":213,"imageIndex":12},{"imageOffset":5606189,"symbol":"-[NSRunLoop(NSRunLoop) runUntilDate:]","symbolLocation":72,"imageIndex":12},{"imageOffset":15823494,"symbol":"-[UIEventFetcher threadMain]","symbolLocation":535,"imageIndex":8},{"imageOffset":5775643,"symbol":"__NSThread__start__","symbolLocation":1009,"imageIndex":12},{"imageOffset":25825,"symbol":"_pthread_start","symbolLocation":125,"imageIndex":1},{"imageOffset":8043,"symbol":"thread_start","symbolLocation":15,"imageIndex":1}]},{"id":1036306,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036307,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036308,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]}],
+ "usedImages" : [
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140704035721216,
+ "size" : 229376,
+ "uuid" : "2fe67e94-4a5e-3506-9e02-502f7270f7ef",
+ "path" : "\/usr\/lib\/system\/libsystem_kernel.dylib",
+ "name" : "libsystem_kernel.dylib"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140704036085760,
+ "size" : 49144,
+ "uuid" : "5a5f7316-85b7-315e-baf3-76211ee65604",
+ "path" : "\/usr\/lib\/system\/libsystem_pthread.dylib",
+ "name" : "libsystem_pthread.dylib"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140703129358336,
+ "size" : 544760,
+ "uuid" : "8a60f5c1-ea1f-352b-b778-967be44e3677",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/system\/libsystem_c.dylib",
+ "name" : "libsystem_c.dylib"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140703131009024,
+ "size" : 90108,
+ "uuid" : "ae8cbd53-0926-3251-b648-6f32d9330a50",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/libc++abi.dylib",
+ "name" : "libc++abi.dylib"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140703128797184,
+ "size" : 212970,
+ "uuid" : "2a7a213a-fdb2-311c-81d7-efdfd9ddf25a",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/libobjc.A.dylib",
+ "name" : "libobjc.A.dylib"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140703129903104,
+ "size" : 311284,
+ "uuid" : "59be51c1-e9f3-3a60-8108-cd70ae082897",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/system\/libdispatch.dylib",
+ "name" : "libdispatch.dylib"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140703131774976,
+ "CFBundleShortVersionString" : "6.9",
+ "CFBundleIdentifier" : "com.apple.CoreFoundation",
+ "size" : 3706877,
+ "uuid" : "2be0f79f-8b25-3614-9e7e-dbac565f72dd",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/Frameworks\/CoreFoundation.framework\/CoreFoundation",
+ "name" : "CoreFoundation",
+ "CFBundleVersion" : "1953.300"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140703292907520,
+ "CFBundleShortVersionString" : "1.0",
+ "CFBundleIdentifier" : "com.apple.GraphicsServices",
+ "size" : 32755,
+ "uuid" : "16365e42-1d5c-363d-84d1-3bb290a43253",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/PrivateFrameworks\/GraphicsServices.framework\/GraphicsServices",
+ "name" : "GraphicsServices",
+ "CFBundleVersion" : "1.0"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 4489777152,
+ "CFBundleShortVersionString" : "1.0",
+ "CFBundleIdentifier" : "com.apple.UIKitCore",
+ "size" : 28098560,
+ "uuid" : "adb282b1-2fb2-38e0-8492-47f9443eb1ef",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/PrivateFrameworks\/UIKitCore.framework\/UIKitCore",
+ "name" : "UIKitCore",
+ "CFBundleVersion" : "6209"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 4357369856,
+ "CFBundleShortVersionString" : "1.0",
+ "CFBundleIdentifier" : "com.unix.kernel.ExploreDSYM",
+ "size" : 16384,
+ "uuid" : "be3ba671-c69f-3a68-9d76-310c4e150256",
+ "path" : "\/Users\/USER\/Library\/Developer\/CoreSimulator\/Devices\/C099153B-7736-4241-A734-2514070F8E90\/data\/Containers\/Bundle\/Application\/E85716C6-245A-4B16-BA20-1A030036DE46\/ExploreDSYM.app\/ExploreDSYM",
+ "name" : "ExploreDSYM",
+ "CFBundleVersion" : "1"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 4359712768,
+ "size" : 393216,
+ "uuid" : "6fb74554-3370-3677-93d4-7f7a01ea6a80",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/dyld_sim",
+ "name" : "dyld_sim"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 4384919552,
+ "size" : 442368,
+ "uuid" : "eea022bb-a6ab-3cd1-8ac1-54ce8cfd3333",
+ "path" : "\/usr\/lib\/dyld",
+ "name" : "dyld"
+ },
+ {
+ "source" : "P",
+ "arch" : "x86_64",
+ "base" : 140703135948800,
+ "CFBundleShortVersionString" : "6.9",
+ "CFBundleIdentifier" : "com.apple.Foundation",
+ "size" : 9637877,
+ "uuid" : "86cd050d-44fc-3045-a1f3-8ad5047b329e",
+ "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/Frameworks\/Foundation.framework\/Foundation",
+ "name" : "Foundation",
+ "CFBundleVersion" : "1953.300"
+ }
+],
+ "sharedCache" : {
+ "base" : 140703128616960,
+ "size" : 3002335232,
+ "uuid" : "229cb26a-91c2-3a54-ac19-858f5ac3393c"
+},
+ "vmSummary" : "ReadOnly portion of Libraries: Total=647.6M resident=0K(0%) swapped_out_or_unallocated=647.6M(100%)\nWritable regions: Total=612.5M written=0K(0%) resident=0K(0%) swapped_out=0K(0%) unallocated=612.5M(100%)\n\n VIRTUAL REGION \nREGION TYPE SIZE COUNT (non-coalesced) \n=========== ======= ======= \nActivity Tracing 256K 1 \nColorSync 32K 4 \nFoundation 16K 1 \nKernel Alloc Once 8K 1 \nMALLOC 216.4M 40 \nMALLOC guard page 32K 7 \nMALLOC_NANO (reserved) 384.0M 1 reserved VM address space (unallocated)\nSTACK GUARD 56.0M 8 \nStack 11.6M 8 \nVM_ALLOCATE 28K 3 \n__DATA 4836K 257 \n__DATA_CONST 23.0M 270 \n__DATA_DIRTY 26K 12 \n__FONT_DATA 4K 1 \n__LINKEDIT 350.8M 18 \n__OBJC_RO 28.4M 1 \n__OBJC_RW 882K 1 \n__TEXT 296.8M 278 \ndyld private memory 1280K 2 \nmapped file 37.1M 7 \nshared memory 16K 1 \n=========== ======= ======= \nTOTAL 1.4G 922 \nTOTAL, minus reserved VM space 1.0G 922 \n",
+ "legacyInfo" : {
+ "threadTriggered" : {
+ "queue" : "com.apple.main-thread"
+ }
+},
+ "trialInfo" : {
+ "rollouts" : [
+ {
+ "rolloutId" : "6112e14f37f5d11121dcd519",
+ "factorPackIds" : {
+ "SIRI_TEXT_TO_SPEECH" : "634710168e8be655c1316aaa"
+ },
+ "deploymentId" : 240000231
+ },
+ {
+ "rolloutId" : "6112dda2fc54bc3389840642",
+ "factorPackIds" : {
+ "SIRI_DICTATION_ASSETS" : "620aec83b02b354d3afd2f50"
+ },
+ "deploymentId" : 240000145
+ }
+ ],
+ "experiments" : [
+
+ ]
+}
+}
```
分析:
-- iOS 存在 ASLR 技术,所以当发生 crash 的时候,获取到的符号是已经经过 dyld 启动加载,赋予过偏移量之后的地址
-- `.dSYM` 文件保存的地址是偏移后的地址。因为虚拟地址的 dyld 启动为了安全才做的随机偏移,且 `.dSYM` 文件是编译阶段就生成的,所以不可能是虚拟地址,一定是偏移地址。
+问题1: `4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25)` Crash 日志中的符号地址是偏移后的还是偏移前的?
+
+- iOS 存在 ASLR 技术,所以当发生 crash 的时候,获取到的符号是已经经过 dyld 启动加载,被偏移之后的地址
+
+问题2:.dSYM 文件保存的是虚拟地址还是偏移后的地址?
+
+处于安全原因,Apple 设计了 ASLR 机制。是 App 启动,经由 dyld 后对加载的动态库做了地址随机偏移,而 .dSYM 文件是在编译、链接后生成的,所以肯定保存的是虚拟地址。
+
+
+
+问题3:怎么计算原始地址?
+
+- 所以为了还原原始地址,也就是**符号偏移量**, 需要经过计算:**RuntimeAddress = ImageBaseAddress + SymbolOffset + PCOffset** => **SymbolOffset = RuntimeAddress - ImageBaseAddress - PCOffset**
+ - RuntimeAddress:**崩溃点运行时地址**,也就是 Crash 日志中符号的地址
+ - ImageBaseAddress:**镜像加载基址**,也就是 Crash 日志中,符号归属库的价值开始地址
+ - PCOffset:崩溃点距离符号起始的指令偏移
+
+如上 Crash 数据:
+
+- crash 发生在:**`4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25)`**,其中 **+55** 偏移量表示程序计数器(PC)距离函数入口点的字节偏移量,所以 **RuntimeAddress = 0x103b84f17,PCOffset = 0x37(十进制55,十六进制37)**
+
+- Crash 发生的的符号所在动态库为:ExploreDSYM。Crash 日志下面的 **Binary Images** 列出来所依赖动态库信息:
+
+ ``` shell
+ ExploreDSYM 0x103b83000 - 0x103b86fff com.unix.kernel.ExploreDSYM (1.0) /Users/USER/Library/Developer/CoreSimulator/Devices/C099153B-7736-4241-A734-2514070F8E90/data/Containers/Bundle/Application/E85716C6-245A-4B16-BA20-1A030036DE46/ExploreDSYM.app/ExploreDSYM
+ ```
+
+ 所以 **ImageBaseAddress = 0x103b83000**
+
+- 根据上述公式:**SymbolOffset = RuntimeAddress - ImageBaseAddress - PCOffset = 0x103b84f17 - 0x103b83000 - 0x37 = 0x100001EE0**
+
+- 3种验证方式:
+
+ - 在终端切目录到 App 内用:**nm -a ExploreDSYM | grep 1ee0** 验证,发现找到该符号的信息
+
+
+
+ - 在终端切目录到 App 内用: `dwarfdump --lookup 地址 --arch 架构 {AppName}.app.dSYM` 来找到相关信息,里面有符号名、文件名、代码行数,具体为:**dwarfdump --lookup 0x100001EE0 ExploreDSYM.app.dSYM** 验证,发现找到该符号的信息
+
+
+
+ - 在终端切目录到 App 内用:**objdump -d --start-address=0x100001ee0 --stop-address=0x100001f17 ExploreDSYM.app/ExploreDSYM** 验证,发现找到该符号的信息
+
+ 指令格式为:
+
+ ```shell
+ objdump -d --start-address={0x100001ee0} \
+ --stop-address={0x100001ee0+0x37} \
+ ExploreDSYM
+ ---->
+ objdump -d --start-address=0x100001ee0 \
+ --stop-address=0x100001F17 \
+ ExploreDSYM
+ ```
+
+
+
+
- 符号表存储了当前文件的符号信息,静态链接器(ld) 和动态链接器(dyld) 在链接的过程中都会读取符号表,另外调试器也会用符号表来把符号映射到源文件。
- 打包上线的时候会把调试符号等裁剪掉,但是线上统计到的堆栈我们仍然要能够知道对应的源代码,这时候就需要把符号写到另外一个单独的文件里,这个文件就是 DSYM。
- 符号化,什么是符号化?依靠符号表根据地址找对应的符号名、代码文件名的这个过程就叫符号化
--
- 但是符号表中记录的信息是未经 ASLR 的,也就是根据偏移地址来记录的。所以 crash 发生拿到的地址,需要计算出原始地址(原始地址也是相对 Mach-O 的地址)才可以根据符号表拿到原始符号、文件信息
-- 怎么计算原始地址?假设业务代码(也就是上面的 DSYMDemo)发生 crash,crash 地址为 `0x1003f4f16`,crash 报告最下面也列出了所以来的镜像(Mach-O),也提供了 base 地址。Mach-O 的开始地址为: `0x1003f3000 - 0x1003f6fff com.unix.kernel.DSYMDemo` 。则 ASLR 前的地址为:`0x1003f4f16 - 0x1003f3000`。计算后再去符号表查找对应的符号、文件信息
-- 然后利用 `dwarfdump --lookup 地址 --arch 架构 DSYMDemo.app.dSYM` 来找到相关信息,里面有符号名、文件名、代码行数
-#### 计算原始地址
+#### 5. 计算原始地址
+
+核心公式:**RuntimeAddress = ImageBaseAddress + SymbolOffset + PCOffset**
```objective-c
#import
#import
-uintptr_t get_slide_address(void) { // 获取 ASLR
+// 获取 ASLR 后的地址,也就是 RuntimeAddress
+uintptr_t get_slide_address(void) {
uintptr_t vmAddress_slide = 0;
- // 遍历所有加载过的 image,包括 ipa中的可执行文件 + 依赖的动态库
+ // 遍历所有加载过的 image,包括 ipa 中的可执行文件(也就是主 App 的可执行文件) + 依赖的动态库
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
+ // 处理当前 image
+ // 当前 image 的名称
const char *imageName = (char *)_dyld_get_image_name(i);
+
const struct mach_header *header = _dyld_get_image_header(i);
if (header->filetype == MH_EXECUTE) {
- // 获取 image 当前的偏移地址。偏移是基于该符号所在 image 的
+ // 获取 image 当前的偏移地址。偏移是基于该符号所在 image 的。vmAddress_slide 也就是 ImageBaseAddress
vmAddress_slide = _dyld_get_image_vmaddr_slide(i);
- }
- NSString *str = [NSString stringWithUTF8String:imageName];
- if ([str containsString:@"DSYMDemo"]) {
- NSLog(@"image name %s at address 0x%llx and ASLR slide 0x%lx.\n", imageName, (mach_vm_address_t)header, vmAddress_slide);
+
+ NSString *str = [NSString stringWithUTF8String:imageName];
+ // 当前 image 名如果包含 ExploreDSYM,则说明是要找的 image 地址
+ if ([str containsString:@"ExploreDSYM"]) {
+ NSLog(@"image name %s at address 0x%llx and ASLR slide 0x%lx.\n", imageName, (mach_vm_address_t)header, vmAddress_slide);
+ break;
+ }
}
}
return vmAddress_slide;
}
-- (void)getMethodVMAddress {
+// 计算虚拟内存地址 SymbolOffset = RuntimeAddress - ImageBaseAddress
+- (void)getRuntimeAddress {
// 获取 sel 的 ASLR 后的地址,因为启动后经过 dyld 做了偏移
- IMP imp = class_getMethodImplementation(self.class, @selector(visitElement));
+ IMP imp = class_getMethodImplementation(self.class, @selector(visitArray));
unsigned long imppos = (unsigned long)imp;
unsigned long slide = get_slide_address();
- // 符号的真实地址,在 ASLR 技术作用下,基于 Mach-O 的一个偏移地址。所以真实地址 = 经过 runtime 拿到的 imp 地址 - 当前 image 的起始地址
+ // SymbolOffset = RuntimeAddress - ImageBaseAddress
unsigned long addr = imppos - slide;
NSLog(@"%lu", addr);
}
```
-拿到真实地址,然后利用 `dwarfdump --lookup 0x0000000100001ce0 DSYMDemo.app.dSYM` 找到对应的信息,可以看到
+拿到真实地址,然后利用 **dwarfdump --lookup 0x0000000100001cb0 ./ExploreDSYM.app.dSYM** 找到对应的信息,可以看到
- 符号名
- 符号所在代码文件
- 符号所在代码文件的多少行
-
+
+
+可以确认:由于 iOS ASLR 的存在,符号真正的地址,是基于 image 的开始地址进行偏移得到的。
+
+**RuntimeAddress = ImageBaseAddress + SymbolOffset + PCOffset**,类似一个 `y = 1x + b` 的运算
+
+注意:PCOffset 理论上一直存在,但**表示上可能缺失**:
+
+- 当 PCOffset = 0 时通常省略
+- 调试信息不足时不显示
+- 系统库/优化代码中常见省略
-## ASLR
+## 十四、编译链接综合实战题目
-### 虚拟内存、物理内存、内存分页、ASLR
+### 1. 动态库还是静态库,对于 App 包体积影响更大?
-早期:应用程序的可执行文件是存储在磁盘上的,启动应用,则需要将可执行文件加载进内存,早期计算机是没有虚拟内存的,一旦加载就会全部加载到内存中,并且进程都是按照顺序排列的,这样存在两个问题:
+核心结论:**在绝大多数情况下,链接静态库对 iOS App 包体积影响更小(即产生的 App 更小)**
-- 当前进程只需要在合适的地址基础上,加减一些地址,就能访问到下一个进程所使用的内存,安全性很低
+**原因分析:**
-- 当前软件越来越大,开启多个软件时会直接全部加载到内存中,导致内存不够用。而且使用软件的时候大多数情况只使用部分功能。如果软件一打开就全部加载到内存中,会造成内存的严重浪费。
+1. **静态库的链接机制与优化:**
+ - 静态库 (`libxxx.a`) 本质上是一个 `.o` 文件的归档。
+ - 当 App 链接静态库时,**链接器 (`ld`)** 并不会把整个静态库文件都塞进最终的可执行文件。
+ - 相反,链接器会执行一个关键步骤:**只提取** App 代码实际**引用到的那些 `.o` 文件**中的代码和数据。
+ - 更进一步,在链接器内部(或通过 Xcode 的 `Dead Code Stripping` 选项)会进行 **`Dead Code Stripping` (死代码剥离)**。这意味着即使链接器提取了一个 `.o` 文件,它也会分析这个 `.o` 文件内部的符号(函数、全局变量等),**只保留那些最终被 App 或其他库代码实际使用到的符号**。没有被任何地方引用的符号会被安全地移除。
+ - **这就是静态库节省体积的关键:链接器在目标文件级别和符号级别进行了精细的裁剪,只包含 App 真正需要的部分。**
+2. **动态库的打包机制:**
+ - 动态库 (`libxxx.dylib` 或 `.framework`) 是一个独立的、预链接好的可执行代码单元。
+ - 当 App 链接并使用一个动态库时,App 本身只包含对这个库的**引用信息**(比如需要调用的函数名、库的安装路径等)。这些信息存储在 `LC_LOAD_DYLIB` 加载命令和符号表中。
+ - **然而,动态库文件本身 (`xxx.dylib` 或 `xxx.framework/xxx`) 必须作为一个完整的文件被包含在 App Bundle 中。**
+ - 即使 App 只使用了动态库中的一小部分功能(比如只用了其中一个函数),**整个动态库文件也必须被打包进去**。链接器没有机会像处理静态库那样只提取动态库中被用到的部分。
+ - **这是动态库增大包体积的关键:无论实际使用多少,整个库文件都必须完整地包含在 App 里。**
+3. **符号表与 Stripping:**
+ - 正如你提到的,无论是静态链接还是动态链接,最终 App 可执行文件 (`Mach-O`) 都会包含一个符号表。
+ - 在 App 发布时(Archive 或 Release build),Xcode 会执行 **`Strip Linked Product`** (通常设置为 `YES`)。这个步骤会移除对运行时**非必需**的符号信息。
+ - **剥离后:**
+ - **静态链接的代码:** 剥离后只剩下极其有限的调试信息(如果保留的话)和动态链接器必需的符号(如 `NSClassFromString` 需要的类名)。大部分原始符号名称都被移除,只保留地址信息。
+ - **动态库引用:** App 本身只需要保留对动态库中**外部符号的引用信息**(如函数名)。这些引用信息在剥离过程中**通常会被保留**,因为它们是动态链接器在运行时正确绑定符号所必需的(除非是隐藏符号)。但更重要的是,**动态库文件本身内部也包含了自己的符号表**。这个符号表在动态库被剥离时会被精简,但**整个动态库文件的体积不会因为 App 使用了其中多少而改变**。
+ - **关键点:** 符号表的剥离 (`strip`) 发生在链接之后,它确实能减小可执行文件的大小,但符号表本身在整个二进制中占比相对较小。**影响包体积的最大因素还是代码段 (`__TEXT`) 和数据段 (`__DATA`) 的大小。** 静态库通过 `Dead Code Stripping` 在链接阶段就移除了未使用的代码/数据,这是它节省体积的主要手段。动态库缺少这种精细的按需提取机制。
+4. **系统动态库的例外:**
+ - **系统提供的动态库** (如 `UIKit`, `Foundation`, `libSystem.dylib`) **不需要**打包到你的 App Bundle 中。它们已经存在于 iOS 设备上。
+ - 链接系统库时,App 只包含引用信息,不会增加额外体积(除了符号表引用,但剥离后也很小)。
+ - **只有你 App 自己打包的第三方动态库或自己开发的动态库才会增加 App 体积。**
-基于上述2个问题,诞生了虚拟内存技术。App 进程通过内存管理单元(Memory Management Unit, MMU)来管理的。MMU 使用一张特殊的表来跟踪虚拟内存地址和物理内存地址之间的映射关系。这张表通常被称为页表(Page Table)
+#### 1. **间接符号表(Indirect Symbol Table)的本质**
-内存分页:
+- **它存储的是索引,不是地址!**
+ - 它位于 Mach-O 文件的 `__LINKEDIT` 段中。
+ - 它是一个 **`uint32_t` 数组**。
+ - 每个条目存储的是一个 **符号在动态符号表(`Dynamic Symbol Table`, `LC_DYSYMTAB` Load Command 指向)中的索引**。
+- **作用:** 它是连接 **需要动态绑定符号的位置(如 `__stubs`, `__got`, `__la_symbol_ptr`)** 和 **符号定义(在某个动态库中)** 的桥梁。
+- **在磁盘上的 Mach-O 文件中:** 间接符号表只包含索引信息,**不包含任何目标地址**。目标地址是未知的,需要在运行时解析。
-- 页表结构:页表包含了一系列的表项,每个表项对应虚拟内存中的一个页(通常是 4KB)。每个表项包含了该虚拟页对应的物理页的信息,包括物理页的起始地址和一些状态信息。
-- 虚拟地址到物理地址的转换:当程序访问一个虚拟地址时,MMU 查看页表来找到对应的表项,然后根据表项中的信息将虚拟地址转换为物理地址。
-- 页表缓存(TLB - Translation Lookaside Buffer):为了提高地址转换的速度,MMU 通常会有一个 TLB,它缓存了最近访问的页表项。这样,对于频繁访问的地址,转换过程可以更快地完成
-- 页错误处理:如果一个虚拟地址在页表中没有对应的条目,就会发生页错误。操作系统的页错误处理程序会被调用,它负责加载缺失的页到物理内存中,并更新页表。
-- 内存分配:当应用程序请求内存时,操作系统会分配虚拟地址空间,并在页表中创建相应的条目。如果需要,操作系统还会分配物理内存页,并将其与虚拟页关联起来。
+#### 2. **绑定时机:启动时绑定 vs 延迟绑定**
-当物理内存满的时候,会发生覆盖。用户使用的活跃的数据,覆盖内存中最不活跃的数据那一页。对应现实的表现就是:iPhone 上永远可以较好的打开一个 App,比如打开了 App1、App2、App3...,等打开使用了一阵子后,内存紧张,我们尝试切换打开最早的 App1,会发现 App1 重新启动了,之前用的功能 A 的页面1,已经不见了,经历一个新的启动流程。
+`dyld` 解析符号地址的时机由符号的 **绑定信息(Binding Info)** 决定。这些信息存储在 `__LINKEDIT` 段中,由 `LC_DYLD_INFO` 或 `LC_DYLD_INFO_ONLY` Load Command 指向。
+
+- **a) 启动时绑定 (Non-Lazy / Load-Time Binding):**
+ - **绑定对象:**
+ - **全局/静态变量 (`__DATA, __got` 或 `__DATA_CONST, __got`):** 这些数据符号**必须在程序启动、任何代码访问它们之前**就被解析并填入正确的地址。因为数据访问通常没有像函数调用那样的“桩”机制来延迟处理。
+ - **某些关键函数 (较少见):** 如果编译器或链接器明确指定需要立即绑定(例如使用 `-bind_at_load` 链接器标志,但在 iOS/macOS 上通常不鼓励)。
+ - **绑定时机:** `dyld` 在 App 启动过程的 **`bind phase`** 完成这些绑定。
+ - **过程:**
+ 1. `dyld` 加载所有依赖的动态库。
+ 2. 在 `bind phase`,`dyld` 遍历 **`bind opcodes`**(一种紧凑的指令序列,描述了哪些位置的指针需要绑定到哪个库的哪个符号)。
+ 3. 根据 `bind opcode` 找到:
+ - 需要绑定的位置(例如 `__got` 中的某个条目地址)。
+ - 目标符号名(通过间接符号表索引找到动态符号表项,得到符号名)。
+ - 目标库。
+ 4. `dyld` 在目标库的导出符号表(通常是 `Trie` 树)中查找该符号,得到其实际内存地址(库基地址 `slide` + 符号偏移)。
+ 5. **将这个实际地址写入**需要绑定的位置(如 `__got` 中的条目)。
+ - **结果:** 当 App 的 `main` 函数执行时,这些非延迟绑定的符号地址**已经确定**。访问这些数据或调用这些函数(如果绑定了函数)不会有额外延迟。
+- **b) 延迟绑定 (Lazy Binding / Run-Time Binding):**
+ - **绑定对象:** **函数符号**(绝大多数情况下)。这是默认且优化的行为。
+ - **绑定时机:** **第一次调用该函数时。**
+ - **机制 (基于 `__stubs` 和 `__la_symbol_ptr`):**
+ 1. **`__stubs` Section (`__TEXT` 段):**
+ - 包含一系列小的桩代码(`stub`)。每个桩代码对应一个需要延迟绑定的函数。
+ - 初始状态下,每个 `stub` 的指令是跳转到 `dyld_stub_binder` 函数(由 `dyld` 提供),并传递一个关键参数:该桩对应的 **延迟绑定信息偏移量(Lazy Binding Info Offset)**。
+ 2. **`__la_symbol_ptr` Section (`__DATA` 段):**
+ - 是一个指针数组(`Pointer`)。
+ - **初始状态:** 每个条目存储的是**对应 `__stubs` 中那个桩代码的地址**。这确保了第一次调用函数时,CPU 会跳转到 `__stubs` 中的桩代码。
+ 3. **第一次调用发生:**
+ - App 代码调用 `NSLog` (编译后通常是 `callq _NSLog`)。
+ - `_NSLog` 符号在编译时被解析为指向 `__stubs` 中 `NSLog` 桩代码的地址(或者在某些架构/优化下直接指向 `__la_symbol_ptr` 条目)。
+ - CPU 执行跳转到 `__stubs` 中的 `NSLog` 桩代码。
+ - 桩代码执行:
+ - 跳转到 `dyld_stub_binder`。
+ - 传递自身对应的延迟绑定信息偏移量。
+ 4. **`dyld_stub_binder` 工作:**
+ - 根据传入的偏移量,找到延迟绑定信息(`lazy bind opcodes`)。
+ - 解析 `opcode`:确定要绑定的符号名(通过间接符号表索引)和目标库。
+ - 在目标库的导出符号表(`Trie`)中查找该符号,得到 `NSLog` 的实际内存地址。
+ - **关键步骤:** `dyld_stub_binder` **将 `__la_symbol_ptr` 中对应 `NSLog` 的条目内容,从指向桩代码的地址,改写为 `NSLog` 函数的实际地址!**
+ - 然后,`dyld_stub_binder` **直接跳转到 `NSLog` 函数的实际地址执行。**
+ 5. **后续调用:**
+ - 由于 `__la_symbol_ptr` 中的 `NSLog` 条目已被更新为真实地址。
+ - 下一次调用 `NSLog` 时,CPU 会**直接跳转到 `NSLog` 的真实地址**,不再经过 `__stubs` 桩代码和 `dyld_stub_binder`。**绑定只发生一次!**
+ - **优点:**
+ - **加速启动:** App 启动时,`dyld` 只需要处理非延迟绑定(主要是数据)。成千上万的函数绑定被推迟到实际使用时,显著减少了启动时间。
+ - **节省内存和 I/O:** 如果某些函数在整个 App 生命周期中从未被调用(例如,特定功能分支下的函数),那么它们就**永远不会被绑定**,避免了不必要的查找和 I/O(读取绑定信息、查找符号)。
+ - **`LC_DYLD_INFO` 中的 `lazy_bind_off/size`:** 专门存储描述哪些函数符号需要延迟绑定以及如何绑定的 `opcode` 指令。
+
+#### 3. GOT 的核心作用和工作原理
+
+##### 1. 解决数据访问的位置无关性问题
+
+- 动态库(或 PIE 可执行文件)会被加载到内存的任意地址(由 ASLR 随机化)。编译器在编译时**无法预知**全局变量(如 `extern int g_globalVar;`)最终会位于哪个内存地址。
+- 如果代码直接使用绝对地址访问 `g_globalVar`(如 `mov eax, [0x12345678]`),在加载地址随机化后,这个地址肯定是错误的。
+- **GOT 提供了间接层:** 代码不直接访问全局变量的绝对地址,而是访问一个 **固定位置** 的指针(即 GOT 条目),这个指针在运行时会被 `dyld` 填充为变量的**真实绝对地址**。
+
+##### 2. GOT 的结构与位置
+
+- **位置:** 在 Mach-O 文件中,GOT 通常位于 `__DATA,__got` 或 `__DATA_CONST,__got` section 中。现代系统倾向于使用 `__DATA_CONST` 以提高安全性(只读)。
+- **结构:** 它是一个 **指针数组(`uintptr_t` 数组)**。每个需要动态解析的**全局或静态数据符号**(变量)在 GOT 中独占一个条目(slot)。
+- **初始状态:** 在磁盘上的 Mach-O 文件中,GOT 条目的值通常为 **0 或其他占位值**(非有效地址)。真正的地址需要在运行时由 `dyld` 解析后填充。
+
+##### 3. 动态库与 App 的全局变量会写入同一个 GOT 吗?
+
+**不会。每个 Mach-O 模块(App 或动态库)拥有独立的 GOT:**
+
+- **主程序 (App)**
+ 在 `__DATA_CONST,__got` 中存储:
+ - 它引用的外部全局变量地址(如动态库中的 `global_in_dylib`)
+ - 自身需动态绑定的全局变量(较少见)
+- **动态库 (如 lib.dylib)**
+ 在自身的 `__DATA,__got` 中存储:
+ - 它引用的其他动态库的全局变量地址
+ - 若自身全局变量被外部引用(如 `global_in_dylib`),**该变量地址不写入 GOT,而是存储在动态库的数据段**(`__DATA,__data`),供其他模块通过其 GOT 间接访问。
+
+##### 4. 总结
+
+1. **提供间接层:** 在 `__DATA/__got` 中为每个需要动态解析的**全局数据变量**预留一个指针槽位。
+2. **支撑位置无关代码 (PIC):** 使代码通过 **RIP 相对寻址 + GOT 间接访问** 的方式,能够在加载地址随机化 (ASLR) 后正确访问外部全局数据。
+3. **存储解析结果:** 在运行时,由 `dyld` 在启动阶段 (**非延迟绑定**) 查找并计算出每个全局数据变量的**真实内存绝对地址**。
+4. **填充真实地址:** `dyld` 将计算得到的全局变量的**真实绝对地址写入**其对应的 GOT 条目。
+5. **实现高效数据访问:** 绑定完成后,代码通过访问 GOT 条目获得变量地址,再进行数据读写。整个过程保证了动态链接环境下全局数据访问的正确性和效率。
+
+#### 4. Non-Lazy Binding(启动时绑定) 和 Lazy Binding(延迟绑定)
+
+```mermaid
+sequenceDiagram
+ participant K as 内核
+ participant D as dyld
+ participant A as App
+ participant L as libSystem.dylib
+ participant M as 内存映射
+
+ rect rgb(240, 248, 255)
+ note over K,D: 启动阶段 - 非延迟绑定(数据符号)
+ A->>K: execve() 启动
+ K->>D: 加载 dyld 到内存
+ D->>D: 解析 App 的 Mach-O 头
+ D->>D: 读取 LC_LOAD_DYLIB(依赖库列表)
+ D->>K: mmap() 请求加载 libSystem.dylib
+ K->>M: 分配随机地址空间 (ASLR)
+ M-->>K: 返回基址 0x7fff80000000
+ K-->>D: 映射成功,基址=0x7fff80000000
+ D->>L: 验证代码签名
+ L-->>D: 签名有效 ✅
+ D->>D: 解析绑定信息 (LC_DYLD_INFO)
+ D->>D: 查找非延迟绑定符号(如 errno)
+ D->>L: 查询导出表 Trie 树
+ L-->>D: 返回 errno 偏移 0x5A000
+ D->>D: 计算真实地址 = 0x7fff805A000
+ D->>A: 更新 __got 条目:
__got[errno] = 0x7fff805A000
+ end
+
+ rect rgb(255, 250, 240)
+ note over A,D: 运行时阶段 - 延迟绑定(函数符号)
+ A->>A: 首次调用 NSLog
+ A->>A: 执行 __stubs[NSLog] 桩代码
+ A->>D: 调用 _dyld_stub_binder(索引42)
+ D->>D: 解析 lazy bind opcodes
+ D->>D: 通过 Indirect Symbol Table
找到符号名 "_NSLog"
+ D->>L: 查询导出表 Trie 树
+ L-->>D: 返回 NSLog 偏移 0x19B20
+ D->>D: 计算真实地址 = 0x7fff8019B20
+ D->>A: 修改 __la_symbol_ptr:
_NSLog_ptr = 0x7fff8019B20
+ D->>L: 跳转到 NSLog 实现
+ L-->>A: 执行 NSLog 函数
+ end
+
+ rect rgb(240, 255, 240)
+ note over A: 后续调用
+ A->>A: 再次调用 NSLog
+ A->>A: 直接通过 __la_symbol_ptr
跳转 0x7fff8019B20
+ A->>L: 执行 NSLog 实现
+ end
+```
+
+##### 1. 启动阶段 - 非延迟绑定(蓝色区域)
+
+1. **内核加载**:内核加载 dyld 并处理 mmap 请求
+2. **ASLR 处理**:为 libSystem 分配随机基址(0x7fff80000000)
+3. **代码签名验证**:dyld 验证动态库签名
+4. **数据符号绑定**:
+ - dyld 解析非延迟绑定信息(如 `errno` 变量)
+ - 查询动态库的导出 Trie 树获取符号偏移
+ - 计算真实地址(基址 + 偏移)
+ - **更新 App 的 GOT(__got 段)**
+ - *完成后数据变量立即可用*
+
+##### 2. 首次调用 - 延迟绑定(橙色区域)
+
+1. **桩代码触发**:App 调用 NSLog(实际执行桩代码)
+2. **绑定器调用**:桩代码调用 `_dyld_stub_binder` 并传递符号索引
+3. **符号解析**:
+ - dyld 通过 Indirect Symbol Table 获取符号名
+ - 查询动态库导出 Trie 获取函数偏移(0x19B20)
+ - 计算真实地址(0x7fff8019B20)
+4. **指针更新**:
+ - **修改 __la_symbol_ptr 条目**
+ - 跳转到真实函数地址
+ - *绑定仅发生一次*
+
+##### 3. 后续调用 - 直接访问(绿色区域)
+
+- 直接通过已更新的 `__la_symbol_ptr` 跳转
+- 无 dyld 参与,全速执行
-但虚拟内存方案带来一个问题。比如黑客不断探索发现,某个重要的功能位于第3页,是不是完全可以通过固定的地址去访问??
+#### 5. 总结对比
-因为早期物理内存方案下,App 启动后位于什么地址是不确定的。有了虚拟内存后,App 内符号的地址都是从0到4G,都是相对地址。
+| 特性 | 静态库 (`libxxx.a`) | 动态库 (`libxxx.dylib` / `.framework`) |
+| :---------------------------- | :---------------------------------------------------- | :----------------------------------------------- |
+| **包含机制** | **按需提取**:只链接 App 实际用到的 `.o` 文件中的符号 | **全量打包**:整个库文件必须包含在 App Bundle 中 |
+| **核心优化** | **Dead Code Stripping**:移除库中未使用的代码/数据 | **无**:即使只用一个函数,整个库文件也得打包 |
+| **对 App 可执行文件大小影响** | 较小 (仅包含实际使用的代码+剥离后符号表) | 很小 (仅包含引用信息+剥离后符号表) |
+| **对 App 整体包体积影响** | **小** | **大** (库文件本身显著增加体积) |
+| **符号表作用** | 链接时定位;剥离后大部分移除 | App 中:运行时绑定;库文件中:自身需要 |
+| **系统库处理** | 通常不直接链接系统静态库 | **不增加体积** (库已存在于系统) |
+| **主要体积来源** | App 可执行文件中链接进来的**有效代码/数据** | App Bundle 中**完整的动态库文件** |
-为了解决该问题,Apple 又推出了 ASLR 技术。核心就是为了解决虚拟内存地址固定不变的问题。就不能通过固定的地址访问内存或者数据了。
+**因此,对于 iOS App 包体积优化:**
-### 未使用 ASLR 的问题
+1. **优先使用静态库:** 对于第三方库或自己的模块库,**静态链接是减小最终包体积的首选方式**,因为它允许链接器移除未使用的代码。
+2. **谨慎使用自定义动态库:** 只有在有**强烈需求**时(如模块热更新 - 需注意 App Store 审核风险、需要在 App Extension 和主 App 间共享大量代码且内存占用优化优先于包体积时)才考虑打包自己的动态库,并意识到它会显著增加包大小。
+3. **充分利用系统动态库:** 链接系统动态库不会增加你的包体积,可以放心使用。
+4. **启用编译选项:** 确保 Xcode 的 `Dead Code Stripping` 设置为 `YES` (默认通常是),并且 `Strip Linked Product` 在 Release 模式下设置为 `YES` (默认也是)。对于动态库本身,也要配置其 Stripping 选项。
-- 函数代码存放在 `__TEXT` 段
+所以,链接静态库对于体积影响更小。关键在于 **`Dead Code Stripping` 机制允许链接器只包含 App 实际需要的代码**,而动态库则必须完整包含整个文件。符号表的影响相对较小,剥离 (`strip`) 操作对两者都有效,但无法弥补动态库全量包含带来的根本性体积增加。
-- 全局变量存放在 `__DATA` 段
+这也解释了 Apple 的官方建议:**iOS 开发优先使用静态库**。
-- 可执行文件的内存地址为 `0x0`
+### 2. 运行报错:Symbol not found 类问题
-- 可执行文件 Header 的内存地址,就是 `LC_SEGMENT(__TEXT)` 中的 VM Address
-
- - arm64 :`0x100000000`
-
- - 非 arm64:`0x4000`
+#### 1. 符号定位错误
-也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
-
-
-
-利用 MachOView 查看如下:
-
-
-
-
-
-
-
-- _PAGEZERO
- - VM Address:0x0
- - VM Size:0x100000000
-- _TEXT
- - VM Address:0x100000000
- - VM Size:0x4000
-- _DATA_CONST:
- - VM Address:0x10004000
- - VM Size:0x4000
-- _DATA
- - VM Address:0x10008000
- - VM Size:0x4000
-- _LINKEDIT
- - VM Address:0x1000C000
- - VM Size:0x8000
-
-
-
-
-
-
-
-
-
-File Offset:在 Mach-O 文件中的位置
-
-File Size:在 Mach-O 文件中的占据的大小
-
-从下面的图可以看出,`_PAGEZERO` 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。
-
-
-
-
-
-
-
-我们会发现根据 Mach-O 文件中的信息,File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全
-
-
-
-### ASLR 诞生
-
-Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
-
-
-
-
-
-- LC_SEGMENT (__TEXT) 的 VM Address `0x10005000`
-
-- ASLR 随机偏移 0x5000,也就是可执行文件的内存地址
-
-在有 ASLR 的时候:__TEXT 代码段地址 = __`PAGEZEROR 地址`
-
-在 Mach-O 文件中的地址是原始地址
+报错日志为:
```shell
-代码运行起来函数真实地址 = ASLR-Offset + __PAGEZERO(arm64:0x100000000,其他:0x4000)+ 函数基于 Mach-O 的地址
-```
\ No newline at end of file
+dyld[5466]: Symbol not found: _NSUserActivityTypeBrowsingWeb
+ Referenced from: <61E75C66-9BA7-3070-B783-EADF1665E135> /private/var/containers/Bundle/Application/51B572E2-1311-459F-89F1-11B96C2B48C4/xxxx.app/xxxx.debug.dylib
+ Expected in: <2C72BAF6-60AA-38C5-BC25-04F76D3EAAC2> /System/Library/Frameworks/CoreServices.framework/CoreServices
+ Symbol not found: _NSUserActivityTypeBrowsingWeb
+ Referenced from: <61E75C66-9BA7-3070-B783-EADF1665E135> /private/var/containers/Bundle/Application/51B572E2-1311-459F-89F1-11B96C2B48C4/xxxx.app/xxxx.debug.dylib
+ Expected in: <2C72BAF6-60AA-38C5-BC25-04F76D3EAAC2> /System/Library/Frameworks/CoreServices.framework/CoreServices
+ dyld config: DYLD_LIBRARY_PATH=/usr/lib/system/introspection DYLD_INSERT_LIBRARIES=/usr/lib/libLogRedirect.dylib:/usr/lib/libBacktraceRecording.dylib:/usr/lib/libRPAC.dylib:/usr/lib/libViewDebuggerSupport.dylib:/System/Library/PrivateFrameworks/GPUToolsCapture.framework/GPUToolsCapture
+```
+
+根本在于:
+
+```shell
+Symbol not found: _NSUserActivityTypeBrowsingWeb
+Expected in: CoreServices.framework
+```
+
+实际上,`NSUserActivityTypeBrowsingWeb` **属于 Foundation.framework**(iOS 10+ 引入),而不是 CoreServices.framework
+
+#### 2. 链接问题
+
+```mermaid
+graph LR
+ A[应用启动] --> B[加载 CoreServices]
+ B --> C[需要 NSUserActivityTypeBrowsingWeb]
+ C --> D{检查已加载框架}
+ D -->|Foundation 未加载| E[符号未找到]
+ D -->|Foundation 已加载| F[成功]
+```
+
+根本问题:**链接顺序错误导致符号解析失败**
+
+- CoreServices.framework 内部依赖 Foundation.framework 的符号
+- 但你的链接顺序是:**先链接 CoreServices,后链接 Foundation**
+- 当 CoreServices 尝试使用 `NSUserActivityTypeBrowsingWeb` 时,Foundation 尚未加载
+
+
+
+#### 3. 系统库为什么出现这种链接错误?
+
+系统库怎么可能会出错??虽然代码都是系统库,但最终都在 App 的链接配置的地方声明链接顺序的,链接顺序影响符号的发现能力。现在的工程都依赖 Cocoapods 构建。Cocoapods 会对依赖的库名称进行字典排序
+
+```shell
+# CocoaPods 处理逻辑
+# 原始顺序
+frameworks = ['CoreServices', 'Foundation', 'UIKit']
+# 排序后的顺序
+sorted_frameworks = frameworks.sort # => ['CoreServices', 'Foundation', 'UIKit']
+```
+
+字典序排序使 "CoreServices" 排在 "Foundation" 前。生成的 `OTHER_LDFLAGS` 变为:
+
+```shell
+-framework CoreServices
+-framework Foundation
+```
+
+[Cocospods::Xcodeproj/lib/xcodeproj /config.rb]( https://github.com/CocoaPods/Xcodeproj/blob/master/lib/xcodeproj/config.rb#L122-L160 ) 源码也可证明该问题:
+
+```ruby
+def to_hash(prefix = nil)
+ list = []
+ list += other_linker_flags[:simple].to_a.sort
+ modifiers = {
+ :frameworks => '-framework ',
+ :weak_frameworks => '-weak_framework ',
+ :libraries => '-l',
+ :arg_files => '@',
+ :force_load => '-force_load',
+ }
+ [:libraries, :frameworks, :weak_frameworks, :arg_files, :force_load].each do |key|
+ modifier = modifiers[key]
+ sorted = other_linker_flags[key].to_a.sort
+ if key == :force_load
+ list += sorted.map { |l| %(#{modifier} #{l}) }
+ else
+ list += sorted.map { |l| %(#{modifier}"#{l}") }
+ end
+ end
+
+ result = attributes.dup
+ result['OTHER_LDFLAGS'] = list.join(' ') unless list.empty?
+ result.reject! { |_, v| INHERITED.any? { |i| i == v.to_s.strip } }
+
+ result = @includes.map do |incl|
+ path = File.expand_path(incl, @filepath.dirname)
+ if File.readable? path
+ Xcodeproj::Config.new(path).to_hash
+ else
+ {}
+ end
+ end.inject(&:merge).merge(result) unless @filepath.nil? || @includes.empty?
+
+ if prefix
+ Hash[result.map { |k, v| [prefix + k, v] }]
+ else
+ result
+ end
+ end
+```
+
+
+
+**链接器的工作方式:**
+
+- **`ld` 按从左到右顺序加载框架**
+- **当 CoreServices 需要 Foundation 的符号时,右侧的 Foundation 尚未加载**
+
+#### 4. 解决方案:调整链接顺序
+
+在 Xcode 工程的 **Build Settings > Other Linker Flags** 中:
+
+1. 在 **最前面** 添加:`-framework Foundation`
+2. 保留 `$(inherited)` 继承 Pods 的设置
+
+```shell
+OTHER_LDFLAGS = (
+ -framework Foundation,
+ $(inherited) # Pods 生成的标志
+)
+```
+
+#### 5. 既然存在问题,Cocoapods 为什么这么设计
+
+##### 1. 排序功能的设计目的
+
+1. **确保生成确定性(Determinism)**
+ CocoaPods 的核心目标之一是保证 **跨环境一致性**。通过字典序排序链接标志,无论依赖库的声明顺序如何,最终生成的 `.xcconfig` 文件内容始终保持一致。这避免了:
+ - 不同机器执行 `pod install` 后出现无关的 Git 差异;
+ - 多人协作时因文件顺序随机变动导致的冲突10。
+2. **简化内部处理逻辑**
+ 排序后更容易实现:
+ - **去重**:合并重复的链接标志(如多次引用的同一框架);
+ - **依赖分析**:在生成 Pods 项目时,清晰映射库与框架的关联关系610。
+3. **兼容非顺序敏感场景**
+ 多数情况下,链接顺序不影响结果(例如独立的功能库)。排序在提升可维护性的同时,对大部分项目无负面影响
+
+##### 2. 排序引发 Bug 的本质原因
+
+当排序与**链接器的顺序敏感性冲突**时,问题显现:
+
+1. **链接器的工作机制**
+ 链接器(`ld`)按从左到右顺序解析符号:
+
+ - 若库 A 依赖库 B,则 B 必须出现在 A 的左侧;
+ - **字典序排序可能破坏隐式依赖关系**(如 `CoreServices` 依赖 `Foundation`,但 `CoreServices < Foundation` 按字母序排在前面)7。
+
+2. **典型案例**
+ 用户遇到的 `NSUserActivityTypeBrowsingWeb` 符号丢失:
+
+ bash
+
+ ```
+ # 排序后错误顺序
+ -framework CoreServices # 先链接,需 Foundation 中的符号
+ -framework Foundation # 后链接,符号未被引用
+ ```
+
+ 此时链接器在 `CoreServices` 中遇到未定义符号,但右侧无 `Foundation`,因此报错
+
+##### 3. 排序的取舍:为何不取消?
+
+| **优点** | **代价** |
+| :----------------- | :----------------------- |
+| ✅ 保证多环境一致性 | ⚠️ 破坏隐式链接顺序 |
+| ✅ 简化依赖管理逻辑 | ⚠️ 需手动调整关键框架顺序 |
+| ✅ 减少冗余标志 | |
+
+CocoaPods 选择排序,本质是**权衡后的工程决策**:
+
+- 多数项目不涉及深层符号依赖,排序收益 > 成本;
+- 顺序敏感性问题可通过显式声明(如 `$(inherited)` 或手动调整)解决
+
+CocoaPods 的排序不是“缺陷”,而是**为确定性牺牲局部灵活性**的典型设计。它解决了协作与维护的核心痛点,代价是将链接顺序的责任转移给开发者。在工程实践中,这种妥协是合理的——毕竟,可预测的构建环境比偶发的链接错误更可控。
+
+正如软件开发中的许多决策:**没有完美解,只有最适,ROI 也相对较高**
diff --git a/Chapter1 - iOS/chapter1.md b/Chapter1 - iOS/chapter1.md
index 15fc324..f79f64e 100644
--- a/Chapter1 - iOS/chapter1.md
+++ b/Chapter1 - iOS/chapter1.md
@@ -4,7 +4,7 @@
第一部分主要介绍 iOS 开发中遇到的问题或者有趣的知识
* [1、工程大小优化之图片资源](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.1.md)
-://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.2.md)
+ * [2、看透构造方法](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.2.md)
* [3、控制器加载的玄机](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.3.md)
* [4、如何优雅地调试手机网页?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md)
* [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md)
@@ -48,7 +48,7 @@
* [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md)
* [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md)
* [44、Awesome Hybrid - 1](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.44.md)
- * [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
+ * [45、](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
* [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md)
* [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md)
* [48、类别(Category)、拓展(Extension)、load、initialize](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
@@ -116,4 +116,5 @@
* [110、妙用设计模式来设计一个客户端校验器](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.110.md)
* [111、写给 iOSer 的鸿蒙开发 tips](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.111.md)
* [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)
\ No newline at end of file
+ * [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)
\ No newline at end of file
diff --git a/Chapter2 - Web FrontEnd/2.32.md b/Chapter2 - Web FrontEnd/2.32.md
index 94766e4..aa36ba6 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。采用不同的策略
diff --git a/SUMMARY.md b/SUMMARY.md
index 59ee173..cd36511 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -47,7 +47,7 @@
* [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md)
* [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md)
* [44、一个 Hybrid SDK 设计与实现](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.44.md)
- * [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
+ * [45、](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
* [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md)
* [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md)
* [48、类别(Category)、拓展(Extension)、load、initialize](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
@@ -116,6 +116,7 @@
* [111、写给 iOSer 的鸿蒙开发 tips](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.111.md)
* [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)
* [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)
diff --git a/assets/ MockCrashAndDiaplayViaConsoleApp.png b/assets/ MockCrashAndDiaplayViaConsoleApp.png
deleted file mode 100644
index 2c60a20..0000000
Binary files a/assets/ MockCrashAndDiaplayViaConsoleApp.png and /dev/null differ
diff --git a/assets/2018-11-16-MVP.png b/assets/2018-11-16-MVP.png
deleted file mode 100644
index 692f004..0000000
Binary files a/assets/2018-11-16-MVP.png and /dev/null differ
diff --git a/assets/2018-11-16-MVVM.png b/assets/2018-11-16-MVVM.png
deleted file mode 100644
index f9a6386..0000000
Binary files a/assets/2018-11-16-MVVM.png and /dev/null differ
diff --git a/assets/AppUseIndirectDynamicLibProcess.png b/assets/AppUseIndirectDynamicLibProcess.png
index 7520d08..249a609 100644
Binary files a/assets/AppUseIndirectDynamicLibProcess.png and b/assets/AppUseIndirectDynamicLibProcess.png differ
diff --git a/assets/AppleMVCImpl.png b/assets/AppleMVCImpl.png
new file mode 100644
index 0000000..4a3055c
Binary files /dev/null and b/assets/AppleMVCImpl.png differ
diff --git a/assets/AppleMVCRefine.png b/assets/AppleMVCRefine.png
new file mode 100644
index 0000000..2a2bb6f
Binary files /dev/null and b/assets/AppleMVCRefine.png differ
diff --git a/assets/BlockVariableDemo0.png b/assets/BlockVariableDemo0.png
new file mode 100644
index 0000000..cbf1cb7
Binary files /dev/null and b/assets/BlockVariableDemo0.png differ
diff --git a/assets/CalcuateARLRAddressViaDyld.png b/assets/CalcuateARLRAddressViaDyld.png
index ac6bd26..1d92ae9 100644
Binary files a/assets/CalcuateARLRAddressViaDyld.png and b/assets/CalcuateARLRAddressViaDyld.png differ
diff --git a/assets/CompilerErrorWhenOnlyFunctionDeclaration.png b/assets/CompilerErrorWhenOnlyFunctionDeclaration.png
new file mode 100644
index 0000000..d4d5e32
Binary files /dev/null and b/assets/CompilerErrorWhenOnlyFunctionDeclaration.png differ
diff --git a/assets/CompilerSuccessWhenOnlyFunctionDeclarationButWeakImport.png b/assets/CompilerSuccessWhenOnlyFunctionDeclarationButWeakImport.png
new file mode 100644
index 0000000..e2048f4
Binary files /dev/null and b/assets/CompilerSuccessWhenOnlyFunctionDeclarationButWeakImport.png differ
diff --git a/assets/DWARFDUMPSymbolViaOffset.png b/assets/DWARFDUMPSymbolViaOffset.png
new file mode 100644
index 0000000..36d2658
Binary files /dev/null and b/assets/DWARFDUMPSymbolViaOffset.png differ
diff --git a/assets/DispatchgroupNotify.png b/assets/DispatchgroupNotify.png
new file mode 100644
index 0000000..4065d9f
Binary files /dev/null and b/assets/DispatchgroupNotify.png differ
diff --git a/assets/DuplicateSymbolErrorInGlobalSymbol.png b/assets/DuplicateSymbolErrorInGlobalSymbol.png
new file mode 100644
index 0000000..b775fc4
Binary files /dev/null and b/assets/DuplicateSymbolErrorInGlobalSymbol.png differ
diff --git a/assets/HarmonyCannotChangeOutputFile.png b/assets/HarmonyCannotChangeOutputFile.png
new file mode 100644
index 0000000..a4c4805
Binary files /dev/null and b/assets/HarmonyCannotChangeOutputFile.png differ
diff --git a/assets/HarmonyCrossPlatformArch.png b/assets/HarmonyCrossPlatformArch.png
new file mode 100644
index 0000000..7c724c3
Binary files /dev/null and b/assets/HarmonyCrossPlatformArch.png differ
diff --git a/assets/HarmonyFunctionHook.png b/assets/HarmonyFunctionHook.png
new file mode 100644
index 0000000..4436ab4
Binary files /dev/null and b/assets/HarmonyFunctionHook.png differ
diff --git a/assets/HarmonyHookCompileTask.png b/assets/HarmonyHookCompileTask.png
new file mode 100644
index 0000000..5bfc78f
Binary files /dev/null and b/assets/HarmonyHookCompileTask.png differ
diff --git a/assets/HarmonyHookStep1.png b/assets/HarmonyHookStep1.png
new file mode 100644
index 0000000..38144d0
Binary files /dev/null and b/assets/HarmonyHookStep1.png differ
diff --git a/assets/HarmonyHookWithAST.png b/assets/HarmonyHookWithAST.png
new file mode 100644
index 0000000..fd59a79
Binary files /dev/null and b/assets/HarmonyHookWithAST.png differ
diff --git a/assets/HarmonyHookWithAST2.png b/assets/HarmonyHookWithAST2.png
new file mode 100644
index 0000000..58d10b1
Binary files /dev/null and b/assets/HarmonyHookWithAST2.png differ
diff --git a/assets/HarmonyHookWithChangeInput.png b/assets/HarmonyHookWithChangeInput.png
new file mode 100644
index 0000000..b417bc1
Binary files /dev/null and b/assets/HarmonyHookWithChangeInput.png differ
diff --git a/assets/HarmonyHookWithInstrument.png b/assets/HarmonyHookWithInstrument.png
new file mode 100644
index 0000000..a6642d5
Binary files /dev/null and b/assets/HarmonyHookWithInstrument.png differ
diff --git a/assets/KVOCannotObserveNSMutableArray.png b/assets/KVOCannotObserveNSMutableArray.png
new file mode 100644
index 0000000..bab1a5a
Binary files /dev/null and b/assets/KVOCannotObserveNSMutableArray.png differ
diff --git a/assets/KVOInstanceVariableCannotObserve.png b/assets/KVOInstanceVariableCannotObserve.png
new file mode 100644
index 0000000..8220f37
Binary files /dev/null and b/assets/KVOInstanceVariableCannotObserve.png differ
diff --git a/assets/KVOObserveNSMutableArray.png b/assets/KVOObserveNSMutableArray.png
new file mode 100644
index 0000000..9b0eb78
Binary files /dev/null and b/assets/KVOObserveNSMutableArray.png differ
diff --git a/assets/KVOObserveNSMutableArraySetter.png b/assets/KVOObserveNSMutableArraySetter.png
new file mode 100644
index 0000000..668f988
Binary files /dev/null and b/assets/KVOObserveNSMutableArraySetter.png differ
diff --git a/assets/KVOObserveNSMutableArrayUseAPI.png b/assets/KVOObserveNSMutableArrayUseAPI.png
new file mode 100644
index 0000000..3e16d79
Binary files /dev/null and b/assets/KVOObserveNSMutableArrayUseAPI.png differ
diff --git a/assets/KVOObservePropertyIsObject.png b/assets/KVOObservePropertyIsObject.png
new file mode 100644
index 0000000..cb5864c
Binary files /dev/null and b/assets/KVOObservePropertyIsObject.png differ
diff --git a/assets/KVOObservePropertyIsObjectUseAPI.png b/assets/KVOObservePropertyIsObjectUseAPI.png
new file mode 100644
index 0000000..580e793
Binary files /dev/null and b/assets/KVOObservePropertyIsObjectUseAPI.png differ
diff --git a/assets/LDIgnoreWeakSymbolWhenMeetSameName.png b/assets/LDIgnoreWeakSymbolWhenMeetSameName.png
new file mode 100644
index 0000000..62080e7
Binary files /dev/null and b/assets/LDIgnoreWeakSymbolWhenMeetSameName.png differ
diff --git a/assets/LDTreatIgnoreWeakSymbolWhenMeetSameName.png b/assets/LDTreatIgnoreWeakSymbolWhenMeetSameName.png
new file mode 100644
index 0000000..6e891c1
Binary files /dev/null and b/assets/LDTreatIgnoreWeakSymbolWhenMeetSameName.png differ
diff --git a/assets/LLVM-Debug.png b/assets/LLVM-Debug.png
new file mode 100644
index 0000000..194d550
Binary files /dev/null and b/assets/LLVM-Debug.png differ
diff --git a/assets/LLVM-Debug1.png b/assets/LLVM-Debug1.png
new file mode 100644
index 0000000..080f196
Binary files /dev/null and b/assets/LLVM-Debug1.png differ
diff --git a/assets/LLVM-Debug2.png b/assets/LLVM-Debug2.png
new file mode 100644
index 0000000..d067e6e
Binary files /dev/null and b/assets/LLVM-Debug2.png differ
diff --git a/assets/LLVM-Debug3.png b/assets/LLVM-Debug3.png
new file mode 100644
index 0000000..b3440de
Binary files /dev/null and b/assets/LLVM-Debug3.png differ
diff --git a/assets/LLVM-Debug4.png b/assets/LLVM-Debug4.png
new file mode 100644
index 0000000..db6c9f7
Binary files /dev/null and b/assets/LLVM-Debug4.png differ
diff --git a/assets/LLVM-Debug5.png b/assets/LLVM-Debug5.png
new file mode 100644
index 0000000..d68a54a
Binary files /dev/null and b/assets/LLVM-Debug5.png differ
diff --git a/assets/LLVM-Debug6.png b/assets/LLVM-Debug6.png
new file mode 100644
index 0000000..62d92fb
Binary files /dev/null and b/assets/LLVM-Debug6.png differ
diff --git a/assets/MVPArchStructure.png b/assets/MVPArchStructure.png
new file mode 100644
index 0000000..69722db
Binary files /dev/null and b/assets/MVPArchStructure.png differ
diff --git a/assets/MVVMArchStructure.png b/assets/MVVMArchStructure.png
new file mode 100644
index 0000000..261c619
Binary files /dev/null and b/assets/MVVMArchStructure.png differ
diff --git a/assets/MergeTwoStaticLib.png b/assets/MergeTwoStaticLib.png
new file mode 100644
index 0000000..f1bec2c
Binary files /dev/null and b/assets/MergeTwoStaticLib.png differ
diff --git a/assets/MockCrashAndDiaplayViaConsoleApp.png b/assets/MockCrashAndDiaplayViaConsoleApp.png
new file mode 100644
index 0000000..56a8f5a
Binary files /dev/null and b/assets/MockCrashAndDiaplayViaConsoleApp.png differ
diff --git a/assets/MultipleTargetWithMacro.png b/assets/MultipleTargetWithMacro.png
new file mode 100644
index 0000000..a17bb56
Binary files /dev/null and b/assets/MultipleTargetWithMacro.png differ
diff --git a/assets/MultipleTargetWithSwiftMacro.png b/assets/MultipleTargetWithSwiftMacro.png
new file mode 100644
index 0000000..b49f3dc
Binary files /dev/null and b/assets/MultipleTargetWithSwiftMacro.png differ
diff --git a/assets/NMListSymbolViaOffset.png b/assets/NMListSymbolViaOffset.png
new file mode 100644
index 0000000..f679187
Binary files /dev/null and b/assets/NMListSymbolViaOffset.png differ
diff --git a/assets/NSMutableArrayCopyIssue.png b/assets/NSMutableArrayCopyIssue.png
new file mode 100644
index 0000000..3f347e5
Binary files /dev/null and b/assets/NSMutableArrayCopyIssue.png differ
diff --git a/assets/NSThreadWillTerminateSoCannotUse.png b/assets/NSThreadWillTerminateSoCannotUse.png
index b2bc3fe..5b57e31 100644
Binary files a/assets/NSThreadWillTerminateSoCannotUse.png and b/assets/NSThreadWillTerminateSoCannotUse.png differ
diff --git a/assets/OBJDUMPSymbolViaOffset.png b/assets/OBJDUMPSymbolViaOffset.png
new file mode 100644
index 0000000..075e6f9
Binary files /dev/null and b/assets/OBJDUMPSymbolViaOffset.png differ
diff --git a/assets/OCCallSwiftAssemble.png b/assets/OCCallSwiftAssemble.png
new file mode 100644
index 0000000..b286ef2
Binary files /dev/null and b/assets/OCCallSwiftAssemble.png differ
diff --git a/assets/OCObjectHiddenByLinker.png b/assets/OCObjectHiddenByLinker.png
new file mode 100644
index 0000000..a051091
Binary files /dev/null and b/assets/OCObjectHiddenByLinker.png differ
diff --git a/assets/OCObjectListHiddenByLinker.png b/assets/OCObjectListHiddenByLinker.png
new file mode 100644
index 0000000..7990972
Binary files /dev/null and b/assets/OCObjectListHiddenByLinker.png differ
diff --git a/assets/OCObjectWasExportedByDefault.png b/assets/OCObjectWasExportedByDefault.png
new file mode 100644
index 0000000..1604ff9
Binary files /dev/null and b/assets/OCObjectWasExportedByDefault.png differ
diff --git a/assets/ObjectFileConvertToStaticLib.png b/assets/ObjectFileConvertToStaticLib.png
new file mode 100644
index 0000000..b61a5e2
Binary files /dev/null and b/assets/ObjectFileConvertToStaticLib.png differ
diff --git a/assets/PerformThreadTaskByRunLoopSource0.png b/assets/PerformThreadTaskByRunLoopSource0.png
new file mode 100644
index 0000000..cb51acc
Binary files /dev/null and b/assets/PerformThreadTaskByRunLoopSource0.png differ
diff --git a/assets/RunloopPerformSelector.png b/assets/RunloopPerformSelector.png
index 85c6d95..9900c1d 100644
Binary files a/assets/RunloopPerformSelector.png and b/assets/RunloopPerformSelector.png differ
diff --git a/assets/RunloopPerformSelectorAfterDelay.png b/assets/RunloopPerformSelectorAfterDelay.png
index 8e389d2..8b7b5f1 100644
Binary files a/assets/RunloopPerformSelectorAfterDelay.png and b/assets/RunloopPerformSelectorAfterDelay.png differ
diff --git a/assets/RunloopPerformSelectorAfterDelayFix.png b/assets/RunloopPerformSelectorAfterDelayFix.png
new file mode 100644
index 0000000..48d88aa
Binary files /dev/null and b/assets/RunloopPerformSelectorAfterDelayFix.png differ
diff --git a/assets/RunloopPerformSelectorAfterDelayFix1.png b/assets/RunloopPerformSelectorAfterDelayFix1.png
new file mode 100644
index 0000000..f4a7a9b
Binary files /dev/null and b/assets/RunloopPerformSelectorAfterDelayFix1.png differ
diff --git a/assets/RunloopPerformSelectorAfterDelayFix2.png b/assets/RunloopPerformSelectorAfterDelayFix2.png
new file mode 100644
index 0000000..941ba86
Binary files /dev/null and b/assets/RunloopPerformSelectorAfterDelayFix2.png differ
diff --git a/assets/RunloopPerformSelectorAfterDelayFix3.png b/assets/RunloopPerformSelectorAfterDelayFix3.png
new file mode 100644
index 0000000..a75871a
Binary files /dev/null and b/assets/RunloopPerformSelectorAfterDelayFix3.png differ
diff --git a/assets/RunloopPerformSelectorAfterDelayFix4.png b/assets/RunloopPerformSelectorAfterDelayFix4.png
new file mode 100644
index 0000000..b570b24
Binary files /dev/null and b/assets/RunloopPerformSelectorAfterDelayFix4.png differ
diff --git a/assets/SchemeChooseBuildConfiguration.png b/assets/SchemeChooseBuildConfiguration.png
new file mode 100644
index 0000000..34eceab
Binary files /dev/null and b/assets/SchemeChooseBuildConfiguration.png differ
diff --git a/assets/SelfDefineFramworkUseStaticLib.png b/assets/SelfDefineFramworkUseStaticLib.png
new file mode 100644
index 0000000..697d000
Binary files /dev/null and b/assets/SelfDefineFramworkUseStaticLib.png differ
diff --git a/assets/ShellScriptToExecuteConfigurationsShellScript.png b/assets/ShellScriptToExecuteConfigurationsShellScript.png
new file mode 100644
index 0000000..561c355
Binary files /dev/null and b/assets/ShellScriptToExecuteConfigurationsShellScript.png differ
diff --git a/assets/StaticLibUseDynamicLibFrameworkHandleByShell.png b/assets/StaticLibUseDynamicLibFrameworkHandleByShell.png
index d062879..ab16e86 100644
Binary files a/assets/StaticLibUseDynamicLibFrameworkHandleByShell.png and b/assets/StaticLibUseDynamicLibFrameworkHandleByShell.png differ
diff --git a/assets/SwiftBlockRetainCycle.png b/assets/SwiftBlockRetainCycle.png
new file mode 100644
index 0000000..0d9ea28
Binary files /dev/null and b/assets/SwiftBlockRetainCycle.png differ
diff --git a/assets/SwiftCallSwiftAssemble.png b/assets/SwiftCallSwiftAssemble.png
new file mode 100644
index 0000000..89cad98
Binary files /dev/null and b/assets/SwiftCallSwiftAssemble.png differ
diff --git a/assets/SwiftCallSwiftMethodUseDynamic.png b/assets/SwiftCallSwiftMethodUseDynamic.png
new file mode 100644
index 0000000..fe27e8b
Binary files /dev/null and b/assets/SwiftCallSwiftMethodUseDynamic.png differ
diff --git a/assets/SwiftCannotOverrideStaticMethod2.png b/assets/SwiftCannotOverrideStaticMethod2.png
new file mode 100644
index 0000000..7fc10af
Binary files /dev/null and b/assets/SwiftCannotOverrideStaticMethod2.png differ
diff --git a/assets/SwiftClassExposedToOCThenCalledBySwift.png b/assets/SwiftClassExposedToOCThenCalledBySwift.png
new file mode 100644
index 0000000..a24b5ff
Binary files /dev/null and b/assets/SwiftClassExposedToOCThenCalledBySwift.png differ
diff --git a/assets/SwiftClassExposedToOCThenCalledBySwift1.png b/assets/SwiftClassExposedToOCThenCalledBySwift1.png
new file mode 100644
index 0000000..2a15910
Binary files /dev/null and b/assets/SwiftClassExposedToOCThenCalledBySwift1.png differ
diff --git a/assets/SwiftClassExposedToOCThenCalledBySwift2.png b/assets/SwiftClassExposedToOCThenCalledBySwift2.png
new file mode 100644
index 0000000..3a01570
Binary files /dev/null and b/assets/SwiftClassExposedToOCThenCalledBySwift2.png differ
diff --git a/assets/SwiftClassPointerDemo2.png b/assets/SwiftClassPointerDemo2.png
index abf4384..cb90a87 100644
Binary files a/assets/SwiftClassPointerDemo2.png and b/assets/SwiftClassPointerDemo2.png differ
diff --git a/assets/SwiftEnumCaseNumAffectMemory.png b/assets/SwiftEnumCaseNumAffectMemory.png
new file mode 100644
index 0000000..b648d7d
Binary files /dev/null and b/assets/SwiftEnumCaseNumAffectMemory.png differ
diff --git a/assets/SwiftTypeProperytyDispatchOnce1.png b/assets/SwiftTypeProperytyDispatchOnce1.png
new file mode 100644
index 0000000..eafd6f0
Binary files /dev/null and b/assets/SwiftTypeProperytyDispatchOnce1.png differ
diff --git a/assets/SwiftTypeProperytyDispatchOnce2.png b/assets/SwiftTypeProperytyDispatchOnce2.png
new file mode 100644
index 0000000..a64ef8a
Binary files /dev/null and b/assets/SwiftTypeProperytyDispatchOnce2.png differ
diff --git a/assets/SwiftTypeProperytyDispatchOnce3.png b/assets/SwiftTypeProperytyDispatchOnce3.png
new file mode 100644
index 0000000..0d17ed0
Binary files /dev/null and b/assets/SwiftTypeProperytyDispatchOnce3.png differ
diff --git a/assets/Thread_priority.jpg b/assets/Thread_priority.jpg
deleted file mode 100644
index caa3781..0000000
Binary files a/assets/Thread_priority.jpg and /dev/null differ
diff --git a/assets/WeakSolveBlockRetainCycle.png b/assets/WeakSolveBlockRetainCycle.png
new file mode 100644
index 0000000..cc5a05d
Binary files /dev/null and b/assets/WeakSolveBlockRetainCycle.png differ
diff --git a/assets/XcconfigGeneratedByCocoapods.png b/assets/XcconfigGeneratedByCocoapods.png
new file mode 100644
index 0000000..7a67a70
Binary files /dev/null and b/assets/XcconfigGeneratedByCocoapods.png differ
diff --git a/assets/XcconfigInSpecificConditionTest.png b/assets/XcconfigInSpecificConditionTest.png
new file mode 100644
index 0000000..c66ec81
Binary files /dev/null and b/assets/XcconfigInSpecificConditionTest.png differ
diff --git a/assets/XcconfigMatchScheme.png b/assets/XcconfigMatchScheme.png
new file mode 100644
index 0000000..be51584
Binary files /dev/null and b/assets/XcconfigMatchScheme.png differ
diff --git a/assets/XcconfigResultCheck.png b/assets/XcconfigResultCheck.png
new file mode 100644
index 0000000..2036447
Binary files /dev/null and b/assets/XcconfigResultCheck.png differ
diff --git a/assets/XcodeAddUserDefinedSetting.png b/assets/XcodeAddUserDefinedSetting.png
new file mode 100644
index 0000000..d8e68dc
Binary files /dev/null and b/assets/XcodeAddUserDefinedSetting.png differ
diff --git a/assets/XcodeCompileErrorOnMockObjcClass.png b/assets/XcodeCompileErrorOnMockObjcClass.png
new file mode 100644
index 0000000..a693ba7
Binary files /dev/null and b/assets/XcodeCompileErrorOnMockObjcClass.png differ
diff --git a/assets/XcodeCreateNewConfiguration.png b/assets/XcodeCreateNewConfiguration.png
new file mode 100644
index 0000000..d52325a
Binary files /dev/null and b/assets/XcodeCreateNewConfiguration.png differ
diff --git a/assets/XcodeSchemeMatchConfiguration.png b/assets/XcodeSchemeMatchConfiguration.png
new file mode 100644
index 0000000..be52ba8
Binary files /dev/null and b/assets/XcodeSchemeMatchConfiguration.png differ
diff --git a/assets/XcodeXcconfigDemo1.png b/assets/XcodeXcconfigDemo1.png
new file mode 100644
index 0000000..88006d5
Binary files /dev/null and b/assets/XcodeXcconfigDemo1.png differ
diff --git a/assets/XcodeXcconfigDemo2.png b/assets/XcodeXcconfigDemo2.png
new file mode 100644
index 0000000..a8c7837
Binary files /dev/null and b/assets/XcodeXcconfigDemo2.png differ
diff --git a/assets/blockCaptureStrongObjectError.png b/assets/blockCaptureStrongObjectError.png
new file mode 100644
index 0000000..ab55090
Binary files /dev/null and b/assets/blockCaptureStrongObjectError.png differ
diff --git a/assets/runtime-objc_msgSend-messageSend.png b/assets/runtime-objc_msgSend-messageSend.png
index 09ea74d..7d8b401 100644
Binary files a/assets/runtime-objc_msgSend-messageSend.png and b/assets/runtime-objc_msgSend-messageSend.png differ
diff --git a/assets/xcode_run_cmd.sh b/assets/xcode_run_cmd.sh
new file mode 100755
index 0000000..bc6e341
--- /dev/null
+++ b/assets/xcode_run_cmd.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+ExecuteCommand() {
+ #判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空
+ #[[是 bash 程序语言的关键字。用于判断
+ if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
+ #作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
+ if [[ -n "$TTY" ]]; then
+ echo "♦ $@" 1>$TTY
+ else
+ echo "♦ $*"
+ fi
+ echo "------------------------------------------------------------------------------" 1>$TTY
+ fi
+ #与$*相同。但是使用时加引号,并在引号中返回每个参数。"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数
+ if [[ -n "$TTY" ]]; then
+ echo `$@ &>$TTY`
+ else
+ "$@"
+ fi
+ #显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
+ return $?
+}
+
+DisplayError() {
+ #在shell脚本中,默认情况下,总是有三个文件处于打开状态,标准输入(键盘输入)、标准输出(输出到屏幕)、标准错误(也是输出到屏幕),它们分别对应的文件描述符是0,1,2
+ # > 默认为标准输出重定向,与 1> 相同
+ # 2>&1 意思是把 标准错误输出 重定向到 标准输出.
+ # &>file 意思是把标准输出 和 标准错误输出 都重定向到文件file中
+ # 1>&2 将标准输出重定向到标准错误输出。实际上就是打印所有参数已标准错误格式
+ if [[ -n "$TTY" ]]; then
+ echo "$@" 1>&2>$TTY
+ else
+ echo "$@" 1>&2
+ fi
+
+}
+
+ExecuteCMDAndDisplayInTerminal() {
+ if [[ ! -e "$TTY" ]]; then
+ DisplayError "=========================================="
+ DisplayError "❌ ERROR Occured: Did not config tty to output."
+ exit -1
+ fi
+
+ if [[ -n "$CMD" ]]; then
+ ExecuteCommand "$CMD" ${CMD_FLAG}
+ else
+ DisplayError "=========================================="
+ DisplayError "❌ ERROR: Failed to run CMD. THE CMD must not be null"
+ fi
+}
+
+
+ExecuteCMDAndDisplayInTerminal