咱们网易前端技术部 - 移动技术组做为公司的移动端基础技术部门,主要为其余部门提供解决方案、技术支持和产品孵化。在几年的积累过程当中,咱们拥有一些本身的框架和 SDK,如轻应用框架、热更新 SDK、网络请求库、本地存储库、页面管理等,服务过网易新闻、云音乐、考拉、易信等亿级产品,前后孵化过青果摄像头、二次元Gacha、严选等重要产品。html
在多年的Android开发中,对于 Android 端产品开发,咱们有以下几点体会:前端
产品孵化排期紧张java
产品经理通常关心的是具体的业务逻辑,而前期基础模块的搭建,如各模块如何组织,使用代码结构如何选择,图片、网络、本地存储等选用哪一个 sdk 等,通常不会有专门排期。node
基础模块的需求具备类似性android
内容型产品,其搭建的基础模块基本上都会包含图片显示、网络请求、本地存储、通讯等。git
基础模块的选型和工具类具备可重用性github
网上相关的第三方库有不少,固然通常的公司也是会有本身开发或者维护的各个基础 SDK。不少时候,SDK 选型会更偏向于本身公司开发维护的 SDK,或者选择本身最熟悉,或最主流、最可靠的 SDK。所以当开发多个相同类型产品时,这里的技术选型是可重用的。windows
网络请求的代码具备机械性网络
客户端开发须要根据网络接口协议,编写相关的 GET、POST 等请求代码和对应的 JavaBean,这部分的代码编写实际上是很是机械的。app
网易工程模板是什么?
对于各个基础模块,咱们团队封装了本身的 SDK,如网络库、本地存储库、页面管理库、图片库等。使用咱们的工程模板生成的初始工程,就已经包含了咱们提供的基础模块,产品团队的开发不须要再花费重复的时间作技术调研、选型、SDK封装集成等工做,而只须要关心本身的业务逻辑编写。咱们指望产品团队只需 1 分钟就能获得本身的初始工程,并能立刻投入业务逻辑开发,既能缩短开发周期,也能保证工程代码质量。
此外,咱们也提供了 Android Studio 插件 (NEIPlugin),集成插件后,就能在 Android Studio 中经过菜单点击自动下载集成咱们的工程模板,也能自动生成网络请求相关的代码。
工程模板 HTTemplate
代码生成结果示例
Android 模板工程实现
最初咱们使用终端脚本命令的方式,经过文件拷贝和文本查找替换(主要是替换包名等)的方式实现。但终归对 Android 开发人员不太友好,毕竟你们更习惯使用 Android Studio 生成工程。所幸,强大的 Android Studio 已经提供了较为全面的模板功能,这里大概能够分为如下几类:
工程模板 (本文内容)
文件模板
注释模板
编码模板(Living Template)
对于 Android Studio,模板位置:
Windows 的路径在 `${android studio 安装路径}/plugins/android/lib/templates/` MacOS 的路径在 `${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/`
有关模板的文件夹:
activities:工程模板相关,如 EmptyActivity 文件夹用于建立一个空页面的模板,GoogleMapsActivity 文件夹对应建立一个地图页面的模板等
gradle:放置了 gradle 模板,用于在新建工程的根目录下生成 gradle 文件夹,支持用户不用安装 gradle 就能使用 gradlew 命令
gradle-project:工程模板相关,用于构建 module,Android Project,Java Library 等
other:构建文件模板等
这里咱们关心的是 activities 文件夹里面的内容
首先查看下 EmtpyActivity (空白页面模板) 里面的内容
globals.xml.ftl: 全局变量文件,保存一些全局变量,当中能够引用其余文件的全局变量
recipe.xml.ftl: 配置要引用的模板路径以及文件的生成规则
template.xml: 模板的配置信息,包括模板的显示图标,界面的表现,全局变量文件和执行文件的指定等
template_blank_activity.png: 显示的缩略图
SimpleActivity.java.ftl: Activity 模板文件
代码生成过程图
图片摘自 Tutorial How To Create Custom Android Code Templates
Android Studio 使用的是 FreeMarker 模板引擎,因此文件后缀都是 .ftl
${}: FreeMarker 的语法,如 ${packageName}, ${superClass} 是 globals.xml.ftl 全局变量文件或template.xml.ftl 中定义变量引用
<#if></#if>: FreeMarker 的语法,条件判断语句
<#include>: FreeMarker 的语法,包含语句
copy: 将文件或者文件夹从 from 标签拷贝到 to 标签指定的路径
instantiate: 将文件或者文件夹,执行 FreeMarker 语法,从 from 标签实例化到 to 标签指定的路径
merge: 合并 from 和 to 标签分别指定的文件
open: 在工程打开后,默认打开指定的文件
实例:使用空白页面模板生成工程并打开后,能够看到默认打开了 MainActivity.java 和 activity_main.xml 文件
新建 HTTemplate 文件夹内容以下:
template.xml
指定模板名、描述、最低支持 sdk 版本、类别等,输入界面要求指定包名和 Application 类名
globals.xml.ftl
引用公共文件内容
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 文件
root 文件夹
放置相关模板源文件,其中将源工程中依赖于配置的代码,按照 FreeMarker 语法进行替换
添加工程模板图标,并在 template.xml 中添加引用
工程模板建立结果
当工程模板实例化时,${} 会被 FreeMarker 语法处理,致使错误。
解决办法:定义 FreeMarker 转义字符以下
$ ==> ${"$"}
根据错误提示,执行合并操做是只能针对 xml 或者 gradle 文件进行,其余文件并不支持合并。另外改用 copy 或 instantiate 命令也一样失败
同 proguard-rules.pro 生成失败。
解决办法:将须要定义常量的代码移动到工程根目录 build.gradle 中:定义在 ext{ } 内。
apply 合并失败
指望结果
apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt'
实际结果
apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'
dependencies 中,apt 引用代码没有出现
为了工程目录结构更清晰些,咱们在 settings.gradle.ftl 文件中指定 module 的相对路径,在 recipe.xml.ftl 执行了 merge 操做。但获得错误提示:settings.gradle.ftl 中只容许 include 命令。
解决办法:将 module 工程放置在默认目录下,再也不指定路径
模板中 java 代码较多,咱们统一放在 root/src/ 文件夹下,里面有部分文件含有 FreeMarker 标签,有部分只是纯粹的 java 代码。而使用instantiate 命令对整个文件夹进行实例化操做,并不会触发 FreeMarker 语法执行。
解决办法:因 java 文件比较多,手写 recipe.xml 标签命令繁琐且容易出错。咱们经过程序递归遍历 root/src/ 下的所有代码文件,并生成相应的 instantiate 或 copy 命令
工程模板遗留问题解答
工程模板相关源码位置:
Mac 平台: ${android studio安装路径}/Contents/plugins/android/lib/android.jar Windows 平台: ${android studio安装路径}/plugins/android/lib/android.jar
具体类在 com/android/tools/idea/templates/ 里面。
gradle.properties 文件执行 copy 或者 instantiate 操做无效果缘由?
copy 和 instantiate 对文件夹操做的区别
查看 DefaultRecipeExecutor.copy 方法,这里是直接简单的调用 copyTemplateResource 方法,该函数的基本逻辑以下:
若是 source 是一个文件夹,则执行 copyDirectory 方法,里面会递归的执行文件夹内的文件,其中若是叶子文件 (非文件夹) 对应的目标文件存在,则不执行拷贝,继续处理其余文件
若是 source 非文件夹,且目标文件存在,则不执行拷贝
当上面的条件都不知足的状况下,执行文件拷贝操做
期间没有使用 FreemarkerUtils 对 FreeMarker 语法进行处理
直接查看 DefaultRecipeExecutor.instantiate 方法,该函数的基本逻辑以下:
若是 from 文件是一个文件夹,则执行 copyTemplateResource 方法,和 copy 流程同样
若是 from 文件非文件夹,且目标文件已经存在了,则不执行文件操做
当上面的条件都不知足的状况下,先执行 FreemarkerUtils 的静态方法 processFreemarkerTemplate 来处理 FreeMarker 语法,以后再执行文件拷贝操做
gradle.properties 文件执行 copy 或者 instantiate 操做无效果缘由?
解答:在执行咱们的工程模板执行,已经执行了 gradle-projects/NewAndroidProject 模板,并生成了 gradle.properties 文件,所以执行 copy 或 instantiate 都因目标文件已经存在而再也不执行
copy 和 instantiate 对文件夹操做的区别
解答:若是 from 指定一个文件夹,都是执行 copyTemplateResource 方法,2 者没有区别
gradle.properties 文件执行 merge 操做失败缘由
settings.gradle 文件合并,指定 module 路径错误缘由
apt 语句消失缘由
apply 语句合并错误缘由
查看 DefaultRecipeExecutor.merge 方法,基本逻辑以下:
settings.gradle 合并
查看 RecipeMergeUtils.mergeGradleSettingsFile 方法,基本逻辑以下:
读取目标文件的每一行内容,并判断每行内容的开头是不是 include 开头
是:在 include 后面插入内容
否:抛出异常
返回合并的内容
查看 GradleFileMerger.mergeGradleFiles 方法,里面会调用 mergePsi 方法,其基本逻辑以下:
读取文件 source 和 dest 文件的内容,并转化获得 GroovyFile 类型对象
执行 mergePsi 方法
这里 mergePsi 执行合并的逻辑是
继续查看 dependencies 合并的源码 GradleFileMerger.mergeDependencies 方法
里面的基本逻辑逻辑是:
收集 toRoot 中能解析的 compile 子元素,并将收集到的子元素从 toRoot 中删除
收集 fromRoot 中的能解析的 compile 子元素,并删除能解析的 compile 子元素,另外单独收集不能解析的 complie 子元素
遍历所有能解析的 compile 子元素,比较相同 compile 语句的最大版本号,并插入到 toRoot 中
遍历不能解析的 compile 子元素,将内容添加至 toRoot 中
fromRoot 是咱们自定义的模板文件夹中定义的 dependencies 内容
toRoot 是执行 gradle-project 中的工程模板初始建立的 dependencies 内容
gradle.properties 文件执行 merge 操做失败缘由
解答:根据 DefaultRecipeExecutor.merge 方法的逻辑,咱们能够看到当 to 文件不存在,则执行 copy 或 instantiate 命令;若是 to 文件存在且可读,则仅对 xml 或 gradle 才能执行 merge 操做
settings.gradle 文件合并,指定 module 路径错误缘由
解答:只容许每行开头是 include 命令,其余状况抛出异常
apt 语句消失缘由
解答:pullDependenciesIntoMap 方法仅处理 from 文件中 dependencies 中的 compile 子元素,其余如 apt、provided 命令都是会被忽略掉。
apply 语句合并错误缘由
// 咱们的工程模板文件内容 - 对应 mergePsi 方法中 toRoot 参数 apply plugin: 'com.neenbedankt.android-apt' // 源工程模板初始生成的 `buidl.gradle` 文件内容 - 对应 mergePsi 方法中 fromRoot 参数 apply plugin: 'com.android.application' // 指望合并结果 apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'com.android.application' // 实际合并结果 apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'
大概画了执行流程,里面的关键流程以下:
步骤 2: fromRoot 和 toRoot 不是 call 语句
步骤 5: 都能找到 apply 类型的子元素
步骤 6: 2 个 apply 的第一个子元素都不是 dependencies
步骤 11: fromRoot 中的 apply 子元素 plugin: 'com.android.application' 和 toRoot 中的 apply 子元素的 plugin: 'com.neenbedankt.android-apt' 不对应
步骤12: 将 plugin: 'com.android.application' 添加到 toRoot 的 apply 子元素前面
根据上面的分析,看起来 apply 的这个合成结果是 google 工程模板的 bug,是否是应该提供对 apply 合并的特殊处理?
到如今,咱们创建了本身的工程模板。原来编码过程当中碰到的问题,如今也已经从源码解析的角度作了解释。一些问题,如 gradle 文件中,dependencies 元素合并忽略自定义模板文件中的非 compile 子元素;apply 元素合并不符合咱们的需求。最后致使咱们不得不放弃 apt 引入。这些问题 (或者说是限制),不知 Google 方面是出于什么考虑仍是自己的 bug。
网络请求代码自动生成
对于 Android 工程模板安装,咱们提供的插件已经实现了下载和安装功能。
其次,在当前的工程当中,咱们还须要有工具,能根据 NEI 接口定义平台中定义的网络接口,自动生成咱们的网络请求相关代码 (包括各个 Request类和 JavaBean)。针对网络请求代码的自动生成,咱们开发了 nei-toolkit,详细安装使用介绍能够查看 README.md
为了让 Android 开发人员能更加方便的使用 nei-toolkit,咱们在插件中集成了 nei-toolkit 的下载、安装、使用。
NEI 接口定义平台:http://nei.hz.netease.com/
nei-toolkit:https://github.com/NEYouFan/nei-toolkit
全部基于 IntelliJ Platform 的IDE,包括 Intellij Idea,Android Studio,Web Storm 等等,均可觉得其添加插件以实现一些额外的功能。插件能够从本地安装,也能够从 JetBrains Plugin Repository 安装。Intellij 提供了一系列 API,使咱们能够自定义插件。
如何配置插件开发的环境,能够查看 Setting Up a Development Environment:http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/setting_up_environment.html
须要注意的是,配置 Project language level 为 Java 6,才能支持大部分的 Android Studio
插件开发的其余基础知识,如设置按钮,如何处理事件逻辑,如何定义插件 id,名称,版本号等内容,能够查看官方文档。
这里代码生成功能最终也仍是执行了 nei-toolkit 中的命令来完成 http 代码生成的,所以咱们使用的是 Runtime 方法来执行。
Process proc = Runtime.getRuntime().exec(command);
// 指定调用程序的工做目录
Process proc = Runtime.getRuntime().exec(cmd, null, new File(project.getBasePath()));
执行下载工程模板命令:
git clone ${ht-template git 地址} /Applications/Android\
Studio.app/Contents/plugins/android/lib/templates/activities/HTTemplate
MacOS 平台
执行代码生成命令
/usr/local/bin/node /usr/local/bin/nei mobile 11321 --lang java --appPackage com.netease.test.httemplatetest --reqAbstract com.netease.hearttouch.http.BaseRequest --baseModelAbstract com.netease.hthttp.model.BaseModel --resOut /app/src/main/hthttp-gen/ --doNotOverwrite
MacOS 平台
此外咱们提供 NeiConsole 控制台,显示脚本执行输出
小结和后续工做
到此,基本上完成了咱们原先指望实现的工程模板和网络请求代码自动生成的工做:
提供 ht-template 支持生成咱们的模板工程
提供 Android Studio 插件 (NEIPlugin)
支持 ht-template 的下载安装
nei-toolkit 和 Node.js 的下载安装
nei-toolkit 和 Node.js 的使用,生成网络请求代码
这里仍是有一些由于 Android 工程模板自身的限制而没法完成的内容点:
没法在 settings.gradle 指定 module 路径
没法合并 proguard-rules.pro 文件,暂时生成 proguard-rules.pro.template 文件
因为 build.gradle 对 apply 命令合并会出错和没法合并 dependencies 中的 apt 命令,因此没法在 build.gradle 中集成 ht-universalrouter
再次,除了网络请求代码编写是机械性的,其余的基于咱们的工程模板生成的初始工程,也存在必定的代码编写机械性:初始页面代码生成、RecycleView 中的各个 ViewHolder 类、本地数据读取保存等,而这些工做将会是咱们的后续工做。
标题
Q:本次的分享是否是须要有idea的插件化知识背景?
A:idea 插件开发的内容,能够查看官方文档 http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/setting_up_environment.html,里面有比较详细的介绍。
若是须要本身学习插件开发的话,就须要学习官方文档,不过个人分享中并无讲述插件开发中的一些细节,应该不会有影响。
此次你们若是以为听起来有点难,我想应该是此次分享须要有工程模板开发的背景,否则确实会有点难。
推荐下这篇文章,能够入门下工程模板开发的内容:
Mobile App Development: Tutorial How To Create Custom Android Code Templates
http://robusttechhouse.com/tutorial-how-to-create-custom-android-code-templates/
Q:请问neitoolkit是作什么的?
A: neitoolkit 在移动端,是一个配合咱们的 NEI 接口管理平台(http://nei.hz.netease.com/),用来生成网络请求相关代码的一个工具,固然能够查看 README 介绍
支持根据 NEI 平台 定制生成项目初始结构及代码
支持 NEJ 发布工具 配置文件自动生成
支持 Fiddler 和 Charles 工具代理本地模拟数据,接口配置文件导出
支持自动生成移动端数据模型、请求类代码
支持自动导出模拟数据
集成了本地模拟容器
Q:请问上文的runtime怎么理解呀?
A:这里的 Runtime,其实就是执行了 java 中的 Runtime.getRuntime().exec(command); 方法。
这个方法的做用就是执行 sh (windows中cmd) 中的脚本命令。
Q:若是模版中有须要apt处理的代码,模版是不支持的是么?
A:恩,工程模板是不支持的,dependencies 中 非 compile 命令所有都是不支持的,这个能够从前面的源码分析中看出来。