这样使用Gradle可以神奇地打各种渠道包 - Go语言中文社区

这样使用Gradle可以神奇地打各种渠道包


640?wx_fmt=jpeg


/   今日科技快讯   /


6月12日,在特斯拉2019年年度股东大会召开之际,正值这家电动汽车制造商处于关键性的历史时刻。对于特斯拉的投资者来说,过去的一年就像坐过山车一样,特斯拉股价最高升至每股387.46美元,最低跌至每股176.99美元。特斯拉首款大众型电动汽车Model 3就处于这些剧烈波动的中心。


/   作者简介   /


本篇文章来自GitLqr的投稿,分享了如何利用gradle来实现多渠道配置,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。


GitLqr的博客地址:

https://juejin.im/user/58a53faf5c497d005fa78737


/   新增渠道   /


使用AndroidStudio配合gradle,可以很方便的输出多个渠道包,只需要在app Module下的build.gradle中,对productFlavors领域进行配置即可,假设我当前开发的项目,需要上线不同的地区,一个是国内版,一个美国版,还有一个免费版,那么gradle可以这么配:



以上多渠道配置完成后,在Android Studio的Build Variants标签中,就会有不同渠道变体供我们选择了。当我们想使用AS直接运行某个渠道的app时,就需要先在Build Variants标签中选择好变体,再点击"运行"按钮运行项目。


640?wx_fmt=png


在productFlavors中还可以配置包名(applicationId)、版本号(versionCode)、版本名(versionName)、icon、应用名 等等,举个例子:


free {
    applicationId 'com.lqr.demo.free'
    versionCode 32
    versionName '1.3.2'
    manifestPlaceholders = [
            app_icon: "@drawable/ic_launcher",
            app_name: "菜鸡【免费版】",
    ]
}


注意:


这里配置的包名是applicationId,而不是清单文件里的packageName,applicationId与packageName是不一样的。


我们常说,一部Android设备上不能同时安装2个相同包名的app,指的是applicationId不能一样。


applicationId与packageName的区别可查阅《ApplicationId versus PackageName》:

https://link.juejin.im/?target=http%3A%2F%2Ftools.android.com%2Ftech-docs%2Fnew-build-system%2Fapplicationid-vs-packagename


如果工程要求不同渠道共存,或者对版本号、icon、应用名等有定制需求的话,那么这个多渠道配置就显得非常有用了。其中,app_icon、app_name是放在manifestPlaceholders的,这个其实是在对AndroidManifest.xml中的占位符进行变量修改,也就是说,要定制icon或者应用名的话,还需要对清单文件做些小修改才行(增加一些占位符),如:


<application
    xmlns:tools="http://schemas.android.com/tools"
    android:icon="${app_icon}"
    android:label="${app_name}"
    android:theme="@style/AppTheme"
    android:largeHeap="true"
    tools:replace="android:label">

    ...
</application>


/   生成渠道变量   /


在新增渠道之后,我们可以对这些渠道进行一起更多的配置,假设项目代码需要根据不同的渠道,赋予不同的数据,当然你可以选择在java代码中通过判断当前渠道名,配合switch来设置静态常量,但其实不用那么烦琐,而且有些静态数据通过类似config.gradle或config.properties这类配置文件来配置有比较好,那么gradle中的applicationVariants完全可以帮助到我们,以下面的配置Demo为例进行说明:


// 多渠道相关设置
applicationVariants.all { variant ->
    buildConfigField("String", "PROUDCT", ""newapp"")
    buildConfigField("String[]", "DNSS", "{"http://119.29.29.29","http://8.8.8.8","http://114.114.114.114"}")
    if (variant.flavorName == 'china') {
        buildConfigField("String", "DNS", ""http://119.29.29.29"")
    } else if (variant.flavorName == 'america') {
        buildConfigField("String", "DNS", ""http://8.8.8.8"")
    } else if (variant.flavorName == 'free') {
        buildConfigField("String", "DNS", ""http://114.114.114.114"")
    }
}


通过gradle中提供的buildConfigField(),AndroidStudio会在执行脚本初始化时,根据当前所选变体将对于的配置转变为BuildConfig.java中的一个个静态常量:


640?wx_fmt=png


当我切换其他变体时,BuildConfig中的DNS也会跟着一起改变,这样,我们在工程代码中,就不需要去判断当前渠道名来为某些静态常量赋值了。这里只是举例了使用buildConfigField()来生成String和String[]常量,当然也可以用来生成其它类型的常量数据,有兴趣的话,可以百度了解下。


/   变体的使用   /


上面提到了变体,那么变体是什么?可以这样理解,变体是由【Build Type】和【Product Flavor】组合而成的,组合情况有【Build Type】*【Product Flavor】种,举个例子,有如下2种构建类型,并配置了2种渠道:


Build Type:release debug
Product Flavor:china free


那么最终会有四种 Build Variant 组成:


chinaRelease chinaDebug freeRelease freeDebug


变体在复杂多渠道工程中是相当有用的,可以做到资源文件合并以及代码整合,这里的合并与整合怎么理解?我们使用Android Studio进行项目开发时,会把代码文件与资源文件都存放在app/src目录下,通常是main下会有java、res、assets来区分存放代码文件和资源文件,你可以把main看作是默认渠道工程文件目录,也就是说main下存放在代码文件和资源文件对所有渠道来说都是共同持有的。


640?wx_fmt=png


那么,一旦出来了某些代码文件或者资源文件是个别渠道专属时,应该怎么办呢?因为main是共有的,所以理想状态下,我们并不会把这类"不通用"的文件放在main下(这样做不会出错,但是做法很low,会增大apk包体积),Android Studio为变体做了很好的支持,我们可以在app/src下,创建一个以渠道名命名的目录,用于存放这类个别渠道专属的代码文件和资源文件,如:


640?wx_fmt=png


可以看到,当我选择freeDebug变体时,app/src/free下的目录高亮了,说明它们被Android Studio识别,在运行工程时,Android Studio会将free和main下的所有资源文件进行合并,将代码文件进行整合。同理,如果我选择的是chinaDebug变体,那么app/src/china下目录就会高亮。知道如何创建变体目录后,下面就开始进行资源合并与代码整合了。


资源合并


资源文件有哪些?我们可以这样认为:


资源文件 = res下的所有文件 + AndroidManifest.xml 


变体的资源合并功能简直是"神器"一般的存在,可以解决很多业务需求,如不同渠道显示的icon不同,应用名不同等等。Android Studio在对变体目录和main目录进行资源合并时,会遵守这样的规则,假设当前选中的变体是freeDebug:


  • 某资源在free下有,在main中没有,那么在打包时,会将该资源直接合并到main资源中。

  • 某资源在free下有,在main中也有,那么在打包时,会以free为主,将free中资源替换掉main中资源。


针对上述2个规则,这里以string.xml为例进行说明,main下的string.xml是:


<resources>
    <string name="app_name">Demo</string>
    <string name="app_author">Lin</string>
</resources>


free下的string.xml是:


<resources>
    <string name="error_append">发生错误</string>
    <string name="app_author">Lqr</string>
</resources>


那么最终打出的apk包里的string.xml是:


<resources>
    <string name="app_name">Demo</string>
    <string name="error_append">发生错误</string>
    <string name="app_author">Lqr</string>
</resources>


除了字符串合并外,还有图片(drawable、mipmap)、布局(layout)、清单文件(AndroidManifest.xml)的合并,具体可以自己尝试一下。其中,清单文件的合并需要提醒一点,如果渠道目录下的AndroidManifest.xml与main下的AndroidManifest.xml拥有相同的节点属性,但属性值不同时,那么就需要对main下的AndroidManifest.xml进行修改了,具体修改要根据编译时报错来处理,所以,报错时不要慌,根据错误提示修改就是了。


注意:布局(layout)文件的合并是对整个文件进行替换的~。


代码整合


代码文件,顾名思义就是指java目录下的.java文件了,为什么代码叫整合,而资源却是合并呢?因为代码文件是没办法合并的,只能是整合,整合是什么意思?假设当前选中的变体是freeDebug,有一个java文件是Test.java,这个Test.java要么只存在free/java下,要么只存在于main/java下,如:


640?wx_fmt=png


可以看到,一切正常,Test.java被AndroidStudio识别,但如果此时在main/java下也存在Test.java,那么Android Studio就会报错了:


640?wx_fmt=png


代码整合是一个比较头痛的事,因为如果你是在渠道目录free下去引用main下的类,那么是完全没有问题的,但如果反过来,在main下去引用free下的专属类时,情况就会变得很糟糕,当你切换其他变体时(如,切换成chinaDebug),这时工程就会报错了,因为变体切换,Test.java是free专属的,在chinaDebug变体下,free不会被识别,于是main就找不到对应的类了。


选择freeDebug变体时,正常引用Test.java:


640?wx_fmt=png


选择chinaDebug变体时,找不到Test.java(只找到junit下的Test.java):


640?wx_fmt=png


所以,对于代码整合,需要我们在开发过程中慎重考虑,多想想如何将渠道目录与main目录进行解耦。比如可以使用Arouter来解耦main与渠道目录下所有的Activity、Fragment,将类引用转换为字符串引用,全部将由Arouter来管理,又或者通过反射来处理,等等,这里顺带记录一下,我项目中使用ARouter来判断Activity、Fragment是否存在,和获取的相关方法:


/**
 * 获取到目标Delegate(仅仅支持Fragment)
 */

public <T extends CommonDelegate> getTargetDelegate(String path) {
    return (T) ARouter.getInstance().build(path).navigation();
}

/**
 * 获取到目标类class(支持Activity、Fragment)
 */

public Class<?> getTargetClass(String path) {
    Postcard postcard = ARouter.getInstance().build(path);
    LogisticsCenter.completion(postcard);
    return postcard.getDestination();
}


其他


前面只说到了res和java这2个目录,那么assets呢,它是属于哪种?很可惜,assets虽然是资源,但它不是合并,而是整合,也就是说,assets文件的处理方式跟java文件的处理方式是一样的,不能在渠道目录和main目录下同时存在相同的assets文件,这将对某些需求实现造成阻碍,举个例子,假设china与free使用的assets资源是一样的,而america单独使用自己的assets资源,并且这些assets资源文件名都是一样的,那这时要怎么办呢?给每个渠道都放一份各自的assets资源吗?这种做法可行,但很low,原因如下:


  1. 复用性差:都说了china与free使用的资源是一样的,从整个工程的角度来看,一个工程里放了2份一模一样的assets资源文件,如果我有10个渠道,其中9个渠道使用的assets资源是一样的要怎么办,copy9次?

  2. 维护成本高:在开发行业里,需求变动是很常见的事,产品经理会时不时改下需求,所以,叫你改assets资源文件也是很有可能的,如果你采用每个渠道都放一份,那么当assets资源需要修改时,你就需要将每个渠道的assets目录资源替换一遍。记得,是每次修改都要替换一遍。


正确的解决方案是使用sourceSets,对于sourceSets的使用,放到下一节去说明。


/   sourceSets   /


强大的gradle,通过sourceSets可以让开发者能够自定义项目结构,如自定义assets目录、java目录、res目录,而且还可以是多个,但要知道的是,sourceSets并不会破坏变体的合并规则,它们是分开的,sourceSets只是起到了“扩充”的作用。这里先摆一下sourceSets的常规使用:


sourceSets {
    main {
        manifest.srcFile 'AndroidManifest.xml'
        java.srcDirs = ['src']
        aidl.srcDirs = ['src']
        renderscript.srcDirs = ['src']
        res.srcDirs = ['res']
        assets.srcDirs = ['assets']
    }
}


复用assets资源


对于多渠道共用同一套assets资源文件这个问题,结合sourceSets,我们可以这么处理,步骤如下:


  1. 把共用的assets资源存放到一个渠道目录下,如free/assets。

  2. 修改sourceSets规则,强制指定china渠道的assets目录为free/assets。


sourceSets {
    china {
        sourceSet.assets.srcDirs = ['src/free/assets']
    }
}


这样配置以后,如果下次需要统一修改china与free的assets资源文件时,你就只需要把free/assets目录下的资源文件替换掉就好了。虽然这种写法已经满足前面说的需求了,但是还不够,还可以再优化一下,假设你有20个渠道,都使用同一套assets资源的话,按前面的写法你就要写19遍sourceSets配置了。


sourceSets {
    china {
        sourceSet.assets.srcDirs = ['src/free/assets']
    }
    a{
        sourceSet.assets.srcDirs = ['src/free/assets']
    }
    b{
        sourceSet.assets.srcDirs = ['src/free/assets']
    }
    ...
}


可以想像,在这个gradle文件中,光sourceSets配置就会有多长,你可能会说,一个项目怎么会有这么多渠道,不好意思,本人所处公司的业务需求就有20+个渠道的情况,话不多说,下面就来看看怎么优化好这段配置,如果你有学习过gradle,就应该知道,gradle是一种脚本,脚本是可以像写代码一样写逻辑的,那么上面的配置就可以转化为一个if-else代码片段:


sourceSets {
    sourceSets.all { sourceSet ->
    // println("sourceSet.name = ${sourceSet.name}")
    if (sourceSet.name.contains('Debug') || sourceSet.name.contains('Release')) {
        if (sourceSet.name.contains("china") 
                || sourceSet.name.contains("a")
                || sourceSet.name.contains("b")
                || ...) {
                sourceSet.assets.srcDirs = ['src/free/assets']
            }
        }
    }
}


现在你可能会觉得这样写好像精简不了多少,不过一旦你的业务复杂起来,像这样用代码的逻辑思维来处理配置,相信这会是一种不错的选择。


有兴趣的可以打印下sourceSet.name;if的写法不一定要用contains(),也可以用其他的判断方式,具体看开发者自己决定。


修改程序主入口


对于sourceSets的使用,除了针对修改assets以外,java文件、res资源文件、清单文件等等都是可以用同样的方式进行“扩充”的,比如不同渠道共用一套java代码逻辑,那么我们可以把这套代码单独抽取出来存放在一个其他目录下,然后使用sourceSets对其进行添加。这里就以我亲身经历来说明,我是如何通过sourceSets对于java和清单文件进行指定,并且完美解决此类"变态"需求的。


背景


新的app项目开发完成,现在需要将项目定制化后上线,项目整体采用 1个Activity + n个Fragment架构,这个Activity便是程序主入口,因为我们产品是做机顶盒app开发,产品开发完成后,需要上线到盒子运营商(局方)的应用商店,然后通过盒子推荐位(EPG)启动我们开发的app,因此上线后,需要提供app的包名和类名给到局方,假设新app的包名和类名分别如下:


包名:com.lqr.newapp
类名:com.lqr.newapp.MainActivity


需求


把新app的包名和类名改成跟旧app的一样,因为局方那边不想换~~假设旧app的包名和类名如下:


包名:com.lqr.oldapp
类名:com.lqr.oldapp.MainActivity


问题


修改包名很简单,但是修改入口类名就很麻烦了,如果我在该渠道目录下新增一个com.lqr.oldapp.MainActivity,并在其清单文件中进行注册,那么,在打包时,渠道目录下的AndroidManifest.xml会与main目录下的AndroidManifest.xml进行合并。


640?wx_fmt=png


而main目录下的AndroidManifest.xml中已经注册了com.lqr.newapp.MainActivity,这样就会导致,最终输出apk包中的清单文件会有2个入口类。


640?wx_fmt=png


是的,这样的产品交付出去,确实也可以应付掉局方的需求,但是,一旦盒子安装了这个app,那么盒子Launcher上可能会同时出现2个入口icon,到时又是一顿折腾,毕竟app上线流程比较麻烦,我们最好是保证产品就一个入口。


分析


因为变体的资源合并规则,只要渠道目录和main目录下都存在AndroidManifest.xml,那么最终apk包里的清单文件合并出来的就会是2个文件的融合,所以,不能在这2个清单文件中分别注册入口。可以抽出2个不同入口的AndroidManifest.xml存放到其他目录,main下的AndroidManifest.xml只注册通用组件即可。


操作


a. 抽离MainActivity(oldapp)


在app目录下,创建一个support/entry目录(名字随意),用于存放入口相关功能的代码及资源文件,将com.lqr.oldapp.MainActivity放到support/entry/java目录下。


640?wx_fmt=png


b. 抽离AndroidManifest.xml


在support目录下,创建manifest(名字随意),用于存放各渠道对应的AndroidManifest.xml,如:


640?wx_fmt=png


其中newapp目录下的AndroidManifest.xml:


<application>
    <activity android:name="com.lqr.newapp.MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>

            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
</application>


oldapp目录下的AndroidManifest.xml:


<application>
    < 版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/c10wtiybq1ye3/article/details/91911155
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

  • 发表于 2019-09-05 17:47:34
  • 阅读 ( 906 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢