<code id='2nvg2'><strong id='2nvg2'></strong></code>
<i id='2nvg2'></i>
<i id='2nvg2'><div id='2nvg2'><ins id='2nvg2'></ins></div></i>

      <ins id='2nvg2'></ins><span id='2nvg2'></span>

    1. <tr id='2nvg2'><strong id='2nvg2'></strong><small id='2nvg2'></small><button id='2nvg2'></button><li id='2nvg2'><noscript id='2nvg2'><big id='2nvg2'></big><dt id='2nvg2'></dt></noscript></li></tr><ol id='2nvg2'><table id='2nvg2'><blockquote id='2nvg2'><tbody id='2nvg2'></tbody></blockquote></table></ol><u id='2nvg2'></u><kbd id='2nvg2'><kbd id='2nvg2'></kbd></kbd>
      1. <acronym id='2nvg2'><em id='2nvg2'></em><td id='2nvg2'><div id='2nvg2'></div></td></acronym><address id='2nvg2'><big id='2nvg2'><big id='2nvg2'></big><legend id='2nvg2'></legend></big></address>
        <fieldset id='2nvg2'></fieldset>
        <dl id='2nvg2'></dl>

          Objective-C Message Throttle and Debounce(Objective-C消息节流和防抖

          • 时间:
          • 浏览:1
          • 来源:124软件资讯网

            在现实项目中经常会遇到因要领挪用频仍而导致的 UI 闪动问题和性能问题  ,这时用某种计谋需要控制挪用频率  ,以到达节省和防抖的效果  。MessageThrottle 是我实现的一个 Objective-C 新闻节省和防抖的轻量级工具库  ,使用便捷且营业无关  。

            读懂本文的条件是对 Objective-C Runtime 和 Objective-C 新闻发送与转发机制原理有一定相识  。

            观点

            函数节省(throttle)是一个很基础的观点  ,经常跟函数防抖(debounce)作比力 。在处置惩罚一连事务时比力常用  ,可以通过这个 Demo 感受下二者区别  。在 JS 中有较多的实现和应用案例  ,可以检察这篇文章 更直接地相识下  。

            虽然在开发 iOS 和 macOS 的时间不用过多体贴一连事务的采样问题  ,但有时也需要制止某个要领被频仍挪用  。好比一个很庞大的页面可能会频仍请求网络  ,每次回包都需更新界面 ,这时就需要防抖  ,控制刷新频率  。

            在 Objective-C 中 ,要领挪用实在就是新闻发送 ,以是我改了个名字  ,叫新闻节省和防抖  。

            使用姿势

            如果我建立了一个 Stub 类的实例 s ,我想限制它挪用 foo: 要领的频率  。先要建立并设置一个 MTRule ,并将规则应用到 MTEngine 单例中:

            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]`

            target 可以是一个实例工具  ,也可以是一个类或元类  。这样可以更天真地控制限制计谋  ,既可以只控制某个工具的新闻发送频率  ,也可以控制某个类的实例要领和类要领的频率 。固然  ,规则的 target 为实例工具的优先级比类更高  ,也不会发生冲突  。

            固然另有更简朴的用法 ,跟上面那段代码作用相同:

            [s limitSelector:@selector(foo:) oncePerDuration:0.01]; // returns MTRule instance

            无论是节省照旧防抖  ,都需要设定一个时间 durationThreshold 阈值来限制频率  ,都意味着要领在最后会延迟挪用  。MTRule 默认的模式是 MTPerformModeDebounce ,也就是防抖模式  ,需要等新闻不再一连频仍发送后才执行  。MTPerformModeLast 和 MTPerformModeFirstly 对应着节省模式  ,也就是控制一准时间内只执行一次  。区别在于前者执行的是这段时间内最后发送的新闻 ,后者执行第一次发送的新闻  。

            好比我想要控制界面上某个 Label 内容的更新频率  ,给用户更好的体验  ,这时间很适合使用 MTPerformModeLast 模式:

            rule.mode = MTPerformModeLast;

            固然所有规则都是可以动态调整的  ,也就是在应用规则以后  ,依然可以改变 MTRule 工具中各项设置 ,并会在下次新闻发送时生效 。若是淘气地将 durationThreshold 改成非正数  ,那么等同于立刻执行要领 ,不会限制频率  。

            当使用 MTPerformModeDebounce 和 MTPerformModeLast 模式的时间  ,由于执行新闻会有延迟 ,可以指定执行新闻的行列 messageQueue  ,默以为主行列 。

            当想要破除某条规则时 ,使用一行代码即可:

            [MTEngine.defaultEngine discardRule:rule]; // or use `[rule discard]`

            应用和破除规则都是线程宁静的  。

            实现原理

            参照 Aspects 和 JSPatch 中 Hook 的原理  ,将限制频率逻辑嵌入新闻转发流程中:

            1. 给类添加一个新的要领 fixed_selector  ,对应实现为 rule.selector 的 IMP 。

            2. 使用 Objective-C runtime 新闻转发机制  ,将 rule.selector 对应的 IMP 改成 _objc_msgForward 从而触发挪用 forwardInvocation: 要领  。

            3. 将 forwardInvocation: 的实现替换为自己实现的 IMP  ,并在自己实现的逻辑中将 invocation.selector 设为 fixed_selector  。并限制 [invocation invoke] 的挪用频率  。

            这种做法的缺陷是若是同时 hook 了基类和子类的统一个要领 ,且子类挪用了基类的要领  ,就会导致循环挪用  。由于挪用 super 要领时  ,传入的 target 照旧 self 工具  ,导致挪用了子类的要领  。幸亏这里并不允许同时 hook 一条继续链上的两个类 ,由于子类和基类限制频率的规则会相互滋扰  ,导致不易发现的 bug  。

            MessageThrottle 从设计上使用 MTEngine 单例这种中央化的的方式来治理所有规则 。Aspects 是将 hook 的上下文插入到对应的 target 中 ,这样的利益是需要袒露的接口较少  。而 MessageThrottle 需要提供当前所有的规则给使用方 。由于要领挪用频率的限制会影响其上游代码和下游代码的运行频率  ,以是中央化治理的做法很有须要  。

            由于设置规则的内容较多  ,若是使用逐个传参的方式  ,要领名会很长  。以是这里用 MTRule 类封装了规则的上下文  ,并使用 applyRule: 和 discardRule: 要领应用和破除规则  。

            治理 MTRule

            MTEngine 内部使用键值对存取 MTRule  ,这里使用 target 和 selector 的组合值作为 key  。这里只要保证唯一性即可区分差别的规则 ,花样不牢固:

            static NSString * mt_methodDescription(id target, SEL selector)
            {
                NSString *selectorName = NSStringFromSelector(selector);
                if (object_isClass(target)) {
                    NSString *className = NSStringFromClass(target);
                    return [NSString stringWithFormat:@"%@ [%@ %@]", class_isMetaClass(target) ? @"+" : @"-", className, selectorName];
                }
                else {
                    return [NSString stringWithFormat:@"[%p %@]", target, selectorName];
                }
            }

            在应用和破除规则的时间  ,需要检查规则正当性  。这里只是简朴检查下库中涉及的类和要领 ,一些内存治理和runtime 的要领并没有做限制  ,究竟用户想作死我也管不着:

            static BOOL mt_checkRuleValid(MTRule *rule)
            {
                if (rule.target && rule.selector && rule.durationThreshold > 0) {
                    NSString *selectorName = NSStringFromSelector(rule.selector);
                    if ([selectorName isEqualToString:@"forwardInvocation:"]) {
                        return NO;
                    }
                    Class cls;
                    if (object_isClass(rule.target)) {
                        cls = rule.target;
                    }
                    else {
                        cls = object_getClass(rule.target);
                    }
                    NSString *className = NSStringFromClass(cls);
                    if ([className isEqualToString:@"MTRule"] || [className isEqualToString:@"MTEngine"]) {
                        return NO;
                    }
                    return YES;
                }
                return NO;
            }


            处置惩罚 NSInvocation

            在进入到新闻转发流程挪用 forwardInvocation: 要领时会进入到自界说的处置惩罚逻辑中 ,然后决议是否执行 [invocation invoke] 。之前已经将原始 selector 的 IMP 替换成了 fixedSelector  ,以是挪用 [invocation invoke] 之前需要挪用 invocation.selector = fixedSelector  。

            下面的函数就是处置惩罚 NSInvocation 工具的逻辑 。先用 target 和 selector 获取 MTRule 工具  ,进而凭据差别的 mode 接纳差别的计谋  。若是 durationThreshold 非正数就立刻执行要领 。

            static void mt_handleInvocation(NSInvocation *invocation, SEL fixedSelector)
            {
                NSString *methodDescriptionForInstance = mt_methodDescription(invocation.target, invocation.selector);
                NSString *methodDescriptionForClass = mt_methodDescription(object_getClass(invocation.target), invocation.selector);

                MTRule *rule = MTEngine.defaultEngine.rules[methodDescriptionForInstance];
                if (!rule) {
                    rule = MTEngine.defaultEngine.rules[methodDescriptionForClass];
                }

                if (rule.durationThreshold <= 0) {
                    [invocation setSelector:fixedSelector];
                    [invocation invoke];
                    return;
                }

                NSTimeInterval now = [[NSDate date] timeIntervalSince1970];

                switch (rule.mode) {
                    case MTPerformModeFirstly:
                        ...
                        break;
                    case MTPerformModeLast:
                        ...
                        break;
                    case MTPerformModeDebounce:
                        ...
                        break;
                }
            }

            上面代码省略了差别 mode 的处置惩罚逻辑  ,下面会逐个解说  。

            MTPerformModeFirstly

            MTModePerformFirstly:
            start                                                                end
            |                           durationThreshold                          |
            @-------------------------@----------@---------------@---------------->>
            |                         |          |               |          
            perform immediately       ignore     ignore          ignore

            最简朴粗暴的实现方式 ,忽略第一次发送新闻之后 durationThreshold 时间段内的所有新闻  。

            if (now - rule.lastTimeRequest > rule.durationThreshold) {
            rule.lastTimeRequest = now;
            invocation.selector = fixedSelector;
            [invocation invoke];
            rule.lastInvocation = nil;
            }


            MTPerformModeLast

            MTModePerformLast:
            start                                                                end
            |                           durationThreshold                          |
            @-------------------------@----------@---------------@---------------->>
            |                         |          |               |          
            ignore                    ignore     ignore          will perform at end

            在 durationThreshold 时间内不停更新 lastInvocation 的值  ,并在到达阈值 durationThreshold 后执行 [lastInvocation invoke] 。这样保证了执行的是最后一次发送的新闻  。需要注重的是 ,NSInvocation 工具默认不会持有参数  ,在异步延迟执行 invoke 的时间参数可能已经被释放了  ,进而野指针 crash  。以是需要挪用 retainArguments 要领提前持有参数  ,防止之后被释放掉 。若是现实传入的参数与参数类型不符  ,可能导致 retainArguments 要领 crash  。我曾想过将参数列表生存到一个 NSArray 里 ,然后放到 MTRule 中  ,这样可以对参数类型做判断 ,制止 crash  ,也顺便持有了参数列表 。但发现需要笼罩的类型太多  ,事情量和风险更多  。我把这个半制品代码放在了 GitHubGist 上: ConvertInvocationArguments.m

            if (now - rule.lastTimeRequest > rule.durationThreshold) {
                rule.lastTimeRequest = now;
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
                    [rule.lastInvocation invoke];
                    rule.lastInvocation = nil;
                });
            }
            else {
                invocation.selector = fixedSelector;
                rule.lastInvocation = invocation;
                [rule.lastInvocation retainArguments];
            }


            MTPerformModeDebounce

            MTModePerformDebounce:
            start                                        end
            |           durationThreshold(old)             |
            @----------------------@---------------------->>
            |                      |                 
            ignore                 will perform at end of new duration
                                   |--------------------------------------------->>
                                   |           durationThreshold(new)             |
                                   start                                        end

            虽然流程看上去庞大但实在实现起来也很简朴  。每次发送新闻完再过 durationThreshold 时间后  ,检查下 lastInvocation 有没有转变  。若是无转变  ,则说明这段时间内没有新的新闻发送 ,则可以执行 lastInvocation 。

            invocation.selector = fixedSelector;
            rule.lastInvocation = invocation;
            [rule.lastInvocation retainArguments];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
            if (rule.lastInvocation == invocation) {
            [rule.lastInvocation invoke];
            rule.lastInvocation = nil;
            }
            });


            规则的应用与破除

            在真正应用规则之前  ,需要检查下规则正当性 ,然后检查继续链上是否已经应用过规则了  。若是有  ,则需要输堕落误信息;否则应用规则 。这里使用 POSIX 的互斥锁保证线程宁静  。mt_overrideMethod() 函数所作的事情就是最先提到的使用新闻转发流程 hook 的三个步骤  。

            - (BOOL)applyRule:(MTRule *)rule
            {
                pthread_mutex_lock(&mutex);
                __block BOOL shouldApply = YES;
                if (mt_checkRuleValid(rule)) {
                    [self.rules enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MTRule * _Nonnull obj, BOOL * _Nonnull stop) {
                        if (rule.selector == obj.selector
                            && object_isClass(rule.target)
                            && object_isClass(obj.target)) {
                            Class clsA = rule.target;
                            Class clsB = obj.target;
                            shouldApply = !([clsA isSubclassOfClass:clsB] || [clsB isSubclassOfClass:clsA]);
                            *stop = shouldApply;
                            NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already apply rule in %@. A message can only have one throttle per class hierarchy."NSStringFromSelector(obj.selector), NSStringFromClass(clsB)];
                            NSLog(@"%@", errorDescription);
                        }
                    }];

                    if (shouldApply) {
                        self.rules[mt_methodDescription(rule.target, rule.selector)] = rule;
                        mt_overrideMethod(rule.target, rule.selector);
                    }
                }
                pthread_mutex_unlock(&mutex);
                return shouldApply;
            }

            破除规则是执行相反的操作 。若是 target 是个实例工具 ,mt_recoverMethod() 函数会判断是否有相同 selector 且 target 为这个实例工具的类的其他规则 。若是有  ,那将不会移除 hook  。

            - (BOOL)discardRule:(MTRule *)rule
            {
                pthread_mutex_lock(&mutex);
                BOOL shouldDiscard = NO;
                if (mt_checkRuleValid(rule)) {
                    NSString *description = mt_methodDescription(rule.target, rule.selector);
                    shouldDiscard = self.rules[description] != nil;
                    if (shouldDiscard) {
                        self.rules[description] = nil;
                        mt_recoverMethod(rule.target, rule.selector);
                    }
                }
                pthread_mutex_unlock(&mutex);
                return shouldDiscard;
            }


            后记

            实在在开发历程中遇到需要限制要领挪用频率的场景并不多  ,只是最近恰巧一连遇到几个刷新 UI 过频仍的问题 ,才想到应该去造个轮子 。由于时间匆匆  ,一定另有思量不周和一些 bug ,待投入使用后逐步完善和修复  。

            实在想在某个特定函数做节省很简朴 ,但每次都需要做重复劳动 ,写脏代码  ,还不如抽象出一个工具类出来  。只管造与营业无关的轮子  ,磨炼手艺  ,也受益整个营业生长  。

            好  ,装逼到此为止  。Github : https://github.com/yulingtianxia/MessageThrottle