iOS AOP框架Aspects實現(xiàn)原理

前言

眾所周知赌渣,Aspects框架運用了AOP(面向切面編程)的思想信夫,這里解釋下AOP的思想:AOP是針對業(yè)務(wù)處理過程中的切面進行提取奸攻,它所面對的是處理過程中的某個步驟或階段稍算,以獲得邏輯過程中各部分之間低耦合性的隔離效果鸣奔。也許大家用Aspects提供的兩個方法用著很爽墨技,有沒有想揭開它的神秘面紗,看一下它的廬山真面目挎狸?接下來主要講下Aspects的主要實現(xiàn)原理扣汪。


ruhua.gif

實現(xiàn)原理

其實主要涉及到兩點,用過runtime的同學(xué)都知道swizzling和消息轉(zhuǎn)發(fā)锨匆,如果還不是很清楚的同學(xué)可以看下我之前寫的一篇文章OC中swizzling的“移魂大法”,“移魂大法”我這邊就不累贅了崭别,提下消息轉(zhuǎn)發(fā)。

消息轉(zhuǎn)發(fā)

MessageForward.jpg

當(dāng)向一個對象發(fā)送消息的時候恐锣,在這個對象的類繼承體系中沒有找到該方法的時候茅主,就會Crash掉,拋出* unrecognized selector sent to …異常信息土榴,但是在拋出這個異常前其實還是可以拯救的诀姚,Crash之前會依次執(zhí)行下面的方法:

  1. 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ā)給其他對象來處理撩银。

  1. methodSignatureForSelector:會去獲取一個方法簽名给涕,如果沒有獲取到的話就回直接挑用doesNotRecognizeSelector,如果能獲取的話系統(tǒng)就會創(chuàng)建一個NSlnvocation傳給forwardInvocation方法蜒蕾。

  2. forwardInvocation:該方法是上一個步傳進來的NSlnvocation,然后調(diào)用NSlnvocationinvokeWithTarget方法焕阿,轉(zhuǎn)發(fā)到對應(yīng)的Target咪啡。

  3. doesNotRecognizeSelector:拋出unrecognized selector sent to …異常。

整體原理

上面講了幾種消息轉(zhuǎn)發(fā)的方法暮屡,Aspects主要是利用了forwardInvocation進行轉(zhuǎn)發(fā)撤摸,Aspects其實利用和kvo類似的原理,通過動態(tài)創(chuàng)建子類的方式,把對應(yīng)的對象isa指針指向創(chuàng)建的子類准夷,然后把子類的forwardInvocationIMP替成__ASPECTS_ARE_BEING_CALLED__钥飞,假設(shè)要hook的方法名XX,在子類中添加一個Aspects_XX的方法,然后將Aspects_XXIMP指向原來的XX方法的IMP衫嵌,這樣方便后面調(diào)用原始的方法读宙,再把要hook的方法XXIMP指向_objc_msgForward,這樣就進入了消息轉(zhuǎn)發(fā)流程楔绞,而forwardInvocationIMP被替換成了__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_addaspect_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)用狮惜。
  • AspectsContainer根據(jù)aliasSelector取出對象, 并組裝一個AspectInfo, 帶有原函數(shù)的調(diào)用參數(shù)和各項屬性, 傳給外部的調(diào)用者 (在這是block) .
  • 調(diào)用完成后銷毀帶有removeOptionhook邏輯, 將原selector掛鉤到原IMP上, 刪除別名selector

總結(jié)

-forwardInvocation:的消息轉(zhuǎn)發(fā)雖然看起來很神奇高诺,但是平時沒什么需求的話盡量不要去碰這個東西,一般的話swizzling基本可以滿足我們項目的大部分需求了碾篡,還有就是JSPatch既然不能用了虱而,但是Aspects這個框架還是正常上AppStore的,利用這個庫的話還是可以弄出輕量級Hotfix,感興趣的同學(xué)可以點擊輕量級低風(fēng)險 iOS 熱更新方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末开泽,一起剝皮案震驚了整個濱河市牡拇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌眼姐,老刑警劉巖诅迷,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異众旗,居然都是意外死亡罢杉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門贡歧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滩租,“玉大人,你說我怎么就攤上這事利朵÷上耄” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵绍弟,是天一觀的道長技即。 經(jīng)常有香客問我,道長樟遣,這世上最難降的妖魔是什么而叼? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮豹悬,結(jié)果婚禮上葵陵,老公的妹妹穿的比我還像新娘。我一直安慰自己瞻佛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布伤柄。 她就那樣靜靜地躺著,像睡著了一般适刀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蔗彤,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天然遏,我揣著相機與錄音,去河邊找鬼丢早。 笑死秧倾,一個胖子當(dāng)著我的面吹牛那先,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斤葱,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼揖闸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了汤纸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后囚企,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體渔呵,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡淫痰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慎颗。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖言询,靈堂內(nèi)的尸體忽然破棺而出俯萎,到底是詐尸還是另有隱情,我是刑警寧澤运杭,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布夫啊,位于F島的核電站,受9級特大地震影響辆憔,放射性物質(zhì)發(fā)生泄漏撇眯。R本人自食惡果不足惜报嵌,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望熊榛。 院中可真熱鬧锚国,春花似錦、人聲如沸玄坦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煎楣。三九已至豺总,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間择懂,已是汗流浹背喻喳。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留困曙,地道東北人沸枯。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像赂弓,于是被迫代替她去往敵國和親绑榴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容