BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

网易NeteaseAPM iOS SDK技术实现分享

| 作者 朱志强 关注 0 他的粉丝 发布于 2016年5月24日. 估计阅读时间: 18 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

嘉宾介绍

朱志强,曾在创业团队中担任技术负责人,开发OSX下办公软件,独立完成的文字编辑工具包含图文混排、图表、输入动画、多媒体等功能。目前是网易APM iOS端负责人,热爱编程之美,享受创造的乐趣。

一. NeteaseAPM是什么

Application Performance Management(APM),应用程序性能管理, 主要指对企业的关键业务应用进行监测、优化,提高企业应用的可靠性和质量,保证用户得到良好的服务。一个企业的关键业务应用的性能强大,可以提高竞争力,并取得商业成功。

NetsaseAPM是网易性能数据分析平台,一个用户数据分析平台,支持非侵入式获取应用性能数据,实时展示多个维度分析结果。目前支持移动端和浏览器端,此次分享只介绍iOS SDK端。

NetsaseAPM移动端支持的功能:

  1. 应用性能分析
    对当前应用请求的各项性能指标进行分析,如响应时间,吞吐量,下载速率等,帮助用户全面了解应用性能表现。

  2. 错误分析
    分析应用每个域名的网络错误率及响应码错误率,快速定位应用问题。

  3. 多维分析
    可以组合域名,地理位置,运营商,网络环境等参数,精确定位应用的性能问题。

下面是从移动端收集的数据在NeteaseAPM Web平台的展示:

(点击放大图像)

(点击放大图像)

(点击放大图像)

二. NeteaseAPM iOS SDK的目标

  1. 最小侵入:
    只需要启动一次,就可以持续收集网络和交互数据,不需要手动收集数据。
    启动方式:

  2. 启动之后,NeteaseAPM会插入收集数据的代码到系统调用中,却不会影响用户的使用,在用户的网络消息中的位置如图:

  3. 最大化自由配置:

    用户可以看到收集到的数据,选择是否上传到NeteaseAPM服务器;

    用户可以自己上传数据到NeteaseAPM服务器;

  4. 更多功能:
    首创监控底层网络库CFNetwork,和竞品监控第三方网络库ASIHttpRequest的方案相比,更加干净,能监控更多数据;

三. 现状

目前实现的功能:

  • 网络请求的响应时间,下载速率,状态码,错误码,网络状态等数据的收集;

  • 页面加载时间的收集,检查出慢交互页面;

已经接入的应用: 二次元,秀品,网易新闻,考拉。

四. 整体设计

NeteaseAPM iOS SDK分为四个部分:

  • Hooker
    Hooker负责在用户感知不到的情况下替换程序原实现,转发消息回调,完成对系统消息的hook和数据的采集。
    Hooker能否监控到更多更准确的数据,是衡量一个APM产品是否优秀的最重要的标准。
    一个APM产品监控的数据面越广,收集的数据越细,就越能准确地取得用户的性能数据,帮助产品优化性能。

  • DataBuilder
    收集监控数据;

  • Persistence
    缓存监控数据;

  • Poster
    上传监控数据到NeteaseAPM;

线程模型: 监控数据的保存和发送都在后台队列中执行,不会影响用户线程。

数据上传规则:

  • 可设置允许数据上传的网络环境;

  • 数据支持批量发送,可自定义发送批量和等待间隔;

================================

五. 一些关键实现

APM SDK使用方式和一般的UI库有很大不同,APM只需要启动一次即可生效,不需要修改代码。

例如:启动APM之后,用户使用NSURLConnection执行的网络请求就会在开始连接,得到响应,获取数据等时机被APM监控到, 而不需要使用者手动添加任何监控代码。

要实现这样的目标,Hooker需要使用一些比较特殊的的方式来实现,下面介绍Hooker使用到的解决方案:

1. 使用面向切面编程

APM需要将监控代码插入到系统实现中,完成系统消息的拦截和数据收集。

比如我们需要知道NSURLConnection在什么时候开始发送请求,就会监控-[NSURLConnection start]方法的调用时机, 这就需要在这个方法中插入APM的逻辑,要做到这一点,需要理解面向切面编程的思想。

Aspect Oriented Programming(AOP),面向切面编程,面对的是处理过程中的某个步骤或阶段。 在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面编程。 借助AOP,甚至不用修改一行代码,就可以修改现有程序的行为,非常高效。

AOP基本原理:将一个函数替换为一个新函数,新的函数中插入代码片段,然后执行原函数。

iOS开发中用到的库按照实现语言可以分为Objective-C和C两种,下面介绍这两种语言能够支持AOP的原理。

Objective-C对AOP的支持非常容易:

Runtime支持方法名和方法实现的分离,Objective-C的方法名类型是SEL,方法实现类型是IMP。

通过一个例子了解SEL和IMP的关系:

一个Objective-C方法[self setFilled:YES]完全可以用下面的代码代替:

所以通过修改SEL对应的IMP,可以方便地hook原方法。

Objective-C上的AOP有一个好听的名字: Method Swizzling 被灌醉的方法 ~~!

C的AOP涉及到程序较底层,比较少见,简单了解一下。

C函数指针的地址可以通过dlsym函数取得,如:

C的AOP需要查找到这个函数指针的地址,再使用新的函数指针替换原函数指针。

基于iOS动态链接器的符号绑定,查找函数指针地址的过程见下图:

这里借助了Facebook的一个开源库:fishhook 。

2. 使用代理模式采集回调消息的数据

通过AOP,可以监控指定类的指定方法了,我们可以取得方法调用的时机了, 但是程序中除了方法调用还存在方法回调,这是一种不适合用AOP监控的情况。

例如NSURLConnection的构造方法和start方法可以通过Method Swizzling监控到, 但是回调消息的接收者delegate的类名不固定,可能是任意一个页面实例, 如果还要使用Method Swizzling的方法来监控,会面对未知个数的页面的delegate方法,不是一个好办法。

解决方法是构造一个回调消息的转发者作为代理,在转发者中收集数据,再转发给用户。

下图演示对NSURLConnection的监控,MAM IMP就是被替换过的新的start方法的实现, ProxyDelegate就是消息转发者,负责将回调消息转发给delegate对象:

要实现一个Objective-C的代理,一定要注意以下问题:

  • 系统在向delegate发送消息时会调用-[NSObject respondsToSelector:]方法,所以ProxyDelegate需要重写此方法,才能正确地获取到回调消息;

  • ProxyDelegate没必要实现的用户delegate方法,如鉴权请求,需要借助Objective-C的动态特性,使用-[NSObject forwardInvocation:]方法将delegate能够响应的方法直接转发给用户的delegate;

ProxyDelegate中的forwardInvocation:方法实现:

不止NSURLConnection,CFNetwork的监控也使用了代理模式:

Proxy Stream 拦截read方法,记录stream读取成功的数据长度,再转发给Original Stream。

3. 借助桥接模式,从面向过程到面向对象的数据

CFNetwork是一个C语言实现的网络系统框架,虽然使用起来比较麻烦,但是可配置的功能更多,仍然被一些产品看好。

但是由于面向过程编程难以扩展的缺点,没有办法选择性地监控和http有关的CFReadStream,而不影响到来自文件或内存的CFReadStream;

虽然面向过程编程难以扩展,但是Objective-C支持从面向过程到面向对象的桥接:

Toll-Free Bridging, 它允许某些CoreFoundation类与其对应的Objective-C类互换使用,使我们在面向过程编程和面向对象编程两种编程思想中自由切换。

NeteaseAPM的策略:在系统构造http stream时,将一个NSInputStream的子类ProxyStream桥接为CFReadStream,返回给用户, 达到单独监控http stream的效果。

使用Toll-Free Bridging时需要重点关注两种编程思想切换时内存管理机制的不同引起的内存问题;

例如-[NSInputStream propertyForKey]方法和CFReadStreamCopyProperty函数是桥接的,但是它们内存管理方法不同,前者是自动管理,后者是手动管理,后者在调用之后需要使用者在随后手动调用CFRelease。

以ProxyStream举例,ProxyStream是CFReadStream的代理,负责管理http stream的数据收集。

如果用户对一个http stream执行如下调用:

ProxyStream的propertyForKey:方法会因为桥接而被调用,ProxyStream需要从original stream中获取正确的property,如果ProxyStream中这么写

那么这个方法返回的结果会被CFReadStreamCopyProperty随后的CFRelease函数错误地释放,引起内存异常。

正确的书写方式应该是:

ProxyStream只需要”转发”CFReadStreamCopyProperty函数给original stream就可以了。

4. 借助NSURLProtocol的UIWebView监控

NSURLProtocol是监控UIWebView请求最普遍的解决方案。

这里用户会产生多个URLProtocol会不会冲突的疑问?

结论是不会,因为即使用户注册了NSURLProtocol,拦截了NeteaseAPM的URLProtocol的请求, 但用户的URLProtocol发送请求时的NSURLConnection仍然会被NeteaseAPM监控到的;

UIWebView的请求比较复杂,下面收集几个容易出错的问题:

  • 某些网页的验证码的请求可能是一个阻塞主线程的请求,在URLProtocol的网络请求需要放在后台执行,否则会被阻塞住而无法得到响应。

  • 重定向的网页请求需要转发给网页,APM不能监控。

================================

六. 后续工作

  • 支持更多的性能数据的收集,如内存,CPU,帧率等;

  • 支持性能问题的自动诊断,如卡顿监控,Out of Memory问题诊断等,同时提高性能问题诊断的能力。

延伸阅读:

Method Swizzling:http://blog.csdn.net/yiyaaixuexi/article/details/9374411

fishhook:https://github.com/facebook/fishhook

QA环节

Q:可以细说一下验证码请求阻塞主线程的例子吗?

A:这个问题的现象是,UIWebView访问https://reg.163.com时,点击注册邮箱,填写完成后点击注册,界面卡住一段时间后无响应,没有跳转。

调查发现:UIWebView访问https://reg.163.com/services/checkSsnAll?isret=1&username=XXX 时,会在主线程使用ajax向服务器发送验证码请求。由于这个请求阻塞了主线程,NeteaseAPM的数据发送失败,导致这么问题。

Q:加入监控后性能影响多少?

A:这个没有具体测试过,不过QA曾经连续跑一个测试跑了两天,没有出现性能问题。

Q:监控数据上报,会缓存到本地,然后再上报么?会有流量问题么?

A:是的,数据上传使用了短码,并且压缩上传,一次上传的数据大小在60比特。并且可以设置上传的网络环境。流量问题可以通过各种方式解决,NeteaseAPM支持自定义数据发送方式,有些产品有免流量的通道,NeteaseAPM就把数据交给产品发送了。

Q:页面加载监控,hook那些方法?

A:页面监控的方法是viewDidLoad,viewDidAppear,viewDidDisappear等方法。如果页面过于复杂渲染时间长,页面的生命周期方法会正常执行的,可以准确地取得页面的状态,慢加载也是通过页面加载时间诊断的。

Q:流媒体能监控吗? 我尝试过oneAPM,用AVPlayerLayer实现的流媒体没法监控。

A:流媒体监控可以使用NSURLProtocol监控到,但是目前还没有针对这方面的测试。

Q:NSURLProtocol不能吧, 我记得oneAPM就是这么做的,然后我在后台看没监控到…

A:AVPlayerLayer的请求非常频繁,可能会被过滤掉,有同事反映,他们后来也选择了过滤掉AVPlayerLayer触发的请求。

Q:回调的信息数据,是程序退出后台的时候发送,还是回调以后就会发送统计服务器。

A:数据保存和数据发送是两个队列,数据保存在请求结束后执行,数据发送的规则是批量发送,支持自定义。

Q:开始介绍有说到可以 "支持页面加载时间的收集,检查出慢交互页面"。这里的加载时间只是请求发出数据到回来的时间,还是包括页面数据渲染时间在内,如果包括,是怎么无侵入的统计的,慢交互页面又是怎么统计的?

A:SDK端负责将网络请求和交互的时间点交给服务器,具体的分析由NeteaseAPM平台诊断,现在使用的规则应该是页面从didload到打开超过1秒就算是慢交互了。

Q:与其他平台的APM服务兼容性如何?都是同样的实现原理吗?

A:目前使用到的监控方式如AOP,代理都是兼容多次监控的,兼容性可以保证。CFNetwork的监控是不一样的。

Q:上传到平台的didload统计时长应该有对应的页面唯一标识吧,这个标记是在业务页面手动加的,还是hook页面方法自动加的,如果是自动加的,页面唯一标识是怎么在hook里传递记录的?

A:页面唯一标识保存在一个容器中,这个容器维护着页面引用和页面标识的对应关系,hook中需要根据页面查询页面标识。

Q:页面类字符串映射页面唯一标识么?

A:会使用页面的指针映射页面唯一标识,防止同时有多个同类页面。


感谢徐川对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

question by 李 冉慧

如果用户没有使用NSURLConnection怎么监控,还有如果只是想获取到建联时间,首字节到达时间,完成时间,一定要监控CFNetWork吗

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT