1. 程式人生 > >如何實現一個IOS網路監控元件

如何實現一個IOS網路監控元件

此文由作者朱志強授權網易雲社群釋出。


Mobile Application Monitor IOS元件設計技術分享

背景

應用程式效能管理Application Performance Management(APM)是近年來比較火的網際網路產業, Mobile Application Monitor(MAM)是其核心功能之一。 APM主要指對企業的關鍵業務應用進行監測、優化,它可以提高企業應用的可靠性和質量,保證使用者得到良好的服務,降低IT總擁有成本(TCO)。 一個企業的關鍵業務應用的效能強大,可以提高競爭力,並取得商業成功,因此,加強應用效能管理可以產生巨大商業利益。 目前成熟的產品有:
AppDynamics

 NewRelic.png Tingyun.png

目標

  • iOS客戶端的網路統計元件,用於統計iOS app的http請求的資料,如請求時間,資料,錯誤

  • 設計一個可複用的框架,方便後續新增幀率、使用者體驗等監測內容

  • 對應用的影響儘可能小,使用方便

設計模型

處理資料分4步:
資料收集,資料組裝,資料持久化,資料傳送
執行緒模型:
資料收集負責初始化MAMDataBuilder,在持久化層佇列完成資料組裝和資料庫插入操作。
滿足傳送資料條件時,首先持久化層佇列從資料庫查詢資料,然後在傳送層佇列中傳送資料,傳送結束後在持久化層佇列刪除該條資料,再處理下一個資料。
下圖使用圖形演示了程式執行過程,灰色矩形代表API介面
PosterPostRule.png

本文主要針對常用網路技術的攔截技術做全面細緻的講解和分析。

資料收集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



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

相關文章:
【推薦】 【0門檻】PR稿的自我修養
【推薦】 Android app如何加密?
【推薦】 Jmeter壓測Thrift服務介面