• 使用Signposts分析App性能


    保持稳定的性能,是我们提供良好的用户体验的关键之一。
    但是很多时候,性能下降很难重现,也无法获得良好的数据支撑。

    为了解决这个问题,苹果在iOS12上推出了一个用于辅助开发调试的工具:Signposts(路标)

    1. Signposts 简介

    Signposts(路标)OSLog 家族成员之一,我们可以用它来测量和收集性能数据,以便结合 Instruments 来做可视化分析。其对应的API为 os_signpost,它主要有两大功能:标记时间段 ( intervals) 和单个时间点 ( events)。

    下面,我们来看看如何使用 Signposts。

    2. 使用 Signposts

    2.1 测量时间间隔

    Signposts 通过用 os_signpost(.begin, ...)os_signpost(.end, ...) 来标记一个路标间隔的开始和结束。

    ![Measuring Intervals with Signposts请添加图片描述

    代码如下:

    import os.signpost
    
    // 1.
    let refreshLog = OSLog(subsystem: "com.example.your-app", category: "RefreshOperations")
    // 2.
    os_signpost(.begin, log: refreshLog, name: "Refresh Panel")
    for element in panel.elements {
        // 3.
        os_signpost(.begin, log: refreshLog, name: "Fetch Asset")
        // 4.
        fetchAsset(for: element)
        // 5.
        os_signpost(.end, log: refreshLog, name: "Fetch Asset")
    }
    // 6.
    os_signpost(.end, log: refreshLog, name: "Refresh Panel")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    下面详细解释一下上面的代码:

    1. Signposts是基于日志系统的,所以需要先创建一个日志句柄(或日志对象),这个日志句柄初始化需要两个参数,其中 subsystem 表示应用子系统,可以是应用的包名或者框架的名称,采用反向DNS 表示法,如 com.your_company.your_subsystem_namecategory 表示子系统中的一个类别,用于对日志消息进行分类和过滤。
    2. 在获取所有资源的开始,添加一个名为Refresh Panel路标
    3. 在获取某个资源的开始,添加一个路标,因为在 for 循环内部,这里会重复调用
    4. 获取资源…
    5. 在获取某个资源结束时,添加一个路标。
    6. 在获取所有资源结束时,添加路标。

    在每次开始、结束获取资源时,我们都添加了一个 路标。因为在开始和结束的路标的标识一样,所以我们可以将两者匹配在一起。

    2.2 Signpost IDs(路标ID)

    for element in panel.elements {
        os_signpost(.begin, log: refreshLog, name: "Fetch Asset")
        fetchAsset(for: element)
        os_signpost(.end, log: refreshLog, name: "Fetch Asset")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    像上面路标名称为 Fetch Asset 会运行多次,我们无法区分是获取哪个资源的时间间隔。为了让系统能够区分它们并知道哪个开始匹配哪个结束,我们可以添加一个路标ID。

    for element in panel.elements {
        // 通过日志句柄来生成一个路标ID。
        let spid = OSSignpostID(log: refreshLog)
        os_signpost(.begin, log: refreshLog, name: "Fetch Asset", signpostID: spid)
        fetchAsset(for: element)
        os_signpost(.end, log: refreshLog, name: "Fetch Asset", signpostID: spid)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果调用的对象本身具有唯一性,还可以用对象作为路标ID。

    let spid = OSSignpostID(log: log, object: element)
    
    • 1

    2.3 测量异步时间间隔

    请添加图片描述

    路标还支持测量异步任务的时间间隔。由于是异步的,这些时间间隔可能会相互重叠,所以我们需要在创建路标时加上路标ID来区分。

    let refreshLog = OSLog(subsystem: "com.example.your-app", category: "RefreshOperations")
    
    let spidForRefresh = OSSignpostID(log: refreshLog)
    os_signpost(.begin, log: refreshLog, name: "Refresh Panel", signpostID: spidForRefresh)
    
    for element in panel.elements {
        // 通过对象去创建路标ID
        let spid = OSSignpostID(log: refreshLog, object: element)
        // 通过ID创建一个路标
        os_signpost(.begin, log: refreshLog, name: "Fetch Asset", signpostID: spid)
        fetchAssetAsync(for: element) {
            // 每一个完成之后的回调
            os_signpost(.end, log: refreshLog, name: "Fetch Asset", signpostID: spid)
        }
    }
    
    // 全部完成完成的回调
    notifyWhenDone {
        os_signpost(.end, log: refreshLog, name: "Refresh Panel", signpostID: spidForRefresh)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2.4 向路标添加元数据

    你可能还给路标的开始/结束提供一些上下文信息,如成功/失败信息,有利于我们后续去分析和追踪特定场景下的问题。

    os_signpost 函数支持可变参数,允许我们在使用的时候通过格式化字符串的方式增加元数据。

    // 添加上下文信息
    os_signpost(.begin, log: log, name: "Compute Physics", "for particle")
    
    // 格式化字符串
    os_signpost(.begin, log: log, name: "Compute Physics", "%d %d %d %d",
    x1, y1, x2, y2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    需要注意的是,os_signpost这个API的 nameformat 参数都是 StaticString 类型(format是可选参数)。StaticStringString 的区别是前者的值是由编译时确认的,其初始化之后无法修改,即使是使用 var 创建。系统的日志库 OSLog 也是选择 StaticString 作为参数类型,这么做的目的一部分在于编译器可采取一定的优化,另一部分则是出于对隐私的考量。

    如果需要动态拼接字符串,需要使用这种格式化语法:%{public}s

    // 拼接动态字符串
    os_signpost(.begin, log: log, name: "Compute Physics",
        "Calculating %{public}s: %d %d %d %d", description, x1, y2, x2, y2)
    
    • 1
    • 2
    • 3

    2.5 标记特定时间点

    除了标记开始和结束之外,您还可以使用 .event 路标类型标记过程中的特定时间点。
    图3

    示例代码:

    os_signpost(.event, log: log, name: "Fetch Asset", "Connected to service")
    os_signpost(.event, log: log, name: "Fetch Asset",
                        "Fetched first chunk, size %u", size)
    
    • 1
    • 2
    • 3

    2.5 启用和禁用路标

    Signposts(路标)是非常轻量级的,通过编译器做了很多优化,这些优化主要是在编译时完成的而不是运行时,而且很多工作都交给了 Instruments后端来完成,这使得Signposts在发送的时候只会占用非常少的系统资源。

    路标默认是开启的,如果我们希望禁用路标,可以将其日志句柄设置为 OSLog.disabled

    let refreshLog: OSLog
    // 可在 Xcode -> Edit Scheme -> Arguments 下面添加环境变量
    if ProcessInfo.processInfo.environment.keys.contains("SIGNPOSTS_FOR_REFRESH") {
        refreshLog = OSLog(subsystem: "com.example.your-app", category: "RefreshOperations")
    } else {
        refreshLog = .disabled //禁用路标
    }
    os_signpost(.begin, log: refreshLog, name: "Refresh Panel")
    for element in panel.elements {
        os_signpost(.begin, log: refreshLog, name: "Fetch Asset")
        fetchAsset(for: element)
        os_signpost(.end, log: refreshLog, name: "Fetch Asset")
    }
    os_signpost(.end, log: refreshLog, name: "Refresh Panel")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如果你有一些基于Instruments的特定的功能,你可以检查特定的日志句柄,查看其 signpostsEnabled 属性。然后用该属性来控制添加该附加操作。

    if refreshLog.signpostsEnabled {
        let information = copyDescription() 
        os_signpost(..., information)
    }
    
    • 1
    • 2
    • 3
    • 4

    2.6 Signposts in C

    Signposts不仅支持Swift,同时也支持C、Objective-c,对应的API如下图所示。
    图4

    2.7 在Instruments中使用路标

    在Xcode中,长按运行按钮,选择Profile,也可以直接用快捷健 CMD + I 来打开Instruments,选择空白(Blank)模块,在右上角添加 os_signpost。

    图5

    然后,点击左上角的Record按钮,开始启动我们的App,然后在 Instruments 可以陆续看到我们添加的路标。

    图6

    参考资料

  • 相关阅读:
    基于Java的师生健康体检管理系统设计与实现(源码+lw+部署文档+讲解等)
    新零售SaaS架构:线上商城系统架构设计
    Linux磁盘管理 - 为CentOS 添加一块硬盘,并设置一个 10G 的 xfs 分区,然后将其挂载到/var/new目录当中,并设置开机自启
    基于STFT和卷积神经网络的时序数据分类 代码+数据 可直接运行
    Java的NIO体系
    [剑指 Offer 28]对称的二叉树
    openpyxl隐藏/删除excel某一列
    我在 vscode 插件里接入了 ChatGPT,解决了代码变量命名的难题
    kubernetes使用(1.25)
    基于python下django框架 实现电影院售票系统详细设计
  • 原文地址:https://blog.csdn.net/lexiaoyao20/article/details/126007465