網(wǎng)易 Android 工程模板化實(shí)踐


視頻鏈接

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ì):

  1. 產(chǎn)品孵化排期緊張

    產(chǎn)品經(jīng)理一般關(guān)心的是具體的業(yè)務(wù)邏輯,而前期基礎(chǔ)模塊的搭建,如各模塊如何組織,使用代碼結(jié)構(gòu)如何選擇,圖片、網(wǎng)絡(luò)、本地存儲(chǔ)等選用哪個(gè) sdk 等,一般不會(huì)有專門排期。

  2. 基礎(chǔ)模塊的需求具有相似性

    內(nèi)容型產(chǎn)品,其搭建的基礎(chǔ)模塊基本上都會(huì)包含圖片顯示、網(wǎng)絡(luò)請(qǐng)求、本地存儲(chǔ)、通信等。

  3. 基礎(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ù)選型是可重用的。

  4. 網(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)的代碼。

image

工程模板 HTTemplate

image

代碼生成結(jié)果示例

2 Android 模板工程實(shí)現(xiàn)

最初我們使用終端腳本命令的方式,通過(guò)文件拷貝和文本查找替換(主要是替換包名等)的方式實(shí)現(xiàn)。但終歸對(duì) Android 開發(fā)人員不太友好,畢竟大家更習(xí)慣使用 Android Studio 生成工程。所幸,強(qiáng)大的 Android Studio 已經(jīng)提供了較為全面的模板功能,這里大概可以分為以下幾類:

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)模板的文件夾:

  1. activities:工程模板相關(guān),如 EmptyActivity 文件夾用于創(chuàng)建一個(gè)空頁(yè)面的模板,GoogleMapsActivity 文件夾對(duì)應(yīng)創(chuàng)建一個(gè)地圖頁(yè)面的模板等

  2. gradle:放置了 gradle 模板,用于在新建工程的根目錄下生成 gradle 文件夾,支持用戶不用安裝 gradle 就能使用 gradlew 命令

  3. gradle-project:工程模板相關(guān),用于構(gòu)建 moduleAndroid Project,Java Library

  4. other:構(gòu)建文件模板等

這里我們關(guān)心的是 activities 文件夾里面的內(nèi)容

首先查看下 EmtpyActivity (空白頁(yè)面模板) 里面的內(nèi)容

  1. globals.xml.ftl: 全局變量文件,保存一些全局變量,當(dāng)中可以引用其他文件的全局變量

  2. recipe.xml.ftl: 配置要引用的模板路徑以及文件的生成規(guī)則

  3. template.xml: 模板的配置信息,包括模板的顯示圖標(biāo),界面的表現(xiàn),全局變量文件和執(zhí)行文件的指定等

    image
  4. template_blank_activity.png: 顯示的縮略圖

  5. SimpleActivity.java.ftl: Activity 模板文件

  6. 代碼生成過(guò)程圖

    image

    圖片摘自 Tutorial How To Create Custom Android Code Templates

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.javaactivity_main.xml 文件

2.2 工程模板創(chuàng)建

新建 HTTemplate 文件夾內(nèi)容如下:

  1. template.xml

    指定模板名、描述、最低支持 sdk 版本、類別等,輸入界面要求指定包名和 Application 類名

  2. globals.xml.ftl

    引用公共文件內(nèi)容

  3. 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 文件

  4. root 文件夾

    放置相關(guān)模板源文件,其中將源工程中依賴于配置的代碼,按照 FreeMarker 語(yǔ)法進(jìn)行替換

  5. 添加工程模板圖標(biāo),并在 template.xml 中添加引用

image

工程模板創(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)行,其他文件并不支持合并。另外改用 copyinstantiate 命令也同樣失敗

proguard-rules.pro 生成失敗。

解決辦法:將需要定義常量的代碼移動(dòng)到工程根目錄 build.gradle 中:定義在 ext{ } 內(nèi)

2.3.3 build.gradle 合并問(wèn)題

  1. 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'
    
  2. 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)的 instantiatecopy 命令

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ú)效果原因?

  • copyinstantiate 對(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í)行 copyinstantiate 都因目標(biāo)文件已經(jīng)存在而不再執(zhí)行

  • copyinstantiate 對(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 方法,基本邏輯如下:

image

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 方法,其基本邏輯如下:

  • 讀取文件 sourcedest 文件的內(nèi)容,并轉(zhuǎn)化得到 GroovyFile 類型對(duì)象

  • 執(zhí)行 mergePsi 方法

這里 mergePsi 執(zhí)行合并的邏輯是

image

繼續(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í)行 copyinstantiate 命令;如果 to 文件存在且可讀,則僅對(duì) xmlgradle 才能執(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)鍵流程如下:

    1. 步驟 2: fromRoot 和 toRoot 不是 call 語(yǔ)句
    2. 步驟 5: 都能找到 apply 類型的子元素
    3. 步驟 6: 2 個(gè) apply 的第一個(gè)子元素都不是 dependencies
    4. 步驟 11: fromRoot 中的 apply 子元素 plugin: 'com.android.application' 和 toRoot 中的 apply 子元素的 plugin: 'com.neenbedankt.android-apt' 不對(duì)應(yīng)
    5. 步驟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 IdeaAndroid Studio,Web Storm 等等,都可以為其添加插件以實(shí)現(xiàn)一些額外的功能。插件可以從本地安裝,也可以從 JetBrains Plugin Repository 安裝。Intellij 提供了一系列 API,使我們可以自定義插件。

  1. 如何配置插件開發(fā)的環(huán)境,可以查看 Setting Up a Development Environment

    需要注意的是,配置 Project language level 為 Java 6,才能支持大部分的 Android Studio

  2. 插件開發(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/HTTemplate
    

    MacOS 平臺(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/ --doNotOverwrite
    

    MacOS 平臺(tái)

此外我們提供 NeiConsole 控制臺(tái),顯示腳本執(zhí)行輸出

image

5 小結(jié)和后續(xù)工作

到此,基本上完成了我們?cè)绕谕麑?shí)現(xiàn)的工程模板和網(wǎng)絡(luò)請(qǐng)求代碼自動(dòng)生成的工作:

  1. 提供 ht-template 支持生成我們的模板工程

  2. 提供 Android Studio 插件 (NEIPlugin)

這里還是有一些因?yàn)?Android 工程模板自身的限制而無(wú)法完成的內(nèi)容點(diǎn):

  1. 無(wú)法在 settings.gradle 指定 module 路徑

  2. 無(wú)法合并 proguard-rules.pro 文件,暫時(shí)生成 proguard-rules.pro.template 文件

  3. 由于 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ù)工作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評(píng)論 25 708
  • 由于項(xiàng)目用上了 mvp 架構(gòu),基本上一個(gè)頁(yè)面就至少需要新創(chuàng)建6個(gè)類,分別是 model view presente...
    大空ts翼閱讀 2,119評(píng)論 0 4
  • 這一章主要針對(duì)項(xiàng)目中可以用到的一些實(shí)用功能來(lái)介紹Android Gradle,比如如何隱藏我們的證書文件,降低風(fēng)險(xiǎn)...
    acc8226閱讀 7,972評(píng)論 3 25
  • 初入Flask學(xué)習(xí),遇到各種問(wèn)題,還是得解決。 基礎(chǔ)知識(shí)就不說(shuō)了,這個(gè)靠學(xué),練,記,從而生巧。今天說(shuō)說(shuō)我的程序在新...
    西北望高樓閱讀 1,358評(píng)論 0 2
  • 萍水相逢亦是緣閱讀 372評(píng)論 0 6

友情鏈接更多精彩內(nèi)容