Gradle构建安装包

Posted by phantomVK on May 10, 2017

一、默认配置

androiddefaultConfig 包含应用默认包名、最低SDK版本、目标SDK版本、应用版本序号和应用版本代号。如果其他地方配置相同参数,新的参数会覆盖默认参数。

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.phantomvk.app"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

defaultConfig定义的变量在所有子域中生效

defaultConfig {
    applicationId "com.phantomvk.app"
    minSdkVersion 16
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    buildConfigField "String", "BUG_REPORT_URL", "\"https://phantomvk.com\api\report\""
}

二、构建类型

buildType构建类型默认为debug模式,同时支持release模式。

由于debug模式很少甚至不需要配置参数,所以debug配置项默认不显示出来的,需要定制的话可以手动添加。release可以开启代码混淆和资源压缩的支持,需要在proguard-android.txt里编写规则。

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

三、渠道配置

3.1 设置不同渠道

productFlavors里可自定义应用不同版本,如免费版和收费版,内部版和公开版。不同版本可以利用静态变量使用各自定义值。

buildConfigField定义值经过编译后可以在Java代码中访问BuildConfig类获得。

下面的示例为内部版和外部版buildConfigField分别设置相同URL字符串类型变量名,但对应不同变量值。

productFlavors {
    internal {
        buildConfigField "String", "TYPE", "\"Internal\""
        buildConfigField "String", "URL", "\"https://api.phantomvk.com\int\""
    }
    external {
        buildConfigField "String", "TYPE", "\"External\""
        buildConfigField "String", "URL", "\"https://api.phantomvk.com\ext\""
    }
}

3.2 渠道包名

如果在渠道中定义了applicationId,这个值会覆盖defaultConfig里面的applicationId,同时修改androidManifestpackage的项目包名。大家可以自己尝试定义一下,然后解apk看androidManifest的内容。

一个名为internal的渠道版本

internal {
    applicationId "com.phantomvk.app.internal"
    buildConfigField "String", "TYPE", "\"Internal\""
    buildConfigField "String", "URL", "\"https://phantomvk.com\api\int\""
}

同理external

external {
    applicationId "com.phantomvk.app.external"
    buildConfigField "String", "TYPE", "\"External\""
    buildConfigField "String", "URL", "\"https://phantomvk.com\api\ext\""
}

如果不想在applicationId重复长字符串,可以使用applicationIdSuffix实现。构建时会把拼接defaultConfig.applicationIdapplicationIdSuffix的结果作为新包名,取代默认的defaultConfig.applicationId

如下applicationIdSuffix的结果是com.phantomvk.app.internal

internal {
    applicationIdSuffix ".internal"
    buildConfigField "String", "TYPE", "\"Internal\""
    buildConfigField "String", "URL", "\"https://phantomvk.com\api\int\""
}

applicationIdapplicationIdSuffix各有优缺点:

  • applicationIdSuffix长度短,易于阅读,作为后缀添加;
  • applicationId可指定其他和本工程完全不同的包名,如com.pvk.app.internal;但管理多个互相没有任何关联的包名容易引起混乱

下面使用applicationId定义一个新的渠道包名

internal {
    applicationId "com.pvk.app.internal"
    buildConfigField "String", "TYPE", "\"Internal\""
    buildConfigField "String", "URL", "\"https://phantomvk.com\api\int\""
}

3.3 资源配置

resValue可以在res/values的子文件中访问。

如自定义不同渠道的App名:先删除res/values/string.xmlapp_name,然后定义一个名为app_nameresValue变量。

internal {
    applicationId "com.pvk.app.internal"
    resValue "string", "AppName", "PhantomvkInt"
    manifestPlaceholders = [app_key: "phantomvk"]
    manifestPlaceholders = [app_secret: "jnce3r93n"]
    buildConfigField "String", "TYPE", "\"Internal\""
    buildConfigField "String", "URL", "\"https://phantomvk.com\api\int\""
}

manifestPlaceholders定义的键值对可以在AndroidManifest.xml里读取

<meta-data android:name="app_key" android:value="${app_key}" />
<meta-data android:name="app_secret" android:value="${app_secret}" />

3.4 安装包重命名

Android Studio默认App安装包名字如下,相比网上发布的应用,缺少应用名、应用版本号等字段。

app-external-debug.apk
app-external-release-unsigned.apk
app-internal-debug.apk
app-internal-release-unsigned.apk

打包过程中可以执行命名规则,对每个成功编译的安装包重命名为合适的名字

applicationVariants.all { variant ->
    variant.outputs.each { output ->
        def apkName = "Phantom_" + "v${defaultConfig.versionName}_" + variant.productFlavors[0].name
        if (variant.buildType.name == ('release')) {
            apkName += '.apk'
        } else if (variant.buildType.name == ('debug')) {
            apkName += '_debug.apk'
        }
        output.outputFile = new File(output.outputFile.parent, apkName)
    }
}

上面的规则产生以下的安装包名。相比默认名字,易读且信息量更多。

Phantom_v1.0_external_debug.apk
Phantom_v1.0_external.apk
Phantom_v1.0_internal_debug.apk
Phantom_v1.0_internal.apk

四、全局配置

4.1 项目全局变量

为了统一项目所有的版本代号,先在Projectbuild.gradle中设置全局变量

ext {
    versionCodeProp = 1
    versionNameProp = "1.0"
}

在各个Module下的build.gradle可以用以下的方式引用

versionCode rootProject.ext.versionCodeProp
versionName rootProject.ext.versionNameProp

4.2 分包名安装报错

我曾经遇到在Gradle中渠道正确分包名,但在同一台手机里安装不同渠道应用会失败的问题。现象是既不能覆盖前一个渠道应用,又不能安装为一个新的应用,仅报错:无法完成安装

这个问题的原因是AndroidManifestContentProvider声明组件时使用了相同授权名。因此多个ContentProvider是不能同时安装的,不然手机会不知道应该提供哪个ContentProvider

<provider
    android:name=".data.ContentProvider"
    android:authorities="com.phantomvk.app.provider"
    android:exported="false" />

我定义的是applicationId绝对包名,所以用如下方法解决问题

<provider
    android:name=".data.ContentProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false" />

五、编译检查

5.1 添加lintOptions

命令行执行gradle编译会执行lint检查,并在遇到error时强制退出。不幸的是,这个错误可能是第三方jar包或aar导致的,我们无法改正。

如果这些error不会导致应用运行时奔溃或抛异常,用abortOnError false忽略所有error继续编译。不放心的可以用 Analyze > Inspect Code 检查应用中存在的warningerror,修正的错误。

lintOptions {
    disable 'InvalidPackage'
    abortOnError false
}

5.2 packagingOptions

多个包中存在相同的文件,在编译合并也可能会报错。因不关系到运行代码,直接排除在打包外即可。

packagingOptions {
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/NOTICE.txt'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/LICENSE.txt'
}

六、构建命令

已经构建好Gradle的环境可使用gradle命令完成构建工作。如果没有搭建环境,也可以用项目目录下的gradlewgradlew.bat临时构建。

# 构建所有版本
> gradle build

# 按照debug或release构建
> gradle assembleDebug
> gradle assembleRelease

# 按照自定义渠道构建
> gradle assembleInternal
> gradle assembleExternal

# 构建指定渠道debug版本
> gradle assembleInternalDebug
> gradle assembleInternalRelease