首頁>科技>

一、背景

地圖空間視覺化作為高德智慧交通前端業務中最重要的功能之一,承擔著城市交通大腦、全境智慧大屏等業務中大量的地圖渲染需求。作為向用戶展示交通資料的視窗,我們需要展現省、市、區、商圈、自定義區域多種場景,包括所有交通事件、擁堵指數、轄區等多種維度的資料,呈現著資料量大、元素種類多、邏輯展現重等特點。

JSAPI作為高德地圖前端戰線的引擎,涵蓋著渲染地圖、展示覆蓋物等底層能力,但對於行業應用領域的開發來說,存在著開發難度大、適配成本高、純原生JS實現與主流框架結合不緊密,無行業圖層能力的問題。

基於以上原因,我們設計了具有適用於垂直行業的、可複用、可擴充套件、二次開發簡單等特點的地圖SDK,已經成為智慧交通地圖空間視覺化能力的首選方案。

二、方案設計

整體框架設計方案

高德智慧交通團隊經過大量專案實踐和思考,以交通行業為切入點,面向整個前端行業地圖設計了一套地圖空間視覺化開發的SDK,整體功能架構設計如下圖所示:

(1) MapContainer是整個SDK的基座,用於承載地圖引擎,裝載在其上渲染的覆蓋物圖層,載入所需要的框架模組,在整個架構中起到中流砥柱的作用。

(2) 配置控制器負責傳入使用者配置,包括地圖應用key配置、載入可選功能配置、樣式配置等,在使用者變更這些配置後,它會把更新後的配置資訊傳遞到流程中的其他模組中。

(3) 接受資料的工作由SourceLoader完成,設計了一套SDK內部使用的標準化的資料格式,Loader負責將使用者傳入的不同型別的資料(已經支援GeoJSON、WKT、資料列表等形式)轉化成專用標準格式資料,分發到地圖容器及各圖層中。

(4) 為了支援不同的主流應用框架,將框架適配層單獨拆分,由它將主要模組封裝成Vue、React等框架相容的元件形式,實現多框架擴充套件。

(5) 地圖API呼叫有著嚴格的順序限制,而封裝框架對於圖層各個生命週期的觸發是非同步的,亂序的,存在無法保證流程一致性的問題,為了應對這種情況我們在SDK中引入了事件佇列機制。

狀態驅動方案的實現

1.生命週期設計

地圖JS API的呼叫邏輯與原生Javascript一樣,是命令式呼叫設計,像下面這樣:

// 建立地圖const map = new AMap.Map(options);// 新增覆蓋物const marker = new AMap.Marker(markerOptions);map.add(marker);// 修改覆蓋物屬性marker.setContext(newContext);// 移除覆蓋物map.remove(marker);map.destroy();

這樣的API呼叫方式,與上層專案的開發框架,如Vue、React等不匹配,如果在一個狀態驅動的框架下充斥著大量命令式驅動的程式碼,會大幅度降低這個專案的可維護性、可擴充套件性。

為了更好地支撐開發的需要,所有業務圖層抽象出了一套完整的生命週期流程。不同的圖層,渲染邏輯的步驟不完全相同,各圖層的額外能力,如支援互動事件的能力、動畫能力也不盡相同,但都可以囊括在這一套生命週期結構內。

SDK圖層元件生命週期定義如下:

(1)地圖註冊

在地圖底座載入完畢後,會通知各個圖層的RegisterMap流程,這是圖層元件生命週期的第一步,圖層中包含的所有元素都在這之後才會開始渲染。

(2)中間層載入

部分型別的元素需要分組批次載入,因此在渲染這些元素之前,需要先將對應的組圖層加載出來。因此,我們設計了組圖層相關的生命週期,相關邏輯只需要在beforeAppendGroup,appendGroup,afterAppendGroup這些流程中實現即可。

(3)元素載入

beforeAppendComponent,appendComponent,afterAppendComponent,這些是元素圖層中最重要的流程,用於實現圖層元素載入的主邏輯。

其中,對於一些元素需要有前置檢查,有資料校驗,可以把相應的檢查邏輯放入beforeAppend中;有的元素需要註冊互動事件,或者需要有新增動畫scheme能力,這部分的實現邏輯可以放到afterAppend流程中。

與之對應的,還有元素的銷燬流程,beforeRemoveComponent,removeComponent,afterRemoveComponent。如果元素綁定了互動事件,將會在beforeRemove的時候解綁;如果元素註冊了動畫或者週期呼叫,也會在beforeRemove的時候銷燬週期timer。

(4)元素更新

shoudlUpdate,diff,updateComponent,用於實現元件資料動態更新後圖層元素的diff、更新過程。

其中為了防止源資料中只有一小部分修改導致整個圖層全部重繪的情況,我們在其中加入了diff的演算法,透過各圖層的校驗資料key的方法,篩選出變更前後一致的資料項,只重繪不同的資料,大大提升了渲染流程的效率。

2.外掛的實現

不同圖層之間存在共同處理流程和共同的屬性,對此,我們設計了各種可複用的內建外掛,供各圖層根據自身特性組合使用。

例如,有實現定時重新整理效果的scheme外掛,實現動畫效果的animate外掛,實現註冊互動事件的event外掛等。這些外掛的設計,必須遵守元件生命週期的規範,外掛功能的實現邏輯,也全部以註冊上述的生命週期函式的方式完成。

這些生命週期需要與主流框架的生命週期設計適配。以目前我們專案中正在使用的Vue框架舉例,Vue也有其自己的元件生命週期,它的設計基本能夠與我們的週期函式相匹配。因此,針對Vue的適配過程其實並不怎麼難:

Vue自身有一套不同層級的元件之間的載入控制流程,父子層級、兄弟層級之間的元件有著嚴格的觸發順序。例如,父元件的beforeCreate總是在子元件beforeCreate之前觸發,而父元件的mounted又總是在子元件mounted之後才會響應,這與我們的多層級圖層之間想要的觸發順序相符。因此,SDK圖層的各生命週期總能在Vue中找到與之對應的觸發時間點。

經過封裝後,用SDK實現的地圖模組在專案中生成的元件樹結構如下:

3.非同步流程的一致性設計

我們使用的底層地圖引擎,對於流程邏輯的順序有著嚴格的要求:

(1)地圖底座的建立須在所有其他流程之前。

(2)Loca、L7底座的初始化須在地圖底座建立完成之後。

(3)地圖元素需要在地圖底座載入完成後才能夠開始載入,銷燬也需要在地圖底座銷燬之前完成。

(4)需要確保狀態與結果的一致性,如果在短時間內觸發了大量的更新資料的操作,即使底層引擎處理需要很長的時間,也要保證最終的展示結果與更新的順序完全一致。

不幸的是,雖然主流的框架有完善的生命週期管理機制,能夠確保各個流程的執行順序不出差錯,但這些流程之間都是非同步的、併發的,而繪圖引擎在處理這些渲染指令時,會由於處理時長的不確定性,導致各指令返回的順序有所變化,這可能會導致下面的情況出現:

地圖容器的載入時間過長,導致載入後續元素時,地圖仍沒有渲染完成而出錯;在短時間內對同一份資料進行變更,如果引擎處理第一次變更花的時間比後一次更長,就會導致第一次更新的結果渲染出來時,會把更早完成的第二次渲染結果覆蓋掉。

為了避免上述情況,我們在SDK中實現了事件佇列控制器,處理順序問題:

(1)所有圖層元件中需要呼叫底座引擎的事件,例如append component,remove component等,不會直接呼叫底座的相關介面,而是在佇列控制器中push一個對應型別的事件。

(2)佇列控制器中的所有事件型別,全部封裝成同步方法實現。由控制器收集所有塗層的呼叫訊息,單執行緒逐一消費。

(3)在控制器中寫入特殊的控制邏輯,地圖基座的載入需要在其他圖層載入之前,則把基座載入的事件的響應優先順序設定為最高。

4.地圖控制指令的最佳化

地圖底座支援使用者透過呼叫相關方法控制地圖展示的視野,SDK在這種設計上加以最佳化,透過在地圖底座元件上配置相應的屬性狀態,來實現定位到選定元素、定位到整個轄區範圍、定位到特定地點及縮放級別等多種視野型別。

同時,地圖的其他控制方法,例如設定周邊避讓區域、設定游標形狀、設定自定義地圖樣式等方法,也全部改為傳遞props屬性的方式實現。

三、其他最佳化

地圖例項快取

就我們使用的底層地圖引擎來說,建立、銷燬一個地圖底座需要消耗大量的效能,而有時候這樣的操作是可以避免的。有時候我們只是切換了一個頁面路由,圖面的上展示物並不需要有什麼變化,但仍然會觸發地圖底座的銷燬與重新生成。這個流程是多餘的。

為了最佳化這個問題,我們設計了可以容納2個底座例項的快取容器。每次在執行銷燬地圖的命令時,我們並不會真正的銷燬它,而是把它隱藏掉並存入快取中。下次需要建立例項時,直接在快取中找到符合要求的例項拿來用。

多例項環境隔離

隨著下游業務專案的功能迭代,產品提出了在同一個頁面內展示多個SDK底座例項的要求。對此,我們對SDK進行了一系列的最佳化:

改造訊息佇列控制器,原來的單執行緒模式已經不再適用,現在已可以支援例項隔離,不同例項之間獨享事件佇列和流程控制邏輯。最佳化圖層與底座的從屬判定機制,在多個底座之間存在父子關係的情況下,能夠讓圖層在最合適的底座上展現。

GL渲染Context沒有正確GC回收導致的崩潰問題

在為L7編寫載入器時,遇到了記憶體洩露的問題:如果在專案中使用了L7相關圖層,銷燬時L7使用的WebGLRenderingContext資源不會正確釋放,反覆建立銷燬幾次後,瀏覽器會因為內部的renderingContext資源不足而渲染崩潰。

分析L7原始碼後發現,L7為了實現與地圖同步resize,在地圖容器DOM上註冊了一個resize事件,並把這個事件的處理函式繫結在了這個容器DOM的一個叫__resize__trigger__的屬性上。

如果開發者在專案中使用Vue作為前端框架,Vue的模板更新機制會引起DOM的重繪,在一次資料變更之後,它會把原來的容器DOM銷燬,替換為一個新的。

但由於註冊的事件函式中含有DOM物件引用的緣故,雖然舊的DOM物件已經從DOM tree上移除,但並不會被GC回收,而是仍然被__resize__trigger__這個函式引用著,同時由於新生成的DOM不具有該屬性,導致在L7引擎銷燬的時候,由於L7找不到這個函式,resize事件解綁也會失敗。在開發者觸發多次切換引擎操作之後,有大量的未被實際引用的容器DOM無法被回收,而這些DOM中又都包含著webGL Canvas物件,導致瀏覽器的GLRendering資源不足的問題出現。

解決方法:我們無法修改L7的原始碼,因此也無法更改它註冊、解綁事件的邏輯。但我們可以透過在每次Vue重新整理之前,對即將被移除的canvas的width和height設定為0,以此來直接釋放renderingContext資源,實測有效。

最佳解決方案:目前我們已經有自行實現的3D圖形類,且也擴充套件了對Loca等其他視覺化庫的支援,可以擺脫對單一庫的依賴,實現相同的能力。

四、多維資料比對

經過高德智慧交通大量的專案實踐和資料比對,充分證明了地圖空間視覺化SDK開發的必要性,業務價值和技術價值都經歷了專案的考驗。以高德交通大腦和全境智慧大屏的資料比對可以得到使用SDK之前和之後的資料比較:

專案落地效果:

使用SDK後的專案開發程式碼:

經過高德智慧交通大量專案的實踐,SDK的建設已經趨於成熟,其開發簡單、穩定性高、效能好的特點可以很好地降低開發者使用高德開放平臺JSAPI來開發地圖空間視覺化專案的成本。我們未來會以開發者官網的形式對外輸出,更好地服務於開發者。

6
最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • 彎道超車擱淺?5nm晶片告一段落,龍芯:14nm晶片是王道