docs: 批量博文

This commit is contained in:
杭城小刘
2020-02-25 17:46:51 +08:00
parent 8e5d2c9e7f
commit 6e99436a9e
373 changed files with 18071 additions and 1116 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,5 +1,4 @@
### 工程大小优化之图片资源
# 工程大小优化之图片资源
> 一点点iOS项目本身功能较多导致应用体积也比较大。一个Xcode工程下图片资源占用了很大的空间且如果有些App需要一键换肤功能呵呵不知道得做多少图片。每套图片还需要设置1x@,2x@,3x@等
@@ -31,13 +30,13 @@ Web领域使用IconFont类似的技术已经多年当我在15年接触BootStr
1. 首先选取一些有丰富资源的网站我使用阿里的IconFont多年其他的没去研究所以此处直接使用阿里的产品。地址[http://www.iconfont.cn/plus](http://www.iconfont.cn/plus)
2. 打开网站在线挑选好合适的图标加入购物车,如图
![](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.43.33.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-28-iconfontPickUp.png)
3. 选择好之后在购物车查看,然后点击下载代码
1. 选择好之后在购物车查看,然后点击下载代码
4. 打开下载好的文件其机构如下我们在iOS项目开发过程中使用unicode的形式使用IconFont,所以打开demo\_unicode.html
2. 打开下载好的文件其机构如下我们在iOS项目开发过程中使用unicode的形式使用IconFont,所以打开demo\_unicode.html
![下载文件目录结构](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.43.48.png)
![下载文件目录结构](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 个字符
@@ -49,9 +48,9 @@ Web领域使用IconFont类似的技术已经多年当我在15年接触BootStr
1. 首先看看如何简单实用IconFont
2. 首先将下载好的文件夹中的 **iconfont.ttf** 加入到Xcode工程中确保加入成功在Build检查
![Xcode检查引入结果](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.51.36.png)
![Xcode检查引入结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-28-iconfintWorkSetting.png)
3. 怎么用?
1. 怎么用?
```Objective-c
@@ -80,7 +79,7 @@ pragma mark - getter and setter
```Objective-c
self.latestImageView.image = [UIImage iconWithInfo:LBPIconFontmake(@"\U0000e6ac", 60, @"000066") ];
```
![封装后的工程目录结构](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.56.00.png)
![封装后的工程目录结构](./../assets/2017-05-28-iconfont.png)
1. LBPFontInfo来封装字体信息
2. UIColor+picker根据十六进制字符串来设置颜色

View File

@@ -1,5 +1,4 @@
## UIWebView加载网页内容
# UIWebView加载网页内容
可以通过本地文件、url等方式。
@@ -127,6 +126,79 @@ setTimeout(function(){
```
## 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脚本的结果。本质上是一个同步调用

View File

@@ -1,4 +1,4 @@
# iOS中的事件

View File

@@ -77,7 +77,7 @@ NSString *filepath1 = @"/Users/geek/Desktop/data.plist";
```
* 获取文件信息
![文件信息](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-07-02%20下午5.58.38.png)
![文件信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-07-02%20下午5.58.38.png)
```
NSError *error = nil;
@@ -124,8 +124,8 @@ NSError *erroe = nil;
* 在指定目录创建文件夹参数说明withIntermediateDirectories后的参数为Bool代表。YES一路创建NO不会做一路创建
![正常创建文件夹成功](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-07-02%20下午7.02.53.png)
![创建文件夹失败](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-07-02%20下午7.07.55.png)
![正常创建文件夹成功](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如果文件夹不存在则停止创建文件

View File

@@ -1,6 +1,4 @@
### UINavagationController重写push和pop方法
# UINavagationController重写push和pop方法
> 有个需求就是在App的Tab的首页需要显示浮动着的交互动画的机器人该机器人具有机器学习的特点因此可以不断的与用户交互怎么样实现只浮动在App的5个tab首页当点击跳转不是首页的时候不需要显示

View File

@@ -1,5 +1,4 @@
###自定义URL Schemes###
# 自定义URL Schemes
1、引言

View File

@@ -1,5 +1,5 @@
###URL Schemes 的发展###
# URL Schemes 的发展
@@ -13,7 +13,7 @@ URL Schemes 的发展过程可以说就是 iOS 效率工具类 App 的发展过
###基本 URL Schemes###
## 基本 URL Schemes
基本 URL Schemes 的能力虽然简单有限,但使用情境却是最普遍的。
@@ -44,7 +44,7 @@ URL Schemes 的发展过程可以说就是 iOS 效率工具类 App 的发展过
<li>在 CFBundleURLSchemes 下的那两行就是该 App 的基本 URL Schemes 了。</li>
</ul>
###复杂 URL Schemes###
## 复杂 URL Schemes
参考链接:[URL Scheme](https://sspai.com/post/31500#fnref:2)

View File

@@ -1,4 +1,4 @@
### Swift、OC混编
# Swift、OC混编
```
1、在oc文件中使用swift文件。

View File

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

View File

@@ -1,4 +1,4 @@
### 简单的 Model 与 JSON 相互转换
# 简单的 Model 与 JSON 相互转换
```
// JSON:

View File

@@ -1,12 +1,12 @@
## 实现原理 {#实现原理}
# 实现波浪动画
波浪的形状绘制在 CAShapeLayer 上。通过 CADisplayLink 与屏幕刷新频率同步,每次刷新都绘制新的波浪,并改变小船的位置和角度。另外,水和天空的颜色是渐变的,由 CAGradientLayer 实现,其中,显示水的 CAGradientLayer 需要有波浪形状的 CAShapeLayer 的遮罩\(mask\)。
### CAShapeLayer {#cashapelayer}
### CAShapeLayer
CAShapeLayer 的属性 path \(CGPath\)就是图层要显示的形状。把波浪的形状绘制出来,赋值给此属性即可。
### CADisplayLink {#cadisplaylink}
### CADisplayLink
创建 CADisplayLink相应的 target 实现屏幕刷新时要调用的方法。把 CADisplayLink 加入 RunLoop 中。通过 isPaused 属性控制 CADisplayLink 是否暂停\(target 是否调用方法\)
@@ -18,7 +18,7 @@ waveLink?.add(to: .current, forMode: .defaultRunLoopMode)
### 绘制波浪 {#绘制波浪}
### 绘制波浪
波浪的形状关键是正弦函数曲线
@@ -32,7 +32,7 @@ y = A*sin(x+B)
为了使波浪高度逐渐变化,用一个属性表示参数 A然后每次绘制后更新此属性加上一个固定的数直到波浪高度达到目标值。
### 小船的位置和旋转角度 {#小船的位置和旋转角度}
### 小船的位置和旋转角度
已知小船 x 轴坐标,通过正弦函数可以直接计算出小船的 y 轴坐标。此外,小船需要随着波浪旋转,旋转至船底与波浪表面相切。这就要对正弦函数进行求导
@@ -48,7 +48,7 @@ angle = atan(y')
用以上旋转角度,改变小船视图\(UIView\)的 transform调用 CGAffineTransformRotate 方法,实现小船的旋转。
### CAGradientLayer {#cagradientlayer}
### CAGradientLayer
CAGradientLayer 默认的颜色渐变方向是由上至下。给 colors 属性赋值一个包含 CGColor 的数组,则图层颜色由上至下,从数组第一个值经中间值渐变至最后一个值。

View File

@@ -1,40 +1,29 @@
# 看透构造方法
# 构造方法
* new方法的内部就是先调用alloc方法再调用init方法
* alloc方法那个类接受alloc消息那么该方法返回该接受类的对象并把对象返回
* init方法是1个对象方法作用初始化对象
* 创建对象的步骤先使用alloc创建1个对象再使用init初始化这个对象才可以使用这个对象
## 构造方法
* new 方法的内部就是先调用 alloc 方法,再调用 init 方法
* alloc 方法:那个类接受 alloc 消息,那么该方法返回该接受类的对象,并把对象返回
* init 方法是1个对象方法作用初始化对象
* 创建对象的步骤:先使用 alloc 创建1个对象再使用 init 初始化这个对象,才可以使用这个对象
* 使用1个未被初始化的对象是很危险的
* init 方法:作用:初始化对象,为对象赋初始值,叫做构造方法
* init方法作用初始化对象为对象赋初始值叫做构造方法
# 重写init构造方法
* 如果想创建出来的对象的属性值不是默认的初始化值,则需要重写init方法
* 重写init方法的规范
* 必须要先调用父类的init方法因为当前类初始化就是通过\`\[父类init\]\`去初始化然后将返回值赋值给self
* 调用init方法有可能会失败如果失败直接返回nil
* 判断父类是否初始化成功。如果self != nil则代表初始化成功
## 重写init构造方法
* 如果想创建出来的对象的属性值不是默认的初始化值,则需要重写 init 方法
* 重写 init 方法的规范:
* 必须要先调用父类的 init 方法(因为当前类初始化就是通过\`\[父类init\]\`去初始化然后将返回值赋值给self
* 调用 init 方法有可能会失败如果失败直接返回nil
* 判断父类是否初始化成功。如果 `self != nil`,则代表初始化成功
* 如果初始化成功就去初始化当前对象的属性
* 最后返回self
* 最后返回 self
#### 解惑:
1. 为什么要调用父类的init方法
1. 当前类有isa指针当前类的isa指针赋值是通过父类的init方法赋值的。
1. 为什么要调用父类的 init 方法?
1. 当前类有 isa 指针,当前类的 isa 指针赋值是通过父类的 init 方法赋值的。
2. 需要保证当前对象的父类属性同时被初始化
2. 重写init方法的规范
2. 重写 init 方法的规范:
```
-(instancetype)init{
@@ -82,7 +71,7 @@
Person *p2 = [Person new]; //p2.name = "杭城小刘"p2.age =22;
```
如果2个类的关系为组合关系且它的一个属性是另一个类的对象那么当该类初始化的时候默认它的属性为nil那么如何初始化
如果2个类的关系为组合关系且它的一个属性是另一个类的对象那么当该类初始化的时候默认它的属性为 nil那么如何初始化
```
-(instancetype)init{
@@ -99,7 +88,7 @@
Person *p1 = [[Person alloc] init]; //p1.dog != nil
```
# 自定义构造方法
## 自定义构造方法
* 现状:虽然每次双肩的对象的属性值不是默认的,但是每次初始化的对象的值都是一样的。
@@ -109,13 +98,13 @@
* 自定义构造方法规范:
* 自定义构造方法的返回值为instancetype
* 自定义构造方法的返回值为 instancetype
* 方法的命名必须以initWith开头
* 方法的命名必须以 initWith 开头
* 方法的实现类似init的实现
* 方法的实现类似 init 的实现
**注意此时不能使用new来调用。因为new的实现是先allocinit默认init的实现是给属性赋默认值**
**注意:此时不能使用 new 来调用。(因为 new 的实现是先 allocinit ,默认 init 的实现是给属性赋默认值)**
```
-(instancetype)initWithName:(NSString *)name andAge:(int)age{
@@ -170,14 +159,14 @@ Person *p3 = [[Person alloc] initWithName:@"杭城小刘2号" andAge:23];
```
![init](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-23-5-56-53.png)
![init](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-23-5-57-08.png)
![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 开头”做个实验
![initwith](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-23-6-01-29.png)
![initwith](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-23-6-01-29.png)
报错信息很明显不能在构造方法之外给self赋值
报错信息很明显:不能在构造方法之外给 self 赋值
因为编译器认为只有以initWith开头的方法是构造方法
因为,编译器认为只有以 initWith 开头的方法是构造方法

View File

@@ -80,7 +80,7 @@ gcc index.c
```
clang -rewrite-objc index.c
```
![clang结果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180516-235614@2x.png)
![clang结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180516-235614@2x.png)
###4、Block 经常造成循环引用

View File

@@ -1,6 +1,6 @@
#禅与 Objective-C 编程艺术
# 禅与 Objective-C 编程艺术
#### 警告和错误
## 警告和错误
* 警告
```

View File

@@ -1,4 +1,4 @@
## UIScrollView 拖拽滑动时收起键盘
# UIScrollView 拖拽滑动时收起键盘
> 当一个页面的 UIScrollView/UITableView 上有输入框时,为了较好的体验,就是当滑动的时候需要回收键盘

View File

@@ -6,7 +6,7 @@
>
> 不不不,今天要带出来的主题是 **CAReplicatorLayer**
![音量柱动画效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmW9ACfS9P5orau43H7gxuxsU4RVMDPD7mPnDKq4pgLmzr.gif)
![音量柱动画效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmW9ACfS9P5orau43H7gxuxsU4RVMDPD7mPnDKq4pgLmzr.gif)
@@ -76,7 +76,7 @@
## 例子1
![倒影效果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmQrU8UxSytnKbWcDVpY5mdy6kmiSHpzyqwt8GykWKNEY2.png)
![倒影效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmQrU8UxSytnKbWcDVpY5mdy6kmiSHpzyqwt8GykWKNEY2.png)
这里比较简单了,关键代码
@@ -104,7 +104,7 @@
## 例子2
![复制层动画综合应用](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/QQ20180610-235637-HD.gif)
![复制层动画综合应用](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180610-235637-HD.gif)
需求分析:

View File

@@ -8,7 +8,7 @@
- 效果图
![QQ粘性动画](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmUhGFJgxj6ofpvZp6MK3bqaH2hLgq9vfKsnwDmMisahGu.gif)
![QQ粘性动画](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmUhGFJgxj6ofpvZp6MK3bqaH2hLgq9vfKsnwDmMisahGu.gif)
- 关键技术点剖析
@@ -16,7 +16,7 @@
- 分析 QQ 粘性动画的关键点就是当手势拖动时候2个圆之间那个形状怎么绘制
答案将2个圆的某一时刻之间形成的形状用数学抽象来计算。
![轨迹分解](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmQUyUSLYB3VGs4juzfsEdncyWetz7BTN2GFtURbmEYbEY.png)
![轨迹分解](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QmQUyUSLYB3VGs4juzfsEdncyWetz7BTN2GFtURbmEYbEY.png)
- 拖动到超过某个范围的时候怎么执行爆炸动画

View File

@@ -55,7 +55,7 @@ self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@
# 效果图
![发微博动画效果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180610-225937-HD.gif)
![发微博动画效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180610-225937-HD.gif)

View File

@@ -1,4 +1,4 @@
JavascriptCore
# JavascriptCore

View File

@@ -24,7 +24,7 @@
```
# 控制器加载view的流程
![控制器加载view的流程](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2287777-b6128646373dfffb.png)
![控制器加载view的流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2287777-b6128646373dfffb.png)
* 控制器的init方法底层会调用initWithNibName方法
@@ -94,6 +94,6 @@ why在AppDelegate中vc.view.backgroundColor就是调用vc的view的getter方
#### 来一个官方解释
![Apple 文档](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2287777-8ff7c3b976ffb29a.png)
![Apple 文档](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2287777-8ff7c3b976ffb29a.png)

83
Chapter1 - iOS/1.30.md Normal file
View File

@@ -0,0 +1,83 @@
# 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

View File

@@ -46,7 +46,7 @@ storeVC.delegate = self;
4. 注意时机哦
我们的目的是能得到用户的正反馈如果在用户刚使用APP时就弹出评分框可能会给某些用户带来反感因此选择一个合适的时机弹出评分很重要不然适得其反。
今天在使用爱奇艺的时候发现他们的弹出场景是这样的。我因为要出门所以下载了一部电影。在会员模式下高速缓存成功后(我很满意)弹出评分按钮。
![爱奇艺评分](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/61530453779_.pic.jpg)
![爱奇艺评分](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/61530453779_.pic.jpg)

View File

@@ -1,4 +1,4 @@
## 一些布局小知识
# 一些布局小知识
1. LaunchScreen 会根据设备大小设置屏幕的显示范围LaunchImage 则根据提供的启动图片设置App的可见范围
2. UITextView 可以设置显示范围
@@ -130,7 +130,7 @@
从 iconfont 网站上面随便选择1个彩色 icon 用来做对比实验
![iconfont小图标](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135721.png?raw=true)
![iconfont小图标](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180719-135721.png)
- 实验1
@@ -138,7 +138,7 @@
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAutomatic]];
![UIImageRenderingModeAutomatic模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135617@2x.png?raw=true)
![UIImageRenderingModeAutomatic模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180719-135617@2x.png)
- 实验2
@@ -146,18 +146,18 @@
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];
![UIImageRenderingModeAlwaysOriginal模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135552@2x.png?raw=true)
![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://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135552@2x.png?raw=true)
![UIImageRenderingModeAlwaysTemplate模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180719-135552@2x.png)
结论:对于 UIImage 来说如果不指定渲染模式的话则默认使用**UIImageRenderingModeAutomatic**,则会根据渲染的环境和上下文进行渲染。如果指定了模式,则根据具体的模式开启渲染。**UIImageRenderingModeAlwaysOriginal**则绘制图片的原始信息,不使用**tintColor**。**UIImageRenderingModeAlwaysTemplate**则始终根据**tintColor**绘制图片,忽略图片本身的信息。
<hr>
![引用自网络的图片](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/4673_140117110629_1.png?raw=true)
![引用自网络的图片](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4673_140117110629_1.png)

View File

@@ -1,4 +1,4 @@
### 数组、集合、字典与 hash、isEqual 方法的关联
# 数组、集合、字典与 hash、isEqual 方法的关联
1. NSArray 允许重复添加元素添加元素的时候不查重所以不会调用上面2个方法。在移出元素的时候会依次遍历数组内的元素每个元素调用 **isEqual** 方法remove 方法传入的元素作为参数),所有返回真值的元素都会被移除。在字典中不涉及 hash 方法。

View File

@@ -1,4 +1,8 @@
## RunLoop 对象
# RunLoop 对象
先附上一张总结的非常棒的RunLoop图
![RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-RunLoop-review.png)
iOS 中有2套 API 可以访问和使用 RunLoop。分别是
@@ -43,7 +47,7 @@ NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的
- kCFRunLoopDefaultModeApp 的默认 Mode通常主线程是在这个 Mode 下运行
- UITrackingRunLoopMode界面跟踪 Mode用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode启动完成后就不再使用
- UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的Mode不是一种真正的Mode
@@ -253,7 +257,7 @@ NSTimer 会受 NSRunLoopMode 影响GCD 的 timer 则不会。
*/
```
![触摸屏幕事件在 RunLoop 下的 source0](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180801-104553@2x.png?raw=true)
![触摸屏幕事件在 RunLoop 下的 source0](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180801-104553@2x.png)
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟

View File

@@ -1,96 +1,95 @@
## 监听 RunLoop
# 监听 RunLoop
```Objective-C
//给 RunLoop 添加监听者
- (void)testRunLoopObserver{
//给 RunLoop 添加监听者
- (void)testRunLoopObserver{
//创建监听者
// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
/*
创建监听对象
参数1:分配内存空间
参数2:要监听的状态 kCFRunLoopAllActivities :所有状态
参数3:是否要持续监听
参数4:优先级
参数5:回调
*/
CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop 闪亮登场");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop 大哥要处理 Timer 了");
break;
case kCFRunLoopBeforeSources:
//Source 有2种。Source0:非基于 port 的用户主动触发的事件。Source1:基于 port通过内核和其它线程互相发送消息
NSLog(@"RunLoop 大哥要处理 Source 了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop 大哥没事干要睡觉了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"");
NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop 大哥要退出离开了");
break;
default:
break;
}
});
/*
参数1:要监听哪个RunLoop
参数2:监听者
参数3:要监听 RunLoop 在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode);
//CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release
CFRelease(oberver);
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES];
}
//等到 RunLoop 休眠后5秒钟叫醒 RunLoop
- (void)wakeupRunLoop{
NSLog(@"%s",__func__);
}
//创建监听者
// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
/*
2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974]
2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974]
2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974]
2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974]
2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
*/
创建监听对象
参数1:分配内存空间
参数2:要监听的状态 kCFRunLoopAllActivities :所有状态
参数3:是否要持续监听
参数4:优先级
参数5:回调
*/
CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop 闪亮登场");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop 大哥要处理 Timer 了");
break;
case kCFRunLoopBeforeSources:
//Source 有2种。Source0:非基于 port 的用户主动触发的事件。Source1:基于 port通过内核和其它线程互相发送消息
NSLog(@"RunLoop 大哥要处理 Source 了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop 大哥没事干要睡觉了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"");
NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop 大哥要退出离开了");
break;
default:
break;
}
});
/*
参数1:要监听哪个RunLoop
参数2:监听者
参数3:要监听 RunLoop 在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode);
//CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release
CFRelease(oberver);
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES];
}
![RunLoop 运行原理图1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180801-104553@2x.png)
//等到 RunLoop 休眠后5秒钟叫醒 RunLoop
- (void)wakeupRunLoop{
NSLog(@"%s",__func__);
}
/*
2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974]
2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974]
2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974]
2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974]
2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
```
![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180801-104553@2x.png)
@@ -170,7 +169,7 @@
## RunLoop 内部运行原理
![RunLoop 运行原理图1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/image-20180801113342611.png)
![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/image-20180801113342611.png)
- 图上左上角的 Input source 是早期 RunLoop 的分法现在分法为Source0 和 Source1。
- Source0:非基于 port 的,用户主动触发的事件。
@@ -180,14 +179,14 @@
运行流程图
![运行流程](/assets/4.png)
![运行流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4.png)
运行流程说明
![流程说明](/assets/3.png)
![流程说明](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/3.png)
## RunLoopMode 的概念
![RunLoop 运行原理图1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/1785352-087fd4b664e0e387.png)
![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1785352-087fd4b664e0e387.png)

View File

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

View File

@@ -16,7 +16,7 @@
发现好资料就整理到这里_随时更新最后一次更新2018年8月6日_
# WWDC {#wwdc}
# WWDC
1. Optimizing App Startup Time
@@ -46,7 +46,7 @@
首页当然也有大量的图片了解Core Image[https://developer.apple.com/videos/play/wwdc2018/719/](https://developer.apple.com/videos/play/wwdc2018/719/)
# 文章 {#文章}
# 文章
**以下文章仅仅是收集,各家之谈,不要全信,也不要反对,各有道理,学习思路即可。**
@@ -98,7 +98,7 @@
[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
@@ -116,19 +116,19 @@
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
@@ -142,7 +142,7 @@
有中文翻译版。
# 总结 {#总结}
# 总结
上面的文章我都看过,或者至少是正在看,总结下来,辅助大家优化启动性能。

View File

@@ -18,7 +18,7 @@
原理:抓包工具工作原理见[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第四部分%20开发杂谈/4.10.md)
![App-Server](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/App-Server.png)
![App-Server](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/App-Server.png)
**验证证书的真伪**其实一般来说这个过程应该是安全的,因为一般的证书都是由操作系统来管理。所以只要操作系统没有证书链验证等方面的 bug 是没有什么问题的,但是为了抓包其实我们是在操作系统中导入了中间人的 CA这样中间人下发的公钥证书就可以被认为是合法的可以通过验证的中间人既承担了颁发证书又承担了验证证书通过验证

16
Chapter1 - iOS/1.43.md Normal file
View File

@@ -0,0 +1,16 @@
# 调试方面的骚操作
1. 在日常开发中我们经常会封装某个功能模块然后暴露某个方法给外部。但是很多时候调用我们封装功能的人可能会不按照约定的方法传递参数。所以我们会使用断言。但是在线上的时候如果使用了断言,那么程序肯定会 **Crash** Xcode 提供了一个小功能可以解决这个问题。
`NS_BLOCK_ASSERTIONS ` 表明在 Release 状态下过滤 NSAssert只需要这一个条件就可以过滤掉 NSAssert。
方法:在 “Build Settings” 下搜索 **Preprocessor Macros** ,然后在 Release 下面添加 NS_BLOCK_ASSERTIONS
![](/Users/liubinpeng/Desktop/Github/knowledge-kit/assets/WX20180830-100631@2x.png)
### BreakPoint
#### 分类
Breakpoint 分为 Normal Breakpoint、Exception Breakpoint、OpenGL ES Error Breakpoint、Symbolic Breakpoint、Test Failure breakpoint、WatchPoint。可以按照具体的情景使用不同类型的 Breakpoint ,解决问题为根本

767
Chapter1 - iOS/1.44..md Normal file
View File

@@ -0,0 +1,767 @@
# Hybrid 设计与实现
随着移动浪潮的兴起,各种 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 架构,站在大前端的视觉,我觉得需要考虑以下核心设计问题。
### 交互设计
Hybrid 架构设计的第一要考虑的问题就是如何设计前端与 Native 的交互,如果这块设计不好会对后续的开发、前端框架的维护造成深远影响。并且这种影响是不可逆、积重难返。所以前期需要前端与 Native 好好配合、提供通用的接口。比如
1. Native UI 组件、Header 组件、消息类组件
2. 通讯录、系统、设备信息读取接口
3. H5 与 Native 的互相跳转。比如 H5 如何跳转到一个 Native 页面H5 如何新开 Webview 并做动画跳转到另一个 H5 页面
### 账号信息设计
账号系统是重要且无法避免的Native 需要设计良好安全的身份验证机制,保证这块对业务开发者足够透明,打通账户体系
### Hybrid 开发调试
功能设计、编码完并不是真正结束Native 与前端需要商量出一套可开发调试的模型,不然很多业务开发的工作难以继续。
[iOS调试技巧](https://www.jianshu.com/p/f430caa81fa8)
Android 调试技巧:
1. App 中开启 Webview 调试(WebView.setWebContentsDebuggingEnabled(true); )
2. chrome 浏览器输入 chrome://inspect/#devices 访问可以调试的 webview 列表
3. 需要翻墙的环境
![结构](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)
### 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 的能力
### 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)
### 格式约定
交互的第一步是设计数据格式。这里分为请求数据格式与响应数据格式,参考 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。
简易版本代码实现。
```
//通用的 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为例
```
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 一定会由应用场景。
### 跳转
跳转是 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 细节。
## 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 是不允许重复的):
```
//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 左边一般来说只有一个按钮,所以其对象可以使用这种形式:
```
this.header.set({
back: function () { },
title: ''
});
//语法糖=>
this.header.set({
left: [{
tagname: 'back',
callback: function(){}
}],
title: '',
});
```
为完成 Native 端的实现,这里会新增两个接口,向 Native 注册事件,以及注销事件:
```
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 组件实现:
```
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();
}
});
});
```
## 请求类
虽然 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 还是浏览器发出。
```
HybridGet = function (url, param, callback) {
};
HybridPost = function (url, param, callback) {
};
```
真实的业务场景会将之封装到数据请求模块在底层做适配在H5站点下使用ajax请求在Native内嵌时使用代理发出与Native的约定为
```
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
```
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
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看起来像是一个协议但其实这是一个类而且必须使用该类的子类并且需要被注册。                                       

View File

@@ -45,7 +45,7 @@
当前的 VC 和 定时器互相引用,造成循环引用。
能在何时的时打破循环引用就不会有问题了
能在合适的时打破循环引用就不会有问题了
1. 控制器不再强引用定时器
2. 定时器不再保留当前的控制器
@@ -54,8 +54,8 @@
//.h文件
#import <Foundation/Foundation.h>
@interface NSTimer (SGLUnRetain)
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
@interface NSTimer (UnRetain)
+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
repeats:(BOOL)repeats
block:(void(^)(NSTimer *timer))block;
@end
@@ -65,12 +65,12 @@
@implementation NSTimer (SGLUnRetain)
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats];
return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(lbp_blcokInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)sgl_blcokInvoke:(NSTimer *)timer {
+ (void)lbp_blcokInvoke:(NSTimer *)timer {
void (^block)(NSTimer *timer) = timer.userInfo;
@@ -83,7 +83,7 @@
//控制器.m
#import "ViewController.h"
#import "NSTimer+SGLUnRetain.h"
#import "NSTimer+UnRetain.h"
//定义了一个__weak的self_weak_变量
#define weakifySelf \
@@ -105,7 +105,7 @@ __strong __typeof(&*weakSelf)self = weakSelf;
__block NSInteger i = 0;
weakifySelf
self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
self.timer = [NSTimer lbp_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
strongifySelf
[self p_doSomething];
NSLog(@"----------------");

259
Chapter1 - iOS/1.46.md Normal file
View File

@@ -0,0 +1,259 @@
# KVC && KVO
## 基本用法
### 字典快速赋值
KVC 可以将字典里面和 model 同名的 property 进行快速赋值 **setValuesForKeysWithDictionary**
```objective-c
//前提model 中的各个 property 必须和 NSDictionary 中的属性一致
- (instancetype)initWithDic:(NSDictionary *)dic{
BannerModel *model = [BannerModel new];
[model setValuesForKeysWithDictionary:dic];
return model;
}
```
但是这里会有2种特殊情况。
- 情况一:在 model 里面有 property 但是在 NSDictionary 里面没有这个值
运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null
- 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值
运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分
```
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
```
- 情况三:如果 Dictionary 和 Model 中的 property 不同名
我们照样可以利用 **setValue:forUndefinedKey:** 去处理
```objective-c
//model
@property (nonatomic,copy)NSString *name;
@property (nonatomic,copy)NSString *sex;
@property (nonatomic,copy) NSString* age;
//NSDictionary
NSDictionary *dic = @{@"username":@"张三",@"sex":@"男",@"id":@"22"};
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
if([key isEqualToString:@"id"]){
self.age=value;
}
if([key isEqualToString:@"username"]){
self.name=value;
}
}
```
- 情况四:如果我们观察对象的属性是数组,我们经常会观察不到变化,因为 KVO 是观察 setter 方法。我们可以用 `mutableArrayValueForKeyPath` 进行属性的操作
```
NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
[hobbies addObject:@"Web"];
```
- 情况五: 注册依赖键.
KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”
```
[self.person addObserver:self
forKeyPath:NSStringFromSelector(@selector(dog))
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:ContextMark];
self.person.dog.name = @"啸天犬";
self.person.dog.weight = 50;
// Person.m
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
```
## KVO 的本质
kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。
### 几个基本的知识点
1. KVO 观察者和属性被观察的对象之间不是强引用的关系
2. KVO 的触发分为`自动触发模式`和`手动触发模式`2种。通常我们使用的都是自动通知注册观察者之后当条件触发的时候会自动调用`-(void)observeValueForKeyPath...`

如果需要实现手动通知,我们需要使用下面的方法
```
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
```
3. 若类有实例变量 NSString *_foo 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key
都可以
4. KVC 的 keyPath 中的集合运算符如何使用
- 必须用在 **集合对象** 或者 **普通对象的集合属性** 上
-简单的集合运算符有 @avg、@count、@max、@min、@sum
5. KVO 和 KVC 的 keyPath 一定是属性吗?
可以是成员变量
### 实现机制
> Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间类,而不是原来真正的类,
通过对被观察的对象断点调试发现 Person 类在执行过 addObserveValueForKeyPath... 方法后 isa 改变了。NSKVONotifying_Person。
- KVO 是基于 Runtime 机制实现的
- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
- 如果当前类为 Person则生成的派生类名称为 `NSKVONotifying_Person`
- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
![KVO原理图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018_11_12_KVO.png)
为什么要选择是继承的子类而不是分类呢?
子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
### 模拟实现系统的 KVO
1. 创建被观察对象的子类
2. 重写观察对象属性的 set 方法,同时调用 `willChangeValueForKey、didChangeValueForKey`
3. 外界改变 isa 指针class方法重写
我们用自己的类模拟系统的 KVO。
```
//NSObject+LBPKVO.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LBPKVO)
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
//NSObject+LBPKVO.m
#import "NSObject+LBPKVO.h"
#import <objc/message.h>
@implementation NSObject (LBPKVO)
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//生成自定义的名称
NSString *className = NSStringFromClass(self.class);
NSString *currentClassName = [@"LBPKVONotifying_" stringByAppendingString:className];
//1. runtime 生成类
Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0);
// 生成后不能马上使用,必须先注册
objc_registerClassPair(myclass);
//2. 重写 setter 方法
class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@");
//3. 修改 isa
object_setClass(self, myclass);
//4. 将观察者保存到当前对象里面
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
//5. 将传递的上下文绑定到当前对象里面
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
}
//
void setName (id self, SEL _cmd, NSString *name) {
NSLog(@"come here");
//先切换到当前类的父类,然后发送消息 setName然后切换当前子类
//1. 切换到父类
Class class = [self class];
object_setClass(self, class_getSuperclass(class));
//2. 调用父类的 setName 方法
objc_msgSend(self, @selector(setName:), name);
//3. 调用观察
id observer = objc_getAssociatedObject(self, "observer");
id context = objc_getAssociatedObject(self, "context");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": name, @"kind": @1 } , context);
}
//4. 改回子类
object_setClass(self, class);
}
@end
//ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[Person alloc] init];
_person.name = @"杭城小刘";
_person.age = 23;
_person.hobbies = [@[@"iOS"] mutableCopy];
NSDictionary *context = @{@"name": @"成吉思汗", @"hobby" : @"弯弓射大雕"};
[_person lbpKVO_addObserver:self forKeyPath:@"hobbies" options:(NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(context)];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_person.name = @"刘斌鹏";
NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
[hobbies addObject:@"Web"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
```
### KVO 的缺陷
KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block没门儿。感觉如果加入 block 就更棒了。
### KVO 的改装
看到官方的做法并不是很方便使用,我们看到无数的优秀框架都支持 block 特性,比如 AFNetworking ,所以我们可以将系统的 KVO 改装成支持 block。

215
Chapter1 - iOS/1.48.md Normal file
View File

@@ -0,0 +1,215 @@
# 类别Category、拓展Extension
> 很多人都知道类别、分类的用法但是对于一些细节就不是很清楚了本文主要梳理下这3个概念的细节
## 类别Category
### 文件特征
- 类别文件有2个分别为 .h 和 .m
- 命名为: “类名+类别名.h”和“类名+类别名.m”
### 文件内容格式
.h 文件格式
```
#import "类名.h"
@interface 类名 (类别名)
// 在此处声明方法
@end
```
.m 文件格式
```
#import "类名+类别名.h"
@implementation 类名 (类别名)
// 在此处实现声明的方法
@end
```
### 类别的作用
拓展当前类,为类添加方法
### 类别的局限性
- 无法向现有的类添加实例变量编译器报“instance variables may not be placed in categories”。Category 一般只为类提供方法的拓展,不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性
- 方法名称冲突的情况下,如果 Category 中的方法与当前类的方法名称重名Category 具有更高的优先级,类别中的方法将完全取代现有类中的方法(调用方法的时候不会去调用现有类里面的方法实现)。
- 当现有类具有多个 Category 的时候,如果每个 Category 都有同名的方法,那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。(可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法)
### Category 的使用和注意
1. Category 中的方法如果和现有类方法一致,工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法比如UIViewCtroller、UITableView这些的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如用Category 重写了 UIViewCtroller 的方法 A那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类,去调用方法 A 时,执行的都是 Category 中重写的方法 A,如果不幸的是,你写的方法 A 有 Bug那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A这样子类调用方法 A 时是调用的自己重写的方法 A消除了父类 Category 中重写方法对自己的影响
2. Category拓展方法按照有没有重写当前类中的方法分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时,只能在 .m 文件中引用(.h 文件引用自己的类别会报错)。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法,不用引入 Category 头文件,系统会自动调用 Category 中的重写方法
3. Category 中如果重写了 A 类从父类继承来的某方法,不会影响与 A 同层级的 B 类
4. 子类会不会继承父类的 Category Category 中重写的方法会对子类造成影响,但是子类不会继承非重写的方法(现有类中没有的方法)。但是在子类中引入父类 Category 的声明文件后,子类就会继承 Category 的非重写方法。继承的表现是:当子类的方法和父类 Category 中的方法名完全相同,那么子类里的方法会覆盖掉父类 Category相当于子类重写了继承自父类的方法
5. Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类当给 A 类添加一个 Category sayHello 方法仅有A 类的子类才可以使用 sayHello 方法
## 拓展Extension
### 文件特征
- 只存在一个文件
- 命名方式“类名_拓展名.h”
```
#import "类名.h"
@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end
```
### 拓展的作用
1. 为类增加额外的属性、成员变量、方法声明
2. 一般将类拓展直接写到当前类的 .m 文件中。不单独创建
3. 一般私有的属性和方法写到类拓展中
4. 和 Category 类似,但是小括号里面没有拓展的名字
### 拓展的局限性
1. Extension 中添加的属性、成员变量、方法属于私有(只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的,同时子类也是无法继承的)。假如我们有这样一个需求,一个属性对外是只读的,对内是可以读写的,那么我们可以通过 Extension 实现。
2. 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错
3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。
```
//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END
//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
self.skills = @"iOS && Web && Node && Hybrid";
self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
NSLog(@"%@",self.skillStacks);
}
@end
```
## 总结
1. Category 只能拓充方法,不能拓展属性和成员变量(包含成员变量会报错。属性虽然不可以直接拓展,利用 Runtime 可以实现)
2. 如果 Category 中声明了1个属性那么 Category 只会生成 setter 和 getter 的声明,不会有实现
3. Extension 也被成为匿名的 Category
4. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。
##「小插曲」:为 Category 实现属性的 Setter 和 Getter
```
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Student)
/**< 学号*/
@property (nonatomic, strong) NSString *studyNumber;
@end
NS_ASSUME_NONNULL_END
#import "Person+Student.h"
#import <objc/message.h>
@implementation Person (Student)
- (void)sayHi {
NSLog(@"大家好,我叫%@,我今年%zd岁了",self.name,self.age);
}
/*
* 传统的做法是在 setter 里面这样写
_studyNumber = studyNumber;
ARC 自动管理内存
MRC
[_studyNumber release];
[studyNumber retain];
_studyNumber = studyNumber;
但是在 Category里面不会生成对应的实例变量因此我们可以利用 Runtime 为我们的 category 关联属性的值
setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
getter:objc_getAssociatedObject(self, @selector(firstView));
}
*/
- (void)setStudyNumber:(NSString *)studyNumber {
objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
, OBJC_ASSOCIATION_RETAIN);
}
//@selector(studyNumber)
- (NSString *)studyNumber {
return objc_getAssociatedObject(self, @selector(studyNumber));
}
@end
```
说明: `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足,所以示例代码选用第二种方式
给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。
比如我们在 Login-Register-Module 模块为 NSURL 的 Category 添加一个 title 的属性的时候,可以这样命名 LR_Title。请查看下面的代码
```Objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (Title)
@property (nonatomic, copy) NSString *LR_title;
@end
NS_ASSUME_NONNULL_END
#import "NSURL+Title.h"
#import <objc/runtime.h>
@implementation NSURL (Title)
- (void)setLR_title:(NSString *)LR_title
{
objc_setAssociatedObject(self, @selector(LR_title), LR_title
, OBJC_ASSOCIATION_RETAIN);
}
- (NSString *)LR_title
{
return objc_getAssociatedObject(self, @selector(LR_title));
}
@end
```

163
Chapter1 - iOS/1.49.md Normal file
View File

@@ -0,0 +1,163 @@
# 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用户得到反馈
所有的通信都是单向的。
## MVP
MVP 模式将 Controller 改名为 Presenter通信改变了通信方向
![MVP架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-MVP.png)
1. 各部分之间的通信都是双向的
2. Model 与 View 不发生联系,都通过 Presenter 传递
3. View 层非常薄。不部署任何业务逻辑称为“被动视图Passive View即没有任何主动性而 Presenter 非常厚,所有的逻辑都部署在这层
## MVVM
MVVM 模式将 Presenter 改名为 ViewModel基本上与 MVP 模式完全一致。
![MVVM架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-16-MVVM.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 配合一个绑定机制效果最好
## 一个简单的例子
PersonModel
```
@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
```
- (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
```
@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 会很轻量
```
- (void)viewDidLoad {
[super viewDidLoad];
self.nameLabel.text = self.viewModel.nameText;
self.birthdateLabel.text = self.viewModel.birthdateText;
}
```

可测试View Controller 是出了名的难测试。因为传动的 VC 很重,在 MVVM 的世界里,代码各司其职,测试 View Controller 变得较为容易
```
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
```

View File

@@ -1,3 +1,5 @@
# 事件响应者链
实验1:
@@ -7,7 +9,7 @@
![UI效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Simulator%20Screen%20Shot%20-%20iPhone%206s%20Plus%20-%202017-10-11%20at%2010.14.37.png)
![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)
```
//BaseView
@@ -44,7 +46,7 @@ NSLog(@"%@",[self class]);
#### 响应者链条
![响应者链条](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/响应者链条.png)
![响应者链条](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/reponseChain.png)
#### 事件传递的完整过程

25
Chapter1 - iOS/1.50.md Normal file
View File

@@ -0,0 +1,25 @@
# 动态库和静态库
通常,我们的 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 选项即可,无需其他设置。

205
Chapter1 - iOS/1.51.md Normal file
View File

@@ -0,0 +1,205 @@
# Pod
## 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
```

541
Chapter1 - iOS/1.52.md Normal file
View File

@@ -0,0 +1,541 @@
# 如何打造团队的代码风格统一以及开发效率的提升
> 最近重构项目组件,看到项目中存在一些命名和方法分块方面存在一些问题,结合平时经验和 [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. Property 属性。敲 **Property_** 自动联想,光标移动选中后敲回车自动补全
2. Mark 标识。 敲 **Mark_** 自动联想,会展示各种常用的 Mark光标移动选中后敲回车自动补全
3. Method 方法。敲 **Method_** 自动联想,会展示各种常用的 Method光标移动选中后敲回车自动补全
4. GCD。敲 **GCD_** 自动联想,会展示各种常用的 GCD光标移动选中后敲回车自动补全
5. 常用 UI 控件的懒加载。敲 **_init** 自动联想,展示常用的 UI 控件的懒加载,光标移动选中后敲回车自动补全
6. Delegate。敲 **Delegate_** 自动联想,会展示各种常用的 Delegate光标移动选中后敲回车自动补全
7. Notification。敲 **NSNotification_** 自动联想,会展示各种常用的 NSNotification 的代码块,比如发送通知、添加观察者、移除观察者、观察者方法的实现等等,光标移动选中后敲回车自动补全
8. Protocol。敲 **Protocol_** 自动联想,会展示各种常用的 Protocol 的代码块,光标移动选中后敲回车自动补全
9. 内存修饰代码块
10. 工程常用 TODO、FIXME、Mark。敲 **Mark_** 自动联想,会展示各种常用的 Mark 的代码块,光标移动选中后敲回车自动补全
11. 内存修饰代码块。敲 **Memory_** 自动联想,会展示各种常用的内存修饰的代码块,光标移动选中后敲回车自动补全
12. 一些常用的代码块。敲 **Thread_** 等自动联想,选中后敲回车自动补全。
## 使用
```shell
chmod +x ./syncSnippets.sh // 为脚本设置可执行权限
chmod +x ./uploadMySnippets.sh // 为脚本设置可执行权限
./syncSnippets.sh // 同步git云端代码块和文件模版到本地
./uploadMySnippets.sh //将本地的代码块和文件模版同步到云端
```
## PS
**不断完善中。大家有好用或者不错的代码块或者文件模版希望参与到这个项目中来,为我们开发效率的提升添砖加瓦、贡献力量**
目前新建了大概58个代码段和6个类文件模版UIViewController控制器带有方法组、模型、线程安全的单例模版、带有布局方法的UIView模版、UITableViewCell、UICollectionViewCell模版
shell 脚本基本有每个函数和关键步骤的代码注释,想学习 shell 的人可以看看代码。[代码传送门](https://github.com/FantasticLBP/codesnippets)

17
Chapter1 - iOS/1.53.md Normal file
View File

@@ -0,0 +1,17 @@
# 数据持久化方案
## 功能
主要将一些网络获取下来的数据保存到 App 本地,增强用户体验、减小网络请求的次数。
| 方案 | 存储量 | 数据类型 | 数据载体 | 特点 | 场景 | 缺点 |
|:-:| :-: | :-: | :-: | :-:| :-:| :-: |
| plist | 适合存储小数据量、并且属于一类的列表类的数据 | 基本数据类型、不支持存储自定义的对象 | 直接在plist文件上操作可见 | 量小、不常变动| 省市列表、职场工作分类| 所有数据都存放在 root dictionary 里,每次都需要将整个 dictiona 读取出来访问所需的对象。数据较大的时候很费时间和空间 |
| 归档 | 存储数据量较大的数据对象 | 遵循NSCoding、NSCopying协议的对象| 必须依赖NSKeyedArchieve、NSUnKeyedArchieve的类方法或者对象方法进行存储 | 存储较为麻烦,需要实现对应的协议。归档需要实现`-(void)encodeWithCoder:(NSCoder*)aCoder;`解码需要实现`-(id)initWithCoder:(NSCoder*)aDecoder;`。除了遵循NSCoding协议外对象要实现复制需要实现`-(id)copyWithZone:(NSZone *)zone;` | 存储一些较小的数据 | 无法存储较大的数据和高效的查找 |
| NSUserDefault偏好设置 | 适合存储数据量较少,有时也会存储一些比如状态开关的标准性值 | 存储OC所有的数据类型| 实例对象、基本数据 | 利用| App应用程序的配置信息、如版本号、app名称、用户权限、标志键值等| 无法存储自定义的对象 |
| 沙盒存储 | 存储较大量数据量的数据 | 基本存储 NSData 类型的数据| 文件 |依赖NSFileManager进行文件的写入和读取过程较为复杂。Application:存放程序源代码上架前经过数字签名上架后不可修改。Documents保存App运行时生成的需要持久化的数据iTunes同步设备会同步该目录。例如将游戏数据保存到该目录下。tmp保存App运行时产生的临时数据程序结束将文件从该目录删除。iTunes同步设备不会同步该目录。Library/Caches保存应用运行时生成的需要持久化的数据iTunes同步设备时不会同步该目录。一般体积大、不需要备份的非重要数据比如网络数据的缓存。Library/Preference保存应用的偏好设置比如OS的设置应用会在该目录下查找用户的设置信息。iTunes同步设备会同步该目录 | 图片、音视频。比如SDWebImage的文件缓存| 缓存太多,文件体积会非常大 |
| 数据库 | 存储大型数据量的数据 | OC所有的数据类型| 数据库文件 | 数据的增删改查较为强大的数据库批量处理指令SQL| 几乎每个大型App都有自己的数据库比如微信、微博为了较好的用户体验在每个小细节都有数据库技术| 需要新建数据库、建立连接、处理数据、关闭数据库连接。也不支持自定义的对象存储 |
## 2个概念Relation、Object

74
Chapter1 - iOS/1.54.md Normal file
View File

@@ -0,0 +1,74 @@
# 自定义工程中的头文件信息
> 我们打开 Xcode 工程的时候新建的文件顶部的信息非常的少且不是我们需要展示信息,看到很多的 GitHub 项目的顶部的头信息还是非常的花哨,所以在此记录如何写自定义模版的文章。
## 现状
```
//
// MASLayoutConstraint.h
// Masonry
//
// Created by Jonas Budelmann on 3/08/13.
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
//
```
## 目标
```
//
// SDGFasterEncoder.h
// XQ_Persistance
//
// Author: @杭城小刘
// Github: https://github.com/FantasticLBP
// E-mail: wsbglbp@outlook.com
//
// Created by 杭城小刘 on 2019/1/23
//
```
## 动手实现
我们利用 Xcode9 新特性,自定义文本宏,来实现上述的需求。
### 步骤
1. 创建 .plist 文件
2. 添加宏名称FILEHEADER
3. 添加宏对应的值,即自定义的注释格式
4. 将文件放置于起作用的文件目录下
- 选中项目的 **.xcodeproj 文件
- 显示包内容
- 进入 xcshareddata 文件夹
- 将之前完成的 IDETemplateMacros.plist 复制到xcshareddata 下面和 xcschemes 的同级目录
- 打开 XQ_Persistance.xcworkspace
- 显示包内容
- 进入 xcuserdata 文件夹
- 将 IDETemplateMacros.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>FILEHEADER</key>
<string>
// ___FILENAME___
// ___PACKAGENAME___
//
// Author: @杭城小刘
// Github: https://github.com/FantasticLBP
// E-mail: wsbglbp@outlook.com
//
// Created by 杭城小刘 on ___DATE___
//
</string>
</dict>
</plist>
```

766
Chapter1 - iOS/1.55.md Normal file
View File

@@ -0,0 +1,766 @@
# 无痕埋点的设计与实现
在移动互联网时代对于每个公司、企业来说用户的行为数据非常重要。重要到什么程度用户在这个页面停留多久、点击了什么按钮、浏览了什么内容、什么手机、什么网络环境、App什么版本等都需要清清楚楚。一些大厂的蛮多业务成果都是基于用户操作行为进行推荐后二次转换。另一方面是以日志的作用帮助开发者分析线上问题的一种辅助手段。
那么有了上述的诉求,那么技术人员如何满足这些需求?引出来了一个技术点-“埋点”
## 0x01. 埋点手段
业界中对于代码埋点主要有3种主流的方案代码手动埋点、可视化埋点、无痕埋点。简单说说这几种埋点方案。
- 代码手动埋点:根据业务需求(运营、产品、开发多个角度出发)在需要埋点地方手动调用埋点接口,上传埋点数据。
- 可视化埋点:通过可视化配置工具完成采集节点,在前端自动解析配置并上报埋点数据,从而实现可视化“无痕埋点”
- 无痕埋点:通过技术手段,完成对用户行为数据无差别的统计上传的工作。后期数据分析处理的时候通过技术手段筛选出合适的数据进行统计分析。
## 0x02. 技术选型
### 1. 代码手动埋点
该方案情况下,如果需要埋点,则需要在工程代码中,写埋点相关代码。因为侵入了业务代码,对业务代码产生了污染,显而易见的缺点是**埋点的成本较高**、且违背了**单一原则**。
例1假如你需要知道用户在点击“购买按钮”时的相关信息手机型号、App版本、页面路径、停留时间、动作等等那么就需要在按钮的点击事件里面去写埋点统计的代码。这样明显的弊端就是在之前业务逻辑的代码上面又多出了埋点的代码。由于埋点代码分散、埋点的工作量很大、代码维护成本较高、后期重构很头痛。
例2假如 App 采用了 Hybrid 架构,当 App 的第一版本发布的时候 H5 的关键业务逻辑统计是由 Native 定义好关键逻辑比如H5调起了Native的分享功能那么存在一个分享的埋点事件的桥接。假如某天增加了一个扫一扫功能未定义扫一扫的埋点桥接那么 H5 页面变动的时候Native 埋点代码不去更新的话,变动的 H5 的业务就未被精确统计。
优点:产品、运营工作量少,对照业务映射表就可以还原出相关业务场景、数据精细无须大量的加工和处理
缺点:开发工作量大、前期需要和运营、产品指定的好业务标识,以便产品和运营进行数据统计分析
### 2. 可视化埋点
**可视化埋点的出现是为解决代码埋点流程复杂、成本高、新开发的页面H5、或者服务端下发的 json 去生成相应页面)不能及时拥有埋点能力**
前端在「埋点编辑模式」下以“可视化”的方式去配置、绑定关键业务模块的路径到前端可以唯一确定到view的xpath过程。
用户每次操作的控件,都生成一个 **xpath** 字符串,然后通过接口将 xpath 字符串(view在前端系统中的唯一定位。以 iOS 为例App名称、控制器名称、一层层view、同类型view的序号“GoodCell.21.RetailTableView.GoodsViewController.*baoApp”)到真正的业务模块“宝App-商城控制器-分销商品列表-第21个商品被点击了”的映射关系上传到服务端。xpath 具体是什么在下文会有介绍。
之后操作 App 就生成对应的 xpath 和埋点数据(开发者通过技术手段将从服务端获取的关键数据塞到前端的 UI 控件上。 iOS 端为例, UIView 的 accessibilityIdentifier 属性可以设置我们从服务端获取的埋点数据)上传到服务端。
优点:数据量相对准确、后期数据分析成本低
缺点:前期控件的唯一识别、定位都需要额外开发;可视化平台的开发成本较高;对于额外需求的分析可能会比较困难
### 3. 无痕埋点
通过技术手段无差别地记录用户在前端页面上的行为。可以正确的获取 PV、UV、IP、Action、Time 等信息。
缺点:前期开发统计基础信息的技术产品成本较高、后期数据分析数据量很大、分析成本较高(大量数据传统的关系型数据库压力大)
优点:开发人员工作量小、数据全面、无遗漏、产品和运营按需分析、支持动态页面的统计分析
### 4. 如何选择
结合上述优缺点,我们选择了**无痕埋点+可视化埋点结合**的技术方案。
怎么说呢?对于关键的业务开发结束上线后、通过可视化方案(类似于一个界面,想想看 Dreamwaver你在界面上拖拖控件简单编辑下就可以生成对应的 HTML 代码)点击一下绑定对应关系到服务端。
那么这个对应关系是什么?我们需要唯一定位一个前端元素,那么想到的办法就是不管 Native 和 Web 前端控件或者元素来说就是一个树形层级DOM tree 或者 UI tree所以我们通过技术手段定位到这个元素以 Native iOS 为例子,假如点击商品详情页的加入购物车按钮会根据 UI 层级结构生成一个唯一标识 “addCartButton.GoodsViewController.GoodsView.*BaoApp” 。但是用户在使用 App 的时候,上传的是这串东西的 MD5到服务端。
这么做有2个原因服务端数据库存储这串很长的东西不是很好埋点数据被劫持的话直接看到明文不太好。所以 MD5 再上传。
## 0x03. 操刀就干
一言以蔽之就是:**AOP -> Event Collector -> Event Cache -> Data Upload**
- AOP通过 runtime hook 的能力做到提供合适的时机去生成点击事件的数据
- Event Collector将步骤1产生的数据统一收集一般是一个内存存储的数据结构
- Event Cachemmap当内存中的数据达到一定的阀值或者应用程序的生命周期切换的时候将内存中的数据同步到缓存中数据库、磁盘、文件
- Data Uploader制定一定的策略当达到触发条件的时候再去上传数据App达到阀值生命周期的切换等App从前台进入后台的时候去上传数据后台线程保活策略数据上传格式的选择zip压缩文件、protoBuf
### 1. 数据的收集
实现方案由以下几个关键指标:
- 现有代码改动少、尽量不要侵入业务代码去实现拦截系统事件
- 全量收集
- 如何唯一标识一个控件元素
### 2. 不侵入业务代码拦截系统事件
以 iOS 为例。我们会想到 **AOPAspect Oriented Programming**面向切面编程思想。动态地在函数调用前后插入相应的代码,在 Objective-C 中我们可以利用 Runtime 特性,用 **Method Swizzling** 来 hook 相应的函数
为了给所有类方便地 hook我们可以给 NSObject 添加个 Category名字叫做 NSObject+MethodSwizzling
```objective-c
#pragma mark - public Method
+ (void)lbp_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
class_swizzleInstanceMethod(self, originalSelector, swizzledSelector);
}
+ (void)lbp_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
//类方法实际上是储存在类对象的类(即元类)中,即类方法相当于元类的实例方法,所以只需要把元类传入,其他逻辑和交互实例方法一样。
Class class2 = object_getClass(self);
class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector);
}
#pragma mark - private method
void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
{
/*
Class class = [self class];
//原有方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//替换原有方法的新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//先尝试給源SEL添加IMP这里是为了避免源SEL没有实现IMP的情况
BOOL didAddMethod = class_addMethod(class,originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {//添加成功表明源SEL没有实现IMP将源SEL的IMP替换到交换SEL的IMP
class_replaceMethod(class,swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {//添加失败表明源SEL已经有IMP直接将两个SEL的IMP交换即可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
*/
Method originMethod = class_getInstanceMethod(class, originalSEL);
Method replaceMethod = class_getInstanceMethod(class, replacementSEL);
if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod))) {
class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod, replaceMethod);
}
}
```
### 3. 全量收集
我们会想到 hook AppDelegate 代理方法、UIViewController 生命周期方法、按钮点击事件、手势事件、各种系统控件的点击回调方法、应用状态切换等等。
| 动作 | 事件 |
| :-----------------------------------------------------: | :-----------------------------------------: |
| App 状态的切换 | 给 Appdelegate 添加分类hook 生命周期 |
| UIViewController 生命周期函数 | 给 UIViewController 添加分类hook 生命周期 |
| UIButton 等的点击 | UIButton 添加分类hook 点击事件 |
| UICollectionView、UITableView 等的 | 在对应的 Cell 添加分类hook 点击事件 |
| 手势事件 UITapGestureRecognizer、UIControl、UIResponder | 相应系统事件 |
以统计页面的打开时间和统计页面的打开、关闭的需求为例,我们对 UIViewController 进行 hook
```objective-c
static char *lbp_viewController_open_time = "lbp_viewController_open_time";
static char *lbp_viewController_close_time = "lbp_viewController_close_time";
@implementation UIViewController (lbpka)
// load 方法里面添加 dispatch_once 是为了防止手动调用 load 方法。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[[self class] lbp_swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(lbp_viewWillAppear:)];
[[self class] lbp_swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(lbp_viewWillDisappear:)];
}
});
}
#pragma mark - add prop
- (void)setOpenTime:(NSDate *)openTime
{
objc_setAssociatedObject(self,&lbp_viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDate *)getOpenTime
{
return objc_getAssociatedObject(self, &lbp_viewController_open_time);
}
- (void)setCloseTime:(NSDate *)closeTime
{
objc_setAssociatedObject(self,&lbp_viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDate *)getCloseTime
{
return objc_getAssociatedObject(self, &lbp_viewController_close_time);
}
- (void)lbp_viewWillAppear:(BOOL)animated
{
NSString *className = NSStringFromClass([self class]);
NSString *refer = [NSString string];
//TODO:TODO 是否只埋本地有url的page
if ([self getPageUrl:className]) {
//设置打开时间
[self setOpenTime:[NSDate dateWithTimeIntervalSinceNow:0]];
if (self.navigationController) {
if (self.navigationController.viewControllers.count >=2) {
//获取当前vc 栈中 上一个VC
UIViewController *referVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-2];
refer = [self getPageUrl:NSStringFromClass([referVC class])];
}
}
if (!refer || refer.length == 0) {
refer = @"unknown";
}
[UserTrackDataCenter openPage:[self getPageUrl:className] fromPage:refer];
}
[self lbp_viewWillAppear:animated];
}
- (void)lbp_viewWillDisappear:(BOOL)animated
{
NSString *className = NSStringFromClass([self class]);
if ([self getPageUrl:className]) {
[self setCloseTime:[NSDate dateWithTimeIntervalSinceNow:0]];
[UserTrackDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]];
}
[self lbp_viewWillDisappear:animated];
}
#pragma mark - private method
- (NSString *)p_calculationTimeSpend
{
if (![self getOpenTime] || ![self getCloseTime]) {
return @"unknown";
}
NSTimeInterval aTimer = [[self getCloseTime] timeIntervalSinceDate:[self getOpenTime]];
int hour = (int)(aTimer/3600);
int minute = (int)(aTimer - hour*3600)/60;
int second = aTimer - hour*3600 - minute*60;
return [NSString stringWithFormat:@"%d",second];
}
@end
```
### 4. 如何唯一标识一个控件元素
**xpath** 是移动端定义可操作区域的唯一标识。既然想通过一个字符串标识前端系统中可操作的控件,那么 xpath 需要2个指标
- 唯一性:在同一系统中不存在不同控件有着相同的 xpath
- 稳定性:不同版本的系统中,在页面结构没有变动的情况下,不同版本的相同页面,相同的控件的 xpath 需要保持一致。
我们想到 Naive、H5 页面等系统渲染的时候都是以树形结构去绘制和渲染,所以我们以当前的 View 到系统的根元素之间的所有关键点UIViewController、UIView、UIView容器UITableView、UICollectionView等、UIButton...)串联起来这样就唯一定位了控件元素。
为了精确定位元素节点,参看下图
假设一个 UIView 中有三个子 view先后顺序是label、button1、button2那么深度依次为 0、1、2。假如用户做了某些操作将 label1 从父 view 中被移除了。此时 UIView 只有 2 个子viewbutton1、button2而且深度变为了0、1。
![view层级](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-01-userTrack.png)
可以看出仅仅由于其中某个子 view 的改变,却导致其它子 view 的深度都发生了变化。因此,在设计的时候需要注意,在新增/移除某一 view 时,尽量减少对已有 view 的深度的影响,调整了对节点的深度的计算方式:采用当前 view 位于其父 view 中的所有 **与当前 view 同类型** 子view 中的索引值。
我们再看一下上面的这个例子,最初 label、button1、button2 的深度依次是0、0、1。在 label 被移除后button1、button2 的深度依次为0、1。可以看出在这个例子中label 的移除并未对 button1、button2 的深度造成影响,这种调整后的计算方式在一定程度上增强了 xpath 的抗干扰性。
另外,调整后的深度的计算方式是依赖于各节点的类型的,因此,此时必须要将各节点的名称放到 `viewPath` 中,而不再是仅仅为了增加可读性。
在标识控件元素的层级时,需要知道「当前 view 位于其父 view 中的所有 **与当前 view 同类型** 子view 中的索引值」。参看上图,如果不是同类型的话,则唯一性得不到保证。
### 5. 同类型的 view 的唯一定位问题
有个问题,比如我们点击的元素是 UITableViewCell那么它虽然可以定位到类似于这个标示 xxApp.GoodsViewController.GoodsTableView.GoodsCell同类型的 Cell 有多个,所以单凭借这个字符串是没有办法定位具体的那个 Cell 被点击了。
当然有解决方案啦。
- 找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素
对当前的控件元素的父视图的全部子视图进行遍历如果存在和当前的控件元素同类型的控件那么需要判断当前控件元素在同类型控件元素中的所处的位置那么则可以唯一定位。举例GoodsCell-3.GoodsTableView.GoodsViewController.xxApp
```objective-c
//UIResponder分类
- (NSString *)lbp_identifierKa
{
// if (self.xq_identifier_ka == nil) {
if ([self isKindOfClass:[UIView class]]) {
UIView *view = (id)self;
NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath];
NSMutableString *str = [NSMutableString string];
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
if (!view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) {
[str appendString:sameViewTreeNode];
[str appendString:@","];
}
while (view.nextResponder) {
[str appendFormat:@"%@,", NSStringFromClass(view.class)];
if ([view.class isSubclassOfClass:[UIViewController class]]) {
break;
}
view = (id)view.nextResponder;
}
self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]];
// self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str];
}
// }
return self.xq_identifier_ka;
}
// UIView 分类
- (NSString *)obtainSameSuperViewSameClassViewTreeIndexPat
{
NSString *classStr = NSStringFromClass([self class]);
//cell的子view
//UITableView 特殊的superview (UITableViewContentView)
//UICollectionViewCell
BOOL shouldUseSuperView =
([classStr isEqualToString:@"UITableViewCellContentView"]) ||
([[self.superview class] isKindOfClass:[UITableViewCell class]])||
([[self.superview class] isKindOfClass:[UICollectionViewCell class]]);
if (shouldUseSuperView) {
return [self obtainIndexPathByView:self.superview];
}else {
return [self obtainIndexPathByView:self];
}
}
- (NSString *)obtainIndexPathByView:(UIView *)view
{
NSInteger viewTreeNodeDepth = NSIntegerMin;
NSInteger sameViewTreeNodeDepth = NSIntegerMin;
NSString *classStr = NSStringFromClass([view class]);
NSMutableArray *sameClassArr = [[NSMutableArray alloc]init];
//所处父view的全部subviews根节点深度
for (NSInteger index =0; index < view.superview.subviews.count; index ++) {
//同类型
if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){
[sameClassArr addObject:view.superview.subviews[index]];
}
if (view == view.superview.subviews[index]) {
viewTreeNodeDepth = index;
break;
}
}
//所处父view的同类型subviews根节点深度
for (NSInteger index =0; index < sameClassArr.count; index ++) {
if (view == sameClassArr[index]) {
sameViewTreeNodeDepth = index;
break;
}
}
return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth];
}
```
![页面唯一标识示意图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-09-UserTrack.png)
### 6. 同类型的view但是点击的意义却不一样。如何唯一标识
问题5说明的是在一个界面上有多个不同的 view他们的类型是同一种CycleBannerView但是数据源不一样那么当数据源长度大于1的时候会轮播下面会展示 UIPageControl。如果数据源是1个那么就不会轮播和展示 UIPageControl。情况6是同一种类型的 View但是根据展示的内容不一样点击的意义也不一样。也就是运营需要去知道用户到底点击的是哪一个。如下图所示「立即抢购」和「分享赚佣金」是同一种类型的 View但是点击意义不一样需要我们需要唯一标识出来。之前的方法通过 **“viewPath 配合同类型的 view 去加索引值“** 的方式还是没有办法唯一标识出来。所以想到一个方案,给 NSObject 添加一个分类,在分类里面添加一个协议。让需要复用但需要唯一标识的 view 去实现协议方法,因为是给 NSObject 分类添加的协议,所以 view 不需要去指定遵循。
!["立即抢购"、"分享赚佣金"同类型view但点击意义不一样](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-11-UserTrack01.png)
关键步骤:
- 添加 NSObject 的 Category。在分类里面声明唯一标识的协议
- 在生成 viewPath 的地方去拿出当前 view 的唯一标识view 调用协议方法)。然后拼接之前拿出的 viewPath
```objective-c
//NSObject+UniqueIdentify.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class NSObject;
@protocol UniqueIdentify<NSObject>
@optional
- (NSString *)setUniqueIdentifier;
@end
@interface NSObject (UniqueIdentify)<UniqueIdentify>
@end
NS_ASSUME_NONNULL_END
//NSObject+UniqueIdentify.m
#import "NSObject+UniqueIdentify.h"
@implementation NSObject (UniqueIdentify)
@end
```
```objective-c
//MallTGoodTagView.h
extern NSString * _Nonnull const ImmediateyPurchase;
extern NSString * _Nonnull const ShareToAward;
//MallTGoodTagView.m
NSString *const ImmediateyPurchase = @"立即抢购";
NSString *const ShareToAward = @"分享赚佣金";
- (NSString *)setUniqueIdentifier
{
if (self.tagString) {
return self.tagString;
} else {
return NSStringFromClass([self class]);
}
}
```
```objective-c
//UIResponder Category 生成 viewPath
- (NSString *)lbp_identifierKa
{
// if (self.xq_identifier_ka == nil) {
if ([self isKindOfClass:[UIView class]]) {
UIView *view = (id)self;
NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath];
NSMutableString *str = [NSMutableString string];
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
if (!view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) {
[str appendString:sameViewTreeNode];
[str appendString:@","];
}
while (view.nextResponder) {
if ([view respondsToSelector:@selector(setUniqueIdentifier)]) {
NSString *unqiueIdentifier = [view setUniqueIdentifier];
if (unqiueIdentifier) {
[str appendFormat:@"%@,", unqiueIdentifier];
}
}00
[str appendFormat:@"%@,", NSStringFromClass(view.class)];
if ([view.class isSubclassOfClass:[UIViewController class]]) {
break;
}
view = (id)view.nextResponder;
}
self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]];
// self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str];
}
// }
return self.xq_identifier_ka;
}
```
![改进版view唯一标识立即抢购](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-11-UserTrack3.png)
![改进版view唯一标识分享赚佣金](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-11-UserTrack2.png)
### 7. 疑惑点
根据在同一个 view 上会有多个 subview那么生成的 xpath 会携带在同类型 views 中的索引,所以一个登录、注册按钮的 xpath 可能为 ...btn1、...btn2。那么在版本A上线后运行了一段时间上传并统计了数据。过了一段时间版本迭代UI 搞事情,把登录和注册按钮的位置欢乐,变成了注册、登录。按照之前的逻辑生成的 xpath 为 ...btn1、...btn2。那么新的 xpath 虽然唯一,但是点击产生的数据会和之前的埋点数据意义不一样。别怕,你忘了还有一步绑定的逻辑。绑定的这一步会把每次开发的功能,通过可视化界面去将 xpath 和功能名称绑定一下。看下面的动图。所以不用担心虽然生成了唯一的 xpath但是 App 在不同版本之间 UI 控件位置更换造成之前的统计数据在分析的时候不准确的问题。因为在绑定的时候就将新的 xpath 和功能名称进行了绑定接口携带版本号。所以分析的时候注意版本号就好了。sql 一句话的事情。
![绑定页面唯一标识与功能描述的对应关系动图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-07-03-XpathBind.gif)
### 8. 数据如何处理
#### A. 如何处理业务数据
利用系统提供的 **accessibilityIdentifier** 官方给出的解释是标识用户界面元素的字符串
> */**
>
> *A string that identifies the user interface element.*
>
> *default == nil*
>
> **/*
>
> **@property**(**nullable**, **nonatomic**, **copy**) NSString *accessibilityIdentifier NS_AVAILABLE_IOS(5_0);
服务端下发唯一标识
接口获取的数据,里面有当前元素的唯一标识。比如在 UITableView 的界面去请求接口拿到数据,那么在在获取到的数据源里面会有一个字段,专门用来存储动态化的经常变动的业务数据。
```objective-c
cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categories[indexPath.section] children][indexPath.row].spmContent yy_modelToJSONString];
```
#### B. 基础数据
设计上分为2个 pod 库,一个是 TriggerKit专门用来 hook 机会需要的所有事件页面停留时间、页面标识、view标识另一个是 Appmonitor专门用来提供基础数据、埋点数据的维护、上传机制。所以在 Appmonitor 里面有个类叫做 UserTrackDataCenter 的类,专门提供一些基础数据(系统版本、操作系统、地理位置、网络等信息)。
对外暴露出一些方法,用来将埋点数据交给 Appmonitor 去维护埋点数据,达到合适的“机制”再去上传埋点数据到服务端。
```objective-c
+ (void)clickEventUuid:(NSString *)uuid otherParam:(NSDictionary *)otherParam spmContent:(NSDictionary *)spmContent
{
if (uuid) {
NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithDictionary:otherParam];
params[SDGStatisticEventtagKey] = @"clickMonitorV1";
NSMutableDictionary *valueDict = [[NSMutableDictionary alloc] initWithDictionary:spmContent];
valueDict[@"xpath"] = uuid?:@"";
params[SDGStatisticEventtagValue] = valueDict?:@{};
[[AppMonotior shareInstance] traceEvent:[AMStatisticEvent eventWithInfo:params]];
}
}
```
### 9. 曝光时间的统计
曝光的意义是什么?
我们的产品中可能有合作伙伴的广告我们需要收取服务费。那如何计价CPMcost per Mille每千人成本、CPCcost per click每点击成本、CPAcost per action每行动成本根据这些指标来计算价格。或者自己的产品中运营人员在商城中投放了一次新的活动为了这次活动在某个钻石展位放了设计人员精心设计的炫酷 Banner。这次活动后运营人员想分析在这个图片的作用下有多少人点击了这个活动页。
何为曝光?
一个 view 或者一个组件或者一个资源位在屏幕上可见区域内停留的时间称为一次曝光。那么这个时间怎么统计?有一个点需要注意,那就是当用户在快速滑动的过程中页面上的元素或者组件都会在页面可见区域内快速闪过,那这种算一次曝光吗?当然不算啊,想了想设置了一个时间临界值,大于这个临界值那么算做一次有效的曝光。
#### A. 有效曝光的判断
显示在屏幕可见区域如何判断?一个 View 显示在屏幕可见区域内,那么它肯定是经过从未初始化到初始化,再到设置 Frame 或者 Bounds 或者 Alpha 或者 Hidden 的。且它的根 view 一定是 UIWindow 对象。所以上面这句话进行分析整理就是下面的条件
- 自身 frame 的改变或者父视图 bounds 的改变
- alpha 小于 0.1 或者 hidden 为 YES
- 根视图为 window
对于上面的三点可以用 AOP 进行判断。 hook 掉相应的方法,然后处理判断是否在可见区域内显示。最后的一个点经过一番查找,看到了一个 api `didMoveToWindow` ,根据它可以判断 view 是否显示到屏幕中(文档中说明:当它的 window 对象发送改变的时候会调用 view 的 didMoveToWindow 方法)。
```
Tells the view that its window object changed.
The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the window changes.
The window property may be nil by the time that this method is called, indicating that the receiver does not currently reside in any window. This occurs when the receiver has just been removed from its superview or when the receiver has just been added to a superview that is not attached to a window. Overrides of this method may choose to ignore such cases if they are not of interest.
```
#### B. 曝光代码的执行效率优化
设想一下,某个复杂的页面可能是一个大的 UIViewController 顶部是店铺的基本信息,下面是 2个 UIViewController左侧负责展示商品的一级、二级、三级分类且负责选中和未选中的 UI 效果;右侧负责展示商品信息(顶部有商品的排序查找 ,下面是商品展示的 UICollectionView。由于页面结构复杂UI 层级嵌套严重所以代码层面不注意的话页面上计算量会比较大CPU 负荷严重,直接影响着手机的 `耗电量`。改进的手段是在合适的地方提前 return 掉(比如 hidden 等于 YES 或者 aplha 小于 0.1 的时候)。
另外一个方面就是当用户在滑动页面到感兴趣的模块的时候,开始点击执行某个逻辑,但此时我们的无痕埋点的代码也在偷偷的工作,那么势必会对用户体验造成影响。该方案的改进是监听 `RunLoop`,等到 RunLoop 空闲的时候判断当前 view 是否是一次有效的曝光。
实际上发现某些 view 的判断会比较特殊,比如当在 UITableView 的 cell 判断的时候,我们发现 cell 的 superview 为 UITableViewWrapperView 时,我们使用 UITableViewWrapperView 的父视图来计算。
iOS 11 以下 UITableViewWrapperView 大小为屏幕中第一个完整的屏幕大小视图,且会随着 contentOffset 的改变而改变。所以当 UITableViewWrapperView 滑出屏幕可见区域的时候cell 判断父视图是否可见的时候不准确。
整个流程见下面的流程图。
![整体流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-07-08-ComponentExposure.jpeg)
### 10. 数据的上报
数据通过上面的办法收集完了,那么如何及时、高效的上传到后端,给运营分析、处理呢?
App 运行期间用户会点击非常多的数据,如果实时上传的话对于网络的利用率较低,所以需要考虑一个机制去控制用户产生的埋点数据的上传。
思路是这样的。对外部暴露出一个接口,用来将产生的数据往数据中心存储。用户产生的数据会先保存到 AppMonitor 的内存中去设置一个临界值memoryEventMax = 50如果存储的值达到设置的临界值 memoryEventMax那么将内存中的数据写入文件系统以 zip 的形式保存下来,然后上传到埋点系统。如果没有达到临界值但是存在一些 App 状态切换的情况,这时候需要及时保存数据到持久化。当下次打开 App 就去从本地持久化的地方读取是否有未上传的数据,如果有就上传日志信息,成功后删除本地的日志压缩包。
App 应用状态的切换策略如下:
- didFinishLaunchWithOptions:内存日志信息写入硬盘
- didBecomeActive上传
- willTerimate内存日志信息写入硬盘
- didEnterBackground内存日志信息写入硬盘
下面的代码是 App 埋点数据的保存与上传
```objective-c
// 将App日志信息写入到内存中。当内存中的数量到达一定规模超过设置的内存中存储的数量的时候就将内存中的日志存储到文件信息中
- (void)joinEvent:(NSDictionary *)dictionary
{
if (dictionary) {
NSDictionary *tmp = [self createDicWithEvent:dictionary];
if (!s_memoryArray) {
s_memoryArray = [NSMutableArray array];
}
[s_memoryArray addObject:tmp];
if ([s_memoryArray count] >= s_flushNum) {
[self writeEventLogsInFilesCompletion:^{
[self startUploadLogFile];
}];
}
}
}
// 外界调用的数据传递入口App埋点统计
- (void)traceEvent:(AMStatisticEvent *)event
{
// 线程锁,防止多处调用产生并发问题
@synchronized (self) {
if (event && event.userInfo) {
[self joinEvent:event.userInfo];
}
}
}
// 将内存中的数据写入到文件中,持久化存储
- (void)writeEventLogsInFilesCompletion:(void(^)(void))completionBlock
{
NSArray *tmp = nil;
@synchronized (self) {
tmp = s_memoryArray;
s_memoryArray = nil;
}
if (tmp) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *jsonFilePath = [weakSelf createTraceJsonFile];
if ([weakSelf writeArr:tmp toFilePath:jsonFilePath]) {
NSString *zipedFilePath = [weakSelf zipJsonFile:jsonFilePath];
if (zipedFilePath) {
[AppMonotior clearCacheFile:jsonFilePath];
if (completionBlock) {
completionBlock();
}
}
}
});
}
}
// 从App埋点统计压缩包文件夹中的每个压缩包文件上传服务端成功后就删除本地的日志压缩包
- (void)startUploadLogFile
{
NSArray *fList = [self listFilesAtPath:[self eventJsonPath]];
if (!fList || [fList count] == 0) {
return;
}
[fList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (![obj hasSuffix:@".zip"]) {
return;
}
NSString *zipedPath = obj;
unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:zipedPath error:nil] fileSize];
if (!fileSize || fileSize < 1) {
return;
}
// 调用接口上传埋点数据
[self uploadZipFileWithPath:zipedPath completion:^(NSString *completionResult) {
if ([completionResult isEqual:@"OK"]) {
[AppMonotior clearCacheFile:zipedPath];
}
}];
}];
}
```
使用的时候就是在 hook 系统事件的时候,去调用统计页面上传数据
```objective-c
//UIViewController
[UserTrackDataCenter openPage:[self getPageUrl:className] fromPage:refer]; // 页面出现
[UserTrackDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]]; //页面消失
```
![绑定页面唯一标识与功能描述的对应关系](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-03-07-UserTracet1.PNG)
总结下来关键步骤:
1. hook 系统的各种事件UIResponder、UITableView、UICollectionView代理事件、UIControl事件、UITapGestureRecognizers、hook 应用程序、控制器生命周期。在做本来的逻辑之前添加额外的监控代码
2. 对于点击的元素按照视图树生成对应的唯一标识addCartButton.GoodsView.GoodsViewController 的 md5 值
3. 在业务开发完毕,进入埋点的编辑模式,将 md5 和关键的页面的关键事件运营、产品想统计的关键模块App层级、业务模块、关键页面、关键操作给绑定起来。比如 addCartButton.GoodsView.GoodsViewController.tbApp 对应了 tbApp-商城模块-商品详情页-加入购物车功能。
4. 将所需要的数据存储下来
5. 设计机制等到合适的时机去上传数据
## 举例说明一个完整的埋点上报流程
埋伏模块分为2个pod组件库TriggerKit 负责拦截系统事件拿到埋点数据。Appmonitor 负责收集埋点数据,本地持久化或内存储存,等到合适时机去上传埋点数据。
1. 通过接口获取数据,给对应的 view 的 accessibilityIdentifier 属性绑定埋点数据
![接口拿到的数据](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack1.png)
![绑定埋点数据到view](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack2.png)
2. hook 系统事件,点击拿到 view获取 accessibilityIdentifier 属性值
![hook系统事件获取accessibilityIdentifier](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack3.png)
3. 将数据向的数据中心发送数据中心处理数据埋点数据结合App基础信息图上 UserTrackDataCenter 对象)。根据情况将数据存储到内存或者本地,等到合适的时机去上传
![拦截系统事件后将数据交给数据中心处理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack4.png)

View File

@@ -1,59 +1,56 @@
# 反爬技术研究
# 大前端时代安全性如何做
> 之前在做过一些爬虫的工作,也帮助爬虫工程师解决过一些问题。然后我写过一些文章发布到网上,之后有一些人就找我做一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,但是我没做。我觉得对于国内的大数据公司没几家是有真正的大数据量,而是通过爬虫工程师团队不断的去各地爬取数据,因此不要以为我们的数据没价值,对于内容型的公司来说,数据是可信竞争力。那么我接下来想说的就是网络和数据的安全性问题。
> 对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App短期内成为你的劲敌。
## 一、爬虫手段
# 爬虫手段
- 目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本
- 有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID这个步骤就可以拦截掉一部分的爬虫开发者
目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本.
有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID这个步骤就可以拦截掉一部分的爬虫开发者
# 制定出**Web 端反爬技术方案**
## 二、制定出**Web 端反爬技术方案**
本人从这2个角度网页所见非所得、查接口请求没用出发制定了下面的反爬方案。
从这2个角度网页所见非所得、查接口请求没用出发制定了下面的反爬方案。
1. 使用HTTPS 协议
2. 单位时间内限制掉请求次数过多,则封锁该账号
3. 前端技术限制 (接下来是核心技术)
- 使用HTTPS 协议
举例:比如需要正确显示的数据为“19950220”
- 单位时间内限制掉请求次数过多,则封锁该账号
- 前端技术限制 (接下来是核心技术)
```markdown
# 比如需要正确显示的数据为“19950220”
#### 2.1 原始数据加密
1. 先按照自己需求利用相应的规则数字乱序映射比如正常的0对应还是0但是乱序就是 0 <-> 11 <-> 9,3 <-> 8,...制作自定义字体ttf
2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 17730220
3. 对于第一步得到的字符串依次遍历每个字符将每个字符根据按照线性变换y=kx+b。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”那么线性变换的 k 为 7b 为 24。
4. 然后将变换后的每个字符串用“3.1415926”拼接返回给接口调用者。(为什么是3.1415926,因为对数字伪造反爬,所以拼接的文本肯定是数字的话不太会引起研究者的注意,但是数字长度太短会误伤正常的数据,所以用所熟悉的 Π)
```
```
1773 -> “1*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “3*7+24” -> 313.1415926733.1415926733.141592645
02 -> "0*7+24" + "3.1415926" + "2*7+24" -> 243.141592638
20 -> "2*7+24" + "3.1415926" + "0*7+24" -> 383.141592624
```
````
#### 2.2 前端拿到数据后再解密,解密后根据自定义的字体 Render 页面
# 前端拿到数据后再解密,解密后根据自定义的字体 Render 页面
1. 先将拿到的字符串按照“3.1415926”拆分为数组
2. 对数组的每1个数据按照“线性变换”y=kx+bk和b同样按照当前的日期求解得到逆向求解到原本的值。
3. 将步骤2的的到的数据依次拼接再根据 ttf 文件 Render 页面上。
```
- 后端需要根据上一步设计的协议将数据进行加密处理
#### 2.3 后端需要根据上一步设计的协议将数据进行加密处理
下面以 **Node.js** 为例讲解后端需要做的事情
- 首先后端设置接口路由
- 获取路由后面的参数
- 根据业务需要根据 SQL 语句生成对应的数据。如果是数字部分,则需要按照上面约定的方法加以转换。
- 将生成数据转换成 JSON 返回给调用者
1. 首先后端设置接口路由
2. 获取路由后面的参数
3. 根据业务需要根据 SQL 语句生成对应的数据。如果是数字部分,则需要按照上面约定的方法加以转换
4. 将生成数据转换成 JSON 返回给调用者
```js
// json
@@ -418,7 +415,7 @@
- 前端根据服务端返回的数据逆向解密
#### 2.4 前端根据服务端返回的数据逆向解密
```javascript
$("#year").html(getRawData(data.year,log));
@@ -462,15 +459,16 @@
```
比如后端返回的是323.14743.14743.1446根据我们约定的算法可以的到结果为1773
比如后端返回的是323.14743.14743.1446根据我们约定的算法可以的到结果为1773
- 根据 ttf 文件 Render 页面
![自定义字体文件](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180724-184215@2x.png)
#### 2.5 根据 ttf 文件 Render 页面
![自定义字体文件](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180724-184215.png)
上面计算的到的1773然后根据ttf文件页面看到的就是1995
- 然后为了防止爬虫人员查看 JS 研究问题,所以对 JS 的文件进行了加密处理。如果你的技术栈是 Vue 、React 等webpack 为你提供了 JS 加密的插件,也很方便处理
#### 2.6 加密混淆
为了防止爬虫人员查看 JS 研究问题,所以对 JS 的文件进行了加密处理。如果你的技术栈是 Vue 、React 等webpack 为你提供了 JS 加密的插件,也很方便处理
[JS混淆工具](http://www.javascriptobfuscator.com/Javascript-Obfuscator.aspx)
@@ -479,23 +477,21 @@
##  反爬升级版
##  三、反爬升级版
个人觉得如果一个前端经验丰富的爬虫开发者来说,上面的方案可能还是会存在被破解的可能,所以在之前的基础上做了升级版本
1. 组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模有4种值 0、1、2、3。这4种值对应不同的字体文件所以当爬虫绞尽脑汁爬到1种情况下的字体时没想到再次请求字体文件的规则变掉了 😂
2. 组合拳2: 前面的规则是字体问题乱序,但是只是数字匹配打乱掉。比如 **1** -> **4**, **5** -> **8**。接下来的套路就是每个数字对应一个 **unicode 码** ,然后制作自己需要的字体,可以是 .ttf、.woff 等等。
![网页检察元素得到的效果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180726-161418.png)
![接口返回数据](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180726-161429.png)
![网页检察元素得到的效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180726-161418.png)
![接口返回数据](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180726-161429.png)
这几种组合拳打下来。对于一般的爬虫就放弃了。
## 反爬手段再升级
## 四、反爬手段再升级
上面说的方法主要是针对**数字**做的反爬手段,如果要对汉字进行反爬怎么办?接下来提供几种方案
@@ -507,7 +503,9 @@
本人将方案1实现到 Demo 中了。
### 关键步骤
#### 关键步骤
1. 先根据你们的产品找到常用的关键词,生成**词云**
2. 根据词云,将每个字生成对应的 unicode 码
@@ -549,12 +547,11 @@ h3,a{
![审查元素效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095124%402x.png)
### 传送门
[字体制作的步骤](https://blog.csdn.net/fdipzone/article/details/68166388)、[ttf转svg](https://everythingfonts.com/ttf-to-svg)、[字体映射规则](https://icomoon.io/app/#/select/font)
传送门:[字体制作的步骤](https://blog.csdn.net/fdipzone/article/details/68166388)、[ttf转svg](https://everythingfonts.com/ttf-to-svg)、[字体映射规则](https://icomoon.io/app/#/select/font)
实现效果:
## 实现的效果
1. 页面上看到的数据跟审查元素看到的结果不一致
2. 去查看接口数据跟审核元素和界面看到的三者不一致
3. 页面每次刷新之前得出的结果更不一致
@@ -566,21 +563,19 @@ h3,a{
![数字反爬-网页显示效果、审查元素、接口结果情况1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180810-151046@2x.png)
![数字反爬-网页显示效果、审查元素、接口结果情况2](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180810-151203@2x.png)
![数字反爬-网页显示效果、审查元素、接口结果情况3](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180810-151239@2x.png?raw=true)
![数字反爬-网页显示效果、审查元素、接口结果情况4](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180810-151308@2x.png?raw=true)
![数字反爬-网页显示效果、审查元素、接口结果情况3](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-151239@2x.png)
![数字反爬-网页显示效果、审查元素、接口结果情况4](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-151308@2x.png)
![汉字反爬-网页显示效果、审查元素、接口结果情况1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095006%402x.png)
![汉字反爬-网页显示效果、审查元素、接口结果情况2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095124%402x.png)
<hr>
前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,下面贴出个新链接。
[ttf转svg](https://convertio.co/zh/font-converter/)
前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,贴出个新链接。[ttf转svg](https://convertio.co/zh/font-converter/)
## [Demo 地址](https://github.com/FantasticLBP/Anti-WebSpider)
![效果演示](https://raw.githubusercontent.com/FantasticLBP/Anti-WebSpider/master/Anti-WebSpider.gif)
@@ -602,3 +597,86 @@ $ cd REST/
$ npm install
$ node app.js
```
### 五、 App 端安全的解决方案
目前 App 的网络通信基本都是用 HTTPS 的服务,但是随便一个抓包工具都是可以看到 HTTPS 接口的详细数据,为了做到防止抓包和无法模拟接口的情况,我们采取以下措施:
1. 中间人盗用数据,我们可以采取 HTTPS 证书的双向认证,这样子实现的效果就是中间人在开启抓包软件分析 App 的网络请求的时候,网络会自动断掉,无法查看分析请求的情况
2. 对于防止用户模仿我们的请求再次发起请求,我们可以采用 「防重放策略」,用户再也无法模仿我们的请求,再次去获取数据了。
3. 对于 App 内的 H5 资源反爬虫方案可以采用上面的解决方案H5 内部的网络请求可以通过 Hybrid 层让 Native 的能力去完成网络请求,完成之后将数据回调给 JS。这么做的目的是往往我们的 Native 层有完善的账号体系和网络层以及良好的安全策略、鉴权体系等等。
<details>
<summary>JS端发起网络请求代码点击展开</summary>
```Javascript
var requestObject = {
url: arg.Api + "SearchInfo/getLawsInfo",
params: requestparams,
Hybrid_Request_Method: 0
};
requestHybrid({
tagname: 'NativeRequest',
param: requestObject,
encryption: 1,
callback: function (data) {
renderUI(data);
}
})
```
</details>
<details>
<summary>Objective-C代码点击展开</summary>
```Objective-C
[self.bridge registerHandler:@"NativeRequest" handler:^(id data, WVJBResponseCallback responseCallback) {
NSAssert([data isKindOfClass:[NSDictionary class]], @"H5 端不按套路");
if ([data isKindOfClass:[NSDictionary class]]) {
NSDictionary *dict = (NSDictionary *)data;
RequestModel *requestModel = [RequestModel yy_modelWithJSON:dict];
NSAssert( (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) || (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get ), @"H5 端不按套路");
[HybridRequest requestWithNative:requestModel hybridRequestSuccess:^(id responseObject) {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:nil];
responseCallback([self convertToJsonData:@{@"success":@"1",@"data":json}]);
} hybridRequestfail:^{
LBPLog(@"H5 call Native`s request failed");
responseCallback([self convertToJsonData:@{@"success":@"0",@"data":@""}]);
}];
}
}];
```
</details>
4. 有些人觉得利用 RSA 加密虽然可以保证数据的安全,但是因为每次都是大量字符串的运算,觉得数据量大的情况下用 RSA 加解密会非常耗时。对,肯定耗时,所以较好的做法就是将通信的 Alice 和 Bob 两方利用 **RSA** 的方式交换密钥。然后两方在通信的时候数据内容采用**对称加密**的方式进行。但是私钥在本地如何存放呢?想到的办法就是将关键密钥的字符串提高到较高的安全级别,比如这个文件用加密保存。接下来推荐一个[工具](https://github.com/RNCryptor/RNCryptor),可以将代码文件进行加密保存和解密访问。
## 六、 数据安全(反爬虫)之「防重放」策略
虽然话题都是大前端时代的安全性,但是防重放策略篇幅较长,开了新的[章节](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md)。感兴趣的同学请移步查看。
## 七、Canvas 反爬虫技术方案
> 后期打到白热化的时候用的技术越来越匪夷所思。举个例子很多人会提做反爬虫会用到canvas指纹并认为是最高境界。其实这个东西对于反爬虫来说也只是个辅助canvas指纹的含义是因为不同硬件对canvas支持不同因此你只要画一个很复杂的canvas那么得出的image总是存在像素级别的误差。考虑到爬虫代码都是统一的就算起selenium也是ghost的因此指纹一般都是一致的因此绕过几率非常低。<br><br>但是这个东西天生有两个缺陷。第一是无法验证合法性。当然了你可以用非对称加密来保证合法但是这个并不靠谱。其次canvas的冲突概率非常高远远不是作者宣称的那样冲突率极低。也许在国外冲突是比较低因为国外的语言比较多。但是国内公司通常是IT统一装机无论是软件还是硬件都惊人的一致。我们测试canvas指纹的时候在携程内部随便找了20多台机器得出的指纹都完全一样一丁点差别都没有。因此有些“高级技巧”其实一点都不实用。<br><br>浏览器指纹技术常用于客户端跟踪及反机器人的场景。核心思路是, 不同浏览器、操作系统、以及操作系统环境会使得canvas的同一绘图操作流程产生不同的结果。如果是相同的运行环境同一套Canvas操作流程会产生相同的结果。 浏览器指纹的优势是不需要浏览器保持本地状态,即可跟踪浏览器。 由于国内特色的Ghost系统安装这种方式的误杀率并不低。
以上是第一阶段的安全性总结后期应该会更新App逆向、防重放、Canvas 反爬虫技术方案做深入探讨等)。
补充:
- 关于 Hybrid 的更多内容,可以看看这篇文章 [Awesome Hybrid](https://github.com/FantasticLBP/knowledge-kit/blob/master/%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%20iOS/1.46.md)
- [基于canvas绘图的网页信息防采集技术研究](https://www.doc88.com/p-9186178372509.html)

72
Chapter1 - iOS/1.57.md Normal file
View File

@@ -0,0 +1,72 @@
# 自动布局
## 1. iOS开发之AutoLayout中的Content Hugging Priority和 Content Compression Resistance Priority解析
- Content Hugging Priority直译成中文就是“内容拥抱优先级”从字面意思上来看就是两个视图谁的“内容拥抱优先级”高谁就优先环绕其内容。稍后我们会根据一些示例进行介绍。
- Content Compression Resistance Priority该优先级直译成中文就是“内容压缩阻力优先级”。也就是视图的“内容压缩阻力优先级”越大那么该视图中的内容越难被压缩。而该优先级小的视图则内容优先被压缩。稍后我们也会通过相应的实例来看一下这个优先级的具体表现。
这两个属性是可以在Storyboard中直接设置的选中要设置的控件在右边约束一栏里边就有Content Hugging Priority以及Content Compression Resistance Priority的设置地方。Content Hugging Priority的水平和竖直方向的默认值都是250而Content Compression Resistance Priority的水平和竖直的默认值是750。我们可以在此对该值进行设置。
假如要实现界面上 Label1、Label2、Label3。Label1宽度固定Label3右侧对齐且Label3必须显示完全Label2距离左侧5px右侧和Label3连在一起当Label3文字较多的时候Label2显示不全显示省略号Label3文字较少则Label2完全显示
![Label3文字较多](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-06-autolayout1.jpg)
![Label3文字较少](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-06-autolayout2.jpg)
下面是代码实现。
```Objective-c
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
label1.text = @"生生世世";
label1.font = [UIFont systemFontOfSize:15];
label1.textColor = [UIColor brownColor];
label1.textAlignment = NSTextAlignmentLeft;
label1.backgroundColor = [UIColor yellowColor];
[self.view addSubview:label1];
[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view);
make.top.equalTo(self.view).offset(100);
make.width.mas_equalTo(64);
make.height.mas_equalTo(30);
}];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
label2.text = @"杭城小刘杭城小刘杭城小刘杭城小刘杭城小刘杭城小刘杭城小刘";
label2.textColor = [UIColor purpleColor];
label2.textAlignment = NSTextAlignmentLeft;
label2.backgroundColor = [UIColor grayColor];
[self.view addSubview:label2];
// [label2 setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
[label2 setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(label1.mas_right).offset(5);
make.top.equalTo(self.view).offset(100);
make.height.mas_equalTo(30);
}];
UILabel *label3 = [[UILabel alloc] initWithFrame:CGRectZero];
label3.text = @"刘";
label3.textColor = [UIColor redColor];
label3.textAlignment = NSTextAlignmentRight;
label3.backgroundColor = [UIColor blueColor];
[self.view addSubview:label3];
[label3 setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
// [label3 setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
[label3 mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.view).offset(-10);
make.top.equalTo(self.view).offset(100);
make.height.mas_equalTo(30);
make.left.equalTo(label2.mas_right);
}];
```

21
Chapter1 - iOS/1.58.md Normal file
View File

@@ -0,0 +1,21 @@
# Swift 版本迁移问题总结
> 工程中存在一部分代码逻辑是 Swift 实现的,每次 Swift 版本升级、Xcode 版本升级或许意味着你需要对 Swfit 实现的这部分代码进行升级改动,此篇文章就记录 Swift 每次升级时踩过的坑
## Swift 5 && Xcode 10.2 的踩坑
1. 一部分改动是由于系统通知的名称改变造成的
| 改动前 | 改动后 |
|:--:|:--:|
| .UIKeyboardWillShow | UIResponder.keyboardWillShowNotification |
| UIAlertControllerStyle.alert | UIAlertController.Style.alert |
| UIAlertActionStyle.cancel | UIAlertAction.Style.alert |
| UIControlState.normal | UIControl.State.normal |
| UIEdgeInsetsMake(0, 20, 0, 20) | UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) |
| XQNetworkingManager.jsonManager | XQNetworkingManager.useJSONSerializer |
| NSNotification.Name.UITextFieldTextDidChange | UITextField.textDidChangeNotification |
2. 当你修改完语法问题的时候,编译工程,发现还是存在一些问题。大体意思是说 Swift 的编译版本不再支持。所以我们需要选中 targets ,切换到 「Build Settings」 下面,搜索 「Swift Language Version」在后面勾选合适的 Swift 版本。在这里我选择了 Swift 5
$$

20
Chapter1 - iOS/1.59.md Normal file
View File

@@ -0,0 +1,20 @@
# 零散知识
1. 为什么线上代码尽量不要使用 NSLog("%@", person)?
因为NSLog使用%@输出本质上是调用了对象的 description 方法,所以代码中存在大量的 NSLog 的时候。会造成性能问题。
解决方案使用宏定义判断当前代码的运行环境DEBUG 模式下才输出 NSLog否则就空实现。
```objective-c
#ifdef DEBUG
///一个区分开发和线上环境的Log。NSLog的本质是调用对象方法的 description 方法所以线上代码使用NSLog会造成性能和安全问题
#define SafeLog(...) NSLog(__VA_ARGS__)
#else
#define SafeLog(...)
#endif
```
2. 如果我们想在某个函数或者方法参数指定参数的类型的话,使用 `id` 编译器不会在编译阶段对其真正的类型做检查,如果我们想指定为一个类的对象或者一个类的子类对象的时候可以使用 `__kindof` 。
```Objective-c
- (void)test:(__kindof UIView *)view
{
view.subviews;
}
```

View File

@@ -1,6 +1,4 @@
### 双列表联动
# 双列表联动
> 用过了那么多的外卖App总结出一个规律那就是“所有的外卖App都有双列表联动功能”。哈哈哈哈这是一个玩笑。
@@ -143,6 +141,6 @@
##### 效果图
![效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2017-09-24%2015_35_52.gif)
![效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-09-24%2015_35_52.gif)
附上Demo[Demo](https://github.com/FantasticLBP/BlogDemos)

688
Chapter1 - iOS/1.60.md Normal file
View File

@@ -0,0 +1,688 @@
# iOS 瘦身之道
App 的包大小做优化的目的就是为了节省用户流量,提高用户的下载速度,也是为了用户手机节省更多的空间。另外 App Store 官方规定 App 安装包如果超过 150MB那么不可以使 OTAover-the-air环境下载也就是只可以在 WiFi 环境下载,企业或者独立开发者万万不想看到这一点。免得失去大量的用户。
同时如果你的 App 需要适配 iOS7、iOS8 那么官方规定主二进制 text 段的大小不能超过 60MB。如果不能满足这个标准则无法上架 App Store。
另一种情况是 App 包体积过大,对用户更新升级率也会有很大影响。
所以应用包的瘦身迫在眉睫。
## 1. App Thinning
App Thinning 是指 iOS9 以后引入的一项优化,官方描述如下
> The App Store and operating system optimize the installation of iOS, tvOS, and watchOS apps by tailoring app delivery to the capabilities of the users particular device, with minimal footprint. This optimization, called app thinning, lets you create apps that use the most device features, occupy minimum disk space, and accommodate future updates that can be applied by Apple. Faster downloads and more space for other apps and content provides a better user experience.
Apple 会尽可能,自动降低分发到具体用户时,所需要下载的 App 大小。其中包含三项主要功能Slicing、Bitcode、On-Demand Resources。
App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术,主要为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户设备存储空间。
### 1.1 Slicing
![Slicing](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppSlicing.jpeg)
当向 App Store Connect 上传 .ipa 后App Store Connect 构建过程中,会自动分割该 App创建特定的变体variant以适配不同设备。然后用户从 App Store 中下载到的安装包,即这个特定的变体,这一过程叫做 Slicing。
> Slicing 是创建、分发不同变体以适应不同目标设备的过程
而变体之间的差异又具体体现在架构和资源上。换句话说App Slicing 仅向设备传送与之相关的资源(取决于屏幕分辨率、系统架构等等)
其中2x 和 3x 的细分,要求图片在 **Assets** 中管理。Bundle 内的则会同时包含。
![变体](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppVariant.jpeg)
### 1.2 Bitcode
> Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the App Store.
Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App Store Connect 上被重新编译和链接,进而对可执行文件做优化。这部分都是在服务端自动完成的。所以假如以后 Apple 新推出了新的 CPU 架构或者以后 LLVM 推出了一系列优化我们不需要重新为其发布新的安装包了。Apple Store 会为我们自动完成这步。然后提供对应的 variant 给具体设备
对于 iOS 而言Bitcode 是可选的Xcode7 以后创建的新项目默认开启watchOS、tvOS 则是必须的。
开启位置Build Settings -> Enable Bitcode -> 设置为 YES
开启 Bitcode有这么2点需要注意
- 全部都要支持。我们所依赖的静态库、动态库、Cocoapods 管理的第三方库,都需要开启 Bitcode。否则会编译失败
- 奔溃定位。开启 Bitcode 后最终生成的可执行文件是 Apple 自动生成的,同时会产生新的符号表文件,所以我们无法使用自己包生成的 dYSM 符号化文件来进行符号化。
> For Bitcode enabled builds that have been released to the iTunes store or submitted to TestFlight, Apple generates new dSYMs. Youll need to download the regenerated dSYMs from Xcode and then upload them to Crashlytics so that we can symbolicate crashes.For Bitcode enabled apps, ensure that you have checked “Include app symbols for your application…” so that we can provide the most accurate crash reports.
上面是 fabric 中关于 Downloading Bitcode dYSMs 的描述:
在上传到 App Store 时需勾选“Includ app symbols for your application...”。勾选之后 Apple 会自动生成对应的 dYSM然后可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下载对应的 dYSM 来进行符号化
![App Connect-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppConnectYSM.jpeg)
![Xcode-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-XcodedYSM.jpeg)
那么 Bitcode 会对 App Thining 有什么作用?
在 New Features in Xcode7 中有这么一段描述:
> Bitcode. When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary.
App Store 会再按需将这个 bitcode 编译进 32/64 位的可执行文件。
所以网上铺天盖地地说 Bitcode 完成了具体架构的拆分,从而实现瘦包
### 1.3 on-Demand Resources
on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
![on-DemandResources](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-on-DemandResources.png)
应用场景:相机应用的贴纸或者滤镜、关卡游戏等
如需支持 iOS9 以下系统,那么无法使用这个功能,否则上传会失败
## 2 包体积
2个概念
- .ipa (iOS Application Package)iOS 应用程序归档文件,即提交到 App Store Connect 的文件
- .app Application应用的具体描述即安装到 iOS 设备上的文件
当我们拿到 Archive 后的 .ipa使用解压软件打开后Payload 目录下存放的就是 .app 文件,二者大小相当
包体积,评判标准是以 App Store 上看到的为准。但是上传到 App Store Connect 处理完后,会自动帮我们生成具体设备上看到的大小。如下:
![App Store 包大小](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppVolume.jpeg)
这其中又可以分为2类 Universal 和具体设备
Universal 指通用设备,即未应用 App slicing 优化,同时包含了所有架构、资源。所以包体积会比较大
观察 .ipa 的大小和 Universal 对应的包大小相当,稍微小一点,因为 App Store 对 .ipa 做了加密处理
有时候下载 App 会提示“此项目大于 150MB除非此项目支持增量下载否则您必须连接至 WiFi 才能下载”。150MB 针对的是下载大小。
- 下载大小:通过 WiFi 下载的压缩 App 大小
- 安装大小:此 App 将在用户设备上占用磁盘空间的大小
所以我们要瘦包,关键在于减小 .app 文件的大小。
### 2.1 Architectures
如果不支持32位以及 iOS8 ,去掉 armv7 ,可执行文件以及库会减小,即本地 .ipa 也会减小
### 2.2 Resources
资源的优化也就是平时的细心与审查。
图片、内置素材、Bundle、多语言、Json、字体、脚本、Plist、音频
图片Assets.car
Bundle: 非放在 Asset Catlog 中管理的图片资源。包括 Bundle散落的 png、jpg 等
瘦包具体的方式:
- 无用资源的删除
- 重复文件的删除
- 大文件压缩
- 图片管理方式规范
- on-Demand Resource游戏的、前置关卡依赖、滤镜App 等的依赖资源,建议用这种方式动态下载图片资源)
#### 2.2.1 无用文件的删除
无用文件主要包含:无用图片、无用非图片部分。
非图片部分:资源较少,使用方式固定。比如音频、字体。需要手动排查
图片部分:主要使用一个开源的 Mac App [LSUnusedResources](https://github.com/tinymind/LSUnusedResources) 进行冗余图片的排查。
删除无用的图片过程可以概括为下面6步
1. 通过 find 命令获取 App 安装包中的所有资源文件
2. 设置用到的资源类型。比如 gif、jpg、jpeg、png、webp
3. 使用正则匹配出在源码中使用到的资源名,比如 pattern = @"@"(.+?)""
4. 使用 find 命令找到篇所有资源文件再去源码中找到使用到的资源文件2个集合的差集就是无用资源了。
5. 确认无用资源后可以使用 NSFileManager 进行文件的删除。
如果不想重新写一个工具,那么可以直接使用开源的工具 LSUnusedResources
但是存在一点问题。会出现误报,因为不同的项目,图片使用方式不一样。
```
- (BOOL)containsSimilarResourceName:(NSString *)name {
NSString *regexStr = @"([-_]?\\d+)";
NSRegularExpression* regexExpression = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:nil];
NSArray* matchs = [regexExpression matchesInString:name options:0 range:NSMakeRange(0, name.length)];
//...
}
```
源码中的正则表达式处理的情况并不是很准确。可以根据自己的情况修改正则即可
#### 2.2.2 图片资源的压缩
删除了无用的资源,那么对于资源这块还是有操作的空间的,比如图片资源的压缩。目前压缩比较好的方案就是 WebP它是谷歌公司的一个开源项目。
WebP 的优势:
- 压缩率高。支持有损和无损2种方式比如将 Gif 图可以转换为 Animated WebP有损模式下可以减小 64%,无损模式下可以减小 19%
- WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够出现毛边。
Google 公司在开源 WebP 的同时,还提供了一个图片压缩工具 [cwebp](https://developers.google.com/speed/webp/docs/precompiled)。
压缩完之后使用 WebP 格式的图片还需使用 libwebp 进行解析,参考这个[Demo](https://github.com/carsonmcdonald/WebP-iOS-example)。
缺点WebP 在 CUP 消耗和解码时间上会比 PNG 高2倍所以我们做选择的时候需要取舍。
#### 2.2.3 重复文件删除
重复文件,即两个内容完全一致的文件。但是文件命名不一样。
借助 [fdupes](https://github.com/adrianlopezroche/fdupes) 这个开源工具,校验各资源的 MD5。
fdupes 是 Linux 下的一个工具,它由 Adrian Lopez 用 C 语言编写并基于 MIT 许可证发行该应用程序可以在指定的目录及子目录中查找重复的文件。fdupes 通过对比文件的 MD5 签名以及逐字节比较文件来识别重复内容fdupes 有各种选项,可以实现对文件的列出、删除、替换为文件副本的硬链接等操作。
文件对比从以下顺序开始:
大小对比 > 部分 MD5 签名对比 > 完整 MD5 签名对比 > 逐字节对比
执行结束后会在命令行展示出来,所以需要我们人工将这些文件确认对比后删除掉。
#### 2.2.4 大文件压缩
图片本身的压缩,建议使用 ImageOptim。它整合了 Win、Linux 上诸多著名图片处理工具的特色,比如 PNGOUT、AdvPNG、Pngcrush、OptiPNG、JpegOptim、Gifsicle 等。
Bundle 内的图片资源必须压缩,因为 Xcode 并不会对其进行压缩。所以做好将图片都用 Assets 管理。
Xcode 提供给我们2个编译选项来帮助压缩图像
- Compress PNG Files: 打包的时候自动对图片进行无损压缩。使用的工具为 pngcrush压缩比蛮高。
- Remove Text Medadata From PNG Files移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息
#### 2.2.5 图片管理方式规范
##### 2.2.5.1 主工程中的图片管理
工程中所有使用的 Asset Catlog 管理的图片(在 .xcassets 文件夹下)最终都会输出到 Asset.car 内。不在 Asset.car 内的都归为 Bundle 管理。
- xcassets 里面的图片。只能通过 imageNamed 加载。 Bundle 里面的图片还可以通过 imageWithContentsOfFile 等方式
- xcassets 里面的 @2x@3x 会根据具体设备分发不会同时包含。Bundle 都包含(不进行 App Slicing
- xcassets 内可以对图片进行 Slicing即裁剪和拉伸、Bundle 不支持
- Bundle 内支持多语言Images.xcassets 不支持
> 使用 imageNamed 创建的 UIImage 会被立即加入到 NSCache 中(解码后的 Image Buffer直到收到内存警告的时候才会释放不使用的 UIImage。而 imageWithContentsOfFile 会每次重新申请内存,相同图片不会缓存,所以 xcassets 内的图片,加载后会产生缓存
综上:常用的、较小的图建议存放在 Images.xcassets 内管理。大图放在 Bundle 内管理。
这里讲一个插曲了,曾经很多文章都在谈一个结论,那就是「图片放在 Images.xcassets 里面更加快速且节省空间,直接放在 bundle 里面会比较慢」。我做过实验,实验环境和结论如下。使用 Instruments 测量耗时。
<details>
<summary>点击展开</summary>
```Objective-C
//实验1
NSMutableArray *images = [NSMutableArray array];
for (NSInteger index = 0; index < 10; index++) {
UIImage *image = [UIImage imageNamed:@"icon-iOS"];
[images addObject:image];
}
self.imageView.image = images.lastObject;
//实验2
NSMutableArray *images = [NSMutableArray array];
for (NSInteger index = 0; index < 10; index++) {
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"iOS" ofType:@"png"];
[UIImage imageNamed:@"icon-iOS"];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
[images addObject:image];
}
self.imageView.image = images.lastObject;
```
</details>
Timeprofile-imageNamedFromAssets
![Timeprofile-imageNamedFromAssets](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Timeprofile-imageNamedFromAssets.png)
TimeProfile-imageWithContentsOfFile
![TimeProfile-imageWithContentsOfFile](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-TimeProfile-imageWithContentsOfFile.png)
Timeprofile-UIImageNamedFromFolder
![Timeprofile-UIImageNamedFromFolder](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png)
Images.xcassets
- 图片大小要精确,不要出现图片太大的情况
- 不要存放大图,不然会产生缓存
- 不要存 jpg 图片,打包会变大
- 图片不需要额外压缩(有人做过实验,对放入 assets 里面的图片进行压缩后打包发现包体积反而增大,怀疑是 Xcode 的编译选项 Compress PNG Files 自动对图片进行压缩2种压缩起了冲突反而增大
##### 2.2.5.2 各个 pod 库中的图片管理
CocoPods 中两种资源引用方式介绍下:
- resource_bundles
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute.
允许定义当前的 pod 库的最远包的名称和文件。用 hash 形式声明key 是 bundle 的名称value 是需要包含文件的通配 patterns
CocoPods 官方强烈推荐该方法引用资源,因为 key-value 可以避免相同资源的名称冲突
- resources
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode.
使用该方法引用资源,被指定的资源会被拷贝进 target 工程的 main bundle 中。
说说项目中的情况吧:在工程中之前是通过 resource_bundles 引用资源的。资源是放在 Resources 目录下的图片引用。查询资料后说「如果图片资源放到 .xcasset 里面 Xcode 会帮我们自动优化、可以使用 Slicing 等(这里不仅仅指的是 resource_bundle 下的 xcassets」。所以动手将各个 Pod 库里面的图片全都通过 Assets Catalog 的方式进行处理。
![Pod组件库图片处理前后对比](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-08-Cocopod-Assets.png)
步骤:
- 在各个 Pod 组件库里面的 Resources 目录下新建 Asset Catalog 文件,命名为 Images.xcassets
- 将 Resources 里面零散的图片资源拖进 Images.xcassets 里面
- 修改每个组件库的 podspec 文件
<details>
<summary>点击展开</summary>
```
s.resource_bundles = {
'XQ_UI' => ['XQ_UI/Assets/*.xcassets']
}
</details>
```
- 主工程执行 pod install
话说 `resources` 和 `resource_bundles` 都可以使用 Asset Catalog那么有何区别
- resources 只会将资源文件 copy 到 target 工程,最后和 target 工程的图片资源以及同样使用该方式的 Pod 库的图片资源共同打包到一个 `Assets.car` 中。因此图片资源会有混乱的可能。
- resource_bundles 会生成一个你在 `podspec` 中指定名称的 bundle且在 bundle 中也会生成一个 Assets.car。所以图片是肯定不会混乱的但是图片的访问方式需要注意。
解决方法:为每个 pod 新建一个图片的分类,比如 UIImage+XQUIModule。然后访问图片的时候通过 `[UIImage xquiModuleImageNamed:@"pull"]` 访问。
<details>
<summary>点击展开</summary>
```Objective-C
#import "UIImage+XQUIModule.h"
#import <SDGBase/UIImage+Bundle.h>
@implementation UIImage (XQUIModule)
+ (nonnull UIImage *)xquiModuleImageNamed:(nonnull NSString *)name
{
return [UIImage imageNamed:name inBundleName:@"XQ_UI"];
}
@end
//UIImage+Bundle.m
#import "UIImage+Bundle.h"
@implementation UIImage (Bundle)
+ (nullable UIImage *)imageNamed:(NSString *)name inBundleName:(nullable NSString *)bundleName {
NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:bundleName withExtension:@"bundle"]];
return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil];
}
@end
```
</details>
#### 2.2.6 矢量图的使用
事实上,对于 App 里面的单色图标,比如左上角的返回按钮、底部的 tabBar等只要是单色的纯色图标都是可以使用矢量图代替的比如 PDF、ttf 字体图标等。这样就不需要添加 @2x、@3x 图标,节省了空间。
iOS 中如何使用 ttf 矢量图,可以查看这个 [Repo](https://github.com/FantasticLBP/IconFont_Demo)
## 3. Executable file
### 3.1 编译选项优化
#### 3.1.1 Generate Debug Symbols
> Enables or disables generation of debug symbos. When debug symbols are enabled, the level of detail can be controller by the build 'Level of Debug Symbols' Setting.
调试符号是在编译时形成的。当 Generate Debug Symbols 选项为 YES 的时,每个源文件在编译成 .o 文件时,编译参数多了 -g 和 -gmodules 两项。打包会生成 symbols 文件。设置为 NO 则 ipa 中不会生成 symbol 文件,可以减少 ipa 大小。但会影响到崩溃的定位。保持默认的开启,不做修改。
#### 3.1.2 Asset Catalog Compiler
optimization 选项设置为 space 可以减少包大小
默认选项,不做修改。
#### 3.1.3 Dead Code Stripping
> For statically linked executables, dead-code stripping is the process of removing unreferenced code from the executable file. If the code is unreferenced, it must not be used and therefore is not needed in the executable file. Removing dead code reduces the size of your executable and can help reduce paging.
删除静态链接的可执行文件中未引用的代码
Debug 设置为 NO Release 设置为 YES 可减少可执行文件大小。
Xcode 默认会开启此选项C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的。因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。
默认选项,不做修改。
#### 3.1.4 Apple Clang - Code Generation
Optimization Level 编译参数决定了程序在编译过程中的两个指标:编译速度和内存的占用,也决定了编译之后可执行结果的两个指标:速度和文件大小。
Build Settings -> code Generation -> Optimization Level
默认情况下Debug 设定为 None[-O0] Release 设定为 Fastest,Smallest[-Os]。
- None[-O0]。 Debug 默认级别。不进行任何优化,直接将源代码编译到执行文件中,结果不进行任何重排,编译时比较长。主要用于调试程序,可以进行设置断点、改变变量 、计算表达式等调试工作。
- Fast[-O,O1]。最常用的优化级别,不考虑速度和文件大小权衡问题。与-O0级别相比它生成的文件更小可执行的速度更快编译时间更少。
- Faster[-O2]。在-O1级别基础上再进行优化增加指令调度的优化。与-O1级别相它生成的文件大小没有变大编译时间变长了编译期间占用的内存更多了但程序的运行速度有所提高。
- Fastest[-O3]。在-O2和-O1级别上进行优化该级别可能会提高程序的运行速度但是也会增加文件的大小。
- Fastest Smallest[-Os]。Release 默认级别。这种级别用于在有限的内存和磁盘空间下生成尽可能小的文件。由于使用了很好的缓存技术,它在某些情况下也会有很快的运行速度。
- Fastest, Aggressive Optimization[-Ofast]。 它是一种更为激进的编译参数, 它以点浮点数的精度为代价。
默认选项,不做修改。
#### 3.1.5 Swift Compiler - Code Generation
Xcode 9.3 版本之后 Swift 编译器提供了新的 Optimization Level 选项来帮助减少 Swift 可执行文件的大小:
- No optimization[-Onone]:不进行优化,能保证较快的编译速度。
- Optimize for Speed[-O]:编译器将会对代码的执行效率进行优化,一定程度上会增加包大小。
- Optimize for Size[-Osize]:编译器会尽可能减少包的大小并且最小限度影响代码的执行效率。
> We have seen that using -Osize reduces code size from 5% to even 30% for some projects. But what about performance? This completely depends on the project. For most applications the performance hit with -Osize will be negligible, i.e. below 5%. But for performance sensitive code -O might still be the better choice.
官方提到,-Osize 根据项目不同,大致可以优化掉 5% - 30% 的代码空间占用。 相比 -0 来说,会损失大概 5% 的运行时性能。 如果你的项目对运行速度不是特别敏感,并且可以接受轻微的性能损失,那么 -Osize 是首选。
除了 -O 和 -Osize 还有另外一个概念也值得说一下。 就是 Single File 和 Whole Module 。 在之前的 XCode 版本,这两个选项和 -O 是连在一起设置的Xcode 9.3 中,将他们分离出来,可以独立设置:
Single File 和 Whole Module 这两个模式分别对应编译器以什么方式处理优化操作。
- Single File逐个文件进行优化它的好处是对于增量编译的项目来说它可以减少编译时间对没有更改的源文件不用每次都重新编译。并且可以充分利用多核 CPU并行优化多个文件提高编译速度。但它的缺点就是对于一些需要跨文件的优化操作它没办法处理。如果某个文件被多次引用那么对这些引用方文件进行优化的时候会反复的重新处理这个被引用的文件如果你项目中类似的交叉引用比较多就会影响性能。
- Whole Module 将项目所有的文件看做一个整体,不会产生 Single File 模式对同一个文件反复处理的问题,并且可以进行最大限度的优化,包括跨文件的优化操作。缺点是,不能充分利用多核处理器的性能,并且对于增量编译,每次也都需要重新编译整个项目。
如果没有特殊情况,使用默认的 Whole Module 优化即可。 它会牺牲部分编译性能,但的优化结果是最好的。
故,在 Relese 模式下 -Osize 和 Whole Module 同时开启效果会最好!
#### 3.1.6 Strip Symbol Information
1、Deployment Postprocessing
2、Strip Linked Product
3、Strip Debug Symbols During Copy
4、Symbols hidden by default
设置为 YES 可以去掉不必要的符号信息,可以减少可执行文件大小。但去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。
Symbols Hidden by Default 会把所有符号都定义成”private extern”详细信息见官方文档。
Release 设置为 YESDebug 设置为 NO。
#### 3.1.7 Exceptions
在 iOS微信安装包瘦身 一文中,有提到:
> 去掉异常支持Enable C++ Exceptions和Enable Objective-C Exceptions设为NO并且Other C Flags添加-fno-exceptions可执行文件减少了27M其中__gcc_except_tab段减少了17.3M__text减少了9.7M,效果特别明显。可以对某些文件单独支持异常,编译选项加上-fexceptions即可。但有个问题假如ABC三个文件AC文件支持了异常B不支持如果C抛了异常在模拟器下A还是能捕获异常不至于Crash但真机下捕获不了有知道原因可以在下面留言。去掉异常后Appstore 后续几个版本 Crash 率没有明显上升。
个人认为关键路径支持异常处理就好像启动时NSCoder读取setting配置文件得要支持捕获异常等等
看这个优化效果,感觉发现了新大陆。关闭后验证.. 毫无感知,基本没什么变化。
可能和项目中用到比较少有关系。故保持开启状态。
#### 3.1.8 Link-Time Optimization
Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码;这个优化也是高效的,因为可以在全局视角下优化代码。
苹果在 WWDC 2016 中明确提出了这个优化的概念Whats New in LLVM。并且说在苹果内部已经广泛地使用这个优化方法进行编译。
它的优化主要体现在如下几个方面:
1. 多余代码去除Dead code elimination如果一段代码分布在多个文件中但是从来没有被使用普通的 -O3 优化方法不能发现跨中间代码文件的多余代码因此是一个“局部优化”。但是Link-Time Optimization 技术可以在 link 时发现跨中间代码文件的多余代码。
2. 跨过程优化Interprocedural analysis and optimization这是一个相对广泛的概念。举个例子来说如果一个 if 方法的某个分支永不可能执行,那么在最后生成的二进制文件中就不应该有这个分支的代码。
3. 内联优化Inlining optimization内联优化形象来说就是在汇编中不使用 “call func_name” 语句,直接将外部方法内的语句“复制”到调用者的代码段内。这样做的好处是不用进行调用函数前的压栈、调用函数后的出栈操作,提高运行效率与栈空间利用率。
在新的版本中,苹果使用了新的优化方式 Incremental大大减少了链接的时间。建议开启。
总结,开启这个优化后,一方面减少了汇编代码的体积,一方面提高了代码的运行效率。
### 3.2 代码瘦身
代码的优化,即通过删除无用类、无用方法、重复方法等,来达到可执行文件大小的减小。
而如何筛选出符合条件的无用类、方法则需要通过一些工具来完成fui
扫描无用代码的基本思路都是查找已经使用的方法/类和所有的类/方法,然后从所有的类/方法当中剔除已经使用的方法/类剩下的基本都是无用的类/方法,但是由于 Objective-C 是动态语言,可以使用字符串来调用类和方法,所以检查结果一般都不是特别准确,需要二次确认。目前市面上的扫描的思路大致可以分为 3 种:
- 基于 Clang 扫描
- 基于可执行文件扫描
- 基于源码扫描
先谈几个概念。
可执行文件就是 **Mach-O** 文件,其大小是油代码量决定的,通常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。找到无用代码的过程类比找到无用图片的思路。
- 找到类和方法的全集
- 找到使用过的类和方法集合
- 取2者差集得到无用代码集合
- 工程师确认后,删除即可
LinkMap 文件分为3部分Object File、Section、Symbols。
![LinkMap结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Structure.png)
- Object File包含了代码工程的所有文件
- Section描述了代码段在生成的 Mach-O 里的偏移位置和大小
- Symbols会列出每个方法、类、Block以及它们的大小
先说说如何快速找到方法和类的全集?
我们可以通过 **LinkMap** 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里面的 **Write Link Map File** 设置为 YES然后指定 **Path to Link Map File** 的路径就可以得到每次编译后的 LinkMap 文件了。
![Xcode中设置获取LinkMap](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Xcode.png)
#### 3.2.1 基于 clang 扫描
基本思路是基于 clang AST。追溯到函数的调用层级记录所有定义的方法/类和所有调用的方法/类,再取差集。具体原理参考 如何使用 Clang Plugin 找到项目中的无用代码,目前只有思路没有现成的工具。
#### 3.2.2 基于可执行文件扫描LinkMap 结合 Mach-O 找无用代码)
上面我们得知可以通过 LinkMap 统计出所有的类和方法,还可以清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
![LinkMap-Object file](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-ObjectFile.png)
![LinkMap-Sections](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Sections.png)
![LinkMap-Symbols](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Symbols.png)
得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSend 在 Mach-O 文件里是通过 **_objc_selrefs** 这个 **section** 来获取 selector 这个参数的。
所以_objc_selrefs 里的方法一定是被调用了的。**_objc_classrefs** 里是被调用过的类, **objc_superrefs** 是调用过 super 的类(继承关系)。通过 _objc_classrefs 和 _objc_superrefs,我们就可以找出使用过的类和子类。
那么Mach-O 文件中的 _objc_selrefs、_objc_classrefs、_objc_superrefs 如何查看呢?
1. 使用 otool 等命令逆向可执行文件中引用到的类/方法和所有定义的类/方法然后计算差集。具体参考iOS微信安装包瘦身目前只有思路没有现成的工具。
2. 使用 [MachOView](https://github.com/gdbinit/MachOView) 查看。但是这个项目运行不起来,这个新的 [Repo](https://github.com/fangshufeng/MachOView) 可以运行起来。
下面举例说明:
前置条件:先运行项目,在生成的 Products 目录下的 BridgeLabiPhone.app 解压,取出对应的和工程同名的 BridgeLabiPhone。然后运行上面的 Github 项目。可以看到运行了一个 Mac App。点击顶部的菜单栏里面的 File->Open。选择电脑上的 BridgeLabiPhone.app 选择里面的 BridgeLabiPhone。见下图
![Mach-O-inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Mach-O-Inspect.png)
由于 Objective-C 是一门动态语言所以检测出的结果仍旧需要我们2次确认。
#### 3.2.3 基于源码扫描
一般都是对源码文件进行字符串匹配。例如将 A *a、[A xxx]、NSStringFromClass("A")、objc_getClass("A") 等归类为使用的类,@interface A : B 归类为定义的类,然后计算差集。
基于源码扫描 有个已经实现的工具 - fui但是它的实现原理是查找所有 #import "A" 和所有的文件进行比对,所以结果相对于上面的思路来说可能更不准确。
#### 3.2.4 通过 AppCode 查找无用代码
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。
![AppCode-code inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-CodeClean.png)
说明AppCode检测出了实际上需要的大部分场景的问题但是由于 Objective-C 是一门动态性语言,所以 AppCode 检测出无用的方法等都需要工程师自己再次确认后删除。(在我们的工程中有一些和 H5 交互的桥接方法,因此 AppCode 视为 Unused Method但是你删除的话那就自己哭去吧 😭)。实际经验告诉我,使用 AppCode 的时候如果工程比较大,则整个 code inspect 会非常耗时(给你打个预防针哦,笔芯)
- 无用类Unused class 是无用类Unused import statement 是无用类引入声明Unused property 是无用的属性;
- 无用方法Unused method 是无用的方法Unused parameter 是无用参数Unused instance variable 是无用的实例变量Unused local variable 是无用的局部变量Unused value 是无用的值;
- 无用宏Unused macro 是无用的宏。
- 无用全局Unused global declaration 是无用全局声明。
#### 3.2.5 运行时真正检测类是否用过
通过上述手段找到并删除了无用代码。App 不断上线迭代蛮多代码都不会被调用了(业务被砍掉了)。这种方式下这些无用的代码也是可以被删除的。
通过 Objective-C 的 runtime 源码,我们可以找到如何判断一个类是否初始化过的函数。
```Objective-c
#define RW_INITIALIZED (1<<29)
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
```
isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信息里, flags 的 1<<29 位记录的就是这个类是否初始化了的信息,而 flags 的其他位记录的信息,可以查看 rumtime 的源码
```Objective-c
// 类的方法列表已修复
#define RW_METHODIZED (1<<30)
// 类已经初始化了
#define RW_INITIALIZED (1<<29)
// 类在初始化过程中
#define RW_INITIALIZING (1<<28)
// class_rw_t->ro 是 class_ro_t 的堆副本
#define RW_COPIED_RO (1<<27)
// 类分配了内存,但没有注册
#define RW_CONSTRUCTING (1<<26)
// 类分配了内存也注册了
#define RW_CONSTRUCTED (1<<25)
// GCclass 有不安全的 finalize 方法
#define RW_FINALIZE_ON_MAIN_THREAD (1<<24)
// 类的 +load 被调用了
#define RW_LOADED (1<<23)
```
既然可以在运行的期间知道类是否初始化了,那么就可以找出哪些类未初始化,即可以找到在真实环境里面没有用到的类并删除掉。
## 4. App Extension
App Extension 的占用,都放在 Plugin 文件夹内。它是独立打包签名,然后再拷贝进 Target App Bundle 的。
关于 Extension有两个点要注意
静态库最终会打包进可执行文件内部,所以如果 App Extension 依赖了三方静态库,同时主工程也引用了相同的静态库的话,最终 App 包中可能会包含两份三方静态库的体积。
动态库是在运行的时候才进行加载链接的,所以 Plugin 的动态库是可以和主工程共享的,把动态库的加载路径 Runpath Search Paths 修改为跟主工程一致就可以共享主工程引入的动态库。
所以,如果可能的话,把相关的依赖改成动态库方式,达到共享。
## 5. 静态库瘦身
项目中都会引入第三方静态库。通过 lipo 工具可以查看支持的指令集,比如查看微博 SDK
终端切换到微博 SDK 的目录下执行下面命令
- 静态库指令集信息查看:`lipo -info libname.a(或者libname.framework/libname)`
```Shell
lipo -info libWeiboSDK.a
//Architectures in the fat file: libWeiboSDK.a are: armv7 arm64 i386 x86_64
```
我们知道 i386、x86_64 是模拟器的指令集。所以我们可以模拟器版本的指令集。因为 armv7 也可以兼容 armv7s。所以 armv7s 也可以删除了。只保留 armv7 和 arm64
- 静态库拆分:`lipo 静态库文件路径 -thin CPU架构 -output 拆分后的静态库文件路径`
- 静态库合并:`lipo -create 静态库1文件路径 静态库2文件路径... 静态库n文件路径 -output 合并后的静态库文件径`
```Shell
lipo libWeiboSDK.a -thin armv7 -output libWeiboSDK-armv7.a
lipo libWeiboSDK.a -thin arm64 -output libWeiboSDK-arm64.a
lipo create libWeiboSDK-armv7.a libWeiboSDK-arm64.a -output libWeiboSDK.device.a
```
通过上面的操作我们将静态库里面支持模拟器的指令集给去掉了,所以模拟器是无法跑代码的,如何解决?
1. 平时使用包含模拟器指令集的静态库,在 App 发布的时候去掉
2. 如果使用 Cocoapods 管理可以使用2份 Podfile 文件。一份包含指令集一份不包含,发布的时候切换 Podfile 文件即可。或者一份 Podfile 文件,但是配置不同的环境设置
补充2个说明
1. dSYM 文件
符号表文件 .dSYM 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,实际用于保存调试信息的是 DWARF 文件
- 自动生成。Xcode 会在工程编译或者归档的时候自动生成 .dSYM 文件,在 Buld setting 设置中有开关可以设置去关掉 .dSYM 文件
- 手动生成。通过脚本从 Mach-O 文件中提取出来。
```
$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM
```
该方式通过 dsymutil 工具,从项目编译结果 .app 目录下的 Mach-O 文件中提取出调试符号表文件。Xcode 在归档的时候是通过它生辰的 .dSYM 文件
2. DWARF 文件
DebuggingWith Arbitrary Record Formats 是 ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式,.dSYM 文件中真正保存符号表数据的是 DWARF 文件。DWARF 文件中不同的数据都保存在相应的 section 中。
最后的一个对比效果图:
![瘦身效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-AppThinning-Comparation.png)
总结瘦身技术常见操作就这些但是维持应用包体积的瘦身却是一个观念从日常开发到线上发布都需要有这个意识。这样当你在写代码的时候就会考虑同样一个效果你的具体实现手段是怎么样的。比如为了一个稍微炫酷的效果就要引入一个很大的三方库有了“瘦身”的意识你很大可能就是自己动手撸一个代码。比如一些无用资源的管理方式、有用的图片资源的高效管理方式等等。有了意识行动自然会往这个方面去靠。😂大道理一套一套的。我也不想的毕竟是playboy
其中遇到了一个神奇的问题。lint 的时候看到一些未使用的依赖库。见 [问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md)
**By the way**
如果在应用包瘦身方面有其他的做法,请告知,完善文章。
参考文章:
- [Humble Assets Catalog](http://lingyuncxb.com/2019/04/14/HumbleAssetCatalog/)
- [关于 Pod 库的资源引用 resource_bundles or resources](http://zhoulingyu.com/2018/02/02/pod-resource-reference/)
- 部分图片或者文字内容引用来自网络(若有引用到,请告诉我地址,及时补充)
- 线程、队列、runloop 的关系?主串行队列,在 Mach 内核中创建
- block堆内存中的结构体变量
- 全局并发队列队列FIFO。所以最后加入到全局并发队列中的任务最后执行执行的时候就可以拿到结果

16
Chapter1 - iOS/1.61.md Normal file
View File

@@ -0,0 +1,16 @@
# App 启动时间优化
在看 App 启动时间优化之前先看2个方法 **load****initialize**
load
> Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
当一个类或者它的分类被加载到 Objective-c 的 runtime 中的时候会出发 load 方法。可以实现这个方法去执行一些类特定的行为。
initialize
> Initializes the class before it receives its first message.
当一个类接收到第一条消息的时候会初始化。
load 方法会在类被加载到 runtime 的时候调用。且父类的 load 方法比子类先执行。load 方法只会执行1次。
initialize 方法会在第一次收到消息的时候调用。父类的 initialize 方法比子类先执行。假如有 Person 类,还有一个子类 children。子类第一次收到消息的时候会先调用父类的 initialize然后调用子类的 initialize如果子类没有实现 initialize 那么父类的 initialize 会执行多次。

534
Chapter1 - iOS/1.62.md Normal file
View File

@@ -0,0 +1,534 @@
# OCLint 实现 Code Review - 给你的代码提提质量
工程代码质量,一个永恒的话题。好的质量的好处不言而喻,团队成员间除了保持统一的风格和较高的自我约束力之外,还需要一些工具来统计分析代码质量问题。
本文就是针对 OC 项目,提出的一个思路和实践步骤的记录,最后形成了一个可以直接用的脚本。如果觉得文章篇幅过长,则直接可以下载[脚本](https://github.com/FantasticLBP/knowledge-kit/tree/master/assets/auto_Lint.sh)
> OCLint is a static code analysis tool for improving quality and reducing defects by inspecting C, C++ and Objective-C code and looking for potential problems ...
从官方的解释来看,它通过检查 C、C++、Objective-C 代码来寻找潜在问题,来提高代码质量并减少缺陷的静态代码分析工具
## OCLint 的下载和安装
有3种方式安装分别为 Homebrew、源代码编译安装、下载安装包安装。
区别:
- 如果需要自定义 Lint 规则,则需要下载源码编译安装
- 如果仅仅是使用自带的规则来 Lint那么以上3种安装方式都可以
### 1. Homebrew 安装
在安装前,确保安装了 homebrew。步骤简单快捷
```Shell
brew tap oclint/formulae
brew install oclint
```
### 2. 安装包安装
- 进入 OCLint 在 Github 中的[地址](https://github.com/oclint/oclint/releases),选择 Release。选择最新版本的安装包目前最新版本为oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz
- 解压下载文件。将文件存放到一个合适的位置。(比如我选择将这些需要的源代码存放到 Document 目录下)
- 在终端编辑当前环境的配置文件,我使用的是 zsh所以编辑 .zshrc 文件。(如果使用系统的终端则编辑 .bash_profile 文件)
```Shell
OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
```
- 将配置文件 source 一下。
```Shell
source .zshrc // 如果你使用系统的终端则执行 soucer .bash_profile
```
- 验证是否安装成功。在终端输入 `oclint --version`
### 3. 源码编译安装
- homebrew 安装 CMake 和 Ninja 这2个编译工具
```Shell
brew install cmake ninja
```
- 进入 Github 搜索 OCLintclone 源码
```Shell
gc https://github.com/oclint/oclint
```
- 进入 oclint-scripts 目录,执行 ./make 命令。这一步的时间非常长。会下载 oclint-json-compilation-database、oclint-xcodebuild、llvm 源码以及 clang 源码。并进行相关的编译得到 oclint。且必须使用翻墙环境不然会报 timeout。如果你的电脑支持翻墙环境但是在终端下不支持翻墙可以查看我的这篇[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第六部分%20开发杂谈/6.11.md)
```Shell
./make
```
- 编译结束,进入同级 build 文件夹,该文件夹下的内容即为 oclint。可以看到 `build/oclint-release`。方式2下载的安装包的内容就是该文件夹下的内容。
- cd 到根目录,编辑环境文件,比如我 zsh 对应的 .zshrc 文件。编辑下面的内容
```Shell
OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH
```
- source 下 .zhsrc 文件
```Shell
source .zshrc // source .bash_profile
```
- 进入 `oclint/build/oclint-release` 目录执行脚本
```Shell
cp ~/Documents/oclint/build/oclint-release/bin/oclint* /usr/local/bin/
ln -s ~/Documents/oclint/build/oclint-release/lib/oclint /usr/local/lib
ln -s ~/Documents/oclint/build/oclint-release/lib/clang /usr/local/lib
```
这里使用 ln -s把 lib 中的 clang 和 oclint 链接到 /usr/local/bin 目录下。这样做的目的是为了后面如果编写了自己创建的 lint 规则,不必要每次更新自定义的 rule 库,必须手动复制到 /usr/local/bin 目录下。
- 验证下 OCLint 是否安装成功。输入 oclint --version
![OCLint-验证安装成功](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-15-OCLint-Verify.png)
注意:如果你采用源码编译的时候直接 clone 官方的源码会有问题,编译不过,所以提供了一个可以编译过的[版本](https://github.com/FantasticLBP/oclint)。分支切换到 llvm-7.0。
### 4. xcodebuild 的安装
xcode 下载安装好就已经成功安装了
### 5. xcpretty 的安装
先决条件,你的机器已经安装好了 Ruby gem.
```Shell
gem install xcpretty
```
## 二、 自定义 Rule
OClint 提供了 70+ 项的检查规则,你可以直接去使用。但是某些时候你需要制作自己的检测规则,接下来就说说如何自定义 lint 规则。
1. 进入 ~/Document/oclint 目录,执行下面的脚本
```shell
oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor
```
其中,*CustomLintRules* 就是定义的检查规则的名字, *ASTVisitor* 就是你继承的 lint 规则
可以继承的规则有ASTVisitor、SourceCodeReader、ASTMatcher。
2. 执行上面的脚本,会生成下面的文件
- Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp
- Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp
3. 要方便的开发自定义的 lint 规则,则需要生成一个 xcodeproj 项目。切换到项目根目录,也就是 Documents/oclint执行下面的命令
```Shell
mkdir Lint-XcodeProject
cd Lint-XcodeProject
touch generate-lint-rules.sh
chmod +x generate-lint-rules.sh
```
给上面的 generate-lint-rules.sh 里面添加下面的脚本
```Shell
#! /bin/sh -e
cmake -G Xcode \
-D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++ \
-D CMAKE_C_COMPILER=../build/llvm-install/bin/clang \
-D OCLINT_BUILD_DIR=../build/oclint-core \
-D OCLINT_SOURCE_DIR=../oclint-core \
-D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics \
-D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics \
-D LLVM_ROOT=../build/llvm-install/ ../oclint-rules
```
4. 执行 generate-lint-rules.sh 脚本(./generate-lint-rules.sh)。如果出现下面的 Log 则说明生成 xcodeproj 项目成功
![生成编写lint规则的xcodeproj工程1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-OCLint-Rule-Xcodeproj.png)
![生成编写lint规则的xcodeproj工程2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-OCLint-Xcode-Rules.png)
5. 打开步骤4生成的项目看到有很多文件夹代表 oclint 自带的 lint 规则,我们自定义的 lint 规则在最下面。
![编写lint自定义规则的代码文件夹](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-OCLint-custom-rule-inXcodeproj.png)
关于如何自定义 lint 规则的具体还没有深入研究,这里给个例子
<details>
<summary>点击查看示例代码</summary>
```C
#include "oclint/AbstractASTVisitorRule.h"
#include "oclint/RuleSet.h"
using namespace std;
using namespace clang;
using namespace oclint;
#include <iostream>
class MVVMRule : public AbstractASTVisitorRule<MVVMRule>
{
public:
virtual const string name() const override
{
return "Property in 'ViewModel' Class interface should be readonly.";
}
virtual int priority() const override
{
return 3;
}
virtual const string category() const override
{
return "mvvm";
}
virtual unsigned int supportedLanguages() const override
{
return LANG_OBJC;
}
#ifdef DOCGEN
virtual const std::string since() const override
{
return "0.18.10";
}
virtual const std::string description() const override
{
return "Property in 'ViewModel' Class interface should be readonly.";
}
virtual const std::string example() const override
{
return R"rst(
.. code-block:: cpp
@interface FooViewModel : NSObject // This is a "ViewModel" Class.
@property (nonatomic, strong) NSObject *bar; // should be readonly.
@end
)rst";
}
virtual const std::string fileName() const override
{
return "MVVMRule.cpp";
}
#endif
virtual void setUp() override {}
virtual void tearDown() override {}
/* Visit ObjCImplementationDecl */
bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
{
ObjCInterfaceDecl *interface = node->getClassInterface();
bool isViewModel = interface->getName().endswith("ViewModel");
if (!isViewModel) {
return false;
}
for (auto property = interface->instprop_begin(),
propertyEnd = interface->instprop_end(); property != propertyEnd; property++)
{
clang::ObjCPropertyDecl *propertyDecl = (clang::ObjCPropertyDecl *)*property;
if (propertyDecl->getName().startswith("UI")) {
addViolation(propertyDecl, this);
}
auto attrs = propertyDecl->getPropertyAttributes();
bool isReadwrite = (attrs & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_readwrite) > 0;
if (isReadwrite && isViewModel) {
addViolation(propertyDecl, this);
}
}
return true;
}
};
static RuleSet rules(new MVVMRule());
```
</details>
6. 修改自定义规则后就需要编译。成功后在 Products 目录下会看到对应名称的 CustomLintRulesRule.dylib 文件,就需要复制到 /Documents/oclint/oclint-release/lib/oclint/rules。讲道理生成新的 lint rule 文件,需要把新的 dylib 文件复制到 /usr/local/lib。因为我们在源代码安装的第4部设置了 ln -s 链接,所以不需要每次复制到相应文件夹。
但是还是比较麻烦,每次都需要编译新的 lint rule 之后需要将相应的 dylib 文件复制到源代码目录下的 oclint-release/lib/oclint/rules 目录下,本着「可以偷懒绝不动手」的原则,在自定义的 rule 的 target 中,在 Build Phases 选项下 CMake PostBuild Rules 中的脚本下将下面的代码复制进去
```Shell
cp /Users/liubinpeng/Documents/oclint/Lint-XcodeProject/rules.dl/Debug/libCustomLintRulesRule.dylib /Users/liubinpeng/Documents/oclint/build/oclint-release/lib/oclint/rules/libCustomLintRulesRule.dylib
```
7. 规则限定的3个类说明
```Shell
RuleBase
|
|-AbstractASTRuleBase
| |_ AbstractASTVisitorRule
| |_AbstractASTMatcherRule
|
|-AbstractSourceCodeReaderRule
```
- AbstractSourceCodeReaderRuleeachLine 方法,读取每行的代码,如果想编写的规则是需要针对每行的代码内容,则可以继承自该类
- AbstractASTVisitorRule可以访问 AST 上特定类型的所有节点,可以检查特定类型的所有节点是递归实现的。在 **apply** 方法内可以看到代码实现。开发者只需要重载 bool visit* 方法来访问特定类型的节点。其值表明是否继续递归检查
- AbstractASTMatcherRule实现 setUpMatcher 方法,在方法中添加 matcher当检查发现匹配结果时会调用 callback 方法。然后通过 callback 方法来继续对匹配到的结果进行处理
8. 知其所以然
oclint 依赖与源代码的语法抽象树AST。开源 clang 是 oclint 获的语法抽象树的依赖工具。你如果想对 AST 有个了解,可以查看这个[视频](https://www.youtube.com/watch?v=VqCkCDFLSsc&feature=youtu.be相关讲解https%3A%2F%2Fjonasdevlieghere.com%2Funderstanding-the-clang-ast%2F)
如果想查看某个文件的 AST 结构,你可以进入该文件的命令行,然后执行下面的脚本
```Shell
clang -Xclang -ast-dump -fsyntax-only main.m
```
## 三、 Homebrew 方式安装的 oclint 如何使用自定义规则
1. 查看 OCLint 安装路径
```Shell
which oclint
// 输出:/usr/local/bin/oclint
ls -al /usr/local/bin/oclint
// 输出:本机安装路径
```
2. 把上面生成的新的 lint rule 下的 dylib 文件复制到步骤1得到的额本机安装路径下
## 四、 使用 oclint
### 在命令行中使用
1. 如果项目使用了 Cocopod则需要指定 -workspace xxx.workspace
2. 每次编译之前需要 clean
实操:
- 进入项目
```Shell
cd /Workspace/Native/iOS/lianhua
```
- 查看项目基本信息
```Shell
xcodebuild -list
//输出
information about project "BridgeLabiPhone":
Targets:
BridgeLabiPhone
lint
Build Configurations:
Debug
Release
If no build configuration is specified and -scheme is not passed then "Release" is used.
Schemes:
BridgeLabiPhone
lint
```
- 编译
```Shell
xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
```
编译成功后,会在项目的文件夹下出现 compile_commands.json 文件
- 生成 html 报表
```Shell
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html
```
看到有报错,但是报错信息太多了,不好定位,利用下面的脚本则可以将报错信息写入 log 文件,方便查看
```Shell
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log
```
报错信息是:**oclint: error: one compiler command contains multiple jobs:**
查找资料,解决方案如下
- 将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 **NO**
- 在 podfile 中 target 'xx' do 前面添加下面的脚本
```Shell
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
end
end
end
```
然后继续尝试编译,发现还是报错,但是报错信息改变了,如下
![generate-lintresult-html-error](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-OCLint-Report-HTML.png)
看到报错信息是默认的警告数量超过限制,则 lint 失败。事实上 lint 后可以跟参数,所以我们修改脚本如下
```Shell
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999
```
生成了 lint 的结果,查看 html 文件可以具体定位哪个代码文件,哪一行哪一列有什么问题,方便修改。
![lint-result-html-report](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-oclint-result-html.png)
- 如果项目工程太大,整个 lint 会比较耗时,所幸 oclint 支持针对某个代码文件夹进行 lint
```Shell
oclint-json-compilation-database -i 需要静态分析的文件夹或文件 -- -report-type html -o oclintReport.html 其他的参数
```
- 参数说明
| 名称 | 描述 | 默认阈值 |
| ----------------------- | ---------------------------- | ---- |
| CYCLOMATIC_COMPLEXITY | 方法的循环复杂性(圈负责度) | 10 |
| LONG_CLASS | C类或Objective-C接口类别协议和实现的行数 | 1000 |
| LONG_LINE | 一行代码的字符数 | 100 |
| LONG_METHOD | 方法或函数的行数 | 50 |
| LONG_VARIABLE_NAME | 变量名称的字符数 | 20 |
| MAXIMUM_IF_LENGTH | `if`语句的行数 | 15 |
| MINIMUM_CASES_IN_SWITCH | switch语句中的case数 | 3 |
| NPATH_COMPLEXITY | 方法的NPath复杂性 | 200 |
| NCSS_METHOD | 一个没有注释的方法语句数 | 30 |
| NESTED_BLOCK_DEPTH | 块或复合语句的深度 | 5 |
| SHORT_VARIABLE_NAME | 变量名称的字符数 | 3 |
| TOO_MANY_FIELDS | 类的字段数 | 20 |
| TOO_MANY_METHODS | 类的方法数 | 30 |
| TOO_MANY_PARAMETERS | 方法的参数数 | 10 |
### 在 Xcode 中使用
- 在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字这里命名为 Lint
![Xcode中创建lint的target](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-LintTarget.png)
- 选择对应的 TARGET -> lint。在 Build Phases 下 Run Script 下写下面的脚本代码
```Shell
export LC_CTYPE=en_US.UTF-8
cd ${SRCROOT}
xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json && oclint-json-compilation-database -e Pods -- -report-type xcode
```
- 说明,虽然有时候没有编译通过,但是看到如下图的关于代码相关的 warning 则达到目的了。
![Xcode中Lint结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-lint-result-inXcode.png)
- lint 结果如下,根据相应的提示信息对代码进行调整。当然这只是一种参考,不一定要采纳 lint 给的提示。
![Xcode中显示lint结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-lint-in-Xcode.png)
## 脚本化
每次都在终端命令行去写 lint 的脚本,效率很低,所以想做成 shell 脚本。需要的同学直接直接拷贝进去,直接在工程的根目录下使用,我这边是一个 Cocopod 工程。拿走拿走别客气
```Shell
#!/bin/bash
COLOR_ERR="\033[1;31m" #出错提示
COLOR_SUCC="\033[0;32m" #成功提示
COLOR_QS="\033[1;37m" #问题颜色
COLOR_AW="\033[0;37m" #答案提示
COLOR_END="\033[1;34m" #颜色结束符
# 寻找项目的 ProjectName
function searchProjectName () {
# maxdepth 查找文件夹的深度
find . -maxdepth 1 -name "*.xcodeproj"
}
function oclintForProject () {
# 预先检测所需的安装包是否存在
if which xcodebuild 2>/dev/null; then
echo 'xcodebuild exist'
else
echo '🤔️ 连 xcodebuild 都没有安装,玩鸡毛啊? 🤔️'
fi
if which oclint 2>/dev/null; then
echo 'oclint exist'
else
echo '😠 完蛋了你,玩 oclint 却不安装吗,你要闹哪样 😠'
echo '😠 乖乖按照博文https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.63.md 安装所需环境 😠'
fi
if which xcpretty 2>/dev/null; then
echo 'xcpretty exist'
else
gem install xcpretty
fi
# 指定编码
export LANG="zh_CN.UTF-8"
export LC_COLLATE="zh_CN.UTF-8"
export LC_CTYPE="zh_CN.UTF-8"
export LC_MESSAGES="zh_CN.UTF-8"
export LC_MONETARY="zh_CN.UTF-8"
export LC_NUMERIC="zh_CN.UTF-8"
export LC_TIME="zh_CN.UTF-8"
export xcpretty=/usr/local/bin/xcpretty # xcpretty 的安装位置可以在终端用 which xcpretty找到
searchFunctionName=`searchProjectName`
path=${searchFunctionName}
# 字符串替换函数。//表示全局替换 /表示匹配到的第一个结果替换。
path=${path//.\//} # ./BridgeLabiPhone.xcodeproj -> BridgeLabiPhone.xcodeproj
path=${path//.xcodeproj/} # BridgeLabiPhone.xcodeproj -> BridgeLabiPhone
myworkspace=$path".xcworkspace" # workspace名字
myscheme=$path # scheme名字
# 清除上次编译数据
if [ -d ./derivedData ]; then
echo -e $COLOR_SUCC'-----清除上次编译数据derivedData-----'$COLOR_SUCC
rm -rf ./derivedData
fi
# xcodebuild clean
xcodebuild -scheme $myscheme -workspace $myworkspace clean
# # 生成编译数据
xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
if [ -f ./compile_commands.json ]; then
echo -e $COLOR_SUCC'编译数据生成完毕😄😄😄'$COLOR_SUCC
else
echo -e $COLOR_ERR'编译数据生成失败😭😭😭'$COLOR_ERR
return -1
fi
# 生成报表
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
-rc LONG_LINE=200 \
-disable-rule ShortVariableName \
-disable-rule ObjCAssignIvarOutsideAccessors \
-disable-rule AssignIvarOutsideAccessors \
-max-priority-1=100000 \
-max-priority-2=100000 \
-max-priority-3=100000
if [ -f ./oclintReport.html ]; then
rm compile_commands.json
echo -e $COLOR_SUCC'😄分析完毕😄'$COLOR_SUCC
else
echo -e $COLOR_ERR'😢分析失败😢'$COLOR_ERR
return -1
fi
echo -e $COLOR_AW'将为您自动打开 lint 的分析结果...'$COLOR_AW
# 用 safari 浏览器打开 oclint 的结果
open -a "/Applications/Safari.app" oclintReport.html
}
oclintForProject
```
同类型的文章:
- [如何打造团队的代码风格统一以及开发效率的提升](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md)
- [oclint介绍](https://github.com/hdw09/CIHexoBlog/blob/master/source/_posts/OClint学习笔记.md)
- [自定义oclint规则](https://github.com/hdw09/CIHexoBlog/blob/master/source/_posts/OCLint-自定义规则101.md)

47
Chapter1 - iOS/1.63.md Normal file
View File

@@ -0,0 +1,47 @@
# 苹果官方开源资料
- [苹果最新开源 opensource 网站](https://developer.apple.com/opensource/)
- [旧版本苹果开源资料](https://opensource.apple.com)
- [苹果开发者](https://developer.apple.com/develop/)
- [苹果 github](https://github.com/apple)
## 视频
WWDC
- [视频分类汇总](https://developer.apple.com/videos/topics/)
- [编译器和LLVM](https://developer.apple.com/videos/developer-tools/compiler-and-llvm)
## 源码
- [开源苹果](https://opensource.apple.com/source/)
- [dyld源代码](https://opensource.apple.com/tarballs/dyld/)
- [iOS11 源码](https://opensource.apple.com/release/ios-110.html)
- JavaScriptCore-7604.1.38.0.7 推荐
- WebKit-7604.1.38.0.7
- WebKit2-7604.1.38.0.7
- libiconv-51
- [objective-c 运行时 源码](https://opensource.apple.com/source/objc4/objc4-723/runtime/)
- [objective-c 消息机制汇编源码](https://opensource.apple.com/source/objc4/objc4-723/runtime/Messengers.subproj/)
## 文档
- [官方新文档入口](https://developer.apple.com/documentation)
- [UI 官方指南](https://developer.apple.com/design/human-interface-guidelines/)
- [Block ABI](https://clang.llvm.org/docs/Block-ABI-Apple.html)
## 旧版本文档汇总(有些补充或者更底层些)
- [旧版本文档](https://developer.apple.com/library/archive/navigation/)
- [WebKit Objective-C 编码指南](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DisplayWebContent/DisplayWebContent.html)
- [Concurrency 并发编程指南](https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html)
- [Kernel 内核编码指南](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/build/build.html)
## 工具
- [下载中心](https://developer.apple.com/download/)
## 第三方资料
- [cassowary 布局算法](https://constraints.cs.washington.edu/cassowary/)
- [fishhook - 高级 hook 框架源码](https://github.com/facebook/fishhook)
- [可运行 runtime 项目](https://github.com/RetVal/objc-runtime)
- [InjectionPlugin - 热加载 DEBUG iOS 代码 插件](https://github.com/johnno1962/InjectionIII)
- [第三方开源]()

35
Chapter1 - iOS/1.64.md Normal file
View File

@@ -0,0 +1,35 @@
# 组件化、模块化、插件、子应用、框架、库理解
> 作为大前端时代下开发的我们,经常会被组件化、模块化、框架、库、插件、子应用等术语所迷惑。甚至有些人将组件化和模块化的概念混混为一谈。大量的博客和文章将这些概念混淆,误导了诸多读者。所以本文的目的主要是结合作者本人前后端、移动端等经验,谈谈这几个概念。
## 组件
组件,最初的目的是为了**代码重用**。功能相对单一、独立。在整个系统结构中位于最底层,被其他代码所依赖。组件是 **“纵向分层”**
## 模块
模块,最初的目的是将同一类型的代码整合在一起,所以模块的功能相对全面、复杂些,但都同属于一个业务。不同模块之间也会存在相互依赖的关系,但大多数情况下这种相互依赖的关系只是业务之间的相互跳转。所以不同模块之间的地位是平级的。模块是 **“横向分块”**
因为从代码组织层面上讲,组件化开发是纵向分层,模块化是横向分块。所以模块化和组件化之间没有什么必然的联系。你可以将工程中的代码,按照功能模块进行逻辑上的拆分,然后将代码实现,按照模块化开发的思想,只需相应的代码按照**高内聚**的方式进行整合。假如一个 iOS 工程,使用 cocoapods 组织代码,将模块 A 相关的代码进行整理打包。
但是这样结果就是你的 App 工程虽然按照模块化的方式进行组织开发那么某个功能模块进行修改或者升级的时候只需要修改相应模块的代码。假如个人中心模块和购物车模块都有数据持久化的代码。在不使用组件化开发的时候可能在2个模块的代码里面都有数据持久化的代码。这样一个地方有问题改动另一个也要改动这样工程组织方式不友好且代码复用率低。
那么在实际的项目中我们一般是组件化结合模块化一起开发的。
|类别| 目的 | 特点 | 接口 | 成果 | 架构定位 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|组件化|重用、解耦|高重用、低耦合|无统一接口|基础库、基础组件|纵向分层|
|模块化|封装、隔离|高内聚、低耦合|有统一接口|业务模块、业务框架|横向切块|
参考:
- https://blog.csdn.net/blog_jihq/article/details/79191008
- https://blog.csdn.net/blog_jihq/article/details/80669616
- RN iOS 插件化开发

89
Chapter1 - iOS/1.65.md Normal file
View File

@@ -0,0 +1,89 @@
# 多端融合方案
## SwfitUI
SwiftUI 自亮相以来,全网就在讨论其与 React、Flutter 之间的关系。
首先是与 Flutter 的对比Flutter 的思路是从 0 开始,即语言、基础库、渲染引擎、排版引擎即框架本身全部由自己实现,其渲染引擎 Skia 只需要操作系统为其提供一个 GL Context 便可以完成所有图形渲染,这使得其跨平台性变得十分强大,到目前为止 Windows、Linux、macOS、Fuchsia 都已经得到了 Flutter 官方的支持。
这种做法我认为有利有弊首先好处是所有平台下行为一致不管是滚动视图、Material Design 控件还是模糊效果这些在其他平台没有的都得到了全平台的支持,开发者并不需要为这些去做平台间的适配,反观 React Native… 当然缺点也是存在的Flutter 这种做法类似于游戏引擎,平台提供的 UI 特性它一概不用,因此 Flutter View 与原生视图的交互就没有那么容易了,同时新的 Dart 语言貌似也不是非常受社区和开发者喜爱。
SwiftUI 没有像 Flutter 那样从头再来,这个全新的框架依旧使用了 UIKit、AppKit 等作为基础。但它并不是一个 UIKit 的声明式封装
许多基础组件,像 Text、Button 等都并不是直接使用 UILabel、UIButton 而是一个名为 DisplayList.ViewUpdater.Platform.CGDrawingView 的 UIView 子类。它们使用了自定义绘制,但又集成于 UIKit 的环境中,因此我猜测 SwiftUI 只提供了组件的自定义渲染和布局引擎,它使用到的底层技术还是 Core Animation、Core Graphics、Core Text 等。使用自定义绘制去实现组件可以理解成为跨平台提供便利,毕竟一个按钮还要区分 UIButton、NSButton 来实现未免有些麻烦。但是部分复杂的控件还是采用了 UIKit 中已有的类,比如 UISwitch 等。由于未脱离 UIKit 体系,嵌入一个 UIView 非常容易你不需要搞什么外部纹理Flutter 需要),因为它们的上下文是同一个,坐标系也是同一个。
所以我认为 SwiftUI 更加类似 React Native使用系统框架提供的组件只不过绘制和布局可以自己来实现这在 SwiftUI 之前也有相关的框架这样实践的,比如 Yoga、ComponentKit 等。
SwiftUI 是声明式的 UI 开发方式。关于声明式和命令式的介绍可以查看这篇[博文](https://github.com/FantasticLBP/knowledge-kit/blob/master/第七部分%20设计模式/7.1.md)。声明式开发框架 SwiftUI、React、Flutter、Vue 等都具备下面的特点。
- 使用各自的 DSL 来描述UI 该长什么样子(样式模版),而不是一句句代码描述来告诉系统该如何一步步构建 UI
- 声明所需要的数据部分
- 框架内部通过模版和数据部分,负责渲染绘制
- 数据发送变动
- 框架根据最新的数据和样式模版计算出最新的样式声明
- 最新的样式声明和之前的样式声明比较,计算出差值。系统重新绘制
```Swift
@State var name: String = "Tom"
var body: some View {
Text("Hello \(name)")
}
```
在 SwiftUI 中, view 是由纯数据结构描述的。因此这些数据的创建和差分计算都不会带来太多的性能开销。
## React && React Native
先谈谈 React 吧。React 的优秀的地方在于Virtual DOM、JSX、单向数据流等等。但是谈谈 React 这些框架为什么可以做 Web 也可以做跨端解决方案 RN。传统的 Web 开发是基于命令式编程的方式,监听事件、发起请求、操作 DOM、刷新页面。想想看每次数据变动了都需要刷新 DOM然后用户就可以在浏览器上看到了最新的数据DOM 的操作是很耗费资源的。怎么理解这句话,其实 DOM 对象本身就是一个 JS 对象,所以 JS 对于 JS 对象的操作来说性能耗费基本不用考虑,微乎其微。但是 DOM 每次变动到真实 UI 的渲染是非常耗费性能的(触发浏览器的布局和绘制)。至于浏览器的布局重绘原理我会新开文章进行讨论和总结,可以先看看文章底部的参考资料。
在 React 中使用数据state、props+ 样式模版JSX的方式开发 UI。以下是 React 大致的工作原理
- 设置页面所需要的数据 State、props
- 创建页面的模版样式部分 JSX
- 根据数据和样式模版生成 Virtual DOM
- 页面首次渲染的时候先根据 Virtual DOM 生成真实的 UI
- 数据变动setState结合「批更新策略」
- 根据变动后的数据和模版样式生成新的 Virtual DOM
- 根据 Diff 算法计算变动的 Virtual DOM 部分
- 根据变动的 Virtual DOM 部分去绘制 UI
什么是 Virtual DOM?
Virtual DOM 就是一个 JS 对象,用来描述真实的 DOM。看看下面的例子
```HTML
<ol id='ol-list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ol>
```
转换为 Virtual DOM
```Javascript
var olElement = {
tagName: 'ol',
props: {
id: 'ol-list'
},
children: [
{tagName: 'li', props: {class: 'item'}, children: ['Item 1']},
{tagName: 'li', props: {class: 'item'}, children: ['Item 2']},
{tagName: 'li', props: {class: 'item'}, children: ['Item 3']}
]
}
```
所以 Virtual DOM 抽象出来后就很方便了,在 Web 端可以去渲染到真实的 DOM在 Native 端可以去映射到 Native UI 组件上。所以 React 有了 Virtual DOM 便可以在 Web 端和 Native 端大展拳脚。
## 参考资料
- [浏览器的布局绘制与DOM操作](https://blog.csdn.net/sinat_32434539/article/details/77894009)
- [前端必读:浏览器内部工作原理](https://www.cnblogs.com/rainy-shurun/p/5603686.html)
- [深度理解 Virtual DOM](https://www.cnblogs.com/wubaiqing/p/6726429.html)

242
Chapter1 - iOS/1.66.md Normal file
View File

@@ -0,0 +1,242 @@
# 移动端网络层优化
当关心 App 的用户体验的时候,不得不考虑网络层相关的问题。因为一个 App 通常来说网络层的操作占据了大多数的场景。几乎每个成熟的 iOS 项目都有一个网络模块,大部分的网络请求都是基于 HTTP 完成iOS 端采用成熟的 AFNetworking 很容易完成一个功能简单的网络模块,但是使用起来往往会有大量的问题。所以网络层优化是需要大量的经验和知识水平的。对数据的分析和调研、用户反馈,现总结网络层相关的优化手段。
优化方面:
1. 速度:网络请求速度如何进一步提升
2. 弱网:移动端网络环境随时变化,经常出现网络连接很不稳定可用性差的情况。怎样在这种情况下最大限度最快完成网络请求
3. 安全:怎样防止被第三方窃听。篡改或冒充,防止运营商劫持,同时有不影响性能
## 一、速度
正常一条网络请求需要经过的流程是:
1. DNS 解析。请求 DNS 服务器,获取域名对应的 IP 地址
2. 与服务器建立连接。包括 TCP 三次握手,安全协议同步流程
3. 连接建立完成,发送、接收数据,解码数据
这里存在3个优化点
1. 直接使用 IP 地址,去除 DNS 解析步骤
2. 不要每次请求都重新建立连接,复用连接或一直使用同一条连接(长连接)
3. 压缩数据,减小传输数据的大小
### 1. DNS
DNS 完整的解析流程很长,会先从本地系统缓存读取,若没有就到最近的 DNS 服务器取,若没有再到主域名服务器取,每一层都有缓存,但为了域名解析的实时性。每一层缓存都设有过期时间,这种 DNS 解析机制有几个缺点:
- 缓存时间设置过长,域名更新不及时。设置时间短,大量 DNS 解析请求影响请求毒素
- 域名劫持。容易被中间人攻击,或者运营商劫持。把域名解析道第三方 IP 地址,据统计劫持率高达 7%
- DNS 解析过程不受控制,无法保证最快的解析速度
- 一次请求只可以借此一个域名
为了解决上述问题,就有了 HTTPDNS。原理就是代替系统的 DNS 解析工作,解决上述问题。
- 域名解析与请求分离,所有请求都直接使用 IP 地址,无需 DNS 解析App 定时请求 HTTPDNS 服务器更新 IP 地址即可
- 通过签名等方式,保证 HTTPDNS 请求的安全,避免被劫持
- DNS 解析由自己控制。可以保证根据用户所在地返回就近的 IP 地址。或根据客户端测速结果使用最快的 IP
- 一次请求可以解析多个域名
对于 DNS 解析的情况,业界主流做法就是 HTTPDNS 或者内置 Server IP 列表。客户端直接访问 HTTPDNS 接口,获取业务在域名配置系统上配置的访问延迟最优的 IP获取到 IP 后就直接往此 IP 发送业务协议请求,不再需要本地 DNS 服务器进行解析,从根本上解决了劫持问题。同时可以降低网络延迟,提高连接的成功率。
建立的 Server IP 列表,是在本地缓存一个 IP 映射表,可以在 App 启动时请求接口下发更新。访问其他的服务的时候根据映射拿到 IP 再发出请求。
![DNS服务器解析示意图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-DNSLookUP.png)
绝大多数的 App 的第一步都是 DNS 解析,解析请求回根据当时的网络情况不同而不同,各平台的 DNS 缓存策略存在差异,因此对于移动 App 网络性能会产生影响。App 网络情况跟很多因素都相关。但是 DNS 是第一步也是最重要的一环。
1. 降低 DNS 请求带来的延迟
客户端 App 请求第一步是 DNS 解析。但是由于 Cache 的存在,使得大部分的解析请求都不会产生任何延迟。各平台都有自己的 Cache 过期策略。像 iOS 系统一般都是 24h 后过期。还有就是从飞行模式切换回来、开关机、重置网络设置等都会导致 DNS Cache 的清除。所以一般情况用户在第二天打开你的 App 都会经历一次完整的 DNS 解析请求。网络情况差的时候会明显的请求总耗时增加。如果可以直接跳过 DNS 解析这一步,就可以提高网络性能了。
2. 预防 NDS 劫持
DNS 劫持指的是改变 DNS请求返回的结果将目的域名对应的 IP 指向另一个地址。一般有两种方式,一种是通过病毒的方式改变本机配置的 DNS 服务器地址,二是通过攻击正常的 DNS 服务器而改变其行为。不管何种方式都会影响 App 的业务请求,如果遇到恶意的攻击还会衍生出各种安全问题。客户端自己做 DNS 到 IP 地址的映射就绕过了向 DNS 服务器请求而可能被遭到攻击的可能,让劫持者无从下手
3. 服务器动态部署
DNS 映射实际上是模拟了 DNS 请求的解析行为。如果客户端将自己的位置信息例如ip地址、国家码等上传给服务器服务器就可以根据位置信息就近推荐合适的 Server IP 地址。从而减小了整体网络请求延迟、实现了动态部署
如何设计自己的 DNS 映射机制?
DNS 服务器做的事情就是输入一个域名,输出一个 IP 地址,做自己的 DNS 映射机制就是在客户端维护一个这样的映射文件。不过这个映射文件可以根据服务器在 App 端进行更新。还需要具备一定的容错处理。
- 一个打包到 App 包里面的默认域名 IP 映射文件,这样就可以避免第一次去服务器取配置文件带来的延迟
- 一个定时器可以每隔一段时间**通过签名等方式(避免被劫持)**去服务器获取最新的域名映射文件,并保存到本地
- 每次取到最新的映射文件后,保存到本地,并将上次的映射文件保存作为备份。一旦出现线上配置错误的情况,不至于导致请求无法处理
- 如果映射文件不能处理域名映射,那么可以回滚到使用默认的 DNS 解析服务
- 如果映射后的一个 IP 持续请求失败,那么应从机制上避免这个问题。也就是需要一个无效使用的淘汰机制
- 无效的 IP 可以上报到服务器。发现问题解决问题
在 iOS 端实践。大致有3个角色mapper、validator、reporter
- mapper
mapper 是负责和外部交互的部分。主要负责在输入 domain name 的情况下输出 ip。同时校验来自应用层请求成功和失败的信息。失败的情况下需要将 ip 进一步验证,以确定是真的无效。如果无效则进行上报。同时还负责更新机制
- validator
负责在接收到请求失败的 ip 时,对这个 ip 进行有效性验证。检测的强弱规则可以自定义。但是一般规则是在后台线程使用这个 ip 进行多次尝试,如果都不成功则告诉 mapper 这个 ip 确实无用,如果成功则说明有效。(某次失败有可能意味着当时的网络环境不稳定)
- reporter
主要负责告诉 server 整个 mapping 机制的健康状况。在出现某个 ip 导致请求失败并由 validator 校验多次还是失败的情况下需要上报到服务端,让服务端去维护或验证。(有可能是在配置的时候少打了个字母 😂)
根据公司业务情况进行改造。比如采用服务器定时更新映射文件的这一步骤,可以更改为 socket 长链接通道在需要更新时 push或利用 HTTP2.0 的 server push 机制。还有上报机制。还可以对请求的总量、成功率、映射成功率等数据做侦测。
### 2. 连接
第二个问题,连接建立的耗时问题,这里主要的优化思路是复用连接,不用每次请求都重新建立连接,如何更有效率地复用连接,可以说是网络请求速度优化里最主要的点了,并且这里的优化在不断的演进中,值得关注
#### 2.1 keep-alive
HTTP 协议里有个 `keep-alive`HTTP1.1 默认开启,一定程度上缓解了每次请求都需要进行 TCP 三次握手建立连接的耗时。原理是请求完成后不立即释放连接,而是放入**连接池**中。若此时有另一个请求要发出,如果请求的端口和域名在复用池里面有一致的,那么就直接拿出连接池中的连接进行发送和接收数据,少了建立连接的耗时。
实际上,现在无论是客户端还是浏览器都默认开启了 keep-alive对同个域名不会再有每发一个请求就进行一次建连的情况纯短连接已经不存在了。但是有问题就是这个 keep-alive 的短连接一次只能发送接收一个请求,在上一个请求处理完成之前。无法接受新的请求。若同时发起多个请求,就有两种情况:
1. 若串行发送请求,可以一直复用一个连接,但速度很慢,每个请求都需要等待上个请求完成再进行发送
2. 若并行发送请求,那么首次每个请求都要进行 TCP 三次握手建立新的连接,虽然第二次可以复用连接池里面的这堆连接,但若连接池里面保留的过多,对服务端资源产生交大浪费,若限制了保持的连接数,并行请求超出的连接仍每次需要建立连接。对于这个问题新一代的 HTTP2.0 提出了多路复用解决方案。
#### 2.2 多路复用
HTTP2 的多路复用机制一样是复用连接。但它复用的这条连接支持同时处理多条请求,所有请求都可以在这条连接上进行,也就是解决了上面说的并发请求需要建立多条连接带来的问题。
![HTTP2多路复用](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-MultiplesRoutes.png)
HTTP1.1 的协议里,在一个连接里传输数据都是串行顺序传输的,必须等上一个请求全部处理完成后,下一个请求才能进行处理,导致这些请求期间这条连接并不是满带宽传输的,即使是 HTTP1.1 的 pipelining 可以同时发送多个 Request但 response 仍是按请求的顺序串行返回,只要其中一个 response 稍微大一点或发送错误,就会阻塞住后面的请求。
HTTP2 这里的多路复用协议解决了这些问题,它把在连接里传输的数据都封装成一个个 stream每个 stream 都有标识stream 的发送和接收可以是乱序的,不依赖顺序,也就不会有阻塞的问题,接收端可以根据 stream 的标识去区分属于哪个请求,再进行数据拼接,得到最终数据。
解释下多路复用这个词,多路可以认为是多个连接,多个操作,复用就是 复用一条连接或一个线程。HTTP2 这里是连接的多路复用,网络相关的还有一个 I/O 的多路复用(select/epoll),指通过事件驱动的方式让多个网络请求返回的数据在同一条线程里完成读写。
客户端来说iOS9 以上 NSURLSession 原生支持 HTTP2只要服务端也支持就可以直接使用Android 的 okhttp3 以上也支持了 HTTP2国内一些大型 APP 会自建网络层,支持 HTTP2 的多路复用,避免系统的限制以及根据自身业务需要增加一些特性,例如微信的开源网络库 [mars](https://github.com/Tencent/mars/issues?page=2&q=is%3Aissue+is%3Aopen),做到一条长连接处理微信上的大部分请求,多路复用的特性上基本跟 HTTP2 一致。
#### 2.3 TCP队头阻塞
HTTP2 的多路复用看起来是完美的解决方案,但还有个问题,就是队头阻塞,这是受限于 TCP 协议TCP 协议为了保证数据的可靠性,若传输过程中一个 TCP 包丢失会等待这个包重传后才会处理后续的包。HTTP2的多路复用让所有请求都在同一条连接进行中间有一个包丢失就会阻塞等待重传所有请求也就被阻塞了。
对于这个问题不改变 TCP 协议就无法优化,但 TCP 协议依赖操作系统实现以及部分硬件的定制,改进缓慢,于是 GOOGLE 提出 QUIC 协议,相当于在 UDP 协议之上再定义一套可靠传输协议,解决 TCP 的一些缺陷,包括队头阻塞。具体解决原理网上资料较多,可以看看。
QUIC 处于起步阶段少有客户端接入QUIC 协议相对于 HTTP2 最大的优势是对TCP队头阻塞的解决其他的像安全握手 0RTT / 证书压缩等优化 TLS1.3 已跟进,可以用于 HTTP2并不是独有特性。TCP 队头阻塞在 HTTP2 上对性能的影响有多大,在速度上 QUIC 能带来多大提升待研究。
### 3. 数据
第三个问题,传输数据大小问题。数据对请求速度的影响分两方面,一是压缩率,二是解压序列化反序列化的速度。目前最流行的两种数据格式是 json 和 protobuf。json 是字符串protobuf 是二进制。即使采用各种压缩算法压缩后protobuf 仍会比 json 小。protobuf 在数据量和序列化速度上均占优势。
压缩算法多种多样,且在不断演进。最新出得 Brotli 和 [Z-standard](https://github.com/facebook/zstd) 实现了更高的压缩率。Z-standard 可以根据业务数据样本训练出适合的字典,进一步提高压缩率。是目前最好的压缩算法
除了传输数据的 body 大小,每个 HTTP 协议头的数据也不可忽视HTTP2 里对 HTTP 协议头也进行了压缩HTTP 头大多是重复数据,固定的字段如 method 可以用静态字典,不固定但多个请求重复的字段例如 cookie 用动态字典,可以打到非常高的压缩率。可以查看这篇[文章](https://imququ.com/post/header-compression-in-http2.html)查看介绍。
总结:通过 HTTPDNS连接多路复用更好的压缩算法可以把网络请求的速度优化到不错的程度了。接下来看看弱网环境和安全方面的手段吧
## 弱网
手机无线网络环境不稳定,针对弱网的优化,微信有较多的实践和分享
1. 提升连接的成功率
复合连接。建立连接时,阶梯式并发连接,其中一条连通后其他连接都关闭。这个方案结合串行和并发的优势。提高弱网下的连接成功率,同时又不会增加服务器资源消耗
![弱网复合连接](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-BadNetwork.png)
2. 制定最合适的超时时间
对总读写超时(从请求到响应的超时)、首包超时、包包超时(两个数据段之间的超时)时间制定不同的计算方案,加快对超时的判断,减少等待时间,尽早重试。这里的超时时间还可以根据网络状态动态设定
3. 调优 TCP 参数,使用 TCP 优化算法
对于服务端的 TCP 协议参数进行调优。以及开启各种优化算法使得业务特性和移动端网络环境,包括 RTO 初始值混合慢启动TLP、F-RTO 等
针对弱网的优化未成为标准方案,系统网络库没有内置,不过前两个客户端优化微信的开源网络库 [mars](https://github.com/Tencent/mars) 有实现。
## 安全
标准安全协议 `TLS` 保证了网络传输的安全,前身是 SSL不断在演进。我们日常使用的 HTTPS 就是 HTTP 协议加上 TLS 安全协议。
安全协议概括性地说解决两个问题1. 保证安全2. 降低加密成本
在保证安全上:
1. 使用加密算法组合对传输数据加密,避免被窃听和篡改,
2. 认证对方身份,避免被第三方冒充
3. 加密算法保持灵活可更新,防止定死算法被破解后无法更换,禁用已破解的算法。
降低加密成本上:
1. 用对称加密算法加密传输数据,解决非对称加密算法的性能低以及长度限制问题
2. 缓存安全协议握手后的密钥数据,加快第二次建连的速度。
3. 加快握手过程2RTT -> 0RTT。加快握手的思路就是原本客户端和服务端需要协商使用什么算法后才能加密发送数据变成通过内置的公钥和默认的算法在握手的同时就把数据发出去也就是不需要等待握手就开始发送数据打到 0RTT
想详细看看 TLS 的可以看看这篇[文章](https://blog.helong.info/blog/2015/09/07/tls-protocol-analysis-and-crypto-protocol-design/)
目前基本主流都支持 TLS 1.2。iOS 网络库默认使用 TLS 1.2Android 4.4 以上支持 1.2。
## 其他优化方案
- 域名合并:淘宝、美团等公司公布的解决方案中都有提到,就是将公司原来的很多域名都合并到较少的几个域名。为什么?因为 HTTP 的通道复用就是基于域名划分的。如果域名只有几个,那么多数请求都可以在长连接通道进行,这样就可以降低延迟、增加成功率
- 预热,尽早建立长连接。这样其他的业务请求就可以复用长连接通道。加快访问速度。因为每次建立连接都需要经过 DNS 域名解析、TCP 三次握手等漫长步骤。建立长连接的时机可以考虑:冷启动、前后台切换、网络切换等
- 如果情况允许,可以将网络切换到 HTTP 2.0,解决了 HTTP1.1 的 head of blocking 降低了网络延迟提供了更强大的多路复用技术。还加入了流量控制、新的二进制格式、Server Push、请求优先级和依赖等待等特性。
- 建立多通道。比如携程、艺龙、美团等公司都有自己的 TCP、UDP 通道。具有多域名共用通道。
- 有些超级大厂还自研了协议。比如 QUIC
- 加入 CDN 加速,动态静态资源分离
- 对于类似埋点的业务数据请求,可以合并请求,减小流量。另外结合埋点数据压缩上传
- App 网络情况诊断
- 根据网络情况,动态设置超时时间等
## 最后
网络层涉及的学问非常多,需要懂得多端的重视才可以提出靠谱的解决方案。希望不断认识不断思考。
## 参考资料
- [2016年携程App网络服务通道治理和性能优化实践](https://chuansongme.com/n/466033251461)
## 参考点:
- 移动调度
1. DNSDNS劫持、运营商DNS层次不齐、1RTT请求DNS、不支持LDC多中心调度、不支持自定义调度。移动调度优势LDC多中心调度、异地多活快速容灾、白名单问题排查
- 接口设计优化
1. 慢逻辑监控
2. 多次查询优化
3. 接口 cache 等
- 静态资源、图片等相关策略
1. 使用更快的图片格式WebP等
2. 不同网络的不同图片下发
3. 资源合并、压缩combo
4. 图片压缩webp
- 让用户觉得快
1. 优先级加载
2. 异步加载
- 减小数据包大小和优化包量
1. 推广 Protocol Buffer 等序列化方式
2. 接入 SYNC
- 监控体系建设
1. 全链路数据打通,问题剖析一杆子到底
2. 多维评价模型、监控预警、数据化研发
3. 管理决策有依据,结果有数据
## 疑难杂症
1. 有人遇到使用网络经常出现内存泄漏的情况。我觉得这是属于基础功不扎实的情况,因为 [AFHTTPSessionManager manager] 它返回的对象持有一个 session。且 session 的 delegate 对象也是强引用。AFHTTPSessionManager 的父类是 AFURLSessionManager。AFURLSessionManager initWithSessionConfiguration 底层就是 `self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];` 所以会强引用
解决方案:
- 每次请求完网络后需要给 [AFHTTPSessionManager manager] 这种方式初始化的 manager 释放掉。比如 AFNetWorking 提供的 *invalidateSessionCancelingTasks* 方法。
- 将 AFHTTPSessionManager 对象做成单例获取这样带来另一个好处NSURLSession 不销毁,另外的请求继续发起的时候不需要初始的网络握手,达到「链路复用」的功能。
```
//AFURLSessionManager.h
@property (readonly, nonatomic, strong) NSURLSession *session;
// AFHTTPSessionManager.m
// NSURLSessionConfiguration
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
```
```Objective-C
- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks {
if (cancelPendingTasks) {
[self.session invalidateAndCancel];
} else {
[self.session finishTasksAndInvalidate];
}
}
```
2. 在 iOS 10.3 系统上存在 SSL 证书校验的问题。报错信息如下图。目前没有找到具体原因和解决方案,如果有人有解决方案请联系我。
![报错信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-08-29-NetworkError2.png)
![问题信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-08-29-NetworkError1.jpg)

11
Chapter1 - iOS/1.67.md Normal file
View File

@@ -0,0 +1,11 @@
# iOS工程编译速度优化
要提升编译速度,我们首先要知道有没有提升?那就需要一个量化的标准。下面的命令让 Xcode 告诉你编译耗费多久时间。
```shell
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
```
## ccache
提高编译速度需要依靠缓存的能力, [ccache](https://ccache.dev) 是一个靠谱的缓存。基于编译器层面。

25
Chapter1 - iOS/1.68.md Normal file
View File

@@ -0,0 +1,25 @@
# 守护你的App安全
App Crash 会严重影响用户体验Crash 率和客户端工程师的个人评级和绩效考核挂钩。因此写出的代码必须安全可靠。崩溃率要保持在一个什么样的水平以下。
App 在不断业务迭代,经过多人开发维护后可能因为开发者的水平层次不齐,造成代码质量较低,所以要实现的效果就是保证 App 稳健运行,常见的问题捕获处理,将造成奔溃或者 Crash 的因素处理掉,让 App 正常运行。
## 功能设想
对业务代码零侵入性地将原本会导致 App 奔溃的 crash 信息处理掉,保证 App 正常稳健运行,再将 Crash 信息提取出来呈现给开发者。开发者可以根据相应的 Crash 信息去处理解决对应的代码。
## KVO
```
*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <LHMainHomePageNavigationBar 0x1035a05a0> for the key path "name" from <LHMainHomePageNavigationBar 0x1035a05a0> because it is not registered as an observer.'
*** First throw call stack:
(0x2045ac518 0x2037879f8 0x2044b6c70 0x204f52470 0x204f5222c 0x101d8ed68 0x2309ad230 0x230456af8 0x100f0edb0 0x230456e18 0x230455e84 0x2309e429c 0x2309e54c4 0x2309c5534 0x230a8b7c0 0x230a8deec 0x230a8711c 0x20453e2bc 0x20453e23c 0x20453db24 0x204538a60 0x204538354 0x20673879c 0x2309abb68 0x101c7086c 0x203ffe8e0)
libc++abi.dylib: terminating with uncaught exception of type NSException
```

8
Chapter1 - iOS/1.69.md Normal file
View File

@@ -0,0 +1,8 @@
# React Native 总结
## 学习方面
学过 React.js 之后你再去学习 React Native 会很简单,一些核心的东西理解之后会很简单。比如 React 中的单向数据流、虚拟 Dom、diff 算法、数据变动的批量更新机制、diff 之后的 UI 渲染。
样式布局方面增加了 flexbox这样子布局在移动端会非常方便非常简单

View File

@@ -14,7 +14,7 @@
* 代码段code segment通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读某些架构也允许代码段为可写即允许修改程序。在代码段中也有可能包含一些只读的常数变量例如字符串常量。
![内存](/assets/内存.png)
![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png)
* ##### 搞清楚上面的概念再来研究下对象在内存中如何存储?
@@ -95,7 +95,10 @@ Person *p1 = [Person new];
**结论**
![p1](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-15%20下午5.35.17.png)
![p2](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-15%20下午5.35.34.png)
![p1](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2017-05-15%20下午5.35.17.png)
![p2](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2017-05-15%20下午5.35.34.png)
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
补充:
- [alloc与init区别](https://www.jianshu.com/p/daf668b76861)

312
Chapter1 - iOS/1.70.md Normal file
View File

@@ -0,0 +1,312 @@
# 不一样的动态化能力
> 对于热修复对于大多数公司来说都是可望而不可及的技术手段。热修复对于线上问题是杀手锏级别项目。Android 热修复方案很多,典型的属微信的 `Tinker` 莫属,而苹果公司对于安全的要求非常高,所以一些动态调用的能力都会被封杀,这篇文章主要研究下 iOS 端的热修复技术方案。
## 热修复方案
- 将下发的原生代码通过自己实现的代码解析引擎将代码转换为AST树然后存储在相关的模型里面在通过一个上下文注入到runtime里面当runtime回调到当前函数的时候上下文从存储的相关模型取出各个参数然后放到当前堆栈里面去执行相关的逻辑执行问之后在返回之前调用的地方这里跟腾讯的OCS有点像.
- JSPatch加加密多混淆关键词替换。其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数然后现在aop、hook、jspatch 都是离不开这些函数的。解决方案将 动态能力的 API 替换名字:而是本地已经处理好,写到代码的静态变量里面,执行的时候去按照相应的解密方法去解密,然后得到 respondsToSelector:, 再去执行)
- 几大app中的方案都是自己研发的不过大同小异有比较多的是从编译器层面出发直接把写的代码编译好然后自己再写解析器解析执行
- lua kithttps://github.com/alibaba/LuaViewSDKhttps://alibaba.github.io/LuaViewSDK/guide.html
其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数然后现在aop、hook、jspatch 都是离不开这些函数的。
## 思路
`JavaScriptCore` 是苹果给开发者操作 Javascript 的一个库,因此使用 JavaScriptCore 基本不存在问题。另外做热修复的基本思路就是在某个类执行某个类方法、某个类的对象执行某个对象方法的时候做一些处理。所以这里涉及到几个因素:类、类对象、类方法、实例方法、方法执行前、方法执行后、方法完全替换。 Objective-C 有运行时特性,所以可以很容易实现上面的几个点,但是直接使用 Runtime 会比较麻烦,这时候就不得不提一下一个面向切面编程的开源库-[Aspects](https://github.com/steipete/Aspects)。
所以剩下来的事情就是将 Aspects 的几个能力暴露给 JavascriptCore 对象。然后 App 在启动的时候去调用热修复接口,拿到修复的字符串,然后给 JavascriptCore 对象,然后 Javascript 对象去执行拿到的热修复的字符串,这样子整个流程下来,当我们去进入某个页面或者调用某个功能的时候,发现 A 类的 methodA 方法有问题,我们下发了热修复代码,就可以在 methodA 的前后加入逻辑,甚至是完全替换。
## 代码实现
<details>
<summary>FixManager</summary>
```Objective-C
#import <Foundation/Foundation.h>
#import "Aspects.h"
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h>
NS_ASSUME_NONNULL_BEGIN
@interface FixManager : NSObject
+ (FixManager *)sharedInstance;
+ (void)fixIt;
+ (void)evalString:(NSString *)javascriptString;
@end
NS_ASSUME_NONNULL_END
#import "FixManager.h"
@implementation FixManager
+ (FixManager *)sharedInstance
{
static FixManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] init];
});
return manager;
}
+ (JSContext *)context
{
static JSContext *_context;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_context = [[JSContext alloc] init];
[_context setExceptionHandler:^(JSContext *context, JSValue *exception) {
NSLog(@"Ooops, %@", exception);
}];
});
return _context;
}
+ (void)fixIt
{
[self context][@"fixInstanceMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl){
[self _fixWithMethod:NO
aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixInstanceMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:NO aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixInstanceMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:NO aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixClassMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:YES aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixClassMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:YES aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixClassMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:YES aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"runClassWithNoParamter"] = ^id(NSString *className, NSString *selectorName) {
return [self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runClassWith1Paramter"] = ^id(NSString *className, NSString *selectorName, id obj1) {
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runClassWith2Paramters"] = ^id(NSString *className, NSString *selectorName, id obj1, id obj2) {
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runVoidClassWithNoParamter"] = ^(NSString *className, NSString *selectorName) {
[self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runVoidClassWith1Paramter"] = ^(NSString *className, NSString *selectorName, id obj1) {
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runVoidClassWith2Paramters"] = ^(NSString *className, NSString *selectorName, id obj1, id obj2) {
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runInstanceWithNoParamter"] = ^id(id instance, NSString *selectorName) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runInstanceWith1Paramter"] = ^id(id instance, NSString *selectorName, id obj1) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runInstanceWith2Paramters"] = ^id(id instance, NSString *selectorName, id obj1, id obj2) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runVoidInstanceWithNoParamter"] = ^(id instance, NSString *selectorName) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runVoidInstanceWith1Paramter"] = ^(id instance, NSString *selectorName, id obj1) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runVoidInstanceWith2Paramters"] = ^(id instance, NSString *selectorName, id obj1, id obj2) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runInvocation"] = ^(NSInvocation *invocation) {
[invocation invoke];
};
// helper将 JS 的 console.log 用 Native Log 替换
[[self context] evaluateScript:@"var console = {}"];
[self context][@"console"][@"log"] = ^(id message) {
NSLog(@"Javascript log: %@",message);
};
}
+ (void)_fixWithMethod:(BOOL)isClassMethod aspectionOptions:(AspectOptions)option instanceName:(NSString *)instanceName selectorName:(NSString *)selectorName fixImpl:(JSValue *)fixImpl
{
Class klass = NSClassFromString(instanceName);
if (isClassMethod) {
klass = object_getClass(klass);
}
SEL sel = NSSelectorFromString(selectorName);
[klass aspect_hookSelector:sel withOptions:option usingBlock:^(id<AspectInfo> aspectInfo){
[fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];
} error:nil];
}
+ (id)_runClassWithClassName:(NSString *)className selector:(NSString *)selector obj1:(id)obj1 obj2:(id)obj2
{
Class klass = NSClassFromString(className);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [klass performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
#pragma clang diagnostic pop
}
+ (id)_runInstanceWithInstance:(id)instance selector:(NSString *)selector obj1:(id)obj1 obj2:(id)obj2
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [instance performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
#pragma clang diagnostic pop
}
+ (void)evalString:(NSString *)javascriptString
{
[[self context] evaluateScript:javascriptString];
}
@end
```
</details>
<details>
<summary>BugProtector</summary>
```Objective-C
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface BugProtector : NSObject
+ (instancetype)sharedInstance;
+ (void)getFixScript:(NSString *)scriptText;
@end
NS_ASSUME_NONNULL_END
#import "BugProtector.h"
#import "FixManager.h"
@interface BugProtector ()
@end
@implementation BugProtector
static BugProtector *_sharedInstance = nil;
#pragma mark - life cycle
+ (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];
[FixManager fixIt];
});
return _sharedInstance;
}
+ (FixManager *)fixManager
{
static FixManager *_manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_manager = [FixManager sharedInstance];
});
return _manager;
}
+ (id)allocWithZone:(struct _NSZone *)zone
{
return [BugProtector sharedInstance];
}
+ (instancetype)alloc
{
return [BugProtector sharedInstance];
}
- (id)copy
{
return self;
}
- (id)mutableCopy
{
return self;
}
- (id)copyWithZone:(struct _NSZone *)zone
{
return self;
}
#ifdef DEBUG
- (void)dealloc
{
NSLog(@"%s",__func__);
}
#endif
#pragma mark - public Method
+ (void)getFixScript:(NSString *)scriptText
{
[FixManager evalString:scriptText];
}
@end
```
</details>
完整的 [Demo](https://github.com/FantasticLBP/BlogDemos/tree/master/HotFix) 可以点此查看链接.
## 未完,待续

102
Chapter1 - iOS/1.71.md Normal file
View File

@@ -0,0 +1,102 @@
# Flutter初体验-安装
> 多端融合能力是现在大前端研究的技术风向标之一,当前 Flutter 风头正盛,它的设计之初就是为了解决移动端的当今的诸多问题。
Flutter 的设计的思想以及出发点不是本篇的重点,所以直奔主题,如何在 Mac 上安装 Flutter 的开发环境。
### 1. Homebrew
Homebrew 是 Mac 是安装各种软件包的一个工具,所以你需要先安装好 Homebrew。
```shell
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
```
### 2. 安装 Flutter SDK
#### 下载 SDK
SDK 下载方式有2种。一是使用 `git clone -b beta https://github.com/flutter/flutter.git` 下载,二是通过官网选择符合自己机器环境的文件。(实验后发现方式一特别慢,建议大家直接使用方式二)
![Flutter文件夹位置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-08-13-FlutterDirectory.png)
下载好之后解压,安装到自己指定的位置。
#### 配置环境变量
Flutter 在运行的时候需要去官网下载一些需要的资源,所以需要设置镜像服务器。我使用的是 iterm2。所以打开 **.zshrc** 文件。如果使用的是系统的 Terminal则需要打开 **.bash_profile**。
```shell
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
export PATH=/Users/liubinpeng/flutter/bin:$PATH
```
`/Users/liubinpeng` 是我电脑环境下的 Flutter 路径,这里改成你自己的路径。之后执行 `source .zshrc`
验证 Flutter 环境是否成功。
```shell
flutter -h
```
### 3. 配置 Android studio
一般认为用户是 iOS 开发,电脑没有 Android 开发环境,首先去[官网](https://developer.android.google.cn/studio)下载。
然后通过 `flutter doctor` 来检查 flutter 的环境配置。一般可以看到多个 ✗ 。每个 ✗ 后面的描述内容都是我们需要解决的问题。
- 打开 Android studio。但是首次打开会报错提示找不到 SDK。解决方案在应用程序文件夹下面找到 Android studio显示包内容。路径如下
`/Applications/Android\ Studio.app/Contents/bin/idea.properties`,用文本编辑器打开,在最下面添加如下代码
```shell
isable.android.first.run=true
```
- 设置 Android studio 的环境变量。
```shell
export ANDROID_HOME=~/Library/Android/sdk
export PATH=${PATH}:${ANDROID_HOME}/emulator
export PATH=${PATH}:${ANDROID_HOME}/tools
export PATH=${PATH}:${ANDROID_HOME}/platform-tools
```
分别是安卓 SDK 路径、安卓模拟器路径、安卓 tools 路径、安卓平台工具。
- 安装 Android studio Flutter 插件
接下来使用 flutter doctor 检查,显示信息
```shell
✗ Flutter plugin not installed; this adds Flutter specific functionality.
✗ Dart plugin not installed; this adds Dart specific functionality.
```
意思是缺少 Flutter 插件。步骤Preferences -> Plugins -> 搜索栏输入 Flutter找到第一个点击 install。此时会弹出对话框让你选择安装 Dart点击 YES。之后重启 Android studio看到在主界面会多出下图红色框的内容。至此我们可以创建 Flutter 工程了。
![Androidstudio](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-08-12-Flutter.png)
- 给 Android studio 设置模拟器
点击右上方区域一个类似手机的按钮选择手机Pixel 2.下载对应的系统pie(需要开启翻墙模式)。
### 配置 iOS 环境
前提安装好 Xcode 和选择好对应的模拟器。并执行下面的脚本
```shell
brew link pkg-config
brew install --HEAD usbmuxd
brew unlink usbmuxd
brew link usbmuxd
brew install --HEAD libimobiledevice
brew install ideviceinstaller
```
### 配置 VSCode
安装好 VSCode在插件的地方搜索 Flutter 和 Dart 对应的插件。
![验证](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-07-22-Flutter_Verify.png)
最后输入 flutter doctor 检测你的全部是否完毕,至此你可以展开 Flutter 之旅了。祝愉快

99
Chapter1 - iOS/1.72.md Normal file
View File

@@ -0,0 +1,99 @@
# 架构心得
> 2019-07月底跳槽从事的工作内容是基础平台内容主要是基础工具和 SDK 的封装;工程化 cli 落地、研发管理、静态代码扫描等。虽然以前写代码也是站在封装、复用、聚合等出发点写代码,但是还是和真正写 SDK 注意点有很多不同,这也是为什么写这篇文章总结的原因。
## 一些注意点
- 当你开发某个功能的时候,轻易不要使用第三方的库。为什么?因为你难以确保业务方是否也在使用这个库,可能库在使用了,但是版本号不一致,就会造成 api 内部实现可能不一样,造成功能不符合预期或者一些神奇的 Bug。
- 假如你遇到上面的情况你出于某种原因不得不使用某个第三方但是你又必须考虑调用者的工程可能也加入了该库。解决方案大体有3种。1、推进业务方不要使用离散的功能三方库比如 AFNetWorkging 不要自己引入而是引入基础平台方封装好的网络功能库2、自己将引入的第三方网络库选取主要用到的功能去自己实现掉。我们首先要自己这个第三方做了什么事情提供了哪些功能其中哪些功能是我们会使用到的那么我们可以借鉴源代码自己去做类似的事情然后一个精简版的 AFNetWorking 就出来了3、将第三方库的类名称、方法名称、Block、宏...都给更换名称(一开始想到找到一定的规则用自动化脚本去做,发现这样子不可能处理全部的 case程序员自己脑子都想不全所有的 case所以代码实现根本不可能一番操作下来发现还是人工手动操作效果最好
- 当你写某个功能的时候,你封装的 SDK 对于提供某个能力,项目以组件化的形式开展,所以你对外暴露的地方在于 Router 文件中, Router 负责解析 url最后调用 [target performSelector withObject],然后在 target 对象内部真正去实现某个功能Router 一定只做最简单的事情,也就是 url parse寻找 target执行 performSelector。target 暴露某个接口,也许接口内部实现也很复杂,需要依赖其他几个 api 或者其他几个类的 api。所以 api 也就是函数需要做到单一原则。可能某个大的能力需要几个能力的聚合,这个大的函数内部依靠几个单独的函数逻辑才实现某个能力。可能由于版本迭代,你需要将之前不对外暴露的能力也要暴露出去,所以做好函数的单一功能非常重要,可拓展性强、易测试。
- 一定要写好 Unit Test。这样子不断版本迭代对于 UT输入恒定输出恒定这样内部实现如何变动不需要关心只需要判断恒定输入恒定输出就足够了。针对每个函数单一原则的基础上也是满足 UT
- 在做 SDK 的时候,对于一些方法或者函数的返回值,尽量要做到 iOS 和 Android 端的输出值的数据类型一致,除非某些特殊情况,无法保证一致的输出。
- 当你想写宏定义的时候应该先判断下是否存在,因为工程中很可能已经存在一个同名的宏。
```Objective-C
#ifndef Hi
#define Hi @"Hello, nice to meet you"
#endif
```
- 避免重复宏定义
因为宏定义可以多次,但是一个工程中有可能因为命名太规范了,大家不小心会为一个功能起一个同名的宏定义,所以我们在宏定义的时候需要做判断,不然多个同名宏定义,最后的功能会根据文件编译顺序决定,最后的宏定义才生效。
```Objective-c
#ifndef CM_IS_CLASS
#define CM_IS_CLASS(obj,cls) [obj isKindOfClass:[cls class]]
#endif
```
- 对于你的某个 SDK你在为某个方法、某个类、某个宏定义命名的时候需要注意选择合适的前缀
比如。你的某个项目是在做监控SDK 的名字叫做 Prism-Client。那么你的类名称、类方法名称、宏定义、分类名称、分类方法名称等都需要合适且统一的前缀一般选取 `前3个字母组合`。当前的项目叫做 `PCT`。类前面加 PCT类里面的方法不加前缀。分类名称加前缀 PCT分类里面的方法前面加前缀小写的 pct。
普通类的方法不加前缀是因为普通类已经通过类名的唯一性确定了方法的唯一。
分类里面方法加前缀是因为分类的方法在工程里面这个类都可以访问。所以要在方法前面区分
```Objective-C
// 安全的数据获取方法
#ifndef PCT_SAFE_STRING
#define PCT_SAFE_STRING(x) (x) != nil ? (x) : @""
#endif
NSData+PCTAES.h
- (NSData *)pct_AES128EncryptWithKey:(NSString *)key gIv:(NSString *)Iv;
PCTRequestFactory.h
+ (void)fetchUploadConfigurationWithRequestURL:(NSString *)requestUrlString
params:(NSDictionary *)params
success:(void (^)(PRCConfigurationModel*model))success
failure:(void (^)(NSError *error))failure;
```
- 一般来说如果你的某个文件代码中高频率的使用宏且宏里面是做一些运算建议使用内联函数代替因为内联函数效率高且在编译阶段可以检查错误。函数的调用顺序底层是出入栈的过程Frame Pointer、Stack Pointer。一个栈保存当前函数的局部变量、参数、返回地址。所以不同函数的调用会效率有影响如果高频使用的函数建议用内联函数。
内联函数和宏的区别
优点相比于函数
- inline 函数避免了普通函数的,在汇编时必须调用 call 的缺点:取消了函数的参数压栈,减少了调用的开销,提高效率.所以执行速度确比一般函数的执行速度要快
- 集成了宏的优点,使用时直接用代码替换(像宏一样)
优点相比于宏
- 避免了宏的缺点:需要预编译.因为 inline 内联函数也是函数,不需要预编译
- 编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性
- 可以使用所在类的保护成员及私有成员。
注意事项
- 内联函数只是我们向编译器提供的申请,编译器不一定采取inline形式调用函数
- 内联函数不能承载大量的代码.如果内联函数的函数体过大,编译器会自动放弃内联
- 内联函数内不允许使用循环语句或开关语句
- 内联函数的定义须在调用之前
- Objective-C 中内联函数用 NS_INLINE ,等价于 static inline。且内联函数的命名需要注意在该模块内的内联函数需要加前缀。
```Objective-C
NS_INLINE NSString * PCTGetTableNameFromType(PCTLogTableType type){
if (type == PCTLogTableTypeMeta) {
return PRC_LOG_TABLE_META;
}
if (type == PCTLogTableTypePayload) {
return PRC_LOG_TABLE_PAYLOAD;
}
return @"";
}
```
- 什么情况下用统跳(路由能力)?
技术 SDK 的话,因为可能依赖非常多的其他技术 SDK 所以会比较难梳理出一个需要暴露的能力,非常难抽象
业务 SDK 很清楚需要暴露哪些能力。所以我们一般将业务 SDK 提供统跳能力,技术 SDK 不提供
- 基础平台组做什么?怎么做?
业务线的同学一般做的事情就是在操作 UI手机屏幕很小要做的事情也会比较单一可能就是单击某个按钮然后多线程异步去处理某个逻辑网络、数据库、File等然后异步回调里面回调主线程去更新 UI。所以做的事情的广度不一样。基础平台组做的事情一般来说脱离独立的 UI换句话说就是焦点不在于 UI而在于整个的架构逻辑比如一个数据上报 SDK。它考虑的事情不是 UI 怎么用,而是数据来源是什么,我设计的接口需要暴露什么信息,数据如何高效存储、数据如何校验、数据如何高效及时上报。
假如我做的数据上报 SDK 可以上报 APM 监控数据、同时也开放能力给业务线使用业务线自己将感兴趣的数据并写入保存保证不丢失的情况下如何高效上报。因为数据实时上报所以需要考虑上传的网络环境、Wi-Fi 环境和 4G 环境下的逻辑不一样的、数据聚合组装成自定义报文并上报、一个自然天内数据上传需要做流量限制等等、App 版本升级一些数据可能会失去意义、当然存储的数据也存在时效性。种种这些东西就是在开发前需要考虑清楚的。所以基础平台做事情基本是 **设计思考时间:编码时间 = 7:3**
为什么假设你一个需求预期10天时间前期架构设计、类的设计、Uint Test 设计估计7天到时候编码开发2天完成。
这么做的好处很多,比如:
1. 除非是非常优秀不然脑子想的再前面到真正开发的时候发现有出入coding 完发现和前期方案设计不一样。所以建议用流程图、UML图、技术架构图、UT 也一样,设计个表格,这样等到时候编码也就是 coding 的工作了,将图翻译成代码
2. 后期和别人讨论或者沟通或者 CTO 进行 code review 的时候不需要一行行看代码。你将相关的架构图、流程图、UML 图给他看看。他再看看一些关键逻辑的 UT保证输入输出正确一般来说这样就够了
3. 软件项目管理也一样制定进度表、确定干系人、kick-of meeting 等、定期碰头
- 一般来说不要在 load 方法里面做非本类的事情。
一般来说,不应该在当前类的 `load` 方法里面写和其他类有关系的代码,除非非做不可。
```Objective-C
+ (void)load
{
NSLog(@"%zd", [AFNetworkReachabilityManager sharedManager].networkReachabilityStatus);
}
```
之前在做一个类 `PCTRequestFactory` 用来管理网络相关的逻辑。需要判断网络状态,我们都知道 AFNetWorking 第一次判断网络状态得到的是 AFNetworkReachabilityStatusUnknown。而我的逻辑需要 SDK 启动的时候判断网络状态,然后去上报数据。所以刚开始 AFNetworkReachabilityStatusUnknown 显然不能上报 Crash 数据,所以想着是将第一次的网络状态获取放到 **load** 方法里。这样是没问题的,可以拿到网络状态,但是我们知道 load 是类加载的时候调用的,打开 Xcode 看到 Build Phases 里面 `Link BiBinary With Libraries` 这个里面的库的顺序决定了里面的类加载顺序。我们知道 Pod 的原理是在 Podfile 里面描述的 pod 库依赖,然后会按照字典序(首字母排序去)引入,所以 AFNetWorking 这个肯定早,所以会成功的。但是万一是人工手动去引入或者修改库的位置,则在 PCTRequestFactory 里面的 load 方法执行的时候不一定可以保证 AFNetworkReachabilityManager 已经加载好。所以将 load 逻辑移动到 init 里面。
另外load 方法一般只做和本类有关系的逻辑,比如 hook 方法。

10
Chapter1 - iOS/1.73.md Normal file
View File

@@ -0,0 +1,10 @@
# Ruby
> 为了 iOS 工程化开展,自己最近开始了 Ruby 的学习,本篇博文就用来记录 Ruby 的学习心得和体验。
- block 最后一行是返回值,不需要指定 return
- ruby 脚本语言在编写好代码后也想像其他 GPL 语言一样断点可以,需要安装依赖和相应的代码调整
- `gem install pry`
- 文件引入 `require 'pry'`
- 需要 debug 的地方加上 `binding.pry`

739
Chapter1 - iOS/1.74.md Normal file
View File

@@ -0,0 +1,739 @@
# APM
> Application Performance Management 应用性能管理对一个应用的持续稳定运行至关重要。所以这篇文章就从一个 iOS App 的性能管理的纬度谈谈如何精确监控以及数据如何上报等技术点
App 的性能问题是影响用户体验的重要因素之一。性能问题主要包含Crash、网络请求错误或者超时、UI 响应速度慢、主线程卡顿、CPU 和内存使用率高、耗电量大等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。
本篇文章着重总结了 APM 的原因以及如何收集数据。APM 数据收集后结合数据上报机制,按照一定策略上传数据到服务端。服务端消费这些信息并产出报告。请结合[姊妹篇](./1.80.md) 总结了如何打造一款灵活可配置、功能强大的数据上报组件。
## 监控项目
- 页面渲染时长
- 主线程卡顿
- 网络错误+
- FPS
- 大文件存储
- CPU
- 内存使用
- Crash
- 启动时长
## 一、卡顿监控
卡顿问题,就是在主线程上无法响应用户交互的问题。影响着用户的直接体验,所以针对 App 的卡顿监控是 APM 里面重要的一环。
FPSframe per second每秒钟的帧刷新次数iPhone 手机以 60 为最佳iPad 某些型号是 120也是作为卡顿监控的一项参考参数为什么说是参考参数因为它不准确。先说说怎么获取到 FPS。CADisplayLink 是一个系统定时器,会以帧刷新频率一样的速率来刷新视图。 `[CADisplayLink displayLinkWithTarget:self selector:@selector(###:)]`。至于为什么不准我们来看看下面的示例代码
```Objective-C
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_displayLinkTick:)];
[_displayLink setPaused:YES];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
```
代码所示CADisplayLink 对象是被添加到指定的 RunLoop 的某个 Mode 下。所以还是 CPU 层面的操作。请继续往下看
#### 1. 屏幕绘制原理
![老式 CRT 显示器原理](./../assets/2020-02-04-ios_screen_scan.png)
讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式从上到下一行行扫描扫面完成后显示器就呈现一帧画面随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步显示器或者其他硬件会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行准备进行扫描时显示器会发出一个水平同步信号horizonal synchronization简称 HSync当一帧画面绘制完成后电子枪恢复到原位准备画下一帧前显示器会发出一个垂直同步信号Vertical synchronization简称 VSync。显示器通常以固定的频率进行刷新这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
![显示器和 CPU、GPU 关系](./../assets/2020-02-02-screen_display_gpu.png)
通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要现实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPUGPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
在帧缓冲区只有一个的情况下帧缓冲区的读取和刷新都存在效率问题为了解决效率问题显示系统会引入2个缓冲区即双缓冲机制。在这种情况下GPU 会预先渲染好一帧放入帧缓冲区让视频控制器来读取当下一帧渲染好后GPU 直接把视频控制器的指针指向第二个缓冲区。提升了效率。
目前来看双缓冲区提高了效率但是带来了新的问题当视频控制器还未读取完成时即屏幕内容显示了部分GPU 将新渲染好的一帧提交到另一个帧缓冲区并把视频控制器的指针指向新的帧缓冲区,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂的情况。
为了解决这个问题GPU 通常有一个机制叫垂直同步信号V-Sync当开启垂直同步信号后GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源
![IPC唤醒 RunLoop](./../assets/2020-02-08-ios_vsync_runloop.png)
#### 答疑
可能有些人会看到「当开启垂直同步信号后GPU 会等到视频控制器发送 V-Sync 信号后才进行新的一帧的渲染和帧缓冲区的更新」这里会想GPU 收到 V-Sync 才进行新的一帧渲染和帧缓冲区的更新,那是不是双缓冲区就失去意义了?
设想一个显示器显示第一帧图像和第二帧图像的过程。首先在双缓冲区的情况下GPU 首先渲染好一帧图像存入到帧缓冲区,然后让视频控制器的指针直接直接这个缓冲区,显示第一帧图像。第一帧图像的内容显示完成后,视频控制器发送 V-Sync 信号GPU 收到 V-Sync 信号后渲染第二帧图像并将视频控制器的指针指向第二个帧缓冲区。
**看上去第二帧图像是在等第一帧显示后的视频控制器发送 V-Sync 信号。是吗?真是这样的吗? 😭 想啥呢,当然不是。 🐷 不然双缓冲区就没有存在的意义了**
揭秘。请看下图
![多缓冲区显示原理](./../assets/2020-02-04-Comparison_double_triple_buffering.png)
当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
请查看资料,需要梯子:[Multiple buffering](https://en.m.wikipedia.org/wiki/Multiple_buffering)
#### 2. 卡顿产生的原因
![卡顿原因](./../assets/2020-02-04-ios_frame_drop.png)
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 AppApp 主线程开始在 CPU 中计算显示内容(视图绘制、图形解码、文本绘制等)。然后将计算的内容提交到 GPUGPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃,等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
目前 iOS 设备有双缓存机制也有三缓冲机制Android 现在主流是三缓冲机制,在早期是单缓冲机制。
[iOS 三缓冲机制例子](https://ios.developreference.com/article/12261072/Metal+newBufferWithBytes+usage)
CPU 和 GPU 资源消耗原因很多比如对象的频繁创建、属性调整、文件读取、视图层级的调整、布局的计算AutoLayout 视图个数多了就是线性方程求解难度变大)、图片解码(大图的读取优化)、图像绘制、文本渲染、数据库读取(多读还是多写乐观锁、悲观锁的场景)、锁的使用(举例:自旋锁使用不当会浪费 CPU等方面。开发者根据自身经验寻找最优解这里不是本文重点
#### 3. APM 如何监控卡顿并上报
CADisplayLink 肯定不用了,这个 FPS 仅作为参考。一般来讲卡顿的监测有2种方案**监听 RunLoop 状态回调、子线程 ping 主线程**
##### 3.1 RunLoop 状态监听的方式
RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、周期性或者延迟事件、异步回调等。RunLoop 会接收2种类型的输入源一种是来自另一个线程或者来自不同应用的异步消息source0事件、另一种是来自预定或者重复间隔的事件。
RunLoop 状态如下图
![RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4.png)
第一步:通知 ObserversRunLoop 要开始进入 loop紧接着进入 loop
```Objective-c
if (currentMode->_observerMask & kCFRunLoopEntry )
// 通知 Observers: RunLoop 即将进入 loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 进入loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
```
第二步:开启 do while 循环保活线程,通知 ObserversRunLoop 触发 Timer 回调、Source0 回调,接着执行被加入的 block
```Objective-c
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
// 通知 Observers: RunLoop 即将触发 Timer 回调
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
if (rlm->_observerMask & kCFRunLoopBeforeSources)
// 通知 Observers: RunLoop 即将触发 Source 回调
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 执行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
```
第三步RunLoop 在触发 Source0 回调后,如果 Source1 是 ready 状态,就会跳转到 handle_msg 去处理消息。
```Objective-c
// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}
```
第四步:回调触发后,通知 Observers 即将进入休眠状态
```Objective-c
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
```
第五步:进入休眠后,会等待 mach_port 消息以便再次唤醒。只有以下4种情况才可以被再次唤醒。
- 基于 port 的 source 事件
- Timer 时间到
- RunLoop 超时
- 被调用者唤醒
```Objective-c
do {
if (kCFUseCollectableAllocator) {
// objc_clear_stack(0);
// <rdar://problem/16393959>
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);
```
第六步:唤醒时通知 ObserverRunLoop 的线程刚刚被唤醒了
```Objective-C
// 通知 Observers: RunLoop 的线程刚刚被唤醒了
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 处理消息
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
```
第七步RunLoop 唤醒后,处理唤醒时收到的消息
- 如果是 Timer 时间到,则触发 Timer 的回调
- 如果是 dispatch则执行 block
- 如果是 source1 事件,则处理这个事件
```Objective-C
#if USE_MK_TIMER_TOO
// 如果一个 Timer 到时间了触发这个Timer的回调
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
// 如果有dispatch到main_queue的block执行block
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
}
// 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
```
第八步:根据当前 RunLoop 状态判断是否需要进入下一个 loop。当被外部强制停止或者 loop 超时,就不继续下一个 loop否则进入下一个 loop
```Objective-C
if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loop时参数说处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer一个都没有
retVal = kCFRunLoopRunFinished;
}
```
完整且带有注释的 RunLoop 代码见[此处](./../assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
![RunLoop 状态](./../assets/2020-02-05-RunLoop.png)
RunLoop 6个状态
```Objective-C
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 回调
kCFRunLoopBeforeSources , // 触发 Source0 回调
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有状态改变
}
```
RunLoop 在进入睡眠前的方法执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步,都会阻塞线程。如果是主线程,则表现为卡顿。
一旦发现进入睡眠前的 KCFRunLoopBeforeSources 状态,或者唤醒后 KCFRunLoopAfterWaiting在设置的时间阈值内没有变化则可判断为卡顿此时 dump 堆栈信息,还原案发现场,进而解决卡顿问题。
开启一个子线程,不断进行循环监测是否卡顿了。在 n 次都超过卡顿阈值后则认为卡顿了。卡顿之后进行堆栈 dump 并上报(具有一定的机制,数据处理在下一 part 讲)。
卡顿阈值的设置的依据是 WatchDog 的机制。APM 系统里面的阈值需要小于 WatchDog 的值,不需要非常小,一般认为启动时间在 5s其他状态下都是 3s。WatchDog 在不同状态下具有不同的值。
- 启动Launch20s
- 恢复Resume10s
- 挂起Suspend10s
- 退出Quit6s
- 后台Background3min在 iOS7 之前可以申请 10min之后改为 3min可连续申请最多到 10min
```Objective-c
// 设置Runloop observer的运行环境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
// 创建Runloop observer对象
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
// 将新建的observer加入到当前thread的runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 创建信号
_semaphore = dispatch_semaphore_create(0);
__weak __typeof(self) weakSelf = self;
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
while (YES) {
if (strongSelf.isCancel) {
return;
}
// N次卡顿超过阈值T记录为一次卡顿
long semaphoreWait = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
if (++strongSelf.countTime < strongSelf.standstillCount){
continue;
}
// 堆栈信息 dump 并结合数据上报机制,按照一定策略上传数据到服务器。堆栈 dump 会在下面讲解。数据上报会在 [打造功能强大、灵活可配置的数据上报组件](./1.80.md) 讲
}
}
strongSelf.countTime = 0;
}
});
```
可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
# todo虽然知道 RunLoop 的状态,但是为什么是 dispatch_semaphore_wait
##### 3.2 子线程 ping 主线程监听的方式
开启一个子线程创建一个初始值为0的信号量、一个初始值为 YES 的布尔值类型标志位。将设置标志位为 NO 的任务派发到主线程中去,子线程休眠阈值时间,时间到后判断标志位是否被主线程成功(值为 NO如果没成功则认为猪线程发生了卡顿情况此时 dump 堆栈信息并结合数据上报机制,按照一定策略上传数据到服务器。数据上报会在 [打造功能强大、灵活可配置的数据上报组件](./1.80.md) 讲
```Objective-c
while (self.isCancelled == NO) {
@autoreleasepool {
__block BOOL isMainThreadNoRespond = YES;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_main_queue(), ^{
isMainThreadNoRespond = NO;
dispatch_semaphore_signal(semaphore);
});
[NSThread sleepForTimeInterval:self.threshold];
if (isMainThreadNoRespond) {
if (self.handlerBlock) {
self.handlerBlock(); // 外部在 block 内部 dump 堆栈(下面会讲),数据上报
}
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
}
```
#### 4. 堆栈 dump
方法堆栈的获取是一个麻烦事。理一下思路。`[NSThread callStackSymbols]` 可以获取当前线程的调用栈。但是当监控到卡顿发生,需要拿到主线程的堆栈信息就无能为力了。从任何线程回到主线程这条路走不通。先做个知识回顾。
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
维基百科搜索到 “Call Stack” 的一张图和例子,如下
![调用栈](./../assets/2020-02-08-StackFrame.png)
上图表示为一个栈。分为若干个栈帧Frame每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B调用函数B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。
栈指针 Stack Pointer 表示当前栈的顶部,大多部分操作系统都是栈向下生长,所以栈指针是最小值。帧指针 Frame Pointer 指向的地址中,存储了上一次 Stack Pointer 的值,也就是返回地址。
大多数操作系统中,每个栈帧还保存了上一个栈帧的帧指针。因此知道当前栈帧的 Stack Pointer 和 Frame Pointer 就可以不断回溯,递归获取栈底的帧。
接下来的步骤就是拿到所有线程的 Stack Pointer 和 Frame Pointer。然后不断回溯还原案发现场。
App 在运行的时候,会对应一个 Mach Task而 Task 下可能有多条线程同时执行任务。《OS X and iOS Kernel Programming》 中描述 Mach Task 为任务Task是一种容器对象虚拟内存空间和其他资源都是通过这个容器对象管理的这些资源包括设备和其他句柄。
系统方法 `kern_return_t task_threads(task_inspect_t target_task, thread_act_array_t *act_list, mach_msg_type_number_t *act_listCnt);` 可以获取到所有的线程,不过这种方法获取到的线程信息是最底层的 **mach 线程**。
对于每个线程,可以用 `kern_return_t thread_get_state(thread_act_t target_act, thread_state_flavor_t flavor, thread_state_t old_state, mach_msg_type_number_t *old_stateCnt);` 方法获取它的所有信息,信息填充在 `_STRUCT_MCONTEXT` 类型的参数中这个方法中有2个参数随着 CPU 架构不同而不同。所以需要定义宏屏蔽不同 CPU 之间的区别。
`_STRUCT_MCONTEXT` 结构体中,存储了当前线程的 Stack Pointer 和最顶部栈帧的 Frame pointer进而回溯整个线程调用堆栈。
但是上述方法拿到的是内核线程,我们需要的信息是 NSThread所以需要将内核线程转换为 NSThread。
pthread 的 p 是 **POSIX** 的缩写表示「可移植操作系统接口」Portable Operating System Interface。设计初衷是每个系统都有自己独特的线程模型且不同系统对于线程操作的 API 都不一样。所以 POSIX 的目的就是提供抽象的 pthread 以及相关 API。这些 API 在不同的操作系统中有不同的实现,但是完成的功能一致。
Unix 系统提供的 `task_threads``thread_get_state` 操作的都是内核系统,每个内核线程由 thread_t 类型的 id 唯一标识。pthread 的唯一标识是 pthread_t 类型。其中内核线程和 pthread 的转换(即 thread_t 和 pthread_t很容易因为 pthread 设计初衷就是「抽象内核线程」。
`pthread_create` 方法创建线程的回调函数为 **nsthreadLauncher**
```Objective-c
static void *nsthreadLauncher(void* thread)
{
NSThread *t = (NSThread*)thread;
[nc postNotificationName: NSThreadDidStartNotification object:t userInfo: nil];
[t _setName: [t name]];
[t main];
[NSThread exit];
return NULL;
}
```
NSThreadDidStartNotification 其实就是字符串 @"_NSThreadDidStartNotification"。
```Objective-c
<NSThread: 0x...>{number = 1, name = main}
```
为了 NSThread 和内核线程对应起来,只能通过 name 一一对应。 pthread 的 API `pthread_getname_np` 也可获取内核线程名字。np 代表 not POSIX所以不能跨平台使用。
思路概括为:将 NSThread 的原始名字存储起来,再将名字改为某个随机数(时间戳),然后遍历内核线程 pthread 的名字,名字匹配则 NSThread 和内核线程对应了起来。找到后将线程的名字还原成原本的名字。对于主线程,由于不能使用 `pthread_getname_np`,所以在当前代码的 load 方法中获取到 thread_t然后匹配名字。
```Objective-c
static mach_port_t main_thread_id;
+ (void)load {
main_thread_id = mach_thread_self();
}
```
## 二、 App 启动时间监控
#### 1. App 启动时间的监控
思路比较简单。如下
- 在监控类的 `load` 方法中先拿到当前的时间值
- 监听 App 启动完成后的通知 `UIApplicationDidFinishLaunchingNotification`
- 收到通知后拿到当前的时间
- 步骤1和3的时间差就是 App 启动时间。
`mach_absolute_time` 是一个 CPU/总线依赖函数返回一个基于系统启动后的时钟的“嘀嗒”数。系统休眠时不会增加。是一个纳秒级别的数字。获取前后2个纳秒后需要转换到秒。需要基于系统时间的基准通过 `mach_timebase_info` 获得。
```Objective-c
mach_timebase_info_data_t g_cmmStartupMonitorTimebaseInfoData = 0;
mach_timebase_info(&g_cmmStartupMonitorTimebaseInfoData);
uint64_t timelapse = mach_absolute_time() - g_cmmLoadTime;
double timeSpan = (timelapse * g_cmmStartupMonitorTimebaseInfoData.numer) / (g_cmmStartupMonitorTimebaseInfoData.denom * 1e9);
```
#### 2. 线上监控启动时间就好,但是在开发阶段需要对启动时间做优化。
要优化启动时间,就先得知道在启动阶段到底做了什么事情,针对现状作出方案。
pre-main 阶段定义为 App 开始启动到系统调用 main 函数这个阶段main 阶段定义为 main 函数入口到主 UI 框架的 viewDidAppear。
App 启动过程:
- 解析 Info.plist加载相关信息例如闪屏沙盒建立、权限检查
- Mach-O 加载:如果是胖二进制文件,寻找合适当前 CPU 架构的部分;加载所有依赖的 Mach-O 文件(递归调用 Mach-O 加载的方法定义内部、外部指针引用例如字符串、函数等加载分类中的方法c++ 静态对象加载、调用 Objc 的 `+load()` 函数;执行声明为 __attribute_((constructor)) 的 c 函数;
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching()
Pre-Main 阶段
![Pre-Main 阶段](./../assets/2020-02-10-AppSpeed-PreMain.png)
Main 阶段
![Main 阶段](./../assets/2020-02-10-AppSpeed-Main.png)
##### 2.1 加载 Dylib
每个动态库的加载dyld 需要
- 分析所依赖的动态库
- 找到动态库的 Mach-O 文件
- 打开文件
- 验证文件
- 在系统核心注册文件签名
- 对动态库的每一个 segment 调用 mmap
优化:
- 减少非系统库的依赖
- 使用静态库而不是动态库
- 合并非系统动态库为一个动态库
##### 2.2 Rebase && Binding
优化:
- 减少 Objc 类数量,减少 selector 数量,把未使用的类和函数都可以删掉
- 减少 c++ 虚函数数量
- 转而使用 Swift struct本质就是减少符号的数量
##### 2.3 Initializers
优化:
- 使用 `+initialize` 代替 `+load`
- 不要使用过 attribute*((constructor)) 将方法显示标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_one、pthread_once() 或 std::once()。也就是第一次使用时才初始化,推迟了一部分工作耗时也尽量不要使用 c++ 的静态对象
##### pre-main 阶段影响因素
- 动态库加载越多,启动越慢。
- ObjC 类越多,函数越多,启动越慢。
- 可执行文件越大启动越慢。
- C 的 constructor 函数越多,启动越慢。
- C++ 静态对象越多,启动越慢。
- ObjC 的 +load 越多,启动越慢。
优化手段:
- 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库
- 检查下 framework应当设为optional和required如果该framework在当前App支持的所有iOS系统版本都存在那么就设为required否则就设为optional因为optional会有些额外的检查
- 合并或者删减一些OC类和函数。关于清理项目中没用到的类使用工具AppCode代码检查功能查到当前项目中没有用到的类也可以用根据linkmap文件来分析但是准确度不算很高
有一个叫做[FUI](https://github.com/dblock/fui)的开源项目能很好的分析出不再使用的类准确率非常高唯一的问题是它处理不了动态库和静态库里提供的类也处理不了C++的类模板
- 删减一些无用的静态变量
- 删减没有被调用到或者已经废弃的方法
- 将不必须在 +load 方法中做的事情延迟到 +initialize中尽量不要用 C++ 虚函数(创建虚函数表有开销)
- 类和方法名不要太长iOS每个类和方法名都在 __cstring 段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的
因还是 Object-c 的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用Object-c 对象模型会把类/方法名字符串都保存下来;
- 用 dispatch_once() 代替所有的 attribute((constructor)) 函数、C++ 静态对象初始化、ObjC 的 +load 函数;
- 在设计师可接受的范围内压缩图片的大小,会有意外收获。
压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,
图片小了IO操作量就小了启动当然就会快了比较靠谱的压缩算法是 TinyPNG。
##### main 阶段优化
- 减少启动初始化的流程。能懒加载就懒加载,能放后台初始化就放后台初始化,能延迟初始化的就延迟初始化,不要卡主线程的启动时间,已经下线的业务代码直接删除
- 优化代码逻辑。去除一些非必要的逻辑和代码,减小每个流程所消耗的时间
- 启动阶段使用多线程来进行初始化,把 CPU 性能发挥最大
- 使用纯代码而不是 xib 或者 storyboard 来描述 UI尤其是主 UI 框架,比如 TabBarController。因为 xib 和 storyboard 还是需要解析成代码来渲染页面,多了一步。
## 三、 CPU 使用率监控
#### 1. CPU 架构
CPUCentral Processing Unit中央处理器市场上主流的架构有 ARMarm64、Intelx86、AMD 等。其中 Intel 使用 CISCComplex Instruction Set ComputerARM 使用 RISCReduced Instruction Set Computer。区别在于**不同的 CPU 设计理念和方法**。
早期 CPU 全部是 CISC 架构,设计目的是**用最少的机器语言指令来完成所需的计算任务**。比如对于乘法运算,在 CISC 架构的 CPU 上。一条指令 `MUL ADDRA, ADDRB` 就可以将内存 ADDRA 和内存 ADDRB 中的数香乘,并将结果存储在 ADDRA 中。做的事情就是:将 ADDRA、ADDRB 中的数据读入到寄存器,相乘的结果写入到内存的操作依赖于 CPU 设计,所以** CISC 架构会增加 CPU 的复杂性和对 CPU 工艺的要求。**
RISC 架构要求软件来指定各个操作步骤。比如上面的乘法,指令实现为 `MOVE A, ADDRA; MOVE B, ADDRB; MUL A, B; STR ADDRA, A;`。这种架构可以降低 CPU 的复杂性以及允许在同样的工艺水平下生产出功能更加强大的 CPU但是对于编译器的设计要求更高。
目前市场是大部分的 iPhone 都是基于 arm64 架构的。且 arm 架构能耗低。
#### 2. 获取线程信息
讲完了区别来讲下如何做 CPU 使用率的监控
- 开启定时器,按照设定的周期不断执行下面的逻辑
- 获取当前任务 task。从当前 task 中获取所有的线程信息(线程个数、线程数组)
- 遍历所有的线程信息,判断是否有线程的 CPU 使用率超过设置的阈值
- 假如有线程使用率超过阈值,则 dump 堆栈
- 组装数据,上报数据
线程信息结构体
```Objective-c
struct thread_basic_info {
time_value_t user_time; /* user run time用户运行时长 */
time_value_t system_time; /* system run time系统运行时长 */
integer_t cpu_usage; /* scaled cpu usage percentageCPU使用率上限1000 */
policy_t policy; /* scheduling policy in effect有效调度策略 */
integer_t run_state; /* run state (运行状态,见下) */
integer_t flags; /* various flags (各种各样的标记) */
integer_t suspend_count; /* suspend count for thread线程挂起次数 */
integer_t sleep_time; /* number of seconds that thread
* has been sleeping休眠时间 */
};
```
代码在讲堆栈还原的时候讲过,忘记的看一下上面的分析
```Objective-C
thread_act_array_t threads;
mach_msg_type_number_t threadCount = 0;
const task_t thisTask = mach_task_self();
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
if (kr != KERN_SUCCESS) {
return ;
}
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount;
kern_return_t kr = thread_info((thread_inspect_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount);
if (kr == KERN_SUCCESS) {
threadBaseInfo = (thread_basic_info_t)threadInfo;
// todo条件判断看不明白
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;
if (cpuUsage > CPUMONITORRATE) {
NSMutableDictionary *CPUMetaDictionary = [NSMutableDictionary dictionary];
NSData *CPUPayloadData = [NSData data];
NSString *backtraceOfAllThread = [BacktraceLogger backtraceOfAllThread];
// 1. 组装卡顿的 Meta 信息
CPUMetaDictionary[@"MONITOR_TYPE"] = CMMonitorCPUType;
// 2. 组装卡顿的 Payload 信息一个JSON对象对象的 Key 为约定好的 STACK_TRACE value 为 base64 后的堆栈信息)
NSData *CPUData = [SAFE_STRING(backtraceOfAllThread) dataUsingEncoding:NSUTF8StringEncoding];
NSString *CPUDataBase64String = [CPUData base64EncodedStringWithOptions:0];
NSDictionary *CPUPayloadDictionary = @{@"STACK_TRACE": SAFE_STRING(CPUDataBase64String)};
NSError *error;
// NSJSONWritingOptions 参数一定要传0因为服务端需要根据 \n 处理逻辑,传递 0 则生成的 json 串不带 \n
NSData *parsedData = [NSJSONSerialization dataWithJSONObject:CPUPayloadDictionary options:0 error:&error];
if (error) {
CMMLog(@"%@", error);
return;
}
CPUPayloadData = [parsedData copy];
// 3. 数据上报会在 [打造功能强大、灵活可配置的数据上报组件](./1.80.md) 讲
[[PrismClient sharedInstance] sendWithType:CMMonitorCPUType meta:CPUMetaDictionary payload:CPUPayloadData];
}
}
}
}
```
## 四、App 占有内存
#### 1. 基础知识回顾
硬盘:也叫做磁盘,用于存储数。你存储的歌曲、图片、视频都是在硬盘里。
内存:由于硬盘读取速度较慢,如果 CPU 运行程序期间,所有的数据都直接从硬盘中读取,则非常影响效率。所以 CPU 会将程序运行所需要的数据从硬盘中读取到内存中。然后 CPU 与内存中的数据进行计算、交换。内存是易失性存储器(断电后,数据消失)。内存条区是计算机内部(在主板上)的一些存储器,用来保存 CPU 运算的中间数据和结果。内存条是程序与 CPU 之间的桥梁。从硬盘读取出数据或者运行程序提供给 CPU。
**虚拟内存** 是计算机系统内存管理的一种技术。它使得程序认为它拥有连续的可用内存而实际上它通常被分割成多个物理内存碎片还是部分暂时存储在外部磁盘硬盘存储器上当需要使用时则用硬盘中数据交换到内存中。Windows 系统中称为 “虚拟内存”Linux/Unix 系统中称为 ”交换空间“。
内存RAM与 CPU 一样都是系统中最稀少的资源也很容易发生竞争应用内存与性能直接相关。iOS 没有交换空间作为备选资源,所以内存资源尤为重要。
内存过大的几种情况
- App 内存消耗较低,同时其他 App 内存管理也很棒,那么即使切换到其他 App我们自己的 App 依旧是“活着”的,保留了用户状态。体验好
- App 内存消耗较低,但其他 App 内存消耗太大(可能是内存管理糟糕,也可能是本身就耗费资源,比如游戏),那么除了在前台的线程,其他 App 都会被系统杀死,回收内存资源,用来给活跃的进程提供内存。
- App 内存消耗较大,切换到其他 App 后,即使其他 App 向系统申请的内存不大,系统也会因为内存资源紧张,优先把内存消耗大的 App 杀死。表现为用户将 App 退出到后台,过会儿再次打开会发现 App 重新加载启动。
- App 内存消耗非常大,在前台运行时就被系统杀死,造成闪退。
#### 2. 获取内存信息
// todoAPM 下 CPU 可以分析什么?什么场景?参数还是单独
App 内存信息的 API 可以在 Mach 层找到,`mach_task_basic_info` 结构体存储了 Mach task 的内存使用信息,其中 resident_size 就是应用使用的物理内存大小virtual_size 是虚拟内存大小。
```Objective-c
#define MACH_TASK_BASIC_INFO 20 /* always 64-bit basic info */
struct mach_task_basic_info {
mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
mach_vm_size_t resident_size; /* resident memory size (bytes) */
mach_vm_size_t resident_size_max; /* maximum resident memory size (bytes) */
time_value_t user_time; /* total user run time for
terminated threads */
time_value_t system_time; /* total system run time for
terminated threads */
policy_t policy; /* default policy for new threads */
integer_t suspend_count; /* suspend count for task */
};
```
所以获取代码为
```Objective-c
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kr = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
if (kr != KERN_SUCCESS) {
return ;
}
CGFloat memoryUsed = (CGFloat)(vmInfo.phys_footprint/1024.0/1024.0);
```
可能有人好奇不应该是 `resident_size` 这个字段获取内存的使用情况吗?一开始测试后发现 resident_size 和 Xcode 测量结果差距较大。而使用 phys_footprint 则接近于 Xcode 给出的结果。且可以从 [WebKit 源码](https://github.com/WebKit/webkit/blob/52bc6f0a96a062cb0eb76e9a81497183dc87c268/Source/WTF/wtf/cocoa/MemoryFootprintCocoa.cpp)中得到印证。
基于近期的发现,可以在线下获取 App 的 high water mark也就是 oom 内存阈值。 那么就产生了方案3
所以对于内存的监控思路就是找到系统给 App 的内存上限然后当接近内存上限值的时候dump 内存情况,组装基础数据信息成一个合格的上报数据,经过一定的数据上报策略到服务端,服务端消费数据,分析产生报表,客户端工程师根据报表分析问题。不同工程的数据以邮件、短信、企业微信等形式通知到该项目的 owner、开发者。情况严重的会直接电话给开发者并给主管跟进每一步的处理结果
问题分析处理后要么发布新版本,要么 hot fix。
监控内存增长在达到  high water mark 附近的时候dump 内存信息,获取对象名称、对象个数、各对象的内存值;如果稳定可以全量开启,不会有性能问题
OOMDetector 可以拿到分配内存的堆栈,对于定位到代码层面更加有效;可以灰度开放
## 五、 App 网络监控
## 参考资料
- [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)
- [Call Stack](https://en.wikipedia.org/wiki/Call_stack)
- [关于函数调用栈(call stack)的个人理解](https://blog.csdn.net/VarusK/article/details/83031643)
- [获取任意线程调用栈的那些事](https://bestswifter.com/callstack/)
- [iOS启动时间优化](https://www.zoomfeng.com/blog/launch-time.html)
- [WWDC2019之启动时间与Dyld3](https://www.zoomfeng.com/blog/launch-optimize-from-wwdc2019.html)

29
Chapter1 - iOS/1.75.md Normal file
View File

@@ -0,0 +1,29 @@
# 如何写好测试
> 软件测试的功能非常重要,现在结合过往经验谈谈如何做软件测试
- 每个类具有什么属性、具有什么方法都是确定的,所以拿 Objective-C 举例子,`.h` 中会有公有的属性以及方法,`.m` 中一般是私有属性和私有方法、公有方法。类的行为设计为方法,写代码之前我们一般需要做到心中有数,一个功能需要几个类、每个类的属性和方法分别是什么,需要暴露什么属性和接口。**UML图** 不是必须要画,但是需要胸有成竹。针对一个类在测试的时候 `.m` 文件中的私有方法没有办法在 Unit Test 类中访问,一般可以将需要测试的类增加分类。在分类的 `.h` 文件中将方法进行声明。在测试的 Uint Test 中引入分类头文件。下面举例子
```Objective-C
// Person.h
- (void)eat;
// Person.m
- (void)eat
{
NSLog(@"eat");
}
- (vood)sleep
{
NSLog(@"sleep");
}
// Person+UnitTestHelper.h
- (void)sleep;
```
另一种思路是没必要针对每个类的私有方法或者每个方法进行测试,因为等全部功能做完后针对每个类的接口测试,一般会覆盖据大多数的方法。等测试完看如果方法未被覆盖,则针对性的补充 Unit Test

37
Chapter1 - iOS/1.76.md Normal file
View File

@@ -0,0 +1,37 @@
# iOS Crash分析
> 目前在重构 APM,在做测试的时候有这样一个需求,分析线上环境的 top10 Crash, APM SDK 的提测 Demo 工程中需要有模拟能力并全链路分析是否可以在 Demo 工程中生成 Crash -> 上报组件上报 -> 服务端符号化. 在查询目前 App 的 top 10 Crash 的时候发现系统层面的 Crash 还是无法符号化成功. 虽然这不重要,但是还是想探讨下如何可以解析这种系统层面的 Crash 信息.
## 背景
APM 监控到 Crash,然后线程回溯拿到堆栈信息,再调用上报组件的能力,上报组件按照一定的策略去上报数据,服务端符号化解析堆栈信息.为了方便查看拿 stack_trace_id 去查询堆栈信息.接口信息如下图
![接口堆栈信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-10-10-APM_Stack_trace_api.png)
拿到堆栈信息里面的 json 本地保存成拓展名为 ***.crash** 文件,Mac 可以打开拓展名为 crash 的文件. 然后根据 **Crashed Thread** 后面的数字去查找对应的 Thread 里面的信息
![Crash 信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-10-10-APM_Crash.png)
结果发现是系统层级的信息,看不懂
```Shell
Thread 12 Crashed:
0 libsystem_platform.dylib 0x00000001c34a87e8 0x1c34a2000 + 26600 (<redacted> + 16)
1 Foundation 0x00000001c42fe698 0x1c41ea000 + 1132184 (<redacted> + 52)
2 Foundation 0x00000001c42091fc 0x1c41ea000 + 127484 (<redacted> + 1744)
3 Foundation 0x00000001c4208af4 0x1c41ea000 + 125684 (<redacted> + 1232)
4 Foundation 0x00000001c42fecec 0x1c41ea000 + 1133804 (<redacted> + 272)
5 libdispatch.dylib 0x00000001c32d8a38 0x1c3279000 + 391736 (<redacted> + 24)
6 libdispatch.dylib 0x00000001c32d97d4 0x1c3279000 + 395220 (<redacted> + 16)
7 libdispatch.dylib 0x00000001c32b0c34 0x1c3279000 + 228404 (<redacted> + 404)
8 libdispatch.dylib 0x00000001c32b0314 0x1c3279000 + 226068 (<redacted> + 592)
9 libdispatch.dylib 0x00000001c32bc9d4 0x1c3279000 + 276948 (<redacted> + 340)
10 libdispatch.dylib 0x00000001c32bd248 0x1c3279000 + 279112 (<redacted> + 116)
11 libsystem_pthread.dylib 0x00000001c34b91b4 0x1c34ad000 + 49588 (_pthread_wqthread + 464)
```
为了搞懂这种 Crash,这篇文章就诞生了.
##

264
Chapter1 - iOS/1.77.md Normal file
View File

@@ -0,0 +1,264 @@
# iOS 打包系统构建加速
### 目标
> iOS 单包构建加速、支持多包并行打包
### 基础知识
CI、CD 在稍微有点规模的公司内部都会内建一套自己的系统。目前主流的是在 Jenkins 的基础上进行的打包系统。公司只有1个 App 的情况下一台打包机就够了,但是有多个 SDK、App 那肯定不够的,各个业务线都需要测试、上架等等,任务太多了,一台机器别人要等到花儿谢了...
分布式构建系统可解决上述问题,即一个 master 为中心,多个 slave 来进行具体的构建操作。多台执行机来进行任务的构建以及自动化脚本的执行。Jenkins 具备分布式特性,是 Master/Slave 模式主从模式将设备分为主设备和从设备主设备负责分配工作并整合结果或作为指令的来源从设备负责完成任务从设备一般只和主设备通信。这个模式有2个好处
- 能够有效分担主节点的压力,加快构建速度
- 能够指定特定的任务在特定的主机上进行
## 背景
![打包平台](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/2019-12-16-candle.png)
- 描述现状
我们公司的 WAES 平台下子平台 candle 是专门用来打包构建的,可以打包 iOS SDK、iOS App、Android SDK、Android App、React Native 包、H5、Node 包、React 包等等。iOS 端将 SDK、App 通过 candle 打包平台进行任务创建、排队、打包,根据任务的特点和语言去调度合适的打包机器进行打包。 现状是整体速度觉得较慢,还有加速空间。
- 问题原因
1. iOS 打包目前都是在使用旧的打包构建系统,所以在单包构建方面会慢一些;
2. 在 pod install 这一步,打包机使用的 1.3.1 版本的 cocoapods 版本在进行依赖分析,它本质上是操纵打包机上全局的 git 目录,由于本质如此所以没办法多包并行打包。
- 风险预警
1. cocoapods 升级到最新版本,目前的 cocoapods 的相关的 ruby 脚本可能会有问题,没办法良好运行。
2. 开启 Xcode 的新构建系统,可能会造成现有工程报错,没办法编译成功,需要改动。这些改动可能不只是主工程,也许是各个 SDK 自身的修改,所以会比较零散。
## 改造
#### 单包加速
##### 一些背景:
1. 打包机暂时不支持各个依赖的 SDK 以静态库的方式引入。
原因是目前 APM 监控系统不支持。因为多个静态库则会生成多个 DYSM 文件,这样子 APM 定位 Crash、ANR 等都会生成多个 DYSM 文件的信息,配套的后端在符号化处理的时候只支持一个 DYSM 的模式,所以在如此背景下打包机不支持静态库。
2. 看到博客说可以通过 `generate_multiple_pod_projects``disable_input_output_paths` 来加速构建速度,这些在本地开发过程中是可以提高构建速度的。但是在打包机这种环境下是不太适用的。因为 iOS 在打包机环境下都会执行 pod install 的过程.
```ruby
install! 'cocoapods', :generate_multiple_pod_projects => true, :incremental_installation => true, :disable_input_output_paths => true
```
- generate_multiple_pod_projects
生成多个 XcodeProj。在 1.7.0 之前cocoapods 只生成一个 Pod.xcodeproj,随着 Library 增多,文件越来越大,解析时间越来越长,在 1.7.0 之后每个 Library 都允许生成单独的 Project提高项目的编译时间。 默认关闭
- incremental_installation
增量安装,每次执行 pod install 都会生成整个 workspace现在支持只更新的 Library 编译。节省时间
3. 另外网上的部分优化提速手段也不太适合,因为这些手段基本上只是会加快一些速度,但是不可能把一个项目的构建速度提升明显,所以这次的方案主要是单包开启 New Build System 和支持多包并行能力。
##### 理论基础
本质上就是开启 New Build System苹果在 WWDC 2017 中描述新构建系统的有点为:**降低构建开销,尤其可以降低大型项目的构建开销**。但是在新构建系统下现有的工程会报错。经过查看报错信息,基本都是在资源方面的错误(图片等)和偶尔一些 SDK 不规范造成的问题。
苹果从 Xcode 9 开始推出了新构建系统New Build System并在 Xcode 10 使用其为默认构建系统来替代旧构建系统Legacy Build System。采用新构建系统能够减少构建时间。
简要介绍一下原理,对于旧构建系统,当我们构建一个程序的时候,会明确所需要构建的所有 Target Dependencies、Link Binary With Libraries这些 Target 之间的依赖关系,以及这些 Target 构建的顺序。采用顺序会造成多处理器系统资源的浪费,从而表现为编译时间的浪费,解决这个问题的方式就是采用并行编译,这也是新构建系统优化的核心思想。详细了解新构建系统,探究 Xcode New Build System 对于构建速度的提升。
##### 测试实验
注意: 报错提示找不到 `coderay`. 可以运行 `sudo gem install coderay` 解决该问题。
本地 cocoapods 版本为 1.4.0,打包机环境为 1.3.1,所以方案评估有问题。这几天花时间做了对比实验,数据如下
1. New Build System 是否可以让单包构建变快?
cocoapods 模拟打包机环境 1.3.1。在 Legacy Build System 和 New Build System 下运行项目。
1.3.1 不能开启 New Build System。报错信息 New Build System Multiple commands produce script phase “[CP] Copy Pods Resources”
1.3.1 Legacy Build System 构建时间为 335.4s.
所以尝试升级 cocoapods 继续做对比实验
2. cocoapods 小版本升级到 1.4.0 在 Legacy Build System 和 New Build System 下运行项目(为什么选择升级到 1.4.0 cocoapods 小版本升级则改动较小,业务线可以快速享受到 New Build System 改动带来的收益)
New Build System: 383.5s
Legacy Build System: 302.9s
3. 升级 cocoapods 到 1.8.0,查看在 New Build System 和 Legacy Build System 下的构建时间
cocoapods 升级到 1.8.0 会报错,修改错误后运行对比。
New Build System: 324.4s
Legacy Build System: 262.2s
实验数据如下:
| App| 构建系统 | Cocoapods 版本 | Build 结果 | 编译时间 |
|:-:|:-:|:-:|:-:|:-:|
| **App | New Build System |1.3.1 | 失败 | ~ |
| **App | Legacy Build System |1.3.1| 成功| 335.4s |
| **App | New Build System | 1.4.0 | 失败 | 383.5s |
| **App | Legacy Build System |1.4.0 | 成功 | 302.9s |
| **App | New Build System | 1.8.4 |成功 | 324.4s|
| **App | Legacy Build System | 1.8.4 | 成功 | 262.2s |
结论:从实验数据来看, New Build System 并不能单包加速。所以 New Build System 不做了。构建加速是升级cocoapods 1.8.4 带来的,并不是 new build system 带来的。后续计划分2步
1. 升级 cocoapods 到 1.8.4,可以体验到单包构建加速的效果。
2. 自建 CDN。
拿自己的电脑部署脚本,当作本地打包机;拿**App App 打包,指定打包机为自己的电脑
| App| Cocoapods 版本 | 编译时间 |
|:-:|:-:|:-:|
| **App | 1.3.1 | 8m37s |
| **App | 1.8.4 | 7min47s |
开启 New Build System 带来的改动
1. SDK 中图片是通过 resource 的方式管理的cocoapods 1.8.4 会将它打包到 `Assets.car` 和 App 主工程图片打包的结果一致,导致 Xcode 主工程报错,大体意思是说工程包含多个 **Assets.car**. 原因在于 SDK 通过 resource 管理图片,打出包所以可以使用 ``resource_bundles`` 的形式管理
- 涉及到的 SDKTrinityConfiguration公共 SDK
- 改造点:
注意:改变 SDK 内部修改 xcassets 文件名是无用的Xcode 编译后查看包内容,结果还是 `Assests.car` 。
图片使用方式改变。由之前的 **resources** 方式改为 **`resource_bundles`** 的形式。这样做有2个优点解决了图片资源打包后造成 `Assets.car` 冲突的问题;`resource_bundles` 还可以解决图片访问速度的优化。
2. 图片资源重复
**App工程中有些图片和 理财的 SDK `SdkFinanceHome` 里面的图片资源重名,但是内容却不一致,需要协商改动。
- 涉及到的 SDKSdkFinanceHome (理财业务线 SDK
- 改造点:
有2张图片在 SdkFinanceHome SDK 内重复出现2次(形状、大小一致)。App 也存在同名的图片,图形一致、尺寸大小不一致。所以需要**App业务线开发者确认保留什么图片或者资源重命名。建议图片资源也用 **`resource_bundles`** 的形式管理
```ruby
s.resource_bundles = {
'SdkFinanceHome' => ['***/Assets/*.xcassets']
}
```
升级 1.8.4 带来的改动点:
1. 部分 SDK 的头文件引用方式有问题
- 涉及到的 SDKSdkFundWax
- 改造点:将 `FCH5AuthRouter.m` 文件中关于 NativeQS 中头文件的引入方式改变下。`#import <NQSParser.h>` 改为 `#import <NativeQS/NativeQS.h>`,或者改为 `#import "NQSParser.h"`
```ruby
"dependencies": {
"NativeQS": "~> 1.0"
},
```
注意:
因为打包机目前是源码引入编译成 .a 文件。如果是以 framework 的形式,则必须以依赖描述的方式进行调整。
新版本 cocoapods 中:
- cocoapods 在 SDK 里面引用别的 SDK 如果 **podspec** 里面存在 **dependencies** 描述,则可以使用 `#import <NativeQS/NativeQS.h>` 或者 `#import "NativeQS.h"`;如果不存在 **dependencies** 描述,则需要使用 `#import <NativeQS/NativeQS.h>`
- 在主工程中引用 SDK 的头文件,使用 `#import <NativeQS/NativeQS.h>`、`#import "NativeQS.h"` 都可以
旧版本 cocoapods 中:
- SDK、App 主工程都可以使用 `#import <NativeQS/NativeQS.h>`、`#import "NativeQS.h"`、`#import <NativeQS.h>`
2. 部分 SDK 的使用了未在 podspec 文件中声明的依赖,在新版本 cocoapods 下会报错(某些 SDK 由于历史原因造成新版本丢失依赖描述)
- 涉及到的 SDKCMRCTToast
- 改造点:
问题基本定位是在于, App 主工程引用的 SdkBbs2 SDK 依赖了 SdkBbs2 版本(该版本的依赖描述为 `CMRCTToast (~> 0.1)` )
历史原因: 早期在做 RN SDK 封装的时候在第一个版本的时候只有某个版本的 React Native 库,所以在 `0.1.0` 的时候依赖的描述可以看到如下的代码
```ruby
s.dependency 'React/Core', '0.41.2'
s.dependency 'React/RCTNetwork', '0.41.2'
s.dependency 'React/RCTImage', '0.41.2'
s.dependency 'React/RCTText', '0.41.2'
s.dependency 'React/RCTWebSocket', '0.41.2'
s.dependency 'React/RCTAnimation', '0.41.2'
```
随着版本的不断迭代,在第二个版本 `0.1.1` 的时候可以看到下面的描述. 可以看到对 RN 的描述不存在了,因为当时的代码对 RN 的2个版本都做了兼容所以 App 主工程肯定是有 RN 的库,所以索性就不在单独描述,直接随着 App 依赖的 RN 库而使用。之后的版本也是如此。
```ruby
s.dependency 'CMDevice', '~> 0.1'
```
所以, 将 `CMRCTToast.podspec` 中的依赖修改掉。需要兼容不同 RN SDK 的版本。
3. 部分 pod 的 hook 脚本会失败。
- 涉及到的 SDK
- 改造点:
`TrinityParams.rb` 类方法 `generate_mods` 会报错。逻辑是通过遍历每个 pod_target,获取到 PBXNativeTarget,然后访问 source_build_phase 属性去遍历内部的每个文件,判断是否是 `properties.yml`。
1. [官方文档地址](https://rubydoc.info/gems/xcodeproj/Xcodeproj/Project/Object/PBXNativeTarget#source_build_phase-instance_method).
2. 公共组做的库Android 和 iOS 都是对应的,但是 SDK 的名字不一定严格一致。但是通跳后台是配置的时候不可能设置多个名字,所以设置了一个通用的名字,然后 iOS SDK 和 Android SDK 各自用一个描述文件将本地的 SDK 和下发需要命中的 SDK 名字做一个对映射关系。iOS 端用 `properties.yml`来描述
cocoapods 新版本里面每个 pod_target 没有 native_target 属性,也就是没办法获取到 PBXNativeTarget。
感觉之前的脚本写法有问题,内部基既有 file_accessors也有 pod_target.native_target 的形式继续访问 yml 文件。所以升级 cocoapods 1.8.4 之后修改脚本直接改为用 file_accessors 寻找 yml
#### 多包加速
目前不能开启多包并行的瓶颈在于打包机操作的是本地下载下来的 `.cocoapods` 文件夹,所以当一个项目操作的时候其他项目没办法操作。
CDN 提供了通过网络接口处理依赖的能力,通过网络去操作文件,所以是可以多包并行打包的。
但是由于以下2个原因我们需要自建``` CDN``` 的能力:
1. 我们的依赖存在的位置有2个1个是私有源、1个是官方源。目前使用官方``` CDN``` 就是官方源,因为我们要并行打包,所以私有源也需要 `CDN` 化。不然难以免于多个项目进行文件的读写锁操作问题。
2. 但是``` CDN``` 跟网络的状态有关,依据所处位置附近的服务器有关系,严重依赖于外界因素,不可控。所以想拥有快速稳定的`` CDN`` 查询能力就需要自建`` CDN`` 了。
另外一个可预期的点就是自建了` CDN` wax SDK 发布的相关逻辑也需要修改。
根据 Cocoapods 的 changeLog 知道 CDN 的实现是借助 **Netlify** 实现的。所以接下去的研究方向就是如何利用 Netlify 自建 CDN。
> ### Directory Listing Denied
>
> It was obvious to many that the spec repo should be put behind a CDN, but there were several constraints:
>
> 1. It had to be a free CDN, as the project is free and open-source.
> 2. It had to allow some way of obtaining directory listings, for retrieving versions of pods.
> 3. It had to auto-update from GitHub as the source of truth.
>
> The [first implementation](https://github.com/CocoaPods/Core/pull/469) was a shell script, polling GitHub and piping `find` into `ls` into index files. This ran on a machine that was not open or free and therefore could not be the true solution. Nevertheless, this auto-updated repo was put behind a [jsDelivr CDN](https://www.jsdelivr.com/) and the client interfacing with it was released in [1.7.0](http://blog.cocoapods.org/CocoaPods-1.7.0-beta#cdn-support) labeled "highly experimental".
>
> ### Final Lap with Netlify
>
> The [final version](https://github.com/CocoaPods/Core/pull/541) of the CDN for CocoaPods/Specs was implemented on [Netlify](https://www.netlify.com/), a static site hosting service supporting flexible site generation. This solution ticked all the boxes: a generous open-source plan, fast CDN and continuous deployment from GitHub.
>
> Upon each commit, Netlify runs a [specialized script](https://github.com/CocoaPods/Specs/tree/master/Scripts) which generates a per-shard index for all the pods and versions in the repo. If you've ever noticed that the directory structure for our Podspecs repo was strange, this is what we call sharding. An example of a shard index can be found at https://cdn.cocoapods.org/all_pods_versions_2_2_2.txt. This would correspond to `~/.cocoapods/repos/master/Specs/2/2/2/` locally.
>
> Additionally, we create an `all_pods.txt` file which contains a list of all pods.
>
> Finally, any other request made is redirected to GitHub's CDN.
###
#### 接入方式
考虑到业务线 App 升级是分开的,不可能同步进行,所以需要考虑到接入计划。
- 能否提供 wax 项目指定到特定环境打包机的能力(该打包机升级了 cocoapods 版本)
- 假如没有上述能力,则考虑其他方式支持业务线自定义打包所需的 cocoapods 版本
- 将 2个版本的 cocoapods 做成2个 Bundle 包,读取 wax 工程配置,指定某个 Bundle
- 假如打包机由于某些原因没办法升级 cocoapods 版本,但是某个 wax 项目又需要新版的 cocoapods 进行打包,则需要则代码上传的时候提交 `Pods` 文件夹。这样在打包机上面不需要执行 install 的操作,将本地的 Pods 目录上传上来,全部使用本地的一套。
## 参考资料
[1. cocoapods changeLog](http://blog.cocoapods.org/CocoaPods-1.7.2/)
[2. 版本清单](https://github.com/CocoaPods/Specs/tree/master/Scripts)
[3.探究Xcode New Build System对于构建速度的提升](https://blog.csdn.net/TuGeLe/article/details/84885211)

342
Chapter1 - iOS/1.78.md Normal file
View File

@@ -0,0 +1,342 @@
# 上架包预检
## 常见被拒原因汇总
### iOS
钱伴
敏感词:审核、热更新、开关
1. App 内包含分发下载分发功能(比如贷款超市、引导用户下载 App 等功能)。
2. 提供的测试账号无法查看贷款实际利率。
3. notification 接口返回showupgradetrue提示用户升级。 审核期间接口不要返回该字段。
4. 审核账号任何时候在任何ip登录看到的都是审核版。
5. 审核的时候屏蔽 getwarehouseinfo 接口tab接口。审核期间 getwarehouseinfo 接口不返回数据。读取本地的配置
6. upgrade签到不能展示。 方案把现在提额页copy一份通过审核开关控制展示哪个页面。隐藏活动入口
7. getSdkConfig 屏蔽接口返回内容。审核阶段接口返回空
8. switch字样接口
9. review字样接口
基金:
没有发版问题
挖财宝:
1. App 内部某个控件点击后跳转到了信用卡的 H5 页面,可以引导用户线下办卡
2. 提供的登陆账号和密码不对,登陆不上
3. 运营填写的营销关键字有问题
4. 元数据问题iPhoneX 截图中 iPhone 壳子是 iPhone7 的,应该是 iPhoneX
5. 说明隐私权限的作用。
闪电公积金:
1. 苹果看到“代缴公积金”,这个需要有政府机构资质,此类功能在审核期间都关闭。
社保:
1. 修改隐私权限相关的文案,做到让审核人员看得懂
记账:
1. Guidelines 3.2.2 审核期间,隐藏邀请好友、天天挖宝相关的运营活动
信用保镖:
1. 狗年大礼包: 修改元数据后申诉;更换销售地区后提审。
### Android
信贷超人:
1. App 无法登陆进去,属于 bug 级别
2. App 没有适配 ipad。
3. Privacy - Data Collection and Storage说明 App 没有做隐私权限的收集。
4. 访问 h5 页面出现问题。 属于 bug 级别
微记账:
暂无
挖财宝:
1. 原因挖财宝集成了设备指纹SDK, 会上传用户设备安装应用列表,所以被拒. 解决移除设备指纹SDK, 成功上架
2. 原因挖财宝中存在更多App按钮会导向应用宝所以被华为拒过其他应用市场没有拒过. 解决移除更多App按钮无再被华为拒过。
基金:
暂未遇到审核被拒的情况。
快贷:
暂未遇到审核被拒的情况。
钱伴:
暂未遇到审核被拒的情况。
信用卡:
1. 经检测你的应用targetsdk版本低于26 ,请修改。
2. 你的应用未加固
3. 你提交的应用被检测到存在不合理获取短信/通话记录相关权限的行为请勿获取SEND_SMS、READ_SMS、READ_CALL_LOG、RECEIVE_SMS权限。
4. 你的应用注册登录界面的《用户协议》内容空白,请添加。
5. 你的应用注册登录时无法接收验证码,注册登录时需输入图形验证码,且无图形参考,请定位修复。
6. 在你的应用内任选一款借贷产品注册点击下载应用后直接进入第三方下载渠道,请修改为华为应用市场下载链接或者直接删除应用分发模块。
7. 你的应用版权未通过审核请提供a.《计算机软件著作权证书》或《APP电子版权证书》 b. ICP备案c.《免责函》d.你提供的合作协议已到期请重新提供至少三家贷款产品合作单位的金融监管部门备案登记文件、授权书。e.请提供《金融许可证》或对应银行信用卡中心授权证明至少三家。免责函模板下载链接https://url.cloud.huawei.com/1vbaVckwPC
8. 你的应用截图与实际内容不相符请修改。如有疑问请发邮件至developer@huawei.com。
9. 使用了动态更新的能力。回复苹果,问具体使用的地方,并贴出代码。
只能根据政策去做,比如当前的隐私权权限问题就是政策
还有一些如理财、贷款类,没有资质就无法过审,这个也是政策。
解决方案:
苹果那边是把接口字段定义的隐晦一点。之前是定义了一个颜色值作为审核的开关
如果是对应的颜色,就开启相应功能。
## 原因汇总
从 Android 和 iOS 2端 App 被驳回的一些信息来看,驳回原因一般划分为下面几类:
1. 审核期间,资源和配置都应该调节为审核模式
2. App 包含某些关键字
3. 审核相关的元数据问题(截图与实际内容不匹配、机型和截图不匹配、提供给审核的账号和密码登陆不上)
4. 使用的隐私权限必须说明,文案描述必须清晰
5. App 存在 bug (账号无法登陆、没有适配 ipad、访问 h5 打不开
6. 诱导用户打开查看更多 App
7. Android 应用未加固
8. 应用缺乏相关的资质和证书
## 方案
### 敏感词云
loan、online、ischeck、isonline、switch、review、审核、热更新、开关、代缴公积金
扫描是基于源代码出发的扫描的
1. 关键词词云在什么阶段分析,构建前、构建后?
放在构建前进行。因为需要对敏感词划分等级,如果是 error 级别,则在扫描构建的时候匹配,匹配任意一个则马上构建失败;如果是 warning 级别,则需要在构建完成后展示出来。
2. 词云要不要分等级,比如 error、warning。
遇到 error 构建马上停止waring 的话给出警告 console
3. 私有 api 要不要扫描,扫描的话怎么做?
性质和关键词一样。做到一个 yml 文件里面。然后用正则将一堆关键词和每个代码文件进行匹配。
4. 私有 api 是做 top 100 呢?还是 top 50还是全部的
私有 api 扫描要做,将私有 api 打包到关键词 yml 文件中。所以本质上还是字符串查找的问题。
5. 私有 api 要不要设置开关,让打包的人选择要不要扫描私有 api
不做关键词扫描的场景目前没想到,所以最好是每个打包都需要做关键词扫描。
测试实验:
NodeJS 实现: 9个铭感词语、代码文件5967个耗时3.5秒
## 技术方案
1. 业务线需要自定义敏感词云(因为每条业务线的关键词云都不一样)
2. 敏感词需要划分等级error、warning。扫描到 error 需要马上停止构建,并提示「已扫描到你的源码中存在敏感词***可能存在提交审核失败的可能请修改后再次构建」。warning 的情况不需要马上停止构建,等任务全部结束后汇总给出提示「已扫描到你的源码中存在敏感词***、***...,可能存在提交审核失败的可能,请开发者自己确认」
3. 铭感词云的格式。scaner.yml 文件。
- error: 数组的格式。后面写需要扫描的关键词,且等级为 error表示扫描到 error 则马上停止构建
- warning数组的格式。后面写需要扫描的关键词且等级为 warning扫描结果不影响构建最终只是展示出来
- searchPath字符串格式。可以让业务线自定义需要进行扫描的路径。
- fileType数组格式。可以让业务线自定义需要扫描的文件类型。默认为 `sh|pch|json|xcconfig|mm|cpp|h|m`
- warningkeywordsScan布尔值。业务线可以设置是否需要扫描 warning 级别的关键词。
- errorKeywordsScan布尔值。业务线可以设置是否需要扫描 error 级别的关键词。
```
error:
- checkSwitch
warning:
- loan
- online
- ischeck
searchPath:
../fixtures
fileType:
- h
- m
- cpp
- mm
- js
warningkeywordsScan:
true
errorKeywordsScan:
true
```
4. iOS 端存在私有 api 的情况Android 端不存在该问题
私有 api 70111个文件每个文件假设10个方法则共70万个 api。所以计划找出 top 100.去扫描匹配,支持业务线是否开启的选项
5. 利用 NodeJS 实现关键词预审能力wax cli 调度各个模版的能力实现相应的功能,所以要做的事情就是找到相应的时机去编码实现相应的需求。几个问题需要确认?包预检需要放在构建的什么阶段?需要不需要设置关键词的等级?需要不需要设置关键词扫描的文件路径?需要不需要支持自定义文件匹配类型?
其实这些问题都是业界标准的做法,肯定需要预留这样的能力,所以自定义规则的格式可以查看上面 yml 文件的各个字段所确定。明确了做什么事,以及做事情的标准,那就可以很快的开展并落地实现。
<details>
<summary>点击展开代码</summary>
```javascript
'use strict'
const { Error, logger } = require('@wac/wax-utils')
const fs = require('fs-extra')
const glob = require('glob')
const YAML = require('yamljs')
module.exports = class PreBuildCommand {
constructor(ctx) {
this.ctx = ctx
this.projectPath = ''
this.fileNum = 0
this.isExist = false
this.errorFiles = []
this.warningFiles = []
this.keywordsObject = {}
this.errorReg = null
this.warningReg = null
this.warningkeywordsScan = false
this.errorKeywordsScan = false
this.scanFileTypes = ''
}
async fetchCodeFiles(dirPath, fileType = 'sh|pch|json|xcconfig|mm|cpp|h|m') {
return new Promise((resolve, reject) => {
glob(`**/*.?(${fileType})`, { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => {
if (err) reject(err)
resolve(files)
})
})
}
async scanConfigurationReader(keywordsPath) {
return new Promise((resolve, reject) => {
fs.readFile(keywordsPath, 'UTF-8', (err, data) => {
if (!err) {
let keywords = YAML.parse(data)
resolve(keywords)
} else {
reject(err)
}
})
})
}
async run() {
const { argv } = this.ctx
const buildParam = {
scheme: argv.opts.scheme,
cert: argv.opts.cert,
env: argv.opts.env
}
// 处理包关键词扫描(敏感词汇 + 私有 api
this.keywordsObject = (await this.scanConfigurationReader(this.ctx.cwd + '/.scaner.yml')) || {}
this.warningkeywordsScan = this.keywordsObject.warningkeywordsScan || false
this.errorKeywordsScan = this.keywordsObject.errorKeywordsScan || false
if (Array.isArray(this.keywordsObject.fileType)) {
this.scanFileTypes = this.keywordsObject.fileType.join('|')
}
if (Array.isArray(this.keywordsObject.error)) {
this.errorReg = this.keywordsObject.error.join('|')
}
if (Array.isArray(this.keywordsObject.warning)) {
this.warningReg = this.keywordsObject.warning.join('|')
}
// 从指定目录下获取所有文件
this.projectPath = this.keywordsObject ? this.keywordsObject.searchPath : this.ctx.cwd
const files = await this.fetchCodeFiles(this.projectPath, this.scanFileTypes)
if (this.errorReg && this.errorKeywordsScan) {
await Promise.all(
files.map(async file => {
try {
const content = await fs.readFile(file, 'utf-8')
const result = await content.match(new RegExp(`(${this.errorReg})`, 'g'))
if (result) {
if (result.length > 0) {
this.isExist = true
this.fileNum++
this.errorFiles.push(
`编号: ${this.fileNum}, 所在文件: ${file}, 出现次数: ${result &&
(result.length || 0)}`
)
}
}
} catch (error) {
throw error
}
})
)
}
if (this.errorFiles.length > 0) {
throw new Error(
`从你的项目中扫描到了 error 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${
this.errorReg
}」\n存在问题的文件有 ${JSON.stringify(this.errorFiles, null, 2)}`
)
}
// warning
if (this.warningReg && !this.isExist && this.fileNum === 0 && this.warningkeywordsScan) {
await Promise.all(
files.map(async file => {
try {
const content = await fs.readFile(file, 'utf-8')
const result = await content.match(new RegExp(`(${this.warningReg})`, 'g'))
if (result) {
if (result.length > 0) {
this.isExist = true
this.fileNum++
this.warningFiles.push(
`编号: ${this.fileNum}, 所在文件: ${file}, 出现次数: ${result &&
(result.length || 0)}`
)
}
}
} catch (error) {
throw error
}
})
)
if (this.warningFiles.length > 0) {
logger.info(
`从你的项目中扫描到了 warning 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${
this.warningReg
}」。有问题的文件有${JSON.stringify(this.warningFiles, null, 2)}`
)
}
}
for (const key in buildParam) {
if (!buildParam[key]) {
throw new Error(`build: ${key} 参数缺失`)
}
}
}
}
```
</details>
## wax
公司自研工具 wax 是「一站式协同开发利器,重新定义混合开发」。包含若干子项目,每个子项目就是所谓的 “**模版**”,每个模版其实就是一个 Node 工程,一个 npm 模块,主要负责以下功能:
- 特定项目类型的目录结构
- 自定义命令供开发、构建等使用
- 模版持续更新及 patch 等
按照 iOS 端 `pod install` 这个过程cocoapods 为我们预留了钩子:`PreInstallHook.rb`、`PostInstallHook.rb`,允许我们在不同的阶段为工程做一些自定义的操作,所以我们的 iOS 模版设计也参考了这个思想,在打包构建前、构建中、构建后提供了钩子:`prebuild`、`build`、`postbuild`。定位好了问题,要做的就是在 prebuild 里面进行关键词扫描的编码工作。
![wax 项目子工程](./../assets/2019-12-18-wax-project.png)

86
Chapter1 - iOS/1.79.md Normal file
View File

@@ -0,0 +1,86 @@
# 深入理解各种锁
## 乐观锁、悲观锁
乐观锁对应于现实生活中乐观的人,思考事情总往好的方向发展;悲观锁对应于现实生活悲观的人,思考事情总往坏的方向发展。不同性格的人都有优缺点,不能抛开场景说一种人好而另一种人不好。
乐观锁和悲观锁是一种广义上的概念,体现了看待线程同步问题的不同角度,在 iOS、Java、数据库中都有此概念。
### 悲观锁
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种几只,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
### 乐观锁
乐观锁认为自己在使用数据的时候不会有别的线程来修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果数据已经被别的线程更新,则根据不同方式执行不同操作(例如报错或者自动重试)。
可以根据版本号机制和 CAS 算法实现。
乐观锁适合多读少写的应用类型或者场景,即冲突真的很少发生的场景,这样省去了锁的开销,加大了系统的吞吐量。但是如果多写少读的情况,一般会经常发生冲突,这样会导致上层应用层不断 retry这样反而降低了性能所以一般建议多写的场景下使用悲观锁比较合适。
![lock](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-12-19-lock.png)
### 乐观锁常见的实现方式
乐观锁一般使用版本号机制或者 CAS 算法实现。
#### 1. 版本号机制
在数据表增加一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时, version 值加1。当线程1更新数据的时候先拿到数据并读取出 version 值,修改完数据进行提交更新的时候时,若读取出的 version 值为当前数据库中 version 值相等时才更新,否则重试更新操作,直到更新成功。
举个例子:
假设数据库中账户信息表有一个字段 version值为1当前账户余额为100。当需要对账户信息表进行更新的时候需要读取 version 字段,以及账户余额信息
- 用户 A 读出数据version = 1balance = 100。从账户余额中扣除 50 balacne = 50
- 用户 B 比用户 A 刚刚晚一点点时间,读出数据 version = 1, balance = 100。从账户余额中扣除 20balance = 80
- 用户 A 完成修改操作,需要提交更新,但是在更新之前会先判断数据库中的版本号 version 值和自己读取到的 version 值是否一致,如果一致,则将版本号 version 字段的值加1version = 2连同账户扣除后的余额balance = 50提交到数据库服务器执行更新操作此时由于提交数据中版本号大于数据库记录中的版本则数据被更新数据库记录 version = 2
- 用户 B 完成修改操作,同样在更新之前先读取数据库中的版本号 version 值和自己读取到的 version 值是否一致,但此时发现自己读取到的 version = 1数据库中的 version = 2很显然不满足“当前最后更新的版本号 version 与操作员第一次读取到的版本号 version 相等”的乐观锁策略,因此用户 B 的提交被驳回。
这样,就避免了用户 B 基于 version = 1 的旧数据修改的结果覆盖用户 A 操作的结果,
#### 2. CAS 算法
**compare and swap比较与交换** ,是一种有名的**无锁算法**。 无锁编程,即在不实用锁的情况下实现多线程之间的数据同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做**非阻塞同步Non-blocking Synchorization**。CAS 算法涉及到的三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A 时CAS 通过原子方式用新值 B 来更新 V否则不会执行任何操作。比较和替换是一个原则操作。一般情况下是一个自旋操作即不断的重试
### 乐观锁的缺点
1. ABA 问题
如果一个变量 V 初次读取的时候的值为 A并且在准备赋值的时候检查到变量 V 的值仍然是 A那么可以说是 V 的值从来没被其他线程修改吗?很明显不能,因为有可能变量 V 的值,从 A 变到 B然后又改回到 A那么 CAS 的标准就会认为变量 V 从来没被修改过,这类问题被成为 CAS 的 **ABA** 问题。
2. 循环时间长、开销大
自旋 CAS (也就是不成功就一直循环操作直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
3. 只能保证一个共享变量的原则操作
CAS 只对单个变量共享有效当操作涉及到多个共享变量时CSA 无效。
### CAS 与 synchorized 的使用场景
一般来说, CAS 适用于乐观锁,多读少写场景,冲突一般较少,则自旋操作的情况非常少,不会消耗 CPU该场景合适。synchorized 使用悲观锁,多写少读场景,冲突一般较多。
1. 对于资源竞争比较少(线程冲突较轻)的情况,如果使用 synchorized 同步锁进行线程阻塞和唤醒切换以及用户内核态间的切换操作额外浪费 CPU 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋的几率较少,因此可以获得更高的性能。
2. 对于资源竞争严重线程冲突严重的情况CAS 自旋的几率会比较大,从而浪费更多的 CPU 资源。效率低于 synchorized
### 参考资料
- [不可不说的Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html)

View File

@@ -1,5 +1,4 @@
### hittest方法
# hittest方法
* 就是用来寻找最合适的view
* 当一个事件传递给一个控件就会调用这个控件的hitTest方法
@@ -13,7 +12,7 @@
在控制器的界面上加5个颜色不同的view每个view自定义view去实现因此在不同的view上的手势就可以由不同的view拦截到。
![UI效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Simulator%20Screen%20Shot%20-%20iPhone%206s%20Plus%20-%202017-10-11%20at%2010.14.37.png)
![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)
```

View File

@@ -0,0 +1,86 @@
# 第一部分
第一部分主要介绍 iOS 开发中遇到的问题或者有趣的知识
* [1、工程大小优化之图片资源](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.1.md)
* [2、看透构造方法](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.2.md)
* [3、控制器加载的玄机](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.3.md)
* [4、如何优雅地调试手机网页](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md)
* [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md)
* [6、外卖App双列表联动](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.6.md)
* [7、在内存剖析对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md)
* [8、长按UIWebView上的图片保存到相册](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.8.md)
* [9、hitTest和pointInside方法你真的熟吗](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.9.md)
* [10、HyBrid探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.10.md)
* [11、iOS中的事件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.11.md)
* [12、NSFileManager终极杀手](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.12.md)
* [13、UINavigationController的妙用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.13.md)
* [14、URL-Schemes深度剖析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.14.md)
* [15、URL Schemes 的发展](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.15.md)
* [16、CocoaPods的使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.16.md)
* [16、OC与Swift混编](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.16.md)
* [17、对于不可调节高度的UI控件进行改变frame](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.17.md)
* [18、YYModel 的使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.18.md)
* [19、实现波浪动画](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.19.md)
* [20、底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.20.md)
* [21、禅与 Objective-C 编程艺术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.21.md)
* [22、修改 UITextField placeholder 样式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.22.md)
* [23、UIScrollView拖拽时回收键盘](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.23.md)
* [24、读 Apple 源码看看 NSRange](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.24.md)
* [25、复制层CAReplicatorLayer](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.25.md)
* [26、CAShapeLayer](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.26.md)
* [27、仿微博动画](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.27.md)
* [28、UILabel 全局匹配并高亮](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.28.md)
* [29、JavascriptCore](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.29.md)
* [30、Xcode 小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.30.md)
* [31、终端效率](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.31.md)
* [32、终极截屏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.32.md)
* [33、推送](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.33.md)
* [34、App 评分](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.34.md)
* [35、一些布局小知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.35.md)
* [36、iOS数值计算精度丢失问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.36.md)
* [37、一些看到但未尝试的知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.37.md)
* [38、RunLoop上](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md)
* [39、RunLoop下](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md)
* [40、RunLoop的应用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md)
* [41、iOS 应用启动性能优化资料汇总](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.41.md)
* [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md)
* [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md)
* [44、Awesome Hybrid - 1](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.44.md)
* [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
* [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md)
* [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md)
* [48、OC类别Catrgory和拓展Extension](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
* [49、MVC、MVP、MVVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.49.md)
* [50、“静态库”和“动态库”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.50.md)
* [51、cocopod](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.51.md)
* [52、如何打造团队的代码风格统一以及开发效率的提升](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.52.md)
* [53、iOS 数据持久化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md)
* [54、Xcode 设置作者信息](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.54.md)
* [55、史上最强、最详细无痕埋点方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.55.md)
* [56、大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)
* [57、自动布局的思考](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.57.md)
* [58、Swift每个版本迁移的总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.58.md)
* [59、iOS零散知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.59.md)
* [60、App瘦身之道](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.60.md)
* [61、App启动时间优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.61.md)
* [62、OCLint实现Code Review](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.62.md)
* [63、苹果官方开源资料](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.63.md)
* [64、组件化、模块化、插件、子应用、框架、库理解](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.64.md)
* [65、多端融合方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.65.md)
* [66、移动端网络层优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.66.md)
* [67、iOS工程编译速度优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.67.md)
* [68、守护你的App安全](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.68.md)
* [69、React-Native总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.69.md)
* [70、不一样的动态化能力](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.70.md)
* [71、Flutter初体验-安装](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.71.md)
* [72、架构设计心得](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.72.md)
* [73、Ruby学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.73.md)
* [74、APM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md)
* [75、如何写好测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.75.md)
* [76、iOS Crash分析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.76.md)
* [77、iOS 打包系统构建加速](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.77.md)
* [78、上架包预检](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.78.md)
* [79、深入理解各种锁](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.79.md)
* [80、打造功能强大的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)

View File

@@ -1,5 +1,4 @@
# :last-child与:last-of-type
# last-child 与 last-of-type
> 同学们遇到过给同一组元素的最后一个元素设置css失效的情况吗我遇到过当时使用:last-child居然不起作用看到名字不科学啊明明是“最后一个元素”那为什么设置CSS失效呢今天来一探究竟吧
@@ -36,7 +35,7 @@
</body>
</html>
```
![效果1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-091957@2x.png)
![效果1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180507-091957@2x.png)
* 再先看一组`:last-child`不正常工作的代码
@@ -73,7 +72,7 @@
```
![效果2](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092046@2x.png)
![效果2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180507-092046@2x.png)
问题抛出来了,那么来研究下:last-child和:last-of-type究竟是何方神圣。
@@ -121,7 +120,7 @@
```
![效果3](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092145@2x.png)
![效果3](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180507-092145@2x.png)
* :nth-last-child不能正常工作的代码
@@ -157,7 +156,7 @@
</html>
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092232@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180507-092232@2x.png)
* 接下来:nth-last-of-type闪亮登场
@@ -194,4 +193,4 @@
</html>
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092358@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180507-092358@2x.png)

View File

@@ -23,7 +23,7 @@ npm install
遇到的问题:
![遇到的问题](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools1.png)
![遇到的问题](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Chrome-Vue-tools1.png)
改用命令
@@ -31,7 +31,7 @@ npm install
npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver
```
![改用命令](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools3.png)
![改用命令](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Chrome-Vue-tools3.png)
继续 npm install
@@ -41,7 +41,7 @@ npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/ch
npm run build
```
![编译项目文件](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools4.png)
![编译项目文件](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Chrome-Vue-tools4.png)
* 第四步:添加至 Chrome 浏览器的拓展
@@ -51,11 +51,11 @@ npm run build
点击“加载已解压的拓展程序”选择本地 clone 下来的文件夹中的 shells -> chrome 文件夹vue-devtools-master/shells/chrome
```
![Chrome 添加拓展](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools5.png)
![Chrome 添加拓展](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Chrome-Vue-tools5.png)
* 第五步:重启浏览器
* 第六步:在浏览器中的调试 Vue 代码
![Chrome 调试 Vue](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools6.png)
![Chrome 调试 Vue](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Chrome-Vue-tools6.png)

View File

@@ -1,4 +1,4 @@
## Promise
# Promise
## 一、基础使用

View File

@@ -4,7 +4,7 @@
1、遇到一个问题 ,一个功能在 iOS 手机上正常工作,但是在 Android 上不正常依照经验来看无非就是2个原因1、URL参数少传递了2、JS 在移动端的 webview 上报错了,所以我让远程对接的人员将 url 打印出来,发现没错。继续让他打印查看下 js 错误日志,发现 “Cannot read property "getItem" of null”。 代码出错行数在 259。看了下具体代码就是读取 localstorage
![报错信息](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/Andoid_Webview_Localstroage_erroe.jpg)
![报错信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Andoid_Webview_Localstroage_erroe.jpg)
想了想,在我们的项目中 Android 原生的代码在使用 webview 的时候额外设置了代码具体如下

View File

@@ -12,7 +12,7 @@ Vue 的组件可以分为全局组件和局部组件
```
//全局组件
Vue.componetns('todo-item',{
template:'<li> {{content}} </li>',
template:'<li> { {content} } </li>',
prop:['content']
});
//局部组件
@@ -82,7 +82,7 @@ style 同名的样式不会对其他组件有影响
慌不要慌,小哥哥带你 hold 住全场。
* 脚手架已经帮你处理好了这块需求了,看下图
![配置图例](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180602-210826@2x.png)
![配置图例](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180602-210826@2x.png)
* 有些大佬不要脚手架,喜欢自己初始化项目,用 npm 挨个安装所需要的依赖。然后自己配置 webpack 的 options。需要调试的话需要做下面的配置
@@ -93,7 +93,7 @@ style 同名的样式不会对其他组件有影响
这样你就可以在浏览器当中像写普通的 JS 一样进行调试代码了。比如
![调试界面](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180602-211328@2x.png)
![调试界面](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180602-211328@2x.png)
10、Vue 中 **<template>...</template>** 底层的做法是 ** render()**方法, 对 template 中的元素依次遍历创造节点
@@ -317,7 +317,7 @@ Vue.use(VueResource);
```
<ul>
<li v-for="(item,key) in news"><router-link to="/content"></router-link>{{item}}</li>
<li v-for="(item,key) in news"><router-link to="/content"></router-link>{ {item} }</li>
</ul>
//新增加的 router-link 需要在路由配置里面添加配置项。
@@ -353,7 +353,7 @@ Vue.use(VueResource);
```
<ul>
<li v-for="(item,key) in news"><router-link :to="'/content' + key" ></router-link>{{item}}</li>
<li v-for="(item,key) in news"><router-link :to="'/content' + key" ></router-link>{ {item} }</li>
</ul>
const routes = [
@@ -378,7 +378,7 @@ Vue.use(VueResource);
];
<ul>
<li v-for="(item,key) in news"><router-link :to="'/content?id='+key" ></router-link>{{item}}</li>
<li v-for="(item,key) in news"><router-link :to="'/content?id='+key" ></router-link>{ {item} }</li>
</ul>
//拿到值
@@ -554,3 +554,23 @@ Vue.use(VueResource);
```
- action 类似于 mutation ,在外面使用的时候用 this.$state.dispath('方法名')
- 某些页面可能我们只需要切换数据源和样式模版。所以数据源有可能是变化的,页面的模版也是变化的。
这样子我们可能会用到 `v-if、v-else-if...v-else` 来切换页面的模版。但是在模版里面去 `v-for` 循环展示数据源。
早期遇到错误,大概意思是说我们循环动态生成的元素,有 key 重复了,最后查找资料得到解决方案。在判断模版的时候需要使用 `<template v-if="type==1"></template>`
```
<template v-else-if="collectType==4">
<b-card v-for="(item, index) in qualifications" v-bind:key="'qualification' + index" :title="item.company"
sub-title="">
<p class="card-text">
<em>法定代表人:{ {item.legalperson} }</em>
<em>成立时间:{ {item.create} }</em>
<em>注册资本:{ {item.capital} }(万元)</em>
</p>
<p v-for="(qualification,index) in item.qualifications" v-bind:key="index" style="font-size:13px;">
{ {qualification} }
</p>
</b-card>
</template>
```

View File

@@ -0,0 +1,605 @@
# 反爬技术研究
> 对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App短期内成为你的劲敌。
## 一、爬虫手段
目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本.
有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID这个步骤就可以拦截掉一部分的爬虫开发者
## 二、制定出**Web 端反爬技术方案**
从这2个角度网页所见非所得、查接口请求没用出发制定了下面的反爬方案。
1. 使用HTTPS 协议
2. 单位时间内限制掉请求次数过多,则封锁该账号
3. 前端技术限制 (接下来是核心技术)
举例:比如需要正确显示的数据为“19950220”
#### 2.1 原始数据加密
1. 先按照自己需求利用相应的规则数字乱序映射比如正常的0对应还是0但是乱序就是 0 <-> 11 <-> 9,3 <-> 8,...制作自定义字体ttf
2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 17730220
3. 对于第一步得到的字符串依次遍历每个字符将每个字符根据按照线性变换y=kx+b。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”那么线性变换的 k 为 7b 为 24。
4. 然后将变换后的每个字符串用“3.1415926”拼接返回给接口调用者。(为什么是3.1415926,因为对数字伪造反爬,所以拼接的文本肯定是数字的话不太会引起研究者的注意,但是数字长度太短会误伤正常的数据,所以用所熟悉的 Π)
```
1773 -> “1*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “3*7+24” -> 313.1415926733.1415926733.141592645
02 -> "0*7+24" + "3.1415926" + "2*7+24" -> 243.141592638
20 -> "2*7+24" + "3.1415926" + "0*7+24" -> 383.141592624
````
#### 2.2 前端拿到数据后再解密,解密后根据自定义的字体 Render 页面
1. 先将拿到的字符串按照“3.1415926”拆分为数组
2. 对数组的每1个数据按照“线性变换”y=kx+bk和b同样按照当前的日期求解得到逆向求解到原本的值。
3. 将步骤2的的到的数据依次拼接再根据 ttf 文件 Render 页面上。
#### 2.3 后端需要根据上一步设计的协议将数据进行加密处理
下面以 **Node.js** 为例讲解后端需要做的事情
1. 首先后端设置接口路由
2. 获取路由后面的参数
3. 根据业务需要根据 SQL 语句生成对应的数据。如果是数字部分,则需要按照上面约定的方法加以转换
4. 将生成数据转换成 JSON 返回给调用者
```js
// json
var JoinOparatorSymbol = "3.1415926";
function encode(rawData, ruleType) {
if (!isNotEmptyStr(rawData)) {
return "";
}
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
var encodeData = "";
for (var index = 0; index < rawData.length; index++) {
var datacomponent = rawData[index];
if (!isNaN(datacomponent)) {
if (ruleType < 3) {
var currentNumber = rawDataMap(String(datacomponent), ruleType);
encodeData += (currentNumber * month + day) + JoinOparatorSymbol;
}
else if (ruleType == 4) {
encodeData += rawDataMap(String(datacomponent), ruleType);
}
else {
encodeData += rawDataMap(String(datacomponent), ruleType) + JoinOparatorSymbol;
}
}
else if (ruleType == 4) {
encodeData += rawDataMap(String(datacomponent), ruleType);
}
}
if (encodeData.length >= JoinOparatorSymbol.length) {
var lastTwoString = encodeData.substring(encodeData.length - JoinOparatorSymbol.length, encodeData.length);
if (lastTwoString == JoinOparatorSymbol) {
encodeData = encodeData.substring(0, encodeData.length - JoinOparatorSymbol.length);
}
}
```
```javascript
//字体映射处理
function rawDataMap(rawData, ruleType) {
if (!isNotEmptyStr(rawData) || !isNotEmptyStr(ruleType)) {
return;
}
var mapData;
var rawNumber = parseInt(rawData);
var ruleTypeNumber = parseInt(ruleType);
if (!isNaN(rawData)) {
lastNumberCategory = ruleTypeNumber;
//字体文件1下的数据加密规则
if (ruleTypeNumber == 1) {
if (rawNumber == 1) {
mapData = 1;
}
else if (rawNumber == 2) {
mapData = 2;
}
else if (rawNumber == 3) {
mapData = 4;
}
else if (rawNumber == 4) {
mapData = 5;
}
else if (rawNumber == 5) {
mapData = 3;
}
else if (rawNumber == 6) {
mapData = 8;
}
else if (rawNumber == 7) {
mapData = 6;
}
else if (rawNumber == 8) {
mapData = 9;
}
else if (rawNumber == 9) {
mapData = 7;
}
else if (rawNumber == 0) {
mapData = 0;
}
}
//字体文件2下的数据加密规则
else if (ruleTypeNumber == 0) {
if (rawNumber == 1) {
mapData = 4;
}
else if (rawNumber == 2) {
mapData = 2;
}
else if (rawNumber == 3) {
mapData = 3;
}
else if (rawNumber == 4) {
mapData = 1;
}
else if (rawNumber == 5) {
mapData = 8;
}
else if (rawNumber == 6) {
mapData = 5;
}
else if (rawNumber == 7) {
mapData = 6;
}
else if (rawNumber == 8) {
mapData = 7;
}
else if (rawNumber == 9) {
mapData = 9;
}
else if (rawNumber == 0) {
mapData = 0;
}
}
//字体文件3下的数据加密规则
else if (ruleTypeNumber == 2) {
if (rawNumber == 1) {
mapData = 6;
}
else if (rawNumber == 2) {
mapData = 2;
}
else if (rawNumber == 3) {
mapData = 1;
}
else if (rawNumber == 4) {
mapData = 3;
}
else if (rawNumber == 5) {
mapData = 4;
}
else if (rawNumber == 6) {
mapData = 8;
}
else if (rawNumber == 7) {
mapData = 3;
}
else if (rawNumber == 8) {
mapData = 7;
}
else if (rawNumber == 9) {
mapData = 9;
}
else if (rawNumber == 0) {
mapData = 0;
}
}
else if (ruleTypeNumber == 3) {
if (rawNumber == 1) {
mapData = "&#xefab;";
}
else if (rawNumber == 2) {
mapData = "&#xeba3;";
}
else if (rawNumber == 3) {
mapData = "&#xecfa;";
}
else if (rawNumber == 4) {
mapData = "&#xedfd;";
}
else if (rawNumber == 5) {
mapData = "&#xeffa;";
}
else if (rawNumber == 6) {
mapData = "&#xef3a;";
}
else if (rawNumber == 7) {
mapData = "&#xe6f5;";
}
else if (rawNumber == 8) {
mapData = "&#xecb2;";
}
else if (rawNumber == 9) {
mapData = "&#xe8ae;";
}
else if (rawNumber == 0) {
mapData = "&#xe1f2;";
}
}
else{
mapData = rawNumber;
}
} else if (ruleTypeNumber == 4) {
var sources = ["年", "万", "业", "人", "信", "元", "千", "司", "州", "资", "造", "钱"];
//判断字符串为汉字
if (/^[\u4e00-\u9fa5]*$/.test(rawData)) {
if (sources.indexOf(rawData) > -1) {
var currentChineseHexcod = rawData.charCodeAt(0).toString(16);
var lastCompoent;
var mapComponetnt;
var numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
var characters = ["a", "b", "c", "d", "e", "f", "g", "h", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
if (currentChineseHexcod.length == 4) {
lastCompoent = currentChineseHexcod.substr(3, 1);
var locationInComponents = 0;
if (/[0-9]/.test(lastCompoent)) {
locationInComponents = numbers.indexOf(lastCompoent);
mapComponetnt = numbers[(locationInComponents + 1) % 10];
}
else if (/[a-z]/.test(lastCompoent)) {
locationInComponents = characters.indexOf(lastCompoent);
mapComponetnt = characters[(locationInComponents + 1) % 26];
}
mapData = "&#x" + currentChineseHexcod.substr(0, 3) + mapComponetnt + ";";
}
} else {
mapData = rawData;
}
}
else if (/[0-9]/.test(rawData)) {
mapData = rawDataMap(rawData, 2);
}
else {
mapData = rawData;
}
}
return mapData;
}
```
```javascript
//api
module.exports = {
"GET /api/products": async (ctx, next) => {
ctx.response.type = "application/json";
ctx.response.body = {
products: products
};
},
"GET /api/solution1": async (ctx, next) => {
try {
var data = fs.readFileSync(pathname, "utf-8");
ruleJson = JSON.parse(data);
rule = ruleJson.data.rule;
} catch (error) {
console.log("fail: " + error);
}
var data = {
code: 200,
message: "success",
data: {
name: "@杭城小刘",
year: LBPEncode("1995", rule),
month: LBPEncode("02", rule),
day: LBPEncode("20", rule),
analysis : rule
}
}
ctx.set("Access-Control-Allow-Origin", "*");
ctx.response.type = "application/json";
ctx.response.body = data;
},
"GET /api/solution2": async (ctx, next) => {
try {
var data = fs.readFileSync(pathname, "utf-8");
ruleJson = JSON.parse(data);
rule = ruleJson.data.rule;
} catch (error) {
console.log("fail: " + error);
}
var data = {
code: 200,
message: "success",
data: {
name: LBPEncode("建造师",rule),
birthday: LBPEncode("1995年02月20日",rule),
company: LBPEncode("中天公司",rule),
address: LBPEncode("浙江省杭州市拱墅区石祥路",rule),
bidprice: LBPEncode("2万元",rule),
negative: LBPEncode("2018年办事效率太高、负面基本没有",rule),
title: LBPEncode("建造师",rule),
honor: LBPEncode("最佳奖",rule),
analysis : rule
}
}
ctx.set("Access-Control-Allow-Origin", "*");
ctx.response.type = "application/json";
ctx.response.body = data;
},
"POST /api/products": async (ctx, next) => {
var p = {
name: ctx.request.body.name,
price: ctx.request.body.price
};
products.push(p);
ctx.response.type = "application/json";
ctx.response.body = p;
}
};
```
```javascript
//路由
const fs = require("fs");
function addMapping(router, mapping){
for(var url in mapping){
if (url.startsWith("GET")) {
var path = url.substring(4);
router.get(path,mapping[url]);
console.log(`Register URL mapping: GET: ${path}`);
}else if (url.startsWith('POST ')) {
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`Register URL mapping: POST ${path}`);
} else if (url.startsWith('PUT ')) {
var path = url.substring(4);
router.put(path, mapping[url]);
console.log(`Register URL mapping: PUT ${path}`);
} else if (url.startsWith('DELETE ')) {
var path = url.substring(7);
router.del(path, mapping[url]);
console.log(`Register URL mapping: DELETE ${path}`);
} else {
console.log(`Invalid URL: ${url}`);
}
}
}
function addControllers(router, dir){
fs.readdirSync(__dirname + "/" + dir).filter( (f) => {
return f.endsWith(".js");
}).forEach( (f) => {
console.log(`Process controllers:${f}...`);
let mapping = require(__dirname + "/" + dir + "/" + f);
addMapping(router,mapping);
});
}
module.exports = function(dir){
let controllers = dir || "controller";
let router = require("koa-router")();
addControllers(router,controllers);
return router.routes();
};
```
#### 2.4 前端根据服务端返回的数据逆向解密
```javascript
$("#year").html(getRawData(data.year,log));
// util.js
var JoinOparatorSymbol = "3.1415926";
function isNotEmptyStr($str) {
if (String($str) == "" || $str == undefined || $str == null || $str == "null") {
return false;
}
return true;
}
function getRawData($json,analisys) {
$json = $json.toString();
if (!isNotEmptyStr($json)) {
return;
}
var date= new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
var datacomponents = $json.split(JoinOparatorSymbol);
var orginalMessage = "";
for(var index = 0;index < datacomponents.length;index++){
var datacomponent = datacomponents[index];
if (!isNaN(datacomponent) && analisys < 3){
var currentNumber = parseInt(datacomponent);
orginalMessage += (currentNumber - day)/month;
}
else if(analisys == 3){
orginalMessage += datacomponent;
}
else{
//其他情况待续,本 Demo 根据本人在研究反爬方面的技术并实践后持续更新
}
}
return orginalMessage;
}
```
比如后端返回的是323.14743.14743.1446根据我们约定的算法可以的到结果为1773
#### 2.5 根据 ttf 文件 Render 页面
![自定义字体文件](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180724-184215.png)
上面计算的到的1773然后根据ttf文件页面看到的就是1995
#### 2.6 加密混淆
为了防止爬虫人员查看 JS 研究问题,所以对 JS 的文件进行了加密处理。如果你的技术栈是 Vue 、React 等webpack 为你提供了 JS 加密的插件,也很方便处理
[JS混淆工具](http://www.javascriptobfuscator.com/Javascript-Obfuscator.aspx)
- 个人觉得这种方式还不是很安全。于是想到了各种方案的组合拳。比如
##  三、反爬升级版
个人觉得如果一个前端经验丰富的爬虫开发者来说,上面的方案可能还是会存在被破解的可能,所以在之前的基础上做了升级版本
1. 组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模有4种值 0、1、2、3。这4种值对应不同的字体文件所以当爬虫绞尽脑汁爬到1种情况下的字体时没想到再次请求字体文件的规则变掉了 😂
2. 组合拳2: 前面的规则是字体问题乱序,但是只是数字匹配打乱掉。比如 **1** -> **4**, **5** -> **8**。接下来的套路就是每个数字对应一个 **unicode 码** ,然后制作自己需要的字体,可以是 .ttf、.woff 等等。
![网页检察元素得到的效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180726-161418.png)
![接口返回数据](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180726-161429.png)
这几种组合拳打下来。对于一般的爬虫就放弃了。
## 四、反爬手段再升级
上面说的方法主要是针对**数字**做的反爬手段,如果要对汉字进行反爬怎么办?接下来提供几种方案
1. **方案1:** 对于你站点频率最高的词云,做一个汉字映射,也就是自定义字体文件,步骤跟数字一样。先将常用的汉字生成对应的 ttf 文件;根据下面提供的链接,将 ttf 文件转换为 svg 文件,然后在下面的“字体映射”链接点进去的网站上面选择前面生成的 svg 文件将svg文件里面的每个汉字做个映射也就是将汉字专为 unicode 码(注意这里的 unicode 码不要去在线直接生成因为直接生成的东西也就是有规律的。我给的做法是先用网站生成然后将得到的结果做个简单的变化比如将“e342”转换为 “e231”然后接口返回的数据按照我们的这个字体文件的规则反过去映射出来。
2. **方案2:** 将网站的重要字体,将 html 部分生成图片,这样子爬虫要识别到需要的内容成本就很高了,需要用到 OCR。效率也很低。所以可以拦截钓一部分的爬虫
3. **方案3:** 看到携程的技术分享“反爬的最高境界就是 Canvas 的指纹,原理是不同的机器不同的硬件对于 Canvas 画出的图总是存在像素级别的误差,因此我们判断当对于访问来说大量的 canvas 的指纹一致的话,则认为是爬虫,则可以封掉它”。
本人将方案1实现到 Demo 中了。
#### 关键步骤
1. 先根据你们的产品找到常用的关键词,生成**词云**
2. 根据词云,将每个字生成对应的 unicode 码
3. 将词云包括的汉字做成一个字体库
4. 将字体库 .ttf 做成 svg 格式,然后上传到 [icomoon](https://icomoon.io/app/#/select/font) 制作自定义的字体,但是有规则,比如 **“年”** 对应的 **unicode 码**是 **“\u5e74”** ,但是我们需要做一个 **恺撒加密** ,比如我们设置 **偏移量** 为1那么经过**恺撒加密** **“年”**对应的 **unicode** 码是**“\u5e75”** 。利用这种规则制作我们需要的字体库
5. 在每次调用接口的时候服务端做的事情是:服务端封装某个方法,将数据经过方法判断是不是在词云中,如果是词云中的字符,利用规则(找到汉字对应的 unicode 码再根据凯撒加密设置对应的偏移量Demo 中为1将每个汉字加密处理加密处理后返回数据
6. 客户端做的事情:
- 先引入我们前面制作好的汉字字体库
- 调用接口拿到数据,显示到对应的 Dom 节点上
- 如果是汉字文本,我们将对应节点的 css 类设置成汉字类,该类对应的 font-family 是我们上面引入的汉字字体库
```css
//style.css
@font-face {
font-family: "NumberFont";
src: url('http://127.0.0.1:8080/Util/analysis');
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@font-face {
font-family: "CharacterFont";
src: url('http://127.0.0.1:8080/Util/map');
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h2 {
font-family: "NumberFont";
}
h3,a{
font-family: "CharacterFont";
}
```
![接口效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095006%402x.png)
![审查元素效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095124%402x.png)
传送门:[字体制作的步骤](https://blog.csdn.net/fdipzone/article/details/68166388)、[ttf转svg](https://everythingfonts.com/ttf-to-svg)、[字体映射规则](https://icomoon.io/app/#/select/font)
实现效果:
1. 页面上看到的数据跟审查元素看到的结果不一致
2. 去查看接口数据跟审核元素和界面看到的三者不一致
3. 页面每次刷新之前得出的结果更不一致
4. 对于数字和汉字的处理手段都不一致
这几种组合拳打下来。对于一般的爬虫就放弃了。
![数字反爬-网页显示效果、审查元素、接口结果情况1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180810-151046@2x.png)
![数字反爬-网页显示效果、审查元素、接口结果情况2](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180810-151203@2x.png)
![数字反爬-网页显示效果、审查元素、接口结果情况3](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-151239@2x.png)
![数字反爬-网页显示效果、审查元素、接口结果情况4](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-151308@2x.png)
![汉字反爬-网页显示效果、审查元素、接口结果情况1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095006%402x.png)
![汉字反爬-网页显示效果、审查元素、接口结果情况2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095124%402x.png)
<hr>
前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,贴出个新链接。[ttf转svg](https://convertio.co/zh/font-converter/)
## [Demo 地址](https://github.com/FantasticLBP/Anti-WebSpider)
![效果演示](https://raw.githubusercontent.com/FantasticLBP/Anti-WebSpider/master/Anti-WebSpider.gif)
运行步骤
```powershell
//客户端先查看本机 ip Demo/Spider-develop/Solution/Solution1.js Demo/Spider-develop/Solution/Solution2.js 里面将接口地址修改为本机 ip
$ cd Demo
$ ls
REST Spider-release file-Server.js
Spider-develop Util rule.json
$ node file-Server.js
Server is runnig at http://127.0.0.1:8080/
//服务端 先安装依赖
$ cd REST/
$ npm install
$ node app.js
```

View File

@@ -38,7 +38,7 @@
hello("hello webpack");
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-095124@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180802-095124@2x.png)
通过报错信息知道, webpack 对于 css 文件并不是默认支持的,需要指定相应的 loader 对其打包。
@@ -84,7 +84,7 @@
6. 查看网页效果。发现函数确实执行了,背景颜色也生效了,我们写的 css 代码新建了一个 **style标签** 被直接写入到 html 中了。
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-100727@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180802-100727@2x.png)
7. 说说2个 loader 的作用。
@@ -131,7 +131,7 @@
webpack --mode=development hello.js --output-file hello.bundle.js --module-bind 'css=style-loader!css-loader' --progress --display-modules
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-103343@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180802-103343@2x.png)
12. 如果想知道打包某个模块的原因,可以使用 **--display-reasons**
@@ -139,7 +139,7 @@
webpack --mode=development hello.js --output-file hello.bundle.js --module-bind 'css=style-loader!css-loader' --progress --display-modules --display-reasons
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-103717@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180802-103717@2x.png)
## 用 webpack.config.js 完成上述步骤
@@ -159,13 +159,13 @@
2. 有了 webpack.config.js 文件,就不需要和上面的方式一样,指定对应的 configuration option。在终端运行 **webpack --mode=development **
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-110653@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180802-110653@2x.png)
3. 注意:如果我们将 webpack.config.js 改名为 webpack.dev.config.js ,然后在命令行打包,会发现没效果。
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-110938@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180802-110938@2x.png)
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-111011@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180802-111011@2x.png)
要将 webpack.dev.config.js 同样生效,我们需要在命令行使用下面命令。
@@ -173,7 +173,7 @@
webpack --mode=development --config webpack.dev.config.js
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-111143@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180802-111143@2x.png)
4. 如果想像上个实验一样,看到打包时候的一些信息,怎么办呢?
@@ -186,7 +186,7 @@
},
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-113022@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180802-113022@2x.png)
5. 对于 webpack 的 entrt 主要有3种写法每种写法都有不同区别。
@@ -230,11 +230,11 @@
}
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-120033@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180802-120033@2x.png)
将文件修改为 **filename: '\[name\]-\[chunkhash\].js'**
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-120113@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/QQ20180802-120113@2x.png)
会发现 hash 和 chunkhash 的输出的文件名并不一样
@@ -409,7 +409,7 @@
publicPath ""
chunks {"main":{"size":28,"entry":"js/main-82c7521f0a4a776cc00b.js","hash":"82c7521f0a4a776cc00b","css":[]},"a":{"size":18,"entry":"js/a-273641522fd044fc27c7.js","hash":"273641522fd044fc27c7","css":[]}}
chunks {"main":{"size":28,"entry":"js/main-82c7521f0a4a776cc00b.js","hash":"82c7521f0a4a776cc00b","css":[]},"a":{"size":18,"entry":"js/a-273641522fd044fc27c7.js","hash":"273641522fd044fc27c7","css":[]} }
js ["js/main-82c7521f0a4a776cc00b.js","js/a-273641522fd044fc27c7.js"]
@@ -945,7 +945,7 @@
test: /\.css$/,
use:[
'style-loader',
{loader: 'css-loader', options: {importLoaders: 1}},
{loader: 'css-loader', options: {importLoaders: 1} },
{
loader: 'postcss-loader',
options:{
@@ -963,7 +963,7 @@
test: /\.less$/,
use:[
'style-loader',
{loader: 'css-loader', options: {importLoaders: 1}},
{loader: 'css-loader', options: {importLoaders: 1} },
{
loader: 'postcss-loader',
options:{
@@ -1028,7 +1028,7 @@
new App()
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180803-152208@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180803-152208@2x.png)
- ejs 模版
@@ -1073,7 +1073,7 @@
new App()
```
![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180803-152612@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180803-152612@2x.png)

View File

@@ -1,4 +1,4 @@
# 正则表达式
* \d :匹配一个数字
* \w : 匹配任意一个字母或数字

View File

@@ -1,8 +1,9 @@
# [Chrome 调试技巧](https://segmentfault.com/a/1190000016256731)
# Chrome 调试技巧
> **写在前面**
> Chrome 有非常强大的调试功能
> 本文包括浏览器调试不包括web移动端调试。
> 本文调试均在chrome浏览器进行
@@ -25,7 +26,7 @@ console.debug("我是个调试");//在控制台打印自定义调试信息
cosole.clear();//清空控制台(这个下方截图中没有)
```
![console](https://segmentfault.com/img/remote/1460000016256734)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-01-15-01.png)
注意上面输出的error和throw出的error不一样前者只是输出错误信息无法捕获不会冒泡更不会中止程序运行。
@@ -43,7 +44,7 @@ console.log("%c自定义样式","font-size:30px;color:#00f");
console.log("%c我是%c自定义样式","font-size:20px;color:green","font-size:10px;color:red");
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256735.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256735.png)
<todo>
#### DOM输出
@@ -55,7 +56,7 @@ var ul = document.getElementsByTagName("ul");
console.dirxml(ul); //树形输出table节点即<table>和它的innerHTML由于document.getElementsByTagName是动态的所以这个得到的结果肯定是动态的
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256736.png)
![console](https://github.com/FantasticLBP/knowledge-kit/tree/master/assets/1460000016256736.png)
#### 对象输出
@@ -67,7 +68,7 @@ var o = {
console.dir(obj);//显示对象自有属性和方法
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256737.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256737.png)
对于多个对象的集合,你可以这样,输出更清晰:
@@ -77,7 +78,7 @@ console.log(stu);
console.table(stu);
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256738.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256738.png)
#### 成组输出
@@ -90,7 +91,7 @@ console.log("sub1");
console.groupEnd("end");
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256739.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256739.png)
#### 函数计数和跟踪
@@ -108,7 +109,7 @@ function fib(n){ //输出前n个斐波那契数列值
fib(6);
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256740.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256740.png)
Chrome开发者工具中的Sources标签页也在Watch表达式下面显示调用栈。
#### 计时
@@ -119,7 +120,7 @@ fib(100); //用上述函数计算100个斐波那契数
console.timeEnd() //计时结束并输出时长
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256741.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256741.png)
断言语句这个c++调试里面也经常用到。js中当第一个表达式或参数为true时候什么也不发生为false时终止程序并报错
```
@@ -127,7 +128,7 @@ console.assert(true, "我错了");
console.assert(false, "我真的错了");
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256742.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256742.png)
#### 性能分析
@@ -148,14 +149,14 @@ F();
console.profileEnd();
```
![console](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256743.png)
![console](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256743.png)
Chrome开发者工具中的Audits标签页也可以实现性能分析。
### debugger
这个重量级的是博主最常用的可能是c++出身对于单步调试由衷的热爱。单步调试就是点一下执行一句程序并且可以查看当前作用域可见的所有变量和值。而debugger就是告诉程序在那里停下来进行单步调试俗称断点。
![debugger](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256744.jpeg)
![debugger](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256744.jpeg)
右边按钮如下:
@@ -167,7 +168,7 @@ console.profileEnd();
- Pause on exceptions异常情况自动断点设置。
其实右侧还有很多强大的功能
![debugger](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256745.png)
![debugger](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256745.png)
- WatchWatch表达式
- Call Stack: 栈中变量的调用,这里是递归调用,肯定是在内存栈部分调用。
@@ -180,7 +181,7 @@ console.profileEnd();
2. 当节点内部子节点变化时断点Break on subtree modifications
3. 当节点被移除时断点Break on node removal
![debugger](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256746.png)
![debugger](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256746.png)
- Global Listeners全局事件监听
- Event Listener Breakpoints事件监听器断点列出了所有页面及脚本事件包括鼠标、键盘、动画、定时器、XHR等等。
@@ -222,8 +223,45 @@ arr[0].num += 1;
}, 1000);
```
![careful](/Users/liubinpeng/Desktop/Github/knowledge-kit/第二部分 Web 前端/assets/1460000016256747.png)
![careful](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1460000016256747.png)
这里第一个属性中对象引用的值是不可靠的。当你第一次在开发者工具中显示这个属性时num的值就已经确定了。之后无论你对同一个引用重新打开多少次都不会变化。
2.尽可能使用 source map。有时生产代码不能使用source map但不管怎样你都不应该直接对生产代码进行调试。
2.尽可能使用 source map。有时生产代码不能使用source map但不管怎样你都不应该直接对生产代码进行调试。
### 异常调试
```
<script>
const age = 23;
age = 24;
console.log(age);
</script>
```
代码会报错。为了诸多原因,我们希望提前解决,所以我们希望准确知道代码在哪里有问题。也就是需要调试,希望 JS 像其他编程语言一样可以调试。
默认情况下会在 console 里面报错。如下图
![默认报错](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-12-24-chrome1.png)
为了准确定位,我们可以在调试模式的右侧开启 “Pause on exception”按钮
![开启Pause on exception](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-12-24-chrome2.png)
如果我们的代码加了 try.catch.,那么之前的设置是不能定位到异常的位置。
```
<script>
try {
const age = 23;
age = 24;
console.log(age);
} catch (error) {
console.log(error)
}
</script>
```
![不能定位try.catch内部的错误](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-12-24-chrome3.png)
如果想捕获try.catch里面的异常则可以在调试面板的右侧勾选“Pause on caught exceptions”设置完即使是 try.catch 里面的异常也可以定位到具体位置
![打开“Pause on caught exceptions”](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-12-24-chrome4.png)

View File

@@ -0,0 +1,193 @@
# 大前端动画
> 大前端开发中经常会遇到动画的开发那么什么是动画在物理学中运动就是研究物体在时间维度和空间维度上改变的现象所以动画也一样动画主要研究2个因素发生运动物体的**时间**和**空间**。
#### Web前端开发中的动画
在 Web 前端开发中实现动画有2种方式。要么依靠 CSS 实现动画,要么依靠 JS 控制实现动画。
##### CSS 实现动画
首先要说 CSS 中的4个概念animation、transition、transform、translate
| 属性 | 含义 |
| :---: | :---: |
| transition\(过度动画\) | 用于设置元素的样式过渡效果,和 animation 有类似的效果,但存在使用场合有着较大差别 |
| transform\(变形\) | 用于设置元素的旋转、位移、缩放。和设置元素的动画并没直接关系,就跟写 css 属性一样 |
| translate\(移动\) | 用于设置元素的位置,就是 transform 的一个属性 |
| animation动画 | 用于设置怨毒的动画属性它是一个简写有6个属性值 |
**transition**
字面意思过渡是指元素从属性a的某个值过渡到属性a的另一个值这就是一个状态的改变但是需要一个条件来触发从而发生这种转变比如 `&:hover,&:checked,&:focus、媒体查询或者 JS`
```
#box {
height: 100px;
width: 100px;
background: green;
transition: transform 1s ease-in 1s;
}
#box:hover{
transform: rotate(180deg) scale(0.5,0.5);
transform: translateX(100px);
transform: translateY(100px) translateX(100px) scale(0.5, 0.5);
}
<div id="box"></div>
```
分析:给 div 添加了一个过渡动画,动画指定了 transform 动画,触发时机为当鼠标移上去的时候。因此当鼠标移入的时候元素的 transform 属性发生变化,那么这个时候触发了 transition 动画,当鼠标移除的时候也产生了 transform 的变化,因此还是会触发 transition产生动画。
上面设置了3个 transform 只有最后一个生效
因此 transition 产生动画的条件是设置的 property 发生变化,这种动画的特点是需要一个驱动力去触发。因此就存在一些缺点:
1. 需要事件触发,没法在网页加载时自动发生
2. 是一次性的,不能重复发生,除非再次触发
3. 只可以定义开始状态和结束状态,不能定义中间状态,因此没有丰富的动画空间
4. 一条 transition 规则,只能定义一个属性的变化,不能涉及多个属性
语法:**transitionproperty duration timing-function delay**
| 属性 |含义 |
| :---: | :---: |
| transition-property | 规定设置过渡效果的 css 属性名称 |
| transition-duration | 规定完成过渡效果需要时间 |
| transition-timing-function | 规定速度效果的速度曲线 |
| transition-delay | 规定动画效果何时开始 |
**animation**
animation 总体来说是对 transition 的增强,不再受限于触发时机和动画的属性值。
```
.box {
height: 100px;
width: 100px;
border: 15px solid black;
animation: changebox 4s ease-in-out 1s 1 alternate running forwards;
}
.box:hover {
animation-play-state: paused;
}
@keyframes changebox {
10% {
background: red;
}
50% {
width: 80px;
}
70% {
border: 15px solid yellow;
}
100% {
width: 180px;
height: 180px;
}
}
<div class="box"></div>
```
**animation: animation-name, animation-duration, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, animation-fill-mode**
|属性 | 含义 |
| :----: | :----: |
| animation-name | 用来调用@keyframes定义好的动画,与@keyframes定义的动画名称一致 |
| animation-duration | 指定元素播放动画所持续的时间 |
| animation-timing-function | 指定速度效果的速度曲线,是针对每一个小动画所在时间范围内的变化频率|
| animation-delay| 定义在执行动画之前的等待时间|
| animation-iteration-count| 定义动画的播放次数可选具体次数或者无限次infinite|
| animation-direction |设置动画播放方向normal按时间轴顺序、alternate轮流即来回往复进行) |
| animation-play-state| 控制元素的播放状态running继续、paused暂停|
| animation-fill-mode | 控制动画结束后元素的样式有4个值 none回到动画之前的状态、forwards元素停留在动画结束后的状态、backwords动画回到第一帧的状态、both根据 animation-direction 轮流应用 forwards 和 backwords 规则)。注意与 iteration-count 不要冲突|
总结:单个动画效果、简单的由 transtion 实现,复杂的用 animation 实现。animation 出现后市面上出现了很多这种 css 动画库,其中我在使用 animate.css 推荐小伙伴们使用下
##### JS 实现动画
大家用 JS 写动画立马想到的是 setTimeout 和 setInterval但是较好的动画体验是保持在 60fps 最好上面的2个 api 由于会受到 runloop 的影响并不会特别准时JS 有个 requestAnimationFrame api 可以保持动画在 60fps
JS 实现动画的本质就是控制元素在时间和空间上的变化的研究。
假如要实现一个匀速直线动画,让一个 div 在 3秒内在水平方向上从向由右移动500px。那么如何实现先从物理问题上解决吧。
总时间: 3s
总位移: 500px
那么每秒移动多少 500px/3s
我们设计一个 JS 函数。4个参数 属性开始值,属性结束值,动画执行时间,回调函数
大体思路是外界传入上面4个参数我们可以记录函数调用刚开始的时刻也就是开始时间start然后通过 performance.now\(\) 拿到当前时间now然后 period = \(now-start\) 就是经过的时间。然后通过 period/time 就是时间的进度百分比,拿这个百分比再去乘以总的属性值差就是当前的属性值,然后将计算结果实时调用回调函数(这个回调函数就是指定这个属性值如何应用到动画元素上)
```
/**
* 执行补间动画方法
*
* @param {Number} start 开始数值
* @param {Number} end 结束数值
* @param {Number} time 补间时间
* @param {Function} callback 每帧回调
* @param {Function} timing 速度曲线,默认匀速
*/
function animate(start, end, time, callback, timing = t => t) {
let startTime = performance.now() // 设置开始的时间戳
let period = end - start // 拿到数值差值
// 创建每帧之前要执行的函数
function loop() {
liveAnimationFunction = requestAnimationFrame(loop) // 下一调用每帧之前要执行的函数
const passTime = performance.now() - startTime // 获取当前时间和开始时间差
let per = passTime / time // 计算当前已过百分比
if (per >= 1) { // 判读如果已经执行
per = 1 // 设置为最后的状态
cancelAnimationFrame(raf) // 停掉动画
}
const pass = period * timing(per) // 通过已过时间百分比*开始结束数值差得出当前的数值
callback(pass)
}
let liveAnimationFunction = requestAnimationFrame(loop) // 下一阵调用每帧之前要执行的函数
}
function doMove(easing) {
asing])
}
function move(box, value) {
box.style.transform = `translateX(${value}px)`
}
```
#### Native 端动画iOS为例
其实动画的本质就是元素时间和空间上发生变化的研究。在 web 前端如此,在 native 端也是如此,不过就是换了一些 api 如此。
举个例子,就拿上面所说的水平位移为例,下面给 iOS 的原生代码
```
//CALayer 层动画
CABasicAnimation *positionAnimation = [CABasicAnimation animation];
//指定动画路径是水平方向x轴
positionAnimation.keyPath = @"position.x";
//指定位移距离
positionAnimation.toValue = @1000;
//下面2行代码让动画停留在动画结束的位置
positionAnimation.fillMode = kCAFillModeForwards;//(效果完全等同于 css 中的 animation-fill-mode属性
positionAnimation.removedOnCompletion = NO;
[self.animationView.layer addAnimation:positionAnimation forKey:nil];
```
css 中的 animation-fill-mode控制动画结束后元素的样式有4个值 none回到动画之前的状态、forwards元素停留在动画结束后的状态、backwords动画回到第一帧的状态、both根据 animation-direction 轮流应用 forwards 和 backwords 规则))和 nativeiOS端的 fillMode 属性一致。
下面提出 iOS 端的 fillMode 取值选项
```
/* `fillMode' options. */
CA_EXTERN CAMediaTimingFillMode const kCAFillModeForwards
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CAMediaTimingFillMode const kCAFillModeBackwards
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CAMediaTimingFillMode const kCAFillModeBoth
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CAMediaTimingFillMode const kCAFillModeRemoved
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
```
看得出来 fillMode web 端和 native 端是一模一样的。
再举个例子,平时我们有可能画一根横线,在 web 端 和 native 端都存在 view 这样的概念,在 web 端可能会是一个 div高度设置为1或者是 canvas 实现canvas 拿到当前上下文、绘制路径、关闭路径、填充颜色)。在 native 端也一样,开启绘图上下文、拿到上下对象、绘制路径、上色、关闭上下文。
所以其他具体的例子也就不举了,本质上大前端的所有动画干的事情都一样,所以我们需要处理好时间和位置的关系。

View File

@@ -0,0 +1,10 @@
# Node.js 在 Linux 下的安装
如果是在新电脑上面部署 Node 项目,首先进入官网下载 Node 包,解压到系统的一个合适文件夹
执行下面2步命令将 Node 的命令关联到全局命令,这样就可以在命令行中执行 Node 脚本
```
sudo ln -s /home/LBP/node-v10.12.0-linux-x64/bin/node /usr/local/bin/
sudo ln -s /home/LBP/node-v10.12.0-linux-x64/bin/npm /usr/local/bin/

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