Aop轻量级框架:Aspects
Posted ExcBadAccess
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Aop轻量级框架:Aspects相关的知识,希望对你有一定的参考价值。
写在前面的话:这是一篇极其不友好的博客,内容枯燥冗长,而且阅读本篇,需要有一定的Runtime、NSInvocation理解基础以及一定的C语言基础。也是因为最近做了埋点统计的需求,顺便看了Aspects的源码,本篇主要是作为个人阅读Aspects的笔记。如果有错误纰漏,欢迎指正。建议拷贝链接发送到电脑端,使用浏览器浏览。
Aop 就是面向切面编程
,针对某一些类、方法、或者某一个功能编程。一般在做日志记录,性能统计,安全控制,事务处理,异常处理,或者对局部进行解耦的时候常用到,是一种新的编程思想,它解决了OOP的扩展问题。Aspects主要是通过runtime,Method swizzling来实现AOP。
Objective-C是一门动态语言,虽然并非一门开源的语言,不过runtime已经被开源在[①这里],它采用了smalltalk语言的一些特性。当我们调用方法或者函数的时候,实质是在给对象发消息,根据函数名生成selector,通过selector(SEL指针)找到指向具体函数实现的IMP,然后才执行函数的实现逻辑。在这个基础上,如果我们人为的改变selector与IMP的对应关系,那就能让原本的函数做一些其它的事情。一般来说,
所有OC中系统方法都有一个与之对应的runtime方法,或者说都存在C实现,所以理论上几乎所有的系统方法都可以hook,利用方法重定向实现多接收者的方法转发。
OC方法的基本组成
Objective-C 在三种层面上与 Runtime进行交互:
通过 Objective-C 源代码
通过 Foundation 框架的 NSObject 类定义的方法
通过对 Runtime 库函数的直接调用
先看一下OC的类、对象和方法的基本结构和一些基本的Runtime术语:
// objc.h
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPEStypedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version
OBJC2_UNAVAILABLE;
long info
OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
一点点的看:
类:Class
类型是一个objc_class结构体指针,在objc_class结构体中包含:
isa:类的isa指针
super_class:class类型的父类类型,这2个元素也为OC的继承和多态做了基础支撑,但也由于它们2个,OC中无法直接实现多继承,需要需要多继承的方式,只能通过NSProxy、协议、扩展、组合实现多引入等方式实现伪多继承
version:默认为0,类的版本信息
info:类信息
instance_size:该类的实例化变量大小
ivars:该类的成员变量链表,runtime提供一些获取成员变量名称,个数,变量链表的的接口。
methodLists:该类的方法链表
cache:方法缓存
protocols:协议链表,因为这种结构,所以OC可以实现多协议
对象:InstanceObject,是一个objc_object结构体isa是它唯一的私有成员变量,即所有对象都有isa
指针,我们在对对象进行操作的时候,可以通过isa
找到这个实例所对应的类。我们可以通过NSClassFromString(<#NSString * _Nonnull aClassName#>)
或者class_getName
拿到类名。
isa指针:isa
是一个objc_class结构体类型的指针,或者说是class类型的指针。
网上关于isa指针理解的博客比较多,这里找了一篇,可以资金异步看看。
id类型:id
是一个objc_object 结构体类型的指针,或者说是对象类型的指针,关于id类型有很多人一般性的理解为与NSObject *(instancetype)
类型一样,实际虽然同样可以作为万能指针使用,但是id类型的范围更大,它可能还包含NSProxy *
方法:OC中方法(method)包括SEL
和IMP
,即interface和implementation。
·SEL
:method selector,是一个objc_selector指针类型,opaque类型,叫方法选择器
,你可以理解为方法的身份证,我们通过这个指针映射到相应IMP,从而调用方法的实现代码。不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。我们可以通过@selector()
或者runtime的sel_registerName
拿到方法的方法选择器。
·IMP
:method implementation,是一个指向方法实现的函数指针,是方法的实现
,通过method_getImplementation
和 method_setImplementation
我们可以拿到和重新设置方法的实现。
从方法开始调用到结束发生了什么和为什么要用Aspects
先来看一个方法调用的例子,创建一个NSObject的实例变量,需要调用Alloc、init方法。
//可以写成下面这样,
//本质上OC代码在runtime也会转化成下面这行代码通过消息发送的方式执行。
//alloc,init都是SEL
//映射到NSOject的IMP链表中对应的实现之后才会再内存中开辟空间然后初始化返回一个实例对象
objc_msgSend(objc_msgSend([NSObject class],@selector(alloc)),@selector(init));
//理论上我们直接采用上面的方式也可以开发出一款产品,但是代码会变得比较难以理解和维护
//因此苹果在这个基础上再进行了一层封装,也就是我们的OC,这也使得它更加接近自然语言。
NSObject *obj=[[NSObject alloc] init];
通过Xcode调试调用栈我们发现从方法调用到最后执行经过了下面这个过程:
通过上面的图,我们知道系统内部依次会经过4个方法resolveInstanceMethod
,forwardingTargetForSelector
,methodSignatureForSelector
,forwardInvocation
等4个函数,去寻找和映射IMP。
Aspects选择了在forwardInvocation
这个阶段处理是因为:resolvedInstanceMethod
适合给类/对象动态添加一个相应的实现,forwardingTargetForSelector
适合将消息转发给其他对象处理,相对而言,forwardInvocation
是里面最灵活,最容易接触和操作到的地方并且不会出现意外bug的地方。
Aspects提供了2个枚举、一个实例方法和一个工厂方法:
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// 在原来的实现之后触发(默认)
AspectPositionInstead = 1, /// 替代原来的实现方法触发.
AspectPositionBefore = 2, /// 在原来的实现之前触发.
AspectOptionAutomaticRemoval = 1 << 3 /// 在执行1次后自动移除.
};
typedef NS_ENUM(NSUInteger, AspectErrorCode) {
...
...
}//AspectErrorCode枚举,这个枚举提供了一些错误码
///一个类方法,一个实例方法,利用它们可以Hook某个特定实例对象的某个对象方法,Hook所有实例对象的某个对象方法,Hook类方法
///传入的参数是selector:要被hook的方法。options:hook的时机,传入AspectOptions值。block:hook进去的代码。error:错误信息。
+ (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;
通过2个例子来说一下Aspects的使用
1.防空值处理,我们根据字符串创建一个URL,如果字符串为空那么程序就会崩溃
runtime
//根据字符串创建一个url
NSString *baidu =@"https://www.baidu.com";
NSURL *url =[NSURL URLWithString:baidu];
Class cls = [self class];
Method originalMethod = class_getInstanceMethod(cls, URLWithString);
Method swizzledMethod = class_getInstanceMethod(cls, Dzw_URLWithString);
BOOL didAddMethod = class_addMethod(cls,
origSelector,
method_getImplementation(Dzw_URLWithString),
method_getTypeEncoding(Dzw_URLWithString));
if (didAddMethod) {
class_replaceMethod(cls,
newSelector,
method_getImplementation(URLWithString),
method_getTypeEncoding(URLWithString));
} else {
method_exchangeImplementations(URLWithString, Dzw_URLWithString);
}
然后实现一个用来替换系统方法的方法
//处理空值 防止崩溃
-(NSURL *)Dzw_URLWithString:(NSString *)string{
if(string != nil){
//这里有人会不理解 为什么又调用了自己,因为前面URLWithString已经和Dzw_URLWithString交换,所以此时如果在这里调用URLWithString,会被系统理解为在调用Dzw_URLWithString,到这里会形成死循环,因此应该调用Dzw_URLWithString,这个时候会被转发调用URLWithString的IMP
[NSURL Dzw_URLWithString:string];
}else{
NSLog(@"抛出错误");
}
}
Aspects处理
[NSURL aspect_hookSelector:@selector(URLWithString:) withOptions:AspectPositionBefore usingBlock:^(idaspectInfo){
if(string == nil){
NSLog(@"抛出错误");
}
} error:NULL];
2.埋点统计,对用户进过的某一个页面进行统计。可以对ViewController的生命周期方法进行拦截,加入统计代码。如果我们仅仅对某一个viewcontroller进行hook那么使用runtime操作也很简单,但是如果我们需要对大量的viewcontroller进行hook的话会产生大量的重复代码,这里runtime的实现方式我就不写了,直接使用Aspects实现。
[TestViewController aspect_hookSelector:@selector(ViewDidAppera:) withOptions:AspectPositionAfter usingBlock:^(idaspectInfo){
NSLog(@"统计次数+1");
} error:NULL];
到这里,使用Aspects我们需要做的就结束了,当然Aspects的使用途径不只是做埋点统计和容错处理,还有更多其他的用途。
Aspects在这个过程中做了什么?
Aspects 的方案是:对于待 hook 的方法,将它的SEL指向 objc_msgForward / _objc_msgForward_stret ,同时生成一个新的 aliasSelector 指向原来的 IMP,并且hook到forwardInvocation 函数,使他指向自己的实现。按照上面的思路,当被 hook 的 selector 被执行的时候,首先根据 selector 找到了 objc_msgForward / _objc_msgForward_stret ,而这个会触发消息转发,从而进入 forwardInvocation。同时由于 forwardInvocation 的指向也被修改了,因此会转入新的forwardInvocation 函数,在里面执行需要嵌入的block里的附加代码,完成之后,再转回原来的 IMP,从而达到向系统方法中嵌入代码的目的。
直接调用runtime的方法交换的方案是:直接交换方法的IMP。在新的IMP中实现调用系统的方法的IMP,并在之前添加新的代码,达到嵌入代码的目的。
Aspects结构
如果不看牛逼的开源库,我就不知道自己有多菜。Aspects核心一共只有几个类,可供外部使用的只有2个方法外加2个协议,一共不到10个方法,.m
不到1000行代码,但是读了1周多。大致的结构:
简单翻译一下Aspects申明的2个协议和4个内部类:
AspectToken协议
只提供了一个方法remove
用于注销切面。
@protocol AspectToken <NSObject>
/// 注销切面.
/// @return 如果成功注销返回YES,失败返回NO.
- (BOOL)remove;
@end
AspectInfo协议
/// AspectInfo 协议是用于设置block的第一个参数
@protocol AspectInfo <NSObject>
/// 当前hook的单例.
- (id)instance;
/// 被hook的方法的原始调用
- (NSInvocation *)originalInvocation;
/// 方法的参数列表封装,这是一个懒执行方法
- (NSArray *)arguments;
@end
内部类AspectInfo
需要与AspectInfo协议区分开,Aspects实现的一个内部类,它遵循AspectInfo协议,是将NSInvocation信息封装了一层。同时内部实现了一个NSInvocation的简单分类用于获取参数列表
@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
内部类AspectIdentifier
我们在外部传入的block,打包成AspectIdentifier。注意添加切面的 Block 的第一个参数,在使用这个block插入我们需要hook进去的代码的时候,这里生成了blockSignature与入参的object的selector得到的方法签名(methodSignature)的兼容,兼容性判断成功了才会走初始化方法。
@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
内部类AspectsContainer
@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
AspectsContainer是用来作为切面的容器的,因为 AspectsContainer 是在 NSObject 分类中通过 AssociatedObject 方法与当前要 Hook 的所hook的方法关联在一起的,里面有3个队列分别承载对应AspectOption中的AspectPositionBefore,AspectPositionInstead,AspectPositionAfter,这里需要特别注意的是为了保证线程安全这3个队列都是使用atomic修饰。
内部类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
他是一个切面追踪器,那什么是切面追踪器?
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];
// 所有的父类拿到子类变化的标识
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
通过AspectTracker指针指向当前类,而指针会被存储在全局静态变量swizzledClassesDict
中,当一个父类有一个子类发生变化(或者被hook)的时候,父类都会知道。 也就是说 AspectTracker 是从下而上追踪,最底层的 parent参数 为 nil,父类的 parent 为子类的 tracker。
Aspects hook的大致思路和核心逻辑
起初我是想先找一个代码量少的读,先练练手,可是我发现,Aspects在这短短的1000行代码中包含了block的细节处理,Runtime、NSInvocation封装、线程安全和线程锁的选择、自旋锁OSSpinLockLock的使用。每一项都够写成好几篇。
还是以调用的入口函数作为切入点,以上面的例子为例:
[NSURL aspect_hookSelector:@selector(URLWithString:) withOptions:AspectPositionBefore usingBlock:^(idaspectInfo){
if(string == nil){
NSLog(@"抛出错误");
}
} error:NULL];
看一下这个方法的实现:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}
它返回了一个函数,这里是Aspects真正的入口函数:aspect_add
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;
}
这个函数写的很简单,进入函数以后有3个断言用于检查空值。
这个方法到底做了什么?
一点点的分析
__block AspectIdentifier *identifier = nil;
为了适应mrc和arc使用block是因为identifier作为一个局部变量,在ARC环境下,Block会对修饰的对象强引用,在MRC环境下对修饰的对象不会强引用。而且 __block
修饰局部变量,表示这个对象是可以在block内部修改,如果不这样写,会报 Variable is not assignable (missing __block type specifier)
的错误。
为什么使用选择
OSSpinLockLock
而不是选择更简单易用的@synchronized
或是NSLock
?
应该是为了效率考虑,因为OSSpinLockLock
的效率确实要高很多,但在这种可能超高频度的调用,并且可能在任何场景调用的方法,它会存在一个很大的问题,具体可以看[②YYKit作者ibireme的这篇博客],可能会引起死锁,框架使用者将会极其难以调试出来。
ect_isSelectorAllowedAndTrack(self, selector, options, error)
这个判断里面主要是检查传入的参数(self, selector, options, error),判断对象或者类是否响应传入的selector,比如说你在hook一些不能swizzling method的方法,hook的对象/类是否响应这个方法等等,传入的如果是类方法还要增加重复替换校验,避免出现重复hook的问题,类方法只能替换一次,是在整个类的继承树上校验,而不只是单单的一个类
//对类方法做替换重复性校验,即,while当前的类是基类的时候执行do{}
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)));
同时在这个里面创建了AspectTracker
,父类会拿到一个子类发生修改的标识,如果没有父类就退出循环,跟上面的do{}while()结合,就能达到避免重复替换的效果。到这里退出判断。
创建AspectsContainer,前面接口介绍部分看到,它包提供了添加和移除切面的接口,以及3个用来存储不同hook时机的切面的队列。
AspectsContainer用来保存某个切面的信息,也就是AspectIdentifier
。AspectIdentifier
包含了每个切面的一些具体的信息,包含一些属性:方法的SEL selector
、block作为需要hook进方法的block
代码块,weak类型的具体信息所属类object
、hook时机AspectOptions
、方法签名NSMethodSignature类型的blockSignature
,注意的是,因为我们在使用Aspects的时候是直接通过block代码块
来hook方法的,所以需要将我们传入的block
转换为具体的方法,而不再以函数指针的身份嵌入,因此需要进行方法签名。这就是存在内部的_AspectsBlock的存在和为什么AspectIdentifier里需要有一个方法签名原因
看一下内部的_AspectsBlock
块的结构
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转换为_AspectsBlock,具体细节看下面注释。
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;
}
//得到我们传入block的类型编码。通过类型编码得到block所对应的方法签名。
const char *signature = (*(const char **)desc);
// Returns an NSMethodSignature object for the given Objective-C method type string.
// 根据类型编码返回真正方法签名
return [NSMethodSignature signatureWithObjCTypes:signature];
}
转方法签名成功以后,把方法签名和实际方法进行比较
(感觉Aspects作者也写到这里才感觉到入口需要对参数进行检测的,或者是我太菜到这里才觉得很有道理)。具体的比较的方法就是拿到替换方法的方法签名和我们将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;
}
比较完成
将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
中,
[aspectContainer addAspect:identifier withOptions:options];
看到这里我有一个疑问,他是怎么知道是实例方法还是类方法的?
注意到Aspects 为了能区别 Class 和 Instance ,实现了名为 aspect_hookClass 的方法,我认为其中的实现值得我仔细看一看,也觉得如果想深入理解Rumtime必要花点时间理解这里的实现逻辑。为了判断方法的类型,Aspects实现下面这个函数。不做过多解析,全在注释里了,比较难理解的点在于Runtime部分的理解,对 .class 和 object_getClass 的区分以及利用Runtime动态创建Class的过程:
static Class aspect_hookClass(NSObject *self, NSError **error) {
// 断言 self
NSCParameterAssert(self);
// class
Class statedClass = self.class;
// isa
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
// 已经子类化过了
// .class 当 target 是 Instance 则返回 Class,当 target 是 Class 则返回自身。object_getClass 返回 isa 指针的指向。
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// 我们混写了一个 class 对象,而非一个单独的 object
}else if (class_isMetaClass(baseClass)) {
// baseClass 是元类,则 self 是 Class 或 MetaClass,混写 self
return aspect_swizzleClassInPlace((Class)self);
// 可能是一个 KVO'ed class。混写就位。也要混写 meta classes。
}else if (statedClass != baseClass) {
// 当 .class 和 isa 指向不同的情况,混写 baseClass
return aspect_swizzleClassInPlace(baseClass);
}
// 默认情况下,动态创建子类
// 拼接子类后缀 AspectsSubclassSuffix
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
// 尝试用拼接后缀的名称获取 isa
Class subclass = objc_getClass(subclassName);
// 找不到 isa,代表还没有动态创建过这个子类,那么动态创建这个类
if (subclass == nil) {
// 创建一个 class pair,baseClass 作为新类的 superClass,类名为 subclassName
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) { // 返回 nil,即创建失败
NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
return nil;
}
// 混写 forwardInvocation:
aspect_swizzleForwardInvocation(subclass);
// subClass.class = statedClass
aspect_hookedGetClass(subclass, statedClass);
// subClass.isa.class = statedClass
aspect_hookedGetClass(object_getClass(subclass), statedClass);
// 注册新类
objc_registerClassPair(subclass);
}
// 覆盖 isa
object_setClass(self, subclass);
return subclass;
}
到这里准备工作就完了,然后把传入的方法hook进系统方法中
主要是通过在forwardInvocation阶段,修改交换方法的IMP来达到目的,具体逻辑可以看代码中的注释已经非常具体了。
static void aspect_swizzleForwardInvocation(Class klass) {
// 断言 klass
NSCParameterAssert(klass);
// 如果没有 method,replace 实际上会像是 class_addMethod 一样
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
// 拿到 originalImplementation 证明是 replace 而不是 add,情况少见
if (originalImplementation) {
// 添加 AspectsForwardInvocationSelectorName 的方法,IMP 为原生 forwardInvocation:
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
上面的方法就是在forwardInvocation阶段拿到原来方法和新生产的方法的IMP,准备交换他们2个方法的IMP,之后 forwardInvocation: 的具体实现在 ASPECTS_ARE_BEING_CALLED 中,里面能看到 invoke 标识位的不同是如何实现的:
// 宏定义,以便于我们有一个更明晰的 stack trace
#define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {\
[aspect invokeWithInfo:info];\
if (aspect.options & AspectOptionAutomaticRemoval) { \
aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
} \
}
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
// __unsafe_unretained NSObject *self 不解释了
// 断言 self, invocation
NSCParameterAssert(self);
NSCParameterAssert(invocation);
// 从 invocation 可以拿到很多东西,比如 originalSelector
SEL originalSelector = invocation.selector;
// originalSelector 加前缀得到 aliasSelector
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
// 用 aliasSelector 替换 invocation.selector
invocation.selector = aliasSelector;
// Instance 的容器
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
// Class 的容器
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) {
// 如果有任何 insteadAspects 就直接替换了
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else { // 否则正常执行
// 遍历 invocation.target 及其 superClass 找到实例可以响应 aliasSelector 的点 invoke
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);
// 如果没有 hook,则执行原始实现(通常会抛出异常)
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
// 如果可以响应 originalForwardInvocationSEL,表示之前是 replace method 而非 add method
if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else {
[self doesNotRecognizeSelector:invocation.selector];
}
}
// 移除 aspectsToRemove 队列中的 AspectIdentifier,执行 remove
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
//宏定义的作用域结束
#undef aspect_invoke
Aspects 让我们在指定 Class 或 Instance 的特定 Selector 执行时,根据 AspectOptions 插入我们自己的 Block 做 Hook,而这个 Block 内部有我们想要的有关于当前 Target 和 Selector 的信息:
- (BOOL)invokeWithInfo:(id)info {
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
NSInvocation *originalInvocation = info.originalInvocation;
NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;
// 偏执。我们已经在 hook 注册的时候检查过了,(不过这里我们还要检查)。
if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
}
// block 的 `self` 将会是 AspectInfo。可选的。
if (numberOfArguments > 1) {
[blockInvocation setArgument:&info atIndex:1];
}
// 简历参数分配内存 argBuf 然后从 originalInvocation 取 argument 赋值给 blockInvocation
void *argBuf = NULL;
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);
// reallocf 优点,如果创建内存失败会自动释放之前的内存,讲究
if (!(argBuf = reallocf(argBuf, argSize))) {
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
}
[originalInvocation getArgument:argBuf atIndex:idx];
[blockInvocation setArgument:argBuf atIndex:idx];
}
// 执行
[blockInvocation invokeWithTarget:self.block];
// 释放 argBuf
if (argBuf != NULL) {
free(argBuf);
}
return YES;
}
到这里大致上的Aspects思路和实现解析就结束了,可以感受到一个优秀的三方框架的细节处理上思路的严谨。整个代码中充满了细节,时间精力有限,加上我又懒,还有很多点我也没有研究到。
文章中涉及的外链:
①官方obj源码 https://opensource.apple.com/source/objc4/objc4-680/)
②不再安全的OSSpinLock https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/
以上是关于Aop轻量级框架:Aspects的主要内容,如果未能解决你的问题,请参考以下文章
[AOP] 4. Spring AOP中提供的种种Aspects - 异步执行
[AOP] 3. Spring AOP中提供的种种Aspects - Tracing相关