花椒Android客户端多变体构建实践

android的多变体构建是指根据不同配置,基于一份代码根据不同需求构建出不同的apk。

目前花椒面临的不同构建需求主要有

  1. 通过配置构建极速版和普通版
  2. 由于历史原因,花椒在某些应用商店需要使用不同的应用名称
  3. 根据渠道特点配置不同功能,如登录前置,启动预览,启动跳转,渠道预装等
  4. 根据商店的广告策略配置sdk

目前花椒Android客户端主要使用的gradle编译属性和product flavor来满足这些需求,目前已经有了较为灵活的构建机制,能够快速响应各方的构建需求。本文就详细介绍这两种技术的原理及在花椒的实践。

product flavor

product flavor是Android Gradle Plugin提供的一种强大的构建配置。它通过配置不同的flavorDimensions,然后组合这些dimension可以构建出不同功能的APK。典型的配置如下:

groovy android { ... buildTypes { debug {...} release {...} }
// 配置dimensions,注意配置的顺序决定了变体中flavor顺序,
  flavorDimensions "api", "mode"

  productFlavors {
    demo {
      // 每个flavor必须配置dimension
      dimension "mode"
      ...
    }

    full {
      dimension "mode"
      ...
    }

    // 每个flavor中都可以配置defaultConfig下的所有配置项目,如minSdkVersion,buildConfigField等
    minApi24 {
      dimension "api"
      minSdkVersion 24
      versionCode 30000 + android.defaultConfig.versionCode
      versionNameSuffix "-minApi24"
      ...
    }

    minApi23 {
      dimension "api"
      minSdkVersion 23
      versionCode 20000  + android.defaultConfig.versionCode
      versionNameSuffix "-minApi23"
      ...
    }
  }
}
...

这里要注意的是productflavors中配置的顺序决定了配置的优先级,如上代码,如果在demo和minApi24中同时配置了minSdkVersion,因为api在flavordimensions中靠前,所以minApi24中的配置会胜出。

那么flavor是如何起作用的呢?我们知道通常的构建都会使用位于src/main目录下的代码,但配置了flavor后我们可以在src下创建以flavor名称命名的文件夹,当构建此flavor时候就会使用此文件夹下的代码。

gradle对代码和配置文件有不同的处理方式:

  1. 对于java代码,不能出现在src/main中,而是需要在每个flavor下的src/main/java下定义。
  2. 对于配置文件,如AndroidMainfest.xml,使用的策略是merge,即每个flavor下只需要配置和src/main下不同的xml节点即可。

在花椒中以上两种配置均有使用,如在配置CTA预装包时,需要对Application代码进行hook跳转特定Activity,就需要分别在ctaEnable目录下创建BaseapplicationHook,在ctaDisable创建BaseapplicationHook,CtaActivity以及AndroidMainfest.xml.目录结构如下图:

此处需要注意的是,某些情况下希望在flavor中删除src/main/AndroidMainfest.xml中的某些配置可以使用tools:node="remove"来实现。

android studio variant窗口

在使用flavor后,应用代码就有了多个variant,同时就会有多个sourceset。在android studio中一次只能显示一个variant代码,我们可以通过Build Variants选择需要的variant。需要注意的是对于module依赖,需要注意variant的传递,比如如果主module 的variant是ctaEnableDebug,其依赖的子module如果会优先选择ctaEnable的variant。但是通常情况下子module没有对应的variant,此时会fallback到debug。

使用gradle构建属性

product flavor虽然很强大,但依然存在一些问题:

  1. 配置相对复杂,当构建需求小且多的时候最后的变体名称会特别长
  2. 要求对每个dimension的变化进行枚举。对某些不能枚举情况,如配置启动跳转链接这种情况就难以实现。
  3. flavor分割了代码,如果修改代码必须保证在每个变体上都能编译通过。

针对以上问题,花椒在对某些简单的构建需求使用gradle构建属性来实现。这些需求一般有如下特点:

  1. 不涉及对resource,AndroidMainfest.xml的修改
  2. 通常比较简单,如开关功能,配置跳转链接等

gradle构建属性的主要原理是:

  1. 在构建时候使用-P参数将希望配置的参数加到编译命令上
  2. 使用project.hasProperty()判断是否存在属性,使用project.property("")获取参数值
  3. 在build.gradle中直接使用这些值。如果需要在java代码中使用,通过buildConfigField配置到BuildConfig上。

以下代码展示了如何通过构建属性配置apk的appname

groovy String appNameX = "花椒" String appNameX*P = "appNameX" def applicationName = "huajiao" if (project.hasProperty(appNameX*P)) { appNameX = project.property(appNameX\_P) }

defaultConfig{ resValue("string", "appNameX", appNameX) }
xml <application android:name="com.huajiao.base.BaseApplication" android:allowBackup="false" android:icon="@drawable/logo180" android:label="@string/appNameX" android:largeHeap="true" android:persistent="true" android:theme="@style/AppBaseTheme" android:networkSecurityConfig="@xml/network_security_config" tools:replace="android:allowBackup,android:label">

通过以上配置我们使用gradlew -PappNameX=花椒直播 assemble打包时候,配置文件中的appNameX的值就会变成花椒直播并通过resValue配置到string.xml中,这样AndroidMainfest.xml中的label名称在最终编译中就会是花椒直播,从而实现了替换应用名称的目的。

除了在xml中使用构建属性外,我们还可以通过buildconfigField将属性传递到Buildconfig类上,这样在java代码中通过引用buildConfig就可以获取构建属性了。

例如在开发中我们可以对某些模块的日志进行开关,下面代码展示了如何通过构建属性打开或关闭统计日志。

groo boolean showStatisticLog = false

if (project.hasProperty("showStatisticLog")) { showStatisticLog = true } defaultConfig { versionCode hostVCode versionName "${hostVName}" buildConfigField "boolean", "SHOW*STATISTIC*LOG", String.valueOf(showStatisticLog) } 
java if (BuildConfig.SHOW_STATISTIC_LOG) { QHStatAgent.setLoggingEnabled(true); QHConfig.setDebugMode(context, true); QosSdk.enableLog(true); }

这里需要注意gradle的构建属性的配置,如果使用命令行可以直接加上-Pkey=value,如果使用android stuido可以在Settins>Compiler中设置。

构建变体实践

使用setIgnore过滤变体

在实际使用中,某些flavor组合出来的variant可能并不是我们需要的。我们可以在variantFilter配置中过滤掉不需要的variant。

variantFilter { variant -> def name = variant.flavors*.name def buildTypeName = variant.buildType.name if (name.contains("friendsChannel")) { setIgnore(!(name.contains("smEnable") && name.contains("loginForceFullScreenEnable") && name.contains("googlePlayDisable") && name.contains("ctaDisable"))) } }

对变体产生的apk命名

当我们综合使用flavor和构建属性时候,最终可能产生有不同特性的apk,如果不加配置很容易产生重名的apk导致互相覆盖,并且在交付的时候容易出现混乱。在花椒的构建实践中我们尽可能的把本次构建的特性写入apk的文件名称中。在gradle中可以使用applicationVariants的outputFilename来配置。

groo applicationVariants.all { variant -> def buildId = "${applicationName}" + "${buildName.length() > 0?"_"+buildName:""}" + "_${variant.buildType.name}" + "_${defaultConfig.versionName}" + "${loginRequestX?"_loginRequest":""}" + flavorNames + "_targetSDK${targetSdk}" + "${huaweiPPS?"_huaweipps":""}" + "${huaweiChannel?"_huaweiChannel":""}" + "${quickStart?"_quickStart":""}" + "${usePhone?"_usePhone":""}" + "${oppoDeepLink?"_oppoDeepLink":""}" + "${desc.length() > 0?"_"+desc:""}" + "${buildNum.length() > 0?"_"+buildNum:""}" variant.outputs.all { outputFileName = "${buildId}.apk" } }

https://mp.weixin.qq.com/s/W_qJExVL0H1O5fD0jp54NA

Android 记一次解决问题的过程

之前我写过一篇文章,介绍我在GitHub开源的滑动控件 ConsecutiveScroller 是如何实现布局吸顶功能的。有兴趣的朋友可以去看一下:Android滑动布局ConsecutiveScrollerLayout实现布局吸顶功能。

发布于:6天以前  |  65次阅读  |  详细内容 »

Android内存异常机制(用户空间)_NE

常见的Android稳定性异常,有内核异常和Android层异常。内核异常也就是常说的“kernel panic”,简称KE异常;Android层异常又分为java层crash和Native层crash,简称JE、NE异常。 上篇文章介绍了JE异常的抓取机制和处理方式,本文再讲一下NE异常。

发布于:1月以前  |  357次阅读  |  详细内容 »

Android-模块化-面向接口编程

随着业务的发展,工程的逐渐增大与开发人员增多,很多工程都走向了模块化、组件化、插件化道路,来方便大家的合作开发与降低业务之间的耦合度。现在就和大家谈谈模块化的交互问题,首先看下模块化的几个优势。

发布于:2月以前  |  667次阅读  |  详细内容 »

Android SurfaceView 播放gif

Android SurfaceView 是Android系统中的高级组件,它有自己的绘制界面,可以在一个独立的线程进行UI的绘制, 因此不会阻塞主线程,这也是我们使用SuefaceView播放gif图片的原因。

发布于:2月以前  |  484次阅读  |  详细内容 »

Android Studio 生成so文件 及调用

so文件是C、C++的函数库,在Android中 调用这些库,使用的是JNI( Java Native interface) JNI 可以使Java程序调用本地程序或者库(一般是使用C、C++ 或者汇编语言编写)。 这篇文章 会介绍 使用Android Studio 如何生成so文件,及如何使用so

发布于:2月以前  |  612次阅读  |  详细内容 »

Android 保活从入门到放弃:乖乖引导用户加白名单吧(附7大机型加白示例)

IM在Android上的保活问题经常在即时通讯网的论坛和技术群里被讨论,自从Android 8.0后系统大大降低了后台运行应用的保活容忍度(详见《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》),保活从黑科技横行的时代进入了技术蛮荒阶段,真要实现保活,技术难度越来越大。

发布于:2月以前  |  652次阅读  |  详细内容 »

安居客 Android APP 走向平台化 | 开发者说·DTalk

安居客 Android App 距离上次的模块化/组件化重构已经两年多了,重构之后很好的支撑了两年多以来的业务发展。但这个世界总是在向前走的,没有任何一种架构能够一劳永逸的解决所有问题,外部环境的不断变化相应的也要求项目架构做出改变,以此来应对环境变化所带来的挑战。

发布于:2月以前  |  496次阅读  |  详细内容 »

Android View 体系竟然还能这么理解?

很多小伙伴可能在学习view的绘制流程源码的时候有点抓不住重点,所以在分析代码的时候绕来绕去脑袋晕乎乎的。今天我就来给大家化繁为简,只关注它最核心的东西。

发布于:2月以前  |  655次阅读  |  详细内容 »

移动端常见崩溃指标

崩溃分析,是将 Android 和 iOS 平台常见的 APP 崩溃问题进行归类分析,帮助企业根据崩溃指标快速发现、定位问题。

发布于:3月以前  |  633次阅读  |  详细内容 »

Android死锁初探

说到死锁,大家可能都不陌生,每次遇到死锁,总会让计算机产生比较严重的后果,比如资源耗尽,界面无响应等。

发布于:3月以前  |  914次阅读  |  详细内容 »

Android AES加密(Kotlin)

halo~最近工作上写的东西比较简单,感觉分享不出来,最近刚好看到数据加密这一块,感觉挺不错的,也挺好用的,所以下面分享给大家!

发布于:3月以前  |  703次阅读  |  详细内容 »

最多阅读

简化Android的UI开发 10月以前  |  269715次阅读
Android设计与开发工作流 10月以前  |  2745次阅读
Google Enjarify:可代替dex2jar的dex反编译 1年以前  |  2613次阅读
30分钟搭建一个android的私有Maven仓库 1年以前  |  2317次阅读
Android多渠道打包工具:apptools 1年以前  |  2208次阅读
Google Java编程风格规范(中文版) 1年以前  |  2181次阅读
Android权限 - 第一篇 1年以前  |  2118次阅读
Android UI基本技术点 1年以前  |  2114次阅读
Stetho 1年以前  |  2057次阅读
2015 Google IO带来的新 Android 开发工具 1年以前  |  1939次阅读
你应该知道的布局和属性 1年以前  |  1904次阅读
听FackBook工程师讲*Custom ViewGroups* 1年以前  |  1886次阅读
MVP在Android平台上的应用 1年以前  |  1877次阅读
Gradle小知识#3:任务的顺序 1年以前  |  1862次阅读