如何實現一個IOS網路監控元件
此文由作者朱志強授權網易雲社群釋出。
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
得到IMPvoid (*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服務介面