preface

Function stabilization and throttling are not new concepts, they are very common in the front end, and they are very common in the interview. Search for “front-end function stabilization” and you will find a lot of articles.

On iOS, by contrast, you don’t see many introductions.

The first time I knew about function stabilization and throttling was when I did the exchange project in 2018:

The scenario was to update trading data in real time.

Trade order data changes very frequently, every time to refresh, obviously is not a good method.

Moreover, data can not be lost directly. The conventional frequency limiting strategy of “first execution, later discard” cannot meet the demand.

At that time, the strategy should meet the following conditions:

  • For a certain amount of time, the merge is triggered once, and the data is up to date when the trigger.

Because the code implementation problem, and the big guy consult.

After explaining the purpose, he said as soon as he heard, isn’t this function shaking and throttling? Very common on the front end..

Well… So I already have a front end? I have worked for two years to know, and learned a new posture, good food is not afraid of late.

And I found that this concept, not only the front end, but also the back end can be applied, and even the TCP traffic control policy, is part of function stabilization.

What are function stabilization and throttling

We’ve explained why we use function stabilization and throttling, now let’s talk about what they are.

Many articles have mentioned a demonstration of the website debounce&Throttle, which simulated several mouse movement event calls, with colored vertical lines, representing a function execution.

The general effect is as follows:

  • regularRepresents the result of a direct call to a function in the normal case, when no restrictions are imposed.
  • deboundceStands for shaking, and you can see that if a function is called all the time, it doesn’t execute immediately. Instead, it executes once after a certain period of time, when there are no new calls to the function.
  • throttleThrottling: throttling is performed only once within a certain period of time

👾 stabilization (Debounce)

In the case of stabilization, it’s a little bit like a person who is extremely appreciative of the opportunity to execute, and as long as there is a task in the time frame, wait a little longer.

Wait until the last time, after a certain time, to make sure that there is no new task, to do the execution.

Some people think it is like the driver of a black car, others describe it as an elevator at work, but the black car or elevator will leave when full capacity.

And I think it’s like a patient monster, wait until all the food is gone, make sure there is no new food, then open its big mouth, a net.

🐯 throttling (Throttle)

Throttling is easy to understand. For a certain period of time, all other triggers are discarded and the execution is done once.

Usage scenarios

Use scenarios for function throttling:

  • Prevent multiple clicks

  • Repeat multiple network requests

    And so on..

In fact, the simplest way to achieve function throttling, only with the timestamp comparison, you can do, you generally write:

if((now-last)<time){
  return;
}
last = now;
//do something

Copy the code

Many people have already used it, but they just don’t know the name.

And special throttling needs:

During the time period, only the last trigger is executed and all previous triggers are discarded.

The application scenario encountered is that the message queue changes data over time and is delivered in batches the last time.

Function stabilization, I see the use scenarios:

  • List refresh, to avoid repeated reload in a short period of time, can be merged multiple times
  • TCP Flow Control
  • Live the full screen game interface of the room, click once to appear the control tool, and within a certain period of time, click many times not to hide the tool. After the time passes, the automatic hiding is performed

Ready-made wheels – MessageThrottle

As usual, it’s time to show up your code and do it.

However, iOS has already been implemented wheel, do not repeat the wheel, you can use directly.

It is found that MessageThrottle is a relatively complete implementation, and applied in hand Q, the quality is more reliable. Recommend it.

MessageThrottle use

It’s simple to use:

Stub *s = [Stub new]; MTRule *rule = [MTRule new]; rule.target = s; // You can also assign `Stub.class` or `mt_metaClass(Stub.class)` rule.selector = @selector(foo:); Rule. DurationThreshold = 0.01; [MTEngine.defaultEngine applyRule:rule]; // or use `[rule apply]`Copy the code

The main thing is the setting of MTRule, which determines which mode and how much time we will control method calls.

MessageThrottle analysis

Don’t reinvent the wheel, but understand what it looks like. Of course, if not interested, it is enough to see the use.

Messagethrottl. h and messagethrottl. m are the only files in the library.

The main idea is: hook the method of throttling and shaking, and then do unified processing.

In fact, inside can learn a lot of points, here is only a general introduction.

Main design ideas

Cite the author’s own diagram illustrating the main class relationships, with the dashed lines representing weak references:

NSMapTable stores data

MTEngine uses NSMapTable to store management data with target as key and selector array as value.

One feature of NSMapTable is that it supports any pointer as Key and does not need to hold it. NSMapTable also automatically removes data with nil keys or values.

Remove a rule from an associated object

A key design point is to associate the MTDealloc object to the target using an associated object:

- (MTDealloc *)mt_deallocObject
{
    MTDealloc *mtDealloc = objc_getAssociatedObject(self.target, self.selector);
    if(! mtDealloc) { mtDealloc = [MTDealloc new]; mtDealloc.rule =self;
        mtDealloc.cls = object_getClass(self.target);
        objc_setAssociatedObject(self.target, self.selector, mtDealloc, OBJC_ASSOCIATION_RETAIN);
    }
    return mtDealloc;
}
Copy the code

The benefits of associated object design are:

When target is freed, the associated objects are also cleared, so MTDealloc objects are also freed, so that the rule is automatically removed when target is freed.

Discard in MTDealloc dealloc

- (void)dealloc
{
    SEL selector = NSSelectorFromString(@"discardRule:whenTargetDealloc:");
    ((void(*) (id, SEL, MTRule *, MTDealloc *))[MTEngine.defaultEngine methodForSelector:selector])(MTEngine.defaultEngine, selector, self.rule, self);
}
Copy the code

Inside the call to write a bit SAO… It is:

[MTEngine.defaultEngine discardRule:self.rule whenTargetDealloc:self];
Copy the code

Core processing logic in message forwarding

The core processing of the entire library is in the MT_handler Invocation:

/** Processing NSInvocation @param INVOCATION NSInvocation @param rule MTRule object */
static void mt_handleInvocation(NSInvocation *invocation, MTRule *rule)
{
    NSCParameterAssert(invocation);
    NSCParameterAssert(rule);
    
    if(! rule.isActive) {// Invoke a rule that is not active
        [invocation invoke];
        return;
    }
    
    if (rule.durationThreshold <= 0 || mt_invokeFilterBlock(rule, invocation)) {// Set aliasSelector(the original method IMP) after execution.
        invocation.selector = rule.aliasSelector;
        [invocation invoke];
        return;
    }
    
    // correctionForSystemTime is used to correct the difference of the system time.
    NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
    now += MTEngine.defaultEngine.correctionForSystemTime;
    
    switch (rule.mode) {
        // Throttling mode: perform the first trigger
        case MTPerformModeFirstly: {
            // If the current interval is greater than the limit time, it is executed directly. Otherwise, it does not respond
            if (now - rule.lastTimeRequest > rule.durationThreshold) {
                invocation.selector = rule.aliasSelector;
                [invocation invoke];
                // After execution, update the latest execution time
                rule.lastTimeRequest = now;
                dispatch_async(rule.messageQueue, ^{
                    // May switch from other modes, set nil just in case.
                    rule.lastInvocation = nil;
                });
            }
            break;
        }
        // Throttling mode: perform the last trigger
        case MTPerformModeLast: {
            invocation.selector = rule.aliasSelector;
            // Invocation Hold the parameter in advance to prevent it from being released when the invocation is delayed
            [invocation retainArguments];
            dispatch_async(rule.messageQueue, ^{
                // Update the most recent Invocation
                rule.lastInvocation = invocation;
                // If the interval exceeds the rule limit, the method is executed. Make sure it's the last call
                if (now - rule.lastTimeRequest > rule.durationThreshold) {
                    // Update the execution time
                    rule.lastTimeRequest = now;
                    // Execute invoke after a regular interval
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
                        if(! rule.isActive) { rule.lastInvocation.selector = rule.selector; } [rule.lastInvocation invoke];// After invoke, set lastInvocation to nil
                        rule.lastInvocation = nil; }); }});break;
        }
        // Shake mode: if no new trigger is triggered for a period of time, execute again
        case MTPerformModeDebounce: {
            // Set the Invocation to the Selector
            invocation.selector = rule.aliasSelector;
            // Hold parameters in advance
            [invocation retainArguments];
            dispatch_async(rule.messageQueue, ^{
                / / update the invocation
                rule.lastInvocation = invocation;
                // Execute after the restricted time period
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
                    // If the rule is the same as the Invocation, prove that there is no new invocation and the execution condition is met
                    if (rule.lastInvocation == invocation) {
                        if(! rule.isActive) { rule.lastInvocation.selector = rule.selector; } [rule.lastInvocation invoke]; rule.lastInvocation =nil; }}); });break; }}}Copy the code

And the author himself has written 4 related notes:

  • Objective-C Message Throttle and Debounce
  • Associated Object and Dealloc
  • MessageThrottle Performance Benchmark and Optimization
  • MessageThrottle Safety

Limited to space, no longer continue to put the code, you can read the author and source code in detail.

My thoughts this time are as follows:

  • Again, I deeply realized that many concepts or strategies are basically common throughout the big front end, and even are common throughout computer technology.
  • Whenever they have an idea, basically predecessors will have a good implementation, we often stand on the shoulders of giants to do things. Thanks to so many excellent programmers for their creation and sharing

Refer to the article

  • Implement function throttling and function stabilization in iOS

  • IOS programming throttle and all that stuff

  • NSMapTable: Not just an NSDictionary with weak Pointers

  • 【OC 解 析 】NSPointerArray, NSMapTable, NSHashTable