开发app,除了通过单元测试确保app满足功能需求,通过lint检查确保代码没有框架上的问题也很重要。使用lint工具,可以找到框架上设计较差的代码,如影响稳定性和效率的代码、使app难以维护的代码。
例如,如果XML资源文件中包含了不使用的命名空间,这会占用空间和增加不必要的处理过程。其他一些框架上的问题,如使用弃用了的元素、API调用了目标版本不支持的API。Lint可以帮助我们查找到这些问题。
为了更好的提高lint分析过程(代码审查过程)的性能,可以在代码中使用标记。
使用代码审查工具,如Lint,可以帮助我们找到问题和改善代码质量。但是代码审查工具也仅能做这些事情。例如,Android资源IDs使用一个int(整型)标识字符串、图表、颜色和其他资源类型,因此代码审查功能不能检测出该使用颜色的地方使用了字符串的问题。这种情况,即使使用了代码审查工具,app也会运行不正确或者无法运行。
代码标记给我们提供了在代码中添加提示,代码审查工具,如Lint就可以检测出细节上的代码问题。标记作为诠释数据标记,可以被添加到变量、参数、返回值。当使用代码审查工具时,标记可以帮助工具检测到问题,如空指针表达式和资源冲突。
Android使用Annotations Support Library支持很多标记。我们可以通过android.support.annotation包,使用包中定义的标记。部分标记如下
注意: Android9.0(SDK 28)及之后版本,不再使用android.support.*,而是使用AndroidX代替。
在工程中使用标记,需要在工程中添加对support-annotations
的依赖。添加的任何标记,在代码审查中,如Lint,都会被检查。
1、确认使用的库仓库中有需要的标记库。一般Google’s Maven仓库会有。
2、在build.gradle中添加如下代码
dependencies {
implementation 'com.android.support:support-annotations:28.0.0'
}
3、sync代码
如果在我们自己的库模块中使用标记功能,标记会被按照XML格式作为库文件的一部分打包在annotations.zip
中。添加support-annotations
库的依赖,也不会为下游开发者引入对我们库的依赖(Adding the support-annotations dependency does not introduce a dependency for any downstream users of your library。不是很明白)。
注意: 如果我们使用了appcompat库,不再需要添加support-annotations
库的依赖。因为appcompat中已经存在对标记库的依赖,我们可以直接使用标记功能。
添加@Nulllable、@NonNull
标记,来检查变量、参数、返回值为Null的情况。@Nulllable
标记表明变量、参数返回值可以为Null;@NonNull
标记表明变量、参数、返回值不可以为Null。
例如,一个变量存在为null的值,作为参数传递给一个标记为@NonNull的方法,编译时就会产生一个警告,说明参在一个non-null冲突;在使用一个被标记为@Nullable的方法返回值,如果没有首先进行是否为null的检查,会产生一个空值的警告。应该在明确需要做返回值null检查的被调用的函数做@Nullable标记。
下面的示例是对参数context、attrs做@NonNull标记,要传入的参数的值不能为null。同时也对onCreateView()方法返回值检查不能为null。
import android.support.annotation.NonNull;
...
/** Add support for inflating the tag. **/
@NonNull
@Override
public View onCreateView(String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
}
...
Android Studio支持运行为空性分析,自动推导和在代码中插入空标记。为空性分析扫描所有方法层级,来检查:
有效的资源被作为一个整形数据传递使用,如字符串。例如,为避免需要字符串ID的调用使用了其他资源ID,可以使用如下标记
public abstract void setTitle(@StringRes int resId)
在代码审查过程,会产生一个警告当传递的不是一个R.string的ID。
其他的资源标记,如@DrawableRes, @DimenRes, @ColorRes
, 和@InterpolatorRes
,在代码中可以被添加使用。如果一个参数支持多个资源类型,参数可以同时有多个类型的资源标记。使用@AnyRes
标记的参数,可以是任何类型的R资源。
尽管可以使用@ColorRes
来标记指定一个参数应该是表示颜色的资源,当颜色资源直接使用颜色值时,不会被认为是一个颜色资源;代替的,使用@ColorInt
标记一个参数必须是一个颜色整形值。
线程标记检查用于一个指定类型线程调用一个方法。线程标记支持以下类型:
注意: 编译工具认为@MainThread和@UiThread之间是可交互的,因此,在@MainThread可以调用@UiThread方法,反之亦然。然而,在一个系统app中存在多个视图使用不同的线程情况下,一个UI线程和主线程是不同的。所以,我们应该按照系统app的视图层级标记@UiThread,和按照app生命周期标记@MainThread。
如果一个类中的所有方法都有一样的线程标记,那么可以单独给类增加相同的线程标记,类的方法不用再添加标记,此时方法都有和类相同的线程标记。
一个通常使用线程标记的是AsyncTask类,用来标明函数重写的有效性,AsyncTask后台处理事务,返回处理结果使用UI线程。
public void setAlpha(@IntRange(from=0,to=255) int alpha) { ... }
数量极值标记
示例
void getLocation(View button, @Size(min=1) int[] location) {
button.getLocationOnScreen(location);
}
使用@RequiresPermission
标记。
需要一个权限
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;
同时需要多个权限
@RequiresPermission(allOf = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE})
public static final void copyFile(String dest, String source) {
//...
}
一个字段被使用需要权限
@RequiresPermission(android.Manifest.permission.BLUETOOTH)
public static final String ACTION_REQUEST_DISCOVERABLE =
"android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
分别需要权限,即拥有声明的权限中的一个权限,也可以使用
RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");
间接权限标记
当一个权限依赖一个传递给函数参数的特定值,使用@RequiresPermission
标记函数参数,不需要列出需要的特定权限。例如startActivity(Intent)方法根据Intent使用一个间接权限:
public abstract void startActivity(@RequiresPermission Intent intent, @Nullable Bundle)
当使用间接权限标记,编译工具会分析传入的变量是否包含有权限标记的数据。如果存在有权限标记的数据,会强制要求具有标记的权限。比如,startActivity(Intent),当Intent没有合适的权限传递到函数,会有一个错误警告,如下
编译工具根据使用的Intent标记产生警告
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
@RequiresPermission(Manifest.permission.CALL_PHONE)
public static final String ACTION_CALL = "android.intent.action.CALL";
当标记多个权限,可以分别有权限时,不能使用@RequiresPermission
标记为间接权限标记。
使用@CheckResult
标记标识一个方法的结果或返回值是有效可用的。除了使用@CheckResult标记每个非void返回函数,也可以澄清一些潜在的混乱不清。例如,一个java新开发者常常错误的认为
例如,标记checkPermissions() 的返回值可用,同时建议开发者使用enforcePermission()
@CheckResult(suggest="#enforcePermission(String,int,int,String)")
public abstract int checkPermission(@NonNull String permission, int pid, int uid);
使用@CallSuper,说明重写函数需要调用父类的接口实现。例如,对onCreate()做标记,确保重写的函数调用super.onCreate()
@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}
使用@IntDef和@StringDef标记,我们可以使用整形和字符串集创建枚举标记集。自定义标记类型可以确保一个特别参数、返回值或字段一组常量中的值。也可以使代码按照定义的常量自动补全。
自定义标记使用**@interface**生命一个枚举标记类型。使用@IntDef和@StringDef,结合@Retention,给一个新的标记做注解,当定义了新的可标记类型,这种做法很有必要。@Retention(RetentionPolicy.SOURCE)
标记告诉编译器不要在.class文件中存储枚举的标记。
举例说明,创建一个标记和确保传递给方法的参数是定义的一组常量中的值的步骤:
import android.support.annotation.IntDef;
//...
public abstract class ActionBar {
//...
// Define the list of accepted constants and declare the NavigationMode annotation
@Retention(RetentionPolicy.SOURCE)
@IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
public @interface NavigationMode {}
// Declare the constants
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
// Decorate the target methods with the annotation
@NavigationMode
public abstract int getNavigationMode();
// Attach the annotation
public abstract void setNavigationMode(@NavigationMode int mode);
}
当编译这部分代码,如果mode参数没有使用(NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, 或NAVIGATION_MODE_TABS)中的一个,会产生一个警告。
我们也可以结合@IntDef和@IntRange来标明一个整数可以是给出的一组常量的值或者是一个范围中的值。
DisplayOptions
标记。import android.support.annotation.IntDef;
...
@IntDef(flag=true, value={
DISPLAY_USE_LOGO,
DISPLAY_SHOW_HOME,
DISPLAY_HOME_AS_UP,
DISPLAY_SHOW_TITLE,
DISPLAY_SHOW_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}
...
当编译时使用一个flag,如果被标记的参数和返回值不符合定义的类型,会产生一个警告。
使用@Keep
标记,确保被标记的类、方法在编译时代码最小化不被移除。该标记特别是在使用反射调用类或方法时使用,阻止编译器认为是无用的代码。
注意: 被标记@Keep的类和方法会被编译到APK中,即使这部分代码在apk中不被使用。为了app大小最小化,确认每一个@Keep标记是否有必要。如果使用了反射调用类或者方法,在混淆规则文件中使用-if条件(具体参考混淆手册)指明反射的调用。
使用以下标记指明指定部分代码(如方法、字段、类、包)的可见性。
使用@VisibleForTesting
标记,表明被标记的方法超出一般可见,可以被调用测试。这个标记有一个选项otherwise
变量,如果不仅仅被用来测试,被用来设置方法有什么样的可见性。Lint使用otherwise变量确认将需要的可见性。
示例如下,myMethod()是私有不可见的,但是对于test是包私有,同包可见。结合VisibleForTesting.PRIVATE
设置,如果这个方法被允许调用private的范围之外的调用(如不同的编译单元),会产生一个信息。
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
void myMethod() { ... }
也可以设置为@VisibleForTesting(otherwise = VisibleForTesting.NONE),来表明一个函数的存在仅用来测试,这个和使用@RestrictTo(TESTS)有同样的效果。
@RestrictTo标记表明访问API使用以下的权限
子类
使用标记@RestrictTo(RestrictTo.Scope.SUBCLASSES)
约束API只能被子类调用。
只有继承标记了的类的子类,才能访问该类的API。Java的protected约束不够,因为同一个包的不相关类也可以访问。由于一些原因,为以后的灵活性,遗留一个public的方法,因为一个protected的方法不可能被重写后为public,同时还可以提供一个提示当使用该接口时。
库
使用@RestrictTo(RestrictTo.Scope.GROUP_ID)
标记来约束仅自己的库可以调用
仅自己的库可以访问标记的API。如此,不仅要求组织代码在想要的包内,而且在多个相关库中共享代码。这个选项在一些库中看到,这些库包含了很多不被外部访问的代码,在支持的库之间只能通过public来来访问。
注意: 现在的Android support libary类和包被标记 @RestrictTo(GROUP_ID),这意味着如果使用这些实现的类,lint会警告禁止使用。
testing
使用@RestrictTo(RestrictTo.Scope.TESTS)
来阻止其他开发者访问测试API。
仅测试用代码可以访问被标记的API。
Android Studio提供了代码审查工具Lint,帮助识别和纠正代码中结构上的质量问题,而不需要运行app或写测试用例。每个发现的问题都有描述信息和严重性等级,我们可以快速按照严重性等级排序,根据优先级解决问题。也可以降低问题严重性级别,忽略问题不予解决;也可以提高特定问题的严重性等级。
Lint可以检测出工程代码中潜在的问题,从而优化提高代码的正确性 、安全性、性能、可用性、易用性、国际化。当使用AndroidStudio,可以配置lint和编译时运行lint。lint可以手动使用AndroidStudio运行,也可以通过命令行运行。
下图显示lint工具怎样改善应用代码
Application source files
组成Android工程的源文件,包括java、Kotlin、XML文件、图标文件(icons)、混淆配置文件
lint.xml文件
配置文件,配置lint要检测和不需要检查的部分,可以自定义问题严重性等级
lint工具
静态代码扫描工具,命令行运行或AndriodStudio手动运行。检查代码中影响app质量和性能的框架性代码。强烈建议在app发行前改正检测出的任何问题。
lint审查代码结果
代码审查完后,会生成一个报告在/app/build/reports目录下。
在AndroidStudio的工程根目录下执行
gradlew lint
可以看到如下输出
> Task :app:lint
Ran lint on variant release: 5 issues found
Ran lint on variant debug: 5 issues found
Wrote HTML report to file:/app/build/reports/lint-results.html
Wrote XML report to file:/app/build/reports/lint-results.xml
当lint完成代码审查,会生成XML和HTML格式的lint报告。
使用浏览器打开html格式的报告,显示如下
工程中有多个编译变体,如果想要只对需要的变体做Lint审查,编译变体名称加上前缀lint编译
gradlew lintDebug
如果不使用AndroidStudio或Gradle,使用SDK管理器安装Android SDK Tools后,可以使用独立的lint工具。独立的lint工具在android_sdk/tools/
目录下。命令使用方式
lint [flags]
例如,可以使用下面的命令审查myproject目录及子目录中的文件。MissingPrefix
告诉lint仅扫描不在Android命名空间XML属性数据。
lint --check MissingPrefix myproject
要看所有的flag,使用
lint --help
以下示例使用lint对工程Earthquake做代码审查
$ lint Earthquake
Scanning Earthquake: ...............................................................................................................................
Scanning Earthquake (Phase 2): .......
AndroidManifest.xml:23: Warning: tag appears after tag [ManifestOrder]
android:minSdkVersion="7" />
^
AndroidManifest.xml:23: Warning: tag should specify a target API level (the highest verified version; when running on later versions, compatibility behaviors may be enabled) with android:targetSdkVersion="?" [UsesMinSdkAttributes]
android:minSdkVersion="7" />
^
res/layout/preferences.xml: Warning: The resource R.layout.preferences appears to be unused [UnusedResources]
res: Warning: Missing density variation folders in res: drawable-xhdpi [IconMissingDensityFolder]
0 errors, 4 warnings
在以上输出中显示了4个警告,没有错误。三个警告(ManifestOrder, UsesMinSdkAttributes, 和UnusedResources)在工程的AndroidManifest.xml中,一个警告(IconMissingDensityFolder)在Preferences.xml布局文件中。
默认情况况下,代码审查,会审查lint支持的所有检查项。我们也可以限制要检查的项和设置不同检查项的严重性等级。例如,我们可以去掉一些和工程代码无关的检查项,可以给一些非关键的检查项配置低的重要性等级。
我们可以配置不同的检测等级:
使用AndroidStudio内置的lint工具,我们查看警告和错误有两种途径:
我们可以用lint.xml文件指定需要检查的条目。如果是手动创建lint.xml,在工程的根目录下创建。
lint.xml文件是由闭合的父标签
"1.0" encoding="UTF-8"?>
我们可以改变一个测试项的严重性等级或者不检查某个检查项,通过修改
小技巧: 如果要获取lint支持的所有检查项和响应的id,运行lint --list命令。
lint.xml的示例文件
"1.0" encoding="UTF-8"?>
"IconMissingDensityFolder" severity="ignore" />
"ObsoleteLayoutParam">
"res/layout/activation.xml" />
"res/layout-xlarge/activation.xml" />
"UselessLeaf">
"res/layout/main.xml" />
"HardcodedText" severity="error" />
我们可以关掉lint对Java、Kotlin、XML资源文件的审查。不同的AndroidStudio版本提供了不同的配置方法,这里不做说明。
Java、Kotlin代码中配置Lint
在代码中,如果一个类或方法不需要lint进行检查,使用@SuppressLint
对类和方法标记。
例如,对onCreate方法不做NewApi检查项的检查,对其他方法继续做NewApi检查
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
以下示例显示对类FeedProvider不做ParserError检查项的检查
@SuppressLint("ParserError")
public class FeedProvider extends ContentProvider {
对一个文件不做任何检查,使用
@SuppressLint("all")
在XML资源文件中配置Lint检查
在XML文件中使用 tools:ignore
属性关闭指定块的lint检查。在lint.xml中增加如下命名空间,lint工具就可以识别该属性
namespace xmlns:tools="http://schemas.android.com/tools"
以下示例显示在一个XML布局文件中对UnusedResources
检查。ignore
属性对于子元素是继承的,如果父元素中发现ignore
属性,子元素也会做ignore
处理。
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="UnusedResources" >
android:text="@string/auto_update_prompt" />
如果要使多个检查项不被检查,使用‘,’隔开多个检查项
tools:ignore="NewApi,StringFormatInvalid"
如果所有的检查项都不对某一个xml节点检查,使用
tools:ignore="all"
Gradle插件可以配置lint选项,在模块的build.gradle中用lintOptions {}
块配置。示例如下
android {
...
lintOptions {
// Turns off checks for the issue IDs you specify.
disable 'TypographyFractions','TypographyQuotes'
// Turns on checks for the issue IDs you specify. These checks are in
// addition to the default lint checks.
enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
// To enable checks for only a subset of issue IDs and ignore all others,
// list the issue IDs with the 'check' property instead. This property overrides
// any issue IDs you enable or disable using the properties above.
check 'NewApi', 'InlinedApi'
// If set to true, turns off analysis progress reporting by lint.
quiet true
// if set to true (default), stops the build if errors are found.
abortOnError false
// if true, only report errors.
ignoreWarnings true
}
}
...
我们可以把工程的当前警告集做一个基线版本文件,再运行lint时就仅报告新的检查项问题。基线版本文件让我们在使用lint,不再不得不回退和定位所有的lint问题。
创建基线版本配置,在项目中的build.gradle中配置如下
android {
lintOptions {
baseline file("lint-baseline.xml")
}
}
当第一次添加这一行配置,lint-baseline.xml文件被创建并建立基线。从这时起,工具读取文件来确定基线。如果要创建新的基线,手动删除基线文件和再次运行lint。
IDE或命令运行lint,输出lint-baseline.xml的位置。
$ ./gradlew lintDebug
...
Wrote XML report to file:///app/lint-baseline.xml
Created baseline file /app/lint-baseline.xml
运行lint,记录当前所有的检查项在lint-baseline.xml
中。当前的检查项集合成为基线,我们可以把该基线版本文件上传到仓库,做版本控制跟踪。
客制化基线版本文件
如果要添加一些检查项到基线版本 ,但并不是所有的,可以在build.gradle中如下配置
android {
lintOptions {
check 'NewApi', 'HandlerLeak'
baseline file("lint-baseline.xml")
}
}
我们建立基线后,可以增加任何新的警告到基线中,如此,lint仅显示新引进的bug。
基线版本警告
当基线版本配置被使用中,我们可以获取被过滤出的警告信息。有这些警告信息的原因是我们使用了基线版本文件,因为我们想要去修改这些检查出来的问题。
这些警告信息不仅准确的告诉错误和警告的数量,也跟踪记录着不再报告的问题。如果已经解决了问题,我们应该重新创建基线版本,如此,当问题再出现不会被漏掉。
注意: 在IDE中以批处理模式运行检查时启用基线,但在编辑文件时在后台运行的编辑器内检查时忽略基线。原因是基线是为统计代码库中存在大量警告,但在编辑代码时希望修复问题。
我们可以手动配置和运行代码审查:Analyze > Inspect Code。
设置审查范围和审查内容
选择要分析审查的文件(审查范围)和要审查的检查项(审查内容)。步骤Analyze > Inspect Code,设置审查范围,设置审查内容,然后确定。
使用自定义审查范围
我们可以使用AndroidStudio提供的自定义审查范围,如下步骤
创建自定义审查范围
不做说明,比较简单
检查和编辑审查内容
不做说明,比较简单