ninja介绍及使用
ninja
简介
ninja 是Google
的一名程序员推出的注重速度的构建工具.一般在Unix/Linux上的程序通过make/makefile
来构建编译,而ninja
通过将编译任务并行组织,大大提高了构建速度。
执行
ninja [-options] targets
支持参数
--version # 打印版本信息
-v # 显示构建中的所有命令行(这个对实际构建的命令核对非常有用)-C DIR # 在执行操作之前,切换到`DIR`目录
-f FILE # 制定`FILE`为构建输入文件。默认文件为当前目录下的`build.ninja`。如 ./ninja -f demo.ninja-j N # 并行执行 N 个作业。默认N=3(需要对应的CPU支持)。如 ./ninja -j 2 all
-k N # 持续构建直到N个作业失败为止。默认N=1
-l N # 如果平均负载大于N,不启动新的作业
-n # 排练(dry run)(不执行命令,视其成功执行。如 ./ninja -n -t clean)-d MODE # 开启调试模式 (用 -d list 罗列所有的模式)
-t TOOL # 执行一个子工具(用 -t list 罗列所有子命令工具)。如 ./ninja -t query all
-w FLAG # 控制告警级别
特点
- 可以通过其他高级的编译系统生成其输入文件;
- 它的设计就是为了更快的编译;
通过其他高级的编译系统生成其输入文件
Ninja与Android
安卓编译系统演进历史
Google
在 Android 7.0
之前都是使用的makefile
进行编译,7.0开始引入了Soong构建系统
,旨在取代make
,它利用 Kati GNU Make 克隆工具
和 ninja 构建系统组件
来加速 Android
的构建。
生成ninja文件
- 工具链关系
Android.bp --> Blueprint --> Soong --> ninja
Makefile or Android.mk --> kati --> ninja
(Android.mk --> Soong --> Blueprint --> Android.bp)
在编译过程中,Android.bp
会被收集到out/soong/build.ninja.d
,blueprint
以此为基础,生成out/soong/build.ninja
Android.mk
会由kati
/ckati
生成为out/build-aosp_arm.ninja
两个ninja文件会被整合进入out/combined-$product_$arch.ninja
- combined-$product_$arch.ninja
$ source build/envsetup.sh
$ lunch pixel3_mainline-userdebug
$ make nothing
$ cat out/combined-pixel3_mainline.ninja builddir = out
pool highmem_pooldepth = 2
subninja out/build-pixel3_mainline.ninja
subninja out/build-pixel3_mainline-package.ninja
subninja out/soong/build.ninja
- 执行
prebuilts/build-tools/linux-x86/bin/ninja \ -f out/combined-pixel3_mainline.ninja
ninja本身
ninja本身就是通过ninja编译出来的
源码获取及编译
git clone https://android.googlesource.com/platform/external/ninjapython3 configure.py --bootstrap
编译过程
- 生成一个build.ninja
- 执行python3 configure.py --bootstrap之后编译源码,生成一个a.out
- 根据这个build.ninja重新编译生成可执行文件ninja
- 在 ninja 根据 ninja.build 来编译时会自动创建一个 build 目录用于存放编译过程中的临时文件
ninja_syntax.py
Ninja提供了一个简单的生成脚本,它实际上是一个python模块misc/ninja_syntax.py
,通过它我们可以较方便的生成build.ninja
文件
from ninja_syntax import Writerwith open("build.ninja", "w") as buildfile:n = Writer(buildfile)if platform.is_msvc():n.rule("link",command="$cxx $in $libs /nologo /link $ldflags /out:$out",description="LINK $out")else:n.rule("link",command="$cxx $ldflags -o $out $in $libs",description="LINK $out")
为了更快的编译
ninja启动过程
与makefile的对比
$ time ninja
ninja 39.24s user 2.16s system 1021% cpu 4.053 total
3.79s$ time make
make 22.29s user 1.59s system 101% cpu 23.543 total
23.13s
语法及概念
- edge(边):build语句,可以指定目标(target)输出(output)、规则(rule)与输入(input)
- target(目标):编译过程需要生成的目标,由build语句指定
- output(输出):build语句的前半段,是target的另一种称呼
- input(输入):build语句的后半段,用于产生output的文件或目标,另一种称呼是依赖
- rule(规则):通过指定command与一些内置变量,决定如何从输入产生输出
- pool(池):一组rule或edge,通过指定其depth,可以控制并行上限
- scope(作用域):变量的作用范围,有rule与build语句的块级,也有文件级别。
Gcc = gcc # 全局变量# rule
rule name # name是rule名command = ${Gcc} ${in} > ${out} # 执行命令var = str # 局部变量
# Edge
# output0 output1 显示输出
# output2 output3 隐式输出
# rule_name 规则名称
build output0 output1 | output2 output3: rule_name $ input0 input1 $ # 显示依赖| input2 input3 $ # 隐式依赖|| input4 input5 # order-only依赖 可有可无var0 = str0 var1 = str1
底层的数据结构
回到这张图,我们来看Ninja的底层的如何处理的(以下数据结构只保留到最简的部分)
State
State
保存单次运行的全局状态
struct State {//内置pool和Rule使用这个虚拟的内置scope来初始化它们的关系位置字段。这个范围内没有任何东西。static Scope kBuiltinScope;static Pool kDefaultPool;static Pool kConsolePool;static Rule kPhonyRule;// 内置的hashmap 保存所有的Nodetypedef ConcurrentHashMap<HashedStrView, Node*> Paths;Paths paths_;// 保存所有的Poolstd::unordered_map<HashedStrView, Pool*> pools_;// 保存所有的edgevector<Edge*> edges_; // 根作用域Scope root_scope_ { ScopePosition {} };vector<Node*> defaults_; // 默认目标private:/// Position 0 is used for built-in decls (e.g. pools).DeclIndex dfs_location_ = 1;
};
Scope
Scope
作用域:变量的作用范围,有rule与build语句的块级,也有文件级别。包含Rule,同时保存了父Scope的位置
struct Scope {Scope(ScopePosition parent) : parent_(parent) {}private:ScopePosition parent_; // 父位置DeclIndex pos_ = 0; // 自己的哈希位置// 变量std::unordered_map<HashedStrView, std::vector<Binding*>> bindings_;// Rulestd::unordered_map<HashedStrView, Rule*> rules_;
};
Rule
Rule
文件的构建规则,存在局部变量
struct Rule {Rule() {}struct {// 该规则在其源文件中的位置。size_t rule_name_diag_pos = 0;} parse_state_;RelativePosition pos_; // 偏移值HashedStr name_; // 规则名std::vector<std::pair<HashedStr, std::string>> bindings_;//保存局部变量
};
Binding & DefaultTarget
Binding
以键值对的形式存在用来变量
DefaultTarget
保存默认的输出的target
struct Binding {RelativePosition pos_; // 偏移位置HashedStr name_; //变量名StringPiece parsed_value_; // 变量值
};struct DefaultTarget {RelativePosition pos_; // 偏移值LexedPath parsed_path_; // StringPiecesize_t diag_pos_ = 0;
};
Node
Node
是最边界的数据结构,ninja语法中的input
,output
,target
,default
的底层保存都是Node
struct Node {Node(const HashedStrView& path, uint64_t initial_slash_bits): path_(path),first_reference_({ kLastDeclIndex, initial_slash_bits }) {}~Node();
private:// 路径值const HashedStr path_;std::atomic<NodeFirstReference> first_reference_;// 作为output所在的Edge位置Edge* in_edge_ = nullptr;// 使用此Node作为输入的所有Edge.列表顺序不确定,每次访问都是对其重新排序struct EdgeList {EdgeList(Edge* edge=nullptr, EdgeList* next=nullptr): edge(edge), next(next) {}Edge* edge = nullptr;EdgeList* next = nullptr;};std::atomic<EdgeList*> out_edges_ { nullptr };std::atomic<EdgeList*> validation_out_edges_ { nullptr };std::vector<Edge*> dep_scan_out_edges_;
};
Edge
Edge
是最核心的数据结构,会将Node
Rule
Binding
等数据结构组合起来
struct Edge {// 固定的属性值 在Rule下进行配置struct DepScanInfo {bool valid = false;bool restat = false;bool generator = false;bool deps = false;bool depfile = false;bool phony_output = false;uint64_t command_hash = 0;};public:struct {StringPiece rule_name; // 保存rule_namesize_t rule_name_diag_pos = 0;size_t final_diag_pos = 0;} parse_state_;const Rule* rule_ = nullptr; // 使用的rulePool* pool_ = nullptr; // 所在的pool// 在一个edge中的input,outputvector<Node*> inputs_;vector<Node*> outputs_;std::vector<std::pair<HashedStr, std::string>> unevaled_bindings_; // 存储局部变量值int explicit_deps_ = 0; // 显式输入int implicit_deps_ = 0; // 隐式输入int order_only_deps_ = 0; // 隐式order-only依赖int explicit_outs_ = 0; // 显示输出int implicit_outs_ = 0; // 隐式输出};
如何区分显隐式,input和output会按照按照 显式 -> 隐式 -> order-only(仅依赖) 的顺序进行push_back()
根据当前的值的位置与显隐式的数量做对比就可以知道
edge->outputs_.reserve(edge->explicit_outs_ + edge->implicit_outs_);
edge->inputs_.reserve(edge->explicit_deps_ + edge->implicit_deps_ +edge->order_only_deps_);
启动过程
入口函数
ninja.cc main() -> real_mian()
1 处理参数
- -f 选择文件
- -C 工作路径
- -t 选择内置工具
NORETURN void real_main(int argc, char** argv) {BuildConfig config;Options options = {};options.input_file = "build.ninja";options.dupe_edges_should_err = true;// 处理参数int exit_code = ReadFlags(&argc, &argv, &options, &config); // return 1 exit...
}
struct Options {// 文件 -fconst char* input_file;// 工作路径 -Cconst char* working_dir;// 工具 -tconst Tool* tool;// 针对一个目标的重复规则是否应该发出警告或打印错误bool dupe_edges_should_err;// 假周期是否应该警告或打印一个错误。bool phony_cycle_should_err;// 在不同的行上有多个目标的删除文件是否应该警告或打印错误。bool depfile_distinct_target_lines_should_err;// 是否保持持久bool persistent;
};
2 读取ninja文件并构建图
static std::vector<ParserItem> ParseManifestChunks(const LoadedFile& file,ThreadPool* thread_pool) {...for (std::vector<ParserItem>& chunk_items :ParallelMap(thread_pool, chunk_views, [&file](StringPiece view) {std::vector<ParserItem> chunk_items;manifest_chunk::ParseChunk(file, view, &chunk_items); // 解析build.ninjareturn chunk_items;})) {std::move(chunk_items.begin(), chunk_items.end(),std::back_inserter(result));}...
}
再执行 ParseFileInclude
class ChunkParser{const LoadedFile& file_;Lexer lexer_;const char* chunk_end_ = nullptr;std::vector<ParserItem>* out_ = nullptr; // 保存include和subninja的文件及ClumpClump* current_clump_ = nullptr; // 读取文件并分析保存文件中的内容
};class Clump{std::vector<Binding*> bindings_; // 保存全局变量std::vector<Rule*> rules_; // rulestd::vector<Pool*> pools_; // poolstd::vector<Edge*> edges_; // Edgestd::vector<DefaultTarget*> default_targets_; // default
};struct ParserItem {enum Kind {kError, kRequiredVersion, kInclude, kClump};Kind kind;union {Error* error;RequiredVersion* required_version;Include* include;Clump* clump;} u;ParserItem(Error* val) : kind(kError) { u.error = val; }ParserItem(RequiredVersion* val) : kind(kRequiredVersion) { u.required_version = val; }ParserItem(Include* val) : kind(kInclude) { u.include = val; }ParserItem(Clump* val) : kind(kClump) { u.clump = val; }
};
ChunkParser::ParseChunk()
此函数为读取文件进行初步分析
的主要位置,按行,循环执行lexer_.ReadToken()
;读取 build.ninja
的内容并根据内容返回枚举属性值,判断属性值并执行对应的函数
bool ChunkParser::ParseChunk() {while (true) {if (lexer_.GetPos() >= chunk_end_) {assert(lexer_.GetPos() == chunk_end_ &&"lexer scanned beyond the end of a manifest chunk");return true;}Lexer::Token token = lexer_.ReadToken();bool success = true;switch (token) {case Lexer::INCLUDE: success = ParseFileInclude(false); break;case Lexer::SUBNINJA: success = ParseFileInclude(true); break;case Lexer::POOL: success = ParsePool(); break;case Lexer::DEFAULT: success = ParseDefault(); break; // 读取默认case Lexer::IDENT: success = ParseBinding(); break; // 读取全局变量并保存case Lexer::RULE: success = ParseRule(); break; // 读取rule , rule保存在clump->rule_中, 在遇到rule内变量时,会保存到rule->bending_ 以键值对的形式顺序保存case Lexer::BUILD: success = ParseEdge(); break; // 获取Edge,一个build就是一个Edgecase Lexer::NEWLINE: break;case Lexer::ERROR: return LexerError(lexer_.DescribeLastError());case Lexer::TNUL: return LexerError("unexpected NUL byte");case Lexer::TEOF:assert(false && "EOF should have been detected before reading a token");break;default:return LexerError(std::string("unexpected ") + Lexer::TokenName(token));}if (!success) return false;}return false; // not reached
}
- ParseFileInclude(false) : 处理
include
,保存文件到ChunkParser::out_
- ParseFileInclude(true) : 处理
subninja
,逻辑同上,区别在于作用域不同 - ParsePool() : 保存
pool
到Clump::pools_
- ParseDefault() : 保存
default
到Clump::default_targets_
- ParseBinding() : 保存
全局变量
到Clump::bindings_
- ParseRule() : 保存
Rule
到Clump::rule_
- ParseEdge() : 保存
Edge
到Clump::redges_
3 构建Edge图
在初步加载分析后,会执行ManifestLoader::FinishLoading(std::vector<Clump*>&,std::string*)
在再次分析得到准确的Edge
和Node
,将其保存到State
,分为5部分
bool ManifestLoader::FinishLoading(const std::vector<Clump*>& clumps,std::string* err) {// 构造输入/输出节点的初始图。// 选择一个可能保持碰撞次数较低的初始大小。// Edge的非隐式输出的数量对于最终的节点的数量是一个足够好的代理。{METRIC_RECORD(".ninja load : edge setup");size_t output_count = 0;// 计算edge的数量for (Clump* clump : clumps)output_count += clump->edge_output_count_;// 重新计算Node的容器大小,默认算Edge的三倍state_->paths_.reserve(state_->paths_.size() + output_count * 3);if (!PropagateError(err, ParallelMap(thread_pool_, clumps,[this](Clump* clump) {std::string err;// 抽出Clump中的Edge,Node,Pool等数据,初步构建Edge图FinishAddingClumpToGraph(clump, &err);return err;}))) {return false;}}// 记录由一条边构建的每个节点的内边。检测到重复的Edge。// 使用 dupbuild=warn(默认直到1.9.0),当两条Edge生成同一Node时,从后面的Edge的输出列表中删除重复的Node。// 如果删除了一条Edge的所有输出,请从graph中删除该Edge。// 简单的说就是会遍历Edge和其中的output的Node,查看是否有重复值如果有就会删除掉{METRIC_RECORD(".ninja load : link edge outputs");for (Clump* clump : clumps) {for (size_t edge_idx = 0; edge_idx < clump->edges_.size(); ) {Edge* edge = clump->edges_[edge_idx];for (size_t i = 0; i < edge->outputs_.size(); ) {Node* output = edge->outputs_[i];if (output->in_edge() == nullptr) {output->set_in_edge(edge);++i;continue;}// 存在两个Edge输出同一节点if (options_.dupe_edge_action_ == kDupeEdgeActionError) {return DecorateError(clump->file_,edge->parse_state_.final_diag_pos,"multiple rules generate " + output->path() +" [-w dupbuild=err]", err);} else {if (!quiet_) {Warning("multiple rules generate %s. ""builds involving this target will not be correct; ""continuing anyway [-w dupbuild=warn]",output->path().c_str());}if (edge->is_implicit_out(i))--edge->implicit_outs_;else--edge->explicit_outs_;edge->outputs_.erase(edge->outputs_.begin() + i);}}if (edge->outputs_.empty()) {clump->edges_.erase(clump->edges_.begin() + edge_idx);continue;}++edge_idx;}}}// 此时所有的重复Edge已经被剔除掉了,现在开始给input添加需要自己的edge{METRIC_RECORD(".ninja load : link edge inputs");ParallelMap(thread_pool_, clumps, [](Clump* clump) {for (Edge* edge : clump->edges_) {for (Node* input : edge->inputs_) {input->AddOutEdge(edge);}for (Node* validation : edge->validations_) {validation->AddValidationOutEdge(edge);}}});}// 添加默认的target{METRIC_RECORD(".ninja load : default targets");for (Clump* clump : clumps) {// 从Clump->default_targets_中获取没有 DefaultTargetfor (DefaultTarget* target : clump->default_targets_) {std::string path;EvaluatePathInScope(&path, target->parsed_path_,target->pos_.scope_pos());uint64_t slash_bits; // Unused because this only does lookup.std::string path_err;if (!CanonicalizePath(&path, &slash_bits, &path_err))return DecorateError(clump->file_, target->diag_pos_, path_err, err);Node* node = state_->LookupNodeAtPos(path, target->pos_.dfs_location());if (node == nullptr) {return DecorateError(clump->file_, target->diag_pos_,"unknown target '" + path + "'", err);}state_->AddDefault(node);}}}// 将所有的Edge添加到全局的Edge vector容器中(*State->edges_),并对其分配id{METRIC_RECORD(".ninja load : build edge table");size_t old_size = state_->edges_.size();size_t new_size = old_size;for (Clump* clump : clumps) {new_size += clump->edges_.size();}state_->edges_.reserve(new_size);for (Clump* clump : clumps) {std::copy(clump->edges_.begin(), clump->edges_.end(),std::back_inserter(state_->edges_));}// Assign edge IDs.ParallelMap(thread_pool_, IntegralRange<size_t>(old_size, new_size),[this](size_t idx) {state_->edges_[idx]->id_ = idx;});}return true;
}
(1) edge setup
构造输入/输出节点的初始图。选择一个可能保持碰撞次数较低的初始大小。Edge的非隐式输出的数量对于最终的节点的数量是一个足够好的代理。
执行FinishAddingClumpToGraph() -> AddEdgeToGraph()
按照顺序,查询Rule
-> 判断Pool
-> 重置容器容量
-> 循环构建Node
构建完成,根据配置进行一些设置,此部分的内容就完成了
(2) link edge outputs
记录由一条边构建的每个节点的内边。检测到重复的Edge。使用 dupbuild=warn(默认直到1.9.0),当两条Edge生成同一Node时,从后面的Edge的输出列表中删除重复的Node。如果删除了一条Edge的所有输出,请从graph中删除该Edge。
简单的说就是,会遍历Edge和其中output的Node,查看是否有重复值如果有就会删除掉
(3) link edge inputs
此时所有的重复Edge已经被剔除掉了,现在开始给input添加需要自己的edge
(4) default targets
添加默认的target
(5) build edge table
将所有的Edge添加到全局的Edge vector容器中(*State->edges_),并对其分配id
4 加载日志文件
会加载两个日志文件命名分别为.ninja_log
和.ninja_deps
.ninja_log
保存ninja运行期间的所有日志
.ninja_deps
保存了ninja的构建图,在此过程将节点添加到构建图中,查找每个节点的最后记录记录输出,并计算开发记录的总数.(使用ninja -t deps
读取)
5 执行编译
在subproc.Start()
中使用posix_spawn()
创建一个新的进程,使用/bin/sh -c "command"
来执行编译指令
总结
两个特点:
- 由其他高级编译系统生成其输入文件
- ninja自带的ninja_syntax.py
- 安卓soong编译系统中的工具
- cmake -Gninja 生成ninja文件
- 更快的编译
- 编译前会构建一张图
- 根据图找到依赖关系链
- 根据依赖关系链,展开编译命令,构建子进程进行编译以提高编译速度
使用拓展
自带工具集
ninja自身集成了 graphviz 等一些对开发有用的工具,可以使用 ninja -t list
查看
ninja subtools:browse # 在浏览器中浏览依赖关系图。(默认会在 8080 端口启动一个基于python的http服务)
clean # 清除构建生成的文件
commands # 罗列重新构建制定目标所需的所有命令
deps # 显示存储在deps日志中的依赖关系
graph # 为指定目标生成 graphviz dot 文件。如 ninja -t graph all |dot -Tpng -o graph.png
inputs # 显示目标的所有(递归)输入
path # 查找两个目标之间的依赖关系路径
paths # 查找两个目标之间的所有依赖项路径
query # 显示一个路径的inputs/outputs
targets # 通过DAG中rule或depth罗列target
compdb # dump JSON兼容的数据库到标准输出
recompact # 重新紧凑化ninja内部数据结构
ninja -t graph ninja | dot -Tpng -o ninja.png
使用ninja提高编译速度
source build/envsetup.sh
lunch xxx
make
从Android O开始,soong已经是google的入口。从soong入口后,会经soong_ui,soong,kati,blueprint几个阶段,把mk,bp转换成ninja文件后,然后执行ninja命令解析ninja文件进行编译。
从图中来看,准备过程十分冗长,每次编译都是需要重新收集相关文件,重新编译成build.ninja,再合并为combined-xxx.ninja文件
如果我们舍弃掉准备的过程那么就可以直接指向ninja以提高速率
添加函数到 envsetup.sh
修改miui/build/envsetup.sh,新增一个quickbuild函数
function quickbuild() {# 备份当前目录local current=$PWDlocal out_dir="$ANDROID_BUILD_TOP/out"local file=$out_dir/combined-$TARGET_PRODUCT.ninjaif [ ! -f $file ]; thenfile=$out_dir/combined-$TARGET_PRODUCT-target-files-package.ninjafiif [ -f $file ]; thenecho "ninja: $file" $*elseecho "ninja: $file not exist"returnficroot && prebuilts/build-tools/linux-x86/bin/ninja -f $file $*cd $current
}
注意 : 此方式只适用于修改c,cpp,java文件等,如果添加文件或修改配置文件,需要重新make生成ninja文件
ninja介绍及使用相关推荐
- Android ninja介绍
ninja简介 早期的Android系统都是采用Android.mk的配置来编译源码,从Android 7.0开始引入ninja.ninja是一个编译框架,会根据相应的ninja格式的配置文件进行编译 ...
- 编译工具 Ninja 介绍
什么是Ninja Ninja是使用C++写的开源项目. http://martine.github.io/ninja/ 在Unix/Linux下通常使用Makefile来控制代码的编译,但是Makef ...
- 史上最全青龙面板2.9/2.10++Ninja从零安装教程+基本命令及仓库全套
Ninja介绍 Ninja是@MoonBegonia大佬开发的一个开源安全的青龙面板扫码工具. 为了方便体验,个人搭建的参考学习 可以登录跑任务:http://jd.51kuaigouwu.com/ ...
- Python系列-Django-Ninja
Python系列-Django-Ninja 适用对象:有一定python和django基础,对此技术感兴趣,或者想快速尝试.实现效果的. 原则: 实用为主,效果为主 不重复造轮子,但应该知道其工作原理 ...
- linux添加patch,如何创建和使用Linux中的patch
今天TTekkaman Ninja 介绍了 一定要学会 打补丁的 偷懒了 转载了很多 diff 选项: -u 以统一格式创建补丁文件,这种格式比缺省格式更紧凑些. -N 确保补丁文件将正确地 ...
- Ninja工具介绍及基本语法
1. Ninja 简介 ninja是一个专注于速度的小型构件系统.是chromium的核心构建系统.小而美的构件系统,只需拷贝一个可执行程序ninja就可以执行,不需要依赖任何库. 1.1 获取源码 ...
- ninja编译方法介绍
痛点现状 首先要了解安卓编译原理: 安卓编译在原先安卓6.0纯MakeFile编译的传统流程前,8.0版本新增了四个步骤: 1.Soong的自举(bootstrap).这个步骤会编译S ...
- 启明云端分享|ESP32-S3开发环境搭建,这里我们会介绍两个比较常用的开发环境搭建:WINDOWS\LINUX
ESP32-S3开发环境搭建,这里我们会介绍两个比较常用的开发环境搭建:WINDOWS\LINUX 详细安装步骤概览 注:目前master分支,支持S3. 官方开发文档:包含多种平台 https:// ...
- WebRTC编译系统之GYP,gn和ninja
GN(Generate Ninja)来生成构建脚本,使用 ninja 来构建. gn 的介绍在这里:https://www.chromium.org/developers/gn-build-confi ...
最新文章
- python生成excel表格-Python生成excel表格并设置样式
- Linux驱动之平台设备
- Community Server页面布局
- 英伟达收购Mellanox接近尾声,将成英伟达史上最大收购案
- python语言采用编译执行方式_Python程序的执行过程 解释型语言和编译型语言
- Java基础day21
- C# 方法中的this参数
- Little Sub and Sequence
- Bug错误openssl_encrypt()
- OpenCV图像处理基础操作汇总
- 您基于JEE的Web项目的结构是什么?
- Python-02-基础知识
- SyntaxError:identifier starts immediately after numeric literal
- 从0到1搭建移动App功能自动化测试平台(2):操作iOS应用的控件
- The process names are sorted in descending
- 美国DHS向国会提交政府《移动设备安全研究》报告
- iOS支付知识及调试技巧:【支付流程 预授权 银行卡验证反洗钱敏感信息的脱敏规范】2、安全设计Checklist(短信验证码、图形验证码、密码管理、身份验证、会话安全、敏感信息、接口安全)
- focal loss详解
- oracle汉字转拼音(获得全拼/拼音首字母/拼音截取等)
- 微信 Windows 版本干了件大事!
热门文章
- KPI自动化异常检测系统——Opprentice
- 大学计算机三级网络技术,考前数天如何突破性通过计算机等级考试之三级网络技术篇...
- 入域client server 利用普通域账号实现远程登录
- Power Pivot关系函数
- php 班级排名,GPA、班级排名、AP/IB,中国学生得多优秀才有申名校的底气?
- SpringBoot Security的oauth2四种授权模式使用
- WJMZBMR在成都赛区开幕式上的讲话
- usb68013Bulkloop固件程序
- #10016. 「一本通 1.2 练习 3」灯泡
- FAILED: SemanticException [Error 10016]: Line 66:45 Argument type mismatch ‘ca_coupon_id‘: “map“ or