iOS-玩转Block(Hook Block 交换block的实现)

前方极其烧脑,建议->点赞再看


本文承接上一篇文章iOS-玩转Block(从入门到底层原理),如果还没看的话建议先阅读一下,会对block的底层原理有更深一层的理解,然后再阅读此文必会事半功倍。


这一篇文章的诞生源于我在网上看到一位大神
发的面试题,如下

以下题目都针对于任意void (^)(void)形式的block:

  • 1.实现下面的函数,将block的实现修改成NSLog(@"Hello world"),也就说,在调用完这个函数后调用block()时,并不调用原始实现,而是打"Hello world"
void HookBlockToPrintHelloWorld(id block){
    
}
  • 2.实现下面的函数,将block的实现修改成打印所有入参,并调用原始实现
//
//比如
//      void(^block)(int a, NSString *b) = ^(int a, NSString *b){
//          NSLog(@"block invoke");
//      }
//      HookBlockToPrintArguments(block);
//      block(123,@"aaa");
//      //这里输出"123, aaa"和"block invoke"
//
void HookBlockToPrintArguments(id block){
    
}
  • 3.实现下面的函数,使得调用这个函数之后,后面创建的任意block都能自动实现第二题的功能
void HookEveryBlockToPrintArguments(void){
    
}

刚看到题目的时候以为跟方法交换(Method Change)差不多的(不是很简单吗???),然后。。。
好吧,我错了,事情远远没有辣么简单


但是作为祖国的未来的希望,怎么能遇到问题就退缩呢。经过一番的思考之后,终于还是默默打开了百度。。。
这一搜,就进入了踩坑之路。。。关于hook block的实现网上并没有太多文章讨论,但还是会有几个大神默默分享,比如BlockHookfishhookBlockHookDemoYSBlockHook...

功能都非常强大,于是我把这些框架的源码都大概看了一遍,这才醍醐灌顶



扯淡时间结束


接下来我会以解决以上三道题为目的,分享实现的思路以及解决方案。

  • 第一题 交换block的实现
    首先我们来思考一下,如何交换一个Block的实现?
    我们很容易能联想到方法交换(关于方法交换推荐使用业内比较出名的库aspect,内部源码实现也是不同于一般人的做法,大神就是大神,建议阅读几遍!!!)
    方法交换的原理就是在运行期间动态交换两个方法所指向的IMP指针,那么换作Block也是一样的道理,只不过难度要大得多。
    通过阅读苹果官方源码libclosure,前面我们分析Block结构体的时候有提到内部存储着invoke指针,封装着函数(方法)的地址,所以不难想到,只要将invoke指针指向我们自定义的函数地址,就可以交换block的实现了。
    仿造苹果源码构造了block相关的结构体,方便我们操作
typedef void(*BlockCopyFunction)(void *, const void *);
typedef void(*BlockDisposeFunction)(const void *);
typedef void(*BlockInvokeFunction)(void *, ...);

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

typedef struct Block_layout *CJJBlockLayout;

#if 0
static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
{
    return aBlock->descriptor;
}
#endif

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}

定义一个类方法,将原始block和目标block作为参数传入,将它们转为CJJBlockLayout类型的结构体,分别取出它们的invoke指针,然后相互交换invoke指针的指向

//第一题
//- **1.实现下面的函数,将`block`的实现修改成`NSL(@"Hello world")`,也就说,在调用完这个函数后调用`block()`时,并不调用原始实现,而是打"`Hello world`"**
//```
void HookBlockToPrintHelloWorld(id block){
    [CJJBlockHook hookOriginBlock:block hookBlock:^{
        NSLog(@"第一题替换后:Hello world");
    }];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    void (^originBlock1)(void) = ^{
          NSLog(@"第一题替换前:你好");
    };
    originBlock1();
    HookBlockToPrintHelloWorld(originBlock1);
    originBlock1();
}

+ (void)hookOriginBlock:(id)originBlock hookBlock:(id)hookBlock{
    NSParameterAssert(originBlock);
    NSParameterAssert(hookBlock);
    CJJBlockLayout originLayout = (__bridge CJJBlockLayout)originBlock;
    CJJBlockLayout hookLayout = (__bridge CJJBlockLayout)hookBlock;
    BlockInvokeFunction originInvoke = originLayout->invoke;
    BlockInvokeFunction hookInvoke = hookLayout->invoke;
    originLayout->invoke = hookInvoke;
    hookLayout->invoke = originInvoke;
}

我们在交换前和交换后都调用一次originBlock1(),输出结果如下

2020-09-01 22:02:13.690715+0800 CJJBlockHook[4421:71893] 第一题替换前:你好
2020-09-01 22:02:13.690900+0800 CJJBlockHook[4421:71893] 第一题替换后:Hello world

可以看到调用了函数HookBlockToPrintHelloWorld(originBlock1)后再调用block,并没有调用原始实现,而是打印"Hello world",第一题解决,非常简单。

  • 第二题 将某个block的实现修改成打印所有入参,并调用原始实现
    第一题的那种做法虽然简单,但仅适用于交换两个block的实现,并不能实现更多的功能,比如在调用原来的block实现前或者实现后注入自己的代码,而这一题显示是要我们在不影响原来的调用的情况下在前面注入代码,打印传入的参数。这时候我们可以利用runtime消息转发流程来实现。
+ (void)hookPrintParamsOriginBlock:(id)originBlock{
    [self hookOriginBlock:originBlock hookBlock:^{} position:CJJBlockHookPositionDoNothing];
}

+ (void)hookOriginBlock:(id)originBlock hookBlock:(id)hookBlock position:(CJJBlockHookPosition)position{
    NSParameterAssert(originBlock);
    NSParameterAssert(hookBlock);
    CJJBlockLayout originLayout = (__bridge CJJBlockLayout)originBlock;
    CJJBlockLayout hookLayout = (__bridge CJJBlockLayout)hookBlock;
    cjjHook_setPosition(originLayout, position);
    cjjHook_setHookBlock(originLayout, hookLayout);
    cjjHook_handleBlock(originBlock);
}

我们仅需要传入原来的block就可以了,同样,先将block转化CJJBlockLayout结构体,然后保存position

typedef NS_ENUM(NSUInteger, CJJBlockHookPosition) {
    CJJBlockHookPositionBefore = 0,
    CJJBlockHookPositionAfter,
    CJJBlockHookPositionReplace,
    CJJBlockHookPositionDoNothing
};

使用的是关联对象技术

static NSString * const CJJBlockHookKey_Position = @"CJJBlockHookKey_Position";

static void cjjHook_setPosition(CJJBlockLayout originLayout, CJJBlockHookPosition position){
    objc_setAssociatedObject((__bridge id)originLayout, CJJBlockHookKey_Position.UTF8String, @(position), OBJC_ASSOCIATION_ASSIGN);
}

static NSNumber * cjjHook_getPosition(CJJBlockLayout originLayout){
    NSNumber *position = objc_getAssociatedObject((__bridge id)originLayout, CJJBlockHookKey_Position.UTF8String);
    if(!position){
        position = @(CJJBlockHookPositionReplace);
    }
    return position;
}

可以在block前后调用自己的实现或者直接替换掉原始block的实现,这里我们可以选择CJJBlockHookPositionDoNothing,什么也不做,因为仅仅是打印参数而已,后面会讲到。

保存自定义的blockCJJBlockHookPositionBeforeCJJBlockHookPositionAfterCJJBlockHookPositionReplace这三种情况用到)

cjjHook_setHookBlock(originLayout, hookLayout);

同样使用的也是关联对象技术

static NSString * const CJJBlockHookKey_HookBlock = @"CJJBlockHookKey_HookBlock";

static void cjjHook_setHookBlock(CJJBlockLayout originLayout, CJJBlockLayout hookLayout){
    objc_setAssociatedObject((__bridge id)originLayout, CJJBlockHookKey_HookBlock.UTF8String, (__bridge id)hookLayout, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

static id cjjHook_getHookBlock(CJJBlockLayout blockLayout){
    return objc_getAssociatedObject((__bridge id)blockLayout, CJJBlockHookKey_HookBlock.UTF8String);
}

重点从这里开始

cjjHook_handleBlock(originBlock);

我们来看看它的实现

static void cjjHook_handleBlock(id originBlock){
    //swizzling系统的消息转发方法
    cjjHook_hookMsgForwardMethod();
    
    //拷贝origin block
    CJJBlockLayout layout = (__bridge CJJBlockLayout)originBlock;
    if(!cjjHook_getOriginBlock(layout)){
        //深拷贝一份origin block的副本
        cjjHook_deepCopy(layout);
        //取出签名
        struct Block_descriptor_3 *desc3 = _Block_descriptor_3(layout);
        //指向消息转发函数
        layout->invoke = (void *)cjjHook_getMsgForward(desc3->signature);
    }
}

首先交换了系统消息转发流程的最后两个方法methodSignatureForSelector:forwardInvocation:,因为在forwardInvocation:里面我们可以做任何我们想做的事情。

static void cjjHook_hookMethod(SEL originSel, IMP hookIMP){
    Class cls = NSClassFromString(@"NSBlock");
    Method method = class_getInstanceMethod([NSObject class], originSel);
    BOOL success = class_addMethod(cls, originSel, hookIMP, method_getTypeEncoding(method));
    if(!success){
        class_replaceMethod(cls, originSel, hookIMP, method_getTypeEncoding(method));
    }
}

static void cjjHook_hookMsgForwardMethod(){
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cjjHook_hookMethod(@selector(methodSignatureForSelector:), (IMP)cjjHook_methodSignatureForSelector);
        cjjHook_hookMethod(@selector(forwardInvocation:), (IMP)cjjHook_forwardInvocation);
    });
}

接着把原来的block拷贝一份保存起来,因为后面会将原来的blockinvoke指针指向_objc_msgForward或者_objc_msgForward_stret,为了之后还能调用原来的实现,所以这里需要拷贝一份。拷贝的方法也是参考苹果源码里的_Block_copy方法,有兴趣的话可以直接阅读源码研究研究

static void cjjHook_deepCopy(CJJBlockLayout layout) {
    struct Block_descriptor_2 *desc_2 = _Block_descriptor_2(layout);
    //如果捕获的变量存在对象或者被__block修饰的变量时,在__main_block_desc_0函数内部会增加copy跟dispose函数,copy函数内部会根据修饰类型(weak or strong)对对象进行强引用还是弱引用,当block释放之后会进行dispose函数,release掉修饰对象的引用,如果都没有引用对象,将对象释放

    if (desc_2) {
        CJJBlockLayout newLayout = malloc(layout->descriptor->size);
        if (!newLayout) {
            return;
        }
        memmove(newLayout, layout, layout->descriptor->size);
        newLayout->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
        newLayout->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        
        (desc_2->copy)(newLayout, layout);
        cjjHook_setOriginBlock(layout, newLayout);
    } else {
        //FishBind缺陷:以前那种grouph方式没办法拷贝变量
        CJJBlockLayout newLayout = malloc(layout->descriptor->size);
        if (!newLayout) {
            return;
        }
        memmove(newLayout, layout, layout->descriptor->size);
        newLayout->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
        newLayout->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        cjjHook_setOriginBlock(layout, newLayout);
    }
}

if(!cjjHook_getOriginBlock(layout)){
        //深拷贝一份origin block的副本
        cjjHook_deepCopy(layout);
        //取出签名
        struct Block_descriptor_3 *desc3 = _Block_descriptor_3(layout);
        //指向消息转发函数
        layout->invoke = (void *)cjjHook_getMsgForward(desc3->signature);
    }

如果原来没有保存过originBlock,就深拷贝一份,拷贝完后同样利用关联对象技术保存起来

static NSString * const CJJBlockHookKey_OriginBlock = @"CJJBlockHookKey_OriginBlock";

static void cjjHook_setOriginBlock(CJJBlockLayout originLayout, CJJBlockLayout originLayoutCopy){
    objc_setAssociatedObject((__bridge id)originLayout, CJJBlockHookKey_OriginBlock.UTF8String, (__bridge id)originLayoutCopy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

static id cjjHook_getOriginBlock(CJJBlockLayout blockLayout){
    return objc_getAssociatedObject((__bridge id)blockLayout, CJJBlockHookKey_OriginBlock.UTF8String);
}

取出desc3里面的signatureblock的签名)赋值给_objc_msgForward函数

// code from
// https://github.com/bang590/JSPatch/blob/master/JSPatch/JPEngine.m
// line 975
static IMP cjjHook_getMsgForward(const char *methodTypes) {
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    if (methodTypes[0] == '{') {
        NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:methodTypes];
        if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
            msgForwardIMP = (IMP)_objc_msgForward_stret;
        }
    }
#endif
    return msgForwardIMP;
}

让原来的block里的invoke指向它,那么一旦我们调用原来的block,就会触发消息转发机制,转发到_objc_msgForward函数,到这里我们已经完成了一大半了,剩下的就是解析block以及取出它的参数并且打印出来。
我们来看看被我们hook掉的methodSignatureForSelector:forwardInvocation:函数

//拿到原来block的签名并返回
static NSMethodSignature * cjjHook_methodSignatureForSelector(id self, SEL _cmd, SEL aSelector){
    struct Block_descriptor_3 *desc3 = _Block_descriptor_3((__bridge void *)self);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:desc3->signature];
    return signature;
}

static void cjjHook_forwardInvocation(id self, SEL _cmd,NSInvocation *anInvocation){
    //拿到invoke指针被交换过的block结构体(注意,这个已经不是原来的block了)
    CJJBlockLayout originLayout = (__bridge void *)anInvocation.target;
    //取出我们之前保存的自定义block结构体
    CJJBlockLayout hookLayout = (__bridge void *)cjjHook_getHookBlock(originLayout);
    //利用originLayout取出我们之前保存的原来的block结构体
    CJJBlockLayout originCopyLayout = (__bridge void *)cjjHook_getOriginBlock(originLayout);
    //在头文件中加了个宏定义CJJBlockHookParamsLog用于打印参数的开关
    if(CJJBlockHookParamsLog == 1){
        //如果为1,就打印传入的参数
        NSString *paramsString = @"";
        //通过遍历签名的参数个数,为什么从1开始?因为block的第一个参数是它自己本身,从第二个开始才是我们传入的参数
        for (int i = 1; i < anInvocation.methodSignature.numberOfArguments; i++) {
            NSString *part = [NSString stringWithFormat:@"%@",[anInvocation cjjHook_argumentAtIndex:i]];
            if(!NullStr(part)){
                if(NullStr(paramsString)){
                    paramsString = [paramsString stringByAppendingString:[NSString stringWithFormat:@"%@",part]];
                }else{
                    paramsString = [paramsString stringByAppendingString:[NSString stringWithFormat:@",%@",part]];
                }
            }
        }
        NSLog(@"%@",paramsString);        
    }
    
    //根据之前保存的position来判断业务逻辑
    NSNumber *position = cjjHook_getPosition(originLayout);
    switch (position.integerValue) {
        case CJJBlockHookPositionBefore:
            [anInvocation invokeWithTarget:(__bridge id _Nonnull)hookLayout];
            [anInvocation invokeWithTarget:(__bridge id _Nonnull)originCopyLayout];
            break;
        case CJJBlockHookPositionAfter:
            [anInvocation invokeWithTarget:(__bridge id _Nonnull)originCopyLayout];
            [anInvocation invokeWithTarget:(__bridge id _Nonnull)hookLayout];
            break;
        case CJJBlockHookPositionReplace:
            [anInvocation invokeWithTarget:(__bridge id _Nonnull)hookLayout];
            break;
        case CJJBlockHookPositionDoNothing:
            [anInvocation invokeWithTarget:(__bridge id _Nonnull)originCopyLayout];
            break;
        default:
            NSLog(@"类型错误");
            break;
    }
}

关于取参[anInvocation cjjHook_argumentAtIndex:i]
我们来看看实现

@implementation NSInvocation (CJJBlockHook)

// Thanks to the ReactiveCocoa team for providing a generic solution for this.
- (id)cjjHook_argumentAtIndex:(NSUInteger)index {
    const char *argType = [self.methodSignature getArgumentTypeAtIndex:index];
    // Skip const type qualifier.
    if (argType[0] == _C_CONST) argType++;

#define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0)
    if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) {
        __autoreleasing id returnObj;
        [self getArgument:&returnObj atIndex:(NSInteger)index];
        return returnObj;
    }else if (strcmp(argType, "@\"NSString\"") == 0){
        NSString *val = @"";
        [self getArgument:&val atIndex:(NSInteger)index];
        return val;
    } else if (strcmp(argType, @encode(SEL)) == 0) {
        SEL selector = 0;
        [self getArgument:&selector atIndex:(NSInteger)index];
        return NSStringFromSelector(selector);
    } else if (strcmp(argType, @encode(Class)) == 0) {
        __autoreleasing Class theClass = Nil;
        [self getArgument:&theClass atIndex:(NSInteger)index];
        return theClass;
        // Using this list will box the number with the appropriate constructor, instead of the generic NSValue.
    } else if (strcmp(argType, @encode(char)) == 0) {
        WRAP_AND_RETURN(char);
    } else if (strcmp(argType, @encode(int)) == 0) {
        WRAP_AND_RETURN(int);
    } else if (strcmp(argType, @encode(short)) == 0) {
        WRAP_AND_RETURN(short);
    } else if (strcmp(argType, @encode(long)) == 0) {
        WRAP_AND_RETURN(long);
    } else if (strcmp(argType, @encode(long long)) == 0) {
        WRAP_AND_RETURN(long long);
    } else if (strcmp(argType, @encode(unsigned char)) == 0) {
        WRAP_AND_RETURN(unsigned char);
    } else if (strcmp(argType, @encode(unsigned int)) == 0) {
        WRAP_AND_RETURN(unsigned int);
    } else if (strcmp(argType, @encode(unsigned short)) == 0) {
        WRAP_AND_RETURN(unsigned short);
    } else if (strcmp(argType, @encode(unsigned long)) == 0) {
        WRAP_AND_RETURN(unsigned long);
    } else if (strcmp(argType, @encode(unsigned long long)) == 0) {
        WRAP_AND_RETURN(unsigned long long);
    } else if (strcmp(argType, @encode(float)) == 0) {
        WRAP_AND_RETURN(float);
    } else if (strcmp(argType, @encode(double)) == 0) {
        WRAP_AND_RETURN(double);
    } else if (strcmp(argType, @encode(BOOL)) == 0) {
        WRAP_AND_RETURN(BOOL);
    } else if (strcmp(argType, @encode(bool)) == 0) {
        WRAP_AND_RETURN(BOOL);
    } else if (strcmp(argType, @encode(char *)) == 0) {
        WRAP_AND_RETURN(const char *);
    } else if (strcmp(argType, @encode(void (^)(void))) == 0) {
        __unsafe_unretained id block = nil;
        [self getArgument:&block atIndex:(NSInteger)index];
        return [block copy];
    } else {
        NSUInteger valueSize = 0;
        NSGetSizeAndAlignment(argType, &valueSize, NULL);

        unsigned char valueBytes[valueSize];
        [self getArgument:valueBytes atIndex:(NSInteger)index];

        return [NSValue valueWithBytes:valueBytes objCType:argType];
    }
    return nil;
#undef WRAP_AND_RETURN
}

@end

这里用到的是ReactiveCocoa团队开源的方法,兼容各种参数类型,其中这一个

else if (strcmp(argType, "@\"NSString\"") == 0){
        NSString *val = @"";
        [self getArgument:&val atIndex:(NSInteger)index];
        return val;
    } 

是我自己加进去的,因为NSString类型它并没有判断到,打印的不是我们想要的,所以我作了修改。
总结:把原来的block和自定义的block分别保存起来,再将原来的block指向消息转发函数,使得一调用原来的block就启动消息转发流程到达_objc_msgForward,在这个函数我们再把之前保存的block以及position取出来,完成业务逻辑处理,并在调用前取出传入的参数打印出来。

//```
//- **2.实现下面的函数,将`block`的实现修改成打印所有入参,并调用原始实现**
//```
////
//比如
//      void(^block)(int a, NSString *b) = ^(int a, NSString *b){
//          NSLog(@"block invoke");
//      }
//      HookBlockToPrintArguments(block);
//      block(123,@"aaa");
//      //这里输出"123, aaa"和"block invoke"
//
void HookBlockToPrintArguments(id block){
//    [CJJBlockHook hookOriginBlock:block hookBlock:^{
//        NSLog(@"Hello world");
//    } position:CJJBlockHookPositionDoNothing];
    [CJJBlockHook hookPrintParamsOriginBlock:block];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //第二题
    void (^originBlock2)(int a, id b, NSString *string) = ^(int a, id b, NSString *string){
        NSLog(@"第二题原实现:%d-%@-%@",a,b,string);
    };
    HookBlockToPrintArguments(originBlock2);
    originBlock2(2,@"笔",@"爱上");
 }

我们在调用HookBlockToPrintArguments(originBlock2)后再调用originBlock2(2,@"笔",@"爱上"),输出结果如下

2020-09-01 22:02:13.691192+0800 CJJBlockHook[4421:71893] 2,笔,爱上
2020-09-01 22:02:13.691283+0800 CJJBlockHook[4421:71893] 第二题原实现:2-笔-爱上

可以看到依次打印了传入的参数,然后才调用原始实现,这样就解决了第二题的需求了。

  • 第三题 将所有block的实现修改成打印所有入参,并调用原始实现
    这个比较难一点,解法参照这里:运行时Hook所有Block方法调用的技术实现
    原理是利用fishhook,谷歌开源的框架,用于交换C函数的实现。
    因为对block进行赋值或者拷贝都会调用_Block_copy函数,所以我们可以利用fishhook库来将其替换成我们自定义的方法的实现
extern const struct mach_header* _NSGetMachExecuteHeader(void);

//声明统一的block的hook函数,这个函数的定义是用汇编代码来实现,具体实现在blockhook-arm64.s/blockhook-x86_64.s中。
extern void blockhook(void);
extern void blockhook_stret(void);

//这两个全局变量保存可执行程序的代码段+数据段的开始和结束位置。
unsigned long imageTextStart = 0;
unsigned long imageTextEnd = 0;
void initImageTextStartAndEndPos()
{
    imageTextStart = (unsigned long)_NSGetMachExecuteHeader();
#ifdef __LP64__
    const struct segment_command_64 *psegment = getsegbyname("__TEXT");
#else
    const struct segment_command *psegment = getsegbyname("__TEXT");
#endif
    //imageTextEnd  等于代码段和数据段的结尾 + 对应的slide值。
    imageTextEnd = get_end() + imageTextStart - psegment->vmaddr;
}

/**
 替换block对象的默认invoke实现

 @param blockObj block对象
 */
void replaceBlockInvokeFunction(const void *blockObj)
{
    //任何一个block对象都可以转化为一个struct Block_layout结构体。
    struct Block_layout *layout = (struct Block_layout*)blockObj;
    if (layout != NULL && layout->descriptor != NULL)
    {
        //这里只hook一个可执行程序image范围内定义的block代码块。
        //因为imageTextStart和imageTextEnd表示可执行程序的代码范围,因此如果某个block是在可执行程序中被定义
        //那么其invoke函数地址就一定是在(imageTextStart,imageTextEnd)范围内。
        //如果将这个条件语句去除就会hook进程中所有的block对象!
        unsigned long invokePos = (unsigned long)layout->invoke;
        if (invokePos > imageTextStart && invokePos < imageTextEnd)
        {
            //将默认的invoke实现保存到保留字段,将统一的hook函数赋值给invoke成员。
            int32_t BLOCK_USE_STRET = (1 << 29);  //如果模拟器下返回的类型是一个大于16字节的结构体,那么block的第一个参数为返回的指针,而不是block对象。
            void *hookfunc = ((layout->flags & BLOCK_USE_STRET) == BLOCK_USE_STRET) ? blockhook_stret : blockhook;
            if (layout->invoke != hookfunc)
            {
                layout->descriptor->reserved = (uintptr_t)layout->invoke;
                layout->invoke = hookfunc;
                //打印参数
                [CJJBlockHook hookPrintParamsOriginBlock:(__bridge id)layout];
            }
        }
    }
    
}

void * (*old_Block_copy)(const void *aBlock);

void *my_Block_copy(const void *aBlock){
    struct Block_layout *block;
    if(!aBlock) return NULL;
    block = (struct Block_layout *)aBlock;
    replaceBlockInvokeFunction(block);
    return old_Block_copy(block);
}

//所有block调用前都会执行blockhookLog,这里的实现就是简单的将block对象的函数符号打印出来!
void blockhookLog(void *blockObj)
{
    struct Block_layout *layout = blockObj;
    
    //注意这段代码在线上的程序是无法获取到符号信息的,因为线上的程序中会删除掉所有block实现函数的符号信息。
    Dl_info dlinfo;
    memset(&dlinfo, 0, sizeof(dlinfo));
    if (dladdr((const void *)layout->descriptor->reserved, &dlinfo))
    {
//        NSLog(@"%s be called with block object:%@", dlinfo.dli_sname, blockObj);
        //打印入参
//        [CJJBlockHook hookPrintParamsOriginBlock:(__bridge id)layout];
    }
}

具体调用方法如下

+ (void)fishHook{
    //初始化并计算可执行程序代码段和数据段的开始和结束位置。
    initImageTextStartAndEndPos();
    
    struct rebinding rebns[1] = {"_Block_copy",my_Block_copy,(void **)&old_Block_copy};
    rebind_symbols(rebns, 1);
}

其中还用到了汇编。。。这里的实现流程可以直接阅读这位大神写的文章,我就不再赘述了。

//```
//- **3.实现下面的函数,使得调用这个函数之后,后面创建的任意`block`都能自动实现第二题的功能**
//```
void HookEveryBlockToPrintArguments(void){
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [CJJBlockHook fishHook];
    });
}
//``

- (void)viewDidLoad {
    [super viewDidLoad];
    //第三题
    HookEveryBlockToPrintArguments();
    void (^originBlock3)(NSString *str, int num, CGFloat meter) = ^(NSString *str, int num, CGFloat meter){
        NSLog(@"第三题原实现:%@-%d-%f",str,num,meter);
    };
    originBlock3(@"呵呵",33,4.0);
}

我们在调用HookEveryBlockToPrintArguments()后再调用任意的block,这里是originBlock3(@"呵呵",33,4.0),输出结果如下

2020-09-01 22:02:13.699379+0800 CJJBlockHook[4421:71893] 呵呵,33,4
2020-09-01 22:02:13.699554+0800 CJJBlockHook[4421:71893] 第三题原实现:呵呵-33-4.000000

可以看到之后任意一个我们定义的block的调用都会先打印了传入的参数,然后才调用原始实现,第三题解决!

demo在这里->CJJBlockHook
我把这些方法都封装到CJJBlockHook里面,仅供大家学习,暂时不建议商用,喜欢的点个赞支持一下,或者你有更好的想法欢迎issue我,共同探讨学习!
·

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容