一. 什么是Aspects
[ˈæspekts] n. 方面;方位;面貌(aspect 的复数)
Aspects是一个轻量的、简单的,面向切面(AOP - Aspect Oriented Programming)的library。
二. 怎么使用Aspects
2.0 先看下头文件
头文件很简单,提供了一个类方法和实例方法
- 类方法:hook所有该类的实例对象的方法
- 实例方法:只hook该实例的方法
参数总共有4个:
AspectPositionAfter && AspectPositionBefore: 比较简单,选择调用block的时机是在原方法之前或者之后
AspectOptionAutomaticRemoval: 只hook一次
AspectPositionInstead:替换原来的实现方法,其实是不调用以前的方法
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.
};
+ (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.1 简单试下,hook下viewcontroller的viewWillAppear
方法,好使
[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];
2.2 试试自定义的方法🤥没问题
@interface AspectsDemo : NSObject
- (void)aspectTest;
@end
@implementation AspectsDemo
- (void)aspectTest {
NSLog(@"%@", NSStringFromSelector(_cmd));//aspects__aspectTest
}
@end
//调用下,没问题
[AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"%@", aspectInfo);
} error:NULL];
[[[AspectsDemo alloc] init] aspectTest];
2.3 先调用,后hook试试, 啥也没发生👌🏻
@implementation AspectsDemo
- (void)aspectTest {
printf(__func__);//-[AspectsDemo aspectTest]
}
@end
[[[AspectsDemo alloc] init] aspectTest];
[AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"%@", aspectInfo);
} error:NULL];
2.4 多次hook试试
两个block都走了,也没啥问题 😲
[AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"%@", aspectInfo);
} error:NULL];
[AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"%@", aspectInfo);
} error:NULL];
[[[AspectsDemo alloc] init] aspectTest];
2.5 试试实例方法
没问题☹️
只hook了demo1, demo2不受影响,很棒
AspectsDemo *demo1 = [[AspectsDemo alloc] initWithSting:@"666"];
[demo1 aspect_hookSelector:@selector(aspectTest) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"%@", aspectInfo);
} error:NULL];
[demo1 aspectTest];
AspectsDemo *demo2 = [[AspectsDemo alloc] initWithSting:@"777"];
[demo2 aspectTest];
2.6 试试AspectPositionInstead参数
优秀啊,使用AspectPositionInstead原方法就不走了
[AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"%@", aspectInfo);
} error:NULL];
[[[AspectsDemo alloc] init] aspectTest];
2.7 hook init方法试试
没问题
@implementation AspectsDemo
- (instancetype)initWithSting:(NSString *)string {
NSLog(@"%@",string);
if (self = [super init]) {
self.string = string;
}
return self;
}
[AspectsDemo aspect_hookSelector:@selector(initWithSting:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo, NSString *str){
NSLog(@"%@", aspectInfo);
} error:NULL];
[[AspectsDemo alloc] initWithSting:@"666"];
2.8 hook init方法, 并且多传入几个参数试试
GG了GG了😮😮😮😮😮😮,多传了id a, id b,参数对不上,就不走block了
[AspectsDemo aspect_hookSelector:@selector(initWithSting:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo,id a, id b){
NSLog(@"%@", aspectInfo);
} error:NULL];
[[AspectsDemo alloc] initWithSting:@"666"];
2.9 hook 系统的init方法试试
😮😮😮😮😮😮😮😮😮😮😮😮不行了😮😮😮😮😮😮😮😮哈哈哈😮😮😮😮😮,不走block了
[NSString aspect_hookSelector:@selector(initWithString:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo) {
NSLog(@"%@", aspectInfo);
} error:NULL];
[[NSString alloc] initWithString:@"666"];
所以我们需要研究一下一下问题
- 入参只能少,不能多,多的话hook失败
- hook 系统的init方法失败
三. 研究下源码
- 返回一个id类型对象,遵守
@protocol AspectToken <NSObject>
协议,可以使用remove
方法撤销hook
id ret = [AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"%@", aspectInfo);
} error:NULL];
[[[AspectsDemo alloc] init] aspectTest];
[ret remove];
- 判断这个类是否能hook
// 这4个方法不能hook
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
//dealloc可以hook,但是只能在调用之前hook
[selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore
// 自省,不响应的不hook
if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector])
//静态变量,记录哪些类被hook了
static NSMutableDictionary *swizzledClassesDict
// 子类的这个方法被hook过了, 就不hook
[tracker subclassHasHookedSelectorName:selectorName]
// 父类被hook过了, 就不hook
[tracker.selectorNames containsObject:selectorName]
- 检测方法签名,验证我们传入的block是否合法
//如果方法签名的参数个数大于原方法的参数个数,不走下面的逻辑了
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
return NO};
- 通过以上测试,开始真正hook的部分
// 替换原来的imp,改成自己的imp
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
// 给类增加一个新的方法,imp为原来的实现方法
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
// 这个函数就是自己的imp,内部会先调用自己的block
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {}
// 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);
//先调用原方法,在调用hook的方法
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];
break;
}
// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
}
四. 解决问题
- 2.8 hook init方法, 并且多传入几个参数试试
这个问题看源码已经解决了,是因为我们block传入的参数个数大于原方法入参个数 - 2.9 hook 系统的init方法试试
这个问题打断点,发现originalImplementation
为nil,所以没有走接下来的hook流程
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
经过百度,其实是因为NSString *realString = [[NSString alloc] initWithString:@"666"];
里面的NSString, 在真正创建的时候,用的其实是它的子类,可以参考这篇文章
(lldb) po realString.class
__NSCFConstantString
(lldb) po realString.superclass
__NSCFString
(lldb) po realString.superclass.superclass
NSMutableString
(lldb) po realString.superclass.superclass.superclass
NSString
所以, 我们应该这样hook
[[NSString alloc].class aspect_hookSelector:@selector(initWithString:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo) {
NSLog(@"%@", aspectInfo);
} error:NULL];
NSString *realString = [[NSString alloc] initWithString:@"666"];
同理,对NSAttributedString
的hook也是如此,因为NSAttributedString
真正使用的也是其簇类。
五. 总结
5.0 什么是Aspects
- Aspects使用runtime的
class_replaceMethod
方法交换类的forwardInvocation
方法,并用block将相关信息传递到调用处的一个AOP框架。
5.1 使用Aspects注意事项
- 不能hook @"retain", @"release", @"autorelease", @"forwardInvocation
- 可以hook
dealloc
方法,但是只能在调用之前hook - block入参内部放法签名的参数个数大于原方法的参数个数,会hook失败
- 对于
NSString
等拥有簇类的类,需要使用其真正的类名去调用hook方法
5.2 如何看到Aspects
- github上作者说了,不建议在生产环境部署Aspects,因为其内部的实现不能保证在未来也是100%安全。
- 使用Method Swizzle也可以做到方法交换, 而且很容易在代码层次做校验判断。 Aspects则不太容易做到替换参数等,不是很直观。
// Method Swizzle
- (instancetype)safe_initWithString:(NSString *)aString{
return [self safe_initWithString:aString?:@""];
}
//Aspects
[[NSString alloc].class aspect_hookSelector:@selector(initWithString:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo) {
需要在这里做判断,替换参数
} error:NULL];
NSString *realString = [[NSString alloc] initWithString:@"666"];
- Aspects hook分散在各个地方,同一个hook可以被执行多次(我测试的),容易产生bug。 当然Method Swizzle也会有这样的问题,建议将hook的代码收拢到一起,便于管理和排查。
六. 待解决的问题
6.1 多次hook依然能走的问题
6.2 实例方法的hook和类的hook是否一致
Final. 学到的知识点:
objc_getAssociatedObject 直接用SEL aliasSelector,
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
说明SEL是个const void *
SEL aliasSelector = aspect_aliasForSelector(selector);
AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
网友评论