iOS AOP 框架 - Aspects 原始碼解讀

要上班的斌哥發表於2017-12-12

Aspects 是什麼?

Aspects 是 iOS 上的一個輕量級 AOP 庫。它利用 method swizzling 技術為已有的類或者例項方法新增額外的程式碼,它是著名框架 PSPDFKit (an iOS PDF framework that ships with apps like Dropbox or Evernote)的一部分。

怎麼使用 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

相關文章