preface

Aspects is an old iOS AOP library that manually triggers message forwarding by replacing the old method function pointer with _objc_msgForward or _objc_msgForward_stret. Also replace the function pointer from the Hook class -(void)forwardInvocation (NSInvocation *)invocation with the argument aligned C function __ASPECTS_ARE_BEING_CALLED__(NSObject) * Self, SEL Selector, NSInvocation * Invocation), in which the invocation invocation executes the original method and several sections before and after the invocation block.

Stinger is an open source AOP library from Ele. me that does not use manual message forwarding. Parses the original method signature and uses ffi_cloSURE_alloc in libffi to construct a “function” — _stingerIMP that matches the original method parameter, replacing the original method function pointer; In addition, the parameter templates CIF and blockCif for the call to the original method and Block are generated. Void st_FFi_function (ffi_cif *cif, void *ret, void **args, void *userdata); The original method implementation and slice block are called based on CIF mainly through FFi_call.

The API of both libraries is similar in that both hook instance methods and hook methods are supported (Aspects support is not perfect. If the Hook invocation is followed by another instance method, the instance method will crash because of the hook forwardInvocation metaclass: This class has no hook), add multiple section code blocks; It also supports method level hooks for a single instance object.

Recently, Stinger released version 0.2.8, which supports the argument and return value of the hook method as a structure; All facets Block execution is also several times faster from message sending to original method implementation (PS: Previous versions were also several times faster than Aspects 😀😁). This article is throwing a dagger at Aspects. How much faster can Stinger end up than Aspects? Look at the following test.

Speed test

1. Equipment and environment

  • Test device: iPhone 7, iOS 13.2
  • Xcode: Version 11.3 (11C29)
  • Stinger:https://github.com/eleme/Stinger 0.2.8
  • Aspects:https://github.com/steipete/Aspects 1.4.1

2. Test scenario

For an empty method, hook the method by adding an empty section Block before and after the method. Execute this method 1000000 times.

3. Test method

MeasureBlock :(XCT_NOESCAPE void (^)(void))block in Xcode unit test 10 times in release mode for each case, record each execution time in seconds, and calculate the average.

4.Test Case

Case 0: “PI”

To reduce unnecessary impact, let’s measure the execution time of the “skin” of the for loop executed 1,000,000 times.

The test code
- (void)testBlank {
  [self measureBlock:^{
    for (NSInteger i = 0; i < 1000000; i++) {
    }
  }];
}
Copy the code
The test results

AVG 1 2 3 4 5 6 7 8 9 10
0.000114 0.000175 0.000113 0.000113 0.000104 0.000153 0.000102 0.0000999 0.0000936 0.000094 0.000094

It can be seen that the execution time of the for loop for 1,000,000 times is on the order of 0.0001s, which has almost no impact on the subsequent test results.

Now, let’s test the actual case.

* Extra code preparation

Start by listing the code for the class being tested. Here we create a new class that implements some empty methods.

@interface TestClassC : NSObject - (void)methodBeforeA; - (void)methodA; - (void)methodAfterA; - (void)methodA1; - (void)methodB1; - (void)methodA2; - (void)methodB2; - (void)methodA3:(NSString *)str num:(double)num rect:(CGRect)rect; - (void)methodB3:(NSString *)str num:(double)num rect:(CGRect)rect; . @end @implementation TestClassC - (void)methodBeforeA { } - (void)methodA { } - (void)methodAfterA { } - (void)methodA1 { } - (void)methodB1 { } - (void)methodA2 { } - (void)methodB2 { } - (void)methodA3:(NSString *)str num:(double)num rect:(CGRect)rect { } - (void)methodB3:(NSString *)str num:(double)num rect:(CGRect)rect { } ... @endCopy the code

Case1: A hook for a method of a particular class

Here, Stinger and Aspects are used to add a section block before and after the instance methods of TestClassC class – (void)methodA1 – (void)methodB1 respectively. Measure the time for the instance object to execute the method 1000000 times.

The test code

Stinger

- (void)testStingerHookMethodA1 {
  [TestClassC st_hookInstanceMethod:@selector(methodA1) option:STOptionBefore usingIdentifier:@"hook methodA1 before" withBlock:^(id<StingerParams> params) {
     }];
  [TestClassC st_hookInstanceMethod:@selector(methodA1) option:STOptionAfter usingIdentifier:@"hook methodA1 After" withBlock:^(id<StingerParams> params) {
  }];
  
  TestClassC *object1 = [TestClassC new];
  [self measureBlock:^{
    for(NSInteger i = 0; i < 1000000; i++) { [object1 methodA1]; }}]; }Copy the code

Aspects

- (void)testAspectHookMethodB1 {
  [TestClassC aspect_hookSelector:@selector(methodB1) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> params) {
   } error:nil];
  [TestClassC aspect_hookSelector:@selector(methodB1) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> params) {
  } error:nil];
  
  TestClassC *object1 = [TestClassC new];
  [self measureBlock:^{
    for(NSInteger i = 0; i < 1000000; i++) { [object1 methodB1]; }}]; }Copy the code
The test results

Stinger

AVG 1 2 3 4 5 6 7 8 9 10
0.283 0.368 0.273 0.277 0.273 0.271 0.271 0.272 0.271 0.273 0.270

Aspects

AVG 1 2 3 4 5 6 7 8 9 10
6.135 6.34 6.19 6.12 6.19 6.11 6.1 6.12 6.12 6.09 6.1
conclusion

In this case, Stinger is executing 21 times faster than Aspects.

In this case, we tested the Hook of the method without any parameters. In other cases, we also tested the case with parameters and no return value, no parameters and return value, and the case with parameters and return value. Stinger is executing 15-22 times faster than Aspects. For more cases, please see: github.com/eleme/Sting…

Case2: A hook for a method on a particular instance object

Here, use Stinger and Aspects respectively to add a section block before and after the instance method – (void)methodA2 – (void)methodB2 of an instance of TestClassC. Measure the time for the instance object to execute the method 1000000 times.

The test code

Stinger

- (void)testStingerHookMethodA2 {
  TestClassC *object1 = [TestClassC new];
  [object1 st_hookInstanceMethod:@selector(methodA2) option:STOptionBefore usingIdentifier:@"hook methodA2 before" withBlock:^(id<StingerParams> params) {
     }];
  [object1 st_hookInstanceMethod:@selector(methodA2) option:STOptionAfter usingIdentifier:@"hook methodA2 After" withBlock:^(id<StingerParams> params) {
  }];
  
  [self measureBlock:^{
    for(NSInteger i = 0; i < 1000000; i++) { [object1 methodA2]; }}]; }Copy the code

Aspects

- (void)testAspectHookMethodB2 { TestClassC *object1 = [TestClassC new]; [object1 aspect_hookSelector:@selector(methodB2) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> params) { } error:nil]; [object1 aspect_hookSelector:@selector(methodB2) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> params) { }  error:nil]; [self measureBlock:^{for(NSInteger i = 0; i < 1000000; i++) { [object1 methodB2]; }}]; }Copy the code
The test results

Stinger

AVG 1 2 3 4 5 6 7 8 9 10
0.547 0.567 0.546 0.543 0.556 0.543 0.542 0.545 0.54 0.544 0.542

Aspects

AVG 1 2 3 4 5 6 7 8 9 10
6.261 6.32 6.24 6.34 6.25 6.25 6.23 6.24 6.26 6.23 6.24
conclusion

In this case, Stinger is executing 11 times faster than Aspects.

Case3: method – swizzing

This simulation uses method-Swizzing to call one method before and one method after the TestClassC instance method- (void)methodA. Measure the time for the instance object to execute the method 1000000 times.

The test code
- (void)testMethodA {
  TestClassC *object1 = [TestClassC new];
  [self measureBlock:^{
    for(NSInteger i = 0; i < 1000000; i++) { [object1 methodBeforeA]; [object1 methodA]; [object1 methodAfterA]; }}]; }Copy the code
The test results

AVG 1 2 3 4 5 6 7 8 9 10
0.015 0.0219 0.0149 0.0149 0.0141 0.0148 0.0153 0.0147 0.013 0.0146 0.0116
conclusion

This case, the original method-Swizzing is about 18 times faster than Stinger’s execution speed; Is about 409 times faster than Aspects;

4. Test conclusions

  • In class-specific hooks, Stinger is about 15 to 22 times faster than Aspects from the time a message is sent to the time the original implementation and before and after section blocks are executed.
  • In hooks for specific instance objects, Stinger is about 10 times faster than Aspects from sending a message to executing the original implementation and before and after section blocks.
  • Unsurprisingly, the naive Method-Swizzing is faster than either AOP library.

Analyze Aspects and Stinger’s speed

Analysis methods

Similar to the above case, the HooK null method adds an empty section block before and after it, and executes 1000000 times, using time profile analysis in instrument (hiding system functions and inverting call stack).

Aspects

Above, method calls 1000000 times, statistics from message sent to the original method and from message sent to the execution of the original implementation and before and after the section block, the average cost of 6.135s, the profile results screenshot below:

Continue to unfold:

From above, several reasons affecting the execution speed of Aspects can be analyzed according to the proportion

  1. Message forwarding is performed when the hook method is called.
  2. static SEL aspect_aliasForSelector(SEL selector)In theAspectsMessagePrefixGet the SEL prefix
  3. - (BOOL)invokeWithInfo:(id<AspectInfo>)infoInvocation creation, execute.
  4. static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation)Create temporary variables in invotion.

Among them, 2 and 4 can be optimized 😀. Now look at Stinger.

Stinger

In the above, the method calls 1000000 times, and the average cost from sending the message to the original method and from sending the message to executing the original implementation and before and after the section block is less than 0.3s. Here is the result screenshot of profile:

A:

Compared with Aspects: Time saved in

  1. The original method finally does not go message forwarding, go normal function pointer search, call.
  2. The deposit_st_Prefix SEL to avoid heavy computation acquisition;
  3. Use FFi_call to invoke the original method implementation and block whenever possible.
  4. To avoid theNS_INLINE void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)Generate large temporary objects in The delay generation Invocation may be invoked by the user in the INSTEAD block as a parameter;
  5. Direct variables reference parameters, without getters; Try not to use OC message to obtain other parameters, save in advance, such as the number of parameters;
  6. Internalize other functions as much as possible.

Method swizzling/Aspects/Stinger contrast

Compare the item swizzling Aspects Stinger
speed Fast 😁 Slow 😭 Very fast 😀
Api friendliness Very poor 😭 Very good 😁 Very good 😁
Type of hook Support 😀 Support 😀 Support 😀
The hook of the instance object Does not support 😭 Support 😁 Support 😁
Change selector when calling the original method Modify 😭 Modify 😭 Do not change the 😁(ffi_call or invokeUsingIMP:)
Methods may have name conflicts Will 😭 Not 😁 Not 😁
Compatible with other hook methods (RAC, JSPactch..) Compatible 😁 Incompatible 😭 Compatible 😁
Support multithreading to increase hook Self lock 🙄 Support 😀 Support 😀
Hook predictability, traceability Very poor 😭 Ok 🙂 Very good 😀
Modify the parent method implementation May 😭 Not 😀 Not 😀
. . . .

So please use Stinger(github.com/eleme/Sting…) Ah, faster and safer implementations of AOP, efficient execution of raw method implementations and sliced code, significantly improved code structure; The instance hook can also be used to meet application scenarios such as KVO/RACObserve/rac_signalForselector.

Thanks for watching, please point out any mistakes!