一次对MKMapView的性能优化

移动开发 iOS
当人多的时候(例如上图所示) 地图滑动起来就能感觉到明显顿卡 那种不流畅感能折磨死人 所以 自然我们要解决这个问题(等等 先不要吐槽为什么不用地图聚合 因为这已经是地图放到最大了 聚合不适合这次的问题讨论)

最近做的项目主要是LBS这块 主打成员定位功能 我们的UI设计是这样的

 

乍一看上去是挺好挺美观的 不同的人会显示不同的头像 可是当人扎堆的时候 问题就来了

 

当人多的时候(例如上图所示) 地图滑动起来就能感觉到明显顿卡 那种不流畅感能折磨死人 所以 自然我们要解决这个问题(等等 先不要吐槽为什么不用地图聚合 因为这已经是地图放到***了 聚合不适合这次的问题讨论)

分析

首先看下我是怎么实现这个annotationView的 由于这个annotationsView是异形的(也就是无法通过设置圆角直接得到) 而且里面的图片还因用户而异 所以解决方案就是使用layer.mask来进行遮罩 代码如下

  1. @implementation MMAnnotationView 
  2. - (instancetype)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier 
  3. self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier]; 
  4. if ( self ) 
  5. self.frame = CGRectMake(00, TRACK_ANNOTATION_SIZE.width, TRACK_ANNOTATION_SIZE.height); 
  6. self.centerOffset = CGPointMake(0, -(TRACK_ANNOTATION_SIZE.height-3)/2); 
  7. self.canShowCallout = NO; 
  8. self.avatarView = [[UIImageView alloc] initWithFrame:self.bounds]; 
  9. [self addSubview:self.avatarView]; 
  10. self.avatarView.contentMode = UIViewContentModeScaleAspectFill; 
  11. CAShapeLayer *shapelayer = [CAShapeLayer layer]; 
  12. shapelayer.frame = self.bounds; 
  13. shapelayer.path = self.framePath.CGPath; 
  14. self.avatarView.layer.mask = shapelayer; 
  15. self.layer.shadowPath = self.framePath.CGPath; 
  16. self.layer.shadowRadius = 1.0f; 
  17. self.layer.shadowColor = [UIColor colorWithHex:0x666666FF].CGColor; 
  18. self.layer.shadowOpacity = 1.0f; 
  19. self.layer.shadowOffset = CGSizeMake(00); 
  20. self.layer.masksToBounds = NO; 
  21. return self; 
  22. //mask路径 
  23. - (UIBezierPath *)framePath 
  24. if ( !_framePath ) 
  25. CGFloat arrowWidth = 14
  26. CGMutablePathRef path = CGPathCreateMutable(); 
  27. CGRect rectangle = CGRectInset(CGRectMake(00, CGRectGetWidth(self.bounds), CGRectGetWidth(self.bounds)), 3,3); 
  28. CGPoint p[3] = { 
  29. {CGRectGetMidX(self.bounds)-arrowWidth/2, CGRectGetWidth(self.bounds)-6}, 
  30. {CGRectGetMidX(self.bounds)+arrowWidth/2, CGRectGetWidth(self.bounds)-6}, 
  31. {CGRectGetMidX(self.bounds), CGRectGetHeight(self.bounds)-4
  32. }; 
  33. CGPathAddRoundedRect(path, NULL, rectangle, 55); 
  34. CGPathAddLines(path, NULL, p, 3); 
  35. CGPathCloseSubpath(path); 
  36. _framePath = [UIBezierPath bezierPathWithCGPath:path]; 
  37. CGPathRelease(path); 
  38. return _framePath; 
  39.  
  40. 我用代码生成了形状路径 并以此生成了layer的mask和shadowPath 
  41.  
  42. 使用时 只要直接用SDWebImage设置头像就行了 
  43. 1 
  44.  
  45. [annotationView.avatarView sd_setImageWithURL:[NSURL URLWithString:avatarURL] placeholderImage:placeHolderImage]; 

 

接下来用工具分析一下问题出来哪 分析性能当然是选择Instrments(用法在这里就不做介绍了) 打开Core Animation 然后运行程序 滑动地图 可以看到性能分析如下

原来平均帧数只有不到30帧 这离我们的目标60帧差得实在太远

再使用Debug Option来深入分析一下

由于MKMapView的原因 这里我们主要关心这几个选项

Color Blended Layers

Color Misaligned Images

Color Offscreen-Rendered Yellow

分别打开这几个选项 结果如下

 

可以看到

Color Blended Layers没有问题 不过这也是正常的 由于使用了mask 没有透明的地方

Color Misaligned Images除了默认头像外全中 这是因为服务器上的图片大小跟显示的大小不一致 导致缩放 而默认头像则是一致的 所以没问题

Color Offscreen-Rendered Yellow全中 由于使用了mask 导致大量的离屏渲染 这也是性能下降的主要原因

解决

问题的原因找到了 那么接下来该如何解决呢?

首先mask是肯定不能用了

其次下载下来的图片我们要预处理成实际大小

那么 直接把下载下来的图片合成为我们要显示的最终结果不就ok了吗? 试试看

  1. - (void)loadAnnotationImageWithURL:(NSString*)url imageView:(UIImageView*)imageView 
  2. //将合成后的图片缓存起来 
  3. NSString *annoImageURL = url; 
  4. NSString *annoImageCacheURL = [annoImageURL stringByAppendingString:@"cache"]; 
  5. UIImage *cacheImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:annoImageCacheURL]; 
  6. if ( cacheImage ) 
  7. //LLLog(@"hit cache"); 
  8. imageView.image = cacheImage; 
  9. else 
  10. //LLLog(@"no cache"); 
  11. [imageView sd_setImageWithURL:[NSURL URLWithString:annoImageURL] 
  12. placeholderImage:placeHolderImage 
  13. completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { 
  14. if (!error) 
  15. UIImage *annoImage = [image annotationImage]; 
  16. imageView.image = annoImage; 
  17. [[SDImageCache sharedImageCache] storeImage:annoImage forKey:annoImageCacheURL]; 
  18. }]; 
  19. @implementation UIImage (LJC) 
  20. - (UIImage*) annotationImage 
  21. static UIView *snapshotView = nil; 
  22. static UIImageView *imageView = nil; 
  23. if ( !snapshotView ) 
  24. snapshotView = [UIView new]; 
  25. snapshotView.frame = CGRectMake(00, TRACK_ANNOTATION_SIZE.width, TRACK_ANNOTATION_SIZE.height); 
  26. imageView = [UIImageView new]; 
  27. [snapshotView addSubview:imageView]; 
  28. imageView.clipsToBounds = YES; 
  29. imageView.frame = snapshotView.bounds; 
  30. imageView.contentMode = UIViewContentModeScaleAspectFill; 
  31. CGFloat arrowWidth = 14
  32. CGMutablePathRef path = CGPathCreateMutable(); 
  33. CGRect rectangle = CGRectInset(CGRectMake(00, CGRectGetWidth(imageView.bounds), CGRectGetWidth(imageView.bounds)), 3,3); 
  34. CGPoint p[3] = { 
  35. {CGRectGetMidX(imageView.bounds)-arrowWidth/2, CGRectGetWidth(imageView.bounds)-6}, 
  36. {CGRectGetMidX(imageView.bounds)+arrowWidth/2, CGRectGetWidth(imageView.bounds)-6}, 
  37. {CGRectGetMidX(imageView.bounds), CGRectGetHeight(imageView.bounds)-4
  38. }; 
  39. CGPathAddRoundedRect(path, NULL, rectangle, 55); 
  40. CGPathAddLines(path, NULL, p, 3); 
  41. CGPathCloseSubpath(path); 
  42. CAShapeLayer *shapelayer = [CAShapeLayer layer]; 
  43. shapelayer.frame = imageView.bounds; 
  44. shapelayer.path = path; 
  45. imageView.layer.mask = shapelayer; 
  46. snapshotView.layer.shadowPath = path; 
  47. snapshotView.layer.shadowRadius = 1.0f; 
  48. snapshotView.layer.shadowColor = [UIColor colorWithHex:0x666666FF].CGColor; 
  49. snapshotView.layer.shadowOpacity = 1.0f; 
  50. snapshotView.layer.shadowOffset = CGSizeMake(00); 
  51. CGPathRelease(path); 
  52. imageView.image = self; 
  53. UIGraphicsBeginImageContextWithOptions(TRACK_ANNOTATION_SIZE, NO, 0); 
  54. [snapshotView.layer renderInContext:UIGraphicsGetCurrentContext()]; 
  55. UIImage *copied = UIGraphicsGetImageFromCurrentImageContext(); 
  56. UIGraphicsEndImageContext(); 
  57. return copied; 
  58. @end 

然后使用的时候 只要简单的如下调用就OK了

  1. [self loadAnnotationImageWithURL:avatarURL imageView:annotationView.avatarView]; 

看看修改之后的Instruments表现如何

 

Color Blended Layers全中 这也是无可避免的 因为显示的就是一张带透明度的图 但是由于地图的特殊性(头像的位置变化间隔较长 所以不会经常引发合成 也没有动画) 所以这里也不是问题

Color Misaligned Images没问题了 因为头像已被缩放成了相同大小

Color Offscreen-Rendered Yellow没问题了 因为只是简单的显示了一张图片 而并没有需要离屏渲染的东西了

再来看下帧数情况

 

Oh-Yeah~ 不光帧数达到了我们的目标60帧(由于还有业务逻辑线程在后台跑 所以没有那么的稳定) 就连平均运行耗时都下降了不少 就算地图上再多显示几十个人 也不成问题了

小结

不光是MKMapView 其实包括UITableView在内的很多地方都可以用文中所说的方法去优化 其核心点就是 合成+缓存 当然 由于合成还是会耗费一部分资源的 所以比较适合头像这种小的资源

关于图形性能优化 可以看下这篇好文(有对文中提到的Debug Option不太明白的 这里有详细的解释)

责任编辑:chenqingxiang 来源: 里脊串的开发随笔
相关推荐

2019-03-19 14:52:00

性能优化MySQL数据库

2020-06-05 08:53:31

接口性能实践

2020-08-10 11:00:02

Python优化代码

2021-03-12 15:08:23

服务器性能优化

2011-09-27 10:35:44

2011-02-22 09:29:23

jQueryJavaScript

2023-11-06 07:45:42

单据图片处理

2020-10-30 14:11:38

服务器SDK堆栈

2021-08-26 22:26:55

性能优化技术

2021-03-18 23:47:18

MySQLselect索引

2022-09-15 10:02:58

测试软件

2011-06-28 10:41:50

DBA

2022-06-08 09:55:19

Data Catal字节跳动业务系统

2021-01-08 13:52:15

Consul微服务服务注册中心

2020-10-27 10:35:38

优化代码项目

2021-12-27 10:08:16

Python编程语言

2020-10-24 13:50:59

Python编程语言

2019-01-21 11:17:13

CPU优化定位

2023-11-29 12:12:24

Oceanbase数据库

2013-12-23 15:46:42

点赞
收藏

51CTO技术栈公众号