如何實現一個IOS網絡監控組件

此文由做者朱志強受權網易雲社區發佈。html


Mobile Application Monitor IOS組件設計技術分享git

背景

應用程序性能管理Application Performance Management(APM)是近年來比較火的互聯網產業, Mobile Application Monitor(MAM)是其核心功能之一。 APM主要指對企業的關鍵業務應用進行監測、優化,它能夠提升企業應用的可靠性和質量,保證用戶獲得良好的服務,下降IT總擁有成本(TCO)。 一個企業的關鍵業務應用的性能強大,能夠提升競爭力,並取得商業成功,所以,增強應用性能管理能夠產生巨大商業利益。 目前成熟的產品有:
AppDynamics NewRelic.png Tingyun.pnggithub

目標

  • iOS客戶端的網絡統計組件,用於統計iOS app的http請求的數據,如請求時間,數據,錯誤數據庫

  • 設計一個可複用的框架,方便後續添加幀率、用戶體驗等監測內容服務器

  • 對應用的影響儘量小,使用方便網絡

設計模型

處理數據分4步:
數據收集,數據組裝,數據持久化,數據發送
線程模型:
數據收集負責初始化MAMDataBuilder,在持久化層隊列完成數據組裝和數據庫插入操做。
知足發送數據條件時,首先持久化層隊列從數據庫查找數據,而後在發送層隊列中發送數據,發送結束後在持久化層隊列刪除該條數據,再處理下一個數據。
下圖使用圖形演示了程序執行過程,灰色矩形表明API接口
PosterPostRule.pngapp

本文主要針對經常使用網絡技術的攔截技術作全面細緻的講解和分析。框架

數據收集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。 屏幕快照 2015-06-29 上午11.23.35.png

  • 代碼演示
    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就行了。 NSURLConnectionHook.png

CFNetwork的hook

對C函數調用的攔截

  • 技術背景

    • 使用Dynamic Loader hook 庫函數 ---- fishhook
      Dynamic Loader (dyld)經過更新Mach-O文件中保存的指針的方法來綁定符號。借用它,能夠在運行時修改C函數調用的函數指針!
      fishhook查找函數符號名的過程見下圖
      fishhook.png
      上圖中,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:
      CFReadStreamTollFridge1.png這樣就成功地將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函數的設計模型以下:
    CSReadStreamReadSize.png



更多網易技術、產品、運營經驗分享請訪問網易雲社區

相關文章:
【推薦】 如何解決在線網頁掛載本地樣式的問題
【推薦】 DDoS 攻擊與防護:從原理到實踐

相關文章
相關標籤/搜索