Block 签名信息的使用

Block在OC中却是一个非常神奇的存在,即具备了对象的特性(具备isa指针),又具备了方法的特性(具有自己的实现函数),这就自然而然注定了Block会与众不同.除了常规使用之外,还可以利用Block做一些"骚操作".

获取Block签名

在OC中中所有的方法都有编码信息,签名信息包含了方法的参数类型和返回值类型等信息.对于对象的实例方法可以利用

        Method method = class_getInstanceMethod([NSObject class], @selector(performSelector:withObject:));
        const char *types = method_getTypeEncoding(method);
        printf("types == %s\n", types);

//输出
types == @32@0:8:16@24

对于类方法可以使用:

        Method method = class_getClassMethod([NSObject class], @selector(copyWithZone:));
        const char *types = method_getTypeEncoding(method);
        printf("types == %s\n", types);

//输出
types == @24@0:8^{_NSZone=}16

获取到方法的签名就可以知道执行方法所需要的参数个数,各个参数的类型以及返回结果类型,执行方法所需要的条件也就随之确定,就可以对方法进行各种各样的操作,例如将方法添加到指定的类中.

既然Block是"方法",方法编码肯定是有的.问题是如何获取该签名呢?

开源的Objc源代码中, 找到了关于block实现的相关细节:

// 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
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#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; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

可以看出在Block的结构分布中:

  • isa: 之所以Block能具备对象的特性也是由于Block的结构中具备了isa指针;
  • flags: Block中的标识位,保存了相关的标识信息,包括是否释放,Block的引用计数信息,是否需要释放Block,是否包含copy,dispose函数,是否具备签名等信息;
  • reserved:预留信息位,默认为0;
  • invoke: 是函数实现的地址;
  • Block_descriptor_1 : Block的描述信息结构体,该结构是动态生成的.一般来说都包含BLOCK_DESCRIPTOR_1,但是是否包含BLOCK_DESCRIPTOR_2和BLOCK_DESCRIPTOR_3需要根据实际的需求来确定.而flags中记录Block实现中是否包含这样的描述结构,(flags & BLOCK_HAS_COPY_DISPOSE)为真则证明包含BLOCK_DESCRIPTOR_2;(flags & BLOCK_HAS_SIGNATURE)为真则证明包含了BLOCK_DESCRIPTOR_3.
  • 其他引入变量.

所以从Block的实现中可以得到启发.通过移动Block的首地址指针来找到Block的签名信息.

static char *getBlockTypes(id blk){
    blk = [blk copy];
    char *types = NULL;
    //1. 转化Block实现地址为void *
    void *_pointer = (__bridge void *)blk;
    //2. 移动指针到falgs
    _pointer = _pointer + sizeof(void *) /* void *isa */;
    //3. 获取flags
    int32_t flags = *(int *)(_pointer);
    
    //4. 判断是否包含签名
    if (!(flags & (1 << 30))) {
        return types;
    }
    
    
    //5. 移动指针到Block_descriptor_1
    _pointer += sizeof(void *) +2 * sizeof(int32_t) ;
    //6. 获取Block_descriptor_1
    _pointer = *(void **)_pointer;
    //7. 移动指针到Block_descriptor_1结尾处(uintptr_t reserved;uintptr_t size;)
    _pointer += 2 * sizeof(uintptr_t);
    //8. 判断是否包含copy和dispose函数
    if (flags & (1 << 25)) {
        _pointer += 2 * sizeof(void *);
    }
    //9. 判断是否包含签名
    types = *(char **)_pointer;
    NSLog(@"types == %s", types);
    
    return types;
}

通过这种直接移动指针的方法可以不依赖任何外部结构查询到Block的签名信息.但是看起来不太优雅,而且有些关于指针的地方可能不太好理解.为了更加方便理解和阅读,对这个实现进行一下简化.

static char *getBlockTypes(id blk){
    blk = [blk copy];
    
    //1. 定义枚举类型
    enum {
        BLOCK_HAS_COPY_DISPOSE = 1 << 25,
        BLOCK_HAS_SIGNATURE = 1 << 30
        
    };
    
    //2. 定义Block_layout结构
    typedef struct {
        void *isa;
        volatile int32_t flags;
        int32_t reserved;
        void(*invoke)(void *, ...);
        struct Block_descriptor {
            uintptr_t reserved;
            uintptr_t size;
            //需要注意的是:以下部分虽然定义在了Block_layout中但是不一定存在,所以不能直接获取
            // requires BLOCK_HAS_COPY_DISPOSE
            void (*copy)(void *dst, const void *src);
            void (*dispose)(const void *);
            // requires BLOCK_HAS_SIGNATURE
            const char *signature;
            const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
        } *descriptor;
    } *Block_layout;
    
    //3. 强制转化void *
    Block_layout block_layout = (__bridge Block_layout)blk;
    //4. 获取descriptor指针
    void *pointer = block_layout->descriptor;
    
    //5. 获取reserved && size结束位置
    pointer += 2 * sizeof(uintptr_t);
    //6. 判断是否存在签名
    if (!(block_layout -> flags & BLOCK_HAS_SIGNATURE)) {
        return NULL;
    }
    //7. 判断是否存在copy和dispose函数
    if (block_layout-> flags & BLOCK_HAS_COPY_DISPOSE) {
        pointer += 2 * sizeof(void *);
    }
    return *(char **)pointer;
}

需要注意的是:

虽然在自定义的Block_layout结构中定义了

            // requires BLOCK_HAS_COPY_DISPOSE
            void (*copy)(void *dst, const void *src);
            void (*dispose)(const void *);
            // requires BLOCK_HAS_SIGNATURE
            const char *signature;
            const char *layout;     

这部分变量但是不能直接使用

char *types = block_layout->descriptor->signature;

来获取签名信息,因为中间copy和dispose函数是否存在需要根据flags标记位中的信息来判断,直接访问则是默认了copy和dispose函数的存在进行游标的移动,可能会访问到非法的内存空间.

当然如果你还是觉得不够简单的话,可以进一步对Block的结果进行拆解.

static char *getBlockTypes(id blk){
    blk = [blk copy];
    //定义block基本实现
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    
    //定义block完整实现
    struct __main_block_impl {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
    };
    struct __main_block_impl *imp = (__bridge struct __main_block_impl *)blk;
    struct __block_impl impl = imp->impl;
    struct __main_block_desc_0 *desc = imp->Desc;
    if (impl.Flags & (1 << 30)) {
        void *pointer = desc;
        pointer += 2 * sizeof(uintptr_t);
        if(impl.Flags & (1 << 25)) {
            //存在dispose and copy
            pointer += 2 * sizeof(void *);
            return *(char **)pointer;
        }
    } else {
        printf("不存在签名");
    }
    
    return NULL;
}


这种结构在Block的C/C++实现中将会经常见到.

获取Block方法实现

通过以上的努力已经获取到了方法的签名,然后就可以做一些有意义的事情.动态替换类的方法实现一般会使用类C的方法或者OC的方法来进行,而事实上使用Block也是可以的.获取Block的方法实现(IMP),一般有两种方法:

  • 直接根据内存分布获取方法
typedef void(*InvokeType)(void *,...);
static InvokeType getImplement(id blk){
    blk = [blk copy];
    void *pointer = (__bridge void *)blk;
    pointer += sizeof(void *) + 2 * sizeof(int32_t);
    typedef void(*InvokeType)(void *,...);
    InvokeType invoke = *(InvokeType*)pointer;
    return invoke;
}
  • 使用系统提供的获取方法

在objc/runtime.h中暴露了可以直接获取Block实现的方法.

/** 
 * Creates a pointer to a function that will call the block
 * when the method is called.
 * 
 * @param block The block that implements this method. Its signature should
 *  be: method_return_type ^(id self, method_args...). 
 *  The selector is not available as a parameter to this block.
 *  The block is copied with \c Block_copy().
 * 
 * @return The IMP that calls this block. Must be disposed of with
 *  \c imp_removeBlock.
 */
OBJC_EXPORT IMP _Nonnull
imp_implementationWithBlock(id _Nonnull block)
    OBJC_AVAILABLE(10.7, 4.3, 9.0, 1.0, 2.0);

这样的话就可以直接使用:

IMP imp = imp_implementationWithBlock(blk);

而这两种实现方式在内部处理上稍有不同,通过移动内存分布来直接获取Block函数实现时第一个参数必须是对象类型(Block默认的第一个参数是Block自身).而通过系统提供的imp_implementationWithBlock方式获取Block函数实现时,不需要处理默认的参数,直接传递其他参数即可.例如已知Block的签名为:

@"NSString"16@?0@"NSString"8

在使用以上两种方式时,会有少许不同:

//使用Block内存分布直接获取实现:需要传递默认参数(只是这个位置你必须要空出来至于想要传递什么,根据自己需要就好)
InvokeType invoke = getImplement(blk);
NSString *result =  ((NSString *(*)(id, NSString *))invoke)(blk, @"Hello world!");
NSLog(@"result == %@", result);

//使用系统提供的获取实现:不需要传递默认参数
IMP imp = imp_implementationWithBlock(blk);
NSLog(@"result == %@", ((NSString *(*)(NSString *))imp)(@"hello world!"));

当然如果其实如果并不关心Block的实现只是想要调用Block的情况下,还有其他的方法可以实现,例如使用NSInvocation类来实现:

    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@\"NSString\"16@?0@\"NSString\"8"];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = blk;
    NSString *params = @"abc";
    [invocation setArgument:&params atIndex:1];
    [invocation invoke];
    
    id result;
    [invocation getReturnValue:&result];
    NSLog(@"result == %@", result);

使用NSInvocation类可以灵活地在动态获取方法签名之后依次根据签名设置各个参数的值,并获取方法执行之后的返回值.这样就可以做一些有趣的事情.

动态解析方法参数

在一般的动态方法替换中,会默认将其中的全部参数传递给新的方法,而事实上进行方法替换之后,除了需要执行原始方法实现的需求之外, 可能只对其中某个或者某些参数比较感兴趣,并不需要全部参数.那是否可以有这样一种方法,可以根据函数形参的类型选择性地返回实参呢?动态获取Block的签名提供了一种很好的思路.

首先定义一个可以存储Block相关实现的类EDIdentifier:

@interface EDIdentifier : NSObject

@property (copy, nonatomic) id block; //替换的block实现
@property (strong, nonatomic, readonly) NSMethodSignature *signature; //Block的方法签名
@property (strong, nonatomic) NSArray<NSNumber *> *allIndexes; //Block中合法的参数在原始方法中的位置
- (instancetype)initWithClass:(__unsafe_unretained Class)class selector:(SEL)sel block:block;

@implementation EDIdentifier
- (instancetype)initWithClass:(Class)class selector:(SEL)sel block:block {
    if (!getTypes(block)) {
        return nil;
    }
    //检查签名
    NSMethodSignature *ori_signature = [class instanceMethodSignatureForSelector:sel];
    NSMethodSignature *blockSignature = [NSMethodSignature signatureWithObjCTypes:getTypes(block)];
    if (blockSignature.numberOfArguments > ori_signature.numberOfArguments) {
        return nil;
    }
    
    BOOL matchesArguments = true;
    NSArray<NSNumber *> *allIndexes = @[];
    if (blockSignature.numberOfArguments >= 2) {
        const char *type = [blockSignature getArgumentTypeAtIndex:1];
        if (type[0] != '@') {
            return nil;
        }
        
        NSUInteger innerIndex = 2;
        for(NSUInteger index = 2; index < blockSignature.numberOfArguments; index++) {
            const char *type = [blockSignature getArgumentTypeAtIndex:index];
            BOOL matchCurrentArgument = false;
            for (NSUInteger _index = innerIndex; _index < ori_signature.numberOfArguments; _index++) {
                const char *_type = [ori_signature getArgumentTypeAtIndex:_index];
                if (type[0] == _type[0]) {
                    matchCurrentArgument = true;
                    allIndexes = [allIndexes arrayByAddingObject:@(_index)];
                    innerIndex = _index;
                    break;
                }
            }
            if (matchCurrentArgument) {
                matchesArguments = true;
            } else {
                matchesArguments = false;
                break;
            }
        }
        
    }
    NSLog(@"参数匹配:%@", matchesArguments ? @"匹配" : @"不匹配");
    if (!matchesArguments) {
        allIndexes = nil;
        return nil;
    }
    
    self.allIndexes = allIndexes;
    
    self = [super init];
    if (self) {
        self.block = [block copy];
    }
    return self;
}

- (NSMethodSignature *)signature {
    static NSMethodSignature *_signature;
    if (!_signature) {
        _signature = [NSMethodSignature signatureWithObjCTypes:getTypes(self.block)];
    }
    return _signature;
}

@end

在这个实现中:

  • Block默认的参数只有一个(Block自己),为了简化实现将Block的默认参数和原始实现的参数做对应,当Block参数大于2时,默认第二个参数为原始实现封装之后的NSInvocation对象. 这样Block的"默认参数"个数就和OC的函数实现一致了;
  • 为了防止signature参数被意外重置,将signature属性设置为只读属性.

进行方法替换的类,在NSObject的Category中,定义交换方法

+ (void)exchangeSelector:(SEL)ori_sel withBlock:(id)block

+ (void)exchangeSelector:(SEL)ori_sel withBlock:(id)block {
    if (!class_respondsToSelector(self, ori_sel)) {
        return;
    }
    EDIdentifier *identifier = [[EDIdentifier alloc] initWithClass:self selector:ori_sel block:block];
    if (!identifier) {
        return;
    }
    
    executeBlock(^{
        SEL newSel = getNewSelector(ori_sel);
        NSMutableArray<EDIdentifier *> *identifiers = objc_getAssociatedObject(self, newSel);
        if (!identifiers) {
            identifiers = @[identifier].mutableCopy;
        } else {
            [identifiers addObject:identifier];
        }
        objc_setAssociatedObject(self, newSel, identifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        //交换方法实现
        [self _exchange:ori_sel];
    });
    
}
//将原始的sel添加前缀作区分
static SEL getNewSelector(SEL sel) {
    NSString *newSelectorName = [NSString stringWithFormat:@"_ED_%@", NSStringFromSelector(sel)];
    return sel_registerName(newSelectorName.UTF8String);
}
//保证存取线程安全
static void executeBlock(dispatch_block_t block) {
    static OSSpinLock lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&lock);
    block();
    OSSpinLockUnlock(&lock);
}

作为方法交换的入口,该方法中保存了需要进行交换的相关属性并封装为EDIdentifier对象

+ (void)_exchange:(SEL)ori_sel

+ (void)_exchange:(SEL)ori_sel {
    SEL newSel = getNewSelector(ori_sel);
    if (class_respondsToSelector(self, newSel)) {
        //证明已经交换过
        return;
    }
    
    Method ori_method = class_getInstanceMethod(self, ori_sel);
    IMP ori_imp = method_getImplementation(ori_method);
    IMP msgForward = getMsgForwardIMP(self, ori_sel);
    if (ori_imp == msgForward) {
        //证明已经交换过
        return;
    }
    const char *types = method_getTypeEncoding(ori_method);
    if (ori_imp) {
        //保存原始实现
        class_replaceMethod(self, newSel, ori_imp, types);
    }
    //将方法指向方法转发
    method_setImplementation(ori_method, msgForward);
    //替换ForwardInvocation
    SEL sel_fowardInvocation = @selector(forwardInvocation:);
    IMP forwardInvocation = class_getMethodImplementation(self, sel_fowardInvocation);
    if (forwardInvocation != (IMP)_ED_forwardInvocation) {
        IMP ori_imp_forwardInvocation = class_replaceMethod(self, sel_fowardInvocation, (IMP)_ED_forwardInvocation, "v@:@");
        if (ori_imp_forwardInvocation) {
            class_addMethod(self, getNewSelector(sel_fowardInvocation), ori_imp_forwardInvocation, "v@:@");
        }
        
    }
}

static IMP getMsgForwardIMP(Class class, SEL sel){
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    //32位下大结构体返回
    Method method = class_getInstanceMethod(class, sel);
    const char *types = method_getTypeEncoding(method);
    BOOL returnValueIsStruct =  types[0] == _C_STRUCT_B;
    BOOL returnValueisBigStruct = false;
    if (returnValueIsStruct) {
        NSUInteger valueSize = 0;
        NSGetSizeAndAlignment(types, &valueSize, 0);
        if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
            returnValueisBigStruct = false;
        } else {
            returnValueisBigStruct = true;
        }
    }
    if (returnValueisBigStruct) {
        msgForwardIMP = (IMP)_objc_msgForward_stret;
    }
    
#endif
    return msgForwardIMP;
    
}

在该实现中:

  • 若当前类已经实现了方法,则将原始实现绑定在新的newSel上,方便之后调用原始实现;
  • 将原始的sel绑定在消息转发函数上(_objc_msgForward或者_objc_msgForward_stret)上, 这样当调用原始的方法时就会进入消息转发流程;
  • 将原始的forwardInvocation:替换为_ED_forwardInvocation:实现,这样当进入消息转发流程时就会进行自定义的函数进行消息转发;

static void _ED_forwardInvocation(id self, SEL _cmd, NSInvocation *invocation)

static void _ED_forwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
    SEL newSel = getNewSelector(invocation.selector);
    invocation.selector = newSel;

    NSArray <EDIdentifier *> *array = getIdentifiers(object_getClass(self), newSel);
    if (!array) {
        //调用原来的forwardInvocation实现
        SEL ori_forwardInvocation = getNewSelector(_cmd);
        ((void(*)(id, SEL, NSInvocation *))objc_msgSend)(self, ori_forwardInvocation, invocation);
        return;
    }
    
    for (EDIdentifier *identifier in array) {
        NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:identifier.signature];
        //获取block的参数个数
        NSUInteger numberOfArguments = identifier.signature.numberOfArguments;
        if (numberOfArguments > 1) {
            [blockInvocation setArgument:&invocation atIndex:1];
        }
        void *arg = NULL;
        
        NSArray<NSNumber *> *allIndexes = identifier.allIndexes;
        for(NSInteger index = 2; index < numberOfArguments; index++) {
            const char *type = [identifier.signature getArgumentTypeAtIndex:index];
            NSUInteger size = 0;
            NSGetSizeAndAlignment(type, &size, 0);
            if (!(arg = reallocf(arg, size))) {
                return ;
            }
             NSInteger realIndex = [allIndexes[index - 2] integerValue];
            [invocation getArgument:arg atIndex:realIndex];
            [blockInvocation setArgument:arg atIndex:index];
        }
        [blockInvocation invokeWithTarget:identifier.block];
    }
}


static NSArray<EDIdentifier *> *getIdentifiers(Class class, SEL sel) {
    Class _class = class;
    NSArray *array;
    do{
        array = objc_getAssociatedObject(_class, sel);
        _class = class_getSuperclass(_class);
    }while (!array || !array.count || !_class);
    return array;
}

在该实现中:

  • 首先判断消息转发流程是我们自定义的方法交换出发的还是其他其他情况触发的(如调用没有实现的方法等),如果不是我们自定义的方法交换触发的就调用原始的方法实现;否则进入下一个流程;
  • 取出方法交换时保存的EDIdentifier对象数组,依次调用.
    • 如果Block的参数个数大于 1,则将原始方法实现封装的NSInvocation对象默认赋值在第二个参数位置;
    • 根据之前保存的allIndexes数组依次获取Block需要的参数并赋值到blockInvocation对应的参数位置;
    • 使用blockInvocation调用Block实现.

 

然后就可以开心地使用方法交换实现切面编程了.例如针对

@interface Man : NSObject
- (void)send:(NSString *)str status:(BOOL) status;
@end

@implementation Man
- (void)send:(NSString *)str status:(BOOL) status {
    NSLog(@"我是原来的实现,其中[str] = %@, [status] : %@", str, status?@"真" : @"假");
}
@end

调用方法:

    Man *man = [[Man alloc] init];
    [man send:@"Hello" status:false];

如果你不想调用原来的实现,只是想替换掉原来的实现:

    SEL sel = sel_registerName("send:status:");
    [Man exchangeSelector:sel withBlock:^(){
        //your code here
        NSLog(@"被替换的实现");
    }];

如果你想调用原来的实现但是对原来的参数没有任何兴趣:

    SEL sel = sel_registerName("send:status:");
    [Man exchangeSelector:sel withBlock:^(NSInvocation *invocation){
        //调用原来的实现
        [invocation invoke];
        //your code here
        NSLog(@"被替换的实现");
    }];

如果你想要调用原来的实现,但是只对第一个字符串类型的参数感兴趣:

    SEL sel = sel_registerName("send:status:");
    [Man exchangeSelector:sel withBlock:^(NSInvocation *invocation, NSString *str){
        //调用原来的实现
        [invocation invoke];
        //your code here
        NSLog(@"被替换的实现:%@", str);
    }];

如果你想要调用原来的实现,但是只对布尔类型的参数感兴趣:

    SEL sel = sel_registerName("send:status:");
    [Man exchangeSelector:sel withBlock:^(NSInvocation *invocation, BOOL status){
        //调用原来的实现
        [invocation invoke];
        //your code here
        NSLog(@"被替换的实现:%@", status ? @"真" : @"假");
    }];

如果你想要调用原来的实现,而且对所有参数都感兴趣:

    SEL sel = sel_registerName("send:status:");
    [Man exchangeSelector:sel withBlock:^(NSInvocation *invocation, NSString *str,BOOL status){
        //调用原来的实现
        [invocation invoke];
        //your code here
        NSLog(@"被替换的实现:[str] = %@, [status] = %@", str, status ? @"真" : @"假");
    }];

这只是一个粗糙的实现没有将更多的细节做实现,事实上可以认为这是一个简化的Aspects的简化版本,当你理解了这个实现之后就可以进一步去探索Aspects的实现.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值