背景
口碑的 O2O 業務 Bundle,目前需要在支付寶和口碑獨客這兩個 App 中的執行。目前口碑 App 也是使用 mPaaS 框架,一些基礎服務比如 ConfigService,H5 容器,RPC 網路庫,AntUI 庫,Sync,掃碼,Push 等,和支付寶保持一致,並對於不相容的地方進行拉分支單獨改造,對於支援多 App 的 Bundle,直接使用支付寶的基線。
那麼,每次業務在支付寶上發版之後,同步到口碑 App 時,都需要將口碑 App 的基線進行升級。所謂基線升級,就是將支付寶中對應的 Bundle 版本號同步到獨客,將有定製分支的 Bundle 進行程式碼 merge。支付寶 App 有幾百個 Bundle,而口碑 App 的 Bundle 規模也已達到相似規模。
這幾百個 Bundle 中,其中幾十個 Bundle 是口碑 App 有分支定製以及特有的,剩下的 Bundle 直接從支付寶已有體系內進行索引。
為了減小包大小,我們需要確定這些 Bundle 之間的依賴關係,更確切一點,我們想知道這些 Bundle 的依賴程度:如果刪除某個 Bundle,將會對剩餘的哪些 Bundle 有影響?有哪些 Bundle 可以直接刪除?解決這些問題,我們需要分析這幾百多個 Bundle 的依賴關係。
1. Bundle 依賴分析方法
幾百個 Bundle,靠人工一個個看,是梳理不過來的。而且,每個版本都有程式碼更新,依賴關係都有可能變化。因此,需要我們開發對應的工具進行分析。
方案 1:分析 build.gradle
我們知道,在 Android 開發中,如果我們需要依賴某個 Jar 包,我們會在 module 的 build.gradle 中新增,比如:
dependencies {
provided
'com.alipay.android.phone.thirdparty:fastjson-api:1.1.45@jar'
provided
'com.alipay.android.phone.thirdparty:wire-api:1.5.3@jar'
...
}
複製程式碼
對於某個 Bundle,我們可以通過分析 Bundle 中的 module,然後解析 build.gradle 檔案,獲取 Bundle 之間的依賴。
【結論】
這種方案出現的問題:
- dependencies 的依賴可能有冗餘,會有多餘的 dependencies 出現,會影響結果的準確性;
- 這種方案我們只能在知道 Bundle 之間的依賴關係,並不知道依賴了其中的多少個類?有哪些地方依賴?
方案 2:分析每個 Bundle 的每個 Java 檔案的 import 區域
為了解決方案 1 中的問題,我們使用方案 2 進行依賴分析,就是分析每個 Bundle 的每個 Java 檔案的 import 區域,然後建立它們之間的對映關係。
以 o2ocommon 這個 Bundle 為例,bundle、module、class 和 import 的區域關係如下:
然後,我們將 Bundle 之間的依賴,轉換為分析 Java 檔案之間的依賴;並且能夠計算出 Bundle 之間有多少個類依賴,以及依賴了多少次。
【結論】
方案 2 通過
複製程式碼
2. 方案實施:JDA 依賴分析工具開發
2.1 拉取每個 Bundle 對應的原始碼
通過指令碼,將 Bundle 對應的 gitlab 程式碼庫拉取到的本地,切換到需要的分支。
2.2 根據 setting.gradle 建立 Bundle
遍歷 Bundle 目錄,如果查詢到 setting.gradle 檔案,就建立一下 Bundle 物件:
public class Bundle implements Serializable, Comparable<Bundle> {
private String localPath;
private String name;//資料夾名稱
private List<GradleModule> moduleList;//包含的module
private String packageId;
private String groupId;//groupId
private String artifactId;//artifactId
private Map<Bundle, Dependency> dependencyMap;//依賴關係表
...
}
複製程式碼
2.3 根據 build.gradle 建立 module
建立好 bundle 之後,遍歷 bundle 的子目錄,查詢 build.gradle 檔案,然後建立 module 物件:
public class GradleModule implements Serializable{
private String localPath;
private String name;//module的名稱
private Bundle bundle;//隸屬那個Bundle
private List<JavaFile> javaFileList;//module包含的import
...
}
複製程式碼
2.4 查詢 Java 檔案所在的 src 目錄,建立 JavaFile
查詢 build.gradle 中的 src 屬性,找到 Java 程式碼的存放位置,獲取 *.java 檔案的列表,建立 JavaFile 物件:
public class JavaFile implements Serializable {
private String className;//類全稱
private List<ImportModel> imports;//該類的imports檔案
private GradleModule parentModule;//所在的Bundle
...
}
複製程式碼
2.5 解析 Java 檔案
這一步是整個方案的核心,需要解析 Java 的語法,將 Java 檔案的 import 區域過濾出來。
2.6 整理 import 區域,刪除多餘的 import
將 import 的類檔案,在 Java 檔案中進行搜尋,如果未引用到,則刪除該 import,如果存在,則保留。然後建立 import 物件:
public class ImportModel implements Serializable {
private String className;//該import的包名+類名
private Bundle dependBundle;//該類所在的Bundle
...
}
複製程式碼
2.7 建立對映關係表 Map
經過上述遞迴演算法,我們建立了 Bundle、Module、JavaFile、ImportModel 之間的樹結構。並且儲存了所有 Java 檔案與其所屬 Bundle 之間的對映關係。
private Map<String, Bundle> mJavaFileBundleMap = new LinkedHashMap<>();//Java檔案與所屬Bundle之間的對映關係
private List<JavaFile> mAllJavaFile = new ArrayList<>();//所有Java檔案的List
private List<Bundle> mBundleList = new ArrayList<>();//所有Bundle的List
複製程式碼
3.8 依賴分析
/**
* 依賴分析
* mochuan.zhb@alibaba-inc.com
*/
private void dependenciesAnalysis() {
for (JavaFile javaFile : mAllJavaFile) {//遍歷所有的Java檔案
//獲取Java檔案的Import區域列表
List<ImportModel> importModelList = javaFile.getImports();
//獲取當前Java檔案所在的Bundle
Bundle currentBundle = javaFile.getParentModule().getBundle();
//獲取當前Bundle與其他Bundle依賴對映表
Map<Bundle, Dependency> dependencyMap = currentBundle.getDependencyMap();
if (dependencyMap == null) {
dependencyMap = new HashMap<>();
currentBundle.setDependencyMap(dependencyMap);
}
if (importModelList == null || importModelList.size() == 0) {
continue;
}
//遍歷Import列表
for (ImportModel importModel : importModelList) {
String importClassName = importModel.getClassName();
if (isClassInWhiteList(importClassName)) {
continue;
}
//查詢import中類,所在的Bundle
Bundle bundle = mJavaFileBundleMap.get(importClassName);
if (bundle == null) {
//沒有查到該類所在的Bundle
JDALog.info(String.format("%s depend bundle not found.", importModel.getClassName()));
} else if (bundle == javaFile.getParentModule().getBundle()) {
//內部依賴;該import類和當前類在同一個Bundle中
JDALog.info("internal depend.");
} else {
//currentBundle依賴bundle
Dependency dependency = dependencyMap.get(bundle);
if (dependency == null) {
dependency = new Dependency();
dependencyMap.put(bundle, dependency);
}
//將依賴次數+1
dependency.setDependCount(dependency.getDependCount() + 1);
//能找到對應的Bundle依賴
JDALog.info(String.format("%s depend %s", javaFile.getParentModule().getBundle().getName(), bundle.getName()));
}
}
}
}
複製程式碼
3. 依賴結果分析
我們把有相互依賴的 Bundle 進行連線,得到如下圖:
化成圓形的圖為:
為了更加準確地衡量 Bundle 之間的依賴程度,後續我們可以將依賴關係轉換成 markdown 表格形式:更具體地展示 Bundle 之間依賴、以及被依賴的情況,以及被依賴多少次也能夠清晰展現。除此之外,我們甚至可以知道具體是依賴哪個類。
4. 總結
1. 上述分析方法有效:
有了上述分析結果,為我們後續減小包大小、增刪 Bundle、Bundle 升級提供了強有力的指導,為後續解除 Bundle 之間的依賴提供了詳細的資料參考;
2. 從依賴表中,我們也可以看到哪些 Bundle 是葉子節點,可以根據是否葉子節點確定 packageId 的分配。
3. 對於通過反射的方式進行依賴的情況,目前還比較難統計到:
比如 Class.forName("com.koubei.android.xxx")
之類的,後續可以考慮其他方案進行完善。