aspects
前言
整理了下AOP相关的东西,AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
方案一(消息转发之前)
该方案是使用`method_exchangeImplementations` 两个已知的方法和实现进行交换。基础AOPDemo 具体参考这个Demo,这里就不展开了,这种实现方式比较直观,用容器抽离出来即可,一样可以实现基本的AOP
方案二(消息转发拦截)
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); //调用这个函数,伪代码...
}
//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; //_objc_msgForward 用于消息转发
return imp;
}
当一个类发送一个无法识别的Selector时,系统会抛出unrecognize selector的报错,根据上面的代码可以看出,当无法识别的方法时,会返回_objc_msgForward进行消息转发,如果我们不处理,就会报错,处理有三步骤(点击打开链接),这里就不细说了,了解runtime的应该都知道
主要是下面三个方法进行重定向,如果不操作就报错
- resolvedinstanceMethod: 适合给类/对象动态添加一个相应的实现,
- forwardingTargetForSelector:适合将消息转发给其他对象处理,
- forwardInvocation: 是里面最灵活,最能符合需求的。
aspects利用forwardInvocation进行消息截获,通过动态生成子类(类似KVO),把子类的forwardInvocation的实现替换成
__Aspect_Are_Being_Called__进行自定义处理。然后把需要Hook的方法例如,在框架内部添加前缀Aspects_XX作为key,把Block和Option都存储起来,再通过动态生成子类,给子类添加一个前缀Aspects_XX的SEL方法,其实现就是原未被Hook函数的IMP,最后把原方法的IMP动态指向_objc_msgForward,那么当方法被触发的时候,就直接进行消息转发,进入forwardInvocation方法拦截,由于实现IMP已更改指向,所以会进入__Aspect_Are_Being_Called__进行拦截处理,处理完之后重新调用前缀Aspects_XX方法,该方法的IMP指向原方法的IMP,整个流程就结束了,流程图自己根据源码画的,做个记录
核心代码
// 核心方法 1.Hook forwardInvocation 到自己的方法
// 2.交换原方法的实现为_objc_msgForward 使其直接进入消息转发模式
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSERROR **error) {
NSCparameterAssert(selector);
// 1 swizzling forwardInvocation
Class klass = aspect_hookClass(self, error);
// // 被 hook 的 selector
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
// 判断需要被Hook的方法是否应指向 _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);
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);
}
// We use forwardInvocation to hook in.
// 把 selector 指向 _objc_msgForward 函数
// 用 _objc_msgForward 函数指针代替 selector 的 imp,然后执行这个 imp 进入消息转发模式
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
这里就是核心代码,主要分为两部分
1.aspect_hookClass
// 动态生成子类 把子类的方法替换成自己的
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
// 如果Self是实例变量 class都是返回isa指针 都是AspectsViewcontroller
Class statedClass = self.class; // 1、对象 返回isa 2、class 返回本类
Class baseClass = object_getClass(self); // 返回isa
NSString *className = NSStringFromClass(baseClass);
// Already subclassed
// 是否有 _Aspects_ 后缀
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// We swizzle a class object, not a single object.
}else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
// Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
}else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}
// Default case. Create dynamic subclass.
// 动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation 方法
const char *subclassName = [className stringByAPPendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
// 创建类AspectsViewController的子类 AspectsViewController_Aspects_ // 生成 baseClass 对象的子类
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;
}
// 把子类的 forwardInvocation 指向 __ASPECTS_ARE_BEING_CALLED__ // 替换子类的 forwardInvocation 方法
aspect_swizzleForwardInvocation(subclass);
// 修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回当前对象的 class 隐藏对外的Class 类似KVO
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
// 将当前对象 isa 指针指向了 subclass
// 将当前 self 设置为子类,这里其实只是更改了 self 的 isa 指针而已
object_setClass(self, subclass);
return subclass;
}
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
// 根据新创建的Self 子类AspectsViewController_Aspects_ //swizzling forwardinvation 方法
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
// If there is no method, replace will act like class_addMethod.
// 由于新创建的class没有实现 forwardInvocation 因此 这里的 originalImplementation就是空的
// 使用 __ASPECTS_ARE_BEING_CALLED__ 替换子类的 forwardInvocation 方法实现
// 由于子类本身并没有实现 forwardInvocation ,
// 所以返回的 originalImplementation 将为空值,所以子类也不会生成 AspectsForwardInvocationSelectorName 这个方法
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
第一部分主要是动态生成一个子类,然后把子类的forwardInvocation函数动态替换成__ASPECTES_ARE_BEING_CALLED__,然后通过object_setClass把self的isa指针指向subClass,这样后期调用的时候,会直接进入到动态创建的这个子类中来
2.交换方法的实现,使其直接进去__msg_forward__转发流程
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// Make a method alias for the existing method implementation, it not already copied.
// 让一个新的子类方法名指向原先方法的实现,处理回调
const char *typeEncoding = method_getTypeEncoding(targetMethod);
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);
}
// We use forwardInvocation to hook in.
// 把 selector 指向 _objc_msgForward 函数
// 用 _objc_msgForward 函数指针代替 selector 的 imp,然后执行这个 imp 进入消息转发模式
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
第二部分aspect_isMsgForwardIMP是会提前进行判断原方法是否有被Hook走,有的话就不会再操作(这里和JSPatch会冲突),然后有两部操作,第一步吧动态生成的带有前缀的方法的IMP指向原方法的实现,第二步把原来函数的IMP指针指向objc_msgForward直接进入消息转发流程
最终Hook到以下函数,执行完之后回调到原来的方法实现,到此所有流程就结束了
// This is the swizzled forwardInvocation: method.
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;
// 拿出对象的关联Aspects
Aspectscontainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
// 拿出类关联的Aspects
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)];
}
该函数为swizzle后, 实现新IMP统一处理的核心方法 , 完成一下几件事
- 处理调用逻辑, 有before, instead, after, remove四种option
- 将block转换成一个NSInvocation对象以供调用
- 从AspectsContainer根据aliasSelector取出对象, 并组装一个AspectInfo, 带有原函数的调用参数和各项属性, 传给外部的调用者 (在这是block) .
- 调用完成后销毁带有removeOption的hook逻辑, 将原selector挂钩到原IMP上, 删除别名selector
Demo测试
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
- (void)testexample {
TestClass *testClass = [TestClass new];
TestClass *testClass2 = [TestClass new];
__block BOOL testCallCalled = NO;
[testClass aspect_hookSelector:@selector(testCall) withOptions:AspectPositionAfter usingBlock:^{
testCallCalled = YES;
} error:NULL];
[testClass2 testCallAndExecuteBlock:^{
[testClass testCall];
} error:NULL];
XCTAssertTrue(testCallCalled, @"calling testCallAndExecuteBlock must call testCall");
}
这两个是官方的用法Demo,第一个是类方法实现全局跟踪Log打印的AOP实现,第二个就是对象方法,针对某个对象的Hook
思路总结
1.找到被 hook 的 originalSelector 的 方法实现
2.新建一个 aliasSelector 指向原来的 originalSelector 的方法实现
3.动态创建一个 originalSelector 所在实例的子类,然后 hook 子类的 forwardInvocation: 方法并将方法的实现替换成 ASPECTS_ARE_BEING_CALLED 方法
4.originalSelector 指向 _objc_msgForward 方法实现
5.实例的 originalSelector 的方法执行的时候,实际上是指向 objc_msgForward ,而 objc_msgForward 的方法实现被替换成ASPECTS_ARE_BEING_CALLED 的方法实现,也就是说 originalSelector 的方法执行之后,实际上执行的是__ASPECTS_ARE_BEING_CALLED 的方法实现。而 aliasSelector 的作用就是用来保存 originalSelector 的方法实现,当 hook 代码执行完成之后,可以回到 originalSelector 的原始方法实现上继续执行
问题
这种Hook方法,都围绕这forwardInvocation函数进行操作,因此如果两个库都进行这样的swizzle,Aspects的库就会出现问题,主要是这段代码导致的
// 判断需要被Hook的方法是否应指向 _objc_msgForward 进入消息转发模式
if (!aspect_isMsgForwardIMP(targetMethodIMP))
如果原来的方法SEL和实现IMP没有被交换,那么这个就是NO,可以进入,把SEL指向objc_msgForward进行转发,但是如果两个库冲突,例如JSPatch也用了这样的Hook方式
具体实现,以替换 UIViewController 的 -viewWillAppear: 方法为例:
- 把UIViewController的 -viewWillAppear: 方法通过 class_replaceMethod() 接口指向一个不存在的IMP: class_getMethodImplementation(cls, @selector(__JPNONimplementselector)),这样调用这个方法时就会走到 -forwardInvocation:。
- 为 UIViewController 添加 -ORIGviewWillAppear: 和 -_JPviewWillAppear: 两个方法,前者指向原来的IMP实现,后者是新的实现,稍后会在这个实现里回调JS函数。
- 改写 UIViewController 的 -forwardInvocation: 方法为自定义实现。一旦OC里调用 UIViewController 的 -viewWillAppear: 方法,经过上面的处理会把这个调用转发到 -forwardInvocation: ,这时已经组装好了一个 NSInvocation,包含了这个调用的参数。在这里把参数从 NSInvocation 反解出来,带着参数调用上述新增加的方法 -JPviewWillAppear: ,在这个新方法里取到参数传给JS,调用JS的实现函数。整个调用过程就结束了
这里的JSPatch也是这么做的,用来Hook这个函数给JS传值,如果这个时候JSPatch已经Hook了,让原方法指向了objc_msgForward,当我们指向Aspects的上面判断的时候就是YES,不会给动态的子类添加带有前缀的方法实现(aliasSelector),那么进入转发阶段的时候(respondsToAlias = [klass instancesRespondToSelector:aliasSelector])不会成立,也就不会调用invoke的方法,会执行下面的代码
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];
}
}
而且上面的核心代码那,子类没有实现forwardInvocation,NSSelectorFromString(AspectsForwardInvocationSelectorName)也就是空的,因此直接调用unrecognize selector进行报错,这个bug的原理差不多就是这样出现的,由于未实现的关系,导致最后转发失败。回调不会去了。。。。。。
网上有个解决方案
出现上诉问题的原因在于,当 aliasSelector 没有被找到的时候,我们没能将消息正常的转发,也就是没有实现一个 NSSelectorFromString(AspectsForwardInvocationSelectorName)
, 使得消息有机会重新转发回去的方法。因此解决方案也就呼之欲出了,我的做法是在对子类的 forwardInvocation
方法进行交换而不仅仅是替换,实现逻辑如下,强制生成一个 NSSelectorFromString(AspectsForwardInvocationSelectorName)
指向原对象的 forwardInvocation
的实现。
static Class aspect_hookClass(NSObject *self, NSError **error) {
...
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
...
IMP originalImplementation = class_replaceMethod(subclass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(subclass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
} else {
Method baseTargetMethod = class_getInstanceMethod(baseClass, @selector(forwardInvocation:));
IMP baseTargetMethodIMP = method_getImplementation(baseTargetMethod);
if (baseTargetMethodIMP) {
class_addMethod(subclass, NSSelectorFromString(AspectsForwardInvocationSelectorName), baseTargetMethodIMP, "v@:@");
}
}
...
}
注意如果 originalImplementation
为空,那么生成的 NSSelectorFromString(AspectsForwardInvocationSelectorName)
将指向 baseClass 也就是真正的这个对象的 forwradInvocation ,这个其实也就是 JSPatch hook 的方法。
这是他的解决方案,就是指向真正这个对象的forwradInvocation,让JSPatch继续执行回原来的方法,避免直接执行doesNotRecognizeSelector,但是我修改完之后还是遇到莫名其妙的bug,后面遇到apple把JSPatch干掉了,就直接拿掉了,冲突是没了,但是这个bug还是未知。。。。。。
我个人感觉这种Hook消息转发的forwardInvocation方法的库最好只有一个,不然遇到哪些莫名的Bug还真是不知道怎么搞,如果真的Hook,直接根据方案一停留在Sel和IMP就行了,也够用了
JSPatch不能用了,这里有个小小的方案做一下简易的数据兼容,可以通过配置文件下发js字符串执行
点击打开链接
JSPatch
掘金大神的分析
简书大神分析
Aspects源码
相关阅读
早报导语千呼万唤使出来,让我们等待无数岁月的IOS7.0的完美越狱终于来了。小编当时就是因为手贱,不小心升级到了7.0,结果一直无法使
IOS 固件下载链接:http://www.pc6.com/mac/gj_715_1.html 或者 https://ipsw.me/降级技巧:https://jingyan.baidu.com/article/
iOS开发:苹果2018最新款手机(iPhone XS Max、iPhone XR
随着苹果设备的不断更新,作为苹果开发者来说既是好事也是坏事,好事是因为苹果设备的更新换代,淘汰了一部分旧设备更新了新设备,坏事
昨天早上,据新浪科技的一篇报道 ,@虎嗅网发了一条微博:【哪些中国公司在iOS上赚的钱最多?】美国知名移动数据监测机构App Annie披露
联系人:石虎 QQ:1224614774 昵称: 嗡嘛呢叭咪哄 QQ群:807236138