如果只是想看怎么重命名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工程的问题的时候不会再摸不着头脑不知所措了吧

更多相关文章

  1. Android(安卓)ButterKnife导入使用出错解决
  2. Tablayout+Viewpager+Fragment组合使用以及懒加载机制
  3. android Run模式也会出现"Waiting for debugger"的解决方法
  4. Android(安卓)studio 常用快捷键记录
  5. Fragment - 用法+demo
  6. Android(安卓)提醒用户输入错误的方法
  7. Android中App内部切换语言包
  8. Android(安卓)setContentView 源码解析
  9. Android的常见控件(TextView、EditText、Button、Menu)使用

随机推荐

  1. Android(安卓)通知栏Notification的整合
  2. Android中自定义一个View的方法详解
  3. 基于 Distcc 的android分布式编译环境的
  4. android:showAsAction的用法
  5. AndroidUnity项目嵌入有米广告注意
  6. Android的IBinder接口及其安全性机制
  7. Android学习路线(十一)管理Activity的生命
  8. Android中Canvas绘图之Shader使用图文详
  9. 直接拿来用!最火的Android开源项目(一)-CSDN
  10. Android中的动画效果学习之二---Tween动