静态代码分析学习

一 背景

1.软件开发过程中,工程师需要花费大量的时间和精力修改代码缺陷。从下图可以看出,在软件开发过程中,测试成本随着设计构建、QA、系统集成阶段的发展在不断增加。因此工程师应该努力在设计开发阶段优化代码、定位修复代码缺陷,这样可以节省大量时间和人力成本。

2.代码review是代码质量保证的很重要一环,但是人力review精力有限,我们应该尽量使用工具完成基础代码逻辑的review工作,teamleader可以将精力更多放在代码设计和代码核心逻辑上面。这样既可以改善代码质量,也可以提高协同开发的效率。

这里写图片描述

二 原理

2.1 抽象语法树

【代码[静态分析](Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术】

这里面提到的词法、语法分析,是通过解析代码文件并转换成抽象语法树实现。

抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦AST被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。

这里写图片描述
比如下图展示了Helloworld.java的AST:
这里写图片描述

2.2 静态分析类型

代码静态分析实现原理分为两种。一种是分析源代码编译后的中间文件(比如JAVA语言的字节码),一种是分析源文件。
静态代码分析技术主要有:
缺陷模式匹配 : 缺陷模式匹配事先从代码分析经验中收集足够多的共性缺陷模式,将待分析代码与已有的共性缺陷模式进行模式匹配,从而完成软件的安全分析。这种方式的优点是简单方便,但是要求内置足够多缺陷模式,且容易产生误报。

类型检查 :类型推断技术是指通过对代码中运算对象类型进行推理,从而保证代码中每条语句都针对正确的类型执行。这种技术首先将预定义一套类型机制,包括类型等价、类型包含等推理规则,而后基于这一规则进行推理计算。类型推断可以检查代码中的类型错误,简单,高效,适合代码缺陷的快速检测。

数据流分析:数据流分析也是一种软件验证技术,这种技术通过收集代码中引用到的变量信息,从而分析变量在程序中的赋值、引用以及传递等情况。对数据流进行分析可以确定变量的定义以及在代码中被引用的情况,同时还能够检查代码数据流异常,如引用在前赋值在后、只赋值无引用等。数据流分析主要适合检验程序中的数据域特性。

2.3 Android lint AST Parser

自从ADT 16第一次引入Android Lint(以下简称:Lint)以来,Lint便成为Android平台上最重要的静态代码扫描工具。与早期基于XPath的静态扫描工具不同,Lint基于AST(Abstract Syntax Tree)进行分析,可以用来定制很复杂的扫描规则。

Lint从第一个版本就选择了lombok-ast作为自己的AST Parser,并且用了很久。但是Java语言本身在不断更新,Android也在不断迭代出新,lombok-ast慢慢跟不上发展,所以Lint在25.2.0版增加了IntelliJ的PSI(Program Structure Interface)作为新的AST Parser。但是PSI于IntelliJ、于Lint也只是个过渡性方案,事实上IntelliJ早已开始了新一代AST Parser,UAST(Unified AST)的开发,而Lint也将于即将发布的25.4.0版中将PSI更新为UAST。

三 AS静态代码分析工具

Analyze是AS内置的代码分析工具,主要包括:

  1. Inspect Code 包括:缺陷模式匹配、编程语言规范检查、类型检查等
  2. Code Clean up 代码自动整理,运行后,会对选择的项目代码进行优化处理
  3. Analyze Dependenciese 分析工程模块、类之间的依赖关系
  4. Analyze Dataflow 数据流分析

下面我们来详细看一下每个功能的描述和使用方法,每一个子功能以一到两个案例来介绍,其他更多描述可以打开AndroidStudio自行阅读。

3.1 Inspect Code

打开File–Settings–Editor–Inspections,列出了AS支持的代码检查规则,如下图:
这里写图片描述

主要包括:Android、C++、General、JAVA、Spelling等语言领域。下面我们以Android为样本来看一下具体对代码坐了哪些方面的分析:

Android(lint)

lint是最著名的C语言工具之一,是由贝尔实验室SteveJohnson于1979在PCC(PortableC Compiler)基础上开发的静态代码分析,一般由UNIX系统提供。与大多数C语言编译器相比,lint可以对程序进行更加广泛的错误分析,是一种更加严密的编译工具。最初,lint这个工具用来扫描C源文件并对源程序中不可移植的代码提出警告。但是现在大多数lint实用程序已经变得更加严密,它不但可以检查出可移植性问题,而且可以检查出那些虽然可移植并且完全合乎语法但却很可能是错误的特性。随着历史的推移,Lint后来形成了一系列的工具,包括PC-Lint/FlexeLint(Gimpel),LintPlus(Cleanscape)以及Splint。

1.accessibility 检查代码书写是否兼容了安卓平台的辅助工具规范

案例:
Image without contentDescription:
问题概要:[Accessibility] Missing contentDescription attribute on image
解决办法:所有的imageview都添加属性 android:contentDescription=”@string/desc” 此处字符串不能为“”
其实这个属性是方便一些生理功能有缺陷的人使用应用程序的。比如我们有一个ImageView里面放置一张颜色复杂的图片,可能一些色弱色盲的人,分不清这张图片中画的是什么东西。
如果用户安装了辅助浏览工具比如TalkBack,TalkBack就会大声朗读出用户目前正在浏览的内容。TextView控件TalkBack可以直接读出里面的内容,但是ImageView TalkBack就只能去 读
contentDescription的值,告诉用户这个图片到底是什么。

2.Correctness 缺陷模式匹配

案例

(1)WifiManager Leak On versions prior to Android N (24), initializing the WifiManager via Context#getSystemService can cause a memory leak if the context is not the application context. Change context.getSystemService(…) to context.getApplicationContext().getSystemService(…).
Android N平台,创建WifiManager实例的时候,对应的context需要使用Apllication Context,不然会引起内存泄漏。

(2)Fragment not instantiatable From the Fragment documentation: Every fragment must have an empty constructor, so it can be instantiated when restoring its activity’s state. It is strongly recommended that subclasses do not have other constructors with parameters, since these constructors will not be called when the fragment is re-instantiated; instead, arguments can be supplied by the caller with setArguments(Bundle) and later retrieved by the Fragment with getArguments().在使用Fragment的时候,只要创建一个空的构造方法,不要创建带参数的构造方法,对应的参数通过setArguments(Bundle)传入。具体原因是:Activity重新创建时,会重新构建它所管理的Fragment,原先的Fragment的字段值将会全部丢失,但是通过Fragment.setArguments(Bundlebundle)方法设置的bundle会保留下来。所以尽量使用Fragment.setArguments(Bundlebundle)方式来传递参数

3.Internationalization 检查代码书写是否兼容了app国际化标准。

案例

  1. Hardcoding text attributes directly in layout files is bad for several reasons: * When creating configuration variations (for example for landscape or portrait)you have to repeat the actual text (and keep it up to date when making changes) * The application cannot be translated to other languages by just adding new translations for existing string resources. There are quickfixes to automatically extract this hardcoded string into a resource lookup.

    在布局文件中不应当直接写死text的内容。首先,没办法通过配置文件实现多语言适配。并且,横竖屏切换需求中,两个配置文件中需要同步更新,这样做很容易出现遗漏。因此,我们在使用过程中应该将硬编码的字符串挪到资源配置文件中。

  1. Performance 给予性能优化提升建议

案例

1.HashMap can be replaced with SparseArray For maps where the keys are of type integer, it’s typically more efficient to use the Android SparseArray API. This check identifies scenarios where you might want to consider using SparseArray instead of HashMap for better performance. This is particularly useful when the value types are primitives like ints, where you can use SparseIntArray and avoid auto-boxing the values from int to Integer. If you need to construct a HashMap because you need to call an API outside of your control which requires a Map, you can suppress this warning using for example the @SuppressLint annotation.HashMap内部是使用一个默认容量为16的数组来存储数据的,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表).
SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间.

2.Static Field Leaks: A static field will leak contexts. Non-static inner classes have an implicit reference to their outer class. If that outer class is for example a Fragment or Activity, then this reference means that the long-running handler/loader/task will hold a reference to the activity which prevents it from getting garbage collected. Similarly, direct field references to activities and fragments from these longer running instances can cause leaks. ViewModel classes should never point to Views or non-application Contexts.
非静态内部类对于外部类存在引用,如果外部类是Activity或者Fragment,被非静态内部类长生命周期对象比如handler、loader、task引用,那么会引起activity或者fragment的资源不会正常被垃圾回收。因此,避免使用长生命周期对象引用activites或者fragments。
5. Security 检查代码书写是否考虑到了访问控制等安全问题

案例

1.Content provider does not require permission Content providers are exported by default and any application on the system can potentially use them to read and write data. If the content provider provides access to sensitive data, it should be protected by specifying export=false in the manifest or by protecting it with a permission that can be granted to other applications.

2.Code might contain an auth leak Strings in java apps can be discovered by decompiling apps, this lint check looks for code which looks like it may contain an url with a username and password

  1. Usability 检查代码书写是否考虑到了最好的使用交互体验

案例

1.Convert to WebP The WebP format is typically more compact than PNG and JPEG. As of Android 4.2.1 it supports transparency and lossless conversion as well. Note that there is a quickfix in the IDE which lets you perform conversion. Launcher icons must be in the PNG format.
WebP格式占用空间比PNG小很多,通过转换格式使用WebP格式的图片,可以有效减小APK包的大小。

2.Text size is too small Avoid using sizes smaller than 12sp.避免使用小于12sp的文字,可能因为太小不方便用户浏览。

3.2 Code Cleanup

代码自动整理,运行后,会对选择的项目代码进行优化处理。

3.3 Analyze Dependenciese

分析工程、模块、类之间的依赖,呈现图标的形式。

3.3.1 Analyze Dependenciese

左侧显示需要查找的类,右侧列出左侧选中类所依赖的类。从这个图可以很方便的看到一个类引用的全部其他类。

3.3.2 Analyze Module Dependenciese

这里写图片描述
上图清晰展示了工程下各个Module之间的依赖情况,方便开发者在初次阅读代码时对整个工程的骨架有个整体的认知。

3.3 Analyze Cyclic Dependenciese

循环依赖,也即在依赖结构中存在有“环”。这种设计缺陷出现在系统及子系统级别,如果两个或更多的子系统相互依赖,维护和重用几乎是不可能的。在代码设计中需要避免类之间的循环依赖。

3.4 Analyze DataFlow

数据流分析也是一种软件验证技术,这种技术通过收集代码中引用到的变量信息,从而分析变量在程序中的赋值、引用以及传递等情况。对数据流进行分析可以确定变量的定义以及在代码中被引用的情况,同时还能够检查代码数据流异常,如引用在前赋值在后、只赋值无引用等。数据流分析主要适合检验程序中的数据域特性

3.4.1 Analyze DataFlow to Here

分析一个字段赋值的路径来源,从哪儿来

3.4.2 Analyze DataFlow from Here

分析一个字段的使用去向,到哪儿去

四 案例

下面以一个具体代码为例,演示Analyze工具静态代码分析使用过程。

打开AS对应的工程,展开工程目录并选中需要检查的代码对应的包目录

点击Analyze,选中Inspect Code。此时可以决定需要检查的代码范围:选择整个工程,一个Module或者我们刚才选择的包目录。
这里写图片描述

然后点击OK,开始运行检测。工具为我们检查出了很多的问题,提出了不少的建议。此时我们可以针对问题目录逐一展开分析,将确认有改进需求的地方进行修改。
这里写图片描述

经过逐一排查分析改进之后,我们再一次使用Analyze工具分析代码,执行步骤同上,结果如图四,可以看到warning的数量减少了很多。
这里写图片描述

五 自定义Lint规则

目前最新的AS Lint是基于OAST引擎实现,我们可以基于SDK自定义开发规则。Google提供了DEMO参考。在这个DEMO基础上我们根据自身需求实现了两个规则检测:

  1. 项目中不允许使用android.util.Log
  2. 项目中的不允许使用除了org.json.JSONObject和google gso’n以外的json解析库

自定义规则包括:

1、定义一个类继承Detector;定义一个ISSUE类型的静态内部成员常量,ISSUE声明了规则的ID、描述,优先级、问题等级、解析文件类型(java源文件或者class文件,对应的实现类);重写方法getApplicableUastTypes,声明可以检测的UAST类型;重写方法createUastHandler,用于定义解析规则与提示内容。

2、定义一个IssueRegistry,重写getIssues方法,返回值可以是步骤1中定义的ISSUE。

3、在build.gradle中声明 attributes(“Lint-Registry-v2”: “com.example.lint.checks.SampleIssueRegistry”)

4、构建工程

5、将生成的java包放入本地.android/lint目录

6、 重启AS,运行Analyze检测,自定义的规则即生效

6 .总结

Android Studio 静态代码分析(static code analysis)工具能够在代码编写过程中帮助开发人员快速、有效的定位代码缺陷并及时纠正这些问题,从而极大地提高软件可靠性并节省软件开发和测试成本,我们有必要及早开始将这个工具使用起来。

7. 参考

常用 Java 静态代码分析工具的分析与比较
Android自定义Lint实践

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值