工程师如何“神还原”用户问题?闲鱼回放技术揭秘

咱们透过系统底层来捕获ui事件流和业务数据的流动,并利用捕获到的这些数据经过事件回放机制来复现线上的问题。本文先介绍录制和回放的总体框架,接着介绍里面涉及到的3个关键技术点,也是这里最复杂的技术(模拟触摸事件,统一拦截器实现,统一hook block)。node

背景git

如今的app基本都会提供用户反馈问题的入口,然而提供给用户反馈问题通常有两种方式:数据库

 ●  直接用文字输入表达,或者截图
 ●  直接录制视频反馈数组

这两种反馈方式经常带来如下抱怨:服务器

 ●  用户:输入文字好费时费力
 ●  开发1:看不懂用户反馈说的是什么意思?
 ●  开发2:大概看懂用户说的是什么意思了,可是我线下没办法复现哈
 ●  开发3:看了用户录制的视频,可是我线下没办法重现,也定位不到问题网络

因此,为了解决以上问题,咱们用一套全新的思路来设计线上问题回放体系。数据结构

线上问题回放体系的意义

 ●  用户不须要输入文字反馈问题,只须要从新操做一下app重现问题步骤便可。
 ●  开发者拿到用户反馈的问题脚本后,经过线下回放对问题一目了然,跟录制视频效果同样,是的,你没看错,就是跟看视频同样。
 ●  经过脚本的回放实时获取到app运行时相关数据(本地数据,网络数据,堆栈等等), 以便排查问题。
 ●  为后续自动测试提供想象空间——你懂的。闭包

效果视频

技术原理

1.app与外部环境的关系

从上面的关系图能够看出,整个app的运行无非是用户ui操做,而后触发app从外界获取数据,包括网络数据,gps数据等等,也包括从手机本地获取数据,好比相册数据,机器数据,系统等数据。 因此咱们要实现问题回放只须要记录用户的UI操做和外界数据,app自身数据便可。架构

app录制 = 用户的UI操做 + 外界数据(手机内和手机外) + app自身数据app

2.线上问题回放架构由两部分组成:录制和回放

 
录制是为回放服务,录制的信息越详细,回放成功率就越高,定位问题就越容易
录制其实就是把ui和数据记录下来,回放其实就是app自动驱动UI操做并把录制时的数据塞回相应的地方。

3.录制架构图

录制流程:

4.回放架构图

回放跟录制框架图基本同样,实际上录制和回放的代码是在一块儿,逻辑也是统一的,为了便于表达,我人为划分红两个架构图出来。

回放的流程: 

1.启动app,点击回放按钮。

2.引擎加载回放脚本。

3.从脚本中解析出须要注册的运行时事件并注册,在回放里不须要业务上层来注册事件,这里跟录制是不同的。

4.从脚本中解析出须要注册的静态数据事件并注册。

5.从脚本中解析出须要播放的事件数据,并组成消费队列。

6.启动播放器,从消费队列里读取一个个事件来播放,若是是ui事件则直接播放,若是是静态数据事件则直接按照指令要求替换数据值,若是是非ui运行时事件则经过事件指令规则来肯定是主动播放仍是等待拦截对应的事件,若是须要等待拦截对应的事件,则播放器会一直等待此事件直到此事件被app消费掉为止。只有此事件被消费了,播放器才能播放下一个事件。

7.当拦截到被注册的事件后,根据此事件指令要求把相应的数据塞到相应的字段里。

8.跳回6继续运行,直到消费队列里的事件被消费完。

注意:回放每一个事件时会实时自动打印出相应的堆栈信息和事件数据,有利于排查问题

关键技术介绍

1.模拟触摸事件

从ui事件数据解中析出被触摸的view,以及此view所在的视图树中的层级关系,并在当前回放界面上查找到对应的view,而后往该view上发送ui操做事件(点击,双击等等),并带上触摸事件的坐标信息,其实这里是模拟触摸事件。咱们先来介绍触摸事件的处理流程:

等待触摸阶段

 ●  手机屏幕处于待机状态,等待触摸事件发生
 ●  手指开始触摸屏幕

系统反应阶段

 ●  屏幕感应器接收到触摸,并将触摸数据传给系统IOKit(IOKit是苹果的硬件驱动框架)
 ●  系统IOKit封装该触摸事件为IOHIDEvent对象
 ●  接着系统IOKit把IOHIDEvent对象转发给SpringBoard进程

SpringBoard进程就是iOS的系统桌面,它存在于iDevice的进程中,不可清除,它的运行原理与Windows中的explorer.exe系统进程相相似。它主要负责界面管理,因此只有它才知道当前触摸到底有谁来响应。

SpringBoard接收阶段

 ●  SpringBoard收到IOHIDEvent消息后,触发runloop中的Source1回调__IOHIDEventSystemClientQueueCallback()方法。
 ●  SpringBoard开始查询前台是否存在正在运行的app,若是存在,则SpringBoard经过进程通讯方式把此触摸事件转发给前台当前app,若是不存在,则SpringBoard进入其本身内部响应过程。

app处理阶段

 ●  前台app主线程Runloop收到SpringBoard转发来的消息,并触发对应runloop 中的Source1回调_UIApplicationHandleEventQueue()。
 ●  _UIApplicationHandleEventQueue()把IOHIDEvent处理包装成UIEvent进行处理分发。
 ●  Soucre0回调内部UIApplication的sendEvent:方法,将UIEvent传给UIWindow。
 ●  在UIWindow为根节点的整棵视图树上经过hitTest(_:with:)和point(inside:with:)这两个方法递归查找到合适响应这个触摸事件的视图。
 ●  找到最终的叶子节点视图后,就开始触发此视图绑定的相应事件,好比跳转页面等等。

从上面触摸事件处理过程当中咱们能够看出要录制ui事件只须要在app处理阶段中的UIApplication sendEvent方法处截获触摸数据,回放时也是在这里把触摸模拟回去。

下面是触摸事件录制的代码,就是把UITouch相应的数据保存下来便可 这里有一个关键点,须要把touch.timestamp的时间戳记录下来,以及把当前touch事件距离上一个touch事件的时间间隔记录下来,由于这个涉及到触摸引发惯性加速度问题。好比咱们平时滑动列表视图时,手指离开屏幕后,列表视图还要惯性地滑动一小段时间。

 
 
  1. - (void)handleUIEvent:(UIEvent *)event

  2. {

  3. if (!self.isEnabled) return;

  4. if (event.type != UIEventTypeTouches) return;

  5. NSSet *allTouches = [event allTouches];

  6. UITouch *touch = (UITouch *)[allTouches anyObject];

  7. if (touch.view) {

  8. if (self.filter && !self.filter(touch.view)) {

  9. return;

  10. }

  11. }

  12. switch (touch.phase) {

  13. case UITouchPhaseBegan:

  14. {

  15. self.machAbsoluteTime = mach_absolute_time();

  16. self.systemStartUptime = touch.timestamp;

  17. self.tuochArray = [NSMutableArray array];

  18. [self recordTouch:touch click:self.machAbsoluteTime];

  19. break;

  20. }

  21. case UITouchPhaseStationary:

  22. {

  23. [self recordTouch:touch click:mach_absolute_time()];

  24. break;

  25. }

  26. case UITouchPhaseCancelled:

  27. {

  28. [self recordTouch:touch click:mach_absolute_time()];

  29. [[NSNotificationCenter defaultCenter] postNotificationName:@"notice_ui_test" object:self.tuochArray];

  30. break;

  31. }

  32. case UITouchPhaseEnded:

  33. {

  34. [self recordTouch:touch click:mach_absolute_time()];

  35. [[NSNotificationCenter defaultCenter] postNotificationName:@"notice_ui_test" object:self.tuochArray];

  36. break;

  37. }

  38. case UITouchPhaseMoved:

  39. {

  40. [self recordTouch:touch click:mach_absolute_time()];

  41. }

  42. default:

  43. break;

  44. }

  45. }

  46.  

 

咱们来看一下代码怎么模拟单击触摸事件(为了容易理解,我把有些不是关键,复杂的代码已经去掉),接着咱们来看一下模拟触摸事件代码 一个基本的触摸事件通常由三部分组成:

 ●  UITouch对象 - 将用于触摸
 ●  第一个UIEvent Began触摸
 ●  第二个UIEvent Ended触摸

实现步骤:

1.代码的前面部分都是一些UITouch和UIEvent私有接口,私有变量字段,因为苹果并不公开它们,为了让其编译不报错,因此咱们须要把这些字段包含进来,回放是在线下,因此没必要担忧私有接口被拒的事情。

2.构造触摸对象:UITouch和UIEvent,把记录对应的字段值塞回相应的字段。塞回去就是用私有接口和私有字段。

3.触摸的view位置转换为Window坐标,而后往app里发送事件 [[UIApplication sharedApplication] sendEvent:event];

4.要回放这些触摸事件,咱们须要把他丢到CADisplayLink里面来执行。

 
 
  1. //

  2. // SimulationTouch.m

  3. //

  4. // Created by 诗壮殷 on 2018/5/15.

  5. //

  6. #import "SimulationTouch.h"

  7. #import <objc/runtime.h>

  8. #include <mach/mach_time.h>

  9.  
  10. @implementation UITouch (replay)

  11. - (id)initPoint:(CGPoint)point window:(UIWindow *)window

  12. {

  13. NSParameterAssert(window);

  14. self = [super init];

  15. if (self) {

  16. [self setTapCount:1];

  17. [self setIsTap:YES];

  18. [self setPhase:UITouchPhaseBegan];

  19. [self setWindow:window];

  20. [self _setLocationInWindow:point resetPrevious:YES];

  21. [self setView:[window hitTest:point withEvent:nil]];

  22. [self _setIsFirstTouchForView:YES];

  23. [self setTimestamp:[[NSProcessInfo processInfo] systemUptime]];

  24. }

  25. return self;

  26. }

  27. @end

  28. @interface UIInternalEvent : UIEvent

  29. - (void)_setHIDEvent:(IOHIDEventRef)event;

  30. @end

  31. @interface UITouchesEvent : UIInternalEvent

  32. - (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)delayedDelivery;

  33. - (void)_clearTouches;

  34. @end

  35.  
  36. typedef enum {

  37. kIOHIDDigitizerEventRange = 0x00000001,

  38. kIOHIDDigitizerEventTouch = 0x00000002,

  39. kIOHIDDigitizerEventPosition = 0x00000004,

  40. } IOHIDDigitizerEventMask;

  41. IOHIDEventRef IOHIDEventCreateDigitizerFingerEvent(CFAllocatorRef allocator,

  42. AbsoluteTime timeStamp,

  43. uint32_t index,

  44. uint32_t identity,

  45. IOHIDDigitizerEventMask eventMask,

  46. IOHIDFloat x,

  47. IOHIDFloat y,

  48. IOHIDFloat z,

  49. IOHIDFloat tipPressure,

  50. IOHIDFloat twist,

  51. Boolean range,

  52. Boolean touch,

  53. IOOptionBits options);

  54. @implementation SimulationTouch

  55. - (void)performTouchInView:(UIView *)view start:(bool)start

  56. {

  57. UIWindow *_window = view.window;

  58. CGRect fInWindow;

  59. if ([view isKindOfClass:[UIWindow class]])

  60. {

  61. fInWindow = view.frame;

  62. }

  63. else

  64. {

  65. fInWindow = [_window convertRect:view.frame fromView:view.superview];

  66. }

  67. CGPoint point =

  68. CGPointMake(fInWindow.origin.x + fInWindow.size.width/2,

  69. fInWindow.origin.y + fInWindow.size.height/2);

  70. if(start)

  71. {

  72. self.touch = [[UITouch alloc] initPoint:point window:_window];

  73. [self.touch setPhase:UITouchPhaseBegan];

  74. }

  75. else

  76. {

  77. [self.touch _setLocationInWindow:point resetPrevious:NO];

  78. [self.touch setPhase:UITouchPhaseEnded];

  79. }

  80. CGPoint currentTouchLocation = point;

  81. UITouchesEvent *event = [[UIApplication sharedApplication] _touchesEvent];

  82. [event _clearTouches];

  83. uint64_t machAbsoluteTime = mach_absolute_time();

  84. AbsoluteTime timeStamp;

  85. timeStamp.hi = (UInt32)(machAbsoluteTime >> 32);

  86. timeStamp.lo = (UInt32)(machAbsoluteTime);

  87. [self.touch setTimestamp:[[NSProcessInfo processInfo] systemUptime]];

  88. IOHIDDigitizerEventMask eventMask = (self.touch.phase == UITouchPhaseMoved)

  89. ? kIOHIDDigitizerEventPosition

  90. : (kIOHIDDigitizerEventRange | kIOHIDDigitizerEventTouch);

  91. Boolean isRangeAndTouch = (self.touch.phase != UITouchPhaseEnded);

  92. IOHIDEventRef hidEvent = IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault,

  93. timeStamp,

  94. 0,

  95. 2,

  96. eventMask,

  97. currentTouchLocation.x,

  98. currentTouchLocation.y,

  99. 0,

  100. 0,

  101. 0,

  102. isRangeAndTouch,

  103. isRangeAndTouch,

  104. 0);

  105. if ([self.touch respondsToSelector:@selector(_setHidEvent:)]) {

  106. [self.touch _setHidEvent:hidEvent];

  107. }

  108. [event _setHIDEvent:hidEvent];

  109. [event _addTouch:self.touch forDelayedDelivery:NO];

  110. [[UIApplication sharedApplication] sendEvent:event];

  111. }

  112. @end

总的来讲就下载苹果提供触摸事件的源码库,分析源码,而后设置断掉调试,甚至反汇编来理解触摸事件的原理。

2.统一拦截器

录制和回放都居于事件流来处理的,而数据的事件流其实就是对一些关键方法的hook,因为咱们为了保证对业务代码无侵入和扩展性(随便注册事件),咱们须要对全部方法统一hook,全部的方法由同一个钩子来响应处理。以下图所示

这个钩子是用用汇编编写,因为汇编代码比较多,并且比较难读懂,因此这里暂时不附上源码,汇编层主要把硬件里面的一些数据统一读取出来,好比通用寄存器数据和浮点寄存器数据,堆栈信息等等,甚至前面的前面的方法参数均可以读取出来,最后转发给c语言层处理。

汇编层把硬件相关信息组装好后调用c层统一拦截接口,汇编层是为c层服务。c层没法读取硬件相关信息,因此这里只能用汇编来读取。c层接口经过硬件相关信息定位到当前的方法是属于哪一个事件,知道了事件,也意味着知道了事件指令,知道了事件指令,也知道了哪些字段须要塞回去,也知道了被hook的原始方法。

c层代码介绍以下: 因为是统一调用这个拦截器,因此拦截器并不知道当前是哪一个业务代码执行过来的,也不知道当前这个业务方法有多少个参数,每一个参数类型是什么等等,这个接口代码处理过程大概以下:

 ●  经过寄存器获取对象self
 ●  经过寄存器获取方法sel
 ●  经过self和sel获取对应的事件指令
 ●  经过事件指令回调上层来决定是否往下执行
 ●  获取须要回放该事件的数据
 ●  把数据塞回去,好比塞到某个寄存器里,或者塞到某个寄存器所指向的对象的某个字段等等
 ●  若是须要当即回放则调用原来被hook的原始方法,若是不是当即回放,则须要把现场信息保存起来,并等待合适的时机由播放队列来播放(调用)

 
 
  1. //xRegs 表示统一汇编器传入当前全部的通用寄存器数据,它们地址存在一个数组指针里

  2. //dRegs 表示统一汇编器传入当前全部的浮点寄存器数据,它们地址也存在一个数组指针里

  3. //dRegs 表示统一汇编器传入当前堆栈指针

  4. //fp 表示调用栈帧指针

  5. void replay_entry_start(void* xRegs, void* dRegs, void* spReg, CallBackRetIns *retIns,StackFrame *fp, void *con_stub_lp)

  6. {

  7. void *objAdr = (((void **)xRegs)[0]);//获取对象自己self或者block对象自己

  8. EngineManager *manager = [EngineManager sharedInstance];

  9. ReplayEventIns *node = [manager getEventInsWithBlock:objAdr];

  10. id obj = (__bridge id)objAdr;

  11. void *xrArg = ((void **)xRegs)+2;

  12. if(nil == node)

  13. {

  14. SEL selecter = (SEL)(((void **)xRegs)[1]); //对应的对象调用的方法

  15. Class tclass = [obj class];//object_getClass(obj);object_getClass方法只能经过对象获取它的类,不能传入class 返回class自己,

  16. do

  17. {

  18. node = [manager getEventIns:tclass sel:selecter];//经过对象和方法获取对应的事件指令节点

  19. }while(nil == node && (tclass = class_getSuperclass(tclass)));

  20. }

  21. else

  22. {

  23. xrArg = ((void **)xRegs)+1;

  24. }

  25. assert(node && "node is nil in replay_call_start");

  26. //回调通知上层当前回放是否打断

  27. if(node.BreakCurReplayExe && node.BreakCurReplayExe(obj,node,xrArg,dRegs))

  28. {

  29. retIns->nodeAddr = NULL;

  30. retIns->recordOrReplayData = NULL;

  31. retIns->return_address = NULL;

  32. return;

  33. }

  34. bool needReplay = true;

  35. //回调通知上层当前即将回放该事件

  36. if(node.willReplay)

  37. {

  38. needReplay = (*(node.willReplay))(obj,node,xrArg,dRegs);

  39. }

  40. if(needReplay)

  41. {

  42. ReplayEventData *replayData = nil;

  43. if(node.getReplayData)

  44. {

  45. //获取回放该事件对应的数据

  46. replayData = (*(node.getReplayData))(obj,node,xrArg,dRegs);

  47. }

  48. else//默认获取方法

  49. {

  50. replayData = [manager getNextReplayEventData:node];

  51. }

  52. //如下就是真正的回放,便是把数据塞回去,并调用原来被hook的方法

  53. if(replayData)

  54. {

  55. if(replay_type_intercept_call == node.replayType)

  56. {

  57. sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);

  58. NSArray *arglist = fetchAllArgInReplay(xRegs, dRegs, spReg, node);

  59. ReplayInvocation *funobj = [[ReplayInvocation alloc] initWithFunPtr:node.callBack ? node.callBack : [node getOrgFun]

  60. args:arglist

  61. argType:[node getFunTypeStr]

  62. retType:rf_return_type_v];

  63. if([[EngineManager sharedInstance] setRepalyEventReady:replayData funObj:funobj])

  64. {

  65. //放到播放队列里播放,返回没调用地址,让其不往下走

  66. retIns->return_address = NULL;

  67. return ;

  68. }

  69. }

  70. else

  71. {

  72. //塞数据

  73. sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);

  74. }

  75. }

  76. retIns->nodeAddr = (__bridge void *)node;

  77. retIns->recordOrReplayData = (__bridge void *)replayData;

  78. retIns->return_address = node.callBack ? node.callBack : [node getOrgFun];

  79. replayData.runStatus = relay_event_run_status_runFinish;

  80. }

  81. else

  82. {

  83. retIns->nodeAddr = NULL;

  84. retIns->recordOrReplayData = NULL;

  85. retIns->return_address = [node getOrgFun];

  86. }

  87. }

3.怎样统一hook block

若是你只是想大概理解block的底层技术,你只需google一下便可。 若是你想全面深刻的理解block底层技术,那网上的那些资料远远知足不了你的需求。 只能阅读苹果编译器clang源码和列出比较有表明性的block例子源码,而后转成c语言和汇编,经过c语言结合汇编研究底层细节。

何谓 oc block?

 ●  block就是闭包,跟回调函数callback很相似,闭包也是对象。
 ●  blcok的特色: 1.可有参数列表 2.可有返回值 3.有方法体 4.capture上下文变量 5.有对象引用计数的内存管理策略(block生命周期)。
 ●  block的通常存储在内存中形态有三种 _NSConcretStackBlock(栈)_NSConcretGlobalBlock(全局)_NSConcretMallocBlock(堆)。

系统底层怎样表达block?

咱们先来看一下block的例子:

 
 
  1. void test()

  2. {

  3. __block int var1 = 8; //上下文变量

  4. NSString *var2 = @"我是第二个变量”; //上下文变量

  5. void (^block)(int) = ^(int arg)//参数列表

  6. {

  7. var1 = 6;

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

  9. };

  10. block(1);//调用block语法

  11. dispatch_async(dispatch_get_global_queue(0, 0), ^

  12. {

  13. block(2); //异步调用block

  14. });

  15. }

这段代码首先定义两个变量,接着定义一个block,最后调用block。

 ●  两个变量:这两个变量都是被block引用,第一个变量有关键字__block,表示能够在block里对该变量赋值,第二个变量没有__block关键字,在block里只能读,不能写。
 ●  两个调用block的语句:第一个直接在当前方法test()里调用,此时的block内存数据在栈上,第二个是异步调用,就是说当执行block(2)时test()可能已经运行完了,test()调用栈可能已经被销毁。那这种状况block的数据确定不能在栈上,只能在堆上或者在全局区。

系统底层表达block比较重要的几种数据结构以下:

注意:虽然底层是用这些结构体来表达block,可是它们并非源码,是二进制代码

 
 
  1. enum

  2. {

  3. BLOCK_REFCOUNT_MASK = (0xffff),

  4. BLOCK_NEEDS_FREE = (1 << 24),

  5. BLOCK_HAS_COPY_DISPOSE = (1 << 25),

  6. BLOCK_HAS_CTOR = (1 << 26),//todo == BLOCK_HAS_CXX_OBJ?

  7. BLOCK_IS_GC = (1 << 27),

  8. BLOCK_IS_GLOBAL = (1 << 28),

  9. BLOCK_HAS_DESCRIPTOR = (1 << 29),//todo == BLOCK_USE_STRET?

  10. BLOCK_HAS_SIGNATURE = (1 << 30),

  11. OBLOCK_HAS_EXTENDED_LAYOUT = (1 << 31)

  12. };

  13. enum

  14. {

  15. BLOCK_FIELD_IS_OBJECT = 3,

  16. BLOCK_FIELD_IS_BLOCK = 7,

  17. BLOCK_FIELD_IS_BYREF = 8,

  18. OBLOCK_FIELD_IS_WEAK = 16,

  19. OBLOCK_BYREF_CALLER = 128

  20. };

  21. typedef struct block_descriptor_head

  22. {

  23. unsigned long int reserved;

  24. unsigned long int size; //表示主体block结构体的内存大小

  25. }block_descriptor_head;

  26. typedef struct block_descriptor_has_help

  27. {

  28. unsigned long int reserved;

  29. unsigned long int size; //表示主体block结构体的内存大小

  30. void (*copy)(void *dst, void *src);//当block被retain时会执行此函数指针

  31. void (*dispose)(void *);//block被销毁时调用

  32. struct block_arg_var_descriptor *argVar;

  33. }block_descriptor_has_help;

  34. typedef struct block_descriptor_has_sig

  35. {

  36. unsigned long int reserved;

  37. unsigned long int size;

  38. const char *signature;//block的签名信息

  39. struct block_arg_var_descriptor *argVar;

  40. }block_descriptor_has_sig;

  41. typedef struct block_descriptor_has_all

  42. {

  43. unsigned long int reserved;

  44. unsigned long int size;

  45. void (*copy)(void *dst, void *src);

  46. void (*dispose)(void *);

  47. const char *signature;

  48. struct block_arg_var_descriptor *argVar;

  49. }block_descriptor_has_all;

  50. typedef struct block_info_1

  51. {

  52. void *isa;//表示当前blcok是在堆上仍是在栈上,或在全局区_NSConcreteGlobalBlock

  53. int flags; //对应上面的enum值,这些枚举值是我从编译器源码拷贝过来的

  54. int reserved;

  55. void (*invoke)(void *, ...);//block对应的方法体(执行体,就是代码段)

  56. void *descriptor;//此处指向上面几个结构体中的一个,具体哪个根据flags值来定,它用来进一步来描述block信息

  57. //从这个字段开始起,后面的字段表示的都是此block对外引用的变量。

  58. NSString *var2;

  59. byref_var1_1 var1;

  60. } block_info_1;

这个例子中的block在底层表达大概以下图:

首先用block_info_1来表达block自己,而后用block_desc_1来具体描述block相关信息(好比block_info_1结构体大小,在堆上仍是在栈上?copy或dispose时调用哪一个方法等等),然而block_desc_1具体是哪一个结构体是由block_info_1中flags字段来决定的,block_info_1里的invoke字段是指向block方法体,便是代码段。block的调用就是执行这个函数指针。因为var1是可写的,因此须要设计一个结构体(byref_var1_1)来表达var1,为何var2直接用他原有的类型表达,而var1要用结构体来表达。篇幅有限,这个本身想一想吧?

block小结

 ●  为了表达block,底层设计三种结构体:block_info_1,block_desc_1,byref_var1_1,三种函数指针: block invoke方法体,copy方法,dispose方法
 ●  其实表达block是很是复杂的,还涉及到block的生命周期,内存管理问题等等,我在这里只是简单的贯穿主流程来介绍的,不少细节都没介绍。

怎样统一 hook block?

经过上面的分析,得知oc里的block就是一个结构体指针,因此我在源码里能够直接把它转成结构体指针来处理。 统一hook block源码以下:

 
 
  1. VoidfunBlock createNewBlock(VoidfunBlock orgblock, ReplayEventIns *blockEvent,bool isRecord)

  2. {

  3. if(orgblock && blockEvent)

  4. {

  5. VoidfunBlock newBlock = ^(void)

  6. {

  7. orgblock();

  8. if(nil == blockEvent)

  9. {

  10. assert(0);

  11. }

  12. };

  13. trace_block_layout *blockLayout = (__bridge trace_block_layout *)newBlock;

  14. blockLayout->invoke = (void (*)(void *, ...))(isRecord?hook_var_block_callBack_record:hook_var_block_callBack_replay);

  15. return newBlock;

  16. }

  17. return nil;

  18. }

咱们首先新建一个新的block newBlock,而后把原来的block orgblock 和 事件指令blockEvent包到新的blcok中,这样达到引用的效果。而后把

新的block转

成结构体指针,并把结构体指针中的字段invoke(方法体)指向统一回调方法。你可能诧异新的block是没有参数类型的,原来block是有参数类型,

外面调用原

来block传递参数时会不会引发crash?答案是否认的,由于这里构造新的block时 咱们只用block数据结构,block的回调方法字段已经被阉割,回

调方法已经指

向统一方法了,这个统一方法能够接受任何类型的参数,包括没有参数类型。这个统一方法也是汇编实现,代码实现跟上面的汇编层代码相似,这

里就不附上源

码了。

那怎样在新的blcok里读取原来的block和事件指令对象呢? 代码以下:

 
 
  1. void var_block_callback_start_record(trace_block_layout * blockLayout)

  2. {

  3. VoidfunBlock orgBlock = (__bridge VoidfunBlock)(*((void **)((char *)blockLayout + sizeof(trace_block_layout))));

  4. ReplayEventIns *node = (__bridge ReplayEventIns *)(*((void **)((char *)blockLayout + 40)));

  5. }

总结

本文大概介绍了问题回放框架,接着介绍三个关键技术。这三个技术相对比较深刻,欢迎在留言区评论,咱们期待与你们交流,共同探讨。

双十一广告:阿里云双十一1折拼团活动:已满6人,都是最低折扣了
【满6人】1核2G云服务器99.5元一年298.5元三年 2核4G云服务器545元一年 1227元三年
【满6人】1核1G MySQL数据库 119.5元一年
【满6人】3000条国内短信包 60元每6月
参团地址:http://click.aliyun.com/m/1000020293/

本文做者:镜空
原文连接 本文为云栖社区原创内容,未经容许不得转载。
相关文章
相关标签/搜索