Git Product home page Git Product logo

messagethrottle's Introduction

MessageThrottle

CI Status Version Carthage compatible codecov Codacy Badge License Platform CocoaPods CocoaPods Twitter Follow

MessageThrottle

MessageThrottle is a lightweight, simple library for controlling frequency of forwarding Objective-C messages. You can choose to control existing methods per instance or per class. It's an implementation of function throttle/debounce developed with Objective-C runtime. For a visual explaination of the differences between throttling and debouncing, see this demo.

📚 Article

🌟 Features

  • Easy to use.
  • Keep your code clear
  • Reserve the whole arguments.
  • Support instance, class and meta class.
  • Support 3 modes: Throttle(Firstly), Throttle(Last) and Debounce.
  • Centralized management of rules.
  • Self-managed rules.
  • Let method MUST invoke at the specified conditions.
  • Persistent rules.

🔮 Example

To run the example project, clone the repo and run MTDemo target.

🐒 How to use

The following example shows how to restrict the frequency of forwarding - [ViewController foo:] message to 100 times per second.

Stub *s = [Stub new];
MTRule *rule = [s limitSelector:@selector(foo:) oncePerDuration:0.01]; // returns MTRule instance

For more control of rule, you can use mt_limitSelector:oncePerDuration:usingMode:onMessageQueue:alwaysInvokeBlock:.

You can also start with a creation of MTRule:

Stub *s = [Stub new];
// You can also assign `Stub.class` or `mt_metaClass(Stub.class)` to `target` argument.
MTRule *rule = [[MTRule alloc] initWithTarget:s selector:@selector(foo:) durationThreshold:0.01];
rule.mode = MTModePerformLast; // Or `MTModePerformFirstly`, ect
[rule apply];

You can let method MUST invoke at the specified conditions using alwaysInvokeBlock. The example code above will invoke message immediately if its 1st parameter equals "1970". BTW, alwaysInvokeBlock can has no parameter, or has one more MTRule before message's parameter list.

You should call discard method When you don't need limit foo: method.

[rule discard];

NOTE: MTRule is self-managed. If the target of rule is a object instance, MTRule will discard itself automatically when the target is deallocated.

Some rules may have large durationThreshold. You can set property persistent to YES, and save them on disk by calling savePersistentRules method. These persistent rules will be applied after MTEngine class is loaded. savePersistentRules will be called automatically when receive terminate notification.

rule.persistent = YES;
[MTEngine.defaultEngine savePersistentRules];

MTRule represents the rule of a message throttle, which contains strategy and frequency of sending messages.

You can assign an instance or (meta)class to target property. When you assign an instance to target, MessageThrottle will only restrict messages send to this instance. If you want to restrict a class method, just using mt_metaClass() to get it's meta class, and assign the meta class to target. Rules with instance target won't conflict with each other, and have a higher priority than rules with class target.

NOTE: A message can only have one rule per class hierarchy. For example, If there is one rule of message - [Stub foo:], you can't add another rule of message - [SuperStub foo:] anymore. PS: Assume that Stub is a subclass of SuperStub.

MTRule also define the mode of performing selector. There are three modes defined in MTMode: MTModePerformFirstly, MTModePerformLast and MTModePerformDebounce. This demo shows the difference between throttle and debounce.

The default mode is MTModePerformDebounce. MTModePerformDebounce will restart timer when another message arrives during durationThreshold. So there must be a delay of durationThreshold at least.

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

MTModePerformFirstly will performs the first message and ignore all following messages during durationThreshold.

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

MTModePerformLast performs the last message at end time. Please note that does not perform message immediately, the delay could be durationThreshold at most.

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

When using MTModePerformLast or MTModePerformDebounce, you can designate a dispatch queue which messages perform on. The messageQueue is main queue by default. MTModePerformLast and MTModePerformDebounce modes will also use the last arguments to perform messages.

MTEngine is a singleton class. It manages all rules of message throttles. You can use applyRule: method to apply a rule or update an old rule that already exists. Using it's discardRule: method to discardRule a rule. There is also a readonly property allRules for obtaining all rules in current application.

📲 Installation

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:

$ gem install cocoapods

To integrate MessageThrottle into your Xcode project using CocoaPods, specify it in your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!
target 'MyApp' do
	pod 'MessageThrottle'
end

You need replace "MyApp" with your project's name.

Then, run the following command:

$ pod install

Carthage

Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.

You can install Carthage with Homebrew using the following command:

$ brew update
$ brew install carthage

To integrate MessageThrottle into your Xcode project using Carthage, specify it in your Cartfile:

github "yulingtianxia/MessageThrottle"

Run carthage update to build the framework and drag the built MessageThrottleKit.framework into your Xcode project.

Manual

Just drag the "MessageThrottle" document folder into your project.

❤️ Contributed

  • If you need help or you'd like to ask a general question, open an issue.
  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, submit a pull request.

👨🏻‍💻 Author

yulingtianxia, [email protected]

👮🏻 License

MessageThrottle is available under the MIT license. See the LICENSE file for more info.

messagethrottle's People

Contributors

yulingtianxia avatar zhongwuzw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

messagethrottle's Issues

只在iOS16的模拟器上crash

0 CoreFoundation 0x00007ff8004278cb __exceptionPreprocess + 242
1 libobjc.A.dylib 0x00007ff80004dba3 objc_exception_throw + 48
2 CoreFoundation 0x00007ff800436ad8 +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0
3 0x000000010076ff6a mt_executeOrigForwardInvocation + 650
4 0x000000010076fabf mt_forwardInvocation + 735
5 CoreFoundation 0x00007ff80042bb28 forwarding + 814
6 CoreFoundation 0x00007ff80042e088 _CF_forwarding_prep_0 + 120

阅读源码有处不理解的

[self.rules enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MTRule * _Nonnull obj, BOOL * _Nonnull stop) {
            if (rule.selector == obj.selector
                && mt_object_isClass(rule.target)
                && mt_object_isClass(obj.target)) {
                Class clsA = rule.target;
                Class clsB = obj.target;
                shouldApply = !([clsA isSubclassOfClass:clsB] || [clsB isSubclassOfClass:clsA]);
                *stop = shouldApply;
                NSCAssert(NO, @"Error: %@ already apply rule in %@. A message can only have one rule per class hierarchy.", NSStringFromSelector(obj.selector), NSStringFromClass(clsB));
            }
        }];

在遍历中如果 clsA 和 clsB 不是继承关系的话,shouldApply = YES,为什么要打印 Error信息呢?

跟想象中不同啊

习惯了 dispatch 那种方式。。。
这种用法有点繁琐,不过你写得很好啊,谢谢你

MTEngine.defaultEngine.classHooked 只有add,没有remove操作,和神策集成发生崩溃

我的工程中集成了神策统计,神策在处理统一的点击事件捕获是,使用了和MessageThrottle类似的生成一个形如EditViewController_6_XXXX 虚拟子类的操作,此处的数字会递增,反复进入同一个页面,可能会生成EditViewController_8_XXXX、EditViewController_9_XXXX 这种情况。

在 mt_overrideMethod 这个方法中,[MTEngine.defaultEngine.classHooked addObject:cls]添加的cls实际是神策统计生成的虚拟子类。
由于没有remove的操作,当下一次限流方法再次执行时,下面的代码在进行isSubclassOfClass判断是会发生崩溃,因为classHooked里的存放是EditViewController_8_XXXX,而传入的是EditViewController_9_XXXX。

  // check if subclass has hooked!
    for (Class clsHooked in MTEngine.defaultEngine.classHooked) {
        
        if (clsHooked != cls && [clsHooked isSubclassOfClass:cls]) {
            NSLog(@"Sorry: %@ used to be applied, can't apply it's super class %@!", NSStringFromClass(cls), NSStringFromClass(cls));
            return NO;
        }
    }

请问为何限制同一个target的多个Rule

你好,我看到在应用Rule的函数里,target == rule.target限制了同一个target的多个Rule

- (BOOL)applyRule:(MTRule *)rule
{
    pthread_mutex_lock(&mutex);
    MTDealloc *mtDealloc = [rule mt_deallocObject];
    [mtDealloc lock];
    __block BOOL shouldApply = YES;
    if (mt_checkRuleValid(rule)) {
        for (id target in [[self.targetSELs keyEnumerator] allObjects]) {
            if (target == rule.target) {
                shouldApply = NO;
                continue;
            }
            NSMutableSet *selectors = [self.targetSELs objectForKey:target];
            for (NSString *selectorName in selectors) {
                if (sel_isEqual(rule.selector, NSSelectorFromString(selectorName))
                    && mt_object_isClass(rule.target)
                    && mt_object_isClass(target)) {
                    Class clsA = rule.target;
                    Class clsB = target;
                    shouldApply = !([clsA isSubclassOfClass:clsB] || [clsB isSubclassOfClass:clsA]) && shouldApply;
                    NSLog(@"Sorry: %@ already apply rule in %@. A message can only have one rule per class hierarchy.", selectorName, NSStringFromClass(clsB));
                }
            }
        }
        shouldApply = shouldApply && mt_overrideMethod(rule);
        if (shouldApply) {
            [self addSelector:rule.selector onTarget:rule.target];
            rule.active = YES;
        }
    }
    else {
        shouldApply = NO;
    }
    [mtDealloc unlock];
    pthread_mutex_unlock(&mutex);
    return shouldApply;
}

导致我没法给同一个target的不同方法设置规则,请问是出于什么考虑?

MTRule *debounceRule = [[MTRule alloc] initWithTarget:self selector:@selector(updateDebounceString:) durationThreshold:2];
debounceRule.mode = MTPerformModeDebounce;
[debounceRule apply];
NSLog(@"%@", [self mt_allRules]);

MTRule *throttleRule = [[MTRule alloc] initWithTarget:self selector:@selector(updateThrottlingString:) durationThreshold:2];
throttleRule.mode = MTPerformModeLast;
[throttleRule apply];

NSLog(@"%@", [self mt_allRules]);

谢谢

MTPerformModeLast下多线程野指针

bug

见截图,多线程下lastInvocation在不同线程被修改,导致野指针crash。
需要保证messageQueue和mt_handleInvocation所在线程一致才能避免。

A message can only have one rule per class hierarchy.

如果在需要节流的对象 -init 中进行 rule 的申请, 有一定几率会出现这个断言, 但又不必现, 所以想问一下, 如果在需要节流的对象中管理 rule, 怎么样的方式比较合适?

- (instancetype)init
{
    self = [super init];
    if (self) {
        MTRule *rule = [[MTRule alloc] initWithTarget:self selector:@selector(updateProgress:) durationThreshold:0.3];
        rule.mode = MTPerformModeFirstly;
        [MTEngine.defaultEngine applyRule:rule];
    }
    return self;
}

MTPerformModeLast

MTPerformModeLast 所存储的lastTimeRequest不应该保存now+rule.durationThreshold的时间吗 不然怎么保证的请求的是最后一次

崩掉了!

Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 CoreFoundation 0x0000000183d30bc8 -[__NSCFString getCString:maxLength:encoding:] + 4
1 Foundation 0x00000001847c1584 NSSelectorFromString + 96
2 xxxx 0x0000000102fe1c04 mt_forwardInvocation + 252932 (MessageThrottle.m:823)
3 CoreFoundation 0x0000000183e342d4 forwarding + 624
4 CoreFoundation 0x0000000183d1a41c CF_forwarding_prep_0 + 92
5 CoreFoundation 0x0000000183e36580 invoking
+ 144
6 CoreFoundation 0x0000000183d15748 -[NSInvocation invoke] + 284
7 CoreFoundation 0x0000000183d1a56c -[NSInvocation invokeWithTarget:] + 60
8 xxxx 0x0000000102fe1f40 mt_forwardInvocation + 253760 (MessageThrottle.m:0)
9 CoreFoundation 0x0000000183e342d4 forwarding + 624
10 CoreFoundation 0x0000000183d1a41c CF_forwarding_prep_0 + 92
11 CoreFoundation 0x0000000183e36580 invoking
+ 144
12 CoreFoundation 0x0000000183d15748 -[NSInvocation invoke] + 284
13 CoreFoundation 0x0000000183d1a56c -[NSInvocation invokeWithTarget:] + 60
14 xxxx 0x0000000102fe1f40 mt_forwardInvocation + 253760 (MessageThrottle.m:0)
15 CoreFoundation 0x0000000183e342d4 forwarding + 624
16 CoreFoundation 0x0000000183d1a41c CF_forwarding_prep_0 + 92
17 CoreFoundation 0x0000000183e36580 invoking
+ 144
18 CoreFoundation 0x0000000183d15748 -[NSInvocation invoke] + 284
19 CoreFoundation 0x0000000183d1a56c -[NSInvocation invokeWithTarget:] + 60
20 xxxx 0x0000000102fe1f40 mt_forwardInvocation + 253760 (MessageThrottle.m:0)
21 CoreFoundation 0x0000000183e342d4 forwarding + 624
22 CoreFoundation 0x0000000183d1a41c CF_forwarding_prep_0 + 92
23 CoreFoundation 0x0000000183e36580 invoking
+ 144
24 CoreFoundation 0x0000000183d15748 -[NSInvocation invoke] + 284
25 CoreFoundation 0x0000000183d1a56c -[NSInvocation invokeWithTarget:] + 60
26 xxxx 0x0000000102fe1f40 mt_forwardInvocation + 253760 (MessageThrottle.m:0)
27 CoreFoundation 0x0000000183e342d4 forwarding + 624
28 CoreFoundation 0x0000000183d1a41c CF_forwarding_prep_0 + 92
29 CoreFoundation 0x0000000183e36580 invoking
+ 144
30 CoreFoundation 0x0000000183d15748 -[NSInvocation invoke] + 284
31 CoreFoundation 0x0000000183d1a56c -[NSInvocation invokeWithTarget:] + 60

调用如下:

[self.collectionView mt_limitSelector:@selector(beginHeaderRefresh) oncePerDuration:5 usingMode:MTPerformModeFirstly];

每次 viewWillAppear 的时候调通 [ self.collectionView beginHeaderRefresh]

applyRule: 判断逻辑疑问?

我在阅读源码的时候,有一个疑问。具体的位置是 MTEngineapplyRule: 的实现;
具体的代码如下

- (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
                && mt_object_isClass(rule.target)
                && mt_object_isClass(obj.target)) {
                Class clsA = rule.target;
                Class clsB = obj.target;
                shouldApply = !([clsA isSubclassOfClass:clsB] || [clsB isSubclassOfClass:clsA]);
                *stop = shouldApply;
                NSCAssert(NO, @"Error: %@ already apply rule in %@. A message can only have one rule per class hierarchy.", NSStringFromSelector(obj.selector), NSStringFromClass(clsB));
            }
        }];
        
        if (shouldApply) {
            self.rules[mt_methodDescription(rule.target, rule.selector)] = rule;
            mt_overrideMethod(rule.target, rule.selector);
        }
    }
    pthread_mutex_unlock(&mutex);
    return shouldApply;
}

如果 mt_checkRuleValid(rule) 返回 NO ,最后的方法返回值就是 YES

tableView使用MessageThrottle崩掉了,在工程中偶现,bugly收集到了一些

libobjc.A.dylib
_objc_setAssociatedObject + 48
1
BuGeElectricContest
-[MTRule deallocObject] (MessageThrottle.m:179)
2
BuGeElectricContest
-[MTRule deallocObject] (MessageThrottle.m:179)
3
BuGeElectricContest
-[MTRule invokingLastInvocation] (MessageThrottle.m:190)
4
libdispatch.dylib
__dispatch_client_callout + 16

13
UIKitCore
_UIApplicationMain + 164
14
BuGeElectricContest
main (main.m:14)
15
libdyld.dylib
_start + 4

image

也许可以换一种调用方式

为了更方便易用,可不可以在方法内部添加过滤限制,

- (IBAction)tapFoo:(UIButton *)sender {
    [self mt_limitSelector:_cmd oncePerDuration:0.5];
}

稍微改了下代码,应该可以实现的,就是第一次有问题,😄
纯粹个人习惯

Bug收集平台获取的崩溃信息

Crashed: Thread
0  libsystem_kernel.dylib         0x180f1d2e0 __pthread_kill + 8
1  libsystem_pthread.dylib        0x1810be288 pthread_kill$VARIANT$mp + 376
2  libsystem_c.dylib              0x180e8bd0c abort + 140
3  libsystem_malloc.dylib         0x180f55838 szone_size + 634
4  CoreFoundation                 0x181362c54 -[__NSArrayM insertObject:atIndex:] + 380
5  CoreFoundation                 0x1813aca8c -[NSInvocation retainArguments] + 212
6  MessageThrottle                0x1025e85a0 mt_forwardInvocation + 1916
7  CoreFoundation                 0x18149c2d4 ___forwarding___ + 624
8  CoreFoundation                 0x18138241c _CF_forwarding_prep_0 + 92
9  AllInOne                       0x102adab84 -[CoreData_3_$Lambda$3 lWithId:] + 84
10 ··· ···

AllInOne是我项目中的内容,我把不相关的堆栈信息去掉了。

从第6行开始调用了MessageThrottlemt_forwardInvocation->mt_handleInvocation->retainArguments,接着可能是数组越界还是插入空值的崩溃?并不太清楚崩溃的原因,大神有空看下,可能与库有关。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.