Source code

Trace the Block parameter object in Objective-C method


Article Contents

1. Usage

2. Implementation principle

2.1. Block parameter of filter method

2.2. Execute Callback

2.3. A little exploration of NSInvocation

3. Summary

The last parameter of many methods is a callback similar to completionBlock. However, some APIs forget to call the incoming Block parameter when implementing some exception logic (of course, this must be a bug), or there are multiple calls. When debugging, you may encounter such pitfalls. You need to track when the Block parameter was called, or even whether it was called. If it is not convenient to add code directly in the Block implementation, or if there is no source code, you need to track the Block parameter object non invasively.

BlockTracker You can track the execution and destruction of the parameters of the Block type passed in when the method is called. It is implemented based on BlockHook. This paper describes its use method and implementation principle.

usage method

Just call the bt_trackBlockArgOfSelector: callback: method to get a callback when the corresponding method executes the incoming block parameter, which is called and destroyed. The contents of the callback include the block object, the callback type, the number of times the block has been executed, the parameters and return values of the executed block, and the stack information.

 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); }];

When you do not want to track the block parameters passed in during the execution of this method, you can also stop tracking:

 [tracker stop];

For example, there is a method called performBlock:, which simply calls the block parameter:

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

Call this method twice, and each time a different block implementation is passed in:

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

Since two different block objects are passed in by executing the method twice, the execution and destruction of the two block objects will be tracked. The printed log is as follows:

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

When the block object is destroyed

You can try to change the implementation of performBlock: to this:

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

Implementation principle

The principle is very simple, that is, the Hook method is followed by the Hook block, and the process is roughly as follows:

  1. Use the Objective-C Runtime mechanism to hook a method, refer to MessageThrottle Implementation principle of.

  2. Before the method is actually executed, use the BlockHook Hook all block type parameters first. Hook modes are BlockHookModeAfter and BlockHookModeDead.

  3. Update the execution times after the block is executed, and call back the relevant information to the Tracker. After destruction, it will also be called back to the Tracker.

The process is simple, reusing the previous code. Here we will mainly talk about the logic of Track.

Block parameter of filter method

Get the Type Encoding of the method in bt_trackBlockArgOfSelector: callback:, then judge whether it contains parameters of Block type, and save the Index of the Block parameter to the blockArgIndex property of BTTracker.

 - (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: The BTTracker object returned by the method also saves the callback callback.

Execute Callback

Traverse the previously saved Block parameter Index list blockArgIndex. After obtaining the Block parameter from NSInvocation, you can hook. The number of block executions is saved to the BHToken, and each execution will be accumulated. Callback will be called after the block is executed or destroyed, but the parameters passed are slightly different.

 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]);            }        }];    } }

A little exploration of NSInvocation

When obtaining parameters from the NSInvocation object, you need to call the retainArguments method first to let NSInvocation copy the Block parameter. Because some Block parameters are __NSStackBlock__, they need to be copied to the heap, otherwise the Block obtained from NSInvocation will not be destroyed.

The getArgument: atIndex: method only copies the value of the index parameter pointer to the buffer, while the reteinArguments is a true copy of the C string and block.

I also did a small experiment for this. A class declares and calls the test: method externally, but it actually implements the foo: method internally. By implementing methodSignatureForSelector:, the message forwarding process is moved to the forwardInvocation: method. Then associate the BTDealloc object with the Block parameter. After the test: method is executed, the dealloc method of the BTDealloc class is not executed. That is, the Block parameter obtained through NSInvocation is not destroyed; If reteinArguments is called first, it will be destroyed.

 - (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]; }

Through the analysis of the NSInvocation object, I found that the parameters of NSInvocation are stored in a private member variable _frame, and try to convert it into a secondary pointer, that is, a pointer array. Get the value blockFromFrame of the corresponding index and compare it with block. It is found that it is the same. Here, you need to forcibly download the _frame. The memory model of NSInvocation is as follows:

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

summary

Since the logic of the Hook Method is to do things in the message forwarding process, it is the same as Aspects. It cannot use the same method for the parent class and child class of the Hook Method at the same time. Because if the subclass calls the implementation of the parent class, it will be in an endless loop. If the Hook method uses Method Swizzling The implementation of exchange IMP will also rely heavily on the hook order, resulting in call confusion. The bridge based Hook is awesome, and the assembly springboard is beyond my comprehension in my life.

At the end of the last day of this month, I finally came up with a big article on hydrology! I'm tired of moving bricks. I don't have time to study technology. Just spray!

fabulous ( one )

This article is written by Contributors Author, article address: https://blog.isoyu.com/archives/zhuizongobjective-cfangfazhongdeblockcanshuduixiang.html
use Knowledge Sharing Attribution 4.0 International License Agreement. Unless the reprint/source is indicated, they are all original or translated by this website. Please sign your name before reprinting. Last editing time: April 25, 2018 at 10:19 p.m

Popular articles

Post reply

[Required]

I am a human?

Please wait three seconds after submission to avoid unsubmission and repetition