docs: 移动端质量保障

This commit is contained in:
LiuBinPeng
2022-03-24 09:28:13 +08:00
parent d3b2102dc9
commit 5f1070bf6b
6 changed files with 265 additions and 35 deletions

108
Chapter1 - iOS/1.101.md Normal file
View File

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

View File

@@ -11,8 +11,7 @@
5. 范围编辑。多光标是个很棒的并且每个高级的编辑器都该有的特训过,快捷键为**Command+Control+E**。将光标移动刀需要编辑的符号,输入快捷键,然后就可以在当前页面全局编辑了。
6. Xcode 设置代码只在 Debug 下起效的几种方式
在日常开发中 Xcode 在 Debug 模式下写很多测试代码,或者引入一些第三方测试用的 .a 和 .framework 动态库,也会通过 CocoaPods 引入一些第三方测试工具或者库;但是不希望这些库在**Release**正式包中被引入,如何做到呢?
在日常开发中 Xcode 在 Debug 模式下写很多测试代码,或者引入一些第三方测试用的 .a 和 .framework 动态库,也会通过 CocoaPods 引入一些第三方测试工具或者库;但是不希望这些库在**Release**正式包中被引入,如何做到呢?
* .h/.m 文件中的测试代码
Xcode 在 Debug 模式下定义了宏 DEBUG=1 ,所以我们可以在代码中把相关的测试代码写在预编译处理命令 **\#ifdef DEBUG... \#endif** 中间即可,如图所示
@@ -25,47 +24,47 @@
对于拖拽到工程中的 .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 里面进行设置。
```
<key>ITSAppUsesNonExemptEncryption</key>
```
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
```
```
8. 让 Xcode 折叠代码
在 VS Code 或者其他 IDE 里面都具有代码折叠的功能Xcode 也支持代码折叠功能,但是默认没有开启。所以我们需要做的就是打开代码折叠功能。步骤:打开 Xcode - Preference - Text Editing - 在「Show」模块下面勾选「Code folding ribbon」。这样 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.");
```
@@ -77,11 +76,11 @@
- + (NSString *)getWeiboAppSupportMaxSDKVersion __attribute__((deprecated));
- #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
result = [self sizeWithFont:font constrainedToSize:size lineBreakMode:lineBreakMode];
#pragma clang diagnostic pop
#pragma clang diagnostic pop
10. Xcode Instruments 内存泄漏检测工具 Leaks 在内存检测后,无法看到具体的堆栈信息。
![Leaks](./../assets/2020-11-25-InstrumentMemoryLeaks.jpg)
@@ -92,4 +91,23 @@
DWARF即 ***Debug With Arbitrary Record Format*** ,是一个标准调试信息格式,即调试信息。这部分信息可以查看我的[这篇文章](./1.74.md)中讲 iOS 符号化的部分。
11.
11. 将 OC 代码还原为 C++ 代码
```objectivec
// 方法1
clang -rewrite-objc xxxx.m -o xxxx.cpp
// 方法2
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxxx.m -o xxxx.cpp
```
12. 工程打开汇编Debug 更多信息
菜单栏Xcode -> Debug -> Debug Workflow -> Always Show Disassembly 可以查看汇编。
查看汇编可以从更深层了解当前函数的汇编层面的执行,为 objc 源码分析提供信
息避免方向性错误,结合 memory read 可以更清楚的看到寄存器之间是如何互相配合
处理配合的;使用汇编查看流程,可以在不确定源码出处和执行流程的情况下,跟踪内
部代码,并可以找到出处!同时,结合下符号断点的方式,能够更清晰的跟踪源码实
现。
13.

View File

@@ -1,4 +1,4 @@
# App 质量把控
# 客户端质量把控
> 笔者结合中台经验,本文重点谈谈 App 的质量稳定性该如何做。业务作为 App 的核心服务之一,业务异常监控当然也很重要,这不是本文重点。
@@ -86,3 +86,108 @@ QA 指派的测试用例一定要冒烟通过,冒烟打回很严重的,这
## SDK 质量 CheckList
- ChangeLog、Podspec、Readme 完善
- BetterMR +3 机制深入贯彻(一名角色为项目的另一端同学,另一名角色为本技术栈更资深的老司机)
- 冒烟通过率100%(假如技术项目、日常优化可以交叉测试)
- 精准测试以及精准测试报告分析。代码行覆盖率至少80%
- 高铁包回归阶段UI 自动化点击页面发现性能APM与稳定性问题Crash、业务异常天网报警监控之前都是忽略未上线前高铁阶段的质量问题的提前感知问题提前修复减少线上问题
- 业务 SDK 正式发布阶段,业务线接入升级时,工程师需充当 QA 角色,评估业务影响面,数据需要全面评估(新老版本兼容性、灰度策略),产出 SDK 性能测试报告和影响面报告
技术 SDK 质量 CheckList
- ChangeLog、Podspec、Readme 完善SDK 发布必须 Lint 通过
- BetterMR +3 机制深入贯彻(一名角色为项目的另一端同学,另一名角色为本技术栈更资深的老司机)
- 新开的 SDK 必须写单测覆盖率90%以上
- 对于存量 SDK 可以通过 BDD 补充测试用例,不如 APM 卡顿测试可以在10s内 Mock 3次卡顿。假设普通卡顿临界值为0.2s严重卡顿为1sANR为5s手动Mock3次卡顿分别为0.3s、2s、6s基于 BDD 我们可以对监控结果进行判断比如断言抓到3次卡顿其中2次严重卡顿、1次 ANR
- 开发阶段必须关心性能:内存、电量、卡顿、网络。用 Instrucments 测试
- 冒烟通过率100%(假如技术项目、日常优化可以交叉测试)
- 高铁包回归阶段UI 自动化点击页面发现性能APM与稳定性问题Crash、业务异常天网报警监控之前都是忽略未上线前高铁阶段的质量问题的提前感知问题提前修复减少线上问题
- 业务 SDK 正式发布阶段,业务线接入升级时,工程师需充当 QA 角色,评估业务影响面,数据需要全面评估(新老版本兼容性、灰度策略),产出 SDK 性能测试报告和影响面报告
## SDK 测试方法
客户端SDK是为第三方开发者提供的软件开发工具包包括 SDK 接口、开发文档和 Demo 示例等。SDK 和应用之间的关系?以 IM 为例App 调用 IM SDK 接口。进行客服消息功能模块的接入,也包括消息 PUSH 功能的使用方。包括 Weex、JS 等资源的更新、商品数据更新 PUSH 后端上的感知能力、智能经营消息等。
### 客户端 SDK 测试的对象
客户端 SDK 测试,就是对提供给开发者的工具包里面的内容进行测试
因此测试的主要内容有:
1. SDK 接口和文档
- SDK 接口是测试的主要对象,也是核心的内容
2. SDK 日志
对开发者来说SDK 接口里面的具体实现是透明的,当上层调用时遇到问题,只能依赖 SDK 打印的日志来定位分析。所以 SDK 日志是否完备,是否有助于解决问题,对应用开发者和 SDK 提供方来说都很重要
3. Demo 或行业解决方案
Demo 测试可以看成是基于行为的测试。Demo 是SDK提供方用来示例如何调用接口实现具体的功能也可以作为开发者直观感受SDK接入效果。行业解决方案类似 Demo但是比 Demo 更加像一个产品,具有比较完整和典型的行业应用场景。可以让行业开发者比较明确知道,接入这个 SDK 做出来的产品效果如何。
4. 其他周边
比如UIkit等可能只是在SDK开发中的附带输出但对有的开发者来说能极大降低接入成本
### 客户端SDK接口测试类型
客户端SDK根据需求和开发平台不同可能需要选择不同的测试类型对SDK接口进行测试
常见的测试类型有:
1. 功能测试
保证 SDK 接口功能正确性和完备性。客户端 SDK 接口测试跟服务端接口测试类似,包括场景覆盖和接口参数覆盖
主要测试各种参数组合下的返回值,考虑数据是否缓存与存储,是否有回调,对于请求成功或失败都能按预期进行处理
2. 性能测试
保证 SDK 接口满足特定的性能需求,比如资源占用、移动设备耗电量等。比如 APM 在卡顿定制抓取堆栈的时候会对设备有内存和 CPU 的影响,如果全量抓取一次堆栈会更加耗时,如果异步抓取主线程堆栈。实现不好,很有可能在发生卡顿的时候,由于 APM 实现不好,导致本来的卡顿变成了 OOM。所以测试时就需要考虑这个场景的性能
3. 兼容性测试
保证 SDK 兼容特定的设备平台,并与其他软件兼容。兼容设备平台的工作量通常是比较大的,先根据产品需求和市场现状对需要适配的设备平台做分析,再根据需要覆盖的机型、系统版本、分辨率等进行优先覆盖排序
移动端 SDK 兼容性测试需要考虑下对模拟器的支持,因为很多开发者可能就是先在模拟器上开发。客户端 SDK 覆盖多平台设备的,还要考虑多端消息数据包的互通
4. 稳定性测试
考察业务场景在一定压力下,持续运行一段时间,接口功能和设备资源占用有无异常。比如早期做 APM 时候,参考腾讯 Matrix 的代码,居然线上发生了 OOM最后二分法排查代码改动居然定位到了 CPU 利用率获取代码中,有 c 对象没有 free所以代码的稳定性也是需要测试去考虑和关注的。
5. 网络相关测试
保证在不同网络类型不同网络环境下SDK 接口都能较好的处理。在涉及到多媒体资源或音视频通信,弱网下测试的需求较多,并且弱网下的处理通常需要反复优化和对比,不仅是新老版本效果对比,还包括竞品的效果对比测试
6. 安全性测试
对隐私数据保护访问权限的控制用户服务鉴权等SDK 接口的安全性问题也是比较突出。安全性很多是在架构设计和开发设计中就考虑进去,但是最好还是有专门的安全性测试
上述诸多测试类型中功能测试先行。在进行客户端SDK测试前需要全面的了解测试对象的细节
- 了解业务流程结合API接口文档和开发指南理顺接口的使用场景和调用关系
- 了解 SDK 协议,理解协议中字段的意义以及服务器端的处理逻辑;
- 了解各接口或协议返回码,分析对应的场景;
- 了解开发实现细节,可以绘制成图,便于测试分析和分层验证。
对客户端 SDK 进行测试,可以采用的分层测试方式由上至下依次有:基于 Demo 和解决方案->基于接口调用->基于代码。
1、基于 Demo 和解决方案的测试
大多客户端SDK在提测时都会有对应的Demo或者解决方案提交给测试因此可以覆盖到该Demo或解决方案对应的接口或业务场景。而且测试人员可以比较直观的看到界面表现上手快所以在客户端SDK测试中比较常用也是比较有效的。
但这种测试方式的缺点也很多Demo对接口和业务场景覆盖比较有限对接口的输入输出参数不能全覆盖发现问题时定位复杂度增加。精心设计的Demo以及多解决方案的形式或许可以最大程度满足测试需要但是需要较大的Demo开发测试投入也使得问题暴露的时间大大滞后。基于Demo和解决方案的测试可以是手工的也可以是UI层自动化测试。
2、基于接口调用的自动化测试
基于接口调用的测试,包括对单个接口的测试,也包括业务场景的覆盖。这种测试方式直接有效,需要一定开发基础
比如 SDK Repo功能实现代码 + XCTest Case 代码),通过脚本同步 SDK 代码到测试 Repo主要包括SDK 功能实现代码 + XCTest Case 代码 + QA 的 XCTest 代码Special TestCase + Normal TestCase
其中,开发的测试 Case 走基础单元测试。QA 的 Special Test Case专项测试的测试 Case最小回归集等日常测试的测试 Case
基于接口调用的自动化测试需要有产品的思路、开发的知识和测试的思维做起来有难度。但是因为SDK接口通常比较稳定所以一旦实现并投入使用测试效率和质量的收益都很大值得拥有。
3、基于代码的单元测试
单元测试是为开发代码质量保驾护航的一个重要环节,在测试左移推进的道路上,大家越来越意识到单元测试的重要价值。特别是在一些核心业务上,值得开发同学投入精力去做。
其他测试类型的展开,跟应用层测试类似,就不再重复了。

View File

@@ -102,4 +102,4 @@
* [96、一个提高 App 运算性能的想法](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.96.md)
* [97、__attribute__ 的骚操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.97.md)
* [98、前端、BFF、后端一些常见的设计模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.98.md)
* [99、App 质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md)
* [99、客户端质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md)

View File

@@ -848,5 +848,4 @@ app.listen(33855)
App 包体积的治理方案可以查看 [App瘦身之道](./../ Chapter1\ -\ iOS/1.60.md) 这篇文章。目的是通过 Electron 这个技术打造有赞自己的移动潘多拉魔盒囊括必要的各种能力所以【App 包体积】这个命题可以结合 Electron将包大小检测能力作为魔盒的能力之一。
2. 桌面端技术选型的时候现在多了一些选择Electron、[Tauri](https://github.com/tauri-apps/tauri)、ImGui。其中 Tauri 就是 WebView + Rust 的实现。ImGui 是一个 C/C++ 实现的即时渲染框架。

View File

@@ -101,7 +101,7 @@
* [96、一个提高 App 运算性能的想法](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.96.md)
* [97、__attribute__ 的骚操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.97.md)
* [98、前端、BFF、后端一些常见的设计模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.98.md)
* [99、App 质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md)
* [99、客户端质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.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)