面向切面編程:Aspects源碼解析

面向切面編程

所謂的面向切面編程(AOP),原理就是在不更改正常業務的流程的前提下,經過一個動態代理類,實現對目標對象嵌入的附加的操做。ios

簡單說,就是在不影響咱們如今正常業務的狀況下,對某些類的某些方法嵌入操做。咱們能夠很通俗的理解一個方法能夠有方法前和方法後這兩個切面,固然還能夠把方法執行過程看過一個整的切面去hook。git

在咱們的iOS開發中,AOP的實現方法就是使用Runtime的Swizzling Method改變selector指向的實現,在新的實現中添加新的操做,執行完新實現以後,再處理以前的實現邏輯。github

Aspects

Aspects是iOS平臺比較成熟的AOP的框架,此次咱們主要來研究一下這個庫的源碼。編程

基於Aspects 1.4.1版本。安全

總覽

Aspects給出了兩個方法,一個類方法一個實例方法,使用起來很是簡單。框架

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;


- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

傳參也很容易理解,selector天然就是咱們要hook的方法,options使咱們要hook的位置,下面具體再說,block是一個回調,也就是咱們所說的要嵌入的代碼邏輯,error就是hook失敗,固然了失敗的緣由較多,咱們下面會提到。ide

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.
};

options也是一個枚舉的類型,看一下里面定義的字段就很容易明白了,AspectPositionAfter是表示嵌入的放大要在被hook方法原來邏輯以前以後執行,AspectPositionBefore是以前執行,AspectPositionInstead表示要用嵌入的代碼替換掉以前的邏輯,AspectOptionAutomaticRemoval表示hook執行後,移除hook。函數

由於是NS_OPTIONS類型,可多選。源碼分析

重要的類

除了上述核心的方法是經過NSObject的Category的方式給出,還有如下幾個類比較重要。.net

AspectsContainer: 一個對象或者類的全部的Aspects總體狀況

AspectIdentifier: 一個Aspects的具體內容,這裏主要包含了單個的 aspect 的具體信息,包括執行時機,要執行 block 所須要用到的具體信息:包括方法簽名、參數等等

AspectInfo: 一個 Aspect 執行環境,主要是 NSInvocation 信息。

核心方法aspect_add

Aspects給出的兩個方法最終都是調用了aspect_add

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(^{
        // 是否容許hook -
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            //取到sel對應的container (一個類不一樣sel對應不一樣的container?)
            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;
}
__block AspectIdentifier *identifier = nil;

每次在添加hook的時候,都會先建立一個AspectIdentifier。
__block是爲了能在下面的block中修改identifier。

aspect_performLocked是封裝了一個自旋鎖。

在自旋鎖中會有一個if語句來判斷selector是否能被hook。那咱們就先來看一下是否能被hook的判斷方法aspect_isSelectorAllowedAndTrack

aspect_isSelectorAllowedAndTrack
  1. 黑名單

    aspect_isSelectorAllowedAndTrack方法中維護了一個NSSet,在初始化的時候加入了一些方法名,在源碼中是下面這些。

    [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];

    這說明了後面這幾個方法是不容許被hook的,若是hook了這些方法會有錯誤的信息提示。

  2. dealloc方法

    在hook的方法中,dealloc屬於一個特殊狀況,由於這個方法是在對象要被銷燬的時候建立,因此Aspects爲了安全起見,在hook dealloc方法的時候。options只容許時AspectPositionBefore,也就是插入的邏輯只能在dealloc原有邏輯以前處理,不容許替換或者在dealloc以後。

  3. 沒找到方法

    這個就沒啥疑問了,若是咱們hook了一個根本不存在的方法也會有錯誤提示。

  4. 方法只容許hook一次(元類相關)

    這個有點麻煩,由於從錯誤提示的枚舉來看,他是對應AspectErrorSelectorAlreadyHookedInClassHierarchy這一項的,從字面意思來看是說方法已經被hook了。

    最開始我對這個類的層級不是很明白,個人最初理解的類的層級是父類和子類不能同時hook,通過驗證這種理解是錯誤的。

    後來我仔細地看了一下源碼,他的元類判斷中是這麼寫的:

    if (class_isMetaClass(object_getClass(self))) {}

    判斷的是object_getClass(self),經過runtime的源碼咱們能夠知道,object_getClass獲得的是傳參的isa指針指向的結構,意識就是self若是是對象,object_getClass(self)獲得的是對應的類,若是self是類,那就獲得了元類。

    那何時會提示這個錯誤呢,我舉一個例子吧。我建立了一個TestHookViewController類,繼承自UIViewController,若是我在TestHookViewController中像下面這樣寫就會有錯誤提示了。

    [[UIViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) {
        NSLog(@"%s",__func__);
    } error:NULL];
    
    [[TestHookViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) {
        NSLog(@"%s",__func__);
    } error:NULL];

    固然了,這兩個hook的先後位置不一樣,打印臺輸出的提示也是不同的,雖然都是一個類層級方法只容許hook一次的錯誤緣由。這個你們自行嘗試一下。

    if語句裏面就是關於方法是否重複hook的判斷邏輯,這裏牽扯到一個相關類,AspectTracker。咱們如今就來看一下這個類。

AspectTracker類

雖然是說AspectTracker類,可是代碼結構仍是接着上面的我們說到的位置繼續往下走。

由於AspectTracker主要就是用在方法只容許hook一次的判斷中。

Aspects維護了一個字典,來儲存被hook方法的類和對應的AspectTracker。

Class currentClass = [self class];
AspectTracker *tracker = swizzledClassesDict[currentClass];

經過上面的方式取到對應的AspectTracker。這裏提一句,這裏的代碼咱們應該先看下面的,要先了解AspectTracker是怎麼存儲到字典裏面的。

這裏咱們要看下面這個do-while循環

currentClass = klass;
AspectTracker *subclassTracker = nil;
do {
    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];
    }

    // All superclasses get marked as having a subclass that is modified.
    subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));

若是最開始沒有tracker,會初始化一個,而後存到字典中。最開始的時候subclassTracker爲nil,因此selector會add到tracker.selectorNames。

而後currentClass從新賦值

currentClass = class_getSuperclass(currentClass)

再次執行do邏輯裏面的代碼,此次subclassTracker就有值了(上一次循環的tracker),就會執行[tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];

咱們進到addSubclassTracker源碼中能夠看到,tracker被存到selectorNamesToSubclassTrackers這個字典中,關鍵的是這個字典的key是selectorName,value是一個集合,tracker是放在這個集合裏面的。爲何要經過集合來存tracker呢?

由於這裏是有子類的狀況的,一個類的子類可能有多個,若是在不一樣的子類中hook了這個父類的一個方法,也就是父類中的這一個selector被屢次hook,因此也會有不一樣的tracker,因此使用一個集合來儲存。

其實說到這個地方你們就差很少能夠理解了,若是知足了
if (class_isMetaClass(object_getClass(self)))這個判斷,咱們會把這個類hook的方法經過封裝爲AspectTracker來進行記錄,固然包括他的層層父類,都對對應一個AspectTracker,並且父類中的還會記錄子類中hook的方法。這部分代碼最好是debug跟一下,會明顯一點。

上面咱們先看了tracker是怎樣被存起來的,接來下再來看關於只能被hook一次的判斷。

首先要判斷子類中是否hook

if ([tracker subclassHasHookedSelectorName:selectorName]) {
    //內部省略    
}

subclassHasHookedSelectorName內部實現很簡單

- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName {
    return self.selectorNamesToSubclassTrackers[selectorName] != nil;
}

就是查詢一下selectorNamesToSubclassTrackers這個字典中,經過seletorName是否能取到值,上面已經說過了,這個字典中經過key:selectorName value: set的方法儲存了子類的tracker。

若是能取到值,就說明子類中已經hook了這個方法了,父類中就不能在hook。

若是沒能取到值,說明當前類可能就是個子類,此時須要看一下他的父類中是否hook了這個selector,因此就會執行下面的的do-while循環。 此處的代碼就不展現了。

但這個位置初步的關於方法可否被hook就已經判斷完了。若是能夠seletor能夠被hook,繼續if裏面的代碼。

Swizzling Method

咱們直接說Swizzling Method這一最重要的邏輯吧。

Swizzling Method主要有兩部分,一個是對對象的 forwardInvocation 進行 swizzling,另外一個是對傳入的 selector 進行 swizzling。

咱們來看aspect_prepareClassAndHookSelector方法的源碼。

替換forwardInvocation

首先是Class klass = aspect_hookClass(self, error);

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

 //
 // 省略一部分代碼
 //
 //
    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;
        }
        //替換forwardInvocation方法
        aspect_swizzleForwardInvocation(subclass);
        // subclass的class方法交替換  替換爲statedClass的class方法   subclass元類也替換
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }
    // 改變當前類的isa指針指向
    object_setClass(self, subclass);
    return subclass;
}

咱們從源碼中能夠看出邏輯,主要是動態建立了一個subclass,名爲subclass,其實最終把咱們hook的類的isa指針指向了這個subclass,實爲是一個父類。

aspect_swizzleForwardInvocation(subclass);

上面這個方法是替換了forwardInvocation:方法。

固然了,也是替換的這個subclass的forwardInvocation:方法,把forwardInvocation:替換爲__ASPECTS_ARE_BEING_CALLED__這個方法,主要的hook後的代碼執行處理邏輯都在這個__ASPECTS_ARE_BEING_CALLED__中。

在替換了aspect_hookClass方法以後,同時修改了 subclass以及其subclass metaclass的class方法,使他返回當前對象的class。這個地方有點繞,其實最終目的就是把全部的swizzling都放到了這個subclass中處理,不影響原來的類,並且對於外部的使用者,又能夠把它當作原對象使用。

替換selector

執行完aspect_hookClass完以後,forwardInvocation:方法已經被替換,下面會執行swizzling selector 的代碼。

在swizzling selector的時候,將selector指向了消息轉發IMP,同時生成一個aliasSelector,指向原方法的IMP。

這裏代碼就不往外粘了。

處理forwardInvocation

其實上面已經把整個過程分析完了,咱們也知道,最後轉發的代碼最終會在__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];  //根據aliasSelector找到以前的邏輯 執行
                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)];
}

從源碼中很容易看出來,分別處理不一樣的hook點,而後中間有根據
aliasSelector找到以前方法的實現,而後執行。

小結

初步的源碼分析就是這個樣子,沒有太關注一些細節,也存在一些本身如今還不是很熟悉的處理方式,畢竟涉及到太多的swizzling,消息轉發一類的方法,這一塊的只是須要後期在多研究runtime來提升。

代碼中有一些其餘的比較小的方法沒有講到,你們本身自行看一下。

參考

https://wereadteam.github.io/...

http://blog.ypli.xyz/ios/aop-...

https://blog.csdn.net/weixin_...

相關文章
相關標籤/搜索