网易 Android工程模板化实践
天使之爱你
8年前
<h2><strong>背景</strong></h2> <p>我们网易前端技术部 - 移动技术组作为公司的移动端基础技术部门,主要为其他部门提供解决方案、技术支持和产品孵化。在几年的积累过程中,我们拥有一些自己的框架和 SDK,如轻应用框架、热更新 SDK、网络请求库、本地存储库、页面管理等,服务过网易新闻、云音乐、考拉、易信等亿级产品,先后孵化过青果摄像头、二次元Gacha、严选等重要产品。</p> <p>在多年的Android开发中,对于 Android 端产品开发,我们有如下几点体会:</p> <ol> <li> <p>产品孵化排期紧张</p> <p>产品经理一般关心的是具体的业务逻辑,而前期基础模块的搭建,如各模块如何组织,使用代码结构如何选择,图片、网络、本地存储等选用哪个 sdk 等,一般不会有专门排期。</p> </li> <li> <p>基础模块的需求具有相似性</p> <p>内容型产品,其搭建的基础模块基本上都会包含图片显示、网络请求、本地存储、通信等。</p> </li> <li> <p>基础模块的选型和工具类具有可重用性</p> <p>网上相关的第三方库有很多,当然一般的公司也是会有自己开发或者维护的各个基础 SDK。很多时候,SDK 选型会更偏向于自己公司开发维护的 SDK,或者选择自己最熟悉,或最主流、最可靠的 SDK。因此当开发多个相同类型产品时,这里的技术选型是可重用的。</p> </li> <li> <p>网络请求的代码具有机械性</p> <p>客户端开发需要根据网络接口协议,编写相关的 GET、POST 等请求代码和对应的 JavaBean,这部分的代码编写其实是非常机械的。</p> </li> </ol> <h2><strong>网易工程模板是什么?</strong></h2> <p>对于各个基础模块,我们团队封装了自己的 SDK,如网络库、本地存储库、页面管理库、图片库等。使用我们的工程模板生成的初始工程,就已经包含了我们提供的基础模块,产品团队的开发不需要再花费重复的时间做技术调研、选型、SDK封装集成等工作,而只需要关心自己的业务逻辑编写。我们期望产品团队只需 1 分钟就能得到自己的初始工程,并能马上投入业务逻辑开发,既能缩短开发周期,也能保证工程代码质量。</p> <p>此外,我们也提供了 Android Studio 插件 (NEIPlugin),集成插件后,就能在 Android Studio 中通过菜单点击自动下载集成我们的工程模板,也能自动生成网络请求相关的代码。</p> <p>(点击放大图像)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d4c6f6bf9e00c0b70a80a15e5e9e3619.jpg"></p> <p>代码生成结果示例</p> <h2><strong>Android 模板工程实现</strong></h2> <p>最初我们使用终端脚本命令的方式,通过文件拷贝和文本查找替换(主要是替换包名等)的方式实现。但终归对 Android 开发人员不太友好,毕竟大家更习惯使用 Android Studio 生成工程。所幸,强大的 Android Studio 已经提供了较为全面的模板功能,这里大概可以分为以下几类:</p> <ul> <li>工程模板 (本文内容)</li> <li>文件模板</li> <li>注释模板</li> <li>编码模板(Living Template)</li> </ul> <h3><strong>Android 工程模板基础知识</strong></h3> <p>工程模板实例介绍</p> <p>对于 Android Studio,模板位置:</p> <p>Windows 的路径在 `${android studio 安装路径}/plugins/android/lib/templates/`</p> <p>MacOS 的路径在 `${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/`</p> <p>有关模板的文件夹:</p> <ol> <li>activities:工程模板相关,如 EmptyActivity 文件夹用于创建一个空页面的模板,GoogleMapsActivity 文件夹对应创建一个地图页面的模板等</li> <li>gradle:放置了 gradle 模板,用于在新建工程的根目录下生成 gradle 文件夹,支持用户不用安装 gradle 就能使用 gradlew 命令</li> <li>gradle-project:工程模板相关,用于构建 module,Android Project,Java Library 等</li> <li>other:构建文件模板等</li> </ol> <p>这里我们关心的是 activities 文件夹里面的内容</p> <p>首先查看下 EmtpyActivity (空白页面模板) 里面的内容</p> <ol> <li> <p>globals.xml.ftl: 全局变量文件,保存一些全局变量,当中可以引用其他文件的全局变量</p> </li> <li> <p>recipe.xml.ftl: 配置要引用的模板路径以及文件的生成规则</p> </li> <li> <p>template.xml: 模板的配置信息,包括模板的显示图标,界面的表现,全局变量文件和执行文件的指定等</p> <p>(点击放大图像)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d3a9cda626ad03960eb688586125872f.jpg"></p> </li> </ol> <p>Android Studio 使用的是 FreeMarker 模板引擎,所以文件后缀都是 .ftl</p> <p>常用标签使用</p> <ul> <li> <p>${}: FreeMarker 的语法,如 ${packageName}, ${superClass} 是 globals.xml.ftl 全局变量文件或template.xml.ftl 中定义变量引用</p> </li> <li> <p><#if></#if>: FreeMarker 的语法,条件判断语句</p> </li> <li> <p><#include>: FreeMarker 的语法,包含语句</p> </li> <li> <p>copy: 将文件或者文件夹从 from 标签拷贝到 to 标签指定的路径</p> </li> <li> <p>instantiate: 将文件或者文件夹,执行 FreeMarker 语法,从 from 标签实例化到 to 标签指定的路径</p> </li> <li> <p>merge: 合并 from 和 to 标签分别指定的文件</p> </li> <li> <p>open: 在工程打开后,默认打开指定的文件</p> <p>实例:使用空白页面模板生成工程并打开后,可以看到默认打开了 MainActivity.java 和 activity_main.xml 文件</p> </li> </ul> <h3><strong>工程模板创建</strong></h3> <p>新建 HTTemplate 文件夹内容如下:</p> <ol> <li> <p>template.xml</p> <p>指定模板名、描述、最低支持 sdk 版本、类别等,输入界面要求指定包名和 Application 类名</p> </li> <li> <p>globals.xml.ftl</p> <p>引用公共文件内容</p> </li> <li> <p>recipe.xml.ftl</p> <ul> <li> <p>merge AndroidManifest.xml 文件</p> </li> <li> <p>copy 或者 merge 资源文件</p> </li> <li> <p>copy 或 instantiate java 代码</p> </li> <li> <p>merge build.gradle 文件</p> </li> <li> <p>merge settings.gradle 文件</p> </li> <li> <p>copy lib 文件夹里面的全部内容</p> </li> <li> <p>copy module 工程</p> </li> <li> <p>copy proguard-rules.pro 文件</p> </li> </ul> </li> <li> <p>root 文件夹</p> <p>放置相关模板源文件,其中将源工程中依赖于配置的代码,按照 FreeMarker 语法进行替换</p> </li> <li> <p>添加工程模板图标,并在 template.xml 中添加引用</p> </li> </ol> <p>(点击放大图像)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/5c76fece325474b3a7b6a100ab1176b6.jpg"></p> <p>settings.gradle 合并</p> <p>查看 RecipeMergeUtils.mergeGradleSettingsFile 方法,基本逻辑如下:</p> <ul> <li> <p>读取目标文件的每一行内容,并判断每行内容的开头是否是 include 开头</p> <ul> <li> <p>是:在 include 后面插入内容</p> </li> <li> <p>否:抛出异常</p> </li> </ul> </li> <li> <p>返回合并的内容</p> </li> </ul> <p>build.gradle 合并</p> <p>查看 GradleFileMerger.mergeGradleFiles 方法,里面会调用 mergePsi 方法,其基本逻辑如下:</p> <ul> <li> <p>读取文件 source 和 dest 文件的内容,并转化得到 GroovyFile 类型对象</p> </li> <li> <p>执行 mergePsi 方法</p> </li> </ul> <p>这里 mergePsi 执行合并的逻辑是</p> <p>(点击放大图像)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/8d4e8e994cb580dfa2c9a1d6dfc7c8ea.jpg"></p> <h2><strong>小结和后续工作</strong></h2> <p>到此,基本上完成了我们原先期望实现的工程模板和网络请求代码自动生成的工作:</p> <ol> <li> <p>提供 ht-template 支持生成我们的模板工程</p> </li> <li> <p>提供 Android Studio 插件 (NEIPlugin)</p> <ul> <li> <p>支持 ht-template 的下载安装</p> </li> <li> <p>nei-toolkit 和 Node.js 的下载安装</p> </li> <li> <p>nei-toolkit 和 Node.js 的使用,生成网络请求代码</p> </li> </ul> </li> </ol> <p>这里还是有一些因为 Android 工程模板自身的限制而无法完成的内容点:</p> <ol> <li> <p>无法在 settings.gradle 指定 module 路径</p> </li> <li> <p>无法合并 proguard-rules.pro 文件,暂时生成 proguard-rules.pro.template 文件</p> </li> <li> <p>由于 build.gradle 对 apply 命令合并会出错和无法合并 dependencies 中的 apt 命令,所以无法在 build.gradle 中集成 ht-universalrouter</p> </li> </ol> <p>再次,除了网络请求代码编写是机械性的,其他的基于我们的工程模板生成的初始工程,也存在一定的代码编写机械性:初始页面代码生成、RecycleView 中的各个 ViewHolder 类、本地数据读取保存等,而这些工作将会是我们的后续工作。</p> <h2><strong>标题</strong></h2> <p><strong>Q:本次的分享是不是需要有idea的插件化知识背景?</strong></p> <p>A:idea 插件开发的内容,可以查看官方文档 http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/setting_up_environment.html ,里面有比较详细的介绍。</p> <p>如果需要自己学习插件开发的话,就需要学习官方文档,不过我的分享中并没有讲述插件开发中的一些细节,应该不会有影响。</p> <p>这次大家如果觉得听起来有点难,我想应该是这次分享需要有工程模板开发的背景,不然确实会有点难。</p> <p><strong>Q:请问neitoolkit是做什么的?</strong></p> <p>A:neitoolkit 在移动端,是一个配合我们的 NEI 接口管理平台( http://nei.hz.netease.com/ ),用来生成网络请求相关代码的一个工具,当然可以查看 README 介绍</p> <ul> <li>支持根据 NEI 平台 定制生成项目初始结构及代码</li> <li>支持 NEJ 发布工具 配置文件自动生成</li> <li>支持 Fiddler 和 Charles 工具代理本地模拟数据,接口配置文件导出</li> <li>支持自动生成移动端数据模型、请求类代码</li> <li>支持自动导出模拟数据</li> <li>集成了本地模拟容器</li> </ul> <p><strong>Q:请问上文的runtime怎么理解呀?</strong></p> <p>A:这里的 Runtime,其实就是执行了 java 中的 Runtime.getRuntime().exec(command); 方法。</p> <p>这个方法的作用就是执行 sh (windows中cmd) 中的脚本命令。</p> <p><strong>Q:如果模版中有需要apt处理的代码,模版是不支持的是么?</strong></p> <p>A:恩,工程模板是不支持的,dependencies 中 非 compile 命令全部都是不支持的,这个可以从前面的源码分析中看出来。</p> <p> </p> <p> </p> <p> </p> <p>来自:http://www.infoq.com/cn/articles/practice-of-netease-android-project-template</p> <p> </p>