iOS 页面的卡顿的原因以及如何解决. 如何优化app的启动速度

卡顿原因

1.死锁: 主线程拿到锁A, 需要获取锁B, 而同时子线程拿了锁B, 需要锁A, 这时主线程等待锁B的释放, 子线程等待锁A的释放, 相互等待.

2.抢锁: 主线程需要访问DB, 而这时某个子线程往DB插入数据. 通常抢锁的体验就是卡顿一阵子就恢复了.

3.主线程大量IO(文件操作): 主线程为了方便直接写入大量数据, 导致页面卡顿.

4.主线程大量计算: 程序中的算法不合理, 大量循环等操作, 导致主线程某个函数占用大量CPU.

5.大量的UI绘制: 复杂的UI, 图文混排等, 带来大量的UI绘制.

卡顿的定位

1.死锁一般会伴随Crash, 我们可以通过Crash日志进行分析.

2.抢锁的问题不太好办, 我们能将锁等待的时间打印出来, 但我们还需要知道是谁占用了锁, 可以检测Runloop的执行,观察耗时.

3.大量的IO可以在函数开始结束打点, 将函数占用时间打到日志中.

4.线程大量计算同理也可以将耗时记录到日志中.

5.大量UI绘制一般是难免的, APP中总会有复杂页面的绘制, 我们可以用AsnycDisplayKit等框架进行预排版,异步绘制,图片解码等.

如何判断主线程是否发生了卡顿?

FPS降低

CPU占用率很高

主线程Runloop执行了很久

FPS能够兼容后面两个特征, 但在实际操作过程中发现FPS不好衡量抖动比较大. 对于抢锁或者大量IO的情况, 光靠CPU是不行的, 所以一般检测判断, CPU占用是否超过了100%, 主线程Runloop执行是够超过阈值.

监控FPS(意思是每秒传输帧数,FPS值越低就越卡顿)

所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。iOS系统中正常的屏幕刷新率为60Hz(60次每秒)。

通过CADisplayLink实现FPS监控,CADisplayLink可以以屏幕刷新的频率调用指定selector,也就是说每次屏幕刷新的时候就调用selector,那么只要在selector方法里面统计每秒这个方法执行的次数,通过次数/时间就可以得出当前屏幕的刷新率了。

可通过YYFPSLabel或者KMCGeigerCounter进行监控,但是前者比较轻量级。(YYFPSLabel)

通过RunLoop监控检查卡顿

通过RunLoop知道主线程上都调用了哪些方法,通过监听 nsrunloop 的状态,知道调用方法是否执行时间过长,从而判断出是否卡顿。

RunLoop 原理来监控卡顿的话,就是要关注这两个阶段。RunLoop 在进入睡眠之前和唤醒后的两个 loop状态定义的值分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting,也就是要触发 Sources回调和接收mach_port消息两个状态。

一个runloop用来管理一个线程 也就是这个线程持续了多久.

1.需要创建一个CFRunLoopObserverContext观察者,然后将观察者runLoopObserver添加到主线程 RunLoop的common模式下观察

    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};

    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);

    2.创建一个持续的子线程专门用来监控主线程的RunLoop状态

// 创建子线程监控

dispatch_async(dispatch_get_global_queue(0, 0), ^{

    // 子线程开启一个持续的 loop 用来进行监控

    while (YES) {

        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));

        if (semaphoreWait != 0) {

            if (!runLoopObserver) {

                timeoutCount = 0;  // 超时次数

                dispatchSemaphore = 0; // dispatch_semaphore_t 信号量

                runLoopActivity = 0;  // CFRunLoopActivity RunLoop原始状态kCFRunLoopEntry

                return;

            }

            //kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting这两个状态能够检测到是否卡顿

            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {

                // 将堆栈信息上报服务器的代码放到这里,包括timeoutCount操作

            }

        }

        timeoutCount = 0;

    }

});

NSEC_PER_SEC代表的是触发卡顿的时间阈值,单位是秒。可以看到,我们把这个阀值设置成了 3 秒。

获取卡顿的方法堆栈信息

方法1:直接调用系统函数方法的主要思路是:用 signal 进行错误信息的获取

性能消耗小。但是,它只能够获取简单的信息,也没有办法配合 dSYM 来获取具体是哪行代码出了问题,而且能够获取的信息类型也有限。适用于观察大盘统计卡顿情况、而不是想要找到卡顿原因的场景。

方法2:利用PLCrashReporter

能够定位到问题代码的具体位置,而且性能消耗也不大

NSData *lagData = [[[PLCrashReporter alloc]

                                          initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];

PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];

NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];

NSLog(@"lag happen, detail below: \n %@",lagReportString);

明确优化方向

在进行优化之前,我们需要明确优化的方向。是什么影响了我们的APP的启动时间?切忌挖空心思的研究优化main()函数调用之前的占用时间,反而忽略了-applicationDidFinishLaunching:withOptions:函数之后那一堆堆臃肿的网络请求以及业务流程。

我们这里只考虑冷启动的优化,因为冷启动包括了热启动,冷启动需要做额外的初始化工作,所以相较而言更慢,导致需要更长的启动等待时间。所以我们先来看看-applicationDidFinishLaunching:withOptions:函数之后,我们的APP都做了哪些事情。

首先会初始化window,加载tabbar,加载首页controller以及数据,可能我们还有一个loading广告页,还有各种各样的业务需求,网络请求。所以这些都是需要去排查的地方,可以尝试通过添加打印时间戳的方式,来测量每个阶段的耗时情况。我们根据排查结果来明确造成启动缓慢的原因。

我们再简单看看main()函数调用之前都发生了什么。


动态链接器 dyld开始将程序依赖的动态链接库递归加载进内存(有缓存机制,第二次启动时会快一些),交由ImageLoader读取所有的类、方法等各种符号,加载完毕后dyld通知runtime调用map_images,遍历所有Class,按继承层次依次调用Class的+load方法和其Category的+load方法。待所有初始化工作结束后,dyld调用main()函数。

我们可以通过在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1,在控制台看到main()函数之前的启动时间。

分解优化目标 分步达到优化目的

    如果有很多业务模块在启动时,都在异步的抢占主线程渲染UI,这就会发生阻塞的现象,如果手机性能较好则看不出差异,如果手机性能较差,会卡的想骂人。

    如果启动流程依赖网络请求回来才能继续,那么需要考虑网络极差情况下的启动速度。

    如果APP有loading广告页并且对分辨率的要求较高,请尝试做缓存吧。

    请尽量精简启动流程的各个阶段和逻辑。

    对于tabBarController以及主页面Controller中的viewDidLoad和viewWillAppear方法中尽量少做事情。

    如果项目历史悠久,请尽快排查清理项目中未使用到的类库以及Framework。

    如果时间允许,请删减合并一些OC类,删减没有用到或者可以不用的静态变量、方法等。

      尽量轻量化+load方法中的内容,可延迟到+initialize中。

  1.从启动 main()之前

2.main()之后 到didFinishLaunchingWithOptions 之前

3.以及didFinishLaunchingWithOptions 作用域结束, 首屏渲染结束

这里我说明一下:

App 启动 分为<冷启动>和<热启动>

冷启动: 是App没有打开.没有做任何准备工作没有进行应用的加载以及构建的时候启动到首屏渲染完成的时间.(我们的App 在这个阶段分为两种首次安装打开以及App不在后台挂起的情况下kill 之后重新打开, 因为做了不同配置导致耗时不同).

热启动: 是你的App已经运行但是是在后台挂起的状态.热启动的时候的时间38ms 个人觉得我们主要优化的应该是冷启动的启动时间

冷启动的优化主要从以下考虑:a. dyld    b. runtime    c. main  (下方图片 从左到右)


1.dyld (动态连接器) :


内核加载主程序,dyld装载Mach-O文件 管理iamges 加载动态库(以后对dyld 在进行深入了解)

装载App可执行文件,递归(动态库依赖于其他动态库)加载所有依赖的动态库;

dyld 把可执行文件以及动态库都装载完之后,通知Runtime进行下一步处理;

2.Runtime (初始化OC结构) :

(这个过程可以将可执行文件和动态库中的所有的符号Class, Protocol, Selecor, IMP等按格式成功加载到内存      中且Runtime对其进行管理,面向对象的相关内容都是Runtime 做的)

1.dyld 方面  :

减少Objc类, 分类的数量, 减少Selector数量 (定期清理不必要的类, 分类). 在dyld 加载Mach-O文件的时候会对类以及分类 进行加载所以减少这些东西可以减少开销

通过分析可以从减少动态库的个数, 合并一些动态库 (定期清理不必要的动态库). 在dyld加载动态库的时候Load dylibs的时候,会分析应用依赖的dylibs.一般情况下iOS 会加载100-400个dylibs,大部分是系统库,针对系统级别的动态库都是经过系统高度优化的(之前我学习底层班的时候进行过学习).而我们自己集成到App的动态库是消耗加载时间的,所以要尽量不要使用内嵌的dylib 这种加载的性能开销最大

所以尽量把多个内嵌dylib 合并成一个来加载或者用static archive(静态库)

减少C++虚函数数量 (多维护虚表)

Swift 尽量使用struct(不用类)

对于dyld 的优化具体方向在dyld 的文章下进行分析(篇幅太长 涉及到dyld 中对缓存Rebase 以及Binding的操作)

3.main 方面  :

在不影响用户体验的前提下, 尽可能将一些操作延迟, 不要全部放到finishLaunching:方法中.重点说明一下几点:

流程减少, 懒加载 放后台初始化 延时初始化 不用的代码删掉;

优化逻辑代码 没用的逻辑该删删

多线程初始化

使用代码不用xib 或者storyboard 进行UI 框架的搭建(尤其是主框架, xib 和 storyboard 也要解析成代码 浪费时间). 对于main部分的优化我项目中的初始化大部分都不是必须的包括token获取等所以基本都是延迟处理的.当然部分也可以交给子线程去做更好.(除了设计到主线程部分的内容或者用到UIKit 的部分). 对于多线程初始化的部分我建议大家仔细读一下这位前辈的探究很有意义对比了各个系统之间的 各个机型之间的 各种处理器之间的不同表现: iOS 启动优化

你可能感兴趣的:(iOS 页面的卡顿的原因以及如何解决. 如何优化app的启动速度)