iOS性能优化-卡顿

前言:本文旨在介绍iOS性能优化中有关页面卡顿的产生、优化以及监控。

一、屏幕的显示

图片加载到显示的过程:通常计算机在显示图片都是CPU与GPU协同合作完成一次渲染工作。在屏幕成像的过程中CPU与GPU起着至关重要的作用。

1、CPU(Central Processing Unit,中央处理器)

CPU负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码以及图像的绘制。

2、GPU(Graphics Processing Unit,图形处理器)

GPU负责纹理的渲染


二、卡顿原因

1、产生卡顿的原因:

  • CPU、GPU渲染流水线耗时过长。
  • 垂直同步Vsync + 双缓存区以掉帧为代价解决了图片撕裂问题。

2、图片撕裂原理:

页面撕裂:图片渲染是需要经过CPU和GPU的共同处理,但是如果CPU和GPU流水线耗时过长,可能出现的情况是,当扫描完当前帧缓存区的图片之后,下一帧还未渲染完成,这时候显示器会从帧缓存区继续扫描旧的图片,因为扫描过程是一行一行扫描的,而如果在旧的图片扫到一部分时发现下一帧新图片来了,然后显示器就会立即扫描下一帧的图片,此时就会出现上半部图片是上一帧的图片,下半部是新的一帧,导致图片看起来是撕裂的,也就是出现了图片撕裂的问题。

3、卡顿解决方法

  • 使用多缓存区,以空间换取时间,最大限制利用CPU和GPU的闲置时间。
  • 提高CPU、GPU的性能和使用效率。

三、CPU层面的卡顿优化

  • 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
  • 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
  • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
  • Autolayout会比直接设置frame消耗更多的CPU资源
  • 图片的size最好刚好跟UIImageView的size保持一致
  • 控制一下线程的最大并发数量
  • 尽量把耗时的操作放到子线程,比如:文本处理(尺寸计算、绘制)、图片处理(解码、绘制)

四、GPU层面的卡顿优化

  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
  • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
  • 尽量减少视图数量和层次
  • 减少透明的视图(alpha<1),不透明的就设置opaque为YES
  • 尽量避免出现离屏渲染

五、离屏渲染

1、在OpenGL中,GPU有2种渲染方式
  • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作。
  • Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
2、离屏渲染消耗性能的原因
  • 需要创建新的缓冲区。
  • 在触发离屏渲染之后会增加GPU的工作量,导致增加了CPU和GPU的总耗时量,有可能会导致UI的卡顿和掉帧。
  • 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。
3、哪些操作会触发离屏渲染?
  • 光栅化,layer.shouldRasterize = YES
    (光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上
    的过程,光栅化的本质是坐标变换、几何离散化)
  • 遮罩,layer.mask
  • 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0
  • 考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
  • 阴影,layer.shadowXXX
  • 如果设置了layer.shadowPath就不会产生离屏渲染

六、RunLoop监控卡顿原理

  • 创建一个Observer来监听RunLoop,在RunLoop的 kCFRunLoopBeforeTimers、kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting 获取一个开始时间值start;
  • 开启一个子线程,在while循环中监控RunLoop的状态,并获取当前时间作为结束时间end。
  • 拿end减去start 获取一个时间段 和一个阀值(long usec_anrShortTime = 2s)做比较,若大于则说明存在卡顿。则获取卡顿信息并上传。
相关代码参考(类名 APMANRMonitorPlugin)
// 创建一个Observer来监听RunLoop
- (void)addRunLoopObserver {
    NSRunLoop *curRunLoop = [NSRunLoop mainRunLoop];
    
    //1.注册default RunloopMode
    //监听开始的observer
    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
    CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &RunLoopBeginCallback, &context);
    CFRetain(beginObserver);
    //监听结束的observer
    CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &RunLoopEndCallback, &context);
    CFRetain(endObserver);
    //监听监听
    CFRunLoopAddObserver([curRunLoop getCFRunLoop], beginObserver, kCFRunLoopCommonModes);
    CFRunLoopAddObserver([curRunLoop getCFRunLoop], endObserver, kCFRunLoopCommonModes);
    //保留observer
    runLoopBeginObserver = beginObserver;
    runLoopEndObserver = endObserver;
}
// RunLoop的状态
//最高优先级
void RunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    g_RunloopActivity = activity;
    g_blockArised = NO;
  
    switch (activity) {
        case kCFRunLoopEntry:
            g_Run = YES;
            break;
        case kCFRunLoopBeforeTimers:
            if (g_Run == NO) {
                gettimeofday(&g_TimevalMark, NULL);
            }
            g_Run = YES;
            break;
        case kCFRunLoopBeforeSources:
            if (g_Run == NO) {
                gettimeofday(&g_TimevalMark, NULL);
            }
            g_Run = YES;
            break;
        case kCFRunLoopAfterWaiting:
            if (g_Run == NO) {
                gettimeofday(&g_TimevalMark, NULL);
            }
            g_Run = YES;
            break;
        case kCFRunLoopAllActivities:
            break;
        default:
            break;
    }
}
// 开启一个子线程,在while循环中监控RunLoop的状态
- (void)addMonitorThread {
    monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMonitoringProcess) object:nil];
    [monitorThread start];
}
- (void)threadMonitoringProcess {
    //保存堆栈信息
    g_BlockStartTimeval = {0,0};
    APMDumpType reportDumpType = APMDumpTypeNormal;
    
    // 卡死卡顿信息打包上报
    [self packageBlockToKillReport];
    
    //重置监控状态
    [self resetANRMonitorStatus];
    
    while (YES) {
        @autoreleasepool {
            //退出策略
            if (_bStop) { break; }
            
            APMDumpType dumpType = [self checkDumpType];
                        
            if (g_Run == YES && dumpType != APMDumpTypeNormal) {
                if (g_BlockReportPath.length <= 0) {
                    g_BlockReportPath = [APMANRMonitorTool dumpReportWithType:dumpType];
                    g_BlockStartTimeval = g_TimevalMark;
                    g_blockArised = YES;
                }
                reportDumpType = dumpType;
            }else{
                if (g_blockArised == NO && g_BlockReportPath.length > 0 && VALID_TIMEVAL(g_BlockStartTimeval)) {
                    //保存当前堆栈信息并上传
                    struct timeval cur;
                    gettimeofday(&cur, NULL);
                    [self recordBlockReportWithStartTimeval:g_BlockStartTimeval
                                                 endTimeval:cur
                                                   dumpType:reportDumpType];
                    //重置监控状态
                    [self resetANRMonitorStatus];
                }
            }
            
            //睡一会
            usleep(_defaultCheckPeriodTime);
            //退出策略
            if (_bStop) { break; }
        }
    }
}

七、Runloop监听卡顿为什么是beforesource,afterwaiting这两个?

 ----------BeforeTimers
 ----------BeforeSources
 ++++++++++ 处理Block
 ++++++++++ 处理Source0(非port),手动处理
 ++++++++++ 如果Source0处理成功,则处理Block
 ==== 如果有Source1事件,直接跳转到HandleMSG

 ----------BeforeWaitting
 ==== 
  唤醒条件【即有任务待处理】:
  runloop时间超时,timer触发,Port有消息出发, 被手动唤醒
 ====
 ----------AfterWaiting

 ==== HandleMSG【被唤醒后需要处理的任务】
 ++++++++++++ 处理计时器事件
 ++++++++++++ 处理主异步到主队列的消息
 ++++++++++++ 处理source1事件
 ++++++++++++ 处理block事件

从上面任务执行流程可以看出:其主要执行任务的时机为:BeforeSources和AfterWaiting。

  • 对于任务比较多,导致CPU+GPU处理不完的卡顿情形可以通过监听这两个通知执行时间是否超时来达到卡顿监控。
  • 对于屏幕静止是卡顿,由于一直是处于BeforeWaiting状态,我们可以通过ping主线程的方式验证主线程是否卡顿。
  • 总结:卡顿监控就是利用Runloop在发出BeforeWaiting通知后触发UI刷新的特点,再依靠任务执行是否超出阈值来判断卡顿的。

以上是有关卡顿的介绍,欢迎补充和指正。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容

  • 一. CPU和GPU的作用 在屏幕成像的过程中,CPU和GPU起着至关重要的作用: CPU(Central Pro...
    Imkata阅读 1,417评论 0 3
  • 1 卡顿产生的原因及优化 产生卡顿是由于屏幕的成像显示导致,而屏幕画面的显示离不开手机的CPU和GPU; CPU:...
    YYFast阅读 773评论 0 4
  • 最近一段时间经常遇到关于iOS性能优化、卡顿优化的问题,今天在这里总结一下。一般困扰我们的性能优化的问题一般是你在...
    OnlyFunny阅读 2,942评论 0 9
  • 卡顿原因 图像的显示可以简单理解成先经过CPU的计算/排版/编解码等操作,然后交由GPU去完成渲染放入缓冲中,当视...
    肥猫记阅读 3,681评论 0 8
  • 成像过程: 在iOS中是双缓冲机制,有前帧缓存、后帧缓存 成像原理: 发出垂直同步信号,告诉屏幕即将显示一帧数据;...
    南城同學阅读 398评论 0 1