Android(安卓)Gradle 学习之二:重命名APK
如果只是想看怎么重命名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的测试工程里面,文件在:/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的。
源代码位于/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 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, [/app/build/outputs/apk/debug])INstallVariantTask opf.getOutputFile().getPath() = /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=/app/build/outputs/apk/debug/CustomGradle-v1.0-debug.apk, properties=}
apkDirectory是在这个文件下面的CreationAction里面设置的,这个是一个固定的目录并能通过gradle文件配置。所以我们要找的都是在outputs这个临时变量里。他是通过ExistingBuildElements获得的,ExistingBuildElements看起来是个挺重要的类,里面很多task都会用到这个类的方法。看一下这个from函数
/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 = 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()) } } @JvmStatic fun load( projectPath: Path, outputType: ArtifactType?, reader: Reader): Collection { val gsonBuilder = GsonBuilder() gsonBuilder.registerTypeAdapter(ApkData::class.java, ApkDataAdapter()) gsonBuilder.registerTypeAdapter( ArtifactType::class.java, OutputTypeTypeAdapter()) val gson = gsonBuilder.create() val recordType = object : TypeToken>() {}.type val buildOutputs = gson.fromJson>(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 { 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里面的代码:
/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/VariantManager.java
@NonNull private final List variantScopes; /** Variant/Task creation entry point. */ public List 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 : variantScopes| | |____VariantScope| | | |____BaseVariantData : variantData| | | | |____OutputScopeFactory : outputFactory| | | | | |____OutputScope : outputSupplier| | | | | | |____ImmutableList : sortedApkDatas
再看下调用createAndroidTasks的地方,位于BasePlugin里面:
/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/BasePlugin.java
@VisibleForTesting final void createAndroidTasks() { ... List 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里面的代码:
/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方法的代码在下面:
/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的代码:
/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/api/BaseVariantImpl.java
@NonNull protected final NamedDomainObjectContainer outputs; public void addOutputs(@NonNull List outputs) { this.outputs.addAll(outputs); }
outputs是一个BaseVariantOuput的container。
这样我们得到了一个variantApi的变量,记得在ApiObjectFactory的create代码里面有一句extension.addVariant(variantApi);extension是AppExtension类型的变量,addVariant的代码如下:
/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/AppExtension.java
private final DefaultDomainObjectSet applicationVariantList = new DefaultDomainObjectSet(ApplicationVariant.class); @Override public void addVariant(BaseVariant variant) { applicationVariantList.add((ApplicationVariant) variant); }
至此我们再来重新看下从AppExtension开始的数据结构
|____AppExtension : extension| |____DefaultDomainObjectSet : applicationVariantList| | |____ApplicationVariant : variantPublicApi| | | |____NamedDomainObjectContainer: 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的代码贴一下:
/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工程的问题的时候不会再摸不着头脑不知所措了吧
更多相关文章
- Android(安卓)ButterKnife导入使用出错解决
- Tablayout+Viewpager+Fragment组合使用以及懒加载机制
- android Run模式也会出现"Waiting for debugger"的解决方法
- Android(安卓)studio 常用快捷键记录
- Fragment - 用法+demo
- Android(安卓)提醒用户输入错误的方法
- Android中App内部切换语言包
- Android(安卓)setContentView 源码解析
- Android的常见控件(TextView、EditText、Button、Menu)使用