Aspects原理详解

Aspects是什么

Aspects是一个开源的的库,面向切面编程,它能允许你在每一个类和每一个实例中存在的方法里面加入任何代码。可以在方法执行之前或者之后执行,也可以替换掉原有的方法。通过Runtime消息转发实现Hook,Aspects不支持hook Static方法,既OC类方法

消息发送

我们知道给一个对象发送消息的时候会这几个步骤

  1. 类对象中查找,找到执行,找不见到则执行_objc_msgForward消息转发
  2. 如果在 1中找不到,则执行resolveInstanceMethod动态添加方法
  3. 如果2中没有处理该消息,则执行forwardingTargetForSelector转发到另一个对象处理
  4. 如果3中仍未处理这次消息则执行forwardInvocation这个时候runtime会将未知消息的所有细节都封装为NSInvocation对象作为当前方法的参数
  5. 如果以上都未处理,则报错

如果让我们去hook一个方法如何做

1.找到将要被hook的方法(original)
2.自定义一个方法(custom)
3.交换方法的实现,把original方法的实现指向custom,custom实现指向original然后在custom内部调用custom(实际指向是original)

+ (void)load {
    Method original  = class_getClassMethod([self class], @selector(original));
    Method exchange  = class_getClassMethod([self class], @selector(aspectMethod));
    method_exchangeImplementations(original, exchange);
}

- (void)original {
    NSLog(@"Busness logic");
}

//交换后调用aspectMethod方法的时候回走custom的方法
- (void)aspectMethod {
    NSLog("aspect 记录日志操作");
    [self aspectMethod];//因为交换了,所以这里☞的是original方法的实现
    NSLog("aspect记录日志操作");
}

Aspects如何做的

我们知道对于未实现的方法会走消息转发流程
Aspects是通过把被hook方法的实现指向了_objc_msgForward,既直接走转发的流程,然后把forwardInvocation的实现指向了__ASPECTS_ARE_BEING_CALLED__方法,相当于调用被hook方法时直接执行__ASPECTS_ARE_BEING_CALLED__方法,在__ASPECTS_ARE_BEING_CALLED__中的不同时机插入一些切面逻辑

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    // 在被hook的方法之前执行
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    //替换被hook的方法
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
         //执行被Hook函数的逻辑
        Class klass = object_getClass(invocation.target);
        do {
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }
    //被hook的方法执行之后执行
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);
}

Aspects源码探索

先介绍四个工具类
AspectInfo:存储了被Hook方法相关的信息,如被hook的方法对应的实例对象,被hook方法的参数,被hook方法的实现,用来在aspects操作结束之后回调原来的实现
@interface AspectInfo : NSObject <AspectInfo>
//被hook的实例
@property (nonatomic, unsafe_unretained, readonly) id instance;
//被hook方法的参数
@property (nonatomic, strong, readonly) NSArray *arguments;
//被hook方法的原始实现
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end
AspectIdentifier:存储了切面相关的信息,包括何时(AspectOptions)、做什么(block)
@interface AspectIdentifier : NSObject
//hook的是哪一个方法,在移除切面的时候会用到
@property (nonatomic, assign) SEL selector;
//具体切面的逻辑block
@property (nonatomic, strong) id block;
//block的签名,用来做安全校验用的
@property (nonatomic, strong) NSMethodSignature *blockSignature;
//被hook的对象
@property (nonatomic, weak) id object;
//被hook的时机
@property (nonatomic, assign) AspectOptions options;
@end
AspectsContainer:容器类,类对象中每一个被hook的方法都对应一个container,用来存放AspectIdentifier,根据AspectOptions分别存储在三个对应的数组中,在执行时循环执行
@interface AspectsContainer : NSObject
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end
AspectTracker:这个类主要是用来判断当前方法能不能被hook
@interface AspectTracker : NSObject
//当前tracker对应的类
@property (nonatomic, strong) Class trackedClass;
//当前tracker对应的类名字
@property (nonatomic, readonly) NSString *trackedClassName;
//当前类所有被hook的方法的名字集合,如果selectorNames包含selector则证明当前类对象是hook了该selector的类对象
@property (nonatomic, strong) NSMutableSet *selectorNames;
//当前类所有被hook的方法对应的子类的trackers
@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;
@end

Aspects中hook类有一个准则(自己总结的,如有错误请指正):
1.@"retain", @"release", @"autorelease", @"forwardInvocation:"这些方法不能被hook
2.@"dealloc"只能在AspectPositionBefore时机执行(因为dealloc后该实例对象就不存在了)
3.同一个类对象的同一个方法可以被多次hook
4.不同类对象如果存在于一条继承链关系链上则只能hook一次(AspectTracker就是用来判断在一条继承链上是否曾经被hook过)(如何判断?? 通过当前类的selectorNamesToSubclassTrackers根据selector找到对应的trackers集合,然后判断tracker.selectorNames是否包含当前selector,递归执行上述步骤,如果找到了则证明被hook过下面有详细介绍)

NSObject +Aspects

头文件入口函数,内部条用了aspect_add

类对象方法
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add((id)self, selector, options, block, error);
}
实例方法

最终还是通过生成一个中间类对象,类似kvo,只对当前对象起作用,而不会影响其他实例对象,后面详细介绍

- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add(self, selector, options, block, error);
}
//hook类方法
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 每一个类对象被hook的方法都对应一个container,存放了对同一个对象的同一个方法的多次hook
            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;
}
判断能不能被hook

这个函数分两部分,添加tracker部分和判断能不能被hook部分

static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {

      //1判断部分

      //2添加部分
}

我们先看添加部分,知道了怎么添加我们去判断的时候才能更好的知道怎么去判断

1.添加Tracker

总结:

  • 根据当前类找到tracker,如果不存在则生成新的并保存到全局字段中
  • 类对象第一次hook该方法时subtracker为nil,selector添加到selectorNames(这里就解释了为什么 tracker.selectorNames如果包含了selector则能够证明该类是hook了selector而不是父类hook的,如果是父类hook回把子类的tracker添加到以 `selector` 为key的selectorNamesToSubclassTrackersNSSet中而不是把selector添加到selectorNames)
  • 查找父类并重复执行直到根类为止,形成了一个链表结构,如图所示


    类和Tracker的关系
    //添加Tracker部分
    // Add the selector as being modified.
    currentClass = klass;
    AspectTracker *subclassTracker = nil;
    do {
        //根据当前类查找类对应的tracker
        tracker = swizzledClassesDict[currentClass];
        //如果不存在则创建,并保存
        if (!tracker) {
            tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
            swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
        }
        if (subclassTracker) {
            [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
        } else {
            [tracker.selectorNames addObject:selectorName];
        }
        subclassTracker = tracker;
    }while ((currentClass = class_getSuperclass(currentClass)));
2.判断能不能被hook

总结:

  • 根据类对象查找对应的Tracker
  • 向下子类查找,根据当前tracker查找链表中的以当前selector为key的所有子tracker,并判断tracker.selectorNames是否包含当前selector,如果包含则证明子类已经hook过
  • 判断当前类是否hook了selector,如果父类hook了则返回NO,如果是当前类对象对同一个方法hook多次,这个是允许的
    if (class_isMetaClass(object_getClass(self))) {//如果self是类对象
        Class klass = [self class];
        NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
        Class currentClass = [self class];
        AspectTracker *tracker = swizzledClassesDict[currentClass];
        if ([tracker subclassHasHookedSelectorName:selectorName]) {//检查直接或间接继承了该类的子类是否hook了selectorName方法
            //链表从header到ender查询hook了selectorName的类对应的AspectTracker对象
            NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName];
            NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"];//获取所有hook该方法的子类名称
            NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames];
            AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
            return NO;
        }
        do {//查找父类是否hook过
            //找到类对应的AspectTracker信息
            tracker = swizzledClassesDict[currentClass];
            if ([tracker.selectorNames containsObject:selectorName]) {//证明hook过该方法
                if (klass == currentClass) {//同一个类对象对同一个方法  hook  多次
                    // Already modified and topmost!
                    return YES;
                }
                //父类的继承链上的类对象曾经hook过
                NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)];
                AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
                return NO;
            }
        } while ((currentClass = class_getSuperclass(currentClass)));

交换方法实现直接走消息转发的逻辑

总结来说就是把原来的selector方法指向消息转发函数,把自定义的函数指向原来的selectorIMP实现

  • selector => _objc_msgForward / _objc_msgForward_stret
  • aspects_`selector`指向selectorIMP
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    Class klass = aspect_hookClass(self, error);//交换
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    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.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}

交换forwardInvocation实现(这一步最初我也很迷惑,为什么会把这个方法hook一遍,直接用forwardInvocation也是可以实现的,这里其实是为了防止子类重写了这个方法而有没有调用父类的)

在消息转发中会走forwardInvocation这个方法,我们交换这个类的实现为我们自己的方法__ASPECTS_ARE_BEING_CALLED__,至此我们熟悉了调用Aspects函数后的准备工作,以及是如何转发到我们自己定义的函数中的

干货来了

总结:
1.根据self和selector找到对应的container
2.执行container beforeAspects中的切面
3.根据aspects_`selector`找到原始的方法实现,赋值给invocation
3.1如果是替换操作则直接执行container insteadAspects
3.2执行原始的没有被hook的方法实现
4执行container afterAspects中的切面
5这一步其实是针对父类hook了一个方法,类相应的方法已经交换过了,这时候如果子类执行了一个不存在的方法也会走消息转发相关的逻辑从而走到这里面来,通过判断是否响应交换后的方法来判断是因为调用了一个不存在的方法走进来的还是通过hook交换方法实现走进来的,一般这里直接抛出异常即可

#pragma mark - Aspect Invoke Point

// This is a macro so we get a cleaner stack trace.
#define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {\
    [aspect invokeWithInfo:info];\
    if (aspect.options & AspectOptionAutomaticRemoval) { \
        aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
    } \
}

// 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;
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    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)];
}

总结:

终于写完了,记录一下吧,这其中只是讲解了主要的流程,有很多细节没有涉及到,如涉及到Block 的底层结构,如何获取Block的签名,如何回调block中的代码,以及如何如何移除aop切面的逻辑等。还是需要自己去多研究源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 1. 简介 Aspects是一个令人愉悦的、简单的面向切面编程的库。 可以把 Aspects 看成是一个 meth...
    ADreamClusive阅读 342评论 0 0
  • 序言 Aspects 是一个开源的的库,面向切面编程,它能允许你在每一个类和每一个实例中存在的方法里面加入任何代码...
    路飞_Luck阅读 5,048评论 0 24
  • 我们一定要给自己提出这样的任务:第一,学习,第二是学习,第三还是学习。 —— 列宁 前面几章大概的说了一下iOS的...
    郭小弟阅读 15,248评论 1 34
  • 作者:郭小弟 我们一定要给自己提出这样的任务:第一,学习,第二是学习,第三还是学习。 —— 列宁 今天看了一个As...
    iOS亮子阅读 747评论 0 6
  • 前言 在读这篇文章之前,需要对Runtime消息发送、消息转发有一定的了解。Runtime系列Objective-...
    Dolphii阅读 2,595评论 0 2