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

amap_tech發表於2019-12-09

一、背景

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

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

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

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

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

二、高德 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 的識別符號),將呼叫側和宣告側資料透過版本號關聯到一起,即可實現全依賴鏈路貫通。

我們把這個關係以及一些後設資料儲存下來,就可以作為源資料來作資料分析了。

四、專案架構

整體專案架構如下:

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

其中資料加工模組是獨立模組,由 Node.js 編寫,支援其它專案複用,未來會計劃在 IDE 等專案複用。

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

五、應用場景及實現原理

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

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

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

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

(逆向依賴查詢頁面)

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

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

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

版本間變動分析

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

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

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

趨勢變化判斷

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

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

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

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

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

包大小最佳化——無用、重複檔案查詢

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

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

六、寫在最後

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

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69941357/viewspace-2667485/,如需轉載,請註明出處,否則將追究法律責任。

相關文章