天天看點

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

一、背景

高德 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 檔案是重災區,有很多假裝自己是另一個清晰度的圖檔被我們揪出來了(我們甚至是以推動了設計師出圖示準化和增加了檢驗工具)。

六、寫在最後

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

繼續閱讀