Aspects框架------汲取

本文探讨Block的内部结构,揭示如何获取Block签名,以及Aspects库中的实用开发技巧,包括对象方法重定向、方法拦截时机选择、避免重复hook和正确使用消息转发。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前我们聊过关于Aspects源代码解析,今天简单聊一聊这个类库中我们可以学习到那些实用的开发技巧.

3.1 如何获取block签名

在apple最新的开源代码中我们可以找到关于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_layout中,包含了以下信息:

  • isa指针:这跟objc_object很像,所以block在多数情况下被认为是对象;
  • flags:就是上面那几个枚举,用来保留block的一些信息,比如是否含有签名信息,是否捕获外部变量等;
  • reserved: 保留信息;
  • invoke:指向函数实现的指针;
  • description:block的附加描述信息,主要保存了内存size以及copy和dispose函数的指针及签名和layout等信息,通过源码可发现,layout中只包含了Block_descriptor_1,并未包含Block_descriptor_2和Block_descriptor_3,这是因为在捕获不同类型变量或者没用到外部变量时,编译器会改变结构体的结构,根据需要动态添加Block_descriptor_2和Block_descriptor_3,所以才需要BLOCK_HAS_COPY_DISPOSE和BLOCK_HAS_SIGNATURE等枚举来判断;
  • variables capture的外部变量,如果Block中使用了外部变量,结构体中就会有相应的信息.Block将使用的变量或者变量指针copy过来,内部才可以访问.

所以block在OC中也是一种特殊的对象,而如何获取block的签名,原始的结构体定义跟了我们一个很好的思路.在Aspects中,将原始block的定义进行了自定义(因为原始定义私有),

typedef struct _AspectBlock {
	__unused Class isa;
	AspectBlockFlags flags;
	__unused int reserved;
	void (__unused *invoke)(struct _AspectBlock *block, ...);
	struct {
		unsigned long int reserved;
		unsigned long int size;
		// requires AspectBlockFlagsHasCopyDisposeHelpers
		void (*copy)(void *dst, const void *src);
		void (*dispose)(const void *);
		// requires AspectBlockFlagsHasSignature
		const char *signature;
		const char *layout;
	} *descriptor;
	// imported variables
} *AspectBlockRef;

这样就可以将我们定义的block强制对齐转化为结构体,我们就可以获取到结构体中的相关变量,在Aspects中主要是为了获取block的方法签名.利用这个结构体,我们还可以做一些好玩的事情,比如:我们可以使用这个结构体像方法调用一样运行block

    void(^block)(void) = ^(void){
        NSLog(@"I like China!");
    };
    AspectBlockRef layout = (__bridge void *)block;
    layout->invoke(layout);
    

这样看block就更加像是一个真实的对象(与objc_msgSend调用类似),只不过与普通对象不同的是:普通对象实现函数有两个默认参数(id, SEL),而block的实现函数默认只有一个block对象自己.

Block虽然很强大,但是当我们并不知道block的内部实现时,如果想要知道一个block的详细详细信息(例如block需要几个参数以及返回值类型)时就会比较麻烦.这时候就可以从block的原始定义中出发回去灵感:根据Block_layout的实现,可以清楚地知道:

  • 在一个block中,
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);

这一部分的内存分布描述是固定的,每一个block都会有;

  • descriptor部分则是相对灵活的
struct Block_descriptor_1 *descriptor;

这部分的组成并不惟一,根据需要这部分可以由Block_descriptor_1, Block_descriptor_2,Block_descriptor_3灵活组合生成,其中可以认为Block_descriptor_1是必备的一个部分,而Block_descriptor_2,Block_descriptor_3这两个部分则是根据flags中是否包含了BLOCK_HAS_COPY_DISPOSE和BLOCK_HAS_SIGNATURE来确定是否包含对应的结构.根据这一理论,可以通过在block中查找方法签名的方式生成对应的NSMethodSignature对象来判断blcok的参数以及返回值等信息(当然如果你熟悉TypeEncodings,也可以直接通过方法签名来获取相关信息).

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    AspectBlockRef layout = (__bridge void *)block;
    //判断是否有AspectBlockFlagsHasSignature标志位,没有报不包含方法签名的error
    if (!(layout->flags & AspectBlockFlagsHasSignature)) {
//这里需要首先做判断:如果block不存在签名就不需要继续执行,否则如果不存在签名而直接去取可能会出现异常(因为对应的空间即使有值却并不是我们想要的)
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
    void *desc = layout->descriptor;//这里需要注意的是layout->descriptor本身已经是指针
    desc += 2 * sizeof(unsigned long int);
    if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
        desc += 2 * sizeof(void *);
    }
    if (!desc) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
    const char *signature = (*(const char **)desc);
    //这里之所以desc指向的空间保存的是signiture变量的指针的值,而不是signiture变量的值,所以需要使用(const char **) desc来做强制转化证明des这个指针指向的空间的值才是signiture变量的指针的值因此是一个二级的指针.
    return [NSMethodSignature signatureWithObjCTypes:signature];
}

事实上,如果觉得上述实现比较麻烦,可以适当做一个简化:

typedef struct Block_layout {
    void *isa;
    volatile int32_t flags;
    int32_t reserved;
    void(*funPtr)(void *, ...);
    void *descriptor;
}*Block_layout_ptr;

char *getBlockTypes(id block) {
    Block_layout_ptr block_layout = (__bridge Block_layout_ptr)block;
    int32_t block_has_signiture = 1 << 30;
    if ((block_layout->flags & block_has_signiture) != block_has_signiture) {
        //是否有签名
        printf("没有签名\n");
        return NULL;
    }
    void *ptr = block_layout->descriptor;
    ptr += 2 * sizeof(uintptr_t);
    if ((block_layout->flags & (1 << 25))) {
        ptr += 2 * sizeof(void *);
    }
    return *(char **)ptr;
};

按照这个思路,也可以尝试一下获取其他成员变量,比如invoke实现:

            NSString *(^appendStringBlock)(NSString *, NSString *) = ^NSString *(NSString *a, NSString *b) {
                return [a stringByAppendingString:b];
        };

            void *pointer = (__bridge void *)appendStringBlock;
            typedef NSString*(*Type)(id,NSString*, NSString *);
            //由于invoke肯定存在所以获取起来就会简单很多,直接移动指针就可以了
            Type imp = *(Type *)(pointer + sizeof(void *) + 2 * sizeof(int32_t));
            //获取到之后可以直接调用
            NSLog(@"result == %@", imp(appendStringBlock,@"abc", @"def"));

 

3.2 如何能让一个类的某个对象方法重定向不影响到其他对象的实现?

使用运行时进行方法替换或者重定向用的好是一个神器,用不好就很容易尴尬,由于进行替换操作默认都是全局的,稍不留意就出现一些你意想不到的问题,由于这类问题在运行时才发挥作用,所以很难排查.Aspects在处理对象的方法重定向时,就使用了KVO的思路,使用给当前对象创建中间类的思想,将方法替换的影响范围限定在指定的某些对象中,同时将中间类的class方法和isa指针指向原始的类隐藏这个类的存在,从而减少了影响.

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
	Class statedClass = self.class;
	Class baseClass = object_getClass(self);
	NSString *className = NSStringFromClass(baseClass);
    // Already subclassed
	if ([className hasSuffix:AspectsSubclassSuffix]) {
		return baseClass;
        // We swizzle a class object, not a single object.
	}else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
        // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
    }else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // Default case. Create dynamic subclass.
	const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
	Class subclass = objc_getClass(subclassName);

	if (subclass == nil) {
		subclass = objc_allocateClassPair(baseClass, subclassName, 0);
		if (subclass == nil) {
            NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
            AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
            return nil;
        }
		aspect_swizzleForwardInvocation(subclass);
		aspect_hookedGetClass(subclass, statedClass);
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
		objc_registerClassPair(subclass);
	}

	object_setClass(self, subclass);
	return subclass;
}

3.3 如何想要拦截或者重定向方法实现,选择什么样的时机比较合适?

在Aspects中,选择了使用将需要拦截的方法实现指向_objc_msgForward,使用自定的方法指针指向原始的方法实现,同时拦截原始forwardInvocation:的实现来完成自定义操作,这样的时机具有以下好处:

  • 该方法对源代码的入侵性小,毕竟forwardInvocation:这个方法只有在消息转发时才会调用;
  • 方便加入自定义实现:可以灵活地添加自定义的代码实现;
  • 可以方便地拿到原始方法的签名信息,并在需要的时候调用原始方法的实现;

3.4  在hook方法时,如何保证不会重复hook?

在Aspects中,对需要hook方法的对象进行了区分处理,如果对于全局的所有的方法都需要拦截,就使用注册全局集合的方式保存对应的类名,同时在继承链上标记被hook过的方法来防止重复;如果只是需要hook某个对象的方法,那就可以通过创建中间类的方法,类通过类名确定是否已经hook操作过.

3.5  将forwardInvocation:方法进行消息转发时,如何确定使用_objc_msgForward_stret还是_objc_msgForward?

因为在32未操作系统上,函数的返回结果如果想要保存在寄存器中,则必须小于寄存器本身位数的2倍(也就是说在32位操作系统上,返回值的大小不能超过8个字节,否则寄存器只能存储该结构体的指针).所以在Aspects中,对返回值结构体的大小进行了判断,在非ARM64位机器上,如果返回值是结构体,且结构图大小是1,2,4,8字节依旧使用_objc_msgForward,否则使用_objc_msgForward_stret.

static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    Method method = class_getInstanceMethod(self.class, selector);
    const char *encoding = method_getTypeEncoding(method);
    BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
    if (methodReturnsStructValue) {
        @try {
            NSUInteger valueSize = 0;
            NSGetSizeAndAlignment(encoding, &valueSize, NULL);

            if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
                methodReturnsStructValue = NO;
            }
        } @catch (NSException *e) {}
    }
    if (methodReturnsStructValue) {
        msgForwardIMP = (IMP)_objc_msgForward_stret;
    }
#endif
    return msgForwardIMP;
}

而事实上,Aspects的这个处理是有瑕疵的.例如,我们使用5s/6s/7的模拟器做如下测试

@interface Person : NSObject

- (CGPoint)funcToSwizzleReturnPoint:(CGPoint)point;
@end
@implementation Person
- (CGPoint)funcToSwizzleReturnPoint:(CGPoint)point {
    return CGPointZero;
}
@end


    Person *person = [[Person alloc] init];
    [person aspect_hookSelector:@selector(funcToSwizzleReturnPoint:) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){
        NSLog(@"aspect obj");
        
    } error:NULL];
    [person funcToSwizzleReturnPoint:(CGPoint){1,2}];

然后发现Crash了.因为这里只考虑了IA-32位的情况,却没有考虑IA-64位机器的情况.在IA-64位操作系统上,寄存器可以保存的结构体大小已经达到了128位,即16字节.所以应该增加一个判断:

static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    Method method = class_getInstanceMethod(self.class, selector);
    const char *encoding = method_getTypeEncoding(method);
    BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
    if (methodReturnsStructValue) {
        @try {
            NSUInteger valueSize = 0;
            NSGetSizeAndAlignment(encoding, &valueSize, NULL);
            
            if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
                methodReturnsStructValue = NO;
            }
            
#if defined(__LP64__) && __LP64__
            if (valueSize == 16) {
                methodReturnsStructValue = NO;
            }
#endif
            
            
        } @catch (NSException *e) {}
    }
    if (methodReturnsStructValue) {
        msgForwardIMP = (IMP)_objc_msgForward_stret;
    }
#endif
    return msgForwardIMP;
}

而在另一个开源框架JSPatch中的处理是这样的:

static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
{
    SEL selector = NSSelectorFromString(selectorName);
    
    if (!typeDescription) {
        Method method = class_getInstanceMethod(cls, selector);
        typeDescription = (char *)method_getTypeEncoding(method);
    }
    
    IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
    
    IMP msgForwardIMP = _objc_msgForward;
    #if !defined(__arm64__)
        if (typeDescription[0] == '{') {
            //In some cases that returns struct, we should use the '_stret' API:
            //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
            //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
            NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
            if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
                msgForwardIMP = (IMP)_objc_msgForward_stret;
            }
        }
    #endif

    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        if (originalForwardImp) {
            class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
        }
    }

    [cls jp_fixMethodSignature];
    if (class_respondsToSelector(cls, selector)) {
        NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
        SEL originalSelector = NSSelectorFromString(originalSelectorName);
        if(!class_respondsToSelector(cls, originalSelector)) {
            class_addMethod(cls, originalSelector, originalImp, typeDescription);
        }
    }
    
    NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
    
    _initJPOverideMethods(cls);
    _JSOverideMethods[cls][JPSelectorName] = function;
    
    // Replace the original selector at last, preventing threading issus when
    // the selector get called during the execution of `overrideMethod`
    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
}

相比较而言,该框架没有没有直接根据返回值所占字节书来判断,而是使用了debugDescription进行判断是否是特殊的结构体,可谓是相当"鸡贼"了.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值