@zyl06
2017-02-11T23:46:18.000000Z
字数 14091
阅读 2893
Android
一个项目开发必然会涉及团队协作,而工程质量就需要团队去保证。一般我们期望的代码:无潜在风险、无重复逻辑、风格无差异、可阅读性好、新人上手速度快等。为了达到上述目标,一般团队都会制定一套自己认可的编码规范,并且周期性进行 code review。然而编码规范的制定,那么一套编码规范需要包含哪些内容,另外编码规范仅仅是一套软规范,实际程序员同学能遵守到什么程序还是一个未知数,所以需要我们进行人肉 code review,而这种人肉排查方式,很容易遗漏部分问题,保障性还是有些不足。
为此,我们从编码前期、编码中期和编码后期保证进行了初步尝试。
对于一个 Android 项目,一般需要涉及的编码规范有:
普通 java 编码风格规范
如每个方法最大行数,每个类文件的最大行数,每个方法最大参数数等
普通 java 编码最佳实践
如if
、for
、try
等嵌套深度规范,变量初始化规范等
通用 Android 编码规范(java 部分和 xml 部分),
包含 Android java 部分和 Android xml 部分,如避免使用普通内部类定义handler,避免 layout xml 中存在无用结点等
Gradle 编码规范
如尽量避免 lib 使用 module,独立工程可以通过 aar 或 mvn 方式导入
具体项目相关的编码规范等
如项目团队规定使用自定义 LogUtil 打日志,Activity、Fragment 等重要类的继承关系,Activity 对应的 xml 文件必须以 activity_
开头等
制定了这些内容可以一定程度上规范程序猿的编码,配合团队进行了周期性的 code review (一般是一个版本一次,大概 4 个星期一次),会有比较好的效果。然而即使这么做,还是存在一定的问题,距离我们期望的目标还是比较远。比如各个单例类的定义五花八门,使用 LogUtil
代替 Log
的使用,Message.Obtain()
代替 new Message()
,Activity
部分文件命名,甚至 Activity
、Fragment
的基类定义规则还是很容易发生错误,并没有被发现。随着编码规范的完善充实,多个开发的编码规范如何保证,就会成为一个显而易见的问题。
为了实现公用代码复用,我们定义了一些 util
工具类,但随着各个开发的补充,这套 util
工具类也越来越多,如 LogUtil
、KeyboardUtil
等,而这些类一部分是为了统一入口,如统一使用 LogUtil
,可以统一做到测试服打开本地日志,线上服关闭日志;KeyboardUtil
方便使用者控制键盘的弹出隐藏等操作。虽然定义了这些工具类,但终究存在应该使用而没有使用的情况。当然这些工具代码并不难,开发在自己的模块也能很容易的实现和使用,一般也不会出问题。然而上述讲的优点都会消失掉。而这些问题依赖 code review 也是件头疼的问题。
此外,RecycleView
的编码方式,单例模式的实现方式等等,各个开发可能写出各式代码,甚至实现的单例模式并不是线程安全的。
提了这么多,另一方面,要求开发在繁忙的业务中严格遵守这些规范,也有些强人所难。所幸,Android Studio 为我们提供了编码模板来解放开发的工作,并一定程度上统一编码风格。
查看一个使用 Android Studio 中很常见的例子,输入 for
,出现下拉列表如下:
当选择 fori
,出现编码片段:
确认循环变量 i
,跳入循环结果值的输入:
上例,就是 Android Studio 中系统提供的 Live Template
一个实例。这个类似于 iOS 中的 Code Snippets
,提供了代码片段的能力。
Android Studio
(Mac) 进入 Settings/Preferences
-> Editor
-> Live Templates
,可以看到已定义的模板组:
查看 fori
编码模板的实现:
Tab
iterations
fori
构建项目 group
输入 group 的名称
构建具体编码模板
变量定义
变量形式为 $<variable_name>$
,点击 Edit variables
可设置变量具体内容:
模板应用环境
点击 No application contexts yet. Define
,设置为 java 环境:
模板文件
构建了模板 group 后,在 android studio config\templates
目录下查看到 yanxuan.xml
。
windows:
C:\Users\\<user>\\.AndroidStudiox.x\config\templates
(user 为你的计算机用户名)
mac:~/Library/Preferences/AndroidStudiox.x/templates
<templateSet group="test">
<template name="yxtest" value="testMethod($a$, $b$);" description="这是一个测试模板" toReformat="false" toShortenFQNames="true">
<variable name="a" expression="lineNumber()" defaultValue="2" alwaysStopAt="false" />
<variable name="b" expression="" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_CODE" value="true" />
<option name="JAVA_STATEMENT" value="true" />
<option name="JAVA_EXPRESSION" value="true" />
<option name="JAVA_DECLARATION" value="true" />
<option name="JAVA_COMMENT" value="true" />
<option name="JAVA_STRING" value="true" />
<option name="COMPLETION" value="true" />
</context>
</template>
</templateSet>
设置完毕,实践查看:
yxtest
singleton
背景
除了 Live Template
之外,工程项目中很多新建的类也有很多机械的代码,如我们定义的 Activity 要么继承自 BaseBlankActivity
,要么继承自 BaseActionBarActivity
,另外项目中采用 MVP
模式,因此一个 Activity 基本上会有一个对应的 presenter
类,一个 layout
文件,同时很多时候,一个页面中会有一个需要支持刷新的 RecycleView
等。除此之外,ViewHolder
、HttpTask
等代码也是固定模式的代码。
这些都是固定机械的代码,而如果是人肉去写的话,难免会出现代码风格不一致、不规范的情况,同时也浪费了一部分的时间。所幸,Android Studio 提供了工程类模板,方便我们实现这样的功能。
系统模板
查看 Android Studio 系统类模板,我们能发现有很多定义好的类模板:
如需要创建一个空的 Activity 页面,可以选择 Empty Activity
,并填写类名,layout 名称等信息,之后就能出现对应的添加或修改:MainActivity.java
、activity_main.xml
、AndroidManifest.xml
:
而这些模板定义,可以在相关路径文件中找到:
${Android Studio 的安装目录}/plugins/android/lib/templates/
Android Studio.app/Contents/plugins/android/lib/templates/
针对 EmpytActivity 这里需要定义的文件有:
globals.xml.ftl
:定义当前模板的一些全局变量recipe.xml.ftl
:定义模板拷贝的逻辑等template.xml
:定义模板对话框的样式template_blank_activity.png
:定义模板的图标root/src/app_package/SimpleActivity.java.ftl
:具体的模板文件图片来自:http://www.slideshare.net/murphonic/custom-android-code-templates-15537501
自定义模板
而针对我们需要自定义的模板,可以在模板定义路径下新建文件夹和文件即可,细节内容可查看 Tutorial How To Create Custom Android Code Templates
项目的模板文件内容:
设置完模板文件之后,重启 Android Studio,可以生效模板文件,使用模板文件如下:
由上,我们定义了编码规范,定义了 Live Template 和 Android Studio Template 方便程序猿更好的准守我们的项目编码规范。然后编码规范毕竟只是软规范,而提供编码模板更多的解决大量 util 的使用问题和便利小伙伴完成机械编码,并不能完全保证程序猿严格按照全部的规范来编码。
为此,我们需要一套静态代码检查机制能检查已有的代码是否遵守规范。总结已有的规范,可以将规范类型归纳为普通 Java 规范、普通 Android 规范、具体项目规范等。而这些检查点,可以配合不同的检查工具进行检查。
对于 java
规范,checkstyle 帮助开发者实现常用的检查。这里 CheckStyle 能检查的内容有:
检查内容很多,而检查项需要和具体的项目规范做结合。如,每行代码字符数控制在 80,单页代码行数控制在 800 等。因此需要结合配置文件,来检查项目中的 java 代码。在 Android Studio 上配置 CheckStyle 流程如下:
在 Android Studio 中添加 gradle Plugin
apply plugin: 'checkstyle'
设置 CheckStyle 版本
checkstyle {
toolVersion '6.1.1'
showViolations true
}
配置 CheckStyle 检查项
task checkstyle(type: Checkstyle) {
configFile file("$configDir/checkstyle/checkstyle.xml")
configProperties.checkstyleSuppressionsPath = file("$configDir/checkstyle/suppressions.xml").absolutePath
source 'src'
include '**/*.java' // 检查 java 代码
exclude '**/gen/**' // 排除生成的代码
classpath = files()
ignoreFailures true // 忽略检查失败的情况,避免gradle命令执行中止
}
配置自定义的检查项:
checkstyle.xml
:
<!--单个文件方法数上限最多为 30-->
<module name="MethodCount">
<property name="maxTotal" value="30"/>
</module>
<!--方法名的首字母是小写-->
<module name="MethodName">
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
</module>
<!--静态变量名的首字符是 s-->
<module name="StaticVariableName">
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
</module>
<!--只有私有构造函数的类需要定义成 final 类型-->
<module name="FinalClass"/>
...
具体其他的检查项配置可以查看 检查配置链接
执行 checkstyle 检查
./gradlew checkstyle
查看检查结果
命令执行结束,查看检查结果文件:
${project}/app/build/reports/checkstyle/checkstyle.html
与 CheckStyle
工具不同的是,FindBugs
不注重样式或者格式,而是试图寻找出真正的缺陷或者现在的性能问题。FindBugs
检查类和 Jar
文件,不是通过分析类文件的形式或结构来分析程序,而是使用 Visitor
模式,将字节码与一组缺陷模式进行对比以发现可能的问题。而这些问题比如如下:
忽略返回值
上述代码执行结束之后,并没有什么意义,变量 a
的值也不会变成 dddbbbccc
。因此,上述代码很可能是程序猿的 bug。为此 FindBugs 能找出这种问题
空指针示例
上述最后一行代码,很明显在执行的时候会发生空指针异常,这里因为 FindBugs 无法知道变量 strMaps
是否确实有 aaa
这个 key,为此这里会检查出错误。
未初始化的成员变量使用
这里由于类成员变量 actions
并未初始化,因此当 actions.add("TEST")
被执行的时候会发生异常。
Android Studio 上 FindBugs 的集成如下:
在 gradle 中引入插件
apply plugin: 'findbugs'
在 gradle 中配置 findbugs task
task findbugs(type: FindBugs, dependsOn: "assembleDebug") {
ignoreFailures = false
effort = "max"
reportLevel = "high"
excludeFilter = new File("$configDir/findbugs/findbugs-filter.xml")
classes = files("${project.rootDir}/app/build/intermediates/classes")
source 'src'
include '**/*.java'
exclude '**/gen/**'
reports {
xml.enabled = false
html.enabled = true
xml {
destination "$reportsDir/findbugs/findbugs.xml"
}
html {
destination "$reportsDir/findbugs/findbugs.html"
}
}
classpath = files()
ignoreFailures true // 避免检查失败 gradle 执行中止
}
执行 findbugs 检查
./gradlew findbugs
查看检查结果
查看检查结果文件:
${project}/app/build/reports/findbugs/findbugs.html
前面 FindBugs
的检查实例(忽略返回值
, 未初始化的成员变量使用
),可以发现在 Android Studio IDE 上,已经出现了标黄提示,我们把光标放上去,就能看到具体的提示了:
按 cmd + F1
可以看到具体的错误提示:
这就原生 Lint
给我们提供的错误提示功能。除了和 FindBugs
重复的纯 java
代码检查之外,Lint 能检查很多其他工具无法检查的内容,也更贴合 Android:
在 Activity 内定义非静态内部类 Handler 的报警
在
AndroidManifest.xml
中定义 export 为 true 的广播接受器,但没有定义权限,Lint 检查认为是不安全的
build.gradle 文件中引用的 support 包的版本低的提示
Android Lint 是一个静态代码检查工具,能够对潜在的 bug,可能的安全性、性能、可用性、可访问性、国际化等优化内容做出监测:
来自官方文档 Improve Your Code with Lint
lint-result.html
在 Android SDK Tools 16 及更高的版本中,Lint 工具会自动安装。原生 Lint 的检查项已经有 200 多项 (包括前面示例的 5 项内容),因此使用原生的功能点,就能检查开发中的大部分通用问题。
Android Studio IDE 上配置 Lint 检查偏好设置
(Mac 下) Preferences
→ Editor
→ Inspections
进入 Android Studio
的 Lint
配置界面
lint.xml
上配置 Lint
除了可以通过 IDE 配置 Lint,还可以通过直接 lint.xml 为单个项目配置检查规则
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Disable the given check in this project -->
<issue id="IconMissingDensityFolder" severity="ignore" />
<!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
<issue id="ObsoleteLayoutParam">
<ignore path="res/layout/activation.xml" />
<ignore path="res/layout-xlarge/activation.xml" />
</issue>
<!-- Ignore the UselessLeaf issue in the specified file -->
<issue id="UselessLeaf">
<ignore path="res/layout/main.xml" />
</issue>
<!-- Change the severity of hardcoded strings to "error" -->
<issue id="HardcodedText" severity="error" />
</lint>
来源 Android Develop 文档 Improve Your Code with Lint
gradle 中配置 Lint task
android {
lintOptions {
abortOnError false // 配置 lint 过程中出错,不中止 gradle 任务
xmlReport false
htmlReport true
lintConfig file("$configDir/lint/lint.xml") // 配置 lint 检查规则
htmlOutput file("$reportsDir/lint/lint-result.html") // 配置 lint 输出文件
xmlOutput file("$reportsDir/lint/lint-result.xml") // 配置 lint 输出文件
}
}
执行检查
在工程根目录执行以下命令 (Mac),以执行检查任务
./gradlew lint
检查结果
生成的检查结果在 ${项目工程}/app/build/reports/lint/lint-result.html
虽然原生的 Lint 检查已经很强大了,检查项也已经很多,然而还是无法满足项目中的特有需求:
LogUtil
activity_XXX
fragment_XXX
BaseBlankActivity
或 BaseActionBarActivity
对于以上这些需求,原生 Lint 检查(包括 CheckStyle
,FindBugs
)就已经无能为力了,我们必须编码支持自定义检查。以项目中集成的 Lint 检查为例,讲述流程:
lint
库
dependencies {
...
compile 'com.android.tools.lint:lint-api:24.5.0'
compile 'com.android.tools.lint:lint-checks:24.5.0'
}
IssueRegistry
类新建一个 MyIssueRegistry
类,继承自 IssueRegistry
。用来注册我们自定义的全部 issue
public class MyIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
System.out.println("********YXLint rules works!!!********");
return Arrays.asList(
LogUsageDetector.ISSUE,
ToastUsageDetector.ISSUE,
ActivitySuperClassDetector.ACTIVITY_SUPER_CLASS_ISSUE,
...
BuildGradleVersionDetector.ISSUE);
}
}
其中:
LogUsageDetector.ISSUE
:用于检查不允许直接使用 Log.*
方式输出本地日志的代码ToastUsageDetector.ISSUE
:用于检查直接用 Toast
方式显示 toast 的代码ActivitySuperClassDetector.ACTIVITY_SUPER_CLASS_ISSUE
:用于检查 Activity 的基类BuildGradleVersionDetector.ISSUE
:用于检查 gradle 文件中不允许直接写数字版本号的代码IssueRegistry
类
jar {
manifest {
attributes('Lint-Registry': 'com.netease.htlint.lintrules.MyIssueRegistry')
}
}
Detector
MyIssueRegistry
类中声明注册了各个 Detector
的 Issue
。Issue
由 Detector
发现并报告,是 Android 程序代码可能存在的风险。而这里就需要真正实现这些 Detector
,以检查 Activity 的基类为例。
ACTIVITY_SUPER_CLASS_ISSUE
这个 Issue
的定义需要使用 Issue.create(...)
方式实现,同时需要传入 6 个参数分别如下:
Issue
Fatal
, Error
, Warning
, Informational
, Ignore
Issue
和 Detector
提供映射关系,Detector
就是当前类。声明扫描检测的范围 Scope
,描述 Detector
需要分析时需要考虑的文件集,包括:Resource
文件或目录、Java
文件、Class
文件ActivitySuperClassDetector
继承自 Detector
,并实现 Detector.JavaScaner
。这里主要自定义实现的方法如上图 H,I
BaseBlankActivity
或 BaseActionBarActivity
?如果都不是的话,则报告错误完成上述步骤,可以在控制台中通过命令 ../../gradlew assemble
来执行编译任务,就可以输出我们需要的 jar 文件 (htlintrules_jar-0.0.1.jar
) 了
按照 Google 方法,可以将 htlintrules_jar-0.0.1.jar
拷贝到 ~/.android/lint
中,但缺点是针对会影响一台机器其他的工程。很明显,我们的自定义 Lint
检查有很多是项目中特有的一些编码规范。
为此,我们采用 LinkedIn
方案:将 jar 放到一个 aar 中。这样我们就可以针对工程进行自定义 Lint,lint.jar 只对当前工程有效。
在现有的 htlintrules_jar
工程的 build.gradle 中添加代码,整体看起来如下:
apply plugin: 'java'
apply plugin: 'maven'
dependencies {
compile 'com.android.tools.lint:lint-api:24.5.0'
compile 'com.android.tools.lint:lint-checks:24.5.0'
}
jar {
manifest {
attributes('Lint-Registry': 'com.netease.htlint.lintrules.MyIssueRegistry')
}
}
configurations {
lintJarOutput
}
dependencies {
lintJarOutput files(jar)
}
defaultTasks 'assemble'
同时新建另一个工程 htlint
,在其 build.gradle
文件中添加如下代码:
/*
* rules for including "lint.jar" in aar
*/
configurations {
lintJarImport
}
dependencies {
lintJarImport project(path: ':htlintrules_jar', configuration: "lintJarOutput")
}
task copyLintJar(type: Copy) {
from (configurations.lintJarImport) {
rename {
String fileName ->
'lint.jar'
}
}
into 'build/intermediates/lint/'
}
project.afterEvaluate {
def compileLintTask = project.tasks.find { it.name == 'compileLint' }
compileLintTask.dependsOn(copyLintJar)
}
最后在 app 工程的 build.gradle
中添加 htlint 引用,配置完成
dependencies {
compile project(':htlint') // lint 检查库
...
}
在 ${项目工程}/app/
目录下执行 ../gradlew lint
:
根据提示查看 lint-result.html
文件,可以查看到前面编写的
ActivitySuperClassDetector.ACTIVITY_SUPER_CLASS_ISSUE
已经生效,并且检查出了相关的非规范代码。前面很好的给出了检查结果了,然而我们会发现,FullScreenVideoActivity
确实是需要的错误检查结果,而 WXEntryActivity
却不是,这个类是有集成微信分享时需要的,并且按照微信开放平台的文档来编写,因此并不需要按照项目规范,继承 BaseBlankActivity
或 BaseActionBarActivity
。为此,我们期望 WXEntryActivity
不应该被检查出 WrongActivitySuperClass
错误
为此,我们可以在 WXEntryActivity
类名签名添加 SuppressLint
注解:
@SuppressLint("WrongActivitySuperClass")
public class WXEntryActivity extends Activity implements IWXAPIEventHandler{
...
}
排除 java 类或者方法的 Lint 检查
若需要抑制某个 Issue 检查,可以在类定义签名或者方法定义签名,添加注解 @SuppressLint(${IssueId})
。这里设置的就是具体某个 Issue
的 id
值
若需要抑制全部的 Issue 检查,可以使用 all
关键字,比如:@SuppressLint("all")
排除 xml 资源的 Lint 检查
如项目中引入微博分享 sdk,按照官方文档,需要在 AndroidManifest 中声明 com.sina.weibo.sdk.net.DownloadService
这个 Service,而这个 Service 会被 Lint 检查为未定义,为此需要 xml 文件中也过滤部分代码的 Lint 的检查:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.netease.yanxuan">
...
<service
android:name="com.sina.weibo.sdk.net.DownloadService"
android:exported="false"
tools:ignore="MissingRegistered" />
</manifest>
这里对于单个 Issue 过滤的规则为:tools:ignore=${IssueId}
如果需要过滤全部的 Issue,可以使用 all
关键字:tools:ignore="all"
360 火线 是 360 公司和信息安全部门深度合作,定制的适用于 360 公司产品的安卓 APP 安全检查规则。总共覆盖 61 项代码检查。使用也非常方便,细节看 使用文档,可以直接使用 jar 包并执行命令或集成 Android Studio Plugin 执行检查
pmd 代码检查工具,包含 16 个规则集,涵盖了 Java 的各种常见问题。其中规则集包含 基本(rulesets/basic.xml)
,终结函数(finalizer)
,未使用的代码(rulesets/unusedcode.xml)
,设计(rulesets/design.xml)
等。
相比 FindBugs
,pmd
的一些规则更具争议,但 pmd
支持我们构建自己的规则集
<?xml version="1.0"?>
<ruleset name="customruleset">
<description>
Sample ruleset for developerWorks article
</description>
<rule ref="rulesets/design.xml"/>
<rule ref="rulesets/naming.xml"/>
<rule ref="rulesets/basic.xml"/>
</ruleset>
为整合这些检查工具,在 gradle
中自定义 check 命名,并依赖其他的 task。在执行检查的时候,可以通过 ./gradlew check
来执行全部的检查命令。
check.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint'
另一方面,这种代码检查,如果等到开发完成的时候再去执行,很可能问题积累了很多,甚至导致产品上线前,开发并不能来得及修正全部的问题。为此,可以将代码检查的命令集成 jenkins
,保证开发每天都能看到当前的代码的缺陷,能及时的修改
我们从编码前的编码规范,编码进行中的编码模板,编码结束后的代码静态检查,保障了程序小伙伴们的代码。除此之外,还有很多不完善的地方需要我们做进一步处理: