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