從 Aspects 來看 Runtime

用 OC 開發(fā) iOS 應(yīng)用的人都知道 OC 是一門動態(tài)語言。我們能夠在運行時通過 Runtime API 獲取并替換任意方法。所以,在 AOP 和 熱更新的實現(xiàn)上都經(jīng)??梢钥吹?Runtime 的身影。這雖然看似美好,但運用不當?shù)脑捦ǔ斐芍卮?bug。Runtime 對于 OC 來說就像是魔力強大但又十分危險的黑魔法。

黑魔法.jpg

1 消息轉(zhuǎn)發(fā)

iOS 的方法執(zhí)行是通過 SEL 找到 對應(yīng)的 IMP,然后執(zhí)行相應(yīng)的函數(shù)。如果 SEL 找不到對應(yīng)的 IMP 就會啟動消息轉(zhuǎn)發(fā)機制。消息轉(zhuǎn)發(fā)會經(jīng)過三個階段。

  • 第一階段:在類的內(nèi)部進行處理

      //處理實例方法
      + (BOOL)resolveInstanceMethod:(SEL)sel 
      //處理類方法
      + (BOOL)resolveClassMethod:(SEL)sel
    

    重寫該方法可以對相應(yīng)的 SEL 進行處理,返回 YES 說明處理完成,不進行下一步轉(zhuǎn)發(fā)。

  • 第二階段:尋找備援接收者

      - (id)forwardingTargetForSelector:(SEL)aSelector
    

    該方法會返回一個能夠處理改消息的備援接收者。如果返回 nil 則繼續(xù)下一步。

  • 第三階段:全局處理

      - (void)forwardInvocation:(NSInvocation *)invocation
    

    將消息包裝成一個 NSInvocation 對象,讓系統(tǒng)對其進行處理。如果這里還是沒有進行,則會拋出相應(yīng)的異常。

整個流程如下圖所示:

消息轉(zhuǎn)發(fā).jpg

2 Aspects

Aspects 通過 swizzling method 和 swizzling isa 實現(xiàn)對原方法的 hook ,以此來幫助使用者完成 AOP 編程。

2.1 Aspect 主要主要類介紹:

@interface AspectInfo : NSObject <AspectInfo>
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end

AspectInfo:aspect 的信息,被 hook 方法的 IMP 信息存放在 NSInvocation 中。

@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

AspectIdentifier:一個 aspect 的具體信息,相應(yīng)的 sel,options,block

@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

AspectsContainer:本類所有的 aspect 的容器

@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, readonly) NSString *trackedClassName;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;
- (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName;
- (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName;
@end

AspectTracker:用來追蹤 aspect 的相關(guān)信息

2.2 Aspects API調(diào)用之后發(fā)生的事情

下面是 aspect 相關(guān)方法調(diào)用之后,主要的執(zhí)行邏輯

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

    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            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;
}

aspect 方法入口,先是斷言判斷傳入的參數(shù)。然后 aspect_performLocked 加鎖同步操作,通過 aspect_isSelectorAllowedAndTrack 判斷該方法是否允許 hook。判斷邏輯如下:

  1. 實例方法的"retain", "release", "autorelease", "forwardInvocation:" 不允許 hook , "dealloc" 方法只能是 AspectPositionBefore 的方式操作
  2. 類方法要判斷父類,子類中沒有 hook 過該方法。

如果可以 hook,則根據(jù)參數(shù)生成一個 AspectIdentifier 對象,之后放在相應(yīng)的 AspectsContainer 容器中,然后調(diào)用 aspect_prepareClassAndHookSelector 進行 class swizzling 和 method swizzling。

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    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));
    }
}

調(diào)用 aspect_hookClass 進行相應(yīng)的 class swizzling 操作。

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);

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

    // Default case. Create dynamic subclass.
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    Class subclass = objc_getClass(subclassName);

    if (subclass == nil) {
        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;
        }

        aspect_swizzleForwardInvocation(subclass);
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }

    object_setClass(self, subclass);
    return subclass;
}

class swizzling 的主要邏輯如下:

  1. 若 class 是元類,swizzling 相應(yīng)的 ForwardInvocation 方法
  2. 若 class 是普通的類, 先判斷是否是 KVO 對象,是的話 swizzling 相應(yīng)的 ForwardInvocation 方法
  3. 若不是 KVO 對象的話,先創(chuàng)建一個子類,并 swizzling 子類的 ForwardInvocation,同時將本類的 isa 指針,指向創(chuàng)建的子類。

hook 過的 class 都保存在 swizzledClasses 對象中。接下去進行 method swizzling, 調(diào)用 aspect_isMsgForwardIMP 確認實現(xiàn)函數(shù)不是消息轉(zhuǎn)發(fā)相關(guān)的。然后讓 aliasSelector 指向 targetMethodIMP,本來的 OriginalSel 指向 msgForwardIMP。至此設(shè)置階段就完成了。

2.3 被 Hook 的方法被調(diào)用之后發(fā)生的事情

在調(diào)用 aspect 的 api 進行相關(guān)設(shè)置之后,程序執(zhí)行到剛被 hook 過的方法就會進入 aspect 的執(zhí)行邏輯。

由于上面的設(shè)置,我們知道調(diào)用實例方法的類的 isa 會指向了一個子類,然后子類的 OriginalSel 指向了 msgForwardIMP, 而子類相應(yīng)的 ForwardInvocation 也被替換了,指向了 __ASPECTS_ARE_BEING_CALLED__,所以,一旦調(diào)用相應(yīng)的方法就會進入 __ASPECTS_ARE_BEING_CALLED__ 里面。

如果是類方法,則是 OriginalSel 指向了 msgForwardIMP ,本身的 ForwardInvocation 被替換指向了__ASPECTS_ARE_BEING_CALLED__ ,所以一樣進入到 __ASPECTS_ARE_BEING_CALLED__ 方法里。

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)];
}

__ASPECTS_ARE_BEING_CALLED__ 內(nèi)部的邏輯是:根據(jù) aliasSelector 獲取之前存儲的類或者元類的 AspectsContainer 對象,之后根據(jù) AspectOptions 在適當?shù)臅r機獲取 AspectsContainer 中對應(yīng)的所有 AspectIdentifier 并執(zhí)行相應(yīng)的 block 。
同時處理被 hook 方法原來的實現(xiàn)( IMP ) , 被封裝在 originalInvocation 中(若有 AspectIdentifier 的 AspectOptions 是 insteadAspects ,則 originalInvocation 將不會被執(zhí)行)。若 AspectIdentifier 的 AspectOptions 是 AspectOptionAutomaticRemoval ,則該 AspectIdentifier 會被存入 aspectsToRemove 中,執(zhí)行結(jié)束之后會被移除。

3 總結(jié)

以上就是本人對于 Aspects 這個強大的 AOP 庫的一個大致理解。同時這里面還有很多細節(jié)很值得學習。

4 參考文章

面向切面編程之 Aspects 源碼解析及應(yīng)用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容