一次對(duì)MKMapView的性能優(yōu)化
最近做的項(xiàng)目主要是LBS這塊 主打成員定位功能 我們的UI設(shè)計(jì)是這樣的
乍一看上去是挺好挺美觀的 不同的人會(huì)顯示不同的頭像 可是當(dāng)人扎堆的時(shí)候 問(wèn)題就來(lái)了
當(dāng)人多的時(shí)候(例如上圖所示) 地圖滑動(dòng)起來(lái)就能感覺(jué)到明顯頓卡 那種不流暢感能折磨死人 所以 自然我們要解決這個(gè)問(wèn)題(等等 先不要吐槽為什么不用地圖聚合 因?yàn)檫@已經(jīng)是地圖放到***了 聚合不適合這次的問(wèn)題討論)
分析
首先看下我是怎么實(shí)現(xiàn)這個(gè)annotationView的 由于這個(gè)annotationsView是異形的(也就是無(wú)法通過(guò)設(shè)置圓角直接得到) 而且里面的圖片還因用戶而異 所以解決方案就是使用layer.mask來(lái)進(jìn)行遮罩 代碼如下
- @implementation MMAnnotationView
- - (instancetype)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier
- {
- self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
- if ( self )
- {
- self.frame = CGRectMake(0, 0, TRACK_ANNOTATION_SIZE.width, TRACK_ANNOTATION_SIZE.height);
- self.centerOffset = CGPointMake(0, -(TRACK_ANNOTATION_SIZE.height-3)/2);
- self.canShowCallout = NO;
- self.avatarView = [[UIImageView alloc] initWithFrame:self.bounds];
- [self addSubview:self.avatarView];
- self.avatarView.contentMode = UIViewContentModeScaleAspectFill;
- CAShapeLayer *shapelayer = [CAShapeLayer layer];
- shapelayer.frame = self.bounds;
- shapelayer.path = self.framePath.CGPath;
- self.avatarView.layer.mask = shapelayer;
- self.layer.shadowPath = self.framePath.CGPath;
- self.layer.shadowRadius = 1.0f;
- self.layer.shadowColor = [UIColor colorWithHex:0x666666FF].CGColor;
- self.layer.shadowOpacity = 1.0f;
- self.layer.shadowOffset = CGSizeMake(0, 0);
- self.layer.masksToBounds = NO;
- }
- return self;
- }
- //mask路徑
- - (UIBezierPath *)framePath
- {
- if ( !_framePath )
- {
- CGFloat arrowWidth = 14;
- CGMutablePathRef path = CGPathCreateMutable();
- CGRect rectangle = CGRectInset(CGRectMake(0, 0, CGRectGetWidth(self.bounds), CGRectGetWidth(self.bounds)), 3,3);
- CGPoint p[3] = {
- {CGRectGetMidX(self.bounds)-arrowWidth/2, CGRectGetWidth(self.bounds)-6},
- {CGRectGetMidX(self.bounds)+arrowWidth/2, CGRectGetWidth(self.bounds)-6},
- {CGRectGetMidX(self.bounds), CGRectGetHeight(self.bounds)-4}
- };
- CGPathAddRoundedRect(path, NULL, rectangle, 5, 5);
- CGPathAddLines(path, NULL, p, 3);
- CGPathCloseSubpath(path);
- _framePath = [UIBezierPath bezierPathWithCGPath:path];
- CGPathRelease(path);
- }
- return _framePath;
- }
- 我用代碼生成了形狀路徑 并以此生成了layer的mask和shadowPath
- 使用時(shí) 只要直接用SDWebImage設(shè)置頭像就行了
- 1
- [annotationView.avatarView sd_setImageWithURL:[NSURL URLWithString:avatarURL] placeholderImage:placeHolderImage];
接下來(lái)用工具分析一下問(wèn)題出來(lái)哪 分析性能當(dāng)然是選擇Instrments(用法在這里就不做介紹了) 打開(kāi)Core Animation 然后運(yùn)行程序 滑動(dòng)地圖 可以看到性能分析如下
原來(lái)平均幀數(shù)只有不到30幀 這離我們的目標(biāo)60幀差得實(shí)在太遠(yuǎn)
再使用Debug Option來(lái)深入分析一下
由于MKMapView的原因 這里我們主要關(guān)心這幾個(gè)選項(xiàng)
Color Blended Layers
Color Misaligned Images
Color Offscreen-Rendered Yellow
分別打開(kāi)這幾個(gè)選項(xiàng) 結(jié)果如下
可以看到
Color Blended Layers沒(méi)有問(wèn)題 不過(guò)這也是正常的 由于使用了mask 沒(méi)有透明的地方
Color Misaligned Images除了默認(rèn)頭像外全中 這是因?yàn)榉?wù)器上的圖片大小跟顯示的大小不一致 導(dǎo)致縮放 而默認(rèn)頭像則是一致的 所以沒(méi)問(wèn)題
Color Offscreen-Rendered Yellow全中 由于使用了mask 導(dǎo)致大量的離屏渲染 這也是性能下降的主要原因
解決
問(wèn)題的原因找到了 那么接下來(lái)該如何解決呢?
首先mask是肯定不能用了
其次下載下來(lái)的圖片我們要預(yù)處理成實(shí)際大小
那么 直接把下載下來(lái)的圖片合成為我們要顯示的最終結(jié)果不就ok了嗎? 試試看
- - (void)loadAnnotationImageWithURL:(NSString*)url imageView:(UIImageView*)imageView
- {
- //將合成后的圖片緩存起來(lái)
- NSString *annoImageURL = url;
- NSString *annoImageCacheURL = [annoImageURL stringByAppendingString:@"cache"];
- UIImage *cacheImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:annoImageCacheURL];
- if ( cacheImage )
- {
- //LLLog(@"hit cache");
- imageView.image = cacheImage;
- }
- else
- {
- //LLLog(@"no cache");
- [imageView sd_setImageWithURL:[NSURL URLWithString:annoImageURL]
- placeholderImage:placeHolderImage
- completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
- if (!error)
- {
- UIImage *annoImage = [image annotationImage];
- imageView.image = annoImage;
- [[SDImageCache sharedImageCache] storeImage:annoImage forKey:annoImageCacheURL];
- }
- }];
- }
- }
- @implementation UIImage (LJC)
- - (UIImage*) annotationImage
- {
- static UIView *snapshotView = nil;
- static UIImageView *imageView = nil;
- if ( !snapshotView )
- {
- snapshotView = [UIView new];
- snapshotView.frame = CGRectMake(0, 0, TRACK_ANNOTATION_SIZE.width, TRACK_ANNOTATION_SIZE.height);
- imageView = [UIImageView new];
- [snapshotView addSubview:imageView];
- imageView.clipsToBounds = YES;
- imageView.frame = snapshotView.bounds;
- imageView.contentMode = UIViewContentModeScaleAspectFill;
- CGFloat arrowWidth = 14;
- CGMutablePathRef path = CGPathCreateMutable();
- CGRect rectangle = CGRectInset(CGRectMake(0, 0, CGRectGetWidth(imageView.bounds), CGRectGetWidth(imageView.bounds)), 3,3);
- CGPoint p[3] = {
- {CGRectGetMidX(imageView.bounds)-arrowWidth/2, CGRectGetWidth(imageView.bounds)-6},
- {CGRectGetMidX(imageView.bounds)+arrowWidth/2, CGRectGetWidth(imageView.bounds)-6},
- {CGRectGetMidX(imageView.bounds), CGRectGetHeight(imageView.bounds)-4}
- };
- CGPathAddRoundedRect(path, NULL, rectangle, 5, 5);
- CGPathAddLines(path, NULL, p, 3);
- CGPathCloseSubpath(path);
- CAShapeLayer *shapelayer = [CAShapeLayer layer];
- shapelayer.frame = imageView.bounds;
- shapelayer.path = path;
- imageView.layer.mask = shapelayer;
- snapshotView.layer.shadowPath = path;
- snapshotView.layer.shadowRadius = 1.0f;
- snapshotView.layer.shadowColor = [UIColor colorWithHex:0x666666FF].CGColor;
- snapshotView.layer.shadowOpacity = 1.0f;
- snapshotView.layer.shadowOffset = CGSizeMake(0, 0);
- CGPathRelease(path);
- }
- imageView.image = self;
- UIGraphicsBeginImageContextWithOptions(TRACK_ANNOTATION_SIZE, NO, 0);
- [snapshotView.layer renderInContext:UIGraphicsGetCurrentContext()];
- UIImage *copied = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
- return copied;
- }
- @end
然后使用的時(shí)候 只要簡(jiǎn)單的如下調(diào)用就OK了
- [self loadAnnotationImageWithURL:avatarURL imageView:annotationView.avatarView];
看看修改之后的Instruments表現(xiàn)如何
Color Blended Layers全中 這也是無(wú)可避免的 因?yàn)轱@示的就是一張帶透明度的圖 但是由于地圖的特殊性(頭像的位置變化間隔較長(zhǎng) 所以不會(huì)經(jīng)常引發(fā)合成 也沒(méi)有動(dòng)畫(huà)) 所以這里也不是問(wèn)題
Color Misaligned Images沒(méi)問(wèn)題了 因?yàn)轭^像已被縮放成了相同大小
Color Offscreen-Rendered Yellow沒(méi)問(wèn)題了 因?yàn)橹皇呛?jiǎn)單的顯示了一張圖片 而并沒(méi)有需要離屏渲染的東西了
再來(lái)看下幀數(shù)情況
Oh-Yeah~ 不光幀數(shù)達(dá)到了我們的目標(biāo)60幀(由于還有業(yè)務(wù)邏輯線程在后臺(tái)跑 所以沒(méi)有那么的穩(wěn)定) 就連平均運(yùn)行耗時(shí)都下降了不少 就算地圖上再多顯示幾十個(gè)人 也不成問(wèn)題了
小結(jié)
不光是MKMapView 其實(shí)包括UITableView在內(nèi)的很多地方都可以用文中所說(shuō)的方法去優(yōu)化 其核心點(diǎn)就是 合成+緩存 當(dāng)然 由于合成還是會(huì)耗費(fèi)一部分資源的 所以比較適合頭像這種小的資源
關(guān)于圖形性能優(yōu)化 可以看下這篇好文(有對(duì)文中提到的Debug Option不太明白的 這里有詳細(xì)的解釋)