此文由作者朱志强授权网易云社区发布。

Mobile Application Monitor IOS组件设计技术分享

背景

应用程序性能管理Application Performance Management(APM)是近年来比较火的互联网产业, Mobile Application Monitor(MAM)是其核心功能之一。 APM主要指对企业的关键业务应用进行监测、优化,它可以提高企业应用的可靠性和质量,保证用户得到良好的服务,降低IT总拥有成本(TCO)。 一个企业的关键业务应用的性能强大,可以提高竞争力,并取得商业成功,因此,加强应用性能管理可以产生巨大商业利益。 目前成熟的产品有:
  

目标

  • iOS客户端的网络统计组件,用于统计iOS app的http请求的数据,如请求时间,数据,错误

  • 设计一个可复用的框架,方便后续添加帧率、用户体验等监测内容

  • 对应用的影响尽可能小,使用方便

设计模型

处理数据分4步:
数据收集,数据组装,数据持久化,数据发送
线程模型:
数据收集负责初始化MAMDataBuilder,在持久化层队列完成数据组装和数据库插入操作。
满足发送数据条件时,首先持久化层队列从数据库查找数据,然后在发送层队列中发送数据,发送结束后在持久化层队列删除该条数据,再处理下一个数据。
下图使用图形演示了程序执行过程,灰色矩形代表API接口

本文主要针对常用网络技术的拦截技术做全面细致的讲解和分析。

数据收集Hooker

针对IOS主要的网络技术:NSURLConnection和CFNetwork的HTTP请求做数据收集

NSURLConnection的hook

对Objective-C对象发送消息的拦截

  • 技术背景

    • Runtime
      Objective-C是一门运行时语言,它会尽可能地把代码执行的决策从编译和链接的时候,推迟到运行时。 这样对写代码带来很大的灵活性,比如说可以把消息转发给你想要的对象,或者随意交换一个方法的实现。 Method Swizzling正是使用交换方法实现的方式来达到hook的目的。

    • 动态绑定
      在编译的时候,我们不知道最终会执行哪一些代码,只有在执行的时候,通过selector去查询,我们才能确定具体的执行代码。
      Objective-C的方法类型是SEL(selector)。实例对象performSelector时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据 selector 去查找具体的方法实现(IMP), 然后用这个方法实现去执行具体的实现代码。

    • IMP类型
      IMP 是消息最终调用的执行代码的函数指针,可以理解为Objective-C的每个方法都会在编译时被转换成C函数,IMP就是这个C函数的函数指针,下面会演示调用这个IMP和调用Objective-C方法是等效的。 一个Objective-C方法:

      -(void)setFilled:(BOOL)arg;

      它的Objective-C调用方式会是:

      [aObject setFilled:YES];

      调用基类NSObject的方法- (IMP)methodForSelector:(SEL)aSelector得到IMP

      void (*setter)(id, SEL, BOOL);  
      setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];

      等价的C调用是对IMP(函数指针)的调用:

      setter(self, @selector(setFilled:), YES)
  • Method Swizzling
    正常情况,我们无法知道系统方法在何时被调用,但替换掉系统方法的代码实现,就可以获取系统方法的调用时机,这就是Method Swizzling!
    如下图,修改selector对应的IMP为保存原IMP的函数,这样就实现了对系统调用的hook。 

  • 代码演示
    Method Swizzling核心代码:

    BOOL HTSwizzleMethodAndStore(Class class, BOOL isClassMethod, SEL original, IMP replacement, IMP* store) {
      IMP imp = NULL;
      Method method ;  if (isClassMethod) {
          method= class_getClassMethod(class, original);
      }else{
          method= class_getInstanceMethod(class, original);
      }  if (method) {
          imp = method_setImplementation(method,(IMP)replacement);      if (!imp) {
              imp = method_getImplementation(method);
          }
      }else{
          MAMLog(@"%@:not found%@!!!!!!!!",NSStringFromClass(class),NSStringFromSelector(original));
      }  if (imp && store) { *store = imp; }//将原方法放在store中
      return (imp != NULL);
    }

    声明函数指针IMP store,实现函数MAM IMP

    static NSURLConnection * (*Original_connectionWithRequest)(id self,
                                                        SEL _cmd,                                                    NSURLRequest *request,                                                    id delegate);static NSURLConnection * MAM_connectionWithRequest(id self,
                                                          SEL _cmd,                                                      NSURLRequest *request,                                                      id delegate){  //使用系统方法的函数指针完成系统的实现
      id result = Original_connectionWithRequest(self,
                                            _cmd,
                                            request,
                                            hookDelegate);//在这里获取到了系统方法调用的时机
      return result;
    }

    在程序启动后调用Swizzling

    HTSwizzleMethodAndStore(NSClassFromString(@"NSURLConnection"),
                              YES,                          @selector(connectionWithRequest:delegate:),
                              (IMP)MAM_connectionWithRequest,
                              (IMP *)&Original_connectionWithRequest);
  • 对委托模型的监控
    Runtime替换方法时需要指定类名,而NSURLConnection的delegate的类并不确定。如果还是使用Method Swizzling拦截delegate的消息,每多一个使用NSURLConnectionDelegate的类都需要动态声明一次IMP store和MAM IMP,效率太低。
    解决办法是使用proxy delegate替换NSURLConnection原来的delegate。只要保证proxy delegate将所有接收到的网络回调,转发给原来的delegate就好了。 

CFNetwork的hook

对C函数调用的拦截

  • 技术背景

    • 使用Dynamic Loader hook 库函数 ---- fishhook
      Dynamic Loader (dyld)通过更新Mach-O文件中保存的指针的方法来绑定符号。借用它,可以在运行时修改C函数调用的函数指针!
      fishhook查找函数符号名的过程见下图

      上图中,1061是间接符号表(Indirect Symbol Table)的偏移量,存放的符号表(Symbol Table)偏移量16343。
      符号表中包含了字符表(String Table)偏移量,然后找到中真实符号名(Actual Symbol Name),fishhook对间接符号表的偏移量做了修改,这样就修改了字符表偏移量,指向字符表中的真实符号名发生了变化,最终,通过修改真实符号名修改了真实调用函数的指针,达到hook的目的。

    • Stream的read size和Toll-Free Bridge
      CFNetwork使用CFReadStreamRef做数据传递,其接收服务器响应的方式是使用回调函数。获取服务器数据的方式是,当回调函数收到流中有数据的通知后,从流中读取数据,保存在客户端内存中。
      对流的读取不适合使用修改字符串表的方式,这样做需要hook 系统也在使用的read函数,而系统的read函数不仅仅被网络请求的stream调用,还有所有的文件处理,并且hook一个频繁调用的函数也是不可取的!
      但是怎么才能只针对网络请求的stream做处理呢,对一个C类型真的是很难,但是倘若对一个对象而言,我们有很多办法可以用,能不能转换呢?
      能,用Toll-Free Bridge!有了它,就可以将CFReadStreamRef类型直接转换成NSInputStream对象!!
      Toll-Free Bridge可以将Cocoa对象转换为CoreFoundation类型,查看CFReadStreamRead源码:

      CFIndex CFReadStreamRead(CFReadStreamRef readStream, UInt8 *buffer, CFIndex bufferLength) {
      CF_OBJC_FUNCDISPATCH2(__kCFReadStreamTypeID, CFIndex, readStream, "read:maxLength:", buffer, bufferLength);

      函数的第一行调用的是Cocoa的方法read:maxLength:,这就确认了Toll-Free Bridge的实现机制——用Objective-C实现了一个可以用纯C调用的类库。
      最后,这样设计被监控的stream:
      这样就成功地将hook一个C函数的问题转变成了hook一个Objective-C方法的问题,但是,NSInputStream仍然是一个底层的公共类,仍然需要对系统的read方法做hook,能不能只针对某个stream对象进行hook呢?
      能,用Trampoline!

    • Objective-C消息转发机制和Trampoline ---- 对指定对象的hook
      当某个实例对象接收到一个消息,但是没有找到这个消息的实现时,会调用下面的两个方法,给开发者提供了转发消息的选择

      -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
      -(void)forwardInvocation:(NSInvocation *)anInvocation;

      借用转发机制,可以实现对指定对象的hook:
      设计一个继承自NSObject的Proxy类,持有一个NSInputStream,记为OriginalStream。
      使用上面的方法中将发向Proxy的消息转发给OriginalStream。这样一来,所有发向Proxy的消息的都由OriginalStream处理了。再重写NSInputStream read方法就可以获取到stream的size了。这种修改程序执行方向的设计就称为Trampoline,它的本意是蹦床,象征着将方法反弹给真正的接收对象。
      MAMNSStreamProxy的核心代码:

      -(instancetype)initWithClient:(id*)stream
      {if (self = ![super init])
      {
      _stream = ![stream retain];
      }return self;
      }
      -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
      {return ![_stream methodSignatureForSelector:aSelector];
      }
      -(void)forwardInvocation:(NSInvocation *)anInvocation
      {
      ![anInvocation invokeWithTarget:_stream];
      }
      -(NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len
      {
      NSInteger rv = [_stream read:buffer maxLength:len];//在这里记录sizereturn rv;
      }
  • 代码演示
    和Method Swizzling类似,需要声明函数指针和函数的实现:

    static CFReadStreamRef(*original_CFReadStreamCreateForHTTPRequest)(CFAllocatorRef alloc,
                                               CFHTTPMessageRef request);/**
    *  MAMNSInputStreamProxy持有original CFReadStreamRef,转发消息到original CFReadStreamRef,在方法 read 中获取数据大小。
    *  以original CFReadStreamRef为键,保存CFHTTPMessageRef request
    */static CFReadStreamRefMAM_CFReadStreamCreateForHTTPRequest(CFAllocatorRef alloc,
                                       CFHTTPMessageRef request){ //使用系统方法的函数指针完成系统的实现
     CFReadStreamRef originalCFStream = original_CFReadStreamCreateForHTTPRequest(alloc,
                                                                                  request); //将CFReadStreamRef转换成NSInputStream,保存在MAMNSInputStreamProxy中,返回的时候再转换成CFReadStreamRef
     NSInputStream *stream = (__bridge NSInputStream*)originalCFStream;
     MAMNSInputStreamProxy *outReadStream = ![![MAMNSInputStreamProxy alloc] initWithStream:stream];  /*内存管理, create的CF stream ref转成NS stream proxy,CF不再引用,使用结束后release掉*/
     CFRelease(originalCFStream); /*内存管理,ARC转交引用管理给CF*/
     CFReadStreamRef result = (__bridge_retained CFReadStreamRef)((id)outReadStream); return result;
    }

    使用fishhook替换函数地址

    save_original_symbols();int bFishHookWork = rebind_symbols((struct rebinding![1])
     {{"CFReadStreamCreateForHTTPRequest", MAM_CFReadStreamCreateForHTTPRequest},},1);
    void save_original_symbols(){
     original_CFReadStreamCreateForHTTPRequest = dlsym(RTLD_DEFAULT, "CFReadStreamCreateForHTTPRequest");
    }
  • 数据拦截模型
    根据CFNetwork API 的调用方式,使用fishhook和proxyStream获取C函数的设计模型如下:

更多网易技术、产品、运营经验分享请访问网易云社区

相关文章:
【推荐】 【0门槛】PR稿的自我修养
【推荐】 Android app如何加密?
【推荐】 Jmeter压测Thrift服务接口

如何实现一个IOS网络监控组件的更多相关文章

  1. iOS网络监控— BMReachability

    1. What's BMReachability? BMReachability是基于AFNetworking的Reachability类封装的监听网络状态变化的组件. 它在AF提供的无网络/wifi ...

  2. 自己动手写一个iOS 网络请求库的三部曲[转]

    代码示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary 开源项目:Pitaya,适合大 ...

  3. 一个ios的各种组件、代码分类,供参考

    http://github.ibireme.com/github/list/ios/#

  4. 对比iOS网络组件:AFNetworking VS ASIHTTPRequest(转载)

    在开发iOS应用过程中,如何高效的与服务端API进行数据交换,是一个常见问题.一般开发者都会选择一个第三方的网络组件作为服务,以提高开发效率和稳定性.这些组件把复杂的网络底层操作封装成友好的类和方法, ...

  5. 对比iOS网络组件:AFNetworking VS ASIHTTPRequest

    对比iOS网络组件:AFNetworking VS ASIHTTPRequest 作者 高嘉峻 发布于 2013年2月28日 | 7 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件 ...

  6. iOS中 WGAFN_网络监控 技术分享

    需要用到第三方AFNetworking/SVProgressHUD 没有的可以关注我微博私信我.http://weibo.com/hanjunqiang AppDelegate.m #import & ...

  7. 如何用 React Native 创建一个iOS APP?(三)

    前两部分,<如何用 React Native 创建一个iOS APP?>,<如何用 React Native 创建一个iOS APP (二)?>中,我们分别讲了用 React ...

  8. 如何用 React Native 创建一个iOS APP?(二)

    我们书接上文<如何用 React Native 创建一个iOS APP?>,继续来讲如何用 React Native 创建一个iOS APP.接下来,我们会涉及到很多控件. 1 AppRe ...

  9. 如何用 React Native 创建一个iOS APP?

    诚然,React Native 结合了 Web 应用和 Native 应用的优势,可以使用 JavaScript 来开发 iOS 和 Android 原生应用.在 JavaScript 中用 Reac ...

随机推荐

  1. 36. Valid Sudoku + 37. Sudoku Solver

    ▶ 有关数独的两个问题. ▶ 36. 检测当前盘面是否有矛盾(同一行.同一列或 3 × 3 的小框内数字重复),而不关心该盘面是否有解. ● 初版代码,24 ms,没有将格子检测函数独立出去,行检测. ...

  2. 读书笔记--大规模web服务开发技术

    总评        这本书是日本一个叫hatena的大型网站的CTO写的,通过hatena网站从小到大的演进来反应一个web系统从小到大过程中的各种系统和技术架构变迁,比较接地气.      书的内容 ...

  3. Oracle安装盘空间不足,对.DBF文件进行迁移

    一. select * from dba_data_files 使用该条语句可以查看当前库中有多少表空间并且DBF文件的存储位置 二. 找到对应的dbf文件,将该文件复制到你需要移动的位置 三. 开始 ...

  4. 迷你MVVM框架 avalonjs 1.2发布

    avalon1.2 带来了许多新特性,让开发更轻松!详见如下: 升级路由系统与分页组件. 对ms-duplex的绑定值进行增强,以前只能prop或prop.prop2,现在可以prop["x ...

  5. 如何用MaskBlt实现两个位图的合并,从而实现背景透明

    我有两个位图,一个前景图,一个背景图(mask用途).请问如何用MaskBlt实现两个位图的合并,从而实现背景透明! 核心代码:dcImage.SetBkColor(crColour);dcMask. ...

  6. Kafka学习总结

    Kafka学习总结 参考资料: 1.http://kafka.apachecn.org/, kafka官方文档 2.https://www.cnblogs.com/likehua/p/3999538. ...

  7. 使用heroku创建应用时报错 heroku does not appear to be a git repository

    在跟着heroku的官方教程创建python应用时,到deploy-the-app这一步,要上传代码到heroku 的git仓库时,报的这个错误: 网上一搜,相关的答案居然极少,首页只出现一篇(还好这 ...

  8. 插件 uploadify

    一.属性 属性名称 默认值 说明 auto true 设置为true当选择文件后就直接上传了,为false需要点击上传按钮才上传 . buttonClass ” 按钮样式 buttonCursor ‘ ...

  9. leetcode 62 不同的路径

    描述: m*n的矩阵,从左上角走到右下角,只能向下或向右走. 解决: 简单dp,dp[i][j]表示到i,j这点总共多少种路径. dp[i][j] = dp[i][j - 1] + dp[i - 1] ...

  10. Qt's Undo Framework

    Overview of Qt's Undo Framework Introduction Qt's Undo Framework is an implementation of the Command ...