高德APP全鏈路源碼依賴分析工程

NO IMAGE

一、背景

高德 App 經過多年的發展,其代碼量已達到數百萬行級別,支撐了高德地圖複雜的業務功能。但與此同時,隨著團隊的擴張和業務的複雜化,越來越碎片化的代碼以及代碼之間複雜的依賴關係帶來諸多維護性問題,較為突出的問題包括:

  • 不敢輕易修改或下線對外暴露的接口或組件,因為不知道有什麼地方對自己有依賴、會受到影響,於是代碼變得臃腫,包大小也變得越來越大;

  • 模塊在沒有變動的情況下,發佈到新版本的客戶端時,需要全量回歸測試整個功能,因為不知道所依賴的模塊是否有變動;

  • 難以判斷 Native 從業務實現轉變為底層支撐的趨勢是否合理,治理是否有效;

這些問題已經達到了我們必須開始治理的程度了,而解決此類問題的關鍵在於需要了解代碼間的依賴關係。

二、高德 APP 平臺架構

為了消除一些疑惑,在討論依賴分析的實現前,先簡單說明一下高德 APP 的平臺架構,以便對一些名詞和場景有一些背景瞭解。

高德APP全鏈路源碼依賴分析工程

高德 APP 從語言平臺上可以分為 4 個部分,JS 層主要負責業務邏輯和 UI 框架;中間有 C++層做高性能渲染(主要是地圖渲染),同時實現了一些切面 API,這樣可以在雙端只維護一套邏輯了;Android 和 iOS 層主要作為適配層,做一些操作系統接口的對接和雙端差異化的(儘可能)抹平。

這裡的切面是指 JS 層與 Native/C++ 層的分界線,這裡會實現一些切面 API,也就是 JS 層與 Native/C++ 層交互的一系列接口,如藍牙接口、系統信息接口等,由 Native/C++ 層來實現接口,然後往 JS 層暴露,由 JS 層調用。

三、基礎實現原理

整個項目最基本也是最重要的數據就是依賴關係。所謂依賴關係,最簡單的例子就是文件 A 依賴文件 B 的某個方法。

要將這個關係查出來,一般來說需要經過兩個步驟。

第一步:編譯源碼,獲得 AST

遍歷所有源碼,通過語法分析,生成抽象語法樹(Abstract Syntax Tree, AST)。以 JS 掃描器為例,我採用了 typeScript 模塊作為編譯器,它同時支持 JS(X)、TS(X),通過 ts.createSourceFile 來生成 AST。除 JS 外,iOS 採用的是 CLang,Android 採用的是字節碼分析,C++ 採用的是符號表分析。

第二步:路徑提取,依賴尋路

從 AST 上我們可以找到所有的引用和暴露表達式,以 JS 為例就是 import/ require 和 export/ module.exports。尋找表達式的方法就是遞歸地遍歷所有語法節點,在 JS 中我採用了 TypeScript 編譯器提供的 ts.forEachChild 來進行遍歷,通過 ts.SyntaxKind 進行語法節點類型的識別。

找到表達式後,通過依賴路徑找到具體的依賴文件。以 JS 為例,我們可以通過 const { identifierName } = require(‘@bundleName/fileName’) 的方式引用其它模塊(bundleName)的某個文件(fileName)的某些標識符(identifierName),我們就需要根據這表達式來定位到具體的標識符。

跨切面的依賴會需要多做一步,需要將切面 API 分為調用側和聲明側,在 JS 層通過 AST 分析出調用側數據,在 Native/C++ 層分析出聲明側數據(對應到具體實現切面 API 的標識符),將調用側和聲明側數據通過版本號關聯到一起,即可實現全依賴鏈路貫通。

我們把這個關係以及一些元數據保存下來,就可以作為源數據來作數據分析了。

四、項目架構

整體項目架構如下:

高德APP全鏈路源碼依賴分析工程

我們使用 Node.js 和集團的 egg.js 框架搭建了本依賴分析工程服務,並且考慮到數據使用場景的多變性和多樣性,我選用了 GraphQL 作為查詢接口,輸出我們定義的數據類型,由上層應用自行封裝,如果出現多個上層應用同時需要類似的數據,我們也會進行整合複用。

其中數據加工模塊是獨立模塊,由 Node.js 編寫,支持其它項目複用,未來會計劃在 IDE 等項目複用。

左側是我們的數據消費方,這裡只列舉了幾個;右側是我們的數據庫,用於儲存分析結果;下側是四端掃描器和觸發器,四端分別對自己平臺的源碼進行源數據生產,觸發器支持發佈流程觸發事件觸發、定時觸發、前端觸發(應用側前端,不是 Web 前端)和人工觸發等。

五、應用場景及實現原理

全鏈路依賴關係的使用場景有無窮的想象力,這裡挑幾個來舉例。

影響範圍判斷(逆向依賴分析)

第一個我們能想到的應用場景就是影響範圍判斷,這也是我們這個項目的第一個抓手。大家都能想到,如果維護一個接口(或組件),我們會發現當越來越多地方用的時候,迭代它的風險會隨之而越來越高,我們需要明確地知道到底有哪些地方調用了這個接口,以確定到底要回歸測試多少功能、要怎麼做發佈、怎麼做兼容等。而這就需要進行逆向依賴分析了。

逆向依賴是相對掃描器中分析出來的依賴關係的,掃描器分析出來的我們稱之為正向依賴,它主要表示「此模塊依賴了哪些別的模塊」;而逆向依賴則指的是「此模塊被哪些模塊依賴了」。所以很自然地,我們的逆向依賴就是基於正向依賴關係做的數據加工。

高德APP全鏈路源碼依賴分析工程

(逆向依賴查詢頁面)

基於逆向依賴數據,結合多個版本的數據,我們還能算出「連續未被引用的版本數」,以衡量下線接口的安全性。

高德APP全鏈路源碼依賴分析工程

(一些切面 API 的連續未被使用的版本數)

組件庫、框架和切面 API 的維護者是這個能力的重度用戶,這個能力為他們帶來了數據支撐,明確了自己的修改將會影響多少的其它模塊,從而進行變更、發佈決策和迴歸測試。

版本間變動分析

版本提測時,我們可以對兩個版本進行依賴鏈比對,分析出文件的變動及其整個影響鏈路,為 QA 提供一些數據支持,能更精確地知道有哪些功能要進行迴歸測試,有哪些不需要。

版本間變動分析有很多場景,除了正常的版本迭代的場景之外,還有一個常見的場景:模塊在未變動的情況下被集成到新版本的高德 APP 中,那就會出現「發佈代碼不變,而所依賴的其它模塊有變動」的情況,尤其有是 Native/C++ 和公用模塊。測試環境需要知道的是,當前模塊所依賴的其它模塊到底有哪些變動、這些變動對此模塊的影響是什麼、需要回歸測試哪些功能點等。

這個數據的主要消費方是 QA 同學,他們利用這個數據可以提高測試效率,也能發現漏考慮的迴歸點。

趨勢變化判斷

前面也提到過,由於高德 APP 時間跨度很大,以及之前未進行限制,所以我們有部分業務邏輯代碼仍然是通過 Native 來實現的,我們希望逐漸遷移到 JS 或 C++ 層實現,Native 僅作適配。

而要判斷這個治理的進度和效果,需要從兩個方面的數據來支撐,一是各平臺代碼行數,這個我們另有專門的服務做,暫且不提;二是接口趨勢。接口趨勢也分為調用側和聲明側兩種,按照我們治理的方向,我們期望的效果應該是:一條 Native 業務切面 API 的調用量按版本/時間不斷減少的曲線,當一些 API 的調用量為 0 後就可以把 API 下線掉,這樣就會隨之出現另一條曲線——Native 業務切面 API 的聲明量也不斷減少。

高德APP全鏈路源碼依賴分析工程

(從某版本開始就不斷減少調用的切面 API)

高德APP全鏈路源碼依賴分析工程

(某版本未被使用的切面 API)

進行架構治理、切面 API 治理的同學是這些數據的主要消費方,有了這些數據他們就能確定架構治理的趨勢是否合理、是否能下線某切面 API 等。

包大小優化——無用、重複文件查找

我們也為包大小優化作了貢獻。根據依賴關係數據,我們可以找出一些沒有被引用或者內容完全一樣(md5 值相同)的文件,這些文件也佔用了不少體積。

我們利用依賴分析工程找出了上千張這樣的圖片,@1x @2x @3x 文件是重災區,有很多假裝自己是另一個清晰度的圖片被我們揪出來了(我們甚至因此推動了設計師出圖標準化和增加了檢驗工具)。

六、寫在最後

以上便是高德全鏈路依賴分析工程的基本概述,在具體的實現當中,會有無數的細節需要處理,如各種歷史遺留問題、多級版本處理產生指數級的代碼快照、變動分析產生指數級的分析結果等,其中也涉及到不少編譯原理、數據結構與算法(尤其是圖結構)等知識,非常考驗編程能力和權衡能力,以及最重要的——韌性。歡迎大家一起討論,一起迸發新的想法、新的場景!

高德APP全鏈路源碼依賴分析工程

相關文章

SpringCloud(二):Eureka服務註冊中心

AndroidStudio:調試的技巧與心得|AndroidDevSummit中文字幕視頻

小白學Python爬蟲(13):urllib基礎使用(三)

詳解設計模式建造者模式