Aspects深度解析-iOS面向切面編程

背景簡述

在平常開發過程當中是否有過這樣的需求:不修改原來的函數,可是又想在函數的執行先後插入一些代碼。這個方式就是面向切面(AOP),在iOS開發中比較知名的框架就是Aspects,而餓了麼新出的Stinger框架先不討論,Aspects的源碼精煉巧妙,很值得學習深究,本文主要從源碼和應用層面來介紹下git

源碼解析

先提出幾個問題

帶着問題去閱讀更容易理解github

  1. Aspects實現的核心原理是什麼
  2. 哪些方法不能被hook
  3. hook的操做是否能夠只對某個實例生效,對同一個類的其餘實例不生效?
  4. block是如何被存儲和調用的

基本原理

正常來說想實現AOP,能夠利用runtime的特性進行method swizzle,但Aspects就是造好的輪子,並且更好用,下面簡述下Aspects的基本原理安全

runtime的消息轉發機制

在OC中,全部的消息調用最後都會經過objc_msgSend()方法進行訪問bash

  1. 經過objc_msgSend()進行消息調用,爲了加快執行速度,這個方法在runtime源碼中是用匯編實現的
  2. 而後調用lookUpImpOrForward()方法,返回值是個IMP指針,若是查找到了調用函數的IMP,則進行方法的訪問
  3. 若是沒有查到對於方法的IMP指針,則進行消息轉發機制
  4. 第一層轉發:會調用resolveInstanceMethod:、resolveClassMethod:,此次轉發是方法級別的,開發者能夠動態添加方法進行補救
  5. 第二層轉發:若是第一層轉發返回NO,則會進行第二層轉發,調用forwardingTargetForSelector:,能夠把調用轉發到另外一個對象,這是類級別的轉發,調用另外一個類的相同的方法
  6. 第三層轉發:若是第二層轉發返回nil,則會進入這一層處理,這層會調用methodSignatureForSelector:、forwardInvocation:,此次是完整的消息轉發,由於你能夠返回方法簽名、動態指定調用方法的Target
  7. 若是轉發都失敗,就會crash

Aspects的基本原理

對外暴露的核心API框架

/**
做用域:針對全部對象生效
selector: 須要hook的方法
options:是個枚舉,主要定義了切面的時機(調用前、替換、調用後)
block: 須要在selector先後插入執行的代碼塊
error: 錯誤信息
*/
+ (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;

複製代碼

上面介紹了消息的轉發機制,而Aspects就是利用了消息轉發機制,經過hook第三層的轉發方法forwardInvocation:,而後根據切面的時機來動態調用block。接下來詳細分析巧妙的設計ide

  1. 類A的方法m被添加切面方法
  2. 建立一個類A的子類B,並hook子類B的forwardInvocation:方法攔截消息轉發,使forwardInvocation:IMP指向事先準備好的__ASPECTS_ARE_BEING_CALLED__函數(後面簡稱ABC函數),block方法的執行就在ABC函數中
  3. 把類A的對象的isa指針指向B,這樣就把消息的處理轉發到類B上,相似KVO的機制,同時會更改class方法的IMP,把它指向類A的class方法,當外界調用class時獲取的仍是類A,並不知道中間類B的存在
  4. 對於方法m,類B會直接把方法m的IMP指向_objc_msgForward()方法,這樣當調用方法m時就會走消息轉發流程,觸發ABC函數

詳細分析

執行入口

- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add(self, selector, options, block, error);
}

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    // 添加自旋鎖,block內容的執行時互斥的
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 獲取容器,容器的對象以關聯對象的方式添加到了當前對象上,key值爲`前綴+selector`
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // 建立標識符,用來存儲SEL、block、切面時機(調用前、調用後)等信息
            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_add(self, selector, options, block, error)方法,這個方法時線程安全的,接下來一步步解析具體作了什麼函數

過濾攔截:aspect_isSelectorAllowedAndTrack()

精簡版的源碼,已經添加了註釋學習

static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
    static NSSet *disallowedSelectorList;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{ // 初始化黑名單列表,有些方法時禁止hook的
        disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
    });

    // 第一步:檢查是否在黑名單內
    NSString *selectorName = NSStringFromSelector(selector);
    if ([disallowedSelectorList containsObject:selectorName]) {
        ...
        return NO;
    }

    // 第二步: dealloc方法只能在調用前插入
    AspectOptions position = options&AspectPositionFilter;
    if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
        ...
        return NO;
    }
    // 第三步:檢查類是否存在這個方法
    if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
        ...
        return NO;
    }

    // 第四步:若是是類而非實例(這個是類,不是類方法,是指hook的做用域對全部對象都生效),則在整個類即繼承鏈中,同一個方法只能被hook一次,即對於全部實例對象都生效的操做,整個繼承鏈中只能被hook一次
    if (class_isMetaClass(object_getClass(self))) {
        ...
    } else {
        return YES;
    }
    return YES;
}

複製代碼
  1. 不容許hookretainreleaseautoreleaseforwardInvocation:,這些很少解釋
  2. 容許hookdealloc,可是隻能在dealloc執行前,這都是爲了程序的安全性設置的
  3. 檢查這個方法是否存在,不存在則不能hook
  4. Aspects對於hook的生效做用域作了區分:全部實例對象&某個具體實例對象。對於全部實例對象在整個繼承鏈中,同一個方法只能被hook一次,這麼作的目的是爲了規避循環調用的問題(詳情能夠了解下supper關鍵字)

關鍵類結構

AspectOptions

是個枚舉,用來定義切面的時機,即原有方法調用前、調用後、替換原有方法、只執行一次(調用完就刪除切面邏輯)ui

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// 原有方法調用前執行 (default)
    AspectPositionInstead = 1,            /// 替換原有方法
    AspectPositionBefore  = 2,            /// 原有方法調用後執行
    
    AspectOptionAutomaticRemoval = 1 << 3 /// 執行完以後就恢復切面操做,即撤銷hook
};

複製代碼

AspectIdentifier類

簡單理解話就是一個存儲model,主要用來存儲hook方法的相關信息,如原有方法、切面block、切面時機等atom

@interface AspectIdentifier : NSObject
...其餘省略
@property (nonatomic, assign) SEL selector; // 原來方法的SEL
@property (nonatomic, strong) id block; // 保存要執行的切面block,即原方法執行先後要調用的方法
@property (nonatomic, strong) NSMethodSignature *blockSignature; // block的方法簽名
@property (nonatomic, weak) id object; // target,即保存當前對象
@property (nonatomic, assign) AspectOptions options; // 是個枚舉,表示切面執行時機,上面已經有介紹
@end

複製代碼

AspectsContainer類

容器類,以關聯對象的形式存儲在當前類或對象中,主要用來存儲當前類或對象全部的切面信息

@interface AspectsContainer : NSObject
...其餘省略
@property (atomic, copy) NSArray <AspectIdentifier *>*beforeAspects; // 存儲原方法調用前要執行的操做
@property (atomic, copy) NSArray <AspectIdentifier *>*insteadAspects;// 存儲替換原方法的操做
@property (atomic, copy) NSArray <AspectIdentifier *>*afterAspects;// 存儲原方法調用後要執行的操做
@end

複製代碼

存儲切面信息

存儲切面信息主要用到了上面介紹的AspectsContainerAspectIdentifier這兩個類,主要操做以下(註釋寫的已經很詳細)

  1. 獲取當前類的容器對象aspectContainer,若是沒有則建立一個
  2. 建立一個標識符對象identifier,用來存儲原方法信息、block、切面時機等信息
  3. 把標識符對象identifier添加到容器中
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    ...
    // 獲取容器對象,主要用來存儲當前類或對象全部的切面信息,容器的對象以關聯對象的方式添加到了當前對象上,key值爲`前綴+selector`
    AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
    // 建立標識符,用來存儲SEL、block、切面時機(調用前、調用後)等信息
    identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
    if (identifier) {
        // 把identifier添加到容器中
        [aspectContainer addAspect:identifier withOptions:options];
        ...
    }
    return identifier;
}
複製代碼

建立中間類

這一步的操做相似kvo的機制,隱式的建立一箇中間類,一:能夠作到hook只對單一對象有效,二:避免了對原有類的侵入

這一步主要作了幾個操做

  1. 若是已經存在中間類,則直接返回
  2. 若是是類對象,則不用建立中間類,並把這個類存儲在swizzledClasses集合中,標記這個類已經被hook了
  3. 若是存在kvo的狀況,那麼系統已經幫咱們建立好了中間類,那就直接使用
  4. 對於不存在kvo且是實例對象的,則單首創建一個繼承當前類的中間類midcls,並hook它的forwardInvocation:方法,並把當前對象的isa指針指向midcls,這樣就作到了hook操做只針對當前對象有效,由於其餘對象的isa指針指向的仍是原有類
static Class aspect_hookClass(NSObject *self, NSError **error) {
	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);
        }else if (statedClass != baseClass) {
        // Probably a KVO class. Swizzle in place. Also swizzle meta classes in place.
        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);
            // hook forwardInvocation方法
	    aspect_swizzleForwardInvocation(subclass);
            // hook class方法,把子類的class方法的IMP指向父類,這樣外界並不知道內部建立了子類
	    aspect_hookedGetClass(subclass, statedClass);
	    aspect_hookedGetClass(object_getClass(subclass), statedClass);
	    objc_registerClassPair(subclass);
	}
    // 把當前對象的isa指向子類,相似kvo的用法
	object_setClass(self, subclass);
	return subclass;
}
複製代碼

替換forwardInvocation:方法

從下面的代碼能夠看到,主要功能就是把當前類的forwardInvocation:替換成__ASPECTS_ARE_BEING_CALLED__,這樣當觸發消息轉發的時候,就會調用__ASPECTS_ARE_BEING_CALLED__方法

對於__ASPECTS_ARE_BEING_CALLED__方法是Aspects的核心操做,主要就是作消息的調用和分發,控制方法的調用的時機,下面會詳細介紹

// hook forwardInvocation方法,用來攔截消息的發送
static void aspect_swizzleForwardInvocation(Class klass) {
    // If there is no method, replace will act like class_addMethod.
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
複製代碼

自動觸發消息轉發機制

Aspects的核心原理是消息轉發,那麼必要出的就是怎麼自動觸發消息轉發機制

runtime中有個方法_objc_msgForward,直接調用能夠觸發消息轉發機制,著名的JSPatch框架也是利用了這個機制

假如要hook的方法叫m1,那麼把m1IMP指向_objc_msgForward,這樣當調用方法m1時就自動觸發消息轉發機制了,詳細實現以下

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {

    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        ...
        // We use forwardInvocation to hook in. 把函數的調用直接觸發轉發函數,轉發函數已經被hook,因此在轉發函數時進行block的調用
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }
}
複製代碼

核心轉發函數處理

上面一切準備就緒,那麼怎麼觸發以前添加的切面block呢,首先咱們梳理下整個流程

  1. 方法m1IMP指向了_objc_msgForward,調用m1則會自動觸發消息轉發機制
  2. 替換forwardInvocation:,把它的IMP指向__ASPECTS_ARE_BEING_CALLED__方法,消息轉發時觸發的就是__ASPECTS_ARE_BEING_CALLED__

上面操做能夠直接看出調用方法m1則會直接觸發__ASPECTS_ARE_BEING_CALLED__方法,而__ASPECTS_ARE_BEING_CALLED__方法就是處理切面block用和原有函數的調用時機,詳細看下面實現步驟

  1. 根據調用的selector,獲取容器對象AspectsContainer,這裏面存儲了這個類或對象的全部切面信息
  2. AspectInfo會存儲當前的參數信息,用於傳遞
  3. 首先觸發函數調用前的block,存儲在容器的beforeAspects對象中
  4. 接下來若是存在替換原有函數的block,即insteadAspects不爲空,則觸發它,若是不存在則調用原來的函數
  5. 觸發函數調用後的block,存在在容器的afterAspects對象中
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation: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 {
        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);

    ...
    // Remove any hooks that are queued for deregistration.
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}

複製代碼

總結

Aspects的核心原理是利用了消息轉發機制,經過替換消息轉發方法來實現切面的分發調用,這個思想很巧妙並且應用很普遍,不少三方庫都利用了這個原理,值得學習

目前這個庫已經很長時間沒有維護了,原子操做的支持使用的仍是自旋鎖,目前這種鎖已經不安全了

另外使用這個庫是須要注意相似原理的其餘框架,可能會有衝突,如JSPatch,不過JSPatch已經被封殺了,但相似需求有不少

相關文章
相關標籤/搜索