如果只是想看怎么重命名apk,只看前两段就可以了。如果想从源码角度了解一下,那么可以先看下上一篇Android Gradle 学习之一:源码下载。
先来看下在gradle中怎么修改生成的apk的名字,在module的build.gradle文件中写如下代码:
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (output.outputFileName != null && output.outputFileName.endsWith('.apk')) {
def fileName = "CustomGradle-v${versionName}-${variant.buildType.name}.apk"
output.outputFileName = fileName
}
}
}
这段代码会根据variant将apk命名成自己想要名称,因为我没有设置flavor,所以最后全量build生成的apk的名字为:
CustomGradle-v1.0-debug.apk
CustomGradle-v1.0-releas.apk
CustomGradle-v1.0-androidTest.apk
重命名APK的方法相信百度一下很多地方都能够查得到,其实官方也给了例子展示了如何改名。也可能只是测试项目不是例子,因为源码gradle的测试工程里面,文件在:</your/gradle/source>/tools/base/build-system/integration-test/test-projects/renamedApk/build.gradle
官方的写法是这样的:
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
try {
outputFileName = new File(output.outputFile.parent, "foo")
throw new RuntimeException("setting an absolute path to outputFileName not caught")
} catch (GradleException e) {
// expected
}
outputFileName = "${variant.name}.apk"
}
}
其实类似,但是注意下细节就会发现,官方的outputFileName前面没有加output。输出下这个闭包的delegate可以发现,variant.outputs.all方法把闭包的delegate设置成了他的成员,所以output.outputFileName这个调用和去掉output直接写outputFileName这个调用是一样的,都是拿到output的成员变量“outputFileName”。delegate是groovy闭包的一个用法,自行查阅吧
其实我的问题并非解决如何修改输出的apk名,每次遇到类似的需求的时候,百度一下就能找到改名的方法,但是看到这些办法似懂非懂,让自己写还写不出来,如果遇到其他需求感觉就不知道怎样修改gradle了。所以本篇主要是从源码的角度来看下为什么要要在gradle里面加上这些代码能够修改打包之后的apk。
1. InstallDebug
在gradle task工具栏里面能够看到一个installDebug的task,他的任务就是安装编译好的apk。想要安装apk就肯定需要知道文件名和文件路径,那我们先从这个task入手,看看他是怎样拿到要安装的文件的apk的。
源代码位于</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/InstallVariantTask.java
找到带有@TaskAction注解的方法install()就是InstallVariantTask的主要执行体,代码如下:
@TaskAction
public void install() throws DeviceException, ProcessException {
TaskDependency mustRunAfter = getMustRunAfter();
final ILogger iLogger = getILogger();
DeviceProvider deviceProvider = new ConnectedDeviceProvider(adbExe.get(),
getTimeOutInMs(),
iLogger);
deviceProvider.init();
try {
BaseVariantData variantData = getVariantData();
GradleVariantConfiguration variantConfig = variantData.getVariantConfiguration();
List<OutputFile> outputs =
ImmutableList.copyOf(
ExistingBuildElements.from(
InternalArtifactType.APK,
BuildableArtifactUtil.singleFile(apkDirectory)));
System.out.println("apkDirectory = " + apkDirectory);
for (OutputFile opf : outputs) {
System.out.println("INstallVariantTask opf.getOutputFile().getPath() = " + opf.getOutputFile().getPath() + " opf = " + opf);
}
install(
getProjectName(),
variantConfig.getFullName(),
deviceProvider,
variantConfig.getMinSdkVersion(),
getProcessExecutor(),
getSplitSelectExe(),
outputs,
variantConfig.getSupportedAbis(),
getInstallOptions(),
getTimeOutInMs(),
getLogger());
} finally {
deviceProvider.terminate();
}
}
里面还有个install方法,这个就是执行真正的安装命令,里面比较复杂就不细说了。大概就是用DeviceConnector执行了一个“pm install -r -t "/data/local/tmp/CustomGradle-v1.0-debug.apk”的命令。
install上面几行System.out.println是我自己加的代码
编译下android gradle,然后在我们的测试工程里面执行installDebug,就能看到apkDirectory的输出了。(怎么编译怎么调试看我的第一篇博客)输出如下:
apkDirectory = FinalBuildableArtifact(APK, com.android.build.gradle.internal.scope.VariantBuildArtifactsHolder@61163dff, [<path/to/CustomGradle>/app/build/outputs/apk/debug])
INstallVariantTask opf.getOutputFile().getPath() = <path/to/CustomGradle>/app/build/outputs/apk/debug/CustomGradle-v1.0-debug.apk opf = BuildOutput{apkData=DefaultApkData(_type=MAIN, _filters=[], _versionCode=1, _versionName=1.0, _filterName=null, _outputFileName=CustomGradle-v1.0-debug.apk, _fullName=debug, _baseName=debug, _enabled=true), path=<path/to/CustomGradle>/app/build/outputs/apk/debug/CustomGradle-v1.0-debug.apk, properties=}
apkDirectory是在这个文件下面的CreationAction里面设置的,这个是一个固定的目录并能通过gradle文件配置。所以我们要找的都是在outputs这个临时变量里。他是通过ExistingBuildElements获得的,ExistingBuildElements看起来是个挺重要的类,里面很多task都会用到这个类的方法。看一下这个from函数
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/scope/ExistingBuildElements.kt
private const val METADATA_FILE_NAME = "output.json"
/**
* create a {@link BuildElement} from a previous task execution metadata file.
* @param elementType the expected element type of the BuildElements.
* @param from the folder containing the metadata file.
*/
@JvmStatic
fun from(elementType: ArtifactType, from: File): BuildElements {
val metadataFile = getMetadataFileIfPresent(from)
return loadFrom(elementType, metadataFile)
}
@JvmStatic
fun getMetadataFileIfPresent(folder: File): File? {
val outputFile = getMetadataFile(folder)
return if (outputFile.exists()) outputFile else null
}
@JvmStatic
fun getMetadataFile(folder: File): File {
return File(folder, METADATA_FILE_NAME)
}
private fun loadFrom(
elementType: ArtifactType?,
metadataFile: File?): BuildElements {
if (metadataFile == null || !metadataFile.exists()) {
val elements: Collection<BuildOutput> = ImmutableList.of()
return BuildElements(ImmutableList.of())
}
try {
FileReader(metadataFile).use { reader ->
return BuildElements(load(metadataFile.parentFile.toPath(),
elementType,
reader))
}
} catch (e: IOException) {
return BuildElements(ImmutableList.of<BuildOutput>())
}
}
@JvmStatic
fun load(
projectPath: Path,
outputType: ArtifactType?,
reader: Reader): Collection<BuildOutput> {
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(ApkData::class.java, ApkDataAdapter())
gsonBuilder.registerTypeAdapter(
ArtifactType::class.java,
OutputTypeTypeAdapter())
val gson = gsonBuilder.create()
val recordType = object : TypeToken<List<BuildOutput>>() {}.type
val buildOutputs = gson.fromJson<Collection<BuildOutput>>(reader, recordType)
// resolve the file path to the current project location.
return buildOutputs
.asSequence()
.filter { outputType == null || it.type == outputType }
.map { buildOutput ->
BuildOutput(
buildOutput.type,
buildOutput.apkData,
projectPath.resolve(buildOutput.outputPath),
buildOutput.properties)
}
.toList()
}
大概意思就是,传入一个apkDirectory的目录路径,在这个目录下找到output.json,读取里面的json构造出BuildElements类。
所以通过看InstallVariantTask的代码可以了解到,生成的apk的目录是固定的,apk的文件名是通过目录下的output.json文件指定的。
那么接下来就需要来找output.json这个文件的生成。
2. BuildElements、ProcessApplicationManifest
output.json文件的生成位于BuildElements.kt里面,代码:
@Throws(IOException::class)
fun save(folder: File): BuildElements {
val persistedOutput = persist(folder.toPath())
FileWriter(ExistingBuildElements.getMetadataFile(folder)).use { writer ->
writer.append(persistedOutput)
}
return this
}
/**
* Persists the passed output types and split output to a [String] using gson.
*
* @param projectPath path to relativize output file paths against.
* @return a json String.
*/
fun persist(projectPath: Path): String {
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(ApkData::class.java, ExistingBuildElements.ApkDataAdapter())
gsonBuilder.registerTypeAdapter(
InternalArtifactType::class.java, ExistingBuildElements.OutputTypeTypeAdapter()
)
gsonBuilder.registerTypeAdapter(
AnchorOutputType::class.java,
ExistingBuildElements.OutputTypeTypeAdapter()
)
val gson = gsonBuilder.create()
// flatten and relativize the file paths to be persisted.
return gson.toJson(elements
.asSequence()
.map { buildOutput ->
BuildOutput(
buildOutput.type,
buildOutput.apkData,
projectPath.relativize(buildOutput.outputPath),
buildOutput.properties
)
}
.toList())
}
private const val METADATA_FILE_NAME = "output.json"
@JvmStatic
fun getMetadataFile(folder: File): File {
return File(folder, METADATA_FILE_NAME)
}
调用save方法的地方有很多,也都位于各个task里面。但是大多数的save操作都是从另一个目录下的output.json取出来再save到task指定的目录下。那么第一个调用save保存ouput.json文件的task叫ProcessApplicationManifest。
private OutputScope outputScope;
@Override
protected void doFullTaskAction() throws IOException {
...
for (ApkData apkData : outputScope.getApkDatas()) {
...
System.out.println(" apkData.getOutputFileName() = " + apkData.getOutputFileName());
mergedManifestOutputs.add(
new BuildOutput(
InternalArtifactType.MERGED_MANIFESTS,
apkData,
manifestOutputFile,
properties));
...
}
new BuildElements(mergedManifestOutputs.build())
.save(getManifestOutputDirectory().get().getAsFile());
...
}
public static class CreationAction
extends AnnotationProcessingTaskCreationAction<ProcessApplicationManifest> {
public CreationAction(
@NonNull VariantScope scope,
// TODO : remove this variable and find ways to access it from scope.
boolean isAdvancedProfilingOn) {
super(
scope,
scope.getTaskName("process", "Manifest"),
ProcessApplicationManifest.class);
this.variantScope = scope;
this.isAdvancedProfilingOn = isAdvancedProfilingOn;
}
@Override
public void configure(@NonNull ProcessApplicationManifest task) {
super.configure(task);
...
final BaseVariantData variantData = variantScope.getVariantData();
...
task.outputScope = variantData.getOutputScope();
...
}
}
代码太多,这里就只放一些关键的片段。我们想要的的apk的名称来自apkData。从头说一下apkData的来源
在构造ProcessApplicationManifest的CreationAction类时,传入了一个variantScope
在执行ProcessApplicationManifest的configure时,调用variantScope.getOutputScope得到outputScope并传给ProcessApplicationManifest这个task
ProcessApplicationManifest执行时通过getApkDatas得到所有的apkData,然后保存到文件里面。
看到这里,可以了解到重命名的根源是来自variantScope里面的apkData数据。后面就来找下variantScope的来源。
3. TaskManager、BasePlugin
来看下VariantManager里面的代码:
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/VariantManager.java
@NonNull private final List<VariantScope> variantScopes;
/** Variant/Task creation entry point. */
public List<VariantScope> createAndroidTasks() {
variantFactory.validateModel(this);
variantFactory.preVariantWork(project);
if (variantScopes.isEmpty()) {
populateVariantDataList();
}
// Create top level test tasks.
taskManager.createTopLevelTestTasks(!productFlavors.isEmpty());
for (final VariantScope variantScope : variantScopes) {
createTasksForVariantData(variantScope);
}
taskManager.createSourceSetArtifactReportTask(globalScope);
taskManager.createReportTasks(variantScopes);
return variantScopes;
}
variantScopes是VariantManager的成员没变量,createAndroidTasks返回variantScopes,函数顾名思义就是在创建我们在android studio里面用到的各种task。我们不需要了解里面的每个variantScope是如何创建的,现在只需要知道是apkData的数据结构关系。apkData最终是存储在OutputScope的sortedApkDatas这个列表里面,也要记住variantData这个变量,下面会用到。
(图例 <数据类型> : 变量名)
|____VariantManager : variantManager
| |____List<VariantScope> : variantScopes
| | |____VariantScope
| | | |____BaseVariantData : variantData
| | | | |____OutputScopeFactory : outputFactory
| | | | | |____OutputScope : outputSupplier
| | | | | | |____ImmutableList<ApkData> : sortedApkDatas
再看下调用createAndroidTasks的地方,位于BasePlugin里面:
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/BasePlugin.java
@VisibleForTesting
final void createAndroidTasks() {
...
List<VariantScope> variantScopes = variantManager.createAndroidTasks();
ApiObjectFactory apiObjectFactory =
new ApiObjectFactory(
globalScope.getAndroidBuilder(),
extension,
variantFactory,
project.getObjects());
for (VariantScope variantScope : variantScopes) {
BaseVariantData variantData = variantScope.getVariantData();
apiObjectFactory.create(variantData);
}
...
}
来到BasePlugin,这已经是AndroidGradle插件比较根源的位置了。在他的createAndroidTasks函数里面调用了VariantManager的createAndroidTasks方法,拿到了variantScopes列表。然后遍历所有的variantScope,每个variantScope得到variantData(上面提到,这是存储apkData的地方)并通过apiOjectFactory进行创建。创建什么呢,看下ApiObjectFactory里面的代码:
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/ApiObjectFactory.java
public BaseVariantImpl create(BaseVariantData variantData) {
...
BaseVariantImpl variantApi =
variantFactory.createVariantApi(
objectFactory,
androidBuilder,
variantData,
readOnlyObjectProvider);
if (variantApi == null) {
return null;
}
...
createVariantOutput(variantData, variantApi); // 这是重点
try {
// Only add the variant API object to the domain object set once it's been fully
// initialized.
extension.addVariant(variantApi); // 这个是重点
} catch (Throwable t) {
// Adding variant to the collection will trigger user-supplied callbacks
throw new ExternalApiUsageException(t);
}
return variantApi;
}
private void createVariantOutput(BaseVariantData variantData, BaseVariantImpl variantApi) {
variantData.variantOutputFactory =
new VariantOutputFactory(
(variantData.getType().isAar())
? LibraryVariantOutputImpl.class
: ApkVariantOutputImpl.class,
objectFactory,
extension, // 重点
variantApi, // 重点
variantData.getTaskContainer(),
variantData
.getScope()
.getGlobalScope()
.getDslScope()
.getDeprecationReporter());
GradleVariantConfiguration config = variantData.getVariantConfiguration();
variantData
.getOutputScope()
.getApkDatas()
.forEach(
apkData -> {
apkData.setVersionCode(config.getVersionCodeSerializableSupplier());
apkData.setVersionName(config.getVersionNameSerializableSupplier());
variantData.variantOutputFactory.create(apkData); // 重点,代码在下面
});
}
一步步来看吧,ApiObjectFactory的create方法里面创建了一个BaseVariantImpl,然后在createVariantOutput方法里面给他塞了一堆数据。再看下createVariantOutput方法里面,extension是重点重的重点,后面再讲,variantApi就是在create方法里面创建的BaseVariantImpl变量。variantData是从BasePlugin里面传进来的通过variantManager的createAndroidTasks方法得到的list遍历的变量,刚才也有提到,apkData就在这个里面。variantData.getOutputScope().getApkDatas()看下这个调用,再对照着刚才的数据结构。这就是拿到了所有的apkDatas。foreach,遍历所有的apkData,然后调用variantData.variantOutputFactory.create(apkData);
variantOutputFactory就是上面刚创建的变量,他的create方法的代码在下面:
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/VariantOutputFactory.java
public class VariantOutputFactory {
...
@Nullable private final BaseVariantImpl variantPublicApi;
@NonNull private final AndroidConfig androidConfig;
...
public VariantOutput create(ApkData apkData) {
BaseVariantOutput variantOutput =
objectFactory.newInstance(targetClass, apkData, taskContainer, deprecationReporter); // 构造函数,并把apkData存到自己的成员变量里面
androidConfig.getBuildOutputs().add(variantOutput); // 后面讲,这是修改apk名的另一种方法
if (variantPublicApi != null) {
variantPublicApi.addOutputs(ImmutableList.of(variantOutput)); // 重点
}
return variantOutput;
}
}
variantPublicApi就是在createVariantOutput传进来的variantApi,也就是ApiObjectFactory.create方法里面创建的变量。
androidConfig就是上面提到的最重点extension。
这里创建了variantOutput一个变量,类型是ApkVariantOutputImpl,在他的构造函数里面把传入的apkData赋给了自己的成员变量。variantPublicApi.addOutputs(ImmutableList.of(variantOutput));这个方法又将创建的variantOutput塞进了variantPublicApi里面。
看下addOutputs的代码:
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/api/BaseVariantImpl.java
@NonNull protected final NamedDomainObjectContainer<BaseVariantOutput> outputs;
public void addOutputs(@NonNull List<BaseVariantOutput> outputs) {
this.outputs.addAll(outputs);
}
outputs是一个BaseVariantOuput的container。
这样我们得到了一个variantApi的变量,记得在ApiObjectFactory的create代码里面有一句extension.addVariant(variantApi);extension是AppExtension类型的变量,addVariant的代码如下:
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/AppExtension.java
private final DefaultDomainObjectSet<ApplicationVariant> applicationVariantList
= new DefaultDomainObjectSet<ApplicationVariant>(ApplicationVariant.class);
@Override
public void addVariant(BaseVariant variant) {
applicationVariantList.add((ApplicationVariant) variant);
}
至此我们再来重新看下从AppExtension开始的数据结构
|____AppExtension : extension
| |____DefaultDomainObjectSet<ApplicationVariant> : applicationVariantList
| | |____ApplicationVariant : variantPublicApi
| | | |____NamedDomainObjectContainer<BaseVariantOutput>: outputs
| | | | |____ApkVariantOutputImpl : variantOutput
| | | | | |____ApkData : apkData
extension下面再讲
applicationVariantList是一个Set,extension创建的时候创建的。
variantPublicApi是ApiObjectFactory新创建的
outputs是一个Container,variantPublicApi的成员变量,是通过project创建一个container
variantOutput是VariantOutputFactory新创建的
apkData就是我们要找的变量,他和variantScope下的apkData是同一个引用。重点圈一下:他和variantScope下的apkData是同一个引用
也就是说我们不管是通过variantScope修改apkData还是通过extension修改apkData效果都是一样的。
4. AppExtension
上面提到了extension是重点,他的类型是AppExtension。也许你是第一次看到这个名字,但是只要你写过Android工程,肯定会经常用到这个类,只是可能你不知道而已。我们看一下一个最进本的android的build.gradle是怎么写的
apply plugin: 'com.android.application'
android { // 这个就是AppExtension
compileSdkVersion 29
defaultConfig {
applicationId "com.xxx.customgradle"
minSdkVersion 15
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (output.outputFileName != null && output.outputFileName.endsWith('.apk')) {
def fileName = "CustomGradle-v${versionName}-${variant.buildType.name}.apk"
output.outputFileName = fileName
}
}
}
}
是不是很熟悉,其实第三行的"android"可以理解为数据类型为AppExtension的变量。里面的compileSdkVersion、defaultConfig、buildTypes都是AppExtension类的方法。applicationVariants也是他的方法,原方法名是getApplicationVariant(相关知识自行查阅groovy语法吧)。
那么现在应该就知道为什么重命名的gradle代码要这么写了吧。对比下上一节列出来的数据结构,以及源代码的各个get方法的调用,最终output.outputFileName = fileName就是给apkData赋值了新的名字。
5. 另一种重命名方法
这种方法是我在写这篇文章时突然看到的方法,试了一下确实可以。再把VariantOutputFactory.java的代码贴一下:
</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/VariantOutputFactory.java
public class VariantOutputFactory {
...
@Nullable private final BaseVariantImpl variantPublicApi;
@NonNull private final AndroidConfig androidConfig;
...
public VariantOutput create(ApkData apkData) {
BaseVariantOutput variantOutput =
objectFactory.newInstance(targetClass, apkData, taskContainer, deprecationReporter); // 构造函数,并把apkData存到自己的成员变量里面
androidConfig.getBuildOutputs().add(variantOutput); // 看这里
if (variantPublicApi != null) {
variantPublicApi.addOutputs(ImmutableList.of(variantOutput));
}
return variantOutput;
}
}
有一句“androidConfig.getBuildOutputs().add(variantOutput);”,我们已知androidConfig就是上面提到的extension,variantOutput里面是存有apkData数据的,而且apkData的引用也是和variantScope的apkData是同一个引用。那么似乎我们也可以用buildOutputs来修改apk的名字。如下android工程的build.gradle:
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.hw.customgradle"
minSdkVersion 15
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildOutputs.all { output ->
output.apkData.outputFileName = "123.apk" // 重名成成123.apk
}
}
编译之后,确实可行。
6.总结
分析源码的过程,我确实是经历了从入门到放弃的再到最后苦苦挣扎的阶段。讲真,个人觉得androidgradle的代码写的真不怎么样。从他的数据结构的管理,到代码风格,命名风格有很多都能让人抓狂。
不过分析完这些也确实掌握了一些androidgradle的内部原理,或许以后再有一些编译android工程的问题的时候不会再摸不着头脑不知所措了吧