概述
对于界面的性能优化, 简单的说就是保持界面流畅不掉帧, 当然原理这种网上一搜一大把, 有空的话看看YYKit也就能够知晓个大概. 硬是要说原理的话, 就是当Vsync信号来临的16.67ms内CPU做完排版, 绘制, 解码, GPU避免离屏渲染之类的, 就会在Runloop下一次来临的时候渲染到屏幕上.
预排版
对于界面流畅, 第一个想到的就是预排版了, 而且预排版的作用显著, 原理也很简单, 就是异步预先计算, 避免每次在layoutSubviews中, cell重用的时候进行重复计算. 诶… 又要被说只讲关键词了… 但原理就是那么简单, 还能讲出个啥? 再说的细了代码就写出来了…
#import "Service.h"
#import
@implementation Service
- (void)fetchMockDataWithParam:(NSDictionary *)parameter completion:(RequestCompletionBlock)completion {
AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
[manager GET:@"http://localhost:8080/fetchMockData" parameters:parameter progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if ([responseObject[@"status"] isEqualToString: @"success"]) {
completion(responseObject[@"data"], nil);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
completion(nil, error);
}];
}
@end
当然你需要请求一下刚才的模拟数据, 注意localhost是http的需要强制越权.
#import "ViewModel.h"
#import "ComponentModel.h"
#import "ComponentLayout.h"
#import "Element.h"
#import
@implementation ViewModel
- (Service *)service {
if (!_service) {
_service = [Service new];
}
return _service;
}
- (void)reloadData:(LayoutCompeltionBlock)completion error:(void(^)(void))errorCompletion {
[self.service fetchMockDataWithParam:nil completion:^(NSArray *models, NSError *error) {
if (models.count > 0 && error == nil) {
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSMutableArray * array = [NSMutableArray new];
for (ComponentModel * model in models) {
ComponentLayout * layout = [self preLayoutFrom:model];
[array addObject:layout];
}
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(array);
}
});
});
} else {
errorCompletion();
}
}];
}
- (void)loadMoreData:(LayoutCompeltionBlock)completion {
[self reloadData:completion error:nil];
}
- (ComponentLayout *)preLayoutFrom:(ComponentModel *)model {
ComponentLayout * layout = [ComponentLayout new];
layout.cellWidth = [UIScreen mainScreen].bounds.size.width;
CGFloat cursor = 0;
CGFloat x = 5.;
CGFloat y = 0.;
CGFloat height = 0.;
NSMutableArray * textElements = [NSMutableArray array];
NSArray * texts = model[@"texts"];
for (NSUInteger i = 0; i < texts.count; i++) {
NSString * text = texts[i];
CGSize size = [text boundingRectWithSize:CGSizeMake(MAXFLOAT, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:7]} context:nil].size;
if ((x + size.width) > layout.cellWidth) {
x = 5;
y += (size.height + 10);
height = y + size.height + 5;
}
CGRect frame = CGRectMake(x, y, size.width + 5, size.height + 5);
x += (size.width + 10);
Element * element = [Element new];
element.value = text;
element.frame = frame;
[textElements addObject:element];
}
cursor += height + 5;
x = 5.; y = cursor; height = 0.;
NSMutableArray * imageElements = [NSMutableArray array];
NSArray * images = model[@"images"];
for (NSUInteger i = 0; i < images.count; i++) {
NSString * url = images[i];
CGSize size = CGSizeMake(40, 40);
if ((x + size.width) > layout.cellWidth) {
x = 5;
y += (size.height + 5);
height = (y - cursor) + size.height + 5;
}
CGRect frame = CGRectMake(x, y, size.width, size.height);
x += (size.width + 5);
Element * element = [Element new];
element.value = url;
element.frame = frame;
[imageElements addObject:element];
}
cursor += height + 5;
layout.cellHeight = cursor;
layout.textElements = textElements;
layout.imageElements = imageElements;
return layout;
}
@end
- (void)setupData:(ComponentLayout *)layout asynchronously:(BOOL)asynchronously {
_layout = layout; _asynchronously = asynchronously;
[self displayImageView];
if (!asynchronously) {
for (Element * element in layout.textElements) {
UILabel * label = (UILabel *)[_labelReusePool dequeueReusableObject];
if (!label) {
label = [UILabel new];
[_labelReusePool addUsingObject:label];
}
label.text = element.value;
label.frame = element.frame;
label.font = [UIFont systemFontOfSize:7];
[self.contentView addSubview:label];
}
[_labelReusePool reset];
}
}
代码也是非常的简单, 即是根据获取的的数据进行排版而已, 然后cell直接获取排版后的frame进行一步赋值操作而已. 有兴趣也可以看看sunnyxx的FDTemplateLayoutCell.
重用池
眼尖的同学肯定就看到上面代码由类似于cell的重用机制, 这个其实就是下面要讲到的重用池的概念, 原理也是非常简单的, 就是维护了两个队列, 一个是当前队列, 一个是可重用队列.
#import "ReusePool.h"
@interface ReusePool ()
@property (nonatomic, strong) NSMutableSet * waitUsedQueue;
@property (nonatomic, strong) NSMutableSet * usingQueue;
@property (nonatomic, strong) NSLock * lock;
@end
@implementation ReusePool
- (instancetype)init
{
self = [super init];
if (self) {
_waitUsedQueue = [NSMutableSet set];
_usingQueue = [NSMutableSet set];
_lock = [NSLock new];
}
return self;
}
- (NSObject *)dequeueReusableObject {
NSObject * object = [_waitUsedQueue anyObject];
if (object == nil) {
return nil;
} else {
[_lock lock];
[_waitUsedQueue removeObject:object];
[_usingQueue addObject:object];
[_lock unlock];
return object;
}
}
- (void)addUsingObject:(UIView *)object {
if (object == nil) {
return;
}
[_usingQueue addObject:object];
}
- (void)reset {
NSObject * object = nil;
while ((object = [_usingQueue anyObject])) {
[_lock lock];
[_usingQueue removeObject:object];
[_waitUsedQueue addObject:object];
[_lock unlock];
}
}
@end
这里由于我这个重用队列, 并不仅仅针对于UI, 所以在多线程访问的情况下, 是需要加锁处理的, 这里就用最通用的pthread-mutex进行加锁.
预解码
这里做的就是通过url进行的异步解码的处理, 相关原理的文章其实很多, 自行查阅.
@implementation NSString (Extension)
- (void)preDecodeThroughQueue:(dispatch_queue_t)queue completion:(void(^)(UIImage *))completion {
dispatch_async(queue, ^{
CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
cgImage = CGBitmapContextCreateImage(context);
UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
CGContextRelease(context);
CGImageRelease(cgImage);
completion(image);
});
}
@end
预渲染
比如imageView.layer.cornerRadius及imageView.layer.masksToBounds = YES;这类操作会导致离屏渲染, GPU会导致新开缓冲区造成消耗. 为了避免离屏渲染,你应当尽量避免使用 layer 的 border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应内容。这种SDWebImage的缓存策略是有很大的参考意义的.
@implementation UIImage (Extension)
- (UIImage *)cornerRadius:(CGFloat)cornerRadius {
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius];
UIGraphicsBeginImageContextWithOptions(self.size, false, [UIScreen mainScreen].scale);
CGContextAddPath(UIGraphicsGetCurrentContext(), bezierPath.CGPath);
CGContextClip(UIGraphicsGetCurrentContext());
[self drawInRect:rect];
CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
@end
- (void)displayImageView {
for (Element * element in _layout.imageElements) {
UIImageView * imageView = (UIImageView *)[_imageReusePool dequeueReusableObject];
if (!imageView) {
imageView = [UIImageView new];
[_imageReusePool addUsingObject:imageView];
}
UIImage * image = (UIImage *)[ComponentCell.asyncReusePool dequeueReusableObject];
if (!image) {
NSString * url = element.value;
[url preDecodeThroughQueue:concurrentQueue completion:^(UIImage * image) {
[ComponentCell.asyncReusePool addUsingObject:image];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = image;
});
}];
} else {
imageView.image = image;
}
imageView.frame = element.frame;
[self.contentView addSubview:imageView];
}
[ComponentCell.asyncReusePool reset];
[_imageReusePool reset];
}
这样通过和重用池的结合就可以达到很好的效果, 当然因为我这里是一张相同的图片, 正常情况下需要设计一整套的缓存策略, hash一下url什么的.
通过上述的方案结合, 我们已经能够比较流畅的显示那么多数据了 而且帧数还是不错的, 但还有优化的空间.
进过了上面的优化, 我们其实已经做的不错了, 但是看下界面的层次, 是不是有点得了密集恐惧症… 接下来要讲的异步绘制就能够很好的解决这个问题. 并更加的优化流畅度.
异步绘制的原理么, 就是异步的drawInLayer, 异步的画在上下文上并统一绘制.
- (void)displayLayer:(CALayer *)layer {
if (!layer) return;
if (layer != self.layer) return;
if (![layer isKindOfClass:[AsyncDrawLayer class]]) return;
if (!self.isAsynchronously) return;
AsyncDrawLayer *tempLayer = (AsyncDrawLayer *)layer;
[tempLayer increaseCount];
NSUInteger oldCount = tempLayer.drawsCount;
CGRect bounds = self.bounds;
UIColor * backgroundColor = self.backgroundColor;
layer.contents = nil;
dispatch_async(serialQueue, ^{
void (^failedBlock)(void) = ^{
NSLog(@"displayLayer failed");
};
if (tempLayer.drawsCount != oldCount) {
failedBlock();
return;
}
CGSize contextSize = layer.bounds.size;
BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
CGContextRef context = NULL;
if (contextSizeValid) {
UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
if (bounds.origin.x || bounds.origin.y) {
CGContextTranslateCTM(context, bounds.origin.x, -bounds.origin.y);
}
if (backgroundColor && backgroundColor != [UIColor clearColor]) {
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGContextFillRect(context, bounds);
}
[self asyncDraw:YES context:context completion:^(BOOL drawingFinished) {
if (drawingFinished && oldCount == tempLayer.drawsCount) {
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
{
UIImage * image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
dispatch_async(dispatch_get_main_queue(), ^{
if (oldCount != tempLayer.drawsCount) {
failedBlock();
return;
}
layer.contents = (id)image.CGImage;
layer.opacity = 0.0;
[UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
layer.opacity = 1.0;
} completion:NULL];
});
}
if (CGImage) {
CGImageRelease(CGImage);
}
} else {
failedBlock();
}
}];
CGContextRestoreGState(context);
}
UIGraphicsEndImageContext();
});
}
代码其实也没什么好讲的只要注意下多线程重绘时的问题也就可以了.
- (void)asyncDraw:(BOOL)asynchronously context:(CGContextRef)context completion:(void(^)(BOOL))completion {
for (Element * element in _layout.textElements) {
NSMutableParagraphStyle * paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
paragraphStyle.alignment = NSTextAlignmentCenter;
[element.value drawInRect:element.frame withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:7],
NSForegroundColorAttributeName:[UIColor blackColor],
NSParagraphStyleAttributeName:paragraphStyle}];
}
completion(YES);
}
当在当前线程全部绘制完上下文后, 会统一的渲染到layer上, 很好理解的其实.
这样我们的图层就会被合成, 帧率也一直保持在60帧的样子.
语法技巧
OC现在也支持类属性了, 具体的使用可以看我swift的renderTree.
@property (class, nonatomic,strong) ReusePool * asyncReusePool;
static ReusePool * _asyncReusePool = nil;
+ (ReusePool *)asyncReusePool {
if (_asyncReusePool == nil) {
_asyncReusePool = [[ReusePool alloc] init];
}
return _asyncReusePool;
}
+ (void)setAsyncReusePool:(ReusePool *)asyncReusePool {
if (_asyncReusePool != asyncReusePool) {
_asyncReusePool = asyncReusePool;
}
}
作者:Castie1
链接:https://juejin.im/post/5b41b0c26fb9a04fdb169cb0
来源:掘金