0%

类别导致的target-action方法不调用问题

Bug

描述:ZATextField 自己添加自己作为target后,发现action方法不被调用。

分析

ZATextField的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import "ZATextField.h"

@implementation ZATextField

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {

__weak typeof(self) weakSelf = self;
[[NSNotificationCenter defaultCenter] addObserverForName:UITextFieldTextDidBeginEditingNotification object:self queue:nil usingBlock:^(NSNotification * _Nonnull note) {
if (weakSelf.rightView && weakSelf.rightViewMode == UITextFieldViewModeWhileEditing) {
weakSelf.rightView.hidden = weakSelf.text.length > 0 ? NO : YES;
}
if (weakSelf.leftView && weakSelf.leftViewMode == UITextFieldViewModeWhileEditing) {
weakSelf.leftView.hidden = weakSelf.text.length > 0 ? NO : YES;
}
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UITextFieldTextDidChangeNotification object:self queue:nil usingBlock:^(NSNotification * _Nonnull note) {
if (weakSelf.rightView && weakSelf.rightViewMode == UITextFieldViewModeWhileEditing) {
weakSelf.rightView.hidden = weakSelf.text.length > 0 ? NO : YES;
}
if (weakSelf.leftView && weakSelf.leftViewMode == UITextFieldViewModeWhileEditing) {
weakSelf.leftView.hidden = weakSelf.text.length > 0 ? NO : YES;
}
}];

[self addTarget:self action:@selector(xxxxchange:) forControlEvents:UIControlEventEditingChanged];
}
return self;
}

- (void)xxxxchange:(UITextField *)sender {
NSLog(@"text:%@", self.text);
}

// 光标大小设置
- (CGRect)caretRectForPosition:(UITextPosition *)position {
CGRect originalRect = [super caretRectForPosition:position];
CGFloat caretHeight = self.font.lineHeight - 4;
originalRect.size.height = caretHeight;
originalRect.origin.y = (self.frame.size.height - caretHeight) / 2.0;
return originalRect;
}

@end

自己添加了自己作为target:

1
[self addTarget:self action:@selector(xxxxchange:) forControlEvents:UIControlEventEditingChanged];

理论上讲当有文字输入时,xxxxchange方法肯定会调用的,但是实际没有。

可能的原因

1.方法被其他地方的类别覆盖了。

2.target被移除了。

针对第一点,改了一个很随意的方法名称xxxxchange:,但是没有用。说明不是因为类别覆盖的原因。

针对第二点,调试时打印了allTargets,竟然发现self赫然在列。WTF!一时间没有任何思路,一度以为事件传递有问题,浪费很多时间。最终搜索所有与UITextField相关的类别,子类等,结果发现:

1
2
3
4
5
6
7
8
9
10
11
ZATextField *textField = [[ZATextField alloc] initWithFrame:CGRectZero];
textField.font = [UIFont systemFontOfSize:21];
textField.textColor = [UIColor colorWithHexString:@"#26273C"];
textField.placeholder = @"请输入密码";
textField.delegate = self;
textField.backgroundColor = UIColor.whiteColor;
textField.layer.cornerRadius = 28;
textField.layer.masksToBounds = YES;
textField.secureTextEntry = YES;
textField.keyboardType = UIKeyboardTypeASCIICapable;
textField.xq_limitTextLength = 20; //就是这里

ZATextField有用到一个长度限制的类别,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSInteger)xq_limitTextLength
{
NSNumber *limit = objc_getAssociatedObject(self, @selector(xq_limitTextLength));
return limit.integerValue;
}

- (void)setXq_limitTextLength:(NSInteger)xq_limitTextLength
{
objc_setAssociatedObject(self, @selector(xq_limitTextLength), @(xq_limitTextLength), OBJC_ASSOCIATION_RETAIN);

[self removeTarget:self action:@selector(xq_textFieldTextDidChanged:) forControlEvents:UIControlEventEditingChanged];
[self addTarget:self action:@selector(xq_textFieldTextDidChanged:) forControlEvents:UIControlEventEditingChanged];
}

里面有removeTarget的操作,但是

1
[self removeTarget:self action:@selector(xq_textFieldTextDidChanged:) forControlEvents:UIControlEventEditingChanged];

这句代码的意思不是移除self—UIControlEventEditingChanged—xq_textFieldTextDidChanged键值对吗?怎么以前的也移除了?看来是我对removeTarget理解的有问题。

重新理解addTarget/removeTarget

addTarget:

1
2
3
4
// add target/action for particular event. you can call this multiple times and you can specify multiple target/actions for a particular event.
// passing in nil as the target goes up the responder chain. The action may optionally include the sender and the event in that order
// the action cannot be NULL. Note that the target is not retained.
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

Note that the target is not retained.新发现,原来UIControl并不会强引用target。所以self addTarget:self,并不会引起内存问题。不过这跟我们现在的问题没啥关系。

removeTarget:

1
2
// remove the target/action for a set of events. pass in NULL for the action to remove all actions for that target
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIControlEvents)controlEvents;

文档说明:

1
2
Use this method to prevent the delivery of control events to target objects associated with control. If you specify a valid object in the target parameter, this method stops the delivery of the specified events to all action methods associated with that object. If you specify nil for the target parameter, this method prevents the delivery of those events to all action methods of all target objects.
Although the action parameter is not considered when stopping the delivery of events, you should specify an appropriate value anyway. If the specified target/action combination no longer has any valid control events associated with it, the control cleans up its corresponding internal data structures. Doing so can affect the set of objects returned by the allTargets method.

里面有一句重要说明:

If you specify a valid object in the target parameter, this method stops the delivery of the specified events to all action methods associated with that object.If you specify nil for the target parameter, this method prevents the delivery of those events to all action methods of all target objects.

翻译过来就是,当传入一个有效的target和指定事件,调用该方法后,该target的所有指定事件的action方法都不会再执行了。如果传入的是nil,那么指定事件的所有target的所有action方法都不会再执行。

也就是说removeTarget方法的三个参数中,action参数没有实际用处,有用的是target和controlEvents参数。

很有可能内部的数据结构是这样的:

1
controlEvents : [target1(m1,m2,m3), target2(m1), target3(m1)]

测试代码:

1
2
3
4
5
6
7
[textF addTarget:self action:@selector(printBeginA:) forControlEvents:UIControlEventEditingDidBegin];
[textF addTarget:self action:@selector(printBeginB:) forControlEvents:UIControlEventEditingDidBegin];
[textF addTarget:self action:@selector(printBeginC:) forControlEvents:UIControlEventEditingDidBegin];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[textF removeTarget:self action:@selector(printBeginA:) forControlEvents:UIControlEventEditingDidBegin];
[textF addTarget:self action:@selector(printBeginD:) forControlEvents:UIControlEventEditingDidBegin];
});

刚开始时数据结构为:

UIControlEventEditingDidBegin : [self(printBeginA, printBeginB, printBeginC)]

调用removeTarget后

1
[textF removeTarget:self action:@selector(printBeginA:) forControlEvents:UIControlEventEditingDidBegin];

数据结构变为:

UIControlEventEditingDidBegin :[],有可能这一栏都没有了。

最后又addTarget,数据结构变为:

UIControlEventEditingDidBegin :[self(printBeginD)]

所以最后只有printBeginD响应了。

而重复addTarget

1
2
3
4
[textF addTarget:self action:@selector(printBegin:) forControlEvents:UIControlEventEditingDidBegin];
[textF addTarget:self action:@selector(printBegin:) forControlEvents:UIControlEventEditingDidBegin];
[textF addTarget:self action:@selector(printBegin:) forControlEvents:UIControlEventEditingDidBegin];
[textF addTarget:self action:@selector(printBegin:) forControlEvents:UIControlEventEditingDidBegin];

数据结构为:

UIControlEventEditingDidBegin :[self(printBegin)]

因此重复addTarget同一action没有什么影响,事件来了,action方法只会执行一次。

解决

删除类别里的removeTarget即可。

1
[self removeTarget:self action:@selector(xq_textFieldTextDidChanged:) forControlEvents:UIControlEventEditingChanged];

总结:

removeTarget没办法移除指定target的指定事件的指定action。这是在开发过程中需要注意的。

觉得文章有帮助可以打赏一下哦!