[关闭]
@zyl06 2017-03-09T19:28:35.000000Z 字数 8205 阅读 1855

网易 Android 工程模板化实践

Android


0 背景

我们网易前端技术部 - 移动技术组作为公司的移动端基础技术部门,主要为其他部门提供解决方案、技术支持和产品孵化。在几年的积累过程中,我们拥有一些自己的框架和 SDK,如轻应用框架、热更新 SDK、网络请求库、本地存储库、页面管理等,服务过网易新闻、云音乐、考拉、易信等亿级产品,先后孵化过青果摄像头、二次元Gacha、严选等重要产品。

在多年的Android开发中,对于 Android 端产品开发,我们有如下几点体会:

  1. 产品孵化排期紧张

    产品经理一般关心的是具体的业务逻辑,而前期基础模块的搭建,如各模块如何组织,使用代码结构如何选择,图片、网络、本地存储等选用哪个 sdk 等,一般不会有专门排期。

  2. 基础模块的需求具有相似性

    内容型产品,其搭建的基础模块基本上都会包含图片显示、网络请求、本地存储、通信等。

  3. 基础模块的选型和工具类具有可重用性

    网上相关的第三方库有很多,当然一般的公司也是会有自己开发或者维护的各个基础 SDK。很多时候,SDK 选型会更偏向于自己公司开发维护的 SDK,或者选择自己最熟悉,或最主流、最可靠的 SDK。因此当开发多个相同类型产品时,这里的技术选型是可重用的。

  4. 网络请求的代码具有机械性

    客户端开发需要根据网络接口协议,编写相关的 GETPOST 等请求代码和对应的 JavaBean,这部分的代码编写其实是非常机械的。

1 网易工程模板是什么?

对于各个基础模块,我们团队封装了自己的 SDK,如网络库、本地存储库、页面管理库、图片库等。使用我们的工程模板生成的初始工程,就已经包含了我们提供的基础模块,产品团队的开发不需要再花费重复的时间做技术调研、选型、SDK封装集成等工作,而只需要关心自己的业务逻辑编写。我们期望产品团队只需 1 分钟就能得到自己的初始工程,并能马上投入业务逻辑开发,既能缩短开发周期,也能保证工程代码质量。

此外,我们也提供了 Android Studio 插件 (NEIPlugin),集成插件后,就能在 Android Studio 中通过菜单点击自动下载集成我们的工程模板,也能自动生成网络请求相关的代码。

image

工程模板 HTTemplate

image

代码生成结果示例

2 Android 模板工程实现

最初我们使用终端脚本命令的方式,通过文件拷贝和文本查找替换(主要是替换包名等)的方式实现。但终归对 Android 开发人员不太友好,毕竟大家更习惯使用 Android Studio 生成工程。所幸,强大的 Android Studio 已经提供了较为全面的模板功能,这里大概可以分为以下几类:

2.1 Android 工程模板基础知识

2.1.1 工程模板实例介绍

对于 Android Studio,模板位置:

  1. Windows 的路径在 `${android studio 安装路径}/plugins/android/lib/templates/`
  2. MacOS 的路径在 `${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/`

有关模板的文件夹:

  1. activities:工程模板相关,如 EmptyActivity 文件夹用于创建一个空页面的模板,GoogleMapsActivity 文件夹对应创建一个地图页面的模板等

  2. gradle:放置了 gradle 模板,用于在新建工程的根目录下生成 gradle 文件夹,支持用户不用安装 gradle 就能使用 gradlew 命令

  3. gradle-project:工程模板相关,用于构建 moduleAndroid ProjectJava Library

  4. other:构建文件模板等

这里我们关心的是 activities 文件夹里面的内容

首先查看下 EmtpyActivity (空白页面模板) 里面的内容

  1. globals.xml.ftl: 全局变量文件,保存一些全局变量,当中可以引用其他文件的全局变量

  2. recipe.xml.ftl: 配置要引用的模板路径以及文件的生成规则

  3. template.xml: 模板的配置信息,包括模板的显示图标,界面的表现,全局变量文件和执行文件的指定等

    image

  4. template_blank_activity.png: 显示的缩略图

  5. SimpleActivity.java.ftl: Activity 模板文件

  6. 代码生成过程图

    image

    图片摘自 Tutorial How To Create Custom Android Code Templates

Android Studio 使用的是 FreeMarker 模板引擎,所以文件后缀都是 .ftl

2.1.2 常用标签使用

2.2 工程模板创建

新建 HTTemplate 文件夹内容如下:

  1. template.xml

    指定模板名、描述、最低支持 sdk 版本、类别等,输入界面要求指定包名和 Application 类名

  2. globals.xml.ftl

    引用公共文件内容

  3. recipe.xml.ftl

    • merge AndroidManifest.xml 文件

    • copy 或者 merge 资源文件

    • copy 或 instantiate java 代码

    • merge build.gradle 文件

    • merge settings.gradle 文件

    • copy lib 文件夹里面的全部内容

    • copy module 工程

    • copy proguard-rules.pro 文件

  4. root 文件夹

    放置相关模板源文件,其中将源工程中依赖于配置的代码,按照 FreeMarker 语法进行替换

  5. 添加工程模板图标,并在 template.xml 中添加引用

image

工程模板创建结果

2.3 遇到的坑与解决办法

2.3.1 build.gradle ${} 通配符冲突

当工程模板实例化时,${} 会被 FreeMarker 语法处理,导致错误。

解决办法:定义 FreeMarker 转义字符如下

  1. $ ==> ${"$"}

2.3.2 gradle.properties.ftl 合并失败

根据错误提示,执行合并操作是只能针对 xml 或者 gradle 文件进行,其他文件并不支持合并。另外改用 copyinstantiate 命令也同样失败

proguard-rules.pro 生成失败。

解决办法:将需要定义常量的代码移动到工程根目录 build.gradle 中:定义在 ext{ }

2.3.3 build.gradle 合并问题

  1. apply 合并失败

    期望结果

    1. apply plugin: 'com.android.application'
    2. apply plugin: 'com.neenbedankt.android-apt'

    实际结果

    1. apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'
  2. dependencies 中,apt 引用代码没有出现

2.3.4 settings.gradle 文件合并问题

为了工程目录结构更清晰些,我们在 settings.gradle.ftl 文件中指定 module 的相对路径,在 recipe.xml.ftl 执行了 merge 操作。但得到错误提示:settings.gradle.ftl 中只允许 include 命令。

解决办法:将 module 工程放置在默认目录下,不再指定路径

2.3.5 Java 代码实例化问题

模板中 java 代码较多,我们统一放在 root/src/ 文件夹下,里面有部分文件含有 FreeMarker 标签,有部分只是纯粹的 java 代码。而使用 instantiate 命令对整个文件夹进行实例化操作,并不会触发 FreeMarker 语法执行。

解决办法:因 java 文件比较多,手写 recipe.xml 标签命令繁琐且容易出错。我们通过程序递归遍历 root/src/ 下的全部代码文件,并生成相应的 instantiatecopy 命令

3 工程模板遗留问题解答

工程模板相关源码位置:

  1. Mac 平台:
  2. ${android studio 安装路径}/Contents/plugins/android/lib/android.jar
  3. Windows 平台:
  4. ${android studio 安装路径}/plugins/android/lib/android.jar

具体类在 com/android/tools/idea/templates/ 里面。

3.1 copy 和 instantiate 问题

3.1.1 copy 命令

查看 DefaultRecipeExecutor.copy 方法,这里是直接简单的调用 copyTemplateResource 方法,该函数的基本逻辑如下:

3.1.2 instantiate 命令

直接查看 DefaultRecipeExecutor.instantiate 方法,该函数的基本逻辑如下:

3.1.3 遗留问题解答

3.2 merge 问题

3.2.1 merge 主流程解析

查看 DefaultRecipeExecutor.merge 方法,基本逻辑如下:

image

3.2.2 settings.gradle 合并

查看 RecipeMergeUtils.mergeGradleSettingsFile 方法,基本逻辑如下:

3.2.3 build.gradle 合并

查看 GradleFileMerger.mergeGradleFiles 方法,里面会调用 mergePsi 方法,其基本逻辑如下:

这里 mergePsi 执行合并的逻辑是

image

继续查看 dependencies 合并的源码 GradleFileMerger.mergeDependencies 方法

里面的基本逻辑逻辑是:

fromRoot 是我们自定义的模板文件夹中定义的 dependencies 内容

toRoot 是执行 gradle-project 中的工程模板初始创建的 dependencies 内容

3.2.4 遗留问题解答

根据上面的分析,看起来 apply 的这个合成结果是 google 工程模板的 bug,是不是应该提供对 apply 合并的特殊处理?

3.3 小结

到现在,我们建立了自己的工程模板。原来编码过程中碰到的问题,现在也已经从源码解析的角度做了解释。一些问题,如 gradle 文件中,dependencies 元素合并忽略自定义模板文件中的非 compile 子元素;apply 元素合并不符合我们的需求。最后导致我们不得不放弃 apt 引入。这些问题 (或者说是限制),不知 Google 方面是出于什么考虑还是本身的 bug

4 网络请求代码自动生成

对于 Android 工程模板安装,我们提供的插件已经实现了下载和安装功能。

其次,在当前的工程当中,我们还需要有工具,能根据 NEI 接口定义平台 中定义的网络接口,自动生成我们的网络请求相关代码 (包括各个 Request 类和 JavaBean)。针对网络请求代码的自动生成,我们开发了 nei-toolkit,详细安装使用介绍可以查看 README.md

为了让 Android 开发人员能更加方便的使用 nei-toolkit,我们在插件中集成了 nei-toolkit 的下载、安装、使用。

4.1 插件开发基础

所有基于 IntelliJ Platform 的IDE,包括 Intellij IdeaAndroid StudioWeb Storm 等等,都可以为其添加插件以实现一些额外的功能。插件可以从本地安装,也可以从 JetBrains Plugin Repository 安装。Intellij 提供了一系列 API,使我们可以自定义插件。

  1. 如何配置插件开发的环境,可以查看 Setting Up a Development Environment

    需要注意的是,配置 Project language level 为 Java 6,才能支持大部分的 Android Studio

  2. 插件开发的其他基础知识,如设置按钮,如何处理事件逻辑,如何定义插件 id,名称,版本号等内容,可以查看 官方文档

4.2 执行终端命令

这里代码生成功能最终也还是执行了 nei-toolkit 中的命令来完成 http 代码生成的,因此我们使用的是 Runtime 方法来执行。

  1. Process proc = Runtime.getRuntime().exec(command);
  2. // 指定调用程序的工作目录
  3. Process proc = Runtime.getRuntime().exec(cmd, null, new File(project.getBasePath()));

此外我们提供 NeiConsole 控制台,显示脚本执行输出

image

5 小结和后续工作

到此,基本上完成了我们原先期望实现的工程模板和网络请求代码自动生成的工作:

  1. 提供 ht-template 支持生成我们的模板工程

  2. 提供 Android Studio 插件 (NEIPlugin)

这里还是有一些因为 Android 工程模板自身的限制而无法完成的内容点:

  1. 无法在 settings.gradle 指定 module 路径

  2. 无法合并 proguard-rules.pro 文件,暂时生成 proguard-rules.pro.template 文件

  3. 由于 build.gradleapply 命令合并会出错和无法合并 dependencies 中的 apt 命令,所以无法在 build.gradle 中集成 ht-universalrouter

再次,除了网络请求代码编写是机械性的,其他的基于我们的工程模板生成的初始工程,也存在一定的代码编写机械性:初始页面代码生成、RecycleView 中的各个 ViewHolder 类、本地数据读取保存等,而这些工作将会是我们的后续工作。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注