iOS AOP 方案的對比與思考

AOP 思想

AOP:Aspect Oriented Programming,譯爲面向切面編程,是可以通過預編譯的方式和運行期動態實現,在不修改源代碼的情況下,給程序動態統一添加功能的技術。

面向對象編程(OOP)適合定義從上到下的關係,但不適用於從左到右,計算機中任何一門新技術或者新概念的出現都是爲了解決一個特定的問題的,我們看下AOP解決了什麼樣的問題。

例如一個電商系統,有很多業務模塊的功能,使用OOP來實現核心業務是合理的,我們需要實現一個日誌系統,和模塊功能不同,日誌系統不屬於業務代碼。如果新建一個工具類,封裝日誌打印方法,再在原有類中進行調用,就增加了耦合性,我們需要從業務代碼中抽離日誌系統,然後獨立到非業務的功能代碼中,這樣我們改變這些行爲時,就不會影響現有業務代碼。

當我們使用各種技術來攔截方法,在方法執行前後做你想做的事,例如日誌打印,就是所謂的AOP。

主流的AOP 方案

Method Swizzle

說到iOS中AOP的方案第一個想到的應該就是 Method Swizzle

作爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流羣:413038000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!

得益於Objective-C這門語言的動態性,我們可以讓程序在運行時做出一些改變,進而調用我們自己定義的方法。使用Runtime 交換方法的核心就是:method_exchangeImplementations, 它實際上將兩個方法的實現進行交換:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class aClass = [self class];

        SEL originalSelector = @selector(method_original:);
        SEL swizzledSelector = @selector(method_swizzle:);

        Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
        BOOL didAddMethod = class_addMethod(aClass,
                                                                originalSelector,
                                                              method_getImplementation(swizzledMethod),
                                                                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(aClass,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
複製代碼

作爲我們常說的黑魔法 Method Swizzle 到底危險不危險,有沒有最佳實踐。

這裏可以通過這篇回答一起深入理解下。這裏列出了一些 Method Swizzling 的陷阱:

  • Method swizzling is not atomic

你會把 Method Swizzling 修改方法實現的操作放在一個加號方法 +(void)load 裏,並在應用程序的一開始就調用執行,通常放在 dispatch_once() 裏面來調用。你絕大多數情況將不會碰到併發問題。

  • Changes behavior of un-owned code

這是 Method Swizzling 的一個問題。我們的目標是改變某些代碼。當你不只是對一個UIButton類的實例進行了修改,而是程序中所有的UIButton實例,對原來的類侵入較大。

  • Possible naming conflicts

命名衝突貫穿整個 Cocoa 的問題. 我們常常在類名和類別方法名前加上前綴。不幸的是,命名衝突仍是個折磨。但是swizzling其實也不必過多考慮這個問題。我們只需要在原始方法命名前做小小的改動來命名就好,比如通常我們這樣命名:

@interface UIView : NSObject
- (void)setFrame:(NSRect)frame;
@end

@implementation UIView (MyViewAdditions)

- (void)my_setFrame:(NSRect)frame {
    // do custom work
    [self my_setFrame:frame];
} 

+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

@end
複製代碼

這段代碼運行是沒問題的,但是如果 my_setFrame: 在別處被定義了會發生什麼呢?比如在別的分類中,當然這個問題不僅僅存在於swizzling 中,其他地方也可能會出現,這裏可以有個變通的方法,利用函數指針來定義

@implementation UIView (MyViewAdditions)

static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}

@end
複製代碼
  • Swizzling changes the method's arguments

我認爲這是最大的問題。想正常調用 Method Swizzling 的方法將會是個問題。比如我想調用 my_setFrame:

[self my_setFrame:frame];
複製代碼

Runtime 做的是 objc_msgSend(self, @selector(my_setFrame:), frame); Runtime去尋找my_setFrame:的方法實現,但因爲已經被交換了,事實上找到的方法實現是原始的 setFrame: 的,如果想調用 Method Swizzling 的方法,可以通過上面的函數的方式來定義,不走Runtime 的消息發送流程。不過這種需求場景很少見。

  • The order of swizzles matters

多個swizzle方法的執行順序也需要注意。假設 setFrame: 只定義在 UIivew 中,想像一下按照下面的順序執行:

[UIView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[UIControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[UIButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
複製代碼

這裏需要注意的是swizzle的順序,多個有繼承關係的類的對象swizzle時,先從父對象開始。 這樣才能保證子類方法拿到父類中的被swizzle的實現。在+(void)load中swizzle不會出錯,就是因爲load類方法會默認從父類開始調用,不過這種場景很少,一般會選擇一個類進行swizzle。

  • Difficult to understand (looks recursive)

新方法的實現裏面會調用自己同名的方法,看起來像遞歸,但是看看上面已經給出的 swizzling 封裝方法, 使用起來就很易讀懂,這個問題是已完全解決的了!

  • Difficult to debug

調試時不管通過bt 命令還是 [NSThread callStackSymbols] 打印調用棧,其中摻雜着被swizzle的方法名,會顯得一團槽!上面介紹的swizzle方案,使backtrace中打印出的方法名還是很清晰的。但仍然很難去debug,因爲很難記住swizzling影響過什麼。給你的代碼寫好文檔(即使只有你一個人會看到),統一管理一些swizzling的方法,而不是分散到業務的各個模塊。相對於調試多線程問題 Method Swizzling 要簡單很多。

Aspects

Aspects 是 iOS 上的一個輕量級 AOP 庫。它利用 Method Swizzling 技術爲已有的類或者實例方法添加額外的代碼,使用起來是很方便:

/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                     error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                             withOptions:(AspectOptions)options
                                usingBlock:(id)block
                                     error:(NSError **)error;
複製代碼

Aspects 提供了2個 AOP 方法,一個用於類,一個用於實例。在確定 hook 的 方法之後, Aspects 允許我們選擇 hook 的時機是在方法執行之前,還是方法執行之後,甚至可以直接替換掉方法的實現。網上有很多介紹其實現原理的文章,在iOS開源社區中算是少有的精品代碼,對深入理解掌握ObjC 的消息發送機制很有幫助。但其存在的缺陷就是性能較差,如官方所說

Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second.

Aspects hooks deep into the class hierarchy and creates dynamic subclasses, much like KVO. There's known issues with this approach, and to this date (February 2019) I STRICTLY DO NOT RECOMMEND TO USE Aspects IN PRODUCTION CODE. We use it for partial test mocks in, PSPDFKit, an iOS PDF framework that ships with apps like Dropbox or Evernote, it's also very useful for quickly hacking something up.

官方強烈不推薦在生產環境中使用,一般用來在單測中做一些mock操作。我們這邊的性能測試也證明了這一點:在iPhone 6 真機上,循環100w次的方法調用(已經通過 Aspects hook 的方法)中會直接報 Terminated due to memory issue crash 錯誤信息。

MPSwizzler

MPSwizzler 這個是開源數據分析SDK MixPanel 中採用的一種 AOP 方案,原理不是很複雜,主要還是基於ObjC 的運行時。

  1. 支持運行時取消對應的hook,這裏可以滿足一些需求場景的

  2. 通過 block 的方式來執行方法塊,避免方法命名的衝突

    • (void)swizzleSelector:(SEL)aSelector onClass:(Class)aClass withBlock:(swizzleBlock)aBlock named:(NSString *)aName

    { Method aMethod = class_getInstanceMethod(aClass, aSelector); if (aMethod) { uint numArgs = method_getNumberOfArguments(aMethod); if (numArgs >= MIN_ARGS && numArgs <= MAX_ARGS) {

            // 判斷該方法是否在自己類的方法列表中,而不是父類
            BOOL isLocal = [self isLocallyDefinedMethod:aMethod onClass:aClass];
            IMP swizzledMethod = (IMP)mp_swizzledMethods[numArgs - 2];
            MPSwizzle *swizzle = [self swizzleForMethod:aMethod];
    
            if (isLocal) {
                if (!swizzle) {
                    IMP originalMethod = method_getImplementation(aMethod);
    
                    // Replace the local implementation of this method with the swizzled one
                    method_setImplementation(aMethod,swizzledMethod);
    
                    // Create and add the swizzle
                    swizzle = [[MPSwizzle alloc] initWithBlock:aBlock named:aName forClass:aClass selector:aSelector originalMethod:originalMethod withNumArgs:numArgs];
                    [self setSwizzle:swizzle forMethod:aMethod];
    
                } else {
                    [swizzle.blocks setObject:aBlock forKey:aName];
                }
            } else {
            // 如果是父類的方法會添加到自身,避免對父類侵入
                IMP originalMethod = swizzle ? swizzle.originalMethod : method_getImplementation(aMethod);
    
                // Add the swizzle as a new local method on the class.
                if (!class_addMethod(aClass, aSelector, swizzledMethod, method_getTypeEncoding(aMethod))) {
                    NSAssert(NO, @"SwizzlerAssert: Could not add swizzled for %@::%@, even though it didn't already exist locally", NSStringFromClass(aClass), NSStringFromSelector(aSelector));
                    return;
                }
                // Now re-get the Method, it should be the one we just added.
                Method newMethod = class_getInstanceMethod(aClass, aSelector);
                if (aMethod == newMethod) {
                    NSAssert(NO, @"SwizzlerAssert: Newly added method for %@::%@ was the same as the old method", NSStringFromClass(aClass), NSStringFromSelector(aSelector));
                    return;
                }
    
                MPSwizzle *newSwizzle = [[MPSwizzle alloc] initWithBlock:aBlock named:aName forClass:aClass selector:aSelector originalMethod:originalMethod withNumArgs:numArgs];
                [self setSwizzle:newSwizzle forMethod:newMethod];
            }
        } else {
            NSAssert(NO, @"SwizzlerAssert: Cannot swizzle method with %d args", numArgs);
        }
    } else {
        NSAssert(NO, @"SwizzlerAssert: Cannot find method for %@ on %@", NSStringFromSelector(aSelector), NSStringFromClass(aClass));
    }
    複製代碼
    

    }

其中最主要的就是 method_setImplementation(aMethod,swizzledMethod); 其中 swizzledMethod 是根據原來方法的參數匹配到對應的如下幾個函數:

  1. static void mp_swizzledMethod_2(id self, SEL _cmd)

  2. static void mp_swizzledMethod_3(id self, SEL _cmd, id arg)

  3. static void mp_swizzledMethod_4(id self, SEL _cmd, id arg, id arg2)

  4. static void mp_swizzledMethod_5(id self, SEL _cmd, id arg, id arg2, id arg3)

這個幾個函數內部實現大體一樣的,以 mp_swizzledMethod_4 爲例:

static void mp_swizzledMethod_4(id self, SEL _cmd, id arg, id arg2)
{
    Method aMethod = class_getInstanceMethod([self class], _cmd);
    // 1\. 獲取保存hook 的實體類
    MPSwizzle *swizzle = (MPSwizzle *)[swizzles objectForKey:(__bridge id)((void *)aMethod)];
    if (swizzle) {
    // 2\. 先調用原來的方法
        ((void(*)(id, SEL, id, id))swizzle.originalMethod)(self, _cmd, arg, arg2);

        NSEnumerator *blocks = [swizzle.blocks objectEnumerator];
        swizzleBlock block;
    // 3\. 再循環調用 hook 的方法塊,可能綁定了多個
        while ((block = [blocks nextObject])) {
            block(self, _cmd, arg, arg2);
        }
    }
}
複製代碼

這個AOP的方案在多數SDK中也均採用了,比如 FBSDKSwizzlerSASwizzler,相比於Aspects 性能好太多、但與 樸素的 Method Swizzling 相比還有差距。

ISA-swizzle KVO

利用 KVO 的運行時 ISA-swizzle 原理,動態創建子類、並重寫相關方法,並且添加我們想要的方法,然後在這個方法中調用原來的方法,從而達到 hook 的目的。這裏以 ReactiveCocoa 的作爲示例。

internal func swizzle(_ pairs: (Selector, Any)..., key hasSwizzledKey: AssociationKey<Bool>) {

        // 動態創建子類
        let subclass: AnyClass = swizzleClass(self)

        ReactiveCocoa.synchronized(subclass) {
            let subclassAssociations = Associations(subclass as AnyObject)

            if !subclassAssociations.value(forKey: hasSwizzledKey) {
                subclassAssociations.setValue(true, forKey: hasSwizzledKey)

                for (selector, body) in pairs {
                    let method = class_getInstanceMethod(subclass, selector)!
                    let typeEncoding = method_getTypeEncoding(method)!

                    if method_getImplementation(method) == _rac_objc_msgForward {
                        let succeeds = class_addMethod(subclass, selector.interopAlias, imp_implementationWithBlock(body), typeEncoding)
                        precondition(succeeds, "RAC attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version.")
                    } else {    
                        // 通過 block 生成一個新的 IMP,爲生成的子類添加該方法實現。
                        let succeeds = class_addMethod(subclass, selector, imp_implementationWithBlock(body), typeEncoding)
                        precondition(succeeds, "RAC attempts to swizzle a selector that has already a runtime injected implementation. This is unsupported in the current version.")
                    }
                }
            }
        }
    }

internal func swizzleClass(_ instance: NSObject) -> AnyClass {
    if let knownSubclass = instance.associations.value(forKey: knownRuntimeSubclassKey) {
        return knownSubclass
    }

    let perceivedClass: AnyClass = instance.objcClass
    let realClass: AnyClass = object_getClass(instance)!
    let realClassAssociations = Associations(realClass as AnyObject)

    if perceivedClass != realClass {
        // If the class is already lying about what it is, it's probably a KVO
        // dynamic subclass or something else that we shouldn't subclass at runtime.
        synchronized(realClass) {
            let isSwizzled = realClassAssociations.value(forKey: runtimeSubclassedKey)
            if !isSwizzled {
                // 重寫類的 -class 和 +class 方法,隱藏真實的子類類型
                replaceGetClass(in: realClass, decoy: perceivedClass)
                realClassAssociations.setValue(true, forKey: runtimeSubclassedKey)
            }
        }

        return realClass
    } else {
        let name = subclassName(of: perceivedClass)
        let subclass: AnyClass = name.withCString { cString in
            if let existingClass = objc_getClass(cString) as! AnyClass? {
                return existingClass
            } else {
                let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0)!
                // 重寫類的 -class 和 +class 方法,隱藏真實的子類類型
                replaceGetClass(in: subclass, decoy: perceivedClass)
                objc_registerClassPair(subclass)
                return subclass
            }
        }

        object_setClass(instance, subclass)
        instance.associations.setValue(subclass, forKey: knownRuntimeSubclassKey)
        return subclass
    }
}
複製代碼

其中RxSwift 中的 _RXObjCRuntime 也提供了類似的思路。
當然也可以不用自己通過objc_registerClassPair() 創建類,直接通過 KVO 由系統幫我們生成子類,例如:

static void growing_viewDidAppear(UIViewController *kvo_self, SEL _sel, BOOL animated) {
    Class kvo_cls = object_getClass(kvo_self);
    Class origin_cls = class_getSuperclass(kvo_cls);

    IMP origin_imp = method_getImplementation(class_getInstanceMethod(origin_cls, _sel));
    assert(origin_imp != NULL);
    void (*origin_method)(UIViewController *, SEL, BOOL) = (void (*)(UIViewController *, SEL, BOOL))origin_imp;

    // 調用原來的方法
    origin_method(kvo_self, _sel, animated);

    // Do something 
}

- (void)createKVOClass {
    [self addObserver:[GrowingKVOObserver shared] forKeyPath:kooUniqueKeyPath options:NSKeyValueObservingOptionNew context:nil];

    GrowingKVORemover *remover = [[GrowingKVORemover alloc] init];
    remover.target = self;
    remover.keyPath = growingUniqueKeyPath;
    objc_setAssociatedObject(self, &growingAssociatedRemoverKey, remover, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // 通過object_getClass 取到的class 是由系統生成的前綴爲 NSKVONotifying_ 的類型
    Class kvoCls = object_getClass(self);

    Class originCls = class_getSuperclass(kvoCls);

    const char *originViewDidAppearEncoding = method_getTypeEncoding(class_getInstanceMethod(originCls, @selector(viewDidAppear:)));

    // 添加我們自己的實現 growing_viewDidAppear
    class_addMethod(kvoCls, @selector(viewDidAppear:), (IMP)growing_viewDidAppear, originViewDidAppearEncoding);
}
複製代碼

這種利用KVO動態生成子類的AOP方案對原來的類侵入最小,因爲它沒有改變原始類的方法和實現的映射關係,也就不會影響到由原始類定義的其他的實例的方法調用。在一些比如更精確的計算頁面加載時間的場景中會發揮很好的作用。但是這個AOP 的方案和其他一些SDK有衝突的情形,比如信鴿、Firebase 以及上面說的 RxSwift,在 RxSwift 中所有的消息機制都被統一成了信號,框架不推薦你使用 Delegate、KVO、Notification,尤其 KVO 會有異常錯誤的。

Fishhook

提高 iOS 的 AOP方案就不得不提到大名鼎鼎的 Fishook,它在做一些性能分析或者越獄分析中經常被用到。

大家都知道 ObjC 的方法之所以可以 Hook 是因爲它的運行時特性,ObjC 的方法調用在底層都是 objc_msgSend(id, SEL) 的形式,這爲我們提供了交換方法實現(IMP)的機會,但 C 函數在編譯鏈接時就確定了函數指針的地址偏移量(Offset),這個偏移量在編譯好的可執行文件中是固定的,而可執行文件每次被重新裝載到內存中時被系統分配的起始地址(在 lldb 中用命令image List獲取)是不斷變化的。運行中的靜態函數指針地址其實就等於上述 Offset + Mach0 文件在內存中的首地址。

既然 C 函數的指針地址是相對固定且不可修改的,那麼 fishhook 又是怎麼實現 對 C 函數的 Hook 呢?其實內部/自定義的 C 函數 fishhook 也 Hook 不了,它只能Hook Mach-O 外部(共享緩存庫中)的函數,比如 NSLog、objc_msgSend 等動態符號表中的符號。

fishhook 利用了 MachO 的動態綁定機制,蘋果的共享緩存庫不會被編譯進我們的 MachO 文件,而是在動態鏈接(依靠動態連接器 dyld)時纔去重新綁定。蘋果採用了PIC(Position-independent code)技術成功讓 C 的底層也能有動態的表現:

  • 編譯時在 Mach-O 文件 _DATA 段的符號表中爲每一個被引用的系統 C 函數建立一個指針(8字節的數據,放的全是0),這個指針用於動態綁定時重定位到共享庫中的函數實現。

  • 在運行時當系統 C 函數被第一次調用時會動態綁定一次,然後將 Mach-O 中的 _DATA 段符號表中對應的指針,指向外部函數(其在共享庫中的實際內存地址)。

fishhook 正是利用了 PIC 技術做了這麼兩個操作:

  • 將指向系統方法(外部函數)的指針重新進行綁定指向內部函數/自定義 C 函數。

  • 將內部函數的指針在動態鏈接時指向系統方法的地址。

這是Facebook 提供的官方示意圖:

Lazy Symbol Pointer Table --> Indirect Symbol Table --> Symbol Table --> String Table

這張圖主要在描述如何由一個字符串(比如 "NSLog"),根據它在 MachO 文件的懶加載表中對應的指針,一步步的找到該指針指向的函數實現地址,我們通過 MachOView 工具來分析下這個步驟:

_la_sysmbol_ptr 該section 表示 懶加載的符號指針,其中的 value,是對保留字段的解析,表示在 Indirect Symbol Table 中的索引

通過 reserve1 找到 對應 section __la_symbol_ptr 在動態符號表(Indirect Symbols)中的位置,比如下圖:#14 就是 __la_symbol_ptr section 所在的起始位置。

[圖片上傳中...(image-6b9ec5-1607149385528-4)]

符號個數計算 是通過 sizeof(void (* )) 指針在64位上時8個字節大小,所要這個__la_symbol_ptr section 有 104 / 8 = 13 個符號,_NSLog 只是其中之一。

[圖片上傳中...(image-81fb87-1607149385528-3)]

注意 Indirect Symbols 動態符號表,其中的Data 值 0x00CO (#192) 表示該符號在符號表中的索引

符號表中的第192號就是 _NSLog 符號,這個Data 0x00CE 就是字符串表中的索引

上面的索引 0x00CE 加上這個字符串表的起始值 0xD2B4 就是該符號在符號表中的位置,如下圖所示:

[圖片上傳中...(image-bedb85-1607149385528-0)]

以上梳理了fishhook 大概的流程,之後看代碼的實現就不是很抽象了,需要對 MachO 文件的結構有較深入的理解。既然fishhook 可以hook 系統靜態的C 函數,那麼也可以hook ObjC 中的 Runtime 相關的方法,比如 objc_msgSendmethod_getImplementationmethod_setImplementationmethod_exchangeImplementations 可以做一些有趣的攻防探索、其中越獄中常用的 Cydia Substrate 其中的 MobileHooker 底層就是調用 fishhook 和 ObjC 的 Runtime 來替換系統或者目標應用的函數。對其封裝較好的 theos 或者 MonkeyDev 開發工具方便越獄進行hook 分析。需要注意的是 fishhook 對於變參函數的處理比較麻煩,不太方便拿到所有的可變的參數,需要藉助彙編來操作棧和寄存器。關於這部分可以參見:TimeProfilerAppleTrace

Thunk 技術

讓我們把鏡頭進一步向前推進,瞭解下 Thunk 技術。

Thunk 程序中文翻譯爲形實轉換程序,簡而言之Thunk程序就是一段代碼塊,這段代碼塊可以在調用真正的函數前後進行一些附加的計算和邏輯處理,或者提供將對原函數的直接調用轉化爲間接調用的能力。Thunk程序在有的地方又被稱爲跳板(trampoline)程序,Thunk程序不會破壞原始被調用函數的棧參數結構,只是提供了一個原始調用的hook的能力。Thunk技術可以在編譯時和運行時兩種場景下被使用。其主要的思想就是在運行時我們自己在內存中構造一段指令讓CPU執行。關於 Thunk 思想在iOS 中的實現可以參見 Thunk程序的實現原理以及在iOS中的應用Thunk程序的實現原理以及在iOS中的應用 從背景理論到實踐來分析這一思想。

關於Thunk 思想的具體實現可以參見下面幾個三方庫以相關的博客:

其中核心都會利用到 libffi 這個庫,底層是彙編寫的,libfii 可以理解爲實現了C語言上的 Runtime。

Clang 插樁

以上iOS AOP 方案中大多是基於運行時的,fishhook 是基於鏈接階段的,而編譯階段能否實現AOP呢,插入我們想要的代碼呢?

作爲 Xcode 內置的編譯器 Clang 其實是提供了一套插樁機制,用於代碼覆蓋檢測,官方文檔如下:Clang自帶的代碼覆蓋工具,關於Clang 插樁的一個應用可以詳見這篇文章,最終是由編譯器在指定的位置幫我們加上了特定的指令,生成最終的可執行文件,編寫更多的自定義的插樁規則需要自己手寫 llvm pass

這種依賴編譯器做的AOP 方案,適用於與開發、測試階段做一些檢測工具,例如:代碼覆蓋、Code Lint、靜態分析等。

總結

以上介紹了iOS 中主流的 AOP 的方案和一些知名的框架,有編譯期、鏈接期、運行時的,從源代碼到程序裝載到內存執行,整個過程的不同階段都可以有相應的方案進行選擇。我們的工具箱又多出了一些可供選擇,同時進一步加深對靜態和動態語言的理解,也對程序從靜態到動態整個過程理解更加深入。

同時我們Android 和 iOS 無埋點SDK 3.0 均已開源,有興趣可以關注下面github 倉庫,瞭解我們最新的開發進展。

Android:github.com/growingio/g…

iOS:github.com/growingio/g…

關於 GrowingIO

GrowingIO 是國內領先的一站式數字化增長整體方案服務商。爲產品、運營、市場、數據團隊及管理者提供客戶數據平臺(CDP)、廣告分析、產品分析、智能運營等產品和諮詢服務,幫助企業在數字化轉型的路上,提升數據驅動能力,實現更好的增長。

作爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流羣:413038000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!

作者:GrowingIO技術社區
鏈接:https://juejin.cn/post/6898192050512986126
來源:掘金

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章