情义无价三哥 發表於 2021-12-17 10:28:00

iOS开发--APP性能检测方案汇总

<div>
<h2>1 . CPU 占用率</h2>
<p>CPU作为手机的中央处理器,可以说是手机最关键的组成部分,所有应用程序都需要它来调度运行,资源有限。所以当我们的APP因设计不当,使 CPU 持续以高负载运行,将会出现APP卡顿、手机发热发烫、电量消耗过快等等严重影响用户体验的现象。</p>
<p>因此我们对应用在<code>CPU</code>中占用率的监控,将变得尤为重要。那么我们应该如何来获取CPU的占有率呢?!</p>
<p>我们都知道,我们的APP在运行的时候,会对应一个<code>Mach Task</code>,而Task下可能有多条线程同时执行任务,每个线程都是作为利用CPU的基本单位。所以我们可以通过获取当前<code>Mach Task</code>下,所有线程占用 CPU 的情况,来计算APP的 CPU 占用率。</p>
<p>在《OS X and iOS Kernel Programming》是这样描述 Mach task 的:</p>
<blockquote>
<p>任务(task)是一种容器(container)对象,虚拟内存空间和其他资源都是通过这个容器对象管理的,这些资源包括设备和其他句柄。严格地说,Mach 的任务并不是其他操作系统中所谓的进程,因为 Mach 作为一个微内核的操作系统,并没有提供“进程”的逻辑,而只是提供了最基本的实现。不过在 BSD 的模型中,这两个概念有1:1的简单映射,每一个 BSD 进程(也就是 OS X 进程)都在底层关联了一个 Mach 任务对象。</p>
</blockquote>
<div class="image-package">
<div class="image-container">
<div class="image-container-fill"><img src="https://img2020.cnblogs.com/blog/1602144/202112/1602144-20211217102452767-1134758308.webp">
<p>&nbsp;</p>
<p>&nbsp;</p>
&nbsp;</div>
<div class="image-view" data-width="477" data-height="543"><img src="//upload-images.jianshu.io/upload_images/877439-18e562c2a9f7612d.png?imageMogr2/auto-orient/strip|imageView2/2/w/477/format/webp"></div>
</div>
<div class="image-caption">Mac OS X 中进程子系统组成的概念图</div>
</div>
<blockquote>
<p>iOS 是基于 <code>Apple Darwin</code> 内核,由<code>kernel</code>、<code>XNU</code>和<code>Runtime</code> 组成,而<code>XNU</code> 是<code>Darwin</code> 的内核,它是“X is not UNIX”的缩写,是一个混合内核,由 Mach 微内核和 BSD 组成。Mach 内核是轻量级的平台,只能完成操作系统最基本的职责,比如:进程和线程、虚拟内存管理、任务调度、进程通信和消息传递机制等。其他的工作,例如文件操作和设备访问,都由 BSD 层实现。</p>
</blockquote>
<p>iOS 的线程技术与Mac OS X类似,也是基于 Mach 线程技术实现的,在 Mach 层中<code>thread_basic_info</code> 结构体封装了单个线程的基本信息:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c"><span class="token keyword">struct <span class="token class-name">thread_basic_info <span class="token punctuation">{
    time_value_tuser_time<span class="token punctuation">;      <span class="token comment">/* user run time */
    time_value_tsystem_time<span class="token punctuation">;    <span class="token comment">/* system run time */
    integer_t    cpu_usage<span class="token punctuation">;       <span class="token comment">/* scaled cpu usage percentage */
    policy_t   policy<span class="token punctuation">;          <span class="token comment">/* scheduling policy in effect */
    integer_t    run_state<span class="token punctuation">;       <span class="token comment">/* run state (see below) */
    integer_t    flags<span class="token punctuation">;         <span class="token comment">/* various flags (see below) */
    integer_t    suspend_count<span class="token punctuation">;   <span class="token comment">/* suspend count for thread */
    integer_t    sleep_time<span class="token punctuation">;      <span class="token comment">/* number of seconds that threadhas been sleeping */
<span class="token punctuation">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>一个<code>Mach Task</code>包含它的线程列表。内核提供了<code>task_threads</code> API 调用获取指定 task 的线程列表,然后可以通过<code>thread_info</code> API调用来查询指定线程的信息,在 thread_act.h 中有相关定义。</p>
<p><code>task_threads</code> 将<code>target_task</code> 任务中的所有线程保存在<code>act_list</code>数组中,act_listCnt表示线程个数:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c">kern_return_t task_threads
<span class="token punctuation">(
    task_t target_task<span class="token punctuation">,
    thread_act_array_t <span class="token operator">*act_list<span class="token punctuation">,
    mach_msg_type_number_t <span class="token operator">*act_listCnt
<span class="token punctuation">)<span class="token punctuation">;
</span></span></span></span></span></span></span></code></pre>
</div>
<p><code>thread_info</code>结构如下:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c">kern_return_t thread_info
<span class="token punctuation">(
    thread_act_t target_act<span class="token punctuation">,
    thread_flavor_t flavor<span class="token punctuation">,<span class="token comment">// 传入不同的宏定义获取不同的线程信息
    thread_info_t thread_info_out<span class="token punctuation">,<span class="token comment">// 查询到的线程信息
    mach_msg_type_number_t <span class="token operator">*thread_info_outCnt<span class="token comment">// 信息的大小
<span class="token punctuation">)<span class="token punctuation">;
</span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>所以我们如下来获取CPU的占有率:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-object-c"><code class="Object-Clanguage-object-c">#import "LSLCpuUsage.h"
#import &lt;mach/task.h&gt;
#import &lt;mach/vm_map.h&gt;
#import &lt;mach/mach_init.h&gt;
#import &lt;mach/thread_act.h&gt;
#import &lt;mach/thread_info.h&gt;

@implementation LSLCpuUsage

+ (double)getCpuUsage {
    kern_return_t         kr;
    thread_array_t          threadList;         // 保存当前Mach task的线程列表
    mach_msg_type_number_tthreadCount;      // 保存当前Mach task的线程个数
    thread_info_data_t      threadInfo;         // 保存单个线程的信息列表
    mach_msg_type_number_tthreadInfoCount;    // 保存当前线程的信息列表大小
    thread_basic_info_t   threadBasicInfo;    // 线程的基本信息
   
    // 通过“task_threads”API调用获取指定 task 的线程列表
    //mach_task_self_,表示获取当前的 Mach task
    kr = task_threads(mach_task_self(), &amp;threadList, &amp;threadCount);
    if (kr != KERN_SUCCESS) {
      return -1;
    }
    double cpuUsage = 0;
    for (int i = 0; i &lt; threadCount; i++) {
      threadInfoCount = THREAD_INFO_MAX;
      // 通过“thread_info”API调用来查询指定线程的信息
      //flavor参数传的是THREAD_BASIC_INFO,使用这个类型会返回线程的基本信息,
      //定义在 thread_basic_info_t 结构体,包含了用户和系统的运行时间、运行状态和调度优先级等
      kr = thread_info(threadList, THREAD_BASIC_INFO, (thread_info_t)threadInfo, &amp;threadInfoCount);
      if (kr != KERN_SUCCESS) {
            return -1;
      }
      
      threadBasicInfo = (thread_basic_info_t)threadInfo;
      if (!(threadBasicInfo-&gt;flags &amp; TH_FLAGS_IDLE)) {
            cpuUsage += threadBasicInfo-&gt;cpu_usage;
      }
    }
   
    // 回收内存,防止内存泄漏
    vm_deallocate(mach_task_self(), (vm_offset_t)threadList, threadCount * sizeof(thread_t));

    return cpuUsage / (double)TH_USAGE_SCALE * 100.0;
}
@end
</code></pre>
</div>
<hr>
<h2>2. 内存</h2>
<p>虽然现在的手机内存越来越大,但毕竟是有限的,如果因为我们的应用设计不当造成内存过高,可能面临被系统“干掉”的风险,这对用户来说是毁灭性的体验。</p>
<p>Mach task 的内存使用信息存放在<code>mach_task_basic_info</code>结构体中 ,其中<code>resident_size</code> 为应用使用的物理内存大小,<code>virtual_size</code>为虚拟内存大小,在<code>task_info.h</code>中:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c"><span class="token macro property">#<span class="token directive keyword">define MACH_TASK_BASIC_INFO   20         <span class="token comment">/* always 64-bit basic info */
<span class="token keyword">struct <span class="token class-name">mach_task_basic_info <span class="token punctuation">{
      mach_vm_size_tvirtual_size<span class="token punctuation">;       <span class="token comment">/* virtual memory size (bytes) */
      mach_vm_size_tresident_size<span class="token punctuation">;      <span class="token comment">/* resident memory size (bytes) */
      mach_vm_size_tresident_size_max<span class="token punctuation">;<span class="token comment">/* maximum resident memory size (bytes) */
      time_value_t    user_time<span class="token punctuation">;          <span class="token comment">/* total user run time for
                                             terminated threads */
      time_value_t    system_time<span class="token punctuation">;      <span class="token comment">/* total system run time for
                                             terminated threads */
      policy_t      policy<span class="token punctuation">;             <span class="token comment">/* default policy for new threads */
      integer_t       suspend_count<span class="token punctuation">;      <span class="token comment">/* suspend count for task */
<span class="token punctuation">}<span class="token punctuation">;
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>获取方式是通过<code>task_info</code>API 根据指定的 flavor 类型,返回 target_task 的信息,在<code>task.h</code>中:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c">kern_return_t task_info
<span class="token punctuation">(
    task_name_t target_task<span class="token punctuation">,
    task_flavor_t flavor<span class="token punctuation">,
    task_info_t task_info_out<span class="token punctuation">,
    mach_msg_type_number_t <span class="token operator">*task_info_outCnt
<span class="token punctuation">)<span class="token punctuation">;
</span></span></span></span></span></span></span></code></pre>
</div>
<p>笔者尝试过使用如下方式获取内存情况,基本和腾讯的GT的相近,但是和Xcode和Instruments的值有较大差距:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c"><span class="token comment">// 获取当前应用的内存占用情况,和Xcode数值相差较大
<span class="token operator">+ <span class="token punctuation">(<span class="token keyword">double<span class="token punctuation">)getResidentMemory <span class="token punctuation">{
    <span class="token keyword">struct <span class="token class-name">mach_task_basic_info info<span class="token punctuation">;
    mach_msg_type_number_t count <span class="token operator">= MACH_TASK_BASIC_INFO_COUNT<span class="token punctuation">;
    <span class="token keyword">if <span class="token punctuation">(<span class="token function">task_info<span class="token punctuation">(<span class="token function">mach_task_self<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">, MACH_TASK_BASIC_INFO<span class="token punctuation">, <span class="token punctuation">(task_info_t<span class="token punctuation">)<span class="token operator">&amp;info<span class="token punctuation">, <span class="token operator">&amp;count<span class="token punctuation">) <span class="token operator">== KERN_SUCCESS<span class="token punctuation">) <span class="token punctuation">{
      <span class="token keyword">return info<span class="token punctuation">.resident_size <span class="token operator">/ <span class="token punctuation">(<span class="token number">1024 <span class="token operator">* <span class="token number">1024<span class="token punctuation">)<span class="token punctuation">;
    <span class="token punctuation">} <span class="token keyword">else <span class="token punctuation">{
      <span class="token keyword">return <span class="token operator">-<span class="token number">1.0<span class="token punctuation">;
    <span class="token punctuation">}
<span class="token punctuation">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>后来看了一篇博主讨论了这个问题,说使用<code>phys_footprint</code>才是正解,博客地址。亲测,基本和Xcode的数值相近。</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c"><span class="token comment">// 获取当前应用的内存占用情况,和Xcode数值相近
<span class="token operator">+ <span class="token punctuation">(<span class="token keyword">double<span class="token punctuation">)getMemoryUsage <span class="token punctuation">{
    task_vm_info_data_t vmInfo<span class="token punctuation">;
    mach_msg_type_number_t count <span class="token operator">= TASK_VM_INFO_COUNT<span class="token punctuation">;
    <span class="token keyword">if<span class="token punctuation">(<span class="token function">task_info<span class="token punctuation">(<span class="token function">mach_task_self<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">, TASK_VM_INFO<span class="token punctuation">, <span class="token punctuation">(task_info_t<span class="token punctuation">) <span class="token operator">&amp;vmInfo<span class="token punctuation">, <span class="token operator">&amp;count<span class="token punctuation">) <span class="token operator">== KERN_SUCCESS<span class="token punctuation">) <span class="token punctuation">{
      <span class="token keyword">return <span class="token punctuation">(<span class="token keyword">double<span class="token punctuation">)vmInfo<span class="token punctuation">.phys_footprint <span class="token operator">/ <span class="token punctuation">(<span class="token number">1024 <span class="token operator">* <span class="token number">1024<span class="token punctuation">)<span class="token punctuation">;
    <span class="token punctuation">} <span class="token keyword">else <span class="token punctuation">{
      <span class="token keyword">return <span class="token operator">-<span class="token number">1.0<span class="token punctuation">;
    <span class="token punctuation">}
<span class="token punctuation">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>博主文中提到:关于 <code>phys_footprint</code> 的定义可以在 XNU 源码中,找到 <code>osfmk/kern/task.c</code> 里对于 <code>phys_footprint</code> 的注释,博主认为注释里提到的公式计算的应该才是应用实际使用的物理内存。</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-c"><code class="Clanguage-c"><span class="token comment">/*
* phys_footprint
*   Physical footprint: This is the sum of:
*   + (internal - alternate_accounting)
*   + (internal_compressed - alternate_accounting_compressed)
*   + iokit_mapped
*   + purgeable_nonvolatile
*   + purgeable_nonvolatile_compressed
*   + page_table
*
* internal
*   The task's anonymous memory, which on iOS is always resident.
*
* internal_compressed
*   Amount of this task's internal memory which is held by the compressor.
*   Such memory is no longer actually resident for the task ,
*   and could be either decompressed back into memory, or paged out to storage, depending
*   on our implementation.
*
* iokit_mapped
*   IOKit mappings: The total size of all IOKit mappings in this task, regardless of
   clean/dirty or internal/external state].
*
* alternate_accounting
*   The number of internal dirty pages which are part of IOKit mappings. By definition, these pages
*   are counted in both internal *and* iokit_mapped, so we must subtract them from the total to avoid
*   double counting.
*/
</span></code></pre>
</div>
<p><strong>当然我也是赞同这点的&gt;.&lt;</strong>。</p>
<hr>
<h2>3. 启动时间</h2>
<p>APP的启动时间,直接影响用户对你的APP的第一体验和判断。如果启动时间过长,不单单体验直线下降,而且可能会激发苹果的watch dog机制kill掉你的APP,那就悲剧了,用户会觉得APP怎么一启动就卡死然后崩溃了,不能用,然后长按APP点击删除键。(Xcode在debug模式下是没有开启watch dog的,所以我们一定要连接真机测试我们的APP)</p>
<p>在衡量APP的启动时间之前我们先了解下,APP的启动流程:</p>
<div class="image-package">
<div class="image-container">
<div class="image-container-fill">&nbsp;<img src="https://img2020.cnblogs.com/blog/1602144/202112/1602144-20211217102602600-868459994.webp">
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
<div class="image-view" data-width="629" data-height="730"><img src="//upload-images.jianshu.io/upload_images/877439-77c0062f78b28b87.png?imageMogr2/auto-orient/strip|imageView2/2/w/629/format/webp"></div>
</div>
<div class="image-caption">APP启动过程</div>
</div>
<p>APP的启动可以分为两个阶段,即<code>main()</code>执行之前和<code>main()</code>执行之后。总结如下:</p>
<blockquote>
<p>t(App 总启动时间) = t1( <code>main()</code>之前的加载时间 ) + t2( <code>main()</code>之后的加载时间 )。</p>
<ul>
<li>t1 = 系统的 dylib (动态链接库)和 App 可执行文件的加载时间;</li>
<li>t2 = <code>main()</code>函数执行之后到<code>AppDelegate</code>类中的<code>applicationDidFinishLaunching:withOptions:</code>方法执行结束前这段时间。</li>
</ul>
</blockquote>
<p>所以我们对APP启动时间的获取和优化都是从这两个阶段着手,下面先看看<code>main()</code>函数执行之前如何获取启动时间。</p>
<h3>衡量main()函数执行之前的耗时</h3>
<p>对于衡量main()之前也就是time1的耗时,苹果官方提供了一种方法,即在真机调试的时候,勾选<code>DYLD_PRINT_STATISTICS</code>选项(如果想获取更详细的信息可以使用<code>DYLD_PRINT_STATISTICS_DETAILS</code>),如下图:</p>
<div class="image-package">
<div class="image-container">
<div class="image-container-fill">&nbsp;<img src="https://img2020.cnblogs.com/blog/1602144/202112/1602144-20211217102619634-201994664.webp">
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
<div class="image-view" data-width="1884" data-height="1100"><img src="//upload-images.jianshu.io/upload_images/877439-f31da849c9cae6b0.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp"></div>
</div>
<div class="image-caption">main()函数之前</div>
</div>
<p>输出结果如下:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token builtin">Total pre<span class="token operator">-main time<span class="token punctuation">:<span class="token number">34.22 milliseconds <span class="token punctuation">(<span class="token number">100.0<span class="token operator">%<span class="token punctuation">)
         dylib loading time<span class="token punctuation">:<span class="token number">14.43 milliseconds <span class="token punctuation">(<span class="token number">42.1<span class="token operator">%<span class="token punctuation">)
      rebase<span class="token operator">/binding time<span class="token punctuation">:   <span class="token number">1.82 milliseconds <span class="token punctuation">(<span class="token number">5.3<span class="token operator">%<span class="token punctuation">)
            <span class="token builtin">ObjC setup time<span class="token punctuation">:   <span class="token number">3.89 milliseconds <span class="token punctuation">(<span class="token number">11.3<span class="token operator">%<span class="token punctuation">)
         initializer time<span class="token punctuation">:<span class="token number">13.99 milliseconds <span class="token punctuation">(<span class="token number">40.9<span class="token operator">%<span class="token punctuation">)
         slowest intializers <span class="token punctuation">:
             libSystem<span class="token punctuation">.B<span class="token punctuation">.dylib <span class="token punctuation">:   <span class="token number">2.20 milliseconds <span class="token punctuation">(<span class="token number">6.4<span class="token operator">%<span class="token punctuation">)
   libBacktraceRecording<span class="token punctuation">.dylib <span class="token punctuation">:   <span class="token number">2.90 milliseconds <span class="token punctuation">(<span class="token number">8.4<span class="token operator">%<span class="token punctuation">)
    libMainThreadChecker<span class="token punctuation">.dylib <span class="token punctuation">:   <span class="token number">6.55 milliseconds <span class="token punctuation">(<span class="token number">19.1<span class="token operator">%<span class="token punctuation">)
       libswiftCoreImage<span class="token punctuation">.dylib <span class="token punctuation">:   <span class="token number">0.71 milliseconds <span class="token punctuation">(<span class="token number">2.0<span class="token operator">%<span class="token punctuation">)
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>系统级别的动态链接库,因为苹果做了优化,所以耗时并不多,而大多数时候,t1的时间大部分会消耗在我们自身App中的代码上和链接第三方库上。</p>
<p>所以我们应如何减少main()调用之前的耗时呢,我们可以优化的点有:</p>
<blockquote><ol>
<li>减少不必要的<code>framework</code>,特别是第三方的,因为动态链接比较耗时;</li>
<li><code>check framework</code>应设为<code>optional</code>和<code>required</code>,如果该<code>framework</code>在当前App支持的所有iOS系统版本都存在,那么就设为<code>required</code>,否则就设为<code>optional</code>,因为<code>optional</code>会有些额外的检查;</li>
<li>合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具:</li>
</ol>
<ul>
<li>删减一些无用的静态变量</li>
<li>删减没有被调用到或者已经废弃的方法</li>
<li>将不必须在<code>+load</code>方法中做的事情延迟到<code>+initialize</code>中</li>
<li>尽量不要用C++虚函数(创建虚函数表有开销)</li>
</ul>
</blockquote>
<h3>衡量main()函数执行之后的耗时</h3>
<p>第二阶段的耗时统计,我们认为是从<code>main ()</code>执行之后到<code>applicationDidFinishLaunching:withOptions:</code>方法最后,那么我们可以通过打点的方式进行统计。<br>
Objective-C项目因为有main文件,所以我么直接可以通过添加代码获取:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token comment">// 1. 在 main.m 添加如下代码:
<span class="token builtin">CFAbsoluteTime <span class="token builtin">AppStartLaunchTime<span class="token punctuation">;

int <span class="token function">main<span class="token punctuation">(int argc<span class="token punctuation">, char <span class="token operator">* argv<span class="token punctuation">[<span class="token punctuation">]<span class="token punctuation">) <span class="token punctuation">{
    <span class="token builtin">AppStartLaunchTime <span class="token operator">= <span class="token function">CFAbsoluteTimeGetCurrent<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;
<span class="token punctuation">.<span class="token punctuation">.<span class="token punctuation">.<span class="token punctuation">.<span class="token punctuation">.
<span class="token punctuation">}

<span class="token comment">// 2. 在 AppDelegate.m 的开头声明
extern <span class="token builtin">CFAbsoluteTime <span class="token builtin">AppStartLaunchTime<span class="token punctuation">;

<span class="token comment">// 3. 最后在AppDelegate.m 的 didFinishLaunchingWithOptions 中添加
<span class="token function">dispatch_async<span class="token punctuation">(<span class="token function">dispatch_get_main_queue<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">, <span class="token operator">^<span class="token punctuation">{
<span class="token function">NSLog<span class="token punctuation">(@<span class="token string">"App启动时间--%f"<span class="token punctuation">,<span class="token punctuation">(<span class="token function">CFAbsoluteTimeGetCurrent<span class="token punctuation">(<span class="token punctuation">)<span class="token operator">-<span class="token builtin">AppStartLaunchTime<span class="token punctuation">)<span class="token punctuation">)<span class="token punctuation">;
<span class="token punctuation">}<span class="token punctuation">)<span class="token punctuation">;
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>大家都知道Swift项目是没有main文件,官方给了如下解释:</p>
<blockquote>
<p>In Xcode, Mac templates default to including a “main.swift” file, but for iOS apps the default for new iOS project templates is to add @UIApplicationMain to a regular Swift file. This causes the compiler to synthesize a mainentry point for your iOS app, and eliminates the need for a “main.swift” file.</p>
</blockquote>
<p>也就是说,通过添加<code>@UIApplicationMain</code>标志的方式,帮我们添加了mian函数了。所以如果是我们需要在mian函数中做一些其它操作的话,需要我们自己来创建main.swift文件,这个也是苹果允许的。</p>
<ul>
<li><ol>
<li>删除<code>AppDelegate</code>类中的 <code>@UIApplicationMain</code>标志;</li>
</ol></li>
<li><ol start="2">
<li>自行创建main.swift文件,并添加程序入口:</li>
</ol></li>
</ul>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token keyword">import <span class="token builtin">UIKit

<span class="token keyword">var appStartLaunchTime<span class="token punctuation">: <span class="token builtin">CFAbsoluteTime <span class="token operator">= <span class="token function">CFAbsoluteTimeGetCurrent<span class="token punctuation">(<span class="token punctuation">)

<span class="token function">UIApplicationMain<span class="token punctuation">(
    <span class="token builtin">CommandLine<span class="token punctuation">.argc<span class="token punctuation">,
    <span class="token function">UnsafeMutableRawPointer<span class="token punctuation">(<span class="token builtin">CommandLine<span class="token punctuation">.unsafeArgv<span class="token punctuation">)
      <span class="token punctuation">.<span class="token function">bindMemory<span class="token punctuation">(
            to<span class="token punctuation">: <span class="token builtin">UnsafeMutablePointer<span class="token operator">&lt;<span class="token builtin">Int8<span class="token operator">&gt;<span class="token punctuation">.<span class="token keyword">self<span class="token punctuation">,
            capacity<span class="token punctuation">: <span class="token function">Int<span class="token punctuation">(<span class="token builtin">CommandLine<span class="token punctuation">.argc<span class="token punctuation">)<span class="token punctuation">)<span class="token punctuation">,
    <span class="token constant">nil<span class="token punctuation">,
    <span class="token function">NSStringFromClass<span class="token punctuation">(<span class="token builtin">AppDelegate<span class="token punctuation">.<span class="token keyword">self<span class="token punctuation">)
<span class="token punctuation">)
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<ul>
<li><ol start="3">
<li>在AppDelegate的<code>didFinishLaunchingWithOptions :</code>方法最后添加:</li>
</ol></li>
</ul>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token comment">// APP启动时间耗时,从mian函数开始到didFinishLaunchingWithOptions方法结束
<span class="token builtin">DispatchQueue<span class="token punctuation">.main<span class="token punctuation">.async <span class="token punctuation">{
<span class="token function">print<span class="token punctuation">(<span class="token string">"APP启动时间耗时,从mian函数开始到didFinishLaunchingWithOptions方法:\(CFAbsoluteTimeGetCurrent() - appStartLaunchTime)。"<span class="token punctuation">)
<span class="token punctuation">}
</span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>main函数之后的优化:</p>
<blockquote>
<ul>
<li><ol>
<li>尽量使用纯代码编写,减少xib的使用;</li>
</ol></li>
<li><ol start="2">
<li>启动阶段的网络请求,是否都放到异步请求;</li>
</ol></li>
<li><ol start="3">
<li>一些耗时的操作是否可以放到后面去执行,或异步执行等。</li>
</ol></li>
</ul>
</blockquote>
<h2>4. FPS</h2>
<p>通过维基百科我们知道,<code>FPS</code>是<code>Frames Per Second</code> 的简称缩写,意思是每秒传输帧数,也就是我们常说的“刷新率(单位为Hz)。</p>
<p><code>FPS</code>是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的画面就会愈流畅,<code>FPS</code>值越低就越卡顿,所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。一般我们的APP的<code>FPS</code>只要保持在 50-60之间,用户体验都是比较流畅的。</p>
<p>苹果手机屏幕的正常刷新频率是每秒60次,即可以理解为<code>FPS</code>值为60。我们都知道<code>CADisplayLink</code>是和屏幕刷新频率保存一致,所以我们是否可以通过它来监控我们的<code>FPS</code>呢?!</p>
<p>首先<code>CADisplayLink</code>是什么</p>
<blockquote>
<p><code>CADisplayLink</code>是<code>CoreAnimation</code>提供的另一个类似于<code>NSTimer</code>的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和<code>NSTimer</code>很类似,所以它实际上就是一个内置实现的替代,但是和<code>timeInterval</code>以秒为单位不同,<code>CADisplayLink</code>有一个整型的<code>frameInterval</code>属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定<code>frameInterval</code>为2,就是说动画每隔一帧执行一次(一秒钟30帧)。</p>
</blockquote>
<p>使用<code>CADisplayLink</code>监控界面的<code>FPS</code>值,参考自YYFPSLabel:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token keyword">import <span class="token builtin">UIKit

<span class="token keyword">class <span class="token class-name">LSLFPSMonitor<span class="token punctuation">: <span class="token builtin">UILabel <span class="token punctuation">{

    <span class="token keyword">private <span class="token keyword">var link<span class="token punctuation">: <span class="token builtin">CADisplayLink <span class="token operator">= <span class="token builtin">CADisplayLink<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(<span class="token punctuation">)
    <span class="token keyword">private <span class="token keyword">var <span class="token builtin">count<span class="token punctuation">: <span class="token builtin">NSInteger <span class="token operator">= <span class="token number">0
    <span class="token keyword">private <span class="token keyword">var lastTime<span class="token punctuation">: <span class="token builtin">TimeInterval <span class="token operator">= <span class="token number">0.0
    <span class="token keyword">private <span class="token keyword">var fpsColor<span class="token punctuation">: <span class="token builtin">UIColor <span class="token operator">= <span class="token builtin">UIColor<span class="token punctuation">.green
    <span class="token keyword">public <span class="token keyword">var fps<span class="token punctuation">: <span class="token builtin">Double <span class="token operator">= <span class="token number">0.0
   
    <span class="token comment">// MARK: - init
   
    <span class="token keyword">override <span class="token keyword">init<span class="token punctuation">(frame<span class="token punctuation">: <span class="token builtin">CGRect<span class="token punctuation">) <span class="token punctuation">{
      <span class="token keyword">var f <span class="token operator">= frame
      <span class="token keyword">if f<span class="token punctuation">.size <span class="token operator">== <span class="token builtin">CGSize<span class="token punctuation">.zero <span class="token punctuation">{
            f<span class="token punctuation">.size <span class="token operator">= <span class="token function">CGSize<span class="token punctuation">(width<span class="token punctuation">: <span class="token number">55.0<span class="token punctuation">, height<span class="token punctuation">: <span class="token number">22.0<span class="token punctuation">)
      <span class="token punctuation">}
      <span class="token keyword">super<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(frame<span class="token punctuation">: f<span class="token punctuation">)
      
      <span class="token keyword">self<span class="token punctuation">.textColor <span class="token operator">= <span class="token builtin">UIColor<span class="token punctuation">.white
      <span class="token keyword">self<span class="token punctuation">.textAlignment <span class="token operator">= <span class="token punctuation">.center
      <span class="token keyword">self<span class="token punctuation">.font <span class="token operator">= <span class="token builtin">UIFont<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(name<span class="token punctuation">: <span class="token string">"Menlo"<span class="token punctuation">, size<span class="token punctuation">: <span class="token number">12.0<span class="token punctuation">)
      <span class="token keyword">self<span class="token punctuation">.backgroundColor <span class="token operator">= <span class="token builtin">UIColor<span class="token punctuation">.black
      
      link <span class="token operator">= <span class="token builtin">CADisplayLink<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(target<span class="token punctuation">: <span class="token function">LSLWeakProxy<span class="token punctuation">(target<span class="token punctuation">: <span class="token keyword">self<span class="token punctuation">)<span class="token punctuation">, selector<span class="token punctuation">: #<span class="token function">selector<span class="token punctuation">(tick<span class="token punctuation">)<span class="token punctuation">)
      link<span class="token punctuation">.<span class="token function">add<span class="token punctuation">(to<span class="token punctuation">: <span class="token builtin">RunLoop<span class="token punctuation">.current<span class="token punctuation">, forMode<span class="token punctuation">: <span class="token builtin">RunLoopMode<span class="token punctuation">.commonModes<span class="token punctuation">)
    <span class="token punctuation">}
   
    <span class="token keyword">deinit <span class="token punctuation">{
      link<span class="token punctuation">.<span class="token function">invalidate<span class="token punctuation">(<span class="token punctuation">)
    <span class="token punctuation">}
   
    <span class="token keyword">required <span class="token keyword">init<span class="token operator">?<span class="token punctuation">(coder aDecoder<span class="token punctuation">: <span class="token builtin">NSCoder<span class="token punctuation">) <span class="token punctuation">{
      <span class="token function">fatalError<span class="token punctuation">(<span class="token string">"init(coder:) has not been implemented"<span class="token punctuation">)
    <span class="token punctuation">}
   
    <span class="token comment">// MARK: - actions
   
    <span class="token atrule">@objc <span class="token keyword">func <span class="token function">tick<span class="token punctuation">(link<span class="token punctuation">: <span class="token builtin">CADisplayLink<span class="token punctuation">) <span class="token punctuation">{
      <span class="token keyword">guard lastTime <span class="token operator">!= <span class="token number">0 <span class="token keyword">else <span class="token punctuation">{
            lastTime <span class="token operator">= link<span class="token punctuation">.timestamp
            <span class="token keyword">return
      <span class="token punctuation">}
      
      <span class="token builtin">count <span class="token operator">+<span class="token operator">= <span class="token number">1
      <span class="token keyword">let delta <span class="token operator">= link<span class="token punctuation">.timestamp <span class="token operator">- lastTime
      <span class="token keyword">guard delta <span class="token operator">&gt;= <span class="token number">1.0 <span class="token keyword">else <span class="token punctuation">{
            <span class="token keyword">return
      <span class="token punctuation">}
      
      lastTime <span class="token operator">= link<span class="token punctuation">.timestamp
      fps <span class="token operator">= <span class="token function">Double<span class="token punctuation">(<span class="token builtin">count<span class="token punctuation">) <span class="token operator">/ delta
      <span class="token keyword">let fpsText <span class="token operator">= <span class="token string">"<span class="token interpolation"><span class="token delimiter variable">\(<span class="token builtin">String<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(format<span class="token punctuation">: <span class="token string">"%.3f"<span class="token punctuation">, fps<span class="token punctuation">)<span class="token delimiter variable">) FPS"
      <span class="token builtin">count <span class="token operator">= <span class="token number">0
      
      <span class="token keyword">let attrMStr <span class="token operator">= <span class="token function">NSMutableAttributedString<span class="token punctuation">(attributedString<span class="token punctuation">: <span class="token function">NSAttributedString<span class="token punctuation">(string<span class="token punctuation">: fpsText<span class="token punctuation">)<span class="token punctuation">)
      <span class="token keyword">if fps <span class="token operator">&gt; <span class="token number">55.0<span class="token punctuation">{
            fpsColor <span class="token operator">= <span class="token builtin">UIColor<span class="token punctuation">.green
      <span class="token punctuation">} <span class="token keyword">else <span class="token keyword">if<span class="token punctuation">(fps <span class="token operator">&gt;= <span class="token number">50.0 <span class="token operator">&amp;&amp; fps <span class="token operator">&lt;= <span class="token number">55.0<span class="token punctuation">) <span class="token punctuation">{
            fpsColor <span class="token operator">= <span class="token builtin">UIColor<span class="token punctuation">.yellow
      <span class="token punctuation">} <span class="token keyword">else <span class="token punctuation">{
            fpsColor <span class="token operator">= <span class="token builtin">UIColor<span class="token punctuation">.red
      <span class="token punctuation">}
      attrMStr<span class="token punctuation">.<span class="token function">setAttributes<span class="token punctuation">(<span class="token punctuation">[<span class="token builtin">NSAttributedStringKey<span class="token punctuation">.foregroundColor<span class="token punctuation">:fpsColor<span class="token punctuation">]<span class="token punctuation">, range<span class="token punctuation">: <span class="token function">NSMakeRange<span class="token punctuation">(<span class="token number">0<span class="token punctuation">, attrMStr<span class="token punctuation">.length <span class="token operator">- <span class="token number">3<span class="token punctuation">)<span class="token punctuation">)
      attrMStr<span class="token punctuation">.<span class="token function">setAttributes<span class="token punctuation">(<span class="token punctuation">[<span class="token builtin">NSAttributedStringKey<span class="token punctuation">.foregroundColor<span class="token punctuation">:<span class="token builtin">UIColor<span class="token punctuation">.white<span class="token punctuation">]<span class="token punctuation">, range<span class="token punctuation">: <span class="token function">NSMakeRange<span class="token punctuation">(attrMStr<span class="token punctuation">.length <span class="token operator">- <span class="token number">3<span class="token punctuation">, <span class="token number">3<span class="token punctuation">)<span class="token punctuation">)
      <span class="token builtin">DispatchQueue<span class="token punctuation">.main<span class="token punctuation">.async <span class="token punctuation">{
            <span class="token keyword">self<span class="token punctuation">.attributedText <span class="token operator">= attrMStr
      <span class="token punctuation">}
    <span class="token punctuation">}
<span class="token punctuation">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>通过<code>CADisplayLink</code>的实现方式,并真机测试之后,确实是可以在很大程度上满足了监控<code>FPS</code>的业务需求和为提高用户体验提供参考,但是和Instruments的值可能会有些出入。下面我们来讨论下使用<code>CADisplayLink</code>的方式,可能存在的问题。</p>
<ul>
<li>(1). 和Instruments值对比有出入,原因如下:</li>
</ul>
<blockquote>
<p><code>CADisplayLink</code>运行在被添加的那个<code>RunLoop</code>之中(一般是在主线程中),因此它只能检测出当前<code>RunLoop</code>下的帧率。<code>RunLoop</code>中所管理的任务的调度时机,受任务所处的<code>RunLoopMode</code>和CPU的繁忙程度所影响。所以想要真正定位到准确的性能问题所在,最好还是通过Instrument来确认。</p>
</blockquote>
<ul>
<li>(2). 使用<code>CADisplayLink</code>可能存在的<strong>循环引用</strong>问题。</li>
</ul>
<p>例如以下写法:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token keyword">let link <span class="token operator">= <span class="token builtin">CADisplayLink<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(target<span class="token punctuation">: <span class="token keyword">self<span class="token punctuation">, selector<span class="token punctuation">: #<span class="token function">selector<span class="token punctuation">(tick<span class="token punctuation">)<span class="token punctuation">)

<span class="token keyword">let timer <span class="token operator">= <span class="token builtin">Timer<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(timeInterval<span class="token punctuation">: <span class="token number">1.0<span class="token punctuation">, target<span class="token punctuation">: <span class="token keyword">self<span class="token punctuation">, selector<span class="token punctuation">: #<span class="token function">selector<span class="token punctuation">(tick<span class="token punctuation">)<span class="token punctuation">, userInfo<span class="token punctuation">: <span class="token constant">nil<span class="token punctuation">, repeats<span class="token punctuation">: <span class="token boolean">true<span class="token punctuation">)

</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p><strong>原因</strong>:以上两种用法,都会对 self 强引用,此时 timer持有 self,self 也持有 timer,循环引用导致页面 dismiss 时,双方都无法释放,造成循环引用。此时使用 weak 也不能有效解决:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token keyword">weak <span class="token keyword">var weakSelf <span class="token operator">= <span class="token keyword">self
<span class="token keyword">let link <span class="token operator">= <span class="token builtin">CADisplayLink<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(target<span class="token punctuation">: weakSelf<span class="token punctuation">, selector<span class="token punctuation">: #<span class="token function">selector<span class="token punctuation">(tick<span class="token punctuation">)<span class="token punctuation">)
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<p>那么我们应该怎样解决这个问题,有人会说在<code>deinit</code>(或<code>dealloc</code>)中调用定时器的<code>invalidate</code>方法,但是这是无效的,因为已经造成循环引用了,不会走到这个方法的。</p>
<p><code>YYKit</code>作者提供的解决方案是使用 YYWeakProxy,这个<code>YYWeakProxy</code>不是继承自<code>NSObject</code>而是继承<code>NSProxy</code>。</p>
<blockquote>
<h3>NSProxy</h3>
<p>An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.</p>
</blockquote>
<p><code>NSProxy</code>是一个为对象定义接口的抽象父类,并且为其它对象或者一些不存在的对象扮演了替身角色。具体的可以看下NSProxy的官方文档<br>
修改后代码如下,亲测定时器如愿释放,<code>LSLWeakProxy</code>的具体实现代码已经同步到github中。</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token keyword">let link <span class="token operator">= <span class="token builtin">CADisplayLink<span class="token punctuation">.<span class="token keyword">init<span class="token punctuation">(target<span class="token punctuation">: <span class="token function">LSLWeakProxy<span class="token punctuation">(target<span class="token punctuation">: <span class="token keyword">self<span class="token punctuation">)<span class="token punctuation">, selector<span class="token punctuation">: #<span class="token function">selector<span class="token punctuation">(tick<span class="token punctuation">)<span class="token punctuation">)
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<h2>5. 卡顿</h2>
<p>在了解卡顿产生的原因之前,先看下屏幕显示图像的原理。</p>
<h4>屏幕显示图像的原理:</h4>
<div class="image-package">
<div class="image-container">
<div class="image-container-fill">&nbsp;<img src="https://img2020.cnblogs.com/blog/1602144/202112/1602144-20211217102711493-111622706.webp">
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
<div class="image-view" data-width="700" data-height="322"><img src="//upload-images.jianshu.io/upload_images/877439-d8f58796bc648a9d.png?imageMogr2/auto-orient/strip|imageView2/2/w/700/format/webp"></div>
</div>
<div class="image-caption">屏幕绘制原理</div>
</div>
<p>现在的手机设备基本都是采用双缓存+垂直同步(即V-Sync)屏幕显示技术。</p>
<p>如上图所示,系统内<code>CPU</code>、<code>GPU</code>和显示器是协同完成显示工作的。其中<code>CPU</code>负责计算显示的内容,例如视图创建、布局计算、图片解码、文本绘制等等。随后<code>CPU</code>将计算好的内容提交给<code>GPU</code>,由<code>GPU</code>进行变换、合成、渲染。<code>GPU</code>会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,<code>GPU</code>会直接将视频控制器的指针指向第二个容器(双缓存原理)。这里,<code>GPU</code>会等待显示器的<code>VSync</code>(即垂直同步)信号发出后,才进行新的一帧渲染和缓冲区更新(这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟)。</p>
<h4>卡顿的原因:</h4>
<div class="image-package">
<div class="image-container">
<div class="image-container-fill"><img src="https://img2020.cnblogs.com/blog/1602144/202112/1602144-20211217102724384-1605530083.webp">
<p>&nbsp;</p>
<p>&nbsp;</p>
&nbsp;</div>
<div class="image-view" data-width="1424" data-height="368"><img src="//upload-images.jianshu.io/upload_images/877439-13fba20b4f543bbb.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp"></div>
</div>
<div class="image-caption">掉帧</div>
</div>
<p>由上面屏幕显示的原理,采用了垂直同步机制的手机设备。如果在一个<code>VSync</code> 时间内,<code>CPU</code> 或<code>GPU</code> 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。例如在主线程里添加了阻碍主线程去响应点击、滑动事件、以及阻碍主线程的UI绘制等的代码,都是造成卡顿的常见原因。</p>
<h4>卡顿监控:</h4>
<p>卡顿监控一般有两种实现方案:</p>
<ul>
<li>
<p>(1). <strong>主线程卡顿监控</strong>。通过子线程监测主线程的<code>runLoop</code>,判断两个状态区域之间的耗时是否达到一定阈值。</p>
</li>
<li>
<p>(2). <strong><code>FPS</code>监控</strong>。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。<code>FPS</code>的监控实现原理,上面已经探讨过这里略过。</p>
</li>
</ul>
<p>在使用<code>FPS</code>监控性能的实践过程中,发现 <code>FPS</code> 值抖动较大,造成侦测卡顿比较困难。为了解决这个问题,<strong>通过采用检测主线程每次执行消息循环的时间,当这一时间大于规定的阈值时,就记为发生了一次卡顿的方式来监控</strong>。<br>
这也是美团的移动端采用的性能监控Hertz 方案,微信团队也在实践过程中提出来类似的方案--微信读书 iOS 性能优化总结。</p>
<div class="image-package">
<div class="image-container">
<div class="image-container-fill">&nbsp;<img src="https://img2020.cnblogs.com/blog/1602144/202112/1602144-20211217102811504-1899699853.webp">
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
<div class="image-view" data-width="1268" data-height="1074"><img src="//upload-images.jianshu.io/upload_images/877439-a61af10b3a84c76f.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp"></div>


</div>
<div class="image-caption">美团Hertz方案流程图</div>


</div>
<p>方案的提出,是根据滚动引发的Sources事件或其它交互事件总是被快速的执行完成,然后进入到kCFRunLoopBeforeWaiting状态下;假如在滚动过程中发生了卡顿现象,那么RunLoop必然会保持kCFRunLoopAfterWaiting或者kCFRunLoopBeforeSources这两个状态之一。</p>
<h4>所以监控主线程卡顿的方案一:</h4>
<p>开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况。<br>
但是由于主线程的RunLoop在闲置时基本处于Before Waiting状态,这就导致了即便没有发生任何卡顿,这种检测方式也总能认定主线程处在卡顿状态。</p>
<p>为了解决这个问题寒神(南栀倾寒)给出了自己的解决方案,<code>Swift</code>的卡顿检测第三方ANREye。这套卡顿监控方案大致思路为:创建一个子线程进行循环检测,每次检测时设置标记位为<code>YES</code>,然后派发任务到主线程中将标记位设置为<code>NO</code>。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成<code>NO</code>,如果没有说明主线程发生了卡顿。</p>
<p>结合这套方案,当主线程处在Before Waiting状态的时候,通过派发任务到主线程来设置标记位的方式处理常态下的卡顿检测:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-objective-c"><code class="Objective-Clanguage-objective-c">#define lsl_SEMAPHORE_SUCCESS 0
static BOOL lsl_is_monitoring = NO;
static dispatch_semaphore_t lsl_semaphore;
static NSTimeInterval lsl_time_out_interval = 0.05;


@implementation LSLAppFluencyMonitor

static inline dispatch_queue_t __lsl_fluecy_monitor_queue() {
    static dispatch_queue_t lsl_fluecy_monitor_queue;
    static dispatch_once_t once;
    dispatch_once(&amp;once, ^{
      lsl_fluecy_monitor_queue = dispatch_queue_create("com.dream.lsl_monitor_queue", NULL);
    });
    return lsl_fluecy_monitor_queue;
}

static inline void __lsl_monitor_init() {
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      lsl_semaphore = dispatch_semaphore_create(0);
    });
}

#pragma mark - Public
+ (instancetype)monitor {
    return ;
}

- (void)startMonitoring {
    if (lsl_is_monitoring) { return; }
    lsl_is_monitoring = YES;
    __lsl_monitor_init();
    dispatch_async(__lsl_fluecy_monitor_queue(), ^{
      while (lsl_is_monitoring) {
            __block BOOL timeOut = YES;
            dispatch_async(dispatch_get_main_queue(), ^{
                timeOut = NO;
                dispatch_semaphore_signal(lsl_semaphore);
            });
            ;
            if (timeOut) {
                ;       // 打印主线程调用栈
//                ;    // 打印当前线程的调用栈
//                ;// 打印所有线程的调用栈
            }
            dispatch_wait(lsl_semaphore, DISPATCH_TIME_FOREVER);
      }
    });
}

- (void)stopMonitoring {
    if (!lsl_is_monitoring) { return; }
    lsl_is_monitoring = NO;
}

@end
</code></pre>
</div>
<p>其中<code>LSLBacktraceLogger</code>是获取堆栈信息的类,详情见代码Github。</p>
<p>打印日志如下:</p>
<div class="_2Uzcx_"><button class="VJbwyy" type="button"></button>
<pre class="line-numberslanguage-swift"><code class="Swiftlanguage-swift"><span class="token number">2018<span class="token operator">-<span class="token number">08<span class="token operator">-<span class="token number">16 <span class="token number">12<span class="token punctuation">:<span class="token number">36<span class="token punctuation">:<span class="token number">33.910491<span class="token operator">+<span class="token number">0800 <span class="token builtin">AppPerformance<span class="token punctuation">[<span class="token number">4802<span class="token punctuation">:<span class="token number">171145<span class="token punctuation">] <span class="token builtin">Backtrace of <span class="token builtin">Thread <span class="token number">771<span class="token punctuation">:
<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">==
libsystem_kernel<span class="token punctuation">.dylib         <span class="token number">0x10d089bce __semwait_signal <span class="token operator">+ <span class="token number">10
libsystem_c<span class="token punctuation">.dylib            <span class="token number">0x10ce55d10 usleep <span class="token operator">+ <span class="token number">53
<span class="token builtin">AppPerformance               <span class="token number">0x108b8b478 $<span class="token builtin">S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtF <span class="token operator">+ <span class="token number">1144
<span class="token builtin">AppPerformance               <span class="token number">0x108b8b60b $<span class="token builtin">S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtFTo <span class="token operator">+ <span class="token number">155
<span class="token builtin">UIKitCore                      <span class="token number">0x1135b104f <span class="token operator">-<span class="token punctuation"> <span class="token operator">+ <span class="token number">95
<span class="token builtin">UIKitCore                      <span class="token number">0x1131ed34d <span class="token operator">-<span class="token punctuation">[<span class="token builtin">UITableView _createPreparedCellForGlobalRow<span class="token punctuation">:withIndexPath<span class="token punctuation">:willDisplay<span class="token punctuation">:<span class="token punctuation">] <span class="token operator">+ <span class="token number">765
<span class="token builtin">UIKitCore                      <span class="token number">0x1131ed8da <span class="token operator">-<span class="token punctuation">[<span class="token builtin">UITableView _createPreparedCellForGlobalRow<span class="token punctuation">:willDisplay<span class="token punctuation">:<span class="token punctuation">] <span class="token operator">+ <span class="token number">73
<span class="token builtin">UIKitCore                      <span class="token number">0x1131b4b1e <span class="token operator">-<span class="token punctuation">[<span class="token builtin">UITableView _updateVisibleCellsNow<span class="token punctuation">:isRecursive<span class="token punctuation">:<span class="token punctuation">] <span class="token operator">+ <span class="token number">2863
<span class="token builtin">UIKitCore                      <span class="token number">0x1131d57eb <span class="token operator">-<span class="token punctuation">[<span class="token builtin">UITableView layoutSubviews<span class="token punctuation">] <span class="token operator">+ <span class="token number">165
<span class="token builtin">UIKitCore                      <span class="token number">0x1133921ee <span class="token operator">-<span class="token punctuation">[<span class="token function">UIView<span class="token punctuation">(<span class="token builtin">CALayerDelegate<span class="token punctuation">) layoutSublayersOfLayer<span class="token punctuation">:<span class="token punctuation">] <span class="token operator">+ <span class="token number">1501
<span class="token builtin">QuartzCore                     <span class="token number">0x10ab72eb1 <span class="token operator">-<span class="token punctuation">[<span class="token builtin">CALayer layoutSublayers<span class="token punctuation">] <span class="token operator">+ <span class="token number">175
<span class="token builtin">QuartzCore                     <span class="token number">0x10ab77d8b _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE <span class="token operator">+ <span class="token number">395
<span class="token builtin">QuartzCore                     <span class="token number">0x10aaf3b45 _ZN2CA7Context18commit_transactionEPNS_11TransactionE <span class="token operator">+ <span class="token number">349
<span class="token builtin">QuartzCore                     <span class="token number">0x10ab285b0 _ZN2CA11Transaction6commitEv <span class="token operator">+ <span class="token number">576
<span class="token builtin">QuartzCore                     <span class="token number">0x10ab29374 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv <span class="token operator">+ <span class="token number">76
<span class="token builtin">CoreFoundation               <span class="token number">0x109dc3757 <span class="token constant">__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ <span class="token operator">+ <span class="token number">23
<span class="token builtin">CoreFoundation               <span class="token number">0x109dbdbde __CFRunLoopDoObservers <span class="token operator">+ <span class="token number">430
<span class="token builtin">CoreFoundation               <span class="token number">0x109dbe271 __CFRunLoopRun <span class="token operator">+ <span class="token number">1537
<span class="token builtin">CoreFoundation               <span class="token number">0x109dbd931 <span class="token builtin">CFRunLoopRunSpecific <span class="token operator">+ <span class="token number">625
<span class="token builtin">GraphicsServices               <span class="token number">0x10f5981b5 <span class="token builtin">GSEventRunModal <span class="token operator">+ <span class="token number">62
<span class="token builtin">UIKitCore                      <span class="token number">0x112c812ce <span class="token builtin">UIApplicationMain <span class="token operator">+ <span class="token number">140
<span class="token builtin">AppPerformance               <span class="token number">0x108b8c1f0 main <span class="token operator">+ <span class="token number">224
libdyld<span class="token punctuation">.dylib                  <span class="token number">0x10cd4dc9d start <span class="token operator">+ <span class="token number">1

<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">===<span class="token operator">==
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<h4>方案二是结合<code>CADisplayLink</code>的方式实现</h4>
<p>在检测FPS值的时候,我们就详细介绍了<code>CADisplayLink</code>的使用方式,在这里也可以通过FPS值是否连续低于某个值开进行监控。</p>
<h3>&nbsp;</h3>
</div>
<p><br><br><br></p>
<p><img src="https://common.cnblogs.com/images/loading.gif"></p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/lijinfu-software/p/15701209.html
頁: [1]
查看完整版本: iOS开发--APP性能检测方案汇总