高德APP全鏈路源碼依賴分析工程
一、背景
高德 App 經過多年的發(fā)展,其代碼量已達到數(shù)百萬行級別,支撐了高德地圖復雜的業(yè)務功能。但與此同時,隨著團隊的擴張和業(yè)務的復雜化,越來越碎片化的代碼以及代碼之間復雜的依賴關系帶來諸多維護性問題,較為突出的問題包括:
- 不敢輕易修改或下線對外暴露的接口或組件,因為不知道有什么地方對自己有依賴、會受到影響,于是代碼變得臃腫,包大小也變得越來越大;
- 模塊在沒有變動的情況下,發(fā)布到新版本的客戶端時,需要全量回歸測試整個功能,因為不知道所依賴的模塊是否有變動;
- 難以判斷 Native 從業(yè)務實現(xiàn)轉變?yōu)榈讓又蔚内厔菔欠窈侠恚卫硎欠裼行?
這些問題已經達到了我們必須開始治理的程度了,而解決此類問題的關鍵在于需要了解代碼間的依賴關系。
二、高德 APP 平臺架構
為了消除一些疑惑,在討論依賴分析的實現(xiàn)前,先簡單說明一下高德 APP 的平臺架構,以便對一些名詞和場景有一些背景了解。
高德 APP 從語言平臺上可以分為 4 個部分,JS 層主要負責業(yè)務邏輯和 UI 框架;中間有 C++層做高性能渲染(主要是地圖渲染),同時實現(xiàn)了一些切面 API,這樣可以在雙端只維護一套邏輯了;Android 和 iOS 層主要作為適配層,做一些操作系統(tǒng)接口的對接和雙端差異化的(盡可能)抹平。
這里的切面是指 JS 層與 Native/C++ 層的分界線,這里會實現(xiàn)一些切面 API,也就是 JS 層與 Native/C++ 層交互的一系列接口,如藍牙接口、系統(tǒng)信息接口等,由 Native/C++ 層來實現(xiàn)接口,然后往 JS 層暴露,由 JS 層調用。
三、基礎實現(xiàn)原理
整個項目最基本也是最重要的數(shù)據就是依賴關系。所謂依賴關系,最簡單的例子就是文件 A 依賴文件 B 的某個方法。
要將這個關系查出來,一般來說需要經過兩個步驟。
第一步:編譯源碼,獲得 AST
遍歷所有源碼,通過語法分析,生成抽象語法樹(Abstract Syntax Tree, AST)。以 JS 掃描器為例,我采用了 typeScript 模塊作為編譯器,它同時支持 JS(X)、TS(X),通過 ts.createSourceFile 來生成 AST。除 JS 外,iOS 采用的是 CLang,Android 采用的是字節(jié)碼分析,C++ 采用的是符號表分析。
第二步:路徑提取,依賴尋路
從 AST 上我們可以找到所有的引用和暴露表達式,以 JS 為例就是 import/ require 和 export/ module.exports。尋找表達式的方法就是遞歸地遍歷所有語法節(jié)點,在 JS 中我采用了 TypeScript 編譯器提供的 ts.forEachChild 來進行遍歷,通過 ts.SyntaxKind 進行語法節(jié)點類型的識別。
找到表達式后,通過依賴路徑找到具體的依賴文件。以 JS 為例,我們可以通過 const { identifierName } = require('@bundleName/fileName') 的方式引用其它模塊(bundleName)的某個文件(fileName)的某些標識符(identifierName),我們就需要根據這表達式來定位到具體的標識符。
跨切面的依賴會需要多做一步,需要將切面 API 分為調用側和聲明側,在 JS 層通過 AST 分析出調用側數(shù)據,在 Native/C++ 層分析出聲明側數(shù)據(對應到具體實現(xiàn)切面 API 的標識符),將調用側和聲明側數(shù)據通過版本號關聯(lián)到一起,即可實現(xiàn)全依賴鏈路貫通。
我們把這個關系以及一些元數(shù)據保存下來,就可以作為源數(shù)據來作數(shù)據分析了。
四、項目架構
整體項目架構如下:
我們使用 Node.js 和集團的 egg.js 框架搭建了本依賴分析工程服務,并且考慮到數(shù)據使用場景的多變性和多樣性,我選用了 GraphQL 作為查詢接口,輸出我們定義的數(shù)據類型,由上層應用自行封裝,如果出現(xiàn)多個上層應用同時需要類似的數(shù)據,我們也會進行整合復用。
其中數(shù)據加工模塊是獨立模塊,由 Node.js 編寫,支持其它項目復用,未來會計劃在 IDE 等項目復用。
左側是我們的數(shù)據消費方,這里只列舉了幾個;右側是我們的數(shù)據庫,用于儲存分析結果;下側是四端掃描器和觸發(fā)器,四端分別對自己平臺的源碼進行源數(shù)據生產,觸發(fā)器支持發(fā)布流程觸發(fā)事件觸發(fā)、定時觸發(fā)、前端觸發(fā)(應用側前端,不是 Web 前端)和人工觸發(fā)等。
五、應用場景及實現(xiàn)原理
全鏈路依賴關系的使用場景有無窮的想象力,這里挑幾個來舉例。
影響范圍判斷(逆向依賴分析)
第一個我們能想到的應用場景就是影響范圍判斷,這也是我們這個項目的第一個抓手。大家都能想到,如果維護一個接口(或組件),我們會發(fā)現(xiàn)當越來越多地方用的時候,迭代它的風險會隨之而越來越高,我們需要明確地知道到底有哪些地方調用了這個接口,以確定到底要回歸測試多少功能、要怎么做發(fā)布、怎么做兼容等。而這就需要進行逆向依賴分析了。
逆向依賴是相對掃描器中分析出來的依賴關系的,掃描器分析出來的我們稱之為正向依賴,它主要表示「此模塊依賴了哪些別的模塊」;而逆向依賴則指的是「此模塊被哪些模塊依賴了」。所以很自然地,我們的逆向依賴就是基于正向依賴關系做的數(shù)據加工。
基于逆向依賴數(shù)據,結合多個版本的數(shù)據,我們還能算出「連續(xù)未被引用的版本數(shù)」,以衡量下線接口的安全性。
組件庫、框架和切面 API 的維護者是這個能力的重度用戶,這個能力為他們帶來了數(shù)據支撐,明確了自己的修改將會影響多少的其它模塊,從而進行變更、發(fā)布決策和回歸測試。
版本間變動分析
版本提測時,我們可以對兩個版本進行依賴鏈比對,分析出文件的變動及其整個影響鏈路,為 QA 提供一些數(shù)據支持,能更精確地知道有哪些功能要進行回歸測試,有哪些不需要。
版本間變動分析有很多場景,除了正常的版本迭代的場景之外,還有一個常見的場景:模塊在未變動的情況下被集成到新版本的高德 APP 中,那就會出現(xiàn)「發(fā)布代碼不變,而所依賴的其它模塊有變動」的情況,尤其有是 Native/C++ 和公用模塊。測試環(huán)境需要知道的是,當前模塊所依賴的其它模塊到底有哪些變動、這些變動對此模塊的影響是什么、需要回歸測試哪些功能點等。
這個數(shù)據的主要消費方是 QA 同學,他們利用這個數(shù)據可以提高測試效率,也能發(fā)現(xiàn)漏考慮的回歸點。
趨勢變化判斷
前面也提到過,由于高德 APP 時間跨度很大,以及之前未進行限制,所以我們有部分業(yè)務邏輯代碼仍然是通過 Native 來實現(xiàn)的,我們希望逐漸遷移到 JS 或 C++ 層實現(xiàn),Native 僅作適配。
而要判斷這個治理的進度和效果,需要從兩個方面的數(shù)據來支撐,一是各平臺代碼行數(shù),這個我們另有專門的服務做,暫且不提;二是接口趨勢。接口趨勢也分為調用側和聲明側兩種,按照我們治理的方向,我們期望的效果應該是:一條 Native 業(yè)務切面 API 的調用量按版本/時間不斷減少的曲線,當一些 API 的調用量為 0 后就可以把 API 下線掉,這樣就會隨之出現(xiàn)另一條曲線——Native 業(yè)務切面 API 的聲明量也不斷減少。
進行架構治理、切面 API 治理的同學是這些數(shù)據的主要消費方,有了這些數(shù)據他們就能確定架構治理的趨勢是否合理、是否能下線某切面 API 等。
包大小優(yōu)化——無用、重復文件查找
我們也為包大小優(yōu)化作了貢獻。根據依賴關系數(shù)據,我們可以找出一些沒有被引用或者內容完全一樣(md5 值相同)的文件,這些文件也占用了不少體積。
我們利用依賴分析工程找出了上千張這樣的圖片,@1x @2x @3x 文件是重災區(qū),有很多假裝自己是另一個清晰度的圖片被我們揪出來了(我們甚至因此推動了設計師出圖標準化和增加了檢驗工具)。
六、寫在最后
以上便是高德全鏈路依賴分析工程的基本概述,在具體的實現(xiàn)當中,會有無數(shù)的細節(jié)需要處理,如各種歷史遺留問題、多級版本處理產生指數(shù)級的代碼快照、變動分析產生指數(shù)級的分析結果等,其中也涉及到不少編譯原理、數(shù)據結構與算法(尤其是圖結構)等知識,非常考驗編程能力和權衡能力,以及最重要的——韌性。歡迎大家一起討論,一起迸發(fā)新的想法、新的場景!