Aspects解決的問題
Aspects是AOP(面向切面編程)思想在iOS下OC的實現。Aspects可以用于hook函數,讓函數執行一些副操作。為嵌入不同函數中的功能相同的操作,每類功能相同的操作可以抽取出一個切面。
- OOP針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。
- AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。
基本原理
OC上,每個類都是這樣一個結構體:
struct objc_class {
struct objc_class * isa;
const char *name;
….
struct objc_method_list **methodLists; /*方法鏈表*/
};
其中 methodList 方法鏈表里存儲的是 Method類型:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;
char *method_types;
IMP method_imp;
};
Method 保存了一個方法的全部信息,包括 SEL 方法名,type各參數和返回值類型,IMP該方法具體實現的函數指針。
通過 Selector 調用方法時,會從methodList 鏈表里找到對應Method結構體進行調用,這個 methodList上的的元素是可以動態替換的,可以把某個Selector對應的函數指針IMP替換成新的,也可以拿到已有的某個 Selector 對應的函數指針IMP,讓另一個Selector 跟它對應。
簡單版實現思路
直接調用method_exchangeImplementations
即可交替兩個selector對應的函數IMP指針, 具體應用例如在UIViewController的category里, 寫一個- (void)viewDidAppear
等生命周期相關的方法, 從切面對控制器的生命周期進行干涉
但這種方法要求在編譯時必須寫好替換的函數, 如果你在需要在運行時獲取一個類的method, 并動態地進行重定向和包裝參數, 這個方法就顯得局限了.
Aspects的原理
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); //調用這個函數,偽代碼...
}
//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; //_objc_msgForward 用于消息轉發
return imp;
}
當我們對一個類對用它無法識別的selector時, 會拋出unrecogniezed selector
的異常, 但是從runtime的偽代碼中可以看到, 當一個類根據selector找不到對應的IMP函數指針時, 會返回_objc_msgForward
這個函數指針, 而并不是直接就拋出異常, 而Aspects正是利用_objc_msgForward
去進行方法重定向的
Aspects的思路
當對一個類調用無法識別selector時, 其實并不是馬上就會觸發異常, 而是會走進動態方法決議的流程, 如下圖
- resolvedInstanceMethod: 適合給類/對象動態添加一個相應的實現,
- forwardingTargetForSelector:適合將消息轉發給其他對象處理,
- forwardInvocation: 是里面最靈活,最能符合需求的。
在這三個方法里, 都可以有機會重新去處理這個無法識別的selector, forwardingTargetForSelector
中, 可以將selector交給別的對象處理, 如果仍然無法處理, 則進入最后一步forwardInvocation
, 在此可以獲得該次調用的所有參數和調用對象, 并隨意重新組裝. 在此依然無法處理方法, 才會拋出異常.
而Aspects正是利用其中最靈活的forwardInvocation
進行封裝的.
首先 , 將類的forwardInvocation
的IMP替換為到Aspects里的__ASPECTS_ARE_BEING_CALLED__
, 在該方法中統一處理hook過來的方法的邏輯處理
然后 , 在methodList鏈表里添加一個帶有別名的selector (例如hook的是viewDidAppaer
, 則會生成一個aspects_viewDidAppear
的函數添加到該類的methodlist里), 并指向原來的IMP. 調用此別名selector, 其實相當于調用原函數
最后 , 原selector的IMP則會統一指向_objc_msgForward
, 啟動消息轉發的流程, 并在__ASPECTS_ARE_BEING_CALLED__
做截獲處理
核心代碼分析一: hook part, 部署好方法之間的掛鉤
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
//將class的forwardInvocation:方法hook走
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (aspect_isMsgForwardIMP(targetMethodIMP) == NO) {
// Make a method alias for the existing method implementation, it not already copied.
const char *typeEncoding = method_getTypeEncoding(targetMethod);
//方法別名, 將原來的IMP放到此selector中, 并添加到class的method列表中
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);
}
//將原來的IMP替換為直接調用forwardInvocation:
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
該函數為swizzle的核心方法, 完成以下幾件事
- 將原selector的各種屬性, 封裝到AspectsContainer中, 以aliasSelector作為key儲存在字典中
- aspect_hookClass將類的
-forwardInvocation:
函數統一hook到__ASPECTS_ARE_BEING_CALLED__
里處理 - 抽出原selector的原IMP, 并生成一個帶有別名的selector (例:原selector為
viewWillAppear:
, 則生成一個aspects_viewWillAppear:
) , 將別名selector與原IMP掛鉤, 添加到類的methodList中 - 替換類的methodList, 將原selector的IMP替換成
-forwardInvocation:
, 當調用原selector時, 將直接啟動消息轉發流程
核心代碼分析: implement part, 實現轉發后函數最終執行的邏輯
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)];
}
該函數為swizzle后, 實現新IMP統一處理的核心方法 , 完成一下幾件事
- 處理調用邏輯, 有before, instead, after, remove四種option
- 將block轉換成一個NSInvocation對象以供調用, 如何實現, 后面會有詳述
- 從AspectsContainer根據aliasSelector取出對象, 并組裝一個AspectInfo, 帶有原函數的調用參數和各項屬性, 傳給外部的調用者 (在這是block) .
- 調用完成后銷毀帶有removeOption的hook邏輯, 將原selector掛鉤到原IMP上, 刪除別名selector
block與NSInvocation的轉換
在轉發時, 需要調用的是NSInvocation, 而在hook的時候緩存起來的是block, 所以需要將block轉換成NSInvocation.
static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
AspectBlockRef layout = (__bridge void *)block;
if (!(layout->flags & AspectBlockFlagsHasSignature)) {
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
NSLog(@"AspectBlockFlagsHasSignature = %lu", (unsigned long)AspectBlockFlagsHasSignature);
NSLog(@"AspectBlockFlagsHasCopyDisposeHelpers = %lu", (unsigned long)AspectBlockFlagsHasCopyDisposeHelpers);
void *desc = layout->descriptor;
desc += 2 * sizeof(unsigned long int);
if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
desc += 2 * sizeof(void *);
}
if (!desc) {
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
const char *signature = (*(const char **)desc);
return [NSMethodSignature signatureWithObjCTypes:signature];
}
NSInvocation的創建, 需要signature, argument.
signature可以從block中轉換過來, 而參數則可以從原來的AspectInfo對象中取出來做轉換
void *desc = layout->descriptor
這個指針就是用于獲取block的簽名了, 根據clang的源碼, 簽名位于descriptor結構體的第三個變量, 所以將指針移動兩個單位.
flag用于判斷block的具體類型BLOCK_HAS_COPY_DISPOSE
, 代表這個block是否有捕獲外部參數, 如果有捕獲, 則descriptor中會多插入兩個變量, 所以需要將desc指針再移動兩個單位
與JSPatch的兼容問題
JSPatch先hook的情況
Aspects先hook的情況
下圖為JSPatch的hook路徑
JSPatch先hook的情況會導致AspectsOption的先后順序失效, 只能before地執行, 但保證能安全執行, 是目前兩者融合使用時的一個折中的解決辦法
在這篇文章中提供了一個修改Aspect源碼的方法, 但我在修改后仍然遇到bug, 有興趣的同學可以去看看
結語
從JSPatch的兼容性問題可以看出, hook方案雖然非常強大, 但不同第三方庫之間并不相互兼容. 尤其大部分的核心邏輯都集中在了-forwardInvocation:
上, 一旦hook的情況多了, 此類bug的表現將十分詭異而且難以排查.
此類AOP庫在整個項目中最好只能使用一個, 如果項目中有自己寫swizzle的需求, 最好停留在sel和IMP一一對應的方式, 而不去觸碰-forwardInvocation:
的消息轉發, 便可避免這類bug的發生