[转]app拆分,多产品打包实录
1. 1前言
公司有需求app需要拆分为两个:云部署版和企业版。
a.云部署版就是原app ,保持不变;
b.企业版是新app,与云部署的界面,功能有差异,并且不上架应用市场,只在企业内网使用。用户手机上可以同时安装这两个app。
1-1-1-1.
1-1-1-2. 拆分策略
根据需求,首先两个app肯定是一个项目,不可能再copy一份代码分开维护。那么至少要处理这些事务:
- gradle要支持差异化打包,通过AndroidStudio要能够打包出两个apk,并且这两个app的报名不能一样,才能同时安装在同一部手机上。
- 代码中有一个全局字段,区分两个版本做界面的差异化(app名称,图标),一些功能
- 由于我们app有推送服务 ,要保证两个app的推送互不干扰,点击推送消息的通知栏能够跳到指定app
- 我们app有QQ/微信分享功能,假设分享一个网页到QQ,那么点击qq里面的消息要能正确启动并跳转到当前分享的app。
- 如果你们使用了teamcity,还要差异化编译脚本。(注意:当前两个项目是同一个svn分支)
2. 2解决方案
2.1 gradle要支持差异化打包
先思考下什么样的两个app能够安装在同一部手机上?很多人说包名不同。
其实这个是eclipse时代的说法,到了AndroidStudio上准确来讲,是ApplicationId不同的app可以安装在同一部手机上。
可以看下AndroidStudio下构建的项目,不同的moduler里面都有一个AndroidManifest.xml文件,并且每个AndroidManifest.xml里面package都不同,这个package影响的是每个moduler里面R.java.xxx的包名,以及这个moduler内所有java文件的包名。不同的moduler这个package肯定不同所以我们在不同的moduler可以使用相同的资源文件名,和java文件名。
再说app目录下build.gradle里面定义的applicationId:
这个值默认跟app目录下manifest.xml里面package一样,如果你不写的话。这个才是决定app唯一性的字段(不仅是本地安装,也包括应用市场判断的依据)。
OK,明白了packageName和applicationId的区别,如何用gradle构建两个app呢?很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
productFlavors { cloud { <span class="">//云部署版本</span> resValue <span class="">"string"</span>,<span class="">"app_name"</span>, <span class="">"MyLuban"</span> resValue <span class="">"string"</span>,<span class="">"app_scheme"</span>, <span class="">"bv4phone"</span> resValue <span class="">"string"</span>,<span class="">"app_link_scheme"</span>, <span class="">"cloud"</span> manifestPlaceholders = [app_icon:<span class="">"@drawable/ic_launcher"</span>] } entp { <span class="">//企业版本</span> applicationIdSuffix <span class="">".entp"</span> resValue <span class="">"string"</span>,<span class="">"app_name"</span>, <span class="">"MyLuban企业版"</span> resValue <span class="">"string"</span>,<span class="">"app_scheme"</span>, <span class="">"bv4phoneentp"</span> resValue <span class="">"string"</span>,<span class="">"app_link_scheme"</span>, <span class="">"entp"</span> manifestPlaceholders = [app_icon:<span class="">"@drawable/ic_launcher_entp"</span>] } } |
关键代码是:(其他代码先忽略)
1 2 3 |
<span class="">//企业版本</span> applicationIdSuffix <span class="">".entp"</span> |
给企业版的app的applicationId加后缀,就可以和云部署区分开了。注意千万不要改变云部署的applicationId,因为你的app在应用市场就是原来的applicationId,一旦改了就无法再覆盖上架了。
2.2 界面和功能差异化
如何定制不同的app名称:
1 2 |
<span class="">resValue</span> <span class="">"string"</span>,<span class="">"app_name"</span>, <span class="">"MyLuban"</span> |
这个语句可以定义任何不同的String,比如这里名字为app_name,取值MyLuban。然后在manifest.xml中使用:
1 2 |
android:label=<span class="">"@string/app_name"</span> |
定制不同的app图标:
1 2 |
<span class="">manifestPlaceholders</span> = [app_icon:<span class="">"@drawable/ic_launcher"</span>] |
在manifest.xml中使用:
1 2 |
android:icon=<span class="">"${app_icon}"</span> |
当我们加入产品差异化编译一次后,在build文件夹的BuildConfig文件可以看到这个app的编译信息:
因此我们可以写一个工具类,来保存当前的产品类型,在Application的onCreate里面初始化:
1 2 3 4 5 6 7 8 9 10 |
<span class="">//APP产品类型</span> <span class="">switch</span> (BuildConfig.FLAVOR){ <span class="">case</span> <span class="">"cloud"</span>: ProductUtil.setProductType(<span class="">CLOUD</span>); <span class="">break</span>; <span class="">case</span> <span class="">"entp"</span>: ProductUtil.setProductType(ENTERPRISE); <span class="">break</span>; } |
ProductUtil里面很简单就是set/get方法,然后其他任何就可以获取当前app的产品类型,继续做界面和功能的差异化。
还有个小问题,此时点击run,AS会编译哪个app呢?
点击run的默认执行:在AS1.5是按照productFlavors 排序从上到下的,执行debug编译
在AS2.1之后是按照首字母排序,比如这里就是执行 cloud-debug版本
更改方式:
2.3 推送差异化
我们app的推送服务是基于webSocket自己实现的,单独开启进程里运行service来实现获取推送。即使使用第三方的推送,原理也差不多。
那么,手机上运行两个app之后,推送服务会相互干扰吗?
很简单看下手机设置里面的应用进程就知道,两个app进程包括启动的推送Servcie进程,完全独立,这样就可以保证app的推送互不干扰而不需要特殊处理。
但是点击推送消息通知栏跳转app,就要区分开了。因为这里的跳转一般是Intent隐式跳转,我们应该为跳转目标Activity的IntentFilter做差异化:
1 2 3 4 5 6 |
<span class=""><intent-filter></span> <span class=""><action android:name="com.myluban.push" /></span> <span class=""><category android:name="android.intent.category.DEFAULT" /></span> <span class=""><data android:scheme="@string/app_link_scheme" /></span> <span class=""></intent-filter></span> |
用两个不同的字段区分data,字段在gradle里面生成(看上文的gradle配置),然后在启动Intent的位置作区分:
1 2 3 4 5 6 7 8 |
<span class="">private</span> String PUSH_SCHEME = <span class="">"cloud"</span>; f(ProductUtil.getProductType()==ProductType.ENTERPRISE){ PUSH_SCHEME = <span class="">"entp"</span>; } Intent jumpIntent = <span class="">new</span> Intent(<span class="">"com.myluban.push"</span>); jumpIntent.setData(Uri.parse(PUSH_SCHEME + <span class="">"://abc"</span>)); <span class="">//...</span> |
2.4 分享差异化
如果你的分享也是打开一个网页,点击网页上的按钮启动自身app。那么,跟推送跳转类似,也是在IntentFilter作区分。只不过要前端配合一下,根据不同产品的分享,设置不同的scheme。
2.5 teamcity编译脚本差异化
teamcity是用于持续集成的工具,主要是方便测试可以随时一键编译最新代码,打包apk。teamcity一般是根据不同的项目,建立不同的编译选项。那这里我们两个app其实是同一个svn分支,怎么区分呢?答案就是建立两套编译脚本,gradle是支持的:(由于每个公司teamcity都不一样,贴出关键代码):
1 2 3 4 5 6 7 8 9 |
set build_cmd=assembleCloudRelease <span class="">if</span> <span class="">"%2%"=="</span>myluban_enterprise<span class="">" ( set apk_prefix=myluban_entp set build_cmd=assembleEntpRelease ) echo //////// 编译release apk //////// set srcPack=%apk_prefix%-release.apk call %workDir%/gradlew.bat -b %workDir%/%projectDir%/build.gradle %build_cmd% -Ptargetdir=%cd%\%workDir%\release -Papkname=%srcPack% -x lint </span> |
其实就是执行2个不同gradle命令。