题外话
一个人做项目的开发和维护,个人觉得还是挺不错的,很自由。当然,也会遇到各种各样的问题,都要自己解决处理,毕竟一个人,但这也是提升个人技术的最快途径。
最近项目上线相对比较频繁,一个基础产品(主要产品,称之为base吧),还有一个作为马甲产品(称之为mP1吧,鬼知道以后会不会有更多马甲),两个产品每两周就要各上一个版本,每个都要打好几十个包,这在之前,是很痛苦的。试想一个场景:
快要下班了,领导问马甲产品的功能都做得差不多了吧,好,那就今天马甲产品上线。那就开始打包吧,需要打60个包呀,好,一切都检查好了,觉得没问题了,那就快马加鞭的开始打包吧,在命令行输入gradlew assembleRelease
,就开始了漫长的等待,要下班啊,着急啊,死死地盯着命令行那一动不动的百分号进度···
终于到了99%,结果无意间发现项目的版本号没有+1,我去,想死的心都有了~~~
后续情景可以自行脑补了,于是,痛定思痛,决定第二天来了,一定要找到一种省时省力的打包方式。这就是下文的由来。
打包进化的三个版本
注意:考虑到多马甲产品,工程里的Java文件的packageName统一,只修改应用的applicationId即可
版本一:复制项目,单独打包
1、将基础工程复制一份,作为马甲项目,只看app/build.gradle
文件:
apply plugin: 'com.android.application'
def releaseTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
android {
compileSdkVersion 25
buildToolsVersion '25.0.2'
useLibrary 'org.apache.http.legacy'
defaultConfig {
applicationId "com.ylzt.mP1"
minSdkVersion 16
targetSdkVersion 25
versionCode 10
versionName "1.0"
ndk {
//选择要添加的对应cpu类型的.so库。
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
// 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
}
multiDexEnabled true
}
lintOptions {
//checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
// abortOnError false
disable "MissingTranslation"
}
//打包渠道
productFlavors {
"0010001" {}
"0010002" {}
"0010003" {}
"0010004" {}
"0010005" {}
"0010006" {}
"0010007" {}
"0010008" {}
"0010009" {}
"0010010" {}
"0010011" {}
"0010012" {}
"0010013" {}
"0010014" {}
"0010015" {}
"0010016" {}
"0010017" {}
"0010018" {}
"0010019" {}
"0010020" {}
"0010021" {}
"0010022" {}
"0010023" {}
"0010024" {}
"0010025" {}
"0010026" {}
"0010027" {}
"0010028" {}
"0010029" {}
"0010030" {}
"0010031" {}
"0010032" {}
"0010033" {}
"0010034" {}
"0010035" {}
"0010036" {}
"0010037" {}
"0010038" {}
"0010039" {}
"0010040" {}
"0010041" {}
"0010042" {}
"0010043" {}
"0010044" {}
"0010045" {}
"0010046" {}
"0010047" {}
"0010048" {}
"0010049" {}
"0010050" {}
"0010051" {}
"0010052" {}
"0010053" {}
"0010054" {}
"0010055" {}
"0010056" {}
"0010057" {}
"0010058" {}
"0010059" {}
"0010060" {}
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
signingConfigs {
debug {
// No debug config
storeFile file("C:/Users/dev-android/.android/debug.keystore")
storePassword "xxxxxx"
keyAlias "xxx"
keyPassword "xxxxxx"
}
release {
storeFile file("D:/keystore/xxx.keystore")
storePassword "xxxxxx"
keyAlias "xxx"
keyPassword "xxxxxx"
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles 'proguard-rules.pro'
}
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
// 是否进行混淆
minifyEnabled true
// 混淆文件的位置
proguardFiles 'proguard-rules.pro'
signingConfig signingConfigs.release
shrinkResources true // 移除无用的resource文件
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:25.1.0'
compile 'com.android.support:recyclerview-v7:25.1.0'
/*依赖module*/
compile project(':..:LibModules/:core')
//图片上传
compile files('libs/httpmime-4.1.1.jar')
//unzip
compile files('libs/apache-ant-zip.jar')
//wx
compile files('libs/libammsdk.jar')
······
}
这里的所有渠道都用编号表示,第二位1就是代表的第一个马甲产品mP1,基础产品的第二位是0(例如:0000001),这是为了区分各个产品,通过不同编号来对应各个渠道。
这里和基础产品不同的有两处:
第一:applicationId "com.ylzt.mP1"
,基础的为applicationId "com.ylzt"
第二处:就是上面所说的,渠道编号的第二位不同
其他不同的,就是代码里的逻辑了,或者页面UI之类的,这个就不说了。
2、修改完app/build.gradle
之后,直接在window命令行中输入命令即可:
Y:\>cd Y:\WorkPlaces\AndroidStudioWP\ApplicationDemos\app
Y:\WorkPlaces\AndroidStudioWP\ApplicationDemos\app>gradlew assembleRelease
缺点:
耗时——渠道越多,所用时间越长
繁琐——多一个马甲,就需要在多复制一份工程
笨拙——一处修改,所有的马甲都需要修改
......
版本二:整合项目,变量控制打包
思路:通过gradle的productFlavors属性创建多个不同版本的App
要点:
1、packageName和applicationId的区别:
packageName即为包名,资源文件(R文件和四大组件等)的路径
applicationId作为应用的唯一标识
[参考官方文档ApplicationId versus PackageName(需要翻墙)]
具体步骤:
1、创建版本文件夹:
在src目录下建立相应的版本文件夹,需要多少个版本就创建多少个文件夹,如图所示:
注:在多版本目录图示中
红框标注的文件是唯一的
蓝绿色框标注的文件表示可以同时存在各个版本之中,但不能出现在main目录中
蓝紫色框标注的文件都可以存在个目录中
这就涉及到了合并规则:
a、图片、音频、 XML 类型的 Drawable 等资源文件,将会进行文件级的覆盖
b、字符串、颜色值、整型等资源以及 AndroidManifest.xml ,将会进行元素级的覆盖
c、代码资源,同一个类, buildTypes 、 productFlavors 、 main 中只能存在一次,否则会有类重复的错误,但可以存在相同的包目录
覆盖等级为:buildTypes > productFlavors > main
简言之:[详见‘文件内容详情图示’]
main目录中资源代码内容都是公共的,java目录下的代码文件是唯一的,其他版本目录不可存在;res目录下的资源文件(如string.xml)可以在其他版本目录存在,但其中的文件内容必须是唯一的。
其他各版本目录下的可以存在名称相同的文件,文件中的资源代码内容皆为各自定制独有的
具体请看官网:https://developer.android.com/studio/build/manifest-merge.html (需要翻墙)
2、更换各自版本资源代码内容:
基础版本或者马甲的页面风格各不相同,可能还会有某处的逻辑跳转等也不相同,因此,需要在相应的文件夹中放入定制化的代码或者资源文件,如图标,启动页等【例如‘文件内容详情图示’中所示的app_name
就需要更换成各自版本的名称】
3、配置产品变体版本:
在app的app/build.gradle
文件中加入
productFlavors {
base { //基础版本
applicationId "com.ylzt.apcapp"
versionCode 20
versionName "2.0"
}
mP1 { //马甲1
applicationId "com.ylzt.apcapp.mp1"
versionCode 10
versionName "1.0"
}
}
其中可构建的内容还有很多,具体可以参考:https://developer.android.com/studio/build/build-variants.html
4、配置友盟渠道:
一个产品版本的时候,是要把友盟各个渠道都配置到productFlavors
中的,如下:
productFlavors {
playStore {}
miui {}
wandoujia {}
//......
}
还有在AndroidManifest.xml中配置标签:
<meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}" />
但是现在将产品作为构建变体,所以就需要使用组合了,即flavorDimensions
属性,来创建两种模式,一种作为产品变体(“verf”),一种作为渠道变体(“channel”),将各个变体进行分组,并通过dimension 来区分所要构建的变体类型,即可完成相应的组合。
注:其中的vcase变量是用来区分产品的,在上面版本一中提到了。fname是用来过滤版本的
配置如下:
def vcase //版本马甲编号,第二版打包方式定义变量
flavorDimensions "verf", "channel" //第二版打包方式
productFlavors {
base { //基础版本
dimension "verf"
applicationId "com.ylzt.apcapp.debug"
// applicationId "com.ylzt.apcapp"
vcase = "0"
fname = "base"
versionCode 20
versionName "2.0"
if (applicationId.endsWith('.debug')) { //debug
manifestPlaceholders = [
JPUSH_APPKEY: "e3b5c9a23c91fe0154ce1e97",
JPUSH_PKGNAME: "com.ylzt.apcapp.debug"
]
} else { //release
manifestPlaceholders = [
JPUSH_APPKEY: "0cf7784913f4ec3ce1cf0eb8",
JPUSH_PKGNAME: "com.ylzt.apcapp"
]
}
}
mP1 { //马甲1
dimension "verf"
applicationId "com.ylzt.apcapp.mp1.debug"
// applicationId "com.ylzt.apcapp.mp1"
// vcase = "1"
// fname = "mP1"
versionCode 10
versionName "1.0"
if (applicationId.endsWith('.debug')) { //debug
manifestPlaceholders = [
JPUSH_APPKEY: "e1313b5e0150cf749cee9784",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1.debug"
]
} else { //release
manifestPlaceholders = [
JPUSH_APPKEY: "c9a21c73c91f3cef0f4eceb8",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1"
]
}
}
//第二版打包方式
//友盟各个渠道编号
"00${vcase}0001" { dimension "channel" }
"00${vcase}0002" { dimension "channel" }
"00${vcase}0003" { dimension "channel" }
"00${vcase}0004" { dimension "channel" }
"00${vcase}0005" { dimension "channel" }
"00${vcase}0006" { dimension "channel" }
"00${vcase}0007" { dimension "channel" }
"00${vcase}0008" { dimension "channel" }
"00${vcase}0009" { dimension "channel" }
"00${vcase}0010" { dimension "channel" }
"00${vcase}0011" { dimension "channel" }
"00${vcase}0012" { dimension "channel" }
"00${vcase}0013" { dimension "channel" }
"00${vcase}0014" { dimension "channel" }
"00${vcase}0015" { dimension "channel" }
"00${vcase}0016" { dimension "channel" }
"00${vcase}0017" { dimension "channel" }
"00${vcase}0018" { dimension "channel" }
"00${vcase}0019" { dimension "channel" }
"00${vcase}0020" { dimension "channel" }
"00${vcase}0021" { dimension "channel" }
"00${vcase}0022" { dimension "channel" }
"00${vcase}0023" { dimension "channel" }
"00${vcase}0024" { dimension "channel" }
"00${vcase}0025" { dimension "channel" }
"00${vcase}0026" { dimension "channel" }
"00${vcase}0027" { dimension "channel" }
"00${vcase}0028" { dimension "channel" }
"00${vcase}0029" { dimension "channel" }
"00${vcase}0030" { dimension "channel" }
"00${vcase}0031" { dimension "channel" }
"00${vcase}0032" { dimension "channel" }
"00${vcase}0033" { dimension "channel" }
"00${vcase}0034" { dimension "channel" }
"00${vcase}0035" { dimension "channel" }
"00${vcase}0036" { dimension "channel" }
"00${vcase}0037" { dimension "channel" }
"00${vcase}0038" { dimension "channel" }
"00${vcase}0039" { dimension "channel" }
"00${vcase}0040" { dimension "channel" }
"00${vcase}0041" { dimension "channel" }
"00${vcase}0042" { dimension "channel" }
"00${vcase}0043" { dimension "channel" }
"00${vcase}0044" { dimension "channel" }
"00${vcase}0045" { dimension "channel" }
"00${vcase}0046" { dimension "channel" }
"00${vcase}0047" { dimension "channel" }
"00${vcase}0048" { dimension "channel" }
"00${vcase}0049" { dimension "channel" }
"00${vcase}0050" { dimension "channel" }
"00${vcase}0051" { dimension "channel" }
"00${vcase}0052" { dimension "channel" }
"00${vcase}0053" { dimension "channel" }
"00${vcase}0054" { dimension "channel" }
"00${vcase}0055" { dimension "channel" }
"00${vcase}0056" { dimension "channel" }
"00${vcase}0057" { dimension "channel" }
"00${vcase}0058" { dimension "channel" }
"00${vcase}0059" { dimension "channel" }
"00${vcase}0060" { dimension "channel" }
/**/
}
productFlavors.all { flavor ->
if (dimension=="channel") {
flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}
//变种过滤
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if (!names.contains(fname)) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
在测试运行的时候,可以选择相应的版本进行运行,如图:
5、相关问题总结:
- 问题一:微信分享支付的回调
根据文档微信分享必须在对应的包名下创建wxapi/WXEntryActivity
才能回调,所以需要在各自的目录下新建各自版本的wxapi/WXEntryActivity
来保证唯一(注意包名),并在各版本下的AndroidManifest.xml中注册,如图:
- 问题二:极光推送相关问题:
极光推送比较坑爹的是,开发环境和生产环境不能使用同一个appkey,所以面临的问题就是:有多少个产品应用,就需要申请双倍的appkey,比如上面我们有base和mP1两个产品,就需要申请四个appkey,分别为:
base的appkey:xxxxxxbase
base的debug_appkey:xxxxxxbaseDebug
mP1的appkey:xxxxxxmP1
mP1的debug_appkey:xxxxxxmP1Debug
注:这里的后缀用于区分而已,实际appkey形如:cce012wwf66bd554999a56w6
当然了,每一个的包名都不能相同,对于这种情况,就需要在构建变体的时候,通过后缀来判断使用哪一个appkey了:
productFlavors {
base { //基础版本
applicationId "com.ylzt.apcapp.debug" //打包的时候关闭注释
// applicationId "com.ylzt.apcapp" //打包的时候打开注释
versionCode 20
versionName "2.0"
if (applicationId.endsWith('.debug')) { //debug
manifestPlaceholders = [
JPUSH_APPKEY: "e3b5c9a23c91fe0154ce1e97",
JPUSH_PKGNAME: "com.ylzt.apcapp.debug"
]
} else { //release
manifestPlaceholders = [
JPUSH_APPKEY: "0cf7784913f4ec3ce1cf0eb8",
JPUSH_PKGNAME: "com.ylzt.apcapp"
]
}
}
mP1 { //马甲1
applicationId "com.ylzt.apcapp.mp1.debug" //打包的时候关闭注释
// applicationId "com.ylzt.apcapp.mp1" //打包的时候打开注释
versionCode 10
versionName "1.0"
if (applicationId.endsWith('.debug')) { //debug
manifestPlaceholders = [
JPUSH_APPKEY: "e1313b5e0150cf749cee9784",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1.debug"
]
} else { //release
manifestPlaceholders = [
JPUSH_APPKEY: "c9a21c73c91f3cef0f4eceb8",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1"
]
}
}
}
在AndroidManifest.xml中配置极光相关代码:
<meta-data
android:name="JPUSH_CHANNEL"
android:value="developer-default" />
<meta-data
android:name="JPUSH_APPKEY"
android:value="${JPUSH_APPKEY}" />
<!-- 包名引用举例-->
<activity
android:name="cn.jpush.android.ui.PushActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@android:style/Theme.NoTitleBar">
<intent-filter>
<action android:name="cn.jpush.android.ui.PushActivity" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="${JPUSH_PKGNAME}" />
</intent-filter>
</activity>
通过这种方式就可以分别使用各自环境下的appkey了,不过有一点麻烦的是发版打包的时候,需要手动打开和关闭相应的注释。
我也考虑过使用applicationIdSuffix ".debug"
,即在配置debug
的时候使用applicationIdSuffix
属性,这样就可以不用去手动打开和关闭注释,因为这两个环境下的applicationId只是差一个“.debug”的后缀而已。但是这样会出现一个问题,就是在开发环境下,会收不到极光推送的消息,原因何在呢?
可以肯定的是,debug版本的应用并未注册到极光推送的服务器上,即并未获得RegistrationId。我个人觉得,实际已经注册了,但是注册的是release版本,即生产环境的版本。这其中似乎是引用的问题,下面我用伪代码以base版本为例来说一说我的猜测:
applicationIdSuffix = ".debug"
applicationId = "com.ylzt.apcapp"
applicationId_result = applicationId + applicationIdSuffix //com.ylzt.apcapp.debug
if (applicationId.endsWith('.debug')) { //debug
manifestPlaceholders = [
JPUSH_APPKEY: "e1313b5e0150cf749cee9784",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1.debug"
]
} else { //release
manifestPlaceholders = [
JPUSH_APPKEY: "c9a21c73c91f3cef0f4eceb8",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1"
]
}
个人觉得两种可能,一种就是伪代码中所示的,是用applicationId_result
作为最终的applicationId的引用;另一种就是applicationId没有及时赋值,虽然是用applicationId作为应用,但是当完成赋值的时候,已经执行了if语句了。
所以才会出现上面我说到的情况,当然,这是本人愚见,若有哪位大神知道原因和解决方案,欢迎留言指正,谢谢。
6、修改完app/build.gradle
之后,直接在window命令行中输入命令即可:
Y:\>cd Y:\WorkPlaces\AndroidStudioWP\ApplicationDemos\app
Y:\WorkPlaces\AndroidStudioWP\ApplicationDemos\app>gradlew assembleRelease
缺点:
耗时——渠道越多,所用时间越长
......
版本三:整合项目,神器打包
到此,放大招了,成为终极打包版本吧,用到的神器就是Walle,参考:https://github.com/Meituan-Dianping/walle
注意:
参考中给定的渠道channel文件是放在app文件夹同级目录中的,而由于本人的项目中的渠道是自定义的编码,每个产品的第二位都不一样,所以就需要放到各自版本的目录之中,位置如图:
闲话少说,直接上代码:
build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'walle'
def releaseTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
def fname //版本马甲名称
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.ylzt.apcapp"
minSdkVersion 15
targetSdkVersion 25
ndk {
//选择要添加的对应cpu类型的.so库。
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
// 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
}
multiDexEnabled true
versionCode 1
versionName "1.0"
}
signingConfigs {
debug {
storeFile file("debug.keystore")
storePassword "123456"
keyAlias "ylzt"
keyPassword "123456"
}
release {
storeFile file("Y:/ylzt.keystore")
storePassword "123456"
keyAlias "ylzt"
keyPassword "123456"
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles 'proguard-rules.pro'
}
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
// 是否进行混淆
minifyEnabled true
// 混淆文件的位置
proguardFiles 'proguard-rules.pro'
signingConfig signingConfigs.release
shrinkResources true // 移除无用的resource文件
}
}
productFlavors {
base { //基础版本
applicationId "com.ylzt.apcapp.debug"
// applicationId "com.ylzt.apcapp"
fname = "base" //需要手动修改,以改变要打包的项目
versionCode 20
versionName "2.0"
if (applicationId.endsWith('.debug')) { //debug
manifestPlaceholders = [
JPUSH_APPKEY: "e3b5c9a23c91fe0154ce1e97",
JPUSH_PKGNAME: "com.ylzt.apcapp.debug"
]
} else { //release
manifestPlaceholders = [
JPUSH_APPKEY: "0cf7784913f4ec3ce1cf0eb8",
JPUSH_PKGNAME: "com.ylzt.apcapp"
]
}
}
mP1 { //马甲1
applicationId "com.ylzt.apcapp.mp1.debug"
// applicationId "com.ylzt.apcapp.mp1"
// fname = "mP1" //需要手动修改,以改变要打包的项目
versionCode 10
versionName "1.0"
if (applicationId.endsWith('.debug')) { //debug
manifestPlaceholders = [
JPUSH_APPKEY: "e1313b5e0150cf749cee9784",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1.debug"
]
} else { //release
manifestPlaceholders = [
JPUSH_APPKEY: "c9a21c73c91f3cef0f4eceb8",
JPUSH_PKGNAME: "com.ylzt.apcapp.mp1"
]
}
}
}
//变种过滤
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if (!names.contains(fname)) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
testCompile 'junit:junit:4.12'
/*依赖module*/
compile project(':..:LibsModules/:core')
compile 'com.umeng.analytics:analytics:latest.integration'
//wx
compile files('libs/libammsdk.jar')
compile files('libs/SocialSDK_WeChat_Simplify.jar')
compile 'com.meituan.android.walle:library:1.1.4'
}
//第三版打包方式:walle打包方式,详见https://github.com/Meituan-Dianping/walle
walle {
// 指定渠道包的输出路径
apkOutputFolder = new File("${project.buildDir}/outputs/channels");
// 定制渠道包的APK的文件名称
apkFileNameFormat = '${flavorName}_0${versionCode}_${channel}.apk';
// 渠道配置文件
// channelFile = new File("${project.getProjectDir()}/channel")
channelFile = new File("${project.getProjectDir()}/src/${fname}/channel")
}
注:其中需要手动修改的地方已经写了注释,即以fname变量来控制打包工程,在变种过滤(variantFilter)中已经做了相应的判断
用这种方式打包确实很爽,不到一分钟,60个包全部完成,妈妈再也不用担心我打包耗时了~~~
结语
经过三个版本的演化,从中学到了不少,gradle的知识还有很多需要学习,希望大家能共同进步。
当然了,其中还有许多要改进的地方,还没有完全做到全自动化,有兴趣的同学可以看一看云打包的相关资料,有好的学习资料也希望大家多多分享。
相关资料:
https://developer.android.com/studio/build/build-variants.html(需要翻墙)
https://developer.android.com/studio/build/manifest-merge.html(需要翻墙)
https://segmentfault.com/a/1190000002910311
https://github.com/Meituan-Dianping/walle
http://tech.meituan.com/android-apk-v2-signature-scheme.html