前言
眾所周知赌渣,Aspects框架運用了AOP(面向切面編程)的思想信夫,這里解釋下AOP的思想:AOP是針對業(yè)務(wù)處理過程中的切面進行提取奸攻,它所面對的是處理過程中的某個步驟或階段稍算,以獲得邏輯過程中各部分之間低耦合性的隔離效果鸣奔。也許大家用Aspects提供的兩個方法用著很爽墨技,有沒有想揭開它的神秘面紗,看一下它的廬山真面目挎狸?接下來主要講下Aspects的主要實現(xiàn)原理扣汪。
實現(xiàn)原理
其實主要涉及到兩點,用過runtime的同學(xué)都知道swizzling
和消息轉(zhuǎn)發(fā)锨匆,如果還不是很清楚的同學(xué)可以看下我之前寫的一篇文章OC中swizzling的“移魂大法”,“移魂大法”我這邊就不累贅了崭别,提下消息轉(zhuǎn)發(fā)。
消息轉(zhuǎn)發(fā)
當(dāng)向一個對象發(fā)送消息的時候恐锣,在這個對象的類繼承體系中沒有找到該方法的時候茅主,就會Crash掉,拋出* unrecognized selector sent to …
異常信息土榴,但是在拋出這個異常前其實還是可以拯救的诀姚,Crash之前會依次執(zhí)行下面的方法:
-
resolveInstanceMethod
(或resolveClassMethod
):實現(xiàn)該方法,可以通過class_addMethod
添加方法玷禽,返回YES的話系統(tǒng)在運行時就會重新啟動一次消息發(fā)送的過程赫段,NO的話會繼續(xù)執(zhí)行下一個方法。
2.forwardingTargetForSelector
:實現(xiàn)該方法可以將消息轉(zhuǎn)發(fā)給其他對象矢赁,只要這個方法返回的不是nil或self糯笙,也會重啟消息發(fā)送的過程,把這消息轉(zhuǎn)發(fā)給其他對象來處理撩银。
methodSignatureForSelector
:會去獲取一個方法簽名给涕,如果沒有獲取到的話就回直接挑用doesNotRecognizeSelector
,如果能獲取的話系統(tǒng)就會創(chuàng)建一個NSlnvocation
傳給forwardInvocation
方法蜒蕾。forwardInvocation
:該方法是上一個步傳進來的NSlnvocation
,然后調(diào)用NSlnvocation
的invokeWithTarget
方法焕阿,轉(zhuǎn)發(fā)到對應(yīng)的Target
咪啡。doesNotRecognizeSelector
:拋出unrecognized selector sent to …
異常。
整體原理
上面講了幾種消息轉(zhuǎn)發(fā)的方法暮屡,Aspects
主要是利用了forwardInvocation
進行轉(zhuǎn)發(fā)撤摸,Aspects
其實利用和kvo
類似的原理,通過動態(tài)創(chuàng)建子類的方式,把對應(yīng)的對象isa指針指向創(chuàng)建的子類准夷,然后把子類的forwardInvocation
的IMP
替成__ASPECTS_ARE_BEING_CALLED__
钥飞,假設(shè)要hook
的方法名XX
,在子類中添加一個Aspects_XX
的方法,然后將Aspects_XX
的IMP
指向原來的XX
方法的IMP
衫嵌,這樣方便后面調(diào)用原始的方法读宙,再把要hook
的方法XX
的IMP
指向_objc_msgForward
,這樣就進入了消息轉(zhuǎn)發(fā)流程楔绞,而forwardInvocation
的IMP
被替換成了__ASPECTS_ARE_BEING_CALLED__
结闸,這樣就會進入__ASPECTS_ARE_BEING_CALLED__
進行攔截處理,這樣整個流程大概結(jié)束酒朵。
代碼解析
代碼解析這塊的話主要講下核心的模塊桦锄,一些邊邊角角的東西去看了自然就明白了。
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.
};
AspectOptions
提供各種姿勢蔫耽,前插结耀、后插、整個方法替換都行匙铡,簡直就是爽图甜。
static void aspect_performLocked(dispatch_block_t block) {
static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&aspect_lock);
block();
OSSpinLockUnlock(&aspect_lock);
}
在aspect_add
、aspect_remove
方法里面用了aspect_performLocked
, 而aspect_performLocked
方法用了OSSpinLockLock
加鎖機制慰枕,保證線程安全并且性能高具则。不過這種鎖已經(jīng)不在安全,主要原因發(fā)生在低優(yōu)先級線程拿到鎖時具帮,高優(yōu)先級線程進入忙等(busy-wait)狀態(tài)博肋,消耗大量 CPU 時間,從而導(dǎo)致低優(yōu)先級線程拿不到 CPU 時間蜂厅,也就無法完成任務(wù)并釋放鎖匪凡。這種問題被稱為優(yōu)先級反轉(zhuǎn),有興趣的可以點擊任意門不再安全的 OSSpinLock
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;
}
aspect_hookClass
這個方法顧名思義就是要hook對應(yīng)的Class掘猿,這在里創(chuàng)建子類病游,通過aspect_swizzleForwardInvocation
方法把forwardInvocation
的實現(xiàn)替換成__ASPECTS_ARE_BEING_CALLED__
, aspect_hookedGetClass
修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回當(dāng)前對象的 class,最后通過object_setClass
把isa指針指向subclass
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));
}
}
通過aspect_isMsgForwardIMP
方法判斷要hook的方法時候被hook了稠通,這里如果有使用JSPatch的話會沖突掉衬衬,不過JSPatch已經(jīng)被蘋果禁止使用了,蘋果爸爸說的算改橘。如果沒有被hook走的話會先創(chuàng)建一個有自己別名的方法滋尉,然后把這個帶有別名方法的IMP指向原始要hook方法的實現(xiàn),最后要hook的方法的IMP指向_objc_msgForward
或者_objc_msgForward_stret
,為什么還會有一個_objc_msgForward_stret
飞主,詳細(xì)原因可以參考JSPatch實現(xiàn)原理詳解<二>里面的Special Struct
// This is the swizzled forwardInvocation: method.
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)];
}
該函數(shù)為swizzle
后, 實現(xiàn)新IMP
統(tǒng)一處理的核心方法 , 完成一下幾件事:
- 處理調(diào)用邏輯, 有before, instead, after, remove四種option
- 將block轉(zhuǎn)換成一個
NSInvocation
對象以供調(diào)用狮惜。 - 從
AspectsContaine
r根據(jù)aliasSelector
取出對象, 并組裝一個AspectInfo
, 帶有原函數(shù)的調(diào)用參數(shù)和各項屬性, 傳給外部的調(diào)用者 (在這是block
) . - 調(diào)用完成后銷毀帶有
removeOption
的hook
邏輯, 將原selecto
r掛鉤到原IMP
上, 刪除別名selector
總結(jié)
-forwardInvocation:
的消息轉(zhuǎn)發(fā)雖然看起來很神奇高诺,但是平時沒什么需求的話盡量不要去碰這個東西,一般的話swizzling
基本可以滿足我們項目的大部分需求了碾篡,還有就是JSPatch
既然不能用了虱而,但是Aspects這個框架還是正常上AppStore的,利用這個庫的話還是可以弄出輕量級Hotfix
,感興趣的同學(xué)可以點擊輕量級低風(fēng)險 iOS 熱更新方案