G.O.D的技术博客

小舟从此逝 代码寄余生


  • 首页

  • 归档

如何更好的自定义 UISegmentedControl

发表于 2019-03-17 | 更新于: 2019-03-17
字数统计: 3,109 | 阅读时长 ≈ 16

前几天因为业务需求,随手写了一个类似于 UISegmentedControl 的控件,写完后觉得代码没什么毛病,简洁好用,可读性也好,也仅仅只有 200 行代码。后来 Code Review 的时候,leader 说了一些观点让我觉得很受用。也让我觉得自定义 UI 控件其实需要更多的思考,而不仅仅是功能上面的实现。所以写篇文章记录下来。大致效果如下:

因为代码不多,就直接放到文章里了。

类的设计思路:
  1. View层面:一个 UIStackView,一堆 UILabel,一个用于滑动的 sliderview。
    UIStackView 负责管理一堆 Label ,Label 的背景色均为透明,sliderView 作为滑块充当 Label 的背景效果。
  2. 交互层面:根据 segmentItems 的数量计算出每个 Label 的宽度,再通过触摸点算出 location 所在的位置的 Label 和 index,然后更新 sliderView 的位置,同时更新 label 的文本颜色。

看一下我最早提交的版本。

类的声明:
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
#import <UIKit/UIKit.h>

@class OUPCornerSegmentedControl;

@protocol OUPCornerSegmentedControlDelegate<NSObject>

@optional

- (void)segmentedControl:(OUPCornerSegmentedControl*)control didSelectedItemAtIndex:(NSUInteger)index;

@end

@interface OUPCornerSegmentedControl : UIControl

@property (nonatomic) UIFont* font;
@property (nonatomic) UIColor* normalTextColor;
@property (nonatomic) UIColor* highlightTextColor;
@property (nonatomic) UIColor* sliderColor;
@property (nonatomic) CGFloat margin;

@property (nonatomic) NSArray<NSString*>* segmentItems;
@property (nonatomic, readonly) NSUInteger selectedSegmentIndex;

@property (nonatomic, weak) id<OUPCornerSegmentedControlDelegate> delegate;

- (void)setSelectedSegmentIndex:(NSUInteger)selectedSegmentIndex animated:(BOOL)animated;

@end
类的实现:
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
@interface OUPCornerSegmentedControl ()

@property (nonatomic) UIStackView* stackView;
@property (nonatomic) UIView* slider;
@property (nonatomic) CGFloat segmentWidth;

@end

@implementation OUPCornerSegmentedControl

- (instancetype)initWithCoder:(NSCoder*)coder
{
self = [super initWithCoder:coder];
if (self)
{
[self commonInit];
}
return self;
}

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

- (void)commonInit
{
_font = [UIFont systemFontOfSize:14];
_normalTextColor = UIColor.grayColor;
_highlightTextColor = UIColor.whiteColor;
_sliderColor = UIColor.darkGrayColor;
_margin = 2.0;
_selectedSegmentIndex = 0;

_slider = [[UIView alloc] init];
_slider.backgroundColor = _sliderColor;
[self addSubview:_slider];

_stackView = [[UIStackView alloc] init];
_stackView.axis = UILayoutConstraintAxisHorizontal;
_stackView.alignment = UIStackViewAlignmentFill;
_stackView.distribution = UIStackViewDistributionFillEqually;
[self addSubview:_stackView];
[_stackView mas_makeConstraints:^(MASConstraintMaker* make) {
make.edges.equalTo(self).insets(UIEdgeInsetsMake(_margin, _margin, _margin, _margin));
}];

[self addTapGesture];
}

- (void)layoutSubviews
{
[super layoutSubviews];

self.segmentWidth = (self.frame.size.width - 2 * self.margin) / self.segmentItems.count;

let sliderHeight = self.frame.size.height - 2 * self.margin;
self.slider.frame = CGRectMake(self.margin + self.selectedSegmentIndex * self.segmentWidth, self.margin, self.segmentWidth, sliderHeight);

[self setupCornerRadius];
}

- (void)setupCornerRadius
{
self.layer.cornerRadius = self.frame.size.height / 2;
self.slider.layer.cornerRadius = _slider.frame.size.height / 2;
}

- (void)insertAllSegments
{
let labelMaker = ^UILabel*(NSString* title, UIColor* textColor, UIFont* font)
{
let label = [[UILabel alloc] init];
label.text = title;
label.textColor = textColor;
label.font = font;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = UIColor.clearColor;

return label;
};

for (int i = 0; i < _segmentItems.count; I++)
{
let title = _segmentItems[I];
let label = labelMaker(title, self.normalTextColor, self.font);
[_stackView addArrangedSubview:label];
}
}

- (void)removeAllSegments
{
for (UIView* v in _stackView.arrangedSubviews)
{
[v removeFromSuperview];
}

_segmentItems = nil;
}

- (void)updateLabelsColor
{
for (UIView* v in _stackView.subviews)
{
if ([v isKindOfClass:[UILabel class]])
{
UILabel* label = (UILabel*)v;
label.textColor = self.normalTextColor;
}
}

UILabel* label = _stackView.arrangedSubviews[self.selectedSegmentIndex];
label.textColor = self.highlightTextColor;
}

- (void)addTapGesture
{
UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
[self addGestureRecognizer:tap];
}

- (NSUInteger)segmentIndexWithLocation:(CGPoint)point
{
NSUInteger index = (point.x - self.margin) / self.segmentWidth;

if (index < 0)
{
index = 0;
}
if (index > self.segmentItems.count - 1)
{
index = self.segmentItems.count - 1;
}

return index;
}

- (void)moveToIndex:(NSUInteger)index animated:(BOOL)animated
{
let frame = self.slider.frame;
let x = index * self.segmentWidth + self.margin;

let duration = animated ? kUPAnimationDurationShort : 0.0;
[UIView animateWithDuration:duration
animations:^{
self.slider.frame = CGRectMake(x, frame.origin.y, frame.size.width, frame.size.height);
}
completion:^(BOOL finished) {
_selectedSegmentIndex = index;
[self updateLabelsColor];
}];
}

#pragma mark - UIGestureRecognizer Actions

- (void)handleTapGesture:(UITapGestureRecognizer*)tapGR
{
CGPoint location = [tapGR locationInView:self];
NSUInteger index = [self segmentIndexWithLocation:location];
[self moveToIndex:index animated:YES];

if ([self.delegate respondsToSelector:@selector(segmentedControl:didSelectedItemAtIndex:)])
{
[self.delegate segmentedControl:self didSelectedItemAtIndex:index];
}
}

#pragma mark - getters && setters

- (void)setSliderColor:(UIColor*)sliderColor
{
_sliderColor = sliderColor;
_slider.backgroundColor = sliderColor;
}

- (void)setSelectedSegmentIndex:(NSUInteger)selectedSegmentIndex animated:(BOOL)animated;
{
_selectedSegmentIndex = selectedSegmentIndex;

[self moveToIndex:selectedSegmentIndex animated:animated];
}

- (void)setSegmentItems:(NSArray<NSString*>*)segmentItems
{
if (_segmentItems)
{
[self removeAllSegments];
}

if (segmentItems.count == 0)
{
return;
}

_segmentItems = [NSArray arrayWithArray:segmentItems];

[self insertAllSegments];
[self updateLabelsColor];
}

@end

现在让我们来看看有什么问题。

实现机制
  1. segmentWidth 这个属性真的有存在的必要吗?通过算坐标值来确定点击到的 UILabel 当然可行,但是计算性的代码写的再好毕竟也不能一目了然。有没有可读性更好的做法?
  2. 既然是 UIControl 的子类,是不是有比 Delegate 更好的机制来传递事件?
  3. 既然重写了selectedSegmentIndex 的 setter 函数,为什么还存在使用下划线变量赋值的代码?
命名问题
  1. Corner 代表的是拐角,并不能描述出圆角的概念
  2. highlightTextColor 是高亮字体色,并不能描述出选中字体色的概念
  3. margin 代表的是外间距,并不是内间距

其实 margin 是 css 布局中的概念,放张图科普一下

  1. slider 是滑动的意思,如果你将一个对象命名为 sliderView。那么至少这个类可以支持拖动,类似于 UISlider 的效果。
  2. - (void)moveToIndex:(NSUInteger)index animated:(BOOL)animated 是不是说清楚 move 什么 to index 比较好?
接口设计
  1. - (void)moveToIndex:(NSUInteger)index animated:(BOOL)animated 的实现中为什么会调用 updateLabelsColor 和更新 _selectedSegmentIndex 的值?简简单单的 move 接口应该做这么多事么?
  2. 声明的可读属性并没有全部重写 setter 函数 。不能默认调用者知道先传入 segmentItems 再设置其他属性才能生效。

当时一下子抛出这么多问题,我有点哑口无言,本来想着一个只有200行代码的类,使用一些 frame 不是很方便么?Delegate 也很好用。然后仔细想了想,这些问题确实很有道理。不知道读到这里,你们的想法是什么?

现在我们来看一下修改之后的代码

修改后的代码:

命名问题
  1. Renamed CornerSegmentedControl -> RoundCornerSegmentedControl
  2. Renamed highlightTextColor -> selectedTextColor
  3. Renamed margin -> padding
  4. Renamed slider -> indicator
  5. Renamed - (void)moveToIndex:(NSUInteger)index animated:(BOOL)animated -> - (void)moveIndicatorToSegmentAtIndex:(NSInteger)index animated:(BOOL)animated
实现机制
  1. 去掉了segmentWidth 属性。既然已经用了 UIStackView 来管理视图,那么通过 UIStackView + AutoLayout 完全可以省去坐标计算的问题 。当用户点击的时候,遍历所有的 label ,判断坐标点在哪一个 label 上,然后更新 indicatorView的布局和 label 的字体颜色。
  2. 使用 Target-Action 机制。在恰当的时机调用
    - (void)sendActionsForControlEvents:(UIControlEvents)controlEvents
    来触发 action。这样既和系统的行为保持一致,对于调用方而言,成本也会小很多。
接口设计

将 - (void)updateLabelsColorAnimated:(BOOL)animated 和 _selectedSegmentIndex = index;从 - (void)moveIndicatorToSegmentAtIndex:(NSInteger)index animated:(BOOL)animated 中拿出来,保证了接口的单一原则:move 应该只做和视图移动有关的事情,它不应该去更新文本颜色,也不应该去更新当前选中的索引值。

类的声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface OUPRoundCornerSegmentedControl : UIControl

@property (nonatomic) UIFont* font; // default [UIFont systemFontOfSize:14]
@property (nonatomic) UIColor* normalTextColor; // default UIColor.grayColor
@property (nonatomic) UIColor* selectedTextColor; // default UIColor.whiteColor
@property (nonatomic) UIColor* indicatorColor; // default UIColor.darkGrayColor
@property (nonatomic) CGFloat edgeInset; // default 2.0

@property (nonatomic) NSArray<NSString*>* segmentItems;
@property (nonatomic) NSInteger selectedSegmentIndex; // default 0

- (void)setSelectedSegmentIndex:(NSInteger)selectedSegmentIndex animated:(BOOL)animated;
- (void)setNormalTextColor:(UIColor*)normalTextColor animated:(BOOL)animated;
- (void)setSelectedTextColor:(UIColor*)selectedTextColor animated:(BOOL)animated;

@end
类的实现
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
@interface OUPRoundCornerSegmentedControl ()

@property (nonatomic) UIStackView* stackView;
@property (nonatomic) UIView* indicatorView;

@end

@implementation OUPRoundCornerSegmentedControl

- (instancetype)initWithCoder:(NSCoder*)coder
{
self = [super initWithCoder:coder];
if (self)
{
[self commonInit];
}
return self;
}

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

- (void)commonInit
{
_font = [UIFont systemFontOfSize:14];
_normalTextColor = UIColor.grayColor;
_selectedTextColor = UIColor.whiteColor;
_indicatorColor = UIColor.darkGrayColor;
_edgeInset = 2.0;
_selectedSegmentIndex = 0;

_indicatorView = [[UIView alloc] init];
_indicatorView.backgroundColor = _indicatorColor;
_indicatorView.userInteractionEnabled = NO;
[self addSubview:_indicatorView];

_stackView = [[UIStackView alloc] init];
_stackView.axis = UILayoutConstraintAxisHorizontal;
_stackView.alignment = UIStackViewAlignmentFill;
_stackView.distribution = UIStackViewDistributionFillEqually;
_stackView.userInteractionEnabled = NO;
[self addSubview:_stackView];

[_stackView mas_makeConstraints:^(MASConstraintMaker* make) {
make.edges.equalTo(self).insets(UIEdgeInsetsMake(_edgeInset, _edgeInset, _edgeInset, _edgeInset));
}];
}

- (void)layoutSubviews
{
[super layoutSubviews];

self.layer.cornerRadius = self.frame.size.height / 2;
_indicatorView.layer.cornerRadius = _indicatorView.frame.size.height / 2;
}

- (void)insertAllSegments
{
let labelMaker = ^UILabel*(NSString* title, UIColor* textColor, UIFont* font)
{
let label = [[UILabel alloc] init];
label.userInteractionEnabled = NO;
label.text = title;
label.textColor = textColor;
label.font = font;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = UIColor.clearColor;

return label;
};

foreach (title, _segmentItems)
{
let label = labelMaker(title, _normalTextColor, _font);
[_stackView addArrangedSubview:label];
}
}

- (void)removeAllSegments
{
foreach (segment, _stackView.arrangedSubviews)
{
[segment removeFromSuperview];
}

_segmentItems = nil;
}

- (void)updateLabelsColorAnimated:(BOOL)animated
{
UILabel* selectedLabel = nil;
if (_selectedSegmentIndex >= 0 && _selectedSegmentIndex < _stackView.arrangedSubviews.count)
{
selectedLabel = _stackView.arrangedSubviews[_selectedSegmentIndex];
}

let duration = animated ? kUPAnimationDurationShort : 0.0;

[UIView animateWithDuration:duration
animations:^{
foreach (segment, _stackView.arrangedSubviews)
{
let label = SC_STATIC_CAST(segment, UILabel);
let isSelected = label == selectedLabel;
label.textColor = isSelected ? _selectedTextColor : _normalTextColor;
}
}
completion:nil];
}

- (NSUInteger)segmentIndexAtPoint:(CGPoint)point
{
for (int i = 0; i < _stackView.arrangedSubviews.count; I++)
{
let segment = _stackView.arrangedSubviews[I];
let frame = segment.frame;
let containsPoint = point.x >= CGRectGetMinX(frame) && point.x <= CGRectGetMaxX(frame);
if (containsPoint)
{
return I;
}
}

if (point.x < 0)
{
return 0;
}
else
{
return _segmentItems.count - 1;
}
}

- (void)moveIndicatorToSegmentAtIndex:(NSInteger)index animated:(BOOL)animated
{
if (index == UISegmentedControlNoSegment)
{
SCNotImplementedAssert();
return;
}

let selectedSegment = _stackView.arrangedSubviews[index];
let duration = animated ? kUPAnimationDurationShort : 0.0;

[UIView animateWithDuration:duration
animations:^{
[_indicatorView mas_remakeConstraints:^(MASConstraintMaker* make) {
make.edges.equalTo(selectedSegment);
}];

[self layoutIfNeeded];
}
completion:nil];
}

#pragma mark - Touch

- (void)endTrackingWithTouch:(UITouch*)touch withEvent:(UIEvent*)event
{
let location = [touch locationInView:self];
let index = [self segmentIndexAtPoint:location];

if (index != _selectedSegmentIndex)
{
[self setSelectedSegmentIndex:index animated:YES];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}

[super endTrackingWithTouch:touch withEvent:event];
}

#pragma mark - getters && setters

- (void)setFont:(UIFont*)font
{
_font = font;

foreach (segment, _stackView.arrangedSubviews)
{
SC_STATIC_CAST(segment, UILabel).font = font;
}
}

- (void)setNormalTextColor:(UIColor*)normalTextColor
{
[self setNormalTextColor:normalTextColor animated:NO];
}

- (void)setNormalTextColor:(UIColor*)normalTextColor animated:(BOOL)animated
{
_normalTextColor = normalTextColor;

[self updateLabelsColorAnimated:animated];
}

- (void)setSelectedTextColor:(UIColor*)selectedTextColor
{
[self setSelectedTextColor:selectedTextColor animated:NO];
}

- (void)setSelectedTextColor:(UIColor*)selectedTextColor animated:(BOOL)animated
{
_selectedTextColor = selectedTextColor;

[self updateLabelsColorAnimated:animated];
}

- (void)setIndicatorColor:(UIColor*)indicatorColor
{
_indicatorColor = indicatorColor;
_indicatorView.backgroundColor = indicatorColor;
}

- (void)setEdgeInset:(CGFloat)edgeInset
{
_edgeInset = edgeInset;

[_stackView mas_remakeConstraints:^(MASConstraintMaker* make) {
make.edges.equalTo(self).insets(UIEdgeInsetsMake(_edgeInset, _edgeInset, _edgeInset, _edgeInset));
}];
}

- (void)setSelectedSegmentIndex:(NSInteger)selectedSegmentIndex
{
[self setSelectedSegmentIndex:selectedSegmentIndex animated:NO];
}

- (void)setSelectedSegmentIndex:(NSInteger)selectedSegmentIndex animated:(BOOL)animated;
{
if (selectedSegmentIndex == UISegmentedControlNoSegment)
{
SCNotImplementedAssert();
return;
}

if (selectedSegmentIndex < UISegmentedControlNoSegment || selectedSegmentIndex >= _segmentItems.count)
{
SCParameterAssert(selectedSegmentIndex > UISegmentedControlNoSegment && selectedSegmentIndex < _segmentItems.count);
return;
}

_selectedSegmentIndex = selectedSegmentIndex;

dispatch_async(dispatch_get_main_queue(), ^{
[self updateLabelsColorAnimated:animated];
[self moveIndicatorToSegmentAtIndex:selectedSegmentIndex animated:animated];
});
}

- (void)setSegmentItems:(NSArray<NSString*>*)segmentItems
{
if (_segmentItems)
{
[self removeAllSegments];
}

NSInteger selectedSegmentIndex = _selectedSegmentIndex;

if (_selectedSegmentIndex >= segmentItems.count)
{
selectedSegmentIndex = UISegmentedControlNoSegment;
}

if (segmentItems.count == 0)
{
SCNotImplementedAssert();
return;
}

_segmentItems = [NSArray arrayWithArray:segmentItems];

[self insertAllSegments];

[self setSelectedSegmentIndex:selectedSegmentIndex animated:NO];
}

@end

改完之后的代码的可读性明显比之前要好很多,无论从接口设计,还是命名上都比之前要规范
(虽然我觉得之前的代码可读性也不差哈哈)。

其实目前的代码看起来还有一些扩展性问题。不过因为业务目前并不需要,我就没有再调整代码了。这里说一下问题,有兴趣的朋友可以自己实现一下。
因为现在仅仅支持两种文本颜色,选中和未选中。如果日后需要支持更多的文本颜色,肯定需要增加一些额外的参数,变量和接口来维护不同的样式。

考虑到整个类是继承自 UIControl ,所以对于字体颜色的设置如果改成和 UIButton 相同的行为就会简单很多:

- (void)setTextColor:(UIColor*)color forState:(UIControlState)state

日后如果需要支持图片,那么接口设计和内部实现也会变得非常简单

- (void)setImage:(UIColor*)color forState:(UIControlState)state
- (void)setImage:(UIColor*)color forState:(UIControlState)state atIndex:(NSInteger)index

所以,如果你要设计一个继承自 UIControl 的类,那么应该注意以下几点:

  1. 使用 Target-Action 机制
  2. 善用 UIControlState 来管理 UI 的行为

脱离 UIControl 的设计原则,来讲讲一些其他基本的注意点:

  1. 如果你重新实现了一个属性的 setter 函数,那么务必确保该变量的值只会在其 setter 函数中更改,在其他接口中你不应该直接使用点语法赋值,应该通过调用 setter 函数来赋值。
  2. 苹果为我们提供了很好的 AutoLayout 机制,应当好好使用,并且 WWCD2018 上也提到了 AutoLayout 性能会得到本质性的改善。重复计算 frame 这种事情除非必要,比如设置 layer 的圆角等,应该学会尽量用 AutoLayout 去解决问题。

Button的代码重构:用状态机来管理状态

发表于 2018-09-14 | 更新于: 2018-10-16
字数统计: 2,033 | 阅读时长 ≈ 8

最近几周的时间都在重构一个控件。仿照 AppStore 中的下载效果写的一个按钮。写篇文章记录一下心得体会。

文章分为重构前,为什么重构,以及重构后三部分去叙述代码层面的设计思想。这里规避了繁琐的业务逻辑,以及为了解决实际应用场景中 cell 的复用而做的一些处理,仅仅聚焦于类的职责设计。

先看一下效果图

IMG_1079.TRIM.gif

重构前的代码

UPDownloadButtonDefines.h

UPDownloadButtonDefines.h 中用枚举定义了 DownloadButton 的 state,每种状态对应一种UI形态。

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSUInteger, EUPDownloadButtonState) {
nUPDownloadButtonStateIdle, //闲置状态,等待下载
nUPDownloadButtonStatePending, //开始下载,还没有接收到数据
nUPDownloadButtonStateDownloading, //接收到数据
nUPDownloadButtonStateDownloaded, //下载完成

nUPDownloadButtonStateDefault = nUPDownloadButtonStateIdle,
};

OUPDownloadButton

OUPDownloadButton 继承自 UIView,它是整个库的核心。它负责:

  1. 初始化一些必要的元素: 比如 OUPDownloadButtonTitleView , 图片,UIControl,转圈图层,进度环图层等。
  2. 处理点击事件。
  3. 管理 state。
  4. 管理 state 相关的 UI 代码。

看一下 OUPDownloadButton 的头文件:

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
@protocol OUPDownloadButtonDelegate<NSObject>

@optional
- (void)downloadButtonDidStartDownloading:(OUPDownloadButton*)downloadButton;
- (void)downloadButtonDidCancelDownloading:(OUPDownloadButton*)downloadButton;
- (void)downloadButtonDidTapAfterDownload:(OUPDownloadButton*)downloadButton;

@end

@interface OUPDownloadButton : UIView

@property (nonatomic) OUPDownloadButtonConfiguration* configuration;

// idle -> pending -> downloading -> downloaded
@property (nonatomic, readonly) EUPDownloadButtonState state;

@property (nonatomic, weak) id<OUPDownloadButtonDelegate> delegate;

// 0.0 ~ 1.0
@property (nonatomic) CGFloat progress;

- (void)start;
- (void)cancel;
- (void)complete;
- (void)restoreWithState:(EUPDownloadButtonState)state progress:(CGFloat)progress;

@end
  • DownloadButton 通过代理来建立与 Controller 的联系。通知任务下载开始,结束,以及取消时的事件。
  • 持有 state 属性,因为不希望在 DownloadButton 以外的地方更改 state ,所以设计成了 readonly。
  • 提供 start 等方法给外界调用,切换 DownloadButton 的状态。

OUPDownloadButtonConfiguration

OUPDownloadButtonConfiguration 用于配置 DownloadButton 在不同状态时的 UI 样式(比如 图片 字体 颜色等)

OUPDownloadButtonTitleView

OUPDownloadButtonTitleView 是按钮的标题视图,它包含了一个 Calyer 和 UILabel 。因为 OUPDownloadButton 的 UI 形态在切换的时候会有一些过度动画,这个过度动画主要由 OUPDownloadButtonTitleView 来负责,所以单独抽个类出来也易于管理和设计接口。

看一下 DownloadButton 的点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)onTap:(id)sender
{
switch (_state)
{
case nUPDownloadButtonStateIdle:
[self exitIdle];
[self enterPendingAndNotify:YES];
break;
case nUPDownloadButtonStatePending:
[self cancelAndNotify:YES];
break;
case nUPDownloadButtonStateDownloading:
[self cancelAndNotify:YES];
break;
case nUPDownloadButtonStateDownloaded:
[self tappedAfterDownload];
break;
default:
SCShouldNotReachAssert();
break;
}
}

当我们点击 DownloadButton ,代码的执行思路大致是这样:根据当前的不同 state执行不同的方法,并手动更新 state 的值。比如当按钮处于 idle 的时候,点击进入 pending。当按钮处于 pending 或是 downloading 的时候,点击取消下载,恢复到idle。

MVC下的实际使用:
屏幕快照 2018-10-15 下午3.08.22.png

为什么需要重构

看完第一节我们就很清楚 DownloadButton 这个类的臃肿性了。如果我们需要额外增加一种状态,那么我们需要在 DownloadButton 中增加额外的状态判断和切换,对应的UI更新。这样 DownloadButton 会变得越来越难以维护,代码的可读性也会越来越差。

DownloadButton 既然是一个 UIView,那么它就应该只负责 UI 层面的事情,state可以交给专业的类去管理,这样既可以减轻 DownloadButton 的负担,又可以让DownloadButton 遵守面向对象的单一原则。所以 DownloadButton 中实际上不应该出现和 state 有关的任何代码。

或许我们可以做的更多。UI层面的事情我们也可以用 Category 来进一步管理。将state 相关的 UI 更新抽到分类中来专门维护。

所以我们希望重构之后的 DownloadButton 既不用关心 state,又不用关心 state 相关的 UI 更新。

重构后的代码

现在来看下重构后的库结构:

首先将类的命名改了: DownloadButton -> OperationButton 因为项目中可能会在其他场景下用到此类的 button,比如内购的支付按钮。所以命名上我们希望这个按钮能够适应所有可能的业务场景而不局限于下载。

OUPOperationButtonImageView

将之前的图片抽了一个单独的类出来 便于管理 结构更加清晰 也方便于接口设计。

OUPOperationButtonProcessView

将之前的转圈图层和进度环图层抽了一个单独的类出来 便于管理 结构更加清晰 也方便于接口设计。

OUPOperationButtonTransitionManager

OUPOperationButtonTransitionManager 负责状态的维护。
OUPOperationButtonTransitionManager 里面维护着一个状态机,在OUPOperationButtonTransitionManager 中我们可以定义各种状态及事件。状态机将状态和事件关联起来。

比如按钮从 state A切换到了 state B。那么我们将 A 称为 SourceState 将 B 称为DestinationState。我们将这个转换状态的过程定义为事件。事件的初始化依赖于SourceState 和 DestinationState。

每个事件的 SourceState 可以有多个,但是 DestinationState 只能有一个。当我们想切换状态的时候,就可以让状态机执行一个以目标状态为 DestinationState 的一个事件。如果当前状态是 SourceState 中的一个,那么这个事件将被正常执行。

比如 按钮从点击下载开始的状态转换过程为:

StateOriginal -> StateReady -> StateExecuting -> StateFinished

当我们想取消下载的时候 状态机会去执行一个 cancel 事件。当按钮在 StateReady的时候你可以 cancel,当按钮在 StateExecuting 的时候你也可以 cancel。但是无论你是从哪种状态 cancel的,cancel 事件成功后状态都会变成 StateOriginal。这里的StateReady 和 StateExecuting 叫做 SourceState。StateOriginal 叫做DestinationState。

那么问题来了,状态的管理问题是解决了,Button 已经不再自己维护状态了。不过如何通知 Button 在状态切换的时候更新UI呢?

嗯… 说的直白一点。我们对 OUPOperationButtonTransitionManager 这个类的期望是:

  1. 帮我们管理好 Button 的状态 当外界的 view action 到来时 OUPOperationButtonTransitionManager 会帮我们切换对应的状态
  2. 状态切换时能够及时通知 Button 去更新 UI

我们来看一下 OUPOperationButtonTransitionManager 的头文件

1
2
3
4
5
6
7
8
9
@interface OUPOperationButtonTransitionManager : NSObject

@property (nonatomic, readonly) EUPOperationButtonState currentState;

- (instancetype)initWithHandlers:(NSDictionary*)handlers;

- (void)fireEventWithTargetState:(EUPOperationButtonState)state;

@end

可以看到OUPOperationButtonTransitionManager有一个初始化方法

1
- (instancetype)initWithHandlers:(NSDictionary*)handlers;

handlers 以 state 作为 key 对应的 UI 更新事件(闭包)作为 value。OUPOperationButtonTransitionManager 会在状态切换的时候根据一个状态的进入或退出去执行对应的闭包。

示例代码

1
2
3
4
5
6
7
8
9
- (void)setDidEnterStateActionForState:(TKState*)state
{
SC_WEAKIFY(self);
[state setDidEnterStateBlock:^(TKState* state, TKTransition* transition) {
SC_STRONGIFY(self);
//state的字符串作为key从handler中取出对应的闭包 然后执行。
self.didEnterStateHandlers[state.name](nil);
}];
}

OUPOperationButton+FSM

OUPOperationButton+FSM 的职责:

  1. 维护状态相关的 UI 代码
  2. 创建 OUPOperationButtonTransitionManager
  3. 给外界提供更改状态的接口
1
2
3
4
5
6
@interface OUPOperationButton (FSM)

- (void)setupFSM;
- (void)transitionToTargetState:(EUPOperationButtonState)state;

@end

在 OUPOperationButton+FSM.m 中我们定义了各种状态切换的 UI 更新代码,那么理所当然,OUPOperationButtonTransitionManager 应该在OUPOperationButton+FSM 中被初始化。

OUPOperationButton_Internal.h

OUPOperationButton 拥有了可以管理状态的OUPOperationButtonTransitionManager 以及管理 state 相关的 UI 代码的OUPOperationButton+FSM。不过当我们将 OUPOperationButton 封装成一个静态库的时候,我可不希望将这两个类的头文件暴露出去。因为调用者并不需要知道OUPOperationButton 是怎么实现的。所以这里还需要一个 internal.h 来帮助我们隐藏掉我们不想暴露给外界的头文件。我们将 Button 中的各个控件以及OUPOperationButtonTransitionManager 放到 Internal.h 中供 Button 模块内部调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "OUPOperationButton.h"
#import "OUPOperationButtonImageView.h"
#import "OUPOperationButtonProcessView.h"
#import "OUPOperationButtonTextView.h"
#import "OUPOperationButtonTransitionManager.h"

@interface OUPOperationButton ()

@property (nonatomic) UIControl* backingControl;
@property (nonatomic) OUPOperationButtonTextView* textView;
@property (nonatomic) OUPOperationButtonImageView* imageView;
@property (nonatomic) OUPOperationButtonProcessView* processView;

@property (nonatomic) OUPOperationButtonTransitionManager* transitionManager;

--- state相关的UI更新接口

@end

现在我们想在 Button 中额外增加一种情况的话,我们需要在OUPOperationButtonTransitionManager 中定义新的 state 和事件,在OUPOperationButton+FSM 中定义对应的UI更新。这样明显比一股脑的全塞在Button 中要清晰的多。

记一次CATextLayer的使用心得

发表于 2018-09-06 | 更新于: 2018-10-16
字数统计: 1,123 | 阅读时长 ≈ 5

今天优化代码的时候发现了一个好玩的事情。

需求是实现一个头部显示value值的Slider控件。这个Slider我们是自己绘制的一个UIControl对象,它的trackline和thumbIcon都是我们用layer绘制上去的,因为Calayer本身具有隐性动画,所以最后的效果和系统几乎是完全一致。

一开始的时候,我就直接用UILabel来绘制这个value。然后将label的size,font以及color等属性暴露给调用者,后来想想size直接放在外面不大好,毕竟别人想调用一个可以显示数值的Slider还需要关心label的大小?

于是就在实现文件中声明了一个变量,给定了一个宽度(一个我认为足够大的值),但是因为字体的大小不固定,所以label的高度没法确定。一开始想的是在font的set方法中用[valueLabel sizetofit]来计算出label的height。后来我查到了一个UIFont的属性lineHeight,lineHeight可以根据不同font值计算出合适的高度,这个值你只需要调用lineHeight的getter方法就可以拿到了,不用计算,非常方便。例如:

1
2
3
4
5
UIFont *font0 = [UIFont systemFontOfSize:15];
UILabel *label0 = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 100, font0.lineHeight;)];

UIFont *font1 = [UIFont systemFontOfSize:25];
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 100, font1.lineHeight)];

这样高度的问题是解决了,但是调试的时候发现动画有问题。

因为要实现一个thumb顶部显示滑动值的文本效果,且希望当其他操作事件更改了滑动值的时候,Slider上需要有一个滑动的动画,thumb自然没有问题,但是对于UILabel来说,我尝试了UIView的基本动画,通过[CATransition animationDuration]拿到Calayer的隐式动画时间来设置UIView的显性动画。但是效果看起来不大好。你会发现,UIView动画的执行会比Calayer稍微慢了那么一点点。整个效果看起来就会有点不连续的感觉。

解决这个问题有两种思路:

  1. 研究Calayer隐式动画的曲线 找出和UIView动画的区别 然后解决
  2. 换掉UILabel 用CATextLayer来绘制文本

我觉得1的复杂度有点深,所以选择了2。不过关于CATextLayer,也是有一些坑的,花了我快半个小时才将这个效果弄好。

首先CATextLayer的几个属性比较特别:

  • CATextLayer的string属性是一个id类型,但是注释中明确的告诉你,要渲染的文本应该是NSString或NSAttributedString中的一种类型。
1
2
3
4
/* The text to be rendered, should be either an NSString or an
* NSAttributedString. Defaults to nil. */

@property(nullable, copy) id string;
  • CATextLayer的font属性是一个CFTypeRef类型。你可以传入一个CTFontRef或者CTFontRef类型,或者直接传入一个字体的名称。默认是Helvetica。font属性仅仅用于当字符串类型是NSAttributedString时。

    1
    2
    3
    4
    5
    /* The font to use, currently may be either a CTFontRef, a CTFontRef,
    * or a string naming the font. Defaults to the Helvetica font. Only
    * used when the `string' property is not an NSAttributedString. */

    @property(nullable) CFTypeRef font;
  • CATextLayer的fontSize是一个CGFloat类型

    1
    2
    3
    4
    /* The font size. Defaults to 36. Only used when the `string' property
    * is not an NSAttributedString. Animatable (Mac OS X 10.6 and later.) */

    @property CGFloat fontSize;
  • foregroundColor用于绘制text的颜色,默认是不透明的白色,仅仅用于string不是一个NSAttributedString类型时

    1
    2
    3
    4
    5
    /* The color object used to draw the text. Defaults to opaque white.
    * Only used when the `string' property is not an NSAttributedString.
    * Animatable (Mac OS X 10.6 and later.) */

    @property(nullable) CGColorRef foregroundColor;
  • text的绘制位置,居中,居右等,类似于UILabel的textAlignment

    1
    2
    3
    /* Describes how individual lines of text are aligned within the layer
    * bounds. The possible options are `natural', `left', `right',
    * `center' and `justified'. Defaults to `natural'. */

其次换成了CATextLayer之后,我将font和fontSize以及foregroundColor属性暴露了出去,因为之前可以通过UIFont的lineHeight属性直接拿到UILabel对于不同字体自适应的高度值,所以这里没有用UIFont后,就换成了sizeWithAttributes:或者sizeWithFont:方法来搞定。

后来codeReview的时候,我觉得完全可以和以前一样,直接将UIFont和UIColor暴露给外部,因为在UIFont的set方法中,通过CGFontCreateWithFontName((__bridge CFStringRef)labelFont.fontName)和labelFont.pointSize就可以拿到对应的CATextLayer的font和fontSize的属性,内部声明两个变量记录就好了。因为调用者其实应该是更熟悉UIFont的,而且只传一个属性简便的多。对于UIColor,set方法中取出它的CGColor就好,之前设置的暴露参数都不用修改,只需要内部做一下改动。

Type Inference with __auto_type in Objective-C

发表于 2018-09-04 | 更新于: 2018-09-05
字数统计: 335 | 阅读时长 ≈ 1

写Objective-C的时候,因为要遵循驼峰命名法,所以写一个变量之前,首先要想如何优雅的为其命名,然后再用对应的类型去接收它。很多时候,这个类型需要重复的去写,非常麻烦。

我们知道在Swift里面,有两种申明变量的方式:let 和 var
let 用于定义常量,定义完后不能修改
var 用于定义变量,定义完后可以修改

申明常量或者变量的同时如果赋值的话,编译器会自动推断类型,列如
let age0 = 10
var age1 = "10"
age0为int型,age1为String型

######那在Objective-C中如何做到这样呢?
得益于苹果在Xcode8中已经支持类型推倒,我们就可以用C的__auto_type和C++的auto来定义一个宏let 这样在项目了我们就可以用let来接收一切类型了

1
2
3
4
5
6
7
8
9
10
11
#if !defined(var)
#if defined(__cplusplus)
#define var auto
#else
#define var __auto_type
#endif
#endif

#if !defined(let)
#define let const var
#endif

比如
定义一个结构体类型 let trackFrame = self.trackLayer.frame;
定义一个数组类型 let selected = [self indexPathsForSelectedItems];
定义一个字符串类型 let title = @"i am a text";

关于Type Inference with __auto_type的更多信息:
https://intii.com/2017/write-objc-like-writing-swift/
https://medium.com/@maicki/type-inference-with-auto-type-55a38ef56372

Runloop知识点小集

发表于 2018-07-31 | 更新于: 2018-07-31
字数统计: 1,403 | 阅读时长 ≈ 6

一. Runloop简介

  1. RunLoop字面意思就是一个跑起来的循环,我们的程序之所以能一直运行不会无端退出就是因为RunLoop的存在。
  2. RunLoop用来处理程序运行过程中出现的各种事件,从而保持程序的持续运行。而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。

二. Runloop的作用

1. 保持程序的一直运行

每个iOS程序都有一个main函数的入口:

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

程序之所以能一直运行是因为UIApplicationMain函数创建了一个Runloop维持着程序运行。
如果我们将main函数改成下面这样,那么程序一旦启动就会结束。

1
2
3
4
5
6
int main(int argc, char * argv[]) {
@autoreleasepool {

}
return 0;
}

2. 处理程序运行中的各种事件:

触摸事件
定时器事件
手势识别
界面刷新
网络请求
Selector
…

三. 获取Runloop对象

iOS中有两套API可以访问和使用Runloop:

  1. Foundation:NSRunloop

    1
    2
    3
    4
    //获取当前线程的RunLoop对象
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    //获取主线程的RunLoop对象
    NSRunLoop *runloop2 = [NSRunLoop mainRunLoop];
  2. Core Foundation:CFRunloopRef

    1
    2
    3
    4
    //获取当前线程的RunLoop对象
    CFRunLoopRef runloop3 = CFRunLoopGetCurrent();
    //获取主线程的RunLoop对象
    CFRunLoopRef runloop4 = CFRunLoopGetMain();

NSRunloop是基于CFRunloopRef的OC包装。

四. Runloop与线程

  1. 每条线程都有唯一的一个与之对应的RunLoop对象
  2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  3. 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  4. RunLoop会在线程结束时销毁
  5. 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

五. Runloop相关的类

CFRunLoopRef:Runloop类
CFRunLoopModeRef:CFRunLoopMode类
CFRunLoopSourceRef:是事件产生的地方。Source有两个版本:Source0 和 Source1。
CFRunLoopTimerRef:基于时间的触发器,它是NSTimer的C语言版本,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef: 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

CFRunLoop的源码结构(__CFRunLoop结构体中包含的元素不单单是这几个,其他不重要的就没有往里面写了,CFRunLoopMode的源码结构同理)

1
2
3
4
5
6
7
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};

pthread_t _pthread:Runloop对象当前所处的线程。
CFMutableSetRef _commonModes:_commonModes是一个集合,里面装的是通用的模式,即Runloop对象可以运行的模式。
CFMutableSetRef _commonModeItems:_commonModeItems也是一个集合,里面装的是通用模式中的source0,source1,obersvers,timers等对象。
CFRunLoopModeRef _currentMode:Runloop对象当前所处的模式。
CFMutableSetRef _modes:Runloop对象可以使用的模式集合。

CFRunLoopMode的源码结构

1
2
3
4
5
6
7
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};

CFStringRef _name:字符串名称
CFMutableSetRef _sources0:包含了一个回调(函数指针),它并不能主动触发事件。使用时,需要将这个 Source 标记为待处理,然后唤醒 RunLoop,让其处理这个事件。
CFMutableSetRef _sources1:包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。 能主动唤醒 RunLoop 的线程。
CFMutableArrayRef _observers:_observers集合
CFMutableArrayRef _timers:_timers集合

六. 常见的几种CFRunLoopMode

  1. kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. kCFRunLoopCommonModes:并不是真正的CFRunLoopMode,而是代表了所有标有common属性的CFRunLoopMode,是一个模式集合。当绑定一个事件源到这个模式集合的时候就相当于绑定到了集合内的每一个模式。上面两个Mode就标有common属性。

七. NSTimer在UITrackingRunLoopMode模式下的Runloop中失效的问题

NSTimer的创建方式有两种:
scheduledTimerWithTimeInterval:

1
2
3
4
__block NSInteger count = 0;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%ld",(long)count++);
}];

timerWithTimeInterval:

1
2
3
4
__block NSInteger count = 0;\
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%ld",(long)count++);
}];

运行上面两段代码,你会发现第一种方式会一直正常的执行打印信息,而第二种创建方式不会有任何打印信息。因为通过scheduledTimerWithTimeInterval创建的timer会被自动加入到kCFRunLoopDefaultMode下的Runloop中执行(就像方法名一样该Timer被安排好执行了)。而通过timerWithTimeInterval:创建的timer需要手动添加到Runloop中执行。

所以通过scheduledTimerWithTimeInterval创建的timer在界面滚动的时候会停止打印,因为界面一旦滚动,当前Runloop会自动切换到UITrackingRunLoopMode模式,而此时timer只能在kCFRunLoopDefaultMode中执行,所以才会失效。
我们可以将创建出来的timer添加到通用模式中执行,这样不管当前Runloop处于哪种模式,timer的执行都不会受到影响:

1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

这里需要注意的是,如果Timer是添加到特定模式下的Runloop中,那么timer对象会存储在CFRunLoopMode的CFMutableArrayRef _timers中。如果Timer是添加到通用模式下的Runloop中,timer对象会储存在CFRunLoop的CFMutableSetRef _commonModeItems中。

CFRunloopRef的源码是完全公开的,可以通过以下链接访问下载源码: https://opensource.apple.com/tarballs/CF/

探究KVO的实现原理

发表于 2018-06-28 | 更新于: 2018-09-05
字数统计: 2,965 | 阅读时长 ≈ 12

一. KVO简介

熟悉iOS开发者模式的都知道 Key-Value-Observe(观察者模式)
该模式的实现基于三个方法:

  1. 给某个对象实例添加监听

    1
    2
    //参数的意思分别是 要被监听的对象 要被监听的属性 监听的类型 上下文
    addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>
  2. 监听事件的回调

    1
    2
    //参数的意思分别是 要被监听的属性 要被监听的对象 被监听的属性发生的改变 上下文
    observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
  3. 移除监听事件

    1
    2
    //参数的意思分别是 需要移除监听的对象 需要移除的监听属性
    removeObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#>

通过比较这两个方法可以发现,很多在注册监听的时候传入的参数在监听回调的方法中都有返回。比如监听的对象,属性以及我们传入的上下文。

二. KVO实现原理

我们通过一个简单的列子来探寻KVO的内部实现细节:

创建一个名为ZBYObject的类。生成两个实例对象,给其中一个实例对象增加监听:

1
2
3
4
@interface ViewController ()
@property (nonatomic,strong) ZBYObject *obj1;
@property (nonatomic,strong) ZBYObject *obj2;
@end

1
2
3
4
5
6
7
self.obj1 = [[ZBYObject alloc]init];
self.obj1.profession = @"Singer";
self.obj2 = [[ZBYObject alloc]init];
self.obj2.profession = @"Student";

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.obj1 addObserver:self forKeyPath:@"profession" options:options context:nil];
1
2
3
4
5
6
7
8
9
10
11
12
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.obj1.profession = @"Doctor";
self.obj2.profession = @"Teacher";
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了:%@",self.obj1,keyPath,change);
}

-(void)dealloc{
[self.obj1 removeObserver:self forKeyPath:@"profession"];
}

从以上代码来看,很容易能得出一个结论:当我们点击屏幕的时候,会Log出obj1的profession属性的变化。而obj2因为没有被监听,所以不会有任何相关的信息被log出。我们看到obj1的新值旧值都被打印了出来。这与我们添加监听的时候,传入的监听类型有关NSKeyValueObservingOptions。如果我们只想获得新值得话,去掉NSKeyValueObservingOptionOld就好了。对于NSKeyValueObservingOption来说,一般常用的两个值就是NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld。

那么对于obj1和obj2来说两者一定在哪方面是有些不同,所以当同时改变其profession属性值得时候,只有obj1可以监听到回调。

我们打印一下obj1和obj2的isa指针看看。

可以看到obj1被监听之后的isa指针指向了NSKVONotifying_ZBYObject这个类。这个类肯定不是我们自己创建的,所以这是系统通过Runtime动态添加的一个类。也就是说当我们给一个实例对象添加监听的时候,系统会自动通过运行时创建一个类。比如现在创建了一个类A,A的实例对象a被监听了,此时系统会动态创建一个叫NSKVONotifying_A的类,这个类是A的子类,原先实例a的isa指针是指向A的,现在指向NSKVONotifying_A。那我们看看具体NSKVONotifying_A和A有哪些不同,为什么NSKVONotifying_A就可以实现监听呢?

NSKVONotifying_XXX是什么

拥有一定编程基础的肯定都能意识到NSKVONotifying_ZBYObject这个类一定重载了某个方法或者实现了一些父类没有的方法才能监听,不然为什么系统要创建一个ZBYObject的子类来完成监听这件事呢?所以我们先获取NSKVONotifying_ZBYObject的实例方法,看看和ZBYObject的实例方法有什么不同。

这时候需要这样一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Describes the instance methods implemented by a class.
*
* @param cls The class you want to inspect.
* @param outCount On return, contains the length of the returned array.
* If outCount is NULL, the length is not returned.
*
* @return An array of pointers of type Method describing the instance methods
* implemented by the class—any instance methods implemented by superclasses are not included.
* The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
*
* If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
*
* @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
* @note To get the implementations of methods that may be implemented by superclasses,
* use \c class_getInstanceMethod or \c class_getClassMethod.
*/
OBJC_EXPORT Method _Nonnull * _Nullable
class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

这个函数的意思是传入一个class返回给你一个包含了class里面所有实例方法的数组指针,如果该class没有实例方法则返回NULL。这样的话我们可以通过这个函数拿到NSKVONotifying_ZBYObject的方法数组然后一一遍历打印。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-(void)printMethodNamesOfClass:(Class )cls{
unsigned int count;
NSMutableArray *methodsNameMA = [NSMutableArray array];
//获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
//遍历所 有方法
for (int i = 0; i<count; i++) {
Method method = methodList[i];
[methodsNameMA addObject:NSStringFromSelector(method_getName(method))];
}
//释放
free(methodList);
NSLog(@"%@ - %@",cls,methodsNameMA);
}
1
2
[self printMethodNamesOfClass:object_getClass(self.obj1)];
[self printMethodNamesOfClass:object_getClass(self.obj2)];

打印信息如下:

我做了一张对比图

可以很清楚的看到两者的区别:

  1. NSKVONotifying_ZBYObject重载了profession的setProfession方法
  2. NSKVONotifying_ZBYObject还实现了父类没有的class,dealloc和_isKVOA的方法

那么很明显,isKVO决定了NSKVONotifying_ZBYObject类能实现监听。不过因为拿不到苹果的源码,所以具体isKVO的实现我们并不清楚。不过我们可以来探讨一些其他的问题。

当我们想要去改变一个实例对象的某个属性值时,一定是通过该属性的set方法去改变的。而且刚刚我们也证实了NSKVONotifying_ZBYObject确实是重载了set方法。我们再分别打印一下NSKVONotifying_ZBYObject和ZBYObject的setProfession方法看一看。

这里要用到一个函数:

1
- (IMP)methodForSelector:(SEL)aSelector;

该函数用于返回一个方法的实现,我们可以打印出方法的地址。
IMP:一个函数指针,保存了方法的地址。在LLDB环境下,通过(IMP)+方法的地址可以打印出来该方法的实现细节。

1
2
NSLog(@"obj1添加KVO监听之前 - %p %p",[self.obj1 methodForSelector:@selector(setProfession:)],[self.obj2 methodForSelector:@selector(setProfession:)]);
NSLog(@"obj1添加KVO监听之后 - %p %p",[self.obj1 methodForSelector:@selector(setProfession:)],[self.obj2 methodForSelector:@selector(setProfession:)]);

我们发现两者的setProfession方法实现是不同的。添加了监听后,setProfession方法的实现打印出了foundation里面的一个NSSetIntValueAndNotify()方法。

通过字面意思可以知道NSSetIntValueAndNotify()是int类型的赋值与通知方法。也就是说,当obj1被监听之后,系统会通过动态创建一个监听类,并重载了该类对应属性的set方法,在set方法中增加了监听实现的相关方法NSSetIntValueAndNotify(),然后让obj1继承这个类。这样obj1就拥有了被监听的能力。

为什么会实现class和dealloc方法呢?

其实你有兴趣的话可以通过[self.obj1 class]来打印一下结果,你会惊奇的发现,打印出来的居然是ZBYObject类,不是说好的继承自 NSKVONotifying_ZBYObject吗?

这就是为什么NSKVONotifying_ZBYObject类会重写class方法的原因。如果 NSKVONotifying_ZBYObject没有重载class方法,那么对 NSKVONotifying_ZBYObject实例对象调用class方法会去元类里面找相应的实现,这样会一直找到NSObject里的class实现。而NSObject的class实现是这样的:那么最终当NSKVONotifying_ZBYObject实例对象调用class方法的返回结果就是NSKVONotifying_ZBYObject。但是苹果并不想让你知道这个类,因为这个类是系统动态添加的一个类,只是用于监听的实现,所以苹果通过重载类的class方法将这个类隐藏起来。

那么为什么会重载dealloc方法呢?

我自己的想法是:既然NSKVONotifying_ZBYObject类重载了被监听属性的set方法。在set方法中实现了监听相关的方法,所以需要在dealloc方法中移除监听,避免内存泄漏。

三. KVO的触发方式

那么问题来了,既然KVO的本质是动态创建一个类重载了被监听属性的set方法。那么如果直接去访问成员变量,能否触发KVO呢?

我们在ZBYObject的头文件中生成一个成员变量:

1
2
3
4
{
@public
NSString *_profession;
}

然后我们直接访问它

1
2
3
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.obj->_profession = @"Singer";
}

会发现监听方法并没有打印。所以直接访问成员变量并不会触发KVO。原因很简单,因为直接访问成员变量并没有触发其set方法,而重载set方法是KVO实现的本质。

我们再想一想,通过KVC来赋值的话能否触发KVO呢?这是个很有意思的事情。首先KVC和KVO在名称上就很相似。

KVC: KeyValueCoding一个非正式的Protocol,允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。

看完KVC的定义,相信读者心里已经有思路了。KVC并没有直接调用属性的存取方法,而是通过key名直接访问了对象的属性。既然是访问了属性,肯定是会访问其存取方法,只不过是间接的,所以KVC是可以触发KVO的。我们来验证一下:

1
2
3
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.obj setValue:@"Singer" forKey:@"profession"];
}

会发现监听方法有打印。

所以想要触发KVO,就一定要以某种方式触发监听属性的set方法。那么是否可以手动触发KVO呢?苹果官方文档中有这么一句话:

To implement manual observer notification, you invoke [willChangeValueForKey:] before changing the value, and [didChangeValueForKey:] after changing the value.

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html

我们来验证一下,在ZBYObject的实现文件里重写set方法,willChangeValueForKey:和didChangeValueForKey:方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)setProfession:(NSString *)profession{
_profession = profession;
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}

然后我们调用KVO,打印信息如下:

1
2
3
4
5
6
7
8
9
2018-07-26 11:44:03.969498+0800 KVO[20563:42844607] willChangeValueForKey: - begin
2018-07-26 11:44:03.969754+0800 KVO[20563:42844607] willChangeValueForKey: - end
2018-07-26 11:44:03.969868+0800 KVO[20563:42844607] didChangeValueForKey: - begin
2018-07-26 11:44:03.970206+0800 KVO[20563:42844607] {
kind = 1;
new = Singer;
old = Student;
}
2018-07-26 11:44:03.970342+0800 KVO[20563:42844607] didChangeValueForKey: - end

我们发现,willChangeValueForKey:和didChangeValueForKey:确实被调用了。而且在didChangeValueForKey:里调用了KVO的回调方法observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context。

现在让我们手动触发KVO:

1
2
3
4
5
6
self.obj.profession = @"Student";
NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew;
[self.obj addObserver:self forKeyPath:@"profession" options:option context:nil];

[self.obj willChangeValueForKey:@"profession"];
[self.obj didChangeValueForKey:@"profession"];

打印信息如下:

1
2
3
4
5
2018-07-26 11:57:38.808634+0800 KVO[20846:42857378] {
kind = 1;
new = Student;
old = Student;
}

这样,即使我们没有手动改变profession的值,但是我们通过手动调用willChangeValueForKey:和didChangeValueForKey:触发了KVO。

进一步我们可以推断,NSSetXXXValueAndNotify()里也应该是先后调用了willChangeValueForKey:,被监听属性的set方法和didChangeValueForKey:。

四. 总结

  1. 经过以上分析,我们知道KVO的实现是基于动态修改属性的set方法来实现的。首先系统会动态创建一个子类,并将当前实例对象继承自该子类,在子类中重载了被监听属性的set方法并且实现了父类没有的两个方法:class和_isKVO。
  2. 该子类在重载的set方法中调用了willChangeValueForKey:和didChangeValueForKey:并且在didChangeValueForKey:里调用了observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context。
  3. 我们可以通过任何可以访问属性存取方法的方式来触发KVO(间接或者直接),我们也可以通过willChangeValueForKey:和didChangeValueForKey:两个方法来手动触发KVO。

GCD知识小集

发表于 2018-06-03 | 更新于: 2018-08-13
字数统计: 5,752 | 阅读时长 ≈ 22

前言

Grand Central Dispatch

Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般讲应用程序中记述的线程管理用的代码在系统级中实现,开发者只需要定义想执行的任务并追加到适当的Dispatch Queue 中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可以统一管理,也可执行任务,这样就比以前的线程更有效率。(摘自苹果的官方说明)

让我们看一下GCD之前,Cocoa框架提供的一些简单的多线程技术:

1
2
self performSelectorOnMainThread: withObject: waitUntilDone:
self performSelectorInBackground: withObject:

举个简单的列子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)doSomething{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
/*
耗时操作
*/
/*
耗时操作结束 调用主线程
*/
[self performSelectorOnMainThread:@selector(workDone) withObject:nil waitUntilDone:NO];
[pool drain];
}

-(void)workDone{
//回到主线程做事 比如UI刷新
}

关于performSelectorOnMainThread:方法中waitUntilDone参数的意义是这样的:如果传YES,则代表[pool drain]需要等待workDone结束之后才能执行,如果传NO,则代表不用等待,直接执行pool drain,再执行workDone。我们还注意到MRC下,需要手动管理内存,所以这里创建了一个自动释放池。

引入GCD之后,我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
dispatch_async(queue, ^{
/*
耗时操作
*/
/*
耗时操作结束 调用主线程
*/
dispatch_async(dispatch_get_main_queue(), ^{
//回到主线程做事 比如UI刷新
});
});

多线程编程

线程是什么?

我们知道一段代码大部分情况下是从上到下依次顺序执行的。那么如何保证其是依次执行的呢?首先编译器会将程序代码转为一长串的CPU命令列(就是通常我们理解的二进制代码),那么当应用程序启动的时候,CPU会从程序制定的位置开始,一个一个地执行CPU命令列。在if或者for语句中控制语句中,执行命令列的地址可能会是不连续的(即顺序不固定)。但是由于一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,可能会来回绕弯,但是一定是单向的,其执行不会出现分歧。
这里所说的一个CPU执行的CPU命令列为一条无分叉路径即为线程。

多线程是什么?

现在一个物理的CPU芯片实际上有64个CPU(即64核),那么一个CPU核可以分为2个虚拟核心(比如因特尔超线程技术,把CPU的一个核心虚拟成2个 )。那么一台计算机上就可以使用多个CPU核来运行了,这种情况下,上文提到的无分叉路径就不止一条了,存在多条时即为多线程。

多线程编程是什么?

iOS的核心XNU内核在发生操作系统事件时会切换执行路径。执行中路径的状态,列入CPU的寄存器等信息保存到各自路径专用的内存块中。从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列,这就是上下文切换。
使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像一个CPU内核可以能够并列的执行多个线程一样。而在具有多个CPU核的情况下,就不是看上去像了,而是真的提供了多个CPU核并行执行了多个线程的技术。这种利用多线程编程的技术就被称为多线程编程。

多线程编程的优缺点

缺点:多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致数据竞争,停止等待事件的线程会导致多个线程相互持续等待死锁,使用太多线程会消耗大量内存等。
优点:保证应用程序的响应性能。应用程序在启动的时候,通过最先执行的线程,即”主线程”来描绘用户界面,处理触摸屏幕事件等。如果在主线程中进行长时间的处理,就会阻塞主线程的执行,即妨碍主线程中被称为RunLoop的主循环的执行,从而导致不能更新用户界面,应用程序的画面长时间卡顿,停滞等问题。而使用多线程编程,在执行长时间的处理时仍可以保证用户界面的响应性能。

GCD的API

Dispatch Queue:

Dispatch Queue是执行处理的等待队列。
苹果的官方说明:开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。用代码来解释:

1
2
3
4
5
dispatch_async(queue, ^{
/*
想执行的任务
*/
});

开发者通过用Block语法记述想执行的任务并将其追加到Dispatch Queue中,这样就可以使指定的任务在另一个线程中执行。Dispatch Queue按照追加的顺序FIFO执行处理。

Dispatch Queue分为两种
  1. 等待现在执行中处理的Serial Dispatch Queue 称为串行队列
  2. 不等待现在执行中处理的Concurrent Dispatch Queue 称为并发队列

假设现在分别在这两种队列中顺序追加了blk0,blk1,blk2,blk3这四个任务。那么在串行队列中,先执行blk0,blk0执行完毕以后才会执行blk1,blk1执行完毕之后才会执行blk2,依次执行下去,也就是说串行队列中的任务会按顺序执行且下一个任务总是在上一个任务执行完毕后开始执行。在并发队列中,先执行blk0,但是无论blk0的执行是否结束,都会开始执行后面的blk1,不管blk1的执行是否结束,都会开始执行后面的blk2,也就是说并发队列中的任务执行并不会等待上一个任务执行完毕。但是虽然并行队列中不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态,即iOS内核基于Dispatch Queue中的处理数,CPU核数以及CPU负荷等当前运行系统的状态来决定的。所谓的并行执行,就是使用多个线程同时执行多个处理。
iOS的核心XUN内核会决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XUN内核会结束不再需要的线程。XUN通过Concurrent Dispatch Queue就可以完美的管理并行执行多个处理的线程。

串行队列的任务执行理解起来很简单,即等待执行。并发队列的任务执行稍微有点复杂,我们再来举个列子详细的说一下:
假设现在有4个线程在并发队列中等待任务的执行,当我们像并发队列中追加了6个任务后,首先blk0在线程0中开始执行,接着blk1在线程1中开始执行,blk2在线程2中开始执行,blk3在线程3中开始执行(因为当前队列中只有4个空闲的线程,所以一次性最多只能调用4个线程去执行任务)。我们假设blk0先执行完毕,那么此时线程0中没有任务执行了,线程0处于空闲的状态,此时队列会将blk4追加到线程0中执行。这个时候我们假设blk2在blk1之前执行完毕了,那么线程2空闲出来,队列会立即将blk5追加到线程2中执行。
像这样在并发队列中执行任务处理时,执行顺序会根据处理内容和系统状态发生改变 它不同于执行顺序固定的串行队列。

dispatch_queue_create

我们现在知道了两种队列:串行和并发队列。那么怎么才能获取到这两种队列呢?
有两种方法:

第一种 通过GCD的API生成的Dispatch Queue:dispatch_queue_create

举例:

1
2
3
4
//生成串行队列
dispatch_queue_t MySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
//生成并行队列
dispatch_queue_t MyConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create 有两个参数,第一个参数是指定的队列的名称,苹果推荐Dispatch Queue的名称使用应用程序ID这种逆序全程域名的方式来命名,该命名会出现在Xcode的Instruments和CrashLog中,方便开发人员定位程序错误和问题。第二个参数在创建串行队列的时候直接传Null,创建并行队列的时候传DISPATCH_QUEUE_CONCURRENT。
dispatch_queue_create 的返回值均为dispatch_queue_t类型来接收。

关于生成的线程数量。
对于串行队列,系统对一个已经追加任务处理的串行队列只会生成一个线程供其使用,因为串行队列的运行机制就是等待处理任务,不论你给它生成多少个线程,在串行队列中永远只会有一个在运行。假如现在需要100个任务同一时间处理,那么使用串行队列的话就需要创建100个串行队列去完成,这样就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。所以串行队列不应该被大量生成,往往我们只在考虑数据锁的情况下使用它:当多个线程更新相同的资源导致数据竞争的时候使用串行队列,这样可以保证数据安全,除此之外我们都应该使用并发队列去执行任务(不考虑系统的主线程更新UI)。

对于并发队列,因为XNU内核只使用有效管理的内核,因为不会出现串行队列这样的性能问题。

第二种 获取系统标准提供的Dispatch Queue

在程序启动的时候,系统提供了两个队列:
Main Dispatch Queue
Globle Dispatch Queue

Main Dispatch Queue是在主线程中执行的Dispatch Queue。因为主线程只有一个,所以Main Dispatch Queue实际上就是Serial Dispatch Queue (串行队列)。
追加到Main Dispatch Queue的处理是在主线程的Runloop中执行的。由于在主线程中执行,因此对于用户界面的更新操作必须是追加到Main Dispatch Queue中的。

Globle Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue(并发队列)。通常我们并不需要额外创建一个并发队列来使用,直接获取Globle Dispatch Queue就可以了。
对于Globle Dispatch Queue来说,有4个执行优先级。分别是
High Priority 高优先级
Default Priority 默认优先级
Low Priority 低优先级
Background Priority 后台优先级
关于执行优先级的使用:在向Globle Dispatch Queue中追加处理时,应选择与处理内容对应的执行优先级的Globle Dispatch Queue。这里需要注意的一点是XNU内核用于Globle Dispatch Queue的线程并不能保证实时性。因此执行优先级只是大致的判断。

关于获取Main Dispatch Queue和Globle Dispatch Queue的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
获取Main Dispatch Queue
*/
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
/*
Globle Dispatch Queue
*/
//高优先级
dispatch_queue_t globleDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//默认优先级
dispatch_queue_t globleDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//低优先级
dispatch_queue_t globleDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//后台优先级
dispatch_queue_t globleDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_set_target_queue

dispatch_queue_create 函数生成的Dispatch Queue(不管是串行还是并发队列),都使用与默认优先级Globle Dispatch Queue相同执行优先级的线程。当我们想变更其优先级的时候,就需要使用dispatch_set_target_queue函数了:

1
2
3
4
5
6
//需要改变优先级的队列
dispatch_queue_t MySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
//作为想改变优先级队列参考的队列
dispatch_queue_t globleDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//设置优先级
dispatch_set_target_queue(MySerialDispatchQueue, globleDispatchQueueHigh);

这样,新生产的MySerialDispatchQueue本来优先级是默认优先级,通过dispatch_set_target_queue设置,其当前的优先级为High了。dispatch_set_target_queue方法的第一个参数为需要改变优先级的队列,第二个参数为优先级参考目标的队列。
通过dispatch_set_target_queue,我们还可以实现多个串行队列的并发执行。比如我们从多个Serial Dispatch Queue中,用dispatch_set_target_queue函数指定目标为某一个Serial Dispatch Queue。那么原本本应并行执行的的多个Serial Dispatch Queue,变成了只能同时处理一个任务。

dispatch_after

当我们想要将一个任务延期执行的时候,就可以用dispatch_after了。比如
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//三秒以后回到主线程更新UI
});
这里要注意的是,dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue。至于什么时候处理会执行,是根据当前的系统和队列状态来决定的。比如上面的代码,如果此时主线程没有其他任务在处理,根据Runloop的执行频率是1/60来看,处理最快是3秒钟执行,最慢是3+1/60秒执行。

Dispatch Group

写业务的时候,我们往往会碰到需要完成一些操作后,才能继续下一步的操作的情况。这个时候,如果放在串行队列中去完成的话,只需要将想执行的处理依次追加到串行队列中,并将下一步的操作放到最后追加。这样串行队列等待执行的机制就会保证业务逻辑的正确性。而在并发队列中,我们想要实现这种业务逻辑,就需要Dispatch Group了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"3");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"Done");
});

因为并行队列中的任务是不等待执行的,顺序不定。执行结果的顺序是不确定的,但Done一定是最后执行的。

dispatch_barrier_async

前面我们提到,使用并发队列进行数据读取和写入操作时,容易产生数据竞争的问题。而放在串行队列中就没有问题。写入处理确实不可与其他的写入处理以及包含读取处理的其他处理并行执行。但是如果只是读取处理与读取处理并发执行,在确保当前没有读取处理进行的情况下载串行队列中追加写入处理,那么就不会发生问题。
我们看看dispatch_barrier_async是如何应用的:

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);

dispatch_barrier_async会等待追加到CONCURRENT Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该CONCURRENT Dispatch Queue中。然后再由dispatch_barrier_async函数追加的处理执行完毕后,CONCURRENT Dispatch Queue才恢复一般的动作,继续往下执行已经追加的处理。即,等blk0,1执行完毕后,blk_for_writing才会执行。blk_for_writing执行完毕后,blk2,3才会执行。
通过使用CONCURRENT Dispatch Queue和dispatch_barrier_async函数可以实现高效率的数据库访问和文件访问。

dispatch_sync和dispatch_async

dispatch_sync意味着将处理“非同步”的追加到队列中,无需等待。
dispatch_async意味着将处理“同步”的追加到队列中,需要等待。
这里的等待意思就是当前线程停止。
关于dispatch_sync要注意的一点是不能在串行队列中同步追加处理,这样会造成死锁。很简单,串行队列在执行这些源代码,而源代码里面的操作需要等串行队列执行完源代码以后才能执行,这样相互等待就造成了死锁。

dispatch_apply

dispatch_apply函数按照指定的次数将指定的任务追加到指定的Dispatch Queue中,并等待全部处理执行结束。
举个列子:

1
2
3
4
5
6
NSArray *array = @[@"1",@"2",@"3",@"4",@"5"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%@",array[index]);
});
NSLog(@"done");

该代码的执行顺序是不定的,但是done一定是最后才输出的。因为在全局队列中执行处理,是并发处理,所以1到5的打印顺序不固定。因为dispatch_apply函数会等待全部处理执行结束,所以 NSLog(@”done”)一定是最后才执行的。
方法的第一个参数是重复次数,第二个参数为追加对象的queue,第三个参数可以理解为索引。
dispatch_apply因为与dispatch_sync函数一样会等待处理执行结束。所以推荐在dispatch_async函数中非同步的执行dispatch_apply函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSArray *array = @[@"1",@"2",@"3",@"4",@"5"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%@",array[index]);
});
//dispatch_apply函数处理全部结束 回到主线程 更新UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"done");
/*
界面更新等操作
*/
});
});

dispatch_suspend和dispatch_resume

当追加大量处理到队列中的时候,有的时候,我们希望暂停处理过程。这个时候调用dispatch_suspend可以挂起当前队列,当前正在执行的处理不会被停止,而尚未执行的处理会停止执行。当我们需要恢复处理过程的时候,调用dispatch_resume则会让尚未执行的已经停止的处理恢复执行。

1
2
dispatch_suspend(queue);
dispatch_resume(queue);

dispatch_semaphore

当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用dispatch_barrier_async或者Serial Dispatch Queue函数可以避免这类问题,有必要进行更细粒度的排他控制。

dispatch_semaphore是持有技术的信号,该技术是多线程编程中的技术类型信号。所谓信号,类似于过马路时常用的手旗,当手旗是举起的时候代表你可以提供,放下手旗代表你不可以通过。对于dispatch_semaphore而言,使用技术实现该功能,即技术为0时等待处理,大于等于1的时候不等待。

dispatch_semaphore的生成函数,参数为技术值。

1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER),如果当前semaphore的技术为0时,改函数会永远等待。当semaphore的技术大于等于1时,dispatch_semaphore_wait会将semaphore的技数减一并返回。
dispatch_semaphore_signal(semaphore),将semaphore的计数加一并返回。

举例1 现在我们不考虑顺序的将一些数据加入到数组中

1
2
3
4
5
6
7
8
9
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *arrayM = [NSMutableArray array];
for (int i = 0; i<1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[arrayM addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semaphore);
});

首先,我们引用了一个全局队列。然后通过dispatch_semaphore_create()生成了一个信号量为1的信号。接下来,我们通过并发操作往数组里写入数据,为了保证写入操作的安全性,我们在每次写入操作之前,调用dispatch_semaphore_wait方法,因为semaphore的初始化信号量为1,所以走到这的时候,dispatch_semaphore_wait会通过执行并将信号量减一返回。等一次数据追加的操作完成后,调用dispatch_semaphore_signal使信号量加一。
这样一来的话,尽管有多个线程并发去写入数据,但一定是同步执行的。因为在第一个线程开始执行的时候,信号量就被减一变为0了,只要第一个线程执行没有结束,信号量就不会被加一。那么其他线程走到这里的时候,会因为信号量为0而永远等待。所以这种写入操作一定是线程安全的。

举例2 有的时候我们会碰到这样的业务逻辑,我们需要发起两个网络请求A,B。但是B一定需要在A请求回调之后再发送(最常见的token验证登录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-(void)loginButtonClick{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self verifyTokenAction:semaphore];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self loginSuccessAction];
}

-(void)verifyTokenAction:(dispatch_semaphore_t)semaphore{
NSURLSessionDataTask *task = [LoginSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data) {
//token获取成功,发送信号量:
dispatch_semaphore_signal(semaphore);
}else{
//token获取错误,不发送信号量
}
}];
[task resume];
}

-(void)loginSuccessAction{
//登录成功
}

首先我们初始化一个信号量为0的信号,然后发送一个网络请求,接着调用dispatch_semaphore_wait,因为信号量为0,所以dispatch_semaphore_wait会一直等待处理,等到请求成功后,调用 dispatch_semaphore_signal使信号量加一,这个是时候因为信号量为1,dispatch_semaphore_wait会执行处理并将信号量减一返回。

关于dispatch_semaphore_create函数的参数我们知道是信号量的意思,可以利用信号量来控制并发线程的数量。比如我们生成了初始化信号量为n的信号,这个时候我们有n+10个任务需要处理。我们追加任务到并发队列中,那么信号会通过n个线程里的dispatch_semaphore_wait函数减一,即减n,信号量变为0,此时除非之前执行的n个处理中有结束的处理调用了dispatch_semaphore_signal函数使信号的信号量加一,否则信号量为0会永远等待执行。我们生成了初始化的信号量为3的信号,则代表最多只会有3个线程可以并发运行。

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
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务1的处理
*/
dispatch_semaphore_signal(semaphore);
});
//任务2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务2的处理
*/
dispatch_semaphore_signal(semaphore);
});
//任务3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务3的处理
*/
dispatch_semaphore_signal(semaphore);
});
//任务4
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务4的处理
*/
dispatch_semaphore_signal(semaphore);
});

dispatch_once

dispatch_once函数是保证在应用程序执行中只执行一次指定处理的函数。如果不用dispatch_once的话,一般我们为了达到只创建一次的效果,我们会这么做:

1
2
3
4
5
6
7
static int hasBeenInitialized = NO;
if (hasBeenInitialized == NO) {
/*
初始化
*/
hasBeenInitialized = YES;
}

而使用dispatch_once的话,代码则更简洁。而且即使在多线程环境下执行,也可以保证绝对的数据安全。多用于单例的创建。

1
2
3
4
5
6
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/*
初始化
*/
});

参考文献: 《Objective-C高级编程》 《Effective Objective-C 2.0》

iOS内存管理

发表于 2018-05-20 | 更新于: 2018-09-05
字数统计: 3,811 | 阅读时长 ≈ 14

前言

Objective-C是一个面向对象的语言,所以内存管理显得尤为重要。不过在Objective-C中,我们只需要管理对象的内存,非对象的内存不需要我们关心,比如char int 等类型的数据是放置在栈上的,交由系统自动回收。Objective-C一直是使用引用计数来管理对象的内存。什么是引用计数?简单来说,每个对象都有一个计数器,用以表示当前有多少个事物想令此对象继续存活下去,也叫做保留计数。当该对象的计数为0时,该对象就可以废弃了。

在iOS5之前,如果你编写iOS应用程序,你需要手动去管理你创建的对象的内存,即MRC(Manual Reference Counting)。iOS5引入了自动引用计数,即ARC(Automatic Reference Counting)。

ARC的出现,将内存管理这个活从开发者本身转移到了编译器上面。LLVM引入了ARC机制后,可以很清楚目标对象,并能立刻释放那些不再被使用的对象,这样不仅大大减少了开发者的工作量,还使得程序本身的稳定性得到很好的提升。开发者可以更加专注于业务逻辑而不是内存管理。

引用计数

在MRC时代,Objective-C就使用引用计数来管理对象的内存。NSObject协议声明了三个方法用于操作计数器,以递增或递减其值:

  • retain 递增
  • release 递减
  • autorelease 递减

举个简单的列子:

1
2
3
4
if ([[self canLog]]) {
NSString *message = [[NSString alloc] initWithString:@"log"];
NSLog(@"%@",message);
}

在MRC下 这段代码存在内存泄漏的问题,因为if语句块末尾并没有手动释放message对象。需要对被释放的对象调用release方法,使其引用计数减1。

1
2
3
4
5
if ([[self canLog]]) {
NSString *message = [[NSString alloc] initWithString:@"log"];
NSLog(@"%@",message);
[message release];//ARC下 编译器会自动完成
}

在ARC下则没有问题,因为ARC下,编译器会自动为你添加保留与释放操作。所以,直接在ARC下调用retain release autorelease 等内存管理方法是不能通过编译的,因为手工调用的话,会让干扰ARC判断何处应该自动调用内存管理方法。

实际上,ARC在调用这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层C语音版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数objc_retain。这也是不能覆写retain release等方法的原因。

所以对于保留计数的概念应该这么来理解:绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了还是递减了该计数。

引用计数是如何管理内存的

我们想一下,编写程序的时候,我们对内存管理的思考方式应该是什么:

  1. 自己生成的对象,自己持有
  2. 非自己生成的对象,自己也能持有
  3. 不再需要自己持有的对象时释放
  4. 非自己持有的对象无法释放

理解好以上4点,对于内存是如何通过引用计数进行管理的就很容易明白了。
这4点里我们提到了3个词很重要:生成 持有 释放。对于Objective-C的内存管理来说还要加上一个词:废弃。这四个词在Objective-C中的对应方法如下:

  1. 生成:alloc new copy mutableCopy
  2. 持有:retain
  3. 释放:release
  4. 废弃:dealloc

这些方法属于Cocoa框架中Foundation框架类库中的NSObject类的方法,适用于OS X和iOS应用开发。

那什么叫自己生成的对象和非自己生成的对象?
这里可不是简单的指你编写的代码和别人编写的代码生成的对象之分。而是调用的方法之分。通过alloc new copy mutableCopy等方法或者是使用这些名称开头的方法生成的对象称为自己生成的对象。而使用这些方法之外创建的对象(类似于[NSArray arrry]这种类方法创建的对象)称为非自己生成的对象。

MRC下的内存管理

  1. 自己生成的对象,自己持有
    1
    2
    3
    4
    5
    //自己生成并持有对象
    id obj = [[NSObject alloc]init];
    id obj2 = [NSObject new];
    id obj3 = [obj copy];
    id obj4 = [obj3 mutableCopy];

copy和mutableCopy的不同在于,copy方法生成并持有不可变的对象副本,mutableCopy生成并持有可变对象的副本。用这两个方法生成的对象,虽然是对象的副本,但同alloc new等方法一样,在自己生成并持有对象这点上是一样的。

  1. 非自己生成的对象,自己也能持有

    1
    2
    3
    4
    //取得对象的存在 但自己并不持有
    id obj = [NSArray array];
    //自己持有对象
    [obj retain];
  2. 不再需要自己持有的对象时释放

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //自己生成并持有对象
    id obj = [[NSObject alloc]init];
    //释放对象
    [obj release];
    //取得对象的存在 但自己并不持有
    id obj = [NSArray array];
    //自己持有对象
    [obj retail];
    //释放对象
    [obj release];
  3. 非自己持有的对象无法释放

    1
    2
    3
    4
    //取得对象的存在 但自己并不持有
    id obj = [NSArray array];
    //释放了非自己持有的对象会导致程序崩溃
    [obj release];

ARC下的内存管理

所有权修饰符

我们知道Objective-C中处理对象,需要将变量类型定义为id类型或各种对象类型。这里要注意的是,事实上并没有对象变量这样的东西存在。它仅仅是一个引用到变量的对象,是一个指针,是一个地址,它并不是一个对象的容器里面装载了对应的对象。比如在Java中,(java是非常注重对象类型的),我们不会也不应该知道引用变量中装载的是什么,它只是用来代表单一的对象(注意 是代表 而不是本身是),只有java虚拟机才会知道如何使用引用来取得该对象。回到Objective-C中,,所谓的对象类型,其实就是指向NSObject的指针,例如NSObject *或者id等(id是万能指针,它可以指向任何类型的对象,你可以理解为它是一个可以修饰任何类型的对象引用,相当于C语音中的void *。
那么在ARC的机制下,所有的对象类型和id类型必须要加上一个东西,就是刚才我们提到的所有权修饰符。
所有权修饰符一共分为4种:__strong __weak __unsafe_unretained __autoreleasing。

__strong

__strong 修饰符表示对对象的强引用,保留了此值。是所有对象类型和id类型的默认修饰符。以下代码是等同关系:

1
2
id obj1 = [[NSObject alloc] init];
id __strong obj1 = [[NSObject alloc] init];

__strong 修饰符会产生循环引用(比如A强引用B的同时B也强引用A,那么A和B永远都不会被销毁,因为彼此强引用着对方,任何一方的强引用失效都只能基于对方的强引用失效,这样就产生了死循环。类似于死锁的问题),这也是引用计数式内存管理必然会产生的问题,为了解决这个问题,所以引入了下面的__weak 。

__weak

__weak 修饰符表示对对象的弱引用,不保留此值。使用__weak 修饰符可以避免循环引用(比如让A和B中的任意一方将其强引用改成弱引用或者都改成互相弱引用)。

__unsafe_unretained

__unsafe_unretained修饰符和_weak修饰符一样,表示对对象的弱引用,不保留此值。是一个不安全的所有权修饰符。尽管ARC下的内存管理是编译器的工作,但附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,所以会造成不安全的情况。

__autoreleasing

要说__autoreleasing所有权修饰符。我们需要先了解自动释放池这个概念。
自动释放池是iOS引用计数架构中的一项重要特性。我们知道调用release会立刻递减对象的保留计数。然而有的时候我们可以不调用release,改为调用autorelease,会将对象加入到对应的自动释放池中,此方法会在稍后递减(通常是在下一次事件循环时递减)。

举个简单的列子:
当我们需要一个方法提供的返回对象时,autorelease就非常有用了。看一下这个方法:
MRC下

1
2
3
4
-(NSString *)stringValue{
NSString *string = [[NSString alloc]initWithFormat:@"i am a %@",self];
return string;
}

这里的string的保留计数比期望值要多1,因为我们进行了alloc操作,但是我们又没有对应的释放操作。这就意味着调用者要负责处理多出来的这一次保留计数,必须设法将其抵消。但是我们又不能再方法里直接释放,否则,return的就是一个空值了。这时候autorelease就非常有用了。它会在稍后释放改对象,从而给调用者留出了足够多的时间,使其在需要的时候保留返回值。

1
2
3
4
-(NSString *)stringValue{
NSString *string = [[NSString alloc]initWithFormat:@"i am a %@",self];
return [string autorelease];
}

这样一来的话,由于返回的string会在稍后自动将其保留计数减一,调用者就无需再对其进行内存管理了。不过在ARC下,这个并不需要开发者去完成,编译器会为我们搞定的。

ARC下,我们很少显式的调用__autoreleasing 比如

1
2
3
4
@autoreleasepool {
id obj = [NSMutableArray array];
//编译器会自动检查方法名,如果不是alloc/new/copy/mutableCopy开始的方法,则自动将返回值对象注册到autoreleasepool
}

再比如ARC下,string作为局部变量的函数返回值,编译器也会自动将其注册到autoreleasepool 中的。

1
2
3
4
-(NSString *)stringValue{
NSString *string = [[NSString alloc]initWithFormat:@"i am a %@",self];
return string;
}

再比如__weak修饰符。虽然__weak修饰符是为了避免循环引用而使用的,但在访问__weak修饰符的变量时,实际上必定要访问注册到autoreleasepool 的对象。以下两个代码是等同的。

1
2
3
4
5
6
id __weak obj = obj2;
NSLog(@"class=%@",[obj Class]);

id __weak obj = obj2;
id __autoreleasing temp = obj;
NSLog(@"%@class=%@",[temp class]);

为什么在访问持有__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢?这是因为__weak修饰符只持有对象的弱引用,而在访问对象的过程中,改对象有可能被废弃,这样就可能会造成内存泄漏。此时把要访问的对象注册到autoreleasepool中,那么在 @autoreleasepool结束之前,都能确保改对象存在。因此,在使用__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象,不过这些在ARC下都有编译器自动完成,不需要我们进行管理。

自动释放池

现在我们来详细了解一下自动释放池(autoreleasePool)。自动释放池机制类似于“栈”,系统创建好自动释放池后将其推入栈中。而清空释放池,就相当于将其从栈中弹出。在对象上执行自动释放操作,相当于将其放入栈顶的那个池中。通常情况下,我们无需担心自动释放池的创建问题。iOS应用程序是在Cocoa Touch环境下运行的,系统会自动创建一些线程,比如主线程。这些线程默认都有自动释放池,每次执行“事件循环”时,就将其清空。因此,不需要自己来创建自动释放池。通常只有一个地方需要创建自动释放池,那就是main函数,我们用自动释放池来包裹应用程序的主入口点。一般iOS程序的main函数:

1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

从技术角度看,这里不是非得有个自动释放池才可以。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果这里不弄一个自动释放池的话,UIApplicationMain函数所释放的那些对象就没有自动释放池可以容纳了,所以说,这个池可以理解成最外围捕捉全部自动释放池对象所用的池。这里就又引入了一个知识点,自动循环池是可以嵌套的。举个例子:

1
2
3
4
5
6
@autoreleasepool {
NSString *string = [[NSString alloc]initWithFormat:@"1=%i",1];
@autoreleasepool {
NSNumber *number = [NSNumber numberWithInt:1];
}
}

将自动释放池嵌套使用的好处是,可以借此控制应用程序的内存峰值,使其不至于过高。看下面这段代码:

1
2
3
for (int i = 0; i<10000; i++) {
[self doSomethingWithInt:i];
}

如果doSomethingWithInt方法要创建一些临时对象,那么这些对象很可能会放在自动释放池里,等待系统稍后将其释放并回收。但是自动释放池要等待程序执行下一个事件循环时才清空。这就意味着在执行for循环时,会持续的有新对象创建出来,并加入自动释放池,这些对象都要等待for循环执行完才会释放。这样一来,在执行for循环的时候,应用程序所占的内存就会持续上涨。而等到所有的临时对象都释放后,内存又会突然下降。
通过嵌套使用自动释放池可以很好的解决这个问题,我们将循环内的代码包裹在自动释放池中:

1
2
3
4
5
for (int i = 0; i<10000; i++) {
@autoreleasepool{
[self doSomethingWithInt:i];
}
}

这样每次for循环创建出的那些临时对象,在用完之后就不用放到线程的主释放池里等待整个for循环结束后释放,而是每次for循环创建的临时对象都会放到对应for循环创建出来的释放池中,等到当前for循环结束后释放。这样内存峰值就会降低了。
这里要注意的一点时,创建自动释放池也是有开销的,所以尽量不要额外的创建自动释放池。

在MRC中,创建自动释放池需要NSAutoreleasePool类。这个类专门用来表示自动释放池。这个了解一下即可,毕竟MRC已经是过去了。

1
2
3
NSAutoreleasePool *pool  = [NSAutoreleasePool alloc] init];
/**比较消耗内存的操作**/
[pool drain];

参考文献: 《Objective-C高级编程》 《Effective Objective-C 2.0》

左博杨

左博杨

8 日志
GitHub E-Mail ow
© 2019 左博杨 | Site words total count: 20.5k
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
0%