如果只是想看怎麼重命名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工程的問題的時候不會再摸不着頭腦不知所措了吧