preface
Recently, I feel frustrated in finding a job. I feel like a headless chicken and I feel anxious. Determined to change this state and improve themselves, then from the most simple calm down to buckle excellent source code.
Introduction of Aspects
Aspects is a lightweight aspect oriented programming (AOP) library. It provides three main pointcuts :before(executing in front of the original method)/instead(executing instead of the original method)/after(executing after the original method, default), implementing hooks through Runtime message forwarding. It supports methods to Hook an instance object. And its internal consideration of a large number of possible triggering problems and the corresponding processing to ensure security. Swizzling has a significant advantage over the Method that simply swaps two IMPs.
Look at the source code with the problem
Before reading the source code or to their first to try, generally in the process of this trial you are more or less will have some questions. When you read the source code with these questions in mind, you may have some specific points. From a specific details cut into the problem than a simple general look at the source to the effect of the good. Let me start with two questions.
1. How do Aspects Hook the methods of a particular instance object
2. How to Hook class methods for Aspects
Aspects provide two methods, an object method and a class method.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
Copy the code
In fact, Aspects provide hooks with three kinds of functions. In this case, we can use Hook method to solve the problem of Hook method
- Hook An object method for a specific instance object
- Hook An object method for all instance objects
- Hook methods
The three functions correspond to two methods, so how do we distinguish them. (In fact, if you know the object memory layout, you should be able to immediately figure out how to do it.)
Get a glimpse of the main logical structure of the code
Common source code analysis articles like to start from an interface, look at the entire code execution process from the top down, and then finally come to a conclusion or frame diagram. But I feel that this way corresponding to the reader is relatively not very friendly, sometimes readers have not a general understanding of the framework, at this time a big push source posted on the reader is a face blind. Here I first do a general introduction to the structure of the whole framework, omitting some details of the specific process.
forwardInvocation
Look back at the question
Take a look at my general flow chart above, and then go back and look at the two questions in conjunction with your knowledge of Runtime. (If you are not familiar with Runtime, please refer to link 1, link 2 and link 3 of frost God’s blog.)
Question 1
From the flowchart above, we can see that when Hook instance object is created, a new class is created, and then the isa of the current instance object points to this new class. So isa refers to two class objects. And nothing is done with the original class object.
Question 2
We all know that class methods exist in metaClass, so if you want to Hook class objects, you can just get metaClass.
Implementation details
Note the difference between [XXX class] and object_getClass(XXX)
[xxx class]
whenxxx
If it is an instance object, it returns a class object. Otherwise, it returns itself.object_getClass(xxx)
Returns the object to which the current object ISA points
OK, so now we have a little bit of an idea of the process. Now we need to dig into some details. The annotations inside Aspects are still very comprehensive
1. Protocol introduction
AspectToken
@protocol AspectToken <NSObject> // Unregister a Hook - (BOOL)remove; @endCopy the code
This is a protocol. There’s only one internal remove method. Following this protocol requires implementing the remove method to unregister the Hook.
AspectInfo
/// The first parameter of the Hook Block follows the protocol @protocol AspectInfo <NSObject> /// The current Hook object - (id)instance; // Hook the originalInvocation - (NSInvocation *) the originalInvocation; /// all method arguments - (NSArray *)arguments; @endCopy the code
The first parameter of the block we add to the Hook follows this protocol
2. Class is introduced
AspectInfo
The AspectInfo protocol follows the AspectInfo protocol above. The three properties correspond to each other on the protocol.
@interface AspectInfo : NSObject <AspectInfo>
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end
Copy the code
Let’s focus on arguments, which is the parameter fetch of a method. The internal fetch logic is that the originalInvocation is called in the NSInvocation classification method – (NSArray *) Aspects_arguments.
- (NSArray *)aspects_arguments {
NSMutableArray *argumentsArray = [NSMutableArray array];
for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) {
[argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null];
}
return [argumentsArray copy];
}
Copy the code
You can see that the logic of the above method is simply to iterate through the Arguments for methodSignature, but you must have noticed that IDx starts with 2. Look at the official document to see this sentence.
A method signature consists of one or more characters for the method return type, followed by the string encodings of the implicit arguments self and _cmd, followed by zero or more explicit arguments
So the signature of a method is made up of the return value + self + _cmd + the encodings value of the method argument but in this case the method argument starts at 3, We will see next that the specific type is obtained by the – (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx method. OK, we see this sentence again in the method documentation.
Indexes begin with 0. The implicit arguments self (of type id) and _cmd (of type SEL) are at indexes 0 and 1; explicit arguments begin at index 2.
We find that zero corresponds to self and not the return value. So it’s pretty obvious that you’re going to start with 2.
Summary: AspectInfo is mainly for saving and encapsulating NSInvocation.
AspectIdentifier
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end
Copy the code
From the initialization method of this class, we can see that this class mainly saves some information of Hook, the execution time of Hook method parameters and so on. The important part of this class is how to resolve the blockSignature of the passed Block. Mainly through the following methods.
static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
AspectBlockRef layout = (__bridge void *)block;
if(! (layout->flags & AspectBlockFlagsHasSignature)) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
void *desc = layout->descriptor;
desc += 2 * sizeof(unsigned long int);
if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
desc += 2 * sizeof(void *);
}
if(! desc) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
const char *signature = (*(const char **)desc);
return [NSMethodSignature signatureWithObjCTypes:signature];
}
Copy the code
In the above method we notice the AspectBlockRef structure, defined as follows.
typedef struct _AspectBlock { __unused Class isa; AspectBlockFlags flags; __unused int reserved; void (__unused *invoke)(struct _AspectBlock *block, ...) ; struct { unsigned long int reserved; unsigned long int size; // requires AspectBlockFlagsHasCopyDisposeHelpers void (*copy)(void *dst, const void *src); void (*dispose)(const void *); // requires AspectBlockFlagsHasSignature const char *signature; const char *layout; } *descriptor; // imported variables } *AspectBlockRef;Copy the code
Let’s go back to the method logic. Take the pointer to the descriptor, offset the signature in the structure by 2 * sizeof(unsigned long int), and then determine whether to include Copy and Dispose (Copy copies Block from stack to heap). The Dispose function is to dispose of functions on the heap when they are discarded. If it is included, offset 2 * sizeof(void *) to get signature’s location. After I got blockSignature, I checked it.
static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
NSCParameterAssert(blockSignature);
NSCParameterAssert(object);
NSCParameterAssert(selector);
BOOL signaturesMatch = YES;
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
}else {
if (blockSignature.numberOfArguments > 1) {
const char *blockType = [blockSignature getArgumentTypeAtIndex:1];
if(blockType[0] ! =The '@') {
signaturesMatch = NO;
}
}
// Argument 0 is self/block, argument 1 is SEL or id<AspectInfo>. We start comparing at argument 2.
// The block can have less arguments than the method, thats ok.
if (signaturesMatch) {
for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) {
const char *methodType = [methodSignature getArgumentTypeAtIndex:idx];
const char *blockType = [blockSignature getArgumentTypeAtIndex:idx];
// Only compare parameter, not the optional type data.
if(! methodType || ! blockType || methodType[0] ! = blockType[0]) { signaturesMatch = NO;break; }}}}if(! signaturesMatch) { NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature];
AspectError(AspectErrorIncompatibleBlockSignature, description);
return NO;
}
return YES;
}
Copy the code
Select id< AspectInfo > from block where parameter = id< AspectInfo >
Summary: AspectIdentifier is a Hook specific content. It contains the specific information of a single Hook, including the execution time and the specific information needed to execute the block, including method signature, parameters, and so on.
AspectsContainer
@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end
Copy the code
Summary: This class is very simple, save some Hook AspectIdentifier.
AspectTracker
@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, readonly) NSString *trackedClassName;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;
- (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName;
- (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName;
@end
Copy the code
Summary: The main purpose of this class is to track the selector of each class Hook. Make sure that only one class in an inheritance chain hooks the method.
3. Specific process
There are only two apis that are exposed through header files
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
returnaspect_add((id)self, selector, options, block, error); } / / / @return A token which allows to later deregister the aspect.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add(self, selector, options, block, error);
}
Copy the code
Both oF these apis end up with the same method.
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 (^{// The lock is thread safeif(aspect_isSelectorAllowedAndTrack(self, selector, options, Error)) {// Determine whether you can Hook // get the AspectIdentifier container AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); / / according to the selector self options generated AspectIdentifier block identifier = [AspectIdentifier identifierWithSelector: the selector object:self options:options block:block error:error];if(identifier) {// add the generated AspectIdentifier to the container [aspectContainer addAspect:identifier withOptions:options]; / / processing aspect_prepareClassAndHookSelector (self, the selector, error); }}});return identifier;
}
Copy the code
Here we focus on aspect_prepareClassAndHookSelector this method.
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if(! aspect_isMsgForwardIMP(targetMethodIMP)) { // Make a methodalias 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)); }}Copy the code
Class klass = aspect_hookClass(self, error); Look at the logic below. The main operation is to point the current selector to _objc_msgForward, so when you call the method, you skip the previous process of finding IMP through ISA, and just forward the message. Finally, let’s look at the core aspect_hookClass method.
static Class aspect_hookClass(NSObject *self, NSError **error) { NSCParameterAssert(self); Class statedClass = self.class; Class baseClass = object_getClass(self); NSString *className = NSStringFromClass(baseClass); / / class objectif ([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.
}else if(statedClass ! = baseClass) {returnaspect_swizzleClassInPlace(baseClass); } / / instance objects 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;
}
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
object_setClass(self, subclass);
return subclass;
}
Copy the code
We can see from the logic of the method
1. The class object
It calls the aspect_swizzleClassInPlace method, The main operation for this method is class_replaceMethod(klass, @selector(forwardInvocation), (IMP)__ASPECTS_ARE_BEING_CALLED__, “v@:@”); That is to replace the message forwarding method with our own __ASPECTS_ARE_BEING_CALLED__ method.
2. Instance objects
If you’re familiar with the underlying implementation of KVO, you know the ISA mash-up, where we sneaky generate a new class object, and then we do the same thing to the class object as 1, and we Hook the class method to make it look like we didn’t do it. Finally, we point the instance object’s ISA pointer to this object. If you are interested, you can follow this logic and try to implement KVO yourself.
Finally, when we call the method it is forwarded directly, and the forwardInvocation has been replaced by our __ASPECTS_ARE_BEING_CALLED__. So the final implementation logic will be in this method.
The last
Here I just made a very shallow introduction to the Aspects, I hope it can be helpful to you, and please give me more advice ~