丝袜人妻一区二区三区_少妇福利无码视频_亚洲理论片在线观看_一级毛片国产A级片

當(dāng)前位置:首頁(yè) > 科技數(shù)碼

關(guān)于0x00000004我想說(shuō)千人千面在線問(wèn)題回放技術(shù)

閑魚(yú)技術(shù)——銀市場(chǎng)(geneyin)

門(mén)

發(fā)布App后,開(kāi)發(fā)者最頭疼的問(wèn)題是如何解決交付后用戶端問(wèn)題的恢復(fù)和定位。業(yè)界缺乏系統(tǒng)解決方案的空白區(qū)域。閑魚(yú)技術(shù)組結(jié)合自己的業(yè)務(wù)痛點(diǎn),提出了新的技術(shù)思路,解決了這個(gè)問(wèn)題,在網(wǎng)上取得了比較滿意的實(shí)戰(zhàn)效果。(約翰f肯尼迪)。

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

背景

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

  • 直接用文字輸入表達(dá),或者截圖
  • 直接錄制視頻反饋

這兩種反饋方式常常帶來(lái)以下抱怨:

  • 用戶:輸入文字好費(fèi)時(shí)費(fèi)力
  • 開(kāi)發(fā)1:看不懂用戶反饋說(shuō)的是什么意思?
  • 開(kāi)發(fā)2:大概看懂用戶說(shuō)的是什么意思了,但是我線下沒(méi)辦法復(fù)現(xiàn)哈
  • 開(kāi)發(fā)3:看了用戶錄制的視頻,但是我線下沒(méi)辦法重現(xiàn),也定位不到問(wèn)題

所以:為了解決以上問(wèn)題,我們用一套全新的思路來(lái)設(shè)計(jì)線上問(wèn)題回放體系

線上問(wèn)題回放體系的意義

  • 用戶不需要輸入文字反饋問(wèn)題,只需要重新操作一下app重現(xiàn)問(wèn)題步驟即可
  • 開(kāi)發(fā)者拿到用戶反饋的問(wèn)題腳本后,通過(guò)線下回放對(duì)問(wèn)題一目了然,跟錄制視頻效果一樣,是的,你沒(méi)看錯(cuò),就是跟看視頻一樣。
  • 通過(guò)腳本的回放實(shí)時(shí)獲取到app運(yùn)行時(shí)相關(guān)數(shù)據(jù)(本地?cái)?shù)據(jù),網(wǎng)絡(luò)數(shù)據(jù),堆棧等等),
  • 以便排查問(wèn)題
  • 為后續(xù)自動(dòng)測(cè)試提供想象空間--你懂的

效果視頻

技術(shù)原理

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

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

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

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

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

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

錄制其實(shí)就是把ui和數(shù)據(jù)記錄下來(lái),回放其實(shí)就是app自動(dòng)

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

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

錄制流程

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

回放跟錄制框架圖基本一樣,實(shí)際上錄制和回放的代碼是在一起,邏輯也是統(tǒng)一的,為了便于表達(dá),我人為劃分成兩個(gè)架構(gòu)圖出來(lái)。

回放的流程:

回放流程圖在這里省略

  • 1.啟動(dòng)app,點(diǎn)擊回放按鈕
  • 2.引擎加載回放腳本
  • 3.從腳本中解析出需要注冊(cè)的運(yùn)行時(shí)事件并注冊(cè),在回放里不需要業(yè)務(wù)上層來(lái)注冊(cè)事件,這里跟錄制是不一樣的。
  • 4.從腳本中解析出需要注冊(cè)的靜態(tài)數(shù)據(jù)事件并注冊(cè)
  • 5.從腳本中解析出需要播放的事件數(shù)據(jù),并組成消費(fèi)隊(duì)列
  • 6.啟動(dòng)播放器,從消費(fèi)隊(duì)列里讀取一個(gè)個(gè)事件來(lái)播放,如果是ui事件則直接播放,如果是靜態(tài)數(shù)據(jù)事件則直接按照指令要求替換數(shù)據(jù)值,如果是非ui運(yùn)行時(shí)事件則通過(guò)事件指令規(guī)則來(lái)確定是主動(dòng)播放還是等待攔截對(duì)應(yīng)的事件,如果需要等待攔截對(duì)應(yīng)的事件,則播放器會(huì)一直等待此事件直到此事件被app消費(fèi)掉為止。只有此事件被消費(fèi)了,播放器才能播放下一個(gè)事件。
  • 7.當(dāng)攔截到被注冊(cè)的事件后,根據(jù)此事件指令要求把相應(yīng)的數(shù)據(jù)塞到相應(yīng)的字段里
  • 8.跳回6繼續(xù)運(yùn)行,直到消費(fèi)隊(duì)列里的事件被消費(fèi)完

注意:回放每個(gè)事件時(shí)會(huì)實(shí)時(shí)自動(dòng)打印出相應(yīng)的堆棧信息和事件數(shù)據(jù),有利于排查問(wèn)題

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

1.模擬觸摸事件

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

我們先來(lái)介紹觸摸事件的處理流程

等待觸摸階段

  • 手機(jī)屏幕處于待機(jī)狀態(tài),等待觸摸事件發(fā)生
  • 手指開(kāi)始觸摸屏幕

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

  • 屏幕感應(yīng)器接收到觸摸,并將觸摸數(shù)據(jù)傳給系統(tǒng)IOKit(IOKit是蘋(píng)果的硬件驅(qū)動(dòng)框架)
  • 系統(tǒng)IOKit封裝該觸摸事件為IOHIDEvent對(duì)象
  • 接著系統(tǒng)IOKit把IOHIDEvent對(duì)象轉(zhuǎn)發(fā)給SpringBoard進(jìn)程

SpringBoard進(jìn)程就是iOS的系統(tǒng)桌面,它存在于iDevice的進(jìn)程中,不可清除,它的運(yùn)行原理與Windows中的ex系統(tǒng)進(jìn)程相類(lèi)似。它主要負(fù)責(zé)界面管理,所以只有它才知道當(dāng)前觸摸到底有誰(shuí)來(lái)響應(yīng)。

SpringBoard接收階段

  • SpringBoard收到IOHIDEvent消息后,觸發(fā)runloop中的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()方法。
  • SpringBoard開(kāi)始查詢(xún)前臺(tái)是否存在正在運(yùn)行的app,如果存在,則SpringBoard通過(guò)進(jìn)程通信方式把此觸摸事件轉(zhuǎn)發(fā)給前臺(tái)當(dāng)前app,如果不存在,則SpringBoard進(jìn)入其自己內(nèi)部響應(yīng)過(guò)程。

app處理階段

  • 前臺(tái)app主線程Runloop收到SpringBoard轉(zhuǎn)發(fā)來(lái)的消息,并觸發(fā)對(duì)應(yīng)runloop 中的Source1回調(diào)_UIApplicationHandleEventQueue()。
  • _UIApplicationHandleEventQueue()把IOHIDEvent處理包裝成UIEvent進(jìn)行處理分發(fā)
  • Soucre0回調(diào)內(nèi)部UIApplication的sendEvent:方法,將UIEvent傳給UIWindow
  • 在UIWindow為根節(jié)點(diǎn)的整棵視圖樹(shù)上通過(guò)hitTest(_:with:)和point(inside:with:)這兩個(gè)方法遞歸查找到合適響應(yīng)這個(gè)觸摸事件的視圖。
  • 找到最終的葉子節(jié)點(diǎn)視圖后,就開(kāi)始觸發(fā)此視圖綁定的相應(yīng)事件,比如跳轉(zhuǎn)頁(yè)面等等。

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

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

這里有一個(gè)關(guān)鍵點(diǎn),需要把的時(shí)間戳記錄下來(lái),以及把當(dāng)前touch事件距離上一個(gè)touch事件的時(shí)間間隔記錄下來(lái),因?yàn)檫@個(gè)涉及到觸摸引起慣性加速度問(wèn)題。比如我們平時(shí)滑動(dòng)列表視圖時(shí),手指離開(kāi)屏幕后,列表視圖還要慣性地滑動(dòng)一小段時(shí)間。

- (void)handleUIEvent:(UIEvent *)event { if (!) return; if != UIEventTypeTouches) return; NSSet *allTouches = [event allTouches]; UITouch *touch = (UITouch *)[allTouches anyObject]; if ) { if && !)) { return; } } switch ) { case UITouchPhaseBegan: { = mach_absolute_time(); = ; = [NSMutableArray array]; [self recordTouch:touch click:]; break; } case UITouchPhaseStationary: { [self recordTouch:touch click:mach_absolute_time()]; break; } case UITouchPhaseCancelled: { [self recordTouch:touch click:mach_absolute_time()]; [[NSNotificationCenter defaultCenter] postNotificationName:@"notice_ui_test" object:]; break; } case UITouchPhaseEnded: { [self recordTouch:touch click:mach_absolute_time()]; [[NSNotificationCenter defaultCenter] postNotificationName:@"notice_ui_test" object:]; break; } case UITouchPhaseMoved: { [self recordTouch:touch click:mach_absolute_time()]; } default: break; } } - (NSDictionary *)recordTouch:(UITouch *)touch click:(uint64_t)curClick { NSDictionary *dic = [NSMutableDictionary dictionary]; NSString *name = NSStringFromClass([ class]); [dic setValue:name forKey:@"viewClass"]; NSString *frame = NSStringFromCGRect.frame); [dic setValue:frame forKey:@"viewframe"]; [dic setValue:[NSNumber numberWithInteger:touch.phase] forKey:@"phase"]; [dic setValue:[NSNumber numberWithInteger:touch.tapCount] forKey:@"tapCount"]; uint64_t click = curClick - ; [dic setValue:[NSNumber numberWithUnsignedLongLong:click] forKey:@"click"]; UIWindow *touchedWindow = [UIWindow windowOfView:]; CGPoint location = [touch locationInView:touchedWindow]; // if(CGPointEqualToPoint(location, )) // { // return nil; // } NSString *pointStr = NSStringFromCGPoint(location); [dic setValue:pointStr forKey:@"LocationInWindow"]; NSTimeInterval timestampGap = - ; [dic setValue:[NSNumber numberWithDouble:timestampGap] forKey:@"timestamp"]; // = location; [ addObject:dic]; return dic; }

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

接著我們來(lái)看一下模擬觸摸事件代碼

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

  • 1.UITouch對(duì)象 - 將用于觸摸
  • 2.第一個(gè)UIEvent Began觸摸
  • 3.第二個(gè)UIEvent Ended觸摸

實(shí)現(xiàn)步驟:

  • 1.代碼的前面部分都是一些UITouch和UIEvent私有接口,私有變量字段,由于蘋(píng)果并不公開(kāi)它們,為了讓其編譯不報(bào)錯(cuò),所以我們需要把這些字段包含進(jìn)來(lái),回放是在線下,所以不必?fù)?dān)心私有接口被拒的事情。
  • 2.構(gòu)造觸摸對(duì)象:UITouch和UIEvent,把記錄對(duì)應(yīng)的字段值塞回相應(yīng)的字段。塞回去就是用私有接口和私有字段
  • 3.觸摸的view位置轉(zhuǎn)換為Window坐標(biāo),然后往app里發(fā)送事件
[[UIApplication sharedApplication] sendEvent:event];
  • 4.要回放這些觸摸事件,我們需要把他丟到CADisplayLink里面來(lái)執(zhí)行
// // Simula // // Created by 詩(shī)壯殷 on 2018/5/15. // #import "Simula; #import <objc; #include <mac; #ifdef __LP64__ typedef double IOHIDFloat; #else typedef float IOHIDFloat; #endif typedef UInt32 IOOptionBits; typedef struct __IOHIDEvent *IOHIDEventRef; @interface UITouch (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 @implementation UITouch (replay) - (id)initPoint:(CGPoint)point window:(UIWindow *)window { NSParameterAssert(window); self = [super init]; if (self) { [self setTapCount:1]; [self setIsTap:YES]; [self setPhase:UITouchPhaseBegan]; [self setWindow:window]; [self _setLocationInWindow:point resetPrevious:YES]; [self setView:[window hitTest:point withEvent:nil]]; [self _setIsFirstTouchForView:YES]; [self setTimestamp:[[NSProcessInfo processInfo] systemUptime]]; } return self; } @end @interface UIInternalEvent : UIEvent - (void)_setHIDEvent:(IOHIDEventRef)event; @end @interface UITouchesEvent : UIInternalEvent - (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)delayedDelivery; - (void)_clearTouches; @end @interface UIApplication (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; @end typedef enum { kIOHIDDigitizerEventRange = 0x00000001, kIOHIDDigitizerEventTouch = 0x00000002, kIOHIDDigitizerEventPosition = 0x00000004, } IOHIDDigitizerEventMask; IOHIDEventRef IOHIDEventCreateDigitizerFingerEvent(CFAllocatorRef allocator, 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); @implementation SimulationTouch - (void)performTouchInView:(UIView *)view start:(bool)start { UIWindow *_window = view.window; CGRect fInWindow; if ([view isKindOfClass:[UIWindow class]]) { fInWindow = view.frame; } else { fInWindow = [_window convertRec fromView:view.superview]; } CGPoint point = CGPointMake + , + ); if(start) { = [[UITouch alloc] initPoint:point window:_window]; [ setPhase:UITouchPhaseBegan]; } else { [ _setLocationInWindow:point resetPrevious:NO]; [ setPhase:UITouchPhaseEnded]; } CGPoint currentTouchLocation = point; UITouchesEvent *event = [[UIApplication sharedApplication] _touchesEvent]; [event _clearTouches]; uint64_t machAbsoluteTime = mach_absolute_time(); AbsoluteTime timeStamp; = (UInt32)(machAbsoluteTime >> 32); = (UInt32)(machAbsoluteTime); [ setTimestamp:[[NSProcessInfo processInfo] systemUptime]]; IOHIDDigitizerEventMask eventMask = (.phase == UITouchPhaseMoved) ? kIOHIDDigitizerEventPosition : (kIOHIDDigitizerEventRange | kIOHIDDigitizerEventTouch); Boolean isRangeAndTouch = (.phase != UITouchPhaseEnded); IOHIDEventRef hidEvent = IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, timeStamp, 0, 2, eventMask, curren, curren, 0, 0, 0, isRangeAndTouch, isRangeAndTouch, 0); if ([ respondsToSelector:@selector(_setHidEvent:)]) { [ _setHidEvent:hidEvent]; } [event _setHIDEvent:hidEvent]; [event _addTouch: forDelayedDelivery:NO]; [[UIApplication sharedApplication] sendEvent:event]; } @end

怎樣調(diào)用私有接口,以及使用哪些私有接口,這點(diǎn)不需要再解釋了,如果感興趣,請(qǐng)關(guān)注我們公眾號(hào),后續(xù)我專(zhuān)門(mén)寫(xiě)篇文章來(lái)揭露這方面的技術(shù),總的來(lái)說(shuō)就下載蘋(píng)果提供觸摸事件的源碼庫(kù),分析源碼,然后設(shè)置斷掉調(diào)試,甚至反匯編來(lái)理解觸摸事件的原理。

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

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

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

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

c層代碼介紹如下:

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

  • 通過(guò)寄存器獲取對(duì)象self
  • 通過(guò)寄存器獲取方法sel
  • 通過(guò)self和sel獲取對(duì)應(yīng)的事件指令
  • 通過(guò)事件指令回調(diào)上層來(lái)決定是否往下執(zhí)行
  • 獲取需要回放該事件的數(shù)據(jù)
  • 把數(shù)據(jù)塞回去,比如塞到某個(gè)寄存器里,或者塞到某個(gè)寄存器所指向的對(duì)象的某個(gè)字段等等
  • 如果需要立即回放則調(diào)用原來(lái)被hook的原始方法,如果不是立即回放,則需要把現(xiàn)場(chǎng)信息保存起來(lái),并等待合適的時(shí)機(jī)由播放隊(duì)列來(lái)播放(調(diào)用)
//xRegs 表示統(tǒng)一匯編器傳入當(dāng)前所有的通用寄存器數(shù)據(jù),它們地址存在一個(gè)數(shù)組指針里 //dRegs 表示統(tǒng)一匯編器傳入當(dāng)前所有的浮點(diǎn)寄存器數(shù)據(jù),它們地址也存在一個(gè)數(shù)組指針里 //dRegs 表示統(tǒng)一匯編器傳入當(dāng)前堆棧指針 //fp 表示調(diào)用棧幀指針 void replay_entry_start(void* xRegs, void* dRegs, void* spReg, CallBackRetIns *retIns,StackFrame *fp, void *con_stub_lp) { void *objAdr = (((void **)xRegs)[0]);//獲取對(duì)象本身self或者block對(duì)象本身 EngineManager *manager = [EngineManager sharedInstance]; ReplayEventIns *node = [manager getEventInsWithBlock:objAdr]; id obj = (__bridge id)objAdr; void *xrArg = ((void **)xRegs)+2; if(nil == node) { SEL selecter = (SEL)(((void **)xRegs)[1]); //對(duì)應(yīng)的對(duì)象調(diào)用的方法 Class tclass = [obj class];//object_getClass(obj);object_getClass方法只能通過(guò)對(duì)象獲取它的類(lèi),不能傳入class 返回class本身, do { node = [manager getEventIns:tclass sel:selecter];//通過(guò)對(duì)象和方法獲取對(duì)應(yīng)的事件指令節(jié)點(diǎn) }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)前回放是否打斷 i && node.BreakCurReplayExe(obj,node,xrArg,dRegs)) { retIns->nodeAddr = NULL; retIns->recordOrReplayData = NULL; retIns->return_address = NULL; return; } bool needReplay = true; //回調(diào)通知上層當(dāng)前即將回放該事件 i) { needReplay = (*))(obj,node,xrArg,dRegs); } if(needReplay) { ReplayEventData *replayData = nil; i) { //獲取回放該事件對(duì)應(yīng)的數(shù)據(jù) replayData = (*))(obj,node,xrArg,dRegs); } else//默認(rèn)獲取方法 { replayData = [manager getNextReplayEventData:node]; } //以下就是真正的回放,即是把數(shù)據(jù)塞回去,并調(diào)用原來(lái)被hook的方法 if(replayData) { if(replay_type_intercept_call == node.replayType) { sstuffArg(xRegs,dRegs,spReg,node,re); 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]) { //放到播放隊(duì)列里播放,返回沒(méi)調(diào)用地址,讓其不往下走 retIns->return_address = NULL; return ; } } else { //塞數(shù)據(jù) sstuffArg(xRegs,dRegs,spReg,node,re); } } retIns->nodeAddr = (__bridge void *)node; retIns->recordOrReplayData = (__bridge void *)replayData; retIns->return_address = node.callBack ? node.callBack : [node getOrgFun]; re = 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)滿足不了你的需求。

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

何謂oc block

  • block就是閉包,跟回調(diào)函數(shù)callback很類(lèi)似,閉包也是對(duì)象
  • blcok的特點(diǎn): 1.可有參數(shù)列表
  • 2.可有返回值

3.有方法體

4.capture上下文變量

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

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

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

我們先來(lái)看一下block的例子:

void test() { __block int var1 = 8; //上下文變量 NSString *var2 = @"我是第二個(gè)變量”; //上下文變量 void (^block)(int) = ^(int arg)//參數(shù)列表 { var1 = 6; NSLog(@"arg = %d,var1 = %d, var2 = %@", arg, var1, var2); }; block(1);//調(diào)用block語(yǔ)法 dispatch_async(dispatch_get_global_queue(0, 0), ^ { block(2); //異步調(diào)用block }); }

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

  • 兩個(gè)變量:這兩個(gè)變量都是被block引用,第一個(gè)變量有關(guān)鍵字__block,表示可以在block里對(duì)該變量賦值,第二個(gè)變量沒(méi)有__block關(guān)鍵字,在block里只能讀,不能寫(xiě)。
  • 兩個(gè)調(diào)用block的語(yǔ)句:第一個(gè)直接在當(dāng)前方法test()里調(diào)用,此時(shí)的block內(nèi)存數(shù)據(jù)在棧上,第二個(gè)是異步調(diào)用,就是說(shuō)當(dāng)執(zhí)行block(2)時(shí)test()可能已經(jīng)運(yùn)行完了,test()調(diào)用棧可能已經(jīng)被銷(xiāo)毀。那這種情況block的數(shù)據(jù)肯定不能在棧上,只能在堆上或者在全局區(qū)。

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

  • 注意:雖然底層是用這些結(jié)構(gòu)體來(lái)表達(dá)block,但是它們并不是源碼,是二進(jìn)制代碼
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 }; typedef struct block_descriptor_head { unsigned long int reserved; unsigned long int size; //表示主體block結(jié)構(gòu)體的內(nèi)存大小 }block_descriptor_head; typedef struct block_descriptor_has_help { unsigned long int reserved; unsigned long int size; //表示主體block結(jié)構(gòu)體的內(nèi)存大小 void (*copy)(void *dst, void *src);//當(dāng)block被retain時(shí)會(huì)執(zhí)行此函數(shù)指針 void (*dispose)(void *);//block被銷(xiāo)毀時(shí)調(diào)用 struct block_arg_var_descriptor *argVar; }block_descriptor_has_help; typedef struct block_descriptor_has_sig { unsigned long int reserved; unsigned long int size; const char *signature;//block的簽名信息 struct block_arg_var_descriptor *argVar; }block_descriptor_has_sig; typedef struct block_descriptor_has_all { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); const char *signature; struct block_arg_var_descriptor *argVar; }block_descriptor_has_all; typedef struct block_info_1 { void *isa;//表示當(dāng)前blcok是在堆上還是在棧上,或在全局區(qū)_NSConcreteGlobalBlock int flags; //對(duì)應(yīng)上面的enum值,這些枚舉值是我從編譯器源碼拷貝過(guò)來(lái)的 int reserved; void (*invoke)(void *, ...);//block對(duì)應(yīng)的方法體(執(zhí)行體,就是代碼段) void *descriptor;//此處指向上面幾個(gè)結(jié)構(gòu)體中的一個(gè),具體哪一個(gè)根據(jù)flags值來(lái)定,它用來(lái)進(jìn)一步來(lái)描述block信息 //從這個(gè)字段開(kāi)始起,后面的字段表示的都是此block對(duì)外引用的變量。 NSString *var2; byref_var1_1 var1; } block_info_1;

這個(gè)例子中的block在底層表達(dá)大概如下圖:

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

block小結(jié)

  • 為了表達(dá)block,底層設(shè)計(jì)三種結(jié)構(gòu)體:block_info_1,block_desc_1,byref_var1_1,三種函數(shù)指針: block invoke方法體,copy方法,dispose方法
  • 其實(shí)表達(dá)block是非常復(fù)雜的,還涉及到block的生命周期,內(nèi)存管理問(wèn)題等等,我在這里只是簡(jiǎn)單的貫穿主流程來(lái)介紹的,很多細(xì)節(jié)都沒(méi)介紹。

怎樣統(tǒng)一hook block

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

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

VoidfunBlock createNewBlock(VoidfunBlock orgblock, ReplayEventIns *blockEvent,bool isRecord) { 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); return newBlock; } return nil; }

我們首先新建一個(gè)新的block newBlock,然后把原來(lái)的block orgblock 和 事件指令blockEvent包到新的blcok中,這樣達(dá)到引用的效果。然后把新的block轉(zhuǎn)成結(jié)構(gòu)體指針,并把結(jié)構(gòu)體指針中的字段invoke(方法體)指向統(tǒng)一回調(diào)方法。你可能詫異新的block是沒(méi)有參數(shù)類(lèi)型的,原來(lái)block是有參數(shù)類(lèi)型,外面調(diào)用原來(lái)block傳遞參數(shù)時(shí)會(huì)不會(huì)引起crash?答案是否定的,因?yàn)檫@里構(gòu)造新的block時(shí) 我們只用block數(shù)據(jù)結(jié)構(gòu),block的回調(diào)方法字段已經(jīng)被閹割,回調(diào)方法已經(jīng)指向統(tǒng)一方法了,這個(gè)統(tǒng)一方法可以接受任何類(lèi)型的參數(shù),包括沒(méi)有參數(shù)類(lèi)型。這個(gè)統(tǒng)一方法也是匯編實(shí)現(xiàn),代碼實(shí)現(xiàn)跟上面的匯編層代碼類(lèi)似,這里就不附上源碼了。

那怎樣在新的blcok里讀取原來(lái)的block和事件指令對(duì)象呢?

代碼如下:

void var_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é)

  • 本文大概介紹了問(wèn)題回放框架,接著介紹三個(gè)關(guān)鍵技術(shù)。這三個(gè)技術(shù)相對(duì)比較深入,估計(jì)很多讀者理解起來(lái)比較費(fèi)勁,請(qǐng)諒解!
  • 如果對(duì)里面的技術(shù)點(diǎn)感興趣,你可以關(guān)注我們的公眾號(hào)。我們后續(xù)會(huì)單獨(dú)對(duì)里面的技術(shù)點(diǎn)詳細(xì)深入的分析發(fā)文。
  • 如果覺(jué)得上面有錯(cuò)誤的地方,請(qǐng)指出。謝謝`js

1.《關(guān)于0x00000004我想說(shuō)千人千面在線問(wèn)題回放技術(shù)》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識(shí),僅代表作者本人觀點(diǎn),與本網(wǎng)站無(wú)關(guān),侵刪請(qǐng)聯(lián)系頁(yè)腳下方聯(lián)系方式。

2.《關(guān)于0x00000004我想說(shuō)千人千面在線問(wèn)題回放技術(shù)》僅供讀者參考,本網(wǎng)站未對(duì)該內(nèi)容進(jìn)行證實(shí),對(duì)其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。

3.文章轉(zhuǎn)載時(shí)請(qǐng)保留本站內(nèi)容來(lái)源地址,http://f99ss.com/keji/1956413.html

上一篇

0x000000c5看這里!從零開(kāi)始維修計(jì)算機(jī)__0X000000C5的解決方法

下一篇

0pp0r9怎么截圖專(zhuān)題之成熟的同時(shí)需要進(jìn)化:OPPO R9使用情況評(píng)估

關(guān)于0x00000004我想說(shuō)藍(lán)屏錯(cuò)誤停機(jī)代碼686例

關(guān)于0x00000004我想說(shuō)藍(lán)屏錯(cuò)誤停機(jī)代碼686例

0x00000004相關(guān)介紹,有的朋友是藍(lán)屏,不知道該怎么辦。以下是藍(lán)屏錯(cuò)誤代碼的說(shuō)明。 自己就查看代碼解決藍(lán)屏問(wèn)題。希望對(duì)友友們有所幫助。歡迎收藏備用。 0x00000000 作業(yè)完成。 0x00000001 不正確的函數(shù)。 0...