配色: 字号:
Gradle 实现 Android 多渠道定制化打包
2016-09-19 | 阅:  转:  |  分享 
  
Gradle实现Android多渠道定制化打包最近在项目中遇到需要实现Apk多渠道、定制化打包,Google、百度查找了一些资料,成功实现了上述功能,在此记录以备不时之需,温故而知新,可以为师矣~

需求可以总结如下:?

如何实现多个Apk安装在同一设备

在之前的印象中,同一个应用在同一设备上只能安装一个,除非手动修改AndroidManifest.xml文件中的包名(?package?),但这么做的后果就是新的应用真的是新的应用,旧版应用再也收不到更新。而现在你通过Gradle,你可以轻松构建多个不同版本的应用,并且在同一设备上安装使用。

这里要用到?productFlavors?,productFlavors可以用来自定义应用构建版本,我们可以用其?applicationId?属性来实现多个Apk安装在同一设备上。

build.gradle中部分配置代码如下:

android{

compileSdkVersion24

buildToolsVersion"24.0.1"



//默认配置,所有productFlavors都会继承defaultConfig中配置的属性

defaultConfig{

//默认的applicationId,一般与AndroidManifest.xml文件package属性相同

applicationId"com.littlejie.multichannel"

minSdkVersion15

targetSdkVersion24

versionCode1

versionName"1.0"

}



//productFlavors定义了一个应用的自定义构建版本

//一个单一的项目可以同时定义多个不同的flavor来改变应用的输出。

//productFlavors这个概念是为了解决不同的版本之间的差异非常小的情况,通常用于区分同一个应用的不同渠道/客户等,可包含少量业务功能差别。

//productFlavors中的flavor不能跟buildType中的一样,否则会报:"ProductFlavornamescannotcollidewithBuildTypenames"

productFlavors{



//默认版本,不设置applicationId,继承defaultConfig中的配置

flavors_default{

}



//开发版本,applicationId替换为com.littlejie.multichannel.dev

flavors_dev{

applicationId"com.littlejie.multichannel.dev"

}



//发布版本,applicationId替换为com.littlejie.multichannel.dev

flavors_release{

applicationId"com.littlejie.multichannel.release"

}

}

}

MainActivity.java:

publicclassMainActivityextendsActivity{



privatestaticfinalStringTAG=MainActivity.class.getSimpleName();



@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);



Log.d(TAG,"packagename="+this.getPackageName());

}

}

在AndroidStudio中执行如下命令:

//打debug包,gradle命令会在后面`gradletask`中详细讲述

gradlecleanassembleDebug

打包完成后,将Apk安装到模拟器(adbinstallname.apk),运行,log如下:

flavors_default:

09-1722:43:55.39019747-19747/com.littlejie.multichannelD/MainActivity:packagename=com.littlejie.multichannel

flavors_dev:

09-1722:11:30.8602638-2638/com.littlejie.multichannel.devD/MainActivity:packagename=com.littlejie.multichannel.dev

flavors_release:

09-1722:44:55.61020650-20650/com.littlejie.multichannel.releaseD/MainActivity:packagename=com.littlejie.multichannel.release

从这里可以看出,不同flavor的packagename被applicationId替换掉了,而且同一个模拟器上可以同时安装以上三个应用。

下面我们再看看AndroidManifest.xml中发生了什么变化。这里需要用到aapt来查看AndroidManifest.xml的信息:

//输出apk的AndroidManifest.xml文件的信息

aaptdumpxmltree.apkAndroidManifest.xml

关于aapt使用的更多用法,可以阅读这篇博文:使用aapt查看apk的各种信息

下面是flavors_dev版本的信息,可以看出Java源文件的包名并没有发生改变,而package属性的值被替换为applicationId了。

lishengjiedeMacBook-Pro:apklittlejie$aaptdumpxmltreemultichannel-flavors_dev-debug.apkAndroidManifest.xml

N:android=http://schemas.android.com/apk/res/android

E:manifest(line=2)

A:android:versionCode(0x0101021b)=(type0x10)0x1

A:android:versionName(0x0101021c)="1.0"(Raw:"1.0")

//此处package的值已替换成applicationId的值

A:package="com.littlejie.multichannel.dev"(Raw:"com.littlejie.multichannel.dev")

A:platformBuildVersionCode=(type0x10)0x18(Raw:"24")

A:platformBuildVersionName=(type0x4)0x40e00000(Raw:"7.0")

E:uses-sdk(line=7)

A:android:minSdkVersion(0x0101020c)=(type0x10)0xf

A:android:targetSdkVersion(0x01010270)=(type0x10)0x18

E:application(line=11)

A:android:theme(0x01010000)=@0x7f08008e

A:android:label(0x01010001)=@0x7f060020

A:android:icon(0x01010002)=@0x7f030000

A:android:debuggable(0x0101000f)=(type0x12)0xffffffff

A:android:allowBackup(0x01010280)=(type0x12)0xffffffff

A:android:supportsRtl(0x010103af)=(type0x12)0xffffffff

//Activity的包名还是原来AndroidManifest.xml中申明的

E:activity(line=17)

A:android:name(0x01010003)="com.littlejie.multichannel.MainActivity"(Raw:"com.littlejie.multichannel.MainActivity")

E:intent-filter(line=18)

E:action(line=19)

A:android:name(0x01010003)="android.intent.action.MAIN"(Raw:"android.intent.action.MAIN")

E:category(line=21)

A:android:name(0x01010003)="android.intent.category.LAUNCHER"(Raw:"android.intent.category.LAUNCHER")

applicationId的原理可以理解为在gradle打包的时,动态合并属性,将package替换为applicationId指定的值,但并不会替换Java文件的包名,包括生成的R文件(可以去对应module下的build/generated目录下查看对应flavor的R文件)。

Android官方文档原文如下:

Therefore,wehavedecoupledthetwousagesofpackagename:

Thefinalpackagethatisusedinyourbuilt.apk''smanifest,andisthepackageyourappisknownasonyourdeviceandintheGooglePlaystore,isthe"applicationid".

ThepackagethatisusedinyoursourcecodetorefertoyourRclass,andtoresolveanyrelativeactivity/serviceregistrations,continuestobecalledthe"package".

补充:ApplicationIdversusPackageName

替换AndroidManifest.xml中的属性

这里可以参考友盟统计SDK中使用的方案。该方案通过在AndroidManifest.xml文件中?application?标签下指定??设置占位符来实现动态替换属性值。



占位符形如${name},在最终执行AndroidManifest.xml文件合并的时候,占位符会被build.gradle中对应值取代。build.gradle的配置需要用到上节讲到的productFlavors的manifestPlaceholders?属性,?manifestPlaceholders?属性直译过来就是清单文件占位符。

下面是?build.gradle?的节选代码:

productFlavors{



//将AndroidManifest.xml文件中的${UMENG_CHANNEL}替换为default

flavors_default{

manifestPlaceholders=[UMENG_CHANNEL:"defalut"]

}



flavors_dev{

applicationId"com.littlejie.multichannel.dev"

manifestPlaceholders=[UMENG_CHANNEL:"dev"]

}



flavors_release{

applicationId"com.littlejie.multichannel.release"

manifestPlaceholders=[UMENG_CHANNEL:"release"]

}



}

如果你要替换多个属性,则只需要将?manifestPlaceholders?的写法如下:

manifestPlaceholders=[VALUE_NAME1:"value",VALUE_NAME2:"value"]

补充:关于AndroidManifest文件合并规则可以查看?官方文档

替换资源文件

多渠道打包的时候可能会碰到这种情况:每个应用市场的启动页图标、应用名称可能会有点小出入,更有甚者,连布局都不一样。这时候我们该怎么办呢?

有一种解决办法就是:在代码里进行判断,根据渠道的不一样,加载不同的图片和布局,这是一种解决办法。但是当渠道有很多时,代码就会变得很难维护,而且指定渠道用到的资源文件都会被打入所有Apk中。所以这个方法并不值得推荐。那么,有什么好的解决办法呢?

办法Google早就给我们想好了,而且相当简单,那就是:在main的同级目录下创建以渠道名命名的文件夹,然后创建资源文件(路径要与main中的一致),然后打包的时候gradle就会自己替换或者合并资源。

例如,App的默认icon路径为?main\res\mipmap-hdpi\ic_launcher.png?,那么flavors_dev的路径就为?flavors_dev\res\mipmap-hdpi\ic_launcher.png?,打包flavors_dev渠道的时候会自动替换图片。

对于资源合并,如果在main下的strings.xml内容为:



MultiChannel

我是string,我暂时没被合并



在flavors_dev下的strings.xml内容为:



我是dev_string,我会把string合并



当打flavors_dev渠道包时,最终strings.xml会变成:



MultiChannel

我是dev_string,我会把string合并



以上特性可以用来替换Apk的应用名称和应用图标,这比使用前面讲到的占位符方便很多。同理,替换图片和合并颜色的原理也相似。

多渠道使用独立签名

多渠道打包的时候,可能每个渠道包的签名都必须不一样,真正做到定制化,那么,怎么实现每个渠道包使用指定的签名呢?

平时我们打包的时候是这样的:

signingConfigs{

release{

storeFilefile("签名文件路径")

storePassword"storePassword"

keyAlias"keyAlias"

keyPassword"keyPassword"

}



}



buildTypes{

release{

minifyEnabledtrue

proguardFilesgetDefaultProguardFile(''proguard-android.txt''),''proguard-rules.pro''

shrinkResourcestrue

//指定打release包时使用的签名文件

signingConfigsigningConfigs.release

}



//如果debug包需要测试诸如微信、地图等第三方sdk,则可以指定debug包使用release包的签名

//debug{

//signingConfigsigningConfigs.release

//}

}

而给每个渠道包指定签名其实也差不多。

Google官方原话:

Thisenableseitherhavingallreleasepackagessharewww.hunanwang.netthesameSigningConfig,bysettingandroid.buildTypes.release.signingConfig,orhaveeachreleasepackageusetheirownSigningConfigbysettingeachandroid.productFlavors..signingConfigobjectsseparately.

大意就是,在buildType下指定签名的具体属性,形如?android.productFlavors..signingConfigsigningConfigs.?,前一个??指代在productFlavors中定义的flavor,后一个??指代在signingConfigs定义的属性。值得注意的是,signingConfigs必须定义在buildType之前。

以下是build.gradle的配置节选:

//定义签名属性

signingConfigs{

flavors_default{

//如果签名文件在项目的根目录下,则可以这么写

storeFilefile("../littlejie.jks")

storePassword""

keyAlias""

keyPassword""

}



flavors_dev{

storeFilefile("../littlejie_dev.jks")

storePassword""

keyAlias""

keyPassword""

}

}



buildTypes{

release{

minifyEnabledtrue

proguardFilesgetDefaultProguardFile(''proguard-android.txt''),''proguard-rules.pro''

shrinkResourcestrue



//多个flavor,指定flavor使用指定签名

productFlavors.flavors_default.signingConfigsigningConfigs.flavors_default

productFlavors.flavors_dev.signingConfigsigningConfigs.flavors_dev

}



//如果debug包需要测试诸如微信、地图等第三方sdk,则可以指定debug包使用release包的签名

//debug并不能设置多个签名

//debug{

//productFlavors.flavors_default.signingConfigsigningConfigs.flavors_default

//productFlavors.flavors_dev.signingConfigsigningConfigs.flavors_dev

//}

}

下面我们来验证下生成的包的签名是否正确,查看签名我们会用到如下两个命令:

//查看签名文件的属性

keytool-list-keystore签名文件



//查看apk的签名,需要提前解压apk,获取CERT.RSA(位于解压目录下/META-INF下)

//以下命令行是在apk解压目录下执行

keytool-printcert-fileMETA-INF/CERT.RSA

更多keytool命令使用可以查看?官方文档

首先,我们来看下littlejie.jks的信息:

lishengjiedeMacBook-Pro:AndroidDemolittlejie$keytool-list-keystorelittlejie.jks

输入密钥库口令:



密钥库类型:JKS

密钥库提供方:SUN



您的密钥库包含1个条目



littlejie,2016-9-18,PrivateKeyEntry,

证书指纹(SHA1):A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84

解压multichannel-flavors_default-release.apk,查看CERT.RSA信息

lishengjiedeMacBook-Pro:apklittlejie$keytool-printcert-filemultichannel-flavors_default-release/META-INF/CERT.RSA

所有者:CN=littlejie

发布者:CN=littlejie

序列号:71693e05

有效期开始日期:SunSep1817:20:34CST2016,截止日期:ThuSep1217:20:34CST2041

证书指纹:

MD5:AC:12:83:51:44:FC:82:68:8B:23:7B:E9:12:24:AE:52

SHA1:A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84

SHA256:AD:04:19:5F:92:00:0D:FA:7C:E5:8A:12:57:72:4C:1E:0E:2E:FC:0D:92:28:05:D0:CC:42:FC:93:95:44:88:88

签名算法名称:SHA256withRSA

版本:3

可以发现两者的SHA1值是相等的。

同理,可以查看littlejie_dev.jks和multichannel-flavors_dev-release.apk的签名信息

//littlejie_dev.jks的签名信息

Lishengjiedewww.visa158.comMacBook-Pro:AndroidDemolittlejie$keytool-list-keystorelittlejie_dev.jks

输入密钥库口令:



密钥库类型:JKS

密钥库提供方:SUN



您的密钥库包含1个条目



littlejie,2016-9-18,PrivateKeyEntry,

证书指纹(SHA1):B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2



//multichannel-flavors_dev-release.apk的签名信息

lishengjiedeMacBook-Pro:apklittlejie$keytool-printcert-filemultichannel-flavors_dev-release/META-INF/CERT.RSA

所有者:CN=littlejie

发布者:CN=littlejie

序列号:48346e15

有效期开始日期:SunSep1817:21:23CST2016,截止日期:ThuSep1217:21:23CST2041

证书指纹:

MD5:15:E9:E1:67:AB:33:8B:04:A4:C3:D0:05:8F:A6:35:37

SHA1:B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2

SHA256:96:A5:14:EC:28:25:32:0D:3E:D0:DB:D0:84:06:E7:9C:17:D7:91:83:A4:51:93:AB:34:3E:D9:FD:C5:FA:A1:8E

签名算法名称:SHA256withRSA

版本:3

但是这里有个问题,就是这种给某个flavor指定签名的方法对debug无效,有兴趣的同学可以看上述注释掉的debug签名部分配置。简单来说,debug签名只能指定一个或者使用默认的debug签名。

若哪位大神有解决方案,欢迎指出~

这里再做几点补充:

多渠道使用独立签名,打包时千万不要使用AndroidStudio中Build菜单下的GenerateSignedAPK,因为当你使用这个打包的时候,AndroidStudio会让你指定使用的签名文件,so你就等着哭吧~楼主因为这个折腾了半天。解决方法就是使用gradletasks。传送门:AndroidGradleBuildTasks

鉴于第一点中的传送门需要FQ,所以在这里简单介绍一下?AndroidGradleBuildTasks?的使用。

打全部包:?gradleassemble

打全部Debug包:?gradleassembleDebug?,可以简写为?gradleaD?,前提是没有相同缩写的参数

打全部Release包:?gradleassembleRelease,可以简写为?gradleaR

打指定flavor包:?gradleassemble(flavor)(Debug|Release)

打包完成后安装(设备上没有安装该apk,否则会失败,而且只能指定flavor,不然也会失败):?gradleinstall(flavor)(Debug|Release)

打包前先clean一下(在测试的时候很必要,如果不clean的话,可能会导致某些小修改不会及时打入新包):?gradlecleanassembleDebug

利用Gradle修改构建版本号

楼主表示对Groovy不是很熟,所以利用Gradle自动修改构建版本这个就先留着,我先去研究几天~

总结

以上就是自己在使用Gradle实现Android多渠道打包时碰到的问题,Android官方关于使用Gradle的已经很详细了,自己总结的只是一点皮毛,有时间要去自习研读下。

工作一年多,愣是没有写博客做总结,好多东西都是用过就忘,下次要用再找,没有成体系的Android知识结构,对工资不满意,可就连想跳槽面试都没底气。这次写这篇博客画了思维导图,自以为逻辑清晰了,可是真正要把这些东西讲述清楚,还真是一件麻烦的事~看来,自己还有很长的路要走~

这段时间自己也在思考,是转行还是去考事业编制,还是继续做Android。转行,除了编程自己好像别的什么也不会,当然自己编程也做的不怎么好。考事业编制,这个可以考虑,毕竟再很多人眼里这是个旱涝保收的职业。继续做Android,这个也不错,除了每次都花大把时间用来改UI,别的都还不错(吐槽产品)。

话说,有没有什么工作,自由、上班时间少、工资高的?当然没有,至少现阶段的自己是接触不到的,所以,骚年,还是努力吧!多读书、多看报、多运动,少吃零食多睡觉~

恩,算是对工作一年多的总结也是吐槽~



献花(0)
+1
(本文系白狐一梦首藏)