第十一章 Android Gradle多渠道构建

简介: 因为我们发布或者推广的渠道不同,就造成了我们的Android App可能会有很多个,因为我们需要细分他们,才能针对不同的渠道做不同的处理,比如统计跟踪、是否升级、App名字是否一致等等。尤其在国内这个各种应用市场百家争鸣的时代,我们需要发布的App渠道甚至多个好几百个,而且各有各的特殊处理,所以这就更需要我们有一套自动的满足多渠道构建的工具来帮我们解决这个问题,有了Android Gradle的Flavor后,我们就可以完美的解决以上问题,并且可以实现批量自动化。这一章主要介绍多渠道构建的基本原理,然后使用Flurry和友盟这两个最常用的分析统计平台作为例子来演示多渠道构建,接着我们介绍下Fl

因为我们发布或者推广的渠道不同,就造成了我们的Android App可能会有很多个,因为我们需要细分他们,才能针对不同的渠道做不同的处理,比如统计跟踪、是否升级、App名字是否一致等等。尤其在国内这个各种应用市场百家争鸣的时代,我们需要发布的App渠道甚至多个好几百个,而且各有各的特殊处理,所以这就更需要我们有一套自动的满足多渠道构建的工具来帮我们解决这个问题,有了Android Gradle的Flavor后,我们就可以完美的解决以上问题,并且可以实现批量自动化。这一章主要介绍多渠道构建的基本原理,然后使用Flurry和友盟这两个最常用的分析统计平台作为例子来演示多渠道构建,接着我们介绍下Flavor的每个配置的用法,让大家可以根据需求定制自己的每个渠道,最后我们会介绍一种快速打包上百个渠道的方法,以提高多渠道构建的效率。


11.1 多渠道构建的基本原理


在Android Gradle中,定义了一个叫Build Variant的概念,直译是构建变体,我喜欢叫它为构件-构建的产物(Apk),一个Build Variant=Build Type+Product Flavor,Build Type就是我们构建的类型,比如release和debug,Product Flavor就是我们构建的渠道,比如baidu,google等等,他们加起来就是baiduRelease,baiduDebug,googleRelease,googleDebug,共有这几种组合的构件产出,Product Flavor也就是我们多渠道构建的基础,下面我们看看如何新增一个Product Flavor。


image.png


Android Gradle为我们提供了productFlavors方法来添加不同的渠道,它接受域对象类型的ProductFlavor闭包作为其参数,前面章节我们在介绍Build Type的时候也介绍过域对象,所以我们可以为productFlavors{}闭包添加很多的渠道,每一个都是一个ProductFlavor类型的渠道,在NamedDomainObjectContainer中的名字就是渠道名,比如baidu,google等。


image.png


以上的渠道配置之后,Android Gradle就会生成很多Task,基本上都是基于Build Type+Product Flavor的方式生成的,比如assembleBaidu,assembleRelease,assembleBaiduRelease等等,assemble开头的负责生成构件产物(Apk),比如assembleBaidu运行之后会生成baidu渠道的release和debug包;assembleRelease运行后会生成所有渠道的release包;而assembleBaiduRelease运行后只会生成baidu的release包。除了assemble系列的,还有compile系列的、install系列的等等,大家可以通过运行./gradlew tasks来查看有哪些任务。除了生成的Task之外,每个ProductFlavor还可以有自己的SourceSet,还可以有自己的Dependencies依赖,这意味着什么,意味着我们可以为每个渠道定义他们自己的资源、代码以及依赖的第三方库,这为我们自定义每个渠道提供很大的便利和灵活性,后面的11.3小节的多渠道定制我们会详细介绍这部分内容。


11.2 Flurry多渠道和友盟多渠道构建


Flurry和友盟是我们常用的两个App统计分析工具,基本上所有的软件都会接入其中的一个。Flurry本身没有渠道的概念,它有Application,所以可以把一个Application当成一个渠道,这样就可以使用Flurry统计每个渠道的活跃新增等情况;友盟本身有渠道的概念,只要我们在AndroidManifest.xml配置标注即可,下面以这两种统计演示下多渠道的用法。


Flurry的统计是以Application划分渠道的,每个Application都有一个Key,我们称之为Flurry Key,这个当我们在Flurry上创建Application的时候就自动帮我们生成好了,现在我们要做的就是为每个渠道配置不同的Flurry Key,还记得我们在第九章讲的自动已BuildConfig吗,利用的就是这个功能。


image.png


这样每个渠道的BuildConfig类中都有会有名字为FLURRY_KEY常量定义,它的值是我们在渠道中使用buildConfigField指定的值,每个渠道都不一样,这样我们只需要在代码中使用这个常量即可,这样每个渠道的统计分析就可以做到了。


`Fluury.init(this, FLUURY_KEY);

友盟的话,本身是有渠道的概念,不过它不是在代码中指定的,而是在AndroidManifest.xml中配置的,通过配置meta-data标签来设置。`

<mete-data android:value="Channel
ID" android:name="UMENG_CHANNEL">


示例中的Channel ID就是我们的渠道值,比如baidu,google等。如果我们就动态的改变的,就需要用到我们在9.5小结讲到的manifestPlaceholders,这一小节就是以友盟的多渠道为例进行讲解的,大家可以再回过头来看一下,这里不在进行详细讲了。


现在通过这两个例子我们可以发现,我们所做的其实就是对每个渠道,根据不同的业务进行不同的定制,这里是两个统计分析,以后可能还有其他监控、推送等业务,在定制的过程中我们用到了Android Gradle提供的不同的配置以及功能,最终来达到我们的目的,所以下一节我们就详细的讲下对渠道(ProductFlavor)的定制,然后大家根据这些Android Gradle提供的对渠道定制的功能,来实现自己不同渠道的业务需求。


11.3 多渠道构建定制


多渠道的定制,其实就是对Android Gradle插件的ProductFlavor的配置,通过配置ProductFlavor达到我们灵活细化的控制每一个渠道的目的。


Flavor这个单词比较有意思,看字面意思是气味、味道的意思,所以ProductFlavor也就是产品的气味或者味道,多种不同的产品味道,就是我们所说的多渠道了。多渠道这个国内特色,特别多的渠道,如果你的App不出海的话,所以针对国内这些渠道就好了,如果要出海,要再加上出海的国外的渠道。


11.3.1 applicationId


它是ProductFlavor的属性,用于设置该渠道的包名,如果你的App想为该渠道设置特别的包名,可以使用applicationId这个属性进行设置。


image.png


如上示例,就可以为google这个渠道设置其特有的包名,这样打包的时候google渠道使用的是org.flysnow.app.example112.google这个包名,而其他渠道使用的是org.flysnow.app.example112这个包名。看下它的方法原型实现


image.png


很明显可以看到是setter方法,接受一个字符串作为参数,作为该渠道的新包名。


11.3.2 consumerProguardFiles


即使一个属性,也有一个同名的方法,它只对Android库项目有用,当我们发布库项目生成一个AAR包的时候,使用consumerProguardFiles配置的混淆文件列表也会被打包到AAR里一起发布,这样当应用项目引用这个AAR包、并且启用混淆的时候,会自动的使用AAR包里的混淆文件对AAR包里的代码进行混淆,这样我们就不用对该AAR包进行混淆配置了,因为它自带了。


image.png


和我们前面讲的配置混淆是一样的方式,可以指定多个,使用逗号分开。


image.png


从源代码中也可可以看出有两种设置方式,一种是我们刚刚演示的方法,另外一种是属性设置,他们两个的区别在于:consumerProguardFiles方法是一直添加的,不会清空以前的混淆文件,而consumerProguardFiles属性配置的方式是每次都是新的混淆文件列表,以前配置的会先被清空。


11.3.3 manifestPlaceholders


这个属性已经在9.5节介绍过,这里不像细讲,大家可以再翻看9.5小节熟悉一下。


11.3.4 multiDexEnabled


这个属性用来启用多个dex的配置,主要用来突破65535方法的问题,大家可以参考9.11一节的介绍,这里不再详细表述。


11.3.5 proguardFiles


混淆使用的文件配置,可以参考8.3一节里关于混淆的讲解,这里不再详述。


11.3.6 signingConfig


签名配置,请参考8.2配置签名信息一节,这里不再详述。


11.3.7 testApplicationId


我们一般都会对Android进行单元测试,这个单元测试有自己的专门的Apk测试包,testApplicationId是用来适配测试包的包名的,它的使用方法和我们前面介绍的applicationId一样。


image.png


一般的testApplicationId的值为App的包名+.test,当然大家也可以设置其他的。


image.png


它是一个属性,自然也是有setter方法的,源代码可以看到,接受一个String类型的值作为参数。


11.3.8 testFunctionalTest和testHandleProfiling


也是和单元测试有关,Boolean型属性,testFunctionalTest表示是否是功能测试,testHandleProfiling表示是否启用分析功能。


image.png


Boolean型,true和false两个选择,示例表示作为功能测试并且启用了分析功能。


image.png


以上是这两个属性的源代码配置,他们主要用来控制测试包生成的AndroidManifest.xml,因为他们最终的配置还要体现在AndroidManifest.xml文件中的instrumentation标签的配置上。


可以参考


http://developer.android.com/intl/zh-cn/guide/topics/manifest/instrumentation-element.html

11.3.9 testInstrumentationRunner


用来配置运行测试使用的Instrumentation Runner的类名,是一个全路径的类名,而且必须是android.app.Instrumentation的子类,一般情况下,我们使用android.test.InstrumentationTestRunner,当然也可以自定义,根据自己的需求。


image.png


和其他的属性的配置一样直接配置即可,接受一个字符串类型的参数,值为android.app.Instrumentation子类的全限定路径的类名。


11.3.10 testInstrumentationRunnerArguments


这个是配合着上一个属性用的,它用来配置Instrumentation Runner使用的参数,其实他们最终使用的都是adb shell am instrument这个命令,参数就是我们使用-e标记指定的那些,所以这里使用testInstrumentationRunnerArguments参数都会被转换传递给am instrument这个命令使用,也就是转为-e key value -e key value这种命令行的方式使用。


image.png


我们可以使用示例中的方法指定很多个参数,从使用上我们也可以看出,它是一个Map<String,String>,和我们前面讲的manifestPlaceholders很相似。其他的一些参数配置可以参考


http://developer.android.com/intl/zh-cn/tools/testing/testing_otheride.html


image.png


11.3.11 versionCode和versionName


配置渠道的版本号和版本名称,详情参考8.1.4和8.1.5两个小节。


11.3.12 useJack


Boolean类型的属性,用于标记是否启用Jack和Jill这个全新的、高性能的编译器。目前我们使用的是常规的成熟的Android编译框架,这个有个问题,就是太慢,所以Google他们又搞了一个全新的、高性能的编译器,这个就是Jack和Jill,目的就是简化编译的流程,提高编译的速度和性能,不过目前他们还处于实验阶段,有写特性还不支持,比如注解,比如JDK8的特性等等,大家可以自己测试用用,但是正式产品种还是不要使用。要启用Jack编译非常简单,只需要设置useJack为true即可,默认是false。


image.png


这样即可启用,如上示例其实调用的是useJack这个方法,我们看下它的源代码


image.png


如果你想使用属性setter的方式,可以直接用=赋值。


image.png


他们的结果是一样的,但是一般我们都是使用方法,这样看着比较优雅。关于Jack和Jill这种编译方式有兴趣的话,可以参考


http://tools.android.com/tech-docs/jackandjill


11.3.13 dimension


有时候,我们想基于不同的标准来构建我们的App,比如免费版还是收费版、x86版还是arm版等等,在不考虑BuildType的情况下,这里有4种组合:x86的免费版、x86的收费版、arm的免费版、arm的收费版。对于这种情况,我们有两种方式来构建,第一种是通俗的用法,就是配置4个ProductFlavor,他们分别是x86free、x86paid、armfree、armpaid,然后针对这4个ProductFlavor配置,满足我们的需求即可。这种方式比较通俗易懂,但是有个问题,就是配置脚本的冗余,比如free的配置是有共性的,但是我们要在两个free里把共性的配置写两遍,同样x86这类也是,脚本冗余了,而且每次改动都要一个个去修改,也很麻烦,而且现在才有两个维度,每个维度的可选项都不会有很多,我们还可以忍受,如果有很多种维度呢?每个维度又有很多可选项呢?下面我们来介绍第二种方法,通过dimension多维度的方式来解决这个问题。


dimension是ProductFlavor的一个属性,接受一个字符串,作为该ProductFlavor的维度,其实我们可以简单的理解为对ProductFlavor进行分组,比如free和paid可以认为他们都是属于版本(version),而x86和arm是属于架构(abi),这样就把他们分成了两组,而dimension接受的参数就是我们分组的组名,也是维度名称。维度名称不是随便指定的,我们在使用他们之前,必须要先声明,这和我们Java的变量差不多,要先定义好才能使用,那么怎么定义的,这个就是使用android对象的flavorDimensions方法声明的。

flavorDimensions是我们使用的android{}里的方法,它和productFlavors{}是平级的,一定要先使用flavorDimensions声明维度,我们才能在ProductFlavor使用。


image.png


该方法可以接受一个可变的字符串类型的参数,所以我们可以 同时指定多个维度,但是一定要记住,这些维度是有顺序的,是有优先级的,第一个参数的优先级最大,其次是第二个,以此类推,所以声明之前一定要根据自己的需求指定好顺序。


image.png


如上例子所示,最后生成的variant(构建产物)会被如下几个ProductFlavor对象配置。

  1. android里的defaultConfig配置,我们前面讲过,它也是一个ProductFlavor。
  2. abi维度的ProductFlavor,被dimension配置标记为abi的ProductFlavor
  3. version维度的ProductFlavor,被dimension配置标记为version的ProductFlavor

维度的 优先级 非常重要,因为高优先级的flavor会替换掉低优先级的资源、代码、配置等,在例子中,优先级为abi>version>defaultConfig,因为abi的顺序在version之前。


声明了维度,我们就可以在ProductFlavor使用他们了。


image.png


通过dimension指定ProductFlavor所属的维度,非常方便,剩下的事情交给Android Gradle即可,它会帮我们生成相应的Task、SourceSet、Dependencies等。以前我们讲一个构建产物(variant)=BuildType+ProductFlavor,现在ProductFlavor这个维度又被我们通过dimension细化分组,所以就多了一些维度,比如示例中的abi和version,现在构建产物(variant)=BuildType+Abi+Version了,所以会生成如下的variant:


  1. ArmFreeDebug


  1. ArmFreeRelease


  1. ArmPaidDebug


  1. ArmPaidRelease


  1. X86FreeDebug


  1. X86FreeRelease


  1. X86PaidDebug


  1. X86PaidRelease


这种我们只用根据维度去分组、去配置,剩下的让Android Gradle帮我们组合可能结果的variant,实现了共性配置--也就是模块化编程,维护起来也很方便。


这一节节本上介绍了所有的ProductFlavor的配置,很多因为以前介绍过,所以这里就略过了,特意用一个标题来说说明是想让大家温故知新一下,除了上面列出的,还有一些方法配置,比如resConfig、resValue、targetSdkVersion、maxSdkVersion、minSdkVersion等,这些我们前面的章节都有讲过,这里就比一一介绍了,他们主要集中在第八章、和第九章,大家可以翻翻熟悉一下。


11.4 提高多渠道构建的效率


我们生成多个渠道包,主要是因为我们想跟踪每个每个渠道的情况,比如新增、活跃、留存等等,跟踪的工具一般是Flurry和友盟,所以除了根据渠道号来区分每个渠道外,大部门情况下,每个渠道并没有什么不同,他们唯一的区别是属于哪个渠道的。


对于这种情况,如果Android Gradle打包,几百个的情况下会非常慢,因为它对每个渠道包都要执行那些构建的过程,但是我们的每个渠道包只是因为渠道号不同而已,为什么要进行一个完成的构建呢,为此了打包效率,美团研究出了另外一个办法,利用了在Apk的META-INF目录下添加空文件不用重新签名的原理,非常高效,其大概就是:


  1. 利用Android Gradle打一个基本包(母包)


  1. 然后基于该包拷贝一个,文件名要能区分出来产品、打包时间、版本、渠道等。


  1. 然后对拷贝出来的Apk文件进行修改,在其META-INF目录下新增空文件,但是空文件的文件名要有意义,必须包含能区分渠道的名字比如mtchannel_google。


  1. 重复2、3生成我们所需的所有的渠道包Apk,这个可以使用Python这类脚本来做


  1. 这样就生成了我们所有发布渠道的Apk包了。


那么我们怎么使用呢,原理也非常简单,我们在Apk启动的时候(Application onCreate)的时候,读取我们写Apk中META-INF目录下的前缀为mtchannel_文件,如果找到的话,把文件名取出来,然后就可以拿到渠道标识(google)了,这里贴一个美团实现的代码,大家可以参考一下:

public static String getChannel(Context context) {
        ApplicationInfo appinfo = context.getApplicationInfo();
        String sourceDir = appinfo.sourceDir;
        String ret = "";
        ZipFile zipfile = null;
        try {
            zipfile = new ZipFile(sourceDir);
            Enumeration<?> entries = zipfile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = ((ZipEntry) entries.nextElement());
                String entryName = entry.getName();
                if (entryName.startsWith("mtchannel")) {
                    ret = entryName;
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zipfile != null) {
                try {
                    zipfile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        String[] split = ret.split("_");
        if (split != null && split.length >= 2) {
            return ret.substring(split[0].length() + 1);
        } else {
            return "";
        }
    }


以上代码逻辑我们可以再优化一下,比如为渠道做个缓存放在SharePreference里,不能总从Apk里读取吧,效率是个问题。


对于Python批处理也很简单,这里给出一段美团的Python代码,大家参考补充

import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED) 
empty_channel_file = "META-INF/mtchannel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)


添加完空渠道文件后的目录,META-INFO目录多了一个名为mtchannel_meituan的空文件:


image.png

以上是核心实现,我们要做的就是保存一个渠道列表,可以用一个文本文件保存,一行一个渠道,然后使用Python读取,for循环生成不同渠道的Apk包,这个我就不写代码了,大家可以自己试一下,这里给出一个开元的美团方案实现,大家可以参考一下, https://github.com/GavinCT/AndroidMultiChannelBuildTool 。美团方案地址:

http://tech.meituan.com/mt-apk-packaging.html


11.5 小结


到这里多渠道构建这一章就结束了,多渠道构建利用的主要是对ProductFlavor的定制,所以我们重点讲了ProductFlavor的各个配置,让大家都熟悉一下,这样在碰到多渠道的需求时,可以对比参考一下能否满足你的要求,或者需要哪些组合可以做到。


此外我们要记得productFlavors是一个ProductFlavor集合,我们可以通过操纵它做很多批量处理的事情,比如9.5小节中的批量修改AndroidManifest.xml中友盟统计的渠道名等等,这个批处理的功能要合理的利用。



本文属自学历程, 仅供参考

详情请支持原书 Android Gradle权威指南


目录
相关文章
|
4天前
|
JavaScript Java Maven
云效产品使用常见问题之android sdk 构建出aar后,上传到私有maven仓库失败如何解决
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
22天前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【4月更文挑战第3天】在移动开发领域,性能优化一直是开发者关注的焦点。随着Kotlin的兴起,其在Android开发中的地位逐渐上升,但关于其与Java在性能方面的对比,尚无明确共识。本文通过深入分析并结合实际测试数据,探讨了Kotlin与Java在Android平台上的性能表现,揭示了在不同场景下两者的差异及其对应用性能的潜在影响,为开发者在选择编程语言时提供参考依据。
|
23天前
|
数据库 Android开发 开发者
构建高效Android应用:Kotlin协程的实践指南
【4月更文挑战第2天】随着移动应用开发的不断进步,开发者们寻求更流畅、高效的用户体验。在Android平台上,Kotlin语言凭借其简洁性和功能性赢得了开发社区的广泛支持。特别是Kotlin协程,作为一种轻量级的并发处理方案,使得异步编程变得更加简单和直观。本文将深入探讨Kotlin协程的核心概念、使用场景以及如何将其应用于Android开发中,以提高应用性能和响应能力。通过实际案例分析,我们将展示协程如何简化复杂任务,优化资源管理,并为最终用户提供更加流畅的体验。
|
26天前
|
缓存 监控 Java
构建高效Android应用:从优化用户体验到提升性能
在竞争激烈的移动应用市场中,为用户提供流畅和高效的体验是至关重要的。本文深入探讨了如何通过多种技术手段来优化Android应用的性能,包括UI响应性、内存管理和多线程处理。同时,我们还将讨论如何利用最新的Android框架和工具来诊断和解决性能瓶颈。通过实例分析和最佳实践,读者将能够理解并实施必要的优化策略,以确保他们的应用在保持响应迅速的同时,还能够有效地利用系统资源。
|
1月前
|
编解码 算法 Java
构建高效的Android应用:内存优化策略详解
随着智能手机在日常生活和工作中的普及,用户对移动应用的性能要求越来越高。特别是对于Android开发者来说,理解并实践内存优化是提升应用程序性能的关键步骤。本文将深入探讨针对Android平台的内存管理机制,并提供一系列实用的内存优化技巧,以帮助开发者减少内存消耗,避免常见的内存泄漏问题,并确保应用的流畅运行。
|
24天前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践与优化
在响应式编程范式日益盛行的今天,Kotlin协程作为一种轻量级的线程管理解决方案,为Android开发带来了性能和效率的双重提升。本文旨在探讨Kotlin协程的核心概念、实践方法及其在Android应用中的优化策略,帮助开发者构建更加流畅和高效的应用程序。通过深入分析协程的原理与应用场景,结合实际案例,本文将指导读者如何优雅地解决异步任务处理,避免阻塞UI线程,从而优化用户体验。
|
29天前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
在开发高性能的Android应用时,选择合适的编程语言至关重要。近年来,Kotlin因其简洁性和功能性受到开发者的青睐,但其性能是否与传统的Java相比有所不足?本文通过对比分析Kotlin与Java在Android平台上的运行效率,揭示二者在编译速度、运行时性能及资源消耗方面的具体差异,并探讨在实际项目中如何做出最佳选择。
17 4
|
1天前
|
数据库 Android开发 开发者
安卓应用开发:构建高效用户界面的策略
【4月更文挑战第24天】 在竞争激烈的移动应用市场中,一个流畅且响应迅速的用户界面(UI)是吸引和保留用户的关键。针对安卓平台,开发者面临着多样化的设备和系统版本,这增加了构建高效UI的复杂性。本文将深入分析安卓平台上构建高效用户界面的最佳实践,包括布局优化、资源管理和绘制性能的考量,旨在为开发者提供实用的技术指南,帮助他们创建更流畅的用户体验。
|
1天前
|
移动开发 Java Android开发
构建高效Android应用:采用Kotlin协程优化网络请求
【4月更文挑战第24天】 在移动开发领域,尤其是对于Android平台而言,网络请求是一个不可或缺的功能。然而,随着用户对应用响应速度和稳定性要求的不断提高,传统的异步处理方式如回调地狱和RxJava已逐渐显示出局限性。本文将探讨如何利用Kotlin协程来简化异步代码,提升网络请求的效率和可读性。我们将深入分析协程的原理,并通过一个实际案例展示如何在Android应用中集成和优化网络请求。
|
1天前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin协程的优势与实践
【4月更文挑战第24天】随着移动开发技术的不断演进,提升应用性能和用户体验已成为开发者的核心任务。在Android平台上,Kotlin语言凭借其简洁性和功能性成为主流选择之一。特别是Kotlin的协程功能,它为异步编程提供了一种轻量级的解决方案,使得处理并发任务更加高效和简洁。本文将深入探讨Kotlin协程在Android开发中的应用,通过实际案例分析协程如何优化应用性能,以及如何在项目中实现协程。