美文网首页
网易Android工程模板化实践

网易Android工程模板化实践

作者: nailperry | 来源:发表于2018-02-28 12:46 被阅读188次

本文由网易杭州前端技术部首发。

背景

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

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

  1. 产品孵化排期紧张

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

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

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

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

Android 模板简介

Android Studio 提供遵循 Android 设计和开发最佳实践的代码模板,以帮助我们快速并正确地创造出漂亮的、功能齐全的应用程序代码模板。Android Studio 中提供的模板列表在不断增加,按照它们添加的组件类型(如Activity或XML文件)可对模板进行如下分组:

template menu

可通过文件->新建菜单或在项目窗口中右键单击调出上述模板菜单。

Android Studio 模板位置:

Windows 的路径在 ${android studio 安装路径}/plugins/android/lib/templates/

MacOS 的路径在 ${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/

该文件夹具体内容如下:

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

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

  • gradle-project:工程模板相关,用于构建 module,Android Project,Java Library 等

  • other:构建文件模板等

模板最常见的用途之一是向现有应用程序模块添加新的 Activity。 activities 文件夹正是 Android Studio 默认提供的 Activity 模板,涵盖了手机和平板电脑应用中常用的 Activiy 模板。用户也可以参照已有模板自定义符合特定需求的 Activity 模板。

1. Activity 模板的文件结构

下面我们分析最简单的一个模板 EmptyActivity,我们首先查看下 EmtpyActivity (空白页面模板) 里面的内容

EmptyActivity Structrue

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

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

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

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

  • template_blank_activity.png: 显示的缩略图

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

2. 代码生成流程

目前我们已经基本了解了一个Activity模板的文件结构了,以及每个文件大致包含的东西,简单总结如下:

  • template 中parameter标签,主要用于提供参数

  • global.xml.ftl 主要用于提供参数

  • recipe.xml.ftl 主要用于生成我们实际需要的代码,资源文件等

    例如,利用参数 + MainActivity.java.ftl -> MainActivity.java,其实就是利用参数将ftl中的变量进行替换

代码生成过程如下图所示:

13_code_generation_process.jpg

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

HTTemplate Activity 模板实现

我们编写一个Activity模板叫作:HTTemplate,内容如下:

HTTemplate Structrue

1. template.xml

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

2. globals.xml.ftl

引用公共文件内容

<?xml version="1.0"?>
<globals>
    <global id="hasNoActionBar" type="boolean" value="false" />
    <#include "../common/common_globals.xml.ftl" />
</globals>

3. recipe.xml.ftl

<?xml version="1.0"?>
<recipe>

    <!-- nei.json -->
    <instantiate from="root/nei.json.ftl"
             to="${escapeXmlAttribute(topOut)}/nei.json" />

    <!-- manifest -->
    <merge from="root/AndroidManifest.xml.ftl"
             to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />

    <merge from="root/AndroidManifestPermissions.xml"
             to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />

    <!-- 全部资源 -->
    <copy from="root/res"
             to="${escapeXmlAttribute(resOut)}" />

    <!-- libs 库 -->
    <copy from="root/libs"
             to="${escapeXmlAttribute(projectOut)}/libs" />

    <!-- gradle -->
    <merge from="root/project_build.gradle.ftl"
             to="${escapeXmlAttribute(topOut)}/build.gradle" />

    <merge from="root/app_build.gradle.ftl"
             to="${escapeXmlAttribute(projectOut)}/build.gradle" />

    <!-- README -->
    <instantiate from="root/README.md.ftl"
             to="${escapeXmlAttribute(topOut)}/README.md" />

    <!-- proguard-rules.pro.ftl -->
    <copy from="root/proguard-rules.pro.ftl"
             to="${escapeXmlAttribute(projectOut)}/proguard-rules.pro.template" />

    <!-- java 代码 -->
    <!-- application 文件夹 -->
    <instantiate from="root/src/app_package/application/AppProfile.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/application/AppProfile.java" />


    ...

    <!-- attrs.xml -->
    <merge from="root/res/values/attrs.xml"
             to="${escapeXmlAttribute(resOut)}/values/attrs.xml" />
             
    ...

</recipe>

省略部分代码,主要的工作是

  • merge AndroidManifest.xml 文件

  • copy 或者 merge 资源文件

  • copy 或 instantiate java 代码

  • merge build.gradle 文件

  • merge settings.gradle 文件

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

  • copy proguard-rules.pro 文件

4. root 文件夹

root Structure

放置相关模板源文件,包括一些Activity、自定义View、通用工具类等。将其中以.ftl为后缀的源代码,按照 FreeMarker 语法进行替换。例如,使用了包名的地方,需要替换成 ${packageName}:

package ${packageName}.application;
import ${packageName}.R;

在gradle文件中配置私有maven库的地址、增加公用的依赖库、关闭lint的严格检查、配置APK多渠道打包等。

5. 模板图标

添加 Activity 模板图标,并在 template.xml 中添加引用

<thumbs>
    <thumb>template_thumb.png</thumb>
</thumbs>

6. 使用 Activity 模板生成初始工程

将上述 HTTemplate 文件夹拷贝至 Android Studio 的 Activity 模板目录下:

Windows 的路径在

${android studio 安装路径}/plugins/android/lib/templates/activities

MacOS 的路径在

${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/activities

重启 Android Studio,在新建工程过程中可以看到,出现了我们自定义的 Activity 模板项:

新建项目流程之选择Activity

直接运行生成的项目,效果如下:

工程运行效果图

到这里,使用我们的 Activity 模板生成的初始工程,就已经包含了我们提供的网络库、本地存储库、页面管理库、图片库等基础模块,开发人员接下来只需专注于业务逻辑的开发。

遇到的问题及解决方案

为分析问题的原因,我们找到了 Android 模板相关源码:

Mac 平台:

${android studio安装路径}/Contents/plugins/android/lib/android.jar

Windows 平台:

${android studio安装路径}/plugins/android/lib/android.jar

以下问题的解答将涉及到部分源码。

1. ${} 通配符冲突

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

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

$ ==> ${"$"}

2. Java 代码实例化问题

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

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

3. copy 和 instantiate 问题

(1) gradle.properties 文件执行 copy 或者 instantiate 操作无效

分析结果:查看 DefaultRecipeExecutor.copy 与 DefaultRecipeExecutor.instantiate 源码处理逻辑,得知执行 copy 和 instantiate 命令时,如果 from 指定一个非文件夹,且目标文件存在,则不执行拷贝。而在执行我们的 Activity 模板之前,已经执行了 gradle-projects/NewAndroidProject 工程模板,并生成了 gradle.properties 文件,因此执行 copy 或 instantiate 都因目标文件已经存在而不再执行。

(2) copy 和 instantiate 对文件夹操作的区别

  • 相同点:如果 from 指定一个文件夹,都是执行 copyTemplateResource 方法,二者没有区别;如果 from 指定一个非文件夹,且目标文件存在,则不执行文件操作。

  • 不同点:copy 命令不使用 FreemarkerUtils 对 FreeMarker 语法进行处理,而 instantiate 命令先执行 FreemarkerUtils 的静态方法 processFreemarkerTemplate 来处理 FreeMarker 语法,之后再执行文件拷贝操作。

4. merge 问题

(1) proguard-rules.pro、gradle.properties 文件执行 merge 操作失败

分析结果:根据 DefaultRecipeExecutor.merge 源码的逻辑,我们得知当 to 文件不存在,则执行 copy 或 instantiate 命令;如果 to 文件存在且可读,则仅对 xml 或 gradle 才能执行 merge 操作。

解决办法:

  • 暂时生成 proguard-rules.pro.template 文件

  • 将定义在 gradle.properties 中的常量移动到 project_build.gradle.ftl 的 ext{ } 内

(2) settings.gradle 文件合并,指定 module 路径错误

执行前:

include ':hteventbus', ':htrefreshrecyclerview', ':htrecycleview', ':hthttp'

project(':hteventbus').projectDir = new File('module/hteventbus')
project(':hthttp').projectDir = new File('module/hthttp')
project(':htrefreshrecyclerview').projectDir = new File('module/htrefreshrecyclerview')
project(':htrecycleview').projectDir = new File('module/htrecycleview')

执行后报错:

RuntimeException: java.lang.RuntimeException: When merging settings.gradle files, only include directives can be merged.

分析结果:查看 RecipeMergeUtils.mergeGradleSettingsFile 源码,得知当 settings.gradle 文件合并时,只允许每行开头是 include 命令,其他情况抛出异常。

解决办法:去掉非 include 的操作代码,改用远程依赖引用这些 module,即在 dependencies{ } 中添加相应的依赖。

(3) build.gradle 文件合并,apply 语句合并错误

执行前:

apply plugin: 'com.neenbedankt.android-apt'

执行后:

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

分析结果:查看 GradleFileMerger 中的 mergeGradleFiles 方法,实际执行的是 mergePsi 方法,根据 mergePsi 合并逻辑,apply 不是 call 语句,且 apply 的第一个子元素不是 dependencies,因此添加 plugin: 'com.neenbedankt.android-apt' 到 toRoot 的 apply 子元素前面。

解决办法:根据上面的分析,看起来 apply 的这个合成结果是 Android 模板的 bug,我们目前只能采用手工添加 apply 语句的方法。

(4) build.gradle 文件合并,dependencies{ } 内的 apt 语句消失

执行前:

dependencies {
    compile "com.netease.hearttouch:ht-universalrouter-dispatch:$HEARTTOUCH_HTROUTER_DISPATCH_VERSION"
    apt "com.netease.hearttouch:ht-universalrouter-dispatch-process:$HEARTTOUCH_HTROUTER_DISPATCH_PROCESS_VERSION"
    ...
}

执行后:

dependencies {
    compile "com.netease.hearttouch:ht-universalrouter-dispatch:$HEARTTOUCH_HTROUTER_DISPATCH_VERSION"
    ...
}

分析结果:查看 GradleFileMerger.mergeDependencies 源码,得知当 dependencies 合并时,仅处理 dependencies 中的 compile 子元素,其他如 apt、provided 命令都会被忽略掉。

解决办法:由于源码并未提供非 compile 子元素的合并方案,我们目前只能采用手工添加 apt 语句的方法。

5. 小结和后续工作

到此,基本上完成了我们原先期望实现的初始工程:

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

  2. 提供 Android Studio 插件 (NEIPlugin)

  • 支持 ht-template 的下载安装

  • nei-toolkit 和 Node.js 的下载安装

  • nei-toolkit 和 Node.js 的使用,生成网络请求代码

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

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

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

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

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

相关文章

网友评论

      本文标题:网易Android工程模板化实践

      本文链接:https://www.haomeiwen.com/subject/qipbxftx.html