iOS AOP 框架 - Aspects 源码解读

Aspects 是什么?

Aspects 是 iOS 上的一个轻量级 AOP 库。它利用 method swizzling 技术为已有的类或者实例方法添加额外的代码,它是著名框架 PSPDFKit (an iOS PDF framework that ships with apps like Dropbox or Evernote)的一部分。希望这篇文章能够抛砖引玉,给大家对 Aspects 的了解增加一些资料。由于个人水平有限,文章若有不对之处恳请指出,我稍作修改,大家共同进步。

怎么使用 Aspects

/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (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;

Aspects 提供了2个 AOP 方法,一个用于类,一个用于实例。在确定 hook 的 方法之后, Aspects 允许我们选择 hook 的时机是在方法执行之前,还是方法执行之后,甚至可以直接替换掉方法的实现。Aspects 的常见使用情景是 log 和 打点统计 等和业务无关的操作。比如 hook ViewController 的 viewWillLayoutSubviews 方法。

[aspectsController aspect_hookSelector:@selector(viewWillLayoutSubviews) withOptions:0 usingBlock:^{
            NSLog(@"Controller is layouting!");
        } error:NULL];

Aspects 使用的技术

在阅读 Aspects 源码之前需要一些 Runtime 的相应知识,可以参考我自己的一些博客。

  1. Objective-C 之 objc_msgSend 简单实现
  2. Objective-C 方法签名和调用
  3. Objective-C 动态实现
  4. Objective-C 消息转发
  5. Objective-C 方法混写

Aspects 的代码

/// 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;

接下来的源码解读,主要是分析 Aspects 的 实例方法的执行流程,以及 Aspects 的设计思路。至于 Aspects 的类方法的执行流程和思路也是大同小异,这里就不再累赘了。

/// @return A token which allows to later deregister the aspect.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error {
    return aspect_add(self, selector, options, block, error);
}

该方法返回一个 AspectToken 对象,这个对象主要是 aspect 的唯一标识符。 该方法调用了 static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) 方法,这个方法用于给一个实例添加 aspect 。

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {

   // ...... 省略代码    
    __block AspectIdentifier *identifier = nil;
    // 给 block 加锁
    aspect_performLocked(^{
        // 判断 selector 是否可以被 hook
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 创建一个 AspectsContainer 对象,用 selector 关联到实例对象
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // 创建一个 AspectIdentifier 对象,
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                // 把 AspectIdentifier 对象加入 AspectsContainer 对象中
                [aspectContainer addAspect:identifier withOptions:options];
                
                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}
  1. 给 block 加锁
  2. 判断 selector 是否符合 hook 的规则
  3. 创建一个 AspectsContainer 对象,用 selector 关联到实例对象。用于管理一个对象或者类的一个方法的所有 aspects
  4. 创建一个 AspectIdentifier 对象,并放入 AspectsContainer 对象管理。AspectIdentifier 对象 表示一个 aspect 的内容

细看 aspect_isSelectorAllowedAndTrack 方法的内容,看如何判断一个 selector 是否符合 hook 规则

// 判断 selector 是否能被 hook
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
    // 不能被 hook 的方法集合
    static NSSet *disallowedSelectorList;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        // 这些方法不能被 hook
        disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
    });
    
    // Check against the blacklist.
    // ...... 省略代码
    
    // Additional checks.
    AspectOptions position = options&AspectPositionFilter;
    // dealloc 方法不允许在执行之后被 hook,因为对象会被销毁
    if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
       // ...... 省略代码
    }
    // 被 hook 的方法不存在于类中
    if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
       // ...... 省略代码
    }
    
    // Search for the current class and the class hierarchy IF we are modifying a class object
    if (class_isMetaClass(object_getClass(self))) {
        Class klass = [self class];
        NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
        Class currentClass = [self class];
        
        AspectTracker *tracker = swizzledClassesDict[currentClass];
        // 判断子类是否已经 hook 该方法
        if ([tracker subclassHasHookedSelectorName:selectorName]) {
            // ...... 省略代码
        }
        
        do {
            // 判断是否已经 hook 了该方法
            tracker = swizzledClassesDict[currentClass];
            if ([tracker.selectorNames containsObject:selectorName]) {
                if (klass == currentClass) {
                    // Already modified and topmost!
                    return YES;
                }
                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)));
        
        // ...... 省略代码
    return YES;
}

selector 不允许被 hook 的判断规则

  1. @"retain", @"release", @"autorelease", @"forwardInvocation:" 这些方法是不允许被 hook 的
  2. dealloc 方法不允许在执行之后被 hook
  3. 被 hook 的方法不存在于类中
  4. 一个方法只能被 hook 一次

接下来看看 static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) 方法实现。

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    Class klass = aspect_hookClass(self, error);// 1  swizzling forwardInvocation
    // 被 hook 的 selector
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {//2  swizzling method
        // 使用一个 aliasSelector 来指向原来 selector 的方法实现
        // 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. swizzling forwardInvocation。
  2. 拿到原始 selector 的 方法实现,再生成一个 aliasSelector 来指向原来 selector 的方法实现
  3. 把 selector 指向 _objc_msgForward 函数,用 _objc_msgForward 函数指针代替 selector 的 IMP ,这样执行 selector 的时候就会执行 _objc_msgForward 函数。

接下来看看 static Class aspect_hookClass(NSObject *self, NSError **error) 的实现


static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);
    // 是否有 _Aspects_ 后缀
    // Already subclassed
    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);
    }
    
    // 动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation 方法
    // Default case. Create dynamic subclass.
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    Class subclass = objc_getClass(subclassName);
    
    if (subclass == nil) {
        // 生成 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 方法
        aspect_swizzleForwardInvocation(subclass);
        // 修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回当前对象的 class。
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }
    // 将当前对象 isa 指针指向了 subclass
    // 将当前 self 设置为子类,这里其实只是更改了 self 的 isa 指针而已
    object_setClass(self, subclass);
    return subclass;
}

该方法的作用是动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation 方法,这样做的好处是不需要去更改对象本身的实例。该方法调用了static void aspect_swizzleForwardInvocation(Class klass) 方法对子类的 forwardInvocation: 方法进行混写;

接下来看看 static void aspect_swizzleForwardInvocation(Class klass) 的方法实现,看它如何实现对 forwardInvocation: 方法的混写

//swizzling forwardinvation 方法
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);
    // If there is no method, replace will act like class_addMethod.
    // 使用 __ASPECTS_ARE_BEING_CALLED__ 替换子类的 forwardInvocation 方法实现
    // 由于子类本身并没有实现 forwardInvocation ,
    // 所以返回的 originalImplementation 将为空值,所以子类也不会生成 AspectsForwardInvocationSelectorName 这个方法
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        NSLog(@"class_addMethod");
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}

关键实现在在这句代码,将 forwardInvocation: 的实现换成 ASPECTS_ARE_BEING_CALLED实现

IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");

到这里我们可以知道了,知道 hook 了一个方法,那么最后都会执行 ASPECTS_ARE_BEING_CALLED 这个方法,代码执行到这里基本就到末尾了。我们看看这个方法实现

// This is the swizzled forwardInvocation: method.
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);
    }else {
        // 重新转回原来的 selector 所指向的函数
        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)
    // 找不到 aliasSelector 的方法实现,也就是没有找到被 hook 的 selector 的原始方法实现,那么进行消息转发
    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)];
}

  1. 根据切面的时机点,执行相应的代码
  2. 执行完额外的代码之后,看下是否需要回到原来的代码上,若是需要则执行原来的代码
  3. 若是没有找到被 hook 的 selector 的原始方法实现,那么进行消息转发
  4. Aspects 做对应的清理工作

Aspects 的思路

  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 的原始方法实现上继续执行。

参考

  1. https://github.com/steipete/Aspects
  2. https://wereadteam.github.io/2016/06/30/Aspects/
  3. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html
  4. Objective-C 之 objc_msgSend 简单实现
  5. Objective-C 方法签名和调用
  6. Objective-C 动态实现
  7. Objective-C 消息转发
  8. Objective-C 方法混写
  9. 代码注释 https://github.com/junbinchencn/Aspects
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容