什么是AOP呢?下面是来自百科的一段话:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
综上所述:面向切面编程就是通过预编译和运行期动态代理实现给程序动态统一添加功能的一种技术。
我们什么情况下可能会用到AOP呢?比如如果需要在每个控制器的viewDidLoad
里面都需要添加统计代码,或者每个类都需要添加日志代码。其实上面的需求很容易想到在每个控制器里面都写一遍,这样的话会有很多重复的代码而且不好维护。另外也可以用继承,但是用继承无形中增加了类的强耦合,所以都不是最好的办法。
这时可能很容易想到runtime
中method swizzle
。method swizzle
是runtime的黑魔法之一,也就是在无法看到一个类的源代码的情况下,改变方法实现或者偷换方法实现的一种强大技术。method swizzle
确实是一个很好的方法,而且降低了业务逻辑各个部分的耦合性。
Aspects
Aspects就是基于method swizzle的第三方库,用的就是AOP面向切面编程思想。
@interface NSObject (Aspects)
/// 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`, 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`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id)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)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
@end
上面两个方法就是实现对类的某些方法进行拦截。比如我们要在每个控制器的viewDidLoad方法中执行doSomethings方法。可以这样用:
/**
* 事件拦截
* 拦截UIViewController的viewDidLoad方法
*/
[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id aspectInfo)
{
//NSLog(@"viewDidLoad调用了 --- %@ --- %@ --- %@",aspectInfo.instance,aspectInfo.arguments, aspectInfo.originalInvocation);
NSLog(@"%@ 对象的viewDidLoad调用了",aspectInfo.instance);
/**
* 添加我们要执行的代码,由于withOptions是AspectPositionAfter。
* 所以每个控制器的viewDidLoad触发都会执行下面的方法
*/
[self doSomethings];
} error:NULL];
- (void)doSomethings
{
//TODO: 比如日志输出、统计代码
NSLog(@"------");
}
上面代码实现了拦截所有控制器的viewDidLoad事件。
1、aspect_hookSelector:表示要拦截指定对象的方法。
2、withOptions:是一个枚举类型,AspectPositionAfter表示viewDidLoad方法执行后会触发usingBlock:的代码。
3、usingBlock:就是拦截事件后执行的自定义方法。我们可以在这个block里面添加我们要执行的代码。
上面的代码运行可以看出,只要任何控制器对象或者其子类的viewDidLoad方法触发,doSomethings方法就会触发。
比如再举一个例子:
比如我们封装一个类似网易的菜单栏,效果如下图所示:
如果实现上面的效果,首先创建一个类,比如名字是DLMenuView
在DLMenuView.h
里面我们定义一个类别,这个方法就是在外部做事件拦截用的,也就是当我们点击每个选项卡按钮的时候,会调用这个方法。如下所示:
@interface DLMenuView (Aspects)
- (void)dlMenuView:(DLMenuView *)menuView atIndex:(NSInteger)atIndex;
@end
在DLMenuView.m
里面定义如下:
@implementation DLMenuView (Aspects)
- (void)dlMenuView:(DLMenuView *)menuView atIndex:(NSInteger)atIndex
{
}
@end
上面实现不需要写任何代码,点击每个选项卡我们会调用这个方法,并且会传递2个参数:第一个是DLMenuView
对象,第二个是点击的选项卡的索引atIndex
。那不实现这个方法我们写有什么用呢?我们想下,拦截某个事件方法首先我们要告诉其他类哪些方法可以拦截,为了方便,我们使用类别。另外,我们总不能把按钮点击触发事件公开吧,这样设计的类也不是很合理。
当定义完成后,我们就可以侦听点击的是第几个选项卡了。我们可以在控制器里面这样用:
DLMenuView *menuView = [[DLMenuView alloc] initWithFrame:CGRectMake(0, 80, self.view.frame.size.width, 40)];
//菜单栏标题
menuView.menuTitles = @[@"新闻",@"头条",@"好声音",@"原创",@"视频",@"土豆",@"优酷",@"科技",@"图片"];
menuView.backgroundColor = [UIColor yellowColor];
//默认选择第1项
menuView.selectedIndex = 0;
[self.view addSubview:menuView];
/**
* 拦截menuView的dlMenuView:atIndex:方法
*/
[menuView aspect_hookSelector:@selector(dlMenuView:atIndex:) withOptions:AspectPositionAfter usingBlock:^(id aspects, DLMenuView *menuView, NSInteger index)
{
NSLog(@"按钮点击了 %ld",index);
} error:nil];
上面就实现了侦听dlMenuView:atIndex:
的调用,拦截这个方法后获取到的index
就是点击的选项卡的索引。
上面方法可以减少类与类的耦合度,我们可以在不设置回调的情况下就能很方便的满足需求。有时候,回调很多或者嵌套对我们处理某些问题带来很多麻烦。总之我们采用的任何处理方式或者设计模式等方案,都需要考虑尽量减少类与类之间的耦合,使类更好用。
Aspects源码解析
关键字:面向切片编程、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)上拿到这个值,怎样能拿到?如果只是针对一个方法写IMP,是可以直接拿到这个参数值的。如何达到通用的效果呢?
如何实现方法替换
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
先来看看有哪些定义:
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)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
- (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
用于跟踪所改变的类,打上标记,用于替换类方法,防止重复替换类方法。
自旋锁(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
//执行的入口
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)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)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就是根类没有父类。这就是退出的条件。
创建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转换之后的方法签名的结构:
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. 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:
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
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)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;
}
用之前将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
结构变了。可参考
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
转,技术也就那样。居安思危!
链接:https://www.jianshu.com/p/2c93446d86bd
链接:https://www.jianshu.com/p/21a82ea227de