feat: 精准测试(OC、Swift)
@@ -518,6 +518,10 @@ using namespace std;
|
||||
using namespace llvm;
|
||||
using namespace clang::ast_matchers;
|
||||
|
||||
#define CodeStyleValidateMethodDeclaration "ObjCMethodDecl"
|
||||
#define CodeStyleValidatePropertyDeclaration "ObjcPropertyDecl"
|
||||
#define CodeStyleValidateInterfaceDeclaration "ObjCInterfaceDecl"
|
||||
|
||||
namespace CodeStyleValidatePlugin {
|
||||
// 自定义 handler
|
||||
class CodeStyleValidateHandler : public MatchFinder::MatchCallback {
|
||||
@@ -533,10 +537,20 @@ namespace CodeStyleValidatePlugin {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 判断属性是否需要用 Copy
|
||||
bool isShouldUseCopyAttribute(const string typeStr) {
|
||||
if (typeStr.find("NSString") != StringRef::npos ||
|
||||
typeStr.find("NSArray") != StringRef::npos ||
|
||||
typeStr.find("NSDictionary") != StringRef::npos
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检测类名
|
||||
void validateInterfaceDeclaration(const ObjCInterfaceDecl *decl) {
|
||||
StringRef className = decl->getName();
|
||||
|
||||
// 判断首字母不能以小写开头
|
||||
char c = className[0];
|
||||
if (isLowercase(c)) {
|
||||
@@ -598,10 +612,11 @@ namespace CodeStyleValidatePlugin {
|
||||
ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
|
||||
SourceLocation location = propertyDecl->getLocation();
|
||||
string typeStr = propertyDecl->getType().getAsString();
|
||||
string propertyName = propertyDecl->getNameAsString();
|
||||
|
||||
// 判断string需要使用copy
|
||||
if ((typeStr.find("NSString")!=string::npos)&& !(attrKind & ObjCPropertyAttribute::Kind::kind_copy)) {
|
||||
showWaringReport(location, "☠️ 杭城小刘提示你:NSString 建议使用 copy 代替 strong ⚠️", NULL);
|
||||
// 判断 Property 需要使用 copy
|
||||
if (isShouldUseCopyAttribute(typeStr) && !(attrKind & ObjCPropertyAttribute::Kind::kind_copy)) {
|
||||
showWaringReport(location, "☠️ 杭城小刘提示你:建议使用 copy 代替 strong ⚠️", NULL);
|
||||
}
|
||||
|
||||
// 判断int需要使用NSInteger
|
||||
@@ -616,7 +631,7 @@ namespace CodeStyleValidatePlugin {
|
||||
}
|
||||
|
||||
// 检测方法
|
||||
void validateMethodDeclaration(const clang::ObjCMethodDecl *methodDecl) {
|
||||
void validateMethodDeclaration(string fileName, const clang::ObjCMethodDecl *methodDecl) {
|
||||
// 检查名称的每部分,都不允许以大写字母开头
|
||||
Selector sel = methodDecl -> getSelector();
|
||||
int selectorPartCount = methodDecl -> getNumSelectorLocs();
|
||||
@@ -685,33 +700,27 @@ namespace CodeStyleValidatePlugin {
|
||||
|
||||
// 主要方法,分配 类、方法、属性 做不同处理
|
||||
void run(const MatchFinder::MatchResult &Result) override {
|
||||
if (const ObjCInterfaceDecl *interfaceDecl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) {
|
||||
if (const ObjCInterfaceDecl *interfaceDecl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>(CodeStyleValidateInterfaceDeclaration)) {
|
||||
string filename = ci.getSourceManager().getFilename(interfaceDecl->getSourceRange().getBegin()).str();
|
||||
if(isDeveloperSourceCode(filename)){
|
||||
std::string tempName = interfaceDecl->getNameAsString();
|
||||
cout << "ObjCInterfaceDecl" + tempName << endl;
|
||||
// 类的检测
|
||||
validateInterfaceDeclaration(interfaceDecl);
|
||||
}
|
||||
}
|
||||
|
||||
if (const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("ObjcPropertyDecl")) {
|
||||
if (const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>(CodeStyleValidatePropertyDeclaration)) {
|
||||
string filename = ci.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
|
||||
if(isDeveloperSourceCode(filename)) {
|
||||
std::string tempName = propertyDecl->getNameAsString();
|
||||
cout << "ObjcPropertyDecl" + tempName << endl;
|
||||
// 属性的检测
|
||||
validatePropertyDeclaration(propertyDecl);
|
||||
}
|
||||
}
|
||||
|
||||
if (const ObjCMethodDecl *methodDecl = Result.Nodes.getNodeAs<ObjCMethodDecl>("ObjCMethodDecl")) {
|
||||
if (const ObjCMethodDecl *methodDecl = Result.Nodes.getNodeAs<ObjCMethodDecl>(CodeStyleValidateMethodDeclaration)) {
|
||||
string filename = ci.getSourceManager().getFilename(methodDecl->getSourceRange().getBegin()).str();
|
||||
if(isDeveloperSourceCode(filename)) {
|
||||
std::string tempName = methodDecl->getNameAsString();
|
||||
cout << "ObjcMethodDecl" + tempName << endl;
|
||||
// 方法的检测
|
||||
validateMethodDeclaration(methodDecl);
|
||||
validateMethodDeclaration(filename, methodDecl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -723,11 +732,11 @@ namespace CodeStyleValidatePlugin {
|
||||
MatchFinder matcher;
|
||||
CodeStyleValidateHandler handler;
|
||||
public:
|
||||
//调用CreateASTConsumer方法后就会加载Consumer里面的方法
|
||||
//调用 CreateASTConsumer 方法后就会加载 Consumer 里面的方法
|
||||
CodeStyleValidateASTConsumer(CompilerInstance &ci) :handler(ci) {
|
||||
matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);
|
||||
matcher.addMatcher(objcMethodDecl().bind("ObjCMethodDecl"), &handler);
|
||||
matcher.addMatcher(objcPropertyDecl().bind("ObjcPropertyDecl"), &handler);
|
||||
matcher.addMatcher(objcInterfaceDecl().bind(CodeStyleValidateInterfaceDeclaration), &handler);
|
||||
matcher.addMatcher(objcMethodDecl().bind(CodeStyleValidateMethodDeclaration), &handler);
|
||||
matcher.addMatcher(objcPropertyDecl().bind(CodeStyleValidatePropertyDeclaration), &handler);
|
||||
}
|
||||
|
||||
// 遍历完一次语法树就会调用一次下面方法。该方法通常被用来处理整个翻译单元的 AST,进行进一步的分析、处理或者其他操作。在处理完整个 AST 后,开发者可以在这个方法中执行他们需要的操作,比如生成代码、执行静态分析、进行重构等。
|
||||
@@ -742,7 +751,7 @@ namespace CodeStyleValidatePlugin {
|
||||
public:
|
||||
// 需要返回一个 Consumer,所以继续创建一个继承自 ASTConsumer 的 Consumer
|
||||
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) override {
|
||||
return unique_ptr<CodeStyleValidateASTConsumer> (new CodeStyleValidateASTConsumer(ci));//使用自定义的处理工具
|
||||
return unique_ptr<CodeStyleValidateASTConsumer> (new CodeStyleValidateASTConsumer(ci)); // 使用自定义的处理工具
|
||||
}
|
||||
|
||||
bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) override {
|
||||
@@ -751,7 +760,7 @@ namespace CodeStyleValidatePlugin {
|
||||
};
|
||||
}
|
||||
|
||||
// 注册插件,告诉 LLVM 插件对应的 Action 是 CodeStyleValidatePlugin
|
||||
// 注册插件,告诉 LLVM 插件对应的 Action 是 FANAction
|
||||
static FrontendPluginRegistry::Add<CodeStyleValidatePlugin::ValidateCodeStyleAction>
|
||||
X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles, powered by @FantasticLBP");
|
||||
```
|
||||
@@ -782,7 +791,32 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
|
||||
|
||||
- 使用开源库 [LIEF](https://github.com/lief-project/LIEF) 的能力
|
||||
- 脚本 Python、Node glob 模块的快速匹配能力
|
||||
- 添加 Xcode 环境变量 `OBJC_PRINT_REPLACED_METHODS`,运行时候会打印出来
|
||||
- 添加 Xcode 环境变量 `OBJC_PRINT_REPLACED_METHODS`,运行时候会打印出来。参考[官方文档](https://developer.apple.com/library/archive/qa/qa1908/_index.html)
|
||||
|
||||
此处再引申聊聊命名规范的事情。[官方文档](https://developer.apple.com/library/archive/qa/qa1908/_index.html) 也说了 Category 命名的的最佳实践
|
||||
|
||||
> ## Category Method Name Best Practice
|
||||
>
|
||||
> It is not possible to tell whether a given method name will conflict with an existing method defined by the original class because classes often contain private methods that are not listed in the classes interface. Further, a future version of the class may add new methods that clash with methods previously defined in your category. In order to avoid undefined behavior, it’s best practice to add a prefix to method names in categories on framework classes, just like you should add a prefix to the names of your own classes. You might choose to use the same three letters you use for your class prefixes, but lowercase to follow the usual convention for method names, then an underscore, before the rest of the method name.
|
||||
|
||||
简单来说,虽然有些类的方法在 `.m` 中可能存在10个方法,但在 `.h` 中公开了3个方法,然后在迭代的过程中,可能另一个对象也新增了3个方法,这3个方法可能是公开的也可能是私有方法,由于大家都遵循常见的 OC 命名策略(见名知意)所以很容易造成命名 冲突。给 Category 或者动态库、静态库命名最好带前缀,以避免方法冲突。这个好处不只是命名规范上的,更是代码逻辑安全出发的,由于 OC 强大的 Runtime 消息机制,重名的方法容易被调用。
|
||||
|
||||
官方给的例子
|
||||
|
||||
```objective-c
|
||||
@interface UIView (MyCategory)
|
||||
|
||||
// CORRECT: The method name is prefixed.
|
||||
- (BOOL)wxyz_isOccludedByView:(UIView*)otherView;
|
||||
|
||||
// INCORRECT: The method name is not prefixed. This method may clash with an existing method in UIView.
|
||||
- (BOOL)isOccludedByView:(UIView*)otherView;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
除了 CI、CD 最后一道防线的拦截外,事前,团队内宣讲统一代码风格,Code Review 阶段看到 Category 方法命名不合理的地方,即使给出严厉的 Comment,也能拦截和规范一部分情况。
|
||||
|
||||
- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClangASTCategoryMethod.png" style="zoom:20%">
|
||||
@@ -791,7 +825,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
|
||||
|
||||
### Pass 插桩,实现精准测试
|
||||
|
||||
|
||||
这部分涉及到 Objective-C 和 Swift 的代码插桩逻辑的不同实现,篇幅较大,可以查看这篇文章 [精准测试最佳实践](./1.108.md)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,829 @@
|
||||
# 精准测试
|
||||
# 精准测试最佳实践
|
||||
|
||||
|
||||
|
||||
## 背景
|
||||
|
||||
下面这张图是22年整理的我们移动中台对于质量的一些把控手段,也是一个有效的 checklist。对于一个业务项目或者技术项目来说,QA 给的测试用例全部通过不能说明代码没有问题。单测覆盖率的最佳实践是针对于基础 SDK,对于业务侧的代码来说,由于经常变化,所以还是以人工测试为主,一些核心的不变的核心链路,沉淀出 UI 自动化用例,每周迭代的时候,交付测试后,开始 UI 自动化回归。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeQualityChecklist.png" style="zoom:25%">
|
||||
|
||||
但,这些回归还是不能 cover 所有问题,我们需要为我们写的每行代码买单,如何衡量每行代码的效果呢?这就是精准测试要做的事情。
|
||||
|
||||
iOS 工程来说,跨端项目暂时不在本文范畴,Native 侧主要是 OC 和 Swift 为主。本文将会从 OC/ Swift 2个技术栈展开说说如何获取精准测试覆盖率报告。
|
||||
|
||||
|
||||
|
||||
## Objective-C 代码覆盖率
|
||||
|
||||
### 理论分析
|
||||
|
||||
#### 覆盖率检测原理
|
||||
|
||||
统计代码覆盖率的实现抓手就是对代码进行插桩,OC 是 C 语言的一个超集,而 LLVM 诞生自 GCC,我们可以使用 GCC 的插桩器对 OC 代码进行编译插桩,具体流程如下:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClangStubFullProgress.png" style="zoom:65%">
|
||||
|
||||
在编译阶段指定 `-fprofile-arcs` `-ftest-coverage` 等测试选项,LLVM 会做这么几件事:
|
||||
|
||||
- 在输出目标文件中留出一段存储区保存统计数据
|
||||
|
||||
打开一个插桩工程,查看 MachO 文件可以印证。可以看到 `__llvm_prf_cnts`、`__llvm_prf_data` 、`__llvm_prf_names`、`__llvm_prf_vnds`、`__llvm_covfun`、`__llvm_covmap` 等 section 就是存储插桩信息的空间。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOSpaceToRecordCodeInstrucment.png" style="zoom:25%">
|
||||
|
||||
- 在源代码中为每个 Basic Block 进行插桩(Basic Block 下文会讲)
|
||||
|
||||
可以看到 `showAssets` 方法内存在一个 if,即2个 Basic Block,所以通过汇编查看的话,存在2个插桩点。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeAssemblyProveBasicBlockCodeInstrument.png" style="zoom:20%">
|
||||
|
||||
- 产生 `.gcno` 文件,它包含 Basic Block 和相应的源码行号信息
|
||||
|
||||
- 在最终可执行文件中,进入 main 函数之前调用 `gcov_init` 内部函数初始化统计数据区,并将 `gcov_init` 内部函数注册为`exit_handers`,用户代码调用 exit 正常结束时,`gcov_exit` 函数得到调用,并继续调用 `__gcov_flush` 输出统计数据到 `.gcda` 文件。
|
||||
|
||||
|
||||
|
||||
生成覆盖率报告,首先需要在 Xcode 中配置编译选项,编译后会为每个可执行文件生成对应的 **`.gcno`** 文件;之后在代码中调用覆盖率分发函数,会生成对应的 **`.gcda`** 文件。
|
||||
|
||||
其中,`.gcno` 包含了代码计数器和源码的映射关系, `.gcda` 记录了每段代码具体的执行次数。覆盖率解析工具需要结合这两个文件给出最后的检测报表。
|
||||
|
||||
|
||||
|
||||
#### .gcno
|
||||
|
||||
利用 Clang 分别生成源文件的 AST 和 IR 文件,对比发现,AST 中不存在计数指令,而 IR 中存在用来记录执行次数的代码。查看 LLVM 源码可以看到 `GCDAProfiling.c` ,该文件主要作用是:覆盖率映射关系生成源码。
|
||||
|
||||
覆盖率映射关系生成源码是 LLVM 的一个 Pass,用来向 IR 中插入计数代码并生成 .gcno 文件(关联计数指令和源文件)。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMCodeCoverageIRPass.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### Basic Block
|
||||
|
||||
从编译器角度出发,基本块(Basic Block,BB)是代码执行的基本单元,LLVM 基于 BB 进行覆盖率计数指令的插入。BB 特点是:
|
||||
|
||||
- 只有1个入口
|
||||
- 只有1个出口
|
||||
- 只要 BB 中第一条指令被执行,那么 BB 中所有指令都会按顺序执行1次
|
||||
|
||||
1个 BB 中,不包含其他的 jump/return/if/switch 等流程控制语句,也就是一个最小可执行单元。
|
||||
|
||||
基本块 BB 是程序中一个顺序执行的**语句序列**,同一个 BB 中所有语句的执行次数一定相同,一般由多个顺序执行语句后跟一个跳转语句组成。
|
||||
|
||||
从一个 BB 到另一个 BB 的跳转称为一个 ARC。
|
||||
|
||||
|
||||
|
||||
#### GCOV 工作原理
|
||||
|
||||
如果跳转语句是有条件的,就产生了一个分支(ARC),该基本块就有2个基本块作为目的地。如果把每个基本块当作一个节点,那么一个函数中 的所有基本块就构成了一个有向图, 称之为基本块图. 只要知道 BB 或 ARC 的执行次数就可以推算出所有 的 BB 和所有的 ARC 的执行次数. GCOV 根据 BB 和 ARC 的统计情况来统计各 BB 内各行代码执行情况, 从而计算整个程序的覆盖率情况。
|
||||
|
||||
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/BasicBlockGraphics.png" style="zoom:70%">
|
||||
|
||||
|
||||
|
||||
也就是说插桩的数量和函数内的代码行数、函数数量都不是一一对应的关系。**插桩数量和 BB 个数一一对应**。
|
||||
|
||||
这样设计的好处是:BB 的概念存在已久,利用现有能力进行功能拓展(插桩分析覆盖率),而不是为每行原始代码都插桩,从而大大减少了可执行文件的大小并且提高了执行的速度,同时还能够精确分析到所有代码的执行情况。x
|
||||
|
||||
覆盖率计数指令的插入会进行两次循环,外层循环遍历编译单元中的函数,内层循环遍历函数的基本块。函数遍历仅用来向 `.gcno` 中写入函数位置信息。
|
||||
|
||||
对下面方法展示控制流程图展示:
|
||||
|
||||
```objective-c
|
||||
- (void)showAssets {
|
||||
NSLog(@"I am a rich man");
|
||||
if (self.name) {
|
||||
[self.cat play];
|
||||
} else {
|
||||
NSLog(@"I am nobody");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeBasicBlockFlow.png" style="zoom:40%">
|
||||
|
||||
|
||||
|
||||
#### .gcon 计数符号和文件位置关联信息
|
||||
|
||||
`.gcon` 文件存储着计数插桩位置和源文件之间的关联信息。`GCOVPass` 通过2层循环插入计数指令的同时,会将文件及 BB 信息写入 `.gcon` 文件。
|
||||
|
||||
- 创建 `.gcno` 文件,写入 Magic number(oncg + version)
|
||||
- 随着函数遍历写入文件地址、函数名和函数在源文件中的起止行数(标记文件名,函数在源文件对应行数)
|
||||
- 随着 BB 遍历,写入 BB 编号、BB 起止范围、BB 的后继节点编号(标记基本块跳转关系)
|
||||
- 写入函数中BB对应行号信息(标注基本块与源码行数关系)
|
||||
|
||||
|
||||
|
||||
`.gcon` 文件由4部分组成:
|
||||
|
||||
- 文件结构
|
||||
- 函数结构
|
||||
- BB 结构
|
||||
- BB 行结构
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMBasicBlockStructure.png" style="zoom:40%">
|
||||
|
||||
|
||||
|
||||
#### .gcda
|
||||
|
||||
关于 `.gcda` 的逻辑可以查看源码的 [GCDAProfiling.c 文件](https://github.com/llvm/llvm-project/blob/main/llvm/lib/Transforms/Instrumentation/GCOVProfiling.cpp),也是覆盖率相关的核心逻辑。
|
||||
|
||||
```c++
|
||||
void GCOVProfiler::emitGlobalConstructor(
|
||||
SmallVectorImpl<std::pair<GlobalVariable *, MDNode *>> &CountersBySP) {
|
||||
Function *WriteoutF = insertCounterWriteout(CountersBySP);
|
||||
Function *ResetF = insertReset(CountersBySP);
|
||||
|
||||
// Create a small bit of code that registers the "__llvm_gcov_writeout" to
|
||||
// be executed at exit and the "__llvm_gcov_reset" function to be executed
|
||||
// when "__gcov_flush" is called.
|
||||
FunctionType *FTy = FunctionType::get(Type::getVoidTy(*Ctx), false);
|
||||
Function *F = createInternalFunction(FTy, "__llvm_gcov_init", "_ZTSFvvE");
|
||||
F->addFnAttr(Attribute::NoInline);
|
||||
|
||||
BasicBlock *BB = BasicBlock::Create(*Ctx, "entry", F);
|
||||
IRBuilder<> Builder(BB);
|
||||
|
||||
FTy = FunctionType::get(Type::getVoidTy(*Ctx), false);
|
||||
auto *PFTy = PointerType::get(FTy, 0);
|
||||
FTy = FunctionType::get(Builder.getVoidTy(), {PFTy, PFTy}, false);
|
||||
|
||||
// Initialize the environment and register the local writeout, flush and
|
||||
// reset functions.
|
||||
FunctionCallee GCOVInit = M->getOrInsertFunction("llvm_gcov_init", FTy);
|
||||
Builder.CreateCall(GCOVInit, {WriteoutF, ResetF});
|
||||
Builder.CreateRetVoid();
|
||||
|
||||
appendToGlobalCtors(*M, F, 0);
|
||||
}
|
||||
```
|
||||
|
||||
二进制代码加载时,调用了 `llvm_gcov_init(fn_ptr wfn, fn_ptr rfn)` 函数,传入了 `__llvm_gcov_writeout` 方法用于写 `.gcov` 文件,`__llvm_gcov_reset` 方法用于 reset 保存的数据。
|
||||
|
||||
然后 `emitGlobalConstructor ` 函数调用 `insertGlobalConstructorCode` 函数,后者负责插入全局构造函数所需的代码。`insertGlobalConstructorCode` 函数进一步调用 `initializeGCOVDataStructures` 函数和 `setupCodeCoverageEnvironment` 函数,分别用于初始化 `.gcov` 数据结构和设置代码覆盖率测试环境。
|
||||
|
||||
```c++
|
||||
COMPILER_RT_VISIBILITY
|
||||
void llvm_gcov_init(fn_ptr wfn, fn_ptr rfn) {
|
||||
static int atexit_ran = 0;
|
||||
|
||||
if (wfn)
|
||||
llvm_register_writeout_function(wfn);
|
||||
|
||||
if (rfn)
|
||||
llvm_register_reset_function(rfn);
|
||||
|
||||
if (atexit_ran == 0) {
|
||||
atexit_ran = 1;
|
||||
|
||||
/* Make sure we write out the data and delete the data structures. */
|
||||
atexit(llvm_delete_reset_function_list);
|
||||
#ifdef _WIN32
|
||||
atexit(llvm_writeout_and_clear);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
代码注释是 `__gcov_flush`(LLVM 老版本的 `__gcov_flush` )已经更新为 `__gcov_dump` ,调用 `__gcov_dump` 会将覆盖率信息写入文件。
|
||||
|
||||
```c++
|
||||
void __gcov_dump(void) {
|
||||
for (struct fn_node *f = writeout_fn_list.head; f; f = f->next)
|
||||
f->fn();
|
||||
}
|
||||
```
|
||||
|
||||
`.gcda` 文件/函数结构和 `.gcno` 基本一致,包含了弧跳变的次数和其他概要信息。利用 `gcov -f Person.gcda` 就可以可视化查看 `.gcda` 文件内容
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GcdaFileViaGcov.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
Xcode 导出 `.gcda` 的时候,断点查看汇编如下
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GcovDumpAssembly.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
#### .info 文件
|
||||
|
||||
拿到 `.gcno` 和 `.gcda` 文件后,我们可以使用 LCOV 工具(基于 gcov )来生成这个源代码文件的覆盖率信息。
|
||||
|
||||
覆盖率信息 `.info` 文件包含以下内容:
|
||||
|
||||
1. TN:测试用例名称
|
||||
2. SF:源码文件路径
|
||||
3. FN:函数名及行号
|
||||
4. FNDA:函数名及执行次数
|
||||
5. FNF:函数总数
|
||||
6. FNH:函数执行数
|
||||
7. DA:代码行及执行次数
|
||||
8. LF:代码总行数
|
||||
9. LH:代码执行行数
|
||||
|
||||
在增量覆盖率信息统计的步骤中,覆盖率信息文件新增了用于统计增量信息的字段:
|
||||
|
||||
1. CA:差异代码行及执行次数
|
||||
2. CF:差异代码行总数
|
||||
3. CH:差异代码行执行数
|
||||
|
||||
|
||||
|
||||
#### 完整流程
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GccCodeCoverageFlow.png" style="zoom:60%">
|
||||
|
||||
- 编译前, 在编译器中加入编译器参数 `-fprofile-arcs` `-ftest-coverage`
|
||||
- 源码经过编译预处理, 在生成汇编文件的阶段完成插桩,生成可执行文件,并且生成关联 BB 和跳转次数 ARC 的 `.gcno` 文件
|
||||
- 运行可执行文件,随着功能被执行,打点插桩的计数值不断更新,收集程序的执行信息
|
||||
- 生成具有 BB 和 ARC 的执行统计次数等数据的 `.gcda` 文件
|
||||
- 通过 lcov、genhtml 将代码覆盖率信息生成 html 格式的报告
|
||||
|
||||
|
||||
|
||||
### 工程实践
|
||||
|
||||
第一步,在 Xcode Build Settings 中,修改 Clang 编译参数 `Instrument Program Flow`、 `Generate Legacy Test Coverage File` 为 true,打开后即**开启插桩能力**。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeEnableCodeCoverageSetting.png" style="zoom:20%">
|
||||
|
||||
第二步,为了控制代码覆盖率保存的位置和文件名,需要我们设置一下 GCC 提供的环境变量
|
||||
|
||||
- `GCOV_PREFIX` 环境变量用于指定代码覆盖率文件的存储路径
|
||||
- `GCOV_PREFIX_STRIP `环境变量用于指定在存储路径中去除的前缀部分。
|
||||
|
||||
```objective-c
|
||||
NSString *covFilePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/coverage_files"];
|
||||
setenv("GCOV_PREFIX", [covFilePath cStringUsingEncoding: NSUTF8StringEncoding], 1);
|
||||
setenv("GCOV_PREFIX_STRIP", "100", 1);
|
||||
```
|
||||
|
||||
第三步,开启插桩后即拥有了原始 BB 信息,也开启了插桩。等待用户操作 App 后,即记录了 BB 执行信息,这些信息需要被写入 `.gcda` 中。早期版本是 `gcov_flush()`。可以看到 `_gcov_flush` 已经不能用了,发现官方已经是 `_gcov_dump` 。修改后编译通过。
|
||||
|
||||
```c++
|
||||
extern void __gcov_dump(void);
|
||||
__gcov_dump();
|
||||
```
|
||||
|
||||
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeBuildErrorWithGcovFlush.png" style="zoom:20%">
|
||||
|
||||
第四步,运行代码。完成测试后,我在屏幕点击事件里,将 BB 执行情况写入到 `.gcda` 中。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeShowGcdaPath.png" style="zoom:20%">
|
||||
|
||||
第五步,获取 `.gcno` 信息。编译器生成与源代码同名的 `.gcno` 文件(note file),这种文件含有重建基本块依赖图和将源代码关联至基本块及源代码行号的必要信息。
|
||||
|
||||
Xcode 选择 products,show In Finder。然后上上层的 `Intermediates.noindex` 目录存储,继续往下寻找,我个人电脑上路径为:`/Users/unix_kernel/Library/Developer/Xcode/DerivedData/CodeCoverageDemo-enpprvshxhvihgavktgzcmeoertf/Build/Intermediates.noindex/CodeCoverageDemo.build/Debug-iphonesimulator/CodeCoverageDemo.build/Objects-normal/x86_64`,存储了 `.gcno` 信息。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeGcnoFileLocation.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeGcnoFiles.png" style="zoom:100%">
|
||||
|
||||
|
||||
|
||||
第六步,将 `.gcno` 和 `.gcda` 文件,保存到一个文件夹下
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GcnoAndGcdaFilesInSameDir.png" style="zoom:40%">
|
||||
|
||||
|
||||
|
||||
第七步,利用 `lcov` 指令可以将 `.gcno` 文件和 `.gcda` 文件结合生成代码覆盖率结果 info 文件
|
||||
|
||||
指令格式为:`lcov -c -d . -o CodeCoverage.info` ,其中 `.` 代表当前目录
|
||||
|
||||
`CodeCoverage.info` 文件内容大概如下(各个字段代表什么上面 info 文件这一节有说明)。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LcovCombineGcovAndGcda.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
第八步,利用指令 `genhtml -o html CodeCoverage.info` 将 info 文件和源代码文件结合转化为可视化网页形式。
|
||||
|
||||
注意:执行 genhtml 指令必须保证和项目源代码(Xcode 项目叫 CodeCoverageDemo,源码则在 CodeCoverageDemo/CodeCoverageDemo 下)在同一文件夹下否则会报错。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LcovGenerateCoverageHTML.png" style="zoom:30%">
|
||||
|
||||
访问覆盖率路径为 html 目录下,和项目同名的文件夹里面的 `index.html`
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LcovCodeCoverageHTMLPath.png" style="zoom:25%">
|
||||
|
||||
第九步,通过类的列表,针对覆盖率低的文件,点进去看看,看看那些代码没有被执行。思考是什么原因造成的:
|
||||
|
||||
- if...else 代码是由于测试条件不满足,测试 case 不充足,导致另一个 case 没有被覆盖??
|
||||
- 某些兜底代码太多,根本走不到???
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeCoverageInClass.png" style="zoom:25%">
|
||||
|
||||
其中:蓝色部分代码已经执行的代码,橘色代表未执行的代码
|
||||
|
||||
第十步,假设我们在另一台设备上进行了测试,对剩余的测试任务内容进行完善,这个时候该怎么处理?Demo 以针对 Person 类的覆盖率完善为例。
|
||||
|
||||
1. 在另一台测试剩余 case 的机器上,执行测试流程。得到测试结果,即 `.gcda` 文件
|
||||
|
||||
2. 新建测试数据分析文件夹 `CodeCoverageAnalysis2`
|
||||
|
||||
3. 将上一步得到的 `.gcda` 文件拷贝到 ``CodeCoverageAnalysis2` 里面
|
||||
|
||||
4. 进入打包产物 App 所在文件夹,进入文件夹 `Build/Intermediates.noindex/CodeCoverageDemo.build/Debug-iphonesimulator/CodeCoverageDemo.build/Objects-normal/x86_64`,可以看到一堆类似 `AppDelegate.d`、 `AppDelegate.dia` `AppDelegate.gcno` 、`AppDelegate.o` 这样的文件。同样移动到 ``CodeCoverageAnalysis2` 里面
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeAnotherGcnoFiles.png" style="zoom:25%">
|
||||
|
||||
5. 在 `CodeCoverageAnalysis2` 目录下利用指令 `lcov -c -d . -o CodeCoverage2.info` 生成新的一份覆盖率信息 `CodeCoverage2.info`
|
||||
|
||||
6. 然后利用 `locv -a` 指令合并2个 `.info` 文件。指令为 `lcov -a CodeCoverage2.info -a ./../CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
|
||||
|
||||
7. 然后利用 `genhtml` 生成合并后的覆盖率可视化 html 文件 `genhtml -o html CodeCoverageCombined.info`
|
||||
|
||||
8. 查看分析最新的覆盖率报告
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCCodeCoverageInfoFileCombined.png" style="zoom:25%">
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCCodeCoverageCombinedReport.png" style="zoom:25%">
|
||||
|
||||
### 缺陷
|
||||
|
||||
Person 类的 showAssets 方法,内部有 Cat 相关逻辑,且 Cat 是 Swift 代码。为什么在代码覆盖列表上看不到 Cat?
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GCCCannotCaptureSwiftCodeCoverage.png" style="zoom:25%">
|
||||
|
||||
因为,clang 是一个基于 LLVM 的编译器前端,它可以编译多种编程语言,包括 C、C++、Objective-C 和 Objective-C++。然而,虽然 clang 本身基于 LLVM,但它并不是 Swift 语言的默认编译器。Swift 语言的官方编译器是 **swiftc**,是基于 LLVM 但专门为 Swift 语言设计的。
|
||||
|
||||
接下来看看 Swift 代码,如何获取代码覆盖率
|
||||
|
||||
|
||||
|
||||
### 工程化
|
||||
|
||||
工程化要解决的3个问题是:
|
||||
|
||||
- 一般来说,iOS 现在采用模块化的方式:壳工程 + 各个业务域子工程 + 3方模块。可通过 ruby 脚本修改壳工程和相应的业务工程的编译配置,开启编译插桩能力。一般对于 Debug 包来说不插桩,所以需要有个配置文件,来对各个模块进行配置。
|
||||
- 单个版本不断测试,生成的代码覆盖率信息如何合并
|
||||
- 多版本增量覆盖率
|
||||
- 打包平台及其服务侧
|
||||
|
||||
|
||||
|
||||
### 模块化配置
|
||||
|
||||
对于各个模块在什么模式下插桩的配置, `CodeCoverageConfig.rb`
|
||||
|
||||
```ruby
|
||||
ENABLE_PROJECTS = {
|
||||
"XXX/XXXPhone.xcodeproj" => "Enterprise",
|
||||
"XXXHD/XXXHD.xcodeproj" => "Enterprise",
|
||||
"Pods/XXXGoods.xcodeproj" => "Enterprise",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Ruby 脚本利用 [xcodeproj](https://github.com/CocoaPods/Xcodeproj) 对每个 target 的编译参数 `GCC_INSTRUMENT_PROGRAM_FLOW_ARCS` 、`GCC_GENERATE_TEST_COVERAGE_FILES`进行修改以开启插桩能力
|
||||
|
||||
```ruby
|
||||
require 'xcodeproj'
|
||||
CONFIG_DIR = Pathname.new(File.join(File.dirname(__FILE__), "../../../..")).realpath
|
||||
CONFIG_FILE = File.join(CONFIG_DIR, "CodeCoverageConfig.rb")
|
||||
|
||||
def update(args)
|
||||
enable = args[0] == "true" ? "YES" : "NO"
|
||||
debug = args[1] == "true" ? true : false
|
||||
load "#{CONFIG_FILE}"
|
||||
projects = ENABLE_PROJECTS
|
||||
projects.each do | proj, conf |
|
||||
proj_file = File.join(CONFIG_DIR, proj)
|
||||
project = Xcodeproj::Project.open(proj_file)
|
||||
project.build_configurations.each do |config|
|
||||
next if debug && config.name != "Debug"
|
||||
next if !debug && config.name != conf
|
||||
config.build_settings['GCC_INSTRUMENT_PROGRAM_FLOW_ARCS'] = enable
|
||||
config.build_settings['GCC_GENERATE_TEST_COVERAGE_FILES'] = enable
|
||||
end
|
||||
project.save
|
||||
end
|
||||
end
|
||||
|
||||
update(ARGV)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 单版本覆盖率
|
||||
|
||||
代码不变的情况下,发现 QA 或者开发自己测试的情况下,发现代码覆盖率不高,测试没有全面,则继续测试。这样生成多分 `.gcda` 文件,
|
||||
|
||||
- 生成覆盖率:`lcov -c -d {$SOURCE} -o {$DEST_INFO}`,比如 `lcov -c -d . -o CodeCoverage2.info`
|
||||
- 合并覆盖率:`lcov -a {$SOURCE_INFO_1} -a {$SOUCE_INFO_2} -o {$DEST_INFO}`,比如 `lcov -a CodeCoverage2.info -a ./../CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
|
||||
|
||||
|
||||
|
||||
### 多版本增量覆盖率
|
||||
|
||||
一个常见的场景是,开发同学基于业务需求 A 做完功能,QA 测试后导出覆盖率报告,发现覆盖率较低;或者 QA 提了3个测试 Bug,开发针对这2个情况,去修改了代码,重新打包让 QA 回归。这个时候 QA 不会重新点点,较好的做法是只回归遗漏或者有问题的代码。
|
||||
|
||||
|
||||
|
||||
核心思路是:基于上个版本的覆盖率数据,利用 git diff 查找出变化的部分,然后将旧版本覆盖率 `.info` 里面喝 git diff 得出的变化的部分关联,将值更新到新测试后的覆盖率 `.info` 里面。
|
||||
|
||||
|
||||
|
||||
git diff 如何解读
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GitDiffDIsplay.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
其中
|
||||
|
||||
- `index.txt` 是文件名
|
||||
- `@@ -3,6 +3,6 @@` ,- 代表删除,+ 代表增加。整体意思为从第3行开始,删除了6行,从第3行开始,增加了6行
|
||||
|
||||
所以步骤如下:
|
||||
|
||||
- 解析 git diffFile:
|
||||
- 根据文件名匹配规则 `diff --git (.*)` 将 diffFile 解析为若干个文件的数组集合 diffInfoList,并且保存文件信息
|
||||
- 根据 diff 块匹配规则 `@@(.*)@@` 将每个文件的 diffInfo 解析为若干个 diff 块的 blockInfoList,并且保存块信息
|
||||
- 根据增 / 删代码匹配规则 `(\+|\-)(.*)` 将每个块的 blockInfo 解析为若干个修改行号的增 / 删行数,并保存增 / 删信息 `{'delLine': 3, 'delCount': 6, 'addLine': 3, 'addCount': 6}`
|
||||
|
||||
- 解析 info 文件
|
||||
- 根据文件名匹配 `SF:*end_of_record:` 规则将 info 解析为若干个文件的 fileInfoList,并且保存文件信息
|
||||
- 根据函数行、函数执行次数、代码行及执行次数匹配规则 `FN、FNDA、DA` 将每个文件的 fileInfo 解析为若干个执行信息的 daList,并且保存数据信息 `{'lineNo': 12, 'exeCount': 1, 'funName': 'eat'}`
|
||||
|
||||
- 生成 info 文件
|
||||
- 根据 diffFile 解析结果,遍历 blockInfo 匹配起始修改行号 `delLine` 及修改行数 `diffline = addCount - delCount`,将 info 的解析结果进行行号匹配和增 / 删操作 `if (lineNo > delLine) lineNo += diffLine`,修改 fileInfoList 。这一步其实就是根据 git diff 信息,将新的覆盖率中的 lineNo 进行更新
|
||||
- 将新的 fileInfoList 中的数据根据 info 的结构进行写入文件操作
|
||||
|
||||
完成行号平移之后,两个版本的 .info 文件中的数据已经对齐了行号,可以用上述 LCOV 工具进行合并,合并完成后,用行号标记来统计差异的代码覆盖率数据。
|
||||
|
||||
|
||||
|
||||
### 打包平台及其服务侧
|
||||
|
||||
- 编写脚本在打包插桩后,将 `.gcno` 和源代码等信息上传到文件服务器上
|
||||
- 移动端各个测试设备测试后,App 可视化导出精准测试覆盖率报告,一键将 `.gcda` 文件上传到文件服务器上
|
||||
- 上传 `.gcda` 触发任务,利用 lcov 处理展示报告,同时也保存到文件服务器上
|
||||
- 最后 lark、企业微信通知能力,发送报告链接给开发、QA和相关人员
|
||||
- 同时 mPass 项目平台,买票上高铁的项目列表也有入口可以展示查看精准覆盖率报告
|
||||
|
||||
|
||||
|
||||
## Swift 代码覆盖率
|
||||
|
||||
这部分我将介绍:
|
||||
|
||||
>- 如何生成 `.profraw` 文件并通过命令行测量代码覆盖率
|
||||
>- 如何在 Swift 项目里调用 c/c++ 方法
|
||||
>
|
||||
>- 如何在 Xcode 中测量完整 Swift App 项目的代码覆盖率
|
||||
|
||||
|
||||
|
||||
### 理论支撑
|
||||
|
||||
#### 编译器参数支持
|
||||
|
||||
思路同 Objective-C 一样,参看 swiftc 编译器的编译参数 `swiftc --help` 可以看到
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCCompileOptionsAboutCoverage.png" style="zoom:25%">
|
||||
|
||||
可以看到这2个参数是大概收集代码覆盖率相关的。
|
||||
|
||||
|
||||
|
||||
#### MachO 和汇编插桩验证
|
||||
|
||||
利用 MachOView 查看产物里的 Mach-O 文件发现,MachO 多了一些和 LLVM 相关的 section,这些 section 看名字猜出来都是用来统计覆盖率的。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOViewSwiftCodeCoverageInfo.png" style="zoom:25%">
|
||||
|
||||
当 Xcode 开启 Swift 插桩统计后,打断点查看汇编代码可以发现,在 sayHi 方法,也就是只有1个 Basic Block 的情况下,编译器只插入1个桩,插桩1次。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeSwiftCodeCoverageViaAssembly.png" style="zoom:25%">
|
||||
|
||||
可以把 `__profc_xxx` 理解为打点计数信息,具体的地址保存在 MachO 文件的 `__DATA` 段 `__llvm_prf_cnts` 节点中。在程序刚启动时,所有的计数器信息为0,每当该代码(BB块)被执行1次,其计数值会加一。
|
||||
|
||||
重要的2个参数:
|
||||
|
||||
- `-profile-generate`:负责插桩代码的生成,是统计插桩信息用来的。`__llvm_prf` 段。
|
||||
- `-profile-coverage-mapping` :则生成一些 LLVM 相关的 `__LLVM_COV` 段。
|
||||
|
||||
之所以要做这样的拆分,猜测可能的原因是,插桩信息除了可以用于覆盖率分析以外,还可以用来进行 PGO 优化。什么是 PGO?即 Profile Guided Optimization ,是编译器用于提升 Application 的性能的一项技术。具体可以查看这篇文章[编译器利用 PGO 优化 App 性能](./1.133.md)
|
||||
|
||||
|
||||
|
||||
#### 导出原理
|
||||
|
||||
`llvm-cov` 如何生成报告的?因为 `.profdata` 文件只有 BB 计数器的调用次数,在生成覆盖率的时候传入了源码,那计数器信息和源码关联应该就是靠 MachO 文件了。
|
||||
|
||||
[LLVM Code Coverage Mapping Format](https://llvm.org/docs/CoverageMappingFormat.html#high-level-overview) 也说明了该细节
|
||||
|
||||
> LLVM’s code coverage mapping format is designed to be a self contained data format that can be embedded into the LLVM IR and into object files. It’s described in this document as a **mapping** format because its goal is to store the data that is required for a code coverage tool to map between the specific source ranges in a file and the execution counts obtained after running the instrumented version of the program.
|
||||
>
|
||||
> The mapping data is used in two places in the code coverage process:
|
||||
>
|
||||
> 1. When clang compiles a source file with `-fcoverage-mapping`, it generates the mapping information that describes the mapping between the source ranges and the profiling instrumentation counters. This information gets embedded into the LLVM IR and conveniently ends up in the final executable file when the program is linked.
|
||||
> 2. It is also used by *llvm-cov* - the mapping information is extracted from an object file and is used to associate the execution counts (the values of the profile instrumentation counters), and the source ranges in a file. After that, the tool is able to generate various code coverage reports for the program.
|
||||
|
||||
LLVM 的代码覆盖率映射格式被设计为一种自包含的数据格式,可以嵌入 LLVM IR 和 `.o` 文件中。在本文档中,它被描述为映射格式,因为它的目标是存储代码覆盖率工具在文件中的特定源范围和运行插入指令的程序版本后获得的执行计数之间进行映射所需的数据。
|
||||
在代码覆盖过程中,映射数据用于两个位置:
|
||||
|
||||
- 当 clang 使用 `-fcoverage-mapping` 编译源文件时,它会生成描述源范围和分析检测计数器之间映射的映射信息。这些信息被嵌入LLVM IR中,并在链接程序时方便地最终出现在最终的可执行文件中。
|
||||
- 它也被 `llvm-cov` 使用-映射信息从对象文件中提取,用于关联文件中的执行计数(配置文件检测计数器的值)和源范围
|
||||
|
||||
在此之后,该工具能够为程序生成各种代码覆盖率报告。
|
||||
|
||||
|
||||
|
||||
完整流程为:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageProgress.png" style="zoom:50%">
|
||||
|
||||
覆盖率生成流程为:编译阶段使用 `-profile-generate` 和 `-profile-coverage-mapping ` 参数,其中` -profile-generate` 会开启插桩能力,为每个 BB 增加插桩代码,`-profile-coverage-mapping` 将记录 BB、计数器值、和文件源码的关联映射信息,并将这些信息存储在编译产物,也就是 `__LLVM_COV` 段中。编译产物运行的过程中, 随着 BB 被执行,计数器的值会不断增加,并且写入 `__DATA` 段。运行结束后生成 `.profraw` 文件,可以处理成 `.profdata` 文件,该文件记录了每个计数器以及调用次数。
|
||||
|
||||
覆盖率解析流程为:利用指令提供的源代码路径,和可执行文件信息,结合 `.profdata` 信息,产出覆盖率报告。具体原理是:遍历 `profdata` 中的每一个计数器,先根据可执行文件中存储的映射关系,找到这个计数器所对应统计的那一段源码,从而生成行级别的覆盖率信息。
|
||||
|
||||
|
||||
|
||||
### 实验
|
||||
|
||||
用简单的单个 Swift 文件进行理论分析。
|
||||
|
||||
第一步,创建一个名为 `test.swift` 的文件,内容如下:
|
||||
|
||||
```swift
|
||||
func sayHi() {
|
||||
print("Hello swift world")
|
||||
}
|
||||
|
||||
func add(_ x: Int, _ y: Int) -> Int {
|
||||
return x + y
|
||||
}
|
||||
|
||||
func minuse(_ x: Int, _ y: Int) -> Int {
|
||||
return x - y
|
||||
}
|
||||
|
||||
sayHi()
|
||||
print(add(2, 4))
|
||||
```
|
||||
|
||||
第二步,在终端命令行,`test.swift` 所在路径下执行下面指令 `swiftc -profile-generate -profile-coverage-mapping test.swift`
|
||||
|
||||
传递给编译器的选项 `-profile-generate` 和 `-profile-coverage-mapping` 将在编译源码时启用覆盖率特性。基于源码的代码覆盖功能直接对 AST 和预处理器信息进行操作。
|
||||
|
||||
第三步,运行二进制文件 `./test`。然后在当前目录执行 `ls`,可以看到多出了一个名为 `default.profraw` 的文件。该文件由 llvm 生成,目的是衡量代码覆盖率。我们必须使用配套工具 llvm-profdata 来组合多个原始配置文件并同时对其进行索引。
|
||||
|
||||
第四步,终端运行指令 `xcrun llvm-profdata merge -sparse default.profraw -o coverage.profdata`,得到一个名为 `coverage.profdata` 的文件,进一步处理,它可以用来展示覆盖率报告。
|
||||
|
||||
第五步,在终端运行指令得到覆盖率信息
|
||||
|
||||
```shell
|
||||
xcrun llvm-cov show ./test -instr-profile=coverage.profdata
|
||||
xcrun llvm-cov export ./test -instr-profile=coverage.profdata
|
||||
```
|
||||
|
||||
整个步骤也可以看这张图
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftcDisplaySwiftCodeCoverageInCommandLine.png" style="zoom:30%">
|
||||
|
||||
在 `test.swift` 中编写的3个函数,只有2个执行了。查看覆盖率可以证实这一点,minuse 函数没有被执行。
|
||||
|
||||
|
||||
|
||||
### 工程实践
|
||||
|
||||
第一步,创建 Swift 项目,编写测试代码
|
||||
|
||||
```swift
|
||||
// Cat.swift
|
||||
import Foundation
|
||||
class Cat {
|
||||
var kind: String
|
||||
init(kind: String) {
|
||||
self.kind = kind
|
||||
}
|
||||
|
||||
func play() {
|
||||
print("I am a \(kind) cat, I am playing now.")
|
||||
}
|
||||
}
|
||||
|
||||
// Person.swift
|
||||
import Foundation
|
||||
class Person {
|
||||
var name: String
|
||||
var cat: Cat?
|
||||
|
||||
init(name: String, cat: Cat? = nil) {
|
||||
self.name = name
|
||||
self.cat = cat
|
||||
}
|
||||
|
||||
func sayHi() {
|
||||
print("Hello world, I am \(name), I have a \(String(describing: cat?.kind)) cat")
|
||||
}
|
||||
|
||||
func eat() {
|
||||
print("eat")
|
||||
}
|
||||
|
||||
func sleep() {
|
||||
print("sleep")
|
||||
}
|
||||
|
||||
func play() {
|
||||
cat?.play()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
第二步,选择 ` Build Settings -> Swift Compiler — Custom Flags`,在 Other Swift Flags 添加 `-profile-generate` 和 `-profile-coverage-mapping` 选项。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeSwiftCoveraeCompileOptions.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
第三步,开启覆盖率收集选项
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeSetTestCoverageOptions.png" style="zoom:30%">
|
||||
|
||||
第四步,要将覆盖率信息导出前,必须要调用 llvm 的一些 c/c++ api,所以要将需要用到的方法,导出为一个模块。
|
||||
|
||||
创建一个名为 `InstrProfiling.h` 的头文件。内容为:
|
||||
|
||||
```c++
|
||||
#ifndef PROFILE_INSTRPROFILING_H_
|
||||
#define PROFILE_INSTRPROFILING_H_int __llvm_profile_runtime = 0;void __llvm_profile_initialize_file(void);
|
||||
|
||||
const char *__llvm_profile_get_filename();
|
||||
void __llvm_profile_set_filename(const char *);
|
||||
int __llvm_profile_write_file();
|
||||
int __llvm_profile_register_write_file_atexit(void);
|
||||
const char *__llvm_profile_get_path_prefix();
|
||||
|
||||
#endif /* PROFILE_INSTRPROFILING_H_ */
|
||||
```
|
||||
|
||||
创建一个 `module.modulemap` 文件并将所有内容导出为一个模块(创建的时候 Xcode 选择 empty 模版)
|
||||
|
||||
```shell
|
||||
module InstrProfiling {
|
||||
header "InstrProfiling.h"
|
||||
export *
|
||||
}
|
||||
```
|
||||
|
||||
第五步,判断时机,在需要导出覆盖率的地方编写函数。我在 ViewController 点击屏幕的时候导出:
|
||||
|
||||
- 导入模块 `import InstrProfiling`
|
||||
- 编写导出方法 `__llvm_profile_set_filename` 和 `__llvm_profile_write_file`
|
||||
|
||||
````
|
||||
import UIKit
|
||||
import InstrProfiling
|
||||
|
||||
class ViewController: UIViewController {
|
||||
var cat: Cat?
|
||||
var person: Person?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.cat = Cat(kind: "Ragdoll")
|
||||
self.person = Person(name: "FantasticLBP", cat: cat)
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
self.person?.sayHi()
|
||||
self.person?.play()
|
||||
// Do any additional setup after loading the view.
|
||||
print("File Path Prefix: \(String(cString: __llvm_profile_get_path_prefix()) )")
|
||||
print("File Name: \(String(cString: __llvm_profile_get_filename()) )")
|
||||
let name = "SwiftCodeCoverage.profraw"
|
||||
let fileManager = FileManager.default
|
||||
|
||||
do {
|
||||
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
|
||||
let filePath: NSString = documentDirectory.appendingPathComponent(name).path as NSString
|
||||
__llvm_profile_set_filename(filePath.utf8String)
|
||||
print("File Name: \(String(cString: __llvm_profile_get_filename()))")
|
||||
__llvm_profile_write_file()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
````
|
||||
|
||||
第六步, 运行代码,生成 `.profraw` 格式的文件。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeGenerateProfDataAboutSwiftCoverage.png" style="zoom:25%">
|
||||
|
||||
第七步,因为产出覆盖率的时候需要用到 MachO 文件。所以在项目根目录下创建名为 `DataAnalysis` 的文件夹。在终端利用 mv 将产物里的 MachO 移动到 `DataAnalysis` 文件夹下。也将 `.profraw` 移动进去。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageMVProdrawAndMachO.png" style="zoom:25%">
|
||||
|
||||
第八步,利用指令 `xcrun llvm-profdata merge -sparse SwiftCodeCoverage.profraw -o SwiftCodeCoverage.profdata`,将 `.profraw` 转换成 `.profdata` 文件
|
||||
|
||||
第九步,利用指令 `xcrun llvm-cov show ./SwiftCodeCoverage.app/SwiftCodeCoverage -instr-profile=SwiftCodeCoverage.profdata` 在终端查看代码的覆盖情况
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageInCommandLine.png" style="zoom:25%">
|
||||
|
||||
第十步,终端查看代码执行情况还是不够直观,可以用 `llvm-cov` 命令生成 HTML 格式的覆盖率报告,指令格式为:
|
||||
|
||||
```shell
|
||||
xcrun llvm-cov show\
|
||||
-use-color\ # 彩色报告
|
||||
-format=html\ # HTML 格式
|
||||
-arch=x86_64\ # 架构指令集
|
||||
-instr-profile=${.profdata 路径}\ # 指定 .profdata 文件路径
|
||||
${MachO 文件路径}\ # 指定 MachO文件路径
|
||||
${SourceCode 路径} # 项目源代码路径
|
||||
-output-dir ${Swift覆盖率报告路径} # 指定覆盖率报告保存的路径
|
||||
```
|
||||
|
||||
我这边具体指令为:
|
||||
|
||||
`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverage.profdata SwiftCodeCoverage ./../ -output-dir ./SwiftCodeCoverageReport `
|
||||
|
||||
第十一步,查看整体的覆盖率信息与单个文件的覆盖率,查看代码执行情况
|
||||
|
||||
其中 `index.html` 是所有文件的覆盖率数据汇总,而每个文件精确到行级别的覆盖率信息,则保存在 `coverage` 文件夹中,每个文件对应一个 HTML 文件。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCodeCoverageReport.png" style="zoom:25%">
|
||||
|
||||
第十二步,假设我们在另一台 CI 机器上也在执行测试任务。那不同机器上的测试结果如何合并?
|
||||
|
||||
生成覆盖率报告是基于插桩实现的,最后 `xcrun llvm-cov` 生成 html 需要的是:Mach-O 文件、源代码路径、`.profdata` 文件。
|
||||
|
||||
看得出来不同 CI 机器上,不同的只有 `.profdata` 文件,所以处理 `.profdata` 即可。所幸 `llvm-profdata` 就支持不同的 `.profraw` 的合并。
|
||||
|
||||
比如第一台机器生成的是 `SwiftCodeCoverage.profraw` 得到的覆盖率如上图所示。第二台机器生成的是 `SwiftCodeCoverage.profraw`。
|
||||
|
||||
接下去利用指令 ` xcrun llvm-profdata merge SwiftCodeCoverage.profraw SwiftCodeCoverage2.profraw -o SwiftCodeCoverageCombined.profdata` 将2份测试原始文件进行合并,然后再利用 `llvm-cov` 生成 html 报告
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftProfdataCombinedAndCoverageReport.png" style="zoom:25%">
|
||||
|
||||
第十三步,我们有时候有需求会更改操作生成的测试文件,`.profdata` 是没办法修改的,但 `llvm-profdata` 指令可以传递参数生成 `.text` 格式的文件,里面的内容可以修改。修改后再从 `.text` 转换为 `.profdata`,最后再利用 `llvm-cov` 生成 html 报告。
|
||||
|
||||
下面演示下如何修改生成的覆盖率数据(注意:不修改 html,而是修改 BB 的计数值)
|
||||
|
||||
1. 利用 ` xcrun llvm-profdata merge SwiftCodeCoverage.profraw SwiftCodeCoverage2.profraw -text -o SwiftCodeCoverageCombined.txt` 将2份 `.profraw` 数据合并为 `.txt` 格式的文件(记录了 BB 和技术值信息)
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftProfdataTextChangeBBCount.png" style="zoom:25%">
|
||||
|
||||
2. 编辑修改 `.txt` BB 的计数值,此处,故意把 `Person:sleep` 的1改为0
|
||||
|
||||
3. 利用指令将 `.txt` 改为 `.profdata` 格式。`xcrun llvm-profdata merge SwiftCodeCoverageCombined.txt -o SwiftCodeCoverageCombinedFromText.profdata`
|
||||
|
||||
4. 再根据合并后的 `.profdata` 生成 html 覆盖率报告。`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverageCombinedFromText.profdata SwiftCodeCoverage ./../ -output-dir ./SwiftCodeCoveragCombinedReportFromText`
|
||||
|
||||
效果如下:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftCoverageFromCombinedProfdataText.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 心得感悟
|
||||
|
||||
下面是一个工作中的实际例子,冒烟用例也全部通过了,代码在 CR 后 MR 了,然后买票上车,开始高铁回归阶段。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/YouzanCodeCoverageUsage.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeCoverageAnalysisReport.png" style="zoom:35%">
|
||||
|
||||
QA 去回归测试,然后会给开发一个精准测试报告。就是原始的本次业务开发分支上的代码执行情况。程序员去分析,覆盖率低的原因是什么,是兜底代码太多、还是某些技术实现是类似夸端的 Weex、RN、Flutter、还是测试 case 不充分以至于看上去用例通过,但是某些代码还是没有测试到,往往这些没有测试到、执行到的代码是线上用户在极端情况下容易走到的 case。所以需要根据精准测试覆盖率反推 QA 完善用例,或者开发自己优化代码。
|
||||
|
||||
精准测试的价值很明显,但 ROI 就见仁见智了,有些人觉得要开发一套 CI 需要耗时耗力,每个项目完成后需要分析精准测试报告、反推 QA 完善用例很麻烦,但有些决策者就觉得这样能 cover 一些平时难以发现的问题。
|
||||
|
||||
|
||||
|
||||
## 参考文章
|
||||
|
||||
[Source-based Code Coverage](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#source-based-code-coverage)
|
||||
|
||||
[llvm-cov - emit coverage information](https://llvm.org/docs/CommandGuide/llvm-cov.html)
|
||||
|
||||
[LLVM Code Coverage Mapping Format](https://llvm.org/docs/CoverageMappingFormat.html#llvm-code-coverage-mapping-format)
|
||||
|
||||
|
||||
|
||||
- https://blog.csdn.net/diy534/article/details/7099049
|
||||
- https://sq.sf.163.com/blog/article/180397339783520256
|
||||
- https://www.jianshu.com/p/a9679f47711c
|
||||
@@ -8,7 +8,7 @@ Xcode 新建项目 Language 选择 Swift 语言、Interface 选择 SwiftUI。然
|
||||
|
||||
可以看到下面的文件:
|
||||
|
||||
<img src="./../assets/SwiftUIDemo1.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDemo1.png" style="zoom:45%">
|
||||
|
||||
奇怪的事情发生了:AppDelegate 不见了,也没地方构建 keyWindow,怎么办?为什么文件叫 `SwiftUIDemoApp`?
|
||||
|
||||
@@ -79,13 +79,13 @@ struct SwiftUIDemoApp: App {
|
||||
|
||||
SwiftUI 的文档写的还是不错。
|
||||
|
||||
<img src="./../assets/SwiftUIOffcialDocument.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIOffcialDocument.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
ContentView.swift 及其效果如下:
|
||||
|
||||
<img src="./../assets/SwiftUISimpleDemo.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUISimpleDemo.png" style="zoom:25%">
|
||||
|
||||
上面的代码 `some view` 中,view 是 SwiftUI 一个核心的协议,代表了闭包中元素描述。如下代码所示,其是通过一个 associatedtype 修饰的,带有这种修饰的协议不能作为类型来使用,只能作为类型约束来使用。
|
||||
|
||||
@@ -99,15 +99,15 @@ ContentView.swift 及其效果如下:
|
||||
|
||||
- Xcode 支持预览
|
||||
|
||||
<img src="./../assets/SwiftUIDemo2.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDemo2.png" style="zoom:45%">
|
||||
|
||||
- 在预览界面选中某个空间,同时按住 `command + 单击`,可以调出一个操作面板。第一个是 UI 检查器,可以查看和修改
|
||||
|
||||
<img src="./../assets/SwiftUIDemo3.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDemo3.png" style="zoom:45%">
|
||||
|
||||
在代码区域选中控件,同时按住 `command + 单击`,同样可以调出一个操作面板
|
||||
|
||||
<img src="./../assets/SwiftUIDemo4.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDemo4.png" style="zoom:45%">
|
||||
|
||||
- 预览模式下,支持代码和预览界面的实时刷新同步。
|
||||
|
||||
@@ -269,7 +269,7 @@ extension View {
|
||||
|
||||
View 上大多数调用的方法都称为 `Modifier`,一种是为 `原地Modifier` ,另外一种为 `封装类Modifier`。`原地Modifier` 是返回同样类型的 View,`封装类Modifier` 则可以返回不同类型的 View,在开发中我们经常需要自定义 `ViewModifier` 来对 View 进行特定的变换操作。
|
||||
|
||||
<img src="./../assets/SwiftUIViewModifier.png" style="zoom:40%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewModifier.png" style="zoom:40%">
|
||||
|
||||
|
||||
|
||||
@@ -768,7 +768,7 @@ struct SwiftUIDemoApp: App {
|
||||
}
|
||||
```
|
||||
|
||||
<img src="./../assets/UIKitCompoentWrappedForSwiftUI.png" style="zoom:35%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/UIKitCompoentWrappedForSwiftUI.png" style="zoom:35%">
|
||||
|
||||
|
||||
|
||||
@@ -891,7 +891,7 @@ struct SwiftUIDemoApp: App {
|
||||
|
||||
SwiftUI 是个 UI 框架、也是个组件库,核心是为了解决 UI 构建复杂、繁琐的问题。Redux 在前端由来已久,有 store、state、action、middleware、reducer 等角色,多个角色各司其职,不存在团队规范和约定后不遵守的情况。通过 store 来管理状态,状态变化后,使用到该状态的 UI 组件会收到通知,更新 UI。用户点击操作 UI,产生 action,action 经历过一系列 middleware 后来到了 store,store 让 reducer 根据 action 和当前的 state 计算,得到一个新的 state。新的 state 变化了,使用到的地方的 UI 也会自动更新。(数据和 UI 的双向绑定)
|
||||
|
||||
<img src="./../assets/2019-06-26-Redux-Structures.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2019-06-26-Redux-Structures.png" style="zoom:30%">
|
||||
|
||||
对比 MVC:
|
||||
|
||||
@@ -917,7 +917,7 @@ Apple 推出了 SwiftUI,但没有像最早 MVC 一样,在 SwiftUI 中推出
|
||||
|
||||
安装依赖:File -> Add Packages,输入 `swift-composable-architecture` 搜索,点击右下角 Add Package 即可。
|
||||
|
||||
<img src="./../assets/SwiftTCADemoStep1.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTCADemoStep1.png" style="zoom:30%">
|
||||
|
||||
然后开始开发:先编写 Reducer 部分,再开发相关 UI
|
||||
|
||||
@@ -1031,7 +1031,7 @@ struct ContentView_Previews: PreviewProvider {
|
||||
}
|
||||
```
|
||||
|
||||
<img src="./../assets/SwiftTCADemo1.png" style="zoom:35%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTCADemo1.png" style="zoom:35%">
|
||||
|
||||
说明:
|
||||
|
||||
@@ -1092,11 +1092,11 @@ final class TCADemoTests: XCTestCase {
|
||||
|
||||
可以看到如果某个单测 case 失败,则会清楚的显示错误的信息。
|
||||
|
||||
<img src="./../assets/SwiftTCAUnitTestDemo1.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTCAUnitTestDemo1.png" style="zoom:25%">
|
||||
|
||||
如果需要在测试的时候使用“重复测试”功能,右击测试按钮,在弹出框里做重复测试的配置修改。
|
||||
|
||||
<img src="./../assets/SwiftTCAUnitTestDemo2.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTCAUnitTestDemo2.png" style="zoom:25%">
|
||||
|
||||
### 动手做一个简易版 Redux
|
||||
|
||||
@@ -1215,7 +1215,7 @@ struct ReduxDemoView_Previews: PreviewProvider {
|
||||
|
||||
实现效果如下:
|
||||
|
||||
<img src="./../assets/SwiftUIReduxDemo.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIReduxDemo.png" style="zoom:30%">
|
||||
|
||||
## 核心技术
|
||||
|
||||
@@ -1299,7 +1299,7 @@ while let input = readLine() {
|
||||
|
||||
下面是 `CFRunLoop` 的示意图,将其与我们的命令行应用程序进行比较。
|
||||
|
||||
<img src="./../assets/RunLoopAndSimpleVersion.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RunLoopAndSimpleVersion.png" style="zoom:30%">
|
||||
|
||||
如果你在 Xcode 调试器中暂停一个 iOS 应用程序,主线程的堆栈信息中便会出现下面的调用栈
|
||||
|
||||
@@ -1389,7 +1389,7 @@ Demo
|
||||
|
||||
|
||||
|
||||
<img src="./../assets/RunLoopAndSwiftUI.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RunLoopAndSwiftUI.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
@@ -1470,7 +1470,7 @@ SwiftUI 中的渲染循环可能隐藏得很好,它所使用的技术与我们
|
||||
|
||||
#### 渲染流程
|
||||
|
||||
<img src="./../assets/SwiftUIRenderProcess" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIRenderProcess" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
@@ -1506,7 +1506,7 @@ SwiftUI 内部是如何处理开发者编写的描述性代码的呢?其内部
|
||||
|
||||
#### 视图标识(View Identity)
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo1.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo1.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
@@ -1514,7 +1514,7 @@ SwiftUI 内部是如何处理开发者编写的描述性代码的呢?其内部
|
||||
|
||||
所以当 SwiftUI 处理你的界面描述时,它也需要 Identity 这个关键信息区分视图是否是同一个。
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo2.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo2.png" style="zoom:30%">
|
||||
|
||||
让我们来看下上面这个 Good Dog, Bad Dog 的小应用,你可以点击屏幕上的任何位置来切换狗狗的状态。但是我们从技术层面分析,上面的界面可以有两种 SwiftUI 的描述方式:
|
||||
|
||||
@@ -1540,7 +1540,7 @@ Identity 既然这么重要,那么开发者是如何用代码来定义的呢
|
||||
|
||||
##### 声明式 Identity
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo3.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo3.png" style="zoom:30%">
|
||||
|
||||
就像上面这两只狗狗,仅通过图片,很难判断这是不是同一只狗狗,但如果我们能用名字来标识它们。就很容易得出结论。像这样给狗狗起名字来标识它们的方式,就是在显式声明 Identity。
|
||||
|
||||
@@ -1548,7 +1548,7 @@ Identity 既然这么重要,那么开发者是如何用代码来定义的呢
|
||||
|
||||
我们可以通过视图的指针来标识每个视图,如果多个视图指针,都共享同一块内存空间,那么它们其实是同一个视图,如下图所示:
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo4.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo4.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
@@ -1556,13 +1556,13 @@ Identity 既然这么重要,那么开发者是如何用代码来定义的呢
|
||||
|
||||
其实,SwiftUI 是用另外一种形式来显式标识 View。通过下面的例子能更好的理解,例如在这个救援犬列表里,用 dogTagID KeyPath 获取相应属性,在参数里指定到 id 上,就是在显式声明 View 的 Identity。这样就能标识出每条数据对应的展示视图。一旦列表数据发生变化, SwiftUI 可以根据这些 ID 来判断,哪些视图需要新生成,哪些视图重复使用,只需要执行动画。
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo5.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo5.png" style="zoom:30%">
|
||||
|
||||
##### 结构性 Identity
|
||||
|
||||
不显式声明 Identity,这并不意味着这些 View 根本没有 Identity,也就是说每个 View 都有一个 Identity,即使它不是显式声明出来的。在这种情况下 SwiftUI 内部会对没有显式 Identity 的 View 根据它的描述层级结构生成一种隐式的 Identity,就叫做结构性 Identity。
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo6.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo6.png" style="zoom:30%">
|
||||
|
||||
如上图,假设我们有两只相似的狗狗,但我们不知道它们的名字,我们仍然还要标识出它们。这时候可以通过它们坐的位置来标识,如 `左边的狗` 或 `右边的狗`。
|
||||
|
||||
@@ -1570,13 +1570,13 @@ Identity 既然这么重要,那么开发者是如何用代码来定义的呢
|
||||
|
||||
SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来标识 View。一个典型的例子就是在 SwiftUI 中 使用 if else 条件判断的时候,条件语句的结构使得 SwiftUI 能够明确的识别每个 View,如下图,第一个 AdoptionDirectory 只在条件为 True 的时候显示,第二个 DogList 只在条件为 False 的显示。
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo7.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo7.png" style="zoom:30%">
|
||||
|
||||
但是 SwiftUI body 计算属性需要一个明确一致的返回类型,但 if else 条件判断使得返回类型不一致了,会引起编译失败。这时候 SwiftUI 引入了一个的黑魔法 - **ViewBuilder (默认是附加在 body 计算属性上的,不需要开发者单独指定)**。ViewBuilder 帮助 SwiftUI 把各种条件判断,封装成 _ConditionalContent 的数据结构。但为了区分在不同分支下的类型不同,用泛型来进行了区分,这样即保证了返回数据的一致性,又保证了 SwiftUI 内部可以通过泛型识别出不同分支下结构性 Identity。
|
||||
|
||||
见源代码:
|
||||
|
||||
<img src="./../assets/SwiftUIViewBuilder.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewBuilder.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
@@ -1584,7 +1584,7 @@ SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来
|
||||
|
||||
其实,如果你回到 UIKit 中理解,也是一样的,我们在 UIView 上执行动画的时候,一般也是在同一个 UIView 的实例里去动态改变它的属性去修改样式, 才会有那种平滑过渡的效果;相反,如果虽然是同一个类型的 UIView,但是对应的是不同的实例,去做那种平滑的过渡效果,也是很难实现的。
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo8.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo8.png" style="zoom:30%">
|
||||
|
||||
综上,**在使用结构性 Identity 的时候,第二种描述 View 的方式是更好的选择。应该尽量避免切换 Identity,这样做会给动画和性能都带来良好的效果,也有利于维持视图的生命周期和数据状态。**
|
||||
|
||||
@@ -1594,7 +1594,7 @@ SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来
|
||||
|
||||
说起 AnyView ,这家伙绝对是 Identity 的克星。
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo9.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo9.png" style="zoom:30%">
|
||||
|
||||
上图是一个使用 AnyView 的示例代码。在这个自定义 View 中,为了保证最终返回一个明确一致的数据类型,每个分支都用一个 AnyView 包裹起来。由于 AnyView 隐藏了所包装视图的类型,让 SwiftUI 无法在条件判断中识别出结构性 Identity,在 SwiftUI 眼里,它看到的都是一些擦除类型的 AnyView,更要命的是,这段代码阅读起来特别困难。
|
||||
|
||||
@@ -1607,7 +1607,7 @@ SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来
|
||||
|
||||
重构后,最终代码和 View 层级结构如下图:
|
||||
|
||||
<img src="./../assets/SwiftUIViewIdentityDemo10.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIViewIdentityDemo10.png" style="zoom:30%">
|
||||
|
||||
一般情况下,还是尽量避免使用 AnyView,因为 AnyView 有如下缺陷:
|
||||
|
||||
@@ -1623,13 +1623,13 @@ SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来
|
||||
|
||||
#### Lifetime 与 Identity 的关系
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityLifeCycle1.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityLifeCycle1.png" style="zoom:30%">
|
||||
|
||||
如上图,这里有个叫 Theseus 的小猫。他在一天中可能有各种不同的状态,一会装可爱,一会睡觉,一会发火,但是无论处于何种状态,他都是那只叫 Theseus 的小猫。
|
||||
|
||||
视图在整个生命周期内有各种不同的状态,每个状态在 SwiftUI 中由不同的 View 实例(值类型)来描述,而 Identity 将这些不同状态下的 View 值随着时间的推移关联起来,它们都对应着同一个视图元素。这就是 Identity 与视图生命周期建立联系的本质。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityLifeCycle2.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityLifeCycle2.png" style="zoom:30%">
|
||||
|
||||
让我们用上面的代码来更清晰的理解这一点,这里我们有一个简单的自定义 View - PurrDecibelView,用来显示猫叫声的强度。一开始的时候 SwiftUI 调用 body 计算属性,获取到叫声为 25 的 View,但是突然小猫饿了,希望获得更多的关注,叫声变大为50,这时候 SwiftUI 监听到叫声这个状态的变化,重新调用 body 计算属性,获取到一个全新的 View,这两个 View 是完全截然不同的两个值。SwiftUI 会在后台对这两个值进行对比并对比出哪些部分发生了变化。得出对比结果后,告诉渲染视图执行变化部分的渲染操作,同时,用完的 View 值也会被销毁。
|
||||
|
||||
@@ -1637,7 +1637,7 @@ SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来
|
||||
|
||||
所以,如下图,我们经常用到的生命周期方法 onAppear 和 onDisappear,其实是在视图显示和消失的时候触发,而不是 View 创建和销毁的时候触发。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityLifeCycle3.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityLifeCycle3.png" style="zoom:30%">
|
||||
|
||||
所以最终我们得出如下公式来阐述 View,LifeTime,Identity 三者之间的关系:
|
||||
|
||||
@@ -1656,11 +1656,11 @@ SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来
|
||||
|
||||
如下图的 CatRecorder 自定义 View,每次的 title 发生变化,由于他被 @State 修饰,SwiftUI 内部会在内存中保存这个数据,并且监听他的变化,一旦发生变化,就调用 body,重新计算。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityLifeCycle4.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityLifeCycle4.png" style="zoom:30%">
|
||||
|
||||
下面,让我们来看一下在有分支的情况下,视图生命周期和数据状态之间的关系。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityLifeCycle5.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityLifeCycle5.png" style="zoom:30%">
|
||||
|
||||
如上图代码,分支里的两个 CatRecorder 由于结构性 Identity 不同,所以它们被 SwiftUI 视为是两个不同的视图。之前说过这样会影响动画效果,其实也会影响它们内部数据状态的维持。
|
||||
|
||||
@@ -1676,7 +1676,7 @@ SwiftUI 几乎在所有地方都采用了这种结构化 Identity 的方式来
|
||||
|
||||
保证 Identity 稳定,这一点非常重要,尤其是在使用数据驱动型的列表控件时,在下面这些控件中,往往都需要用数据的 id 来给 View 显式声明 Identity。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityStableDemo1.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityStableDemo1.png" style="zoom:30%">
|
||||
|
||||
下面两张图是两种不同的 ForEach 的用法。其中第一种用法是一个常数的 Range,SwiftUI 可以直接用 Range 的值来为视图生成 Identity,以确保在视图的整个生命周期内 Identity 是稳定的。但当使用一个动态的 Range 时,会导致声明式的 Identity 数值是不可预期的,Identity 一旦切换,视图都会重新生成,这样就会出现性能问题。 所以在 Xcode 12 的时候,检查到这种使用方式会编译报错,而在新版本的 Xcode 13 这将变为一个警告 (beta 版本似乎不生效)。
|
||||
|
||||
@@ -1692,7 +1692,7 @@ ForEach(0..<sheeps) { offset in
|
||||
|
||||
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityStableDemo2.png" style="zoom:40%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityStableDemo2.png" style="zoom:40%">
|
||||
|
||||
|
||||
|
||||
@@ -1747,15 +1747,15 @@ struct DogView: View {
|
||||
|
||||
该 View 有两个属性 dog 和 treat,它们都可以理解为视图的依赖关系。依赖关系就是视图更新的入口。当依赖关系发生变化时,会重新调用 View 的 body,获取整个 View 的层级描述信息。在这个例子中,描述的就是一个有触发行为的按钮。他对应的视图层级结构如下:
|
||||
|
||||
<img src="./../assets/SwiftUIDependencies1.png" style="zoom:40%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDependencies1.png" style="zoom:40%">
|
||||
|
||||
看上面这张图的话,是一个树结构,但是有可能多个视图都依赖同一个状态。有可能某个子视图也依赖顶级视图中的状态。情况越来越复杂后,这就不再是一个树结构。重新整理,避免让连接线之间交叉,如下图,可以看出它们之间的关系实际上是一个图结构。我们可以称之为**依赖关系图**。
|
||||
|
||||
<img src="./../assets/SwiftUIDependencies2.png" style="zoom:40%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDependencies2.png" style="zoom:40%">
|
||||
|
||||
深入的理解这个依赖关系图很重要,因为它保证了 SwiftUI 只更新那些需要重新调用 body 的 View。以最底部的依赖关系为例。如果我们检查这个依赖关系,会发现有两个 View 依赖它,当依赖的数据状态发生变化,只有这两个 View 会被标记为无效。同时 SwiftUI 开始调用每个视图的 body 计算属性,只为标记为无效的视图产生一个新的 body 值。
|
||||
|
||||
<img src="./../assets/SwiftUIDependencies3.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDependencies3.png" style="zoom:30%">
|
||||
|
||||
状态管理工具,在 SwiftUI 依赖关系的建立就是通过它们来实现的:
|
||||
|
||||
@@ -1783,11 +1783,11 @@ Identity 就是依赖关系图的灵魂,重要性不言而喻。正如之前
|
||||
|
||||
在下图的例子中,每次都生成一个 UUID 和 直接用 Indices 来显式声明 Identity 都是不稳定的方式,因为它们都会随着时间推移发生变化,不能准确地标识一个视图,最终导致的结果就是,当我们在列表头部新插入数据时,整个列表都会重新刷新。相反,我们如果用一个 databaseID 就是可以的,因为这个 ID 只对应一个数据,能够清晰的标识一个与该数据对应的视图。这时候我们在头部新插入数据,所有的动画效果都非常自然了。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityStable1.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityStable1.png" style="zoom:30%">
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityStable2.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityStable2.png" style="zoom:30%">
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityStable3.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityStable3.png" style="zoom:30%">
|
||||
|
||||
#### 唯一性
|
||||
|
||||
@@ -1801,9 +1801,9 @@ Identity 就是依赖关系图的灵魂,重要性不言而喻。正如之前
|
||||
|
||||
像下面的代码中使用 name 的 KeyPath 来给 View 显式声明 Identity,是不合理的,因为我们无法保证 name 的唯一性,一旦出现重名的情况,新的视图很有可能不会展示出来。但当把 name 换成 serialNumber,一切都正常了
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityUnique1.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityUnique1.png" style="zoom:30%">
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityUnique2.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityUnique2.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
@@ -1811,13 +1811,13 @@ Identity 就是依赖关系图的灵魂,重要性不言而喻。正如之前
|
||||
|
||||
上面,我们都是用声明式 Identity 来说明如何改进 Identity,接下来看看如何改进结构性 Identity。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityBranchLess1.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityBranchLess1.png" style="zoom:30%">
|
||||
|
||||
上面的代码,乍一看似乎没什么问题。但是仔细分析会发现,这里有个性能问题。**content 在不同的分支条件下,会产生不同的结构性 Identity,这就导致了分支切换后针对同一个 View 会生成两个不同的视图元素,也就是在内存中分配两份内存空间**。这点其实是可以避免的。虽然这里我们很轻易的发现了这个问题,但当项目大了之后,有可能这些 ViewModifier 的代码都不在一起,所以这种问题很容易被忽视。
|
||||
|
||||
修改:把分支结构去掉,改为在 opacity 修饰器上添加三目运算的方式来动态修改透明度。由于去掉了分支结构,所以 content 只会生成单一的结构性 Identity,也就避免了不必要的内存开销,提高了性能。
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityBranchLess2.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityBranchLess2.png" style="zoom:30%">
|
||||
|
||||
像上面代码直接把透明度设置为 1,也就是跟初始状态一致,其实 SwiftUI 发现这种情况是不执行任何渲染操作的。我们把这样的修饰器称为 "惰性修饰器",因为它们不影响渲染的结果。
|
||||
|
||||
@@ -1829,7 +1829,7 @@ Identity 就是依赖关系图的灵魂,重要性不言而喻。正如之前
|
||||
|
||||
在下图中还给出了一些其他的惰性修饰器作为参考:
|
||||
|
||||
<img src="./../assets/SwiftUIIdentityBranchLess3.png" style="zoom:30%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIIdentityBranchLess3.png" style="zoom:30%">
|
||||
|
||||
|
||||
|
||||
|
||||
61
Chapter1 - iOS/1.133.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 编译器利用 PGO 优化 App 性能
|
||||
|
||||
## 什么是 PGO?
|
||||
|
||||
> Profile Guided Optimization (PGO) is an advanced feature for wringing every last bit of performance out of your app. It is not hard to use but requires some extra build steps and some care to collect good profile information. Depending on the nature of your app’s code, PGO may improve performance by 5 to 10 percent, but not all applications will benefit from it. If you have performance-sensitive code that needs that extra optimization, PGO may be able to help.
|
||||
|
||||
PGO 即 Profile Guided Optimization,
|
||||
|
||||
[Using Profile Guided Optimization](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/xcode_profile_guided_optimization/pgo-using/pgo-using.html) 官方文档大概意思是:PGO 是一种根据运行时 profiling data 来进行优化的技术。用起来不难,但需要一些额外的构建步骤和一些收集良好的 profile 文件。如果一个 application 的使用方式没有什么特点,那么我们可以认为代码的调用没有什么倾向性。但实际上,我们操作一个 application 的时候,往往有一套固定流程,尤其在程序启动的时候,这个特点更加明显。采集这种“典型操作流”的 profiling data,然后让编译器根据这些 data 重新编译代码,就可以把运行时得到的知识,运用到编译期,从而获得一定的性能提升。然而,值得指出的一点是,这样获得的性能提升并不是十分明显,通常只有 5-10%。如果已经没有其他办法,再考虑试试 PGO。
|
||||
|
||||
|
||||
## 何时使用 PGO?
|
||||
正常情况下不用,通常来说 LLVM 经过编译前端,通过词法分析、语法分析、语义分析构建出 AST,然后转换为 IR,IR 经过一些列优化 Pass,常见的 Pass 比如死代码消除等,LLVM 已经做了大量的优化工作。所以通常来说,我们需要正向的优化代码、优化算法、设计正确合理的架构、合理的 UI 层级。除此之外,你还想让 App 获得更好的性能,可以考虑采用 PGO 技术。
|
||||
|
||||
## PGO 怎么样工作的?
|
||||
PGO 假设你的应用程序的行为是可预测的,这样一个有代表性的配置文件就可以捕捉代码所有性能敏感方面的未来行为。当启用 PGO 时,Xcode 会构建一个专门检测的应用程序版本,然后运行它。您可以手动运行该应用程序,也可以使用 UI 自动化 XCTest 测试 App。当应用程序运行时,会统计并记录每条语句的执行次数。
|
||||
|
||||
应该收集一份具有典型、能够代表 App 真正用户行为的 profile 数据,PGO 统计每个语句的执行次数,并创建一个为该行为建模的概要文件。依据该文件,LLVM 编译器将优化工作集中在最重要的代码上。
|
||||
|
||||
举个例子:有一个稍微长一点的函数,刚好长到编译器不对它的调用进行 inline 优化,但是实际上,这个函数是一个热点调用,在运行时被调用的次数非常多。那么如果此时编译器也能帮我们把它优化掉,是不是很好呢?但是,编译器怎么能知道这个“稍微长一点的函数”是一个热点调用呢?PGO 根据这个 profile 文件进行优化
|
||||
|
||||
是一种优化编译器的技术,通过收集程序的实际运行数据,例如程序执行的分支情况,来指导编译器生成更优化的代码。
|
||||
|
||||
|
||||
## tips
|
||||
首先,Xcode 已经提供了 PGO 的 UI 操作([详情可参考](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/xcode_profile_guided_optimization/Introduction/Introduction.html#//apple_ref/doc/uid/TP40014459-CH1-SW1)),所以如果是简单的 application,可以直接使用 UI 操作的方式,简单省事。不过,UI 操作有一些缺陷,具体表现在:
|
||||
|
||||
- 控制粒度粗糙,要么不打开 PGO,要么对所有 code 进行 PGO。如果项目中有 swift 代码,那么这种方式就不能用了,因为 swift 不支持 PGO;
|
||||
- 只支持两种方式采集 profiling data。第一种是每次手动运行,运行结束后退出 application,Xcode 会产生一个 xxx.profdata,之后的编译,都会用这个文件,来进行优化;如果代码发生变更,Xcode 会提示 profdata file out of date。第二种方法是借助 XCTest 来采集 profiling data,这种方法提供了一定的 automation 能力,但是另一方面也限制了 automation team 的手脚,他们可能在使用另一些更好用的工具而不是 XCTest。
|
||||
|
||||
因为 PGO 优化是靠的 Profile 文件,所以每次代码变化后需要保证生成最新的 Profile 文件。
|
||||
随着继续开发和更改应用项目中的代码,优化配置文件会过时。LLVM 编译器会识别出配置文件何时不再与应用程序的当前状态良好匹配,并提供警告。当收到此警告时,可以再次使用 Generate Optimization Profile 命令来创建更新的配置文件。每次重新生成优化配置文件时,Xcode 都会替换现有的配置文件数据。
|
||||
|
||||
这个很难办,所以需要利用 CI 手段,可以借助 `-fprofile-instr-generate` 和 `-fprofile-instr-use` 这两个 Clang 提供的编译选项搭配 CI。
|
||||
|
||||
## LLVM 利用 PGO 大概怎么优化代码
|
||||
|
||||
LLVM利用PGO(Profile-Guided Optimization)可以实现多种优化,其中一些主要优化包括:
|
||||
- 函数内联(Function Inlining):PGO可以根据实际运行时的函数调用情况,选择性地内联函数,减少函数调用的开销,提高程序执行效率。
|
||||
- 循环展开(Loop Unrolling):通过分析循环执行次数和循环体内的代码,PGO可以决定是否展开循环,减少循环控制开销,提高循环执行效率。
|
||||
- 代码重排(Code Reordering):根据实际运行时的代码执行路径,PGO可以优化代码的布局顺序,使得频繁执行的代码更容易被CPU缓存命中,提高程序的局部性和性能。
|
||||
- 分支优化(Branch Optimization):PGO可以根据实际运行时的分支预测情况,优化分支指令,减少分支预测错误,提高程序的执行效率。
|
||||
- 常量传播(Constant Propagation):根据实际运行时的数据流分析,PGO可以更好地进行常量传播,减少不必要的变量存储和加载操作,提高程序的执行效率。
|
||||
- 内存访问优化(Memory Access Optimization):PGO可以根据实际运行时的内存访问模式,优化内存访问方式,减少内存访问延迟,提高程序的内存访问效率。
|
||||
通过这些优化手段,PGO可以根据实际运行时的数据和行为模式,生成更加针对性和高效的优化代码,从而提高程序的性能和执行效率
|
||||
|
||||
|
||||
## PGO 和二进制重排的异同
|
||||
PGO 是一个编译器特性,能够过程序实际执行的方法进行打点统计,找出最常执行的代码路径(热点函数),并根据这些信息对程序进行优化,这种优化包括重排代码已减少分支预测错误、优化内存使用以提高缓存命中率、函数内联、分支优化等等,这是一种动态优化技术,会根据实际程序运行收集到的 profile 信息做改变。
|
||||
|
||||
二进制重排则是程序编译完成后,对二进制代码进行优化的技术,主要要解决的是内存缺页异常的问题,可以减少缓存的错失率。这是一种静态优化技术,因为它不需要实际运行程序就能进行。
|
||||
|
||||
|
||||
## 为什么我的App需要重排的符号个数这么少
|
||||
二进制重排主要通过调整二进制文件中的代码顺序,以改善性能。以 iOS 举例,App 启动慢的一个原因就是 App 启动过程中用到的函数方法、可能排布在不同的 page 上,所以由于不断的切换 page,导致启动慢。App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
|
||||
|
||||
然而,虽然理论上所有的代码都可以进行重排,但实际上,根据应用程序的特性,可能只有一部分代码是频繁执行的,也就是所谓的"热点"代码。这部分代码会被优先考虑进行重排,因为这样可以最大化性能提升。
|
||||
|
||||
另外,重排过程需要考虑到许多限制和约束,如符号之间的依赖关系,这可能会限制哪些符号可以移动和重新排序。如果一些符号因为它们之间的关系而不能被移动,那么这些符号就不会被考虑在重排中。
|
||||
|
||||
因此,你看到的"需要重排的符号个数"相对较少,可能是因为只有这些符号是被识别出来的"热点"代码,或者它们是唯一可以在不违反任何约束和限制的情况下被移动的符号
|
||||
69
Chapter1 - iOS/1.134.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 去除无用代码
|
||||
|
||||
通用方案
|
||||
Mach—O
|
||||
|
||||
__DATA , __objc_selrefs 标记方法被调用信息
|
||||
|
||||
otools -v -s _DATA _objc_selrefs Mach-O
|
||||
|
||||
|
||||
linkmap - selfrefs = 无用方法
|
||||
|
||||
问题:不准(OC 语言,动态性)
|
||||
|
||||
|
||||
## clang plugin
|
||||
|
||||
重载
|
||||
RecursiveASTVistor::visitDecl
|
||||
RecursiveASTVistor::visitStmt
|
||||
|
||||
上线前,通过静态方式去查找。不安全、不全面
|
||||
|
||||
|
||||
## 运行时查找
|
||||
Code Coverage
|
||||
clang -fprofile-instr-generate -fcoverafe-mapping a.m -o a
|
||||
swiftc -profile-generate -peofile-coverage-mapping a.swift
|
||||
|
||||
|
||||
缺点:难以定制
|
||||
|
||||
## Fuzzing 方案
|
||||
|
||||
## Sanitizee Coverage
|
||||
缺点: 编译慢、且无法进一步定制,包体积负向影响
|
||||
|
||||
## 自定义 llvm Pass
|
||||
|
||||
针对 LLVM IR 进行处理。
|
||||
|
||||
低级别编程语言,类似 RISC 指令集。和高级语言对应,LLVM 利用一些列 Pass 对 IR 进行优化。
|
||||
|
||||
LLVM 的优化是由 Pass 完成的,每个 Pass 完成特定的优化
|
||||
自己开发 Pass 是独立的,不会影响 LLVM 的结构
|
||||
Pass 之间可以有关联,也可分租
|
||||
|
||||
LLVM 有c/c++ 接口,还可以用 c/Swift 编写 Pass
|
||||
|
||||
c 接口较稳定,C++ 接口更新较新
|
||||
|
||||
|
||||
LLVM 内置 Pass
|
||||
memcpyopt: memset 指令替换 memcpy
|
||||
always_inline: 总是内联用 alwaysinline 修饰的函数
|
||||
dce:死代码消除
|
||||
loop_deletion:删除未使用的循环
|
||||
|
||||
|
||||
Pass 生成:
|
||||
静态:在 LLVM 工程中设置 CMake,重新构建 opt
|
||||
动态:opt 用 `-load-pass-plugin` 选项加载
|
||||
|
||||
怎么写 Pass?
|
||||
对 IR 分析,继承 AnalysisInfoMixin
|
||||
对 IR 转换,继承 PassInfoMixin
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,253 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
|
||||
|
||||

|
||||
|
||||
## 二、研究下对象在内存中如何存储?
|
||||
|
||||
```Objective-C
|
||||
|
||||
## 二、 类的本质
|
||||
|
||||
Demo1
|
||||
|
||||
```objective-c
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
NSObject *obj = [[NSObject alloc] init];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
因为 OC 本质就是 c/c++,所以转成 c/c++ 来窥探下,采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp`(不要采用 `clang -rewrite-objc main.m -o main.cpp` 因为这样生成的 c++ 文件由于没有指明设备和对应的架构指令集,不够准确)。
|
||||
|
||||
产看生成的 `main-arm64.cpp` 其中有段代码是定义结构体。
|
||||
|
||||
```c++
|
||||
struct NSObject_IMPL {
|
||||
Class isa;
|
||||
};
|
||||
```
|
||||
|
||||
然后点击 NSObject 跳转官方的声明可以看到
|
||||
|
||||
```c++
|
||||
@interface NSObject <NSObject> {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
|
||||
Class isa OBJC_ISA_AVAILABILITY;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
// 剔除一些无效信息后
|
||||
@interface NSObject {
|
||||
Class isa;
|
||||
}
|
||||
```
|
||||
|
||||
因此可以知道,OC 的类底层是由 c/c++ 的继承实现的。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
|
||||
|
||||
由于 obj 对象没有任何属性和方法,只有一个 isa 指针,且类的本质就是结构体,所以当结构体只有1个成员时,该成员的地址值,就是该结构体的地址。
|
||||
|
||||
即 isa 指针值为 0x100200110,结构体地址为 0x100200110,obj 指针值为 0x100200110。
|
||||
|
||||
|
||||
|
||||
Demo2
|
||||
|
||||
```objective-c
|
||||
@interface Student : NSObject
|
||||
{
|
||||
@public
|
||||
int _no;
|
||||
int _age;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation Student
|
||||
@end
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
Student *st = [[Student alloc] init];
|
||||
st->_no = 1;
|
||||
st->_age = 29;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp` 转换为 c++ 代码
|
||||
|
||||
```c++
|
||||
struct NSObject_IMPL {
|
||||
Class isa;
|
||||
};
|
||||
|
||||
struct Student_IMPL {
|
||||
struct NSObject_IMPL NSObject_IVARS; // 父类的所有 ivars。由于 Student 父类是 NSObject,所以其实只有 isa
|
||||
int _no;
|
||||
int _age;
|
||||
};
|
||||
```
|
||||
|
||||
`struct NSObject_IMPL NSObject_IVARS;` 代表父类的所有 ivars。由于 Student 父类是 NSObject,所以其实只有 isa
|
||||
|
||||
由于 `NSObject_IMPL` 结构体只有1个 isa 成员,所以上面代码等价于
|
||||
|
||||
```c++
|
||||
struct Student_IMPL {
|
||||
Class isa;
|
||||
int _no;
|
||||
int _age;
|
||||
};
|
||||
```
|
||||
|
||||
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StudentClassLayout.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StructPointerVistorClassIvars.png" style="zoom:25%">
|
||||
|
||||
发现是可以正确访问的。
|
||||
|
||||
为什么 `class_getInstanceSize` 打印出16?因为本质是计算所有 ivars 的内存大小,1个 isa,2个 int 就是16.
|
||||
|
||||
而 `malloc_size` 是系统真正分配的,由于最小分配16,所以刚好就是16.
|
||||
|
||||
|
||||
|
||||
Demo3
|
||||
|
||||
```objective-c
|
||||
@interface Person : NSObject
|
||||
{
|
||||
@public
|
||||
int _age;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation Person
|
||||
@end
|
||||
|
||||
@interface Student : Person
|
||||
{
|
||||
@public
|
||||
int _no;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation Student
|
||||
|
||||
@end
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
Person *p = [[Person alloc] init];
|
||||
NSLog(@"%zd", class_getInstanceSize([Person class])); // 16
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 16
|
||||
Student *st = [[Student alloc] init];
|
||||
NSLog(@"%zd", class_getInstanceSize([Student class])); // 16
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)st)); // 16
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp` 转换为 c++ 代码
|
||||
|
||||
```c++
|
||||
struct NSObject_IMPL {
|
||||
Class isa;
|
||||
};
|
||||
|
||||
struct Person_IMPL {
|
||||
struct NSObject_IMPL NSObject_IVARS;
|
||||
int _age;
|
||||
};
|
||||
|
||||
struct Student_IMPL {
|
||||
struct Person_IMPL Person_IVARS;
|
||||
int _no;
|
||||
};
|
||||
```
|
||||
|
||||
为什么 `class_getInstanceSize([Person class])` 也是16,不是8+4吗?因为存在内存对齐,结构体的大小必须是最大成员大小的倍数(Person 中也就是8的倍数)
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
Demo4
|
||||
|
||||
```objective-c
|
||||
@interface Person : NSObject
|
||||
{
|
||||
@public
|
||||
int _age;
|
||||
int _no;
|
||||
int _height;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation Person
|
||||
@end
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
Person *p = [[Person alloc] init];
|
||||
NSLog(@"%zd", class_getInstanceSize([Person class])); // 24
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp` 转换为 c++ 代码
|
||||
|
||||
```c++
|
||||
struct NSObject_IMPL {
|
||||
Class isa;
|
||||
};
|
||||
struct Person_IMPL {
|
||||
struct NSObject_IMPL NSObject_IVARS;
|
||||
int _age;
|
||||
int _no;
|
||||
int _height;
|
||||
};
|
||||
// ->
|
||||
struct Person_IMPL {
|
||||
Class isa;
|
||||
int _age;
|
||||
int _no;
|
||||
int _height;
|
||||
};
|
||||
```
|
||||
|
||||
2个问题:
|
||||
|
||||
- `class_getInstanceSize` 不是一个 isa 8 + 3个 Int 4 = 8 + 3 * 4 = 20吗?查看源码知道 `class_getInstanceSize` 返回的是内存对齐后的成员变量内存大小
|
||||
- `malloc_size` 为什么是32?只需要24字节,为什么分配32?
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 三、研究下对象在内存中如何存储?
|
||||
|
||||
```objective-c
|
||||
Person *p1 = [Person new]
|
||||
```
|
||||
|
||||
@@ -25,10 +269,16 @@ Person *p1 = [Person new]
|
||||
new底层做的事情:
|
||||
|
||||
* 在堆内存中申请1块合适大小的空间
|
||||
* 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做**isa**,是一个指针,指向对象所属的类在代码段中地址
|
||||
* 初始化对象的属性。这里初始化有几个原则:a、如果属性的数据类型是基本数据类型则赋值为0;b、如果属性的数据类型是C语言的指针类型则赋值为NULL;c、如果属性的数据类型为OC的指针类型则赋值为nil。
|
||||
* 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做 **isa**,是一个指针,指向对象所属的类在代码段中地址
|
||||
* 初始化对象的属性。这里初始化有几个原则:
|
||||
* 如果属性的数据类型是基本数据类型则赋值为 0
|
||||
* 如果属性的数据类型是 C 语言的指针类型则赋值为 NULL
|
||||
* 如果属性的数据类型为 OC 的指针类型则赋值为 nil
|
||||
|
||||
* 返回堆空间上对象的地址
|
||||
|
||||
|
||||
|
||||
注意:
|
||||
|
||||
* 对象只有属性,没有方法。包括类本身的属性和一个指向代码段中的类isa指针
|
||||
@@ -78,7 +328,7 @@ int main(int argc, const char * argv[]) {
|
||||
}
|
||||
```
|
||||
|
||||
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
|
||||
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
|
||||
|
||||

|
||||
|
||||
@@ -89,7 +339,26 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
|
||||
|
||||
## 三、一个对象占用多少内存空间?
|
||||
|
||||
|
||||
## 四、一个对象占用多少内存空间?
|
||||
|
||||
Demo
|
||||
|
||||
```objective-c
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
NSObject *obj = [[NSObject alloc] init];
|
||||
// 获取类的实例对象的成员变量所占用内存大小
|
||||
NSLog(@"%zd", class_getInstanceSize([NSObject class])); // 8
|
||||
// 获取 obj 指针,所指向内存大小
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)obj)); // 16
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
为什么一个是8,一个是16?查看 objc 源代码可以看到:
|
||||
|
||||
```objectivec
|
||||
size_t class_getInstanceSize(Class cls)
|
||||
@@ -161,38 +430,164 @@ size_t instanceSize(size_t extraBytes) {
|
||||
}
|
||||
```
|
||||
|
||||
用2种方式获取:
|
||||
`alloc` 本质上调用的就是 `_objc_rootAllocWithZone` ,继续查看源码
|
||||
|
||||
- class_getInstanceSize([NSObject class]):8。返回实例对象的成员变量所占用的内存大小,一个空对象,只有 isa 指针,所以只有8字节
|
||||
- malloc_size((__bridge const void *)obj):16。Apple 规定,对象至少16个字节。但是只有一个 isa,所以只占用8个字节。
|
||||
内存对齐:结构体的最终大小必须是最大成员的倍数。比如
|
||||
```c++
|
||||
// NSObject.mm
|
||||
id
|
||||
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
|
||||
{
|
||||
id obj;
|
||||
|
||||
#if __OBJC2__
|
||||
// allocWithZone under __OBJC2__ ignores the zone parameter
|
||||
(void)zone;
|
||||
obj = class_createInstance(cls, 0);
|
||||
#else
|
||||
if (!zone) {
|
||||
obj = class_createInstance(cls, 0);
|
||||
}
|
||||
else {
|
||||
obj = class_createInstanceFromZone(cls, 0, zone);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
|
||||
return obj;
|
||||
}
|
||||
```
|
||||
|
||||
继续调用的是 `class_createInstance`
|
||||
|
||||
```c++
|
||||
// objc-runtime-new.mm
|
||||
id
|
||||
class_createInstance(Class cls, size_t extraBytes)
|
||||
{
|
||||
return _class_createInstanceFromZone(cls, extraBytes, nil);
|
||||
}
|
||||
|
||||
static __attribute__((always_inline))
|
||||
id
|
||||
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
|
||||
bool cxxConstruct = true,
|
||||
size_t *outAllocatedSize = nil)
|
||||
{
|
||||
if (!cls) return nil;
|
||||
|
||||
assert(cls->isRealized());
|
||||
|
||||
// Read class's info bits all at once for performance
|
||||
bool hasCxxCtor = cls->hasCxxCtor();
|
||||
bool hasCxxDtor = cls->hasCxxDtor();
|
||||
bool fast = cls->canAllocNonpointer();
|
||||
|
||||
size_t size = cls->instanceSize(extraBytes);
|
||||
if (outAllocatedSize) *outAllocatedSize = size;
|
||||
|
||||
id obj;
|
||||
if (!zone && fast) {
|
||||
obj = (id)calloc(1, size);
|
||||
if (!obj) return nil;
|
||||
obj->initInstanceIsa(cls, hasCxxDtor);
|
||||
}
|
||||
else {
|
||||
if (zone) {
|
||||
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
|
||||
} else {
|
||||
obj = (id)calloc(1, size);
|
||||
}
|
||||
if (!obj) return nil;
|
||||
|
||||
// Use raw pointer isa on the assumption that they might be
|
||||
// doing something weird with the zone or RR.
|
||||
obj->initIsa(cls);
|
||||
}
|
||||
|
||||
if (cxxConstruct && hasCxxCtor) {
|
||||
obj = _objc_constructOrFree(obj, cls);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到调用的是 c 的 `obj = (id)calloc(1, size);`,其中 size 是前面计算好的,继续看看这个 size 的计算 `size_t size = cls->instanceSize(extraBytes);`
|
||||
|
||||
```c++
|
||||
// objc-runtime-new.h
|
||||
size_t instanceSize(size_t extraBytes) {
|
||||
size_t size = alignedInstanceSize() + extraBytes;
|
||||
// CF requires all objects be at least 16 bytes.
|
||||
if (size < 16) size = 16;
|
||||
return size;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到计算好 size 之后有个判断,如果小于16,则赋值为16,也就是最小为16(`CF requires all objects be at least 16 bytes`)。
|
||||
|
||||
结论:我们用2种方式获取内存大小,其中
|
||||
|
||||
- `class_getInstanceSize([NSObject class])` :8,返回实例对象内存对齐后的的成员变量所占用的内存大小(即代码注释的 `Class's ivar size rounded up to a pointer-size boundary.` ),一个空对象,只有 isa 指针,所以只有8字节。可以理解为 **创建一个对象,至少需要多少内存**
|
||||
- `malloc_size((__bridge const void *)obj)` :16,Apple 规定,对象至少16个字节。但是只有一个 isa,所以只占用8个字节。
|
||||
内存对齐:结构体的最终大小必须是最大成员的倍数。可以理解为**创建一个对象,实际上分配了多少内存**
|
||||
|
||||
- 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
|
||||
- 但 NSObject 对象内部只使用了8个字节的空间(64位环境下,通过 class_getInstanceSize 函数获得)
|
||||
|
||||
|
||||
|
||||
## 五、属性和方法
|
||||
|
||||
```objective-c
|
||||
@interface Person : NSObject
|
||||
{
|
||||
@public
|
||||
int _age;
|
||||
}
|
||||
@property (nonatomic, assign) NSInteger height;
|
||||
@end
|
||||
```
|
||||
|
||||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp`
|
||||
|
||||
```c++
|
||||
struct NSObject_IMPL {
|
||||
Class isa;
|
||||
};
|
||||
|
||||
```c
|
||||
struct Person_IMPL {
|
||||
struct NSObject_IMPL NSObject_IVARS; // 8字节
|
||||
int age; // 4字节
|
||||
struct NSObject_IMPL NSObject_IVARS;
|
||||
int _age;
|
||||
NSInteger _height;
|
||||
};
|
||||
```
|
||||
|
||||
8*2=16字节
|
||||
我们知道 `@property` 的本质是生成一个带下划线的 ivar,即 `_height`,还有 height 的 getter、setter 方法。为什么在结构体里没有看到方法?因为对象可以存在多个,这些方法的实现需要公用,没必要每个对象里都保存一份。
|
||||
|
||||
## 四、类继承的本质
|
||||
|
||||
写一个最基础的类
|
||||
|
||||
## 五、类继承的本质
|
||||
|
||||
QA: 结构体计算大小为什么需要内存对齐?
|
||||
|
||||
iOS 分配内存,为什么需要内存对齐?libmalloc 可以看到至少是16的倍数。
|
||||
|
||||
写一个最基础的 Person 类
|
||||
|
||||
```objectivec
|
||||
@interface Person:NSObject
|
||||
@interface Person : NSObject
|
||||
@end
|
||||
|
||||
@implementation Person
|
||||
@end
|
||||
```
|
||||
|
||||
clang 转为 c 代码看看, `xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp`
|
||||
clang 转为 c 代码看看,因为同样的代码经过 clang 后转成 c/c++ 后,不同平台具有不同的实现,所以为了精确研究 iOS,最好指明 arm64 架构后再研究,具体指令为:`xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp`
|
||||
|
||||
```c
|
||||
struct NSObject_IMPL {
|
||||
Class isa;
|
||||
Class isa;
|
||||
}
|
||||
|
||||
struct Person_IMPL {
|
||||
@@ -200,20 +595,81 @@ struct Person_IMPL {
|
||||
};
|
||||
```
|
||||
|
||||
如果创建一个继承自 Person 的 Student 类呢
|
||||
如果给 Person 增加属性
|
||||
|
||||
```objective-c
|
||||
@interface Person : NSObject
|
||||
@property (nonatomic, assign) double height;
|
||||
@property (nonatomic, assign) double weight;
|
||||
@property (nonatomic, assign) int salary;
|
||||
- (void)test;
|
||||
@end
|
||||
|
||||
@implementation Person
|
||||
@end
|
||||
```
|
||||
|
||||
创建一个继承自 Person 的 Student 类
|
||||
|
||||
```objective-c
|
||||
@interface Student : Person
|
||||
@property (nonatomic, assign) NSInteger score;
|
||||
- (void)test;
|
||||
@end
|
||||
|
||||
@implementation Student
|
||||
@end
|
||||
```
|
||||
|
||||
利用 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp` 转换为 c++,将主要的摘出来
|
||||
|
||||
```c++
|
||||
struct NSObject_IMPL {
|
||||
Class isa;
|
||||
};
|
||||
|
||||
struct Person_IMPL {
|
||||
struct NSObject_IMPL NSObject_IVARS;
|
||||
int _salary;
|
||||
double _height;
|
||||
double _weight;
|
||||
};
|
||||
|
||||
```c
|
||||
struct Student_IMPL {
|
||||
struct Person_IMPL Person_IVARS;
|
||||
NSInteger _age;
|
||||
NSString *_name;
|
||||
struct Person_IMPL Person_IVARS;
|
||||
NSInteger _score;
|
||||
};
|
||||
```
|
||||
|
||||
首先我们可以知道一个 OC 类的属性可以写多种数据类型,那么大概率是 C 结构体实现。用 clang 转为 c 可以得到印证。另外存在类的继承关系的时候,子类结构体中第一个信息是父类结构体对象。其次是当前子类自己的信息。根节点一定是 NSObject_IMPL 结构体,且其中只有 `Class isa`
|
||||
结构体 `Person_IMPL` 等价于
|
||||
|
||||
```c++
|
||||
struct Person_IMPL {
|
||||
Class isa;
|
||||
int _salary;
|
||||
double _height;
|
||||
double _weight;
|
||||
};
|
||||
```
|
||||
|
||||
结构体 `Student_IMPL` 等价于
|
||||
|
||||
```c++
|
||||
struct Student_IMPL {
|
||||
Class isa;
|
||||
int _salary;
|
||||
double _height;
|
||||
double _weight;
|
||||
NSInteger _score;
|
||||
};
|
||||
```
|
||||
|
||||
首先我们可以知道一个 OC 类的属性可以写多种数据类型,那么大概率是 C 结构体实现。用 clang 转为 c 可以得到印证。另外存在类的继承关系的时候:**子类结构体中第一个信息是父类结构体对象;其次是当前子类自己的信息;根节点一定是 NSObject_IMPL 结构体;且其中只有 `Class isa`**。也就是说,**一个实例对象,内部的第一个成员就是 isa 指针,其次是父类属性,最后的自己的属性。且 isa 指针地址就是当前实例对象的地址**。
|
||||
|
||||
|
||||
|
||||
观察 clang 转换后的 c 代码,发现 property 没有看到 setter、getter 方法。为什么这么设计?
|
||||
方法不会存储到实例对象中去的。因为方法在各个对象中是通用的,方法存储在类对象的方法列表中。
|
||||
**方法不会存储到实例对象中去的。因为方法在各个对象中是通用的,方法存储在类对象的方法列表中。**
|
||||
|
||||
```objectivec
|
||||
@interface Person:NSObject
|
||||
@@ -245,10 +701,15 @@ NSLog(@"%zd", sizeof(person)); // 24,这个数值代表我们这个类,这
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32。iOS 系统会做优化,比如为了加速访问速度,会按照16的倍数进行分配。
|
||||
```
|
||||
|
||||
`class_getInstanceSize`这个数值代表我们这个类,这个结构体,创建出来至少只需要24字节. `malloc_size` iOS 系统会做优化,比如为了加速访问速度,会按照16的倍数进行分配。
|
||||
`class_getInstanceSize`这个数值代表我们这个类,这个结构体,创建出来至少只需要24字节,`malloc_size` iOS 系统会做优化,比如为了加速访问速度,会按照16的倍数进行分配。
|
||||
|
||||
iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内存的时候也存在内存对齐。
|
||||
GUN 都存在内存对齐这个概念。
|
||||
sizeof 是运算符。
|
||||
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
实例对象:
|
||||
类对象:isa、superclass、属性信息、对象方法信息、协议信息、成员变量信息...
|
||||
@@ -469,7 +930,10 @@ class_rw_t *studentMetaClassData = studentClass->metaClass()->data();
|
||||
class_rw_t *personMetaClassData = personClass->metaClass()->data();
|
||||
```
|
||||
|
||||
## 五、 内存对齐
|
||||
|
||||
|
||||
## 六、 内存对齐
|
||||
|
||||
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
|
||||
1. 提高访问速度:内存对齐可以使数据在内存中的存储更加高效,因为大部分计算机体系结构都要求数据按照特定的边界对齐,这样可以减少内存访问的次数,提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
|
||||
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
|
||||
@@ -542,7 +1006,7 @@ NSLog(@"%zd", malloc_size(temp));
|
||||
|
||||
可以看到 malloc 申请了4个字节,但是打印却看到16个字节。
|
||||
|
||||
查看 libmalloc源码也可以出来分配内存最小是以16的倍数为基准进行分配的。
|
||||
查看 libmalloc 源码也可以出来分配内存最小是以16的倍数为基准进行分配的。
|
||||
|
||||
```c
|
||||
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
|
||||
@@ -552,6 +1016,141 @@ NSLog(@"%zd", malloc_size(temp));
|
||||
|
||||
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
|
||||
|
||||
上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GlibcInXcodeProject.png" style="zoom:25%">
|
||||
|
||||
可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT`
|
||||
在 i386 里面是16,在非 i386 里面有个判断
|
||||
```c
|
||||
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
|
||||
? __alignof__ (long double) : 2 * SIZE_SZ)
|
||||
```
|
||||
三目运算符的后2个结果分别是 `__alignof__ (long double)` 和 `2 * SIZE_SZ`。其中 `SIZE_SZ` 又是一个宏定义,等价于 `(sizeof (INTERNAL_SIZE_T))`,即 `2*sizeof(size_t)`
|
||||
```c
|
||||
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
|
||||
# define INTERNAL_SIZE_T size_t
|
||||
```
|
||||
在 Xcode 打印输出, `__alignof__ (long double)` 为16,`sizeof(size_t)` 为8,即 `2 * SIZE_SZ` = 16,所以不管怎么看,在 GUN 里面内存对齐一定都是16.
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GLibcMallocAlignment.png" style="zoom:25%">
|
||||
Todo: 研究探索 libmalloc 源码
|
||||
|
||||
|
||||
Todo: 研究探索 glibc 源代码,分析内存对齐、内存分配的原理
|
||||
|
||||
## 七、class 对象
|
||||
|
||||
```objective-c
|
||||
// instance 对象,实例对象
|
||||
NSObject *obj1 = [[NSObject alloc] init];
|
||||
NSObject *obj2 = [[NSObject alloc] init];
|
||||
// class 对象,类对象
|
||||
Class cls1 = [obj1 class];
|
||||
Class cls2 = [obj2 class];
|
||||
Class cls3 = object_getClass(obj1);
|
||||
Class cls4 = object_getClass(obj2);
|
||||
Class cls5 = [NSObject class];
|
||||
NSLog(@"%p %p", obj1, obj2); // 0x600000004040 0x600000004050
|
||||
NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5); // 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270
|
||||
```
|
||||
|
||||
- cls1...cls5 都是 NSObject 的 class 对象,也就是类对象。
|
||||
- 它都是同一个对象,每个类在内存中有且只有一个 class 对象
|
||||
|
||||
|
||||
|
||||
Class 对象在内存中存储的信息主要包括:
|
||||
|
||||
- isa 指针
|
||||
- superclass 指针
|
||||
- 类的属性信息(@property)、类的对象方法信息(instance method)
|
||||
- 类的协议信息(@protocol、)类的成员变量信息(ivars)
|
||||
|
||||
|
||||
|
||||
## 八、元类对象
|
||||
|
||||
Objective-C 中对象分维三类:
|
||||
|
||||
- instance 对象,实例对象。例如 `NSObject *obj1 = [[NSObject alloc] init];`
|
||||
- class 对象,类对象。例如 `Class cls1 = [obj1 class];`
|
||||
- 元类对象(meta-class)。例如 `Class metaClass = object_getClass(cls1);`
|
||||
|
||||
如何获取元类对象?
|
||||
|
||||
利用 runtime `object_getClass`API,传入类对象获取。例如 `Class objectMetaClass = object_getClass([NSObject class])`
|
||||
|
||||
不可以通过2次调用 class 方法获取 meta-class 对象。调用 class 方法只可以获取到 class 对象。
|
||||
|
||||
- 每个类在 内存中有且只有一个 meta-class 对象
|
||||
|
||||
- meta-class 对象和 Class 对象的内存结构是一样的(都是 Class),但是用途不一样,在内存中存储的信息主要包括:
|
||||
|
||||
- isa 指针
|
||||
- superclass 指针
|
||||
- 类的类方法信息(class method)
|
||||
|
||||
如何判断是否是元类对象 `class_isMetaClass()`
|
||||
|
||||
Demo:
|
||||
|
||||
```objective-c
|
||||
// instance 对象,实例对象
|
||||
NSObject *obj1 = [[NSObject alloc] init];
|
||||
NSObject *obj2 = [[NSObject alloc] init];
|
||||
// class 对象,类对象
|
||||
// class 方法返回的就是类对象
|
||||
Class cls1 = [obj1 class];
|
||||
Class cls2 = [obj2 class];
|
||||
Class cls3 = object_getClass(obj1);
|
||||
Class cls4 = object_getClass(obj2);
|
||||
Class cls5 = [NSObject class];
|
||||
NSLog(@"%p %p", obj1, obj2);
|
||||
NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5);
|
||||
|
||||
// 将类对象当作参数传入,获取元类对象(meta-class)
|
||||
Class metaClass = object_getClass(cls1);
|
||||
Class metaClass2 = [cls2 class];
|
||||
NSLog(@"%p %p", metaClass, metaClass2);
|
||||
|
||||
BOOL isMetaClass = class_isMetaClass(metaClass);
|
||||
NSLog(@"%@", isMetaClass ? @"是元类对象" : @"不是元类对象");
|
||||
|
||||
isMetaClass = class_isMetaClass(metaClass2);
|
||||
NSLog(@"%@", isMetaClass ? @"是元类对象" : @"不是元类对象");
|
||||
|
||||
// console
|
||||
0x60000000c030 0x60000000c040
|
||||
0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270
|
||||
0x7ff85ec7c220 0x7ff85ec7c270
|
||||
是元类对象
|
||||
不是元类对象
|
||||
```
|
||||
|
||||
objc 源代码中:
|
||||
|
||||
```c++
|
||||
Class object_getClass(id obj)
|
||||
{
|
||||
// 传入的如果是实例对象,则返回 class 类对象
|
||||
// 传入的如果是 class 类对象,则返回 meta-class 元类对象
|
||||
// 传入的如果是 meta-class 元类对象,返回 NSObject(基类)的 meta-class 元类对象
|
||||
if (obj) return obj->getIsa();
|
||||
else return Nil;
|
||||
}
|
||||
|
||||
Class objc_getClass(const char *aClassName)
|
||||
{
|
||||
if (!aClassName) return Nil;
|
||||
|
||||
// NO unconnected, YES class handler
|
||||
return look_up_class(aClassName, NO, YES);
|
||||
}
|
||||
```
|
||||
|
||||
`object_getClass`:
|
||||
|
||||
- 传入的如果是实例对象,则返回 class 类对象
|
||||
- 传入的如果是 class 类对象,则返回 meta-class 元类对象
|
||||
- 传入的如果是 meta-class 元类对象,返回 NSObject(基类)的 meta-class 元类对象
|
||||
|
||||
`-(Class)class`、`+(Class)class`:返回的是类对象
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ int main(int argc, const char* argv[])
|
||||
|
||||
dyld 本身就是一种 MachO 文件,MachO 文件类型为7,是一种包含多种架构的结构,如下图:
|
||||
|
||||
<img src="./../assets/DyldStructure.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/DyldStructure.png" style="zoom:25%">
|
||||
|
||||
可以加载以下类型的 Mach-O 文件:MH_EXECUTE、MH_DYLIB、MH_BUNDLE
|
||||
|
||||
@@ -127,7 +127,7 @@ Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||
Tips:`file` 命令可以查看文件类型。
|
||||
|
||||
<img src="./../assets/FileCommandToWatchFileType.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/FileCommandToWatchFileType.png" style="zoom:45%">
|
||||
|
||||
`find . -name "*.c"` 比如在当前路径查找 .c 文件
|
||||
|
||||
@@ -160,7 +160,7 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
|
||||
|
||||
### Mach-O 结构
|
||||
|
||||
<img src="./../assets/Mach-OStructure.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Mach-OStructure.png" style="zoom:45%">
|
||||
|
||||
一个 Mach-O 文件包含3块
|
||||
|
||||
@@ -172,17 +172,17 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
|
||||
|
||||
可以用 [MachOView:](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
|
||||
|
||||
<img src="./../assets/otoolhelp.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/otoolhelp.png" style="zoom:25%">
|
||||
|
||||
比如我用 otool 查看我编写的一个 SwiftUIDemo 所依赖的共享缓存库
|
||||
|
||||
<img src="./../assets/SwiftUIDemoDependencyLibrary.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftUIDemoDependencyLibrary.png" style="zoom:25%">
|
||||
|
||||
用 MachOView 查看 DDD Mach-O 文件
|
||||
|
||||
<img src="./../assets/MachOPageZero.png" style="zoom:35%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOPageZero.png" style="zoom:35%">
|
||||
|
||||
<img src="./../assets/MachOText.png" style="zoom:35%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOText.png" style="zoom:35%">
|
||||
|
||||
可以看到在 Mach-O 文件上,`__PAGEZERO` 的 `VM Size` 有值,但是 File Size 为0,也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0(如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0)。
|
||||
|
||||
@@ -208,13 +208,13 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
|
||||
|
||||
也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
|
||||
|
||||
<img src="./../assets/MachOInsepect.png" style="zoom:35%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOInsepect.png" style="zoom:35%">
|
||||
|
||||
利用 MachOView 查看如下:
|
||||
|
||||
<img src="./../assets/MachOViewDemo1.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOViewDemo1.png" style="zoom:25%">
|
||||
|
||||
<img src="./../assets/MachOViewDemo2.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOViewDemo2.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
|
||||
|
||||
|
||||
|
||||
<img src="./../assets/ASLRDemo.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ASLRDemo.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ File Size:在 Mach-O 文件中的占据的大小
|
||||
|
||||
从下面的图可以看出,`_PAGEZERO` 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。
|
||||
|
||||
<img src="./../assets/MachOViewDemo1.png" style="zoom:25%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOViewDemo1.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ File Size:在 Mach-O 文件中的占据的大小
|
||||
|
||||
Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
|
||||
|
||||
<img src="./../assets/ASLROffset.png" style="zoom:45%">
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ASLROffset.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
|
||||
2
Chapter1 - iOS/Untitled-1
Normal file
@@ -0,0 +1,2 @@
|
||||
//print(MemoryLayout.stride(ofValue: str1))
|
||||
//print(Mems.memStr(ofVal: &str1))
|
||||
@@ -111,7 +111,7 @@
|
||||
* [105、iOS 界面渲染流程](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.105.md)
|
||||
* [106、NSUserDefault 底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.106.md)
|
||||
* [107、IM技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.107.md)
|
||||
* [108、精准测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
|
||||
* [108、精准测试最佳实践](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
|
||||
* [109、汇编学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.109.md)
|
||||
* [110、妙用设计模式来设计一个客户端校验器](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.110.md)
|
||||
* [111、写给 iOSer 的鸿蒙开发 tips](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.111.md)
|
||||
|
||||
BIN
assets/BasicBlockGraphics.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/ClangStubFullProgress.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/CodeBasicBlockFlow.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/CodeCoverageAnalysisReport.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
assets/CodeCoverageInClass.png
Normal file
|
After Width: | Height: | Size: 537 KiB |
BIN
assets/CodeQualityChecklist.PNG
Normal file
|
After Width: | Height: | Size: 524 KiB |
BIN
assets/GCCCannotCaptureSwiftCodeCoverage.png
Normal file
|
After Width: | Height: | Size: 616 KiB |
BIN
assets/GLibcMallocAlignment.png
Normal file
|
After Width: | Height: | Size: 985 KiB |
BIN
assets/GccCodeCoverageFlow.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
assets/GcdaFileViaGcov.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
assets/GcnoAndGcdaFilesInSameDir.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
assets/GcovDumpAssembly.png
Normal file
|
After Width: | Height: | Size: 783 KiB |
BIN
assets/GitDiffDIsplay.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
assets/GlibcInXcodeProject.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
assets/LLVMBasicBlockStructure.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/LLVMCodeCoverageIRPass.png
Normal file
|
After Width: | Height: | Size: 893 KiB |
BIN
assets/LcovCodeCoverageHTMLPath.png
Normal file
|
After Width: | Height: | Size: 995 KiB |
BIN
assets/LcovCombineGcovAndGcda.png
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
assets/LcovGenerateCoverageHTML.png
Normal file
|
After Width: | Height: | Size: 714 KiB |
BIN
assets/MachOSpaceToRecordCodeInstrucment.png
Normal file
|
After Width: | Height: | Size: 553 KiB |
BIN
assets/MachOViewSwiftCodeCoverageInfo.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
assets/OCCodeCoverageCombinedReport.png
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
assets/OCCodeCoverageInfoFileCombined.png
Normal file
|
After Width: | Height: | Size: 535 KiB |
BIN
assets/OCCodeInstrucment.png
Normal file
|
After Width: | Height: | Size: 555 KiB |
BIN
assets/OCObjectLayoutWhenISA.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/StructPointerVistorClassIvars.png
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
assets/StudentClassExtendsFromPersonClass.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
assets/StudentClassLayout.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/SwiftCCompileOptionsAboutCoverage.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
assets/SwiftCodeCoverageInCommandLine.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
assets/SwiftCodeCoverageMVProdrawAndMachO.png
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
assets/SwiftCodeCoverageProgress.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/SwiftCodeCoverageReport.png
Normal file
|
After Width: | Height: | Size: 670 KiB |
BIN
assets/SwiftCoverageFromCombinedProfdataText.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/SwiftProfdataCombinedAndCoverageReport.png
Normal file
|
After Width: | Height: | Size: 618 KiB |
BIN
assets/SwiftProfdataTextChangeBBCount.png
Normal file
|
After Width: | Height: | Size: 968 KiB |
BIN
assets/SwiftcDisplaySwiftCodeCoverageInCommandLine.png
Normal file
|
After Width: | Height: | Size: 548 KiB |
BIN
assets/XcodeAnotherGcnoFiles.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
assets/XcodeAssemblyProveBasicBlockCodeInstrument.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/XcodeBuildErrorWithGcovFlush.png
Normal file
|
After Width: | Height: | Size: 682 KiB |
BIN
assets/XcodeEnableCodeCoverageSetting.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
assets/XcodeGcnoFileLocation.png
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
assets/XcodeGcnoFiles.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
assets/XcodeGenerateProfDataAboutSwiftCoverage.png
Normal file
|
After Width: | Height: | Size: 579 KiB |
BIN
assets/XcodeSetTestCoverageOptions.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
assets/XcodeShowGcdaPath.png
Normal file
|
After Width: | Height: | Size: 648 KiB |
BIN
assets/XcodeSwiftCodeCoverageViaAssembly.png
Normal file
|
After Width: | Height: | Size: 705 KiB |
BIN
assets/XcodeSwiftCoveraeCompileOptions.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
assets/XcoedeViewSizeOfViaAssembly.png
Normal file
|
After Width: | Height: | Size: 860 KiB |
BIN
assets/YouzanCodeCoverageUsage.png
Normal file
|
After Width: | Height: | Size: 343 KiB |