AOP thought

AOP: Aspect Oriented Programming (AOP) is a technique for dynamically adding functions to programs without modifying the source code, which can be implemented dynamically by precompiling and running time.

Object-oriented programming (OOP) is good for defining relationships from the top down, but not from the left to the right. Every new technology or concept in a computer emerges to solve a particular problem. Let’s take a look at what AOP solves.

For example, an e-commerce system has many functions of business modules. It is reasonable to use OOP to realize the core business. We need to implement a log system, which is different from module functions and does not belong to business code. If we create a new utility class that encapsulates the logging printing methods and then invoke them in the original class, we increase the coupling. We need to separate the logging system from the business code and separate it from the non-business functional code so that when we change these behaviors, we don’t affect the existing business code.

When we use various techniques to intercept methods and do what you want before and after the method executes, such as logging, it’s called AOP.

Mainstream AOP schemes

Method Swizzle

Method Swizzle is the first Method that comes to mind when talking about AOP in iOS

Thanks to the dynamic nature of objective-C, we can make changes at runtime to call our own methods. The gist of using Runtime to swap methods is method_exchangeImplementations, which is actually exchanging implementations of two methods:

+ (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); }}); }Copy the code

As we often say the dark magic Method Swizzle is dangerous or not dangerous, there is no best practice.

Here can be read through this answer together in-depth understanding. Here are some Method Swizzling traps:

  • Method swizzling is not atomic

You would put Method Swizzling in a plus Method +(void)load and call it at the start of the application, usually in dispatch_once(). Most of the time you won’t have concurrency problems.

  • Changes behavior of un-owned code

This is a question for Method Swizzling. Our goal is to change some code. When you modify not just one instance of A UIButton class, but all UIButton instances in your program, it’s a big intrusion into the original class.

  • Possible naming conflicts

Name conflicts are a problem throughout Cocoa. We often prefix class names and class method names. Unfortunately, naming conflicts are still a torment. But Swizzling doesn’t have to think too much about it. All we need to do is make a small change to the original method name, like this:

@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
Copy the code

This code works fine, but what happens if my_setFrame: is defined elsewhere? For example, in other categories, and this is not just a problem in Swizzling, but in other places as well, there is a workaround, using function Pointers

@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
Copy the code
  • Swizzling changes the method’s arguments

I think that’s the biggest problem. Calling Method Swizzling normally will be a problem. Let’s say I want to call my_setFrame:

[self my_setFrame:frame];
Copy the code

So what Runtime does is objc_msgSend(self, @selector(my_setFrame:), frame); Runtime looks for the my_setFrame: method implementation, but since it has been switched, the actual method implementation found is the original setFrame: If you want to call Method Swizzling, you can define Method Swizzling as above, without going through Runtime’s message sending process. However, this requirement scenario is rare.

  • The order of swizzles matters

The order in which multiple swizzle methods are executed also requires attention. Assuming that setFrame: is defined only in UIivew, imagine doing it in the following order:

[UIView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[UIControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[UIButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
Copy the code

The important thing to note here is the order of swizzle. When an object swizzle of multiple inherited classes is swizzle, it starts with the parent. This ensures that subclass methods get the swizzle implementation of the superclass. Swizzle is not an error in +(void)load because the load method is called from the parent class by default, but this is rare, and a class is usually selected to swizzle.

  • Difficult to understand (looks recursive)

The implementation of the new method calls its own method of the same name, which looks like a recursion, but looking at the swizzling wrapper method shown above makes it easy to read and use. This problem is completely solved!

  • Difficult to debug

When debugging, either by using the BT command or [NSThread callStackSymbols] to print the call stack, which is mixed with the swizzle method name, it will look like a bunch of slots! The Swizzle scheme, described above, makes the method names printed in the backtrace very clear. But it’s still hard to debug because it’s hard to remember what swizzling has affected. Document your code (even if only you will see it) and manage a few Swizzling approaches, rather than spreading them across the business. Method Swizzling is much simpler than debugging multithreaded problems.

Aspects

Aspects is a lightweight AOP library on iOS. It uses the Method Swizzling technique to add additional code to an existing class or instance Method, which is convenient to use:

/// 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;
Copy the code

Aspects provides two AOP approaches, one for classes and one for instances. After determining the method of the hook, Aspects allow us to choose whether to hook before or after the method is executed, and even directly replace the method implementation. There are a lot of articles on how it works. It’s one of the few gems in the iOS open source community, and it’s a great way to understand ObjC’s messaging mechanism. But the flaw is poor performance, as the official said

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.

It is not officially recommended for use in production environments, but is usually used to mock in single tests. The performance tests on our side also confirmed this: on the real iPhone 6, a method call that loops 100w times (methods that have passed the Aspects hook) will report the Terminated due to Memory issue crash message directly.

MPSwizzler

MPSwizzler is an AOP solution used in the open source data analysis SDK MixPanel. The principle is not very complex and is based on the ObjC runtime.

  1. Support to cancel the corresponding hook at runtime, which can meet some requirement scenarios

  2. Block executes method blocks to avoid method name conflicts

    • (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) {

    / / whether the method in the list of methods in his class, rather than the parent class 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)); }Copy the code

    }

The most important one is method_setImplementation(aMethod,swizzledMethod); Where swizzledMethod matches the following functions according to the parameters of the original method:

  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)

The internal implementation of these functions is roughly the same. Take mp_swizzledMethod_4 as an example:

static void mp_swizzledMethod_4(id self, SEL _cmd, id arg, id arg2) { Method aMethod = class_getInstanceMethod([self class], _cmd); MPSwizzle *swizzle = (MPSwizzle *)[swizzles objectForKey (__bridge ID)((void *)aMethod))]; Void (*)(id, SEL, id))swizzle.originalMethod)(self, _cmd, arg, arg2); NSEnumerator *blocks = [swizzle.blocks objectEnumerator]; swizzleBlock block; While ((block = [blocks nextObject])) {block(self, _cmd, arg, arg2); }}}Copy the code

This AOP approach is also used in most SDKS, such as FBSDKSwizzler and SASwizzler, which perform much better than Aspects, but not as well as the naive Method Swizzling.

ISA-swizzle KVO

Using the runtime ISA-Swizzle principle of KVO, we can dynamically create subclasses, overwrite related methods, and add the method we want, and then call the original method in this method, so as to achieve the hook. Here’s an example of ReactiveCocoa.

internal func swizzle(_ pairs: (Selector, Any)... , key hasSwizzledKey: AssociationKey<Bool>) {// Create 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 {// Generate a new IMP by block, and add the method implementation to the generated subclass. 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 {// override the -class and +class methods of the class to hide the real subclass type 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)! ReplaceGetClass (in: subclass, decoy: in: subclass, decoy: in: subclass, decoy: perceivedClass) objc_registerClassPair(subclass) return subclass } } object_setClass(instance, subclass) instance.associations.setValue(subclass, forKey: knownRuntimeSubclassKey) return subclass } }Copy the code

_RXObjCRuntime in RxSwift provides a similar idea. Instead of creating a class yourself with objc_registerClassPair(), you can subclass it directly with KVO. For example:

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; // Call 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); Class kvoCls = object_getClass(self); Class originCls = class_getSuperclass(kvoCls); const char *originViewDidAppearEncoding = method_getTypeEncoding(class_getInstanceMethod(originCls, @selector(viewDidAppear:))); // add our own implementation growing_viewDidAppear class_addMethod(kvoCls, @selector(viewDidAppear:), (IMP)growing_viewDidAppear, originViewDidAppearEncoding); }Copy the code

This AOP scheme, which uses KVO to dynamically subclass, has minimal intrusion into the original class because it does not change the mapping between the methods and implementations of the original class, and therefore does not affect the method calls of other instances defined by the original class. This is useful in scenarios such as more accurate calculation of page load times. However, this AOP solution conflicts with other SDKS, such as homing pigeon, Firebase, and RxSwift. In RxSwift, all messaging mechanisms are unified as signals. The framework does not recommend that you use Delegate, KVO, or Notification. In particular, KVO will have exception errors.

Fishhook

The AOP solution to improve iOS has to mention the famous Fishook, which is often used to do some performance analysis or jailbreak analysis.

All of you know that ObjC’s methods Hook because of its runtime nature, ObjC’s method calls are in the form objc_msgSend(ID, SEL) at the bottom, which gives us the opportunity to swap method implementations (IMP), But the C function determines the Offset of the function pointer when compiling the link. This Offset is fixed in the compiled executable, while the starting address allocated by the system (obtained by the image List command in LLDB) changes each time the executable is reloaded into memory. The address of the running static function pointer is equal to the first address in memory of the above Offset + Mach0 file.

Since the pointer address of C function is relatively fixed and cannot be modified, how does fishhook implement the Hook of C function? It hooks only the external (shared cache) functions of Mach-o, such as NSLog, objc_msgSend and other dynamic symbol table symbols.

Fishhook takes advantage of MachO’s dynamic binding mechanism. Apple’s shared cache libraries are not compiled into our MachO files, but are rebound when dynamically linked (relying on the dynamic linker dyld). Apple uses PIC (Position-independent Code) technology to successfully make the bottom layer of C also have dynamic performance:

  • At compile time, a pointer (8 bytes of data, all zeros) is created for each referenced system C function in the symbol table in the _DATA section of the Mach-o file. This pointer is used to relocate the function implementation to the shared library during dynamic binding.

  • At run time the system C function is dynamically bound once the first time it is called, and then the corresponding pointer in the symbol table of the _DATA segment in Mach-o is pointed to the external function (its actual memory address in the shared library).

Fishhook uses PIC technology to do two things:

  • Rebind Pointers to system methods (external functions) to internal/custom C functions.

  • The pointer to the inner function is dynamically linked to the address of the system method.

Here’s the official schematic provided by Facebook:

Lazy Symbol Pointer Table –> Indirect Symbol Table –> Symbol Table –> String Table

This diagram describes how to take a string (such as “NSLog”) and find the address of the function that the pointer points to in the MachO file lazy load table step by step. We use MachOView tool to analyze this step:

_la_sysmbol_ptr This section represents a lazy Symbol pointer, where value is the resolution of a reserved field and represents the index in an Indirect Symbol Table

Use reserve1 to locate the position of section __la_symbol_ptr in the Indirect Symbols table. For example, #14 is the starting position of the __la_symbol_ptr section.

The number of symbols is calculated by sizeof(void (*)) and the sizeof the 64 bit pointer is 8 bytes, so the **__la_symbol_ptr** section has 104/8 = 13 symbols, _NSLog is just one of them.

Note the Indirect Symbols dynamic symbol table, where the Data value 0x00CO (#192) represents the symbol’s index in the symbol table

Number 192 in the symbol table is the _NSLog symbol, and the Data 0x00CE is the index in the string table

The above index 0x00CE plus the starting value of the string table 0xD2B4 is the symbol’s position in the symbol table, as shown below:

After that, the implementation of the MachO file is not very abstract. It requires a deeper understanding of the structure of the MachO file. Since fishhook can hook system static C functions, it can also hook Runtime related methods in ObjC, For example, objc_msgSend, method_getImplementation, method_setImplementation, and method_exchangeImplementations can do some interesting offensively explorations, which are commonly used in jailbreaking The MobileHooker underlayer of Cydia Substrate calls fishhook and ObjC Runtime to replace system or target application functions. The well-encapsulated Theos or MonkeyDev development tool is convenient for jailbreak hook analysis. It should be noted that Fishhook is more troublesome for the variable parameter function processing, it is not convenient to get all the variable parameters, need to use assembly to operate the stack and register. See TimeProfiler, AppleTrace for details on this section.

Thunk technology

Let’s push the camera a little further and learn about Thunk technology.

In short, a Thunk program is a block of code that does some additional computation and logic before and after calling the actual function, or that provides the ability to turn a direct call to the original function into an indirect call. A Thunk program, also known in some places as a trampoline program, does not break the stack argument structure of the original called function, but merely provides the ability to hook the original call. Thunk technology can be used in both compile-time and run-time scenarios. The main idea is that at runtime we ourselves construct an instruction in memory for the CPU to execute. For the implementation of Thunk in iOS, see the implementation principle of Thunk program and its application in iOS.

A detailed implementation of the Thunk idea can be found in the following three libraries and related blogs:

  • Stinger

  • TrampolineHook

The core will use libffi this library, the bottom is written in assembly, libfii can be understood as the implementation of C language Runtime.

Clang plugging pile

While most of the above iOS AOP solutions are run-time based, fishhook is based on the link phase. Can we implement AOP at compile stage and insert the code we want?

Clang, a built-in compiler for Xcode, provides a piling mechanism for code coverage detection. The official documentation is as follows: An application of Clang pilings can be seen in this article. In the end, it is the compiler that helps us add specific instructions at the specified location to generate the final executable file. Writing more custom pilings rules requires writing LLVM Pass by hand.

This compiler dependent AOP scheme is suitable for use with development and testing tools such as Code coverage, Code Lint, static analysis, etc.

conclusion

The above introduces the mainstream AOP schemes and some well-known frameworks in iOS, including compile time, link time and run time. From source code to program loading to memory execution, different stages of the whole process can have corresponding schemes to choose. We’ve added a few more options to our toolbox, deepened our understanding of both static and dynamic languages, and deepened our understanding of how programs move from static to dynamic.

At the same time, our Android and iOS unburied SDK 3.0 are open source. If you are interested, please follow the github repository below to learn about our latest development progress.

Android:github.com/growingio/g…

IOS:github.com/growingio/g…

About GrowingIO

GrowingIO is a leading one-stop digital growth solution provider in China. Provide customer data platform (CDP), advertising analytics, product analytics, intelligent operations and other products and consulting services for product, operations, marketing, data teams and managers to help enterprises improve data-driven capabilities and achieve better growth in the digital transformation.

Click “here” to get GrowingIO for 15 days!