写点什么

iOS 应用性能数据采集原理和优化实践 | 详细版

  • 2022 年 3 月 09 日
  • 本文字数:2497 字

    阅读完需:约 8 分钟

云智慧集团成立于 2009 年,是全栈智能业务运维解决方案服务商。经过多年自主研发,公司形成了从 IT 运维、电力运维到 IoT 运维的产业布局,覆盖 ITOM、ITOA、ITSM、DevOps 以及 IoT 几大领域,为金融、政府、运营商、能源、交通、制造等上百家行业的客户,提供了数字化运维体系建设及全生命周期运维管理解决方案。云智慧秉承 Make Digital Online 的使命,致力于通过先进的产品技术,为企业数字化转型和提升 IT 运营效率持续赋能。

作者简介

刘徐兵(Alvin Liu),云智慧/开发经理。曾在高德、当当有多年大型 App 开发经验,在云智慧从事 APM SDK 研发工作 5+年。对 App 开发和性能优化有深入的研究和实践。

iOS 应用数据采集的基础 Objective-C Runtime

1、消息转发

Objective-C 语言扩展了 C 语言,扩展的核心在于引入了 Runtime 库,使 Objective-C 语言拥有了面向对象和动态运行时的特性。而动态运行时机制的核心和表现是消息转发机制。

Objective-C 语言拥有动态运行时的机制,方法的执行是在运行阶段决定的而不是在编译阶段决定的。而方法执行的实质是向对象发送了一个消息,官方 API 为

objc_msgSend(void /* id self, SEL op, ... */ )
复制代码

objc_msgSend 有 2 个常用参数:id self 和 SEL op,用于标识对象和方法,即向某个对象发送了某个消息。因此 Objective-C 的[instance method]调用会被编译器转换成 C 语言 API objc_msgSend 的调用。

以下消息转发机制原理图从官方文档翻译而来:

消息转发机制示例图

如上图所示,Objective-C 的消息转发流程中,在当前对象方法列表中找不到方法的实现时,运行时环境会依次进行三个阶段的查找

第一阶段

对象在收到无法处理的消息时,首先调用所属类的下列类方法。

+(BOOL)resolveInstanceMethod:(SEL)sel{     //默认返回NO     return NO;} +(BOOL)resolveClassMethod:(SEL)sel{     //默认返回NO     return NO;}
复制代码

如果在其中找到了方法的实现,则进行消息处理;否则就进入第二阶段。

第二阶段

在当前类里找不到该方法的实现时,运行时系统尝试更换调用的对象,会调用如下方法

-(id)forwardingTargetForSelector:(SEL)aSelector{}
复制代码

如果在该方法中还找不到方法的实现,就进入第三阶段。

第三阶段

到这里是最后一个阶段,通过创建 NSInvocation 实例,将与未处理的消息有关的细节封装起来,运行时系统调用的接口为

-(void)forwardInvocation:(NSInvocation *)anInvocation{}
复制代码

该方法会沿着类的继承链一直往上调用,直至在 NSObject 的该方法中抛出 doesNotRecognizeSelector:异常。

2、函数指针

SEL 是 Objective-C 语言的方法选择器,也就是 selector 的指针。再往底层去,每个方法 SEL 还对应着一个 IMP 函数指针,指向方法实现的首地址。官方 API 为:

typedef id (*IMP)(id, SEL, ...); 
复制代码

有了它就可以直接执行 IMP 指向的函数(方法)了。

以上介绍了理论基础,下面介绍下基于 Objective-C 语言动态运行时的方法拦截(Hook)操作。

Hook 原理

Hook 步骤

  1. 使用 Category 特性往类中添加用于拦截的方法 SEL

  2. 使用系统 runtime 的接口(Swizzle)交换方法实现体(IMP)

Hook 使用示例图

如图所示,开发者调用原有方法时,会转发到拦截的方法里,因交换了原方法与拦截方法的入口地址(IMP),在拦截方法执行结束时能调回原方法,对原有业务没有影响。

Hook 回调函数

难点:不是类的实例方式,不能用 Category 特性。

优化前:全工程扫描,再拦截。

缺点:只能在主线程中操作,扫描文件数和消耗的时间与 App 的规模大小成正比。

回调函数拦截优化

步骤:

  1. 针对要使用 protocol 的系统类使用 Category 特性添加拦截方法

  2. 拦截设置 delegate 的 setter 方法,获取到 delegate 的实例

  3. 针对获取到的 delegate 实例再进行回调方法拦截

优化后

1、延迟拦截:在方法被调用时才被拦截而且只拦截一次

2、SDK 的启动操作跟 App 的业务和规模无关,对 App 的影响降到最低

崩溃解码实践

目的:了解 iOS 崩溃解码原理



崩溃解码步骤

  1. 虚拟内存偏移量:145624->0x238d8->0x00000001000238d8

  2. 使用 dwarfdump 命令,将 dSYM 文件解析成可读文件

dwarfdum-e--debug-info 包含全部类和方法的代码和地址映射信息->信息文件

dwarfdum-e--debug-line 包含全部类的代码行号和地址映射信息->行号文件

  1. 在信息文件中查找偏移量所在的类和方法,得到崩溃的类和对应的方法

  2. 在行号文件中查找偏移量所在的行号,得到具体的崩溃行号

虚拟内存偏移量:

0x00000001000238d8

dSYM 文件解析命令

  1. symbolicatecrash->全文本解析

  2. atos(mac)/atosl(linux)->单行解析

H5 页面监控原理

以上介绍了系统原生接口的数据采集原理,下面介绍下 H5 页面数据采集实现原理。

H5 页面数据采集通过自动往 H5 页面注入 JS 代码实现。流程如下图所示

H5 页面注入 JS 代码示意图

如上图所示,在 UIWebView 时代,通过 NSURLProtocol 协议簇的接口,拦截到加载页面数据的接口,获取到 H5 数据块,检测数据块中的<head>标签,将 JS 代码注入到<head>中。这样 JS 代码就和 H5 页面代码一起加载、工作,能够采集到 H5 页面的相关信息。JS 代码采集到信息后,通过 iframe 的方式触发 UIWebView 的回调将信息发送给原生 SDK 端存储、上报。

在 WKWebView 时代,由于 WKWebView 是单独的进程,在 App 里从系统的网络协议簇无法获取到 WKWebView 的数据,可通过拦截 WKWebView 的回调函数去执行 JS 代码,能起到与 UIWebView 同样的数据采集效果。这里不再赘述。

PS:JS 代码的工作原理是另一个技术领域的范畴,不在这里赘述。

写在最后

近年来,在 AIOps 领域快速发展的背景下,IT 工具、平台能力、解决方案、AI 场景及可用数据集的迫切需求在各行业迸发。基于此,云智慧在 2021 年 8 月发布了 AIOps 社区,旨在树起一面开源旗帜,为各行业客户、用户、研究者和开发者们构建活跃的用户及开发者社区,共同贡献及解决行业难题、促进该领域技术发展。

社区先后开源了数据可视化编排平台-FlyFish、运维管理平台 OMP、云服务管理平台-摩尔平台、Hours 算法等产品。

项目介绍:https://www.cloudwise.ai/flyFish.html

Github 地址: https://github.com/CloudWise-OpenSource/FlyFish

Gitee 地址: https://gitee.com/CloudWise/fly-fish

请您通过下方链接了解我们,添加小助手微信(xiaoyuerwie),备注:飞鱼。申请加入开发者交流群,可与业内大咖进行 1V1 交流!

也可通过下方微信获取 AIOps 资讯,了解云智慧开源社区其他项目开源情况!


用户头像

全栈智能业务运维服务商 2021.03.10 加入

我们秉承Make Digital Online的使命,致力于通过先进的产品技术,为企业数字化转型和提升IT运营效率持续赋能。 https://www.cloudwise.com/

评论

发布
暂无评论
iOS应用性能数据采集原理和优化实践 | 详细版_ios_云智慧AIOps社区_InfoQ写作平台