咱們網易前端技術部 - 移動技術組做爲公司的移動端基礎技術部門,主要爲其餘部門提供解決方案、技術支持和產品孵化。在幾年的積累過程當中,咱們擁有一些本身的框架和 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 命令所有都是不支持的,這個能夠從前面的源碼分析中看出來。