- setCenterCoordinate和setRegion方法不能同时或短时间内同时执行
- 需求:大头针根据CLLocationManager代理的方位指示,并且会随着CLLocationManager的位置变化为移动,方位指示代码如下
self.userAnnotationView.transform = CGAffineTransformMakeRotation(self.rotateDegree);
异常现象:用户位置指针方向偶尔回到正北方向的闪烁问题
跳点原因:每次定位回调时,更改用户位置,系统会将大头针的状态回到最初的指向正北方,导致指针指向出现闪烁变化(概率事件)
解决方法:将当前旋转角度作为全局变量保存,每次定位回调的时候再次赋值
判断用户是否拖拽过地图。
地图因为自身手势屏蔽了系统的Touch事件,导致Touch事件无法完整监听。而手动添加手势也不会生效(通过设置代码方法也可以使其生效,但是麻烦),有个取巧,经过测试,发现每次的拖拽地图事件,都会触发touchesCancelled方法,而地图点击事件不会触发,这里判断最简单内存问题。
地图销毁时通常执行如下代码回收内存
- (void)clearMapView {
self.showsUserLocation = NO;
self.delegate = nil;
[self removeAnnotations:self.annotations];
[self removeOverlays:self.overlays];
[self removeFromSuperview];
}
但是地图在缩放,或者移动过程中内存还是会持续增大,无法有效控制。根据文档,发现切换地图的类型会删除地图缓存,牺牲GPU重绘换取内存优化。
self.mapType = MKMapTypeHybrid;
self.mapType = MKMapTypeStandard;
这段代码也有弊端,会使地图出现短暂的空白再绘制现象,影响体验。手机越好,影响越小,在iphonex上面影响已经非常小了。考虑到地图内存在缩放时增加最快,可以在代理方法regionDidChangeAnimated中判断是否是缩放事件,且地图缩放达到一定比例才执行上面的释放内存方法。
通过该方法,地图页面内存始终保持在相对低位
地图轨迹线出现的残影问题
地图上的轨迹线宽度随着地图的放大而缩小,有可能会在屏幕上留下尾部伪像,地图从很小变到很大时出现概率较大,手动缩放地图时变化比例不大,出现概率很低
当刚进入地图,而地图上绘制了轨迹线,如果使用setVisibleMapRect,setRegion等带有动画效果,则地图放大过程中轨迹线两端宽度可能来不及随地图放大而缩小,导致屏幕上留下轨迹线尾部伪像。
解决方案:
5.1. 取消缩放动画,这个对手动缩放地图导致的尾部伪像无能为力,但是因为手动出现概率很低,勉强使用
5.2. 子类MKPolylineRenderer,重写applyStrokePropertiesToContext方法让轨迹线宽度恒定,但这样会导致地图缩放时线的宽度太小而看不见了
5.3. 子类MKPolyline,重写boundingMapRect方法返回MKMapRectWorld,让轨迹线全量绘制,配合上面的效果可以地图上大头针的显示次序问题
地图上存在多个大头针,当地图缩放时希望某一个大头针始终在最前面
- (void)updateAnnotationzIndex{
for (PAXMapAnnotation *anno in self.annotations) {
MKAnnotationView *annotationView = [self viewForAnnotation:anno];
if (annotationView.top) {
[annotationView.superview bringSubviewToFront:annotationView];
}else{
[annotationView.superview sendSubviewToBack:annotationView];
}
}
}
在代理方法didAddAnnotationViews,和regionDidChangeAnimated(判断是缩放地图时),这两种情况下调用上面方法,效果算凑合,大多能实现所需效果,但是不完美,有时候还是会出现不希望的效果,有知道完美解决的请留言
- 实现用户位置在地图正中心,且能显示下地图上的所有大头针
CLLocationDegrees maxLatitude = -MAXFLOAT;
CLLocationDegrees maxLongitude = -MAXFLOAT;
CLLocationDegrees minLatitude = MAXFLOAT;
CLLocationDegrees minLongitude = MAXFLOAT;
BOOL hasValidData = NO;
for (PAXMapAnnotation *anno in self.annotations) {
if (anno.annotationType > PAXMapAnnotationTypeMyLocation && anno.annotationType != PAXMapAnnotationTypeDriveCurrentPoint) continue;
if (isSearch && anno.annotationType >= PAXMapAnnotationTypeMyLocation) continue;
hasValidData = YES;
CLLocationCoordinate2D coordinate = anno.coordinate;
maxLatitude = MAX(maxLatitude, coordinate.latitude);
maxLongitude = MAX(maxLongitude, coordinate.longitude);
minLatitude = MIN(minLatitude, coordinate.latitude);
minLongitude = MIN(minLongitude, coordinate.longitude);
}
if (!hasValidData) { // 当没有任何大头针时,防止计算出的圈没有边界
[self setMapLevel:coordinate level:PAXMapViewZoomLevelDefault];
return;
}
CLLocationDegrees offsetMaxLatitude = fabs(maxLatitude - coordinate.latitude);
CLLocationDegrees offsetMaxLongitude = fabs(maxLongitude - coordinate.longitude);
CLLocationDegrees offsetMinLatitude = fabs(coordinate.latitude - minLatitude);
CLLocationDegrees offsetMinLongitude = fabs(coordinate.longitude - minLongitude);
CLLocationDegrees deltaLatitude = 2 * MAX(offsetMaxLatitude, offsetMinLatitude) * 1.1;
CLLocationDegrees deltaLongitude = 2 * MAX(offsetMaxLongitude, offsetMinLongitude) * 1.2;
MKCoordinateRegion region = MKCoordinateRegionMake(coordinate, MKCoordinateSpanMake(deltaLatitude, deltaLongitude));
[UIView animateWithDuration:0.5 animations:^{
// 有发现听云bug,deltaLatitude是197,deltaLongitude是200从而导致MKCoordinateRegion无效闪退
// 检查上面算法没找到逻辑问题,采用下面的过滤避免闪退
if (CLLocationCoordinate2DIsValid(region.center) && deltaLatitude < 1 && deltaLongitude < 1){
[self setRegion:region animated:YES];
}else{
[self showAnnotations:self.annotations animated:YES];
}
}];
原理:求出最大最小经纬度所围成的一个矩形圈,然后计算这个圈四周距离用户位置的最大的两边,将这两边和用户位置的间距*2即得跨度。(上面代码忽略了边界处理,如没有大头针时,或者大头针经纬度异常时)
- 动画
地图的很多方法系统自带动画,例如setRegion,setCenterCoordinate,setVisibleMapRect,动画的时间没法控制,可以采用如下控制动画时间
[UIView animateWithDuration:0.5 animations:^{
[self setRegion:region animated:YES];
}];
- 定位点漂移现象
地图一般配合CLLocationManager定位,定位出现的漂移采用如下过滤
- (BOOL)filterLocation:(CLLocation *)location{
if (location.horizontalAccuracy >= 200) return YES;
if (location.horizontalAccuracy < 50) return NO;
NSTimeInterval offsetNow = [location.timestamp timeIntervalSinceNow];
if (offsetNow >= 20) return YES;
CLLocationDistance meter = [location distanceFromLocation:self.currentLocation];
NSTimeInterval offsetTime = [location.timestamp timeIntervalSinceDate:self.currentLocation.timestamp];
CGFloat speed = meter / offsetTime;
BOOL isAutoDriveing = [Globals sharedInstance].driverSegment == segment_drivering;
BOOL isManualDriveing = [Globals sharedInstance].manualSwitchOn;
BOOL isDriveing = isAutoDriveing || isManualDriveing;
if (isDriveing && speed > 41.6) return YES;
if (!isDriveing && (speed > 17 / 3.6 * 1.3)) return YES;
return NO;
}
采用定位精度,定位缓存时间,判断非开车状态速度上限等过滤。我这里能知道是否开车,所以能设置速度上限,过滤漂移严重的点。
- 大头针重复删除添加优化
我这里需求是,移动地图不断加载附近一公里的停车位在地图上显示出来。为了减少请求数量,判断地图移动达到一个阀值(如300m)才发起新的请求,两次请求会有很多重复的数据,简单的方法是,请求回来删除地图上的所有大头针,再添加新的。因为删除添加大头针是UI操作,在滑动地图时会有卡顿现象。
优化:判断两次请求数据差异,删除新的请求没有了的数据对应的大头针,同时添加新的请求增加了的数据对应的大头针,对两次请求共有的大头针不做处理。
datas = [datas sortedArrayUsingComparator:^NSComparisonResult(id<PAXBinarySearchProtocol> o1, id<PAXBinarySearchProtocol> o2) {
return o1.ID > o2.ID;
}];
NSMutableSet *containModels = [NSMutableSet set];
// 移除旧的大头针
// 只需要移除消失了的数据对应的大头针,同时添加未有得数据
// 例:原有大头针对应的模型为1,3,5,现接受到的数据为1,4,5,则移除3大头针,同时添加4大头针即可
for (PAXMapAnnotation *annotation in self.mapView.annotations) {
NSInteger findIndex = [self binarySearch:datas target:annotation.model];
if (findIndex == kBinarySearchNotFind) {
[self.mapView removeAnnotation:annotation];// 例: 移除3
}else{
[containModels addObject:datas[findIndex]];// 例:[1,5]
}
}
PAXMapAnnotation *annotation;
for (id model in datas) {// 添加新的大头针
if ([containModels containsObject:model]) continue; // 例:添加4
annotation = [PAXMapAnnotation annotationWithType:type coordinate:[model coordinate] model:model];
[self.mapView addAnnotation:annotation];
}
binarySearch就是一个简单的二分搜索方法
- (NSInteger)binarySearch:(NSArray<id<PAXBinarySearchProtocol>> *)array target:(id<PAXBinarySearchProtocol>)target{
if (!array.count) return kBinarySearchNotFind;
if (![[array lastObject] isKindOfClass:[target class]]) return kBinarySearchNotFind;
NSInteger low = 0;
NSInteger high = array.count - 1;
while (low <= high) {
NSInteger mid = low + ((high - low)/2);
id<PAXBinarySearchProtocol> num = array[mid];
if (num.ID == target.ID) {
return mid;
}else if (num.ID > target.ID){
high = mid - 1;
}else{
low = mid +1;
}
}
return kBinarySearchNotFind;
}
二分搜索的数组需要排序,因为我这里有两种不同的模型需要共用,定义了一个二分搜索的专门协议
@protocol PAXBinarySearchProtocol <NSObject>
@property (nonatomic, assign, readonly) NSInteger ID; // 用来排序,二分搜索用
@end
- 同一个请求依次多次请求,返回顺序问题
地图移动时会不断发送请求,有时候会出现后面发出的请求先回来,前面发出的请求后回来现象,导致页面显示异常。
举个栗子:顺序发出1,2,3请求,若响应的顺序是1,3,2,希望达到处理1,3响应,丢弃2响应的目的
static NSInteger requestIndex = 0;
static NSInteger responseIndex = 0;
NSInteger currentRequestIndex = ++requestIndex;
[HTTP POST:params result:^(NSError *error, id responseData) {
if (responseIndex > currentRequestIndex) return;
responseIndex = currentRequestIndex;
}
原理:block捕获局部变量为值捕获
若不是block而是代理,如采用高德搜索,当连续输入搜索, 不断发起搜索时
static NSString *k_pax_searchRequestIndex = @"k_pax_searchRequestIndex";
static NSInteger _requestIndex = 0;
static NSInteger _responseIndex = 0;
- (void)searchHeaderViewTextDidChangeAction:(NSString *)text{
AMapInputTipsSearchRequest *tipReq = [[AMapInputTipsSearchRequest alloc] init];
tipReq.keywords = text;
_requestIndex++;
objc_setAssociatedObject(tipReq, &k_pax_searchRequestIndex, @(_requestIndex), OBJC_ASSOCIATION_COPY);
[self.searchAPI AMapInputTipsSearch:tipReq];
}
- (void)onInputTipsSearchDone:(AMapInputTipsSearchRequest *)request response:(AMapInputTipsSearchResponse *)response{
NSInteger requestIndex = [objc_getAssociatedObject(request, &k_pax_searchRequestIndex) integerValue];
if (_responseIndex >= requestIndex) return;
_responseIndex = requestIndex;
}
- 地图轨迹线毛刺现象优化
采用道格拉斯-普克-抽稀算法,实现如下
+ (CLLocationCoordinate2D *)douglasAlgorithm:(CLLocationCoordinate2D *)coordinates count:(NSInteger *)count threshold:(CGFloat)threshold{
if (!coordinates || *count <= 2) return coordinates;
if (*count > 10000 && threshold < 10) return coordinates; // 避免耗时太久的优化
NSMutableArray *list = [self convertCoordinatesToLatLngModels:coordinates count:*count].mutableCopy;
NSMutableArray *points = @[list.firstObject,list.lastObject].mutableCopy;
free(coordinates);
for (NSInteger i = 0; i < points.count - 1; i++) {
NSInteger startIndex = [list indexOfObject:points[i]];
NSInteger endIndex = [list indexOfObject:points[i+1]];
if ((endIndex - startIndex) == 1) continue;
NSString *value = [self getMaxDistance:list startIndex:startIndex endIndex:endIndex threshold:threshold];
if ([value componentsSeparatedByString:@","][0].doubleValue >= threshold) {
NSInteger position = [[value componentsSeparatedByString:@","][1] integerValue];
[points insertObject:list[position] atIndex:i + 1];
i--; // 重新遍历当前节点
}else {
[list removeObjectsInRange:NSMakeRange(startIndex + 1, endIndex - startIndex - 1)];
}
}
// 必须改变C数组对应的长度
*count = points.count;
return [self convertLatLngModelsToCoordinates:points];
}
// 由startIndex和endIndex组成的线段,计算这条线段上面的所有点距离该线段的最大距离
// 返回:“最大距离,索引”,如 @"20,5",表明最大点距离直线为20,索引为5
+ (NSString *)getMaxDistance:(NSArray <PAXLatLngModel *>*)coordinateList startIndex:(NSInteger)startIndex endIndex:(NSInteger)endIndex threshold:(CGFloat)threshold{
CGFloat maxDistance = -1;
NSInteger position = -1;
CGFloat distance = [self getDistance:coordinateList[startIndex] lastEntity:coordinateList[endIndex]];
for(NSInteger i = startIndex; i < endIndex; i++){
CGFloat firstSide = [self getDistance:coordinateList[startIndex] lastEntity:coordinateList[i]];
if(firstSide < threshold) continue;
CGFloat lastSide = [self getDistance:coordinateList[endIndex] lastEntity:coordinateList[i]];
if(lastSide < threshold) continue;
// 海伦公式求距离
CGFloat p = (distance + firstSide + lastSide) / 2.0;
CGFloat dis = sqrt(p * (p - distance) * (p - firstSide) * (p - lastSide)) / distance * 2;
if(dis > maxDistance){
maxDistance = dis;
position = i;
}
}
return [NSString stringWithFormat:@"%f,%ld", maxDistance,(long)position];
}
// 两点间距离公式
+ (CGFloat)getDistance:(PAXLatLngModel*)firstEntity lastEntity:(PAXLatLngModel*)lastEntity{
CLLocation *firstLocation = [[CLLocation alloc] initWithLatitude:firstEntity.latitude longitude:firstEntity.longitude];
CLLocation *lastLocation = [[CLLocation alloc] initWithLatitude:lastEntity.latitude longitude:lastEntity.longitude];
return [firstLocation distanceFromLocation:lastLocation];
}
+ (NSArray *)convertCoordinatesToLatLngModels:(CLLocationCoordinate2D *)coordinates count:(NSInteger )count{
NSMutableArray *LatLngs = @[].mutableCopy;
for (int i = 0; i < count; i++) {
PAXLatLngModel *model = [PAXLatLngModel new];
model.latitude = coordinates[i].latitude;
model.longitude = coordinates[i].longitude;
[LatLngs addObject:model];
}
return LatLngs;
}
+ (CLLocationCoordinate2D *)convertLatLngModelsToCoordinates:(NSArray<PAXLatLngModel *>*)latLngModels{
NSInteger size = sizeof(CLLocationCoordinate2D);
CLLocationCoordinate2D *coordinates = malloc(latLngModels.count * size);
for (int i = 0; i < latLngModels.count; i++) {
PAXLatLngModel *model = latLngModels[i];
coordinates[i] = CLLocationCoordinate2DMake(model.latitude, model.longitude);
}
return coordinates;
}
本质为过滤点,因为改变了C数组的长度,所以相对应的改变C数组的长度count值。threshold为算法阀值,太大则会将细微转弯过滤,不行。太小则无法起到作用,需要测试调节注意,该算法为耗时操作,尤其是当count很大,而threshold很小时。测试,iphone6,深圳到北京的路途点,共17000多个点,threshold为2.0时,耗时约3.8s