Android Lint:基本使用与配置

说明

本文部分内容要求读者有一定的Gradle基础,例如Gradle基本操作、Gradle插件开发基础等。

Lint简介

Lint是Android提供的一套静态代码检查工具,可对Java代码、资源文件等做检查。

在Android Studio中开发时,经常会看到一些关于Android API的警告,例如使用Toast时忘了调用show方法、布局文件中直接引用了硬编码的文本而没有定义到strings.xml中等,这些都是Android Lint检查实现的。

Lint检查执行方式

Lint有多种执行方式,这里做个简单总结。

也可以参考谷歌官方文档 https://developer.android.com/studio/write/lint.html

Android Studio相关的使用技巧可以参考 http://www.paincker.com/android-studio-skill

Android Studio的Code Inspections

Inspections是IDEA中用于代码静态检查的组件,包括语法检查、拼写检查等,Android Studio在IDEA的Inspections基础上实现了Lint检查。

实时执行

在Android Studio中写代码时,Inspections会实时执行,其中包括Android定义的Lint规则,并在代码中直接标为黄色警告或红色错误。还可以使用Andorid Studio的Intentions工具快速添加SupressLint或自动修复。

Intentions的使用可参考 http://www.paincker.com/android-studio-skill

可在Android Studio的设置中配置代码实时检查时,要检查的项

  1. Preference - Editor - Inspections:代码检查
  2. 默认有Default和Project Default两套配置,作用范围分别是全局和当前工程

手动执行

在Android Studio中可从菜单Analyze - Inspect Code手动执行Inspections,可选择要检查代码的范围(Inspection Scope),以及要检查的问题(Inspection Profile)。执行完成后效果如图,可以双击结果定位有问题的代码

Gradle Task执行

Gradle中编译代码时,会以任务(Task)的形式组织每一步操作,Task之间可以存在依赖关系,执行一个Task前,会先执行它所依赖的Task。

Android的Gradle环境下,有下面几个常见的Task。

  • assemble:Gradle内建的编译任务
  • check:Gradle内建的检查任务
  • test:Gradle内建的测试任务
  • build:包含assemble、check、test
  • lint:Android插件定义的Lint检查任务,被包含到check任务中

在命令行中可调用gradle执行Task,例如在命令行中调用gradle lint即可执行lint任务。对于某个具体的BuildType(例如Debug)和ProductFlavor(例如Huawei),还可以执行gradle lintHuaweiDebug只对这种Build版本做Lint检查。

Lint默认会把所有结果以XML和HTML格式,输出到build/reports/lint-results-xxx中,可以在此查看所有Lint问题,包括具体在哪一行,以及Lint问题对应的ID。

Lint命令行

直接执行lint命令,可执行文件位于<android-home>/tools/lint(<android-home>即AndroidSDK所在目录)。结果也会输出到报表中。具体参数配置可以用lint --help查看。

Lint配置

Issue、Severity

  • 每个Lint规则,称为一个Issue。

  • 每个Issue有一个id,例如NewApi、WrongViewCast、ShowToast等。

  • 每个Issue都定义了默认的优先级(Severity),包括Fatal、Error、Warning、Information、Ignore。其中Ignore表示忽略该Issue的检查。

  • 实际Issue的Severity可被Lint配置覆盖。

SupressLint

有时Lint会误报,或者特定代码明确不希望或不需要检查特定Lint问题。此时可使用SupressLint注解,对这个类、方法等跳过指定id的Lint检查,类似下面的写法。

1
2
3
4
5
6
7
8
9
@SuppressLint("NewApi")
private void method() {
new View(this).setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {

}
});
}

Lint.xml的配置

在Android工程中,可以创建XML文件配置Lint规则的优先级和忽略路径,示例如下。配置文件默认路径为模块目录下的lint.xml,也可以通过LintOptions指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8" ?>
<lint>
<!--配置所有Issue的默认优先级-->
<issue id="all" severity="warning"/>
<!--配置指定Issue的优先级-->
<issue id="ShowToast" severity="error"/>
<!--忽略指定路径的指定问题,支持正则表达式或path匹配-->
<issue id="NewApi">
<ignore regexp=".*MainActivity.java"/>
<ignore path="**/com/demo/lint/MainActivity.java"/>
<ignore path="res/layout/activation.xml"/>
</issue>
</lint>

LintOptions的配置

在Android工程的build.gradle中,可以通过lintOptions配置Lint,例如下面的代码。LintOptions中配置的Issue优先级,会覆盖lint.xml中的配置。

1
2
3
4
5
6
7
8
9
android {
lintOptions {
abortOnError true
ignoreWarnings false
warningsAsErrors false
check('LogUse', 'SystemOut')
lintConfig file('lint.xml')
}
}

其中一些常用配置项如下:

  • abortOnError = true,则发现Error级别Lint问题时会中断编译

  • ignoreWarnings = true,则不检查Warning级别的Lint问题

  • warningsAsErrors = true,则对Warnning问题会和Error一样处理

  • checkReleaseBuilds,默认为true,Release时检查fatal级别的问题

  • lintConfig,默认为lint.xml,指定XML配置文件位置

  • enable,配置要检查的问题

  • disable,指定不检查的问题

  • check,指定所有要检查的问题,没有指定的都不检查

LintOptions的配置,可以参考

Android Gradle DSL官方文档 http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.LintOptions.html

Severity计算规则

实际学习发现,官方文档中说的LintOptions配置并不是很明白,特别是check、disable、enable等参数的优先级。通过查看其代码实现,才弄清楚了配置的使用。

对于某个Issue的Severity计算规则,可用伪代码描述如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CliConfiguration.getSeverity() {
severity = CliConfiguration.computeSeverity() {

severity = DefaultConfiguration.getSeverity() {
XML配置文件中设置的优先级(例如<issue id="NewApi" severity="ignore"/>)
XML配置文件中用“all”设置的默认优先级(例如<issue id="all" severity="ignore"/>)
如果mParent非空,则返回mParent.getSeverity (例如defaultConfig和Flavor中分别配置了LintOptions)
LintCliClient.getDefaultSeverity() {
如果设置了checkAllWarnings,则返回Issue定义的优先级
DefaultConfiguration.getDefaultSeverity {
如果Issue定义了默认不启用(isEnabledByDefault),则返回Ignore
返回Issue定义的优先级
}
}
}

如果disable参数包含了Issue则返回Ignore
SeverityOverrides参数设置的优先级
如果是enabled和check参数都包含的Issue {
如果severity == Ignore,则返回Warning
返回severity
}
如果check非空且没有包含该Issue,则返回Ignore
返回severity
}

如果设置了FatalOnly且severity != Fatal,则返回Ignore
如果设置了WarningsAsErrors且severity == Warning,则返回Error
如果设置了IgnoreWarning且severity == Warning,则返回Ignore
返回severity
}

具体实现可参考下面代码中的CliConfiguration.getSeverity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// com.android.tools.lint.client.api.DefaultConfiguration

@NonNull
protected Severity getDefaultSeverity(@NonNull Issue issue) {
if (!issue.isEnabledByDefault()) {
return Severity.IGNORE;
}

return issue.getDefaultSeverity();
}

@Override
@NonNull
public Severity getSeverity(@NonNull Issue issue) {
ensureInitialized();

Severity severity = mSeverity.get(issue.getId());
if (severity == null) {
severity = mSeverity.get(VALUE_ALL);
}

if (severity != null) {
return severity;
}

if (mParent != null) {
return mParent.getSeverity(issue);
}

return getDefaultSeverity(issue);
}

// com.android.tools.lint.LintCliClient$CliConfiguration extends DefaultConfiguration

@NonNull
@Override
protected Severity getDefaultSeverity(@NonNull Issue issue) {
if (mFlags.isCheckAllWarnings()) {
return issue.getDefaultSeverity();
}

return super.getDefaultSeverity(issue);
}

private Severity computeSeverity(@NonNull Issue issue) {
Severity severity = super.getSeverity(issue);

String id = issue.getId();
Set<String> suppress = mFlags.getSuppressedIds();
if (suppress.contains(id)) {
return Severity.IGNORE;
}

Severity manual = mFlags.getSeverityOverrides().get(id);
if (manual != null) {
return manual;
}

Set<String> enabled = mFlags.getEnabledIds();
Set<String> check = mFlags.getExactCheckedIds();
if (enabled.contains(id) (check != null && check.contains(id))) {
// Overriding default
// Detectors shouldn't be returning ignore as a default severity,
// but in case they do, force it up to warning here to ensure that
// it's run
if (severity == Severity.IGNORE) {
severity = issue.getDefaultSeverity();
if (severity == Severity.IGNORE) {
severity = Severity.WARNING;
}
}

return severity;
}

if (check != null && issue != LINT_ERROR && issue != PARSER_ERROR) {
return Severity.IGNORE;
}

return severity;
}

@NonNull
@Override
public Severity getSeverity(@NonNull Issue issue) {
Severity severity = computeSeverity(issue);

if (mFatalOnly && severity != Severity.FATAL) {
return Severity.IGNORE;
}

if (mFlags.isWarningsAsErrors() && severity == Severity.WARNING) {
severity = Severity.ERROR;
}

if (mFlags.isIgnoreWarnings() && severity == Severity.WARNING) {
severity = Severity.IGNORE;
}

return severity;
}

查看支持的所有Lint Issue Id

执行<android-home>/tools/lint --list命令,可查看支持的所有Issue的id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
Valid issue categories:
Correctness
Correctness:Messages
Security
Performance
Usability:Typography
Usability:Icons
Usability
Accessibility
Internationalization
Bi-directional Text

Valid issue id's:
"ContentDescription": Image without contentDescription
"AddJavascriptInterface": addJavascriptInterface Called
"ShortAlarm": Short or Frequent Alarm
"AlwaysShowAction": Usage of showAsAction=always
"ShiftFlags": Dangerous Flag Constant Declaration
"LocalSuppress": @SuppressLint on invalid element
"UniqueConstants": Overlapping Enumeration Constants
"InlinedApi": Using inlined constants on older versions
"Override": Method conflicts with new inherited method
"NewApi": Calling new methods on older versions
"UnusedAttribute": Attribute unused on older versions
"AppCompatMethod": Using Wrong AppCompat Method
"AppCompatResource": Menu namespace
"AppIndexingError": Wrong Usage of App Indexing
"AppIndexingWarning": Missing App Indexing Support
"InconsistentArrays": Inconsistencies in array element counts
"Assert": Assertions
"BackButton": Back button
"ButtonCase": Cancel/OK dialog button capitalization
"ButtonOrder": Button order
"ButtonStyle": Button should be borderless
"ByteOrderMark": Byte order mark inside files
"MissingSuperCall": Missing Super Call
"AdapterViewChildren": AdapterViews cannot have children in XML
"ScrollViewCount": ScrollViews can have only one child
"GetInstance": Cipher.getInstance with ECB
"CommitTransaction": Missing commit() calls
"Recycle": Missing recycle() calls
"ClickableViewAccessibility": Accessibility in Custom Views
"EasterEgg": Code contains easter egg
"StopShip": Code contains STOPSHIP marker
"CustomViewStyleable": Mismatched Styleable/Custom View Name
"CutPasteId": Likely cut & paste mistakes
"SimpleDateFormat": Implied locale in date format
"Deprecated": Using deprecated resources
"MissingPrefix": Missing Android XML namespace
"MangledCRLF": Mangled file line endings
"DuplicateIncludedIds": Duplicate ids across layouts combined with include
tags
"DuplicateIds": Duplicate ids within a single layout
"DuplicateDefinition": Duplicate definitions of resources
"ReferenceType": Incorrect reference types
"ExtraText": Extraneous text in resource files
"FieldGetter": Using getter instead of field
"FullBackupContent": Valid Full Backup Content File
"ValidFragment": Fragment not instantiatable
"PackageManagerGetSignatures": Potential Multiple Certificate Exploit
"GradleCompatible": Incompatible Gradle Versions
"AndroidGradlePluginVersion": Incompatible Android Gradle Plugin
"GradleDependency": Obsolete Gradle Dependency
"GradleDeprecated": Deprecated Gradle Construct
"GradleGetter": Gradle Implicit Getter Call
"GradleIdeError": Gradle IDE Support Issues
"GradlePath": Gradle Path Issues
"GradleDynamicVersion": Gradle Dynamic Version
"StringShouldBeInt": String should be int
"NewerVersionAvailable": Newer Library Versions Available
"AccidentalOctal": Accidental Octal
"GridLayout": GridLayout validation
"HandlerLeak": Handler reference leaks
"HardcodedDebugMode": Hardcoded value of android:debuggable in the manifest
"HardcodedText": Hardcoded text
"IconDuplicatesConfig": Identical bitmaps across various configurations
"IconDuplicates": Duplicated icons under different names
"GifUsage": Using .gif format for bitmaps is discouraged
"IconColors": Icon colors do not follow the recommended visual style
"IconDensities": Icon densities validation
"IconDipSize": Icon density-independent size validation
"IconExpectedSize": Icon has incorrect size
"IconExtension": Icon format does not match the file extension
"IconLauncherShape": The launcher icon shape should use a distinct silhouette
"IconLocation": Image defined in density-independent drawable folder
"IconMissingDensityFolder": Missing density folder
"IconMixedNinePatch": Clashing PNG and 9-PNG files
"IconNoDpi": Icon appears in both -nodpi and dpi folders
"IconXmlAndPng": Icon is specified both as .xml file and as a bitmap
"IncludeLayoutParam": Ignored layout params on include
"DisableBaselineAlignment": Missing baselineAligned attribute
"InefficientWeight": Inefficient layout weight
"NestedWeights": Nested layout weights
"Orientation": Missing explicit orientation
"Suspicious0dp": Suspicious 0dp dimension
"InvalidPackage": Package not included in Android
"DrawAllocation": Memory allocations within drawing code
"UseSparseArrays": HashMap can be replaced with SparseArray
"UseValueOf": Should use valueOf instead of new
"JavascriptInterface": Missing @JavascriptInterface on methods
"LabelFor": Missing labelFor attribute
"InconsistentLayout": Inconsistent Layouts
"InflateParams": Layout Inflation without a Parent
"DefaultLocale": Implied default locale in case conversion
"LocaleFolder": Wrong locale name
"InvalidResourceFolder": Invalid Resource Folder
"WrongRegion": Suspicious Language/Region Combination
"UseAlpha2": Using 3-letter Codes
"LogConditional": Unconditional Logging Calls
"LongLogTag": Too Long Log Tags
"LogTagMismatch": Mismatched Log Tags
"AllowBackup": Missing allowBackup attribute
"MissingApplicationIcon": Missing application icon
"DeviceAdmin": Malformed Device Admin
"DuplicateActivity": Activity registered more than once
"DuplicateUsesFeature": Feature declared more than once
"GradleOverrides": Value overridden by Gradle build script
"IllegalResourceRef": Name and version must be integer or string, not
resource
"MipmapIcons": Use Mipmap Launcher Icons
"MockLocation": Using mock location provider in production
"MultipleUsesSdk": Multiple <uses-sdk> elements in the manifest
"ManifestOrder": Incorrect order of elements in manifest
"MissingVersion": Missing application name/version
"OldTargetApi": Target SDK attribute is not targeting latest version
"UniquePermission": Permission names are not unique
"UsesMinSdkAttributes": Minimum SDK and target SDK attributes not defined
"WrongManifestParent": Wrong manifest parent
"ManifestTypo": Typos in manifest tags
"FloatMath": Using FloatMath instead of Math
"MergeRootFrame": FrameLayout can be replaced with <merge> tag
"InnerclassSeparator": Inner classes should use $ rather than .
"Instantiatable": Registered class is not instantiatable
"MissingRegistered": Missing registered class
"MissingId": Fragments should specify an id or tag
"LibraryCustomView": Custom views in libraries should use res-auto-namespace
"ResAuto": Hardcoded Package in Namespace
"NamespaceTypo": Misspelled namespace declaration
"UnusedNamespace": Unused namespace
"NegativeMargin": Negative Margins
"NestedScrolling": Nested scrolling widgets
"NfcTechWhitespace": Whitespace in NFC tech lists
"UnlocalizedSms": SMS phone number missing country code
"ObsoleteLayoutParam": Obsolete layout params
"OnClick": onClick method does not exist
"Overdraw": Overdraw: Painting regions more than once
"DalvikOverride": Method considered overridden by Dalvik
"OverrideAbstract": Not overriding abstract methods on older platforms
"ParcelCreator": Missing Parcelable CREATOR field
"UnusedQuantity": Unused quantity translations
"MissingQuantity": Missing quantity translation
"ImpliedQuantity": Implied Quantities
"ExportedPreferenceActivity": PreferenceActivity should not be exported
"PackagedPrivateKey": Packaged private key
"PrivateResource": Using private resources
"ProguardSplit": Proguard.cfg file contains generic Android rules
"Proguard": Using obsolete ProGuard configuration
"PropertyEscape": Incorrect property escapes
"UsingHttp": Using HTTP instead of HTTPS
"SpUsage": Using dp instead of sp for text sizes
"InOrMmUsage": Using mm or in dimensions
"PxUsage": Using 'px' dimension
"SmallSp": Text size is too small
"Registered": Class is not registered in the manifest
"RelativeOverlap": Overlapping items in RelativeLayout
"RequiredSize": Missing layout_width or layout_height attributes
"AaptCrash": Potential AAPT crash
"ResourceCycle": Cycle in resource definitions
"ResourceName": Resource with Wrong Prefix
"RtlCompat": Right-to-left text compatibility issues
"RtlEnabled": Using RTL attributes without enabling RTL support
"RtlSymmetry": Padding and margin symmetry
"RtlHardcoded": Using left/right instead of start/end attributes
"ScrollViewSize": ScrollView size validation
"SdCardPath": Hardcoded reference to /sdcard
"SecureRandom": Using a fixed seed with SecureRandom
"TrulyRandom": Weak RNG
"ExportedContentProvider": Content provider does not require permission
"ExportedReceiver": Receiver does not require permission
"ExportedService": Exported service does not require permission
"GrantAllUris": Content provider shares everything
"WorldReadableFiles": openFileOutput() call passing MODE_WORLD_READABLE
"WorldWriteableFiles": openFileOutput() call passing MODE_WORLD_WRITEABLE
"ServiceCast": Wrong system service casts
"SetJavaScriptEnabled": Using setJavaScriptEnabled
"CommitPrefEdits": Missing commit() on SharedPreference editor
"SignatureOrSystemPermissions": signatureOrSystem permissions declared
"SQLiteString": Using STRING instead of TEXT
"StateListReachable": Unreachable state in a <selector>
"StringFormatCount": Formatting argument types incomplete or inconsistent
"StringFormatMatches": String.format string doesn't match the XML format
string
"StringFormatInvalid": Invalid format string
"PluralsCandidate": Potential Plurals
"UseCheckPermission": Using the result of check permission calls
"CheckResult": Ignoring results
"ResourceAsColor": Should pass resolved color instead of resource id
"MissingPermission": Missing Permissions
"Range": Outside Range
"ResourceType": Wrong Resource Type
"WrongThread": Wrong Thread
"WrongConstant": Incorrect constant
"ProtectedPermissions": Using system app permission
"TextFields": Missing inputType or hint
"TextViewEdits": TextView should probably be an EditText instead
"SelectableText": Dynamic text should probably be selectable
"MenuTitle": Missing menu title
"ShowToast": Toast created but not shown
"TooDeepLayout": Layout hierarchy is too deep
"TooManyViews": Layout has too many views
"ExtraTranslation": Extra translation
"MissingTranslation": Incomplete translation
"Typos": Spelling error
"TypographyDashes": Hyphen can be replaced with dash
"TypographyEllipsis": Ellipsis string can be replaced with ellipsis character
"TypographyFractions": Fraction string can be replaced with fraction
character
"TypographyOther": Other typographical problems
"TypographyQuotes": Straight quotes can be replaced with curvy quotes
"UnusedResources": Unused resources
"UnusedIds": Unused id
"UseCompoundDrawables": Node can be replaced by a TextView with compound
drawables
"UselessLeaf": Useless leaf layout
"UselessParent": Useless parent layout
"EnforceUTF8": Encoding used in resource files is not UTF-8
"ViewConstructor": Missing View constructors for XML inflation
"ViewHolder": View Holder Candidates
"ViewTag": Tagged object leaks
"WrongViewCast": Mismatched view type
"Wakelock": Incorrect WakeLock usage
"WebViewLayout": WebViews in wrap_content parents
"WrongCall": Using wrong draw/layout method
"WrongCase": Wrong case for view tag
"InvalidId": Invalid ID declaration
"NotSibling": RelativeLayout Invalid Constraints
"UnknownId": Reference to an unknown id
"UnknownIdInLayout": Reference to an id that is not in the current layout
"SuspiciousImport": 'import android.R' statement
"WrongFolder": Resource file in the wrong res folder

参考资料与扩展阅读