diff --git a/.DS_Store b/.DS_Store index 4e3941c..35f6121 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index b512c09..ea479ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -node_modules \ No newline at end of file +node_modules +.DS_Store +prompt.txt +unused_assets.py +/t diff --git a/Chapter1 - iOS/1.1.md b/Chapter1 - iOS/1.1.md deleted file mode 100644 index 7f09fe0..0000000 --- a/Chapter1 - iOS/1.1.md +++ /dev/null @@ -1,91 +0,0 @@ -# 工程大小优化之图片资源 - -> 一点点iOS项目本身功能较多,导致应用体积也比较大。一个Xcode工程下图片资源占用了很大的空间,且如果有些App需要一键换肤功能,呵呵,不知道得做多少图片。每套图片还需要设置1x@,2x@,3x@等 - -## 简介 - -IconFont技术起源于Web领域的Web Font技术。随着时间的推移,网页设计越来越漂亮。但是电脑预装的字体远远无法满足设计者的要求,于是Web Font技术诞生了。一个英文字库并不大,通过网络下载字体,完成网页的显示。有了Web Font技术,大大提升了设计师的发挥空间。 - -网页设计中图标需要适配多个分辨率,每个图标需要占用一次网络请求。于是有人想到了用Web Font的方法来解决这两个问题,就是IconFont技术。将矢量的图标做成字体,一次网络请求就够了,可以保真缩放。解决这个问题的另一个方式是图片拼合的Sprite图。 - -Web领域使用IconFont类似的技术已经多年,当我在15年接触BootStrap的时候Font Awesome技术大行其道。最近IconFont技术在iOS图片资源方面得以应用,最近有点时间自己研究整理了一番,在此记录学习点滴。 - -## 优点 - -* 减小体积,字体文件比图片要小 -* 图标保真缩放,解决2x/3x乃至将来的nx图问题 -* 方便更改颜色大小,图片复用 - -## 缺点 - -* 只适用于 - `纯色icon` -* 使用unicode字符难以理解 -* 需要维护字体库 - -网上说了一大堆如何制作IconFont的方法,在此不做讨论。 - -## 我们说说怎么用 - -1. 首先选取一些有丰富资源的网站,我使用阿里的IconFont多年,其他的没去研究,所以此处直接使用阿里的产品。地址:[http://www.iconfont.cn/plus](http://www.iconfont.cn/plus) - -2. 打开网站在线挑选好合适的图标加入购物车,如图 - - -1. 选择好之后在购物车查看,然后点击下载代码 - -2. 打开下载好的文件,其机构如下,我们在iOS项目开发过程中使用unicode的形式使用IconFont,所以打开demo\_unicode.html - - - -**注意:** 创建 UIFont 使用的是字体名,而不是文件名;文本值为 8 位的 Unicode 字符,我们可以打开 demo.html 查找每个图标所对应的 HTML 实体 Unicode 码,比如: "店" 对应的 HTML 实体 Unicode 码为:0x3439 转换后为:\U00003439 就是将 0x 替换为 \U 中间用 0 填补满长度为 8 个字符 - -# Xcode中使用IconFont - -初步尝试使用 - -1. 首先看看如何简单实用IconFont -2. 首先将下载好的文件夹中的 **iconfont.ttf** 加入到Xcode工程中,确保加入成功在Build检查 - - - -1. 怎么用? - -```Objective-c - -NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:@"\U0000e696 \U0000e6ab \U0000e6ac \U0000e6ae"]; -[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 1)]; -[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor orangeColor] range:NSMakeRange(3, 1)]; -[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:NSMakeRange(9, 1)]; -self.label.attributedText = attributedStr; -[self.view addSubview:self.label]; - -pragma mark - getter and setter --(UILabel *)label{ - if (!_label) { - _label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, BoundWidth-200, 40)]; - _label.font = [UIFont fontWithName:@"iconfont" size:24]; - _label.textColor = [UIColor purpleColor]; - } - return _label; - } -``` - -#### 做进一步封装,实用更加方便 - -利用IconFont生成1个UIImage只需要 LBPIconFontmake(par1, par2, par3),par1:iconfont的unicode值;par2:图片大小;par3:图片的颜色值。其中,LBPIconFontmake是一个宏,#define LBPIconFontmake(text,size,color) [[LBPFontInfo alloc] initWithText:text withSize:size andColor:color]。 - -```Objective-c -self.latestImageView.image = [UIImage iconWithInfo:LBPIconFontmake(@"\U0000e6ac", 60, @"000066") ]; -``` - - -1. LBPFontInfo来封装字体信息 -2. UIColor+picker根据十六进制字符串来设置颜色 -3. LBPIconFont向系统中注册IconFont字体库,并使用 -4. UIImage+LBPIconFont封装一个使用IconFont的Image分类 - - -# [Demo地址](https://github.com/FantasticLBP/IconFont_Demo) - - diff --git a/Chapter1 - iOS/1.10.md b/Chapter1 - iOS/1.10.md deleted file mode 100644 index 4779544..0000000 --- a/Chapter1 - iOS/1.10.md +++ /dev/null @@ -1,206 +0,0 @@ -# UIWebView加载网页内容 - -可以通过本地文件、url等方式。 - -```objective-c -NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"]; -NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:htmlPath]]; -[self.webView loadRequest:request]; -``` - -## Native调用JavaScript - -Native调用JS是通过UIWebView的stringByEvaluatingJavaScriptFromString 方法实现的,该方法返回js脚本的执行结果。 - -```objective-c -[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"]; -``` - -实际上就是调用了网页的Window下的一个对象。如果我们需要让native端调用js方法,那么这个js方法必须在window下可以访问到。 - - -## JavaScript调用Native - -反过来,JavaScript调用Native,并没有现成的API可以调用,而是间接地通过一些其它手段来实现。UIWebView有个代理方法:在UIWebView内发起的任何网络请求都可以通过delegate函数在Native层得到通知。由此思路,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:**jsbridge://methodName?param1=value1¶m2=value2...** - -在UIWebView的delegate函数中,我们判断请求的scheme,如果request.URL.scheme是jsbridge,那么就不进行网页内容的加载,而是去执行相应的方法。方法名称就是request.URL.host。参数可以通过request.URL.query得到。 - -问题来了?? - -发起这样1个网络请求有2种方式。1:location.href .2:iframe。通过location.href有个问题,就是如果js多次调用原生的方法也就是location.href的值多次变化,Native端只能接受到最后一次请求,前面的请求会被忽略掉。 - -使用ifrmae方式,以调用Native端的方法。 - -```javascript -var iFrame; -iFrame = document.createElement("iframe"); -iFrame.style.height = "1px"; -iFrame.style.width = "1px"; -iFrame.style.display = "none"; -iFrame.src = url; -document.body.appendChild(iFrame); -setTimeout(function(){ - iFrame.remove(); -},100); -``` - -举个🌰: - -需求: - -原生端提供一个UIWebView,加载一个网页内容。还有1个按钮,按钮点击一下网页增加一段段落文本。网页上有2个输入框,用户输入数字,点击按钮,js将用户输入的参数告诉native端,native去执行加法,计算完成后将结果返回给js - -```html - -
- - - - - - + - - - - -``` - -```objective-c --(void)addContentToWebView{ - NSString *jsString = @" var pNode = document.createElement(\"p\"); pNode.innerText = \"我是由原生代码调用js后将一段文件添加到html上,也就是注入\";document.body.appendChild(pNode);"; - [self.webView stringByEvaluatingJavaScriptFromString:jsString]; -} - - --(NSInteger)plusparm:(NSInteger)par1 parm2:(NSInteger)par2{ - return par1 + par2; -} - - -#pragma mark -- UIWebViewDelegate -- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ - NSURL *url = request.URL; - NSString *scheme = url.scheme; - NSString *method = url.host; - NSString *parms = url.query; - NSArray *pars = [parms componentsSeparatedByString:@"&"]; - NSInteger par1 = [[pars[0] substringFromIndex:5] integerValue]; - NSInteger par2 = [[pars[1] substringFromIndex:5] integerValue]; - if ([scheme isEqualToString:@"jsbridge"]) { - //发现scheme是JSBridge,那么就是自定义的URLscheme,不去加载网页内容而拦截去处理事件。 - - if ([method isEqualToString:@"plus"]) { - NSInteger result = [self plusparm:par1 parm2:par2]; - [self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"receiveValue(%@);",@(result)]]; - } - - return NO; - } - return YES; -} -``` - - -## Android 端如何与 JS 通信(2种方法) - -- webview.loadUrl() -- Webview.evaluateJavascript() - -> 2者区别: -> -> 1. loadUrl() 会刷新页面,evaluateJavascript() 则不会刷新页面,效率高 -> 2. loadUrl() 得不到 JS 的返回值;evaluateJavascrip() 则可以获取返回值 -> 3. evaluateJavascrip() 在 Android 4.4 之后才可以使用 - -注意:Android 可以直接调用 JS 的 alert() 方法是因为 alert 方法直接挂载在 window 对象上。但是 Native 与 JS 可能不止一个方法、多个方法多个属性去访问,这样都直接挂载在 window 对象上不是明智之举。因为后期维护很不方便。所以我们在 Native 和 JS 之间会设置一个桥接对象,像一个中间层一样,让2端互调。 - -Android 需要在页面加载完,也就是 webview 的 onPageFinished 方法中写调用逻辑,否则不会执行 - -```java -webView.loadUrl("javascript:callJsFunction('soloname')") -webView.evaluateJavascript("javascript:callJsFunction('soloname')" -``` - - -### JS 如何与 Android 通信 - -- 通过 Webview 的 addJavascriptInterface() 进行对象映射 -- 通过 WebviewClient 的 shouldOverrideUrlLoading() 方法回调拦截 Url -- 通过 webChromeClient 的 onJsAlert()、onJSPrompt() 方法回调拦截 JS 对话框 alert()、confirm()、prompt() 等消息 - -第一种最简洁,但是在 Android 4.2 以下存在漏洞。 - -实验:Android webview 上跑一个网页,点击网页的按钮,让 Native 弹出一个字符串。 - -```vue -methods: { - showAndroidToast() { - $App.showToast("哈哈,我是js调用的") - } -} -``` - -``` -public class JsJavaBridge { - - private Activity activity; - private WebView webView; - - public JsJavaBridge(Activity activity, WebView webView) { - this.activity = activity; - this.webView = webView; - } - - @JavascriptInterface - public void onFinishActivity() { - activity.finish(); - } - - @JavascriptInterface - public void showToast(String msg) { - ToastUtils.show(msg); - } -} - -``` - -然后通过 webview 设置 Android 类与 JS 代码的映射 - -``` -webView.addJavascriptInterface(new JsJavaBridge(this, tbsWebView), "$App"); -``` - -这里将类 JsJavaBridge 在 JS 中映射为了 $App,所以在 Vue 中可以这样调用 `$App.showToast("哈哈,我是js调用的")`。 - - - -## 同步和异步问题 - -js调用native是通过在一个网页上插入一个iframe,这个iframe插入完了就完了,执行的结果需要native另外调用stringByEvaluatingJavaScriptString 方法通知js。这明显是1个异步的调用。而stringByEvaluatingJavaScriptString方法会返回执行js脚本的结果。本质上是一个同步调用 - -所以js call native是异步,native call js是同步。 diff --git a/Chapter1 - iOS/1.100.md b/Chapter1 - iOS/1.100.md deleted file mode 100644 index 5fbc2e8..0000000 --- a/Chapter1 - iOS/1.100.md +++ /dev/null @@ -1,217 +0,0 @@ -# iOS 端底层网络错误 - -> 本篇文章主要记录在 iOS 侧,一些底层网络问题的产生和解决。包括一些 socket 的疑难杂症 - -## 典型案例 - -### 1. Socket 断开后会收到 SIGPIPE 类型的信号,如果不处理会 crash - -同事问了我一个问题,说收到一个 crash 信息,去 mpaas 平台看到如下的 crash 信息 - - - -看了代码,显示在某某文件的313行代码,代码如下 - - - -Socket 属于网络最底层的实现,一般我们开发不需要用到,但是用到了就需要小心翼翼,比如 Hook 网络层、长链接等。查看官方文档会说看到一些说明。 - -当使用 socket 进行网络连接时,如果连接中断,在默认情况下, 进程会收到一个 `SIGPIPE` 信号。如果你没有处理这个信号,app 会 crash。 - -Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异常机制之上构建了信号处理机制。硬件产生的信号被 Mach 层捕捉,然后转换为对应的 UNIX 信号,为了维护一个统一的机制,操作系统和用户产生的信号首先被转换为 Mach 异常,然后再转换为信号。 - -Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。 - - - -有2种解决办法: - -- Ignore the signal globally with the following line of code.(在全局范围内忽略这个信号 。缺点是所有的 `SIGPIPE` 信号都将被忽略) - - ```objective-c - signal(SIGPIPE, SIG_IGN); - ``` - -- Tell the socket not to send the signal in the first place with the following lines of code (substituting the variable containing your socket in place of `sock`)(告诉 socket 不要发送信号:SO_NOSIGPIPE) - - ```c++ - int value = 1; - setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value)); - ``` - -`SO_NOSIGPIPE` 是一个宏定义,跳过去看一下实现 - -```c++ -#define SO_NOSIGPIPE 0x1022 /* APPLE: No SIGPIPE on EPIPE */ -``` - -什么意思呢?没有 SIGPIPE 信号在 EPIPE。那啥是 `EPIPE`。 - -其中:**EPIPE** 是 socket send 函数可能返回的错误码之一。如果发送数据的话会在 Client 端触发 RST(指Client端的 FIN_WAIT_2 状态超时后连接已经销毁的情况),导致send操作返回 `EPIPE`(errno 32)错误,并触发 `SIGPIPE` 信号(默认行为是 **Terminate**)。 - -> What happens if the client ignores the error return from readline and writes more data to the server? This can happen, for example, if the client needs to perform two writes to the server before reading anything back, with the first write eliciting the RST. -> -> The rule that applies is: When a process writes to a socket that has received an RST, the SIGPIPE signal is sent to the process. The default action of this signal is to terminate the process, so the process must catch the signal to avoid being involuntarily terminated. -> -> If the process either catches the signal and returns from the signal handler, or ignores the signal, the write operation returns EPIPE. - -UNP(unix network program) 建议应用根据需要处理 `SIGPIPE`信号,至少不要用系统缺省的处理方式处理这个信号,系统缺省的处理方式是退出进程,这样你的应用就很难查处处理进程为什么退出。对 UNP 感兴趣的可以查看:http://www.unpbook.com/unpv13e.tar.gz。 - -下面是2个苹果官方文档,描述了 socket 和 SIGPIPE 信号,以及最佳实践: - -[Avoiding Common Networking Mistakes](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/CommonPitfalls/CommonPitfalls.html) - -[Using Sockets and Socket Streams](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/UsingSocketsandSocketStreams.html) - -但是线上的代码还是存在 Crash。查了下代码,发现奔溃堆栈在 PingFoundation 中的 `sendPingWithData`。也就是虽然在 AppDelegate 中设置忽略了 SIGPIPE 信号,但是还是会在某些函数下「重置」掉。 - -``` -- (void)sendPingWithData:(NSData *)data { - int err; - NSData * payload; - NSData * packet; - ssize_t bytesSent; - id
-
-
-
-## Clang
-
-[Clang](http://clang.llvm.org/) 是 LLVM 的一个子项目,基于 LLVM 架构的 c/c++/Objective-C 语言的编译器前端
-
-GCC 是 c/c++ 等的编译器
-
-Clang 相较于 GCC,具备下面优点:
-
-- 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
-
-- 占用内存小:Clang 生成的 AST 所占用的内存是 GCC 的五分之一左右
-
-- 模块化设计:Clang 采用基于库的模块化设计,易于 IDE 集成及其他用途的重用
-
-- 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
-
-- 设计清晰简单,容易理解,易于扩展增强
-
-
-
-
-
-## 各个编译阶段
-
-Demo
-
-```c++
-#import
-
-
-
-可以看到经历了:**输入、预处理、编译、LLVM Backend、汇编、链接、绑定架构**7个阶段。
-
-
-
-### 预处理
-
-查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import)、宏定义替换等。展示如下:
-
-
-
-
-
-### 词法分析
-
-词法分析阶段,主要生成 Token。使用指令 `clang -fmodules -E -Xclang -dump-tokens main.m` 查看具体做了什么
-
-
-
-
-
-### 语法分析
-
-语法分析阶段,生成语法树(AST,Abstract Syntax Tree)。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看
-
-
-
-对 main.m 的代码进行改造
-
-```
-#import
-
-其中:
-
-- `FunctionDecl` 节点下存在2个 `ParamVarDecl` 和1个 `CompoundStmt` 也就是2个参数和1个函数体
-- 函数体 `CompoundStmt` 内部存在一个变量声明 `VarDecl`
-- `-`是一个操作符。
-- 红色框框内的是第一层树形结构。操作符 `-` 有2个参数。首先是最下面的字面量 `IntegerLiteral` 4。另一个就是蓝色框内的运算结果
-- 蓝色框内操作符 `+` 也有2个 `DeclRefExpr`
-
-也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。
-
-
-
-
-
-### LLVM IR
-
-IR 作为中间语言具有语言无关的特性,下面是 IR 中与语言无关的类型信息:
-
-- 语言共有的基础类型(void、bool、signed 等)
-- 复杂类型,pointer、array、structure、function
-- 弱类型的支持,用 cast 来实现一种类型到另一种任意类型的转换
-- 支持地址运算,getelmentptr 指令用于获取结构体子元素,比如 a.b 或 [a b]
-
-LLVM IR 有3种表示格式:
-
-- text:便于阅读的文本格式,类似于汇编语言,推展名为 `.ll`。使用指令 `clang -S -emit-llvm main.m` 进行转换
-
-
-
- 学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp`
-
- 一些读 IR 的 tips:
-
- - 注释以分号 `;` 开头
- - 全局变量以 `@` 开头
- - 局部变量以 `%` 开头
- - `alloca` 在当前函数栈帧中分配内存,为当前执行的函数分配内存,当该函数执行完毕时自动释放内存
- - `i32`,表示整数占几位,例如 i32 就代表 32 bit,4个字节的意思
- - `align` 内存对齐。比如单个 int 占4字节,为了对齐,只占1字节的 char 要对齐,就需要占用 4 字节
- - `store` ,写入数据
- - `load` ,读取数据
- - `icmp`,2个整数值比较,返回布尔值
- - `br`,选择分支,根据条件跳转到对应的 label
- - `label`,代码标签
-
- 更多的可以参考[官方文档](https://llvm.org/docs/LangRef.html)
-
-- memory 格式:内存格式
-
-- bitcode:二进制格式,拓展名为 `.bc`.使用指令 `clang -c -emit-llvm main.m` 进行转换。
-
-
-
-## 调试 LLVM
-选择 Edit Scheme.
-
-
-
-
-
-
-最后就可以加断点进行 Debug 了。但为了让调试更有意义,类似 `nm -a /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LDExploreDemo-ehvvtxafpkdkubgrswvvsudzhqbb/Build/Products/Debug-iphonesimulator/LDExploreDemo.app/LDExploreDemo` 一样可以查看到更有意义的信息,可以在 Edit Scheme 面板中 `Run -> Arguments -> Arguments Passed On Launch` section 中的 **+** 点击,添加一些参数,如下图:
-
-
-
-最后允许测试。注意:LLVM 项目较大,可以选择顶部 "Product -> Perform Action -> Run Without Building".
-
-
-## 用途
-
-LLVM 的一些插件,比如 libclang、libTooling,可以查看官方文档:https://clang.llvm.org/docs/Tooling.html,可以做一些**语法树解**
-
-**析、语言转换**等工作。
-
-应用场景分为3大类:
-
-- Clang 插件开发,可以参考官方文档:
-
- - https://clang.llvm.org/docs/ClangPlugins.html
-
- - https://clang.llvm.org/docs/RAVFrontendAction.html
-
- - https://clang.llvm.org/docs/ExternalClangExamples.html
-
- 应用场景是:代码检查(命名规范、代码规范)等。
-
-- Pass 开发,可以参考官方文档:
-
- - https://llvm.org/docs/WritingAnLLVMPass.html
-
- 应用场景是:代码优化、代码混淆、精准测试等
-
-- [libclang](https://clang.llvm.org/doxygen/group__CINDEX.html)、[Clang plugins](https://clang.llvm.org/docs/ClangPlugins.html)、[libTooling](https://clang.llvm.org/docs/LibTooling.html) 做语法树分析,实现语言转换 OC 转 Swift、JS 等其它语言;字符串加密;开发新的语言,例如 Swift 语言。可以参考博客:
-
- - https://kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh-cn/latest/
- - https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
-
-
-
- 其中:
-
- libclang 供了一个相对较小的 API,它将用于解析源代码的工具暴露给抽象语法树(AST),加载已经解析的 AST,遍历 AST,将物理源位置与 AST 内的元素相关联。
-
- libclang 是一个稳定的高级 C 语言接口,隔离了编译器底层的复杂设计,拥有更强的 Clang 版本兼容性,以及更好的多语言支持能力,对于大多数分析 AST 的场景来说,libclang 是一个很好入手的选择。
-
- ##### 优点
-
- 1. 可以使用 C++ 之外的语言与 Clang 交互。
- 2. 稳定的交互接口和向后兼容。
- 3. 强大的高级抽象,比如用光标迭代 AST,并且不用学习 Clang AST 的所有细节。
-
- ##### 缺点:不能完全控制 Clang AST。
-
-
-
- Clang Plugin 允许你在编译过程中对 AST 执行其他操作。Clang Plugin 是动态库,由编译器在运行时加载,并且它们很容易集成到构建环境中。
-
-
-
- LibTooling 是一个独立的库,它允许使用者很方便地搭建属于你自己的编译器前端工具,它的优点与缺点一样明显,它基于 C++ 接口,读起来晦涩难懂,但是提供给使用者远比 libclang 强大全面的 AST 解析和控制能力,同时由于它与 Clang 的内核过于接近导致它的版本兼容能力比 libclang 差得多,Clang 的变动很容易影响到 LibTooling。libTooling 还提供了完整的参数解析方案,可以很方便的构建一个独立的命令行工具。这是 libclang 所不具备的能力。一般来说,如果你只需要语法分析或者做代码补全这类功能,libclang 将是你避免掉坑的最佳的选择。
-
-
-
-### 编写 Xcode 插件
-
-比如检查类名的合法性,Xcode 默认认为类名带有下划线或者小写开头的类名是合法的。但是这个不符合团队代码规范,使用 LLVM 就可以编写 Xcode 插件,来检查类名的合法性。
-
-判断类名是否合法,这肯定是编译前端做的事情。搞清楚这点,就好办了
-
-接下来就一步步实现该功能。
-
-
-
-#### 下载
-
-创建文件夹 `llvm_explore` ,shell 进入到文件夹执行指令 `git clone https://github.com/llvm/llvm-project.git`
-
-
-
-#### 编译
-
-用 brew 安装 cmake 和 ninja:`brew install cmake` 、`brew install ninja`
-
-Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/ninja-build/ninja/releases) 获取 release 版放入`/usr/local/bin`中
-
-
-
-编译方式有2种:
-
-- ninja 编译
-
- 在 LLVM 源码同层目录下创建一个 `llvm_build` 目录,最终会在 `llvm_build` 目录下生成 `build.ninja`
-
- ```shell
- cd llvm_build
- cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的安装路径
- ```
-
- 然后执行编译指令,使用 `ninja`
-
- 再执行安装指令,使用 `ninja install`
-
-- Xcode 编译
-
- 在 LLVM 源码同层目录下创建一个 `llvm_xcode_build` 目录
-
- ```shell
- mkdir llvm_xcode_build
- cd llvm_xcode_build
- cmake -S ../../llvm-project/llvm -B ./ -G Xcode -DLLVM_ENABLE_PROJECTS="clang"
- ```
-
-
-因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功
-
-
-
-
-
-
-
-#### LLVM 角色说明
-
-- LLVM Core:包含一个现在的源代码/目标设备无关的优化器,一集一个针对很多主流(甚至于一些非主流)的 CPU 的汇编代码生成支持。
-- Clang:一个 C/C++/Objective-C 编译器,致力于提供令人惊讶的快速编译,极其有用的错误和警告信息,提供一个可用于构建很棒的源代码级别的工具
-- dragonegg: gcc 插件,可将 GCC 的优化和代码生成器替换为 LLVM 的相应工具。
-- LLDB:基于 LLVM 提供的库和 Clang 构建的优秀的本地调试器。
-- libc++、libc++ ABI:符合标准的,高性能的 C++ 标准库实现,以及对 C++11 的完整支持
-- compiler-rt:针对 __fixunsdfdi 和其他目标机器上没有一个核心 IR(intermediate representation) 对应的短原生指令序列时,提供高度调优过的底层代码生成支持
-- OpenMP:Clang 中对多平台并行编程的 runtime 支持
-- vmkit:基于 LLVM 的 Java 和 .NET 虚拟机
-- polly: 支持高级别的循环和数据本地化优化支持的 LLVM 框架。
-- libclc: OpenCL 标准库的实现
-- klee:基于L LVM 编译基础设施的符号化虚拟机
-- SAFECode:内存安全的 C/C++ 编译器
-- lld: clang/llvm 内置的链接器
-
-
-
-#### 添加插件目录
-
-进入目录 `/Users/unix_kernel/Desktop/LLVM_Explore/llvm-project/clang/tools`:
-
-- 先创建一个插件文件夹 `code-style-validate-plugin`
-
-
-
-- 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)`
-
-
-
-
-
-#### 配置插件
-
-在上一步创建的 `code-style-validate-plugin` 文件夹下:
-
-- 创建插件代码文件 `CodeStyleValidatePlugin.cpp`
-
-- 创建 `CMakeLists.txt` ,添加配置代码,其中 `FANPlugin` 是插件名,CodeStyleValidatePlugin 是插件源码文件名
-
- ```shell
- add_llvm_library(CodeStyleValidatePlugin MODULE BUILDTREE_ONLY
- CodeStyleValidatePlugin.cpp
- )
- ```
-
-
-由于新做了配置,并且要开发 `CodeStyleValidatePlugin.cpp` ,所以重新生成 `cmake -S .https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/llvm-project/llvm -B ./ -G Xcode -DLLVM_ENABLE_PROJECTS="clang"`
-
-
-
-#### 编写插件代码
-
-Xcode 打开项目,选择自动创建 Schemes
-
-
-
-
-
-选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑
-
-
-
-
-
-初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
-
-
-
-
-
-#### 编译 clang/clang++
-
-此步骤前需要做一步编译 Clang 的动作。Xcode 打开 LLVM 项目,选中 `ALL_BUILD` target,进行编译,此过程耗时较长(1h+)
-
-此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话,Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。
-
-
-
-
-
-#### Xcode 加载插件
-
-新建一个名字叫做 ` TestLLVM` 的 Xcode 项目。要在 Xcode 中加载指定的动态库,需要修改 Build Settings 配置,操作路径为:`Build Settings -> Other C Flags`。
-
-添加:
-
-- `-Xclang`
-- `-load`
-- `-Xclang`
-- 动态库路径
-- `-Xclang`
-- `-add-plugin`
-- `-Xclang`
-- 插件名称
-
-
-
-
-
-#### 设置编译器
-
-在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
-
-
-
-
-
-解决方案是在 Build Setiings 中增加2项用户自定义的设置:
-
-- `CC`:对应的是自己编译的 clang 的绝对路径
-
-- `CXX`:对应的是自己编译的 clang++ 绝对路径
-
-如下所示:
-
-
-
-
-
-继续编译还是会报错,报错如下:
-
-
-
-解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality` 的 ` Default` 改为 `NO`。
-
-
-
-
-
-#### 编译插件,验证正确性
-
-编译项目后,会在编译日志看到 `FANPlugin` 插件的打印信息,说明前面的配置没有问题,接下去就是继续编写 `FANPlugin.cpp` 的逻辑代码,继续验证。
-
-Tips: 由于重新修改了插件的源码,所以每次 Build 构建完 FANPlugin 之后,在 `TestLLVM` Xcode 项目中,最好每次都执行一下 Clean 操作。
-
-编译成功,可以看到在日志中输出了我们编写的日志信息。
-
-
-
-
-
-#### Clang 插件编写说明
-
-- `AnalysisConsumer`:`AnalysisConsumer` 是 clang AST 中做实事儿的接口,根据具体情况 `ASTFrontendAction` 可能对应一个或多个 `AnalysisConsumer`
-- `RecursiveASTVisitor` & `StmtVisitor`:`RecursiveASTVisitor `是顶层的遍历 clang AST 的工具,虽然也能处理 `stmt` 级别的处理,但是终归没有 `StmtVisitor` 用的顺手
-- `PluginASTAction`:clang 插件的关键组件之一。通过 PluginASTAction,可以在编译过程中运行额外的用户定义操作。这个类允许创建 AST 消费者对象,并处理插件命令行参数,以便根据需要执行特定操作。您可以通过实现 `ParseArgs` 方法来处理插件的命令行选项,以及通过覆盖 `getActionType` 方法来确定插件的执行时机,例如在主要操作之前或之后执行。这样的灵活性使得开发人员能够根据需求定制 clang 插件的行为
-- `ASTConsumer` :用于处理抽象语法树(AST)的重要组件。ASTConsumer 负责遍历和处理由 clang 前端生成的 AST 节点,执行特定的操作或分析。通过实现 ASTConsumer,开发人员可以访问和处理 AST 中的各种节点,例如函数、变量声明、表达式等,以便进行静态分析、代码转换或其他编译器任务
-- `MatchFinder`:提供类似 DSL 的方式用于匹配 AST 节点,用于做进一步的检验,获取节点来做判断或者进一步的处理。
-- `MatchFinder::MatchCallback`:用于在 MatchFinder 中处理匹配结果的回调函数。当 MatchFinder 在抽象语法树(AST)中找到与匹配器描述的模式相匹配的节点时,会调用注册的 MatchCallback 来处理这些匹配结果。MatchCallback 通常包含一些虚拟方法,如 `run()`、`onStartOfTranslationUnit()`、`onEndOfTranslationUnit()` 等,开发人员可以根据需要重写这些方法来实现自定义的处理逻辑。例如,在 `run()` 方法中处理每个匹配结果,在 `onStartOfTranslationUnit()` 方法中处理每个翻译单元的开始,在 `onEndOfTranslationUnit()` 方法中处理每个翻译单元的结束。
-
-
-
-#### 继续完善代码
-
-类名不符合规范的情况。
-
-```objective-c
-#import
-
-核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。
-
-步骤为:
-
-- 注册插件,需要指定 Action 是什么。这里我们指定自定义的继承自 `PluginASTAction` 的 `PluginASTAction`
-- Action 内部会调用 `CreateASTConsumer` 方法,所以需要创建一个继承自 `ASTConsumer` 的 consumer,即 ·`FANCounsumer`
-- Consumer 在 Xcode 解析完 AST 后会调用 `HandleTranslationUnit` 方法,`HandleTranslationUnit` 方法的参数是一个类行为 `ASTContext` 的对象,携带了 AST 的全部信息
-- 然后创建一个 `MatchFinder ` 对象。在构造器里指定 Macther 找什么 `matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler)`,以及找到后做什么事情,将找到后的逻辑交给了一个 CallBack,即 `handler` 的 `void run(const MatchFinder::MatchResult &Result)` 方法
-- `size_t pos = decl->getName().find('_')` 用来找类名中有没有下划线 `_`。
-- `pos != StringRef::npos` 不等于 `StringRef::npos` 则说明找到了下划线,则执行括号里面的逻辑
-- `DiagnosticsEngine &D = ci.getDiagnostics()` 对象具有报错能力,`D.Report()`
-- 为了精确报错,需要找到具体的位置信息 `SourceLocation loc = decl->getLocation().getLocWithOffset(pos)`
-
-
-
-完整代码
-
-```c++
-#include
-
-
-
-
-
-
-
-#### 有没有其他方式?
-
-利用 LLVM 编译前端 Clang + AST 的能力可以解决大多数编译器相关的问题,但是过程可能较为复杂。还有个思路是利用脚本能力,各种脚本语言,比如 Python、Node 都具备 `glob` 模块。`glob` 可以快速匹配并实现字符串的查找能力。
-
-利用关键词 `@interface 类名 : 父类名` 的特点,找到到所有的类名,判断类名带有 "_",然后将类名保存起来,最后输出有问题的类信息。
-
-
-
-### 检查 Category 中重名的方法
-
-- 使用开源库 [LIEF](https://github.com/lief-project/LIEF) 的能力
-- 脚本 Python、Node glob 模块的快速匹配能力
-- 添加 Xcode 环境变量 `OBJC_PRINT_REPLACED_METHODS`,运行时候会打印出来。参考[官方文档](https://developer.apple.com/library/archive/qa/qa1908/_index.html)
-
- 此处再引申聊聊命名规范的事情。[官方文档](https://developer.apple.com/library/archive/qa/qa1908/_index.html) 也说了 Category 命名的的最佳实践
-
- > ## Category Method Name Best Practice
- >
- > It is not possible to tell whether a given method name will conflict with an existing method defined by the original class because classes often contain private methods that are not listed in the classes interface. Further, a future version of the class may add new methods that clash with methods previously defined in your category. In order to avoid undefined behavior, it’s best practice to add a prefix to method names in categories on framework classes, just like you should add a prefix to the names of your own classes. You might choose to use the same three letters you use for your class prefixes, but lowercase to follow the usual convention for method names, then an underscore, before the rest of the method name.
-
- 简单来说,虽然有些类的方法在 `.m` 中可能存在10个方法,但在 `.h` 中公开了3个方法,然后在迭代的过程中,可能另一个对象也新增了3个方法,这3个方法可能是公开的也可能是私有方法,由于大家都遵循常见的 OC 命名策略(见名知意)所以很容易造成命名 冲突。给 Category 或者动态库、静态库命名最好带前缀,以避免方法冲突。这个好处不只是命名规范上的,更是代码逻辑安全出发的,由于 OC 强大的 Runtime 消息机制,重名的方法容易被调用。
-
- 官方给的例子
-
- ```objective-c
- @interface UIView (MyCategory)
-
- // CORRECT: The method name is prefixed.
- - (BOOL)wxyz_isOccludedByView:(UIView*)otherView;
-
- // INCORRECT: The method name is not prefixed. This method may clash with an existing method in UIView.
- - (BOOL)isOccludedByView:(UIView*)otherView;
-
- @end
- ```
-
- 除了 CI、CD 最后一道防线的拦截外,事前,团队内宣讲统一代码风格,Code Review 阶段看到 Category 方法命名不合理的地方,即使给出严厉的 Comment,也能拦截和规范一部分情况。
-
-- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
-
-
-
-
-
-### Pass 插桩,实现精准测试
-
-这部分涉及到 Objective-C 和 Swift 的代码插桩逻辑的不同实现,篇幅较大,可以查看这篇文章 [精准测试最佳实践](./1.108.md)
-
-
-
-### 静态检测、静态分析
-
-通过语法树进行代码静态分析,找出非语法性错误。模拟代码执行路径,分析出 control-flow graph(CFG)。
-
-LLVM 项目中 clang 内置了一堆 checker,用于实现 lint。
-
-具体的使用可以查看这篇文章:[质量检测](./1.137.md)
-
-
-
-### CodeGen - IR 代码生成与 OC Runtime 桥接
-
-- Class/Meta Class/Protocol/Category 内存结构生成,并存放在指定的 section 中(如 Class:`_DATA, _objc_classrefs`)
-
-- Non-Fragile ABI:为每个 Ivar 合成 `OBJC_IVAR_$_` 偏移值常量
-
-- 存取 Ivar 的语句(_ivar = 123; int a = _ivar) 转成 base + `OBJC_IVAR_$_` 的形式
-
-- 将语法树中的 `ObjcMessageExpr` 翻译成相应版本的 `objc_msgSend`,super 翻译成 `objc_msgSendSuper`
-
-- 根据修饰符 strong、weak、copy、atomic 合成 @property 自动实现的 setter/getter。处理 `@synthesize`
-
-- ARC:分析对象引用关系,将 `objc_storeStrong` `objc_storeWeak` 等 ARC 代码插入
-
-- 将 ObjcAutoreleasePoolStmt 翻译成 `objc_autoreleasePoolPush`、`objc_autoreleasePoolPop`
-
-- 自动调用 `[super dealloc]`
-
-
-
-- 为每个拥有 ivar 的 Class 合成` .cxx_destructor` 方法来自动释放类的成员变量,代替 MRC 时代的 `self.xxx = nil`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Chapter1 - iOS/1.103.md b/Chapter1 - iOS/1.103.md
deleted file mode 100644
index 28ec154..0000000
--- a/Chapter1 - iOS/1.103.md
+++ /dev/null
@@ -1 +0,0 @@
-# 设计模式及其场景
diff --git a/Chapter1 - iOS/1.104.md b/Chapter1 - iOS/1.104.md
deleted file mode 100644
index 22d3589..0000000
--- a/Chapter1 - iOS/1.104.md
+++ /dev/null
@@ -1,906 +0,0 @@
-# NSNotification底层原理
-
-> 有人聊起来 NSNotification 可以在不同的线程发和接收吗?对于不知道或者不确定的知识,有必要探究记录下
-
-
-
-## NSNotificationCenter
-
-```objectivec
-@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
-// 添加 Observer
-- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
-// 发送通知
-- (void)postNotification:(NSNotification *)notification;
-- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
-- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
-// 移除通知
-- (void)removeObserver:(id)observer;
-- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
-// 添加 Observer
-- (id
-
-
-
-当调用 UIView `[UIView setNeedsDisplay]` 方法时,系统会立刻调用其 Layer 的同名方法 `[view.layer setNeedsDisplay]` 方法,之后相当于给当前 Layer 打上一个脏标记,之后会在当前 RunLoop 快要结束的时候才会调用 Layer 的 `[CALayer display]` 方法。然后进入当前 UIView 真正的绘制流程中。
-
-其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法
-
-- 如果没有实现,则进入系统的绘制流程:比如:创建绘制上下文、调用 `drawInContext:`、生成内容并赋值给 `contents`
-- 如果实现了,则可能是异步绘制或者自定义渲染的实现。是**代理自定义绘制的入口**。代理可以在这个方法里直接设置`layer.contents`(比如异步绘制生成 UIImage 后赋值给`contents`),完全接管 layer 的内容渲染
-
-
-
-Demo1:
-
-自定义 View,不实现 `displayInContext` 方法
-
-```objective-c
-#import
-
-
-
-
-
-接下来看看系统的绘制实现流程:
-
-
-
-如何实现异步绘制?
-
-`[layer.delegate displayPlayer:]`
-
-- 代理负责生成对应的 bitmap
-- 设置该 bitmap 作为 layer.contents 属性的值
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## 渲染机制
-
-
-
-iOS 渲染框架可以分为4层,顶层是 UIKit,包括图形界面的高级 API 和常用的各种 UI 控件。UIKit 下层是 Core Animation,不要被名字误解了,它不光是处理动画相关,也在做图形渲染相关的事情(比如 UIView 的 CALayer 就处于 Core Animation 中)。Core Animation 之下就是由 OpenGL ES 和 CoreGraphics 组成的图形渲染层,OpenGL ES 主要操作 GPU 进行图形渲染,CoreGraphics 主要操作 CPU 进行图形渲染。上面3层都属于渲染图形软件层,再下层就是图形显示硬件层。
-
-iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core Graphics、Core Image 有 CPU 预处理,最终通过 OpenGL ES 将数据传输给 GPU,最终显示到屏幕上。
-
-
-
-- Core Animation 提交会话(事务),包括自己和子树(view hierarchy) 的布局状态
-
-- Render Server 解析所提交的子树状态,生成绘制指令
-
-- GPU 执行绘制指令
-
-- 显示器显示渲染后的数据
-
-## Core Animation
-
-
-
-可以看到 Core Animation pipeline 由4部分组成:Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
-
-### Application 层 Core Animation 部分
-
-
-
-- 布局(Layout):`layoutSubviews`、`addSubview`,这里通常是 CPU、IO 繁忙
-
-- 显示(Display):调用 view 重写的 `drawRect` 方法,或者绘制字符串。这里主要是 CPU 繁忙、消费较多内存。每个 UIView 都有 CALayer,同时图层又一个像素存储控件,存储视图,调用 `setNeedsDisplay` 仅会设置图层为 dirty。当渲染系统准备就绪,调用视图的 `display` 方法,同时装配像素存储空间,建立一个 Core Graphics 上下文(CGContextRef),将上下文 push 进上下文堆栈,绘图程序进入对应的内存存储空间。
-
-- 准备(Prepare):图片解码、图片格式转换。GPU 不支持某些图片格式,尽量使用 GPU 能支持的图片格式
-
-- 提交(Commit):打包 layers 并发送给 Render Server,递归提交子树的 layers。如果子树层级较多(复杂),则对性能造成影响
-
-### Render Server 中 Core Animation 部分
-
-Render Server 是一个独立的渲染进程,当收到来自 Application 的 (IPC) 事务时,首先解析 layer 层级关系,然后 Decode。最后执行 Draw Calls(执行对应的 OpenGL ES 命令)
-
-### GPU 渲染
-
-- OpenGL ES 的 command buffer 进行定点变换,三角形拼接、光栅话变为 parameter buffer
-
-- parameter buffer 进行像素变化,testing、blending 生成 frame buffe
-
-### 显示器显示
-
-视频控制器从 frame buffer 中读取数据显示在显示屏上。
-
-## UIView 绘制流程
-
-
-
-- 每个 UIView 都有一个 CALayer,layer 属性都有 contents,contents 其实是一块缓存,叫做 backing store
-
-- 当 UIView 被绘制时,CPU 执行 drawRect 方法,通过 context 将数据写入 backing store 中(位图 bitmap)
-
-- 当 backing store 写完后,通过 Render Server 交给 GPU 去渲染,最后显示到屏幕上
-
-
-
-- 调用 `[UIView setNeedsDisplay]` 方法时,并没有立即执行绘制工作,而是马上调用 `[view.layer setNeedsDisplay]` 方法,给当前 layer 打上标记
-
-- 在当前 RunLoop 快要结束的时候调用 layer 的 display 方法,来进入到当前视图真正的绘制流程
-
-- 在 layer 的 display 方法内部,系统会判断 layer 的 layer.delegate 是否实现了 `displayLayer` 方法
-
- - 如果没有,则执行系统的绘制流程
-
- - 如果实现了,则会进入异步绘制流程
-
-- 最后把绘制完的 backing store 提交给 GPU
-
-### 系统绘制流程
-
-
-
-- 首先 CALayer 内部会创建一个 CGContextRef,在 drwaRect 方法中,可以通过上下文堆栈取出 context,拿到当前视图渲染上下文也就是 backing store
-
-- 然后 layer 会判断是否存在代理,若没有,则调用 CALayer 的 drawInContext
-
-- 如果存在代理,则调用代理方法。然后做当前视图的绘制工作,然后调用 view 的 drawRect 方法
-
-- 最后由 CALayer 上传对应的 backing store(可以理解为位图)提交给 GPU。
-
-### 异步绘制流程
-
-
-
-- 如果 layer 有代理对象,且代理对象实现了代理方法,则可以进入异步绘制流程
-
-- 异步绘制流程中主要生成对应的 bitmap。目的是最后一步,需要将 bitmap 设置为 layer.contents 的值
-
- - 左侧是主队列,右侧是全局并发队列
-
- - 调用了setNeedsDiaplay 方法后,在当前 Runloop 将要结束的时候,会有系统调用视图所对应 layer 的 display 方法
-
- - 通过在子线程中去做位图的绘制,此时主线程可以去做些其他的工作。在子线程中:主要通过 CGBitmapContextCreate 方法,来创建一个位图的上下文、通过CoreGraphic API,绘制 UI、通过 CGBitmapContextCreatImage 方法,根据所绘制的上下文,生成一张 CGImage 图片
-
- - 然后再回到主队列中,提交这个位图,设置给 CALayer 的 contents 属性
-
-## 图片加载库都做了什么事
-
-众所周知,iOS应用的渲染模式,是完全基于Core Animation和CALayer的(macOS上可选,另说)。因此,当一个UIImageView需要把图片呈现到设备的屏幕上时候,其实它的Pipeline是这样的:
-
-1. 一次Runloop完结 ->
-2. Core Animation提交渲染树CA::render::commit ->
-3. 遍历所有Layer的contents ->
-4. UIImageView的contents是CGImage ->
-5. 拷贝CGImage的Bitmap Buffer到Surface(Metal或者OpenGL ES Texture)上 ->
-6. Surface(Metal或者OpenGL ES)渲染到硬件管线上
-
-这个流程看起来没有什么问题,但是注意,Core Animation库自身,虽然支持异步线程渲染(在macOS上可以手动开启),但是UIKit的这套内建的pipeline,全部都是发生在主线程的。
-
-因此,当一个CGImage,是采取了惰性解码(通过Image/IO生成出来的),那么将会在主线程触发先前提到的惰性解码callback(实际上Core Animation的调用,触发了一个`CGDataProviderRetainBytePtr`),这时候Image/IO的具体解码器,会根据先前的图像元信息,去分配内存,创建Bitmap Buffer,这一步骤也发生在主线程。
-
-这个流程带来的问题在于,主线程过多的频繁操作,会造成渲染帧率的下降。实验可以看出,通过原生这一套流程,对于一个1000*1000的PNG图片,第一次滚动帧率大概会降低5-6帧(iPhone 5S上当年有人的测试)。后续帧率不受影响,因为是惰性解码,解码完成后的Bitmap Buffer会复用。
-
-所以,最早不知是哪个团队的人(可能是[FastImageCache](https://github.com/path/FastImageCache),不确定)发现,并提出了另一种方案:通过预先调用获取Bitmap,强制Image/IO产生的CGImage解码,这样到最终渲染的时候,主线程就不会触发任何额外操作,带来明显的帧率提升。后面的一系列图片库,都互相效仿,来解决这个问题。
-
-具体到解决方案上,目前主流的方式,是通过CGContext开一个额外的画布,然后通过`CGContextDrawImage`来画一遍原始的空壳CGImage,由于在`CGContextDrawImage`的执行中,会触发到`CGDataProviderRetainBytePtr`,因此这时候Image/IO就会立即解码并分配Bitmap内存。得到的产物用来真正产出一个CGImage-based的UIImage,交由UIImageView渲染。
-
-## ForceDecode的优缺点
-
-上面解释了ForceDecode具体解决的问题,当然,这个方案肯定存在一定的问题,不然苹果研发团队早已经改变了这套Pipeline流程了
-
-优点:可以提升,图像第一次渲染到屏幕上时候的性能和滚动帧率
-
-缺点:提前解码会立即分配Bitmap Buffer的内存,增加了内存压力。举例子对于一张大图(2048*2048像素,32位色)来说,就会立即分配16MB(2048 * 2048 * 4 Bytes)的内存。
-
-由此可见,这是一个拿空间换时间的策略。但是实际上,iOS设备早期的内存都是非常有限的,UIKit整套渲染机制很多地方采取的都是时间换空间,因此最终苹果没有使用这套Pipeline,而是依赖于高性能的硬件解码器+其他优化,来保证内存开销稳定。当然,作为图片库和开发者,这就属于仁者见仁的策略了。如大量小图渲染的时候,开启Force Decode能明显提升帧率,同时内存开销也比较稳定。
-
-
-
-
-
-## iOS 图片解压缩到渲染过程
-
-- 假设我们使用 `+imageWithContentsOfFile:` 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩
-
-- 然后将生成的 `UIImage` 赋值给 `UIImageView`
-- 接着一个隐式的 `CATransaction` 捕获到了 `UIImageView` 图层树的变化
-- 在主线程的下一个 `runloop` 到来时,`Core Animation` 提交了这个隐式的 `transaction` ,这个过程可能会对图片进行 `copy` 操作,而受图片是否字节对齐等因素的影响,这个 `copy` 操作可能会涉及以下部分或全部步骤
- - 分配内存缓冲区用于管理文件 IO 和解压缩操作
- - 将文件数据从磁盘读到内存中
- - 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作
- - 最后 `Core Animation` 中 `CALayer` 使用未压缩的位图数据渲染 `UIImageView` 的图层
- - CPU 计算好图片的 Frame,对图片解压之后.就会交给 GPU 来做图片渲染
-- 渲染流程
- - GPU 获取图片的坐标
- - 将坐标交给顶点着色器(顶点计算)
- - 将图片光栅化(获取图片对应屏幕上的像素点)
- - 片元着色器计算(计算每个像素点的最终显示颜色值)
- - 从帧缓存区中渲染到屏幕上
-
-我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出
-
-
-
-## 为什么要解压缩图片
-
-既然图片的解压缩很耗费 CPU 时间,那么为什么还要对图片进行解压缩?是否可以不解压缩直接显示图片?不能
-
-
-
-其实位图,就是一个像素数组,数组中的每个像素就代表图片中的一个点。平时遇到的 png、jpeg 就是位图。
-
-
-
-```objective-c
-UIImage *image = [UIImage imageNamed:@"text.png"];
-CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
-```
-
-rawData 就是图片原始数据。
-
-jpg、png 都是一种压缩格式,只不过 png 是无损压缩,支持 alpha 通道。而 jpeg 是有损压缩,可以指定0~100%压缩比。iOS 提供2个函数来生成 png、jpeg 图片。
-
-```objective-c
-// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
-UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);
-
-// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
-UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);
-```
-
-所以,在磁盘的图片渲染到屏幕之前,必须先得到图片的原始像素数据,才可以执行后续的操作。所以必须先解压缩。
-
-
-
-## 图片解压缩原理
-
-既然图片的解压缩不可避免,而我们也不想让它在主线程执行,影响 App 性能,那么是否有比较好的解决方案呢?
-
-我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在**子线程提前对图片进行强制解压缩**。
-
-而强制解压缩的原理就是**对图片进行重新绘制,得到一张新的解压缩后的位图**。其中,用到的最核心的函数是 `CGBitmapContextCreate`
-
-```objective-c
-CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
- size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
- CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
- CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
-```
-
-参数说明:
-
-- data:如果不为 `NULL` ,那么它应该指向一块大小至少为 `bytesPerRow * height` 字节的内存;如果 为 `NULL`,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 `NULL` 即可;
-- width 和 height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
-- bitsPerComponent:像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
-- bytesPerRow :位图的每一行使用的字节数,大小至少为 `width * bytes per pixel` 字节。当我们指定 0/NULL 时,系统不仅会为我们自动计算,而且还会进行 `cache line alignment` 的优化
-- space :就是我们前面提到的颜色空间,一般使用 RGB 即可;
-- bitmapInfo :位图的布局信息.`kCGImageAlphaPremultipliedFirst`
-
-
-
-参考 YYImage/SDWebImage 都有图片解压缩的实现
-
-```objective-c
-CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
- ...
-
- if (decodeForDisplay) { // decode with redraw (may lose some precision)
- CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
-
- BOOL hasAlpha = NO;
- if (alphaInfo == kCGImageAlphaPremultipliedLast ||
- alphaInfo == kCGImageAlphaPremultipliedFirst ||
- alphaInfo == kCGImageAlphaLast ||
- alphaInfo == kCGImageAlphaFirst) {
- hasAlpha = YES;
- }
-
- // BGRA8888 (premultiplied) or BGRX8888
- // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
- CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
- bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
-
- CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
- if (!context) return NULL;
-
- CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
- CGImageRef newImage = CGBitmapContextCreateImage(context);
- CFRelease(context);
-
- return newImage;
- } else {
- ...
- }
-}
-```
-
-自己也可以实现
-
-```objective-c
-- (void)setImage {
- SP_BEGIN_LOG(custome, gl_log, imageSet);
- [self decodeImage:[UIImage imageNamed:@"peacock"] completion:^(UIImage *image) {
- self.imageView.image = image;
- SP_END_LOG(imageSet);
- }];
-}
-
-- (void)decodeImage:(UIImage *)image completion:(void(^)(UIImage *image))completionHandler {
- if (!image) return;
- //在子线程执行解码操作
- dispatch_async(dispatch_get_global_queue(0, 0), ^{
- CGImageRef imageRef = image.CGImage;
- //获取像素宽和像素高
- size_t width = CGImageGetWidth(imageRef);
- size_t height = CGImageGetHeight(imageRef);
- if (width == 0 || height == 0) return ;
- CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
- BOOL hasAlpha = NO;
- //判断颜色是否含有alpha通道
- if (alphaInfo == kCGImageAlphaPremultipliedLast ||
- alphaInfo == kCGImageAlphaPremultipliedFirst ||
- alphaInfo == kCGImageAlphaLast ||
- alphaInfo == kCGImageAlphaFirst) {
- hasAlpha = YES;
- }
- //在iOS中,使用的是小端模式,在mac中使用的是大端模式,为了兼容,我们使用kCGBitmapByteOrder32Host,32位字节顺序,该宏在不同的平台上面会自动组装换成不同的模式。
- /*
- #ifdef __BIG_ENDIAN__
- # define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
- # define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
- #else //Little endian.
- # define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
- # define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
- #endif
- */
-
- CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
- //根据是否含有alpha通道,如果有则使用kCGImageAlphaPremultipliedFirst,ARGB否则使用kCGImageAlphaNoneSkipFirst,RGB
- bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
- //创建一个位图上下文
- CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
- if (!context) return;
- //将原始图片绘制到上下文当中
- CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
- //创建一张新的解压后的位图
- CGImageRef newImage = CGBitmapContextCreateImage(context);
- CFRelease(context);
- UIImage *originImage =[UIImage imageWithCGImage:newImage scale:[UIScreen mainScreen].scale orientation:image.imageOrientation];
- //回到主线程回调
- dispatch_async(dispatch_get_main_queue(), ^{
- !completionHandler ?: completionHandler(originImage);
- });
- });
-}
-```
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Chapter1 - iOS/1.106.md b/Chapter1 - iOS/1.106.md
deleted file mode 100644
index 23df224..0000000
--- a/Chapter1 - iOS/1.106.md
+++ /dev/null
@@ -1,256 +0,0 @@
-# NSUserDefault 底层原理探究
-
-最近看到字节一篇文章 [卡死崩溃监控原理及最佳实践](https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247488080&idx=1&sn=39d0386b97b9ac06c6af1966f48387fc&chksm=e9d0d9b2dea750a4a7d21fd383aefa014d63f0dc79f2e3a13c97ad52bba1578dca8b50d6a40a&scene=21&cur_album_id=1590407423234719749#wechat_redirect) ,里面写到 NSUserDefault 底层实现存在直接或者间接跨进程通信,在主线程同步调用容易卡死。以前只是用过,但是没有仔细研究,看到这里就有必要研究下底层实现啦。
-
-## 回顾
-
-NSUserDefault 不安全。因为数据自动保存在沙盒的 `Libarary/Preferences` 目录下。
-
-数据按照 plist (property list)格式存储在沙盒中。当攻击者破解 App 就可轻而易举拿到里面的数据(可能有些人会将 token、password、secret 明文存在里面)
-
-另外 App 卸载重装会导致之前存储的数据丢失。这里推荐使用 Keychain。Keychain 是 iOS 提供的安全存储数据的方案,用于存储一些账号、密码等敏感信息。数据也不在沙盒中,即使删除 App,重新安装则可以继续从 Keychain 中获取数据。
-
-NSUserDefaults 的原理和 plist 序列化不同。
-
-iOS 上应用必须被沙盒化,各个不同 App 之间的 Defaults Domain 不一样,通常是 Bundle Identifier,或者是 App Group 中约定的 Suite Name。当使用 NSUserDefaults 的时候会按照下面 Domain 顺序:
-
-- NSArgumentDomain
-
-- 应用的 Bundle Identifier
-
-- NSGlobalDomain
-
-- 系统语言的标识符
-
-- NSRegistrationDomain
-
-任何应用,通过 NSUserDefaults 访问值都需要经历从上到下搜索各个 Domain 的过程,期间如何某个 Domain 有这个值,就会取出其对应的值。如果全部访问完还是没找到,则返回 undefined result。
-
-## 如何保证多线程安全
-
-通过设置符号断点可以看出, NSUserDefaults 内部在读写时会通过 `os_unfair_lock` 加锁进行多线程安全保护。
-
-
-
-## 存储性能如何
-
-查看 GUN 源码
-
-```c
-- (BOOL) synchronize
-{
- BOOL isLocked = NO;
- BOOL wasLocked = NO;
- BOOL shouldLock = NO;
- BOOL defaultsChanged = NO;
- BOOL hasLocalChanges = NO;
- // 合法性校验
- if ([removed count] || [added count] || [modified count])
- {
- hasLocalChanges = YES;
- }
- if (YES == hasLocalChanges && NO == [owner _readOnly])
- {
- shouldLock = YES;
- }
- if (YES == shouldLock && YES == [owner _lockDefaultsFile: &wasLocked])
- {
- isLocked = YES;
- }
- NS_DURING
- {
- NSFileManager *mgr;
- NSMutableDictionary *disk;
- // 利用 NSFileManager 读取文件
- mgr = [NSFileManager defaultManager];
- disk = nil;
- if (YES == [mgr isReadableFileAtPath: path])
- {
- NSData *data;
- // 文件存在,则将里面的内容读取出来
- data = [NSData dataWithContentsOfFile: path];
- if (nil != data)
- {
- id o;
- // 将文件数据利用 NSPropertyListSerialization 序列化为 NSDictionary 信息
- o = [NSPropertyListSerialization
- propertyListWithData: data
- options: NSPropertyListImmutable
- format: 0
- error: 0];
- // 将之前已经持久化好的本地文件数据写入到 disk 可变字典
- if ([o isKindOfClass: [NSDictionary class]])
- {
- disk = AUTORELEASE([o mutableCopy]);
- }
- }
- }
- if (nil == disk)
- {
- disk = [NSMutableDictionary dictionary];
- }
- loaded = YES;
- // 判断是否有新数据
- if (NO == [contents isEqual: disk])
- {
- defaultsChanged = YES;
- if (YES == hasLocalChanges)
- {
- NSEnumerator *e;
- NSString *k;
- // 从标记为待删除的数据中遍历,删除 disk 可变字典中的数据
- e = [removed objectEnumerator];
- while (nil != (k = [e nextObject]))
- {
- [disk removeObjectForKey: k];
- }
- // 遍历需要添加的数据,添加到 disk 中
- e = [added objectEnumerator];
- while (nil != (k = [e nextObject]))
- {
- [disk setObject: [contents objectForKey: k] forKey: k];
- }
- // 遍历需要修改的数据,添加到 disk 中
- e = [modified objectEnumerator];
- while (nil != (k = [e nextObject]))
- {
- [disk setObject: [contents objectForKey: k] forKey: k];
- }
- }
- // 将 disk 数据拷贝到 contents
- ASSIGN(contents, disk);
- }
- if (YES == hasLocalChanges)
- {
- BOOL written = NO;
-
- if (NO == [owner _readOnly])
- {
- if (YES == isLocked)
- {
- // 判断 contents 字典是否有值,没有则给指定路径写入 nil
- if (0 == [contents count])
- {
- /* Remove empty defaults dictionary.
- */
- written = writeDictionary(nil, path);
- }
- else
- {
- /* Write dictionary to file.
- */
- // 判断 contents 字典有值,则将 contents 给指定路径写入
- written = writeDictionary(contents, path);
- }
- }
- }
- // 写入成功删除内存缓存
- if (YES == written)
- {
- [added removeAllObjects];
- [removed removeAllObjects];
- [modified removeAllObjects];
- }
- }
- if (YES == isLocked && NO == wasLocked)
- {
- isLocked = NO;
- [owner _unlockDefaultsFile];
- }
- }
- NS_HANDLER
- {
- fprintf(stderr, "problem synchronising defaults domain '%s': %s\n",
- [name UTF8String], [[localException description] UTF8String]);
- if (YES == isLocked && NO == wasLocked)
- {
- [owner _unlockDefaultsFile];
- }
- }
- NS_ENDHANDLER
- return defaultsChanged;
-}
-```
-
-会发现:性能也就那么回事,底层实现通过内存缓存 `contents` 来缓存数据写入文件。
-
-## NSUserDefaults 为什么触发 XPC 通信
-
-通过对代码添加符号断点 `xpc_connection_send_message_with_reply_sync` 可以看到下面的堆栈
-
-
-
-执行 `[NSUserDefaults standardUserDefaults];` 可以发现是调用了 XPC,创建名称为 “com.apple.cfprefsd.daemon” 的 XPC Connection,且会发送一个 `xpc_connection_send_message_with_reply_sync` 的消息。
-
-执行 `[defaults setObject:@"杭城小刘" forKey:@"name"];` 会调用 `xpc_connection_send_message_with_reply_sync`,发送一个消息。
-
-通过 Demo 得出结论:
-
-- `NSUserDefaults` 调用 `set...forKey:`, 会触发 XPC 通信,调用 `...ForKey:` 、`synchronized` 不会调用 XPC 通信
-
-- 为了提高性能,尽量减少调用 `set...forKey:`
-
-## 异步持久化
-
-XPC 该`xpc_connection_send_message_with_reply_sync` API 因为 XPC 同步通信,所以在主线程容易存在卡死。那么有没有异步调用的能力?
-
-发现2个 API 可以用于异步发送
-
-- xpc_connection_send_message
-
-- xpc_connection_send_message_with_reply
-
-所以想异步持久化,则需要自定义 XPC Connection,然后将数据用 xpc_dictionary_create 创造出的 Dictionary 去接,最后调用 `xpc_connection_send_message_with_reply` 去持久化数据
-
-```objectivec
- xpc_connection_t conn = xpc_connection_create_mach_service("com.apple.cfprefsd.daemon", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
-#pragma mark - 开始构建信息
-// (lldb) po $rsi
-//
-
-但,这些回归还是不能 cover 所有问题,我们需要为我们写的每行代码买单,如何衡量每行代码的效果呢?这就是精准测试要做的事情。
-
-iOS 工程来说,跨端项目暂时不在本文范畴,Native 侧主要是 OC 和 Swift 为主。本文将会从 OC/ Swift 2个技术栈展开说说如何获取精准测试覆盖率报告。
-
-
-
-## Objective-C 代码覆盖率
-
-### 理论分析
-
-#### 覆盖率检测原理
-
-统计代码覆盖率的实现抓手就是对代码进行插桩,OC 是 C 语言的一个超集,而 LLVM 诞生自 GCC,我们可以使用 GCC 的插桩器对 OC 代码进行编译插桩,具体流程如下:
-
-
-
-在编译阶段指定 `-fprofile-arcs` `-ftest-coverage` 等测试选项,LLVM 会做这么几件事:
-
-- 在输出目标文件中留出一段存储区保存统计数据
-
- 打开一个插桩工程,查看 MachO 文件可以印证。可以看到 `__llvm_prf_cnts`、`__llvm_prf_data` 、`__llvm_prf_names`、`__llvm_prf_vnds`、`__llvm_covfun`、`__llvm_covmap` 等 section 就是存储插桩信息的空间。
-
-
-
-- 在源代码中为每个 Basic Block 进行插桩(Basic Block 下文会讲)
-
- 可以看到 `showAssets` 方法内存在一个 if,即2个 Basic Block,所以通过汇编查看的话,存在2个插桩点。
-
-
-
-- 产生 `.gcno` 文件,它包含 Basic Block 和相应的源码行号信息
-
-- 在最终可执行文件中,进入 main 函数之前调用 `gcov_init` 内部函数初始化统计数据区,并将 `gcov_init` 内部函数注册为`exit_handers`,用户代码调用 exit 正常结束时,`gcov_exit` 函数得到调用,并继续调用 `__gcov_flush` 输出统计数据到 `.gcda` 文件。
-
-
-
-生成覆盖率报告,首先需要在 Xcode 中配置编译选项,编译后会为每个可执行文件生成对应的 **`.gcno`** 文件;之后在代码中调用覆盖率分发函数,会生成对应的 **`.gcda`** 文件。
-
-其中,`.gcno` 包含了代码计数器和源码的映射关系, `.gcda` 记录了每段代码具体的执行次数。覆盖率解析工具需要结合这两个文件给出最后的检测报表。
-
-
-
-#### .gcno
-
-利用 Clang 分别生成源文件的 AST 和 IR 文件,对比发现,AST 中不存在计数指令,而 IR 中存在用来记录执行次数的代码。查看 LLVM 源码可以看到 `GCDAProfiling.c` ,该文件主要作用是:覆盖率映射关系生成源码。
-
-覆盖率映射关系生成源码是 LLVM 的一个 Pass,用来向 IR 中插入计数代码并生成 .gcno 文件(关联计数指令和源文件)。
-
-
-
-
-
-
-
-#### Basic Block
-
-从编译器角度出发,基本块(Basic Block,BB)是代码执行的基本单元,LLVM 基于 BB 进行覆盖率计数指令的插入。BB 特点是:
-
-- 只有1个入口
-- 只有1个出口
-- 只要 BB 中第一条指令被执行,那么 BB 中所有指令都会按顺序执行1次
-
-1个 BB 中,不包含其他的 jump/return/if/switch 等流程控制语句,也就是一个最小可执行单元。
-
-基本块 BB 是程序中一个顺序执行的**语句序列**,同一个 BB 中所有语句的执行次数一定相同,一般由多个顺序执行语句后跟一个跳转语句组成。
-
-从一个 BB 到另一个 BB 的跳转称为一个 ARC。
-
-
-
-#### GCOV 工作原理
-
-如果跳转语句是有条件的,就产生了一个分支(ARC),该基本块就有2个基本块作为目的地。如果把每个基本块当作一个节点,那么一个函数中 的所有基本块就构成了一个有向图, 称之为基本块图. 只要知道 BB 或 ARC 的执行次数就可以推算出所有 的 BB 和所有的 ARC 的执行次数. GCOV 根据 BB 和 ARC 的统计情况来统计各 BB 内各行代码执行情况, 从而计算整个程序的覆盖率情况。
-
-
-
-
-
-
-
- 也就是说插桩的数量和函数内的代码行数、函数数量都不是一一对应的关系。**插桩数量和 BB 个数一一对应**。
-
-这样设计的好处是:BB 的概念存在已久,利用现有能力进行功能拓展(插桩分析覆盖率),而不是为每行原始代码都插桩,从而大大减少了可执行文件的大小并且提高了执行的速度,同时还能够精确分析到所有代码的执行情况。x
-
-覆盖率计数指令的插入会进行两次循环,外层循环遍历编译单元中的函数,内层循环遍历函数的基本块。函数遍历仅用来向 `.gcno` 中写入函数位置信息。
-
-对下面方法展示控制流程图展示:
-
-```objective-c
-- (void)showAssets {
- NSLog(@"I am a rich man");
- if (self.name) {
- [self.cat play];
- } else {
- NSLog(@"I am nobody");
- }
-}
-```
-
-
-
-
-
-
-
-#### .gcon 计数符号和文件位置关联信息
-
-`.gcon` 文件存储着计数插桩位置和源文件之间的关联信息。`GCOVPass` 通过2层循环插入计数指令的同时,会将文件及 BB 信息写入 `.gcon` 文件。
-
-- 创建 `.gcno` 文件,写入 Magic number(oncg + version)
-- 随着函数遍历写入文件地址、函数名和函数在源文件中的起止行数(标记文件名,函数在源文件对应行数)
-- 随着 BB 遍历,写入 BB 编号、BB 起止范围、BB 的后继节点编号(标记基本块跳转关系)
-- 写入函数中BB对应行号信息(标注基本块与源码行数关系)
-
-
-
-`.gcon` 文件由4部分组成:
-
-- 文件结构
-- 函数结构
-- BB 结构
-- BB 行结构
-
-
-
-
-
-#### .gcda
-
-关于 `.gcda` 的逻辑可以查看源码的 [GCDAProfiling.c 文件](https://github.com/llvm/llvm-project/blob/main/llvm/lib/Transforms/Instrumentation/GCOVProfiling.cpp),也是覆盖率相关的核心逻辑。
-
-```c++
-void GCOVProfiler::emitGlobalConstructor(
- SmallVectorImpl
-
-
-
-Xcode 导出 `.gcda` 的时候,断点查看汇编如下
-
-
-
-
-
-#### .info 文件
-
-拿到 `.gcno` 和 `.gcda` 文件后,我们可以使用 LCOV 工具(基于 gcov )来生成这个源代码文件的覆盖率信息。
-
-覆盖率信息 `.info` 文件包含以下内容:
-
-1. TN:测试用例名称
-2. SF:源码文件路径
-3. FN:函数名及行号
-4. FNDA:函数名及执行次数
-5. FNF:函数总数
-6. FNH:函数执行数
-7. DA:代码行及执行次数
-8. LF:代码总行数
-9. LH:代码执行行数
-
-在增量覆盖率信息统计的步骤中,覆盖率信息文件新增了用于统计增量信息的字段:
-
-1. CA:差异代码行及执行次数
-2. CF:差异代码行总数
-3. CH:差异代码行执行数
-
-
-
-#### 完整流程
-
-
-
-- 编译前, 在编译器中加入编译器参数 `-fprofile-arcs` `-ftest-coverage`
-- 源码经过编译预处理, 在生成汇编文件的阶段完成插桩,生成可执行文件,并且生成关联 BB 和跳转次数 ARC 的 `.gcno` 文件
-- 运行可执行文件,随着功能被执行,打点插桩的计数值不断更新,收集程序的执行信息
-- 生成具有 BB 和 ARC 的执行统计次数等数据的 `.gcda` 文件
-- 通过 lcov、genhtml 将代码覆盖率信息生成 html 格式的报告
-
-
-
-### 工程实践
-
-第一步,在 Xcode Build Settings 中,修改 Clang 编译参数 `Instrument Program Flow`、 `Generate Legacy Test Coverage File` 为 true,打开后即**开启插桩能力**。
-
-
-
-第二步,为了控制代码覆盖率保存的位置和文件名,需要我们设置一下 GCC 提供的环境变量
-
-- `GCOV_PREFIX` 环境变量用于指定代码覆盖率文件的存储路径
-- `GCOV_PREFIX_STRIP `环境变量用于指定在存储路径中去除的前缀部分。
-
-```objective-c
-NSString *covFilePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/coverage_files"];
-setenv("GCOV_PREFIX", [covFilePath cStringUsingEncoding: NSUTF8StringEncoding], 1);
-setenv("GCOV_PREFIX_STRIP", "100", 1);
-```
-
-第三步,开启插桩后即拥有了原始 BB 信息,也开启了插桩。等待用户操作 App 后,即记录了 BB 执行信息,这些信息需要被写入 `.gcda` 中。早期版本是 `gcov_flush()`。可以看到 `_gcov_flush` 已经不能用了,发现官方已经是 `_gcov_dump` 。修改后编译通过。
-
-```c++
-extern void __gcov_dump(void);
-__gcov_dump();
-```
-
-
-
-
-
-第四步,运行代码。完成测试后,我在屏幕点击事件里,将 BB 执行情况写入到 `.gcda` 中。
-
-
-
-第五步,获取 `.gcno` 信息。编译器生成与源代码同名的 `.gcno` 文件(note file),这种文件含有重建基本块依赖图和将源代码关联至基本块及源代码行号的必要信息。
-
-Xcode 选择 products,show In Finder。然后上上层的 `Intermediates.noindex` 目录存储,继续往下寻找,我个人电脑上路径为:`/Users/unix_kernel/Library/Developer/Xcode/DerivedData/CodeCoverageDemo-enpprvshxhvihgavktgzcmeoertf/Build/Intermediates.noindex/CodeCoverageDemo.build/Debug-iphonesimulator/CodeCoverageDemo.build/Objects-normal/x86_64`,存储了 `.gcno` 信息。
-
-
-
-
-
-
-
-
-
-第六步,将 `.gcno` 和 `.gcda` 文件,保存到一个文件夹下
-
-
-
-
-
-第七步,利用 `lcov` 指令可以将 `.gcno` 文件和 `.gcda` 文件结合生成代码覆盖率结果 info 文件
-
-指令格式为:`lcov -c -d . -o CodeCoverage.info` ,其中 `.` 代表当前目录
-
-`CodeCoverage.info` 文件内容大概如下(各个字段代表什么上面 info 文件这一节有说明)。
-
-
-
-
-
-第八步,利用指令 `genhtml -o html CodeCoverage.info` 将 info 文件和源代码文件结合转化为可视化网页形式。
-
-注意:执行 genhtml 指令必须保证和项目源代码(Xcode 项目叫 CodeCoverageDemo,源码则在 CodeCoverageDemo/CodeCoverageDemo 下)在同一文件夹下否则会报错。
-
-
-
-访问覆盖率路径为 html 目录下,和项目同名的文件夹里面的 `index.html`
-
-
-
-第九步,通过类的列表,针对覆盖率低的文件,点进去看看,看看那些代码没有被执行。思考是什么原因造成的:
-
-- if...else 代码是由于测试条件不满足,测试 case 不充足,导致另一个 case 没有被覆盖??
-- 某些兜底代码太多,根本走不到???
-
-
-
-其中:蓝色部分代码已经执行的代码,橘色代表未执行的代码
-
-第十步,假设我们在另一台设备上进行了测试,对剩余的测试任务内容进行完善,这个时候该怎么处理?Demo 以针对 Person 类的覆盖率完善为例。
-
-1. 在另一台测试剩余 case 的机器上,执行测试流程。得到测试结果,即 `.gcda` 文件
-
-2. 新建测试数据分析文件夹 `CodeCoverageAnalysis2`
-
-3. 将上一步得到的 `.gcda` 文件拷贝到 ``CodeCoverageAnalysis2` 里面
-
-4. 进入打包产物 App 所在文件夹,进入文件夹 `Build/Intermediates.noindex/CodeCoverageDemo.build/Debug-iphonesimulator/CodeCoverageDemo.build/Objects-normal/x86_64`,可以看到一堆类似 `AppDelegate.d`、 `AppDelegate.dia` `AppDelegate.gcno` 、`AppDelegate.o` 这样的文件。同样移动到 ``CodeCoverageAnalysis2` 里面
-
-
-
-5. 在 `CodeCoverageAnalysis2` 目录下利用指令 `lcov -c -d . -o CodeCoverage2.info` 生成新的一份覆盖率信息 `CodeCoverage2.info`
-
-6. 然后利用 `locv -a` 指令合并2个 `.info` 文件。指令为 `lcov -a CodeCoverage2.info -a https://github.com/FantasticLBP/knowledge-kit/raw/master/CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
-
-7. 然后利用 `genhtml` 生成合并后的覆盖率可视化 html 文件 `genhtml -o html CodeCoverageCombined.info`
-
-8. 查看分析最新的覆盖率报告
-
-
-
-
-
-### 缺陷
-
-Person 类的 showAssets 方法,内部有 Cat 相关逻辑,且 Cat 是 Swift 代码。为什么在代码覆盖列表上看不到 Cat?
-
-
-
-因为,clang 是一个基于 LLVM 的编译器前端,它可以编译多种编程语言,包括 C、C++、Objective-C 和 Objective-C++。然而,虽然 clang 本身基于 LLVM,但它并不是 Swift 语言的默认编译器。Swift 语言的官方编译器是 **swiftc**,是基于 LLVM 但专门为 Swift 语言设计的。
-
-接下来看看 Swift 代码,如何获取代码覆盖率
-
-
-
-### 工程化
-
-工程化要解决的3个问题是:
-
-- 一般来说,iOS 现在采用模块化的方式:壳工程 + 各个业务域子工程 + 3方模块。可通过 ruby 脚本修改壳工程和相应的业务工程的编译配置,开启编译插桩能力。一般对于 Debug 包来说不插桩,所以需要有个配置文件,来对各个模块进行配置。
-- 单个版本不断测试,生成的代码覆盖率信息如何合并
-- 多版本增量覆盖率
-- 打包平台及其服务侧
-
-
-
-### 模块化配置
-
-对于各个模块在什么模式下插桩的配置, `CodeCoverageConfig.rb`
-
-```ruby
-ENABLE_PROJECTS = {
- "XXX/XXXPhone.xcodeproj" => "Enterprise",
- "XXXHD/XXXHD.xcodeproj" => "Enterprise",
- "Pods/XXXGoods.xcodeproj" => "Enterprise",
- // ...
-}
-```
-
-Ruby 脚本利用 [xcodeproj](https://github.com/CocoaPods/Xcodeproj) 对每个 target 的编译参数 `GCC_INSTRUMENT_PROGRAM_FLOW_ARCS` 、`GCC_GENERATE_TEST_COVERAGE_FILES`进行修改以开启插桩能力
-
-```ruby
-require 'xcodeproj'
-CONFIG_DIR = Pathname.new(File.join(File.dirname(__FILE__), ".https://github.com/FantasticLBP/knowledge-kit/raw/master/../..")).realpath
-CONFIG_FILE = File.join(CONFIG_DIR, "CodeCoverageConfig.rb")
-
-def update(args)
- enable = args[0] == "true" ? "YES" : "NO"
- debug = args[1] == "true" ? true : false
- load "#{CONFIG_FILE}"
- projects = ENABLE_PROJECTS
- projects.each do | proj, conf |
- proj_file = File.join(CONFIG_DIR, proj)
- project = Xcodeproj::Project.open(proj_file)
- project.build_configurations.each do |config|
- next if debug && config.name != "Debug"
- next if !debug && config.name != conf
- config.build_settings['GCC_INSTRUMENT_PROGRAM_FLOW_ARCS'] = enable
- config.build_settings['GCC_GENERATE_TEST_COVERAGE_FILES'] = enable
- end
- project.save
- end
-end
-
-update(ARGV)
-```
-
-
-
-### 单版本覆盖率
-
-代码不变的情况下,发现 QA 或者开发自己测试的情况下,发现代码覆盖率不高,测试没有全面,则继续测试。这样生成多分 `.gcda` 文件,
-
-- 生成覆盖率:`lcov -c -d {$SOURCE} -o {$DEST_INFO}`,比如 `lcov -c -d . -o CodeCoverage2.info`
-- 合并覆盖率:`lcov -a {$SOURCE_INFO_1} -a {$SOUCE_INFO_2} -o {$DEST_INFO}`,比如 `lcov -a CodeCoverage2.info -a https://github.com/FantasticLBP/knowledge-kit/raw/master/CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
-
-
-
-### 多版本增量覆盖率
-
-一个常见的场景是,开发同学基于业务需求 A 做完功能,QA 测试后导出覆盖率报告,发现覆盖率较低;或者 QA 提了3个测试 Bug,开发针对这2个情况,去修改了代码,重新打包让 QA 回归。这个时候 QA 不会重新点点,较好的做法是只回归遗漏或者有问题的代码。
-
-
-
-核心思路是:基于上个版本的覆盖率数据,利用 git diff 查找出变化的部分,然后将旧版本覆盖率 `.info` 里面喝 git diff 得出的变化的部分关联,将值更新到新测试后的覆盖率 `.info` 里面。
-
-
-
-git diff 如何解读
-
-
-
-
-
-其中
-
-- `index.txt` 是文件名
-- `@@ -3,6 +3,6 @@` ,- 代表删除,+ 代表增加。整体意思为从第3行开始,删除了6行,从第3行开始,增加了6行
-
-所以步骤如下:
-
-- 解析 git diffFile:
- - 根据文件名匹配规则 `diff --git (.*)` 将 diffFile 解析为若干个文件的数组集合 diffInfoList,并且保存文件信息
- - 根据 diff 块匹配规则 `@@(.*)@@` 将每个文件的 diffInfo 解析为若干个 diff 块的 blockInfoList,并且保存块信息
- - 根据增 / 删代码匹配规则 `(\+|\-)(.*)` 将每个块的 blockInfo 解析为若干个修改行号的增 / 删行数,并保存增 / 删信息 `{'delLine': 3, 'delCount': 6, 'addLine': 3, 'addCount': 6}`
-
-- 解析 info 文件
- - 根据文件名匹配 `SF:*end_of_record:` 规则将 info 解析为若干个文件的 fileInfoList,并且保存文件信息
- - 根据函数行、函数执行次数、代码行及执行次数匹配规则 `FN、FNDA、DA` 将每个文件的 fileInfo 解析为若干个执行信息的 daList,并且保存数据信息 `{'lineNo': 12, 'exeCount': 1, 'funName': 'eat'}`
-
-- 生成 info 文件
- - 根据 diffFile 解析结果,遍历 blockInfo 匹配起始修改行号 `delLine` 及修改行数 `diffline = addCount - delCount`,将 info 的解析结果进行行号匹配和增 / 删操作 `if (lineNo > delLine) lineNo += diffLine`,修改 fileInfoList 。这一步其实就是根据 git diff 信息,将新的覆盖率中的 lineNo 进行更新
- - 将新的 fileInfoList 中的数据根据 info 的结构进行写入文件操作
-
-完成行号平移之后,两个版本的 .info 文件中的数据已经对齐了行号,可以用上述 LCOV 工具进行合并,合并完成后,用行号标记来统计差异的代码覆盖率数据。
-
-
-
-### 打包平台及其服务侧
-
-- 编写脚本在打包插桩后,将 `.gcno` 和源代码等信息上传到文件服务器上
-- 移动端各个测试设备测试后,App 可视化导出精准测试覆盖率报告,一键将 `.gcda` 文件上传到文件服务器上
-- 上传 `.gcda` 触发任务,利用 lcov 处理展示报告,同时也保存到文件服务器上
-- 最后 lark、企业微信通知能力,发送报告链接给开发、QA和相关人员
-- 同时 mPass 项目平台,买票上高铁的项目列表也有入口可以展示查看精准覆盖率报告
-
-
-
-## Swift 代码覆盖率
-
-这部分我将介绍:
-
->- 如何生成 `.profraw` 文件并通过命令行测量代码覆盖率
->- 如何在 Swift 项目里调用 c/c++ 方法
->
->- 如何在 Xcode 中测量完整 Swift App 项目的代码覆盖率
-
-
-
-### 理论支撑
-
-#### 编译器参数支持
-
-思路同 Objective-C 一样,参看 swiftc 编译器的编译参数 `swiftc --help` 可以看到
-
-
-
-可以看到这2个参数是大概收集代码覆盖率相关的。
-
-
-
-#### MachO 和汇编插桩验证
-
-利用 MachOView 查看产物里的 Mach-O 文件发现,MachO 多了一些和 LLVM 相关的 section,这些 section 看名字猜出来都是用来统计覆盖率的。
-
-
-
-当 Xcode 开启 Swift 插桩统计后,打断点查看汇编代码可以发现,在 sayHi 方法,也就是只有1个 Basic Block 的情况下,编译器只插入1个桩,插桩1次。
-
-
-
-可以把 `__profc_xxx` 理解为打点计数信息,具体的地址保存在 MachO 文件的 `__DATA` 段 `__llvm_prf_cnts` 节点中。在程序刚启动时,所有的计数器信息为0,每当该代码(BB块)被执行1次,其计数值会加一。
-
-重要的2个参数:
-
-- `-profile-generate`:负责插桩代码的生成,是统计插桩信息用来的。`__llvm_prf` 段。
-- `-profile-coverage-mapping` :则生成一些 LLVM 相关的 `__LLVM_COV` 段。
-
-之所以要做这样的拆分,猜测可能的原因是,插桩信息除了可以用于覆盖率分析以外,还可以用来进行 PGO 优化。什么是 PGO?即 Profile Guided Optimization ,是编译器用于提升 Application 的性能的一项技术。具体可以查看这篇文章[编译器利用 PGO 优化 App 性能](./1.133.md)
-
-
-
-#### 导出原理
-
-`llvm-cov` 如何生成报告的?因为 `.profdata` 文件只有 BB 计数器的调用次数,在生成覆盖率的时候传入了源码,那计数器信息和源码关联应该就是靠 MachO 文件了。
-
-[LLVM Code Coverage Mapping Format](https://llvm.org/docs/CoverageMappingFormat.html#high-level-overview) 也说明了该细节
-
-> LLVM’s code coverage mapping format is designed to be a self contained data format that can be embedded into the LLVM IR and into object files. It’s described in this document as a **mapping** format because its goal is to store the data that is required for a code coverage tool to map between the specific source ranges in a file and the execution counts obtained after running the instrumented version of the program.
->
-> The mapping data is used in two places in the code coverage process:
->
-> 1. When clang compiles a source file with `-fcoverage-mapping`, it generates the mapping information that describes the mapping between the source ranges and the profiling instrumentation counters. This information gets embedded into the LLVM IR and conveniently ends up in the final executable file when the program is linked.
-> 2. It is also used by *llvm-cov* - the mapping information is extracted from an object file and is used to associate the execution counts (the values of the profile instrumentation counters), and the source ranges in a file. After that, the tool is able to generate various code coverage reports for the program.
-
-LLVM 的代码覆盖率映射格式被设计为一种自包含的数据格式,可以嵌入 LLVM IR 和 `.o` 文件中。在本文档中,它被描述为映射格式,因为它的目标是存储代码覆盖率工具在文件中的特定源范围和运行插入指令的程序版本后获得的执行计数之间进行映射所需的数据。
-在代码覆盖过程中,映射数据用于两个位置:
-
-- 当 clang 使用 `-fcoverage-mapping` 编译源文件时,它会生成描述源范围和分析检测计数器之间映射的映射信息。这些信息被嵌入LLVM IR中,并在链接程序时方便地最终出现在最终的可执行文件中。
-- 它也被 `llvm-cov` 使用-映射信息从对象文件中提取,用于关联文件中的执行计数(配置文件检测计数器的值)和源范围
-
-在此之后,该工具能够为程序生成各种代码覆盖率报告。
-
-
-
-完整流程为:
-
-
-
-覆盖率生成流程为:编译阶段使用 `-profile-generate` 和 `-profile-coverage-mapping ` 参数,其中` -profile-generate` 会开启插桩能力,为每个 BB 增加插桩代码,`-profile-coverage-mapping` 将记录 BB、计数器值、和文件源码的关联映射信息,并将这些信息存储在编译产物,也就是 `__LLVM_COV` 段中。编译产物运行的过程中, 随着 BB 被执行,计数器的值会不断增加,并且写入 `__DATA` 段。运行结束后生成 `.profraw` 文件,可以处理成 `.profdata` 文件,该文件记录了每个计数器以及调用次数。
-
-覆盖率解析流程为:利用指令提供的源代码路径,和可执行文件信息,结合 `.profdata` 信息,产出覆盖率报告。具体原理是:遍历 `profdata` 中的每一个计数器,先根据可执行文件中存储的映射关系,找到这个计数器所对应统计的那一段源码,从而生成行级别的覆盖率信息。
-
-
-
-### 实验
-
-用简单的单个 Swift 文件进行理论分析。
-
-第一步,创建一个名为 `test.swift` 的文件,内容如下:
-
-```swift
-func sayHi() {
- print("Hello swift world")
-}
-
-func add(_ x: Int, _ y: Int) -> Int {
- return x + y
-}
-
-func minuse(_ x: Int, _ y: Int) -> Int {
- return x - y
-}
-
-sayHi()
-print(add(2, 4))
-```
-
-第二步,在终端命令行,`test.swift` 所在路径下执行下面指令 `swiftc -profile-generate -profile-coverage-mapping test.swift`
-
-传递给编译器的选项 `-profile-generate` 和 `-profile-coverage-mapping` 将在编译源码时启用覆盖率特性。基于源码的代码覆盖功能直接对 AST 和预处理器信息进行操作。
-
-第三步,运行二进制文件 `./test`。然后在当前目录执行 `ls`,可以看到多出了一个名为 `default.profraw` 的文件。该文件由 llvm 生成,目的是衡量代码覆盖率。我们必须使用配套工具 llvm-profdata 来组合多个原始配置文件并同时对其进行索引。
-
-第四步,终端运行指令 `xcrun llvm-profdata merge -sparse default.profraw -o coverage.profdata`,得到一个名为 `coverage.profdata` 的文件,进一步处理,它可以用来展示覆盖率报告。
-
-第五步,在终端运行指令得到覆盖率信息
-
-```shell
-xcrun llvm-cov show ./test -instr-profile=coverage.profdata
-xcrun llvm-cov export ./test -instr-profile=coverage.profdata
-```
-
-整个步骤也可以看这张图
-
-
-
-在 `test.swift` 中编写的3个函数,只有2个执行了。查看覆盖率可以证实这一点,minuse 函数没有被执行。
-
-
-
-### 工程实践
-
-第一步,创建 Swift 项目,编写测试代码
-
-```swift
-// Cat.swift
-import Foundation
-class Cat {
- var kind: String
- init(kind: String) {
- self.kind = kind
- }
-
- func play() {
- print("I am a \(kind) cat, I am playing now.")
- }
-}
-
-// Person.swift
-import Foundation
-class Person {
- var name: String
- var cat: Cat?
-
- init(name: String, cat: Cat? = nil) {
- self.name = name
- self.cat = cat
- }
-
- func sayHi() {
- print("Hello world, I am \(name), I have a \(String(describing: cat?.kind)) cat")
- }
-
- func eat() {
- print("eat")
- }
-
- func sleep() {
- print("sleep")
- }
-
- func play() {
- cat?.play()
- }
-}
-```
-
-第二步,选择 ` Build Settings -> Swift Compiler — Custom Flags`,在 Other Swift Flags 添加 `-profile-generate` 和 `-profile-coverage-mapping` 选项。
-
-
-
-
-
-第三步,开启覆盖率收集选项
-
-
-
-第四步,要将覆盖率信息导出前,必须要调用 llvm 的一些 c/c++ api,所以要将需要用到的方法,导出为一个模块。
-
-创建一个名为 `InstrProfiling.h` 的头文件。内容为:
-
-```c++
-#ifndef PROFILE_INSTRPROFILING_H_
-#define PROFILE_INSTRPROFILING_H_int __llvm_profile_runtime = 0;void __llvm_profile_initialize_file(void);
-
-const char *__llvm_profile_get_filename();
-void __llvm_profile_set_filename(const char *);
-int __llvm_profile_write_file();
-int __llvm_profile_register_write_file_atexit(void);
-const char *__llvm_profile_get_path_prefix();
-
-#endif /* PROFILE_INSTRPROFILING_H_ */
-```
-
-创建一个 `module.modulemap` 文件并将所有内容导出为一个模块(创建的时候 Xcode 选择 empty 模版)
-
-```shell
-module InstrProfiling {
- header "InstrProfiling.h"
- export *
-}
-```
-
-第五步,判断时机,在需要导出覆盖率的地方编写函数。我在 ViewController 点击屏幕的时候导出:
-
-- 导入模块 `import InstrProfiling`
-- 编写导出方法 `__llvm_profile_set_filename` 和 `__llvm_profile_write_file`
-
-````
-import UIKit
-import InstrProfiling
-
-class ViewController: UIViewController {
- var cat: Cat?
- var person: Person?
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- self.cat = Cat(kind: "Ragdoll")
- self.person = Person(name: "FantasticLBP", cat: cat)
- }
-
- override func touchesBegan(_ touches: Set
-
-第七步,因为产出覆盖率的时候需要用到 MachO 文件。所以在项目根目录下创建名为 `DataAnalysis` 的文件夹。在终端利用 mv 将产物里的 MachO 移动到 `DataAnalysis` 文件夹下。也将 `.profraw` 移动进去。
-
-
-
-第八步,利用指令 `xcrun llvm-profdata merge -sparse SwiftCodeCoverage.profraw -o SwiftCodeCoverage.profdata`,将 `.profraw` 转换成 `.profdata` 文件
-
-第九步,利用指令 `xcrun llvm-cov show ./SwiftCodeCoverage.app/SwiftCodeCoverage -instr-profile=SwiftCodeCoverage.profdata` 在终端查看代码的覆盖情况
-
-
-
-第十步,终端查看代码执行情况还是不够直观,可以用 `llvm-cov` 命令生成 HTML 格式的覆盖率报告,指令格式为:
-
-```shell
-xcrun llvm-cov show\
- -use-color\ # 彩色报告
- -format=html\ # HTML 格式
- -arch=x86_64\ # 架构指令集
- -instr-profile=${.profdata 路径}\ # 指定 .profdata 文件路径
- ${MachO 文件路径}\ # 指定 MachO文件路径
- ${SourceCode 路径} # 项目源代码路径
- -output-dir ${Swift覆盖率报告路径} # 指定覆盖率报告保存的路径
-```
-
-我这边具体指令为:
-
-`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverage.profdata SwiftCodeCoverage https://github.com/FantasticLBP/knowledge-kit/raw/master/ -output-dir ./SwiftCodeCoverageReport `
-
-第十一步,查看整体的覆盖率信息与单个文件的覆盖率,查看代码执行情况
-
-其中 `index.html` 是所有文件的覆盖率数据汇总,而每个文件精确到行级别的覆盖率信息,则保存在 `coverage` 文件夹中,每个文件对应一个 HTML 文件。
-
-
-
-第十二步,假设我们在另一台 CI 机器上也在执行测试任务。那不同机器上的测试结果如何合并?
-
-生成覆盖率报告是基于插桩实现的,最后 `xcrun llvm-cov` 生成 html 需要的是:Mach-O 文件、源代码路径、`.profdata` 文件。
-
-看得出来不同 CI 机器上,不同的只有 `.profdata` 文件,所以处理 `.profdata` 即可。所幸 `llvm-profdata` 就支持不同的 `.profraw` 的合并。
-
-比如第一台机器生成的是 `SwiftCodeCoverage.profraw` 得到的覆盖率如上图所示。第二台机器生成的是 `SwiftCodeCoverage.profraw`。
-
-接下去利用指令 ` xcrun llvm-profdata merge SwiftCodeCoverage.profraw SwiftCodeCoverage2.profraw -o SwiftCodeCoverageCombined.profdata` 将2份测试原始文件进行合并,然后再利用 `llvm-cov` 生成 html 报告
-
-
-
-第十三步,我们有时候有需求会更改操作生成的测试文件,`.profdata` 是没办法修改的,但 `llvm-profdata` 指令可以传递参数生成 `.text` 格式的文件,里面的内容可以修改。修改后再从 `.text` 转换为 `.profdata`,最后再利用 `llvm-cov` 生成 html 报告。
-
-下面演示下如何修改生成的覆盖率数据(注意:不修改 html,而是修改 BB 的计数值)
-
-1. 利用 ` xcrun llvm-profdata merge SwiftCodeCoverage.profraw SwiftCodeCoverage2.profraw -text -o SwiftCodeCoverageCombined.txt` 将2份 `.profraw` 数据合并为 `.txt` 格式的文件(记录了 BB 和技术值信息)
-
-
-
-2. 编辑修改 `.txt` BB 的计数值,此处,故意把 `Person:sleep` 的1改为0
-
-3. 利用指令将 `.txt` 改为 `.profdata` 格式。`xcrun llvm-profdata merge SwiftCodeCoverageCombined.txt -o SwiftCodeCoverageCombinedFromText.profdata`
-
-4. 再根据合并后的 `.profdata` 生成 html 覆盖率报告。`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverageCombinedFromText.profdata SwiftCodeCoverage https://github.com/FantasticLBP/knowledge-kit/raw/master/ -output-dir ./SwiftCodeCoveragCombinedReportFromText`
-
-效果如下:
-
-
-
-
-
-
-
-## 心得感悟
-
-下面是一个工作中的实际例子,冒烟用例也全部通过了,代码在 CR 后 MR 了,然后买票上车,开始高铁回归阶段。
-
-
-
-
-
-
-
-QA 去回归测试,然后会给开发一个精准测试报告。就是原始的本次业务开发分支上的代码执行情况。程序员去分析,覆盖率低的原因是什么,是兜底代码太多、还是某些技术实现是类似夸端的 Weex、RN、Flutter、还是测试 case 不充分以至于看上去用例通过,但是某些代码还是没有测试到,往往这些没有测试到、执行到的代码是线上用户在极端情况下容易走到的 case。所以需要根据精准测试覆盖率反推 QA 完善用例,或者开发自己优化代码。
-
-精准测试的价值很明显,但 ROI 就见仁见智了,有些人觉得要开发一套 CI 需要耗时耗力,每个项目完成后需要分析精准测试报告、反推 QA 完善用例很麻烦,但有些决策者就觉得这样能 cover 一些平时难以发现的问题。
-
-
-
-## 参考文章
-
-[Source-based Code Coverage](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#source-based-code-coverage)
-
-[llvm-cov - emit coverage information](https://llvm.org/docs/CommandGuide/llvm-cov.html)
-
-[LLVM Code Coverage Mapping Format](https://llvm.org/docs/CoverageMappingFormat.html#llvm-code-coverage-mapping-format)
-
-
-
diff --git a/Chapter1 - iOS/1.109.md b/Chapter1 - iOS/1.109.md
deleted file mode 100644
index f14415e..0000000
--- a/Chapter1 - iOS/1.109.md
+++ /dev/null
@@ -1,1023 +0,0 @@
-# 汇编学习
-
-## 基础知识回顾
-
-地址总线:它的宽度决定了 CPU 的寻址能力。比如8086地址总线宽度为20,则寻址能力为1M(2的20次方)
-数据总线:它的宽度决定了 CPU 单次数据传送量,也就是数据传输速度。比如8086的数据总线宽度为16,所以单次最大传递2个字节的数据/
-控制总线:它的宽度决定了 CPU 对其他其间的控制能力,能有多少种控制。
-
-内存的分段管理:起始地址+偏移地址=物理地址,计算出物理地址再去访问内存。
-
-偏移地址为16位,16位地址的寻址能力位64kb,所以一个段的长度最大为64kb。
-
-
-
-## CPU 的典型构成
-
-- 寄存器:信息存储
-
-- 运算器:信息处理
-
-- 控制器:控制其他器件进行工作
-
-对开发同学来说,CPU 中最主要的部件就是寄存器,“可以通过改变寄存器的内容来实现对 CPU 的控制”。
-
-不同的 CPU,寄存器个数、结构是不同的(比如8086是16为结构的 CPU,8086有14个寄存器)
-
-
-
-## 说明
-
-- 汇编中,小括号内存放的一定是内存地址。
-
-- 指令后面的字母代表操作数长度。比如 b = byte(8-bit),s = short(16-bit integer or 32-bit floating point)、w = word(16-bit)、l=long(32-bit integer or 64-bit floating point)、q=quad(64 bit)、t=tem bytes(80-bit floating point)。比如 ` movq $0xa, 0x86c1(%rip)` 是 `let a:Int = 10` 的汇编实现。
-
-- rip 存储的说指令的地址。CPU 要执行的下一条指令地址就存储在 rip 中
-
-- rax、rdx 寄存器一般作为函数返回值使用
-
- ```swift
- func getValue() -> Int {
- return 10
- }
- var v = getValue()
- ```
-
-
-
-
-
- 在第5行代码加断点,第4行汇编遇到 call 函数调用,LLDB 输入 `si` 进去,可以看到将十六进制 `0xa` 也就是10,保存到寄存器 `%eax` 也就是`%rax` 中。
-
- ```assembly
- SwiftDemo`getValue():
- -> 0x100003b20 <+0>: pushq %rbp
- 0x100003b21 <+1>: movq %rsp, %rbp
- 0x100003b24 <+4>: movl $0xa, %eax
- 0x100003b29 <+9>: popq %rbp
- 0x100003b2a <+10>: retq
- ```
-
- LLDB 输入 `finsh` 结束函数调用这段汇编,可以看到在汇编的第5行,将 `%rax` 保存的 10 赋值到 `%rip + 0x86d0 ` 地址。可以看 `%rip + 0x86d0` 是个全局变量,大概就是 v 的地址(可以继续用汇编验证,绝对是 v)。
-
-
-
-- rdi、rsi、rdx、rcx、r8、r9 寄存器一般用来存储函数参数。
-
-
-
- 可以看到第四行汇编的 `%edi` ... `%r9d` 和上面描述的寄存器顺序一致。
-
-- rsp、rbp 寄存器用于栈操作。栈顶指针,指向栈的顶部
-- leaq 和 movq 是有区别的。`leaq 0xd(%rip), %rax` 是从 `%rip + 0xd` 算出来的地址值赋值给 `%rax` ,`movq 0xd(%rip), %rax` 是从 `%rip + 0xd ` 算出来的地址值,取8个字节给 `%rax`。
-- `xorl` 异或运算。
-
-## 寄存器的高低位兼容设计
-
-汇编中高位对于低位寄存器的兼容性设计:
-%r 开头的寄存器都是64位(8 Byte)
-%e 开头的寄存器都是32位的(4 Byte)
-那如果所有的寄存器再去分 %r、%e 那就会存在很多寄存器了,使用和记忆很难了。
-
-同时早期的寄存器之下写的汇编代码,升级的时候要改写,成本太大了。如何设计才可以兼容升级呢?
-
-设计很巧妙。假设一个 %rax 的64位寄存器(0~63位)
-
-- 64位:则 all in 全部使用
-- 32位:为了兼容低的32位寄存器,则拿出低的4字节(0~31位)当作 %eax 32位寄存器来使用
-- 16位:为了兼容16位的寄存器,则拿出低的2个字节(0~15位)当作 %ax 16位寄存器来使用;
-- 8位:为了兼容8位的寄存器,则拿出低的2个字节(0~15位)分为2段,高8位、低8位来使用,分别是 %ah、%al 寄存器。
-
-
-
-
-寄存器:
-- r 开头:64 bit,8 Byte
-- e 开头:32 bit,4 Byte
-- ax、bx、cx、dx:16 bit,2 Byte
-- ah、al、bh、bl...:8 bit,1 Byte
-
-
-
-## 通用寄存器
-
-AX、BX、CX、DX 这4个寄存器通常用来存放一般性的数据,成为通用寄存器(有时候也有特定用途)
-
-通常 CPU 会把内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
-
-例如:在内存中有一个内存空间a值为3,需要将它的值加1,然后将结果存储到内存空间b上。
-
-- `mov ax, a`。CPU 首先会将内存空间 a 的值放到寄存器 ax 中
-
-- `add ax, 1`。调用 add 指令,将 ax + 1
-
-- `mov b, ax`。调用 mov 将 ax 中的值赋值给内存空间 b
-
-
-
-### CS和IP
-
-CS 为代码段,IP 为指令指针寄存器,它们代表 CPU 当前要读取指令的地址
-
- 任意时刻,8086 CPU 都会将 `CS:IP` 指向的指令作为下一条需要取出执行的指令。
-
-IP 只为 CS 提供服务。
-
-8086 CPU 工作过程如下:
-
-- 从 `CS:IP` 指向的内存单元读取指令,读取的指令进入指令缓冲器
-
-- IP = IP + 当前所读取指令的长度。从而指向 IP 指向下一条指令
-
-- 执行指令,转到步骤1,重复执行
-
-在8086CPU 加电启动或者复位后(即 CPU 刚开始工作时)CS 被设置位 CS=FFFFH,IP 被设置为 IP=0000H,即在8086PC 机刚启动时,CPU 从内存 FFFF0H 单元中读取指令执行,FFFF0H 单元中的指令是 8086PC 机开机后执行的第一条指令。
-
-注意:在内存或者磁盘上,指令和数据其实没有差别,都是二进制信息。CPU 在工作时把有的信息看成指令,有些信息看数据,为同样的信息赋予了不同意义。
-
-那么 CPU 根据什么来判断这块内存上的信息是指令还是数据?
-
-- CPU 将 `CS:IP` 所指向的内存单元的内容看作指令
-
-- 如果内存中的某段内容曾被 CPU 执行过,那么它所在的内存单元肯定被 `CS:IP` 指向过
-
-
-
-### jmp 指令
-
-mov 指令不能用于设置 CS、IP 的值,8086没有提供该功能。可以通过 jmp 指令来实现修改 CS、IP 的值,这些指令被成为转移指令。
-
- `jmp 段地址:偏移地址` 可以实现同时修改 CS、IP 的值,表示用指令中给出的段地址修改 CS,偏移地址 IP。
-
-`jmp 2AE3:3` 执行后表示:CS=2AE3H,IP=0003H,CPU 将从2AE33H处读取指令。
-
-QA:下面3条指令执行完毕后,CPU 修改了几次 IP 寄存器?
-
-```shell
-mov ax, bx
-sub ax, ax
-jmp ax
-```
-
-修改了4次。每执行一条指令,IP 都会被修改1次(IP=IP+该条指令的长度),最后一条指令执行后,IP 寄存器的值也会被修改1次,共3+1=4次。
-
-`jmp *%rax` jmp 后面如果跟寄存器地址,则一定要加 `*`,地址存放在 `%rax` 中
-
-
-
-### ds 寄存器
-
-CPU 要读写一个内存单元时,必须要给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成。
-
-8086中有一个 DS 段寄存器,通常用来存放要访问数据的段地址
-
-```shell
-mov bx, 1000H
-mov ds, bx
-mov al, [0]
-```
-
-上面3条指令的意思是将 10000H (1000:0)中的内存数据,赋值到 al 寄存器中。
-
-`mov al, [address]` 的意思是将 DS:address 该地址中的内存数据赋值到 al 寄存器中。
-
-由于 al 是8位寄存器,所以上述命令是将一个字节的数据赋值给 al 寄存器。
-
-tips:8086 不支持将数据直接送入段寄存器,所以 `mov ds, 1000H` 是错误的。
-
-QA:写指令来实现将 al 中的数据写入到内存单元 10000H 中
-
-```shell
-mov ax, 1000
-mov ds, ax
-mov [0], al
-```
-
-QA:内存中有如下数据,写出下面指令执行后寄存器 ax 的值?
-
-| 10000H | 23 |
-| ------ | --- |
-| 10001H | 11 |
-| 10002H | 22 |
-| 10003H | 66 |
-
-```shell
-mov ax, 1000H
-mov ds, ax
-mov ax, [2]
-```
-
-代码分析:
-
-- 第一条指令,ax 寄存器存放了 1000H 这个地址
-
-- 第二条指令,将访问 ds 数据段,在 1000H 这个地址出访问
-
-- 第三条指令,将数据段中 1000H 这个地址处,偏移 2,也就是内存中 10002H 这个的值22写入到 ax 中,由于 ax 寄存器是16位,所以会取2个单位的数据,22和66。所以 ax 的值为2266。
-
-8086 CPU 下,AX、BX、CX、DX 等通用寄存器均被分为高位和低位,AX = AH + AL,其中高位寄存器和低位寄存器。高位和低位都是16位。所以会从10002H 开始取2个16位的数据赋值给 ax。
-
-思考:如果代码改变下呢,如下
-
-```shell
-mov ax, 1000H
-mov ds, ax
-mov al, [2]
-```
-
-此时 al 的值为多少?al 和 ax 的区别在于 ax = ah + al,所以 al 的情况下直接从 10002H 开始取1个16位的数据,所以 al 为 0022。
-
-
-
-### 大小端序
-
-小端序,指的是数据的高字节保存在内存的高地址中,数据的低字节保存在内存的低地址中
-
-大端序,指的是数据的高字节保存在内存的低地址中,数据的低字节保存在内存的高地址中。
-
-注意:这里的大小端序还存在网络大小端序 NBO 和主机大小端序 HBO,详细可以查看我[这篇文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md)
-
-Big Endian:PowerPC、IBM、Sun
-
-Little Endian:x86、DEC
-
-ARM:大小端序模式下均可工作。
-
-16bit 宽的数0x1234 分别在大小端序模式的 CPU 内存存放形式为
-
-| 内存地址 | 小端序 | 大端序 |
-| ------ | ---- | ---- |
-| 0x4000 | 0x34 | 0x12 |
-| 0x4001 | 0x12 | 0x34 |
-
-32bit宽的 0x12345678 分别在大小端序模式的 CPU 内存存放形式为
-
-| 内存地址 | 小端序 | 大端序 |
-| ------ | ---- | ---- |
-| 0x4000 | 0x78 | 0x12 |
-| 0x4001 | 0x56 | 0x34 |
-| 0x4002 | 0x34 | 0x56 |
-| 0x4003 | 0x12 | 0x78 |
-
-QA:将 0x1122 存放在 0x40002 中,如何存储?
-
-分析 0x1122需要2个字节,0x40002 是1个字节,所以肯定需要在 0x40002和向后的一个字节中存储。然后考虑主机序的大小端情况。假设小端模式下:
-
-0x40000
-
-0x40001
-
-0x40002 0x22
-
-0x40003 0x11
-
-
-
-### 指令操作明确 CPU 操作的内存
-
-```shell
-mov ax, 1000H
-mov ds, ax
-mov word ptr [0], 66h
-```
-
-上述代码先把 1000H 写入 ax 寄存器,然后访问数据段的 1000H 内存,然后将66h写入到数据段的0位置,但是 word 告诉了 CPU 需要操作2个字节,也就是 00 66
-
-指令执行前:1000: 0000 11 22 00 00 00 00 00 00
-
-指令执行后:1000: 0000 00 66 00 00 00 00 00 00
-
-如果将第三行代码改为 `mov byte ptr [0], 66h`,意味着明确告诉计算机需要操作1个字节,也就是66 。
-
-指令执行前:1000: 0000 11 22 00 00 00 00 00 00
-
-指令执行后:1000: 0000 66 22 00 00 00 00 00 00
-
-
-
-## 栈
-
-栈是一种后进先出特点的数据存储空间(LIFO)
-
-
-
-- 8086 会将 CS 作为代码段的段地址,将 `CS:IP` 指向的指令作为下一条需要取出执行的指令
-
-- 8060会将 DS 作为数据段的段地址,`mov ax, [address]` 就是取出 `DS:address` 内存区域上的数据放到 ax 寄存器中
-
-- 8086会将 SS 作为栈段的段地址,`SS:SP` 指向栈顶元素
-
-- 8086提供了 PUSH 指令用来入栈,POP 出栈。PUSH ax 是将 ax 的数据入栈,pop ax 是将栈顶的数据送入 ax
-
-SS: 栈的段地址
-
-SP:堆栈寄存器存放栈的偏移地址
-
-
-
-### push
-
-
-
-`push ax` 指令执行,会拆解为:
-
-- `SP = SP-2` ,`SS:SP` 指向当前栈顶前面的单元,更新栈顶指针
-
-- 将 ax 中的数据送入到 `SS:SP` 所指向的内存单元处
-
-ax = ah + al,所以 ax 中的数据入栈需要占据2个单位(sp = sp - 2)
-
-
-
-### pop
-
-
-
-`pop ax` 指令执行,会拆解为:
-
-- 将 `SS:SP` 栈顶指向的内存单元中的数据(2个单位)写入到 ax 寄存器中
-
-- `SP = SP + 2`,更新 `SS:SP` 栈顶的地址
-
-注意:
-
-当一个栈空间是空的时候,`SS:SP` 指向栈空间最高地址单元的下一个单元。
-
-
-
-当一个栈空或者满的时候,执行 PUSH、POP 指令需要注意,因为 `SP = SP + 2`、`SP = SP - 2` 都会导致将错误的数据入栈或者错误的数据出栈,导致发生不可预期的事情。
-
-QA:将10000H~1000FH 这段空间当作栈,初始栈为空,AX = 001AH,BX=001BH,利用栈,交换 AX、BX 中的数据
-
-```shell
-push ax
-push bx
-pop ax
-pop bx
-```
-
-### 段总结
-
-数据段:存放数据的段
-
-代码段:存放代码的段
-
-栈段:将一个段当作栈
-
-对于数据段,将它的段地址放在 DS 中,用 mov、add、sub 等访问内存单元的指令时,CPU 则认为数据段中的内容是数据来访问
-
-对于代码段,将它的段地址存放在 CS 中,将段中的第一条指令的偏移地址放在 IP 中,这样 CPU 就将执行我们定义的代码段的指令(每执行一条指令之前,就会将 IP 的值更新,规则为 IP = IP + 当前指令的长度,以保证该条指令执行完可以根据 段地址 + 偏移地址获取到下条指令的地址)
-
-对于栈段,将它的段地址存放在 SS 中,将栈顶单元的偏移地址存放在 SP 中,这样 CPU 在进行栈操作(LIFO)的时候比如 push、pop 指令,就可以操作 SP,将我们定义的栈段当作栈空间来使用
-
-
-
-## 中断
-
-中断是由于软件或者硬件的信号,使得 CPU 暂停当前的任务,转而去执行另一段子程序。
-
-在程序运行过程中,系统出现了一个必须由 CPU 立即处理的情况,此时,CPU 暂时终止当前程序的执行转而处理这个新情况的过程就叫中断。
-
-中断分为:
-
-- 硬中断(外中断):由外部设备(网卡、硬盘)随机引发的,比如当网卡收到数据包的时候,就会发出一个中断
-
-- 软中断(内中断):由执行中断指令产生,可以通过程序控制触发
-
-汇编中主要指的是软中断,可以通过指令 `int n` 产生中断,其中 `n` 表示中断码,内存中有一张中断向量表,用来存放中断码对应中断处理程序的入口地址。
-
-Demo:写一个打印 Hello World 的汇编代码
-
-```powershell
-; 提醒开发者每个段的定义(增加可读性)
-assume cs:code, ds:data
-;------ 数据段 begin --------
-data segment
- age db 20h
- no dw 30h
- db 10 dup(6) ; 生成连续10个6
- string db 'Hello world!$'
-data ends
-;------ 数据段 end --------
-
-;------ 代码段 begin --------
-code sgement
-start:
- ; 设置 ds 的值
- mov ax, data
- mov ds, ax
-
- mov ax, no
- mov bl, age
-
- ; 打印字符串
- mov dx, offset string ; offset string 代表 string 的偏移地址(将地址赋值给 dx)
- mov ah, 9h
- int 21h ; 打印字符串其实也是一次中断
-
- ; 退出程序
- mov ax, 4c00h
- int 21h
-code ends
-end start
-;------ 代码段 end --------
-```
-
-- start 代表汇编程序的入口
-
-- `mov ax, 4c00h` 和 `int 21h` 代表程序正常中断
-
-QA:“全局变量的地址在编译那一刻就确定好了”怎么理解?
-
-全局变量存放在数据段,我们开发者写的代码存放在代码段,位置不一样,编译期就可以确定全局变量的地址。
-
-
-
-## call 和 ret 指令
-
-实现打印3次 "Hello"
-
-方法1
-
-```powershell
-assume ds:data, ss: stack, cs: code
-; 栈段
-stack segment
-
-ends stack
-
-; 数据段
-data segment
-
-
-ends data
-
-; 代码段
-code segment
-start:
- ; 设置 ds、ss
- mov ax, data
- mov ds, ax
- mov ax, stack
- mov ss, ax
-
- ; 业务逻辑
- ; 打印
- ; ds:dx 告诉字符串地址
- mov dx, offset string
- mov ah, 9h
- int 21h
-
- mov dx, offset string
- mov ah, 9h
- int 21h
-
- mov dx, offset string
- mov ah, 9h
- int 21h
-
- ; 程序正常退出
- mov ax, 4c00h
- int 21h
-code ends
-end start
-```
-
-有没有问题?重复出现2次以及以上,需要封装为函数,汇编也遵循这个原则
-
-方法2
-
-```shell
-assume ds:data, ss: stack, cs: code
-; 栈段
-stack segment
-
-ends stack
-
-; 数据段
-data segment
-
-
-ends data
-
-; 代码段
-code segment
-start:
- ; 设置 ds、ss
- mov ax, data
- mov ds, ax
- mov ax, stack
- mov ss, ax
-
- ; 业务逻辑
- call print
- call print
- call print
-
- ; 程序正常退出
- mov ax, 4c00h
- int 21h
-print:
- ; 打印
- ; ds:dx 告诉字符串地址
- mov dx, offset string
- mov ah, 9h
- int 21h
- ; 函数正常退出
- ret
-
-code ends
-end start
-```
-
-说明:
-
-call 会将下一条指令的偏移地址入栈;会转到标号(print:) 处执行指令
-
-ret 会将栈顶的值出栈,赋值给 `CS:IP` ,ret 即 return
-
-## 函数调用的本质
-
-函数的3要素:参数、返回值、局部变量
-
-
-
-### 返回值
-
-函数运算的结果,一般是放在 ax 通用寄存器中。可以拿 Xcode 将下面的代码执行下,断点开启在 test 方法内的 return 处(Debug - Debug WorkFlow - Always show Disassembly)
-
-```objectivec
-#import
-
-2. 编写汇编代码
-
- ```assembly
- .text
- .global _test
-
- _test:
- movq $0x8, %rax;
- ret;
- ```
-
- 创建一个名为 `test` 的函数,内部给 rax 寄存器赋值为8,然后 ret 返回。
-
- `.text` 是保存在 _TEXT 段上。并将函数暴露给全局,函数名为 test,暴露的时候就要写 _test
-
-3. 汇编函数给外部调用,就需要声明一个头文件 `Asm.h`,写好需要暴露的方法声明
-
-4. 最后在使用的地方,引入暴露的汇编头文件 `Asm.h`,正常调用函数即可。
-
-
-
-
-
-
-
-
-
-## 汇编编写“函数”
-
-上面的例子也顺带看了汇编是如何编写“函数”的,为什么加引号,因为这个概念是不存在的,汇编只有指令,这个函数概念是方便组织代码,参考定义的。类似给一段代码打了个标签。
-
-1. 创建汇编文件
-
-2. 编写代码
-
- ```assembly
- .text
- .global _test, _add, _sub
-
- _test:
- movq $0x8, %rax;
- ret;
-
- _add:
- movq %rsi, %rax
- movq %rdi, %rbx
- addq %rbx, %rax
- retq
- _sub:
- movq %rdi, %rax
- movq %rsi, %rbx
- subq %rbx, %rax
- ret
- ```
-
-
-
- 说明:笔者编写平台是老款 MBP,Xcode 连接模拟器跑的代码,也就是 X86_64 架构的汇编。真机运行一般跑 arm64 汇编语法,会 X86_64 的话 arm64 类似,翻译下写法就好。
-
- 看这2个函数,都是从 `rsi` `rdi` 寄存器里面获取函数参数,内部调用系统指令,做了减加运算逻辑后,将函数返回值保存到 `rax` 寄存器中,直接 return。不需要显示声明 `return rax`,汇编会自动将 `rax` 寄存器里的值,交给函数调用者。
-
-3. 汇编函数给外部调用,就需要声明一个头文件 `Asm.h`,写好需要暴露的方法声明
-
-4. 最后在使用的地方,引入暴露的汇编头文件 `Asm.h`,正常调用函数即可。
-
-
-
-
-
-## iOS 源码探索
-
-经常需要将黑盒的 iOS 代码结合 GNU 之外,还需要将源文件编译成汇编代码去分析。格式为:
-
-`xcrun --sdk iphoneos clang -S -arch arm64 main.m -o main.s`
-
-
-
-## arm64 汇编
-
-### 寄存器
-
-#### 通用寄存器
-
-- 64位: x0~x28
-- 32位:w0~w28(属于 x0~x28 的低32位)
-- x0~x7 经常用来存放函数的参数,更多的函数参数用堆栈来传递
-- x0 经常用来存放函数的返回值
-
-
-
-Demo:汇编定义加减法,OC 去调用
-
-```assembly
-// Asm.s
-.text
-.global _add, _sub
-
-_add:
- add x0, x0, x1
- ret
-_sub:
- sub x0, x0, x1
- ret
-
-// ViewContoller.m
-#import "Asm.h"
-NSInteger sum = add(2, 4) // 6
-NSInteger res = sub(4, 2) // 2
-```
-
-
-
-
-
-#### 程序计数器
-
-pc(Program Counter)
-
-
-
-#### 堆栈指针
-
-- sp(Stack Pointer)
-- fp(Frame Pointer),也就是 x29
-
-#### 链接寄存器
-
-lr(link register),也就是 x30
-
-
-
-#### 程序状态寄存器
-
-- cpsr(Current Program Status Register)
-- spsr(Saved Program Status Register),异常状态下使用
-
-
-
-### 指令
-
-- ret:函数返回
-
-- cmp:将2个寄存器的值相减,结果会影响 cpsr 寄存器的标志位
-
-- b:跳转指令。格式为:`b{条件} 目标地址` 。b 指令是最简单的跳转指令,一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。
-
- 条件跳转一般搭配 cmp 使用。条件跳转对应 `if...else...`
-
- Demo:定义一段汇编代码一个标签,然后跳转执行。跳转前传递参数,跳转后读取并相加
-
- ```assembly
- .text
- .global _jump
-
- _jump:
- movq $0x1, %rsi
- jmp myCode
- myCode:
- movq %rsi, %rax
- movq $0x2, %rbx
- addq %rbx, %rax
- ret
- ```
-
-
-
-
-
- 上面是 x86_64 的汇编,`jmp` 跳转指令在 arm64 中对应 `b` 指令。类似下面代码
-
- ```assembly
- .text
- .global _jump:
-
- _jump:
- // ...
- b myCode
- myCode:
- // ...
- ```
-
- 条件跳转:`bgt conditionJump`
-
- ```assembly
- .text
- .global _jump
-
- _jump:
- mov x0, #0x5
- mov x1, #0x5
- cmp x0, x1
- bgt conditionJump
- conditionJump:
- mov x1, #0x6
- ret
- ```
-
-- bl:带返回值的跳转指令。格式为:`bl{条件} 目标地址`。bl 跳转前,会在寄存器 r14 中保存 pc 的当前内容,因此,可以通过将 r14 的内容重新加载到 pc 中,来返回到跳转指令之后的那个指令处执行。该指令是实现子程序调用的一个基本但常用的手段。在 x86_64 中就是 call 指令。
-
-
-
-
-
-### 条件域
-
-- EQ:equal 相等
-- NE:not equal 不想等
-- GT:great than 大于
-- GE:greater equal 大于等于
-- LT:less than 小于
-- LE:less equal 小于等于
-
-
-
-### 内存操作
-
-- load 从内存中装载数据
-
- - ldr
-
- `ldr x0, [x1]` 代表从地址 x1 处,取8个字节的数据,赋值给 x0(会将 x1 寄存器中存储的内存地址所指向的值加载到 x0 寄存器中)。`ldr w0, [x1]` 代表从地址 x1 处,取4个字节的数据,赋值给 w0。一般会搭配 CPU 寻址能力一起使用。
-
- - ldur
-
- 和 ldr 一样,作用都是从一个寄存器中存储的内存地址所指向的值加载到某个寄存器上。ldr 搭配正数地址,如 `ldr x1, [sp, #0x28]` ,ldur 搭配负数地址,如 `ldur w8, [x29, #-0x8]`
-
- - ldp, `ldp w0, w1, [x2, #0x10]` 代表从 x2 + 0x10 计算结果对应的内存出,取出前4个字节的值赋值给寄存器 w0,后4个字节对应的值赋值给寄存器 w1
-
-- store 往内存中存储数据
-
- - str。`str w0, [x1, #0x5]` 将 w0 寄存器的值赋值给 `x1 + #0x5` 地址开始,4个字节处。str 搭配正数地址偏移
-
- - stur。`str w0, [x1, #-0x5]` 将 w0 寄存器的值赋值给 `x1 - #0x5` 地址开始,4个字节处。stur 搭配正数地址偏移
-
- - stp。`stp w0, w1, [x1, #0x5]` 将 w0 寄存器的值赋值给 `x1 + #0x5` 地址开始,前4个字节处,w1 寄存器的值赋值给后4个字节
-
- - 零寄存器
-
- - wzr(32bit)即 word zero register。
- - xzr(64bit)
-
- ```objective-c
- int a = 0;
- long b = 0;
- ```
-
- 转换为 arm64 汇编就是
-
- ```assembly
- stur wzr, [x29, #-0x14]
- stur xzr, [x29, #-0x24]
- ```
-
-
-
-## 经验小结
-
-- 内存地址格式为:`0x7ab60(%rip)` 一般是全局变量
-- 内存地址格式为:`-0x50(%rbp)` 一般是局部变量
-- 源代码 -> 汇编 -> 机器码,从机器码到汇编是可逆的。但是无法做到汇编到源代码的反编译,因为不同的源代码可能生成的汇编代码是一样的。
-
-
-
diff --git a/Chapter1 - iOS/1.11 b/Chapter1 - iOS/1.11
deleted file mode 100644
index e8c608b..0000000
--- a/Chapter1 - iOS/1.11
+++ /dev/null
@@ -1,2 +0,0 @@
-//print(MemoryLayout.stride(ofValue: str1))
-//print(Mems.memStr(ofVal: &str1))
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.11.md b/Chapter1 - iOS/1.11.md
deleted file mode 100644
index bfe0c99..0000000
--- a/Chapter1 - iOS/1.11.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# iOS中的事件
-
-
-
-* 用户在使用App的时候会产生各种事件
-* 触摸事件、重力加速计事件、远程遥控事件
-* 只有继承自UIResponder才可以响应事件
-* UIView、UIApplication、UIViewController都可以响应事件
-* ## UIResponder
-* UIResponder内部提供了一些方法处理事件
-
-```
-//触摸事件
--(void)touchBegan:(NSSet *)touches withEvent:(UIEvent *)event;
--(void)touchMoved:(NSSet *)touches withEvent:(UIEvent *)event;
--(void)touchEnded:(NSSet *)touches withEvent:(UIEvent *)event;
--(void)touchCanceled:(NSSet *)touches withEvent:(UIEvent *)event;
-
-//加速计事件
--(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
--(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
--(void)motionCanceled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
-
-//远程控制事件
--(void)remoteControlReceivedWithEvent:(UIEvent *)event;
-```
-
-# 事件的产生和传递
-
-* 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中去
-* UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先分发事件给应用程序的主窗口(keyWindow)
-* 主窗口会在视图层次结构中寻找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程中最重要的一步。
-
-
-
-找到合适的视图控件后,就会调用视图控件的touch方法来做具体的事件处理逻辑
-
-
-
-## UIView不接收事件的3种情况
-
-1. 不接收用户交互。view.userInteractionEnabled = NO
-2. 隐藏。view.hidden = YES
-3. 透明度很低。view.alpha = 0.0 ~ 0.01
-
-
-
-注意:UIImageView的userInteractionEnabled默认为NO,因此UIImageView及其它上面的子控件默认是不能接受触摸事件的。
-
-
-
diff --git a/Chapter1 - iOS/1.110.md b/Chapter1 - iOS/1.110.md
deleted file mode 100644
index 504c403..0000000
--- a/Chapter1 - iOS/1.110.md
+++ /dev/null
@@ -1,431 +0,0 @@
-## 妙用设计模式来设计一个客户端校验器
-
-
-> 业务逻辑千变万化,弹窗优先级不断改变,代码冗余问题和难以维护问题如何解决?
-> 本篇文章从设计模式角度出发,讨论责任链设计模式和工厂设计模式2个方式,如何去设计一个校验器,同时解决代码冗余和难以维护的问题
-
-
-## 问题背景
-
-订单在提交的时候会面临不同的校验规则,不同的校验规则会有不同的处理。假设这个处理就是弹窗。
-有的时候会命中规则1,则弹窗1,有的时候同时命中规则1、2、3,但由于存在规则的优先级,则会处理优先级最高的弹窗1。
-
-老的业务背景下,弹窗优先级或者说校验规则是统一的。直接用函数翻译实现,写多个 if 问题不大。
-但在新业务背景下,不同的条件,弹窗优先级不一致,之前的写法需要写大量的嵌套判断,代码难以维护。
-
-所以问题抽象为:如何设计一个校验器
-
-
-为了清晰说明问题,假设线上的弹窗校验规则为:A -> B -> C
-
-```Plain
-typedef NS_ENUM(NSUInteger, OrderSubmitReminderType) {
- OrderSubmitReminderTypeNormal = 0, // 没有命中校验规则
- OrderSubmitReminderTypeA, // 命中校验规则 A
- OrderSubmitReminderTypeB, // 命中校验规则 B
- OrderSubmitReminderTypeC, // 命中校验规则 C
-}
-```
-
-老规则比较简单,不存在不同的校验规则,所以需求可以直接用代码翻译,不需要额外设计
-
-```Shell
-+ (OrderSubmitReminderType)acquireOrderValidateType:(id)params {
- if ([OrderSubmitUtils validateA:params]) {
- return OrderSubmitReminderTypeA;
- }
- if ([OrderSubmitUtils validateB:params]) {
- return OrderSubmitReminderTypeB;
- }
- if ([OrderSubmitUtils validateC:params]) {
- return OrderSubmitReminderTypeC;
- }
- return OrderSubmitReminderTypeNormal;
-}
-```
-
-假设只有2个弹窗条件:是否是 VIP 账户(isVIP)、是否是付费用户(isChargedAccount)。
-
-- isVIP & isChargedAccount: A -> B -> C
-- isVIP & !isChargedAccount:B -> C-> A
-- !isVIP: C -> B -> A
-
-如果直接改,代码就是一坨垃圾了
-
-```Shell
-+ (OrderSubmitReminderType)acquireOrderValidateType:(id)params {
- if (isVIP) {
- if (isChargedAccount) {
- if ([OrderSubmitUtils validateA:params]) {
- return OrderSubmitReminderTypeA;
- }
- if ([OrderSubmitUtils validateB:params]) {
- return OrderSubmitReminderTypeB;
- }
- if ([OrderSubmitUtils validateC:params]) {
- return OrderSubmitReminderTypeC;
- }
- return OrderSubmitReminderTypeNormal;
- } else {
- if ([OrderSubmitUtils validateB:params]) {
- return OrderSubmitReminderTypeB;
- }
- if ([OrderSubmitUtils validateC:params]) {
- return OrderSubmitReminderTypeC;
- }
- if ([OrderSubmitUtils validateA:params]) {
- return OrderSubmitReminderTypeA;
- }
- return OrderSubmitReminderTypeNormal;
- }
- } else {
- if ([OrderSubmitUtils validateC:params]) {
- return OrderSubmitReminderTypeC;
- }
- if ([OrderSubmitUtils validateB:params]) {
- return OrderSubmitReminderTypeB;
- }
- if ([OrderSubmitUtils validateA:params]) {
- return OrderSubmitReminderTypeA;
- }
- return OrderSubmitReminderTypeNormal;
- }
-}
-```
-
-
-
-## 思路
-
-可能有些人会觉得,那不简单,我将不同组合条件下的弹窗抽取为3个方法,照样很简洁
-
-```Shell
-+ (OrderSubmitReminderType)acquireOrderValidateTypeWhenIsVIPAndChargedAccount:(id)params {
- // A->B->C
- if ([OrderSubmitUtils validateA:params]) {
- return OrderSubmitReminderTypeA;
- }
- if ([OrderSubmitUtils validateB:params]) {
- return OrderSubmitReminderTypeB;
- }
- if ([OrderSubmitUtils validateC:params]) {
- return OrderSubmitReminderTypeC;
- }
- return OrderSubmitReminderTypeNormal;
-}
-
-+ (OrderSubmitReminderType)acquireOrderValidateTypeWhenIsVIPAndNotChargedAccount:(id)params {
- // B -> C-> A
- if ([OrderSubmitUtils validateB:params]) {
- return OrderSubmitReminderTypeB;
- }
- if ([OrderSubmitUtils validateC:params]) {
- return OrderSubmitReminderTypeC;
- }
- if ([OrderSubmitUtils validateA:params]) {
- return OrderSubmitReminderTypeA;
- }
- return OrderSubmitReminderTypeNormal;
-}
-
-+ (OrderSubmitReminderType)acquireOrderValidateTypeWhenIsNotVIP:(id)params {
- // C -> B-> A
- if ([OrderSubmitUtils validateC:params]) {
- return OrderSubmitReminderTypeC;
- }
- if ([OrderSubmitUtils validateB:params]) {
- return OrderSubmitReminderTypeB;
- }
- if ([OrderSubmitUtils validateA:params]) {
- return OrderSubmitReminderTypeA;
- }
- return OrderSubmitReminderTypeNormal;
-}
-```
-
-其实不然,问题还是很多:
-
-- 虽然抽取为不同方法,但是每个方法内部存在大量冗余代码,因为每个校验规则的代码是一样的,重复存在,只不过先后顺序不同
-- 存在隐含逻辑。 return 顺序决定了弹窗优先级的高低(这一点不够痛)
-
-
-
-## 方案
-
-那能不能优化呢?有3个思路:责任链设计模式、工厂设计模式、策略模式
-
-策略模式:当需要根据客户端的条件选择算法、策略时,可用该模式,客户端会根据条件选择合适的算法或策略,并将其传递给使用它的对象。典型设计前端 Vue-Validator form 各种 rules
-
-职责链模式:当需要根据请求的内容选择处理器时,可用该模式,请求会沿着链传递,直到被处理,如 Node 洋葱模型
-
-不过目前来看,策略模式被 Pass 了
-
-### 责任链设计模式
-
-责任链模式即 Chain Of Responsibility,属于行为型模式。行为型模式不仅描述对象或类的模式,还描述他们之间的通信模式,比如对操作的处理该如何传递等等。
-
-为什么会有这个思路?
-
-主要来源于2个方向:Node 的洋葱模式、移动端的点击事件传递。
-
-移动端的事件响应模型:点击 view 看看能不能响应,不能响应则继续向上抛,直到抛到 window 为止;
-
-前端 JS 事件冒泡机制:点击事件假设是动态绑定到 DOM 节点上的,浏览器本身不知道哪些地方会处理点击事件,但又要让每层 DOM 拥有对该点击事件的平等处理权,所以就诞生了事件冒泡和组织冒泡的能力 `event.stopPropagation()`
-
-
-
-Node 洋葱模式:发送一个 Request 一层层中间件去处理,比如添加日志、添加请求拦截转发、处理核心业务逻辑、添加日志、添加自定义 response header等,一个中间件层只关注聚焦自己层需要做的事情,处理完继续向下一层抛。
-
-设想下如果没有中间价模型,假设实现一个记录请求事件和自定义 HTTP Header 的需求,业务逻辑 curd 代码和记录请求时间和自定义 Header 代码全都杂糅在一起,难以维护。
-
-责任链的核心就是:**使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。**
-
-- 降低处理者对象之间的耦合度。一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
-
-- 增强了系统的可扩展性。可以根据业务需求增加或者调整新的请求处理类,满足开闭原则(类似维护链表的节点信息)
-
-- 可插拔,增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
-
-- 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
-
-- 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
-
-
-
-采用责任链设计模式。基类 `OrderSubmitBaseValidator` 声明接口,是一个抽象类:
-
-- 有一个属性 `nextValidator` 用于指向下一个校验器
-- 有一个方法 `- (void)validate:(id)params;` 用于处理校验,内部利用模版模式,默认实现是传递给下一个校验器
-
-```Shell
-//.h
-OrderSubmitBaseValidator {
- @property nextValidator;
-
- - (void)validate:(id)params;
- - (BOOL)isValidate:(id)params;
- - (void)handleWhenCapture;
-}
-
-
-// .m
-#pragma mark - public Method
-- (BOOL)isValidate:(id)params {
- Assert(0, @"must override by subclass");
- return NO;
-}
-- (void)validate:(id)params {
- BOOL isValid = [self isValidate:params];
- if (isValid) {
- [self.nextValidator validate:params];
- } else {
- [self handleWhenCapture];
- }
-}
-
-- (void)handleWhenCapture {
- Assert(0, @"must override by subclass");
-}
-```
-
-然后针对不同的校验规则声明不同的子类,继承自 `OrderSubmitBaseValidator`。根据A、B、C 3个校验规则,有:OrderSubmitAValidator、OrderSubmitBValidator、OrderSubmitCValidator。
-
-子类去重写父类方法
-
-```Shell
-OrderSubmitAValidator {
- - (BOOL)isValidate:(id)params {
- // 处理是否满足校验规则A
- }
-
- - (void)handleWhenCapture {
- // 当不满足条件规则的时候的处理逻辑
- displayDialogA();
- }
-}
-```
-
-为了设计的健壮,假设没有命中任何校验规则,需要如何处理?这个能力需要有兜底默认的行为,比如打印日志:`NSLog(@"暂无命中任何弹窗类型,参数为:%@",params);` 也可以由业务方传递
-
-```Shell
-OrderSubmitDefaultValidator *defaultValidator = [OrderSubmitDefaultValidator validateWithBloock:^ {
- SafeBlock(self.deafaultHandler, params);
- if (!self.deafaultHandler) {
- NSLog(@"暂无命中任何弹窗类型,参数为:%@",params);
- }
-}];
-```
-
-初始化多个校验规则
-
-```Shell
-OrderSubmitAValidator *aValidator = [[OrderSubmitAValidator alloc] initWithParams:params];
-OrderSubmitBValidator *bValidator = [[OrderSubmitBValidator alloc] initWithParams:params];
-OrderSubmitCValidator *cValidator = [[OrderSubmitCValidator alloc] initWithParams:params];
-```
-
-不同优先级的校验如何指定:
-
-```Shell
-if (isVIP) {
- if (isChargedAccount) {
- aValidator.nextValidator = bValidator;
- bValidator.nextValidator = cValidator;
- } else {
- bValidator.nextValidator = cValidator;
- cValidator.nextValidator = aValidator;
- }
-} else {
- cValidator.nextValidator = bValidator;
- bValidator.nextValidator = aValidator;
-}
-```
-
-但还是不够优雅,这个优先级需要用户感知。能不能做到业务方只传递参数,内部判断命中什么弹窗优先级组合。所以接口可以设计为
-
-```Shell
-[OrderSubmitValidator validateWithParams:params handleWhenNotCapture:^{
- NSLog(@"暂无命中任何弹窗类型,参数为:%@",params);
-}];
-```
-
-上述方法其实等价于
-
-```Shell
-let validateType = [OrderSubmitValidator generateTypeWithParams:params];
-[OrderSubmitValidator validateWith:validateType];
-```
-
-利用策略模式 `validateWith` 方法内部根据 validateType 去组装 Map 的 key,然后从 Map 中取出具体规则组合,然后依次迭代遍历执行
-
-```
-let rulesMap = {
- isVIP && isCharged : [a-b-c-d],
- isVIP && !isCharged: [a-b-d-c],
- !isVIP: [a-c-d-b],
-}
-```
-这部分策略的生成也可以单独抽取出去,比如 ValidateStrategyFactory 去根据不同的信息,生成不同的策略。
-
-优点:
-
-1. 解决了现在的错误弹窗的隐含逻辑,后续人接手,弹窗优先级清晰可见,提高可维护性,减少出错概率
-2. 对于判断(校验)的增减都无需关心其他的校验规则。类似维护链表,仅在一开始指定即可,符合“开闭原则”
-3. 对于现有校验规则的修改足够收口,每个规则都有自己的 validator 和 validate 方法
-4. 目前弹窗优先级针对 isVIP、isCharged 存在不同优先级顺序,如果按照现有的方案实施,则会存在很多冗余代码
-5. 按照策略模式,不同的校验规则,组装不同的策略,也可以单独抽取出去,独立维护,更清晰
-6. validate 内部按照模版模式,调用 `isValidate` 方法,每个单独的 Validator 不需要额外去调用 next,设计更加健壮,防止别人漏写
-
-
-
-### 工厂设计模式
-
-设计基类
-
-```Shell
-OrderSubmitBaseValidator {
- - (void)validate;
-
- - (BOOL)validateA;
- - (BOOL)validateB;
- - (BOOL)validateC;
-}
-
-- (void)validate {
- Assert(0, @"must override by subclass");
-}
-
-- (BOOL)validateA {
- // 判断是否命中规则 A
-}
-- (BOOL)validateB {
- // 判断是否命中规则 B
-}
-
-- (BOOL)validateC {
- // 判断是否命中规则 C
-}
-```
-
-根据不同的弹窗优先级条件,声明3个不同的子类:`OrderSubmitAValidator`、`OrderSubmitBValidator`、`OrderSubmitCValidator`。各自重写 `validate` 方法
-
-```Shell
-OrderSubmitAValidator {
- - (void)validate {
- [self validateA];
- [self validateB];
- [self validateC];
- }
-}
-
-OrderSubmitBValidator {
- - (void)validate {
- [self validateB];
- [self validateC];
- [self validateA];
- }
-}
-
-OrderSubmitCValidator {
- - (void)validate {
- [self validateC];
- [self validateB];
- [self validateA];
- }
-}
-```
-
-设计工厂类`OrderSumitValidatorFactory`,提供工厂初始化方法
-
-```Shell
-OrderSumitValidatorFactory {
- + (OrderSubmitBaseValidator *)generateValidatorWithParams:(id)params;
-}
-
-+ (OrderSubmitBaseValidator *)generateValidatorWithParams:(id)params {
- if (isVIP) {
- if (isChargedAccount) {
- return [[OrderSubmitAValidator alloc] initWithParams:params];
- } else {
- return [[OrderSubmitBValidator alloc] initWithParams:params];
- }
- } else {
- return [[OrderSubmitCValidator alloc] initWithParams:params];
- }
-}
-```
-
-优点:
-
-- 没有重复逻辑,判断方法都守口在基类中
-- 优先级的关系维护在不同的子类中,各司其职,独立维护
-
-
-最后选什么?组合优于继承,个人倾向使用责任链模式去组织代码。关于责任链设计模式的文章也可以看这篇[文章](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/Chapter6%20-%20Design%20Pattern/6.23.md)
-
-
-
-## 拓展
-
-如果业务真的是高频迭代变化,但校验顺序不变的话,甚至可以做成多端协定后,对应业务校验编号和业务关联,动态下发
-
-```json
-// Version1
-{
- "validatorRuleOrder": ["1", "4", "3", "2"]
-}
-
-// Version2
-{
- "validatorRuleOrder": ["1", "3", "4", "2"]
-}
-```
-
-App 动态请求,然后执行业务逻辑。需思考一些问题:
-
-- 网络请求慢怎么处理?
-- 需不需要缓存?
-- 有缓存的话,更新策略是什么?
-- 需不需要内置的产品逻辑?
-
-当然,这不在本篇文章范畴内,不做展开。
diff --git a/Chapter1 - iOS/1.111.md b/Chapter1 - iOS/1.111.md
deleted file mode 100644
index 13494ab..0000000
--- a/Chapter1 - iOS/1.111.md
+++ /dev/null
@@ -1,364 +0,0 @@
-## 写给 iOSer 的鸿蒙开发 tips
-
-## 下载问题
-
-The other possible cause is that the system language of the PC is English and the region code is US. You could try to perform the following operations to change the region code to CN. Before changing the region code, close DevEco Studio.
-
-
-For Mac OS: ~/Library/Application Support/Huawei/DevEcoStudio3.0/options/country.region.xml
-
-修改为 CN
-
-```xml
-
-
-左侧的 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
-
-如何实现编译时替换?
-
-鸿蒙提供了 Hvigor Plugin 编译时自定义插件,用于实现定制化构建。思路:直接利用 Hvigor Plugin hook 某个编译 task
-
-
-
-那到底 hook 哪个编译 task?
-
-
-
-问题:
-
-- output 修改无效
-
-
-
-
-
- ArkTS 编译之后会产生临时目录,将 ets 编译为 ts。那是不是可以直接修改产物,看看最后能不能影响方舟字节码。
-
- 发现修改了 index.protoBin 、ts 文件,发现最终无法影响编译产物 `*.abc` 文件
-
- 联系了鸿蒙团队的工程师,验证了说是2条并行链路。并不是先编译产生临时文件,再通过临时文件产生 `*.abc` 文件。事实上是2个并行过程。所以此路不通
-
-- input 无法 hook,Hvigor plugin 暂未开放相关能力
-
-
-
- Hvigor 目前开放的能力有限,仅支持在默认的 task 前后加一些钩子函数。但无法修改 input。此路不通
-
-- 既然没法直接修改默认 task,怎么实现?
-
- 思路:copy -> modify -> revert
-
-
-
-
-
- 插桩只能影响产物,不能影响源码。所以先对源码进行备份。
-
- ```ts
- export function HllEntreyPlugin(): HvigorPlugin {
- return {
- pluginId: 'HllEntryPlugin',
- apply: (node: HvigorNode) => {
- node.registerTask({
- name: 'HllEntreyPluginInjectTask',
- run: () => {
- dispatcherToPlugins(node) // copy & modify
- }
- });
- node.registerTask({
- name: 'HllEntryPluginResetTask',
- run: () => {
- resetPluginCodes(originalBackUpFiles) // revert
- },
- dependcies: ['default@CompileArkTS'],
- postDependencies: ['assembleHap']
- })
- }
- }
- }
- ```
-
- 如何修改 AST?
-
- 鸿蒙目前没有提供修改 AST 的能力。如何做?
-
- ArkTS 虽然没有提供 AST 相关 API。但从 CompileArkTS Task 产物来看,ArkTS 最终会编译成 TS。
-
- 且 TS 提供 AST 相关 API。TS 是开源的,TS 开源代码中有关于 AST 的模块。[TS Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)
-
- 可以基于源码中 AST 相关的 API 可以抽取封装一下。
-
- 于是,整个流程就变成了
-
-
-
- 可以利用 [Typescript AST Viewer](https://github.com/dsherret/ts-ast-viewer) 来查看 AST 抽象语法树信息。可以在线查看 AST,官网地址为:[Typescript AST Viewer](https://ts-ast-viewer.com)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-#### Aspect(运行时)
-
-官方在 API 11 开始提供的方案,可快速实现对类方法前后进行插桩或替换。
-
-关键点一:属于可修改-即 addBefore、addAfter、replace 接口的原理,基于 class
-
-
-
-#### AspectPro V1(编译时<正则> + 运行时)
-
-#### AspectPro V2(编译时
-
-- `season = Season.summer`,此时可以看到第一个字节的位置是1.
-
-
-
-- `season = Season.antumn` ,此时可以看到第一个字节的位置是2
-
-
-
-结论:查看内存信息,可以看到**不带关联值、不带原始值的基础枚举,只占1个字节大小空间,且值为默认值**
-
-延伸:对于无关联值、无原始值的简单枚举,Swift 编译器会进行内存优化:
-
-- 当枚举 case ≤ 256 个时,使用 1 字节(UInt8)
-- 当 case ≤ 65536 时,使用 2 字节(UInt16)
-
-
-
-## 只有原始值的枚举
-
-不带关联值、只有原始值的枚举
-
-```swift
-enum Season : Int {
- case spring = 1
- case summer = 2
- case antumn = 3
- case winter = 4
-}
-
-//print(MemoryLayout
-
-- `season = .winter` 基础枚举,当赋值为 winter 的时候,可以看到第一个字节的位置是3
-
-
-
-结论:**只带有原始值的枚举,同样只占用1个字节,该字节的值为枚举的位置索引(比如1、2),而非原始值。原始值不占用枚举的内存**
-
-
-
-## 只带有关联值的枚举
-
-```swift
-enum Season {
- case spring(Int, Int, Int)
- case summer(Int, Int)
- case antumn(Int)
- case winter(Bool)
- case unknown
-}
-
-print(MemoryLayout
-
- 其内存信息如下(8字节为1组,对应上图)
-
- ```shell
- 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个字节,为空。
-
-
-
- 其内存信息如下(8字节为1组,对应上图)
-
- ```shell
- 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个字节,为空。
-
-
-
- 其内存信息如下(8字节为1组,对应上图)
-
- ```shell
- 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,单个 Bool 占1个字节空间,所以红色框代表 winter 的 true,蓝色框为空,绿色框为空,黄色框代表枚举的第4个 case,剩余7个字节,为空。
-
-
-
- 其内存信息如下(8字节为1组,对应上图)
-
- ```shell
- case winter 的关联值的第1个 Int: 01 00 00 00 00 00 00 00
- 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
- 空(按照最大内存预留的位置): 00 00 00 00 00 00 00 00
- 表明哪个 case 的索引值: 03
- 内存对齐占用: 00 00 00 00 00 00 00
- ```
-
-- `season = Season.unknown` 带有关联值的枚举,`unknown` 没有关联值,所以红色框为空,蓝色框为空,绿色框为空,黄色框代表枚举的第5个 case,剩余7个字节,为空。
-
-
-
- 其内存信息如下(8字节为1组,对应上图)
-
- ```shell
- 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
-
-可以看到 enum 只有1个 case 的时候,内存大小只和最大关联值大小有关,1个 case 的情况下不需要额外的空间来判断所属哪个 case。
-
-因此此时枚举的内存大小 = 最大关联值的内存大小 = 8
-
-
-
-## 用汇编验证下内存
-
-```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(Mems.ptr(ofVal: &season))
-season = Season.summer(4, 5)
-season = Season.antumn(6)
-season = Season.winter(true)
-season = Season.unknown
-print("over")
-```
-
-断点停到 `var season: Season = Season.spring(1, 2, 3)` 位置
-
-
-
-将断点处的汇编单独摘出来研究
-
-```assembly
-0x10000334b <+11>: movq $0x1, 0x8eaa(%rip) ; demangling cache variable for type metadata for Swift.Array
-
-实验2:struct 内不自己加 init
-
-```swift
-struct Point {
- var x: Int = 0
- var y: Int = 0
-}
-var point = Point()
-```
-
-在`var point = Point()`处加 断点,如下所示
-
-
-
-现象:可以看到加不加自定义初始化器的汇编代码基本相同。
-
-结论:**如果没有为结构体声明初始化器,编译器会自动生成1个初始化器。目的是保证所有成员都有初始值。**
-
-
-
-## 结构体内存布局
-
-```swift
-struct CustomDate {
- var year: Int
- var month: Int
- var isLeapYear: Bool
-}
-var date = CustomDate(year: 2024, month: 3, isLeapYear: false)
-print(MemoryLayout
-
-乍一看如果不认识的话,先找字面量(立即数),比如红色框内的 `0xa`,就是 10,`0x14` 就是20。[之前](./109.md)学过寄存器的设计,64位寄存器是兼容32位寄存器的。红色框内将 `0xa`,也就是 10 保存到 `%edi ` 寄存器内部,也就是保存到 `%rdi` 中,将 `0x14` 也就是20,保存到 `%esi` 也就是保存到 `%rsi` 寄存器中。
-
-LLDB 模式下输入 `si` 进入 init 方法内部。
-
-
-
-可以查看到将 `%rsi` 里的10 保存到 `%rdx` 中了;将 `%rdi` 里的20 保存到 `%rax` 中了。这也就是 struct `init` 方法做的事情。
-
-LLDB 模式下输入 `finish` 结束 init 方法。
-
-可以看到下面几句汇编
-
-```assembly
-0x1000035ac <+44>: movq %rax, -0x10(%rbp) // rbp - 0x10
-0x1000035b0 <+48>: movq %rdx, -0x8(%rbp) // rbp - 0x8
-0x1000035b4 <+52>: movq %rax, -0x20(%rbp) // rbp - 0x20
-0x1000035b8 <+56>: movq %rdx, -0x18(%rbp) // rbp - 0x18
-0x1000035bc <+60>: movq $0xb, -0x20(%rbp)
-0x1000035c4 <+68>: movq $0x16, -0x18(%rbp)
-```
-
-可以看到分别将 `%rax` 里的10赋值给内存地址为 `%rbp - 0x10` ,`%rdx` 里的20赋值给内存地址为 `%rbp - 0x8` 了。
-
-可与看到 `0x10` 和 `0x8` 地址相差8,且地址连续,也就是 point1 的内存地址。同样下面的 `0x20` 和 `0x18` 地址相差8,且地址连续,也就是 point2 的内存地址。
-
-第五行将 `0xb` 也就是11 赋值给 `%rbp - 0x20`的地址,`0x16` 也就是22赋值给 `%rbp-0x18`的地址,也就是 point2 的 x、y
-
-
-
-### COW 机制
-
-**值类型的赋值操作:Swift 标准库中的 String、Array、Dictionary 和 Set 确实采用了 Copy-On-Write(COW,写时复制) 技术,这是一种内存优化策略,旨在提升性能并减少不必要的内存复制**
-
-核心思想:
-
-- **延迟复制**:当多个变量引用同一份数据时,它们共享底层存储,直到某个变量尝试修改数据时,才会真正复制一份独立的副本。
-- **节省资源**:避免对不可变数据进行冗余复制,减少内存占用和计算开销
-
-仅当有“写”操作时,才会真正执行拷贝操作:
-
-- 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
-- 自定义的值类型,比如结构体,在赋值的时候,就会立马发生深拷贝
-
-举个例子
-
-```swift
-var array1 = [1, 2, 3]
-var array2 = array1 // 此时共享底层存储
-
-array2.append(4) // 触发 COW:array1 和 array2 的存储分离
-```
-
-工作过程:
-
-- 赋值时:`array2` 与 `array1` 共享同一块内存
-- 修改时:当 `array2` 被修改时,检查引用计数。如果引用计数 > 1(即存在多个所有者),则复制底层存储,确保修改不影响其他变量
-
-写操作触发检查机制:
-
-- **修改前检查**:执行写操作(删除、添加、修改)时,检查缓冲区的引用计数
-
-- **唯一性检查**:若引用计数为1,则直接修改缓冲区;否则,复制缓冲区并修改新副本
-
- 伪代码
-
- ```swift
- // 伪代码逻辑
- mutating func append(_ element: Element) {
- if !isUniquelyReferenced(&buffer) {
- buffer = buffer.copy() // 复制缓冲区
- }
- buffer.append(element) // 修改新副本
- }
- ```
-
-#### 什么是缓冲区
-
- Array 结构体(值类型)
- +-------------------+
- | 指向缓冲区的指针 |-----→ Buffer 类(引用类型)
- | | +----------------+
- | 其他元数据(长度、容量) | | 存储元素的内存块 |
- +-------------------+ | [1, 2, 3, ...] |
- +----------------+
-
-1. **结构体轻量级**:
- `Array` 结构体本身只包含一个指针和少量元数据(如长度、容量),占用固定大小(如 8 字节指针 + 8 字节长度 + 8 字节容量 = 24 字节)。
-2. **缓冲区动态分配**:
- 实际存储元素的连续内存块由缓冲区动态分配在堆上,容量可扩展。
-3. **共享与复制**:
- - **赋值时**:仅复制结构体的指针(浅拷贝),多个数组共享同一缓冲区。
- - **修改时**:通过 COW(写时复制)机制,仅在需要时复制缓冲区。
-
-
-
-### 引用类型
-
-引用赋值给 var、let 或者给函数传参,是将内存地址拷贝一份。属于浅拷贝
-
-```swift
-func testReferenceType() {
- class Size {
- var width: Int
- var height: Int
- init(width: Int, height: Int) {
- self.width = width
- self.height = height
- }
- }
-
- var size1 = Size(width: 10, height: 20)
- var size2 = size1
- size2.width = 11
- size2.height = 22
-}
-testReferenceType()
-```
-
-下断点,可以看到下面的汇编:
-
-
-
-在调用(汇编的 call)完 `allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
-
-
-
- 红色框代表类信息的地址,蓝色框代表引用计数,绿色框代表10,黄色框代表20.
-
-汇编的第17行,`movq %rdi, -0x50(%rbp)` 应该就是将 `%rdi` 里面的 对象内存地址赋值到 `%rbp - 0x50` 中去,也就是 `size1` 指针地址。
-
-汇编的第20行,`movq %rdi, -0x10(%rbp)` 应该就是将 `%rdi` 里面的 对象内存地址赋值到 `%rbp - 0x10` 中去,也就是 `size12 指针地址。
-
-再接下去的汇编
-
-```swift
-0x100003525 <+133>: movq -0x50(%rbp), %rax
-0x100003529 <+137>: movq $0xb, 0x10(%rax)
-0x100003531 <+145>: callq 0x100007434 ; symbol stub for: swift_endAccess
-0x100003536 <+150>: movq -0x50(%rbp), %rdi
-0x10000353a <+154>: callq 0x100007476 ; symbol stub for: swift_release
-0x10000353f <+159>: movq -0x68(%rbp), %rdx
-0x100003543 <+163>: movq -0x60(%rbp), %rcx
-0x100003547 <+167>: movq -0x50(%rbp), %rdi
-0x10000354b <+171>: addq $0x18, %rdi
-0x10000354f <+175>: leaq -0x48(%rbp), %rsi
-0x100003553 <+179>: movq %rsi, -0x58(%rbp)
-0x100003557 <+183>: callq 0x100007410 ; symbol stub for: swift_beginAccess
-0x10000355c <+188>: movq -0x58(%rbp), %rdi
-0x100003560 <+192>: movq -0x50(%rbp), %rax
-0x100003564 <+196>: movq $0x16, 0x18(%rax)
-```
-
-可以看到将 `%rbp - 0x50 ` 的值赋值给 `%rax` ,然后将 `oxb` 也就是 11 保存到 `%rax` 也就是 size1 指针的所指向的内存的 `0x10`处,为什么是前面空了16位?因为前8位保存类信息、后8位保存引用计数信息,所以从16位开始。
-
-`movq $0x16, 0x18(%rax)` 将 `0x16` 也就是 22 保存到 `%rax` 也就是 size1 指针的所指向的内存的 `0x18`处,为什么是前面空了24位?因为前8位保存类信息、中间8位保存引用计数信息,后8位保存 Int 的 width,所以从24位开始。
diff --git a/Chapter1 - iOS/1.114.md b/Chapter1 - iOS/1.114.md
deleted file mode 100644
index 5fc2a74..0000000
--- a/Chapter1 - iOS/1.114.md
+++ /dev/null
@@ -1,590 +0,0 @@
-# Swift 闭包研究
-
-## 方法占用对象内存吗?
-
-实验一:
-
- ```swift
-class Point {
- var test = true
- var age = 29
- var height = 175
-}
-var p = Point()
-print(Mems.size(ofRef: p)) // 48
- ```
-
-为什么是48,而不是40?
-
-Point 类前16位的前8位表示类信息,后8位表示引用计算信息,2个 Int 占2*8 = 16 Byte,Bool 占用1 Byte。所以实际占用 8 + 8 + 2 * 8 + 1 = 33 Byte。但由于存在内存对齐(内存对齐以8为 base,都是8的整数倍),但 malloc 函数分配的内存都是 16的倍数,所以占用48 Byte。
-
-
-
-Demo:
-
-```swift
-class Person {
- var age = 29
- func sayHi () {
- var height = 175
- print("局部变量", Mems.ptr(ofVal: &height))
- print("I am \(age) old")
- }
-}
-func sayOuterHi () {
- print("Hello world")
-}
-
-var p = Person()
-p.sayHi()
-sayOuterHi()
-
-print("全局变量", Mems.ptr(ofVal: &p))
-print("堆空间", Mems.ptr(ofRef: p))
-```
-
-
-
-
-
-代码段:Person.sayHi 0x1000034d0
-
-代码段:sayOuterHi 0x1000038e0
-
-全局变量: 0x000000010000c388
-
-堆空间: 0x20c820
-
-局部变量(栈): 0x00007ff7bfeff2f8
-
-可以看到写在类里面的方法地址和写在 Class 外部的方法地址差不多,都在代码段。
-
-结论:方法不占用对象的内存。方法、函数存放于代码段。
-
-
-
-## 闭包
-
-### 定义
-
-什么是闭包?一个函数和它所捕获的变量/常量环境组合起来,称为闭包。
-
-- 一般指定义在函数内部的函数
-- 一般捕获的是外层函数的局部变量、常量
-
-
-
-### 原理窥探
-
-Demo
-
-```swift
-func exec(a: Int, b: Int, fn: (Int, Int) -> Int) {
- print(fn(a, b))
-}
-// 写法1
-func sum(a: Int, b: Int) -> Int { return a + b }
-exec(a: 1, b: 2, fn: sum)
-
-// 写法2:闭包
-exec(a: 1, b: 2, fn: {
- (a: Int, b: Int) -> Int in
- return a + b
-})
-// 写法3:闭包简写
-exec(a: 1, b: 2, fn: {
- a,b in return a + b
-})
-// 写法4:闭包简写
-exec(a: 1, b: 2, fn: {
- a,b in a + b
-})
-// 写法5:闭包简写。用$0、$1来获取参数。
-exec(a: 1, b: 2, fn: { $0 + $1 })
-// 写法5:闭包简写。用 + 来代表操作,让编译器进行推断
-exec(a: 1, b: 2, fn: + )
-```
-
-如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
-
-上面的写法等价于
-
-```swift
-// 写法6:尾随闭包
-exec(a: 1, b: 2) {
- $0 + $1
-}
-```
-
-如果闭包表达式是函数的唯一实参,而且使用饿尾随闭包的语法,那就不需要在函数名后面写圆括号
-
-```swift
-func exec(fn: (Int, Int) -> Int) {
- print(fn(1, 2))
-}
-
-exec(fn: { $0 + $1 }) // 3
-exec() { $0 + $1 } // 3
-exec{ $0 + $1 } // 3
-```
-
-来个 Demo 看看系统数组的排序
-
-```swift
-var array = [1, 8, 9, 12, 32, 2]
-//array.sort()
-func compare(a: Int, b: Int) -> Bool {
- return a < b
-}
-// 写法1
-//array.sort(by: compare)
-
-// 写法2
-// array.sort { $0 < $1 }
-
-// 写法3
-//array.sort { a, b in
-// return a < b
-//}
-
-// 写法4
-//array.sort(by: {
-// (a: Int, b: Int) -> Bool in
-// return a < b
-//})
-
-// 写法5
-//array.sort(by: <)
-
-// 写法6
-array.sort() { $0 < $1 }
-
-print(array) // [1, 2, 8, 9, 12, 32]
-```
-
-
-
-Demo2
-
-闭包的变量捕获
-
-```swift
-typealias Fn = (Int) -> Int
-func getFn() -> Fn {
- var num = 0
- func plus(_ i: Int) -> Int {
- return i
- }
- return plus
-}
-var fn = getFn()
-print(fn(1)) // 1
-print(fn(2)) // 2
-print(fn(3)) // 3
-```
-
-
-
-
-
-简单修改下代码
-
-```swift
-typealias Fn = (Int) -> Int
-func getFn() -> Fn {
- var num = 0
- func plus(_ i: Int) -> Int {
- num += i
- return num
- }
- return plus
-}
-var fn = getFn()
-print(fn(1)) // 1
-print(fn(2)) // 3
-print(fn(3)) // 6
-```
-
-
-
-
-
-可以看到上面有 `allocObject` 方法调用,说明产生了堆空间对象,用于存放 `num` 。是由 `var fn = getFn()` 造成的,调用1次 `getFn` 则产生1次堆空间分配,用于保存 num。
-
-也就是说如果一个内层函数访问了外层函数的局部变量,也就会发生变量捕获,做法就是延长局部变量的生命周期,在堆空间申请一段内存,用户保存局部变量。
-
-对代码进行修改
-
-```swift
-typealias Fn = (Int) -> Int
-func getFn() -> Fn {
- var num = 1
- func plus(_ i: Int) -> Int {
- num += i
- return num
- }
- return plus
-}
-var fn1 = getFn()
-var fn2 = getFn()
-var fn3 = getFn()
-print(fn1(1)) // 2
-print(fn2(2)) // 3
-print(fn3(3)) // 4
-```
-
-我们在汇编 `swift_allocObject` 下面下个断点
-
-
-
-
-
-第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x0000600000210000`。此时还没值。
-
-敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
-
-
-可以看到内存数据发生了改变。绿色框内有了值1。
-
-
-
-第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x00006000002042c0`。此时还没值。
-
-敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
-
-
-第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x000060000020d460`。此时还没值。
-
-敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
-
-
-打印结果也说明了问题,因为调用3次 `getFn()` 所以会在堆上 alloc 3块内存,用于保存捕获的变量。所以调用 fn1 得到 2,调用 fn2 得到 3,调用 fn3 得到 4。
-
-BTW,堆空间分配的内存,如果没有 `init` 或者赋特定的值,数据会是随机的。因为有可能是之前别的地方使用过的内存。
-
-
-
-### 闭包内存结构
-
-先来个简单的函数,看看指针内存结构
-
-```swift
-func sum(_ a: Int, _ b: Int) -> Int { return a + b }
-var fn = sum
-print(fn(1, 2)) // 3
-```
-
-在 `var fn = sum` 处下断点,可以看到下面汇编
-
-
-
-我们知道 `leaq` 是从 `%rip + 0x10f` 算出来的地址,赋值给 `%rax`。所以大概可以推测 `%rip + 0x10f` 就是 `sum` 函数地址。LLDM p 打印 `sum` 地址为 `0x0000000100003a30`。
-
-`%rip` 是下一条指令的地址 `0x100003921`,`%rip + 0x10f` 也就是 `0x0000000100003a30`。和猜想一致。
-
-第六行汇编的意思就是将 `sum` 函数的地址赋值给 `%rax`。
-
-第七行汇编的意思是将保存在 `%rax` 内的地址,从 `%rip + 0x88d8` 处,取8个字节用来保存 `%rax` 的地址。`0x100003928 + 0x88d8 = 0x10000C200`
-
-第八行汇编的意思是将保存在 `%rax` 内的地址,从 `%rip + 0x88d85` 处,取8个字节用来保存 `$0x0` 。`0x100003933 + 0x88d5 = 0x10000C208`
-
-`0x10000C200` 到 `0x10000C208` 差8位,也是连续的。说明分配了一个函数指针,长度为16位。通过`MemoryLayout.stride(ofValue: sum)` 看到也是16位。符合猜想。
-
-
-
-直奔主题,研究闭包内存
-
-
-
-可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么,LLDB 输入 `si`
-
-
-
-可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算,2个 `ecx` 异或,结果为0,写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx`,`edx` 也就是 `rdx`。走完第6行的汇编,继续看第7、8行
-
-
-
-将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 `, 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。
-
-也就是 fn1 前8个字节存放 plus 的函数地址,后8个字节存放0.
-
-
-
-继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包)
-
-
-
-基本可以断定:函数会返回一个长度为16 Byte 的内存。分别保存在 `rax` 和 `rdx`上。所以针对性的研究 `rdx` 和 `rax`
-
-汇编第10行,经过在堆上为捕获的变量 alloc 内存后,将内存保存到 `rax` 中,然后赋值给 `rdi`,第11行,将 `rdi` 再赋值给 `rbp - 0x10` 的地址。所以汇编19行的 `rbp - 0x10 ` 保存的也就是堆内存,赋值给 `rdx ` 了。
-
-20行的 `rax` 保存了一个看似是 `plus` 的函数地址。具体是:`rip + 0x167 = 0x100003969 + 0x167= 0x100003C37 `
-
-
-
-继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。
-
-
-
-问题变得微妙起来了,`getFn` 方法返回一个地址,占用16个字节,但是前8个字节存储 `plus` 方法地址,后8个字节存储闭包捕获后在堆上申请内存存放的数据地址。那如何根据一个地址的前8位去真正调用方法?
-
-
-
-Tips:由于地址是动态生成的,所以真正去调用 plus 的时候一定不是写死的地址,一定是动态生成的。这种属于间接调用。
-
-- call 后面直接加一个函数地址,属于直接调用。类似 `callq 0x100003970`
-- call 后面的函数地址不是固定的,而是动态生成的叫间接调用。在 `at&t` 汇编里面,间接调用前面要加 `*` 。类似 `callq *%rax`,意思是从 `%rax` 里面取出一个地址值出来,当成函数地址,去调用
-
-顺着思路,分析下汇编:
-
-
-
-我们看到25行 `callq *%rax` 存在动态调用,所以需要找到 `%rax` 里的值是哪里来的。然后顺着向上找,找到23行 `movq -0x30(%rbp), %rax`。然后继续向上看到16行 `movq %rax, -0x30(%rbp)`。继续向上看到15行 `movq 0x88db(%rip), %rax`。相当于就是在 `rip + 0x88db ` 处取出8个字节出来,当作函数地址调用(汇编代码的右边写了,`fn1` ),地址为:`0x88db(%rip) = rip + 0x88db = 0x10000392d + 0x88db = 0x10000C208` 。
-
-断点继续放开,在汇编25行处加断点 `callq *%rax`
-
-
-
-可以看到在方法内部,第6行汇编处,直接调用一个代码段的函数地址 `jmp 0x1000039f0 `
-
-指令 `jmp`、`call` 的区别在于:
-
-- `jmp` 指令控制程序直接跳转到目标地址执行程序,程序总是顺序执行,指令本身无堆栈操作过程。
-- `call` 指令跳转到指定目标地址执行子程序,执行完子程序后,会返回 `call` 指令的下一条指令处执行程序,执行 `call` 指令有堆栈操作过程
-
-LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
-
-
-
-
-
-`fn1` 函数调用的时候,参数如何传递?
-
-
-
-汇编17行 `movq 0x88d8(%rip), %r13 ; SwiftDemo.fn1 : (Swift.Int) -> Swift.Int + 8`可以看到将返回的 fn1 本身16字节的后8个字节,也就是堆地址空间值,保存到寄存器 `edi` 也就是寄存器 `rdi` 上了。
-
-汇编24行 `movl $0x1, %edi` 将参数1传给寄存器 `rdi` 了。
-
-然后 LLDB 输入 `si` 去分析 callq 内部
-
-
-
-
-
-可以看到 `movq %r13, %rsi` 将寄存器 r13 的值写入到 `rsi` 了。目前为止:`rdi` 保存参数1,`rsi` 保存堆地址值。
-
-继续输入 `si`
-
-
-
-可以看到汇编的第5行 `movq %rsi, -0x50(%rbp)` 将 `rsi` 里的1,保存到 `rbp - 0x50` 处。第6行汇编 `movq %rdi, -0x58(%rbp)` 将 `rdi` 堆地址值保存到 `rbp - 0x58` 处。
-
-汇编26行 `movq -0x58(%rbp), %rdi` 将 `rbp - 0x58` 的值写入到 `rdi`,也就是堆地址值。第27行 `movq -0x50(%rbp), %rsi` 将 `rbp - 0x50` 的值写入到 `rsi`,也就是参数值1。
-
-然后真正做 `plus` 加法运算的就是28行的 `addq 0x10(%rsi), %rdi`, 从 `rsi` 堆地址值的第16个字节的地方取出8个字节的值,也就是捕获的外部变量 `num` 再和参数1相加。
-
-
-
-可以看到第6行堆地址空间的值写入到 `rbp -0x58` ,第26行又将 `rbp -0x58` 写入到 `rdi`,29行将 `rdi` 的值,写入到 `rbp - 0x48`,34行将 `rbp - 0x48` 写入到 `rcx`,35行将 `rcx` 的地址值,写入到 `rax` 对应的存储空间。也就是堆上的 `num` 值已经修改,被覆盖了。
-
-
-
-总结:当 `getFn` 内部没有发生闭包的时候,fn1 的地址就是16 Byte,前8 Byte就是 `plus` 的函数地址。当发生函数闭包的时候,`fn1` 的16 Byte,前8 Byte 存储间接调用 `plus` 函数的中转函数,后8 Byte 存储着捕获的且在堆上分配内存的地址值。真正调用 `plus` 的时候会通过寄存器传递2个参数:1个是 fn1 函数的参数,1个是堆空间的地址值。
-
-```swift
-var fn1 = getFn()
-fn1(1) // 2
-fn1(3) //4
-```
-
-因为只调用1次 `getFn` 所以堆内存分配了1个被捕获的变量地址,调用过1次 fn1 后,堆地址所指向的内存上的数被修改了。当第二次调用的时候操作的还是同一块堆内存地址,也就是同一个被捕获的堆地址所指向的变量。
-
-```swift
-var fn1 = getFn()
-fn1(1) // 2
-fn1(3) //4
-var fn2 = getFn()
-fn2(2) // 3
-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
- }
-}
-```
-
-
-
-
-
-
-
-## 自动闭包
-
-**自动闭包是一种自动创建的,用来把作为实际参数传递给函数的表达式打包的闭包**。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值
-
-这个语法的好处在于通过写普通表达式代替显示闭包,而使你省略包围函数的形式参数的括号.
-
-比如系统的断言 `assert`
-
-```swift
-public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)
-
-```
-
-
-
-```swift
-// 语法糖。自动闭包
-func getPositiveValue(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
- return v1 > 0 ? v1 : v2()
-}
-
-//print(getPositiveValue(10, {20}))
-//print(getPositiveValue(-10) {20})
-//print(getPositiveValue(-20) {
-// let a = 10
-// return a + 1
-//}
-//)
-
-print(getPositiveValue(-10, 22))
-```
-
-`@autoclosure` 会自动将 22 封装成闭包 `{ 22 }`。
-
-`@autoclosure` 只支持 `() -> T` 无参数,并且有一个返回值的闭包。
-
-`@autoclosure` 并非只支持最后1个参数。
-
-`??` 函数的本质就是自动闭包
-
-
-
-自动闭包允许延迟处理,因此任何闭包内部的代码直到你调用的时候才会运行。对于有副作用或者占用资源的代码来说很有用。因为它可以允许你控制代码何时进行求值
-
-```swift
-var group = ["zhangsan", "lisi", "wangwu"]
-//print(group.count)
-//let groupRemover = { group.remove(at: 0) }
-//print(group.count)
-//
-////print("execute remove function \(groupRemover())")
-//print(group.count)
-
-
-func serve(customer customerProvider: @autoclosure () -> String) {
- print("Now serving \(customerProvider())!")
-}
-serve(customer: group.remove(at: 0))
-// Now serving zhangsan!
-```
-
-如果一个闭包作为参数,是可以去掉 `{}` 的,参数加了 `@autoclosure` 后,是会自动转换为闭包的。
-
-但如果函数参数没有加 `@autoclosure`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的
-
-
-
-正确的做法是:要么加 `@autoclosure` 要么在函数调用的时候加 `{}`
-
-```swift
-// 改法1
-func collectCustomerProviders(_ customerProvider: @escaping () -> String) {
- customerProoviders.append(customerProvider)
-}
-collectCustomerProviders( { group.remove(at: 0) })
-// 改法2
-func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
- customerProoviders.append(customerProvider)
-}
-collectCustomerProviders(group.remove(at: 0))
-```
-
-如果你的自动闭包允许逃逸,就可以同时使用 `@autoclosure` 和 `@escaping `
-
-```swift
-var customerProoviders: [() -> String] = []
-func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
- customerProoviders.append(customerProvider)
-}
-collectCustomerProviders(group.remove(at: 0))
-```
-
-
-
-## 闭包和闭包表达式的区别
-
-### 闭包
-
-定义:闭包是自包含的功能代码块,可以在代码中被传递和使用。它能捕获并存储其所在上下文的常量和变量(即“闭合”环境)
-
-种类:
-
-- 全局函数(有名称,不捕获任何值)
-- 嵌套函数(有名称,可捕获外曾函数的变量)
-- 闭包表达式(匿名,轻量语法,可以捕获上下文变量)
-
-闭包强调的是:捕获上下文的能力。不管是函数还是闭包表达式
-
-### 闭包表达式
-
-定义:是 Swift 中一种简洁的语法格式,用于编写匿名闭包。是闭包的一种实现方式之一(函数和闭包表达式都可以实现闭包)
-
-特点:
-
-- 没有函数名
-- 语法清凉,支持参数类型推断、隐式返回、简写参数名(如 $0、$1)等特性
-- 通常用于作为函数的参数传递
-
-
-
-### 总结
-
-- **闭包**是广义概念,包含所有能捕获上下文的代码块(如函数、闭包表达式)。
-- **闭包表达式**是闭包的一种具体实现方式,专指用轻量语法编写的匿名闭包。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Chapter1 - iOS/1.115.md b/Chapter1 - iOS/1.115.md
deleted file mode 100644
index e59869c..0000000
--- a/Chapter1 - iOS/1.115.md
+++ /dev/null
@@ -1,673 +0,0 @@
-# 属性
-
-## 实例相关属性分类
-
-### 存储属性
-
-英文叫 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
-
-然后断点继续,在 `let diameter = circle.diameter` 处加断点,可以看到调用了 getter 方法
-
-
-
-计算属性的本质就是方法,看上去是属性,但是不占用结构体的内存。而是独立在代码段中,所以只占用1个 Int 即8个字节的大小。
-
-
-
-- **计算属性可以有只读计算属性**
-
- 也就是只有 getter,没有 setter 方法
-
- ```swift
- struct Circle {
- var radius: Int // 存储属性
- var diameter: Int { radius*2 }
- }
- ```
-
-
-
-### 延迟存储属性
-
-常规写法
-
-```swift
-class Car {
- init () {
- print("Car init")
- }
-
- func run () {
- print("Car is running")
- }
-}
-
-class Person {
- let car:Car = Car()
- init () {
- print("Person init")
- }
-
- func goOut() {
- car.run()
- }
-}
-
-
-let p = Person()
-print("---")
-p.goOut()
-
-// console
-Car init
-Person init
----
-Car is running
-```
-
-但是想实现一个需求,就是在 Person 初始化的时候先不初始化 Car,当调用 Person 对象的 goOut 方法的时候再初始化,该怎么办?
-
-**使用 lazy 可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化**
-
-对 Person 改造如下
-
-```swift
-class Person {
- lazy var car:Car = Car()
- init () {
- print("Person init")
- }
-
- func goOut() {
- car.run()
- }
-}
-// console
-Person init
----
-Car init
-Car is running
-```
-
-注意:延迟属性 lazy 必须和 var 搭配使用,不能是 let
-
-
-
-## 异同点
-
-存储属性:
-
-- 类似于成员变量
-
-- 存储在实例的内存中
-
-- 结构体、类可以定义存储属性
-
-- 枚举不可以定义存储属性
-
-- 在创建类、结构体的实例时,必须为所有的存储属性设置一个合适的初始值
-
-- 延迟存储属性必须是 `var`,不能是 `let`。 因为 Swift 规定 let 必须在实例的初始化方法完成之前就拥有值
-
-- `lazy` 在多线程情况下,无法保证属性只被初始化1次。
-
- ```swift
- struct Point {
- var x:Int
- lazy var y = 0
- init(_ x: Int = 0) {
- self.x = x
- }
- }
- var p = Point(2)
- print(p.y)
- ```
-
-- 当结构体包含一个延迟存储属性时,只有 var 才能访问延迟存储属性(因为延迟属性初始化的时候需要改变结构体内存)。Class 的话,实例可以用 let 修饰,访问延迟存储属性是可以的。
-
- QA:为什么结构体包含一个延迟存储属性时,只有 var 才能访问延迟存储属性?
-
- 之前在 [Swift 结构体和类的内存布局](./1.113.md) 探究过 `struct` 的内存布局,`struct` 的成员变量在内存中是连续存储的。由于是延迟存储属性,等真正使用的时候才会执行延迟属性的初始化逻辑,才会更改 `struct `的内存,所以 `let` 无法满足更改内存的需求。
-
- ```swift
- struct Point {
- var x = 0
- lazy var y = 0
- }
-
- let p = Point()
- print(p.y) // Cannot use mutating getter on immutable value: 'p' is a 'let' constant
-
- var p2 = Point()
- print(p2.y) // 0
-
-
- class Point {
- var x:Int
- lazy var y = 0
- init(_ x: Int = 0) {
- self.x = x
- }
- }
-
- let p = Point(2)
- print(p.y) // 0
- ```
-
-计算属性:
-
-- 本质就是方法
-- 不占用实例内存
-- 枚举、结构体、类都可以定义计算属性
-- 计算属性只能用 var,不能用 let
-
-
-
-
-
-## 枚举 rawValue 的原理
-
-枚举原始值 rawValue 的本质:只读的计算属性,不占用实例内存。
-
-```swift
-enum Season: Int {
- case spring = 10
- case summer = 20
- case autumn = 30
- case winter = 40
-}
-
-let season = Season.summer
-// season.rawValue = 22 // Cannot assign to property: 'rawValue' is immutable
-print(season.rawValue)
-```
-
-
-
-通过汇编 `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 不占用枚举的内存空间(是方法,存储在代码段)
-
-
-
-## 属性观察器
-
-- 可以为非 `lazy` 的 `var` 存储属性设置属性观察期
-
-- 计算属性由于有 set 和 get,因此不能有属性观察器 willSet 和 didSet
-
-- 在初始化器中设置属性值不会触发 `willSet`、`didSet`
-
-- 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上
-
- ```swift
- var num: Int {
- get {
- return 10
- }
- set {
- print("newValue", newValue)
- }
- }
- num = 11
- print(num)
- // console
- newValue 11
- 10
-
- func test () {
- var age: Int {
- set {
- print("new age is ", newValue)
- }
- get {
- 28
- }
- }
- age = 29
- print(age)
- }
- test()
- // console
- new age is 29
- 28
- ```
-
-
-
-## Inout 核心原理
-
-### 普通的存储属性
-
-```swift
-struct Shape {
- var width: Int
- var side: Int {
- willSet {
- print("willset side", newValue)
- }
- didSet {
- print("didset side", oldValue, side)
- }
- }
- var girth: Int {
- set {
- width = newValue/side
- print("set girth ", newValue)
- }
- get {
- print("get girth")
- return width * side
- }
- }
- func show() {
- print("width is \(width), side is \(side), girth is \(girth)")
- }
-}
-
-func changeValue(_ value: inout Int) {
- value = 20
-}
-
-var shape = Shape(width: 10, side: 4)
-changeValue(&shape.width)
-shape.show()
-
-// console
-get girth
-width is 20, side is 4, girth is 80
-```
-
-在 `changeValue(&shape.width)` 处加汇编可以看到断点停在第10行 `leaq 0x953c(%rip), %rdi ` 即将 `rip + 0x953c = 0x100002cbc + 0x953c = 0x10000C1F8 ` 赋值给 `rdi`。
-
-第16行也是一样,`leaq 0x9523(%rip), %rdi ` 即将 `rip + 0x9523 = 0x100002cd5 + 0x953c = 0x10000C1F8 ` 赋值给 `rdi`。
-
-
-
-
-
-然后看到17行的关键代码,LLDB 输入 `si`,可以看到在第6行 `movq $0x14, (%rdi)`,将16进制的 `0x14` 也就是20,移动到指定的内存地址 `rdi` 上
-
-
-
-因为 `struct` 结构体内存布局中,成员变量在内存中是连续存储的。所以 `struct `的地址也就是 `struct` 中第一个成员变量 `width` 的地址。
-
-
-
-总结:普通的存储属性,在调用方法的时候,如果参数是 `inout` 传递引用,则直接传递存储属性的地址值即可。在方法内部,对该地址对应的值进行修改即可。
-
-
-
-### 计算属性
-
-对调用的代码进行调整
-
-```swift
-var shape = Shape(width: 10, side: 4)
-changeValue(&shape.girth)
-shape.show()
-// console
-get girth
-set girth 20
-get girth
-width is 5, side is 4, girth is 20
-```
-
-在 `changeValue(&shape.girth)` 处下断点,查看汇编
-
-
-
-核心思路:方法参数用 `inout`修饰,则传递的是引用(内存地址)。
-
-- 汇编19行 `callq 0x1000030e0 ; SwiftDemo.Shape.girth.getter : Swift.Int at main.swift:16` 调用了 `girth` 计算属性的 `getter`,`getter` 的返回值存放在寄存器 `rax` 上
-
-- 20行将 `movq %rax, -0x28(%rbp)` 函数返回值 `rax` 存放在 `main` 函数的栈空间上(虽然没有写调用函数,但是代码主入口就是 `main` 函数),且 `rbp` 到 `rsp` 之间都是函数的栈空间。也就是一个局部变量
-
-- 21行 `leaq -0x28(%rbp), %rdi` 将栈空间上 `-0x28(%rbp)` 的地址值赋值给 `rdi` 寄存器
-
-- 22行 `callq 0x1000037b0 ; SwiftDemo.changeValue(inout Swift.Int) -> () at main.swift:26` 调用 `changeValue` 方法,参数通过寄存器 `rdi` 传递,里面是栈空间 getter 值的地址。
-
-- LLDB 输入 `si` 查看 changeValue 内部。可以看到第6行 `movq $0x14, (%rdi)` 直接将20赋值给 `rdi`
-
-
-
-
-
-- LLDB 输入 `finish` 结束 `changeValue` 细节,查看外部23行汇编 `movq -0x28(%rbp), %rdi` ,将 `main` 函数栈空间上 `getter` 返回值的内存对应的值,保存到寄存器 `rdi` 上。
-
-- 25行 `callq 0x100003250 ; SwiftDemo.Shape.girth.setter : Swift.Int at main.swift:12` 调用计算属性的 `setter`,函数参数为 `rid` 寄存器里的值(也就是20)
-
-总结:带有计算属性的存储属性,如果调用的方法参数是 `inout` 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很多简单,满足了修改值的需求。但是属性观察器的 `set`、`get` 就没办法触发了。所以为了触发属性观察器系统的设计是:
-
-- 第一步:先将传递进去的属性调用 `getter` ,保存在函数的栈地址空间内的某个内存上
-- 第二步:然后调用带有 `inout` 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
-- 第三步:将步骤二得到的值后,调用 `setter` 方法。
-
-这个流程下来,满足了修改值,且触发了原始属性观察器的需求。
-
-
-
-### 带有属性观察器的存储属性
-
-对调用的代码进行调整
-
-```swift
-var shape = Shape(width: 10, side: 4)
-changeValue(&shape.side)
-shape.show()
-// console
-willset side 20
-didset side 4 20
-get girth
-width is 10, side is 20, girth is 200
-```
-
-在 `changeValue(&shape.side)` 处添加断点,查看汇编
-
-
-
-分析:
-
-- 17行 `movq 0x9549(%rip), %rax ; SwiftDemo.shape : SwiftDemo.Shape + 8` 将地址格式为 `0x9549(%rip)` 一个全局变量,也就是 `shape` 的地址 + 8 的值,赋值给 `rax` 寄存器
-
-- 18行 `movq %rax, -0x28(%rbp)` 将寄存器 `rax` 里的值,赋值给 `main` 函数的栈空间上的局部变量 `-0x28(%rbp)`
-
-- 19行 将 `main` 函数的栈空间上的局部变量 `-0x28(%rbp)` 的地址值,赋值给寄存器 `rdi`
-
-- 20行 `callq 0x1000037b0 ; SwiftDemo.changeValue(inout Swift.Int) -> () at main.swift:26` 调用 `changeValue `方法。函数参数通过寄存器 `rdi` 传递,函数内部修改了该内存上的值
-
-- 21行 `movq -0x28(%rbp), %rdi` 将 `main` 函数的栈空间上的局部变量 `-0x28(%rbp)` 的值,赋值给寄存器 `rdi`。也就是修改后的20
-
-- 23行 `callq 0x100002db0 ; SwiftDemo.Shape.side.setter : Swift.Int at main.swift:3` 调用 setter
-
-- LLDB 输入 `si`,可以看到 `setter` 方法内部,调用了 `willSet`、`didSet`
-
-
-
-
-
-总结:带有属性观察器的存储属性,如果调用的方法参数是 `inout` 类型,系统真正的实现,并不是直接将 inout 参数的地址传进去,因为传进去很简单,满足了修改值的需求,但属性观察器的 `willSet`、`didSet` 就没办法触发了。
-
-问题症结是:**直接传递 inout 参数的地址,可以满足直接修改值的需求,但直接修改没办法触发属性观察器的 willSet 和 didSet**
-
-所以为了触发属性观察器系统的设计是:
-
-- 第一步:先将传递进去的地址,保存在函数的栈地址空间内的某个内存上
-- 第二步:然后调用带有 `inout` 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
-- 第三步:将步骤二得到的值后,调用一个 `setter` 方法,`setter` 方法内,先调用 `willSet`,再修改真正的带有观察属性的存储属性的值,最后调用 `didSet` 方法。
-
-这个流程下来,满足了修改值,且触发了原始属性观察器的需求。
-
-
-
-
-
-### 总结
-
-1. 如果实参有内存地址,且没有设置属性观察器和计算属性,实现是直接将实参的内存地址传入带 `inout` 函数
-2. 如果实参是计算属性或者设置了属性观察器:系统采用了 **Copy In Copy Out** 的策略
- - 调用带 `inout` 函数时,先复制实参的值,产生副本 (get。栈空间上的局部变量 rbx + offset)
- - 将副本的内存地址传入带 `inout` 函数(副本进行引用传递),在函数内部修改副本的值
- - 函数返回后,再将副本的值覆盖实参的值(set。willSet、didSet)
-
-
-
-## 类型属性
-
-- 不同于存储属性,类型属性必须设置初始值,永伟类型属性不像存储属性那样有 `init` 初始化器来初始化存储属性
-
-- 类型属性就不存储在每个实例的内存里
-
-- 存储属性默认就是 `lazy`,会在第一次使用的时候才初始化
-
-- 存储属性就算被多个线程同时访问,但系统会保证只初始化1次
-
-- 存储类型属性可以是 `let`
-
-- 存储属性可以用 `static` 修饰
-
-- **枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性**
-
- ```swift
- enum Season {
- static let age: Int = 0
- case spring, summer, antumn, winter
- }
- var season = Season.summer
- ```
-
-- 类型属性的经典场景就是单例模式
-
- ```swift
- class FileManager {
- private init() {}
- public static let sharedInstance: FileManager = FileManager()
- }
-
- var manager1 = FileManager.sharedInstance
- var manager2 = FileManager.sharedInstance
- var manager3 = FileManager.sharedInstance
- print(Mems.ptr(ofRef: manager1)) // 0x0000600000008030
- print(Mems.ptr(ofRef: manager2)) // 0x0000600000008030
- print(Mems.ptr(ofRef: manager3)) // 0x0000600000008030
- ```
-
-
-
-### 内存角度分析:类型属性存储在哪
-
-Demo1:
-
-
-
- `movq $0xa, 0x86d1(%rip) ` num1 的地址为: `rip + 0x86d1 = 0x100003b0f + 0x86d1 = 0x10000C1E0 `
-
-`movq $0xb, 0x86ce(%rip)` num2 的地址为: `rip + 0x86ce = 0x100003b1a + 0x86ce = 0x10000C1E8 `
-
- `movq $0xc, 0x86cb(%rip) ` num3 的地址为: `rip + 0x86cb = 0x100003b25 + 0x86cb = 0x10000C1F0 `
-
-可以看到 `0x10000C1E0` `0x10000C1E8` `0x10000C1F0` 在内存上是连续的,间隔8Byte。可见分配的3个全局变量内存是连续的
-
-
-
-Demo2
-
-
-
-`movq $0xa, 0x8b65(%rip)` num1 的内存为 `rip + 0x8b65 = 0x1000037c3 + 0x8b65 = 0x10000C328 `
-
-可以看到15行将11赋值给 rax,所以直接读取 rax 的地址:`0x000000010000c330`
-
-`movq $0xc, 0x8b38(%rip) ` num1 的内存为 `rip + 0x8b38 = 0x100003800 + 0x8b38 = 0x10000C338 `
-
-可以看到 `0x10000C328` `0x10000c330` `0x10000C338` 也是内存连续的。所以**类型属性就是带有访问控制(必须通过类来访问)的全局变量**
-
-
-
-### 类型属性是线程安全的
-
-看个 Demo
-
-```swift
-class Manager {
- static var count = Int.random(in: 1...100)
-}
-
-Manager.count = 10
-Manager.count = 11
-```
-
-下断点可以看到,调用了**地址访问器函数**。为什么需要地址访问器函数:
-
-- 线程安全初始化首次访问时触发初始化(可能包含 `dispatch_once` 逻辑)
-- 抽象内存访问隐藏实际存储位置(可能位于不同段或延迟分配)
-- 支持属性观察(didSet)通过封装访问点插入回调逻辑
-
-
-
-lldb 输入 si 查看具体实现
-
-
-
-可以看到底层调用了 `swift_once` 函数,函数传递了2个参数, rsi 存储 dispatch_once 的 block 参数,rdi 存储了 onceToken
-
-继续敲 si 可以看到底层调用的就是 GCD 的 `dispatch_once` 函数。
-
-
-
-
-
-类型属性如何保证线程安全的?如何保证只会初始化一次
-
-底层会调用 `swift_once` 进而调用 `dispatch_once_f`,`dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
-
-所以类型存储属性底层通过 dispatch_once 保证了只会初始化1次,线程安全。
-
-
diff --git a/Chapter1 - iOS/1.116.md b/Chapter1 - iOS/1.116.md
deleted file mode 100644
index 67d00bd..0000000
--- a/Chapter1 - iOS/1.116.md
+++ /dev/null
@@ -1,1012 +0,0 @@
-# Swift 类底层剖析
-
-## 类的内存结构
-
-```swift
-class Person {
- var age: Int = 0
-}
-
-class Student: Person {
- var score: Int = 0
-}
-
-class Worker: Student {
- var salary: Int = 0
-}
-
-let person = Person()
-person.age = 28
-print(Mems.size(ofRef: person))
-print(Mems.memStr(ofRef: person))
-
-32
-0x000000010000c400 0x0000000000000003
-0x000000000000001c 0x0000000000000000
-
-let student = Student()
-student.score = 100
-print(Mems.size(ofRef: student))
-print(Mems.memStr(ofRef: student))
-32
-0x000000010000c4b0 0x0000000000000003
-0x000000000000001c 0x0000000000000064
-
-let worker = Worker()
-worker.salary = 1000
-print(Mems.size(ofRef: worker))
-print(Mems.memStr(ofRef: worker))
-48
-0x000000010000c580 0x0000000000000003
-0x000000000000001c 0x0000000000000064 0x00000000000003e8 0x00007ff8501c0938
-```
-
-- 内存对齐都是16 Byte 的整数倍
-- 一个类内存中,至少占16字节的内存。前8位是类信息、其次的8位是引用计数信息,接着是属性内存区域
-- 由于类存在继承,所以子类中,前16字节存储类信息和引用计数信息,其次是属性内存,存在继承的话,前面的属性是父类的属性,后面才是自己的属性。
-
-所以:
-
-- Person 类的内存: 8 Byte 的类信息 + 8 Byte 引用计数信息 + 8 Byte Int Age 属性 = 24 Byte,由于需要16的倍数,所以是32 Byte
-- Student 类的内存: 8 Byte 的类信息 + 8 Byte 引用计数信息 + 8 Byte Int Age 属性 + 8 Byte 的 Int Score 属性 = 32 Byte,由于需要16的倍数,所以是32 Byte
-- Worker 类的内存: 8 Byte 的类信息 + 8 Byte 引用计数信息 + 8 Byte Int Age 属性 + 8 Byte 的 Int Score 属性 + 8 Byte 的 Int Salary 属性 = 40 Byte,由于需要16的倍数,所以是 48 Byte
-
-
-
-## 继承
-
-值类型(枚举、结构体)不支持继承,只有类支持继承
-
-没有父类的类,称为基类。Swift 并不像 OC、Java 那样规定:任何类最终都要继承自某个基类(OC 的 NSObject)。
-
-```swift
-import Foundation
-class Person {}
-class Student: Person {}
-print(class_getSuperclass(Student.self)!) // Person
-print(class_getSuperclass(Person.self)!) // _TtCs12_SwiftObject
-```
-
-丛输出可以看出 Swift 还存在一个隐藏基类:`Swift._SwiftObject`,可查看 [Swift 源码](https://github.com/apple/swift/blob/main/stdlib/public/runtime/SwiftObject.h)
-
-
-
-## 方法
-
-结构体和枚举是值类型,默认情况下,值类型的属性是不能被自身的实例方法修改。
-
-如果想在方法内修改,需要在 `func` 前加 `mutating` 才可以
-
-```swift
-struct Point {
- var x: Double = 0.0
- var y: Double = 0.0
- func moveBy(_ delatX: Double, _ delatY: Double) {
- self.x += delatX
- self.y += delatY
- }
-}
-var point = Point()
-point.moveBy(0.2, 0.2)
-// compiler error
-Left side of mutating operator isn't mutable: 'self' is immutable
-```
-
-改进
-
-```swift
-struct Point {
- var x: Double = 0.0
- var y: Double = 0.0
- mutating func moveBy(_ delatX: Double, _ delatY: Double) {
- self.x += delatX
- self.y += delatY
- }
-}
-var point = Point()
-point.moveBy(0.2, 0.4)
-print(point.x, point.y)
-// 0.2 0.4
-```
-
-
-
-## 重写方法
-
-`override`
-
-被 class 修饰的类型方法、下标,允许被子类重写
-
-被 static 修饰的类型方法、下标,不允许被子类重写
-
-```swift
-class Animal {
- static var innerValue:Int = 0
- class func speak() {
- print("Animal speak")
- }
-
- class subscript(index: Int) -> Int {
- set {
- innerValue = newValue
- }
- get {
- innerValue
- }
- }
-}
-
-class Dog: Animal {
- override class func speak() {
- super.speak()
- print("dog is bark")
- }
- override class subscript(index: Int) -> Int {
- set {
- innerValue = newValue
- }
- get {
- innerValue
- }
- }
-}
-
-Animal.speak() // Animal speak
-Animal[5] = 3
-print(Animal[5]) // 3
-Dog.speak() // Animal speak dog is bark
-```
-
-但如果将 `Animal` 方法的 `class` 改为 `static`,就无法 `override` 了
-
-
-
-
-
-
-- 如果父类的方法是被 class 修饰的,子类继承后重写时,可以将 class 改为 static。
-- 虽然子类可以将父类方法的 class 改为 static。但影响的是当前子类的子类,无法再重写方法了。
-
-
-
-## 重写属性
-
-- 子类不可以将父类的属性改写为存储属性
-- 子类可以将父类的属性(存储属性、计算属性)重写为计算属性
-- 只能重写 var 属性,不能重写 let 属性
-- 重写时,属性名、类型要一致
-- 子类重写后的属性权限(读写),不能小于父类属性的权限
- - 如果父类属性是只读的,子类重写后的属性要么是只读的,要么是可读可写的
- - 如果父类的属性是可读可写的,子类重写后的属性也必须是可读可写的
-
-
-
-## 重写类型属性
-
-- 被 class 修饰的计算类型属性,可以被子类重写
-- 被 static 修饰的类型属性(存储、计算),不可以被子类重写
-- 可以在子类中为父类属性(除了只读的计算属性、let 属性)增加属性观察器
-
-```swift
-class Shape {
- var radius: Int = 1 {
- willSet {
- print("Shape will set radius", newValue)
- }
- didSet {
- print("Shape did set radius", oldValue, radius)
- }
- }
-}
-class Circle: Shape {
- override var radius: Int {
- willSet {
- print("Cirle will set radius", newValue)
- }
- didSet {
- print("Circle did set radius", oldValue, radius)
- }
- }
-}
-var circle = Circle()
-circle.radius = 2
-// console
-Cirle will set radius 2
-Shape will set radius 2
-Shape did set radius 1 2
-Circle did set radius 1 2
-```
-
-可以看到输出类似 Node 的洋葱模型,willset 从外到里,didset 从里到外。
-
-
-
-## final
-
-- 被 final 修饰的方法、属性、下标是禁止被重写的
-
-- 被 final 修饰的类,禁止被继承
-
-
-## Swift 协议(Protocol)中声明的属性必须使用 var 关键字
-
-协议的核心目标:定义“能力”而非“实现”
-协议是描述类型应该具备什么能力的抽象蓝图,而不是具体实现。
-属性在协议中本质上定义的是对外的访问接口(读、写),而不是存储方式(常量或变量)。
-因此,**协议中的属性声明必须明确其访问权限({ get } 或 { get set }),而 var 是唯一能表达这种动态性的关键字**。
-
-- 协议中的属性用 var:统一表示“访问接口”,支持动态约束({ get } 或 { get set })。
-- 遵循类型可用 let 或 var:只要满足协议的访问权限要求即可。
-- let 无法用于协议:因其无法表达可写性,违背协议动态描述能力的初衷。
-
-
-
-
-## 多态的实现原理
-
-- OC: Runtime
-- C++:虚函数表
-- Swift:没有 Runtime,所以多态的实现类似 C++
-
-来个 Demo
-```swift
-class Animal {
- func speak () {
- print("Animal speak")
- }
- func eat () {
- print("Animal eat")
- }
- func sleep () {
- print("Animal sleep")
- }
-}
-
-class Dog: Animal {
- override func speak() {
- print("Dog speak")
- }
- override func eat() {
- print("Dog eat")
- }
- func run () {
- print("Dog run")
- }
-}
-
-var animal = Animal()
-animal.speak()
-animal.eat()
-animal.sleep()
-
-animal = Dog()
-animal.speak()
-animal.eat()
-animal.sleep()
-// console
-Animal speak
-Animal eat
-Animal sleep
-Dog speak
-Dog eat
-Animal sleep
-```
-
-在 `animal.speak()` 处加断点,可以看到
-
-
-
-
-
-解释:
-
-- 汇编84行 `movq 0x9356(%rip), %r13 ` 是将全局变量 `animal` 的地址赋值给 `r13`
-- 汇编90行 `movq (%r13), %rax` 将 `r13` 处取出内存的前8个字节,赋值给 `rax`
-- 汇编91行 `callq *0x50(%rax)` ,也就是计算出 `rax + 0x50` 的地址,然后取出8 Byte 出来,也就是 `Dog.speak` 然后调用
-- 汇编107行 `callq *0x58(%rax)` ,也就是计算出 `rax + 0x508` 的地址,然后取出8 Byte 出来,也就是 `Dog.eat` 然后调用
-- 汇编123行 `callq *0x60(%rax)` ,也就是计算出 `rax + 0x60` 的地址,然后取出8 Byte 出来,也就是 `Animal.sleep` 然后调用
-
-
-
-画了张图,也就是说 `rax` 中存放了 Dog 对象内存中的前8个字节,也就是下图的最右侧
-
-
-
-
-
-核心是上面的内存布局图。结合汇编就知道多态是如何实现的。
-
-1. Swift 多态的实现原理
-
-Swift 的多态通过 虚函数表(vtable) 实现,这是一种 编译时确定的动态派发机制。其核心逻辑是:
-
-- 每个类类型在编译时会生成一个 虚函数表,表中存储了类的方法实现指针
-- 子类继承父类时,会复制父类的虚函数表,并替换重写方法的指针为自己的实现
-- 在运行时,通过对象的 类型元数据指针 找到对应的虚函数表,从而调用正确的方法
-
-动态派发与静态派发的区别:
-- 动态派发:通过虚函数表实现(例如普通类方法),允许子类重写。
-- 静态派发:编译时直接绑定方法地址(例如 final 方法、static 方法、结构体和枚举的方法),性能更高
-
-2. 虚函数表(vtable)的作用
-
-虚函数表的核心作用是为 动态派发 提供支持:
-- 方法重写:子类通过覆盖虚函数表中的方法指针,实现多态。
-- 运行时方法查找:对象调用方法时,通过虚函数表找到实际的方法实现。
-- 类型安全性:保证方法调用的正确性,即使对象被向上转型(例如 父类引用 = 子类对象)。
-
-
-总结: **虚函数表**(vtable)是一种用于实现动态多态性的机制,通常用于面向对象的编程语言中(C++ 也是一样)。在 Swift 中,虚函数表用于存储类或协议中方法的地址,以便在运行时进行动态分派。
-
-在 Swift 中,虚函数表的作用是为每个类或协议创建一个表,其中包含了对应方法的地址。当调用对象的方法时,运行时系统会根据对象的实际类型查找对应的虚函数表,然后调用表中存储的方法地址,从而触发特定的实现。
-
-虚函数表在 Swift 中的作用是实现动态分派,使得在运行时根据对象的实际类型确定调用的具体实现。这为 Swift 中的多态性提供了基础,允许相同的方法名称根据对象的类型触发不同的实现,从而实现灵活的对象行为。
-
-最小内存占用:一个没有属性的类对象至少占用 16 字节(类型元数据指针 8 字节 + 引用计数 8 字节)。
-属性存储:属性从第 17 字节开始存储
-引用计数细节:
-- 默认情况下,引用计数直接存储在对象头部。
-- 当引用计数溢出时,Swift 会使用 Side Table 扩展存储,此时对象头部的引用计数字段会指向 Side Table。
-
-
-## 类的类型信息存储在哪
-
-说明:同一个类的不同对象,它的类信息是一样的。也就是说不通的对象指针,所指向的类信息内存是同一块。
-
-```swift
-var dog1 = Dog()
-var dog2 = Dog()
-```
-
-存储在全局区。可以利用 MachOView 去查看。
-
-
-
-## 初始化器
-
-### 初始化器可以继承
-- convenience 便捷初始化器只可以横向调用,不可以纵向调用(比如子类继承父类后,子类重写指定初始化器的时候,必须加 override 且子类中只能调用父类的指定初始化器,不能调用便捷初始化器)
-- 便捷初始化器是不能被子类调用的
-
-
-### 自动继承
-- 如果子类没有自定义任何指定初始化器,则会自动继承父类所有的指定初始化器
-
-
-### require
-
-- 用 required 修饰的指定初始化器,表明其所有的子类都必须实现该初始化器(通过继承或者重写来实现)
-- 如果子类重写了 required 初始化器,也必须加上 required,不用加 override
-
-
-
-### 可失败初始化器
-
-类、结构体、枚举都可以使用 `init?` 定义可失败初始化器,也可以用 `init!` 来定义可失败初始化器。区别下面会讲
-
-```swift
-class Person {
- var name: String
- init?(_ name: String) {
- if name.isEmpty {
- return nil
- }
- self.name = name
- }
-}
-
-var person1 = Person("")
-print(person1) // nil
-var person2 = Person("FantasticLBP")
-print(person2) // Optional(SwiftDemo.Person)
-print(person2!) // SwiftDemo.Person
-```
-
-这种设计系统中也存在,比如 Int 的可失败初始化器:`@inlinable public init?(_ description: String)`
-
-```swift
-var num = Int("12e2")
-print(num) // nil
-num = Int("12")
-print(num) // Optional(12)
-```
-
-注意点:
-
-1. 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器。因为在外部调用的时候,不知道到底是使用哪个初始化方法。编译器会报错 `Invalid redeclaration of 'init(_:)'`
-
-
-
-2. 可以用 `init!` 来定义隐式解包的可失败初始化器
-
-3. 可失败初始化器可以调用非可失败初始化器,非可失败初始化器调用可失败初始化器需要进行解包。如果直接调用会报错 `A non-failable initializer cannot delegate to failable initializer 'init(_:)' written with 'init?'`
-
- ```swift
- class Person {
- var name: String
- init?(_ name: String) {
- if name.isEmpty {
- return nil
- }
- self.name = name
- }
- convenience init() {
- self.init("")! // 极端 case,设计不合理
- }
- }
- ```
-
- 非可失败初始化器也可以调用可失败初始化器的隐式解包。
-
- ```swift
- class Person2 {
- var name: String
- init!(_ name: String) {
- if name.isEmpty {
- return nil
- }
- self.name = name
- }
- convenience init() {
- self.init("")
- }
- }
- ```
-
-
-
- 且前面的写法比较危险,假设第一个 `init?` 返回 `nil`,第二个 `convenience init()` 去对 nil 强制解包,则会 crash
-
-4. 可以用一个非可失败初始化器重写一个可失败初始化器,但反过来不行
-
-5. 如果初始化器调用一个可失败初始化器导致初始化失败,那么整个初始化过程都失败,并且之后的代码都停止执行
-
- ```swift
- class Person {
- var name: String
- init?(_ name: String) {
- if name.isEmpty {
- return nil
- }
- self.name = name
- }
- convenience init?() {
- self.init("")
- print("我是后面的代码1")
- print("我是后面的代码2")
- }
- }
-
- var person1 = Person()
- print(person1)
- ```
-
- `init` 初始化失败,后面的 `我是后面的代码1` 均不会执行
-
-### 可失败初始化器设计哲学
-
-- 安全性优先:Swift 注重安全性,可失败初始化器的设计使得对象的初始化过程更加可靠和安全。通过返回一个可选值来表示初始化成功或失败,可以避免在初始化失败时产生不确定的对象状态
-- 错误处理:可失败初始化器与 Swift 的错误处理机制结合使用,使得在初始化失败时能够更好地捕获和处理错误。这种设计哲学强调了对异常情况的处理和错误信息的传递。
-- **灵活性**:可失败初始化器提供了一种灵活的初始化机制,允许开发者更加精确地控制对象的初始化过程。这种设计哲学使得对象初始化更加灵活和可定制。
-
-
-
-### 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()`。
-
-## 可选链
-
-```swift
-var dict:[String: (Int, Int) -> Int] = [
- "sum": (+),
- "minus": (-),
- "multiple": (*),
- "divide": (/)
-]
-print(dict["sum"]) // Optional((Function))
-var result = dict["divide"]?(40, 20) // 2
-print(result!)
-```
-
-- 如果可选项为 nil,调用方法、下标、属性失败,结果为 nil
-- 如果可选项不为 nil,调用方法、下标、属性成功,结果会被包装为可选项
-- 如果结果本来是可选项,则不会进行再次包装
-- 如果链中任何一个节点为 nil,那么整个链就会调用失败。`var weight = person?.dog?.weight // Int?`
-- 多个 `?` 可以链接在一起 `var weight = person?.dog?.weight`
-
-
-
-## 可选项 Optional 的本质
-
-可选项的本质是 **enum 类型 + 泛型**
-
-```swift
-@frozen public enum Optional
-
-在第二行代码下断点,可以看到关键的汇编是第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` 结构类似下图右侧
-
-
-
-
-
-`X.self` 和 `type(of:x)` 效果等价
-
-```swift
-class Person { }
-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 {
- required init() {}
-}
-class Worker: Person {}
-class Student: Person {}
-func createInstance(_ items: [Person.Type]) -> [Person] {
- var people:[Person] = Array
-
-查表是一种简单、易实现、性能可预知的方式。然而,这种派发方式比起直接派发来说,还是慢了一点(从字节码的角度来看,多了两次读和一次跳转。由此带来了性能损耗)。另一个慢的原因在于编译器可能会由于函数内执行的任务,导致无法优化(如果函数带有副作用的话)
-
-这种基于数组的实现,缺陷在于函数表无法拓展。子类会在虚函数表的最后插入新函数,没有位置可以让 extension 安全地插入函数。
-
-
-
-#### 消息机制派发(Message Dispatch)
-
-消息机制是调用函数最动态的方式,也是 Cocoa 的基石,催生了 KVO、UIAppearance、CoreData 等,这种运作方式的关键在于开发者可以在运行时改变函数的行为。不止可以通过 swizzling 来改变,甚至可以用 isa-swizzling 修改对象的继承关系,可以在面向对象的基础上实现自定义派发。
-
-
-
-### OC 运行时
-
-主要体现在
-- 动态类型(dynamic typing)
-- 动态绑定(dynamic binding)
-- 动态装载(dynamic loading)
-
-
-
-### Swift 运行时
-- 纯 Swift 类的函数调用已经不再是 Objective-C 的运行时发消息,而是类似 c++ 的虚表 vtable,在编译时就确定了调用哪个函数,所以没办法通过 runtime 获取方法、属性
-- 而 Swift 为了兼容 Objective-C,凡是继承自 NSObject 的类都会保留其动态性,所以能够通过 runtime 拿到方法。老版本的 swift(如2.2)是编译期隐式的自动帮你加上了 `@objc`,而4.0以后版本的 swift 编译期去掉了隐式特性,必须显示声明
-- 不管是 Swift 类,还是继承自 NSObject 的类,只要在属性和方法前面加 `@objc` 关键字,就可以使用 runtime
-
-
-
-| | 原始定义 | 拓展 |
-| -------------------- | ---------- | ---------- |
-| 值类型 | 直接派发 | 直接派发 |
-| 协议 | 函数表派发 | 直接派发 |
-| 类 | 函数表派发 | 直接派发 |
-| 继承自 NSObject 的类 | 函数表派发 | 函数表派发 |
-
-
-
-- 值类型总是会使用直接派发,简单易懂
-- 协议和类的 extension 都会使用直接派发
-- NSObject 的 extention 会使用消息机制进行派发
-- NSObject 声明作用域的函数都会使函数表进行派发
-- 协议里声明的,并且带有默认实现的函数会使用函数表进行派发
-
-
-
-修饰符
-
-| final | 直接派发 |
-| ---------------- | ---------------------- |
-| dynaminc | 消息机制派发 |
-| @objc & @nonobjc | 改变在 oc 里的可见性 |
-| @inline | 告诉编译器可以直接派发 |
-
-
-
-有个特殊的组合 final 和 @objc。在标记为 final 的同时,也可以使用 @objc 来让函数可以使用消息机制派发。这么做的结果就是,调用函数的时候会使用直接派发,但也会在 Objective-C 的运行时里注册对应的 selector,函数可以响应 `perform(selector:)` 以及别的 Objective-C 特性,但在直接调用时,又可以有直接派发的性能。
-
diff --git a/Chapter1 - iOS/1.117.md b/Chapter1 - iOS/1.117.md
deleted file mode 100644
index a02889d..0000000
--- a/Chapter1 - iOS/1.117.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# Swift 协议探究
-
-- 协议可以用来定义属性、方法、下标的声明,协议可以被类、枚举、结构体遵守(多个协议用逗号隔开)
-
-- 协议中定义的方法不能有默认参数值
-
-- 协议中定义属性必须是 `var`
-
-- 实现协议时定义的属性权限,要不小于协议中定义的属性权限
-
-- 协议定义属性是 `get`、`set` 时,用 `var` 存储属性或者 `get`、`set` 计算属性实现
-
-- 为了保证通用,协议中必须用 `static` 定义类型方法、类型属性、类型下标
-
-- 只有将协议中的实例方法标记为 `mutating`
-
- - 才可以允许结构体、枚举对象在方法里修改自身内存。否则编译器会报错:`Cannot assign to property: 'self' is immutable`
- - 类遵循协议,实现方法的时候不用加 `mutating`,枚举、结构体的实现需要加 `mutating`
-
-
-
- ```swift
- protocol Drawable {
- func draw()
- }
-
- class Size: Drawable {
- var width: Int = 0
- func draw() {
- width = 10
- }
- }
- var size = Size()
- print(size.width) // 0
- size.draw()
- print(size.width) // 10
-
- struct Point: Drawable {
- var x : Int = 0
- var y: Int = 0
- func draw() {
- x = 10 // Cannot assign to property: 'self' is immutable
- y = 10 // Cannot assign to property: 'self' is immutable
- }
- }
- ```
-
- 要想修改需要加 `mumating`
-
- ```swift
- struct Point: Drawable {
- var x : Int = 0
- var y: Int = 0
- mutating func draw() {
- x = 10
- y = 10
- }
- }
- var point = Point()
- print(point.x, point.y)
- point.draw()
- print(point.x, point.y)
- ```
-
-- 协议中还可以定义初始化器 `init`,非 `final` 类实现协议时, `init` 方法必须加 `required`
-
- ```swift
- protocol Drawable {
- init(x: Int, y: Int)
- }
- class Point: Drawable {
- var x: Int = 0
- var y: Int = 0
- required init(x: Int, y: Int) {
- self.x = x
- self.y = y
- }
- }
- final class Size: Drawable {
- var x: Int = 0
- var y: Int = 0
- init(x: Int, y: Int) {
- self.x = x
- self.y = y
- }
- }
-
- var point = Point(x: 10, y: 20)
- print(point.x , point.y) // 10 20
- var size = Size(x: 30, y: 40)
- print(size.x , size.y) // 30 40
- ```
-
-- 如果协议声明了初始化器,某个类遵循协议并实现了初始化器。且该初始化器也恰好是父类指定初始化器,那么这个初始化必须同事加 `required` 和 `override`
-
- ```swift
- protocol Drawable {
- init(x: Int, y: Int)
- }
-
- class Shape {
- init(x: Int, y: Int) {}
- }
-
- class Circle: Shape, Drawable {
- var x: Int = 0
- var y: Int = 0
- required override init(x: Int, y: Int) {
- super.init(x: x, y: y)
- self.x = x
- self.y = y
- }
- }
- var circle = Circle(x: 10, y: 20)
- print(circle.x , circle.y) // 10 20
- ```
-
-- 协议也可以继承
-
-- 协议也可以组合
-
- ```swift
- protocol Drawable {}
- protocol Colorable {}
- func test1(obj: Shape) {} // 参数接收 Shape 类或者 Shape 类的子类
- func test2(obj: Drawable) {} // 参数接收遵循 Drawable 的实例
- func test3(obj: Drawable & Colorable) {} // 参数接收同时遵循 Colorable 和 Drawable 2个协议的实例
- func test4(obj: Shape & Drawable & Colorable) {} // 参数接收同时遵循 Colorable 和 Drawable 2个协议,且是 Shape 的子类的实例
- ```
-
-- 遵循 `CustomStringConvertible` 可以自定义打印的字符串内容
-
- ```swift
- class Person: CustomStringConvertible {
- var name: String
- var age: Int
- init(name: String, age: Int) {
- self.name = name
- self.age = age
- }
- var description: String {
- "My name is \(name), age is \(age)"
- }
- }
- var p = Person(name: "杭城小刘", age: 28)
- print(p) // My name is 杭城小刘, age is 28
- ```
-
-
-
-## Any、AnyObject
-
-Swift 提供了2种特殊的类型:Any、AnyObject
-
-- Any 可以代表任意类型(枚举、结构体、类、函数类型)
-- AnyObject:代表任意类类型。比如可以在协议后面加上 AnyObject 则代表只有类能遵循这个协议。编译器会做检查 `Non-class type 'point' cannot conform to class protocol 'Eatable'`
-
- ```swift
- protocol Eatable: AnyObject {}
- class Person: Eatable { }
- struct point: Eatable {} // Non-class type 'point' cannot conform to class protocol 'Eatable'
- ```
-
-
-
-## 关联类型
-
-关联类型的作用:给协议中用到的类型,定义一个占位名称
-
-协议中可以拥有多个关联类型
-
-```swift
-protocol Stackable {
- associatedtype Element
- mutating func push(_ element: Element)
- mutating func pop() -> Element
- func top() -> Element
- func size() -> Int
-}
-
-class Stack
-
-可以看到:
-
-- 在第一处调用 `swapValue ` 方法的时候,将8字节的 metadata 信息保存到 `rdx` 寄存器了。也就是在调用 `swapValue` 方法的时候,分别将 `i1`(0x000000000000000b,也就是11)的地址值赋值给 rdi 寄存器,将 `i2`(0x0000000000000016,也就是22)的地址值赋值给 rsi 寄存器
-- 将 `Int` 的 `metadata` 赋值给 `rdx` 寄存器
-- 然后调用 `swapValue` 方法
-- 后续的 `String` 的 `SwapValue` 过程类似
-
-所以编译器最后在执行的时候,会将泛型真正的类型对应的 metadata 信息当作函数参数,传递进去,再去执行函数
-
-
-
-## 泛型类型约束
-
-泛型必须遵循协议,可以在方法后加 `<>`,在 `<>` 内写泛型 `T: 需要继承的类 & 协议` (也可以是其他名字,大多数语言都写 T),
-
-```swift
-protocol Runable {}
-class Person {}
-func swapValue
-
-简单分析下:
-
-- 第4行 `leaq 0x3d45(%rip), %rdi` 将 `rip + 0x3d45` 计算出的地址值赋值给寄存器 `rdi`
-- 第5行 `movl $0xa, %esi` 将 10 赋值给寄存器 `esi`,也就是 `rsi`
-- 第6行 `movl $0x1, %edx` 将 1 赋值给寄存器 `edx`,也就是 `rdx `
-- 第7行 `callq 0x100007578 ; symbol stub for: Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String `
-- 调用完第7行的方法有2个返回值,保存到寄存器 `rax` 、`rdx` 中
-- 第8行 `movq %rax, 0x86cf(%rip)` 将 `rax` 的值赋值给 `rip + 0x86cf `
-- 第8行 `movq %rdx, 0x86d0(%rip) ` 将 `rdx` 的值赋值给 `rip + 0x86d0 `
-
-可以看到 String 指针占用8 + 8 = 16个字节.
-
-
-
-QA:这个10是什么东西?1是什么东西?
-
-结合调用 `Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1)` 方法猜测,10 应该是 `utf8CodeUnitCount` 即 utf8格式的字符个数,1 应该是 `isASCII` 即是 ASCII
-
-做个实验验证下
-
-```swift
-var str1: String = "01234"
-```
-
-
-
-可以看到其他和上面的没变化,唯一不同的是 `movl $0x5, %esi` 将5赋值给寄存器 `esi`,即寄存器 `rsi`。实验的字符串 "01234" UTF8 字符个数为5
-
-继续改变
-
-```swift
-var str1: String = "01234😄"
-```
-
-
-
-可以看到将9赋值给寄存器 `esi` 即 `rsi`,字符串赋值给寄存器 `rdi`,`xorl %edx, %edx` 异或运算结果 0 赋值给寄存器 `edx` 即 `rdx`,也就是不纯粹为
-
-所以猜想正确。具体字符串创建过程可以查看 [Swift String 源码](https://github.com/apple/swift/blob/81812eaf2a6589610054a5db655bf7de3f3c8de6/stdlib/public/core/String.swift#L774)
-
-```swift
-// stdlib/public/core/String.swift
-extension String: _ExpressibleByBuiltinStringLiteral {
- @inlinable @inline(__always)
- @_effects(readonly) @_semantics("string.makeUTF8")
- public init(
- _builtinStringLiteral start: Builtin.RawPointer,
- utf8CodeUnitCount: Builtin.Word,
- isASCII: Builtin.Int1
- ) {
- let bufPtr = UnsafeBufferPointer(
- start: UnsafeRawPointer(start).assumingMemoryBound(to: UInt8.self),
- count: Int(utf8CodeUnitCount))
- if let smol = _SmallString(bufPtr) {
- self = String(_StringGuts(smol))
- return
- }
- self.init(_StringGuts(bufPtr, isASCII: Bool(isASCII)))
- }
-}
-```
-
-
-
- 也就是说 String 本身会占用16个字节长度,会调用 `Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String` 方法,该方法传递3个参数:字符串真实地址、字符串 UTF8 格式的个数、是否是 ASCII 。
-
-
-
-### 字符串长度小于15位的创建
-
-继续探索:
-
-
-
-可以看到 `str1` 地址为 `rip + 0x8853 = 0x1000039a5 + 0x8853 = 0x10000C1F8 `,然后读取对应堆上的内容为:
-
-```shell
-0x10000c1f8: 30 31 32 33 34 35 36 37 38 39 00 00 00 00 00 ea 0123456789......
-0x10000c208: 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff ................
-```
-
-打印出 `0x3736353433323130 0xea00000000003938`。怎么理解呢?
-
-从 [ASCII 码表](https://www.ascii-code.com) 可以看出 0 对应 `0x30`,1 对应 `0x31`,所以字符串`0123456789` 从 `0x30` 到 `0x39`
-
-`ea` 代表什么?
-
-a 即10,代表10个字符。最大为 f,只能存储15个字符。e 代表字符串类型。
-
-```swift
-var str1: String = "0123456789ABCDE"
-print(Mems.memStr(ofVal: &str1)) // "0x3736353433323130 0xef45444342413938"
-```
-
-当把字符改为15个时,输出的内存上的值为 `0x3736353433323130 0xef45444342413938`。
-
-也就是说:当字符串长度小于16位的时候,通常会使用内联存储来存储字符串的内容。内联存储意味着字符串的实际内容会直接存储在字符串对象本身的内存空间中,而不需要额外的内存分配。类似 OC `NSString` 的 `NSTaggedPointerString`
-
-
-
-### 字符串长度大于15位
-
-```swift
-var str1: String = "0123456789ABCDEF"
-print(Mems.memStr(ofVal: &str1))
-```
-
-
-
-分析下:
-
-- 第12行 `cmpq $0xf, %rsi` 拿 `0xf` 15 和寄存器 `rsi` 的值进行比较。上面已经分析过了,`rsi` 里面存放的是字符串的长度
-
-- 第13行 `jle 0x7ff81a7b9017 ` 如果12行比较结果为真,则跳转到 `0x7ff81a7b9017`
-
-- 实际发现,字符串长度大于15,则继续向下执行
-
-- 第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 值。
-
- 所以 **`字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`**,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
-
-- 经过23行后 `orq %rdi, %rdx` 可以看到 `rdx` 、`rdi` 里面存储的都是:`字符串真实地址` + `0x7fffffffffffffe0`
-
-- LLDB 输入 finish 结束函数细节,外部可以看到第10行 `movq %rdx, 0x8864(%rip) ` 将 `rdx` 寄存器里的值(也就是:`字符串真实地址` + `0x7fffffffffffffe0` )赋值给 `str1` 指针的后8个字节
-
-
-
-
-
-
-
- `字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
-
-
-
-### 字符串存储在内存中什么地方
-
-```swift
-var str1: String = "0123456789ABCDEF"
-```
-
-字符串地址为 ` 0x10000397f + 0x3ea1 = 0x100007820`,看着像全局变量、也有可能是常量区?字符串内容写死的情况下,编译器编译后内存地址应该可以确定,那到底在什么地方呢?利用 `MachOView` 窥探下 `MachO` 文件吧
-
-
-
-
-
-利用 MachOView 打开如下
-
-
-
-
-
-X86_64 架构下,我们在 MachOView 看到的地址,真实对应地址为: `0x10000000 + 00007820 = 0x10007820` 好巧啊,发现计算出的值刚好就是字符串 `str1` 的真实地址。MachOView 右侧也显示了字符串的内容,刚好就是 str1
-
-解释下:
-
-在 macOS 和 iOS 的二进制文件(如 Mach-O 格式的可执行文件或动态库)中,`Section64` 是一个结构体,用于描述二进制文件中的一个段(section)。每个段包含特定类型的数据或代码,并且具有特定的属性,比如是否可写、是否可执行等。
-
- `Section64(__TEXT,__cstring)` 中:
-
-- `__TEXT` 是段的段名(segment name),存储**只读且可执行**的内容,包括代码和只读数据。**典型节(Sections)**:
- - `__text`:存放机器指令(代码段)。
- - `__cstring`:存放字符串常量。
- - `__const`:存放其他常量数据。
-
-- **`__DATA` 段**:存储**可读写**的数据(如全局变量、静态变量)。
-- `__cstring` 节:
- - **功能**:`_cstring` 专门存储硬编码的字符串常量(如 `"Hello, World!"`)。
- - **内存权限**:映射到内存时,`__TEXT` 段整体为**只读**(`r--` 或 `r-x`),但 `_cstring` 本身**不可执行**,仅用于数据存储
- - **所属区域**:
- - 逻辑上属于**常量区**(类似 ELF 格式的 `.rodata`)。
- - 物理上可能与代码段(`__text`)同属 `__TEXT` 段,但用途和权限不同。
-
-
-因此,`Section64(__TEXT,__cstring)` 描述的是二进制文件中存储 C 字符串字面量的一个节。这个节位于 `__TEXT__` 段中,并且包含的是只读数据。
-
-
-
-再做下调整
-
-```swift
-var str1: String = "0123456789ABCDEF"
-var str2: String = "012345"
-print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x8000000100007800
-print(Mems.memStr(ofVal: &str2)) // 0x0000353433323130 0xe600000000000000
-```
-
-
-
-可以看到:
-
-- str1、str2 指针长度为均为16个字节,且内存连续 `00007820`、`00007830`
-- 字符串长度小于15的时候,打印出 str2 的内存值的前8个字节存储的就是字符串本身 `0x0000353433323130`,后8个字节 `0xe600000000000000` e 代表字符串类型,6代表字符串长度
-- 字符串长度大于15的时候,内存值的前8位 `0xd000000000000010` 最后的10也就是16,代表字符串长度。内存的后8位代表字符串计算后的地址(`字符串真实地址` + `0x7fffffffffffffe0` )
-- 字符串是存储在 `__TEXT__` 段的 `__cstring` 节中,属于常量区。
-
-做了调整,可以看到 str3 内存值的前8个字节 `0xd000000000000015` 中的15也就是21位字符串,符合预期。
-
-```swift
-var str1: String = "0123456789ABCDEF"
-var str2: String = "012345"
-var str3: String = "0123456789ABCDEFGHIJK"
-print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x80000001000077e0
-print(Mems.memStr(ofVal: &str2)) // 0x0000353433323130 0xe600000000000000
-print(Mems.memStr(ofVal: &str3)) // 0xd000000000000015 0x8000000100007800
-```
-
-
-
-### Swift 字符串存储本质
-
-Swift 字符串存储的两种模式:
-
-- **内联存储(Small String Optimization,SSO)**:
- - **条件**:字符串长度 ≤15 个 **ASCII 字符**(或 ≤7 个 **UTF-16 字符**)。
- - **特点**:字符串内容直接存储在 `StringObject` 的 16 字节内存中,无需堆分配。有点类似 Objective-C 的 **Tagged Pointer**
-- **堆存储(Heap-Allocated)**:
- - **条件**:字符串长度超过上述限制(字符串长度 > 15 个 **ASCII 字符** 或 > 7 个 **UTF-16 字符**)
- - **特点**:字符串内容存储在堆内存。其指针结构是一个 16 字节的 `StringObject`,`StringObject` 存储堆地址和元数据
-
-
-
-#### 内联存储(SSO)的具体实现
-
-内存布局:前8个字节(元数据 + 部分字符) + 后8个字节(剩余字符 + 填充)
-
-元数据编码:
-
-最低有效位(LSB)用于标识存储模式:
-
-- **0**:内联存储
-- **1**:堆存储
-
-其余位存储字符串长度和编码信息(ASCII 或 UTF-16)
-
-Demo
-
-````Swift
-let str = "Hello" // 5 个 ASCII 字符
-内存布局如下:
-0x0000000000000a05 // 元数据(长度=5, ASCII, 内联标志位=0)
-0x48656c6c6f000000 // ASCII 字符 "Hello" 的十六进制表示 + 填充
-````
-
-与 Objective-C Tagged Pointer 的区别
-
-| **特性** | **Swift 内联存储 (SSO)** | **Objective-C Tagged Pointer** |
-| :----------- | :------------------------- | :------------------------------ |
-| **存储位置** | 字符串对象的 16 字节内存中 | 指针值本身(64 位) |
-| **标识方式** | 元数据的最低有效位 (LSB) | 指针的最高有效位 (MSB) |
-| **兼容性** | 需考虑 Unicode 编码复杂性 | 仅支持有限类型(如短 NSString) |
-| **内存安全** | 完全由编译器管理 | 需运行时特殊处理 |
-
-
-
-
-
-
-
-## 字符串拼接
-
-### 长度小于15的字符串拼接
-
-```swift
-var str1: String = "012345"
-print(Mems.memStr(ofVal: &str1)) // 0x0000353433323130 0xe600000000000000
-str1.append("ABC")
-print(Mems.memStr(ofVal: &str1)) // 0x4241353433323130 0xe900000000000043
-str1.append("DEFGHI")
-print(Mems.memStr(ofVal: &str1)) // 0x4241353433323130 0xef49484746454443
-```
-
-可以看到不管字符串怎么拼接,只要拼接后的内容小于小于等于15,则依旧是在字符串的内容存放在自身的16个字节中。
-
-
-
-### 长度大于15的字符串拼接
-
-```swift
-var str1: String = "0123456789ABCDEF"
-print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x8000000100007800
-str1.append("G")
-print(Mems.memStr(ofVal: &str1)) // 0xf000000000000011 0x0000600001700440
-print("explore")
-```
-
-可以看到:长度为16的字符串拼接后
-
-- 内存的前8个字节,从 `0xd000000000000010` 变到了 `0xf000000000000011`,最后2位代表字符串长度,16进制的10就是16。从16位变成17位。
-- 内存的后8个字节,字符串的地址改变了
-
-上面可知, `字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
-
-
-
-字符串真实地址:`0x0000600001700440 + 0x20 = 0x0000600001700460`,LLDB `x 0x0000600001700460 ` 可以看到字符串拼接后的结果。看上去是存储在堆上。如何验证?
-
-
-
-我们知道堆上的内存初始化在 Swift 侧,关键方法为 `swift_allocObject ` -> `swift_slowAlloc` -> `malloc `。给 malloc 下断点,然后在断点出查看调用堆栈,可以看到在 `string.append()` 后有堆分配内存
-
-
-
-结束当前函数调用,在外层可以看到 str2 地址值的后8个字节为 `0x000060000170410`,再 + `0x20` 就是字符串真实地址,打印如下。
-
-
-
-0x20 是什么?这32个字节存放了什么信息?存储字符串的描述信息,比如:引用计数、字符串长度等信息。
-
-
-
- 总结:
-
-- 当字符串长度小于等于 0xF(也就是15),字符串内容直接存放在指针变量对应的内存中
-- 当字符拼接时候,拼接后字符串长度小于等于15,则字符串内容依旧存储在指针变量的内存中
-- 当字符串长度大于 0xF(也就是15),字符串的内容存放在 `__TEXT,__cstring` 中(常量区)。字符串的地址值信息存放在指针变量的后8个字节中,且真正的地址值为 (`后8个字节值` + `0x20` ),前32字节存储字符串的基础信息(长度、引用计数等)
-
-
-
-## dyld_stub_binder
-
-`__TEXT` 是 Mach-O 文件中通常包含代码和只读数据的段。这个段包含了程序的主要执行代码,以及常量字符串、符号表等。
-
-`dyld_stub_binder` 是一个由动态链接器(dyld)在运行时生成和使用的函数。它的主要目的是在首次调用某个动态链接的符号(如函数或方法)时,将该符号的实际地址绑定到调用点
-
-Swift 中 `String` 类型的初始化方法(`init`)的地址是否采用延迟绑定(Lazy Binding),取决于 **编译环境、优化级别和具体方法实现**
-
-### 延迟绑定的基本原理
-
-在编译时,对于动态链接的符号,编译器会生成一个桩(stub),而不是直接调用该符号。桩是一个小段的代码,当被首次执行时,它会触发 `dyld_stub_binder` 的调用。`dyld_stub_binder` 的任务就是找到该符号的实际地址,并将其写入桩中,从而替换桩的原始代,这样,下一次调用该符号时,就可以直接跳转到实际的地址,而无需再次通过桩和 `dyld_stub_binder`。
-
-延迟绑定(Lazy Binding)是动态链接的机制,用于推迟符号(如函数、方法)地址的解析到首次调用时。其核心步骤为:
-
-1. **编译阶段**:生成存根(Stub),指向符号占位地址。
-2. **启动阶段**:存根指向动态链接器(如 `dyld`)的解析函数(如 `dyld_stub_binder`)。
-3. **首次调用**:触发符号解析,动态链接器填充真实地址到存根。
-4. **后续调用**:直接跳转到已解析的地址。
-
-
-
-替换桩,位于 `__DATA,__la_symbol_ptr` 数据段可读可写,所以可以修改。
-
-
-
-
-
-`__stub_helper` 节是 `__TEXT` 段中的一个特定节,它包含了用于处理符号懒加载的辅助函数和代码。当动态链接的符号首次被调用时,这些辅助函数会被触发,以解析符号并将其地址绑定到调用点。
-
-这个过程也叫 `Lazy_binding`。懒加载是一种优化技术,允许程序在启动时不必立即解析和绑定所有动态链接的符号。相反,这些符号的解析和绑定被推迟到它们实际被使用时进行。这种延迟可以减少应用程序启动时的内存和性能开销。
diff --git a/Chapter1 - iOS/1.12.md b/Chapter1 - iOS/1.12.md
deleted file mode 100644
index 78acdff..0000000
--- a/Chapter1 - iOS/1.12.md
+++ /dev/null
@@ -1,224 +0,0 @@
-
-# NSFileManager
-
-> 想操作文件,该去了解下NSFileManager
-
-注意://小窍门:打印数组或者字典,里面包含中文,直接用%@打印会看不到中文,可用for遍历访问
-
-* 单例方法得到文件管理者对象
-
-```
- NSFileManager *fileManager = [NSFileManager defaultManager];
-```
-
-* 判断是否存在指定的文件
-
-```
- #define LogBool(value) NSLog(@"%@",value==YES?@"YES":@"NO");
-
- NSString *filepath = @"/Users/geek/Desktop/data.plist";
- BOOL res = [fileManager fileExistsAtPath:filepath];
- LogBool(res)
-```
-
-* 根据给出的文件路径判断是否存在文件,且判断路径是文件还是文件夹
-
-```
-NSString *filepath1 = @"/Users/geek/Desktop/data.plist";
- BOOL isDirectory = NO;
- BOOL isExist = [fileManager fileExistsAtPath:filepath1 isDirectory:&isDirectory];
- if (isExist) {
- NSLog(@"文件存在");
- if (isDirectory) {
- NSLog(@"文件夹路径");
- }else{
- NSLog(@"文件路径");
- }
- }else{
- NSLog(@"给定的路径不存在");
- }
-```
-
-* 判断文件或者文件夹是否可以读取
-
-```
- //这是一个系统文件(不可读)
- NSString *filePath2 = @"/.DocumentRevisions-V100 ";
- BOOL isReadable = [fileManager isReadableFileAtPath:filePath2];
- if (isReadable) {
- NSLog(@"文件可读取");
- } else {
- NSLog(@"文件不可读取");
- }
-```
-
-* 判断文件是否可以写入
-
-```
- //系统文件不可写入
- BOOL isWriteAble = [fileManager isWritableFileAtPath:filePath2];
- if (isWriteAble) {
- NSLog(@"文件可写入");
- } else {
- NSLog(@"文件不可写入");
- }
-```
-
-* 判断文件是否可以删除
-
-```
-//系统文件不可删除
- BOOL isDeleteAble = [fileManager isDeletableFileAtPath:filePath2];
- if (isDeleteAble) {
- NSLog(@"文件可以删除");
- } else {
- NSLog(@"文件不可删除");
- }
-```
-
-* 获取文件信息
-
-
-```
- NSError *error = nil;
- NSDictionary *fileInfo = [fileManager attributesOfItemAtPath:filepath1 error:&error];
-// NSLog(@"文件信息:%@,错误信息:%@",fileInfo,error);
- NSLog(@"文件大小:%@",fileInfo[NSFileSize]);
-```
-
-* 获取指定目录下的所有目录(列出所有的文件和文件夹)
-
-```
-NSString *filePath3 = @"/Users/geek/desktop";
- NSArray *subs = [fileManager subpathsAtPath:filePath3];
- NSLog(@"Desktop目录下所有的所有文件和文件夹");
- //小窍门:打印数组或者字典,里面包含中文,直接用%@打印会看不到中文,可用for遍历访问
- for (NSString *item in subs) {
- NSLog(@"%@",item);
- }
-```
-
-* 获取指定目录下的子目录和文件(不包含子孙)
-
-```
-NSError *erroe = nil;
- NSArray *children = [fileManager contentsOfDirectoryAtPath:filePath3 error:&erroe];
- NSLog(@"Desktop目录下的文件和文件夹");
- for (NSString *item in children) {
- NSLog(@"%@",item);
- }
-```
-
-* 在指定目录创建文件
-
-```
- NSString *filePath1 = @"/Users/geek/Desktop/data.text";
- NSData *data = [@"我要学好OC" dataUsingEncoding:NSUTF8StringEncoding];
- BOOL createFile = [fileManager createFileAtPath:filePath1 contents:data attributes:nil];
- if (createFile) {
- NSLog(@"文件创建成功");
- } else {
- NSLog(@"文件创建失败");
- }
-```
-
-* 在指定目录创建文件夹(参数说明:withIntermediateDirectories后的参数为Bool代表。YES:一路创建;NO:不会做一路创建)
-
-
-
-
-
-设置一路创建为NO,如果文件夹不存在则停止创建文件
-
-```
- NSString *filePath2 = @"/Users/geek/Desktop/海贼王";
- NSError *error = nil;
- BOOL createDirectory = [fileManager createDirectoryAtPath:filePath2 withIntermediateDirectories:NO attributes:nil error:&error];
- if (createDirectory) {
- NSLog(@"文件夹创建成功");
- } else {
- NSLog(@"文件夹创建失败,原因:%@",error);
- }
-
-
-
- //一路创建失败(文件夹不存在就不创建)
- NSString *filePath3 = @"/Users/geek/Desktop/海贼王";
- BOOL createDirectory1 = [fileManager createDirectoryAtPath:filePath3 withIntermediateDirectories:NO attributes:nil error:&error];
- if (createDirectory1) {
- NSLog(@"文件夹创建成功");
- } else {
- NSLog(@"文件夹创建失败,原因:%@",error);
- }
-```
-
-* 复制文件
-
-```
- NSString *filePath4 = @"/Users/geek/Desktop/动漫";
-
- BOOL copyRes = [fileManager copyItemAtPath:filePath3 toPath:filePath4 error:nil];
- if (copyRes) {
- NSLog(@"文件复制成功");
- } else {
- NSLog(@"文件复制失败");
- }
-```
-
-* 移动文件
-
-```
- NSString *filePath5 = @"/Users/geek/Downloads/动漫";
- BOOL moveRes = [fileManager moveItemAtPath:filePath3 toPath:filePath5 error:nil];
- if (moveRes) {
- NSLog(@"文件移动成功");
- } else {
- NSLog(@"文件移动失败");
- }
-```
-
-* 可以给文件重命名
-
-```
- //可以给文件重命名
- NSString *filePath6 = @"/Users/geek/Downloads/卡通";
- [fileManager moveItemAtPath:filePath5 toPath:filePath6 error:nil];
-```
-
-* 删除文件
-
-```
- BOOL deleteRes = [fileManager removeItemAtPath:filePath6 error:nil];
- if (deleteRes) {
- NSLog(@"文件删除成功");
- } else {
- NSLog(@"文件删除失败");
- }
-```
-
-# NSFileManager小病毒
-```
- //单例方法得到文件管理者对象
- NSFileManager *fileManager = [NSFileManager defaultManager];
- NSString *filePath = @"/Users/geek/desktop/delete/";
- while (1) {
- //判断该文件路径是否存在
- BOOL exist = [fileManager fileExistsAtPath:filePath];
- if (exist) {
- //找出该路径下的所有文件
- NSArray *subs = [fileManager contentsOfDirectoryAtPath:filePath error:nil];
- if (subs.count > 0) {
- for (int i=0; i
-上面的代码会发生循环引用,会导致局部变量的 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
-
-- 非逃逸闭包、逃逸闭包,一般都是当作参数传递给函数
-- 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内
-- 逃逸闭包:闭包油可能在函数结束后调用, 闭包调用逃离了函数的作用域,需要通过 `@eascaping` 声明
-
-```swift
-typealias Fn = () -> ()
-var globalFn:Fn?
-func setFn(_ fn: @escaping Fn) {
- globalFn = fn
-}
-setFn {
- print("Hello world")
-}
-globalFn?() // Hello world
-```
-
-
-
-注意点:逃逸闭包不可以捕获 `inout` 参数
-
-```swift
-typealias Fn = () -> ()
-func other1(_ fn: Fn) {
- fn()
-}
-
-func other2(_ fn: @escaping Fn) {
- fn()
-}
-
-func test(value: inout Int) -> Fn {
- other1 {
- value += 1
- }
- other2 { // compile error:Escaping closure captures 'inout' parameter 'value'
- value += 1
- }
- func add() {
- value += 1
- }
- return add // compile error:Escaping closure captures 'inout' parameter 'value'
-}
-```
-
-原因:因为 `inout` 参数的本质是要求函数在调用期间直接操作变量的内存地址,而逃逸闭包可能会在函数返回后的任何时刻调用(不确定),这时 `inout` 参数所在的内存地址可能已经不再有效或者已经被其他值覆盖。因此,允许逃逸闭包捕获 `inout` 参数会导致潜在的数据不一致和安全问题。
-
-
-
-
-
-## 内存访问冲突
-
-Confilicting Access to Memory, 内存访问冲突发生在:
-
-- 至少一个是写入操作
-- 它们访问的是同一块内存
-- 它们的访问时间重叠(比如在同一个函数内)
-
-
-
-Demo1:
-
-```swift
-var step = 1
-func increament(_ num: inout Int) {
- num += step
-}
-increament(&step)
-```
-
-
-
-
-
-解决办法就是打破3个条件之一。显然不可以换函数,只有改变「同时访问一块内存地址」这个条件了
-
-```swift
-var step = 1
-func increament(_ num: inout Int) {
- num += step
-}
-var stepCopy = step
-increament(&stepCopy)
-step = stepCopy
-```
-
-
-
-Demo2:
-
-```swift
-func balance(_ x: inout Int, _ y: inout Int) {
- let sum = x + y
- x = sum/2
- y = sum - x
-}
-
-var num1 = 1
-var num2 = 2
-balance(&num1, &num2) //
-balance(&num1, &num1) // compile error: Inout arguments are not allowed to alias each other
-```
-
-Demo3: 下面代码虽然看着传入的是不同内存地址,但是 health 和 power 都属于元祖,还是同一个内存地址。
-
-
-
-
-
-如何解决?
-
-Swift 规定以下 case,就说明重叠访问结构体的属性就是安全的
-
-- 只访问实例存储属性,不是计算属性或者类属性
-- 结构体是局部变量而非全局变量
-- 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获
-
-```swift
-func balance(_ x: inout Int, _ y: inout Int) {
- let sum = x + y
- x = sum/2
- y = sum - x
-}
-func testConflictingAccessToMemory() {
- var tumple = (health: 100, power: 100)
- balance(&tumple.health, &tumple.power)
-}
-testConflictingAccessToMemory()
-```
-
-
-
-
-
-## 指针
-
-Swift 也有专门的指针类型,都被定义为 Unsafe(不安全的),有:
-
-- `UnsafePointer
-
-问题本质是:`inout` 关键词代表要对函数的参数进行写操作,而函数内部的实现利用 step 进行反问操作。读写同时存在就有问题。
-
-如何解决?产生一个备份,然后调用函数,最后将备份产生的结果覆盖老值
-```
-var step = 1
-func increment(_ number: inout Int) {
- number += step
-}
-// make an explicit copy
-var copyOfStep = step
-// invoke
-increment(©OfStep)
-// update the original value
-step = copyOfStep
-print(step) // 2
-```swift
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.122.md b/Chapter1 - iOS/1.122.md
deleted file mode 100644
index b00f1c0..0000000
--- a/Chapter1 - iOS/1.122.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# Swift 字面量本质
-
-## 常见的字面量默认类型
-
-- `public typealias IntegerLiteralType = Int`
-- `public typealias FloatLiteralType = Float`
-- `public typealias BooleanLiteralType = Bool`
-- `public typealias StringLiteralType = String`
-
-可以通过 typealias 修改字面量的默认类型,Demo 如下:
-
-```swift
-typealias IntegerLiteralType = UInt8
-var val = 8
-val
-```
-
-
-
-
-
-## 字面量协议
-
-Swift 自带的数据类型基本都可通过字面量初始化,本质原因是遵循了对应的协议
-
-| | | |
-| ------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
-| Bool | ExpressibleByBooleanLiteral | var b:Bool = false |
-| Int | ExpressibleByIntegerLiteral | var num:Int = 2 |
-| Float、Double | ExpressibleByIntegerLiteral、ExpressibleByFloatLiteral | var height:Float = 175
-
-LLDB 输入 `si` 进去窥探下
-
-
-
-看样子,函数还没到底,看到地址 `0x00007ff81a86f530` 很大很大,猜测应该是一个系统动态库方法地址,继续跟进去研究 `si`
-
-
-
-
-
-可以看到内部还有函数调用
-
-
-
-继续跟进 `si`
-
-
-
-看上去是在做继续 `clasedRange` 区间符合的判断,继续跟进 `si`,里面确实是在判断是否命中区间的判断。不一一研究了,这次目的是判断 switch...case pattern 的实现。
-
-
-
-
-
-结论:switch case pattern 模式匹配,系统底层实现是依赖
-
-
-
-## 模式匹配的应用
-
-自定义模式匹配。根据上面通过汇编进行分析,我们知道
-
-- `static func ~=(pattern: case 后面的值, value: switch 后面的值)` pattern 代表的是 case 后面的值,value 代表 switch 后面的值。
-
-- 当 case 有不同类型的时候,如果编译报错,则需要重写 `static func ~=(pattern: ,value: )` 方法,调整 pattern 的数据类型,数据类型应该和 case 后的数据类型一致(可以是函数等)
-
-```swift
-struct Student {
- var score = 0
- var name = ""
-
-
- /// Student 和 Int 模式匹配的方法
- /// - Parameters:
- /// - pattern: case 后面的内容
- /// - value: switch 后面的内容
- /// - Returns: 是否命中
- static func ~= (pattern: Int, value: Student) -> Bool {
- value.score >= pattern
- }
- static func ~= (pattern: Range
-
- 可以看到即使在 Swift 代码中,调用 OC 对象方法,本质上还是走 Objc Runtime 的一套流程。50行代码,将 showPower 的地址赋值给 `rsi` 寄存器,然后调用 `objc_msgSend` 方法。
-
- LLDB 下 输入 `si` 窥探下实现。
-
-
-
- 可以看到一个很大的地址 `0x00007ff80002d7c0` 就是动态库的符号方法地址。同时 Xcode 很智能,右侧给出了函数名称。
-
-3. OC 调用 Swift 底层又是如何调用的?在 OC 类 Person 中,底层调用 Swift Cat 类的 sayHi 方法。
-
- 断点加在 `[self.cat sayHi]` 处,可以看到本质上还是 Runtime objc_msgSend 那一套。
-
-
-
-4. `cat.run()` 底层是怎么调用的?
-
- 如果一个 Swift 类,不继承自 NSObject,那么方法调用的本质就是走虚表那套逻辑,找到指针的前8个字节,根据前8个字节找到类信息,然后在类信息中,前面一些内存地址存储类型信息,后续根据偏移在方法列表中,找到需要调用的函数地址。类似下面的图。
-
-
-
- 那 Swift 类继承自 NSObject 后,依然在 Swfit 中调用方法,背后的原理是什么?
-
- 在 ViewController.swift 中 `cat.sayHi()` 下断点
-
-
-
-
-
-## Swift 方法如何走 Runtime 消息机制
-
-可以看到,即使一个 Swift 类继承自 NSObject,但依旧在 Swift 中调用对象方法,本质上还是走虚表那套方法调用流程,不会走 Runtime 消息机制。
-
-如果想让 Swift 方法调用走 Runtime 消息机制,可以在方法前加 `@objc dynamic`
-
-```swift
-dynamic func sayHi () {
- print("My name is \(name)")
-}
-```
-
-断点查看,发现在 Swift 代码中调用同样的 Swift 对象方法,此时走了 Runtime 消息机制。
-
-
-
-
-
-## Swift OC 混编,内存布局会改变吗
-
-如果一个 Swift 类继承自 NSObject,内存布局会改变
-
-```swift
-class Person {
- var age: Int = 28
- var height: Int = 175
-}
-let p: Person = Person()
-print(Mems.memStr(ofRef: p))
-// console
-0x0000000100010540 0x0000000000000003 0x000000000000001c 0x00000000000000af
-```
-
-可以看到一个 Swift 类,前8个字节用来存放类信息的指针,其次8个字节用来存放引用计数信息,后16个字节用来存放28和175,就是存储属性信息
-
-调整下:
-
-```swift
-import Foundation
-class Person: NSObject {
- var age: Int = 28
- var height: Int = 175
-}
-let p: Person = Person()
-print(Mems.memStr(ofRef: p))
-// console
-0x011d8001000104e9 0x000000000000001c 0x00000000000000af 0x0000000000000000
-```
-
-可以看到当 Swift 类继承自 NSObject 后,前8个字节存放的是 isa 指针,其次的16个字节存放存储属性信息,最后的8个字节用来内存对齐。
-
-
-## 混编
-### OC 调用 Swift
-OC 项目,使用 Swift 开发默认会创建 `项目名-Swift.swift` 文件。Swift 类都可以在该文件中找到
-
-默认情况下生成的 Swift class 是不可以直接在 OC 中使用的,如果需要**访问需要在 class 前加 `@objc` 且继承自 NSObject**,编译器生成的代码如下:
-
-
-class 不仅需要创建对象,还需要访问属性和方法,可以在属性或者方法前加 `@objc`,效果如下
-
-
-但这样很麻烦,需要给每个属性、方法添加 `@objc`。有简便方法,可以直接在 class 前加 `@objcMembers`,这样该 class 所有的属性、方法都可以在 OC 中访问
-
-
-**Swift 写的 extension,在`项目名-Swift.swift` 文件中可以看到,是被编译器编译为 OC 的分类 Category**。
-```swift
-@objcMembers class Car : NSObject {
- var price: Double
- var band: String
- init(price: Double, band: String) {
- self.price = price
- self.band = band
- }
-}
-
-extension Car {
- func test() {
- print("Car test")
- }
-}
-```
-
-```objective-c
-@interface Car : NSObject
-@property (nonatomic) double price;
-@property (nonatomic, copy) NSString * _Nonnull band;
-- (nonnull instancetype)initWithPrice:(double)price band:(NSString * _Nonnull)band OBJC_DESIGNATED_INITIALIZER;
-@end
-
-@interface Car(SWIFT_EXTENSION(TestSwift))
-- (void)test
-@end
-
-```
-
-可以通过 `@objc(name)` 重命名 Swift 暴露给 OC 的符号名(类名、属性名、函数名等)
-
-
-### Swift 中访问 OC 的对象、方法
-要在 Swift 中访问 OC 类,需要创建桥接文件,OC 工程首次创建 Swift 文件时,Xcode 默认创建桥接文件 `项目名-Bridging-Header.h`。如果是手动创建的,则需要配置(在项目的 Build Settings 中,找到 Objective-C Bridging Header 设置项,并指定桥接头文件的路径。确保桥接头文件的路径正确无误,并且文件名和扩展名都正确)。
-
-在桥接文件中(`项目名-Bridging-Header.h`) 写好需要在 Swift 中使用的 objective-C 类。
-
-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 对象方法
-
-
-
-纯 Swift 环境中,调用对象的方法,走的是虚表的逻辑。最终底层会调用 `callq *0x78(%rax)`
-
-可以看到:Swift 调用 Swift 对象和方法,断点处显示直接调用方法地址,lldb 模式下输入 `si` 可以看到汇编代码停在了 **SwiftDemo`Cat.sayHi():** 的地方。所以走的是虚函数表逻辑。
-
-Demo2: OC 调用 Swift 对象方法
-1. Swift 类继承自 NSObject,在前面加 `@objcMembers` 暴露给 OC 环境
-2. Swift 环境调用 OC 对象和方法
-3. OC 方法中调用 Swift 对象和方法
-4. 给 OC 环境中,调用 Swift 对象方法的地方下个断点,查看走的是 OC 的 Runtime 还是 Swift 的虚函数表的逻辑
-断点截图如下:
-
-
-可以看到在 OC 环境中,调用 Swift 对象的方法,本质上走的是 Runtime 的流程,汇编可以看到走的是 `objc_msgSend` 流程,效果类似 `objc_msgSend(p, @selector(run))`
-
-结论:OC 类暴露给 Swift 环境后,调用 OC 对象的方法,本质走的是 Runtime 流程。
-
-### 被 @objcMembers 修饰的 Swift 对象,在 Swift 中调用
-
-Demo3: 暴露给 OC 的 Swift 对象,被 Swift 环境调用
-1. 继承自 NSObject 的 Swift 类
-2. 被 `@objcMembers` 修饰
-3. 在 Swift 环境中调用暴露给 OC 的 Swift 对象方法
-4. 断点查看方法调用的本质
-
-
-可以看到,在 Swift 环境中,即使某个 Swift 类暴露给了 OC,调用其对象方法的本质,依旧是走虚函数表。因为此时用不到 Runtime 的能力
-
-Demo4: 在 Demo3 的基础上,swift 方法内部调用 OC 对象方法
-
-也就是说:
-1. Cat 类继承自 NSObject,被 `@objcMembers` 修饰
-2. 在 Swift 中调用 Cat 对象的 sayHi 方法
-3. 在 sayHi 方法内部,调用 OC Person 对象的 run 方法
-
-下断点可以看到:
-
-
-
-分为2个阶段:
-1. 第一阶段:在 Swift 环境调用虽然暴露给 OC 的 Swift 对象方法,但因为没有和 OC 直接交互,所以走的是 Swift 虚函数表逻辑
-2. 第二阶段:在 Swift 环境调用 OC 对象方法,因为底层是 OC 方法调用,所以走的是 OC Runtime 逻辑
-
-思考:想让 Swift 方法也走 OC 的 Runtime,可以利用 **`dymanic`** 关键词修饰方法。如下:
-
-
-
-
-## dynamic 的作用
-在 Swift 中,dynamic 关键字用于强制方法或属性通过 Objective-C 运行时(Runtime)进行动态派发,即使该方法或属性在纯 Swift 代码中被调用。它的核心应用场景与 Objective-C 运行时的动态特性(如 KVO、方法交换、动态解析等)紧密相关
-
-核心作用:绕过 Swift 的静态优化
-Swift 默认会尝试优化方法派发(如使用虚函数表或直接派发),而 dynamic 会强制方法或属性始终通过 Objective-C 的 objc_msgSend 机制调用,确保动态性。
-启用 Objective-C 运行时特性
-
-若需要实现以下功能,必须使用 dynamic:
-- 键值观察(KVO):标记为 dynamic 的属性会自动支持 KVO。
-- 方法交换(Method Swizzling):运行时替换方法实现。
-- 动态方法解析:通过 resolveInstanceMethod: 动态添加方法实现。
-- 消息转发:通过 forwardingTargetForSelector: 或 forwardInvocation: 处理未实现的方法。
-
-
-### 支持 KVO
-Swift 中默认的存储属性不支持自动 KVO 通知,但通过 dynamic 标记属性后,属性访问会通过 Objective-C 运行时,从而触发 KVO 机制。
-```swift
-@objcMembers class Cat: NSObject {
- dynamic var name: String // 支持 KVO
- init(name: String) { self.name = name }
-}
-```
-在 OC 中监听 name 变化
-```Objective-c
-Cat *cat = [[Cat alloc] initWithName:@"PiPi"];
-[cat addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
-```
-
-### 方法交换
-若要在运行时替换方法实现(如 AOP 编程、调试 Hook),必须确保目标方法是动态派发的。
-
-```swift
-@objcMembers class Cat: NSObject {
- dynamic func sayHi() { print("Original") }
-}
-```
-在 Objective-C 中交换方法实现
-```Objective-c
-Method originalMethod = class_getInstanceMethod([Cat class], @selector(sayHi));
-Method swizzledMethod = class_getInstanceMethod([Cat class], @selector(swizzled_sayHi));
-method_exchangeImplementations(originalMethod, swizzledMethod);
-```
-
-### 动态解析未实现的方法
-当调用一个未实现的方法时,可通过 resolveInstanceMethod: 动态添加实现。
-```swift
-@objcMembers class Cat: NSObject {
- dynamic func sayHi() { print("Hello") } // 假设此方法未实现,运行时动态添加
-}
-```
-Objective-C 运行时动态解析
-```Objective-c
-+ (BOOL)resolveInstanceMethod:(SEL)sel {
- if (sel == @selector(sayHi)) {
- class_addMethod([self class], sel, (IMP)dynamicSayHi, "v@:");
- return YES;
- }
- return [super resolveInstanceMethod:sel];
-}
-
-void dynamicSayHi(id self, SEL _cmd) {
- NSLog(@"Dynamic Hello");
-}
-```
-
-### 动态调用
-当 Swift 类的方法需要被 Objective-C 或其他动态语言(如通过 performSelector:)调用时,若方法未被标记为 dynamic,可能因编译器优化导致动态调用失败。
-```swift
-@objcMembers class Cat: NSObject {
- dynamic func sayHi() { print("Hello") }
-}
-```
-Objective-C 中动态调用
-```Objective-c
-Cat *cat = [[Cat alloc] init];
-[cat performSelector:@selector(sayHi)]; // 需 dynamic 支持
-```
-
-
-## 数据类型转换
-在 Swift 和 Objective-C 的类型桥接机制中,String 与 NSString 可以互相转换,而 String 不能直接与 NSMutableString 互相转换,但 NSMutableString 可以转为 String。类似的情况也出现在 Array/NSArray/NSMutableArray 和 Dictionary/NSDictionary/NSMutableDictionary 之间
-
-### 核心原因
-可变性的语义差异:
-- Swift 的值类型(String、Array、Dictionary) 被设计为不可变的值语义。每次修改会产生新实例(Copy on Write)。比如 `var str = "A"; str += "B"`
-会创建新字符串 'AB',而非修改原内存
-- OC 的类型 NSString 是不可变的引用类型,NSMutableString 是可变的引用类型,允许直接修改内容
-这种差异导致桥接时需要严格处理可变性,确保类型安全和语义一致。
-
-
-### Swift string 与 OC NSString
-双向隐式桥接。因为两者都是不可变的,语义一致,没有副作用,都可以直接互相转换。
-
-```swift
-// Swift -> OC
-let swiftStr1: String = "Hello"
-let nsStr1: NSString = swiftStr1 as NSString
-// OC -> Swift
-let nsStr12: NSString = "World"
-let swiftStr2: String = nsStr12 as String
-```
-### Swift string 与 OC NSMutableString
-
-Swift string 与 OC NSMutableString 是单向桥接的。OC NSMutableString 可以转为 Swift String。但 Swift String 不能转为 OC NSMutableString
-
-原因:OC NSMutableString 是可变类型,转为 Swift String 时会创建一份不可变的副本,避免被 Swift String 修改造成意外修改。
-若允许 Swift String 直接转换为 OC NSMutableString,则可能通过 OC 代码修改 String 的值,破坏 Swift 值语义。
-```swift
-// Objective-C → Swift(允许)
-let mutableStr: NSMutableString = "Hello".mutableCopy() as! NSMutableString
-let swiftStr: String = mutableStr as String // 隐式桥接,生成不可变副本
-
-// Swift → Objective-C(禁止隐式桥接)
-let swiftStr = "Hello"
-let mutableStr = swiftStr as NSMutableString // ❌ 编译错误
-```
-
-### 类似情况
-- Swift Array 与 OC NSArray 可以互相转换。
-- OC 的 NSMutableArray 可以转换为 Swift Array。但是 Swift Array 不能转换为 OC NSMutableArray
-
-### 底层原理
-1. 类型桥接的实现方式:Swift 编译器通过 `_ObjectiveCBridgeable` 协议实现与 OC 类型的桥接。
-例如 String 实现了 `_ObjectiveCBridgeable`,使其能与 NSString 自动转换
-
-2. 可变类型桥接限制
-Swift String:
-- 值类型:Swift String 是结构体,遵循值语义。每次赋值或者修改都会生成新的独立副本,确保数据不可变性和线程安全
-- 不可变:即使用 var 声明,修改 String 也会通过创建新实例实现,而非直接修改内存
-Objective-C 的 NSMutableString
-- 引用类型(Reference Type):NSMutableString 是类(class),遵循引用语义。变量持有的是指向内存地址的指针。
-- 可变(Mutable):允许直接修改内存中的内容(如追加、删除字符),所有持有该引用的代码都会感知到变化。
-
-3. 为什么 String 不能直接桥接为 NSMutableString?
-
-- 原因 1:值语义与引用语义的冲突
-若允许将 Swift 的 String 直接桥接为 NSMutableString,则相当于将一个值类型强制转换为可变的引用类型。
-风险示例:
-```swift
-let swiftStr = "Hello"
-let mutableStr = unsafeBridgeToNSMutableString(swiftStr) // 假设存在这种桥接
-mutableStr.append("!") // 修改 mutableStr
-print(swiftStr) // 若桥接是共享内存,此处会输出 "Hello!",违背值语义!
-```
-
-这会导致 Swift 的 String 失去其不可变性保证,破坏类型安全。
-
-- 原因 2:内存管理的不兼容
-Swift 的 String 可能存储在栈内存或静态区(尤其是短字符串),而 NSMutableString 必须分配在堆内存。
-直接桥接可能导致内存访问错误(如悬垂指针)。
-
-- 原因 3:设计哲学的保护
-Swift 强调安全性,禁止隐式共享可变状态。若允许直接桥接,开发者可能无意中在多个地方修改同一块内存,导致难以调试的问题。
-
-QA: 如何显式实现 String → NSMutableString?
-若需要将 Swift String 转为 NSMutableString,必须显式创建新对象,而非直接桥接:
-```swift
-let swiftStr = "Hello"
-let mutableStr = NSMutableString(string: swiftStr) // 显式拷贝
-mutableStr.append("!") // 安全修改
-```
-
-在 Swift 和 Objective-C 的互操作中,`String` 和 `NSMutableString` 之间的转换规则是由两者的**类型语义**和**内存管理机制**共同决定的。以下是具体原因和底层逻辑:
-
----
-
-### **1. 类型语义的根本差异**
-#### **Swift 的 `String`**
-- **值类型(Value Type)**:
- Swift 的 `String` 是结构体(`struct`),遵循值语义。每次赋值或修改都会生成新的独立副本,确保数据不可变性和线程安全。
-- **不可变(Immutable)**:
- 即使使用 `var` 声明,修改 `String` 也会通过创建新实例实现,而非直接修改内存。
-
-#### **Objective-C 的 `NSMutableString`**
-- **引用类型(Reference Type)**:
- `NSMutableString` 是类(`class`),遵循引用语义。变量持有的是指向内存地址的指针。
-- **可变(Mutable)**:
- 允许直接修改内存中的内容(如追加、删除字符),所有持有该引用的代码都会感知到变化。
-
----
-
-### **2. 为什么 `String` 不能直接桥接为 `NSMutableString`?**
-#### **原因 1:值语义与引用语义的冲突**
-- 若允许将 Swift 的 `String` 直接桥接为 `NSMutableString`,则相当于将一个值类型强制转换为可变的引用类型。
- **风险示例**:
- ```swift
- let swiftStr = "Hello"
- let mutableStr = unsafeBridgeToNSMutableString(swiftStr) // 假设存在这种桥接
- mutableStr.append("!") // 修改 mutableStr
- print(swiftStr) // 若桥接是共享内存,此处会输出 "Hello!",违背值语义!
- ```
- 这会导致 Swift 的 `String` 失去其不可变性保证,破坏类型安全。
-
-#### **原因 2:内存管理的不兼容**
-- Swift 的 `String` 可能存储在栈内存或静态区(尤其是短字符串),而 `NSMutableString` 必须分配在堆内存。
- 直接桥接可能导致内存访问错误(如悬垂指针)。
-
-#### **原因 3:设计哲学的保护**
-- Swift 强调安全性,禁止隐式共享可变状态。若允许直接桥接,开发者可能无意中在多个地方修改同一块内存,导致难以调试的问题。
-
----
-
-### **3. 为什么 `NSMutableString` 可以转为 `String`?**
-当 `NSMutableString` 桥接到 Swift 时,**会生成一个不可变的副本**,切断与原对象的关联:
-```swift
-let mutableStr: NSMutableString = "Hello".mutableCopy() as! NSMutableString
-mutableStr.append("!") // 修改原对象
-
-let swiftStr = mutableStr as String // 桥接生成新副本
-print(swiftStr) // "Hello!"
-mutableStr.append("?") // 继续修改原对象
-print(swiftStr) // 仍然是 "Hello!",不受影响
-```
-- **行为安全**:生成的 `String` 是独立的不可变副本,与原 `NSMutableString` 解耦。
-- **符合语义**:Swift 的 `String` 仍然是值类型,后续修改不会影响副本。
-
----
-
-### **4. 如何显式实现 `String` → `NSMutableString`?**
-若需要将 Swift `String` 转为 `NSMutableString`,必须**显式创建新对象**,而非直接桥接:
-```swift
-let swiftStr = "Hello"
-let mutableStr = NSMutableString(string: swiftStr) // 显式拷贝
-mutableStr.append("!") // 安全修改
-```
-- **显式拷贝**:通过 `NSMutableString` 的构造器生成独立可变对象,避免共享内存。
-
----
-
-### **5. 类似场景:`Array` ↔ `NSMutableArray`**
-同样的规则适用于集合类型:
-- **Swift `Array`**:
- 值类型,桥接到 `NSArray`(不可变),但无法直接桥接为 `NSMutableArray`。
-- **`NSMutableArray` → `Array`**:
- 生成不可变副本,与原对象解耦。
-
-```swift
-let swiftArray = [1, 2, 3]
-let mutableArray = NSMutableArray(array: swiftArray) // 显式拷贝
-mutableArray.add(4) // 安全修改
-```
-
-Swift 通过严格的类型桥接规则,确保值类型的不可变性和引用类型的可控性。这种设计虽然牺牲了部分灵活性,但从根本上避免了数据竞争、意外修改等风险,符合其安全至上的哲学。
-
-
-
-| Swift 数据类型 | 单向转换 or 双向转换 | OC 数据类型 |
-| -------------- | -------------------- | ------------------- |
-| String | <--------> | NSString |
-| String | <-------- | NSMutableString |
-| Array | <--------> | NSArray |
-| Array | <-------- | NSMutableArray |
-| Dictionary | <--------> | NSDictionary |
-| Dictionary | <-------- | NSMutableDictionary |
-
diff --git a/Chapter1 - iOS/1.125.md b/Chapter1 - iOS/1.125.md
deleted file mode 100644
index b086ada..0000000
--- a/Chapter1 - iOS/1.125.md
+++ /dev/null
@@ -1,220 +0,0 @@
-# Swift 函数式编程
-
-## 定义
-
- 函数式编程(Funtional Programming,简称 FP)是一种编程范式,也就是如何编写程序的方法论
-
-- 主要思想:把计算过程尽量分解成一系列可复用函数的调用
-- 主要特征:函数是“第一等公民”。函数与其他数据类型一样的地位,可以赋值给其他变量,也可以作为函数参数、函数返回值
-
-
-
-函数式编程最早出现在 LISP 语言,绝大部分的现代编程语言也对函数式编程做了不同程度的支持,比如:Haskell、JavaScript、Python、Swift、Kotlin、Scala 等
-
-
-
-函数式编程中几个常用的概念:
-
-- Higher-Order Function、Function Currying
-- Functor、Applicative Functor、Monad
-
-
-
-## 高阶函数
-
- 高阶函数是至少满足下列一个条件的函数:
-
-- 接受一个或多个函数作为输入(map、filter、reduce等)
-- 返回一个函数
-
-FP中到处都是高阶函数
-
-
-
-## 柯里化(Currying)
-
-将一个接受多个参数的函数变换成为一系列只接受单个参数的函数
-
-Demo:
-
-```swift
-// 函数式编程,为了过程的复用
-let num = 10
-func add(_ v: Int) -> (Int) -> Int {{ $0 + v }}
-func sub(_ v: Int) -> (Int) -> Int { { $0 - v } }
-func multiple( _ v: Int) -> (Int) -> Int { { $0 * v }}
-func divide(_ v: Int) -> (Int) -> Int { { $0 / v } }
-func mod(_ v: Int) -> (Int) -> Int { { $0 % v }}
-
-// 函数合成,泛型
-infix operator >>> : AdditionPrecedence
-func >>>(_ f1: @escaping (A) -> B,
- _ f2: @escaping (B) -> C)
- -> (A) -> C {
- { f2(f1($0)) }
-}
-
-// result = ((((x + 3)*5) - 1 )%10)/2
-let fn = add(3) >>> multiple(5) >>> sub(1) >>> mod(10) >>> divide(2)
-
-print(fn(num))
-
-
-func multipleAdd(_ v1: Int) -> ((Int) -> ((Int) -> Int)) {
- return { v2 in
- return { v3 in
- return
- v1 + v2 + v3
- }
- }
-}
-
-/*
- - multipleAdd(1),调用函数,传递1给参数V3,此时继续返回一个函数。
- - multipleAdd(1)(2) 拿着上一步返回的函数,再去调用,传入的2给参数V2.此时继续返回一个函数
- - multipleAdd(1)(2)(3) 拿着上一步返回的函数,再去调用,此时传入的3给参数V1,此时则执行相加操作。V1 + V2 + V3
- */
-let rs = multipleAdd(1)(2)(3)
-print(rs)
-```
-
-参数对应如下图圈选部分
-
-
-
-
-
-Demo2:将一个三个参数的函数变成柯里化
-
-```swift
-func addThree(_ v1: Int, _ v2: Int, _ v3: Int) -> Int { v1 + v2 + v3 }
-func addThree(_ v1: Int) -> ((Int) -> (Int) -> Int) {
- return { v2 in
- return { v3 in
- return v1 + v2 + v3
- }
- }
-}
-
-func currying(_ fn: @escaping (A, B, C) -> D) -> ((A) -> ((B) -> ((C) -> D))) {
- return { v1 in
- return { v2 in
- return { v3 in
- fn(v1, v2, v3)
- }
- }
- }
-}
-
-print(addThree(1, 2, 3)) // 6
-print(addThree(1)(2)(3)) // 6
-print(currying(addThree)(1)(2)(3)) // 6
-```
-
-
-
-## 函子
-
-### 概念
-
-在函数式编程中,**函子(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
-
-奇怪的事情发生了:AppDelegate 不见了,也没地方构建 keyWindow,怎么办?为什么文件叫 `SwiftUIDemoApp`?
-
-其实:
-
-- SwiftUIDemo 是项目名称,SwiftUI 规范约定,默认生成 `项目名 + App.swfit`
-- Apple 设计 SwiftUI 的时候,打算让 UIKit、AppKit 退居二线,所以默认没有 AppDelegate、KeyWindow
-- `@mian` 属性告诉编译器,这是应用程序主入口。编译器会自动生成一个入口函数,类似传统的 main 函数,内部初始化应用程序和启动 RunLoop
-
-
-
-如果需要使用 AppDelegate 来处理一些逻辑,可以按照下面的方式:
-
-- 声明一个类,需继承自 NSObject、遵循 `UIApplicationDelegate` 协议,实现协议方法
-- 在主入口处,添加 `@UIApplicationDelegateAdaptor` 标记
-
-```swift
-class AppDelegate: NSObject, UIApplicationDelegate {
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
- print("applicationDidFinishLaunching")
- return true
- }
- func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
- print("applicationDidReceiveMemoryWarning")
- }
-}
-
-@main
-struct SwiftUIDemoApp: App {
- @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
- var body: some Scene {
- WindowGroup {
- ContentView()
- }
- }
-}
-```
-
-QA:需要注意的是,在 SwiftUI 中只有部分代理可以使用。为什么?
-
-iOS 13 以前,由 UIApplicationDelegate 来控制生命周期,iOS 13 以后,由 UISceneDelegate 来控制生命周期。在 iOS 13 之后,用UIScene 替代了之前 UIWindow 来管理视图,背后的设计考量主要是为了解决 iPadOS 展示多窗口的问题。
-
-在 iOS 14 之后,Apple 又给 SwiftUI 提供了更优雅的 API 来显示和控制 Scene。所以控制应用展示可以这样:
-
-```swift
-@main
-struct SwiftUIDemoApp: App {
- @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
- @Environment(\.scenePhase) var scenePhase
- var body: some Scene {
- WindowGroup {
- ContentView()
- }.onChange(of: scenePhase) { newScenePhase in
- switch newScenePhase {
- case .active:
- print("应用启动了")
- case .inactive:
- print("应用休眠了")
- case .background:
- print("应用在后台展示")
- @unknown default:
- print("default")
- }
- }
- }
-}
-```
-
-SwiftUI 的文档写的还是不错。
-
-
-
-
-
-ContentView.swift 及其效果如下:
-
-
-
-上面的代码 `some view` 中,view 是 SwiftUI 一个核心的协议,代表了闭包中元素描述。如下代码所示,其是通过一个 associatedtype 修饰的,带有这种修饰的协议不能作为类型来使用,只能作为类型约束来使用。
-
-通过 Some View 的修饰,其向编译器保证:每次闭包中返回的一定是一个确定,而且遵守 view 协议的类型。这样的设计,为开发者提供了一个灵活的开发模式,抹掉了具体的类型,不需要修改公共 API 来确定每次闭包的返回类型,也降低了代码书写难度。
-
-@State 内部是在 Get 的时候建立数据源与视图的关系,并且返回当前的数据引用,使视图能够获取,在 Set 方法中会监听数据发生变化、会通知 SwiftUI 重新获取视图 body,再通过 Function Builders 方法重构 UI,绘制界面,在绘制过程中会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制,资源浪费。
-
-
-
-## Xcode 对于 SwiftUI 的支持
-
-- Xcode 支持预览
-
-
-
-- 在预览界面选中某个空间,同时按住 `command + 单击`,可以调出一个操作面板。第一个是 UI 检查器,可以查看和修改
-
-
-
- 在代码区域选中控件,同时按住 `command + 单击`,同样可以调出一个操作面板
-
-
-
-- 预览模式下,支持代码和预览界面的实时刷新同步。
-
-
-
-## FunctionBuilder
-
-Swift 源代码路径:`lib/Parse/ParseDecl.cpp`
-
-```swift
-// Historical name for result builders.
-checkInvalidAttrName("_functionBuilder", "resultBuilder",
- DeclAttrKind::ResultBuilder, diag::attr_renamed_warning);
-```
-
-遵循 View 协议的,其实本质上都是调用 body 来绘制 UI 的。`@ViewBuilder` 其实就是 `@_functionBuilder`,编译器会对它所包含的方法有一定的要求,其隐藏在各个容器类型的最后一个闭包参数中。
-
-```swift
-@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
-public protocol View {
-
- /// The type of view representing the body of this view.
- ///
- /// When you create a custom view, Swift infers this type from your
- /// implementation of the required ``View/body-swift.property`` property.
- associatedtype Body : View
-
- /// The content and behavior of the view.
- ///
- /// When you implement a custom view, you must implement a computed
- /// `body` property to provide the content for your view. Return a view
- /// that's composed of built-in views that SwiftUI provides, plus other
- /// composite views that you've already defined:
- ///
- /// struct MyView: View {
- /// var body: some View {
- /// Text("Hello, World!")
- /// }
- /// }
- ///
- /// For more information about composing views and a view hierarchy,
- /// see
-
-
-
-
-
-这种设计有以下好处:
-
-- 声明式编程:通过将 `foregroundColor` 设计为 `ViewModifier`,符合声明式编程范式。意味着你可以通过描述你想要的界面外观和行为,而不是通过编写更新界面的代码,来构建用户界面。这种方式使代码更易于阅读和维护,同时也减少了与界面状态同步相关的错误
-- 链式调用与组合性:`ViewModifier` 允许开发者以链式调用的方式组合多个修饰符,从而轻松创建复杂的视图层次结构
-
-
-
-## SwiftUI 元控件
-
-在 SwiftUI 系统中我们使用结构体遵守 `View` 协议,通过组合现有的控件描述,实现 `Body` 方法,但 `Body` 的方法会不会无限递归下去?
-
-在 SwiftUI 系统中定义了 6 个元/主 View `Text` `Color` `Spacer` `Image` `Shape` `Divider`, 它们都不遵守 View 协议,只是基本的视图数据结构。
-
-其他常见的视图组件都是通过组合元控件和修饰器来组合视图结构的。如 `Button` `Toggle` 等。
-
-
-
-## 状态管理
-
-不像 Vue/React,SwiftUI 关键字太多了,容易搞混淆:@State、@Binding、ObservableObject、@ObservedObject、.environmentObject()、@EnvironmentObject、@StateObject
-
-### @State
-
-和一般的存储属性不同,@State 修饰的值,在 SwiftUI 内部会被自动转换为一对 setter 和 getter,对这个属性进行赋值的操作将会触发 View 的刷新,它的 body 会被再次调用,底层渲染引擎会找出界面上被改变的部分,根据新的属性值计算出新的 View,并进行刷新。
-
-```swift
-import SwiftUI
-
-struct StateDemoView: View {
- @State var name: String = ""
- var body: some View {
- VStack {
- Text(name)
- Spacer().frame(height: 100)
- Button {
- name = "杭城小刘"
- } label: {
- Text("change name")
- }
- }
- }
-}
-```
-
-### @Binding
-
-和 @State 类似,@Binding 也是对属性的修饰,它做的事情是将值语义的属性“转换”为引用语义。对被声明为 @Binding 的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递.
-
-```swift
-struct Dog {
- var name: String = "Unknown"
-}
-struct BindDemoView: View {
- @State var dog: Dog = Dog()
- var body: some View {
- VStack {
- Text(dog.name)
- Spacer().frame(height: 100)
- ChildView(childDog: $dog)
- }
- }
-}
-
-struct ChildView: View {
- @Binding var childDog: Dog
- var body: some View {
- Button {
- childDog.name = "TaoTao"
- } label: {
- Text("点我")
- }
- }
-}
-```
-
-```swift
-@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
-@frozen @propertyWrapper @dynamicMemberLookup public struct Binding
-
-
-
-
-
-### 第二种方式:UIViewControllerRepresentable
-
-- 按照传统的方式写一个 Swift Class。可以按照纯代码的方式写,也可以按照 StoryBoard 结合代码的方式写
-
- CustomViewController.swift
-
- ```swift
- import UIKit
-
- class CustomTableViewCell: UITableViewCell {
-
- @IBOutlet weak var titleLabel: UILabel!
- @IBOutlet weak var contentLabel: UILabel!
-
- override func awakeFromNib() {
- super.awakeFromNib()
-
- }
- }
-
- class CustomViewController: UIViewController {
-
- @IBOutlet weak var tableView: UITableView!
-
- override func viewDidLoad() {
- super.viewDidLoad()
- tableView.delegate = self
- tableView.dataSource = self
- }
- }
-
- extension CustomViewController: UITableViewDataSource, UITableViewDelegate {
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- 100
- }
-
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- var cell:CustomTableViewCell? = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell") as? CustomTableViewCell
- if cell == nil {
- cell = UITableViewCell(style: .default, reuseIdentifier: "CustomTableViewCell") as? CustomTableViewCell
- }
- cell?.titleLabel.text = "第\(indexPath.row + 1)行"
- cell?.contentLabel.text = "我是内容\(indexPath.row + 1)"
- return cell!
- }
-
- func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- 50
- }
-
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- print("点击了第\(indexPath.row + 1)行")
- }
- }
- ```
-
-- 在使用的地方,新建一个结构体,遵循 `UIViewControllerRepresentable` 协议,实现协议方法
-
- - `associatedtype UIViewControllerType : UIViewController` 指定 `associatedtype UIViewControllerType : UIViewController` 关联类型声明,该关联类型指定 `UIViewControllerRepresentable` 对象将桥接的具体的 UIViewController 子类型
- - `@MainActor func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType` 负责创建并配置 `UIViewController` 实例。这个方法在第一次需要创建视图控制器时被调用,允许你在 SwiftUI 中集成和使用 UIKit 中的视图控制器。
- - `@MainActor func updateUIViewController(_ uiViewController: Self.UIViewControllerType, context: Self.Context)` 方法在视图控制器的生命周期中可能会被多次调用,用于更新视图控制器的状态或属性。
-
- UIKitGeneratedViewController.swift
-
- ```swift
- import SwiftUI
-
- struct UIKitGeneratedViewController: View {
- var body: some View {
- VStack {
- CustomViewControllerWarpper()
- }
- }
- }
-
- struct CustomViewControllerWarpper: UIViewControllerRepresentable {
- typealias UIViewControllerType = CustomViewController
-
- func makeUIViewController(context: Context) -> CustomViewController {
- // 纯代码生成的用 CustomViewController()。 StoryBoard 生成的用 UIStoryboard(name:bundle).instantiateInitialViewController()
- let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "CustomViewController") as! CustomViewController
- return vc
- }
-
- func updateUIViewController(_ uiViewController: CustomViewController, context: Context) {
- print(context)
- }
- }
-
- struct UIKitGeneratedViewController_Previews: PreviewProvider {
- static var previews: some View {
- UIKitGeneratedViewController()
- }
- }
- ```
-
- 使用
-
- ```
- @main
- struct SwiftUIDemoApp: App {
- var body: some Scene {
- WindowGroup {
- UIKitGeneratedViewController()
- }
- }
- }
- ```
-
-
-
-
-
-## 最佳实践
-
-SwiftUI 是个 UI 框架、也是个组件库,核心是为了解决 UI 构建复杂、繁琐的问题。Redux 在前端由来已久,有 store、state、action、middleware、reducer 等角色,多个角色各司其职,不存在团队规范和约定后不遵守的情况。通过 store 来管理状态,状态变化后,使用到该状态的 UI 组件会收到通知,更新 UI。用户点击操作 UI,产生 action,action 经历过一系列 middleware 后来到了 store,store 让 reducer 根据 action 和当前的 state 计算,得到一个新的 state。新的 state 变化了,使用到的地方的 UI 也会自动更新。(数据和 UI 的双向绑定)
-
-
-
-对比 MVC:
-
-- 苹果早期官方给的 MVC 缺少了状态管理能力,导致了实现状态管理,控制器的代码很复杂。
-- 另一个问题是状态传递很混乱,不同的开发有自己的偏好: callback、delegate、kvo、notification,代码中可能存在多种状态传递的手段,代码的可读性、可维护性下降,团队协作很困难。
-- 很难看出某些状态会和哪些 UI 相关,在实际开发迭代、修复 bug 的过程中很容易引入新 bug
-
-对比 MVVM:
-
-- 状态绑定的代码比较多,代码冗余比较多,比较枯燥且可能出错。选哪个技术手段也容易受到挑战。而 Redux 则是框架已经帮忙处理好了状态绑定。
-- 可测性。容易测试业务逻辑,相对于 ViewModel 的测试,对 reducer 进行测试更容易编写,reducer 是纯函数,对于给定的输入,输出也恒定,不会修改外部状态。
-- 代码风格较 Redux 不够统一,导致代码易读性不如 Redux。Redux 多个角色清晰分明,没有理解成本,对于框架层的东西,团队小伙伴不需要按照“素质”、“约定”去遵循实现。
-- 状态的改变较 Redux 不容易跟踪。如果出了问题,需要调试比较麻烦。
-- 状态传递不太方便。如果需要将状态传递到比较深的视图上,往往是不太方便的。而 Redux 可以通过框架的能力轻松的将状态送到任何地方。
-
-
-
-### 开源项目
-
-Apple 推出了 SwiftUI,但没有像最早 MVC 一样,在 SwiftUI 中推出一个状态管理的官方架构,虽然 SwiftUI 有 `@State`、`@ObservedObject` 、`@StateObject` 等,但这些东西在不同父子组件、兄弟组件的状态传递、状态管理 case 下,该如何组织是一个没有规范的问题。另外单测困难,因为逻辑代码耦合在 View 相关的代码中。为此,业界借鉴前端领域的 Redux, Redux-like 的方案很多,比较有名的是 [ReSwift](https://github.com/ReSwift/ReSwift) 和 [TCA](https://github.com/pointfreeco/swift-composable-architecture),个人更倾向于 TCA,全称是 The Composable Architecture。
-
-利用 TCA 做一个简易版的计数器 App
-
-安装依赖:File -> Add Packages,输入 `swift-composable-architecture` 搜索,点击右下角 Add Package 即可。
-
-
-
-然后开始开发:先编写 Reducer 部分,再开发相关 UI
-
-```swift
-// Counter.swift
-import ComposableArchitecture
-import SwiftUI
-
-struct Counter: Reducer {
- // State
- struct State: Equatable {
- var count: Int = 0
- }
- // Action
- enum Action {
- case increment
- case decrement
- case reset
- case setCount(String)
- }
- // Reducer
- var body: some Reducer
-
-说明:
-
-- 发送消息,而非直接改变状态。按钮响应事件里,通过 store 发送 action 的方式 `store.send(.increment)`
-- 只在 Reducer 中改变状态。类似 `func reduce(into state: inout State, action: Action) -> Effect
-
-如果需要在测试的时候使用“重复测试”功能,右击测试按钮,在弹出框里做重复测试的配置修改。
-
-
-
-### 动手做一个简易版 Redux
-
-新建 `Redux.swift ` 是一个纯逻辑 Swift 文件。
-
-```swift
-//
-// Redux.swift
-// SwiftUIDemo
-//
-// Created by Unix_Kernel on 4/3/24.
-//
-
-import Foundation
-
-protocol Action {}
-class IncreaseAction: Action {}
-class DecreaseAction: Action {}
-
-struct ReduxState {
- var count: Int
- init(count: Int) {
- self.count = count
- }
-}
-
-typealias Reducer = (ReduxState, Action) -> ReduxState
-
-final class Store: ObservableObject {
- var reducer: Reducer
- @Published private (set) var state: ReduxState
-
- init(reducer: @escaping Reducer, state: ReduxState) {
- self.reducer = reducer
- self.state = state
- }
-
- func dispatch(_ action: Action) {
- self.state = self.reducer(self.state, action)
- }
-}
-```
-
-使用的地方
-
-工程入口文件 `SwiftUIDemoApp.swift`
-
-```swift
-@main
-struct SwiftUIDemoApp: App {
- @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
- @Environment(\.scenePhase) var scenePhase
- var body: some Scene {
- WindowGroup {
- let state = ReduxState(count: 0)
- let reducer: Reducer = { (state, action) -> ReduxState in
- switch action {
- case is IncreaseAction:
- return ReduxState(count: state.count + 1)
- case is DecreaseAction:
- return ReduxState(count: state.count - 1)
- default:
- return ReduxState(count: state.count)
- }
- }
- let store: Store = Store(reducer: reducer, state: state)
- ReduxDemoView().environmentObject(store)
- }
- }
-}
-```
-
-另一个展示的页面 `ReduxDemoView.swift`
-
-```swift
-//
-// ReduxDemoView.swift
-// SwiftUIDemo
-//
-// Created by Unix_Kernel on 4/3/24.
-//
-
-import SwiftUI
-
-struct ReduxDemoView: View {
- @EnvironmentObject private var store: Store
- var body: some View {
- VStack {
- Text("数据:\(store.state.count)")
- Spacer()
- .frame(height: 50)
- HStack {
- Button {
- store.dispatch(IncreaseAction())
- } label: {
- Text("+1")
- }
- Spacer()
- .frame(width: 100)
- Button {
- store.dispatch(DecreaseAction())
- } label: {
- Text("-1")
- }
- }.buttonStyle(.borderedProminent)
- }
- }
-}
-
-struct ReduxDemoView_Previews: PreviewProvider {
- static var previews: some View {
- ReduxDemoView()
- }
-}
-```
-
-实现效果如下:
-
-
-
-## 核心技术
-
-### SwiftUI 的渲染机制
-
-Render loop 是驱动 SwiftUI 进行渲染更新的重要机制,了解它的原理和策略,可以揭秘 SwiftUI 高性能背后的秘密。
-
-- event loop:事件循环,基于消息事件的循环,例如触摸被系统包装成一个事件一层一层传递给 UI 组件并最终触发 UI 组件渲染。
-
-- render loop:渲染循环,是一个更小的概念,更多关注在消息处理和屏幕渲染上
-
-- invalidated:无效、失效,类似于 Flutter 的 dirty 。当一个 View 的关联属性改变了,或者其他原因导致 View 需要刷新,View 就会被标记为 invalidated,此时框架会对 View 的body 进行 evaluate 。
-
-- evaluate:直译是评估,我更倾向于翻译成计算,也就是当框架发现一个 View 被标记为 invalidated 后,框架会尝试比对改变前和改变后的 body 内容。如果框架认为 body 内容改变了,就会重新渲染。注意,evaluation 并不一定会导致重新渲染,这取决于框架对 body 的评估结果。评估虽然不会必然导致渲染,但框架仍需读取 body 数据并进行(可能复杂的)计算以确定内容是否改变。
-
-GUI 的本质离不开 EventLoop,对于 iOS 来说,无论 UIKit 还是 SwiftUI 背后都是 RunLoop。RunLoop 会向 UI 代码分发消息,进而出发屏幕的一部分重新渲染。消息的处理和屏幕上的图形渲染构成一个应用程序的 render loop。
-
-#### onAppear
-
-在 SwiftUI 中,我们没法得到像 UIKit 中那么丰富的视图生命周期。如果我们想在一个视图出现时执行一个动作,我们只能使用一个函数:`onAppear` 。但是它到底是什么时候被调用的呢?是不是像 `viewWillAppear` 那样,在视图被渲染并在屏幕上可见之前调用?如果是的话,我们可以信赖它吗?
-
-```swift
-// ViewModel.h
-import Foundation
-class ViewModel: ObservableObject {
- @Published var statusText: String = "invalid"
-
- func fetch() {
- self.statusText = "loading"
- }
-}
-// ContentView.swift
-struct ContentView: View {
- let store: StoreOf
-
-如果你在 Xcode 调试器中暂停一个 iOS 应用程序,主线程的堆栈信息中便会出现下面的调用栈
-
-```shell
-* frame #0: libsystem_kernel.dylib`mach_msg_trap + 10
- frame #1: libsystem_kernel.dylib`mach_msg + 59
- frame #2: CoreFoundation`__CFRunLoopServiceMachPort + 319
- frame #3: CoreFoundation`__CFRunLoopRun + 1249
-```
-
-`mach_msg` 是系统调用,`CFRunLoop` 用它来等待多个可能的事件中的任何一个。在这期间,我们的应用程序没有使用 CPU ,或者至少主线程没有使用。
-
-一个 `CFRunLoop` 被配置为一组传递事件的输入源。当一个应用程序被启动时,它在主线程上启动一个 run loop ,用一个 input sources(输入源)来传递触摸事件。其他的输入源之后也可以被添加到其中。你也可以在辅助线程上启动新的 run loop 。我们可以用一个带有两个输入源的 `CFRunLoop` 实现一个处理用户输入和网络事件的命令行程序。
-
-来自输入源的事件会按照特定的顺序[进行处理](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Farchive%2Fdocumentation%2FCocoa%2FConceptual%2FMultithreading%2FRunLoopManagement%2FRunLoopManagement.html)。run loop 一共有 4 种类型的输入源:
-
-- input sources 0。这是自定义的输入源,它们手动调用 `CFRunLoop` 函数来传递事件。iOS 应用程序中的触摸事件在一个辅助线程上处理,然后通过 input sources 0 送到主线程的 run loop 中。
-- input sources 1。这是基于机器端口的输入源。例如 CADisplayLink,可用于将绘图代码与显示器刷新率同步。异步网络代码也可以使用 input sources 1。(然而,请注意,许多网络库在内部调度队列上使用阻塞的 I/O 调用来代替网络调用,然后通过主调度队列将代码调度到主线程)
-- Timer sources。计时器,如Timer,使用这种特殊的输入源。
-- The main dispatch queue。调度到主队列的代码,以及与主队列相关的调度源也构成了一个输入源。这允许旧代码和基于调度的代码之间的沟通。(其他队列没有基于 `CFRunLoop` 实现)
-
-除了添加输入源,我们还可以向 `CFRunLoop` 添加观察者,当 run loop 到达特定周期时会发送通知。run loop 的周期是由 `CFRunLoopActivity` 定义的,观察者可以选择对其中的一个或几个周期进行监听。run loop 观察者在苹果自己的框架中被广泛使用
-
-```c
-__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
-__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
-__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
-__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
-__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
-```
-
-
-
-#### Core Animation & render Server
-
-你是否有过这样的经历:当一个应用程序出现卡顿时,你认为它不可能是卡顿,因为还有一些动画在进行中?即使应用程序的主线程被卡住,指示器(菊花)仍在旋转,这总是让我困惑。即使主线程繁忙或暂停,iOS中的动画也可以继续。这不是因为动画发生在另一个**线程**中,而是因为它们发生在另一个**进程**中。
-
-Core Animation 与 render server 对话,告诉它要画什么和做什么动画。一般来说,我们会对一个视图进行多次修改,作为对用户操作的反馈。在 UIKit 中,为了响应一个按钮的点击,你可能会同时改变一个视图的大小和背景颜色,或者你可能会调用多个方法来触发 `setNeedsDisplay` 。如果我们每改变一个参数就渲染一次,很明显效率会非常低,也会导致一些奇怪的问题。为了告诉系统该把哪几个参数打包一起渲染,Core Animation 框架暴露了 `CATransactions` 。
-
-`CATransaction` 包含了 `begin` 和 `commit` 两个方法。你可以手动 begin (启动)和 commit (提交)一个 `CATransaction` 事务。如果你不主动调用 `CATransaction` API,`CATransaction` 也会在引擎下被隐式调用。
-
-Demo
-
-```swift
-@IBAction func buttonPress() {
- self.view.backgroundColor = .red
- sleep(2)
- self.view.backgroundColor = .white
-}
-```
-
-在按下按钮后,应用程序被卡住 2 秒,但它所处的视图的背景颜色保持为白色。视图层的变化在睡眠前没有被渲染。这是因为设置背景颜色启动(begin)了一个隐式渲染事务,而这个事务在睡眠前没有提交(commit)。
-
-改进
-
-```swift
-@IBAction func buttonPress() {
- CATransaction.begin()
- self.view.backgroundColor = .red
- CATransaction.commit()
- sleep(2)
- self.view.backgroundColor = .white
-}
-```
-
-我们现在按下这个按钮,它所在的视图的背景颜色就会变成红色,然后在应用程序卡住的时候保持红色两秒钟,然后变成白色。我们主动提交(commit)了一个事务,因此视图在睡眠之前改变了颜色。由此可以推断,仅仅改变一个视图的背景颜色(不主动调用 CATransaction ),只会隐式地创建渲染事务,并不会去提交这个事务。
-
-那么隐式的事务究竟何时提交?答案是:每当一个隐式事务被启动,就会在当前 run loop 周期结束时被安排提交。它的底层是用一个 run loop 观察者来完成的,这个观察者是由 Core Animation 添加到主 `CFRunLoop` 中的,观察的周期是 `CFRunLoopActivity.beforeWaiting`。
-
-`CATransaction` 是可嵌套的。你可以在一个 `CATransaction` 里面启动另一个 `CATransaction` ,**但是只有外部事务会被用来渲染和改变屏幕内容**。外层事务可以是一个被隐式地启动的事务。举个例子:有些控件可能在调用它们的 action handlers 之前就已经调用了动画代码,动画代码启动了一个隐式事务。然后当你在 action handlers 内使用显式事务(手动调用 `CATransaction.commit()` )对一个图层进行修改时,提交它不会立即产生任何效果(需要等外层的隐式事务提交时才会改变)。
-
-虽然你在 SwiftUI 应用程序中不直接使用 CATransactions,但 SwiftUI 框架在内部仍然使用 Core Animation 和 CATransactions 进行绘制和动画。与 render server 一起,Core Animation 对 iOS 来说是非常基础的。
-
-#### 触摸事件和显示器刷新率
-
-需要自定义动画或使用物理引擎的应用程序可以使用 `CADisplayLink` 来使绘图代码与显示器的刷新率同步。在这个 API 可用之前,特别是游戏开发者很难做到这一点,他们不得不使用 `NSTimer` 并想办法绕过很多限制。
-
-应用程序从操作系统接收触摸事件的频率与显示屏刷新的频率相同。这是合理的,因为我们使用触摸来更新视图,如果比显示的频率更高,那就是一种浪费。但是,如果我们将收到这些触摸事件的时间与 `CADisplayLink` 启动的时间进行比较,我们会看到它们并不完全同步。
-
-在具有高触摸刷新率的 iPhone 上,一个显示刷新周期内会发生多个触摸事件,但我们不会单独接收它们。在 UIKit 中,我们可以从 UITouch 对象中获得那些中间的触摸事件。
-
-所有的 run loop 输入源,包括用于实现 `CADisplayLink` 和接收触摸的输入源,都以不同的方式应对系统繁忙的情况。如果多个触摸事件发生时,应用程序仍在忙于响应前一个触摸,它们将不会被单独传递,但仍可从最近的触摸事件中恢复触摸。相反,如果在下一次显示刷新即将发生时系统仍在忙碌, `CADisplayLink` 根本不会通知我们。
-
-
-
-#### 全貌
-
-
-
-
-
-
-
-当 APP 不做任何事情时,一个 SwiftUI 应用程序将有一个空闲的 CFRunLoop 。CFRunLoop 将等待来自输入源的事件,如触摸、网络事件、定时器或显示器刷新。为了响应触摸,SwiftUI 可能会调用一个 Button 的 `action handler`。如果我们在 `action handler` 中设置一个断点,我们会在堆栈跟踪中看到 `__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__` 。这是因为触摸事件是由 input sources 0 输入源传递的。
-
-为了响应来自输入源的事件,我们可能会修改视图的一些 `@State` 变量,或者在 `@ObservedObject ` 上调用一个函数,进而触发 `objectWillChange`,SwiftUI 视图会被标记为 invalidated(无效),意味着它的 body 需要被重新评估,但它不是立即重新评估,之后会评估。因为会可能存在这样一个 case:T1 时刻,值修改为1,T2 时刻修改为2,T3 时刻修改为1,如果每次都立即评估,效率会很低。
-
-那之后是什么时候?给 body 方法内加断点,在 LLDB 输入` bt` 可以看到
-
-```shell
-(lldb) bt
-* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
- * frame #0: 0x000000010c64f2cd TCADemo`closure #1 in closure #2 in ContentView.body.getter(self=TCADemo.ContentView @ 0x00007ff7b38aea80, viewStore=0x0000600003ccd140) at ContentView.swift:19:22
- frame #1: 0x000000010c65100e TCADemo`partial apply for closure #1 in closure #2 in ContentView.body.getter at
-
-
-
-上图中这两只狗狗,到底是不是同一个呢?我们似乎无法准确地给出答案。为什么呢?因为我们缺乏一些关键信息,那就是 Identity。
-
-所以当 SwiftUI 处理你的界面描述时,它也需要 Identity 这个关键信息区分视图是否是同一个。
-
-
-
-让我们来看下上面这个 Good Dog, Bad Dog 的小应用,你可以点击屏幕上的任何位置来切换狗狗的状态。但是我们从技术层面分析,上面的界面可以有两种 SwiftUI 的描述方式:
-
-1. 自定义两个完全不同的 SwiftUI View,根据当前狗狗的状态去做逻辑判断描述
-2. 把上面的界面描述成一个 SwiftUI 自定义 View,在区别展示的地方,用不同的颜色来区分
-
-这两种 SwiftUI 的描述方式,会让视图从一种状态过渡到另一种状态的方式截然不同:
-
-- 按照第一种方式,由于是完全不同的视图,就意味着上面狗爪子的图标应该独立执行过渡动画,最终看起来只有淡入和淡出的效果
-- 按照第二种方式,SwiftUI 内部认为它是同一个视图,这就意味着在过渡期间,狗爪子图标会执行在屏幕上滑动的动画效果
-
-可以看出 SwiftUI 在处理过渡动画的时候,会根据不同状态下的 View 是如何连接的来进行处理,而决定 View 连接方式的关键就是 **View Identity**:
-
-- 共享 Identity 的 View 代表的是同一个 UI 界面元素,只是处在不同状态下而已 (Same identity = Same element)
-- 代表不同 UI 界面元素的 View,它的 Identity 也总是不同 (Different identities = Distinct elements)
-
-Identity 既然这么重要,那么开发者是如何用代码来定义的呢?在 SwiftUI 中分两种方式来定义 Identity:
-
-- 声明式 Identity,一般是在 View 上添加一个 `id(_:)` 修饰器或者在数据驱动列表控件中显示声明 Identifier,如 ForEach、List。参考前端 Vue、React List 中的 id
-- 结构性 Identity,是 SwiftUI 根据 View 的类型和层级结构来动态识别,虽然这种 Identity 不需要开发者指定,但也需要开发者清晰的将 View 的层级结构描述出来,方便 SwiftUI 内部识别。类似 xpath,根据 UI 层级和结构,生成唯一的 Identity
-
-
-
-##### 声明式 Identity
-
-
-
-就像上面这两只狗狗,仅通过图片,很难判断这是不是同一只狗狗,但如果我们能用名字来标识它们。就很容易得出结论。像这样给狗狗起名字来标识它们的方式,就是在显式声明 Identity。
-
-需要注意的是,声明式 Identity 是非常强大且灵活的。我们在之前 AppKit 或 UIKit 中编写界面的方式,其实就是采用的显式声明 Identity 的方式。怎么理解?由于 UIView 和 NSView 都是类,引用类型,所以它们的实例其实是一个指针,这个指针指向了一块内存空间。其中指针所代表的内存地址就是一种显式声明的 Identity。
-
-我们可以通过视图的指针来标识每个视图,如果多个视图指针,都共享同一块内存空间,那么它们其实是同一个视图,如下图所示:
-
-
-
-
-
-问题来了,SwiftUI 中的 View 都是结构体, 值类型,没有指针的概念,那 SwiftUI 怎么来唯一标识一个 View 的呢?
-
-其实,SwiftUI 是用另外一种形式来显式标识 View。通过下面的例子能更好的理解,例如在这个救援犬列表里,用 dogTagID KeyPath 获取相应属性,在参数里指定到 id 上,就是在显式声明 View 的 Identity。这样就能标识出每条数据对应的展示视图。一旦列表数据发生变化, SwiftUI 可以根据这些 ID 来判断,哪些视图需要新生成,哪些视图重复使用,只需要执行动画。
-
-
-
-##### 结构性 Identity
-
-不显式声明 Identity,这并不意味着这些 View 根本没有 Identity,也就是说每个 View 都有一个 Identity,即使它不是显式声明出来的。在这种情况下 SwiftUI 内部会对没有显式 Identity 的 View 根据它的描述层级结构生成一种隐式的 Identity,就叫做结构性 Identity。
-
-
-
-如上图,假设我们有两只相似的狗狗,但我们不知道它们的名字,我们仍然还要标识出它们。这时候可以通过它们坐的位置来标识,如 `左边的狗` 或 `右边的狗`。
-
-像这种利用排列位置的不同,来区分它们的方式就是所谓的 **结构性 Identity**。
-
-SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来标识 View。一个典型的例子就是在 SwiftUI 中 使用 if else 条件判断的时候,条件语句的结构使得 SwiftUI 能够明确的识别每个 View,如下图,第一个 AdoptionDirectory 只在条件为 True 的时候显示,第二个 DogList 只在条件为 False 的显示。
-
-
-
-但是 SwiftUI body 计算属性需要一个明确一致的返回类型,但 if else 条件判断使得返回类型不一致了,会引起编译失败。这时候 SwiftUI 引入了一个的黑魔法 - **ViewBuilder (默认是附加在 body 计算属性上的,不需要开发者单独指定)**。ViewBuilder 帮助 SwiftUI 把各种条件判断,封装成 _ConditionalContent 的数据结构。但为了区分在不同分支下的类型不同,用泛型来进行了区分,这样即保证了返回数据的一致性,又保证了 SwiftUI 内部可以通过泛型识别出不同分支下结构性 Identity。
-
-见源代码:
-
-
-
-
-
-如下图,我们只用一个 PawView 自定义View,在这个自定义的 View 的 Modifier 上利用三目运算的方式来动态改变需要变化部分的数值,当在不同状态之间发生界面切换的时候,由于始终是一个视图元素,所以就会执行平滑的滑动动画。
-
-其实,如果你回到 UIKit 中理解,也是一样的,我们在 UIView 上执行动画的时候,一般也是在同一个 UIView 的实例里去动态改变它的属性去修改样式, 才会有那种平滑过渡的效果;相反,如果虽然是同一个类型的 UIView,但是对应的是不同的实例,去做那种平滑的过渡效果,也是很难实现的。
-
-
-
-综上,**在使用结构性 Identity 的时候,第二种描述 View 的方式是更好的选择。应该尽量避免切换 Identity,这样做会给动画和性能都带来良好的效果,也有利于维持视图的生命周期和数据状态。**
-
-
-
-#### 危险的 AnyView
-
-说起 AnyView ,这家伙绝对是 Identity 的克星。
-
-
-
-上图是一个使用 AnyView 的示例代码。在这个自定义 View 中,为了保证最终返回一个明确一致的数据类型,每个分支都用一个 AnyView 包裹起来。由于 AnyView 隐藏了所包装视图的类型,让 SwiftUI 无法在条件判断中识别出结构性 Identity,在 SwiftUI 眼里,它看到的都是一些擦除类型的 AnyView,更要命的是,这段代码阅读起来特别困难。
-
-那么,接下来让我们用正确的方式来重构这段代码:
-
-- 第一步:消除 AnyView 包裹,把内部具体的 View 类型暴露出来
-- 第二步:去掉 所有的 return 关键字
-- 第三步:在方法上添加 @ViewBuilder 标识,保证最终返回的是一个明确的类型,编译通过
-- 第四步:由于我们只是在 dog 的 breed 状态之间来回判断,那么把 if else 改为 switch case 会更合适
-
-重构后,最终代码和 View 层级结构如下图:
-
-
-
-一般情况下,还是尽量避免使用 AnyView,因为 AnyView 有如下缺陷:
-
-- 代码难于阅读
-- 由于擦除了所有的 View 类型,无法在编译的过程中给出相应的提示
-- 可能会导致不必要的性能损失
-
-
-
-
-
-### 生命周期(Lifetime)
-
-#### Lifetime 与 Identity 的关系
-
-
-
-如上图,这里有个叫 Theseus 的小猫。他在一天中可能有各种不同的状态,一会装可爱,一会睡觉,一会发火,但是无论处于何种状态,他都是那只叫 Theseus 的小猫。
-
-视图在整个生命周期内有各种不同的状态,每个状态在 SwiftUI 中由不同的 View 实例(值类型)来描述,而 Identity 将这些不同状态下的 View 值随着时间的推移关联起来,它们都对应着同一个视图元素。这就是 Identity 与视图生命周期建立联系的本质。
-
-
-
-让我们用上面的代码来更清晰的理解这一点,这里我们有一个简单的自定义 View - PurrDecibelView,用来显示猫叫声的强度。一开始的时候 SwiftUI 调用 body 计算属性,获取到叫声为 25 的 View,但是突然小猫饿了,希望获得更多的关注,叫声变大为50,这时候 SwiftUI 监听到叫声这个状态的变化,重新调用 body 计算属性,获取到一个全新的 View,这两个 View 是完全截然不同的两个值。SwiftUI 会在后台对这两个值进行对比并对比出哪些部分发生了变化。得出对比结果后,告诉渲染视图执行变化部分的渲染操作,同时,用完的 View 值也会被销毁。
-
-这里非常重要的一点就是 View 的值跟 Identity 生命周期是不同的。值类型的 View 生命周期是非常短暂的。开发者要控制好的其实是它们的 Identity。也就是说,随着时间的推移, SwiftUI 创建很多新的 View 用来描述视图当前状态下的显示方式,但是 SwiftUI 内部只是拿这些 View 来进行样式和布局的对比,用完了这些 View 值就会销毁,其内部用 Identity 唯一标识的那个视图(RenderNode)会一直在内存中,并且一直都是同一个。但是一旦 Identity 发生变化,内部的视图元素生命周期也会结束。
-
-所以,如下图,我们经常用到的生命周期方法 onAppear 和 onDisappear,其实是在视图显示和消失的时候触发,而不是 View 创建和销毁的时候触发。
-
-
-
-所以最终我们得出如下公式来阐述 View,LifeTime,Identity 三者之间的关系:
-
-- View Value ≠ View Identity
-- View(视图)'s LifeTime = duration of the Identity
-
-视图和 struct 值类型的 View 没有严格对应关系,但持续可见的视图必然对应一个 identity。一个视图对应 >= 1个 DSL 描述的 View(也就是结构体)
-
-
-
-#### Lifetime 与 State 的关系
-
-理解了 Identity 与视图生命周期之间的联系,也能够帮助你更好地理解 SwiftUI 如何维持数据状态
-
-提到维持数据状态,那肯定要用到 State 和 StateObject。这两个状态管理工具可以保证在不同的 View 实例被创建的时候,封装的数据能够一直维持在内存中,相当于一种内存记忆。但是你去看它们的定义会发现它们都是结构体。按理说,在每次创建新的 View 实例后,应该就销毁重新生成了,那咋维持数据的呀?其实它们内部都会有一个 Storage 类,用来存储它们所修饰的数据。当一个视图根据 Identity 第一次创建的时候,SwiftUI 在内部为 State 和 StateObject 的 Storage 分配相应的内存空间,用来保存状态的初始值。**注意这里的 Storage 跟 Identity 是对应的,生命周期也是一致的**。
-
-如下图的 CatRecorder 自定义 View,每次的 title 发生变化,由于他被 @State 修饰,SwiftUI 内部会在内存中保存这个数据,并且监听他的变化,一旦发生变化,就调用 body,重新计算。
-
-
-
-下面,让我们来看一下在有分支的情况下,视图生命周期和数据状态之间的关系。
-
-
-
-如上图代码,分支里的两个 CatRecorder 由于结构性 Identity 不同,所以它们被 SwiftUI 视为是两个不同的视图。之前说过这样会影响动画效果,其实也会影响它们内部数据状态的维持。
-
-比方说,第一次进入的是 True 分支,SwiftUI 会为 CatRecorder 生成一个新的视图,并为数据分配内存空间,以存储状态的初始值。当 CatRecorder 内部状态发生变化时,只要都是在 True 分支下,由于 Identity 没变,所以还是同一个视图,所以状态也会连续性的变化,不会有数据丢失的情况。但是一旦 dayTime 发生变化,进入了 False 分支,SwiftUI 发现 Identity 发生了变化,会生成新的视图和与之对应状态的内存空间,这时候新的 CatRecorder 内部的所有状态都是初始值。True 分支下的视图和对应的状态接下来也会被释放。如果我们再切回到 True 分支,之前 True 分支的状态也回不来了,因为相较于上次的 View 类型,这又是一个全新的 Identity,会重新创建视图和数据状态存储空间。所以,最终分支切换后,在界面上有时候会发现记录的小猫状态突然丢失了。
-
-所以可以得出的结论是:View Identity 一旦变化,视图内部对应的数据状态也会被重新替换。也就是说:
-
-`State's Lifetime = 视图's Lifetime != View's Lifetime`
-
-
-
-### 稳定的 Identity
-
-保证 Identity 稳定,这一点非常重要,尤其是在使用数据驱动型的列表控件时,在下面这些控件中,往往都需要用数据的 id 来给 View 显式声明 Identity。
-
-
-
-下面两张图是两种不同的 ForEach 的用法。其中第一种用法是一个常数的 Range,SwiftUI 可以直接用 Range 的值来为视图生成 Identity,以确保在视图的整个生命周期内 Identity 是稳定的。但当使用一个动态的 Range 时,会导致声明式的 Identity 数值是不可预期的,Identity 一旦切换,视图都会重新生成,这样就会出现性能问题。 所以在 Xcode 12 的时候,检查到这种使用方式会编译报错,而在新版本的 Xcode 13 这将变为一个警告 (beta 版本似乎不生效)。
-
-```swift
-ForEach(0..<5) { offset in
- Text("🐑 \(offset)")
-}
-
-ForEach(0..
-
-看上面这张图的话,是一个树结构,但是有可能多个视图都依赖同一个状态。有可能某个子视图也依赖顶级视图中的状态。情况越来越复杂后,这就不再是一个树结构。重新整理,避免让连接线之间交叉,如下图,可以看出它们之间的关系实际上是一个图结构。我们可以称之为**依赖关系图**。
-
-
-
-深入的理解这个依赖关系图很重要,因为它保证了 SwiftUI 只更新那些需要重新调用 body 的 View。以最底部的依赖关系为例。如果我们检查这个依赖关系,会发现有两个 View 依赖它,当依赖的数据状态发生变化,只有这两个 View 会被标记为无效。同时 SwiftUI 开始调用每个视图的 body 计算属性,只为标记为无效的视图产生一个新的 body 值。
-
-
-
-状态管理工具,在 SwiftUI 依赖关系的建立就是通过它们来实现的:
-
-- @Binding
-- @Environment
-- @State
-- @StateObject
-- @ObservableObject
-- @EnvironmentObject
-
-
-
-### 改进 Identity
-
-Identity 就是依赖关系图的灵魂,重要性不言而喻。正如之前所说,Identity 用来标识一个视图,所以 SwiftUI 会根据 Identity 来高效的判断哪些视图需要更新,哪些视图需要新建,哪些视图需要销毁。
-
-#### 稳定性
-
-对于开发者来说,首先要确保的就是 Identity 的稳定性。稳定的 Identity 会给 SwiftUI 带来如下好处:
-
-- 确保视图生命周期的准确性,一个视图的生命周期是由 Identity 来决定的,一个不稳定的 Identity 会导致视图生命周期意外缩短
-- 提高应用程序的性能,SwiftUI 无需在依赖关系图更新的过程中为不必要的视图和状态重新分配内存空间
-- 缩小影响依赖关系影响的范围
-- 保证数据状态不会无故丢失
-
-在下图的例子中,每次都生成一个 UUID 和 直接用 Indices 来显式声明 Identity 都是不稳定的方式,因为它们都会随着时间推移发生变化,不能准确地标识一个视图,最终导致的结果就是,当我们在列表头部新插入数据时,整个列表都会重新刷新。相反,我们如果用一个 databaseID 就是可以的,因为这个 ID 只对应一个数据,能够清晰的标识一个与该数据对应的视图。这时候我们在头部新插入数据,所有的动画效果都非常自然了。
-
-
-
-
-
-
-
-#### 唯一性
-
-但是只保证 Identity 的稳定性还是不够的。好的 Identity 还要确保唯一性。每个 Identity 都应该准确映射到一个单一的视图。
-
-唯一的 Identity 会给 SwiftUI 带来如下好处:
-
-- 平滑的动画效果
-- 同样可以提高性能
-- 准确地的反应视图和状态之间的依赖关系
-
-像下面的代码中使用 name 的 KeyPath 来给 View 显式声明 Identity,是不合理的,因为我们无法保证 name 的唯一性,一旦出现重名的情况,新的视图很有可能不会展示出来。但当把 name 换成 serialNumber,一切都正常了
-
-
-
-
-
-
-
-#### 去分支
-
-上面,我们都是用声明式 Identity 来说明如何改进 Identity,接下来看看如何改进结构性 Identity。
-
-
-
-上面的代码,乍一看似乎没什么问题。但是仔细分析会发现,这里有个性能问题。**content 在不同的分支条件下,会产生不同的结构性 Identity,这就导致了分支切换后针对同一个 View 会生成两个不同的视图元素,也就是在内存中分配两份内存空间**。这点其实是可以避免的。虽然这里我们很轻易的发现了这个问题,但当项目大了之后,有可能这些 ViewModifier 的代码都不在一起,所以这种问题很容易被忽视。
-
-修改:把分支结构去掉,改为在 opacity 修饰器上添加三目运算的方式来动态修改透明度。由于去掉了分支结构,所以 content 只会生成单一的结构性 Identity,也就避免了不必要的内存开销,提高了性能。
-
-
-
-像上面代码直接把透明度设置为 1,也就是跟初始状态一致,其实 SwiftUI 发现这种情况是不执行任何渲染操作的。我们把这样的修饰器称为 "惰性修饰器",因为它们不影响渲染的结果。
-
-#### 最佳实践
-
-是不是跟觉得在 SwiftUI 中使用条件分支很可怕?不要担心,想用分支的时候还是得用,只是用完之后,要多考虑下这个地方用分支来描述 View 结构的必要性,也就是要考虑当前代码的 View 到底是用来代表多个视图还是代表同一个视图的不同状态。
-
-如果是代表同一个视图的不同状态,那么使用一个惰性修饰器来标识一个单一的视图,往往是更好的选择。
-
-在下图中还给出了一些其他的惰性修饰器作为参考:
-
-
-
-
-
-### 总结
-
-日常开发的时候可能对 Identity 的思考、认知不够,不知道它原来影响动画、视图生命周期、状态生命周期都有关系。Identity 帮助 SwiftUI 系统做了很多决策。
-
--SwiftUI View 和 视图元素之间采用 Identity 关联起来,它们之间并非一一对应,Identity 有声明式的,也有结构式的。在 SwiftUI 中每当状态发生变化,都会调用对应的 body 生成新的 View 值,但是否生成新的视图则完全由 Identity 来决定。如果 Identity 一致,就会根据 Identity 去内存中查找之前创建的视图,换言之,相当于保持之前视图的生命周期,并且在内存中用类维持住之前的数据状态,只对更改数据后,视图变化的部分进行渲染操作。如果 Identity 不一致,则会新建视图元素,同时视图所依赖的状态也会被重新分配,回到初始值。
-
-总而言之,View Identity 对 SwiftUI 来说是至关重要的。我们一定要时刻注意 View 的 **显式 Identity** 和 **结构性 Identity**,并提高 Identity 的稳定性,确保 Identity 的唯一性。
-
-
-
-## 开发 tips
-
-要回答好这个问题,其实就是在**聊 UIKit SwiftUI 的能力边界在哪里**?它们各有优缺点,开发人员可以根据具体需求和场景来选择如何搭配使用它们。
-
-SwiftUI 的优势:
-
-- 声明式 UI:和主流的前端框架一样,提供了声明式 UI 编程模式,使得构建和管理 UI 变得简单直观。但它不只是一个开发框架,更是一个组件库,具备一些常见的 UI 组件能力(能力小于等于 UIKit)
-- 响应式 UI:支持响应式编程,可以轻松处理各种状态,聚焦于逻辑。又和前端主流框架做了一样的事情(不如说是大前端优秀的设计在客户端落地,并在官方侧取得了支持)
-- 跨平台特性:SwiftUI 可用于 iOS、macOS、watchOS、tvOS 的应用程序,具备较好的跨平台特性
-
-UIKit 的优势:
-
-- 成熟的生态系统:UIKit 拥有丰富的第三方库和组件,可以满足各种复杂的 UI 和功能需求
-- 定制能力:UIKit 提供了更多的自定义和底层控制能力,适用于需要高度定制化的界面和交互
-
-在实际开发中,可以根据以下场景来搭配使用 SwiftUI 和 UIKit:
-
-- 逐步迁移:对于已有的 UIKit 项目,可以逐步引入 SwiftUI,例如在新功能或模块中使用 SwiftUI,逐步迁移现有界面和功能。但需要考虑的是 SwiftUI 必须用 Swift 语言开发,新语言开发简单,但背后的比如 Crash 监控、热修复、动态路由、打包构建系统等,新语言如何与现有的基建打通是需要调研和考虑的一个事情。不过都2024了,Apple 官方拥抱了类似大前端很成熟的声明式开发、响应式编程,客户端同学该开始 SwiftUI 了。
-- 混合使用:可以在同一个应用程序中同时使用 SwiftUI 和 UIKit,根据具体需求选择合适的界面构建方式。例如,可以使用 SwiftUI 构建应用程序的主界面,同时使用 UIKit 来展示特定的复杂界面或功能。因为 SwiftUI 封装的是大多数常见、高频使用的 UI 组件,所以不可能满足所有需求,那些交互复杂的,还需要使用 UIKit 的能力。
-- 复用现有组件:可以将现有的 UIKit View 或者 ViewController 封装为 SwiftUI 可以用的组件。遵循 `UIViewRepresentable` 或 `UIViewControllerRepresentable` 协议即可。
-- 跨平台开发:对于需要在多个平台上展示相似界面的应用程序,可以使用 SwiftUI 来实现跨平台的 UI 共享,同时使用 UIKit 来处理特定平台的细节和定制化
-
-一言以蔽之:大多数情况下,使用 UIKit 足以,但想要使用新的框架, SwiftUI 很棒,但某些交互复杂的功能无法实现,还需要借助 UIKit 的能力,包括丰富的组件库和开源项目的支持。
-
-
-
-
-
-## 参考资料
-
-- [The SwiftUI render loop](https://rensbr.eu/blog/swiftui-render-loop/?utm_campaign=iOS%2BDev%2BWeekly&utm_medium=rss&utm_source=iOS%2BDev%2BWeekly%2BIssue%2B558)
diff --git a/Chapter1 - iOS/1.129.md b/Chapter1 - iOS/1.129.md
deleted file mode 100644
index eca7a42..0000000
--- a/Chapter1 - iOS/1.129.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Swift Dictionary 扩容机制
-
-
-
-## Dictionary
-
-```swift
-let dic = ["d": 4, "a": 1, "b": 2, "c": 3]
-print(dic)
-// ["c": 3, "d": 4, "a": 1, "b": 2]
-```
-
-Dictionary 顺序会乱序
-
-
-
-## KeyValuePairs
-
-KeyValuePairs 顺序不会乱序
-
-```swift
-let kvs: KeyValuePairs = ["d": 4, "a": 1, "b": 2, "c": 3]
-print(kvs)
-// ["d": 4, "a": 1, "b": 2, "c": 3]
-```
-
-
-
-开放寻址法
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.13.md b/Chapter1 - iOS/1.13.md
deleted file mode 100644
index 2a3b9cf..0000000
--- a/Chapter1 - iOS/1.13.md
+++ /dev/null
@@ -1,226 +0,0 @@
-# UINavagationController重写push和pop方法
-
-> 有个需求就是在App的Tab的首页需要显示浮动着的交互动画的机器人,该机器人具有机器学习的特点,因此可以不断的与用户交互,怎么样实现只浮动在App的5个tab首页,当点击跳转不是首页的时候不需要显示
-
-因为5个tab上是5个自定义的导航控制器,所以我们可以监听导航控制器的push和pop事件,并且在push和pop的事件中判断当前控制器的字控制器的数量来判断窗口上的机器人是否需要显示,其实这里要说的就是如何监听push和pop事件。
-
-```
-/**
-* 重写这个方法的目的:为了拦截整个push过程,拿到所有push进来的子控制器
-*
-* @param viewController 当前push进来的子控制器
-*/
--(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
-{
- // if (viewController != 栈底控制器) {
- if (self.viewControllers.count > 0) {
-
- for (UIView *view in [UIApplication sharedApplication].keyWindow.subviews) {
- if ([view isKindOfClass:[XLRobotImageView class]]) {
- if (self.viewControllers.count > 0) {
- self.robotView = (XLRobotImageView *)view;
- [view removeFromSuperview];
- }
- }
- }
-
-
- // 当push这个子控制器时, 隐藏底部的工具条
- viewController.hidesBottomBarWhenPushed = YES;
-
- UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
- backButton.frame = CGRectMake(0, 0, 44, 44);
- [backButton setImage:[UIImage imageNamed:@"backArror"] forState:UIControlStateNormal];
- [backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
-
- backButton.adjustsImageWhenHighlighted = NO;
- backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
-
- backButton.titleLabel.font = [UIFont systemFontOfSize:16];
-
- [backButton addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
- [backButton setImageEdgeInsets:UIEdgeInsetsMake(0, 5 * BoundWidth/375, 0, 0)];
- viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
- }
-
- // 将viewController压入栈中
- [super pushViewController:viewController animated:animated];
-}
-
-
--(UIViewController *)popViewControllerAnimated:(BOOL)animated{
- //在5个tab的首页需要显示
- NSArray *vcs = self.viewControllers;
- UIViewController *topVC = vcs[vcs.count - 2];
- if (self.viewControllers.count >= 2) {
- if ([topVC isKindOfClass:[MZPregnancyHomeController class]] ||
- [topVC isKindOfClass:[HLSettingViewController class]] ||
- [topVC isKindOfClass:[BBXEditViewController class]] ||
- [topVC isKindOfClass:[HLFriendTopicController class]] ||
- [topVC isKindOfClass:[MZBookViewController class]]
- ) {
- [[UIApplication sharedApplication].keyWindow addSubview:self.robotView];
- }
- }
- return [super popViewControllerAnimated:animated];
-}
-
-```
-
-**敲黑板,注意啦**
-
-因为我做的一个全局的机器人只需要浮动在App的5个模块的首页,所以当页面进入第二层的时候就需要隐藏机器人,当App的顶层控制器是最外层的首页的时候再显示机器人,用导航控制器的push和pop监听就可以实现这个需求,但是遇到的一个问题就是当App从首页进入到第二层页面,用于手动右滑且滑到一半停止,这样子页面还是停留在第二层但是此时也会触发pop方法上面的代码就有点问题
-
-因此想办法需要监听导航控制器里面每个控制器的出现事件,找到一个方法 **- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;** 恰好满足需求,以前没用过记录下来
-
-```
--(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
- self.interactivePopGestureRecognizer.enabled = [self.viewControllers count] > 1 ;
-
- if ([viewController isKindOfClass:[MZPregnancyHomeController class]] ||
- [viewController isKindOfClass:[HLSettingViewController class]] ||
- [viewController isKindOfClass:[BBXEditViewController class]] ||
- [viewController isKindOfClass:[HLFriendTopicController class]] ||
- [viewController isKindOfClass:[MZBookViewController class]]
- ) {
- [[UIApplication sharedApplication].keyWindow addSubview:self.robotView];
- }
-}
-
-```
-
-
-
-
-
-```
-#
-
-
-@interface HLNavigationController ()
-
-### 使用
-
-- 创建 cartfile 文件 `touch cartfile`
-- 修改 cartfile 文件
-```
-github "Alamofire/Alamofire" "5.0.0-rc.3"
-github "onevcat/Kingfisher"
-github "SnapKit/SnapKit" ~> 5.0.0
-```
-
-
-
-
-
-### cartfile - dependency origin
-
-Carthage 支持两种类型的源,一个是 github,一个是 git
-- github 表示依赖源,告诉 Carthage 去哪里下载文件。依赖源之后跟上要下载的库,格式为 `Username/ProjectName`
-- git 关键字后面跟的是资料库的地址,可以是远程的 URL 地址,使用 `git://` `http://` `ssh://` 或者是本地资料库地址。
-
-
-
-### cartfile - dependency version
-
-告诉 Carthage 使用哪个版本,这是可选的。不指定的话,默认使用最新版本
-
-- `==1.0` 表示使用 `1.0` 版本
-- `>= 1.0` 表示使用 `1.0` 或更高的版本
-- `~> 1.0` 表示使用版本 `1.0` 以上,但是低于2.0的最新版本,例如:1.2, 1.6
-- branch 名称/ tag 名称/ commit 名称,意思是使用特定的分支/标签/提交。比如可以说分支名 master,也可以是 commit id: 5c8w72
-
-
-
-### 更新
-
-`carthage update` 但默认会编译4个平台(macOS、iOS、watchOS、tvOS) 会比较耗时,所以可以优化下,指定特定平台
-`carthage update --platform iOS`
-
-
-
-
-### 安装后生成的文件
-
-#### Cartfile.resolved
-该文件是生成后的依赖关系以及各个库的版本号,不能修改。
-该文件确保提交的项目可以使用完全相同的配置与方式运行启用。跟踪项目当前所用的依赖版本号,保持多端开发一致,出于这个原因。建议提交这个文件到版 本控制中。
-```
-github "Alamofire/Alamofire" "5.0.0-rc.3"
-github "onevcat/Kingfisher"
-github "SnapKit/SnapKit" ~> 5.0.0
-```
-
-#### Carthage 目录
-
-该目录包含2个子目录:
-- Checkouts 保存从 git 拉取的依赖库源文件
-- Build 包含编译后的文件,包含了4个平台(Mac、iOS、tvOS、watchOS)对应的 `.framework`
-
-```
-- Carthage
- - Build
- - Checkouts
-```
-
-
-
-### 项目配置
-
-`Target -> Build Setting -> Search Paths -> Framework Search Paths` 添加 `$(PROJECT_DIR)/Carthage/Build/iOS`
-
-此时可以正常编写代码,和使用库的 api,但项目运行会 crash,报错为:`dyld: Library not loaded: @rpath/SnapKit.framework/SnapKit Referenced from: ...`
-
-
-
-由于是非侵入式,所以需要程序员自己配置依赖,`Target -> Build Phases -> '+' -> New Run Script Phase`
-
-- 添加脚本 `/usr/local/bin/Carthage copy-frameworks`。
-- 添加 "Input Files" `$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework` 等等
-
-## Swift Package Manager
-
-Swift Package Manager 是苹果推出的用于管理分发 swift 代码的工具,可以用于创建使用 swift 的库和可执行程序
-
-能够通过命令快速创建 library 或者可执行的 swift 程序,能够跨平台使用,是开发出来的项目能够运行在不同的平台上。
-
-
-### Xcode 集成
-
-Xcode 菜单栏:File -> Swift Packages -> Add Package Dependency...
-输入项目地址后,在弹出框 Rules 里选择版本策略。
-
-
diff --git a/Chapter1 - iOS/1.132.md b/Chapter1 - iOS/1.132.md
deleted file mode 100644
index 78f8dbf..0000000
--- a/Chapter1 - iOS/1.132.md
+++ /dev/null
@@ -1,518 +0,0 @@
-# 动态调试
-
-
-
-## Xcode 调试的原理
-
-Xcode 是电脑端的程序,Xcode 使用 LLDB 进行调试。真机连接 Xcode 运行起来,点击屏幕,对应的事件处理方法里加了断点。手机是如何与 Xcode 断点连接同步的呢?
-
-
-
-
-
-Xcode 编译器:GCC -> LLVM
-
-Xcode 调试器:GDB -> LLDB
-
-- `debugServer` 存放在:`/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/12.2/DeveloperDiskImage.dmg/usr/bin/debugserver`
-- 当 Xcode 识别到手机设备时,Xcode 会自动将 `debugserver` 安装到 iPhone 上。`/Developer/usr/bin/debugserver`
-- 一般情况下,Xcode 只可以调试通过 Xcode 安装的 App
-
-
-
-
-
-## 动态调试任意 App
-
-### 核心原因
-
-
-
-
-
-上面说了 `debugserver` 只能调试 Xcode 连接安装的程序。这句话不够严谨,Xcode 连接 iPhone 的时候,会自动将 `debugserver` 安装到 iPhone 上,但是权限会做收敛。具体表现就是权限 plist。
-
-所以我们可以自行修改权限,重新签名即可:
-
-- 将 `debugserver` 拷贝到电脑上
-
-- 利用 ` ldid -e debugserver > debugserver.entitlements` 命令导出权限文件
-
-- 打开 `debugserver.entitlements` 添加 `get-task-allow`、`task_for_pid-allow` 2个权限
-
- ```shell
-
-
-
-
-
-
-### expression
-
-`expression
-
-
-
-
-
-### 方法返回
-
-`thread return [
-
-### frame variable
-
-`frame variable [
-
-
-
-### 调试指令
-
-`thread continue`、`continue`、`c`:程序继续运行
-
-`thred step-over`、`next`、`n` :单步运行,把字函数当作整体一步执行
-
-`thread step-in`、`step`、`s`:单步运行,遇到子函数会进入子函数
-
-`thread step-out`、`finish`:直接执行完当前函数的所有代码,返回到上一个函数
-
-`si` 、`ni` 和 `s` `n`:类似
-
-- `s` `n` 是源码级别
-
-- `si`、`ni` 是汇编指令级别
-
-
-
-### breakpoint
-
-设置断点的指令
-
-#### 函数名
-
-`breakpoint set -n "函数名"`,但可能存在多个断点,因为同样方法名称的方法,都会被设置断点
-
-比如 Person 类和 ViewController 类,都有 `- (NSInteger)add:(NSInteger)a withB:(NSInteger)b` 方法。` breakpoint set -n "add:withB:"` 指令设置了2个断点。
-
-
-
-
-
-当有多个方法同名的时候,只对当前类设置断点,指令格式为 `breakpoint set -n "[类名 方法名]"`。
-
-比如通过 `breakpoint set -n "[ViewController add:withB:]"` 对 ViewController 类的 `- (NSInteger)add:(NSInteger)a withB:(NSInteger)b` 方法设置断点
-
-
-
-如果一个方法没有参数,也可以通过 `breakpoint set -n sayHi` 的方式设置断点
-
-
-
-#### 函数地址
-
-在逆向,调试别人的 App 的时候我们无法知道函数名称,所以给函数地址打断点就很重要了。
-
-`breakpoint set -a 函数地址`。注意:函数地址是需要处理的,因为 iOS 有 `ASLR 技术`。
-
-
-
-#### 正则表达式
-
-`breakpoint set -r 正则表达式`,效果就是给所有函数名符合正则表达式的函数,设置断点。
-
-
-
-#### 动态库
-
-`breakpoint set -s 动态库 -n 函数名`
-
-
-
-#### 列出所有断点
-
-`breakpoint list` 用于列出所有的断点,每个断点都有自己的编号
-
-
-
-
-
-#### 断点的删除、禁用、开启
-
-`breakpoint disable 断点编号` ,比如 `breakpoint disable 2.1 2.2` 禁用了2个断点
-
-
-
-`breakpoint delete 断点编号`,比如 `breakpoint delete 2`。
-
-
-
-比较奇怪,断点开启、禁用是可以跟子序号的,比如2.1 2.2,而断点删除必须是一级序号
-
-
-
- #### 断点指令信息
-
-`breakpoint command add 断点编号`,该指令会给断点预先设置需要执行的命令,到触发断点时,就会按照指令添加的顺序执行。
-
-指令可以添加多个,最后以 "DONE" 结束。
-
-
-
-
-
-`breakpoint command list 断点编号` 用于查看该断点下的所有指令
-
-`breakpoint command delete 断点编号` 用于删除该断点下的所有指令
-
-
-
-
-
-
-
-
-
-### 内存断点
-
-在内存数据发生改变时触发
-
-`watchpoint set variable 变量`
-
-在 `viewDidLoad` 中 通过 `watchpoint set variable self->_age` 给 age property 设置了断点。当改变的时候就触发断点
-
-
-
-
-
-`watchpoint set expression 变量地址`
-
-在 `viewDidLoad` 中 通过 `watchpoint set expression 0x00007fcd20306a60` 给 age property 设置了断点。当改变的时候就触发断点
-
-
-
-
-
-`watchpoint list`
-
-`watchpoint disable 断点编号`
-
-`watchpoint enable 断点编号`
-
-`watchpoint disable 断点编号`
-
-`watchpoint delete 断点编号`
-
-`watchpoint command add 断点编号`
-
-`watchpoint command list 断点编号`
-
-`watchpoint command delete 断点编号`
-
-
-
-### image
-
-#### image list
-
-`image list` 列举所加载的模块信息
-
-
-
-
-
-`image list -o -f` 打印出模块的偏移地址、全路径
-
-
-
-#### image lookup
-
-`image lookup -t 类型`:查找某个类型的信息
-
-
-
-
-
-`image lookup -a 地址`:根据内存地址查找在模块中的位置
-
-举例:声明一个数组,只有5个元素,但通过下标6来访问数组的时候 crash 了,假设我们代码很长,crash 后想知道具体是哪一行代码造成了 crash,怎么办呢?
-
-我们项目叫做 Demo111,那么 crash 堆栈中第4行有个地址 `0x000000010d534dfc`,可以通过该地址来分析具体的 crash 位置。通过
-
-`image lookup -a 0x000000010d534dfc` 可以知道 `Summary: Demo111 -[ViewController touchesBegan:withEvent:] + 108 at ViewController.m:29:18` 是在 ViewController 的29行处发生了 crash。
-
-
-
-`image lookup -n 符号或者函数名`:查找某个符号或者函数的位置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Chapter1 - iOS/1.133.md b/Chapter1 - iOS/1.133.md
deleted file mode 100644
index 36b37c2..0000000
--- a/Chapter1 - iOS/1.133.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# 编译器利用 PGO 优化 App 性能
-
-## 什么是 PGO?
-
-> Profile Guided Optimization (PGO) is an advanced feature for wringing every last bit of performance out of your app. It is not hard to use but requires some extra build steps and some care to collect good profile information. Depending on the nature of your app’s code, PGO may improve performance by 5 to 10 percent, but not all applications will benefit from it. If you have performance-sensitive code that needs that extra optimization, PGO may be able to help.
-
-PGO 即 Profile Guided Optimization,
-
-[Using Profile Guided Optimization](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/xcode_profile_guided_optimization/pgo-using/pgo-using.html) 官方文档大概意思是:PGO 是一种根据运行时 profiling data 来进行优化的技术。用起来不难,但需要一些额外的构建步骤和一些收集良好的 profile 文件。如果一个 application 的使用方式没有什么特点,那么我们可以认为代码的调用没有什么倾向性。但实际上,我们操作一个 application 的时候,往往有一套固定流程,尤其在程序启动的时候,这个特点更加明显。采集这种“典型操作流”的 profiling data,然后让编译器根据这些 data 重新编译代码,就可以把运行时得到的知识,运用到编译期,从而获得一定的性能提升。然而,值得指出的一点是,这样获得的性能提升并不是十分明显,通常只有 5-10%。如果已经没有其他办法,再考虑试试 PGO。
-
-
-## 何时使用 PGO?
-正常情况下不用,通常来说 LLVM 经过编译前端,通过词法分析、语法分析、语义分析构建出 AST,然后转换为 IR,IR 经过一些列优化 Pass,常见的 Pass 比如死代码消除等,LLVM 已经做了大量的优化工作。所以通常来说,我们需要正向的优化代码、优化算法、设计正确合理的架构、合理的 UI 层级。除此之外,你还想让 App 获得更好的性能,可以考虑采用 PGO 技术。
-
-## PGO 怎么样工作的?
-PGO 假设你的应用程序的行为是可预测的,这样一个有代表性的配置文件就可以捕捉代码所有性能敏感方面的未来行为。当启用 PGO 时,Xcode 会构建一个专门检测的应用程序版本,然后运行它。您可以手动运行该应用程序,也可以使用 UI 自动化 XCTest 测试 App。当应用程序运行时,会统计并记录每条语句的执行次数。
-
-应该收集一份具有典型、能够代表 App 真正用户行为的 profile 数据,PGO 统计每个语句的执行次数,并创建一个为该行为建模的概要文件。依据该文件,LLVM 编译器将优化工作集中在最重要的代码上。
-
-举个例子:有一个稍微长一点的函数,刚好长到编译器不对它的调用进行 inline 优化,但是实际上,这个函数是一个热点调用,在运行时被调用的次数非常多。那么如果此时编译器也能帮我们把它优化掉,是不是很好呢?但是,编译器怎么能知道这个“稍微长一点的函数”是一个热点调用呢?PGO 根据这个 profile 文件进行优化
-
-是一种优化编译器的技术,通过收集程序的实际运行数据,例如程序执行的分支情况,来指导编译器生成更优化的代码。
-
-
-## tips
-首先,Xcode 已经提供了 PGO 的 UI 操作([详情可参考](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/xcode_profile_guided_optimization/Introduction/Introduction.html#//apple_ref/doc/uid/TP40014459-CH1-SW1)),所以如果是简单的 application,可以直接使用 UI 操作的方式,简单省事。不过,UI 操作有一些缺陷,具体表现在:
-
-- 控制粒度粗糙,要么不打开 PGO,要么对所有 code 进行 PGO。如果项目中有 swift 代码,那么这种方式就不能用了,因为 swift 不支持 PGO;
-- 只支持两种方式采集 profiling data。第一种是每次手动运行,运行结束后退出 application,Xcode 会产生一个 xxx.profdata,之后的编译,都会用这个文件,来进行优化;如果代码发生变更,Xcode 会提示 profdata file out of date。第二种方法是借助 XCTest 来采集 profiling data,这种方法提供了一定的 automation 能力,但是另一方面也限制了 automation team 的手脚,他们可能在使用另一些更好用的工具而不是 XCTest。
-
-因为 PGO 优化是靠的 Profile 文件,所以每次代码变化后需要保证生成最新的 Profile 文件。
-随着继续开发和更改应用项目中的代码,优化配置文件会过时。LLVM 编译器会识别出配置文件何时不再与应用程序的当前状态良好匹配,并提供警告。当收到此警告时,可以再次使用 Generate Optimization Profile 命令来创建更新的配置文件。每次重新生成优化配置文件时,Xcode 都会替换现有的配置文件数据。
-
-这个很难办,所以需要利用 CI 手段,可以借助 `-fprofile-instr-generate` 和 `-fprofile-instr-use` 这两个 Clang 提供的编译选项搭配 CI。
-
-## LLVM 利用 PGO 大概怎么优化代码
-
-LLVM利用PGO(Profile-Guided Optimization)可以实现多种优化,其中一些主要优化包括:
-- 函数内联(Function Inlining):PGO可以根据实际运行时的函数调用情况,选择性地内联函数,减少函数调用的开销,提高程序执行效率。
-- 循环展开(Loop Unrolling):通过分析循环执行次数和循环体内的代码,PGO可以决定是否展开循环,减少循环控制开销,提高循环执行效率。
-- 代码重排(Code Reordering):根据实际运行时的代码执行路径,PGO可以优化代码的布局顺序,使得频繁执行的代码更容易被CPU缓存命中,提高程序的局部性和性能。
-- 分支优化(Branch Optimization):PGO可以根据实际运行时的分支预测情况,优化分支指令,减少分支预测错误,提高程序的执行效率。
-- 常量传播(Constant Propagation):根据实际运行时的数据流分析,PGO可以更好地进行常量传播,减少不必要的变量存储和加载操作,提高程序的执行效率。
-- 内存访问优化(Memory Access Optimization):PGO可以根据实际运行时的内存访问模式,优化内存访问方式,减少内存访问延迟,提高程序的内存访问效率。
-通过这些优化手段,PGO可以根据实际运行时的数据和行为模式,生成更加针对性和高效的优化代码,从而提高程序的性能和执行效率
-
-
-## PGO 和二进制重排的异同
-PGO 是一个编译器特性,能够过程序实际执行的方法进行打点统计,找出最常执行的代码路径(热点函数),并根据这些信息对程序进行优化,这种优化包括重排代码已减少分支预测错误、优化内存使用以提高缓存命中率、函数内联、分支优化等等,这是一种动态优化技术,会根据实际程序运行收集到的 profile 信息做改变。
-
-二进制重排则是程序编译完成后,对二进制代码进行优化的技术,主要要解决的是内存缺页异常的问题,可以减少缓存的错失率。这是一种静态优化技术,因为它不需要实际运行程序就能进行。
-
-
-## 为什么我的App需要重排的符号个数这么少
-二进制重排主要通过调整二进制文件中的代码顺序,以改善性能。以 iOS 举例,App 启动慢的一个原因就是 App 启动过程中用到的函数方法、可能排布在不同的 page 上,所以由于不断的切换 page,导致启动慢。App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
-
-然而,虽然理论上所有的代码都可以进行重排,但实际上,根据应用程序的特性,可能只有一部分代码是频繁执行的,也就是所谓的"热点"代码。这部分代码会被优先考虑进行重排,因为这样可以最大化性能提升。
-
-另外,重排过程需要考虑到许多限制和约束,如符号之间的依赖关系,这可能会限制哪些符号可以移动和重新排序。如果一些符号因为它们之间的关系而不能被移动,那么这些符号就不会被考虑在重排中。
-
-因此,你看到的"需要重排的符号个数"相对较少,可能是因为只有这些符号是被识别出来的"热点"代码,或者它们是唯一可以在不违反任何约束和限制的情况下被移动的符号
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.134.md b/Chapter1 - iOS/1.134.md
deleted file mode 100644
index c67c067..0000000
--- a/Chapter1 - iOS/1.134.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# 去除无用代码
-
-通用方案
-Mach—O
-
-__DATA , __objc_selrefs 标记方法被调用信息
-
-otools -v -s _DATA _objc_selrefs Mach-O
-
-
-linkmap - selfrefs = 无用方法
-
-问题:不准(OC 语言,动态性)
-
-
-## clang plugin
-
-重载
-RecursiveASTVistor::visitDecl
-RecursiveASTVistor::visitStmt
-
-上线前,通过静态方式去查找。不安全、不全面
-
-
-## 运行时查找
-Code Coverage
-clang -fprofile-instr-generate -fcoverafe-mapping a.m -o a
-swiftc -profile-generate -peofile-coverage-mapping a.swift
-
-
-缺点:难以定制
-
-## Fuzzing 方案
-
-## Sanitizee Coverage
-缺点: 编译慢、且无法进一步定制,包体积负向影响
-
-## 自定义 llvm Pass
-
-针对 LLVM IR 进行处理。
-
-低级别编程语言,类似 RISC 指令集。和高级语言对应,LLVM 利用一些列 Pass 对 IR 进行优化。
-
-LLVM 的优化是由 Pass 完成的,每个 Pass 完成特定的优化
-自己开发 Pass 是独立的,不会影响 LLVM 的结构
-Pass 之间可以有关联,也可分租
-
-LLVM 有c/c++ 接口,还可以用 c/Swift 编写 Pass
-
-c 接口较稳定,C++ 接口更新较新
-
-
-LLVM 内置 Pass
-memcpyopt: memset 指令替换 memcpy
-always_inline: 总是内联用 alwaysinline 修饰的函数
-dce:死代码消除
-loop_deletion:删除未使用的循环
-
-
-Pass 生成:
-静态:在 LLVM 工程中设置 CMake,重新构建 opt
-动态:opt 用 `-load-pass-plugin` 选项加载
-
-怎么写 Pass?
-对 IR 分析,继承 AnalysisInfoMixin
-对 IR 转换,继承 PassInfoMixin
-
-
-
diff --git a/Chapter1 - iOS/1.135.md b/Chapter1 - iOS/1.135.md
deleted file mode 100644
index e92530c..0000000
--- a/Chapter1 - iOS/1.135.md
+++ /dev/null
@@ -1,234 +0,0 @@
-# 框架设计
-
-
-
-## 图片框架
-
-### 角色
-
-- Manager
- - 内存
- - 磁盘
- - 网络
-- Code Manager
- - 图片解码
- - 图片压缩/解压缩
-
-
-
-### 图片读写过程
-
-- 以图片 url 的 hash 值为 key,存储
-
-
-
-### 读取过程
-
-
-
-
-
-### 内存设计
-
-- 内存存储的空间
-
- - 10kb 以下,使用场景多,设计50张容量
- - 100kb 以下,使用场景次之,设计20张容量
- - 100kb 以上,使用场景最小,设计10张容量
-
-- 淘汰策略
-
- 队列实现,先进先出。
-
-
-
-### 淘汰策略
-
-LRU,最近最久未使用算法。比如3天内没有使用过的,则认为需要被淘汰。
-
-
-
-### 淘汰时机
-
-- 定时检查,比如 30分钟检查一次
-
-- 提高检查频率:
-
- - 每次进行图片读写时
- - 前后台切换时
-
-
-
-### 磁盘设计
-
-- 存储方式
-- 大小限制(如200MB)
-- 淘汰策略:如果某一张图片存储时间距今已经超过7天
-
-
-
-### 网络设计
-
-- 图片请求的最大并发量
-- 请求超时策略。超时重试1次,再次超时则取消
-- 请求优先级
-
-
-
-### 图片解码
-
-对于不同格式的图片,图片解码怎么处理?
-
-应用**策略模式**,对不同图片格式进行解码。一方面可以解码不同格式、另一个方面替换解码算法,对于稳定性有帮助
-
-在哪个阶段进行解码?
-
-磁盘读取后、网络请求返回后
-
-
-
-### 线程处理
-
-
-
-## 阅读时长记录器
-
-
-
-### 记录器种类
-
-- 页面式:普通的 push、pop 页面
-- feed 流式:类似 weibo 这种 feed 流式的记录
-- 自定义式:可拓展性的体现,面向未来
-
-
-
-QA:为什么要有不同类型的记录器?
-
-- 基于不同分类场景提供的关于记录的封装、适配
--
-
-### 记录数据存储
-
-- 内存缓存
-- 磁盘存储
-
-
-
-### 准确性
-
-数据收集(存储)、上报(移除)2个核心流程。准确性也和这2个方面息息相关。
-
-- 定时写磁盘 从内存中 flush 到本地磁盘。定时器1分钟 flush 一次
-- 限定内存缓存条数。超过该条数,即写磁盘。内存记录每满10条 flush 一次
-
-
-
-### 上传策略
-
-思考:
-
-- 需要立马上传吗?每收集到1次页面阅读时长就需要立马上传1次吗?ROI 衡量。性能、线程数
-- 关于延时上传的场景有哪些?
-
-
-
-上传时机:
-
-- 定时器,比如每5分钟上传1次。
-- 前后台切换,比如从后台切换到前台触发1次上传逻辑
-- 无网切换到有网
-
-
-
-### 网络上传效率
-
-自定义报文,高效传输。
-
-iOS 小端序,网络大端序。
-
-
-
-
-
-## 复杂页面架构设计
-
-- MVVM
-
-- Redux 数据流
-
-
-
-## 客户端架构
-
-
-
-## 业务之间解耦后的通信方式
-
-- openURL
-- 依赖注入:中间层
-
-
-
-## AFNetworking
-
-### 主要类关系图
-
-
-
-
-
- ### AFURLSessionManager
-
-- 创建和管理 NSURLSession、NSURLSessionTask
-- 实现 NSURLSessionDelegate 协议代理方法,处理网络请求的重定向、认证、网络数据的处理
-- 引入 AFSecurityPolicy,用来保证请求安全
-- 引入 AFNetworkReachabilityManager 监控网络状态
-
-
-
-## SDWebImage
-
-### 架构图
-
-
-
-
-
-## 图片加载流程
-
-- 查找内存缓存
-- 查找磁盘缓存
-- 网络下载图片并磁盘缓存
-
-
-
-## AsyncDisplayKit
-
-### 主要处理问题
-
-主要通过减轻主线程压力,尽量将一些可以放到子线程的任务都放到子线程处理,减轻主线程压力
-
-主要分3方面:
-
-- UI 布局 layout:文本宽高计算、视图布局计算
-- 渲染 Rendering:文本渲染、图片解码、图形绘制
-- UIKit Objects:对象创建、调整、销毁
-
-
-
-### 基本原理
-
-
-
-- UIView 作为 CALayer 的代理实现。CALayer 作为 UIView 的实例变量,负责展示规工作。
-
-- 针对 UIView 的修改,都抽象为针对 ASNode 的修改,这些修改可以在子线程进行。针对 ASNode 的修改和提交,会对其进行封装,提交到一个全局容器中。
-- 对 Runloop 状态进行监听,进入休眠前,ASDK 执行该 loop 内提交的所有任务。
-
-
-
-
-
-
-
diff --git a/Chapter1 - iOS/1.136.md b/Chapter1 - iOS/1.136.md
deleted file mode 100644
index 9ccb5c9..0000000
--- a/Chapter1 - iOS/1.136.md
+++ /dev/null
@@ -1,380 +0,0 @@
-# 工程化
-
-## 多环境配置
-Project、Target、Scheme 主要管理什么?
-- Project:包含了项目所有的代码、资源文件,所有信息
-- Scheme:对于指定 Target 的环境配置
-- Target:对于指定代码和资源文件的具体构建方式
-
-多环境配置的3种方式:
-- 多 target 配置
-- Scheme 多 target 进行环境配置
-- xconfig 文件配置
-
-
-
-## 多环境配置的不同方式
-
-### 多 Target 的方式
-
-#### 方案
-
-针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。 **多 Target(Targets)** 是管理不同应用变体(如免费版/付费版、测试版/生产版、多客户定制版)的高效方式。
-
-所以,**为了区分不同的环境,做一些逻辑的控制。所以需要搭配不同的宏定义,来实现控制逻辑的效果。**
-
-注意:duplicate 之后,target 虽然多了一份,但是代码和资源不变
-
-
-
-#### 关键步骤
-
-
-
-
-
-
-
-##### 管理配置文件
-
-1. **独立的 Info.plist**
-
- - 复制原 `Info.plist` 并重命名(如 `Pro-Info.plist`)
-
- 当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
-
-
-
- - 在新 Target 的 `Build Settings` → `Packaging` → `Info.plist File` 指定新路径
-
-2. **环境配置分离**
-
- - 创建 `Config-Pro.xcconfig` 文件定义专属配置:
-
- ```shell
- // Config-Pro.xcconfig
- API_URL = https://api.pro.com
- APP_NAME = Pro App
- ```
-
- - 在 Target 的 `Build Settings` → `Base Configuration` 指定配置文件
-
-
-
-##### 宏定义
-
-- OC:Build Settings -> Preprocessor Macros 里面的 Debug/Release 模式下添加自定义宏。比如在 debug 模式下 `IsOCDebug = 1`
-- Swift:Build Settings -> Other Swift Flags 里的 Debug/Release 模式下添加自宏定义。注意命名有格式要求:`-D + 宏名称`
-
-
-
-#### 思考
-
-该方式还是存在弊端:
-
-- 工程存在多份 info.plist(实际上 plist 文件很少改动,所以没有这种需求)
-- 配置比较零散、比较乱
-
-
-
-### 多 Scheme 的方式
-
-#### 方案
-
-针对多 Target 方案存在的问题,可以用**「多 Scheme + 多 Configuration 」**的方式解决。
-
-
-
-#### 关键步骤
-
-##### 创建 Configuration
-
-针对一个 Target 可以添加多个 **Configuration**,步骤如下:
-
-先选中 Project,然后在右侧选择 Info 选项卡,在 Configurations Section ,点击 "+" ,即可创建新的 Configuration。
-
-
-
-
-
-创建好之后,该 Target 存在3份 Configuration 了。不同的 Configuration 有什么作用呢?设置宏定义的时候可以针对不同的 Configuration 进行设置。
-
-
-
-
-
-针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
-
-
-
-点击 Edit Scheme,在 Run 里面选择对应的 Configuration。
-
-但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Configuration。有没有什么办法解决切换问题呢。
-
-
-
-##### 创建实体 Scheme
-
-创建 Scheme 步骤:Xcode -> New Scheme,再弹出的方框内,选择对应的 Target,然后输入需要创建的 Scheme 名称。此次我们创建了:Debug、Beta 2个新的 Scheme。
-
-
-
-
-
-创建好之后,可以看到实体 Configuration 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme,然后在 Run 里面 “Build Configuration” 里面选择对应的 Configuration 与之对应。
-
-
-
-
-
-##### plist 暴露自定义字段
-
-1. 创建之后就可以根据 Configuration 设置值,在 `Build Settings -> User-Defined` 下添加自定义的字段,同时可以根据 Configuration 设置不同的值。
-2. 设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用。
-
-完整如下图:
-
-
-
-切换不同的 Scheme,可以运行不同的效果,当前 case 下,选择 Debug Scheme,输出不同结果 `HOST_URL: http://www.debug.baidu.com`
-
-
-
-#### 思考
-
-目前的方案已经优雅不少,该方式还是存在弊端:自定义宏的时候需要选择不同的 Scheme,过程繁琐
-
-
-
-### Xcconfig
-
-#### 方案
-
-使用过 CocoaPods 的都会留意到工程存在 `*.Pro.xcconfig` 文件。里面是一些工程相关的配置。所以我们也可以用该方式处理工程问题。
-
-
-
-#### 关键步骤
-
-Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,还可以修改 Build Settings 里面的选项。
-
-第一:创建步骤如下:
-
-
-
-文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig`
-
-几个 Scheme 就创建几个对应的 Xcconfig 文件。
-
-
-
-第二:修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations,选择对应的 Target,然后选择右边对应的 Xcconfig 文件。如下图
-
-
-
-
-
-我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`,Xcode 切换到 debug scheme 下,然后 Command + B 编译。
-
-
-
-验证结果:
-
-- 编译前切换到 Build Settings 后,查看 ”Other Linker Flags“ 的 Debug 项为空
-- 编译后切换到 Build Settings 后,查看 ”Other Linker Flags“ 的 Debug 项为 `-framework ”AFNetworking“`
-
-因为 Xcconfig 文件,具有操作和修改 Build Settings 的能力,所以用好 Xcconfig 文件,不只可以实现替代宏定义和切换繁琐的问题,还可以实现很多其他手动修改 Build Settings 的问题。
-
-
-
-说明:在 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}`
-
-
-
- - 代码中使用
-
- ```swift
- let host = Bundle.main.object(forInfoDictionaryKey: "ServerHost") as! String
- let apiKey = Bundle.main.object(forInfoDictionaryKey: "ApiSecretKey") as! String
- print("Host: \(host), API Key: \(apiKey)")
- ```
-
-
-
-QA:思考一个问题:为什么在 `xcconfig` 文件中设置的值,最后会显示在 Xcode 的 Build Settings 的 GUI 面板上?
-
-这便是接下去的内容:「Xcode 配置的层级机制」
-
-
-
-## Xcode 配置的层级机制
-
-### 层级机制
-
-Xcode 的 Build Settings 是一个**多层叠加系统**,优先级从高到低如下:
-
-**Target 设置 > Project 设置 > .xcconfig 文件 > Xcode 默认值**
-
-Apple 在 [Build Settings Reference](https://help.apple.com/xcode/mac/current/#/itcaec37c2a6) 中明确说明:
-
-> **继承规则**:
->
-> - Target 设置继承 Project 设置,Project 设置继承底层默认值。
-> - 若 Target 显式定义某配置项,则覆盖 Project 中的相同项16。
-
-**关键推论**:
-
-> Target 作为具体构建目标,其配置需独立于 Project 的通用设置。若二者冲突,**Target 优先级更高**。
-
-根据 [Xcode Build System Guide](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/XcodeBuildSystem/):
-
-> - `.xcconfig` 是**基础配置层**,通过 `baseConfigurationReference` 字段被 Project/Target 引用23。
-> - 若 Project 或 Target 显式设置了某值(即使为空),**将覆盖 .xcconfig 中的定义**24。
-
-
-
-典型案例:当 `project.pbxproj` 中定义 `OTHER_LDFLAGS = ""`(空字符串)时,它会覆盖 `.xcconfig` 中的非空值,导致链接标志失效
-
-说明: xcconfig 优先级低于前2者。
-
-结论:配置的优先级顺序为:**Target 设置 > Project 设置 > .xcconfig 文件 > Xcode 默认值**
-
-
-
-### 为什么 xcconfig 的结果会显示在 Build Settings 中
-
-1. **配置文件的显式声明**
- `.xcconfig` 文件是 Build Settings 的合法数据源。通过以下方式关联:
-
- ```
- OTHER_LDFLAGS = -framework "AFNetworking"
- ```
-
- Xcode 会将其视为项目配置的一部分,并在 GUI 中显示。
-
-2. **Build Settings 的“继承”特性**
- Xcode 的 Build Settings 界面本质是一个**实时计算的合并视图**,它会展示:
-
- - 所有直接通过 GUI 设置的值
- - 从 `.xcconfig` 导入的值
- - 继承的默认值(如 `$(inherited)`)
-
-3. 如果使用 CocoaPods 安装的依赖,则会生成2个 CocoaPods 生成的 `.xcconfig` 文件。如果开发者自己再创建 `.xcconfig` 则需要处理2者的逻辑,因为 Xcode 一个工程只可以选择一个 `.xcconfig` 文件
-
-
-
-### Xcode Project Management Guide
-
-这份[文档](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/XcodeBuildSystem/000-Introduction/Introduction.html#//apple_ref/doc/uid/TP40006904-CH1-SW1)深入讲解了 Xcode 项目结构、Build Settings 的继承关系、环境变量(如 `$(SRCROOT)`)等,适合想系统理解设置机制的人
-
-聊起工程化,不得不查看 CocoaPods 的 [Podfile 配置指南](https://guides.cocoapods.org/syntax/podfile.html)
-
-
-
-### 实践验证
-
-#### Demo1
-
-第一步:新建 Xcode iOS 工程。
-
-第二步:新建的工程配置了一份基础的 `Base.xcconfig` 来配置基础的编译信息。`Dev.xcconfig` 包含 `Base.xcconfig` 信息,在此基础上增加了一些编译参数。
-
-`Base.xcconfig` 配置如下:
-
-```shell
-BASE_LD_CONFIG = -framework "WantUIKit"
-OTHER_LDFLAGS = $(BASE_LD_CONFIG)
-```
-
-`Dev.xcconfig` 配置如下:
-
-```shell
-#include "Base.xcconfig"
-TEMP_LDFLAGS = $(BASE_LDFLAGS) -framework "AFNetworking" -framework "SDWebImage" -framework "PrismClient"
-OTHER_LDFLAGS = $(TEMP_LDFLAGS)
-```
-
-
-
-第三步:
-
-- 当前 xcconfig 是为 Dev 模式下设置的。所以项目的 scheme 选择 `Debug` 模式。
-- 选中 `PROJECT`,然后在 `Configurations` 下给 `Debug` 配置 `Dev.xcconfig` 文件。
-
-
-
-结果:编译工程,可以看到报错了。符合预期
-
-
-
-原因:本 Demo 的目的就是通过 `xcconfig` 文件和继承关系来验证对 Xcode Build Settings 中的 `Other Linker Flags` GUI 面板来验证 xcconfig 及其层级关系会正确影响到最终的编译参数上。
-
-注意:为什么不用 `$(inherited)`?
-
-`$(inherited)` 的作用范围:
-
-- 仅继承来自 **Xcode 构建系统层级**的值(Target 设置 → Project 设置)
-- 不继承 **同一配置文件链** 中通过 `#include` 引入的值
-
-所以此时用**中间变量**的方法。
-
-
-
-#### Demo2
-
-验证 `$(inherited)` 的继承效果。
-
-第一步:创建工程,配置 Podfile 文件。Podfile 文件内容如下
-
-```shell
-source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' # 清华源
-
-platform :ios, '16.2'
-inhibit_all_warnings! # 屏蔽第三方库警告
-
-target 'LDExploreDemo' do
- # Pods for InstallDyanmicAndStaticFramework
- pod 'SDWebImage'
- pod 'AFNetworking'
-end
-```
-
-第二步:`pod install` 后,可以看到自动生成的 xcconfig 文件内容如下。
-
-为了测试 xcconfig 配置信息的继承,故意把生成的原始信息注释掉。去掉了 `-framework "ImageIO"`
-
-
-
-第三步:创建 `Base.xcconfig` 文件。引入 Cocoapods 自动生成的 `Pods-LDExploreDemo.debug.xcconfig` 然后声明 `OTHER_LDFLAGS = $(inherited) -framework "ImageIO"`由2部分组成,一部分是 `$(inherited)` 一部分是新加的 `-framework "ImageIO"`
-
-第三步:创建 `Dev.xcconfig` 文件。引入第三步创建的 `Base.xcconfig` 文件。声明 `OTHER_LDFLAGS = $(inherited)` 为继承来的配置。
-
-第四步:项目的 `AppDelete.m` 中引入 `#import
-
-说明:
-
-- Cocoapods install 后,自动创建链接器所需参数。都在 `Pods-项目名.debug.xcconfig` 配置文件中
-- 我们可以自己创建的 `*.xcconfig` 是可以引入自动生成的配置文件的。并在此基础上可以修改。然后在 Xcode Project Configuration 里可以指定为新创建的 xcconfig 文件
-- 并且是可以生效的
diff --git a/Chapter1 - iOS/1.137.md b/Chapter1 - iOS/1.137.md
deleted file mode 100644
index fd3626d..0000000
--- a/Chapter1 - iOS/1.137.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# 质量检测
-
-## 静态检测
-### 概念
-
-静态分析器是一个 Xcode 内置的工具,使用它可以在不运行源代码的状态下进行静态的代码分析,并且找出其中的 Bug,为 app 提供质量保证。
-
-静态分析器工作的时候并不会动态地执行你的代码,它只是进行静态分析,因此它甚至会去分析代码中没有被常规的测试用例覆盖到的代码执行路径。
-
-静态分析器只支持 C/C++/Objective-C 语言,因为静态分析器隶属于 Clang 体系中。不过即便如此,它对于 Objective-C 和 Swift 混编的工程也可以很好地支持。
-
-其中包括了安全、逻辑、以及 API 方面的问题。分析器可以帮你找到以上的这些问题,并且解释原因以及指出代码的执行路径。
-
-
-
-
-### 原理
-
-- 通过语法树进行代码静态分析,找出非语法性错误
-- 模拟代码执行路径,分析出 control-flow graph(CFG)
-- 预置了常用的 checker
-
-一个大型项目代码行数非常多,所有跑完全部的 CFG 必定很耗时。
-
-
-
-### 如何使用
-
-Xcode 静态分析功能是在程序未运行的情况下,对代码的上下文语义、语法、和内存情况进行分析,可以检测出代码潜在的文本本地化问题(Localizability Issue)、逻辑问题(Logic error)、内存问题(Memery error)、数据问题(Dead store)和语法问题(Core Foundation/Objective-C)等。
-功能入口在: `菜单栏 -> Product -> Analyze`。可以使用快捷键:Command+Shift+B
-
-#### 文本国际化
-
-Xcode Target -> Build Settings -> Static Analysis - Issues - Apple APIs -> Miss Localizablity 设置为 YES。可以帮助检测发现,缺少国际化的文本。
-
-
-提示说明 `User-facing text should use localized string macro` 缺少本地化的 API,正确的采用下面一行的写法。
-
-#### 逻辑问题
-
-分母不能为0.
-
-
-
-#### 内存问题
-
-虽然 Xcode 默认使用 ARC 管理内存,但是某些 C API 还需要开发者自己进行内存管理。比如 CF 框架下的 API。
-以及 block nil 判断等。
-
-
-
-
-
-#### 数据问题
-
-在编码过程中,一些数据问题可以通过Analyze很好的提示出来。比如下图:
-
-
-
-
-
-#### Xcode 13 新增的检查项
-
-在 Xcode 13 中,静态分析器也得以升级,现在它可以捕获更多的一些逻辑问题:
-
-1. 断言 Assert 的副作用
-2. 死循环
-3. 无用的冗余代码(例如多余的分支条件)
-4. C++ 中 move 和 forward 的滥用导致的潜在问题
-
-一部分的改进来自于开源作者们对 Apple Clang 编译器的贡献。
-
-
-
-##### NSAssert 中的副作用
-
-使用 NSAssert 规避非预期的代码逻辑是很常见的好习惯,但是不规范地使用也会带来一些副作用,例如在 NSAssert 的判断条件中对变量或内存进行修改操作。
-
-本来 `self.count` 默认为0,经过 `mockAssertIssue` 方法中,赋值为1,然后写了 NSAssert 是为了增加健壮性,但这个断言有副作用,虽然判断了赋值后是1,再自增判断等于2,但这不符合预期。经过断言已经修改为2了。
-
-
-
-
-
-##### 死循环
-
-下面是一个很常见的死循环的案例,这种稍微复杂一些的逻辑,乍眼一看,似乎没有什么问题:
-
-
-
-这段代码中,是一个二层循环,但是在内层的循环中,没有对 j 做递增,而是做了 result 的递增,这个问题虽然会隐晦,但是新版本的静态检查器会检测出来。
-
-
-
-Analyze 功能强大,其实际能检测出的问题会更多。
-
-
-
-### 自定义分析器参数
-
-Xcode 也为静态分析器提供了很多的自定义项,方便开发者根据自身工作流进行定制。在 BuildSetting 中通过搜索 `Static Analysis` 关键字,可以筛选出跟分析器相关的设置项。
-
-
-
-### 每一次编译都执行静态分析
-
-通过打开 `Analyze During 'Build'` 可以使得每一次编译操作都执行分析器:
-
-
-
-### 设置静态分析器的运行模式
-
-`Mode of Analysis for 'Analyze'` 可以配置分析器运行的模式,Xcode 提供了两种运行模式:
-
-- `Shallow(faster)` :`Shallow`规避了去检查一些耗时复杂的检查操作,所以 Shallow 运行的更快
-- `Deep` 则进行深入的检查,能抛出更全面的错误
-
-同一个工程,分别看看 Shallow 和 Deep 的耗时差别:
-
-
-
-
-
-### 专项检查配置
-
-静态分析器也提供了一些专项检查的配置,可以根据工程情况定制选择。假设,项目有严格的安全检查,可以打开下图中选中的这些配置项目:
-
-
-
-
-
-再或者,如果静态分析器抛出的一些问题不想关注,可以在 Xcode Build Settings 中关闭掉。从而更聚焦于感兴趣、更关注的问题。
-
-
-
-### 单个文件的分析
-
-也可以针对单个文件做静态检查。操作路径为:Product -> Perform Action -> Analyze "FileName"。
-
-这样只会对单个文件检测,且不会分析 import 进来的文件(可以看到右边的 Person.m 的问题没有被检测出来)
-
-
-
diff --git a/Chapter1 - iOS/1.138.md b/Chapter1 - iOS/1.138.md
deleted file mode 100644
index 91ecf2d..0000000
--- a/Chapter1 - iOS/1.138.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# AFNetworking 源码解读
-
-
-
-## 结构
-
-核心包含5个功能模块:
-
-- 网络通信模块(AFURLSessionManager、AFHTTPSessionManger)
-- 网络状态监听模块(Reachability)
-- 网络通信安全策略模块(Security)
-- 网络通信信息序列化/反序列化模块(Serialization)
-- 对于iOS UIKit库的扩展(UIKit)
-
-AF 是基于 NSURLSession 来封装的,所以核心类 AFURLSessionManager 也是针对 NSURLSession 做的封装,其余的4个模块,是为了网络通信(请求、响应数据的序列化、HTTPS 安全认证、UIKit 推展)
-
-
-
-其中 AFHTTPSessionManager 继承自 AFURLSessionManager,一般的网络请求都是用这个类,但是该类本身没有处理实际的网络,而是做了一些封装,把请求逻辑分发给父类 AFURLSessionManager 或者其他类去做。
-
-
-
-## 以 get 请求为例
-
-```objective-c
-AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]init];
-
-[manager GET:@"https://somehost.com/goods" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
-
-} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
-
-}];
-```
-
-调用初始化方法生成一个 manager,去看看具体做了什么
-
-```objective-c
-- (instancetype)init {
- return [self initWithBaseURL:nil];
-}
-
-- (instancetype)initWithBaseURL:(NSURL *)url {
- return [self initWithBaseURL:url sessionConfiguration:nil];
-}
-
-- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
- return [self initWithBaseURL:nil sessionConfiguration:configuration];
-}
-
-- (instancetype)initWithBaseURL:(NSURL *)url
- sessionConfiguration:(NSURLSessionConfiguration *)configuration
-{
- self = [super initWithSessionConfiguration:configuration];
- if (!self) {
- return nil;
- }
- // 对传过来的 BaseUrl 进行处理,如果有值且最后不包含/,url加上"/"
- if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
- url = [url URLByAppendingPathComponent:@""];
- }
-
- self.baseURL = url;
-
- self.requestSerializer = [AFHTTPRequestSerializer serializer];
- self.responseSerializer = [AFJSONResponseSerializer serializer];
-
- return self;
-}
-```
-
-初始化都调用到
-
-
-
-## 数字证书
-数字签名,它能确认消息的完整性,进行认证。
-和公钥密码一样也要用到一对公钥、私钥。但相反的是,签名是用私钥加密(生成签名),公钥解密(验证签名)。私钥加密只能有吃有私钥的人完成,而由于公钥是对外公开的,因此任何人都可以用公钥解密(验证签名)。
-
-
-公钥基础设置(PKI)是为了能够更有效地运用公钥而制定的一系列规范和规格的总称,使用最广泛的 X.509 规范也是 PKI 的一种。
-
-### 证书链
-
-CA 有层级关系,处于最顶层的认证机构一般是根 CA,下面证书是经过上层签名的,而根 CA 则会对自己的证书进行签名。即自签名。
-
-
-怎么验证证书有没有被篡改?
-当客户端走 HTTPS 访问站点时,服务器会返回整个证书链。先从最底层的CA开始,用上层的公钥对下层证书的数字签名进行验证。这样逐层向上验证,直到遇到了锚点证书。
-
-
-## 以 get 请求为例,展开探索
-
-1. 请求入口
-```
-- (NSURLSessionDataTask *)GET:(NSString *)URLString
- parameters:(id)parameters
- success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
- failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
-```
-2. 创建 NSURLSessionDataTask
-```
-- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
- URLString:(NSString *)URLString
- parameters:(id)parameters
- uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
- downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
- success:(void (^)(NSURLSessionDataTask *, id))success
- failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
-
-```
-
-
-
-## 较原生相比,AF 做了什么事情
-拿 get、post 为入口,分析源码
-- 初始化很多属性
-- 安全措施,iOS 8 TaskID 不唯一的问题。dispatch_sync
-- 自定义了一个 block 进度回调,使用起来更加方便
-- 用到了解耦,降低了主类的复杂度,维护起来更加方便
-
-## AFSecurityPolicy 源码分析 HTTPS 认证
-
-
-
-
-
-https://www.jianshu.com/p/856f0e26279d
-
-https://www.jianshu.com/u/14431e509ae8
-
-https://blog.csdn.net/ZCMUCZX/article/details/79399517
diff --git a/Chapter1 - iOS/1.139.md b/Chapter1 - iOS/1.139.md
deleted file mode 100644
index ab3fa73..0000000
--- a/Chapter1 - iOS/1.139.md
+++ /dev/null
@@ -1,347 +0,0 @@
-# 图形渲染技巧
-
-## GPU、CPU
-
-难道不能直接将数据从 CPU 跨到 GPU 处理?为什么需要多此一举,出现 OpenGL 这个框架?
-
-
-
-数据饥饿:从一块内存中将数据复制到另一块内存中,传输速度是非常慢的。内存复制数据时,CPU 和 GPU 都不能操作数据,避免引起错误。
-
-- 如果 CPU、GPU 同时操作内存,同步和处理非常麻烦,加一些锁或额外手段将造成速度损耗。
-- 如果 GPU 处理完处于等待状态
-所以加了 buffer 缓冲区来处理该问题。有很多缓冲区,比如颜色缓冲区、深度缓冲区...
-
-
-
-## 着色器渲染流程
-
-缺点:
-- 需要遮盖的部分,画了多次,造成了渲染性能问题,浪费了资源。
-- 叠加的情况,油画算法无法处理。
- 使用油画算法,只要将场景按照物理距离观察者的距离远近排序。由远及近的绘制即可。 但某些情况下,这个距离无法排序。比如下面的场景:
-
-
- 如果三个三角形是叠加的情况,油画算法无法处理。
-
-##### 正背面剔除(Face Culling)
-
-想象一个 3D 图形,你从任何一个方向去观察,最多可以看到几个面?最多3个,从一个立方体的任意位置和方向去看,最多可以看到3个面。
-
-思考:为什么需要多余的去绘制根本看不到的3个面?
-如果能以某种方式丢弃这部分数据,OpenGL 渲染性能可以提高超过50%。
-
-正背面剔除方案,不仅可以解决隐藏面消除问题,还可以带来性能提升。
-
-如何知道某个面再观察者的视野中会不会出现?任何平面都有2个面,正面、背面。意味着同一个时刻只能看到一个面。
-
-OpenGL 可以做到检查所有正面朝向观察者的面,并渲染他们,从而丢弃背面朝向的面,这样可以节约片元着色器的开销,提高性能。
-
-核心:OpenGL 如何知道绘制的图形中,哪个是正面,哪个是背面?
-通过分析顶点数据的顺序。
-
-- 正面:按照逆时针顶点连接顺序的三角形面
-- 背面:按照顺时针顶点连接顺序的三角形面
-
-
-
-
-用顺时针、逆时针判断正反面不是绝对的,还需要结合观察者的位置。
-
-
-
-
-分析:
-- 左侧三角形的顶点顺序为:1->2->3;右侧三角形顶点顺序为:1->2->3
-- 当观察者在右侧时,则右边三角形方向为逆时针方向,则为正面。左侧三角形为顺时针,则为反面
-- 当观察者在左侧时,则左边三角形顶点为逆时针方向,则为正面。右侧三角形为顺时针,则为反面
-总结:
-正面和背面是由三角形顶点顺序和观察者方向共同决定的。随着观察者的角度方向改变,正反面也会改变。
-
-
-API
-```
-// 开启表面剔除(默认背面剔除)
-void glEnable(GL_CULL_FACE);
-// 关闭表面剔除(默认背面剔除)
-void glDisable(GL_CULL_FACE);
-// 用户选择剔除哪个面(设置面剔除的方式)
-void glCullFace(GLenum mode); // mode 为:GL_FRONT,GL_BACK,GL_FRONT_AND_BACK。默认 GL_BACK
-// 用户指定绕序那个为正面
-void glFrontFace(GLenum mode); // mode 为:GL_CCW,GL_CW。默认 GL_CCW
-// 剔除正面实现
-glCullFace(GL_BACK);
-glFrontFace(GL_CW);
-```
-
-### 深度问题
-- 什么是深度?深度其实就是该像素点在 3D 世界中距离观察者的距离,z 值。
-- 什么是深度缓冲区?一块内存区域,专门存储着每个像素点(绘制在屏幕上的深度值)。深度值 Z 越大,则离摄像机越远。
-- 为什么需要深度缓冲区?在不使用深度测试的时候,如果先绘制了一个距离比较近的物体,再绘制距离比较远的物体,则距离远的位图因为后绘制,则会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度写入到缓冲区中。除非调用 glDepthMask(GL_FALSE) 来禁止写入。
-
-#### Z-buffer 方法(深度缓冲区 Depth-buffer)
-深度测试。深度缓冲区(Depth buffer)和颜色缓冲区(Color buffer)是一一对应的,颜色缓冲区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。
-在决定是否绘制一个物体表面时,首先要将表面对应的像素深度值与当前深度缓冲区中的值进行比较。如果大于深度缓冲区的值,则丢弃这部分。否则利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓冲区。这个过程称为“深度测试”。
-
-#### 使用深度测试
-深度缓冲区,一般由窗口管理系统 GLFW 创建,深度值一般由16位、24位、32位值表示,通常是24位,位数越高,深度精确度越高。
-- 开启深度测试:`glEnable(GL_DEPTH_TEST)`
-- 在绘制场景前,清除颜色和深度缓冲区:`glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0.0f, 0.0f, 0.0f, 1.0f);`
-- 清除深度缓冲区默认值为1.0,表示最大的深度之,深度值范围是(0, 1),值越小表示越靠近观察者。值越大表示越远离观察者
-- 指定深度测试判断模式:`glDepthFunc(GLEnum mode);`
- - GL_ALWAYS:总是绘制
- - GL_NEVER:永远不绘制
- - GL_LESS:如果当前深度值小于测试值,则绘制
- - GL_LEQUAL:如果当前深度值小于等于测试值,则绘制
- - GL_GREATER:如果当前深度值大于测试值,则绘制
- - GL_GEQUAL:如果当前深度值大于等于测试值,则绘制
- - GL_NOTEQUAL:如果当前深度值不等于测试值,则绘制
-
-
-
-### ZFighting 闪烁问题
-为什么会出现闪烁问题?
-因为开启深度测试后,OpenGL 就不会再去绘制模型被遮盖的部分,而是直接丢弃。这样的实现显示更真实,但是由于深度缓冲区精度的限制,对于深度相差非常小的情况下(例如在同一平面上进行2次绘制)OpenGL 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测,显示出来的现象是交错闪烁。
-
-
-#### ZFighting 闪烁问题解决
-第一步:启用 Polygon offset 方式解决
-让深度值之间产生间隔,如果2个图形之间有间隔,是不是意味着就不会产生干涉。可以理解为在执行深度测试前将立方体的深度值做一些细微的增加,于是就能将重叠的2个图形深度值之间有所区分。
-```
-// 启用 Polygon offset 方式
-glEnable(GL_POLYGOn_OFFSET_FILL)
-```
-参数列表:
-- GL_POLYGON_OFFSET_FILL:对应光栅化 GL_FILL
-- GL_POLYGON_OFFSET_LINE:对应光栅化 GL_LINE
-- GL_POLYGON_OFFSET_POINT:对应光栅化 GL_POINT
-
-第二步:指定偏移量。
-- 通过 `glPolygonOffset` 来指定 .glPolygonOffset 需要2个参数:factor 和 units。
-- 每个 Fragment 的深度值都会增加如下所示的偏移量。`Offset = (m * factor) + (r * units);`
- - m: 多边形的深度斜率的最大值。理解一个多边形越是与近裁剪面平行,m 就越接近于0
- - r:能产生于窗口坐标系的深度值中可分辨的差异最小值。r 是由具体 OpenGL 平台指定的一个敞亮
-- 一个大于 0 的 Offset 会把模型推到离你(摄像机)更远的位置,相应的一个小于 0 的 Offset 会把模型拉近
-- 一般而言,只需要将 -1.0 和 0 这样简单赋值给 glPolygonOffset 基本可以满足需求
-
-```
-void glPolygonOffset(Glfloat factor, Glfloat units);
-应用到片段上总偏移计算公式:
-Depth offset = (DZ * factor) + (r * units);
-DZ:深度值(Z 值)
-r:使深度缓冲区产生变化的最小值
-```
-
-
-
-### 混合
-我们把 OpenGL 渲染时,会把颜色值存储在颜色缓冲区中,每个片段的深度值也存储在深度缓冲区。当深度缓冲区被关闭时,新的颜色将简单的覆盖原来的颜色缓冲区存在的颜色值,当深度缓冲区再次打开时,新的颜色片段只是当它们比原来的值更接近邻近的裁剪平面才会替换原来的颜色片段。`glEnable(Gl_BLEND)`
-
-#### 组合颜色
-目标颜色:已经存储在颜色缓冲区的颜色值
-源颜色:作为当前渲染命令结果进入颜色缓冲区的颜色值
-当混合功能被启用时,源颜色和目标颜色的组合方式是混合方程式控制的。在默认的情况下,混合方程式如下所示:
-`Cf = (Cs * s) + (Cd *d)`
-- Cf:最终计算参数的颜色
-- Cs:源颜色
-- Cd:目标颜色
-- s:源混合因子
-- d:目标混合因子
-
-下面通过一个常见的混合函数组合来说明问题:`glBlendFunc(Gl_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)`
-
-如果颜色缓冲区存在一种红色(1.0f, 0.0f, 0.0f, 0.0f),目标颜色 Cd,如果在这上面用一种 alpha 为0.6的蓝色(0.0f, 0.0f, 1.0f, 0.6f)
-```
-Cd(目标颜色) = (1.0f, 0.0f, 0.0f, 0.0f)
-Cs(源颜色) = (0.0f, 0.0f, 1.0f, 0.6f)
-S(源 Alpha) = 0.6f
-D(目标 Alpha) = 1- S = 0.4f
-Cf = (Cs * s) + (Cd * D)
-```
-
-总结:混合函数经常用于实现在其他一些不透明物体前面绘制一个透明物体的效果。
-
-
-
-## GPUImage
-
-### 说明
-
-开源的基于 GPU 处理图片/视频的一个框架,本身内置了几百种常见的滤镜效果,支持自定义滤镜(由开发者基于 OpenGLES GLSL 实现片元着色器)
-
-GPUImage 基于以下框架:
-
-- CoreMedia
-- CoreVideo
-- AVFoundation
-- QuartzCore
-- OpenGL ES2.0
-
-采用 GPU 加速处理图片/视频滤镜效果。对比 GPUImage/CoreImage
-
-- GPUImage 可以自定义滤镜,缺乏人脸识别功能
-- GPUImage 在 GPU 上处理速度高于 CPU,百倍。
-
-目的:隐藏/减弱关于 OpenGL ES 的复杂性。
-
-滤镜处理的原理:就是把静态图片/视频上每一帧图片进行图形变换(饱和度/色温...)处理之后,再现实到屏幕上,本质上(像素点、颜色的变化)
-
-
-
-
-
-### 模块划分
-
-- 上下文环境:包括运行 GPUImage 的上下文定义、资源定义、缓存管理相关类都包括在其中
-- 输入源:即滤镜处理链路的源头,包括视频、图片在内的各种输入源都定义其中
-
-- 输出源:即处理链路的尽头,用于将处理后的数据绘制到屏幕、或者转成二进制数据推流等等
-
-- 滤镜:提供多达上百种的滤镜效果使用来进行图像处理
-
-
-
-
-
-### 核心流程
-
-#### OpenGL ES 处理图片的流程
-
-- 初始化 OpenGL ES 环境、编译、链接顶点着色器、片元着色器
-- 缓存顶点/纹理/坐标数据,传输相关数据到 GPU
-- 图片绘制在帧缓存区
-- 从帧缓存区绘制图像
-
-#### GPUImage 处理图片的流程
-
-整体环节:Source(图片/视频数据源) -> filters(一堆滤镜)-> final target(处理好的图片/视频)
-
-
-
-##### Source(数据源)
-
-- GPUImageVideoCamera : 摄像头(用于实时拍摄视频)
-- GPUImageStillCamera:摄像头(用于拍照片)
-- GPUImagePicture:用于处理已经拍摄完成的照片
-- GPUImageVideo:用于处理已经拍摄好的视频
-
-##### Filter(滤镜)
-
-GPUImageFilter:用来接收图形源,通过自定义顶点/片元着色器来渲染新的图像,完成滤镜处理后交给响应链的下一个对象。
-
-GPUImage 中的滤镜均继承自 `GPUImageFilter`其定义了一个滤镜处理的基本流程。GPUImageFilter 继承自 GPUImgaeOutput,同时实现了 GPUImageInput 协议,这就使得 GPUImageFilter 即可接收 frameBuffer 输入进行图形处理。
-
-`@interface GPUImageFilter : GPUImageOutput
-
-分析:可以发现 block 本质就是结构体,和 OC 对象一样,也有 isa 指针。block 传递进去的方法,被包装成 block 的成员变量,是一个叫做 FuncPtr 的函数指针了。
-
-是不是我们可以按照系统定义,来构造一个 struct,承接一个 block,然后发起调用呢?
-
-```objective-c
-typedef NS_OPTIONS(int, AspectBlockFlags) {
- AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
- AspectBlockFlagsHasSignature = (1 << 30)
-};
-
-typedef struct AspectBlock {
- __unused Class isa;
- AspectBlockFlags Flags;
- __unused int Reserved;
- void (__unused *invoke)(struct AspectBlock *block, ...);
-
- struct {
- size_t reserved;
- size_t Block_size;
- void (*copy)(void *dst, const void *src);
- void (*dispose)(const void *);
- } *descriptor;
-} *AspectBlockRef;
-
-void(^printBlock)(NSString *) = ^void(NSString *msg) {
- NSLog(@"%@", msg);
-};
-printBlock(@"Hello world");
-
-struct AspectBlock *fakeBlock = (__bridge struct AspectBlock *)printBlock;
-((void (*)(void *, NSString *))fakeBlock->invoke)(fakeBlock, @"Hello world");
-```
-
-
-
-思考:我们目前已经用自定义的 struct 来承接了 block 并成功执行了。能否用 NSInvocation 来触发 block?
-
-
-
-## NSInvocation 触发 block
-
-一个方法需要成功调用并执行需要3要素:
-
-- 方法名称 `SEL`
-- 方法签名(参数个人、参数类型、返回值类型等信息) `Method Type Encoding`
-- 方法地址、方法实现 `IMP`
-
-如何从自定义的 block 结构体中获取这些信息呢?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-AspectsIdentifier:每做一次方法交换,都会转换为一次 AspectsIdentifier。是核心逻辑。
-
-
-
-以一个例子作为源码探索切入口,点击跳转到源码中
-
-
-
-可以看到给 NSObject 添加分类,核心 2 个 API。一个对象方法、一个类方法:
-
-```objective-c
-+ (id
-
-第二步:因为我们通过 AOP 给原始方法添加了 block,最后的效果是既可以调用原始方法,又可以调用 block 添加的代码。实现的前提是什么?
-
-比较 block 和 hook 类的方法的签名信息需要 Match。具体逻辑查看注释。
-
-
-
-```shell
-(lldb) po blockSignature
-
-
-
-
-## objc_msgForward
-
-骚操作:
-
-- 将待 hook 的方法,和 `objc_msgForward` 进行交换。 `objc_msgForward` 不管对象有没有实现,都会触发消息转发流程
-- 此时会走 Runtime 的 NSObject `forwardInvocation` 流程。且 Aspects 将 `forwardInvocation` 方法指向了 `__ASPECTS_ARE_BEING_CALLED__` 方法。
-
-经历这么一波处理,hook 最后都守口到了 `__ASPECTS_ARE_BEING_CALLED__` 方法中。
-
-前面研究过了 `AspectIdentifier` 的逻辑。接下去继续看看后续步骤。
-
-
-
-可以看到内部执行 `aspect_prepareClassAndHookSelector`,其内部会调用 `aspect_hookClass`,又会调用 `aspect_swizzleClassInPlace`,最后调用 `aspect_swizzleForwardInvocation` 方法。
-
-```objective-c
-static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
-static void aspect_swizzleForwardInvocation(Class klass) {
- NSCParameterAssert(klass);
- // If there is no method, replace will act like class_addMethod.
- IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
- if (originalImplementation) {
- class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
- }
- AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
-}
-```
-
-该方法将被 hook 对象的 `forwardInvocation:` 方法替换为 `__aspects_forwardInvocation:`。
-
-回归头继续看下面的逻辑。
-
-
-
-`class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding)` 可以实现将被 hook 类的 hook 方法,替换为 `_objc_msgForward` 或者某些版本下需要的 `_objc_msgForward_stret`。
-
-
-
-比如 ViewController 的 viewWillAppear hook 流程就是:
-
-`[UIViewController viewWillAppear:]` -> `_objc_msgForwar` -> `[UIViewController forwardInvocation:]` -> `__ASPECTS_ARE_BEING_CALLED__`
-
-
-
-
-
-## 总结
-
-Aspects 是 Runtime 使用的一个经典库,处理好核心逻辑后,也做了一些黑名单、线程安全等的保护。也有一些类似日志回放功能的处理。
-
-
-
-
-
diff --git a/Chapter1 - iOS/1.141.md b/Chapter1 - iOS/1.141.md
deleted file mode 100644
index dd99c20..0000000
--- a/Chapter1 - iOS/1.141.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# LLDB
-
-
-
-## LLDB 架构
-
-
-
-说明:类似一个 CS 架构。
-
-- **`lldb-mi` 的角色:** 它是一个**协议适配器**,专注于将 **GDB/MI 协议** 翻译成 **LLDB API 调用**。
-- **`API` 是关键桥梁:** `lldb-mi` 几乎所有的功能都通过调用 **LLDB Public API** 来实现。
-- **Core 下的模块是 LLDB 的能力:** 图中 `Core` 下列出的 `Object Files`, `Symbols`, `Process` 等,代表了 **LLDB 调试器引擎本身提供的强大核心功能**。`lldb-mi` 依赖 API 来利用这些功能。
-- **依赖关系:** `lldb-mi` -> `API` -> (LLDB 核心引擎的) `Object Files`, `Mach-O/ELF`, `Symbols`, `DWARF`, `Disassembly`, `LLVM`, `Process`, `GDB Remote`。
-- **目的:** 这张图说明了 `lldb-mi` 是如何构建在 LLDB 强大的核心调试引擎之上,通过标准的 API 调用其功能,从而为支持 GDB/MI 协议的客户端提供调试服务。
-
-
-
-
-
-## LLDB Workflow
-
-
-
-说明:
-
-- **lldb-server**:调试服务端,`lldb-server` 作为“调试代理”,直接操控目标程序。分为两种部署方式:
-
- - **Host 端**:调试本地程序(如 macOS 进程)。
-
- - **Remote 端**:部署到目标设备(如手机/嵌入式设备),直接控制被调试程序
-
-- 通信层:TCP + GDB RSP 协议
-
- - **TCP Socket**:物理传输通道,连接 Host 的 LLDB 和 Remote 的 lldb-server
- - **GDB RSP (Remote Serial Protocol)**
- - **基于 ASCII 的调试协议**(明文消息,如 `$m
-此外,`mobileprovision` 文件是不可读的。可以通过 **security cms -D -i 195103db-6d6f-4da1-bd0e-66d5db88176f.mobileprovision -o profil
-e.plist** 指令,dump 成为一个 plist 格式的文件。如下图所示:
-
-
-指令解读:
-- security:是 macOS 自带的安全相关的命令行工具,用于处理证书、配置文件、密钥等的处理
-- cms:表示使用 CMD(Cryptographic Message Syntax,密码消息语法)相关功能,用于处理消息签名和加密消息
-- -i:指定输入文件
-- -o:指定输出文件
-
-重要信息:
-- DeveloperCertificates: 允许使用的开发者证书
-- Entitlements:允许使用的权限列表
-- ProvisionedDevices:允许安装的设备列表(ProvisionsAllDevices,代表授权任意设备)
-
-
-
-### 3. 授权文件(Entitlements)
-声明了 App 所需的权限
-
-
-
-### 4. 签名 codesign
-
-#### 1. 基础签名
-
-- **-s**:--sign identity 指定签名所用的证书(- 代表 ad-hoc 签名)
-- **--entitlements**:entitlements_file 指定签名所需要的 entitlements 文件
-- **-f**: --force 强制替换现有签名
-- **-preserve-metadata=identifier,entitlements**: 重用就签名的一些信息
-- **-deep**: 递归对该 bundle 内包含的其他文件签名
-
-
-注意:为什么不推荐使用 `--deep`?
-
-在没有 `--deep` 的情况下,codesign 命令只会对指定的主目标(main target)(例如 .app 包或 .framework)进行签名。它不会自动递归地签名的 .app 包内部的任何嵌套的组件(如嵌入的 .framework 或 .dylib)。
-
---deep 选项的设计初衷是试图递归地签名一个 bundle 内部的所有嵌套代码。例如,如果你的 MyApp.app 内部嵌入了 ThirdParty.framework,使用 `codesign --deep -f -s "Your Identity" MyApp`.app 会同时签名 MyApp.app 和其内部的 ThirdParty.framework。
-
-错误的签名顺序(主要问题):
-- 代码签名不仅是对二进制文件盖章,它还计算并存储每个组件的哈希值到其 _CodeSignature/CodeResources 文件中。
-- 当你签名一个 .app 时,它也会计算其内部所有文件(包括嵌套的 .framework)的哈希值。如果这些嵌套的组件在 .app 被签名之后又被修改了,其哈希值就会改变,导致签名无效。
-- `--deep` 的工作方式是:先递归地签名最内层的组件(如 ThirdParty.framework),然后签名外层的容器(MyApp.app)。
-
-这看起来是正确的,对吗? 但实际上,在签名外层 .app 时,它记录的仍然是内层组件签名前的哈希值。而内层组件在签名后其内容(因为附加了签名信息)已经发生了变化,这就导致了内外记录不一致,从而使整个签名变得无效且不可靠。
-
-与现代构建系统不兼容:
-- Xcode 和标准的构建流程(如 xcodebuild)的正确做法是:先单独签名每一个嵌套的组件(Embedded Framework),最后再签名主应用。这确保了每个组件都有自己独立的有效签名,并且主应用在签名时记录的是所有嵌套组件的最终状态。
-- `--deep` 破坏了这种明确的、分阶段的签名流程,试图用一步代替多步,反而引入了混乱。
-
-
-
-#### 2. 动态库签名
-
-动态库可以上架,是因为对动态库可以进行签名。来一个 Demo 工程。一个 iOS 工程,以动态库的形式使用 SDWebImage
-
-##### 1. Demo1
-
-1个 iOS App 工程,使用动态库的方式,依赖了 AFNetworking。选择模拟器进行编译,查看日志:
-
-
-
-日志分析:
-1. 日志中的 `--sign` 后一般接的是证书的名称,但是当前日志中是 `-`,代表使用自动签名模式(Automatic Signing)。
- - **--sign 参数**:这是 codesign 命令的核心参数,用于指定用于签名的身份(Identity)。这个身份通常对应着钥匙串(Keychain)中的一个证书(Certificate)及其关联的私钥。
- - **-**:这是一个特殊的标识符,在这里它不代表一个具体的证书名称。它的含义是:“使用临时生成的、匿名的 Ad-Hoc 签名身份来进行签名,而不需要指定一个具体的、来自苹果开发者账户的证书。”
- 在模拟器(Simulator)上运行(正如你的日志中 Debug-iphonesimulator 所示):
- 根本原因:iOS 模拟器不像真机设备那样需要验证苹果官方的代码签名证书来确保安全。模拟器的运行环境更加宽松,其主要目的是为了快速调试。为了方便:使用 Ad-Hoc 签名可以省去为模拟器编译时配置和选择开发证书的步骤,极大地加快了编译和调试的速度。Xcode 默认就会为模拟器构建采用这种方式。
-
-2. 日志中的 `--entitlements /Users/unix_kernel/Library/Developer/Xcode/DerivedData/InstallDyanmicAndStaticFramework-bmcbqvmynpdalkdhzqirbithtfwp/Build/Intermediates.noindex/InstallDyanmicAndStaticFramework.build/Debug-iphonesimulator/InstallDyanmicAndStaticFramework.build/InstallDyanmicAndStaticFramework.app.xcent` 代表 Xcode 开启的 App 能力信息。`--entitlements` 参数后面跟随的是一个 `.xcent` 文件的路径,这个文件包含了应用程序的权限(Entitlements)配置信息,也就是 “App 能力信息”
-
-对其查看,内容如下:
-
-
-`security find-identity -v -p codesigning` 指令用于 macOS 系统中用于查看可用代码签名证书的命令,主要用于开发者在进行代码签名操作前确认可用的证书信息
-
-##### 2. Demo2
-
-1个 iOS App 工程,使用动态库的方式,依赖了 AFNetworking。选择真机进行编译,查看日志:
-
-日志分析:
-1. 日志中的 `Signing Identity: "Apple Development: FantasticLBP@github.com (953PZFXZFR)"` 变成了开发者证书。多了一个配置文件。
-2. 使用的动态库 AFNetworking 是如何签名的?
-选择 Pods 的 Product 里面的 AFNetworking 动态库,右击 “show in finder”。看到并没有一个 **`_CodeSignature`** 的文件夹,也就是没有签名信息。然后用指令 ` objdump --macho --private-headers /Users/unix_kernel/Library/Developer/Xcode/DerivedData/InstallDyanmicAndStaticFramework-bmcbqvmynpdalkdhzqirbithtfwp/Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/AFNetworking` 进行查看,发现也不存在 **`LC_CODE_SIGNATURE`** 存储签名信息的 load command。
-
-
-可能会问,如何确定是动态库?使用 `file AFNetworking` 指令即可验证
-
-
-问题:动态库 AFNetworking 没有经过签名,为什么拷贝到 App 里面后,可以上架?
-其实 Cocoapods 自动生成了脚本,在主工程的 `Build Phases -> Embed Pods Frameworks` 下。且 `Input File Lists` 配置的文件内容,就是所依赖库的文件路径。会被当作参数传递给 `Embed Pods Frameworks` 脚本。
-
-
-观察编译日志,会发现 「Run custom shell script '[CP] Embed Pods Frameworks'」这里,先对 Frameworks 目录进行了创建和拷贝。然后对 AFNetworking 进行签名。
-在主工程的 Products 目录下,选择 App,show in finder,然后显示包内容。查看 `Frameworks` 文件夹下的 `AFNetworking.framework` 已经存在了 `_CodeSignature` 文件夹,也就是已经签名完成。继续查看 Load Command,发现也存在了 **LC_CODE_SIGNATURE** Load Command。
-
-
-同时,也可以看到是先对动态库签名,再对 App 签名。
-
-#### 3. 如何查看签名信息
-
-[jtool2](https://github.com/excitedplus1s/jtool2) 工具可以方便的查看签名信息。jtool2 类似于 otool,但添加了许多 Mach-O 相关的命令,功能更完善。它支持多种运行平台
-
-安装方式为:`brew install --no-quarantine excitedplus1s/repo/jtool2`
-使用方式为:**`jtool2 --sig -vv ${MachOFile}`**
-
-
-
-
-
-### 5. fastlane 相关概念
-
-
-- fastlane 本质就是一套命令行工具,专为用来简化并实现我们与 Apple 交互时的自动化
-- fastlane 的每一个单独工具都是为了解决常见的 App Store 或其他问题而设计的
-- fastlane 通过脚本方式集合了一系列常见的行为,叫做 lane。也就意味着可以通过 lane 来对自己的 App 做一些量身定制的需求
-- fastlane 包含大量的 action
-- action 表示特定的应用商店或者其他开发者工作流任务
-- lane 表示工作流程
-
-- Appfile: 存储有关开发者账号相关信息
-- Fastfile:核心文件,主要用于命令行调用和处理具体的流程,lane 相对于一个方法或者函数。
-
-#### 1. action
-
-- cert: 创建和维护签名证书
-- sigh:配置文件
-- gym:构建打包应用程序
-- deliver:上传应用程序和屏幕截图到 App Store Connect
-- pilot:为 TestFlight 上传构建并处理其管理
-- scan:自动化测试
-- match:团队中同步证书和配置文件
-- boarding:测试邀请
-- pem:管理推送配置文件
-- produce:在 App Store Connect 创建新的 App
-
-
-使用方式有3种:
-- 第一种:命令行方式。比如
- ```
- fastlane scane --workspace "fastlaneDemo.xcworkspace" --scheme "fastlaneDemo" --device "iPhone 8" --clean
- ```
-- 第二种:Fastfile 方式。采用 FastLane 约定的格式去编写逻辑。比如
- ```
- default_platform(:ios)
-
- platform :ios do
-
- lane :builds do
- # 单元测试
- scan(
- workspace: "fastlaneDemo.xcworkspace",
- scheme: "fastlaneDemo",
- devices: ["iPhone 14 Pro Max", "iPhone 14", "iPhone SE (3rd generation)"],
- clean: true,
- code_coverage: true,
- output_types: "html,junit",
- output_directory: "./fastlane/test_output"
- )
- # 证书
- cert
- # 配置文件
- sigh
- # 打包
- gym(
- workspace: "fastlaneDemo.xcworkspace",
- scheme: "fastlaneDemo",
- clean: true,
- output_directory: "./fastlane/build_output",
- output_name: "fastlaneDemo.ipa",
- silent: false,
- include_symbols: true,
- include_bitcode: true
- )
- end
- end
- ```
-- 第三种:使用特定的 Fastlane 文件。比如使用脚本 `Fastlane scan init` 会生成关于 scan 相关逻辑的脚本文件 `Scanfile`
-
-
-建议使用方式二、三,不建议使用方式一。关于 fastlane 脚本编写文档查看 [fastlane docs](http://docs.fastlane.tools)
-
-使用方式:
-Scanfile 是 专门用于配置 scan 动作(即运行测试) 的配置文件。它的唯一目的是为 scan 提供默认参数。你可以在里面设置 scheme、output_directory、code_coverage 等测试相关的选项。当你在命令行直接运行 fastlane scan 或在 Fastfile 的 lane 中调用 scan 时,它会自动读取 Scanfile 中的配置。
-
-
-
-| 文件 | 角色 | 用途 | 示例内容 |
-| :-------------- | :---------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
-| **`Fastfile`** | ****指挥官\**/\**剧本** | **定义 lanes(工作流)**。这是 Fastlane 的核心,你在这里组合不同的动作来创建自动化流程。 | `lane :test { scan }` `lane :beta { cert; sigh; gym; upload_to_testflight }` |
-| **`Scanfile`** | **专项配置员** | 只为 **`scan`** 动作提供默认配置参数。 | `scheme("MyApp")` `clean(true)` `output_directory("./test_results")` |
-| **`Gymfile`** | **专项配置员** | 只为 **`gym`** 动作(构建 IPA)提供默认配置参数。 | `workspace("MyApp.xcworkspace")` `export_method("app-store")` `output_directory("./builds")` |
-| **`Matchfile`** | **专项配置员** | 只为 **`match`** 动作(证书管理)提供默认配置参数。 | git_url("https://github.com/.../certs")` `app_identifier("com.yourapp") |
-
-总结:
-
-- **`Fastfile`** 就像是一个**总剧本**,里面写着:第一场戏(`lane :test`)是跑步测试,第二场戏(`lane :beta`)是打包上传。
-- **`Scanfile`**、**`Gymfile`** 等就像是每个**演员(scan, gym 动作)的个人小抄**,上面写着他们的默认表情、站位等细节。
-- 当“总剧本”喊到某个演员时,演员就会按照自己“小抄”上的默认设置来表演,除非剧本特意指定了另一种表演方式。
-
-
-
-QA:按照上面的思路和角色分工、能力边界,将下面的代码优化
-
-```ruby
-platform :ios do
-
- lane :builds do
- # 单元测试
- scan(
- workspace: "fastlaneDemo.xcworkspace",
- scheme: "fastlaneDemo",
- devices: ["iPhone 14 Pro Max", "iPhone 14", "iPhone SE (3rd generation)"],
- clean: true,
- code_coverage: true,
- output_types: "html,junit",
- output_directory: "./fastlane/test_output"
- )
- # 证书
- cert
- # 配置文件
- sigh
- # 打包
- gym(
- workspace: "fastlaneDemo.xcworkspace",
- scheme: "fastlaneDemo",
- clean: true,
- output_directory: "./fastlane/build_output",
- output_name: "fastlaneDemo.ipa",
- silent: false,
- include_symbols: true,
- include_bitcode: true
- )
- end
-end
-```
-
-优化:
-
-- 文件一:`./fastlane/Scanfile` 只存放测试相关的配置
-
- ```ruby
- # Scanfile 专用配置
- workspace("fastlaneDemo.xcworkspace")
- scheme("fastlaneDemo")
- devices(["iPhone 14 Pro Max", "iPhone 14", "iPhone SE (3rd generation)"])
- clean(true)
- code_coverage(true)
- output_types("html,junit")
- output_directory("./fastlane/test_output")
- skip_build(true)
- ```
-
-- 文件二:`./fastlane/Gymfile` 存放构建相关的配置
-
- ```ruby
- # Gymfile 专用配置
- workspace("fastlaneDemo.xcworkspace")
- scheme("fastlaneDemo")
- clean(true)
- output_directory("./fastlane/build_output")
- include_symbols(true)
- include_bitcode(true)
- ```
-
-- 文件三:`./fastlane/Fastfile` 存放 lane 相关逻辑
-
- ```ruby
- # Fastfile - 定义 lanes
- default_platform(:ios)
-
- platform :ios do
- # 定义一个名为 builds 的 lane
- lane :builds do
- # 运行测试,所有配置会自动从 Scanfile 读取
- scan
- # 管理证书
- cert
- sigh
- # 构建 ipa,所有配置会自动从 Gymfile 读取
- gym
- end
- end
- ```
-
-分析:
-
-1. **清晰与模块化**:每个文件职责单一,易于维护。
-
-2. **复用性**:你可以在 `Fastfile` 中创建多个不同的 lane(如 `lane :tests`、`lane :adhoc`),它们都可以共享 `Scanfile` 和 `Gymfile` 中的通用配置,无需重复代码。
-
-3. **可覆盖性**:在 `Fastfile` 的 lane 中调用 `scan` 或 `gym` 时,你可以随时覆盖对应 `*.file` 中的默认设置。例如:
-
- ```ruby
- lane :special_build do
- gym(
- output_name: "SpecialRelease.ipa" # 覆盖 Gymfile 中的默认命名规则
- )
- end
- ```
-
-
-
-#### 2. Matchfile
-
-终端使用 `fastlane match init` 指令创建 Matchfile,同时根据提示选择一些模版和输入信息。
-
-
-
-
-
-- `git_url`:
-
- 是 `match` 最核心的配置,指定了存储加密证书和配置文件的 Git 仓库地址。`match` 不会将敏感的证书文件(`.cer`)、私钥文件(`.p12`)和配置文件(`.mobileprovision`)保存在本地或只留在苹果开发者门户。
-
- 相反,它会将这些文件**加密后**推送到这个指定的 Git 仓库(`https://github.com/FantasticLBP/knowledge-kit`)中。
-
- 当任何开发者或 CI/CD 系统需要这些文件时,`match` 会从这个仓库克隆或拉取最新的加密文件,然后在本地解密并安装到钥匙串和 Xcode 中。
-
- 这个仓库应该是**私有的(Private)**,因为里面存储的是你应用的敏感安全凭证
-
-- `storage_mode("git")`:
-
- 明确指定 `match` 使用 Git 作为存储后端。
-
- match` 支持多种存储方式,`git` 是**默认且最常用**的一种。
-
- 其他可选模式包括 `s3`(Amazon S3)和 `google_cloud`(Google Cloud Storage)。这些通常在更复杂或企业级的 CI/CD 环境中使用,以提高大文件的下载速度
-
-- `type("development")`
-
- 设置 `match` 的**默认操作类型**为 `development`(开发证书和配置文件)
-
- 这个参数定义了你要管理哪类证书。iOS 开发中有几种主要类型:
-
- - `"development"`:用于开发阶段,可在真机上调试应用。
- - `"appstore"`:用于提交到 App Store 或 TestFlight。
- - `"adhoc"`:用于分发给有限数量的测试设备(最多 100 台)。
- - `"enterprise"`:用于企业账号的内部分发。
-
- 在这里设置为 `"development"` 意味着:
-
- - 当你直接运行 `fastlane match`(而不指定类型)时,它会默认操作开发证书。
- - 当你运行 `fastlane match development` 时,它也会使用这个配置(尽管命令行参数已经指定了类型,但其他相关配置会从这里读取)。
-
-这个 `Matchfile` 配置告诉我们:
-
-1. **存储位置**:所有加密的证书和配置文件都将被同步到 `https://github.com/FantasticLBP/knowledge-kit` 这个 Git 仓库中。
-2. **存储方式**:使用 Git 进行版本管理和同步(这是标准做法)。
-3. **默认环境**:默认情况下,操作的是用于**开发环境**的证书和配置文件。
-
-**一个典型的工作流程:**
-
-1. 团队成员 A 首次运行 `fastlane match development`。
-2. `match` 会检查指定的 Git 仓库中是否已有开发证书。
- - 如果**没有**,它会连接到苹果开发者门户,创建新的开发证书和配置文件,加密后推送到 Git 仓库。
- - 如果**已有**,它会将加密文件拉取到本地,解密后安装到 Xcode 和钥匙串中。
-3. 团队成员 B 加入项目,同样运行 `fastlane match development`。
-4. `match` 从同一个 Git 仓库拉取**完全相同的**证书和配置文件,确保团队环境一致。
-
-
-
-## 三、持续集成
-
-Cotinuous Integration,持续集成意味着每次代码的变更都在构建服务器上运行测试,并在指定场景下触发。这样如果开发者将测试失败的代码推送到代码库,也称为破坏构建,CI 会触发警告。
-
-- 主动式 CI:CI 提供商
-- 托管式 CI:github action
-- 手动式 CI:自己管理,Travis CI、Jenkins
-
-
-
-- Travis CI:小型开源项目,付费、简单、方便、
-- Jenkins:大型企业、丰富的自定义选项、定制化,不能做到开箱即用,免费
-
-### 1. docker
-
-> 问:为什么一定要 docker?不能在服务器上按照本地的配置安装所需的各个软件吗
->
-> 答:在一个团队中,每个开发者的本地环境都可能略有不同(macOS 版本、Xcode 通过 App Store 安装还是手动安装、Homebrew 的使用方式等)。
->
-> - **问题**:新同事加入,需要花费**一整天甚至更长时间**来按照文档一步步配置环境,任何一步的疏漏都会导致环境配置失败。
-> - **后果**: onboarding 成本极高,而且无法保证所有人的环境真正一致,为后续的协作埋下了隐患。
->
-> 为什么“手动安装一下”不是最优解?它的成功依赖于:
->
-> - **人的记忆和文档**:需要有人(或文档)准确地记录下所有依赖的**精确版本**(不仅仅是 `fastlane`,还包括它的依赖,以及依赖的依赖)。这份文档需要随着项目的每一次依赖变更而**实时更新**,这几乎是不可能的任务。
-> - **手动操作的准确性**:需要操作人员完全正确地执行安装步骤,不能有任何错漏。这是一个枯燥且容易出错的过程
-
-#### 1. 定义
-
-**Docker 概述**: Docker 是一种成熟高效的软件部署技术,利用容器化技术为应用程序封装独立的运行环境。每个运行环境即为一个**容器**,承载容器运行的计算机称为**宿主机**。
-
-容器。容器虚拟化指的是操作系统而不是硬件,容器之间是共享同一套操作系统资源的。
-
-- 镜像(image):提供容器运行时所需的程序、库、资源、配置等文件,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)
-- 容器(Container):镜像运行时的实体,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务
-- 仓库(Repository):镜像构建完成后,可以很容易的在当前容器上运行
-
-虚拟机:虚拟出一套硬件后,在其上运行一个完整的操作系统。
-
-
-
-**容器与虚拟机的区别**:
-
-* **Docker容器**: 多个容器共享同一个系统内核
-* **虚拟机**: 每个虚拟机包含一个操作系统的完整内核
-* **优势**: 所以 Docker 容器比虚拟机更轻量、占用空间更小、启动速度更快
-
-**镜像 (Image)**:
-
-* **定义**: 镜像是容器的模板,可类比为软件安装包
-* **类比**: 类似于制作糕点的模具,可用于创建多个糕点(容器),并可分享给他人
-
-**容器 (Container)**:
-
-- **定义**: 容器是基于镜像运行的应用程序实例,可类比为安装好的软件。
-- **类比**: 类似于模具制作出的糕点。
-
-**Docker仓库 (Registry)**:
-
-* **定义**: 用于存放和分享Docker镜像的场所
-* **Docker Hub**: Docker的官方公共仓库,存储了大量用户分享的Docker镜像。
-
-
-
-Docker 理解为:标准化、一摸一样的工具箱和工作环境。保证无论哪个工人来,用的工具和环境都完全一样,避免“在我这儿是好的”问题。
-
-- 环境标准化:创建一个包含特定版本 Ruby、Fastlane、Xcode 命令行工具、Cocoapods 的镜像
-- 隔离一致性:确保开发、测试、生产环境的构建完全一致,消除“环境依赖”问题
-- 快速搭建与清理:轻松为 Jenkins 提供纯净、可随时销毁和重建的构建环境。
-
-**价值:开发者需要花费大量时间排查环境差异,而不是专注于修复真正的业务逻辑 Bug,严重拖慢开发效率。**
-
-
-
-#### 2. docker 的核心角色与能力
-
-docker 在 iOS CI/CD 管道中(通常与 Jenkins、Gitlab CI、Github Actions 等工具配合)中主要负责“管理阶段”和“环境保障”
-
-1. 环境标准化与一致性
-
- - 问题:传统的 CI 机器(无论是物理机还是虚拟机)环境复杂,不同项目所依赖的 Ruby、NodeJS、Python、Fastlane、Cocoapods 等工具版本可能冲突。造成“在我本地是好的,在 CI 机器上发布后就失败了”。(CI 机器上并不一定只部署一个 iOS 的 CI 项目,可能其他项目也部署了,可能由于 Ruby 安装了2.0,但是 iOS 的 CI 服务需要 Ruby 3.0+,这样就造成了本地 CI 服务正常,部署到 CI 服务器之后就有了问题)
- - Docker 解决方案:构建一个专门的 Docker 镜像,预安装项目所需的所有命令行工具和依赖的特定版本,专门负责 CI 逻辑(如 Ruby 3.1.2、Fastlane 2.2.1、Cocoapods 1.22.1、Xcode 16)
- - 结果:每一次 CI 构建都会从一个纯净、一致、已知状态的镜像环境启动,彻底消除了环境差异导致的构建失败,保证了构建结果的可靠性和可重现性。
-
-2. 依赖隔离
-
- 多个 iOS 项目可能在同一台 CI 服务器上运行。使用 Docker 可以将每个项目需要的构建环境相互隔离。避免了项目之间的依赖冲突(例如,项目 A 需要 Cocoapods 1.10,项目 B 需要 Cocoapods 1.11)
-
-3. 快速的构建环境准备:
-
- 相比启动一个完整的虚拟机,启动一个 Docker 容器是秒级的。这大大减少了 CI 流水线等待构建环境就绪的时间,加快了整个构建流程的反馈循环
-
-4. 版本控制与可追溯性:
-
- DockerFile 是文本文件,可以放入 git 仓库进行版本管理。对构建环境的任何更改(例如升级 Fastlane 版本)都像代码更改一样,可以通过 Pull Request 进行审查、测试和记录,实现了基础设施即代码 (IaC)。
-
-5. 作为轻量级的“任务运行器”
-
- 在 CI 流程中,Docker 容器通常被当做一个一次性的、执行特定任务的“沙盒”,比如:执行 pod install、执行 lint 或代码分析、运行 fastlane。
-
-
-
-#### 3. docker 的能力边界
-
-尽管 docker 很强大,但在 iOS 开发领域,它有非常明确且无法跨越的边界:
-
-1. 无法直接构建 iOS 应用(最关键的边界)
-
- - 根本原因:iOS 最终编译、链接和打包必须依赖 MacOS 内核和 Xcode、Clang、SwiftCli
- - Docker 局限性:标准的 Docker 容器基于 Linux 内核。它无法运行 MacOS 系统或 Xcode 这样的 GUI 应用(所以无法执行 xcodebuild 指令)
- - 结论:无法在 linux docker 容器内编译出 `.ipa` 文件
-
-2. 无法运行 MacOS 镜像
-
- Apple 的许可协议和硬件限制,导致不存在官方的 MacOS Docker 镜像。虽然有类似 LinuxKit 这样的非官方项目,但不够稳定,不能用于生成环境,风险较大
-
-
-
-#### 4. 最佳搭配
-
-明确了 docker 的作用和能力边界后,一个典型的 iOS CI/CD 架构就清晰了:**“在 Linux Docker 容器中准备环境和运行脚本,在 MacOS 主机/节点上进行最终编译”**
-
-- CI Server(Jenkins Controller):可以运行在任何系统上,负责任务调度
-- MacOS Build Agent/Node:一台或多台安装了 Xcode 的 MacOS 系统的电脑,它被注册到 CI Server 上,专门用于执行需要的 xcodebuild 任何
-- Docker 的使用:
- - CI 流水线启动后,CI Server 会现在 Linux Docker 容器中完成所有它能做的工作:代码拉取、依赖安装(pod install)、运行单元测试(如果测试不依赖 MacOS 框架)、执行静态分析等
- - 当需要进行 Xcode 的步骤(如编译、打包、签名)时,CI Server 会将工作委托给一台 MacOS Build Agent
- - MacOS Agent:接收任务,它可能本身会通过一个 Docker 来获取一个一致的环境(用于运行 fastlane 等工具),或者直接使用本地安装的工具,然后调用 xcodebuild 和 fastlane 完成最终的构建和打包
- -
-
-
-
-
-
-### 2. fastlane
-
-fastlane 可以理解为**专业高效的“工人”**。它精通所有 iOS 打包、签名、测试、上传的具体细节,干活又快又好。
-
-是一个自动化命令工具集。
-
-### 3. jenkins
-
-统筹全局的“项目经理”,它不亲手干活,也不提供工具,但负责安排任务、监控进度、触发流程(比如代码一来就让工人工作),并向大家汇报结果。
-
-- 调度与触发:监听 git 代码推送,定时或者其他事件触发整个流水线
-- 流程编排:定义 pipeline 流水线,决定先做什么后做什么
-- 资源管理:分配和管理执行任务的服务器(成为 Agent/Node)
-- 状态监控与报告:集中展示构建结果、测试报告、日志、并发送通知
-
-
-
-### 4. 黄金搭档
-
-```mermaid
-flowchart TD
-A[开发者推送代码] --> B[Jenkins 监听到推送事件]
-
-subgraph Jenkins_Agent_Node[Jenkins 代理节点]
- direction TB
- B --> C[Jenkins 触发 Pipeline]
- C --> D[Pipeline 步骤: 准备环境]
- D --> E["执行 Docker Run项目 ${env.JOB_NAME} 的测试已通过。
- -构建详情:
-测试覆盖率:
-${coverageReport}
-
- 此MR已获得足够审批,可以合并。
- """, - to: "${changeAuthor},${reviewers}", - mimeType: 'text/html' - ) -} - -// 发送构建成功通知 -def sendBuildSuccessNotification() { - emailext ( - subject: "✅ 构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}", - body: """ -项目 ${env.JOB_NAME} 的构建已成功完成。
-构建详情:
-编译检查通过,可以继续代码审查流程。
- """, - to: env.CHANGE_AUTHOR ?: 'team@example.com', - mimeType: 'text/html' - ) -} - -// 发送失败通知 -def sendFailureNotification() { - def recipients = env.CHANGE_AUTHOR ? "${env.CHANGE_AUTHOR},team@example.com" : 'team@example.com' - - emailext ( - subject: "❌ 构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}", - body: """ -项目 ${env.JOB_NAME} 的构建失败。
-构建详情:
-
-
-效果:线上低概率异常的根源被提前消灭,问题无法逃逸到现线上,业务层几乎无需处理这类异常(因为代码本身就没有漏洞)
-
-关于如何编写 LLVM 插件,做到在 Xcode 中实时展示代码中存在的问题,可以查看:[LLVM 插件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md) 这篇文章。
-
-
-
-### 方案 4:轻量级熔断 / 降级(适合核心业务场景)
-
-对于 “万分之一” 但影响大的异常(如支付金额计算、商品价格),采用「熔断策略」:
-第一次出现异常:上报 + 降级为默认值(如价格显示 “--”);
-短时间内多次出现(如 1 分钟内 > 3 次):触发熔断,建议:展示兜底 UI,提示安抚用户,稍等片刻后尝试(起码不会发生稳定的 crash 或者业务异常)
-熔断状态上报 APM,研发收到告警后优先修复。
-
-### 方案 5:语义化默认值 + 业务无感知适配(美团 / 饿了么实践)
-
-返回**业务可识别的语义化空值**,并在 UI 层做统一适配:
-
-| 异常场景 | 兜底值 | UI 层统一处理 | 业务层感知 |
-| ---------------- | -------- | --------------- | ---------- |
-| 数组越界 | NSNull | 显示 “--” | 无 |
-| 字典 key 为 nil | NSNull | 显示 “暂无数据” | 无 |
-| 分母为 0 | INFINITY | 显示 “计算异常” | 无 |
-| 字符串转数字失败 | NSNull | 显示 “--” | 无 |
-
-**实现方式**:
-
-- 封装全局 UI 工具类(如`WXUILabel+Safe.h`),重写`setText:`方法:
-
-```objective-c
-- (void) safe_setText:(id)text {
- if (text == [NSNull null] || text == nil) {
- self.text = @"--";
- } else if ([text isKindOfClass:[NSNumber class]] && isinf([text doubleValue])) {
- self.text = @"计算异常";
- } else {
- self.text = [text description];
- }
-}
-```
-
-业务层只需使用`safe_setText:`,无需为每个异常场景写判断逻辑。
-
-
-
-## 四、最佳实践
-
-1. **开发环境零容忍**:通过静态分析、自定义 LLVM 插件 + 运行时 Hook,让问题在测试阶段暴露,比如异常发生时,通过日志打印案发现场数据,或者在 Xcode 面板上可视化的在有问题的代码地方显示 error,及时 crash 掉,阻塞正常流程,以防止开发偷懒不去及时修复,产生技术债,同时也防止问题逃逸到线上。
-2. **生产环境软兜底**:不要一刀切,做到可配置。业务方在配置平台可以针对不同业务域的配置,决定是否开启兜底展示(某些业务域的架构师可能更严格,需要开发在 debug 阶段解决这些问题,线上如果出现就统计为 crash)。如果开启,那么安全气垫就返回语义化默认值(而非无意义的 0),全局 UI 统一处理,业务层零侵入;
-3. **可观测驱动修复**:APM 平台统计异常频率 / 影响度,优先处理高频高影响的异常;
-4. **核心场景按需关注**:仅对价格、支付等核心场景注册回调,非核心场景无需处理。
-
-虽然机制和策略已经制定了,但是执行还是靠人,难以保证100%解决问题。所以开发解决通过 lint 和 crash 可以拦截到大部分问题,但是逃逸到线上的部分问题,还是需要安全气垫去 cover,线下无限毕竟去清空问题 + 少数逃逸到线上的问题通过安全气垫去 cover 就可以保证相对全面的安全守护。
-
-
-
-## 五、平台做些什么
-
-获取到 问题数据是第一步,获取之后问题需要及时上报,需要有一个数据上报系统,数据及时准确、高效的上传到服务端(这就是另一个命题,可以查看这篇文章:[打造一个通用、可配置、多句柄的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)),到 APM 后台。后台产生工单,根据业务模块分配到对应的责任人、群组。根据问题的紧急程度,报警策略和提醒频次也不一样,比如**波动报警、自定义报警策略等(这也是另一个命题:智能报警策略、波动报警策略、业务线自定义可配置责任人等)**。责任人及时解决问题、修改工单状态、热修复或者发布新版本,问题闭环。
-
-线上异常上报后,APM 平台需提供:
-
-- 异常频率(如 “数组越界” 仅 1/10000);
-- 影响用户数(如仅影响 10 个用户);
-- 业务上下文(如 “商品详情页 - 价格数组”);
-- 调用栈 + 设备信息。
-
-**核心逻辑**:
-
-- 低频率、低影响的异常(如 1/10000,影响 10 用户):暂时无需业务层处理,研发排期修复即可;
-- 高频率、高影响的异常(如 1/100,影响 1000 用户):触发告警,研发紧急修复,业务层临时加兜底。
-
-这样业务开发无需为 “万分之一” 的异常写逻辑,仅需处理 “高频高影响” 的异常。
-
-
-
-## 总结
-
-“最佳实践”部分选择的方案既避免了 “安全气垫返回错误值导致业务异常”,又解决了 “业务开发为低概率异常写冗余逻辑” 的问题,是网易、腾讯、阿里等大厂的通用实践 ——**优雅的方案不是 “兜底所有异常”,而是 “让异常可被提前发现、可被观测、最小化影响”**,即使少数逃逸到线上的,再由安全气垫去兜底解决(解决了展示问题、异常上报和案发现场还原问题),一个好的 APM 平台不光是告诉你又问题,还会告诉你哪里有问题。
-
diff --git a/Chapter1 - iOS/1.15.md b/Chapter1 - iOS/1.15.md
deleted file mode 100644
index 9efffd5..0000000
--- a/Chapter1 - iOS/1.15.md
+++ /dev/null
@@ -1,54 +0,0 @@
-
-# URL Schemes 的发展
-
-
-
-URL Schemes 的发展过程可以说就是 iOS 效率工具类 App 的发展过程。
-
-起初的苹果建立的 Apple URL Schemes 只是用于自用,里面只有邮件、电话、iTunes 搜索、Youtube 视频等一些内置服务的 URL。
-
-个人认为 URL Schemes 第一次大火是在 2011 年末(如有异议欢迎指正),那个时期也是越狱的鼎盛时期,那个时期越狱后大家都会装的一个插件是 SBSettings[1]。越狱的人都知道每当新系统发布的时候,等待新系统的越狱发布是最撩人的,而这段时期那些「不越狱就能做到某种越狱功能」的应用经常一时间风头无两。
-
-2011年 iOS 5 发布带来了通知中心,没过多久,出现了一大批使用 iOS 系统设置的 URL Schemes 的 App 神奇地完成了接近 SBSettings 的功能——它们可以让我们从通知中心直接跳转到某些 App 的特定界面,比如 Twitter 的发推界面。它们甚至还可以直接跳转到系统设置里的 Wi-Fi 选项。在这一批 App 中,就有如今效率软件霸主之一 Launch Center Pro 的前身——Launch Center。
-
-
-
-## 基本 URL Schemes
-
-基本 URL Schemes 的能力虽然简单有限,但使用情境却是最普遍的。
-
-我所谓的基本 URL Schemes,是指一个 URL 的 Schemes 部分,比如上文提到的微信的 weixin:。这个部分的唯一功能,就是打开相应应用,而不能够跳转到任何功能。
-
-绝大多数所谓支持 URL Schemes 的应用,一般都是只有这么一个部分,它一般是这个应用的名称,比如 OmniFocus 这款应用,它的基本 URL Schemes 就是 Omnifocus:。如果应用的主名称是个中文名的话,它的 URL Schemes 也许会是应用名的拼音,比如 墨客 这款应用,它的基本 URL Schemes 是 moke:。
-
-但,我前面提过了网页 URL 和 iOS 应用的 URL 的三个重要区别,其中第三项,就是 iOS 上的 URL Schemes 并不规范,一个应用的 URL 可以是各种各样的:
-
diff --git a/Chapter1 - iOS/1.17.md b/Chapter1 - iOS/1.17.md
deleted file mode 100644
index aec3fd6..0000000
--- a/Chapter1 - iOS/1.17.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# 对于不可调节高度的UI控件进行改变frame
-
-* 对于不能调节高度的控件比如 UISlider、UISwitch、UIProgressView 等控件的宽高可以用 \(仿射变化\)transform 属性控制高度。
-
-```
-myswitch.transform = CGAffineTransformMakeScale(1,5);
-```
-
-
-
diff --git a/Chapter1 - iOS/1.18.md b/Chapter1 - iOS/1.18.md
deleted file mode 100644
index ab99ba4..0000000
--- a/Chapter1 - iOS/1.18.md
+++ /dev/null
@@ -1,193 +0,0 @@
-# 简单的 Model 与 JSON 相互转换
-
-```
-// JSON:
-{
-"uid":123456,
-"name":"Harry",
-"created":"1965-07-31T00:00:00+0000"
-}
-
-// Model:
-@interface User : NSObject
-@property UInt64 uid;
-@property NSString *name;
-@property NSDate *created;
-@end
-@implementation User
-@end
-
-// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
-User *user = [User yy_modelWithJSON:json];
-// 将 Model 转换为 JSON 对象:
-NSDictionary *json = [user yy_modelToJSONObject];
-
-```
-
-
-### Model 属性名和 JSON 中的 Key 不相同
-
-```
-// JSON:
-{
- "n":"Harry Pottery",
- "p": 256,
- "ext" : {
- "desc" : "A book written by J.K.Rowing."
- },
- "ID" : 100010
-}
-
-// Model:
-@interface Book : NSObject
-@property NSString *name;
-@property NSInteger page;
-@property NSString *desc;
-@property NSString *bookID;
-@end
-@implementation Book
-//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
-+ (NSDictionary *)modelCustomPropertyMapper {
- return @{@"name" : @"n",
- @"page" : @"p",
- @"desc" : @"ext.desc",
- @"bookID" : @[@"id",@"ID",@"book_id"]};
-}
-@end
-```
-
-你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
-
-在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。
-
-在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。
-
-
-### Model 包含其他 Model
-```
-// JSON
-{
- "author":{
- "name":"J.K.Rowling",
- "birthday":"1965-07-31T00:00:00+0000"
- },
- "name":"Harry Potter",
- "pages":256
-}
-
-// Model: 什么都不用做,转换会自动完成
-@interface Author : NSObject
-@property NSString *name;
-@property NSDate *birthday;
-@end
-@implementation Author
-@end
-
-@interface Book : NSObject
-@property NSString *name;
-@property NSUInteger pages;
-@property Author *author; //Book 包含 Author 属性
-@end
-@implementation Book
-@end
-```
-
-
-### 容器类属性
-```
-@class Shadow, Border, Attachment;
-
-@interface Attributes
-@property NSString *name;
-@property NSArray *shadows; //Array***
- - - - - - - - -Native call JS - - //Native - - (void)callJS{ - JSValue *functionName = self.jsContext[@"sum"]; - NSInteger sum = [[functionName callWithArguments:@[@"2",@"18"]] toInt32];; - - UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"来自JS 的计算" message:[NSString stringWithFormat:@"%zd",sum] preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]; - [alertVC addAction:okAction]; - [self presentViewController:alertVC animated:YES completion:nil]; - } - - //JS - function sum(a ,b){ - return parseInt(a) + parseInt(b); - } - - diff --git a/Chapter1 - iOS/1.3.md b/Chapter1 - iOS/1.3.md deleted file mode 100644 index 264b134..0000000 --- a/Chapter1 - iOS/1.3.md +++ /dev/null @@ -1,103 +0,0 @@ - - -# loadView - -1. 作用:加载控制器的view - -2. 何时调用:当控制器的view第一次使用的时候就会调用 - -3. 使用场景:只要想自定义控制器的view就调用此方法 - -访问控制器的View就相当于调用控制器中的view get方法 - -```objective-c --(UIView *)view -{ - if(_view == nil){ - [self loadView]; - [self viewDidload]; - } - return _view; -} -``` - - - -# 控制器加载view的流程 - - - - -* 控制器的 `init` 方法底层会调用 `initWithNibName` 方法 - -`MyViewController *vc = [[MyViewController alloc] init];` - -注意点: - -系统做判断的前提提条件:没有指定nibName;没有自定义loadView方法;控制器以...Controller命名 - - - -判断原则: - -* 判断下有没有指定nibName,如果指定了就去加载nib - -* 判断有没有跟控制器同名的xib,但是xib的名称不带Controller的xib,如果有就去加载 - -* 如果第二步没有指定,就判断有没有跟控制器类名同名的xib,如果有就去加载 - -* 如果没有任何xib描述控制器的view,就不加载xib - - - -## MyViewController加载view的处理 - -* 判断有没有指定xibName,如果有就去加载指定的xib - -* 判断有没有跟控制器类名同名的xib,但是名字不带controller - -* 判断有没有跟控制器类名同名的xib,有就去加载 - -* 直接创建一个空的xib - -例子 - -```objective-c -//在Appdelegate中 -ViewController *vc = [[ViewController alloc] init]; -vc.view.backgroundColkor = [UIColor redColor]; -self.window.rootViewController = vc; -[pself.window makeKeyAndVisable]; - -//ViewController --(UIView *)view{ - if(!_view){ - [self loadView]; - [self viewDidLoad]; - } -} - --(void)loadView{ - UIView*view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds]; - view.backgroundColor = [UIColor greenColor]; self.view = view; -} - --(void)viewDidload{ - [super viewDidload]; - self.view.backgroundColor = [UIColor brownColor]; -} -``` - - - -### 请问此时界面颜色是什么? - -可能很多人会回到绿色。其实答案是 红色 - -why?在AppDelegate中vc.view.backgroundColor就是调用vc的view的getter方法,在getter方法内部判断_view是否存在,不存在则新建一个UIView,新建view是通过 `[self loadView]` 方法创建,创建成功直接调用viewdidload方法;存在则直接返回,所以界面先是绿色,再是棕色最后是红色 - -#### 来一个官方解释 - - - - diff --git a/Chapter1 - iOS/1.30.md b/Chapter1 - iOS/1.30.md deleted file mode 100644 index 8f103a5..0000000 --- a/Chapter1 - iOS/1.30.md +++ /dev/null @@ -1,135 +0,0 @@ -# Xcode 小技巧 - -1. 快速打开:**Command+Shift+O** 。这个命令可以开启一个小窗格用来快速搜索浏览文件、类、算法以及函数等,且支持模糊搜索,这个命令可以说是日常开发中最常用的一个命令了。 - -2. 显示项目导航器:**Command+Shift+J**。使用快速打开命令跳转到对应文件后,如果需要在左侧显示出该文件在项目中的目录结构,只需要键入这个命令,非常方便 - -3. 显示编辑历史。如果一行代码写的很好或者很糟糕,不需要专门跑到 diff 工具去查看代码历史。在该行代码处右击,选择**Show Last Change For Line** - -4. 跳转到方法。在使用类或者结构时,我们经常需要快速的跳转到类的某个特定方法。通过快捷键**control+6**再输入方法的头几个字母就可以非常方便的做到这点。 - -5. 范围编辑。多光标是个很棒的并且每个高级的编辑器都该有的特训过,快捷键为**Command+Control+E**。将光标移动刀需要编辑的符号,输入快捷键,然后就可以在当前页面全局编辑了。 - -6. Xcode 设置代码只在 Debug 下起效的几种方式 - 在日常开发中 Xcode 在 Debug 模式下写很多测试代码,或者引入一些第三方测试用的 .a 和 .framework 动态库,也会通过 CocoaPods 引入一些第三方测试工具或者库;但是不希望这些库在**Release**正式包中被引入,如何做到呢? -* .h/.m 文件中的测试代码 - - Xcode 在 Debug 模式下定义了宏 DEBUG=1 ,所以我们可以在代码中把相关的测试代码写在预编译处理命令 **\#ifdef DEBUG... \#endif** 中间即可,如图所示 - - - - - -* 测试用的 .a 和 .framework - - 对于拖拽到工程中的 .a .framework 静态库,可以在 **target->Build Settings->Search Paths**这2个选项,分别设置 **Library Search Paths**和**Framework Search Paths**这2个选项。如果我们需要在测试的时候会用到,那么我们可以将 **Debug** 对应的值留下,删掉**Release** 对应的值。这样我们打包 Release 包的时候就不会包含不需要的包。 - - - -* CocoPods 引入的库 - 对于 CocoPods 方式引入的库,在配置的时候就可以处理掉,比如下面的方式 - - ``` - platform: iOS, '8.0' - ... - pod 'PonyDebugger', :configurations => ['Debug'] - ``` -7. App Store Connect 经常在上架的时候需要开发人员判断是否满足出口合规的证明,每次写都很麻烦,所以可以在工程里面的 plist 里面进行设置。 - - ``` -
-
-
-
-一言以蔽之,什么是 RunLoop?为什么 main 函数可以保持一直运行而不退出?
-
-iOS 侧 main 函数中,调用 UIApplicationMain 方法,内部启动主线程的 RunLoop,RunLoop 是一个事件循环的维护机制。有事情做的时候做事情(Source0、Source1),没有事做的时,从用户态到内核态的切换,去实现线程休眠。避免资源浪费。
-
-
-
-## RunLoop 几个重要角色
-
-### 获取 RunLoop
-
-iOS 中有2套 API 可以访问和使用 RunLoop。分别是
-
-- Foundation:NSRunLoop
-
-- CoreFoundation:CFRunLoopRef
-
-```
-//Foundation
-[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
-[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
-
-//Core Foundation
-CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
-CFRunLoopGetMain(); // 获得主线程的RunLoop对象
-```
-
-NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的内部结果,就需要了解 CFRunLoopRef
-
-- RunLoop 保存在一个全局的 Dictionary 里,线程作为 key,RunLoop 作为 value
-
-- 每条线程都有与之一一对应的 RunLoop 对象
-
-- 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建
-
-- RunLoop 在第一次获取时创建,在线程结束时消失
-
-
-
-### RunLoop 相关的5个类
-
-- CFRunLoopRef
-- CFRunLoopModeRef
-- CFRunLoopSourceRef
-- CFRunLoopTimerRef
-- CFRunLoopObserverRef
-
-CFRunLoopRef 是什么?
-
-查看源码 CFRunLoop 发现是结构体对象别名,`typedef struct __CFRunLoop * CFRunLoopRef;`摘取主要信息如下
-
-```c
-struct __CFRunLoop {
- pthread_t _pthread;
- CFMutableSetRef _commonModes;
- CFMutableSetRef _commonModeItems;
- CFRunLoopModeRef _currentMode;
- CFMutableSetRef _modes;
-};
-```
-
-其中 `_modes` 代表一个 RunLoop 有一个 set 存储运行模式,有多个 Mode。`_currentMode` 表示当前时刻只有一个 Mode。
-
-`CFRunLoopModeRef` 是什么?查看发现
-
-`typedef struct __CFRunLoopMode *CFRunLoopModeRef;`
-
-```c
-struct __CFRunLoopMode {
- CFStringRef _name;
- CFMutableSetRef _sources0;
- CFMutableSetRef _sources1;
- CFMutableArrayRef _observers;
- CFMutableArrayRef _timers;
-};
-```
-
-
-
-### CFRunLoopModeRef 代表 RunLoop 的运行模式
-
-- 一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个 Source/Timer/Observer
-- 每次 RunLoop 启动,只能指定一个 Mode,这个 Mode 被叫做 CurrentMode
-- 如果需要切换 Mode,只能退出 RunLoop,则以一个 Mode 进入
-- 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出
-
-
-
-系统默认注册了5个Mode
-
-- kCFRunLoopDefaultMode:App 的默认 Mode,通常主线程是在这个 Mode 下运行
-- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
-- UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用
-- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
-- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
-
-
-
-Demo:
-
-
-
-
-
-结论:NSRunLoop 是对 CFRunLoop 的一层包装。
-
-
-
-QA:为什么一个 RunLoop 需要创建这么多 Mode?
-
-这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。
-
-在UITableView场景下,不同的 RunLoop Mode 主要是与 UITableView 的滑动优化和事件处理相关的。当 UITableView 滑动时,RunLoop的运行模式会从默认的 CFRunLoopDefaultMode 切换到 CFRunLoopTrackingMode,此 Mode 下 RunLoop 主要关注于处理与滑动相关的触摸事件和动画效果,而忽略其他类型的事件,如定时器事件。这是因为如果同时处理所有类型的事件,可能会导致滑动不流畅,影响用户体验。之前添加到 CFRunLoopDefaultMode 上的事件通知(如定时器事件)可能无法被及时处理,这就是为什么在UITableView 滑动时,添加到主线程的 NSTimer 可能会停止执行的原因。
-
-
-
-### Source0、Source1、Timer、Observers 是什么
-
-```c
-struct __CFRunLoopMode {
- CFStringRef _name;
- CFMutableSetRef _sources0;
- CFMutableSetRef _sources1;
- CFMutableArrayRef _observers;
- CFMutableArrayRef _timers;
-};
-```
-
-RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 Source0、Source1、Timer、Observer 事件。
-
-Source0:
-
-- 处理开发者主动提交的任务或应用内部逻辑。
- - `performSelector:onThread:`
- - `dispatch_async`到主线程的任务(最终封装为Source0)。
- - 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源。UIKit控件的事件处理(如按钮点击后的回调)。
-
-
-Demo1:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
-
-
-
-Demo2: `- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait` 该 API 的底层就是:向目标线程的 RunLoop 添加一个 Source0 事件,标记后唤醒线程执行对应的事件。
-
-
-
-Source1:
-
-- 基于 Port 的线程间通信,可以主动唤醒 RunLoop
-- 用户触摸屏幕时,系统通过 Mach Port 将事件传递到应用主线程的 RunLoop。RunLoop 被 Source1 唤醒后,将事件分发给 Source0处理具体的 UI 响应逻辑(如 `hitTest:withEvent:` 和响应链)。
-- 字典。`{machport : 1}`
-
-Timers:
-
-- NSTimer
-
-- `performSelector:withObject:afterDelay:`,底层也是 Timer
-
-Observers:
-
-- 用于监听 RunLoop 状态
-
-- UI刷新(BeforeWaiting)
-
-- AutoReleasePool 实现(BeforeWaiting)
-
-CFRunLoopSourceRef 事件源(输入源)
-
-早期的分法:
-
-- Ported-Based Source
-- Custom Input Source
-- Cocoa Perform Selector Source
-
-现在的分法
-
-- Source0:非基于 port 的,用户主动触发的事件
-- Source1: 基于 port的,通过内核在线程间相互发送消息
-
-
-
-### 一对多的关系
-
-
-
-
-
-#### RunLoopTimer 的封装
-
-- `+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
-- `+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
-- `- (void)performSelector:(SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds inModes: (NSArray*)modes;`
-- `+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;`
-- `- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;`
-
-
-
-#### CFRunLoopSource
-
-Source 是 RunLoop 的数据源抽象类(protocol)
-
-RunLoop 定义了2个 Version 的 Source:
-
-- Source0:处理 App 内部事件、App 自己负责管理(触发),如 UIEvent、CGSocket
-- Source1:由 RunLoop 和内核管理,Mach port 驱动,如 CFMachPort、CFMessagePort。
-
-定义如下:
-
-```c++
-struct __CFRunLoopSource {
- CFRuntimeBase _base;
- uint32_t _bits;
- pthread_mutex_t _lock;
- CFIndex _order; /* immutable */
- CFMutableBagRef _runLoops;
- union {
- CFRunLoopSourceContext version0; /* immutable, except invalidation */
- CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
- } _context;
-};
-
-typedef struct {
- CFIndex version;
- void * info;
- const void *(*retain)(const void *info);
- void (*release)(const void *info);
- CFStringRef (*copyDescription)(const void *info);
- Boolean (*equal)(const void *info1, const void *info2);
- CFHashCode (*hash)(const void *info);
- void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
- void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
- void (*perform)(void *info);
-} CFRunLoopSourceContext;
-
-typedef struct {
- CFIndex version;
- void * info;
- const void *(*retain)(const void *info);
- void (*release)(const void *info);
- CFStringRef (*copyDescription)(const void *info);
- Boolean (*equal)(const void *info1, const void *info2);
- CFHashCode (*hash)(const void *info);
-#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
- mach_port_t (*getPort)(void *info);
- void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
-#else
- void * (*getPort)(void *info);
- void (*perform)(void *info);
-#endif
-} CFRunLoopSourceContext1;
-```
-
-
-
-#### CFRunLoopObserver
-
-向外部报告 RunLoop 当前状态的更改。框架中很多机制都是由 RunLoopObserver 触发,比如 CAAnimation、AutoReleasePool。
-
-系统或者开发者很多都是 RunLoop 的业务方。
-
-
-
-#### CFRunLoopMode
-
-Mode 是 iOS App 滑动流畅的关键。
-
-不同任务被添加到不同 Mode 中去。
-
-UITrackingMode 模式下,核心关注滚动时 UI 流畅相关逻辑。
-
-
-
-
-
-### CFRunLoopObserverRef 监听 RunLoop 状态变化
-
-```objective-c
-/* Run Loop Observer Activities */
-typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
- kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
- kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 NSTimer
- kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
- kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
- kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
- kCFRunLoopExit = (1UL << 7), // 退出 RunLoop
- kCFRunLoopAllActivities = 0x0FFFFFFFU
-};
-```
-
- 添加 Observer
-
-```objective-c
-//1、获得当前线程下的 RunLoop
-CFRunLoopRef runloop = CFRunLoopGetCurrent();
-//2、为 RunLoop 创建观察者
-CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
-});
-//3、为当前的 RunLoop 添加观察者
-CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode);
-//4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release
-CFRelease(obersver);
-```
-
-注意:CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain 等字眼的函数创建出来的对象都需要在最后调用 `release`
-
-```objective-c
-//给 RunLoop 添加监听者
-- (void) {
-
- //创建监听者
-// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
- /*
- 创建监听对象
- 参数1:分配内存空间
- 参数2:要监听的状态 kCFRunLoopAllActivities :所有状态
- 参数3:是否要持续监听
- 参数4:优先级
- 参数5:回调
- */
-
- CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
- switch (activity) {
- case kCFRunLoopEntry:
- NSLog(@"RunLoop 闪亮登场");
- break;
- case kCFRunLoopBeforeTimers:
- NSLog(@"RunLoop 大哥要处理 Timer 了");
- break;
- case kCF RunLoopBeforeSources:
- //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息
- NSLog(@"RunLoop 大哥要处理 Source 了");
- break;
- case kCFRunLoopBeforeWaiting:
- NSLog(@"RunLoop 大哥没事干要睡觉了");
- break;
- case kCFRunLoopAfterWaiting:
- NSLog(@"");
- NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
- break;
- case kCFRunLoopExit:
- NSLog(@"RunLoop 大哥要退出离开了");
- break;
- default:
- break;
- }
- });
- /*
- 参数1:要监听哪个RunLoop
- 参数2:监听者
- 参数3:要监听 RunLoop 在哪种运行模式下的状态
- */
- CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode);
- CFRelease(oberver);
- [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES];
-}
-
-
-//等到 RunLoop 休眠后,5秒钟叫醒 RunLoop
-- (void)wakeupRunLoop{
- NSLog(@"%s",__func__);
-}
-/*
-2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
-2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974]
-2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
-2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
-2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974]
-2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
-2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
-2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974]
-2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
-2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
-2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
-2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974]
-2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
-2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
-2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
-2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
-2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
-*/
-```
-
-
-
-上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
-
-```objective-c
-- (void)testRunLoopObserverOnSubThread{
-
- //创建并发队列
- dispatch_queue_t queue = dispatch_queue_create("com.lbp.testRunLoopOnSubThread", DISPATCH_QUEUE_CONCURRENT);
- //开启子线程
- dispatch_async(queue, ^{
-
- //1、获得当前线程下的 RunLoop
- CFRunLoopRef runloop = CFRunLoopGetCurrent();
-
-
- //2、为 RunLoop 创建观察者
- CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
- switch (activity) {
- case kCFRunLoopEntry:
- NSLog(@"RunLoop 闪亮登场");
- break;
- case kCFRunLoopBeforeTimers:
- NSLog(@"RunLoop 大哥要处理 Timer 了");
- break;
- case kCFRunLoopBeforeSources:
- //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息
- NSLog(@"RunLoop 大哥要处理 Source 了");
- break;
- case kCFRunLoopBeforeWaiting:
- NSLog(@"RunLoop 大哥没事干要睡觉了");
- break;
- case kCFRunLoopAfterWaiting:
- NSLog(@"");
- NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
- break;
- case kCFRunLoopExit:
- NSLog(@"RunLoop 大哥要退出离开了");
- break;
- default:
- break;
- }
- });
- //为了运行 RunLoop 必须触发事件
- [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(wakeUpRunLoopOnSubThread) userInfo:nil repeats:NO];
- //3、为当前的 RunLoop 添加观察者
- CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode);
- //4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release
- CFRelease(obersver);
- //5、在非主线程创建的 RunLoop 必须触发运行
- [[NSRunLoop currentRunLoop] run];
- });
-}
-
-
-- (void)wakeUpRunLoopOnSubThread{
- NSLog(@"%s",__func__);
-}
-/*
-2018-08-01 14:23:06.453282+0800 RunLoop[2376:115968] RunLoop 闪亮登场
-2018-08-01 14:23:06.453608+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Timer 了
-2018-08-01 14:23:06.453781+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Source 了
-2018-08-01 14:23:06.453982+0800 RunLoop[2376:115968] RunLoop 大哥没事干要睡觉了
-2018-08-01 14:23:08.458237+0800 RunLoop[2376:115968]
-2018-08-01 14:23:08.458658+0800 RunLoop[2376:115968] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
-2018-08-01 14:23:08.458894+0800 RunLoop[2376:115968] -[ViewController wakeUpRunLoopOnSubThread]
-2018-08-01 14:23:08.459082+0800 RunLoop[2376:115968] RunLoop 大哥要退出离开了
-*/
-```
-
-
-
-## RunLoop 运行原理
-
-### 运行原概要
-
-
-
-- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
- - Source0:非基于 port 的,用户主动触发的事件。
- - Source1:基于 port,通过内核和其它线程互相发送消息
-- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop)
-
-
-
-### 源码探究
-
-内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer)
-
-我们来看看苹果官方开源的 [CFRunLoop.c 文件](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。看几个关键函数的实现猜测下 RunLoop 的内部原理
-
-但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
-
-
-
-查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
-
-```c
-SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
- // ...
- // 通知 Observers 进入 RunLoop
- __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
- // RunLoop 运行循环主逻辑
- result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
- // 通知 Observers 退出 RunLoop
- __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
- return result;
-}
-```
-
-`CFRunLoopRunSpecific` 方法就是系统启动 RunLoop 的入口。内部先通知 Runloop 的观察者进入 Runloop 了,然后 调用 `__CFRunLoopRun` 执行核心逻辑(处理 timers、source 事件、block),最后告诉观察者退出 Runloop。
-
-我们继续看看 `__CFRunLoopRun` 。源码很多很乱,对无关代码进行裁剪,便于理解流程逻辑
-
-```c
-static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
- int32_t retVal = 0;
- do {
- // 通知 Obserers:即将处理 Timers
- if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
- // 通知 Obserers:即将处理 Sources
- if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
- // 处理 blocks
- __CFRunLoopDoBlocks(rl, rlm);
- // 处理 Source0
- Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
- if (sourceHandledThisLoop) {
- // 处理 blocks
- __CFRunLoopDoBlocks(rl, rlm);
- }
-
- Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
- // 判断有无 Source1
- if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
- msg = (mach_msg_header_t *)msg_buffer;
- // 如果有 Source1 则跳转到 handle_msg
- if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
- goto handle_msg;
- }
- }
-
- didDispatchPortLastTime = false;
- // 通知 Observers:即将休眠
- if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
-
- CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
-
-
- do {
- if (kCFUseCollectableAllocator) {
- // objc_clear_stack(0);
- //
-
-Demo:
-
-1. 上面第4步的 blocks 是指可以给 RunLoop 添加 Block 任务。
-
- ```objective-c
- CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{
- NSLog(@"runloop block task");
- });
- ```
-
-2. 上面8>2,Runloop 处理 GCD Async To Main Quque
-
-
-
-
-
-
-
-
-
-### RunLoop 休眠原理
-
-> Runloop 在处理完 timer、source、block 后会检查有没有 source1 事件,没有则休眠。这个休眠是 while 循环死等吗?怎么实现的?
-
-可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
-
-
-
-
-
-本质上就是函数 `__CFRunLoopServiceMachPort` 来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。
-
-**`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省资源的作用,等到由新消息来到,继续切换到用户态**。能力更底层,效果更好,从而更加省电
-
-```c
-static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
- Boolean originalBuffer = true;
- kern_return_t ret = KERN_SUCCESS;
- for (;;) { /* In that sleep of death what nightmares may come ... */
- mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
- msg->msgh_bits = 0;
- msg->msgh_local_port = port;
- msg->msgh_remote_port = MACH_PORT_NULL;
- msg->msgh_size = buffer_size;
- msg->msgh_id = 0;
- if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
- ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
-
- // Take care of all voucher-related work right after mach_msg.
- // If we don't release the previous voucher we're going to leak it.
- voucher_mach_msg_revert(*voucherState);
-
- // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
- *voucherState = voucher_mach_msg_adopt(msg);
-
- if (voucherCopy) {
- if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
- // Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy.
- // CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged.
- *voucherCopy = voucher_copy();
- } else {
- *voucherCopy = NULL;
- }
- }
-
- CFRUNLOOP_WAKEUP(ret);
- if (MACH_MSG_SUCCESS == ret) {
- *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
- return true;
- }
- if (MACH_RCV_TIMED_OUT == ret) {
- if (!originalBuffer) free(msg);
- *buffer = NULL;
- *livePort = MACH_PORT_NULL;
- return false;
- }
- if (MACH_RCV_TOO_LARGE != ret) break;
- buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
- if (originalBuffer) *buffer = NULL;
- originalBuffer = false;
- *buffer = realloc(*buffer, buffer_size);
- }
- HALT;
- return false;
-}
-```
-
-
-
-### RunLoop 是如何响应用户操作的?
-
-用户交互事件首先在 IOHID 层生成 HIDEvent,然后向事件处理线程的 Source1 的 mach port 发送 HIDEvent 消息,Source1 的回调函数将事件转化为 UIEvent 并筛选需要处理的事件推入待处理事件队列,向主线程的事件处理 Source0 发送信号,并唤醒主线程,主线程检查到事件处理 Source0 有待处理信号后,触发 Source0 的回调函数,从待处理事件队列中提取 UIEvent,最后进入 hit-test 等 UIEvent 事件响应流程
-
-等待梳理完善。
-
-
-
-## CFRunLoopTimerRef 是基于时间的触发器
-
-- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响
-- GCD 的 timer 不受 RunLoopMode 的影响
-
-## 源码解读
-
-- performSelector 是在 source0 上实现的
-- RunLoopTimer 外部接口设置的精度,精度大于0,则使用 dispatch_source_set_timer,精度小于0,则使用 mk_timer_arm
-- timer_source 使用 dispatch_timer_STRICT 创建,则系统会尽最大努力遵守设置的 leeway 值
-- NSTimer 不准的原因:底层 RunLoop Timer 底层使用的 timer 的精度不高(mk_timer);与 RunLoop 底层的调用机制有关系
-- 那么为什么存在 RunLoopTimer?意义是什么?应用场景
-
-```c
-// Data structure to hold TSD data, cleanup functions for each
-typedef struct __CFTSDTable {
- uint32_t destructorCount;
- uintptr_t data[CF_TSD_MAX_SLOTS];
- tsdDestructor destructors[CF_TSD_MAX_SLOTS];
-} __CFTSDTable;
-```
-
-```c
-// 主线程 RunLoop
-CFRunLoopRef CFRunLoopGetMain(void) {
- CHECK_FOR_FORK();
- // 局部静态变量
- static CFRunLoopRef __main = NULL; // no retain needed
- // 创建主线程对应的 RunLoop
- if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
- return __main;
-}
-
-// 子线程 RunLoop
-// 先从 __CFTSDTable 获取 RunLoop,如果有则 return,没有则调用 _CFRunLoopGet0,_CFRunLoopGet0 内部调用 __CFRunLoopCreate 创建 CFRunLoop,然后写入 __CFTSDTable
-CFRunLoopRef CFRunLoopGetCurrent(void) {
- CHECK_FOR_FORK();
- // __CFTSDTable
- CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
- if (rl) return rl;
- // 没有则创建
- return _CFRunLoopGet0(pthread_self());
-}
-
-CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) {
- if (pthread_equal(t, kNilPthreadT)) {
- t = pthread_main_thread_np();
- }
- __CFLock(&loopsLock);
- if (!__CFRunLoops) {
- CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
- CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
- CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
- if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
- CFRelease(dict);
- }
- CFRelease(mainLoop);
- }
- CFRunLoopRef newLoop = NULL;
- CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
- if (!loop) {
- newLoop = __CFRunLoopCreate(t);
- CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
- loop = newLoop;
- }
- __CFUnlock(&loopsLock);
- // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
- if (newLoop) { CFRelease(newLoop); }
-
- if (pthread_equal(t, pthread_self())) {
- _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
- if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
-#if _POSIX_THREADS
- _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
-#else
- _CFSetTSD(__CFTSDKeyRunLoopCntr, 0, &__CFFinalizeRunLoop);
-#endif
- }
- }
- return loop;
-}
-```
-
-为什么只有当 RunLoop 中存在 Timer Sourcrs、Input Sources 时,才能保证 RunLoop 不退出?
-
-RunLoop 本质就是一个有条件的 do...while 循环。__CFRunLoopModeIsEmpty 里面去判断 source0、source1、timers 不存在则 while 循环条件不满足,RunLoop 退出
-
-```c
-void CFRunLoopRun(void) { /* DOES CALLOUT */
- int32_t result;
- do {
- result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
- CHECK_FOR_FORK();
- } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
-}
-
-// expects rl and rlm locked
-static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
- // ...
- if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
- if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
- if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
- // ...
- return true;
-}
-```
-
-```c
-SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
-// ...
-CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
-if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
- Boolean did = false;
- if (currentMode) __CFRunLoopModeUnlock(currentMode);
- __CFRunLoopUnlock(rl);
- return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
-}
-// ...
-```
-
-
-
-## Mach Port 跨线程通信
-
-1. Mach IPC 基于 Mach 内核实现进程间通讯。
-
-2. Mach IPC 被抽象为3种操作:messages、ports、and port sets
-
-3. Mach port 跨线程通信
-
- 线程 B 有个 port 在等待消息,具有消息接收权限,等待消息到来的时候会阻塞当前线程
-
- 线程 A 有个 port 在发送消息,具有发送消息权限,要把发送的消息包装成消息,通过消息队列传递,message 包括:header(目的地 port、size)、data。
-
- 线程 B 收到消息后,解除 block,线程继续向下运行
-
-4. Mach port 如何进行跨线程通信?
-
- 线程开启一个 port,然后给 port 申请接收、发送的权限。mach_msg 是通信函数,在等待消息的时候不加 timeout 则会一直阻塞,等到消息到来
-
-5. 在测试工作中 main.m 文件中打印当前的 RunLoop
-
- ```objectivec
- int main(int argc, char * argv[]) {
- NSString * appDelegateClassName;
- @autoreleasepool {
- appDelegateClassName = NSStringFromClass([AppDelegate class]);
- }
- NSLog(@"%@", NSRunLoop.currentRunLoop);
- return UIApplicationMain(argc, argv, nil, appDelegateClassName);
- }
-
- //
- 2020-08-13 09:49:41.326621+0800 ***[52423:2383402]
-
-改进代码如下
-
-```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
-
-
-
-注意:
-
-- 线程的 RunLoop 结束了,线程也无法执行任务了,所以需要给线程对象设置为 nil。同时任务派发的地方也需要判断线程是否存在,否则会 crash
-
-- NSRunLoop 常驻线程可以运行在 NSRunLoopCommonModes 下吗?
-
- 不可以。因为在跑的时候如果 modeName 等于 kCFRunLoopCommonModes 则直接 kCFRunLoopRunFinished,则 RunLoop 的 while 循环条件失败
-
- ```objective-c
- SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
- CHECK_FOR_FORK();
- if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- CFLog(kCFLogLevelError, CFSTR("invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution."), modeName);
- _CFRunLoopError_RunCalledWithInvalidMode();
- });
- return kCFRunLoopRunFinished;
- }
- // ...
- return result;
- }
- ```
-
-线程保活的目的是保证线程处于激活状态,而不是使用强指针让线程不要释放。为让其处于激活状态就需要使用 RunLoop。
-
-
-
-### 线程封装
-
-思考:如何设计一个常驻线程工具类?
-
-继承自 NSThread 吗?不行,这样的话, api 不够收口,留的口子太多,不方便管控。
-
-```objectivec
-#import