界面優(yōu)化

卡頓的原理

想要進(jìn)行界面優(yōu)化,首先就要了解怎么產(chǎn)生卡頓?
通常來(lái)說(shuō)計(jì)算機(jī)中的顯示過(guò)程是下面這樣的,通過(guò)CPU、GPU、顯示器協(xié)同工作來(lái)將圖片顯示到屏幕上

圖像顯示過(guò)程
  • CPU計(jì)算好顯示內(nèi)容,提交至GPU
  • GPU經(jīng)過(guò)渲染完成后將渲染的結(jié)果放入FrameBuffer(幀緩存區(qū))
  • 隨后視頻控制器會(huì)按照VSync垂直信號(hào)逐行讀取FrameBuffer的數(shù)據(jù)
  • 經(jīng)過(guò)可能的數(shù)模轉(zhuǎn)換傳遞給顯示器進(jìn)行顯示

最開(kāi)始時(shí)FrameBuffer只有一個(gè),這種情況下FrameBuffer的讀取和刷新有很大的效率問(wèn)題,為了解決這個(gè)問(wèn)題,引入了雙緩存區(qū)雙緩沖機(jī)制。在這種情況下,GPU會(huì)預(yù)先渲染好一幀放入FrameBuffer,讓視頻控制器讀取。當(dāng)下一幀渲染好后,GPU會(huì)直接將視頻控制器的指針指向第二個(gè)FrameBuffer。

雙緩存機(jī)制解決了效率問(wèn)題,但隨之而來(lái)的是新的問(wèn)題。比如當(dāng)前這一幀處理比較慢,GPU會(huì)將視頻控制器的指針指向第二個(gè)FrameBuffer,那么上一幀的圖像處理就會(huì)丟掉即掉幀。現(xiàn)象就是屏幕出現(xiàn)跳屏卡頓

屏幕卡頓原因

VSync信號(hào)到來(lái)后,系統(tǒng)圖形服務(wù)會(huì)通過(guò)CADisplayLink等機(jī)制通知App。App主線程開(kāi)始在CPU中計(jì)算顯示內(nèi)容,隨后CPU 會(huì)將計(jì)算好的內(nèi)容提交到GPU,由GPU進(jìn)行變換、合成、渲染。隨后GPU會(huì)把渲染結(jié)果提交到幀緩沖區(qū),等待下一次VSync信號(hào)到來(lái)時(shí)顯示到屏幕上。由于垂直同步的機(jī)制,如果在一個(gè)VSync時(shí)間內(nèi),CPU 或者 GPU 沒(méi)有完成內(nèi)容提交,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。

如下圖顯示過(guò)程,第1幀在VSync到來(lái)前,處理完成,正常顯示,第2幀在VSync到來(lái)后,仍在處理中,此時(shí)屏幕不刷新依舊顯示第1幀,此時(shí)就出現(xiàn)了掉幀情況,渲染時(shí)就會(huì)出現(xiàn)明顯的卡頓現(xiàn)象。

掉幀圖示

由上圖可知CPUGPU無(wú)論哪個(gè)阻礙了顯示流程,都會(huì)造成掉幀現(xiàn)象。為了給用戶(hù)提供更好的體驗(yàn),我們需要進(jìn)行卡頓檢測(cè)以及相應(yīng)的優(yōu)化。

卡頓的檢測(cè)

卡頓監(jiān)控的方案一般有兩種

  • FPS監(jiān)控:為了保持流程的UI交互,App的刷新頻率應(yīng)該保持在60fps左右,其原因是iOS設(shè)備默認(rèn)的刷新頻率是60次/秒,而1次刷新(即VSync信號(hào)發(fā)出)的間隔是1000ms/60 = 16.67ms。如果在16.67ms內(nèi)沒(méi)有準(zhǔn)備好下一幀數(shù)據(jù),就會(huì)產(chǎn)生卡頓
  • 主線程卡頓監(jiān)控:通過(guò)子線程監(jiān)測(cè)主線程的RunLoop,判斷兩個(gè)狀態(tài)(kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting)之間的耗時(shí)是否達(dá)到一定閾值
FPS監(jiān)控
  • 方案一:參考YYKit中的YYFPSLabel通過(guò)CADisplayLink實(shí)現(xiàn)。借助link的時(shí)間差,來(lái)計(jì)算一次刷新所需的時(shí)間,然后通過(guò)刷新次數(shù) / 時(shí)間差得到刷新頻次,并判斷是否符合范圍,通過(guò)顯示不同的文字顏色來(lái)表示卡頓嚴(yán)重程度。
<!-- YYFPSLabel.h -->
#import <UIKit/UIKit.h>

/**
 Show Screen FPS...
 
 The maximum fps in OSX/iOS Simulator is 60.00.
 The maximum fps on iPhone is 59.97.
 The maxmium fps on iPad is 60.0.
 */
@interface YYFPSLabel : UILabel

@end

<!-- YYFPSLabel.m -->
#import "YYFPSLabel.h"
#import "YYKit.h"

#define kSize CGSizeMake(55, 20)

@implementation YYFPSLabel {
    CADisplayLink *_link;
    NSUInteger _count;
    NSTimeInterval _lastTime;
    UIFont *_font;
    UIFont *_subFont;
    
    NSTimeInterval _llll;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (frame.size.width == 0 && frame.size.height == 0) {
        frame.size = kSize;
    }
    self = [super initWithFrame:frame];
    
    self.layer.cornerRadius = 5;
    self.clipsToBounds = YES;
    self.textAlignment = NSTextAlignmentCenter;
    self.userInteractionEnabled = NO;
    self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
    
    _font = [UIFont fontWithName:@"Menlo" size:14];
    if (_font) {
        _subFont = [UIFont fontWithName:@"Menlo" size:4];
    } else {
        _font = [UIFont fontWithName:@"Courier" size:14];
        _subFont = [UIFont fontWithName:@"Courier" size:4];
    }
    
    _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    return self;
}

- (void)dealloc {
    [_link invalidate];
}

- (CGSize)sizeThatFits:(CGSize)size {
    return kSize;
}

// 60 vs 16.67ms
// 1/60  * 1000 
- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    float fps = _count / delta;
    _count = 0;
    
    CGFloat progress = fps / 60.0;
    UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
    
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
    [text setColor:color range:NSMakeRange(0, text.length - 3)];
    [text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
    text.font = _font;
    [text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
    
    self.attributedText = text;
}

@end
主線程卡頓監(jiān)控
  • 方案二:通過(guò)RunLoop來(lái)監(jiān)控,因?yàn)榭D的是事務(wù),而事務(wù)是交由主線程RunLoop處理的。
    實(shí)現(xiàn)原理:檢測(cè)主線程每次執(zhí)行消息循環(huán)的時(shí)間,當(dāng)這個(gè)時(shí)間大于規(guī)定的閾值時(shí),就記為發(fā)生了一次卡頓。
<!-- LGBlockMonitor.h -->
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN


@interface LGBlockMonitor : NSObject

+ (instancetype)sharedInstance;

- (void)start;

@end

NS_ASSUME_NONNULL_END

<!-- LGBlockMonitor.m -->
#import "LGBlockMonitor.h"

@interface LGBlockMonitor (){
    CFRunLoopActivity activity;
}

@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;

@end

@implementation LGBlockMonitor

+ (instancetype)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (void)start{
    [self registerObserver];
    [self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
    monitor->activity = activity;
    // 發(fā)送信號(hào)
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //NSIntegerMax : 優(yōu)先級(jí)最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            NSIntegerMax,
                                                            &CallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
    // 創(chuàng)建信號(hào)
    _semaphore = dispatch_semaphore_create(0);
    // 在子線程監(jiān)控時(shí)長(zhǎng)
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 超時(shí)時(shí)間是 1 秒,沒(méi)有等到信號(hào)量,st 就不等于 0, RunLoop 所有的任務(wù)
            long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
            if (st != 0)
            {
                if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
                {
                    if (++self->_timeoutCount < 2){
                        NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                        continue;
                    }
                    // 一秒左右的衡量尺度 很大可能性連續(xù)來(lái) 避免大規(guī)模打印!
                    NSLog(@"檢測(cè)到超過(guò)兩次連續(xù)卡頓");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}

@end

使用方式:

[[LGBlockMonitor sharedInstance] start];
  • 方案三:直接使用三方庫(kù)
  1. Swift可以使用ANREye,其實(shí)現(xiàn)思路是:創(chuàng)建一個(gè)子線程通過(guò)信號(hào)量去ping主線程,因?yàn)閜ing的時(shí)候主線程肯定是在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之間。每次檢測(cè)時(shí)設(shè)置標(biāo)記位為YES,然后派發(fā)任務(wù)到主線程中將標(biāo)記位設(shè)置為NO。接著子線程沉睡超過(guò)閾值時(shí),判斷標(biāo)志位是否成功設(shè)置成NO,如果沒(méi)有說(shuō)明主線程發(fā)生了卡頓。ANREye是使用子線程Ping的方式監(jiān)測(cè)卡頓的。
  2. OC可以使用 微信matrix、滴滴DoraemonKit

界面優(yōu)化-預(yù)排版

開(kāi)發(fā)圖文混排頁(yè)面時(shí),滑動(dòng)頁(yè)面需要不停的計(jì)算和渲染,比如計(jì)算cell高度。案例代碼如下

// 頁(yè)面數(shù)據(jù)源
- (void)loadData{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"timeLine" ofType:@"json"];
    NSData *data = [[NSData alloc] initWithContentsOfFile:path];
    NSDictionary *dicJson=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
   
    for (id json in dicJson[@"data"]) {
        LGTimeLineModel *timeLineModel = [LGTimeLineModel yy_modelWithJSON:json];
        [self.timeLineModels addObject:timeLineModel];
    }
    [self.timeLineTableView reloadData];
}

#pragma mark -- UITableViewDelegate
// 返回cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    LGTimeLineModel *timeLineModel = self.timeLineModels[indexPath.row];
    timeLineModel.cacheId = indexPath.row + 1;
    NSString *stateKey = nil;
    
    if (timeLineModel.isExpand) {
        stateKey = @"expanded";
    } else {
        stateKey = @"unexpanded";
    }
    
    LGTimeLineCell *cell = [[LGTimeLineCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
    
    [cell configureTimeLineCell:timeLineModel];
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];
    [cell setNeedsLayout];
    [cell layoutIfNeeded];
    
    CGFloat rowHeight = 0;
    for (UIView *bottomView in cell.contentView.subviews) {
        if (rowHeight < CGRectGetMaxY(bottomView.frame)) {
            rowHeight = CGRectGetMaxY(bottomView.frame);
        }
    }
    return rowHeight;
}

其實(shí)在網(wǎng)絡(luò)請(qǐng)求的時(shí)候,我們已經(jīng)拿到了數(shù)據(jù)。有了這些數(shù)據(jù),我們就能知道cell的高度。這個(gè)時(shí)候可以對(duì)頁(yè)面進(jìn)行預(yù)排版,而不需要等到tableView渲染的時(shí)候才去進(jìn)行大量計(jì)算。我們可以在model中提前計(jì)算好cell行高,頁(yè)面frame,富文本等等。其主要思想是把耗時(shí)的操作放在頁(yè)面顯示前處理,這樣頁(yè)面滑動(dòng)的時(shí)候就不需要計(jì)算很多遍,只是在處理數(shù)據(jù)的時(shí)候計(jì)算一次,這就是對(duì)頁(yè)面做了優(yōu)化處理。

// 優(yōu)化后代碼
- (void)loadData{
    //外面的異步線程:網(wǎng)絡(luò)請(qǐng)求的線程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       //加載`JSON 文件`
       NSString *path = [[NSBundle mainBundle] pathForResource:@"timeLine" ofType:@"json"];
       NSData *data = [[NSData alloc] initWithContentsOfFile:path];
       NSDictionary *dicJson=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
       for (id json in dicJson[@"data"]) {
            LGTimeLineModel *timeLineModel = [LGTimeLineModel yy_modelWithJSON:json];
            [self.timeLineModels addObject:timeLineModel];
       }
           
       for (LGTimeLineModel *timeLineModel in self.timeLineModels) {
            LGTimeLineCellLayout *cellLayout = [[LGTimeLineCellLayout alloc] initWithModel:timeLineModel];
            [self.layouts addObject:cellLayout];
       }
           
       dispatch_async(dispatch_get_main_queue(), ^{
            [self.timeLineTableView reloadData];
       });
    });
}

#pragma mark -- UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return  self.layouts[indexPath.row].height;
}

<!-- LGTimeLineCellLayout.m文件 -->
// 把cell行高,頁(yè)面frame,富文本等等提前處理好
- (instancetype)initWithModel:(LGTimeLineModel *)timeLineModel{
    if (!timeLineModel) return nil;
    self = [super init];
    if (self) {
        _timeLineModel = timeLineModel;
        [self layout];
    }
    return self;
}

- (void)setTimeLineModel:(LGTimeLineModel *)timeLineModel{
    _timeLineModel = timeLineModel;
    [self layout];
}

- (void)layout{

    CGFloat sWidth = [UIScreen mainScreen].bounds.size.width;

    self.iconRect = CGRectMake(10, 10, 45, 45);
    CGFloat nameWidth = [self calcWidthWithTitle:_timeLineModel.name font:titleFont];
    CGFloat nameHeight = [self calcLabelHeight:_timeLineModel.name fontSize:titleFont width:nameWidth];
    self.nameRect = CGRectMake(CGRectGetMaxX(self.iconRect) + nameLeftSpaceToHeadIcon, 17, nameWidth, nameHeight);

    CGFloat msgWidth = sWidth - 10 - 16;
    CGFloat msgHeight = 0;

    //文本信息高度計(jì)算
    NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    [paragraphStyle setLineSpacing:5];
    NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:msgFont],
                                 NSForegroundColorAttributeName: [UIColor colorWithRed:26/255.0 green:26/255.0 blue:26/255.0 alpha:1]
                                 ,NSParagraphStyleAttributeName: paragraphStyle
                                 ,NSKernAttributeName:@0
                                 };
    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:_timeLineModel.msgContent attributes:attributes];
    msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];

    if (attrStr.length > msgExpandLimitHeight) {
        if (_timeLineModel.isExpand) {
            self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
        } else {
            attrStr = [[NSMutableAttributedString alloc] initWithString:[_timeLineModel.msgContent substringToIndex:msgExpandLimitHeight] attributes:attributes];
            msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];
            self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
        }
    } else {
        self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
    }

    if (attrStr.length < msgExpandLimitHeight) {
        self.expandHidden = YES;
        self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) - 20, 30, 20);
    } else {
        self.expandHidden = NO;
        self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) + 10, 30, 20);
    }
    
    CGFloat timeWidth = [self calcWidthWithTitle:_timeLineModel.time font:timeAndLocationFont];
    CGFloat timeHeight = [self calcLabelHeight:_timeLineModel.time fontSize:timeAndLocationFont width:timeWidth];
    self.imageRects = [NSMutableArray array];
    if (_timeLineModel.contentImages.count == 0) {
//        self.timeRect = CGRectMake(10, CGRectGetMaxY(self.expandRect) + 10, timeWidth, timeHeight);
    } else {
        if (_timeLineModel.contentImages.count == 1) {
            CGRect imageRect = CGRectMake(11, CGRectGetMaxY(self.expandRect) + 10, 250, 150);
            [self.imageRects addObject:@(imageRect)];
        } else if (_timeLineModel.contentImages.count == 2 || _timeLineModel.contentImages.count == 3) {
            for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
                CGRect imageRect = CGRectMake(11 + i * (10 + 90), CGRectGetMaxY(self.expandRect) + 10, 90, 90);
                [self.imageRects addObject:@(imageRect)];
            }
        } else if (_timeLineModel.contentImages.count == 4) {
            for (int i = 0; i < 2; i++) {
                for (int j = 0; j < 2; j++) {
                    CGRect imageRect = CGRectMake(11 + j * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + i * (10 + 90), 90, 90);
                    [self.imageRects addObject:@(imageRect)];
                }
            }
        } else if (_timeLineModel.contentImages.count == 5 || _timeLineModel.contentImages.count == 6 || _timeLineModel.contentImages.count == 7 || _timeLineModel.contentImages.count == 8 || _timeLineModel.contentImages.count == 9) {
            for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
                CGRect imageRect = CGRectMake(11 + (i % 3) * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + (i / 3) * (10 + 90), 90, 90);
                [self.imageRects addObject:@(imageRect)];
            }
        }
    }

    if (self.imageRects.count > 0) {
        CGRect lastRect = [self.imageRects[self.imageRects.count - 1] CGRectValue];
        self.seperatorViewRect = CGRectMake(0, CGRectGetMaxY(lastRect) + 10, sWidth, 15);
    }
    
    self.height = CGRectGetMaxY(self.seperatorViewRect);
}

本質(zhì)是在model中把所有頁(yè)面相關(guān)邏輯提前處理好,轉(zhuǎn)成layoutModel如上面所示。

界面優(yōu)化-預(yù)解碼

比如頁(yè)面加載一張圖片,其加載流程如下

image.png
image.png
  • UIImageView的本質(zhì)是一個(gè)模型,里面包含了UIImage
  • UIImage中包含了Data Buffer,圖片是通過(guò)Data Buffer二進(jìn)制流轉(zhuǎn)換過(guò)來(lái)的。
  • 再通過(guò)image Buffer緩存區(qū)進(jìn)行儲(chǔ)存。
  • 最后通過(guò)ViewController顯示到UIImageView上。

UIImageViewmodel屬性依賴(lài)于Data Buffer的解碼過(guò)程,解碼之后Image Buffer才能夠進(jìn)行緩存,緩存之后才能在幀緩存區(qū)Frame Buffer中進(jìn)行渲染。

我們?cè)诩虞d圖片的時(shí)候,一般使用SDWebImage,下面探索其原理......

  • 查看sd_setImageWithURL方法
image.png
  • image圖片來(lái)源于網(wǎng)絡(luò)請(qǐng)求didComplete
image.png
  • 這里拿到的是二進(jìn)制文件imageData
image.png
  • 對(duì)二進(jìn)制文件進(jìn)行解碼
image.png

把圖片的所有二進(jìn)制流進(jìn)行解碼,比如對(duì)圖片的寬高imageRef、大小、縮放因子maxPixelSize進(jìn)行解碼,最終形成了UIImage,最終就是顯示。

圖片為什么需要預(yù)解碼

SDWebImage在子線程對(duì)圖片的二進(jìn)制文件imageData做了解碼操作,那么圖片的展示為什么需要進(jìn)行上面的解碼呢?SDWebImage的解碼操作又放在了哪里?通過(guò)添加符號(hào)斷點(diǎn)、打印堆棧信息進(jìn)行調(diào)試查看......

添加符號(hào)斷點(diǎn)
查看堆棧信息

最終發(fā)現(xiàn)SDWebImage在這一層面先做了預(yù)解碼操作,原因是頁(yè)面的卡頓大都是來(lái)自于圖片展示。

圖片加載流程
  • 網(wǎng)絡(luò)請(qǐng)求中獲取到了Data BufferImageData
  • ImageData交給子線程進(jìn)行解碼,解碼完成之后進(jìn)行回調(diào),回調(diào)回來(lái)的就是Image Buffer像素緩存區(qū)
  • 最后交給Frame Buffer去顯示。

最終優(yōu)化的就是Data Buffer解碼成Image Buffer的過(guò)程,所以大部分的三方框架都是在這一過(guò)程做了大量處理。

蘋(píng)果在底層提供了一圖形編解碼插件,比如原生音視頻框架AVFoundation、FFmpeg。其中FFmpeg中最好的點(diǎn)就是對(duì)視頻的編解碼過(guò)程。

異步渲染

按需加載

只有需要了才去加載。例如TableView滑動(dòng)時(shí),滑動(dòng)的越快也就意味著計(jì)算、渲染的頻率越高。這樣就有可能導(dǎo)致頁(yè)面卡頓......

  • 優(yōu)化思路一:比如滑動(dòng)時(shí)使用默認(rèn)占位圖,當(dāng)滑動(dòng)了10條cell,我們只處理可視范圍內(nèi)的3條cell
  • 優(yōu)化思路二:滑動(dòng)時(shí)使用默認(rèn)占位圖,而是在滑動(dòng)停止時(shí)處理加載圖片的數(shù)據(jù)
異步渲染

關(guān)于UIViewLayer之間的關(guān)系?

  • UIView主要是用于頁(yè)面交互,比如頁(yè)面點(diǎn)擊等等
  • Layer主要用于頁(yè)面的渲染

真正的頁(yè)面展示并不是UIView去做,而是Layer層做的。

頁(yè)面渲染原理

渲染的過(guò)程是非常耗時(shí)的,這個(gè)過(guò)程稱(chēng)之為事物。事務(wù)里面有如下環(huán)節(jié)

  • layout構(gòu)建視圖
  • displayer繪制
  • prepare關(guān)于coreAnimation動(dòng)畫(huà)的操作
  • commit提交事務(wù) reader server去做事務(wù)相關(guān)的處理
drawRect的流程
  • drawRect是依賴(lài)于當(dāng)前UIView提供的一個(gè)UIViewRendering的功能
image.png
  • 查看drawRect方法的堆棧信息
drawRect的堆棧信息
  • 繪制圖層的耗時(shí)操作放在子線程進(jìn)行,最后渲染的步驟放在主線程
<!-- 下面繪制的耗時(shí)操作放在子線程處理 -->
//繪制流程的發(fā)起函數(shù)
- (void)display{
    // Graver 實(shí)現(xiàn)思路
    CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
    // 渲染整個(gè)圖層
    [self.delegate layerWillDraw:self];
    [self drawInContext:context];
    [self.delegate displayLayer:self];
    [self.delegate performSelector:@selector(closeContext)];
}

<!-- 渲染的步驟放在主線程 -->
//layer.contents = (位圖)
- (void)displayLayer:(CALayer *)layer{
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
        layer.contents = (__bridge id)(image.CGImage);
    });
}

異步渲染框架Graver渲染流程

Graver異步渲染流程

界面優(yōu)化總結(jié)

CPU層面的優(yōu)化

  • 盡量用輕量級(jí)的對(duì)象代替重量級(jí)的對(duì)象,可以對(duì)性能有所優(yōu)化。例如不需要相應(yīng)觸摸事件的控件,用CALayer代替UIView
  • 盡量減少對(duì)UIViewCALayer的屬性修改
  1. CALayer內(nèi)部并沒(méi)有屬性,當(dāng)調(diào)用屬性方法時(shí),其內(nèi)部是通過(guò)運(yùn)行時(shí)resolveInstanceMethod為對(duì)象臨時(shí)添加一個(gè)方法,并將對(duì)應(yīng)屬性值保存在內(nèi)部的Dictionary中,同時(shí)還會(huì)通知delegate、創(chuàng)建動(dòng)畫(huà)等,非常耗時(shí)
  2. UIView相關(guān)的顯示屬性,例如framebounds、transform等,實(shí)際上都是從CALayer映射來(lái)的,對(duì)其進(jìn)行調(diào)整時(shí),消耗的資源比一般屬性要大
  • 當(dāng)有大量對(duì)象釋放時(shí),也是非常耗時(shí)的,盡量挪到后臺(tái)線程去釋放
  • 盡量提前計(jì)算視圖布局預(yù)排版,例如計(jì)算cell的行高
  • Autolayout在簡(jiǎn)單頁(yè)面情況下們可以很好的提升開(kāi)發(fā)效率,但是對(duì)于復(fù)雜視圖而言,會(huì)產(chǎn)生嚴(yán)重的性能問(wèn)題,隨著視圖數(shù)量的增長(zhǎng),Autolayout帶來(lái)的CPU消耗是呈指數(shù)上升的,所以盡量使用代碼布局。如果不想手動(dòng)調(diào)整frame等,也可以借助三方庫(kù),例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
  • 文本處理的優(yōu)化:當(dāng)一個(gè)界面有大量文本時(shí),其行高的計(jì)算、繪制也是非常耗時(shí)的
  1. 如果對(duì)文本沒(méi)有特殊要求,可以使用UILabel內(nèi)部的實(shí)現(xiàn)方式,且需要放到子線程中進(jìn)行,避免阻塞主線程
    計(jì)算文本寬高:[NSAttributedString boundingRectWithSize:options:context:]
    文本繪制:[NSAttributedString drawWithRect:options:context:]
  2. 自定義文本控件,利用TextKit 或最底層的 CoreText 對(duì)文本異步繪制。并且CoreText 對(duì)象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整和繪制都需要計(jì)算一次)。CoreText直接使用了CoreGraphics占用內(nèi)存小,效率高
  • 圖片處理(解碼 + 繪制)
  1. 當(dāng)使用UIImageCGImageSource 的方法創(chuàng)建圖片時(shí),圖片的數(shù)據(jù)不會(huì)立即解碼,而是在設(shè)置時(shí)解碼(即圖片設(shè)置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的數(shù)據(jù)才進(jìn)行解碼)。這一步是無(wú)可避免的,且是發(fā)生在主線程中的。想要繞開(kāi)這個(gè)機(jī)制,常見(jiàn)的做法是在子線程中先將圖片繪制到CGBitmapContext,然后從Bitmap 直接創(chuàng)建圖片,例如SDWebImage三方框架中對(duì)圖片編解碼的處理。這就是Image的預(yù)解碼
  2. 當(dāng)使用CG開(kāi)頭的方法繪制圖像到畫(huà)布中,然后從畫(huà)布中創(chuàng)建圖片時(shí),可以將圖像的繪制子線程中進(jìn)行
  • 圖片優(yōu)化
  1. 盡量使用PNG圖片,不使用JPGE圖片
  2. 通過(guò)子線程預(yù)解碼,主線程渲染,即通過(guò)Bitmap創(chuàng)建圖片,在子線程賦值image
  3. 優(yōu)化圖片大小,盡量避免動(dòng)態(tài)縮放
  4. 盡量將多張圖合為一張進(jìn)行顯示
  • 盡量避免使用透明view,因?yàn)槭褂猛该鱲iew,會(huì)導(dǎo)致在GPU計(jì)算像素時(shí),會(huì)將透明view下層圖層的像素也計(jì)算進(jìn)來(lái)即顏色混合處理。
  • 按需加載,例如在TableView中滑動(dòng)時(shí)不加載圖片,使用默認(rèn)占位圖,而是在滑動(dòng)停止時(shí)加載
  • 少使用addViewcell動(dòng)態(tài)添加view

GPU層面優(yōu)化

相對(duì)于CPU而言,GPU主要是接收CPU提交的紋理+頂點(diǎn),經(jīng)過(guò)一系列transform,最終混合并渲染輸出到屏幕上。

  • 盡量減少在短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合為一張顯示,主要是因?yàn)楫?dāng)有大量圖片進(jìn)行顯示時(shí),無(wú)論是CPU的計(jì)算還是GPU的渲染,都是非常耗時(shí)的,很可能出現(xiàn)掉幀的情況
  • 盡量避免圖片的尺寸超過(guò)4096×4096,因?yàn)楫?dāng)圖片超過(guò)這個(gè)尺寸時(shí),會(huì)先由CPU進(jìn)行預(yù)處理,然后再提交給GPU處理,導(dǎo)致額外CPU資源消耗
  • 盡量減少視圖數(shù)量和層次,主要是因?yàn)橐晥D過(guò)多且重疊時(shí),GPU會(huì)將其混合,混合的過(guò)程也是非常耗時(shí)的
  • 盡量避免離屏渲染
  • 異步渲染,例如可以將cell中的所有控件、視圖合成一張圖片進(jìn)行顯示。參考Graver異步渲染框架
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容