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的測試工程裏面,文件在:</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工程的問題的時候不會再摸不着頭腦不知所措了吧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章