起因
百度地图SDK版本:7.5.4
(接手项目时用的是7.4.2,在尽量保证项目正常运作勉强升级到了7.5.4,最新的版本完全不兼容现有逻辑了,等后续慢慢迭代吧)
因为业务需要,公司的所用到的百度地图需要绘制大量的mark图例,客户那边又不接受海量点。
原有的方案是给定后端范围(传入当前经纬度和半径值),后端计算后返回对应范围的图例数据列表,App拿到图例数据列表后进行绘制。
但在过程中出现了以下问题,如图:
每次拖拽地图都会去请求接口拿到数据并由App进行绘制。
那先不说考虑服务器的压力,就用户而言,这拖一下闪一下的体验是在太差,而且貌似频繁的绘制清除貌似会造成OOM?【用户一直在崩溃,我们这边各种压力测试却都无法复现】。
伪代码:
private MapView mMapView;
private BaiduMap mBaiduMap;
//地图状态改变监听
mBaiduMap.setOnMapStatusChangeListener(new OnMapStatusChangeListener() {
//... 省略不重要方法
@Override
public void onMapStatusChangeFinish(MapStatus status) {
addMark();
}
}
// 添加点位
private void addMark() {
mBaiduMap.clear();
if (binding.showMark.isChecked()) {
addMark1();
addMark2();
}
}
//点位集1
private void addMark1() {
executor.execute(() -> {
List<MarkerPoint> data = getMarker1Point();
for (MarkerPoint point : data) {
BitmapDescriptor descriptor = BitmapDescriptorFactory.fromResource(Markers.getIcon(point.icon));
//...
MarkerOptions overlay = new MarkerOptions()
.position(latLng) //当前经纬度
.extraInfo(bundle) //附带信息
.icon(descriptor); //图标
mBaiduMap.addOverlay(overlay);
}
}
}
//点位集2
private void addMark2() {
executor.execute(() -> {
List<MarkerPoint> data = getMarker2Point();
for (MarkerPoint point : data) {
BitmapDescriptor descriptor = BitmapDescriptorFactory.fromResource(Markers.getIcon(point.icon));
//...
MarkerOptions overlay = new MarkerOptions()
.position(latLng) //当前经纬度
.extraInfo(bundle) //附带信息
.icon(descriptor); //图标
mBaiduMap.addOverlay(overlay);
}
}
}
经过排查发现,百度地图再进行 BaiduMap.addOverlay 添加图层时有一个等待队列,貌似是通过 BaiduMap.clear() 进行区分的(因为混淆了就没去看内部,结合效果单纯的猜测),也就是说当用户拖拽地图完成后,请求接口传入当前经纬度和范围半径,返回mark点位集。
这时,假如接口下放了1000个点位,开始循环调用BaiduMap.addOverlay 进行添加进绘制队列,而当 addOverlay 内部并未添加绘制完(假如在400个左右时),用户再一次拖拽了地图,更新了当前经纬度又去请求了接口拿到了mark点位集,而按照我们之前的逻辑,拿到新的点位集就应该清理掉上一次的 Overlay,重新添加绘制本次的点位集,所以这里调用了BaiduMap.clear()。
可令人意外的是,它并未立即清理,仍在继续绘制前一次的图例;在漫长的等待后,才被清理掉,进行了本次点位的添加,如下图:
问题大概率出现在这儿了,于是就想着能不能从滑动方面入手,优化一下加载。
再于是,本着记录的方式写下了这一篇文章。
简单的公式分析
按照原来的逻辑,每次拖拽地图完成后 onMapStatusChangeFinish 都会立马走 addMark,然后拿到mark点位集并绘制。
private MapView mMapView;
private BaiduMap mBaiduMap;
//地图状态改变监听
mBaiduMap.setOnMapStatusChangeListener(new OnMapStatusChangeListener() {
//... 省略不重要方法
@Override
public void onMapStatusChangeFinish(MapStatus status) {
addMark();
}
}
// 添加点位
private void addMark() {
mBaiduMap.clear();
if (binding.showMark.isChecked()) {
addMark1();
addMark2();
}
}
但是通过上图明显能够看到,短距离的拖动,请求的mark位点数据是完全一致的。
那,该怎么优化呢?
想了想可以这么来:
1、当进入地图后,Mark点位在绘制时,将当前的坐标经纬度先存起来。
2、下次拖动的时候,将拖拽完成后的经纬度坐标与存起来的经纬度做比较,如果大于一定的范围,再mBaiduMap.clear(),并去接口拉取数据。
这样之后,是否就避免了频繁的入队出队,也减少了接口请求次数服务器的压力了呢?
说干就干,改一下:
private MapView mMapView;
private BaiduMap mBaiduMap;
private LatLng lastLatlng; //上次绘制时的经纬度
private LatLng movedLatlng; //每次移动后的经纬度
// 添加点位
private void addMark() {
if (lastLatlng == null) //判断是否首次移动,避免空指针情况
lastLatlng = movedLatlng;
//上次坐标与本次坐标的距离
double distance = DistanceUtil.getDistance(lastLatlng, movedLatlng);
//上次绘制是否超过了50米
if (distance != 0 && distance < 50)
return;
mBaiduMap.clear();
lastLatlng = movedLatlng; //存起来
if (binding.showMark.isChecked()) {
addMark1();
addMark2();
}
}
这么一改,是否就能正常了呢?跑一下看看。
诶,简单!可以收……继续优化了。
进一步优化算式
我们知道,地图是有缩放等级的。也就是左下角的范围。
可以通过以下方式拿到:
int level = mMapView.getMapLevel()
如果按照上面的写法直接收工,在不改变地图层级的情况下效果倒是很好;
但这个是厘米比呀,一旦比例被修改(200米,400米)呢,轻轻拖拽就奔出去好几百米了。
那就再改改吧(猛男叹气)。
private MapView mMapView;
private BaiduMap mBaiduMap;
private LatLng lastLatlng; //上次绘制时的经纬度
private LatLng movedLatlng; //每次移动后的经纬度
// 添加点位
private void addMark() {
if (lastLatlng == null) //判断是否首次移动,避免空指针情况
lastLatlng = movedLatlng;
//上次坐标与本次坐标的距离
double distance = DistanceUtil.getDistance(lastLatlng, movedLatlng);
//通过地图层级比例计算比例 (向上取整,至少保证比例值为1)
int levelRatio = (int) Math.ceil(mMapView.getMapLevel() / 50);
//上次绘制是否超过了50米的整数比
if (distance != 0 && distance < (50 * levelRatio))
return;
mBaiduMap.clear();
lastLatlng = movedLatlng; //存起来
if (binding.showMark.isChecked()) {
addMark1();
addMark2();
}
}
这样应该可以了吧?跑跑试试。
诶!可以了,比例也有了,丢给测试,收工!!
再优化亿次
正当我打算摸会儿,测试丢了一张图给我。
卧槽,这个忘记考虑了,好家伙,真给我找事做啊。
那就再亿改吧(人都麻了):
private void addMark() {
if (lastLatlng == null)
lastLatlng = movedLatlng;
//上次坐标与本次坐标的距离
double distance = DistanceUtil.getDistance(lastLatlng, movedLatlng);
//范围的一半(半径的一半)
double rangeHalf = KTextUtils.toDouble(ViewUtil.getTag(binding.filterRange)) / 2; //tag存的是范围
//通过地图层级比例计算比例
int levelRatio = (int) Math.ceil(mMapView.getMapLevel() / rangeHalf);
//上次绘制是否超过了比例值
if (distance != 0 && distance < (rangeHalf * levelRatio))
return;
mBaiduMap.clear();
lastLatlng = movedLatlng;
if (binding.showMark.isChecked()) {
addMark1(); //请求点位数据1并绘制
addMark2(); //请求点位数据2并绘制
}
}
ok,这亿次应该可以了吧,看看效果。
嗯,美滋滋,这下没话说了吧。
最后
这个计算公式就目前的业务而言应该算是极致优化了,在写本文时一直在想要不要省略部分调试和分析的步骤毕竟图有点多,而且有点大。
但仔细了想想,既然是记录,就尽量详细一些吧,不然几个月后,除了只知道这篇文是自己写的,恐怕就没有其他了(滑稽.jpg)
前人不愿意动的屎山终于被我挖了一坨下来(说多了都是泪啊)。