diff --git a/.DS_Store b/.DS_Store index 055f4bc..70de2e6 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/第一部分 iOS/1.1.md b/Chapter1 - iOS/1.1.md similarity index 86% rename from 第一部分 iOS/1.1.md rename to Chapter1 - iOS/1.1.md index 477fc9c..b8cf459 100644 --- a/第一部分 iOS/1.1.md +++ b/Chapter1 - iOS/1.1.md @@ -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根据十六进制字符串来设置颜色 diff --git a/第一部分 iOS/1.10.md b/Chapter1 - iOS/1.10.md similarity index 69% rename from 第一部分 iOS/1.10.md rename to Chapter1 - iOS/1.10.md index 2957029..4779544 100644 --- a/第一部分 iOS/1.10.md +++ b/Chapter1 - iOS/1.10.md @@ -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脚本的结果。本质上是一个同步调用 diff --git a/第一部分 iOS/1.11.md b/Chapter1 - iOS/1.11.md similarity index 98% rename from 第一部分 iOS/1.11.md rename to Chapter1 - iOS/1.11.md index 3b8df60..bfe0c99 100644 --- a/第一部分 iOS/1.11.md +++ b/Chapter1 - iOS/1.11.md @@ -1,4 +1,4 @@ - +# iOS中的事件 diff --git a/第一部分 iOS/1.12.md b/Chapter1 - iOS/1.12.md similarity index 94% rename from 第一部分 iOS/1.12.md rename to Chapter1 - iOS/1.12.md index 7bdf6c2..78acdff 100644 --- a/第一部分 iOS/1.12.md +++ b/Chapter1 - iOS/1.12.md @@ -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,如果文件夹不存在则停止创建文件 diff --git a/第一部分 iOS/1.13.md b/Chapter1 - iOS/1.13.md similarity index 99% rename from 第一部分 iOS/1.13.md rename to Chapter1 - iOS/1.13.md index cd83d3c..2a3b9cf 100644 --- a/第一部分 iOS/1.13.md +++ b/Chapter1 - iOS/1.13.md @@ -1,6 +1,4 @@ - - -### UINavagationController重写push和pop方法 +# UINavagationController重写push和pop方法 > 有个需求就是在App的Tab的首页需要显示浮动着的交互动画的机器人,该机器人具有机器学习的特点,因此可以不断的与用户交互,怎么样实现只浮动在App的5个tab首页,当点击跳转不是首页的时候不需要显示 diff --git a/第一部分 iOS/1.14.md b/Chapter1 - iOS/1.14.md similarity index 99% rename from 第一部分 iOS/1.14.md rename to Chapter1 - iOS/1.14.md index 60037a9..8678da8 100644 --- a/第一部分 iOS/1.14.md +++ b/Chapter1 - iOS/1.14.md @@ -1,5 +1,4 @@ - -###自定义URL Schemes### +# 自定义URL Schemes 1、引言 diff --git a/第一部分 iOS/1.15.md b/Chapter1 - iOS/1.15.md similarity index 97% rename from 第一部分 iOS/1.15.md rename to Chapter1 - iOS/1.15.md index dc31747..9efffd5 100644 --- a/第一部分 iOS/1.15.md +++ b/Chapter1 - iOS/1.15.md @@ -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 的发展过
  • 在 CFBundleURLSchemes 下的那两行就是该 App 的基本 URL Schemes 了。
  • -###复杂 URL Schemes### +## 复杂 URL Schemes 参考链接:[URL Scheme](https://sspai.com/post/31500#fnref:2) diff --git a/第一部分 iOS/1.17.md b/Chapter1 - iOS/1.16.md similarity index 92% rename from 第一部分 iOS/1.17.md rename to Chapter1 - iOS/1.16.md index d3ad5a0..a44c262 100644 --- a/第一部分 iOS/1.17.md +++ b/Chapter1 - iOS/1.16.md @@ -1,4 +1,4 @@ -### Swift、OC混编 +# Swift、OC混编 ``` 1、在oc文件中使用swift文件。 diff --git a/第一部分 iOS/1.18.md b/Chapter1 - iOS/1.17.md similarity index 79% rename from 第一部分 iOS/1.18.md rename to Chapter1 - iOS/1.17.md index e7ba4b3..aec3fd6 100644 --- a/第一部分 iOS/1.18.md +++ b/Chapter1 - iOS/1.17.md @@ -1,3 +1,5 @@ +# 对于不可调节高度的UI控件进行改变frame + * 对于不能调节高度的控件比如 UISlider、UISwitch、UIProgressView 等控件的宽高可以用 \(仿射变化\)transform 属性控制高度。 ``` diff --git a/第一部分 iOS/1.19.md b/Chapter1 - iOS/1.18.md similarity index 99% rename from 第一部分 iOS/1.19.md rename to Chapter1 - iOS/1.18.md index 1f01186..ab99ba4 100644 --- a/第一部分 iOS/1.19.md +++ b/Chapter1 - iOS/1.18.md @@ -1,4 +1,4 @@ -### 简单的 Model 与 JSON 相互转换 +# 简单的 Model 与 JSON 相互转换 ``` // JSON: diff --git a/第一部分 iOS/1.21.md b/Chapter1 - iOS/1.19.md similarity index 91% rename from 第一部分 iOS/1.21.md rename to Chapter1 - iOS/1.19.md index f9d8526..5698b69 100644 --- a/第一部分 iOS/1.21.md +++ b/Chapter1 - iOS/1.19.md @@ -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 的数组,则图层颜色由上至下,从数组第一个值经中间值渐变至最后一个值。 diff --git a/第一部分 iOS/1.2.md b/Chapter1 - iOS/1.2.md similarity index 56% rename from 第一部分 iOS/1.2.md rename to Chapter1 - iOS/1.2.md index 607cbc6..cd9e177 100644 --- a/第一部分 iOS/1.2.md +++ b/Chapter1 - iOS/1.2.md @@ -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的实现是先alloc再init,默认init的实现是给属性赋默认值)** +**注意:此时不能使用 new 来调用。(因为 new 的实现是先 alloc 再 init ,默认 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 开头的方法是构造方法 diff --git a/第一部分 iOS/1.22.md b/Chapter1 - iOS/1.20.md similarity index 93% rename from 第一部分 iOS/1.22.md rename to Chapter1 - iOS/1.20.md index 2a644e1..cbf322a 100644 --- a/第一部分 iOS/1.22.md +++ b/Chapter1 - iOS/1.20.md @@ -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 经常造成循环引用 diff --git a/第一部分 iOS/1.23.md b/Chapter1 - iOS/1.21.md similarity index 97% rename from 第一部分 iOS/1.23.md rename to Chapter1 - iOS/1.21.md index 6c7f81a..2ef9a43 100644 --- a/第一部分 iOS/1.23.md +++ b/Chapter1 - iOS/1.21.md @@ -1,6 +1,6 @@ -#禅与 Objective-C 编程艺术 +# 禅与 Objective-C 编程艺术 -#### 警告和错误 +## 警告和错误 * 警告 ``` diff --git a/第一部分 iOS/1.24.md b/Chapter1 - iOS/1.22.md similarity index 100% rename from 第一部分 iOS/1.24.md rename to Chapter1 - iOS/1.22.md diff --git a/第一部分 iOS/1.25.md b/Chapter1 - iOS/1.23.md similarity index 96% rename from 第一部分 iOS/1.25.md rename to Chapter1 - iOS/1.23.md index d963128..c9123e4 100644 --- a/第一部分 iOS/1.25.md +++ b/Chapter1 - iOS/1.23.md @@ -1,4 +1,4 @@ -## UIScrollView 拖拽滑动时收起键盘 +# UIScrollView 拖拽滑动时收起键盘 > 当一个页面的 UIScrollView/UITableView 上有输入框时,为了较好的体验,就是当滑动的时候需要回收键盘 diff --git a/第一部分 iOS/1.26.md b/Chapter1 - iOS/1.24.md similarity index 100% rename from 第一部分 iOS/1.26.md rename to Chapter1 - iOS/1.24.md diff --git a/第一部分 iOS/1.27.md b/Chapter1 - iOS/1.25.md similarity index 94% rename from 第一部分 iOS/1.27.md rename to Chapter1 - iOS/1.25.md index b080254..2e23e02 100644 --- a/第一部分 iOS/1.27.md +++ b/Chapter1 - iOS/1.25.md @@ -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) 需求分析: diff --git a/第一部分 iOS/1.28.md b/Chapter1 - iOS/1.26.md similarity index 95% rename from 第一部分 iOS/1.28.md rename to Chapter1 - iOS/1.26.md index abebcc3..912fc9c 100644 --- a/第一部分 iOS/1.28.md +++ b/Chapter1 - iOS/1.26.md @@ -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) - 拖动到超过某个范围的时候怎么执行爆炸动画 diff --git a/第一部分 iOS/1.29.md b/Chapter1 - iOS/1.27.md similarity index 93% rename from 第一部分 iOS/1.29.md rename to Chapter1 - iOS/1.27.md index 2474f26..1ab8987 100644 --- a/第一部分 iOS/1.29.md +++ b/Chapter1 - iOS/1.27.md @@ -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) diff --git a/第一部分 iOS/1.30.md b/Chapter1 - iOS/1.28.md similarity index 100% rename from 第一部分 iOS/1.30.md rename to Chapter1 - iOS/1.28.md diff --git a/第一部分 iOS/1.31.md b/Chapter1 - iOS/1.29.md similarity index 99% rename from 第一部分 iOS/1.31.md rename to Chapter1 - iOS/1.29.md index e7be9ad..41aea00 100644 --- a/第一部分 iOS/1.31.md +++ b/Chapter1 - iOS/1.29.md @@ -1,4 +1,4 @@ -JavascriptCore +# JavascriptCore diff --git a/第一部分 iOS/1.3.md b/Chapter1 - iOS/1.3.md similarity index 90% rename from 第一部分 iOS/1.3.md rename to Chapter1 - iOS/1.3.md index 2d67921..6a6674a 100644 --- a/第一部分 iOS/1.3.md +++ b/Chapter1 - iOS/1.3.md @@ -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) diff --git a/Chapter1 - iOS/1.30.md b/Chapter1 - iOS/1.30.md new file mode 100644 index 0000000..b21d0a5 --- /dev/null +++ b/Chapter1 - iOS/1.30.md @@ -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->Build Settings->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 里面进行设置。 +``` +ITSAppUsesNonExemptEncryption + +``` + +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 + + diff --git a/第一部分 iOS/1.33.md b/Chapter1 - iOS/1.31.md similarity index 100% rename from 第一部分 iOS/1.33.md rename to Chapter1 - iOS/1.31.md diff --git a/第一部分 iOS/1.34.md b/Chapter1 - iOS/1.32.md similarity index 100% rename from 第一部分 iOS/1.34.md rename to Chapter1 - iOS/1.32.md diff --git a/第一部分 iOS/1.35.md b/Chapter1 - iOS/1.33.md similarity index 100% rename from 第一部分 iOS/1.35.md rename to Chapter1 - iOS/1.33.md diff --git a/第一部分 iOS/1.36.md b/Chapter1 - iOS/1.34.md similarity index 95% rename from 第一部分 iOS/1.36.md rename to Chapter1 - iOS/1.34.md index 9d0d687..833d5c7 100644 --- a/第一部分 iOS/1.36.md +++ b/Chapter1 - iOS/1.34.md @@ -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) diff --git a/第一部分 iOS/1.37.md b/Chapter1 - iOS/1.35.md similarity index 93% rename from 第一部分 iOS/1.37.md rename to Chapter1 - iOS/1.35.md index fedb034..69cd577 100644 --- a/第一部分 iOS/1.37.md +++ b/Chapter1 - iOS/1.35.md @@ -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**绘制图片,忽略图片本身的信息。
    -![引用自网络的图片](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/4673_140117110629_1.png?raw=true) \ No newline at end of file +![引用自网络的图片](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4673_140117110629_1.png) \ No newline at end of file diff --git a/第一部分 iOS/1.38.md b/Chapter1 - iOS/1.36.md similarity index 100% rename from 第一部分 iOS/1.38.md rename to Chapter1 - iOS/1.36.md diff --git a/第一部分 iOS/1.39.md b/Chapter1 - iOS/1.37.md similarity index 97% rename from 第一部分 iOS/1.39.md rename to Chapter1 - iOS/1.37.md index b46ad3b..eb3035f 100644 --- a/第一部分 iOS/1.39.md +++ b/Chapter1 - iOS/1.37.md @@ -1,4 +1,4 @@ -### 数组、集合、字典与 hash、isEqual 方法的关联 +# 数组、集合、字典与 hash、isEqual 方法的关联 1. NSArray 允许重复添加元素,添加元素的时候不查重,所以不会调用上面2个方法。在移出元素的时候会依次遍历数组内的元素,每个元素调用 **isEqual** 方法(remove 方法传入的元素作为参数),所有返回真值的元素都会被移除。在字典中不涉及 hash 方法。 diff --git a/第一部分 iOS/1.40.md b/Chapter1 - iOS/1.38.md similarity index 98% rename from 第一部分 iOS/1.40.md rename to Chapter1 - iOS/1.38.md index f900241..b627a04 100644 --- a/第一部分 iOS/1.40.md +++ b/Chapter1 - iOS/1.38.md @@ -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 的 - kCFRunLoopDefaultMode:App 的默认 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 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟 diff --git a/第一部分 iOS/1.41.md b/Chapter1 - iOS/1.39.md similarity index 82% rename from 第一部分 iOS/1.41.md rename to Chapter1 - iOS/1.39.md index b4e3d0b..99698da 100644 --- a/第一部分 iOS/1.41.md +++ b/Chapter1 - iOS/1.39.md @@ -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) diff --git a/第一部分 iOS/1.4.md b/Chapter1 - iOS/1.4.md similarity index 70% rename from 第一部分 iOS/1.4.md rename to Chapter1 - iOS/1.4.md index 49e1383..328a276 100644 --- a/第一部分 iOS/1.4.md +++ b/Chapter1 - iOS/1.4.md @@ -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) diff --git a/第一部分 iOS/1.42.md b/Chapter1 - iOS/1.40.md similarity index 100% rename from 第一部分 iOS/1.42.md rename to Chapter1 - iOS/1.40.md diff --git a/第一部分 iOS/1.43.md b/Chapter1 - iOS/1.41.md similarity index 98% rename from 第一部分 iOS/1.43.md rename to Chapter1 - iOS/1.41.md index 1a820ae..3dad364 100644 --- a/第一部分 iOS/1.43.md +++ b/Chapter1 - iOS/1.41.md @@ -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 @@ 有中文翻译版。 -# 总结 {#总结} +# 总结 上面的文章我都看过,或者至少是正在看,总结下来,辅助大家优化启动性能。 diff --git a/第一部分 iOS/1.44.md b/Chapter1 - iOS/1.42.md similarity index 97% rename from 第一部分 iOS/1.44.md rename to Chapter1 - iOS/1.42.md index 0d07412..460c6d1 100644 --- a/第一部分 iOS/1.44.md +++ b/Chapter1 - iOS/1.42.md @@ -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,这样中间人下发的公钥证书就可以被认为是合法的,可以通过验证的(中间人既承担了颁发证书,又承担了验证证书,通过验证)。 diff --git a/Chapter1 - iOS/1.43.md b/Chapter1 - iOS/1.43.md new file mode 100644 index 0000000..1d86089 --- /dev/null +++ b/Chapter1 - iOS/1.43.md @@ -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 ,解决问题为根本 + + diff --git a/Chapter1 - iOS/1.44..md b/Chapter1 - iOS/1.44..md new file mode 100644 index 0000000..ff6f80f --- /dev/null +++ b/Chapter1 - iOS/1.44..md @@ -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¶m=%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 对象中的 callback(Hybrid_时间戳) + +数据返回的格式和普通的接口返回格式类似 +``` +{ + 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 += "¶m=" + 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¶m=%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¶m=%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 与 righticon(icon居中) + +3. 满足一些特殊配置,比如标签类 Header + +所以,站在前端业务方来说,Header 的使用方式为(其中 tagname 是不允许重复的): + +``` + //Native以及前端框架会对特殊tagname的标识做默认回调,如果未注册callback,或者点击回调callback无返回则执行默认方法 + // back前端默认执行History.back,如果不可后退则回到指定URL,Native如果检测到不可后退则返回Naive大首页 + // home前端默认返回指定URL,Native默认返回大首页 + 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 框架中的 Vuex,React.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请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等。当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSURLProtocol看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。                                        + + diff --git a/第一部分 iOS/1.47.md b/Chapter1 - iOS/1.45.md similarity index 90% rename from 第一部分 iOS/1.47.md rename to Chapter1 - iOS/1.45.md index 31cd56f..f8d1619 100644 --- a/第一部分 iOS/1.47.md +++ b/Chapter1 - iOS/1.45.md @@ -45,7 +45,7 @@ 当前的 VC 和 定时器互相引用,造成循环引用。 -如何能在何时的时候打破循环引用就不会有问题了 +如果能在合适的时机打破循环引用就不会有问题了 1. 控制器不再强引用定时器 2. 定时器不再保留当前的控制器 @@ -54,8 +54,8 @@ //.h文件 #import -@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(@"----------------"); diff --git a/Chapter1 - iOS/1.46.md b/Chapter1 - iOS/1.46.md new file mode 100644 index 0000000..eab6645 --- /dev/null +++ b/Chapter1 - iOS/1.46.md @@ -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 *)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 + +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 + +@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 *)touches withEvent:(UIEvent *)event{ + _person.name = @"刘斌鹏"; + NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"]; + [hobbies addObject:@"Web"]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ + NSLog(@"%@",change); +} + +``` + +### KVO 的缺陷 + +KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block,没门儿。感觉如果加入 block 就更棒了。 + +### KVO 的改装 + +看到官方的做法并不是很方便使用,我们看到无数的优秀框架都支持 block 特性,比如 AFNetworking ,所以我们可以将系统的 KVO 改装成支持 block。 + + + + diff --git a/第一部分 iOS/1.49.md b/Chapter1 - iOS/1.47.md similarity index 100% rename from 第一部分 iOS/1.49.md rename to Chapter1 - iOS/1.47.md diff --git a/Chapter1 - iOS/1.48.md b/Chapter1 - iOS/1.48.md new file mode 100644 index 0000000..8522eb2 --- /dev/null +++ b/Chapter1 - iOS/1.48.md @@ -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 + +@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 + +NS_ASSUME_NONNULL_BEGIN + +@interface NSURL (Title) + +@property (nonatomic, copy) NSString *LR_title; + +@end + +NS_ASSUME_NONNULL_END + +#import "NSURL+Title.h" +#import + +@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 + +``` + + + + + + diff --git a/Chapter1 - iOS/1.49.md b/Chapter1 - iOS/1.49.md new file mode 100644 index 0000000..87dc8ff --- /dev/null +++ b/Chapter1 - iOS/1.49.md @@ -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 +``` + diff --git a/第一部分 iOS/1.5.md b/Chapter1 - iOS/1.5.md similarity index 88% rename from 第一部分 iOS/1.5.md rename to Chapter1 - iOS/1.5.md index 66bf795..bdfbac8 100644 --- a/第一部分 iOS/1.5.md +++ b/Chapter1 - iOS/1.5.md @@ -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) #### 事件传递的完整过程 diff --git a/Chapter1 - iOS/1.50.md b/Chapter1 - iOS/1.50.md new file mode 100644 index 0000000..c7a4a16 --- /dev/null +++ b/Chapter1 - iOS/1.50.md @@ -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 Time),Xcode 会自动筛选出静态库中的不同 architecture 合并到对应处理器架构的主可执行二进制文件中;而在打包归档(Archive)时,Xcode 会自动忽略掉静态库中未用到的 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 选项即可,无需其他设置。 \ No newline at end of file diff --git a/Chapter1 - iOS/1.51.md b/Chapter1 - iOS/1.51.md new file mode 100644 index 0000000..3e69994 --- /dev/null +++ b/Chapter1 - iOS/1.51.md @@ -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 +#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,下载旧版本 Xcode,pod 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 +``` \ No newline at end of file diff --git a/Chapter1 - iOS/1.52.md b/Chapter1 - iOS/1.52.md new file mode 100644 index 0000000..66f9c2f --- /dev/null +++ b/Chapter1 - iOS/1.52.md @@ -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. 对于属性的注释需要写在属性后面的地方。 //** 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) diff --git a/Chapter1 - iOS/1.53.md b/Chapter1 - iOS/1.53.md new file mode 100644 index 0000000..94d52a0 --- /dev/null +++ b/Chapter1 - iOS/1.53.md @@ -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 diff --git a/Chapter1 - iOS/1.54.md b/Chapter1 - iOS/1.54.md new file mode 100644 index 0000000..3f7aba2 --- /dev/null +++ b/Chapter1 - iOS/1.54.md @@ -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 复制进去,生效 + +### 模版 + +``` + + + + + FILEHEADER + +// ___FILENAME___ +// ___PACKAGENAME___ +// +// Author: @杭城小刘 +// Github: https://github.com/FantasticLBP +// E-mail: wsbglbp@outlook.com +// +// Created by 杭城小刘 on ___DATE___ +// + + + +``` diff --git a/Chapter1 - iOS/1.55.md b/Chapter1 - iOS/1.55.md new file mode 100644 index 0000000..2d22662 --- /dev/null +++ b/Chapter1 - iOS/1.55.md @@ -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 Cache:mmap,当内存中的数据达到一定的阀值,或者应用程序的生命周期切换的时候将内存中的数据同步到缓存中(数据库、磁盘、文件) +- Data Uploader:制定一定的策略,当达到触发条件的时候再去上传数据(App达到阀值,生命周期的切换等);App从前台进入后台的时候去上传数据(后台线程保活策略);数据上传格式的选择(zip压缩文件、protoBuf) + +### 1. 数据的收集 + +实现方案由以下几个关键指标: + +- 现有代码改动少、尽量不要侵入业务代码去实现拦截系统事件 +- 全量收集 +- 如何唯一标识一个控件元素 + + + +### 2. 不侵入业务代码拦截系统事件 + +以 iOS 为例。我们会想到 **AOP(Aspect 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 个子view:button1、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 + +NS_ASSUME_NONNULL_BEGIN + +@class NSObject; +@protocol UniqueIdentify + +@optional +- (NSString *)setUniqueIdentifier; + +@end + +@interface NSObject (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. 曝光时间的统计 + + + +曝光的意义是什么? + +我们的产品中可能有合作伙伴的广告,我们需要收取服务费。那如何计价?CPM(cost per Mille)每千人成本、CPC(cost per click)每点击成本、CPA(cost 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) \ No newline at end of file diff --git a/第二部分 Web 前端/2.18.md b/Chapter1 - iOS/1.56.md similarity index 68% rename from 第二部分 Web 前端/2.18.md rename to Chapter1 - iOS/1.56.md index f21de45..245f9b1 100644 --- a/第二部分 Web 前端/2.18.md +++ b/Chapter1 - iOS/1.56.md @@ -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 <-> 1,1 <-> 9,3 <-> 8,...)制作自定义字体(ttf) 2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 17730220 3. 对于第一步得到的字符串,依次遍历每个字符,将每个字符根据按照线性变换(y=kx+b)。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”,那么线性变换的 k 为 7,b 为 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+b,k和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)
    -前面的 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 层有完善的账号体系和网络层以及良好的安全策略、鉴权体系等等。 +
    + JS端发起网络请求代码:点击展开 + + ```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); + } + }) + ``` +
    + +
    + Objective-C代码:点击展开 + + ```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":@""}]); + }]; + } + }]; + ``` +
    + + 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的,因此指纹一般都是一致的,因此绕过几率非常低。

    但是!这个东西天生有两个缺陷。第一是,无法验证合法性。当然了,你可以用非对称加密来保证合法,但是这个并不靠谱。其次,canvas的冲突概率非常高,远远不是作者宣称的那样,冲突率极低。也许在国外冲突是比较低,因为国外的语言比较多。但是国内公司通常是IT统一装机,无论是软件还是硬件都惊人的一致。我们测试canvas指纹的时候,在携程内部随便找了20多台机器,得出的指纹都完全一样,一丁点差别都没有。因此,有些“高级技巧”其实一点都不实用。

    浏览器指纹技术常用于客户端跟踪及反机器人的场景。核心思路是, 不同浏览器、操作系统、以及操作系统环境,会使得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) \ No newline at end of file diff --git a/Chapter1 - iOS/1.57.md b/Chapter1 - iOS/1.57.md new file mode 100644 index 0000000..cdfb983 --- /dev/null +++ b/Chapter1 - iOS/1.57.md @@ -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); + }]; +``` \ No newline at end of file diff --git a/Chapter1 - iOS/1.58.md b/Chapter1 - iOS/1.58.md new file mode 100644 index 0000000..f80e764 --- /dev/null +++ b/Chapter1 - iOS/1.58.md @@ -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 + + $$ \ No newline at end of file diff --git a/Chapter1 - iOS/1.59.md b/Chapter1 - iOS/1.59.md new file mode 100644 index 0000000..fda8a50 --- /dev/null +++ b/Chapter1 - iOS/1.59.md @@ -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; + } + ``` diff --git a/第一部分 iOS/1.6.md b/Chapter1 - iOS/1.6.md similarity index 97% rename from 第一部分 iOS/1.6.md rename to Chapter1 - iOS/1.6.md index 57500ed..e017909 100644 --- a/第一部分 iOS/1.6.md +++ b/Chapter1 - iOS/1.6.md @@ -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) diff --git a/Chapter1 - iOS/1.60.md b/Chapter1 - iOS/1.60.md new file mode 100644 index 0000000..f450854 --- /dev/null +++ b/Chapter1 - iOS/1.60.md @@ -0,0 +1,688 @@ +# iOS 瘦身之道 + + + +App 的包大小做优化的目的就是为了节省用户流量,提高用户的下载速度,也是为了用户手机节省更多的空间。另外 App Store 官方规定 App 安装包如果超过 150MB,那么不可以使 OTA(over-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 user’s 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. You’ll 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 测量耗时。 + +
    +点击展开 + +```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; +``` +
    + +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 文件 +
    + + 点击展开 + + ``` + s.resource_bundles = { + 'XQ_UI' => ['XQ_UI/Assets/*.xcassets'] + } +
    + ``` +- 主工程执行 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"]` 访问。 + +
    +点击展开 + +```Objective-C +#import "UIImage+XQUIModule.h" +#import + +@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 +``` +
    + + + +#### 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 设置为 YES,Debug 设置为 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 中,明确提出了这个优化的概念,What’s 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) + +// GC:class 有不安全的 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)。所以最后加入到全局并发队列中的任务最后执行,执行的时候就可以拿到结果 \ No newline at end of file diff --git a/Chapter1 - iOS/1.61.md b/Chapter1 - iOS/1.61.md new file mode 100644 index 0000000..3b59b04 --- /dev/null +++ b/Chapter1 - iOS/1.61.md @@ -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 会执行多次。 + + diff --git a/Chapter1 - iOS/1.62.md b/Chapter1 - iOS/1.62.md new file mode 100644 index 0000000..a7b35d5 --- /dev/null +++ b/Chapter1 - iOS/1.62.md @@ -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 搜索 OCLint,clone 源码 +```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 规则的具体还没有深入研究,这里给个例子 + +
    +点击查看示例代码 + +```C +#include "oclint/AbstractASTVisitorRule.h" +#include "oclint/RuleSet.h" + +using namespace std; +using namespace clang; +using namespace oclint; +#include + +class MVVMRule : public AbstractASTVisitorRule +{ +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()); +``` +
    + +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 +``` +- AbstractSourceCodeReaderRule:eachLine 方法,读取每行的代码,如果想编写的规则是需要针对每行的代码内容,则可以继承自该类 +- 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) \ No newline at end of file diff --git a/Chapter1 - iOS/1.63.md b/Chapter1 - iOS/1.63.md new file mode 100644 index 0000000..5f54bb7 --- /dev/null +++ b/Chapter1 - iOS/1.63.md @@ -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) +- [第三方开源]() \ No newline at end of file diff --git a/Chapter1 - iOS/1.64.md b/Chapter1 - iOS/1.64.md new file mode 100644 index 0000000..6688272 --- /dev/null +++ b/Chapter1 - iOS/1.64.md @@ -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 插件化开发 \ No newline at end of file diff --git a/Chapter1 - iOS/1.65.md b/Chapter1 - iOS/1.65.md new file mode 100644 index 0000000..7292eca --- /dev/null +++ b/Chapter1 - iOS/1.65.md @@ -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 +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    5. Item 3
    6. +
    +``` + +转换为 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) + + diff --git a/Chapter1 - iOS/1.66.md b/Chapter1 - iOS/1.66.md new file mode 100644 index 0000000..ede0c65 --- /dev/null +++ b/Chapter1 - iOS/1.66.md @@ -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.2,Android 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. DNS(DNS劫持、运营商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 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) diff --git a/Chapter1 - iOS/1.67.md b/Chapter1 - iOS/1.67.md new file mode 100644 index 0000000..6f76006 --- /dev/null +++ b/Chapter1 - iOS/1.67.md @@ -0,0 +1,11 @@ +# iOS工程编译速度优化 + +要提升编译速度,我们首先要知道有没有提升?那就需要一个量化的标准。下面的命令让 Xcode 告诉你编译耗费多久时间。 + +```shell +defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES +``` + +## ccache + +提高编译速度需要依靠缓存的能力, [ccache](https://ccache.dev) 是一个靠谱的缓存。基于编译器层面。 \ No newline at end of file diff --git a/Chapter1 - iOS/1.68.md b/Chapter1 - iOS/1.68.md new file mode 100644 index 0000000..f567cfc --- /dev/null +++ b/Chapter1 - iOS/1.68.md @@ -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 for the key path "name" from 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 +``` \ No newline at end of file diff --git a/Chapter1 - iOS/1.69.md b/Chapter1 - iOS/1.69.md new file mode 100644 index 0000000..375878a --- /dev/null +++ b/Chapter1 - iOS/1.69.md @@ -0,0 +1,8 @@ +# React Native 总结 + +## 学习方面 + +学过 React.js 之后你再去学习 React Native 会很简单,一些核心的东西理解之后会很简单。比如 React 中的单向数据流、虚拟 Dom、diff 算法、数据变动的批量更新机制、diff 之后的 UI 渲染。 + + +样式布局方面增加了 flexbox,这样子布局在移动端会非常方便,非常简单, \ No newline at end of file diff --git a/第一部分 iOS/1.7.md b/Chapter1 - iOS/1.7.md similarity index 91% rename from 第一部分 iOS/1.7.md rename to Chapter1 - iOS/1.7.md index 49cd954..4087f3a 100644 --- a/第一部分 iOS/1.7.md +++ b/Chapter1 - iOS/1.7.md @@ -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) diff --git a/Chapter1 - iOS/1.70.md b/Chapter1 - iOS/1.70.md new file mode 100644 index 0000000..ffd1606 --- /dev/null +++ b/Chapter1 - iOS/1.70.md @@ -0,0 +1,312 @@ +# 不一样的动态化能力 + +> 对于热修复,对于大多数公司来说都是可望而不可及的技术手段。热修复对于线上问题是杀手锏级别项目。Android 热修复方案很多,典型的属微信的 `Tinker` 莫属,而苹果公司对于安全的要求非常高,所以一些动态调用的能力都会被封杀,这篇文章主要研究下 iOS 端的热修复技术方案。 + + +## 热修复方案 + +- 将下发的原生代码,通过自己实现的代码解析引擎,将代码转换为AST树,然后存储在相关的模型里面,在通过一个上下文注入到runtime里面,当runtime回调到当前函数的时候,上下文从存储的相关模型取出各个参数,然后放到当前堆栈里面去执行相关的逻辑,执行问之后,在返回之前调用的地方,这里跟腾讯的OCS有点像. + +- JSPatch:加加密,多混淆,关键词替换。(其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数,然后现在aop、hook、jspatch 都是离不开这些函数的。解决方案将 动态能力的 API 替换名字:而是本地已经处理好,写到代码的静态变量里面,执行的时候去按照相应的解密方法去解密,然后得到 respondsToSelector:, 再去执行) + +- 几大app中的方案都是自己研发的,不过大同小异,有比较多的是从编译器层面出发,直接把写的代码编译好,然后自己再写解析器解析执行 + +- lua kit:https://github.com/alibaba/LuaViewSDK;https://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 的前后加入逻辑,甚至是完全替换。 + + +## 代码实现 + +
    +FixManager + +```Objective-C +#import +#import "Aspects.h" +#import +#import + +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){ + [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 +``` +
    + + + + + + + + +
    +BugProtector + +```Objective-C +#import + +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 +``` +
    + + +完整的 [Demo](https://github.com/FantasticLBP/BlogDemos/tree/master/HotFix) 可以点此查看链接. + + +## 未完,待续 \ No newline at end of file diff --git a/Chapter1 - iOS/1.71.md b/Chapter1 - iOS/1.71.md new file mode 100644 index 0000000..0f82497 --- /dev/null +++ b/Chapter1 - iOS/1.71.md @@ -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 之旅了。祝愉快 + + diff --git a/Chapter1 - iOS/1.72.md b/Chapter1 - iOS/1.72.md new file mode 100644 index 0000000..79a1eba --- /dev/null +++ b/Chapter1 - iOS/1.72.md @@ -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 方法。 \ No newline at end of file diff --git a/Chapter1 - iOS/1.73.md b/Chapter1 - iOS/1.73.md new file mode 100644 index 0000000..a0cf032 --- /dev/null +++ b/Chapter1 - iOS/1.73.md @@ -0,0 +1,10 @@ + +# Ruby + +> 为了 iOS 工程化开展,自己最近开始了 Ruby 的学习,本篇博文就用来记录 Ruby 的学习心得和体验。 + +- block 最后一行是返回值,不需要指定 return +- ruby 脚本语言在编写好代码后也想像其他 GPL 语言一样断点可以,需要安装依赖和相应的代码调整 + - `gem install pry` + - 文件引入 `require 'pry'` + - 需要 debug 的地方加上 `binding.pry` \ No newline at end of file diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md new file mode 100644 index 0000000..970e1d6 --- /dev/null +++ b/Chapter1 - iOS/1.74.md @@ -0,0 +1,739 @@ +# APM + +> Application Performance Management 应用性能管理对一个应用的持续稳定运行至关重要。所以这篇文章就从一个 iOS App 的性能管理的纬度谈谈如何精确监控以及数据如何上报等技术点 + +App 的性能问题是影响用户体验的重要因素之一。性能问题主要包含:Crash、网络请求错误或者超时、UI 响应速度慢、主线程卡顿、CPU 和内存使用率高、耗电量大等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。 + +本篇文章着重总结了 APM 的原因以及如何收集数据。APM 数据收集后结合数据上报机制,按照一定策略上传数据到服务端。服务端消费这些信息并产出报告。请结合[姊妹篇](./1.80.md), 总结了如何打造一款灵活可配置、功能强大的数据上报组件。 + + + +## 监控项目 + +- 页面渲染时长 +- 主线程卡顿 +- 网络错误+ +- FPS +- 大文件存储 +- CPU +- 内存使用 +- Crash +- 启动时长 + + + + +## 一、卡顿监控 + +卡顿问题,就是在主线程上无法响应用户交互的问题。影响着用户的直接体验,所以针对 App 的卡顿监控是 APM 里面重要的一环。 + +FPS(frame 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 根据工程师写的代码计算好需要现实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPU,GPU 负责图层合成、纹理渲染,随后 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 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图绘制、图形解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 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) + +第一步:通知 Observers,RunLoop 要开始进入 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 循环保活线程,通知 Observers,RunLoop 触发 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); + // + 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); +``` + +第六步:唤醒时通知 Observer,RunLoop 的线程刚刚被唤醒了 +```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 在不同状态下具有不同的值。 +- 启动(Launch):20s +- 恢复(Resume):10s +- 挂起(Suspend):10s +- 退出(Quit):6s +- 后台(Background):3min(在 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 +{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 架构 + +CPU(Central Processing Unit)中央处理器,市场上主流的架构有 ARM(arm64)、Intel(x86)、AMD 等。其中 Intel 使用 CISC(Complex Instruction Set Computer),ARM 使用 RISC(Reduced 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 percentage(CPU使用率,上限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. 获取内存信息 + +// todo:APM 下 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) + diff --git a/Chapter1 - iOS/1.75.md b/Chapter1 - iOS/1.75.md new file mode 100644 index 0000000..3ce45c9 --- /dev/null +++ b/Chapter1 - iOS/1.75.md @@ -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 diff --git a/Chapter1 - iOS/1.76.md b/Chapter1 - iOS/1.76.md new file mode 100644 index 0000000..bb33815 --- /dev/null +++ b/Chapter1 - iOS/1.76.md @@ -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 ( + 16) +1 Foundation 0x00000001c42fe698 0x1c41ea000 + 1132184 ( + 52) +2 Foundation 0x00000001c42091fc 0x1c41ea000 + 127484 ( + 1744) +3 Foundation 0x00000001c4208af4 0x1c41ea000 + 125684 ( + 1232) +4 Foundation 0x00000001c42fecec 0x1c41ea000 + 1133804 ( + 272) +5 libdispatch.dylib 0x00000001c32d8a38 0x1c3279000 + 391736 ( + 24) +6 libdispatch.dylib 0x00000001c32d97d4 0x1c3279000 + 395220 ( + 16) +7 libdispatch.dylib 0x00000001c32b0c34 0x1c3279000 + 228404 ( + 404) +8 libdispatch.dylib 0x00000001c32b0314 0x1c3279000 + 226068 ( + 592) +9 libdispatch.dylib 0x00000001c32bc9d4 0x1c3279000 + 276948 ( + 340) +10 libdispatch.dylib 0x00000001c32bd248 0x1c3279000 + 279112 ( + 116) +11 libsystem_pthread.dylib 0x00000001c34b91b4 0x1c34ad000 + 49588 (_pthread_wqthread + 464) +``` + +为了搞懂这种 Crash,这篇文章就诞生了. + + +## \ No newline at end of file diff --git a/Chapter1 - iOS/1.77.md b/Chapter1 - iOS/1.77.md new file mode 100644 index 0000000..673ec56 --- /dev/null +++ b/Chapter1 - iOS/1.77.md @@ -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`` 的形式管理 + - 涉及到的 SDK:TrinityConfiguration(公共 SDK) + - 改造点: + 注意:改变 SDK 内部修改 xcassets 文件名是无用的,Xcode 编译后查看包内容,结果还是 `Assests.car` 。 + 图片使用方式改变。由之前的 **resources** 方式改为 **`resource_bundles`** 的形式。这样做有2个优点:解决了图片资源打包后造成 `Assets.car` 冲突的问题;`resource_bundles` 还可以解决图片访问速度的优化。 +2. 图片资源重复 + **App工程中有些图片和 理财的 SDK `SdkFinanceHome` 里面的图片资源重名,但是内容却不一致,需要协商改动。 + + - 涉及到的 SDK:SdkFinanceHome (理财业务线 SDK) + - 改造点: + 有2张图片在 SdkFinanceHome SDK 内重复出现2次(形状、大小一致)。App 也存在同名的图片,图形一致、尺寸大小不一致。所以需要**App业务线开发者确认,保留什么图片或者资源重命名。建议图片资源也用 **`resource_bundles`** 的形式管理 + ```ruby + s.resource_bundles = { + 'SdkFinanceHome' => ['***/Assets/*.xcassets'] + } + ``` + +升级 1.8.4 带来的改动点: + +1. 部分 SDK 的头文件引用方式有问题 + - 涉及到的 SDK:SdkFundWax + - 改造点:将 `FCH5AuthRouter.m` 文件中关于 NativeQS 中头文件的引入方式改变下。`#import ` 改为 `#import `,或者改为 `#import "NQSParser.h"` + ```ruby + "dependencies": { + "NativeQS": "~> 1.0" + }, + ``` + + 注意: + 因为打包机目前是源码引入编译成 .a 文件。如果是以 framework 的形式,则必须以依赖描述的方式进行调整。 + + 新版本 cocoapods 中: + + - cocoapods 在 SDK 里面引用别的 SDK 如果 **podspec** 里面存在 **dependencies** 描述,则可以使用 `#import ` 或者 `#import "NativeQS.h"`;如果不存在 **dependencies** 描述,则需要使用 `#import ` + - 在主工程中引用 SDK 的头文件,使用 `#import `、`#import "NativeQS.h"` 都可以 + + 旧版本 cocoapods 中: + + - SDK、App 主工程都可以使用 `#import `、`#import "NativeQS.h"`、`#import ` +2. 部分 SDK 的使用了未在 podspec 文件中声明的依赖,在新版本 cocoapods 下会报错(某些 SDK 由于历史原因造成新版本丢失依赖描述) + - 涉及到的 SDK:CMRCTToast + - 改造点: + 问题基本定位是在于, 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) + diff --git a/Chapter1 - iOS/1.78.md b/Chapter1 - iOS/1.78.md new file mode 100644 index 0000000..fad0ee3 --- /dev/null +++ b/Chapter1 - iOS/1.78.md @@ -0,0 +1,342 @@ +# 上架包预检 + +## 常见被拒原因汇总 + +### iOS + +钱伴 + +敏感词:审核、热更新、开关 +1. App 内包含分发下载分发功能(比如贷款超市、引导用户下载 App 等功能)。 +2. 提供的测试账号无法查看贷款实际利率。 +3. notification 接口返回:showupgrade:true,提示用户升级。 审核期间接口不要返回该字段。 +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 文件的各个字段所确定。明确了做什么事,以及做事情的标准,那就可以很快的开展并落地实现。 + + + +
    + +点击展开代码 + +```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} 参数缺失`) + } + } + } +} +``` + +
    + + +## 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) + + + + diff --git a/Chapter1 - iOS/1.79.md b/Chapter1 - iOS/1.79.md new file mode 100644 index 0000000..623947c --- /dev/null +++ b/Chapter1 - iOS/1.79.md @@ -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 = 1,balance = 100。从账户余额中扣除 50, balacne = 50 + +- 用户 B 比用户 A 刚刚晚一点点时间,读出数据 :version = 1, balance = 100。从账户余额中扣除 20,balance = 80 + +- 用户 A 完成修改操作,需要提交更新,但是在更新之前会先判断数据库中的版本号 version 值和自己读取到的 version 值是否一致,如果一致,则将版本号 version 字段的值加1(version = 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) \ No newline at end of file diff --git a/第一部分 iOS/1.8.md b/Chapter1 - iOS/1.8.md similarity index 100% rename from 第一部分 iOS/1.8.md rename to Chapter1 - iOS/1.8.md diff --git a/第一部分 iOS/1.9.md b/Chapter1 - iOS/1.9.md similarity index 97% rename from 第一部分 iOS/1.9.md rename to Chapter1 - iOS/1.9.md index 7a35b62..f96ff1f 100644 --- a/第一部分 iOS/1.9.md +++ b/Chapter1 - iOS/1.9.md @@ -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) ``` diff --git a/Chapter1 - iOS/chapter1.md b/Chapter1 - iOS/chapter1.md new file mode 100644 index 0000000..560af97 --- /dev/null +++ b/Chapter1 - iOS/chapter1.md @@ -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) \ No newline at end of file diff --git a/第二部分 Web 前端/2.1.md b/Chapter2 - Web FrontEnd/2.1.md similarity index 89% rename from 第二部分 Web 前端/2.1.md rename to Chapter2 - Web FrontEnd/2.1.md index f2c5601..c13a5d1 100644 --- a/第二部分 Web 前端/2.1.md +++ b/Chapter2 - Web FrontEnd/2.1.md @@ -1,5 +1,4 @@ - -# :last-child与:last-of-type +# last-child 与 last-of-type > 同学们遇到过给同一组元素的最后一个元素设置css失效的情况吗?我遇到过,当时使用:last-child居然不起作用,看到名字不科学啊,明明是“最后一个元素”,那为什么设置CSS失效呢?今天来一探究竟吧 @@ -36,7 +35,7 @@ ``` -![效果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 @@ ``` -![](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 @@ ``` -![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092358@2x.png) \ No newline at end of file +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180507-092358@2x.png) \ No newline at end of file diff --git a/第二部分 Web 前端/2.10.md b/Chapter2 - Web FrontEnd/2.10.md similarity index 60% rename from 第二部分 Web 前端/2.10.md rename to Chapter2 - Web FrontEnd/2.10.md index bf5009a..7211706 100644 --- a/第二部分 Web 前端/2.10.md +++ b/Chapter2 - Web FrontEnd/2.10.md @@ -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) diff --git a/第二部分 Web 前端/2.11.md b/Chapter2 - Web FrontEnd/2.11.md similarity index 100% rename from 第二部分 Web 前端/2.11.md rename to Chapter2 - Web FrontEnd/2.11.md diff --git a/第二部分 Web 前端/2.12.md b/Chapter2 - Web FrontEnd/2.12.md similarity index 99% rename from 第二部分 Web 前端/2.12.md rename to Chapter2 - Web FrontEnd/2.12.md index d374b3e..7983fdb 100644 --- a/第二部分 Web 前端/2.12.md +++ b/Chapter2 - Web FrontEnd/2.12.md @@ -1,4 +1,4 @@ -## Promise +# Promise ## 一、基础使用 diff --git a/第二部分 Web 前端/2.13.md b/Chapter2 - Web FrontEnd/2.13.md similarity index 100% rename from 第二部分 Web 前端/2.13.md rename to Chapter2 - Web FrontEnd/2.13.md diff --git a/第二部分 Web 前端/2.14.md b/Chapter2 - Web FrontEnd/2.14.md similarity index 84% rename from 第二部分 Web 前端/2.14.md rename to Chapter2 - Web FrontEnd/2.14.md index 70f86c8..676c8ee 100644 --- a/第二部分 Web 前端/2.14.md +++ b/Chapter2 - Web FrontEnd/2.14.md @@ -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 的时候额外设置了代码具体如下 diff --git a/第二部分 Web 前端/2.15.md b/Chapter2 - Web FrontEnd/2.15.md similarity index 100% rename from 第二部分 Web 前端/2.15.md rename to Chapter2 - Web FrontEnd/2.15.md diff --git a/第二部分 Web 前端/2.16.md b/Chapter2 - Web FrontEnd/2.16.md similarity index 100% rename from 第二部分 Web 前端/2.16.md rename to Chapter2 - Web FrontEnd/2.16.md diff --git a/第二部分 Web 前端/2.17.md b/Chapter2 - Web FrontEnd/2.17.md similarity index 89% rename from 第二部分 Web 前端/2.17.md rename to Chapter2 - Web FrontEnd/2.17.md index 4d5b182..33f7e8e 100644 --- a/第二部分 Web 前端/2.17.md +++ b/Chapter2 - Web FrontEnd/2.17.md @@ -12,7 +12,7 @@ Vue 的组件可以分为全局组件和局部组件 ``` //全局组件 Vue.componetns('todo-item',{ - template:'
  • {{content}}
  • ', + template:'
  • { {content} }
  • ', 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 中 **** 底层的做法是 ** render()**方法, 对 template 中的元素依次遍历创造节点 @@ -317,7 +317,7 @@ Vue.use(VueResource); ```
      -
    • {{item}}
    • +
    • { {item} }
    //新增加的 router-link 需要在路由配置里面添加配置项。 @@ -353,7 +353,7 @@ Vue.use(VueResource); ```
      -
    • {{item}}
    • +
    • { {item} }
    const routes = [ @@ -378,7 +378,7 @@ Vue.use(VueResource); ];
      -
    • {{item}}
    • +
    • { {item} }
    //拿到值 @@ -554,3 +554,23 @@ Vue.use(VueResource); ``` - action 类似于 mutation ,在外面使用的时候用 this.$state.dispath('方法名') + + +- 某些页面可能我们只需要切换数据源和样式模版。所以数据源有可能是变化的,页面的模版也是变化的。 + 这样子我们可能会用到 `v-if、v-else-if...v-else` 来切换页面的模版。但是在模版里面去 `v-for` 循环展示数据源。 + 早期遇到错误,大概意思是说我们循环动态生成的元素,有 key 重复了,最后查找资料得到解决方案。在判断模版的时候需要使用 `` + ``` + + ``` \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.18.md b/Chapter2 - Web FrontEnd/2.18.md new file mode 100644 index 0000000..8fd50f2 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.18.md @@ -0,0 +1,605 @@ +# 反爬技术研究 + +> 对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App,短期内成为你的劲敌。 + + + +## 一、爬虫手段 + +目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本. +有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item,将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID),这个步骤就可以拦截掉一部分的爬虫开发者 + + + +## 二、制定出**Web 端反爬技术方案** + +从这2个角度(网页所见非所得、查接口请求没用)出发,制定了下面的反爬方案。 + + +1. 使用HTTPS 协议 +2. 单位时间内限制掉请求次数过多,则封锁该账号 +3. 前端技术限制 (接下来是核心技术) + +举例:比如需要正确显示的数据为“19950220” + +#### 2.1 原始数据加密 + +1. 先按照自己需求利用相应的规则(数字乱序映射,比如正常的0对应还是0,但是乱序就是 0 <-> 1,1 <-> 9,3 <-> 8,...)制作自定义字体(ttf) +2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 17730220 +3. 对于第一步得到的字符串,依次遍历每个字符,将每个字符根据按照线性变换(y=kx+b)。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”,那么线性变换的 k 为 7,b 为 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+b,k和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 = ""; + } + else if (rawNumber == 2) { + mapData = ""; + } + else if (rawNumber == 3) { + mapData = ""; + } + else if (rawNumber == 4) { + mapData = ""; + } + else if (rawNumber == 5) { + mapData = ""; + } + else if (rawNumber == 6) { + mapData = ""; + } + else if (rawNumber == 7) { + mapData = ""; + } + else if (rawNumber == 8) { + mapData = ""; + } + else if (rawNumber == 9) { + mapData = ""; + } + else if (rawNumber == 0) { + mapData = ""; + } + } + 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) + +
    + + +前面的 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 +``` + diff --git a/第二部分 Web 前端/2.19.md b/Chapter2 - Web FrontEnd/2.19.md similarity index 95% rename from 第二部分 Web 前端/2.19.md rename to Chapter2 - Web FrontEnd/2.19.md index 5d6de55..4b24f28 100644 --- a/第二部分 Web 前端/2.19.md +++ b/Chapter2 - Web FrontEnd/2.19.md @@ -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) diff --git a/第二部分 Web 前端/2.2.md b/Chapter2 - Web FrontEnd/2.2.md similarity index 99% rename from 第二部分 Web 前端/2.2.md rename to Chapter2 - Web FrontEnd/2.2.md index b35d7aa..bd11743 100644 --- a/第二部分 Web 前端/2.2.md +++ b/Chapter2 - Web FrontEnd/2.2.md @@ -1,4 +1,4 @@ - +# 正则表达式 * \d :匹配一个数字 * \w : 匹配任意一个字母或数字 diff --git a/第二部分 Web 前端/2.20.md b/Chapter2 - Web FrontEnd/2.20.md similarity index 100% rename from 第二部分 Web 前端/2.20.md rename to Chapter2 - Web FrontEnd/2.20.md diff --git a/第二部分 Web 前端/2.21.md b/Chapter2 - Web FrontEnd/2.21.md similarity index 100% rename from 第二部分 Web 前端/2.21.md rename to Chapter2 - Web FrontEnd/2.21.md diff --git a/第二部分 Web 前端/2.22.md b/Chapter2 - Web FrontEnd/2.22.md similarity index 100% rename from 第二部分 Web 前端/2.22.md rename to Chapter2 - Web FrontEnd/2.22.md diff --git a/第二部分 Web 前端/2.23.md b/Chapter2 - Web FrontEnd/2.23.md similarity index 100% rename from 第二部分 Web 前端/2.23.md rename to Chapter2 - Web FrontEnd/2.23.md diff --git a/第二部分 Web 前端/2.24.md b/Chapter2 - Web FrontEnd/2.24.md similarity index 72% rename from 第二部分 Web 前端/2.24.md rename to Chapter2 - Web FrontEnd/2.24.md index 5f8ea33..8cb0c8d 100644 --- a/第二部分 Web 前端/2.24.md +++ b/Chapter2 - Web FrontEnd/2.24.md @@ -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) #### DOM输出 @@ -55,7 +56,7 @@ var ul = document.getElementsByTagName("ul"); console.dirxml(ul); //树形输出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) - Watch:Watch表达式 - 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,但不管怎样,你都不应该直接对生产代码进行调试。 \ No newline at end of file +2.尽可能使用 source map。有时生产代码不能使用source map,但不管怎样,你都不应该直接对生产代码进行调试。 + + +### 异常调试 + +``` + +``` + +代码会报错。为了诸多原因,我们希望提前解决,所以我们希望准确知道代码在哪里有问题。也就是需要调试,希望 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.,那么之前的设置是不能定位到异常的位置。 + +``` + +``` + ![不能定位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) \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.25.md b/Chapter2 - Web FrontEnd/2.25.md new file mode 100644 index 0000000..ded103d --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.25.md @@ -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 添加了一个过渡动画,动画指定了 transform 动画,触发时机为当鼠标移上去的时候。因此当鼠标移入的时候元素的 transform 属性发生变化,那么这个时候触发了 transition 动画,当鼠标移除的时候也产生了 transform 的变化,因此还是会触发 transition,产生动画。 +上面设置了3个 transform 只有最后一个生效 +因此 transition 产生动画的条件是设置的 property 发生变化,这种动画的特点是需要一个驱动力去触发。因此就存在一些缺点: +1. 需要事件触发,没法在网页加载时自动发生 +2. 是一次性的,不能重复发生,除非再次触发 +3. 只可以定义开始状态和结束状态,不能定义中间状态,因此没有丰富的动画空间 +4. 一条 transition 规则,只能定义一个属性的变化,不能涉及多个属性 + +语法:**transition:property 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; + } +} +
    +``` + +**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 规则))和 native(iOS)端的 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 端也一样,开启绘图上下文、拿到上下对象、绘制路径、上色、关闭上下文。 + +所以其他具体的例子也就不举了,本质上大前端的所有动画干的事情都一样,所以我们需要处理好时间和位置的关系。 diff --git a/Chapter2 - Web FrontEnd/2.26.md b/Chapter2 - Web FrontEnd/2.26.md new file mode 100644 index 0000000..e1595ab --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.26.md @@ -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/ \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.27.md b/Chapter2 - Web FrontEnd/2.27.md new file mode 100644 index 0000000..d5ce995 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.27.md @@ -0,0 +1,48 @@ +# 浏览器不通窗口之间的通信 + +## localStorage + +“同源”情况下,一个窗口更新 localStorage,另一个窗口监听 window 对象的 “storage” 事件,来实现通信 + +``` +window.localStorage.setItem('name','杭城小刘'); +window.addEventListener('storage', function (e) { + console.log(e); + console.log(e.newValue); +}) +``` + +## WebSocket + +所有的 WebSocket 都监听同一个服务器地址,利用 `send` 发消息,利用 `onmessage` 获取消息的变化,不仅能窗口,还可以跨浏览器通信,兼容性最佳。只是需要消耗点服务器资源 +``` +var ws = new WebSockte('ws://192.168.0.0.1:8080/'); +ws.onopen = function (event) { + ws.send({now: new Date()}); +} + +ws.onmessage = function (event) { + console.log(event) +} +``` + + +## postMessage + +借助 iframe 或 window.open +``` +otherWindow.postMessage(message targetOrigin, [transfer]) +``` + +## cookie + setInterval + +在页面 A 设置一个使用 setInterval 定时器不断刷新,检查 Cookies 的值是否变化,如果变化就是进行刷新操作 +由于 Cookies 在同一个域下可读,所以这样做的缺点是浪费资源 + +## SharedWorker + +HTML5 中的 Web Worker 可以分为2种不同线程类型,一种是专用线程 Dedicated Worker,一种是共享线程 Shared Worker +- Dedicated Worker 直接使用 new Worker() 创建,这种 webWorker 是当前页面专有的 +- SharedWorker 可以被多个 window、标签页、iframe 共同使用,但必须保证这些标签页都是同源的 + + diff --git a/Chapter2 - Web FrontEnd/2.28.md b/Chapter2 - Web FrontEnd/2.28.md new file mode 100644 index 0000000..10d72c4 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.28.md @@ -0,0 +1,9 @@ +# 神器Puppeteer + +> 为什么要说神器呢?因为在我眼里它就是神器,即使有些场景它没办法完美解决,但是它已经很强大了,足以满足很多开发或者需求场景。比如网页脚本注入、网页截图、爬虫、自动化测试等等功能 + +## 介绍 + +[官方文档](https://pptr.dev) + + diff --git a/Chapter2 - Web FrontEnd/2.29.md b/Chapter2 - Web FrontEnd/2.29.md new file mode 100644 index 0000000..d10adcd --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.29.md @@ -0,0 +1,839 @@ +# Vue 小结 + +> 本次串讲的主要目的在于给我们移动端的同学揭秘下目前前端开发的现状,和一些典型框架或者说是库的产生背景、以及设计思想和解决了什么样的问题。以 **Vue.js** 为例。此次讲解围绕以下几个方面展开: +> - [MV* 框架模式](#1) +> - [Vue.js 的概述](#2) +> - [Vue MVVM 的实现](#3) +> - [Vue 与 React 的对比](#4) +> - [有 Vue 基础如何快速上手 Weex](#5) + + +## MV* 框架模式
    + +### 历史 + +最早期的 Web 开发是洪荒时代,开发者可能写着类似以下的代码。检查用户的输入合法性,然后提交用户的表单字段到达服务器。服务器再校验一遍用户的合法性 +```HTML + + + + + 洪荒时代 + + + + + + + + + + + + + +``` + +```php +$username = addslashes($_REQUEST['username']); +$password = md5($_REQUEST['password']); +//数据表 + $table = "user"; + +//3.得到连接对象 +$PdoMySQL = new PdoMySQL(); +if ($action == "login") { + $salt = "CRO"; + $identidier = md5($salt.md5($username.$salt)); + $token = md5(uniqid(rand(),true)); + $time = time()+60*60*24*7; + $currentime = time(); + $allrow = $PdoMySQL->find($table,"username='{$username}' and password='{$password}'"); + $PdoMySQL->update(["time"=>$time,"identifier"=>$identidier],$table,"username='{$username}' and password='{$password}'"); + + $autoRows = $PdoMySQL->find($table,"username='".$username."' and identifier='".$userid."'"); + + if(count($autoRows) == 1){ + if($currentime < $autoRows[0]["time"]){ + setcookie('auth',base64_encode($autoRows[0]["id"])); + // 跳转到主页 + }else{ + // 给出用户信息失败的提示 alert + } + } +} +``` + + +再到后来 Javascript 技术的发展越来越完善,网页开发有了更复杂的 JS 动画、CSS的特性也越来越强,让洪荒时代的 web 开发步入到“火药文明时代”。一些大型应用的场景,页面的数据状态非常多,传统的页面开发方式有了一些问题。 +1. 比如页面一个报错如果是服务端渲染,那么 error 信息直接显示到页面上。对于用户而言这些 error 信息很懵逼,体验很不好 +2. error 信息里面有你的服务端信息,比如什么语言,什么框架,什么版本,什么引擎、什么服务器,这些东西对于不怀好心的 Eve 就可以利用现有漏洞去攻击服务器 +3. 开发维护方式很不友好。假如你的页面有报错信息,你甚至需要前端开发者和服务端开发者一起去排查问题。开发方式就是前端开发者写模版代码,写好之后将代码交给服务端开发者,服务端开发者根据业务,去操作数据库执行 SQL ,再通过类似于 JSP、PHP 这种传统的技术渲染页面。开发效率极低。 + +后来诞生了 ajax 技术。通过 ajax 提高一个较好的体验( 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。Ajax 在浏览器与 Web 服务器之间使用异步数据传输(HTTP 请求),这样就可使网页从服务器请求少量的信息,而不是整个页面)。有了 ajax 赋能前端开发采用了前后端分离的方案,服务端、前端各司其职。前后端开发者通过接口通信,前端开发者专心做提高用户体验的前端事情,比如写酷炫的动画。传统的服务端渲染的路子走不通了。在此背景下催生了 **REST api** 。前端开发人员高兴坏了,开发者有了能力去开发大型应用。 + + +再到后来旧版本、性能低、不主动拥抱变化的浏览器逐渐淘汰,体验不好,用户自然不愿意去用,那么就要淘汰。移动智能设备的诞生让传统的 PC 页面开始在移动端进行尝试,发现效果还可以。当用户也越来越挑剔、用户体验的要求也越来越高。那么传统的开发方式也不能满足现在的需求了。用户多了,业务复杂了,那么 MVC 也满足不了现在开发者的要求,于是 MVVM 诞生了。当然前端也在搞工程化。 + + +应用越复杂,现有状况就是数据状态分散在 model 和 view 中。假如Jquery时代经常将数据隐藏在form表单中只不过是隐藏的。比如 `` 点击按钮更新用户信息的时候经常需要将隐藏的数据也提交掉。在此背景下诞生了最早一批的框架,代表有 Backbone、Ember。 + + +### MV* 说明(MVC、MVP、MVVM...) + +1. 先不讲 MVC 是什么,先谈谈软件设计的一些原则和理念。 + - 可靠性:应用的功能可以正常使用 + - 健壮性:在用户非正常使用的时候,应用也可以正常反应,不要奔溃 + - 效率性:启动时间、响应时间、效率等在用户可以容忍范围之内 + +以上3点是表象层的东西,大多数开发者或者团队都会注意。除了这三点,还有一些东西是需要在工程层面需要注意的方面。 + + - 可拓展性:软件不是一次性产品,需要不断的迭代更新 + - 容易理解:代码易读、规范 + - 可测试性:代码能够方便的编写单元测试和集成测试 + - 可复用性:可复用,不需要一次次编写轮子 + +于是,软件设计领域有了几个通用设计原则帮助我们实现这些目标:单一功能原则、聚合复用原则、接口隔离原则、依赖倒置原则... + +基于这些设计目标和理念又有了设计模式:MVC、MVVM 就属于这个范畴。 + + +2. MV* + +![MVC](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-01-29-MVC.png) +- MVC:Model(模型) + View(视图) + Controller(控制器),主要目的在于分层,各司其职。 View 通过 Controller 来和 Model 联系。Controller 用来管理 View 和 Model。View 将事件传递给 Controller,Controller 完成业务逻辑后要求 Model 改变,Model 将新的数据发送到 View,用户得到反馈。 + +![MVP](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2019-01-29-MVP.png) +- MVP:从 MVC 演变而来,都通过 Presenter/Controller 负责逻辑处理,View 负责界面展示,Model 负责数据。在 MVP 中主要逻辑在 Presenter 中。View 与 Model 不发生联系,都通过 Presenter 传递。View 层非常薄,不部署任何业务逻辑,没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。 + +![MVVM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-01-29-MVVM.png) +- MVVM:将 MVP 中的 中,Presenter 变成了 ViewModel,View 的变动会自动同步到 ViewModel,ViewModel 的变化也会同步到 View 上,这种同步的实现是对 ViewModel 中的属性实现了 Observer,当对属性存取会触发 setter 和 getter,都会触发对应的操作。 + + + + +## Vue.js
    + +对于 Vue.js 来说不只是技术的革新也是开发方式的革新。前端框架和移动端框架的差异:前端框架更像是革命性的革新,连开发方式都是天翻地覆的变化。前端里面 MVVM 的思想每个库基本都有实现;移动端的话比较少,几个大厂才有实现方式,但是使用起来感觉并不是很美好。 +举个例子:iOS 端的 ReactiveCocoa 使用起来高学习门槛、易出错、调试困难、风格不统一等被诟病。后来美团自研了 EasyReact。它的诞生是为了解决 iOS 工程实现 MVVM 架构但没有对应的框架支撑,而导致的风格不统一、可维护性差、开发效率低等多种问题。而 MVVM 中最重要的一个功能就是绑定,EasyReact 就是为了让绑定和响应式的代码变得 Easy 起来。 + +### 什么是 Vue.js + +> Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。 + +在我看来 Vue.js 的核心思想就是「数据驱动、组件化开发、虚拟Dom」。当然结合它的脚手架让你开发一个复杂且良好的大型应用变得很容易。下面看一个 Demo 来说明下 Vue.js 的强大威力。 + +```HTML + + + Vue + + + + +
    + +

    { {username} }

    +
      +
    • { {el.msg} }
    • +
    +
    + + + +``` + +在上面的代码中就声明了一个 MVVM 框架的 Web 应用,怎么体现?你可以在打开 Chrome 的调试界面,快捷键为 `Command + Option + i`,你可以在 console 中输入以下指令,可以看到界面会自动更新 + +```Javascript +vm.$data.username = '刘斌鹏' +vm.username = '刘斌鹏' +vm._data.username = '刘斌鹏' + +vm.$data.hobby.push({msg: '探索本质'}) +vm.$data.hobby.pop() +vm.$data.hobby.shift() +``` + +为什么呢?底层实现原理是通过 `new Vue({})` 声明了一个 MVVM 对象,绑定的 View 通过 el 获取到,数据就是原生的 Javascript 对象,这个 ViewModel 将 View 和 Model 绑定在一起, View 和 Model 不直接联系,但是 `v-model="username"` 是个什么鬼? `v-model` 是 Vue.js 中的一个指令,底层实现就是 Vue.js 将该 input 的值和 Model 中的 username 进行了绑定,代码如下 +```HTML + +``` + +我们通过 ViewModel 操纵的是 Model 当 Model 中的数据改变,假如通过 `vm.$data.username` 就会触发属性的 getter,如果通过 `vm.$data.username = '刘斌鹏'` 访问的就是属性的 setter,Vue 观察到属性变化会自动操作 View 的响应式变化。 + + +### 如何学习(前置条件) + +- npm + npm其实是Node.js的包管理工具(package manager)。开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到 npm 官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。更重要的是,如果我们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠我们自己手动管理,肯定既麻烦又容易出错。 + +- AMD、CommonJS、CMD 等规范 + 1. CommonJS 规范 + 由于为了编写大型应用程序,代码不可能编写在一个文件里,所以代码(函数、变量)分散在多个文件里面,每个应用程序都有相应的解决方案,在 Node 中就是“模块”。模块的好处也是不言而喻的,当你编写好某个功能拓展的时候可以很方便的集成到其他的模块中去引用。那么 Node 如何实现模块?由于 Javascript 是函数式编程语言,所以可以利用闭包实现。将我们的代码用闭包实现起来就可以实现将“变量”只在当前代码内有效,外部无法访问,实现了模块的隔离。所以我们可以将需要暴露出去的东西暴露给外部,这样子就可以组织大型应用程序的开发 + + 模拟 CommonJS 的实现 + ```Javascript + // 准备module对象: + var module = { + id: 'hello', + exports: {} + }; + var load = function (module) { + // 读取的hello.js代码: + function greet(name) { + console.log('Hello, ' + name + '!'); + } + + module.exports = greet; + // hello.js代码结束 + return module.exports; + }; + var exported = load(module); + // 保存module: + save(module, exported); + ``` + 上述代码就可以实现将所需要的东西实现模块。CommonJS 规范使用步骤:1. 编写代码逻辑,通过 `module.export = 变量;` 暴露给外部;2. 调用者通过 `let 变量名 = require('模块名')` 来导入所需要的模块,用一个变量去承接,然后访问属性和方法 + 2.由于 CommonJS 中的规范针对于 Node 很适合,因为代码文件是放在服务端磁盘,所以是同步的,读取速度很快,代码同步执行没问题。但是要在浏览器端使用这套规范显然是行不通的。为什么?看看下面代码有什么问题? + ```Javascript + let Hello = require('./Hello'); + Hello.sayHi() + ``` + 用户访问页面后卡死了?因为浏览器的环境下代码资源都需要通过网络获取,所以会比较慢,如果是同步用户访问的话基本上不会去第二次访问你的网站了。在此背景下产生了针对浏览器环境下的模块问题的 AMD 规范(Asynchronous Module Definition),想一想如果是你的话如何设计?采用异步加载的方式,模块的加载不影响后续代码的执行,如果遇到的代码是依赖于模块,那么这些代码都会被放到一个回调函数中,等模块加载完毕才会去执行回调函数里面的内容。AMD 也采用 `require()` 语句,不同于 CommonJS 它要求2个参数。 + ```Javascript + reuqire([module], callback) + ``` + 说明:第一个参数是一个数组,里面是要加载模块;第二个参数 callback 是加载成功的回调函数。比如 + ```Javascript + require(['./Hello'], () => { + Hello.sayHi() + }) + ``` + +- Webpack + 查看以前的文章 [Webpack](./2.19.md)、[webpack-dev-server](./2.13.md) + +- ES6 + 几个概念:ES、JS、CoffeeScript、TypeScript + ES(ECMAScript):标准 + JS:浏览器对其的实现 + CoffeeScript:可以编译为 Javascript,抛弃 JS 中一些不好的设计 + TypeScript 是现今对 JavaScript 的改进中,唯一完全兼容并作为它的超集存在的解决方案 + +- Flexbox + 传统布局解决方案比如盒模型在实现一些效果的时候不是很方便,所以 W3C 在2009年提出了 Flex 布局系统。 + [Flex参考资料](http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html) + +- html、CSS + [MDN](https://developer.mozilla.org/zh-CN/search?q=Session&topic=apps&topic=html&topic=css&topic=js&topic=api&topic=canvas&topic=svg&topic=webgl&topic=mobile&topic=webdev&topic=http&topic=webext) + + +### 如何学习、进阶 + +#### 学习 + - 看着 [Vue官方文档](https://cn.vuejs.org/v2/guide/) 边看边写,因为在你 coding 的时候是拿着键盘写代码的,也需要感觉,所以平时多敲代码,边思考 + - 对于没有接触过 ES6 和 Webpack 的童鞋来说,不建议直接用官方的脚手架 **vue-cli** 构件项目。所以先花点时间去学习下 ES6 的威力和 Webpack 解决了什么样的问题和它的简单用法 + - 了解下 npm 的概念和解决了什么样的问题 + - 一些 CSS 的知识 + - 等适应了 Vue-cli 和工程构建方式以及代码组织方式后可以看看 Vue-Router、Vuex + - Vue-Router、Vuex 应用到工程项目中去,做一个 TodoList 项目 + - 项目结束复盘、review 下 + - [项目 Vue 小结](./2.17.md) +#### 进阶 + - [Vue 代码风格指南](https://cn.vuejs.org/v2/style-guide/#避免-v-if-和-v-for-用在一起-必要) + - ES6 吃透(万变不离其宗,不要一昧追求新技术,掌握本质核心) + - 封装高阶组件(slot 等技术点) + - 设计优秀良好的组件(比如用 TS 书写代码类型更为安全) + - 封装公司或者业务线或者产品为核心点的组件库 + - 关注代码实现原理 + - 关注前端的技术社区:[segmentfault](https://segmentfault.com)... + - 思考 Vue 框架设计的思想。类比其他框架甚至是大前端如何实现或者有没有类似的问题 + - 尝试找到应用的性能症结所在,分析问题,给出解决方案并优化 + - 参加行业的大会。VueConf、ReactConf + + +​ +## MVVM 实现原理
    + +### 几种实现双向绑定的实现原理。 + +看看下面的代码 + +```Javascript +var Book = {}; + var name = ''; + Object.defineProperty(Book, 'name', { + set: function (value) { + name = value; + console.log('本书名称叫做:' + value); + }, + get: function () { + return '<' + name + '>'; + } + }); +Book.name = 'Vue.js 权威指北' +console.log(`我买了本书叫做${Book.name}`); +``` +![Object.defineProperty](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-01-28-Object.defineProperty.png) + +发现打印出来的东西和 Vue console 中输出基本一直,所以猜想 Vue 的实现也是依赖 `Object.defineProperty` + +目前主流的框架基本都实现了单向数据绑定,在我看来双向数据绑定无非就是在单项数据绑定的基础上实现了给可输入元素(input、textarea)添加了 change(input)事件来动态修改 Model 和 View,所以我们的注意力不需要注意双向还是单向数据绑定。Vue 支持单双向数据绑定。 + +实现数据绑定的做法大致有如下几种方式: +- 发布者-订阅者模式:Backbone.js。不去讨论 +- 脏值检查:Angular.js。基本通过 DOM 事件、比如用户输入、按钮点击、XHR 响应事件、浏览器 Location 变更事件、Timer、apply 等 +- 数据劫持:Vue.js。通过数据劫持结合发布者-订阅者模式实现。`Object.defineProperty()` 拦截属性的 setter 和 getter。在数据变动的时候发布消息给订阅者、触发相应的监听回调。 + +思路整理: + +- 实现一个属性监听器 Observer,能够对数据对象的所有属性进行监听,如果有变动则将最新的值通知给订阅者 +- 实现一个指令解析 Compiler,对每个元素节点进行扫描和解析,根据指令模版替换数据,以及绑定相应的更新函数 +- 实现一个 Wacther,作为连接 Observer 和 Compiler 的桥梁,能够订阅并观察到每个属性的变化通知,执行指令绑定的相应回调,从而更新视图 +- MVVM 入口函数,整合Observer、Compiler、Wacther + +![MVVM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-01-28-Vue-MVVM.png) + +看几个属性:Object.defineProperty 中的 writable 和 configurable 和 enumerable 的理解 +configurable 如果为 false 则不可以修改, 不可以删除。writable 如果设置为 false 则不可以采用数据运算符进行赋值 +做个实验看看特殊情况。如果 writable 为 true 的时候, configurable 为 false 结果如何? + +```Javascript +var o = {}; // 创建一个新对象 +Object.defineProperty(o, "a", { + value : "original", + writable : false, // 这个地方为 false + enumerable : true, + configurable : true +}); +o.a = 'LBP'; +console.log(o.a) // "original" 此时候, 是更改不了 a 的. + +var o = {}; // 创建一个新对象 +Object.defineProperty(o, "a", { + value : "original", + writable : true, + enumerable : true, + configurable : false //这里为false +}); +o.a = "LBP"; +console.log(o.a) //LBP.此时候, a 进行了改变 + +delete o.a // 返回 false +``` + +结论:onfigurable 控制是否可以删除; writable 控制是否可以修改(赋值); enumerable 控制是否可以枚举 + +1. 实现 Observer +可以利用 Obeject.defineProperty() 来监听属性变动,将需要 Observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter +给这个对象的某个值赋值就会触发setter,那么就能监听到了数据变化。 + +```javascript +var data = {name: '杭城小刘'}; +observe(data); +data.name = 'LBP'; + +function observe(data) { + if (!data || typeof data !== 'object') { + return; + } + // 取出所有属性遍历 + Object.keys(data).forEach(function(key) { + defineReactive(data, key, data[key]); + }); +}; + +function defineReactive(data, key, val) { + observe(val); // 监听子属性 + Object.defineProperty(data, key, { + enumerable: true, // 可枚举 + configurable: false, //不能再delete + get: function() { + return val; + }, + set: function(newVal) { + console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal); + val = newVal; + } + }); +} +``` +这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发 notify,再调用订阅者的 update 方法,代码改善之后是这样: +```javascript +... +function defineReactive(data, key, val) { + var dep = new Dep(); + observe(val); // 监听子属性 + + Object.defineProperty(data, key, { + ... + set: function(newVal) { + if (val === newVal) return; + console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal); + val = newVal; + dep.notify(); // 通知所有订阅者 + } + }); +} + +function Dep() { + this.subs = []; +} +Dep.prototype = { + addSub: function(sub) { + this.subs.push(sub); + }, + notify: function() { + this.subs.forEach(function(sub) { + sub.update(); + }); + } +}; +``` +那么问题来了,谁是订阅者?怎么往订阅器添加订阅者? +没错,上面的思路整理中我们已经明确订阅者应该是 Watcher, 而且 `var dep = new Dep()` 是在 `defineReactive` 方法内部定义的,所以想通过 `dep` 添加订阅者,就必须要在闭包内操作,所以我们可以在 `getter`里面动手脚: + +```javascript +// Observer.js +... +Object.defineProperty(data, key, { + get: function() { + // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除 + Dep.target && dep.addDep(Dep.target); + return val; + } + ... +}); + +// Watcher.js +Watcher.prototype = { + get: function(key) { + Dep.target = this; + this.value = data[key]; // 这里会触发属性的getter,从而添加订阅者 + Dep.target = null; + } +} +``` +这里已经实现了一个 Observer 了,已经具备了监听数据和数据变化通知订阅者的功能 + +2. 实现 Compile + +compile 主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示: +![MVVM-Compile](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-01-28-Vue-MVVM-2.png) + +因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将根节点 `el` 转换成文档碎片 `fragment` 进行解析编译操作,解析完成,再将 `fragment` 添加回原来的真实dom节点中 +```javascript +function Compile(el) { + this.$el = this.isElementNode(el) ? el : document.querySelector(el); + if (this.$el) { + this.$fragment = this.node2Fragment(this.$el); + this.init(); + this.$el.appendChild(this.$fragment); + } +} +Compile.prototype = { + init: function() { this.compileElement(this.$fragment); }, + node2Fragment: function(el) { + var fragment = document.createDocumentFragment(), child; + // 将原生节点拷贝到fragment + while (child = el.firstChild) { + fragment.appendChild(child); + } + return fragment; + } +}; +``` + +compileElement 方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明: + +```javascript +Compile.prototype = { + ... + compileElement: function(el) { + var childNodes = el.childNodes, me = this; + [].slice.call(childNodes).forEach(function(node) { + var text = node.textContent; + var reg = /\{\{(.*)\}\}/; // 表达式文本 + // 按元素节点方式编译 + if (me.isElementNode(node)) { + me.compile(node); + } else if (me.isTextNode(node) && reg.test(text)) { + me.compileText(node, RegExp.$1); + } + // 遍历编译子节点 + if (node.childNodes && node.childNodes.length) { + me.compileElement(node); + } + }); + }, + + compile: function(node) { + var nodeAttrs = node.attributes, me = this; + [].slice.call(nodeAttrs).forEach(function(attr) { + // 规定:指令以 v-xxx 命名 + // 如 中指令为 v-text + var attrName = attr.name; // v-text + if (me.isDirective(attrName)) { + var exp = attr.value; // content + var dir = attrName.substring(2); // text + if (me.isEventDirective(dir)) { + // 事件指令, 如 v-on:click + compileUtil.eventHandler(node, me.$vm, exp, dir); + } else { + // 普通指令 + compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); + } + } + }); + } +}; + +// 指令处理集合 +var compileUtil = { + text: function(node, vm, exp) { + this.bind(node, vm, exp, 'text'); + }, + ... + bind: function(node, vm, exp, dir) { + var updaterFn = updater[dir + 'Updater']; + // 第一次初始化视图 + updaterFn && updaterFn(node, vm[exp]); + // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher + new Watcher(vm, exp, function(value, oldValue) { + // 一旦属性值有变化,会收到通知执行此更新函数,更新视图 + updaterFn && updaterFn(node, value, oldValue); + }); + } +}; + +// 更新函数 +var updater = { + textUpdater: function(node, value) { + node.textContent = typeof value == 'undefined' ? '' : value; + } + ... +}; +``` +这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{ {} }表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如 ` 视图更新;视图交互变化(input) -> 数据 Model 变更的双向绑定效果。 + +一个简单的 MVVM 构造器是这样子: +```javascript +function MVVM(options) { + this.$options = options; + var data = this._data = this.$options.data; + observe(data, this); + this.$compile = new Compile(options.el || document.body, this) +} +``` + +但是这里有个问题,从代码中可看出监听的数据对象是 options.data,每次需要更新视图,则必须通过 `var vm = new MVVM({data:{name: '杭城小刘'} }); vm._data.name = 'LBP'; ` 这样的方式来改变数据。 + +显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的: + `var vm = new MVVM({data: {name: '杭城小刘'} }); vm.name = 'LBP';` + +所以这里需要给 MVVM 实例添加一个属性代理的方法,使访问 vm 的属性代理为访问 vm._data 的属性,改造后的代码如下: + +```javascript +function MVVM(options) { + this.$options = options; + var data = this._data = this.$options.data, me = this; + // 属性代理,实现 vm.xxx -> vm._data.xxx + Object.keys(data).forEach(function(key) { + me._proxy(key); + }); + observe(data, this); + this.$compile = new Compile(options.el || document.body, this) +} + +MVVM.prototype = { + _proxy: function(key) { + var me = this; + Object.defineProperty(me, key, { + configurable: false, + enumerable: true, + get: function proxyGetter() { + return me._data[key]; + }, + set: function proxySetter(newVal) { + me._data[key] = newVal; + } + }); + } +}; + +``` +这里主要还是利用了 `Object.defineProperty()` 这个方法来劫持了 vm 实例对象的属性的读写权,使读写 vm 实例的属性转成读写了 `vm._data` 的属性值,达到鱼目混珠的效果 + + +1. 什么是单向绑定和双向绑定? + 单向绑定:将 Model 绑定到 View 上。当我们通过接口或者事件操作 Model 的改变的时候那么 View 的改变会自动触发,View 自动刷新改变。 +2. 双向绑定:将 Model 绑定到 View 上,通过也将 View 绑定到 Model 上。这样 View 的改变会触发 Model 的改变,Model 的改变也会自动触发 View 的自动更新。 +3. Vue 中如何实现单项数据绑定? + - 通过插值表达式。通过 `{ {data} }` 的形式将数据 Model 中的某个属性绑定到 Dom 节点上 + - 通过 v-bind 指令。通过 `v-bind:class="hasError"` 将某个 Model 的属性绑定到对应的属性上。这样 Vue 在识别到 v-bind 指令的时候会自动将属性跟 Model 绑定起来,这样就可以通过 ViewModel 操作 Model 来动态的更新 View 层。 +4. Vue 中实现双向绑定 + Vue 中通过 `v-model` 实现双向绑定。可以实现 View 到 Model 的双向绑定。View 变动了 Model 会跟着变, Model 变了 View 会自动更新。 + +## Vue 与 React 的对比
    + +先看看以下代码,针对同一个字符串反转的功能,2个库如何实现 +```vue +
    +

    { { message } }

    + +
    + +new Vue({ + el: '#app', + data: { + message: 'Hello Vue.js! + }, + methods: { + reverseMessage: function () { + this.message = this.message.split('').reverse().join(''); + } + } +}); +``` + +```react + class App extends React.Component { + constructor(props) { + super(props); + this.state = { + message: 'Hello React.js!' + }; + } + reverseMessage() { + this.setState({ + message: this.state.message.split('').reverse().join('') + }); + } + render() { + return ( +
    +

    {this.state.message}

    + +
    + ) + } +} +ReactDOM.render(App, document.getElementById('app')); +``` + + +相似之处: +- 都有非常多的 star,开发者非常拥护 +- 都使用 Virtua DOM +- 提供了响应式和组件化的视图组件 +- 将注意力放在核心实现,将其他功能比如路由、全局状态管理交给相关的库 + + +差别: +- React 严格上只针对 MVC 的 view 层,Vue 则是 MVVM 模式 +- 数据绑定: Vue 实现了数据的双向绑定,React 数据流动是单向的 + 单向数据流是指数据的流向只能由父组件通过props将数据传递给子组件,不能由子组件向父组件传递数据,要想实现数据的双向绑定,只能由子组件接收父组件props传过来的方法去改变父组件的数据,而不是直接将子组件的数据传递给父组件。 + 单向数据量组件props是父级往下传递,你不能向上去修改父组件的数据,并且也不能在自身组件中修改props的值。React不算mvvm,虽然可以实现双向绑定,在React中实现双向绑定通过state属性,但如果将state绑定到视图中时,直接修改state属性是不可的,需要通过调用setState去触发更新视图,反过来视图如果要更新也需要监听视图变化 然后调用setState去同步state状态。标准MVVM应该属于给视图绑定数据后,操作数据即是更新视图 +- virtual DOM 不一样,Vue 会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树.而对于 React 而言,每当应用的状态被改变时,全部组件都会重新渲染,所以 React 中会需要 shouldComponentUpdate 这个生命周期函数方法来进行控制 +- 组件写法不一样, React推荐的做法是 JSX + inline style, 也就是把 HTML 和 CSS 全都写进 JavaScript 中,即 'all in js'; Vue 推荐的做法是 webpack+vue-loader 的单文件组件格式,即 html,css,JS 写在同一个文件 + +- 代码书写方式 + 使用 Vue 你可以很方便的将现有的工程迁移或者接入 Vue,因为工程现有的 HTML 就是 Vue 中的视图模版,你只需要做一些 Webpack 配置化的东西,代码改动成本低,后期不用 Vue 了你更换框架的成本也比较低。 + 但是使用 React 你如果需要对现有工程接入的话成本很高,你甚至是重写代码,代码组织方式,工程处理方式基本也改变了。开发者可能需要适应一段时间,门槛稍高。 +- 运行时性能 + 在 React 中当某个组件的状态发生变化的时候,它会以该组件为根,将所有的子组件树进行更新。对于如果知道不需要更新的组件可能需要使用 `PureComponent` 或者手动实现 `shouldComponentUpdate` 方法。Vue 中不需要额外注意这些事情,默认实现的。使得开发者专心做业务开发。 + Vue.js使用基于依赖追踪的观察并且使用异步队列更新。轻量,高性能 +- 开发方式 + 在 React 中组件的渲染功能都依赖于 JSX(Javascript的一种语法糖,尽管这种方式对于 Javascript 来说很爽,但是对于已有业务进行重构是很麻烦的,为什么?你需要将你页面的东西拆分为组件,但是在 React 中组件的输出是靠 `render` 函数,render 函数内部不能直接写 HTML,而是需要 JSX 语法糖。 + Vue.js 在这方面就比较友好,对于已经有的项目可以低成本的接入,因为已有的 HTML 代码就是模版代码,然后将业务写入到 Script 标签,操作 ViewModel。虽然 Vue.js 的组件也支持 JSX 的方法来写代码,为的就是让 React 开发者很快上手。 + + ```react + import React, { Component } from 'react'; + import { Image, ScrollView, Text } from 'react-native'; + + class AwkwardScrollingImageWithText extends Component { + render() { + return ( + + + + 在iOS上,React Native的ScrollView组件封装的是原生的UIScrollView。 + 在Android上,封装的则是原生的ScrollView。 + 在iOS上,React Native的Image组件封装的是原生的UIImageView。 + 在Android上,封装的则是原生的ImageView。 + React Native封装了这些基础的原生组件,使你在得到媲美原生应用性能的同时,还能受益于React优雅的架构设计。 + + + ); + } + } + ``` +- 组件作用域内的 CSS + React 中的 css 是通过 css-in-JS 来实现的,和传统书写 CSS 是有区别的,不是无缝对接的, + Vue 中的 css 编写和传统的开发是一致的,你可以在  .vue 文件中对标签添加 scoped 属性来告诉 css-loader 这些 css 规则只在该模块内有效。 + + ```CSS + + ``` + 这个属性的作用就是会自动添加一个属性,为组件内的 css 指定作用域,编译成 `.list-container[data-v-21e5b78]:hover` +- 向上拓展 + React 和 Vue 都提供路由、全局状态管理的解决方案,区别在于 Vue 是官方维护的,React 则是社区维护的。(Vuex、Redux、Vue-Router) + 都有脚手架,Vue-cli 允许你自定义一些设备而 React 不支持。 + +- 向下拓展 + React 学习曲线比较陡峭、也可以说对现有的工程改造门槛较高,需要大范围改写,相比 Vue 则较为友善点,侵入性低。可以像 jQuery 一样引入一个核心的 min.js 文件就可以改造接入现有工程。 + +- 原生渲染 + React 有 React Native 一个较为成熟的方案,Vue 则有阿里的 Weex。差别在于你写了 React Native 应用则不能在浏览器运行,而 Weex 可以在浏览器中和移动设备上运行。所谓多端运行的能力, + +- 开发缺点 + Vue 中不能检测到属性的添加、删除,所以可以用类似 React 中的 set 方法。 + +## 有 Vue 基础如何快速上手 Weex
    + +1. quick demo +2. 虽然都是采用 Vue.js 开发,但是存在 Weex 与平台的差异:上下文、DOM、样式、事件(Weex 不支持事件冒泡和捕获)、样式(Weex支持单个类选择器、并且只支持 CSS 规则的子集)、Vue 网页端的一些配置、钩子、在 Weex 中不支持 + + - html 标签 + 目前 Weex 支持了基本容器(div)、文本(text)、图片(image)、视频(video)等组件,但是需要注意是组件而不是标签,虽然写起来跟标签一样很像,但是写其他的标签必须和这些组合起来使用。类比 Native 的视图层级 + - Weex 中不存在 Dom + Weex 解析 Vue 得到的不是 dom,而是原生布局树 + - 支持有限的事件 + 因为在移动端中所有有些网页端的事件是不支持的,请查看[支持的事件列表](http://weex.apache.org/cn/wiki/common-events.html) + - 没有 BOM,但可以调用原生 Api + DOM?BOM? + javascript组成:ECMAScript 基本语法;BOM(Borwser Object Model:浏览器对象模型,使用对象模拟了浏览器的各个部分内容);DOM(Document Object Model:文档对象模型:浏览器加载显示网页的时候浏览器会为每个标签都创建一个对应的对象描述该标签的所有信息) + + 在 Weex 中能够调用原生设备的 api,使用方法是通过注册、调用模块来实现的,其中一些模块是 Weex 内置的,比如 clipboard、navigator、storage 等。为了保持框架的通用性,Weex 内置的原生模块很有限,不过 Weex 提供了横向拓展的能力,可以拓展原生模块。具体参考 [Androi 拓展](http://weex.apache.org/cn/guide/extend-android.html)、[iOS 拓展](http://weex.apache.org/cn/guide/extend-ios.html) + - 样式差异 + Weex 中的样式是由原生渲染器解析的,出于性能和功能复杂角度的考虑,Weex 对于 css 特性做了一些取舍。(Weex 中只支持单个类名选择器,不支持关系选择器、也不知支持属性选择器;组件级别的作用域,为了保持 Web 和 Native 的一致性,需要使用 style scoped 的写法;支持基本的盒模型和 flexbox 的写法,box-sizing 默认为 border-box,margin,padding,border 属性不支持合并简写;不支持 display:none;可以用 display: 0; 代替,display < 0.01 的时候可以点击穿透;样式属性不支持简写、提高解析效率;css 不支持 3D 变化) + - 单位 + Weex 中所有的 css 属性值单位为 px,也可以省略不写 + - Flexbox 支持不完全 + `align-items: baseline;align-content:space-around;align-self:wrap_revserse;` 等 + - 显隐性 + 在 Weex 中的 iOS 和 Android 端不支持 `display:none;` 所以 v-show 条件渲染写法也是不支持的,可以用 `v-if` 代替,或者 `display:0;` 模拟。由于移动端的渲染特点是当 opacity < 0.01 的时候 view 是可以点击穿透,所以 Weex 中当元素 display < 0.01 的时候元素看不见,但是占位空间还在,但用户无法与之交互,同样点击时会发生穿透的效果。 + - css3 + 相比 React Native 不能用 css3,Weex 的 css3 的支持程度算比较高,但是有一些 css3 的属性还是不支持的。transform 支持 2D;font-family 支持 ttf 和 woff 字体格式的自定义的字体;liner-gradient 只支持双色渐变 + - 调试方式 + 如果说 React Native 的调试方式解放了原生开发调试、那么 Weex 就是赋予了 web 模式调试原生应用的能力。 diff --git a/第二部分 Web 前端/2.3.md b/Chapter2 - Web FrontEnd/2.3.md similarity index 99% rename from 第二部分 Web 前端/2.3.md rename to Chapter2 - Web FrontEnd/2.3.md index 2e7b4e9..c3fe1fc 100644 --- a/第二部分 Web 前端/2.3.md +++ b/Chapter2 - Web FrontEnd/2.3.md @@ -1,4 +1,4 @@ - +# CSS-埋点统计 > 当一个网站或者 App 的规模达到一定程度,需要分析用户在 App 或者网站的相应操作,则需要埋点统计用户行为,这个不用多说,具体实现有 JS 脚本写好埋点事件并调接口,今天 get 到一种新的埋点统计方式保证耳目一新。下面代码简单示范一下。 diff --git a/Chapter2 - Web FrontEnd/2.30.md b/Chapter2 - Web FrontEnd/2.30.md new file mode 100644 index 0000000..c208971 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.30.md @@ -0,0 +1,20 @@ +# Javascript 常用工具封装 + +## 类型判断 + +```Javascript +function type (o) { + return Object.prototype.toString.call(o).toString().slice(8, -1); +} +function sayHi () { + console.log(`Everybody sayHi`); +} + +console.log(type(1)) // Number +console.log(type('@航程小刘(http://github.com/FantasticLBP)')) // String +console.log(type(null)) // Null +console.log(type(false)) // Boolean +console.log(type({})) // Object +console.log(type([1,2,3])) // Array +console.log(type(sayHi)) // Function +``` \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.31.md b/Chapter2 - Web FrontEnd/2.31.md new file mode 100644 index 0000000..4b25da4 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.31.md @@ -0,0 +1,2 @@ +# 浏览器布局与DOM绘制 + diff --git a/Chapter2 - Web FrontEnd/2.32.md b/Chapter2 - Web FrontEnd/2.32.md new file mode 100644 index 0000000..2234082 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.32.md @@ -0,0 +1,238 @@ +# React核心技术剖析 + + + +## 虚拟 Dom + +传统的开发方式的是关心接口请求后的数据处理以及 Dom 操作。在推出 React 和 Vue 之流后,开发者无须直接操作 Dom,而是去关心数据流动。因为在 MVVM 框架中很重要,但是在名字上没有体现出其重要性的一个角色:**binder** 层帮我们做好了 view 到 model 的绑定关系(底层实现类似对属性的 getter、setter 进行拦截,然后设置 watcher。然后通过指令去绑定到 Dom 和 数据。setter 的时候发现数据变动了则发送通知,然后 watcher 监听到后去自动刷新 Dom)。你只需要关心数据。 + +那么如何实现的呢?我们知道 Dom 操作很耗费性能,但是到底消耗在哪里,重排、重绘、渲染算一个,可以看看这篇[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.31.md)。 +结合实际开发步骤想想看,先声明数据,再写 jsx 模版,然后数据驱动(setState),拿到第一份的 Virtual Dom。在数据变动后 setState 拿到最新的数据和样式模版,生成最新的 Virtual Dom。然后根据 diff 算法找出变动的地方,然后该组件重新渲染其子组件也会跟着一起渲染。 + +虚拟 Dom 本质上来看就是 JS 对象(将一个 Dom 节点用 js 对象模拟表示出来)。直接操作 Dom 成本高,采用一种方式将 Dom 的树形结构表示出来,那么就采用了 js 对象去描述一个 Dom 结构。 然后再每次数据变动之后,根据数据和样式模版渲染生成新的虚拟 Dom。再利用 Diff 算法计算出差异部分,再去渲染。 + +React 性能高效的一个原因就是 Virtual Dom 的应用和 diff 之后的 Batch Update(批量处理,类比 Vue 中的 $nextTick。有 Native 开发经验的同学对于这里应该有似曾相识的感觉,和 RunLoop 很像。任何 UI 层变动的东西提交给系统,系统再下一次的运行循环到来的时候统一去渲染。) + + + +## Diff 算法 + +diff 算法大体上做的事情就是拿到前后2个状态的 Virtual Dom ,然后按照同层级节点去比较,发现当前的节点有差异,则不向下进行比较,直接将当前节点重新渲染。 + + + +## JSX 的原理 + +JSX 做的事情是为了告诉 React 样式模版是什么。本质上来说 JSX 就是 `React.createElement` 的可读性更强的版本。`React.createElement` 接收三个参数。参数1:标签类型;参数2:属性;参数3:子元素。 + + +```Javascript +render () { + const { content } = this.props + return
    item-testing
    +} +// 等价于下面的写法 +render () { + const { content } = this.props + return React.createElement('div', {}, React.createElement('span', {}, 'item-testing')) +} +``` + + + +## 生命周期 + +![React生命周期](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-17-ReactLifecycle.PNG) + + + +## 状态管理 + +Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍了。解决了各个组件之间数据传递的复杂问题。先看看 Redux 进行状态管理的一个流程吧。 + +![Redux-数据流动](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-26-Redux-Structures.png) + +### 开发步骤 + +- 各个组件在需要修改传递数据的时候创建一个 Action +- 利用 dispatch 提交给 store +- store 本身不处理 state,所以将 action 转发给 reducer +- reducer 根据 action 的 type 判断具体如何处理数据。 reducer 中返回的函数是纯函数(输入给定的时候,输出的结果也是恒定的。且不改变输入值),函数的返回值就是 state,state 返回给 store,store 可以通过 `getState()` 拿到最新的 state 数据。 +- 各个组件如果需要知道 state 的数据变化,那么可以在组件的 constructor 中设置监听订阅(subscribe) store(代码:store.subscribe(this.handleStateChange))。订阅的地方设置一个处理函数,然后在处理函数里面根据 store 获取到最新的 state(this.setState(store.getState()))。 + +### 开发经验 + +- Redux 中每次创建 action 都需要设置 type,type 为字符串,所以很容易写错,且各个组件都直接用字符串的方式创建 action 的 type 会比较分散,字符串拼写错误造成的 bug 难以排查,所以需要一个地方集中统一处理 action type。思路为在 `src/store` 文件夹下面创建 actionTypes.js 文件夹,创建全部大写的变量,然后导出。 +
    + 展开示例代码 + + ```javascript + export const CHANGE_INPUT_VALUE = 'change_input_value'; + export const ADD_TODO_ITEM = 'add_todo_item'; + export const DELETE_TODO_ITEM = 'delete_todo_item'; + export const INIT_TODO_DATA = 'init_todo_data'; + export const GET_INIT_LIST = 'get_init_list' + ``` +
    + +- Redux 使用的时候全局工程会创建很多 action,所以和上面的思想一样,需要集中统一处理,符合“收口原则”、“单一原则”。做法就是在 `src/store` 目录下创建一个 actionCreators.js 文件。然后在里面引入 actionType.js,根据业务导出几个产生 action 的函数。 +
    + 展开示例代码 + + ```javascript + import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes' + + export const getInputChangeAction = (value) => { + return { + type: CHANGE_INPUT_VALUE, + value + } + }; + + export const getAddTodoItemAction = () => ({ + type: ADD_TODO_ITEM + }); + + export const getDeleteTodoItemAction = (value) => ({ + type: DELETE_TODO_ITEM, + value + }); + ``` +
    +- store 发现 action 提交的数据是函数类型的时候,会自动执行函数 + + +### 核心思想 + +- 单一数据源:整个应用的 state 被存储在一棵 Object tree 中,并且这个 object tree 只存在于唯一一个 store 中 +- State 是只读的:唯一改变 state 的方式就是触发 action,action 是描述已发生时间的普通对象 +- 使用纯函数来执行修改:为了描述 action 如何改变 state tree,你需要根据业务编写 reducer + +### Redux-thunk + +Redux-thunk 是 redux 里面常用的一个中间件。中间件?针对谁和谁的中间?对 action 和 store 的中间件。本来 action 只可以返回一个对象,灵活性较低,但是采用了 redux-thunk 之后,action 不仅可以传递对象,还可以传递函数。 action 通过 dispatch 传递给 store。 dispatch 判断 action 的类型,如果是对象则直接传递;如果是函数则直接执行。 + +![不使用redux-thunk时action返回函数报错](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-24-reduxThunk.png) + +- 异步函数不应该放在组件的生命周期函数里面。复杂的业务逻辑和异步函数适合拆分。目前主流的解决方案有2种中间件:redux-thunk、redux-saga。采用不同的策略 + - redux-thunk:将异步任务分离到 action 中。 + - redux-saga:将异步任务拆分到单独的文件中,而不是 action 里面。 + 相比较而言,redux-saga 比 redux-thunk 功能更加强大,提供的有用的功能更多。 + +- react-redux:网上经常说的 react-redux 里面既有 UI 组件、也有容器组件。connect 方法将一个 UI 组件(傻瓜组件) 和 store、dispatch 联合在一起后,connect 函数的返回结果就是一个容器组件 + ```javascript + export default connect(mapStateToProps, mapDispatchToProps)(TodoListReactRedux) + ``` + + +## 组件的写法 + +```javascript +// 功能组件 +function Welcome (props) { + return

    Hello, {props.name}

    ; +} +// 等价于下面的写法。ES6 类 +class Welcome extends React.Component { + render () { + return

    hello, {this.props.name}

    ; + } +} + +``` + +### + +## 开发tips + +- 不要直接操作 state,只能通过 setState 操作数据;props 是只读的 + +- setState 的时候如果依赖之前的 state 数据,那么 setState 第一个参数可以更改为函数方式,这个函数有2个参数 + ```javascript + setState((state, props) => ({ count: state.count + props.increment })); + ``` +- 路由设置的时候,我们经常会设置路径。每个路径匹配到具体的页面资源会呈现出来。但是在一开始的时候会遇到疑问,为什么我在浏览器里面输入了 “/home”。但是出来的内容还是 “/” 皮配到的页面,后来知道了还可以设置 **exact** 属性可以精确控制。 + ```javascript + + +
    +
    + + + + +
    +
    +
    + ``` +- 在 React 的开发中,有2个名字会很熟悉:傻瓜组件、容器组件。假如一个 TodoList 的 UI 部分和逻辑处理部分都在 一个 TodoList 组件里面进行解决,那么代码将会冗余且不易测试,为了解决此问题,我们常常会将 UI 部分单独抽离出去,只负责显示出 UI,这种组件叫做傻瓜组件(UI组件)。页面需要的数据或者点击事件的处理函数都通过 **props** 的形式由父组件传递下来。父组件在渲染的时候只负责逻辑的展示,在自身的 render 函数里面调用之前分离出去的傻瓜组件(UI组件)。为了保证代码的健壮性和安全性,UI 组件需要的数据和函数都通过 props 传递,且加一个 propTypes 安全校验。 + +- 无状态组件:当一个组件只有 render 函数的时候,这样的组件被叫做**无状态组件**。做法就是将 `class TodoList extends React.Dom` 修改成一个函数。函数形式的无状态组件效率比较高。因为类形式的组件,会有生命周期等函数,效率会低一些。 + + ```javascript + const TodoListUI = (props) => { + return
    props.name
    ; + } + ``` + +- export default 在一个模块里面只可以存在一个,使用的时候不需要 `{}`;export 可以存在多个,使用的时候需要使用 `{}` + ```javascript + export const person = { + name: 'lbp', + age: 22 + } + export const testing = 'testing' + export default store; + + import store from './store' + import { person, testing } from './store' + ``` + + +## React 和 Vue 的对比 + +- React 是单向数据流,数据是不可变的。Vue 是双向数据流,数据是可以变的。什么意思?看下面的例子 + Vue.js + ```javascript + + ``` + React.js + ```javascript + constructor(props) { + super(props); + this.state = store.getState() + // 函数的 this 绑定放在顶部对 React 性能提升有好处 + this.handleInputChange = this.handleInputChange.bind(this) + store.subscribe(this.handleStateChange); + } + + + handleInputChange(e) { + /* + // 方式1 + this.setState({ + inputValue: this.inputRef.value + }) + */ + //方式2:redux:先创建 action,然后 dispatch 给 store,然后 store 转发给 reducer、reducer处理完后给 store、依赖的组件设置监听,然后在监听的回调里面拿到最新的数据去 render UI + const action = getInputChangeAction(e.target.value); + store.dispatch(action); + } + // 负责订阅 store 里面 state 的改变;感知到 store 里面的 state 改变,则在当前组件里面 setState + handleStateChange () { + this.setState(store.getState()); + } + ``` + 可以看出来,Vue 通过简单的一个内置命令 `v-model` 将 model 和 view 双向绑定了起来,数据是双向的。model 改变 view 自动刷新;用户在 input 写了文字,model 的值也自动改变。React 先设置一个 input 组件,监听用户的输入事件(onChange),然后在 onChange 里面拿到当前输入框里面的数据,然后你可以直接 setState 去操作数据,setState 后才会触发 render 函数,dom 才会跟着更新。 + +- React 比 Vue 更加适合构建大型项目。 + 什么是大型项目?这句话没什么意义,那就说一些区别吧。通过上面的说明知道 Vue 在内部做了双向绑定,对于数据的处理更加方便。React 对于数据变动还需要自己手动去调用 setState。假如有多个数据,按照 Vue 的原理会启动 n 个 watcher 去监听,所以性能会有一些问题。React 性能相关都需要开发者去处理。 + +- Vue 设计思想:How easy it can be。React:How corrct it can be 和 all in js(css写法也在用 js 控制,比如 styled-component) + + + \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.33.md b/Chapter2 - Web FrontEnd/2.33.md new file mode 100644 index 0000000..df04005 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.33.md @@ -0,0 +1,58 @@ +# ES6学习总结 + +## 闭包 +匿名函数保存的是引用。看看下面代码例子 + +```javascript +var array = new Array(); +// 实验1: +for (var index = 0;index < 3; index++) { + array.push(function(){ + console.log(index) + }) +} +array[1]() // 输出2 + +// 实验2 +for (var index = 0; index < 3; index++) { + (function (n) { + array.push(function(){ + console.log(n); + }) + })(index) +} +array[1]() // 输出1 +``` + +### 闭包与内存泄漏 + +js 内存控制主要是通过计数器。当某个对象 A 引用对象 B,那么 B 的计数器加1,若 A 释放掉了,那么 A 引用对于 B 的计数器将变为 0。若 B 的所有计数器变为0的时候,那么 B 将会被释放。引用循环将会造成内存泄漏,什么是引用循环?A 引用 B,B 引用 A,会造成引用循环,内存泄漏 + +正是因为闭包的这个特点,我们可以将闭包应用在某些场合。 + + +### 闭包应用场合 + +- 封装 +```javascript +var Person = function () { + var name = "LBP"; + return { + getName () { + return 'I am ' + name; + }, + setName (newName) { + name = newName; + } + } +}(); + +console.log(Person.name); +console.log(Person.getName()); +Person.name = "杭城小刘"; +Person.setName('杭城小刘'); +console.log(Person.getName()); +``` + +- + diff --git a/Chapter2 - Web FrontEnd/2.34.md b/Chapter2 - Web FrontEnd/2.34.md new file mode 100644 index 0000000..262d663 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.34.md @@ -0,0 +1,165 @@ +# 富文本编辑器原理探索 + +> 经常在做企业网站的管理系统的时候需要用到富文本编辑器,之前基本上都是直接去 npm 或者 github 上面搜找一些排名考前或者 readme 写的好的库,直接拿来用。万变不离其宗,是时候探索下本质了。 + + + +## contenteditable + +要想实现富文本需要开启“编辑”的能力,系统提供了一个 api:**contenteditable** 允许我们对内容进行编辑。下面是来自 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable) 的官方解释。 + +> The contenteditable global attribute is an enumerated attribute indicating if the element should be editable by the user. If so, the browser modifies its widget to allow editing. +> The attribute must take one of the following values: +> - true or the empty string, which indicates that the element must be editable; +> - false, which indicates that the element must not be editable. +> If this attribute is not set, its default value is inherited from its parent element. + +contenteditable 的值设置为 true 或者空字符串`""` 允许内容被编辑,false 则代表不可被编辑。 +它不仅可以作用在 textarea、div、甚至是网页所见的都可以进行编辑。所以利用这点儿你可以做一些😈坏事情 ,比如修改教务处网页上的成绩单和绩点分数,修改天气预报的温度走势情况,反手修改某一天的温度为 66度。 + + + +## document.execCommand + +想想看,你在输入框里面输入了一段文字,你点击上面的加粗按钮如何实现?MDN 告诉我们有个 api 可以满足需求。当元素进入编辑模式的时候,document 对象暴露出一个 **execCommand** 方法去操纵当前的可编辑区域 。看看下方 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand) 给出的解释。 + +> When an HTML document has been switched to designMode, its document object exposes an execCommand method to run commands that manipulate the current editable region, such as form inputs or contentEditable elements. +> document.execCommand(aCommandName, aShowDefaultUI, aValueArgument) +> A Boolean that is false if the command is unsupported or disabled. + +aCommandName:命令名称。比如加粗、下划线、无序列表、段落、H1等等 +aShowDefaultUI:布尔值。是否展示默认的样式,一般为 false +aValueArgument:一些命令所需要的额外参数,比如 insertImage 插入图片所需要的图片 url + +完整的命令和各个浏览器的支持情况可以查看 MDN。 + + + +## Selection 和 Range 对象 + +在执行 document.execCommand 的时候需要知道对谁在什么范围内执行命令。这里有一个选区的概念,也就是 Selection,用来表示用户选择的范围。(说明:用户不选中任何内容,也就是只有一闪一闪的光标的情况也算是一种特殊的选中)。 + +一个页面包含多个选中区域(Firefox) 支持。所以 Selection 可以看作是 Range 对象的集合。通常情况下我们一般只存在一个选中的区域,所以 `document.getSelection().getRangeAt(0)` 就可以拿到当前选区的信息。 + +Range 对象请看下图 + +![选区Range对象1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-28-DocumentRangeObject1.png) + +![选区Range对象2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-28-DocumentRangeObject2.png) + +上面说到**光标**也是一个特殊的选区,当 endOffset 和 startOffset 相等的时候,collapsed 属性就为 true。 + +通过 document.getSelection().getRangeAt(0) 就可以获取到选区的信息,那么可以将当前选区保存下来,等到需要的时候再拿出来并展示。Selection 对象还有几个开放的方法(addRange、collapse、collapseToEnd、collapseToStart)可以操纵光标(比如插入文字后光标的位置)。 + + + +## 理论联系实际、一切从实际出发 + +动手做一个简易的富文本编辑器吧。(不想写一个 Vue 或者 React 工程,拿最简单的 html 撸一个吧) + +思路: +- 新建 html 文件 +- 设置2个大的 div,一个展示功能按钮,一个展示编辑区域(编辑区域需要设置允许可编辑) +- 样式布局 +- 各个功能按钮添加点击事件监听 +- 因为点击各个按钮都是执行 document.execCommand,唯一不同的就是命名名称和参数不一样,所以简单封装函数 +- 调用封装的函数,传递参数 + +下面贴出代码 + +```html + + + 富文本 + + + + +
    + + + + + + + + + +
    +
    + + + +``` + +![简易富文本编辑器](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-28-RichTextEditor.png) + + + +## 注意点 + +- 执行的是原生的 document.execCommand 方法,浏览器自身会对 contenteditable 这个可编辑区维护一个 undo 栈和一个 redo 栈,所以我们才能执行前进和后退的操作,如果我们改写了原生方法,就会破坏原有的栈结构,这时就需要自己去维护,代价很大 +- 如果是 Vue style 里面如果加上 scope 的话,里面的样式对编辑区的内容是不生效的,因为编辑区里面是后来才创建的元素,所以要么删了 scope,要么用 /deep/ 解决(Vue 是这样)。React 的 styled-components 也有类似问题。 \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.35.md b/Chapter2 - Web FrontEnd/2.35.md new file mode 100644 index 0000000..6f1d5b1 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.35.md @@ -0,0 +1,181 @@ +# Vue3 核心技术 + +写这篇文章是之前看到了尤大的微博,Vue3 发布了新版本,其在声明中写到对于之前的 Object.defineProperty 用 Proxy 改写了。 + +> The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + +## 为什么要改写? + +Vue3 之前的版本响应式是通过 `Object.defineProperty` 实现的。getter 用来依赖收集,setter 在数据变化时通知订阅者更新视图。这种方式存在一些缺陷: +1. 无法检测到对象属性的新增或者删除。 + Object.defineProperty 只会是属性的读写才会触发,所以新增、删除都无法检测。Vue 为了做到新增、删除也是响应式,做了一些手段 `Vue.set(obj, propertName/index, value)`、响应式对象的子对象新增属性,可以给子响应式对象重新赋值。 +2. 不能监听数组的变化 + Vue 在数组做了 hack,把无法监听的情况通过重写数组某些方法实现响应式。数组的操作也限制在 `push/pop/shift/unshift/splice/sort/reverse` 7个方法。 + + +## Proxy 如何解决? + +Vue3 利用 Proxy 实现数据读取和设置拦截,在拦截 trap 中实现数据依赖收集和触发视图更新的操作。 + +```Javascript +function get(target, key, receiver) { // handler.get的拦截实现 + const res = Reflect.get(target, key, receiver) + if(isSymbol(key) && builtInSymbols.has(key)) return res + if (isRef(res)) return res.value + track(target, OperationTypes.GET, key) // 收集依赖 + return isObject(res) ? reactive(res) : res +} +// handler.set的拦截操作 +function set(target, key, value, receiver) { + value = toRaw(value) // 获取缓存响应数据 + oldValue = target[key] + if (isRef(oldValue) && !isRef(value)) { + oldValue.value = value + return true + } + const result = Reflect.set(target, key, value, receiver) + if (target === toRaw(receiver)) { //set拦截只限对象本身 + ... // 不同环境操作处理,并省略下面trigger方法第二参数获取逻辑 + trigger(target, OperationTypes.x, key) // 触发视图更新 + } + return result +} +``` + +## Proxy ? + +Proxy - “代理”。当访问对象之前增加一个中间层,这样就不直接访问对象,而是通过中间层做一个中转。这个中间层就是本文主角 - **Proxy**。相信大家很早听到代理可能是`网络代理`。比如你需要访问国外站点,请求的时候你的电脑发起的网络请求被代理服务器做转发,然后再请求真正的目的服务器,响应的时候国外站点将响应发送到代理服务器,代理服务器再将数据内容返回到你的电脑。 `网络代理`分为「正向代理」和「反向代理」。nginx 就是架设在用户浏览器和目的服务器之间的中间层。 浏览器通过 nginx 才可以访问到服务器。通过代理可以实现负载均衡、访问控制等目的。 + +Javascript 中的 Proxy 类似。区别在于 Proxy 是对象和对象之间的一层代理。应用了 Proxy 之后,我们通过 Proxy 来访问或者操作目标对象。Proxy 会改变 Javascript 中的一些基础操作执行路径,比如属性读写、对象遍历、函数调用等。这种强大的能力被叫做**元编程**(meta programming)机制,可实现很多之前无法实现的功能。 + +Proxy 会修改 Javascript 的一些底层代码执行方式,所以它无法被完全 polyfill。 + +## 核心概念 + +- target:指的是目标对象,也就是被代理的对象 +- trap:可以理解为一些预定义的触发点以及定义在这些触发点上的函数。比如属性的读取、函数调用等。如果在这些触发点上定义了函数,那么在对 Proxy 执行对应操作的时候,定义的函数将会被调用 +- handler:是一个对象,包装了所有 Proxy 提供的 trap 函数,可以认为是 trap 的集合 + + +## Proxy 创建 + +在以下简单的例子中,当对象中不存在属性名时,缺省返回数为37。例子中使用了 get。 + +```Javascript +let handler = { + get: function(target, name){ + return name in target ? target[name] : 37; + } +}; + +let proxy = new Proxy({}, handler); + +proxy.a = 1; +proxy.b = undefined; + +console.log(proxy.a, proxy.b); // 1, undefined +console.log('c' in proxy, proxy.c); // false, 37 +``` +上面的代码定义了 Proxy 对象 proxy,被代理的对象是一个空对象 {}, 有了代理对象,对被代理对象的操作都会通过代理对象进行处理。当访问 proxy.c 的时候,我们定义的 handle 对象中的 get 函数(trap)会被调用。 get 函数接受的参数分别为 target对象和需要访问的属性名称。 + + +## 有哪些 traps? + +所有的traps是可选的。如果某个trap没有定义,那么默认的行为会应用到目标对象上。 + +- handler.getPrototypeOf() + 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。 +- handler.setPrototypeOf() + 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。 +- handler.isExtensible() + 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。 +- handler.preventExtensions() + 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。 +- handler.getOwnPropertyDescriptor() + 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。 +- handler.defineProperty() + 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。 +- handler.has() + 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。 +- handler.get() + 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。 +- handler.set() + 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。 +- handler.deleteProperty() + 在删除代理对象的某个属性时触发该操作,即使用delete运算符,比如在执行 delete proxy.foo 时。 +- handler.ownKeys() + Object.getOwnPropertyNames 和Object.getOwnPropertySymbols的trap. +- handler.apply() + 当目标对象为函数,且被调用时触发。 +- handler.construct() + new 运算符的trap。 + + +## Reflect + +**Reflect** 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。 +与大多数全局对象不同,Reflect 不是一个构造函数。你不能将其与一个 new 运算符一起使用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是**静态**的(就像Math对象) + +```Javascript +let handler = { + get: function(target, name){ + return Reflect.get(target, name); + } +}; + +let proxy = new Proxy({}, handler); + +proxy.a = 1; +proxy.b = undefined; + +console.log(proxy.a, proxy.b); // 1, undefined +console.log('c' in proxy, proxy.c); // false, 37 +``` + +Reflect 对象提供和了以下 static 方法,用来方便写 trap 函数时将操作传递到目标对象上。 + +- Reflect.apply() + 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。 +- Reflect.construct() + 对构造函数进行 new 操作,相当于执行 new target(...args)。 +- Reflect.defineProperty() + 和 Object.defineProperty() 类似。 +- Reflect.deleteProperty() + 作为函数的delete操作符,相当于执行 delete target[name]。 +- Reflect.enumerate() + 该方法会返回一个包含有目标对象身上所有可枚举的自身字符串属性以及继承字符串属性的迭代器,for...in 操作遍历到的正是这些属性。 +- Reflect.get() + 获取对象身上某个属性的值,类似于 target[name]。 +- Reflect.getOwnPropertyDescriptor() + 类似于 Object.getOwnPropertyDescriptor()。 +- Reflect.getPrototypeOf() + 类似于 Object.getPrototypeOf()。 +- Reflect.has() + 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。 +- Reflect.isExtensible() + 类似于 Object.isExtensible(). +- Reflect.ownKeys() + 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响). +- Reflect.preventExtensions() + 类似于 Object.preventExtensions()。返回一个Boolean。 +- Reflect.set() + 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。 +- Reflect.setPrototypeOf() + 类似于 Object.setPrototypeOf()。 + + +## Proxy 优缺点 + +优点: + +看上去的 getter、setter 的功能一样,都可以做到当访问或者改写一个对象的时候调用某个特定的函数。但是还是有区别的: +1. Proxy除了对属性值的获取和改写还可以改变其他 JavaScript 基础操作的执行方式,也就是它的功能比 getter、setter 多很多 +2. getter、setter 是定义在对象本身上的,而 Proxy 是定义在另一个对象上的。这表示 **Proxy 允许我们比较方便的修改一些我们不方便直接改变其定义方式的对象的执行方式,比如一些第三方库中定义的对象等**。 + +缺点: +1. 兼容性问题,无完全 polyfill + 虽然大部分浏览器支持 Proxy 特性,但是一些浏览器或者其低版本不支持 Proxy,其中 IE、QQ 浏览器、百度浏览器等完全不支持,因此 Proxy 有兼容性问题。那能否像ES6其他特性那样有对应的 polyfill 解决方案呢,答案并不那么乐观。其中作为ES6转换的翘楚 Babel,在其官网明确做了说明: + > Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled. + 也就是说,由于ES5的限制,ES6的Proxy没办法被完全polyfill,所以babel没有提供对应的转换支持,Proxy的实现是需要JS引擎级别提供支持,目前大部分主要的JS引擎提供了支持 +2. 性能问题 + Proxy 的性能比 Promise 还差,这就要需要在性能和简单实用上进行权衡。另外,Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,性能这块相信会逐步得到改善。 \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.36.md b/Chapter2 - Web FrontEnd/2.36.md new file mode 100644 index 0000000..6f2817f --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.36.md @@ -0,0 +1,16 @@ +# immutable.jS 与 React + + +## 引用带来副作用 + +js 中数据类型分为2种,primitive value 和 Object。js 中的对象非常灵活,某个地方对对象进行修改后产生的结果难以预期,所以我们需要不可变对象。 +但是自己实现的对象拷贝算法,可能浪费时间也浪费空间, 所以诞生了 immutable.js 这个库。 + + + +## 核心实现 + +immutable.js 基于哈希映射树和 vector map tries。只 clone 该节点和其祖先节点,其他保持不变,这样可以共享大部分相同的节点,大大提高性能。 + +1. 避免直接修改线性结构 +2. 减小内存开销(增量更新) \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.37.md b/Chapter2 - Web FrontEnd/2.37.md new file mode 100644 index 0000000..2d3f968 --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.37.md @@ -0,0 +1,63 @@ +## 前端工程化与 CI、CD + +首先明确,这个话题很大,所以文章篇幅特别长,所以拆分成多个部分进行记录。 + + + +### 用 husky 和 lint-staged + +1. 什么是 lint? + + + > In computer programming, lint is a Unix utility that flags some suspicious and non-portable constructs (likely to be bugs) in C language source code; generically, lint or a linter is any tool that flags suspicious usage in software written in any computer language. The term lint-like behavior is sometimes applied to the process of flagging suspicious language usage. Lint-like tools generally perform static analysis of source code. + + 上面是[维基百科](https://en.wikipedia.org/wiki/Lint_%28software%29)的定义。 **Lint 就是对代码进行静态分析,并试图找出潜在问题的工具。** + + +2. Lint 的好处,有以下三点: + + - 可以让代码更加阅读性良好 + - 具有更高的开发效率。工程师会花 50% 时间去做问题定位和解决 bug,其中很多错误是低级的,而 lint 可以很容易定位发现这些低级错误 + - 可以让代码存在更少 Bug。 + + +3. Lint 的反馈链条太长 + + 假如 lint 是在 ci、cd 的后续步骤则会发现整个反馈流程太长,所以应该是 commit 之前做 lint。CI 流程做 lint 的缺点:Lint 在整个开发工作流中反馈链条太长,浪费时间 + +4. 所以有工具可以做这个事情。 + lint-staged、husky 允许你在代码提交的时候做掉 eslint。commitlint 可以让你在提交的时候将 commit message 写的规范, + ```json + "dependencies": { + "eslint": "^6.7.2", + "lint-staged": "^9.5.0", + "mocha": "^6.2.2" + }, + "script": { + "test": "NODE_ENV=development jest", + "lint": "eslint . --fix --format codefrane" + }, + "devDependencies": { + "@commitlint/cli": "^8.2.0", + "@commitlint/config-conventional": "^8.2.0", + "husky": "^3.1.0" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "lint-staged": { + "**/*.js": [ + "eslint --fix --cache --format=pretty", + "git add" + ] + } + ``` +关键词: commitlint、lint-staged + + +### 前端代码风格自动化系列 + + diff --git a/Chapter2 - Web FrontEnd/2.38.md b/Chapter2 - Web FrontEnd/2.38.md new file mode 100644 index 0000000..f6232ed --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.38.md @@ -0,0 +1,12 @@ +# npm 改进之工程化 + +npm 3 最大的改进就是对于依赖的处理不一样了。[官方文档](https://docs.npmjs.com/how-npm-works/npm3?utm_source=ourjs.com) + +## 例子 + +模块 A,依赖于模块 B + + +https://www.jianshu.com/p/69ba32550c08 + +https://www.cnblogs.com/wonyun/p/9349691.html \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.39.md b/Chapter2 - Web FrontEnd/2.39.md new file mode 100644 index 0000000..e2197ce --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.39.md @@ -0,0 +1,338 @@ +# 前端模块化演进之路 + +有这样一个场景,客户端运行很久,但是法务部和数据部需要收集用户的一些信息,这些信息收集好之后需要进行相应的数据处理,之后上报到服务端。客户端提供一个纯粹的 JS 执行引擎,不需要 WebView 容器。iOS 端有成熟的 JavaScriptCore、Android 可以使用 V8 引擎。这样一个引擎配套有一个 SDK,访问 Native 的基础能力和数据运算能力,可以看成是一个阉割版的 Hybrid SDK 额外增加了一些数据处理能力。 + +问题结束了吗?处理逻辑的时候还需要用到2个库:[cheerio](https://github.com/cheeriojs/cheerio) 和 [sql](https://github.com/nearform/sql)。因为都是 Node 工程,所以纯粹的 JS 环境是没办法直接执行。所以需求就进行了转变 ———— 将 Node 项目打包成 UMD 规范。这样就可以在纯粹的 JS 环境下运行。接下来的文章就分析下各种规范。其实也就是前端模块化演进之路的几种规范。 + + + + +## 前端模块化演进之路开发的价值 + +随着互联网的飞速发展,前端开发越来越复杂。本文将从实际项目中遇到的问题出发,讲述模块化能解决哪些问题,以及以 Sea.js 为例讲解如何进行前端的模块化开发。 + +1. 恼人的命名冲突 +我们从一个简单的习惯出发。我做项目时,常常会将一些通用的、底层的功能抽象出来,独立成一个个函数,比如 + +```Javascript +function each(arr) { + // 实现代码 +} + +function log(str) { + // 实现代码 +} +``` +并像模像样的将这些代码抽取出来并统一到 `util.js` 中,在需要使用的地方引入该文件,看起来很棒,团队内的同事很感激我提供了这么便利的工具包。 + +直到团队越来越大,开始有人抱怨 + +> 小杨:我定义了一个 each 方法遍历对象,但是 util.js 中已经存在一个 each 方法,每次都需要改方法名,我只能叫 eachObject 方法。
    张三:我定义了一个 log 方法,可是王武的代码出问题了,谁来看看? + +抱怨越来越多,最后参照 Java 的方式,引入**命名空间**解决问题。于是 util.js 代码变成了 + +```Javascript +var org = {}; +org.Utils = {}; + +org.Utils.each = function (arr) { + // 实现代码 +}; + +org.Utils.log = function (str) { + // 实现代码 +}; +``` + +可能看上去的代码很 low,其实命名空间在前端领域的布道者是 Yahoo!的 YUI2 项目,看看下面的代码,是 Yahoo!的一个开源项目 + +```Javascript +if (org.cometd.Utils.isString(response)) { + return org.cometd.JSON.fromJSON(response); +} +if (org.cometd.Utils.isArray(response)) { + return response; +} +``` + +通过命名空间虽然可以极大的解决冲突问题,但是每次在调用一个方法时都需要写一大堆命名空间相关的代码,剥夺了编码乐趣。 + +另一种方式是一个自执行函数来实现。 + +```Javascript +(function (args) { + //... +})(this); +``` + +2. 繁琐的文件依赖 + +继续上述场景,很多情况下都需要开发 UI 层通用组件,这样项目组就不需要重复造轮子。其中有一个高频使用的组件就是 dialog.js +```Javascript + + + +``` + +虽然公共组做项目都会编写使用文档、发送邮件告知全员(项目地址、使用方式等),但是还是有人问「为什么 dialog.js 有问题」,最后排查的结果基本都是没有引入 util.js + +```Javascript + + +``` + +**命名冲突**和**文件依赖**是前端开发中2个经典问题,经过开发者不断的思考和研究,诞生了模块化的解决方案,以 CMD 为例 + +```Javascript +define(function(require, exports) { + exports.each = function (array) { + // ... + }; + exports.log = function(message) { + // ... + }; +}); +``` + +通过 exports 就可以向外提供接口, dialog.js 代码变成 + +```Javascript +define(function(require, exports) { + var util = require('./util.js') + + exports.init = function () { + // ... + }; +}); +``` + +使用的时候可以通过 `require('./util.js')` 获取到 util.js 中通过 exports 暴露的接口。 require 的方式在其他很多语言中都有解决方案:include、 + + + + +## 模块化的好处 + +1. 模块的版本管理:通过别名等配置,配合构建工具,可以轻松实现模块的版本管理 +2. 提高可维护性: 模块化可以实现每个文件的职责单一,非常有利于代码的维护。 +3. 前端性能优化: 对于前端开发来说,异步加载模块对于页面性能非常有益。 +4. 跨环境共享模块: CMD 模块定义规范与 NodeJS 的模块规范非常相近,所以通过 Sea.JS 的 NodeJS 版本,可以方便的实现模块的跨服务器和浏览器共享。 + + +## CommonJS 规范 + +CommonJS 是服务器端模块的规范。NodeJS 采用了这个规范。CommonJS 加载模块是同步的,所以只有加载完成后才能执行后面的操作。 +因为服务器的特点,加载的模块文件一般都存在在本地硬盘,所以加载起来比较快,不用考虑异步的方式。 + +CommonJS 模块化的饿规范中,每个文件都是一个模块,拥有独立的作用域、变量、以及方法等,对其他模块不可见。 CommonJS 规范规定,每个模块内部, **module** 变量表示当前模块,它是一个对象,它的 **exports** 属性是对外的接口,加载某个模块,其实是加载该模块的 module.exports 属性,require 方法用于加载模块。 + + +```Javascript +// Person.js +function Person () { + this.eat = function () { + console.log('eat something') + } + + this.sleep = function () { + console.log('sleep') + } +} + +var person = new Person(); +exports.person = person; +exports.name = name; + +// index.js +let person = require('./Person').person; +person.eat() +``` + +### CommonJS 与 ES6 模块的差异 + +1. CommonJS 模块输出的是**值的拷贝**,ES6 模块输出的是**值的引用** +2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口 + +CommonJS 模块导出的是一个对象(module.exports 属性),该对象只在脚本运行完才会生成。 +ES6 的模块机制是 JS 引擎对脚本进行静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用到被加载的模块中取值, + + + + +## AMD 规范 + +AMD(Asynchronous Module Definition) 是在 Require.JS 推广的过程中对模块定义的规范化产出。AMD 推崇依赖前置。它是 CommonJS 模块化规范的超集,作用在浏览器上。它的特点是异步,利用了浏览器的并发能力,让模块的依赖阻塞变少。 + +AMD 的 API +```Javascript +define(id?, dependencyies?, factory); +``` + +id 是模块的名字,是可选参数。 dependencies 指定了该模块所依赖的模块列表,是一个数组,也是可选参数。每个依赖的模块的输出都将作为参数依次传入 factory 中。 + +```Javascript +require([module], callback) +``` + +**AMD 规范允许输出模块兼容 CommonJS 规范,这时 define 方法如下** + +```Javascript +define(['module1', 'module2'], function(module1, module2) { + function foo () { + // ... + } + return { foo: foo }; +}); +``` + +```Javascript +define(function(require, exports, module) { + var requestedModule1 = require('./module1') + var requestedModule2 = require('./module2') + + function foo () { + // ... + } + return { foo: foo }; +}); +``` + +优点: 适合在浏览器环境中加载模块,可以实现并行加载多个模块 +缺点: 提高了开发成本,并不能按需加载,而是提前加载所有的依赖 + + + + +## CMD 规范 + +CMD 是 Sea.JS 推广的过程中对模块定义的规范化产出。CMD 推崇依赖就近。 +CMD 规范尽量保持简单,并与 CommonJS 规范中的 Module 保持兼容,通过 CMD 规范编写的模块,可以在 NodeJS 中运行。 + +[CMD 模块定义规范 ](https://github.com/seajs/seajs/issues/242) + +CMD 中 require 依赖的描述用数组,则是异步加载,如果是单个依赖使用字符串,则是同步加载。 + +AMD 是 RequireJS 在推广过程中对模块定义的规范化产出,CMD是SeaJS 在推广过程中被广泛认知。SeaJS 出自国内蚂蚁金服玉伯。二者的区别,玉伯在12年如是说: + +RequireJS 和 SeaJS 都是很不错的模块加载器,两者区别如下: + +1. 两者定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端 + +2. 两者遵循的标准有差异。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。 + +3. 两者社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,而采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。 + +4. 两者代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。 + +5. 两者对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便便捷。RequireJS无这方面的支持。 + +6. 两者的插件机制有差异。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Node 的方式一致开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。 + + + + +## UMD 规范 + +UMD(Universal Module Definition)是随着大前端的趋势产生,希望提供一个前后端跨平台的解决方案(支持 AMD、CMD、CommonJS 模块方式)。 + +实现原理: +1. 先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式 +2. 再判断是否支持 AMD 模块格式(define 是否存在),存在则使用 AMD 模块格式 +3. 前2个都不存在则将模块公开到全局(window 或 global) + +```Javascript +// if the module has no dependencies, the above pattern can be simplified to +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.returnExports = factory(); + } +}(this, function () { + + // Just return a value to define the module export. + // This example returns an object, but the module + // can return a function as the exported value. + return {}; +})); +``` + +可能有些人就要问了,为什么在上面的判断中写了 AMD,怎么没有 CMD? 😂 因为前端构建工具 Webpack 不可识别 CMD 规范,使用 CMD 就需要引用工具,比如 Sea.JS + +讲道理,如果想判断 CMD,那 UMD 代码如何写? + +```Javascript +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof define === 'function' && define.cmd) { + // CMD + define(function(require, exports, module) { + module.exports = factory() + }) + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.returnExports = factory(); + } +}(this, function() { + // Just return a value to define the module export. + // This example returns an object, but the module + // can return a function as the exported value. + return {}; +})) +``` + +![模块化](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-02-JSModule.png) + + + +## 回到正题 + +Cheerio 如何打包到普通的 JS 执行环境中。 + +Webpack 支持的模块化参数如下图所示: +![Webpack 模块化参数](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-02-WebpackModuleOptions.png) + +借助 Webpack 可以方便的打出一个 umd 规范的包。 + +```Javascript +module.exports = { + entry: './src/cheerio.js', + output: { + filename: 'cheerio.js', + // export to AMD, CommonJS, or window + libraryTarget: 'umd', + // the name exported to window + library: 'cheerio', + globalObject: 'this' + } +} +``` + + + + +## 总结 + +手机端(无论 iOS 还是 Android)的底层渲染内核都是类 Chrome v8 引擎。v8 引擎在执行 JS 代码时,是将代码先以 MacroAssembler 汇编库在内存中先编译成机器码再送往 CPU 执行的,并不是像其它 JS 引擎那样解析一行执行一行。所以,静态加载的 ES6 模块规范,更有助于 v8 引擎发挥价值。而运行时加载的 CommonJS、AMD、CMD 规范等,均不利于 v8 引擎施展拳脚。 + +在 NodeJS 开发项目中,Node9 已经支持 ES6 语法,完全可以使用 ES6 模块规范。NodeJS 的诞生,本身就基于 Google 的 v8 引擎,没有理由不考虑发挥 v8 的最大潜能。 + +在浏览器 JS 开发项目中,因为从服务器加载文件需要时间,使用 CommonJS 规范肯定是不合适了。至于是使用原生的 ES 模块规范,还是使用 Sea.js,要看具体场景。如果想页面尽快加载,Sea.js 适合;如果是单页面网站,适合使用原生的 ES6 模块规范。还有一点,浏览器并非只有 Chrome 一家,对于没有使用 v8 引擎的浏览器,使用 ES6 原生规范的优势就又减少了一点。 \ No newline at end of file diff --git a/第二部分 Web 前端/2.4.md b/Chapter2 - Web FrontEnd/2.4.md similarity index 98% rename from 第二部分 Web 前端/2.4.md rename to Chapter2 - Web FrontEnd/2.4.md index be23c47..b427923 100644 --- a/第二部分 Web 前端/2.4.md +++ b/Chapter2 - Web FrontEnd/2.4.md @@ -1,4 +1,4 @@ - +# generator函数 generator(生成器)是ES6标准引入的新的数据类型,一个generator看上起像一个函数,但是可以返回多次. diff --git a/第二部分 Web 前端/2.5.md b/Chapter2 - Web FrontEnd/2.5.md similarity index 100% rename from 第二部分 Web 前端/2.5.md rename to Chapter2 - Web FrontEnd/2.5.md diff --git a/第二部分 Web 前端/2.6.md b/Chapter2 - Web FrontEnd/2.6.md similarity index 89% rename from 第二部分 Web 前端/2.6.md rename to Chapter2 - Web FrontEnd/2.6.md index 3c69a3b..c2262f4 100644 --- a/第二部分 Web 前端/2.6.md +++ b/Chapter2 - Web FrontEnd/2.6.md @@ -1,5 +1,6 @@ +# h5自定义对象 -# 一、方式一 +### 一、方式一 在很早以前我们自定义元素的属性要通过 `user-defined-attribute="value"`的方式来设置自己需要的属性 @@ -15,7 +16,7 @@ document.getElementsByTagName("h1")[0].getAttribute("user-defined-attribute") ``` -# 二、方式二 +## 二、方式二 现在H5为我们提供了一个data属性 **"data-" **作为前缀,可以让所有的HTML元素都支持自定义的属性,只要在标签里面以 **"data-"** @@ -43,7 +44,7 @@ document.getElementsByTagName("h1")[0].getAttribute("data-share") `DOMStringMap`是HTML5一种新的含有多个名-值对的交互变量 -# 三、H5 dataset的操作 +## 三、H5 dataset的操作 删掉一个data属性 @@ -57,7 +58,7 @@ delete myDiv.dataset.share myDiv.dataset.happy="ok" ``` -# 四、dataset兼容性处理 +## 四、dataset兼容性处理 如果不支持dataset,有必要做一下兼容性处理 @@ -99,6 +100,6 @@ myDiv.dataset.happy="ok" ``` 然后利用chrome调试,在console命令行分别输入3条指令,结果如下图 -![实验结果](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-12-05%20下午10.19.04.png) +![实验结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-12-05%20下午10.19.04.png) 可以看出来,dataset后跟的属性是驼峰命名原则,如果多个单词第二个单词首字母需要大写,检查元素可以看到神奇的变化。 diff --git a/第二部分 Web 前端/2.7.md b/Chapter2 - Web FrontEnd/2.7.md similarity index 99% rename from 第二部分 Web 前端/2.7.md rename to Chapter2 - Web FrontEnd/2.7.md index 218823d..62c6949 100644 --- a/第二部分 Web 前端/2.7.md +++ b/Chapter2 - Web FrontEnd/2.7.md @@ -1,5 +1,5 @@ +# 原型继承 -### 原型继承 JS中的继承跟其他面向对象语言的继承有点不太一样,要正确的了解JS中的继承还得了解JS的前世今生。 #### 一、从JS的诞生之初说起 diff --git a/第二部分 Web 前端/2.8.md b/Chapter2 - Web FrontEnd/2.8.md similarity index 95% rename from 第二部分 Web 前端/2.8.md rename to Chapter2 - Web FrontEnd/2.8.md index 9d6b7f2..5c7d3b2 100644 --- a/第二部分 Web 前端/2.8.md +++ b/Chapter2 - Web FrontEnd/2.8.md @@ -1,3 +1,4 @@ +# JSON的一些骚操作 * 将json转换为对象JSON.parse\(\)函数的第二个参数用来转换解析出的属性 diff --git a/Chapter2 - Web FrontEnd/2.9.md b/Chapter2 - Web FrontEnd/2.9.md new file mode 100644 index 0000000..ec6e05a --- /dev/null +++ b/Chapter2 - Web FrontEnd/2.9.md @@ -0,0 +1,641 @@ +# Vue 技术的小技巧 + + + +1. Vue 项目开发中经常遇到事件阻止冒泡的需求。在传统的写法上就是调用 `event.preventDefault()、event.stopPropagation()`。尽管我们可以在 methods 里面这样写,但是更好的规范是:methods 只做数据的逻辑处理,而不是去处理 DOM 相关的细节(阻止事件冒泡) + +2. 为了解决这个问题, Vue 为 v-on 提供了事件修饰符。通过`.`表示的指令后缀来调用修饰符 +- .stop +- .prevent +- .capture +- .self +- .once +```Javascript + + + + +
    + + + + + +
    + + +
    ...
    + + +
    ...
    +``` + +3. 给 props 属性设置多个类型 + +props 在开发组件的时候几乎是必用,为了更好的容错处理,比如给自定义的 button 上暴露一个 width 属性 + +`flexiable-button` +``` + + +export default { + prop: { + width: [String, Number], + default: '100px' + }, + computed: { + computedWidth () { + let attr = {} + if (typeof this.width === 'String') { + attr.width = this.width + } + if (typeof this.width === 'Number') { + attr.width = this.width + 'px' + } + return attr + } + } +} +``` +`在其他组件里面使用` +``` + +``` + + +4. data 初始化 + +因为 props 要比 data 先初始化完成,所以我们可以用 props 对 data 完成初始化操作 + +``` +export default { + data () { + return { + buttonSize: this.size + } + }, + props: { + size: String + } +} +``` + +除了以上,子组件的 `data` 函数还可以有参数(当前实例对象),我们可以用这一点做一些自己的判断 + +``` +export default { + data (vm) { + return { + buttonSize: vm.size + } + }, + props: { + size: String + } +} +``` + +5. template + +我们经常需要条件渲染或者循环展示,当 `v-for` 遇上 `v-if` 会结合产生什么故事呢? +之前有一个需求,在个人中心模块展示「收藏列表」。收藏列表分为很多类:公司、个人、商品、类别等等。当用户点击某个大的类别的时候再去展示相应的列表。当然想到的是 v-for 和 v-if。但是当直接操作 ul 加上 v-if 和 v-for 之后发现结果并不符合预期,最后想到的是 `template`。这样子解决了我们的需求 + +``` + + +``` + +6. lifecyele hook + +- 生命周期钩子可以是一个数组类型,且数组中的函数会依次执行 + +- 生命周期的钩子函数还可以作用于 DOM 元素上,利用这一点,我们可以用父组件中的方法来初始化子组件的生命周期钩子 + +下面的例子结合了上面2个知识点 + +`company-basic-info.vue子组件` +``` +created: [ + function callA () { + console.log('子组件-callA') + }, + function callB () { + console.log('子组件-callB') + }, + function callC () { + console.log('子组件-callC') + } +] +``` + +`company-detail.vue父组件` +``` + + + +``` +``` +子组件-callA +company-basic-info.vue?58e9:85 子组件-callB +company-basic-info.vue?58e9:88 子组件-callC +company-detail.vue?8308:103 我观察到子组件创建了 +``` + + +7.render 函数 + +``` + +``` + +用 render 函数实现 + +``` +export default { + render (h) { + let _c = h + return _c('div', + { class: 'box'}, + [_c('h2', {}, 'title'), 'this is content']) + } +} +``` + +事实上,Vue 会把模版(template)编译成渲染函数(render)。上面的模版会被编译成类似下面的渲染函数 + +``` +let render = function () { + return _c('div', + {staticClass:"box"}, + [_c('h2', [_v("title")]), _v("this is content")]) +} +``` + +官方解释说 render 函数比 template 更加接近编译器。 + +template -> 预编译工具(vue-loader、vue-template-compile)-> render -> resolve vnode + +![Vue 生命周期](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-13-lifecycle.png) + +渲染函数的好处: + + +8. errorCaptured + +它会在捕获一个来自子孙组件的错误时被调用。有时候我们想收集错误日志,却不想把错误信息暴露到浏览器控制台,利用它可以实现。 + + +`company-basic-info.vue`子组件报错 +``` +mounted () { + this.$nextTick(() => { + consol.log('我是故意发生的错误') + }) +} +``` + +`company.vue`父组件拦截到错误 + +``` +methods: { + errorCaptured (err, vm, info) { + console.log(err) + console.log(vm) + console.log(info) + //return false + //不 return false 的话报错信息继续向上冒泡 + } +} +``` + +`App.vue`拦截错误 + +``` +errorCaptured (err, vm, info) { + console.log('AppVue' + err) + console.log('vm' + vm) + console.log('info' + info) +} +``` + +9. v-once + +v-once 创建低开销的静态组件。渲染普通的 HTML 元素在 Vue 中是非常快的,但是有的时候你可能需要一个包含了大量内容的组件。你可以在**根元素上**添加 `v-once` 特性以确保这些内容只计算了一次然后缓存起来。 + +``` + +``` + +10. slot-scope + +作用域插槽,当我们用过很多框架就知道很多设计的第三方组件都支持插槽。用户可以插入自定义的 HTML 内容来自定义使用。 + +插槽也就是 slot,是组件的一块 HTML 模版,这块模版的显不显示、以及怎样显示由父组件决定。所以 slot 关心的问题是「显示不显示」、「怎样显示」 + +由于插槽是一块模版,所以从模版种类划分为为:非插槽模版、插槽模版 + +- 非插槽模版指的是 HTML 模版,指的是 'div、span、ul、table' 这些,非插槽模版的显示与隐藏以及怎样显示由插件自身控制 + +- 插槽模版也就是 slot,是一个空壳子,因为它的显示于隐藏以及最后用什么样的 HTML 模版显示由父控件自身控制。但是插槽的显示位置却由子组件自身决定, slot 写的组件 template 的哪块,父组件传过来的模版将来就显示在哪块。 + +### 单个插槽 | 默认插槽 | 匿名插槽 + +单个插槽是 Vue 官方的叫法,但其实它也被叫做「默认插槽」。与「具名插槽」相对应。我们可以叫做「匿名插槽」,因为它不用设置 name 属性 +单个插槽可以放在组件的任何位置。但是见名知意,一个组件中只可以存在一个匿名插槽。相对应具名插槽就可以有很多个,但是必须设置名字(name属性)不同就可以了。 +父组件 +``` + +``` + +子组件 +``` + +``` +在父组件里面写了 html 模版所哟子组件匿名插槽这块模版被使用了。最终结果 +![效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-14-slot1.png) + +### 具名插槽 +匿名插槽没有设置 name 属性,所以是匿名插槽。那么插槽添加了 name 属性,那么就变成了「具名插槽」。具名插槽可以在一个组件中出现 n次。 + +父组件 +``` + +``` +子组件 +``` + +``` + +![效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-14-slot2.png) + +父组件通过 html 模版上的 slot 属性关联具名插槽,没有 slot 属性的 html 模版默认关联匿名插槽 + + +### 作用域插槽 | 带数据的插槽 + +Vue 官方叫做作用域插槽。对比前面几种情况我们可以叫做「带数据的插槽」。作用域插槽要求在 slot 上面绑定数据 + +``` + + export default { + data: function(){ + return { + data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba'] + } + }, +} +``` + +11. 组件之间的通信 + +- 父->子组件之间的通信 +> 父向子组件传递数据通过 props + +``` +//父 + +//子 + + +``` + +- 子->父组件之间的通信 +> 通过 $emit 和 $on 传递数据 + +``` +//父 + + + + +//子 + + + +``` + +- 非父子组件 +> 可以通过一个空的 Vue 实例作为中央事件总线(也可以使用当前的 App 实例,不需要额外创建) + +方式1 +``` +import Vue from 'Vue' +export default new Vue() +``` +方式2 +``` +window.VueInstance = new Vue({ + el: '#app', + router, + store, + components: { App }, + template: '', + beforeCreate () { + Vue.prototype.bus = this + }, + mounted () { + this.$nextTick(function () { + GlobalUtil.util.setConsole() + }) + } +}) +//传递数据 +this.bus.$emit('HotSearchChange', itemIndex) +//接收数据 +this.bus.$on('HotSearchChange', (itemIndex) => { + this.fetchHotSearchWords(itemIndex) +}) +``` + +12. 路由切换判断是否需要登录 + +我们在做前端项目的时候经常需要添加路由设置。其中有个需求就是当你可能跳转一个页面的时候需要判断用户是否登录,如果登录的话直接跳转,未登录先跳转到登录页。 + +> 我们可能想到的是监听 Router 的变化,在跳转前判断,如果用户是登录状态直接跳转;用户未登录状态则先跳转到登陆页,登录成功后回退到之前的页面。 +Vue 的 router 已经提供了这样的功能 + + +- 在 main.js 里面添加监听、判断的逻辑 + +``` +/* 控制判断哪些页面需要用户登录才可以访问 */ +router.beforeEach((to, from, next) => { + if (to.matched.some(record => record.meta.requireAuth)) { + if (!window.VueInstance.$store.getters.getUserInfo) { + next({ + path: '/Auth/login', + query: {redirect: to.fullPath} // 将跳转的路由path作为参数,登录成功后跳转到该路由 + }) + } else { + if (!window.VueInstance.$store.getters.getUserInfo.uid && !window.VueInstance.$store.getters.getUserInfo.token) { + next({ + path: '/Auth/login', + query: {redirect: to.fullPath} // 将跳转的路由path作为参数,登录成功后跳转到该路由 + }) + } else { + next() + } + } + } else { + next() + } +}) +``` + +- 在 router 的 index.js 里面为需要登录才可以访问的页面添加配置项 + +``` +let routes = [ + { path: '/SearchCompany', name: 'SearchCompany', component: SearchCompany, meta: {requireAuth: true} }, + //... +] +``` + +- 在登录页为登录成功后做判断处理 + +``` +if (this.$route.query.redirect) { + this.$router.push({path: this.$route.query.redirect}) +} else { + this.$router.push({path: '/' }) +} +``` + +13. Vue 上传文件 + +``` +if (document.getElementById("bidFile").files[0]) { + const formData = new FormData() + formData.append('file', document.getElementById("bidFile").files[0]) + formData.append('uid', this.$store.getters.getUserInfo.uid) + formData.append('token', this.$store.getters.getUserInfo.token) + formData.append('project_id', this.project_id) + let loader = this.$loading.show({ + loader: 'dots', + container: this.fullPage ? null : this.$refs.formContainer, + canCancel: true, + onCancel: this.onCancel, + }) + fetch.bhfilepost('order/upBidingFile',formData).then((res) => { + loader.hide() + if (res.status !== -404) { + this.selectedProjectInfo = res.data.data + } + }) +} +``` + +``` +//fetch.js +bhfilepost (url, params) { + Axios.defaults.baseURL = 'https://bh.datacubr.com' + params['key'] = key + return Axios({ + method: 'post', + url, + data: params, + headers: { + 'Content-Type': ' multipart/form-data' + } + }).then((response) => { + return checkStatus(response) + }).then((res) => { + return checkCode(res) + }) +}, + +``` + + +14. Vue 以base64形式下载pdf/压缩包也可以 +压缩包的话,`content-type` 设置为 `type: "application/zip"` + +``` +base64ToArrayBuffer(data) { + let binaryString = window.atob(data) + let binaryLen = binaryString.length + let bytes = new Uint8Array(binaryLen) + for (let i = 0; i < binaryLen; i++) { + let ascii = binaryString.charCodeAt(i) + bytes[i] = ascii + } + return bytes +} + +handleDownload (filename, operationname) { + let loader = this.$loading.show({ + loader: 'dots', + container: this.fullPage ? null : this.$refs.formContainer, + canCancel: true, + onCancel: this.onCancel, + }) + fetch.bhpost('document/templateTmp',{ + type: operationname, + uid: this.$store.getters.getUserInfo.uid, + token: this.$store.getters.getUserInfo.token, + order_no: this.order_no + }).then((res) => { + loader.hide() + if (res.status === 200) { + var arrBuffer = this.base64ToArrayBuffer(res.data.data.info) + // It is necessary to create a new blob object with mime-type explicitly set + // otherwise only Chrome works like it should + var newBlob = new Blob([arrBuffer], { type: "application/pdf" }) + // IE doesn't allow using a blob object directly as link href + // instead it is necessary to use msSaveOrOpenBlob + if (window.navigator && window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveOrOpenBlob(newBlob) + return + } + // For other browsers: + // Create a link pointing to the ObjectURL containing the blob. + var data = window.URL.createObjectURL(newBlob) + var link = document.createElement('a') + document.body.appendChild(link) //required in FF, optional for Chrome + link.href = data + link.download = filename + ".pdf" + link.click() + window.URL.revokeObjectURL(data) + link.remove() + } + }) +}, +``` + +15. 推荐几个好用的库 + +最近用 Vue 全家桶和 Bootstrap-Vue 框架做了一版网站。所以有一些好用的包或者组件分享一下。 + +- [cxlt-vue2-toastr](https://www.npmjs.com/package/cxlt-vue2-toastr) + 说明:cxlt-vue2-toastr是弹出提示的Vue2组件,基于toastr的样式和animate.css的动画效果。 +- [vue-photo-preview](https://www.npmjs.com/package/vue-photo-preview) + 说明:可以全屏、切换、预览、文字说明的图片查看器 +- [animate.css](https://www.npmjs.com/package/animate.css) + 说明:基本上 JS 可以实现的动画效果基本都可以实现。蛮强大的 +- [axios](https://www.npmjs.com/package/axios) + 说明:支持 promise、可以跑在浏览器和 Node 环境、可以拦截请求和响应头 +- [vee-validate](https://www.npmjs.com/package/vee-validate) + 说明:一个校验的包,内置蛮多规则,你也可以自定义,支持国际化 + diff --git a/Chapter2 - Web FrontEnd/chapter2.md b/Chapter2 - Web FrontEnd/chapter2.md new file mode 100644 index 0000000..2b29059 --- /dev/null +++ b/Chapter2 - Web FrontEnd/chapter2.md @@ -0,0 +1,44 @@ +# 第二部分 + +第二部分主要介绍 Web 前端开发中遇到的问题或者有趣的知识 + + * [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md) + * [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md) + * [3、CSS-埋点统计](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.3.md) + * [4、generator函数](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.4.md) + * [5、H5性能优化方面的探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.5.md) + * [6、h5自定义对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.6.md) + * [7、Javascript-prototype](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.7.md) + * [8、JSON的一些骚操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.8.md) + * [9、Vue开发小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.9.md) + * [10、Vue-devtools](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.10.md) + * [11、H5页面保存页面为图片](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.11.md) + * [12、Promise](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.12.md) + * [13、webpack-dev-server 的配置和使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.13.md) + * [14、Web 与 H5 交互的坑](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.14.md) + * [15、前端持久化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.15.md) + * [16、VS-Code](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.16.md) + * [17、Vue tips](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.17.md) + * [18、Web反爬虫技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.18.md) + * [19、webpack小集](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.19.md) + * [20、大前端](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.20.md) + * [21、canvas](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.21.md) + * [21、动画控制](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.22.md) + * [23、打造自己的图标字体文件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.23.md) + * [24、Chrome 调试技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.24.md) + * [25、大前端动画](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.25.md) + * [26、Linux 下安装 Node](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.26.md) + * [27、浏览器不通窗口之间的通信之道](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.27.md) + * [28、神器Puppeteer](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.28.md) + * [29、从Vue.js谈谈前端开发的技术栈演变](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.29.md) + * [30、Javascript 常用工具封装](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.30.md) + * [31、浏览器布局与DOM绘制](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.31.md) + * [32、React核心技术剖析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.32.md) + * [33、ES6学习总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.33.md) + * [34、富文本编辑器的原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.34.md) + * [35、Vue3 核心技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.35.md) + * [36、immutable.js 与 React](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.36.md) + * [37、前端工程化与 CI、CD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.37.md) + * [38、npm 改进之工程化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.38.md) + * [39、前端模块化演进之路](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.39.md) + diff --git a/第三部分 后端技术/3.1.md b/Chapter3 - Server/3.1.md similarity index 99% rename from 第三部分 后端技术/3.1.md rename to Chapter3 - Server/3.1.md index 64a443a..e4ace52 100644 --- a/第三部分 后端技术/3.1.md +++ b/Chapter3 - Server/3.1.md @@ -1,4 +1,4 @@ - +# 利用分页和模糊查询技术实现一个App接口 摘要: 模糊查询技术、分页技术、App接口 diff --git a/第三部分 后端技术/3.2.md b/Chapter3 - Server/3.2.md similarity index 88% rename from 第三部分 后端技术/3.2.md rename to Chapter3 - Server/3.2.md index 4db2fad..91c4161 100644 --- a/第三部分 后端技术/3.2.md +++ b/Chapter3 - Server/3.2.md @@ -8,13 +8,13 @@ -![淘宝扫码登录](https://github.com/FantasticLBP/knowledge-kit/blob/master/第三部分%20后端技术/assets/6693515-d566f3006b82c505.jpeg?raw=true) +![淘宝扫码登录](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/6693515-d566f3006b82c505.jpeg) -![QQ扫码登录](https://github.com/FantasticLBP/knowledge-kit/blob/master/第三部分%20后端技术/assets/6693515-3b17bdcc22e1a5a3.png?raw=true) +![QQ扫码登录](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/6693515-3b17bdcc22e1a5a3.png) @@ -42,5 +42,5 @@ **登录原理图如下:** -![扫码登录流程图](https://github.com/FantasticLBP/knowledge-kit/blob/master/第三部分%20后端技术/assets/6693515-8e5bea48462a7904.jpeg?raw=true) +![扫码登录流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/6693515-8e5bea48462a7904.jpeg) diff --git a/Chapter3 - Server/3.3.md b/Chapter3 - Server/3.3.md new file mode 100644 index 0000000..ac185fc --- /dev/null +++ b/Chapter3 - Server/3.3.md @@ -0,0 +1,97 @@ +# 从 Node.js 看看服务端框架的一些感想 + +> 为什么写这篇文章?因为早上在思考一个问题「想获取一家公司的数据(内容型公司),反爬措施做的比较好(VIP会员制度,访问次数太多会锁掉账号)。有几个方向想去尝试:1.App 逆向破解看看网络请求部分的参数是如何生成的;2.Charles 抓包破解参数部分看看能否模拟;3.查看小程序是否有漏洞。最后想来想去还是算了,因为反爬虫措施即使破解了,但是当请求的次数较多的时候还是会封锁 VIP 账号。最后想的是找出封锁账号的请求次数的临界值,然后用爬虫手段去获取数据,但是不能超过临界值就可以。因为 VIP 账号价格实在太高就放弃了。过了几天我想到了用浏览器插件的方式去做这个事情。也就是在对方的网站里面注入我们的 JS 脚本,脚本会在网站上面添加一个按钮,点击按钮就可以将数据同步到我们自己的数据中心。废话说了一大堆,因为 JS 在浏览器环境里面不具备服务端的能力所以想到的是通过接口将数据让一个 Node.js 的服务去处理,将数据入库等操作。问题正式进入主题,要开发微服务的过程中选择用 Node 的 express 还是 Koa 还是 eggjs 等问题困扰了我一会儿。这篇文章不针对这些具体的库进行讨论,而是对于服务端的一些思考」 + +## 优秀的后端框架? + +一个什么样的框架算得上是优秀或者合格?有个需求让你写一个 HTTP 服务,借助于 express 你可能初始化项目、安装依赖、写完代码都用不了6分钟?觉得似乎很简单,哥们儿你想想这是一个服务,而不是说让你能跑就行了。计算机学生的平时作业差不多满足了。但是你说你这个东西能打吗?可以说“战五渣”。一旦部署到线上环境,可能瞬间就被大量涌入的请求击垮,更何况有些人要攻击你。换个角度思考问题。加入你的线上程序需要升级,你该怎么办?停止当前的服务让用户等待一段时间吗? + +所以一个后端服务必须满足2个特性: + +- 容错性强(Fault tolerate) +- 可拓展性高(Scalability) + +其他的特性也很重要,比如程序的健壮性、接口设计友好、代码修改起来灵活等等特性。但是容错性、可拓展性是服务正常运行的基本保障。至少得向用户保证服务是可用的。无论代码写的多优雅它都是为业务所服务的。 + + +## 拓展性(Scalability) + +![拓展性](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-27-Server-scalability.png) + +- X轴:纯粹对服务的实例进行拓展。为了响应更多的请求 +- Y轴:未服务添加新的给你。功能性拓展 +- Z轴:按照业务数据对服务进行拓展 + +## 实例拓展 + + +增加服务实例包括两类:横向拓展、纵向拓展。横向拓展表示利用更多的及其。纵向拓展表示在一台及其上挖掘它的潜力。 + +NodeJS 程序是单进程运行的。32位机器上最多只有 1GB 内存的实用权限(在 64GB 机器上的最大内存权限扩大到 1.7GB)。目前绝大部分线上服务器 CPU 都是多核并且至少 16GB。如此 Node 便无法发挥机器的最大能力。Node 早就意识到这一点,它允许创建多个子进程运行多个实例。 + +![多进程模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-27-Nodecluster.png) + + +有一个主进程 master,但是 master 进程并不实际处理业务逻辑,但是除了业务逻辑之外的事情它都负责。它是 manager,负责启动子进程、管理子进程(如果进程挂到了则需要重启)。同时扮演 Router 的角色,也就是对程序的所有访问请求都先到达主进程,主进程分配请求给子进程 worker(子进程负责处理业务逻辑) + +这个机制下有两条细节需要处理。 +1. 如何把外界的任务平均分配给不同的 worker 处理?这里的平均并不是指数量上的平均(因为每个请求的工作量可能不同)。不能让某个子进程太闲,也不能让某个子进程太忙,而是始终处于工作的状态。也就是「负载均衡(load-balancing)」。默认情况下 Clust 模块采用的是 **round robin** 负载均衡算法,说白了就是依次按照顺序把请求指派给列表上的子进程,到结尾之后重头开始。 + +这个算法只能保证每个子进程收到的请求个数是是平均的。但如果某个进程本来的任务很复杂,后来又由于不断的收到被平均指配的任务,那么这个子进程的压力就很大了。除此之外我们需要考虑超时、重做机制,所以主进程 master 作为路由时不仅仅需要转发请求,还需要智能的分配请求 + + +另一个问题是状态共享问题,假如某个用户第一次访问该服务时是分配给了线程A上的实例A处理,并且用户在这个实例上进行了登陆,而没有过几秒钟之后当用户第二次访问时分配给了线程B上的实例B处理,如果此时用户在A上的登陆状态没有共享给其他实例的话,那么用户不得不重新登陆一次,这样的用户体验是无法接受的。如下图所示 + +![用户信息不共享](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-27-session-share-problem.png) + +解决方案1:将状态共享 + +![解决方案1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-27-session-share-solution01.png) + +解决方案2:新增一个模块专门用于记录用户第一次访问的实例。并在之后当用户访问服务时始终指派访问该实例 + +![解决方案2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-27-session-share-solution02.png) + + +主进程-子进程的模式思路不仅可以用于「纵向拓展」,还适用于「横向拓展」。当单台机器已经无法满足你需求的时候,你可以把单实例子进程的概念拓展为单台机器:我们将在多台机器上部署多个进行实例,用户的访问请求也并非直接到达它们,而是先到达前方的代理机器,它也是负责负载均衡的机器,负责将请求转发给部署了应用实例的机器。这样的模式我们也通常称为反向代理模式: + +![负载均衡模式](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-27-reverse-proxy.png) + +这个模式仍然可以继续改进:动态的启动或者关闭机器上的若干实例用于节省资源、移除负载均衡这一环节用于提高通讯的效率。 + +对于所有的开发来说,很多道理都是通用的。比如设计模块、解耦思想。上面说的负载均衡、反向代理等等不只是 Java、Node、PHP、.Net 等都存在。所以只要是服务端的概念,Node 里面一样存在。(有的 Node 工程师是从前端开发转过来的,所以在此强调。)虽然 Node.js 较新,但是解决思路或者方案可以借鉴传统的服务端方案。跳出语言的限制去看待问题、解决问题、寻找思路和方案 + + +## 功能拓展 + +你也许会问新增功能有什么难点?每个程序员的日常就是不断的进行功能迭代。但在这里我们希望解决一个问题,就是既然我们无法保证功能不会出错,那我们有没有办法保证当一个功能出错之后不会影响整个程序的正常运行?这也是我们所说的容错性。 + +道理都懂,我们都明白程序需要容错,所以try/catch是从编码上解决这个问题。但问题是try/catch不是万能的,万无一失的程序也是不存在的,所以我们要换个思路解决这个问题,我们允许程序出错,但是要及时把错误隔离,并且不再影响程序的运行。这个就要从架构上解决这个问题。例如使用微服务(Microservices)架构。 + +在介绍微服务架构之前,我们要了解其它架构为什么没法满足我们的要求。例如我们常用的单体(monolithic)架构。单体架构这个词你可能不熟悉,但几乎我们每天都在和它打交道,大部分的后端服务都归属于单体架构,对它的解释我翻译Martin Fowler的描述: + +企业级应用通常分为三个部分:用户界面(包含运行在用户浏览器上的html页面和javascript脚本),数据库(通常是包含许多表的关系数据库),和服务端应用。服务端应用将会处理http请求,执行业务逻辑,从数据库中取得数据,生成html视图返回给浏览器。这样的服务端应用就被称为单体(monolith)——单个具有逻辑性的执行过程。任何针对系统的修改都会导致重新构建和部署一个新版本的服务端应用。 + +(注:以上这段描述摘自Martin Fowler的文章Microservices,我认为这是对微架构描述最全面的文章,如果想对这一小节做更深入的了解可以把这篇文章细读。 这也是我读到的Martin Fowler所写的文章中最通俗的文章。个人认为Martin Fowler的文章读起来比较晦涩,John Resig紧随其后) + +单体架构是一种很自然的搭建应用的方式,它符合我们对业务处理流程的认知。但单体应用也存在问题:任何一处,无论大小的修改都会导致整个应用被重新构建和重新部署。随着应用规模和复杂性的不断增大,参与维护的人数增多,每一轮迭代修改的模块增多,对上线来说是极大的考验,对于内部单个模块的拓展也是极为不利的。例如当图片压缩请求剧增时,需要新增图片压缩模块的实例,但实际上不得不扩展整个单体应用的实例。 + +微服务架构解决的就是这一系列问题。顾名思义,微服务架构下软件是由多个独立的服务组成。这些服务相互独立互不干预。以拆分上面所说的单体应用为例,我们可以把处理HTTP请求的模块和负责数据库读写的模块分离出来成为独立的服务,这两个模块从功能上看是没有任何交集。这样的好处就是,我们可以独立的部署,拓展,修改这些服务。例如应用需要添加新的接口时,我们只需要修改处理HTTP请求的服务,只公开这部分代码给修改者,只上线这部分服务,拓展时也只需要新添这部分服务的实例。 + +微服务和我们通常编写的模块(以文件为单位,以命名空间为单位)相比更加独立,更像是一个五脏俱全的“小应用”,如果你读完了我之前推荐的Martin Fowler关于微服务的文章的话,你会对这点更深有感触:微服务除了在运维上独立以外,它还可以拥有独立的数据库,还应该配备独立的团队维护。它甚至可以允许使用其他的语言进行开发,只要对外接口正常即可。 + +当然微服务也存在不足,例如如何将诸多的微服务在大型架构中组织起来,如何提高不同服务之间的通信效率都是需要在实际工作中解决的问题。 + +微服务说到底还是解耦思想的实践。从这个意义上来说,React下的Flux架构某种意义上也属于微服务。如果你了解Flux的起源的话,Flux架构其实来源于后端的CQRS,即Command Query Responsibility Segregation,命令与查询职责分离,也就是将数据的读操作和写操作分离开。这么设计的理由有很多,举例说一点:在许多业务场景中,数据的读和写的次数是不平衡,可能上千次的读操作才对应一次写操作,比如机票余票信息的查询和更新。所以把读和写操作分开能够有针对性的分别优化它们。例如提高程序的scalability,scalability意味着我们能够在部署程序时,给读操作和写操作部署不同数量的线上实例来满足实际的需求。 + + +![微服务架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-28-CQRS.png) + +如果你也有Unity编程经验的话会对解耦更有感触,在Unity中我们已经不能称之为解耦,而是自治,这是Unity的设计模式。举个例子,屏幕上少则可能有十几个游戏元素,例如玩家、敌人还有子弹。你必须为它们编写“死亡”的规则,“诞生”的规则,交互的规则。因为你根本无法预料玩家在何时何种位置发射出子弹,也无法预料子弹何时在什么位置碰撞上什么状态敌人。所以你只能让它们在规则下自由发挥。这和微服务有异曲同工之妙:独立,隔离,自治。 + +## 总结 + +Node 作为服务端的新人,应该学习前辈的经验。借用奔驰广告的一句话:经典是对经典的继承、经典是对经典的背叛。只有站在前人的肩膀上,我们才有可能创新,看的更远 + + +(以上文章部分参考自网络,因为本人看到后相见恨晚,和我思想观念一致,所以搬运总结于此,望共勉) \ No newline at end of file diff --git a/Chapter3 - Server/3.4.md b/Chapter3 - Server/3.4.md new file mode 100644 index 0000000..07f60a2 --- /dev/null +++ b/Chapter3 - Server/3.4.md @@ -0,0 +1,5 @@ +# Let's Encrypt + +https://imququ.com/post/letsencrypt-certificate.html + +可以创建免费的HTTPS证书 \ No newline at end of file diff --git a/Chapter3 - Server/3.5.md b/Chapter3 - Server/3.5.md new file mode 100644 index 0000000..11ed220 --- /dev/null +++ b/Chapter3 - Server/3.5.md @@ -0,0 +1,19 @@ +# PHP、Mysql 乱码 + + +- 查看 Mysql 数据表字符编码 +- 查看表字段编码 +- 查看 PDO 连接编码 +- 查看 Mysql 数据库服务器编码 + +这次吃亏就是在 Mysql 数据库服务器编码导致。 + +- 查看数据库服务器编码 +```SQL +show variables like 'character%'; +``` + +所以知道其他编码都正常,问题定位到数据库服务器编码问题。 +- vim /etc/my.cnf +- character-set-server=utf8 +- 重启服务器 \ No newline at end of file diff --git a/Chapter3 - Server/3.6.md b/Chapter3 - Server/3.6.md new file mode 100644 index 0000000..17f8f20 --- /dev/null +++ b/Chapter3 - Server/3.6.md @@ -0,0 +1,377 @@ +# YAML + +我司基础平台技术组整合了一套 cli 和相关工具,React Native、iOS、Android、Node、前端项目统一进行依赖管理、打包、提测、发布等,项目工程中有 `.yml` 文件,不太了解作用,所以本文对其进行了解和学习。 + +## 一、 YAML 是什么 + +> YAML (a recursive acronym for "YAML Ain't Markup Language") is a human-readable data-serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted. YAML targets many of the same communications applications as Extensible Markup Language (XML) but has a minimal syntax which intentionally differs from SGML .[1] It uses both Python-style indentation to indicate nesting, and a more compact format that uses [] for lists and {} for maps[1] making YAML 1.2 a superset of JSON.[2] ~[维基百科](https://en.wikipedia.org/wiki/YAML) + + +编程难免需要写配置文件,怎么写配置有很多种方式,XML、JSON、YAML... +YAML 是用来专门写配置文件的语言,非常简洁、强大,比 JSON 格式更简洁。 +YAML 是一种人类可读的数据序列化语言,通常用于配置文件和用于存储或传输数据的应用程序中。 + +基本语法: + +- 大小写敏感 +- 使用缩进表示层级关系 +- 缩进时不允许使用 Tab 键,只允许使用空格键 +- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 + +`#` 表示注释,从这个字符串开始到这一行结束,都会被解析器认为是注释而忽略 + +YAML 支持3种数据结构 + +- 对象:键值对的集合,又称为映射(mapping)、哈希(hashes)、字典(dictionary) +- 数组:一组按次序排列的值,又称为序列(sequence)/列表(list) +- 纯量:单个的、不可再分的值 + + +## 二、对象 + +对象:一组键值对,用冒号结构表示 + +```yaml +hobby: coding +``` + +转换为 Javascript + +```Javascript +{ hobby: 'coding' } +``` + +Yaml 也允许另一种写法,将所有键值对写成一个行内对象 + +```yaml +person: { age: 24, hobby: coding, title: 资深工程师 } +``` + +转换为 Javascript + +```Javascript +{ person: { age: 24, hobby: 'coding', title: '资深工程师' } } +``` + + +## 三、数组 + +一组连词线开头的行,构成一个数组 + +```yml +- movie +- coding +- music +- bicycle +``` + +转换为 Javascript + +```Javascript +[ 'movie', 'coding', 'music', 'bicycle' ] +``` + +数据结构的子成员是一个数组,则可以在该项下面缩进一个空格 + +```yml +- music +- coding + - swift + - Javascript + - shell + - php + - ruby + - python +- cat +- movie +``` + +转换为 Javascript + +```Javascript +['music', ['swift', 'Javascript', 'shell', 'php', 'ruby', 'python'], 'cat', 'movie'] +``` + +数组也可以用行内表示法 + +```yml +coding: [swift, Javascript, shell, php, ruby, python] +``` + +转换为 Javascript + +```Javascript +{ coding: ['swift', 'Javascript', 'shell', 'php', 'ruby', 'python']} +``` + + +## 四、复合结构 + +对象和数组可以结合使用,形成复合结构。 + + +```yml +hobby: + - coding + - music + - movie + - bicycle +skills: + swift: iOS + ruby: cocoapod + php: server + python: spider +``` + +转换为 Javascript + +```Javascript +{ + hobby: ['coding', 'music', 'movie', 'bicycle'], + skills: { + swift: 'iOS', + ruby: 'cocoapod', + php: 'server', + python: 'spider' + } +} +``` + +## 五、纯量 + +纯量是最基本、不可再分的值。以下数据类型都属于 Javascript 的纯量 + +- 字符串 +- 布尔值 +- null +- 整数 +- 浮点数 +- 日期 +- 时间 + +数值直接以字面量的形式表示: + +```yml +number: 12.30 +``` + + + +转换为 Javascript + +```Javascript +{ number: 12.30} +``` + +布尔值用 true 和 false 表示 +```yml +isDeveloper: true +``` + +转换为 Javascript + +```Javascript +{ isDeveloper: true} +``` + +null 用 ~ 表示 + +```yml +pet: ~ +``` + +转换为 Javascript + +```Javascript +{ pet: null } +``` + +时间采用 ISO8601 格式 + +```yml +iso8601: 2019-11-27-14t21:59:43.10-05:00 +``` + +转换为 Javascript + +```Javascript +{ iso8601: new Date('2019-11-27-14t21:59:43.10-05:00') } +``` + +YAML 允许使用2个感叹号,强制数据类型转换。 + +```yml +age: !!str 24 +isDeveloper: !!str true +``` + +转换为 Javascript + +```Javascript +{ age: '24', isDeveloper: 'true' } +``` + +## 六、字符串 + +字符串是 YAML 中最常见,也是最复杂的一种数据类型。 + +字符串默认不使用引号表示。 + +```yml +str: 这是一行字符串 +``` + +转换为 Javascript + +```Javascript +{ str: '这是一行字符串' } +``` + +如果字符串之中包含空格或特殊字符,需要放在引号之中。 + +```yml +str: '内容: 字符串' +``` + +转换为 Javascript + +```Javascript +{ str: '内容: 字符串' } +``` + +单引号和双引号都可以使用,双引号不会对特殊字符转义。 + +```yml +s1: '姓名\n杭城小刘' +s2: "姓名\n杭城小刘" +``` + +转换为 Javascript + +```Javascript +{ s1: "姓名\\n杭城小刘", s2: "姓名\n杭城小刘" } +``` + +单引号之中如果还有单引号,必须连续使用2个单引号转义 + +```yml +str: 'labor''s day' +``` + +转换为 Javascript + +```Javascript +{ s1: "labor's day" } +``` + + +字符串可以写成多行,从第二行开始,必须有一个单空格缩进。换行符会被转为空格。 + +```yml +str: 这是第一段 + 多行 + 字符串 +``` + +转换为 Javascript + +```Javascript +{ str: "这是第一段 多行 字符串" } +``` + + +多行字符串可以使用 | 保留换行符,也可以使用 > 折叠换行。 + +```yml +this: | + Foo + Bar +that: > + Foo + Bar +``` + +转换为 Javascript + +```Javascript +{ this: "Foo\nBar\n", that: "Foo Bar\n" } +``` + ++ 表示保留文字末尾的换行,- 表示删除字符串末尾的换行 + +```yml +name: | + LBP +hobby: |+ + Coding + Movie + + +sports: |- + Bicycle + +``` + +转换为 Javascript + +```Javascript +{ name: "LBP\n", hobby: "Coding\nMovie\n\n\n", sports: "Bicycle" } +``` + +字符串之中还可以插入 HTML 标记 + +```yml +message: | + +

    + yaml +

    +``` + +转换为 Javascript + +```Javascript +{message: "\n

    \n yaml\n

    \n"} +``` + + +## 七、引用 + +锚点 `&` 和别名 `*` 用来表示引用 + +```yml +engineer: &engineer + name: 杭城小刘 + age: 24 + title: 资深工程师 +iOSer: + kind: iOS + <<: *engineer +``` + +等同于下面的 yml 代码 + +```yml +engineer: + name: 杭城小刘 + age: 24 + title: 资深工程师 +iOSer: + kind: iOS + name: 杭城小刘 + age: 24 + title: 资深工程师 +``` + +其中: `<<` 表示合并到当前数据,`&` 用来建立锚点, `*` 用来表示引用锚点。 + + +## 其他 + +1. 有个 [NodeJS 库](https://github.com/nodeca/js-yaml)可以将 YAML 函数和正则转换为 JS 对象。反过来也可以 +2. [Ruby](https://yaml.org/YAML_for_ruby.html) 可以将 YAML 转换为 Ruby,反过来也可以 + + +参考链接: + +- [YAML 语言教程:阮一峰](http://www.ruanyifeng.com/blog/2016/07/yaml.html) \ No newline at end of file diff --git a/Chapter3 - Server/3.7.md b/Chapter3 - Server/3.7.md new file mode 100644 index 0000000..f143d6e --- /dev/null +++ b/Chapter3 - Server/3.7.md @@ -0,0 +1,126 @@ +# Node 单元测试 + +## 为什么做这个事情 + +我在我写的文章里面多次提到单元测试的重要性。重要的事情说三遍“单测很重要”、“单测很重要”、“单测很重要”。 +单纯说这句话没公信力和权威性,那我举例子来说明吧。 + +场景1 + +某业务线在不断的版本迭代,在版本6的时候发现功能 A 的代码太乱太多了。小刘同学打算重构,他辛辛苦苦解决后,打算提交给测试工程师进行测试。测试工程师说“小刘,你这个代码全是 Bug 呀,我点进去就 Crash”。小刘听到后很尴尬,心里想“功能 A 的代码我写的很小心,一行行检查过去的,不可能有问题”。测试说“我点击商品页,点击加入购物车,马上 Crash 了”。你这个测试被打回,阻塞主流程了。小刘想了想才发现他自己开发的模块是没有问题的,但是他在重构功能 A 的时候不小心把依赖功能 A 的地方少传递了配置参数 😅 + +场景2 + +某公司基础普通组有小刘同学,在设计某个新技术方案的时候,辛辛苦苦花了3天时间出了技术方案,他去找老板聊,老板让他把思路描述下,再把设计的测试用例给一下。小刘吞吞吐吐讲完了设计思路,但是他说还没有设计测试用例。老板说你没测试用例,我怎么 review 你的设计,一行行看代码理解逻辑吗?一句句听你的设计判断有没有问题吗?以后你找我听技术设计,你理好设计思路,和测试用例。我看看主流程和一些边界的输入输出,确保这些东西正确那就是没问题的。我没有那么多时间一行行看代码。(说的也是,组长是 P9 忙得很) + + +场景3 + +某公司基础普通组有小刘同学在做了移动端的 APM 监控和数据上报 SDK 的第一版,但是他很乖,写好了单元测试。忘了说了小刘同学是负责 iOS 端的,同事做 Android 端的小张同学和他对应,不同就是他没有写单元测试的代码。 需求下来了,需要迭代版本2,小刘和小张都开发好代码了,需要进行测试。哈哈哈小刘乐坏了,他花了0.5天就测试结束,小张花了1.5天进行测试。为什么呢?小刘写代码都要写单元测试代码,小张不写。虽然写单元测试代码可能需要花一点点时间。但是当新版本迭代的时候就不需要回头继续**全量测试**。他只要按下 `Command + U`, Xcode 就会跑单元测试相关的代码。 + + +那单元测试的好处?😂 什么?你还问好处,上面那么清晰明了,那我再总结下: +- 确保你写的代码的每个分支都可以被覆盖,防止线上代在用户端,不小心执行到未知的分支里面 +- 在进行新版本迭代或者重构的时候,可以集中精力到新逻辑里面,旧的逻辑可以用 UT 测试覆盖率来确保 +- 在团队内进行 code review 或者 merge review 的时候,review 的人可以看代码中主要逻辑,结合 ut 来判断。 + + +另外,测试来说一般是结合 CI、CD 的,像我们公司有自己的工具 cli 工具, iOS、Android、RN、React、Vue、Node 项目都一起处理,分析依赖、打包构建(打包系统根据工程特点调度特定打包机)、测试、hot fix、埋点统计、APM等等。 + +所以如果公司规模小,就写好 UT 然后结合 lint 做一些处理,公司规模大、开发有能力则需要结合 ci、cd 将测试的能力结合进去。 + + +另外 UT 是一道工序,最好在每个开发者写代码的时候做 MR,团队内 MR +1 数大于3才可以合并到分支,且 +1 的人里面必须有一个同一个项目的同学,必须有一个同技术栈且比你高水平的人,MR 指出的问题修改好才可以合并。且 MR 代码不能太大,因为太大,给你做 MR 的同学会很耗费精力。人家阅读你代码时间成本太大。 + +## Node 侧如何进行 UT 开发 + +Node 在大学三年级的时候就听说了,也写过,之前也用来写过爬虫、自动化脚本、cli 等,之前学习过如何在 Node 侧写 ut,这篇文章用来总结下。 + + + +举个例子,有个 Node 工程,一个模块的主要功能是获取该目录下的所有文件。开发代码如下 + +```javascript +// fetchCodeFiles.js +const fs = require('fs-extra'), + glob = require('glob') + +const fetchCodeFiles = async (dirPath) => { + return new Promise((reslove, reject) => { + glob('**/*.?(sh|pch|json|xcconfig|mm|cpp|h|m)', { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => { + if (err) reject(err) + reslove(files) + }) + }); +} + +module.exports = fetchCodeFiles +``` + +单元测试该怎么做? +1. 在终端下切换到当前工程目录,安装 `npm install yamljs --save` +2. 在工程根目录下新建 `test` 文件夹 +3. 为你需要的开发文件写测试代码。文件命名建议 `模块名称.test.js` +4. 测试代码需要引入 `assert`。 +5. 通过 `describe` 方法、 `it` 方法、`assert` 方法描写测试代码 +6. 为了方便测试,在 `package.json` 文件中的 `scripts` 下添加描述 `"test": "mocha"` +7. 为了更方便,我使用的是 iterm2,在 .zshrc 文件里设置别名 `alias nt="node test"` + +提升效率的配置 .zshrc 可以查看文章: [Mac 终端效率神技](./../Chapter7 - Geek Talk/7.10.md) + +上面开发代码的测试代码如下: + +```javascript +// fetchCodeFiles.test.js +const fetchCodeFiles = require('./../src/fetchCodeFiles'), + assert = require('assert') + +describe("fetch all code files", () => { + describe("fetch all code files", () => { + it("should return 12 when code directory is '/Users/liubinpeng/Workspace/search_key/test/code'", (done) => { + done(assert(fetchCodeFiles('/Users/liubinpeng/Workspace/search_key/test/code')) === 12) + }) + it("should return 4 when code directory is '/Users/liubinpeng/Workspace/search_key/test/code/Classes'", (done) => { + done(assert(fetchCodeFiles('/Users/liubinpeng/Workspace/search_key/test/code/Classes')) === 4) + }) + it("should return 0 when code directory is '/Users/liubinpeng/Workspace/search_key/test/code/EmptyCodeFiles'", (done) => { + done(assert(fetchCodeFiles('/Users/liubinpeng/Workspace/search_key/test/code/EmptyCodeFiles')) === 0) + }) + }) +}) +``` + +```json +// package.json + +{ + "name": "search", + "version": "1.0.0", + "description": "des", + "main": "index.js", + "scripts": { + "test": "mocha", + "start": "node src/index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "fs-extra": "^8.1.0", + "glob": "^7.1.6", + "yamljs": "^0.3.0" + }, + "devDependencies": { + "mocha": "^6.2.2" + } +} +``` + +运行结果 + +![测试结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-12-16-nodeUT.png) + + + +## Mocha + +本文主要想说明的是 UT 的重要性,以及 Node 测如何做单元测试。当然 UT 没这么简单,具体深入的不是本文的终点,感兴趣的可以查看 Mocha 这个项目的[官方文档](https://mochajs.org)。 \ No newline at end of file diff --git a/Chapter3 - Server/3.8.md b/Chapter3 - Server/3.8.md new file mode 100644 index 0000000..ddffc81 --- /dev/null +++ b/Chapter3 - Server/3.8.md @@ -0,0 +1,112 @@ +# 数据安全(反爬虫)之「防重放」策略 + +> 在[大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)一文中讲了 Web 前端和 Native 客户端如何从数据安全层面做反爬虫策略,本文接着之前的背景,将从 **API 数据接口**的层面讲一种技术方案,实现数据安全。 + + + + +## 一、 API 接口请求安全性问题 + +API 接口存在很多常见的安全性问题,常见的有下面几种情况 +1. 即使采用 HTTPS,诸如 Charles、Wireshark 之类的专业抓包工具可以扮演证书颁发、校验的角色,因此可以查看到数据 +2. 拿到请求信息后原封不动的发起第二个请求,在服务器上生产了部分脏数据(接口是背后的逻辑是对 DB 的数据插入、删除等) + + +所以针对上述的问题也有一些解决方案: +1. HTTPS 证书的双向认证解决抓包工具问题 +2. 假如通过网络层高手截获了 HTTPS 加证书认证后的数据,所以需要对请求参数做签名 +2. 「防重放策略」解决请求的多次发起问题 + +关于 HTTPS 证书双向认证和 Web 端反爬虫技术方案均在[大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)一文中有具体讲解。接下来引出本文主角:防重放 + + + + +## 二、 请求参数防篡改 + +在之前的文章也讲过,HTTPS 依旧可以被抓包,造成安全问题。抓包工具下数据依旧是裸奔的,可以查看[Charles 从入门到精通](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.2.md)文中讲的如何获取 HTTPS 数据。 + +假如通过网络层高手截获了 HTTPS 加证书认证后的数据,所以需要对请求参数做签名。步骤如下 +- 客户端使用约定好的密钥对请求参数进行加密,得到签名 signature。并将签名加入到请求参数中,发送给服务端 +- 服务端接收到客户端请求,使用约定好的密钥对请求参数(不包括 signature)进行再次签名,得到值 autograph +- 服务器对比 signature 和 autograph,相等则认为是一次合法请求,否则则认为参数被篡改,判定为一次非法请求 + +因为中间人不知道签名密钥,所以即使拦截到请求,修改了某项参数,但是无法得到正确的签名 signature,这样构造的一个请求,会被服务器判定为一次非法请求。 + + + + +## 三、 防重放策略 + +在工程师文化中,我们要做一个事情,就首先要对这个事情下个定义。我们才能知道做什么、怎么做。 + +理论上,一个 API 接口请求被收到,服务会做校验,但是当一个合法请求被中间人拦截后,中间人原封不动得重复发送该请求一次或多次,这种重复利用合法请求进行得攻击被成为**重放**。 + +重放会造成服务器问题,所以我们需要针对重放做防重放。本质上就是如何区别去一次正常、合法的请求。 + + + +### 3.1 基于 timestamp 的方案 + +理论上,客户端发起一次请求,到服务端接收到这个请求的时间,业界判定为不超过60秒。利用这个特征,客户端每次请求都加上 timestamp1,客户端将 timestamp1 和其他请求参数一起签名得到 signature,之后发送请求到服务器。 + +- 服务器拿到当前时间戳 timestamp2,timestap2 - timestamp1 > 60s,则认为非法 +- 服务端接收到客户端请求,使用约定好的密钥对请求参数(不包括 signature、timestamp1)进行再次签名,得到值 autograph。比对 signature 和 autograph,若不相等则认为是一次非法请求 + +假如中间人拦截到请求,修改了 timestamp 或者其他的任何参数,但是不知道密钥,所以服务器依旧判定为非法请求。 +中间人从抓包、篡改参数、发起请求的过程一般来说大于60秒,所以服务器依旧会判定为非法请求。 + +基于 timestamp 的设计缺陷也很明显,种种原因下,60秒内的请求,会钻规则漏洞,服务器判定为一次合法请求。 + + + +### 3.2 基于 nonce 的方案 + +既然时间戳会有漏洞,那么新方案是基于随机字符串 nonce。也就是说每次请求都加入一个随机字符串,然后将其他参数一起利用密钥加密得到签名 signature。服务端收到请求后 +- 先判断 nonce 参数是否能存在于某个集合中,如果存在则认为是非法请求;如果不存在,则将 nonce 添加到当前的集合中 +- 服务端将客户端请求参数(除 nonce)结合密钥加密得到 autograph,将 signature 和 autograph 比对,不相等则认为非法请求 + +但是该方案也有缺点,因为当次的请求都需要和集合中去搜索匹配,所以该集合不能太大,不然匹配算法特别耗时,接口性能降低。所以不得不定期删除部分 nonce 值。但是这样的情况下,被删除的 nonce 被利用为重放攻击,服务器判定为合法请求。 + +假设服务器只保存24小时内请求的 nonce,该存储仍旧是一笔不小的开销。 + + + +### 3.3 基于 timestamp + nonce 的方案 + +根据 timestamp 和 nonce 各自的特点:timestamp 无法解决60秒内的重放请求;nonce 存储和查找消耗较大。所以结合2者的特点,便有了 「timestamp + nonce 的防重放方案」。 + +- 利用 timestamp 解决超过60秒被认为非法请求的问题 +- 利用 nonce 解决 timestamp 60秒内的漏网之鱼 + +步骤: + +1. 客户端将当前 timestamp1、随机字符串和其他请求参数,按照密钥,生成签名 signature +2. 服务端收到请求,利用服务端密钥,将除 timestamp1、随机字符串之外的请求参数,加密生成签名 autograph +3. 服务端对比 signature 和 autograph,不相等则认为非法请求 +4. 拿到服务端时间戳, timestamp2 - timestamp1 < 60,则判定为一次合法请求,然后保存 nonce +5. 服务端只保存60秒内的 nonce,定时将集合内过期的 nonce 删除 + +该集合不应该直接操作文件或者数据库,否则服务端 IO 太多,造成性能瓶颈。可以是 mmap 或者其他内存到文件的读写机制。根据场景可以选择乐观锁、悲观锁。 + +其中有一个 timestamp 的问题,服务器会将请求参数中的 timestamp 判断差值,其中一个致命的缺点是服务器的时间和客户端的时间是存在时间差的,当然你也可以通过校验时间戳解决此问题。时间同步请继续看下面部分。 + + + + +## 四、 计算机网络时间同步技术原理 + +客户端和服务端的时间同步在很多场景下非常重要,比如秒杀系统(页面打开,各个类目的商品展示倒计时秒杀功能。如果直接请求接口页面数据,然后拿到服务器时间进行倒计时,则会因为网络传输的耗时,导致时间不精确)、接口 timestamp 等。 + +1. 如果精度要求不高的情况下:先请求服务器上的时间 ServerTime,然后记录下来,同时记录当前的时间 LocalTime1;需要获取当前的时间时,用最新的当前时间 (LocalTime2 - LocalTime1 + ServerTime) + + 拿 iOS 端举例: + - App 启动后通过接口获取服务器时间 ServerTime,保存本地。并同时记录当前时间 LocalTime1 + - 需要使用服务器时间时,先拿到当前时间 LocalTime2 - LocalTime1 + ServerTime + - 若获取服务器时间接口失败,则从缓存中拿到之前同步的结果(初始的时间在 App 打包阶段内置了) + - 使用 `NSSystemClockDidChangeNotification` 监测系统时间发生改变,若变化则重新获取接口,进行时同步 + + +2. 如果需要精度更高,比如 100纳秒的情况,则需要使用 NTP(Network Time Protocol)网络时间协议、PTP (Precision Time Protocol)精确时间同步协议了。 + +NTP、PTP 不在本文的范畴,不懂得可以查看这篇[文章](https://segmentfault.com/a/1190000005337116) diff --git a/Chapter3 - Server/chapter3.md b/Chapter3 - Server/chapter3.md new file mode 100644 index 0000000..c8d1ef5 --- /dev/null +++ b/Chapter3 - Server/chapter3.md @@ -0,0 +1,13 @@ +# 第三部分 + +第三部分主要记录在后端技术的经验或学习心得。包括 Node、PHP、Python等 + + +* [1、利用分页和模糊查询技术实现一个App接口](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.1.md) +* [2、网页端扫码登录实现原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.2.md) +* [3、从Node.js看看服务端框架的感想](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.3.md) +* [4、免费的配置HTTPS证书](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.4.md) +* [5、PHP、Mysql 中文乱码](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.5.md) +* [6、YAML](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.6.md) +* [7、Node单元测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.7.md) +* [8、数据安全(反爬虫)之「防重放」策略](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md) diff --git a/第四部分 数据库/4.1.md b/Chapter4 - DataBase/4.1.md similarity index 100% rename from 第四部分 数据库/4.1.md rename to Chapter4 - DataBase/4.1.md diff --git a/Chapter4 - DataBase/chapter4.md b/Chapter4 - DataBase/chapter4.md new file mode 100644 index 0000000..721fce2 --- /dev/null +++ b/Chapter4 - DataBase/chapter4.md @@ -0,0 +1,7 @@ + +# 第四部分 + +第四部分主要介绍数据库相关知识 + + * [1、Mysql JSON 类型的字段](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter4%20-%20DataBase/4.1.md) + \ No newline at end of file diff --git a/Chapter5 - Network/5.1.md b/Chapter5 - Network/5.1.md new file mode 100644 index 0000000..fbd5da1 --- /dev/null +++ b/Chapter5 - Network/5.1.md @@ -0,0 +1,48 @@ +# HTTP 请求头 Range 信息 + +请求资源的部分内容,单位是 byte(字节),从0开始。 +如果请求头携带了 Range 信息,也就是分批下载,这时候服务器会返回 206 Partial Content 的状态码及说明。 + +如果服务器不支持分批下载,那么会返回整个资源的大小以及状态码为200。 + + +## Range 请求头 + +`Range: bytes=start-end` + +例如: +``` +Range: bytes=10- //:从第10个字节开始到最后一个字节的数据 +Range:bytes=20-39 //:从第20个字节到第39个字节之间的数据 +``` + +注意:整个表示 [start, end] 是前闭后闭的,也就是包含请求头的 start 和 end。所以下次请求应该是 [end+1, nextEnd]。 + + +## Content-Range 响应头 + +`Content-Range:bytes 0-10/3000` +表示服务器返回了前(0-10)个字节的数据,总共3000字节的数据。 + + +## Content-Type 数据类型 +`Content-Type:image/png` 表示资源类型是 png 格式的图片 + +## Content-Length 资源的长度 + +`Content-Length:11` 表示服务器响应了11个字节的数据 + +## Last-Modified + +`Last-Modified:Tue, 30 Jun 2018 03:12:48 GMT` 表示资源最近被修改的时间,如果分批下载的时候发现 Last-Modified 被修改了,那么需要重新下载 + +## ETag + +`ETag: W/"3103-1435633968000"` 表示资源版本的标示符。通常是消息摘要(类似MD5)。分段下载时需要注意,或者缓存控制也需要注意。如果是分布式缓存系统,需要确保每台计算机的 ETag 计算规则的一致性,缓存的过期需要结合 ETag 和 Last-Modified 共同决定。 + +## 分段下载 + +利用 HTTP 的头信息的上述几个特点,我们可以充分利用多线程的能力。 + +- 先发送一个 HEAD 方法的请求,知道总文件大小(Content-Length 就是总字节大小) +- 多线程下载(线程1:Range:bytes=0-100,线程2:Range:bytes=100-200,...) \ No newline at end of file diff --git a/Chapter5 - Network/5.2.md b/Chapter5 - Network/5.2.md new file mode 100644 index 0000000..b5a2cee --- /dev/null +++ b/Chapter5 - Network/5.2.md @@ -0,0 +1,145 @@ +# TCP、UDP 的比较 + +> 网络协议是每个工程师都需要了解和掌握的知识。 TCP/IP 中有2个最具代表性的传输层协议:TCP、UDP。 + + +## 一、TCP/IP 网络模型 + +计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。 + +TCP/IP 是协议簇,比如:TCP、UDP、IP、FTP、HTTP、ICMP、SMTP 等都属于 TCP/IP 簇内的协议。 + +TCP/IP模型是互联网的基础,它是一系列网络协议的总称。这些协议可以划分为四层,分别为链路层、网络层、传输层和应用层。 + +- 链路层:负责封装和解封装 IP 报文,发送和接受 ARP/RARP 报文等。 +- 网络层:负责路由以及把分组报文发送给目标网络或主机。 +- 传输层:负责对报文进行分组和重组,并以 TCP 或 UDP 协议格式封装报文。 +- 应用层:负责向用户提供应用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。 + + +| OSI 七层模型 | TCP/IP 概念层模型 | 功能 | TCP/IP 协议簇 | +|-|-|-|-| +|应用层 |应用层 |文件传输、电子邮件、文件服务、虚拟终端 | FTP、SMTP、TELNET、DNS| +|表示层 |应用层 |数据格式化、代码转换、数据加密 | 没有协议| +|会话层 |应用层 |解除或建立别的节点的联系 | 没有协议 | +|传输层 | 传输层 |提供端对端的接口 | UDP、TCP | +|网络层 |网络层 |为数据包选择路由 | IP、ICMP、RIP、OSPF、BGP、IGMP| +|物理链路层 |链路层 |传输有地址的帧以及错误检测功能 | SLIP、CSLIP、PPP、ARP、RARP、MTU| +|物理层 |链路层 |以二进制数据形式在物理媒体上传输数据 | ISO2110、IEEE802、IEEE802.2| + + +在网络体系结构中网络通信的建立必须是在通信双方的对等层进行,不能交错。 在整个数据传输过程中,数据在发送端时经过各层时都要附加上相应层的协议头和协议尾(仅数据链路层需要封装协议尾)部分,也就是要对数据进行协议封装,以标识对应层所用的通信协议。接下去介绍 TCP/IP 中有两个具有代表性的传输层协议:TCP 和 UDP。 + + + +## 二、UDP + +UDP(User Data Protocol)协议全称是用户数据报协议,在网络中它与 TCP 协议一样用于处理数据包,是一种无连接的协议。在 OSI 模型中,位于第四层即传输层,处于 IP 协议的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。 + +特点: + +### 1.面向无连接 + +首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。 + +具体来说就是: +- 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了 +- 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作 + +### 2.有单播、多播、广播的功能 + +UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。 + +### 3.UDP是面向报文的 + +发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文 + +### 4.不可靠性 + +- 首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。 +- 并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。 +- 再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。 + +![UDP传输示意图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-13-UDP.gif) + +从上面的动态图可以得知,UDP只会把想发的数据报文一股脑的丢给对方,并不在意数据有无安全完整到达。 + +### 5.头部开销小,传输数据高效 + +![UDP报文头部结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-13-UDPHeader.png) + +- 两个十六位的端口号,分别为源端口(可选字段)和目标端口 +- 整个数据报文的长度 +- 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误 + +因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的 + + +## 三、TCP + +当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。 + +TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。 + +![TCP传输示意图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-13-TCPAnimation.gif) + + +### 三次握手 + +HTTP 协议是建立在请求相应模型上的。首先客户端建立一条与服务器的 TCP 链接,并发送请求到服务器,包含请求方法、URI、协议版本以及相关的 MIME 信息。服务器响应一个状态行,包含协议版本、一个成功和失败码、以及相关的 MIME 信息。 + +HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此一个包含图片和多媒体的网页将建立多次的短期 TCP 链接。且一次 TCP 链接都需要三次握手、四次挥手。 + +![TCP三次握手](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-13-TCPThreeShakeHandle.png) + +- 第一次握手:客户端将 SYN 置为1,选择一个 **seq=x**(序号,起始发送位)。SYN 报文段不能携带数据,但要消耗掉一个序号。此时客户端进入 **SYN_SENT** (同步已发送)状态。SYN(Synchronize Sequence Number):同步序列编号 +- 第二次握手:服务端收到报文段后,若同意建立请求则向客户端发送确认。在确认报文中把 SYN 和 ACK 都置为1,确认号是 **ack=x+1**,同时也选择一个初始序号 **seq=y**。这个报文段也不能携带数据,但同样需消耗掉一个序号。此时服务器进入 **SYN_RECV** 状态;ACK(Ackonwledgement):确认字符 +- 第三次握手:客户端收到服务端 ACK+SYN 包,需要向服务端确认。确认报文段的 ACK 置为1(建立连接,2端的ACK都为1),确认号 **ack=y+1**,而自己的序号 **seq=x+1**。客户端和服务端进入 **ESTABLISHED** (TCP 连接成功)状态,完成三次握手。 + + +### 四次挥手 + +![TCP四次挥手](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-13-TCPFourShakeHands.png) + +- 第一次挥手:客户端应用进程先向其 TCP 发出连接释放报文段,并停止发送数据,主动关闭 TCP 连接(此时关闭的是自己与客户端的连接)。FIN 置为1,序号 seq=u,它等于前面已传送过的数据的最后一个字节的序号加1。这时客户端进入 **FIN_WAIT_1** (终止等待1)状态,等待服务端的确认。TCP 规定,FIN 报文段即使不携带数据段,它也会消耗掉一个序号。 +- 第二次挥手:服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。发出确认,确认号 ack=u+1,序号为 seq=v,等于服务端前面已经传输过的数据的最后一个字节的序号加1。然后服务端就进入了 CLOSE_WAIT(关闭等待)状态。TCP 服务器进程通知高层应用数据,因而从客户端到服务端这个芳香的连接就释放了。此时 TCP 处于半关闭状态。即客户端已经没数据要发送了,服务端还在发送数据,但是客户端仍需接收数据。也就是说从服务端到客户端的连接尚未关闭 +- 第三次挥手:客户端收到来自服务端的消息后进入了 FIN_WAIT_2 (终止等待2)状态。等待服务端发出的连接释放报文段。若服务端已经没有要向客户端发送数据,其应用进程就通知 TCP 释放连接。这时候服务端发出的报文段必须 FIN=1。假设服务端序号为 W(在在关闭状态下服务端可能又发送了一些数据,因此与 V 有一定距离)。服务端还必须重复上次发送过的确认号 ack=u+1。这时服务端就进入 LAST_ACK(最后确认)状态,等待客户端的确认 +- 第四次挥手:客户端收到服务端发送的连接释放报文段后,必须对此发出确认。在确认报文段中把 ACK 置1,确认号为 ack=w+1,而自己的序列号 seq=u+1(根据 TCP 标准,前面发送过的 FIN 报文段要消耗一个序号)。然后进入到 TIME_WAIT(时间等待)状态。现在 TCP 还没有释放掉。必须经过时间等待计时器设置的时间 2MSL 后,客户端才可以进入到 CLOSED 状态。时间 MSL 叫做最长报文段寿命。 + + +### TCP 协议的特点 + +- 面向连接 + 面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是三次握手,这样能建立可靠的连接。建立连接是为数据的可靠传输打下了基础。 +- 仅支持单向传输 + 每条 TCP 传输连接只能有2个端点,只能进行点对点的数据传输,不支持多播和广播传输方式 +- 面向字节流 + TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输 +- 可靠传输 + 对于可靠传输,判断丢包,误码靠的是 TCP 的段编号以及确认号。TCP 为保证报文传输的可靠,给每个包一个序号,同时序号也保证了传输到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认 ACK;如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据将被重传。 +- 提供拥塞控制 + 当网络出现拥塞时,TCP 能够减小向网络注入数据的速率和数量,缓解拥塞 +- TCP 提供全双工通信 + TCP 允许通信双方的应用程序在任何都能发送数据,因为 TCP 连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP 可以发送一个数据段,也可以缓存一段时间一边一次发送更多的数据段(最大的数据段大小取决于 MSS) + +## TCP、UDP 对比 + +1. 对比 + +| | UDP | TCP | +| - | - | - | +|是否连接| 无连接 | 面向连接 | +|是否可靠| 不可靠传输(不存在流量控制、拥塞控制) | 可靠传输(使用流量控制、拥塞控制) | +|连接对象个数| 一对一、一对多、多对一、多对多互相通信 | 只可一对一通信 | +|传输方式| 面向报文 | 面向字节流 | +|首部开销| 开销小,仅8字节 | 开销大,20字节 | +|使用场景| 实时应用,比如IP电话、视频会议、直播 | 要求可靠传输的应用,比如文件传输 | + +2. 使用场景 + +基于上面的优缺点,那么: 什么时候应该使用TCP: 当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。 在日常生活中,常见使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输 ………… 什么时候应该使用UDP: 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。 比如,日常生活中,常见使用UDP协议的应用如下: QQ语音 QQ视频 TFTP …… + + + + + diff --git a/Chapter5 - Network/5.3.md b/Chapter5 - Network/5.3.md new file mode 100644 index 0000000..0bd9653 --- /dev/null +++ b/Chapter5 - Network/5.3.md @@ -0,0 +1,248 @@ +# 字节序 + +最近在调一个自定义报文的接口时,本来以为挺简单的,发现踩了好几个坑,其中一个比较“刻骨铭心”的问题就是数据的字节序问题。 + + + +## 背景 + +自定义报文,调用接口,服务端报文解析失败 +![代码截图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-09-15-BigEndian.png) + + + +## iOS 小端序 + +查看 iOS 设备使用的端序 +```Objective-C + if (NSHostByteOrder() == NS_LittleEndian) { + NSLog(@"NS_LittleEndian"); +} if (NSHostByteOrder() == NS_BigEndian) { + NSLog(@"NS_BigEndian"); +} else { + NSLog(@"Unknown"); +} +``` + + + +## 概念 + +字节序,字节顺序,又称端序或尾序(Endianness),在计算机科学领域中,指「存储器」中或者「数字通信链路」中,组成多字节的字的**字节排列顺序**。 + +在几乎所有的机器上,**多字节对象都被存储为连续的字节序列**。例如在 C 语言中,一个 `int` 类型的变量 x 地址为 0x100,那么其对应的地址表达式 `&x` 的值为 `0x100`,且 x 的4个字节将被存储在存储器的 `0x100`,`0x101`,`0x102`,`0x103` 位置。 + +字节的排列方式有2个通用规则。例如一个多位整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)排在最高有效字节前面,则成为**“小端序“**;反之成为**”大端序“**。在计算机网络中,字节序是一个必须要考虑的因素,因为不同类型的机器可能采用不同标准的字节序,所以均需要按照网络标准进行转化。 + + +假设一个类型为 int 的变量 x,位于地址 0x100 处,它的值为 0x01234567,地址范围为 0x100~0x103字节,其内部的排列顺序由机器决定,也就是和 CPU 有关,和操作系统无关。 + +- 大端序(Big Endian):也叫大尾序。高字节存储在内存的低地址 + | 地址增长方向 |内存地址序号|16进制| + |:- |:-|:-| + |↓ |0x100| 01| + |↓ |0x101| 23| + |↓ |0x102| 45| + |↓|0x103| 67| + +- 小端序(Little Endian):也叫小尾序。低字节存储在内存的低地址 + | 地址增长方向 |内存地址序号|16进制| + |:- |:-|:-| + |↓ |0x100| 67| + |↓ |0x101| 45| + |↓ |0x102| 23| jvyyhr + |↓|0x103| 01| + + + +## 缘起 + +“endian”一词来源于十八世紀愛爾蘭作家乔纳森·斯威夫特(Jonathan Swift)的小说《格列佛游记》(Gulliver's Travels)。小说中,小人国为水煮蛋该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论,争论的双方分别被称为“大端派”和“小端派”。以下是1726年关于大小端之争历史的描述: + +> “我下面要告诉你的是,Lilliput和Blefuscu这两大强国在过去36个月里一直在苦战。战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端,可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了。因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。老百姓们对这项命令极其反感。历史告诉我们,由此曾经发生过6次叛乱,其中一个皇帝送了命,另一个丢了王位。这些叛乱大多都是由Blefuscu的国王大臣们煽动起来的。叛乱平息后,流亡的人总是逃到那个帝国去寻求避难。据估计,先后几次有11000人情愿受死也不肯去打破鸡蛋较小的一端。关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派任何人不得做官。” +—— 《格列夫游记》 第一卷第4章 蒋剑锋(译) + +1980年,丹尼·科恩(Danny Cohen),一位网络协议的早期开发者,在其著名的论文"On Holy Wars and a Plea for Peace"中,为平息一场关于字节该以什么样的顺序传送的争论,而第一次引用了该词。 + + + + +## 字节顺序 + +对于单一的字节(a byte),大部分处理器以相同的顺序处理位元(bit),因此单字节的存放方法和传输方式一般相同。 +对于多字节数据,如整数(32位机中一般占4字节),在不同的处理器的存放方式主要有两种:大、小端序。 + + + +## 拓展 + +以内存中0x0A0B0C0D的存放方式为例,分别有以下几种方式: +注:0x 前缀代表十六进制。 + + +1. 大端序 + +- 数据以8bit为单位: + + 地址增长方向 → + ... 0x0A 0x0B 0x0C 0x0D ... + 示例中,最高位字节是0x0A 存储在最低的内存地址处。下一个字节0x0B存在后面的地址处。正类似于十六进制字节从左到右的阅读顺序。 + +- 数据以16bit为单位: + + 地址增长方向 → + ... 0x0A0B 0x0C0D ... + 最高的16bit单元0x0A0B存储在低位。 + +2. 小端序 + +- 数据以8bit为单位: + + 地址增长方向 → + ... 0x0D 0x0C 0x0B 0x0A ... + 最低位字节是0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。 + +- 数据以16bit为单位: + + 地址增长方向 → + ... 0x0C0D 0x0A0B ... + 最低的16bit单元0x0C0D存储在低位。 + +- 更改地址的增长方向: + 当更改地址的增长方向,使之由右至左时,表格更具有可阅读性。 + + + + ← 地址增长方向 + ... 0x0A 0x0B 0x0C 0x0D ... + + + + 最低有效位(LSB)是0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。 + + + + ← 地址增长方向 + ... 0x0A0B 0x0C0D ... + + + + 最低的16bit单元0x0C0D存储在低位。 + +3. 混合序(英:middle-endian)具有更复杂的顺序。以 PDP-11 为例,0x0A0B0C0D 被存储为: + 32bit在PDP-11的存储方式 + + + + 地址增长方向 → + ... 0x0B 0x0A 0x0D 0x0C ... + + + + 可以看作高16bit和低16bit以大端序存储,但16bit内部以小端存储。 + + + + +## 处理器体系 + +- x86、MOS Technology 6502、Z80、VAX、PDP-11等处理器为小端序; +- Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC(除V9外)等处理器为大端序; +- ARM、PowerPC(除PowerPC 970外)、DEC Alpha、SPARC V9、MIPS、PA-RISC及IA64的字节序是可配置的。 + + + +## 网络字节顺序(NBO) + +通常我们认为,在网络传输的字节顺序即为网络字节序为标准顺序,考虑到与协议的一致以及与其他平台产品的互通,在程序发送数据包的时候,将主机字节序转换为网络字节序,收数据包处将网络字节序转换为主机字节序。 + +NBO(Network Byte Order):按照从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。TCP/IP中规定好的一种数据表示格式,与具体的 CPU 类型、操作系统等无关。从而保证数据在不同主机之间传输时能够被正确解释。网络字节序采用大端序。 + + + +## 主机字节顺序 HBO + +主机字节顺序(HBO:Host Byte Order):不同机器 HBO 不相同,与 CPU 有关。计算机存储数据有两种字节优先顺序:Big Endian 和 Little Endian。Internet 以 Big Endian 顺序在网络上传输,所以对于在内部是以 Little Endian 方式存储数据的机器,在网络通信时就需要进行转换。 + + + +## 如何转换 + +由于 Internet 和 Intel 处理器的字节顺序不同,所以开发者需要使用 Sockets API 提供的标准转换函数。 + +BSD Socket 提供了转换函数 +- htons() : unsigned short 从主机序转换到网络序 +- htonl(): unsigned long 从主机序转换到网络序 +- ntohs():unsigned short 从网络序转换到主机序 +- ntohl():unsigned long 从网络序转换到主机序 + +之前的代码采用端序转换 + +```objective-c +- (NSData *)handlePayloadData:(NSArray *)rawArray +{ + if (rawArray.count == 0) { + return 0; + } + // 2. 加密压缩处理:(meta 整体先加密再压缩,payload一条条先加密再压缩) + __block NSMutableString *metaStrings = [NSMutableString string]; + __block NSMutableArray *payloads = [NSMutableArray array]; + + // 2.1. 遍历拼接model,取出 meta,用 \n 拼接 + [rawArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (PCT_IS_CLASS(obj, PCTLogPayloadModel)) { + + PCTLogPayloadModel *payloadModel = (PCTLogPayloadModel *)obj; + BOOL shouldAppendLineBreakSymbol = idx < (rawArray.count - 1); + [metaStrings appendString:[NSString stringWithFormat:@"%@%@", payloadModel.meta, shouldAppendLineBreakSymbol ? @"\n" : @""]]; + + // 2.2 判断是否需要上传 payload 信息。如果需要则将 payload 取出。(Payload 可能为空) + if ([self needUploadPayload:payloadModel]) { + if (payloadModel.payload) { + NSData *payloadData = [PCTDataSerializer compressAndEncryptWithData:payloadModel.payload]; + [payloads addObject:payloadData]; + } + } + } + }]; + + if (metaStrings.length == 0) { + return nil; + } + + NSData *metaData = [PCTDataSerializer compressAndEncryptWithString:metaStrings]; + + __block NSMutableData *headerData = [NSMutableData data]; + unsigned short metaLength = (unsigned short)metaData.length; + HTONS(metaLength); // 处理2字节的大端序 + [headerData appendData:[NSData dataWithBytes:&metaLength length:sizeof(metaLength)]]; + + Byte payloadCountbytes[] = {payloads.count}; + NSData *payloadCountData = [[NSData alloc] initWithBytes:payloadCountbytes length:sizeof(payloadCountbytes)]; + [headerData appendData:payloadCountData]; + + [payloads enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + unsigned int payloadLength = (unsigned int)obj.length; + HTONL(payloadLength); // 处理4字节的大端序 + [headerData appendData:[NSData dataWithBytes:&payloadLength length:sizeof(payloadLength)]]; + }]; + + __block NSMutableData *uploadData = [NSMutableData data]; + // 先添加 header 基础信息,不需要加密压缩 + [uploadData appendData:[headerData copy]]; + // 再添加 meta 信息,meta 信息需要先压缩再加密 + [uploadData appendData:metaData]; + // 再添加 payload 信息 + [payloads enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [uploadData appendData:obj]; + }]; + return [uploadData copy]; +} +``` + + + + +## 参考资料 + +- [维基百科:字节序](https://zh.wikipedia.org/wiki/字节序) \ No newline at end of file diff --git a/Chapter5 - Network/5.4.md b/Chapter5 - Network/5.4.md new file mode 100644 index 0000000..fb63fc3 --- /dev/null +++ b/Chapter5 - Network/5.4.md @@ -0,0 +1,52 @@ +# 自定义报头协议 + +> 在学习过计算机网络的课程,我们知道刚开始计算机都是单独脱机工作的,没有联网的情况下计算机的信息共享能力、运算能力都非常有限,后来诞生了计算机网络.有了就是那几网络,计算机 A 的信息和数据可以通过网络传递到计算机 B,同样计算机 A 可以获取到来自计算机 B 的数据. 但是不同计算机之间交换数据的时候就要通过网络来传输了.传输的过程中需要不同的计算机都遵循一定的规则来组装数据、传递信息,那么这样的规则就叫做协议. + +## 1. 协议 + +计算机网络中有非常多协议,这些协议位于 OSI 的不同层中,比如 TCP/IP、UDP、SMTP、FTP 等. 协议之所以称为协议,是因为它具有约束效应,信息在端到端的传输过程中,同等层次之间通过使用同样的协议规则,这样发送方在该层次按照协议约定处理数据,接收方在该层次按照协议约定解析数据.成对存在. + +![OSI七层模型](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-10-07-OSI.jpg) +![TCP协议](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-10--07-TCP.png) + + +## 2. 自定义协议 + +在日常开发的时候处于某些原因可能需要自定义报文协议.这个协议是建立在 TCP 连接的基础上,比如,移动端在做 APM 的时候将功能拆分为2个模块,一个是 APM 监控模块、一个为了方便可拓展单独做了一个数据上报组件,具有动态下发上报策略的配置. + +所以上报组件这里涉及到和服务端高效通信,所以客户端和服务端约定了一套自定义的报文协议,如下所示. + + + +- PACKET 整体结构 + | HEAD | META | BATCH_PAYLOAD | + +- PACKET::HEAD 结构 + + | META_SIZE (2bytes unsigned int) | BATCH_COUNT (1 bytes unsigned int) | PAYLOAD_SIZE (4bytes unsigned int) | PAYLOAD_SIZE (4bytes unsigned int) | ... | + +- PACKET::META 结构 + + 换行符分割的 JSON 字符串 + + crypto(deflate(JSON\nJSON...)) + +- PACKET::BATCH_PAYLOAD 结构 + + JSON 体里每个字段的值都是 BASE64 编码的 + + crypto(deflate(JSON) | crypto(deflate(JSON) | ... + + +其实计算机网络过程中传输的就是二进制数据,所以拿 iOS 举例来说,我们自定义报文协议的目的就是按照协议的约定,自定义组装好一个 NSData 的数据,报文里面的头规定了各种信息的组装格式. + + - HEAD 里面需要携带3个信息. 信息1:meta 的 size 信息,而且协议规定了必须使用 2byte 的 unsigned int 数据类型. 信息2: payload 报文的个数,必须使用 1byte 的 unsigned int 数据类型. 信息3:每个报文的大小,必须使用 4byte 的 unsigned int 数据类型. + - META 里面需要携带1个信息. 将多条元数据用 "\n" 换行符拼接,另外拼接完的整体数据先压缩再加密 + - PAYLOAD 里面将每条数据先压缩,然后整体再加密 + +所以核心就上面的3点,一点都不难,只不过第一次做的时候可能会踩坑.列觉如下 + +- 协议明确规定了该采用什么样的数据类型,另外数据大小做了明确限制,如果你不这么做,服务端按照字节长度去解析数据就会出错,相应的客户端接口的返回结果就是失败.自讨苦吃 +- 设计到数据的处理,就应该注意字节序的问题,比如 iOS 采用的小端序,网络规定数据传输使用大端序,假如你不知道这个问题,那么你很可能接口调用失败,所以你需要将你的数据转换为大端序的数据,关于大小端序的问题可以查看我的[这篇文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第五部分%20计算机网络/5.3.md) + +Objective-C 语言中处理 unsigned int 的数据,所以你需要直接操作 unsign short 到 NSData, NSData 的接口很方便, `[NSData dataWithBytes:&metaLength length:sizeof(metaLength)]]` 就可以处理成 2byte 的 unsigned int 的数据 diff --git a/Chapter5 - Network/chapter5.md b/Chapter5 - Network/chapter5.md new file mode 100644 index 0000000..444ebe1 --- /dev/null +++ b/Chapter5 - Network/chapter5.md @@ -0,0 +1,8 @@ +# 第五部分 + +第五部分主要记录在计算机网络知识 + + * [1、HTTP请求头Range](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md) + * [2、认识HTTP、TCP、UDP](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.2.md) + * [3、你知道字节序吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md) + * [4、自定义报头协议](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.4.md) diff --git a/Chapter6 - Design Pattern/6.1.md b/Chapter6 - Design Pattern/6.1.md new file mode 100644 index 0000000..a3b7854 --- /dev/null +++ b/Chapter6 - Design Pattern/6.1.md @@ -0,0 +1,159 @@ +# 声明式和命令式 + +> In computer science, declarative programming is a programming paradigm — a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow. +> This is in contrast with imperative programming, which implements algorithms in explicit steps. + +声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。 + +> In computer science, imperative programming is a programming paradigm that uses statements that change a program's state. In much the same way that the imperative mood in natural languages expresses commands, an imperative program consists of commands for the computer to perform. Imperative programming focuses on describing how a program operates. +> The term is often used in contrast to declarative programming, which focuses on what the program should accomplish without specifying how the program should achieve the result. + + +命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。 + + +## 声明式编程和命令式编程的代码例子 + +举个简单的例子,假设我们想让一个数组里的数值翻倍。 + +我们用命令式编程风格实现,像下面这样: + +```JavaScript +var numbers = [1,2,3,4,5] + +var doubled = [] + +for(var i = 0; i < numbers.length; i++) { + + var newNumber = numbers[i] * 2 + doubled.push(newNumber) + +} +console.log(doubled) //=> [2,4,6,8,10] +``` + +我们直接遍历整个数组,取出每个元素,乘以二,然后把翻倍后的值放入新数组,每次都要操作这个双倍数组,直到计算完所有元素。 + +而使用声明式编程方法,我们可以用 Array.map 函数,像下面这样: + +```JavaScript +var numbers = [1,2,3,4,5] + +var doubled = numbers.map(function(n) { + + return n * 2 +}) +console.log(doubled) //=> [2,4,6,8,10] +``` + +map 利用当前的数组创建了一个新数组,新数组里的每个元素都是经过了传入 map 的函数处理。 + +map 函数所作的事情是将直接遍历整个数组的过程归纳抽离出来,让我们专注于描述我们想要的是什么(what)。注意,我们传入 map 的是一个纯函数;**它不具有任何副作用(不会改变外部状态)**,它只是接收一个数字,返回乘以二后的值。 + +在一些具有函数式编程特征的语言里,对于**集合数据类型**,还有一些其他常用的声明式的函数方法。例如 **reduce、filter** + +## 声明式编程很奇怪吗? + +作为程序员,我们非常习惯去指出事情应该如何运行。当我们已经知道了如何告诉机器该如何做事时(假如集合数据类型没有 reduce、filter、map 等函数,那么作为程序员,肯定想到的是 for 循环,变量操作等等),为什么我们需要去学习这种看起来有些怪异的归纳抽离出来的函数工具? + +很多情况下,命令式编程很好用。当我们写业务逻辑,我们通常必须要写命令式代码,没有可能在我们的专项业务里也存在一个可以归纳抽离的实现。但是我们可以在小的功能组件或者 UI 渲染等层面做声明式编程。比如 SwiftUI、Flutter、React、Vue 等。 + +但是,如果我们花时间去学习(或发现)声明式的可以归纳抽离的部分,它们能为我们的编程带来巨大的便捷。首先,我可以少写代码,这就是通往成功的捷径。而且它们能让我们站在更高的层面是思考,站在顶层去思考我们想要的是什么,而不是站在最底层思考事情该如何去做。 + +## 声明式编程语言:SQL + +没意识到我们在学习数据库的时候,SQL 就是声明式编程。 +你可以把 SQL 当做一个处理数据的声明式查询语言。完全用SQL写一个应用程序?这不可能。但如果是处理相互关联的数据集,它就显的无比强大了。 + +像下面这样的查询语句: + +```SQL +SELECT * from dogs INNER JOIN owners WHERE dogs.owner_id = owners.id; +``` + +如果我们用命令式编程方式实现这段逻辑: + +```JavaScript +//dogs = [{name: 'Fido', owner_id: 1}, {...}, ... ] +//owners = [{id: 1, name: 'Bob'}, {...}, ...] + +var dogsWithOwners = [] +var dog, owner + +for(var di=0; di < dogs.length; di++) { + + dog = dogs[di] + + for(var oi=0; oi < owners.length; oi++) { + + owner = owners[oi] + if (owner && dog.owner_id == owner.id) { + + dogsWithOwners.push({ + dog: dog, + owner: owner + + }) + } + } } +} +``` + +我可没说SQL是一种很容易懂的语言,也没说一眼就能把它们看明白,但基本上还是很整洁的。 + +SQL代码不仅很短,不不仅容易读懂,它还有更大的优势。因为我们归纳抽离了 how,我们就可以专注于 what,让数据库来实现 how。 + +而SQL例子里我们可以让数据库来处理how,来替我们去找我们想要的数据。如果需要用到索引(假设我们建了索引),数据库知道如何使用索引,这样性能又有了大的提升。如果在此不久之前它执行过相同的查询,它也许会从缓存里立即找到。通过放手how,让机器来做这些有难度的事,我们不需要掌握数据库原理就能轻松的完成任务。 + +## 声明式编程:d3.js + +一个能体现出声明式编程的真正强大之处地方是用户界面、图形、动画编程。 + +开发用户界面是有难度的事。因为有用户交互,我们希望能创建漂亮的动态用户交互方式,通常我们会用到大量的状态声明和很多相同作用的代码,这些代码实际上是可以归纳提炼出来的。 + +d3.js 里面一个非常好的声明时归纳提炼的例子就是它的一个工具包,能够帮助我们使用 JavaScript 和 SVG 来开发交互和动画型的数据可视化模型。 + +下面是一个例子。这是一个d3可视化实现,它为data数组里的每个对象画一个圆。 + + +```JavaScript +//var data = [{x: 5, y: 10}, {x: 20, y: 5}] + +var circles = svg.selectAll('circle') + + .data(data) + +circles.enter().append('circle') + + .attr('cx', function(d) { return d.x }) + + .attr('cy', function(d) { return d.y }) + + .attr('r', 0) + .transition().duration(500).attr('r', 5) +``` + +从头再看一遍代码,想一想,我们是在声明我们想要的图案是什么样子,还是在说如何作图。你会发现这里根本没有关于how的代码。我们只是在一个相当高的层面描述我们想要的是什么: + +我要画圆,圆心在data数据里,当增加新圆时,用动画表示半径的增加。 +太酷了,我们没有写任何循环,这里没有状态管理。画图操作通常是很难写,很麻烦,很让人讨厌,但这里,d3归纳提取了一些常用的操作,让我们专注于描述我们想要的是什么。 + +现在再看,d3.js 很容易理解吗?不是,它绝对需要你花一段时间去学习。而学习的过程基本上需要你放弃去指明如何做事的习惯,而去学会如何描述我想要的是什么。 + +最初,这可能是很困难的事,但经过一些时间的学习后,一些神奇的事情发生了——你变得非常非常有效率了。通过归纳提取how,d3.js 能让你真正的专注说明你想要看到的是什么,让你在一个个更高的层面解决问题,解放你的创作力。 + + +## 声明式编程的总结 + +- 声明式编程让我们去描述我们想要的是什么,让底层的软件、计算机去解决如何实现。 + +- 在很多情况中,就像我们看到的一样,声明式编程能给我们的编程带来真正的提升,通过站在更高层面写代码,我们可以更多的专注于 what,而这正是我们开发软件真正的目标。 + +问题是,程序员习惯了去描述 how,这让我们感觉很好很安心能够控制程序的运行状态,不放走任何我们不能看见不能理解的处理过程。 + +有时候这种紧盯着 how 不放的做法是没问题的。如果我需要对代码进行更高性能的优化,我需要对 what 进行更深一步的描述来指导 how。有时候对于某个业务逻辑没有任何可以归纳提取的通用实现,我们只能写命令式编程代码。 + +但大多数时候,我们可以、而且应该寻求声明式的写代码方式,如果没有发现现成的归纳提取好的实现,我们应该自己去创建。起初这会很难。但就像我们使用 SQL 和 D3.js, 我们会长期从中获得巨大的回报! + + +最近苹果开发者大会推出的 SwiftUI 和流行的 React、Vue 等都是声明式编程方式。和传统的 Dom 技术相比,声明式真的是开发效率的大幅提升(可以看看 Vue.js 官网的[宣传视频](https://cn.vuejs.org/index.html)) diff --git a/Chapter6 - Design Pattern/chapter6.md b/Chapter6 - Design Pattern/chapter6.md new file mode 100644 index 0000000..e18dfdd --- /dev/null +++ b/Chapter6 - Design Pattern/chapter6.md @@ -0,0 +1,8 @@ + +# 第六部分 + +第六部分主要介绍设计模式相关的概念和思考 + + * [1、声明式与命令式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.1.md) + + \ No newline at end of file diff --git a/第五部分 开发杂谈/5.2.md b/Chapter7 - Geek Talk/7.1.md similarity index 98% rename from 第五部分 开发杂谈/5.2.md rename to Chapter7 - Geek Talk/7.1.md index 8f168c7..e11bc2f 100644 --- a/第五部分 开发杂谈/5.2.md +++ b/Chapter7 - Geek Talk/7.1.md @@ -1,4 +1,4 @@ - +# 命令行文件查找 > 在日常中,find的使用频率很高,熟练掌握对提供工作效率很有帮助。 @@ -91,8 +91,11 @@ find . -mindepth 2 -name "*.png" ``` find . -type f -size +100M + +find ./ -size +100MB ``` + 11、根据访问/修改/变化时间 * 访问时间(-atime/天,-amin/分钟):用户最近一次访问时间 diff --git a/Chapter7 - Geek Talk/7.10.md b/Chapter7 - Geek Talk/7.10.md new file mode 100644 index 0000000..b772f7e --- /dev/null +++ b/Chapter7 - Geek Talk/7.10.md @@ -0,0 +1,315 @@ +# Mac 终端效率神技 + +## 增强各种预览的插件 + +- 预览查看图片分辨率&大小 +- 代码语法高亮 +- 快速预览zip压缩包内容 +- 快速预览markdown格式内容 + +```powershell +brew cask install qlcolorcode betterzipql qlimagesize qlmarkdown +``` + +## iTerm2 + +具体的配置网上一大堆。贴一个本人亲身操刀操作过的[教程](https://www.jianshu.com/p/7de00c73a2bb) + +程序员经常与终端操作打交道,所以很多命令便是做成了命令行模式,在自带的 Terminal 命令都保存在 `.bash_profile` 文件中,使用了 iterm2,命令都保存在 `.zshrc` 中。 + +所以我们将很多命令保存且编辑。这里也是分享出我个人常用的配置。不断更新,喜欢的同学可以拿去直接使用 + +```Shell +# 输入自己常用的命令 +# finder 相关指令 +alias co='code ./' +alias fo='open ./' + +# pod 和 xcode 工程相关指令 +alias o='open *.xcodeproj' +alias po='open *.xcworkspace' +alias pru='pod repo update' +alias pi='pod install' +alias pu='pod update' +alias piu='pod install --repo-update' +alias repoanalysis='specbackwarddependency /Users/liubinpeng/.cocoapods/repos/51xianqu-xq_specs' +alias plint='pod lib lint --sources=git@git.caimi-inc.com:client/i-New-All-Specs.git,git@git.caimi-inc.com:client/CocoaPods-Specs.git --allow-warnings --verbose --use-libraries' +alias errorShow=' >1.log 2>&1' +# git 相关指令 +alias gck='git checkout' +alias gm='git merge' +alias gb='git branch' +alias gbr='git branch -a' +alias gs='git status' +alias gc='git clone' +alias gl='git log' +alias ga='git add .' +alias gpull='git pull' +alias gpush='git push' +alias gcm='git commit -m' +alias glocalbranchPush='git push --set-upstream origin ' +alias glg="git log --graph --pretty=format:'%Cred%h%Crest -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative" +# npm 相关指令 +alias ns='npm start' +alias ni='npm install' +alias nb='npm run build' +alias nig='npm install -g ' +alias nt='npm test' + +# Vue 相关命令 +alias vc='vue-init webpack' # (vue-init webpack test1)用法 vc test1 + +# React +alias rc='create-react-app' #(create-react-app todolist)用法 rc todolist + +# React Native 命令 +alias rnc='react-native init' #(react-native init todolist)用法 rnc todolist + + +# 终端打开应用程序 +## 浏览器打开 +alias OpenWithSafari='open -a "/Applications/Safari.app" ' +alias OpenWithChrome='open -a "/Applications/Google Chrome.app" ' +## 用 Typora 打开 markdown 文件预览写作效果。 +alias OpenMDPreview='open -a "/Applications/Typora.app" ' +## 用 DB Browser for SQLite 打开 db 文件 +alias OpenDB='open -a "/Applications/DB Browser for SQLite.app" ' +## 用 SourceTree 打开工程 +alias openSourceTree='open -a "/Applications/Sourcetree.app/" ' +# Flutter 环境变量 + +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 + + +# Android SDK 路径 + +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 + + +# iOS 模拟器开启 +alias iOSSimulator='open -a Simulator' + +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + + +# Node Version Manager +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion +# Add RVM to PATH for scripting. Make sure this is the last PATH variable change. +export PATH="$PATH:$HOME/.rvm/bin" + +export N_PREFIX=/usr/local/bin/node #根据你的安装路径而定 +export PATH=$N_PREFIX/bin:$PATH +export PATH=/Users/liubinpeng/Desktop/Github/GitWorkflow/bin:$PATH + + +# chrome 源码探究 +# export PATH=/Users/liubinpeng/Desktop/Tech-Research/iOS/depot_tools:$PATH + + +# 指定 pyhton 版本 +# Setting PATH for Python 2.7 +PATH="/System/Library/Frameworks/Python.framework/Versions/2.7/bin:${PATH}" +export PATH +# Setting PATH for Python 3.7.4 +PATH="/usr/local/Cellar/python/3.7.4/bin:${PATH}" + +alias python='/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7' +alias python3='/usr/local/Cellar/python/3.7.4/bin/python3' + + + +# 禁止终端利用 homebrew 安装插件时候的自动更新 + +alias disableHomebrewUpdate="export HOMEBREW_NO_AUTO_UPDATE=true" + + +# 终端翻墙相关的设置(开关开启后稍微有点延迟) + +function proxy_off(){ + unset ALL_PROXY + echo -e "已关闭代理" +} + +function proxy_on(){ + export ALL_PROXY=socks5://127.0.0.1:1081 + echo -e "已开启代理" +} + + +# PHP 包管理工具,composer +export PATH="~/.composer/vendor/bin:$PATH" + +# python 版本切换工具,全局生效 +export PYENV_ROOT=~/.pyenv +export PATH=$PYENV_ROOT/shims:$PATH + + +# 效率 +# 统计当前文件夹下文件的数量 +alias showFilesCount='ls -l |grep "^-"|wc -l' +``` + +退出编辑,执行 `source .zshrc` + + + +验证:在你的 git 项目所在的目录的终端下输入 `glg` +![Git日志](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-08-13-GitLogs.png) + + +## 为你的终端添加常用快捷键 + +我们经常在终端做着一些纯指令的事情,天天敲、月月敲这个时间的很浪费的,一天节约5分钟,一年节约365*5/60 = 30H。一算吓一跳。我们每年在一些终端的指令上浪费了这么多时间。今天记录下如何给自己的 Mac 终端添加快捷键。 + +如果是 zsh 的话,可以编辑 `.zshrc` 文件里面的内容。自带的终端则编辑 `bash_profile` +脚本具体看上一条。 + + +## 输出文件目录结构 + +```powershell +brew install tree +``` + +用法: + +1. 我们可以在目录遍历时使用 -L 参数指定遍历层级 + + ```powershell + tree -L 2 + ``` + +2. 如果你想把一个目录的结构树导出到文件 Readme.md ,可以这样操作 + + ```powershell + tree -L 2 >README.md //然后我们看下当前目录下的 README.md 文件 + ``` + +3. 只显示文件夹 + + ```powershell + tree -d + ``` + +4. 显示项目的层级,n表示层级数。例:显示项目三层结构,tree -l 3 + + ```powershell + tree -L n + ``` + +5. tree -I pattern 用于过滤不想要显示的文件或者文件夹。比如要过滤项目中的node_modules文件夹 + + ```powershell + tree -I “node_modules” + ``` + + +## 浏览器相关 + +1. 搜索 + +在指定的站点下搜索 `inurl: jobbole.com intitle:Hybrid` + +## 百度云盘破解 + +1. 会员体验一般为60秒,通过本代码可以一直以会员的速度下载。 + +``` +git clone https://github.com/CodeTips/BaiduNetdiskPlugin-macOS.git && ./BaiduNetdiskPlugin-macOS/Other/Install.sh +``` + +2. 百度网盘全速下载 + +- 先将你需要下载的地址复制进浏览器 +- 然后在域名 baidu 后面拼接 wp +- 回车。访问页面,选择下载地址1即可全速下载。 + +``` +// 之前 +https://pan.baidu.com/s/1ubcQH34m69hIjYu3CD2S2g +// 之后 +https://pan.baiduwp.com/s/1ubcQH34m69hIjYu3CD2S2g +``` + +## 「安全与隐私」中系统不显示「任何来源」 + +在终端执行下面的命令 +``` +sudo spctl --master-disable +``` + +## 系统错误信息的集中展示 + +```Shell +pod spec lint *** 2>&1|tee 1.log +``` + +经常在终端做操作,有个情况就是在 iOS 的组件库维护的时候去检测合法性。你会发现满屏幕都是信息,甚至好几页,但是事实上错了问题后我们去翻页的时候发现很不方便定位问题,所以想到的就是将该过程产生的任何输出,集中打印到一个地方去查看。代码如上。 + +几个概念: +- 0 stdin,1 stdout,2 stderr +- |:管道。管道的作用是提供一个通道,将上一个程序的标准输出重定向到下一个程序作为下一个程序的标准输入。 +- tee:从标准输入中读取,并将内容写到标准输出以及文件中。 + + +## 终端查找文件 + +1. 终端查找以‘.log’结尾的文件 + +```Shell +find . -name '*.log' +``` + +2. 安装 **ack** 包. +```shell +brew install ack +``` + +使用起来很简单,比如 `ack + 你要查找的关键词`,它可以将查到的结果展示在下面,有完整的文件路径. + +![效果图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2019-09-30-searchTools.png) + + +### 终端每次执行 brew install 都会更新,非常耗时,如何禁止更新。 +```shell +export HOMEBREW_NO_AUTO_UPDATE=true +``` + +## Mac 翻墙环境和终端翻墙环境 + +强烈安利一个我用过最快速、最便宜也就是性价比最高的科学上网工具。[链接](http://su.nseducloud.com/aff.php?aff=362) 个人使用的也就是一年120元不到,一个月 60G 流量足够了,打开一些网站秒开。 + +另外很多开发都需要终端下载一些资源,但是终端走的通道和浏览器不一样,所以浏览器可以翻墙,终端还是不可以,所以可以在 `.zshrc` 或者 `.bash_profile` 下加下面的脚本 + +```shell +function proxy_off(){ + unset ALL_PROXY + echo -e "已关闭代理" +} + +function proxy_on(){ + export ALL_PROXY=socks5://127.0.0.1:1081 + echo -e "已开启代理" +} +``` + +使用 proxy_on 开启终端翻墙模式、proxy_off 关闭终端翻墙模式。 + + +## 终端快捷键 + +很多情况下,我们会在终端编辑文件,为了提交效率就有了以下快捷键。 + +- ESC + dd:删除当前一行的数据 + + +持续更新中... diff --git a/Chapter7 - Geek Talk/7.11.md b/Chapter7 - Geek Talk/7.11.md new file mode 100644 index 0000000..2286822 --- /dev/null +++ b/Chapter7 - Geek Talk/7.11.md @@ -0,0 +1,60 @@ +# 2018年年终总结 + +> 临近年终,各大 App 纷纷给用户推送了年度小结,你在这年发了多少条微博、第一条微博是什么、最晚几点在发微博...这是微博给我推送的小结。我这么写的原因并不是让自己盲目跟风,看别人写自己也去写一个。古人云“吾日三省吾身”,那么年终肯定也是需要自我总结一下的。那么本文我将从今年的见闻方面、学习方面、健身方面、生活方面、来年计划对自己做一定的总结和规划 + +## 时间轴 + +2018年3月份进入一个大数据公司担任技术负责人,一个月期间完成 App 开发,期间为了调试接口用 Charles 抓包分析 Android App 通信问题发现了 Android 端的一些 Bug 和服务端的 Bug,找到并反馈给相应的技术。短时间开发完 App,App 进入测试阶段,因为个人较为完美主义,也算是瞎搞吧,给 App 加入了 3D Touch 功能,不知道是不是鸡肋。完了之后组织技术分享。研究出了一套 Web 端的反爬虫技术方案,并找公司的4个爬虫工程师调研,看看我的研究成果好不好爬取。觉得效果不错,公司内技术分享将技术选型、研发思路、部署等都给小伙伴们讲了。慢慢的小伙伴们叫我“大佬”。之后因为我们自己老爬取别人的数据,所以对产品的数据安全问题比较在乎,研究出 App 端的防抓包技术方案。 + +因为有 Web 经验,用 Vue 全家桶对公司的官网进行重构。技术、业务都进行了全新改版。 + +之后帮爬虫工程师解决了一些他们不太好解决的技术问题。比如爬取小程序内的数据、网站的数据。我们的爬虫程序经常在爬取一个网站数据后还需要给网站进行截图,现有的截图的成果会不太满意,比如会截不全、黑屏等,我将这个模块拎出来重写。我们有专门的编辑人员负责将一个报告去别的第三方平台校验数据正确性,这个过程比较耗费时间。利用浏览器插件的原理,给那个第三方平台页面上注入了一个”一键导出“按钮。点击一下当前页面的数据全部更新到我们自己的数据平台。 + +期间负责招聘一些职位、解决技术问题,成长较大,在身边的同事和领导那里得到一个较为良好的反馈,由于好的口碑让我做事更加快乐。形成一个良性循环。因为自己年龄很小,所以一直还是一个学生的态度吧,但是在这个地方完成了从学生到职场人的角色转换。 + +2018年12月底进入一个新零售大公司,各方面很酷,最近入职培训刚考完试,还有3个月考核期,希望自己继续加油吧 + + +## 学习方面 + +iOS 方面:RunLoop、Runtime、KVC、KVO 、GCD、多线程等的底层原理等。 +Web 前端方面:Vue 技术的熟练使用、包括在移动端的 H5 开发、Hybrid 的设计、通信规范。React Native 的学习等,Flutter 的关注。 +服务端方面: Node.js 做了一些接口开发、爬虫方面的事情。PHP 做了几个网站的服务端开发。 +数据库方面:几个网站的数据库设计。知道了关系型数据库、非关系型数据库的区别。实战操作了 Redis 的使用场景。 +自己也写了一些这方面的文章记录。 + + +自己买了一些感兴趣的书,但是由于杂七杂八的事情,或者说懒惰吧。 +- JavaScript高级程序设计(第3版) +- Objective-C高级编程:iOS与OS X多线程和内存管理 +- Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法 +- Node.js实战 第2版 +- 啊哈!算法 +- iOS Auto Layout开发秘籍(第2版) +- HTTP权威指南 +- 音视频开发进阶指南:基于Android与iOS平台的实践 +- iOS应用逆向与安全 + +来年计划要把前5本书看完吧。 + + +## 健身方面 + +健身的话零零散散,可能这周的话保持在4天,下周可能保持在1天或者干脆没有。总的来说天气在温暖的时候不是很热很冷的情况下健身的次数会比较多。希望自己保持一定的健身频率。 + +## 生活方面 + +喜欢到处玩、认识有趣的人、吃好吃的、看各种电影、打乒乓球、游西湖、穿梭在杭州和附近城市的大街小巷,所以总体在这一方面花了较大的时间。所以来年要控制好生活娱乐和学习方面的比重。找到 Miss.Right,携手未来 + +## 来年计划 + +2019年是自己的本命年,我希望在学习、健身、生活、心智各方面都“能打”吧。现在是一个“iOS 高级工程师”的 title,但是希望在这一年继续深入,做一个很能打的工程师。身体是革命的本钱,话糙理不糙,我需要保持充分的运动时间和频率,有一个更好的身材和更高的健康状态。不断学习深入,看完自己喜欢领域的书籍,做一些读书笔记(应该是技术博客)。优质社交、心智更加成熟。更懂人性,做产品、过人生、都需要这一点。做一个感性且善良的人。平时喜欢帮助身边的朋友解决技术问题,2019年继续、助人为乐一辈子践行。不要局限自己的思维模式、理科生可能有一些些太理性了。之前洗澡听到湖畔大学一个老师提到2个词语:“管理员视角”、“用户视角”,这里就不细说,大体意思是蛮多理科生、程序员都在以“管理员视角”做系统,将自己想的、现有的东西做成产品,让用户按照现有的约定设置好的情况去使用产品,而不是站在用户角度出发去设计一个易用的产品。 + + +2019年多做一些技术输出、积累自己的知识和学习路上的一些心得、或者是总结吧。看完2本 iOS 、1本 Javascript、1本Nodejs 、1本设计模式、1本算法方面的书籍。培养正确的三观,看到事物的本质,做一个享受生活、善良的人 + +具体到技术细节就是:大前端落地到移动端身上的多端融合能力和动态化的能力。在这一年内有一定的输出和一定高度的总结。 +(蛮多人谈Hybrid、Flutter、RN,但是需要细节到技术栈的选取,比如你的开发效率、业务迭代能力、程序性能等细节方面,一个学习和应用到工作内容中去需要非常多的细节去考虑,也需要有经验。) +做过前端的人都知道 Redux、Vuex 蛮多人都觉得好用,但是不去想想这个技术方案产生的原因是什么?符合什么场景、符合什么设计思想、设计模式在?你会用它那就是一个普通的初级程序员,你会修改代码去定制化也就是中级程序员,你懂得设计原理,你将前端的这个技术方案换到 Native 里面去实现一套类似的方案,你就是一个高级程序员,你很清楚的知道原理和设计模式、思想、利弊、场景后设计一个更加优秀的东西你就是专家。所以对于新技术先考量场景、再入门、再去思考设计原理、设计模式、设计哲学、设计思想... + +总之2019年是自己的本命年,希望自己更加能打、各方面不断优化自己,过完2019年,回归头来看看希望自己进步更多。 diff --git a/Chapter7 - Geek Talk/7.12.md b/Chapter7 - Geek Talk/7.12.md new file mode 100644 index 0000000..f9f465f --- /dev/null +++ b/Chapter7 - Geek Talk/7.12.md @@ -0,0 +1,54 @@ +# OKR + +## 背景 + +OKR大概在2013年传入中国,开始主要是一些有硅谷背景的初创企业在推行,现在OKR逐步受到IT、互联网、高科技企业的追捧,开始变得流行起来,国内知名的互联网公司豌豆荚、知乎都成功的在企业内部实施了OKR + +## 正确认识 OKR + +百科定义,OKR(Objectives and Key Results)即目标与关键成果法,是一套明确和跟踪目标及其完成情况的管理工具和方法,其实我更偏向于Paul R. Niven和Ben Lamorte给出的另外一个定义: + +OKR是一套严密的思考框架和持续的纪律要求,旨在确保员工紧密协作,把精力聚焦在能促进组织成长的、可衡量的贡献上。 + +按照这个定义可以明确以下几点: + +严密的思考框架:OKR并不是简单的每个周期跟踪一下执行的结果,而是要超越数字本身,思考这些数字对你以及组织来说意味着什么。 +持续的纪律要求:OKR代表了一种时间和精力上的承诺。 +确保员工紧密协作:OKR的目的在于促进员工团队的协作,与组织的目标对齐,而不是对员工的绩效考核。 +精力聚焦:OKR用于识别最关键的业务目标,而不是一些待办事项的简单罗列。 +可衡量的贡献:对最终的结果确保可以衡量,而不是靠主观评价。 +促进组织成长:判断OKR实施成功与否的最终标准,就是看是否促进了组织成长。 +事实上OKR并不是什么新鲜的事物,它是在目标管理的发展过程中,融合了一系列框架、方法和哲学的产物,Peter Drucker在上世纪60年代提出了MBO的思想,此后80年代S.M.A.R.T目标和KPI开始流行起来,1999年John Doerr把OKR引入Google。 + +## 准备启动OKR + +在组织或者公司中实施OKR,最困难的部分在于前期的准备环节,盲目的实施只会导致OKR流于形式,只得其形,不得其神,最终的效果其实只是变成另一种形式的KPI而已,不能给组织、公司和个人带来任何的成长。所以我建议在准备实施OKR之前,先思考清楚下面几个问题。 + +## 为什么要实施OKR + +在开始实施OKR之前,不妨先问自己这样一个问题:为什么要实施OKR?不能很好的回答这个问题,后面所做的一切都没有意义。如果答案只是“因为Google、Intel在用”、“想让公司变得更好”诸如此类的毫无意义的空洞答案,那就不如暂时搁置,直到思考清楚这个问题为止,要让公司全员明白为什么实施OKR。 + +## 在哪个层面实施OKR + +一般来讲OKR的实施有三个层面:公司级、部门级、个人级,但这并不意味着从开始就要三个层面一起实施。更好的做法是选定一个层面,由点到面,逐步推广,最终全员实施OKR。 + +根据公司具体业务情况,可以有两种方式: + +一是纵向实施,开始只实施公司级的OKR,在高管层实施成功后,再推广到部门级,最后再推广到个人级; +二是横向实施,选定某个业务单元或者部门,在该业务单元中同时进行公司、部门、个人级OKR实施,最后再在全公司范围推广。 + +## 实施OKR的周期 + +在开始OKR前需要考虑以多长的周期进行实施,推荐的做法是按季度,但这并不是绝对的,也可以根据公司的业务情况按月为周期进行实施,不建议按年、半年或者周为周期。周期太长,导致目标的制定不合理;周期太短,关键结果的制定就变成了待办事项,无法做到聚焦目标。推荐在季度和月之间选择一个作为公司实施OKR的周期。 + +## 对OKR的统一认识 + +最后最为重要的一点是参与实施OKR的所有人员是否对于OKR有统一的认识?在没有达成共识前不要推行OKR,否则在实施的过程中,由于认识的偏差,最终的OKR实施也会出现偏差。推荐的方式是在开始前通过OKR宣讲的方式进行统一认识,在该宣讲会上需要明确回答上面提到的三个问题,即:为什么要实施OKR,在哪个层面实施OKR以及实施OKR的周期。 + +## 制定有效的OKR + +接下里我将会详细介绍如何制定有效的OKR,首先分别说明一下O(目标)和KR(关键结果): + +目标:目标回答的是“我们想做什么”问题,是定性的,好的目标应该是有时限要求的,简洁直白的陈述,能鼓舞人心的、能激发团队共鸣。 +关键结果:关键结果回答的是“我们如何知道自己是否达成了目标要求?”问题,是定量的,设计KR最具挑战的部分是如何把目标中定性的部分翻译为定量的数字化的表示。 + diff --git a/Chapter7 - Geek Talk/7.13.md b/Chapter7 - Geek Talk/7.13.md new file mode 100644 index 0000000..285cb55 --- /dev/null +++ b/Chapter7 - Geek Talk/7.13.md @@ -0,0 +1,60 @@ +# 面试学问 + +1. 技术面试问你遇到的难题是什么,如何解决的? +在考察你的技术边界 + +2. 至今遇到过的比较麻烦的技术问题? + +- 在App首次安装启动的时候,商品列表快速滑动到底部,再向上慢慢滑动,看到商品view上面的文字和价格都显示正确,但是图片显示是有问题的。想到的是由于SDWebImageView图片问题导致的,因为再快速滑动的时候,图片开启了下载任务,但是异步下载的,cell也在复用池了,所以当你停止滑动看到商品view的时候正是之前的其他的图片任务下载结束,这样子再去响应给view设置图片就会造成当前的显示的图片并不是当前model对应的图片。知道问题的原因那么事情就好解决啦。思路就是如何区分当前下载好的cell是不是当前正在现实的cell。所以思路有2个。 + - 自定义的 cell 里面可以拿到 model.imageUrl,那么当下载图片完成的时候可以在 block 里面去判断回调完成里面的 url 和 model.imageUrl 是否一致,一致的话就设置图片,否则不设置 + - 在 cellForRow 里面去通过 indexPathForCell: 这个 api 拿到 indexPath,然后根据拿到的复用的 indexPath.row 再和当前现实的 indexPath.row 做比较,如果一致则设置图片,不一致则不设置图片 + + + + + +- 代码在编译的时候一直报出问题:某个pod的某个头文件file not found。最后排查问题发现工程中存在2个同名的 Framework,一个版本新、一个版本旧。主工程中链接到旧工程中了,所以一直找不到文件。 + + +## 网络篇 + +- [App网络优化](https://blog.cnbang.net/tech/3531/) +- [IP 直连](https://www.jianshu.com/p/63a94cb46cd20。SNI:Server Name Indication。一个服务器的 IP 可能对应多个域名,也就说可能有多个 HTTPS 证书,如果在请求的时候不指定具体哪个的证书,服务器可能会返回错误的证书 +- 多路复用 + + +## 系统优化 + +- [移动端监控体系](https://www.jianshu.com/p/8123fc17fe0e) +- [启动时间优化](http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/) + +稳定性、常见的稳定排查、crash率,监控、提升工程效率的工具(性能,效率,安全,成本,体验)、电量优化、看大图体验 + + +## 思考 + +组件:重用、解耦。模块:封装、隔离。 + +架构中,模块复用的第一要求便是代码的功能组件化。组件化意味着拥有独立功能的代码从系统中进行抽象并剥离,再以“插件”的形式插回原有系统中。这样剥离出来的功能组件,便可以供其他APP使用,从而降低系统中模块与模块之间的耦合性;也同时提高了APP之间代码的复用性 + + +借助于 Redux 的方式,将 MVC 更改为 MVCS(s为store)。MVC 中的 view 做到无状态,完全依赖于 Store 中返回的各种状态信息。 + +组件化之后分为技术组件、公用业务组件、每个业务线都是单独组件。通过把不同功能的组件代码拆分到不同的 pod 里,实现了业务线仅依赖于公共就可以迭代开发。改善了功能开发和协同版本。 + +流量过大,和服务器交互过多,采用 protoBuf 作为数据载体,有效减小流量消耗。 + +热修复:JavascriptCore + Aspects + + + +组件化、模块化、热修复、RN 兜底后架构模式: +![App架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-07-05-App-structure.jpeg) + + + +- Runtime +- 网络 +- Hybrid +- RN、Weex +- JavascoriptCore 如何传递 Block \ No newline at end of file diff --git a/Chapter7 - Geek Talk/7.14.md b/Chapter7 - Geek Talk/7.14.md new file mode 100644 index 0000000..0eba7ea --- /dev/null +++ b/Chapter7 - Geek Talk/7.14.md @@ -0,0 +1,17 @@ +# Markdown 的妙用 + +## 1. 代码折叠功能 + +有时候我们会在文章上插入一些代码,但是这样的情况下我们的文章整体会显得很长,所以为了保证阅读性比较好,我们最好是将代码折叠起来,有需要的时候再点击展开。 + +我们需要使用 **\
    ** 和 **\** 标签。如下所示 + + + +
    +代码详情 + +```Objective-c +NSLog(@"hello world."); +``` +
    \ No newline at end of file diff --git a/Chapter7 - Geek Talk/7.15.md b/Chapter7 - Geek Talk/7.15.md new file mode 100644 index 0000000..729b697 --- /dev/null +++ b/Chapter7 - Geek Talk/7.15.md @@ -0,0 +1,83 @@ +# 1995年的资深工程师,和你谈谈如何进阶 + +## 自我介绍 + +网络ID:杭城小刘,城市:顾名思义,人在杭州。1995年出生,本科毕业,现在是一名 iOS 资深工程师。兴趣爱好广泛:乒乓球、美食、电影、健身、山地车、养了2只布偶猫(Simba & Bella)、养花。技术领域:iOS、Web 前端,写过 Node、PHP 后端服务、写过爬虫、研究过[反爬虫技术方案](https://github.com/FantasticLBP/Anti-WebSpider)。在成长的路上... + +## 工程师生涯的两三事 + +刚毕业开始还是一名普通的 iOS 工程师,做的东西一般是跟着 TL 开会讨论需求,完了自己脑补技术细节,完了编码、UI 还原、测试、发布、维护。在第一家公司做开发的时候,某次和一个后端工程师对接口遇到问题,人家说“别跟我说接口有问题。你跟我说是什么样的问题?我需要参数”。然后初生牛犊(小菜鸟)就把 Xcode 下面的 Debug 信息截图发出去,被人怼,说我需要网络的具体参数,Request、Reponse 信息。当时很尴尬,我在想封装好的网络框架,我一个个下断点 Debug 截图给你吗。后来才知道有 Charles 这么个抓包工具,简单搞了搞 Charles 之后就将截图 Request、Response 信息给他了,这样他没话说了,老老实实改了接口 Bug。当时他问我 Request、Response 的时候真的超尴尬,我说你等我半小时,我找好发给你。(😂 Too young, too simple)。 + +第一次看到 Charles 有那种看到仙女一样的感觉,因为它不只是可以抓包看 HTTP、HTTPS 的网络请求,还可以模拟数据、篡改网络请求信息、篡改网络返回的信息、模拟弱网环境、接口压力测试(类似于 Load Runner)、还可以隔山打牛(Map Remote:请求域名 A 下的接口 a 却重定向到域名 B 下的接口 b)。功能非常多,让我爱上了它,顺道写了一篇[文章](https://segmentfault.com/a/1190000017955654)。 + + +另一个有趣的事情是慢慢在公司有了起色,自己钻研进步了很多。在做的东西 iOS Native + Hybrid 模式的应用,因为在校实验室期间有 web 前端经验,所以在公司经常设计 JS 与 Native(iOS、Android)的通信机制、Hybrid 的能力等等,Debug 的时候经常踩坑,比如 Android 浏览器访问 localStorage 需要开启权限、某些低版本的 Android 不支持 ES6 写法(Vue、React等工程自动引入 Babel 打包构建的工程是转换过的。之前的老工程直接写 ES6,则低版本浏览器不支持)。还有一些 CSS 在 iOS、Android 两端表现不一致。因为公司和别的上市公司合作项目,成立了微信群,我们公司对外输出 SDK,由于另一名同事解决不了。直接跟我说“刘哥,上吧”。由于经常解决疑难杂症,导致对面那个公司的项目经理,问我要不要跳槽,让我去他们公司工作 😂 + + + +## 优秀工程师如何进阶 + +说到工程师,肯定要谈如何进阶。那么我作为一个刚毕业不久,从 普通工程师 -> 高级工程师 -> 资深工程师,谈经验不敢当,说说个人的心路历程吧。 + +首先刚踏入职场,你必须完成从学生到职场的转换,这个转换不是说换个地方做事就行了,而是态度和观念需要改变。学生时代在校不管是课程作业还是毕设、或者学校实验室的那些东西,现在看看都很 low(排除一些高端院校的顶级实验室项目)。 + +遇到项目,你要认真分析、思考、编码。完了之后想想有没有优化空间、[代码规范](https://github.com/FantasticLBP/codesnippets)。代码写多了,可以自定义自己的[工作流](https://github.com/FantasticLBP/GitWorkflow) 和[快捷键](https://github.com/FantasticLBP/knowledge-kit/blob/master/第六部分%20开发杂谈/6.11.md)。 + +很多人强调没时间去学习,说什么白天在工作,下班后可能要回家路上花费时间、吃晚饭、然后累了一天可能要想着健身、娱乐下。一天除非睡觉、吃饭时间你有多少时间?所以我觉得学习和工作不是互斥的事件,你需要矫正态度,工作中的每个需求都是一次学习进阶和检验的过程,别人给了需求,你需要系统设计、架构设计、思考需要几个类、每个类负责什么行为、属性,对外暴露什么接口,类与类如何通信、如何低耦合、高内聚、可拓展?Unit test 设计等等问题都要考虑清楚。剩下就是编码实现了,这个时候考虑的就是一行行代码如何书写,才符合团队规范、业界规范、代码如何分组、如何做到自解释、代码注释等等都需要做到极致。完成后跑单元测试、最后集成测试。提交给测试工程师的项目个人至少保证 90% 的测试 case 通过,ut 覆盖率至少 90%。关于测试也有一堆经验,等后期我会出文章讲讲。 + + +个人学习和工作的时候注意积累和归纳总结,梳理自己的技术栈和知识体系,等下次学到了新的东西塞到体系里面对应的地方去。最后你的知识系统会越来越完善。 + +工程师强调几个方面: +- 专业能力是第一要素。你的专业能力决定你是否具有不可代替行和价值。 +- 软技能决定你的广度和效率:比如查找知识的能力、快速定位问题的能力、快捷键的熟练程度、自己的 WorkFlow、代码规范程度、为团队打造工程化的程度。比如我经常翻墙查找资料,公司的网络和自己家里的网络都可以翻墙查找资料。使用 iterm2 自己制定了很多 iOS、Web 相关的快捷键、这将显著提高工作效率、一天提高1分钟,一年下来就不少的时间节约(如果想看我的快捷键或者 WorkFlow 可以在评论区留言)。团队的代码规范在前端开发中有 ESLint 集合业界的 js 规范比如 airbnb 或者标准的。iOS 这端我使用 shell 脚本开发了一套团队的代码规范,最后还有一套 shell 脚本检测全局工程的代码规范,最后会生成报表用浏览器自动打开。 +- 不要过分追求框架等表层东西,要思考原理。很多人初学前端会纠结用 Vue、React、Angular哪个?我觉得没必要,你首先需要打好基础功,基础功好了,框架就很好学习了。框架一般做的事情是在现状的基础上做了封装,让你很方便的做某些事情,或者使用一些设计模式或者先进一些的开发方式(比如单向数据流、虚拟 Dom、组件化等)。这些东西为什么诞生?还不是传统的命令式编程效率太低、操作 DOM 成本太高了,复杂逻辑的页面维护很复杂吗?因为是命令式编程,所以你需要思考每一步步骤,以及中间产生的状态变量,告诉计算机如何处理具体逻辑,导致代码量太大,维护不方便。响应式编程 + 虚拟 DOM + 单向数据流催生了类 React 的前端框架。比如我之前就看过 Vue 的关键逻辑的源代码,看过 React 的 Redux 的源代码。 +- 你的水平高了你关心的技术点应该就是业界的研究方向了,比如多端融合能力。你会看到现在的 Flutter、React Native、Hybrid 他们不是没有关系的,而是一步步演进升级。电商公司(其他业务不再举例)业务经常变、运营活动经常变,传统的开发方式需要发布、审核、上线,iOS 这里尤其负责,审核严格,随意这样的流程不能满足,早期的 Hybyrid 就是这样诞生的,Native 提供基础能力,JS 写业务逻辑和 UI操作,但是这样很零散,比如 UI 操作,JS 需要一个 UI,Native 就需要事先注册号或者提供好这样的能力。React Native 解决了这个问题,JSX 的方式写业务,操作数据,setState 做 diff 算法计算出需要更新的虚拟 DOM,在移动端这样的虚拟 DOM 就可以和 Native UI 组件进行绑定,这样就可以有 Native UI 能力。JS 写业务,Native 捕获事件回调给 JS,然后操作数据,继续 setState。这样还不错,但是在低端手机上会卡顿,其中一个问题就是不同语言(JS、native语言)之间通信效率比较低,Flutter 诞生了,Dart 诞生就是为了解决 JS 的缺点。Google 自己的 skia 渲染引擎封装了 OpenGL 接口,所以不需要 Native 提供 UI 能力,Dart 使用声明式写 UI、Skia 去渲染看上去还不错,所以一开始国内的咸鱼团队就在跟进 Flutter,现在阿里 all in Flutter。 + + + +## 经验分享 + +大多数人喜欢碎片化时间阅读一些文章或者自己感兴趣的技术博文。比如你在上下班路上、点餐等饭时间看几个公众号技术文章,很多人会大致浏览完。我觉得这样不如不读,或者收效甚微。因为脑子只有印象的话,看完的文章过半年后问,肯定一问三不知了。与其多篇文章大致浏览,不如精读一篇文章,并动手实践每个技术点和细节。必要时做好博文记录最后总结输出。 + + +另外对于每个技术不要停留在会用(我称之为 api 资深工程师)而是知道这个 api 背后的原理,设计模式、设计的优缺点。比如 iOS 领域著名的 WKWebView 很多人知道 NSURLProtocol 可以拦截其他的网络请求却拦截不到它里面 post 的 body 内容。你查看了 webkit 源代码之后就知道 WKWebView 官方宣传快,是说自身做的事情少了,很多任务比如网络是新开了一个独立线程去处理,所以独立线程处理网络完了通过 IPC 的方式将 post 的 body 通过压缩然后 IPC 给 WKWebView 这会非常消耗资源,所以系统索性不给你传递了。 + +带着这个疑问看看 Chrome for iOS 的开源项目(至于为什么会看它?因为个人研究多端融合能力、Chrome 这样的浏览器如何渲染处理等流程感兴趣所以看的),看到它里面在用 post 传 body。纳闷了,和 webkit2 源代码不一致。这些现象等都是需要深入才可以理解的。当你遇到一个问题发现网上找不到资料或者资料比较少的时候你就算对这个问题的研究比较深入了。 + +此外,不管做产品还是技术都不要凑合,必须要做到极致或者最好。上面说的反爬虫技术是我在公司担任 iOS 工程师的时候做的。当时进去一个月写完一个 App,追到 Android 进度。然后不满足于进度,在此基础上做到的一些优化、且通过抓包方式进行业务测试的时候找到了 Android 和服务端的一些 Bug,最后还发现安全性较低,在此基础上,做了 HTTPS + 证书验证 +RSA 证书校验、AES 数据加密,做到了 App 被别人抓包马上就断掉链接。假如技术再高明些看到请求信息也是加密过的(不是 HTTPS 自己的加密,是自定义的加密)和防重放策略。 + +之后鉴于公司的网站太落后,用 Vue 进行重写,再做了安全升级等工作,这样总经理看到个人能力,担任小公司大前端负责人的岗位。这阶段的成长也蛮快的 +然后换工作,也是一样,严格要求自己,半年时间做了无痕埋点、组件化、模块化、Hybrid 能力提升、商城业务模块开发等等,从高级工程师升级为资深工程师。个人目标2年后成为技术专家。 + + +很多人会去问有没有较好的学习资料是什么?我觉得如果有人回答除官方文档之外的答案,那么这个人本身就不够专业。在我看来官方文档是设计者(最熟悉技术细节的人)写出的注释和说明。那么肯定是最佳实践。举个例子 Objective-C 里面对 NSDictionary 进行处理成 JSON 字符串,解析的结果是会带空格和换行符的,但是服务端恰好如果对空格和换行敏感的话,那么你们的逻辑就会和预期的不一致。看看下面的代码。 + + +```Objective-C +NSJSONWritingSortedKeys(兼容性)、NSJSONWritingPrettyPrinted (换行符) +NSDictionary *dict = @{@"name": @"lbp"}; +NSData *json = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil]; +NSString *jsonString = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; +```` + +很多人转换后可能会去做字符串处理,正则替换空格和换行符。稍微好一些的可能会看到官方的在 iOS11 推出一个新的枚举值 `NSJSONWritingSortedKeys`。那么就是判断当前系统小于 11 则字符串替换,否则就使用新的参数。如果你仔细看官方文档,上面说的非常仔细。看看下面的官方说明。 + + +> Generate JSON data from a Foundation object. If the object will not produce valid JSON then an exception will be thrown. Setting the NSJSONWritingPrettyPrinted option will generate JSON with whitespace designed to make the output more readable. **If that option is not set, the most compact possible JSON will be generated.** If an error occurs, the error parameter will be set and the return value will be nil. The resulting data is a encoded in UTF-8. + + + +另外,越来越觉得架构设计对于架构组同学来说是非常重要的。 + +为什么?假设你一个需求,预期10天时间;前期架构设计、类的设计、Uint Test 设计估计7天,到时候编码开发2天完成。 + +这么做的好处很多,比如: +- 除非是非常优秀,不然脑子想的再前面到真正开发的时候发现有出入,coding 完发现和前期方案设计不一样。所以建议用流程图、UML图、技术架构图、UT 也一样,设计个表格,这样等到时候编码也就是 coding 的工作了,将图翻译成代码 +- 后期和别人讨论或者沟通或者 CTO 进行 code review 的时候不需要一行行看代码。你将相关的架构图、流程图、UML 图给他看看。他再看看一些关键逻辑的 UT,保证输入输出正确,一般来说这样就够了 +- 软件项目管理也一样,确定好关键时间节点、甘特图、确定干系人、kick-of meeting、定期碰头等 + + +## 程序员这个工作怎么样? + +个人是很喜欢程序员这个工种的。做事情纯粹些、且培养了不断学习思考的能力和习惯。不断学习和思考是每个行业都需要的基本素养,所以看到事情本质、不断学习、不断进阶就是人生常态吧 diff --git a/Chapter7 - Geek Talk/7.16.md b/Chapter7 - Geek Talk/7.16.md new file mode 100644 index 0000000..74468fd --- /dev/null +++ b/Chapter7 - Geek Talk/7.16.md @@ -0,0 +1,22 @@ +# 区块链技术和比特币 + +> 近日, 国家在中央政治局第十八次集体学习时强调: 把区块链作为核心技术自主创新重要突破口,加快推动区块链技术和产业创新发展. + +一夜之前,区块链技术成为大家口中的焦点, 此外各种相关的“币”也摇身一变,翻了好几番身价.可能有人要问了区块链技术和比特币、各种币到底是什么关系? 网上各种“炒币、割韭菜”这些词捆绑在一起. + +那么到底什么是区块链技术、什么是比特币? + +### 中心化和去中心化 + +1. 周末在家从电影天堂站点下载的电影,这个行为属于中心化 +2. 假如电影天堂的某部电影被10万用户下载过,都保存在自己的电脑里.除非10万人的电脑同时坏掉、互联网服务不可用,否则这部电影不可能消失,这就是去中心化 + +假如存在每个人电脑上面的电影被黑客入侵,破坏或删除了电影内容. 怎么办? + +有一个很聪明的人叫做“中本聪”,他发明了一种新技术.假如有这样一个协议,协议内容规定 + +``` +本人承认:不得编译修改本影片中的任何细节, 同时针对这部影片的任何行为, 都会按时间戳记录, 并同步广播更新存储 +``` + +这样一来, 假如张三看电影到 12分50秒, 他电脑重的影片会按时间戳更新最新播放进度的记录,然后广播传输给其他的 10万台电脑,同步更新保存张三电脑中的数据. \ No newline at end of file diff --git a/Chapter7 - Geek Talk/7.17.md b/Chapter7 - Geek Talk/7.17.md new file mode 100644 index 0000000..af1c026 --- /dev/null +++ b/Chapter7 - Geek Talk/7.17.md @@ -0,0 +1,52 @@ +# 如何写一份夺目的简历 + +​​ +经常有很多朋友或者学弟学妹让我辅导简历, 让简历在筛选的时候从一堆被筛选的简历里面更加夺目,从而获得一个面试机会.自己写过几次,也帮助朋友辅导过、也面试过别人,所以有一点点小心得,而且这个心得是随着自己的技术阅历和对人生阅历的增长而持续改变的,所以这篇文章是会持续更新的. + +> 如何让简历加分、夺目,本质上就是让简历的内容更加有价值、让看的人更加知道你具备什么样的技能和综合素质, 和当前的招聘需求是否足够匹配, 以致于看完之后能否给你邀约,给你和面试官一个互相了解的机会.(偏向于社招,对于学生而言也有一定的参考价值) + +## 什么是简历 + +> 个人简历是求职者给招聘单位发的一份简要介绍。包含自己的基本信息:姓名、性别、年龄、民族、籍贯、政治面貌、学历、联系方式,以及自我评价、工作经历、学习经历、荣誉与成就、求职愿望、对这份工作的简要理解等等。以简洁重点为最佳标准。 ~摘抄自网络 + +一份简历可以让查看简历的人(为什么不用面试官? 因为一般来说你投出去的简历可能是某公司的 HR、也有可能是交给朋友内推、也有可能是某个猎头,所以查看简历的人身份可能有很多)快速的了解你, 知道你的基本信息(什么学校、什么专业、时间段)、专业技能信息(掌握什么技术、掌握什么行业经验)、实践经验信息(在什么时间段在什么公司做过什么项目,有何成长). + +所以鉴于上述目的和意义,你的简历需要具有真实性、针对性、条理性. + + + +## 怎么样写好 + +简历只是一个敲门砖,我们的目的是让面试官看上去产生好感,然后给我们一个面试的机会,简历不需要表达全部的自己,面试官一定是从面试中获取大量的情报来深入了解你。所以简历写到什么程度,我们的目标是相当明确的。 + +如何从简历中让人看出你【做成了】什么,以及你未来【可能做成】什么,以下几点可以参考: + +1. 表达一件有挑战的完整的事情. + 比如实现了一个平台的架构推导和落地; 再如使用三个月时间经历四轮优化将系统的整体性能提升了 1.2 倍等等;而不是我写了某个网站的前端,沉淀了几个通用组件等等 +2. 展示你跨专业的特性 + 比如作为前端工程师,我在运维工作上做了比较突出的贡献;在图形图像方面有深入研究和良好产出;曾并发管理 4 个中型项目并保证准时交付等等; +3. 从业务价值的角度去发现问题和分析问题 + 比如通过融合两个相似系统后,降低了运营的成本,提高了数据从线下到线上展示的效率等;而不是简单地说,实现了数据层的合并,统一了前台交互等这种纯技术的表达 + +说到底,还是需要让面试官看到你这几方面的能力: + +- 技术综合能力/学习能力(硬实力) +- 业务洞察力(理解力) +- 拿结果的能力(执行力) + + +## 可参考的点 + +看过不少简历,优秀的人大概都有这么一些特征(具备两三条甚至更多): + +1. 名企呆过,有大厂背书 +2. 参与的项目非常有震撼力 +3. 在技术领域跨度大的项目有成绩 +4. 有门槛的技术领域里有沉淀 +5. 技术产出对团队/业务有突出的影响 +6. 接二连三的专利 +7. 拿过一些很难拿到的奖项 +8. 非常好的韧劲,某一/几件事情上坚持不懈 +9. 思考力惊人,行动力惊人 +10. 思维敏捷且逻辑无比严谨 +11. 具备一定的社区影响力​​​​ \ No newline at end of file diff --git a/Chapter7 - Geek Talk/7.18.md b/Chapter7 - Geek Talk/7.18.md new file mode 100644 index 0000000..0e096bd --- /dev/null +++ b/Chapter7 - Geek Talk/7.18.md @@ -0,0 +1,4 @@ +# 团队写作、标准开发流程 + +### 研发流程规范 + diff --git a/Chapter7 - Geek Talk/7.19.md b/Chapter7 - Geek Talk/7.19.md new file mode 100644 index 0000000..9a0a1bd --- /dev/null +++ b/Chapter7 - Geek Talk/7.19.md @@ -0,0 +1,26 @@ +# 云服务器靠谱推荐 + +作为计算机从业人员,不管做前后端、算法、运维有台自己的云服务器至关重要。 + + +## 为什么要买云服务器 + +首先,我作为一名客户端工程师分析下我购买云服务器的初衷。 我首先是想大搭建自己的博客系统、然后我还会写一些 App 然后自己写 App 的后台服务器,可能是 Node 语言也可能是 PHP 语言。也有可能部署一些定时服务或者 Spider 项目 😂 + +1. 可能有些人说有成熟的博客系统,为什么要自建? + 我想着自己搭建的话,可以随意定制开发;另外可以掌握服务端运维的一些知识 +2. 那有了云服务器可以做什么? + 自己做文件服务器、邮件服务器、App 服务器、爬虫脚本、定时服务、个人博客、社区平台等等、也可以训练模型等等 + + +## 推荐靠谱的产品 + +之前用过阿里云服务、新浪云服务。这次良心安利一波腾讯云服务,在 11.11 折扣还在,价格很良心。推荐我购买的配置 + + +![云服务器](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-11-26-VPS.png) + +[购买地址](https://cloud.tencent.com/act/cps/redirect?fromSource=gwzcw.3018172.3018172.3018172&redirect=10140&cps_key=8ec198cb362709e63ff6e05753a3d0d3&from=activity) + + + diff --git a/第五部分 开发杂谈/5.3.md b/Chapter7 - Geek Talk/7.2.md similarity index 77% rename from 第五部分 开发杂谈/5.3.md rename to Chapter7 - Geek Talk/7.2.md index c37102b..7ed060b 100644 --- a/第五部分 开发杂谈/5.3.md +++ b/Chapter7 - Geek Talk/7.2.md @@ -41,7 +41,7 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 打开 Charles,当第一次启动的时候如果没有购买或者没有破解,会有倒计时,之后会看到软件的主界面,然后会请求你赋予它为系统代理的权限。点击授权会让你输入当前系统用户的密码。当然你也可以忽略或者拒绝该请求,然后等想要抓包的时候将它设置为系统的代理服务器。步骤:**选择菜单中的“Proxy” -> "Mac OS X Proxy"。**如下图: -![Charles在MAC的初始化](https://user-gold-cdn.xitu.io/2018/7/18/164ac9aa738ccd2a?w=622&h=588&f=png&s=176517) +![Charles在MAC的初始化](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Charles-Setting.png) 之后你的电脑上的任何网络请求都可以在 Charles 的请求面板中看到 @@ -49,8 +49,8 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 看看 Charles 的主界面 -![Structure模式查看网络请求](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-004135@2x.png) -![Sequence模式查看网络请求](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-004435@2x.png) +![Structure模式查看网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-004135@2x.png) +![Sequence模式查看网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-004435@2x.png) - 图上红色圈1:这里代表所有网络请求的展示方式。分别名为 “Structure” 和 “Sequence”。 @@ -66,17 +66,17 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 由于 Charles 可以将电脑或者设置过的手机的所有网络请求捕获到,而且我们分析网络传输应该是针对某个特定的网络下的抓包分析,为了清楚明显地看到我们感兴趣的网络请求通常会用到 Charles 的**“过滤网络请求的功能”**。 - 方法1:在 Charles 主面板的左侧所有网络请求的下方可以看到看到一个 **”Filter“** 输入栏,在这里你可以输入关键词来筛选出自己感兴趣的网络请求。比如我想分析的网络请求来自于”www.baidu.com" 下,你可以在下面输入"baidu"即可。 - ![Filter 过滤网络请求](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-090550.png) + ![Filter 过滤网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-090550.png) - 方法2:在 Charles 菜单栏的顶部会看到 “Proxy” 的选项,点击菜单栏选择 “Proxy” -> "Recording Settings" 。选择 “include”。看到面板上面有一个 “Add” 按钮,点击后在弹出的面板里面设置好我们需要分析的网络请求的**协议、主机名、端口、路径、参数**,当然你也可以只设置一些主要的信息,比如协议和主机名的组合。 - ![Recording Settings 过滤网络请求](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-090926.png) + ![Recording Settings 过滤网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-090926.png) - 方法3:一般打开 Charles 并设置好配置信息后(比如电脑本机或者设置过代理的手机)所有的网络请求都将在 Charles 的面板上显示,同时我们感兴趣的网络请求如果也在面板上显示的话,**“Structure”模式下**可以选中需要分析的网络请求,鼠标右击选择**“Focus”**。**“Sequence”模式下**可以在面板的网络请求显示面板的右下角看到一个**Focus**按钮,点击勾选后 Charles 只会显示你感兴趣的网络请求。 - ![Structure模式下Focus过滤网络请求](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/屏幕快照%202018-07-23%20上午9.22.39.png) + ![Structure模式下Focus过滤网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-07-23%20上午9.22.39.png) - ![Sequence模式下Focus过滤网络请求](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180723-092259.png?raw=true) + ![Sequence模式下Focus过滤网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-092259.png) @@ -90,7 +90,7 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 要截获 iPhone 的网络请求就需要为 Charles 开启代理功能。在菜单栏选择**“Proxy” ->"Proxy Settings"**。填写代理的端口号并将**“Enable transparent HTTP proxying”**勾选上。 - ![抓取手机网络请求的电脑端设置](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-092856.png) + ![抓取手机网络请求的电脑端设置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-092856.png) @@ -98,7 +98,7 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 在电脑“系统偏好设置”中心打开网络查看本机 IP 地址,打开手机“设置”->“无线局域网”,进入当前使用的网络,点击进入当前 WIFI 的详情页(可以看到当前 WIFI 的基本信息,包括子网掩码、端口、IP地址、路由器),在最下角可以看到**“DNS”和“HTTP代理”**2个section。我们点击**“配置代理”**,设置 HTTP 代理选中“手动”。服务器处填写电脑ip地址,端口写8888。设置好后,我们打开 iPhone 上的任意需要网络请求的应用,就可以看到 Charles 弹出请求的确认菜单,单击"Allow"按钮,即可完成设置。 - ![抓取手机网络请求的手机端设置](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/11532309922_.pic.jpg) + ![抓取手机网络请求的手机端设置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/11532309922_.pic.jpg) @@ -110,51 +110,51 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 - 首先需要在 MAC 上安装证书。点击 Charles 顶部的菜单栏,选择 **“Help” -> "SSL Proxying" -> "Install Charles Root Certificate"**。 - ![HTTPS抓包电脑端证书安装](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/164ac9aa40822368.png) + ![HTTPS抓包电脑端证书安装](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9aa40822368.png) - 在 keychain 处将新安装的证书设置为永久信任 - ![HTTPS抓包电脑端证书信任](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/164ac9aab5332151.png) + ![HTTPS抓包电脑端证书信任](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9aab5332151.png) - 即使安装了 CA 证书,Charles 默认是不捕获 HTTPS 协议的网络请求,所以我们需要对某个主机下的网络请求抓包分析的话,选中该网络请求右击选中 **“SSL Proxying Enabled”**。这样就可以看到我们感兴趣的HTTPS 网络请求了。 - ![Charles确认开启抓取HTTPS](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/屏幕快照%202018-07-23%20上午9.47.09.png?raw=true) + ![Charles确认开启抓取HTTPS](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-07-23%20上午9.47.09.png) 如果你需要捕获移动设备的 HTTPS 网络请求,则需要在移动设备上安装证书并作简单的设置 - 选择 Charles 顶部菜单栏选择 **“Help” ->"Install Charles Root Certificate on a Mobile Device or Remote Browser"**。然后就可以看到 Charles 弹出的安装说明了。 - ![Charles提示手机端安装CA证书](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180723-101259.png?raw=true) + ![Charles提示手机端安装CA证书](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-101259.png) - 在手机设置好 Charles 代理的情况下,在手机浏览器输入 **“chls.pro/ssl”**。安装提示下载好**CA证书**。 - 验证刚刚安装的 CA证书 - ![描述文件的验证](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/164ac9a99ea05b2e.png) + ![描述文件的验证](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9a99ea05b2e.png) - iPhone 打开设置 -> 通用 -> 关于本机 -> 证书信任设置 -> 开启开关 - ![手机端CA证书的信任](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/164ac9a9ca26c907.png) + ![手机端CA证书的信任](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9a9ca26c907.png) - 在 Charles 菜单栏 Proxy -> SSL Proxying Setting -> 点击 Add 按钮 -> 在弹出的对对话框设置需要监听的 HTTPS 域(*:代表通配符) - ![HTTPS抓包端口和主机设置](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/164ac9aaad2c0ff8.png?raw=true) + ![HTTPS抓包端口和主机设置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9aaad2c0ff8.png) - 设置完毕,尽情抓取你想要的 HTTPS 网络请求吧。 - ![抓取京东HTTPS数据](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/164ac9a9a966fafe.png?raw=true) + ![抓取京东HTTPS数据](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/164ac9a9a966fafe.png) - ## 模拟弱网环境 +## 模拟弱网环境 在平时开发的时候我们经常需要模拟弱网环境,并作弱网环境下的适配工作。Charles 为我们提供了这个服务。 在 Charles 菜单栏选择 **“Proxy” -> "Throttle Settings"**。在弹出的面板上设置网络请求的参数(上行,下行带宽、利用率、可靠性等等信息)。如下图所示。 - ![模拟弱网环境](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/屏幕快照%202018-07-23%20上午10.27.22.png?raw=true) + ![模拟弱网环境](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-07-23%20上午10.27.22.png) 如果你想对**指定主机**进行弱网环境下的测试,可以点击上图的“Add”按钮,在弹出的面板上设置协议、主机、端口来对指定的主机进行弱网设置。 - ![设置指定网络请求的弱网模拟](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-102606.png?raw=true) + ![设置指定网络请求的弱网模拟](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-102606.png) @@ -164,7 +164,7 @@ Charles 的工作原理是将自身设置为系统的代理服务器来捕获所 对于捕获的网络请求,我们经常需要修改网络请求的cookie、Headers、Url等信息。Charles 提供了对网络请求的编辑和重发功能。只需要选中需要修改编辑的网络请求,在对应的右上角看到有一个“钢笔”的按钮,点击后就可以对选中的网络请求进行编辑了,编辑好后可以在右下角看到 **Execute** 按钮。这样我们编辑后的网络请求就可以被执行了。 -![修改网络请求](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-105440.png?raw=true) +![修改网络请求](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-105440.png) @@ -189,7 +189,7 @@ Map 功能分为 Map Local(将某个网络请求重定向到本地 JSON 文件 适合于切换线上到本地、测试服务到正式服务的场景。比如下图从正式服务切换到测试服务 -![Map Remote](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-113200@2x.png?raw=true) +![Map Remote](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-113200@2x.png) @@ -197,11 +197,11 @@ Map 功能分为 Map Local(将某个网络请求重定向到本地 JSON 文件 我们需要填写重定向的原地址信息和本地目标文件。我们可以先将某个接口的响应内容保存下来(选择对应的网络请求,右击点击 **Save Response** )成为 data.json 文件。然后我们编辑里面的 status 、message、data 等信息为我们想要的目标映射文件。 -![Save Response](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/屏幕快照%202018-07-23%20上午11.37.44.png?raw=true) +![Save Response](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-07-23%20上午11.37.44.png) 如下所示,我将一个网络请求的内容映射到我本地的一个 JSON 文件。之后这个请求的内容都从网络变为返回我本地的数据了。 -![Map Local](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-113951.png?raw=true) +![Map Local](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-113951.png) @@ -216,7 +216,7 @@ Rewrite 适合对某个网络请求进行正则替换,以达到修改结果的 - 可以针对特定的网络请求进行 Rewrite。可以点击右上角 **Location** 面板下面的 **Add按钮**。在弹出的面板上设置网络请求配置信息。注意此时需要同时设置 Protocol、Port、Host、Path信息(我测试加了 Protocol、Host、Port这3个是无效的) -![Rewrite 针对特定网络请求的设置](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-115820.png?raw=true) +![Rewrite 针对特定网络请求的设置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-115820.png) - 然后对指定的 **Type** 和 **Action** 进行 Rewrite。 @@ -224,11 +224,11 @@ Rewrite 适合对某个网络请求进行正则替换,以达到修改结果的 Where 可以选择 Request 和 Response。指的是下面的修改是针对 Request 还是 Response - ![Rewrite 设置范围](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-115855.png?raw=true) + ![Rewrite 设置范围](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-115855.png) - 完成设置后点击 **Apply** 按钮,即可生效。下次继续请求该网络,返回的内容就是我们刚刚设置的内容。比如当前的“政策法规”要变成“哈哈哈,我是假的政策法规”。这时候就可以使用 Rewrite 功能 - ![Rewrite 测试结果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-114826.png?raw=true) + ![Rewrite 测试结果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-114826.png) #### Breakpoints 功能 @@ -238,9 +238,9 @@ Breakpoints 相比于其他几个修改网络请求的特点是只是针对当 如下图:对该接口设置了 Breakpoints。请求网络后 Edit Response,点击 execute 后服务端返回的结果就是我们编辑的内容了。 -![对指定的网路请求设置断点](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-151811.png?raw=true) -![在Reponse的时候修改返回的数据](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-151850.png?raw=true) -![再次请求该接口返回的数据为我们设置过的](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-151906.png?raw=true) +![对指定的网路请求设置断点](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-151811.png) +![在Reponse的时候修改返回的数据](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-151850.png) +![再次请求该接口返回的数据为我们设置过的](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-151906.png) @@ -248,7 +248,7 @@ Breakpoints 相比于其他几个修改网络请求的特点是只是针对当 我们可以使用 Charles 的 **Repeat** 功能地对服务器进行并发访问进行压力测试。步骤:**选中某个网络请求 -> 右击 -> Repeat Advanced -> 在弹出的面板里面设置总共的迭代次数(Iterations)、并发数(Concurrency) -> 点击“OK” 。**开始执行可以看到以设置的并发数的规模,进行总共达设置的总共迭代次数的访问。(专业的压力测试工具:**Load Runner**) -![简单压力测试](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-135943.png?raw=true) +![简单压力测试](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-135943.png) ## 反向代理 @@ -258,7 +258,7 @@ Charles 的反向代理功能允许我们将本地指定端口的请求映射到 如下所示,我将本地的 8080 端口映射到远程的 80 端口上,点击 OK 生效后,当我继续访问本地的 80 端口,实际返回的就是远程 80 端口的提供的内容了。 -![反向代理设置](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180723-141057.png?raw=true) +![反向代理设置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180723-141057.png) ## 解决与翻墙软件的冲突 diff --git a/Chapter7 - Geek Talk/7.20.md b/Chapter7 - Geek Talk/7.20.md new file mode 100644 index 0000000..06c5fb2 --- /dev/null +++ b/Chapter7 - Geek Talk/7.20.md @@ -0,0 +1,127 @@ +# 规范化团队 git 提交信息 + +> 同一个工程项目,为了方便管理,git 的 commit 信息最好按照一定的格式规范,以便在需要的时候方便使用。什么是方便的时候,比如出现了一个线上 bug,所以需要回滚操作,知道了提交信息可以方便的定位问题。 + + + + +## 实现 + +可以马上想到的是利用 shell 结合 git hook 实现在 git commit 阶段检查输入是否符合规范。符合就通过,不符合就不通过,抛出警告信息,并给出提示信息。 + + +### 规范是什么 + +常见的分类有下面几种情况: + +- build:修改项目的的构建系统(xcodebuild、webpack、glup等)的提交 +- ci:修改项目的持续集成流程(Kenkins、Travis等)的提交 +- chore:构建过程或辅助工具的变化 +- docs:文档提交(documents) +- feat:新增功能(feature) +- fix:修复 bug +- pref:性能、体验相关的提交 +- refactor:代码重构 +- revert:回滚某个更早的提交 +- style:不影响程序逻辑的代码修改、主要是样式方面的优化、修改 +- test:测试相关的开发 + + + +### 轮子 + +在 github 上有 [commitlint](https://github.com/conventional-changelog/commitlint) 这个项目,它可以很方便的在工程中做配置,并允许你自定义上面说的「规范」、「分类」。 + +[commitlint](https://github.com/conventional-changelog/commitlint):用于检查提交信息 +[husky](https://github.com/typicode/husky):hook 工具,用于 git-commit 和 git-push 阶段。 + + +怎么用? + +1. 初始化一个 node 项目:`npm init -y` +2. 安装所需依赖。`npm install --save-dev @commitlint/config-conventional @commitlint/cli husky` +3. 在工程根目录下新建配置文件,名称为 `commitlint.config.js`。 +4. 在 commitlint.config.js 中添加配置信息 + ```shell + const types = [ + 'build', + 'ci', + 'chore', + 'docs', + 'feat', + 'fix', + 'pref', + 'refactor', + 'revert', + 'style', + 'test' + ]; + + typeEnum = { + rules: { + 'type-enum': [2, 'always', types] + }, + value: () => types + } + + module.exports = { + extends: [ + "@commitlint/config-conventional" + ], + rules: { + 'type-case': [0], + 'type-empty': [0], + 'scope-empty': [0], + 'scope-case': [0], + 'subject-full-stop': [0, 'never'], + 'subject-case': [0, 'never'], + 'header-max-length': [0, 'always', 72], + 'type-enum': typeEnum.rules['type-enum'] + } + }; + ``` +5. 在 package.json 文件中添加以下代码,代码层级跟 **devDependencies** 同级。 + ```json + "husky": { + "hooks": { + "pre-commit": "echo '哈喽,小伙伴们,在这里可以做测试相关的逻辑哦,一般结合公司的 ci'", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", + "pre-push": "echo 提交代码前需要先进行单元测试 && 可以做测试相关" + } + } + ``` + + +上面的流程配置完成,当你在提交 commit 信息的时候如果输入不符合 `: ` 规则,会给出提示信息。 + +type 就是上面的种类;subject 就是需要提交的文字概括。比如:feature:增加摇一摇推荐酒店功能。 + + +小说明: +- 如果某次提交想禁用 husky,可以添加参数 `--no-verify` + ```shell + git commit --no-verify -m "xxx" + ``` + + + + +### 大体流程 + +安装包 husky 的时候,会在目录 `.git/hooks/` 下生成一堆 shell 脚本,负责 hook git 相关时机。 + +`"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"` 这个配置告诉 git hooks,当执行 `git commit -m` 的时候触发 commit-msg 钩子,并通知 husky,从而执行 `commitlint -E HUSKY_GIT_PARAMS`,实际上执行的是 `./node_modules/husky/bin/run.js`,读取 commitlint.config.js 里的配置,然后对我们 commit -m 里的字符串校验,如不通过则输出错误信息并终止。 + + + + + +## 拓展篇 + +git commit 的几个钩子,也暴露出来了,所以可以结合时机做一些额外的逻辑。 + +- pre-commit:在 git commit 之前触发 +- commit-msg:在编写 commit 信息的时候触发 +- pre-push:在 git push 之前触发 + +所以基于上述时机,可以根据项目特点做一些别的事情。比如在 git commit 之前判断是否单元测试通过。是否自动化根据 commit 信息生成 changeLog 等。 diff --git a/Chapter7 - Geek Talk/7.3.md b/Chapter7 - Geek Talk/7.3.md new file mode 100644 index 0000000..1a556fe --- /dev/null +++ b/Chapter7 - Geek Talk/7.3.md @@ -0,0 +1,68 @@ +# git常见使用 + +``` +http://blog.csdn.net/wirelessqa/article/details/20153689 +``` + +1. git init +2. git add . +3. git commit -am "\#\#\#" +4. git remote add origin git@xx.xx.xx.xx:repos/xxx/xxx/xxx.git +5. git push origin 本地分支:远程分支 + fatal: refusing to merge unrelated histories + 决办法:git pull origin master --allow-unrelated-histories + + + +### 创建新版本 + +* 从现有的分支创建新分支 + * git checkout -b dev +* 将新创建的分支提交到远端 + * git push origin dev +* 提交代码 + * git add . + * git commit -m 'add a new branch named dev' + * git pull origin dev + * git push origin dev + + + +## Githubu 不允许上传超过 100MB 文件 +关键点: +- git-lfs 添加允许上传的文件名称或者文件类型 +- 如果按照顺序1-2-3-4-5-6-10-11-12 还是会存在上传不成功的情况,所以需要严格按照下面的顺序执行 + +```Shell +//1 +git rm --cached -r video/Company-Website-Pro.mov +//2 +git commit --amend +//3 +git push +//4 +brew install git-lfs +//5 +git lfs install +//6 +git lfs track "*.mov" +//7 +git add .gitattributes +//8 +git commit -m '.gitattributes' +//9 +git push origin master +//10 +git add /Users/liubinpeng/Desktop/Github/Company-Website-Pro/video/Company-Website-Pro.mov +//11 +git commit -m 'video' +//12 +git push origin master + +``` + +## 代码回滚到某个 tag + +- 先查看所有的 tag: `git tag` +- 查看需要回滚的 tag 对应的 commitid: `git show {tag号}` +- 回滚: `git reset --hard {commitId}` \ No newline at end of file diff --git a/第五部分 开发杂谈/5.5.md b/Chapter7 - Geek Talk/7.4.md similarity index 100% rename from 第五部分 开发杂谈/5.5.md rename to Chapter7 - Geek Talk/7.4.md diff --git a/第五部分 开发杂谈/5.6.md b/Chapter7 - Geek Talk/7.5.md similarity index 56% rename from 第五部分 开发杂谈/5.6.md rename to Chapter7 - Geek Talk/7.5.md index 223b44c..b52969e 100644 --- a/第五部分 开发杂谈/5.6.md +++ b/Chapter7 - Geek Talk/7.5.md @@ -1,6 +1,6 @@ # Git 实用操作 -1、合并多次提交记录 +## 合并多次提交记录 有的时候我们对于某个功能为了实时保存自己写的代码,可能会有多次提交,所以等功能稳定下来,我们可能会有这种需求:将前面多余几次的提交记录合并为1个记录。幸运的是 Git 为我们提供了这样的命令。 @@ -29,4 +29,42 @@ 将全部的提交记录合并为1个 - \ No newline at end of file + +## 删除项目中所有的提交记录 + +之前个人在 Github 开源了一些完整的项目,但是有些人用于商业用途,所以有了这个需求,就是将项目改动一下,删除一些重要代码提交最新的代码到仓库并且让用户不能通过提交的历史记录会滚到指定的版本看到代码。 +以下为步骤 + + +``` +1.Checkout + + git checkout --orphan latest_branch + +2. Add all the files + + git add -A + +3. Commit the changes + + git commit -am "commit message" + + +4. Delete the branch + + git branch -D master + +5.Rename the current branch to master + + git branch -m master + +6.Finally, force update your repository + + git push -f origin master +``` + +## 给项目打 tag + +```shell +git tag -a 1.0.0 -m 'release SPM lib' +``` \ No newline at end of file diff --git a/第五部分 开发杂谈/5.7.md b/Chapter7 - Geek Talk/7.6.md similarity index 100% rename from 第五部分 开发杂谈/5.7.md rename to Chapter7 - Geek Talk/7.6.md diff --git a/第五部分 开发杂谈/5.8.md b/Chapter7 - Geek Talk/7.7.md similarity index 90% rename from 第五部分 开发杂谈/5.8.md rename to Chapter7 - Geek Talk/7.7.md index 861150e..b5da89c 100644 --- a/第五部分 开发杂谈/5.8.md +++ b/Chapter7 - Geek Talk/7.7.md @@ -1,4 +1,4 @@ -## 浅谈iOS和Android后台实时消息推送的原理和区别 +# 浅谈iOS和Android后台实时消息推送的原理和区别 > 前言 iOS和Android上的实时消息推送差异很大,往小了说是技术实现的差异,往大了说是系统实现理念的不同。实时消息推送在移动端互联网时代很平常,也很重要,它的存在让智能终端真正成为全时信息传播的工具。本文将从原理上谈谈两个平台上实时消息推送的区别。 @@ -17,17 +17,17 @@ * iOS 实时消息推送 iOS的推送是通过苹果自己的APNs服务进行的,用户需要将device\_token以及消息内容等推送信息交给APNs服务器,剩下的均由苹果自己来完成。iOS应用的推送大部分情况下都要依赖苹果生态提供的APNs(Apple Push Notification Service)服务。 - - ![](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/db29e6a729147172d199cde6e2cf3682_hd.png?raw=true) +https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/db29e6a729147172d199cde6e2cf3682_hd.png + ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/db29e6a729147172d199cde6e2cf3682_hd.png) 首先作为设备标识的device-token是由APNs颁发的,App开发者或者第三方推送平台\(图中的Provider\)做的工作是收集这个device-token,APNs的推送是要求基于APNs颁发的device-token来推送的。只有正确的device-token会被APNs接受,如果是一个错误的、或者无效的device-token\(比如App已经卸载了\),APNs就不会接受。 - ![](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/442e4085cf4b8f62c7ad359343c5f155_hd.png?raw=true) + ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/442e4085cf4b8f62c7ad359343c5f155_hd.png) 接着开发者使用第三方推送平台(图中的Provider)在将推送内容与范围选定之后进行推送,第三方推送平台将信息提交给APNs,剩下的操作全部都由APNs来进行完成,整个过程第三方推送平台就不能控制了。但是如果提供的device\_token是失效的(app被卸载、系统版本升级导致device\_token变化等情况)那么推送过程就会被中断,频繁的断线重连甚至会被APNs认为是一直DoS攻击。([详情可以参考为什么苹果的推送,两次推送之间间隔比较久的话,第二次推送会很慢?](http://www.baidu.com)) * Android 实时消息推送 - Android平台在不使用GCM的情况下就需要将自己的服务器或是第三方推送服务提供商的服务器与设备建立一条长连接,通过长连接进行推送。但是不建议自己设置服务器实现推送功能,一是因为成本太高(开发成本、维护成本),自己搭建的服务器无论是稳定性还是速度上都比不了第三方推送服务提供商的效果。另一个是因为自己的数据量较小,使用第三方推送服务提供商可以用他们的维度进行推送,实现精准推送。友盟推送就是做的比较好的,可以根据用户分群、地区、语言等多维度进行推送,最大程度减少对于用户的干扰,仅把消息推送给相关用户。![](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/224420fhxsp8e00v0s0b0s.png?raw=true) + Android平台在不使用GCM的情况下就需要将自己的服务器或是第三方推送服务提供商的服务器与设备建立一条长连接,通过长连接进行推送。但是不建议自己设置服务器实现推送功能,一是因为成本太高(开发成本、维护成本),自己搭建的服务器无论是稳定性还是速度上都比不了第三方推送服务提供商的效果。另一个是因为自己的数据量较小,使用第三方推送服务提供商可以用他们的维度进行推送,实现精准推送。友盟推送就是做的比较好的,可以根据用户分群、地区、语言等多维度进行推送,最大程度减少对于用户的干扰,仅把消息推送给相关用户。![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/224420fhxsp8e00v0s0b0s.png) * 开发者通过第三方推送服务提供商将信息直接下发给需要的设备,第三方推送服务提供商与设备建立一条长连接通道,并且将消息路由到APP中(图中的设备1与设备2),对于像设备3这种无网络连接或是没有成功建立长连接通道的设备,会在设备3连网且推送消息没有过期的情况下自动收到由第三方推送服务提供商推送过来的消息,保证消息不会丢失。 @@ -37,7 +37,7 @@ iOS 在系统级别有一个推送服务程序使用 5223 端口。使用这个端口的协议源于 Jabber 后来发展为 XMPP ,被用于 Gtalk 等 IM 软件中。 - ![](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/224819kyaynn1zzoyoulzn.jpg.png?raw=true) + ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/224819kyaynn1zzoyoulzn.jpg.png) 所以 iOS 的推送可以不严谨的理解为: diff --git a/第五部分 开发杂谈/5.9.md b/Chapter7 - Geek Talk/7.8.md similarity index 96% rename from 第五部分 开发杂谈/5.9.md rename to Chapter7 - Geek Talk/7.8.md index b3a7e47..75f0aa5 100644 --- a/第五部分 开发杂谈/5.9.md +++ b/Chapter7 - Geek Talk/7.8.md @@ -1,8 +1,8 @@ -## iOS 隔了较久时间推送变得缓慢 +# iOS 隔了较久时间推送变得缓慢 -![](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/442e4085cf4b8f62c7ad359343c5f155_hd.png?raw=true) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/442e4085cf4b8f62c7ad359343c5f155_hd.png) diff --git a/第五部分 开发杂谈/5.10.md b/Chapter7 - Geek Talk/7.9.md similarity index 97% rename from 第五部分 开发杂谈/5.10.md rename to Chapter7 - Geek Talk/7.9.md index c4e8600..b231811 100644 --- a/第五部分 开发杂谈/5.10.md +++ b/Chapter7 - Geek Talk/7.9.md @@ -12,7 +12,7 @@ 实际上SSL/TLS协议的基本思路是非对称加密和对称加密结合来传输数据,**一言以弊之,HTTPS是通过一次非对称加密算法(如RSA算法)进行了协商密钥的生成与交换,然后在后续通信过程中就使用协商密钥进行对称加密通信**,之所以要使用这两种加密方式的原因在于非对称加密计算量较大,如果一直使用非对称加密来传输数据的话,会影响效率。 -![Charles 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/Charles-method.png) +![Charles 原理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Charles-method.png) diff --git a/Chapter7 - Geek Talk/chapter7.md b/Chapter7 - Geek Talk/chapter7.md new file mode 100644 index 0000000..1938516 --- /dev/null +++ b/Chapter7 - Geek Talk/chapter7.md @@ -0,0 +1,23 @@ +# 第七部分 + +第七部分主要记录在开发中遇到的工具经验或者效率工具总结。 + + * [1、命令行文件查找](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.1.md) + * [2、Charles 从入门到精通](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.2.md) + * [3、git常见使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.3.md) + * [4、Chrome 正确调试姿势](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.4.md) + * [5、Git 实用操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.5.md) + * [6、短链接](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.6.md) + * [7、移动端推送杂谈-part one](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.7.md) + * [8、iOS 隔了较久时间推送变得缓慢](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.8.md) + * [9、抓包工具的原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.9.md) + * [10、Mac 成吨提升效率的技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.10.md) + * [11、OKR](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.12.md) + * [12、面试技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.13.md) + * [13、Markdown的妙用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.14.md) + * [14、1995年的资深工程师,和你谈谈如何进阶](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.15.md) + * [15、区块链技术和比特币](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.16.md) + * [16、如何写一份夺目的简历](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.17.md) + * [17、一套开发规范](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.18.md) + * [18、云服务器靠谱推荐](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.19.md) + * [19、规范化团队 git 提交信息](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.20.md) \ No newline at end of file diff --git a/Chapter8 - Finance/8.1.md b/Chapter8 - Finance/8.1.md new file mode 100644 index 0000000..0b08b8b --- /dev/null +++ b/Chapter8 - Finance/8.1.md @@ -0,0 +1,39 @@ +# 账期 + + +### “账期” 是什么? + +账期,又叫**现金(循环)周期**, 是指企业在经营中**从付出现金**到**收到现金**所需的平均时间. 它的变化会**直接影响企业所需运营资金**的数额. + +### “账期“ 计算公式 + +下面的公式就是充分认识”账期“的武林秘籍 + +``` +现金循环周期 = 存货周转期 + 应收账款周转期 - 应付账款周转期 +``` + +### 举个🌰 + +- 例1:某商贸公司 A, 从供应商 B 赊购100万货物,账期60天; 在自己仓库存储20天后, 以120万的价格出售给 C, 收款时间30天. + A 商贸公司的现金循环周期 = 20天(存货周转期) + 30天(应收账款周转期) - 60天(应付账款周转期) = -10天 + + 这个数字有什么意义呢? + 这意味着商贸公司 A, 不仅在这个经营过程中不用垫付任何一分钱,还能多利用应支付的货款100万10天时间. + +- 例2:还是这家商贸公司 A, 公司领导把收款时间改为60天,再将付款时间定为30天 + A 商贸公司的现金循环周期 = 20天(存货周转期) + 60天(应收账款周转期) - 30天(应付账款周转期) = 50天 + + 这个数字有什么意义呢? + 这意味着商贸公司 A,需要为经营最少准备100万,使用期限为50天. + +结论: +例1和例2, 虽然变化的都是账期,但是最终现金周期却差别巨大,所需运营资金更是天差地别. + + +### 你的 “账期” 也需要管理 + +各个企业拼命都在让企业保持充足的现金流,让每日公司运营的成本最低. 根据上述公式也就是拼命**延长应付账款时间**, 同时拼命**缩短应收账款时间、商品生成周期、商品在库时间**的内在商业逻辑. 能够保证**长期充足的现金流**是企业的核心能力之一. + + + diff --git a/Chapter8 - Finance/8.10.md b/Chapter8 - Finance/8.10.md new file mode 100644 index 0000000..5e0d6ee --- /dev/null +++ b/Chapter8 - Finance/8.10.md @@ -0,0 +1,30 @@ +# 存款准备金率的衍生知识 + + +​​”降准“可能大家都会从新闻联播上听到,很多人可能不太懂它具体是什么意思?降准是简称,全称是「**降低存款准备金率**」. + +率,亘古不变就是一个数字除以另一个数字. 比如利率就是利息除以本金. 汇率就是在货币兑换的过程中用一种货币除以另一种货币.比如用100人民币换了 15美元,100/15 = 6.5,也就是1美元对人民币的汇率为6.5 + +**银行的职能: 存钱、贷款**. 在我们眼中银行非常有钱,因为无数的企业、个人都从银行借钱. 似乎银行就是有钱的代名词. + +可是,问题来了,银行的钱来自哪里? 或者开银行的人的哪来那么多钱? 其实答案就是”借“. 可以说「近现代几乎所有的金融活动,本质上都离不开借」. 问题是银行向谁借钱? 背后的金主是谁? 答案再普通不过了,我们生活中的每个人都是银行的金主,都在向银行借钱,这个借钱表现为——向银行存款,也就是银行欠了我们的钱. 是不是觉得”哇,原来银行的本质是这样的, 那他们到底有多少钱是自己的?“ 可以说一个银行,大约不到10%的钱是自己的. 中国的银行资产负债率大多数在90%以上. 所谓资产负债率就是总资产里面,有多少钱是负债(指借钱). + +银行就是做存款和借贷的. 人们把钱存进去随时有可能取出,那么银行又要把钱借贷给其他人,但又怕人们取出钱,这就是一个矛盾点(定期存款也一样可以随时取出,只不过就不能享受那么高的利率了).假如你银行的利率为3%,贷款利率为5%,中间的差价2%就是银行的收入来源,用来维护银行的开销收支. +既然把钱存到银行,大多数人,大多数情况下,是不会全部把钱取出来的。至少,通常情况下,大家不会步调一致的、同一个时间把钱全部取出来。这既是一个生活常识,也是经过长期的统计得出的结论。总之,这个判断是靠谱的。 + +这样银行就可以把钱借贷给其他人或者企业,银行留一部分钱用来给随时取钱的人,可是留多少钱合适?我们去取钱,银行说没钱了.这种事情我们不能接受、人民不能接受、国家不能接受.(银行失信于人是很可怕的) + +这种情况有2个保障:国家出手管制、银行自身自律. + +国家对银行是“你把别人存在你那里的钱,拿一部分存在我这里”.存在国家的那部分钱,国家是不会再借给别人的,讲道理是非常保险的,这也是老百姓把钱存在银行的安全保证、也是最后的保证. +这个保证关系到银行的信誉. 银行的信誉也就是整个金融体系的信誉,整个金融体系的信誉也就是国家的信誉,如果失去这一点,后果不可设想... + +**银行准备金也就是银行把人们存入银行的钱拿给国家的那部分钱.银行每吸纳我们这样的人一百块钱存款,他要交给国家多少呢?这个比例就叫做存款准备金率**. 比如我们国家这一次降准之后,大型金融机构的存款准备金率就是15.5%,中小型金融机构的存款准备金率就是13.5%。 + +也就是说,对于大型金融机构来说,他每吸纳一百块钱的存款,要向国家交15块5。剩下的84块5,他才能够用于对外贷款,当然实际操作中,他还要留一部分钱,预备我们这些储户,随时到他那里去取钱。存在国家那里的那一部分准备金,不是随随便便可以用的,从逻辑上只是为了应付突发事件。对于中小型金融机构来说,也是一样的道理。 + +**存款准备金率, 就是银行存放在国家那里的那部分钱,占他所吸纳的总存款的比例** + +理解了存款准备金率以后。我们也就很好理解降低存款准备金率是什么意思。就拿这次来说,对于大型金融机构来说,也就是,把存款准备金率由原来的16%,降到15.5%,也就是降了0.5个百分点。这也就意味着,降准以前,银行每吸纳一百块钱存款,他要在国家那里存16块钱。降准以后,它只需要在国家那里存15块5。这意味着,他留在自己手上的钱多了五毛。这就是存款准备金率的来源和功效,也是银行在接收了你我这些人的存款之后,背后操作的内幕。 + +事情说到这儿,似乎基本说清楚了。但事实不是这样。恰恰相反,它才刚刚开始。因为,如果存款准备金仅仅是为了保证你我这些储户的资金安全的话,我们其实完全可以不去管它,因为我们在心理上,还从来没有担心过,我们存在银行里的钱的安全问题。相反我们口口声声地说,钱存在银行里是最安全的。所以如果存款准备金只是这一个原始的含义,那么国家降不降准,对我们应该没有太大心理上的影响,我们也不会去关心它,更不会去热火朝天地议论它。 diff --git a/Chapter8 - Finance/8.11.md b/Chapter8 - Finance/8.11.md new file mode 100644 index 0000000..f33d597 --- /dev/null +++ b/Chapter8 - Finance/8.11.md @@ -0,0 +1,30 @@ +# 降准的魔法效应 + + + +​​6月24日,央行宣布降准0.5个百分点。存款准备金率由16%降为15.5% 意味着什么? 对于大型金融机构来说,降准之前,他每吸纳100元存款就要存16块钱到国家那里。降准之后,它只需要存放15块5到国家那里。 + +这样说来,降低或者提高存款准备金率,主要是为了保证我们这些储户存在银行里的钱的安全问题。但之前的文章的最后说了,降低或者提高存款准备金率。它的意义远远不止这些。 + +它的意义在哪儿呢?它的意义在于,可以增加或者减少市场上货币的供应量。这里先做概述「**降准可以增加市场上货币供应量、升准可以减小市场上货币供应量**」 + +以大型金融机构为例。原来吸纳一百块钱存款,手上可以留84块钱。这84块钱,他可以再借给别人,这意味着,市场上存在着84块钱的货币供应量。而存款准备金率降低0.5个百分点之后,意味着它吸纳一百块钱存款之后,手上可以留84块5。这意味着市场上存在的货币供应量,由84增加到84.5。增加了五毛钱。 + +不要小看0.5。由于金融机构吸纳的存款总额非常大,每一百块钱的存款增加五毛钱的货币供应量。那一千,1万,一亿,十亿,百亿的存款,分别能增加多少货币供应量呢?毫无疑问,月到后面,这个数字越大。 + +假如,我们国家目前所有金融机构的存款余额大约是140万亿。这次存款准备金率降低0.5个百分点。用140万亿乘以0.5%,结果是7000亿。所以我们在很多文章上都可以看到7000亿这个数字。这一次降准,市场上可以增加7000亿的货币供应量。 + +事实上,这一次降准0.5个百分点,市场上可以增加的货币供应量,远远不是7000亿,而是大约5万亿。也就是7000亿的7倍左右,银行怎么做到的?那不得不谈谈银行的本质和工作原理(存款、借贷) +还是从我们把一百块钱存进银行说起。 + +以大型金融机构当前15.5%的存款准备金率为例。当我们把一百块钱存到银行。那么,他需要拿出15块5,存放在国家那里,剩下的84块5,他可以对外贷款。当他把这84块5毛钱都贷款给别人以后,会是什么样子呢? + +自然,这84块5毛钱,就变成了某个企业的钱,或者某个个人的钱。不管这些企业或者个人,将要把这些钱用作什么用途,有一点是可以肯定的。那就是,在绝大多数情况下,他不是把这些钱取成现金,放在自己口袋里,或者压在自己枕头下,或者放在自己的床底下,或者保险柜里。 + +他会怎么办呢?答案很简单,他还是会把钱继续存在银行里。问题就出在这儿。 + +因为这样做,会产生非常神奇的效果,会让银行像变戏法一样变出钱来。我们用生活常识就可以得出判断。那就是,这84块5毛钱又变成了银行所吸纳的存款。 + +银行新吸纳了84块5毛钱的存款之后,会怎么办呢?规则不变,那就是要把这84块5毛钱的15.5%,存到国家那里,剩下的部分,银行还可以用于对外贷款。剩下的钱也一样,拿到的人将钱继续存入银行,银行在规则不变的情况下继续将拿到钱的15.5%拿给国家,剩下的84.5%用来继续投资. + +其实上面的问题不应该是经济学关心的问题,而是数学领域关心的问题.所幸这问题非常简单,计算的结果是n倍,这个n的具体数值,就是**存款准备金率的倒数**. 比如当前的存款准备金率是15.5%,那么这个n就是 1/15.5% = 6.5,6.5有一个专门的称呼,叫做:**货币倍数**。当我们知道了货币倍数的原理就可以根据降准和升准对市场上资金的影响的具体值. 比如某大型金融机构直接释放的存款是5000亿,货币倍数是6.5,那么市场上多出的钱就是 5000*6.5亿​​​​ \ No newline at end of file diff --git a/Chapter8 - Finance/8.12.md b/Chapter8 - Finance/8.12.md new file mode 100644 index 0000000..1a8affa --- /dev/null +++ b/Chapter8 - Finance/8.12.md @@ -0,0 +1,34 @@ +# 揭开″定向降准″的神秘面纱 + +> 可能你听过“-降准”,可能懂它的意.那你知道“定向降准”是什么意思吗?这篇文章带你揭秘 + + + +之前的文章说到,这一次的降准释放了7000亿的资金,根据货币倍数的原理,大约可以向市场上提供将近5万亿的货币供应。那么,这些钱会流向哪里呢? + +那就不得不谈谈“定向”.所谓**定向,就是指这一次降准释放出来的资金只能用于指定的用途,而不能用于其他。而这个指定的用途,自然是不包括股市和楼市的**。 + +这一次的降准是定到了哪个方向呢?两个方向。 + +一个方向是**债转股**。债转股是什么意思呢?**就是说有很多企业向银行借了钱,到期是要还的。实行债转股之后,银行就可以不让他还钱,直接把这个债权转成股权**。 + +这样说貌似还是不太懂,那么就举一个具体的例子。比如说某企业有净资产9个亿。他同时在银行贷款了1个亿。还款期限是今年10月底。正常情况下,他在10月底以前必须要筹到钱,把这1个亿的资金还上。有了债转股这个政策之后。银行可以让他下属的资产管理公司跟他说:“算了,你不用还银行的钱了,那1个亿,算我投资,我来当你的股东”。于是两家算账,这个企业原来的净资产是九个亿,加上银行的这一个亿,就变成总资产十个亿。其中资产管理公司的投资是一个亿,占股比例是10%。 + +那资产管理公司的这一个亿的钱从哪儿来呢?自然是银行贷款给他。那银行贷款给资产管理公司的钱又是从哪儿来呢?自然是来源于这一次降准当中释放出来的钱。 + +这么一种操作,你说钱会不会流向股市和楼市? + +上面说的是这种操作的背后本质,在具体的操作程序上通常是这样的,这里也顺便说一说: +第一步,银行基于这样一个宏大的规划,向资产管理公司贷款一个亿。 +第二步,银行将自己所拥有的债权转移给资产管理公司,而资产管理公司支付给银行一个亿的资金。这样,银行贷款给资产管理公司的一个亿的资金,又回到了银行,于是银行的风险解除。与此同时,企业欠银行的钱就变成了欠资产管理公司的钱。从逻辑上讲,到这一步,企业依然要还钱的,只不过还钱的对象由银行变成了资产管理公司。 +第三步,资产管理公司和企业签订合同,放弃这一个亿的债权,而同时拥有一个亿的股权。到了这一步,对于企业来说,它就不需要还钱了。但同时它又多了一个股东,这个股东就是资产管理公司。对于资产管理公司来说,他没有权利要求企业还它的欠款了,但他成了企业的股东,所以他可以从企业的收益当中分红。 + +走到这一步,债转股的过程就全部走完了,当然还会有后面的故事。后面的故事大致情节是这样的:在这种情况下成为股东,他并不是真心要当这个企业的股东,只是想帮助企业渡过难关,所以当企业经营状况好转的时候,这样特殊的股东就会通过适当的方式,把自己的股权再转移出去。转移出去之后,他失去了股权,但会得到一大笔资金,他可以用这笔资金偿还银行的贷款。 + +第二个方向,那就是:**小微企业贷款**。 +这个方向的分析要简单得多,没那么复杂。**一言以蔽之,就是这释放出来的资金,只能借贷给那些小微企业**。而且对这些小微企业要进行甄别的,在甄别的过程中,搞房地产开发的小微企业自然是会被筛选掉的。 + +所以说这一部分钱也不会流向股市和楼市。 + +总之,从定向的两个方向分析可以知道,这一次降准所释放出来的资金不会流向股市和楼市。 +虽然不会直接流向股市和楼市,但是还是会有间接的因素,让股市和楼市的资金供应变得更加充裕一些。怎么回事呢,以后文章再说 ​​​​ \ No newline at end of file diff --git a/Chapter8 - Finance/8.13.md b/Chapter8 - Finance/8.13.md new file mode 100644 index 0000000..0436d2a --- /dev/null +++ b/Chapter8 - Finance/8.13.md @@ -0,0 +1,10 @@ +# 股市飘来了降准的芳香 + +之前的文章讲了**定向降准,大型金融机构多出来的钱会用于指定的方向(债转股、小微企业贷款)**. 所以理论上市面上多出来的这些钱不会直接进入房地产和股市. 那这么讲吧,在国家没有降准的时候这些企业向银行借贷的钱,到了还款之日,企业就不得不向民间、或者其他来源去筹集资金向银行还款,有了债转股,企业就不需要筹集资金,那么市场上多余的这部分资金就可以用于其他用途,或多或少都会进入股市、楼市. + +所以说债转股虽然不会直接将资金流入股市、楼市,但是多多少少或间接增加股市、楼市的资金供应量. + +小微企业贷款也是上面的思路,本来要向民间或者市场借款去维持小微企业运转,现在国家降准后,大型金融机构、小型金融机构多出的这部分资金就可以按照定向降准用于指定的用途了,比如这里所说的方向2,小微企业贷款,那么国家在未发布降准的时候,小微企业要贷款去运营,发布降准后金融机构按照政策贷款给小微企业,那么市场上多余的这部分资金一样会用于投资,或多或少会进入股市和楼市. +或者说某些小微企业本来不缺钱,但是看到有这样的政策,他肯定会去贷款,这个钱到手后可以用来托大经营规模或扩展市场改善生产经营,也有可能用于其他用途,如果用于其他用途,难免不会进入股市、楼市 + +**定向降准,虽然并不是针对股市和楼市,但是对股市和楼市的资金供应还是有正面的积极作用。不管它的作用大小,总之是有的**。 \ No newline at end of file diff --git a/Chapter8 - Finance/8.2.md b/Chapter8 - Finance/8.2.md new file mode 100644 index 0000000..1edfc43 --- /dev/null +++ b/Chapter8 - Finance/8.2.md @@ -0,0 +1,17 @@ +# 亚洲金融危机两三谈 + +> 借钱是近现代金融危机、金融现象的一个重要看点. + + + +1997年泰铢下跌的危机.泰国政府采取了三项反击策略,其中之一就是提高**利率**. 而在随后香港政府的金融危机的反击政策也是提高利率.可以说**当外汇汇率下跌需要进行政府干预时,央行提高利率是最常见的做法**. + +为什么提高利率可以遏制金融危机? 因为索罗斯发起的金融危机本质上是大量借钱,买到大量目标国家的货币(泰铢),然后疯狂卖出(做空).造成市场上存在大量的目标国家的货币,大家都知道价格是由供求关系决定的,市场上该国货币供大于求,造成外汇汇率下降. + +如果你会好奇疯狂卖出泰铢怎么赚钱? 这个回答就和股票一样, 当你散播某些消息或者其他某些原因, 金融玩家疯狂卖出手中的泰铢或者股票, 普通民众跟风卖出, 造成市面上充斥着大量的泰铢或者股票, 那么单位泰铢或者股票的价格会下降, 然后金融玩家偷偷让其他合作者或者小账号买入大量泰铢或者股票, 等市场回暖便卖出获利. + +为什么政府提高利率会克制汇率下跌? 当有钱人知道利率上升时, 他就会衡量把钱存入银行赚钱还是参与金融活动赚钱,要是金融活动的获利风险大,且获得的回报不如银行利率提升后的回报,那他肯定会选择存入银行.当手里没钱的人想从银行借钱去参与金融活动时,他就会考虑一样的问题,金融活动获得的回报是否大于从银行获得的回报.这样可以有效反抗一波金融危机. + +2018年阿根廷比索断崖式下跌,阿根廷政府将指标利率(七天期LELIQ)大幅上调1500个基点,由45%升至60%.1.把利率提高到如此高的程度,说明阿根廷政府知道情况已经很糟糕了. + +可以看出来,外汇市场水很深.​​​​ diff --git a/Chapter8 - Finance/8.3.md b/Chapter8 - Finance/8.3.md new file mode 100644 index 0000000..ddff22d --- /dev/null +++ b/Chapter8 - Finance/8.3.md @@ -0,0 +1,12 @@ +# 提高利率的副作用 + +> 利率提高也有很多副作用.俗话说“杀敌一千,止损八百”. + +​​1997~1998年的亚洲危机这场金融危机中,国际金融玩家索罗斯连续攻占了泰铢、韩元、印度尼西亚盾、马来西亚林吉特4个国家货币后,1998年1月和5月开始对港元进行攻击,当时的香港金管局局长任志刚,获得了一个稍带贬义的称号“任一招”,因为面对接二连三的金融攻击,采取了同一招“提高利率”. +为什么说这个外号略带贬义呢?因为这一招的实际效果不好,而且产生了很多副负面作用。 + +1. 提高利率的负面作用之一是表现在股市上。提高利率的后果就是提高资金的使用成本,这当然可以遏制市场上的金融大玩家和金融小玩家来卖空货币,其背后的作用机理在上一章已经认真地说过。但是这同时也提高了在股市上操作的资金成本,所以有很多人就会从股市撤出资金。一方,面在股市玩股票的很多人是借钱在玩,由于利率提高了,成本增加了,他们就不再借钱玩儿了。另一方面,有更多的人更愿意把钱存到银行,赚取利息,而不愿意把钱放在股市去炒作股票。前面我们说过,任何一种商品,包括萝卜白菜之类的实物商品,也包括股票或货币这样子的特殊商品,只要市场上的需求下降了,也就是买的人少了,价格就会下跌。总而言之,大体的结论是,提高利率对股市的影响是股市下跌。股市下跌自然不是什么好事儿。当然,为什么股市下跌不是什么好事,这又是另外一个问题,我们以后有机会了再说。总之不是好事儿,所以算是提高利率的一个负面作用. + +2. 提高利率的负面作用之二是表现在实体经济上。这个负面作用应该比第一个负面作用更加要命。实体经济是整个社会经济的基础中的基础。虚拟经济说到底,只是为实体经济服务的。炒作外汇股票之类的虚拟经济,说到底就是玩钱,玩来玩去,并不创造社会财富。而办工厂、做贸易,却是实实在在地创造社会财富,所以叫实体经济。全社会的人要想日子过得好点儿,归根结底是要实体经济发达。可是提高利率,既提高了虚拟经济当中的资金成本,同时也提高了实体经济当中的资金成本。打个比方,如果银行利率是5%,一个企业问银行借了100万块钱,一年要支付5万块钱的利息,如果他用这100万块钱赚到了7万块钱,那么一年之后,他还清了100万块钱的本金,又还清了5万块钱的利息,最后他还可以赚2万块钱。如果银行利息提高到8%,那么他一年就要支付利息8万块钱,而他一年只赚了7万块钱,于是他还亏本1万块钱。或者情况比这好一点,他拿着100万块钱,一年可以赚10万块钱,那么在利率是5%的情况下,他还清了本金和利息之后,他另外还赚了5万块钱。但是当利率提高到8%的时候,他还清了100万的本金和8万块钱的利息之后,他只赚了2万块钱。他原来可以赚5万,现在却只能赚2万,虽然还能赚钱,但是生产的积极性却大大的降低了。总之,不管是亏本,还是因为虽然在赚钱,但赚钱数量减少,总之都是对实体经济的伤害。 + +中央政府和香港政府联合作用下,打赢了这场金融危机.​​​​ \ No newline at end of file diff --git a/Chapter8 - Finance/8.4.md b/Chapter8 - Finance/8.4.md new file mode 100644 index 0000000..4b0733e --- /dev/null +++ b/Chapter8 - Finance/8.4.md @@ -0,0 +1,13 @@ +# 白话贸易战 + + 之前和阿里巴巴商学院学弟聊天,讲到了贸易战。他说应该同意美国提议,让中国更加开放。让美国低价优质商品进入国内,让大家提高生活质量 + +抛开正确与否。美国不是慈善家。早前我们国家答应了美国的大豆出口。刚开始美国大豆凭借低价格、高品质快速取得了国内市场,大家纷纷购买进口大豆。这样国内种植大豆的人们都不种植大豆了,因为没市场。等美国发现中国大豆领域没啥竞品的时候就开始疯狂提高价格,人们很难过。 + +这时候国企出手了,中粮集团将存储的大豆低价投放到市场上。然后政策和资金鼓励农民种植大豆。 + +美国搞芯片的模式是通过资本手段(发行股票和债券)以及风险投资为创业公司注入资金,创业公司获得资金支持后,进行持续的技术创新获得市场,提升公司估值,然后上市,风险资本卖出股票获利退出。这种模式以市场为导向,效率高,但体量小,公司之间整合资源难。造就了一批芯片企业。 + +日本在70年代开始发展芯片企业。模式是:集中力量办大事。这种模式很可怕,举国之力办芯片,很快发展起来后以低价高性能占领本国市场,之后走向全球,占领了 Intel 的市场,让美国芯片企业裁员。这些企业见识到日本芯片的可怕,以芯片是军工的核心为由向政府汇报。美国很看重军工的,之后就提高税率,让日本政府保证美国芯片在日本国内份额不能低于一定额度,这样日本芯片基本就没能好好发展。但是不至于消亡。 + +因为日本和美国搞事情的时候,韩国 DRAM 钻了空子,趁机发展。讨好美国,美国都在大规模使用韩国的芯片,遂日本芯片行业破灭​​​​ \ No newline at end of file diff --git a/Chapter8 - Finance/8.5.md b/Chapter8 - Finance/8.5.md new file mode 100644 index 0000000..59738f2 --- /dev/null +++ b/Chapter8 - Finance/8.5.md @@ -0,0 +1,14 @@ +# 股票的本质是帮助企业高飞 + +> 半小时洗澡过程中的思考 + + + +很多人在玩股票,但是对于股票、股市的认知却不够。本人不玩理财,却在最近自学金融和经济学相关的知识。谈一谈个人理解吧。 + +股票最早诞生于资本主义国家。17世纪初,随着资本主义大工业的发展,企业生产经营规模不断扩大,相应造成资本短缺。「资本」不足便成为制约企业经营和发展的重要因素之一。为了募集更多资本,于是,出现了以「股份公司」形态,由股东出资共同经营,进而将筹集资本的范围扩大到社会,产生了以股票表示投资者投资入股(按出资额大小享受权益和分红)。世界上最早的股份有限公司是在荷兰成立的东印度公司。 +16世纪,商人在荷兰阿姆斯特丹进行荷兰东印度公司的股票买卖交易,形成世界上第一个股票市场。 + +讲道理,上市公司是为了资本匹配的,通过发行股票,将社会资本给了最需要的企业 。有些上市公司增发股票,可能是出发点好,继续扩大经营规模,可是还是存在一些“差强人意”的企业,通过增发股票,(可能自己本身就有很多现金流)吸收更多的社会资本去理财(炒楼、买股票、借贷给第三方企业等),这算是对社会资源的极大浪费。 + +股票的价值就是:**最强的创始人带着最牛的团队去做最有价值的事情,创造更大的价值**。 diff --git a/Chapter8 - Finance/8.6.md b/Chapter8 - Finance/8.6.md new file mode 100644 index 0000000..5babc0b --- /dev/null +++ b/Chapter8 - Finance/8.6.md @@ -0,0 +1,8 @@ +# 需求是分层次的 + + +需求是分层次的,比如马斯洛理论。但这个层次的概念存在于大多数的场合,在职场中也是一样。比如一个高级工程师的招聘是为了部门需求,那么你的薪资可能是20k;部门存在的意义是为了公司需求(财务部为了处理金钱有关的一切、法务部、人力资源部...),那么作为部门的领导,你的薪资可能是35k;公司存在的意义是为了服务客户(市场)需求(满足用户什么样的问题、这个产品如何帮助人们提高效率等),那么作为公司的Boss,那么薪资可能是100k。 + +所以对于即将步入社会或者已经在社会中摸索的同学们应该懂得什么是自己上升的最根本的原因。当然自己的硬实力也是非常重要的一部分。 + +但切莫眼光局限,被动改变。比如部门需要员工一个什么证书,你就去报名学习,需要一个什么学历你就马上报班考研。​​​​ diff --git a/Chapter8 - Finance/8.7.md b/Chapter8 - Finance/8.7.md new file mode 100644 index 0000000..68bde58 --- /dev/null +++ b/Chapter8 - Finance/8.7.md @@ -0,0 +1,7 @@ +# 炒期货比炒股风险高的本质原因 + +​​一般人都知道炒期货比炒股风险高。知其然但是不知其所以然。原因有几个点: + +1. 期货必须使用现金交易; +2. 要想高收益必须使用杠杆的力量 +3. 需要长期持有再交易。这几个原因就决定炒期货的风险要高​​​​ diff --git a/Chapter8 - Finance/8.8.md b/Chapter8 - Finance/8.8.md new file mode 100644 index 0000000..e7889dc --- /dev/null +++ b/Chapter8 - Finance/8.8.md @@ -0,0 +1,14 @@ +# 浅谈一带一路 + + +先聊一个现象。有些东西明明价格很低,卖不出去。然而这些生产者在卖不出去的情况下即使扔掉、倒掉、销毁掉,却不给那些穷人或者需要的人。以前不明白为什么。 + +举个例子,亚马逊和很多品牌企业有合作,由于种种原因这些东西卖不出去。那么出于要更换商品等其他原因,这些商家宁愿当作垃圾销毁也不给那些有需要的人。为什么? + +在卖不出去,价格再低的情况,你如果再白白送给那些人,那市场的需求就更低了。更加没人愿意买。 + +“产能过剩”和“一带一路”有关系吗?哎,还真有。目前中国面临的一个问题就是产能过剩,生产的钢铁、家电等等都供大于求,价格很低了。为了更好的发展,国家用差不多4000亿美元用来搞“一带一路”,很多国家也参与进来和中国签订合作。这样有什么好处?中国提供资金给这些国家,然后这些国家的新项目都用人民币来交易了。那么可以更好的让人民币走向世界。如果越来越多的国家用人民币进行交易,那么人民币的地位上去,好处非常多。 + +一般来说,国内房价的上涨和不断印币有关系。以前这个价格可以买一套房,现在不断印刷人民币,那么需要买这样一套房的总价就要上去。普通老百姓一般都是储蓄,很少跑赢通货膨胀。所以就又不断贷款,依次循环。 + +保「房价」、「货币」?为什么保护货币?因为一个国家的货币代表着这个国家的信誉。代表着国际地位等。俄罗斯、日本等都面临过和中国一样的问题。俄罗斯选择保护房价。日本选择保护货币。都有劣势。我们国家选择保护货币和控制房价。通过政策抑制房价上涨、抑制人民币的借贷。 \ No newline at end of file diff --git a/Chapter8 - Finance/8.9.md b/Chapter8 - Finance/8.9.md new file mode 100644 index 0000000..392f794 --- /dev/null +++ b/Chapter8 - Finance/8.9.md @@ -0,0 +1,34 @@ + +# 隐形的投资你看得到吗? + +> 生活中每天都发生着各种投资,有些很传统一眼可辨,有些比较神秘,你可能不认识它,文章为你揭秘 + +1. 彩票:彩票不属于投资,它更多类似于赌博。低额的门槛去尝试取得n倍的回报。之前看到过一些数据,世界各国通过彩票中奖的人挺多的,而且是高额的中奖。有人追踪过这些人的结局,基本都是破产或者千金散尽。因为彩票手段获取的财富一点都不会珍惜,基本都是挥霍并且摧毁这些人踏实工作或者认真生活的能力。 + +2. 贵金属:黄金、白银等贵金属,基本都属于期货。很多人没有大量的现金交易,所以基本采取保证金交易。这样子就会造成无上限的投资。因为是期货所以利用杠杆的手段,加上需要时间的因素(类似于囤积居奇),等到几十年找到合适的时机换取巨额回报,但是一般人根本不会有这样的机会,且成功的比较少 + +3. 古董:如果是古董世家或者本身是专家可以从事。一般人不太适合。因为一般人根本无法判断这东西的真假。讲个故事:之前有人参考一个经济村,全村流水线、工厂模式生产假古币(各种朝代的铜钱、刀型币等等)。最后按斤交易。(突然发现水很深啊😂) + +4. 艺术品:优秀的艺术品是非常棒的。可是有些人把它当作资本炒作的时候就很恶心了。艺术品的恶劣交易和炒资本一样。一批投资者找到一个合适的画家合作者(一般都是年纪40左右, 画画水平可以,但是名气不大)。谈好合作模式。几位操盘者帮这个画家报名了几乎所有的画展,一般人的画假如300万,这个画家的画被这些投资者抬高,然后投资者自己出手买到。一般是高价,比如500万。然后参加了几乎所有的画展交易后这个画家的名气就起来了。等到下一次画展交易,有“小白”就会接盘。以高额的价格买了他所谓的名作。之前的几个投资者早就和画家签订了合约,每幅画的50%都要参与枫红 + +5. 域名:早起微博的域名是 weibo.sina.com ,后来发现不太好记。有个小孩叫张伟波,爱好计算机,他父亲就买了一个域名送给小孩。新浪的人知道这事后找到小孩家里人,说给你2小时思考一个价格,我们不还价。最后880万成交。我觉得要是他不卖给新浪,后来腾讯和网易也做过博客,要是找上去买的话,以腾讯的手笔,估计1000万以上吧。同样的故事很多,阿里巴巴早起创建的启动资金是50万。马云当时从老外手中用1万美元买来,差不多是启动资金的20%。为什么买这个域名?因为全世界小孩学过阿里巴巴与四十大盗的故事,一听到阿里巴巴就是商人。从今天来看阿里巴巴这个域名的价值应该非常高吧 + +6. 邮票、书籍:这些可以当作兴趣爱好,但是想作为投资的话,成效甚微。 + +7. 信托:信托门槛非常高。主要投资一些大项目,比如房地产。起步几百万,高风险高收益。 + +8. 比特币:比特币之前概念很火,但是真的很有前景吗?如果它壮大会冲击各个国家政府的铸币权,这一点是非常可怕的。所以它会作为一些场景下的监督模式,或者现有金融体系的补充模式。 + +9. 储蓄:储蓄不算是投资。20年前的2万块钱,可以买一所房子。假如你存银行,现在连本带利拿出来,连个厕所都不够卖😂。所以储蓄绝壁不算投资,只可以说是现金流的一个形式。储蓄基本上贬值的,基本跑不赢每年的通货膨胀。同样,理财也算是减小或者跑赢通货膨胀(大爷大妈在银行购买的理财产品一般在5%左右,每年的通货膨胀差不多7%)。 + + +## 画一道分割线。 + + +记者采访巴菲特,你认为最成功的投资是什么?巴菲特说是自己对自己的投资。 +这种投资属于「广义投资」。巴菲特举了两个例子。年轻的巴菲特不擅长演讲,他报班学习演讲课程,这让他成就感很足,可以在公众场合进行演讲。另一个例子是他为了研究自己投资或者计划投资的公司的情况,经常找那个公司的职员或者经历,要么自己认识,要么朋友认识介绍给他。他们聊完业务和合作之后觉得对方职业水平很高、思想很棒,就交了朋友,这对巴菲特是非常棒的,优秀的人脉的价值是无限且无形的。 + +广义的投资是对自己的投资、对自己的人脉、身体健康素质、专业技能、能力、行业视野等 +狭义的投资显而易见就是大家经常玩的投资。 + +随着年龄的增长,你不断成长,广义投资和狭义投资相辅相成,不断互促,良性循环。​​​​ \ No newline at end of file diff --git a/Chapter8 - Finance/Chapter8.md b/Chapter8 - Finance/Chapter8.md new file mode 100644 index 0000000..9ea3c25 --- /dev/null +++ b/Chapter8 - Finance/Chapter8.md @@ -0,0 +1,18 @@ +# 第八部分 + +第八部分主要记录在学习金融学中遇到的概念、生活中的经济学现象的解读 + + * [1、“账期”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [2、亚洲金融危机两三谈](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [3、提高利率的副作用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [4、白话贸易战](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [5、股票的本质是帮助企业高飞](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [6、需求是分层次的](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [7、炒期货比炒股风险高的本质原因](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [8、浅谈一带一路](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [9、隐形的投资你看得到吗](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [10、存款准备金率的衍生知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [11、降准的魔法效应](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [12、揭开″定向降准″的神秘面纱](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [13、股市飘来了降准的芳香](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + \ No newline at end of file diff --git a/README.md b/README.md index 7008b6b..0e57cfa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,24 @@ +## **knowledge-kit** +### 目的 -iOS、Web前端、后端、Swift、计算机网络踩坑以及学习记录 +形式记录大前端路上的探索,主要研究基础平台开发、工程效率、质量监控保证、SDK 输出、多端融合能力、动态化、组件化、工程化能力。 + +技术点: iOS、Web 前端、后端、Hybrid、Node 的应用、爬虫、反爬虫、后端、数据库、算法等领域。 + +偶尔记录自学经济学遇到的概念或者有趣的生活现象解读. + + +## 目录 + +如果要方便的查看文章目录,请点击[链接](https://github.com/FantasticLBP/knowledge-kit/blob/master/SUMMARY.md) + + +## 反馈 + +定期更新博文。如果在查看文章的时候发现了问题可以提出 issue。(95年小双鱼, 技术专家进阶中,有技术问题或者想聊聊程序员的事情可以通过微博联系)[微博](http://weibo.com/u/3194053975) + +## 交流 + +如果你也是大前端路上的一名修行者,可以加我 Wechat:704568245,拉你入群一起在「大前端自习室」学习交流。 \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md index cdb32e4..4bd1934 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -2,96 +2,189 @@ * [Introduction](README.md) -* [第一部分 iOS](第一部分 iOS/chapter1.md) - * [1、工程大小优化之图片资源](第一部分 iOS/1.1.md ) - * [2、看透构造方法](第一部分 iOS/1.2.md) - * [3、控制器加载的玄机](第一部分 iOS/1.3.md) - * [4、如何优雅地调试手机网页?](第一部分 iOS/1.4.md) - * [5、事件响应者链](第一部分 iOS/1.5.md) - * [6、外卖App双列表联动](第一部分 iOS/1.6.md) - * [7、在内存剖析对象](第一部分 iOS/1.7.md) - * [8、长按UIWebView上的图片保存到相册](第一部分 iOS/1.8.md) - * [9、hitTest和pointInside方法你真的熟吗?](第一部分 iOS/1.9.md) - * [10、HyBrid探索(一)](第一部分 iOS/1.10.md) - * [11、iOS中的事件](第一部分 iOS/1.11.md) - * [12、NSFileManager终极杀手](第一部分 iOS/1.12.md) - * [13、UINavigationController的妙用](第一部分 iOS/1.13.md) - * [14、URL-Schemes深度剖析(上)](第一部分 iOS/1.14.md) - * [15、URL Schemes 的发展](第一部分 iOS/1.15.md) - * [16、CocoaPods的使用](第一部分 iOS/1.16.md) - * [17、OC与Swift混编](第一部分 iOS/1.17.md) - * [18、对于不可调节高度的UI控件进行改变frame](第一部分 iOS/1.18.md) - * [19、YYModel 的使用](第一部分 iOS/1.19.md) - * [20、模块化](第一部分 iOS/1.20.md) - * [21、实现波浪动画](第一部分 iOS/1.21.md) - * [22、底层原理探究](第一部分 iOS/1.22.md) - * [23、禅与 Objective-C 编程艺术](第一部分 iOS/1.23.md) - * [24、修改 UITextField placeholder 样式](第一部分 iOS/1.24.md) - * [25、UIScrollView拖拽时回收键盘](第一部分 iOS/1.25.md) - * [26、读 Apple 源码看看 NSRange](第一部分 iOS/1.26.md) - * [27、复制层(CAReplicatorLayer)](第一部分 iOS/2.17.md) - * [28、CAShapeLayer](第一部分 iOS/1.28.md) - * [29、仿微博动画](第一部分 iOS/1.29.md) - * [30、UILabel 全局匹配并高亮](第一部分 iOS/1.30.md) - * [31、JavascriptCore](第一部分 iOS/1.31.md) - * [32、Xcode 小技巧](第一部分 iOS/1.32.md) - * [33、终端效率](第一部分 iOS/1.33.md) - * [34、终极截屏](第一部分 iOS/1.34.md) - * [35、推送](第一部分 iOS/1.35.md) - * [36、App 评分](第一部分 iOS/1.36.md) - * [37、一些布局小知识](第一部分 iOS/1.37.md) - * [38、iOS数值计算精度丢失问题](第一部分 iOS/1.38.md) - * [39、一些看到但未尝试的知识](第一部分 iOS/1.39.md) - * [40、RunLoop上](第一部分 iOS/1.40.md) - * [41、RunLoop下](第一部分 iOS/1.41.md) - * [42、RunLoop的应用](第一部分 iOS/1.42.md) - * [43、iOS 应用启动性能优化资料汇总](第一部分 iOS/1.43.md) - * [44、App security](第一部分 iOS/1.44.md) - * [45、调试技巧](第一部分 iOS/1.45.md) - * [46、Awesome Hybrid - 1](第一部分 iOS/1.46.md) - * [46、Awesome Hybrid - 1](第一部分 iOS/1.46.md) - * [47、NSTimer 的内存泄漏](第一部分 iOS/1.47.md) - * [48、KVC && KVO](第一部分 iOS/1.48.md) - * [48、金额格式化](第一部分 iOS/1.49.md) -* [第二部分 Web前端](第二部分 Web 前端/chapter2.md) - * [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](第二部分 Web 前端/2.1.md) - * [2、正则表达式](第二部分 Web 前端/2.2.md) - * [3、CSS-埋点统计](第二部分 Web 前端/2.3.md) - * [4、generator函数](第二部分 Web 前端/2.4.md) - * [5、H5性能优化方面的探索](第二部分 Web 前端/2.5.md) - * [6、h5自定义对象](第二部分 Web 前端/2.6.md) - * [7、Javascript-prototype](第二部分 Web 前端/2.7.md) - * [8、JSON的一些骚操作](第二部分 Web 前端/2.8.md) - * [9、vue-cli](第二部分 Web 前端/2.9.md) - * [10、Vue-devtools](第二部分 Web 前端/2.10.md) - * [11、H5页面保存页面为图片](第二部分 Web 前端/2.11.md) - * [12、Promise](第二部分 Web 前端/2.12.md) - * [13、webpack-dev-server 的配置和使用](第二部分 Web 前端/2.13.md) - * [14、Web 与 H5 交互的坑](第二部分 Web 前端/2.14.md) - * [15、前端持久化](第二部分 Web 前端/2.15.md) - * [16、VS-Code](第二部分 Web 前端/2.16md) - * [17、Vue tips](第二部分 Web 前端/2.17.md) - * [18、Web反爬虫技术](第二部分 Web 前端/2.18.md) - * [19、webpack小集](第二部分 Web 前端/2.19.md) - * [20、大前端](第二部分 Web 前端/2.20.md) - * [21、canvas](第二部分 Web 前端/2.21.md) - * [21、动画控制](第二部分 Web 前端/2.22.md) - * [23、打造自己的图标字体文件](第二部分 Web 前端/2.23.md) - * [24、Chrome 调试技巧](第二部分 Web 前端/2.24.md) -* [第三部分 后端技术](第三部分 后端技术/chapter3.md) - * [1、利用分页和模糊查询技术实现一个App接口](第三部分 后端技术/3.1.md) - * [2、网页端扫码登录实现原理](第三部分 后端技术/3.2.md) -* [第四部分 数据库](第四部分 数据库/chapter4.md) +* [Chapter1 - iOS](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/chapter1.md) + * [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、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) - * [1、Mysql JSON 类型的字段](第四部分 数据库/4.1.md) -* [第五部分 开发杂谈](第五部分 开发杂谈/chapter5.md) - * [1、开始hexo博客之旅](第四部分 开发杂谈/4.1.md) - * [2、命令行文件查找](第四部分 开发杂谈/4.2.md) - * [3、Charles 从入门到精通](第四部分 开发杂谈/4.3.md) - * [4、git常见使用](第四部分 开发杂谈/4.4.md) - * [5、Chrome 正确调试姿势](第四部分 开发杂谈/4.5.md) - * [6、Git 实用操作](第四部分 开发杂谈/4.6.md) - * [7、短链接](第四部分 开发杂谈/4.7.md) - * [8、移动端推送杂谈-part one](第四部分 开发杂谈/4.8.md) - * [9、iOS 隔了较久时间推送变得缓慢](第四部分 开发杂谈/4.9.md) - * [10、抓包工具的原理](第四部分 开发杂谈/4.10.md) +* [Chapter2 - Web FrontEnd](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/chapter2.md) + * [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md) + * [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md) + * [3、CSS-埋点统计](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.3.md) + * [4、generator函数](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.4.md) + * [5、H5性能优化方面的探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.5.md) + * [6、h5自定义对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.6.md) + * [7、Javascript-prototype](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.7.md) + * [8、JSON的一些骚操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.8.md) + * [9、Vue开发小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.9.md) + * [10、Vue-devtools](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.10.md) + * [11、H5页面保存页面为图片](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.11.md) + * [12、Promise](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.12.md) + * [13、webpack-dev-server 的配置和使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.13.md) + * [14、Web 与 H5 交互的坑](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.14.md) + * [15、前端持久化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.15.md) + * [16、VS-Code](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.16.md) + * [17、Vue tips](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.17.md) + * [18、Web反爬虫技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.18.md) + * [19、webpack小集](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.19.md) + * [20、大前端](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.20.md) + * [21、canvas](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.21.md) + * [21、动画控制](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.22.md) + * [23、打造自己的图标字体文件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.23.md) + * [24、Chrome 调试技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.24.md) + * [25、大前端动画](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.25.md) + * [26、Linux 下安装 Node](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.26.md) + * [27、浏览器不通窗口之间的通信之道](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.27.md) + * [28、神器Puppeteer](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.28.md) + * [29、从Vue.js谈谈前端开发的技术栈演变](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.29.md) + * [30、Javascript 常用工具封装](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.30.md) + * [31、浏览器布局与DOM绘制](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.31.md) + * [32、React核心技术剖析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.32.md) + * [33、ES6学习总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.33.md) + * [34、富文本编辑器的原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.34.md) + * [35、Vue3 核心技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.35.md) + * [36、immutable.js 与 React](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.36.md) + * [37、前端工程化与 CI、CD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.37.md) + * [38、npm 改进之工程化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.38.md) + * [39、前端模块化演进之路](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.39.md) + +* [Chapter3 - Server](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/chapter3.md) + * [1、利用分页和模糊查询技术实现一个App接口](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.1.md) + * [2、网页端扫码登录实现原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.2.md) + * [3、从Node.js看看服务端框架的感想](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.3.md) + * [4、免费的配置HTTPS证书](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.4.md) + * [5、PHP、Mysql 中文乱码](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.5.md) + * [6、YAML](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.6.md) + * [7、Node单元测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.7.md) + * [8、数据安全(反爬虫)之「防重放」策略](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md) + + + +* [Chapter4 - DataBase](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter4%20-%20DataBase/chapter4.md) + * [1、Mysql JSON 类型的字段](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter4%20-%20DataBase/4.1.md) + + +* [Chapter5 - Network](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/chapter5.md) + * [1、HTTP请求头Range](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md) + * [2、认识HTTP、TCP、UDP](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.2.md) + * [3、你知道字节序吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md) + * [4、自定义报头协议](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.4.md) + +* [Chapter6 - Design Pattern](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/chapter7.md) + * [1、声明式与命令式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.1.md) + +* [Chapter7 - Geek Talk](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/chapter7.md) + * [1、命令行文件查找](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.1.md) + * [2、Charles 从入门到精通](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.2.md) + * [3、git常见使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.3.md) + * [4、Chrome 正确调试姿势](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.4.md) + * [5、Git 实用操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.5.md) + * [6、短链接](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.6.md) + * [7、移动端推送杂谈-part one](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.7.md) + * [8、iOS 隔了较久时间推送变得缓慢](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.8.md) + * [9、抓包工具的原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.9.md) + * [10、Mac 成吨提升效率的技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.10.md) + * [11、OKR](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.12.md) + * [12、面试技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.13.md) + * [13、Markdown的妙用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.14.md) + * [14、1995年的资深工程师,和你谈谈如何进阶](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.15.md) + * [15、区块链技术和比特币](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.16.md) + * [16、如何写一份夺目的简历](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.17.md) + * [17、一套开发规范](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.18.md) + * [18、云服务器靠谱推荐](https://github.com/FantasticLBP/knowledge-kit/blob/master/ + Chapter7%20-%20Geek%20Talk/7.19.md) + * [19、规范化团队 git 提交信息](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.20.md) + + + +* [Chapter8 - Finance](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/chapter8.md) + * [1、“账期”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [2、亚洲金融危机两三谈](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [3、提高利率的副作用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [4、白话贸易战](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [5、股票的本质是帮助企业高飞](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [6、需求是分层次的](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [7、炒期货比炒股风险高的本质原因](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [8、浅谈一带一路](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [9、隐形的投资你看得到吗](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [10、存款准备金率的衍生知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [11、降准的魔法效应](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [12、揭开″定向降准″的神秘面纱](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) + * [13、股市飘来了降准的芳香](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter8%20-%20Finance/8.1.md) \ No newline at end of file diff --git a/assets/.DS_Store b/assets/.DS_Store index 5008ddf..107b5cb 100644 Binary files a/assets/.DS_Store and b/assets/.DS_Store differ diff --git a/第二部分 Web 前端/assets/1460000016256735.png b/assets/1460000016256735.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256735.png rename to assets/1460000016256735.png diff --git a/第二部分 Web 前端/assets/1460000016256736.png b/assets/1460000016256736.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256736.png rename to assets/1460000016256736.png diff --git a/第二部分 Web 前端/assets/1460000016256737.png b/assets/1460000016256737.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256737.png rename to assets/1460000016256737.png diff --git a/第二部分 Web 前端/assets/1460000016256738.png b/assets/1460000016256738.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256738.png rename to assets/1460000016256738.png diff --git a/第二部分 Web 前端/assets/1460000016256739.png b/assets/1460000016256739.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256739.png rename to assets/1460000016256739.png diff --git a/第二部分 Web 前端/assets/1460000016256740.png b/assets/1460000016256740.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256740.png rename to assets/1460000016256740.png diff --git a/第二部分 Web 前端/assets/1460000016256741.png b/assets/1460000016256741.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256741.png rename to assets/1460000016256741.png diff --git a/第二部分 Web 前端/assets/1460000016256742.png b/assets/1460000016256742.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256742.png rename to assets/1460000016256742.png diff --git a/第二部分 Web 前端/assets/1460000016256743.png b/assets/1460000016256743.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256743.png rename to assets/1460000016256743.png diff --git a/第二部分 Web 前端/assets/1460000016256744.jpeg b/assets/1460000016256744.jpeg similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256744.jpeg rename to assets/1460000016256744.jpeg diff --git a/第二部分 Web 前端/assets/1460000016256745.png b/assets/1460000016256745.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256745.png rename to assets/1460000016256745.png diff --git a/第二部分 Web 前端/assets/1460000016256746.png b/assets/1460000016256746.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256746.png rename to assets/1460000016256746.png diff --git a/第二部分 Web 前端/assets/1460000016256747.png b/assets/1460000016256747.png similarity index 100% rename from 第二部分 Web 前端/assets/1460000016256747.png rename to assets/1460000016256747.png diff --git a/assets/屏幕快照 2017-05-15 下午5.35.17.png b/assets/2017-05-15 下午5.35.17.png similarity index 100% rename from assets/屏幕快照 2017-05-15 下午5.35.17.png rename to assets/2017-05-15 下午5.35.17.png diff --git a/assets/屏幕快照 2017-05-15 下午5.35.34.png b/assets/2017-05-15 下午5.35.34.png similarity index 100% rename from assets/屏幕快照 2017-05-15 下午5.35.34.png rename to assets/2017-05-15 下午5.35.34.png diff --git a/assets/屏幕快照 2017-05-23-5-56-53.png b/assets/2017-05-23-5-56-53.png similarity index 100% rename from assets/屏幕快照 2017-05-23-5-56-53.png rename to assets/2017-05-23-5-56-53.png diff --git a/assets/屏幕快照 2017-05-23-5-57-08.png b/assets/2017-05-23-5-57-08.png similarity index 100% rename from assets/屏幕快照 2017-05-23-5-57-08.png rename to assets/2017-05-23-5-57-08.png diff --git a/assets/屏幕快照 2017-05-23-6-01-29.png b/assets/2017-05-23-6-01-29.png similarity index 100% rename from assets/屏幕快照 2017-05-23-6-01-29.png rename to assets/2017-05-23-6-01-29.png diff --git a/assets/屏幕快照 2017-05-28 下午2.43.33.png b/assets/2017-05-28 下午2.43.33.png similarity index 100% rename from assets/屏幕快照 2017-05-28 下午2.43.33.png rename to assets/2017-05-28 下午2.43.33.png diff --git a/assets/屏幕快照 2017-05-28 下午2.51.36.png b/assets/2017-05-28-iconfintWorkSetting.png similarity index 100% rename from assets/屏幕快照 2017-05-28 下午2.51.36.png rename to assets/2017-05-28-iconfintWorkSetting.png diff --git a/assets/屏幕快照 2017-05-28 下午2.43.48.png b/assets/2017-05-28-iconfont.png similarity index 100% rename from assets/屏幕快照 2017-05-28 下午2.43.48.png rename to assets/2017-05-28-iconfont.png diff --git a/assets/屏幕快照 2017-05-28 下午2.44.22.png b/assets/2017-05-28-iconfontPickUp.png similarity index 100% rename from assets/屏幕快照 2017-05-28 下午2.44.22.png rename to assets/2017-05-28-iconfontPickUp.png diff --git a/assets/屏幕快照 2017-05-28 下午2.56.00.png b/assets/2017-05-28-iconfontWorkDirectory.png similarity index 100% rename from assets/屏幕快照 2017-05-28 下午2.56.00.png rename to assets/2017-05-28-iconfontWorkDirectory.png diff --git a/assets/屏幕快照 2017-06-07 下午3.21.18.png b/assets/2017-06-07 下午3.21.18.png similarity index 100% rename from assets/屏幕快照 2017-06-07 下午3.21.18.png rename to assets/2017-06-07 下午3.21.18.png diff --git a/assets/屏幕快照 2017-07-02 下午5.58.38.png b/assets/2017-07-02 下午5.58.38.png similarity index 100% rename from assets/屏幕快照 2017-07-02 下午5.58.38.png rename to assets/2017-07-02 下午5.58.38.png diff --git a/assets/屏幕快照 2017-07-02 下午7.02.53.png b/assets/2017-07-02 下午7.02.53.png similarity index 100% rename from assets/屏幕快照 2017-07-02 下午7.02.53.png rename to assets/2017-07-02 下午7.02.53.png diff --git a/assets/屏幕快照 2017-07-02 下午7.07.55.png b/assets/2017-07-02 下午7.07.55.png similarity index 100% rename from assets/屏幕快照 2017-07-02 下午7.07.55.png rename to assets/2017-07-02 下午7.07.55.png diff --git a/assets/屏幕快照 2017-09-15 下午5.56.28.png b/assets/2017-09-15 下午5.56.28.png similarity index 100% rename from assets/屏幕快照 2017-09-15 下午5.56.28.png rename to assets/2017-09-15 下午5.56.28.png diff --git a/assets/屏幕快照 2017-09-15 下午6.27.16.png b/assets/2017-09-15 下午6.27.16.png similarity index 100% rename from assets/屏幕快照 2017-09-15 下午6.27.16.png rename to assets/2017-09-15 下午6.27.16.png diff --git a/assets/屏幕快照 2017-11-24 上午10.49.35.png b/assets/2017-11-24 上午10.49.35.png similarity index 100% rename from assets/屏幕快照 2017-11-24 上午10.49.35.png rename to assets/2017-11-24 上午10.49.35.png diff --git a/assets/屏幕快照 2017-12-05 下午10.19.04.png b/assets/2017-12-05 下午10.19.04.png similarity index 100% rename from assets/屏幕快照 2017-12-05 下午10.19.04.png rename to assets/2017-12-05 下午10.19.04.png diff --git a/assets/屏幕快照 2018-07-23 上午10.27.22.png b/assets/2018-07-23 上午10.27.22.png similarity index 100% rename from assets/屏幕快照 2018-07-23 上午10.27.22.png rename to assets/2018-07-23 上午10.27.22.png diff --git a/assets/屏幕快照 2018-07-23 上午11.37.44.png b/assets/2018-07-23 上午11.37.44.png similarity index 100% rename from assets/屏幕快照 2018-07-23 上午11.37.44.png rename to assets/2018-07-23 上午11.37.44.png diff --git a/assets/屏幕快照 2018-07-23 上午9.22.39.png b/assets/2018-07-23 上午9.22.39.png similarity index 100% rename from assets/屏幕快照 2018-07-23 上午9.22.39.png rename to assets/2018-07-23 上午9.22.39.png diff --git a/assets/屏幕快照 2018-07-23 上午9.47.09.png b/assets/2018-07-23 上午9.47.09.png similarity index 100% rename from assets/屏幕快照 2018-07-23 上午9.47.09.png rename to assets/2018-07-23 上午9.47.09.png diff --git a/assets/2018-11-13-lifecycle.png b/assets/2018-11-13-lifecycle.png new file mode 100644 index 0000000..f8cd62c Binary files /dev/null and b/assets/2018-11-13-lifecycle.png differ diff --git a/assets/2018-11-14-slot1.png b/assets/2018-11-14-slot1.png new file mode 100644 index 0000000..36aa38c Binary files /dev/null and b/assets/2018-11-14-slot1.png differ diff --git a/assets/2018-11-14-slot2.png b/assets/2018-11-14-slot2.png new file mode 100644 index 0000000..a44ddd1 Binary files /dev/null and b/assets/2018-11-14-slot2.png differ diff --git a/assets/2018-11-15-AppConnectYSM.jpeg b/assets/2018-11-15-AppConnectYSM.jpeg new file mode 100644 index 0000000..53f63a8 Binary files /dev/null and b/assets/2018-11-15-AppConnectYSM.jpeg differ diff --git a/assets/2018-11-15-AppConstruct.jpeg b/assets/2018-11-15-AppConstruct.jpeg new file mode 100644 index 0000000..cf6f3d3 Binary files /dev/null and b/assets/2018-11-15-AppConstruct.jpeg differ diff --git a/assets/2018-11-15-AppSlicing.jpeg b/assets/2018-11-15-AppSlicing.jpeg new file mode 100644 index 0000000..7179fd1 Binary files /dev/null and b/assets/2018-11-15-AppSlicing.jpeg differ diff --git a/assets/2018-11-15-AppVariant.jpeg b/assets/2018-11-15-AppVariant.jpeg new file mode 100644 index 0000000..f410d5e Binary files /dev/null and b/assets/2018-11-15-AppVariant.jpeg differ diff --git a/assets/2018-11-15-AppVolume.jpeg b/assets/2018-11-15-AppVolume.jpeg new file mode 100644 index 0000000..5109829 Binary files /dev/null and b/assets/2018-11-15-AppVolume.jpeg differ diff --git a/assets/2018-11-15-XcodedYSM.jpeg b/assets/2018-11-15-XcodedYSM.jpeg new file mode 100644 index 0000000..30f5227 Binary files /dev/null and b/assets/2018-11-15-XcodedYSM.jpeg differ diff --git a/assets/2018-11-15-on-DemandResources.png b/assets/2018-11-15-on-DemandResources.png new file mode 100644 index 0000000..bd07ad8 Binary files /dev/null and b/assets/2018-11-15-on-DemandResources.png differ diff --git a/assets/2018-11-16-MVC.png b/assets/2018-11-16-MVC.png new file mode 100644 index 0000000..3b3dc19 Binary files /dev/null and b/assets/2018-11-16-MVC.png differ diff --git a/assets/2018-11-16-MVP.png b/assets/2018-11-16-MVP.png new file mode 100644 index 0000000..692f004 Binary files /dev/null and b/assets/2018-11-16-MVP.png differ diff --git a/assets/2018-11-16-MVVM.png b/assets/2018-11-16-MVVM.png new file mode 100644 index 0000000..f9a6386 Binary files /dev/null and b/assets/2018-11-16-MVVM.png differ diff --git a/assets/2018-11-16-VController-Model.png b/assets/2018-11-16-VController-Model.png new file mode 100644 index 0000000..32b37cb Binary files /dev/null and b/assets/2018-11-16-VController-Model.png differ diff --git a/assets/2018-11-16-iOSMVC.png b/assets/2018-11-16-iOSMVC.png new file mode 100644 index 0000000..66ed88f Binary files /dev/null and b/assets/2018-11-16-iOSMVC.png differ diff --git a/assets/2018-11-16-iOSmvvm.png b/assets/2018-11-16-iOSmvvm.png new file mode 100644 index 0000000..282310a Binary files /dev/null and b/assets/2018-11-16-iOSmvvm.png differ diff --git a/assets/2018-11-27-Nodecluster.png b/assets/2018-11-27-Nodecluster.png new file mode 100644 index 0000000..f2c0580 Binary files /dev/null and b/assets/2018-11-27-Nodecluster.png differ diff --git a/assets/2018-11-27-Server-scalability.png b/assets/2018-11-27-Server-scalability.png new file mode 100644 index 0000000..66760db Binary files /dev/null and b/assets/2018-11-27-Server-scalability.png differ diff --git a/assets/2018-11-27-reverse-proxy.png b/assets/2018-11-27-reverse-proxy.png new file mode 100644 index 0000000..fcd31ef Binary files /dev/null and b/assets/2018-11-27-reverse-proxy.png differ diff --git a/assets/2018-11-27-session-share-problem.png b/assets/2018-11-27-session-share-problem.png new file mode 100644 index 0000000..312bcf4 Binary files /dev/null and b/assets/2018-11-27-session-share-problem.png differ diff --git a/assets/2018-11-27-session-share-solution01.png b/assets/2018-11-27-session-share-solution01.png new file mode 100644 index 0000000..4629a5d Binary files /dev/null and b/assets/2018-11-27-session-share-solution01.png differ diff --git a/assets/2018-11-27-session-share-solution02.png b/assets/2018-11-27-session-share-solution02.png new file mode 100644 index 0000000..336d0eb Binary files /dev/null and b/assets/2018-11-27-session-share-solution02.png differ diff --git a/assets/2018-11-28-CQRS.png b/assets/2018-11-28-CQRS.png new file mode 100644 index 0000000..d66d9cd Binary files /dev/null and b/assets/2018-11-28-CQRS.png differ diff --git a/assets/2018-12-24-chrome1.png b/assets/2018-12-24-chrome1.png new file mode 100644 index 0000000..f6c5ed4 Binary files /dev/null and b/assets/2018-12-24-chrome1.png differ diff --git a/assets/2018-12-24-chrome2.png b/assets/2018-12-24-chrome2.png new file mode 100644 index 0000000..6a75bea Binary files /dev/null and b/assets/2018-12-24-chrome2.png differ diff --git a/assets/2018-12-24-chrome3.png b/assets/2018-12-24-chrome3.png new file mode 100644 index 0000000..e5b33fc Binary files /dev/null and b/assets/2018-12-24-chrome3.png differ diff --git a/assets/2018-12-24-chrome4.png b/assets/2018-12-24-chrome4.png new file mode 100644 index 0000000..6fc1f42 Binary files /dev/null and b/assets/2018-12-24-chrome4.png differ diff --git a/assets/2018_11_12_KVO.png b/assets/2018_11_12_KVO.png new file mode 100644 index 0000000..aec81ee Binary files /dev/null and b/assets/2018_11_12_KVO.png differ diff --git a/assets/2019-01-10-1.png b/assets/2019-01-10-1.png new file mode 100644 index 0000000..aecbed3 Binary files /dev/null and b/assets/2019-01-10-1.png differ diff --git a/assets/2019-01-10-2.png b/assets/2019-01-10-2.png new file mode 100644 index 0000000..e0e19e0 Binary files /dev/null and b/assets/2019-01-10-2.png differ diff --git a/assets/2019-01-10-3.png b/assets/2019-01-10-3.png new file mode 100644 index 0000000..8dd42b9 Binary files /dev/null and b/assets/2019-01-10-3.png differ diff --git a/assets/2019-01-15-01.png b/assets/2019-01-15-01.png new file mode 100644 index 0000000..274659d Binary files /dev/null and b/assets/2019-01-15-01.png differ diff --git a/assets/2019-01-28-Object.defineProperty.png b/assets/2019-01-28-Object.defineProperty.png new file mode 100644 index 0000000..c2f29ab Binary files /dev/null and b/assets/2019-01-28-Object.defineProperty.png differ diff --git a/assets/2019-01-28-Vue-MVVM-2.png b/assets/2019-01-28-Vue-MVVM-2.png new file mode 100644 index 0000000..46e2852 Binary files /dev/null and b/assets/2019-01-28-Vue-MVVM-2.png differ diff --git a/assets/2019-01-28-Vue-MVVM.png b/assets/2019-01-28-Vue-MVVM.png new file mode 100644 index 0000000..9eaee5c Binary files /dev/null and b/assets/2019-01-28-Vue-MVVM.png differ diff --git a/assets/2019-01-29-MVC.png b/assets/2019-01-29-MVC.png new file mode 100644 index 0000000..3b3dc19 Binary files /dev/null and b/assets/2019-01-29-MVC.png differ diff --git a/assets/2019-01-29-MVP.png b/assets/2019-01-29-MVP.png new file mode 100644 index 0000000..692f004 Binary files /dev/null and b/assets/2019-01-29-MVP.png differ diff --git a/assets/2019-01-29-MVVM.png b/assets/2019-01-29-MVVM.png new file mode 100644 index 0000000..f9a6386 Binary files /dev/null and b/assets/2019-01-29-MVVM.png differ diff --git a/assets/2019-03-07-UserTracet1.PNG b/assets/2019-03-07-UserTracet1.PNG new file mode 100644 index 0000000..87dbaa1 Binary files /dev/null and b/assets/2019-03-07-UserTracet1.PNG differ diff --git a/assets/2019-04-01-userTrack.png b/assets/2019-04-01-userTrack.png new file mode 100644 index 0000000..7ca2720 Binary files /dev/null and b/assets/2019-04-01-userTrack.png differ diff --git a/assets/2019-04-05-userTrack1.png b/assets/2019-04-05-userTrack1.png new file mode 100644 index 0000000..24da767 Binary files /dev/null and b/assets/2019-04-05-userTrack1.png differ diff --git a/assets/2019-04-05-userTrack2.png b/assets/2019-04-05-userTrack2.png new file mode 100644 index 0000000..efa78f3 Binary files /dev/null and b/assets/2019-04-05-userTrack2.png differ diff --git a/assets/2019-04-05-userTrack3.png b/assets/2019-04-05-userTrack3.png new file mode 100644 index 0000000..e7c9c8a Binary files /dev/null and b/assets/2019-04-05-userTrack3.png differ diff --git a/assets/2019-04-05-userTrack4.png b/assets/2019-04-05-userTrack4.png new file mode 100644 index 0000000..bbdce5b Binary files /dev/null and b/assets/2019-04-05-userTrack4.png differ diff --git a/assets/2019-04-06-autolayout1.jpg b/assets/2019-04-06-autolayout1.jpg new file mode 100644 index 0000000..27e1b52 Binary files /dev/null and b/assets/2019-04-06-autolayout1.jpg differ diff --git a/assets/2019-04-06-autolayout2.jpg b/assets/2019-04-06-autolayout2.jpg new file mode 100644 index 0000000..87c76c6 Binary files /dev/null and b/assets/2019-04-06-autolayout2.jpg differ diff --git a/assets/2019-04-09-UserTrack.png b/assets/2019-04-09-UserTrack.png new file mode 100644 index 0000000..2b57bb9 Binary files /dev/null and b/assets/2019-04-09-UserTrack.png differ diff --git a/assets/2019-04-11-UserTrack01.png b/assets/2019-04-11-UserTrack01.png new file mode 100644 index 0000000..55e5d25 Binary files /dev/null and b/assets/2019-04-11-UserTrack01.png differ diff --git a/assets/2019-04-11-UserTrack2.png b/assets/2019-04-11-UserTrack2.png new file mode 100644 index 0000000..44df321 Binary files /dev/null and b/assets/2019-04-11-UserTrack2.png differ diff --git a/assets/2019-04-11-UserTrack3.png b/assets/2019-04-11-UserTrack3.png new file mode 100644 index 0000000..08b0137 Binary files /dev/null and b/assets/2019-04-11-UserTrack3.png differ diff --git a/assets/2019-05-05-CodeClean.png b/assets/2019-05-05-CodeClean.png new file mode 100644 index 0000000..b0f8283 Binary files /dev/null and b/assets/2019-05-05-CodeClean.png differ diff --git a/assets/2019-05-05-LinkMap-ObjectFile.png b/assets/2019-05-05-LinkMap-ObjectFile.png new file mode 100644 index 0000000..002076b Binary files /dev/null and b/assets/2019-05-05-LinkMap-ObjectFile.png differ diff --git a/assets/2019-05-05-LinkMap-Sections.png b/assets/2019-05-05-LinkMap-Sections.png new file mode 100644 index 0000000..85b4844 Binary files /dev/null and b/assets/2019-05-05-LinkMap-Sections.png differ diff --git a/assets/2019-05-05-LinkMap-Symbols.png b/assets/2019-05-05-LinkMap-Symbols.png new file mode 100644 index 0000000..ce26918 Binary files /dev/null and b/assets/2019-05-05-LinkMap-Symbols.png differ diff --git a/assets/2019-05-06-LinkMap-Structure.png b/assets/2019-05-06-LinkMap-Structure.png new file mode 100644 index 0000000..c147d95 Binary files /dev/null and b/assets/2019-05-06-LinkMap-Structure.png differ diff --git a/assets/2019-05-06-LinkMap-Xcode.png b/assets/2019-05-06-LinkMap-Xcode.png new file mode 100644 index 0000000..c7bf074 Binary files /dev/null and b/assets/2019-05-06-LinkMap-Xcode.png differ diff --git a/assets/2019-05-07-Mach-O-Inspect.png b/assets/2019-05-07-Mach-O-Inspect.png new file mode 100644 index 0000000..5a35db4 Binary files /dev/null and b/assets/2019-05-07-Mach-O-Inspect.png differ diff --git a/assets/2019-05-07-TimeProfile-imageWithContentsOfFile.png b/assets/2019-05-07-TimeProfile-imageWithContentsOfFile.png new file mode 100644 index 0000000..3885184 Binary files /dev/null and b/assets/2019-05-07-TimeProfile-imageWithContentsOfFile.png differ diff --git a/assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png b/assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png new file mode 100644 index 0000000..0a99ec4 Binary files /dev/null and b/assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png differ diff --git a/assets/2019-05-07-Timeprofile-imageNamedFromAssets.png b/assets/2019-05-07-Timeprofile-imageNamedFromAssets.png new file mode 100644 index 0000000..9c653d2 Binary files /dev/null and b/assets/2019-05-07-Timeprofile-imageNamedFromAssets.png differ diff --git a/assets/2019-05-08-Cocopod-Assets.png b/assets/2019-05-08-Cocopod-Assets.png new file mode 100644 index 0000000..2453981 Binary files /dev/null and b/assets/2019-05-08-Cocopod-Assets.png differ diff --git a/assets/2019-05-09-AppThinning-Comparation.png b/assets/2019-05-09-AppThinning-Comparation.png new file mode 100644 index 0000000..c44d331 Binary files /dev/null and b/assets/2019-05-09-AppThinning-Comparation.png differ diff --git a/assets/2019-05-09-PodComponentAnalysis.png b/assets/2019-05-09-PodComponentAnalysis.png new file mode 100644 index 0000000..39d5516 Binary files /dev/null and b/assets/2019-05-09-PodComponentAnalysis.png differ diff --git a/assets/2019-05-09-RunLoop-review.png b/assets/2019-05-09-RunLoop-review.png new file mode 100644 index 0000000..f0eeb27 Binary files /dev/null and b/assets/2019-05-09-RunLoop-review.png differ diff --git a/assets/2019-05-13-Cocopod-Lint-Cache.png b/assets/2019-05-13-Cocopod-Lint-Cache.png new file mode 100644 index 0000000..6be6e5c Binary files /dev/null and b/assets/2019-05-13-Cocopod-Lint-Cache.png differ diff --git a/assets/2019-05-15-OCLint-Verify.png b/assets/2019-05-15-OCLint-Verify.png new file mode 100644 index 0000000..3341f92 Binary files /dev/null and b/assets/2019-05-15-OCLint-Verify.png differ diff --git a/assets/2019-05-23-LintTarget.png b/assets/2019-05-23-LintTarget.png new file mode 100644 index 0000000..05b57af Binary files /dev/null and b/assets/2019-05-23-LintTarget.png differ diff --git a/assets/2019-05-23-OCLint-Report-HTML.png b/assets/2019-05-23-OCLint-Report-HTML.png new file mode 100644 index 0000000..e355dc0 Binary files /dev/null and b/assets/2019-05-23-OCLint-Report-HTML.png differ diff --git a/assets/2019-05-23-OCLint-Rule-Xcodeproj.png b/assets/2019-05-23-OCLint-Rule-Xcodeproj.png new file mode 100644 index 0000000..3894894 Binary files /dev/null and b/assets/2019-05-23-OCLint-Rule-Xcodeproj.png differ diff --git a/assets/2019-05-23-OCLint-Xcode-Rules.png b/assets/2019-05-23-OCLint-Xcode-Rules.png new file mode 100644 index 0000000..edc7c9b Binary files /dev/null and b/assets/2019-05-23-OCLint-Xcode-Rules.png differ diff --git a/assets/2019-05-23-OCLint-custom-rule-inXcodeproj.png b/assets/2019-05-23-OCLint-custom-rule-inXcodeproj.png new file mode 100644 index 0000000..8beea18 Binary files /dev/null and b/assets/2019-05-23-OCLint-custom-rule-inXcodeproj.png differ diff --git a/assets/2019-05-23-lint-in-Xcode.png b/assets/2019-05-23-lint-in-Xcode.png new file mode 100644 index 0000000..5c8e7f4 Binary files /dev/null and b/assets/2019-05-23-lint-in-Xcode.png differ diff --git a/assets/2019-05-23-lint-result-inXcode.png b/assets/2019-05-23-lint-result-inXcode.png new file mode 100644 index 0000000..a57bb96 Binary files /dev/null and b/assets/2019-05-23-lint-result-inXcode.png differ diff --git a/assets/2019-05-23-oclint-result-html.png b/assets/2019-05-23-oclint-result-html.png new file mode 100644 index 0000000..65a272e Binary files /dev/null and b/assets/2019-05-23-oclint-result-html.png differ diff --git a/assets/2019-06-13-TCPAnimation.gif b/assets/2019-06-13-TCPAnimation.gif new file mode 100644 index 0000000..43696aa Binary files /dev/null and b/assets/2019-06-13-TCPAnimation.gif differ diff --git a/assets/2019-06-13-TCPFourShakeHands.png b/assets/2019-06-13-TCPFourShakeHands.png new file mode 100644 index 0000000..af84b62 Binary files /dev/null and b/assets/2019-06-13-TCPFourShakeHands.png differ diff --git a/assets/2019-06-13-TCPThreeShakeHandle.png b/assets/2019-06-13-TCPThreeShakeHandle.png new file mode 100644 index 0000000..63714d7 Binary files /dev/null and b/assets/2019-06-13-TCPThreeShakeHandle.png differ diff --git a/assets/2019-06-13-UDP.gif b/assets/2019-06-13-UDP.gif new file mode 100644 index 0000000..2df8d6c Binary files /dev/null and b/assets/2019-06-13-UDP.gif differ diff --git a/assets/2019-06-13-UDPHeader.png b/assets/2019-06-13-UDPHeader.png new file mode 100644 index 0000000..3854e4c Binary files /dev/null and b/assets/2019-06-13-UDPHeader.png differ diff --git a/assets/2019-06-14-BadNetwork.png b/assets/2019-06-14-BadNetwork.png new file mode 100644 index 0000000..fb86ef6 Binary files /dev/null and b/assets/2019-06-14-BadNetwork.png differ diff --git a/assets/2019-06-14-DNSLookUP.png b/assets/2019-06-14-DNSLookUP.png new file mode 100644 index 0000000..7e989f3 Binary files /dev/null and b/assets/2019-06-14-DNSLookUP.png differ diff --git a/assets/2019-06-14-MultiplesRoutes.png b/assets/2019-06-14-MultiplesRoutes.png new file mode 100644 index 0000000..24cbc47 Binary files /dev/null and b/assets/2019-06-14-MultiplesRoutes.png differ diff --git a/assets/2019-06-17-ReactLifecycle.png b/assets/2019-06-17-ReactLifecycle.png new file mode 100644 index 0000000..dd792e3 Binary files /dev/null and b/assets/2019-06-17-ReactLifecycle.png differ diff --git a/assets/2019-06-24-reduxDataflow.png b/assets/2019-06-24-reduxDataflow.png new file mode 100644 index 0000000..6151979 Binary files /dev/null and b/assets/2019-06-24-reduxDataflow.png differ diff --git a/assets/2019-06-24-reduxThunk.png b/assets/2019-06-24-reduxThunk.png new file mode 100644 index 0000000..b964b7e Binary files /dev/null and b/assets/2019-06-24-reduxThunk.png differ diff --git a/assets/2019-06-26-Redux-Structures.png b/assets/2019-06-26-Redux-Structures.png new file mode 100644 index 0000000..6e33a1e Binary files /dev/null and b/assets/2019-06-26-Redux-Structures.png differ diff --git a/assets/2019-06-28-DocumentRangeObject1.png b/assets/2019-06-28-DocumentRangeObject1.png new file mode 100644 index 0000000..465160c Binary files /dev/null and b/assets/2019-06-28-DocumentRangeObject1.png differ diff --git a/assets/2019-06-28-DocumentRangeObject2.png b/assets/2019-06-28-DocumentRangeObject2.png new file mode 100644 index 0000000..6d75c44 Binary files /dev/null and b/assets/2019-06-28-DocumentRangeObject2.png differ diff --git a/assets/2019-06-28-RichTextEditor.png b/assets/2019-06-28-RichTextEditor.png new file mode 100644 index 0000000..e53480d Binary files /dev/null and b/assets/2019-06-28-RichTextEditor.png differ diff --git a/assets/2019-07-03-XpathBind.gif b/assets/2019-07-03-XpathBind.gif new file mode 100644 index 0000000..7a644e5 Binary files /dev/null and b/assets/2019-07-03-XpathBind.gif differ diff --git a/assets/2019-07-05-App-structure.jpeg b/assets/2019-07-05-App-structure.jpeg new file mode 100644 index 0000000..76e1790 Binary files /dev/null and b/assets/2019-07-05-App-structure.jpeg differ diff --git a/assets/2019-07-08-ComponentExposure.jpeg b/assets/2019-07-08-ComponentExposure.jpeg new file mode 100644 index 0000000..5826674 Binary files /dev/null and b/assets/2019-07-08-ComponentExposure.jpeg differ diff --git a/assets/2019-07-08-Hybrid-ResourceStructure.png b/assets/2019-07-08-Hybrid-ResourceStructure.png new file mode 100644 index 0000000..d241777 Binary files /dev/null and b/assets/2019-07-08-Hybrid-ResourceStructure.png differ diff --git a/assets/2019-07-22-Flutter_Verify.png b/assets/2019-07-22-Flutter_Verify.png new file mode 100644 index 0000000..c5b2e91 Binary files /dev/null and b/assets/2019-07-22-Flutter_Verify.png differ diff --git a/assets/2019-08-12-Flutter.png b/assets/2019-08-12-Flutter.png new file mode 100644 index 0000000..8e2c38b Binary files /dev/null and b/assets/2019-08-12-Flutter.png differ diff --git a/assets/2019-08-13-FlutterDirectory.png b/assets/2019-08-13-FlutterDirectory.png new file mode 100644 index 0000000..8d67ed5 Binary files /dev/null and b/assets/2019-08-13-FlutterDirectory.png differ diff --git a/assets/2019-08-13-GitLogs.png b/assets/2019-08-13-GitLogs.png new file mode 100644 index 0000000..fe62fd8 Binary files /dev/null and b/assets/2019-08-13-GitLogs.png differ diff --git a/assets/2019-08-29-NetworkError1.jpg b/assets/2019-08-29-NetworkError1.jpg new file mode 100644 index 0000000..0ed9535 Binary files /dev/null and b/assets/2019-08-29-NetworkError1.jpg differ diff --git a/assets/2019-08-29-NetworkError2.png b/assets/2019-08-29-NetworkError2.png new file mode 100644 index 0000000..280c00e Binary files /dev/null and b/assets/2019-08-29-NetworkError2.png differ diff --git a/assets/2019-09-15-BigEndian.png b/assets/2019-09-15-BigEndian.png new file mode 100644 index 0000000..b10d98b Binary files /dev/null and b/assets/2019-09-15-BigEndian.png differ diff --git a/assets/2019-09-30-searchTools.png b/assets/2019-09-30-searchTools.png new file mode 100644 index 0000000..4de056c Binary files /dev/null and b/assets/2019-09-30-searchTools.png differ diff --git a/assets/2019-10--07-TCP.png b/assets/2019-10--07-TCP.png new file mode 100644 index 0000000..dd04ac5 Binary files /dev/null and b/assets/2019-10--07-TCP.png differ diff --git a/assets/2019-10-07-OSI.jpg b/assets/2019-10-07-OSI.jpg new file mode 100644 index 0000000..29ccf8c Binary files /dev/null and b/assets/2019-10-07-OSI.jpg differ diff --git a/assets/2019-10-10-APM_Crash.png b/assets/2019-10-10-APM_Crash.png new file mode 100644 index 0000000..fafd776 Binary files /dev/null and b/assets/2019-10-10-APM_Crash.png differ diff --git a/assets/2019-10-10-APM_Stack_trace_api.png b/assets/2019-10-10-APM_Stack_trace_api.png new file mode 100644 index 0000000..25dd314 Binary files /dev/null and b/assets/2019-10-10-APM_Stack_trace_api.png differ diff --git a/assets/2019-11-26-VPS.png b/assets/2019-11-26-VPS.png new file mode 100644 index 0000000..470c802 Binary files /dev/null and b/assets/2019-11-26-VPS.png differ diff --git a/assets/2019-12-16-candle.png b/assets/2019-12-16-candle.png new file mode 100644 index 0000000..25f8855 Binary files /dev/null and b/assets/2019-12-16-candle.png differ diff --git a/assets/2019-12-16-nodeUT.png b/assets/2019-12-16-nodeUT.png new file mode 100644 index 0000000..eea1029 Binary files /dev/null and b/assets/2019-12-16-nodeUT.png differ diff --git a/assets/2019-12-18-wax-project.png b/assets/2019-12-18-wax-project.png new file mode 100644 index 0000000..f6651f3 Binary files /dev/null and b/assets/2019-12-18-wax-project.png differ diff --git a/assets/2019-12-19-lock.png b/assets/2019-12-19-lock.png new file mode 100644 index 0000000..508760b Binary files /dev/null and b/assets/2019-12-19-lock.png differ diff --git a/assets/2019-12-20-RNErrorMonitor.png b/assets/2019-12-20-RNErrorMonitor.png new file mode 100644 index 0000000..6a4cf3d Binary files /dev/null and b/assets/2019-12-20-RNErrorMonitor.png differ diff --git a/assets/20190304-fileTmplates3.png b/assets/20190304-fileTmplates3.png new file mode 100644 index 0000000..8968daf Binary files /dev/null and b/assets/20190304-fileTmplates3.png differ diff --git a/assets/20190304_filetemplates.png b/assets/20190304_filetemplates.png new file mode 100644 index 0000000..bcbaa72 Binary files /dev/null and b/assets/20190304_filetemplates.png differ diff --git a/assets/20190304_filetemplates4.png b/assets/20190304_filetemplates4.png new file mode 100644 index 0000000..96ed1eb Binary files /dev/null and b/assets/20190304_filetemplates4.png differ diff --git a/assets/20190319-filetemplate5.png b/assets/20190319-filetemplate5.png new file mode 100644 index 0000000..483e40d Binary files /dev/null and b/assets/20190319-filetemplate5.png differ diff --git a/assets/20190319-filetemplate6.png b/assets/20190319-filetemplate6.png new file mode 100644 index 0000000..4ade175 Binary files /dev/null and b/assets/20190319-filetemplate6.png differ diff --git a/assets/20190319-filetemplateSelf.png b/assets/20190319-filetemplateSelf.png new file mode 100644 index 0000000..4f10e91 Binary files /dev/null and b/assets/20190319-filetemplateSelf.png differ diff --git a/assets/2020-02-02-JSModule.png b/assets/2020-02-02-JSModule.png new file mode 100644 index 0000000..9d4c770 Binary files /dev/null and b/assets/2020-02-02-JSModule.png differ diff --git a/assets/2020-02-02-WebpackModuleOptions.png b/assets/2020-02-02-WebpackModuleOptions.png new file mode 100644 index 0000000..6ec8dde Binary files /dev/null and b/assets/2020-02-02-WebpackModuleOptions.png differ diff --git a/assets/2020-02-02-screen_display_gpu.png b/assets/2020-02-02-screen_display_gpu.png new file mode 100644 index 0000000..7835602 Binary files /dev/null and b/assets/2020-02-02-screen_display_gpu.png differ diff --git a/assets/2020-02-04-Comparison_double_triple_buffering.png b/assets/2020-02-04-Comparison_double_triple_buffering.png new file mode 100644 index 0000000..348d304 Binary files /dev/null and b/assets/2020-02-04-Comparison_double_triple_buffering.png differ diff --git a/assets/2020-02-04-ios_frame_drop.png b/assets/2020-02-04-ios_frame_drop.png new file mode 100644 index 0000000..cd1840f Binary files /dev/null and b/assets/2020-02-04-ios_frame_drop.png differ diff --git a/assets/2020-02-04-ios_screen_scan.png b/assets/2020-02-04-ios_screen_scan.png new file mode 100644 index 0000000..f3055ef Binary files /dev/null and b/assets/2020-02-04-ios_screen_scan.png differ diff --git a/assets/2020-02-05-RunLoop.png b/assets/2020-02-05-RunLoop.png new file mode 100644 index 0000000..9d4cc22 Binary files /dev/null and b/assets/2020-02-05-RunLoop.png differ diff --git a/assets/2020-02-08-StackFrame.png b/assets/2020-02-08-StackFrame.png new file mode 100644 index 0000000..01316bf Binary files /dev/null and b/assets/2020-02-08-StackFrame.png differ diff --git a/assets/2020-02-08-ios_vsync_runloop.png b/assets/2020-02-08-ios_vsync_runloop.png new file mode 100644 index 0000000..1229cd1 Binary files /dev/null and b/assets/2020-02-08-ios_vsync_runloop.png differ diff --git a/assets/2020-02-10-AppSpeed-Main.png b/assets/2020-02-10-AppSpeed-Main.png new file mode 100644 index 0000000..486a837 Binary files /dev/null and b/assets/2020-02-10-AppSpeed-Main.png differ diff --git a/assets/2020-02-10-AppSpeed-PreMain.png b/assets/2020-02-10-AppSpeed-PreMain.png new file mode 100644 index 0000000..8481c8f Binary files /dev/null and b/assets/2020-02-10-AppSpeed-PreMain.png differ diff --git a/assets/4.png b/assets/4.png index 4bfa3f2..8a24beb 100644 Binary files a/assets/4.png and b/assets/4.png differ diff --git a/第三部分 后端技术/assets/6693515-3b17bdcc22e1a5a3.png b/assets/6693515-3b17bdcc22e1a5a3.png similarity index 100% rename from 第三部分 后端技术/assets/6693515-3b17bdcc22e1a5a3.png rename to assets/6693515-3b17bdcc22e1a5a3.png diff --git a/第三部分 后端技术/assets/6693515-8e5bea48462a7904.jpeg b/assets/6693515-8e5bea48462a7904.jpeg similarity index 100% rename from 第三部分 后端技术/assets/6693515-8e5bea48462a7904.jpeg rename to assets/6693515-8e5bea48462a7904.jpeg diff --git a/第三部分 后端技术/assets/6693515-d566f3006b82c505.jpeg b/assets/6693515-d566f3006b82c505.jpeg similarity index 100% rename from 第三部分 后端技术/assets/6693515-d566f3006b82c505.jpeg rename to assets/6693515-d566f3006b82c505.jpeg diff --git a/assets/CFRunLoop.c b/assets/CFRunLoop.c new file mode 100755 index 0000000..fc33785 --- /dev/null +++ b/assets/CFRunLoop.c @@ -0,0 +1,4008 @@ +/* + * Copyright (c) 2014 Apple Inc. All rights reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ + +/* CFRunLoop.c + Copyright (c) 1998-2014, Apple Inc. All rights reserved. + Responsibility: Tony Parker +*/ + +#include +#include +#include +#include +#include +#include "CFInternal.h" +#include +#include +#include +#include +#include + + +#if DEPLOYMENT_TARGET_WINDOWS +#include +#endif +#include + +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern mach_port_t _dispatch_get_main_queue_port_4CF(void); +extern void _dispatch_main_queue_callback_4CF(mach_msg_header_t *msg); +#elif DEPLOYMENT_TARGET_WINDOWS +#include +DISPATCH_EXPORT HANDLE _dispatch_get_main_queue_handle_4CF(void); +DISPATCH_EXPORT void _dispatch_main_queue_callback_4CF(void); + +#define MACH_PORT_NULL 0 +#define mach_port_name_t HANDLE +#define mach_port_t HANDLE +#define _dispatch_get_main_queue_port_4CF _dispatch_get_main_queue_handle_4CF +#define _dispatch_main_queue_callback_4CF(x) _dispatch_main_queue_callback_4CF() + +#define AbsoluteTime LARGE_INTEGER + +#endif + +#if DEPLOYMENT_TARGET_WINDOWS || DEPLOYMENT_TARGET_IPHONESIMULATOR +CF_EXPORT pthread_t _CF_pthread_main_thread_np(void); +#define pthread_main_thread_np() _CF_pthread_main_thread_np() +#endif + +#include +#include + +#if DEPLOYMENT_TARGET_MACOSX +#define USE_DISPATCH_SOURCE_FOR_TIMERS 1 +#define USE_MK_TIMER_TOO 1 +#else +#define USE_DISPATCH_SOURCE_FOR_TIMERS 0 +#define USE_MK_TIMER_TOO 1 +#endif + + +static int _LogCFRunLoop = 0; +static void _runLoopTimerWithBlockContext(CFRunLoopTimerRef timer, void *opaqueBlock); + +// for conservative arithmetic safety, such that (TIMER_DATE_LIMIT + TIMER_INTERVAL_LIMIT + kCFAbsoluteTimeIntervalSince1970) * 10^9 < 2^63 +#define TIMER_DATE_LIMIT 4039289856.0 +#define TIMER_INTERVAL_LIMIT 504911232.0 + +#define HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY 0 + +#define CRASH(string, errcode) do { char msg[256]; snprintf(msg, 256, string, errcode); CRSetCrashLogMessage(msg); HALT; } while (0) + +#if DEPLOYMENT_TARGET_WINDOWS + +static pthread_t kNilPthreadT = { nil, nil }; +#define pthreadPointer(a) a.p +typedef int kern_return_t; +#define KERN_SUCCESS 0 + +#else + +static pthread_t kNilPthreadT = (pthread_t)0; +#define pthreadPointer(a) a +#define lockCount(a) a +#endif + +#pragma mark - + +#define CF_RUN_LOOP_PROBES 0 + +#if CF_RUN_LOOP_PROBES +#include "CFRunLoopProbes.h" +#else +#define CFRUNLOOP_NEXT_TIMER_ARMED(arg0) do { } while (0) +#define CFRUNLOOP_NEXT_TIMER_ARMED_ENABLED() (0) +#define CFRUNLOOP_POLL() do { } while (0) +#define CFRUNLOOP_POLL_ENABLED() (0) +#define CFRUNLOOP_SLEEP() do { } while (0) +#define CFRUNLOOP_SLEEP_ENABLED() (0) +#define CFRUNLOOP_SOURCE_FIRED(arg0, arg1, arg2) do { } while (0) +#define CFRUNLOOP_SOURCE_FIRED_ENABLED() (0) +#define CFRUNLOOP_TIMER_CREATED(arg0, arg1, arg2, arg3, arg4, arg5, arg6) do { } while (0) +#define CFRUNLOOP_TIMER_CREATED_ENABLED() (0) +#define CFRUNLOOP_TIMER_FIRED(arg0, arg1, arg2, arg3, arg4) do { } while (0) +#define CFRUNLOOP_TIMER_FIRED_ENABLED() (0) +#define CFRUNLOOP_TIMER_RESCHEDULED(arg0, arg1, arg2, arg3, arg4, arg5) do { } while (0) +#define CFRUNLOOP_TIMER_RESCHEDULED_ENABLED() (0) +#define CFRUNLOOP_WAKEUP(arg0) do { } while (0) +#define CFRUNLOOP_WAKEUP_ENABLED() (0) +#define CFRUNLOOP_WAKEUP_FOR_DISPATCH() do { } while (0) +#define CFRUNLOOP_WAKEUP_FOR_DISPATCH_ENABLED() (0) +#define CFRUNLOOP_WAKEUP_FOR_NOTHING() do { } while (0) +#define CFRUNLOOP_WAKEUP_FOR_NOTHING_ENABLED() (0) +#define CFRUNLOOP_WAKEUP_FOR_SOURCE() do { } while (0) +#define CFRUNLOOP_WAKEUP_FOR_SOURCE_ENABLED() (0) +#define CFRUNLOOP_WAKEUP_FOR_TIMEOUT() do { } while (0) +#define CFRUNLOOP_WAKEUP_FOR_TIMEOUT_ENABLED() (0) +#define CFRUNLOOP_WAKEUP_FOR_TIMER() do { } while (0) +#define CFRUNLOOP_WAKEUP_FOR_TIMER_ENABLED() (0) +#define CFRUNLOOP_WAKEUP_FOR_WAKEUP() do { } while (0) +#define CFRUNLOOP_WAKEUP_FOR_WAKEUP_ENABLED() (0) +#endif + +// In order to reuse most of the code across Mach and Windows v1 RunLoopSources, we define a +// simple abstraction layer spanning Mach ports and Windows HANDLES +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + +CF_PRIVATE uint32_t __CFGetProcessPortCount(void) { + ipc_info_space_t info; + ipc_info_name_array_t table = 0; + mach_msg_type_number_t tableCount = 0; + ipc_info_tree_name_array_t tree = 0; + mach_msg_type_number_t treeCount = 0; + + kern_return_t ret = mach_port_space_info(mach_task_self(), &info, &table, &tableCount, &tree, &treeCount); + if (ret != KERN_SUCCESS) { + return (uint32_t)0; + } + if (table != NULL) { + ret = vm_deallocate(mach_task_self(), (vm_address_t)table, tableCount * sizeof(*table)); + } + if (tree != NULL) { + ret = vm_deallocate(mach_task_self(), (vm_address_t)tree, treeCount * sizeof(*tree)); + } + return (uint32_t)tableCount; +} + +CF_PRIVATE CFArrayRef __CFStopAllThreads(void) { + CFMutableArrayRef suspended_list = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, NULL); + mach_port_t my_task = mach_task_self(); + mach_port_t my_thread = mach_thread_self(); + thread_act_array_t thr_list = 0; + mach_msg_type_number_t thr_cnt = 0; + + // really, should loop doing the stopping until no more threads get added to the list N times in a row + kern_return_t ret = task_threads(my_task, &thr_list, &thr_cnt); + if (ret == KERN_SUCCESS) { + for (CFIndex idx = 0; idx < thr_cnt; idx++) { + thread_act_t thread = thr_list[idx]; + if (thread == my_thread) continue; + if (CFArrayContainsValue(suspended_list, CFRangeMake(0, CFArrayGetCount(suspended_list)), (const void *)(uintptr_t)thread)) continue; + ret = thread_suspend(thread); + if (ret == KERN_SUCCESS) { + CFArrayAppendValue(suspended_list, (const void *)(uintptr_t)thread); + } else { + mach_port_deallocate(my_task, thread); + } + } + vm_deallocate(my_task, (vm_address_t)thr_list, sizeof(thread_t) * thr_cnt); + } + mach_port_deallocate(my_task, my_thread); + return suspended_list; +} + +CF_PRIVATE void __CFRestartAllThreads(CFArrayRef threads) { + for (CFIndex idx = 0; idx < CFArrayGetCount(threads); idx++) { + thread_act_t thread = (thread_act_t)(uintptr_t)CFArrayGetValueAtIndex(threads, idx); + kern_return_t ret = thread_resume(thread); + if (ret != KERN_SUCCESS) CRASH("*** Failure from thread_resume (%d) ***", ret); + mach_port_deallocate(mach_task_self(), thread); + } +} + +static uint32_t __CF_last_warned_port_count = 0; + +static void foo() __attribute__((unused)); +static void foo() { + uint32_t pcnt = __CFGetProcessPortCount(); + if (__CF_last_warned_port_count + 1000 < pcnt) { + CFArrayRef threads = __CFStopAllThreads(); + + +// do stuff here +CFOptionFlags responseFlags = 0; +SInt32 result = CFUserNotificationDisplayAlert(0.0, kCFUserNotificationCautionAlertLevel, NULL, NULL, NULL, CFSTR("High Mach Port Usage"), CFSTR("This application is using a lot of Mach ports."), CFSTR("Default"), CFSTR("Altern"), CFSTR("Other b"), &responseFlags); +if (0 != result) { + CFLog(3, CFSTR("ERROR")); +} else { + switch (responseFlags) { + case kCFUserNotificationDefaultResponse: CFLog(3, CFSTR("DefaultR")); break; + case kCFUserNotificationAlternateResponse: CFLog(3, CFSTR("AltR")); break; + case kCFUserNotificationOtherResponse: CFLog(3, CFSTR("OtherR")); break; + case kCFUserNotificationCancelResponse: CFLog(3, CFSTR("CancelR")); break; + } +} + + + __CFRestartAllThreads(threads); + CFRelease(threads); + __CF_last_warned_port_count = pcnt; + } +} + + +typedef mach_port_t __CFPort; +#define CFPORT_NULL MACH_PORT_NULL +typedef mach_port_t __CFPortSet; + +static void __THE_SYSTEM_HAS_NO_PORTS_AVAILABLE__(kern_return_t ret) __attribute__((noinline)); +static void __THE_SYSTEM_HAS_NO_PORTS_AVAILABLE__(kern_return_t ret) { HALT; }; + +static __CFPort __CFPortAllocate(void) { + __CFPort result = CFPORT_NULL; + kern_return_t ret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &result); + if (KERN_SUCCESS != ret) { + char msg[256]; + snprintf(msg, 256, "*** The system has no mach ports available. You may be able to diagnose which application(s) are using ports by using 'top' or Activity Monitor. (%d) ***", ret); + CRSetCrashLogMessage(msg); + __THE_SYSTEM_HAS_NO_PORTS_AVAILABLE__(ret); + return CFPORT_NULL; + } + + ret = mach_port_insert_right(mach_task_self(), result, result, MACH_MSG_TYPE_MAKE_SEND); + if (KERN_SUCCESS != ret) CRASH("*** Unable to set send right on mach port. (%d) ***", ret); + + + mach_port_limits_t limits; + limits.mpl_qlimit = 1; + ret = mach_port_set_attributes(mach_task_self(), result, MACH_PORT_LIMITS_INFO, (mach_port_info_t)&limits, MACH_PORT_LIMITS_INFO_COUNT); + if (KERN_SUCCESS != ret) CRASH("*** Unable to set attributes on mach port. (%d) ***", ret); + + return result; +} + +CF_INLINE void __CFPortFree(__CFPort port) { + mach_port_destroy(mach_task_self(), port); +} + +static void __THE_SYSTEM_HAS_NO_PORT_SETS_AVAILABLE__(kern_return_t ret) __attribute__((noinline)); +static void __THE_SYSTEM_HAS_NO_PORT_SETS_AVAILABLE__(kern_return_t ret) { HALT; }; + +CF_INLINE __CFPortSet __CFPortSetAllocate(void) { + __CFPortSet result; + kern_return_t ret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_PORT_SET, &result); + if (KERN_SUCCESS != ret) { __THE_SYSTEM_HAS_NO_PORT_SETS_AVAILABLE__(ret); } + return (KERN_SUCCESS == ret) ? result : CFPORT_NULL; +} + +CF_INLINE kern_return_t __CFPortSetInsert(__CFPort port, __CFPortSet portSet) { + if (MACH_PORT_NULL == port) { + return -1; + } + return mach_port_insert_member(mach_task_self(), port, portSet); +} + +CF_INLINE kern_return_t __CFPortSetRemove(__CFPort port, __CFPortSet portSet) { + if (MACH_PORT_NULL == port) { + return -1; + } + return mach_port_extract_member(mach_task_self(), port, portSet); +} + +CF_INLINE void __CFPortSetFree(__CFPortSet portSet) { + kern_return_t ret; + mach_port_name_array_t array; + mach_msg_type_number_t idx, number; + + ret = mach_port_get_set_status(mach_task_self(), portSet, &array, &number); + if (KERN_SUCCESS == ret) { + for (idx = 0; idx < number; idx++) { + mach_port_extract_member(mach_task_self(), array[idx], portSet); + } + vm_deallocate(mach_task_self(), (vm_address_t)array, number * sizeof(mach_port_name_t)); + } + mach_port_destroy(mach_task_self(), portSet); +} + +#elif DEPLOYMENT_TARGET_WINDOWS + +typedef HANDLE __CFPort; +#define CFPORT_NULL NULL + +// A simple dynamic array of HANDLEs, which grows to a high-water mark +typedef struct ___CFPortSet { + uint16_t used; + uint16_t size; + HANDLE *handles; + CFLock_t lock; // insert and remove must be thread safe, like the Mach calls +} *__CFPortSet; + +CF_INLINE __CFPort __CFPortAllocate(void) { + return CreateEventA(NULL, true, false, NULL); +} + +CF_INLINE void __CFPortFree(__CFPort port) { + CloseHandle(port); +} + +static __CFPortSet __CFPortSetAllocate(void) { + __CFPortSet result = (__CFPortSet)CFAllocatorAllocate(kCFAllocatorSystemDefault, sizeof(struct ___CFPortSet), 0); + result->used = 0; + result->size = 4; + result->handles = (HANDLE *)CFAllocatorAllocate(kCFAllocatorSystemDefault, result->size * sizeof(HANDLE), 0); + CF_SPINLOCK_INIT_FOR_STRUCTS(result->lock); + return result; +} + +static void __CFPortSetFree(__CFPortSet portSet) { + CFAllocatorDeallocate(kCFAllocatorSystemDefault, portSet->handles); + CFAllocatorDeallocate(kCFAllocatorSystemDefault, portSet); +} + +// Returns portBuf if ports fit in that space, else returns another ptr that must be freed +static __CFPort *__CFPortSetGetPorts(__CFPortSet portSet, __CFPort *portBuf, uint32_t bufSize, uint32_t *portsUsed) { + __CFLock(&(portSet->lock)); + __CFPort *result = portBuf; + if (bufSize < portSet->used) + result = (__CFPort *)CFAllocatorAllocate(kCFAllocatorSystemDefault, portSet->used * sizeof(HANDLE), 0); + if (portSet->used > 1) { + // rotate the ports to vaguely simulate round-robin behaviour + uint16_t lastPort = portSet->used - 1; + HANDLE swapHandle = portSet->handles[0]; + memmove(portSet->handles, &portSet->handles[1], lastPort * sizeof(HANDLE)); + portSet->handles[lastPort] = swapHandle; + } + memmove(result, portSet->handles, portSet->used * sizeof(HANDLE)); + *portsUsed = portSet->used; + __CFUnlock(&(portSet->lock)); + return result; +} + +static kern_return_t __CFPortSetInsert(__CFPort port, __CFPortSet portSet) { + if (NULL == port) { + return -1; + } + __CFLock(&(portSet->lock)); + if (portSet->used >= portSet->size) { + portSet->size += 4; + portSet->handles = (HANDLE *)CFAllocatorReallocate(kCFAllocatorSystemDefault, portSet->handles, portSet->size * sizeof(HANDLE), 0); + } + if (portSet->used >= MAXIMUM_WAIT_OBJECTS) { + CFLog(kCFLogLevelWarning, CFSTR("*** More than MAXIMUM_WAIT_OBJECTS (%d) ports add to a port set. The last ones will be ignored."), MAXIMUM_WAIT_OBJECTS); + } + portSet->handles[portSet->used++] = port; + __CFUnlock(&(portSet->lock)); + return KERN_SUCCESS; +} + +static kern_return_t __CFPortSetRemove(__CFPort port, __CFPortSet portSet) { + int i, j; + if (NULL == port) { + return -1; + } + __CFLock(&(portSet->lock)); + for (i = 0; i < portSet->used; i++) { + if (portSet->handles[i] == port) { + for (j = i+1; j < portSet->used; j++) { + portSet->handles[j-1] = portSet->handles[j]; + } + portSet->used--; + __CFUnlock(&(portSet->lock)); + return true; + } + } + __CFUnlock(&(portSet->lock)); + return KERN_SUCCESS; +} + +#endif + +#if !defined(__MACTYPES__) && !defined(_OS_OSTYPES_H) +#if defined(__BIG_ENDIAN__) +typedef struct UnsignedWide { + UInt32 hi; + UInt32 lo; +} UnsignedWide; +#elif defined(__LITTLE_ENDIAN__) +typedef struct UnsignedWide { + UInt32 lo; + UInt32 hi; +} UnsignedWide; +#endif +typedef UnsignedWide AbsoluteTime; +#endif + +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + +#if USE_DISPATCH_SOURCE_FOR_TIMERS +#endif +#if USE_MK_TIMER_TOO +extern mach_port_name_t mk_timer_create(void); +extern kern_return_t mk_timer_destroy(mach_port_name_t name); +extern kern_return_t mk_timer_arm(mach_port_name_t name, AbsoluteTime expire_time); +extern kern_return_t mk_timer_cancel(mach_port_name_t name, AbsoluteTime *result_time); + +CF_INLINE AbsoluteTime __CFUInt64ToAbsoluteTime(uint64_t x) { + AbsoluteTime a; + a.hi = x >> 32; + a.lo = x & (uint64_t)0xFFFFFFFF; + return a; +} +#endif + +static uint32_t __CFSendTrivialMachMessage(mach_port_t port, uint32_t msg_id, CFOptionFlags options, uint32_t timeout) { + kern_return_t result; + mach_msg_header_t header; + header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); + header.msgh_size = sizeof(mach_msg_header_t); + header.msgh_remote_port = port; + header.msgh_local_port = MACH_PORT_NULL; + header.msgh_id = msg_id; + result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL); + if (result == MACH_SEND_TIMED_OUT) mach_msg_destroy(&header); + return result; +} +#elif DEPLOYMENT_TARGET_WINDOWS + +static HANDLE mk_timer_create(void) { + return CreateWaitableTimer(NULL, FALSE, NULL); +} + +static kern_return_t mk_timer_destroy(HANDLE name) { + BOOL res = CloseHandle(name); + if (!res) { + DWORD err = GetLastError(); + CFLog(kCFLogLevelError, CFSTR("CFRunLoop: Unable to destroy timer: %d"), err); + } + return (int)res; +} + +static kern_return_t mk_timer_arm(HANDLE name, LARGE_INTEGER expire_time) { + BOOL res = SetWaitableTimer(name, &expire_time, 0, NULL, NULL, FALSE); + if (!res) { + DWORD err = GetLastError(); + CFLog(kCFLogLevelError, CFSTR("CFRunLoop: Unable to set timer: %d"), err); + } + return (int)res; +} + +static kern_return_t mk_timer_cancel(HANDLE name, LARGE_INTEGER *result_time) { + BOOL res = CancelWaitableTimer(name); + if (!res) { + DWORD err = GetLastError(); + CFLog(kCFLogLevelError, CFSTR("CFRunLoop: Unable to cancel timer: %d"), err); + } + return (int)res; +} + +// The name of this function is a lie on Windows. The return value matches the argument of the fake mk_timer functions above. Note that the Windows timers expect to be given "system time". We have to do some calculations to get the right value, which is a FILETIME-like value. +CF_INLINE LARGE_INTEGER __CFUInt64ToAbsoluteTime(uint64_t desiredFireTime) { + LARGE_INTEGER result; + // There is a race we know about here, (timer fire time calculated -> thread suspended -> timer armed == late timer fire), but we don't have a way to avoid it at this time, since the only way to specify an absolute value to the timer is to calculate the relative time first. Fixing that would probably require not using the TSR for timers on Windows. + uint64_t now = mach_absolute_time(); + if (now > desiredFireTime) { + result.QuadPart = 0; + } else { + uint64_t timeDiff = desiredFireTime - now; + CFTimeInterval amountOfTimeToWait = __CFTSRToTimeInterval(timeDiff); + // Result is in 100 ns (10**-7 sec) units to be consistent with a FILETIME. + // CFTimeInterval is in seconds. + result.QuadPart = -(amountOfTimeToWait * 10000000); + } + return result; +} + +#endif + +#pragma mark - +#pragma mark Modes + +/* unlock a run loop and modes before doing callouts/sleeping */ +/* never try to take the run loop lock with a mode locked */ +/* be very careful of common subexpression elimination and compacting code, particular across locks and unlocks! */ +/* run loop mode structures should never be deallocated, even if they become empty */ + +static CFTypeID __kCFRunLoopModeTypeID = _kCFRuntimeNotATypeID; +static CFTypeID __kCFRunLoopTypeID = _kCFRuntimeNotATypeID; +static CFTypeID __kCFRunLoopSourceTypeID = _kCFRuntimeNotATypeID; +static CFTypeID __kCFRunLoopObserverTypeID = _kCFRuntimeNotATypeID; +static CFTypeID __kCFRunLoopTimerTypeID = _kCFRuntimeNotATypeID; + +typedef struct __CFRunLoopMode *CFRunLoopModeRef; + +struct __CFRunLoopMode { + CFRuntimeBase _base; + pthread_mutex_t _lock; /* must have the run loop locked before locking this */ + CFStringRef _name; + Boolean _stopped; + char _padding[3]; + CFMutableSetRef _sources0; + CFMutableSetRef _sources1; + CFMutableArrayRef _observers; + CFMutableArrayRef _timers; + CFMutableDictionaryRef _portToV1SourceMap; + __CFPortSet _portSet; + CFIndex _observerMask; +#if USE_DISPATCH_SOURCE_FOR_TIMERS + dispatch_source_t _timerSource; + dispatch_queue_t _queue; + Boolean _timerFired; // set to true by the source when a timer has fired + Boolean _dispatchTimerArmed; +#endif +#if USE_MK_TIMER_TOO + mach_port_t _timerPort; + Boolean _mkTimerArmed; +#endif +#if DEPLOYMENT_TARGET_WINDOWS + DWORD _msgQMask; + void (*_msgPump)(void); +#endif + uint64_t _timerSoftDeadline; /* TSR */ + uint64_t _timerHardDeadline; /* TSR */ +}; + +CF_INLINE void __CFRunLoopModeLock(CFRunLoopModeRef rlm) { + pthread_mutex_lock(&(rlm->_lock)); + //CFLog(6, CFSTR("__CFRunLoopModeLock locked %p"), rlm); +} + +CF_INLINE void __CFRunLoopModeUnlock(CFRunLoopModeRef rlm) { + //CFLog(6, CFSTR("__CFRunLoopModeLock unlocking %p"), rlm); + pthread_mutex_unlock(&(rlm->_lock)); +} + +static Boolean __CFRunLoopModeEqual(CFTypeRef cf1, CFTypeRef cf2) { + CFRunLoopModeRef rlm1 = (CFRunLoopModeRef)cf1; + CFRunLoopModeRef rlm2 = (CFRunLoopModeRef)cf2; + return CFEqual(rlm1->_name, rlm2->_name); +} + +static CFHashCode __CFRunLoopModeHash(CFTypeRef cf) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)cf; + return CFHash(rlm->_name); +} + +static CFStringRef __CFRunLoopModeCopyDescription(CFTypeRef cf) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)cf; + CFMutableStringRef result; + result = CFStringCreateMutable(kCFAllocatorSystemDefault, 0); + CFStringAppendFormat(result, NULL, CFSTR("{name = %@, "), rlm, CFGetAllocator(rlm), rlm->_name); + CFStringAppendFormat(result, NULL, CFSTR("port set = 0x%x, "), rlm->_portSet); +#if USE_DISPATCH_SOURCE_FOR_TIMERS + CFStringAppendFormat(result, NULL, CFSTR("queue = %p, "), rlm->_queue); + CFStringAppendFormat(result, NULL, CFSTR("source = %p (%s), "), rlm->_timerSource, rlm->_timerFired ? "fired" : "not fired"); +#endif +#if USE_MK_TIMER_TOO + CFStringAppendFormat(result, NULL, CFSTR("timer port = 0x%x, "), rlm->_timerPort); +#endif +#if DEPLOYMENT_TARGET_WINDOWS + CFStringAppendFormat(result, NULL, CFSTR("MSGQ mask = %p, "), rlm->_msgQMask); +#endif + CFStringAppendFormat(result, NULL, CFSTR("\n\tsources0 = %@,\n\tsources1 = %@,\n\tobservers = %@,\n\ttimers = %@,\n\tcurrently %0.09g (%lld) / soft deadline in: %0.09g sec (@ %lld) / hard deadline in: %0.09g sec (@ %lld)\n},\n"), rlm->_sources0, rlm->_sources1, rlm->_observers, rlm->_timers, CFAbsoluteTimeGetCurrent(), mach_absolute_time(), __CFTSRToTimeInterval(rlm->_timerSoftDeadline - mach_absolute_time()), rlm->_timerSoftDeadline, __CFTSRToTimeInterval(rlm->_timerHardDeadline - mach_absolute_time()), rlm->_timerHardDeadline); + return result; +} + +static void __CFRunLoopModeDeallocate(CFTypeRef cf) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)cf; + if (NULL != rlm->_sources0) CFRelease(rlm->_sources0); + if (NULL != rlm->_sources1) CFRelease(rlm->_sources1); + if (NULL != rlm->_observers) CFRelease(rlm->_observers); + if (NULL != rlm->_timers) CFRelease(rlm->_timers); + if (NULL != rlm->_portToV1SourceMap) CFRelease(rlm->_portToV1SourceMap); + CFRelease(rlm->_name); + __CFPortSetFree(rlm->_portSet); +#if USE_DISPATCH_SOURCE_FOR_TIMERS + if (rlm->_timerSource) { + dispatch_source_cancel(rlm->_timerSource); + dispatch_release(rlm->_timerSource); + } + if (rlm->_queue) { + dispatch_release(rlm->_queue); + } +#endif +#if USE_MK_TIMER_TOO + if (MACH_PORT_NULL != rlm->_timerPort) mk_timer_destroy(rlm->_timerPort); +#endif + pthread_mutex_destroy(&rlm->_lock); + memset((char *)cf + sizeof(CFRuntimeBase), 0x7C, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase)); +} + +#pragma mark - +#pragma mark Run Loops + +struct _block_item { + struct _block_item *_next; + CFTypeRef _mode; // CFString or CFSet + void (^_block)(void); +}; + +typedef struct _per_run_data { + uint32_t a; + uint32_t b; + uint32_t stopped; + uint32_t ignoreWakeUps; +} _per_run_data; + +struct __CFRunLoop { + CFRuntimeBase _base; + pthread_mutex_t _lock; /* locked for accessing mode list */ + __CFPort _wakeUpPort; // used for CFRunLoopWakeUp + Boolean _unused; + volatile _per_run_data *_perRunData; // reset for runs of the run loop + pthread_t _pthread; + uint32_t _winthread; + CFMutableSetRef _commonModes; + CFMutableSetRef _commonModeItems; + CFRunLoopModeRef _currentMode; + CFMutableSetRef _modes; + struct _block_item *_blocks_head; + struct _block_item *_blocks_tail; + CFAbsoluteTime _runTime; + CFAbsoluteTime _sleepTime; + CFTypeRef _counterpart; +}; + +/* Bit 0 of the base reserved bits is used for stopped state */ +/* Bit 1 of the base reserved bits is used for sleeping state */ +/* Bit 2 of the base reserved bits is used for deallocating state */ + +CF_INLINE volatile _per_run_data *__CFRunLoopPushPerRunData(CFRunLoopRef rl) { + volatile _per_run_data *previous = rl->_perRunData; + rl->_perRunData = (volatile _per_run_data *)CFAllocatorAllocate(kCFAllocatorSystemDefault, sizeof(_per_run_data), 0); + rl->_perRunData->a = 0x4346524C; + rl->_perRunData->b = 0x4346524C; // 'CFRL' + rl->_perRunData->stopped = 0x00000000; + rl->_perRunData->ignoreWakeUps = 0x00000000; + return previous; +} + +CF_INLINE void __CFRunLoopPopPerRunData(CFRunLoopRef rl, volatile _per_run_data *previous) { + if (rl->_perRunData) CFAllocatorDeallocate(kCFAllocatorSystemDefault, (void *)rl->_perRunData); + rl->_perRunData = previous; +} + +CF_INLINE Boolean __CFRunLoopIsStopped(CFRunLoopRef rl) { + return (rl->_perRunData->stopped) ? true : false; +} + +CF_INLINE void __CFRunLoopSetStopped(CFRunLoopRef rl) { + rl->_perRunData->stopped = 0x53544F50; // 'STOP' +} + +CF_INLINE void __CFRunLoopUnsetStopped(CFRunLoopRef rl) { + rl->_perRunData->stopped = 0x0; +} + +CF_INLINE Boolean __CFRunLoopIsIgnoringWakeUps(CFRunLoopRef rl) { + return (rl->_perRunData->ignoreWakeUps) ? true : false; +} + +CF_INLINE void __CFRunLoopSetIgnoreWakeUps(CFRunLoopRef rl) { + rl->_perRunData->ignoreWakeUps = 0x57414B45; // 'WAKE' +} + +CF_INLINE void __CFRunLoopUnsetIgnoreWakeUps(CFRunLoopRef rl) { + rl->_perRunData->ignoreWakeUps = 0x0; +} + +CF_INLINE Boolean __CFRunLoopIsSleeping(CFRunLoopRef rl) { + return (Boolean)__CFBitfieldGetValue(((const CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 1, 1); +} + +CF_INLINE void __CFRunLoopSetSleeping(CFRunLoopRef rl) { + __CFBitfieldSetValue(((CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 1, 1, 1); +} + +CF_INLINE void __CFRunLoopUnsetSleeping(CFRunLoopRef rl) { + __CFBitfieldSetValue(((CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 1, 1, 0); +} + +CF_INLINE Boolean __CFRunLoopIsDeallocating(CFRunLoopRef rl) { + return (Boolean)__CFBitfieldGetValue(((const CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 2, 2); +} + +CF_INLINE void __CFRunLoopSetDeallocating(CFRunLoopRef rl) { + __CFBitfieldSetValue(((CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 2, 2, 1); +} + +CF_INLINE void __CFRunLoopLock(CFRunLoopRef rl) { + pthread_mutex_lock(&(((CFRunLoopRef)rl)->_lock)); + // CFLog(6, CFSTR("__CFRunLoopLock locked %p"), rl); +} + +CF_INLINE void __CFRunLoopUnlock(CFRunLoopRef rl) { + // CFLog(6, CFSTR("__CFRunLoopLock unlocking %p"), rl); + pthread_mutex_unlock(&(((CFRunLoopRef)rl)->_lock)); +} + +static CFStringRef __CFRunLoopCopyDescription(CFTypeRef cf) { + CFRunLoopRef rl = (CFRunLoopRef)cf; + CFMutableStringRef result; + result = CFStringCreateMutable(kCFAllocatorSystemDefault, 0); +#if DEPLOYMENT_TARGET_WINDOWS + CFStringAppendFormat(result, NULL, CFSTR("{wakeup port = 0x%x, stopped = %s, ignoreWakeUps = %s, \ncurrent mode = %@,\n"), cf, CFGetAllocator(cf), rl->_wakeUpPort, __CFRunLoopIsStopped(rl) ? "true" : "false", __CFRunLoopIsIgnoringWakeUps(rl) ? "true" : "false", rl->_currentMode ? rl->_currentMode->_name : CFSTR("(none)")); +#else + CFStringAppendFormat(result, NULL, CFSTR("{wakeup port = 0x%x, stopped = %s, ignoreWakeUps = %s, \ncurrent mode = %@,\n"), cf, CFGetAllocator(cf), rl->_wakeUpPort, __CFRunLoopIsStopped(rl) ? "true" : "false", __CFRunLoopIsIgnoringWakeUps(rl) ? "true" : "false", rl->_currentMode ? rl->_currentMode->_name : CFSTR("(none)")); +#endif + CFStringAppendFormat(result, NULL, CFSTR("common modes = %@,\ncommon mode items = %@,\nmodes = %@}\n"), rl->_commonModes, rl->_commonModeItems, rl->_modes); + return result; +} + +CF_PRIVATE void __CFRunLoopDump() { // __private_extern__ to keep the compiler from discarding it + CFShow(CFCopyDescription(CFRunLoopGetCurrent())); +} + +CF_INLINE void __CFRunLoopLockInit(pthread_mutex_t *lock) { + pthread_mutexattr_t mattr; + pthread_mutexattr_init(&mattr); + pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE); + int32_t mret = pthread_mutex_init(lock, &mattr); + pthread_mutexattr_destroy(&mattr); + if (0 != mret) { + } +} + +/* call with rl locked, returns mode locked */ +static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + struct __CFRunLoopMode srlm; + memset(&srlm, 0, sizeof(srlm)); + _CFRuntimeSetInstanceTypeIDAndIsa(&srlm, __kCFRunLoopModeTypeID); + srlm._name = modeName; + rlm = (CFRunLoopModeRef)CFSetGetValue(rl->_modes, &srlm); + if (NULL != rlm) { + __CFRunLoopModeLock(rlm); + return rlm; + } + if (!create) { + return NULL; + } + rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL); + if (NULL == rlm) { + return NULL; + } + __CFRunLoopLockInit(&rlm->_lock); + rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName); + rlm->_stopped = false; + rlm->_portToV1SourceMap = NULL; + rlm->_sources0 = NULL; + rlm->_sources1 = NULL; + rlm->_observers = NULL; + rlm->_timers = NULL; + rlm->_observerMask = 0; + rlm->_portSet = __CFPortSetAllocate(); + rlm->_timerSoftDeadline = UINT64_MAX; + rlm->_timerHardDeadline = UINT64_MAX; + + kern_return_t ret = KERN_SUCCESS; +#if USE_DISPATCH_SOURCE_FOR_TIMERS + rlm->_timerFired = false; + rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0); + mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue); + if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1); + rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue); + + __block Boolean *timerFiredPointer = &(rlm->_timerFired); + dispatch_source_set_event_handler(rlm->_timerSource, ^{ + *timerFiredPointer = true; + }); + + // Set timer to far out there. The unique leeway makes this timer easy to spot in debug output. + _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321); + dispatch_resume(rlm->_timerSource); + + ret = __CFPortSetInsert(queuePort, rlm->_portSet); + if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret); + +#endif +#if USE_MK_TIMER_TOO + rlm->_timerPort = mk_timer_create(); + ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet); + if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret); +#endif + + ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet); + if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret); + +#if DEPLOYMENT_TARGET_WINDOWS + rlm->_msgQMask = 0; + rlm->_msgPump = NULL; +#endif + CFSetAddValue(rl->_modes, rlm); + CFRelease(rlm); + __CFRunLoopModeLock(rlm); /* return mode locked */ + return rlm; +} + + +// expects rl and rlm locked +static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { + CHECK_FOR_FORK(); + if (NULL == rlm) return true; +#if DEPLOYMENT_TARGET_WINDOWS + if (0 != rlm->_msgQMask) return false; +#endif + Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); + if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue + // 判断时候有没有_sources0 + if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; + // 判断时候有没有_sources1 + + if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; + // 判断时候有没有_timers + if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; + + struct _block_item *item = rl->_blocks_head; + while (item) { + struct _block_item *curr = item; + item = item->_next; + Boolean doit = false; + if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { + doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } else { + doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } + if (doit) return false; + } + return true; +} + +#if DEPLOYMENT_TARGET_WINDOWS + +uint32_t _CFRunLoopGetWindowsMessageQueueMask(CFRunLoopRef rl, CFStringRef modeName) { + if (modeName == kCFRunLoopCommonModes) { + CFLog(kCFLogLevelError, CFSTR("_CFRunLoopGetWindowsMessageQueueMask: kCFRunLoopCommonModes unsupported")); + HALT; + } + DWORD result = 0; + __CFRunLoopLock(rl); + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false); + if (rlm) { + result = rlm->_msgQMask; + __CFRunLoopModeUnlock(rlm); + } + __CFRunLoopUnlock(rl); + return (uint32_t)result; +} + +void _CFRunLoopSetWindowsMessageQueueMask(CFRunLoopRef rl, uint32_t mask, CFStringRef modeName) { + if (modeName == kCFRunLoopCommonModes) { + CFLog(kCFLogLevelError, CFSTR("_CFRunLoopSetWindowsMessageQueueMask: kCFRunLoopCommonModes unsupported")); + HALT; + } + __CFRunLoopLock(rl); + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true); + rlm->_msgQMask = (DWORD)mask; + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); +} + +uint32_t _CFRunLoopGetWindowsThreadID(CFRunLoopRef rl) { + return rl->_winthread; +} + +CFWindowsMessageQueueHandler _CFRunLoopGetWindowsMessageQueueHandler(CFRunLoopRef rl, CFStringRef modeName) { + if (modeName == kCFRunLoopCommonModes) { + CFLog(kCFLogLevelError, CFSTR("_CFRunLoopGetWindowsMessageQueueMask: kCFRunLoopCommonModes unsupported")); + HALT; + } + if (rl != CFRunLoopGetCurrent()) { + CFLog(kCFLogLevelError, CFSTR("_CFRunLoopGetWindowsMessageQueueHandler: run loop parameter must be the current run loop")); + HALT; + } + void (*result)(void) = NULL; + __CFRunLoopLock(rl); + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false); + if (rlm) { + result = rlm->_msgPump; + __CFRunLoopModeUnlock(rlm); + } + __CFRunLoopUnlock(rl); + return result; +} + +void _CFRunLoopSetWindowsMessageQueueHandler(CFRunLoopRef rl, CFStringRef modeName, CFWindowsMessageQueueHandler func) { + if (modeName == kCFRunLoopCommonModes) { + CFLog(kCFLogLevelError, CFSTR("_CFRunLoopGetWindowsMessageQueueMask: kCFRunLoopCommonModes unsupported")); + HALT; + } + if (rl != CFRunLoopGetCurrent()) { + CFLog(kCFLogLevelError, CFSTR("_CFRunLoopGetWindowsMessageQueueHandler: run loop parameter must be the current run loop")); + HALT; + } + __CFRunLoopLock(rl); + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true); + rlm->_msgPump = func; + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); +} + +#endif + +#pragma mark - +#pragma mark Sources + +/* Bit 3 in the base reserved bits is used for invalid state in run loop objects */ + +CF_INLINE Boolean __CFIsValid(const void *cf) { + return (Boolean)__CFBitfieldGetValue(((const CFRuntimeBase *)cf)->_cfinfo[CF_INFO_BITS], 3, 3); +} + +CF_INLINE void __CFSetValid(void *cf) { + __CFBitfieldSetValue(((CFRuntimeBase *)cf)->_cfinfo[CF_INFO_BITS], 3, 3, 1); +} + +CF_INLINE void __CFUnsetValid(void *cf) { + __CFBitfieldSetValue(((CFRuntimeBase *)cf)->_cfinfo[CF_INFO_BITS], 3, 3, 0); +} + +struct __CFRunLoopSource { + CFRuntimeBase _base; + uint32_t _bits; + pthread_mutex_t _lock; + CFIndex _order; /* immutable */ + CFMutableBagRef _runLoops; + union { + CFRunLoopSourceContext version0; /* immutable, except invalidation */ + CFRunLoopSourceContext1 version1; /* immutable, except invalidation */ + } _context; +}; + +/* Bit 1 of the base reserved bits is used for signalled state */ + +CF_INLINE Boolean __CFRunLoopSourceIsSignaled(CFRunLoopSourceRef rls) { + return (Boolean)__CFBitfieldGetValue(rls->_bits, 1, 1); +} + +CF_INLINE void __CFRunLoopSourceSetSignaled(CFRunLoopSourceRef rls) { + __CFBitfieldSetValue(rls->_bits, 1, 1, 1); +} + +CF_INLINE void __CFRunLoopSourceUnsetSignaled(CFRunLoopSourceRef rls) { + __CFBitfieldSetValue(rls->_bits, 1, 1, 0); +} + +CF_INLINE void __CFRunLoopSourceLock(CFRunLoopSourceRef rls) { + pthread_mutex_lock(&(rls->_lock)); +// CFLog(6, CFSTR("__CFRunLoopSourceLock locked %p"), rls); +} + +CF_INLINE void __CFRunLoopSourceUnlock(CFRunLoopSourceRef rls) { +// CFLog(6, CFSTR("__CFRunLoopSourceLock unlocking %p"), rls); + pthread_mutex_unlock(&(rls->_lock)); +} + +#pragma mark Observers + +struct __CFRunLoopObserver { + CFRuntimeBase _base; + pthread_mutex_t _lock; + CFRunLoopRef _runLoop; + CFIndex _rlCount; + CFOptionFlags _activities; /* immutable */ + CFIndex _order; /* immutable */ + CFRunLoopObserverCallBack _callout; /* immutable */ + CFRunLoopObserverContext _context; /* immutable, except invalidation */ +}; + +/* Bit 0 of the base reserved bits is used for firing state */ +/* Bit 1 of the base reserved bits is used for repeats state */ + +CF_INLINE Boolean __CFRunLoopObserverIsFiring(CFRunLoopObserverRef rlo) { + return (Boolean)__CFBitfieldGetValue(((const CFRuntimeBase *)rlo)->_cfinfo[CF_INFO_BITS], 0, 0); +} + +CF_INLINE void __CFRunLoopObserverSetFiring(CFRunLoopObserverRef rlo) { + __CFBitfieldSetValue(((CFRuntimeBase *)rlo)->_cfinfo[CF_INFO_BITS], 0, 0, 1); +} + +CF_INLINE void __CFRunLoopObserverUnsetFiring(CFRunLoopObserverRef rlo) { + __CFBitfieldSetValue(((CFRuntimeBase *)rlo)->_cfinfo[CF_INFO_BITS], 0, 0, 0); +} + +CF_INLINE Boolean __CFRunLoopObserverRepeats(CFRunLoopObserverRef rlo) { + return (Boolean)__CFBitfieldGetValue(((const CFRuntimeBase *)rlo)->_cfinfo[CF_INFO_BITS], 1, 1); +} + +CF_INLINE void __CFRunLoopObserverSetRepeats(CFRunLoopObserverRef rlo) { + __CFBitfieldSetValue(((CFRuntimeBase *)rlo)->_cfinfo[CF_INFO_BITS], 1, 1, 1); +} + +CF_INLINE void __CFRunLoopObserverUnsetRepeats(CFRunLoopObserverRef rlo) { + __CFBitfieldSetValue(((CFRuntimeBase *)rlo)->_cfinfo[CF_INFO_BITS], 1, 1, 0); +} + +CF_INLINE void __CFRunLoopObserverLock(CFRunLoopObserverRef rlo) { + pthread_mutex_lock(&(rlo->_lock)); +// CFLog(6, CFSTR("__CFRunLoopObserverLock locked %p"), rlo); +} + +CF_INLINE void __CFRunLoopObserverUnlock(CFRunLoopObserverRef rlo) { +// CFLog(6, CFSTR("__CFRunLoopObserverLock unlocking %p"), rlo); + pthread_mutex_unlock(&(rlo->_lock)); +} + +static void __CFRunLoopObserverSchedule(CFRunLoopObserverRef rlo, CFRunLoopRef rl, CFRunLoopModeRef rlm) { + __CFRunLoopObserverLock(rlo); + if (0 == rlo->_rlCount) { + rlo->_runLoop = rl; + } + rlo->_rlCount++; + __CFRunLoopObserverUnlock(rlo); +} + +static void __CFRunLoopObserverCancel(CFRunLoopObserverRef rlo, CFRunLoopRef rl, CFRunLoopModeRef rlm) { + __CFRunLoopObserverLock(rlo); + rlo->_rlCount--; + if (0 == rlo->_rlCount) { + rlo->_runLoop = NULL; + } + __CFRunLoopObserverUnlock(rlo); +} + +#pragma mark Timers + +struct __CFRunLoopTimer { + CFRuntimeBase _base; + uint16_t _bits; + pthread_mutex_t _lock; + CFRunLoopRef _runLoop; + CFMutableSetRef _rlModes; + CFAbsoluteTime _nextFireDate; + CFTimeInterval _interval; /* immutable */ + CFTimeInterval _tolerance; /* mutable */ + uint64_t _fireTSR; /* TSR units */ + CFIndex _order; /* immutable */ + CFRunLoopTimerCallBack _callout; /* immutable */ + CFRunLoopTimerContext _context; /* immutable, except invalidation */ +}; + +/* Bit 0 of the base reserved bits is used for firing state */ +/* Bit 1 of the base reserved bits is used for fired-during-callout state */ +/* Bit 2 of the base reserved bits is used for waking state */ + +CF_INLINE Boolean __CFRunLoopTimerIsFiring(CFRunLoopTimerRef rlt) { + return (Boolean)__CFBitfieldGetValue(rlt->_bits, 0, 0); +} + +CF_INLINE void __CFRunLoopTimerSetFiring(CFRunLoopTimerRef rlt) { + __CFBitfieldSetValue(rlt->_bits, 0, 0, 1); +} + +CF_INLINE void __CFRunLoopTimerUnsetFiring(CFRunLoopTimerRef rlt) { + __CFBitfieldSetValue(rlt->_bits, 0, 0, 0); +} + +CF_INLINE Boolean __CFRunLoopTimerIsDeallocating(CFRunLoopTimerRef rlt) { + return (Boolean)__CFBitfieldGetValue(rlt->_bits, 2, 2); +} + +CF_INLINE void __CFRunLoopTimerSetDeallocating(CFRunLoopTimerRef rlt) { + __CFBitfieldSetValue(rlt->_bits, 2, 2, 1); +} + +CF_INLINE void __CFRunLoopTimerLock(CFRunLoopTimerRef rlt) { + pthread_mutex_lock(&(rlt->_lock)); +// CFLog(6, CFSTR("__CFRunLoopTimerLock locked %p"), rlt); +} + +CF_INLINE void __CFRunLoopTimerUnlock(CFRunLoopTimerRef rlt) { +// CFLog(6, CFSTR("__CFRunLoopTimerLock unlocking %p"), rlt); + pthread_mutex_unlock(&(rlt->_lock)); +} + +static CFLock_t __CFRLTFireTSRLock = CFLockInit; + +CF_INLINE void __CFRunLoopTimerFireTSRLock(void) { + __CFLock(&__CFRLTFireTSRLock); +} + +CF_INLINE void __CFRunLoopTimerFireTSRUnlock(void) { + __CFUnlock(&__CFRLTFireTSRLock); +} + +#pragma mark - + +/* CFRunLoop */ + +CONST_STRING_DECL(kCFRunLoopDefaultMode, "kCFRunLoopDefaultMode") +CONST_STRING_DECL(kCFRunLoopCommonModes, "kCFRunLoopCommonModes") + +// call with rl and rlm locked +static CFRunLoopSourceRef __CFRunLoopModeFindSourceForMachPort(CFRunLoopRef rl, CFRunLoopModeRef rlm, __CFPort port) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + CFRunLoopSourceRef found = rlm->_portToV1SourceMap ? (CFRunLoopSourceRef)CFDictionaryGetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)port) : NULL; + return found; +} + +// Remove backreferences the mode's sources have to the rl (context); +// the primary purpose of rls->_runLoops is so that Invalidation can remove +// the source from the run loops it is in, but during deallocation of a +// run loop, we already know that the sources are going to be punted +// from it, so invalidation of sources does not need to remove from a +// deallocating run loop. +static void __CFRunLoopCleanseSources(const void *value, void *context) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)value; + CFRunLoopRef rl = (CFRunLoopRef)context; + CFIndex idx, cnt; + const void **list, *buffer[256]; + if (NULL == rlm->_sources0 && NULL == rlm->_sources1) return; + + cnt = (rlm->_sources0 ? CFSetGetCount(rlm->_sources0) : 0) + (rlm->_sources1 ? CFSetGetCount(rlm->_sources1) : 0); + list = (const void **)((cnt <= 256) ? buffer : CFAllocatorAllocate(kCFAllocatorSystemDefault, cnt * sizeof(void *), 0)); + if (rlm->_sources0) CFSetGetValues(rlm->_sources0, list); + if (rlm->_sources1) CFSetGetValues(rlm->_sources1, list + (rlm->_sources0 ? CFSetGetCount(rlm->_sources0) : 0)); + for (idx = 0; idx < cnt; idx++) { + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)list[idx]; + __CFRunLoopSourceLock(rls); + if (NULL != rls->_runLoops) { + CFBagRemoveValue(rls->_runLoops, rl); + } + __CFRunLoopSourceUnlock(rls); + } + if (list != buffer) CFAllocatorDeallocate(kCFAllocatorSystemDefault, list); +} + +static void __CFRunLoopDeallocateSources(const void *value, void *context) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)value; + CFRunLoopRef rl = (CFRunLoopRef)context; + CFIndex idx, cnt; + const void **list, *buffer[256]; + if (NULL == rlm->_sources0 && NULL == rlm->_sources1) return; + + cnt = (rlm->_sources0 ? CFSetGetCount(rlm->_sources0) : 0) + (rlm->_sources1 ? CFSetGetCount(rlm->_sources1) : 0); + list = (const void **)((cnt <= 256) ? buffer : CFAllocatorAllocate(kCFAllocatorSystemDefault, cnt * sizeof(void *), 0)); + if (rlm->_sources0) CFSetGetValues(rlm->_sources0, list); + if (rlm->_sources1) CFSetGetValues(rlm->_sources1, list + (rlm->_sources0 ? CFSetGetCount(rlm->_sources0) : 0)); + for (idx = 0; idx < cnt; idx++) { + CFRetain(list[idx]); + } + if (rlm->_sources0) CFSetRemoveAllValues(rlm->_sources0); + if (rlm->_sources1) CFSetRemoveAllValues(rlm->_sources1); + for (idx = 0; idx < cnt; idx++) { + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)list[idx]; + __CFRunLoopSourceLock(rls); + if (NULL != rls->_runLoops) { + CFBagRemoveValue(rls->_runLoops, rl); + } + __CFRunLoopSourceUnlock(rls); + if (0 == rls->_context.version0.version) { + if (NULL != rls->_context.version0.cancel) { + rls->_context.version0.cancel(rls->_context.version0.info, rl, rlm->_name); /* CALLOUT */ + } + } else if (1 == rls->_context.version0.version) { + __CFPort port = rls->_context.version1.getPort(rls->_context.version1.info); /* CALLOUT */ + if (CFPORT_NULL != port) { + __CFPortSetRemove(port, rlm->_portSet); + } + } + CFRelease(rls); + } + if (list != buffer) CFAllocatorDeallocate(kCFAllocatorSystemDefault, list); +} + +static void __CFRunLoopDeallocateObservers(const void *value, void *context) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)value; + CFRunLoopRef rl = (CFRunLoopRef)context; + CFIndex idx, cnt; + const void **list, *buffer[256]; + if (NULL == rlm->_observers) return; + cnt = CFArrayGetCount(rlm->_observers); + list = (const void **)((cnt <= 256) ? buffer : CFAllocatorAllocate(kCFAllocatorSystemDefault, cnt * sizeof(void *), 0)); + CFArrayGetValues(rlm->_observers, CFRangeMake(0, cnt), list); + for (idx = 0; idx < cnt; idx++) { + CFRetain(list[idx]); + } + CFArrayRemoveAllValues(rlm->_observers); + for (idx = 0; idx < cnt; idx++) { + __CFRunLoopObserverCancel((CFRunLoopObserverRef)list[idx], rl, rlm); + CFRelease(list[idx]); + } + if (list != buffer) CFAllocatorDeallocate(kCFAllocatorSystemDefault, list); +} + +static void __CFRunLoopDeallocateTimers(const void *value, void *context) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)value; + if (NULL == rlm->_timers) return; + void (^deallocateTimers)(CFMutableArrayRef timers) = ^(CFMutableArrayRef timers) { + CFIndex idx, cnt; + const void **list, *buffer[256]; + cnt = CFArrayGetCount(timers); + list = (const void **)((cnt <= 256) ? buffer : CFAllocatorAllocate(kCFAllocatorSystemDefault, cnt * sizeof(void *), 0)); + CFArrayGetValues(timers, CFRangeMake(0, CFArrayGetCount(timers)), list); + for (idx = 0; idx < cnt; idx++) { + CFRetain(list[idx]); + } + CFArrayRemoveAllValues(timers); + for (idx = 0; idx < cnt; idx++) { + CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)list[idx]; + __CFRunLoopTimerLock(rlt); + // if the run loop is deallocating, and since a timer can only be in one + // run loop, we're going to be removing the timer from all modes, so be + // a little heavy-handed and direct + CFSetRemoveAllValues(rlt->_rlModes); + rlt->_runLoop = NULL; + __CFRunLoopTimerUnlock(rlt); + CFRelease(list[idx]); + } + if (list != buffer) CFAllocatorDeallocate(kCFAllocatorSystemDefault, list); + }; + + if (rlm->_timers && CFArrayGetCount(rlm->_timers)) deallocateTimers(rlm->_timers); +} + +CF_EXPORT CFRunLoopRef _CFRunLoopGet0b(pthread_t t); + +static void __CFRunLoopDeallocate(CFTypeRef cf) { + CFRunLoopRef rl = (CFRunLoopRef)cf; + + if (_CFRunLoopGet0b(pthread_main_thread_np()) == cf) HALT; + + /* We try to keep the run loop in a valid state as long as possible, + since sources may have non-retained references to the run loop. + Another reason is that we don't want to lock the run loop for + callback reasons, if we can get away without that. We start by + eliminating the sources, since they are the most likely to call + back into the run loop during their "cancellation". Common mode + items will be removed from the mode indirectly by the following + three lines. */ + __CFRunLoopSetDeallocating(rl); + if (NULL != rl->_modes) { + CFSetApplyFunction(rl->_modes, (__CFRunLoopCleanseSources), rl); // remove references to rl + CFSetApplyFunction(rl->_modes, (__CFRunLoopDeallocateSources), rl); + CFSetApplyFunction(rl->_modes, (__CFRunLoopDeallocateObservers), rl); + CFSetApplyFunction(rl->_modes, (__CFRunLoopDeallocateTimers), rl); + } + __CFRunLoopLock(rl); + struct _block_item *item = rl->_blocks_head; + while (item) { + struct _block_item *curr = item; + item = item->_next; + CFRelease(curr->_mode); + Block_release(curr->_block); + free(curr); + } + if (NULL != rl->_commonModeItems) { + CFRelease(rl->_commonModeItems); + } + if (NULL != rl->_commonModes) { + CFRelease(rl->_commonModes); + } + if (NULL != rl->_modes) { + CFRelease(rl->_modes); + } + __CFPortFree(rl->_wakeUpPort); + rl->_wakeUpPort = CFPORT_NULL; + __CFRunLoopPopPerRunData(rl, NULL); + __CFRunLoopUnlock(rl); + pthread_mutex_destroy(&rl->_lock); + memset((char *)cf + sizeof(CFRuntimeBase), 0x8C, sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase)); +} + +static const CFRuntimeClass __CFRunLoopModeClass = { + 0, + "CFRunLoopMode", + NULL, // init + NULL, // copy + __CFRunLoopModeDeallocate, + __CFRunLoopModeEqual, + __CFRunLoopModeHash, + NULL, // + __CFRunLoopModeCopyDescription +}; + +static const CFRuntimeClass __CFRunLoopClass = { + 0, + "CFRunLoop", + NULL, // init + NULL, // copy + __CFRunLoopDeallocate, + NULL, + NULL, + NULL, // + __CFRunLoopCopyDescription +}; + +CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data); + +CFTypeID CFRunLoopGetTypeID(void) { + static dispatch_once_t initOnce; + dispatch_once(&initOnce, ^{ __kCFRunLoopTypeID = _CFRuntimeRegisterClass(&__CFRunLoopClass); __kCFRunLoopModeTypeID = _CFRuntimeRegisterClass(&__CFRunLoopModeClass); }); + return __kCFRunLoopTypeID; +} + +static CFRunLoopRef __CFRunLoopCreate(pthread_t t) { + CFRunLoopRef loop = NULL; + CFRunLoopModeRef rlm; + uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase); + loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL); + if (NULL == loop) { + return NULL; + } + (void)__CFRunLoopPushPerRunData(loop); + __CFRunLoopLockInit(&loop->_lock); + loop->_wakeUpPort = __CFPortAllocate(); + if (CFPORT_NULL == loop->_wakeUpPort) HALT; + __CFRunLoopSetIgnoreWakeUps(loop); + loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode); + loop->_commonModeItems = NULL; + loop->_currentMode = NULL; + loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + loop->_blocks_head = NULL; + loop->_blocks_tail = NULL; + loop->_counterpart = NULL; + loop->_pthread = t; +#if DEPLOYMENT_TARGET_WINDOWS + loop->_winthread = GetCurrentThreadId(); +#else + loop->_winthread = 0; +#endif + rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true); + if (NULL != rlm) __CFRunLoopModeUnlock(rlm); + return loop; +} + +static CFMutableDictionaryRef __CFRunLoops = NULL; +static CFLock_t loopsLock = CFLockInit; + +// should only be called by Foundation +// t==0 is a synonym for "main thread" that always works +CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { + if (pthread_equal(t, kNilPthreadT)) { + t = pthread_main_thread_np(); + } + __CFLock(&loopsLock); + if (!__CFRunLoops) { + __CFUnlock(&loopsLock); + // 创建字典 + CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); + // 创建主线程 + CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); + // 保存主线程 + CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); + + if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { + CFRelease(dict); + } + CFRelease(mainLoop); + __CFLock(&loopsLock); + } + // 从字典中获取子线程的runloop + CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); + __CFUnlock(&loopsLock); + if (!loop) { + // 如果子线程的runloop不存在,那么就为该线程创建一个对应的runloop + CFRunLoopRef newLoop = __CFRunLoopCreate(t); + __CFLock(&loopsLock); + loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); + // 把当前子线程和对应的runloop保存到字典中 + if (!loop) { + CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); + loop = newLoop; + } + // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it + __CFUnlock(&loopsLock); + CFRelease(newLoop); + } + if (pthread_equal(t, pthread_self())) { + _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); + if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { + _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); + } + } + return loop; +} + +// should only be called by Foundation +CFRunLoopRef _CFRunLoopGet0b(pthread_t t) { + if (pthread_equal(t, kNilPthreadT)) { + t = pthread_main_thread_np(); + } + __CFLock(&loopsLock); + CFRunLoopRef loop = NULL; + if (__CFRunLoops) { + loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); + } + __CFUnlock(&loopsLock); + return loop; +} + +static void __CFRunLoopRemoveAllSources(CFRunLoopRef rl, CFStringRef modeName); + +// Called for each thread as it exits +CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) { + CFRunLoopRef rl = NULL; + if (data <= 1) { + __CFLock(&loopsLock); + if (__CFRunLoops) { + rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self())); + if (rl) CFRetain(rl); + CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self())); + } + __CFUnlock(&loopsLock); + } else { + _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop); + } + if (rl && CFRunLoopGetMain() != rl) { // protect against cooperative threads + if (NULL != rl->_counterpart) { + CFRelease(rl->_counterpart); + rl->_counterpart = NULL; + } + // purge all sources before deallocation + CFArrayRef array = CFRunLoopCopyAllModes(rl); + for (CFIndex idx = CFArrayGetCount(array); idx--;) { + CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx); + __CFRunLoopRemoveAllSources(rl, modeName); + } + __CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes); + CFRelease(array); + } + if (rl) CFRelease(rl); +} + +pthread_t _CFRunLoopGet1(CFRunLoopRef rl) { + return rl->_pthread; +} + +// should only be called by Foundation +CF_EXPORT CFTypeRef _CFRunLoopGet2(CFRunLoopRef rl) { + CFTypeRef ret = NULL; + __CFLock(&loopsLock); + ret = rl->_counterpart; + __CFUnlock(&loopsLock); + return ret; +} + +// should only be called by Foundation +CF_EXPORT CFTypeRef _CFRunLoopGet2b(CFRunLoopRef rl) { + return rl->_counterpart; +} + +#if DEPLOYMENT_TARGET_MACOSX +void _CFRunLoopSetCurrent(CFRunLoopRef rl) { + if (pthread_main_np()) return; + CFRunLoopRef currentLoop = CFRunLoopGetCurrent(); + if (rl != currentLoop) { + CFRetain(currentLoop); // avoid a deallocation of the currentLoop inside the lock + __CFLock(&loopsLock); + if (rl) { + CFDictionarySetValue(__CFRunLoops, pthreadPointer(pthread_self()), rl); + } else { + CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self())); + } + __CFUnlock(&loopsLock); + CFRelease(currentLoop); + _CFSetTSD(__CFTSDKeyRunLoop, NULL, NULL); + _CFSetTSD(__CFTSDKeyRunLoopCntr, 0, (void (*)(void *))__CFFinalizeRunLoop); + } +} +#endif + +CFRunLoopRef CFRunLoopGetMain(void) { + CHECK_FOR_FORK(); + static CFRunLoopRef __main = NULL; // no retain needed + if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed + return __main; +} + +CFRunLoopRef CFRunLoopGetCurrent(void) { + CHECK_FOR_FORK(); + CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); + if (rl) return rl; + return _CFRunLoopGet0(pthread_self()); +} + +CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl) { + CHECK_FOR_FORK(); + CFStringRef result = NULL; + __CFRunLoopLock(rl); + if (NULL != rl->_currentMode) { + result = (CFStringRef)CFRetain(rl->_currentMode->_name); + } + __CFRunLoopUnlock(rl); + return result; +} + +static void __CFRunLoopGetModeName(const void *value, void *context) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)value; + CFMutableArrayRef array = (CFMutableArrayRef)context; + CFArrayAppendValue(array, rlm->_name); +} + +CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl) { + CHECK_FOR_FORK(); + CFMutableArrayRef array; + __CFRunLoopLock(rl); + array = CFArrayCreateMutable(kCFAllocatorSystemDefault, CFSetGetCount(rl->_modes), &kCFTypeArrayCallBacks); + CFSetApplyFunction(rl->_modes, (__CFRunLoopGetModeName), array); + __CFRunLoopUnlock(rl); + return array; +} + +static void __CFRunLoopAddItemsToCommonMode(const void *value, void *ctx) { + CFTypeRef item = (CFTypeRef)value; + CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]); + CFStringRef modeName = (CFStringRef)(((CFTypeRef *)ctx)[1]); + if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) { + CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName); + } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) { + CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName); + } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) { + CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName); + } +} + +static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) { + CFStringRef modeName = (CFStringRef)value; + CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]); + CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]); + if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) { + CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName); + } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) { + CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName); + } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) { + CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName); + } +} + +static void __CFRunLoopRemoveItemFromCommonModes(const void *value, void *ctx) { + CFStringRef modeName = (CFStringRef)value; + CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]); + CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]); + if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) { + CFRunLoopRemoveSource(rl, (CFRunLoopSourceRef)item, modeName); + } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) { + CFRunLoopRemoveObserver(rl, (CFRunLoopObserverRef)item, modeName); + } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) { + CFRunLoopRemoveTimer(rl, (CFRunLoopTimerRef)item, modeName); + } +} + +CF_EXPORT Boolean _CFRunLoop01(CFRunLoopRef rl, CFStringRef modeName) { + __CFRunLoopLock(rl); + Boolean present = CFSetContainsValue(rl->_commonModes, modeName); + __CFRunLoopUnlock(rl); + return present; +} + +void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) { + CHECK_FOR_FORK(); + if (__CFRunLoopIsDeallocating(rl)) return; + __CFRunLoopLock(rl); + if (!CFSetContainsValue(rl->_commonModes, modeName)) { + CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL; + CFSetAddValue(rl->_commonModes, modeName); + if (NULL != set) { + CFTypeRef context[2] = {rl, modeName}; + /* add all common-modes items to new mode */ + CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context); + CFRelease(set); + } + } else { + } + __CFRunLoopUnlock(rl); +} + + +static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() __attribute__((noinline)); +static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg) { + _dispatch_main_queue_callback_4CF(msg); + asm __volatile__(""); // thwart tail-call optimization +} + +static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline)); +static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { + if (func) { + func(observer, activity, info); + } + asm __volatile__(""); // thwart tail-call optimization +} + +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline)); +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) { + if (func) { + func(timer, info); + } + asm __volatile__(""); // thwart tail-call optimization +} + +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline)); +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) { + if (block) { + block(); + } + asm __volatile__(""); // thwart tail-call optimization +} + +static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { // Call with rl and rlm locked + if (!rl->_blocks_head) return false; + if (!rlm || !rlm->_name) return false; + Boolean did = false; + struct _block_item *head = rl->_blocks_head; + struct _block_item *tail = rl->_blocks_tail; + rl->_blocks_head = NULL; + rl->_blocks_tail = NULL; + CFSetRef commonModes = rl->_commonModes; + CFStringRef curMode = rlm->_name; + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + struct _block_item *prev = NULL; + struct _block_item *item = head; + while (item) { + struct _block_item *curr = item; + item = item->_next; + Boolean doit = false; + if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { + doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)); + } else { + doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)); + } + if (!doit) prev = curr; + if (doit) { + if (prev) prev->_next = item; + if (curr == head) head = item; + if (curr == tail) tail = prev; + void (^block)(void) = curr->_block; + CFRelease(curr->_mode); + free(curr); + if (doit) { + __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); + did = true; + } + Block_release(block); // do this before relocking to prevent deadlocks where some yahoo wants to run the run loop reentrantly from their dealloc + } + } + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + if (head) { + tail->_next = rl->_blocks_head; + rl->_blocks_head = head; + if (!rl->_blocks_tail) rl->_blocks_tail = tail; + } + return did; +} + +/* rl is locked, rlm is locked on entrance and exit */ +static void __CFRunLoopDoObservers() __attribute__((noinline)); +static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + + CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0; + if (cnt < 1) return; + + /* Fire the observers */ + STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1); + CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef)); + CFIndex obs_cnt = 0; + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx); + if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) { + collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); + } + } + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + for (CFIndex idx = 0; idx < obs_cnt; idx++) { + CFRunLoopObserverRef rlo = collectedObservers[idx]; + __CFRunLoopObserverLock(rlo); + if (__CFIsValid(rlo)) { + Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo); + __CFRunLoopObserverSetFiring(rlo); + __CFRunLoopObserverUnlock(rlo); + __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info); + if (doInvalidate) { + CFRunLoopObserverInvalidate(rlo); + } + __CFRunLoopObserverUnsetFiring(rlo); + } else { + __CFRunLoopObserverUnlock(rlo); + } + CFRelease(rlo); + } + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + + if (collectedObservers != buffer) free(collectedObservers); +} + +static CFComparisonResult __CFRunLoopSourceComparator(const void *val1, const void *val2, void *context) { + CFRunLoopSourceRef o1 = (CFRunLoopSourceRef)val1; + CFRunLoopSourceRef o2 = (CFRunLoopSourceRef)val2; + if (o1->_order < o2->_order) return kCFCompareLessThan; + if (o2->_order < o1->_order) return kCFCompareGreaterThan; + return kCFCompareEqualTo; +} + +static void __CFRunLoopCollectSources0(const void *value, void *context) { + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)value; + CFTypeRef *sources = (CFTypeRef *)context; + if (0 == rls->_context.version0.version && __CFIsValid(rls) && __CFRunLoopSourceIsSignaled(rls)) { + if (NULL == *sources) { + *sources = CFRetain(rls); + } else if (CFGetTypeID(*sources) == CFRunLoopSourceGetTypeID()) { + CFTypeRef oldrls = *sources; + *sources = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks); + CFArrayAppendValue((CFMutableArrayRef)*sources, oldrls); + CFArrayAppendValue((CFMutableArrayRef)*sources, rls); + CFRelease(oldrls); + } else { + CFArrayAppendValue((CFMutableArrayRef)*sources, rls); + } + } +} + +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline)); +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) { + if (perform) { + perform(info); + } + asm __volatile__(""); // thwart tail-call optimization +} + +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __attribute__((noinline)); +static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__( +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info), + mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply, +#else + void (*perform)(void *), +#endif + void *info) { + if (perform) { +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + *reply = perform(msg, size, kCFAllocatorSystemDefault, info); +#else + perform(info); +#endif + } + asm __volatile__(""); // thwart tail-call optimization +} + +/* rl is locked, rlm is locked on entrance and exit */ +static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) __attribute__((noinline)); +static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + CFTypeRef sources = NULL; + Boolean sourceHandled = false; + + /* Fire the version 0 sources */ + if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) { + CFSetApplyFunction(rlm->_sources0, (__CFRunLoopCollectSources0), &sources); + } + if (NULL != sources) { + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + // sources is either a single (retained) CFRunLoopSourceRef or an array of (retained) CFRunLoopSourceRef + if (CFGetTypeID(sources) == CFRunLoopSourceGetTypeID()) { + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)sources; + __CFRunLoopSourceLock(rls); + if (__CFRunLoopSourceIsSignaled(rls)) { + __CFRunLoopSourceUnsetSignaled(rls); + if (__CFIsValid(rls)) { + __CFRunLoopSourceUnlock(rls); + __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); + CHECK_FOR_FORK(); + sourceHandled = true; + } else { + __CFRunLoopSourceUnlock(rls); + } + } else { + __CFRunLoopSourceUnlock(rls); + } + } else { + CFIndex cnt = CFArrayGetCount((CFArrayRef)sources); + CFArraySortValues((CFMutableArrayRef)sources, CFRangeMake(0, cnt), (__CFRunLoopSourceComparator), NULL); + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)CFArrayGetValueAtIndex((CFArrayRef)sources, idx); + __CFRunLoopSourceLock(rls); + if (__CFRunLoopSourceIsSignaled(rls)) { + __CFRunLoopSourceUnsetSignaled(rls); + if (__CFIsValid(rls)) { + __CFRunLoopSourceUnlock(rls); + __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); + CHECK_FOR_FORK(); + sourceHandled = true; + } else { + __CFRunLoopSourceUnlock(rls); + } + } else { + __CFRunLoopSourceUnlock(rls); + } + if (stopAfterHandle && sourceHandled) { + break; + } + } + } + CFRelease(sources); + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + } + return sourceHandled; +} + +CF_INLINE void __CFRunLoopDebugInfoForRunLoopSource(CFRunLoopSourceRef rls) { +} + +// msg, size and reply are unused on Windows +static Boolean __CFRunLoopDoSource1() __attribute__((noinline)); +static Boolean __CFRunLoopDoSource1(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopSourceRef rls +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + , mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply +#endif + ) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + Boolean sourceHandled = false; + + /* Fire a version 1 source */ + CFRetain(rls); + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + __CFRunLoopSourceLock(rls); + if (__CFIsValid(rls)) { + __CFRunLoopSourceUnsetSignaled(rls); + __CFRunLoopSourceUnlock(rls); + __CFRunLoopDebugInfoForRunLoopSource(rls); + __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(rls->_context.version1.perform, +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + msg, size, reply, +#endif + rls->_context.version1.info); + CHECK_FOR_FORK(); + sourceHandled = true; + } else { + if (_LogCFRunLoop) { CFLog(kCFLogLevelDebug, CFSTR("%p (%s) __CFRunLoopDoSource1 rls %p is invalid"), CFRunLoopGetCurrent(), *_CFGetProgname(), rls); } + __CFRunLoopSourceUnlock(rls); + } + CFRelease(rls); + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + return sourceHandled; +} + +static CFIndex __CFRunLoopInsertionIndexInTimerArray(CFArrayRef array, CFRunLoopTimerRef rlt) __attribute__((noinline)); +static CFIndex __CFRunLoopInsertionIndexInTimerArray(CFArrayRef array, CFRunLoopTimerRef rlt) { + CFIndex cnt = CFArrayGetCount(array); + if (cnt <= 0) { + return 0; + } + if (256 < cnt) { + CFRunLoopTimerRef item = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(array, cnt - 1); + if (item->_fireTSR <= rlt->_fireTSR) { + return cnt; + } + item = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(array, 0); + if (rlt->_fireTSR < item->_fireTSR) { + return 0; + } + } + + CFIndex add = (1 << flsl(cnt)) * 2; + CFIndex idx = 0; + Boolean lastTestLEQ; + do { + add = add / 2; + lastTestLEQ = false; + CFIndex testIdx = idx + add; + if (testIdx < cnt) { + CFRunLoopTimerRef item = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(array, testIdx); + if (item->_fireTSR <= rlt->_fireTSR) { + idx = testIdx; + lastTestLEQ = true; + } + } + } while (0 < add); + + return lastTestLEQ ? idx + 1 : idx; +} + +static void __CFArmNextTimerInMode(CFRunLoopModeRef rlm, CFRunLoopRef rl) { + uint64_t nextHardDeadline = UINT64_MAX; + uint64_t nextSoftDeadline = UINT64_MAX; + + if (rlm->_timers) { + // Look at the list of timers. We will calculate two TSR values; the next soft and next hard deadline. + // The next soft deadline is the first time we can fire any timer. This is the fire date of the first timer in our sorted list of timers. + // The next hard deadline is the last time at which we can fire the timer before we've moved out of the allowable tolerance of the timers in our list. + for (CFIndex idx = 0, cnt = CFArrayGetCount(rlm->_timers); idx < cnt; idx++) { + CFRunLoopTimerRef t = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers , idx); + // discount timers currently firing + if (__CFRunLoopTimerIsFiring(t)) continue; + + int32_t err = CHECKINT_NO_ERROR; + uint64_t oneTimerSoftDeadline = t->_fireTSR; + uint64_t oneTimerHardDeadline = check_uint64_add(t->_fireTSR, __CFTimeIntervalToTSR(t->_tolerance), &err); + if (err != CHECKINT_NO_ERROR) oneTimerHardDeadline = UINT64_MAX; + + // We can stop searching if the soft deadline for this timer exceeds the current hard deadline. Otherwise, later timers with lower tolerance could still have earlier hard deadlines. + if (oneTimerSoftDeadline > nextHardDeadline) { + break; + } + + if (oneTimerSoftDeadline < nextSoftDeadline) { + nextSoftDeadline = oneTimerSoftDeadline; + } + + if (oneTimerHardDeadline < nextHardDeadline) { + nextHardDeadline = oneTimerHardDeadline; + } + } + + if (nextSoftDeadline < UINT64_MAX && (nextHardDeadline != rlm->_timerHardDeadline || nextSoftDeadline != rlm->_timerSoftDeadline)) { + if (CFRUNLOOP_NEXT_TIMER_ARMED_ENABLED()) { + CFRUNLOOP_NEXT_TIMER_ARMED((unsigned long)(nextSoftDeadline - mach_absolute_time())); + } +#if USE_DISPATCH_SOURCE_FOR_TIMERS + // We're going to hand off the range of allowable timer fire date to dispatch and let it fire when appropriate for the system. + uint64_t leeway = __CFTSRToNanoseconds(nextHardDeadline - nextSoftDeadline); + dispatch_time_t deadline = __CFTSRToDispatchTime(nextSoftDeadline); +#if USE_MK_TIMER_TOO + if (leeway > 0) { + // Only use the dispatch timer if we have any leeway + // + + // Cancel the mk timer + if (rlm->_mkTimerArmed && rlm->_timerPort) { + AbsoluteTime dummy; + mk_timer_cancel(rlm->_timerPort, &dummy); + rlm->_mkTimerArmed = false; + } + + // Arm the dispatch timer + _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, deadline, DISPATCH_TIME_FOREVER, leeway); + rlm->_dispatchTimerArmed = true; + } else { + // Cancel the dispatch timer + if (rlm->_dispatchTimerArmed) { + // Cancel the dispatch timer + _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 888); + rlm->_dispatchTimerArmed = false; + } + + // Arm the mk timer + if (rlm->_timerPort) { + mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline)); + rlm->_mkTimerArmed = true; + } + } +#else + _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, deadline, DISPATCH_TIME_FOREVER, leeway); +#endif +#else + if (rlm->_timerPort) { + mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline)); + } +#endif + } else if (nextSoftDeadline == UINT64_MAX) { + // Disarm the timers - there is no timer scheduled + + if (rlm->_mkTimerArmed && rlm->_timerPort) { + AbsoluteTime dummy; + mk_timer_cancel(rlm->_timerPort, &dummy); + rlm->_mkTimerArmed = false; + } + +#if USE_DISPATCH_SOURCE_FOR_TIMERS + if (rlm->_dispatchTimerArmed) { + _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 333); + rlm->_dispatchTimerArmed = false; + } +#endif + } + } + rlm->_timerHardDeadline = nextHardDeadline; + rlm->_timerSoftDeadline = nextSoftDeadline; +} + +// call with rlm and its run loop locked, and the TSRLock locked; rlt not locked; returns with same state +static void __CFRepositionTimerInMode(CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt, Boolean isInArray) __attribute__((noinline)); +static void __CFRepositionTimerInMode(CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt, Boolean isInArray) { + if (!rlt) return; + + CFMutableArrayRef timerArray = rlm->_timers; + if (!timerArray) return; + Boolean found = false; + + // If we know in advance that the timer is not in the array (just being added now) then we can skip this search + if (isInArray) { + CFIndex idx = CFArrayGetFirstIndexOfValue(timerArray, CFRangeMake(0, CFArrayGetCount(timerArray)), rlt); + if (kCFNotFound != idx) { + CFRetain(rlt); + CFArrayRemoveValueAtIndex(timerArray, idx); + found = true; + } + } + if (!found && isInArray) return; + CFIndex newIdx = __CFRunLoopInsertionIndexInTimerArray(timerArray, rlt); + CFArrayInsertValueAtIndex(timerArray, newIdx, rlt); + __CFArmNextTimerInMode(rlm, rlt->_runLoop); + if (isInArray) CFRelease(rlt); +} + + +// mode and rl are locked on entry and exit +static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) { /* DOES CALLOUT */ + Boolean timerHandled = false; + uint64_t oldFireTSR = 0; + + /* Fire a timer */ + CFRetain(rlt); + __CFRunLoopTimerLock(rlt); + + if (__CFIsValid(rlt) && rlt->_fireTSR <= mach_absolute_time() && !__CFRunLoopTimerIsFiring(rlt) && rlt->_runLoop == rl) { + void *context_info = NULL; + void (*context_release)(const void *) = NULL; + if (rlt->_context.retain) { + context_info = (void *)rlt->_context.retain(rlt->_context.info); + context_release = rlt->_context.release; + } else { + context_info = rlt->_context.info; + } + Boolean doInvalidate = (0.0 == rlt->_interval); + __CFRunLoopTimerSetFiring(rlt); + // Just in case the next timer has exactly the same deadlines as this one, we reset these values so that the arm next timer code can correctly find the next timer in the list and arm the underlying timer. + rlm->_timerSoftDeadline = UINT64_MAX; + rlm->_timerHardDeadline = UINT64_MAX; + __CFRunLoopTimerUnlock(rlt); + __CFRunLoopTimerFireTSRLock(); + oldFireTSR = rlt->_fireTSR; + __CFRunLoopTimerFireTSRUnlock(); + + __CFArmNextTimerInMode(rlm, rl); + + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info); + CHECK_FOR_FORK(); + if (doInvalidate) { + CFRunLoopTimerInvalidate(rlt); /* DOES CALLOUT */ + } + if (context_release) { + context_release(context_info); + } + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + __CFRunLoopTimerLock(rlt); + timerHandled = true; + __CFRunLoopTimerUnsetFiring(rlt); + } + if (__CFIsValid(rlt) && timerHandled) { + /* This is just a little bit tricky: we want to support calling + * CFRunLoopTimerSetNextFireDate() from within the callout and + * honor that new time here if it is a later date, otherwise + * it is completely ignored. */ + if (oldFireTSR < rlt->_fireTSR) { + /* Next fire TSR was set, and set to a date after the previous + * fire date, so we honor it. */ + __CFRunLoopTimerUnlock(rlt); + // The timer was adjusted and repositioned, during the + // callout, but if it was still the min timer, it was + // skipped because it was firing. Need to redo the + // min timer calculation in case rlt should now be that + // timer instead of whatever was chosen. + __CFArmNextTimerInMode(rlm, rl); + } else { + uint64_t nextFireTSR = 0LL; + uint64_t intervalTSR = 0LL; + if (rlt->_interval <= 0.0) { + } else if (TIMER_INTERVAL_LIMIT < rlt->_interval) { + intervalTSR = __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT); + } else { + intervalTSR = __CFTimeIntervalToTSR(rlt->_interval); + } + if (LLONG_MAX - intervalTSR <= oldFireTSR) { + nextFireTSR = LLONG_MAX; + } else { + if (intervalTSR == 0) { + // 15304159: Make sure we don't accidentally loop forever here + CRSetCrashLogMessage("A CFRunLoopTimer with an interval of 0 is set to repeat"); + HALT; + } + uint64_t currentTSR = mach_absolute_time(); + nextFireTSR = oldFireTSR; + while (nextFireTSR <= currentTSR) { + nextFireTSR += intervalTSR; + } + } + CFRunLoopRef rlt_rl = rlt->_runLoop; + if (rlt_rl) { + CFRetain(rlt_rl); + CFIndex cnt = CFSetGetCount(rlt->_rlModes); + STACK_BUFFER_DECL(CFTypeRef, modes, cnt); + CFSetGetValues(rlt->_rlModes, (const void **)modes); + // To avoid A->B, B->A lock ordering issues when coming up + // towards the run loop from a source, the timer has to be + // unlocked, which means we have to protect from object + // invalidation, although that's somewhat expensive. + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRetain(modes[idx]); + } + __CFRunLoopTimerUnlock(rlt); + for (CFIndex idx = 0; idx < cnt; idx++) { + CFStringRef name = (CFStringRef)modes[idx]; + modes[idx] = (CFTypeRef)__CFRunLoopFindMode(rlt_rl, name, false); + CFRelease(name); + } + __CFRunLoopTimerFireTSRLock(); + rlt->_fireTSR = nextFireTSR; + rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR); + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)modes[idx]; + if (rlm) { + __CFRepositionTimerInMode(rlm, rlt, true); + } + } + __CFRunLoopTimerFireTSRUnlock(); + for (CFIndex idx = 0; idx < cnt; idx++) { + __CFRunLoopModeUnlock((CFRunLoopModeRef)modes[idx]); + } + CFRelease(rlt_rl); + } else { + __CFRunLoopTimerUnlock(rlt); + __CFRunLoopTimerFireTSRLock(); + rlt->_fireTSR = nextFireTSR; + rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR); + __CFRunLoopTimerFireTSRUnlock(); + } + } + } else { + __CFRunLoopTimerUnlock(rlt); + } + CFRelease(rlt); + return timerHandled; +} + + +// rl and rlm are locked on entry and exit +static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) { /* DOES CALLOUT */ + Boolean timerHandled = false; + CFMutableArrayRef timers = NULL; + for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) { + CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx); + + if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) { + if (rlt->_fireTSR <= limitTSR) { + if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks); + CFArrayAppendValue(timers, rlt); + } + } + } + + for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) { + CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx); + Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt); + timerHandled = timerHandled || did; + } + if (timers) CFRelease(timers); + return timerHandled; +} + + +CF_EXPORT Boolean _CFRunLoopFinished(CFRunLoopRef rl, CFStringRef modeName) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + Boolean result = false; + __CFRunLoopLock(rl); + rlm = __CFRunLoopFindMode(rl, modeName, false); + if (NULL == rlm || __CFRunLoopModeIsEmpty(rl, rlm, NULL)) { + result = true; + } + if (rlm) __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + return result; +} + +// 进入RunLoop内部实现 +static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) __attribute__((noinline)); + +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + +#define TIMEOUT_INFINITY (~(mach_msg_timeout_t)0) + +static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) { + Boolean originalBuffer = true; + kern_return_t ret = KERN_SUCCESS; + for (;;) { /* In that sleep of death what nightmares may come ... */ + mach_msg_header_t *msg = (mach_msg_header_t *)*buffer; + msg->msgh_bits = 0; + msg->msgh_local_port = port; + msg->msgh_remote_port = MACH_PORT_NULL; + msg->msgh_size = buffer_size; + msg->msgh_id = 0; + if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); } + ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL); + + // Take care of all voucher-related work right after mach_msg. + // If we don't release the previous voucher we're going to leak it. + voucher_mach_msg_revert(*voucherState); + + // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one. + *voucherState = voucher_mach_msg_adopt(msg); + + if (voucherCopy) { + if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) { + // Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy. + // CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged. + *voucherCopy = voucher_copy(); + } else { + *voucherCopy = NULL; + } + } + + CFRUNLOOP_WAKEUP(ret); + if (MACH_MSG_SUCCESS == ret) { + *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL; + return true; + } + if (MACH_RCV_TIMED_OUT == ret) { + if (!originalBuffer) free(msg); + *buffer = NULL; + *livePort = MACH_PORT_NULL; + return false; + } + if (MACH_RCV_TOO_LARGE != ret) break; + buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE); + if (originalBuffer) *buffer = NULL; + originalBuffer = false; + *buffer = realloc(*buffer, buffer_size); + } + HALT; + return false; +} + +#elif DEPLOYMENT_TARGET_WINDOWS + +#define TIMEOUT_INFINITY INFINITE + +// pass in either a portSet or onePort +static Boolean __CFRunLoopWaitForMultipleObjects(__CFPortSet portSet, HANDLE *onePort, DWORD timeout, DWORD mask, HANDLE *livePort, Boolean *msgReceived) { + DWORD waitResult = WAIT_TIMEOUT; + HANDLE handleBuf[MAXIMUM_WAIT_OBJECTS]; + HANDLE *handles = NULL; + uint32_t handleCount = 0; + Boolean freeHandles = false; + Boolean result = false; + + if (portSet) { + // copy out the handles to be safe from other threads at work + handles = __CFPortSetGetPorts(portSet, handleBuf, MAXIMUM_WAIT_OBJECTS, &handleCount); + freeHandles = (handles != handleBuf); + } else { + handles = onePort; + handleCount = 1; + freeHandles = FALSE; + } + + // The run loop mode and loop are already in proper unlocked state from caller + waitResult = MsgWaitForMultipleObjectsEx(__CFMin(handleCount, MAXIMUM_WAIT_OBJECTS), handles, timeout, mask, MWMO_INPUTAVAILABLE); + + CFAssert2(waitResult != WAIT_FAILED, __kCFLogAssertion, "%s(): error %d from MsgWaitForMultipleObjects", __PRETTY_FUNCTION__, GetLastError()); + + if (waitResult == WAIT_TIMEOUT) { + // do nothing, just return to caller + result = false; + } else if (waitResult >= WAIT_OBJECT_0 && waitResult < WAIT_OBJECT_0+handleCount) { + // a handle was signaled + if (livePort) *livePort = handles[waitResult-WAIT_OBJECT_0]; + result = true; + } else if (waitResult == WAIT_OBJECT_0+handleCount) { + // windows message received + if (msgReceived) *msgReceived = true; + result = true; + } else if (waitResult >= WAIT_ABANDONED_0 && waitResult < WAIT_ABANDONED_0+handleCount) { + // an "abandoned mutex object" + if (livePort) *livePort = handles[waitResult-WAIT_ABANDONED_0]; + result = true; + } else { + CFAssert2(waitResult == WAIT_FAILED, __kCFLogAssertion, "%s(): unexpected result from MsgWaitForMultipleObjects: %d", __PRETTY_FUNCTION__, waitResult); + result = false; + } + + if (freeHandles) { + CFAllocatorDeallocate(kCFAllocatorSystemDefault, handles); + } + + return result; +} + +#endif + +struct __timeout_context { + dispatch_source_t ds; + CFRunLoopRef rl; + uint64_t termTSR; +}; + +static void __CFRunLoopTimeoutCancel(void *arg) { + struct __timeout_context *context = (struct __timeout_context *)arg; + CFRelease(context->rl); + dispatch_release(context->ds); + free(context); +} + +static void __CFRunLoopTimeout(void *arg) { + struct __timeout_context *context = (struct __timeout_context *)arg; + context->termTSR = 0ULL; + CFRUNLOOP_WAKEUP_FOR_TIMEOUT(); + CFRunLoopWakeUp(context->rl); + // The interval is DISPATCH_TIME_FOREVER, so this won't fire again +} + +/* rl, rlm are locked on entrance and exit */ +static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { + + uint64_t startTSR = mach_absolute_time(); + + if (__CFRunLoopIsStopped(rl)) { + __CFRunLoopUnsetStopped(rl); + return kCFRunLoopRunStopped; + } else if (rlm->_stopped) { + rlm->_stopped = false; + return kCFRunLoopRunStopped; + } + + mach_port_name_t dispatchPort = MACH_PORT_NULL; + Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); + if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); + +#if USE_DISPATCH_SOURCE_FOR_TIMERS + mach_port_name_t modeQueuePort = MACH_PORT_NULL; + if (rlm->_queue) { + modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue); + if (!modeQueuePort) { + CRASH("Unable to get port for run loop mode queue (%d)", -1); + } + } +#endif + + dispatch_source_t timeout_timer = NULL; + struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context)); + if (seconds <= 0.0) { // instant timeout + seconds = 0.0; + timeout_context->termTSR = 0ULL; + } else if (seconds <= TIMER_INTERVAL_LIMIT) { + dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground(); + timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_retain(timeout_timer); + timeout_context->ds = timeout_timer; + timeout_context->rl = (CFRunLoopRef)CFRetain(rl); + timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds); + dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context + dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout); + dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel); + uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL); + dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL); + dispatch_resume(timeout_timer); + } else { + // 设置RunLoop超时时间 + seconds = 9999999999.0; + timeout_context->termTSR = UINT64_MAX; + } + + Boolean didDispatchPortLastTime = true; + int32_t retVal = 0; + do { +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED; + voucher_t voucherCopy = NULL; +#endif + uint8_t msg_buffer[3 * 1024]; +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + mach_msg_header_t *msg = NULL; + mach_port_t livePort = MACH_PORT_NULL; +#elif DEPLOYMENT_TARGET_WINDOWS + HANDLE livePort = NULL; + Boolean windowsMessageReceived = false; +#endif + __CFPortSet waitSet = rlm->_portSet; + + __CFRunLoopUnsetIgnoreWakeUps(rl); + + if (rlm->_observerMask & kCFRunLoopBeforeTimers) + // 2. 通知 Observers: RunLoop 即将触发 Timer 回调 + __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); + if (rlm->_observerMask & kCFRunLoopBeforeSources) + // 3. 通知 Observers: RunLoop 即将触发 Source 回调 + __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); + // 执行被加入的block + __CFRunLoopDoBlocks(rl, rlm); + + // 4. RunLoop 触发 Source0 (非port) 回调 + Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); + if (sourceHandledThisLoop) { + // 执行被加入的block + __CFRunLoopDoBlocks(rl, rlm); + } + + Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); + + // 5. 如果有 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 + } + + didDispatchPortLastTime = false; + + // 通知 Observers: RunLoop 的线程即将进入休眠(sleep) + if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); + __CFRunLoopSetSleeping(rl); + // do not do any user callouts after this point (after notifying of sleeping) + + // Must push the local-to-this-activation ports in on every loop + // iteration, as this mode could be run re-entrantly and we don't + // want these ports to get serviced. + + __CFPortSetInsert(dispatchPort, waitSet); + + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent(); + +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI +#if USE_DISPATCH_SOURCE_FOR_TIMERS + do { + if (kCFUseCollectableAllocator) { + // objc_clear_stack(0); + // + 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); +#else + if (kCFUseCollectableAllocator) { + // objc_clear_stack(0); + // + 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); +#endif + + +#elif DEPLOYMENT_TARGET_WINDOWS + // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages. + __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived); +#endif + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + + rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart)); + + // Must remove the local-to-this-activation ports in on every loop + // iteration, as this mode could be run re-entrantly and we don't + // want these ports to get serviced. Also, we don't want them left + // in there if this function returns. + + __CFPortSetRemove(dispatchPort, waitSet); + + __CFRunLoopSetIgnoreWakeUps(rl); + + // user callouts now OK again + __CFRunLoopUnsetSleeping(rl); + + // 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了 + if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); + // 处理消息 + handle_msg:; + __CFRunLoopSetIgnoreWakeUps(rl); + +#if DEPLOYMENT_TARGET_WINDOWS + if (windowsMessageReceived) { + // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + if (rlm->_msgPump) { + rlm->_msgPump(); + } else { + MSG msg; + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + sourceHandledThisLoop = true; + + // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced + // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later. + // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling. + __CFRunLoopSetSleeping(rl); + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL); + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + __CFRunLoopUnsetSleeping(rl); + // If we have a new live port then it will be handled below as normal + } + + +#endif + if (MACH_PORT_NULL == livePort) { + CFRUNLOOP_WAKEUP_FOR_NOTHING(); + // handle nothing + } else if (livePort == rl->_wakeUpPort) { + CFRUNLOOP_WAKEUP_FOR_WAKEUP(); + // do nothing on Mac OS +#if DEPLOYMENT_TARGET_WINDOWS + // Always reset the wake up port, or risk spinning forever + ResetEvent(rl->_wakeUpPort); +#endif + } +#if USE_DISPATCH_SOURCE_FOR_TIMERS + else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { + CFRUNLOOP_WAKEUP_FOR_TIMER(); + + if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { + // Re-arm the next timer, because we apparently fired early + __CFArmNextTimerInMode(rlm, rl); + } + } +#endif +#if USE_MK_TIMER_TOO + // 9.1 如果一个 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 + // 9.2 如果有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; + } + // 9.3 如果一个 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 + } + + // Restore the previous voucher + _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release); + + } +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); +#endif + // 执行加入到Loop的block + __CFRunLoopDoBlocks(rl, rlm); + + + 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; + } + +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + voucher_mach_msg_revert(voucherState); + os_release(voucherCopy); +#endif + // 如果没超时,mode里没空,loop也没被停止,那继续loop + } while (0 == retVal); + + if (timeout_timer) { + dispatch_source_cancel(timeout_timer); + dispatch_release(timeout_timer); + } else { + free(timeout_context); + } + + return retVal; +} + +// RunLoop的实现 +SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + if (__CFRunLoopIsDeallocating(rl)) + return kCFRunLoopRunFinished; + __CFRunLoopLock(rl); + + // 根据modeName找到对应mode + CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); + + // 判断mode里没有source/timer, 没有直接返回。 + if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { + Boolean did = false; + if (currentMode) + __CFRunLoopModeUnlock(currentMode); + __CFRunLoopUnlock(rl); + return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; + } + + volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); + + CFRunLoopModeRef previousMode = rl->_currentMode; + rl->_currentMode = currentMode; + int32_t result = kCFRunLoopRunFinished; + + if (currentMode->_observerMask & kCFRunLoopEntry ) + // 1. 通知 Observers: RunLoop 即将进入 loop + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); + // 进入loop + result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); + + if (currentMode->_observerMask & kCFRunLoopExit ) + // 10.通知 Observers: RunLoop 即将退出。 + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); + + __CFRunLoopModeUnlock(currentMode); + __CFRunLoopPopPerRunData(rl, previousPerRun); + rl->_currentMode = previousMode; + __CFRunLoopUnlock(rl); + return result; +} + +// 用DefaultMode启动 +void CFRunLoopRun(void) { /* DOES CALLOUT */ + int32_t result; + do { + result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); + CHECK_FOR_FORK(); + } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); +} + +// 用指定的Mode启动,允许设置RunLoop超时时间 +SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); +} + +CFAbsoluteTime CFRunLoopGetNextTimerFireDate(CFRunLoopRef rl, CFStringRef modeName) { + CHECK_FOR_FORK(); + __CFRunLoopLock(rl); + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false); + CFAbsoluteTime at = 0.0; + CFRunLoopTimerRef nextTimer = (rlm && rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) ? (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, 0) : NULL; + if (nextTimer) { + at = CFRunLoopTimerGetNextFireDate(nextTimer); + } + if (rlm) __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + return at; +} + +Boolean CFRunLoopIsWaiting(CFRunLoopRef rl) { + CHECK_FOR_FORK(); + return __CFRunLoopIsSleeping(rl); +} + +void CFRunLoopWakeUp(CFRunLoopRef rl) { + CHECK_FOR_FORK(); + // This lock is crucial to ignorable wakeups, do not remove it. + __CFRunLoopLock(rl); + if (__CFRunLoopIsIgnoringWakeUps(rl)) { + __CFRunLoopUnlock(rl); + return; + } +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + kern_return_t ret; + /* We unconditionally try to send the message, since we don't want + * to lose a wakeup, but the send may fail if there is already a + * wakeup pending, since the queue length is 1. */ + ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0); + if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret); +#elif DEPLOYMENT_TARGET_WINDOWS + SetEvent(rl->_wakeUpPort); +#endif + __CFRunLoopUnlock(rl); +} + +void CFRunLoopStop(CFRunLoopRef rl) { + Boolean doWake = false; + CHECK_FOR_FORK(); + __CFRunLoopLock(rl); + if (rl->_currentMode) { + __CFRunLoopSetStopped(rl); + doWake = true; + } + __CFRunLoopUnlock(rl); + if (doWake) { + CFRunLoopWakeUp(rl); + } +} + +CF_EXPORT void _CFRunLoopStopMode(CFRunLoopRef rl, CFStringRef modeName) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + __CFRunLoopLock(rl); + rlm = __CFRunLoopFindMode(rl, modeName, true); + if (NULL != rlm) { + rlm->_stopped = true; + __CFRunLoopModeUnlock(rlm); + } + __CFRunLoopUnlock(rl); + CFRunLoopWakeUp(rl); +} + +CF_EXPORT Boolean _CFRunLoopModeContainsMode(CFRunLoopRef rl, CFStringRef modeName, CFStringRef candidateContainedName) { + CHECK_FOR_FORK(); + return false; +} + +void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void)) { + CHECK_FOR_FORK(); + if (CFStringGetTypeID() == CFGetTypeID(mode)) { + mode = CFStringCreateCopy(kCFAllocatorSystemDefault, (CFStringRef)mode); + __CFRunLoopLock(rl); + // ensure mode exists + CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, (CFStringRef)mode, true); + if (currentMode) __CFRunLoopModeUnlock(currentMode); + __CFRunLoopUnlock(rl); + } else if (CFArrayGetTypeID() == CFGetTypeID(mode)) { + CFIndex cnt = CFArrayGetCount((CFArrayRef)mode); + const void **values = (const void **)malloc(sizeof(const void *) * cnt); + CFArrayGetValues((CFArrayRef)mode, CFRangeMake(0, cnt), values); + mode = CFSetCreate(kCFAllocatorSystemDefault, values, cnt, &kCFTypeSetCallBacks); + __CFRunLoopLock(rl); + // ensure modes exist + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, (CFStringRef)values[idx], true); + if (currentMode) __CFRunLoopModeUnlock(currentMode); + } + __CFRunLoopUnlock(rl); + free(values); + } else if (CFSetGetTypeID() == CFGetTypeID(mode)) { + CFIndex cnt = CFSetGetCount((CFSetRef)mode); + const void **values = (const void **)malloc(sizeof(const void *) * cnt); + CFSetGetValues((CFSetRef)mode, values); + mode = CFSetCreate(kCFAllocatorSystemDefault, values, cnt, &kCFTypeSetCallBacks); + __CFRunLoopLock(rl); + // ensure modes exist + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, (CFStringRef)values[idx], true); + if (currentMode) __CFRunLoopModeUnlock(currentMode); + } + __CFRunLoopUnlock(rl); + free(values); + } else { + mode = NULL; + } + block = Block_copy(block); + if (!mode || !block) { + if (mode) CFRelease(mode); + if (block) Block_release(block); + return; + } + __CFRunLoopLock(rl); + struct _block_item *new_item = (struct _block_item *)malloc(sizeof(struct _block_item)); + new_item->_next = NULL; + new_item->_mode = mode; + new_item->_block = block; + if (!rl->_blocks_tail) { + rl->_blocks_head = new_item; + } else { + rl->_blocks_tail->_next = new_item; + } + rl->_blocks_tail = new_item; + __CFRunLoopUnlock(rl); +} + +Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + Boolean hasValue = false; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + if (NULL != rl->_commonModeItems) { + hasValue = CFSetContainsValue(rl->_commonModeItems, rls); + } + } else { + rlm = __CFRunLoopFindMode(rl, modeName, false); + if (NULL != rlm) { + hasValue = (rlm->_sources0 ? CFSetContainsValue(rlm->_sources0, rls) : false) || (rlm->_sources1 ? CFSetContainsValue(rlm->_sources1, rls) : false); + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); + return hasValue; +} + +void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + if (__CFRunLoopIsDeallocating(rl)) return; + if (!__CFIsValid(rls)) return; + Boolean doVer0Callout = false; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; + if (NULL == rl->_commonModeItems) { + rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + } + CFSetAddValue(rl->_commonModeItems, rls); + if (NULL != set) { + CFTypeRef context[2] = {rl, rls}; + /* add new item to all common-modes */ + CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context); + CFRelease(set); + } + } else { + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true); + if (NULL != rlm && NULL == rlm->_sources0) { + rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL); + } + if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) { + if (0 == rls->_context.version0.version) { + CFSetAddValue(rlm->_sources0, rls); + } else if (1 == rls->_context.version0.version) { + CFSetAddValue(rlm->_sources1, rls); + __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info); + if (CFPORT_NULL != src_port) { + CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls); + __CFPortSetInsert(src_port, rlm->_portSet); + } + } + __CFRunLoopSourceLock(rls); + if (NULL == rls->_runLoops) { + rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops! + } + CFBagAddValue(rls->_runLoops, rl); + __CFRunLoopSourceUnlock(rls); + if (0 == rls->_context.version0.version) { + if (NULL != rls->_context.version0.schedule) { + doVer0Callout = true; + } + } + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); + if (doVer0Callout) { + // although it looses some protection for the source, we have no choice but + // to do this after unlocking the run loop and mode locks, to avoid deadlocks + // where the source wants to take a lock which is already held in another + // thread which is itself waiting for a run loop/mode lock + rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */ + } +} + +void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + Boolean doVer0Callout = false, doRLSRelease = false; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) { + CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; + CFSetRemoveValue(rl->_commonModeItems, rls); + if (NULL != set) { + CFTypeRef context[2] = {rl, rls}; + /* remove new item from all common-modes */ + CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context); + CFRelease(set); + } + } else { + } + } else { + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false); + if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) { + CFRetain(rls); + if (1 == rls->_context.version0.version) { + __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info); + if (CFPORT_NULL != src_port) { + CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port); + __CFPortSetRemove(src_port, rlm->_portSet); + } + } + CFSetRemoveValue(rlm->_sources0, rls); + CFSetRemoveValue(rlm->_sources1, rls); + __CFRunLoopSourceLock(rls); + if (NULL != rls->_runLoops) { + CFBagRemoveValue(rls->_runLoops, rl); + } + __CFRunLoopSourceUnlock(rls); + if (0 == rls->_context.version0.version) { + if (NULL != rls->_context.version0.cancel) { + doVer0Callout = true; + } + } + doRLSRelease = true; + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); + if (doVer0Callout) { + // although it looses some protection for the source, we have no choice but + // to do this after unlocking the run loop and mode locks, to avoid deadlocks + // where the source wants to take a lock which is already held in another + // thread which is itself waiting for a run loop/mode lock + rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName); /* CALLOUT */ + } + if (doRLSRelease) CFRelease(rls); +} + +static void __CFRunLoopRemoveSourcesFromCommonMode(const void *value, void *ctx) { + CFStringRef modeName = (CFStringRef)value; + CFRunLoopRef rl = (CFRunLoopRef)ctx; + __CFRunLoopRemoveAllSources(rl, modeName); +} + +static void __CFRunLoopRemoveSourceFromMode(const void *value, void *ctx) { + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)value; + CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]); + CFStringRef modeName = (CFStringRef)(((CFTypeRef *)ctx)[1]); + CFRunLoopRemoveSource(rl, rls, modeName); +} + +static void __CFRunLoopRemoveAllSources(CFRunLoopRef rl, CFStringRef modeName) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + if (NULL != rl->_commonModeItems) { + CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; + if (NULL != set) { + CFSetApplyFunction(set, (__CFRunLoopRemoveSourcesFromCommonMode), (void *)rl); + CFRelease(set); + } + } else { + } + } else { + rlm = __CFRunLoopFindMode(rl, modeName, false); + if (NULL != rlm && NULL != rlm->_sources0) { + CFSetRef set = CFSetCreateCopy(kCFAllocatorSystemDefault, rlm->_sources0); + CFTypeRef context[2] = {rl, modeName}; + CFSetApplyFunction(set, (__CFRunLoopRemoveSourceFromMode), (void *)context); + CFRelease(set); + } + if (NULL != rlm && NULL != rlm->_sources1) { + CFSetRef set = CFSetCreateCopy(kCFAllocatorSystemDefault, rlm->_sources1); + CFTypeRef context[2] = {rl, modeName}; + CFSetApplyFunction(set, (__CFRunLoopRemoveSourceFromMode), (void *)context); + CFRelease(set); + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); +} + +Boolean CFRunLoopContainsObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + Boolean hasValue = false; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + if (NULL != rl->_commonModeItems) { + hasValue = CFSetContainsValue(rl->_commonModeItems, rlo); + } + } else { + rlm = __CFRunLoopFindMode(rl, modeName, false); + if (NULL != rlm && NULL != rlm->_observers) { + hasValue = CFArrayContainsValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo); + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); + return hasValue; +} + +void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + if (__CFRunLoopIsDeallocating(rl)) return; + if (!__CFIsValid(rlo) || (NULL != rlo->_runLoop && rlo->_runLoop != rl)) return; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; + if (NULL == rl->_commonModeItems) { + rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + } + CFSetAddValue(rl->_commonModeItems, rlo); + if (NULL != set) { + CFTypeRef context[2] = {rl, rlo}; + /* add new item to all common-modes */ + CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context); + CFRelease(set); + } + } else { + rlm = __CFRunLoopFindMode(rl, modeName, true); + if (NULL != rlm && NULL == rlm->_observers) { + rlm->_observers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks); + } + if (NULL != rlm && !CFArrayContainsValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo)) { + Boolean inserted = false; + for (CFIndex idx = CFArrayGetCount(rlm->_observers); idx--; ) { + CFRunLoopObserverRef obs = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx); + if (obs->_order <= rlo->_order) { + CFArrayInsertValueAtIndex(rlm->_observers, idx + 1, rlo); + inserted = true; + break; + } + } + if (!inserted) { + CFArrayInsertValueAtIndex(rlm->_observers, 0, rlo); + } + rlm->_observerMask |= rlo->_activities; + __CFRunLoopObserverSchedule(rlo, rl, rlm); + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); +} + +void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) { + CHECK_FOR_FORK(); + CFRunLoopModeRef rlm; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rlo)) { + CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; + CFSetRemoveValue(rl->_commonModeItems, rlo); + if (NULL != set) { + CFTypeRef context[2] = {rl, rlo}; + /* remove new item from all common-modes */ + CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context); + CFRelease(set); + } + } else { + } + } else { + rlm = __CFRunLoopFindMode(rl, modeName, false); + if (NULL != rlm && NULL != rlm->_observers) { + CFRetain(rlo); + CFIndex idx = CFArrayGetFirstIndexOfValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo); + if (kCFNotFound != idx) { + CFArrayRemoveValueAtIndex(rlm->_observers, idx); + __CFRunLoopObserverCancel(rlo, rl, rlm); + } + CFRelease(rlo); + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); +} + +Boolean CFRunLoopContainsTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) { + CHECK_FOR_FORK(); + if (NULL == rlt->_runLoop || rl != rlt->_runLoop) return false; + Boolean hasValue = false; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + if (NULL != rl->_commonModeItems) { + hasValue = CFSetContainsValue(rl->_commonModeItems, rlt); + } + } else { + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false); + if (NULL != rlm) { + if (NULL != rlm->_timers) { + CFIndex idx = CFArrayGetFirstIndexOfValue(rlm->_timers, CFRangeMake(0, CFArrayGetCount(rlm->_timers)), rlt); + hasValue = (kCFNotFound != idx); + } + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); + return hasValue; +} + +void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) { + CHECK_FOR_FORK(); + if (__CFRunLoopIsDeallocating(rl)) return; + if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; + if (NULL == rl->_commonModeItems) { + rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + } + CFSetAddValue(rl->_commonModeItems, rlt); + if (NULL != set) { + CFTypeRef context[2] = {rl, rlt}; + /* add new item to all common-modes */ + CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context); + CFRelease(set); + } + } else { + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true); + if (NULL != rlm) { + if (NULL == rlm->_timers) { + CFArrayCallBacks cb = kCFTypeArrayCallBacks; + cb.equal = NULL; + rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb); + } + } + if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) { + __CFRunLoopTimerLock(rlt); + if (NULL == rlt->_runLoop) { + rlt->_runLoop = rl; + } else if (rl != rlt->_runLoop) { + __CFRunLoopTimerUnlock(rlt); + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + return; + } + CFSetAddValue(rlt->_rlModes, rlm->_name); + __CFRunLoopTimerUnlock(rlt); + __CFRunLoopTimerFireTSRLock(); + __CFRepositionTimerInMode(rlm, rlt, false); + __CFRunLoopTimerFireTSRUnlock(); + if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) { + // Normally we don't do this on behalf of clients, but for + // backwards compatibility due to the change in timer handling... + if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl); + } + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); +} + +void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) { + CHECK_FOR_FORK(); + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { + if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rlt)) { + CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; + CFSetRemoveValue(rl->_commonModeItems, rlt); + if (NULL != set) { + CFTypeRef context[2] = {rl, rlt}; + /* remove new item from all common-modes */ + CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context); + CFRelease(set); + } + } else { + } + } else { + CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false); + CFIndex idx = kCFNotFound; + CFMutableArrayRef timerList = NULL; + if (NULL != rlm) { + timerList = rlm->_timers; + if (NULL != timerList) { + idx = CFArrayGetFirstIndexOfValue(timerList, CFRangeMake(0, CFArrayGetCount(timerList)), rlt); + } + } + if (kCFNotFound != idx) { + __CFRunLoopTimerLock(rlt); + CFSetRemoveValue(rlt->_rlModes, rlm->_name); + if (0 == CFSetGetCount(rlt->_rlModes)) { + rlt->_runLoop = NULL; + } + __CFRunLoopTimerUnlock(rlt); + CFArrayRemoveValueAtIndex(timerList, idx); + __CFArmNextTimerInMode(rlm, rl); + } + if (NULL != rlm) { + __CFRunLoopModeUnlock(rlm); + } + } + __CFRunLoopUnlock(rl); +} + +/* CFRunLoopSource */ + +static Boolean __CFRunLoopSourceEqual(CFTypeRef cf1, CFTypeRef cf2) { /* DOES CALLOUT */ + CFRunLoopSourceRef rls1 = (CFRunLoopSourceRef)cf1; + CFRunLoopSourceRef rls2 = (CFRunLoopSourceRef)cf2; + if (rls1 == rls2) return true; + if (__CFIsValid(rls1) != __CFIsValid(rls2)) return false; + if (rls1->_order != rls2->_order) return false; + if (rls1->_context.version0.version != rls2->_context.version0.version) return false; + if (rls1->_context.version0.hash != rls2->_context.version0.hash) return false; + if (rls1->_context.version0.equal != rls2->_context.version0.equal) return false; + if (0 == rls1->_context.version0.version && rls1->_context.version0.perform != rls2->_context.version0.perform) return false; + if (1 == rls1->_context.version0.version && rls1->_context.version1.perform != rls2->_context.version1.perform) return false; + if (rls1->_context.version0.equal) + return rls1->_context.version0.equal(rls1->_context.version0.info, rls2->_context.version0.info); + return (rls1->_context.version0.info == rls2->_context.version0.info); +} + +static CFHashCode __CFRunLoopSourceHash(CFTypeRef cf) { /* DOES CALLOUT */ + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)cf; + if (rls->_context.version0.hash) + return rls->_context.version0.hash(rls->_context.version0.info); + return (CFHashCode)rls->_context.version0.info; +} + +static CFStringRef __CFRunLoopSourceCopyDescription(CFTypeRef cf) { /* DOES CALLOUT */ + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)cf; + CFStringRef result; + CFStringRef contextDesc = NULL; + if (NULL != rls->_context.version0.copyDescription) { + contextDesc = rls->_context.version0.copyDescription(rls->_context.version0.info); + } + if (NULL == contextDesc) { + void *addr = rls->_context.version0.version == 0 ? (void *)rls->_context.version0.perform : (rls->_context.version0.version == 1 ? (void *)rls->_context.version1.perform : NULL); +#if DEPLOYMENT_TARGET_WINDOWS + contextDesc = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("{version = %ld, info = %p, callout = %p}"), rls->_context.version0.version, rls->_context.version0.info, addr); +#elif DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + Dl_info info; + const char *name = (dladdr(addr, &info) && info.dli_saddr == addr && info.dli_sname) ? info.dli_sname : "???"; + contextDesc = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("{version = %ld, info = %p, callout = %s (%p)}"), rls->_context.version0.version, rls->_context.version0.info, name, addr); +#endif + } +#if DEPLOYMENT_TARGET_WINDOWS + result = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("{signalled = %s, valid = %s, order = %d, context = %@}"), cf, CFGetAllocator(rls), __CFRunLoopSourceIsSignaled(rls) ? "Yes" : "No", __CFIsValid(rls) ? "Yes" : "No", rls->_order, contextDesc); +#else + result = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("{signalled = %s, valid = %s, order = %ld, context = %@}"), cf, CFGetAllocator(rls), __CFRunLoopSourceIsSignaled(rls) ? "Yes" : "No", __CFIsValid(rls) ? "Yes" : "No", (unsigned long)rls->_order, contextDesc); +#endif + CFRelease(contextDesc); + return result; +} + +static void __CFRunLoopSourceDeallocate(CFTypeRef cf) { /* DOES CALLOUT */ + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)cf; + CFRunLoopSourceInvalidate(rls); + if (rls->_context.version0.release) { + rls->_context.version0.release(rls->_context.version0.info); + } + pthread_mutex_destroy(&rls->_lock); + memset((char *)cf + sizeof(CFRuntimeBase), 0, sizeof(struct __CFRunLoopSource) - sizeof(CFRuntimeBase)); +} + +static const CFRuntimeClass __CFRunLoopSourceClass = { + _kCFRuntimeScannedObject, + "CFRunLoopSource", + NULL, // init + NULL, // copy + __CFRunLoopSourceDeallocate, + __CFRunLoopSourceEqual, + __CFRunLoopSourceHash, + NULL, // + __CFRunLoopSourceCopyDescription +}; + +CFTypeID CFRunLoopSourceGetTypeID(void) { + static dispatch_once_t initOnce; + dispatch_once(&initOnce, ^{ __kCFRunLoopSourceTypeID = _CFRuntimeRegisterClass(&__CFRunLoopSourceClass); }); + return __kCFRunLoopSourceTypeID; +} + +CFRunLoopSourceRef CFRunLoopSourceCreate(CFAllocatorRef allocator, CFIndex order, CFRunLoopSourceContext *context) { + CHECK_FOR_FORK(); + CFRunLoopSourceRef memory; + uint32_t size; + if (NULL == context) CRASH("*** NULL context value passed to CFRunLoopSourceCreate(). (%d) ***", -1); + + size = sizeof(struct __CFRunLoopSource) - sizeof(CFRuntimeBase); + memory = (CFRunLoopSourceRef)_CFRuntimeCreateInstance(allocator, CFRunLoopSourceGetTypeID(), size, NULL); + if (NULL == memory) { + return NULL; + } + __CFSetValid(memory); + __CFRunLoopSourceUnsetSignaled(memory); + __CFRunLoopLockInit(&memory->_lock); + memory->_bits = 0; + memory->_order = order; + memory->_runLoops = NULL; + size = 0; + switch (context->version) { + case 0: + size = sizeof(CFRunLoopSourceContext); + break; + case 1: + size = sizeof(CFRunLoopSourceContext1); + break; + } + objc_memmove_collectable(&memory->_context, context, size); + if (context->retain) { + memory->_context.version0.info = (void *)context->retain(context->info); + } + return memory; +} + +CFIndex CFRunLoopSourceGetOrder(CFRunLoopSourceRef rls) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rls, CFRunLoopSourceGetTypeID()); + return rls->_order; +} + +static void __CFRunLoopSourceWakeUpLoop(const void *value, void *context) { + CFRunLoopWakeUp((CFRunLoopRef)value); +} + +static void __CFRunLoopSourceRemoveFromRunLoop(const void *value, void *context) { + CFRunLoopRef rl = (CFRunLoopRef)value; + CFTypeRef *params = (CFTypeRef *)context; + CFRunLoopSourceRef rls = (CFRunLoopSourceRef)params[0]; + CFIndex idx; + if (rl == params[1]) return; + + // CFRunLoopRemoveSource will lock the run loop while it + // needs that, but we also lock it out here to keep + // changes from occurring for this whole sequence. + __CFRunLoopLock(rl); + CFArrayRef array = CFRunLoopCopyAllModes(rl); + for (idx = CFArrayGetCount(array); idx--;) { + CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx); + CFRunLoopRemoveSource(rl, rls, modeName); + } + CFRunLoopRemoveSource(rl, rls, kCFRunLoopCommonModes); + __CFRunLoopUnlock(rl); + CFRelease(array); + params[1] = rl; +} + +void CFRunLoopSourceInvalidate(CFRunLoopSourceRef rls) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rls, CFRunLoopSourceGetTypeID()); + __CFRunLoopSourceLock(rls); + CFRetain(rls); + if (__CFIsValid(rls)) { + CFBagRef rloops = rls->_runLoops; + __CFUnsetValid(rls); + __CFRunLoopSourceUnsetSignaled(rls); + if (NULL != rloops) { + // To avoid A->B, B->A lock ordering issues when coming up + // towards the run loop from a source, the source has to be + // unlocked, which means we have to protect from object + // invalidation. + rls->_runLoops = NULL; // transfer ownership to local stack + __CFRunLoopSourceUnlock(rls); + CFTypeRef params[2] = {rls, NULL}; + CFBagApplyFunction(rloops, (__CFRunLoopSourceRemoveFromRunLoop), params); + CFRelease(rloops); + __CFRunLoopSourceLock(rls); + } + /* for hashing- and equality-use purposes, can't actually release the context here */ + } + __CFRunLoopSourceUnlock(rls); + CFRelease(rls); +} + +Boolean CFRunLoopSourceIsValid(CFRunLoopSourceRef rls) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rls, CFRunLoopSourceGetTypeID()); + return __CFIsValid(rls); +} + +void CFRunLoopSourceGetContext(CFRunLoopSourceRef rls, CFRunLoopSourceContext *context) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rls, CFRunLoopSourceGetTypeID()); + CFAssert1(0 == context->version || 1 == context->version, __kCFLogAssertion, "%s(): context version not initialized to 0 or 1", __PRETTY_FUNCTION__); + CFIndex size = 0; + switch (context->version) { + case 0: + size = sizeof(CFRunLoopSourceContext); + break; + case 1: + size = sizeof(CFRunLoopSourceContext1); + break; + } + memmove(context, &rls->_context, size); +} + +void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) { + CHECK_FOR_FORK(); + __CFRunLoopSourceLock(rls); + if (__CFIsValid(rls)) { + __CFRunLoopSourceSetSignaled(rls); + } + __CFRunLoopSourceUnlock(rls); +} + +Boolean CFRunLoopSourceIsSignalled(CFRunLoopSourceRef rls) { + CHECK_FOR_FORK(); + __CFRunLoopSourceLock(rls); + Boolean ret = __CFRunLoopSourceIsSignaled(rls) ? true : false; + __CFRunLoopSourceUnlock(rls); + return ret; +} + +CF_PRIVATE void _CFRunLoopSourceWakeUpRunLoops(CFRunLoopSourceRef rls) { + CFBagRef loops = NULL; + __CFRunLoopSourceLock(rls); + if (__CFIsValid(rls) && NULL != rls->_runLoops) { + loops = CFBagCreateCopy(kCFAllocatorSystemDefault, rls->_runLoops); + } + __CFRunLoopSourceUnlock(rls); + if (loops) { + CFBagApplyFunction(loops, __CFRunLoopSourceWakeUpLoop, NULL); + CFRelease(loops); + } +} + +/* CFRunLoopObserver */ + +static CFStringRef __CFRunLoopObserverCopyDescription(CFTypeRef cf) { /* DOES CALLOUT */ + CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)cf; + CFStringRef result; + CFStringRef contextDesc = NULL; + if (NULL != rlo->_context.copyDescription) { + contextDesc = rlo->_context.copyDescription(rlo->_context.info); + } + if (!contextDesc) { + contextDesc = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR(""), rlo->_context.info); + } +#if DEPLOYMENT_TARGET_WINDOWS + result = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("{valid = %s, activities = 0x%x, repeats = %s, order = %d, callout = %p, context = %@}"), cf, CFGetAllocator(rlo), __CFIsValid(rlo) ? "Yes" : "No", rlo->_activities, __CFRunLoopObserverRepeats(rlo) ? "Yes" : "No", rlo->_order, rlo->_callout, contextDesc); +#elif DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + void *addr = rlo->_callout; + Dl_info info; + const char *name = (dladdr(addr, &info) && info.dli_saddr == addr && info.dli_sname) ? info.dli_sname : "???"; + result = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("{valid = %s, activities = 0x%lx, repeats = %s, order = %ld, callout = %s (%p), context = %@}"), cf, CFGetAllocator(rlo), __CFIsValid(rlo) ? "Yes" : "No", (long)rlo->_activities, __CFRunLoopObserverRepeats(rlo) ? "Yes" : "No", (long)rlo->_order, name, addr, contextDesc); +#endif + CFRelease(contextDesc); + return result; +} + +static void __CFRunLoopObserverDeallocate(CFTypeRef cf) { /* DOES CALLOUT */ + CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)cf; + CFRunLoopObserverInvalidate(rlo); + pthread_mutex_destroy(&rlo->_lock); +} + +static const CFRuntimeClass __CFRunLoopObserverClass = { + 0, + "CFRunLoopObserver", + NULL, // init + NULL, // copy + __CFRunLoopObserverDeallocate, + NULL, + NULL, + NULL, // + __CFRunLoopObserverCopyDescription +}; + +CFTypeID CFRunLoopObserverGetTypeID(void) { + static dispatch_once_t initOnce; + dispatch_once(&initOnce, ^{ __kCFRunLoopObserverTypeID = _CFRuntimeRegisterClass(&__CFRunLoopObserverClass); }); + return __kCFRunLoopObserverTypeID; +} + +CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context) { + CHECK_FOR_FORK(); + CFRunLoopObserverRef memory; + UInt32 size; + size = sizeof(struct __CFRunLoopObserver) - sizeof(CFRuntimeBase); + memory = (CFRunLoopObserverRef)_CFRuntimeCreateInstance(allocator, CFRunLoopObserverGetTypeID(), size, NULL); + if (NULL == memory) { + return NULL; + } + __CFSetValid(memory); + __CFRunLoopObserverUnsetFiring(memory); + if (repeats) { + __CFRunLoopObserverSetRepeats(memory); + } else { + __CFRunLoopObserverUnsetRepeats(memory); + } + __CFRunLoopLockInit(&memory->_lock); + memory->_runLoop = NULL; + memory->_rlCount = 0; + memory->_activities = activities; + memory->_order = order; + memory->_callout = callout; + if (context) { + if (context->retain) { + memory->_context.info = (void *)context->retain(context->info); + } else { + memory->_context.info = context->info; + } + memory->_context.retain = context->retain; + memory->_context.release = context->release; + memory->_context.copyDescription = context->copyDescription; + } else { + memory->_context.info = 0; + memory->_context.retain = 0; + memory->_context.release = 0; + memory->_context.copyDescription = 0; + } + return memory; +} + +static void _runLoopObserverWithBlockContext(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *opaqueBlock) { + typedef void (^observer_block_t) (CFRunLoopObserverRef observer, CFRunLoopActivity activity); + observer_block_t block = (observer_block_t)opaqueBlock; + block(observer, activity); +} + +CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, + void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity)) { + CFRunLoopObserverContext blockContext; + blockContext.version = 0; + blockContext.info = (void *)block; + blockContext.retain = (const void *(*)(const void *info))_Block_copy; + blockContext.release = (void (*)(const void *info))_Block_release; + blockContext.copyDescription = NULL; + return CFRunLoopObserverCreate(allocator, activities, repeats, order, _runLoopObserverWithBlockContext, &blockContext); +} + +CFOptionFlags CFRunLoopObserverGetActivities(CFRunLoopObserverRef rlo) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rlo, CFRunLoopObserverGetTypeID()); + return rlo->_activities; +} + +CFIndex CFRunLoopObserverGetOrder(CFRunLoopObserverRef rlo) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rlo, CFRunLoopObserverGetTypeID()); + return rlo->_order; +} + +Boolean CFRunLoopObserverDoesRepeat(CFRunLoopObserverRef rlo) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rlo, CFRunLoopObserverGetTypeID()); + return __CFRunLoopObserverRepeats(rlo); +} + +void CFRunLoopObserverInvalidate(CFRunLoopObserverRef rlo) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + __CFGenericValidateType(rlo, CFRunLoopObserverGetTypeID()); + __CFRunLoopObserverLock(rlo); + CFRetain(rlo); + if (__CFIsValid(rlo)) { + CFRunLoopRef rl = rlo->_runLoop; + void *info = rlo->_context.info; + rlo->_context.info = NULL; + __CFUnsetValid(rlo); + if (NULL != rl) { + // To avoid A->B, B->A lock ordering issues when coming up + // towards the run loop from an observer, it has to be + // unlocked, which means we have to protect from object + // invalidation. + CFRetain(rl); + __CFRunLoopObserverUnlock(rlo); + // CFRunLoopRemoveObserver will lock the run loop while it + // needs that, but we also lock it out here to keep + // changes from occurring for this whole sequence. + __CFRunLoopLock(rl); + CFArrayRef array = CFRunLoopCopyAllModes(rl); + for (CFIndex idx = CFArrayGetCount(array); idx--;) { + CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx); + CFRunLoopRemoveObserver(rl, rlo, modeName); + } + CFRunLoopRemoveObserver(rl, rlo, kCFRunLoopCommonModes); + __CFRunLoopUnlock(rl); + CFRelease(array); + CFRelease(rl); + __CFRunLoopObserverLock(rlo); + } + if (NULL != rlo->_context.release) { + rlo->_context.release(info); /* CALLOUT */ + } + } + __CFRunLoopObserverUnlock(rlo); + CFRelease(rlo); +} + +Boolean CFRunLoopObserverIsValid(CFRunLoopObserverRef rlo) { + CHECK_FOR_FORK(); + return __CFIsValid(rlo); +} + +void CFRunLoopObserverGetContext(CFRunLoopObserverRef rlo, CFRunLoopObserverContext *context) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rlo, CFRunLoopObserverGetTypeID()); + CFAssert1(0 == context->version, __kCFLogAssertion, "%s(): context version not initialized to 0", __PRETTY_FUNCTION__); + *context = rlo->_context; +} + +#pragma mark - +#pragma mark CFRunLoopTimer + +static CFStringRef __CFRunLoopTimerCopyDescription(CFTypeRef cf) { /* DOES CALLOUT */ + CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)cf; + CFStringRef contextDesc = NULL; + if (NULL != rlt->_context.copyDescription) { + contextDesc = rlt->_context.copyDescription(rlt->_context.info); + } + if (NULL == contextDesc) { + contextDesc = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR(""), rlt->_context.info); + } + void *addr = (void *)rlt->_callout; + char libraryName[2048]; + char functionName[2048]; + void *functionPtr = NULL; + libraryName[0] = '?'; libraryName[1] = '\0'; + functionName[0] = '?'; functionName[1] = '\0'; + CFStringRef result = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, + CFSTR("{valid = %s, firing = %s, interval = %0.09g, tolerance = %0.09g, next fire date = %0.09g (%0.09g @ %lld), callout = %s (%p / %p) (%s), context = %@}"), + cf, + CFGetAllocator(rlt), + __CFIsValid(rlt) ? "Yes" : "No", + __CFRunLoopTimerIsFiring(rlt) ? "Yes" : "No", + rlt->_interval, + rlt->_tolerance, + rlt->_nextFireDate, + rlt->_nextFireDate - CFAbsoluteTimeGetCurrent(), + rlt->_fireTSR, + functionName, + addr, + functionPtr, + libraryName, + contextDesc); + CFRelease(contextDesc); + return result; +} + +static void __CFRunLoopTimerDeallocate(CFTypeRef cf) { /* DOES CALLOUT */ +//CFLog(6, CFSTR("__CFRunLoopTimerDeallocate(%p)"), cf); + CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)cf; + __CFRunLoopTimerSetDeallocating(rlt); + CFRunLoopTimerInvalidate(rlt); /* DOES CALLOUT */ + CFRelease(rlt->_rlModes); + rlt->_rlModes = NULL; + pthread_mutex_destroy(&rlt->_lock); +} + +static const CFRuntimeClass __CFRunLoopTimerClass = { + 0, + "CFRunLoopTimer", + NULL, // init + NULL, // copy + __CFRunLoopTimerDeallocate, + NULL, // equal + NULL, + NULL, // + __CFRunLoopTimerCopyDescription +}; + +CFTypeID CFRunLoopTimerGetTypeID(void) { + static dispatch_once_t initOnce; + dispatch_once(&initOnce, ^{ __kCFRunLoopTimerTypeID = _CFRuntimeRegisterClass(&__CFRunLoopTimerClass); }); + return __kCFRunLoopTimerTypeID; +} + +CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context) { + CHECK_FOR_FORK(); + if (isnan(interval)) { + CRSetCrashLogMessage("NaN was used as an interval for a CFRunLoopTimer"); + HALT; + } + CFRunLoopTimerRef memory; + UInt32 size; + size = sizeof(struct __CFRunLoopTimer) - sizeof(CFRuntimeBase); + memory = (CFRunLoopTimerRef)_CFRuntimeCreateInstance(allocator, CFRunLoopTimerGetTypeID(), size, NULL); + if (NULL == memory) { + return NULL; + } + __CFSetValid(memory); + __CFRunLoopTimerUnsetFiring(memory); + __CFRunLoopLockInit(&memory->_lock); + memory->_runLoop = NULL; + memory->_rlModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); + memory->_order = order; + if (interval < 0.0) interval = 0.0; + memory->_interval = interval; + memory->_tolerance = 0.0; + if (TIMER_DATE_LIMIT < fireDate) fireDate = TIMER_DATE_LIMIT; + memory->_nextFireDate = fireDate; + memory->_fireTSR = 0ULL; + uint64_t now2 = mach_absolute_time(); + CFAbsoluteTime now1 = CFAbsoluteTimeGetCurrent(); + if (fireDate < now1) { + memory->_fireTSR = now2; + } else if (TIMER_INTERVAL_LIMIT < fireDate - now1) { + memory->_fireTSR = now2 + __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT); + } else { + memory->_fireTSR = now2 + __CFTimeIntervalToTSR(fireDate - now1); + } + memory->_callout = callout; + if (NULL != context) { + if (context->retain) { + memory->_context.info = (void *)context->retain(context->info); + } else { + memory->_context.info = context->info; + } + memory->_context.retain = context->retain; + memory->_context.release = context->release; + memory->_context.copyDescription = context->copyDescription; + } else { + memory->_context.info = 0; + memory->_context.retain = 0; + memory->_context.release = 0; + memory->_context.copyDescription = 0; + } + return memory; +} + +static void _runLoopTimerWithBlockContext(CFRunLoopTimerRef timer, void *opaqueBlock) { + typedef void (^timer_block_t) (CFRunLoopTimerRef timer); + timer_block_t block = (timer_block_t)opaqueBlock; + block(timer); +} + +CFRunLoopTimerRef CFRunLoopTimerCreateWithHandler(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, + void (^block) (CFRunLoopTimerRef timer)) { + + CFRunLoopTimerContext blockContext; + blockContext.version = 0; + blockContext.info = (void *)block; + blockContext.retain = (const void *(*)(const void *info))_Block_copy; + blockContext.release = (void (*)(const void *info))_Block_release; + blockContext.copyDescription = NULL; + return CFRunLoopTimerCreate(allocator, fireDate, interval, flags, order, _runLoopTimerWithBlockContext, &blockContext); +} + +CFAbsoluteTime CFRunLoopTimerGetNextFireDate(CFRunLoopTimerRef rlt) { + CHECK_FOR_FORK(); + CF_OBJC_FUNCDISPATCHV(CFRunLoopTimerGetTypeID(), CFAbsoluteTime, (NSTimer *)rlt, _cffireTime); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + CFAbsoluteTime at = 0.0; + __CFRunLoopTimerLock(rlt); + __CFRunLoopTimerFireTSRLock(); + if (__CFIsValid(rlt)) { + at = rlt->_nextFireDate; + } + __CFRunLoopTimerFireTSRUnlock(); + __CFRunLoopTimerUnlock(rlt); + return at; +} + +void CFRunLoopTimerSetNextFireDate(CFRunLoopTimerRef rlt, CFAbsoluteTime fireDate) { + CHECK_FOR_FORK(); + if (!__CFIsValid(rlt)) return; + if (TIMER_DATE_LIMIT < fireDate) fireDate = TIMER_DATE_LIMIT; + uint64_t nextFireTSR = 0ULL; + uint64_t now2 = mach_absolute_time(); + CFAbsoluteTime now1 = CFAbsoluteTimeGetCurrent(); + if (fireDate < now1) { + nextFireTSR = now2; + } else if (TIMER_INTERVAL_LIMIT < fireDate - now1) { + nextFireTSR = now2 + __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT); + } else { + nextFireTSR = now2 + __CFTimeIntervalToTSR(fireDate - now1); + } + __CFRunLoopTimerLock(rlt); + if (NULL != rlt->_runLoop) { + CFIndex cnt = CFSetGetCount(rlt->_rlModes); + STACK_BUFFER_DECL(CFTypeRef, modes, cnt); + CFSetGetValues(rlt->_rlModes, (const void **)modes); + // To avoid A->B, B->A lock ordering issues when coming up + // towards the run loop from a source, the timer has to be + // unlocked, which means we have to protect from object + // invalidation, although that's somewhat expensive. + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRetain(modes[idx]); + } + CFRunLoopRef rl = (CFRunLoopRef)CFRetain(rlt->_runLoop); + __CFRunLoopTimerUnlock(rlt); + __CFRunLoopLock(rl); + for (CFIndex idx = 0; idx < cnt; idx++) { + CFStringRef name = (CFStringRef)modes[idx]; + modes[idx] = __CFRunLoopFindMode(rl, name, false); + CFRelease(name); + } + __CFRunLoopTimerFireTSRLock(); + rlt->_fireTSR = nextFireTSR; + rlt->_nextFireDate = fireDate; + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopModeRef rlm = (CFRunLoopModeRef)modes[idx]; + if (rlm) { + __CFRepositionTimerInMode(rlm, rlt, true); + } + } + __CFRunLoopTimerFireTSRUnlock(); + for (CFIndex idx = 0; idx < cnt; idx++) { + __CFRunLoopModeUnlock((CFRunLoopModeRef)modes[idx]); + } + __CFRunLoopUnlock(rl); + // This is setting the date of a timer, not a direct + // interaction with a run loop, so we'll do a wakeup + // (which may be costly) for the caller, just in case. + // (And useful for binary compatibility with older + // code used to the older timer implementation.) + if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl); + CFRelease(rl); + } else { + __CFRunLoopTimerFireTSRLock(); + rlt->_fireTSR = nextFireTSR; + rlt->_nextFireDate = fireDate; + __CFRunLoopTimerFireTSRUnlock(); + __CFRunLoopTimerUnlock(rlt); + } +} + +CFTimeInterval CFRunLoopTimerGetInterval(CFRunLoopTimerRef rlt) { + CHECK_FOR_FORK(); + CF_OBJC_FUNCDISPATCHV(CFRunLoopTimerGetTypeID(), CFTimeInterval, (NSTimer *)rlt, timeInterval); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + return rlt->_interval; +} + +Boolean CFRunLoopTimerDoesRepeat(CFRunLoopTimerRef rlt) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + return (0.0 < rlt->_interval); +} + +CFIndex CFRunLoopTimerGetOrder(CFRunLoopTimerRef rlt) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + return rlt->_order; +} + +void CFRunLoopTimerInvalidate(CFRunLoopTimerRef rlt) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + CF_OBJC_FUNCDISPATCHV(CFRunLoopTimerGetTypeID(), void, (NSTimer *)rlt, invalidate); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + __CFRunLoopTimerLock(rlt); + if (!__CFRunLoopTimerIsDeallocating(rlt)) { + CFRetain(rlt); + } + if (__CFIsValid(rlt)) { + CFRunLoopRef rl = rlt->_runLoop; + void *info = rlt->_context.info; + rlt->_context.info = NULL; + __CFUnsetValid(rlt); + if (NULL != rl) { + CFIndex cnt = CFSetGetCount(rlt->_rlModes); + STACK_BUFFER_DECL(CFStringRef, modes, cnt); + CFSetGetValues(rlt->_rlModes, (const void **)modes); + // To avoid A->B, B->A lock ordering issues when coming up + // towards the run loop from a source, the timer has to be + // unlocked, which means we have to protect from object + // invalidation, although that's somewhat expensive. + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRetain(modes[idx]); + } + CFRetain(rl); + __CFRunLoopTimerUnlock(rlt); + // CFRunLoopRemoveTimer will lock the run loop while it + // needs that, but we also lock it out here to keep + // changes from occurring for this whole sequence. + __CFRunLoopLock(rl); + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopRemoveTimer(rl, rlt, modes[idx]); + } + CFRunLoopRemoveTimer(rl, rlt, kCFRunLoopCommonModes); + __CFRunLoopUnlock(rl); + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRelease(modes[idx]); + } + CFRelease(rl); + __CFRunLoopTimerLock(rlt); + } + if (NULL != rlt->_context.release) { + rlt->_context.release(info); /* CALLOUT */ + } + } + __CFRunLoopTimerUnlock(rlt); + if (!__CFRunLoopTimerIsDeallocating(rlt)) { + CFRelease(rlt); + } +} + +Boolean CFRunLoopTimerIsValid(CFRunLoopTimerRef rlt) { + CHECK_FOR_FORK(); + CF_OBJC_FUNCDISPATCHV(CFRunLoopTimerGetTypeID(), Boolean, (NSTimer *)rlt, isValid); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + return __CFIsValid(rlt); +} + +void CFRunLoopTimerGetContext(CFRunLoopTimerRef rlt, CFRunLoopTimerContext *context) { + CHECK_FOR_FORK(); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + CFAssert1(0 == context->version, __kCFLogAssertion, "%s(): context version not initialized to 0", __PRETTY_FUNCTION__); + *context = rlt->_context; +} + +CFTimeInterval CFRunLoopTimerGetTolerance(CFRunLoopTimerRef rlt) { +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + CHECK_FOR_FORK(); + CF_OBJC_FUNCDISPATCHV(CFRunLoopTimerGetTypeID(), CFTimeInterval, (NSTimer *)rlt, tolerance); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + return rlt->_tolerance; +#else + return 0.0; +#endif +} + +void CFRunLoopTimerSetTolerance(CFRunLoopTimerRef rlt, CFTimeInterval tolerance) { +#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + CHECK_FOR_FORK(); + CF_OBJC_FUNCDISPATCHV(CFRunLoopTimerGetTypeID(), void, (NSTimer *)rlt, setTolerance:tolerance); + __CFGenericValidateType(rlt, CFRunLoopTimerGetTypeID()); + /* + * dispatch rules: + * + * For the initial timer fire at 'start', the upper limit to the allowable + * delay is set to 'leeway' nanoseconds. For the subsequent timer fires at + * 'start' + N * 'interval', the upper limit is MIN('leeway','interval'/2). + */ + if (rlt->_interval > 0) { + rlt->_tolerance = MIN(tolerance, rlt->_interval / 2); + } else { + // Tolerance must be a positive value or zero + if (tolerance < 0) tolerance = 0.0; + rlt->_tolerance = tolerance; + } +#endif +} + diff --git a/assets/Charles-Setting.png b/assets/Charles-Setting.png new file mode 100644 index 0000000..ae970ab Binary files /dev/null and b/assets/Charles-Setting.png differ diff --git a/assets/DataViaNative.png b/assets/DataViaNative.png new file mode 100644 index 0000000..cfdabf0 Binary files /dev/null and b/assets/DataViaNative.png differ diff --git a/assets/Hybrid-Ajax.jpg b/assets/Hybrid-Ajax.jpg new file mode 100644 index 0000000..515f46a Binary files /dev/null and b/assets/Hybrid-Ajax.jpg differ diff --git a/assets/Hybrid-Structure.png b/assets/Hybrid-Structure.png new file mode 100644 index 0000000..2078d30 Binary files /dev/null and b/assets/Hybrid-Structure.png differ diff --git a/assets/HybridApi.jpg b/assets/HybridApi.jpg new file mode 100644 index 0000000..db30f86 Binary files /dev/null and b/assets/HybridApi.jpg differ diff --git a/assets/HybridStructure.jpg b/assets/HybridStructure.jpg new file mode 100644 index 0000000..d8349bf Binary files /dev/null and b/assets/HybridStructure.jpg differ diff --git a/assets/Native-JS-Communication.png b/assets/Native-JS-Communication.png new file mode 100644 index 0000000..5f2e2ae Binary files /dev/null and b/assets/Native-JS-Communication.png differ diff --git a/assets/NativeJSCommunication.jpeg b/assets/NativeJSCommunication.jpeg new file mode 100644 index 0000000..6acb8db Binary files /dev/null and b/assets/NativeJSCommunication.jpeg differ diff --git a/assets/WX20180724-184215@2x.png b/assets/WX20180724-184215.png similarity index 100% rename from assets/WX20180724-184215@2x.png rename to assets/WX20180724-184215.png diff --git a/assets/auto_Lint.sh b/assets/auto_Lint.sh new file mode 100755 index 0000000..85e5a55 --- /dev/null +++ b/assets/auto_Lint.sh @@ -0,0 +1,97 @@ +#!/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 () { + 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 \ No newline at end of file diff --git a/assets/内存.png b/assets/ram.png similarity index 100% rename from assets/内存.png rename to assets/ram.png diff --git a/assets/响应者链条.png b/assets/reponseChain.png similarity index 100% rename from assets/响应者链条.png rename to assets/reponseChain.png diff --git a/第一部分 iOS/.DS_Store b/第一部分 iOS/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/第一部分 iOS/.DS_Store and /dev/null differ diff --git a/第一部分 iOS/1.16.md b/第一部分 iOS/1.16.md deleted file mode 100644 index 939170b..0000000 --- a/第一部分 iOS/1.16.md +++ /dev/null @@ -1,137 +0,0 @@ -# CocoaPods最新安装步骤 - -## 一、简介 - -* ### 什么是CocoaPods - - * CocoaPods是OS X和iOS下的一个第三方库管理工具,通过CocoaPods我们可以为项目添加所依赖的库。(这种库必须是CocoaPods本身可以找到且支持的),且可以方便管理这些库的版本(更新、回退) -* ### 好处 - - * 在引入第三方库的时候它自动为我们完成各种各样的配置,包括配置编译阶段、连接器选项、甚至是ARC环境下的-fno-objc-arc配置等 - - * 使用CocoaPods可以方便查找第三方库(pod search AFNetworking\),这些库是比较标准的,因为在CocoaPods上可以找得到所以符合CocoaPods的规范比较标准 - -## 二、CocoaPods的安装步骤 - -### 1、升级机器的Ruby环境 - -``` -终端输入:$ gem update --system -``` - -此时有可能出现You do not have write permissions for the /Library/Ruby/Gems/2.0.0 directory - -明显告诉我们没有权限,用过linux的人知道需要加上sudo接下来会让你输入密码。不出意外接下来会提示 - -``` -RubyGems system software updated -``` - -### 2、更换Ruby镜像 - -* 首先移除现有的Ruby镜像 - -``` -终端输入:$ gem sources --remove https://rubygems.org/ -``` - -* 然后添加国内镜像 - -``` -终端输入:$ gem sources -a https://gems.ruby-china.org/ -``` - -* 执行完毕之后输入gem sources -l来查看当前镜像 - -``` -终端输入:$ gem sources -l -``` - -如果结果是 - -`*** CURRENT SOURCES ***https://gems.ruby-china.org/` - -### 3、安装Cocopods - -输入 - -``` -终端输入:$ sudo gem install cocoapods -``` - -在我的MBP上出现了提示说权限不足,接下来再次输入 - -``` -终端输入:$ sudo gem install -n /usr/local/bin cocoapods -``` - -安装成功 - -``` -终端显示 21 gems installed -``` - -继续 - -``` -pod setup -``` - -这个过程比较长 - - - -## 三、Cocopods的使用 - -### 1、查找 - -``` -终端输入:pod search ReactiveObjc -``` - -会显示找到的最新库及其历史版本 -![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/屏幕快照%202017-11-24%20上午10.49.35.png) - - - -### 2、用法 - -* 新建工程 -* 命令行切换到工程目录下 -* 新建一个Podfile - -``` -终端:touch Podfile -``` - -* 编辑Podfile - -``` -终端:vim Podfile -``` - -输入以下内容 - -``` -target 'RAC' do - -pod 'ReactiveObjC', '~> 3.0.0' - -end -``` - -* 终端安装库 - -``` -终端:pod install -``` - -* 完成之后会看到一个在项目目录下多出一个RAC.xcworkspace文件 -* 终端输入 - -``` -终端:open RAC.xcworkspace -``` - - - diff --git a/第一部分 iOS/1.20.md b/第一部分 iOS/1.20.md deleted file mode 100644 index 9ba27ce..0000000 --- a/第一部分 iOS/1.20.md +++ /dev/null @@ -1,4 +0,0 @@ -1、豆瓣模块化解决方案:https://github.com/lincode/FRDModuleManager(主要解决Appdelegate臃肿) - - - diff --git a/第一部分 iOS/1.32.md b/第一部分 iOS/1.32.md deleted file mode 100644 index dd5e752..0000000 --- a/第一部分 iOS/1.32.md +++ /dev/null @@ -1,39 +0,0 @@ -# Xcode 小技巧 - -1、快速打开:**Command+Shift+O** 。这个命令可以开启一个小窗格用来快速搜索浏览文件、类、算法以及函数等,且支持模糊搜索,这个命令可以说是日常开发中最常用的一个命令了。 - -2、显示项目导航器:**Command+Shift+J**。使用快速打开命令跳转到对应文件后,如果需要在左侧显示出该文件在项目中的目录结构,只需要键入这个命令,非常方便 - -3、显示编辑历史。如果一行代码写的很好或者很糟糕,不需要专门跑到 diff 工具去查看代码历史。在该行代码处右击,选择**Show Last Change For Line** - -4、跳转到方法。在使用类或者结构时,我们经常需要快速的跳转到类的某个特定方法。通过快捷键**control+6**再输入方法的头几个字母就可以非常方便的做到这点。 - -5、范围编辑。多光标是个很棒的并且每个高级的编辑器都该有的特训过,快捷键为**Command+Control+E**。将光标移动刀需要编辑的符号,输入快捷键,然后就可以在当前页面全局编辑了。 - -6、Xcode 设置代码只在 Debug 下起效的几种方式 -在日常开发中 Xcode 在 Debug 模式下写很多测试代码,或者引入一些第三方测试用的 .a 和 .framework 动态库,也会通过 CocoaPods 引入一些第三方测试工具或者库;但是不希望这些库在**Release**正式包中被引入,如何做到呢? - -* .h/.m 文件中的测试代码 - - Xcode 在 Debug 模式下定义了宏 DEBUG=1 ,所以我们可以在代码中把相关的测试代码写在预编译处理命令 **\#ifdef DEBUG... \#endif** 中间即可,如图所示 - -![DEBUG宏在头文件](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180626-144101@2x.png) - -![DEBUG宏在代码块](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180626-144240@2x.png?raw=true) - -* 测试用的 .a 和 .framework - - 对于拖拽到工程中的 .a .framework 静态库,可以在 **target->Build Settings->Search Paths**这2个选项,分别设置 **Library Search Paths**和**Framework Search Paths**这2个选项。如果我们需要在测试的时候会用到,那么我们可以将 **Debug** 对应的值留下,删掉**Release** 对应的值。这样我们打包 Release 包的时候就不会包含不需要的包。 - -![不需要的包删除即可](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180626-144819@2x.png?raw=true) - -* CocoPods 引入的库 - 对于 CocoPods 方式引入的库,在配置的时候就可以处理掉,比如下面的方式 - ``` - platform: iOS, '8.0' - ... - pod 'PonyDebugger', :configurations => ['Debug'] - ``` - -\(参考来自:「 iOS知识小集 」2018 · 第 18 期\) - diff --git a/第一部分 iOS/1.45.md b/第一部分 iOS/1.45.md deleted file mode 100644 index 393ba16..0000000 --- a/第一部分 iOS/1.45.md +++ /dev/null @@ -1,11 +0,0 @@ -# 调试小技巧 - -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) \ No newline at end of file diff --git a/第一部分 iOS/1.46.md b/第一部分 iOS/1.46.md deleted file mode 100644 index c17490f..0000000 --- a/第一部分 iOS/1.46.md +++ /dev/null @@ -1,75 +0,0 @@ -# Hybrid - - - -### 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调用的")`。 \ No newline at end of file diff --git a/第一部分 iOS/1.48.md b/第一部分 iOS/1.48.md deleted file mode 100644 index 0314054..0000000 --- a/第一部分 iOS/1.48.md +++ /dev/null @@ -1,57 +0,0 @@ -### 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; - } -} -``` - - - diff --git a/第一部分 iOS/chapter1.md b/第一部分 iOS/chapter1.md deleted file mode 100644 index 981a1c2..0000000 --- a/第一部分 iOS/chapter1.md +++ /dev/null @@ -1,6 +0,0 @@ - -# 第一部分 - -第一部分主要介绍 iOS 开发中遇到的问题或者有趣的知识 - - diff --git a/第三部分 后端技术/chapter3.md b/第三部分 后端技术/chapter3.md deleted file mode 100644 index 9e633db..0000000 --- a/第三部分 后端技术/chapter3.md +++ /dev/null @@ -1,5 +0,0 @@ -# 第三部分 - -第三部分主要记录在后端技术的经验或学习心得。包括 Node、PHP、Python等 - - diff --git a/第二部分 Web 前端/.DS_Store b/第二部分 Web 前端/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/第二部分 Web 前端/.DS_Store and /dev/null differ diff --git a/第二部分 Web 前端/2.9.md b/第二部分 Web 前端/2.9.md deleted file mode 100644 index a31fb93..0000000 --- a/第二部分 Web 前端/2.9.md +++ /dev/null @@ -1,241 +0,0 @@ - -> vue-cli作为vue的脚手架,可以帮助我们在实际开发中自动生成vue.js的模板工程。第一次使用这个工具,记录下步骤 - - - -前提:需要 vue 和 webpack - -1、全局安装 vue-cli - -``` -npm install vue-cli -g -``` - -2、初始化 - -``` -vue init webpack 项目名称 -``` - -学过 Node 的人都知道此时会生成一个 package.json 文件,需要你输入或者选择一些工程信息。 - -3、进入项目文件夹 - -``` -cd 项目名称 -``` - -4、然后启动项目 - -``` -npm run dev -``` - - - - - -## 关键点讲解 - -1、程序主要入口是 main.js - -``` -import Vue from 'vue' -import App from './App' - -Vue.config.productionTip = true - -/* eslint-disable no-new */ -new Vue({ -el:"#app", -components:{App}, -template:'' -}) -``` - -* 可以看到注册了 App 组建(Vue 是组件化思想) - -2、App.vue - -``` - - - - - -``` - -* 导入了 HelloWorld 模块,并注册为一个组件。 -* 其中看到一个 name ,只有作为组件选项时起作用。允许组件模板递归地调用自身。注意,组件在全局用`Vue.component()`注册时,全局 ID 自动作为组件的 name。 -指定`name`选项的另一个好处是便于调试。有名字的组件有更友好的警告信息。另外,当在有[vue-devtools](https://github.com/vuejs/vue-devtools),未命名组件将显示成``,这很没有语义。通过提供`name`选项,可以获得更有语义信息的组件树。 - -* <HelloWorld /> 这样写就是将注册好的组件使用(可以看成是搭积木) - -3、HelloWorld.vue - -``` - - - - - - - -``` - -* 做为组件,对外暴露了 name -* {{msg}} 绑定了数据 - -效果图 - -![效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Vue-20180225-1.png) - -## 实验 - -有了基本的了解,我们参考 vue 的组件化思想,动手做一个展示 编程语言兴趣的小组件。 - - - -``` -//Hobby.vue - - - - - - - - - -//HelloWorld.vue - - - - - - - -``` - -效果图 - -![添加了Hobby组件](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Vue-20180225-2.png) - diff --git a/第二部分 Web 前端/chapter2.md b/第二部分 Web 前端/chapter2.md deleted file mode 100644 index fad7b44..0000000 --- a/第二部分 Web 前端/chapter2.md +++ /dev/null @@ -1,4 +0,0 @@ -# 第二部分 - -第二部分主要介绍 Web 前端开发中遇到的问题或者有趣的知识 - diff --git a/第五部分 开发杂谈/5.1.md b/第五部分 开发杂谈/5.1.md deleted file mode 100644 index e9cbc68..0000000 --- a/第五部分 开发杂谈/5.1.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 开始hexo博客之旅 -date: 2017-10-10 14:04:27 -tags: home ---- - - -### 首先列一下关键的几个步骤,网站的教程特别多。要看的自己搜一搜 - -1、下载安装node环境 -2、本机配置git环境 -3、node安装hexo环境 -4、下载自己细化的主题,我选择的是next主题 -5、更改next主题文件夹下的 _config.yml 文件,做一些常用设置的更改 -6、开始写文章,格式采用markdown语法,写完后发布一下 hexo d - - -### 开启订阅 - -1、下载安装hexo插件 npm install hexo-generator-feed --save -2、修改根目录_config.yml配置 -``` -# RSS -plugin: hexo-generator-feed -#Feed Atom -feed: -type: atom -path: atom.xml -limit: 20 -``` -3、修改主题_config.yml配置 -``` -rss: /atom.xml -``` diff --git a/第五部分 开发杂谈/5.4.md b/第五部分 开发杂谈/5.4.md deleted file mode 100644 index 6bb413a..0000000 --- a/第五部分 开发杂谈/5.4.md +++ /dev/null @@ -1,30 +0,0 @@ -### git常见使用 - -``` -http://blog.csdn.net/wirelessqa/article/details/20153689 -``` - -1. git init -2. git add . -3. git commit -am "\#\#\#" -4. git remote add origin git@xx.xx.xx.xx:repos/xxx/xxx/xxx.git -5. git push origin 本地分支:远程分支 - fatal: refusing to merge unrelated histories - 决办法:git pull origin master --allow-unrelated-histories - - - -### 创建新版本 - -* 从现有的分支创建新分支 - * git checkout -b dev -* 将新创建的分支提交到远端 - * git push origin dev -* 提交代码 - * git add . - * git commit -m 'add a new branch named dev' - * git pull origin dev - * git push origin dev - - - diff --git a/第五部分 开发杂谈/chapter5.md b/第五部分 开发杂谈/chapter5.md deleted file mode 100644 index d7d5ad1..0000000 --- a/第五部分 开发杂谈/chapter5.md +++ /dev/null @@ -1,4 +0,0 @@ -# 第四部分 - -第四部分主要记录在开发中遇到的工具经验或者效率工具总结。 - diff --git a/第四部分 数据库/.DS_Store b/第四部分 数据库/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/第四部分 数据库/.DS_Store and /dev/null differ