update: 动态库、静态库的编译链接细节

This commit is contained in:
FantasticLBP
2025-06-23 01:18:55 +08:00
parent aca020701b
commit 1142064d28
129 changed files with 10932 additions and 2615 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
Chapter1 - iOS/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -197,6 +197,21 @@ LLVM IR 有3种表示格式
## 调试 LLVM
选择 Edit Scheme.
<img src="./../assets/LLVM-Debug1.png" style="zoom:30%" />
<img src="./../assets/LLVM-Debug2.png" style="zoom:30%" />
<img src="./../assets/LLVM-Debug3.png" style="zoom:30%" />
<img src="./../assets/LLVM-Debug4.png" style="zoom:30%" />
<img src="./../assets/LLVM-Debug5.png" style="zoom:30%" />
<img src="./../assets/LLVM-Debug6.png" style="zoom:30%" />
最后就可以加断点进行 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 中的 **+** 点击,添加一些参数,如下图:
<img src="./../assets/LLVM-Debug7.png" style="zoom:30%" />
最后允许测试。注意LLVM 项目较大,可以选择顶部 "Product -> Perform Action -> Run Without Building".
## 用途
LLVM 的一些插件,比如 libclang、libTooling可以查看官方文档https://clang.llvm.org/docs/Tooling.html可以做一些**语法树解**

View File

@@ -1,7 +1,7 @@
## 写给 iOSer 的鸿蒙开发 tips
## 下载问题
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/DevEco-Studio-DownloadNetworkErrror.png)
![](./../assets/DevEco-Studio-DownloadNetworkErrror.png)
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.
@@ -16,3 +16,349 @@ For Mac OS: ~/Library/Application Support/Huawei/DevEcoStudio3.0/options/country
</component>
</application>
```
## 鸿蒙开发任务拆分
### 依赖梳理
- 梳理依赖的二方、三方 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 等一些原生系统弹窗能力
- keyboardkeyboard 桥主要用来控制键盘相关的操作
- NavigatorNavigator 桥主要用来负责导航以及对导航栏的定制操作
- NetworkNetwork 桥主要用于网络相关的操作
- StorageStorage 桥主要用来持久化存储和获取数据
- BroadcastBroadcast 桥主要用于接收和发送通知
### 自定义桥能力开发
- 定义桥协议:定义需要实现的功能、确定桥所属模块与方法名、参数等
- TS 侧开发:在 TS 侧实现桥协议的方法,确定以同步还是异步的方式回调
- Native 侧开发:通过公共通信层解析 TS 侧透传的方法与参数,实现 Native 侧功能与回调
- 桥注册:桥开发完毕后,需要在 Native 侧通过 registerCustomModule 方法,注册后才可以使用
- 桥使用TS 侧业务代码通过调用桥协议,来使用自定义桥功能
### 渲染
将 js 引擎返回的 UI 数据通过解析进行渲染,根布局为 stack子组件通过 offset 确定位置、size 确定大小、type 确定组件类型。会有重叠、内嵌的情况,则递归循环渲染即可
参考鸿蒙 RN 团队在1月份的方案使用 stack 组件内部forEach 循环渲染子组件。适配初期,在嵌套不深的页面没有发现问题,整体上打通了从 JS 代码到引擎渲染的核心流程。
<img src='./../assets/HarmonyCrossPlatformArch.png' style="zoom:40%" />
左侧的 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<typeof HttpClient.Builder>;
builderContext._eventListener = new MyEnevtListener()
builderContext.addInterceptor(new MyEnevtListener());
},
propertyMethodNameOrType: 'build'
})
```
无统一修改点场景三:`router.pushUrl`,编译时 + 运行时组合实现偷梁换柱。
<img src="./../assets/HarmonyHookWithInstrument.png" style="zoom:40%" />
如何实现编译时替换?
鸿蒙提供了 Hvigor Plugin 编译时自定义插件,用于实现定制化构建。思路:直接利用 Hvigor Plugin hook 某个编译 task
<img src="./../assets/HarmonyHookStep1.png" style="zoom:40%" />
那到底 hook 哪个编译 task
<img src="./../assets/HarmonyHookCompileTask.png" style="zoom:40%" />
问题:
- output 修改无效
<img src="./../assets/HarmonyCannotChangeOutputFile.png" style="zoom:40%" />
ArkTS 编译之后会产生临时目录,将 ets 编译为 ts。那是不是可以直接修改产物看看最后能不能影响方舟字节码。
发现修改了 index.protoBin 、ts 文件,发现最终无法影响编译产物 `*.abc` 文件
联系了鸿蒙团队的工程师验证了说是2条并行链路。并不是先编译产生临时文件再通过临时文件产生 `*.abc` 文件。事实上是2个并行过程。所以此路不通
- input 无法 hookHvigor plugin 暂未开放相关能力
<img src="./../assets/HarmonyHookWithChangeInput.png" style="zoom:30%" />
Hvigor 目前开放的能力有限,仅支持在默认的 task 前后加一些钩子函数。但无法修改 input。此路不通
- 既然没法直接修改默认 task怎么实现
思路copy -> modify -> revert
<img src="./../assets/HarmonyHookWithAST.png" style="zoom:40%" />
插桩只能影响产物,不能影响源码。所以先对源码进行备份。
```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 可以抽取封装一下。
于是,整个流程就变成了
<img src="./../assets/HarmonyHookWithAST.png" style="zoom:40%" />
可以利用 [Typescript AST Viewer](https://github.com/dsherret/ts-ast-viewer) 来查看 AST 抽象语法树信息。可以在线查看 AST官网地址为[Typescript AST Viewer](https://ts-ast-viewer.com)
<img src="./../assets/HarmonyFunctionHook.png" style="zoom:40%" />
#### Aspect运行时
官方在 API 11 开始提供的方案,可快速实现对类方法前后进行插桩或替换。
关键点一:属于可修改-即 addBefore、addAfter、replace 接口的原理,基于 class
#### AspectPro V1编译时<正则> + 运行时)
#### AspectPro V2编译时<AST> + 运行时)

View File

@@ -1,8 +1,12 @@
# Swift 枚举值内存布局
> enum 使用很简单,那大家有没有思考过系统针对枚举的实现是怎么样的?接下去会针对不同情况的枚举,结合汇编来窥探下系统实现原理。
> enum 使用很简单,那大家有没有思考过系统针对枚举的实现是怎么样的?
>
> 不同类型的枚举占用多大内存空间?下面结合汇编来窥探下系统实现原理
### 基础枚举
## 基础枚举(不带关联值、不带原始值)
```swift
enum Season {
@@ -21,22 +25,31 @@ print("over")
- `var season: Season = Season.spring` 基础枚举默认值是0。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/EnumBaseMemoryLayoutDemo1.png" style="zoom:25%">
<img src="./../assets/EnumBaseMemoryLayoutDemo1.png" style="zoom:25%">
- `season = Season.summer`此时可以看到第一个字节的位置是1.
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/EnumBaseMemoryLayoutDemo2.png" style="zoom:25%">
<img src="./../assets/EnumBaseMemoryLayoutDemo2.png" style="zoom:25%">
- `season = Season.antumn` 此时可以看到第一个字节的位置是2
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/EnumBaseMemoryLayoutDemo3.png" style="zoom:25%">
<img src="./../assets/EnumBaseMemoryLayoutDemo3.png" style="zoom:25%">
结论查看内存信息可以看到基础枚举只占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
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/EnumWithRawValueMemoryLayoutDemo1.png" style="zoom:25%">
<img src="./../assets/EnumWithRawValueMemoryLayoutDemo1.png" style="zoom:25%">
- `season = .winter` 基础枚举,当赋值为 winter 的时候可以看到第一个字节的位置是3
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/EnumWithRawValueMemoryLayoutDemo2.png" style="zoom:25%">
<img src="./../assets/EnumWithRawValueMemoryLayoutDemo2.png" style="zoom:25%">
结论带有原始值的枚举同样只占用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)` 带有关联值的枚举
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssociatedEnumMemoryLayoutDemo1.png" style="zoom:25%">
`.spring` 有3个 Int单个 Int 占8个字节空间所以红色框代表 spring 的1蓝色框代表 spring 的2绿色框代表 spring 的3黄色框代表枚举的第1个 case剩余7个字节为空。后续的7个字节是为了内存对齐而补齐的内存。
<img src="./../assets/AssociatedEnumMemoryLayoutDemo1.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssociatedEnumMemoryLayoutDemo3.png" style="zoom:25%">
<img src="./../assets/AssociatedEnumMemoryLayoutDemo3.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssociatedEnumMemoryLayoutDemo4.png" style="zoom:25%">
<img src="./../assets/AssociatedEnumMemoryLayoutDemo4.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssociatedEnumMemoryLayoutDemo5.png" style="zoom:25%">
<img src="./../assets/AssociatedEnumMemoryLayoutDemo5.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssociatedEnumMemoryLayoutDemo6.png" style="zoom:25%">
<img src="./../assets/AssociatedEnumMemoryLayoutDemo6.png" style="zoom:25%">
其内存信息如下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<Season>.size` 3个 Int 最大为3*81个字节用来表达位置信息`3*8 + 1 = 25`
- `MemoryLayout<Season>.stride` :获取系统分配给数据类型的内存大小,也就是实际内存大小(对齐后的)
- `MemoryLayout<Season>.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<SimpleEnum>.alignment) // 1
现在好理解2个 case 需要存储1个 Byte 的值来区分是哪个 case1 Byte 可以代表最多256个 case
### 只有1个 case 且带关联值的枚举
## 只有1个 case 且带关联值的枚举
```swift
enum SimpleEnum {
@@ -217,7 +243,10 @@ print(MemoryLayout<SimpleEnum>.stride) // 8
print(MemoryLayout<SimpleEnum>.alignment) // 8
```
带有关联值且只有1个 case 的枚举因为有1个 Int 的关联值,但只有1个 case所以只需要8 Byte 存储关联值即可。
- 带有关联值且只有1个 case 的枚举因为有1个 Int 的关联值需要8 Byte 存储关联值
- 但只有1个 case不需要额外空间来判断所属哪个枚举值所以不需要额外空间
请看下面的对照实验
@@ -232,13 +261,55 @@ print(MemoryLayout<SimpleEnum>.stride) // 16
print(MemoryLayout<SimpleEnum>.alignment) // 8
```
2个 case其中一个 case 有关联值 Int所以需要8 Byte 存 Int 值1 Byte 区分是哪个 case实际需要占用 8 + 1 = 9 Byte内存对齐单位是89向上为16.
2个 case其中一个 case 有关联值 Int所以需要8 Byte 存 Int 值1 Byte 区分是哪个 case实际需要占用 8 + 1 = 9 Byte内存对齐单位是89向上为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 字节)。
更改验证标签对于枚举占用内存大小的影响
<img src="./../assets/SwiftEnumCaseNumAffectMemory.png" style="zoom:40%" />
可以看到 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)` 位置
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssociatedEnumMemoryLayoutExplore.png" style="zoom:25%">
<img src="./../assets/AssociatedEnumMemoryLayoutExplore.png" style="zoom:25%">
将断点处的汇编单独摘出来研究
@@ -295,3 +366,77 @@ print("over")
- 且存在内存对齐,所以占用大小为 n 和 1 的最大值,再结合内存对齐。
- 如果枚举的定义非常简单系统会用1个字节来存放值最大范围是256个 case。
- 枚举定义如果有原始值,也不会影响内存布局。
## 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`)。

View File

@@ -1,6 +1,6 @@
# Swift 结构体和类的内存布局
## 结构体内存布局
## 结构体初始化器
实验1在 struct 内部自己实现 init
@@ -18,7 +18,7 @@ var point = Point()
`init` 方法内第一行处加 断点,如下所示
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStructMemoryLayoutDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftStructMemoryLayoutDemo1.png" style="zoom:25%">
实验2struct 内不自己加 init
@@ -32,11 +32,15 @@ var point = Point()
`var point = Point()`处加 断点,如下所示
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StructMemoryLayoutDemo2.png" style="zoom:25%">
<img src="./../assets/StructMemoryLayoutDemo2.png" style="zoom:25%">
结论:结构体会有一个编译器自动生成的初始化编译器。目的是保证所有成员都有初始值
现象:可以看到加不加自定义初始化器的汇编代码基本相同
实验3
结论:**如果没有为结构体声明初始化器编译器会自动生成1个初始化器。目的是保证所有成员都有初始值。**
## 结构体内存布局
```swift
struct CustomDate {
@@ -62,6 +66,7 @@ Int 占8 ByteBool 占1 Byte共 2*8 + 1 = 17 Byte由于存在内存对
- 与类的比较:与 `class`(类)不同,`struct` 不需要额外的内存来存储类型信息、引用计数或其他元数据。这使得 `struct`通常比 `class` 更轻量级,并且在某些情况下具有更好的性能。
## 类的内存布局
类和结构体类似,但是编译器不会为类自动生成可以传入成员值的初始化器。
@@ -124,13 +129,13 @@ test()
断点打在 `var point1 = Point(x: 10, y: 20)` 处,查看汇编
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftValuePassDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftValuePassDemo1.png" style="zoom:25%">
乍一看如果不认识的话,先找字面量(立即数),比如红色框内的 `0xa`,就是 10`0x14` 就是20。[之前](./109.md)学过寄存器的设计64位寄存器是兼容32位寄存器的。红色框内将 `0xa`,也就是 10 保存到 `%edi ` 寄存器内部,也就是保存到 `%rdi` 中,将 `0x14` 也就是20保存到 `%esi` 也就是保存到 `%rsi` 寄存器中。
LLDB 模式下输入 `si` 进入 init 方法内部。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftValuePassDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftValuePassDemo2.png" style="zoom:25%">
可以查看到将 `%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-WriteCOW写时复制 技术,这是一种内存优化策略,旨在提升性能并减少不必要的内存复制**
对于标准库值类型的赋值操作Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值。
核心思想:
- **延迟复制**:当多个变量引用同一份数据时,它们共享底层存储,直到某个变量尝试修改数据时,才会真正复制一份独立的副本。
- **节省资源**:避免对不可变数据进行冗余复制,减少内存占用和计算开销
仅当有“写”操作时,才会真正执行拷贝操作:
- 对于标准库值类型的赋值操作Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
- 自定义的值类型,比如结构体,在赋值的时候,就会立马发生深拷贝
举个例子
```swift
var array1 = [1, 2, 3]
var array2 = array1 //
array2.append(4) // COWarray1 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()
下断点,可以看到下面的汇编:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClassReferenceTypeMemoryLayoutDemo1.png" style="zoom:25%">
<img src="./../assets/ClassReferenceTypeMemoryLayoutDemo1.png" style="zoom:25%">
在调用(汇编的 call完 `allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClassReferenceTypeMemoryLayoutDemo2.png" style="zoom:25%">
<img src="./../assets/ClassReferenceTypeMemoryLayoutDemo2.png" style="zoom:25%">
红色框代表类信息的地址蓝色框代表引用计数绿色框代表10黄色框代表20.

View File

@@ -43,9 +43,9 @@ print("全局变量", Mems.ptr(ofVal: &p))
print("堆空间", Mems.ptr(ofRef: p))
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClassMemoryLayoutExcludeFunction.png" style="zoom:25%">
<img src="./../assets/ClassMemoryLayoutExcludeFunction.png" style="zoom:25%">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClassMemoryLayoutExcludeFunction2.png" style="zoom:25%">
<img src="./../assets/ClassMemoryLayoutExcludeFunction2.png" style="zoom:25%">
代码段Person.sayHi 0x1000034d0
@@ -183,7 +183,7 @@ print(fn(2)) // 2
print(fn(3)) // 3
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClosureCaptureVariableDemo1.png" style="zoom:25%">
<img src="./../assets/ClosureCaptureVariableDemo1.png" style="zoom:25%">
@@ -207,7 +207,7 @@ print(fn(3)) // 6
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClosureCaptureVariableDemo2.png" style="zoom:25%">
<img src="./../assets/ClosureCaptureVariableDemo2.png" style="zoom:25%">
可以看到上面有 `allocObject` 方法调用,说明产生了堆空间对象,用于存放 `num` 。是由 `var fn = getFn()` 造成的调用1次 `getFn` 则产生1次堆空间分配用于保存 num。
@@ -235,31 +235,31 @@ print(fn3(3)) // 4
我们在汇编 `swift_allocObject` 下面下个断点
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClosureCaptureVariableDemo3.png" style="zoom:25%">
<img src="./../assets/ClosureCaptureVariableDemo3.png" style="zoom:25%">
第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x0000600000210000`。此时还没值。
第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x0000600000210000`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClosureCaptureVariableDemo4.png" style="zoom:25%">
<img src="./../assets/ClosureCaptureVariableDemo4.png" style="zoom:25%">
可以看到内存数据发生了改变。绿色框内有了值1。
第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x00006000002042c0`。此时还没值。
第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x00006000002042c0`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClosureCaptureVariableDemo5.png" style="zoom:25%">
<img src="./../assets/ClosureCaptureVariableDemo5.png" style="zoom:25%">
第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x000060000020d460`。此时还没值。
第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x000060000020d460`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClosureCaptureVariableDemo6.png" style="zoom:25%">
<img src="./../assets/ClosureCaptureVariableDemo6.png" style="zoom:25%">
打印结果也说明了问题因为调用3次 `getFn()` 所以会在堆上 alloc 3块内存用于保存捕获的变量。所以调用 fn1 得到 2调用 fn2 得到 3调用 fn3 得到 4。
@@ -279,7 +279,7 @@ print(fn(1, 2)) // 3
在 `var fn = sum` 处下断点,可以看到下面汇编
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouserMemoryLayoutExploreDemo1.png" style="zoom:25%">
<img src="./../assets/ClouserMemoryLayoutExploreDemo1.png" style="zoom:25%">
我们知道 `leaq` 是从 `%rip + 0x10f` 算出来的地址,赋值给 `%rax`。所以大概可以推测 `%rip + 0x10f` 就是 `sum` 函数地址。LLDM p 打印 `sum` 地址为 `0x0000000100003a30`。
@@ -297,15 +297,15 @@ print(fn(1, 2)) // 3
直奔主题,研究闭包内存
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
<img src="./../assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么LLDB 输入 `si`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo2.png" style="zoom:25%">
<img src="./../assets/ClouseExploreNotCaptureVariableDemo2.png" style="zoom:25%">
可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算2个 `ecx` 异或结果为0写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx``edx` 也就是 `rdx`。走完第6行的汇编继续看第7、8行
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
<img src="./../assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 ` 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。
@@ -315,7 +315,7 @@ print(fn(1, 2)) // 3
继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包)
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
<img src="./../assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
基本可以断定函数会返回一个长度为16 Byte 的内存。分别保存在 `rax` 和 `rdx`上。所以针对性的研究 `rdx` 和 `rax`
@@ -323,7 +323,7 @@ print(fn(1, 2)) // 3
20行的 `rax` 保存了一个看似是 `plus` 的函数地址。具体是:`rip + 0x167 = 0x100003969 + 0x167= 0x100003C37 `
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
<img src="./../assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。
@@ -340,13 +340,13 @@ Tips由于地址是动态生成的所以真正去调用 plus 的时候一
顺着思路,分析下汇编:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreCaptureVariableDemo1.png" style="zoom:25%">
<img src="./../assets/ClouseExploreCaptureVariableDemo1.png" style="zoom:25%">
我们看到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`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreCaptureVariableDemo2.png" style="zoom:25%">
<img src="./../assets/ClouseExploreCaptureVariableDemo2.png" style="zoom:25%">
可以看到在方法内部第6行汇编处直接调用一个代码段的函数地址 `jmp 0x1000039f0 `
@@ -357,13 +357,13 @@ Tips由于地址是动态生成的所以真正去调用 plus 的时候一
LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreCaptureVariableDemo3.png" style="zoom:25%">
<img src="./../assets/ClouseExploreCaptureVariableDemo3.png" style="zoom:25%">
`fn1` 函数调用的时候,参数如何传递?
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreCaptureVariableDemo4.png" style="zoom:25%">
<img src="./../assets/ClouseExploreCaptureVariableDemo4.png" style="zoom:25%">
汇编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 内部
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreCaptureVariableDemo5.png" style="zoom:25%">
<img src="./../assets/ClouseExploreCaptureVariableDemo5.png" style="zoom:25%">
@@ -379,7 +379,7 @@ LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
继续输入 `si`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreCaptureVariableDemo6.png" style="zoom:25%">
<img src="./../assets/ClouseExploreCaptureVariableDemo6.png" style="zoom:25%">
可以看到汇编的第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相加。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreCaptureVariableDemo7.png" style="zoom:25%">
<img src="./../assets/ClouseExploreCaptureVariableDemo7.png" style="zoom:25%">
可以看到第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`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftAutoClosureError.png" style="zoom:25%">
<img src="./../assets/SwiftAutoClosureError.png" style="zoom:25%">
正确的做法是:要么加 `@autoclosure` 要么在函数调用的时候加 `{}`
@@ -507,4 +528,63 @@ func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> S
}
collectCustomerProviders(group.remove(at: 0))
```
## 闭包和闭包表达式的区别
### 闭包
定义:闭包是自包含的功能代码块,可以在代码中被传递和使用。它能捕获并存储其所在上下文的常量和变量(即“闭合”环境)
种类:
- 全局函数(有名称,不捕获任何值)
- 嵌套函数(有名称,可捕获外曾函数的变量)
- 闭包表达式(匿名,轻量语法,可以捕获上下文变量)
闭包强调的是:捕获上下文的能力。不管是函数还是闭包表达式
### 闭包表达式
定义:是 Swift 中一种简洁的语法格式,用于编写匿名闭包。是闭包的一种实现方式之一(函数和闭包表达式都可以实现闭包)
特点:
- 没有函数名
- 语法清凉,支持参数类型推断、隐式返回、简写参数名(如 $0、$1等特性
- 通常用于作为函数的参数传递
### 总结
- **闭包**是广义概念,包含所有能捕获上下文的代码块(如函数、闭包表达式)。
- **闭包表达式**是闭包的一种具体实现方式,专指用轻量语法编写的匿名闭包。

View File

@@ -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<Season>.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<Season>.size) // 1
print(MemoryLayout<Season>.stride) // 1
print(MemoryLayout<Season>.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<Circle>.size) // 8
print(MemoryLayout<Circle>.stride) // 8
print(MemoryLayout<Circle>.alignment) // 8
```
计算属性 `y` 等价于下面的代码:
@@ -36,11 +122,94 @@ getDiameter () {
然后通过汇编来窥探下,在 `circle.diameter = 22` 处加断点,可以看到本质上调用的就是 `setter` 方法,`setter` 内部的实现就不一一窥探了
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStorePropertySetterDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftStorePropertySetterDemo1.png" style="zoom:25%">
然后断点继续,在 `let diameter = circle.diameter` 处加断点,可以看到调用了 getter 方法
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStorePropertySetterDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftStorePropertySetterDemo2.png" style="zoom:25%">
计算属性的本质就是方法看上去是属性但是不占用结构体的内存。而是独立在代码段中所以只占用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)
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftEnumRawValueExplore.png" style="zoom:25%">
<img src="./../assets/SwiftEnumRawValueExplore.png" style="zoom:25%">
通过汇编可以可以看到,在调用 `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
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftInoutExploreDemo1.png" style="zoom:25%">
然后看到17行的关键代码LLDB 输入 `si`可以看到在第6行 `movq $0x14, (%rdi)`将16进制的 `0x14` 也就是20移动到指定的内存地址 `rdi` 上
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftInoutExploreDemo2.png" style="zoom:25%">
因为 `struct` 结构体内存布局中,成员变量在内存中是连续存储的。所以 `struct `的地址也就是 `struct` 中第一个成员变量 `width` 的地址。
@@ -262,13 +462,13 @@ width is 5, side is 4, girth is 20
在 `changeValue(&shape.girth)` 处下断点,查看汇编
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo3.png" style="zoom:25%">
<img src="./../assets/SwiftInoutExploreDemo3.png" style="zoom:25%">
核心思路:方法参数用 `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`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo4.png" style="zoom:25%">
<img src="./../assets/SwiftInoutExploreDemo4.png" style="zoom:25%">
@@ -311,7 +511,7 @@ width is 10, side is 20, girth is 200
在 `changeValue(&shape.side)` 处添加断点,查看汇编
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo5.png" style="zoom:25%">
<img src="./../assets/SwiftInoutExploreDemo5.png" style="zoom:25%">
分析:
@@ -329,11 +529,15 @@ width is 10, side is 20, girth is 200
- LLDB 输入 `si`,可以看到 `setter` 方法内部,调用了 `willSet`、`didSet`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo6.png" style="zoom:25%">
<img src="./../assets/SwiftInoutExploreDemo6.png" style="zoom:25%">
总结:带有属性观察器的存储属性,如果调用的方法参数是 `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. 函数返回后再将副本的值覆盖实参的值setterwillSet、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:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTypePropertyDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftTypePropertyDemo1.png" style="zoom:25%">
`movq $0xa, 0x86d1(%rip) ` num1 的地址为: `rip + 0x86d1 = 0x100003b0f + 0x86d1 = 0x10000C1E0 `
@@ -414,7 +619,7 @@ Demo1:
Demo2
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTypePropertyDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftTypePropertyDemo2.png" style="zoom:25%">
`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
class Manager {
static var count = Int.random(in: 1...100)
}
Manager.count = 10
Manager.count = 11
```
下断点可以看到,调用了**地址访问器函数**。为什么需要地址访问器函数:
- 线程安全初始化首次访问时触发初始化(可能包含 `dispatch_once` 逻辑)
- 抽象内存访问隐藏实际存储位置(可能位于不同段或延迟分配)
- 支持属性观察didSet通过封装访问点插入回调逻辑
<img src="./../assets/SwiftTypeProperytyDispatchOnce1.png" style="zoom:30%" />
lldb 输入 si 查看具体实现
<img src="./../assets/SwiftTypeProperytyDispatchOnce2.png" style="zoom:30%" />
可以看到底层调用了 `swift_once` 函数函数传递了2个参数 rsi 存储 dispatch_once 的 block 参数rdi 存储了 onceToken
继续敲 si 可以看到底层调用的就是 GCD 的 `dispatch_once` 函数。
<img src="./../assets/SwiftTypeProperytyDispatchOnce3.png" style="zoom:30%" />
类型属性如何保证线程安全的?如何保证只会初始化一次
底层会调用 `swift_once` 进而调用 `dispatch_once_f``dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
所以类型存储属性底层通过 dispatch_once 保证了只会初始化1次线程安全。
类型属性如何保证线程安全的?如何保证只会初始化一次。
底层会调用 `swift_once` 进而调用 `dispatch_once_t``dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。

View File

@@ -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`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCannotOverrideStaticMethod.png" style="zoom:25%">
<img src="./../assets/SwiftCannotOverrideStaticMethod.png" style="zoom:25%">
<img src="./../assets/SwiftCannotOverrideStaticMethod2.png" style="zoom:25%">
- 如果父类的方法是被 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 无法用于协议:因其无法表达可写性,违背协议动态描述能力的初衷。
## <span id="target-anchor">多态的实现原理</span>
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()` 处加断点,可以看到
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassPointerDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftClassPointerDemo1.png" style="zoom:25%">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassPointerDemo3.png" style="zoom:25%">
<img src="./../assets/SwiftClassPointerDemo3.png" style="zoom:25%">
解释:
@@ -292,20 +310,43 @@ Animal sleep
画了张图,也就是说 `rax` 中存放了 Dog 对象内存中的前8个字节也就是下图的最右侧
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassPointerDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftClassPointerDemo2.png" style="zoom:25%">
核心是上面的内存布局图。结合汇编就知道多态是如何实现的。
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(_:)'`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCanFailedInit.png" style="zoom:25%">
<img src="./../assets/SwiftCanFailedInit.png" style="zoom:25%">
2. 可以用 `init!` 来定义隐式解包的可失败初始化器
@@ -403,7 +453,7 @@ print(num) // Optional(12)
}
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCanFailedInit2.png" style="zoom:25%">
<img src="./../assets/SwiftCanFailedInit2.png" style="zoom:25%">
且前面的写法比较危险,假设第一个 `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<Wrapped> : 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<Int> = .some(20)` 所以下面写法是一样的
```swift
// 写法1
var age1: Int? = 30
age1 = 20
age1 = nil
// 写法2
let age2: Optional<Int> = .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<Int?>.some(Optional<Int>.some(30))
var age2: Int?? = 30
var age3: Optional<Optional> = .some(.some(30))
var age4: Optional<Int?> = .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
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassMetaDataTypeDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftClassMetaDataTypeDemo1.png" style="zoom:25%">
在第二行代码下断点可以看到关键的汇编是第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` 结构类似下图右侧
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassPointerDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftClassPointerDemo2.png" style="zoom:25%">
@@ -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<Person> = 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)这里讲到的一样
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassPointerDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftClassPointerDemo2.png" style="zoom:25%">
查表是一种简单、易实现、性能可预知的方式。然而,这种派发方式比起直接派发来说,还是慢了一点(从字节码的角度来看,多了两次读和一次跳转。由此带来了性能损耗)。另一个慢的原因在于编译器可能会由于函数内执行的任务,导致无法优化(如果函数带有副作用的话)

View File

@@ -197,7 +197,7 @@ class Stack<Element>: Stackable {
## Swift 型本质
## Swift 型本质
```swift
func swapValue<T>(_ 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<T: Person & Runable>(_ a: inout T, _ b: inout T) {
}
```
另一种场景是在方法参数是某个型且遵循协议后,对其型有更多限制,则用 `where` 去处理。如下例子
另一种场景是在方法参数是某个型且遵循协议后,对其型有更多限制,则用 `where` 去处理。如下例子
```swift
protocol Stackable {

View File

@@ -1,15 +1,13 @@
# 剖析 Swift String
带着问题研究下 Swift 中的 String
带着问题研究下 Swift 中的 String
- 1个 String 变量占用多少内存?
- String 存放在什么位置?
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MemoryLayout.png)
![](./../assets//MemoryLayout.png)
@@ -21,7 +19,7 @@ var str1: String = "0123456789"
实验很简单,就一行代码。来窥探下 String 的初始化和内存结构。出发断点看到下面汇编:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo1.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo1.png" style="zoom:25%">
简单分析下:
@@ -47,7 +45,7 @@ QA这个10是什么东西1是什么东西
var str1: String = "01234"
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo2.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo2.png" style="zoom:25%">
可以看到其他和上面的没变化,唯一不同的是 `movl $0x5, %esi` 将5赋值给寄存器 `esi`,即寄存器 `rsi`。实验的字符串 "01234" UTF8 字符个数为5
@@ -57,7 +55,7 @@ var str1: String = "01234"
var str1: String = "01234😄"
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo3.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo3.png" style="zoom:25%">
可以看到将9赋值给寄存器 `esi``rsi`,字符串赋值给寄存器 `rdi``xorl %edx, %edx` 异或运算结果 0 赋值给寄存器 `edx``rdx`,也就是不纯粹为
@@ -95,7 +93,7 @@ extension String: _ExpressibleByBuiltinStringLiteral {
继续探索:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo4.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo4.png" style="zoom:25%">
可以看到 `str1` 地址为 `rip + 0x8853 = 0x1000039a5 + 0x8853 = 0x10000C1F8 `,然后读取对应堆上的内容为:
@@ -130,7 +128,7 @@ var str1: String = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1))
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo5.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo5.png" style="zoom:25%">
分析下:
@@ -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个字节
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo6.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo6.png" style="zoom:25%">
@@ -168,13 +172,13 @@ var str1: String = "0123456789ABCDEF"
字符串地址为 ` 0x10000397f + 0x3ea1 = 0x100007820`,看着像全局变量、也有可能是常量区?字符串内容写死的情况下,编译器编译后内存地址应该可以确定,那到底在什么地方呢?利用 `MachOView` 窥探下 `MachO` 文件吧
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo7.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo7.png" style="zoom:25%">
利用 MachOView 打开如下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo1.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo1.png" style="zoom:25%">
@@ -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
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo2.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo2.png" style="zoom:25%">
可以看到:
@@ -224,6 +239,52 @@ print(Mems.memStr(ofVal: &str3)) // 0xd000000000000015 0x8000000100007800
### Swift 字符串存储本质
Swift 字符串存储的两种模式:
- **内联存储Small String OptimizationSSO**
- **条件**:字符串长度 ≤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`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo3.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo3.png" style="zoom:25%">
字符串真实地址:`0x0000600001700440 + 0x20 = 0x0000600001700460`LLDB `x 0x0000600001700460 ` 可以看到字符串拼接后的结果。看上去是存储在堆上。如何验证?
@@ -270,11 +331,11 @@ print("explore")
我们知道堆上的内存初始化在 Swift 侧,关键方法为 `swift_allocObject ` -> `swift_slowAlloc` -> `malloc `。给 malloc 下断点,然后在断点出查看调用堆栈,可以看到在 `string.append()` 后有堆分配内存
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo4.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo4.png" style="zoom:25%">
结束当前函数调用,在外层可以看到 str2 地址值的后8个字节为 `0x000060000170410`,再 + `0x20` 就是字符串真实地址,打印如下。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo5.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo5.png" style="zoom:25%">
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` 数据段可读可写,所以可以修改。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOStubBinderAndLazyBinding.png" style="zoom:25%">
<img src="./../assets//MachOStubBinderAndLazyBinding.png" style="zoom:25%">
`__stub_helper` 节是 `__TEXT` 段中的一个特定节,它包含了用于处理符号懒加载的辅助函数和代码。当动态链接的符号首次被调用时,这些辅助函数会被触发,以解析符号并将其地址绑定到调用点。

View File

@@ -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 为 internalDog 为 fileprivate所以最低的是 fileprivate所以 Person 类型的访问级别为 fileprivate。因此 Person 类型变量的访问级别为 fileprivate 或者比 fileprivate 更低的访问级别。
- Swift 默认所有的变量、方法、类的默认访问级别为 internal。internal 访问级别高于 fileprivate所以报错。
看似规则比较多,实际上就是对「一个实体不可以被更低访问级别的实体定义」的解释。
```swift
internal class Car { }
fileprivate class Dog { }
public class Person<T1, T2> { }
var person: Person<Car, Dog> = Person() // compile error: Variable must be declared private or fileprivate because its type uses a fileprivate type
```
改进下: `fileprivate var person: Person<Car, Dog> = Person()` 和 `private var person: Person<Car, Dog> = 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
// 编译通过

View File

@@ -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
## 闭包的循环引用
<img src="./../assets/SwiftBlockRetainCycle.png" style="zoom:40%">
上面的代码会发生循环引用,会导致局部变量的 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` 去访问

View File

@@ -185,9 +185,10 @@ print(Mems.memStr(ofRef: p))
## 混编
### Swift 类如何在 OC 中使用
### OC 调用 Swift
OC 项目,使用 Swift 开发默认会创建 `项目名-Swift.swift` 文件。Swift 类都可以在该文件中找到
默认情况下生成的 Swift class 是不可以直接在 OC 中使用的,如果需要访问需要在 class 前加 `@objc`,编译器生成的代码如下:
默认情况下生成的 Swift class 是不可以直接在 OC 中使用的,如果需要**访问需要在 class 前加 `@objc` 且继承自 NSObject**,编译器生成的代码如下:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC1.png" style="zoom:25%">
class 不仅需要创建对象,还需要访问属性和方法,可以在属性或者方法前加 `@objc`,效果如下
@@ -196,9 +197,41 @@ class 不仅需要创建对象,还需要访问属性和方法,可以在属
但这样很麻烦,需要给每个属性、方法添加 `@objc`。有简便方法,可以直接在 class 前加 `@objcMembers`,这样该 class 所有的属性、方法都可以在 OC 中访问
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC3.png" style="zoom:25%">
**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 类。
@@ -208,3 +241,350 @@ 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(原因)`
### 符号名映射
`@_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 对象方法
<img src="./../assets/SwiftCallSwiftAssemble.png" style="zoom:30%" />
纯 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 的虚函数表的逻辑
断点截图如下:
<img src="./../assets/OCCallSwiftAssemble.png" style="zoom:30%" />
可以看到在 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. 断点查看方法调用的本质
<img src="./../assets/SwiftClassExposedToOCThenCalledBySwift.png" style="zoom:30%" />
可以看到,在 Swift 环境中,即使某个 Swift 类暴露给了 OC调用其对象方法的本质依旧是走虚函数表。因为此时用不到 Runtime 的能力
Demo4: 在 Demo3 的基础上swift 方法内部调用 OC 对象方法
也就是说:
1. Cat 类继承自 NSObject`@objcMembers` 修饰
2. 在 Swift 中调用 Cat 对象的 sayHi 方法
3. 在 sayHi 方法内部,调用 OC Person 对象的 run 方法
下断点可以看到:
<img src="./../assets/SwiftClassExposedToOCThenCalledBySwift1.png" style="zoom:30%" />
<img src="./../assets/SwiftClassExposedToOCThenCalledBySwift2.png" style="zoom:30%" />
分为2个阶段
1. 第一阶段:在 Swift 环境调用虽然暴露给 OC 的 Swift 对象方法,但因为没有和 OC 直接交互,所以走的是 Swift 虚函数表逻辑
2. 第二阶段:在 Swift 环境调用 OC 对象方法,因为底层是 OC 方法调用,所以走的是 OC Runtime 逻辑
思考:想让 Swift 方法也走 OC 的 Runtime可以利用 **`dymanic`** 关键词修饰方法。如下:
<img src="./../assets/SwiftCallSwiftMethodUseDynamic.png" style="zoom:30%" />
## 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 TypeNSMutableString 是类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 |

View File

@@ -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 >>><A, B, C>(_ 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<A, B, C>(_ 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<A, B, C, D>(_ 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<A, B, C, D>(_ 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<Element>
public func map<T>(_ transform: (Element) -> T) -> Array<T>
// Array<Element>
@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
// Optional<Wrapped>
public func map<U>(_ transform: (Wrapped) -> U) -> Optional<U>
@inlinable public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
```
跳出语言来看,符合函数式编程的语言都存在**函子** 这一概念,符合下面的形式,都可以叫函子
```swift
func map<T>(_ fn: (Element) -> T) -> Type<T>
```

View File

@@ -1,4 +1,4 @@
# Swift 面向协议编程
# Swift 面向协议编程
## 概念
@@ -125,8 +125,13 @@ extension MyCompitable {
get{ MY<Self>.self }
}
}
```
具体使用的地方
```swift
// String my
extension String: MyCompitable { }
// String.myString().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.myString().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<Int>.timer(.second(2), period: .second(1), scheduler: MainScheduler.instance)
observable.map{ "\($0)" }.bind(to: priceLabel.rx.text)
```

View File

@@ -1,4 +1,4 @@
# 响应式编程
# 响应式编程

View File

@@ -1,116 +1,166 @@
# 工程化
## 多环境配置
Project、Target、Scheme 主要管理什么?
- Project包含了项目所有的代码、资源文件所有信息
- Scheme对于指定 Target 的环境配置
- Target对于指定代码和资源文件的具体构建方式
多环境配置的3种方式
- 多 target 配置
- Scheme 多 target 进行环境配置
- xconfig 文件配置
QATarget、Scheme 的关系是什么?
- Project包含了项目所有的代码、资源文件所有信息
- Scheme对于指定 Target 的环境配置
- Target对于指定代码和资源文件的具体构建方式
## 多环境配置的不同方式
### 多 Target 的方式
针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。搭配不同的宏定义来实现控制逻辑的效果。
#### 方案
注意duplicate 之后target 虽然多了一份,但是代码和资源不变,所以
针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。 **多 TargetTargets** 是管理不同应用变体(如免费版/付费版、测试版/生产版、多客户定制版)的高效方式。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MultipleTargetProjectConfig.png" style="zoom:30%" />
所以,**为了区分不同的环境,做一些逻辑的控制。所以需要搭配不同的宏定义,来实现控制逻辑的效果。**
注意duplicate 之后target 虽然多了一份,但是代码和资源不变
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeMacroSupportedWithOCAndSwift.png" style="zoom:30%" />
#### 关键步骤
<img src="./../assets/MultipleTargetProjectConfig.png" style="zoom:30%" />
<img src="./../assets/XcodeMacroSupportedWithOCAndSwift.png" style="zoom:30%" />
##### 管理配置文件
1. **独立的 Info.plist**
- 复制原 `Info.plist` 并重命名(如 `Pro-Info.plist`
当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
<img src="./../assets/MultiplePListAfterDuplicateTarget.png" style="zoom:30%" />
- 在新 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` 指定配置文件
##### 宏定义
- OCBuild Settings -> Preprocessor Macros 里面的 Debug/Release 模式下添加自定义宏。比如在 debug 模式下 `IsOCDebug = 1`
- SwiftBuild Settings -> Other Swift Flags 里的 Debug/Release 模式下添加自宏定义。注意命名有格式要求:`-D + 宏名称`
#### 思考
该方式还是存在弊端:
当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MultiplePListAfterDuplicateTarget.png" style="zoom:30%" />
思考:该方式还是存在的问题:多个 info.plist、配置比较乱
- 工程存在多份 info.plist实际上 plist 文件很少改动,所以没有这种需求)
- 配置比较零散、比较乱
### 多 Scheme 的方式
针对一个 Target 可以添加多个 Scheme步骤如下
#### 方案
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeAddScheme.png" style="zoom:30%" />
针对多 Target 方案存在的问题,可以用**「多 Scheme + 多 Configuration 」**的方式解决。
这样的创建好之后,该 Target 存在3个 Scheme 了。有了 Scheme 有什么作用呢?设置宏定义的时候可以针对不同的 Scheme 进行设置。
#### 关键步骤
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AddMacroForDifferentScheme.png" style="zoom:30%" />
##### 创建 Configuration
针对一个 Target 可以添加多个 **Configuration**,步骤如下:
先选中 Project然后在右侧选择 Info 选项卡,在 Configurations Section ,点击 "+" ,即可创建新的 Configuration。
<img src="./../assets/XcodeAddScheme.png" style="zoom:30%" />
创建好之后,该 Target 存在3份 Configuration 了。不同的 Configuration 有什么作用呢?设置宏定义的时候可以针对不同的 Configuration 进行设置。
<img src="./../assets/AddMacroForDifferentScheme.png" style="zoom:30%" />
针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSwitchSchemeManually.png" style="zoom:30%" />
<img src="./../assets/XcodeSwitchSchemeManually.png" style="zoom:30%" />
点击 Edit Scheme在 Run 里面选择对应的 Scheme
点击 Edit Scheme在 Run 里面选择对应的 Configuration
但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Scheme。有没有什么办法解决切换问题呢。
但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Configuration。有没有什么办法解决切换问题呢。
创建实体 Scheme
##### 创建实体 Scheme
创建 Scheme 步骤Xcode -> New Scheme再弹出的方框内选择对应的 Target然后输入需要创建的 Scheme 名称。此次我们创建了Debug、Beta 2个新的 Scheme。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCreateScheme.png" style="zoom:40%" />
<img src="./../assets/XcodeCreateScheme.png" style="zoom:40%" />
创建好之后,可以看到实体 Scheme 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme然后在 Run 里面 “Build Configuration” 里面选择对应的 Scheme 与之对应。
创建好之后,可以看到实体 Configuration 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme然后在 Run 里面 “Build Configuration” 里面选择对应的 Configuration 与之对应。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSchemeMatchWithConfigScheme.png" style="zoom:40%" />
<img src="./../assets/XcodeSchemeMatchWithConfigScheme.png" style="zoom:40%" />
创建之后就可以根据 Scheme 设置值,在 `Build Settings -> User-Defined` 下添加自定义字段,同时可以根据 Scheme 设置不同的值。
##### plist 暴露自定义字段
设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用
1. 创建之后就可以根据 Configuration 设置值,在 `Build Settings -> User-Defined` 下添加自定义的字段,同时可以根据 Configuration 设置不同的值
2. 设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用。
完整如下图:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SetValueUseDifferentSchemeAndUseViaPlist.png" style="zoom:40%" />
<img src="./../assets/SetValueUseDifferentSchemeAndUseViaPlist.png" style="zoom:40%" />
切换不同的 Scheme可以运行不同的效果当前 case 下,选择 Debug Scheme输出不同结果 `HOST_URL: http://www.debug.baidu.com`
思考:目前的方案已经优雅不少,但是还是存在,自定义宏的时候需要选择不同的 Scheme过程繁琐。
#### 思考
目前的方案已经优雅不少,该方式还是存在弊端:自定义宏的时候需要选择不同的 Scheme过程繁琐
### Xcconfig
#### 方案
使用过 CocoaPods 的都会留意到工程存在 `*.Pro.xcconfig` 文件。里面是一些工程相关的配置。所以我们也可以用该方式处理工程问题。
#### 关键步骤
Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,还可以修改 Build Settings 里面的选项。
创建步骤如下:
第一:创建步骤如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCreateXCConfig.png" style="zoom:30%" />
<img src="./../assets/XcodeCreateXCConfig.png" style="zoom:30%" />
文件命名为:`文件夹名称-项目名称.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 文件。如下图
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSpecifySchemeWithConfig.png" style="zoom:30%" />
<img src="./../assets/XcodeSpecifySchemeWithConfig.png" style="zoom:30%" />
我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`Xcode 切换到 debug scheme 下,然后 Command + B 编译。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png" style="zoom:30%" />
<img src="./../assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png" style="zoom:30%" />
验证结果:
@@ -140,3 +190,191 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
说明:在 Xcode Build Settings 手动配置的信息,和通过 Xcconfig 方式编写的信息,不会冲突。
对于 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}`
<img src="./../assets/XccofigValueUseInPlist.png" style="zoom:30%" />
- 代码中使用
```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)
```
<img src="./../assets/XcodeXcconfigDemo1.png" style="zoom:30%" />
第三步:
- 当前 xcconfig 是为 Dev 模式下设置的。所以项目的 scheme 选择 `Debug` 模式。
- 选中 `PROJECT`,然后在 `Configurations` 下给 `Debug` 配置 `Dev.xcconfig` 文件。
结果:编译工程,可以看到报错了。符合预期
<img src="./../assets/XcodeXcconfigDemo2.png" style="zoom:30%" />
原因:本 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"`
<img src="./../assets/XcconfigGeneratedByCocoapods.png" style="zoom:30%" />
第三步:创建 `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 <AFNetworking/AFNetworking.h>`,然后创建对象并验证证
````objective-c
AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];
NSLog(@"%@", policy);
````
<img src="./../assets/XcconfigInheritedTest.png" style="zoom:30%" />
说明:
- Cocoapods install 后,自动创建链接器所需参数。都在 `Pods-项目名.debug.xcconfig` 配置文件中
- 我们可以自己创建的 `*.xcconfig` 是可以引入自动生成的配置文件的。并在此基础上可以修改。然后在 Xcode Project Configuration 里可以指定为新创建的 xcconfig 文件
- 并且是可以生效的

View File

@@ -114,3 +114,22 @@
- 关闭Xcode在终端中键入以下命令sudo chmod 1777 /tmp
- 清理此路径中的dyld文件夹/Library/Developer/CoreSimulator/Caches
- 重新启动Xcode完成
14. Xcode 自动设置 __nonnull.
升级到 Xcode 10 , 新建类的时候发现头文件中多了2个宏
NS_ASSUME_NONNULL_BEGIN
NS_ASSUME_NONNULL_END
作用
这两个东西是Nonnull区域设置(Audited Regions) 。
这两个宏之间的代码里的所有简单指针对象都被默认为 ___nonnull我们只需要去指定 __nullable 的指针。
2014 年的 Apple WWDC 发布了强语言 swift 必须要指定一个对象是否为空。为了迎合swiftOC中增加了 __nullable 和 ___nonnull 用于指定对象是否为空。
每个属性、方法都指定 ___nonnull 和 __nullable 是一件非常繁琐的事。为了减轻开发工作量苹果提供了两个宏NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 。这两个宏之间的代码里的所有简单指针对象都被默认为 ___nonnull我们只需要去指定 __nullable 的指针。
解决的问题
- 减少冗余代码:避免在每个属性、方法参数或返回值前手动添加 nonnull提高代码简洁性。
- 提升类型安全性:编译器会对默认的 nonnull 指针进行静态检查,传递 nil 时会触发警告。
- 改善 Swift 互操作性Swift 能识别这些注解,将 nonnull 指针转换为非可选类型(如 String将 nullable 指针转换为可选类型(如 String?),使接口更清晰。

View File

@@ -35,7 +35,7 @@
先附上一张总结的非常棒的RunLoop图
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-RunLoop-review.png" style="zoom:30%" />
<img src="./../assets/2019-05-09-RunLoop-review.png" style="zoom:30%" />
@@ -138,7 +138,7 @@ struct __CFRunLoopMode {
Demo:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" />
<img src="./../assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" />
@@ -170,24 +170,24 @@ RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 So
Source0:
- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源
- `performSelector:onThread:`
- 数组
Demo给屏幕点击事件加断点查看堆栈可以看到是 Source0 触发的。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TouchActionByRunLoopSource0.png" style="zoom:25%" />
- 处理开发者主动提交的任务或应用内部逻辑
- `performSelector:onThread:`
- `dispatch_async`到主线程的任务最终封装为Source0
- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源。UIKit控件的事件处理如按钮点击后的回调
Demo1给屏幕点击事件加断点查看堆栈可以看到是 Source0 触发的。
<img src="./../assets/TouchActionByRunLoopSource0.png" style="zoom:25%" />
Demo2: `- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait` 该 API 的底层就是:向目标线程的 RunLoop 添加一个 Source0 事件,标记后唤醒线程执行对应的事件。
<img src="./../assets/PerformThreadTaskByRunLoopSource0.png" style="zoom:25%" />
Source1:
- 基于 Port 的线程间通信,可以主动唤醒 RunLoop
- 系统事件捕捉比如屏幕触摸事件Source1捕捉后派发给 Source0 处理)
- 用户触摸屏幕时,系统通过 Mach Port 将事件传递到应用主线程的 RunLoop。RunLoop 被 Source1 唤醒后,将事件分发给 Source0处理具体的 UI 响应逻辑(如 `hitTest:withEvent:` 和响应链)。
- 字典。`{machport : 1}`
Timers
@@ -221,7 +221,7 @@ CFRunLoopSourceRef 事件源(输入源)
### 一对多的关系
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ThreadRunLoopModeStructure.png" style="zoom:70%" />
<img src="./../assets/ThreadRunLoopModeStructure.png" style="zoom:70%" />
@@ -434,7 +434,7 @@ CFRelease(obersver);
*/
```
![触摸屏幕事件在 RunLoop 下的 source0](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180801-104553@2x.png)
![触摸屏幕事件在 RunLoop 下的 source0](./../assets/WX20180801-104553@2x.png)
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
@@ -510,7 +510,7 @@ CFRelease(obersver);
### 运行原概要
![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/image-20180801113342611.png)
![RunLoop 运行原理图1](./../assets/image-20180801113342611.png)
- 图上左上角的 Input source 是早期 RunLoop 的分法现在分法为Source0 和 Source1。
- Source0:非基于 port 的,用户主动触发的事件。
@@ -527,7 +527,7 @@ CFRelease(obersver);
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-Specific.png)
![](./../assets/RunLoop-Specific.png)
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
@@ -1137,7 +1137,7 @@ RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopSer
上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-SourceCode.png" style="zoom:30%" />
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:40%" />
Demo
@@ -1151,7 +1151,7 @@ Demo
2. 上面8>2Runloop 处理 GCD Async To Main Quque
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" />
<img src="./../assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" />
@@ -1165,7 +1165,7 @@ Demo
可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopSleepSystemCall.png" style="zoom:30%" />
<img src="./../assets/RunLoopSleepSystemCall.png" style="zoom:30%" />
@@ -1887,10 +1887,72 @@ main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop可以替换 API比如设置一个变量标记是否需要结束 RunLoop
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-RunIssue.png" style="zoom:45%" />
<img src="./../assets/RunLoop-RunIssue.png" style="zoom:45%" />
改进代码如下
```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<UITouch *> *)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 改为 YESdealloc 也就没执行完毕,为什么 weak 指针指向的 weakself 就为 nil 了?
`weak` 指针的特性是:**当对象开始销毁时(即 `dealloc` 被调用时),所有指向它的 `weak` 指针会立即被置为 `nil`**。这一行为发生在 `dealloc` 方法执行之前,而不是之后。
继续优化版本:
```objective-c
__weak ViewController *weakself = self;
self.thread = [[LifeThread alloc] initWithBlock:^{
@@ -1933,14 +1995,12 @@ self.thread = [[LifeThread alloc] initWithBlock:^{
效果如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopThreadKeepLive.png" style="zoom:30%" />
<img src="./../assets/RunLoopThreadKeepLive.png" style="zoom:30%" />
注意: 
- 如果 `stop`  方法内部的 `waitUntilDone` 为 NO则会出现 Crash。因为该参数代表后续代表会不会等该 selector 执行完毕。因为为 NO所以 ViewController 执行 dealloc 了,所以 `dealloc` 方法和 Thread 内部的 block 同时进行,不能确保在 block 内部执行的时候 dealloc 有没有执行完,访问 weakSelf.isStoped 可能会 crash
- 线程的 RunLoop 结束了,线程也无法执行任务了,所以需要给线程对象设置为 nil。同时任务派发的地方也需要判断线程是否存在否则会 crash
- NSRunLoop 常驻线程可以运行在 NSRunLoopCommonModes 下吗?

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,571 +0,0 @@
# NSTimer、CSDisplayLink 中的内存泄露
## CADisplayLink 内存泄漏
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CSDisplayLinkMemoryLeak.png" style="zoom:30%" />
可以看到 CADisplayLink 和 VCVC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
## NSTimer 内存泄漏
### 对比实验
NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
Demo 如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoeryLeakDemo.png" style="zoom:30%" />
但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoeryNotLeakWhenRepeatNO.png" style="zoom:30%" />
### 源码分析
查看 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
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerFixMemoryLeakIssueByBlockAPI.png" style="zoom:30%" />
该种方式,控制器 self强引用 timertimer 强引用 blockblock 弱引用 self3者没有形成环。
### 采用系统 NSProxy 代替自定义的中间类
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoryLeakFixedByNSProxy.png" style="zoom:30%" />
注意:继承自 NSProxy 的类,不能 init。
QA自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?看上去反而是自定义的 NSObject 使用更简单呀?
答:**NSProxy 效率更高**。NSProxy 的主要作用是为消息转发提供一个通用的接口,是一个继承自 NSObject 的对象,虽然看上去 API 更简单,写法简单,但内部运行的时候还是基于 isa 去查找类对象、元类对象的 cache 中查找,找不到再去 class_rw_t 中查找,找不到再从 superclass 找父类的类对象、元类对象...流程,最后还是找不到,则走 runtime 的动态方法解析、消息转发阶段。
看一段神奇的代码
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSProxyAndNSObjectMethodImpl.png" style="zoom:30%" />
为什么打印出 `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 任务繁重的时候,定时器可能不准。**
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-SourceCode.png" style="zoom:30%" />
假设一个 NSTimer 被加到 RunLoop 开头NSTimer 执行周期为1sRunLoop 前面任务繁重,第一次走完一个完整的 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 会更准确?因为普通定时器运行依赖 RunLoopRunLoop 一个运行周期内的任务繁忙程度是不确定的。当某次任务繁重,那么定时器调度就不准时。
GCD timer 不依赖 RunLoop系统底层驱动所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
### 打破循环引用NSTimer target 自定义
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoryLeakFixedByProxy.png" style="zoom:30%" />
### 高精度定时器封装
项目中经常使用定时器,普通定时器存在精度丢失的问题、循环引用的问题,为了使用方法我们封装一个定时器
```objectivec
#import <Foundation/Foundation.h>
@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<UITouch *> *)touches withEvent:(UIEvent *)event
{
[PreciousTimer cancelTask:self.timerId];
}
```
说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ignoreXcodewarning.png)
### 采用 Block 的形式为 NSTimer 增加分类
```objectivec
//.h文件
#import <Foundation/Foundation.h>
@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)

File diff suppressed because it is too large Load Diff

View File

@@ -1177,13 +1177,13 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
过程如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-categoryattachLists.png)
![](./../assets/runtime-categoryattachLists.png)
结果就是类方法列表中,最前面的就是所有分类的方法列表,最后是类自身的方法列表。
效果为 `[[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
}
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryMethodOrderExplore.png" style="zoom:25%">
<img src="./../assets/OCCategoryMethodOrderExplore.png" style="zoom:25%">
可以看到 sayHi 方法存在多个,但是由于 Category 同名的方法在方法列表的前面,所以类自身的方法实现”被覆盖了“(根据 isa 查找方法实现的时候,优先查找到 Category 的方法实现,则停止查找了)
@@ -1291,17 +1347,17 @@ Demo: 为 Person 类创建2个 Category分别存在同名方法 study
Demo: 为 Person 类创建2个 Category分别存在同名方法 study具有不同实现。探索编译顺序决定方法实现
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo1.png" style="zoom:25%">
<img src="./../assets/OCCategoryBuildOrderDemo1.png" style="zoom:25%">
2个对比实验
让 `Person+Study` 参与后编译
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo2.png" style="zoom:25%">
<img src="./../assets/OCCategoryBuildOrderDemo2.png" style="zoom:25%">
让 `Person+Learn` 参与后编译
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo3.png" style="zoom:25%">
<img src="./../assets/OCCategoryBuildOrderDemo3.png" style="zoom:25%">
@@ -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,8 +2270,6 @@ 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 方法打印顺序是这样的?
因为调用 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 是类方法)如下图:
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-categoryattachLists.png)
![](./../assets/runtime-categoryattachLists.png)
### 为什么给子类发消息,父类和子类的 +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
梳理后,如下图所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedSaveValueInRuntimeStructure.png" style="zoom:45%">
<img src="./../assets/AssociatedSaveValueInRuntimeStructure.png" style="zoom:45%">
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
### 声明私有方法
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CategoryUsageDeclPrivateMethod.png" style="zoom:30%" />
<img src="./../assets/CategoryUsageDeclPrivateMethod.png" style="zoom:30%" />

View File

@@ -1,10 +1,12 @@
# MVC、MVP、MVVM
## MVC
## MVC 架构
MVC 模式下软件被划分为视图View用户界面、控制器Controller业务逻辑、模型Model数据保存
![MVC架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-MVC.png)
![MVC架构](./../assets/2018-11-16-MVC.png)
1. 用户操作 View在 View 上面的事件都将被传递到 Controller 处理
2. Controller 处理事件、请求网络,操作 Model 更新状态
@@ -14,44 +16,667 @@ MVC 模式下软件被划分为视图View用户界面、控制器
### Apple MVC 架构
## MVP
效果等价于:
<img src="./../assets/AppleMVCImpl.png" style="zoom:60%" />
最典型的就是 iOS 侧的 UITableView 的设计:
- Controller 感知 ViewUIViewController 通过 View 的形式持有 UITableView
- View 事件代理到 Controller而 View 上的 UI 事件自身不处理,通过代理的形式交给 UIViewController 处理
- Controller 感知 ModelUIViewController 负责从网络或者数据库中拉取数据,持有 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 架构变种
<img src="./../assets/AppleMVCRefine.png" style="zoom:60%" />
改变:
- 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<GoodsDisplayable>` 协议,在仅有一个 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 <GoodsDisplayable>
@end
// GoodsCell.h
@interface GoodsCell
@property (nonatomic, strong) id<GoodsDisplayable> 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通信改变了通信方向
![MVP架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-MVP.png)
![MVP架构](./../assets/MVPArchStructure.png)
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() <PersonalInfoViewDelegate>
@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 <UIKit/UIKit.h>
@class PersonalInfoView;
@protocol PersonalInfoViewDelegate <NSObject>
@optional
- (void)personalInfoViewDidClick:(PersonalInfoView *)personalInfoView;
@end
@interface PersonalInfoView : UIView
- (void)setName:(NSString *)name andImage:(NSString *)image;
@property (weak, nonatomic) id<PersonalInfoViewDelegate> 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<UITouch *> *)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 <NSObject>
- (void)displayName:(NSString *)name image:(NSString *)image;
@end
```
View 实现协议
```objective-c
// PersonalInfoView.h
@interface PersonalInfoView : UIView <PersonalInfoViewProtocol>
@property (nonatomic, weak) id<PersonalInfoViewDelegate> 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 <NSObject>
- (void)fetchPersonalInfoWithCompletion:(void (^)(PersonalInfoModel *model))completion;
@end
```
定义数据拉取 Services
```objective-c
// PersonalInfoService.h
@interface PersonalInfoService : NSObject <PersonalInfoServiceProtocol>
@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<PersonalInfoViewProtocol>)view
service:(id<PersonalInfoServiceProtocol>)service;
- (void)loadData;
@end
// PersonInfoPresenter.m
@interface PersonInfoPresenter()
// 重要地方:遵循相应协议的 View 对象
@property (nonatomic, weak) id<PersonalInfoViewProtocol> view;
// 重要地方:遵循相应协议的 Services 对象
@property (nonatomic, strong) id<PersonalInfoServiceProtocol> service;
@end
@implementation PersonInfoPresenter
- (instancetype)initWithView:(id<PersonalInfoViewProtocol>)view
service:(id<PersonalInfoServiceProtocol>)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 () <PersonalInfoViewDelegate>
@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<PersonalInfoServiceProtocol> 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 模式完全一致。
![MVVM架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-MVVM.png)
![MVVM架构](./../assets/MVVMArchStructure.png)
区别在于采用双向绑定的模式。View 的改变,自动反应在 ViewModel 上。 ViewModel 的改变会发应在 View 上。
MVC 等到业务逻辑很复杂的时候被称为 Massive View Controller (重量级视图控制器)。这样的 Controller 后期维护看着阅读成本很高、不易于测试、维护。当时写这个代码的人离职后,几千行规模的代码在后期添加新功能或者修改 Bug 都是一件折磨人的事情。
![典型MVC架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-iOSMVC.png)
![典型MVC架构](./../assets/2018-11-16-iOSMVC.png)
看到 View 和 ViewController 是不同的技术组件,但是日常开发中它们总是成对的存在,为什么不考虑将他们的连接正规化呢?
看到 View 和 ViewController 是不同的技术组件,但是日常开发中它们总是成对的存在,为什么不考虑将他们的连接正规化呢?
![存在问题](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-VController-Model.png)
![存在问题](./../assets/2018-11-16-VController-Model.png)
典型的 MVC 存在弊端就是 Controller 层非常复杂很多逻辑都在里面包括一些不是逻辑的“表示逻辑”presentation logic。用 MVVM 术语来说就是将那些 Model 数据转换为 View 可以呈现的东西。例如将一个 NSDate 格式化为一个“2018-11-17” 这样的 NSString。
上图中缺少一个环节,一个专门用来处理所有的表示逻辑。称为 “ViewModel”。位于 ViewController 与 Model 之间。
![MVVM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-iOSmvvm.png)
![MVVM](./../assets/2018-11-16-iOSmvvm.png)
MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel。iOS 使用 MVVM 可以降低 ViewController 的复杂性并使得表示逻辑易于测试。
@@ -62,6 +687,169 @@ MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel
### **MVVM 核心改造点**
1. **引入 ViewModel**:负责数据转换、业务逻辑,通过数据流驱动 UI 更新
2. **数据双向绑定**:使用 ReactiveCocoaRAC建立 View 和 ViewModel 的绑定关系
3. **事件命令化**:用户交互事件封装为 Command由 ViewModel 处理
改造如下:
定义 ViewModel核心
```objc
// PersonalInfoViewModel.h
#import <ReactiveObjC/ReactiveObjC.h>
@class PersonalInfoModel;
@interface PersonalInfoViewModel : NSObject
// 输出属性(供 View 绑定)
@property (nonatomic, strong, readonly) RACSignal<NSString *> *nameSignal;
@property (nonatomic, strong, readonly) RACSignal<UIImage *> *imageSignal;
// 输入命令(处理 View 事件)
@property (nonatomic, strong, readonly) RACCommand *viewDidClickCommand;
// 初始化方法(依赖注入)
- (instancetype)initWithService:(id<PersonalInfoServiceProtocol>)service;
@end
// PersonalInfoViewModel.m
@interface PersonalInfoViewModel()
@property (nonatomic, strong) id<PersonalInfoServiceProtocol> service;
@property (nonatomic, strong) RACSubject *nameSubject;
@property (nonatomic, strong) RACSubject *imageSubject;
@end
@implementation PersonalInfoViewModel
- (instancetype)initWithService:(id<PersonalInfoServiceProtocol>)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 <ReactiveObjC/ReactiveObjC.h>
@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<PersonalInfoServiceProtocol> 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 在客户端的简易实现」。

View File

@@ -10,7 +10,7 @@ App 的包大小做优化的目的就是为了节省用户流量,提高用户
App 瘦身一般指的是安装包IPA主要由可执行文件、资源组成。
App 瘦身一般指的是安装包IPA主要由**可执行文件、资源组成**
对于产物的分析,可以查看可执行文件的具体组成。
@@ -32,7 +32,7 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术
### 1.1 Slicing
![Slicing](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppSlicing.jpeg)
![Slicing](./../assets/2018-11-15-AppSlicing.jpeg)
当向 App Store Connect 上传 .ipa 后App Store Connect 构建过程中,会自动分割该 App创建特定的变体variant以适配不同设备。然后用户从 App Store 中下载到的安装包,即这个特定的变体,这一过程叫做 Slicing。
@@ -42,7 +42,7 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术
其中2x 和 3x 的细分,要求图片在 **Assets** 中管理。Bundle 内的则会同时包含。
![变体](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppVariant.jpeg)
![变体](./../assets/2018-11-15-AppVariant.jpeg)
### 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 来进行符号化
![App Connect-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppConnectYSM.jpeg)
![App Connect-dYSM](./../assets/2018-11-15-AppConnectYSM.jpeg)
![Xcode-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-XcodedYSM.jpeg)
![Xcode-dYSM](./../assets/2018-11-15-XcodedYSM.jpeg)
那么 Bitcode 会对 App Thining 有什么作用?
@@ -83,7 +83,7 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
![on-DemandResources](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-on-DemandResources.png)
![on-DemandResources](./../assets/2018-11-15-on-DemandResources.png)
应用场景:相机应用的贴纸或者滤镜、关卡游戏等
@@ -101,7 +101,7 @@ on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,
包体积,评判标准是以 App Store 上看到的为准。但是上传到 App Store Connect 处理完后,会自动帮我们生成具体设备上看到的大小。如下:
![App Store 包大小](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppVolume.jpeg)
![App Store 包大小](./../assets/2018-11-15-AppVolume.jpeg)
这其中又可以分为2类 Universal 和具体设备
Universal 指通用设备,即未应用 App slicing 优化,同时包含了所有架构、资源。所以包体积会比较大
@@ -245,13 +245,13 @@ self.imageView.image = images.lastObject;
</details>
Timeprofile-imageNamedFromAssets
![Timeprofile-imageNamedFromAssets](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Timeprofile-imageNamedFromAssets.png)
![Timeprofile-imageNamedFromAssets](./../assets/2019-05-07-Timeprofile-imageNamedFromAssets.png)
TimeProfile-imageWithContentsOfFile
![TimeProfile-imageWithContentsOfFile](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-TimeProfile-imageWithContentsOfFile.png)
![TimeProfile-imageWithContentsOfFile](./../assets/2019-05-07-TimeProfile-imageWithContentsOfFile.png)
Timeprofile-UIImageNamedFromFolder
![Timeprofile-UIImageNamedFromFolder](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png)
![Timeprofile-UIImageNamedFromFolder](./../assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png)
Images.xcassets
@@ -276,7 +276,7 @@ CocoPods 中两种资源引用方式介绍下:
说说项目中的情况吧:在工程中之前是通过 resource_bundles 引用资源的。资源是放在 Resources 目录下的图片引用。查询资料后说「如果图片资源放到 .xcasset 里面 Xcode 会帮我们自动优化、可以使用 Slicing 等(这里不仅仅指的是 resource_bundle 下的 xcassets」。所以动手将各个 Pod 库里面的图片全都通过 Assets Catalog 的方式进行处理。
![Pod组件库图片处理前后对比](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-08-Cocopod-Assets.png)
![Pod组件库图片处理前后对比](./../assets/2019-05-08-Cocopod-Assets.png)
步骤:
@@ -433,14 +433,51 @@ Symbols Hidden by Default 会把所有符号都定义成”private extern”
在 iOS微信安装包瘦身 一文中,有提到:
> 去掉异常支持Enable C++ ExceptionsEnable 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++ ExceptionsEnable 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。
![LinkMap结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Structure.png)
<img src="./../assets/2019-05-06-LinkMap-Structure.png" style="zoom:50%"/>
- 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 文件了。
![Xcode中设置获取LinkMap](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Xcode.png)
![c](./../assets/2019-05-06-LinkMap-Xcode.png)
产出的 LinkMap 阅读起来比较累github 有个[可视化项目](https://github.com/jayden320/LinkMap) 用来查看 LinkMap 文件。
#### 3.2.1 基于 clang 扫描
@@ -499,11 +541,11 @@ LinkMap 文件分为3部分Object File、Section、Symbols。
上面我们得知可以通过 LinkMap 统计出所有的类和方法,还可以清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
![LinkMap-Object file](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-ObjectFile.png)
![LinkMap-Object file](./../assets/2019-05-05-LinkMap-ObjectFile.png)
![LinkMap-Sections](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Sections.png)
![LinkMap-Sections](./../assets/2019-05-05-LinkMap-Sections.png)
![LinkMap-Symbols](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Symbols.png)
![LinkMap-Symbols](./../assets/2019-05-05-LinkMap-Symbols.png)
得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
@@ -520,7 +562,7 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
前置条件:先运行项目,在生成的 Products 目录下的 BridgeLabiPhone.app 解压,取出对应的和工程同名的 BridgeLabiPhone。然后运行上面的 Github 项目。可以看到运行了一个 Mac App。点击顶部的菜单栏里面的 File->Open。选择电脑上的 BridgeLabiPhone.app 选择里面的 BridgeLabiPhone。见下图
![Mach-O-inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Mach-O-Inspect.png)
![Mach-O-inspect](./../assets/2019-05-07-Mach-O-Inspect.png)
由于 Objective-C 是一门动态语言所以检测出的结果仍旧需要我们2次确认。
@@ -534,7 +576,7 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。
![AppCode-code inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-CodeClean.png)
![AppCode-code inspect](./../assets/2019-05-05-CodeClean.png)
说明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 中。
最后的一个对比效果图:
![瘦身效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-AppThinning-Comparation.png)
![瘦身效果图](./../assets/2019-05-09-AppThinning-Comparation.png)
总结瘦身技术常见操作就这些但是维持应用包体积的瘦身却是一个观念从日常开发到线上发布都需要有这个意识。这样当你在写代码的时候就会考虑同样一个效果你的具体实现手段是怎么样的。比如为了一个稍微炫酷的效果就要引入一个很大的三方库有了“瘦身”的意识你很大可能就是自己动手撸一个代码。比如一些无用资源的管理方式、有用的图片资源的高效管理方式等等。有了意识行动自然会往这个方面去靠。😂大道理一套一套的。我也不想的毕竟是playboy

View File

@@ -12,7 +12,7 @@ BSS段bss segment通常用来存储程序中未被初始化的全局变
代码段code segment通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读某些架构也允许代码段为可写即允许修改程序。在代码段中也有可能包含一些只读的常数变量例如字符串常量。
![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png)
![内存](./../assets/ram.png)
@@ -58,7 +58,7 @@ struct NSObject_IMPL {
因此可以知道OC 的类底层是由 c/c++ 的继承实现的。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
<img src="./../assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
由于 obj 对象没有任何属性和方法,只有一个 isa 指针且类的本质就是结构体所以当结构体只有1个成员时该成员的地址值就是该结构体的地址。
@@ -118,7 +118,7 @@ struct Student_IMPL {
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassLayout.png" style="zoom:45%">
<img src="./../assets/StudentClassLayout.png" style="zoom:45%">
@@ -126,7 +126,7 @@ struct Student_IMPL {
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StructPointerVistorClassIvars.png" style="zoom:25%">
<img src="./../assets/StructPointerVistorClassIvars.png" style="zoom:25%">
发现是可以正确访问的。
@@ -193,7 +193,7 @@ struct Student_IMPL {
为什么 `class_getInstanceSize([Person class])` 也是16不是8+4吗因为存在内存对齐结构体的大小必须是最大成员大小的倍数Person 中也就是8的倍数
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
<img src="./../assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
@@ -330,12 +330,12 @@ int main(int argc, const char * argv[]) {
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
![解析图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Untitled%20Diagram-2.png)
![解析图](./../assets/Untitled%20Diagram-2.png)
**结论**
![p1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-15%20下午5.35.17.png)
![p2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-15%20下午5.35.34.png)
![p1](./../assets/2017-05-15%20下午5.35.17.png)
![p2](./../assets/2017-05-15%20下午5.35.34.png)
**可以 看到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)` 16Apple 规定对象至少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 <objc/runtime.h>
class_getInstanceSize([Person class])
sizeOf()
```
创建一个实例对象,实际上分配了多少内存?
```objective-c
#import <malloc/malloc.h>
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。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
<img src="./../assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
@@ -718,21 +779,21 @@ GUN 都存在内存对齐这个概念。
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-isa.png)
![](./../assets/objc-isa.png)
instance 的 isa 指向 Class。当调用方法时通过 instance 的 isa 找到 Class最后找到对象方法的实现进行调用
class 的 isa 指向 meta-class。当调用类方法的时通过 class 的 isa 找到 meta-class最后找到类方法进行调用。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-superclass.png)
![](./../assets/objc-superclass.png)
当 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` 方法并调用。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-metaclass-superclass.png)
![](./../assets/objc-metaclass-superclass.png)
当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/class-isa-superclass.png)
<img src="./../assets/class-isa-superclass.png" style="zoom: 40%" />
```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 <Foundation/Foundation.h>
@@ -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++ 混编。
<img src='./../assets/XcodeCompileErrorOnMockObjcClass.png' style="zoom:40%" />
## 六、 内存对齐
@@ -937,7 +1006,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data();
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
1. 提高访问速度内存对齐可以使数据在内存中的存储更加高效因为大部分计算机体系结构都要求数据按照特定的边界对齐这样可以减少内存访问的次数提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MemoryAlignReason.png" style="zoom:30%">
<img src="./../assets/MemoryAlignReason.png" style="zoom:30%">
2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问只能访问对齐后的内存地址否则会报异常。
很多 CPU如基于 AlphaIA-64MIPS和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARMMIPS和 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 中查看
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GlibcInXcodeProject.png" style="zoom:25%">
<img src="./../assets/GlibcInXcodeProject.png" style="zoom:25%">
可以看到 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.
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GLibcMallocAlignment.png" style="zoom:25%">
<img src="./../assets/GLibcMallocAlignment.png" style="zoom:25%">
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 对象在内存中存储的信息主要包括:
@@ -1193,3 +1298,50 @@ Class objc_getClass(const char *aClassName)
- Class 对象属性信息、对象方法信息、成员变量信息、协议信息、superclass、isa 存放在类对象中。
- Meta-Class 对象:类方法信息,存放在元类对象中。
## 总结
<img src="./../assets/class-isa-superclass.png" style="zoom: 40%" />
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<NSCopying> {
int _no;
}
@property (nonatomic, assign) int score;
- (void)study;
+ (void)live;
@end
```
- 对象方法、属性、成员变量、协议信息存储在类对象class 对象)中
比如 `-(void)study` 方法、score 属性、`_no` 成员变量,`NSCopying` 协议
- 类方法存储在元类对象meta-class 对象)中
比如 `+(void)live` 方法
- 成员变量的具体值,存放在实例对象的内存中。比如 student1 的 score 为30student2 的 score 为 90

View File

@@ -22,11 +22,11 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 1. 屏幕绘制原理
![老式 CRT 显示器原理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_screen_scan.png)
![老式 CRT 显示器原理](./../assets/2020-02-04-ios_screen_scan.png)
讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式从上到下一行行扫描扫面完成后显示器就呈现一帧画面随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步显示器或者其他硬件会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行准备进行扫描时显示器会发出一个水平同步信号horizonal synchronization简称 HSync当一帧画面绘制完成后电子枪恢复到原位准备画下一帧前显示器会发出一个垂直同步信号Vertical synchronization简称 VSync。显示器通常以固定的频率进行刷新这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
![显示器和 CPU、GPU 关系](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-02-screen_display_gpu.png)
![显示器和 CPU、GPU 关系](./../assets/2020-02-02-screen_display_gpu.png)
通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要显实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPUGPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
@@ -36,7 +36,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
为了解决这个问题GPU 通常有一个机制叫垂直同步信号V-Sync当开启垂直同步信号后GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源
![IPC唤醒 RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-08-ios_vsync_runloop.png)
![IPC唤醒 RunLoop](./../assets/2020-02-08-ios_vsync_runloop.png)
答疑
@@ -48,7 +48,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
揭秘。请看下图
![多缓冲区显示原理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-Comparison_double_triple_buffering.png)
![多缓冲区显示原理](./../assets/2020-02-04-Comparison_double_triple_buffering.png)
当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
@@ -56,7 +56,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 2. 卡顿产生的原因
![卡顿原因](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_frame_drop.png)
![卡顿原因](./../assets/2020-02-04-ios_frame_drop.png)
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 AppApp 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPUGPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
@@ -75,7 +75,7 @@ RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、
RunLoop 状态如下图
![RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4.png)
![RunLoop](./../assets/4.png)
第一步:通知 ObserversRunLoop 要开始进入 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 状态](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-05-RunLoop.png)
![RunLoop 状态](./../assets/2020-02-05-RunLoop.png)
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-ANR](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-APM-RunLoopANR.jpg)
![RunLoop-ANR](./../assets/2020-04-04-APM-RunLoopANR.jpg)
可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
Runloop 检测卡顿流程图如下:
![RunLoop ANR](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-ANRRunloop.png)
![RunLoop ANR](./../assets/2020-04-04-ANRRunloop.png)
关键代码如下:
@@ -371,7 +371,7 @@ while (self.isCancelled == NO) {
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
维基百科搜索到 “Call Stack” 的一张图和例子,如下
![函数调用栈](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-08-StackFrame.png)
![函数调用栈](./../assets/2020-02-08-StackFrame.png)
上图表示为一个栈。分为若干个栈帧Frame每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B调用函数 B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。
@@ -464,11 +464,11 @@ static mach_port_t main_thread_id;
这里有个策略,卡顿是由于主线程某个或某组方法执行过长而造成的卡顿,所以卡顿堆栈只需要分析主线程即可,但是此时是未符号化的方法(完成流程如下)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/callStackSymbolicate.png" style="zoom:30%" />
<img src="./../assets/callStackSymbolicate.png" style="zoom:30%" />
测试过单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/callstackCostTime.png)
![](./../assets/callstackCostTime.png)
按照栈顶函数地址和当前堆栈的深度作为判断依据,如果某个堆栈栈顶地址和深度相同,则出现次数最多的一个堆栈,可以认为是一个卡顿堆栈,因为认为当发生卡顿的时候,函数堆栈大概率不会变化。
@@ -505,7 +505,7 @@ static mach_port_t main_thread_id;
上传这些信息到服务端后APM 后端调用符号化服务的能力,符号化机器找到对应的 DSYM 文件,调用 atos 的指令进行符号化,如下效果。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LagStackSymbolicate.png)
![](./../assets/LagStackSymbolicate.png)
系统堆栈符号化的问题,也就是获取系统符号文件的问题。可以通过 ipsw也就是刷机所需要的固件信息。
@@ -517,7 +517,7 @@ static mach_port_t main_thread_id;
服务端聚合策略
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CallStackGroupHash.png)
![](./../assets/CallStackGroupHash.png)
找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 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 启动时间](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-03-30-APMAppLaunch.png)
![App 启动时间](./../assets/2020-03-30-APMAppLaunch.png)
冷启动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 阶段
![Pre-Main 阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-10-AppSpeed-PreMain.png)
![Pre-Main 阶段](./../assets/2020-02-10-AppSpeed-PreMain.png)
Main 阶段
![Main 阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-10-AppSpeed-Main.png)
![Main 阶段](./../assets/2020-02-10-AppSpeed-Main.png)
#### 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. 精确版启动时间监控
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AppStarupPipeline.png)
![](./../assets/AppStarupPipeline.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APMStartup.png)
![](./../assets/APMStartup.png)
进程创建:通过 sysctl 可以拿到
@@ -685,7 +772,7 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CATransactionCommit.png)
![](./../assets/CATransactionCommit.png)
对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree通过 IPC 发送给 Render Server。
@@ -697,21 +784,21 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
- Layout 布局:调用 `layout` 等与布局相关的 API
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline1.png)
![](./../assets/CoreAnimationPipeline1.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline2.png)
![](./../assets/CoreAnimationPipeline2.png)
- Display 绘制:调用 `drawRect` 等与绘制相关方法
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline3.png)
![](./../assets/CoreAnimationPipeline3.png)
- Prepare图片解码
- Commit递归打包 Render Tree通过 IPC 计数发送给 Render Server
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline4.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline5.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline6.png)
![](./../assets/CoreAnimationPipeline4.png)
![](./../assets/CoreAnimationPipeline5.png)
![](./../assets/CoreAnimationPipeline6.png)
断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。
@@ -919,14 +1006,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使
Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。
![内存page种类](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryType.png)
![内存page种类](./../assets/2020-02-28-iOSMemoryType.png)
- Clean Memory
Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling那么变为 dirty
一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件也是只读的、clean page。
![Clean memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeClean.png)
![Clean memory](./../assets/2020-02-28-iOSMemoryTypeClean.png)
- Dirty Memory
@@ -934,7 +1021,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
在使用 framework 的过程中会产生 Dirty memory使用单例或者全局初始化方法有助于帮助减少 Dirty memory因为单例一旦创建就不销毁一直在内存中系统不认为是 Dirty memory
![Dirty memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeDirty.png)
![Dirty memory](./../assets/2020-02-28-iOSMemoryTypeDirty.png)
- 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`。
![Memory footprint](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryFootprint.png)
![Memory footprint](./../assets/2020-02-28-iOSMemoryFootprint.png)
接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。
@@ -1740,7 +1827,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
FacekBook 提出排除法监控 OOM。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Facebook-OOM.jpeg)
![](./../assets/Facebook-OOM.jpeg)
- App 更新了版本
@@ -2145,7 +2232,7 @@ App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试
#### 野指针可能存在的问题
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WildPointerCategory.png" style="zoom:30%" />
<img src="./../assets/WildPointerCategory.png" style="zoom:30%" />
### 2. Zombie Object
@@ -2192,11 +2279,11 @@ self:_NSZombie_Person - superClass:nil
利用 Instruments 下的 Zombies 工具检测,看看野指针的情况,系统是怎么捕获的。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/InstrumentZombiesCaptureZombieObject.png" style="zoom:30%" />
<img src="./../assets/InstrumentZombiesCaptureZombieObject.png" style="zoom:30%" />
切换到 `Call Trees` 模式下可以看到系调用了 `_dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeZombieDetect.png" style="zoom:30%" />
<img src="./../assets/XcodeZombieDetect.png" style="zoom:30%" />
通过符号名称大概可以猜系统会在调用 dealloc 方法时,类似 KVO派生出一个新的僵尸类将当前类的 isa 指针指向僵尸类。
@@ -2903,7 +2990,7 @@ bool init_safe_free(void)
注意ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC加 `-fno-objc-arc`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ZombieObjectsDetector.png" style="zoom:30%" />
<img src="./../assets/ZombieObjectsDetector.png" style="zoom:30%" />
### 6. 方案对比
@@ -2918,7 +3005,7 @@ bool init_safe_free(void)
### 1. App 网络请求过程
![网络请求各阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-03-NetworkTime.png)
![网络请求各阶段](./../assets/2020-04-03-NetworkTime.png)
App 发送一次网络请求一般会经历下面几个关键步骤:
@@ -2956,7 +3043,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
iOS 网络框架层级关系如下:
![Network Level](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-05-NetworkLevel.png)
![Network Level](./../assets/2020-04-05-NetworkLevel.png)
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对于网络监控需要做如下的处理
![network hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-network_monitor.png)
![network hook](./../assets/2020-04-04-network_monitor.png)
可能对于 CFNetwork 比较陌生,可以看一下 CFNetwork 的层级和简单用法
![CFNetwork Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-CFNetworkStructure.png)
![CFNetwork Structure](./../assets/2020-04-04-CFNetworkStructure.png)
CFNetwork 的基础是 CFSocket 和 CFStream。
@@ -3645,9 +3732,9 @@ void printResponseData (CFDataRef responseData) {
NSURLSession、NSURLConnection hook 如下。
![NSURLSession Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLSessionHook.jpeg)
![NSURLSession Hook](./../assets//2020-04-13-NSURLSessionHook.jpeg)
![NSURLConnection Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLConnectionHook.jpeg)
![NSURLConnection Hook](./../assets//2020-04-13-NSURLConnectionHook.jpeg)
业界有 APM 针对 CFNetwork 的方案,整理描述下:
@@ -3922,7 +4009,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
};
```
![method swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-09-methodSwizzling.png)
![method swizzling](./../assets/2020-04-09-methodSwizzling.png)
method swizzling 改进版如下
@@ -3949,7 +4036,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
typedef struct objc_object *id;
```
![isa swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-13-isaSwizzling.png)
![isa swizzling](./../assets/2020-04-13-isaSwizzling.png)
我们来分析一下为什么修改 `isa` 可以实现目的呢?
@@ -4107,11 +4194,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
HTTP 请求报文结构
![请求报文结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-16-HTTPRequestStructure.png)
![请求报文结构](./../assets/2020-05-16-HTTPRequestStructure.png)
响应报文的结构
![响应报文结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-16-HTTPResponseStructure.png)
![响应报文结构](./../assets/2020-05-16-HTTPResponseStructure.png)
1. HTTP 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。
2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符)
@@ -4138,11 +4225,11 @@ HTTP 请求报文结构
下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。
![请求数据结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-13-HTTPDataStructure.png)
![请求数据结构](./../assets/2020-05-13-HTTPDataStructure.png)
下图是在终端使用 `curl` 查看一个完整的请求和响应数据
![curl查看HTTP响应](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-14-HTTPRequestStructure.png)
![curl查看HTTP响应](./../assets/2020-05-14-HTTPRequestStructure.png)
我们都知道在 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;
}
```
可以看到主要逻辑是先从磁盘中读取图片,如果配置允许开启内存缓存,则将图片保存到 NSCache 中,使用的时候也是从 NSCache 中读取图片。NSCache 的 `totalCostLimit、countLimit` 属性,
`- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;` 方法用来设置缓存条件。所以我们写磁盘、内存的文件操作时可以借鉴该策略,以优化耗电量
除了 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秒`),减少数据处理的能耗
## 八、 Crash 监控
@@ -4704,7 +4875,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。
![Mach 异常处理以及转换为 Unix 信号的流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-19-BSDCatchSignal.png)
![Mach 异常处理以及转换为 Unix 信号的流程](./../assets/2020-05-19-BSDCatchSignal.png)
### 2. Crash 收集方式
@@ -4768,7 +4939,7 @@ KSCrash 功能齐全,可以捕获如下类型的 Crash
流程图如下:
![KSCrash流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-KSCrashStructure.png)
![KSCrash流程图](./../assets/2020-05-20-KSCrashStructure.png)
对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
@@ -5079,7 +5250,7 @@ static void restoreExceptionPorts(void)
KSCrash 在这里的处理逻辑如下图:
![signal 处理步骤](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-signalCrash.png)
![signal 处理步骤](./../assets/2020-05-20-signalCrash.png)
看一下关键代码:
@@ -5441,9 +5612,9 @@ static void setEnabled(bool isEnabled)
阅读下源码,看看为什么 `NSUncaughtExceptionHandler` 可以收集 crash 信息。查看 objc 源码
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcInitExceptionHandlerSet.png" style="zoom:30%" />
<img src="./../assets/ObjcInitExceptionHandlerSet.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcExceptionHandlerExplore.png" style="zoom:30%" />
<img src="./../assets/ObjcExceptionHandlerExplore.png" style="zoom:30%" />
发现入口函数 `_objc_init` 中调用可设置异常处理的 `exception_init` 方法。内部通过底层 API `std::set_terminate(&_objc_terminate)` 来设置异常处理的 handler。
@@ -5545,7 +5716,7 @@ static void setEnabled(bool isEnabled)
其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。
![caller](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-03-KSCrashCaller.png)
![caller](./../assets/2020-06-03-KSCrashCaller.png)
```c
/** Start general exception processing.
@@ -6046,7 +6217,7 @@ static int64_t getReportIDFromFilename(const char* filename)
}
```
![KSCrash 存储 Crash 数据位置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-31-KSCrashStoreCrashData.png)
![KSCrash 存储 Crash 数据位置](./../assets/2020-05-31-KSCrashStoreCrashData.png)
#### 2.7 前端 js 相关的 Crash 的监控
@@ -6070,7 +6241,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
};
```
![h5 异常监控](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-19-JSErrorCatch.png)
![h5 异常监控](./../assets/2020-06-19-JSErrorCatch.png)
##### 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 代码了。
![React Native Crash Monitor](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-19-ReactNativeCrashMonitor.png)
![React Native Crash Monitor](./../assets/2020-06-19-ReactNativeCrashMonitor.png)
查看到 crash stack 后点击可以跳转到 sourceMap 的地方。
@@ -6117,7 +6288,7 @@ TipsRN 项目打 Release 包
现象iOS 项目奔溃。截图以及日志如下
![RN crash](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-22-RNUncaughtCrash.png)
![RN crash](./../assets/2020-06-22-RNUncaughtCrash.png)
```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。
![RN release log](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-23-RNReleaseLog.png)
![RN release log](./../assets/2020-06-23-RNReleaseLog.png)
结论:
@@ -6592,7 +6763,7 @@ parseJSError(line, column);
5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
![RN Log analysis](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-23-RNCrashLogAnalysis.png)
![RN Log analysis](./../assets/2020-06-23-RNCrashLogAnalysis.png)
##### 2.7.5 SourceMap 解析系统设计
@@ -6884,7 +7055,7 @@ parseJSError(line, column);
`.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下:
![.DSYM文件结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-29-DYSMStructure.png)
![.DSYM文件结构](./../assets/2020-05-29-DYSMStructure.png)
#### 4.2 DWARF 文件
@@ -7416,7 +7587,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/
```
![系统符号化文件](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-28-SymbolicateLib.png)
![系统符号化文件](./../assets/2020-05-28-SymbolicateLib.png)
### 5. 服务端处理
@@ -7426,7 +7597,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。
![ELK架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-14-ELK.png)
![ELK架构图](./../assets/2020-06-14-ELK.png)
上图展示了一个 ELK 的日志架构图。简单说明下:
@@ -7436,13 +7607,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。
![Elasticsearch & APM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-ElastiicsearchAPM.png)
![Elasticsearch & APM](./../assets/2020-06-17-ElastiicsearchAPM.png)
##### 5.2 服务侧
Crash log 统一入库 Kibana 时是没有符号化的所以需要符号化处理以方便定位问题、crash 产生报表和后续处理。
![crash log 处理流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-14-CrashLogSymbolicate.png)
![crash log 处理流程](./../assets/2020-06-14-CrashLogSymbolicate.png)
所以整个流程就是:客户端 APM SDK 收集 crash log -> Kafka 存储 -> Mac 机执行定时任务符号化 -> 数据回传 Kafka -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
@@ -7454,7 +7625,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下
![crash 符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMServerArch.png)
![crash 符号化流程图](./../assets/2020-06-17-APMServerArch.png)
说明:
@@ -7466,19 +7637,19 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
![符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-symolication_flow.png)
![符号化流程图](./../assets/2020-06-17-symolication_flow.png)
其中符号化服务是大前端背景下大前端团队的产物,所以是 NodeJS 实现的单线程所以为了提高机器利用率就要开启多进程能力。iOS 的符号化机器是 双核的 Mac mini这就需要做实验测评到底需要开启几个 worker 进程做符号化服务。结果是双进程处理 crash log比单进程效率高近一倍而四进程比双进程效率提升不明显符合双核 mac mini 的特点。所以开启两个 worker 进程做符号化处理。
下图是完整设计图
![符号化技术设计图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMServerWorker.png)
![符号化技术设计图](./../assets/2020-06-17-APMServerWorker.png)
简单说明下,符号化流程是一个主从模式,一台 master 机,多个 slave 机master 机读取 .DSYM 和 crash 结果的 cache。「数据处理和任务调度框架」调度符号化服务内部 2 个 symbolocate worker同时从七牛云上获取 .DSYM 文件。
系统架构图如下
![符号化服务架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-SymbolicateServerArch.png)
![符号化服务架构图](./../assets/2020-06-17-SymbolicateServerArch.png)
## 九、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 问题
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png)
![](./../assets/WeexResourcePull.png)
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能比如某个收银员角色固定的情况下他日常的操作行为是固定的商品扫码、开单
@@ -7928,17 +8099,17 @@ runZoned<Future<Null>>(() async {
可能有些人一直没有遇到过因为在子线程操作 UI导致在开发阶段 Xcode console 输出了一堆日志,大体如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcode1@2x.png)
![](./../assets/2022-0204-SubThreadUIXcode1@2x.png)
本来常见的开发都会规避这些写法,没机会看到子线程操作 UI 的问题,但是 Weex 的业务代码,检测出存在子线程操作 UI 的问题,所以还是有必要增加这个能力的。
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` type 选择 `Main Thread Checker` 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png)
![](./../assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png)
效果如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png)
![](./../assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png)
### 2. 问题及解决方案
@@ -7952,7 +8123,7 @@ runZoned<Future<Null>>(() async {
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIMonitor@2x.PNG)
![](./../assets/2022-0204-SubThreadUIMonitor@2x.PNG)
另外该功能可以在线下 debug 阶段开启,判断是否是在 Xcode debug 状态,可以通过苹果提供的[官方判断方法](https://developer.apple.com/library/archive/qa/qa1361/_index.html#//apple_ref/doc/uid/DTS10003368)实现。
@@ -7966,7 +8137,7 @@ runZoned<Future<Null>>(() async {
好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PageLoadFullTime.png)
![](./../assets/PageLoadFullTime.png)
页面作为承载用户交互的具体战场我们需要对页面的性能有个直观的指标。业界一般有2个指标**页面渲染时长、页面可交互时长**。
@@ -8078,7 +8249,7 @@ runZoned<Future<Null>>(() async {
6. 整个 APM 的架构图如下
![APM Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMStructure.jpg)
![APM Structure](./../assets/2020-06-17-APMStructure.jpg)
说明:

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,7 @@
- 当某个类继承自 NSObject 的时候如果没有其他属性则这个类占据16个字节。`class_getInstanceSize` 占据 8 `malloc_size` 占据 16
- 当某个类继承自 NSObject 的时候如果有其他属性则这个类占据16个字节。`class_getInstanceSize` 占据 16 `malloc_size` 占据 16
方法二: 从源代码角度出发验证(从上下)
方法二: 从源代码角度出发验证(从上下)
```c++
// NSObject.mm

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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)
@@ -117,3 +117,4 @@
* [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)

View File

@@ -37,13 +37,13 @@ render () {
## 生命周期
![React生命周期](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-17-ReactLifecycle.PNG)
![React生命周期](./../assets/2019-06-17-ReactLifecycle.PNG)
## 状态管理
Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍了。解决了各个组件之间数据传递的复杂问题。先看看 Redux 进行状态管理的一个流程吧。
![Redux-数据流动](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-26-Redux-Structures.png)
![Redux-数据流动](./../assets/2019-06-26-Redux-Structures.png)
### 开发步骤
@@ -109,7 +109,7 @@ Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍
Redux-thunk 是 redux 里面常用的一个中间件。中间件?针对谁和谁的中间?对 action 和 store 的中间件。本来 action 只可以返回一个对象,灵活性较低,但是采用了 redux-thunk 之后action 不仅可以传递对象,还可以传递函数。 action 通过 dispatch 传递给 store。 dispatch 判断 action 的类型,如果是对象则直接传递;如果是函数则直接执行。
![不使用redux-thunk时action返回函数报错](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-24-reduxThunk.png)
![不使用redux-thunk时action返回函数报错](./../assets/2019-06-24-reduxThunk.png)
- 异步函数不应该放在组件的生命周期函数里面。复杂的业务逻辑和异步函数适合拆分。目前主流的解决方案有2种中间件redux-thunk、redux-saga。采用不同的策略

View File

@@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/AppleMVCImpl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/AppleMVCRefine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

BIN
assets/HarmonyHookStep1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
assets/LLVM-Debug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
assets/LLVM-Debug1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
assets/LLVM-Debug2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

BIN
assets/LLVM-Debug3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
assets/LLVM-Debug4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

BIN
assets/LLVM-Debug5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

BIN
assets/LLVM-Debug6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

BIN
assets/MVPArchStructure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 915 KiB

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Some files were not shown because too many files have changed in this diff Show More