iOS 開源庫系列 Aspects核心原始碼分析

Junyiii發表於2019-02-21

簡介

Aspects是一個面向切面程式設計的庫。
如果想深入瞭解iOS Runtime中的訊息傳送機制,Aspects的原始碼是值得分析的。

專案主頁
Aspects

整體分析

閱讀Aspects的原始碼需要以下知識作為基礎

  1. Objective-C Runtime
  2. 理解OC的訊息分發機制
  3. KVO中的指標交換技術

閱讀本文之前,建議應該先斷點除錯下Aspects的Demo,瞭解大致的過程。

核心實現

Aspects的核心實現就是利用Runtime中的訊息分發機制如圖:

Aspects通過把selector的方法替換為msg_forward方法轉發 轉而呼叫 forwardInvocation(forwardInvocation的實現被Aspects替換,將原來的方法實現與新增的實現組合在了一起)

核心原始碼分析

這是Aspects 面向切面程式設計的入口方法

- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add(self, selector, options, block, error);
}複製程式碼

這段程式碼可以分三部分來看

  1. aspect_isSelectorAllowedAndTrack 這個方法 對父子類同時hook一個方法進行了一些限制
  2. aspect_getContainerForObject 通過Runtime新增關聯值的方式 管理hook的方法
  3. aspect_prepareClassAndHookSelector 這是核心的實現,涉及到動態生成子類,改變isa指標的指向,改變方法的實現 一系列操作
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(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            //一個例項 只有一個container
            //這是區分例項物件和類物件的關鍵
            //例項物件可以有很多個,但是同一個類的類物件只能有一個
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            //原來的selector block
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                //container 裡 存有 identifier (selector,block)
                [aspectContainer addAspect:identifier withOptions:options];

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

核心方法

aspect_prepareClassAndHookSelector這是核心的實現,涉及到動態生成子類,改變isa指標,改變方法的實現 一系列操作

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    //動態建立子類,改變forwardInvocation方法的實現
    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]) {
            //子類的aliasSelector的實現為 當前類的selector
            __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);
        }

        //selector方法替換為_objc_msgForward
        // We use forwardInvocation to hook in.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}複製程式碼

動態生成子類,改變isa指標

#pragma mark - Hook Class

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    //這裡可以思考一下 class 方法 和 isa 的區別
    //[self class] KVO可能改變了isa指標的指向
    Class statedClass = self.class;

    // object_getClass 能準確的找到isa指標
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);

    // Already subclassed
    //如果已經子類化了 就返回
    if ([className hasSuffix:AspectsSubclassSuffix]) {
        return baseClass;

        //如果是類 就改掉類的forwardInvocation 而不是一個子類物件
        // We swizzle a class object, not a single object.
    }else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);

        //考慮到KVO,KVO的底層實現,交換了isa指標
        // 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;
        }
        // forwardInvocation 替換成 (IMP)_ASPECTS_ARE_BEING_CALLED__
        aspect_swizzleForwardInvocation(subclass);


        //子類的class方法返回當前被hook的物件的class
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);

        objc_registerClassPair(subclass);
    }

    //將當前self設定為子類,這裡其實只是更改了self的isa指標而已, 這裡hook了子類的forwardInvocation方法,再次使用當前類時,其實是使用了子類的forwardInvocation方法。
    object_setClass(self, subclass);
    return subclass;
}複製程式碼

打個廣告嘿嘿
最近寫了個開源圖表框架
github.com/JunyiXie/XJ…
覺得不錯的話可以給我個star
當然也歡迎issue!
程式碼貢獻!

相關文章