前言:Android实现多渠道打包,这个问题并不新鲜,解决方案是固定的那么几种,网上的博客也有很多,我这里只是针对近期开发中遇到的坑进行整理,方便自己方便他人。
无疑要实现一个壳工程打出不同样式的包,这个技术解决方案Android已经替我们考虑到了,也就是使用Gradle中的productFlavors,在做定制或适配的时候,不需要建立多个工程、来回切换项目分支、逐个编译apk,使用productFlavors可以帮我们简化这一步操作,快速打包所有项目版本的apk。
productFlavors用处
1.新建工程名为MultiAppDemo,打开app module下的build.gradle文件,在android结构下添加productFlavors,示例如下:
添加完成后,需要gradle同步一下,让我们的配置生效。
这个时候我们点击左侧工具栏中的Build Variants(翻译为编译变体)中可以看到现在对应三种编译类型:
2.完成第一步,接下来需要在app/src下,建立和productFlavors中声明的类型同名的目录,当中分别添加java和res两个目录,示例如下:
我在res目录下又添加了drawable-xxhdpi和valuesu 两个目录,可以看到values目录下有colors.xml和strings.xml资源文件,这里存放的就是对应的产品下的资源文件,稍后会具体看到。
3.分别在productFlavors对应的res/values/colors.xml下添加资源属性,如下图所示:
4.同理,在productFlavors对应的res/values/strings.xml下添加资源属性,如下图所示:
这里对应不同产物的app名称,同时我们需要删除/注释app module下的main/res/strings.xml中的app_name属性,避免冲突。
5.打开activity_main.xml布局文件,进行一些小改动,如图所示:
现在我们可以看到一个雏形,我们在三个productFlavors对应目录下的资源会被找到,这是根据上面提到的Build Variants中选择的编译类型决定的,会自动寻找对应的资源文件中的属性。
6.在MainActivity中设置文字显示当前应用包名:
7.接下来看看AndroidManifest.xml,打开AndroidManifest.xml文件:
注意到现在package="com.multi.app"是这个值,但是这个并不是最终值,最终值是什么呢?马上揭晓!
8.是骡子是马,跑起来看看,当前编译环境选择的是firstappdebug,点击运行,这个时候找到如图目录下的文件:
在我们的app/build/intermediates/manifests/full/firstapp/debug下,有我们最终生成的清单文件,这个才是最终的输出产物,可以看到这个时候package的值已经变成了我们在productFlavors中给firstapp设置的applicationId的值了,这里简单提一下,因为清单文件会在打包的时候汇总所有module下的AndroidManifest.xml文件,去重,然后生成一个最终产物,如果有不了解的同学可以自行查阅相关资料。
9.我们选择不同的编译类型,各自运行起来,截个图做个对比,如图所示:
可以看到我们并没有手动创建3个布局文件,而是在布局中引用了@color/main_color和@string/app_name,在选择对应的Build Variants的时候,就会加载对应Variants目录下的资源,并且获取到的包名也是不一样的,这就达到了我们开头说的多渠道打包的原理,一套代码,多套资源,根据不同编译变体自动选择所需资源,节省了我们的开发工作量。
10.我们这里只做了简单的string和color的演示,如果大家有图片资源的需要,在对应目录下创建drawable文件夹,然后在对应的drawable文件夹添加对应的图片资源即可,操作方式一致,但是有一点,所有多渠道资源,文件名都必须保持一致,否则同一套代码在编译不同产物时会找不到对应的资源文件,会导致编译失败!
还记得我们上面看到的AndroidManifest.xml中的package中的值在最终产物里会变成和applicationId中设置的一样么,那么你会不会理所当然的认为BuildConfig的路径也是和applicationId一样呢?
答案是否!
下面来科普一下:
需要注意的是:
来看看我们代码中的:
可以看到BuildConfig的包名就是package中的那个,并没有因为applicationId改变了而变化,所以这会导致什么问题呢?
问题:
在某些框架初始化的时候,会在初始化方法中通过反射机制来获取BuildConfig,通过BuildConfig获取一些配置信息,但是当中做了一些判断,如果我们没有传入包名的时候,他们会根据context.getPackageName()拼接“BuildConfig”字段,然后通过反射获取,但是如果我们设置了applicationId和package不一致的话,这个时候拿到的packageName其实是applicationId的值(比如我们现在的com.zy.special.firstapp),然后用"com.zy.special.firstapp"+"."+“BuildConfig”,通过反射去获取就会抛出ClassNotFoundException异常,因为我们的BuildConfig路径只是com.multi.app.BuildConfig,举个例子:
static void initAppDefault(Context context,String packageName) {
mAppContext = context;
String[] modules = null;
try {
if(TextUtils.isEmpty(packageName)){
packageName = context.getPackageName();
}
Class> buildConfig = Class.forName(packageName + DOT + "BuildConfig");
if (buildConfig == null) return;
Field allModules = buildConfig.getField(ALL_MODULES);
String modules_name = (String) allModules.get(buildConfig);
modules = modules_name.split(",");
if (modules.length == 0) return;
} catch (ClassNotFoundException e) {
LogUtil.e(TAG, "Initialization failed, have you forgotten to apply plugin in application module?", e);
} catch (Exception e) {
LogUtil.w(TAG, e.getMessage());
}
......
}
可以看到这就是我们说的这种情况,所以这种问题怎么解决呢?那就是在框架初始化的时候需要手动传入和AndroidManifest.xml中package一样的值,这个参数必须和package值保持一致,这样才能保证BuildConfig路径能被正确的找到。
通过上述的示例,相信大家可以很直观的感受到productFlavors带来的便利,但是也可能有一些大家没有注意到的坑,另外有一点就是AndroidManifest.xml中package值无法通过manifestPlaceholders这种方式占位使用,编译报错找不到包名,毕竟这个和我们的代码目录息息相关。
有问题大家可以留言关注,共同探讨,看到回复,感恩~