【iOS应⽤瘦⾝】使⽤Clang插件扫描⽆⽤代码(Part1)
前⾔
最近组⾥的项⽬遇到了⼀个瓶颈问题:代码段超标,简单的说,就是编译后输出的可执⾏⽂件太⼤了,来看看 中的相关规定:
For iOS and tvOS apps, check that your app size fits within the App Store requirements.
Your app’s total uncompressed size must be less than 4GB. Each Mach-O executable file (for example,
app_name.app/app_name) must not exceed these limits:
For apps whose MinimumOSVersion is less than 7.0: maximum of 80 MB for the total of all TEXT sections in the binary.
For apps whose MinimumOSVersion is 7.x through 8.x: maximum of 60 MB per slice for the TEXT section of each
architecture slice in the binary.
For apps whose MinimumOSVersion is 9.0 or greater: maximum of 500 MB for the total of all __TEXT sections in the
binary.
可以看到,iOS 9+ ⽀持 500MB 的代码段体积,⽽ iOS 8.x 只⽀持 60MB。⾯对不断增加的业务代码,我们需要⼀个⼿段,来及时删除
已经废弃的代码,以减⼩代码段体积。
在尝试分析 LinkMap ⽂件⽆果之后,我到了另外⼀个路线,那就是分析 Clang AST,在静态分析时从语法树中,到未被显⽰调⽤到
的⽅法。尽管由于 oc 的动态特性,即便静态阶段其未被显⽰调⽤,它依然可能在动态期间被调⽤,但不论如何,我们都可以通过分析 AST
来得到未被静态调⽤的⽅法,对它们进⾏校对、确认。
C lang & L L VM
有关 Clang 和 LLVM 的知识,远不是三⾔两语能讲完的,我个⼈对这块也不是⼗分熟悉,感兴趣的推荐 ,油管上也有⼀个简明扼要的 可
以⽤于⼊门。当然,遇事 Google ⼀下总能得到许多有⽤的结果。
简单的说,Clang 是 LLVM 编译器前端,将 C、C++、OC 等⾼级语⾔进⾏编译优化,输出 IR 交给 LLVM 编译器后端,再进⼀步翻译成
对应平台的底层语⾔。
G e t Your Hand s D ir ty
编译你的 Clang
截⾄本⽂执笔时,XCode ⾃带的 Clang 是不⽀持加载插件的,因此,想要在实际的项⽬中使⽤ Clang 插件,需要替换为⾃⼰编译的
Clang。
跟着 的步骤,按指定路径 checkout 好各个分⽀后,就可以编译 LLVM 了。需要注意的是,LLVM 不⽀持“原地编译”,需要另开⼀个
⽂件夹作为 build 输出⽂件路径。编译 LLVM 的⽅式有多种,⽽本⽂使⽤的是 CMake,使⽤的指令是
cmake -G Xcode -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES:STRING=x86_64 -DLLVM_TARGETS_TO_BUILD=host -DLLVM_INCLU
等待编译完成后,在输出的⽬录打开 deproj ,选择 ALL_BUILD scheme 进⾏编译,此处会有⼀个 compiler_rt 相关的 error
,尽管我全量 co 了所有的 LLVM 仓库,这块依然编译失败,尚不清楚原因,但这不影响后续插件的开发,故不做理会。
接下来,你可以跟着 ,编写属于⾃⼰的 Clang 插件。我的建议是,动⼿让 Clang 插件跑起来就可以了,第 7 节之后的内容,快速阅读即
可。(上⽂中的⽰例代码有⼀些问题,需要把 MobCodeConsumer 改成 MyPluginConsumer)。
抽象语法树(AST)
现在,你已经成功运⾏了你的第⼀个 Clang 插件,接下来让我们弄明⽩,如何通过 Clang AST,来对现有的代码进⾏分析。回想⼀下⼤学
时期所学到的编译原理,亦或是直接在⾕歌上搜索⼀下,对 AST 的解释⼤概是这么⼀张图 :
语法树是编译器对我们所书写的代码的“理解”,如上图中的 x = a + b; 语句,编译器会先将 operator = 作为节点,将语句拆分为左节点和右节点,随后继续分析其⼦节点,直到叶⼦节点为⽌。对于⼀个基本的运算表达式,我想我们都能很轻松的写出它的 AST,但我们在⽇常业务开发时所写的代码,可不都是简单⽽基础的表达式⽽已,诸如
- (void)viewDidLoad{
[self doSomething];
}
这样的代码,其 AST 是什么样的呢?好消息是 Clang 提供了对应的命令,让我们能够输出 Clang 对特定⽂件编译所输出的 AST,先创建⼀个简单的 CommandLine ⽰例⼯程,在 main 函数之后如下代码:
@interface HelloAST : NSObject
@end
@implementation HelloAST
- (void)hello{
[self print:@"hello!"];
}
- (void)print:(NSString *)msg{
NSLog(@"%@",msg);
}
@end
随后,在 Terminal 中进⼊ main.m 所在⽂件夹,执⾏如下指令:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
让我们把⽬光定位到 import 语句之后的位置:
我们可以看到⼀个清晰的树状结构,我们可以看到⾃⼰的类定义、⽅法定义、⽅法调⽤在 AST 中所对应的节点。
其中第⼀个框为类定义,可以看到该节点名称为 ,该类型节点为 objc 类定义(声明)。
第⼆个框名称为 ,说明该节点定义了⼀个 objc ⽅法(包含类、实例⽅法,包含普通⽅法和协议⽅法)。
第三个框名称为 ,说明该节点是⼀个标准的 objc 消息发送表达式([obj foo])。
这些名称对应的都是 Clang 中定义的类,其中所包含的信息为我们的分析提供了可能。Clang 提供的各种类信息,可以在 进⾏进⼀步查阅。
同时,我们也看到在函数定义的时候,ImplicitParamDecl 节点声明了隐式参数 self 和 _cmd,这正是函数体内 self 关键字的来源。
再把⽬光放到整个树的最顶部,我们可以看到根节点是 TranslationUnitDecl 的声明,由于 Clang 的语法树分析是基于单个⽂件的,所以该节点将会是我们所有分析的根节点。
初步分析
在⼀个 oc 的程序中,⼏乎所有代码都可以被划分为两类:(声明),(语句),上述各个 ObjCXXXDecl 类都是 Decl 的⼦
类,ObjCXXXExpr 也是 Stmt 的⼦类,根据 中声明的⽅法,我们可以看到对应的⼊⼝⽅法:bool VisitDecl (Decl *D) 以及 bool VisitStmt (Stmt *S),要知道如何这两个⽅法,我们还得先看看它们的实现,就拿 Decl 为例,在 中,我们可以看到如下代码:
//code
#define DEF_TRAVERSE_DECL(DECL, CODE) \
template <typename Derived> \
bool RecursiveASTVisitor<Derived>::Traverse##DECL(DECL *D) { \
bool ShouldVisitChildren = true; \
bool ReturnValue = true; \xcode怎么打开
if (!getDerived().shouldTraversePostOrder()) \
TRY_TO(WalkUpFrom##DECL(D)); \
{ CODE; } \
if (ReturnValue && ShouldVisitChildren) \
TRY_TO(TraverseDeclContextHelper(dyn_cast<DeclContext>(D))); \
if (ReturnValue && getDerived().shouldTraversePostOrder()) \
TRY_TO(WalkUpFrom##DECL(D)); \
return ReturnValue; \
}
//code
bool WalkUpFromDecl(Decl *D) { return getDerived().VisitDecl(D); }
bool VisitDecl(Decl *D) { return true; }
#define DECL(CLASS, BASE) \
bool WalkUpFrom##CLASS##Decl(CLASS##Decl *D) { \
TRY_TO(WalkUpFrom##BASE(D)); \
TRY_TO(Visit##CLASS##Decl(D)); \
return true; \
} \
bool Visit##CLASS##Decl(CLASS##Decl *D) { return true; }
上⾯的⼏个宏,定义了以具体类名为⽅法名的各种 Visit ⽅法,⽽上下滑动,可以看到许多这样的定义:
DEF_TRAVERSE_DECL(ObjCInterfaceDecl, {
...
})
DEF_TRAVERSE_DECL(ObjCProtocolDecl, {// FIXME: implement
})
DEF_TRAVERSE_DECL(ObjCMethodDecl, {
...
})
可以看出,我们如果想对某个特定的 XXXDecl 类进⾏分析,只需要实现 VisitXXXDecl(XXXDecl *D) 即可,⽽ VisitStmt 也可以使⽤类似⽅法,得到 Clang 回调。
现在让我们⼩试⽜⼑,在所有类定义和⽅法调⽤的地⽅打出 Warning:
//statement
bool VisitObjCMessageExpr(ObjCMessageExpr *expr){
DiagnosticsEngine &D = Diagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Meet Msg Expr : %0");
D.Report(expr->getLocStart(), diagID) << expr->getSelector().getAsString();
return true;
}
//declaration
bool VisitObjCMethodDecl(ObjCMethodDecl *decl){ // 包括了 protocol ⽅法的定义
if (!isUserSourceCode(decl)){
return true;
}
DiagnosticsEngine &D = Diagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Meet Method Decl : %0");
D.Report(decl->getLocStart(), diagID) << decl->getSelector().getAsString();
return true;
}
//helper
bool isUserSourceCode (Decl *decl){
std::string filename = SourceManager().getFilename(decl->getSourceRange().getBegin()).str();
if (pty())
return false;
// /Applications/Xcode.app/xxx
if(filename.find("/Applications/Xcode.app/") == 0)
return false;
return true;
}
进⾏编译,现在在警告⾯板应该可以看到我们打出来的警告了。
总结
现在我们成功的编写了第⼀个 Clang 插件,弄清楚了 Clang AST 各个节点的意义,接⼊了 Clang 的回调⽅法,在下⼀篇⽂章中,我们将探索如何检查⽅法的有效性。
参考资料
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论