微信团队分享:极致优化,iOS版微信编译速度3倍提升的实践总结
1、引言
岁月真是个养猪场,这几年,人胖了,微信代码也翻了。
记得 14 年转岗来微信时,用自己笔记本编译微信工程才十来分钟。如今用公司配的 17 年款 27-inch iMac 编译要接近半小时;偶然间更新完代码,又莫名其妙需要全新编译。在这么低的编译效率下,开发心情受到严重影响。
于是年初我向上头请示,优化微信编译效率,上头也同意了。

2、现有方案
在动手之前,先搜索目前已有方案,大概情况如下。
2.1 优化工程配置
1)将 Debug Information Format 改为 DWARF:
Debug 时是不需要生成符号表,可以检查一下子工程(尤其开源库)有没有设置正确。
2)将 Build Active Architecture Only 改为 Yes:
Debug 时是不需要生成全架构,可以检查一下子工程(尤其开源库)有没有设置正确。
3)优化头文件搜索路径:
避免工程 Header Search Paths 设置了路径递归引用:

Xcode 编译源文件时,会根据 Header Search Paths 自动添加 -I 参数,如果递归引用的路径下子目录越多,-I 参数也越多,编译器预处理头文件效率就越低,所以不能简单的设置路径递归引用。同样 Framework Search Paths 也类似处理。
2.2 使用 CocoaPods 管理第三方库
这是业界常用的做法,利用 cocoapods 插件 cocoapods-packager 将任意的 pod 打包成 Static Library,省去重复编译的时间;但缺点是不方便调试源码,如果库代码反复修改,需要重新生成二进制并上传到内部服务器,等等。
2.3 CCache
CCache 是一个能够把编译的中间产物缓存起来的工具,不需要过多修改项目配置,也不需要修改开发工具链。Xcode 9 有个很偶然的 bug,在源码没有任何修改的情况下经常触发全新编译,用 CCache 很好的解决这一问题。但随着 Xcode 10 修复全量编译问题,这一方案逐步弃用了。
2.4 distcc
distcc 是一个分布式编译工具,它原理是把本地多个编译任务分发到网络中多个机器,其他机器编译完成后,再把产物返回给本机上执行链接,最终得到编译结果。
2.5 硬件解决
如把 Derived Data 目录放到由内存创建的虚拟磁盘,或者购买最新款的 iMac Pro...
3、实践过程
3.1 优化编译选项
1)优化头文件搜索路径:
把一些递归引用路径去了后,整体编译速度快了 20s。
2)关闭 Enable Index-While-Building Functionality:
这选项无意中找到的(Xcode 9 的新特性?),默认打开,作用是 Xcode 编译时会顺带建立代码索引,但影响编译速度。关闭后整体编译速度快 80s(Xcode 会换回以前的方式,在空闲时间建立代码索引)。
3.2 优化 kinda
kinda 是今年引入支付跨平台框架(C++),但编译速度奇慢,一个源文件编译都要 30s。另外生成的二进制大小在 App 占比较高,感觉有不少冗余代码,理论上减少冗余代码也能加快编译速度。
经过分析 LinkMap 文件和使用 Xcode Preprocess 某些源文件,发现有以下问题:
1)proto 文件生成的代码较多;
2)某个基类/宏使用了大量模版。
对于问题一:可以设置 proto 文件选项为 optimize_for=CODE_SIZE 来让 protobuf 编译器生成精简版代码。但我是用自己的工具生成(具体原理可看《iOS版微信安装包“减肥”实战记录》),代码更少。
对于问题二:由于模版是编译期间的多态(增加代码膨胀和编译时间),所以可以把模版基类改成虚基类这种运行时的多态;另外推荐使用 hyper_function 取代 std::function,使得基类用通用函数指针,就能存储任意 lambda 回调函数,从而避免基类模板化。
例如:
template<typenameRequest, typenameResponse>
classBaseCgi {
public:
BaseCgi(Request request, std::function<void(Response &)> &callback) {
_request = request;
_callback = callback;
}
voidonRequest(std::vector<uint8_t> &outData) {
_request.toData(outData);
}
voidonResponse(std::vector<uint8_t> &inData) {
Response response;
response.fromData(inData);
callback(response);
}
public:
Request _request;
std::function<void(Response &)> _callback;
};
classCgiA : publicBaseCgi<RequestA, ResponseA> {
public:
CgiA(RequestA &request, std::function<void(ResponseA &)> &callback) :
BaseCgi(request, callback) {}
};
可改成:
class BaseRequest {
public:
virtual void toData(std::vector<uint8_t> &outData) = 0;
};
classBaseResponse {
public:
virtualvoidfromData(std::vector<uint8_t> &outData) = 0;
};
classBaseCgi {
public:
template<typenameRequest, typenameResponse>
BaseCgi(Request &request, hyper_function<void(Response &)> callback) {
_request = newRequest(request);
_response = newResponse;
_callback = callback;
}
voidonRequest(std::vector<uint8_t> &outData) {
_request->toData(outData);
}
voidonResponse(std::vector<uint8_t> &inData) {
_response->fromData(inData);
_callback(*_response);
}
public:
BaseRequest *_request;
BaseResponse *_response;
hyper_function<void(BaseResponse &)> _callback;
};
classRequestA : publicBaseRequest { ... };
classResponseA : publicBaseResponse { ... };
classCgiA : publicBaseCgi {
public:
CgiA(RequestA &request, hyper_function<void(ResponseA &)> &callback) :
BaseCgi(request, callback) {}
};
BaseCgi 由模版基类变成只有构造函数是模板的基类,onRequest 和 onResponse 逻辑代码并不因为基类模版实例化而被“复制黏贴”。
经过上述优化:整体编译速度快了 70s,而 kinda 二进制也减少了 60%,效果特别明显。
4.3 使用 PCH 预编译头文件
PCH(Precompile Prefix Header File)文件,也就是预编译头文件,其文件里的内容能被项目中的其他所有源文件访问。通常放一些通用的宏和头文件,方便编写代码,提高效率。
另外 PCH 文件预编译完成后,后面用到 PCH 文件的源文件编译速度也会加快。缺点是 PCH 文件和 PCH 引用到的头文件内容一旦发生变化,引用到 PCH 的所有源文件都要重新编译。所以使用时要谨慎。
在 Xcode 里设置 Prefix Header 和 Precompile Prefix Header 即可使用 PCH 文件并对它进行预编译:

微信使用 PCH 预编译后:编译速度提升非常可观,快了接近 280s。
4、终极优化
通过上述优化,微信工程的编译时间由原来的 1,626.4s 下降到 1,182.8s,快了将近 450s,但仍然需要 20 分钟,令人不满意。
如果继续优化,得从编译器下手。正如我们平常做的客户端性能优化,在优化之前,先分析原理,输出每个地方的耗时,针对耗时做相对应的优化。
4.1 编译原理
编译器,是把一种语言(通常是高级语言)转换为另一种语言(通常是低级语言)的程序。
大多数编译器由三部分组成:

各部分的作用如下:
前端(Frontend):负责解析源码,检查错误,生成抽象语法树(AST),并把 AST 转化成类汇编中间代码;
优化器(Optimizer):对中间代码进行架构无关的优化,提高运行效率,减少代码体积,例如删除 if (0) 无效分支;
后端(Backend):把中间代码转换成目标平台的机器码。
LLVM 实现了更通用的编译框架,它提供了一系列模块化的编译器组件和工具链。首先它定义了一种 LLVM IR(Intermediate Representation,中间表达码)。Frontend 把原始语言转换成 LLVM IR;LLVM Optimizer 优化 LLVM IR;Backend 把 LLVM IR 转换为目标平台的机器语言。这样一来,不管是新的语言,还是新的平台,只要实现对应的 Frontend 和 Backend,新的编译器就出来了。

在 Xcode,C/C++/ObjC 的编译器是 Clang(前端)+LLVM(后端),简称 Clang。
Clang 的编译过程有这几个阶段:
➜ clang -ccc-print-phases main.m
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
1)预处理:
这阶段的工作主要是头文件导入,宏展开/替换,预编译指令处理,以及注释的去除。
2)编译:
这阶段做的事情比较多,主要有:
a. 词法分析(Lexical Analysis):将代码转换成一系列 token,如大中小括号 paren'()' square'[]' brace'{}'、标识符 identifier、字符串 string_literal、数字常量 numeric_constant 等等;
b. 语法分析(Semantic Analysis):将 token 流组成抽象语法树 AST;
c. 静态分析(Static Analysis):检查代码错误,例如参数类型是否错误,调用对象方法是否有实现;
d. 中间代码生成(Code Generation):将语法树自顶向下遍历逐步翻译成 LLVM IR。
3)生成汇编代码:
LLVM 将 LLVM IR 生成当前平台的汇编代码,期间 LLVM 根据编译设置的优化级别 Optimization Level 做对应的优化(Optimize),例如 Debug 的 -O0 不需要优化,而 Release 的 -Os 是尽可能优化代码效率并减少体积。
4)生成目标文件:
汇编器(Assembler)将汇编代码转换为机器代码,它会创建一个目标对象文件,以 .o 结尾。
5)链接:
链接器(Linker)把若干个目标文件链接在一起,生成可执行文件。
4.2 分析耗时
Clang/LLVM 编译器是开源的,我们可以从官网下载其源码,根据上述编译过程,在每个编译阶段埋点输出耗时,生成定制化的编译器。在自己准备动手的前一周,国外大神 Aras Pranckevičius 已经在 LLVM 项目提交了 rL357340 修改:clang 增加 -ftime-trace 选项,编译时生成 Chrome(chrome://tracing) JSON 格式的耗时报告,列出所有阶段的耗时。
效果如下:

说明如下:
1)整体编译(ExecuteCompiler)耗时 8,423.8ms
2)其中前端(Frontend)耗时 5,307.9ms,后端(Backend)耗时 3,009.6ms
3)而前端编译里头文件 SourceA 耗时 xx ms,B 耗时 xx ms,...
4)头文件处理里 Parse ClassA 耗时 xx ms,B 耗时 xx ms,...
5)等等
这就是我想要的耗时报告!
接下来修改工程 CC={YOUR PATH}/clang,让 Xcode 编译时使用自己的编译器;同时编译选项 OTHER_CFLAGS 后面增加 -ftime-trace,每个源文件编译后输出耗时报告。
最终把所有报告汇聚起来,形成整体的编译耗时:

由整体耗时可以看出:
1)编译器前端处理(Frontend)耗时 7,659.2s,占整体 87%;
2)而前端处理下头文件处理(Source)耗时 7,146.2s,占整体 71.9%!
猜测:头文件嵌套严重,每个源文件都要引入几十个甚至几百个头文件,每个头文件源码要做预处理、词法分析、语法分析等等。实际上源文件不需要使用某些头文件里的定义(如 class、function),所以编译时间才那么长。
于是又写了个工具,统计所有头文件被引用次数、总处理时间、头文件分组(指一个耗时顶部的头文件所引用到的所有子头文件的集合)。
列出一份表格(截取 Top10):

如上表所示:
Header1 处理时间 1187.7s,被引用 2,304 次;
Header2 处理时间 1,124.9s,被引用 3,831 次;
后面 Header3~10 都是被 Header1 引用。
所以可以尝试优化 TopN 头文件里的头文件引用,尽量不包含其他头文件。
4.3 解决耗时
通常我们写代码时,如果用到某个类,就直接 include 该类声明所在头文件,但在头文件,我们可以用前置声明解决。
因此优化头文件思路很简单:就是能用前置声明,就用前置声明替代 include。
实际上改动量非常大:我跟组内另外的同事 vakeee 分工优化 Header1 和 Header2,花了整整 5 个工作日,才改完。效果还是有,整体编译时间减少 80s。
但需要优化的头文件还有几十个,我们不可能继续做这种体力活。因此我们可以做这样的工具,通过 AST 找到代码里出现的标识符(包括类型、函数、宏),以及标识符定义所在文件,然后分析是否需要 include 它定义所在文件。
先看看代码如何转换 AST,如以下代码:
// HeaderA.h
struct StructA {
intval;
};
// HeaderB.h
structStructB {
intval;
};
// main.c
#include "HeaderA.h"
#include "HeaderB.h"
inttestAndReturn(structStructA *a, structStructB *b) {
returna->val;
}
控制台输入:
➜ TestContainer clang -Xclang -ast-dump -fsyntax-only main.c
TranslationUnitDecl 0x7f8f36834208 <<invalid sloc>> <invalid sloc>
|-RecordDecl 0x7faa62831d78 <./HeaderA.h:12:1, line:14:1> line:12:8 struct StructA definition
| `-FieldDecl 0x7faa6383da38 <line:13:2, col:6> col:6 referenced val 'int'
|-RecordDecl 0x7faa6383da80 <./HeaderB.h:12:1, line:14:1> line:12:8 struct StructB definition
| `-FieldDecl 0x7faa6383db38 <line:13:2, col:6> col:6 val 'int'
`-FunctionDecl 0x7faa6383de50 <main.c:35:1, line:37:1> line:35:5 testAndReturn 'int (struct StructA *, struct StructB *)'
|-ParmVarDecl 0x7faa6383dc30 <col:19, col:35> col:35 used a 'struct StructA *'
|-ParmVarDecl 0x7faa6383dd40 <col:38, col:54> col:54 b 'struct StructB *'
`-CompoundStmt 0x7faa6383dfc8 <col:57, line:37:1>
`-ReturnStmt 0x7faa6383dfb8 <line:36:2, col:12>
`-ImplicitCastExpr 0x7faa6383dfa0 <col:9, col:12> 'int'<LValueToRValue>
`-MemberExpr 0x7faa6383df70 <col:9, col:12> 'int'lvalue ->val 0x7faa6383da38
`-ImplicitCastExpr 0x7faa6383df58 <col:9> 'struct StructA *'<LValueToRValue>
`-DeclRefExpr 0x7faa6383df38 <col:9> 'struct StructA *'lvalue ParmVar 0x7faa6383dc30 'a''struct StructA *'
从上可以看出:每一行包括 AST Node 的类型、所在位置(文件名,行号,列号)和结点描述信息。头文件定义的类也包含进 AST 中。AST Node 常见类型有 Decl(如 RecordDecl 结构体定义,FunctionDecl 函数定义)、Stmt(如 CompoundStmt 函数体括号内实现)。

Clang AST 有三个重要的基类:ASTFrontendAction、ASTConsumer 以及 RecursiveASTVisitor。
ClangTool 类读入命令行配置项后初始化 CompilerInstance;CompilerInstance 成员函数 ExcutionAction 会调用 ASTFrontendAction 3 个成员函数 BeginSourceFile(准备遍历 AST)、Execute(解析 AST)、EndSourceFileAction(结束遍历)。
ASTFrontendAction 有个重要的纯虚函数 CreateASTConsumer(会被自己 BeginSourceFile 调用),用于返回读取 AST 的 ASTConsumer 对象。
代码如下:
class MyFrontendAction : public clang::ASTFrontendAction {
public:
virtualstd::unique_ptr<clang::ASTConsumer> CreateASTConsumer(clang::CompilerInstance &CI, llvm::StringRef file) override {
TheRewriter.setSourceMgr(CI.getASTContext().getSourceManager(), CI.getASTContext().getLangOpts());
returnllvm::make_unique<MyASTConsumer>(&CI);
}
};
intmain(intargc, constchar**argv) {
clang::tooling::CommonOptionsParser op(argc, argv, OptsCategory);
clang::tooling::ClangTool Tool(op.getCompilations(), op.getSourcePathList());
intresult = Tool.run(clang::tooling::newFrontendActionFactory<MyFrontendAction>().get());
returnresult;
}
ASTConsumer 有若干个可以 override 的方法,用来接收 AST 解析过程中的回调,其中之一是工具用到的 HandleTranslationUnit 方法。当编译单元 TranslationUnit 的 AST 完整解析后,HandleTranslationUnit 会被回调。我们在 HandleTranslationUnit 使用 RecursiveASTVisitor 对象以深度优先的方式遍历 AST 所有结点。
代码如下:
class MyASTVisitor
: public clang::RecursiveASTVisitor<MyASTVisitor> {
public:
explicitMyASTVisitor(clang::ASTContext *Ctx) {}
boolVisitFunctionDecl(clang::FunctionDecl* decl) {
// FunctionDecl 下的所有参数声明允许前置声明取代 include
// 如上面 Demo 代码里 StructA、StructB
returntrue;
}
boolVisitMemberExpr(clang::MemberExpr* expr) {
// 被引用的成员所在的类,需要 include 它定义所在文件
// 如 StructA
returntrue;
}
boolVisitXXX(XXX) {
returntrue;
}
// 同一个类型,可能出现若干次判定结果
// 如果其中一个判断的结果需要 include,则 include
// 否则使用前置声明代替 include
// 例如 StructA 只能 include,StructB 可以前置声明
};
class MyASTConsumer : public clang::ASTConsumer {
private:
MyASTVisitor Visitor;
public:
explicitMyASTConsumer(clang::CompilerInstance *aCI)
: Visitor(&(aCI->getASTContext())) {}
void HandleTranslationUnit(clang::ASTContext &context) override {
clang::TranslationUnitDecl *decl = context.getTranslationUnitDecl();
Visitor.TraverseTranslationUnitDecl(decl);
}
};
工具框架大致如上所示。
不过早在 2011 年 Google 内部做了个基于 Clang libTooling 的工具 include-what-you-use,用来整理 C/C++ 头文件。
这个工具的使用效果如下:
➜ include-what-you-use main.c
HeaderA.h has correct #includes/fwd-decls)
HeaderB.h has correct #includes/fwd-decls)
main.c should add these lines:
struct StructB;
main.c should remove these lines:
- #include "HeaderB.h" // lines 2-2
The full include-list formain.c:
#include "HeaderA.h" // for StructA
struct StructB;
我们在 IWYU 基础上,增加了 ObjC 语言的支持,并增强它的逻辑,让结果更好看(通常 IWYU 处理完后,会引入很多头文件和前置声明,我们做剪枝处理,进一步去掉多余的头文件和前置声明,篇幅限制就不多做解释了)。
微信源码通过工具优化头文件引入后,整体编译时间降到了 710s。另外头文件依赖的减少,也能降低因修改头文件引起大规模源码重编的可能性。
我们再用编译耗时分析工具分析当前瓶颈:

WCDB 头文件处理时间太长了,业务代码(如 Model 类)没有很好的隔离 WCDB 代码,把 WINQ 暴露出去,外面被动 include WCDB 头文件。解决方法有很多,例如 WCDB 相关放 category 头文件(XXModel+WCDB.h)里引入,或者跟其他库一样,把 放 PCH。
最终编译时间优化到 540s 以下,是原来的三分之一,编译效率得到巨大的提升。
5、优化总结
总结微信的编译优化方案:

即:
A)优化头文件搜索路径;
B)关闭 Enable Index-While-Building Functionality;
C)优化 PB/模版,减少冗余代码;
D)使用 PCH 预编译;
E)使用工具优化头文件引入;尽量避免头文件里包含 C++ 标准库。
6、未来展望
期待公司的蓝盾分布式编译 for ObjC;另外可以把业务代码模块化,项目文件按模块加载,目前 kinda/小程序/mars 在很好的实践中。
7、参考文献
[3] Clang之语法抽象语法树AST
微信团队分享:极致优化,iOS版微信编译速度3倍提升的实践总结的更多相关文章
- 微信团队分享:iOS版微信的高性能通用key-value组件技术实践
本文来自微信开发团队guoling的技术分享. 1.前言 本文要分享的是iOS版微信内部正在推广和使用的一个高性能通用key-value 组件的技术实践过程,该组件在微信内部被命名为MMKV(以下简称 ...
- 微信团队分享:iOS版微信是如何防止特殊字符导致的炸群、APP崩溃的?
本文来自微信开发团队yanyang的技术分享. 1.引言 相信大家都遇到过一段特殊文本可以让iOS设备所有app闪退的经历.前段时间大年初一,又出现某个印度语字符引起iOS11系统奔溃,所幸iOS版微 ...
- 微信团队原创分享:iOS版微信的内存监控系统技术实践
本文来自微信开发团队yangyang的技术分享. 一.前言 FOOM(Foreground Out Of Memory),是指App在前台因消耗内存过多引起系统强杀.对用户而言,表现跟crash一样. ...
- 微信团队分享:Kotlin渐被认可,Android版微信的技术尝鲜之旅
本文由微信开发团队工程是由“oneliang”原创发表于WeMobileDev公众号,内容稍有改动. 1.引言 Kotlin 是一个用于现代多平台应用的静态编程语言,由 JetBrains 开发( ...
- 腾讯微信被怼,iOS版微信不能打赏了
2017年4月19日,估计很多有着大量粉丝的微信自媒体作者会感到很不爽,因为他们的苹果粉丝再也无法很爽快地.肆意.任性地打赏他们了,按目前iphone手机的占有率,估计打赏率会掉一半以上. 据微信派微 ...
- iOS版微信6.5.21发布 适配iPhone X
昨日,iOS版微信迎来v6.5.21正式版发布,本次升级主要适配iPhone X,在聊天中查找聊天内容时,可以查找交易消息.可以给聊天中的消息设置日期提醒.上一个正式版v6.5.16发布于9月13日, ...
- 解决alert在ios版微信中显示url的问题(重写alert)
为了解决alert在ios版微信中显示url的问题 window.alert = function(name){ var iframe = document.createElement("I ...
- iOS版微信朋友圈数据库的简要分析
本文版权归cxun所有,如有转载请注明出处与本文链接,谢谢!原文地址:http://www.cnblogs.com/cxun/p/4550523.html 之前写了一些关于微信聊天记录的博文之后,不少 ...
- iOS版微信开发小结(微信支付,APP跳转微信公众号)
最近公司心血来潮,一心要搞微信.废话不多说,直接上干货. 开发前准备: 1.在微信开发者平台获取开发者认证:(一年300元人民币) PS:具体流程按照微信流程指示操作即可,在这就不废话了. 2.下载微 ...
随机推荐
- Java :一文掌握 Lambda 表达式
本文将介绍 Java 8 新增的 Lambda 表达式,包括 Lambda 表达式的常见用法以及方法引用的用法,并对 Lambda 表达式的原理进行分析,最后对 Lambda 表达式的优缺点进行一个总 ...
- IE6下CSS常见兼容性问题及解决方案
1. 在IE6元素浮动,如果宽度需要内容撑开,就给里面的块元素加浮动. 2. IE6下最小高度问题:在IE6下元素高度小于19px的时候,会被当作19px处理.解决方案:给元素加 overflow:h ...
- 关于手机微信端ios的input不能选中问题解决方案
最近在做一个微信端的商城,以前做web端的比较多,手机端做的相对来说要少点,老板说让我用俗称”靠谱的移动前端框架”—-AUI来搭建项目. 当时觉得用不用框架无所谓啦.结果后来写到一半把项目发布到手机上 ...
- 20190926-2 选题 Scrum立会报告+燃尽图05
此作业要求参见:https://edu.cnblogs.com/campus/nenu/2019fall/homework/8678 一.小组情况组长:迟俊文组员:宋晓丽 梁梦瑶 韩昊 刘信鹏队名:扛 ...
- 【python测试开发栈】python内存管理机制(一)—引用计数
什么是内存 在开始进入正题之前,我们先来回忆下,计算机基础原理的知识,为什么需要内存.我们都知道计算机的CPU相当于人类的大脑,其运算速度非常的快,而我们平时写的数据,比如:文档.代码等都是存储在磁盘 ...
- Magicodes.IE之导入学生数据教程
基础教程之导入学生数据 说明 本教程主要说明如果使用Magicodes.IE.Excel完成学生数据的Excel导入. 要点 本教程使用Magicodes.IE.Excel来完成Excel数据导入 需 ...
- 2019-10-31:渗透测试,基础学习,mongodb注入学习
mongodb命令删除文档,db.集合名.remove({键:值})更新文档,db.集合名.update({被更新的对象},{$set:{要更新的内容}}),默认只更新检测到的第一条文档更新全部匹配到 ...
- Spring与Shiro整合 登陆操作
Spring与Shiro整合 登陆操作 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 编写登陆Controller方法 讲解: 首先,如果你登陆失败的时候,它会把你的异常信息丢到 ...
- 最全 HTTP 安全响应头设置指南
销售“安全记分卡”的公司正在崛起,并已开始成为企业销售的一个因素.这些公司组合使用 HTTP 安全报头和 IP 信誉来进行评级.不过,在很大程度上,公司的得分取决于对外开放网站上设置的安全响应报头.本 ...
- ctf线下赛中检测外来IP的shell脚本
该脚本可用于ctf线下赛中,用来检测攻击IP的接入,及时做出响应. #!/bin/bash #写自己队的ip ipA="172.22.60.230" ipB="172.2 ...