一、背景
地圖空間可視化作為高德智慧交通前端業務中最重要的功能之一,承擔著城市交通大腦、全境智能大屏等業務中大量的地圖渲染需求,作為向用戶展示交通資料的視窗,我們需要展現省、市、區、商圈、自定義區域多種場景,包括所有交通事件、擁堵指數、轄區等多種維度的資料,呈現著資料量大、元素種類多、邏輯展現重等特點,
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)引入篩選機制,針對佇列中存在同一圖層的互逆操作,如短時間內加載一份資料,之后又remove掉,由于這一對操作不會對當前的結果有任何影響,因此這一對操作將會被過濾邏輯洗掉,達到優化渲染性能的效果,
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后的專案開發代碼:
<template>
<!-- 地圖底座 -->
<CommonMap
:center="center"
:zoom="zoom"
:city="fitViewCity"
view-mode="3D"
:map-style="mapStyle"
>
<!-- 場景1 -->
<TrafficScene>
<!-- 地圖元素層 -->
<TrafficPointLayer :list="trafficPointList" />
<!-- 帶有事件監聽的地圖元素層 -->
<TrafficRoadMarkerLayer
:list="trafficRoadList"
@click="handleTrafficRoadMarkerClick"
/>
</TrafficScene>
<!-- 場景2 -->
<PublishScene>
<PublishMarkerLayer
:list="publishList"
@mouseover="handlePublishMouseOver"
@mouseout="handlePublishMouseOut"
@click="handlePublishClick"
/>
</PublishScene>
<!-- 可視化場景 -->
<VisualizationScene use="amap-loca">
<!-- 可視化資料層 -->
<TrafficRoadLineLayer :list="trafficRoadList" />
</VisualizationScene>
</CommonMap>
</template>
五、展望
經過高德智慧交通大量專案的實踐,SDK的建設已經趨于成熟,其開發簡單、穩定性高、性能好的特點可以很好地降低開發者使用高德開放平臺JSAPI來開發地圖空間可視化專案的成本,我們未來會以開發者官網的形式對外輸出,更好地服務于開發者,
招聘
阿里巴巴高德地圖技術中心長期招聘Java、Golang、Python、Android、iOS 前端資深工程師和技術專家,職位地點:北京,歡迎投遞簡歷到gdtech@alibaba-inc.com,郵件主題為:姓名-應聘團隊-應聘方向,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/255890.html
標籤:其他
