0 背景
我們網(wǎng)易前端技術(shù)部 - 移動技術(shù)組作為公司的移動端基礎(chǔ)技術(shù)部門寺庄,主要為其他部門提供解決方案艾蓝、技術(shù)支持和產(chǎn)品孵化。在幾年的積累過程中斗塘,我們擁有一些自己的框架和 SDK
赢织,如輕應(yīng)用框架、熱更新 SDK馍盟、網(wǎng)絡(luò)請求庫于置、本地存儲庫、頁面管理等贞岭,服務(wù)過網(wǎng)易新聞八毯、云音樂、考拉瞄桨、易信等億級產(chǎn)品话速,先后孵化過青果攝像頭、二次元Gacha芯侥、嚴(yán)選等重要產(chǎn)品泊交。
在多年的Android開發(fā)中,對于 Android 端產(chǎn)品開發(fā)柱查,我們有如下幾點體會:
-
產(chǎn)品孵化排期緊張
產(chǎn)品經(jīng)理一般關(guān)心的是具體的業(yè)務(wù)邏輯廓俭,而前期基礎(chǔ)模塊的搭建,如各模塊如何組織物赶,使用代碼結(jié)構(gòu)如何選擇白指,圖片、網(wǎng)絡(luò)酵紫、本地存儲等選用哪個 sdk 等告嘲,一般不會有專門排期错维。
-
基礎(chǔ)模塊的需求具有相似性
內(nèi)容型產(chǎn)品,其搭建的基礎(chǔ)模塊基本上都會包含圖片顯示橄唬、網(wǎng)絡(luò)請求赋焕、本地存儲、通信等仰楚。
-
基礎(chǔ)模塊的選型和工具類具有可重用性
網(wǎng)上相關(guān)的第三方庫有很多隆判,當(dāng)然一般的公司也是會有自己開發(fā)或者維護的各個基礎(chǔ) SDK。很多時候僧界,SDK 選型會更偏向于自己公司開發(fā)維護的 SDK侨嘀,或者選擇自己最熟悉,或最主流捂襟、最可靠的 SDK咬腕。因此當(dāng)開發(fā)多個相同類型產(chǎn)品時,這里的技術(shù)選型是可重用的葬荷。
-
網(wǎng)絡(luò)請求的代碼具有機械性
客戶端開發(fā)需要根據(jù)網(wǎng)絡(luò)接口協(xié)議涨共,編寫相關(guān)的
GET
、POST
等請求代碼和對應(yīng)的JavaBean
宠漩,這部分的代碼編寫其實是非常機械的举反。
1 網(wǎng)易工程模板是什么?
對于各個基礎(chǔ)模塊扒吁,我們團隊封裝了自己的 SDK火鼻,如網(wǎng)絡(luò)庫、本地存儲庫雕崩、頁面管理庫凝危、圖片庫等。使用我們的工程模板生成的初始工程晨逝,就已經(jīng)包含了我們提供的基礎(chǔ)模塊蛾默,產(chǎn)品團隊的開發(fā)不需要再花費重復(fù)的時間做技術(shù)調(diào)研、選型捉貌、SDK封裝集成等工作支鸡,而只需要關(guān)心自己的業(yè)務(wù)邏輯編寫。我們期望產(chǎn)品團隊只需 1 分鐘就能得到自己的初始工程趁窃,并能馬上投入業(yè)務(wù)邏輯開發(fā)牧挣,既能縮短開發(fā)周期,也能保證工程代碼質(zhì)量醒陆。
此外瀑构,我們也提供了 Android Studio 插件 (NEIPlugin
),集成插件后刨摩,就能在 Android Studio 中通過菜單點擊自動下載集成我們的工程模板寺晌,也能自動生成網(wǎng)絡(luò)請求相關(guān)的代碼世吨。
工程模板
HTTemplate
代碼生成結(jié)果示例
2 Android 模板工程實現(xiàn)
最初我們使用終端腳本命令的方式,通過文件拷貝和文本查找替換(主要是替換包名等)的方式實現(xiàn)呻征。但終歸對 Android 開發(fā)人員不太友好耘婚,畢竟大家更習(xí)慣使用 Android Studio 生成工程。所幸陆赋,強大的 Android Studio 已經(jīng)提供了較為全面的模板功能沐祷,這里大概可以分為以下幾類:
工程模板 (本文內(nèi)容)
2.1 Android 工程模板基礎(chǔ)知識
2.1.1 工程模板實例介紹
對于 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)建一個空頁面的模板赖临,GoogleMapsActivity
文件夾對應(yīng)創(chuàng)建一個地圖頁面的模板等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
(空白頁面模板) 里面的內(nèi)容
globals.xml.ftl
: 全局變量文件,保存一些全局變量誊册,當(dāng)中可以引用其他文件的全局變量recipe.xml.ftl
: 配置要引用的模板路徑以及文件的生成規(guī)則-
template.xml
: 模板的配置信息领突,包括模板的顯示圖標(biāo),界面的表現(xiàn)案怯,全局變量文件和執(zhí)行文件的指定等 template_blank_activity.png
: 顯示的縮略圖SimpleActivity.java.ftl
: Activity 模板文件-
代碼生成過程圖
Android Studio 使用的是
FreeMarker
模板引擎君旦,所以文件后綴都是.ftl
2.1.2 常用標(biāo)簽使用
${}
:FreeMarker
的語法,如${packageName}
,${superClass}
是globals.xml.ftl
全局變量文件或template.xml.ftl
中定義變量引用<#if></#if>
:FreeMarker
的語法嘲碱,條件判斷語句<#include>
:FreeMarker
的語法金砍,包含語句copy
: 將文件或者文件夾從 from 標(biāo)簽拷貝到 to 標(biāo)簽指定的路徑instantiate
: 將文件或者文件夾,執(zhí)行FreeMarker
語法麦锯,從 from 標(biāo)簽實例化到 to 標(biāo)簽指定的路徑merge
: 合并 from 和 to 標(biāo)簽分別指定的文件-
open
: 在工程打開后恕稠,默認(rèn)打開指定的文件實例:使用空白頁面模板生成工程并打開后,可以看到默認(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
文件夾里面的全部內(nèi)容copy
module
工程copy
proguard-rules.pro
文件
-
root 文件夾
放置相關(guān)模板源文件,其中將源工程中依賴于配置的代碼候学,按照
FreeMarker
語法進(jìn)行替換 添加工程模板圖標(biāo),并在
template.xml
中添加引用
工程模板創(chuàng)建結(jié)果
2.3 遇到的坑與解決辦法
2.3.1 build.gradle ${}
通配符沖突
當(dāng)工程模板實例化時,${}
會被 FreeMarker
語法處理敛苇,導(dǎo)致錯誤。
解決辦法:定義 FreeMarker
轉(zhuǎn)義字符如下
$ ==> ${"$"}
2.3.2 gradle.properties.ftl
合并失敗
根據(jù)錯誤提示顺呕,執(zhí)行合并操作是只能針對 xml
或者 gradle
文件進(jìn)行枫攀,其他文件并不支持合并括饶。另外改用 copy
或 instantiate
命令也同樣失敗
同
proguard-rules.pro
生成失敗。
解決辦法:將需要定義常量的代碼移動到工程根目錄 build.gradle
中:定義在 ext{ }
內(nèi)
2.3.3 build.gradle 合并問題
-
apply
合并失敗期望結(jié)果
apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt'
實際結(jié)果
apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'
dependencies
中脓豪,apt
引用代碼沒有出現(xiàn)
2.3.4 settings.gradle
文件合并問題
為了工程目錄結(jié)構(gòu)更清晰些巷帝,我們在 settings.gradle.ftl
文件中指定 module
的相對路徑,在 recipe.xml.ftl
執(zhí)行了 merge
操作扫夜。但得到錯誤提示:settings.gradle.ftl
中只允許 include
命令楞泼。
解決辦法:將 module
工程放置在默認(rèn)目錄下,不再指定路徑
2.3.5 Java 代碼實例化問題
模板中 java
代碼較多笤闯,我們統(tǒng)一放在 root/src/
文件夾下堕阔,里面有部分文件含有 FreeMarker
標(biāo)簽,有部分只是純粹的 java 代碼颗味。而使用 instantiate
命令對整個文件夾進(jìn)行實例化操作超陆,并不會觸發(fā) FreeMarker
語法執(zhí)行。
解決辦法:因 java
文件比較多浦马,手寫 recipe.xml
標(biāo)簽命令繁瑣且容易出錯时呀。我們通過程序遞歸遍歷 root/src/
下的全部代碼文件,并生成相應(yīng)的 instantiate
或 copy
命令
3 工程模板遺留問題解答
工程模板相關(guān)源碼位置:
Mac 平臺:
${android studio 安裝路徑}/Contents/plugins/android/lib/android.jar
Windows 平臺:
${android studio 安裝路徑}/plugins/android/lib/android.jar
具體類在 com/android/tools/idea/templates/
里面晶默。
3.1 copy 和 instantiate 問題
gradle.properties
文件執(zhí)行copy
或者instantiate
操作無效果原因谨娜?copy
和instantiate
對文件夾操作的區(qū)別
3.1.1 copy 命令
查看 DefaultRecipeExecutor.copy
方法,這里是直接簡單的調(diào)用 copyTemplateResource
方法磺陡,該函數(shù)的基本邏輯如下:
如果 source 是一個文件夾趴梢,則執(zhí)行
copyDirectory
方法,里面會遞歸的執(zhí)行文件夾內(nèi)的文件币他,其中如果葉子文件 (非文件夾) 對應(yīng)的目標(biāo)文件存在坞靶,則不執(zhí)行拷貝,繼續(xù)處理其他文件如果 source 非文件夾蝴悉,且目標(biāo)文件存在彰阴,則不執(zhí)行拷貝
當(dāng)上面的條件都不滿足的情況下,執(zhí)行文件拷貝操作
期間沒有使用
FreemarkerUtils
對FreeMarker
語法進(jìn)行處理
3.1.2 instantiate 命令
直接查看 DefaultRecipeExecutor.instantiate
方法拍冠,該函數(shù)的基本邏輯如下:
如果
from
文件是一個文件夾硝枉,則執(zhí)行copyTemplateResource
方法,和copy
流程一樣如果
from
文件非文件夾倦微,且目標(biāo)文件已經(jīng)存在了妻味,則不執(zhí)行文件操作當(dāng)上面的條件都不滿足的情況下,先執(zhí)行
FreemarkerUtils
的靜態(tài)方法processFreemarkerTemplate
來處理FreeMarker
語法欣福,之后再執(zhí)行文件拷貝操作
3.1.3 遺留問題解答
-
gradle.properties
文件執(zhí)行copy
或者instantiate
操作無效果原因责球?解答:在執(zhí)行我們的工程模板執(zhí)行,已經(jīng)執(zhí)行了
gradle-projects/NewAndroidProject
模板,并生成了gradle.properties
文件雏逾,因此執(zhí)行copy
或instantiate
都因目標(biāo)文件已經(jīng)存在而不再執(zhí)行 -
copy
和instantiate
對文件夾操作的區(qū)別解答:如果
from
指定一個文件夾嘉裤,都是執(zhí)行copyTemplateResource
方法,2 者沒有區(qū)別
3.2 merge 問題
gradle.properties
文件執(zhí)行merge
操作失敗原因settings.gradle
文件合并栖博,指定module
路徑錯誤原因apt
語句消失原因apply
語句合并錯誤原因
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
方法典奉,里面會調(diào)用 mergePsi
方法,其基本邏輯如下:
讀取文件
source
和dest
文件的內(nèi)容丧叽,并轉(zhuǎn)化得到GroovyFile
類型對象執(zhí)行
mergePsi
方法
這里 mergePsi
執(zhí)行合并的邏輯是
繼續(xù)查看 dependencies
合并的源碼 GradleFileMerger.mergeDependencies
方法
里面的基本邏輯邏輯是:
收集 toRoot 中能解析的
compile
子元素卫玖,并將收集到的子元素從 toRoot 中刪除收集 fromRoot 中的能解析的
compile
子元素,并刪除能解析的compile
子元素踊淳,另外單獨收集不能解析的complie
子元素遍歷全部能解析的
compile
子元素假瞬,比較相同compile
語句的最大版本號,并插入到 toRoot 中遍歷不能解析的
compile
子元素迂尝,將內(nèi)容添加至toRoot
中
fromRoot 是我們自定義的模板文件夾中定義的
dependencies
內(nèi)容
toRoot 是執(zhí)行
gradle-project
中的工程模板初始創(chuàng)建的dependencies
內(nèi)容
3.2.4 遺留問題解答
-
gradle.properties
文件執(zhí)行merge
操作失敗原因解答:根據(jù)
DefaultRecipeExecutor.merge
方法的邏輯脱茉,我們可以看到當(dāng)to
文件不存在,則執(zhí)行copy
或instantiate
命令垄开;如果to
文件存在且可讀琴许,則僅對xml
或gradle
才能執(zhí)行merge
操作 -
settings.gradle
文件合并,指定module
路徑錯誤原因解答:只允許每行開頭是
include
命令说榆,其他情況拋出異常 -
apt
語句消失原因解答:
pullDependenciesIntoMap
方法僅處理from
文件中dependencies
中的compile
子元素,其他如apt
寸认、provided
命令都是會被忽略掉签财。 -
apply
語句合并錯誤原因// 我們的工程模板文件內(nèi)容 - 對應(yīng) mergePsi 方法中 toRoot 參數(shù) apply plugin: 'com.neenbedankt.android-apt' // 源工程模板初始生成的 `buidl.gradle` 文件內(nèi)容 - 對應(yīng) mergePsi 方法中 fromRoot 參數(shù) apply plugin: 'com.android.application' // 期望合并結(jié)果 apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'com.android.application' // 實際合并結(jié)果 apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'
大概畫了執(zhí)行流程,里面的關(guān)鍵流程如下:
- 步驟 2: fromRoot 和 toRoot 不是 call 語句
- 步驟 5: 都能找到
apply
類型的子元素 - 步驟 6: 2 個
apply
的第一個子元素都不是 dependencies - 步驟 11: fromRoot 中的 apply 子元素
plugin: 'com.android.application'
和 toRoot 中的apply
子元素的plugin: 'com.neenbedankt.android-apt'
不對應(yīng) - 步驟12: 將
plugin: 'com.android.application'
添加到 toRoot 的apply
子元素前面
根據(jù)上面的分析偏塞,看起來 apply
的這個合成結(jié)果是 google
工程模板的 bug
唱蒸,是不是應(yīng)該提供對 apply 合并的特殊處理?
3.3 小結(jié)
到現(xiàn)在灸叼,我們建立了自己的工程模板神汹。原來編碼過程中碰到的問題,現(xiàn)在也已經(jīng)從源碼解析的角度做了解釋古今。一些問題屁魏,如 gradle
文件中,dependencies
元素合并忽略自定義模板文件中的非 compile
子元素捉腥;apply
元素合并不符合我們的需求氓拼。最后導(dǎo)致我們不得不放棄 apt
引入。這些問題 (或者說是限制),不知 Google
方面是出于什么考慮還是本身的 bug
桃漾。
4 網(wǎng)絡(luò)請求代碼自動生成
對于 Android 工程模板安裝坏匪,我們提供的插件已經(jīng)實現(xiàn)了下載和安裝功能。
其次撬统,在當(dāng)前的工程當(dāng)中适滓,我們還需要有工具,能根據(jù) NEI 接口定義平臺 中定義的網(wǎng)絡(luò)接口恋追,自動生成我們的網(wǎng)絡(luò)請求相關(guān)代碼 (包括各個 Request
類和 JavaBean
)凭迹。針對網(wǎng)絡(luò)請求代碼的自動生成,我們開發(fā)了 nei-toolkit几于,詳細(xì)安裝使用介紹可以查看 README.md
為了讓 Android 開發(fā)人員能更加方便的使用 nei-toolkit蕊苗,我們在插件中集成了 nei-toolkit
的下載、安裝沿彭、使用朽砰。
4.1 插件開發(fā)基礎(chǔ)
所有基于 IntelliJ Platform
的IDE,包括 Intellij Idea
喉刘,Android Studio
瞧柔,Web Storm
等等,都可以為其添加插件以實現(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è)置按鈕牵祟,如何處理事件邏輯深夯,如何定義插件 id,名稱诺苹,版本號等內(nèi)容咕晋,可以查看 官方文檔
4.2 執(zhí)行終端命令
這里代碼生成功能最終也還是執(zhí)行了 nei-toolkit 中的命令來完成 http 代碼生成的,因此我們使用的是 Runtime
方法來執(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/HTTemplate
MacOS 平臺
-
執(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/ --doNotOverwrite
MacOS 平臺
此外我們提供 NeiConsole
控制臺掌呜,顯示腳本執(zhí)行輸出
5 小結(jié)和后續(xù)工作
到此,基本上完成了我們原先期望實現(xiàn)的工程模板和網(wǎng)絡(luò)請求代碼自動生成的工作:
提供
ht-template
支持生成我們的模板工程-
提供 Android Studio 插件 (
NEIPlugin
)- 支持
ht-template
的下載安裝 - nei-toolkit 和 Node.js 的下載安裝
- nei-toolkit 和 Node.js 的使用坪哄,生成網(wǎng)絡(luò)請求代碼
- 支持
這里還是有一些因為 Android 工程模板自身的限制而無法完成的內(nèi)容點:
無法在
settings.gradle
指定module
路徑無法合并
proguard-rules.pro
文件站辉,暫時生成proguard-rules.pro.template
文件由于
build.gradle
對apply
命令合并會出錯和無法合并dependencies
中的apt
命令呢撞,所以無法在build.gradle
中集成ht-universalrouter
再次,除了網(wǎng)絡(luò)請求代碼編寫是機械性的饰剥,其他的基于我們的工程模板生成的初始工程殊霞,也存在一定的代碼編寫機械性:初始頁面代碼生成、RecycleView
中的各個 ViewHolder
類汰蓉、本地數(shù)據(jù)讀取保存等绷蹲,而這些工作將會是我們的后續(xù)工作。