前言:Android实现多渠道打包,这个问题并不新鲜,解决方案是固定的那么几种,网上的博客也有很多,我这里只是针对近期开发中遇到的坑进行整理,方便自己方便他人。

一、初识productFlavors

无疑要实现一个壳工程打出不同样式的包,这个技术解决方案Android已经替我们考虑到了,也就是使用Gradle中的productFlavors,在做定制或适配的时候,不需要建立多个工程、来回切换项目分支、逐个编译apk,使用productFlavors可以帮我们简化这一步操作,快速打包所有项目版本的apk。

productFlavors用处

  • 创建不同的产品并为不同产品分配专有属性
  • 设置不同代码引用
  • 先在src目录下建立对应的文件夹,比如java代码则建立product/java,res文件夹则建立product/res(product为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文件夹添加对应的图片资源即可,操作方式一致,但是有一点,所有多渠道资源,文件名都必须保持一致,否则同一套代码在编译不同产物时会找不到对应的资源文件,会导致编译失败!

三、applicationId和BuildConfig路径不匹配的问题

还记得我们上面看到的AndroidManifest.xml中的package中的值在最终产物里会变成和applicationId中设置的一样么,那么你会不会理所当然的认为BuildConfig的路径也是和applicationId一样呢?

答案是

下面来科普一下:

  • 每个BuildConfig的类名是由这个buildConfig所在module(不管是app还是lib)的AndroidManifest中的package属性决定,和应用的包名无关。
  • 比如 package=“com.xxx.yyy” 则BuildConfig的类名为 com.xxx.yyy.BuildConfig

需要注意的是:

  • build.gradle可以通过applicationId修改应用包名,但是BuildConfig类名不会变
  • build.gradle可以通过buildConfigField给BuildConfig添加属性,用于代码配置,每个module的BuildConfig只能获取自己module的配置。

来看看我们代码中的:
在这里插入图片描述
可以看到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这种方式占位使用,编译报错找不到包名,毕竟这个和我们的代码目录息息相关。

有问题大家可以留言关注,共同探讨,看到回复,感恩~

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐