2

博客主页

参考资料
https://developer.android.goo...

Android Gradle 多项目构建

Android 项目一般分为库项目、应用项目、测试项目,对应的插件是com.android.librarycom.android.applicationcom.android.test

应用项目一般只有一个,最终打包成一个APK,库项目可以有多个,可以被应用项目引用。

多项目设置

在Gradle中可以创建多个项目,并且可以通过文件夹管理,最终在settings.gradle里配置就可以。

// 项目结构
MyProject
  + app
  + libraries
     + lib1
     + lib2
settings.gradle

上面项目结构中,一个根项目MyProject,并有一个settings.gradle配置文件,Sub Project有一个应用项目 App,两个库项目 lib1 和 lib2 放在libraries文件夹下。

settings.gradle文件中配置

// void include(String[] projectPaths);
include ':app', ':libraries:lib1', ':libraries:lib1:lib2'

如果项目路径很多,可以下面方式指定配置

include ':example1'
project(":example1").projectDir = new File(rootDir, 'chapter/example1')

库项目引用和配置

库项目引用通过dependencies实现。Android Lib打包生成的是aar包,Java Lib打包生成的是jar包,aar包可以有res资源。

dependencies {
  implementation project(':libraries:lib1')
}

引用Android库项目,其实就是引用库项目发布的aar包。 默认Android库项目发布都是release版本,可以配置修改默认发布

android {
    // 配置 发布debug版本的aar包
    defaultPublishConfig "debug"

   // 如果配置多个flavor,可以配置flavor + buildtype
   // defaultPublishConfig "flavorDebug"
}

发布多个版本aar,默认情况下,是不能同时发布多个arr包,但是可以开启

android {
    // 告诉Android Gradle插件,可同时发布不同的aar包
    publishNonDefault true
}

其它项目就可以引用不同的aar

dependencies {
  flavor1Implementation project(path: ':lib1', configuration: 'flavor1Release')
  flavor2Implementation project(path: ':lib1', configuration: 'flavor2Release')
}

发布aar包到Maven中心库

  1. build.gradle文件中应用Maven插件
apply plugin: 'com.android.library'
// 应用Maven仓库
apply plugin: 'maven'
  1. 配置Maven构建三要素,分别是group:artifact:version
// build.gradle
apply plugin: 'com.android.library'
// 应用Maven仓库
apply plugin: 'maven

group = 'com.custom.plugin'
version = '1.0.2'

为了更好的联调测试,提供快照版本SNAPSHOT,如:配置成1.0.0-SNAPSHOT。发布到snapshot中心库时,每次发布版本号不会变化,只会在版本号后按顺序号+1,如:1.0.0-1,1.0.0-2,1.0.0-3等。引用时版本号写成1.0.0-SNAPSHOT即可,Maven会自动下载最新版本快照。

  1. 发布配置,如:发布哪个Maven仓库,使用的用户名和密码,发布什么格式的存档,artifact是什么等
boolean  needUploadToLocal = false;//是否将Library发布到本地
boolean  isArchivesRelease = false;//是否将Library发布到Release仓库;false 为发布到SnapShot仓库
//gradlew :sub-project:newsindiasdk:clean :sub-project:newsindiasdk:uploadArchives
//com.cmcm.onews.sdk:onews_sdk:5.3.1.12-SNAPSHOT@aar


apply plugin: 'maven'

//注意了,以后maven帐户请在local.properties里配置,eg:
//maven.u= your user
//maven.p= your pwd
Properties props = new Properties()
props.load(new FileInputStream(project.rootProject.file("local.properties")))
String u = props.get('maven.u');
String p = props.get('maven.p');


uploadArchives {
    repositories {
        mavenDeployer {
            if (needUploadToLocal) {
                pom.version = "Debug"
                repository(url: "D:/NewsArch")
            } else {
                pom.version = "6.3.1.3"

                if (isArchivesRelease) {
                    repository(url: "http://10.60.80.74:8081/nexus/content/repositories/cleanmasterrelease") {
                        authentication(userName: u, password: p)
                    }
                } else {
                    pom.version += "-SNAPSHOT"  // -SNAPSHOT
                    repository(url: "http://10.60.80.74:8081/nexus/content/repositories/cleanmastersnapshot") {
                        authentication(userName: u, password: p)
                    }
                }
            }
            pom.artifactId = "onews_sdk"
            pom.groupId = "com.cmcm.onews.sdk"
        }
    }
}
  1. 使用它们需要配置仓库,因为是私有仓库,使用时告诉Gradle
// Root Project中build.gradle
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
    }
}

allprojects {
    repositories {
        jcenter()
        // 发布版本
        maven {
            url "http://10.60.80.74:8081/nexus/content/repositories/cleanmasterrelease"
        }
        // 快照版本
        maven {
            url "http://10.60.80.74:8081/nexus/content/repositories/cleanmastersnapshot"
        }
    }
}


// Sub Project中添加依赖
dependencies {
    implementation "com.cmcm.onews.sdk:onews_sdk:6.2.4.7-SNAPSHOT@aar"
}

Android Gradle 多渠道构建

多渠道构建基本原理

在Android Gradle中,有一个Build Variant概念,翻译就是构建变体,构建的产物(APK)。

Build Variant = Project Flavor + Build Type

Build Type 构建类型,如:Release,Debug;Project Flavor 构建渠道,如:Baidu,Google。
Build Variant 构建变体,如:baiduRelease,baiduDebug,googleRelease,googleDebug

Android Gradle提供productFlavors 方法添加不同的渠道,参数接受域对象类型,ProductFlavor作为闭包参数

android {
  productFlavors {
    baidu {}
    google {}
  }
}

配置发布渠道后,Android Gradle就会产生很多Task,基本上都是基于 Project Flavor + Build Type方式生成的,如:assembleBaidu,assembleRelease,assembleBaiduRelease。assemble开头的负责生成构建产物APK。

每个Project Flavor,也就是每个渠道,可以定义自己的SourceSet,Dependencies依赖。

Flurry多渠道 和 友盟多渠道 构建

  1. Flurry多渠道配置

Flurry的统计是已Application划分渠道的,每个Application都有一个key。在Flurry上创建Application时自动生成,可以为每个渠道配置不同的Flurry Key,使用BuildConfig配置。

android {
  productFlavors {
    baidu {
        buildConfigField 'String', 'FLURRY_KEY', "\"QHHJNNGGHJK\""
    }

    google {
        buildConfigField 'String', 'FLURRY_KEY', "\"kkkiihhhgggv\""
    }
  }
}


...
Flurry.init(this, BuildConfig.FLURRY_KEY);
  1. 友盟多渠道配置

友盟存在渠道概念,但它不是在代码中指定的,而是在AndroidManifest.xml文件中配置的,通过配置meta-data标签来设置。

        <meta-data
            android:name="UMENG_CHANNEL"
            android:value="Channel ID" />

Channel ID就是渠道值,如:Baidu,Google。可通过manifestPlaceholders来动态改变渠道值。

多渠道构建定制

通过配置Android Gradle 插件的 ProductFlavor可灵活控制每个渠道包.

  1. applicationId

它是ProductFlavor属性,设置该渠道的包名,想为渠道设置特别的包名,可以使用applicationId这个属性设置

android {
  productFlavors {
    baidu {
        applicationId "com.gradle.test.baidu"
    }
  }
}
  1. consumerProguardFiles

即是一个属性,也有一个同名的方法,只对Android库项目有用。consumerProguardFiles方法是一直添加,不会清空之前的混淆文件,而consumerProguardFiles属性方式每次都是新的混淆文件列表,以前的配置会先被清空。

   // 属性
    public void setConsumerProguardFiles(@NonNull Iterable<?> proguardFileIterable) {
        getConsumerProguardFiles().clear();
        consumerProguardFiles(Iterables.toArray(proguardFileIterable, Object.class));
    }
   // 方法
   public void consumerProguardFiles(@NonNull Object... proguardFiles) {
        for (Object proguardFile : proguardFiles) {
            consumerProguardFile(proguardFile);
        }
    }

当发布库项目生成AAR时,使用consumerProguardFiles配置的混淆文件也会被打包到AAR里一起发布,当应用项目引用这个AAR时,并启动混淆时,会自动使用AAR包里的混淆文件对AAR包里代码进行混淆,就不用对AAR包进行混淆配置,因为AAR自带了。

android {
  productFlavors {
    baidu {
        consumerProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}
  1. manifestPlaceholders
  2. multiDexEnabled

启动多个dex配置,用来突破65535方法问题。

  1. proguardFiles

配置混淆文件

  1. signingConfig

配置签名

  1. testApplicationId

是一个属性,用来适配测试包的包名,值一般为App的包名+.test

android {
  productFlavors {
    baidu {
        testApplicationId "com.gradle.test"
    }
  }
}
  1. testFunctionalTest 和 testHandleProfiling

testFunctionalTest 表示是否为功能测试,testHandleProfiling表示是否启动分析功能。

android {
  productFlavors {
    baidu {
          testFunctionalTest true
          testHandleProfiling true
    }
  }
}
  1. testInstrumentationRunner

配置运行测试使用的 Instrumentation Runner 的全路径的类名,且必须是android.app.Instrumentation 的子类。

android {
  productFlavors {
    baidu {
          testInstrumentationRunner "android.test.InstrumentationTestRunner"
    }
  }
}
  1. testInstrumentationRunnerArguments

与testInstrumentationRunner一起使用,配置 Instrumentation Runner 使用的参数,最终使用的都是adb shell am instrument 这个命令。testInstrumentationRunnerArguments 参数被转换传递给 am instrument这个命令使用,如:-e key value

android {
  productFlavors {
    baidu {
          testInstrumentationRunnerArguments.put("converage", 'true')
    }
  }
}
  1. versionName 和 versionCode

配置渠道的版本号和版本名称

android {
  productFlavors {
    baidu {
          versionName "2.1.5"
          versionCode 215
    }
  }
}
  1. dimension

为了基于不同标准构建App,可以通过dimension 多维度的方式解决。

dimensionProductFlavor的属性,接受一个字符串,该字符串就是维度名称,作为ProductFlavor的维度。维度名称不是随意指定的,在使用前需要声明,可以通过flavorDimensions 方法声明。

     android {
          // 声明维度后,才能在productFlavors中使用
          // flavorDimensions 可同时指定多个维度,但是维度有顺序优先级的,第一个优先级最大
          flavorDimensions 'api', 'version'
     
          productFlavors {
              demo {
                dimension 'version'
                ...
              }
     
             full {
                dimension 'version'
                 ...
             }
     
             minApi24 {.
               dimension 'api'
               minSdkVersion '24'
               versionNameSuffix "-minApi24"
               ...
             }
     
             minApi21 {
              dimension "api"
              minSdkVersion '21'
              versionNameSuffix "-minApi21"
              ...
            }
         }
      }

上例中,最后生成的variant(构建变体)会被几个 ProductFlavor对象配置:

  1. Android中的defaultConfig配置,也是一个ProductFlavor
  2. api维度的ProductFlavor,被dimension 配置标记为api的ProductFlavor
  3. version维度的ProductFlavor, 被dimension 配置标记为version的ProductFlavor

维度优先级很重要,高优先级的flavor会替换掉低优先级的资源、代码、配置等,上例中优先级:api>version>defaultConfig

通过dimension 指定维度后,Android Gradle会帮助生成相应 Task、SourceSet、Dependencies等。现在构建变体的产物=Api+Version+BuildType, 如:MinApi21DemoRelease、MinApi21FullRelease、MinApi21DemoDebug、MinApi21FullDebug等

提供多渠道构建的效率

生成多个渠道包主要是为了跟踪每个渠道的情况,如:新增、活跃、留存。除了根据渠道号区分每个渠道外,大部分情况下没有什么不同,唯一区别是属于哪个渠道。

因为Android Gradle对每个渠道包都要执行构建过程,导致速度变慢。美团研究一个办法,在APK的MEAT-INF目录下添加空文件不用重新签名原理。

  1. 利用Android Gradle打一个基本包(母包)
  2. 基于母包复制一个,文件名要区分产品,打包时间,版本,渠道
  3. 对复制的APK进行修改,在META-INF目录下新增空文件,文件名必须要区分渠道,如:mtchannel_google
  4. 利用python脚本执行 2, 3 步骤操作

使用时,在APK启动(Application onCreate)读取APK中META-INF目录下的前缀为mtchannel_文件,如果找到,把文件名取出来,然后就可以得到渠道标识(google)了,美团实现的代码:

 public static String getChannel(Context context) {
        ApplicationInfo appinfo = context.getApplicationInfo();
        String sourceDir = appinfo.sourceDir;
        String ret = "";
        ZipFile zipfile = null;
        try {
            zipfile = new ZipFile(sourceDir);
            Enumeration<?> entries = zipfile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = ((ZipEntry) entries.nextElement());
                String entryName = entry.getName();
                if (entryName.startsWith("mtchannel")) {
                    ret = entryName;
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zipfile != null) {
                try {
                    zipfile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        String[] split = ret.split("_");
        if (split != null && split.length >= 2) {
            return ret.substring(split[0].length() + 1);

        } else {
            return "";
        }
    }

利用python脚本批处理,向APK中META-INF目录写入渠道文件,文件名前缀为mtchannel_

import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED) 
empty_channel_file = "META-INF/mtchannel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)

然后就是配置渠道列表,下载AndroidMultiChannelBuildTool工程后,在PythonTool/Info/channel.txt文件中添加渠道,渠道以换行隔开。

将想要批量打包的apk文件拷贝到PythonTool目录下(与MultiChannelBuildTool.py同级),运行py脚本即可打包完成。(生成的渠道apk包在output_** 目录下)

参考资料:
https://www.cnblogs.com/ct201...
https://github.com/GavinCT/An...
https://github.com/Meituan-Di...

Android Gradle 测试

Android为测试程序提供了很好支持,既可以使用传统的JUnit测试,又可以使用Android提供的Instrument测试。

基本概念

使用Android Studio新建一个项目时,会帮助我们默认生成 mainandroidTest SourceSet。运行测试时,androidTest SourceSet会被构建成一个可以安装到设备上测试的APK,这个测试APK中有写好的测试用例,会被执行来测试APP。

在androidTest SourceSet中可以依赖各种测试库,如:单元测试的,集成测试的,espresso UI测试的,uiautomator自动化测试的。
一般测试APK会统一配置,而不是针对每个渠道都配置,在defaultConfig对测试APK配置后,会自动生成所需的包名、AndroidManifest.xml文件等信息。

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        testApplicationId "com.example.myapplication.test"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        testFunctionalTest true
        testHandleProfiling true
    }
}

根据配置自动生成AndroidManifest.xml文件,android:targetPackage是Android自动生成的。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication.test" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="29" />

    <instrumentation
        android:name="androidx.test.runner.AndroidJUnitRunner"
        android:functionalTest="true"
        android:handleProfiling="true"
        android:label="Tests for com.example.myapplication"
        android:targetPackage="com.example.myapplication" />

    <application android:debuggable="true" >
        <uses-library android:name="android.test.runner" />
    </application>

</manifest>

也可以在androidTest中配置依赖,正式APK不会编译到APK中,只有Android测试的时候才会被编译到测试APK中。

dependencies {
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

默认情况下测试APK主要在debug模式下,debug模式下不会混淆代码,有利于发现问题,且对测试代码覆盖率也有帮助。Android Gradle提供了testBuildType,可以修改BuildType

android {
   // 修改测试的是release类型的apk,默认类型是debug
   testBuildType 'release'
}

怎么运行写好的测试代码呢?
使用 gradlew connectedCheck 任务来运行测试。这个任务是一个引导性的任务,首先使用assembleAndroidTestassembleDebug 任务构建测试应用 和 被测试应用,然后通过*install任务安装这两个应用,再运行写好的测试代码,运行完后,卸载这两个应用。

最后测试结果会保存在build/reports/androidTests/connected目录下,可以通过浏览器查看index.html测试结果。

本地单元测试

这种测试和原生的java测试一样,不依赖android框架或只有非常少的依赖,直接运行在本地开发机器上,不需要运行在Android设备上。但有时也需要Android框架本身一些代码依赖,如:Context,可以使用模拟框架来模拟这种依赖关系,如:Mockito 和 JMock

AndroidTest测试有自己的SourceSet目录 src/androidTest/java; 对于本地单元测试也有自己的目录src/test/java,测试用例用来测试main这个SourceSet代码。

Android本地单元测试,也使用JUnit这个流行的测试框架测试

dependencies {
    // JUnit3的测试用例需要集成junit.framework.TestCase,且测试方法要以test为前缀
    // JUnit4只需要使用 @Test 注解标记就可以,推荐使用JUnit4
    testImplementation 'junit:junit:4.12'

编写好测试用例后, 运行 gradlew test 任务可以运行所有的单元测试用例,然后在build/reports/tests目录下生成测试报告。

如果想运行debug模式下的使用gradlew testDebugUnitTest任务。

在执行test任务时,如果想依赖Android框架,只能使用模拟对象框架,如:Mockito ,版本要是1.9.5以上,与Android单元测试兼容。

dependencies {
    testImplementation 'junit:junit:4.12'
    testImplementation 'org.mockito:mockito-all:1.10.19'
}

编写需要测试的代码,需要使用Context

public class Utils {

    private Context mContext;
    public Utils(Context context) {
        this.mContext = context;
    }


    public String getAppName(){
        return mContext.getString(R.string.app_name);
    }

}

如果要测试上面的代码,因为需要一个Context,就要使用Mockito来模拟Context

import android.content.Context;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

/**
 * kerwin <xujinbing839@pingan.com.cn> 2019-09-11
 */
@RunWith(MockitoJUnitRunner.class)
public class UtilsTest {

    private static final String APP_NAME = "MyApplication";

    @Mock
    Context mMockContext;

    @Test
    public void readAppNameFromContext() {
        when(mMockContext.getString(R.string.app_name)).thenReturn(APP_NAME);

        Utils utils = new Utils(mMockContext);
        String appName = utils.getAppName();

        assertThat(appName, is(APP_NAME));
    }
}

首先要告诉JUnit4,要使用MockitoJUnitRunner这个单元测试的运行者来执行,不然 @Mock 注解就不认识了。使用@Mock 注解模拟一个Context对象,mMockContext就是被Mockito模拟出来的。

when逻辑需要和Utils里的getAppName方法逻辑一样,然后使用thenReturn告诉模拟期望返回的值

使用 gradlew test 执行任务,查看报告结果。

参考文献
https://static.javadoc.io/org...

Instrument测试

Instrument测试是基于Android设备或模拟器的测试,是一种高模拟和仿真测试。它可以使用Android SDK框架的所有类和特性,如:Context。还提供了Instrumenttation类,可以很方便的获得测试APK的Context、Activity。且可以使用Instrument测试做单元测试、UI自动化测试、集成测试。

Instrument测试要生成一个测试的APK,所以要对测试APK配置。testInstrumentationRunner 这个runner可以编写基于JUnit4测试用例,且可搭配使用JUnit4新特性。

android {
    defaultConfig {
        // 指定生成测试APK的包名,默认:被测试APK包名+test
        testApplicationId "com.example.myapplication.test"
        // 配置使用Runner
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

// 添加依赖
dependencies {
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test:rules:1.2.0'
    // Optional
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    // Optional
    androidTestImplementation 'org.hamcrest:hamcrest-library:2.1'
    // Optional
    androidTestImplementation 'androidx.test.uiautomator:uiautomator-v18:2.2.0-alpha1'
}

rules库,为测试定义一些规则,实现自JUnit的rule,可以对JUnit扩展。如:ActivityTestRule指定要测试的Activity。编写好测试用例后,运行gradlew connectedAndroidTest 执行所有Instrument测试,在build/reports/androidTests目录下查看报告.

import android.util.Log;

import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
// LargeTest标记,说明有更高的权限,如多线程、访问数据库、时间限制也更长
@LargeTest
public class ExampleInstrumentedTest {
  
    // 指定规则,测试MainActivity
    @Rule
    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);

    @Before
    public void init() {
        Log.d("kerwin_test", "init :::" + Thread.currentThread().getName());
        //  init :::Instr: androidx.test.runner.AndroidJUnitRunner
    }

    @Test
    public void  valid() throws Throwable {
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mActivityRule.getActivity().findViewById(R.id.text1).performClick();
            }
        });
    }
}

测试选项配置

Android Gradle插件提供testOptions { } ,可对测试进行配置,如:生成测试报告的目录。

TestOptions提供配置项:

  1. resultsDir 是一个属性,配置生成测试结果目录
  2. reportDir 是一个属性,配置生成测试报告目录
  3. unitTests 即是属性,也是一个闭包,控制单元测试的执行
android {
    testOptions {
        resultsDir "${project.buildDir}/myResults"
        reportDir "${project.buildDir}/myReports"
    }
}

单个项目,测试报告可以生成在指定的目录下,有多个项目怎么办呢?

比如引用了多个库项目,每个库项目也有自己的测试,生成自己的报告,这样比较分散,不容易查看,如果统一起来查看就方便了。Android 提供了另一个插件 android-reporting ,应用后新增一个名为 mergeAndroidReports 任务,执行完测试后调用即可。

// 在Root Project中的build.gradle 文件最后应用后,添加的任务也在Root项目中。
apply plugin: 'android-reporting'

然后执行 gradlew deviceCheck mergeAndroidReports --continue 任务。mergeAndroidReports合并报告,--continue在测试失败的时候,也继续执行其他测试用例,一直执行完成为止。合并后的报告在Root项目的build目录中。

unitTests配置,对应的类型是UnitTestOptions,它是所有测试任务的一个集合。UnitTestOptions 对象有一个Test类型的域对象集合DomainObjectSet。对应源码:

public static class UnitTestOptions {
     private DomainObjectSet<Test> testTasks = new DefaultDomainObjectSet<Test>(Test.class);

     public void all(final Closure<Test> configClosure) {
            testTasks.all(
                    new Action<Test>() {
                        @Override
                        public void execute(Test testTask) {
                            ConfigureUtil.configure(configClosure, testTask);
                        }
                    });
     }
}

all方法可以遍所有的Test,它是Task类型。可以对他们做一些配置,或者根据任务做一些判断等。

android {
     testOptions {
        unitTests.all {
           println "testName: >>>>>>${it.name}"
        }
    }
}

代码覆盖率

有了测试用例,就要有相应的测试代码覆盖率统计,这样才能知道代码是否被测试用例完全覆盖,还有哪些没有覆盖到,如何进行补全测试用例。Android Gradle内置了代码覆盖lv的报告生成,默认是关闭的。

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }

        debug {
            testCoverageEnabled true
        }
    }
}

testCoverageEnabled用于控制代码覆盖率统计是否开启,它是BuildType的一个属性,true代表开启,默认是false。

运行 gradlew createDebugCoverageReport 任务后,就会自动执行测试用例,并生成测试代码覆盖率报告,报告路径build/reports/coverage

Lint支持

Android提供了针对代码、资源优化工具Lint。可帮助检查哪些资源没有被使用,哪些使用了新的API,哪些资源没有国际化等,并生成一份报告,告诉哪些需要优化。

运行 gradle lint 任务即可生成报告,默认生成报告在build/reports/lint-results.html

Lint是一个命令行工具,在Android Tools目录下。在Android Gradle插件提供了 lintOptions {}这个闭包配置Lint。

android {
    lintOptions {
        // 遇到错误终止构建
        abortOnError true
        // 警告也会被当前错误处理
        warningsAsErrors true
        // 需要检查是否使用了新的API
        check 'NewApi'
    }
}

1. abortOnError

是一个属性,接受boolean类型值,配置Lint发现错误时是否退出Gradle构建。默认true

2. absolutePaths

是一个属性,接受boolean类型值,配置错误的输出里是否应该显示绝对路径,默认true,显示相对路径

3. check

是一个属性,也是一个方法,配置哪些项目需要Lint检查,这个项目就是Issue Id(s)

NewApi这个就是一个issue id,lint有很多可用的issue id,通过lint --list可以查看可用的id。冒号前面是id,后面是对这个issue id的说明。可以使用lint --show命令查看详细说明.

Valid issue categories:
    Correctness
    Correctness:Messages
    Correctness:Chrome OS
    Security
    Performance
    Usability:Typography
    Usability:Icons
    Usability
    Accessibility
    Internationalization
    Internationalization:Bidirectional Text

Valid issue id's:
"ContentDescription": Image without contentDescription
"AddJavascriptInterface": addJavascriptInterface Called
"ShortAlarm": Short or Frequent Alarm
"AllCaps": Combining textAllCaps and markup
"AllowAllHostnameVerifier": Insecure HostnameVerifier
"AlwaysShowAction": Usage of showAsAction=always
"InvalidUsesTagAttribute": Invalid name attribute for uses element.
"MissingIntentFilterForMediaSearch": Missing intent-filter with action
      android.media.action.MEDIA_PLAY_FROM_SEARCH
"MissingMediaBrowserServiceIntentFilter": Missing intent-filter with action
      android.media.browse.MediaBrowserService.
"MissingOnPlayFromSearch": Missing onPlayFromSearch.
"ImpliedTouchscreenHardware": Hardware feature touchscreen not explicitly
      marked as optional
"MissingTvBanner": TV Missing Banner
"MissingLeanbackLauncher": Missing Leanback Launcher Intent Filter.
"MissingLeanbackSupport": Missing Leanback Support.
"PermissionImpliesUnsupportedHardware": Permission Implies Unsupported
      Hardware
"UnsupportedTvHardware": Unsupported TV Hardware Feature
"SupportAnnotationUsage": Incorrect support annotation usage
"ShiftFlags": Dangerous Flag Constant Declaration
"LocalSuppress": @SuppressLint on invalid element
"SwitchIntDef": Missing @IntDef in Switch
"UniqueConstants": Overlapping Enumeration Constants
"InlinedApi": Using inlined constants on older versions
"Override": Method conflicts with new inherited method
"ObsoleteSdkInt": Obsolete SDK_INT Version Check
"NewApi": Calling new methods on older versions
"UnusedAttribute": Attribute unused on older versions
"AppCompatMethod": Using Wrong AppCompat Method
"AppCompatCustomView": Appcompat Custom Widgets
"AppCompatResource": Menu namespace
"GoogleAppIndexingApiWarning": Missing support for Firebase App Indexing Api
"GoogleAppIndexingWarning": Missing support for Firebase App Indexing
"AppLinksAutoVerifyError": App Links Auto Verification Failure
"AppLinksAutoVerifyWarning": Potential App Links Auto Verification Failure
"AppLinkUrlError": URL not supported by app for Firebase App Indexing
"TestAppLink": Unmatched URLs
"InconsistentArrays": Inconsistencies in array element counts
"Assert": Assertions
"BadHostnameVerifier": Insecure HostnameVerifier
"BatteryLife": Battery Life Issues
"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
"PermissionImpliesUnsupportedChromeOsHardware": Permission Implies Unsupported
      Chrome OS Hardware
"UnsupportedChromeOsHardware": Unsupported Chrome OS Hardware Feature
"GetInstance": Cipher.getInstance with ECB
"CommitTransaction": Missing commit() calls
"Recycle": Missing recycle() calls
"CommitPrefEdits": Missing commit() on SharedPreference editor
"ApplySharedPref": Use apply() on SharedPreferences
"ClickableViewAccessibility": Accessibility in Custom Views
"EasterEgg": Code contains easter egg
"StopShip": Code contains STOPSHIP marker
"MissingConstraints": Missing Constraints in ConstraintLayout
"VulnerableCordovaVersion": Vulnerable Cordova Version
"CustomViewStyleable": Mismatched Styleable/Custom View Name
"CutPasteId": Likely cut & paste mistakes
"SimpleDateFormat": Implied locale in date format
"SetTextI18n": TextView Internationalization
"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
"StringEscaping": Invalid string escapes
"UnpackedNativeCode": Missing android:extractNativeLibs=false
"UnsafeDynamicallyLoadedCode": load used to dynamically load code
"UnsafeNativeCodeLocation": Native code outside library directory
"EllipsizeMaxLines": Combining Ellipsize and Maxlines
"ExifInterface": Using android.media.ExifInterface
"ExtraText": Extraneous text in resource files
"FieldGetter": Using getter instead of field
"InvalidAnalyticsName": Invalid Analytics Name
"MissingFirebaseInstanceTokenRefresh": Missing Firebase Instance ID Token
      Refresh
"FontValidationError": Validation of font files
"FontValidationWarning": Validation of font files
"FullBackupContent": Valid Full Backup Content File
"ValidFragment": Fragment not instantiatable
"GetContentDescriptionOverride": Overriding getContentDescription() on a View
"PackageManagerGetSignatures": Potential Multiple Certificate Exploit
"AccidentalOctal": Accidental Octal
"UseOfBundledGooglePlayServices": Use of bundled version of Google Play
      services
"GradleCompatible": Incompatible Gradle Versions
"GradleDependency": Obsolete Gradle Dependency
"GradleDeprecated": Deprecated Gradle Construct
"DevModeObsolete": Dev Mode Obsolete
"DuplicatePlatformClasses": Duplicate Platform Classes
"GradleGetter": Gradle Implicit Getter Call
"GradlePluginVersion": Incompatible Android Gradle Plugin
"HighAppVersionCode": VersionCode too high
"GradleIdeError": Gradle IDE Support Issues
"GradlePath": Gradle Path Issues
"GradleDynamicVersion": Gradle Dynamic Version
"NotInterpolated": Incorrect Interpolation
"StringShouldBeInt": String should be int
"NewerVersionAvailable": Newer Library Versions Available
"MinSdkTooLow": API Version Too Low
"GridLayout": GridLayout validation
"HandlerLeak": Handler reference leaks
"HardcodedDebugMode": Hardcoded value of android:debuggable in the manifest
"HardcodedText": Hardcoded text
"HardwareIds": Hardware Id Usage
"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
"ConvertToWebp": Convert to WebP
"WebpUnsupported": WebP Unsupported
"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
"InstantApps": Instant App Issues
"DuplicateDivider": Unnecessary Divider Copy
"TrustAllX509TrustManager": Insecure TLS/SSL trust manager
"InvalidImeActionId": Invalid imeActionId declaration
"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
"JobSchedulerService": JobScheduler problems
"KeyboardInaccessibleWidget": Keyboard inaccessible widget
"LabelFor": Missing labelFor attribute
"InconsistentLayout": Inconsistent Layouts
"InflateParams": Layout Inflation without a Parent
"StaticFieldLeak": Static Field Leaks
"DefaultLocale": Implied default locale in case conversion
"LocaleFolder": Wrong locale name
"GetLocales": Locale crash
"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": AllowBackup/FullBackupContent Problems
"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
"WearableBindListener": Usage of Android Wear BIND_LISTENER is deprecated
"WrongManifestParent": Wrong manifest parent
"InvalidPermission": Invalid Permission Attribute
"ManifestResource": Manifest Resource References
"ManifestTypo": Typos in manifest tags
"FloatMath": Using FloatMath instead of Math
"MergeMarker": Code contains merge marker
"MergeRootFrame": FrameLayout can be replaced with <merge> tag
"IncompatibleMediaBrowserServiceCompatVersion": Obsolete version of
      MediaBrowserServiceCompat
"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
"NetworkSecurityConfig": Valid Network Security Config File
"MissingBackupPin": Missing Backup Pin
"PinSetExpiry": Validate <pin-set> expiration attribute
"NfcTechWhitespace": Whitespace in NFC tech lists
"UnlocalizedSms": SMS phone number missing country code
"ObjectAnimatorBinding": Incorrect ObjectAnimator Property
"AnimatorKeep": Missing @Keep for Animated Properties
"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
"PrivateApi": Using Private APIs
"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
"ParcelClassLoader": Default Parcel Class Loader
"PendingBindings": Missing Pending Bindings
"RecyclerView": RecyclerView Problems
"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
"ValidRestrictions": Invalid Restrictions Descriptor
"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
"SetWorldReadable": File.setReadable() used to make file world-readable
"SetWorldWritable": File.setWritable() used to make file world-writable
"GrantAllUris": Content provider shares everything
"WorldReadableFiles": openFileOutput() or similar call passing
      MODE_WORLD_READABLE
"WorldWriteableFiles": openFileOutput() or similar call passing
      MODE_WORLD_WRITEABLE
"ServiceCast": Wrong system service casts
"WifiManagerLeak": WifiManager Leak
"WifiManagerPotentialLeak": WifiManager Potential Leak
"SetJavaScriptEnabled": Using setJavaScriptEnabled
"SignatureOrSystemPermissions": signatureOrSystem permissions declared
"SQLiteString": Using STRING instead of TEXT
"SSLCertificateSocketFactoryCreateSocket": Insecure call to
      SSLCertificateSocketFactory.createSocket()
"SSLCertificateSocketFactoryGetInsecure": Call to
      SSLCertificateSocketFactory.getInsecure()
"StateListReachable": Unreachable state in a <selector>
"AuthLeak": Code might contain an auth leak
"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
"RestrictedApi": Restricted API
"WrongThread": Wrong Thread
"WrongConstant": Incorrect constant
"VisibleForTests": Visible Only For Tests
"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
"UnsafeProtectedBroadcastReceiver": Unsafe Protected BroadcastReceiver
"UnprotectedSMSBroadcastReceiver": Unprotected SMS BroadcastReceiver
"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
"VectorRaster": Vector Image Generation
"VectorDrawableCompat": Using VectorDrawableCompat
"VectorPath": Long vector paths
"InvalidVectorPath": Invalid vector paths
"ViewConstructor": Missing View constructors for XML inflation
"ViewHolder": View Holder Candidates
"ViewTag": Tagged object leaks
"WrongViewCast": Mismatched view type
"FindViewByIdCast": Add Explicit Cast
"Wakelock": Incorrect WakeLock usage
"WakelockTimeout": Using wakeLock without timeout
"InvalidWearFeatureAttribute": Invalid attribute for Wear uses-feature
"WearStandaloneAppFlag": Invalid or missing Wear standalone app flag
"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
"WrongThreadInterprocedural": Wrong Thread (Interprocedural)

4. checkAllWarnings

是一个属性,接受boolean类型值,true表示需要检查所有警告的issue,包括默认被关闭的issue;false不检查

5. checkReleaseBuilds

是一个属性,接受boolean类型值,配置在release构建的过程中,Lint 是否检查致命的错误问题,默认true,一旦发现有fatal级别的问题,release构建就会终止。

6. disable

用来关闭给定issue ids的Lint检查,参数接受是issue id。

7. enable

与disable相反

8. explainIssues

是一个属性,接受boolean类型值,配置Lint检查出的错误报告是否应该包含解释说明,默认开启

9. htmlOutput

是一个属性,接受一个File类型参数,配置HTML报告输出的文件路径

android {
    lintOptions {
        htmlOutput new File("${buildDir}/lintReports/lint-results.html")
    }
}

10. htmlReport

是一个属性,接受boolean类型值,用于配置是否生成HTML报告,默认true

11. ignoreWarnings

是一个属性,接受boolean类型值,用于配置Lint是否忽略警告级别的检查,只检查错误级别的。默认false,不忽略警告级别的检查

12. lintConfig

是一个属性,接受一个File类型参数,用于指定Lint的配置文件,这是一个XML格式的文件,可以指定一些默认的设置。

13. noLines

是一个属性,接受boolean类型值,如果true,error输出将不会包含源代码的行号,默认是false

14. quiet

是一个属性,接受boolean类型值,表示是否开启安静模式,true代表安静模式,Lint分析的进度或者其他信息将不会显示。默认false

15. severityOverrides

是一个只读属性,返回一个Map类型的结果,用来获取issue的优先级。Map的key是issue id, value是优先级,优先级是"fatal"、"error"、"warning"、"informational"、"ignore"

16. showAll

是一个属性,接受boolean类型值,用于标记是否应该显示所有的输出,不会对过长的消息截断等

17. textOutput

是一个只读属性,也有对应同名方法,接受一个File类型参数,用于指定生成的text格式的报告路径。如果指定stdout这个值,会被指向标准的输出,一般是终端控制台

18. textReport

是一个属性,接受boolean类型值,用于配置是否生成text报告,默认false,不生成报告

19. warningsAsErrors

是一个属性,接受boolean类型值,用于配置是否把所有的警告也当做错误处理,默认false。

20. xmlOutput

是一个属性,接受一个File类型参数,用于生成XML报告的路径

21. xmlReport

是一个属性,接受boolean类型值,用于控制是否生成XML格式的报告,默认true

22. error、fatal、ignore、warning、informational

这5个方法用来配置issue的优先级,接受的都是issue id作为参数。

/** A severity for Lint. Corresponds to com.android.tools.lint.detector.api.Severity#FATAL */
    int SEVERITY_FATAL         = 1;
    /** A severity for Lint. Corresponds to com.android.tools.lint.detector.api.Severity#ERROR */
    int SEVERITY_ERROR         = 2;
    /** A severity for Lint. Corresponds to com.android.tools.lint.detector.api.Severity#WARNING */
    int SEVERITY_WARNING       = 3;
    /** A severity for Lint. Corresponds to com.android.tools.lint.detector.api.Severity#INFORMATIONAL */
    int SEVERITY_INFORMATIONAL = 4;
    /** A severity for Lint. Corresponds to com.android.tools.lint.detector.api.Severity#IGNORE */
    int SEVERITY_IGNORE        = 5;
    /**
     * A severity for lint. This severity means that the severity should be whatever the default
     * is for this issue (this is used when the DSL just says "enable", and Gradle doesn't know
     * what the default severity is.)
     */
    int SEVERITY_DEFAULT_ENABLED = 6;

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。