界面優(yōu)化

卡頓的原理

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

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

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

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

屏幕卡頓原因

VSync信號到來后,系統(tǒng)圖形服務會通過CADisplayLink等機制通知App。App主線程開始在CPU中計算顯示內(nèi)容,隨后CPU 會將計算好的內(nèi)容提交到GPU,由GPU進行變換、合成、渲染。隨后GPU會把渲染結果提交到幀緩沖區(qū),等待下一次VSync信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個VSync時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內(nèi)容不變

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

掉幀圖示

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

卡頓的檢測

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

  • FPS監(jiān)控:為了保持流程的UI交互,App的刷新頻率應該保持在60fps左右,其原因是iOS設備默認的刷新頻率是60次/秒,而1次刷新(即VSync信號發(fā)出)的間隔是1000ms/60 = 16.67ms。如果在16.67ms內(nèi)沒有準備好下一幀數(shù)據(jù),就會產(chǎn)生卡頓
  • 主線程卡頓監(jiān)控:通過子線程監(jiān)測主線程的RunLoop,判斷兩個狀態(tài)(kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting)之間的耗時是否達到一定閾值
FPS監(jiān)控
  • 方案一:參考YYKit中的YYFPSLabel通過CADisplayLink實現(xiàn)。借助link的時間差,來計算一次刷新所需的時間,然后通過刷新次數(shù) / 時間差得到刷新頻次,并判斷是否符合范圍,通過顯示不同的文字顏色來表示卡頓嚴重程度。
<!-- 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)控
  • 方案二:通過RunLoop來監(jiān)控,因為卡頓的是事務,而事務是交由主線程RunLoop處理的。
    實現(xiàn)原理:檢測主線程每次執(zhí)行消息循環(huán)的時間,當這個時間大于規(guī)定的閾值時,就記為發(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ā)送信號
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

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

- (void)startMonitor{
    // 創(chuàng)建信號
    _semaphore = dispatch_semaphore_create(0);
    // 在子線程監(jiān)控時長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 超時時間是 1 秒,沒有等到信號量,st 就不等于 0, RunLoop 所有的任務
            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ù)來 避免大規(guī)模打印!
                    NSLog(@"檢測到超過兩次連續(xù)卡頓");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}

@end

使用方式:

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

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

開發(fā)圖文混排頁面時,滑動頁面需要不停的計算和渲染,比如計算cell高度。案例代碼如下

// 頁面數(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;
}

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

// 優(yōu)化后代碼
- (void)loadData{
    //外面的異步線程:網(wǎ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行高,頁面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;

    //文本信息高度計算
    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中把所有頁面相關邏輯提前處理好,轉(zhuǎn)成layoutModel如上面所示。

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

比如頁面加載一張圖片,其加載流程如下

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

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

我們在加載圖片的時候,一般使用SDWebImage,下面探索其原理......

  • 查看sd_setImageWithURL方法
image.png
  • image圖片來源于網(wǎng)絡請求didComplete
image.png
  • 這里拿到的是二進制文件imageData
image.png
  • 對二進制文件進行解碼
image.png

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

圖片為什么需要預解碼

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

添加符號斷點
查看堆棧信息

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

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

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

蘋果在底層提供了一圖形編解碼插件,比如原生音視頻框架AVFoundation、FFmpeg。其中FFmpeg中最好的點就是對視頻的編解碼過程。

異步渲染

按需加載

只有需要了才去加載。例如TableView滑動時,滑動的越快也就意味著計算、渲染的頻率越高。這樣就有可能導致頁面卡頓......

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

關于UIViewLayer之間的關系?

  • UIView主要是用于頁面交互,比如頁面點擊等等
  • Layer主要用于頁面的渲染

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

頁面渲染原理

渲染的過程是非常耗時的,這個過程稱之為事物。事務里面有如下環(huán)節(jié)

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

CPU層面的優(yōu)化

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

GPU層面優(yōu)化

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

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

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

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