<span id='rl6h'></span>

    1. <i id='rl6h'></i>

      <acronym id='rl6h'><em id='rl6h'></em><td id='rl6h'><div id='rl6h'></div></td></acronym><address id='rl6h'><big id='rl6h'><big id='rl6h'></big><legend id='rl6h'></legend></big></address>

        <ins id='rl6h'></ins>
          <dl id='rl6h'></dl>

          <code id='rl6h'><strong id='rl6h'></strong></code>
          1. <fieldset id='rl6h'></fieldset>

          2. <tr id='rl6h'><strong id='rl6h'></strong><small id='rl6h'></small><button id='rl6h'></button><li id='rl6h'><noscript id='rl6h'><big id='rl6h'></big><dt id='rl6h'></dt></noscript></li></tr><ol id='rl6h'><table id='rl6h'><blockquote id='rl6h'><tbody id='rl6h'></tbody></blockquote></table></ol><u id='rl6h'></u><kbd id='rl6h'><kbd id='rl6h'></kbd></kbd>
          3. <i id='rl6h'><div id='rl6h'><ins id='rl6h'></ins></div></i>

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

            • 时间:
            • 浏览:32
            • 来源: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