This commit is contained in:
√(noham)²
2026-02-22 21:56:47 +01:00
parent ddfbd0bc29
commit 898f333ab0
737 changed files with 278 additions and 103050 deletions

BIN
.DS_Store vendored

Binary file not shown.

6
.gitignore vendored
View File

@@ -1 +1,5 @@
node_modules
node_modules
.DS_Store
prompt.txt
unused_assets.py
/t

View File

@@ -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. 打开网站在线挑选好合适的图标加入购物车,如图
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-28-iconfontPickUp.png)
1. 选择好之后在购物车查看,然后点击下载代码
2. 打开下载好的文件其机构如下我们在iOS项目开发过程中使用unicode的形式使用IconFont,所以打开demo\_unicode.html
![下载文件目录结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-28-iconfontWorkDirectory.png)
**注意:** 创建 UIFont 使用的是字体名,而不是文件名;文本值为 8 位的 Unicode 字符,我们可以打开 demo.html 查找每个图标所对应的 HTML 实体 Unicode 码,比如: "店" 对应的 HTML 实体 Unicode 码为0x3439 转换后为:\U00003439 就是将 0x 替换为 \U 中间用 0 填补满长度为 8 个字符
# Xcode中使用IconFont
初步尝试使用
1. 首先看看如何简单实用IconFont
2. 首先将下载好的文件夹中的 **iconfont.ttf** 加入到Xcode工程中确保加入成功在Build检查
![Xcode检查引入结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-28-iconfintWorkSetting.png)
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") ];
```
![封装后的工程目录结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2017-05-28-iconfont.png)
1. LBPFontInfo来封装字体信息
2. UIColor+picker根据十六进制字符串来设置颜色
3. LBPIconFont向系统中注册IconFont字体库并使用
4. UIImage+LBPIconFont封装一个使用IconFont的Image分类
# [Demo地址](https://github.com/FantasticLBP/IconFont_Demo)

View File

@@ -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&param2=value2...**
在UIWebView的delegate函数中我们判断请求的scheme如果request.URL.scheme是jsbridge那么就不进行网页内容的加载而是去执行相应的方法。方法名称就是request.URL.host。参数可以通过request.URL.query得到。
问题来了??
发起这样1个网络请求有2种方式。1:location.href .2iframe。通过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
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf8">
<script language="javascript">
function loadURL(url) {
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);
}
function receiveValue(value) {
alert("从原生拿到加法结果为:" + value);
}
function check() {
var par1 = document.getElementById("par1").value;
var par2 = document.getElementById("par2").value;
loadURL("JSBridge://plus?par1=" + par1 + "&par2=" + par2);
}
</script>
</head>
<body>
<input type="text" placeholder="请输入数字" id="par1" > +
<input type="text" placeholder="请输入数字" id="par2" >
<input type="button" value="=" onclick="check()" />
</body>
</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是同步。

View File

@@ -1,217 +0,0 @@
# iOS 端底层网络错误
> 本篇文章主要记录在 iOS 侧,一些底层网络问题的产生和解决。包括一些 socket 的疑难杂症
## 典型案例
### 1. Socket 断开后会收到 SIGPIPE 类型的信号,如果不处理会 crash
同事问了我一个问题,说收到一个 crash 信息,去 mpaas 平台看到如下的 crash 信息
![2021-04-06-NetworkFatlError.png](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-04-06-NetworkFatlError.png)
看了代码显示在某某文件的313行代码代码如下
![2021-04-06-NetworkFatlError.png](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-04-06-NetworkFatalError2.png)
Socket 属于网络最底层的实现,一般我们开发不需要用到,但是用到了就需要小心翼翼,比如 Hook 网络层、长链接等。查看官方文档会说看到一些说明。
当使用 socket 进行网络连接时,如果连接中断,在默认情况下, 进程会收到一个 `SIGPIPE` 信号。如果你没有处理这个信号app 会 crash。
Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异常机制之上构建了信号处理机制。硬件产生的信号被 Mach 层捕捉,然后转换为对应的 UNIX 信号,为了维护一个统一的机制,操作系统和用户产生的信号首先被转换为 Mach 异常,然后再转换为信号。
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。
![Mach 异常处理以及转换为 Unix 信号的流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-19-BSDCatchSignal.png)
有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<PingFoundationDelegate> strongDelegate;
// ...
// Send the packet.
if (self.socket == NULL) {
bytesSent = -1;
err = EBADF;
} else if (!CFSocketIsValid(self.socket)) {
//Returns a Boolean value that indicates whether a CFSocket object is valid and able to send or receive messages.
bytesSent = -1;
err = EPIPE;
} else {
[self ignoreSIGPIPE];
bytesSent = sendto(
CFSocketGetNative(self.socket),
packet.bytes,
packet.length,
SO_NOSIGPIPE,
self.hostAddress.bytes,
(socklen_t) self.hostAddress.length
);
err = 0;
if (bytesSent < 0) {
err = errno;
}
}
// ...
}
- (void)ignoreSIGPIPE {
int value = 1;
setsockopt(CFSocketGetNative(self.socket), SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value));
}
- (void)dealloc {
[self stop];
}
- (void)stop {
[self stopHostResolution];
[self stopSocket];
// Junk the host address on stop. If the client calls -start again, we'll
// re-resolve the host name.
self.hostAddress = NULL;
}
```
也就是说在调用 `sendto()` 的时候需要判断下,调用 `CFSocketIsValid` 判断当前通道的质量。该函数返回当前 Socket 对象是否有效且可以发送或者接收消息。之
前的判断是,当 self.socket 对象不为 NULL则直接发送消息。但是有种情况就是 Socket 对象不为空,但是通道不可用,这时候会 Crash。
> Returns a Boolean value that indicates whether a CFSocket object is valid and able to send or receive messages.
```
if (self.socket == NULL) {
bytesSent = -1;
err = EBADF;
} else {
[self ignoreSIGPIPE];
bytesSent = sendto(
CFSocketGetNative(self.socket),
packet.bytes,
packet.length,
SO_NOSIGPIPE,
self.hostAddress.bytes,
(socklen_t) self.hostAddress.length
);
err = 0;
if (bytesSent < 0) {
err = errno;
}
}
```
### 2. 设备无可用空间问题
![设备无可用空间问题](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NoSpaceLeftOnDevice.png)
最早遇到这个问题,直观的判断是某个接口所在的服务器机器,出现了存储问题(因为查了代码是网络回调存在 Error 的时候会调用我们公司基础),因为不是稳定必现,所以也就没怎么重视。直到后来发现线上有商家反馈这个问题最近经常出现。经过排查该问题该问题 `Error Domain=NSPOSIXErrorDomain Code=28 "No space left on device"` 是系统报出来的,开启 Instrucments Network 面板后看到显示 Session 过多。为了将问题复现,定时器去触发“切店”逻辑,切店则会触发首页所需的各个网络请求,则可以复现问题。工程中查找 NSURLSession 创建的代码将问题定位到某几个底层库HOOK 网络监控的能力上。一个是 APM 网络监控,确定 APMM 网路监控 Session 创建是收敛的另一个库是动态域名替换的库之前出现过线上故障。所以思考之下暂时将这个库发布热修代码。之前是采用“悲观策略”99%的概率不会出现故障然后牺牲线上每个网络的性能增加一道流程而且该流程的实现还存在问题。思考之下采用乐观策略假设线上大概率不会出现故障保留2个方法。线上出现故障马上发布热修调用下面的方法。
```
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
return NO;
}
//下面代码保留着,以防热修复使用
+ (BOOL)open_canInitWithRequest:(NSURLRequest *)request {
// 代理网络请求
}
```
问题临时解决后,后续动态域名替换的库可以参考 WeexSDK 的实现。见 [WXResourceRequestHandlerDefaultImpl.m](https://github.com/apache/incubator-weex/blob/master/ios/sdk/WeexSDK/Sources/Network/WXResourceRequestHandlerDefaultImpl.m#L37)。WeexSDK 这个代码实现考虑到了多个网络监听对象的问题、且考虑到了 Session 创建多个的问题,是一个合理解法。
```
- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id<WXResourceRequestDelegate>)delegate
{
if (!_session) {
NSURLSessionConfiguration *urlSessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
if ([WXAppConfiguration customizeProtocolClasses].count > 0) {
NSArray *defaultProtocols = urlSessionConfig.protocolClasses;
urlSessionConfig.protocolClasses = [[WXAppConfiguration customizeProtocolClasses] arrayByAddingObjectsFromArray:defaultProtocols];
}
_session = [NSURLSession sessionWithConfiguration:urlSessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
_delegates = [WXThreadSafeMutableDictionary new];
}
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
request.taskIdentifier = task;
[_delegates setObject:delegate forKey:task];
[task resume];
}
```
### NSURLProtocol 主意事项
使用 NSURLProtocol 的时候,如果是代理 NSURLSession 的网络请求,则需要重写 protocolClasses 方法。但是在你往给方法设置 protocolClasses 的时候可能全局也有其他 SDK、工具类也做了修改。这样子需要注意不能丢弃别人的也不能丢弃自己的。参考 OHHTTPStubs 在注册 NSURLProtocol 子类的处理
```
+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{
// Runtime check to make sure the API is available on this version
if ( [sessionConfig respondsToSelector:@selector(protocolClasses)]
&& [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
{
NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
Class protoCls = HTTPStubsProtocol.class;
if (enable && ![urlProtocolClasses containsObject:protoCls])
{
[urlProtocolClasses insertObject:protoCls atIndex:0];
}
else if (!enable && [urlProtocolClasses containsObject:protoCls])
{
[urlProtocolClasses removeObject:protoCls];
}
sessionConfig.protocolClasses = urlProtocolClasses;
}
else
{
NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
@"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
@"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
}
}
```

View File

@@ -1,117 +0,0 @@
# 离屏渲染
## 什么是离屏渲染
什么是在屏渲染?
在当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行的。
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的 Frame Buffer帧缓冲区作为像素数据存储区域然后由显示控制器把帧缓冲区的数据显示到屏幕上。如果因为面临一些限制比如阴影、光栅、遮罩等CPU 无法把渲染结果直接写入 Frame Buffer而是先暂时把中间的临时状态保存在额外的内存区域之后再写入 Frame Buffer那么这个过程被称为离屏渲染。
系统如果没有直接把渲染结果直接写入到 GPU FrameBuffer 中则认为发生了一次离屏渲染。离屏渲染指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
## 哪些 case 会触发离屏渲染
- 光栅化shouldRasterize
属于系统的优化机制,当开启光栅化的时候,系统会将该图片以 BitMap 的形式缓存起来缓存时间为100ms当下次需要显示这张图片的时候系统会从缓存中获取出这张图片传递给 GPU不需要 GPU 再次渲染这部分图层,达到减少 GPU 运算量的目的。
应用场景非常小因为时间仅为100ms且会触发离屏渲染。
```
self.imageView.layer.shouldRasterize = YES
```
- 遮罩mask
遮罩 Mask 相当于增加了 GPU 绘制复杂度,无法一次计算完成,需要增加一块新的帧缓冲区计算。
```
CALayer *layer = [CALayer layer];
layer.frame = // ...;
self.imageView.layer.mask = layer;
```
- 阴影(shadow)
因为阴影位于视图下层,绘制完阴影,界面还没有绘制完主要内容,所以需要一块额外的帧缓冲区中转。
```
self.imageView.layer.shadowColor = [UIColor redColor].CGColor;
self.imageView.layer.shadowOpacity = 0.2;
```
如果给阴影设置 shadowPath 则不会触发离屏幕渲染。因为 shadowPath 预先告诉 CoreAnimation 框架阴影的几何形状,因此不需要依赖 layer 本体,可以独立渲染。
- 抗锯齿(竖直图片旋转后会出现锯齿)
可能会触发离屏渲染,假如 UIImageView 控件的尺寸和图片素材的大小不一致,比如设置旋转,则会触发离屏渲染,否则不会。
抗锯齿的计算量很大,因此需要额外的帧缓冲区保存计算结果,则触发离屏渲染。
```
CGFloate angle = M_PI/60.0;
self.iamgeView.layer setTransform3DRotate(self.imageView.layer.transform, angle, 0, 0, 1);
self.iamgeView.layer.allowsEdgeAntialiasing = YES;
```
- 不透明
当 alpha 为1的时候不会触发离屏渲染。
当 alpha 不为1的时候且设置了父视图的 allowsGroupOpacity 为 YES则会触发离屏渲染。因为父视图的 allowsGroupOpacity 为 YES则代表子视图的透明度是否和父视图一样一样则需要额外的帧缓冲区计算。
```
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,0, 10, 20)];
view.backgroundColor = [UIColor redColor];
[self.imageView addSubview: view];
self.iamgeView.alpha = 1;
self.iamgeView.layer.allowsGroupOpacity = YES;
```
- 圆角
当给视图设置了背景颜色且设置了圆角,则会触发离屏渲染。
UILabel 比较特殊,给 UILabel 设置 backgroundColor 其实就是给 UILabel 的 contents 设置背景颜色contents 层级比 layer 高,所以 UILabel 整体显示为红色。
下面显示结果为:红绿
```
self.label.backgroundColor = [UIColor redColor];
self.label.layer.backgroundColor = [UIColor greenColor].CGColor;
self.label.layer.cornerRadius = YES;
self.iamgeView.backgroundColor = [UIColor redColor];
self.iamgeView.layer.backgroundColor = [UIColor greenColor].CGColor;
self.imageView.layer.cornerRadius = YES;
```
## 离屏渲染的影响?
触发离屏渲染后,会增加 GPU 的工作量CPU + GPU 工作时间可能会超过一次渲染周期,会发生 UI 卡顿。
离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么影响,数量的多少才显著影响性能。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。
上下文切换不管是在GPU渲染过程中还是广为人知的进程切换上下文切换都是一个相当耗时的操作。首先要保存当前屏幕渲染环境然后切换到一个新的绘制环境申请绘制资源初始化环境然后开始一个绘制绘制完毕后销毁这个绘制环境如需要切换到On-Screen Rendering 或者再开始一个新的离屏渲染重复之前的操作。
一次 mask 发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换一次mask需要渲染三次才能在屏幕上显示这已经是普通视图显示3陪耗时若再加上下文环境切换一次 mask 就是普通渲染的nn>3倍以上耗时操作
正常流程App Source Code -> CPU -> Frame Buffer -> Dispaly
离屏渲染流程App Source Code -> CPU -> Off Screen Frame Buffer -> Frame Buffer -> Dispaly
## 如何优化?
- 针对 shadow 可以增加 shadowPath
- 针对圆角可以增加贝塞尔曲线或者一张图片实现(类似遮罩)。
## 特殊的离屏渲染
- drawRect 是一种特殊情况,因为是依赖 Core Graphics 将绘制结果保存在 backing store 中,是 CPU 层面的操作,离屏渲染是 GPU 层面的。
1. 重写 drawRect 方法的时候系统会为该 View 创建一块内存区域,渲染结果先保存到该内存区域,最后将该内存区域的结果写入到 FrameBuffer 中,但是该现象不属于严格意义的离屏渲染。
## 画家算法
画家算法,也叫做优先填充算法,是计算机三维图形学中处理可见性问题的一种解法(三维场景投影到二维平面上)。画家算法先将场景中的多边形按照深度进行排序,然后按照由远及近的顺序进行描述,这样可以将不可见的部分覆盖,解决“可见性”问题(也就是一层层画布进行绘制,最后叠加)。
GPU 利用片元将整个图片分为一个个像素,并且并行计算了每一个像素的颜色。在同一个栅格内可能存在多个视图,根据距离眼睛的远近,存在多个不同的物体。显而易见,我们应该将最近物体的颜色作为该栅栏的颜色,后面物体的颜色应该被遮挡(如果后面物体的颜色被传递给片元着色器,这时候就是一个显示错误,比如我们打游戏的时候可以看到墙后的人)
画家算法带来2个问题。第一个问题是相互交错的物体类似于死循环无法pick出谁应该最先被渲染按照画家算法这样的情况GPU 会无从下手。所以早期的时候,设计师总是避免这样相互交错的设计。
第二个问题是过度绘制,因为画家算法总是一层层绘制,所以存在重合叠加的情况,层级较低的物体总是会被过度绘制,浪费资源。
因为 GPU 的设计是并发、无序的,所以我们期望的画家算法是不希望浪费、等待,同时为了绘制速度,所以在此基础上引入了 Depth Buffer 和 Early-Z 和深度缓冲。
画家算法有个缺点,就是当后面的图层开始渲染时,是无法回过头去处理之前的图层,这就对于一些前后依赖的图层时,无法实现,因此需要申请一块额外的帧缓冲区来完成,比如阴影、圆角。
明白了画家算法的工作原理,也就明白了为什么会发生离屏渲染。
- 离屏渲染需要创建额外的帧缓冲区
- 渲染相关的上下文对象、帧缓冲区都比较大,切换会带来性能损耗
- 内存拷贝,需要将临时帧缓冲区的内容拷贝到真正的帧缓冲区。单帧渲染都会比较耗费性能了,如果屏幕上多个视图渲染都存在离屏渲染,整个界面会发生卡顿。
## 如何检测离屏渲染
Xcode 就提供了检测功能,打开路径为: Xcode -> Debug -> View Debugging -> Rendering -> Color Offscreen-Renderer Yellow
## 引申阅读
- [实时渲染管线中的光源渲染问题](https://zhuanlan.zhihu.com/p/392748735)
- [蒙特卡罗方法](https://wiki.mbalib.com/wiki/蒙特卡罗方法)
- [抗锯齿方法](https://zhuanlan.zhihu.com/p/56385707)

View File

@@ -1,906 +0,0 @@
# LLVM
[LLVM](https://llvm.org/) 项目是模块化、可重用的编译器以及工具链技术的集合
> The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
LLVM 不是 low level virtual machine 的缩写,就是项目名称。
## 结构
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-segment.png)
LLVM 由三部分构成:
- FrontEnd前端词法分析、语法分析、语义分析、生成中间代码
- Optimizer优化器优化中间代码
- Backend后端生成目标程序机器码。比如编写好的 Swift 代码,在编译后端这一步根据在手机上运行,则生成 arm64 的代码,如果运行在 windows 平台上,则生成 x86_64 的代码。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Structure.png)
正是由于这样的设计,使得 LLVM 具备很多有点:
- 不同的前端后端使用统一的中间代码 LLVM Intermediate Representation (LLVM IR)
- 如果需要支持一种新的编程语言,那么只需要实现一个新的前端
- 如果需要支持一种新的硬件设备,那么只需要实现一个新的后端
- 优化阶段是一个通用的阶段,它针对的是统一的 LLVM IR不论是支持新的编程语言还是支持新的硬件设备都不需要对优化阶段做修改
- 相比之下GCC 的前端和后端没分得太开,前端后端耦合在了一起。所以 GCC 为了支持一门新的语言,或者为了支持一个新的目标平台,就变得特别困难
LLVM 现在被作为实现各种静态和运行时编译语言的通用基础结构GCC 家族、Java、.NET、Python、Ruby、Scheme、Haskell、D 等)
广义上来讲LLVM 说的是一种架构。狭义上来讲LLVM 强调的是偏后端部分,如下图的除了 clang 编译前端外的部分,包括优化器和编译后端,统称为 LLVM 后端。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMFullStructure.png" style="zoom:45%">
## 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),有利于调试和错误报告
- 设计清晰简单,容易理解,易于扩展增强
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-phase.png)
## 各个编译阶段
Demo
```c++
#import <stdio.h>
#define AGE 29
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
int sum = a + b + AGE;
return 0;
}
```
查看 `main.m` 的整个编译过程
```shell
clang -ccc-print-phases main.m
```
展示如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMDisplayPhases.png" style="zoom:45%">
可以看到经历了:**输入、预处理、编译、LLVM Backend、汇编、链接、绑定架构**7个阶段。
### 预处理
查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import、宏定义替换等。展示如下
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMPreProcessorPhase.png" style="zoom:35%">
### 词法分析
词法分析阶段,主要生成 Token。使用指令 `clang -fmodules -E -Xclang -dump-tokens main.m` 查看具体做了什么
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMAnalysize.png" style="zoom:35%">
### 语法分析
语法分析阶段生成语法树ASTAbstract Syntax Tree。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMASTAnalysis.png" style="zoom:35%">
对 main.m 的代码进行改造
```
#import <stdio.h>
#define AGE 29
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
int sum = a + b + AGE;
return 0;
}
void test(int a, int b) {
int c = a + b - 4;
}
```
再次查看 AST 可以加深理解
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMASTAnalysis2.png" style="zoom:35%">
其中:
- `FunctionDecl` 节点下存在2个 `ParamVarDecl` 和1个 `CompoundStmt` 也就是2个参数和1个函数体
- 函数体 `CompoundStmt` 内部存在一个变量声明 `VarDecl`
- `-`是一个操作符。
- 红色框框内的是第一层树形结构。操作符 `-` 有2个参数。首先是最下面的字面量 `IntegerLiteral` 4。另一个就是蓝色框内的运算结果
- 蓝色框内操作符 `+` 也有2个 `DeclRefExpr`
也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMASTTreeDemo.png" style="zoom:10%">
### 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` 进行转换
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMIRType1.png" style="zoom:30%">
学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp`
一些读 IR 的 tips
- 注释以分号 `;` 开头
- 全局变量以 `@` 开头
- 局部变量以 `%` 开头
- `alloca` 在当前函数栈帧中分配内存,为当前执行的函数分配内存,当该函数执行完毕时自动释放内存
- `i32`,表示整数占几位,例如 i32 就代表 32 bit4个字节的意思
- `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.
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Debug1.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Debug2.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Debug3.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Debug4.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Debug5.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Debug6.png" style="zoom:30%" />
最后就可以加断点进行 Debug 了。但为了让调试更有意义,类似 `nm -a /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LDExploreDemo-ehvvtxafpkdkubgrswvvsudzhqbb/Build/Products/Debug-iphonesimulator/LDExploreDemo.app/LDExploreDemo` 一样可以查看到更有意义的信息,可以在 Edit Scheme 面板中 `Run -> Arguments -> Arguments Passed On Launch` section 中的 **+** 点击,添加一些参数,如下图:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Debug7.png" style="zoom:30%" />
最后允许测试。注意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`
Tipsninja 如果安装失败,可以直接从 [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 进行编译。如下图所示,代表编译成功
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMComplieXcode1.png" style="zoom:20%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMComplieXcode2.png" style="zoom:20%">
#### 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) 对应的短原生指令序列时,提供高度调优过的底层代码生成支持
- OpenMPClang 中对多平台并行编程的 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`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMAddConfiguration1.png" style="zoom:20%">
- 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMAddConfiguration2.png" style="zoom:20%">
#### 配置插件
在上一步创建的 `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
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeOpenLLVMProject.png" style="zoom:30%">
选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangPluginSourceCode.png" style="zoom:20%">
初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangCompileProducts.png" style="zoom:20%">
#### 编译 clang/clang++
此步骤前需要做一步编译 Clang 的动作。Xcode 打开 LLVM 项目,选中 `ALL_BUILD` target进行编译此过程耗时较长1h+
此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCompileClang.png" style="zoom:20%">
#### Xcode 加载插件
新建一个名字叫做 ` TestLLVM` 的 Xcode 项目。要在 Xcode 中加载指定的动态库,需要修改 Build Settings 配置,操作路径为:`Build Settings -> Other C Flags`
添加:
- `-Xclang`
- `-load`
- `-Xclang`
- 动态库路径
- `-Xclang`
- `-add-plugin`
- `-Xclang`
- 插件名称
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadPlugin.png" style="zoom:20%">
#### 设置编译器
在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginError.png" style="zoom:20%">
解决方案是在 Build Setiings 中增加2项用户自定义的设置
- `CC`:对应的是自己编译的 clang 的绝对路径
- `CXX`:对应的是自己编译的 clang++ 绝对路径
如下所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSpecifyClangPath.png" style="zoom:20%">
继续编译还是会报错,报错如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginError2.png" style="zoom:20%">
解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality`` Default` 改为 `NO`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangThenBuildErrorFix.png" style="zoom:20%">
#### 编译插件,验证正确性
编译项目后,会在编译日志看到 `FANPlugin` 插件的打印信息,说明前面的配置没有问题,接下去就是继续编写 `FANPlugin.cpp` 的逻辑代码,继续验证。
Tips 由于重新修改了插件的源码,所以每次 Build 构建完 FANPlugin 之后,在 `TestLLVM` Xcode 项目中,最好每次都执行一下 Clean 操作。
编译成功,可以看到在日志中输出了我们编写的日志信息。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginTest.png" style="zoom:20%">
#### 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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface workaholic_person : NSObject
@end
NS_ASSUME_NONNULL_END
```
利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassNameViaClangAST.png" style="zoom:20%">
核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 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 <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include <iostream>
#include <string>
#include <algorithm>
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
#define CodeStyleValidateMethodDeclaration "ObjCMethodDecl"
#define CodeStyleValidatePropertyDeclaration "ObjcPropertyDecl"
#define CodeStyleValidateInterfaceDeclaration "ObjCInterfaceDecl"
namespace CodeStyleValidatePlugin {
// 自定义 handler
class CodeStyleValidateHandler : public MatchFinder::MatchCallback {
private:
CompilerInstance &ci; // 编译器实例
// 判断是否为开发者写的代码
bool isDeveloperSourceCode (string filename) {
if (filename.empty())
return false;
if(filename.find("/Applications/Xcode.app/") == 0)
return false;
return true;
}
// 判断属性是否需要用 Copy
bool isShouldUseCopyAttribute(const string typeStr) {
if (typeStr.find("NSString") != StringRef::npos ||
typeStr.find("NSArray") != StringRef::npos ||
typeStr.find("NSDictionary") != StringRef::npos
) {
return true;
}
return false;
}
// 检测类名
void validateInterfaceDeclaration(const ObjCInterfaceDecl *decl) {
StringRef className = decl->getName();
// 判断首字母不能以小写开头
char c = className[0];
if (isLowercase(c)) {
std::string tempName = decl->getNameAsString();
tempName[0] = toUppercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(static_cast<int32_t>(className.size() - 1));
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告警告
SourceLocation location = decl->getLocation();
showWaringReport(location, "☠️ 杭城小刘提示你Class 名不能以小写字母开头 ⚠️", &fixItHint);
}
// 判断下划线不能在类名有没有包含下划线
size_t pos = decl->getName().find('_');
if (pos != StringRef::npos) {
std::string tempName = decl->getNameAsString();
std::string::iterator end_pos = std::remove(tempName.begin(), tempName.end(), '_');
tempName.erase(end_pos, tempName.end());
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(static_cast<int32_t>(className.size() - 1));
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告警告
SourceLocation loc = decl->getLocation().getLocWithOffset(static_cast<int32_t>(pos));
showWaringReport(loc, "☠️ 杭城小刘提示你Class 名中不能带有下划线 ⚠️", &fixItHint);
}
}
// 检测属性
void validatePropertyDeclaration(const clang::ObjCPropertyDecl *propertyDecl) {
StringRef name = propertyDecl -> getName();
// 名称必须以小写字母开头
bool checkUppercaseNameIndex = 0;
if (name.find('_') == 0) {
// 以下划线开头则首字母位置变为1
checkUppercaseNameIndex = 1;
}
char c = name[checkUppercaseNameIndex];
if (isUppercase(c)) {
// 修正提示
std::string tempName = name.str();
tempName[checkUppercaseNameIndex] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = propertyDecl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(static_cast<int32_t>(name.size() - 1));
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
SourceLocation location = propertyDecl->getLocation();
// 报告警告
showWaringReport(location, "☠️ 杭城小刘提示你:@property 名称必须以小写字母开头 ⚠️", &fixItHint);
}
// 检测属性
if (propertyDecl->getTypeSourceInfo()) {
ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
SourceLocation location = propertyDecl->getLocation();
string typeStr = propertyDecl->getType().getAsString();
string propertyName = propertyDecl->getNameAsString();
// 判断 Property 需要使用 copy
if (isShouldUseCopyAttribute(typeStr) && !(attrKind & ObjCPropertyAttribute::Kind::kind_copy)) {
showWaringReport(location, "☠️ 杭城小刘提示你:建议使用 copy 代替 strong ⚠️", NULL);
}
// 判断int需要使用NSInteger
if(!typeStr.compare("int")){
showWaringReport(location, "☠️ 杭城小刘提示你:建议使用 NSInteger 替换 int ⚠️", NULL);
}
// 判断delegat使用weak
if ((typeStr.find("<")!=string::npos && typeStr.find(">")!=string::npos) && (typeStr.find("Array")==string::npos) && !(attrKind & ObjCPropertyAttribute::Kind::kind_weak)) {
showErrorReport(location, "☠️ 杭城小刘提示你:建议使用 weak 定义 Delegate ⚠️", NULL);
}
}
}
// 检测方法
void validateMethodDeclaration(string fileName, const clang::ObjCMethodDecl *methodDecl) {
// 检查名称的每部分,都不允许以大写字母开头
Selector sel = methodDecl -> getSelector();
int selectorPartCount = methodDecl -> getNumSelectorLocs();
for (int i = 0; i < selectorPartCount; i++) {
StringRef selName = sel.getNameForSlot(i);
char c = selName[0];
if (isUppercase(c)) {
// 修正提示
std::string tempName = selName.str();
tempName[0] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = methodDecl -> getSelectorLoc(i);
SourceLocation nameEnd = nameStart.getLocWithOffset(static_cast<int32_t>(selName.size() - 1));
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
// 报告警告
SourceLocation location = methodDecl->getLocation();
showWaringReport(location, "☠️ 杭城小刘提示你:方法名要以小写开头 ⚠️", &fixItHint);
}
}
// 检测方法中定义的参数名称是否存在大写开头
for (ObjCMethodDecl::param_const_iterator it = methodDecl->param_begin(); it != methodDecl->param_end(); it++) {
const ParmVarDecl *parmVarDecl = *it;
StringRef name = parmVarDecl -> getName();
char c = name[0];
if (isUppercase(c)) {
// 修正提示
std::string tempName = name.str();
tempName[0] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = parmVarDecl -> getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(static_cast<int32_t>(name.size() - 1));
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告警告
SourceLocation location = methodDecl->getLocation();
showWaringReport(location, "☠️ 杭城小刘提示你:参数名称要小写开头 ⚠️", &fixItHint);
}
}
}
template <unsigned N>
/// 抛出警告
/// @param Loc 位置
/// @param Hint 修改提示
void showWaringReport(SourceLocation Loc, const char (&FormatString)[N], FixItHint *Hint) {
DiagnosticsEngine &diagEngine = ci.getDiagnostics();
unsigned DiagID = diagEngine.getCustomDiagID(clang::DiagnosticsEngine::Warning, FormatString);
(Hint!=NULL) ? diagEngine.Report(Loc, DiagID) << *Hint : diagEngine.Report(Loc, DiagID);
}
template <unsigned N>
/// 抛出错误
/// @param Loc 位置
/// @param Hint 修改提示
void showErrorReport(SourceLocation Loc, const char (&FormatString)[N], FixItHint *Hint) {
DiagnosticsEngine &diagEngine = ci.getDiagnostics();
unsigned DiagID = diagEngine.getCustomDiagID(clang::DiagnosticsEngine::Error, FormatString);
(Hint!=NULL) ? diagEngine.Report(Loc, DiagID) << *Hint : diagEngine.Report(Loc, DiagID);
}
public:
CodeStyleValidateHandler(CompilerInstance &ci) :ci(ci) {}
// 主要方法,分配 类、方法、属性 做不同处理
void run(const MatchFinder::MatchResult &Result) override {
if (const ObjCInterfaceDecl *interfaceDecl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>(CodeStyleValidateInterfaceDeclaration)) {
string filename = ci.getSourceManager().getFilename(interfaceDecl->getSourceRange().getBegin()).str();
if(isDeveloperSourceCode(filename)){
// 类的检测
validateInterfaceDeclaration(interfaceDecl);
}
}
if (const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>(CodeStyleValidatePropertyDeclaration)) {
string filename = ci.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
if(isDeveloperSourceCode(filename)) {
// 属性的检测
validatePropertyDeclaration(propertyDecl);
}
}
if (const ObjCMethodDecl *methodDecl = Result.Nodes.getNodeAs<ObjCMethodDecl>(CodeStyleValidateMethodDeclaration)) {
string filename = ci.getSourceManager().getFilename(methodDecl->getSourceRange().getBegin()).str();
if(isDeveloperSourceCode(filename)) {
// 方法的检测
validateMethodDeclaration(filename, methodDecl);
}
}
}
};
// 自定义的处理工具
class CodeStyleValidateASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
CodeStyleValidateHandler handler;
public:
//调用 CreateASTConsumer 方法后就会加载 Consumer 里面的方法
CodeStyleValidateASTConsumer(CompilerInstance &ci) :handler(ci) {
matcher.addMatcher(objcInterfaceDecl().bind(CodeStyleValidateInterfaceDeclaration), &handler);
matcher.addMatcher(objcMethodDecl().bind(CodeStyleValidateMethodDeclaration), &handler);
matcher.addMatcher(objcPropertyDecl().bind(CodeStyleValidatePropertyDeclaration), &handler);
}
// 遍历完一次语法树就会调用一次下面方法。该方法通常被用来处理整个翻译单元的 AST进行进一步的分析、处理或者其他操作。在处理完整个 AST 后,开发者可以在这个方法中执行他们需要的操作,比如生成代码、执行静态分析、进行重构等。
void HandleTranslationUnit(ASTContext &context) override {
matcher.matchAST(context);
}
};
// 入口,解析 AST 后的动作
class ValidateCodeStyleAction: public PluginASTAction {
std::set<std::string> ParsedTemplates;
public:
// 需要返回一个 Consumer所以继续创建一个继承自 ASTConsumer 的 Consumer
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) override {
return unique_ptr<CodeStyleValidateASTConsumer> (new CodeStyleValidateASTConsumer(ci)); // 使用自定义的处理工具
}
bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) override {
return true;
}
};
}
// 注册插件,告诉 LLVM 插件对应的 Action 是 FANAction
static FrontendPluginRegistry::Add<CodeStyleValidatePlugin::ValidateCodeStyleAction>
X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles, powered by @FantasticLBP");
```
效果如下:
- 可以对类名检测,如果带下划线,则报错提示并给出修改意见
- 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见
- 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错遇到1个则终止编译请注意该区别按需编写自己的插件逻辑。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
#### 有没有其他方式?
利用 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, its 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 中的 所有方法,再判断方法是否同名
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangASTCategoryMethod.png" style="zoom:20%">
### 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]`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMGenerateSuperDealloc.png" style="zoom:30%" />
- 为每个拥有 ivar 的 Class 合成` .cxx_destructor` 方法来自动释放类的成员变量,代替 MRC 时代的 `self.xxx = nil`

View File

@@ -1 +0,0 @@
# 设计模式及其场景

View File

@@ -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 <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
@end
```
通过官方 API 可以窥探得到,系统内部应该是维护了 NSNotificationName、Observer、selector、object 之间的关系。
直接上 GUNStep 源码探索下
### Observation
```c
typedef struct Obs {
id observer; /* Object to receive message. */
SEL selector; /* Method selector. */
struct Obs *next; /* Next item in linked list. */
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
```
结构体存储了 observer、selector 信息。此外可以看出是一个链表结构next指向注册了同一个通知的下一个观察者。
### NCTbl
```c
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
// ...
} NCTable;
```
查看 NCTbl 结构体定义发现其内部存在2张 MapTable。
- named用于保存添加观察者的时候传入 NotificationName 的情况
- nemeless同于保存添加观察者时没有传递 NotificationName 的情况
### named Table
该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系object 为 keyobserver 为 value。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/notification-namedTable.png)
- 第一个 MapTable key 为 notificationNamevalue 为另一个 MapTable子 Table
- 子 Table 以 object 为 keyvalue 为链表,存储所有的观察者
- object 为 nil 的时候,系统会根据 nil 自动生成一个 key相当于这个 key 对应的值链表保存的就是当前通知传入了 NotificationName 且没有 object 的所有观察者。
### nameless Table
nameless Table 结构较为简单,因为没有 notificationName所以就一层 MapTable。key 为 objectvalue 为链表,存储所有的观察者。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Notification-namelessTable.png)
### wildcard
wildcard 也是链表结构。如果在添加 Observer 的时候没有传递 notificationName、object 则会将 Observer 添加到 wildcard 链表中,注册到这里的观察者可以响应所有的系统通知。
### 添加通知
添加通知的所有 API最终都会调用该 API。
```c
- (void)addObserver:(id)observer
selector:(SEL)selector
name:(NSString *)name
object:(id)object {
Observation *list;
Observation *o;
GSIMapTable m;
GSIMapNode n;
// 参数合法性校验
if (observer == nil)
[NSException raise:NSInvalidArgumentException
format:@"Nil observer passed to addObserver ..."];
if (selector == 0)
[NSException raise:NSInvalidArgumentException
format:@"Null selector passed to addObserver ..."];
if ([observer respondsToSelector:selector] == NO)
{
[NSException raise:NSInvalidArgumentException
format:@"[%@-%@] Observer '%@' does not respond to selector '%@'",
NSStringFromClass([self class]), NSStringFromSelector(_cmd),
observer, NSStringFromSelector(selector)];
}
lockNCTable(TABLE);
// 调用 obsNew 方法,创建 observation 对象,持有 SEL、object
o = obsNew(TABLE, selector, observer);
/*
* Record the Observation in one of the linked lists.
*
* NB. It is possible to register an observer for a notification more than
* once - in which case, the observer will receive multiple messages when
* the notification is posted... odd, but the MacOS-X docs specify this.
*/
// notificationName 存在的逻辑
if (name) {
/*
* Locate the map table for this name - create it if not present.
*/
// NAMED 是一个宏定义,表示名为 named 的字典key 为 name从 named 表中获取对应的 mapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
// 不存在,则创建
if (n == 0) {
m = mapNew(TABLE); // 先从缓存中取,如果没有则新建 mapTable
/*
* As this is the first observation for the given name, we take a
* copy of the name so it cannot be mutated while in the map.
*/
name = [name copyWithZone:NSDefaultMallocZone()];
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void *)m);
GS_CONSUMED(name)
} else {
// 存在 mapTable 则取出值也就是 named MapTable
m = (GSIMapTable)n->value.ptr;
}
/*
* Add the observation to the list for the correct object.
*/
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
// 从 named MapTable 中以 key 为 object 获取存储着观察者的链表对象。不存在则创建
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
} else {
list = (Observation *)n->value.ptr;
o->next = list->next;
list->next = o;
}
} else if (object) {
// 走到这里代表 name 为空,但 object 不为空。此时从 nameless MapTable 中以 object 获取链表对象值。
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 不存在则创建新的链表,并存入 nameless MapTable
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
} else {
// 存在,则将新的观察者添加到链表
list = (Observation *)n->value.ptr;
o->next = list->next;
list->next = o;
}
} else {
// name、object 都为空,则将添加的 observation 观察者添加到 WILDCARD 链表
o->next = WILDCARD;
WILDCARD = o;
}
unlockNCTable(TABLE);
}
static Observation *obsNew(NCTable *t, SEL s, id o)
{
Observation *obs;
if (t->freeList == 0)
{
Observation *block;
if (t->chunkIndex == CHUNKSIZE)
{
unsigned size;
t->numChunks++;
size = t->numChunks * sizeof(Observation *);
t->chunks = (Observation **)NSReallocateCollectable(
t->chunks, size, NSScannedOption);
size = CHUNKSIZE * sizeof(Observation);
t->chunks[t->numChunks - 1] = (Observation *)NSAllocateCollectable(size, 0);
t->chunkIndex = 0;
}
block = t->chunks[t->numChunks - 1];
t->freeList = &block[t->chunkIndex];
t->chunkIndex++;
t->freeList->link = 0;
}
obs = t->freeList;
t->freeList = (Observation *)obs->link;
obs->link = (void *)t;
obs->retained = 0;
obs->next = 0;
// 持有 observer 和 selector
obs->selector = s;
obs->observer = o;
return obs;
}
```
通过源码,我们可以发现和设想差不多,得出以下结论:
- 在调用 `(void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;`方法后,系统内部会调用 `obsNew` 方法,创建 Observation 对象(内部结构是一个结构体),内部保存了观察者 Observer、接收通知时会执行的方法 selector
- 如果传递了 notificationName 则会去 Named MapTable 中,以 notificationName 为 key查找对应的 value子 MapTable。如果不存在则创建一个新的 MapTable
- 在子 MapTable 中,以 object 为 key去取对应的 Observation 链表。如果没有 object 则会生成一个默认的 key表示所有的通知都会被监听
- 通过 object 生成的 key 查找 Observation 链表,如果不存在,则创建一个节点,并作为头节点。如果有链表,则将 Observation 插入尾部(链表的基础操作)
- 如果 notificationName 不存在,且 object 不为空,则通过 object 生成的 key查找 Observation 链表,如果没有则创建一个节点,且作为头节点,如果有链表,则插入到尾部
- 如果 notificationName、object 都为空,则直接把创建的 Observation 对象存储在 wildcard 链表结构中
### 发送通知
postNotification 相关的 API 最终都会调用到 `_postAndRelease` 方法,源码如下
```c
- (void) _postAndRelease: (NSNotification*)notification {
Observation *o;
unsigned count;
NSString *name = [notification name];
id object;
GSIMapNode n;
GSIMapTable m;
GSIArrayItem i[64];
GSIArray_t b;
GSIArray a = &b;
// 参数合法性判断
if (name == nil) {
RELEASE(notification);
[NSException raise: NSInvalidArgumentException
format: @"Tried to post a notification with no name."];
}
object = [notification object];
// 创建 Array 来存储 Observation
GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);
lockNCTable(TABLE);
// 遍历 WILDCARD 链表,将其中的 Observation 对象都添加到 Array 中的 既没有 notificationName又没有 object
for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next) {
GSIArrayAddItem(a, (GSIArrayItem)o);
}
// 拿到 object在 nameless 表中,以 object 为 key查找对应的链表
if (object) {
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 链表存在,则遍历链表,将 Observation 对象添加到 Array 中
if (n != 0){
o = purgeCollectedFromMapNode(NAMELESS, n);
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
// 如果 NotificationName 存在
if (name) {
//则以 NotificationName 为 key在 Named MapTable 中寻找对应的子 MapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
if (n) {
m = (GSIMapTable)n->value.ptr;
} else {
m = 0;
}
if (m != 0) {
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
// 当子 MapTable 存在的时候,以 object 为 key获取对应的链表
if (n != 0) {
o = purgeCollectedFromMapNode(m, n);
// 当链表存在的时候,遍历链表,将其中的 Observation 对象添加到数组
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
if (object != nil) {
// 以 nil 为 object key查找对应的 Observation添加到数组中
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
if (n != 0) {
o = purgeCollectedFromMapNode(m, n);
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
}
}
/* Finished with the table ... we can unlock it,
*/
unlockNCTable(TABLE);
// 最后遍历数组,调用 Observation 结构体中的 selector 方法。
count = GSIArrayCount(a);
while (count-- > 0) {
o = GSIArrayItemAtIndex(a, count).ext;
if (o->next != 0) {
NS_DURING
{
[o->observer performSelector: o->selector
withObject: notification];
}
NS_HANDLER
{
BOOL logged;
/* Try to report the notification along with the exception,
* but if there's a problem with the notification itself,
* we just log the exception.
*/
NS_DURING
NSLog(@"Problem posting %@: %@", notification, localException);
logged = YES;
NS_HANDLER
logged = NO;
NS_ENDHANDLER
if (NO == logged){
NSLog(@"Problem posting notification: %@", localException);
}
}
NS_ENDHANDLER
}
}
lockNCTable(TABLE);
GSIArrayEmpty(a);
unlockNCTable(TABLE);
// 释放 NSNotification 对象
RELEASE(notification);
}
```
通过源码分析,得出以下结论:
- 调用 `-(void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject` 的时候,内部会生成一个 Array 来存储 Observation 信息
- 先将 WILDCARD 中的所有 Observation 添加到 Array 中去通过分析发送通知源码知道WILDCARD 中 Observation 都是没有 Object、NotificationName 的,也就是可以接收所有的通知
- 然后在 NAMELESS MapTable 中,以 Object 为 key获取对应的链表遍历链表将其中的 Observation 添加到数组中
- 在 NAMED MapTable 中,先以 NotificationName 为 key获取对应的子 MapTable
- 在子 MapTable 中根据 object 为 key获取对应的链表。遍历链表将其中的 Observation 对象添加到数组
- 如果 object 不为 nil则以 nil 为 object key查找对应的 Observation添加到 Array 中
- 最后遍历 Array调用 Observation 结构体成员变量 observer 的 selector 方法。以 `performSelector: withObject:` 形式调用的,所以可以看出都是在同一个线程同步执行的
- 释放 NSNootification 对象
### 移除通知
```c
// 遍历 Observation 链表,判断节点的 observer 等于传递来的参数,则删除该节点
static Observation *listPurge(Observation *list, id observer)
{
Observation *tmp;
while (list != ENDOBS && list->observer == observer)
{
tmp = list->next;
list->next = 0;
obsFree(list);
list = tmp;
}
if (list != ENDOBS)
{
tmp = list;
while (tmp->next != ENDOBS)
{
if (tmp->next->observer == observer)
{
Observation *next = tmp->next;
tmp->next = next->next;
next->next = 0;
obsFree(next);
}
else
{
tmp = tmp->next;
}
}
}
return list;
}
- (void) removeObserver: (id)observer
name: (NSString*)name
object: (id)object{
// 参数合法性校验
if (name == nil && object == nil && observer == nil)
return;
/*
* NB. The removal algorithm depends on an implementation characteristic
* of our map tables - while enumerating a table, it is safe to remove
* the entry returned by the enumerator.
*/
lockNCTable(TABLE);
// NotificationName、object 都为 nil则说明被加入到了 WILDCARD 链表中,调用 listPurge 方法,遍历链表,删除节点中 observer 等于换入参数的节点
if (name == nil && object == nil)
{
WILDCARD = listPurge(WILDCARD, observer);
}
// NotficationName 为空
if (name == nil)
{
GSIMapEnumerator_t e0;
GSIMapNode n0;
/*
* First try removing all named items set for this object.
*/
e0 = GSIMapEnumeratorForMap(NAMED);
n0 = GSIMapEnumeratorNextNode(&e0);
// 现在 NAMED MapTable 中遍历所有子 MapTable
while (n0 != 0)
{
GSIMapTable m = (GSIMapTable)n0->value.ptr;
NSString *thisName = (NSString*)n0->key.obj;
n0 = GSIMapEnumeratorNextNode(&e0);
// object 为空,则遍历 MapTable 中的节点
if (object == nil)
{
GSIMapEnumerator_t e1 = GSIMapEnumeratorForMap(m);
GSIMapNode n1 = GSIMapEnumeratorNextNode(&e1);
/*
* Nil object and nil name, so we step through all the maps
* keyed under the current name and remove all the objects
* that match the observer.
*/
while (n1 != 0)
{
GSIMapNode next = GSIMapEnumeratorNextNode(&e1);
// 清空与 observer 相同的节点
purgeMapNode(m, n1, observer);
n1 = next;
}
}
else
// 如果 object 不为空
{
GSIMapNode n1;
// 则以 object 为 key在子 MapTable 中找到链表,清空链表中所有与 observer 相同的节点
n1 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n1 != 0)
{
purgeMapNode(m, n1, observer);
}
}
/*
* If we removed all the observations keyed under this name, we
* must remove the map table too.
*/
if (m->nodeCount == 0)
{
mapFree(TABLE, m);
GSIMapRemoveKey(NAMED, (GSIMapKey)(id)thisName);
}
}
// 处理 NAMELESS MapTable
if (object == nil)
{
e0 = GSIMapEnumeratorForMap(NAMELESS);
n0 = GSIMapEnumeratorNextNode(&e0);
// object 为空,则遍历链表,删除与 observer 相同的节点
while (n0 != 0)
{
GSIMapNode next = GSIMapEnumeratorNextNode(&e0);
purgeMapNode(NAMELESS, n0, observer);
n0 = next;
}
}
else
{
// 如果 object 不为空,则根据 object 从 NAMELESS MapTable 中以 object 为 key找到对应的链表
n0 = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n0 != 0)
{
// 删除与 Observer 相同的节点
purgeMapNode(NAMELESS, n0, observer);
}
}
}
else
// NotificationName 不为空美好
{
GSIMapTable m;
GSIMapEnumerator_t e0;
GSIMapNode n0;
// 则从 NAMED MapTable 中以 name 为 key获取对应的子 MapTable
n0 = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
if (n0 == 0)
{
unlockNCTable(TABLE);
return; /* Nothing to do. */
}
m = (GSIMapTable)n0->value.ptr;
// object 为空,则在子 MapTable 中删除节点(节点值与 observer 相同)
if (object == nil)
{
e0 = GSIMapEnumeratorForMap(m);
n0 = GSIMapEnumeratorNextNode(&e0);
while (n0 != 0)
{
GSIMapNode next = GSIMapEnumeratorNextNode(&e0);
purgeMapNode(m, n0, observer);
n0 = next;
}
}
else
{
// 如果 object 不为空,则以 object 为 key取出对应的链表然后将链表中与 observer 相同的节点删除
n0 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n0 != 0)
{
purgeMapNode(m, n0, observer);
}
}
if (m->nodeCount == 0)
{
mapFree(TABLE, m);
GSIMapRemoveKey(NAMED, (GSIMapKey)((id)name));
}
}
unlockNCTable(TABLE);
}
```
查看源码,得出以下结论:
- 调用删除通知观察者最后都会收敛到该API `(void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;`
- 内部会先判断 NotificationName、object 如果都是 nil则移除 WILDCARD 链表中与 observer 相同的节点
- 如果 NotificationName 为 nil
- 先在 NAMED MapTable 中遍历子 MapTable
- 如果 object 为 nil则遍历子 MapTable且删除与 observer 相同的节点
- object 不为 nil则以 object 为 key获取子 MapTable 中对应的链表,然后清空链表中与 observer 相同的节点
- 再处理 NAMLESS MapTable
- 如果 object 为 nil则直接遍历并删除 table 中所有与 observer 相同的节点
- 如果 object 不为 nil则以 object 为 key获取对应的链表遍历并删除 table 中所有与 observer 相同的节点
- 如果 NotificationName 不为 nil则在 NAMED MapTable 中以 NotificationName 为 key获取链表
- 如果 object 为 nil则遍历 MapTable 中所有的节点,清空与 observer 相同的节点
- 如果 object 不为 nil则以 object 为 key获取子 MapTable 中对应的链表,然后清空链表中与 observer 相同的节点
## 如何异步发送通知
这个 case 就需引入 `NSNotificationQueue` 也就是通知队列了。
简单来说 NSNotificationQueue 是 NSNotificationCenter 的缓冲池。当我们调用 `-(void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject` 的这种方式的时候就是同步发送通知。通知会直接发送到 NSNotificationCenter然后 NSNotificationCenter 会直接将其发送给注册了该通知的观察者。
使用 NSNotificationQueue ,则通知不是直接发送给 NSNotificationCenter而是先存储在 NSNotificationQueue 中,然后再由 notification 转发给注册的观察者。
且可以实现合并相同 NSNotificationName 的通知功。NSNotificationQueue 遵循队列先进先出的特性FIFO当一个通知处于对头的时候它会被发送给 NSNotificationCenter然后 NSNotificationCenter 再将该 notiication 转发给注册了该通知的所以监听者。
每一个线程都有一个默认的 NSNotificationQueue该队列与通知中心联系在一起。也可以为一个线程创建多个 NSNotificationQueue。
其所有 API
```objectivec
@interface NSNotificationQueue : NSObject {
@private
id _notificationCenter;
id _asapQueue;
id _asapObs;
id _idleQueue;
id _idleObs;
}
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
// 创建
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
// 添加观察者(入队)
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
// 移除观察者(出队)
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
@end
```
其中 postingStyle 是枚举参数
```objectivec
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1,
NSPostASAP = 2,
NSPostNow = 3
};
```
- NSPostWhenIdle代表在空闲时发送 notification 到 NSNotificationCenter。也就是本线程 RunLoop 空闲时即发送通知到通知中心
- NSPostASAPas soon as possible尽可能快。即当前通知或者 timer 回调执行结束就发送通知到通知中心,还是需要依赖 RunLoop
- NSPostNow马上发送
postingStyle 也是枚举
```objectivec
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0,
NSNotificationCoalescingOnName = 1,
NSNotificationCoalescingOnSender = 2
};
```
- NSNotificationNoCoalescing不管是否 NSNotificationName 是否重复,都不合并
- NSNotificationCoalescingOnNameNSNotificationName 相同的多个 NSNotification 会被合并为一个。
- NSNotificationCoalescingOnSender按照发送方如果多个通知发送方相同则保留一个
测试异步发送通知
```objectivec
- (void)mockNotificationQueue
{
//每个进程默认有一个通知队列,默认是没有开启的,底层通过队列实现,队列维护一个调度表
NSNotification *notifi = [NSNotification notificationWithName:@"Notification" object:nil];
NSNotificationQueue *queue = [NSNotificationQueue defaultQueue];
//FIFO
NSLog(@"notifi before");
[queue enqueueNotification:notifi postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, nil]];
NSLog(@"notifi after");
NSPort *port = [[NSPort alloc] init];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
NSLog(@"runloop over");
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotifi:) name:@"Notification" object:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self mockNotificationQueue];
}
- (void)handleNotifi:(NSNotification *)notification
{
NSLog(@"%@", [NSThread currentThread]);
}
// NSPostWhenIdle、NSPostASAP
2022-05-07 01:18:01.859643+0800 DDD[62783:2383065] notifi before
2022-05-07 01:18:01.859924+0800 DDD[62783:2383065] notifi after
2022-05-07 01:18:01.860887+0800 DDD[62783:2383065] <_NSMainThread: 0x600003530840>{number = 1, name = main}
2022-05-07 01:18:02.005840+0800 DDD[62783:2383065] notifi before
2022-05-07 01:18:02.006072+0800 DDD[62783:2383065] notifi after
2022-05-07 01:18:02.006882+0800 DDD[62783:2383065] <_NSMainThread: 0x600003530840>{number = 1, name = main}
// NSPostNow
2022-05-07 01:35:21.387512+0800 DDD[63186:2401325] notifi before
2022-05-07 01:35:21.387748+0800 DDD[63186:2401325] <_NSMainThread: 0x60000315c880>{number = 1, name = main}
2022-05-07 01:35:21.387917+0800 DDD[63186:2401325] notifi after
2022-05-07 01:35:21.532892+0800 DDD[63186:2401325] notifi before
2022-05-07 01:35:21.533130+0800 DDD[63186:2401325] <_NSMainThread: 0x60000315c880>{number = 1, name = main}
2022-05-07 01:35:21.533292+0800 DDD[63186:2401325] notifi after
```
改变参数发现:
NSPostWhenIdle异步
NSPostASAP 异步
NSPostNow 同步
所以要实现异步发送通知,则必须使用 NSNotificationQueue 相关 API`postingStyle` 参数必须为 NSPostASAP、NSPostWhenIdle不能为 NSPostNow、不能为 NSPostNow、不能为 NSPostNow。
## 通知和 RunLoop 的关系
```objectivec
- (void)notifiWithRunloop
{
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if(activity == kCFRunLoopEntry){
NSLog(@"进入 Runloop");
}else if(activity == kCFRunLoopBeforeWaiting){
NSLog(@"即将进入等待状态");
}else if(activity == kCFRunLoopAfterWaiting){
NSLog(@"结束等待状态");
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
NSNotification *notification1 = [NSNotification notificationWithName:@"notify" object:nil userInfo:@{@"key":@"1"}];
NSNotification *notification2 = [NSNotification notificationWithName:@"notify" object:nil userInfo:@{@"key":@"2"}];
NSNotification *notification3 = [NSNotification notificationWithName:@"notify" object:nil userInfo:@{@"key":@"3"}];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification1 postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification2 postingStyle:NSPostASAP coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification3 postingStyle:NSPostNow coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
NSPort *port = [[NSPort alloc] init];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotifi:) name:@"notify" object:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self notifiWithRunloop];
}
- (void)handleNotifi:(NSNotification *)notification
{
NSLog(@"%@", notification.userInfo);
}
2022-05-07 01:43:51.470047+0800 DDD[63370:2409134] {
key = 3;
}
2022-05-07 01:43:51.470312+0800 DDD[63370:2409134] Runloop
2022-05-07 01:43:51.470522+0800 DDD[63370:2409134] {
key = 2;
}
2022-05-07 01:43:51.470962+0800 DDD[63370:2409134] Runloop
2022-05-07 01:43:51.471223+0800 DDD[63370:2409134]
2022-05-07 01:43:51.471422+0800 DDD[63370:2409134] {
key = 1;
}
2022-05-07 01:43:51.471613+0800 DDD[63370:2409134]
2022-05-07 01:43:51.471759+0800 DDD[63370:2409134]
2022-05-07 01:43:51.479267+0800 DDD[63370:2409134]
2022-05-07 01:43:51.480009+0800 DDD[63370:2409134] Runloop
2022-05-07 01:43:51.480172+0800 DDD[63370:2409134]
2022-05-07 01:43:51.842003+0800 DDD[63370:2409134]
2022-05-07 01:43:51.842938+0800 DDD[63370:2409134]
2022-05-07 01:44:33.109154+0800 DDD[63370:2409134]
```
通过 Demo 和对应的打印可以看出NSNotificationQueue 相关的 API 和 RunLoop 有关系,当 `postingStyle` 参数为 NSPostNow 的时候则说明通知没有进入 RunLoop而是直接立即执行。参数为 NSPostASAP、NSPostWhenIdle 的时候都和 RunLoop 有关NSPostASAP 通知快于 NSPostWhenIdle。
## 通知重定向
```objectivec
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"dispatch thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TESTNOTIFICATION" object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"TESTNOTIFICATION" object:nil userInfo:nil];
});
}
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"receive thread = %@", [NSThread currentThread]);
}
2022-05-07 01:55:21.042542+0800 DDD[63607:2419849] dispatch thread = <_NSMainThread: 0x600001b44840>{number = 1, name = main}
2022-05-07 01:55:21.042835+0800 DDD[63607:2419937] receive thread = <NSThread: 0x600001b1c9c0>{number = 5, name = (null)}
```
虽然我们在主线程中注册了通知的观察者,但在全局队列中 postNotification 并不是在主线程处理的。如果我们想在回调中处理与 UI 相关的操作,需要确保是在主线程中执行回调。
为什么不直接在处理通知事件的地方强制切回主线程?
不推荐。假如子线程发送多个通知,注册多个不同的观察者,那你是否要在每一个通知处理的地方都去切主线程,不够收口
> For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.
这里谈到重定向。一种重定向的实现思路是自定义一个通知队列注意不是NSNotificationQueue 对象而是一个数组让这个队列去维护那些我们需要重定向的Notification。我们仍然是像平常一样去注册一个通知的观察者当 Notification 来了时,判断 postNotification 的线程是不是所期望的线程,如果不是,则将这个 Notification 存储到我们的队列中,并发送一个信号 signal 到期望的线程中来告诉这个线程需要处理一个Notification。指定的线程在收到信号后将 Notification 从队列中移除,并进行处理。
官方 Demo 如下
```objectivec
@interface ViewController () <NSMachPortDelegate>
@property (nonatomic) NSMutableArray *notifications; // 通知队列
@property (nonatomic) NSThread *notificationThread; // 期望线程
@property (nonatomic) NSLock *notificationLock; // 用于对通知队列加锁的锁对象,避免线程冲突
@property (nonatomic) NSMachPort *notificationPort; // 用于向期望线程发送信号的通信端口
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"current thread = %@", [NSThread currentThread]);
// 初始化
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
self.notificationPort.delegate = self;
// 往当前线程的run loop添加端口源
// 当Mach消息到达而接收线程的run loop没有运行时则内核会保存这条消息直到下一次进入run loop
[[NSRunLoop currentRunLoop] addPort:self.notificationPort
forMode:(__bridge NSString *)kCFRunLoopCommonModes];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"TestNotification" object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
});
}
- (void)handleMachMessage:(void *)msg {
[self.notificationLock lock];
while ([self.notifications count]) {
NSNotification *notification = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
[self processNotification:notification];
[self.notificationLock lock];
};
[self.notificationLock unlock];
}
- (void)processNotification:(NSNotification *)notification {
if ([NSThread currentThread] != _notificationThread) {
// Forward the notification to the correct thread.
[self.notificationLock lock];
[self.notifications addObject:notification];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate:[NSDate date]
components:nil
from:nil
reserved:0];
}
else {
// Process the notification here;
NSLog(@"current thread = %@", [NSThread currentThread]);
NSLog(@"process notification");
}
}
@end
```
这种方式先将当前线程存储下来,在收到通知的时候去遍历当前数组(数组代替队列),判断当前线程是不是目标线程,不是则将对头元素移动到对尾。
## FAQ
### 通知的发送是同步还是异步?
通过 postNotification 源码可以看到,通知的发送是同步的,且在同一个线程中
### 页面销毁时,不移除通知会 crash 吗?
通过这段文档,我们可以看出
- 使用 `addObserverForName:object:queue:usingBlock` 必须自己手动移除
- 使用 `addObserver:selector:name:object:` ios9后系统会自动移除
如何自动移除
ios9以后系统使用weak指针修饰observe,当observe被释放后再次发送消息给nil发送不会引起崩溃并且根据描述中提到系统会下次发送通知时移除这些oboserve为nil的观察者
### 多次添加同一个通知的观察者会出现什么问题?多次移除同一个通知会有问题吗?
查看 addObserver 源码会发现,针对同一个 NSNotificationName 进行多次添加,系统并不会过滤,假设有 object则会维护 NAMED MapTablekey 为 NSNotificationNamevalue 为子 MapTable子 MapTable 中 object 为 keyvalue 为 observer。所以多次添加则会造成当 postNotification 的时候会有多次响应。
查看 removeObserver 源码发现,移除都会针对 NSNotificationName 进行操作,从 NAMED MapTable 中,以 NSNofiticationName 为 key获取 value 为子 MapTable 。子 MapTable 根据 object 为 key获取对应的链表然后根据参数 observer 移除链表中所有 observer 都为传递的 observer 的节点。所以多次调用不会存在问题。

View File

@@ -1,538 +0,0 @@
# iOS 界面渲染流程
> 下面几个问题你熟悉吗?
>
> - 为什么调用 `[UIView serNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
## 视图显示原理
为什么调用 `[UIView setNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
UIView 绘制流程。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIViewRefreshProcess.png" style="zoom:60%" />
当调用 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 <UIKit/UIKit.h>
@interface CustomDrawView : UIView
@end
@implementation CustomDrawView
// 重写drawRect: —— 系统绘制流程的上层入口
- (void)drawRect:(CGRect)rect {
// 1. 系统自动创建绘制上下文,这里可以直接获取
CGContextRef context = UIGraphicsGetCurrentContext();
// 2. 绘制操作对应系统流程的「调用drawInContext:」阶段)
// 设置填充色为红色
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
// 绘制一个矩形
CGContextFillRect(context, CGRectMake(20, 20, 100, 100));
NSLog(@"执行drawRect: → 底层对应系统调用CALayer的drawInContext:");
}
// 关键:不实现 displayLayer: 代理方法
// - (void)displayLayer:(CALayer *)layer {} // 注释掉,模拟「未实现」场景
@end
```
在 ViewController 中使用
```objective-c
#import "ViewController.h"
#import "CustomDrawView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 1. 创建自定义View并添加到界面
CustomDrawView *drawView = [[CustomDrawView alloc] initWithFrame:CGRectMake(50, 100, 150, 150)];
drawView.backgroundColor = [UIColor lightGrayColor]; // 浅灰色背景,方便区分绘制区域
[self.view addSubview:drawView];
// 2. 触发绘制打脏标记RunLoop阶段系统会执行layer.display
[drawView setNeedsDisplay];
}
@end
```
结果:屏幕上会显示「浅灰色背景 + 红色矩形」;控制台打印: `执行drawRect: → 底层对应系统调用CALayer的drawInContext:`
分析:
- `[drawView setNeedsDisplay]` → 内部调用 `layer.setNeedsDisplay`,给 layer 打 “脏标记”
- 当前 RunLoop 的 CATransaction 阶段,系统调用 `[layer display]`
- layer 检查代理CustomDrawView有没有实现代理方法 → 未实现`-(void)displayLayer:(CALayer *)layer`
- 系统**自动创建绘制上下文** → 调用 `[layer drawInContext:]`UIView 的`drawRect:`是对这个方法的封装,所以`drawRect:`被执行)
- 系统将绘制结果生成位图 → 赋值给 `layer.contents`
- 最终 layer 把`contents`内容渲染到屏幕
Demo2:
自定义 Layer实现 `displatLayer:` 代理方法的 Layer
```objective-c
#import <QuartzCore/QuartzCore.h>
@interface CustomLayer : CALayer
@end
@implementation CustomLayer
// 重写CALayer的drawInContext: —— 系统绘制流程的核心方法
- (void)drawInContext:(CGContextRef)ctx {
// 系统创建的上下文会传入这个方法
NSLog(@"系统调用drawInContext: → 进入核心绘制阶段");
// 绘制操作:画一个蓝色圆形
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextFillEllipseInRect(ctx, CGRectMake(20, 20, 100, 100));
}
@end
```
在 VC 中使用
```objective-c
#import "ViewController.h"
#import "CustomLayer.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 1. 创建自定义Layer
CustomLayer *customLayer = [CustomLayer layer];
customLayer.frame = CGRectMake(50, 250, 150, 150);
customLayer.backgroundColor = [UIColor lightGrayColor].CGColor;
[self.view.layer addSublayer:customLayer];
// 2. 触发绘制(打脏标记)
[customLayer setNeedsDisplay];
}
@end
```
结果:屏幕上显示「浅灰色背景 + 蓝色圆形」;并且输出:`系统调用drawInContext: → 进入核心绘制阶段`
分析:
- 调用 CALayer 的 setNeedsDisplay 方法,内部会调用 display 方法
- 系统会将其 CALayer 打上 dirty 标记
- RunLoop 会在一次 loop 的末尾,提交 CATranscation。然后去绘制 layer 的 displayLayer 方法
- 判断没有实现 displayLayer 方法,然后自动创建渲染上下文。
- 然后调用 `drawInContext:(CGContextRef)ctx` ,方法的 ctx 参数就是系统自动创建的上下文对象
- 该方法内创建的渲染内容,最后会合成一张 bitmap最后交给 layer.contents 属性
- 屏幕渲染 contents 内容
下面来个 Demo 展示下简单的异步绘制一个 String。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AsyncUILabelRender.png" style="zoom:30%" />
接下来看看系统的绘制实现流程:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIViewSystemRenderProcess.png" style="zoom:60%" />
如何实现异步绘制?
`[layer.delegate displayPlayer:]`
- 代理负责生成对应的 bitmap
- 设置该 bitmap 作为 layer.contents 属性的值
<img src='https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AsyncRenderProcessAPI.png' style="zoom:30%" />
## 渲染机制
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RenderStructure.png)
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最终显示到屏幕上。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RenderPipeline.png)
- Core Animation 提交会话(事务),包括自己和子树(view hierarchy) 的布局状态
- Render Server 解析所提交的子树状态,生成绘制指令
- GPU 执行绘制指令
- 显示器显示渲染后的数据
## Core Animation
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APM-CoreAnimationPipeline.png)
可以看到 Core Animation pipeline 由4部分组成Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
### Application 层 Core Animation 部分
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationCommit.png)
- 布局(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 绘制流程
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIRenderPipeline.png)
- 每个 UIView 都有一个 CALayerlayer 属性都有 contentscontents 其实是一块缓存,叫做 backing store
- 当 UIView 被绘制时CPU 执行 drawRect 方法,通过 context 将数据写入 backing store 中(位图 bitmap)
- 当 backing store 写完后,通过 Render Server 交给 GPU 去渲染,最后显示到屏幕上
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIViewRenderPipeline.png)
- 调用 `[UIView setNeedsDisplay]` 方法时,并没有立即执行绘制工作,而是马上调用 `[view.layer setNeedsDisplay]` 方法,给当前 layer 打上标记
- 在当前 RunLoop 快要结束的时候调用 layer 的 display 方法,来进入到当前视图真正的绘制流程
- 在 layer 的 display 方法内部,系统会判断 layer 的 layer.delegate 是否实现了 `displayLayer` 方法
- 如果没有,则执行系统的绘制流程
- 如果实现了,则会进入异步绘制流程
- 最后把绘制完的 backing store 提交给 GPU
### 系统绘制流程
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSRenderProcess.png)
- 首先 CALayer 内部会创建一个 CGContextRef在 drwaRect 方法中,可以通过上下文堆栈取出 context拿到当前视图渲染上下文也就是 backing store
- 然后 layer 会判断是否存在代理,若没有,则调用 CALayer 的 drawInContext
- 如果存在代理,则调用代理方法。然后做当前视图的绘制工作,然后调用 view 的 drawRect 方法
- 最后由 CALayer 上传对应的 backing store(可以理解为位图)提交给 GPU。
### 异步绘制流程
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSAsyncRender.png)
- 如果 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到SurfaceMetal或者OpenGL ES Texture上 ->
6. SurfaceMetal或者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 是有损压缩可以指定0100%压缩比。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中使用的是大端模式为了兼容我们使用kCGBitmapByteOrder32Host32位字节顺序该宏在不同的平台上面会自动组装换成不同的模式。
/*
#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else //Little endian.
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
*/
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
//根据是否含有alpha通道如果有则使用kCGImageAlphaPremultipliedFirstARGB否则使用kCGImageAlphaNoneSkipFirstRGB
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);
});
});
}
```

View File

@@ -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` 加锁进行多线程安全保护。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/NSUserDfault-lock.png)
## 存储性能如何
查看 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` 可以看到下面的堆栈
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/NSUserDefault-XPC.png)
执行 `[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
// <OS_xpc_dictionary: dictionary[0x7fa975908010]: { refcnt = 1, xrefcnt = 1, subtype = 0, count = 8 } <dictionary: 0x7fa975908010> { count = 8, transaction: 0, voucher = 0x0, contents =
// "CFPreferencesHostBundleIdentifier" => <string: 0x7fa9759080d0> { length = 9, contents = "test.demo" }
// "CFPreferencesUser" => <string: 0x7fa975908250> { length = 25, contents = "kCFPreferencesCurrentUser" }
// "CFPreferencesOperation" => <int64: 0x8ccdbf87dd7d7a91>: 1
// "Value" => <string: 0x7fa9759084b0> { length = 16, contents = "酷酷的哀殿2" }
// "Key" => <string: 0x7fa975908430> { length = 3, contents = "key" }
// "CFPreferencesContainer" => <string: 0x7fa9759083a0> { length = 169, contents = "/private/var/mobile/Containers/Data/Application/0C224166-1674-4D36-9CDB-9FCDB633C7E3/" }
// "CFPreferencesCurrentApplicationDomain" => <bool: 0x7fff80002fd0>: true
// "CFPreferencesDomain" => <string: 0x7fa975906ea0> { length = 9, contents = "test.demo" }
// }>
xpc_object_t hello = xpc_dictionary_create(NULL, NULL, 0);
// 注释1test.demo 是 bundleid。测试代码时需要根据需要修改
xpc_dictionary_set_string(hello, "CFPreferencesHostBundleIdentifier", "test.demo");
xpc_dictionary_set_string(hello, "CFPreferencesUser", "kCFPreferencesCurrentUser");
// 注释2存储值
xpc_dictionary_set_int64(hello, "CFPreferencesOperation", 1);
// 注释3存储的内容
xpc_dictionary_set_string(hello, "Value", "this is a test");
xpc_dictionary_set_string(hello, "Key", "key");
// 注释4存储的位置
CFURLRef url = CFCopyHomeDirectoryURL();
const char *container = CFStringGetCStringPtr(CFURLCopyPath(url), kCFStringEncodingASCII);
xpc_dictionary_set_string(hello, "CFPreferencesContainer", container);
xpc_dictionary_set_bool(hello, "CFPreferencesCurrentApplicationDomain", true);
xpc_dictionary_set_string(hello, "CFPreferencesDomain", "test.demo");
xpc_connection_set_event_handler(conn, ^(xpc_object_t object) {
printf("xpc_connection_set_event_handler:收到返回消息: %sn", xpc_copy_description(object));
});
xpc_connection_resume(conn);
#pragma mark - 异步方案一 (没有回应)
// xpc_connection_send_message(conn, hello);
#pragma mark - 异步方案二 (有回应)
xpc_connection_send_message_with_reply(conn, hello, NULL, ^(xpc_object_t _Nonnull object) {
printf("xpc_connection_send_message_with_reply:收到返回消息: %sn", xpc_copy_description(object));
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // mach_msg_trap
printf("从 NSUserDefaults 读取:%s\n", [defaults stringForKey:@"key"].UTF8String);
});
#pragma mark - 同步方案
// xpc_object_t obj = xpc_connection_send_message_with_reply_sync(conn, hello);
// NSLog(@"xpc_connection_send_message_with_reply_sync:收到返回消息:%s", xpc_copy_description(obj));
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // mach_msg_trap
printf("从 NSUserDefaults 读取:%s\n", [defaults stringForKey:@"key"].UTF8String);
```

View File

@@ -1 +0,0 @@
# IM技术

View File

@@ -1,829 +0,0 @@
# 精准测试最佳实践
## 背景
下面这张图是22年整理的我们移动中台对于质量的一些把控手段也是一个有效的 checklist。对于一个业务项目或者技术项目来说QA 给的测试用例全部通过不能说明代码没有问题。单测覆盖率的最佳实践是针对于基础 SDK对于业务侧的代码来说由于经常变化所以还是以人工测试为主一些核心的不变的核心链路沉淀出 UI 自动化用例,每周迭代的时候,交付测试后,开始 UI 自动化回归。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CodeQualityChecklist.png" style="zoom:25%">
但,这些回归还是不能 cover 所有问题,我们需要为我们写的每行代码买单,如何衡量每行代码的效果呢?这就是精准测试要做的事情。
iOS 工程来说跨端项目暂时不在本文范畴Native 侧主要是 OC 和 Swift 为主。本文将会从 OC/ Swift 2个技术栈展开说说如何获取精准测试覆盖率报告。
## Objective-C 代码覆盖率
### 理论分析
#### 覆盖率检测原理
统计代码覆盖率的实现抓手就是对代码进行插桩OC 是 C 语言的一个超集,而 LLVM 诞生自 GCC我们可以使用 GCC 的插桩器对 OC 代码进行编译插桩,具体流程如下:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClangStubFullProgress.png" style="zoom:65%">
在编译阶段指定 `-fprofile-arcs` `-ftest-coverage` 等测试选项LLVM 会做这么几件事:
- 在输出目标文件中留出一段存储区保存统计数据
打开一个插桩工程,查看 MachO 文件可以印证。可以看到 `__llvm_prf_cnts``__llvm_prf_data``__llvm_prf_names``__llvm_prf_vnds``__llvm_covfun``__llvm_covmap` 等 section 就是存储插桩信息的空间。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOSpaceToRecordCodeInstrucment.png" style="zoom:25%">
- 在源代码中为每个 Basic Block 进行插桩Basic Block 下文会讲)
可以看到 `showAssets` 方法内存在一个 if即2个 Basic Block所以通过汇编查看的话存在2个插桩点。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeAssemblyProveBasicBlockCodeInstrument.png" style="zoom:20%">
- 产生 `.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 文件(关联计数指令和源文件)。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMCodeCoverageIRPass.png" style="zoom:20%">
#### Basic Block
从编译器角度出发基本块Basic BlockBB是代码执行的基本单元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 内各行代码执行情况, 从而计算整个程序的覆盖率情况。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/BasicBlockGraphics.png" style="zoom:70%">
也就是说插桩的数量和函数内的代码行数、函数数量都不是一一对应的关系。**插桩数量和 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");
}
}
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeBasicBlockFlow.png" style="zoom:40%">
#### .gcon 计数符号和文件位置关联信息
`.gcon` 文件存储着计数插桩位置和源文件之间的关联信息。`GCOVPass` 通过2层循环插入计数指令的同时会将文件及 BB 信息写入 `.gcon` 文件。
- 创建 `.gcno` 文件,写入 Magic number(oncg + version)
- 随着函数遍历写入文件地址、函数名和函数在源文件中的起止行数(标记文件名,函数在源文件对应行数)
- 随着 BB 遍历,写入 BB 编号、BB 起止范围、BB 的后继节点编号(标记基本块跳转关系)
- 写入函数中BB对应行号信息标注基本块与源码行数关系
`.gcon` 文件由4部分组成
- 文件结构
- 函数结构
- BB 结构
- BB 行结构
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMBasicBlockStructure.png" style="zoom:40%">
#### .gcda
关于 `.gcda` 的逻辑可以查看源码的 [GCDAProfiling.c 文件](https://github.com/llvm/llvm-project/blob/main/llvm/lib/Transforms/Instrumentation/GCOVProfiling.cpp),也是覆盖率相关的核心逻辑。
```c++
void GCOVProfiler::emitGlobalConstructor(
SmallVectorImpl<std::pair<GlobalVariable *, MDNode *>> &CountersBySP) {
Function *WriteoutF = insertCounterWriteout(CountersBySP);
Function *ResetF = insertReset(CountersBySP);
// Create a small bit of code that registers the "__llvm_gcov_writeout" to
// be executed at exit and the "__llvm_gcov_reset" function to be executed
// when "__gcov_flush" is called.
FunctionType *FTy = FunctionType::get(Type::getVoidTy(*Ctx), false);
Function *F = createInternalFunction(FTy, "__llvm_gcov_init", "_ZTSFvvE");
F->addFnAttr(Attribute::NoInline);
BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", F);
IRBuilder<> Builder(BB);
FTy = FunctionType::get(Type::getVoidTy(*Ctx), false);
auto *PFTy = PointerType::get(FTy, 0);
FTy = FunctionType::get(Builder.getVoidTy(), {PFTy, PFTy}, false);
// Initialize the environment and register the local writeout, flush and
// reset functions.
FunctionCallee GCOVInit = M->getOrInsertFunction("llvm_gcov_init", FTy);
Builder.CreateCall(GCOVInit, {WriteoutF, ResetF});
Builder.CreateRetVoid();
appendToGlobalCtors(*M, F, 0);
}
```
二进制代码加载时,调用了 `llvm_gcov_init(fn_ptr wfn, fn_ptr rfn)` 函数,传入了 `__llvm_gcov_writeout` 方法用于写 `.gcov` 文件,`__llvm_gcov_reset` 方法用于 reset 保存的数据。
然后 `emitGlobalConstructor ` 函数调用 `insertGlobalConstructorCode` 函数,后者负责插入全局构造函数所需的代码。`insertGlobalConstructorCode` 函数进一步调用 `initializeGCOVDataStructures` 函数和 `setupCodeCoverageEnvironment` 函数,分别用于初始化 `.gcov` 数据结构和设置代码覆盖率测试环境。
```c++
COMPILER_RT_VISIBILITY
void llvm_gcov_init(fn_ptr wfn, fn_ptr rfn) {
static int atexit_ran = 0;
if (wfn)
llvm_register_writeout_function(wfn);
if (rfn)
llvm_register_reset_function(rfn);
if (atexit_ran == 0) {
atexit_ran = 1;
/* Make sure we write out the data and delete the data structures. */
atexit(llvm_delete_reset_function_list);
#ifdef _WIN32
atexit(llvm_writeout_and_clear);
#endif
}
}
```
代码注释是 `__gcov_flush`LLVM 老版本的 `__gcov_flush` )已经更新为 `__gcov_dump` ,调用 `__gcov_dump` 会将覆盖率信息写入文件。
```c++
void __gcov_dump(void) {
for (struct fn_node *f = writeout_fn_list.head; f; f = f->next)
f->fn();
}
```
`.gcda` 文件/函数结构和 `.gcno` 基本一致,包含了弧跳变的次数和其他概要信息。利用 `gcov -f Person.gcda` 就可以可视化查看 `.gcda` 文件内容
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GcdaFileViaGcov.png" style="zoom:30%">
Xcode 导出 `.gcda` 的时候,断点查看汇编如下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GcovDumpAssembly.png" style="zoom:25%">
#### .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差异代码行执行数
#### 完整流程
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GccCodeCoverageFlow.png" style="zoom:60%">
- 编译前, 在编译器中加入编译器参数 `-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打开后即**开启插桩能力**。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeEnableCodeCoverageSetting.png" style="zoom:20%">
第二步,为了控制代码覆盖率保存的位置和文件名,需要我们设置一下 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();
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeBuildErrorWithGcovFlush.png" style="zoom:20%">
第四步,运行代码。完成测试后,我在屏幕点击事件里,将 BB 执行情况写入到 `.gcda` 中。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeShowGcdaPath.png" style="zoom:20%">
第五步,获取 `.gcno` 信息。编译器生成与源代码同名的 `.gcno` 文件note file这种文件含有重建基本块依赖图和将源代码关联至基本块及源代码行号的必要信息。
Xcode 选择 productsshow 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` 信息。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeGcnoFileLocation.png" style="zoom:20%">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeGcnoFiles.png" style="zoom:100%">
第六步,将 `.gcno` 和 `.gcda` 文件,保存到一个文件夹下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GcnoAndGcdaFilesInSameDir.png" style="zoom:40%">
第七步,利用 `lcov` 指令可以将 `.gcno` 文件和 `.gcda` 文件结合生成代码覆盖率结果 info 文件
指令格式为:`lcov -c -d . -o CodeCoverage.info` ,其中 `.` 代表当前目录
`CodeCoverage.info` 文件内容大概如下(各个字段代表什么上面 info 文件这一节有说明)。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LcovCombineGcovAndGcda.png" style="zoom:30%">
第八步,利用指令 `genhtml -o html CodeCoverage.info` 将 info 文件和源代码文件结合转化为可视化网页形式。
注意:执行 genhtml 指令必须保证和项目源代码Xcode 项目叫 CodeCoverageDemo源码则在 CodeCoverageDemo/CodeCoverageDemo 下)在同一文件夹下否则会报错。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LcovGenerateCoverageHTML.png" style="zoom:30%">
访问覆盖率路径为 html 目录下,和项目同名的文件夹里面的 `index.html`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LcovCodeCoverageHTMLPath.png" style="zoom:25%">
第九步,通过类的列表,针对覆盖率低的文件,点进去看看,看看那些代码没有被执行。思考是什么原因造成的:
- if...else 代码是由于测试条件不满足,测试 case 不充足,导致另一个 case 没有被覆盖??
- 某些兜底代码太多,根本走不到???
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeCoverageInClass.png" style="zoom:25%">
其中:蓝色部分代码已经执行的代码,橘色代表未执行的代码
第十步假设我们在另一台设备上进行了测试对剩余的测试任务内容进行完善这个时候该怎么处理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` 里面
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeAnotherGcnoFiles.png" style="zoom:25%">
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. 查看分析最新的覆盖率报告
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCCodeCoverageInfoFileCombined.png" style="zoom:25%">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCCodeCoverageCombinedReport.png" style="zoom:25%">
### 缺陷
Person 类的 showAssets 方法,内部有 Cat 相关逻辑,且 Cat 是 Swift 代码。为什么在代码覆盖列表上看不到 Cat
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GCCCannotCaptureSwiftCodeCoverage.png" style="zoom:25%">
因为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 如何解读
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GitDiffDIsplay.png" style="zoom:45%">
其中
- `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` 可以看到
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCCompileOptionsAboutCoverage.png" style="zoom:25%">
可以看到这2个参数是大概收集代码覆盖率相关的。
#### MachO 和汇编插桩验证
利用 MachOView 查看产物里的 Mach-O 文件发现MachO 多了一些和 LLVM 相关的 section这些 section 看名字猜出来都是用来统计覆盖率的。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOViewSwiftCodeCoverageInfo.png" style="zoom:25%">
当 Xcode 开启 Swift 插桩统计后,打断点查看汇编代码可以发现,在 sayHi 方法也就是只有1个 Basic Block 的情况下编译器只插入1个桩插桩1次。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeSwiftCodeCoverageViaAssembly.png" style="zoom:25%">
可以把 `__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) 也说明了该细节
> LLVMs 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. Its 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` 使用-映射信息从对象文件中提取,用于关联文件中的执行计数(配置文件检测计数器的值)和源范围
在此之后,该工具能够为程序生成各种代码覆盖率报告。
完整流程为:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageProgress.png" style="zoom:50%">
覆盖率生成流程为:编译阶段使用 `-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
```
整个步骤也可以看这张图
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftcDisplaySwiftCodeCoverageInCommandLine.png" style="zoom:30%">
`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` 选项。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeSwiftCoveraeCompileOptions.png" style="zoom:30%">
第三步,开启覆盖率收集选项
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeSetTestCoverageOptions.png" style="zoom:30%">
第四步,要将覆盖率信息导出前,必须要调用 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<UITouch>, with event: UIEvent?) {
self.person?.sayHi()
self.person?.play()
// Do any additional setup after loading the view.
print("File Path Prefix: \(String(cString: __llvm_profile_get_path_prefix()) )")
print("File Name: \(String(cString: __llvm_profile_get_filename()) )")
let name = "SwiftCodeCoverage.profraw"
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let filePath: NSString = documentDirectory.appendingPathComponent(name).path as NSString
__llvm_profile_set_filename(filePath.utf8String)
print("File Name: \(String(cString: __llvm_profile_get_filename()))")
__llvm_profile_write_file()
} catch {
print(error)
}
}
}
````
第六步, 运行代码,生成 `.profraw` 格式的文件。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeGenerateProfDataAboutSwiftCoverage.png" style="zoom:25%">
第七步,因为产出覆盖率的时候需要用到 MachO 文件。所以在项目根目录下创建名为 `DataAnalysis` 的文件夹。在终端利用 mv 将产物里的 MachO 移动到 `DataAnalysis` 文件夹下。也将 `.profraw` 移动进去。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageMVProdrawAndMachO.png" style="zoom:25%">
第八步,利用指令 `xcrun llvm-profdata merge -sparse SwiftCodeCoverage.profraw -o SwiftCodeCoverage.profdata`,将 `.profraw` 转换成 `.profdata` 文件
第九步,利用指令 `xcrun llvm-cov show ./SwiftCodeCoverage.app/SwiftCodeCoverage -instr-profile=SwiftCodeCoverage.profdata` 在终端查看代码的覆盖情况
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageInCommandLine.png" style="zoom:25%">
第十步,终端查看代码执行情况还是不够直观,可以用 `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 文件。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageReport.png" style="zoom:25%">
第十二步,假设我们在另一台 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 报告
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftProfdataCombinedAndCoverageReport.png" style="zoom:25%">
第十三步,我们有时候有需求会更改操作生成的测试文件,`.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 和技术值信息)
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftProfdataTextChangeBBCount.png" style="zoom:25%">
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`
效果如下:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCoverageFromCombinedProfdataText.png" style="zoom:25%">
## 心得感悟
下面是一个工作中的实际例子,冒烟用例也全部通过了,代码在 CR 后 MR 了,然后买票上车,开始高铁回归阶段。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/YouzanCodeCoverageUsage.png" style="zoom:45%">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeCoverageAnalysisReport.png" style="zoom:35%">
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)

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
//print(MemoryLayout.stride(ofValue: str1))
//print(Mems.memStr(ofVal: &str1))

View File

@@ -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及其它上面的子控件默认是不能接受触摸事件的。

View File

@@ -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 & !isChargedAccountB -> 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 动态请求,然后执行业务逻辑。需思考一些问题:
- 网络请求慢怎么处理?
- 需不需要缓存?
- 有缓存的话,更新策略是什么?
- 需不需要内置的产品逻辑?
当然,这不在本篇文章范畴内,不做展开。

View File

@@ -1,364 +0,0 @@
## 写给 iOSer 的鸿蒙开发 tips
## 下载问题
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DevEco-Studio-DownloadNetworkErrror.png)
The other possible cause is that the system language of the PC is English and the region code is US. You could try to perform the following operations to change the region code to CN. Before changing the region code, close DevEco Studio.
For Mac OS: ~/Library/Application Support/Huawei/DevEcoStudio3.0/options/country.region.xml
修改为 CN
```xml
<application>
<component name="CountryRegionSetting">
<countryregion name="CN"/>
</component>
</application>
```
## 鸿蒙开发任务拆分
### 依赖梳理
- 梳理依赖的二方、三方 SDK根据对应 SDK 的鸿蒙化排期调整整体项目的排期
- 暂时不能鸿蒙化的 SDK 功能降级
### 优先级对齐
- 根据依赖梳理的排期,倒推二方 SDK 的优先级
- 梳理各个团队的 P1、P2、P3 任务
### 功能定版
- 考虑到部分基础能力无法实现和人力资源排期的问题,无法完全对齐 Android 版本
- 根据前期梳理,确定每个节点的功能范围
### 架构设计
- App 模块架构基本对齐 Android
- 鸿蒙特性部分微调
- 基础能力下沉到 C++,提升复用
## 规范
### 代码规范制定
除了常规流程的 MR但对于鸿蒙大家都属于摸着石头过河的状态。开发也是摸着石头过河所以会变开发定期制定、更新代码规范文档并定期 review 项目代码,完善代码规范,避免糟糕代码野蛮生长
### 踩坑分享
及时收集整理开发过程中的踩坑点,定期在团队内分享,减少或避免相同问题的再次发生
### 最佳实践分享
定期梳理最佳实践,鼓励分享。快速提高、拉齐大家的鸿蒙水平
## 鸿蒙背景下的跨端方案
业务使用 TS 开发,公用一套渲染引擎
### 引擎选择
| 引擎 | | 优点 | 缺点 |
| -------------- | --------------- | --------------------------------- | ----------------------------- |
| JavaScriptCore | Apple | 在 iOS 平台上有非常明显的主场优势 | 在 Android 缺乏优化,适配不好 |
| V8 | Google | 性能优异,支持 JIT支持调试 | 体积大、内存占用大 |
| quickJS | Fabrice Bellard | 体积小、内存小 | 无法进行 JIT不支持调试 |
| panda 熊猫 | 鸿蒙 | 内置 JS 引擎,支持 JIT | 没有太多关于引擎的介绍 |
所以 iOS 侧旋 JavascriptCore 引擎Android 选 quickJS 作为 JS 引擎,后续增加 V8 作为本地开发调试的引擎。
## 公共能力
### 公共桥能力
业务方可以通过公共桥直接使用 Native 侧的能力:
- modal 弹窗Modal 桥主要用来展示 toast、dialog、window 等一些原生系统弹窗能力
- keyboardkeyboard 桥主要用来控制键盘相关的操作
- NavigatorNavigator 桥主要用来负责导航以及对导航栏的定制操作
- NetworkNetwork 桥主要用于网络相关的操作
- StorageStorage 桥主要用来持久化存储和获取数据
- BroadcastBroadcast 桥主要用于接收和发送通知
### 自定义桥能力开发
- 定义桥协议:定义需要实现的功能、确定桥所属模块与方法名、参数等
- TS 侧开发:在 TS 侧实现桥协议的方法,确定以同步还是异步的方式回调
- Native 侧开发:通过公共通信层解析 TS 侧透传的方法与参数,实现 Native 侧功能与回调
- 桥注册:桥开发完毕后,需要在 Native 侧通过 registerCustomModule 方法,注册后才可以使用
- 桥使用TS 侧业务代码通过调用桥协议,来使用自定义桥功能
### 渲染
将 js 引擎返回的 UI 数据通过解析进行渲染,根布局为 stack子组件通过 offset 确定位置、size 确定大小、type 确定组件类型。会有重叠、内嵌的情况,则递归循环渲染即可
参考鸿蒙 RN 团队在1月份的方案使用 stack 组件内部forEach 循环渲染子组件。适配初期,在嵌套不深的页面没有发现问题,整体上打通了从 JS 代码到引擎渲染的核心流程。
<img src='https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyCrossPlatformArch.png' style="zoom:40%" />
左侧的 UI 树和右侧的 Model 树,通过 `@ObjectLink``@Observed` 来进行数据渲染和刷新。
## 鸿蒙APO 探索之路
AOP Aspect Oriented Programming 是一种编程范式,被允许开发者将关注点与业务逻辑中分离出来。
AOP 优势:解耦、复用、模块化
AOP 应用场景:日志、埋点、监控
### AOP 的实现方案
- 编译时AspectJ
- 类加载时AspectJ
- 链接时fishhook
- 运行时Epic
### 为什么使用 AOP
日志、网络、性能监控、埋点等多个 AOP 使用场景。
### 鸿蒙 AOP 方案探索
#### 痛点
- 匿名函数、箭头函数:`Button.onClick(ClickEvent)`
- 函数局部类: `HttpClient#Builder()#build()`
```ts
View().
onClick(() => {
// handle business logic
})
```
- 属性不可修改场景:`router.pushUrl`
`Object.defineProperty()` 当 writeable 特性设置为 false 的时候,该属性是不可修改的。尝试对一个不可修改的属性进行写入时不会改变它。在严格模式下还会报错。
鸿蒙也是基于 TS 的,所以也可以调用 `Object.freeze()` 冻结属性。比如鸿蒙早期路由实现,是基于 Router 的,很多 API 的参数writeable 属性都是 false。
解决方案
无统一修改点场景一:箭头参数函数 `Button.onClick(ClickEvent)`
构造一个第一个参数为函数的 wrappFn 函数,持有目标参数函数。就跟 Native 侧的 hook 一样。构造一个一样的 hook 函数。
```ts
function hookMethod(traget, action, beforeFn?, afterFn?) {
wrapMethod(target, action, (originalMethod) => function(callback) {
const wrappedCallback = (...args) => {
beforeFn?.apply(this, args)
callback.apply(this, args)
afterFn?.apply(this, args)
}
originalMethod.call(this, wrappedCallback)
});
}
```
使用
```ts
Aspect.addBefore(Button, 'onClick', () => {
router.pushUrl({url: 'pages/Index' })
logger.w(TAG, '1.Aspect add before --- Button#onClick()#action, do your business...');
}, true)
```
无统一修改点场景二:局部类 `HttpClient#Builder()#build()`
AOP 的本质是关注点分离,面对这种情况,每次通过 HttpClient 获取 Budiler() 就会产生不同的对象,所以传统的通过 hook 某个类的方法形式,已经不再适用了。所以需要通过更高层的 hook`Object.defineProperty`
通过属性定义拦截 HttpClient 的 Builder() 属性的获取builder 的获取,都会被收口拦截。
```ts
function hookMethod(target: any, propertyName: string, methodName: string, beforeFn?: (context: any, args: any[]) => void, afterFn?: (context: any, args: any[], result: any) => void) {
const propertyDescriptor = Object.getOwnPropertyDescriptor(target, propertyName)
if (propertyDescriptor && propertyDescriptor.get) {
Object.defineProperty(target, propertyName, {
get() {
const originalTarget = propertyDescriptor.get!.call(this)
const originalMethod = originalTarget.prototype[methodName]
originalTarget.prototype[methodName] = function (...args: any[]) {
beforeFn?.call(this, this, args)
let result = originalMethod.apply(this, args)
afterFn?.call(this, this, args, result)
return result
}
return originalTarget
}
})
}
}
```
使用
```ts
Apsect.hookMethod({
target: HttpClient,
methodNameOrProperty: 'Builder',
beforeFn: (context: args) => {
const builderContext = context as InstanceType<typeof HttpClient.Builder>;
builderContext._eventListener = new MyEnevtListener()
builderContext.addInterceptor(new MyEnevtListener());
},
propertyMethodNameOrType: 'build'
})
```
无统一修改点场景三:`router.pushUrl`,编译时 + 运行时组合实现偷梁换柱。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyHookWithInstrument.png" style="zoom:40%" />
如何实现编译时替换?
鸿蒙提供了 Hvigor Plugin 编译时自定义插件,用于实现定制化构建。思路:直接利用 Hvigor Plugin hook 某个编译 task
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyHookStep1.png" style="zoom:40%" />
那到底 hook 哪个编译 task
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyHookCompileTask.png" style="zoom:40%" />
问题:
- output 修改无效
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyCannotChangeOutputFile.png" style="zoom:40%" />
ArkTS 编译之后会产生临时目录,将 ets 编译为 ts。那是不是可以直接修改产物看看最后能不能影响方舟字节码。
发现修改了 index.protoBin 、ts 文件,发现最终无法影响编译产物 `*.abc` 文件
联系了鸿蒙团队的工程师验证了说是2条并行链路。并不是先编译产生临时文件再通过临时文件产生 `*.abc` 文件。事实上是2个并行过程。所以此路不通
- input 无法 hookHvigor plugin 暂未开放相关能力
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyHookWithChangeInput.png" style="zoom:30%" />
Hvigor 目前开放的能力有限,仅支持在默认的 task 前后加一些钩子函数。但无法修改 input。此路不通
- 既然没法直接修改默认 task怎么实现
思路copy -> modify -> revert
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyHookWithAST.png" style="zoom:40%" />
插桩只能影响产物,不能影响源码。所以先对源码进行备份。
```ts
export function HllEntreyPlugin(): HvigorPlugin {
return {
pluginId: 'HllEntryPlugin',
apply: (node: HvigorNode) => {
node.registerTask({
name: 'HllEntreyPluginInjectTask',
run: () => {
dispatcherToPlugins(node) // copy & modify
}
});
node.registerTask({
name: 'HllEntryPluginResetTask',
run: () => {
resetPluginCodes(originalBackUpFiles) // revert
},
dependcies: ['default@CompileArkTS'],
postDependencies: ['assembleHap']
})
}
}
}
```
如何修改 AST
鸿蒙目前没有提供修改 AST 的能力。如何做?
ArkTS 虽然没有提供 AST 相关 API。但从 CompileArkTS Task 产物来看ArkTS 最终会编译成 TS。
且 TS 提供 AST 相关 API。TS 是开源的TS 开源代码中有关于 AST 的模块。[TS Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)
可以基于源码中 AST 相关的 API 可以抽取封装一下。
于是,整个流程就变成了
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyHookWithAST.png" style="zoom:40%" />
可以利用 [Typescript AST Viewer](https://github.com/dsherret/ts-ast-viewer) 来查看 AST 抽象语法树信息。可以在线查看 AST官网地址为[Typescript AST Viewer](https://ts-ast-viewer.com)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HarmonyFunctionHook.png" style="zoom:40%" />
#### Aspect运行时
官方在 API 11 开始提供的方案,可快速实现对类方法前后进行插桩或替换。
关键点一:属于可修改-即 addBefore、addAfter、replace 接口的原理,基于 class
#### AspectPro V1编译时<正则> + 运行时)
#### AspectPro V2编译时<AST> + 运行时)

View File

@@ -1,442 +0,0 @@
# Swift 枚举值内存布局
> enum 使用很简单,那大家有没有思考过系统针对枚举的实现是怎么样的?
>
> 不同类型的枚举占用多大内存空间?下面结合汇编来窥探下系统实现原理
## 基础枚举(不带关联值、不带原始值)
```swift
enum Season {
case spring
case summer
case antumn
case winter
}
var season: Season = Season.spring
print(Mems.ptr(ofVal: &season))
season = Season.summer
season = Season.antumn
print("over")
```
- `var season: Season = Season.spring` 基础枚举默认值是0。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/EnumBaseMemoryLayoutDemo1.png" style="zoom:25%">
- `season = Season.summer`此时可以看到第一个字节的位置是1.
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/EnumBaseMemoryLayoutDemo2.png" style="zoom:25%">
- `season = Season.antumn` 此时可以看到第一个字节的位置是2
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/EnumBaseMemoryLayoutDemo3.png" style="zoom:25%">
结论:查看内存信息,可以看到**不带关联值、不带原始值的基础枚举只占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>.size)
//print(MemoryLayout<Season>.stride)
//print(MemoryLayout<Season>.alignment)
var season: Season = Season.spring
print(Mems.ptr(ofVal: &season))
season = .summer
season = .winter
print("over")
```
- `var season: Season = Season.spring` 基础枚举变量默认值可以看到第一个字节的位置是0
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/EnumWithRawValueMemoryLayoutDemo1.png" style="zoom:25%">
- `season = .winter` 基础枚举,当赋值为 winter 的时候可以看到第一个字节的位置是3
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/EnumWithRawValueMemoryLayoutDemo2.png" style="zoom:25%">
结论:**只带有原始值的枚举同样只占用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<Season>.size)
print(MemoryLayout<Season>.stride)
print(MemoryLayout<Season>.alignment)
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)` 带有关联值的枚举:
`.spring` 有3个 Int单个 Int 占8个字节空间所以红色框代表 spring 的1蓝色框代表 spring 的2绿色框代表 spring 的3黄色框代表枚举的第1个 case剩余7个字节为空。后续的7个字节是为了内存对齐而补齐的内存。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedEnumMemoryLayoutDemo1.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedEnumMemoryLayoutDemo3.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedEnumMemoryLayoutDemo4.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedEnumMemoryLayoutDemo5.png" style="zoom:25%">
其内存信息如下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个字节为空。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedEnumMemoryLayoutDemo6.png" style="zoom:25%">
其内存信息如下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<Season>.size` 3个 Int 最大为3*81个字节用来表达位置信息`3*8 + 1 = 25`
- `MemoryLayout<Season>.stride` :获取系统分配给数据类型的内存大小,也就是实际内存大小(对齐后的)
- `MemoryLayout<Season>.alignment` 内存对齐系数以8 Byte 为单位,对象分配的内存必须是该值的整数倍
结论:
- **对于有不同枚举值有不同关联结构的枚举,其内存由关联值最大的 case 的内存决定(其余的 case 沿用该结构。所有 case 共享同一块内存区域。该区域大小为最大关联值所需内存)**
- **除了最大内存的 case 外还需要1个字节存储所属哪个 case**(通常 1 字节,具体取决于 case 数量1字节能表示的 case 数量为256个
- **枚举总内存实际大小 = 1个字节存储所属哪个 case + 关联值所占内存最大的 case 的内存大小**
- **另外枚举所占内存还需要考虑内存对齐的情况。比如本例中实际内存为25内存对齐为8字节所以最终分配了32字节的内存**
## 只有一个 case 的枚举
```swift
enum SimpleEnum {
case one
}
var caseOne = SimpleEnum.one
print(MemoryLayout<SimpleEnum>.size) // 0
print(MemoryLayout<SimpleEnum>.stride) // 1
print(MemoryLayout<SimpleEnum>.alignment) // 1
```
为什么 size 为0看上去是一个变量但根本不占内存。因为枚举里面就一个 case所以里面根本不需要存储值来区分是哪个 case。
```swift
enum SimpleEnum {
case one
case two
}
var caseOne = SimpleEnum.one
print(MemoryLayout<SimpleEnum>.size) // 1
print(MemoryLayout<SimpleEnum>.stride) // 1
print(MemoryLayout<SimpleEnum>.alignment) // 1
```
现在好理解2个 case 需要存储1个 Byte 的值来区分是哪个 case1 Byte 可以代表最多256个 case
## 只有1个 case 且带关联值的枚举
```swift
enum SimpleEnum {
case one(Int)
}
var caseOne = SimpleEnum.one(4)
print(MemoryLayout<SimpleEnum>.size) // 8
print(MemoryLayout<SimpleEnum>.stride) // 8
print(MemoryLayout<SimpleEnum>.alignment) // 8
```
- 带有关联值且只有1个 case 的枚举因为有1个 Int 的关联值需要8 Byte 存储关联值
- 但只有1个 case不需要额外空间来判断所属哪个枚举值所以不需要额外空间
请看下面的对照实验
```swift
enum SimpleEnum {
case one(Int)
case two
}
var caseOne = SimpleEnum.one(4)
print(MemoryLayout<SimpleEnum>.size) // 9
print(MemoryLayout<SimpleEnum>.stride) // 16
print(MemoryLayout<SimpleEnum>.alignment) // 8
```
2个 case其中一个 case 有关联值 Int所以需要8 Byte 存 Int 值1 Byte 区分是哪个 case实际需要占用 8 + 1 = 9 Byte内存对齐单位是89向上为16。
### 为什么不能复用关联值的空间
case1 占用8个字节case2 占用1个字节能用 case1 的8个字节的最开始的位置存储 case2 的信息吗?这样的话节省内存
- **内存布局的确定性**Swift 要求枚举实例的内存布局在编译时确定。若允许不同 case 复用同一块内存,会导致运行时动态解析内存布局,降低性能和安全性。
- **所有 case 共享同一块内存**:枚举实例的内存大小由**最大关联值的大小 + 标签所需空间**决定。
- **标签Discriminant**:用于区分不同 case通常占用 1 字节(但具体由 case 数量决定)。
**标签的占用大小**由枚举的 `case` 数量决定:
- **1 个 `case`**:不需要标签(因为没有其他可能性)。
- **2~256 个 `case`**:标签通常占用 **1 字节**`UInt8`,可以表示 256 种可能)。
- **257~65536 个 `case`**:标签占用 **2 字节**`UInt16`)。
- 更大数量依此类推(但实际中几乎不会用到如此多的 `case`)。
- **内存对齐约束**:即使标签的逻辑占用小于 1 字节(例如只有 2 个 `case`,理论上只需 1 位),实际仍会占用 **至少 1 字节**(因为内存按字节寻址)。
- **内存对齐**:总大小会按对齐要求(如 8 字节)向上取整到最近的倍数(即 `stride`)。
以示例中的 `SimpleEnum` 为例:
- `case one(Int)`:需要 8 字节存储 `Int` + 1 字节标签 → 共 9 字节。
- `case two`:仅需 1 字节标签 → 但内存仍需按最大 case 分配(即 8 字节关联值空间 + 1 字节标签 → 总 9 字节)。
因此,无论当前是哪个 case枚举实例始终占用 **9 字节**(对齐后 `stride` 为 16 字节)。
更改验证标签对于枚举占用内存大小的影响
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftEnumCaseNumAffectMemory.png" style="zoom:40%" />
可以看到 enum 只有1个 case 的时候内存大小只和最大关联值大小有关1个 case 的情况下不需要额外的空间来判断所属哪个 case。
因此此时枚举的内存大小 = 最大关联值的内存大小 = 8
## 用汇编验证下内存
```swift
enum Season {
case spring(Int, Int, Int)
case summer(Int, Int)
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)` 位置
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedEnumMemoryLayoutExplore.png" style="zoom:25%">
将断点处的汇编单独摘出来研究
```assembly
0x10000334b <+11>: movq $0x1, 0x8eaa(%rip) ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
0x100003356 <+22>: movq $0x2, 0x8ea7(%rip) ; SwiftDemo.season : SwiftDemo.Season + 4
0x100003361 <+33>: movq $0x3, 0x8ea4(%rip) ; SwiftDemo.season : SwiftDemo.Season + 12
0x10000336c <+44>: movb $0x0, 0x8ea5(%rip) ; SwiftDemo.season : SwiftDemo.Season + 23
0x100003373 <+51>: movl $0x1, %edi
```
`rip` 存储的说指令的地址。CPU 要执行的下一条指令地址就存储在 rip 中。所以在执行第一行的时候rip 寄存器的值。
所以第一句汇编代码的意思是rip 为 `0x100003356`,再加上 `0x8eaa`,得到一个地址值(用 Mac 自带的计算器可以算出)`0X10000C200`,然后 movq 是将十六进制的1赋值给 `0X10000C200` 这个地址。
第二句汇编代码类似,此时 rip 为 `0x100003361`,再加上 `0x8ea7`,得到一个地址值 `0X10000C208`,然后 movq 将十六进制的2赋值给 `0X10000C208` 这个地址。
第三句汇编代码类似,此时 rip 为 `0x10000336c`,再加上 `0x8ea4`,得到一个地址值 `0X10000C210`,然后 movq 将十六进制的3赋值给 `0X10000C210` 这个地址。
第四句汇编代码类似,此时 rip 为 `0x100003373`,再加上 `0x8ea5`,得到一个地址值 `0X10000C218`,然后 movq 将十六进制的0赋值给 `0X10000C218` 这个地址。
此时断点走到下一行,拿到 season 的内存地址 `0X10000C200` ,查看内存发现和上面理论分析一直
```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
```
结论:如果枚举存在关联值,内存大小为:
- 1个字节用来存储成员值
- n个字节用来存储关联值n取占用内存最大的关联值任何一个 case 的关联值都共用这 n 个字节
- 且存在内存对齐,所以占用大小为 n 和 1 的最大值,再结合内存对齐。
- 如果枚举的定义非常简单系统会用1个字节来存放值最大范围是256个 case。
- 枚举定义如果有原始值,也不会影响内存布局。
## switch 的工作原理
```swift
enum Season {
case spring(Int, Int, Int)
case summer(Int, Int)
case antumn(Int)
case winter(Bool)
case unknown
}
var season: Season = Season.spring(1, 2, 3)
switch season {
case let .spring(v1, v2, v3):
print(".spring", v1, v2, v3)
case let .summer(v1, v2):
print(".summer", v1, v2)
case let .antumn(v1):
print(".antumn", v1)
case let .winter(v1):
print(".winter", v1)
case .unknown:
print(".unkown")
}
```
- Swift 先判断 season 的成员值,判断属于哪个 case。
- 如果发现成员值为0则走第1个 case将 season 的前24个字节分别赋值给第1个 case 的 v1、v2、v3
- 如果发现成员值为1则走第2个 case将 season 的前16个字节分别赋值给第2个 case 的 v1、v2
- 如果发现成员值为2则走第3个 case将 season 的前8个字节赋值给第3个 case 的 v1
- 如果发现成员值为3则走第4个 case将 season 的第1个字节赋值给第4个 case 的 v1
- 如果发现成员值为4则走第5个 case则执行第5个 case 的打印逻辑
## Swift 枚举的本质
从表象来看,枚举存在以下情况:
- **不带关联值、不带原始值的基础枚举只占1个字节大小空间且值为默认值**
- **只带有原始值的枚举同样只占用1个字节该字节的值为枚举的位置索引比如1、2而非原始值**
- **带有关联值的枚举内存大小:**
- **对于有不同枚举值有不同关联结构的枚举,其内存由关联值最大的 case 的内存决定(其余的 case 沿用该结构。所有 case 共享同一块内存区域。该区域大小为最大关联值所需内存)**
- **除了最大内存的 case 外还需要1个字节存储所属哪个 case**(通常 1 字节,具体取决于 case 数量1字节能表示的 case 数量为256个
- **枚举总内存实际大小 = 1个字节存储所属哪个 case + 关联值所占内存最大的 case 的内存大小**
- **另外枚举所占内存还需要考虑内存对齐的情况。比如本例中实际内存为25内存对齐为8字节所以最终分配了32字节的内存**
但从本质来讲:
- 对于无关联值、无原始值的简单枚举Swift 编译器会进行内存优化:
- 当枚举 case ≤ 256 个时,使用 1 字节UInt8
- 当 case ≤ 65536 时,使用 2 字节UInt16
- **内存布局的确定性**Swift 要求枚举实例的内存布局在编译时确定。若允许不同 case 复用同一块内存,会导致运行时动态解析内存布局,降低性能和安全性。
- **所有 case 共享同一块内存**:枚举实例的内存大小由**最大关联值的大小 + 标签所需空间**决定。
- **标签Discriminant**:用于区分不同 case通常占用 1 字节(但具体由 case 数量决定)。
**标签的占用大小**由枚举的 `case` 数量决定:
- **1 个 `case`**:不需要标签(因为没有其他可能性)。
- **2~256 个 `case`**:标签通常占用 **1 字节**`UInt8`,可以表示 256 种可能)。
- **257~65536 个 `case`**:标签占用 **2 字节**`UInt16`)。
- 更大数量依此类推(但实际中几乎不会用到如此多的 `case`)。
- **内存对齐约束**:即使标签的逻辑占用小于 1 字节(例如只有 2 个 `case`,理论上只需 1 位),实际仍会占用 **至少 1 字节**(因为内存按字节寻址)。
- **内存对齐**:总大小会按对齐要求(如 8 字节)向上取整到最近的倍数(即 `stride`)。

View File

@@ -1,288 +0,0 @@
# Swift 结构体和类的内存布局
## 结构体初始化器
实验1在 struct 内部自己实现 init
```swift
struct Point {
var x: Int
var y: Int
init () {
x = 0
y = 0
}
}
var point = Point()
```
`init` 方法内第一行处加 断点,如下所示
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftStructMemoryLayoutDemo1.png" style="zoom:25%">
实验2struct 内不自己加 init
```swift
struct Point {
var x: Int = 0
var y: Int = 0
}
var point = Point()
```
`var point = Point()`处加 断点,如下所示
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StructMemoryLayoutDemo2.png" style="zoom:25%">
现象:可以看到加不加自定义初始化器的汇编代码基本相同。
结论:**如果没有为结构体声明初始化器编译器会自动生成1个初始化器。目的是保证所有成员都有初始值。**
## 结构体内存布局
```swift
struct CustomDate {
var year: Int
var month: Int
var isLeapYear: Bool
}
var date = CustomDate(year: 2024, month: 3, isLeapYear: false)
print(MemoryLayout<CustomDate>.size) // 17
print(MemoryLayout<CustomDate>.stride) // 24
print(MemoryLayout<CustomDate>.alignment) // 8
print(Mems.memStr(ofVal: &date)) // 0x00000000000007e8 0x0000000000000003 0x0000000000000000
```
Int 占8 ByteBool 占1 Byte共 2*8 + 1 = 17 Byte由于存在内存对齐所以17向上到24 Byte。
- 值语义:`struct` 是值类型,这意味着当你将一个 `struct` 赋值给另一个变量或传递给函数时,会创建一个新的副本。每个副本都有其自己的内存空间,对其中一个副本的修改不会影响其他副本。
- 内存连续性:`struct` 的成员变量在内存中是连续存储的,没有额外的内存开销(如对象指针或元数据)。这使得访问 `struct` 的成员变量非常高效。
- 内存对齐:为了确保访问效率,编译器可能会对 `struct` 的成员变量进行内存对齐。这意味着某些成员变量之间可能会有未使用的内存空间(填充字节)。这种对齐通常是基于目标平台的硬件架构和访问性能考虑。
- 嵌套结构体:如果 `struct` 包含其他 `struct` 或枚举作为成员,那么这些嵌套的类型也会按照它们自己的内存布局规则进行排列。
- 可变大小结构体:在某些情况下,`struct` 的大小可能不是固定的。例如,如果 `struct` 包含可变长度的数组或字符串,那么它的实际大小将取决于这些成员的大小。然而,即使在这种情况下,`struct` 的内存布局仍然是紧凑的,并且遵循相同的访问规则。
- 与类的比较:与 `class`(类)不同,`struct` 不需要额外的内存来存储类型信息、引用计数或其他元数据。这使得 `struct`通常比 `class` 更轻量级,并且在某些情况下具有更好的性能。
## 类的内存布局
类和结构体类似,但是编译器不会为类自动生成可以传入成员值的初始化器。
```swift
// 1
class CustomDate {
var year: Int = 2024
var month: Int = 3
}
// 2
class CustomDate {
var year: Int
var month: Int
init () {
year = 2024
month = 3
}
}
```
上面2个写法是等价的。
结构体和类的区别:
- 结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
值类型赋值给 var、let 或者给函数传参,是直接将所有内容拷贝一份。产生了全新副本,属于深拷贝。
### 值类型
```swift
func test() {
struct Point {
var x: Int
var y: Int
}
var point1 = Point(x: 10, y: 20)
var point2 = point1
point2.x = 11
point2.y = 22
print(point1.x) // 10
print(point1.x) // 20
print("over")
}
test()
```
因为是在函数内部变量,所以是在栈上分布
| 内存地址 | 内存数据 | 说明 |
| -------- | ---------------------------------- | -------- |
| 0x10000 | 10 ----赋值改变------> 11 | point2.x |
| 0x10008 | 20 ----赋值改变------> 22 | point2.y |
| 0x10010 | 10 | point1.x |
| 0x10018 | 20 | point1.y |
断点打在 `var point1 = Point(x: 10, y: 20)` 处,查看汇编
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftValuePassDemo1.png" style="zoom:25%">
乍一看如果不认识的话,先找字面量(立即数),比如红色框内的 `0xa`,就是 10`0x14` 就是20。[之前](./109.md)学过寄存器的设计64位寄存器是兼容32位寄存器的。红色框内将 `0xa`,也就是 10 保存到 `%edi ` 寄存器内部,也就是保存到 `%rdi` 中,将 `0x14` 也就是20保存到 `%esi` 也就是保存到 `%rsi` 寄存器中。
LLDB 模式下输入 `si` 进入 init 方法内部。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftValuePassDemo2.png" style="zoom:25%">
可以查看到将 `%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-WriteCOW写时复制 技术,这是一种内存优化策略,旨在提升性能并减少不必要的内存复制**
核心思想:
- **延迟复制**:当多个变量引用同一份数据时,它们共享底层存储,直到某个变量尝试修改数据时,才会真正复制一份独立的副本。
- **节省资源**:避免对不可变数据进行冗余复制,减少内存占用和计算开销
仅当有“写”操作时,才会真正执行拷贝操作:
- 对于标准库值类型的赋值操作Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
- 自定义的值类型,比如结构体,在赋值的时候,就会立马发生深拷贝
举个例子
```swift
var array1 = [1, 2, 3]
var array2 = array1 //
array2.append(4) // COWarray1 array2
```
工作过程:
- 赋值时:`array2``array1` 共享同一块内存
- 修改时:当 `array2` 被修改时,检查引用计数。如果引用计数 > 1即存在多个所有者则复制底层存储确保修改不影响其他变量
写操作触发检查机制:
- **修改前检查**:执行写操作(删除、添加、修改)时,检查缓冲区的引用计数
- **唯一性检查**若引用计数为1则直接修改缓冲区否则复制缓冲区并修改新副本
伪代码
```swift
// 伪代码逻辑
mutating func append(_ element: Element) {
if !isUniquelyReferenced(&buffer) {
buffer = buffer.copy() // 复制缓冲区
}
buffer.append(element) // 修改新副本
}
```
#### 什么是缓冲区
Array 结构体(值类型)
+-------------------+
| 指向缓冲区的指针 |-----→ Buffer 类(引用类型)
| | +----------------+
| 其他元数据(长度、容量) | | 存储元素的内存块 |
+-------------------+ | [1, 2, 3, ...] |
+----------------+
1. **结构体轻量级**
`Array` 结构体本身只包含一个指针和少量元数据(如长度、容量),占用固定大小(如 8 字节指针 + 8 字节长度 + 8 字节容量 = 24 字节)。
2. **缓冲区动态分配**
实际存储元素的连续内存块由缓冲区动态分配在堆上,容量可扩展。
3. **共享与复制**
- **赋值时**:仅复制结构体的指针(浅拷贝),多个数组共享同一缓冲区。
- **修改时**:通过 COW写时复制机制仅在需要时复制缓冲区。
### 引用类型
引用赋值给 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()
```
下断点,可以看到下面的汇编:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassReferenceTypeMemoryLayoutDemo1.png" style="zoom:25%">
在调用(汇编的 call完 `allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassReferenceTypeMemoryLayoutDemo2.png" style="zoom:25%">
红色框代表类信息的地址蓝色框代表引用计数绿色框代表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位开始。

View File

@@ -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 ByteBool 占用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))
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassMemoryLayoutExcludeFunction.png" style="zoom:25%">
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassMemoryLayoutExcludeFunction2.png" style="zoom:25%">
代码段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
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClosureCaptureVariableDemo1.png" style="zoom:25%">
简单修改下代码
```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
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClosureCaptureVariableDemo2.png" style="zoom:25%">
可以看到上面有 `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` 下面下个断点
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClosureCaptureVariableDemo3.png" style="zoom:25%">
第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x0000600000210000`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClosureCaptureVariableDemo4.png" style="zoom:25%">
可以看到内存数据发生了改变。绿色框内有了值1。
第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x00006000002042c0`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClosureCaptureVariableDemo5.png" style="zoom:25%">
第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x000060000020d460`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClosureCaptureVariableDemo6.png" style="zoom:25%">
打印结果也说明了问题因为调用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` 处下断点,可以看到下面汇编
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouserMemoryLayoutExploreDemo1.png" style="zoom:25%">
我们知道 `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位。符合猜想。
直奔主题,研究闭包内存
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么LLDB 输入 `si`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreNotCaptureVariableDemo2.png" style="zoom:25%">
可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算2个 `ecx` 异或结果为0写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx``edx` 也就是 `rdx`。走完第6行的汇编继续看第7、8行
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 ` 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。
也就是 fn1 前8个字节存放 plus 的函数地址后8个字节存放0.
继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
基本可以断定函数会返回一个长度为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 `
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。
问题变得微妙起来了,`getFn` 方法返回一个地址占用16个字节但是前8个字节存储 `plus` 方法地址后8个字节存储闭包捕获后在堆上申请内存存放的数据地址。那如何根据一个地址的前8位去真正调用方法
Tips由于地址是动态生成的所以真正去调用 plus 的时候一定不是写死的地址,一定是动态生成的。这种属于间接调用。
- call 后面直接加一个函数地址,属于直接调用。类似 `callq 0x100003970`
- call 后面的函数地址不是固定的,而是动态生成的叫间接调用。在 `at&t` 汇编里面,间接调用前面要加 `*` 。类似 `callq *%rax`,意思是从 `%rax` 里面取出一个地址值出来,当成函数地址,去调用
顺着思路,分析下汇编:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreCaptureVariableDemo1.png" style="zoom:25%">
我们看到25行 `callq *%rax` 存在动态调用,所以需要找到 `%rax` 里的值是哪里来的。然后顺着向上找找到23行 `movq -0x30(%rbp), %rax`。然后继续向上看到16行 `movq %rax, -0x30(%rbp)`。继续向上看到15行 `movq 0x88db(%rip), %rax`。相当于就是在 `rip + 0x88db ` 处取出8个字节出来当作函数地址调用汇编代码的右边写了`fn1` ),地址为:`0x88db(%rip) = rip + 0x88db = 0x10000392d + 0x88db = 0x10000C208` 。
断点继续放开在汇编25行处加断点 `callq *%rax`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreCaptureVariableDemo2.png" style="zoom:25%">
可以看到在方法内部第6行汇编处直接调用一个代码段的函数地址 `jmp 0x1000039f0 `
指令 `jmp`、`call` 的区别在于:
- `jmp` 指令控制程序直接跳转到目标地址执行程序,程序总是顺序执行,指令本身无堆栈操作过程。
- `call` 指令跳转到指定目标地址执行子程序,执行完子程序后,会返回 `call` 指令的下一条指令处执行程序,执行 `call` 指令有堆栈操作过程
LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreCaptureVariableDemo3.png" style="zoom:25%">
`fn1` 函数调用的时候,参数如何传递?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreCaptureVariableDemo4.png" style="zoom:25%">
汇编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 内部
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreCaptureVariableDemo5.png" style="zoom:25%">
可以看到 `movq %r13, %rsi` 将寄存器 r13 的值写入到 `rsi` 了。目前为止:`rdi` 保存参数1`rsi` 保存堆地址值。
继续输入 `si`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreCaptureVariableDemo6.png" style="zoom:25%">
可以看到汇编的第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相加。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClouseExploreCaptureVariableDemo7.png" style="zoom:25%">
可以看到第6行堆地址空间的值写入到 `rbp -0x58` 第26行又将 `rbp -0x58` 写入到 `rdi`29行将 `rdi` 的值,写入到 `rbp - 0x48`34行将 `rbp - 0x48` 写入到 `rcx`35行将 `rcx` 的地址值,写入到 `rax` 对应的存储空间。也就是堆上的 `num` 值已经修改,被覆盖了。
总结:当 `getFn` 内部没有发生闭包的时候fn1 的地址就是16 Byte前8 Byte就是 `plus` 的函数地址。当发生函数闭包的时候,`fn1` 的16 Byte前8 Byte 存储间接调用 `plus` 函数的中转函数后8 Byte 存储着捕获的且在堆上分配内存的地址值。真正调用 `plus` 的时候会通过寄存器传递2个参数1个是 fn1 函数的参数1个是堆空间的地址值。
```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`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftAutoClosureError.png" style="zoom:25%">
正确的做法是:要么加 `@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等特性
- 通常用于作为函数的参数传递
### 总结
- **闭包**是广义概念,包含所有能捕获上下文的代码块(如函数、闭包表达式)。
- **闭包表达式**是闭包的一种具体实现方式,专指用轻量语法编写的匿名闭包。

View File

@@ -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<Season>.size) // 3*8 + 1
print(MemoryLayout.stride(ofValue: season)) // 32
```
`.spring` 有3个 Int单个 Int 占8个字节空间所以红色框代表 spring 的1蓝色框代表 spring 的2绿色框代表 spring 的3黄色框代表枚举的第1个 case剩余7个字节为空。后续的7个字节是为了内存对齐而补齐的内存。
1个字节用来表达位置信息。共 `3*8 + 1 = 25`
内存对齐系数以8 Byte 为单位对象分配的内存必须是该值的整数倍所以实际分配后的内存为32字节
带有原始值的枚举
```swift
enum Season : Int {
case spring = 1
case summer = 2
case antumn = 3
case winter = 4
}
print(MemoryLayout<Season>.size) // 1
print(MemoryLayout<Season>.stride) // 1
print(MemoryLayout<Season>.alignment) // 1
```
只带有原始值的枚举同样只占用1个字节该字节的值为枚举的位置索引比如1、2而非原始值。原始值不占用枚举的内存
总结:
- Swift 的枚举是一种**值类型**,核心目的是表示一组**互斥的、有限的可能性**
- 允许存储属性,当枚举实例处于不同 case 时,这些属性的存在性和内存占用会变得不可预测
### 计算属性
英文名为Computed Property
- 计算属性的本质是方法
- 不占用实例的内存
- 计算属性只能用 var不能用 let
- 有了 setter必须有 getter
- 可以只有 getter没有 setter
- **枚举、结构体、 类都可以定义计算属性**
```swift
struct Circle {
var radius: Int //
var diameter: Int { //
set {
radius = newValue/2
}
get {
2 * radius
}
}
}
var circle = Circle(radius: 10)
// print(circle.diameter) // 20
circle.diameter = 24
// print(circle.radius) // 12
let diameter = circle.diameter
print(MemoryLayout<Circle>.size) // 8
print(MemoryLayout<Circle>.stride) // 8
print(MemoryLayout<Circle>.alignment) // 8
```
计算属性 `y` 等价于下面的代码:
```swift
setDiameter (newValue: Int) {
radius = newValue/2
}
getDiameter () {
return 2*radius
}
```
然后通过汇编来窥探下,在 `circle.diameter = 22` 处加断点,可以看到本质上调用的就是 `setter` 方法,`setter` 内部的实现就不一一窥探了
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftStorePropertySetterDemo1.png" style="zoom:25%">
然后断点继续,在 `let diameter = circle.diameter` 处加断点,可以看到调用了 getter 方法
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftStorePropertySetterDemo2.png" style="zoom:25%">
计算属性的本质就是方法看上去是属性但是不占用结构体的内存。而是独立在代码段中所以只占用1个 Int 即8个字节的大小。
- **计算属性可以有只读计算属性**
也就是只有 getter没有 setter 方法
```swift
struct Circle {
var radius: Int // 存储属性
var diameter: Int { radius*2 }
}
```
### 延迟存储属性
常规写法
```swift
class Car {
init () {
print("Car init")
}
func run () {
print("Car is running")
}
}
class Person {
let car:Car = Car()
init () {
print("Person init")
}
func goOut() {
car.run()
}
}
let p = Person()
print("---")
p.goOut()
// console
Car init
Person init
---
Car is running
```
但是想实现一个需求,就是在 Person 初始化的时候先不初始化 Car当调用 Person 对象的 goOut 方法的时候再初始化,该怎么办?
**使用 lazy 可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化**
对 Person 改造如下
```swift
class Person {
lazy var car:Car = Car()
init () {
print("Person init")
}
func goOut() {
car.run()
}
}
// console
Person init
---
Car init
Car is running
```
注意:延迟属性 lazy 必须和 var 搭配使用,不能是 let
## 异同点
存储属性:
- 类似于成员变量
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举不可以定义存储属性
- 在创建类、结构体的实例时,必须为所有的存储属性设置一个合适的初始值
- 延迟存储属性必须是 `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)
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftEnumRawValueExplore.png" style="zoom:25%">
通过汇编 `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`。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo1.png" style="zoom:25%">
然后看到17行的关键代码LLDB 输入 `si`可以看到在第6行 `movq $0x14, (%rdi)`将16进制的 `0x14` 也就是20移动到指定的内存地址 `rdi` 上
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo2.png" style="zoom:25%">
因为 `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)` 处下断点,查看汇编
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo3.png" style="zoom:25%">
核心思路:方法参数用 `inout`修饰,则传递的是引用(内存地址)。
- 汇编19行 `callq 0x1000030e0 ; SwiftDemo.Shape.girth.getter : Swift.Int at main.swift:16` 调用了 `girth` 计算属性的 `getter``getter` 的返回值存放在寄存器 `rax` 上
- 20行将 `movq %rax, -0x28(%rbp)` 函数返回值 `rax` 存放在 `main` 函数的栈空间上(虽然没有写调用函数,但是代码主入口就是 `main` 函数),且 `rbp` 到 `rsp` 之间都是函数的栈空间。也就是一个局部变量
- 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`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo4.png" style="zoom:25%">
- 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)` 处添加断点,查看汇编
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo5.png" style="zoom:25%">
分析:
- 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`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo6.png" style="zoom:25%">
总结:带有属性观察器的存储属性,如果调用的方法参数是 `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:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypePropertyDemo1.png" style="zoom:25%">
`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
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypePropertyDemo2.png" style="zoom:25%">
`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通过封装访问点插入回调逻辑
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypeProperytyDispatchOnce1.png" style="zoom:30%" />
lldb 输入 si 查看具体实现
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypeProperytyDispatchOnce2.png" style="zoom:30%" />
可以看到底层调用了 `swift_once` 函数函数传递了2个参数 rsi 存储 dispatch_once 的 block 参数rdi 存储了 onceToken
继续敲 si 可以看到底层调用的就是 GCD 的 `dispatch_once` 函数。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypeProperytyDispatchOnce3.png" style="zoom:30%" />
类型属性如何保证线程安全的?如何保证只会初始化一次
底层会调用 `swift_once` 进而调用 `dispatch_once_f``dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
所以类型存储属性底层通过 dispatch_once 保证了只会初始化1次线程安全。

File diff suppressed because it is too large Load Diff

View File

@@ -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<Element>: Stackable {
var elements = Array<Element>()
func push(_ element: Element) {
elements.append(element)
}
func pop() -> Element {
elements.removeLast()
}
func top() -> Element {
elements.last!
}
func size() -> Int {
elements.count
}
}
```
## Swift 泛型本质
```swift
func swapValue<T>(_ value1: inout T, _ value2: inout T) {
(value1, value2) = (value2, value1)
}
var i1 = 11
var i2 = 22
swapValue(&i1, &i2)
var s1 = "Hello"
var s2 = "world"
swapValue(&s1, &s2)
```
在 `swapValue(&i1, &i2)` 处下断点可以看到下面的汇编代码:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftGenericExploreDemo1.png" style="zoom:25%">
可以看到:
- 在第一处调用 `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<T: Person & Runable>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
```
另一种场景是在方法参数是某个泛型且遵循协议后,对其泛型有更多限制,则用 `where` 去处理。如下例子
```swift
protocol Stackable {
associatedtype Element: Equatable
}
class Stack<E: Equatable>: Stackable {
typealias Element = E
}
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2: S2)-> Bool
where S1.Element == S2.Element, S1.Element: Hashable {
return true
}
let s1 = Stack<Int>()
let s2 = Stack<Int>()
let s3 = Stack<String>()
var result:Bool = equal(s1, s2)
print(result) // true
result = equal(s1, s3) // Global function 'equal' requires the types 'Stack<Int>.Element' (aka 'Int') and 'Stack<String>.Element' (aka 'String') be equivalent
print(result)
```

View File

@@ -1,136 +0,0 @@
# Swift 错误处理
```swift
enum SomeError: Error {
case illegalArg(String)
case outOfBounds(Int, Int)
case outOfMemory
}
func divide(_ num1: Int, _ num2: Int) throws -> Int {
if num2 == 0 {
throw SomeError.illegalArg("分母不能为0")
}
return num1/num2
}
func testErrorCapture() {
print("1")
do {
print("2")
print(try divide(20, 0))
print("3")
} catch let SomeError.illegalArg(msg) {
print("参数异常:", msg)
} catch let SomeError.outOfBounds(size, index) {
print("下标越界: size = \(size), index = \(index)")
} catch SomeError.outOfMemory {
print("内存溢出")
} catch {
print("其他异常")
}
print("4")
}
testErrorCapture()
// console
1
2
0
4
```
如果 `try divide(20, 0)` 则输出: 1 、2、参数异常 分母不能为0、4
如果 `try divide(20, 2)` 则输出: 1 、2、10、3、4
说明:如果 do 抛出异常,则作用域内的后续的其他代码都不会被执行
## Error 处理方式
- 通过 do-catch 捕捉 Error
- 不捕捉 Error在当前函数增加 throws 声明Error 继续向上层调用函数抛
```swift
func divide(_ num1: Int, _ num2: Int) throws -> Int {
if num2 == 0 {
throw SomeError.illegalArg("分母不能为0")
}
return num1/num2
}
func safeDivide(_ num1: Int, _ num2: Int) -> Int {
var result:Int = 0
do {
result = try divide(num1, num2)
} catch let SomeError.illegalArg(msg) {
print("参数异常:", msg)
} catch let SomeError.outOfBounds(size, index) {
print("下标越界: size = \(size), index = \(index)")
} catch SomeError.outOfMemory {
print("内存溢出")
} catch {
print("其他异常")
}
return result
}
print("1")
print(safeDivide(8, 2))
print("3")
// console
1
4
3
```
- 如果有些错误不需要向上抛,可以用 `try?` ` try!` 调用可能会抛出 Error 的函数,这样就不需要去处理 Error
```swift
func testIgnoreError () {
print("1")
print(try? divide(10, 0))
print("2")
}
testIgnoreError()
// console
1
nil
2
```
## rethrows
如果一个函数本身不会抛出错误,但某个参数是闭包,且闭包抛出错误,那么函数需要用 `rethrows` 向上抛出错误
```swift
func exec(_ fn: (Int, Int) throws -> Int, _ num1: Int, _ num2: Int) rethrows {
print(try fn(num1, num2))
}
func testCaptureClouserError () {
do {
try exec(divide, 20, 0)
} catch let error {
switch error {
case let SomeError.illegalArgs(msg):
print("参数异常:", msg)
break
case let SomeError.outOfBounds(size, index):
print("下标越界: size = \(size), index = \(index)")
break
case SomeError.outOfMemory :
print("内存溢出")
break
default:
print("其他异常")
}
}
}
testCaptureClouserError() // 参数异常: 分母不能为0
```

View File

@@ -1,381 +0,0 @@
# 剖析 Swift String
带着问题研究下 Swift 中的 String
- 1个 String 变量占用多少内存?
- String 存放在什么位置?
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//MemoryLayout.png)
## 字符串的创建过程
```swift
var str1: String = "0123456789"
```
实验很简单,就一行代码。来窥探下 String 的初始化和内存结构。出发断点看到下面汇编:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//SwiftStringExploreDemo1.png" style="zoom:25%">
简单分析下:
- 第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"
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//SwiftStringExploreDemo2.png" style="zoom:25%">
可以看到其他和上面的没变化,唯一不同的是 `movl $0x5, %esi` 将5赋值给寄存器 `esi`,即寄存器 `rsi`。实验的字符串 "01234" UTF8 字符个数为5
继续改变
```swift
var str1: String = "01234😄"
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//SwiftStringExploreDemo3.png" style="zoom:25%">
可以看到将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位的创建
继续探索:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//SwiftStringExploreDemo4.png" style="zoom:25%">
可以看到 `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))
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//SwiftStringExploreDemo5.png" style="zoom:25%">
分析下:
- 第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个字节
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//SwiftStringExploreDemo6.png" style="zoom:25%">
`字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
### 字符串存储在内存中什么地方
```swift
var str1: String = "0123456789ABCDEF"
```
字符串地址为 ` 0x10000397f + 0x3ea1 = 0x100007820`,看着像全局变量、也有可能是常量区?字符串内容写死的情况下,编译器编译后内存地址应该可以确定,那到底在什么地方呢?利用 `MachOView` 窥探下 `MachO` 文件吧
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//SwiftStringExploreDemo7.png" style="zoom:25%">
利用 MachOView 打开如下
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//MacMachOViewExploreStringLocationDemo1.png" style="zoom:25%">
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
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//MacMachOViewExploreStringLocationDemo2.png" style="zoom:25%">
可以看到:
- 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 OptimizationSSO**
- **条件**:字符串长度 ≤15 个 **ASCII 字符**(或 ≤7 个 **UTF-16 字符**)。
- **特点**:字符串内容直接存储在 `StringObject` 的 16 字节内存中,无需堆分配。有点类似 Objective-C 的 **Tagged Pointer**
- **堆存储Heap-Allocated**
- **条件**:字符串长度超过上述限制(字符串长度 > 15 个 **ASCII 字符** 或 > 7 个 **UTF-16 字符**
- **特点**:字符串内容存储在堆内存。其指针结构是一个 16 字节的 `StringObject``StringObject` 存储堆地址和元数据
#### 内联存储SSO的具体实现
内存布局前8个字节元数据 + 部分字符) + 后8个字节剩余字符 + 填充)
元数据编码:
最低有效位LSB用于标识存储模式:
- **0**:内联存储
- **1**:堆存储
其余位存储字符串长度和编码信息ASCII 或 UTF-16
Demo
````Swift
let str = "Hello" // 5 个 ASCII 字符
内存布局如下:
0x0000000000000a05 // 元数据(长度=5, ASCII, 内联标志位=0
0x48656c6c6f000000 // ASCII 字符 "Hello" 的十六进制表示 + 填充
````
与 Objective-C Tagged Pointer 的区别
| **特性** | **Swift 内联存储 (SSO)** | **Objective-C Tagged Pointer** |
| :----------- | :------------------------- | :------------------------------ |
| **存储位置** | 字符串对象的 16 字节内存中 | 指针值本身64 位) |
| **标识方式** | 元数据的最低有效位 (LSB) | 指针的最高有效位 (MSB) |
| **兼容性** | 需考虑 Unicode 编码复杂性 | 仅支持有限类型(如短 NSString |
| **内存安全** | 完全由编译器管理 | 需运行时特殊处理 |
## 字符串拼接
### 长度小于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`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//MacMachOViewExploreStringLocationDemo3.png" style="zoom:25%">
字符串真实地址:`0x0000600001700440 + 0x20 = 0x0000600001700460`LLDB `x 0x0000600001700460 ` 可以看到字符串拼接后的结果。看上去是存储在堆上。如何验证?
我们知道堆上的内存初始化在 Swift 侧,关键方法为 `swift_allocObject ` -> `swift_slowAlloc` -> `malloc `。给 malloc 下断点,然后在断点出查看调用堆栈,可以看到在 `string.append()` 后有堆分配内存
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//MacMachOViewExploreStringLocationDemo4.png" style="zoom:25%">
结束当前函数调用,在外层可以看到 str2 地址值的后8个字节为 `0x000060000170410`,再 + `0x20` 就是字符串真实地址,打印如下。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//MacMachOViewExploreStringLocationDemo5.png" style="zoom:25%">
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` 数据段可读可写,所以可以修改。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//MachOStubBinderAndLazyBinding.png" style="zoom:25%">
`__stub_helper` 节是 `__TEXT` 段中的一个特定节,它包含了用于处理符号懒加载的辅助函数和代码。当动态链接的符号首次被调用时,这些辅助函数会被触发,以解析符号并将其地址绑定到调用点。
这个过程也叫 `Lazy_binding`。懒加载是一种优化技术,允许程序在启动时不必立即解析和绑定所有动态链接的符号。相反,这些符号的解析和绑定被推迟到它们实际被使用时进行。这种延迟可以减少应用程序启动时的内存和性能开销。

View File

@@ -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(@"文件不可删除");
}
```
* 获取文件信息
![文件信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-07-02%20下午5.58.38.png)
```
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不会做一路创建
![正常创建文件夹成功](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-07-02%20下午7.02.53.png)
![创建文件夹失败](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-07-02%20下午7.07.55.png)
设置一路创建为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<subs.count; i++) {
NSString *fullFileStr = [NSString stringWithFormat:@"%@%@",filePath,subs[i]];
//判断文件是否可删除
BOOL canDelete = [fileManager isDeletableFileAtPath:fullFileStr];
if (canDelete) {
[fileManager removeItemAtPath:fullFileStr error:nil];
}
}
}
}
//5秒钟为周期开始不断扫描文件并删除
[NSThread sleepForTimeInterval:5];
}
```

View File

@@ -1,201 +0,0 @@
# Swift 访问控制
## 访问控制
在访问权限控制这块Swift 提供了5个不同的访问级别(以下是从高到低排列, 实体指被访问级别修饰的内容)
- open允许在定义实体的模块、其他模块中访问允许其他模块进行继承、重写open只能用在类、类成员上)
- public允许在定义实体的模块、其他模块中访问不允许其他模块进行继承、重写
- internal只允许在定义实体的模块中访问不允许在其他模块中访问
- fileprivate只允许在定义实体的源文件中访问
- private只允许在定义实体的封闭声明中访问
绝大部分实现默认都是 internal
## 使用准则
各种 case 如下:
- 变量类型 >= 变量
```swift
fileprivate class Person {
func sayHi() {}
}
internal var p:Person = Person() // Variable cannot be declared internal because its type uses a fileprivate type
class Dog {
func sepak() {
p.sayHi()
}
}
```
编译器报错:`Variable cannot be declared internal because its type uses a fileprivate type`。 访问权限前后冲突了。变量 p 被声明为 internal也就是其他类中也可以使用该对象。但 Person 类被 fileprivate 修饰了,也就是只可以在定义该类的文件中使用。所以会冲突
- 参数类型、返回值类型 >= 函数
- 父类 >= 子类
因为定义的子类如果是 internal而父类是 fileprivate则可以访问到子类的地方访问不到父类这是不合理的。
- 父协议 >= 子协议
协议也可以继承,父协议里的一些属性如果访问级别较低,遵循子协议,实现子协议的类,是无法访问到父协议中的属性的。
- 原类型 >= typealias
```swift
class Person {}
fileprivate typealias People = Person // 编译通过
public typealias People = Person // Type alias cannot be declared public because its underlying type uses an internal type
```
`Person` 默认是 internal所以 typealias 的权限修饰符需要小于等于 internal。
- 原始值类型、关联值类型 >= 枚举类型
```swift
// 编译通过
typealias OwnInt = Int
typealias OwnString = String
internal enum DataKind {
case int(OwnInt)
case string(OwnString)
}
// 编译失败
fileprivate typealias OwnInt = Int
fileprivate typealias OwnString = String
public enum DataKind {
case int(OwnInt) // Enum case in a public enum uses a fileprivate type
case string(OwnString) // Enum case in a public enum uses a fileprivate type
}
```
- 定义类型 A 时用到的其他类型 >= 类型A
- 元祖类型:元祖类型的访问级别是所有成员类型最低的那个(木桶原理)
```swift
internal struct Dog {}
fileprivate class Person {}
internal var data1:(Dog, Person) // Error:Variable cannot be declared internal because its type uses a fileprivate type
fileprivate var data1:(Dog, Person)
private var data2:(Dog, Person)
```
- 泛型类型:泛型类型的访问级别由:类型的访问级别以及所有泛型类型参数的访问级别最低的那个决定
Demo1: 编译器报错:`Variable must be declared private or fileprivate because its type uses a fileprivate type`
分析:
- 泛型的访问级别由最低的类别的访问级别决定。Car 为 internalDog 为 fileprivate所以最低的是 fileprivate所以 Person 类型的访问级别为 fileprivate。因此 Person 类型变量的访问级别为 fileprivate 或者比 fileprivate 更低的访问级别。
- Swift 默认所有的变量、方法、类的默认访问级别为 internal。internal 访问级别高于 fileprivate所以报错。
```swift
internal class Car { }
fileprivate class Dog { }
public class Person<T1, T2> { }
var person: Person<Car, Dog> = Person() // compile error: Variable must be declared private or fileprivate because its type uses a fileprivate type
```
改进下: `fileprivate var person: Person<Car, Dog> = Person()` 和 `private var person: Person<Car, Dog> = Person()` 都不会报错。
- 类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别:
- 一般情况下,类型为 fileprivate、private那么成员、嵌套类型默认也是 private、fileprivate
- 一般情况下,类型为 internal、public那么成员、嵌套类型默认也是 internal
测试:
```swift
// 编译不通过
class TestClass {
private class Person {}
fileprivate class Student : Person {} // Class cannot be declared fileprivate because its superclass is private
}
// 编译通过
private class Person {}
fileprivate class Student : Person {}
```
- 当 fileprivate、private 都写在文件的全局作用域时,访问权限是一样的。
总结:看似规则比较多,实际上就是对「一个实体不可以被更低访问级别的实体定义」的解释。
## getter、setter
- getter、setter 默认自动接收他们所属换的访问级别
- **可以给 setter 单独设置一个比 getter 权限更低的访问级别,用以限制写权限**
```swift
private(set) var num = 10
class Person {
private(set) var age = 0
init(age: Int = 0) {
self.age = age
}
public var weight: Int {
set {}
get { 10 }
}
}
var p = Person(age: 10)
//print(p.age)
//p.age = 20
print(num)
num = 20
print(num)
```
## 初始化器
- 如果一个 public 类,想在另一个模块调用编译生成的默认无参初始化器,必须显示提供 public 的无参初始化器(因为 public class编译器生成的 init 初始化器是 internal。另一个模块是无法访问的
```swift
// APM.dylib
public class APMManager {
public init () {
}
}
// 另一个模块(动态库/静态库/主工程)
var manager = APMManager()
```
## 枚举类型
不能给 enum 的 case 单独设置访问级别,每个 case 自动对齐 enum 的访问级别比如public 的 enum各个 case 也是 public
## 协议
协议中定义的要求自动接收协议的访问级别不能单独设置访问级别比如public 定义的协议,各个属性、方法也是 public 级别)
**协议实现的访问级别 >= 类型的访问级别(协议的访问级别)**
```swift
// 编译通过
public protocol Runnable {
func run()
}
internal class Person : Runnable {
internal func run() { }
}
// 编译失败
public protocol Runnable {
func run()
}
public class Person : Runnable {
internal func run() { } // Method 'run()' must be declared public because it matches a requirement in public protocol 'Runnable'
}
```

View File

@@ -1,644 +0,0 @@
# Swift 内存管理
## 弱引用
Swift 和 OC 都是通过引用计数方式来管理内存的。
Swift 的 ARC 存在3种情况
- 强引用strong reference。默认情况下都是强引用。
当一个强指针离开作用域后,会自动释放对象,调用 deinit 方法。
```swift
class Person {
deinit {
print("Person deinit")
}
}
func test () {
let p: Person = Person()
}
print("1")
test()
print("2")
// console
1
Person deinit
2
```
- 弱引用weak reference。通过 weak 定义弱引用。**必须是可选类型因为实例销毁后ARC 会自动将弱引用设置为 nil**。
```swift
weak var p: Person? = Person()
```
- 弱引用如果被设置为 nil是不会触发属性观察器的 willSet、didSet 方法的
```swift
class Dog {
deinit {
print("Dog deinit")
}
}
class Person {
weak var dog: Dog? {
willSet {
print("willSet")
}
didSet {
print("didSet")
}
}
deinit {
print("Person deinit")
}
}
func test () {
let p: Person = Person()
p.dog = Dog()
print(p)
}
print("1")
test()
print("2")
// console
1
willSet // 这里的触发是 test 方法里,给 person 对象设置了 dog 属性时触发的。但是 weak 指针设置为 nil 的时候没有触发属性观察器
didSet
Dog deinit
SwiftDemo.Person
Person deinit
2
```
换一种写法。可以发现在 init 方法里面,属性观察器 willSet、didSet 是不会触发的。
```swift
class Dog {
deinit {
print("Dog deinit")
}
}
class Person {
weak var dog: Dog? {
willSet {
print("willSet")
}
didSet {
print("didSet")
}
}
init (dog: Dog?) {
self.dog = dog
}
deinit {
print("Person deinit")
}
}
func test () {
let p: Person = Person(dog: Dog())
print(p)
}
print("1")
test()
print("2")
// console
1
Dog deinit
SwiftDemo.Person
Person deinit
2
```
- 无主引用unowned reference。通过 unowned 定义无主引用
- 不会产生强引用,非可选类型。实例销毁后仍然存储着实例的内存地址,类似 OC 的 `unsafe_retained`
- 如果在实例销毁后访问无主引用,会产生野指针错误
weak、unowned 只能用在类实例上。比如:
```swift
protocol Liveavle: AnyObject { }
class Person { }
weak var p1: Person?
weak var p2: AnyObject?
weak var p3: Liveavle?
unowned var p4: Person?
unowned var p5: AnyObject?
unowned var p6: Liveavle?
```
## 循环引用
weak、unowned 都能解决循环引用问题。但是 weak 由于当对象释放后,会把指针设置为 nil。所以 unowned 会比 weak 的性能更好。
- 在生命周期中对象可能会变为 nil推荐使用 weak
- 初始化赋值后再也不会变为 nil 的对象,推荐使用 unowned
## 闭包的循环引用
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftBlockRetainCycle.png" style="zoom:40%">
上面的代码会发生循环引用,会导致局部变量的 p 无法释放(看不到 Person 的 deinit 方法调用)
解法:
- **在闭包表达式的捕获列表声明 weak 或者 unowned 引用,解决循环引用的问题**
因为在闭包里,声明的捕获列表中将 p 用 weak 修饰,所以可以为 nil。p 使用到的地方必须用 `p?.run()`
```swift
class Person {
var fn:(() -> ())?
func run () { print("run") }
deinit { print("deinit") }
}
func test () {
let p = Person()
p.fn = {
[weak p] in
p?.run()
}
}
test()
// deinit
```
另一种写法
```swift
p.fn = {
[unowned p] in
p.run()
}
```
-
```swift
class Person {
lazy var fn:() -> () = {
self.run()
}
func run () { print("run") }
deinit { print("deinit") }
}
```
## @escaping
- 非逃逸闭包、逃逸闭包,一般都是当作参数传递给函数
- 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内
- 逃逸闭包:闭包油可能在函数结束后调用, 闭包调用逃离了函数的作用域,需要通过 `@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)
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftConflictingAccessToMemoryDemo.png" style="zoom:25%">
解决办法就是打破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 都属于元祖,还是同一个内存地址。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftConflictingAccessToMemoryDemo2.png" style="zoom:25%">
如何解决?
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<Person>` 类似于 `const Person *`
- `UnsafeMutablePointer<Person>` 类似于 `Person *`
- `UnsafeRawPonter` 类似于 `const void *`
- `UnsafeMutableRawPonter` 类似于 `void *`
Demo1
因为 changeValue1 的参数是不可变的指针,所以方法内部去修改值,编译器会报错。
```swift
var age = 27
func changeValue1(_ num: UnsafePointer<Int>) {
num.pointee = 28 // compile error: Cannot assign to property: 'pointee' is a get-only property
}
func changeValue2(_ num: UnsafeMutablePointer<Int>) {
num.pointee = 28
}
changeValue1(&age)
print(age)
changeValue2(&age)
print(age)
```
- `changeValue1` 的参数是不可变的指针,所以方法内部去修改值,编译器会报错
- `changeValue2` 的参数是可变的指针,所以方法内部去修改值,编译没问题
- 指针加了泛型,访问真实的值可以通过 `指针.pointee` 去访问
Demo2
```swift
func changeValue3(_ num: UnsafeRawPointer) {
let value = num.load(as: Int.self)
print("value is \(value)")
}
func changeValue4(_ num: UnsafeMutableRawPointer) {
num.storeBytes(of: 30, as: Int.self)
}
changeValue3(&age)
print(age)
changeValue4(&age)
print(age)
// console
value is 27
27
30
```
- `changeValue3` 传递了不可变的原始指针,所以访问内存上的值,需要程序员自己指定访问的数据类型。使用 `指针.load(as: 数据类型.self)` 这种格式
- `changeValue4` 传递了可变的原始指针,所以修改内存上的值,需要程序员自己指定访问的数据类型。使用 `指针.storeBytes(of: 值, as: 数据类型.self)` 这种格式
系统使用场景
```swift
import Foundation
var objcArray = NSArray(objects: 10, 11, 12, 13)
objcArray.enumerateObjects { element, idx, stop in
print("element is \(element) at index \(idx)")
}
print("--------------------")
objcArray.enumerateObjects { element, idx, stop in
if idx == 1 {
stop.pointee = true
}
print("element is \(element) at index \(idx)")
}
element is 10 at index 0
element is 11 at index 1
element is 12 at index 2
element is 13 at index 3
--------------------
element is 10 at index 0
element is 11 at index 1
```
tips 不可以在数组 `enumerateObjects` 方法中使用 break否则编译器会提示 `Unlabeled 'break' is only allowed inside a loop or switch, a labeled break is required to exit an if or do`
## 获取某个变量的指针
`withUnsafePointer` `withUnsafeMutablePointer` 可以获取到不可变、可变的指针
```swift
var age = 27
let pointer = withUnsafePointer(to: &age) { pointer in
let address = UnsafeRawPointer(pointer).load(as: Int.self)
print("Memory address is \(pointer), the value is \(address)")
return pointer
}
var ptr = withUnsafePointer(to: &age) { $0 }
print(ptr)
// console
Memory address is 0x000000010000c208, the value is 27
0x000000010000c208
0x000000010000c208
```
继续添加代码,利用指针修改值,编译器报错 `Cannot assign to property: 'pointee' is a get-only property`
```swift
ptr.pointee = 28 // Cannot assign to property: 'pointee' is a get-only property
```
再修改代码
```swift
var mutablePtr = withUnsafeMutablePointer(to: &age) { $0 }
mutablePtr.pointee = 28
print("mutable address is \(mutablePtr), value is \(age)")
// console
mutable address is 0x000000010000c218, value is 28
```
说明:
```
let pointer = withUnsafePointer(to: &age) { pointer in
return pointer
}
```
等价于下面的写法($0 是第一个参数return 可以省略)
```swift
let pointer = withUnsafePointer(to: &age) { pointer in
return $0
}
let pointer = withUnsafePointer(to: &age) { $0 }
```
那如何获取不可变和可变的 rawPointer
```swift
let rawPointer: UnsafeRawPointer = withUnsafePointer(to: &age) {
UnsafeRawPointer($0)
}
print("Raw pointer address is \(rawPointer), the value is \(rawPointer.load(as: Int.self))")
let mutableRawPointer: UnsafeMutableRawPointer = withUnsafeMutablePointer(to: &age) {
UnsafeMutableRawPointer($0)
}
print("Mutable raw pointer address is \(rawPointer), the value is \(rawPointer.load(as: Int.self))")
mutableRawPointer.storeBytes(of: 28, as: Int.self)
print(age)
// console
Raw pointer address is 0x000000010000c218, the value is 27
Mutable raw pointer address is 0x000000010000c218, the value is 27
28
```
pointer、pointee英语中 er、eeer 表示主动ee 表示被动,分别是:指针、被指向的对象。
上述方式获取的都是指针变量的地址值,而不是堆空间对象的地址值。
## 获取堆空间对象的指针
先获取 `UnsafeRawPointer`,然后利用 `UnsafeRawPointer(bitPattern:**)` 获取堆空间对象的地址值
```swift
class Person {
var age: Int
init(age: Int) {
self.age = age
}
}
var p: Person = Person(age: 27)
var ptr1 = withUnsafePointer(to: p) { UnsafeRawPointer($0) }
var personHeapAddress = ptr1.load(as: UInt.self)
var ptr2 = UnsafeRawPointer(bitPattern: personHeapAddress)
print(ptr2)
print(Mems.ptr(ofRef: p))
```
## 创建指针
创建内存方法1 `malloc`
```swift
import Foundation
var ptr = malloc(16)
print("malloc address is \(ptr)")
// 存
ptr?.storeBytes(of: 10, as: Int.self)
ptr?.storeBytes(of: 20, toByteOffset: 8, as: Int.self)
let firstValue = (ptr?.load(as: Int.self))!
let secondValue = (ptr?.load(fromByteOffset: 8, as: Int.self))!
print("The first part is \(firstValue), second part is \(secondValue)")
free(ptr)
// console
malloc address is Optional(0x0000600000008040)
The first part is 10, second part is 20
```
创建内存方法2 `UnsafeMutableRawPointer.allocate(byteCount: 字节数, alignment: 内存对齐)`
```swift
// 创建
let ptr: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
print("malloc address is \(ptr)")
ptr.storeBytes(of: 10, as: Int.self)
// ptr.storeBytes(of: 20, toByteOffset: 8, as: Int.self)
ptr.advanced(by: 8).storeBytes(of: 20, as: Int.self)
let firstValue = ptr.load(as: Int.self)
let secondValue = ptr.load(fromByteOffset: 8, as: Int.self)
print("The first part is \(firstValue), second part is \(secondValue)")
// 释放
ptr.deallocate()
// console
malloc address is 0x0000000100604370
The first part is 10, second part is 20
```
上面的 `ptr.storeBytes(of: 20, toByteOffset: 8, as: Int.self)` 写法等价于 `ptr.advanced(by: 8).storeBytes(of: 20, as: Int.self)`
创建内存方法3: `UnsafeMutablePointer<Int>.allocate(capacity: 2)` 创建2* 8 Byte 大小的内存
```swift
import Foundation
var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 2)
print("malloc address is \(ptr)")
// 初始化赋值
//ptr.pointee = 27
ptr.initialize(to: 27)
ptr.successor().initialize(to: 10)
// 访问
print(ptr.pointee)
print((ptr + 1).pointee)
print(ptr[0])
print(ptr[1])
print(ptr.pointee)
print(ptr.successor().pointee)
ptr.deinitialize(count: 2)
ptr.deallocate()
// console
malloc address is 0x0000000100604190
27
10
27
10
27
10
```
## 指针之间的转换
`unsafeBitCast` 是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据。类似 C++ 中的 `reterpret_cast`
## 内存泄漏
weak 和 unowned 是两种用于处理引用循环retain cycles的关键字它们主要用在类的属性中以确保对象之间的引用不会导致内存泄漏。这两种引用类型都用于表示对另一个对象的非拥有non-owning引用但它们在行为上有所不同。
weak 引用是一个不持有对象引用的引用。这意味着它不会增加对象的引用计数。当对象不再被拥有时(即其引用计数为 0weak 引用会自动设置为 nil。
weak 引用通常用于避免循环引用特别是在闭包或代理模式中。例如如果你有一个视图控制器ViewController和一个代理Delegate并且 ViewController 持有一个对 Delegate 的强引用那么为了避免循环引用Delegate 通常会对 ViewController 持有一个 weak 引用。
unowned 引用也是一个不持有对象引用的引用,但它不会在对象被释放时自动设置为 nil。因此使用 unowned 引用时需要格外小心,因为如果引用的对象被释放了,而你的代码仍然试图访问它,那么你的程序将会崩溃。
通常,当你确信引用的对象在其生命周期内始终存在时,才会使用 unowned 引用。例如,在一个父对象和子对象的关系中,如果父对象始终在子对象之前存在,并且子对象需要引用父对象,那么子对象可以使用 unowned 引用指向父对象。
## inout 参数访问冲突
```
var step = 1
func increment(_ number: inout Int) {
number += step
}
let rs = increment(&step)
print(rs)
```swift
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInou tReadWriteError.png" style="zoom:25%">
问题本质是:`inout` 关键词代表要对函数的参数进行写操作,而函数内部的实现利用 step 进行反问操作。读写同时存在就有问题。
如何解决?产生一个备份,然后调用函数,最后将备份产生的结果覆盖老值
```
var step = 1
func increment(_ number: inout Int) {
number += step
}
// make an explicit copy
var copyOfStep = step
// invoke
increment(&copyOfStep)
// update the original value
step = copyOfStep
print(step) // 2
```swift

View File

@@ -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
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftChangeLiteral.png" style="zoom:25%">
## 字面量协议
Swift 自带的数据类型基本都可通过字面量初始化,本质原因是遵循了对应的协议
| | | |
| ------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
| Bool | ExpressibleByBooleanLiteral | var b:Bool = false |
| Int | ExpressibleByIntegerLiteral | var num:Int = 2 |
| Float、Double | ExpressibleByIntegerLiteral、ExpressibleByFloatLiteral | var height:Float = 175 <br>var height1:Float = 175.2 <br>var weight:Double = 130 <br>var weight1:Double = 130.1 |
| Dictionary | ExpressibleByDictionaryLiteral | var dic:Dictionary = ["name": "FantasticLBP"] |
| String | ExpressibleByStringLiteral | var name:String = "FantasticLBP" |
| Array、Set | ExpressibleByArrayLiteral | var arr:Array = [1, 2, 3] |
| Optional | ExpressibleByNilLiteral | var o:Optional<Int> = nil |
## 字面量协议的使用
Demo1
```swift
extension Int : ExpressibleByBooleanLiteral {
public init(booleanLiteral value: Bool) {
self = value ? 1 : 0
}
}
var num: Int = true
print(num) // 1
num = false
print(num) // 0
```
Demo2
```swift
class Student: ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByStringLiteral, CustomStringConvertible {
var name: String = ""
var score: Double = 0
required init(floatLiteral value: FloatLiteralType) {
self.score = value
}
required init(stringLiteral value: StringLiteralType) {
self.name = value
}
required init(integerLiteral value: IntegerLiteralType) {
self.score = Double(value)
}
required init(extendedGraphemeClusterLiteral value: String) {
self.name = value
}
required init(unicodeScalarLiteral value: String) {
self.name = value
}
var description: String {
"name is \(self.name), score is \(self.score)"
}
}
var student: Student = "杭城小刘"
print(student) // name is , score is 0.0
student = 100
print(student) // name is , score is 100.0
```

View File

@@ -1,142 +0,0 @@
# Swift 模式匹配
## 模式匹配的底层实现
为了去除其他语句对汇编研究造成的干扰,所以 case 和 default 里面都很简单,聚焦于研究 case 的模式匹配实现。测试代码如下:
```swift
var age = 28
switch age {
case 0...30:
break
default:
break
}
```
`case 0...30:` 处加断点可以看到汇编,然后看到可疑方法 `Swift.RangeExpression.~= infix(τ_0_0, τ_0_0.Bound) -> Swift.Bool`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftPatternMatchExplore.png" style="zoom:25%">
LLDB 输入 `si` 进去窥探下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftPatternMatchExplore2.png" style="zoom:25%">
看样子,函数还没到底,看到地址 `0x00007ff81a86f530` 很大很大,猜测应该是一个系统动态库方法地址,继续跟进去研究 `si`
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MemoryLayout.png)
可以看到内部还有函数调用
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftPatternMatchExplore3.png" style="zoom:25%">
继续跟进 `si`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftPatternMatchExplore4.png" style="zoom:25%">
看上去是在做继续 `clasedRange` 区间符合的判断,继续跟进 `si`,里面确实是在判断是否命中区间的判断。不一一研究了,这次目的是判断 switch...case pattern 的实现。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftPatternMatchExplore5.png" style="zoom:25%">
结论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<Int>, value: Student) -> Bool {
pattern.contains(value.score)
}
static func ~= (pattern: ClosedRange<Int>, value: Student) -> Bool {
pattern.contains(value.score)
}
static func ~= (pattern: String, value: Student) -> Bool {
Int(pattern) == value.score
}
}
var student: Student = Student(score: 55, name: "杭城小刘")
switch student {
case 100:
print(">=100")
case 90:
print(">=90")
case 80..<90:
print("[80, 90)")
case 60...79:
print("[60, 79]")
case "55":
print("斯国一")
default:
print("just so so")
}
// console
```
修改 `var student: Student = Student(score: 78, name: "杭城小刘")` 则输出 `[60, 79]`
```swift
extension String {
static func ~=(pattern: (String) -> Bool ,value: String) -> Bool {
pattern(value)
}
}
func hasPrefix(_ prefix: String) -> (String) -> Bool {
{$0.hasPrefix(prefix)}
}
func hasSuffix(_ prefix: String) -> (String) -> Bool {
{$0.hasSuffix(prefix)}
}
var name = "城小刘"
switch name {
case hasPrefix(""):
print("有杭哦")
case hasSuffix(""):
print("有刘哦")
default:
print("just a name")
}
// console
```
##

View File

@@ -1,590 +0,0 @@
# 从 OC 到 Swift
## OC 与 Swift 混编模式下,方法调用原理探究
OC 与 Swift 混编
`Person.h`
```objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
- (instancetype)initWithCat:(id)cat;
- (void)showPower;
@end
NS_ASSUME_NONNULL_END
#import "Person.h"
#import "TestiOSWithSwift-Swift.h"
@interface Person()
@property (nonatomic, strong) Cat *cat;
@end
@implementation Person
- (instancetype)initWithCat:(id)cat {
if (self = [super init]) {
_cat = cat;
}
return self;
}
- (void)showPower {
NSLog(@"I have a cat");
[self.cat sayHi];
[self.cat run];
}
@end
```
`Cat.Swift`
```swift
import Foundation
@objcMembers class Cat: NSObject {
var name: String
init(_ name: String = "Tom") {
self.name = name
}
func sayHi () {
print("My name is \(name)")
}
func test1(v1: Int) {
print("test1")
}
func test2(v1: Int, v2: Int) {
print("test2")
}
func test2(_ v1: Double, _ v2: Double) {
print("test2 _")
}
func run () {
perform(#selector(test1))
perform(#selector(test1(v1:)))
perform(#selector(test2(v1:v2:)))
perform(#selector(test2(_:_:)))
}
}
```
点击屏幕触发事件,在 `ViewController.swift`
```swift
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var cat: Cat = Cat("屁屁")
var person: Person = Person(cat: cat)
person.showPower()
}
```
问题:
1. 为什么 Swift 暴露给 OC 的类最终要继承自 NSObject
因为在 OC 中,方法消息走的是消息传递,也就是 Runtime 的机制Runtime 的实现依赖于 isa 指针,所以类必须继承自 NSObject。
2. Swift 代码中调用 OC 对象的方法 `person.showPower() ` 底层是怎么调用的?
底层实现还是需要用汇编来验证。断点加在 `person.showPower() ` 处
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo1.png" style="zoom:25%">
可以看到即使在 Swift 代码中,调用 OC 对象方法,本质上还是走 Objc Runtime 的一套流程。50行代码将 showPower 的地址赋值给 `rsi` 寄存器,然后调用 `objc_msgSend` 方法。
LLDB 下 输入 `si` 窥探下实现。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo2.png" style="zoom:25%">
可以看到一个很大的地址 `0x00007ff80002d7c0` 就是动态库的符号方法地址。同时 Xcode 很智能,右侧给出了函数名称。
3. OC 调用 Swift 底层又是如何调用的?在 OC 类 Person 中,底层调用 Swift Cat 类的 sayHi 方法。
断点加在 `[self.cat sayHi]` 处,可以看到本质上还是 Runtime objc_msgSend 那一套。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo3.png" style="zoom:25%">
4. `cat.run()` 底层是怎么调用的?
如果一个 Swift 类,不继承自 NSObject那么方法调用的本质就是走虚表那套逻辑找到指针的前8个字节根据前8个字节找到类信息然后在类信息中前面一些内存地址存储类型信息后续根据偏移在方法列表中找到需要调用的函数地址。类似下面的图。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassPointerDemo2.png" style="zoom:25%">
那 Swift 类继承自 NSObject 后,依然在 Swfit 中调用方法,背后的原理是什么?
在 ViewController.swift 中 `cat.sayHi()` 下断点
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo4.png" style="zoom:25%">
## Swift 方法如何走 Runtime 消息机制
可以看到,即使一个 Swift 类继承自 NSObject但依旧在 Swift 中调用对象方法,本质上还是走虚表那套方法调用流程,不会走 Runtime 消息机制。
如果想让 Swift 方法调用走 Runtime 消息机制,可以在方法前加 `@objc dynamic`
```swift
dynamic func sayHi () {
print("My name is \(name)")
}
```
断点查看,发现在 Swift 代码中调用同样的 Swift 对象方法,此时走了 Runtime 消息机制。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo5.png" style="zoom:25%">
## 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**,编译器生成的代码如下:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC1.png" style="zoom:25%">
class 不仅需要创建对象,还需要访问属性和方法,可以在属性或者方法前加 `@objc`,效果如下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC2.png" style="zoom:25%">
但这样很麻烦,需要给每个属性、方法添加 `@objc`。有简便方法,可以直接在 class 前加 `@objcMembers`,这样该 class 所有的属性、方法都可以在 OC 中访问
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC3.png" style="zoom:25%">
**Swift 写的 extension在`项目名-Swift.swift` 文件中可以看到,是被编译器编译为 OC 的分类 Category**。
```swift
@objcMembers class Car : NSObject {
var price: Double
var band: String
init(price: Double, band: String) {
self.price = price
self.band = band
}
}
extension Car {
func test() {
print("Car test")
}
}
```
```objective-c
@interface Car : NSObject
@property (nonatomic) double price;
@property (nonatomic, copy) NSString * _Nonnull band;
- (nonnull instancetype)initWithPrice:(double)price band:(NSString * _Nonnull)band OBJC_DESIGNATED_INITIALIZER;
@end
@interface Car(SWIFT_EXTENSION(TestSwift))
- (void)test
@end
```
可以通过 `@objc(name)` 重命名 Swift 暴露给 OC 的符号名(类名、属性名、函数名等)
### 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 的方法或者需要换个方法名去调用,该怎么实现?
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ObjcMethodCannotUseInSwift.png" style="zoom:25%">
`- (void)showPower NS_SWIFT_NAME(diaplayPower());` oc 对象方法名,在 Swift 中使用时,想换个名字,可以用 `NS_SWIFT_NAME(新的方法名())`
`- (void)displayPower NS_SWIFT_UNAVAILABLE("请使用 showPower");` oc 对象方法名,不想在 Swift 使用时,可以加 `NS_SWIFT_UNAVAILABLE(原因)`
### 符号名映射
`@_silgen_name` 是 Swift 中用于底层符号控制的工具,适合需要直接操作函数符号的场景
- 符号名称映射
将 Swift 函数直接映射到指定的 C 函数名(或其他语言符号),绕过 Swift 默认的名称修饰name mangling
声明 `@_silgen_name("my_c_function") func mySwiftFunction()` 后,意味着在 Swift 代码中调用 `mySwiftFunction` 会直接链接到 C 函数 `my_c_function` 中
- 与系统 API 或 C 函数交互
直接调用系统库函数或 C 函数,无需通过 Swift 的桥接机制(如 @_cdecl 或 Objective-C 兼容层)。
适用于需要精确控制符号名称的场景(如调用 libc 函数、系统调用等
- 导出 Swift 函数供外部使用
强制 Swift 函数在编译后使用特定名称导出,方便其他语言(如 C、Python通过动态链接调用。
## QA
### 为什么 Swift 暴露给 OC 的类,最终都要继承自 NSObject
什么时候会用到一个类?肯定是抽象一个问题为类吧,那么也一定会访问该类的属性或者方法吧。但在 OC 的世界中,一切皆对象,也遵循 NSObject 的内部布局,也会走 Runtime 的标准流程。
1. OC 运行时依赖
- 必须是 OC 对象Objective-C 的 id 类型指向的对象,本质是 objc_object 结构体,其核心是通过 isa 指针关联到类objc_class
- 必须支持运行时、消息系统的Objective-C 的方法调用依赖运行时动态查找方法实现(通过 `objc_msgSend`),而这一机制需要类继承自 NSObject
如果 Swift 类不继承 NSObject
- 它的实例在内存中缺少 isa 指针,无法被 Objective-C 运行时识别为有效对象。
- Objective-C 代码无法通过 id 类型接收该对象,也无法调用其方法。
2. NSObject 基类提供的基础能力
NSObject 是 Objective-C 的根类,定义了对象的基本行为:
- 内存管理实现引用计数retain/release和 weak 指针、 dealloc 方法
- 运行时元数据:提供 class、respondsToSelector: 等反射方法
- 协议支持:实现 NSObjectProtocol如 isEqual:、hash、description
若 Swift 类不继承 NSObject则无法直接使用这些基础功能导致与 Objective-C 交互时出现兼容性问题。
3. 互操作性的桥梁
- 当 Swift 类继承 NSObject 时,编译器会生成一个 Objective-C 兼容的类结构(包括 isa 指针和元数据)
- 若使用 @objc 修饰非 NSObject 子类,编译器会报错
### `p.run()` 底层是怎么调用的?
Demo1: Swift 调用 Swift 对象方法
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftCallSwiftAssemble.png" style="zoom:30%" />
纯 Swift 环境中,调用对象的方法,走的是虚表的逻辑。最终底层会调用 `callq *0x78(%rax)`
可以看到Swift 调用 Swift 对象和方法断点处显示直接调用方法地址lldb 模式下输入 `si` 可以看到汇编代码停在了 **SwiftDemo`Cat.sayHi():** 的地方。所以走的是虚函数表逻辑。
Demo2: OC 调用 Swift 对象方法
1. Swift 类继承自 NSObject在前面加 `@objcMembers` 暴露给 OC 环境
2. Swift 环境调用 OC 对象和方法
3. OC 方法中调用 Swift 对象和方法
4. 给 OC 环境中,调用 Swift 对象方法的地方下个断点,查看走的是 OC 的 Runtime 还是 Swift 的虚函数表的逻辑
断点截图如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCallSwiftAssemble.png" style="zoom:30%" />
可以看到在 OC 环境中,调用 Swift 对象的方法,本质上走的是 Runtime 的流程,汇编可以看到走的是 `objc_msgSend` 流程,效果类似 `objc_msgSend(p, @selector(run))`
结论OC 类暴露给 Swift 环境后,调用 OC 对象的方法,本质走的是 Runtime 流程。
### 被 @objcMembers 修饰的 Swift 对象,在 Swift 中调用
Demo3: 暴露给 OC 的 Swift 对象,被 Swift 环境调用
1. 继承自 NSObject 的 Swift 类
2.`@objcMembers` 修饰
3. 在 Swift 环境中调用暴露给 OC 的 Swift 对象方法
4. 断点查看方法调用的本质
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftClassExposedToOCThenCalledBySwift.png" style="zoom:30%" />
可以看到,在 Swift 环境中,即使某个 Swift 类暴露给了 OC调用其对象方法的本质依旧是走虚函数表。因为此时用不到 Runtime 的能力
Demo4: 在 Demo3 的基础上swift 方法内部调用 OC 对象方法
也就是说:
1. Cat 类继承自 NSObject`@objcMembers` 修饰
2. 在 Swift 中调用 Cat 对象的 sayHi 方法
3. 在 sayHi 方法内部,调用 OC Person 对象的 run 方法
下断点可以看到:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftClassExposedToOCThenCalledBySwift1.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftClassExposedToOCThenCalledBySwift2.png" style="zoom:30%" />
分为2个阶段
1. 第一阶段:在 Swift 环境调用虽然暴露给 OC 的 Swift 对象方法,但因为没有和 OC 直接交互,所以走的是 Swift 虚函数表逻辑
2. 第二阶段:在 Swift 环境调用 OC 对象方法,因为底层是 OC 方法调用,所以走的是 OC Runtime 逻辑
思考:想让 Swift 方法也走 OC 的 Runtime可以利用 **`dymanic`** 关键词修饰方法。如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftCallSwiftMethodUseDynamic.png" style="zoom:30%" />
## dynamic 的作用
在 Swift 中dynamic 关键字用于强制方法或属性通过 Objective-C 运行时Runtime进行动态派发即使该方法或属性在纯 Swift 代码中被调用。它的核心应用场景与 Objective-C 运行时的动态特性(如 KVO、方法交换、动态解析等紧密相关
核心作用:绕过 Swift 的静态优化
Swift 默认会尝试优化方法派发(如使用虚函数表或直接派发),而 dynamic 会强制方法或属性始终通过 Objective-C 的 objc_msgSend 机制调用,确保动态性。
启用 Objective-C 运行时特性
若需要实现以下功能,必须使用 dynamic
- 键值观察KVO标记为 dynamic 的属性会自动支持 KVO。
- 方法交换Method Swizzling运行时替换方法实现。
- 动态方法解析:通过 resolveInstanceMethod: 动态添加方法实现。
- 消息转发:通过 forwardingTargetForSelector: 或 forwardInvocation: 处理未实现的方法。
### 支持 KVO
Swift 中默认的存储属性不支持自动 KVO 通知,但通过 dynamic 标记属性后,属性访问会通过 Objective-C 运行时,从而触发 KVO 机制。
```swift
@objcMembers class Cat: NSObject {
dynamic var name: String // KVO
init(name: String) { self.name = name }
}
```
在 OC 中监听 name 变化
```Objective-c
Cat *cat = [[Cat alloc] initWithName:@"PiPi"];
[cat addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
```
### 方法交换
若要在运行时替换方法实现(如 AOP 编程、调试 Hook必须确保目标方法是动态派发的。
```swift
@objcMembers class Cat: NSObject {
dynamic func sayHi() { print("Original") }
}
```
在 Objective-C 中交换方法实现
```Objective-c
Method originalMethod = class_getInstanceMethod([Cat class], @selector(sayHi));
Method swizzledMethod = class_getInstanceMethod([Cat class], @selector(swizzled_sayHi));
method_exchangeImplementations(originalMethod, swizzledMethod);
```
### 动态解析未实现的方法
当调用一个未实现的方法时,可通过 resolveInstanceMethod: 动态添加实现。
```swift
@objcMembers class Cat: NSObject {
dynamic func sayHi() { print("Hello") } // 假设此方法未实现,运行时动态添加
}
```
Objective-C 运行时动态解析
```Objective-c
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayHi)) {
class_addMethod([self class], sel, (IMP)dynamicSayHi, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void dynamicSayHi(id self, SEL _cmd) {
NSLog(@"Dynamic Hello");
}
```
### 动态调用
当 Swift 类的方法需要被 Objective-C 或其他动态语言(如通过 performSelector:)调用时,若方法未被标记为 dynamic可能因编译器优化导致动态调用失败。
```swift
@objcMembers class Cat: NSObject {
dynamic func sayHi() { print("Hello") }
}
```
Objective-C 中动态调用
```Objective-c
Cat *cat = [[Cat alloc] init];
[cat performSelector:@selector(sayHi)]; // 需 dynamic 支持
```
## 数据类型转换
在 Swift 和 Objective-C 的类型桥接机制中String 与 NSString 可以互相转换,而 String 不能直接与 NSMutableString 互相转换,但 NSMutableString 可以转为 String。类似的情况也出现在 Array/NSArray/NSMutableArray 和 Dictionary/NSDictionary/NSMutableDictionary 之间
### 核心原因
可变性的语义差异:
- Swift 的值类型String、Array、Dictionary 被设计为不可变的值语义。每次修改会产生新实例Copy on Write。比如 `var str = "A"; str += "B"`
会创建新字符串 'AB',而非修改原内存
- OC 的类型 NSString 是不可变的引用类型NSMutableString 是可变的引用类型,允许直接修改内容
这种差异导致桥接时需要严格处理可变性,确保类型安全和语义一致。
### Swift string 与 OC NSString
双向隐式桥接。因为两者都是不可变的,语义一致,没有副作用,都可以直接互相转换。
```swift
// Swift -> OC
let swiftStr1: String = "Hello"
let nsStr1: NSString = swiftStr1 as NSString
// OC -> Swift
let nsStr12: NSString = "World"
let swiftStr2: String = nsStr12 as String
```
### Swift string 与 OC NSMutableString
Swift string 与 OC NSMutableString 是单向桥接的。OC NSMutableString 可以转为 Swift String。但 Swift String 不能转为 OC NSMutableString
原因OC NSMutableString 是可变类型,转为 Swift String 时会创建一份不可变的副本,避免被 Swift String 修改造成意外修改。
若允许 Swift String 直接转换为 OC NSMutableString则可能通过 OC 代码修改 String 的值,破坏 Swift 值语义。
```swift
// Objective-C → Swift允许
let mutableStr: NSMutableString = "Hello".mutableCopy() as! NSMutableString
let swiftStr: String = mutableStr as String // 隐式桥接,生成不可变副本
// Swift → Objective-C禁止隐式桥接
let swiftStr = "Hello"
let mutableStr = swiftStr as NSMutableString // ❌ 编译错误
```
### 类似情况
- Swift Array 与 OC NSArray 可以互相转换。
- OC 的 NSMutableArray 可以转换为 Swift Array。但是 Swift Array 不能转换为 OC NSMutableArray
### 底层原理
1. 类型桥接的实现方式Swift 编译器通过 `_ObjectiveCBridgeable` 协议实现与 OC 类型的桥接。
例如 String 实现了 `_ObjectiveCBridgeable`,使其能与 NSString 自动转换
2. 可变类型桥接限制
Swift String
- 值类型Swift String 是结构体,遵循值语义。每次赋值或者修改都会生成新的独立副本,确保数据不可变性和线程安全
- 不可变:即使用 var 声明,修改 String 也会通过创建新实例实现,而非直接修改内存
Objective-C 的 NSMutableString
- 引用类型Reference TypeNSMutableString 是类class遵循引用语义。变量持有的是指向内存地址的指针。
- 可变Mutable允许直接修改内存中的内容如追加、删除字符所有持有该引用的代码都会感知到变化。
3. 为什么 String 不能直接桥接为 NSMutableString
- 原因 1值语义与引用语义的冲突
若允许将 Swift 的 String 直接桥接为 NSMutableString则相当于将一个值类型强制转换为可变的引用类型。
风险示例:
```swift
let swiftStr = "Hello"
let mutableStr = unsafeBridgeToNSMutableString(swiftStr) // 假设存在这种桥接
mutableStr.append("!") // 修改 mutableStr
print(swiftStr) // 若桥接是共享内存,此处会输出 "Hello!",违背值语义!
```
这会导致 Swift 的 String 失去其不可变性保证,破坏类型安全。
- 原因 2内存管理的不兼容
Swift 的 String 可能存储在栈内存或静态区(尤其是短字符串),而 NSMutableString 必须分配在堆内存。
直接桥接可能导致内存访问错误(如悬垂指针)。
- 原因 3设计哲学的保护
Swift 强调安全性,禁止隐式共享可变状态。若允许直接桥接,开发者可能无意中在多个地方修改同一块内存,导致难以调试的问题。
QA: 如何显式实现 String → NSMutableString
若需要将 Swift String 转为 NSMutableString必须显式创建新对象而非直接桥接
```swift
let swiftStr = "Hello"
let mutableStr = NSMutableString(string: swiftStr) // 显式拷贝
mutableStr.append("!") // 安全修改
```
在 Swift 和 Objective-C 的互操作中,`String` 和 `NSMutableString` 之间的转换规则是由两者的**类型语义**和**内存管理机制**共同决定的。以下是具体原因和底层逻辑:
---
### **1. 类型语义的根本差异**
#### **Swift 的 `String`**
- **值类型Value Type**
Swift 的 `String` 是结构体(`struct`),遵循值语义。每次赋值或修改都会生成新的独立副本,确保数据不可变性和线程安全。
- **不可变Immutable**
即使使用 `var` 声明,修改 `String` 也会通过创建新实例实现,而非直接修改内存。
#### **Objective-C 的 `NSMutableString`**
- **引用类型Reference Type**
`NSMutableString` 是类(`class`),遵循引用语义。变量持有的是指向内存地址的指针。
- **可变Mutable**
允许直接修改内存中的内容(如追加、删除字符),所有持有该引用的代码都会感知到变化。
---
### **2. 为什么 `String` 不能直接桥接为 `NSMutableString`**
#### **原因 1值语义与引用语义的冲突**
- 若允许将 Swift 的 `String` 直接桥接为 `NSMutableString`,则相当于将一个值类型强制转换为可变的引用类型。
**风险示例**
```swift
let swiftStr = "Hello"
let mutableStr = unsafeBridgeToNSMutableString(swiftStr) // 假设存在这种桥接
mutableStr.append("!") // 修改 mutableStr
print(swiftStr) // 若桥接是共享内存,此处会输出 "Hello!",违背值语义!
```
这会导致 Swift 的 `String` 失去其不可变性保证,破坏类型安全。
#### **原因 2内存管理的不兼容**
- Swift 的 `String` 可能存储在栈内存或静态区(尤其是短字符串),而 `NSMutableString` 必须分配在堆内存。
直接桥接可能导致内存访问错误(如悬垂指针)。
#### **原因 3设计哲学的保护**
- Swift 强调安全性,禁止隐式共享可变状态。若允许直接桥接,开发者可能无意中在多个地方修改同一块内存,导致难以调试的问题。
---
### **3. 为什么 `NSMutableString` 可以转为 `String`**
当 `NSMutableString` 桥接到 Swift 时,**会生成一个不可变的副本**,切断与原对象的关联:
```swift
let mutableStr: NSMutableString = "Hello".mutableCopy() as! NSMutableString
mutableStr.append("!") // 修改原对象
let swiftStr = mutableStr as String // 桥接生成新副本
print(swiftStr) // "Hello!"
mutableStr.append("?") // 继续修改原对象
print(swiftStr) // 仍然是 "Hello!",不受影响
```
- **行为安全**:生成的 `String` 是独立的不可变副本,与原 `NSMutableString` 解耦。
- **符合语义**Swift 的 `String` 仍然是值类型,后续修改不会影响副本。
---
### **4. 如何显式实现 `String` → `NSMutableString`**
若需要将 Swift `String` 转为 `NSMutableString`,必须**显式创建新对象**,而非直接桥接:
```swift
let swiftStr = "Hello"
let mutableStr = NSMutableString(string: swiftStr) // 显式拷贝
mutableStr.append("!") // 安全修改
```
- **显式拷贝**:通过 `NSMutableString` 的构造器生成独立可变对象,避免共享内存。
---
### **5. 类似场景:`Array` ↔ `NSMutableArray`**
同样的规则适用于集合类型:
- **Swift `Array`**
值类型,桥接到 `NSArray`(不可变),但无法直接桥接为 `NSMutableArray`。
- **`NSMutableArray` → `Array`**
生成不可变副本,与原对象解耦。
```swift
let swiftArray = [1, 2, 3]
let mutableArray = NSMutableArray(array: swiftArray) // 显式拷贝
mutableArray.add(4) // 安全修改
```
Swift 通过严格的类型桥接规则,确保值类型的不可变性和引用类型的可控性。这种设计虽然牺牲了部分灵活性,但从根本上避免了数据竞争、意外修改等风险,符合其安全至上的哲学。
| Swift 数据类型 | 单向转换 or 双向转换 | OC 数据类型 |
| -------------- | -------------------- | ------------------- |
| String | <--------> | NSString |
| String | <-------- | NSMutableString |
| Array | <--------> | NSArray |
| Array | <-------- | NSMutableArray |
| Dictionary | <--------> | NSDictionary |
| Dictionary | <-------- | NSMutableDictionary |

View File

@@ -1,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 >>><A, B, C>(_ 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)1V3
- multipleAdd(1)(2) 2V2.
- multipleAdd(1)(2)(3) 3V1V1 + V2 + V3
*/
let rs = multipleAdd(1)(2)(3)
print(rs)
```
参数对应如下图圈选部分
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftFunctionalProgrammingCurrying.png" style="zoom:25%">
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<A, B, C, D>(_ fn: @escaping (A, B, C) -> D) -> ((A) -> ((B) -> ((C) -> D))) {
return { v1 in
return { v2 in
return { v3 in
fn(v1, v2, v3)
}
}
}
}
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<Element>
@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
// Optional<Wrapped>
@inlinable public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
```
跳出语言来看,符合函数式编程的语言都存在**函子** 这一概念,符合下面的形式,都可以叫函子
```swift
func map<T>(_ fn: (Element) -> T) -> Type<T>
```
## 适用函子(Applicative Functor)
对任意一个函子 F如果能支持以下运算该函子就是一个适用函子
```swift
func pure<A>(_ value: A) -> F<A>
func <*><A, B>(fn: F<(A) -> B>, value: F<A>) -> F<B>
```
Array 可以成为适用函子
```swift
func pure<A>(_ value: A) -> [A] {
[value]
}
infix operator <*> : AdditionPrecedence
func <*><A, B>(fn:[(A) -> B], value: [A]) -> [B] {
var resultArray: [B] = []
if fn.count == value.count {
for i in fn.startIndex..<fn.endIndex {
resultArray.append(fn[i](value[i]))
}
}
return resultArray
}
print(pure(10))
var array = [{$0 * 2}, {$0 + 10}, {$0 - 5}] <*> [1, 2, 3]
print(array)
// console
[10]
[2, 12, -2]
```
## 单子
对任意一个类型 F如果能支持以下运算那么就可以称为是一个单子Monad
```swift
func pure<A>(_ value: A) -> F<A>
func flatMap<A, B>(_ value: F<A>, _ fn: (A) -> F<B>) -> F<B>
```
很显然Array、Optional 都是单子
“单子”Monad是一个抽象概念它代表了一种设计模式用于组合计算并管理可能包含副作用的值。单子是一种在函数式编程中用于封装和组合计算的通用结构它允许程序员以一致的方式处理各种复杂的计算情况包括错误处理、异步操作、状态管理等。
单子通常定义了几个操作,这些操作允许你以统一的方式对封装在单子中的值进行操作。这些操作包括:
- `return``pure`:将一个值“提升”到单子中。
- `bind``flatMap`:用于组合单子中的计算,允许你将一个单子的输出作为另一个单子的输入。
在 Swift 中,单子并没有像在其他一些语言(如 Haskell 或 Scala中那样作为语言内建的概念但你可以通过定义自己的类型和方法来实现单子模式。例如Swift 中的 `Optional` 类型可以看作是一个简单的单子,它表示一个值可能存在或不存在。`Optional` 提供了 `map``flatMap` 方法,允许你对封装的值进行链式操作。
其他常见的单子实现包括处理异步操作的单子(如 Promise 或 Future处理错误和异常的单子以及管理状态的单子如 State Monad
单子提供了一种强大的方式来管理复杂性和副作用,使代码更易于理解和测试。然而,它们也增加了抽象层次,可能需要一些时间来适应和理解。在 Swift 中,你可以根据自己的需要选择是否使用单子,以及使用哪种类型的单子。

View File

@@ -1,225 +0,0 @@
# Swift 面向协议编程
## 概念
面向协议编程Protocol Oriented Programming简称 POP
- 是 Swift 的一种编程范式, Apple 于2015年 WWDC 提出
- 在 Swift 的标准库中,能见到大量 POP 的影子
同时Swift 也是一门面向对象的编程语言Object Oriented Programming简称OOP
在 Swift 开发中OOP 和 POP 是相辅相成的任何一方并不能取代另一方POP 能弥补 OOP 一些设计上的不足
## 优势
OOP 三大特性:继承、封装、多态
继承的经典使用场合:当多个类(比如 A、B、C 类)具有很多共性时,可以将这些共性抽取到一个父类中(比如 D 类最后A、B、C类继承 D 类。
但一些情况下,继承并不能解决问题
比如 AVC 继承自 UIViewController 有 run 方法BVC 继承自 UITableViewController有 run 方法。AVC、BVC 有重复代码,如何消除?继承吗?可能会存在菱形继承问题。
菱形继承,也称为钻石继承或多头继承,发生在当一个类从两个或多个具有共同父类的类继承时。这种结构形成了一个菱形的继承图,因此得名。菱形继承可能导致一些问题,主要是二义性和数据冗余。
1. **二义性**:在菱形继承中,子类可能会从多个路径继承同一个基类的方法或属性。这会导致在子类中调用该方法或属性时存在不确定性,编译器或解释器不知道应该使用哪个版本的方法或属性。这种不确定性可能导致难以调试的错误和不可预测的行为。
2. **数据冗余**:菱形继承也可能导致数据冗余。如果子类从多个父类继承了相同的数据成员,那么这些数据成员在内存中会有多个拷贝。这不仅浪费了存储空间,而且可能导致数据不一致的问题,因为对其中一个拷贝的修改不会影响到其他拷贝。
为了解决这些问题一些编程语言提供了虚继承virtual inheritance的机制。虚继承允许子类只继承共享基类的一个拷贝从而避免了二义性和数据冗余的问题。在虚继承中共享基类被视为虚基类子类只通过其中一个父类继承该基类的实现。
然而,虚继承并不是没有代价的。它可能会引入一些性能开销,因为编译器需要处理额外的间接引用和内存布局问题。此外,虚继承也可能使代码更加复杂和难以理解,特别是对于不熟悉该概念的开发者来说。
因此,在使用多继承时,需要谨慎考虑其潜在的问题和代价。在可能的情况下,尽量避免菱形继承结构,并通过其他方式(如接口、组合或委托)来实现所需的功能。如果必须使用菱形继承,那么应该仔细规划类的设计和继承关系,并充分利用虚继承等机制来减少潜在的问题。
采用 POP 实现。
```swift
protocol Runable {
func run()
}
extension Runable {
func run() {
print("running")
}
}
class AVC: UIViewController, Runable {
// ...
}
class BVC: UITableViewController, Runable {
// ...
}
```
## 思想转换
- 优先考虑创建协议,而不是基类
- 优先考虑值类型structenum而不是引用类型class
- 巧用协议的拓展功能 extension Runable { ... }
- 不要为了面向协议而使用协议
## 应用
统计字符串中数字个数
```swift
// 1
var testString: String = "ab1783893cs"
extension String {
var countOfNumber: Int {
var count: Int = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(testString.countOfNumber) // 7
// 2. Swift
struct Counter {
var originalString: String
init(originalString: String) {
self.originalString = originalString
}
var countOfNumber: Int {
var count: Int = 0
for c in originalString where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
extension String {
var counter: Counter {
Counter(originalString: self)
}
}
print(testString.counter.countOfNumber) // 7
```
上述2个方法虽然实现了统计功能但是不够优雅没有那么的 Swift 化。改写如下
```swift
struct MY<Base> {
let base: Base
init(base: Base) {
self.base = base
}
}
protocol MyCompitable {}
extension MyCompitable {
var my: MY<Self> {
set{}
get{ MY(base: self)}
}
static var my: MY<Self>.Type {
set{}
get{ MY<Self>.self }
}
}
```
具体使用的地方
```swift
// String my
extension String: MyCompitable { }
// String.myString().my extension my my
extension MY where Base == String {
func countOfNumber() -> Int {
var count: Int = 0
for c in base where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("I am a static method")
}
mutating func modify() {
print("I am a mutating method")
}
}
print(testString.my.countOfNumber())
String.my.test()
testString.my.modify()
// console
7
I am a static method
I am a mutating method
```
要拓展系统提供的类型,可以按照上述模版进行修改。
- 加前缀 `my` 的目的是防止重复,系统实现是黑盒,如果自己直接提供类似 `testString.countOfNumber` 怕后续系统也提供 `countOfNumber` 方法。所以加前缀 `testString.my.countOfNumber`
QA如果是 NSString、NSMutableString 可以满足需求吗?
答案是不行的。但是可以按照上述方式进行修改调整。怎么修改?按照 `extension MY where Base == String``extension MY where Base == NSString` 的方式写2遍
当然不,找共性,**String、NSString、NSMutableString 都遵循 ExpressibleByStringLiteral 协议**。所以实现 `extension MY where Base: ExpressibleByStringLiteral` 即可。
```swift
// String my
extension String: MyCompitable { }
extension NSString: MyCompitable { }
// String.myString().my extension my my
extension MY where Base: ExpressibleByStringLiteral {
func countOfNumber() -> Int {
var count: Int = 0
for c in (base as! String) where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("I am a static method")
}
mutating func modify() {
print("I am a mutating method")
}
}
var str:String = "123"
print(str.my.countOfNumber())
String.my.test()
str.my.modify()
print("")
var ocStr: NSString = "456" as NSString
print(ocStr.my.countOfNumber())
String.my.test()
ocStr.my.modify()
print("")
var ocMutableStr: NSString = "456" as NSMutableString
print(ocMutableStr.my.countOfNumber())
String.my.test()
ocMutableStr.my.modify()
```
这种设计在 SnapKit 中也存在:
```swift
//
view.my.makeConstraints { ... }
//
UIView.my.registerDefaultConfig()
```
在 RxSwift 中也存在:
```swift
let observable = Observable<Int>.timer(.second(2), period: .second(1), scheduler: MainScheduler.instance)
observable.map{ "\($0)" }.bind(to: priceLabel.rx.text)
```

View File

@@ -1,14 +0,0 @@
# 响应式编程
## 概念
响应式编程Reactive Programming简称RP
也是一种编程范式于1997年提出可以简化异步编程提供更优雅的数据绑定。一般与函数式融合在一起所以也会叫做函数响应式编程Functional Reactive Programming简称 FRP
比较著名的、成熟的响应式框架:
- ReactiveCocoa 简称 RAC有 Objective-C、Swift版本。[官网](http://reactivecocoa.io/) 、[github](https://github.com/ReactiveCocoa)
- ReactiveX 简称 Rx类似一个规范有众多编程语言的版本比如 RxJava、RxKotlin、RxJS、RxCpp、RxPHP、RxGo、RxSwift 等等。[官网](http://reactivex.io/)、[github]( https://github.com/ReactiveX)

File diff suppressed because it is too large Load Diff

View File

@@ -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]
```
开放寻址法

View File

@@ -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 ()<UIGestureRecognizerDelegate>
@property (nonatomic, strong) XLRobotImageView *robotView;
@end
@implementation HLNavigationController
+ (void)initialize
{
[[UINavigationBar appearance] setTitleTextAttributes:
[NSDictionary dictionaryWithObjectsAndKeys:[UIColor whiteColor], NSForegroundColorAttributeName, [UIFont fontWithName:@"Lato-Regular" size:18], NSFontAttributeName, nil]];
[[UINavigationBar appearance] setTranslucent:NO];
NSMutableDictionary *testAttr = [NSMutableDictionary dictionary];
testAttr[NSForegroundColorAttributeName] = [UIColor whiteColor];
testAttr[NSFontAttributeName] = [UIFont systemFontOfSize:18];
[[UINavigationBar appearance] setTitleTextAttributes:testAttr];
testAttr = [NSMutableDictionary dictionary];
testAttr[NSForegroundColorAttributeName] = [UIColor whiteColor];
[[UIBarButtonItem appearance] setTitleTextAttributes:testAttr forState:UIControlStateNormal];
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];
[[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
[[UINavigationBar appearance] setBackgroundImage:[[UIImage alloc] init] forBarMetrics:UIBarMetricsDefault];
[[UINavigationBar appearance] setShadowImage:[[UIImage alloc] init]];
}
- (void)viewDidLoad {
[super viewDidLoad];
[[UINavigationBar appearance] setBarTintColor:GlobalMainColor];
// 设置pop手势的代理
self.interactivePopGestureRecognizer.delegate = self;
self.delegate = self;
}
/**
* 重写这个方法的目的:为了拦截整个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];
}
- (void)back{
[self popViewControllerAnimated:YES];
}
#pragma mark - <UIGestureRecognizerDelegate>
/**
* 这个代理方法的作用:决定pop手势是否有效
*
* @return YES:手势有效, NO:手势无效
*/
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (self.disableGesture) {
return NO;
}
return self.viewControllers.count > 1;
}
- (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];
}
}
@end
```

View File

@@ -1 +0,0 @@
# GCD 源码探索

View File

@@ -1,124 +0,0 @@
# 包管理工具
## CocoaPods
CocoaPods 是非常好用的第三方依赖管理工具它于2011年发布经过这几年的发展已经非常完善。CocoaPods 支持项目中采用 Objective-C 或者 Swift 语言。CocoaPods 会将第三方库的源代码编译为静态库 `.a` 文件或者动态库 `.framework` 文件的形式,并将它们添加到项目中,建立依赖关系。
## Carthage
Carthage 是一个轻量级的项目依赖管理工具。Carthage 主张“去中心化”和“非侵入性”。
CocoaPods 搭建了一个中心库,第三方库被收入到该中心库,所以没有被收录的第三方库是不能使用 CocoaPods 管理的。这就是“中心化”的思想。而 Carthage 没有这样的中心库,第三方库基本都是从 Github 或者私有 git 库中下载的。这就是“去中心化”。
另外CocoaPods 下载第三方库后,会将其变异成静态链接库或者动态框架文件,这种做法会修改 Xcode 项目属性依赖关系,这就是所谓的“侵入性”。而 Carthage 下载成功后会讲第三方库编译为动态框架由程序员自己配置依赖关系Carthage 不会修改 Xcode 项目配置,这就是所谓的“非侵入性”
### 安装
```
brew update
brew install carthage
```
觉得 brew update 更新慢,不想更新的也可以执行 `export HOMEBREW_NO_AUTO_UPDATE=true`。由于经常在终端干活,所以设置了别名。
```shell
# 禁止终端利用 homebrew 安装插件时候的自动更新
alias disableHomebrewUpdate="export HOMEBREW_NO_AUTO_UPDATE=true"
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CarthageInstall.png" style="zoom:25%">
### 使用
- 创建 cartfile 文件 `touch cartfile`
- 修改 cartfile 文件
```
github "Alamofire/Alamofire" "5.0.0-rc.3"
github "onevcat/Kingfisher"
github "SnapKit/SnapKit" ~> 5.0.0
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CarthageUpdate.png" style="zoom:25%">
### 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 里选择版本策略。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftPackageRules.png" style="zoom:45%">

View File

@@ -1,518 +0,0 @@
# 动态调试
## Xcode 调试的原理
Xcode 是电脑端的程序Xcode 使用 LLDB 进行调试。真机连接 Xcode 运行起来,点击屏幕,对应的事件处理方法里加了断点。手机是如何与 Xcode 断点连接同步的呢?
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeDebugWithiPhone.png" style="zoom:25%">
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
### 核心原因
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeDebugWithAnyAppiPhone.png" style="zoom:25%">
上面说了 `debugserver` 只能调试 Xcode 连接安装的程序。这句话不够严谨Xcode 连接 iPhone 的时候,会自动将 `debugserver` 安装到 iPhone 上,但是权限会做收敛。具体表现就是权限 plist。
所以我们可以自行修改权限,重新签名即可:
-`debugserver` 拷贝到电脑上
- 利用 ` ldid -e debugserver > debugserver.entitlements` 命令导出权限文件
- 打开 `debugserver.entitlements` 添加 `get-task-allow``task_for_pid-allow` 2个权限
```shell
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>com.apple.backboardd.launchapplications</key>
<true/>
<key>com.apple.backboardd.debugapplications</key>
<true/>
<key>com.apple.frontboard.launchapplications</key>
<true/>
<key>com.apple.frontboard.debugapplications</key>
<true/>
<key>seatbelt-profiles</key>
<array>
<string>debugserver</string>
</array>
<key>com.apple.diagnosticd.diagnostic</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.private.memorystatus</key>
<true/>
<key>com.apple.private.cs.debugger</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>com.apple.backboardd.launchapplications</key>
<true/>
<key>com.apple.backboardd.debugapplications</key>
<true/>
<key>com.apple.frontboard.launchapplications</key>
<true/>
<key>com.apple.frontboard.debugapplications</key>
<true/>
<key>seatbelt-profiles</key>
<array>
<string>debugserver</string>
</array>
<key>com.apple.diagnosticd.diagnostic</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.private.memorystatus</key>
<true/>
<key>com.apple.private.cs.debugger</key>
<true/>
</dict>
</plist>
```
- 利用 ldid `ldid -S debugserver.entitlements debugserver` 进行重签
自动安装的 `debugserver` 存放目录为 `Device/Developer/usr/bin` 下的,但是这个目录是只读的。我们没办法将重签后的 `debugserver` 拖放到该位置。
但以后的使用场景是,在电脑终端 `sh ~/login.sh` 登录到手机后,在命令行模式下使用 `debugserver AppProcessName`,所以 `debugserver` 就需要安装(拖放 )到 `Device/usr/bin`
此时还是无法使用 `debugserver` ,需要修改权限 `chmod +x /usr/bin/debugserver `
### debugserver 附加到某个 App 进程
`debugserver *:端口号 -a 进程 `
- `*:端口号` :使用 iPhone 的某个端口启动 `debugserver` 服务(只要不是保留端口号就可以)
- `进程`:输入 App 的进程信息(进程 ID 或者进程名称)
比如:`debugserver *:10011 -a Wechat`
### Mac 上启动 LLDB远程连接 iPhone 上的 debugserver
- 启动 LLDB直接在终端输入 `lldb`
- 连接 debugserver 服务:`process connect connect://手机 IP 地址:debugserver 服务端口号 `。其中 `connect://` 代表协议
- 第二步连接成功后iPhone 的进程暂时就暂停了,下断点的状态,此时需要使用 LLDB 的 c 命令让程序先继续运行:`c`
- 接下来就用 LLDB 常规命令调试 App
### 通过 debugserver 启动 App
`debugserver -x auto *:端口号 App 的可执行文件路径`
## LLDB 调试指令
### 指令格式
指令格式为:`<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [arguments [argument...]]`
- `<command>`:命令
- `<subcommand>` 子命令
- `<action>` :命令操作
- `<options>` :命令选项
- `<arguments>`:命令参数
比如给函数 sayHi 设置断点:`breakpoint set -n sayHi`,其中
- breakpoint 是 `<command>`
- set 是 `<action>`
- -n 是 `<options>`
- sayHi 是 `<arguments>`
### help 查看帮助
`help <command> <subcommand>`:用来查看某个指令和子指令 `<command> <subcommand>` 的说明。比如 `help breakpoint set`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBHelpCommand.png" style="zoom:25%">
### expression
`expression <cmd-options> -- <expr>` 用于执行一个表达式
- `<cmd-options>` :命令选项
- `--`:命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,`--` 可以省了
- `<expr>` 需要执行的表达式
比如经常在断点的时候,想额外执行某个函数或者处理某个逻辑,举个例子。在 `touchesMoved` 方法的断点模式下,想修改 view 的背景颜色,此时不需要重新运行。利用 expression 执行指令即可
```swift
(lldb) expression self.view.backgroundColor = .red
```
- `expression`、`expression --` 和指令 print、p、call 的效果一样
- `expression -O --` 和指令 po 效果一样。比如
### 堆栈信息
`thread backtrace` :打印线程的堆栈信息。效果等同于 `bt`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBThreadBacktrace.png" style="zoom:25%">
### 方法返回
`thread return [<expr>]` 让函数直接返回某个值,不会执行断点后面的代码
例如下面的代码,直接将函数的返回值进行修改了
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBThreadReturn.png" style="zoom:25%">
### frame variable
`frame variable [<variable-name>]` 打印当前栈帧变量
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBFrameVariable.png" style="zoom:25%">
### 调试指令
`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个断点。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakPointSetSameSymbol.png" style="zoom:25%">
当有多个方法同名的时候,只对当前类设置断点,指令格式为 `breakpoint set -n "[类名 方法名]"`。
比如通过 `breakpoint set -n "[ViewController add:withB:]"` 对 ViewController 类的 `- (NSInteger)add:(NSInteger)a withB:(NSInteger)b` 方法设置断点
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakPointsSetWithSpecificClass.png" style="zoom:25%">
如果一个方法没有参数,也可以通过 `breakpoint set -n sayHi` 的方式设置断点
#### 函数地址
在逆向,调试别人的 App 的时候我们无法知道函数名称,所以给函数地址打断点就很重要了。
`breakpoint set -a 函数地址`。注意:函数地址是需要处理的,因为 iOS 有 `ASLR 技术`。
#### 正则表达式
`breakpoint set -r 正则表达式`,效果就是给所有函数名符合正则表达式的函数,设置断点。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointSetRegx.png" style="zoom:25%">
#### 动态库
`breakpoint set -s 动态库 -n 函数名`
#### 列出所有断点
`breakpoint list` 用于列出所有的断点,每个断点都有自己的编号
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakPointList.png" style="zoom:25%">
#### 断点的删除、禁用、开启
`breakpoint disable 断点编号` ,比如 `breakpoint disable 2.1 2.2` 禁用了2个断点
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointDisable.png" style="zoom:25%">
`breakpoint delete 断点编号`,比如 `breakpoint delete 2`。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointDelete.png" style="zoom:25%">
比较奇怪断点开启、禁用是可以跟子序号的比如2.1 2.2,而断点删除必须是一级序号
#### 断点指令信息
`breakpoint command add 断点编号`,该指令会给断点预先设置需要执行的命令,到触发断点时,就会按照指令添加的顺序执行。
指令可以添加多个,最后以 "DONE" 结束。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointAddCommand.png" style="zoom:25%">
`breakpoint command list 断点编号` 用于查看该断点下的所有指令
`breakpoint command delete 断点编号` 用于删除该断点下的所有指令
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointCommandList.png" style="zoom:25%">
### 内存断点
在内存数据发生改变时触发
`watchpoint set variable 变量`
在 `viewDidLoad` 中 通过 `watchpoint set variable self->_age` 给 age property 设置了断点。当改变的时候就触发断点
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBWatchpoint.png" style="zoom:25%">
`watchpoint set expression 变量地址`
在 `viewDidLoad` 中 通过 `watchpoint set expression 0x00007fcd20306a60` 给 age property 设置了断点。当改变的时候就触发断点
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBWatchpointExpression.png" style="zoom:25%">
`watchpoint list`
`watchpoint disable 断点编号`
`watchpoint enable 断点编号`
`watchpoint disable 断点编号`
`watchpoint delete 断点编号`
`watchpoint command add 断点编号`
`watchpoint command list 断点编号`
`watchpoint command delete 断点编号`
### image
#### image list
`image list` 列举所加载的模块信息
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageList.png" style="zoom:25%">
`image list -o -f` 打印出模块的偏移地址、全路径
#### image lookup
`image lookup -t 类型`:查找某个类型的信息
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageLookupT.png" style="zoom:25%">
`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。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageLookupAddress.png" style="zoom:25%">
`image lookup -n 符号或者函数名`:查找某个符号或者函数的位置
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageLookupSymbol.png" style="zoom:25%">

View File

@@ -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 apps 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然后转换为 IRIR 经过一些列优化 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。第一种是每次手动运行运行结束后退出 applicationXcode 会产生一个 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利用PGOProfile-Guided Optimization可以实现多种优化其中一些主要优化包括
- 函数内联Function InliningPGO可以根据实际运行时的函数调用情况选择性地内联函数减少函数调用的开销提高程序执行效率。
- 循环展开Loop Unrolling通过分析循环执行次数和循环体内的代码PGO可以决定是否展开循环减少循环控制开销提高循环执行效率。
- 代码重排Code Reordering根据实际运行时的代码执行路径PGO可以优化代码的布局顺序使得频繁执行的代码更容易被CPU缓存命中提高程序的局部性和性能。
- 分支优化Branch OptimizationPGO可以根据实际运行时的分支预测情况优化分支指令减少分支预测错误提高程序的执行效率。
- 常量传播Constant Propagation根据实际运行时的数据流分析PGO可以更好地进行常量传播减少不必要的变量存储和加载操作提高程序的执行效率。
- 内存访问优化Memory Access OptimizationPGO可以根据实际运行时的内存访问模式优化内存访问方式减少内存访问延迟提高程序的内存访问效率。
通过这些优化手段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 参数)。
然而,虽然理论上所有的代码都可以进行重排,但实际上,根据应用程序的特性,可能只有一部分代码是频繁执行的,也就是所谓的"热点"代码。这部分代码会被优先考虑进行重排,因为这样可以最大化性能提升。
另外,重排过程需要考虑到许多限制和约束,如符号之间的依赖关系,这可能会限制哪些符号可以移动和重新排序。如果一些符号因为它们之间的关系而不能被移动,那么这些符号就不会被考虑在重排中。
因此,你看到的"需要重排的符号个数"相对较少,可能是因为只有这些符号是被识别出来的"热点"代码,或者它们是唯一可以在不违反任何约束和限制的情况下被移动的符号

View File

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

View File

@@ -1,234 +0,0 @@
# 框架设计
## 图片框架
### 角色
- Manager
- 内存
- 磁盘
- 网络
- Code Manager
- 图片解码
- 图片压缩/解压缩
### 图片读写过程
- 以图片 url 的 hash 值为 key存储
### 读取过程
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SDWebImageProcess.png" style="zoom:30%" />
### 内存设计
- 内存存储的空间
- 10kb 以下使用场景多设计50张容量
- 100kb 以下使用场景次之设计20张容量
- 100kb 以上使用场景最小设计10张容量
- 淘汰策略
队列实现,先进先出。
### 淘汰策略
LRU最近最久未使用算法。比如3天内没有使用过的则认为需要被淘汰。
### 淘汰时机
- 定时检查,比如 30分钟检查一次
- 提高检查频率:
- 每次进行图片读写时
- 前后台切换时
### 磁盘设计
- 存储方式
- 大小限制如200MB
- 淘汰策略如果某一张图片存储时间距今已经超过7天
### 网络设计
- 图片请求的最大并发量
- 请求超时策略。超时重试1次再次超时则取消
- 请求优先级
### 图片解码
对于不同格式的图片,图片解码怎么处理?
应用**策略模式**,对不同图片格式进行解码。一方面可以解码不同格式、另一个方面替换解码算法,对于稳定性有帮助
在哪个阶段进行解码?
磁盘读取后、网络请求返回后
### 线程处理
## 阅读时长记录器
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ReadTimeCounter.png" style="zoom:30%" />
### 记录器种类
- 页面式:普通的 push、pop 页面
- feed 流式:类似 weibo 这种 feed 流式的记录
- 自定义式:可拓展性的体现,面向未来
QA为什么要有不同类型的记录器
- 基于不同分类场景提供的关于记录的封装、适配
-
### 记录数据存储
- 内存缓存
- 磁盘存储
### 准确性
数据收集存储、上报移除2个核心流程。准确性也和这2个方面息息相关。
- 定时写磁盘 从内存中 flush 到本地磁盘。定时器1分钟 flush 一次
- 限定内存缓存条数。超过该条数即写磁盘。内存记录每满10条 flush 一次
### 上传策略
思考:
- 需要立马上传吗每收集到1次页面阅读时长就需要立马上传1次吗ROI 衡量。性能、线程数
- 关于延时上传的场景有哪些?
上传时机:
- 定时器比如每5分钟上传1次。
- 前后台切换比如从后台切换到前台触发1次上传逻辑
- 无网切换到有网
### 网络上传效率
自定义报文,高效传输。
iOS 小端序,网络大端序。
## 复杂页面架构设计
- MVVM
- Redux 数据流
## 客户端架构
## 业务之间解耦后的通信方式
- openURL
- 依赖注入:中间层
## AFNetworking
### 主要类关系图
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AFNetworkingClassArch.png" style="zoom:30%" />
### AFURLSessionManager
- 创建和管理 NSURLSession、NSURLSessionTask
- 实现 NSURLSessionDelegate 协议代理方法,处理网络请求的重定向、认证、网络数据的处理
- 引入 AFSecurityPolicy用来保证请求安全
- 引入 AFNetworkReachabilityManager 监控网络状态
## SDWebImage
### 架构图
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SDWebImageArch.png" style="zoom:30%" />
## 图片加载流程
- 查找内存缓存
- 查找磁盘缓存
- 网络下载图片并磁盘缓存
## AsyncDisplayKit
### 主要处理问题
主要通过减轻主线程压力,尽量将一些可以放到子线程的任务都放到子线程处理,减轻主线程压力
主要分3方面
- UI 布局 layout文本宽高计算、视图布局计算
- 渲染 Rendering文本渲染、图片解码、图形绘制
- UIKit Objects对象创建、调整、销毁
### 基本原理
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AsyncDisplayKitArch.png" style="zoom:30%" />
- UIView 作为 CALayer 的代理实现。CALayer 作为 UIView 的实例变量,负责展示规工作。
- 针对 UIView 的修改,都抽象为针对 ASNode 的修改,这些修改可以在子线程进行。针对 ASNode 的修改和提交,会对其进行封装,提交到一个全局容器中。
- 对 Runloop 状态进行监听进入休眠前ASDK 执行该 loop 内提交的所有任务。

View File

@@ -1,380 +0,0 @@
# 工程化
## 多环境配置
Project、Target、Scheme 主要管理什么?
- Project包含了项目所有的代码、资源文件所有信息
- Scheme对于指定 Target 的环境配置
- Target对于指定代码和资源文件的具体构建方式
多环境配置的3种方式
- 多 target 配置
- Scheme 多 target 进行环境配置
- xconfig 文件配置
## 多环境配置的不同方式
### 多 Target 的方式
#### 方案
针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。 **多 TargetTargets** 是管理不同应用变体(如免费版/付费版、测试版/生产版、多客户定制版)的高效方式。
所以,**为了区分不同的环境,做一些逻辑的控制。所以需要搭配不同的宏定义,来实现控制逻辑的效果。**
注意duplicate 之后target 虽然多了一份,但是代码和资源不变
#### 关键步骤
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MultipleTargetProjectConfig.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeMacroSupportedWithOCAndSwift.png" style="zoom:30%" />
##### 管理配置文件
1. **独立的 Info.plist**
- 复制原 `Info.plist` 并重命名(如 `Pro-Info.plist`
当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MultiplePListAfterDuplicateTarget.png" style="zoom:30%" />
- 在新 Target 的 `Build Settings``Packaging``Info.plist File` 指定新路径
2. **环境配置分离**
- 创建 `Config-Pro.xcconfig` 文件定义专属配置:
```shell
// Config-Pro.xcconfig
API_URL = https://api.pro.com
APP_NAME = Pro App
```
- 在 Target 的 `Build Settings` → `Base Configuration` 指定配置文件
##### 宏定义
- OCBuild Settings -> Preprocessor Macros 里面的 Debug/Release 模式下添加自定义宏。比如在 debug 模式下 `IsOCDebug = 1`
- SwiftBuild Settings -> Other Swift Flags 里的 Debug/Release 模式下添加自宏定义。注意命名有格式要求:`-D + 宏名称`
#### 思考
该方式还是存在弊端:
- 工程存在多份 info.plist实际上 plist 文件很少改动,所以没有这种需求)
- 配置比较零散、比较乱
### 多 Scheme 的方式
#### 方案
针对多 Target 方案存在的问题,可以用**「多 Scheme + 多 Configuration 」**的方式解决。
#### 关键步骤
##### 创建 Configuration
针对一个 Target 可以添加多个 **Configuration**,步骤如下:
先选中 Project然后在右侧选择 Info 选项卡,在 Configurations Section ,点击 "+" ,即可创建新的 Configuration。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeAddScheme.png" style="zoom:30%" />
创建好之后,该 Target 存在3份 Configuration 了。不同的 Configuration 有什么作用呢?设置宏定义的时候可以针对不同的 Configuration 进行设置。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AddMacroForDifferentScheme.png" style="zoom:30%" />
针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSwitchSchemeManually.png" style="zoom:30%" />
点击 Edit Scheme在 Run 里面选择对应的 Configuration。
但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Configuration。有没有什么办法解决切换问题呢。
##### 创建实体 Scheme
创建 Scheme 步骤Xcode -> New Scheme再弹出的方框内选择对应的 Target然后输入需要创建的 Scheme 名称。此次我们创建了Debug、Beta 2个新的 Scheme。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCreateScheme.png" style="zoom:40%" />
创建好之后,可以看到实体 Configuration 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme然后在 Run 里面 “Build Configuration” 里面选择对应的 Configuration 与之对应。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSchemeMatchWithConfigScheme.png" style="zoom:40%" />
##### plist 暴露自定义字段
1. 创建之后就可以根据 Configuration 设置值,在 `Build Settings -> User-Defined` 下添加自定义的字段,同时可以根据 Configuration 设置不同的值。
2. 设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用。
完整如下图:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SetValueUseDifferentSchemeAndUseViaPlist.png" style="zoom:40%" />
切换不同的 Scheme可以运行不同的效果当前 case 下,选择 Debug Scheme输出不同结果 `HOST_URL: http://www.debug.baidu.com`
#### 思考
目前的方案已经优雅不少,该方式还是存在弊端:自定义宏的时候需要选择不同的 Scheme过程繁琐
### Xcconfig
#### 方案
使用过 CocoaPods 的都会留意到工程存在 `*.Pro.xcconfig` 文件。里面是一些工程相关的配置。所以我们也可以用该方式处理工程问题。
#### 关键步骤
Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,还可以修改 Build Settings 里面的选项。
第一:创建步骤如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCreateXCConfig.png" style="zoom:30%" />
文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig`
几个 Scheme 就创建几个对应的 Xcconfig 文件。
第二:修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations选择对应的 Target然后选择右边对应的 Xcconfig 文件。如下图
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSpecifySchemeWithConfig.png" style="zoom:30%" />
我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`Xcode 切换到 debug scheme 下,然后 Command + B 编译。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png" style="zoom:30%" />
验证结果:
- 编译前切换到 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}`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XccofigValueUseInPlist.png" style="zoom:30%" />
- 代码中使用
```swift
let host = Bundle.main.object(forInfoDictionaryKey: "ServerHost") as! String
let apiKey = Bundle.main.object(forInfoDictionaryKey: "ApiSecretKey") as! String
print("Host: \(host), API Key: \(apiKey)")
```
QA思考一个问题为什么在 `xcconfig` 文件中设置的值,最后会显示在 Xcode 的 Build Settings 的 GUI 面板上?
这便是接下去的内容「Xcode 配置的层级机制」
## Xcode 配置的层级机制
### 层级机制
Xcode 的 Build Settings 是一个**多层叠加系统**,优先级从高到低如下:
**Target 设置 > Project 设置 > .xcconfig 文件 > Xcode 默认值**
Apple 在 [Build Settings Reference](https://help.apple.com/xcode/mac/current/#/itcaec37c2a6) 中明确说明:
> **继承规则**
>
> - Target 设置继承 Project 设置Project 设置继承底层默认值。
> - 若 Target 显式定义某配置项,则覆盖 Project 中的相同项16。
**关键推论**
> Target 作为具体构建目标,其配置需独立于 Project 的通用设置。若二者冲突,**Target 优先级更高**。
根据 [Xcode Build System Guide](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/XcodeBuildSystem/)
> - `.xcconfig` 是**基础配置层**,通过 `baseConfigurationReference` 字段被 Project/Target 引用23。
> - 若 Project 或 Target 显式设置了某值(即使为空),**将覆盖 .xcconfig 中的定义**24。
典型案例:当 `project.pbxproj` 中定义 `OTHER_LDFLAGS = ""`(空字符串)时,它会覆盖 `.xcconfig` 中的非空值,导致链接标志失效
说明: xcconfig 优先级低于前2者。
结论:配置的优先级顺序为:**Target 设置 > Project 设置 > .xcconfig 文件 > Xcode 默认值**
### 为什么 xcconfig 的结果会显示在 Build Settings 中
1. **配置文件的显式声明**
`.xcconfig` 文件是 Build Settings 的合法数据源。通过以下方式关联:
```
OTHER_LDFLAGS = -framework "AFNetworking"
```
Xcode 会将其视为项目配置的一部分,并在 GUI 中显示。
2. **Build Settings 的“继承”特性**
Xcode 的 Build Settings 界面本质是一个**实时计算的合并视图**,它会展示:
- 所有直接通过 GUI 设置的值
- 从 `.xcconfig` 导入的值
- 继承的默认值(如 `$(inherited)`
3. 如果使用 CocoaPods 安装的依赖则会生成2个 CocoaPods 生成的 `.xcconfig` 文件。如果开发者自己再创建 `.xcconfig` 则需要处理2者的逻辑因为 Xcode 一个工程只可以选择一个 `.xcconfig` 文件
### Xcode Project Management Guide
这份[文档](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/XcodeBuildSystem/000-Introduction/Introduction.html#//apple_ref/doc/uid/TP40006904-CH1-SW1)深入讲解了 Xcode 项目结构、Build Settings 的继承关系、环境变量(如 `$(SRCROOT)`)等,适合想系统理解设置机制的人
聊起工程化,不得不查看 CocoaPods 的 [Podfile 配置指南](https://guides.cocoapods.org/syntax/podfile.html)
### 实践验证
#### Demo1
第一步:新建 Xcode iOS 工程。
第二步:新建的工程配置了一份基础的 `Base.xcconfig` 来配置基础的编译信息。`Dev.xcconfig` 包含 `Base.xcconfig` 信息,在此基础上增加了一些编译参数。
`Base.xcconfig` 配置如下:
```shell
BASE_LD_CONFIG = -framework "WantUIKit"
OTHER_LDFLAGS = $(BASE_LD_CONFIG)
```
`Dev.xcconfig` 配置如下:
```shell
#include "Base.xcconfig"
TEMP_LDFLAGS = $(BASE_LDFLAGS) -framework "AFNetworking" -framework "SDWebImage" -framework "PrismClient"
OTHER_LDFLAGS = $(TEMP_LDFLAGS)
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeXcconfigDemo1.png" style="zoom:30%" />
第三步:
- 当前 xcconfig 是为 Dev 模式下设置的。所以项目的 scheme 选择 `Debug` 模式。
- 选中 `PROJECT`,然后在 `Configurations` 下给 `Debug` 配置 `Dev.xcconfig` 文件。
结果:编译工程,可以看到报错了。符合预期
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeXcconfigDemo2.png" style="zoom:30%" />
原因:本 Demo 的目的就是通过 `xcconfig` 文件和继承关系来验证对 Xcode Build Settings 中的 `Other Linker Flags` GUI 面板来验证 xcconfig 及其层级关系会正确影响到最终的编译参数上。
注意:为什么不用 `$(inherited)`
`$(inherited)` 的作用范围:
- 仅继承来自 **Xcode 构建系统层级**的值Target 设置 → Project 设置)
- 不继承 **同一配置文件链** 中通过 `#include` 引入的值
所以此时用**中间变量**的方法。
#### Demo2
验证 `$(inherited)` 的继承效果。
第一步:创建工程,配置 Podfile 文件。Podfile 文件内容如下
```shell
source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' # 清华源
platform :ios, '16.2'
inhibit_all_warnings! # 屏蔽第三方库警告
target 'LDExploreDemo' do
# Pods for InstallDyanmicAndStaticFramework
pod 'SDWebImage'
pod 'AFNetworking'
end
```
第二步:`pod install` 后,可以看到自动生成的 xcconfig 文件内容如下。
为了测试 xcconfig 配置信息的继承,故意把生成的原始信息注释掉。去掉了 `-framework "ImageIO"`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcconfigGeneratedByCocoapods.png" style="zoom:30%" />
第三步:创建 `Base.xcconfig` 文件。引入 Cocoapods 自动生成的 `Pods-LDExploreDemo.debug.xcconfig` 然后声明 `OTHER_LDFLAGS = $(inherited) -framework "ImageIO"`由2部分组成一部分是 `$(inherited)` 一部分是新加的 `-framework "ImageIO"`
第三步:创建 `Dev.xcconfig` 文件。引入第三步创建的 `Base.xcconfig` 文件。声明 `OTHER_LDFLAGS = $(inherited)` 为继承来的配置。
第四步:项目的 `AppDelete.m` 中引入 `#import <AFNetworking/AFNetworking.h>`,然后创建对象并验证证
````objective-c
AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];
NSLog(@"%@", policy);
````
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcconfigInheritedTest.png" style="zoom:30%" />
说明:
- Cocoapods install 后,自动创建链接器所需参数。都在 `Pods-项目名.debug.xcconfig` 配置文件中
- 我们可以自己创建的 `*.xcconfig` 是可以引入自动生成的配置文件的。并在此基础上可以修改。然后在 Xcode Project Configuration 里可以指定为新创建的 xcconfig 文件
- 并且是可以生效的

View File

@@ -1,143 +0,0 @@
# 质量检测
## 静态检测
### 概念
静态分析器是一个 Xcode 内置的工具,使用它可以在不运行源代码的状态下进行静态的代码分析,并且找出其中的 Bug为 app 提供质量保证。
静态分析器工作的时候并不会动态地执行你的代码,它只是进行静态分析,因此它甚至会去分析代码中没有被常规的测试用例覆盖到的代码执行路径。
静态分析器只支持 C/C++/Objective-C 语言,因为静态分析器隶属于 Clang 体系中。不过即便如此,它对于 Objective-C 和 Swift 混编的工程也可以很好地支持。
其中包括了安全、逻辑、以及 API 方面的问题。分析器可以帮你找到以上的这些问题,并且解释原因以及指出代码的执行路径。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerCheckPoint.png" style="zoom:30%" />
### 原理
- 通过语法树进行代码静态分析,找出非语法性错误
- 模拟代码执行路径,分析出 control-flow graphCFG
- 预置了常用的 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。可以帮助检测发现缺少国际化的文本。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerLocalizationIssue.png" style="zoom:30%" />
提示说明 `User-facing text should use localized string macro` 缺少本地化的 API正确的采用下面一行的写法。
#### 逻辑问题
分母不能为0.
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisLogicIssue.png" style="zoom:30%" />
#### 内存问题
虽然 Xcode 默认使用 ARC 管理内存,但是某些 C API 还需要开发者自己进行内存管理。比如 CF 框架下的 API。
以及 block nil 判断等。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisMemoryIssue.png" style="zoom:30%" />
#### 数据问题
在编码过程中一些数据问题可以通过Analyze很好的提示出来。比如下图
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisDataIssue.png" style="zoom:30%" />
#### 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了。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisNSAssertSideEffectIssue.png" style="zoom:30%" />
##### 死循环
下面是一个很常见的死循环的案例,这种稍微复杂一些的逻辑,乍眼一看,似乎没有什么问题:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisEndelessLoopIssue.png" style="zoom:30%" />
这段代码中,是一个二层循环,但是在内层的循环中,没有对 j 做递增,而是做了 result 的递增,这个问题虽然会隐晦,但是新版本的静态检查器会检测出来。
Analyze 功能强大,其实际能检测出的问题会更多。
### 自定义分析器参数
Xcode 也为静态分析器提供了很多的自定义项,方便开发者根据自身工作流进行定制。在 BuildSetting 中通过搜索 `Static Analysis` 关键字,可以筛选出跟分析器相关的设置项。
### 每一次编译都执行静态分析
通过打开 `Analyze During 'Build'` 可以使得每一次编译操作都执行分析器:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerEachBuild.png" style="zoom:30%" />
### 设置静态分析器的运行模式
`Mode of Analysis for 'Analyze'` 可以配置分析器运行的模式Xcode 提供了两种运行模式:
- `Shallow(faster)` `Shallow`规避了去检查一些耗时复杂的检查操作,所以 Shallow 运行的更快
- `Deep` 则进行深入的检查,能抛出更全面的错误
同一个工程,分别看看 Shallow 和 Deep 的耗时差别:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerDeepAndShallowBuildDuration.png" style="zoom:30%" />
### 专项检查配置
静态分析器也提供了一些专项检查的配置,可以根据工程情况定制选择。假设,项目有严格的安全检查,可以打开下图中选中的这些配置项目:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerSecuritySetting.png" style="zoom:30%" />
再或者,如果静态分析器抛出的一些问题不想关注,可以在 Xcode Build Settings 中关闭掉。从而更聚焦于感兴趣、更关注的问题。
### 单个文件的分析
也可以针对单个文件做静态检查。操作路径为Product -> Perform Action -> Analyze "FileName"。
这样只会对单个文件检测,且不会分析 import 进来的文件(可以看到右边的 Person.m 的问题没有被检测出来)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisForSingleFile.png" style="zoom:30%" />

View File

@@ -1,131 +0,0 @@
# AFNetworking 源码解读
## 结构
核心包含5个功能模块
- 网络通信模块(AFURLSessionManager、AFHTTPSessionManger)
- 网络状态监听模块(Reachability)
- 网络通信安全策略模块(Security)
- 网络通信信息序列化/反序列化模块(Serialization)
- 对于iOS UIKit库的扩展(UIKit)
AF 是基于 NSURLSession 来封装的,所以核心类 AFURLSessionManager 也是针对 NSURLSession 做的封装其余的4个模块是为了网络通信请求、响应数据的序列化、HTTPS 安全认证、UIKit 推展)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AFNetworkingProcess.png" style="zoom:60%" />
其中 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;
}
```
初始化都调用到
## 数字证书
数字签名,它能确认消息的完整性,进行认证。
和公钥密码一样也要用到一对公钥、私钥。但相反的是,签名是用私钥加密(生成签名),公钥解密(验证签名)。私钥加密只能有吃有私钥的人完成,而由于公钥是对外公开的,因此任何人都可以用公钥解密(验证签名)。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DataSignProcess.png" style="zoom:30%" />
公钥基础设置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

View File

@@ -1,347 +0,0 @@
# 图形渲染技巧
## GPU、CPU
难道不能直接将数据从 CPU 跨到 GPU 处理?为什么需要多此一举,出现 OpenGL 这个框架?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CPUGPUWithOpenGLBuffer.png" style="zoom:50%" />
数据饥饿从一块内存中将数据复制到另一块内存中传输速度是非常慢的。内存复制数据时CPU 和 GPU 都不能操作数据,避免引起错误。
- 如果 CPU、GPU 同时操作内存,同步和处理非常麻烦,加一些锁或额外手段将造成速度损耗。
- 如果 GPU 处理完处于等待状态
所以加了 buffer 缓冲区来处理该问题。有很多缓冲区,比如颜色缓冲区、深度缓冲区...
## 着色器渲染流程
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLShaderProcess.pngg" style="zoom:50%" />
顶点着色器:有多少个顶点,就执行多少次顶点着色器。
光栅化:将顶点着色器的结果转换为像素。将输入的图元描述,转换为与屏幕对应的位置像素片元。
片元着色器将光栅化的结果转换为颜色。iOS 显示图片的核心原因)
顶点着色器执行次数多还是片元着色器执行次数多?一般来说片元着色器执行次数多。比如三角形,顶点着色器执行三次,有多少个像素点片元着色器就执行多少次。
## 着色器的渲染
- 顶点着色器(必要)
- 细分着色器(可选)
- 几何着色器(可选)
- 片元着色器(必选)
QA
- 什么是管线?
### 什么是可编程管线
可编程管线Programmable Pipeline是一种灵活的渲染流程它允许开发者通过编写特定的程序代码来控制图形渲染的各个阶段。与固定管线相比可编程管线提供了更高的灵活性和控制能力使得开发者能够实现更复杂的图形效果和优化性能。
编程通过 Shading Language 语言(基于 C++)编写,开发者可以控制图形渲染的各个阶段,包括顶点着色器、细分着色器、几何着色器和片元着色器等。这些着色器可以实现各种复杂的图形效果和优化性能。
Apple 的 Metal 中叫 Metal Shading Language简称 MSL 或 Metal 着色语言是苹果公司为其图形和计算API Metal 设计的着色语言。它是一种低级别的编程语言用于编写3D图形渲染逻辑和并行计算核心逻辑。
### 什么是固定管线
固定管线Fixed-Function Pipeline是指一种渲染流程其中图形渲染的各个阶段都是预定义的开发者不能直接控制这些阶段的内部操作
- 顶点处理Vertex Processing顶点坐标转换、光照处理等。
- 图元装配Primitive Assembly将顶点组装成图元如三角形、线段等。
- 光栅化Rasterization将图元转换为像素。
- 片元处理Fragment Processing对每个像素进行颜色、纹理等处理。
- 输出合并Output Merging将处理后的像素输出到帧缓冲区。
固定管线的优点是简单易用,对于初学者来说,可以快速上手进行图形渲染。但是,它的缺点是不够灵活,不能满足高级渲染技术的需求,如自定义着色器、高级光照模型等
### 什么是管线
在OpenGL中管线Pipeline是一个处理图形数据的序列化过程它将顶点数据转换成最终屏幕上的像素。管线分为几个阶段每个阶段对数据进行特定的处理
## 渲染过程中可能产生的问题
### 隐藏面消除
在绘制 3D 的场景时候我们需要决定哪些部分是对观察者可见的哪些部分是对观察者不可见的。对于不可见的部分应该尽早丢弃例如在一个不透明的墙壁后就不应该渲染这个情况叫“隐藏面消除”Hidden surface elimination
#### 解决方案
##### 油画算法(画家算法)
先绘制场景中离观察者较远的物体,再绘制离观察者较近的物体
例如下面的场景中,先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分。即可解决隐藏面消除的问题。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLDrawerAlgorithm.png" style="zoom:30%" />
缺点:
- 需要遮盖的部分,画了多次,造成了渲染性能问题,浪费了资源。
- 叠加的情况,油画算法无法处理。
使用油画算法,只要将场景按照物理距离观察者的距离远近排序。由远及近的绘制即可。 但某些情况下,这个距离无法排序。比如下面的场景:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGlDrawerAlgorithmIssue.png" style="zoom:30%" />
如果三个三角形是叠加的情况,油画算法无法处理。
##### 正背面剔除Face Culling
想象一个 3D 图形你从任何一个方向去观察最多可以看到几个面最多3个从一个立方体的任意位置和方向去看最多可以看到3个面。
思考为什么需要多余的去绘制根本看不到的3个面
如果能以某种方式丢弃这部分数据OpenGL 渲染性能可以提高超过50%。
正背面剔除方案,不仅可以解决隐藏面消除问题,还可以带来性能提升。
如何知道某个面再观察者的视野中会不会出现任何平面都有2个面正面、背面。意味着同一个时刻只能看到一个面。
OpenGL 可以做到检查所有正面朝向观察者的面,并渲染他们,从而丢弃背面朝向的面,这样可以节约片元着色器的开销,提高性能。
核心OpenGL 如何知道绘制的图形中,哪个是正面,哪个是背面?
通过分析顶点数据的顺序。
- 正面:按照逆时针顶点连接顺序的三角形面
- 背面:按照顺时针顶点连接顺序的三角形面
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLFrontAndBackSurface.png" style="zoom:30%" />
用顺时针、逆时针判断正反面不是绝对的,还需要结合观察者的位置。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLFrontAndBackSurfaceWithViewer.png" style="zoom:30%" />
分析:
- 左侧三角形的顶点顺序为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 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测,显示出来的现象是交错闪烁。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLZFightingIssue.png" style="zoom:30%" />
#### 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 <GPUImageInput>`
源对象将静止图像帧上传到OpenGL ES作为纹理然后将这些纹理交给处理链中的下一个对象。
- GPUImage中的一个非常重要的基类 `GPUImageOutput` 和一个协议 `GPUImageInput`。基本上所有重要的 `GPUImage` 处理类都是`GPUImageOutput` 的子类,它实现了一个输出的基本功能
- 所有的 `GPUImage` 处理类也都遵循 `GPUImageInput` 协议。它定义了一个能够接收 frameBuffer 的接收者所必须实现的基本功能。主要包括:
- 接收上一个GPUImageOutput的相关信息
- 接收并处理上一个GPUImageOutput渲染完成的通知
##### Final 环节
| 输出源 | 类型 | 说明 |
| --------------------- | ----------------- | ----------------------------------------------------- |
| GPUImageView | 继承自 UIView | 处理后的图像直接渲染到指定的原生 View 上 |
| GPUImageMovieWriter | 封装AVAssetWriter | 将处理后的视频数据逐帧写入指定路径文件中 |
| GPUImageRawDataOutput | 二进制数据 | 获取出来后纹理的二进制数据,可用于上行推流 |
| GPUImageRawDataOutput | 纹理数据 | 每一帧渲染结束后,通过 texture 属性返回输入纹理的索引 |
### 责任链模式
对 GPUImage 源码的解读可以看到GPUImage 采用了责任链设计模式来实现链式处理。
GPUImage 定义了一个 GPUImageOutput 类和一个 GPUImageInput 协议,实现了 GPUImageInput 协议的对象具备接收 frameBuffer 纹理输入并进行处理的能力。而继承自 GPUImageOutput 的对象,则可以将处理后的输出纹理传递到下一个 filter。
输入源 input 继承自 GPUImageOutput可以将图片、视频等数据上传到 frameBuffer 后传递到 GPUImageFilter 中处理。最后一个 filter 处理完成后,将数据传递到了实现 GPUImageInput 协议的输出源 Output 中进行上屏绘制或者上行推流。上下游链路的打通。

View File

@@ -1,144 +0,0 @@
# 自定义URL Schemes
1、引言
URL Schemes 应用在 iOS 上已经很久了。对于使用者来说,在沙盒机制下的 iOS 中,如果想做到一定程度上的自动化就不可避免地要用到 URL Schemes。但因为 URL Schemes 的使用方式不像传统 iOS 使用者接触到的图形界面那样可以直观地点来点去,造成了对它有兴趣的人(尤其是对英文有恐惧的人)一定程度上理解的困难。
2、简介苹果的沙盒机制
苹果选择沙盒来保障用户的隐私和安全,但沙盒也阻碍了应用间合理的信息共享,于是有了 URL Schemes 这个解决办法。
一般来说,我们使用的智能设备上有许多我们的个人信息。比如:联系方式、银行卡/信用卡信息、支付宝/Paypal/各大商城的账户密码、照片甚至行程与位置信息等。
如果说,你设备上的每一个应用,不管是官方的还是你从任何商城安装的应用都可以随意地获取这些信息,那么你轻则收到骚扰信息和邮件、重则后果不堪设想。如何让这些信息不被其它应用随意使用,或者说,如何让这些信息仅在设备所有者本人知情并允许的情况下被使用,是所有智能设备与操作系统所要在乎的核心安全问题。
在 iOS 这个操作系统中,针对这个问题,苹果使用了名为「沙盒」的机制:应用只能访问它声明可能访问的资源。一切提交到 App Store 的应用都必须遵守这个机制。
在安全方面沙盒是个很好的解决办法,但是有些矫枉过正。敏感的个人信息我们不愿意透露,却不代表所有的信息我们都不想与其它应用共享。
比如说我们要一次性地(没错,只按一次)把多个事件放到日历中,这些事件包含日期时间以及持续时间等信息,如果 App 之间信息不能沟通,就无法做到这点。(在下文中的 x-callback-URL 的部分会详述整个过程)
类似于一次性添加多个日历事件这样的,我们在使用智能设备的过程中会遇到很多不必要的重复的步骤。大多数人对这些重复的步骤是不自觉的,就像当自己电脑里有一批文件需要批量重命名的时候,他们机械地重复着重命名的过程。但是当我们掌握了这些设备运行的模式,或者有了一些工具,我们就能将这些重复的步骤全部节省下来。在 iOS 上,我们可以利用的工具就是 URL Schemes。
3、URL Schemes 是什么
Custom URL scheme 的好处就是你可以在其它程序中通过这个url打开应用程序。如应用程序注册了一个url scheme:myApp, 那么就在mobile浏览器中就可以通过<href=myApp://>打开你的应用程序A。
对比网页url就比较好理解url scheme。给出一个url “http://bxu2359670321.my3w.com/view/login.php”它的格式protocol :// hostname[:port] / path / [;parameters][?query]#fragment
因此这个url的protocol就是http。对比URL Scheme给出例子“weixin://dl/moments“前面的weixin:就代表微信的scheme。你可以完全按照理解一个网页的 URL ——也就是它的网址——的方式来理解一个 iOS 应用的 URL。即Scheme是**://**之前的那段字符
###注意###
1、所有的网页都有url但未必所有的应用都有自己的 URL Schemes更不是每个应用的每个功能都有相应的 URL Schemes
2、一个网址只对应一个网页但并非每个 URL Schemes 都只对应一款应用。这点是因为苹果没有对 URL Schemes 有不允许重复的硬性要求
3、一般网页的 URL 比较好预测,而 iOS 上的 URL Schemes 因为没有统一标准,所以非常难猜,通过猜来获取 iOS 应用的 URL Schemes 是不现实的。我推荐将Bundle identifier反转
###上干货###
1、注册自定义 URL Scheme
1注册自定义 URL Scheme 的第一步是创建 URL Scheme — 在 Xcode Project Navigator 中找到并点击工程 info.plist 文件。当该文件显示在右边窗口,在列表上点击鼠标右键,选择 Add Row:
![注册url scheme](/assets/2287777-e22f24acf7823cfa.png)
2点击左边剪头打开列表可以看到 Item 0一个字典实体。展开 Item 0可以看到 URL Identifier一个字符串对象。该字符串是你自定义的 URL scheme 的名字。建议采用反转Bundle idenmtifier的方法保证该名字的唯一性
![](/assets/2287777-67f09fb472c6b87d.png)
3点击 Item 0 新增一行,从下拉列表中选择 URL Schemes敲击键盘回车键完成插入。注意 URL Schemes 是一个数组,允许应用定义多个 URL schemes。展开该数据并点击 Item 0。你将在这里定义自定义 URL scheme 的名字。只需要名字,不要在后面追加 ://
![新增scheme](/assets/2287777-b9c1d5245529fa1b.png)
2、拿浏览器做简单验证
在地址栏中熟入自定的url scheme。此时必须保证该浏览器所在设备上已经安装了具有自定义url scheme的App。
![浏览器检验 urlscheme](/assets/2287777-93cc952da314d7bf.PNG)
3、新建Xcode工程做个App试试看这里我就放一个Button点击打开url
代码。
```
- (IBAction)open:(id)sender {
NSString *url = @"zhunaer://?name=lbp&age=22";
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:url]]) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
}else{
NSLog(@"打不开");
}
}
```
结果打不开,为什么?
因为在新建App的plist中没加query schemes
```
<key>LSApplicationQueriesSchemes</key>
<array>
<string>zhunaer</string>
</array>
```
4、如果需要在2个App之间传值怎么办可以用URL Scheme解决。
在被打开的App的Appdelegate.m中实现-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation
```
-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation{
NSLog(@"calling application bundle id: %@",sourceApplication);
NSLog(@"url shceme:%@",[url scheme]);
NSLog(@"参数:%@",[url query]);
if ([sourceApplication isEqualToString:@"com.geek.test1"]) {
return YES;
}
return NO;
}
```
在需要打开第三方App的点击事件处的url处后面加上参数类似NSString *url = @"zhunaer://?name=lbp&age=22";
注意在URL Scheme后加?然后跟网页的url的参数一样写法。
5、如何判断是指定App打开或者某些App不让打开我们的App
做了实验。
A在需要打开第三方App的工程中将Bundle identifier改为“com.geek.test2”,其余不变
B在被打开的App的AppDelegate.m中
```
-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation{
NSLog(@"calling application bundle id: %@",sourceApplication);
NSLog(@"url shceme:%@",[url scheme]);
NSLog(@"参数:%@",[url query]);
if ([sourceApplication isEqualToString:@"com.geek.test1"]) {
return YES;
}
return NO;
}
```
![测序](/assets/2287777-5ddf86e7d30b1c05.png)
###实验结果###
依旧可以打开App即使断点走入Return NO
结论:如果你想阻止其它应用调用你的应用,**创建一个与众不同的 URL scheme**。尽管这不能保证你的应用不会被调用,但至少大大降低了这种可能性。
参考https://sspai.com/post/31500#01

View File

@@ -1,264 +0,0 @@
# Aspects
> Aspects 核心原理涉及3个技术点
>
> - objc_msgForward(触发消息转发机制)
> - NSInvocation
> - block 的本质
## 函数指针、指针函数的区别
定义不同:
- 函数指针:本质是一个指针,该指针指向一个函数
- 指针函数:本质是一个函数,函数的返回值是一个指针类型
写法不同
- 函数指针:`int (*fun)(int x,int y)`
- 指针函数:`int* fun(int x,int y)`
用法不同:
- 函数指针
```c++
typedef int (*FuncPtr)(int, int);
int add(int a, int b) {
return a + b;
}
FuncPtr addPtr = add;
int result = addPtr(3, 2);
```
- 指针函数
```c++
int (*getAddFunction())(int, int) {
return add;
}
int add(int a, int b) {
return a + b;
}
int (*addPtr)(int, int) = getAddFunction();
int result = addPtr(3, 2);
```
## block 本质
block 详细探索步骤请查看这篇文章 [block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)。接下去查看建议版本的分析。
第一步:编写一个基础 block
第二步:用 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m -o ViewController-arm64.cpp` 转成 c++ 查看原理
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsBlockExplore.png" style="zoom:30%" />
分析:可以发现 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");
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectMockBlock.png" style="zoom:30%" />
思考:我们目前已经用自定义的 struct 来承接了 block 并成功执行了。能否用 NSInvocation 来触发 block
## NSInvocation 触发 block
一个方法需要成功调用并执行需要3要素
- 方法名称 `SEL`
- 方法签名(参数个人、参数类型、返回值类型等信息) `Method Type Encoding`
- 方法地址、方法实现 `IMP`
如何从自定义的 block 结构体中获取这些信息呢?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsProcess.png" style="zoom:60%" />
AspectsIdentifier每做一次方法交换都会转换为一次 AspectsIdentifier。是核心逻辑。
以一个例子作为源码探索切入口,点击跳转到源码中
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsIdentifierModule.png" style="zoom:30%" />
可以看到给 NSObject 添加分类,核心 2 个 API。一个对象方法、一个类方法
```objective-c
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
```
内部都走到 `aspect_add` 方法中。其中都会生成 `AspectIdentifier` ,看看是如何生成的?
第一步:生成 block 签名。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsBlockSignature.png" style="zoom:30%" />
第二步:因为我们通过 AOP 给原始方法添加了 block最后的效果是既可以调用原始方法又可以调用 block 添加的代码。实现的前提是什么?
比较 block 和 hook 类的方法的签名信息需要 Match。具体逻辑查看注释。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsSignatureCompare.png" style="zoom:30%" />
```shell
(lldb) po blockSignature
<NSMethodSignature: 0x600003829c20>
number of arguments = 2
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?'
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (@) '@"<AspectInfo>"'
flags {isObject}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
conforms to protocol 'AspectInfo'
```
OC 方法签名和 block 方法签名是有区别的
- 比如在 Aspects 框架中block 方法签名的参数个数是比 oc 方法参数个数少的。oc 方法自带 `id self, SEL _cmd` 2个参数。
- 都使用相同的类型编码系统,但是 block 签名可能包含额外的信息,例如捕获的变量类型。比如上面的 block 方法签名的最后一个参数 `'@"<AspectInfo>"'`,其中的 `AspectInfo` 就代表捕获的变量类型。
比如2个不带参数的 OC 方法签名和 block 方法签名:
- oc 方法签名:`v@:` = `v` + `@` + `:`,返回值 `void`、参数1 `@` 代表对象、参数2 `` 代表 SEL 类型
- block 方法签名:`v@?` = `v` + `@?`,返回值 `void`、参数1 `@?` 代表既是对象,又是 block
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectInvokeBlock.png" style="zoom:30%" />
## objc_msgForward
骚操作:
- 将待 hook 的方法,和 `objc_msgForward` 进行交换。 `objc_msgForward` 不管对象有没有实现,都会触发消息转发流程
- 此时会走 Runtime 的 NSObject `forwardInvocation` 流程。且 Aspects 将 `forwardInvocation` 方法指向了 `__ASPECTS_ARE_BEING_CALLED__` 方法。
经历这么一波处理hook 最后都守口到了 `__ASPECTS_ARE_BEING_CALLED__` 方法中。
前面研究过了 `AspectIdentifier` 的逻辑。接下去继续看看后续步骤。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsHookForwardInvocation.png" style="zoom:30%" />
可以看到内部执行 `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:`。
回归头继续看下面的逻辑。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsHookMethodWithObjcMsgForward.png" style="zoom:30%" />
`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 使用的一个经典库,处理好核心逻辑后,也做了一些黑名单、线程安全等的保护。也有一些类似日志回放功能的处理。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsProcessXmind.png" style="zoom:30%" />

View File

@@ -1,46 +0,0 @@
# LLDB
## LLDB 架构
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLDBArchitecture.png" style="zoom:40%" />
说明:类似一个 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
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLDBWorkflow.png" style="zoom:40%" />
说明:
- **lldb-server**:调试服务端,`lldb-server` 作为“调试代理”,直接操控目标程序。分为两种部署方式:
- **Host 端**:调试本地程序(如 macOS 进程)。
- **Remote 端**:部署到目标设备(如手机/嵌入式设备),直接控制被调试程序
- 通信层TCP + GDB RSP 协议
- **TCP Socket**:物理传输通道,连接 Host 的 LLDB 和 Remote 的 lldb-server
- **GDB RSP (Remote Serial Protocol)**
- **基于 ASCII 的调试协议**(明文消息,如 `$m<addr>,<length>#<checksum>` 读取内存)
- 历史原因:兼容 GDB 的远程协议,使 LLDB 能对接各类设备Android gdbserver 等)
QA好像有点反人类设计手机上反而是 Server电脑端的 LLDB 反而是 Client也就是为什么必须让手机充当 Server
**权限问题**
- 手机操作系统(如 iOS禁止外部进程直接访问 App 内存。
- 只有手机本地的 `lldb-server` 可通过系统权限(如 `task_for_pid()`)操控目标进程。

File diff suppressed because it is too large Load Diff

View File

@@ -1,461 +0,0 @@
# AI 对端上的赋能
## 一、实时特征回流
传统智能的问题、弊端是什么?
![MobilePhone AI Capture User Behavior Data](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-DataCapture.png)
- 推荐系统需要收集用户的行为,将这些行为作为用户的意图表征,表征他的偏好,回流到服务端。
- 服务端拿到这个数据,在发起一次实时请求的时候,会根据用户的行为特征,去商品池里面召回一批用户喜欢的商品,再返回给端上,给用户做展示
- 同时,这个用户的行为数据,还会作为训练模型的一个样本
![MobilePhone AI Disadvantage](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-Disadvantage.png)
可以发现传统的推荐系统存在一些瓶颈:
- 实时性不足
行为数据回流的时效性,会影响算法对于用户意图变化的感知,会影响推荐的准确性。
现有的系统,在手淘这种亿级用户体量下面,想要做到用户数据的实时回流,技术上面挑战很大。
另外涉及到用户隐私方面的风险考虑,以及在服务端的存储瓶颈相关的考虑,是不能把用户所有的行为都回流到云端
- 数据丰富度有限
在服务端,整个用户行为数据的丰富度非常有限的。
同时我们的一次推荐内容的更新,也是受限于一次新的请求时机的发起。
即使我们发现用户的意图,通过数据发现了意图的变化,但是也很难实时对用户的前台界面去做一次干预,去及时调整推荐的内容
- 但用户算力/存储瓶颈
- 千人一面的模型
目前服务端的算法模型更多的还是千人一面。受到算力和存储的瓶颈,很难针对每一个用户去建立一个属于他自己的模型。
去做更加精准的预测。
针对上面的这些问题,就是端上的智能可以去发挥的空间所在。
前面说过,用户的数据回流时效性会影响推荐的准确度。那么是不是可以把用户的特征、用户的数据,做到最实时的回流?
这里,我们做了一些这样的尝试:
![MobilePhone AI Capture User Behavior Data Flow](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-UserDataFlow.png)
- 首先可以把用户的最原始的行为数据回流。比如用户在逛手淘的过程中产生的一些浏览、点击等行为回流到服务端
- 同时,也可以把用户的这些原始行为数据做一定的聚合,生成一个信息量含量更高,但是数据量更小的用户特征数据(数据聚合)
比如把用户在商品详情页的一系列的特征,聚合成用户对商品详情的更精炼的一些数据,比如用户有没有对商品点击过收藏、有没有点击过加购,聚合成
一条商品详情的浏览特征
- 也可以对这个特征继续做精加工处理,变成一个算法模型可直接使用的特征向量。当然了,它也可以表征用户的行为和意图
- 还可以把这个向量做进一步精加工处理,生成一些用户的意图打分。比如用户对于某个商品详情的意图分,是强还是弱,用分数去表明。
比如用户在手淘里面浏览的过程当中,用户逛着逛着是不是不感兴趣了?是不是疲劳了?这也可以用来表征用户的一个“跳失意图”。
这几种数据都可以回流到服务端的,图上数据可以看到:
- 从左往右:数据的加工度越来越高的、数据量是越来越少的
- 从右往左:数据量大,信息密度低
回流到服务端的时候对于这4种不同类型的数据一一做过尝试
- 首先,如果直接回流用户的原始数据,那么这个数据量会非常大,服务端的存储存在压力。另一方面也会涉及到用户的隐私风险
- 其次,我们也尝试过,将用户的行为数据聚合成一条向量直接回流到服务端,数据量虽然小了,但是会丢失一些信息。另外向量这种数据格式,通用性会非常的受限。智能针对特定的模型去回流特定的向量。
- 另外,也尝试过直接回流用户的意图分,比如回流一条对于商品详情页的意图分,在整个手淘的流失的意图,这个数据是有效的,但是它丢失的信息也是非常多的
所以最后选择的是将用户的数据在端上做一定的标准的加工化处理,聚合成特征回流到服务端,这是实践过比较好的,既能保证数据的有效性又能保证实时性的一种方式。
### 1. 实时特征回流
- 数据本地加工,做标准化处理,然后按照需要将需要的那部分数据回传到服务端
解决了什么问题?
- 提升了数据的丰富度,能够在服务端拿到用户更多的、更细粒度的一些行为数据。能够让服务端的数据输入变得更丰富
- 通过对数据加工之后,建立一条实时的特征回流通道,保证了从端到云上的数据实时性
遗留了什么问题?
- 通过实时的回流方式,解决了用户实时感知的在实时性方面的问题
- 但即使感知到了用户意图发生了变化,,也缺少一个实时在前台去干预用户的方式
### 2. 信息流的“回退推荐”
针对实时特征无法具备实时干预能力的问题,在信息流方面做了一种叫“回退推荐的策略”
想象这样一个场景:用户在商品列表页,对某个商品感兴趣,点击某个商品到达详情页,在详情页看了一番之后,用户点击了收藏或者加入到购物车了。有收藏、加购行为表示用户对这个商品是很感兴趣的。
此时,从商品详情页到回退到外面这个商品列表页的时候,会根据用户刚刚的浏览、加购行为,推荐一个相似的商品。
![MobilePhone AI Goods Recommended when page back](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-PageBackRecommended.png)
会根据用户刚刚浏览的这些行为,去给他推荐一个相似的商品。这个过程会发生很多行为,通过滚动、曝光等行为可以推测用户在信息流的浏览意图是逐渐从逛切换到了买。
如果页面回退,我们不做干预的情况下,他可能继续往下去浏览,购买这个行为的意图差“临门一脚”就可以转换为一次成交购买。如果不加以干预,可能逛着逛着就丧失购买意愿了,流失一笔潜在交易。
选择的策略是:在页面回退的时候,在原来的商品卡片周围,立即推荐一个相似的商品,希望能继续促成,希望能够留住他刚刚这次对商品购买的强意图。这个就是回退推荐。
本质上就是抓住了用户从逛到买的这个强意图聚焦。针对强意图的聚焦,做了一次成交转换的促成。
商品卡片的回退推荐策略落地上线后,效果是非常好的。比普通商品卡片的转换率高五六倍左右。
![MobilePhone AI Goods Recommended Issue when page back](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-PageBackRecommendedIssue.png)
存在什么问题?
类似程序员和产品设计沟通出的一种机制,可以理解为命令式编程,是人为先验地找到了一个能够代表用户意图的时机,也就是在回退时机。这种时机靠人去发现梳理,往往是很难覆盖全面的。依赖于对于用户强意图的梳理、选择。那么有没有一种方式可以自动的去预测用户意图的变化?
## 二、应用场景
### 1. 信息流的端侧重排
在信息流上面做了另外一个尝试:在本地进行了一次端上的重排。
![MobilePhone AI Relayout](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-reLayout.png)
一个用户在逛信息流的过程中,会产生滚动、曝光、点击、加购、收藏、停留、回退、滚动这些行为。从用户的实时的行为序列中,其实是表征了用户背后的一个隐式的意图表达。
可以把用户的实时行为序列去输入到一个意图的模型当中,去计算用户当前的意图是什么?他偏好哪些、不喜欢哪些?得到用户的正负一些意图反馈。去判断用户当前是不是正在一个疲劳的状态,去计算他即将要跳失的可能性。
然后将这些用户意图,输入到一个本地的实时决策模型中去,去决定接下去要给这个用户去做什么事情。例如用户是不是对于接下来要滚动浏览的商品兴趣是不是发生了变化的时候,能够根据用户的实时意图,去做一次实时的调整,永远把用户最喜欢的内容放在他排序更靠前的位置。
或者当发现对这批商品都不感兴趣的时候,就理解去重新更新一次商品。或者当发现用户即将跳失之前,去做一些强干预,去挽留他继续留在这个页面上。比如通过一些权益去做挽留。
做完决策后,就可以将这个决策结果通知到前台,去做相应的响应。
还存在一些问题:
![MobilePhone AI Relayout Issue](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-reLayoutIssue.png)
决策选择还是受限于产品策略。程序员还需要和产品去约定设计产品策略。开发和产品共同约定,在用户的某一时机之下,接下来对应的一个处理是什么。它的整个呈现形式和所处业务域还是存在紧密关系的。
这种应用形式,应用在信息流上是非常好的,但是很难迁移到其他业务域。
### 2. 智能 Push
在这样的背景之下,接下来开始下一个尝试,去做一个更加通用的、端上的智能应用。去 Push 业务。
传统业务上Push 是服务端发起的。服务端存在一个任务,不断计算:我需要给什么样的一批用户、推送一批什么内容。服务端跑了这个任务后,会圈选一批人群,定一个任务,给这些人要去发一个推送。客户端收到这个推送,展示这个推送的消息内容。
服务端发起的 Push 有啥问题?
- 缺乏感知能力,难以精细化运营
服务端不知道用户当前 App 的状态,用户在手淘内还是不在。没有办法知道用户的实时状态,很难针对用户的实时状态去做精细化运营。
- 被动触达,错失最佳营销时机
更希望的是针对用户的某一个精细化行为,去做一次响应的时候,服务端 Push 是做不到的。
完整流程:
![MobilePhone AI Push](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-Push.png)
用户进入手淘后,会不断收集端上的行为(滚动、曝光、点击、加购、收藏、停留)数据,然后会把行为数据输入到另一个意图模型中,去判断他当前的一些实时意图。不断的根据行为做分析。也会输入到另一个决策模型中去,但这个(智能 Push场景下的决策模型和前面的端上重排场景的决策模型是不一样的。这个决策模型会**判断用户当前的状态适不适合接受一次干预**,或者适不适合接受某一次营销的推荐。当我们发现用户当前处于一个相对空闲的状态,这个时机更适合接受一条干预的时候,我们就会把这个信号通知到服务端。这时候服务端在海量的内容中去筛选出一条对应着用户当前的意图,有效的一条信息,再推送给客户端。
这种从客户端发起的 Push 推送相比于服务端推送来说,对于用户当前的实时状态有着非常强的感知的,用户发生的任何一个行为在端上的决策模型中,可以以毫秒级的速度获取到,当我们真的需要去对用户做一次精准的干预的时候,这种方式相比于服务端推送来说,是有着非常大的优势的。
- 从“平台视角”向“用户视角”的转变
传统的服务端推送是站在平台视角,来去筛选内容、筛选用户去发送消息的。而“端智能 Push”更多站在用户视角去分析用户在什么时机下适合接受干预。
- 解决了什么问题
对端上单用户的算力空间的充分利用;同时智能 Push 分离了用户的感知决策。用户的感知可以作为一个独立的模块存在。用户的决策:接下来要做什么响应。也是一个比较独立的模块。在应用性上具备初步的可移植性。可以在多个应用场景,不只是信息流这样一个较为垂直的业务域上去使用。
- 没有解决什么问题?
决策依然需要先验制定。
我们对于 AI 的期待是美好的,期望 AI 可以帮助我们决定下一步做什么。然而通过这些案例可以发现,现阶段,我们还是只能做到在一个已经决策好的产品框架下面去做。是需要先有一系列的决定(在什么样的情况下面,可以有哪些响应),那么 AI 是帮助这样一个决策的结果更加精准。
### 3. 智能预加载
根据用户身份、角色、常见行为路径,预测接下去要使用的功能,对可能要进入的页面进行预热、预加载,在用户访问这个页面之前,把页面准备好,来做到秒开的效果。
也就是说,如果能精准预测用户下一步将要去往哪里,对于性能来说,提升是会非常明显的。
那么最关键问题就是:**如何预测用户下一步将要去往哪里**
### 4. 手势热点识别
判断用户热点的操作区域是什么?针对这块区域来做一些定制化的特定推荐。
### 5.智能营销投放
用算法的更加精准的预测,去替代 以往的业务规则的人群圈选。
对于端智能来时,它属于基础能力。它用在哪里,才是能不能用好的一个关键,也就是业务价值能不能提升。
从过去的应用来说,我们认为端智能对于客户端的改变主要体现在:
- 更多的数据
在端上做算法模型的预测,可以拿到用户在端上更富丰富、更细粒度的数据,去避免在服务端取不到这样丰富数据的缺陷。
- 更实时的响应
相比于服务端,很多系统称具有小时、分钟、秒级别的实时响应,在端上的实时性是带来本质性的变化。
- 更低的消耗
闲置算力去运算和存储资源,带来本质上的实时性的提升。同时也节约了服务端资源的消耗。
## 三、端智能整体架构
要素:算法、数据、调度框架、运行环境
架构如下:
![MobilePhone AI Arch](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAIArch.png)
1. 围绕着端上的算法分为2种模型
- 用户意图计算模型:不断分析用户当前所处状态
- 决策模型:会根据用户意图计算的结果,去判断下一步要做什么样的处理。比如一次本地重排、还是去做一次数据的重新刷新
2. 作为端上的算法输入,建立了一个端上的特征中心,用来提供给端上的算法使用:提供标准的用户行为数据、以及一些特征服务。
3. 还建立了端上的用户决策框架:接受用户的每条行为数据,然后根据这些行为去决定接下来什么时机要去唤起一个什么样的模型。拿到这个模型的响应结果后再一路回传到我们的客户端应用层。应用层根据这个结果来做前台界面上的渲染。
4. 围绕着端上的算法所在的执行环境,是在底层有一个端计算的容器,提供 Python 的运行环境以及 MNN 轻量级的推理引擎
5. 对于整个算法研发的 workflow配套的做了一个端计算的一体化研发平台。算法同学在这个平台完成开发到发布、再到 AB 实验以及模型训练的一系列工作。
模型从这个平台发布后,是会下发到客户端,然后在端上跑。
### 1. 端上算法方案
![MobilePhone AI Algorithm](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-AlgorithmModel.png)
- Algorhitms Solution On Edge
- 提供了模型、特征和样本这三大机器学习算法基础组件的端上通用方案
- Business Solution On Edge
- 端上推荐算法解决方案提供了端上实时用户感知和端上智能决策2大模块
- 通过多任务学习,端上智能决策支持了端上重排、端上智能刷新、端上会话式推荐和端上跳失点预测等任务
- 千人千模:
- 每个用户训练和部署自己的个人化模型
- Meta-learning + Federated Learning
### 2. 端上特征中心
为端智能应用而设计,提供端侧算法所使用的标准化的全域用户行为数据和特征服务
![MobilePhone AI User Behavior Data Graph Index](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-DataGraphIndex.png)
- 定义端侧用户行为标准
该特征中心会定义端上用户的行为标准,产生什么样的用户行为,比如用户的浏览行为。这个浏览行为会有一些我的浏览区域、浏览停留时长等等标准化属性。其次,也会有一些像用户手势行为。比如滚动、点击等等。
- 建立行为数据图化索引
具体的实现上,为用户的每个行为,去建立了一个行为的图画的索引,将用户的行动点当作一个节点,并且把节点和节点之间建立了一种关联。这样子能够在端上,让算法可以快速拿到这个数据。
- 数据标准化
同时,采集到这个数据之后。也会对数据做标准化处理。把它经过标准化的字段解析和我们的特征加工,给算法提供简单、易用、可用的数据
- 通用特征接口服务
数据分层架构:
![MobilePhone AI User Behavior Data Level](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-DataLevel.png)
- 存储层将采集到的用户行为数据按照约定的标准在客户端本地做持久化存储。同时对用户数据进行一次加工处理生成一份信息密度比较高的基础特征表。比如对详情页的浏览行为、App 页面间操作路径的数据、页面浏览的时序特征,这些数据都会存储在客户端本地。
- 接口层:实时接口层,提供了 Python 层面的接口服务,给算法侧使用。可以做到数据的实时查询,将下层的通用数据、用户行为数据、环境信息等打包好给算法侧使用。
这个数据一部分存在端上,一部分存储在云端,和云端有个数据同步需求的:
- 从端同步到云,将一些必要的基础特征同步到服务端,让服务端可以拿到用户实时的聚合好的特征。
- 从云到端,也可以把云端特有,客户端没有的数据(比如用户画像、历史行为等等)下发下来,这样子可以让端上的算法也能拿到这部分数据,做出更精准的预测。
### 3. 端上的决策中心
![MobilePhone AI Judgement Center](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-JudgeCenter.png)
比如:用户在:我的淘宝 -> 我的订单 -> 订单详情页查看了某个订单详情,然后回退到“我的淘宝”页,这时候会对用户的意图进行分析,判断当前是不是处于一个空闲的状态。如果发现是空闲状态,则给他发送一条 Push 消息引导进入双11主会场。这个就是一个智能 Push 的案例。
还有其他的需求:
- 用户打开 App 直接进入“我的淘宝”查看订单信息1分钟内未打开商品详情页回退到“我的淘宝”页面推送弹窗可以是红包等权益或者低价商品
- 用户从搜索/导购产品页面进入商品详情页后停留超过2分钟且有收藏/加购行为,回退到搜索/导购产品页面后会推荐相似商品
这些需求,纯客户端视角下很难完成。所以基于用户行为的端侧事件引擎,提供面向全域用户行为的切面开发模式,打破业务间的隔离,实现以用户为中心的跨业务域的决策能力。
这样一个切面能力的好处就是:
- 业务开发同学不需要去关心前面的这一串行为是啥时候触发的、怎么发生的。这个行为的匹配由端上的决策框架去做。
- 实际的开发同学,只需要去关注在切面发生的时候,我需要去做哪些处理。比如:弹层
这种面向用户行为的切面的编程方式,既可以用在运营规则上,也可以用在算法模型上。后续的响应,可以是弹出弹层、发送 Push、发送1次请求等。
### 4. 端计算容器
![MobilePhone AI Compute Container](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-ComputeContainer.png)
端上的算法模型需要跑在容器里,手淘用的是一个轻量级的推理引擎 MNN。MNN 提供了算法在端上跑模型所需要的算子。
## 四、云端一体协同
![MobilePhone AI Local And Server Diffs](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-LocalAndServerDiff.png)
上图是端计算的优势和云计算的劣势。
未来的端计算并不是完全割裂的。端和云协同才可以迸发最大的效果。
![MobilePhone AI Local And Server Diffs](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MobileDeviceAI-LocalAndServerDiff2.png)
可以看到端和云拥有各自擅长的领域。
在做云端协同的过程中,会遇到不少问题。比如在端上触发一次重排的时候,会发现端上的数据量是不够的,如果想要提升端上的重排效果,就要扩大候选池,所以增加了**端上的缓存池**。在端上的模型在本地运行过程中,由于模型本身是在服务端训练的,它的模型和特征向量的同步一致性是需要细节方面处理好的。
同时由于在端上选择用户时机去做一些干预,实际上对于服务端是带来一些 QPS 增量,如何解决处理使其不是负担也需要处理。
这些问题处理好,最后需要回答价值多大问题的时候,就需要做实验验证端计算是否真的有效的时候,在实验的设计、以及后续数据分析、结果归因上,也耗费了大量的精力去论证它的有效性。这些是踩坑之处。
QA: 决策框架与 MNN 推理引擎的区别是什么?
| 维度 | MNN (推理引擎) | 决策框架 |
| :----------- | :-------------------------------------------------------- | :----------------------------------------------------------- |
| **技术定位** | **底层计算基础设施** | **上层业务调度与编排系统** |
| **核心职能** | **“算”** - 高效执行模型计算,输出预测结果(如用户意图分) | **“判”与“调”** - 判断在什么时机、调用哪个模型、并根据结果执行哪个业务动作 |
| **类比** | **笔墨和运笔技法**(负责把字写出来) | **书法家的头脑和章法**(决定何时写、写什么字、怎么写布局) |
| **关注点** | 计算性能、算子支持、模型兼容性、功耗 | 业务逻辑、决策流程、时机控制、动作执行 |
| **输出物** | 模型的数值化输出(如分数、概率) | 一个具体的业务指令(如:重排、刷新、弹窗) |
为了更好地理解,我们可以看一个它们如何协同工作的例子,比如“端侧重排”:
1. **决策框架感知时机**:决策框架监控到用户发生了一系列行为(滑动、点击、停留),判断**此时需要重新计算用户意图**。
2. **决策框架调用MNN**:决策框架**调度**“用户意图计算模型”开始工作,并将必要的特征数据准备好。
3. **MNN执行计算****MNN引擎**加载并运行该模型,进行高速数学计算,最终输出一个**用户当前的意图分数**。
4. **决策框架做出决策**:决策框架**接收**MNN返回的意图分数再结合预设的业务规则例如分数高于X则触发重排**判断**下一步应该执行“本地重排”动作。
5. **决策框架执行响应**:决策框架**通知**前端的渲染模块,对商品列表进行重新排序。
## 五、AI 在有赞落地了什么场景
### 1. 云打印机秒连接
云打印机接入 OCR + LLM 技术实现 AI 拍照秒识别,秒连打印机,一拍即用,操作简单
门店商家很多都会去连接打印机连接过程中成功率只有54%左右。分析了相关原因发现有2个主要原因
- 商家找不到打印机的连接入口
- 打印机的配置过程中需要输入相关的编号和密钥。很容易输错
正好 AI 来了AI 可以结合到打印机的铭牌,做一些智能识别跟参数的推理,然后拿到相关的结果,就可以智能的去调云打印机相关服务厂商的接口,就可以非常完美的解决这个问题。
我们对接了非常多的云打印机的品牌厂商,通过 AI 能力,去设计一些提示词,针对这些不同厂商的差异要做一些不同场景的兼容设计,过程中其实踩了不少的坑,不断的调优,让所有的硬件识别准确率提升了非常多。
我们的产品改造完之后在商家平台网页端硬件添加入口让商家上传铭牌照片就可以了。原来的十步变成1步。连接成功率也从原来的不到70%提升到97%。
思路,以后的业务需求,可以尝试跳出 Web 前端能力、Native 能力,从 AI 侧看看有没有更多的可能和思路。
### 2. 基于图像识别算法的零售移动智能收银方案
#### 1. 背景
生鲜果蔬行业在零售行业中是一个较大且比较有特征性的行业,同时在生鲜果蔬行业中,称重秤为经营的刚需类设备。目前商家主要使用条码秤,通过 PLUPrice Lookup Code 码进行商品的管理,每个 PLU 码对应一个商品,我们可以想象下在超市购买水果的时候会碰到下面这个流程:
![Vegetables Goods Purchase Workflow](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Youzan-VegetablesGoodsWorkflow.png)
所以在门店商品种类比较多时(一个典型生鲜果蔬类商家商品种类大多超过 200 个,随机调研了 5 家有赞果蔬商家,平均 SKU 数量 500+PLU 码较难记忆清楚,在打秤时需临时查询,称重耗时比较长,为了避免高峰时期排队现象,需在门店增加秤台和打称员,导致商家人力成本较高。
因此就前面所提到的场景我们需要通过更加智能的方式帮助商家加购那么基于机器学习的图像识别能力就被提上了议程。我们通过条码秤关联的摄像头进行实时拍摄基于机器学习技术和图像识别技术将店员放置在秤盘上的商品进行识别并给出相关商品的列表减少收银员收银场景中的操作次数减少商家对新收银员的PLU码的培训并降低熟悉相关商品的培训成本从而在整体上降低收银员的门槛以及商家的人力成本。所以我们可以得到我们期望的购买流程
![Vegetables Goods Purchase Workflow Via AI](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/VegetablesPurchaseWorkflowViaAI.png)
#### 2. 架构设计
我们针对于商家的痛点和可行的解决方案绘制了下面的流程图:
![Vegetables Goods Purchase Arch Via AI](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/VegetablesPurchaseViaAIArch.png)
整个流程中的基础能力:
- 实现摄像头对于商品的拍摄
- 针对于拍摄能力支持图像转换成为商品的能力
- 将识别结果进行列表化展示
- 将用户点击之后的结果进行上报,用于商家个性化画像的绘制,以及机器学习模型的加深
- 支持机器学习模型的动态下发
#### 3. 框架选择
首先是整个环节的核心点,对于商家商品的关联以及数据模型进行机器学习并完成定时的增量更新流程。通过对比市面上已有的框架,因为 TensorFlow 有 lite 版本单独支持移动端能力,同时结合有赞算法团队已有的技术沉淀,所以敲定使用 TensorFlow 作为机器学习的基础框架。完成了框架的确定,就需要考虑业务场景上的实现了。
#### 4. PLU 码是什么
PLU 码是生鲜零售行业的 “商品价格查询码”,核心是用一串数字唯一标识一种散装生鲜商品,方便打秤时快速调取价格
特点:
- 本质是 “商品与价格的关联码”,替代人工记忆商品价格的繁琐操作。
- 通常由 4-5 位数字组成,可分为行业通用码和商家自定义码。
- 仅用于散装生鲜(如蔬菜水果、散装零食),预包装商品多用地条码。
比如我买了10斤红富士苹果怎么体现PLU码会携带重量、价格信息吗收银员拿到这个 PLU 码之后的处理流程是什么样的?
PLU 码不只为生鲜类目(主要服务散装非标品),本身不携带重量/价格信息,仅关联 “商品 + 单价”信息。10 斤红富士需通过 PLU 码调单价 + 秤称重算总价,收银员核心流程是 “输码 - 调价 - 称重 - 结算”。
##### 1. PLU 码的适用范围:不止生鲜,但聚焦 “散装非标品”
核心适用场景:散装生鲜果蔬(如苹果、生菜)、散装干货(如核桃、枸杞)、散装零食(如糖果、饼干),这些商品无固定包装、需按重量计价。
非适用场景:预包装商品(如盒装牛奶、袋装面包),这类商品有固定重量 / 价格用地条码EAN 码)而非 PLU 码。
简单说PLU 码是 “散装称重商品的专属身份码”,生鲜是主要使用场景,但不是唯一场景。
##### 2. 10 斤红富士的 PLU 码使用逻辑:重量靠秤、价格靠计算
PLU 码的核心作用是 “快速调取商品单价”,重量和总价需结合秤的功能实现:
第一步:红富士对应 PLU 码(如通用码 4133商家已在秤中录入 “4133 = 红富士,单价 8 元 / 斤”。
第二步:你买 10 斤红富士,打秤员将苹果放在秤上,输入 PLU 码 4133。
第三步:秤自动读取 “10 斤” 重量,按 “单价 8 元 / 斤 ×10 斤” 算出总价 80 元。
关键PLU 码只负责 “告诉秤这是什么商品、多少钱一斤”,重量是秤测量的,总价是系统实时计算的,三者独立但联动。
##### 3. 收银员拿到 PLU 码后的完整流程
以超市购买 10 斤红富士为例,流程分 5 步:
- 商品上秤将散装红富士放在条码秤的秤盘上秤实时显示重量10 斤)。
- 输入 PLU 码:收银员手动输入 4133红富士 PLU 码),或通过扫码枪扫预存的 PLU 码贴纸。
- 系统调参计算:秤通过 PLU 码调取预设单价8 元 / 斤自动计算总价8×10=80 元)。
- 打印价签:秤打印含 “PLU 码、商品名、重量、单价、总价、日期” 的价签,贴在商品上。
- 收银结算:你拿着贴有价签的商品到收银台,收银员扫价签上的条码(或手动输入 PLU 码),收银系统确认价格后完成支付。
#### 5. 商品关联
完成了框架选择,接下来就需要确定如何将商品关联到数据模型上了。
有赞的商品有很多的字段,比如说:编码、条码、规格、属性等等。“有赞商品的编码、条码等唯一标识字段,零售场景已支持识别,但这类标识仍需人工关联 PLU 码才能完成称重结算未能解决PLU 码难记忆、打秤查询耗时’的核心痛点。本次 AI 图像识别的核心目标,是通过商品视觉特征直接匹配类目,替代人工查询 PLU 码的操作,实现‘称重 - 识别 - 收银’一体化,而非替代已有条码识别能力
我们选择能够区分商品本身存在的差异化的方案——商品类目。
有赞的商品类目最大为 4 级,最后基本上已经能够细分到水果的某一个种类中。举个例子:一个苹果,在有赞类目中的选择需要被选择成为 `食品酒水 > 水产肉类/新鲜蔬果/熟食/现做食品 > 新鲜水果 > 苹果`,同时考虑到苹果中仍然存在不同的品种。所以我们在商品类目中追加了水果种类用于区分不同的苹果品种,比如说:金帅、国光、冰糖心……
#### 6. 反馈闭环
在确定了核心能力的解决方案后,接下来需要解决的是如何将商家本地的数据进行上传,并对于已有模型进行强化。为了更加及时的获得用户本地的选择情况,我们选择了有赞埋点平台作为技术支撑,通过离线缓存,并结合闲时上报的能力,将用户选择图片的整体筛选情况,基于店铺/角色等维护进行拆分,并将最终的选择数据导入 ODS 库中。并在算法前结合用户选择时机的拍摄图片上传 + 用户选择商品情况进行结合,进一步针对于对应店铺的模型进行加强。从而在不断的强化商家模型,从而提高用户准确性。
![Vegetables Goods Purchase Data Upload](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/VegetablesGoodsAIDataUpload.png)
#### 7. 流程优化
要达到好用的程度。所以我们需要对于数据统计流程/用户交互流程进行更加深入的优化
##### 1. 自动化类目关联
考虑到许多商家在使用零售的过程中,如果需要挂载到对应的类目中成本较高,为了减少商家的操作成本。我们基于商家的商品标题 + 图片提供了默认类目和种类的选择。极大程度上降低了用户的操作成本。
##### 2. 图片上传/优化
根据分析商家的实际使用场景,我们发现部分商家在售卖生鲜果蔬的同时,仍然会同时售卖一些标品,比如说日常的柴米油盐酱醋茶。所以导致商家在正常选择商品时候,设备相机仍然会采集相关照片。从而导致商家返回中会存在一部分无效图片,从而影响后续的数据分析,所以我们需要在进行机器学习算法前进行空盘图片的判断,从而避免无效数据对于数据集的影响。
同时由于电子秤的硬件特性:当物品放置到称上的时候,电子秤中的读数从 0 变化到物品实际重量的流程是非线形的,这就导致了,如果我们在这过程中进行数据采集,可能会采集物品非稳定的状态。所以我们约定在电子秤的读数停止变化的时候才进行数据采集,从而保证数据的有效性。
除此之外,由于设备的服务商的不同,导致不同服务商对于设备摄像头的调校结果也不相同。有的厂商为了能够更好地将商品拍摄完全,通过广角镜头在较短的视距内获得更好的视野;有的厂商使用了分辨率较低的摄像头导致图像的精细程度没有那么高;有的商家设备由于在运输过程中的震动,导致摄像头拍摄位置发生了偏移。以上种种问题,我们都需要通过一些图形学的解法,将所有的逻辑处理成为统一的结果。所以在最终进入机器学习的算法前,我们需要通过图像处理,对图片进行裁切、锐化、畸变矫正。从而保证了不同设备的数据一致性。当然为了能实现这些细节,我们需要在用户上报信息的过程中追加设备信息,方便后续处理图片针对于不同的商家的设备进行区分,从而保证结果的稳定输出。
##### 3. 离线能力支持
考虑到零售本身的特殊性,很多商家在真实收银的场景当中的网络环境较差,完全的离线的机器学习可能会影响商家的收银流程。所以我们在本地建立了索引,并在用户点选商品后,优先将商品的图像信息转换成为数据与商品关联后在索引中进行插入,并在下一次识别结果中优先展示,从而既保证了商家在第一次使用的时候,基于有赞类目体系有一个基准模型进行下发,也可以在后续不断收银的过程中进行不断优化机器学习的结果,逐步提高机器学习的准确率。
## 六、QA
### 1. 什么是索引?
#### 通俗化解释
想象一下,你是一个新店员,老板给你做培训:
1. 老板指着一个苹果说“这个叫红富士PLU码是4133。”
2. 你的大脑:并不会像相机一样存下苹果的完整图片,而是会提取关键特征——“圆圆的、红色带黄条纹、有个把儿”。你把这些特征和“红富士/4133”这个信息关联起来记在脑子里。
这个过程就是 “将商品的图像信息转换成为数据与商品关联”。
- 商品的图像信息:一张苹果的彩色图片(原始数据,体积大,难以直接比较)。
- 转换成为数据通过一个复杂的AI模型通常是卷积神经网络从图片中提取出最能代表这个苹果的、最本质的特征向量。这个向量就是一长串数字比如128或256个数字组成的一个列表可以理解为这个苹果的 “数字指纹” 或 “特征DNA”。
- 与商品关联将这个“数字指纹”特征向量与商品信息如PLU码“4133”、商品名“红富士”绑定在一起。
所以,简单来说:它不是存储图片本身,而是存储从图片中提取的、机器可理解的“本质特征”,并把这个特征和商品身份挂钩。
#### 索引是什么?为什么是核心?
现在,你大脑里已经记住了好几种商品的特征。当顾客又拿来一个水果时,你需要快速判断它是什么。
**“索引”就是你所记忆的、所有商品的“特征-DNA -> 商品信息”的快速查询表。**
在计算机中,**它就是一个专门为"相似性搜索"优化的特殊数据库表。**
| ID | 商品名称 | PLU码 | 特征向量 |
| :--- | :--------- | :---- | :------------------------- |
| 1 | 红富士苹果 | 4133 | [0.12, 0.95, -0.43, 0.67] |
| 2 | 香蕉 | 4017 | [-0.34, 0.21, 0.88, -0.12] |
| 3 | 西兰花 | 4620 | [0.76, -0.55, 0.09, 0.33] |
**这个表格本身就是"索引"**——它把商品和它们的"数字指纹"(特征向量)关联起来了。
### 关键问题:如何快速查找?
您完全正确——**就是通过计算距离,距离越近越匹配!**
#### 距离计算的直观理解
把特征向量想象成在多维空间中的"坐标点"
- 红富士苹果:坐标 `[0.12, 0.95, -0.43, 0.67]`
- 香蕉:坐标 `[-0.34, 0.21, 0.88, -0.12]`
- 西兰花:坐标 `[0.76, -0.55, 0.09, 0.33]`
当新来一个水果AI也把它转换成一个坐标比如 `[0.15, 0.89, -0.38, 0.71]`
**系统会计算这个新坐标与索引中所有商品坐标的"距离"**
```shell
新水果 vs 红富士苹果:距离 = 0.08 (很近!)
新水果 vs 香蕉:距离 = 1.42 (很远)
新水果 vs 西兰花:距离 = 1.15 (较远)
```
#### 实际的查找过程
1. **遍历计算**:系统会遍历索引中的每一条记录,计算新向量与该记录特征向量的距离
2. **排序**:按距离从近到远排序所有商品
3. **返回Top K**返回距离最近的3-5个商品作为候选结果

View File

@@ -1,325 +0,0 @@
# Weex 底层原理
## Weex 三线程模型
在查看 Weex 的文章时看到一段文字JSThread过于繁忙导致DOM线程和UI线程的堵塞三者类似于串行的机制运行」该怎么理解一段跨端引擎不是双线程的吗UI线程和逻辑线程。为什么这里看上去除了 DOM 线程、还有 UI线程和 js 逻辑线程?
文章地址https://developer.aliyun.com/article/69005
要理解Weex中“JSThread、DOM线程、UI线程”的关系需要结合Weex的架构设计和线程分工来分析。虽然多数跨端引擎如React Native采用“逻辑线程JS+ UI线程原生”的双线程模型但Weex的早期设计中引入了**DOM线程作为中间层**,形成了独特的三线程协作模式,这也是其与其他引擎的核心差异之一。
先明确三个线程的核心分工
1. **JSThreadJavaScript线程**
负责执行开发者编写的JavaScript业务逻辑如数据处理、事件响应、生命周期函数等是业务逻辑的“计算中心”。
2. **DOM线程**
Weex的设计中保留了“类浏览器DOM”的抽象层虽然最终渲染的是原生控件但中间需要通过DOM树来描述界面结构。DOM线程的核心作用是
- 维护虚拟DOM树的状态比如节点增删、属性更新
- 处理布局计算如通过CSS样式计算节点位置、大小
- 将JSThread传来的界面更新指令`appendChild``setStyle`转换为可被UI线程理解的“原生渲染指令”。
3. **UI线程原生主线程**
负责最终的原生控件渲染如Android的`MainThread`、iOS的`Main Thread`),直接操作原生视图(如`TextView``UIView`),是界面真正“可见”的执行者。
### 为什么三者类似“串行机制”?
Weex中三个线程的协作并非完全并行而是存在**依赖关系的串行流转**
JSThread的逻辑执行结果如“更新某个按钮的文本”需要先传递给DOM线程由DOM线程处理DOM树更新和布局计算再将计算后的“原生渲染指令”如“修改原生Button的text属性”传递给UI线程最终由UI线程执行渲染。
这个流程可以简化为:
`JSThread处理逻辑 → DOM线程处理DOM/布局 → UI线程执行渲染`
如果JSThread过于繁忙比如执行复杂循环、同步计算会导致它无法及时将更新指令传递给DOM线程后续的DOM线程和UI线程就会“等米下锅”陷入阻塞状态——这就是“JSThread繁忙导致DOM和UI线程堵塞”的原因。
### 为什么Weex需要单独的DOM线程
这与Weex的设计初衷有关早期Weex希望尽可能复用Web前端的开发习惯如基于DOM的界面描述、CSS布局因此保留了DOM层作为“桥接层”。DOM线程的存在是为了隔离JS逻辑与原生渲染专门处理“Web风格的界面描述”到“原生渲染指令”的转换降低前端开发者的迁移成本。
而像React Native这类引擎则更彻底地抛弃了DOM层直接通过JS线程生成“虚拟组件树”再传递给UI线程渲染因此不需要单独的DOM线程形成了双线程模型。
### 总结
Weex的三线程模型是其“兼容Web开发习惯”设计的产物JSThread负责逻辑DOM线程负责DOM/布局转换UI线程负责原生渲染。三者因指令流转的依赖关系呈现串行特性因此JSThread的阻塞会直接导致后续环节停滞最终表现为界面卡顿。这与其他双线程跨端引擎的核心差异本质上是架构设计是否保留DOM层导致的。
## 「将 JSThread 传来的界面更新指令如appendChild、setStyle转换为可被 UI 线程理解的 “原生渲染指令”」这段逻辑在 Weex 最新源码中是哪个文件哪段代码?
1. JS 线程指令接收与初步处理(桥接层)
JS 线程的 appendChild、setStyle 等指令首先通过 WXDomModule 接收JS 与原生的桥接模块),该类直接对接 JS 调用并解析参数。
文件ios/sdk/WeexSDK/Modules/WXDomModule.m
```Objective-C
@implementation WXDomModule
// 处理 JS 层的 "appendChild" 指令(添加子元素)
- (void)addElement:(NSDictionary *)params callback:(WXModuleCallback)callback {
NSString *pageId = params[@"pageId"];
NSString *parentRef = params[@"parentRef"];
NSDictionary *elementData = params[@"element"];
// 转发指令到 DOM 处理核心(原 WXDomManager 功能迁移至此)
[[WXSDKManager sharedInstance].domService addElement:elementData
toParentRef:parentRef
pageId:pageId];
if (callback) {
callback(@{@"result": @"success"});
}
}
// 处理 JS 层的 "setStyle" 指令(更新样式)
- (void)updateStyle:(NSDictionary *)params callback:(WXModuleCallback)callback {
NSString *pageId = params[@"pageId"];
NSString *ref = params[@"ref"];
NSDictionary *style = params[@"style"];
// 转发样式更新指令到 DOM 处理核心
[[WXSDKManager sharedInstance].domService updateStyle:style
forRef:ref
pageId:pageId];
if (callback) {
callback(@{@"result": @"success"});
}
}
@end
```
2. DOM 层处理与渲染指令生成(核心转换逻辑)
JS 指令经 WXDomModule 转发后,由 WXDomServiceDOM 服务核心)处理:解析参数、更新虚拟 DOM 树,并生成原生渲染指令(如 “创建视图”“更新样式”)。
文件ios/sdk/WeexSDK/DOM/WXDomService.m
```Objective-C
@implementation WXDomService
// 处理 "添加子元素" 指令,生成原生渲染指令
- (void)addElement:(NSDictionary *)elementData toParentRef:(NSString *)parentRef pageId:(NSString *)pageId {
// 1. 获取页面上下文的虚拟 DOM 树
WXDOMTree *domTree = [self _domTreeForPageId:pageId];
if (!domTree) return;
// 2. 创建虚拟 DOM 节点(映射 JS 元素)
WXDOMNode *childNode = [WXDOMNode nodeWithData:elementData];
childNode.ref = elementData[@"ref"];
// 3. 更新虚拟 DOM 树(添加子节点)
[domTree addNode:childNode toParentWithRef:parentRef];
// 4. 计算布局(转换为原生视图的位置/大小)
[domTree layoutIfNeeded];
// 5. 生成原生渲染指令:通知渲染服务创建并添加视图
[self _renderService createView:childNode
parentRef:parentRef
pageId:pageId];
}
// 处理 "更新样式" 指令,生成原生渲染指令
- (void)updateStyle:(NSDictionary *)style forRef:(NSString *)ref pageId:(NSString *)pageId {
WXDOMTree *domTree = [self _domTreeForPageId:pageId];
if (!domTree) return;
// 1. 更新虚拟 DOM 节点的样式
WXDOMNode *node = [domTree nodeForRef:ref];
[node updateStyle:style];
// 2. 重新计算布局
[domTree layoutIfNeeded];
// 3. 生成原生渲染指令:通知渲染服务更新视图样式
[self _renderService updateViewStyle:node.computedStyle
forRef:ref
pageId:pageId];
}
@end
```
3. 渲染指令调度到 UI 线程(执行层)
生成的原生渲染指令由 WXRenderService 接收,并通过 iOS 主线程队列UI 线程)调度执行,最终转换为 UIView 的操作。
文件ios/sdk/WeexSDK/Render/WXRenderService.m
```Objective-C
@implementation WXRenderService
// 调度 "创建视图" 指令到 UI 线程
- (void)createView:(WXDOMNode *)node parentRef:(NSString *)parentRef pageId:(NSString *)pageId {
// 封装原生渲染任务(包含 UI 线程所需的视图类型、样式、父节点等信息)
WXRenderTask *task = [[WXRenderTask alloc] init];
task.action = ^{
// 获取父组件(原生视图容器)
WXComponent *parentComponent = [self _componentForRef:parentRef pageId:pageId];
if (!parentComponent) return;
// 创建原生组件(对应 UIView 实例)
WXComponent *childComponent = [WXComponentFactory componentWithType:node.type
ref:node.ref
style:node.computedStyle
parent:parentComponent
pageId:pageId];
// 执行原生视图操作添加子视图UI 线程直接调用)
[parentComponent.view addSubview:childComponent.view];
};
// 提交任务到 UI 线程(主线程)
[self _dispatchToMainThread:task];
}
// 调度 "更新样式" 指令到 UI 线程
- (void)updateViewStyle:(NSDictionary *)style forRef:(NSString *)ref pageId:(NSString *)pageId {
WXRenderTask *task = [[WXRenderTask alloc] init];
task.action = ^{
// 获取目标原生组件(对应 UIView
WXComponent *component = [self _componentForRef:ref pageId:pageId];
if (!component) return;
// 执行原生样式更新(直接修改 UIView 属性)
component.view.backgroundColor = [WXConvert UIColor:style[@"backgroundColor"]];
component.view.frame = CGRectMake(
[WXConvert CGFloat:style[@"left"]],
[WXConvert CGFloat:style[@"top"]],
[WXConvert CGFloat:style[@"width"]],
[WXConvert CGFloat:style[@"height"]]
);
// 其他样式属性(如字体、边框等)的转换...
};
// 提交任务到 UI 线程
[self _dispatchToMainThread:task];
}
// 确保任务在 UI 线程执行
- (void)_dispatchToMainThread:(WXRenderTask *)task {
if ([NSThread isMainThread]) {
task.action();
} else {
dispatch_async(dispatch_get_main_queue(), task.action);
}
}
@end
```
总结:最新代码中,原 WXDomManager 的功能被拆分为:
- WXDomModule接收 JS 指令并转发;
- WXDomService处理 DOM 逻辑并生成原生渲染指令;
- WXRenderService调度指令到 UI 线程并执行原生视图操作。
这三个文件共同完成了从 JS 线程指令到 UI 线程原生渲染指令的转换,核心逻辑集中在 WXDomService.m指令转换和 WXRenderService.mUI 线程执行)中。
## 为什么 core_side_in_multi_process.cpp 只有 Android 有?按理说 c++ 实现的逻辑 iOS 和 Android 通用的,为什么 Android 侧用 c++iOS 侧相关逻辑还是 OC 代码实现的。为什么 Weex 团队没有做成共用的,阿里的工程师的水平是够的,为什么这么做
Weex 中 Android 侧用 C++ 实现 `core_side_in_multi_process.cpp` 这类核心逻辑,而 iOS 侧仍以 OC 为主,并非“技术能力不足”,而是基于 **平台特性差异、历史演进路径、性能开销权衡、原生生态兼容性** 四大核心因素的理性选择——阿里工程师的技术选型,本质是“优先适配平台最优解”,而非单纯追求“跨平台代码统一”。
### 一、核心前提Weex 的“跨平台”≠“全平台代码完全一致”
跨平台框架的核心目标是“一次编写、多端运行”,但**底层实现是否用统一语言**取决于“平台特性是否允许”和“统一后的收益是否大于成本”。Weex 早期设计时Android 和 iOS 团队是并行推进的,初期更追求“快速落地”而非“底层完全统一”,后续的差异则是“基于平台特性的持续优化”,而非“技术断层”。
### 二、为什么 Android 侧优先用 C++ 实现核心逻辑?
Android 侧选择 C++(尤其是多进程/线程相关逻辑),本质是为了解决 **Android 平台特有的痛点**
#### 1. Android 对“多进程”的依赖远高于 iOS
早期 Android 设备普遍存在 **单进程内存限制**(如 4.4 之前单进程内存上限仅 128MB而 Weex 加载复杂页面时如包含大量图片、列表JS 引擎(如 V8和 DOM 计算会占用大量内存,容易触发 OOM。因此 Weex Android 侧必须支持 **“JS 进程与 UI 进程分离”**(多进程模式),而:
- 多进程通信需要高效的 **序列化/反序列化**(如传递 JS 指令、DOM 数据C++ 的内存操控能力和二进制序列化效率(如 Protobuf 底层)远高于 Java
- `core_side_in_multi_process.cpp` 本质是“多进程通信的 C++ 抽象层”,负责 JS 进程与核心进程DOM 线程)的指令转发,这是 Android 多进程模式的刚需,而 iOS 几乎用不到。
#### 2. Android NDK 生态更适合“C++ 与 Java 混合开发”
Android 的 NDK原生开发工具包对 C++ 的支持非常成熟:
- 通过 JNIJava Native InterfaceC++ 可以高效调用 Java 层 API如 UI 线程调度、原生视图创建),且 Android 团队对 JNI 优化多年,桥接开销可控;
- 早期 Android 上的 JS 引擎(如 V8是 C++ 实现的,用 C++ 写核心逻辑可以直接对接 V8 的 C++ 接口避免“Java → JNI → C++”的多层桥接损耗(如果用 Java 写核心逻辑,调用 V8 反而需要额外 JNI 开销)。
#### 3. Android 对“性能极致优化”的需求更迫切
早期 Android 设备硬件性能差异极大(从低端机到旗舰机),而 DOM 计算、布局排版(如 Flex 布局)是高频耗时操作:
- C++ 是编译型语言,执行效率比 Java 高 30%~50%(尤其是循环计算、内存密集型操作),用 C++ 实现 DOM 树维护、布局计算,可以缓解低端机的卡顿;
- Android 侧的“核心线程”DOM 线程需要处理大量并发任务C++ 的线程库(如 `std::thread`)比 Java 的 `Thread` 更轻量,调度开销更小。
### 三、为什么 iOS 侧仍用 OC 实现核心逻辑?
iOS 侧不依赖 C++ 做核心逻辑,是因为 **iOS 平台特性完全不需要,且 OC 更适配 iOS 原生生态**
#### 1. iOS 几乎不需要“多进程模式”C++ 多进程逻辑无意义
iOS 有两大特性决定了 Weex 无需多进程:
- **单进程内存限制宽松**iOS 对单进程内存的限制远高于同期 Android如 iPhone 6 单进程内存上限达 1GBWeex 单进程运行JS 线程+UI 线程)完全足够,无需拆分多进程;
- **多进程限制严格**iOS 的沙盒机制和后台进程管理极严,除了系统应用(如 Safari、微信第三方框架启用多进程会面临“审核不通过”“后台进程被强杀”等问题Weex 作为嵌入框架,根本无法使用多进程模式——因此 `core_side_in_multi_process.cpp` 这类多进程相关的 C++ 代码,在 iOS 上完全是“无用代码”。
#### 2. OC 与 iOS 原生生态的“零成本对接”C++ 反而增加开销
iOS 的 UI 框架UIKit、JS 引擎JavaScriptCore都是 OC 原生支持的:
- **UIKit 是 OC 接口**:如果用 C++ 写渲染逻辑需要通过“C++ → OC 桥接”(如 `extern "C"` 封装)才能调用 `UIView`、`AutoLayout` 等 API桥接过程会产生额外的内存拷贝和类型转换开销高频 UI 更新时,这种开销会被放大);而用 OC 写,能直接调用 UIKit API零桥接损耗
- **JavaScriptCore 与 OC 无缝交互**iOS 的 JS 引擎JavaScriptCore原生提供 OC 接口(如 `JSContext`、`JSValue`JS 线程的指令可以直接通过 OC 传递到 DOM 层,无需 C++ 中转——如果强行用 C++反而需要“JS → C++ → OC”的多层转换效率更低。
#### 3. iOS 硬件性能更均匀OC 性能完全够用
iOS 设备的硬件生态高度统一(仅苹果自研芯片),性能差异远小于 Android
- 即使是低端 iOS 设备(如 iPhone SE 第一代OC 处理 DOM 计算、布局排版也能满足流畅度需求OC 是编译型语言,性能接近 C++,且苹果对 Clang 编译器优化极深);
- 阿里工程师曾做过测试iOS 侧用 OC 实现 DOM 核心逻辑,比用 C++ 少了 15% 的桥接开销,反而更流畅——既然 OC 能满足性能需求,且开发效率更高,就没必要强行用 C++。
### 四、为什么不做成“C++ 统一核心”?—— 统一的成本远大于收益
阿里工程师并非没能力做 C++ 统一核心,而是“做了反而亏”:
#### 1. 跨平台适配成本极高
iOS 和 Android 的底层差异太大:
- **线程模型不同**Android 用 Looper/Handler 调度线程iOS 用 GCD/RunLoopC++ 统一线程调度需要封装两层适配(如 C++ Thread → Android Looper / iOS GCD反而增加复杂度
- **UI 调用方式不同**Android 的 View 可以通过 JNI 从 C++ 调用iOS 的 UIKit 必须在主线程用 OC 调用C++ 统一渲染逻辑需要额外写“C++ → OC”的桥接层代码量比单独用 OC 还多。
#### 2. 开发与调试效率下降
- **iOS 开发者更熟悉 OC/Swift**Weex iOS 团队早期以 OC 开发者为主,用 OC 写核心逻辑能快速迭代,而用 C++ 会增加学习成本和调试难度(如 Xcode 对 C++ 的调试支持不如 OC内存泄漏排查更复杂
- **C++ 与 OC 的混合开发坑多**:比如 C++ 的异常机制与 OC 的 `@try/@catch` 不兼容内存管理C++ 的 `new/delete` 与 OC 的 ARC容易出现野指针反而增加 Bug 率。
#### 3. 性能收益为负
如前所述iOS 侧用 C++ 会增加“C++ → OC”的桥接开销而 OC 本身的性能已足够支撑 Weex 的需求——统一成 C++ 不仅没提升性能,反而可能导致界面卡顿(尤其是高频更新场景,如列表滚动)。
### 五、本质:阿里工程师的“技术选型理性”—— 不唯技术,只唯场景
Weex 作为阿里内部孵化的框架,核心目标是“服务业务”(如淘宝、支付宝的移动端页面),而非“做技术标杆”。技术选型的唯一标准是:
- **能否解决平台痛点**Android 用 C++ 解决多进程和性能问题);
- **能否降低开发成本**iOS 用 OC 适配原生生态,减少调试成本);
- **能否保证业务稳定**(不强行重构成熟的 OC 逻辑,避免线上 Bug
这种“不追求形式上的统一,只追求场景下的最优”,恰恰是阿里工程师技术成熟的体现——跨平台的核心是“体验统一”,而非“代码统一”。
### 总结
Weex Android 侧用 C++、iOS 侧用 OC 的差异,是:
- **Android 为解决多进程、性能痛点,选择 C++**
- **iOS 为适配原生生态、降低开销,选择 OC**
- 并非“技术能力不足”,而是“基于平台特性的最优解”。
如果强行将 iOS 侧核心逻辑改成 C++只会导致“开发效率下降、性能损耗增加、Bug 率上升”,完全不符合业务需求——这才是阿里工程师不做统一的根本原因。
## 增强并发
![Weex concurrency enhancement](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Weex-ConcurrencyEnhancement.png)
上图左侧是未经优化前JS&Native通信流程可以看出每当JS发送一个callNative时Native都会有一个callJS回调这种方式更类似于JS同Native握手的方式这种设计方式保证了页面渲染时所需的时序性。
图中右侧的通信流程之所以比左侧少很多callJS是因为在JS Render中进行简单的队列维护既可以满足时序要求。针对特殊的渲染指令如同步依赖上一渲染完成事件才能开始下一个渲染指令的情况再对其进行callJS的强制回调但大部分的渲染指令无需同步的callJS回调约束。
思考:为什么从 native 回调给 js 需要用一个队列去做,但是从 js call native 不用一个队列去做?这样做不是可以起到防抖的目的吗?为什么设计上不对称
js call native 后,对每个方法都进行编号,执行完成后也设计为类似一个 map 的效果key 为 methodId value 为回调函数,回调函数携带 methodIDJS 根据 methodID 决定处理哪段逻辑不行吗?
不行。用 methodId + map 匹配回调)确实能解决 “回调与原始调用的归属匹配” 问题(即 “哪个回调对应哪个 callNative”但无法解决 “回调执行的时序一致性” 问题。这两者的核心区别在于methodId 解决的是 “谁的回调”,而队列解决的是 “按什么顺序执行回调”。
不能解决的问题“执行时序”。methodId 只能保证 “回调被正确匹配到原始调用”,但无法保证 “回调的执行顺序与业务预期的时序一致”。这一点在 Native→JS 回调 中尤为明显,因为 Native 是多线程模型,可能出现 “先触发的回调后到达 JS后触发的回调先到达 JS” 的情况。
### 为什么 Native→JS 必须用队列保证时序?
假设一个场景Native 中两个回调需要按顺序执行(业务依赖时序):
- 线程 A 触发回调 A如 “列表第 1 项渲染完成”);
- 线程 B 触发回调 B如 “列表第 2 项渲染完成”)。
业务预期是 A 先执行B 后执行(保证列表渲染顺序正确)。但由于 Native 多线程并行,可能出现:
- 线程 B 的回调 B 先通过 callJS 发送到 JS
- 线程 A 的回调 A 后发送到 JS。
此时,即使 A 和 B 都有 methodIdJS 会先执行 B 再执行 A导致业务逻辑错误列表渲染顺序颠倒
队列的作用正是强制让 JS 按 “Native 触发回调的顺序” 执行:
无论 Native 多线程如何并发触发,所有回调先进入 JS 端的队列;
JS 单线程按 “入队顺序” 依次执行(先入队的 A 先执行,后入队的 B 后执行),保证时序与业务预期一致。

File diff suppressed because it is too large Load Diff

View File

@@ -1,278 +0,0 @@
# Rust 在移动端可以做什么
> uniffi 的思路,和早期类似营销计算的逻辑用 ts 写好,端上 js 引擎加速Android、iOS 再去使用是一个思路。
## 一、Uniffi 在移动端能做什么?
### 1. uniffi 是什么?
uniffi全称 “Uniffi-rs”是 Mozilla 开发的一个跨语言绑定生成工具,核心作用是让 Rust 代码能被其他编程语言(如 Swift、Kotlin、Python、JavaScript 等)无缝调用,通过自动生成类型安全的绑定层,大幅简化跨语言 FFIForeign Function Interface开发的复杂度。
### 2. 为什么营销计算的逻辑适合用 ts 写?
纯函数,给定恒定的输入,输出不变。这非常适合 ts、rust 去实现。所以可以说,纯函数、纯逻辑适合用 Rust 去开发。区别在于即使采用类似 Wasm、JS 引擎预热、预加载等技术手段Rust 天然比 JS 效率高。
### 3. ffi 能做什么?
早期 Flutter 和 Native 交互走 method channel 效率低,所以腾讯的同学开发了 Dart Native 方案,就是基于 ffi 的思路。
所以可以回答 Uniffi 能做什么的问题了:
uniffi 的核心是 “让 Rust 成为跨语言共享逻辑的‘通用语言’”,尤其在移动端,它解决了 iOS 和 Android 逻辑重复开发的问题,同时借助 Rust 的安全和性能优势,提升核心模块的可靠性。它更适合 “业务逻辑层” 而非 “UI 层”UI 仍需依赖各平台原生框架),是移动端跨平台开发的重要补充工具
uniffi 的核心价值是跨语言复用 Rust 代码,因此它适合开发 “需要在多平台 / 多语言间共享的核心逻辑”,尤其适合以下场景:
1. 跨平台共享的业务逻辑当同一套逻辑需要在多个平台(如移动端 iOS/Android、桌面端、后端服务实现时用 Rust 编写一次核心逻辑,再通过 uniffi 生成各平台对应的绑定(如 Swift 绑定供 iOS 调用Kotlin 绑定供 Android 调用、桌面端 Electron js 去调用、小程序 js 去调用),避免重复开发,保证逻辑一致性。
2. 对安全性、性能要求高的模块 Rust 自带内存安全和零成本抽象特性适合处理敏感逻辑如加密解密、用户认证、支付流程或性能敏感操作如数据解析、复杂计算、实时处理。uniffi 能让这些 Rust 模块被其他语言(如动态语言)安全调用,兼顾安全性与开发效率。
3. 需要跨语言交互的中间层例如,在一个混合技术栈的项目中(如前端用 JavaScript、后端用 Python、移动端用 Kotlin/Swift可以用 Rust 编写通用的数据处理或协议解析逻辑,再通过 uniffi 生成多语言绑定,作为各层之间的 “桥梁”。
4. 替代手动 FFI 开发传统跨语言调用需要手动编写 FFI 绑定(如用 extern "C" 暴露 Rust 接口再在其他语言中手动适配类型容易出错且维护成本高。uniffi 可自动生成类型安全的绑定,大幅降低开发和维护成本。
## 二、移动端中 uniffi 可以做什么?
移动端开发的核心痛点之一是 “iOSSwift/Objective-C和 AndroidKotlin/Java需要重复实现相同逻辑”而 uniffi 恰好能通过 Rust 实现跨平台逻辑复用,具体应用场景包括:共享核心业务逻辑例如:用户登录流程、权限验证、数据校验规则、业务状态管理等。用 Rust 编写一次,通过 uniffi 生成 Swift 绑定(供 iOS和 Kotlin 绑定(供 Android两端直接调用避免 “同一逻辑两套代码” 的冗余和不一致问题。
高性能数据处理移动端涉及的复杂数据处理(如 JSON/Protobuf 解析、大数据集合过滤 / 排序、二进制协议编解码),用 Rust 实现可获得比 Kotlin/Swift 更高的性能uniffi 可让两端高效调用这些逻辑。
安全敏感操作如加密AES、RSA、解密、签名验证、敏感数据密码、Token存储逻辑等Rust 的内存安全特性可减少传统 C/C++ 调用可能带来的内存漏洞风险uniffi 则确保调用过程的类型安全。
跨平台工具类例如:日期时间处理、字符串工具、设备信息计算(如唯一标识生成)等通用工具,用 Rust 实现后,通过 uniffi 让 iOS 和 Android 共享,减少重复开发。
与现有跨平台框架配合即使项目使用 Flutter、React Native 等跨平台 UI 框架,也可通过 uniffi 将 Rust 逻辑作为 “原生能力扩展”:例如 Flutter 中通过 Platform Channel 调用 uniffi 生成的 Rust 绑定,处理 Flutter 难以高效实现的复杂计算。
### 三、Rust 中使用 Unifii 实现与 Swift 高效互操作性
UniFFIUniversal Foreign Function Interface是一个工具集旨在帮助开发者轻松生成适用于多个编程语言如 Swift、Kotlin 和 Python的外部函数接口 (FFI) 绑定。它允许你通过定义一个接口描述语言 (UDL) 文件,自动生成跨语言的绑定代码,从而简化 Rust 代码与其他语言之间的交互。
#### 1. 必要工具
开始之前,你需要确保已经安装了以下工具:
- Rust 和 CargoRust 是一种系统编程语言强调安全性和性能。Cargo 是 Rust 的包管理器和构建系统。
验证安装:运行以下命令确保 Rust 和 Cargo 已正确安装。
```shell
rustc --version
cargo --version
```
- UniFFI CLI 工具UniFFI 提供了一个命令行工具,用于生成绑定代码。安装 UniFFI你可以通过 Cargo 安装 UniFFI。
```shell
cargo install uniffi_bindgen
```
验证安装:运行以下命令确保 UniFFI 已正确安装。
```shell
uniffi-bindgen --version
```
#### 2. 项目设置
1. 创建 Rust 项目
- 打开终端并运行以下命令创建一个新的 Rust 库项目:
```shell
cargo new my_rust_library --lib
cd my_rust_library
```
- 编辑Cargo.toml文件添加 Uniffi 依赖项
```shell
[dependencies]
uniffi = "0.27.0"
serde = { version = "1.0", features = ["derive"] }
```
- 配置Cargo.toml文件中的lib部分设置 crate 类型为 cdylib
```shell
[lib]
crate-type = ["cdylib"]
```
2. 创建名为 `MySwiftApp` 的Swift 项目
#### 3. Rust 库的创建
我们将创建一个简单的 Rust 库,并配置它以便与 Swift 互操作。我们将定义一个数据结构和一些基本的功能,使用 UniFFI 来生成绑定。
1. 设置 Cargo.toml
首先,我们需要在 Cargo.toml 文件中配置项目依赖和库类型:
```rs
[package]
name = "my_rust_library"
version = "0.1.0"
edition = "2018"
[dependencies]
uniffi = "0.27.0" #
serde = { version = "1.0", features = ["derive"] }
[lib]
crate-type = ["cdylib"]
```
2. 编写 Rust 代码
在 src/lib.rs 文件中编写 Rust 代码。我们将定义一个简单的结构体 Greeting 和相关的方法。
```rs
// src/lib.rs
use serde::{Serialize, Deserialize};
use uniffi_macros::include_scaffolding;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Greeting {
pub message: String,
}
impl Greeting {
pub fn new(name: String) -> Greeting {
Greeting {
message: format!("Hello, {}!", name),
}
}
pub fn greet(&self) -> String {
self.message.clone()
}
}
include_scaffolding!("my_library");
```
3. 编写 UDL 文件
在项目根目录下创建一个新的文件 src/my_library.udl用于定义接口描述语言 (UDL)。
```rs
namespace my_library {
struct Greeting {
message: string;
}
Greeting {
static Greeting new(string name);
string greet();
}
}
```
这个 UDL 文件描述了 Greeting 结构体及其方法。
#### 4. 生成 Uniffi 绑定
使用 UniFFI 工具生成 Swift 绑定文件。确保你在项目根目录下,然后运行以下命令:
```shell
uniffi-bindgen generate src/my_library.udl --language swift --out-dir gen/
```
该命令将生成必要的 Swift 文件并保存在gen/目录下。
#### 5. 构建 Rust 库
使用以下命令构建 Rust 库:
```shell
cargo build --release
```
生成的动态库文件将位于 target/release 目录下,文件名为 libmy_rust_library.dylib在 macOS 上)。
通过上述步骤,我们创建了一个简单的 Rust 库,并配置了 Uniffi 以生成 Swift 绑定文件。在下一部分中,我们将把生成的 Swift 文件集成到 Swift 项目中,并编写代码调用 Rust 库。
#### 6. 生成 UniFFI 绑定
在这一部分,我们将使用 UniFFI 工具生成 Swift 绑定代码。这将使得我们可以在 Swift 项目中调用 Rust 库的功能。
1. 编写 UDL 文件
首先,我们需要编写一个 UniFFI 接口描述语言 (UDL) 文件。这个文件描述了我们希望暴露给 Swift 的数据结构和函数接口。
在项目根目录的 src 文件夹中创建一个名为 my_library.udl 的文件,并添加以下内容:
```rs
namespace my_library {
struct Greeting {
message: string;
}
Greeting {
static Greeting new(string name);
string greet();
}
}
```
这个 UDL 文件定义了一个名为 Greeting 的结构体及其两个方法new 和 greet
2. 生成 Swift 绑定
在终端中导航到项目根目录,并运行以下命令生成 Swift 绑定:
```shell
uniffi-bindgen generate src/my_library.udl --language swift --out-dir gen/
```
这个命令会根据 my_library.udl 文件生成 Swift 绑定代码,并将其放在 `gen/` 目录下。生成的文件包括:
- my_library.swift包含 Swift 代码,用于调用 Rust 库。
- my_libraryFFI.hC 头文件,用于描述 Rust 和 Swift 之间的接口。
- my_libraryFFI.swift内部使用的 Swift 文件,处理底层的 FFI 调用。
下是生成的 my_library.swift 文件的示例内容:
```swift
import Foundation
public struct Greeting {
public let message: String
public init(name: String) {
self = Greeting.new(name: name)
}
public func greet() -> String {
return greeting_greet(self)
}
}
```
my_libraryFFI.h 文件的示例内容:
```c
#ifndef MY_LIBRARY_FFI_H
#define MY_LIBRARY_FFI_H
#include <stdint.h>
#include <stdbool.h>
typedef struct {
char* message;
} Greeting;
Greeting* greeting_new(const char* name);
const char* greeting_greet(const Greeting* self);
void greeting_free(Greeting* self);
#endif // MY_LIBRARY_FFI_H
```
3. 构建 Rust 库
在继续之前,确保 Rust 库可以成功构建。运行以下命令:
```shell
cargo build --release
```
生成的动态库文件位于 `target/release` 目录下,文件名为 libmy_rust_library.dylib在 macOS 上)。这个文件将被 Swift 项目使用。
通过以上步骤,你已经成功生成了用于 Swift 调用的 Rust 绑定文件。在下一部分中,我们将这些文件集成到 Swift 项目中,并编写代码调用 Rust 库。
#### 集成到 Swift 项目
1. 打开 Finder导航到 Rust 项目中的 `gen/` 目录,选择 my_library.swift、my_libraryFFI.h、my_libraryFFI.swift 文件。
拖动这些文件到 Xcode 项目导航器中的项目目录下。在弹出的对话框中,确保选中 "Copy items if needed" 选项,然后点击 "Finish"。
2. 添加 Rust 动态库
为了让 Swift 项目能够找到并使用 Rust 动态库,需要将动态库文件添加到 Xcode 项目中:
- 在 Finder 中,导航到 Rust 项目的target/release目录。
- 找到生成的动态库文件libmy_rust_library.dylib在 macOS 上)。
- 拖动这个文件到 Xcode 项目导航器中的项目目录下。在弹出的对话框中,确保选中 "Copy items if needed" 选项,然后点击 "Finish"。
3. 配置 Xcode 项目
为了让 Xcode 项目能够正确地链接和加载 Rust 动态库,需要进行一些配置:
- 选择项目文件,在左侧的 "TARGETS" 列表中选择你的应用目标。
- 选择 "Build Phases" 选项卡。
- 展开 "Link Binary With Libraries" 部分,点击 "+" 按钮,添加刚才拖动到项目中的 libmy_rust_library.dylib 文件。
4. 配置动态库加载路径
选择项目文件,在左侧的 "TARGETS" 列表中选择你的应用目标。
- 选择 "Build Settings" 选项卡。
- 搜索 "Runpath Search Paths"。
- 添加以下路径到 "Runpath Search Paths" 配置项中:`@executable_path/../Frameworks`
5. 编写 Swift 调用代码
在 ViewController.swift 文件中,编写代码调用 Rust 库:
```swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Rust
let greeting = Greeting(name: "World")
print(greeting.greet())
}
}
```
通过这些步骤,已经掌握了如何在 Swift 项目中高效地使用 Rust 库,并了解了相关的高级技术。这种跨语言的集成不仅能利用 Rust 的性能优势,还能享受 Swift 的便捷开发体验。

View File

@@ -1,585 +0,0 @@
# 移动端的“安全气垫”
> 安全气垫是端上一个老生常谈的话题,技术方案已经是好多年前的产物。唯一的问题是不同公司对于安全气垫在拦截到异常的时候,“做与不做”策略的判断不同。比如数组索引越界的时候要返回一个错误位置的值吗?会不会产生业务异常?要一刀切吗?那么本文就尝试回答下这个问题
## 一、一个经典的场景
Weex 源码中,设计了一个 `WXThreadSafeMutableDictionary`。在字典操作的地方使用锁:
```Objective-C
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
{
id originalObject = nil; // make sure that object is not released in lock
@try {
pthread_mutex_lock(&_safeThreadDictionaryMutex);
originalObject = [_dict objectForKey:aKey];
[_dict setObject:anObject forKey:aKey];
}
@finally {
pthread_mutex_unlock(&_safeThreadDictionaryMutex);
}
originalObject = nil;
}
```
这么写的价值:**解锁逻辑「绝对执行」,彻底避免死锁**
这是 `@try-finally` 最核心的价值 ——无论 try 块内发生什么(正常执行、提前 return、抛异常finally 块的解锁逻辑一定会执行
对比无 try-finally 的写法
```Objective-C
// Bad: 若setObject抛异常unlock不会执行→死锁
pthread_mutex_lock(&_mutex);
[_dict setObject:anObject forKey:aKey];
pthread_mutex_unlock(&_mutex);
```
问题:`[_dict setObject:anObject forKey:aKey]` 可能抛异常(比如 aKey = nil 时会触发 NSInvalidArgumentException若没有 finally锁会被永久持有→其他线程调用 lock 时死锁,整个字典无法再操作。
设计优点:
- `@try-finallly`:即使 try 内逻辑出错finally 也会执行 pthread_mutex_unlock保证锁最终释放这是**线程安全的「兜底保障」**
- 注意,不是 `try...catch...finally`: 如果加了 catch 逻辑,则字典的 key 为 nil 产生的崩溃也会被捕获掉,这属于不符合预期的行为。因为 key 为 nil 产生的原因太多了,可能是业务代码异常,也可能是数据异常,也可能是逻辑错误,如果一刀切直接用 `try...catch...finally` 捕获了异常,但是没有配置异常的收集、上报、处理逻辑,属于边界不清晰,本质是为了解决加解锁不匹配而可能带来的线程安全问题,却"多管闲事",把字典 key 为 nil 本该向上跑的异常而卡住了。
这是一个经典的策略问题,端上的异常发生时,安全气垫的“做与不做”问题
聊聊类似网易的大白解决方案或者业界其他公司中,安全气垫虽然保证了代码不 crash影响用户体验但是比如数组本该越界现在却不越界
1. 唯一能做的就是返回一个错误的值比如数组长度为3访问4现在不 crash返回了 0 的值,那是不是产生了业务异常?比如商品价格
2. 不 crash也不返回错误位置的值类似给一个回调告诉业务方出现了异常可以做一些业务层面的提醒或者配置(比如开发阶段商品卡片的价格 Label 显示:商品价格获取错误,数组越界),同时产生的异常案发现场信息和其他的一些数据会上报,用于 APM 平台去分析和定位。
但这也产生一个问题类似数组越界的场景可能10000次里面9999次都正常只有1次异常业务开发为了这万分之一出现的异常还需要写一些异常处理的逻辑比如商品卡片展示价格获取错误数组越界。那字典的 key 为 nil 呢除法的分母为0呢诸如此类类似乐观锁和悲观锁的场景
## 二、核心原则
要解决「安全气垫防崩溃但引发隐性业务异常」「低概率异常导致业务开发冗余逻辑」的核心矛盾,业界的优雅方案核心思路是:「环境差异化策略」+「分层兜底 + 语义化默认值」+「可观测驱动的轻量处理」,既避免线上 Crash又最小化业务侵入同时保证问题可被发现和修复。
**开发阶段让问题 “炸出来”,生产阶段让问题 “软落地”**。从源头减少线上低概率异常的发生,业务开发无需为 “万分之一” 的异常写冗余逻辑
| 环境 | 核心目标 | 策略 |
| ----------- | ---------------------------------- | ----------------------------------- |
| 开发 / 测试 | 提前暴露问题,杜绝上线 | 「零容忍」:直接 Crash + 详细上下文 |
| 生产 | 避免 Crash + 可观测 + 最小业务影响 | 「软兜底」:语义化默认值 + 全量上报 |
## 三、多个方案
### 方案1:环境差异化 + 开发强感知(网易大白/腾讯 Bugly 核心思路)
对 NSArray、NSDictionary、NSNumber 等基础类做运行时 Hook区分环境处理异常
NSArray+DWSafeHook.m
```Objective-C
// 核心逻辑:
// 1. Method Swizzling Hook数组核心读写方法
// 2. Debug环境越界直接Crash+详细上下文,强制研发修复
// 3. Release环境拦截崩溃+上报异常+返回语义化空值,避免用户感知
//
#import "NSArray+DWSafeHook.h"
#import <objc/runtime.h>
#import "DWEnvironmentUtils.h" // 环境判断工具类(自研)
#import "DWCrashReporter.h" // 崩溃上报工具类(自研)
@implementation NSArray (DWSafeHook)
#pragma mark - 对外入口初始化Hook
+ (void)dw_setupSafeHook {
// 防止重复Hook
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Hook 只读数组核心方法
[self dw_swizzleInstanceMethod:@selector(objectAtIndex:)
withNewMethod:@selector(dw_safe_objectAtIndex:)];
[self dw_swizzleInstanceMethod:@selector(objectAtIndexedSubscript:)
withNewMethod:@selector(dw_safe_objectAtIndexedSubscript:)];
// Hook 可变数组核心方法(写操作)
[NSMutableArray dw_swizzleInstanceMethod:@selector(addObject:)
withNewMethod:@selector(dw_safe_addObject:)];
[NSMutableArray dw_swizzleInstanceMethod:@selector(insertObject:atIndex:)
withNewMethod:@selector(dw_safe_insertObject:atIndex:)];
[NSMutableArray dw_swizzleInstanceMethod:@selector(removeObjectAtIndex:)
withNewMethod:@selector(dw_safe_removeObjectAtIndex:)];
});
}
#pragma mark - 私有工具Method Swizzling封装
/// 通用Swizzling方法避免重复代码
/// @param originalSEL 原方法SEL
/// @param newSEL 替换后的方法SEL
+ (void)dw_swizzleInstanceMethod:(SEL)originalSEL withNewMethod:(SEL)newSEL {
Class cls = [self class];
// 获取原方法和新方法
Method originalMethod = class_getInstanceMethod(cls, originalSEL);
Method newMethod = class_getInstanceMethod(cls, newSEL);
if (!originalMethod || !newMethod) {
NSLog(@"[DWCrashGuard] Swizzling失败方法不存在 originalSEL: %@, class: %@",
NSStringFromSelector(originalSEL), NSStringFromClass(cls));
return;
}
// 尝试添加新方法(防止原方法未实现)
BOOL isAdded = class_addMethod(cls,
originalSEL,
method_getImplementation(newMethod),
method_getTypeEncoding(newMethod));
if (isAdded) {
// 替换原方法实现
class_replaceMethod(cls,
newSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 交换方法实现
method_exchangeImplementations(originalMethod, newMethod);
}
}
#pragma mark - Hook实现只读数组读操作核心防越界
/// 拦截 [array objectAtIndex:] 越界
- (id)dw_safe_objectAtIndex:(NSUInteger)index {
// 安全校验:索引越界判断
if (index >= self.count) {
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"objectAtIndex:"];
return [NSNull null]; // Release环境返回语义化空值而非0
}
// 正常逻辑调用原方法Swizzle后此处实际是原objectAtIndex:
return [self dw_safe_objectAtIndex:index];
}
/// 拦截 array[index] 下标访问越界
- (id)dw_safe_objectAtIndexedSubscript:(NSUInteger)index {
if (index >= self.count) {
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"objectAtIndexedSubscript:"];
return [NSNull null];
}
return [self dw_safe_objectAtIndexedSubscript:index];
}
#pragma mark - 私有工具:数组越界统一处理
/// 数组越界异常处理(区分环境)
/// @param index 访问的索引
/// @param method 触发异常的方法名
- (void)dw_handleArrayOutOfBoundsWithIndex:(NSUInteger)index method:(NSString *)method {
// 1. 构建异常上下文(用于上报/调试)
NSDictionary *context = @{
@"crashType": @"NSArrayOutOfBounds",
@"method": method,
@"arrayCount": @(self.count),
@"accessIndex": @(index),
@"arrayDescription": [self description], // 数组内容(便于定位)
@"callStack": [NSThread callStackSymbols], // 完整调用栈
@"timestamp": @([[NSDate date] timeIntervalSince1970]),
@"deviceInfo": [DWEnvironmentUtils deviceInfo] // 设备型号/系统版本等
};
// 2. 区分环境处理
if ([DWEnvironmentUtils isDebugEnvironment]) {
// Debug环境直接Crash+详细日志,强制研发修复
NSString *errorMsg = [NSString stringWithFormat:
@"【网易大白】NSArray越界崩溃\n"
@"方法:%@\n"
@"数组长度:%lu访问索引%lu\n"
@"数组内容:%@\n"
@"调用栈:%@",
method, self.count, index, self, [NSThread callStackSymbols]];
NSAssert(NO, @"%@", errorMsg);
abort(); // 确保CrashNSAssert在Release下失效
} else {
// Release环境拦截崩溃+上报APM平台
[DWCrashReporter reportCrashWithType:@"NSArrayOutOfBounds"
context:context
severity:DWCrashSeverityLow]; // 低优先级(万分之一概率)
NSLog(@"[DWCrashGuard] 拦截NSArray越界%@, count:%lu, index:%lu", method, self.count, index);
}
}
@end
#pragma mark - 可变数组Hook实现写操作防护
@implementation NSMutableArray (DWSafeHook)
/// 拦截 addObject:nil 崩溃
- (void)dw_safe_addObject:(id)anObject {
if (anObject == nil) {
// 构建异常上下文
NSDictionary *context = @{
@"crashType": @"NSMutableArrayAddNil",
@"callStack": [NSThread callStackSymbols],
@"timestamp": @([[NSDate date] timeIntervalSince1970])
};
if ([DWEnvironmentUtils isDebugEnvironment]) {
// Debug环境Crash+提示
NSAssert(NO, @"【网易大白】NSMutableArray添加nil对象调用栈%@", [NSThread callStackSymbols]);
abort();
} else {
// Release环境拦截+上报忽略nil添加
[DWCrashReporter reportCrashWithType:@"NSMutableArrayAddNil"
context:context
severity:DWCrashSeverityLow];
NSLog(@"[DWCrashGuard] 拦截NSMutableArray添加nil对象");
return;
}
}
// 正常逻辑:调用原方法
[self dw_safe_addObject:anObject];
}
/// 拦截 insertObject:atIndex: 越界/nil
- (void)dw_safe_insertObject:(id)anObject atIndex:(NSUInteger)index {
// 1. 校验nil
if (anObject == nil) {
[self dw_handleMutableArrayNilObjectWithMethod:@"insertObject:atIndex:"];
return;
}
// 2. 校验越界
if (index > self.count) { // insert允许index == count追加
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"insertObject:atIndex:"];
return;
}
// 正常逻辑
[self dw_safe_insertObject:anObject atIndex:index];
}
/// 拦截 removeObjectAtIndex: 越界
- (void)dw_safe_removeObjectAtIndex:(NSUInteger)index {
if (index >= self.count) {
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"removeObjectAtIndex:"];
return;
}
// 正常逻辑
[self dw_safe_removeObjectAtIndex:index];
}
#pragma mark - 私有工具可变数组nil处理
- (void)dw_handleMutableArrayNilObjectWithMethod:(NSString *)method {
NSDictionary *context = @{
@"crashType": @"NSMutableArrayInsertNil",
@"method": method,
@"callStack": [NSThread callStackSymbols]
};
if ([DWEnvironmentUtils isDebugEnvironment]) {
NSAssert(NO, @"【网易大白】NSMutableArray插入nil对象方法%@,调用栈:%@", method, [NSThread callStackSymbols]);
abort();
} else {
[DWCrashReporter reportCrashWithType:@"NSMutableArrayInsertNil"
context:context
severity:DWCrashSeverityLow];
NSLog(@"[DWCrashGuard] 拦截NSMutableArray插入nil对象%@", method);
}
}
@end
```
简化下,逻辑基本为:
```Objective-C
// 伪代码Hook NSArray的objectAtIndex:
- (id)safe_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
// 开发环境Crash + 打印完整的案发现场信息 + 调用栈 + 上下文(数组内容、访问索引、业务模块)
#ifdef DEBUG
NSAssert(NO, @"数组越界:数组长度%lu访问索引%lu调用栈%@", self.count, index, [NSThread callStackSymbols]);
abort();
#else
// 生产环境返回语义化空值而非0+ 上报APM
[APMManager reportExceptionWithType:@"数组越界"
context:@{@"arrayCount": @(self.count), @"index": @(index), @"callStack": [NSThread callStackSymbols]}];
return [NSNull null]; // 而非0业务层可统一识别比如价格展示为: '--'
#endif
}
return [self original_objectAtIndex:index];
}
```
核心优势:
- 开发环境:不仅 Crash还打印「业务数据上下文」比如当前是商品详情页、数组是价格数组开发一眼定位问题
- 生产环境返回NSNull而非无意义的 0业务层只需做一次全局 UI 处理(比如所有 Label 展示时判断值为NSNull则显示 “--”),无需为每个场景写逻辑
覆盖场景:
- 数组越界返回NSNull
- 字典 key 为 nilsetObject:forKey:时忽略 nil key 并上报objectForKey:时返回NSNull
- 分母为 0返回INFINITY全局工具类判断isinf(),统一返回 “--”);
- 字符串转数字失败返回NSNull而非 0
### 方案2:声明式全局兜底 + 业务按需关注(阿里/字节)
避免业务层 “零散处理异常”,而是**全局统一兜底 + 业务选择性注册关注的异常类型**。
- 全局层面:所有基础类异常返回 NSNullUI 层统一处理 NSNull 为 “--”/“获取失败”;
- 业务层面:仅对 “核心场景”(如商品价格、支付金额)注册异常回调,非核心场景无需处理:
WXExceptionManager.h
```Objective-C
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 异常类型枚举(替代字符串,避免硬编码)
typedef NS_ENUM(NSUInteger, WXExceptionType) {
WXExceptionTypeArrayOutOfBounds, // 数组越界
WXExceptionTypeDictionaryNilKey, // 字典nil key
WXExceptionTypeDivideByZero, // 分母为0
};
/// 异常上下文Key定义统一常量避免拼写错误
FOUNDATION_EXTERN NSString *const WXExceptionContextBizModuleKey; // 业务模块
FOUNDATION_EXTERN NSString *const WXExceptionContextArrayCountKey;// 数组长度
FOUNDATION_EXTERN NSString *const WXExceptionContextAccessIndexKey;// 访问索引
FOUNDATION_EXTERN NSString *const WXExceptionContextCallStackKey; // 调用栈
FOUNDATION_EXTERN NSString *const WXExceptionContextExtraKey; // 扩展信息
@interface WXExceptionManager : NSObject
/// 单例(全局唯一)
+ (instancetype)sharedManager;
/// 注册异常回调
/// @param type 异常类型
/// @param handler 回调主线程执行避免UI操作崩溃
- (void)registerCallbackForType:(WXExceptionType)type handler:(void(^)(NSDictionary *context))handler;
/// 上报异常内部Hook调用业务层无需调用
/// @param type 异常类型
/// @param context 异常上下文
- (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context;
@end
NS_ASSUME_NONNULL_END
```
WXExceptionManager.m
```Objective-C
#import "WXExceptionManager.h"
#import <pthread/pthread.h>
// 上下文Key常量定义
NSString *const WXExceptionContextBizModuleKey = @"bizModule";
NSString *const WXExceptionContextArrayCountKey = @"arrayCount";
NSString *const WXExceptionContextAccessIndexKey = @"accessIndex";
NSString *const WXExceptionContextCallStackKey = @"callStack";
NSString *const WXExceptionContextExtraKey = @"extra";
@interface WXExceptionManager ()
/// 存储不同异常类型的回调key: WXExceptionType的NSNumbervalue: 回调数组)
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSMutableArray<void(^)(NSDictionary *)> *> *callbackDict;
/// 线程安全锁
@property (nonatomic, assign) pthread_mutex_t mutex;
@end
@implementation WXExceptionManager
+ (instancetype)sharedManager {
static WXExceptionManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_callbackDict = [NSMutableDictionary dictionary];
// 初始化线程锁
pthread_mutex_init(&_mutex, NULL);
}
return self;
}
/// 注册异常回调
- (void)registerCallbackForType:(WXExceptionType)type handler:(void (^)(NSDictionary *))handler {
if (!handler) return;
pthread_mutex_lock(&_mutex);
// 按异常类型分组存储回调
NSNumber *typeKey = @(type);
if (!_callbackDict[typeKey]) {
_callbackDict[typeKey] = [NSMutableArray array];
}
[_callbackDict[typeKey] addObject:handler];
pthread_mutex_unlock(&_mutex);
}
/// 上报异常(触发回调+APM上报
- (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context {
// 1. APM平台上报模拟实际对接公司APM如Bugly/云监控)
NSLog(@"【APM上报】异常类型%lu上下文%@", type, context);
// 2. 触发注册的回调主线程执行避免UI操作崩溃
dispatch_async(dispatch_get_main_queue(), ^{
pthread_mutex_lock(&self->_mutex);
NSNumber *typeKey = @(type);
NSArray *handlers = self->_callbackDict[typeKey];
pthread_mutex_unlock(&self->_mutex);
for (void(^handler)(NSDictionary *) in handlers) {
if (handler) {
handler(context);
}
}
});
}
- (void)dealloc {
pthread_mutex_destroy(&_mutex);
}
#pragma mark - 类方法封装(简化调用)
+ (void)registerCallbackForType:(WXExceptionType)type handler:(void (^)(NSDictionary *))handler {
[[self sharedManager] registerCallbackForType:type handler:handler];
}
+ (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context {
[[self sharedManager] reportExceptionWithType:type context:context];
}
@end
```
使用的地方
```Objective-C
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 1. 初始化价格标签
self.priceLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 200, 200, 40)];
self.priceLabel.font = [UIFont systemFontOfSize:18];
[self.view addSubview:self.priceLabel];
// 2. 模拟业务数据价格数组长度3索引0-2
self.priceArray = @[@"99.9", @"199.9", @"299.9"];
// 标记数组所属业务模块(关键:用于回调筛选)
self.priceArray.bizModule = @"goodsPrice";
// 3. 注册数组越界异常回调(仅关注价格模块)
[WXExceptionManager registerCallbackForType:WXExceptionTypeArrayOutOfBounds handler:^(NSDictionary *context) {
// 筛选:仅处理“价格数组”的越界异常
if ([context[WXExceptionContextBizModuleKey] isEqualToString:@"goodsPrice"]) {
NSLog(@"【业务降级】商品价格数组越界触发UI兜底");
// 生产环境:展示友好提示(而非错误值)
self.priceLabel.text = @"价格获取失败";
// 可选:触发其他降级逻辑(如隐藏价格、显示默认价)
[self triggerPriceFallback];
}
}];
// 4. 模拟异常场景访问索引3越界
[self testPriceArrayOutOfBounds];
}
/// 模拟价格数组越界访问
- (void)testPriceArrayOutOfBounds {
// 访问索引3数组长度3正常索引0-2
id price = [self.priceArray objectAtIndex:3];
// 正常场景显示价格异常场景price是NSNull显示兜底
if ([price isKindOfClass:[NSNull class]]) {
self.priceLabel.text = @"价格获取失败";
} else {
self.priceLabel.text = [NSString stringWithFormat:@"¥%@", price];
}
}
/// 价格降级逻辑(可选)
- (void)triggerPriceFallback {
// 比如:隐藏优惠券、显示默认包邮等
NSLog(@"【降级】隐藏优惠券模块,显示默认包邮文案");
}
@end
```
核心优势:
- 99% 的低概率异常由全局兜底处理(返回 NSNull+UI 显示 --
- 仅核心业务场景(价格、支付)需写少量回调逻辑,避免冗余
效果,类似针对核心的业务场景做专项化监控和优化。
### 方案 3静态分析 + CI/CD 前置拦截(从源头消灭异常)
比运行时 Hook 更优雅的是 **“提前拦截”**。通过静态分析工具Clang Static Analyzer、OCLint、自定义 LLVM 插件),在代码提交/编译阶段检测出:
- 数组越界风险如array[index]中 index 未做长度校验)
- 字典 key 为 nil如dict[nil]
- 分母为 0如a / b中 b 未判 0
- 强制类型转换风险(如(NSNumber *)nil
落地方式:
- 集成到 CI/CD Pipeline代码提交时触发静态分析有风险则阻断合入
- 通过编写 LLVM 插件,就可以在 Xcode 有问题的代码上实时提示 “数组越界风险”“字典 key 可能为 nil”。比如
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
效果:线上低概率异常的根源被提前消灭,问题无法逃逸到现线上,业务层几乎无需处理这类异常(因为代码本身就没有漏洞)
关于如何编写 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 平台不光是告诉你又问题,还会告诉你哪里有问题。

View File

@@ -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 可以是各种各样的:
<ul>
<li>Coursera 的 URL 是coursera-mobile:</li>
<li>Duet 这款游戏的 URL 是x-kumo-duet:</li>
<li>Monument 这款游戏的 URL 是fb690517270143345:</li>
<li>Feedly 的 URL 是fb129765800430121:</li>
<li>扇贝新闻的 URL 是wx95962d02b9c3e2f7:</li>
</ul>
它们目前并没有统一的规则,所以猜测一个应用的意义并不太大,你可以试试,但不要过于指望这种方式。如何查找一个应用的基本 URL Schemes只要那个应用支持 URL Schemes 就能找到。
步骤
<ul>
<li>首先,在 iTunes 找到你想用 URL 打开的 App右键选择在文件夹中显示</li>
<li>然后解压该文件:</li>
<li>解压完毕后,在解压出的文件夹中,找到 .app 文件:</li>
<li>然后选择显示包内容:</li>
<li>找到 info.plist 这个文件,用你电脑里能打开它的 App 打开它Xcode没得说。</li>
<li>然后查找 URLSchemes</li>
<li>在 CFBundleURLSchemes 下的那两行就是该 App 的基本 URL Schemes 了。</li>
</ul>
## 复杂 URL Schemes
参考链接:[URL Scheme](https://sspai.com/post/31500#fnref:2)

View File

@@ -1,49 +0,0 @@
# Swift、OC混编
## apinotes 文件
经常在 Swift、OC 混编的时候,系统会给方法命名等做一些优化,比如 OC 侧的枚举,在 Swift 就是结构体。为了代码规范或者某些因素考量,我们需要做一些约定,不让编译器自动处理,比如一些常见的宏:
- `NS_SWIFT_NAME`
- `NS_TYPED_EXTENSIABLE_ENUM`
- `NS_REFINED_FOR_SWIFT`
宏来配置存在弊端,手动去处理一个工程、一个 SDK 的话假设有10000个方法工作量太大。
Xcode 推出解决方案:
- 创建 `SDK名称.apinotes` 文件
- 放到 SDK 根目录下
- 按照 yaml 格式,编写内容
比如:
```yaml
---
Name: PersonFramework
Classes:
- Name: WorkHard
# SwiftName: WorkHardAtSwift
Methods:
- Selector: "upgradeToLeader:"
Parameters:
- Position: 0
Nullability: O
MethodKind: Instance
SwiftPrivate: true
# Availability: nonswift // WorkHard 类的 upgradeToLeader 方法,在 Swift 侧不允许调用
#AvailabilityMsg: "prefer 'deinit'" // 如果调用,则提示对应的信息
- Selector: "initWithName:"
MethodKind: Instance
DesignatedInit: true
```
更多格式,请参考 [clang::APINOTES](https://clang.llvm.org/docs/APINotes.html)
该方案是 Apple 标准做法不是骚操作Objc 源码中也有使用。如下所示
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APINoteInObjcSourceCode.png" style="zoom:30%" />

View File

@@ -1,10 +0,0 @@
# 对于不可调节高度的UI控件进行改变frame
* 对于不能调节高度的控件比如 UISlider、UISwitch、UIProgressView 等控件的宽高可以用 \(仿射变化\)transform 属性控制高度。
```
myswitch.transform = CGAffineTransformMakeScale(1,5);
```

View File

@@ -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<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end
@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{@"shadows" : [Shadow class],
@"borders" : Border.class,
@"attachments" : @"Attachment" };
}
@end
```
### 黑名单与白名单
```
@interface User
@property NSString *name;
@property NSUInteger age;
@end
@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
return @[@"name"];
}
@end
```
### 数据校验与自定义转换
```
// JSON:
{
"name":"Harry",
"timestamp" : 1445534567
}
// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end
@implementation User
// 当 JSON 转为 Model 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
NSNumber *timestamp = dic[@"timestamp"];
if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
return YES;
}
// 当 Model 转为 JSON 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
if (!_createdAt) return NO;
dic[@"timestamp"] = @(n.timeIntervalSince1970);
return YES;
}
@end
```
### Coding/Copying/hash/equal/description
```
@interface YYShadow :NSObject <NSCoding, NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGSize size;
@end
@implementation YYShadow
// 直接添加以下代码即可自动完成
- (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; }
- (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; }
- (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; }
- (NSUInteger)hash { return [self yy_modelHash]; }
- (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; }
- (NSString *)description { return [self yy_modelDescription]; }
@end
```

View File

@@ -1,56 +0,0 @@
# 实现波浪动画
波浪的形状绘制在 CAShapeLayer 上。通过 CADisplayLink 与屏幕刷新频率同步,每次刷新都绘制新的波浪,并改变小船的位置和角度。另外,水和天空的颜色是渐变的,由 CAGradientLayer 实现,其中,显示水的 CAGradientLayer 需要有波浪形状的 CAShapeLayer 的遮罩\(mask\)。
### CAShapeLayer
CAShapeLayer 的属性 path \(CGPath\)就是图层要显示的形状。把波浪的形状绘制出来,赋值给此属性即可。
### CADisplayLink
创建 CADisplayLink相应的 target 实现屏幕刷新时要调用的方法。把 CADisplayLink 加入 RunLoop 中。通过 isPaused 属性控制 CADisplayLink 是否暂停\(target 是否调用方法\)
```
private var waveLink: CADisplayLink?waveLink = CADisplayLink(target: self, selector: #selector(waveLinkRefresh))
waveLink?.isPaused = true
waveLink?.add(to: .current, forMode: .defaultRunLoopMode)
```
### 绘制波浪
波浪的形状关键是正弦函数曲线
```
y = A*sin(x+B)
```
参数 A 决定了波浪的高度;参数 B 决定了波浪在 x 轴的位置。
用一个属性 currentPhase 表示参数 B。每次屏幕刷新的时候用 currentPhase 绘制,然后更新此属性,加上一个固定的数。这样波浪就会朝左或右匀速移动。
为了使波浪高度逐渐变化,用一个属性表示参数 A然后每次绘制后更新此属性加上一个固定的数直到波浪高度达到目标值。
### 小船的位置和旋转角度
已知小船 x 轴坐标,通过正弦函数可以直接计算出小船的 y 轴坐标。此外,小船需要随着波浪旋转,旋转至船底与波浪表面相切。这就要对正弦函数进行求导
```
y' = A * cos(x + B)
```
用以上式子计算出小船所在位置的 y',表示正弦函数在此处的切线斜率,几何意义是切线与 x 轴的夹角的正切值。反正切即可求出切线与 x 轴的夹角,也就是小船需要旋转的角度
```
angle = atan(y')
```
用以上旋转角度,改变小船视图\(UIView\)的 transform调用 CGAffineTransformRotate 方法,实现小船的旋转。
### CAGradientLayer
CAGradientLayer 默认的颜色渐变方向是由上至下。给 colors 属性赋值一个包含 CGColor 的数组,则图层颜色由上至下,从数组第一个值经中间值渐变至最后一个值。
显示水的 CAGradientLayer 需要呈现波浪形状,需要 CAShapeLayer 的遮罩。把绘制好波浪形状的 CAShapeLayer 赋值给 CAGradientLayer 的 mask 属性即可。

View File

@@ -1,172 +0,0 @@
# 看透构造方法
## 构造方法
* new 方法的内部就是先调用 alloc 方法,再调用 init 方法
* alloc 方法:那个类接受 alloc 消息,那么该方法返回该接受类的对象,并把对象返回
* init 方法是1个对象方法作用初始化对象
* 创建对象的步骤:先使用 alloc 创建1个对象再使用 init 初始化这个对象,才可以使用这个对象
* 使用1个未被初始化的对象是很危险的
* init 方法:作用:初始化对象,为对象赋初始值,叫做构造方法
## 重写init构造方法
* 如果想创建出来的对象的属性值不是默认的初始化值,则需要重写 init 方法
* 重写 init 方法的规范:
* 必须要先调用父类的 init 方法(因为当前类初始化就是通过\`\[父类init\]\`去初始化然后将返回值赋值给self
* 调用 init 方法有可能会失败如果失败直接返回nil
* 判断父类是否初始化成功。如果 `self != nil`,则代表初始化成功
* 如果初始化成功就去初始化当前对象的属性
* 最后返回 self
#### 解惑:
1. 为什么要调用父类的 init 方法?
1. 当前类有 isa 指针,当前类的 isa 指针赋值是通过父类的 init 方法赋值的。
2. 需要保证当前对象的父类属性同时被初始化
2. 重写 init 方法的规范:
```
-(instancetype)init{
if (self = [super init]) {
//todo自定义属性的初始化
}
return self;
}
```
```
//Person
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property NSString* name;
@property int age;
-(void)sayHi;
@end
#import "Person.h"
@implementation Person
-(void)sayHi{
NSLog(@"Hi");
}
-(instancetype)init{
self = [super init];
if (self) {
self.name = @"杭城小刘";
self.age = 22;
}
return self;
}
@end
//测试
Person *p1 = [[Person alloc] init]; //p1.name = "杭城小刘"p1.age =22;
Person *p2 = [Person new]; //p2.name = "杭城小刘"p2.age =22;
```
如果2个类的关系为组合关系且它的一个属性是另一个类的对象那么当该类初始化的时候默认它的属性为 nil那么如何初始化
```
-(instancetype)init{
self = [super init];
if (self) {
self.name = @"lbp";
self.age = 22;
self.pig = [[Pig alloc] init];
}
return self;
}
//测试
Person *p1 = [[Person alloc] init]; //p1.dog != nil
```
## 自定义构造方法
* 现状:虽然每次双肩的对象的属性值不是默认的,但是每次初始化的对象的值都是一样的。
* 需求:每次实例化的对象的属性值由调用者决定
* 解决办法:自定义构造方法
* 自定义构造方法规范:
* 自定义构造方法的返回值为 instancetype
* 方法的命名必须以 initWith 开头
* 方法的实现类似 init 的实现
**注意:此时不能使用 new 来调用。(因为 new 的实现是先 alloc 再 init ,默认 init 的实现是给属性赋默认值)**
```
-(instancetype)initWithName:(NSString *)name andAge:(int)age{
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
```
```
//Person
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property NSString* name;
@property int age;
-(instancetype)initWithName:(NSString *)name andAge:(int)age;
@end
#import "Person.h"
@implementation Person
-(instancetype)init{
self = [super init];
if (self) {
self.name = @"lbp";
self.age = 22;
}
return self;
}
//不能在构造方法之外给self赋值
//编译器认为只有以initWith开头的方法是构造方法
-(instancetype)initWithName:(NSString *)name andAge:(int)age{
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
@end
//测试
Person *p1 = [[Person alloc] init];
Person *p2 = [Person new];
Person *p3 = [[Person alloc] initWithName:@"杭城小刘2号" andAge:23];
```
![init](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-23-5-56-53.png)
![init](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-23-5-57-08.png)
关于“自定义构造方法必须以 initWith 开头”做个实验
![initwith](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-23-6-01-29.png)
报错信息很明显:不能在构造方法之外给 self 赋值
因为,编译器认为只有以 initWith 开头的方法是构造方法

View File

@@ -1,88 +0,0 @@
# Block探究
### 1、Block作为函数参数可以应用到函数式编程
```
self.prepare.play(@"女人");
- (ViewController *(^)(NSString *))play{
NSLog(@"即将吃喝玩乐");
ViewController *(^block)(NSString *) = ^ViewController *(NSString *fun){
NSLog(@"接下来玩%@,好不好?",fun);
return self;
};
return block;
}
- (ViewController *)prepare{
NSLog(@"我们先好好休息一下。😂\n");
return self;
}
```
###2、Block作为函数的返回值可以作为链式编程
```
[self blockAsFunctionalProgramming];
- (void)blockAsFunctionalProgramming{
[self reprepare:^{
NSLog(@"接下来玩女人,好不好?😊");
}];
}
- (void)reprepare:(void(^)(void))replay{
NSLog(@"我们先好好休息一下。😂\n");
replay();
}
@end
```
###3、Block 访问、修改外部变量
* 打开 Terminal.app编写一段c代码
```
#include "stdio.h"
int main(){
printf("Coming\n");
__block int a = 10;
printf("开始->%p %d\n",&a,a);
void(^block)(int a) = ^void(int a){
a += 10;
printf("中间->%p %d\n",&a,a);
};
block(a);
printf("结束->%p %d\n",&a,a);
return 0;
}
```
* 之后用 **gcc** 编译一下。在同层目录下得到一个 **a.out** 的可执行文件。
```
gcc index.c
```
* 之后用 **clang** 编译成 C++ 文件,可以看到系统底层是如何处理 block 外部的变量、以及如何在 block 里面处理变量的。
```
clang -rewrite-objc index.c
```
![clang结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180516-235614@2x.png)
###4、Block 经常造成循环引用
* 如果 block 作为函数参数的话,且这个函数是在对象的层级,那么可能会造成循环应用。 self -> func -> block -> self.
此时需要在 block 里面访问 self 的时候将 self 修饰为 __weak

View File

@@ -1,80 +0,0 @@
# 禅与 Objective-C 编程艺术
## 警告和错误
* 警告
```
#warning Dude, don't compare floating point numbers like this!
```
* 错误
```
#warning Dude, don't compare floating point numbers like this!
```
* 让编译器忽略忽略你这段代码的警告
大多数 iOS 开发者平时并没有和很多编译器选项打交道。一些选项是对控制严格检查(或者不检查)你的代码或者错误的。有时候,你想要用 pragma 直接产生一个异常,临时打断编译器的行为。
当你使用ARC的时候编译器帮你插入了内存管理相关的调用。但是这样可能产生一些烦人的事情。比如你使用 NSSelectorFromString 来动态地产生一个 selector 调用的时候ARC不知道这个方法是哪个并且不知道应该用那种内存管理方法你会被提示 performSelector may cause a leak because its selector is unknown执行 selector 可能导致泄漏,因为这个 selector 是未知的).
如果你知道你的代码不会导致内存泄露,你可以通过加入这些代码忽略这些警告
```
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[myObj performSelector:mySelector withObject:name];
#pragma clang diagnostic pop
```
* 忽略没用使用变量的编译警告
```
#pragma used(foo)
```
```
- (NSInteger)giveMeFive
{
NSString *foo;
#pragma unused (foo)
return 5;
}
```
* 善用代码块
一个 GCC 非常模糊的特性、以及 Clang 也有的特性:代码块如果在闭合的括号内,会返回最后语句的值。
```
self. = ({
NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
[NSURL URLWithString:urlString];
});
```
这个特性非常适合组织小块的代码,给代码阅读者一个重要的入口且减小相关干扰,能让读者聚焦于关键的变量和函数中,此外这个方法有个优点:变量在代码块的区域内有效,可以减小对其他作用域的命名污染。
* 方法参数断言
你的方法可能需要一些参数来满足特定的条件(比如不能为 nil在这种情况下最好使用 **NSParameterAssert()**  来断言条件是否成立
括号内的条件为 false 的时候则断言抛出异常
```
NSParameterAssert(message.length > 0);
```
```
[self testAssert:nil]; //*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: message.length > 0'
- (void)testAssert:(NSString *)message{
NSParameterAssert(!(message.length < 1));
NSLog(@"%@",message);
}
```

View File

@@ -1,35 +0,0 @@
# 修改 UITextField 的 placeholder样式
> 对于 UITextField 的 placeholder 私有属性来说 Apple 不允许我们直接修改但是按照经验我们有2种方式可以实现自定义 placeholder 的样式
### 1、利用 KVC 对 UITextField 的私有属性修改
```
[self.invitecodeTextfield setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[self.invitecodeTextfield setValue:[UIFont systemFontOfSize:35] forKeyPath:@"_placeholderLabel.font"];
```
### 2、利用 Apple 提供的 API 进行修改
UITextField 有个属性 attributedPlaceholder利用它我们可以修改 placeholder 的样式
```
self.invitecodeTextfield.attributedPlaceholder = [LBPHightedAttributedString setAllText:@"我要Testing" andSpcifiStr:@"Testing" withColor:[UIColor redColor] specifiStrFont:[UIFont systemFontOfSize:17]];
```
其中 **LBPHightedAttributedString** 是我封装的一个关于 NSMutableAttributedString 的工具,可以对一个指定的字符串内部的字符串进行全局查找并高亮设置的小工具,具体可以查看地址
[LBPHightedAttributedString](https://github.com/FantasticLBP/BlogDemos/tree/master/LBPAttributedStringTools/LBPHightedAttributedString "LBPHightedAttributedString")

View File

@@ -1,27 +0,0 @@
# UIScrollView 拖拽滑动时收起键盘
> 当一个页面的 UIScrollView/UITableView 上有输入框时,为了较好的体验,就是当滑动的时候需要回收键盘
* 最开始的做法是设置 UIScrollView 的代理位当前控制器,监听 scrollViewWillBeginDragging 方法,找到 keyWindow 并且 endEditing
```
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
[[UIApplication sharedApplication].keyWindow endEditing:YES];
}
```
* 之后偶然有幸看到一个 UIScrollView 的属性"keyboardDismissModel"。实现上述需求只需要一行代码
```
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
```
* keyboardDismissMode 有3个枚举值
* UIScrollViewKeyboardDismissModeNone默认值也就是拖拽时对于键盘没有任何影响。
* UIScrollViewKeyboardDismissModeOnDragdismisses the keyboard when a drag begins当刚拖拽的时候就会回收键盘
* UIScrollViewKeyboardDismissModeInteractivethe keyboard follows the dragging touch off screen, and may be pulled upward again to cancel the dismiss当向下滑动的时候键盘会跟随手势一起下滑当向上滑动的时候键盘也会跟随手势向上滑动而出现。

View File

@@ -1,86 +0,0 @@
# NSRange 设计之美
> typedef struct _NSRange {
NSUInteger location;
NSUInteger length;
} NSRange;
1、看到官方文档的源代码就知道 NSRange 是个结构体,但是如果是你设计一个这样的数据类型你会怎么办??
设计成结构体,然后有些属性怎么办?比如为了开发者方便,让你设计出一个办法,让开发者可以很快知道这个结构体的上限是什么?
苹果就很机智,设计了一个内联函数
```
NS_INLINE NSUInteger NSMaxRange(NSRange range) {
return (range.location + range.length);
}
```
2、什么是内联函数
```
NS_INLINE 返回值类型 函数名(参数列表) {
//函数实现
//return ;
}
```
3、内联函数的应用
比如自定义一个弹窗
```
NS_INLINE void tipWithMessage(NSString *message){
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alerView = [[UIAlertView alloc] initWithTitle:@"提示" message:message delegate:nil cancelButtonTitle:nil otherButtonTitles:nil, nil];
[alerView show];
[alerView performSelector:@selector(dismissWithClickedButtonIndex:animated:) withObject:@[@0, @1] afterDelay:0.9];
});
}
```
4、内联函数的注意事项
内联函数是以代码膨胀为代价, 仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数内代码的时间相比于函数调用的开销大,那么效率收获会很小。
此外每一处调用内联函数的地方都会复制一遍代码,所以会使得程序的体积变大,消耗更多的代码段区域。
所以下面的情况不适合使用内联函数:
* 如果函数体内的代码比较长,使用内联将导致内存消耗比较大
* 如果函数体内出现循环,那么执行函数体内代码的时间要比函数的调用开销大
5、FOUNDATION_EXPORT
查看 NSRange 的代码还会发现一个关键词 **FOUNDATION_EXPORT**,它可以用作定义常量。
FOUNDATION_EXPORT 和 #define 都可用来定义常量。
用法
```
//.h
FOUNDATION_EXPORT NSString *const NickName;
//.m
NSString *const NickName = @"杭城小刘";
```
那么它和 **#define**  有何区别?
FOUNDATION_EXPORT 在检测字符串的值是否相等的时候效率更高
使用** NickName == MyName** 来判断,而 #define 是用 **[NickName isEqualToString:MyName]** 来判断。
* 本质上 FOUNDATION_EXPORT 是比较指针的自己
* \#define 是比较每个字符串是否相等

View File

@@ -1,210 +0,0 @@
# 复制层CAReplicatorLayer
> 对于下面的效果大家是否有实现思路?
>
> 有些人可能要说老夫撸起袖子敲键盘就是干不需要手势交互那么直接用5个**CALayer**,处理不同的位置以及定时器、透明度等等,貌似很简单。
>
> 不不不,今天要带出来的主题是 **CAReplicatorLayer**
![音量柱动画效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmW9ACfS9P5orau43H7gxuxsU4RVMDPD7mPnDKq4pgLmzr.gif)
## 1、CAReplicatorLayer
> /* The replicator layer creates a specified number of copies of its
>
> - sublayers, each copy potentially having geometric, temporal and
> - color transformations applied to it.
>
> *
>
> - Note: the CALayer -hitTest: method currently only tests the first
> - instance of z replicator layer's sublayers. This may change in the
> - future. */
官方给出的意思就不翻译了,使用场景大致是一个形状、特性差不多的 layer我们不需要重复创建可以利用它来实现复制多个 layer ,然后通过 CAReplicatorLayer 的一些属性实现我们的需求。
上述效果的代码
```objective-c
//创建复制层,因为我们做的多个音量柱变化的动画都是一样的,所以创建了一个复制层,这个复制层可以对里面的 sublayer 进行复制,所以我们不需要重复创建了
CAReplicatorLayer *replicatorrLayer = [CAReplicatorLayer layer];
replicatorrLayer.frame = CGRectMake(0, 0, self.contentView.frame.size.width, self.contentView.frame.size.height);
replicatorrLayer.backgroundColor = [UIColor blackColor].CGColor;
self.replicatorrLayer = replicatorrLayer;
[self.contentView.layer addSublayer:replicatorrLayer];
//创建音量震动条
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor whiteColor].CGColor;
CGFloat width = 30;
CGFloat height = 100;
layer.bounds = CGRectMake(0, self.contentView.frame.size.height - height, width, height);
layer.anchorPoint = CGPointMake(0, 1);
layer.position = CGPointMake(0, self.contentView.frame.size.height);
[self.contentView.layer addSublayer:layer];
//创建音量震动动画
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale.y"];
animation.toValue = @0;
animation.duration = 1;
animation.repeatCount = MAXFLOAT;
animation.autoreverses = YES;
[layer addAnimation:animation forKey:nil];
[replicatorrLayer addSublayer:layer];
//* The number of copies to create, including the source object.
replicatorrLayer.instanceCount = 6; //复制 sublayer 的个数包括创建的第一个sublayer 在内的个数
replicatorrLayer.instanceDelay = 0.4; //设置动画延迟执行的时间
replicatorrLayer.instanceAlphaOffset = -0.15; //设置透明度递减
replicatorrLayer.instanceTransform = CATransform3DMakeTranslation(50, 0, 0);
```
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用1-音量柱动画)
## 例子1
![倒影效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmQrU8UxSytnKbWcDVpY5mdy6kmiSHpzyqwt8GykWKNEY2.png)
这里比较简单了,关键代码
```objective-c
CAReplicatorLayer *replicatorLayer = (CAReplicatorLayer *)self.view.layer;
replicatorLayer.instanceCount = 2;
replicatorLayer.instanceTransform = CATransform3DMakeRotation(M_PI, 1, 0, 0);
replicatorLayer.instanceRedOffset -= 0.1;
replicatorLayer.instanceGreenOffset -= 0.1;
replicatorLayer.instanceBlueOffset -= 0.1;
replicatorLayer.instanceAlphaOffset -= 0.3;
```
- 需要说明是这里我用 storyboard 处理的,因为已经拉好了控件,所以我们没办法将图片直接加到复制层上去。间接做法是将 UIViewController 的 view 的 layer 类型改变为 复制层
```
//该方法返回 UIView 的层
//改写 UIView 的层:重写 layerClass 方法
+ (Class)layerClass{
return [CAReplicatorLayer class];
}
```
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用2-倒影效果)
## 例子2
![复制层动画综合应用](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180610-235637-HD.gif)
需求分析:
- 先画图。也就是添加一个滑动手势并监听它。然后强制绘图self setNeedsDisplay
- 添加一个 layer 到 self.layer 上
- 改变当前 view 的 layer 类型。
```
+ (Class)layerClass{
return [CAReplicatorLayer class];
}
```
- 设置 CAReplicatorLayer 的 instanceCount 和 instanceDelay 属性
- 添加了小点,并为小点设置关键帧动画。
- 重置功能实现靠的是清除 path 上面的 points ,并移除 小点上面的动画
```
#import "ViewControllerView.h"
@interface ViewControllerView()
@property (nonatomic, strong) UIBezierPath *path;
@property (nonatomic, weak) CALayer *dotLayer;
@end
@implementation ViewControllerView
+ (Class)layerClass{
return [CAReplicatorLayer class];
}
- (void)awakeFromNib{
[super awakeFromNib];
UIPanGestureRecognizer *tapGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(draw:)];
[self addGestureRecognizer:tapGesture];
self.path = [UIBezierPath bezierPath];
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(-UIScreen.mainScreen.bounds.size.width, 0, 15, 15);
layer.backgroundColor = [UIColor orangeColor].CGColor;
layer.cornerRadius = 7.5;
self.dotLayer = layer;
[self.layer addSublayer:layer];
CAReplicatorLayer *replicatorLayer = (CAReplicatorLayer *)self.layer;
replicatorLayer.instanceCount = 20;
replicatorLayer.instanceDelay = 0.25;
}
- (void)draw:(UIPanGestureRecognizer *)tap{
CGPoint currentPoint = [tap locationInView:self];
if (tap.state == UIGestureRecognizerStateBegan) {
[self.path moveToPoint:currentPoint];
}
else if(tap.state == UIGestureRecognizerStateChanged){
[self.path addLineToPoint:currentPoint];
[self setNeedsDisplay];
}
}
- (void)startAnimation{
//要实现动画围绕着给定的形状执行,那么需要关键帧动画(类比于Flash概念中的关键帧动画只需要给定指定的关键帧其余的帧系统会创建出来。)。关键帧动画的 path 和 values 是互斥的,也就是说如果设置了 values 还设置了 path 那么 path 属性会覆盖 values 属性。
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.path = self.path.CGPath;
animation.duration = 5;
animation.repeatCount = MAXFLOAT;
[self.dotLayer addAnimation:animation forKey:nil];
}
- (void)redraw{
//清空路径:移除 path 上面所有的点,然后重绘
[self.path removeAllPoints];
[self setNeedsDisplay];
//移除动画
[self.dotLayer removeAllAnimations];
}
- (void)drawRect:(CGRect)rect{
[self.path stroke];
}
@end
```
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用3-粒子闪烁效果)
### CALayer 层的动画有2个概念非常重要AnchorPoint 和 position
- postion 用来确定 layer 层在父层中的位置
- anchorPoint 用来确定 layer 身上哪个点会在 position 所指的位置。

View File

@@ -1,172 +0,0 @@
# CAShapeLayer
> 一言以蔽之CAShapeLayer 可以根据贝塞尔曲线描绘出的路径而生成对应的图形
## 综合例子
- 效果图
![QQ粘性动画](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmUhGFJgxj6ofpvZp6MK3bqaH2hLgq9vfKsnwDmMisahGu.gif)
- 关键技术点剖析
- 分析 QQ 粘性动画的关键点就是当手势拖动时候2个圆之间那个形状怎么绘制
答案将2个圆的某一时刻之间形成的形状用数学抽象来计算。
![轨迹分解](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmQUyUSLYB3VGs4juzfsEdncyWetz7BTN2GFtURbmEYbEY.png)
- 拖动到超过某个范围的时候怎么执行爆炸动画
UIImageView 可以执行帧动画,类似于 Flash 效果
### 关键代码
```
- (void)pan:(UIPanGestureRecognizer *)pan{
//当前移动的偏移量
CGPoint transP = [pan translationInView:self];
//改变红点的位置
//transform并没有修改自身的 centercenter 是 layer 的position只是修改了 frame
NSLog(@"偏移量:%@",NSStringFromCGPoint(transP));
CGPoint center = self.center;
center.x += transP.x;
center.y += transP.y;
self.center = center;
//self.transform = CGAffineTransformTranslate(self.transform, transP.x, transP.y);
//手势复位:设置坐标原点位上次的坐标
[pan setTranslation:CGPointZero inView:self];
CGFloat distance = [self distanceWith:self.smallCircle bigCircle:self];
NSLog(@"%f",distance);
CGFloat smallCircleRadius = self.bounds.size.width * 0.5;
smallCircleRadius = smallCircleRadius - distance/10;
if (smallCircleRadius < 3) {
smallCircleRadius = 3;
}
self.smallCircle.bounds = CGRectMake(0, 0, smallCircleRadius*2, smallCircleRadius*2);
self.smallCircle.layer.cornerRadius = smallCircleRadius;
if (self.smallCircle.hidden == NO) {
//返回一个不规则的路径
UIBezierPath *path = [self drawTracertWithSmallCircle:self.smallCircle bigCircle:self];
//将形状转换为一个形状图层
self.shapeLayer.path = path.CGPath;//根据路径生成形状
}
//创建形状图层
[self.superview.layer insertSublayer:self.shapeLayer atIndex:0];
if (distance > 60) {
self.smallCircle.hidden = YES;
[self.shapeLayer removeFromSuperlayer];
}
if (pan.state == UIGestureRecognizerStateEnded) {
//结束手势
if (distance < 60) {
[self.shapeLayer removeFromSuperlayer];
self.center = self.smallCircle.center;
self.smallCircle.hidden = NO;
}
else{
//手势拖拽超过60则播放一个动画
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds];
NSMutableArray *images = [NSMutableArray array];
for (int i=0; i<8; i++) {
NSString *imageName = [NSString stringWithFormat:@"%d",i+1];
UIImage *image = [UIImage imageNamed:imageName];
[images addObject:image];
}
imageView.animationImages = images;
[imageView setAnimationDuration:1];
[imageView startAnimating];
[self addSubview:imageView];
//动画结束移除本身
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self removeFromSuperview];
});
}
}
}
- (CGFloat )distanceWith:(UIView *)smallCircle bigCircle:(UIView *)bigScirle{
CGFloat offsetX = bigScirle.frame.origin.x - smallCircle.frame.origin.x;
CGFloat offsetY = bigScirle.frame.origin.y - smallCircle.frame.origin.y;
return sqrt( pow(offsetX, 2) + pow(offsetY, 2));
}
//将2个圆运行的变化轨迹用代码模拟
- (UIBezierPath *)drawTracertWithSmallCircle:(UIView *)smallCircle bigCircle:(UIView *)bigCircle{
CGFloat X1 = smallCircle.center.x;
CGFloat X2 = bigCircle.center.x;
CGFloat Y1 = smallCircle.center.y;
CGFloat Y2 = bigCircle.center.y;
CGFloat r1 = smallCircle.bounds.size.width/2;
CGFloat r2 = bigCircle.bounds.size.width/2;
CGFloat d = [self distanceWith:smallCircle bigCircle:bigCircle];
//Ø 代表角度
CGFloat SinØ = (X2 - X1)/d;
CGFloat CosØ = (Y2 - Y1)/d;
CGPoint pointA = CGPointMake(X1 - r1*CosØ, Y1 + r1*SinØ);
CGPoint pointB = CGPointMake(X1 + r1*CosØ, Y1 - r1*SinØ);
CGPoint pointC = CGPointMake(X2 + r2*CosØ, Y2 - r2*SinØ);
CGPoint pointD = CGPointMake(X2 - r2*CosØ, Y2 + r2*SinØ);
CGPoint pointO = CGPointMake(X1 + SinØ *d/2, Y1 + CosØ*d/2);
CGPoint pointP = CGPointMake(X1 + SinØ *d/2,Y1 + CosØ*d/2 );
//描述路径
UIBezierPath *path = [UIBezierPath bezierPath];
//AB
[path moveToPoint:pointA];
[path addLineToPoint:pointB];
//BC(曲线)
[path addQuadCurveToPoint:pointC controlPoint:pointP];
//CD
[path addLineToPoint:pointD];
//DA(曲线)
[path addQuadCurveToPoint:pointA controlPoint:pointO];
return path;
}
```
完整的代码,[Github地址](https://github.com/FantasticLBP/BlogDemos/tree/master/QQ粘性动画)

View File

@@ -1,65 +0,0 @@
# 仿微博弹簧动画
> 老玩微博,最近在研究动画,周末抽空写了个发微博的动画
# 实现步骤
- 首先模打出一个控制器
- 这个控制器用来显示多个按钮。(按钮是图文上下排列的,所以我们需要自定义按钮的布局样式)
- 动画思路:先在界面添加好几个 UIButton之后给每个 button 添加**y**方向的平移动画 -> 设置一个定时器,每次执行的时候依次取出按钮,将按钮添加一个弹簧动画(**usingSpringWithDamping **)将形变动画恢复原位
- 给按钮添加2种事件按下的事件、点击后抬起的事件
## 关键代码
```
//开始时让所有按钮都移动到最底部
btn.transform = CGAffineTransformMakeTranslation(0, self.view.bounds.size.height);
//添加定时器
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(update) userInfo:nil repeats:YES];
- (void)update{
if (self.btnIndex == self.btnArray.count) {
[self.timer invalidate];
return ;
}
VerticalStyleButton *button = self.btnArray[self.btnIndex];
//弹簧动画
[UIView animateWithDuration:0.3 delay:0.2 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{
button.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
}];
self.btnIndex++;
}
- (void)btnClick:(UIButton *)button{
[UIView animateWithDuration:0.25 animations:^{
button.transform = CGAffineTransformMakeScale(1.2, 1.2);
}];
}
- (void)btnClick1:(UIButton *)button{
[UIView animateWithDuration:0.25 animations:^{
button.alpha = 0;
button.transform = CGAffineTransformMakeScale(2, 2);
}];
}
```
# 效果图
![发微博动画效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180610-225937-HD.gif)
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/微博发帖动画)

View File

@@ -1,39 +0,0 @@
# UILabel 给关键字模糊匹配并高亮
> 有些情况就是需要查找某个字符串并高亮,但有些需求就是需要全局模糊查找,找到符合的字符串并高亮。造了个小轮子
###效果图
![模糊匹配文字并高亮
](https://raw.githubusercontent.com/FantasticLBP/BlogDemos/master/image/QQ20180610-235439%402x.png)
```
#pragma mark -- 设置在一个文本中所有特殊字符的特殊颜色
+ (NSMutableAttributedString *)setAllText:(NSString *)allStr andSpcifiStr:(NSString *)keyWords withColor:(UIColor *)color specifiStrFont:(UIFont *)font{
NSMutableAttributedString *mutableAttributedStr = [[NSMutableAttributedString alloc] initWithString:allStr];
if (color == nil) {
color = [UIColor redColor];
}
if (font == nil) {
font = [UIFont systemFontOfSize:17];
}
for (NSInteger j=0; j<=keyWords.length-1; j++) {
NSRange searchRange = NSMakeRange(0, [allStr length]);
NSRange range;
NSString *singleStr = [keyWords substringWithRange:NSMakeRange(j, 1)];
while
((range = [allStr rangeOfString:singleStr options:NSLiteralSearch range:searchRange]).location != NSNotFound) {
//改变多次搜索时searchRange的位置
searchRange = NSMakeRange(NSMaxRange(range), [allStr length] - NSMaxRange(range));
//设置富文本
[mutableAttributedStr addAttribute:NSForegroundColorAttributeName value:color range:range];
[mutableAttributedStr addAttribute:NSFontAttributeName value:font range:range];
}
}
return mutableAttributedStr;
}
```
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/LBPAttributedStringTools)

View File

@@ -1,155 +0,0 @@
# JavascriptCore
1、JSCore 是基于 webkit 以 C/C++ 实现的一个 js 包装,让 js 和 Native 交互变得更加简单。
- JScontext
JSContext 代表一个 JavaScript 的执行环境的一个实例。所有JavaScript执行都是在上下文内进行。JSContext还用于管理对象的生命周期内 JavaScript 的虚拟机
- JSValue
JSValue 是用来接收 JSContext 执行后的返回结果。JSValue 可以是 JS 的任意类型(变量、对象、函数...
- JSManagedValue
JSManagedValue 是对 JSValue 的封装,可以解决 JS 和 OC 之间循环引用的问题。JSManagedValue 最常用的用法就是安全的从内存堆区里面引用 JSValue 对象.如果 JSValue 存储在内存的堆区的方式是不正确的,很容易造成循环引用,然后导致 JSContext 对象不能正确的释放掉.
- JSExport
是一个协议,用来将 Native 对象暴露给 JS这个对象可以指向给自身和别的对象。
- JSVirtualMachine
管理 JS 对象空间和所需的资源
2、Native 调用 JS
- 加载 JS 代码
(JSValue *)evaluateScript:(NSString *)script;
- 调用 JS 方法
JSvalue *callBack = self.context[@"sayHi"];
[callback callWithArguments:@[@"杭城小刘"]];
3、JS 调用 Native
- 通过 Block 实现。然后在 JS 中直接调用方法即可。需要注意的是在 Block 内部不要直接使用外部定义的 JScontext 对象或 JSValue ,应该作为参数传递进来,或者通过 + (JSContext *)currentContext; 来获取。否则会造成循环引用、内存无法被正确回收
self.context[@"showMessage"] = ^(NSString *message){
UIAlertController *alertCtr = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil];
[alertCtr addAction:cancel];
//注意方法是在子线程中执行的需要跟新UI的话需要切入主线程。
dispatch_async(dispatch_get_main_queue(), ^{
[weakSealf presentViewController:alertCtr animated:YES completion:nil];
});
};
- 通过 JSExport 协议实现; JS 需要通过 OC 中注入的对象来调方法,那么方法需要在协议中声明,并且在注入的对象中实现;在 webview 加载完成的时候注入实现协议的 Native 对象
//声明协议
@proptocol JSInject<JSExport>
- (void)showMessage:(NSString *)message;
@end
//实现相应的协议
- (void)showMessage:(NSString *)message{
UIAlertController *alertCtr = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil];
[alertCtr addAction:cancel];
//注意方法是在子线程中执行的需要跟新UI的话需要切入主线程。
dispatch_async(dispatch_get_main_queue(), ^{
[weakSealf presentViewController:alertCtr animated:YES completion:nil];
});
}
//注入
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//从webview上获取相应的JSContext。
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//注入JS需要的“OC”对象
self.context[@"Bridge"] = [JSInject new];
}
举个🌰
JS call Native
//对外要暴露的 native 对象(其中挂载了一些属性和方法)
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol PersonInjectExport<JSExport>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *hobby;
- (id)sayHi;
@end
@interface PersonInject : NSObject<PersonInjectExport>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *hobby;
- (id)sayHi;
@end
// viewcontroller
- (void)webViewDidFinishLoad:(UIWebView *)webView{
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
PersonInject *person = [[PersonInject alloc] init];
person.name = @"杭城小刘";
person.hobby = @"Coding、Movie、Music、Table tennis、Fit";
self.jsContext[@"lbp"] = person;
}
//JS
<body>
嗨。大家好我是
<p id="name">***</p>
<button id="show">那你做个自我介绍吧</button>
</body>
<script>
var u = navigator.userAgent
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1
var isiOS = u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) //ios终端
document.getElementById("show").onclick = function () {
if (isiOS) {
document.getElementById("name").innerHTML = lbp.name;
setTimeout(() => {
alert(lbp.sayHi());
}, 1000);
}
}
</script>
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);
}

View File

@@ -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的流程
![控制器加载view的流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2287777-b6128646373dfffb.png)
* 控制器的 `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方法存在则直接返回所以界面先是绿色再是棕色最后是红色
#### 来一个官方解释
![Apple 文档](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2287777-8ff7c3b976ffb29a.png)

View File

@@ -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** 中间即可,如图所示
![DEBUG宏在头文件](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180626-144101@2x.png)
![DEBUG宏在代码块](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180626-144240@2x.png)
* 测试用的 .a 和 .framework
对于拖拽到工程中的 .a .framework 静态库,可以在 **target-&gt;Build Settings-&gt;Search Paths**这2个选项分别设置 **Library Search Paths**和**Framework Search Paths**这2个选项。如果我们需要在测试的时候会用到那么我们可以将 **Debug** 对应的值留下,删掉**Release** 对应的值。这样我们打包 Release 包的时候就不会包含不需要的包。
![不需要的包删除即可](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180626-144819@2x.png)
* CocoPods 引入的库
对于 CocoPods 方式引入的库,在配置的时候就可以处理掉,比如下面的方式
```
platform: iOS, '8.0'
...
pod 'PonyDebugger', :configurations => ['Debug']
```
7. App Store Connect 经常在上架的时候需要开发人员判断是否满足出口合规的证明,每次写都很麻烦,所以可以在工程里面的 plist 里面进行设置。
```
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
```
8. 让 Xcode 折叠代码
在 VS Code 或者其他 IDE 里面都具有代码折叠的功能Xcode 也支持代码折叠功能,但是默认没有开启。所以我们需要做的就是打开代码折叠功能。步骤:打开 Xcode - Preference - Text Editing - 在「Show」模块下面勾选「Code folding ribbon」。这样 Xcode 就具备代码折叠的功能了。
快捷键:
- command + option + 左右方向键 折叠或展开鼠标光标所在位置的代码
- command + option + shift + 左右方向键:折叠或展开当前页面全部的方法(函数)
9. 几种设置废弃 Api 的方法
- __deprecated
- NS_UNAVAILABLE。`- (instancetype)init NS_UNAVAILABLE;`
- #define MJRefreshDeprecated(instead) NS_DEPRECATED(2_0, 2_0, 2_0, 2_0, instead)
```
MJRefreshDeprecated("请使用automaticallyChangeAlpha属性");
```
- DEPRECATED_ATTRIBUTE
```
@property (nonatomic, strong, readonly) UILabel *dateLabel DEPRECATED_ATTRIBUTE;
```
- DEPRECATED_MSG_ATTRIBUTE
```
@property (nonatomic, assign) NSStringEncoding stringEncoding DEPRECATED_MSG_ATTRIBUTE("The string encoding is never used. AFHTTPResponseSerializer only validates status codes and content types but does not try to decode the received data in any way.");
```
- @property(nullable, nonatomic, strong) IBOutlet NSLayoutConstraint *IQLayoutGuideConstraint __attribute__((deprecated("Due to change in core-logic of handling distance between textField and keyboard distance, this layout contraint tweak is no longer needed and things will just work out of the box regardless of constraint pinned with safeArea/layoutGuide/superview.")));
- + (CLLocationDistance)getCurrentLocationDistanceFilter __deprecated_msg("废弃方法空实现使用distanceFilter属性替换");
- + (NSString *)getWeiboAppSupportMaxSDKVersion __attribute__((deprecated));
- #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
result = [self sizeWithFont:font constrainedToSize:size lineBreakMode:lineBreakMode];
#pragma clang diagnostic pop
10. Xcode Instruments 内存泄漏检测工具 Leaks 在内存检测后,无法看到具体的堆栈信息。
![Leaks](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-11-25-InstrumentMemoryLeaks.jpg)
涂上右下方的 `Heaviest Stack Trace` 模块看不到对应的堆栈信息。一番定位问题后发现是工程项目在 debug 阶段Build Setting 中的 **Debug Information Format** 选项的 debug 条目是没有 dSYM 文件的,我们要想看到堆栈信息,就必须选择 `DWARF with dSYM File` 选项。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-11-25-BuildSettingsDebugInformationFormat.png)
DWARF即 ***Debug With Arbitrary Record Format*** ,是一个标准调试信息格式,即调试信息。这部分信息可以查看我的[这篇文章](./1.74.md)中讲 iOS 符号化的部分。
11. 将 OC 代码还原为 C++ 代码
```objectivec
// 方法1
clang -rewrite-objc xxxx.m -o xxxx.cpp
// 方法2
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxxx.m -o xxxx.cpp
```
12. 工程打开汇编Debug 更多信息
菜单栏Xcode -> Debug -> Debug Workflow -> Always Show Disassembly 可以查看汇编。
查看汇编可以从更深层了解当前函数的汇编层面的执行,为 objc 源码分析提供信
息避免方向性错误,结合 memory read 可以更清楚的看到寄存器之间是如何互相配合
处理配合的;使用汇编查看流程,可以在不确定源码出处和执行流程的情况下,跟踪内
部代码,并可以找到出处!同时,结合下符号断点的方式,能够更清晰的跟踪源码实
现。
13. Xcode 运行项目,模拟器启动失败。报错 `Failed to start launchd_sim: could not bind to session, launchd_sim may have crashed or quit respond`
- 关闭Xcode在终端中键入以下命令sudo chmod 1777 /tmp
- 清理此路径中的dyld文件夹/Library/Developer/CoreSimulator/Caches
- 重新启动Xcode完成
14. Xcode 自动设置 __nonnull.
升级到 Xcode 10 , 新建类的时候发现头文件中多了2个宏
NS_ASSUME_NONNULL_BEGIN
NS_ASSUME_NONNULL_END
作用
这两个东西是Nonnull区域设置(Audited Regions) 。
这两个宏之间的代码里的所有简单指针对象都被默认为 ___nonnull我们只需要去指定 __nullable 的指针。
2014 年的 Apple WWDC 发布了强语言 swift 必须要指定一个对象是否为空。为了迎合swiftOC中增加了 __nullable 和 ___nonnull 用于指定对象是否为空。
每个属性、方法都指定 ___nonnull 和 __nullable 是一件非常繁琐的事。为了减轻开发工作量苹果提供了两个宏NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 。这两个宏之间的代码里的所有简单指针对象都被默认为 ___nonnull我们只需要去指定 __nullable 的指针。
解决的问题
- 减少冗余代码:避免在每个属性、方法参数或返回值前手动添加 nonnull提高代码简洁性。
- 提升类型安全性:编译器会对默认的 nonnull 指针进行静态检查,传递 nil 时会触发警告。
- 改善 Swift 互操作性Swift 能识别这些注解,将 nonnull 指针转换为非可选类型(如 String将 nullable 指针转换为可选类型(如 String?),使接口更清晰。

View File

@@ -1,30 +0,0 @@
# 终端效率
## tree
如果要在终端查看当前目录的层级结构,不妨了解下**tree**。它可以以树状的形式展示当前的目录结构。
安装:
在终端输入**brew install tree**
使用:
在当前目录下,显示树状目录结构。**tree -L 2 -d**。其中 -L 表示遍历的深度,-d 表示只显示目录。
## 合并写法
之前在终端操作的时候都是老老实实一行行的写代码,最近发现可以合并起来写。比如
```
//写法一
cd Desktop
mkdir awesome-project
cd awesome-project
//写法二
cd Desktop && mkdir awesome-project && cd awesome-project
```

View File

@@ -1,303 +0,0 @@
# 终极截屏
- **-(void)snapshotForView:(__kindof UIView *)view;**
今天新学到这种写法__kindof 是苹果声明的一个特性,是 Xcode7 出现的新特性。
- 假如我们想声明一个方法,这个方法的参数必须是一个 UIView 类型的对象,那么我们应该可以写成下面这个样子
```
-(void)snapshotForView:(UIView *)view;
```
- 那么我们想声明一个方法,这个方法的参数必须是 UIView 及其 UIView 的子类,那么前一种写法就满足不了我们的需求了,这时候引入了 __kindof 方法
```
-(void)snapshotForView:(__kindof UIView *)view;
```
**UIWebView 截图**
对 UIWebView 截图比较简单renderInContext 这个方法相信大家都不会陌生,这个方法是 CALayer 的一个实例方法,可以用来对大部分 View 进行截图。我们知道UIWebView 承载内容的其实是作为其子 View 的 UIScrollView所以对 UIWebView 截图应该对其 scrollView 进行截图。具体的截图方法如下:
```
- (void)snapshotForScrollView:(UIScrollView *)scrollView
{
// 1. 记录当前 scrollView 的偏移和位置
CGPoint currentOffset = scrollView.contentOffset;
CGRect currentFrame = scrollView.frame;
scrollView.contentOffset = CGPointZero;
// 2. 将 scrollView 展开为其实际内容的大小
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
// 3. 第三个参数设置为 0 表示设置为屏幕的默认缩放因子
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, 0);
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 4. 重新设置 scrollView 的偏移和位置,还原现场
scrollView.contentOffset = currentOffset;
scrollView.frame = currentFrame;
}
```
**WKWebView 截图**
虽然 WKWebView 里也有 scrollView但是直接对这个 scrollView 截图得到的是一片空白的,具体原因不明。一番 Google 之后可以看到好些人提到 drawViewHierarchyInRect 方法, 可以看到这个方法是 iOS 7.0 开始引入的。官方文档中描述为:
> Renders a snapshot of the complete view hierarchy as visible onscreen into the current context.
注意其中的 **visible onscreen**,也就是将屏幕中可见部分渲染到上下文中,这也解释了为什么对 WKWebView 中的 scrollView 展开为实际内容大小,再调用 drawViewHierarchyInRect 方法总是得到一张不完整的截图(只有屏幕可见区域被正确截到,其他区域为空白)。
不过,这样倒是给我们提供了一个思路,可以将 WKWebView 按屏幕高度裁成 n 页,然后将 WKWebView 一页一页的往上推,每推一页就调用一次 drawViewHierarchyInRect 将当前屏幕的截图渲染到上下文中,最后调用 UIGraphicsGetImageFromCurrentImageContext 从上下文中获取的图片即为完整截图。
核心代码如下:
```
- (void)snapshotForWKWebView:(WKWebView *)webView
{
// 1
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
[webView.superview addSubview:snapshotView];
// 2
CGPoint currentOffset = webView.scrollView.contentOffset;
...
// 3
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
[webView removeFromSuperview];
[containerView addSubview:webView];
// 4
CGSize totalSize = webView.scrollView.contentSize;
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
webView.scrollView.contentOffset = CGPointZero;
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
[self drawContentPage:0 maxIndex:page completion:^{
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 8
[webView removeFromSuperview];
...
}];
}
- (void)drawContentPage(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
{
// 5
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(containerView.bounds), containerView.bounds.size.width, containerView.frame.size.height);
CGRect myFrame = webView.frame;
myFrame.origin.y = -(index * containerView.frame.size.height);
webView.frame = myFrame;
// 6
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
// 7
if (index < maxIndex) {
[self drawContentPage:index + 1 maxIndex:maxIndex completion:completion];
} else {
completion();
}
}
```
代码注意项如下(对应代码注释中的序号):
1. 为了截图时对 frame 进行操作不会出现闪屏等现象,我们需要盖一个“假”的 webView 到现在的位置上,并将真正的 webView “摘下来”。调用 snapshotViewAfterScreenUpdates 即可得到这样一个“假”的 webView
2. 保存真正的 webView 的偏移、位置等信息,以便截图完成之后“还原现场”
3. 用一个新的视图承载“真正的” webView这个视图也是绘图所用到的上下文
4. 将 webView 按照实际内容高度和屏幕高度分成 page 页
5. 得到每一页的实际位置,并将 webView 往上推到该位置
6. 调用 drawViewHierarchyInRect 将当前位置的 webView 渲染到上下文中
7. 如果还未到达最后一页,则递归调用 drawViewHierarchyInRect 方法进行渲染;如果已经渲染完了全部页,则回调通知截图完成
8. 调用 UIGraphicsGetImageFromCurrentImageContext 方法从当前上下文中获取到完整截图,将第 2 步中保存的信息重新赋予到 webView 上,“还原现场”
注意:我们的截图方法中有对 webView 的 frame 进行操作,如果其他地方如果有对 frame 进行操作的话,是会影响我们截图的。所以在截图时应该禁用掉其他地方对 frame 的改变,就像这样:
```
- (void)layoutWebView
{
if (!_isCapturing) {
self.wkWebView.frame = [self frameForWebView];
}
}
```
```
#import <UIKit/UIKit.h>
@class PPSnapshotHandler;
@protocol PPSnapshotHandlerDelegate <NSObject>
@optional
- (void)snapshotHandler:(PPSnapshotHandler *)snapshotHandler didFinish:(UIImage *)captureImage forView:(UIView *)view;
@end
@interface PPSnapshotHandler : NSObject
+ (instancetype)defaultHandler;
@property (nonatomic, weak) id<PPSnapshotHandlerDelegate> delegate;
- (void)snapshotForView:(__kindof UIView *)view;
@end
#import "PPSnapshotHandler.h"
#import <WebKit/WebKit.h>
#define DELAY_TIME_DRAW 0.1
@interface PPSnapshotHandler () {
BOOL _isCapturing;
UIView *_captureView;
}
@end
@implementation PPSnapshotHandler
+ (instancetype)defaultHandler
{
static dispatch_once_t onceToken;
static PPSnapshotHandler *defaultHandler = nil;
dispatch_once(&onceToken, ^{
defaultHandler = [[PPSnapshotHandler alloc] init];
});
return defaultHandler;
}
#pragma mark - public method
- (void)snapshotForView:(__kindof UIView *)view
{
if (!view || _isCapturing) {
return;
}
_captureView = view;
if ([view isKindOfClass:[UIScrollView class]]) {
[self snapshotForScrollView:(UIScrollView *)view];
} else if ([view isKindOfClass:[UIWebView class]]) {
UIWebView *webView = (UIWebView *)view;
[self snapshotForScrollView:webView.scrollView];
} else if ([view isKindOfClass:[WKWebView class]]) {
[self snapshotForWKWebView:(WKWebView *)view];
}
}
#pragma mark - WKWebView
- (void)snapshotForWKWebView:(WKWebView *)webView
{
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
snapshotView.frame = webView.frame;
[webView.superview addSubview:snapshotView];
CGPoint currentOffset = webView.scrollView.contentOffset;
CGRect currentFrame = webView.frame;
UIView *currentSuperView = webView.superview;
NSUInteger currentIndex = [webView.superview.subviews indexOfObject:webView];
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
[webView removeFromSuperview];
[containerView addSubview:webView];
CGSize totalSize = webView.scrollView.contentSize;
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
webView.scrollView.contentOffset = CGPointZero;
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
[self drawContentPage:containerView webView:webView index:0 maxIndex:page completion:^{
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[webView removeFromSuperview];
[currentSuperView insertSubview:webView atIndex:currentIndex];
webView.frame = currentFrame;
webView.scrollView.contentOffset = currentOffset;
[snapshotView removeFromSuperview];
self->_isCapturing = NO;
if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) {
[self.delegate snapshotHandler:self didFinish:snapshotImage forView:self->_captureView];
}
}];
}
- (void)drawContentPage:(UIView *)targetView webView:(WKWebView *)webView index:(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
{
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(targetView.bounds), targetView.bounds.size.width, targetView.frame.size.height);
CGRect myFrame = webView.frame;
myFrame.origin.y = -(index * targetView.frame.size.height);
webView.frame = myFrame;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(DELAY_TIME_DRAW * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
if (index < maxIndex) {
[self drawContentPage:targetView webView:webView index:index + 1 maxIndex:maxIndex completion:completion];
} else {
completion();
}
});
}
#pragma mark - UIScrollView
- (void)snapshotForScrollView:(UIScrollView *)scrollView
{
CGPoint currentOffset = scrollView.contentOffset;
CGRect currentFrame = scrollView.frame;
scrollView.contentOffset = CGPointZero;
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, UIScreen.mainScreen.scale);
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
scrollView.contentOffset = currentOffset;
scrollView.frame = currentFrame;
if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) {
[self.delegate snapshotHandler:self didFinish:snapshotImage forView:_captureView];
}
}
@end
```

View File

@@ -1,28 +0,0 @@
# 推送
> 1、现在 App 开发推送功能,一般都是接入极光推送,那么为什么极光推送就可以实现推送呢?
2、极光推送做了哪些事情与 APNS 怎么交互的?
带着这2个问题来看看推送吧
## 一、推送原理
![推送原理](/assets/4316713-49ef454cca917acd.jpg)
(这张图转载于网络)
说说推送的步骤:
1、你的 App 需要推送服务,要向苹果的 APNS 注册推送功能
2、当苹果 APNS 推送服务器收到你的注册请求后会返回给你一串 device token
3、当应用收到 device token 后,需要将 device token 传给自己的应用服务器(自己公司的服务端)
4、当你需要为你的应用推送消息的时候自己的应用服务器会将消息以及 device token 打包发送给苹果的 APNS。
5、APNS 再将消息推送给你的 手机 App
## 不接入极光推送的话,自己怎么做推送功能
参考这篇文章:[自己做推送](https://blog.csdn.net/shenjie12345678/article/details/41120637)
## 所以极光推送逗帮我们做了什么?
简化了获取 device token 的步骤,我们将申请号的证书上传到极光服务器,程序运行接入极光 SDK ,手机获取 device token, 然后将 device token 上传给极光推送服务器,。

View File

@@ -1,55 +0,0 @@
# App 评分
> 经常有这样的需求-引导用户在合适的时机对 App 做出好评。本文就尝试谈一谈这一块的一些知识
1. 评分的方式
可以跳出应用对 App 进行评分,也可以在应用内进行评分(>= iOS 10.3)。
2. 跳出 App 评分
利用系统方法打开 URL跳到 App store 后跳转到自己 App 的评价页面)
```
NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]];
```
3. 应用内评分
iOS 10.3 之后系统为我们评分这个需求引入 **StoreKit**。利用它,我们可以很方便地在应用内对 App 进行快速评分,而不用跳出去。
+ 在 App 内部打开 App store并跳转到App 评价页面
```
#import <StoreKit/StoreKit.h>
SKStoreProductViewController *storeVC = [[SKStoreProductViewController alloc] init];
storeVC.delegate = self;
[storeVC loadProductWithParameters:@{SKStoreProductParameterITunesItemIdentifier:@"1401834682"} completionBlock:^(BOOL result, NSError * _Nullable error) {
if (error) {
}else{
[self presentViewController:storeVC animated:YES completion:nil];
}
}];
```
+ 在 App 内弹出评分对话框,用户星级评分后可以继续输入文字
```
if (@available(iOS 10.3, *)) {
if([SKStoreReviewController respondsToSelector:@selector(requestReview)]){
[SKStoreReviewController requestReview];
else{
NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]];
}
}else {
// Fallback on earlier versions
NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]];
}
```
4. 注意时机哦
我们的目的是能得到用户的正反馈如果在用户刚使用APP时就弹出评分框可能会给某些用户带来反感因此选择一个合适的时机弹出评分很重要不然适得其反。
今天在使用爱奇艺的时候发现他们的弹出场景是这样的。我因为要出门所以下载了一部电影。在会员模式下高速缓存成功后(我很满意)弹出评分按钮。
![爱奇艺评分](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/61530453779_.pic.jpg)

View File

@@ -1,163 +0,0 @@
# 一些布局小知识
1. LaunchScreen 会根据设备大小设置屏幕的显示范围LaunchImage 则根据提供的启动图片设置App的可见范围
2. UITextView 可以设置显示范围
textView.textContainerInset = UIEdgeInsetsMake(40, 0, 0, 0);
3. UITextView 可以设置像 Word 一样文字环绕在图片四周的效果。其中用到的属性就是exclusionPaths
// Default value : empty array An array of UIBezierPath representing the exclusion paths inside the receiver's bounding rect.
@property (copy, NS_NONATOMIC_IOSONLY) NSArray<UIBezierPath *> *exclusionPaths NS_AVAILABLE(10_11, 7_0);
NSString *str = @“xxx”;//xxx为文字内容
textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 20, self.view.frame.size.width-20, self.view.frame.size.height-30)];
textView.text = str;
[self.view addSubview:textView];
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(140, 280, 160, 100)]; imageView.backgroundColor = [UIColor orangeColor];
imageView.image = [UIImage imageNamed:@"mao.jpg"];
[self.view addSubview:imageView];
textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];
- (UIBezierPath *)translatedBezierPath{
CGRect imageRect = [textView convertRect:imageView.frame fromView:self.view];
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(imageRect.origin.x+5, imageRect.origin.y, imageRect.size.width-5, imageRect.size.height-5)];
return bezierPath;
}
1. 引申学习 CoreText
2. 在读很多第方库的时候经常会看到2个关键词“IB_DESIGNABLE”和”IBInspectable“。如果你想让你纯代码写的 View 具有可以在 StoryBoard 和 xib 文件可预览,就要在自定义的 UIView 头文件加上 IB_DESIGNABLE
3. 如果想让你自定义的 View 的参数可以在 xib 或者 storyboard 上 Attributes inspector 栏目中被看到且可以被修改,那么你需要在每个 property 前面加上 IBInspectable
#import <UIKit/UIKit.h>
IB_DESIGNABLE
@interface DashView : UIView
@property (nonatomic, copy) void(^TimerBlock)(NSInteger);
@property (nonatomic, strong) IBInspectable UIColor *color;
//跃动数字刷新
- (void)refreshJumpNOFromNO:(NSString *)startNO toNO:(NSString *)toNO andTime:(NSString *)time;
@end
4.UITabBarController 设置图片不能过大,不然不能显示
5.设置导航控制器的 NavigationBar 的 BackgroundImage 且 使用了 UIBarMetericsDefault 会导航控制器的子控制器的 view 的高度会减小 64。只有设置为 UIBarMetricsDefault 的时候给 NavigationBar 设置背景图片才会显示。UIBarMetricsCompact 意味着导航条是透明的
[self.navigationBar setBackgroundImage:[UIImage imageNamed:@"Report_customreport"] forBarMetrics:UIBarMetricsDefault];
1. 在 iOS 6及之前的系统上默认都是 NO在 iOS 7及其以后都是默认为 YES。效果表现为顶部的 NavigationBar 都是有透明度的效果
@property(nonatomic,assign,getter=isTranslucent) BOOL translucent NS_AVAILABLE_IOS(3_0) UI_APPEARANCE_SELECTOR; // Default is NO on iOS 6 and earlier. Always YES if barStyle is set to UIBarStyleBlackTranslucent
translucent 设置为 YES ,则布局 view 从屏幕的左上角开始计算,如果设置为 NO那么布局从 NavigationBar 的下面开始布局。
2. 总结:需要让导航控制器里面的控制器的 view 从导航栏以下开始布局有2种方法可以实现。
- 设置导航控制器 setTranslucent = NO
- 给导航控制器的 NavigationBar 设置背景图片,且 BarMetrics 需要设置为 UIBarMetricsDefault
3. + (void)load 和 + (void)initialize 的使用分析
- initialize第一次使用这个类或者它的子类的时候调用
- load :这个方法在类加载的时候调用一次。
//window 下有一个导航控制器,导航控制器的根控制器是 ViewController ,点击屏幕跳转到 SubViewController继承自 ViewController
//ViewController
+ (void)initialize{
NSLog(@"%s",__func__);
}
+ (void)load{
NSLog(@"%s",__func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"test";
self.view.backgroundColor = [UIColor whiteColor];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
SubViewController *vc = [[SubViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
//SubViewController
+ (void)initialize{
[super initialize];
NSLog(@"%s",__func__);
}
//这个方法在类加载的时候调用一次。
+ (void)load{
NSLog(@"%s",__func__);
}
2018-07-19 11:26:04.621740+0800 Test[14617:1049502] +[ViewController load]
2018-07-19 11:26:04.622463+0800 Test[14617:1049502] +[SubViewController load]
2018-07-19 11:26:04.743541+0800 Test[14617:1049502] +[ViewController initialize]
2018-07-19 11:26:07.648425+0800 Test[14617:1049502] +[ViewController initialize]
2018-07-19 11:26:07.648610+0800 Test[14617:1049502] +[SubViewController initialize]
结果分析来看,类都被加载了(调用了 load 方法,其中页面显示的是 ViewController 所以它的 initialize 被调用,点击屏幕跳转到 SubViewController所以 SubViewController 的 initialize 方法会被调用,在调用的时候调用了 super 关键字,调用父类的 initialize 方法)
4. UIAppearance appearanceWhenContainedInInstancesOfClasses : 这个方法可以控制让自定义的导航控制器的 appearance 只修改自己需要修改的样式,不至于对于全部的导航控制器的 navigationBar 全部修改。
5. UIImage 与 UIImageRenderingMode
在 iOS 系统中经常会用到 UIImage 来渲染一些控件,比如 UITabBar 和 UIBarButtonItem
在日常开发的时候我们可以为 UITabBar 设置 items 属性。其中可以指定 UITabBar 的 image 和 selectedImage。此时你可以提供2张图片比如下面的代码
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home_selected"]];
你也可以按照下面的写法
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home"]];
这是因为系统会渲染。如果不为 UIImage 设置渲染模式,系统会在合适的地方根据上下文渲染,比如在这个地方的 UITabBar 就会根据上下文渲染出选中的效果。我们不必要必须设置选中的颜色。
当然你也可以指定 UIImage 的渲染模式。下面看看官方文档讲的 UIImage 的渲染模式
// Create a version of this image with the specified rendering mode. By default, images have a rendering mode of UIImageRenderingModeAutomatic.
- (UIImage *)imageWithRenderingMode:(UIImageRenderingMode)renderingMode NS_AVAILABLE_IOS(7_0);
@property(nonatomic, readonly) UIImageRenderingMode renderingMode NS_AVAILABLE_IOS(7_0);
/* Images are created with UIImageRenderingModeAutomatic by default. An image with this mode is interpreted as a template image or an original image based on the context in which it is rendered. For example, navigation bars, tab bars, toolbars, and segmented controls automatically treat their foreground images as templates, while image views and web views treat their images as originals. You can use UIImageRenderingModeAlwaysTemplate to force your image to always be rendered as a template or UIImageRenderingModeAlwaysOriginal to force your image to always be rendered as an original.
*/
typedef NS_ENUM(NSInteger, UIImageRenderingMode) {
UIImageRenderingModeAutomatic, // Use the default rendering mode for the context where the image is used
UIImageRenderingModeAlwaysOriginal, // Always draw the original image, without treating it as a template
UIImageRenderingModeAlwaysTemplate, // Always draw the image as a template image, ignoring its color information
} NS_ENUM_AVAILABLE_IOS(7_0);
UIImage 的渲染模式共有3种值可以选择
- UIImageRenderingModeAutomatic根据所使用的环境和绘图上下文自动调整渲染模式
- UIImageRenderingModeAlwaysOriginal始终绘制图片原始状态不使用 tintColor
- UIImageRenderingModeAlwaysTemplate始终根据tintColor绘制图片不管图片本身的颜色状态
6.下面举个例子。在 UITabBarController 设置 tabBar
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home_selected"]];
从 iconfont 网站上面随便选择1个彩色 icon 用来做对比实验
![iconfont小图标](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180719-135721.png)
- 实验1
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAutomatic]];
![UIImageRenderingModeAutomatic模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180719-135617@2x.png)
- 实验2
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];
![UIImageRenderingModeAlwaysOriginal模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180719-135552@2x.png)
- 实验3
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
![UIImageRenderingModeAlwaysTemplate模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180719-135552@2x.png)
结论:对于 UIImage 来说如果不指定渲染模式的话则默认使用**UIImageRenderingModeAutomatic**,则会根据渲染的环境和上下文进行渲染。如果指定了模式,则根据具体的模式开启渲染。**UIImageRenderingModeAlwaysOriginal**则绘制图片的原始信息,不使用**tintColor**。**UIImageRenderingModeAlwaysTemplate**则始终根据**tintColor**绘制图片,忽略图片本身的信息。
<hr>
![引用自网络的图片](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4673_140117110629_1.png)

View File

@@ -1,72 +0,0 @@
# iOS 数值计算精度丢失问题
> 在 iOS 中经常会计算金额和价格,我们有时会定义数据类型为 double 或者 float这样在做过一些运算后会发现精度丢失了这显然不是我们想要的结果。今日偶然间看到一篇技术博文为了记忆顺道解决我的这个问题所以记录了下来。
### 存在的问题(精度丢失)
```objective-c
float a = 0.01;
int b = 99999999;
double c = 0.0;
c = a*b;
NSLog(@"c-%f",c); // c-1000000.000000
NSLog(@"c-%.2f",c); // c-1000000.00
```
通过上面的代码我们看到简单的数学运算后对于数据类型不合理的数值进行过运算后精度丢失了,这如果是在我们的 App 中,用户看到自己的金额不正确,那还不吓一跳??
接下来看看如何做简单的改进
```objective-c
NSString *aString = [NSString stringWithFormat:@"%f",a];
NSString *bString = [NSString stringWithFormat:@"%.2f",(double)b];
c = [aString doubleValue]*[bString doubleValue];
NSLog(@"%f",c); //999999.990000
NSLog(@"%.2f",c); //999999.99
```
这样虽然可以达成目的但是计算的过程比较麻烦并不是我们想要的解决方案。通过查阅资料得知苹果推出了一个类专门解决数据计算的精度问题NSDecimalNumber 。
### NSDecimalNumber 为数据精度应用而生
NSDecimalNumber 是 NSNumber 的子类专门负责精度计算。提供了完善的初始化方案对于头疼的精度计算问题金额它提供了便利的解决方案加、减、乘、除、次方运算并且可以给计算出的结果设置明显的精度方案四舍五入、取上、取下等等。NSDecimalNumberHandler 可以对计算出的结果做一些策略比如舍入的模式、数据溢出、除0等异常情况的处理规则。
我们来说说上面的问题吧,引入了 NSDecimalNumber解决上面的问题就不费吹灰之力了。
```objective-c
NSString *decimalNumberMutiplyWithString(NSString *multiplierValue, NSString *multiplicandvalue){
NSDecimalNumber *multiplierNumber = [NSDecimalNumber decimalNumberWithString:multiplierValue];
NSDecimalNumber *multiplicandNumber = [NSDecimalNumber decimalNumberWithString:multiplicandvalue];
NSDecimalNumber *result = [multiplierNumber decimalNumberByMultiplyingBy:multiplicandNumber];
return [result stringValue];
}
NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithMantissa:18992 exponent:-3 isNegative:NO];
NSDecimalNumber *price2 = [NSDecimalNumber decimalNumberWithString:@"18.992"];
NSLog(@"price:%@",[price stringValue]); //999999.990000
NSLog(@"price2:%@",[price2 stringValue]); //999999.99
//设置计算的精度(小数点位数、舍入规则)
NSDecimalNumberHandler *roundPlain = [NSDecimalNumberHandler
decimalNumberHandlerWithRoundingMode:NSRoundPlain
scale:1
raiseOnExactness:NO
raiseOnOverflow:NO
raiseOnUnderflow:NO
raiseOnDivideByZero:YES];
NSDecimalNumber *resultDecimal = [multiplierNumber decimalNumberByMultiplyingBy:multiplicandNumber withBehavior:roundPlain];
```
### 参考文章
[文章1](https://www.jianshu.com/p/25d24a184016)、[文章2](https://www.jianshu.com/p/ea4da259a062)

View File

@@ -1,34 +0,0 @@
# 数组、集合、字典与 hash、isEqual 方法的关联
1. NSArray 允许重复添加元素添加元素的时候不查重所以不会调用上面2个方法。在移出元素的时候会依次遍历数组内的元素每个元素调用 **isEqual** 方法remove 方法传入的元素作为参数),所有返回真值的元素都会被移除。在字典中不涉及 hash 方法。
2. NSSet 不允许重复添加元素,所以在添加新元素的时候,该元素的 **hash** 方法会被调用,若集合中不存在与此元素 hash 相同的元素,则它会被直接加入集合,不调用 **isEqual** 方法;若存在,则依次调用该集合每个元素的 **isEqual** 方法,返回真值则判等,不加入,处理结合,若返回 false 则判定集合内不存在该元素,将其加入。
3. 集合中移除元素时,首先调用它的 **hash方法**,若集合中存在与其 **hash值** 相等的元素,则调用该元素的 **isEqual方法**,若返回真值则判断,进行移除;若不存在,则会依次调用集合中每个元素的 **isEqual方法**,只要找到一个返回真值的元素,就进行移除,并结束整个过程(所有这样会有其它满足 isEqual 方法但却漏掉未被删除的元素)。调用 contains 方法时类似
4. 因此如果自定义对象被加入到集合或作为字典的 key 时,需要同时重写 isEqual 方法和 hash 方法,这样,若集合存在某元素,则调用它的 contains 和 remove 方法时,可以在 O(1) 完成查询,否则需要 O(n) 完成。
 
5. 需要注意的是NSDictionary 的 key、value 都说对象类型即可,但是被设为 key 的对象需要遵循 NSCopying 协议。
6. hash 方法出现的目的是:当我们从数组中查找元素时,需要依次遍历数组中的元素,时间复杂度为 O(n),为了解决效率问题,引入了 Hash Table 方法。当添加元素的时候,为每个元素设置了 hash 值,这样当下次查找的时候就直接通过 hash 值找到对应的位置,时间复杂度为 O(1)。设计一个合理的 hash 算法的指标是对于每个参数,其返回的 hash 值唯一。
7. 查阅资料可知,一个推荐的自定义对象的 **hash** 算法是将关键属性的 hash 值,按照位或运算
```
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
@end
- (NSUInteger)hash{
return [self.name hash] ^ [self.birthday hash];
}
```
8. NSObject 类中的 equal 方法的判断是包括内存地址的也就是说NSObject 若想判断2个对象相等那么这2个对象的内存地址必须相等

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
# 如何优雅地调试手机网页
> 在web开发的过程中抓包、调试页面样式、查看请求头是很常用的技巧。其实在iOS开发中这些技巧也能用无论是模拟器还是真机不过我们需要用到mac自带的浏览器Safari。所以本文将讲解如何使用Safari对iOS程序中的webview进行调试。
* 1、打开真机模拟器的开发者模式
【设置】-> 【Safari】 -> 【高级】 -> 【Web检查器】打开
![打开手机的调试模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2287777-e937adb9c77a3768.png)
* 2、打开MBP上的Safari的开发者模式
【Safari】->【偏好设置】->【高级】-> 【在菜单栏中显示“开发”菜单】勾选。
* 3、调试你的WebView页面。
* 4、在MBP的Safari选项中的开发看到手机右击可以看到正在调试的WebView的url
![选择需要调试等页面](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2287777-c12eb2da00e79f34.png)
* 5、在弹出的这个框里面可以查看网页源代码以及可以调试样样式、查看localStorage、sessionStorage、Cookie的值等等给原生端调试带来很大方便不过这样前端调试更加方便啊谷歌的模拟器不能完全模真实环境下的iphone使用效果啊。
![调试手机页面](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2287777-4d55fd205fa81cc8.png)

File diff suppressed because it is too large Load Diff

View File

@@ -1,154 +0,0 @@
# iOS应用启动性能优化资料汇总
[WWDC](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#wwdc)
[文章](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E6%96%87%E7%AB%A0)
[工具](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E5%B7%A5%E5%85%B7)
[代码](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E4%BB%A3%E7%A0%81)
[偏门古董](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E5%81%8F%E9%97%A8%E5%8F%A4%E8%91%A3)
[书籍](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E4%B9%A6%E7%B1%8D)
[总结](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E6%80%BB%E7%BB%93)
发现好资料就整理到这里_随时更新最后一次更新2018年8月6日_
# WWDC
1. Optimizing App Startup Time
必看官方资料,从底层到上层[https://developer.apple.com/videos/play/wwdc2016/406/](https://developer.apple.com/videos/play/wwdc2016/406/)
2. App Startup Time: Past, Present, and Future
dyld层面的优化[https://developer.apple.com/videos/play/wwdc2017/413/](https://developer.apple.com/videos/play/wwdc2017/413/)
3. Optimizing I/O for Performance and Battery Life
IO是启动性能的重要影响部分[https://developer.apple.com/videos/play/wwdc2016/719/](https://developer.apple.com/videos/play/wwdc2016/719/)
4. Practical Approaches to Great App Performance
现场一步一步解决性能问题[https://developer.apple.com/videos/play/wwdc2018/407/](https://developer.apple.com/videos/play/wwdc2018/407/)
5. Using Time Profiler in Instruments
TimeProfiler是必备好帮手[https://developer.apple.com/videos/play/wwdc2016/418/](https://developer.apple.com/videos/play/wwdc2016/418/)
6. High Performance Auto Layout
App首页如果是AutoLayout的那么以后看来不是问题了[https://developer.apple.com/videos/play/wwdc2018/220/](https://developer.apple.com/videos/play/wwdc2018/220/)
7. Core Image: Performance, Prototyping, and Python
首页当然也有大量的图片了解Core Image[https://developer.apple.com/videos/play/wwdc2018/719/](https://developer.apple.com/videos/play/wwdc2018/719/)
# 文章
**以下文章仅仅是收集,各家之谈,不要全信,也不要反对,各有道理,学习思路即可。**
1. 即刻技术团队iOS app 启动速度研究实践
地址[https://zhuanlan.zhihu.com/p/38183046?from=1086193010&wm=3333\_2001&weiboauthoruid=1690182120](https://zhuanlan.zhihu.com/p/38183046?from=1086193010&wm=3333_2001&weiboauthoruid=1690182120)学习思路。
2. iOS Dynamic Framework 对App启动时间影响实测
[https://www.jianshu.com/p/3263009e9228](https://www.jianshu.com/p/3263009e9228)动态库的测试。可知:启动过程中尽量不要加载动态库了。
3. Optimizing Facebook for iOS start time
[https://code.fb.com/ios/optimizing-facebook-for-ios-start-time/](https://code.fb.com/ios/optimizing-facebook-for-ios-start-time/)Facebook的思路。虽然Facebook的启动很慢。
4. Bugly: iOS App 启动性能优化
[https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA](https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA)这篇文章最后透露了一个很给力的思路。强烈推荐仔细看文章最后。
5. 今日头条iOS客户端启动速度优化
[https://techblog.toutiao.com/2017/01/17/iosspeed/](https://techblog.toutiao.com/2017/01/17/iosspeed/)文章开头的信息很多,但减少代码量,貌似很难行得通。
6. 如何精确度量 iOS App 的启动时间
[https://www.jianshu.com/p/c14987eee107](https://www.jianshu.com/p/c14987eee107)文章的思路可参考。
7. 优化 App 的启动时间
[http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/](http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/)主要是对WWDC的笔记但仍然很给力。南萧玉北子棋。这篇文章就是南萧玉所作。
8. 手淘iOS性能优化探索
[https://github.com/izhangxb/GMTC/blob/master/%E5%85%A8%E7%90%83%E7%A7%BB%E5%8A%A8%E6%8A%80%E6%9C%AF%E5%A4%A7%E4%BC%9AGMTC%202017%20PPT/%E6%89%8B%E6%B7%98iOS%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8E%A2%E7%B4%A2%20.pdf](https://github.com/izhangxb/GMTC/blob/master/%E5%85%A8%E7%90%83%E7%A7%BB%E5%8A%A8%E6%8A%80%E6%9C%AF%E5%A4%A7%E4%BC%9AGMTC%202017%20PPT/%E6%89%8B%E6%B7%98iOS%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8E%A2%E7%B4%A2%20.pdf)这是GMTC 2017手机淘宝专家的技术分享可以参考。
9. iOS应用启动性能优化\(1\)-premain
[https://everettjf.github.io/2018/05/26/ios-app-launch-performance-part1/](https://everettjf.github.io/2018/05/26/ios-app-launch-performance-part1/)仅仅是pre-main阶段的思路。作者说有后续的文章但很久没动静了不知道在搞什么。
10. 一种 hook objective c +load 的方法
[https://everettjf.github.io/2017/01/06/a-method-of-hook-objective-c-load/](https://everettjf.github.io/2017/01/06/a-method-of-hook-objective-c-load/)这篇文章的hook比较麻烦其实还可以参考上面的一篇文章[https://www.jianshu.com/p/c14987eee107](https://www.jianshu.com/p/c14987eee107)这里有批量hook +load的代码。未来我也有计划会把这些相关代码整理到一个repo中
11. 一种 hook C++ static initializers 的方法
[https://everettjf.github.io/2017/02/06/a-method-of-hook-static-initializers/](https://everettjf.github.io/2017/02/06/a-method-of-hook-static-initializers/)这篇文章的hook方法有较大的可能是我首创强烈推荐。手淘的分享中也提了这个方法。
12. 一种延迟 premain code 的方法
[https://everettjf.github.io/2017/03/06/a-method-of-delay-premain-code/](https://everettjf.github.io/2017/03/06/a-method-of-delay-premain-code/)通过学习Facebook的App中特有的section参考文章[https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable/](https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable/)),发现的一种思路。
# 工具
1. TimeProfiler
都知道是啥。
2. AppleTrace
[https://github.com/everettjf/AppleTrace](https://github.com/everettjf/AppleTrace)使用 HookZz hook了objc\_msgSend会有较大性能损耗但可根据相对比例来知道大概的耗时占比。另外也手动定义开始结尾生成chrome tracing。
3. DTrace
只能用于模拟器。使用方法可参考这本书Advanced Apple Debugging & Reverse Engineering[https://store.raywenderlich.com/products/advanced-apple-debugging-and-reverse-engineering](https://store.raywenderlich.com/products/advanced-apple-debugging-and-reverse-engineering)
4. Xcode 环境变量
DYLD\_PRINT\_STATISTIC 及其他类似环境变量[https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/LoggingDynamicLoaderEvents.html](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/LoggingDynamicLoaderEvents.html)
# 代码
1. FastImageCache
[https://github.com/path/FastImageCache](https://github.com/path/FastImageCache)优化图片加载的速度。空间换时间。
# 偏门古董
1. Code Size Performance Guidelines
[https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html)页面最下面提出的思路很好但文章是gcc时代的了。有没有clang时代对应的呢。 Improving Locality of Reference[https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/ImprovingLocality.html\#//apple\_ref/doc/uid/20001862-CJBJFIDD](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/ImprovingLocality.html#//apple_ref/doc/uid/20001862-CJBJFIDD)
# 书籍
1. Pro iOS Apps Performance Optimization
貌似比较古老,仅参考。
2. iOS and macOS Performance Tuning
很细致,我正在看。有中文翻译版。
3. High Performance iOS Apps
有中文翻译版。
# 总结
上面的文章我都看过,或者至少是正在看,总结下来,辅助大家优化启动性能。
---
转载于https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/

View File

@@ -1,84 +0,0 @@
# App 数据安全篇
> 之前的研究了 web 站点的数据安全,同时也用[文章](https://github.com/FantasticLBP/Anti-WebSpider)记录下来分享给大家。接着又研究了下 App 的安全,同样写文章记录下来
## 现状
目前 App 的安全比较低,体现在哪?很多人在想用了 HTTPS 不是就很安全吗?其实并不是,专业的抓包工具还是可以抓 HTTPS 包。根据接口规律,做自动化请求接口,将数据保存窃取是我们不想看到的结果。所以如果只用了 HTTPS 还是不安全。
所以需要实现的安全表现在1. App 数据防止抓包2. 防止中间人攻击3. 下下策。即使抓包成功拿到的数据也是密文。如果想解密,是不可逆的。
## 解决方案
1. App 数据防止抓包
原理:抓包工具工作原理见[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第四部分%20开发杂谈/4.10.md)
![App-Server](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/App-Server.png)
**验证证书的真伪**其实一般来说这个过程应该是安全的,因为一般的证书都是由操作系统来管理。所以只要操作系统没有证书链验证等方面的 bug 是没有什么问题的,但是为了抓包其实我们是在操作系统中导入了中间人的 CA这样中间人下发的公钥证书就可以被认为是合法的可以通过验证的中间人既承担了颁发证书又承担了验证证书通过验证
措施: 客户端为了解决这个问题,最好的方式其实就是内嵌证书,比对一下这个证书到底是不是自己真正的“服务端”发来的,而不是中间被替换了。
- 跟服务端人员拿到 https 证书,导入 Xcode 工程项目中
- AFNetworking设置以下代码
```objective-c
AFSecurityPolicy * policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
manager.securityPolicy = policy;
```
AF的安全策略会自动的在bundle里面查找公钥证书建立https的时候进行比对。不一样直接就失败了。
AFNetworking 的 AFSSLPinningMode 的三个级别
  AFSSLPinningModeNone: (默认级别),客户端无条件信任任何下发的公钥证书
  AFSSLPinningModePublicKey: 客户端本地去验证服务端下发的公钥证书的 public keys部分。如果正确才通过
  AFSSLPinningModeCertificate: 客户端本地去验证服务端下发的公钥证书的所有部分。如果正确才通过
这样做了之后就可以即使手机上安装了抓包工具的CA抓包工具也不能抓到包了。因为你的客户端在验证“服务端”下发的公钥证书的真伪的时候就不会通过“中间人”下发的公钥证书也就不会建立起来https的连接了。
2. 防止中间人攻击
即使我们的 App 被大神逆向了iOS + 网络精通),抓到网络请求,然后原封不动去向服务器发起请求,但是服务端做了防重放,也是很安全的。所以防重放机制是 Server 端的安全措施
[防重放](https://www.cnblogs.com/yjf512/p/6590890.html)
3. 密文,反向解密不可逆
采用 RSA 非对称加密算法。
- iOS 端和 Server 端各生成自己的公钥和私钥
使用 **openssl** 生成所需秘钥
- iOS 端生成的公钥和私钥定义为 **iOSPublicKey、iOSPrivateKey**Server 端生成的公钥私钥定义为**ServerPublicKey、ServerPrivateKey**。将 **iOSPublicKey ** 给 Server 端使用,让它用 **iOSPublicKey ** 加密数据传给 iOS 端iOS 端用 **iOSPrivateKey** 解密Server 端将 **ServerPublicKey** 给 iOS 端iOS 端用 **ServerPublicKey** 加密数据后上传给 Server 端Server 端利用 **ServerPrivateKey** 去解密,这样就实现了数据传输过程中的加密与解密
## 资料
[RSA算法原理](http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html)
[RSA算法原理](http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.htmll)
[素数](https://zh.wikipedia.org/zh-cn/互質)
[防重放](https://www.cnblogs.com/yjf512/p/6590890.html)

View File

@@ -1,94 +0,0 @@
# 调试方面的骚操作
1. 在日常开发中我们经常会封装某个功能模块然后暴露某个方法给外部。但是很多时候调用我们封装功能的人可能会不按照约定的方法传递参数。所以我们会使用断言。但是在线上的时候如果使用了断言,那么程序肯定会 **Crash** Xcode 提供了一个小功能可以解决这个问题。
`NS_BLOCK_ASSERTIONS ` 表明在 Release 状态下过滤 NSAssert只需要这一个条件就可以过滤掉 NSAssert。
方法:在 “Build Settings” 下搜索 **Preprocessor Macros** ,然后在 Release 下面添加 NS_BLOCK_ASSERTIONS
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180830-100631@2x.png)
### BreakPoint
#### 分类
Breakpoint 分为 Normal Breakpoint、Exception Breakpoint、OpenGL ES Error Breakpoint、Symbolic Breakpoint、Test Failure breakpoint、WatchPoint。可以按照具体的情景使用不同类型的 Breakpoint。
### NSAssert 与 dispatch_once
开发中非常常见 NSAssert尤其是在 SDK 和类库的开发中,使用断言帮助在开发阶段发现问题,督促达到预期的结果。
NSAssert 的本质就是产生一个 ExceptionException 发生触发 `objc_exception_throw` 这个 c 函数。
![NSAssert 断言](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-0311-NSAssert.png)
callback 信息如下
```Objective-c
*** First throw call stack:
(
0 CoreFoundation 0x00007fff23c7127e __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff513fbb20 objc_exception_throw + 48
2 CoreFoundation 0x00007fff23c70ff8 +[NSException raise:format:arguments:] + 88
3 Foundation 0x00007fff256e9b51 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
4 TEst 0x0000000106edfeef -[AppDelegate application:didFinishLaunchingWithOptions:] + 287
5 UIKitCore 0x00007fff48089ad8 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 232
6 UIKitCore 0x00007fff4808b460 -[UIApplication _callInitializationDelegatesWithActions:forCanvas:payload:fromOriginatingProcess:] + 3980
7 UIKitCore 0x00007fff48090f05 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1226
8 UIKitCore 0x00007fff477c57a6 -[_UISceneLifecycleMultiplexer completeApplicationLaunchWithFBSScene:transitionContext:] + 179
9 UIKitCore 0x00007fff4808d514 -[UIApplication _compellApplicationLaunchToCompleteUnconditionally] + 59
10 UIKitCore 0x00007fff4808d813 -[UIApplication _run] + 754
11 UIKitCore 0x00007fff48092d4d UIApplicationMain + 1621
12 TEst 0x0000000106ee0144 main + 116
13 libdyld.dylib 0x00007fff5227ec25 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
```
可以清楚看到当断言失败的时候Xcode 可以精确定位到 NSAssert 有问题的那行代码,这种情况下是有源代码。
其实有些场景下发生异常是定位不到真正产生异常的地方。比如当 App 规模较大,为了提高构建速度,很多人将 Pod 打成静态库,但是这种情况下产生的异常不会被精确定位到产生问题的行。如果断言在 GCD 的 block 中,而且上下文中没有源码,则也定位不到。
输出信息如下:
```Objective-c
*** First throw call stack:
(
0 CoreFoundation 0x00007fff23c7127e __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff513fbb20 objc_exception_throw + 48
2 CoreFoundation 0x00007fff23c70ff8 +[NSException raise:format:arguments:] + 88
3 Foundation 0x00007fff256e9b51 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
4 TEst 0x000000010f242e95 __57-[AppDelegate application:didFinishLaunchingWithOptions:]_block_invoke + 229
5 libdispatch.dylib 0x000000010f55fdd4 _dispatch_call_block_and_release + 12
6 libdispatch.dylib 0x000000010f560d48 _dispatch_client_callout + 8
7 libdispatch.dylib 0x000000010f56ede6 _dispatch_main_queue_callback_4CF + 1500
8 CoreFoundation 0x00007fff23bd4049 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
9 CoreFoundation 0x00007fff23bceca9 __CFRunLoopRun + 2329
10 CoreFoundation 0x00007fff23bce066 CFRunLoopRunSpecific + 438
11 GraphicsServices 0x00007fff384c0bb0 GSEventRunModal + 65
12 UIKitCore 0x00007fff48092d4d UIApplicationMain + 1621
13 TEst 0x000000010f243134 main + 116
14 libdyld.dylib 0x00007fff5227ec25 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
```
给 Xcode 添加 **Symbolic Breakpoint** 类型的断点Symbol 为 `objc_exception_throw`,就可以在 Xcode 的左侧堆栈中看到了。
![objc_exception_throw](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-03-11-objc_exception_throw.png)
其实 GCD 的 方法 **_dispatch_client_callout** 去查看下有没有猫腻,再从这里找到 libdispatch 的代码https://opensource.apple.com/tarballs/libdispatch/。
```Objective-c
// object.m
#undef _dispatch_client_callout
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
@try {
return f(ctxt);
}
@catch (...) {
objc_terminate();
}
}
```
发现 _dispatch_client_callout 把 GCD block 中的 OC Exception try catch 捕获了,然后调用 objc_terminate导致了 CallStack 断开。
有个情景,工作 Crash 到 dispatch_once 这里了,因为 dispatch_once 中的代码 throw OC 异常。一般大公司初期这种情况经常遇到,后期一般都会针对断言专门开发一些代码用来定位 Owner。

View File

@@ -1,847 +0,0 @@
# 一个 Hybrid SDK 设计与实现
> 随着移动浪潮的兴起,各种 App 层出不穷,极速发展的业务拓展提升了团队对开发效率的要求,这个时候纯粹使用 Native 开发技术成本难免会更高一点。而 H5 的低成本、高效率、跨平台等特性马上被利用起来了,形成一种新的开发模式: Hybrid App
>
> 作为一种混合开发的模式Hybrid App 底层依赖于 Native 提供的容器Webview上层使用各种前端技术完成业务开发现在三足鼎立的 Vue、React、Angular底层透明化、上层多样化。这种场景非常有利于前端介入非常适合业务的快速迭代。于是 Hybrid 火了。
>
> 大道理谁都懂,但是按照我知道的情况,还是有非常多的人和公司在 Hybrid 这一块并没有做的很好,所以我将我的经验做一个总结,希望可以帮助广大开发者的技术选型有所帮助
## 一、Hybrid 现状
可能早期都是 PC 端的网页开发随着移动互联网的发展iOS、Android 智能手机的普及,非常多的业务和场景都从 PC 端转移到移动端。开始有前端开发者为移动端开发网页。这样子早期资源打包到 Native App 中会造成应用包体积的增大。越来越多的业务开始用 H5 尝试,这样子难免会需要一个需要访问 Native 功能的地方,这样子可能早期就是懂点前端技术的 Native 开发者自己封装或者暴露 Native 能力给 JS 端,等业务较多的时候者样子很明显不现实,就需要专门的 Hybrid 团队做这个事情;量大了,就需要规矩,就需要规范。
总结:
1. Hybrid 开发效率高、跨平台、低成本
2. Hybrid 从业务上讲,没有版本问题,有 Bug 可以及时修复
Hybrid 在大量应用的时候就需要一定的规范,那么本文将讨论一个 Hybrid 的设计知识。
- Hybrid 、Native、前端各自的工作是什么
- Hybrid 交互接口如何设计
- Hybrid 的 Header 如何设计
- Hybrid 的如何设计目录结构以及增量机制如何实现
- 资源缓存策略,白屏问题...
## 二、Native 与前端分工
在做 Hybird 架构设计之前我们需要分清 Native 与前端的界限。首先 Native 提供的是宿主环境,要合理利用 Native 提供的能力,要实现通用的 Hybrid 架构,站在大前端的视觉,我觉得需要考虑以下核心设计问题。
### 1. 交互设计
Hybrid 架构设计的第一要考虑的问题就是如何设计前端与 Native 的交互,如果这块设计不好会对后续的开发、前端框架的维护造成深远影响。并且这种影响是不可逆、积重难返。所以前期需要前端与 Native 好好配合、提供通用的接口。比如
1. Native UI 组件、Header 组件、消息类组件
2. 通讯录、系统、设备信息读取接口
3. H5 与 Native 的互相跳转。比如 H5 如何跳转到一个 Native 页面H5 如何新开 Webview 并做动画跳转到另一个 H5 页面
### 2. 账号信息设计
账号系统是重要且无法避免的Native 需要设计良好安全的身份验证机制,保证这块对业务开发者足够透明,打通账户体系。
举个例子,客户端提供了一个 WebView 容器Native 侧的个人中心,用户是登陆态。但用户去访问 A 业务A 业务的实现是前端实现的。访问页面时,页面内容都可以看到了,但是某个接口需要用户登录态,然后忽然跳转到登陆页,对于用户体验很不好。用户一脸懵逼,我不是登陆了吗?为什么还跳转到登陆页面,管你页面的技术实现是 Native 还是前端,对于用户来说,用户不是专业技术人员,不会判断是 Native 还是跨端方案,也不需要判断。
所以解决方案是 Native 和 Hybrid 打通账号体系,通过 WebView 去访问 H5 的时候,应该保持同样的登陆态,用户账号信息是打通的。
Todo
WebView 的鉴权
举个例子:携程的动态化很高现在 RN 居多,前几年的时候大部分页面还是 Hybrid 架构。假设用户在浏览器里面访问了一个页面 A输入手机号登陆成功了也在页面 A 上完成了自己的业务。此时
### 3. Hybrid 开发调试
功能设计、编码完并不是真正结束Native 与前端需要商量出一套可开发调试的模型,不然很多业务开发的工作难以继续。
[iOS调试技巧](https://www.jianshu.com/p/f430caa81fa8)
Android 调试技巧:
- App 中开启 Webview 调试(WebView.setWebContentsDebuggingEnabled(true); )
- chrome 浏览器输入 chrome://inspect/#devices 访问可以调试的 webview 列表
- 需要翻墙的环境
![结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HybridStructure.jpg)
## 三、Hybrid 交互设计
Hybrid 交互无非是 Native 调用 H5 页面JS 方法,或者 H5 页面通过 JS 调 Native 提供的接口。2者通信的桥梁是 Webview。
业界主流的通信方法1.桥接对象时机问题不太主张这种方式2.自定义 Url scheme
![通信设计](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Native-JS-Communication.png)
App 自身定义了 url scheme将自定义的 url 注册到调度中心,例如
weixin:// 可以打开微信。
关于 Url scheme 如果不太清楚可以看看 [这篇文章](https://www.jianshu.com/p/253479ccc83a)
### 1. JS to Native
Native 在每个版本都会提供一些 Api前端会有一个对应的框架团队对其封装释放业务接口。举例
```
SDGHybrid.http.get() // 向业务服务器拿数据
SDGHybrid.http.post() // 向业务服务器提交数据
SDGHybrid.http.sign() // 计算签名
SDGHybrid.http.getUA() // 获取UserAgent
```
```
SDGHybridReady(function(arg){
SDGHybrid.http.post({
url: arg.baseurl + '/feedback',
params:{
title: '点菜很慢',
content: '服务差'
},
success: (data) => {
renderUI(data);
},
fail: (err) => {
console.log(err);
}
})
})
```
前端框架定义了一个全局变量 SDGHybrid 作为 Native 与前端交互的桥梁,前端可以通过这个对象获得访问 Native 的能力
### 2. Api 交互
调用 Native Api 接口的方式和使用传统的 Ajax 调用服务器,或者 Native 的网络请求提供的接口相似
![Api交互](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HybridApi.jpg)
所以我们需要封装的就是模拟创建一个类似 Ajax 模型的 Native 请求。
![通信示例](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Hybrid-Ajax.jpg)
### 3. 格式约定
交互的第一步是设计数据格式。这里分为请求数据格式与响应数据格式,参考 Ajax 模型:
```
$.ajax({
type: "GET",
url: "test.json",
data: {username:$("#username").val(), content:$("#content").val()},
dataType: "json",
success: function(data){
renderUI(data);
}
});
```
```
$.ajax(options) => XMLHTTPRequest
type(默认值GET)HTTP请求方法GET|POST|DELETE|...
url(默认值当前url)请求的url地址
data(默认值:'') 请求中的数据如果是字符串则不变如果为Object则需要转换为String含有中文则会encodeURI
```
所以 Hybrid 中的请求模型为:
```
requestHybrid({
// H5 请求由 Native 完成
tagname: 'NativeRequest',
// 请求参数
param: requestObject,
// 结果的回调
callback: function (data) {
renderUI(data);
}
});
```
这个方法会形成一个 URL比如
`SDGHybrid://NativeRequest?t=1545840397616&callback=Hybrid_1545840397616&param=%7B%22url%22%3A%22https%3A%2F%2Fwww.datacubr.com%2FApi%2FSearchInfo%2FgetLawsInfo%22%2C%22params%22%3A%7B%22key%22%3A%22%22%2C%22page%22%3A1%2C%22encryption%22%3A1%7D%2C%22Hybrid_Request_Method%22%3A0%7D`
Native 的 webview 环境可以监控内部任何的资源请求,判断如果是 SDGHybrid 则分发事件,处理结束可能会携带参数,参数需要先 urldecode 然后将结果数据通过 Webview 获取 window 对象中的 callbackHybrid_时间戳
数据返回的格式和普通的接口返回格式类似
```
{
errno: 1,
message: 'App版本过低请升级App版本',
data: {}
}
```
这里注意:真实数据在 data 节点中。如果 errno 不为0则需要提示 message。
简易版本代码实现。
```javascript
//通用的 Hybrid call Native
window.SDGbrHybrid = window.SDGbrHybrid || {};
var loadURL = function (url) {
var iframe = document.createElement('iframe');
iframe.style.display = "none";
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function () {
iframe.remove();
}, 100);
};
var _getHybridUrl = function (params) {
var paramStr = '', url = 'SDGHybrid://';
url += params.tagname + "?t=" + new Date().getTime();
if (params.callback) {
url += "&callback=" + params.callback;
delete params.callback;
}
if (params.param) {
paramStr = typeof params.param == "object" ? JSON.stringify(params.param) : params.param;
url += "&param=" + encodeURIComponent(paramStr);
}
return url;
};
var requestHybrid = function (params) {
//生成随机函数
var tt = (new Date().getTime());
var t = "Hybrid_" + tt;
var tmpFn;
if (params.callback) {
tmpFn = params.callback;
params.callback = t;
window.SDGHybrid[t] = function (data) {
tmpFn(data);
delete window.SDGHybrid[t];
}
}
loadURL(_getHybridUrl(params));
};
//获取版本信息约定APP的navigator.userAgent版本包含版本信息scheme/xx.xx.xx
var getHybridInfo = function () {
var platform_version = {};
var na = navigator.userAgent;
var info = na.match(/scheme\/\d\.\d\.\d/);
if (info && info[0]) {
info = info[0].split('/');
if (info && info.length == 2) {
platform_version.platform = info[0];
platform_version.version = info[1];
}
}
return platform_version;
};
```
Native 对于 H5 来说有个 Webview 容器,框架&&底层不太关心 H5 的业务实现,所以真实业务中 Native 调用 H5 场景较少。
上面的网络访问 Native 代码iOS为例
```objective-c
typedef NS_ENUM(NSInteger){
Hybrid_Request_Method_Post = 0,
Hybrid_Request_Method_Get = 1
} Hybrid_Request_Method;
@interface RequestModel : NSObject
@property (nonatomic, strong) NSString *url;
@property (nonatomic, assign) Hybrid_Request_Method Hybrid_Request_Method;
@property (nonatomic, strong) NSDictionary *params;
@end
@interface HybridRequest : NSObject
+ (void)requestWithNative:(RequestModel *)requestModel hybridRequestSuccess:(void (^)(id responseObject))success hybridRequestfail:(void (^)(void))fail;
+ (void)requestWithNative:(RequestModel *)requestModel hybridRequestSuccess:(void (^)(id responseObject))success hybridRequestfail:(void (^)(void))fail{
//处理请求不全的情况
NSAssert(requestModel || success || fail, @"Something goes wrong");
NSString *url = requestModel.url;
NSDictionary *params = requestModel.params;
if (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get) {
[AFNetPackage getJSONWithUrl:url parameters:params success:^(id responseObject) {
success(responseObject);
} fail:^{
fail();
}];
}
else if (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) {
[AFNetPackage postJSONWithUrl:url parameters:params success:^(id responseObject) {
success(responseObject);
} fail:^{
fail();
}];
}
}
```
## 四、常用交互 Api
良好的交互设计是第一步,在真实业务开发中有一些 Api 一定会由应用场景。
### 1. 跳转
跳转是 Hybrid 必用的 Api 之一,对前端来说有以下情况:
- 页面内跳转,与 Hybrid 无关
- H5 跳转 Native 界面
- H5 新开 Webview 跳转 H5 页面,一般动画切换页面
如果使用动画按照业务来说分为前进、后退。forward & backword规定如下首先是 H5 跳 Native 某个页面
```
//H5跳Native页面
//=>SDGHybrid://forward?t=1446297487682&param=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D
requestHybrid({
tagname: 'forward',
param: {
// 要去到的页面
topage: 'home',
// 跳转方式H5跳Native
type: 'native',
// 其它参数
data2: 2
}
});
```
H5 页面要去 Native 某个页面
```
//=>SDGHybrid://forward?t=1446297653344&param=%7B%22topage%22%253A%22Goods%252Fdetail%20%20%22%252C%22type%22%253A%22h2n%22%252C%22id%22%253A20151031%7D
requestHybrid({
tagname: 'forward',
param: {
// 要去到的页面
topage: 'Goods/detail',
// 跳转方式H5跳Native
type: 'native',
// 其它参数
id: 20151031
}
});
```
H5 新开 Webview 的方式去跳转 H5
```
requestHybrid({
tagname: 'forward',
param: {
// 要去到的页面首先找到goods频道然后定位到detail模块
topage: 'goods/detail ',
//跳转方式H5新开Webview跳转最后装载H5页面
type: 'webview',
//其它参数
id: 20151031
}
});
```
back 与 forward 一致,可能会有 animatetype 参数决定页面切换的时候的动画效果。真实使用的时候可能会全局封装方法去忽略 tagname 细节。
### 2. Header 组件的设计
Native 每次改动都比较“慢”,所以类似 Header 就很需要。
1. 主流容器都是这么做的,比如微信、手机百度、携程
2. 没有 Header 一旦出现网络错误或者白屏App 将陷入假死状态
PS Native 打开 H5如果 300ms 没有响应则需要 loading 组件,避免白屏
因为 H5 App 本身就有 Header 组件,站在前端框架层来说,需要确保业务代码是一致的,所有的差异需要在框架层做到透明化,简单来说 Header 的设计需要遵循:
- H5 Header 组件与 Native 提供的 Header 组件使用调用层接口一致
- 前端框架层根据环境判断选择应该使用 H5 的 Header 组件异或 Native 的 Header 组件
一般来说 Header 组件需要完成以下功能:
1. Header 左侧与右侧可配置,显示为文字或者图标(这里要求 Header 实现主流图标,并且也可由业务控制图标),并需要控制其点击回调
2. Header 的 title 可设置为单标题或者主标题、子标题类型,并且可配置 lefticon 与 righticonicon居中
3. 满足一些特殊配置,比如标签类 Header
所以站在前端业务方来说Header 的使用方式为(其中 tagname 是不允许重复的):
```javascript
//Native以及前端框架会对特殊tagname的标识做默认回调如果未注册callback或者点击回调callback无返回则执行默认方法
// back前端默认执行History.back如果不可后退则回到指定URLNative如果检测到不可后退则返回Naive大首页
// home前端默认返回指定URLNative默认返回大首页
this.header.set({
left: [
{
//如果出现value字段则默认不使用icon
tagname: 'back',
value: '回退',
//如果设置了lefticon或者righticon则显示icon
//native会提供常用图标icon映射如果找不到便会去当前业务频道专用目录获取图标
lefticon: 'back',
callback: function () { }
}
],
right: [
{
//默认icon为tagname这里为icon
tagname: 'search',
callback: function () { }
},
//自定义图标
{
tagname: 'me',
//会去hotel频道存储静态header图标资源目录搜寻该图标没有便使用默认图标
icon: 'hotel/me.png',
callback: function () { }
}
],
title: 'title',
//显示主标题,子标题的场景
title: ['title', 'subtitle'],
//定制化title
title: {
value: 'title',
//标题右边图标
righticon: 'down', //也可以设置lefticon
//标题类型,默认为空,设置的话需要特殊处理
//type: 'tabs',
//点击标题时的回调,默认为空
callback: function () { }
}
});
```
因为 Header 左边一般来说只有一个按钮,所以其对象可以使用这种形式:
```javascript
this.header.set({
back: function () { },
title: ''
});
//语法糖=>
this.header.set({
left: [{
tagname: 'back',
callback: function(){}
}],
title: '',
});
```
为完成 Native 端的实现,这里会新增两个接口,向 Native 注册事件,以及注销事件:
```javascript
var registerHybridCallback = function (ns, name, callback) {
if(!window.Hybrid[ns]) window.Hybrid[ns] = {};
window.Hybrid[ns][name] = callback;
};
var unRegisterHybridCallback = function (ns) {
if(!window.Hybrid[ns]) return;
delete window.Hybrid[ns];
};
```
Native Header 组件实现:
```javascript
define([], function () {
'use strict';
return _.inherit({
propertys: function () {
this.left = [];
this.right = [];
this.title = {};
this.view = null;
this.hybridEventFlag = 'Header_Event';
},
//全部更新
set: function (opts) {
if (!opts) return;
var left = [];
var right = [];
var title = {};
var tmp = {};
//语法糖适配
if (opts.back) {
tmp = { tagname: 'back' };
if (typeof opts.back == 'string') tmp.value = opts.back;
else if (typeof opts.back == 'function') tmp.callback = opts.back;
else if (typeof opts.back == 'object') _.extend(tmp, opts.back);
left.push(tmp);
} else {
if (opts.left) left = opts.left;
}
//右边按钮必须保持数据一致性
if (typeof opts.right == 'object' && opts.right.length) right = opts.right
if (typeof opts.title == 'string') {
title.title = opts.title;
} else if (_.isArray(opts.title) && opts.title.length > 1) {
title.title = opts.title[0];
title.subtitle = opts.title[1];
} else if (typeof opts.title == 'object') {
_.extend(title, opts.title);
}
this.left = left;
this.right = right;
this.title = title;
this.view = opts.view;
this.registerEvents();
_.requestHybrid({
tagname: 'updateheader',
param: {
left: this.left,
right: this.right,
title: this.title
}
});
},
//注册事件,将事件存于本地
registerEvents: function () {
_.unRegisterHybridCallback(this.hybridEventFlag);
this._addEvent(this.left);
this._addEvent(this.right);
this._addEvent(this.title);
},
_addEvent: function (data) {
if (!_.isArray(data)) data = [data];
var i, len, tmp, fn, tagname;
var t = 'header_' + (new Date().getTime());
for (i = 0, len = data.length; i < len; i++) {
tmp = data[i];
tagname = tmp.tagname || '';
if (tmp.callback) {
fn = $.proxy(tmp.callback, this.view);
tmp.callback = t;
_.registerHeaderCallback(this.hybridEventFlag, t + '_' + tagname, fn);
}
}
},
//显示header
show: function () {
_.requestHybrid({
tagname: 'showheader'
});
},
//隐藏header
hide: function () {
_.requestHybrid({
tagname: 'hideheader',
param: {
animate: true
}
});
},
//只更新title不重置事件不对header其它地方造成变化仅仅最简单的header能如此操作
update: function (title) {
_.requestHybrid({
tagname: 'updateheadertitle',
param: {
title: 'aaaaa'
}
});
},
initialize: function () {
this.propertys();
}
});
});
```
### 3. 请求类
虽然 get 类请求可以用 jsonp 方式绕过跨域问题,但是 post 请求是一个拦路虎。为了安全性问题服务器会设置 cors 仅仅针对几个域名Hybrid 内嵌静态资源可能是通过本地 file 的方式读取,所以 cors 就行不通了。另外一个问题是防止爬虫获取数据,由于 Native 针对网络做了安全性设置(鉴权、防抓包等),所以 H5 的网络请求由 Native 完成。可能有些人说 H5 的网络请求让 Native 走就安全了吗?我可以继续爬取你的 Dom 节点啊。这个是针对反爬虫的手段一。想知道更多的反爬虫策略可以看看我这篇文章 [Web反爬虫方案](https://github.com/FantasticLBP/Anti-WebSpider)
![Web网络请求由Native完成](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DataViaNative.png)
这个使用场景和 Header 组件一致,前端框架层必须做到对业务透明化,业务事实上不必关心这个网络请求到底是由 Native 还是浏览器发出。
```javascript
HybridGet = function (url, param, callback) {
};
HybridPost = function (url, param, callback) {
};
```
真实的业务场景会将之封装到数据请求模块在底层做适配在H5站点下使用ajax请求在Native内嵌时使用代理发出与Native的约定为
```javascript
requestHybrid({
tagname: 'NativeRequest',
param: {
url: arg.Api + "SearchInfo/getLawsInfo",
params: requestparams,
Hybrid_Request_Method: 0,
encryption: 1
},
callback: function (data) {
renderUI(data);
}
});
```
## 五、常用 NativeUI 组件
一般情况 Native 通常会提供常用的 UI比如 加载层loading、消息框toast
```javascript
var HybridUI = {};
HybridUI.showLoading();
//=>
requestHybrid({
tagname: 'showLoading'
});
HybridUI.showToast({
title: '111',
//几秒后自动关闭提示框,-1需要点击才会关闭
hidesec: 3,
//弹出层关闭时的回调
callback: function () { }
});
//=>
requestHybrid({
tagname: 'showToast',
param: {
title: '111',
hidesec: 3,
callback: function () { }
}
});
```
Native UI与前端UI不容易打通所以在真实业务开发过程中一般只会使用几个关键的Native UI。
## 六、账号系统的设计
Webview 中跑的网页,账号登录与否由是否携带密钥 cookie 决定(不能保证密钥的有效性)。因为 Native 不关注业务实现,所以每次载入都有可能是登录成功跳转回来的结果,所以每次载入都需要关注密钥 cookie 变化,以做到登录态数据的一致性。
- 使用 Native 代理做请求接口,如果没有登录则 Native 层唤起登录页
- 直连方式使用 ajax 请求接口如果没登录则在底层唤起登录页H5
```javascript
/*
无论成功与否皆会关闭登录框
参数包括:
success 登录成功的回调
error 登录失败的回调
url 如果没有设置success或者success执行后没有返回true则默认跳往此url
*/
HybridUI.Login = function (opts) {
//...
};
//=>
requestHybrid({
tagname: 'login',
param: {
success: function () { },
error: function () { },
url: '...'
}
});
//与登录接口一致,参数一致
HybridUI.logout = function () {
//...
};
```
在设计 Hybrid 层的时候,接口要做到对于处于 Hybrid 环境中的代码乐意通过接口获取 Native 端存储的用户账号信息;对于处于传统的网页环境,可以通过接口获取线上的账号信息,然后将非敏感的信息存储到 LocalStorage 中,然后每次页面加载从 LocalStorage 读取数据到内存中(比如 Vue.js 框架中的 VuexReact.js 中的 Redux
## 七、Hybrid 资源管理
Hybrid 的资源需要 `增量更新` 需要拆分方便,所以一个 Hybrid 资源结构类似于下面的样子
![Hybrid资源结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-07-08-Hybrid-ResourceStructure.png)
假设有2个业务线商城、购物车
```tex
WebApp
│- Mall
│- Cart
│ index.html //业务入口html资源如果不是单页应用会有多个入口
│ │ main.js //业务所有js资源打包
│ │
│ └─static //静态样式资源
│ ├─css
│ ├─hybrid //存储业务定制化类Native Header图标
│ └─images
├─libs
│ libs.js //框架所有js资源打包
└─static
├─css
└─images
```
## 八、增量更新
每次业务开发完毕后都需要在打包分发平台进行部署上线,之后会生成一个版本号。
| Channel | Version | md5 |
| ------- | ------- | ----------- |
| Mall | 1.0.1 | 12233000ww |
| Cart | 1.1.2 | 28211122wt2 |
当 Native App 启动的时候会从服务端请求一个接口,接口的返回一个 json 串,内容是 App 所包含的各个 H5 业务线的版本号和 md5 信息。
拿到 json 后和 App 本地保存的版本信息作比较,发现变动了则去请求相应的接口,接口返回 md5 对应的文件。Native 拿到后完成解压替换。
全部替换完毕后将这次接口请求到的资源版本号信息保存替换到 Native 本地。
因为是每个资源有版本号,所以如果线上的某个版本存在问题,那么可以根据相应的稳定的版本号回滚到稳定的版本。
## 九、体验优化
### 1. 静态直出
“直出”这个概念对前端同学来说,并不陌生。为了优化首屏体验,大部分主流的页面都会在服务器端拉取首屏数据后通过 NodeJs 进行渲染,然后生成一个包含了首屏数据的 Html 文件,这样子展示首屏的时候,就可以解决内容转菊花的问题了。
当然这种页面“直出”的方式也会带来一个问题,服务器需要拉取首屏数据,意味着服务端处理耗时增加。
不过因为现在 Html 都会发布到 CDN 上WebView 直接从 CDN 上面获取,这块耗时没有对用户造成影响。
手 Q 里面有一套自动化的构建系统 Vnues当产品经理修改数据发布后可以一键启动构建任务Vnues 系统就会自动同步最新的代码和数据,然后生成新的含首屏 Html并发布到 CDN 上面去。
我们可以做一个类似的事情,自动同步最新的代码和数据,然后生成新的含首屏 Html并发布到 CDN 上面去
### 2. 离线预推
页面发布到 CDN 上面去后,那么 WebView 需要发起网络请求去拉取。当用户在弱网络或者网速比较差的环境下,这个加载时间会很长。于是我们通过离线预推的方式,把页面的资源提前拉取到本地,当用户加载资源的时候,相当于从本地加载,即使没有网络,也能展示首屏页面。这个也就是大家熟悉的离线包。
手 Q 使用 7Z 生成离线包, 同时离线包服务器将新的离线包跟业务对应的历史离线包进行 BsDiff 做二进制差分生成增量包进一步降低下载离线包时的带宽成本下载所消耗的流量从一个完整的离线包253KB降低为一个增量包3KB
[手Q开源Hybrid框架VasSonic介绍极致的页面加载速度优化](https://mp.weixin.qq.com/s?__biz=MzUxMzcxMzE5Ng==&mid=2247488218&idx=1&sn=21afe07eb642162111ee210e4a040db2&chksm=f951a799ce262e8f6c1f5bb85e84c2db49ae4ca0acb6df40d9c172fc0baaba58937cf9f0afe4&scene=27#wechat_redirect)
### 3. 拦截加载
事实上,在高度定制的 wap 页面场景下,我们对于 webview 中可能出现的页面类型会进行严格控制。可以通过内容的控制,避免 wap 页中出现外部页面的跳转,也可以通过 webview 的对应代理方法,禁掉我们不希望出现的跳转类型,或者同时使用,双重保护来确保当前 webview 容器中只会出现我们定制过的内容。既然 wap 页的类型是有限的,自然想到,同类型页面大都由前端采用模板生成,页面所使用的 html、css、js 的资源很可能是同一份,或者是有限的几份,把它们直接随客户端打包在本地也就变得可行。加载对应的 url 时,直接 load 本地的资源。
对于 webview 中的网络请求,其实也可以交由客户端接管,比如在你所采用的 Hybrid 框架中为前端注册一个发起网络请求的接口。wap 页中的所有网络请求都通过这个接口来发送。这样客户端可以做的事情就非常多了举个例子NSURLProtocol 无法拦截 WKWebview 发起的网络请求,采用 Hybrid 方式交由客户端来发送,便可以实现对应的拦截。
基于上面的方案,我们的 wap 页的完整展示流程是这样:客户端在 webview 中加载某个 url判断符合规则load 本地的模板 html该页面的内部实现是通过客户端提供的网络请求接口发起获取具体页面内容的网络请求获得填充的数据从而完成展示。
NSURLProtocol能够让你去重新定义苹果的URL加载系统(URL Loading System)的行为URL Loading System里有许多类用于处理URL请求比如NSURLNSURLRequestNSURLConnection和NSURLSession等。当URL Loading System使用NSURLRequest去获取资源的时候它会创建一个NSURLProtocol子类的实例你不应该直接实例化一个NSURLProtocolNSURLProtocol看起来像是一个协议但其实这是一个类而且必须使用该类的子类并且需要被注册。                                       
### 4. WKWebView 网络请求拦截
- 方法一Native 侧):
原生 WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此在 WKWebView 上直接使用 NSURLProtocol 是无法拦截请求的。
但是由于 mPaas 的离线包机制强依赖网络拦截所以基于此mPaaS 利用了 WKWebview 的隐藏 api去注册拦截网络请求去满足离线包的业务场景需求参考代码如下
```Objective-c
[WKBrowsingContextController registerSchemeForCustomProtocol:@"https"]
```
但是因为出于性能的原因WKWebView 的网络请求在给主进程传递数据的时候会把请求的 body 去掉,导致拦截后请求的 body 参数丢失。
在离线包场景,由于页面的资源不需要 body 数据,所以离线包可以正常使用不受影响。但是在 H5 页面内的其他 post 请求会丢失 data 参数。
为了解决 post 参数丢失的问题mPaas 通过在 js 注入代码hook 了 js 上下文里的 XMLHTTPRequest 对象解决。
通过在 JS 层把方法内容组装好,然后通过 WKWebView 的 messageHandler 机制把内容传到主进程,把对应 HTTPBody 然后存起来,随后通知 JS 端继续这个请求,网络请求到主进程后,在将 post 请求对应的 HttpBody 添加上,这样就完成了一次 post 请求的处理。整体流程可以参考如下:
![ajax-时序图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-05-28-WKWebViewRequestHook)
通过上面的机制,既满足了离线包的资源拦截诉求,也解决了 post 请求 body 丢失的问题。但是在一些场景还是存在一些问题,需要开发者进行适配。
- 方法二JS 侧):
通过 AJAX 请求的 hook 方式,将网络请求的信息代理到客户端本地。能拿到 WKWebView 里面的 post 请求信息,剩下的就不是问题啦。
AJAX hook 的实现可以看这个 [Repo](https://github.com/wendux/Ajax-hook).
## 十、离线包
传统的 H5 技术容易受到网络环境影响,因而降低 H5 页面的性能。通过使用离线包,可以解决该问题,同时保留 H5 的优点。
**离线包** 是将包括 HTML、JavaScript、CSS 等页面内静态资源打包到一个压缩包内。预先下载该离线包到本地,然后通过客户端打开,直接从本地加载离线包,从而最大程度地摆脱网络环境对 H5 页面的影响。
使用 H5 离线包可以给您带来以下优势:
- **提升用户体验**:通过离线包的方式把页面内静态资源嵌入到应用中并发布,当用户第一次开启应用的时候,就无需依赖网络环境下载该资源,而是马上开始使用该应用。
- **实现动态更新**:在推出新版本或是紧急发布的时候,您可以把修改的资源放入离线包,通过更新配置让应用自动下载更新。因此,您无需通过应用商店审核,就能让用户及早接收更新。
![离线包下载流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Hybrid-OfflinePackageDownload.png)
![离线包加载流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Hybrid-OfflinePackageLoad.png)
## 十一、如何落地和推进
如果开展 Hybrid 就存在一个问题,很多业务在迭代或者新开的时候就会选择用 H5 前端技术去实现了,但是如果团队内的客户端同学不懂前端或者不是很懂的时候,就会存在一个抵触的心理,觉得是前端在“侵占”、“蚕食”客户端的领地。所以我们做 Hybrid 跨端项目的时候就需要诚恳一些,抱着“双赢”的出发点去聊、去推进。
- 你在咱们公司当前的项目1中不选择 Hyrbid 技术将来的项目2、项目3其他合作的同学可能就会拥抱 Hybrid 技术来水岸
- 多端融合能力、跨端是趋势,即使在我们公司一直不做,你离开公司去新的公司,肯定也会遇到用跨端去写业务的场景,这是趋势,尽早拥抱吧
- 你如果只做 Native 以后新的项目或者新的机会给你,你抓不住,不如趁此机和我合作,或者分配一些前端开发的小任务给你,趁此学会前端技术和 Hybrid 的设计成为一个跨端工程师点亮、丰富自己的技能树日后项目的技术选型方面类似小程序、Weex/RN/Flutter 方案等,上手就会很方便了,也在做技术方案调研、评估方面多一个可选项。
客户端同学都是程序员,都比较爱学习,拥抱新技术、新设计,早点拥抱技术红利,享受 Hybrid 设计哲学带来的思维增益。我们要实现的效果并不是前端去侵占、蚕食 Native 领地的效果,而是拥抱优雅的、高效的技术方案,去拓展业务上更多的可能性,也在技术方面增加更多的视野维度

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
# 金融 App 金额格式化
> 在一些金融类的 App 中,对于表示金额类的字符串,通常需要进行格式化后再显示出来。例如:
>
> 0 显示为0.00
>
> 123 显示为123.00
>
> 123.456 显示为123.46
>
> 102000 显示为102,000.00
>
> 10204500 显示为10,204,500.00
>
> 它的规则为:个位数起每隔三位数字添加一个逗号,同时保留两位小数,也称为“千分位格式”。
### 方法一
首先根据小数点 `.` 将传入的字符串分割为两部分,整数部分和小数部分(如果没有小数点,则补 `.00`,如果有多个小数点则报金额格式错误)。对于小数部分,只取前两位;然后对整数部分字符串进行遍历,从右到左,每三位数前插入一个逗号 `,`,最后再把两部分拼接起来,代码大致如图 1 和图 2 所示。
```
- (void)method1{
NSArray *temps = @[@"0",@"123",@"123.456",@"102000",@"10204500"];
self.pricelabel.text = [self moneyFromat:temps[arc4random()%5]];
}
- (NSString *)moneyFromat:(NSString *)money{
if (!money || money.length == 0) {
return money;
}
BOOL hasPoint = NO;
if ([money rangeOfString:@"."].length > 0) {
hasPoint = YES;
}
NSMutableString *pointMoney = [NSMutableString stringWithString:money];
if (hasPoint == NO) {
[pointMoney appendFormat:@".00"];
}
NSArray *moneys = [pointMoney componentsSeparatedByString:@"."];
if (moneys.count > 2) {
return pointMoney;
}
else if (moneys.count == 1) {
return [NSString stringWithFormat:@"%@.00",moneys[0]];
}
else {
//整数部分每隔3位插入一个“,”
NSString *frontMoney = [self stringFromToThreeBit:moneys[0]];
if ([frontMoney isEqualToString:@""]) {
frontMoney = @"0";
}
//拼接整数部分和消暑部分
NSString *backMoney = moneys[1];
if (backMoney.length == 1) {
return [NSString stringWithFormat:@"%@.%@",frontMoney,backMoney];
}
else if (backMoney.length > 2) {
return [NSString stringWithFormat:@"%@.%@",frontMoney,[backMoney substringToIndex:2]];
}
else {
return [NSString stringWithFormat:@"%@.%@",frontMoney,backMoney];
}
}
}
- (NSString *)stringFromToThreeBit:(NSString *)string{
NSString *tempString = [string stringByReplacingOccurrencesOfString:@"," withString:@""];
NSMutableString *mutableString = [NSMutableString stringWithString:tempString];
NSInteger n = 2;
if (mutableString.length > 3) {
for (NSUInteger i = mutableString.length - 3; i > 0; i--) {
n++;
if (n == 3) {
[mutableString insertString:@"," atIndex:i];
n = 0;
}
}
}
return mutableString;
}
```
### 方法二
其实,苹果提供了 NSNumberFormatter 用来处理 NSString 和 NSNumber 之间的转化,可以满足基本的数字形式的格式化。我们通过设置 NSNumberFormatter 的 `numberStyle``positiveFormat` 属性,即可实现上述功能,非常简洁。
```objective-c
- (void)method2{
NSArray *temps = @[@"0",@"123",@"123.456",@"102000",@"10204500"];
self.pricelabel.text = [self formatDecimalNumber:temps[arc4random()%5]];
}
- (NSString *)formatDecimalNumber:(NSString *)string{
if (!string || string.length == 0) {
return string;
}
NSNumber *number = @([string doubleValue]);
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = kCFNumberFormatterDecimalStyle;
formatter.positiveFormat = @"###,##0.00";
formatter.positiveFormat = @"###,##0.00";
NSString *amountString = [formatter stringFromNumber:number];
return amountString;
}
```
### [参考资料](https://www.jianshu.com/p/817029422a72)

File diff suppressed because it is too large Load Diff

View File

@@ -1,979 +0,0 @@
# MVC、MVP、MVVM
## MVC 架构
MVC 模式下软件被划分为视图View用户界面、控制器Controller业务逻辑、模型Model数据保存
![MVC架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-MVC.png)
1. 用户操作 View在 View 上面的事件都将被传递到 Controller 处理
2. Controller 处理事件、请求网络,操作 Model 更新状态
3. Model 将更新后的数据发送到 View用户得到反馈
所有的通信都是单向的。
### Apple MVC 架构
效果等价于:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AppleMVCImpl.png" style="zoom:60%" />
最典型的就是 iOS 侧的 UITableView 的设计:
- Controller 感知 ViewUIViewController 通过 View 的形式持有 UITableView
- View 事件代理到 Controller而 View 上的 UI 事件自身不处理,通过代理的形式交给 UIViewController 处理
- Controller 感知 ModelUIViewController 负责从网络或者数据库中拉取数据,持有 Model。在合适的时机上UIViewController 负责将 Model 数据交给 UIView 去展示UITableView 的 cellforRow 代理方法中cell.titleLabel.text = model.title 的形式展示上去) 。也就是说 View 无法感知 Model 的存在。
- Model 的变化通过 Controller 传递到 View 上View 的点击事件通过代理交给 Controller 处理,往往会涉及 Model 的变化。Model 变化后,还是不直接操作 View。依旧通过 Controller 操作 View 的接口,更新数据
优点:
- View 可以重用。因为 View 不用感知 Model 的存在View 展示的东西,外部通过访问修改 UI 控件属性的形式去实现。
```objective-c
@interface GoodCell
@property (nonatomic, strong, readonly) UIImageView *goodsImageView;
@property (nonatomic, strong, readonly) UILabel *goodsNameLabel;
@end
// GoodsListViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
GoodsModel *model = self.goods[indexPath.row];
cell.goodsImageView.image = [UIImage imageNamed:model.iamge];
cell.goodsNameLabel.text = model.name;
return cell;
}
```
- Model 可以重用。Model 也不用感知 View 的存在。
缺点:
- Controller 过于臃肿。因为 View 和 Model 彼此独立,所以读取 Model 然后拼装数据,外部通过访问修改 UI 控件属性,负责展示 UI 的逻辑都放在 Controller 中,所以 Controller 就会很臃肿。
### MVC 架构变种
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AppleMVCRefine.png" style="zoom:60%" />
改变:
- View 可以拥有 Model感知 Model。一部分之前放在 Controller 里的逻辑,拆分到 View 里面了
优点:
- 对 Controller 进行了瘦身,这符合“单一职责原则”,使 Controller 更专注于协调和业务逻辑。
将 UI 配置逻辑内聚到 View 中,避免了 Controller 中冗长的控件操作代码(如 `cell.imageView.image = model.image;`
- 将 View 内部的细节进行了隐藏,更具备封装性。外界不知道 View 内部具体的实现Apple MVC 会暴露 UI 控件,现在不用暴露了),更具封装性
```objective-c
// GoodCell.h
@interface GoodCell
@property (nonatomic, strong) GoodsModel *model;
@end
// GoodCell.m
@interface GoodCell()
@property (nonatomic, strong, readonly) UIImageView *goodsImageView;
@property (nonatomic, strong, readonly) UILabel *goodsNameLabel;
@end
-(void)setModel:(GoodsModel *)model {
_model = model;
self.goodsImageView.image = [UIImage imageNamed:model.iamge];
self.goodsNameLabel.text = model.name;
}
// GoodsListViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
cell.model = self.goods[indexPath.row];
return cell;
}
```
缺点:
- View 强依赖于 Model。
### 思考View 强依赖于 Model 真的是缺点吗?
这个缺点就真的是缺点吗?我一个商品的 GoodsView 展示,就需要展示商品图片、商品名称、商品原价、商品折扣价、销量等信息。单独暴露 UI 控件,然后访问的话很灵活也很独立,但工作量很大,为了 UI 的展示,让 View 持有 Model 真的是缺点吗?
要回答是不是缺点需要回答2个问题
1. 何时是合理的设计?
- View 与 Model 高度绑定
如果 GoodsCell 仅用于展示 GoodsModel且2者生命周期一致这种直接依赖反而合理此时 View 与 Model 的耦合是**高内聚** 的体现
- 避免过度抽象
强行解耦,为 View 抽象设计出一个通用接口,可能引入不必要的复杂度。比如为 GoodsCell 设计一个 `id<GoodsDisplayable>` 协议,在仅有一个 Model 的场景下属于过度设计了
2. 何时可能成为问题?
- 复用 View 展示不同 Model
如果希望 GoodsCell 复用展示其他数据(如 `DiscountGoodsModel`),之前那种 GoodsCell 依赖 GoodsModel 的设计,会导致导致代码难以拓展。此时可以考虑 `ViewModel` 模式或者**协议抽象**解耦
- Model 频繁变更
如果 GoodsModel 的字段,比如 image 频繁变更,所有直接依赖它的 View 都需要同步修改,此时可以将 Model 到 View 的映射逻辑,迁移到独立的转换层,比如 `GoodsModelConverter`
怎么样优化?
1. 可将 Model 到 View 的映射逻辑移到 **独立的转换层**,隔离变化
1. `GoodsModel` 结构如下:
```objective-c
// GoodsModel.h
@interface GoodsModel : NSObject
@property (nonatomic, copy) NSString *imageName;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGFloat price;
@end
```
现在需要将 `GoodsModel` 转换为 View 可直接使用的数据(如处理价格格式化、图片加载):
实现 GoodsModelConverter
```objective-c
// GoodsModelConverter.h
@interface GoodsModelConverter : NSObject
// 将 GoodsModel 转换为字典Key-Value 形式)
+ (NSDictionary *)convertToDisplayData:(GoodsModel *)model;
@end
// GoodsModelConverter.m
@implementation GoodsModelConverter
+ (NSDictionary *)convertToDisplayData:(GoodsModel *)model {
// 处理价格格式化
NSString *priceText = [NSString stringWithFormat:@"¥%.2f", model.price];
// 加载图片(假设 imageName 是本地资源名)
UIImage *image = [UIImage imageNamed:model.imageName];
return @{
@"name": model.name,
@"price": priceText,
@"image": image
};
}
@end
```
修改 View 使用转换后的数据
```objective-c
// GoodsCell.h
@interface GoodsCell : UITableViewCell
// 不再直接依赖 GoodsModel而是通过字典传递数据
- (void)configureWithDisplayData:(NSDictionary *)displayData;
@end
// GoodsCell.m
@implementation GoodsCell {
UIImageView *_goodsImageView;
UILabel *_nameLabel;
UILabel *_priceLabel;
}
- (void)configureWithDisplayData:(NSDictionary *)displayData {
_goodsImageView.image = displayData[@"image"];
_nameLabel.text = displayData[@"name"];
_priceLabel.text = displayData[@"price"];
}
@end
```
Controller 调用
```objective-c
// GoodsListViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
GoodsModel *model = self.goods[indexPath.row];
// 通过 Converter 转换数据
NSDictionary *displayData = [GoodsModelConverter convertToDisplayData:model];
[cell configureWithDisplayData:displayData];
return cell;
}
```
如何应对 Model 的变化?
假设 GoodsModel 的 image 字段变为 remoteImageURL则不需要每处修改 View 使用的地方,统一在 GoodsModelConverter 即可
```objective-c
// GoodsModelConverter.m
+ (NSDictionary *)convertToDisplayData:(GoodsModel *)model {
// 新增网络图片加载逻辑(伪代码)
UIImage *placeholderImage = [UIImage imageNamed:@"placeholder"];
UIImage *image = placeholderImage;
return @{
@"name": model.name,
@"price": priceText,
@"image": image
};
}
```
View 和 Controller 无需任何修改。
- GoodsCell 仍然接收 displayData 字典,不用关心具体来源
- Controller 仍然调用 convertToDisplayData 方法
更优雅的方案是引入 ViewModel 对象。
2. 使用 ViewModel 模式
将 Model 转换为 View 专用的数据结构View 仅依赖 ViewModel
```objective-c
// GoodsCellViewModel.h
@interface GoodsCellViewModel
@property (nonatomic, readonly) UIImage *image;
@property (nonatomic, readonly) NSString *name;
- (instancetype)initWithGoods:(GoodsModel *)goods;
@end
// GoodsCell.h
@interface GoodsCell
@property (nonatomic, strong) GoodsCellViewModel *viewModel;
@end
```
优点:
- View 与 Model 解耦,便于复用
- ViewModel 可以封装数据转换逻辑(比如多语言本地化、图片加载、字符串格式化拼接)
3. 通过抽象协议依赖
定义 View 所需数据的协议Model 实现该协议。
定义 GoodsDisplayable 协议
```objective-c
// GoodsDisplayable.h
@protocol GoodsDisplayable
@property (nonatomic, readonly) NSString *goodsImageName;
@property (nonatomic, readonly) NSString *goodsName;
@end
// GoodsModel.h
@interface GoodsModel : NSObject <GoodsDisplayable>
@end
// GoodsCell.h
@interface GoodsCell
@property (nonatomic, strong) id<GoodsDisplayable> displayData;
@end
```
在正常商品列表页面Controller 逻辑为:
```objective-c
// GoodsListViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
cell.displayData = self.goods[indexPath.row];
return cell;
}
```
在打折商品列表页面Controller 逻辑为:
```objective-c
// DiscountGoodsListViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
GoodsCell *cell = [tableView dequeueReusableCellWithIdentifier:GoodsCellID forIndexPath:indexPath];
cell.displayData = self.goods[indexPath.row];
return cell;
}
```
现在我们可以尝试概括性回答该问题了:
**View 持有 Model 是否是缺点,取决于具体场景:**
- **单一用途、无复用需求**:直接依赖 Model 是合理选择,简化代码且无过度设计。
- **需复用或 Model 不稳定**:通过 ViewModel 或协议解耦,提高灵活性。
最终,**没有绝对的最佳实践,只有适合场景的权衡**。Apple 的 MVC 变种在简单场景下有效,但在复杂场景需结合其他模式优化。
## MVP 架构
MVP 模式将 Controller 改名为 Presenter通信改变了通信方向
![MVP架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MVPArchStructure.png)
1. 各部分之间的通信都是双向的
2. Model 与 View 不发生联系,都通过 Presenter 传递
3. View 层非常薄。不部署任何业务逻辑称为“被动视图Passive View即没有任何主动性
4. 而 Presenter 非常厚,所有的逻辑都部署在这层。比如在 Presenter 里组装 View 展示到 Controller 的 View 上,加载数据,展示到 View 上。
5. 如果 Presenter 这一层很厚的话,可以继续拆,比如再增加一个 Interactor 的角色,专门处理处理 View UI 响应事件。这样子角色更多,职责也更清晰。维护也方便。
### 错误实现
举个例子:
App 只有一个个人中心 ViewController其 View 上只有1个展示个人信息的子 View子 View 上只有1个展示头像 UIImageView 和 1个展示昵称的 UILabel。用 MVP 的思想实现如下
模型为
```objective-c
@interface PersonalInfoModel : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *image;
@end
```
关键就是要增加一个 `Presenter` 的角色,负责组装 View 展示到 Controller 的 View 上,加载数据,展示到 View 上。
```objective-c
// PersonInfoPresenter.h
@interface PersonInfoPresenter : NSObject
- (instancetype)initWithController:(UIViewController *)controller;
@end
// PersonInfoPresenter.m
@interface PersonInfoPresenter() <PersonalInfoViewDelegate>
@property (weak, nonatomic) UIViewController *controller;
@end
@implementation PersonInfoPresenter
- (instancetype)initWithController:(UIViewController *)controller
{
if (self = [super init]) {
self.controller = controller;
// 创建View
PersonalInfoView *personalInfoView = [[PersonalInfoView alloc] init];
personalInfoView.frame = CGRectMake(100, 100, 100, 150);
personalInfoView.delegate = self;
[controller.view addSubview:personalInfoView];
// 加载模型数据
PersonalInfoModel *model = [[PersonalInfoModel alloc] init];
model.name = @"杭城小刘";
model.image = @"UnixKernel";
// 赋值数据
[personalInfoView setName:app.name andImage:app.image];
}
return self;
}
#pragma mark - MJAppViewDelegate
- (void)personalInfoViewDidClick:(PersonalInfoView *)personalInfoView
{
NSLog(@"presenter 监听了个人信息 View 的点击事件");
}
```
PersonalInfoView
```objective-c
#import <UIKit/UIKit.h>
@class PersonalInfoView;
@protocol PersonalInfoViewDelegate <NSObject>
@optional
- (void)personalInfoViewDidClick:(PersonalInfoView *)personalInfoView;
@end
@interface PersonalInfoView : UIView
- (void)setName:(NSString *)name andImage:(NSString *)image;
@property (weak, nonatomic) id<PersonalInfoViewDelegate> delegate;
@end
@implementation MJAppView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
UIImageView *iconView = [[UIImageView alloc] init];
iconView.frame = CGRectMake(0, 0, 100, 100);
[self addSubview:iconView];
_iconView = iconView;
UILabel *nameLabel = [[UILabel alloc] init];
nameLabel.frame = CGRectMake(0, 100, 100, 30);
nameLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:nameLabel];
_nameLabel = nameLabel;
}
return self;
}
- (void)setName:(NSString *)name andImage:(NSString *)image {
_iconView.image = [UIImage imageNamed:image];
_nameLabel.text = name;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.delegate && [self.delegate respondsToSelector:@selector(personalInfoViewDidClick:)]) {
[self.delegate personalInfoViewDidClick:self];
}
}
@end
```
在 ViewController 的使用
```objective-c
@interface ViewController ()
@property (strong, nonatomic) PersonInfoPresenter *presenter;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.presenter = [[PersonInfoPresenter alloc] initWithController:self];
}
@end
```
目前实现的效果是:
- Presenter 负责了 View 的创建与展示,同时调用 Model 的能力,从数据库或者网络加载业务数据,然后更新到 View 上。使得 Controller 逻辑更加单一
- 将大部分 View 的组装逻辑从 Controller 中抽离到 Presenter 中,实现了 Controller 的瘦身
- Model 和 View 无法感知对方,也无法直接访问,保证了 Model 和 View 的复用能力
### 正确实现
目前的实现中Presenter 中拥有了 View对于 Presenter 的单元测试不好展开,该如何修改?
- View 还是放在 Controller 中创造
- 为了解耦,将 View UI 事件和数据获取抽成对应的协议
- Model 依旧是薄薄的一层,数据获取放在 Service 里进行
定义 View 刷新协议
```objective-c
// PersonalInfoViewProtocol.h
@protocol PersonalInfoViewProtocol <NSObject>
- (void)displayName:(NSString *)name image:(NSString *)image;
@end
```
View 实现协议
```objective-c
// PersonalInfoView.h
@interface PersonalInfoView : UIView <PersonalInfoViewProtocol>
@property (nonatomic, weak) id<PersonalInfoViewDelegate> delegate;
@end
// PersonalInfoView.m
@implementation PersonalInfoView
// 原有代码不变,但实现 displayName:image: 方法
- (void)displayName:(NSString *)name image:(NSString *)image {
_iconView.image = [UIImage imageNamed:image];
_nameLabel.text = name;
}
- (void)didClickProfileView {
if (self.delegate && [self.delegate respondsToSelector:@selector(personalInfoViewDidClick:)]) {
[self.delegate personalInfoViewDidClick:self];
}
}
@en
```
定义数据拉取协议
```objective-c
// PersonalInfoServiceProtocol.h
@protocol PersonalInfoServiceProtocol <NSObject>
- (void)fetchPersonalInfoWithCompletion:(void (^)(PersonalInfoModel *model))completion;
@end
```
定义数据拉取 Services
```objective-c
// PersonalInfoService.h
@interface PersonalInfoService : NSObject <PersonalInfoServiceProtocol>
@end
// PersonalInfoService.m
@implementation PersonalInfoService
- (void)fetchPersonalInfoWithCompletion:(void (^)(PersonalInfoModel *))completion {
// 模拟网络请求
dispatch_async(dispatch_get_global_queue(0, 0), ^{
PersonalInfoModel *model = [[PersonalInfoModel alloc] init];
model.name = @"杭城小刘";
model.image = @"UnixKernel";
dispatch_async(dispatch_get_main_queue(), ^{
completion(model);
});
});
}
@end
```
重构 Presenter。loadData 数据加载成功后,调用其 View 的代理方法,刷新 UI
```objective-c
// PersonInfoPresenter.h
@interface PersonInfoPresenter : NSObject
- (instancetype)initWithView:(id<PersonalInfoViewProtocol>)view
service:(id<PersonalInfoServiceProtocol>)service;
- (void)loadData;
@end
// PersonInfoPresenter.m
@interface PersonInfoPresenter()
// 重要地方:遵循相应协议的 View 对象
@property (nonatomic, weak) id<PersonalInfoViewProtocol> view;
// 重要地方:遵循相应协议的 Services 对象
@property (nonatomic, strong) id<PersonalInfoServiceProtocol> service;
@end
@implementation PersonInfoPresenter
- (instancetype)initWithView:(id<PersonalInfoViewProtocol>)view
service:(id<PersonalInfoServiceProtocol>)service {
if (self = [super init]) {
_view = view;
_service = service;
}
return self;
}
- (void)loadData {
[self.service fetchPersonalInfoWithCompletion:^(PersonalInfoModel *model) {
[self.view displayName:model.name image:model.image];
}];
}
#pragma mark - 点击事件处理(可选)
- (void)handleViewClick {
NSLog(@"Presenter 处理点击事件");
}
@end
```
Controller 里组装和创建 Presenter在 `viewDidLoad` 里面调用 presenter 的 loadData 方法,让其内部的 Services 加载网络数据。
```objc
// ViewController.m
@interface ViewController () <PersonalInfoViewDelegate>
@property (nonatomic, strong) PersonInfoPresenter *presenter;
@property (nonatomic, strong) PersonalInfoView *infoView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建 View
self.infoView = [[PersonalInfoView alloc] initWithFrame:CGRectMake(100, 100, 100, 150)];
self.infoView.delegate = self;
[self.view addSubview:self.infoView];
// 注入依赖
id<PersonalInfoServiceProtocol> service = [[PersonalInfoService alloc] init];
self.presenter = [[PersonInfoPresenter alloc] initWithView:self.infoView service:service];
// 触发数据加载
[self.presenter loadData];
}
#pragma mark - PersonalInfoViewDelegate
- (void)personalInfoViewDidClick:(PersonalInfoView *)view {
[self.presenter handleViewClick];
}
@end
```
关键改动点:
1. **Presenter 不再持有 View**:通过协议与 View 通信,避免直接依赖具体类。
2. **数据加载抽象为 Service**Presenter 通过协议调用 Service便于模拟不同场景。
3. **布局职责回归 View**Presenter 不再设置 `frame`,保证单一职责。
4. **依赖注入**Presenter 的 Service 和 View 通过初始化注入,测试时可替换为 Mock。
思考:如何一个复杂的购物车页面包含很多子 View按照 MVP 模式,该怎么处理?
- **模块化拆分**:每个功能单元独立为 View-Presenter-Service 组合
可以把复杂的购物车 view 拆分为独立的几个 View。比如
- 商品列表 GoodsListView、商品列表 GoodsListPresenter、商品列表 GoodsListServices
- 商品统计信息 GoodsSummaryInfoView、商品统计信息 GoodsSummaryInfoPresenter、商品统计信息 GoodsSummaryInfoServices
- 营销活动展示 GoodsPromoptionView、营销活动展示 GoodsPromoptionPresenter、营销活动展示 GoodsPromoptionServices
- 每个 View 设计自己对应的 Presenter
- Controller 依旧复杂 View 的组装,和各个 Presenter 的创建工作
- **协调机制**:使用 Coordinator 或响应式编程管理跨模块事件
- 其他流程和单个 View、单个 Presenter 没啥不同。区别在于复杂的业务下Controller 会存在多个 View、多个 Presenter
## MVVM 架构
MVVM 模式将 Presenter 改名为 ViewModel基本上与 MVP 模式完全一致。
![MVVM架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MVVMArchStructure.png)
区别在于采用双向绑定的模式。View 的改变,自动反应在 ViewModel 上。 ViewModel 的改变会发应在 View 上。
MVC 等到业务逻辑很复杂的时候被称为 Massive View Controller (重量级视图控制器)。这样的 Controller 后期维护看着阅读成本很高、不易于测试、维护。当时写这个代码的人离职后,几千行规模的代码在后期添加新功能或者修改 Bug 都是一件折磨人的事情。
![典型MVC架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-iOSMVC.png)
看到 View 和 ViewController 是不同的技术组件,但是日常开发中它们总是成对的存在,为什么不考虑将他们的连接正规化呢?
![存在问题](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-VController-Model.png)
典型的 MVC 存在弊端就是 Controller 层非常复杂很多逻辑都在里面包括一些不是逻辑的“表示逻辑”presentation logic。用 MVVM 术语来说就是将那些 Model 数据转换为 View 可以呈现的东西。例如将一个 NSDate 格式化为一个“2018-11-17” 这样的 NSString。
上图中缺少一个环节,一个专门用来处理所有的表示逻辑。称为 “ViewModel”。位于 ViewController 与 Model 之间。
![MVVM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-iOSmvvm.png)
MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel。iOS 使用 MVVM 可以降低 ViewController 的复杂性并使得表示逻辑易于测试。
- MVVM 兼容当下的 MVC 机构
- MVVM 增加应用的可测试性
- MVVM 配合一个绑定机制效果最好
### **MVVM 核心改造点**
1. **引入 ViewModel**:负责数据转换、业务逻辑,通过数据流驱动 UI 更新
2. **数据双向绑定**:使用 ReactiveCocoaRAC建立 View 和 ViewModel 的绑定关系
3. **事件命令化**:用户交互事件封装为 Command由 ViewModel 处理
改造如下:
定义 ViewModel核心
```objc
// PersonalInfoViewModel.h
#import <ReactiveObjC/ReactiveObjC.h>
@class PersonalInfoModel;
@interface PersonalInfoViewModel : NSObject
// 输出属性(供 View 绑定)
@property (nonatomic, strong, readonly) RACSignal<NSString *> *nameSignal;
@property (nonatomic, strong, readonly) RACSignal<UIImage *> *imageSignal;
// 输入命令(处理 View 事件)
@property (nonatomic, strong, readonly) RACCommand *viewDidClickCommand;
// 初始化方法(依赖注入)
- (instancetype)initWithService:(id<PersonalInfoServiceProtocol>)service;
@end
// PersonalInfoViewModel.m
@interface PersonalInfoViewModel()
@property (nonatomic, strong) id<PersonalInfoServiceProtocol> service;
@property (nonatomic, strong) RACSubject *nameSubject;
@property (nonatomic, strong) RACSubject *imageSubject;
@end
@implementation PersonalInfoViewModel
- (instancetype)initWithService:(id<PersonalInfoServiceProtocol>)service {
if (self = [super init]) {
_service = service;
_nameSubject = [RACSubject subject];
_imageSubject = [RACSubject subject];
// 暴露信号
_nameSignal = _nameSubject;
_imageSignal = _imageSubject;
// 初始化命令
[self setupCommands];
[self loadData];
}
return self;
}
#pragma mark - 初始化命令
- (void)setupCommands {
@weakify(self);
_viewDidClickCommand = [[RACCommand alloc]
initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
NSLog(@"ViewModel 处理点击事件");
return [RACSignal empty];
}];
}
#pragma mark - 数据加载
- (void)loadData {
@weakify(self);
[self.service fetchPersonalInfoWithCompletion:^(PersonalInfoModel *model) {
@strongify(self);
// 更新数据流
[self.nameSubject sendNext:model.name];
[self.imageSubject sendNext:[UIImage imageNamed:model.image]];
}];
}
@end
```
改造 View
```objective-c
// PersonalInfoView.h协议不再需要
@interface PersonalInfoView : UIView
// 暴露 UI 组件用于绑定
@property (nonatomic, strong) UIImageView *iconView;
@property (nonatomic, strong) UILabel *nameLabel;
// 绑定 ViewModel
- (void)bindViewModel:(PersonalInfoViewModel *)viewModel;
@end
// PersonalInfoView.m
#import <ReactiveObjC/ReactiveObjC.h>
@implementation PersonalInfoView
- (instancetype)initWithFrame:(CGRect)frame {
// ... 原有初始化代码不变 ...
}
- (void)bindViewModel:(PersonalInfoViewModel *)viewModel {
// 绑定数据到 UI
RAC(self.nameLabel, text) = [viewModel.nameSignal deliverOnMainThread];
RAC(self.iconView, image) = [viewModel.imageSignal deliverOnMainThread];
// 绑定点击事件到 Command
@weakify(viewModel);
[self addGestureRecognizer:[[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleTap)]];
[[self rac_signalForSelector:@selector(handleTap)]
subscribeNext:^(id x) {
@strongify(viewModel);
[viewModel.viewDidClickCommand execute:nil];
}];
}
- (void)handleTap {
// 空方法,仅用于触发 RAC 信号
}
@end
```
改造 ViewController胶水、组装
```objective-c
// ViewController.m
@interface ViewController ()
@property (nonatomic, strong) PersonalInfoView *infoView;
@property (nonatomic, strong) PersonalInfoViewModel *viewModel;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建 View
self.infoView = [[PersonalInfoView alloc] initWithFrame:CGRectMake(100, 100, 100, 150)];
[self.view addSubview:self.infoView];
// 创建 Service 和 ViewModel
id<PersonalInfoServiceProtocol> service = [[PersonalInfoService alloc] init];
self.viewModel = [[PersonalInfoViewModel alloc] initWithService:service];
// 绑定 ViewModel
[self.infoView bindViewModel:self.viewModel];
}
@end
```
## 一个简单的例子
PersonModel
```objective-c
@interface Person : NSObject
- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;
@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;
@end
```
PersonViewController
```objective-c
- (void)viewDidLoad {
[super viewDidLoad];
if (self.model.salutation.length > 0) {
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
} else {
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
}
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}
```
上面是标准的 MVC。现在我们考虑用 ViewModel 增加改进下
PersonViewModel
```objective-c
@interface PersonViewModel : NSObject
- (instancetype)initWithPerson:(Person *)person;
@property (nonatomic, readonly) Person *person;
@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *birthdateText;
- (instancetype)initWithPerson:(Person *)person;
@end
@implementation PersonViewModel
- (instancetype)initWithPerson:(Person *)person {
self = [super init];
if (!self) return nil;
_person = person;
if (person.salutation.length > 0) {
_nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
} else {
_nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
}
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
_birthdateText = [dateFormatter stringFromDate:person.birthdate];
return self;
}
@end
```
此时,我们的 ViewController 会很轻量
```objective-c
- (void)viewDidLoad {
[super viewDidLoad];
self.nameLabel.text = self.viewModel.nameText;
self.birthdateLabel.text = self.viewModel.birthdateText;
}
```
可测试View Controller 是出了名的难测试。因为传动的 VC 很重,在 MVVM 的世界里,代码各司其职,测试 View Controller 变得较为容易
```objective-c
SpecBegin(Person)
NSString *salutation = @"Dr.";
NSString *firstName = @"first";
NSString *lastName = @"last";
NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0];
it (@"should use the salutation available. ", ^{
Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate];
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
expect(viewModel.nameText).to.equal(@"Dr. first last");
});
it (@"should not use an unavailable salutation. ", ^{
Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
expect(viewModel.nameText).to.equal(@"first last");
});
it (@"should use the correct date format. ", ^{
Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");
});
SpecEnd
```
## VIPER 架构
`View-Interactor-Presenter-Entity-Routing` ,一种基于单一职责原则和清晰的模块界限的架构模式,包含五个主要组成部分:
- View视图层负责显示用户界面和接收用户输入。它将用户输入传递给 Presenter
- Presenter演示者将从 View 层获取指令转到 Interactor 处理并接收处理后的数据,并将其格式化为适合 View 显示的格式,然后将这些数据传递给 View 去显示更新
- Interactor交互器负责处理具体的业务逻辑。它获取数据可能来自网络、数据库等处理业务规则和数据并将结果传递给Presenter
- Entity实体层应用的基本数据对象类似 MVC 架构中的 Model 层,例如数据库对象或网络请求的 JSON 对象等
- Routing路由层负责页面之间的导航逻辑跳转
VIPER 架构的优点在于模块职责划分清晰,模块间的耦合度低,特别适合大项目的开发, 其主要缺点是由于层的划分较多,增加了代码复杂性,对于小型项目而言可能会显得过度设计
这么看来 VIPER 很像前端中 Redux 的设计:
- VIPER 相比 Redux 简化了 UI 事件 Action 和 ActionCreator 这个角色
- Interactor 做的事情类似 Reducer ,都会根据对应的事件类型,判断如何处理数据。区别在于前端约定 Reducer 的实现必须是纯函数
除了角色不同外VIPER 和 Redux 对于 UI 展示和事件响应、处理的整个流程很类似。所以可以理解为「 VIPER 是 Redux 在客户端的简易实现」。

View File

@@ -1,93 +0,0 @@
# 事件响应者链
实验1:
定义 BaseView在里面实现方法touchBegan监听当前哪个类调用了该方法。
在控制器的界面上加5个颜色不同的view每个view自定义view去实现因此在不同的view上的手势就可以由不同的view拦截到。
![UI效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Simulator%20Screen%20Shot%20-%20iPhone%206s%20Plus%20-%202017-10-11%20at%2010.14.37.png)
```objective-c
//BaseView
#import "BaseView.h"
@implementation BaseView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%@",[self class]);
}
```
结果点击不同的View打印出不同的类名。
结论:
* 触摸事件是从父控件传递到子控件的。
* 点击了绿色图上的2级的viewUIApplication-&gt; UIWindow -&gt; UIViewController的view -&gt; 绿色的view
* 点击了蓝色图上的3级的viewUIApplication-&gt; UIWindow -&gt; UIViewController的view -&gt; 红棕色的view -&gt; 蓝色的view
* 点击了黄色图上的4级的viewUIApplication -&gt; UIWindow -&gt; UIViewController的view -&gt; 红棕色的view -&gt; 蓝色的view -&gt; 黄色的view
注意:如果父控件不能接收触摸事件,那么这个父控件的子控件也不能接收触摸事件
#### 如何找到最合适的控件来接收触摸事件?
* 自己能否接收触摸事件?
* 触摸点是否在自己身上?
* 从后往前遍历子控件重复前面2个步骤
* 如果没有符合条件的子控件,那么就自己最适合处理
# 事件响应原理
产生的touch方法的默认做法是将事件顺着响应者链条向上传递将事件交给上一个响应者处理。
#### 响应者链条
![响应者链条](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/reponseChain.png)
#### 事件传递的完整过程
1. 先将事件对象由上往下传递(父控件传递给子控件),找到最合适的控件来处理
2. 调用最合适控件的touch方法
3. 如果调用了\[super touches...\]方法就会将事件顺着响应者链条向上传递,传递给上一个响应者
4. 接着就会调用上一个响应者的touches...方法
#### 事件响应者
##### 如何判断该控件的上一个响应者?
1. 如果当前这个view是控制器的view那么上一个响应者就是控制器
2. 如果当前这个view不是控制器的view那么上一个响应者就是父控件。
事件传递给UIApplication后如果不处理的话该事件会销毁掉。
控制器view上的子控件的touch...方法如果子控件不处理那么都会顺着响应者链条向上传递给上一层响应者对象,比如可以交给控制器处理。
## 手饰事件和点击事件的响应顺序
假如给某个 view 所在的父视图添加了手饰识别器。
**手势识别器的优先级**:如果你将 `UITapGestureRecognizer` 添加到了视图上UIKit 会首先尝试识别手势。如果视图上添加了多个手势识别器,它们的识别顺序将根据它们被添加到视图的顺序或者它们的 `delaysTouchesBegan` 和 `delaysTouchesEnded` 属性来决定。
想要子 view 响应事件而不是被根视图拦截,则需要给手势识别器添加代理,实现代理方法
```objective-c
UIGestureRecognizer *gesture;
gesture.delegate = self;
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
return NO;
}
```
如果 `UITapGestureRecognizer` 识别了一个手势,它可以通过设置 `cancelsTouchesInView` 属性为 `YES` 来取消视图上的触摸事件,这样点击事件就不会被进一步传递到视图控制器的 `touchesBegan` 或 `touchesEnded` 方法。

View File

@@ -1,25 +0,0 @@
# 动态库和静态库
通常,我们的 Xcode 工程会依赖一些第三方库,包括:.a 静态库Static Library和 .framework 动态库Dynamic Library
不过简单地把 .framework 后缀的文件称为“动态库”并不严谨,因为在 iOS/macOS 开发中framework 又分为 “静态 framework” 和 “动态 framework”区别如下
* 静态 framework可以理解为是 .a 静态文件 + .h 公共头文件 + 资源文件 的集合,本质上与 .a 静态库是一致的;
* 动态 framework即真正意义上的动态库一般包括动态二进制文件、头文件和资源文件等。
对于一个 Static Library 工程,其编译产物为 .a 静态二进制文件 + 公共 .h 头文件;
对于一个 Framework 工程,其编译的最终产物是动态库还是静态库,我们可以通过在 Build Settings -> Linking -> Mach-O Type 中进行选择设置其值为 Dynamic Library 或者 Static Library。
此外,我们知道,对于一个 Mach-O 二进制文件,不管是 static 还是 dynamic一般都包含了几种不同的处理器架构Architectures例如i386, x86_64, armv7, armv7s, arm64 等。
Xcode 在编译链接时,对于静态库和动态库的处理方式是不同的。
对于静态库在链接时Linking TimeXcode 会自动筛选出静态库中的不同 architecture 合并到对应处理器架构的主可执行二进制文件中而在打包归档ArchiveXcode 会自动忽略掉静态库中未用到的 architecture例如会移除掉 i386, x86_64 等 Mac 上模拟器专用的架构。
而对于动态库在编译打包时Xcode 会直接拷贝整个动态 framework 文件到最终的 .ipa 包中,只有在 App 真正启动运行时,才会进行动态链接。但是苹果是不允许最终上传到 App Store Connect 后台的 .ipa 文件包含 i386, x86_64 等模拟器架构的,会报 Invalid 错误,所以对于工程中的动态 framework我们在打 Release 正式包时,一般会通过执行命令或者脚本的方式移除掉这些 Invalid Architectures。
最后,如何在 Xcode 工程中添加这些静态/动态库呢?
对于 “.a 静态库” 和 “静态 framework” ,直接拖拽到工程中,并勾选 Copy if needed 选项即可,无需其他设置。

View File

@@ -1,280 +0,0 @@
# cocoapods 相关小技巧
## 1. 组件的地址
我们在做组件化的时候经常将一些业务模块封装打包,做成 pod 管理的形式,然后当在开发的时候需要修改一些模块化的代码。
当维护好组件的时候我们可能在一个新的工程设置好 podfile 引入组件,但是有可能需要继续修改组件的源代码,代码需要可编辑。所以我们可能需要将 Podfile 中的 pod 源修改为本地。
然后安装 pod install 后就可以看到在项目文件里面有可编辑的组件代码
```
pod 'GoodsCategoryModule', :path => '../GoodsCategoryModule'
#pod 'GoodsCategoryModule',:git => 'git@gitlab.xxx.com:forntend_ios/GoodsCategoryModule.git',:branch => 'release/Appstore'
```
注意点:
1. 我们本地的组件源代码需要和 pod 文件中的代码工程文件名称一致
2. 注释掉远端仓库的地址
path 后面是相对路径
## 2. 组件库使用 pch 头文件
```Objective-c
///一个区分开发和线上环境的Log。NSLog的本质是调用对象方法的 description 方法所以线上代码使用NSLog会造成性能和安全问题
#ifdef DEBUG
#define SafeLog(...) NSLog(__VA_ARGS__)
#else
#define SafeLog(...)
#endif
```
所以我们需要对各个组件库里面的 **NSLog** 进行改造,变成 **SafeLog**。没做特殊处理,当你在 pod 库的 pch 文件写好代码,发现主工程执行 pod install 之后,看到之前写的 SafeLog 代码不见了。所以我们需要指定 pch 文件。
操作:
- 新建 **PrefixHeader.h** 头文件
- 在当前 pod 库的 ***.podspec 文件中写如下代码
```Ruby
s.prefix_header_contents = '#import "PrefixHeader.h"'
```
说明:该代码的作用相当于将 PrefixHeader.h 文件写入到当前 pod 库 XQTriggerKit-prefix.pch 文件的最后一行。相当于
```Objective-c
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
#import "PrefixHeader.h"
```
缺点:项目存在多个 pod 组件库。主工程修改 pch 还好,但是每个 pod 库都去新建 PrefixHeader.h 文件和 podspec 文件添加一行代码。工作量非常大。
改进方案:对 NSLog 进行 Hook。
步骤:
- 引入 fishhook 库
- 新建 SafeLog.h 和 SafeLog.m 文件
- 在 SafeLog.m 文件的 load 方法中对 NSLog 进行 hook判断是 Debug 环境就打印。代码如下
```Objective-c
#import "SafeLog.h"
#import "fishhook.h"
// orig_NSLog是原有方法被替换后 把原来的实现方法放到另一个地址中
// new_NSLog就是替换后的方法了
static void (*orig_NSLog)(NSString *format, ...);
@implementation SafeLog
void(new_NSLog)(NSString *format, ...)
{
va_list args;
if (format) {
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
#ifdef DEBUG
orig_NSLog(@"%@", message);
#endif
va_end(args);
}
}
+ (void)load
{
rebind_symbols((struct rebinding[1]){ {"NSLog", new_NSLog, (void *)&orig_NSLog} }, 1);
}
@end
```
## 3. CocoPods 指定版本号带 ~> 与不带的区别
- 带 ~> 是指库版本号的一个范围。大于等于指定的版本号,小于高一位的版本号
- 不带 ~> 是指固定的版本号
```
pod 'aaa', '~> 0.1.2' // 大于等于 0.1.2 且小于 0.2
pod 'bbb', '1.1' 版本号指定为 1.1
```
## 4. 查看当前 Pod 库被何处引用
出于某种原因经常会需要查看当前 Pod 库被何处引用了你需要修改组件库A然后A修改完之后可能需要在依赖组件库A的地方去修改版本号。这时候就需要查询了例子可能不优雅但是确实有需要查询引用的情况有轮子
- 先下载所需要的库
```Shell
gem install reversepoddependency
```
- 利用脚本在我们的组件库 repo 中去查询
```
specbackwarddependency /Users/liubinpeng/.cocoapods/repos/51xianqu-xq_specs(本地CocoPod repo地址) xq_baaidumapkit(组件库名称)
```
鉴于这个命令比较长,且本人使用的是 iTerm2+Zsh所以将命令写入到 .zshrc 文件中, source 编译链接过可以快速使用。例如 `repoanalysis xq_baaidumapkit`
```Shell
alias repoanalysis='specbackwarddependency /Users/liubinpeng/.cocoapods/repos/51xianqu-xq_specs'
```
![Pod组件库依赖分析](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-PodComponentAnalysis.png)
具体的操作步骤可以参考我的这个[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第六部分%20开发杂谈/6.11.md)
## 5. lint 的时候安装一些神奇的依赖
在对 App 做应用包瘦身的时候发现了一些问题。某个组件库 lint 的时候通过终端的信息,发现安装了一些不是 podspec 里面指定的依赖仓库。百思不得其解,同事说可能是之前的某个版本依赖了这些项目。有了这个思路就好办事了。
![遇到问题](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-13-Cocopod-Lint-Cache.png)
- 打开本地的 `/Users/liubinpeng/.cocoapods/repos` 文件夹。查看本地的私有 repo 的管理的所有的项目,找到出问题的 repo进去删掉有问题的 tag。
- 出问题的 repo 将远端的有问题的 tag 也删掉
- 删除远端的 repo 仓库的有问题的 tag。
## 6. lint 失败 - 报错为 `error: invalid task ('StripNIB...`
```Shell
Build system information
error: invalid task ('StripNIB /Users/liubinpeng/Library/Developer/Xcode/DerivedData/App-fgbnpsgtrtstroctiqnanvyrfwyr/Build/Products/Release-iphonesimulator/XQLoginModule/SDGMemberCardBindViewController.nib') with mutable output but no other virtual output node (in target 'XQLoginModule')
```
原因为 xib 和图片资源都属于资源文件不可以放在源文件Classes需要放在 Assets 中。如果放到 Classes 文件夹中 lint 会报错。
## 7. 一台电脑安装了最新版本的,出问题删除最新版 Xcode下载旧版本 Xcodepod install 失败
```Shell
You need at least git version 1.8.5 to use CocoaPods
```
- 可能是cocoapods安装成功了但是链接Xcode的版本过低所以需要更新Xcode
- 电脑安装了多个版本的Xcode就需要修改链接Xcode路径改成链接电脑比较高版本的Xcode。
```Shell
sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
```
## 8. 一台电脑安装了最新版本的,出问题删除最新版 Xcode下载旧版本 Xcode打开工程 Xib 报错
- sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService
- xcrun simctl erase all
## 9. 开发电脑上安装了旧版本 Cocopods,Mac 系统升级后, pod lint 失败
解决方案: 将 **fourflusher** 仓库中上的 [find.rb]((https://raw.githubusercontent.com/CocoaPods/fourflusher/master/lib/fourflusher/find.rb)) 文件中的 ruby 脚本复制到本地的 fourflusher 下.
查找本地 fourflusher 文件夹所在位置.
```Shell
gem which fourflusher
```
我的电脑 find.rb 文件所在位置.
`/Library/Ruby/Gems/2.6.0/gems/fourflusher-2.3.1/lib/fourflusher/find.rb`
注意: 文件保存需要权限,所以加 sudo
## 10. pod lint 产生的信息太多,一屏显示不全,但是出错之后我们可能需要去查看 error 信息,上下翻页不方便
解决方案: 利用脚本 ` >1.log 2>&1` 将当前的 pod lint 产生的信息写入文件.
完整代码
```Ruby
pod lib lint --sources=****,**** --allow-warnings --verbose --use-libraries >1.log 2>&1
```
## 11. pod 库每次修改代码,主工程必须 clean 再安装才可以看到新改动的代码
解决方案:
```ruby
install! 'cocoapods', :disable_input_output_paths => true
```
## 12. pod 库太多,每次构建编译都很耗费时间
```ruby
install! 'cocoapods', generate_multiple_pod_projects: true
```
## 13. 卸载旧版本 cocoapods 安装新的
```shell
sudo gem uninstall cocoapods-core cocoapods cocoapods-deintegrate cocoapods-downloader cocoapods-plugins cocoapods-search cocoapods-stats cocoapods-trunk cocoapods-try coderay colored2 concurrent-ruby cocoapods-clean
sudo gem install cocoapods
```
## 14. pod 拉取代码太慢
在平时开发的时候,使用 cocoapods 拉取代码经常会比较慢,偶尔一次两次也还可以忍受,但是某些哭每次都很慢,而且 install 的时候工程被修改,没办法编译开发。所以需要想个办法解决这种问题。
其实知道 cocoapods 的工作原理的话,我们可以投机取巧。做过 SDK 开发的同学一般都知道,代码开发、测试完毕后需要 lint将 podspec 文件提交到中心。项目中 Podfile 中添加依赖描述,依赖可以用 分支、tag、path 等形式指定。
方法一:
所以明白怎么工作的,我们可以在本地搭建一个静态服务,专门用来提供较大 SDK 的下载,拿 NodeJS、python、Java、php 都可以,很快写一个静态服务。然后从本地 `.cocoapods` 里找到对应的 SDK 文件夹,修改 podspec 文件,修改 **source** 为本地服务器地址资源地址。
由于本地静态服务里面的资源可能会停留在较早版本,所以可以使用定时服务拉取 github 项目最新代码。crontab + shell 做这个很容易。
方法二:
前端有 jsdeliver针对 js/css 加速访问。但是它也支持 github 上的仓库。
但是包大小限制jsdeliver 规定单个文件不能大于 20M仓库的某版本不能大于 50M。所以可以对大文件进行 **xz 压缩**。
Mac split 命令可以拆分包cat 可以合并包。
```shell
// 分割文件
split -b 10m xxxx.tar.xz xxxx.tar.xz .
// 查看效果
ll xxxx.xz*
// 合并
cat xxxx.tar.xz.* > xxxx.tar.xz
// 再次验证大小
ll xxxx.tar.xz
// 解压
mkdir unpackedDir
mv xxxx.tar.xz unpackedDir
cd unpackedDir
tar vxf xxxx.tar.xz
```
所以我们可以将较大的库拆分多个,部署到 jsdeliver然后使用的时候进行加速。
这一步可以是脚本自动完成,写脚本,将文件夹压缩,拆分,上传。
所以使用的时候类似
```shell
wget https://cdn.jsdelivr.net/mmm/xxxx.tar.xz.aa
wget https://cdn.jsdelivr.net/mmm/xxxx.tar.xz.ab
wget https://cdn.jsdelivr.net/mmm/xxxx.tar.xz.ac
cat xxxx.tar.xz.* > xxxx.tar.xz
tar xvf xxxx.tar.xz
rm xxxx.tar.xz.* xxxx.tar.xz
```
结合 cocoapods 的 **prepare_command** 使用。
比如
```ruby
Pod::Spec.new do |s|
s.name = 'xxxx'
s.prepare_command = <<-CMD
sh cat.sh
CMD
end
```
## 15. `pod lib create` 报错 `Ignoring ffi-1.16.3 because its extensions are not built`
开始可能发现错误
Ignoring ffi-1.16.3 because its extensions are not built. Try: gem pristine ffi --version 1.16.3
类似这样的错误
```
sudo gem install --user-install rexml
sudo gem install --user-install xcodeproj
```

View File

@@ -1,566 +0,0 @@
# 开发效率提升利器
> 软件的生命周期贯穿产品的开发,测试,生产,用户使用,版本升级和后期维护等过程,只有易读,易维护的软件代码才具有生命力。
## 一、思路
最近重构项目组件,看到项目中存在一些命名和方法分块方面存在一些问题,结合平时经验和 [Apple官方代码规范](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.html) 在此整理出 iOS 工程规范。提出第一个版本,如果后期觉得有不完善的地方,继续提出来不断完善,文档在此记录的目的就是为了大家的代码可读性较好,后来的人或者团队里面的其他人看到代码可以不会因为代码风格和可读性上面造成较大时间的开销。
先梳理出规范,然后使用一些脚本的方式,提高大家使用的便捷性与效率,最后开发一些协作脚本。
## 二、一些原则
1. 长的,描述性的方法和变量命名是好的。不要使用简写,除非是一些大家都知道的场景比如 VIP。不要使用 bgView推荐使用 backgroundView
2. 见名知意。含义清楚,做好不加注释代码自我表述能力强。(前提是代码足够规范)
3. 不要过分追求技巧,降低代码可读性
4. 删除没必要的代码。比如我们新建一个控制器,里面会有一些不会用到的代码,或者注释起来的代码,如果这些代码不需要,那就删除它,留着偷懒吗?下次需要自己手写
5. 在方法内部不要重复计算某个值,适当的情况下可以将计算结果缓存起来
6. 尽量减少单例的使用。
7. 提供一个统一的数据管理入口,不管是 MVC、MVVM、MVP 模块内提供一个统一的数据管理入口会使得代码变得更容易管理和维护。
8. 除了 .m 文件中方法,其他的地方"{"不需要另起一行。
```Objective-c
- (void)getGooodsList
{
...
}
- (void)doHomework
{
if (self.hungry) {
return;
}
if (self.thirsty) {
return;
}
if (self.tired) {
return;
}
papapa.then.over;
}
```
```
### 变量
1. 一个变量最好只有一个作用,切勿为了节省代码行数,觉得一个变量可以做多个用途。(单一原则)
2. 方法内部如果有局部变量,那么局部变量应该靠近在使用的地方,而不是全部在顶部声明全部的局部变量。
### 运算符
1. 1元运算符和变量之间不需要空格。例如++n
2. 2元运算符与变量之间需要空格隔开。例如 containerWidth = 0.3 * Screen_Width
3. 当有多个运算符的时候需要使用括号来明确正确的顺序,可读性较好。例如: 2 << (1 + 2 * 3 - 4)
### 条件表达式
1. 当有条件过多、过长的时候需要换行,为了代码看起来整齐些
```
//good
if (condition1() &&
condition2() &&
condition3() &&
condition4()) {
// Do something
}
//bad
if (condition1() && condition2() && condition3() && condition4() { // Do something }
```
2. 在一个代码块里面有个可能的情况时善于使用 `return` 来结束异常的情况。
```
- (void)doHomework
{
if (self.hungry) {
return;
}
if (self.thirsty) {
return;
}
if (self.tired) {
return;
}
papapa.then.over;
}
```
3. 每个分支的实现都必须使用 {} 包含。
```
// bad
if (self.hungry) self.eat()
// good
if (self.hungry) {
self.eat()
}
```
4. 条件判断的时候应该是变量在左,条件在右。 if ( currentCursor == 2 ) { //... }
5. switch 语句后面的每个分支都需要用大括号括起来。
6. switch 语句后面的 default 分支必须存在,除非是在对枚举进行 switch。
```
switch (menuType) {
case menuTypeLeft: {
...
break;
}
case menuTypeRight: {
...
break;
}
case menuTypeTop: {
...
break;
}
case menuTypeBottom: {
...
break;
}
}
```
### 类名
1. 大写驼峰式命名。每个单词首字母大写。比如「申请记录控制器」ApplyRecordsViewController
2. 每个类型的命名以该类型结尾。
- ViewController使用 `ViewController` 结尾。例子ApplyRecordsViewController
- View使用 `View` 结尾。例子分界线boundaryView
- NSArray使用 `s` 结尾。比如商品分类数据源。categories
- UITableViewCell使用 `Cell` 结尾。比如 MyProfileCell
- Protocol使用 `Delegate` 或者 `Datasource` 结尾。比如 XQScanViewDelegate
- Tool工具类
- 代理类Delegate
- Service 类Service
### 类的注释
有时候我们需要为我们创建的类设置一些注释。我们可以在类的下面添加。
### 枚举
枚举的命名和类的命名相近。
```
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
UIControlContentVerticalAlignmentCenter = 0,
UIControlContentVerticalAlignmentTop = 1,
UIControlContentVerticalAlignmentBottom = 2,
UIControlContentVerticalAlignmentFill = 3,
};
```
### 宏
1. 全部大写,单词与单词之间用 `_` 连接。
2. 以 `K` 开头。后面遵循大写驼峰命名。「不带参数」
```
#define HOME_PAGE_DID_SCROLL @"com.xq.home.page.tableview.did.scroll"
#define KHomePageDidScroll @"com.xq.home.page.tableview.did.scroll"
```
### 属性
书写规则,基本上就是 `@property 之后空一格,括号,里面的 线程修饰词、内存修饰词、读写修饰词,空一格 类 对象名称`
根据不同的场景选择合适的修饰符。
```
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, assign, readonly) BOOL loading;
@property (nonatomic, weak) id<#delegate#> delegate;
@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);
```
### 单例
单例适合全局管理状态或者事件的场景。一旦创建,对象的指针保存在静态区,单例对象在堆内存中分配的内存空间只有程序销毁的时候才会释放。基于这种特点,那么我们类似 UIApplication 对象,需要全局访问唯一一个对象的情况才适合单例,或者访问频次较高的情况。我们的功能模块的生命周期肯定小于 App 的生命周期,如果多个单例对象的话,势必 App 的开销会很大,糟糕的情况系统会杀死 App。如果觉得非要用单例比较好那么注意需要在合适的场合 tearDown 掉。
单例的使用场景概括如下:
- 控制资源的使用,通过线程同步来控制资源的并发访问。
- 控制实例的产生,以达到节约资源的目的。
- 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
```objective-c
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//because has rewrited allocWithZone use NULL avoid endless loop lol.
_sharedInstance = [[super allocWithZone:NULL] init];
});
return _sharedInstance;
}
+ (id)allocWithZone:(struct _NSZone *)zone
{
return [TestNSObject sharedInstance];
}
+ (instancetype)alloc
{
return [TestNSObject sharedInstance];
}
- (id)copy
{
return self;
}
- (id)mutableCopy
{
return self;
}
- (id)copyWithZone:(struct _NSZone *)zone
{
return self;
}
```
### 私有变量
推荐以 `_` 开头,写在 .m 文件中。例如 NSString * _somePrivateVariable
### 代理方法
1. 类的实例必须作为方法的参数之一。
2. 对于一些连续的状态的,可以加一些 will将要、did已经
3. 以类的名称开头
```objective-c
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
```
### 方法
1. 方法与方法之间间隔一行
2. 大量的方法尽量要以组的形式放在一起比如生命周期函数、公有方法、私有方法、setter && getter、代理方法..
3. 方法最后面的括号需要另起一行。遵循 Apple 的规范
4. 对于其他场景的括号,括号不需要单独换行。比如 if 后面的括号。
5. 如果方法参数过多过长,建议多行书写。用冒号进行对齐。
6. 一个方法内的代码最好保持在50行以内一般经验来看如果一个方法里面的代码行数过多代码的阅读体验就很差别问为什么做过重构代码行数很长的人都有类似的心情
7. 一个函数只做一个事情,做到单一原则。所有的类、方法设计好后就可以类似搭积木一样实现一个系统。
8. 对于有返回值的函数,且函数内有分支情况。确保每个分支都有返回值。
9. 函数如果有多个参数,外部传入的参数需要检验参数的非空、数据类型的合法性,参数错误做一些措施:立即返回、断言。
10. 多个函数如果有逻辑重复的代码,建议将重复的部分抽取出来,成为独立的函数进行调用
```objective-c
- (instancetype)init
{
self = [super init];
if (self) {
<#statements#>
}
return self;
}
- (void)doHomework:(NSString *)name
period:(NSInteger)second
score:(NSInteger)score;
```
11. 方法如果有多个参数的情况下需要注意是否需要介词和连词。很多时候在不知道如何抉择测时候思考下苹果的一些 API 的方法命名。
```objective-c
//good
- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
//bad
- (instancetype)initWithAge:(NSInteger)age andName:(NSString *)name;
- (void)tableView:(UITableView *)tableView :(NSIndexPath *)indexPath;
```
12. `.m` 文件中的私有方法需要在顶部进行声明
13. 方法组之间也有个顺序问题。
- 在文件最顶部实现属性的声明、私有方法的声明(很多人省去这一步,问题不大,但是蛮多第三方的库都写了,看起来还是会很方便,建议书写)。
- 在生命周期的方法里面,比如 viewDidLoad 里面只做界面的添加,而不是做界面的初始化,所有的 view 初始化建议放在 getter 里面去做。往往 view 的初始化的代码长度会比较长、且一般会有多个 view 所以 getter 和 setter 一般建议放在最下面,这样子顶部就可以很清楚的看到代码的主要逻辑。
- 所有button、gestureRecognizer 的响应事件都放在这个区域里面,不要到处乱放。
文件基本上就是
```objective-c
//___FILEHEADER___
#import "___FILEBASENAME___.h"
/*ViewController*/
/*View&&Util*/
/*model*/
/*NetWork InterFace*/
/*Vender*/
@interface ___FILEBASENAMEASIDENTIFIER___ ()
@end
@implementation ___FILEBASENAMEASIDENTIFIER___
#pragma mark - life cycle
- (void)viewWillAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = <#value#>;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidAppear:animated];
}
#ifdef DEBUG
- (void)dealloc
{
NSLog(@"%s",__func__);
}
#endif
#pragma mark - public Method
#pragma mark - private method
#pragma mark - event response
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource
//...(多个代理方法依次往下写)
#pragma mark - getters and setters
@end
```
### 图片资源
1. 单个文件的命名
文件资源的命名也需要一定的规范形式为功能模块名_类别_功能_状态@nx.png
Setting_Button_search_selected@2x.png、Setting_Button_search_selected@3x.png
Setting_Button_search_unselected@2x.png、Setting_Button_search_unselected@3x.png
2. 资源的文件夹命名
最好也参考 App 按照功能模块建立对应的实体文件夹目录,最后到对应的目录下添加相应的资源文件。
### 注释
1. 对于类的注释写在当前类文件的顶部
2. 对于属性的注释需要写在属性后面的地方。 //**<userId*/
3. 对于 .h 文件中方法的注释,一律按快捷键 `command+option+/`。三个快捷键解决。按需在旁边对方法进行说明解释、返回值、参数的说明和解释
4. 对于 .m 文件中的方法的注释,在方法的旁边添加 `//`。
5. 注释符和注释内容需要间隔一个空格。 例如: // fetch goods list
### 版本规范
采用 A.B.C 三位数字命名比如1.0.2,当有更新的情况下按照下面的依据
| 版本号 | 右说明对齐标题 | 示例 |
|:---------:|:----------:|:--------------:|
| **A**.b.c | 属于重大内容的更新 | 1.0.2 -> 2.0.0 |
| a.**B**.c | 属于小部分内容的更新 | 1.0.2 -> 1.1.1 |
| a.b.**C** | 属于补丁更新 | 1.0.2 -> 1.0.3 |
### 改进
我们知道了平时在使用 Xcode 开发的过程中使用的系统提供的代码块所在的地址和新建控制器、模型、view等的文件模版的存放文件夹地址后我们就可以设想下我们是否可以定制自己团队风格的控制器模版、是否可以打造和维护自己团队的高频使用的代码块
答案是可以的。
Xcode 代码块的存放地址:`~/Library/Developer/Xcode/UserData/CodeSnippets`
Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/
### 意义
1. 为了个人或者团队开发者的代码更加规范。Property的书写的时候的空格、线程修饰词、内存修饰词的先后顺序
2. 提供大量可用的代码块,提高开发效率。比如在 Xcode 里面敲 UITableView_init 便可以自动懒加载创建一个 UITabelView 对象,你只需要设置在指定的位置写相应的参数
3. 通过一些代码块提高代码规范、避免一些bug。比如曾看到过 block 属性用 strong 修饰的代码,造成内存泄漏。举个例子你在 Xcode 中输入 **Property_delegate** 就会出来 `@property (nonatomic, weak) id<<#delegate#>> delegate;`,你输入 **Property_block** 就会出来 `@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);`
## 三、 代码块的改造
我们可以将属性、控制器生命周期方法、单例构造一个对象的方法、代理方法、block、GCD、UITableView 懒加载、UITableViewCell 注册、UITableView 代理方法的实现、UICollectionVIew 懒加载、UICollectionVIewCell 注册、UICollectionView 的代理方法实现等等组织为 codesnippets
### 思考
- 封装好 codesnippets 之后团队除了你编写这个项目的人如何使用?如何知道是否有这个代码块?
方案:先在团队内召开代码规范会议,大家都统一知道这个事情在。之后大家共同维护 codesnippets。用法见下
属性:通过 **Property_类型** 开头,回车键自动补全。比如 Strong 类型,编写代码通过 Property_Strong 回车键自动补全成如下格式
```objective-c
@property (nonatomic, strong) <#Class#> *<#object#>;
```
方法:以 **Method_关键词** 回车键确认,自动补全。比如 Method_UIScrollViewDelegate 回车键自动补全成 如下格式
```objective-c
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
}
```
各种常见的 Mark以 **Mark_关键词** 回车确认,自动补全。比如 Method_MethodsGroup 回车键自动补全成 如下格式
```objective-c
#pragma mark - life cycle
#pragma mark - public Method
#pragma mark - private method
#pragma mark - event response
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource
#pragma mark - getters and setters
```
- 封装好 codesnippets 之后团队内如何统一?想到一个方案,可以将团队内的 codesnippets 共享到 git团队内的其他成员再从云端拉取同步。这样的话团队内的每个成员都可以使用最新的 codesnippets 来编码。
编写 shell 脚本。几个关键步骤:
1. 给系统文件夹授权
2. 在脚本所在文件夹新建存放代码块的文件夹
3. 将系统文件夹下面的代码块复制到步骤2创建的文件夹下面
4. 将当前的所有文件提交到 Git 仓库
## 四、文件模版的改造
我们观察系统文件模版的特点,和在 Xcode 新建文件模版对应。
![Xcode file template存放地址](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190304_filetemplates.png)
所以我们新建 Custom 文件夹,将系统 Source 文件夹下面的 Cocoa Touch Class.xctemplate 复制到 Custom 文件夹下。重命名为我们需要的名字我这里以“Power”为例
![自定义文件模版示例](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190319-filetemplateSelf.png)
进入 PowerViewController.xctemplate/PowerViewControllerObjective-C
修改 `___FILEBASENAME___.h` 和 `___FILEBASENAME___.m` 文件内容
![注意点1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190304-fileTmplates3.png)
在替换 .h 文件内容的时候后面改为 UIViewController不然其他开发者新建文件模版的时候出现的不是 UIViewController 而是我们的 PowerViewController
![.m文件内容](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190304_filetemplates4.png)
修改 TemplateInfo.plist
![plist注意点](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190319-filetemplate5.png)
思考:
- 如何使用
商量好一个标识“Power”。比如我新建了单例、控制器、Model、UIView、UITableViewCell、UICollectionViewCell6个模版都以为 Power 开头。
![模版用法](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190319-filetemplate6.png)
- 如何共享
以 shell 脚本为工具。使用脚本将 git 云端的代码模版同步到本地 Xcode 文件夹对应的位置就可以使用了。关键步骤:
1. git clone 代码到脚本所在文件夹
2. 进入存放 codesnippets 的文件夹将内容复制到系统存放 codesnippets 的地方
3. 进入存放 file template 的文件夹将内容复制到系统存放 file template 的地方
## 六、使用
### 1. Xcode 中开发
- Property 属性。敲 **Property_** 自动联想,光标移动选中后敲回车自动补全
- Mark 标识。 敲 **Mark_** 自动联想,会展示各种常用的 Mark光标移动选中后敲回车自动补全
- Method 方法。敲 **Method_** 自动联想,会展示各种常用的 Method光标移动选中后敲回车自动补全
- GCD。敲 **GCD_** 自动联想,会展示各种常用的 GCD光标移动选中后敲回车自动补全
- 常用 UI 控件的懒加载。敲 **_init** 自动联想,展示常用的 UI 控件的懒加载,光标移动选中后敲回车自动补全
- Delegate。敲 **Delegate_** 自动联想,会展示各种常用的 Delegate光标移动选中后敲回车自动补全
- Notification。敲 **NSNotification_** 自动联想,会展示各种常用的 NSNotification 的代码块,比如发送通知、添加观察者、移除观察者、观察者方法的实现等等,光标移动选中后敲回车自动补全
- Protocol。敲 **Protocol_** 自动联想,会展示各种常用的 Protocol 的代码块,光标移动选中后敲回车自动补全
- 内存修饰代码块
- 工程常用 TODO、FIXME、Mark。敲 **Mark_** 自动联想,会展示各种常用的 Mark 的代码块,光标移动选中后敲回车自动补全
- 内存修饰代码块。敲 **Memory_** 自动联想,会展示各种常用的内存修饰的代码块,光标移动选中后敲回车自动补全
- 一些常用的代码块。敲 **Thread_** 等自动联想,选中后敲回车自动补全。
### 2. Code Snippet 同步
你可能是代码块的创建者,也可能是使用方,使用的时候直接先给脚本赋权
```
chmod +x ./syncSnippets.sh // 为脚本设置可执行权限
./syncSnippets.sh // 同步git云端代码块和文件模版到本地
```
如果自己有新的代码块创建,觉得不错,想同步到远端 github repo则可以使用下面的命令
```shell
chmod +x ./uploadMySnippets.sh // 为脚本设置可执行权限
./uploadMySnippets.sh //将本地的代码块和文件模版同步到云端
```
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-1008-CodeSnippetMethodGroup.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-1008-CodeSnippetProperty.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-1008-CodeSnippetLazyLoad.png)
## PS
**不断完善中。大家有好用或者不错的代码块或者文件模版希望参与到这个项目中来,为我们开发效率的提升添砖加瓦、贡献力量**
目前新建了大概58个代码段和6个类文件模版UIViewController控制器带有方法组、模型、线程安全的单例模版、带有布局方法的UIView模版、UITableViewCell、UICollectionViewCell模版
shell 脚本基本有每个函数和关键步骤的代码注释,想学习 shell 的人可以看看代码。[代码传送门](https://github.com/FantasticLBP/codesnippets)

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