1. 程式人生 > >Andorid百度地圖聚合優化(大量marker卡頓)

Andorid百度地圖聚合優化(大量marker卡頓)

百度地圖聚合方法使用:http://blog.csdn.net/aconghui/article/details/50958715;

百度地圖聚合原始碼(上): http://blog.csdn.net/javine/article/details/51195014

百度地圖聚合原始碼(下): http://blog.csdn.net/javine/article/details/51234279

百度地圖官方聚合demo,對於大量marker來說,使用起來非常卡,在網上也搜尋的不少資料,但是優化聚合卡的方法基本沒找到,這裡在研究了聚合原始碼之後,本人優化的思路,僅供參考(閱讀之前,請先瀏覽聚合原始碼);

以下是百度地圖優化的兩個點:

1.降低marker之間聚合的條件。

看下百度地圖聚合核心演算法(NonHierarchicalDistanceBasedAlgorithm):

 /**
     *  cluster演算法核心
     * @param zoom map的級別
     * @return
     */
    @Override
    public Set<? extends Cluster<T>> getClusters(double zoom) {
        final int discreteZoom = (int) zoom;
        final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom);
        final Set<QuadItem<T>> visitedCandidates = new HashSet<QuadItem<T>>();
        final Set<Cluster<T>> results = new HashSet<Cluster<T>>();
        final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<QuadItem<T>, Double>();
        final Map<QuadItem<T>, com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster<T>> itemToCluster =
                new HashMap<QuadItem<T>, com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster<T>>();

        synchronized (mQuadTree) {
            for (QuadItem<T> candidate : mItems) {
                if (visitedCandidates.contains(candidate)) {
                    // Candidate is already part of another cluster.
                    continue;
                }

                Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
                Collection<QuadItem<T>> clusterItems;
                // search 某邊界範圍內的clusterItems
                clusterItems = mQuadTree.search(searchBounds);
                if (clusterItems.size() == 1) {
                    // Only the current marker is in range. Just add the single item to the results.
                    results.add(candidate);
                    visitedCandidates.add(candidate);
                    distanceToCluster.put(candidate, 0d);
                    continue;
                }
                com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster<T> cluster =
                        new com.baidu.mapapi.clusterutil.clustering.algo
                                .StaticCluster<T>(candidate.mClusterItem.getPosition());
                results.add(cluster);

                for (QuadItem<T> clusterItem : clusterItems) {
                    Double existingDistance = distanceToCluster.get(clusterItem);
                    double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
                    if (existingDistance != null) {
                        // Item already belongs to another cluster. Check if it's closer to this cluster.
                        if (existingDistance < distance) {
                            continue;
                        }
                        // Move item to the closer cluster.
                        itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
                    }
                    distanceToCluster.put(clusterItem, distance);
                    cluster.add(clusterItem.mClusterItem);
                    itemToCluster.put(clusterItem, cluster);
                }
                visitedCandidates.addAll(clusterItems);
            }
        }
        return results;
    }
其中final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom),表明marker之間聚合的距離,如果zoomSpecificSpanyu越大越容易聚合,反之越不容易聚合,因此我將它修改為final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom-2)。這樣降低了聚合的條件,會使地圖上的marker減少,節省渲染時間。

2.減少marker渲染數量(DefaultClusterRenderer)

這是節省時間最大的地方,先看原始碼:

public void run() {
            if (clusters.equals(DefaultClusterRenderer.this.mClusters)) {
                mCallback.run();//判斷如果新的clusters等於上一次儲存的clusters,直接return出去
                return;
            }

            final MarkerModifier markerModifier = new MarkerModifier();//這個類處理顯示和動畫

            final float zoom = mMapZoom;//最新的zoom值
            final boolean zoomingIn = zoom > mZoom;//mZoom為上一次儲存的zoom值
            final float zoomDelta = zoom - mZoom;//zoom變化量級,超過一定量級就不執行動畫了

            final Set<MarkerWithPosition> markersToRemove = mMarkers;//需呀刪除的點。請思考什麼樣的點需要被刪除?
            final LatLngBounds visibleBounds = mMap.getMapStatus().bound;//地圖在手機螢幕上的可見範圍

            //1.新增點
            // 找出所有螢幕上的原來的cluster中心點,在增加點的時候有些動畫需要用到這些點
            List<Point> existingClustersOnScreen = null;
            if (DefaultClusterRenderer.this.mClusters != null && SHOULD_ANIMATE) {
                existingClustersOnScreen = new ArrayList<Point>();
                for (Cluster<T> c : DefaultClusterRenderer.this.mClusters) { //迭代上一次儲存的clusters
                    if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {//只有已經聚合了的cluster才可以新增點
                        Point point = mSphericalMercatorProjection.toPoint(c.getPosition());//position轉換成point
                        existingClustersOnScreen.add(point);//儲存螢幕上已經聚合的cluster
                    }
                }
            }

            // Create the new markers and animate them to their new positions.
            final Set<MarkerWithPosition> newMarkers = Collections.newSetFromMap( 
                    new ConcurrentHashMap<MarkerWithPosition, Boolean>());//儲存新的clusters中需要顯示的點,轉成MarkerWithPosition型別
            for (Cluster<T> c : clusters) {             //迭代新的clusters
                boolean onScreen = visibleBounds.contains(c.getPosition());//是否在螢幕內
                if (zoomingIn && onScreen && SHOULD_ANIMATE) { //地圖放大 + 此cluster在螢幕內 + 可以動畫(SDK版本>11)
                    Point point = mSphericalMercatorProjection.toPoint(c.getPosition());//position轉成point
                    Point closest = findClosestCluster(existingClustersOnScreen, point);//找出與這個cluster距離最近的原螢幕上的點
                    if (closest != null) {//存在,則實現動畫
                        LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);
                        markerModifier.add(true, new CreateMarkerTask(c, newMarkers, animateTo));
                    } else {//不存在,則直接新增不生成動畫
                        markerModifier.add(true, new CreateMarkerTask(c, newMarkers, null));
                    }
                } else {//直接新增點,不生成動畫
                    markerModifier.add(onScreen, new CreateMarkerTask(c, newMarkers, null));
                }
            }

            // 2.等待新增點的任務完成
            markerModifier.waitUntilFree();

            // 把newMarkers中的點從markersToRemove中移除,markersToRemove中的點都是需要從地圖上移除的
            markersToRemove.removeAll(newMarkers);

            //3.移除點
            // 找出現在螢幕上顯示的cluster中心點,在移除點時需要用到這些點來實現動畫
            List<Point> newClustersOnScreen = null;
            if (SHOULD_ANIMATE) {
                newClustersOnScreen = new ArrayList<Point>();
                for (Cluster<T> c : clusters) {
                    if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {
                        Point p = mSphericalMercatorProjection.toPoint(c.getPosition());
                        newClustersOnScreen.add(p);
                    }
                }
            }
            
            for (final MarkerWithPosition marker : markersToRemove) { //迭代所有需要移除的點
                boolean onScreen = visibleBounds.contains(marker.position);

                if (!zoomingIn && zoomDelta > -3 && onScreen && SHOULD_ANIMATE) { // 地圖縮小 + zoom改變不超過3
                    final Point point = mSphericalMercatorProjection.toPoint(marker.position);
                    final Point closest = findClosestCluster(newClustersOnScreen, point);//找出最近的cluster
                    if (closest != null) {
                        LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);//動畫移動的終點
                        markerModifier.animateThenRemove(marker, marker.position, animateTo);
                    } else {
                        markerModifier.remove(true, marker.marker);//無動畫
                    }
                } else {
                    markerModifier.remove(onScreen, marker.marker);//無動畫
                }
            }
            //等待移除點的任務完成
            markerModifier.waitUntilFree();

            mMarkers = newMarkers;//儲存新的點
            DefaultClusterRenderer.this.mClusters = clusters;
            mZoom = zoom;//儲存最新的zoom

            mCallback.run();//執行執行緒執行完成的回撥函式
        }
這是渲染marker的程式碼部分,我們看到在這部分程式碼中,說明渲染時是先新增點位,再刪除點位,主要耗時部分在於新增點位,百度的做法是將所有的merker全部新增一遍,這樣非常消耗時間,特別在大量merker的時候。我的思路是,只渲染螢幕能看到的marker:
   for (Cluster<T> c : clusters) {
                boolean onScreen = visibleBounds.contains(c.getPosition());
                if(onScreen){
                    if (zoomingIn && SHOULD_ANIMATE) {
                        Point point = mSphericalMercatorProjection.toPoint(c.getPosition());
                        Point closest = findClosestCluster(existingClustersOnScreen, point);
                        if (closest != null) {
                            LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);
                            markerModifier.add(true, new CreateMarkerTask(c, newMarkers, animateTo));
                        } else {
                            markerModifier.add(true, new CreateMarkerTask(c, newMarkers, null));
                        }
                    } else {
                        markerModifier.add(onScreen, new CreateMarkerTask(c, newMarkers, null));
                    }
                }

            }
只有在視線範圍內的merker才去建立CreateMarkerTask渲染,不在視線範圍內的不建立CreateMarkerTask,好了效能方面優化接結束了,不敢說能承載多少marker量,我想1W以內應該還能接受吧。

補充:

經過上述2優化後,會出現一個問題,就是移動地圖時不會重新渲染marker,只有縮放地圖時才重新渲染merker.

解決方案:

找到ClusterManager的onMapStatusChange方法,註釋一下程式碼:

if (mPreviousCameraPosition != null && mPreviousCameraPosition.zoom == position.zoom) {
            return;
        }
這段程式碼的意思是地圖zoom不發生變化時,將不呼叫後面的方法。

找到2中的原始碼,在優化後的這個迴圈之前新增:

if(DefaultClusterRenderer.this.mClustersOnScreen!=null&&DefaultClusterRenderer.this.mClustersOnScreen.equals(existingClustersOnScreen)){
                mCallback.run();
                return;
            }
            DefaultClusterRenderer.this.mClustersOnScreen = existingClustersOnScreen;
  並在DefaultClusterRenderer中新增新屬性List<Point> mClustersOnScreen,用於儲存在地圖視線內的marker位置,如果視線內的marker沒有發生變化,將不再重新渲染。

註釋程式碼:

if (clusters.equals(DefaultClusterRenderer.this.mClusters)) {
                mCallback.run();
                return;
            }
 這段程式碼的意思是,如果核心演算法計算後的marker沒有發生變化,那麼就不再執行後面的渲染程式碼,因為移動地圖時聚合marker並沒有發生變化,因此移動地圖時永遠不會出現重繪marker。