0 背景
我們網(wǎng)易前端技術(shù)部 - 移動(dòng)技術(shù)組作為公司的移動(dòng)端基礎(chǔ)技術(shù)部門,主要為其他部門提供解決方案、技術(shù)支持和產(chǎn)品孵化。在幾年的積累過(guò)程中,我們擁有一些自己的框架和 SDK,如輕應(yīng)用框架、熱更新 SDK、網(wǎng)絡(luò)請(qǐng)求庫(kù)、本地存儲(chǔ)庫(kù)、頁(yè)面管理等,服務(wù)過(guò)網(wǎng)易新聞、云音樂(lè)、考拉、易信等億級(jí)產(chǎn)品,先后孵化過(guò)青果攝像頭、二次元Gacha、嚴(yán)選等重要產(chǎn)品。
在多年的Android開發(fā)中,對(duì)于 Android 端產(chǎn)品開發(fā),我們有如下幾點(diǎn)體會(huì):
-
產(chǎn)品孵化排期緊張
產(chǎn)品經(jīng)理一般關(guān)心的是具體的業(yè)務(wù)邏輯,而前期基礎(chǔ)模塊的搭建,如各模塊如何組織,使用代碼結(jié)構(gòu)如何選擇,圖片、網(wǎng)絡(luò)、本地存儲(chǔ)等選用哪個(gè) sdk 等,一般不會(huì)有專門排期。
-
基礎(chǔ)模塊的需求具有相似性
內(nèi)容型產(chǎn)品,其搭建的基礎(chǔ)模塊基本上都會(huì)包含圖片顯示、網(wǎng)絡(luò)請(qǐng)求、本地存儲(chǔ)、通信等。
-
基礎(chǔ)模塊的選型和工具類具有可重用性
網(wǎng)上相關(guān)的第三方庫(kù)有很多,當(dāng)然一般的公司也是會(huì)有自己開發(fā)或者維護(hù)的各個(gè)基礎(chǔ) SDK。很多時(shí)候,SDK 選型會(huì)更偏向于自己公司開發(fā)維護(hù)的 SDK,或者選擇自己最熟悉,或最主流、最可靠的 SDK。因此當(dāng)開發(fā)多個(gè)相同類型產(chǎn)品時(shí),這里的技術(shù)選型是可重用的。
-
網(wǎng)絡(luò)請(qǐng)求的代碼具有機(jī)械性
客戶端開發(fā)需要根據(jù)網(wǎng)絡(luò)接口協(xié)議,編寫相關(guān)的
GET、POST等請(qǐng)求代碼和對(duì)應(yīng)的JavaBean,這部分的代碼編寫其實(shí)是非常機(jī)械的。
1 網(wǎng)易工程模板是什么?
對(duì)于各個(gè)基礎(chǔ)模塊,我們團(tuán)隊(duì)封裝了自己的 SDK,如網(wǎng)絡(luò)庫(kù)、本地存儲(chǔ)庫(kù)、頁(yè)面管理庫(kù)、圖片庫(kù)等。使用我們的工程模板生成的初始工程,就已經(jīng)包含了我們提供的基礎(chǔ)模塊,產(chǎn)品團(tuán)隊(duì)的開發(fā)不需要再花費(fèi)重復(fù)的時(shí)間做技術(shù)調(diào)研、選型、SDK封裝集成等工作,而只需要關(guān)心自己的業(yè)務(wù)邏輯編寫。我們期望產(chǎn)品團(tuán)隊(duì)只需 1 分鐘就能得到自己的初始工程,并能馬上投入業(yè)務(wù)邏輯開發(fā),既能縮短開發(fā)周期,也能保證工程代碼質(zhì)量。
此外,我們也提供了 Android Studio 插件 (NEIPlugin),集成插件后,就能在 Android Studio 中通過(guò)菜單點(diǎn)擊自動(dòng)下載集成我們的工程模板,也能自動(dòng)生成網(wǎng)絡(luò)請(qǐng)求相關(guān)的代碼。

工程模板
HTTemplate

代碼生成結(jié)果示例
2 Android 模板工程實(shí)現(xiàn)
最初我們使用終端腳本命令的方式,通過(guò)文件拷貝和文本查找替換(主要是替換包名等)的方式實(shí)現(xiàn)。但終歸對(duì) Android 開發(fā)人員不太友好,畢竟大家更習(xí)慣使用 Android Studio 生成工程。所幸,強(qiáng)大的 Android Studio 已經(jīng)提供了較為全面的模板功能,這里大概可以分為以下幾類:
工程模板 (本文內(nèi)容)
2.1 Android 工程模板基礎(chǔ)知識(shí)
2.1.1 工程模板實(shí)例介紹
對(duì)于 Android Studio,模板位置:
Windows 的路徑在 `${android studio 安裝路徑}/plugins/android/lib/templates/`
MacOS 的路徑在 `${Android Studio.app 存放路徑}/Contents/plugins/android/lib/templates/`
有關(guān)模板的文件夾:
activities:工程模板相關(guān),如EmptyActivity文件夾用于創(chuàng)建一個(gè)空頁(yè)面的模板,GoogleMapsActivity文件夾對(duì)應(yīng)創(chuàng)建一個(gè)地圖頁(yè)面的模板等gradle:放置了gradle模板,用于在新建工程的根目錄下生成gradle文件夾,支持用戶不用安裝gradle就能使用gradlew命令gradle-project:工程模板相關(guān),用于構(gòu)建module,Android Project,Java Library等other:構(gòu)建文件模板等
這里我們關(guān)心的是 activities 文件夾里面的內(nèi)容
首先查看下 EmtpyActivity (空白頁(yè)面模板) 里面的內(nèi)容
globals.xml.ftl: 全局變量文件,保存一些全局變量,當(dāng)中可以引用其他文件的全局變量recipe.xml.ftl: 配置要引用的模板路徑以及文件的生成規(guī)則-
template.xml: 模板的配置信息,包括模板的顯示圖標(biāo),界面的表現(xiàn),全局變量文件和執(zhí)行文件的指定等image template_blank_activity.png: 顯示的縮略圖SimpleActivity.java.ftl: Activity 模板文件-
代碼生成過(guò)程圖
image
Android Studio 使用的是
FreeMarker模板引擎,所以文件后綴都是.ftl
2.1.2 常用標(biāo)簽使用
${}:FreeMarker的語(yǔ)法,如${packageName},${superClass}是globals.xml.ftl全局變量文件或template.xml.ftl中定義變量引用<#if></#if>:FreeMarker的語(yǔ)法,條件判斷語(yǔ)句<#include>:FreeMarker的語(yǔ)法,包含語(yǔ)句copy: 將文件或者文件夾從 from 標(biāo)簽拷貝到 to 標(biāo)簽指定的路徑instantiate: 將文件或者文件夾,執(zhí)行FreeMarker語(yǔ)法,從 from 標(biāo)簽實(shí)例化到 to 標(biāo)簽指定的路徑merge: 合并 from 和 to 標(biāo)簽分別指定的文件-
open: 在工程打開后,默認(rèn)打開指定的文件實(shí)例:使用空白頁(yè)面模板生成工程并打開后,可以看到默認(rèn)打開了
MainActivity.java和activity_main.xml文件
2.2 工程模板創(chuàng)建
新建 HTTemplate 文件夾內(nèi)容如下:
-
template.xml
指定模板名、描述、最低支持 sdk 版本、類別等,輸入界面要求指定包名和
Application類名 -
globals.xml.ftl
引用公共文件內(nèi)容
-
recipe.xml.ftl
merge
AndroidManifest.xml文件copy 或者 merge 資源文件
copy 或 instantiate
java代碼merge
build.gradle文件merge
settings.gradle文件copy
lib文件夾里面的全部?jī)?nèi)容copy
module工程copy
proguard-rules.pro文件
-
root 文件夾
放置相關(guān)模板源文件,其中將源工程中依賴于配置的代碼,按照
FreeMarker語(yǔ)法進(jìn)行替換 添加工程模板圖標(biāo),并在
template.xml中添加引用

工程模板創(chuàng)建結(jié)果
2.3 遇到的坑與解決辦法
2.3.1 build.gradle ${} 通配符沖突
當(dāng)工程模板實(shí)例化時(shí),${} 會(huì)被 FreeMarker 語(yǔ)法處理,導(dǎo)致錯(cuò)誤。
解決辦法:定義 FreeMarker 轉(zhuǎn)義字符如下
$ ==> ${"$"}
2.3.2 gradle.properties.ftl 合并失敗
根據(jù)錯(cuò)誤提示,執(zhí)行合并操作是只能針對(duì) xml 或者 gradle 文件進(jìn)行,其他文件并不支持合并。另外改用 copy 或 instantiate 命令也同樣失敗
同
proguard-rules.pro生成失敗。
解決辦法:將需要定義常量的代碼移動(dòng)到工程根目錄 build.gradle 中:定義在 ext{ } 內(nèi)
2.3.3 build.gradle 合并問(wèn)題
-
apply合并失敗期望結(jié)果
apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt'實(shí)際結(jié)果
apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application' dependencies中,apt引用代碼沒(méi)有出現(xiàn)
2.3.4 settings.gradle 文件合并問(wèn)題
為了工程目錄結(jié)構(gòu)更清晰些,我們?cè)?settings.gradle.ftl 文件中指定 module 的相對(duì)路徑,在 recipe.xml.ftl 執(zhí)行了 merge 操作。但得到錯(cuò)誤提示:settings.gradle.ftl 中只允許 include 命令。
解決辦法:將 module 工程放置在默認(rèn)目錄下,不再指定路徑
2.3.5 Java 代碼實(shí)例化問(wèn)題
模板中 java 代碼較多,我們統(tǒng)一放在 root/src/ 文件夾下,里面有部分文件含有 FreeMarker 標(biāo)簽,有部分只是純粹的 java 代碼。而使用 instantiate 命令對(duì)整個(gè)文件夾進(jìn)行實(shí)例化操作,并不會(huì)觸發(fā) FreeMarker 語(yǔ)法執(zhí)行。
解決辦法:因 java 文件比較多,手寫 recipe.xml 標(biāo)簽命令繁瑣且容易出錯(cuò)。我們通過(guò)程序遞歸遍歷 root/src/ 下的全部代碼文件,并生成相應(yīng)的 instantiate 或 copy 命令
3 工程模板遺留問(wèn)題解答
工程模板相關(guān)源碼位置:
Mac 平臺(tái):
${android studio 安裝路徑}/Contents/plugins/android/lib/android.jar
Windows 平臺(tái):
${android studio 安裝路徑}/plugins/android/lib/android.jar
具體類在 com/android/tools/idea/templates/ 里面。
3.1 copy 和 instantiate 問(wèn)題
gradle.properties文件執(zhí)行copy或者instantiate操作無(wú)效果原因?copy和instantiate對(duì)文件夾操作的區(qū)別
3.1.1 copy 命令
查看 DefaultRecipeExecutor.copy 方法,這里是直接簡(jiǎn)單的調(diào)用 copyTemplateResource 方法,該函數(shù)的基本邏輯如下:
如果 source 是一個(gè)文件夾,則執(zhí)行
copyDirectory方法,里面會(huì)遞歸的執(zhí)行文件夾內(nèi)的文件,其中如果葉子文件 (非文件夾) 對(duì)應(yīng)的目標(biāo)文件存在,則不執(zhí)行拷貝,繼續(xù)處理其他文件如果 source 非文件夾,且目標(biāo)文件存在,則不執(zhí)行拷貝
當(dāng)上面的條件都不滿足的情況下,執(zhí)行文件拷貝操作
期間沒(méi)有使用
FreemarkerUtils對(duì)FreeMarker語(yǔ)法進(jìn)行處理
3.1.2 instantiate 命令
直接查看 DefaultRecipeExecutor.instantiate 方法,該函數(shù)的基本邏輯如下:
如果
from文件是一個(gè)文件夾,則執(zhí)行copyTemplateResource方法,和copy流程一樣如果
from文件非文件夾,且目標(biāo)文件已經(jīng)存在了,則不執(zhí)行文件操作當(dāng)上面的條件都不滿足的情況下,先執(zhí)行
FreemarkerUtils的靜態(tài)方法processFreemarkerTemplate來(lái)處理FreeMarker語(yǔ)法,之后再執(zhí)行文件拷貝操作
3.1.3 遺留問(wèn)題解答
-
gradle.properties文件執(zhí)行copy或者instantiate操作無(wú)效果原因?解答:在執(zhí)行我們的工程模板執(zhí)行,已經(jīng)執(zhí)行了
gradle-projects/NewAndroidProject模板,并生成了gradle.properties文件,因此執(zhí)行copy或instantiate都因目標(biāo)文件已經(jīng)存在而不再執(zhí)行 -
copy和instantiate對(duì)文件夾操作的區(qū)別解答:如果
from指定一個(gè)文件夾,都是執(zhí)行copyTemplateResource方法,2 者沒(méi)有區(qū)別
3.2 merge 問(wèn)題
gradle.properties文件執(zhí)行merge操作失敗原因settings.gradle文件合并,指定module路徑錯(cuò)誤原因apt語(yǔ)句消失原因apply語(yǔ)句合并錯(cuò)誤原因
3.2.1 merge 主流程解析
查看 DefaultRecipeExecutor.merge 方法,基本邏輯如下:

3.2.2 settings.gradle 合并
查看 RecipeMergeUtils.mergeGradleSettingsFile 方法,基本邏輯如下:
-
讀取目標(biāo)文件的每一行內(nèi)容,并判斷每行內(nèi)容的開頭是否是
include開頭- 是:在 include 后面插入內(nèi)容
- 否:拋出異常
返回合并的內(nèi)容
3.2.3 build.gradle 合并
查看 GradleFileMerger.mergeGradleFiles 方法,里面會(huì)調(diào)用 mergePsi 方法,其基本邏輯如下:
讀取文件
source和dest文件的內(nèi)容,并轉(zhuǎn)化得到GroovyFile類型對(duì)象執(zhí)行
mergePsi方法
這里 mergePsi 執(zhí)行合并的邏輯是

繼續(xù)查看 dependencies 合并的源碼 GradleFileMerger.mergeDependencies 方法
里面的基本邏輯邏輯是:
收集 toRoot 中能解析的
compile子元素,并將收集到的子元素從 toRoot 中刪除收集 fromRoot 中的能解析的
compile子元素,并刪除能解析的compile子元素,另外單獨(dú)收集不能解析的complie子元素遍歷全部能解析的
compile子元素,比較相同compile語(yǔ)句的最大版本號(hào),并插入到 toRoot 中遍歷不能解析的
compile子元素,將內(nèi)容添加至toRoot中
fromRoot 是我們自定義的模板文件夾中定義的
dependencies內(nèi)容
toRoot 是執(zhí)行
gradle-project中的工程模板初始創(chuàng)建的dependencies內(nèi)容
3.2.4 遺留問(wèn)題解答
-
gradle.properties文件執(zhí)行merge操作失敗原因解答:根據(jù)
DefaultRecipeExecutor.merge方法的邏輯,我們可以看到當(dāng)to文件不存在,則執(zhí)行copy或instantiate命令;如果to文件存在且可讀,則僅對(duì)xml或gradle才能執(zhí)行merge操作 -
settings.gradle文件合并,指定module路徑錯(cuò)誤原因解答:只允許每行開頭是
include命令,其他情況拋出異常 -
apt語(yǔ)句消失原因解答:
pullDependenciesIntoMap方法僅處理from文件中dependencies中的compile子元素,其他如apt、provided命令都是會(huì)被忽略掉。 -
apply語(yǔ)句合并錯(cuò)誤原因// 我們的工程模板文件內(nèi)容 - 對(duì)應(yīng) mergePsi 方法中 toRoot 參數(shù) apply plugin: 'com.neenbedankt.android-apt' // 源工程模板初始生成的 `buidl.gradle` 文件內(nèi)容 - 對(duì)應(yīng) mergePsi 方法中 fromRoot 參數(shù) apply plugin: 'com.android.application' // 期望合并結(jié)果 apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'com.android.application' // 實(shí)際合并結(jié)果 apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'image大概畫了執(zhí)行流程,里面的關(guān)鍵流程如下:
- 步驟 2: fromRoot 和 toRoot 不是 call 語(yǔ)句
- 步驟 5: 都能找到
apply類型的子元素 - 步驟 6: 2 個(gè)
apply的第一個(gè)子元素都不是 dependencies - 步驟 11: fromRoot 中的 apply 子元素
plugin: 'com.android.application'和 toRoot 中的apply子元素的plugin: 'com.neenbedankt.android-apt'不對(duì)應(yīng) - 步驟12: 將
plugin: 'com.android.application'添加到 toRoot 的apply子元素前面
根據(jù)上面的分析,看起來(lái) apply 的這個(gè)合成結(jié)果是 google 工程模板的 bug,是不是應(yīng)該提供對(duì) apply 合并的特殊處理?
3.3 小結(jié)
到現(xiàn)在,我們建立了自己的工程模板。原來(lái)編碼過(guò)程中碰到的問(wèn)題,現(xiàn)在也已經(jīng)從源碼解析的角度做了解釋。一些問(wèn)題,如 gradle 文件中,dependencies 元素合并忽略自定義模板文件中的非 compile 子元素;apply 元素合并不符合我們的需求。最后導(dǎo)致我們不得不放棄 apt 引入。這些問(wèn)題 (或者說(shuō)是限制),不知 Google 方面是出于什么考慮還是本身的 bug。
4 網(wǎng)絡(luò)請(qǐng)求代碼自動(dòng)生成
對(duì)于 Android 工程模板安裝,我們提供的插件已經(jīng)實(shí)現(xiàn)了下載和安裝功能。
其次,在當(dāng)前的工程當(dāng)中,我們還需要有工具,能根據(jù) NEI 接口定義平臺(tái) 中定義的網(wǎng)絡(luò)接口,自動(dòng)生成我們的網(wǎng)絡(luò)請(qǐng)求相關(guān)代碼 (包括各個(gè) Request 類和 JavaBean)。針對(duì)網(wǎng)絡(luò)請(qǐng)求代碼的自動(dòng)生成,我們開發(fā)了 nei-toolkit,詳細(xì)安裝使用介紹可以查看 README.md
為了讓 Android 開發(fā)人員能更加方便的使用 nei-toolkit,我們?cè)诓寮屑闪?nei-toolkit 的下載、安裝、使用。
4.1 插件開發(fā)基礎(chǔ)
所有基于 IntelliJ Platform 的IDE,包括 Intellij Idea,Android Studio,Web Storm 等等,都可以為其添加插件以實(shí)現(xiàn)一些額外的功能。插件可以從本地安裝,也可以從 JetBrains Plugin Repository 安裝。Intellij 提供了一系列 API,使我們可以自定義插件。
-
如何配置插件開發(fā)的環(huán)境,可以查看 Setting Up a Development Environment
需要注意的是,配置 Project language level 為
Java 6,才能支持大部分的 Android Studio 插件開發(fā)的其他基礎(chǔ)知識(shí),如設(shè)置按鈕,如何處理事件邏輯,如何定義插件 id,名稱,版本號(hào)等內(nèi)容,可以查看 官方文檔
4.2 執(zhí)行終端命令
這里代碼生成功能最終也還是執(zhí)行了 nei-toolkit 中的命令來(lái)完成 http 代碼生成的,因此我們使用的是 Runtime 方法來(lái)執(zhí)行。
Process proc = Runtime.getRuntime().exec(command);
// 指定調(diào)用程序的工作目錄
Process proc = Runtime.getRuntime().exec(cmd, null, new File(project.getBasePath()));
-
執(zhí)行下載工程模板命令:
git clone ${ht-template git 地址} /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/activities/HTTemplateMacOS 平臺(tái)
-
執(zhí)行代碼生成命令
/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/ --doNotOverwriteMacOS 平臺(tái)
此外我們提供 NeiConsole 控制臺(tái),顯示腳本執(zhí)行輸出

5 小結(jié)和后續(xù)工作
到此,基本上完成了我們?cè)绕谕麑?shí)現(xiàn)的工程模板和網(wǎng)絡(luò)請(qǐng)求代碼自動(dòng)生成的工作:
提供
ht-template支持生成我們的模板工程-
提供 Android Studio 插件 (
NEIPlugin)- 支持
ht-template的下載安裝 - nei-toolkit 和 Node.js 的下載安裝
- nei-toolkit 和 Node.js 的使用,生成網(wǎng)絡(luò)請(qǐng)求代碼
- 支持
這里還是有一些因?yàn)?Android 工程模板自身的限制而無(wú)法完成的內(nèi)容點(diǎn):
無(wú)法在
settings.gradle指定module路徑無(wú)法合并
proguard-rules.pro文件,暫時(shí)生成proguard-rules.pro.template文件由于
build.gradle對(duì)apply命令合并會(huì)出錯(cuò)和無(wú)法合并dependencies中的apt命令,所以無(wú)法在build.gradle中集成ht-universalrouter
再次,除了網(wǎng)絡(luò)請(qǐng)求代碼編寫是機(jī)械性的,其他的基于我們的工程模板生成的初始工程,也存在一定的代碼編寫機(jī)械性:初始頁(yè)面代碼生成、RecycleView 中的各個(gè) ViewHolder 類、本地?cái)?shù)據(jù)讀取保存等,而這些工作將會(huì)是我們的后續(xù)工作。


