1. 程式人生 > 其它 >Aspects框架的原始碼解讀及問題解析

Aspects框架的原始碼解讀及問題解析

前言

在iOS日常開發中,對某些方法進行hook是很常見的操作。最常見的是使用Category在+load中進行方法swizzle,它是針對類的,會改變這個類所有例項的行為。但是有時候我們只想針對單個例項進行hook,這種方法就顯得無力了。而Aspects框架可以搞定這個問題。 它的原理是通過Runtime動態的建立子類,把例項的isa指標指向新建立的子類,然後在子類中對hook的方法進行處理,這樣就支援了對單個例項的hook。Aspects框架支援對類和例項的hook,API很易用,可以方便的讓你在任何地方進行hook,是執行緒安全的。但是Aspects框架也有一些缺陷,一不小心就會掉坑裡面,我會通過原始碼解析進行說明。

原始碼解析

我主要使用圖示對Aspects的原始碼進行說明,建議參考原始碼一起檢視。要看懂這些內容,需要對isa指標訊息轉發機制runtime有一定的瞭解,本文中不會對這些內容展開來講,因為要把這些東西講清楚,每一項都需要單獨寫一篇文章了。

主要流程解析

  1. 它第一個流程是使用關聯物件新增Container,在這個過程中會進行一些前置條件的判斷,例如這個方法是否支援被hook等,如果條件驗證通過,就會把這次hook的資訊儲存起來,在方法呼叫的時候,查詢出來使用。
  2. 第二個流程是動態建立子類,如果是針對類的hook,則不會走這一步。
  3. 第三步是替換這個類的forwardInvocation:方法為__ASPECTS_ARE_BEING_CALLED__
    ,這個方法內部會查詢到之前建立的Container,然後根據Container中的邏輯進行實際的呼叫。
  4. 第四步是將原有方法的IMP改為_objc_msgForward,改完後當呼叫原有方法時,就會呼叫_objc_msgForward,從而觸發forwardInvocation:方法。

我對它的流程做了一個簡化的圖示,標有每個流程的序號,後面會對每個流程進行解析。流程如下:

圖示中的取出物件型別,是指的呼叫hook的物件的型別,如果是例項物件,那麼就走路徑;如果是物件,則走元類路徑;如果是kvo等實際型別不一致的情況,則走其它子類路徑。

①新增Container流程

這個流程中,把hook的邏輯封裝成Container,並使用關聯物件進行儲存。這個過程中會判斷hook的方法是否被支援、判斷被hook類的繼承關係、驗證回撥block正確性等操作。具體圖示如下:

關鍵程式碼如下:

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    ...
    aspect_performLocked(^{ // 加鎖
        // hook前置條件判斷
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 用selector作key,通過關聯物件獲得Container物件。
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // 內部會判斷block與hook的selector是否匹配,不匹配返回nil。
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                // 新增identifier,包含了hook的型別和回撥。 
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}

static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
    static NSSet *disallowedSelectorList;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
    });

    // 這裡對不支援hook的方法進行過濾
    NSString *selectorName = NSStringFromSelector(selector);
    if ([disallowedSelectorList containsObject:selectorName]) {
        NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
        AspectError(AspectErrorSelectorBlacklisted, errorDescription);
        return NO;
    }

    // dealloc只支援AspectPositionBefore型別下呼叫
    AspectOptions position = options&AspectPositionFilter;
    if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
        NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc.";
        AspectError(AspectErrorSelectorDeallocPosition, errorDesc);
        return NO;
    }

    // 判斷是否存在這個方法
    if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
        NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName];
        AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
        return NO;
    }

    // 這裡禁止有繼承關係的類hook同一個方法,程式碼量較多,不是關鍵內容,這裡不貼出
    if (class_isMetaClass(object_getClass(self))) {
        ...
    }

    return YES;
}

/// AspectsContainer內部新增AspectIdentifier的實現。
/// 這裡可以看出對同一個方法的多次hook都會被呼叫,不會出現後面hook的覆蓋前面的情況。
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
    NSParameterAssert(aspect);
    NSUInteger position = options&AspectPositionFilter;
    switch (position) {
        case AspectPositionBefore:  self.beforeAspects  = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break;
        case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break;
        case AspectPositionAfter:   self.afterAspects   = [(self.afterAspects  ?:@[]) arrayByAddingObject:aspect]; break;
    }
}

複製程式碼
  1. 從原始碼中可以看到,不支援的hook方法有[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];。其中retain,release,autorelease在arc下是被禁用的,框架本身是hookforwardInvocation:進行實現的,所以對它的hook也不支援。
  2. dealloc只支援AspectPositionBefore型別,使用AspectPositionInstead會導致系統預設的dealloc操作被替換無法執行而出現問題。AspectPositionAfter型別,呼叫時物件可能已經已經被釋放了,從而引發野指標錯誤。
  3. Aspects禁止有繼承關係的類hook同一個方法,具體可以參見它的一個issue,它報告了這樣操作會導致死迴圈,我會在文章後面再進行說明。
  4. Aspects使用block進行hook的呼叫,涉及到方法引數的傳遞和返回值問題,所以其中會對block進行校驗。

②runtime建立子類

iOS中的KVO就是通過runtime動態建立子類,然後在子類中重寫對應的setter方法來實現的,Aspects支援對單個例項的hook原理與此有一些類似。圖示如下:具體說明請檢視原始碼中的註釋

// 執行hook
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    // 針對例項型別,會通過runtime動態建立子類。類型別則直接hook。
    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);

    // 已經被hook過的類,直接返回
	if ([className hasSuffix:AspectsSubclassSuffix]) {
		return baseClass;

    // 是元類(MetaClass),則代表是對類進行hook。(非單個例項)
	}else if (class_isMetaClass(baseClass)) {
        // 內部是將類的forwardInvocation:方法替換為__ASPECTS_ARE_BEING_CALLED__
        return aspect_swizzleClassInPlace((Class)self);
    // 可能是一個KVO物件等情況,傳入實際的型別進行hook。
    }else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // 單個例項的情況,動態建立子類進行hook.
	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;
        }
        // 內部是將類的forwardInvocation:方法替換為__ASPECTS_ARE_BEING_CALLED__
		aspect_swizzleForwardInvocation(subclass);
        // 重寫class方法,返回之前的型別,而不是新建立的子類。避免hook後,型別判斷出現問題。
		aspect_hookedGetClass(subclass, statedClass);
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
		objc_registerClassPair(subclass);
	}

	object_setClass(self, subclass);
	return subclass;
}

複製程式碼

③替換forwardInvocation:

這部分就是把原有的forwardInvocation:替換為自定義的實現:__ASPECTS_ARE_BEING_CALLED__。原始碼如下:

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.
    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));
}
複製程式碼

替換後的對應關係圖示如下:

④hook方法交換IMP:

圖示如下:

第③步和第④步可能有些同學會感到疑惑,為什麼要替換forwardInvocation以及為什麼要將hook的方法的IMP替換為_objc_msgForward,這個和iOS的訊息轉發機制有關,可以自行查詢相關資料,這裡就不做說明了。需要注意的是有些框架也是通過iOS的訊息傳送機制來做一些操作,例如JSPatch,使用的時候需要注意,避免發生衝突。

被hook方法的呼叫流程

當hook注入後,對hook方法進行呼叫時,呼叫流程就會發生變化。圖示如下:

從上述解析過程中,我們可以看到Aspects這個框架是設計的很巧妙的,從中可以看到非常多runtime知識的應用。但是作者並不推薦在實際專案中進行使用:

因為Apsects對類的底層進行了修改,這種修改是基礎方面的修改,需要考慮到各種場景和邊界問題,一旦某方面考慮不周,就會引發出一些未知問題。另外這個框架是有缺陷的,很久沒有進行更新了,我對它的已知問題點進行了總結,在下面進行說明。如果有未總結到位的,歡迎補充。

問題點

基於類的hooking,同一條繼承鏈條上的所有類,一個方法只能被hook一次,後hook的無效。

之前這樣會出現死迴圈,後面作者進行了修改,對這個行為進行了禁止並加了錯誤提示。詳見這個issue

@interface A : NSObject
- (void)foo;
@end

@implementation A
- (void)foo {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}
@end

@interface B : A @end

@implementation B
- (void)foo {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    [super foo]; // 導致死迴圈的程式碼
}
@end

int main(int argc, char *argv[]) {
    [B aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
        NSLog(@"before -[B foo]");
    }];
    [A aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
        NSLog(@"before -[A foo]");
    }];

    B *b = [[B alloc] init];
    [b foo]; // 呼叫後死迴圈
}
複製程式碼

我們都知道,super是從它的父類開始查詢方法,然後傳入self進行呼叫。 根據我們之前對原始碼的解析,在這裡呼叫[super foo]後會從父類查詢fooIMP,查到後發現父類的IMP已經被替換為_objc_msgForward,然後傳入self呼叫。 因為是傳入的self,所以實際會呼叫到它自身的forwardInvocation:,這樣就導致了死迴圈。

針對單個例項的hook,hook後使用kvo沒問題,使用kvo後hook會出現問題。

這裡通過程式碼進行說明,以Animal物件為例:

@interface Animal : NSObject
@property(strong, nonatomic) NSString * name;
@end

@implementation Animal
- (void)testKVO {
    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    self.name = @"Animal";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"observeValueForKeyPath keypath:%@ name:%@", keyPath, self.name);
}

- (void)dealloc {
    [self removeObserver:self forKeyPath:@"name"];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        [animal testKVO];
        // 這裡如果改為針對類進行hook,則不會存在問題,因為類hook修改的是Animal類,而例項hook修改的是NSKVONotifying_Animal類
        [animal aspect_hookSelector:@selector(setName:) 
                        withOptions:AspectPositionAfter 
                         usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
            NSLog(@"aspects hook setName");
        } error:nil];
        // 這裡會crash
        animal.name = @"ChangedAnimalName";
    }
}
複製程式碼

異常原因分析圖示如下:

上面是繼承鏈和方法呼叫流程的圖示,可以看出,_NSSetObjectValueAndNotify是被aspects__setName:呼叫的,_NSSetObjectValueAndNotify的內部實現邏輯是取呼叫它的selector,去父類查詢方法,即aspects__setName:方法,而Animal物件並沒有這個方法的實現,這就導致了crash。

與category的共存問題

先用aspects進行hook,再使用category進行hook,會導致crash。反之則沒有問題。樣例程式碼如下:

@interface Animal : NSObject
@property(strong, nonatomic) NSString * name;
@end

@implementation Animal
- (void)setName:(NSString *)name {
    NSLog(@"%s", __func__);
    _name = name;
}
@end

@interface Animal(hook)
+ (void)categoryHook;
@end

@implementation Animal(hook)
+ (void)categoryHook {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [super class];
        SEL originalSelector = @selector(setName:);
        SEL swizzledSelector = @selector(lx_setName:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)lx_setName:(NSString *)name {
    NSLog(@"%s", __func__);
    [self lx_setName:name];
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        [Animal aspect_hookSelector:@selector(setName:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
            NSLog(@"aspects hook setName");
        } error:nil];
        
        [Animal categoryHook];
        // 呼叫後crash:[Animal lx_setName:]: unrecognized selector sent to instance 0x100608dc0
        animal.name = @"ChangedAnimalName";
    }
}
複製程式碼

這個與__ASPECTS_ARE_BEING_CALLED__的內部邏輯有關,裡面會對呼叫的方法新增字首aspect__進行呼叫,以呼叫到原始的IMP,但是categoryhook後破壞了這個流程。圖示如下:

根據上述圖示,實際只有aspects__setName,沒有aspects__lx_setName,導致找不到方法而crash

基於類的hook,如果對同一個類同時hook類方法和例項方法,那麼後hook的方法呼叫時會crash。樣例程式碼如下:

@interface Animal : NSObject
- (void)testInstanceMethod;
+ (void)testClassMethod;
@end

@implementation Animal
- (void)testInstanceMethod {
    NSLog(@"%s", __func__);
}
+ (void)testClassMethod {
    NSLog(@"%s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        [Animal aspect_hookSelector:@selector(testInstanceMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
            NSLog(@"aspects hook testInstanceMethod");
        } error:nil];
        
        [object_getClass([Animal class]) aspect_hookSelector:@selector(testClassMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
            NSLog(@"aspects hook testClassMethod");
        } error:nil];
        
        [animal testInstanceMethod];
        // crash: "+[Animal testClassMethod]: unrecognized selector sent to class 0x1000114a0"
        [Animal testClassMethod];
    }
}
複製程式碼

這樣的呼叫在日常開發中非常正常,但是它會導致crash。它是由於aspect_swizzleClassInPlace方法中的邏輯缺陷導致的。

static Class aspect_swizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    // Animal類物件與Animal元類物件會得到同一個字串。
    NSString *className = NSStringFromClass(klass);
    NSLog(@"aspect_swizzleClassInPlace %@ %p", klass, object_getClass(klass));
    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        // 類物件和元類物件得到同一個className,這裡後加入的會被錯誤的過濾掉。
        if (![swizzledClasses containsObject:className]) {
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    });
    return klass;
}
複製程式碼

從上述程式碼可以看到,它的去重邏輯只是簡單的字串判斷,取Animal的元類名得到同一個字串Animal,導致後新增的被過濾,當呼叫後被hook的方法後,執行_objc_msgForward,因為後hook的aspect_swizzleForwardInvocation被過濾了沒有執行,所以找不到forwardInvocation:IMP,導致了crash。

_objc_msgForward會出現衝突的問題

內部是通過訊息轉發機制來實現的,使用時要注意,避免與其它使用_objc_msgForward或相關邏輯的框架發生衝突。

效能問題

hook後的方法,通過原有訊息機制找到IMP後,並不會直接呼叫。而是會進行訊息轉發進入到__ASPECTS_ARE_BEING_CALLED__方法,內部再通過key取出相應的Coantiner進行呼叫,相對於未hook之前,額外增加了呼叫成本。所以不建議對頻繁呼叫的方法和在專案中大量使用。

執行緒問題

框架內部為了保證執行緒安全,有進行加鎖,但是使用的是自旋鎖OSSpinLock,存線上程反轉的問題,在iOS10已經被標記為棄用。

對類方法的hook,需要使用object_getClass來獲取元類物件進行hook

這個不是框架問題,而是有些同學不知道如何對類方法進行hook,這裡進行說明。

@interface Animal : NSObject
+ (void)testClassMethod;
@end

// 需要通過object_getClass來獲取元類物件進行hook
[object_getClass(Animal) aspect_hookSelector:@selector(testClassMethod)     
                                 withOptions:AspectPositionAfter 
                                  usingBlock:^(id<AspectInfo> aspectInfo){
    NSLog(@"aspects hook setName");
} error:null];

https://juejin.cn/post/7025783407540076581
------------------越是喧囂的世界,越需要寧靜的思考------------------ 合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。 積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千里;不積小流,無以成江海。騏驥一躍,不能十步;駑馬十駕,功在不捨。鍥而舍之,朽木不折;鍥而不捨,金石可鏤。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鱔之穴無可寄託者,用心躁也。