百度地圖加載海量標註性能優化策略

在上一篇博客中關於Vue表單驗證的話題裏,我提到了這段時間在做的城市配載功能,這個功能主要着眼於,如何爲客戶提供一條路線最優、時效最短、裝載率最高的路線。事實上,這是目前物流運輸行業智能化、專業化的一個趨勢,即面向特定行業的局部最優解問題,簡單來說,怎麼樣能在裝更多貨物的同時滿足運費更低的條件,同時要考慮超載等等不可抗性因素,所以,這實際上是一個數學問題。而作爲這個功能本身,在地圖上加載大量標註更是基礎中的基礎,所以,今天這篇博客想說說,通過百度地圖API加載海量標註時,關於性能優化方面的一點點經驗。

問題還原

根據IP定位至用戶所在城市後,後臺一次性查詢出近一個月內的訂單,然後將其全部在地圖上展示出來。當用戶點擊或者框選標註物時,對應的訂單配載到當前運單中。當用戶再次點擊標註物,則對應的訂單從當前運單中刪除。以西安市爲例,一次性加載850個左右的訂單,用戶操作一段時間後,Chrome內存佔用達250多兆,拖拽地圖的過程中可以明顯地感覺到頁面卡頓。因爲自始至終,地圖上的訂單數量不變,即不會移除覆蓋物,同時需要在內存中持久化訂單相關的信息。所以,在城市配載1.0版本的時候,測試同事給我提了一個性能方面的Bug。可開始提方案並堅持這樣做的,難道不是產品嗎?爲什麼要給開發提Bug呢?OK,我們來給不靠譜的產品一點點填坑吧,大概想到了下面三種方案,分別是標註物聚合Canvas API視野內可見

密密麻麻的地圖

標註物聚合方案

所謂“標註物聚合”,就是指在一定的地圖層級上,地圖上的覆蓋物主要是以聚合的形式顯示的,譬如顯示某一個省份裏共有多少個訂單,而不是把所有訂單都展示出來,除非地圖放大到一定的層級。這種其實在我們產品上是有應用的,比如運單可視化基本上是全國範圍內的車輛位置,這個時候在省一級縮放比例上使用聚合展示就非常有必要。可在城市配載這裏就相當尷尬啦,因爲據說客戶會把地圖放大到市區街道這種程度來對訂單進行配載,所以,這種標註物聚合方案的效果簡直是微乎其微,而且更尷尬的問題在於,官方的 MarkerClusterer 插件支持的是標準的覆蓋物,即Marker類。而我們的產品爲了好看、做更復雜的交互,設計了更復雜的標記物原型,這就迫使我們必須使用自定義覆蓋物,而自定義覆蓋物通常會用HTML+CSS來實現。

標註聚合器MarkerClusterer

所以,一個簡潔的Marker類和複雜的DOM結構,會在性能上存在巨大差異,這恰恰是我們加載了800多個點就產生性能問題的原因,因爲一個“好看”的標註物,居然由4個DOM節點組成,而這個“好看”的標註物還不知道要怎麼樣實現Marker類裏的右鍵菜單。所以,追求“好看”有問題嗎?沒有,可華而不實的“好看”,恰恰是性能降低的萬惡之源,更不用說,因爲覆蓋物不會從地圖上刪除,每次框選都要進行800多次的點的檢測了,而這些除了開發沒有人會在乎,總有人擺出一副**“這個需求很簡單,怎麼實現我不管”**的態度……雖然這種方案已經被Pass掉了,這裏我們還是通過一個簡單的示例,來演示下MarkerClusterer插件的簡單使用吧!以後對於前端類的代碼,博主會優先使用CodePen進行展示,因爲這樣子顯然比貼代碼要生動呀!

{% codepen qinyuanpei qBWJgGE dark [css,result 480px] %}

這裏稍微提帶說一下這個插件的優化,經博主測試,在標記物數目達到100000的時候,拖拽地圖的時候可以明顯的感覺的卡頓,這一點大家可以直接在CodePen中進行測試。產生性能問題的原因主要在以下代碼片段:

   /**
     * 向該聚合添加一個標記。
     * @param {Marker} marker 要添加的標記。
     * @return 無返回值。
     */
    Cluster.prototype.addMarker = function(marker){
        if(this.isMarkerInCluster(marker)){
            return false;
        }//也可用marker.isInCluster判斷,外面判斷OK,這裏基本不會命中
    
        if (!this._center){
            this._center = marker.getPosition();
            this.updateGridBounds();//
        } else {
            if(this._isAverageCenter){
                var l = this._markers.length + 1;
                var lat = (this._center.lat * (l - 1) + marker.getPosition().lat) / l;
                var lng = (this._center.lng * (l - 1) + marker.getPosition().lng) / l;
                this._center = new BMap.Point(lng, lat);
                this.updateGridBounds();
            }//計算新的Center
        }
    
        marker.isInCluster = true;
        this._markers.push(marker);
    
        var len = this._markers.length;
        if(len < this._minClusterSize ){     
            this._map.addOverlay(marker);
            //this.updateClusterMarker();
            return true;
        } else if (len === this._minClusterSize) {
            for (var i = 0; i < len; i++) {
                this._markers[i].getMap() && this._map.removeOverlay(this._markers[i]);
            }
			
        } 
        this._map.addOverlay(this._clusterMarker);
        this._isReal = true;
        this.updateClusterMarker();
        return true;
    };

這段代碼主要的問題在於頻繁地向地圖添加覆蓋物,換言之,在這裏產生了對DOM的頻繁修改,具體可參考_addToClosestCluster方法。一種比較好的優化是,等所有計算結束後再一次性應用到DOM。所以,這裏我們可以封裝一個render()方法:

Cluster.prototype.render = function(){
    var len = this._markers.length; 
    if (len < this._minClusterSize) {
            for (var i = 0; i < len; i++) {
                this._map.addOverlay(this._markers[i]);
            }
    } else {
            this._map.addOverlay(this._clusterMarker);
            this._isReal = true;
            this.updateClusterMarker();
    }
}

關於原理介紹及性能對比方面的內容,大家可以參考這篇文章:百度地圖點聚合MarkerClusterer性能優化

Canvas API方案

OK,接下來介紹第二種方案,其實從Canvas API你就可以想到我要說什麼了。Canvas API是HTML5中提供的圖形繪製接口,類似於我們曾經接觸過的GDI/GDI+、Direct2D、OpenGL等等。有沒有覺得和遊戲越來越近啦,哈哈!百度地圖API v3中提供了基於Canvas API的接口,我們可以把這些“好看”的覆蓋物繪製到一個層上面去,顯然這種方式會比DOM更高效,因爲博主親自做了實驗,一次性繪製10萬個點放到地圖上,真的是一點都不卡誒,要說缺點的話嘛,嗯,你想嘛,這都是不是DOM了,產品經理那些吊炸天的腦洞還怎麼搞?比如最基本的點擊,可能要用簡單的2D碰撞來處理啦,然後就是常規的座標系轉換,聽起來更像是在做遊戲了,對不對?誰讓那麼多的遊戲都是用HTML5開發的呢?同樣的,這裏給出一個簡單的示例:

{% codepen qinyuanpei aboRxYq dark [css,result 480px] %}

這個方案真正嘗試去做的時候,發現最難的地方是給Canvas裏的元素綁定事件,細心的朋友會發現,博主在這裏嘗試了兩種方案。**第一種,通過判斷點是否在矩形內來判斷是否完成了點擊,主要的問題是隨着點的數目的增加判斷的量級會越來越大。第二種,通過addHitRegion()增加一個可點擊區域,這種的性能明顯要比第一種好,唯一的限制在於瀏覽器的兼容性。**目前,需要在Chrome中開啓Experimental Web Platform features。這個探索的過程是相當不易的,大家可以通過CodePen進一步感受一下哈!

視野內可見方案

相信大家聽完前兩個方案都相當失望吧,一個方案用不了,一個方案太麻煩,那這個肯定就是最終可行的方案了吧!猜對了,這真的是體現了大道至簡,一開始試着從內存裏持久化的數據入手,可最終收到效果的反而是這個最不起眼的方案。簡單來說,就是把視野內的覆蓋物設爲visible,而把視野外的覆蓋物設置hidden。相當樸素的一種思維對吧,百度地圖API中有一個返回當前視野的接口GetBounds(),它回返回一個矩形。所以,我們只需要調用百度接口判斷覆蓋物在不在這個矩形裏就可以了,顯然,這裏又會循環800多次,不過產品經理們都不在乎對吧……順着這個思路,我們可以寫出下面的代碼,並在拖動地圖和縮放地圖的時候調用它:

//監聽地圖縮放/拖拽事件
map.addEventListener("moveend", showOverlaysByView);
map.addEventListener("zoomend", showOverlaysByView);

//根據視野來顯示或隱藏覆蓋物
function showOverlaysByView() {
    var bounds = map.getBounds();
    for (var i = 0; i < overlays.length; i++) {
        var overlay = overlays[i];
        var point = overlay._point;
        if (BMapLib.GeoUtils.isPointInRect(point, bounds) || BMapLib.GeoUtils.isPointOnRect(point, bounds)) {
            overlay.show();
        } else {
            overlay.hide();
        }
    }
 }

現在,我只能說,效果挺顯著,拖動地圖的時候不會卡頓了,因爲visible和hidden的切換會引發瀏覽器重繪,對於這一切我個人表示滿意。當然,這一切離好還很遙遠,因爲,人類的需要是永無止境的啊。

本文小結

就在我寫下這篇博客的時候,產品經理熱情洋溢地給我描述了城市配載2.0的設想。看了看同類產品的相關設計,我預感這個功能會變成一個以地圖爲核心的可視化運輸系統,這符合國內用戶一貫的“大而全”的使用習慣,地圖上的交互會更加複雜,需要展示的信息會越來越多,所以,這篇文章裏提到的優化,在未來到底有沒有用猶未可知。我只能告訴你這樣幾個原則:儘可能的使用Marker類;儘可能的簡化DOM結構;地圖層級變化越大越要考慮使用聚合;視野外的覆蓋物該隱藏就隱藏(反正看不到咯……)。一次性加載百萬級別數據要求,我從來不覺得合理,因爲就算我能加載出來,你能看的過來嗎?本身就是僞需求好吧(逃……好了,這就是這篇博客的全部內容啦,以上……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章