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 的執行時。
-
支援執行時取消對應的hook,這裡可以滿足一些需求場景的
-
通過 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 是根據原來方法的引數匹配到對應的如下幾個函式:
-
static void mp_swizzledMethod_2(id self, SEL _cmd)
-
static void mp_swizzledMethod_3(id self, SEL _cmd, id arg)
-
static void mp_swizzledMethod_4(id self, SEL _cmd, id arg, id arg2)
-
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中也均採用了,比如 FBSDKSwizzler 、SASwizzler,相比於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_msgSend
、method_getImplementation
、method_setImplementation
、method_exchangeImplementations
可以做一些有趣的攻防探索、其中越獄中常用的 Cydia Substrate 其中的 MobileHooker 底層就是呼叫 fishhook 和 ObjC 的 Runtime 來替換系統或者目標應用的函式。對其封裝較好的 theos 或者 MonkeyDev 開發工具方便越獄進行hook 分析。需要注意的是 fishhook 對於變參函式的處理比較麻煩,不太方便拿到所有的可變的引數,需要藉助彙編來操作棧和暫存器。關於這部分可以參見:TimeProfiler、 AppleTrace。
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…
關於 GrowingIO
GrowingIO 是國內領先的一站式數字化增長整體方案服務商。為產品、運營、市場、資料團隊及管理者提供客戶資料平臺(CDP)、廣告分析、產品分析、智慧運營等產品和諮詢服務,幫助企業在數字化轉型的路上,提升資料驅動能力,實現更好的增長。
作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:413038000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!
作者:GrowingIO技術社群
連結:https://juejin.cn/post/6898192050512986126
來源:掘金