追踪 Objective-C 方法中的 Block 参数对象

Posted Cocoa开发者社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了追踪 Objective-C 方法中的 Block 参数对象相关的知识,希望对你有一定的参考价值。

文章目录


1. 使用方法


2. 实现原理


2.1. 过滤方法的Block参数


2.2. 执行Callback


2.3. 对NSInvocation的一点探索


3. 总结


很多方法最后一个参数是类似于completionBlock这种回调,然而有些API实现一些异常逻辑时会忘记调用传入的Block参数(当然这肯定是bug啦),或者存在多次调用。在调试的时候可能会碰到这种大坑,需要追踪下Block参数何时调用了,甚至是否调用过。如果不方便直接在Block实现中加代码,或者没有源码的情况下,就需要无侵入式地追踪Block参数对象。


可以追踪方法调用时传入的 Block 类型的参数的执行和销毁。基于实现。本文讲述了它的使用方法和实现原理。


使用方法


只需要调用bt_trackBlockArgOfSelector:callback:方法,就能在对应方法执行传入的block参数被调用和销毁的时候得到回调。回调中的内容包含了block对象,回调类型,block 已经执行的次数,执行block的参数、返回值,堆栈信息。


BTTracker *tracker = [self bt_trackBlockArgOfSelector:@selector(performBlock:) callback:^(id  _Nullable block, BlockTrackerCallbackType type, NSInteger invokeCount, void * _Nullable * _Null_unspecified args, void * _Nullable result, NSArray * _Nonnull callStackSymbols) {
  NSLog(@"%@ invoke count = %ld", BlockTrackerCallbackTypeInvoke == type ? @"BlockTrackerCallBackTypeInvoke" : @"BlockTrackerCallBackTypeDead", (long)invokeCount);
}];


当你不想追踪这个方法执行时传入的block参数时,也可以停止追踪:


[tracker stop];


举个栗子,现在有个方法叫performBlock:,只是简单地调用了block参数:


- (void)performBlock:(void(^)(void))block {
   block();
}


调用两次这个方法,每次都传入不同的 block 实现:


__block NSString *word = @"I'm a block";
[self performBlock:^{
  NSLog(@"add '!!!' to word");
  word = [word stringByAppendingString:@"!!!"];
}];
[self performBlock:^{
  NSLog(@"%@", word);
}];


因为执行两次方法传入的是两个不同的block对象,所以会追踪两个block对象的执行和销毁,打印的log如下:


add '!!!' to word
BlockTrackerCallBackTypeInvoke invoke count = 1
I'm a block!!!
BlockTrackerCallBackTypeInvoke invoke count = 1
BlockTrackerCallBackTypeDead invoke count = 1
BlockTrackerCallBackTypeDead invoke count = 1


在block对象销毁的时候


你可以尝试着把performBlock: 的实现改成这样试试:


- (void)performBlock:(void(^)(void))block {
   block();
   block();
   block();
}


实现原理


原理很简单,就是Hook方法后再Hook下Block,流程大致如下:


  1. 利用Objective-C Runtime机制Hook某个方法,参考的实现原理。

  2. 在方法真正执行前,使用先Hook所有Block类型的参数。Hook模式为BlockHookModeAfter和BlockHookModeDead。

  3. 在Block执行后更新执行次数,并将相关信息回调给Tracker。销毁后也会回调给Tracker。


流程大概很简单,复用以前代码。这里主要讲下Track的逻辑。


过滤方法的 Block 参数


在bt_trackBlockArgOfSelector:callback:里获取方法的Type Encoding后判断是否含有Block类型的参数,并将Block参数的Index保存到BTTracker的blockArgIndex属性。


- (nullable BTTracker *)bt_trackBlockArgOfSelector:(SEL)selector callback:(BlockTrackerCallbackBlock)callback
{
   Class cls = bt_classOfTarget(self);
   Method originMethod = class_getInstanceMethod(cls, selector);
   if (!originMethod) {
       return nil;
   }
   const char *originType = (char *)method_getTypeEncoding(originMethod);
   if (![[NSString stringWithUTF8String:originType] containsString:@"@?"]) {
       return nil;
   }
   NSMutableArray *blockArgIndex = [NSMutableArray array];
   int argIndex = 0; // return type is the first one
   while(originType && *originType)
   {
       originType = BHSizeAndAlignment(originType, NULL, NULL, NULL);
       if ([[NSString stringWithUTF8String:originType] hasPrefix:@"@?"]) {
           [blockArgIndex addObject:@(argIndex)];
       }
       argIndex++;
   }
   BTTracker *tracker = BTEngine.defaultEngine.trackers[bt_methodDescription(self, selector)];
   if (!tracker) {
       tracker = [[BTTracker alloc] initWithTarget:self selector:selector];
       tracker.callback = callback;
       tracker.blockArgIndex = [blockArgIndex copy];
   }
   return [tracker apply] ? tracker : nil;
}


bt_trackBlockArgOfSelector:callback:方法返回的BTTracker对象也保存了callback回调。


执行Callback


遍历之前保存的Block参数Index列表blockArgIndex,从NSInvocation中取到Block参数后,就可以Hook了。Block的执行次数保存到了BHToken上,每次执行都会累加。在Block执行或销毁后都会调用callback,只是传的参数稍有不同。


for (NSNumber *index in tracker.blockArgIndex) {
  if (index.integerValue < invocation.methodSignature.numberOfArguments) {
      __unsafe_unretained id block;
      [invocation getArgument:&block atIndex:index.integerValue];
      __weak typeof(block) weakBlock = block;
      __weak typeof(tracker) weakTracker = tracker;
      BHToken *tokenAfter = [block block_hookWithMode:BlockHookModeAfter usingBlock:^(BHToken *token) {
          __strong typeof(weakBlock) strongBlock = weakBlock;
          __strong typeof(weakTracker) strongTracker = weakTracker;
          NSNumber *invokeCount = objc_getAssociatedObject(token, NSSelectorFromString(@"invokeCount"));
          if (!invokeCount) {
              invokeCount = @(1);
          }
          else {
              invokeCount = [NSNumber numberWithInt:invokeCount.intValue + 1];
          }
          objc_setAssociatedObject(token, NSSelectorFromString(@"invokeCount"), invokeCount, OBJC_ASSOCIATION_RETAIN);
          if (strongTracker.callback) {
              strongTracker.callback(strongBlock, BlockTrackerCallbackTypeInvoke, invokeCount.intValue, token.args, token.retValue, [NSThread callStackSymbols]);
          }
      }];
      [block block_hookWithMode:BlockHookModeDead usingBlock:^(BHToken *token) {
          __strong typeof(weakTracker) strongTracker = weakTracker;
          NSNumber *invokeCount = objc_getAssociatedObject(tokenAfter, NSSelectorFromString(@"invokeCount"));
          if (strongTracker.callback) {
              strongTracker.callback(nil, BlockTrackerCallbackTypeDead, invokeCount.intValue, nil, nil, [NSThread callStackSymbols]);
          }
      }];
  }
}


对NSInvocation的一点探索


在从NSInvocation对象获取参数时,需要先调用retainArguments方法让NSInvocation将Block参数copy。因为有些Block参数类型是__NSStackBlock__,需要拷贝到堆上,否则从NSInvocation获取的Block不会销毁。


getArgument:atIndex: 方法只是将第index个参数指针的值拷贝到buffer中,而retainArguments才是真的对C字符串和Block拷贝。


我还为此做了个小实验。一个类外部声明并调用了test:方法,但其实内部实现的foo: 方法。通过实现methodSignatureForSelector:让消息转发流程走到forwardInvocation:方法中。然后向Block参数关联BTDealloc对象,在test:方法执行后,BTDealloc类的dealloc方法并没有执行。也就是说通过NSInvocation获取的Block参数没销毁;如果先调用了retainArguments就会销毁。


- (void)test:(void(^)(void))block;
- (void)foo: (void(^)(void)) block {
   block();
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
   return [NSMethodSignature signatureWithObjCTypes:"v@:@?"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//    [anInvocation retainArguments];
   void **invocationFrame = ((__bridge struct BTInvocaton *)anInvocation)->frame;
   void *blockFromFrame = invocationFrame[2];
   void *block;
   [anInvocation getArgument:&block atIndex:2];
   BTDealloc *btDealloc = [BTDealloc new];
   objc_setAssociatedObject((__bridge id)block, @selector(foo:), btDealloc, OBJC_ASSOCIATION_RETAIN);
   anInvocation.selector = @selector(foo:);
   [anInvocation invoke];
}


通过对NSInvocation对象的解析,我发现NSInvocation的参数存储于一个私有成员变量_frame中,试着将其强转为二级指针,也就是指针数组。拿到对应index的值blockFromFrame跟block作比较,发现是一样的。这里获取_frame需要强转下,NSInvocation的内存模型如下:


struct BTInvocaton {
   void *isa;
   void *frame;
   void *retdata;
   void *signature;
   void *container;
   uint8_t retainedArgs;
   uint8_t reserved[15];
};


总结


由于Hook Method的逻辑是在消息转发流程搞事情,所以跟Aspects一样不能同时Hook父类和子类类相同方法。因为如果子类调用父类的实现,就会死循环。如果Hook方法这部分使用等交换IMP的方式实现,也会有着严重依赖Hook顺序导致调用错乱的问题。还是基于桥的Hook牛逼,汇编跳板,我这辈子是看不懂了。


老子终于在这个月最后一天快结束的时候憋出来一篇大水文!搬砖累死了没时间研究技术,你们尽管喷!


相关推荐:


以上是关于追踪 Objective-C 方法中的 Block 参数对象的主要内容,如果未能解决你的问题,请参考以下文章

浅谈Objective-C中的block那些事

lldb快速打印Objective-C方法中block参数的签名

Objective-C中的Block

Objective-C中的Block

从C语言的变量声明到Objective-C中的Block语法

Objective-C(13)代码块---block