引子:Aspects简述
Aspects是iOS支持AOP(Aspect Oriented Programming,即切面编程)的一个支持包。说的浅显一些:如果你想让任何VC的viewDidAppear方法调用前都打印一句“[Class] been called!” 的话,或许Aspects可以成为你的选择。
但我们今天不讲太多Aspects的使用(因为它提供的对外接口很简单,有需求一看就很容易明白),而是讲讲Aspects实现中的一些关键流程思路。所以,该篇文章适合:
1)已经应用过Aspects并对其实现原理颇有兴趣的同学;
2)对iOS runtime,消息转发机制的应用场景有初步了解的同学;
3)技术发烧粉当然也欢迎!
让我们开门见山,直入主题
目录
1 Aspects关键流程
1.1 主流程-方法改写
1.2 主流程-方法调用
1.3 应用的底层技术参考
2 Aspects关键流程实现
2.1 对某个类的所有实例进行hook
2.1.1 关联流程说明
2.1.2 方法调用流程说明
2.2 对某个类实例进行hook
3 花絮(讨论)
3.1 Aspects无法对类方法进行关联?
3.2 关于消息转发的一点讨论
3.2.1 入坑
3.2.2 为什么要消息转发
1、Aspects的关键流程
1.1 主流程-方法改写
Aspects思路简述:针对 目标类/目标实例 对象的目标函数,基于消息转发函数(forwardInvocation:)的重写,在目标方法前/后添加代码段(基于block)或直接替换目标函数实现。
对此,Aspects提供了如下仅有的两个简单到不能再简单的对外支持接口。
/* Aspects的类对象hook接口 */
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/* Aspects的实例对象hook接口 */
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
selector: 要hook的函数。
注:两个方法都只能hook“-“函数(即实例方法,对”+“方法的hook无效)
options: 设定你要添加的代码段是期望加到目标函数之前、之后,或直接替换目标函数。
block: 要添加的或替换原函数的代码段
error: 接受错误消息的指针
1.2 主流程-方法调用
Aspects的思路:通过将目标函数的方法实现改写为转发函数的实现(_objc_msgForward),从而使对目标方法的调用可以走入主流程-方法改写中被改写的转发函数实现中,从而相当于调用了改写后的方法。
1.3 应用的底层技术参考
- 消息转发(Message Forward)
- 运行时(Runtime)
2 Aspect的关键流程实现
Aspects的具体实现中,针对类对象关联(即对某个类的所有生成实例进行关联)和实例关联略有差异。
2.1 对某个类的所有实例进行hook
举例:对UIViewController 的 viewAppear:方法进行关联
2.1.1 关联流程说明
图1.1)允许关联检查:除了基础的关联允许检查(比如某些特定方法如retain拒绝进行关联)外,也构造了一个防止重复关联的数据结构(一个全局的字典,如下图)。
图1.2)填写hook信息:针对关联的类对象,会动态关联一个AspectContainer结构,来保存进行关联的代码段(block)的信息
图1.3)重写转发方法:将forwardInvocation:的方法实现替换为Aspects的自定义实现。
图1.4)添加别名方法:为目标方法添加别名方法,并将别名方法的实现指向原始方法的实现(比如aspects__viewWillAppear:的实现实际上是viewWillAppear:的实现)
图1.5)原始方法指向转发方法:替换原始关联的方法实现为转发(_objc_msgForward)
步骤1.3~1.5 Aspects关键代码对应关系
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
/* 【Chris】aspect_hookClass方法完成了步骤1.3:改写forwardInvocation:的实现 */
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
const char *typeEncoding = method_getTypeEncoding(targetMethod);
/* 【Chris】下面几行代码完成了步骤1.4:添加别名方法,并将别名方法的实现指向原方法的实现 */
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);
}
/* 【Chris】下面一行代码完成了步骤1.5:将被hook方法的实现改为消息转发 */
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
2.1.2 方法调用流程
图2.1)调用消息转发方法:当被hook的方法(如viewWillAppear:)被某个类实例调用时,实际上会进行消息转发,从而触发对应实例forwardInvocation:方法
图2.2)执行改造的代码块:进而会调用到Aspects自定义的forwardInvocation:方法实现.(如下面截取的关键代码)
/* __ASPECTS_ARE_BEING_CALLED__方法部分代码截取 */
/* 【Chris】在原方法实现前添加的代码段(block)调用 */
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
/* 【Chris】调用原始方法或者替换的代码段(block)调用 */
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)));
}
/* 【Chris】在原方法实现后添加的代码段(block)调用 */
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
2.2 对某个类实例进行hook
举例:Aspects对UIViewController类的实例tmpObj进行hook
针对实例对象的关联流程、调用流程与类对象的大同小异,所以我们只针对其中有差异的部分进行简单的说明
图1.1)简化的hook允许判定:对实例的关联影响范围很小,所以是否允许hook只进行了诸如是否特殊方法(如retain)的检查,而不构造额外的数据结构(如swizzedClassesDict字典)进行辅助判定。
图1.2)构建Aspect子类:针对实例的关联,Aspects会为实例对应的类动态创建一个子类(比如UIViewController的就叫 UIViewController_Aspects_)
图1.3)重置实例类类型: 将实例的类型设定为动态添加的子类类型(即:UIViewController_Aspects_)
步骤1.2 ~1.3 Aspects关键代码的对应关系
/* 【Chris】完成步骤1.2:动态为hook的实例的类添加子类 */
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {
NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
return nil;
}
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
/* 【Chris】完成步骤1.3:将关联的实例的类型设定为新添加的子类 */
object_setClass(self, subclass);
图2.1)调用消息转发方法: 这边调用消息转发的类是Aspects动态创建的子类(即UIViewController_Aspects_类)
3 花絮(讨论)
3.1 Aspect无法对类方法进行关联?
在文章的1.1节,针对Aspects接口参数selector进行解读的时候我添加了一个注:说Aspects只能hook“-”方法,无法hook“+”方法。类似于下面的一张表。
“罪魁祸首”就是下面的代码了
static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
/* 【Chris】其他代码略 */
......
BOOL signaturesMatch = YES;
/ * 【Chris】hook一个类方法,instanceMethodSignatureForSelector:定会返回nil */
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
/* 【Chris】blockSignature.numberOfArguments = 2 > 0,触发了match=NO,在上层方法中结束hook流程 */
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
}
/* 【Chris】其他代码略 */
......
}
那么我们怎么对类方法(“+”方法)进行hook呢?
很简单,不用Aspects直接操作运行时呗!
(提供一个交换类方法的源代码参考,使用上不再赘述,可自行搜索methodSwizzling)
/* 交换某个类的类方法 */
+ (void)swizzlingClassMethodWithOriginalSel:(SEL)originalSel swizzledSel:(SEL)swizzledSel {
Class class = [self class];
SEL originalSelector = originalSel;
SEL swizzledSelector = swizzledSel;
Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
3.2 关于消息转发的一点讨论
3.2.1 入坑
Step1:
如果开始学习iOS的消息转发,很可能的一个出发点就是forwardInvocation:函数。
Step2:
接下来你会尝试重写forwardInvocation:函数,然后发现重写的函数根本不会被调用!
Step3:
百度后的你愕然得到提示!必须同时重写methodSignatureForSelector:函数,并反回一个signature才可以使forwardInvocation:函数被调用!你兴奋的贴上了还看不太懂的网上代码demo,哇!forwardInvocation:果然被调用了!
Step4:
兴致勃勃的带着睥睨天下的姿态研究下Aspects源码,却发现它只重写了forwardInvocation:函数但根本没有重写methodSignatureForSelector:函数!狗日的!那重写的forwardInvocation:是怎么被调用到的!?
Step5:
知道你看到了这篇文章,发现原来forwardInvocation:函数的调用条件本质上与methodSignatureForSelector:无关,只要对应的对象接收到消息转发函数(IMP为_objc_msgForward)的调用就OK了。
3.2.2 为什么要消息转发
场景布置
1)对一个类定义一个方法如:- (void)testAAA; 但不对方法进行实现。
2)提供一个testAAA被调用的操作入口。比如让某个按钮的点击调用该方法。
实验开始
1)点击那个可以调用testAAA方法的按钮。
2)系统会去找testAAA的方法实现,发现——木有!
3)系统接下来会看看你有没有尝试修复这个方法(在此不赘述),如果你没有修复,那么系统就认为你要进行消息转发了!
4)系统会调用methodSignatureForSelector:来询问你这个转发的方法的实现描述,你要给它!比如“v@:”代表方法实现的返回值为void(v),第一个参数为方法实例对象(@),第二个参数为方法名(:),看下面的定义可能会稍微清晰一些(细节一样暂不赘述)
static void __chrisTest(id self, SEL _cmd);
5)拿到方法实现描述后,系统会调用forwardInvocation:来给你操作消息转发的机会。你可以将消息转发给该实例对象的其他函数,也可以将消息转发给其他类实例对象的某个函数。(当然,函数的描述,即返回值,参数等要一致)
/* 选择目标方法 */
anInvocation.selector = @selector(anSel);
/* 选择目标对象并执行方法调用 */
[anInvocation invokeWithTarget:anObj];
结语
Aspects能挖掘的点还有很多,比如【根据具体需求局部改写Aspects的实现】【Aspects调用的runtime接口功能模拟】【Aspects数据结构分析】等等等等。或许不仅仅是Aspects,任何一个被大众广泛接受的设计中的每一个确定参数的设定,都值得问一句Why!就如比特币的总数2100万个,每2016个区块调整难度……哈哈,扯远了,但愿你可以有收获,结束!