千人千面線上問題回放技術(shù)

導(dǎo)語

發(fā)布app后,開發(fā)者最頭疼的問題就是如何解決交付后的用戶側(cè)問題的還原和定位富弦,是業(yè)界缺乏一整套系統(tǒng)的解決方案的空白領(lǐng)域,閑魚技術(shù)團隊結(jié)合自己業(yè)務(wù)痛點提出一套全新的技術(shù)思路解決這個問題并在線上取得了比較滿意的實踐效果。

我們透過系統(tǒng)底層來捕獲ui事件流和業(yè)務(wù)數(shù)據(jù)的流動,并利用捕獲到的這些數(shù)據(jù)通過事件回放機制來復(fù)現(xiàn)線上的問題撬即。本文先介紹錄制和回放的整體框架,接著介紹里面涉及到的3個關(guān)鍵技術(shù)點呈队,也是這里最復(fù)雜的技術(shù)(模擬觸摸事件剥槐,統(tǒng)一攔截器實現(xiàn),統(tǒng)一hook block)

背景

現(xiàn)在的app基本都會提供用戶反饋問題的入口宪摧,然而提供給用戶反饋問題一般有兩種方式:

直接用文字輸入表達粒竖,或者截圖

直接錄制視頻反饋

這兩種反饋方式常常帶來以下抱怨:

用戶:輸入文字好費時費力

開發(fā)1:看不懂用戶反饋說的是什么意思?

開發(fā)2:大概看懂用戶說的是什么意思了几于,但是我線下沒辦法復(fù)現(xiàn)哈

開發(fā)3:看了用戶錄制的視頻蕊苗,但是我線下沒辦法重現(xiàn),也定位不到問題

所以:為了解決以上問題沿彭,我們用一套全新的思路來設(shè)計線上問題回放體系

線上問題回放體系的意義

用戶不需要輸入文字反饋問題朽砰,只需要重新操作一下app重現(xiàn)問題步驟即可

開發(fā)者拿到用戶反饋的問題腳本后,通過線下回放對問題一目了然喉刘,跟錄制視頻效果一樣瞧柔,是的,你沒看錯睦裳,就是跟看視頻一樣造锅。

通過腳本的回放實時獲取到app運行時相關(guān)數(shù)據(jù)(本地數(shù)據(jù),網(wǎng)絡(luò)數(shù)據(jù)廉邑,堆棧等等)哥蔚,

以便排查問題

為后續(xù)自動測試提供想象空間--你懂的

效果視頻

技術(shù)原理

1.app與外部環(huán)境的關(guān)系

問題回放1.png

從上面的關(guān)系圖可以看出,整個app的運行無非是用戶ui操作蛛蒙,然后觸發(fā)app從外界獲取數(shù)據(jù)糙箍,包括網(wǎng)絡(luò)數(shù)據(jù),gps數(shù)據(jù)等等宇驾,也包括從手機本地獲取數(shù)據(jù)倍靡,比如相冊數(shù)據(jù),機器數(shù)據(jù)课舍,系統(tǒng)等數(shù)據(jù)。

所以我們要實現(xiàn)問題回放只需要記錄用戶的UI操作和外界數(shù)據(jù)他挎,app自身數(shù)據(jù)即可筝尾。

app錄制 = 用戶的UI操作 + 外界數(shù)據(jù)(手機內(nèi)和手機外) + app自身數(shù)據(jù)

2.線上問題回放架構(gòu)由兩部分組成:錄制和回放

錄制是為回放服務(wù),錄制的信息越詳細(xì)办桨,回放成功率就越高筹淫,定位問題就越容易

錄制其實就是把ui和數(shù)據(jù)記錄下來,回放其實就是app自動

驅(qū)動UI操作并把錄制時的數(shù)據(jù)塞回相應(yīng)的地方呢撞。

3.錄制架構(gòu)圖

問題回放2.png

錄制流程

問題回放3.png

4.回放架構(gòu)圖

問題回放4.png

回放跟錄制框架圖基本一樣损姜,實際上錄制和回放的代碼是在一起饰剥,邏輯也是統(tǒng)一的,為了便于表達摧阅,我人為劃分成兩個架構(gòu)圖出來汰蓉。

回放的流程:

回放流程圖在這里省略

1.啟動app,點擊回放按鈕

2.引擎加載回放腳本

3.從腳本中解析出需要注冊的運行時事件并注冊棒卷,在回放里不需要業(yè)務(wù)上層來注冊事件顾孽,這里跟錄制是不一樣的。

4.從腳本中解析出需要注冊的靜態(tài)數(shù)據(jù)事件并注冊

5.從腳本中解析出需要播放的事件數(shù)據(jù)比规,并組成消費隊列

6.啟動播放器若厚,從消費隊列里讀取一個個事件來播放,如果是ui事件則直接播放蜒什,如果是靜態(tài)數(shù)據(jù)事件則直接按照指令要求替換數(shù)據(jù)值测秸,如果是非ui運行時事件則通過事件指令規(guī)則來確定是主動播放還是等待攔截對應(yīng)的事件,如果需要等待攔截對應(yīng)的事件灾常,則播放器會一直等待此事件直到此事件被app消費掉為止乞封。只有此事件被消費了,播放器才能播放下一個事件岗憋。

7.當(dāng)攔截到被注冊的事件后肃晚,根據(jù)此事件指令要求把相應(yīng)的數(shù)據(jù)塞到相應(yīng)的字段里

8.跳回6繼續(xù)運行,直到消費隊列里的事件被消費完

注意:回放每個事件時會實時自動打印出相應(yīng)的堆棧信息和事件數(shù)據(jù)仔戈,有利于排查問題

關(guān)鍵技術(shù)介紹

1.模擬觸摸事件

從ui事件數(shù)據(jù)解中析出被觸摸的view关串,以及此view所在的視圖樹中的層級關(guān)系,并在當(dāng)前回放界面上查找到對應(yīng)的view监徘,然后往該view上發(fā)送ui操作事件(點擊晋修,雙擊等等),并帶上觸摸事件的坐標(biāo)信息凰盔,其實這里是模擬觸摸事件墓卦。

我們先來介紹觸摸事件的處理流程

等待觸摸階段

手機屏幕處于待機狀態(tài),等待觸摸事件發(fā)生

手指開始觸摸屏幕

系統(tǒng)反應(yīng)階段

屏幕感應(yīng)器接收到觸摸户敬,并將觸摸數(shù)據(jù)傳給系統(tǒng)IOKit(IOKit是蘋果的硬件驅(qū)動框架)

系統(tǒng)IOKit封裝該觸摸事件為IOHIDEvent對象

接著系統(tǒng)IOKit把IOHIDEvent對象轉(zhuǎn)發(fā)給SpringBoard進程

SpringBoard進程就是iOS的系統(tǒng)桌面落剪,它存在于iDevice的進程中,不可清除尿庐,它的運行原理與Windows中的explorer.exe系統(tǒng)進程相類似忠怖。它主要負(fù)責(zé)界面管理,所以只有它才知道當(dāng)前觸摸到底有誰來響應(yīng)。

SpringBoard接收階段

SpringBoard收到IOHIDEvent消息后抄瑟,觸發(fā)runloop中的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()方法凡泣。

SpringBoard開始查詢前臺是否存在正在運行的app,如果存在,則SpringBoard通過進程通信方式把此觸摸事件轉(zhuǎn)發(fā)給前臺當(dāng)前app鞋拟,如果不存在骂维,則SpringBoard進入其自己內(nèi)部響應(yīng)過程。

app處理階段

前臺app主線程Runloop收到SpringBoard轉(zhuǎn)發(fā)來的消息贺纲,并觸發(fā)對應(yīng)runloop 中的Source1回調(diào)_UIApplicationHandleEventQueue()航闺。

_UIApplicationHandleEventQueue()把IOHIDEvent處理包裝成UIEvent進行處理分發(fā)

Soucre0回調(diào)內(nèi)部UIApplication的sendEvent:方法,將UIEvent傳給UIWindow

在UIWindow為根節(jié)點的整棵視圖樹上通過hitTest(_:with:)和point(inside:with:)這兩個方法遞歸查找到合適響應(yīng)這個觸摸事件的視圖哮笆。

找到最終的葉子節(jié)點視圖后来颤,就開始觸發(fā)此視圖綁定的相應(yīng)事件,比如跳轉(zhuǎn)頁面等等稠肘。

從上面觸摸事件處理過程中我們可以看出要錄制ui事件只需要在app處理階段中的UIApplication sendEvent方法處截獲觸摸數(shù)據(jù)福铅,回放時也是在這里把觸摸模擬回去。

下面是觸摸事件錄制的代碼项阴,就是把UITouch相應(yīng)的數(shù)據(jù)保存下來即可

這里有一個關(guān)鍵點滑黔,需要把touch.timestamp的時間戳記錄下來,以及把當(dāng)前touch事件距離上一個touch事件的時間間隔記錄下來环揽,因為這個涉及到觸摸引起慣性加速度問題略荡。比如我們平時滑動列表視圖時,手指離開屏幕后歉胶,列表視圖還要慣性地滑動一小段時間预明。

- (void)handleUIEvent:(UIEvent*)event{if(!self.isEnabled)return;if(event.type !=UIEventTypeTouches)return;NSSet*allTouches = [event allTouches];UITouch*touch = (UITouch*)[allTouches anyObject];if(touch.view) {if(self.filter && !self.filter(touch.view)) {return;? ? ? ? }? ? }switch(touch.phase) {caseUITouchPhaseBegan:? ? ? ? {self.machAbsoluteTime = mach_absolute_time();self.systemStartUptime = touch.timestamp;self.tuochArray = [NSMutableArrayarray];? ? ? ? ? ? [selfrecordTouch:touch click:self.machAbsoluteTime];break;? ? ? ? }caseUITouchPhaseStationary:? ? ? ? {? ? ? ? ? ? [selfrecordTouch:touch click:mach_absolute_time()];break;? ? ? ? }caseUITouchPhaseCancelled:? ? ? ? {? ? ? ? ? ? [selfrecordTouch:touch click:mach_absolute_time()];? ? ? ? ? ? [[NSNotificationCenterdefaultCenter] postNotificationName:@"notice_ui_test"object:self.tuochArray];break;? ? ? ? }caseUITouchPhaseEnded:? ? ? ? {? ? ? ? ? ? [selfrecordTouch:touch click:mach_absolute_time()];? ? ? ? ? ? [[NSNotificationCenterdefaultCenter] postNotificationName:@"notice_ui_test"object:self.tuochArray];break;? ? ? ? }caseUITouchPhaseMoved:? ? ? ? {? ? ? ? ? ? [selfrecordTouch:touch click:mach_absolute_time()];? ? ? ? }default:break;? ? }}- (NSDictionary*)recordTouch:(UITouch*)touch? ? ? ? ? ? ? ? ? ? ? ? click:(uint64_t)curClick{NSDictionary*dic = [NSMutableDictionarydictionary];NSString*name =NSStringFromClass([touch.viewclass]);? ? [dic setValue:name forKey:@"viewClass"];NSString*frame =NSStringFromCGRect(touch.view.frame);? ? [dic setValue:frame forKey:@"viewframe"];? ? [dic setValue:[NSNumbernumberWithInteger:touch.phase] forKey:@"phase"];? ? [dic setValue:[NSNumbernumberWithInteger:touch.tapCount] forKey:@"tapCount"];? ? uint64_t click = curClick -self.machAbsoluteTime;? ? [dic setValue:[NSNumbernumberWithUnsignedLongLong:click] forKey:@"click"];UIWindow*touchedWindow = [UIWindowwindowOfView:touch.view];CGPointlocation = [touch locationInView:touchedWindow];//? ? if(CGPointEqualToPoint(location, self.perlocation))//? ? {//? ? ? ? return nil;//? ? }NSString*pointStr =NSStringFromCGPoint(location);? ? [dic setValue:pointStr forKey:@"LocationInWindow"];NSTimeIntervaltimestampGap = touch.timestamp -self.systemStartUptime;? ? [dic setValue:[NSNumbernumberWithDouble:timestampGap] forKey:@"timestamp"];//? ? self.perlocation = location;[self.tuochArray addObject:dic];returndic;}

我們來看一下代碼怎么模擬單擊觸摸事件(為了容易理解汁尺,我把有些不是關(guān)鍵斩熊,復(fù)雜的代碼已經(jīng)去掉)

接著我們來看一下模擬觸摸事件代碼

一個基本的觸摸事件一般由三部分組成:

1.UITouch對象 - 將用于觸摸

2.第一個UIEvent Began觸摸

3.第二個UIEvent Ended觸摸

實現(xiàn)步驟:

1.代碼的前面部分都是一些UITouch和UIEvent私有接口娇妓,私有變量字段,由于蘋果并不公開它們辫塌,為了讓其編譯不報錯漏策,所以我們需要把這些字段包含進來,回放是在線下臼氨,所以不必?fù)?dān)心私有接口被拒的事情掺喻。

2.構(gòu)造觸摸對象:UITouch和UIEvent,把記錄對應(yīng)的字段值塞回相應(yīng)的字段储矩。塞回去就是用私有接口和私有字段

3.觸摸的view位置轉(zhuǎn)換為Window坐標(biāo)感耙,然后往app里發(fā)送事件

[[UIApplication sharedApplication]sendEvent:event];

4.要回放這些觸摸事件,我們需要把他丟到CADisplayLink里面來執(zhí)行

////? SimulationTouch.m////? Created by 詩壯殷 on 2018/5/15.//#import"SimulationTouch.h"#import<objc/runtime.h>#include<mach/mach_time.h>#ifdef __LP64__typedefdoubleIOHIDFloat;#elsetypedeffloatIOHIDFloat;#endiftypedefUInt32IOOptionBits;typedefstruct__IOHIDEvent *IOHIDEventRef;@interfaceUITouch(replay)- (void)_setIsFirstTouchForView:(BOOL)first;- (void)setIsTap:(BOOL)isTap;- (void)_setLocationInWindow:(CGPoint)location resetPrevious:(BOOL)reset;- (void)setPhase:(UITouchPhase)phase;- (void)setTapCount:(NSUInteger)tapCount;- (void)setTimestamp:(NSTimeInterval)timestamp;- (void)setView:(UIView*)view;- (void)setWindow:(UIWindow*)window;- (void)_setHidEvent:(IOHIDEventRef)event;@end@implementationUITouch(replay)- (id)initPoint:(CGPoint)point window:(UIWindow*)window{NSParameterAssert(window);self= [superinit];if(self) {? ? ? ? [selfsetTapCount:1];? ? ? ? [selfsetIsTap:YES];? ? ? ? [selfsetPhase:UITouchPhaseBegan];? ? ? ? [selfsetWindow:window];? ? ? ? [self_setLocationInWindow:point resetPrevious:YES];? ? ? ? [selfsetView:[window hitTest:point withEvent:nil]];? ? ? ? [self_setIsFirstTouchForView:YES];? ? ? ? [selfsetTimestamp:[[NSProcessInfoprocessInfo] systemUptime]];? ? }returnself;}@end@interfaceUIInternalEvent:UIEvent- (void)_setHIDEvent:(IOHIDEventRef)event;@end@interfaceUITouchesEvent:UIInternalEvent- (void)_addTouch:(UITouch*)touch forDelayedDelivery:(BOOL)delayedDelivery;- (void)_clearTouches;@end@interfaceUIApplication(replay)- (BOOL)_isSpringBoardShowingAnAlert;- (UIWindow*)statusBarWindow;- (void)pushRunLoopMode:(NSString*)mode;- (void)pushRunLoopMode:(NSString*)mode requester:(id)requester;- (void)popRunLoopMode:(NSString*)mode;- (void)popRunLoopMode:(NSString*)mode requester:(id)requester;- (UITouchesEvent*)_touchesEvent;@endtypedefenum{? ? kIOHIDDigitizerEventRange =0x00000001,? ? kIOHIDDigitizerEventTouch =0x00000002,? ? kIOHIDDigitizerEventPosition =0x00000004,} IOHIDDigitizerEventMask;IOHIDEventRef IOHIDEventCreateDigitizerFingerEvent(CFAllocatorRefallocator,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? AbsoluteTime timeStamp,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? uint32_t index,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? uint32_t identity,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOHIDDigitizerEventMask eventMask,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOHIDFloat x,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOHIDFloat y,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOHIDFloat z,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOHIDFloat tipPressure,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOHIDFloat twist,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Boolean range,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Boolean touch,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOOptionBits options);@implementationSimulationTouch- (void)performTouchInView:(UIView*)view start:(bool)start{UIWindow*_window = view.window;CGRectfInWindow;if([view isKindOfClass:[UIWindowclass]])? ? {? ? ? ? fInWindow = view.frame;? ? }else{? ? ? ? fInWindow = [_window convertRect:view.frame fromView:view.superview];? ? }CGPointpoint =CGPointMake(fInWindow.origin.x + fInWindow.size.width/2,? ? ? ? ? ? ? ? fInWindow.origin.y + fInWindow.size.height/2);if(start)? ? {self.touch = [[UITouchalloc] initPoint:point window:_window];? ? ? ? [self.touch setPhase:UITouchPhaseBegan];? ? }else{? ? ? ? [self.touch _setLocationInWindow:point resetPrevious:NO];? ? ? ? [self.touch setPhase:UITouchPhaseEnded];? ? }CGPointcurrentTouchLocation = point;UITouchesEvent*event = [[UIApplicationsharedApplication] _touchesEvent];? ? [event _clearTouches];? ? uint64_t machAbsoluteTime = mach_absolute_time();? ? AbsoluteTime timeStamp;? ? timeStamp.hi = (UInt32)(machAbsoluteTime >>32);? ? timeStamp.lo = (UInt32)(machAbsoluteTime);? ? [self.touch setTimestamp:[[NSProcessInfoprocessInfo] systemUptime]];? ? IOHIDDigitizerEventMask eventMask = (self.touch.phase ==UITouchPhaseMoved)? ? ? kIOHIDDigitizerEventPosition? ? : (kIOHIDDigitizerEventRange | kIOHIDDigitizerEventTouch);? ? Boolean isRangeAndTouch = (self.touch.phase !=UITouchPhaseEnded);? ? IOHIDEventRef hidEvent = IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? timeStamp,0,2,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? eventMask,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? currentTouchLocation.x,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? currentTouchLocation.y,0,0,0,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? isRangeAndTouch,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? isRangeAndTouch,0);if([self.touch respondsToSelector:@selector(_setHidEvent:)]) {? ? ? ? [self.touch _setHidEvent:hidEvent];? ? }? ? [event _setHIDEvent:hidEvent];? ? [event _addTouch:self.touch forDelayedDelivery:NO];? ? [[UIApplicationsharedApplication] sendEvent:event];}@end

怎樣調(diào)用私有接口椰苟,以及使用哪些私有接口抑月,這點不需要再解釋了,如果感興趣舆蝴,請關(guān)注我們公眾號,后續(xù)我專門寫篇文章來揭露這方面的技術(shù),總的來說就下載蘋果提供觸摸事件的源碼庫洁仗,分析源碼层皱,然后設(shè)置斷掉調(diào)試,甚至反匯編來理解觸摸事件的原理赠潦。

2.統(tǒng)一攔截器

錄制和回放都居于事件流來處理的叫胖,而數(shù)據(jù)的事件流其實就是對一些關(guān)鍵方法的hook,由于我們?yōu)榱吮WC對業(yè)務(wù)代碼無侵入和擴展性(隨便注冊事件)她奥,我們需要對所有方法統(tǒng)一hook瓮增,所有的方法由同一個鉤子來響應(yīng)處理。如下圖所示

image

這個鉤子是用用匯編編寫哩俭,由于匯編代碼比較多绷跑,而且比較難讀懂,所以這里暫時不附上源碼凡资,匯編層主要把硬件里面的一些數(shù)據(jù)統(tǒng)一讀取出來砸捏,比如通用寄存器數(shù)據(jù)和浮點寄存器數(shù)據(jù),堆棧信息等等,甚至前面的前面的方法參數(shù)都可以讀取出來隙赁,最后轉(zhuǎn)發(fā)給c語言層處理垦藏。

匯編層把硬件相關(guān)信息組裝好后調(diào)用c層統(tǒng)一攔截接口,匯編層是為c層服務(wù)伞访。c層無法讀取硬件相關(guān)信息掂骏,所以這里只能用匯編來讀取。c層接口通過硬件相關(guān)信息定位到當(dāng)前的方法是屬于哪個事件厚掷,知道了事件弟灼,也意味著知道了事件指令,知道了事件指令蝗肪,也知道了哪些字段需要塞回去袜爪,也知道了被hook的原始方法。

c層代碼介紹如下:

由于是統(tǒng)一調(diào)用這個攔截器薛闪,所以攔截器并不知道當(dāng)前是哪個業(yè)務(wù)代碼執(zhí)行過來的辛馆,也不知道當(dāng)前這個業(yè)務(wù)方法有多少個參數(shù),每個參數(shù)類型是什么等等豁延,這個接口代碼處理過程大概如下

通過寄存器獲取對象self

通過寄存器獲取方法sel

通過self和sel獲取對應(yīng)的事件指令

通過事件指令回調(diào)上層來決定是否往下執(zhí)行

獲取需要回放該事件的數(shù)據(jù)

把數(shù)據(jù)塞回去昙篙,比如塞到某個寄存器里,或者塞到某個寄存器所指向的對象的某個字段等等

如果需要立即回放則調(diào)用原來被hook的原始方法诱咏,如果不是立即回放苔可,則需要把現(xiàn)場信息保存起來,并等待合適的時機由播放隊列來播放(調(diào)用)

//xRegs 表示統(tǒng)一匯編器傳入當(dāng)前所有的通用寄存器數(shù)據(jù)袋狞,它們地址存在一個數(shù)組指針里//dRegs 表示統(tǒng)一匯編器傳入當(dāng)前所有的浮點寄存器數(shù)據(jù)焚辅,它們地址也存在一個數(shù)組指針里//dRegs 表示統(tǒng)一匯編器傳入當(dāng)前堆棧指針//fp? 表示調(diào)用棧幀指針voidreplay_entry_start(void* xRegs,void* dRegs,void* spReg, CallBackRetIns *retIns,StackFrame *fp,void*con_stub_lp){void*objAdr = (((void**)xRegs)[0]);//獲取對象本身self或者block對象本身EngineManager *manager = [EngineManager sharedInstance];? ? ReplayEventIns *node = [manager getEventInsWithBlock:objAdr];idobj = (__bridgeid)objAdr;void*xrArg = ((void**)xRegs)+2;if(nil== node)? ? {? ? ? ? SEL selecter = (SEL)(((void**)xRegs)[1]);//對應(yīng)的對象調(diào)用的方法Class tclass = [objclass];//object_getClass(obj);object_getClass方法只能通過對象獲取它的類映屋,不能傳入class 返回class本身,do{? ? ? ? ? ? node = [manager getEventIns:tclass sel:selecter];//通過對象和方法獲取對應(yīng)的事件指令節(jié)點}while(nil== node && (tclass = class_getSuperclass(tclass)));? ? }else{? ? ? ? xrArg = ((void**)xRegs)+1;? ? }? ? assert(node &&"node is nil in replay_call_start");//回調(diào)通知上層當(dāng)前回放是否打斷if(node.BreakCurReplayExe && node.BreakCurReplayExe(obj,node,xrArg,dRegs))? ? {? ? ? ? retIns->nodeAddr =NULL;? ? ? ? retIns->recordOrReplayData =NULL;? ? ? ? retIns->return_address =NULL;return;? ? }boolneedReplay =true;//回調(diào)通知上層當(dāng)前即將回放該事件if(node.willReplay)? ? {? ? ? ? needReplay = (*(node.willReplay))(obj,node,xrArg,dRegs);? ? }if(needReplay)? ? {? ? ? ? ReplayEventData *replayData =nil;if(node.getReplayData)? ? ? ? {//獲取回放該事件對應(yīng)的數(shù)據(jù)replayData = (*(node.getReplayData))(obj,node,xrArg,dRegs);? ? ? ? }else//默認(rèn)獲取方法{? ? ? ? ? ? replayData = [manager getNextReplayEventData:node];? ? ? ? }//以下就是真正的回放同蜻,即是把數(shù)據(jù)塞回去棚点,并調(diào)用原來被hook的方法if(replayData)? ? ? ? {if(replay_type_intercept_call == node.replayType)? ? ? ? ? ? {? ? ? ? ? ? ? ? sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);NSArray*arglist = fetchAllArgInReplay(xRegs, dRegs, spReg, node);? ? ? ? ? ? ? ? ReplayInvocation *funobj = [[ReplayInvocation alloc] initWithFunPtr:node.callBack ? node.callBack : [node getOrgFun]? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? args:arglist? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? argType:[node getFunTypeStr]? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? retType:rf_return_type_v];if([[EngineManager sharedInstance] setRepalyEventReady:replayData funObj:funobj])? ? ? ? ? ? ? ? {//放到播放隊列里播放,返回沒調(diào)用地址湾蔓,讓其不往下走retIns->return_address =NULL;return;? ? ? ? ? ? ? ? }? ? ? ? ? ? }else{//塞數(shù)據(jù)sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);? ? ? ? ? ? }? ? ? ? }? ? ? ? retIns->nodeAddr = (__bridgevoid*)node;? ? ? ? retIns->recordOrReplayData = (__bridgevoid*)replayData;? ? ? ? retIns->return_address = node.callBack ? node.callBack : [node getOrgFun];? ? ? ? replayData.runStatus = relay_event_run_status_runFinish;? ? }else{? ? ? ? retIns->nodeAddr =NULL;? ? ? ? retIns->recordOrReplayData =NULL;? ? ? ? retIns->return_address = [node getOrgFun];? ? }}

3.怎樣統(tǒng)一hook block

如果你只是想大概理解block的底層技術(shù)瘫析,你只需google一下即可。

如果你想全面深入的理解block底層技術(shù)默责,那網(wǎng)上的那些資料遠(yuǎn)遠(yuǎn)滿足不了你的需求贬循。

只能閱讀蘋果編譯器clang源碼和列出比較有代表性的block例子源碼,然后轉(zhuǎn)成c語言和匯編桃序,通過c語言結(jié)合匯編研究底層細(xì)節(jié)杖虾。

何謂oc block

block就是閉包,跟回調(diào)函數(shù)callback很類似葡缰,閉包也是對象

blcok的特點: 1.可有參數(shù)列表

2.可有返回值

3.有方法體

4.capture上下文變量

5.有對象引用計數(shù)的內(nèi)存管理策略(block生命周期)

block的一般存儲在內(nèi)存中形態(tài)有三種 _NSConcretStackBlock(棧)_NSConcretGlobalBlock(全局)_NSConcretMallocBlock(堆)

系統(tǒng)底層怎樣表達block

我們先來看一下block的例子:

void test(){? ? __block int var1 =8;//上下文變量NSString *var2 = @"我是第二個變量”; //上下文變量

? ? void (^block)(int) = ^(int arg)//參數(shù)列表

? ? {

? ? ? ? var1 = 6;

? ? ? ? NSLog(@"arg = %d,var1 = %d, var2 = %@", arg, var1, var2);

? ? };

? ? block(1);//調(diào)用block語法

? ? dispatch_async(dispatch_get_global_queue(0, 0), ^

? ? {

? ? ? ? block(2); //異步調(diào)用block

? ? });

}

這段代碼首先定義兩個變量亏掀,接著定義一個block,最后調(diào)用block泛释。

兩個變量:這兩個變量都是被block引用滤愕,第一個變量有關(guān)鍵字__block,表示可以在block里對該變量賦值怜校,第二個變量沒有__block關(guān)鍵字间影,在block里只能讀,不能寫茄茁。

兩個調(diào)用block的語句:第一個直接在當(dāng)前方法test()里調(diào)用魂贬,此時的block內(nèi)存數(shù)據(jù)在棧上,第二個是異步調(diào)用裙顽,就是說當(dāng)執(zhí)行block(2)時test()可能已經(jīng)運行完了付燥,test()調(diào)用棧可能已經(jīng)被銷毀愈犹。那這種情況block的數(shù)據(jù)肯定不能在棧上键科,只能在堆上或者在全局區(qū)。

系統(tǒng)底層表達block比較重要的幾種數(shù)據(jù)結(jié)構(gòu)如下:

注意:雖然底層是用這些結(jié)構(gòu)體來表達block漩怎,但是它們并不是源碼勋颖,是二進制代碼

enum{? ? BLOCK_REFCOUNT_MASK =? ? (0xffff),? ? BLOCK_NEEDS_FREE =? ? ? ? (1<<24),? ? BLOCK_HAS_COPY_DISPOSE =? (1<<25),? ? BLOCK_HAS_CTOR =? ? ? ? ? (1<<26),//todo == BLOCK_HAS_CXX_OBJ?BLOCK_IS_GC =? ? ? ? ? ? (1<<27),? ? BLOCK_IS_GLOBAL =? ? ? ? (1<<28),? ? BLOCK_HAS_DESCRIPTOR =? ? (1<<29),//todo == BLOCK_USE_STRET勋锤?BLOCK_HAS_SIGNATURE? =? ? (1<<30),? ? OBLOCK_HAS_EXTENDED_LAYOUT = (1<<31)};enum{? ? BLOCK_FIELD_IS_OBJECT? =3,? ? BLOCK_FIELD_IS_BLOCK? ? =7,? ? BLOCK_FIELD_IS_BYREF? ? =8,? ? OBLOCK_FIELD_IS_WEAK? ? =16,? ? OBLOCK_BYREF_CALLER? ? ? =128};typedefstructblock_descriptor_head{unsignedlongintreserved;unsignedlongintsize;//表示主體block結(jié)構(gòu)體的內(nèi)存大小}block_descriptor_head;typedefstructblock_descriptor_has_help{unsignedlongintreserved;unsignedlongintsize;//表示主體block結(jié)構(gòu)體的內(nèi)存大小void(*copy)(void*dst,void*src);//當(dāng)block被retain時會執(zhí)行此函數(shù)指針void(*dispose)(void*);//block被銷毀時調(diào)用structblock_arg_var_descriptor*argVar;}block_descriptor_has_help;typedefstructblock_descriptor_has_sig{unsignedlongintreserved;unsignedlongintsize;constchar*signature;//block的簽名信息structblock_arg_var_descriptor*argVar;}block_descriptor_has_sig;typedefstructblock_descriptor_has_all{unsignedlongintreserved;unsignedlongintsize;void(*copy)(void*dst,void*src);void(*dispose)(void*);constchar*signature;structblock_arg_var_descriptor*argVar;}block_descriptor_has_all;typedefstructblock_info_1{void*isa;//表示當(dāng)前blcok是在堆上還是在棧上饭玲,或在全局區(qū)_NSConcreteGlobalBlockintflags;//對應(yīng)上面的enum值,這些枚舉值是我從編譯器源碼拷貝過來的intreserved;void(*invoke)(void*, ...);//block對應(yīng)的方法體(執(zhí)行體叁执,就是代碼段)void*descriptor;//此處指向上面幾個結(jié)構(gòu)體中的一個茄厘,具體哪一個根據(jù)flags值來定矮冬,它用來進一步來描述block信息//從這個字段開始起,后面的字段表示的都是此block對外引用的變量蚕断。NSString *var2;? ? byref_var1_1 var1; } block_info_1;

這個例子中的block在底層表達大概如下圖:

image

首先用block_info_1來表達block本身欢伏,然后用block_desc_1來具體描述block相關(guān)信息(比如block_info_1結(jié)構(gòu)體大小入挣,在堆上還是在棧上亿乳?copy或dispose時調(diào)用哪個方法等等),然而block_desc_1具體是哪個結(jié)構(gòu)體是由block_info_1中flags字段來決定的径筏,block_info_1里的invoke字段是指向block方法體葛假,即是代碼段。block的調(diào)用就是執(zhí)行這個函數(shù)指針滋恬。由于var1是可寫的聊训,所以需要設(shè)計一個結(jié)構(gòu)體(byref_var1_1)來表達var1,為什么var2直接用他原有的類型表達恢氯,而var1要用結(jié)構(gòu)體來表達带斑。篇幅有限,這個自己想想吧勋拟?

block小結(jié)

為了表達block勋磕,底層設(shè)計三種結(jié)構(gòu)體:block_info_1,block_desc_1敢靡,byref_var1_1挂滓,三種函數(shù)指針: block invoke方法體,copy方法啸胧,dispose方法

其實表達block是非常復(fù)雜的赶站,還涉及到block的生命周期,內(nèi)存管理問題等等纺念,我在這里只是簡單的貫穿主流程來介紹的贝椿,很多細(xì)節(jié)都沒介紹。

怎樣統(tǒng)一hook block

通過上面的分析陷谱,得知oc里的block就是一個結(jié)構(gòu)體指針烙博,所以我在源碼里可以直接把它轉(zhuǎn)成結(jié)構(gòu)體指針來處理。

統(tǒng)一hook block源碼如下

VoidfunBlock createNewBlock(VoidfunBlock orgblock, ReplayEventIns *blockEvent,boolisRecord){if(orgblock && blockEvent)? ? {? ? ? ? VoidfunBlock newBlock = ^(void)? ? ? ? {? ? ? ? ? ? orgblock();if(nil== blockEvent)? ? ? ? ? ? {? ? ? ? ? ? ? ? assert(0);? ? ? ? ? ? }? ? ? ? };? ? ? ? trace_block_layout *blockLayout = (__bridge trace_block_layout *)newBlock;? ? ? ? blockLayout->invoke = (void(*)(void*, ...))(isRecord?hook_var_block_callBack_record:hook_var_block_callBack_replay);returnnewBlock;? ? }returnnil;}

我們首先新建一個新的block newBlock叭首,然后把原來的block orgblock 和 事件指令blockEvent包到新的blcok中习勤,這樣達到引用的效果。然后把新的block轉(zhuǎn)成結(jié)構(gòu)體指針焙格,并把結(jié)構(gòu)體指針中的字段invoke(方法體)指向統(tǒng)一回調(diào)方法图毕。你可能詫異新的block是沒有參數(shù)類型的,原來block是有參數(shù)類型眷唉,外面調(diào)用原來block傳遞參數(shù)時會不會引起crash予颤?答案是否定的囤官,因為這里構(gòu)造新的block時 我們只用block數(shù)據(jù)結(jié)構(gòu),block的回調(diào)方法字段已經(jīng)被閹割蛤虐,回調(diào)方法已經(jīng)指向統(tǒng)一方法了党饮,這個統(tǒng)一方法可以接受任何類型的參數(shù),包括沒有參數(shù)類型驳庭。這個統(tǒng)一方法也是匯編實現(xiàn)刑顺,代碼實現(xiàn)跟上面的匯編層代碼類似,這里就不附上源碼了饲常。

那怎樣在新的blcok里讀取原來的block和事件指令對象呢蹲堂?

代碼如下:

voidvar_block_callback_start_record(trace_block_layout * blockLayout){? ? VoidfunBlock orgBlock = (__bridge VoidfunBlock)(*((void**)((char*)blockLayout +sizeof(trace_block_layout))));? ? ReplayEventIns *node = (__bridge ReplayEventIns *)(*((void**)((char*)blockLayout +40)));}

總結(jié)

本文大概介紹了問題回放框架,接著介紹三個關(guān)鍵技術(shù)贝淤。這三個技術(shù)相對比較深入柒竞,估計很多讀者理解起來比較費勁,請諒解播聪!

如果對里面的技術(shù)點感興趣朽基,你可以關(guān)注我們的公眾號。我們后續(xù)會單獨對里面的技術(shù)點詳細(xì)深入的分析發(fā)文离陶。

如果覺得上面有錯誤的地方稼虎,請指出。謝謝```js

本文作者:閑魚技術(shù)

作者:阿里云云棲社區(qū)

鏈接:http://www.reibang.com/p/ed2f17d9471b

來源:簡書

簡書著作權(quán)歸作者所有枕磁,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處渡蜻。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市计济,隨后出現(xiàn)的幾起案子茸苇,更是在濱河造成了極大的恐慌,老刑警劉巖沦寂,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件学密,死亡現(xiàn)場離奇詭異,居然都是意外死亡传藏,警方通過查閱死者的電腦和手機腻暮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毯侦,“玉大人哭靖,你說我怎么就攤上這事〕蘩耄” “怎么了试幽?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長卦碾。 經(jīng)常有香客問我铺坞,道長起宽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任济榨,我火速辦了婚禮坯沪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘擒滑。我一直安慰自己腐晾,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布橘忱。 她就那樣靜靜地躺著赴魁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钝诚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天榄棵,我揣著相機與錄音凝颇,去河邊找鬼。 笑死疹鳄,一個胖子當(dāng)著我的面吹牛拧略,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瘪弓,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼垫蛆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了腺怯?” 一聲冷哼從身側(cè)響起袱饭,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呛占,沒想到半個月后虑乖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡晾虑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年疹味,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帜篇。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡糙捺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笙隙,到底是詐尸還是另有隱情洪灯,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布逃沿,位于F島的核電站婴渡,受9級特大地震影響幻锁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜边臼,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一哄尔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柠并,春花似錦岭接、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至粘拾,卻和暖如春窄锅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缰雇。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工入偷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人械哟。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓疏之,卻偏偏與公主長得像,于是被迫代替她去往敵國和親暇咆。 傳聞我的和親對象是個殘疾皇子锋爪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容