美文网首页
网易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