Using method_swizzle + NSProxy to solve the problem of full buried points in App.
Then I learned about NSProxy, the virtual base class, and found that NSProxy could be used to solve the problem of timer cyclic reference, solve the problem of OC multiple inheritance, and make the framework have better expansibility.
Immediately open YYWeakProxy in the project to have a look, and find that it is achieved by message forwarding. In my walnut-sized head, the message forwarding is still stuck in the interception of resolving the crash when the in-app method is not implemented.
An immediate search for the purpose of message forwarding turned up two interesting frameworks:
- CTMediator solves the componentization problem
- Aspects are used for iOS hooks
Ps: All are old frame, do not hinder learning.
This paper is the study of Aspects framework.
1. Sample Demo:
The following example comes from The Aspects Demo:
// ViewController.m
@interface** ViewController (a)
@end
@implementation** ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIViewController *testController = [[UIImagePickerController alloc] init];
testController.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentViewController:testController animated:YES completion:NULL];
[testController aspect_hookSelector: @selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info, BOOL animated) {
NSLog(@"Popped Hello from Aspects", [info instance]);
} error:NULL];
}
Copy the code
2. Method list
/** 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. Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe. */
@interface NSObject (Aspects)
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @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;
/// 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;
@end
Copy the code
Aspects is a class of NSObject, with only two methods, a class method and an instance method, and the invocation of instance methods is investigated using the code in the Demo.
3. Call the process
Describes the process of calling the master. The process of destroying is not included in this paper.
4. Code analysis
Following the code analysis, most of the understanding is commented out in the code.
1, the entry method aspect_add
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
NSCParameterAssert(self);
NSCParameterAssert(selector);
NSCParameterAssert(block);
__block AspectIdentifier *identifier = nil;
Os_unfair_lock = os_unfair_lock
aspect_performLocked(^{
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
// Load or create the aspect container.
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
// Create an AspectIdentifier object
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
// Stores AspectIdentifier objects classified by options
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
// Modify the class to allow message interception.aspect_prepareClassAndHookSelector(self, selector, error); }}});return identifier;
}
Copy the code
2, whether the method can hook aspect_isSelectorAllowedAndTrack
Current method of filtering the retain, release, autorelease, forwardInvocation: can’t hook method, and the hook before dealloc method can only be destroyed.
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
/// These methods do not allow hooks
disallowedSelectorList = [NSSet setWithObjects:@"retain"The @"release"The @"autorelease"The @"forwardInvocation:", nil];
});
// Check against the blacklist.
// disallowedSelectorList returns NO and does not allow hooks
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}
// Additional checks.
// Check that the dealloc method can only hook before destruction.
AspectOptions position = options&AspectPositionFilter;
if ([selectorName isEqualToString:@"dealloc"] && position ! = AspectPositionBefore) { NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc.";
AspectError(AspectErrorSelectorDeallocPosition, errorDesc);
return NO;
}
// Check the method when implemented, not implemented hook, no meaning.
if(! [self respondsToSelector:selector] && ! [self.class instancesRespondToSelector:selector]) { NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName];
AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
return NO;
}
// Search for the current class and the class hierarchy IF we are modifying a class object
// If we are modifying a class object, search the current class and its hierarchy
if (class_isMetaClass(object_getClass(self))) {
/// enter the hook class method
Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];
do {
/// prevent repeated hooks
AspectTracker *tracker = swizzledClassesDict[currentClass];
if ([tracker.selectorNames containsObject:selectorName]) {
// Find the topmost class for the log.
if (tracker.parentEntry) {
AspectTracker *topmostEntry = tracker.parentEntry;
while (topmostEntry.parentEntry) {
topmostEntry = topmostEntry.parentEntry;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}else if (klass == currentClass) {
// Already modified and topmost!
returnYES; }}}while ((currentClass = class_getSuperclass(currentClass)));
// Add the selector as being modified.
currentClass = klass;
AspectTracker *parentTracker = nil;
do {
// add the dictionary to the parent
AspectTracker *tracker = swizzledClassesDict[currentClass];
if(! tracker) { tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker]; swizzledClassesDict[(id<NSCopying>)currentClass] = tracker; } [tracker.selectorNames addObject:selectorName];// All superclasses get marked as having a subclass that is modified.
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
}
return YES;
}
Copy the code
3. Get aspect_getContainerForObject
// Loads or creates the aspect container.
// Load or create the aspect container.
static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
NSCParameterAssert(self);
// Prefix methods with aspects_ xx
SEL aliasSelector = aspect_aliasForSelector(selector);
// initialize the AspectsContainer object, associate it with the current object,
AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
if(! aspectContainer) { aspectContainer = [AspectsContainernew];
objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
}
return aspectContainer;
}
Copy the code
4. Initialize AspectIdentifier
In this method, just focus on aspect_blockMethodSignature to get the block’s method signature.
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {
NSCParameterAssert(block);
NSCParameterAssert(selector);
// get the block's method signature
NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc.
/// verify that the parameters in the block are compatible with the parameters in the hook method
if(! aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {return nil;
}
/// Build identifier objects
AspectIdentifier *identifier = nil;
if (blockSignature) {
identifier = [AspectIdentifier new];
identifier.selector = selector;
identifier.block = block;
identifier.blockSignature = blockSignature;
identifier.options = options;
identifier.object = object; // weak
}
return identifier;
}
Copy the code
4.1 Obtaining the block’s method signature aspect_blockMethodSignature
In this method, focus on the first line AspectBlockRef layout = (__bridge void *)block; , Bridges the block to facilitate the value.
The bridge structure is as follows:
// Block internals.
typedef NS_OPTIONS(int, AspectBlockFlags) {
AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
AspectBlockFlagsHasSignature = (1 << 30)};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
Source code implementation:
// get the block's method signature
static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
// It is possible to bridge blocks because of their internal structure.
/ / Block that you are like (a) https://juejin.cn/post/6955409380321787940
/ / Block, the original you are such a (2) https://juejin.cn/post/6956895256998576164
AspectBlockRef layout = (__bridge void *)block;
// Generate rules internally according to flags. Check BlockFlagsHasSignature returns nil if no block method is signed
if(! (layout->flags & AspectBlockFlagsHasSignature)) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
/// Get the first address of the descriptor
void *desc = layout->descriptor;
/** 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; * /
// Offset by 2 int units (pointer over unsigned long int reserved; And unsigned long int size;)
desc += 2 * sizeof(unsigned long int);
/ / void * if BlockFlagsHasCopyDisposeHelpers deviation of 2 units (pointer through the void (* copy) and void * the dispose ())
if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
desc += 2 * sizeof(void *);
}
// Current pointer to const char *signature;
// If the pointer address does not exist. If signature does not exist, return nil
if(! desc) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
// Return NSMethodSignature object
const char *signature = (*(const char **)desc);
return [NSMethodSignature signatureWithObjCTypes:signature];
}
Copy the code
If you don’t understand how the bridge works, take a look at these two articles, which examine the internal structure of the Block.
- So this is what you look like (1)
- Block so you look like this (two)
5, the parameters in the test block, and the parameters of the hook method is compatible aspect_isCompatibleBlockSignature
/// verify that the parameters in the block are compatible with the parameters in the hook method
static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
NSCParameterAssert(blockSignature);
NSCParameterAssert(object);
NSCParameterAssert(selector);
// The signature matches
BOOL signaturesMatch = YES;
// get the method signature that requires hook SEL
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
/** * example: hook the viewWillDisappear * ArgumentTypeAtIndex * 1: self * 2: SEL * 3: bool (animated) * * blockSignature: ArgumentTypeAtIndex * 1: self * 2: AspectInfo * 3: Bool (animated) * If the hook method itself has only 2 arguments such as: Dealloc (self,SEL) * BlockSignature three Argument (self, SEL, bool) * if such blockSignature numberOfArguments > methodSignature. NumberOfArguments It doesn't match. * So this method allows blockSignature to be less than the arguments for methodSignature (some not), but no more. * /
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
}else {
if (blockSignature.numberOfArguments > 1) {
/// We need to force the second argument to be AspectInfo
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, that's ok.
// Argument 0 is self/block, and argument 1 is SEL or ID. Let's start with index = 2.
// Blocks can have fewer arguments than methods, which is fine.
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.
// This allows blockSignature to be small, but does not allow skipping arguments.
// For example, the hook method has 4 arguments (self,SEL,Bool,Int)
// Block can (self,SEL,Bool) not (self,SEL,Int)
if(! methodType || ! blockType || methodType[0] != blockType[0]) {
signaturesMatch = NO;
break; }}}}/// Return No if there is No print error
if(! signaturesMatch) { NSString *description = [NSString stringWithFormat:@"Blog signature %@ doesn't match %@.", blockSignature, methodSignature];
AspectError(AspectErrorIncompatibleBlockSignature, description);
return NO;
}
return YES;
}
Copy the code
See here NSMethodSignature * methodSignature = [[object class] instanceMethodSignatureForSelector: selector]; To acquiring hook SEL method signature found that when using the instanceMethodSignatureForSelector, so in need of hook type method, requires the incoming class. The metaClass.
6, changing the class to allow message interception aspect_prepareClassAndHookSelector
This method is the core method, implemented using ISA_swizzle.
Soul painting, never mind.
A brief introduction to the process:
There is a class MyClass that derives an object myObj and now needs the hook helloMethod method.
- Aspect framework through
aspect_hookClass
Method to generate aMyClass
A subclass ofMyClass_Aspect_
- will
myObj
的isa
Point to theMyClass_Aspect_
- To obtain
MyClass
helloMethod
methodsIMP
- to
MyClass_Aspect_
Add method toaspect_helloMethod
Method, and willIMP
Point to thehelloMethod
methodsIMP
(Actually copyhelloMethod
Method implementation) - Replace or add
MyClass_Aspect_
Of a list of methods in a classforwardInvcation
To let itIMP
Points to the Aspect framework’s own implementation__ASPECTS_ARE_BEING_CALLED__
Self handling method forwarding - Replace or add
MyClass_Aspect_
Of a list of methods in a classhelloMethod
Method, let itIMP
Point to the_objc_msgForward
When you call MyClass to derive the instance method helloMethod of myObj, you’re actually calling the helloMethod method in MyClass_Aspect_.
But helloMethod’s IMP is _objc_msgForward and calls the Aspect framework’s own implementation of IMP __ASPECTS_ARE_BEING_CALLED__.
/// prepare the class and hook selector
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
Class klass = aspect_hookClass(self, error);
/// klass is a subclass of the original class. So you can get the method of the parent class
Method targetMethod = class_getInstanceMethod(klass, selector);
/// get method implementation
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);
/// Add the prefix AspectsMessagePrefix_ to the method
SEL aliasSelector = aspect_aliasForSelector(selector);
/// If the new class klass does not implement aliasSelector
if(! [klass instancesRespondToSelector:aliasSelector]) {/// klass class added AspectsMessagePrefix_xxx copy XXX implementation
__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);
}
/** * Klass (Class_Aspects_) * aspects__viewWillDisappear (add in aspect_prepareClassAndHookSelector. Class_addMethod) * class (in Added at aspect_hookedge class) * forwardInvocation (added at aspect_hookClass -> aspect_swizzleForwardInvocation) */
// We use forwardInvocation to hook in.
// Add the original selector method to the subclass and make the IMP point to _objc_msgForward.
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
* viewWillDisappear: IMP -> _objc_msgForward * aspects__viewWillDisappear: Imp -> viewWillDisappear * class IMP -> Original CLS * forwardInvocation: IMP -> __ASPECTS_ARE_BEING_CALLED__ */}}Copy the code
6.1 Dynamically generate a subclass aspect_hookClass
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
/// object_getClass is the class of the object if self is an object, or the metaclass if self isa class
/// class self takes the class for the object, if self is the class, returns itself.
// if self is the object, then statedClass == baseClass
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
// Already subclassed
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// We swizzle a class object, not a single object.
// We are mixing a class object instead of 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) {return aspect_swizzleClassInPlace(baseClass);
}
// Default case. Create dynamic subclass.
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
// Create a class dynamically
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {
// Create failed, return null
NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
return nil;
}
// add the forwardInvocation method
aspect_swizzleForwardInvocation(subclass);
/** * object_getClass = 'isa'; /** * The difference between object_getClass(obj) and [obj class] is two: * 1, if object_getClass is an instance object, they are the same; * 2, if it isa class object, class is self and object_getClass is isa */
// class derived object gets class object,
aspect_hookedGetClass(subclass, statedClass);
// Prevent a class object derived from a metaclass from getting a metaclass
aspect_hookedGetClass(object_getClass(subclass), statedClass);
/// dynamically register classes
objc_registerClassPair(subclass);
}
// change the isa pointer to our subclass
object_setClass(self, subclass);
return subclass;
}
Copy the code
6.2 adding aspect_swizzleForwardInvocation
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
// If there is no method, replace will act like class_addMethod.
// If there is no method, replace plays a role similar to class_addMethod.
// class_replaceMethod: if there is a method to replace, replace the IMP of the original method and return the IMP of the original method; if there is no original method, add the method dynamically and return nil
// if the forwardInvocation is implemented, the IMP will be replaced with __ASPECTS_ARE_BEING_CALLED__
// If not, add a forwardInvocation IMP to __ASPECTS_ARE_BEING_CALLED__
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
// determine if forwardInvocation is implemented
if (originalImplementation) {
// Add an __aspects_forwardInvocation method,
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
Copy the code
6.3 Change the aspect_hookedGetClass returned by @selector(class)
This method is used in the iOS framework KVO, and a subclass is derived for keyvalue listening, but calling the class method and objc_getClass() method return different results.
static void aspect_hookedGetClass(Class class, Class statedClass) {
NSCParameterAssert(class);
NSCParameterAssert(statedClass);
Method method = class_getInstanceMethod(class, @selector(class));
IMP newIMP = imp_implementationWithBlock(^(id self) {
return statedClass;
});
/// Add the class method and return the previous class name
class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method));
}
Copy the code
7. Aspects’ own message forwardingASPECTS_ARE_BEING_CALLED
This method is to get the object message forward, handle the Before hooks call block, and then handle the Instead hooks if there are method replacements. If there is no method replacement, loop back to the parent class for the calling method, and finally handle After hooks, which are destroyed.
// This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
NSCParameterAssert(self);
NSCParameterAssert(invocation);
SEL originalSelector = invocation.selector;
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
invocation.selector = aliasSelector;
/// handle the object's hook
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
/// handle the hooks of the class
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
NSArray *aspectsToRemove = nil;
// Before hooks.
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
// call the original method if there is no replacement method
Class klass = object_getClass(invocation.target);
do {
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];
break; }}while(! respondsToAlias && (klass = class_getSuperclass(klass))); }// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if(! respondsToAlias) { invocation.selector = originalSelector; SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else{ [self doesNotRecognizeSelector:invocation.selector]; }}// Remove any hooks that are queued for deregistration.
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
Copy the code
5, conclusion
This is the end of the Aspects analysis. This paper only analyzes the hook process of Aspects, and does not list the destruction process analysis of Aspects. If you are interested, you can download the source code.
And finally, happy New Year’s Day 2022.