A delightful, simple library for aspect oriented programming
关键字:
面向切片编程
、OC动态性
、消息转发
、类型编码
、Swizzle
...-
使用场景:
- 1.统一处理逻辑
- 2.在不改变源码的情况下,插入代码(如无侵染更改第三方库代码,干一些坏坏的事情)
Aspects
只有一个类文件,非常轻量级,在实现的思路上和JSPatch
差不多。都主要用到OC的消息转发
,最终都交给ForwardInvocation
实现。二者很多地方有异曲同工之妙。
基本原理
我们知道 OC
是动态语言,我们执行一个函数的时候,其实是在发一条消息:[receiver message]
,这个过程就是根据 message
生成 selector
,然后根据 selector
寻找指向函数具体实现的指针IMP
,然后找到真正的函数执行逻辑。这种处理流程给我们提供了动态性的可能,试想一下,如果在运行时,动态的改变了 selector 和 IMP 的对应关系,那么就能使得原来的[receiver message]进入到新的函数实现了。
还是先来普及一下:
OC
上,每个类都是这样一个结构体:
struct objc_class {
struct objc_class * isa;
const char *name;
….
struct objc_method_list **methodLists; /*方法链表*/
};
其中 methodList
方法链表里存储的是 Method
类型:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
Method
保存了一个方法的全部信息,包括 SEL
方法名,type
各参数和返回值类型,IMP
该方法具体实现的函数指针。
通过 Selector
调用方法时,会从methodList
链表里找到对应Method
进行调用,这个 methodList
上的的元素是可以动态替换的,可以把某个Selector
对应的函数指针IMP
替换成新的,也可以拿到已有的某个 Selector
对应的函数指针IMP
,让另一个Selector
跟它对应,Runtime
提供了一些接口做这些事。
比如:
static void viewDidLoadIMP (id slf, SEL sel) {
// Custom Code
}
Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);
//获得viewDidLoad方法的函数指针
IMP imp = method_getImplementation(method)
//获得viewDidLoad方法的参数类型
char *typeDescription = (char *)method_getTypeEncoding(method);
//新增一个ORIGViewDidLoad方法,指向原来的viewDidLoad实现
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);
//把viewDidLoad IMP指向自定义新的实现
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);
这样就把 UIViewController
的 -viewDidLoad
方法给替换成我们自定义的方法,APP
里调用 UIViewController
的 viewDidLoad
方法都会去到上述 viewDidLoadIMP
函数里,在这个新的IMP
函数里调用新增的方法,就实现了替换viewDidLoad
方法,同时为 UIViewController
新增了个方法 -ORIGViewDidLoad
指向原来viewDidLoad
的IMP
, 可以通过这个方法调用到原来的实现。
.Aspect要的是实现一个通用的IMP,任意方法任意参数都可以通过这个IMP中转。上面讲的都是针对某一个方法的替换,但如果这个方法有参数,怎样把参数值传给我们新的 IMP
函数呢?例如 UIViewController
的 -viewDidAppear:
方法,调用者会传一个 Bool
值,我们需要在自己实现的IMP
(上述的viewDidLoadIMP
)上拿到这个值,怎样能拿到?如果只是针对一个方法写IM
P,是可以直接拿到这个参数值的。如何达到通用的效果呢?
如何实现方法替换
- va_list实现(一次取出方法的参数)
这段代码摘至JSPatch
:
static void commonIMP(id slf, ...)
va_list args;
va_start(args, slf);
NSMutableArray *list = [[NSMutableArray alloc] init];
NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:selector];
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
id obj;
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
switch(argumentType[0]) {
case 'i':
obj = @(va_arg(args, int));
break;
case 'B':
obj = @(va_arg(args, BOOL));
break;
case 'f':
case 'd':
obj = @(va_arg(args, double));
break;
…… //其他数值类型
default: {
obj = va_arg(args, id);
break;
}
}
[list addObject:obj];
}
va_end(args);
[function callWithArguments:list];
}
这样无论方法参数是什么,有多少个,都可以通过va_list
的一组方法一个个取出来,组成 NSArray
。很完美地解决了参数的问题,一直运行正常,但是在arm64
下 va_list
的结构改变了,导致无法上述这样取参数。
所以需要找到另一种方法。
-
ForwardInvocation实现
- 看图说话
从上面我们可以发现,在发消息的时候,如果 selector
有对应的 IMP
,则直接执行,如果没有,oc
给我们提供了几个可供补救的机会,依次有 resolveInstanceMethod
、forwardingTargetForSelector
、forwardInvocation
。
Aspects
之所以选择在 forwardInvocation
这里处理是因为,这几个阶段特性都不太一样:
-
resolvedInstanceMethod
: 适合给类/对象动态添加一个相应的实现, -
forwardingTargetForSelector
:适合将消息转发给其他对象处理, -
forwardInvocation
: 是里面最灵活,最能符合需求的。
因此 Aspects
的方案就是,对于待 hook
的 selector
,将其指向 objc_msgForward / _objc_msgForward_stret
,同时生成一个新的 aliasSelector
指向原来的 IMP,并且 hook
住 forwardInvocation
函数,通过forwardInvocation
调用到原来的IMP。
核心原理:按照上面的思路,当被
hook
的selector
被执行的时候,首先根据selector
找到了objc_msgForward / _objc_msgForward_stret
,而这个会触发消息转发,从而进入forwardInvocation
。同时由于forwardInvocation
的指向也被修改了,因此会转入新的forwardInvocation
函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的IMP
。
大致流程如下:
-forwardInvocation:
方法的实现给替换掉了,如果程序里真有用到这个方法对消息进行转发,原来的逻辑怎么办?首先我们在替换 -forwardInvocation:
方法前会新建一个方法 -ORIGforwardInvocation:
,保存原来的实现IMP,在新的 -forwardInvocation:
实现里做了个判断,如果转发的方法是我们想改写的,就走我们的逻辑,若不是,就调 -ORIGforwardInvocation:
走原来的流程。
将了这么多可能有些饶。Talk is sheap,show me the code
源码分析
从头文件中可以看到使用aspects有两种使用方式:
- 1.类方法
- 2.实例方法
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
两者的主要原理基本差不多.
先来看看有哪些定义:
AspectOptions
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// Called after the original implementation (default)
AspectPositionInstead = 1, /// Will replace the original implementation.
AspectPositionBefore = 2, /// Called before the original implementation.
AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};
定义切片的调用时机
AspectErrorCode
typedef NS_ENUM(NSUInteger, AspectErrorCode) {
AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted.
AspectErrorDoesNotRespondToSelector, /// Selector could not be found.
AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed.
AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed.
AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair.
AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called.
AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large.
AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated.
};
这里定义了在执行的时候的错误码,在平时开发中我们也经常使用这种方式,尤其是在定义网络请求的时候。
AspectsContainer
// Tracks all aspects for an object/class.
@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end
一个对象或者类的所有的 Aspects
整体情况,注意这里数组是通过atomic
修饰的。
关于atomic
需要注意在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)
。如果属性具备nonatomic
特质,则不需要同步锁。
注意一共有两中容器,一个是对象的切片,一个是类的切片。
AspectIdentifier
// Tracks a single aspect.
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end
一个Aspect
的具体内容。主要包含了单个的 aspect
的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。其实就是将我们传入的bloc
,包装成AspectIdentifier,便于后续使用。通过我们替换的block实例化。也就是将我们传入的block,包装成了AspectIdentifier
AspectInfo
@interface AspectInfo : NSObject <AspctInfo>
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end
主要是 NSInvocation
信息。将NSInvocation
包装一层,比如参数信息等。便于直接使用。
AspectTracker
@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)parent;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, weak) AspectTracker *parentEntry;
@end
用于跟踪所改变的类,打上标记,用于替换类方法,防止重复替换类方法。
流程
读源码是一件辛苦的事情。😂😂
__Block的使用
做过iOS的都知道,__Block
在ARCX
和MRC
环境修饰对象下是不同的。具体的内容可以看看我的另一篇文章。这里只给出结论:
在
ARC
环境下,__Block
会对修饰的对象强引用,在MRC
环境下对修饰的对象不会强引用。而且__block
修饰局部变量,表示这个对象是可以在block
内部修改,如果不这样写,会报Variable is not assignable (missing__block type specifier)
的错误。
来看看代码:
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
NSCParameterAssert(self);
NSCParameterAssert(selector);
NSCParameterAssert(block);
__block AspectIdentifier *identifier = nil;
aspect_performLocked(^{
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
注意这里的__block AspectIdentifier *identifier = nil;
为什么要这样写呢。上面已经说过得很清楚。因为identifier
是局部变量,如果不加__block
修饰,block
里面不能改变identifier
。
自旋锁(OSSpinLockLock)
Aspect
是线程安全的,那么它是通过什么方式办到的呢。如果你对iOS中的几个锁不清楚,可以看看我的另一篇文章,里面有介绍。
自旋锁是效率比较高的一种锁,相比@synchronized
来说效率高得多。但是需要注意,如果访问这个所的线程不是同一优先级的话,会有死锁的潜在风险。具体原因请看YYKit
作者博客。
static void aspect_performLocked(dispatch_block_t block) {
static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&aspect_lock);
// 加锁执行block
block();
// 执行完之后释放锁
OSSpinLockUnlock(&aspect_lock);
}
通过这样的加锁方式,所以Aspect
作者说它是线程安全的。
真正烧脑环节
执行的入口
aspect_performLocked(^{
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
上面这句代码是执行Aspect
的入口。看是简单其实里面有非常复杂的地方。
看一看block
做了哪些事情。
- 1.对出入进来的参数进行检验,保证参数合法
- 2.创建aspect容器,注意容器是懒加载形式动态添加到
NSObject
分类中作为属性。 - 3.根据参数,比如
selector
,option
,创建AspectIdentifier
实例,上面已经说过AspectIdentifier
主要包含了单个的Aspect
的具体信息,包括执行时机,要执行block
所需要用到的具体信息。 - 4.将单个的
Aspect
的具体信息加到属性aspectContainer
中 - 5.最为重要的部分进行Hook操作,生成子类,类型编码处理,方法替换等。
下面就对上面5个部分分别仔细的分析。
参数检验
严格来说,在调用每一个方法的时候都需要对传入进来的参数做一次校验。尤其是在做SDK
的时候,因为你根本不知道外面传进来的是什么数据,到底是否为空,数据类型是否正确。平时开发的过程中,由于我们都知道传入的参数大部分来说都是我们复合预期的所以就没有做什么检验工作。
回到Aspect中。检验的内容主要有如下几个:
- 1.Swizzle了不能Swizzle的方法,比如
@retain", @"release", @"autorelease", @"forwardInvocation:"
:如果替换了这样的方法,我们是不能成功进行Swizzle的。 - 2.传入的执行时机是否正确,比如对于
dealloc
方法,`Swizzle之能在之前进行调用。 - 3.对象或者类是否响应传入的selector
- 4.如果替换的是类方法,则进行是否重复替换的检查
这里重点捋一捋,类方法的检验参数,为了检验类的方法只能修改一次。
if (class_isMetaClass(object_getClass(self))) {
Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if ([tracker.selectorNames containsObject:selectorName]) {
// Find the topmost class for the log.
if (tracker.parentEntry) {
AspectTracker *topmostEntry = tracker.parentEntry;
while (topmostEntry.parentEntry) {
topmostEntry = topmostEntry.parentEntry;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}else if (klass == currentClass) {
// Already modified and topmost!
return YES;
}
}
}while ((currentClass = class_getSuperclass(currentClass)));
// Add the selector as being modified.
currentClass = klass;
AspectTracker *parentTracker = nil;
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
}
[tracker.selectorNames addObject:selectorName];
// All superclasses get marked as having a subclass that is modified.
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
}
- 如何判断传入的是类而不是对象:
class_isMetaClass(object_getClass(self)))
,object_getClass
是获取当前对象由什么实例化。
类方法只能替换一次,是在整个类的继承树上校验,而不只是单单的一个类,从下面代码可以看出:
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if ([tracker.selectorNames containsObject:selectorName]) {
// Find the topmost class for the log.
if (tracker.parentEntry) {
AspectTracker *topmostEntry = tracker.parentEntry;
while (topmostEntry.parentEntry) {
topmostEntry = topmostEntry.parentEntry;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}else if (klass == currentClass) {
// Already modified and topmost!
return YES;
}
}
}while ((currentClass = class_getSuperclass(currentClass)));
注意这句(currentClass = class_getSuperclass(currentClass)
,当且只有当这个类为根类的时候才不会继续循环查找。
再来看看这段:
currentClass = klass;
AspectTracker *parentTracker = nil;
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
}
[tracker.selectorNames addObject:selectorName];
// All superclasses get marked as having a subclass that is modified.
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
这段的作用就是如果类被修改了,给其父类打上标记。然后结合上面的判断是否重复替换。这里为什么要用父类呢。这个runtime类与父类,根类关系有关为了有效的遍历,需要找到一个退出的条件,而退出的条件,结合到runtime就是根类没有父类。这就是退出的条件。
如果还没有弄懂,回去看看
runtime的基本知识
创建Aspect容器
AspectsContainer
的作用是为了保存整个Apects
的情况,包括添加/删除的aspect
,根据执行的时机(before,instead,after)而保存的所有的aspcet
。其中用到了比较常用的动态添加属性。
static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
NSCParameterAssert(self);
// 得到新的SEL(加上了前缀aspects_)并新增为属性
SEL aliasSelector = aspect_aliasForSelector(selector);
// 得到和aliasSelector相对应的AspectsContainer
AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
if (!aspectContainer) {
aspectContainer = [AspectsContainer new];
objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
}
return aspectContainer;
}
这一步还是比较简单。
单个的 Aspect 的具体信息AspectIdentifier
上面介绍的AspectsContainer
里面装的具体的东东就是AspectIdentifier
。AspectIdentifier
包含的是具体到每一个aspect
的具体信息,直接看属性:
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object; // 具体信息所属类,用weak
@property (nonatomic, assign) AspectOptions options;
初始化方法将需要的比如:sel
、block
、option
,传进去。`
- (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error`
需要重点注意方法签名NSMethodSignature
,因为我们在使用Aspect
的时候是直接通过block
来替换方法的,所以需要将我们传入的block
转换为具体的方法。这也是为什么会在AspectIdentifier
中多一个方法签名的属性。
在介绍方法签名之前需要对block
具体的结构有一定了解。在Aspect
中,我们定义了一个结构体用来代替系统的block
。总体结构其实和系统的一样。
// 模仿系统的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
转为方法签名的代码:
static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
// 将block转换为自定义的形式
AspectBlockRef layout = (__bridge void *)block;
// 过滤
if (!(layout->flags & AspectBlockFlagsHasSignature)) {// flags不是AspectBlockFlagsHasSignature类型
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
void *desc = 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);
// Returns an NSMethodSignature object for the given Objective-C method type string.
// 根据类型编码返回真正方法签名
return [NSMethodSignature signatureWithObjCTypes:signature];
}
block
是作为id
类型进行传递的,而且是全局类型的block
。如果转换成功,layout
里面都会有相应的值。
这句话const char *signature = (*(const char **)desc);
得到我们传入block
的类型编码。通过类型编码得到block
所对应的方法签名。
这里大致给出返回值为空,无参数的block
转换之后的方法签名的结构:
<NSMethodSignature: 0x7f9219d064e0>
number of arguments = 1
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?'
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
得到了方法签名,需要对这个方法签名和替换实际方法进行比较。从这里可以感受到做一个第三方对参数的检测是非常非常重要的。这也是我们平时在应用开发中所欠缺的。
那怎么和原来的方法比较呢。比较直接的方法就是,拿到替换方法的方法签名和我们将block
转换之后的方法签名对比。
直接看到代码:
// 原方法签名
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
// 参数不匹配
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
}else {
if (blockSignature.numberOfArguments > 1) {
// blockSignature参数没有_cmd,
const char *blockType = [blockSignature getArgumentTypeAtIndex:1];
// 类型编码
if (blockType[0] != '@') {
signaturesMatch = NO;
}
}
// Argument 0 is self/block, argument 1 is SEL or id<AspectInfo>. We start comparing at argument 2.
// 对于block来说
// The block can have less arguments than the method, that's ok.
if (signaturesMatch) {
for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) {
const char *methodType = [methodSignature getArgumentTypeAtIndex:idx];
const char *blockType = [blockSignature getArgumentTypeAtIndex:idx];
// Only compare parameter, not the optional type data.
// 参数匹配
if (!methodType || !blockType || methodType[0] != blockType[0]) {
signaturesMatch = NO;
break;
}
}
}
}
if (!signaturesMatch) {
NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature];
AspectError(AspectErrorIncompatibleBlockSignature, description);
return NO;
}
说一说为什么要从第二个参数开始比较呢。首先我们常见的方法都有连个隐藏的参数,一个是__cmd
,一个是self
。而方法签名的参数也有两个(针对一个返回值为空,参数为空的方法)。第一个是self
,第二个是SEL
。源代码有相关说明。
我直接打印一下:
-
methodSignature:
<NSMethodSignature: 0x7f9219d0cab0>
number of arguments = 2
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (B) 'B'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = -7}
memory {offset = 0, size = 1}
argument 0: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (:) ':'
flags {}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}-
blockSignature
<NSMethodSignature: 0x7f9219d064e0>
number of arguments = 1
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?'
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (:) ':'
flags {}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
将`block`转换为方法签名之后就可以给`AspectIdentifier`具体属性赋值了。
AspectIdentifier *identifier = nil;
if (blockSignature) {
identifier = [AspectIdentifier new];
identifier.selector = selector;
identifier.block = block;
identifier.blockSignature = blockSignature;
identifier.options = options;
identifier.object = object; // weak
}
### 将具体的`AspectIdentifier `添加到容器中
这一步比较简单`[aspectContainer addAspect:identifier withOptions:options];`
### 方法替换
方法替换是这个库最为核心的部分,也是最难的部分。里面的思路以及方法很多都值得学习。这部分也是自己花了很多时间去了解。
大致的步骤:
- 1.创建子类,`hook`子类
- 2.处理实现过`MsgForwardIMP`的类或者对象
- 3.将需要替换的`selector`,指向`_objc_msgForward`
来具体看一看
#### 创建子类
创建子类将`runtime`用到了极致。其实我们所做的替换完全是在运行时动态创建子类的时候实现的。这样对原来替换的类或者对象没有任何影响而且可以在子类基础上新增或者删除`aspect`。
注意`Class statedClass = self.class;`和`Class baseClass = object_getClass(self);`的区别,前者获取类对象,后者获取本类是由什么实例化。
**所有的操作是在类对象基础上操作的,而不是一个对象**最为重要的就是`aspect_swizzleClassInPlace`方法
static Class aspect_swizzleClassInPlace(Class klass) {
NSCParameterAssert(klass);
NSString *className = NSStringFromClass(klass);
// 1.保证线程安全,2保证数组单例
_aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
// 不包含这个类,如果已经包含
if (![swizzledClasses containsObject:className]) {
/** 深藏的核心部分 **/
// 将原IMP指向forwardInvocation
aspect_swizzleForwardInvocation(klass);
// 添加进来
[swizzledClasses addObject:className];
}
});
return klass;
}
为了保证线程安全,所以用了`_aspect_modifySwizzledClasses`是单例方法。将传入的`block`安全执行
static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) {
static NSMutableSet *swizzledClasses;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
swizzledClasses = [NSMutableSet new];
});
@synchronized(swizzledClasses) {
block(swizzledClasses);
}
}
来看第一个重头戏:**aspect_swizzleForwardInvocation**这个方法的目的就是将本类中的`forwardInvocation`方法替换为我们自定义的`__ASPECTS_ARE_BEING_CALLED__`方法。这样只要类没有找到消息处理着都会走到自定义的`__ASPECTS_ARE_BEING_CALLED__ `。这样就可以统一处理了。
这里有个小小的问题,如果`forwardInvocation `已经被替换了,那么就需要特殊处理。比如这里判断的方法是,如果原来实现过`forwardInvocation `。则新增一个方法`AspectsForwardInvocationSelectorName`,指向`originalImplementation `。这个时候已经`forwardInvocation`已经指向了我们自定义的`AspectsForwardInvocationSelectorName `。所以是同理的。
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)ASPECTS_ARE_BEING_CALLED, "v@:@");
if (originalImplementation) {
// 将__aspects_forwardInvocation:指向originalImplementation,
// 将originalImplementation添加到Method,以便下次调用,直接就可以了
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
再来看看`__ASPECTS_ARE_BEING_CALLED__`这个方法如何将转发过来的参数成功的转换为我们需要的参数。
static void ASPECTS_ARE_BEING_CALLED(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
NSCParameterAssert(self);
NSCParameterAssert(invocation);
SEL originalSelector = invocation.selector;
// 加前缀
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
invocation.selector = aliasSelector;
// 本对象的AspectsContainer,添加到对象的aspect
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
// 这个类的AspectsContainer,添加类上面的aspect
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
NSArray *aspectsToRemove = nil;
// Before hooks.
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count ||
classContainer.insteadAspects.count) {
// 类方法和
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
Class klass = object_getClass(invocation.target);
do {
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
// 直接调用
[invocation invoke];
break;
}
}while (!respondsToAlias &&
(klass = class_getSuperclass(klass)));
}
// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else {
[self doesNotRecognizeSelector:invocation.selector];
}
}
// Remove any hooks that are queued for deregistration.
// 😥
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
用自定的`aliasSelector`替换掉`invocation`的`selector`。然后将`invocation`转化为`AspectInfo `。`AspectInfo `主要包含了`invocation `相关的信息,比如参数数组。如何获取参数数组呢?代码是通过为`NSInvocation`写一个分类来实现。原理就是涉及到类型编码的那块。来看看:
// Thanks to the ReactiveCocoa team for providing a generic solution for this.
- (id)aspect_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, @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
}
有几个基础的地方。
- @encode:获取类型编码:比如`strcmp(argType, @encode(SEL)) == 0`就是判断参数`argType`是否为SEL类型。
- WRAP_AND_RETURN:是一个宏定义,为了将一般类型,比如`Bool`,`Int`转为对象,然后能够添加到数组中。
开始调用通过宏定义`aspect_invoke`
define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {
[aspect invokeWithInfo:info];
if (aspect.options & AspectOptionAutomaticRemoval) {
aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect];
}
}
为什么这里用宏定义来调用呢。因为宏能够让我获得堆栈信息。
**进入到了最后调用的地方了。**
-
(BOOL)invokeWithInfo:(id<AspectInfo>)info {
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
NSInvocation *originalInvocation = info.originalInvocation;
NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;// Be extra paranoid. We already check that on hook registration.
if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
}// The
self
of the block will be the AspectInfo. Optional.
/**- index: Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; you should set these values directly with the target and selector properties
*/
if (numberOfArguments > 1) {
[blockInvocation setArgument:&info atIndex:1];
}
void *argBuf = NULL;
// 根据NSInvocation参数规则,从第二个参数开始取
// 参数处理
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
// 取出类型编码
const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);if (!(argBuf = reallocf(argBuf, argSize))) { AspectLogError(@"Failed to allocate memory for block invocation."); return NO; } // 从originalInvocation取出参数 [originalInvocation getArgument:argBuf atIndex:idx]; // 给blockInvocation设置参数 [blockInvocation setArgument:argBuf atIndex:idx];
}
// 调用
[blockInvocation invokeWithTarget:self.block];if (argBuf != NULL) {
free(argBuf);
}
return YES;
} - index: Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; you should set these values directly with the target and selector properties
用之前将`block`转为的`blockSignature`初始化`blockSignature`得到`invocation`。然后处理参数,如果参数`block`中的参数大于1个,则把包装成`AspectInfo `。
然后从`originalInvocation `中取出参数给`blockInvocation `赋值。最后调用。` [blockInvocation invokeWithTarget:self.block];`这里`Target`设置为`self.block`。注意`You must set the receiver’s selector and argument values before calling this method`
#### 在子类中将替换的`selector`指向`_objc_msgForward`
Method targetMethod = class_getInstanceMethod(kclass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
由于`kclass`是替换类的子类,所以正常情况下`targetMethod `不会为空。相当雨得到原有的`IMP`,如果原有的`IMP`就是指向的`_objc_msgForward`。则不用处理了。我们已经在前面处理过了。
直接来看正常流程
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// Make a method alias for the existing method implementation, it not already copied.
const char *typeEncoding = method_getTypeEncoding(targetMethod);
// 3- 得到替换的SEL
SEL aliasSelector = aspect_aliasForSelector(selector);
// 子类没有这个方法,添加方法到子类
if (![klass instancesRespondToSelector:aliasSelector]) {
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
}
// 使用forwardInvocation来统一处理,将原SEL指向原SEL找不到方法的forwardInvocation的IMP
// We use forwardInvocation to hook in.
// 4- 将需要替换的selector,指向_objc_msgForward,进行统一处理
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
先获取`targetMethodIMP `的类型编码。然后将我们自定义的`aliasSelector `添加的子类上。最后进行替换。`class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);`最后来看看这个函数:
static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
// self 成为子类
IMP msgForwardIMP = _objc_msgForward;
// 需要兼容__arm64__,用不同的方式
if !defined(arm64)
// As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id.
// https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html
// https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783
// http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4)
Method method = class_getInstanceMethod(self.class, selector);
// 类型编码字符
const char *encoding = method_getTypeEncoding(method);
BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
if (methodReturnsStructValue) {
// 通过Try Cathch 捕获异常
@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;
}
这个函数主要是处理`__arm64__ `中`var_list`结构变了。[可参考](https://blog.nelhage.com/2010/10/amd64-and-va_arg)
` BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;`这句话是判断类型编码第一个字符是否为`_C_STRUCT_B `
define _C_ID '@'
define _C_CLASS '#'
define _C_SEL ':'
define _C_CHR 'c'
define _C_UCHR 'C'
define _C_SHT 's'
define _C_USHT 'S'
define _C_INT 'i'
define _C_UINT 'I'
define _C_LNG 'l'
define _C_ULNG 'L'
define _C_LNG_LNG 'q'
define _C_ULNG_LNG 'Q'
define _C_FLT 'f'
define _C_DBL 'd'
define _C_BFLD 'b'
define _C_BOOL 'B'
define _C_VOID 'v'
define _C_UNDEF '?'
define _C_PTR '^'
define _C_CHARPTR '*'
define _C_ATOM '%'
define _C_ARY_B '['
define _C_ARY_E ']'
define _C_UNION_B '('
define _C_UNION_E ')'
define _C_STRUCT_B '{'
define _C_STRUCT_E '}'
define _C_VECTOR '!'
define _C_CONST 'r'
这个是对类型编码的宏定义。太深了搞起来太费脑子了。大概了解到这个程度吧。
**替换之后,调用原有的Method的时候,就会消息转发**
## 写在最后
> 平时接触事物决定了我们的高度。如果整天围着`UIKit`转,技术也就那样。居安思危!