iOS Aspects的實(shí)現(xiàn)原理

Aspects是iOS上技巧性很強(qiáng)的一個(gè)第三方類庫(kù)偎窘,主要針對(duì)于AOP編程(面向切面編程)的思想。

“面向切面編程” 顧名思義就是帶有一定侵入性的編程方式溜在,網(wǎng)上對(duì)AOP編程比較專業(yè)的解釋就是:可以通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)在不修改源代碼的情況下給程序動(dòng)態(tài)統(tǒng)一添加功能的一種技術(shù)陌知。比較常用于APP內(nèi)部事件埋點(diǎn)的操作。

Aspects的使用:

我們捕捉viewWillAppear的這個(gè)方法掖肋,把option設(shè)為AspectPositionAfter仆葡,block就會(huì)在viewWillAppear執(zhí)行之后再執(zhí)行:

- (void)viewWillAppear:(BOOL)animated {
    NSLog(@"viewWillAppear");
}
   [self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info){
        NSLog(@"hook_viewWillAppear");
    } error:nil];

執(zhí)行結(jié)果:


image.png

而且不光能對(duì)對(duì)象使用,也可以直接對(duì)類使用

 [ViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info){
        NSLog(@"hook_viewWillAppear");
    } error:nil];

這樣所有這個(gè)類的對(duì)象在執(zhí)行viewWillAppear的時(shí)候志笼,都會(huì)觸發(fā)block沿盅。

當(dāng)然,對(duì)類的hook和對(duì)對(duì)象的hook是不一樣的纫溃,類應(yīng)該是整體起效腰涧,而對(duì)象是獨(dú)立起效。我們先看一下兩種hook之后的情況紊浩,看看大概做了什么窖铡,然后就比較容易理解。

1坊谁、類的hook:
對(duì)類進(jìn)行hook之后费彼,我們打印出方法列表,發(fā)現(xiàn)確實(shí)動(dòng)態(tài)添加了些方法口芍,

image.png

aspects__viewWillAppear:加了aspects__前綴的方法箍铲,用來(lái)保留原的方法的實(shí)現(xiàn),因?yàn)樵瓉?lái)的方法指針指向forwardInvocation了阶界。

forwardInvocation方法:即使沒(méi)實(shí)現(xiàn)虹钮,也會(huì)動(dòng)態(tài)添加這個(gè)方法,指針指向了真正實(shí)現(xiàn)功能的ASPECTS_ARE_BEING_CALLED方法膘融。

2、對(duì)象的hook:
對(duì)對(duì)象進(jìn)行hook之后祭玉,我們也打印出方法列表:

image.png

為啥并沒(méi)有新增任何方法氧映?我們猜測(cè)會(huì)不會(huì)是這個(gè)對(duì)象已經(jīng)不指向原來(lái)的類了呢,那我們就利用object_getClass(self)方法來(lái)獲取這個(gè)對(duì)象的類看看:


image.png

可以看到對(duì)象的類變化了脱货,對(duì)象的hook由于不能使其他同族的對(duì)象收到影響岛都,所以不能對(duì)原來(lái)的類直接進(jìn)行改變律姨,aspects就動(dòng)態(tài)生成了一個(gè)類(類似kvo思想),我們?cè)俅蛴〕鰟?dòng)態(tài)類的方法列表:


image.png

如我們所料臼疫,他把整體的過(guò)程移到了動(dòng)態(tài)類去實(shí)現(xiàn)择份。

Aspects的內(nèi)部實(shí)現(xiàn):

Aspects的實(shí)現(xiàn)核心還是利用iOS消息轉(zhuǎn)發(fā)的機(jī)制,利用轉(zhuǎn)發(fā)的第三部forwardInvocation來(lái)捕捉方法的調(diào)用時(shí)機(jī)烫堤,然后用利用method swizzling將函數(shù)指針指向自己的實(shí)現(xiàn)荣赶,然后在自己的實(shí)現(xiàn)里調(diào)用原來(lái)的方法,再根據(jù)option的時(shí)機(jī)調(diào)用block鸽斟。

跳轉(zhuǎn)方法拔创,可以看到統(tǒng)一的方法入口是aspect_add,先是進(jìn)行hook之前的準(zhǔn)備工作富蓄。

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

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    NSCParameterAssert(self);
    NSCParameterAssert(selector);
    NSCParameterAssert(block);

    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{    #為了安全加了鎖
        #判斷方法是否能被hook
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {  

           #將被hook的對(duì)象先通過(guò)AspectsContainer對(duì)象先進(jìn)行動(dòng)態(tài)綁定剩燥,以供后續(xù)使用 。
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);  
           #生成block的方法簽名立倍,并將各個(gè)參數(shù)保存生成identifier對(duì)象
           #MARK:(為什么要對(duì)block這么處理灭红,后面再說(shuō))
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) { 
               #將identifier存入入aspectContainer
                [aspectContainer addAspect:identifier withOptions:options];

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

然后執(zhí)行到aspect_prepareClassAndHookSelector方法,這個(gè)方法會(huì)對(duì)對(duì)象和實(shí)例區(qū)別處理口注,然后改變函數(shù)指針的指向比伏。

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    #這里地方根據(jù)hook的對(duì)象是實(shí)例還是類的,進(jìn)行區(qū)別處理疆导,返回需要處理的類
    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);

      #接下來(lái)主要就是改變指針指向赁项,將原來(lái)方法的指針指向forwardInvocation方法。
    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_hookClass的實(shí)現(xiàn):

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
      #class會(huì)返回對(duì)象的類澈段,但是如果本身是類悠菜,就會(huì)返回自身。
      #object_getClass會(huì)返回ISA指針的指向败富,對(duì)象返回類悔醋,類則會(huì)返回元類。
    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);
     
        # isa已經(jīng)指向動(dòng)態(tài)生成的類了
       // 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.
    #如果是KVO的對(duì)象的話兽叮。
    #因?yàn)镵VO也會(huì)動(dòng)態(tài)生成子類芬骄,并且改變isa指針指向
    }else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }
     #以下說(shuō)明被Hook的是對(duì)象了
    // Default case. Create dynamic subclass.
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    Class subclass = objc_getClass(subclassName);

    if (subclass == nil) {
        #動(dòng)態(tài)創(chuàng)建子類
        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指針
        aspect_swizzleForwardInvocation(subclass);
           #這里做了kvo一樣的事……讓class方法返回還是原來(lái)的類。
           #不過(guò)我們還是可以用object_getClass看到對(duì)象的類已經(jīng)是XXX_Aspects_了    ㄟ( ▔, ▔ )ㄏ
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }
        #將對(duì)象的isa指針指向動(dòng)態(tài)生成的子類鹦聪。
    object_setClass(self, subclass);
    return subclass;
}

我們也看一下aspect_swizzleClassInPlace這個(gè)方法的實(shí)現(xiàn)账阻,這是對(duì)類的hook的處理

static Class aspect_swizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    NSString *className = NSStringFromClass(klass);

    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        #其實(shí)主要的也還是交換了swizzleForwardInvocation的imp指針
        #加了個(gè)數(shù)組判斷,防止反復(fù)交換
        if (![swizzledClasses containsObject:className]) {
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    });
    return klass;
}

交換了forwardInvocation的imp指針泽本,指向了ASPECTS_ARE_BEING_CALLED方法:

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));
}

然后當(dāng)被hook的方法被調(diào)用時(shí)淘太,就會(huì)觸發(fā)以下的方法,也就是真正的執(zhí)行的方法:

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;
   
    #根據(jù)option的情況,區(qū)分處理蒲牧,執(zhí)行block
    #三種option分別由三個(gè)數(shù)組進(jìn)行保存管理
    // Before hooks.
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        #選擇instead 撇贺,原來(lái)的方法就不執(zhí)行了。
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
       #這里執(zhí)行原來(lái)的方法 
        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. 
    #移除option為AspectOptionAutomaticRemoval的hook信息
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}

PS:關(guān)于Aspects的block簽名問(wèn)題:

Aspects里面對(duì)block進(jìn)行了簽名冰抢,然后又用NSInvocation去調(diào)用block松嘶。
有的人不理解,我們不是拿到block了嗎挎扰,為什么不直接調(diào)用block翠订,非要弄得這么麻煩?鼓鲁?蕴轨?因?yàn)锳spects要讓block能夠支持動(dòng)態(tài)參數(shù)
block在用普通方式調(diào)用的時(shí)候骇吭,必須要明確參數(shù)橙弱,在不確定參數(shù)個(gè)數(shù)的情況下,我們就無(wú)法這么調(diào)用了燥狰,而要使用NSInvocation配置參數(shù)再進(jìn)行調(diào)用棘脐。

嘗試一下,依然以hook控制器的viewWillAppear方法為例龙致,在block后面加入被hook方法的參數(shù)(即animated)

 [self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info,BOOL animated){
        NSLog(@"hook_show animatied:%d",animated);
    } error:nil];

輸出結(jié)果:
image.png

確實(shí)是成功傳遞了參數(shù)蛀缝。

接下來(lái)看一下內(nèi)部的實(shí)現(xiàn):

- (BOOL)invokeWithInfo:(id<AspectInfo>)info {
     #根據(jù)block簽名生成block的invocation
    NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
     #這個(gè)是原方法的invocation
    NSInvocation *originalInvocation = info.originalInvocation;
    NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;
    #檢測(cè)兩個(gè)invocation的參數(shù)必須匹配
    // Be extra paranoid. We already check that on hook registration.
    if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
        AspectLogError(@"Block has too many arguments. Not calling %@", info);
        return NO;
    }
    #把id<AspectInfo> info先作為第二個(gè)參數(shù),第一個(gè)參數(shù)為self目代,block的方法沒(méi)有_cmd
    // The `self` of the block will be the AspectInfo. Optional.
    if (numberOfArguments > 1) {
        [blockInvocation setArgument:&info atIndex:1];
    }
    
    void *argBuf = NULL;
      #這個(gè)地方是從2開始屈梁,因?yàn)槲覀兤綍r(shí)的方法其實(shí)會(huì)有兩個(gè)隱形的參數(shù)self和_cmd,這了兩個(gè)參數(shù)占了0和1的位置
    for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
        const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
        NSUInteger argSize;
        NSGetSizeAndAlignment(type, &argSize, NULL);
        
        if (!(argBuf = reallocf(argBuf, argSize))) {
            AspectLogError(@"Failed to allocate memory for block invocation.");
            return NO;
        }
                #把原方法的參數(shù)傳遞給block
        [originalInvocation getArgument:argBuf atIndex:idx];
        [blockInvocation setArgument:argBuf atIndex:idx];
    }
    #執(zhí)行block
    #因?yàn)閎lock的本質(zhì)就是一個(gè)對(duì)象榛了,內(nèi)部保存著函數(shù)指針在讶,
    #所以它執(zhí)行的target就是自己,不需要通過(guò)selector 
   找到方法名稱霜大。
    [blockInvocation invokeWithTarget:self.block];
    
    if (argBuf != NULL) {
        free(argBuf);
    }
    return YES;
}

我們打印出執(zhí)行之后的兩個(gè)invocation看一下:


image.png

可以看到argument 2為{B} 0, 說(shuō)明是bool類型构哺,值為0。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末战坤,一起剝皮案震驚了整個(gè)濱河市曙强,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌途茫,老刑警劉巖碟嘴,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異慈省,居然都是意外死亡臀防,警方通過(guò)查閱死者的電腦和手機(jī)眠菇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門边败,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)袱衷,“玉大人,你說(shuō)我怎么就攤上這事笑窜≈略铮” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵排截,是天一觀的道長(zhǎng)嫌蚤。 經(jīng)常有香客問(wèn)我,道長(zhǎng)断傲,這世上最難降的妖魔是什么脱吱? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮认罩,結(jié)果婚禮上箱蝠,老公的妹妹穿的比我還像新娘。我一直安慰自己垦垂,他們只是感情好宦搬,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著劫拗,像睡著了一般间校。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上页慷,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天憔足,我揣著相機(jī)與錄音,去河邊找鬼酒繁。 笑死滓彰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的欲逃。 我是一名探鬼主播找蜜,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼稳析!你這毒婦竟也來(lái)了洗做?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤彰居,失蹤者是張志新(化名)和其女友劉穎诚纸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陈惰,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡畦徘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片井辆。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡关筒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出杯缺,到底是詐尸還是另有隱情蒸播,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布萍肆,位于F島的核電站袍榆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏塘揣。R本人自食惡果不足惜包雀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亲铡。 院中可真熱鬧才写,春花似錦、人聲如沸奴愉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)锭硼。三九已至房资,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間檀头,已是汗流浹背轰异。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留暑始,地道東北人搭独。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像廊镜,于是被迫代替她去往敵國(guó)和親牙肝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348