iOS问题集锦

前言

JavaScriptCore引擎的分析由于其复杂性,耗时性,暂时先告一段落,后续会进行回归,本篇将总结一些日常开发中遇到的问题

问题列表

启动图

之前在做视频闪屏的时候,遇到了一个比较棘手的问题:每次加载本地视频都会有一段黑屏的时间(大概0.2s左右),这本身和AVPlayer有很大的关系,猜测和视频的解码有关,无奈由于闭源,无计可施。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)setupVideoPlayer
{
NSURL* fileUrl = [NSURL fileURLWithPath:@"本地视频url"];
AVAsset *movieAsset = [AVURLAsset URLAssetWithURL:fileUrl options:nil];
AVPlayerItem * playerItem = [AVPlayerItem playerItemWithAsset:movieAsset];
self.player = [AVPlayer playerWithPlayerItem:playerItem];

self.moviePlayer = [[AVPlayerViewController alloc] init];
self.moviePlayer.showsPlaybackControls = NO;
self.moviePlayer.player = self.player;
self.moviePlayer.view.backgroundColor = self.view.backgroundColor;
[self.view addSubview:self.moviePlayer.view];
self.moviePlayer.view.frame = self.view.frame;
self.moviePlayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
}

后来有童鞋想了个办法:因为闪屏是在启动图之后,所以人为地在这段黑屏的时间里,在闪屏的VC上面盖一个UIImageView,其image就是启动图,等过了这段黑屏时间,再把这个UIImageView移除,一个不错的解决方案,尽管这里的黑屏时间还是无法得到精确的控制!

然而又碰到了问题:
日常开发天天见的启动图,这个时候竟然找不到!!!在mainBundle里面找不到任何启动图资源!!!
难道要将所有的启动图像添加资源又往项目中添加了一遍吗???
我的感觉是:好残忍!每一张启动图都很大!并且有2x和3x之分,关键是还有很多尺寸…

经过调研,发现还是有办法拿到启动图的,只是这个时候不再是url,而是直接从内存中获取UIImage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (NSString *)getLaunchImageName
{
CGSize viewSize = [UIApplication sharedApplication].delegate.window.bounds.size;

NSString *viewOrientation = @"Portrait";
if(UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)){
viewOrientation = @"Landscape";
}

NSString *launchImageName = nil;
NSArray* imagesDict = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"UILaunchImages"];
for (NSDictionary* dict in imagesDict)
{
CGSize imageSize = CGSizeFromString(dict[@"UILaunchImageSize"]);
if (CGSizeEqualToSize(imageSize, viewSize) && [viewOrientation isEqualToString:dict[@"UILaunchImageOrientation"]])
{
launchImageName = dict[@"UILaunchImageName"];
}
}
return launchImageName;
}

你真的在复用cell吗?

iOS开发中,cell复用是一件再正常不过的事情了,然而有许多比较复杂的cell,里面的元素个数不确定,每次拿到复用的cell都要进行各种逻辑判断,得到一个合适的cell高度,并进行cell内部的重新布局,这使得cell的复用大打折扣。有的童鞋为了避免这种麻烦,干脆在拿到cell之后,首先把内部的子视图全部移除,然后重新按照模型数据生成新的子视图,这样固然是省事多了,但是随之也带来了性能问题。
以QQ阅读的漫画书城为例:
在每次给cell设置数据的时候是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)setViewModel:(id<XXXViewModelDelegate>)viewModel
{
// 每次拿到cell的时候都会把cell上的子视图全部清理掉
[self.subviews enumerateObjectsUsingBlock:^(XXXSubView * _Nonnull obj,
NSUInteger idx,
BOOL * _Nonnull stop) {
[obj removeFromSuperview];
}];

// 然后重新初始化子视图,并设置数据
...
}

通过intrument测量发现,cellForRow的时间占比高达17.6%
优化前

秉着以下原则,对漫画书城的cell的逻辑进行了修改:

  • 复用cell,且cell中的每个子视图创建且仅应该创建1次;
  • 每个子视图采用懒加载的方式进行创建,即只在需要的时候进行加载;
  • 当复用的cell的子视图对于当前的数据”过剩”时,采取隐藏的方式;
  • 每次网络请求到的数据,每个数据模型的高度计算且只应该计算1次,计算之后缓存下来复用;当重新刷新后,模型数据及其高度应该全部抛弃;

之后通过intrument测量发现,发现cellForRow的时间占比已经下降到7.5%
优化后

所以不要小觑创建视图带来的性能消耗!

会说谎的屏幕尺寸

之前项目中是这样判定iPhoneX设备的

1
#define IS_IPHONEX                      ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)

这段代码看上去倒是没有什么问题,适配以来在iPhoneX上也并未出过什么问题,然而直到今年苹果爸爸又推出了新的3款iPhone设备,iPhoneXS、iPhoneXR、iPhoneXSMax,问题来了:

这3款新的设备都是刘海屏,刘海高度都是一致的,这样其UI适配和iPhoneX的适配无二异,所以按照搬砖惯性,不自觉地就会往这个宏里面加点东西:

1
2
3
4
5
#define IS_IPHONEX                      ([UIScreen instancesRespondToSelector:@selector(currentMode)] ?\
(CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) ||\
CGSizeEqualToSize(CGSizeMake(1242, 2688), [[UIScreen mainScreen] currentMode].size) ||\
CGSizeEqualToSize(CGSizeMake(828, 1792) , [[UIScreen mainScreen] currentMode].size) )\
: NO)

自然这个时候IS_IPHONEX不仅仅代表iPhoneX设备,而是一系列刘海屏设备。

然而在一款新的设备iPhoneXR上,该宏总是返回NO,上了断点,才发现该机器返回的屏幕尺寸
(width = 750, height = 1624)
瞬间有一种颠覆人生观的感觉。难道买了个假的iPhoneXR ?
纠结了半天,才有同学想起来是 放大模式 的问题,去设置里面看了下,果然是!

问题找到了,所以分别针对iPhoneXR和iPhoneXSMax,继续加了两个尺寸(经过确认和其它的机型不会重复)

1
2
(width = 750, height = 1624) iphonexr 放大模式
(width = 1125, height = 2436) iphonexmax 放大模式(和iphonex的正常模式大小一致)

然而,这样的解决方式有很大的缺陷:

  • 代码很臃肿,判定很繁琐,并且可能考虑的不够周到,目前支持放大模式的设备还不少:iphone6/6s、iphone7/7s、iphone8/8s、iphonexr、iphonexmax都有放大模式;
  • 如果留海设备A的放大尺寸正好是非留海设备B的正常尺寸,会导致宏判定失效,即使当前没有,如果以后出了一款正常屏幕的设备,但是尺寸却和某款刘海屏的放大模式一致,也会导致判定失效

有同学给出了这样的解决方案:

1
2
3
4
5
6
#define IS_IPHONEX \
({BOOL isPhoneX = NO;\
if (@available(iOS 11.0, *)) {\
isPhoneX = [[UIApplication sharedApplication] delegate].window.safeAreaInsets.bottom > 0.0;\
}\
(isPhoneX);})

相对来说就比较简单了,用到了异形屏的安全区的概念,比较漂亮的解法!

不过也有一个缺陷:在window创建之前,这个宏是无法使用的,有的情况下,例如在load里面对IS_IPHONEX做判定,这个宏是无法做到的

实际上,屏幕会说谎,设备却不会,用下面的方法更简单准确:

1
2
3
4
5
6
7
8
9
10
- (NSString *)platform {

size_t size;
sysctlbyname("hw.machine", NULL, &size, NULL, 0);
char *machine = malloc(size);
sysctlbyname("hw.machine", machine, &size, NULL, 0);
NSString *platform = [NSString stringWithUTF8String:machine];
free(machine);
return platform;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (BOOL)isPhoneX
{
NSString* plat = [self platform];

if ([platform isEqualToString:@"iPhone10,3"] ||
[platform isEqualToString:@"iPhone10,6"]) {
// modelType = DeviceModelTypeIPhoneX
return YES;
}
else if([platform isEqualToString:@"iPhone11,2"]) {
// modelType = DeviceModelTypeIPhoneXS
return YES;
}
else if ([platform isEqualToString:@"iPhone11,4"] ||
[platform isEqualToString:@"iPhone11,6"]){
// modelType = DeviceModelTypeIPhoneXSMAX
return YES;
}
else if([platform isEqualToString:@"iPhone11,8"]){
// modelType = DeviceModelTypeIPhoneXR;
return YES;
}
return NO;
}

再也不用担心老年机模式了,打完收工。

躺着的tableview

平常情况下,UITableView可以通过简单的设置实现SectionHeader悬浮问题,然而想要一个躺着的UITableView来实现悬浮就没那么容易了,需要借助于UICollectionView,悬浮要靠自己来实现。



这里主要实现了一个QRHorizontalFloatingHeaderLayout,它继承自UICollectionViewLayout,使用方式如下,之后需要把QRHorizontalFloatingHeaderLayoutDelegate中的代理方法实现一下就可以了:

1
2
3
4
QRHorizontalFloatingHeaderLayout *layout = [[QRHorizontalFloatingHeaderLayout alloc] init];

_collectionView = [[UICollectionView alloc]initWithFrame:frame
collectionViewLayout:layout];

QRHorizontalFloatingHeaderLayout是用OC实现的,主要参照与这里的Swift版本:
https://github.com/cruzdiego/HorizontalFloatingHeaderLayout,代码的核心在于在滚动的过程中去定位FloatingHeader的位置坐标,具体细节请查看代码

实现的OC版本代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@protocol QRHorizontalFloatingHeaderLayoutDelegate <NSObject>

//Item size
- (CGSize) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderItemSizeAt:(NSIndexPath*)indexPath;

//Header size
- (CGSize) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderSizeAt:(NSInteger)section;

//Section Inset
- (CGFloat) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderItemSpacingForSectionAt:(NSInteger)section;

//Item Spacing
- (CGFloat) collectionView:(UICollectionView *)collectionView horizontalFloatingHeaderColumnSpacingForSectionAt:(NSInteger)section;

// Line Spacing
- (UIEdgeInsets) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderSectionInsetAt:(NSInteger)section;

// Left Margin
- (CGFloat) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderLeftMarginForSectionAt:(NSInteger)section;
@end

@interface QRHorizontalFloatingHeaderLayout : UICollectionViewLayout

@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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#import "QRHorizontalFloatingHeaderLayout.h"

@interface QRHorizontalFloatingHeaderLayout()
@property(nonatomic,strong)NSMutableDictionary* itemsAttributes;

@property(nonatomic,assign)CGFloat currentMinX;
@property(nonatomic,assign)CGFloat currentMinY;
@property(nonatomic,assign)CGFloat currentMaxX;
@end

@implementation QRHorizontalFloatingHeaderLayout

- (instancetype)init
{
if(self = [super init])
{
_itemsAttributes = [NSMutableDictionary dictionary];
_currentMaxX = 0;
_currentMinX = 0;
_currentMinY = 0;
}
return self;
}

- (CGSize)collectionViewContentSize
{
if(!self.collectionView){
return CGSizeZero;
}



UICollectionView* collectionView = self.collectionView;
NSInteger lastSection = collectionView.numberOfSections - 1;

if(lastSection < 0){
return CGSizeZero;
}

CGFloat contentWidth = self.lastItemMaxX + [self insetForSection:lastSection].right;
CGFloat contentHeight =
collectionView.bounds.size.height -
collectionView.contentInset.top -
collectionView.contentInset.bottom;

return CGSizeMake(contentWidth,contentHeight);
}

- (CGFloat)lastItemMaxX
{
UICollectionView* collectionView = self.collectionView;
NSInteger lastSection = collectionView.numberOfSections - 1;
NSInteger lastRow = [collectionView numberOfItemsInSection:lastSection] -1;
UICollectionViewLayoutAttributes* lastItemAttributes =
[self layoutAttributesForItemAtIndexPath:[NSIndexPath
indexPathForRow:lastRow inSection:lastSection]];
return lastItemAttributes ? CGRectGetMaxX(lastItemAttributes.frame) : 0;
}

- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath* aIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
return self.itemsAttributes[aIndexPath];
}

- (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind
atIndexPath:(NSIndexPath *)indexPath
{
if([elementKind isEqualToString:UICollectionElementKindSectionHeader])
{
return [self.sectionHeadersAttributes qr_safeObjectForKey:indexPath];
}
return nil;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return true;
}

- (UICollectionViewLayoutInvalidationContext*)invalidationContextForBoundsChange:(CGRect)newBounds
{
UICollectionViewLayoutInvalidationContext* context = [super invalidationContextForBoundsChange:newBounds];
CGRect oldBounds = self.collectionView.bounds;

BOOL isSizeChanged =
oldBounds.size.width != newBounds.size.width ||
oldBounds.size.height != newBounds.size.height;
if(!isSizeChanged)
{
NSArray* headersIndexPaths = [self.sectionHeadersAttributes allKeys];
[context invalidateSupplementaryElementsOfKind:UICollectionElementKindSectionHeader
atIndexPaths:headersIndexPaths];
}
return context;
}

- (void)prepareLayout
{
[self prepareItemsAttributes];
}

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray<UICollectionViewLayoutAttributes *> * itemsA =
[self attributes:self.itemsAttributes containedInRect:rect];
NSArray<UICollectionViewLayoutAttributes *> * itemsB =
[self.sectionHeadersAttributes allValues];
return [itemsA arrayByAddingObjectsFromArray:itemsB];
}

#pragma mark - Private


- (NSMutableDictionary<NSIndexPath*,UICollectionViewLayoutAttributes*>*)sectionHeadersAttributes
{
UICollectionView* collectionView = self.collectionView;
NSInteger sectionCount = collectionView.numberOfSections;
if(!collectionView || sectionCount < 0)
{
return [NSMutableDictionary dictionary];
}

NSMutableDictionary<NSIndexPath*,UICollectionViewLayoutAttributes*>* attributes =
[NSMutableDictionary dictionary];
for (NSInteger section = 0; section < sectionCount; section++) {
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:section];
UICollectionViewLayoutAttributes* attribute = [self attributeForSectionHeaderAtIndexPath:indexPath];
[attributes qr_safeSetObject:attribute forKey:indexPath];
}
return attributes;
}

- (UICollectionViewLayoutAttributes*)attributeForSectionHeaderAtIndexPath:(NSIndexPath*)indexPath
{
UICollectionViewLayoutAttributes* attribute =
[UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withIndexPath:indexPath];

UICollectionView* collectionView = self.collectionView;
CGSize mySize = [self headerSizeforSection:indexPath.section];

NSInteger itemsCount = [collectionView numberOfItemsInSection:indexPath.section];

UICollectionViewLayoutAttributes* firstItemAttribute = [self layoutAttributesForItemAtIndexPath:indexPath];
UICollectionViewLayoutAttributes* lastItemAttribute = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:itemsCount-1 inSection:indexPath.section]];

CGPoint myPosition;
CGFloat leftMargin = [self leftMarginForSection:indexPath.section];
if(itemsCount > 0 && firstItemAttribute && lastItemAttribute)
{
CGFloat edgeX = collectionView.contentOffset.x + collectionView.contentInset.left;
CGFloat xByLeftBoundary = MAX(edgeX+leftMargin, CGRectGetMinX(firstItemAttribute.frame));
CGFloat xByRightBoundary = CGRectGetMaxX(lastItemAttribute.frame) - mySize.width;
CGFloat x = MIN(xByLeftBoundary, xByRightBoundary);
myPosition = CGPointMake(x, 0);
}
else
{
CGFloat x = [self insetForSection:indexPath.section].left;
myPosition = CGPointMake(x, 0);
}

CGRect frame = CGRectMake(myPosition.x, myPosition.y, mySize.width, mySize.height);
attribute.frame = frame;
return attribute;
}

- (NSArray<UICollectionViewLayoutAttributes*>*)attributes:(NSDictionary*)attributes
containedInRect:(CGRect)rect
{
NSMutableArray<UICollectionViewLayoutAttributes*>* finalAttributes =
[NSMutableArray array];
for (UICollectionViewLayoutAttributes* attribute in attributes.allValues) {
if(CGRectIntersectsRect(rect, attribute.frame))
{
[finalAttributes qr_safeAddObject:attribute];
}
}
return finalAttributes;
}

- (void)prepareItemsAttributes
{

UICollectionView* collectionView = self.collectionView;
NSInteger sectionCount = collectionView.numberOfSections;
if(!collectionView || sectionCount <= 0){return;}

[self resetAttributes];

for (NSInteger section = 0; section < sectionCount; section++) {
[self configureVariablesforSection:section];
NSInteger itemCount = [collectionView numberOfItemsInSection:section];
if(itemCount <= 0){ continue;}
for (NSInteger row = 0; row < itemCount; row++) {
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:row inSection:section];
UICollectionViewLayoutAttributes* attribute = [self itemAttributeAtIndexPath:indexPath];
[self.itemsAttributes qr_safeSetObject:attribute forKey:indexPath];
}
}

}

- (void)resetAttributes
{
[self.itemsAttributes removeAllObjects];
self.currentMinX = 0;
self.currentMaxX = 0;
self.currentMinY = 0;
}

- (void)configureVariablesforSection:(NSInteger)section
{
UIEdgeInsets sectionInset = [self insetForSection:section];
UIEdgeInsets lastSectionInset = [self insetForSection:section-1];
self.currentMinX = (self.currentMaxX + sectionInset.left + lastSectionInset.right);
self.currentMinY = sectionInset.top + [self headerSizeforSection:section].height;
self.currentMaxX = self.currentMinX;
}

- (UICollectionViewLayoutAttributes*)itemAttributeAtIndexPath:(NSIndexPath*)indexPath
{
CGSize size = [self itemSizeForIndexPath:indexPath];
CGFloat newMaxY = self.currentMinY + size.height;
CGPoint origin = CGPointZero;
if(newMaxY >= [self availableHeightAtSection:indexPath.section])
{
origin.x = self.currentMaxX + [self columnSpacingForSection:indexPath.section];
origin.y = [self insetForSection:indexPath.section].top + [self headerSizeforSection:indexPath.section].height;
}
else
{
origin.x = self.currentMinX;
origin.y = self.currentMinY;
}

CGRect frame = CGRectMake(origin.x, origin.y, size.width, size.height);
UICollectionViewLayoutAttributes* attribute =
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attribute.frame = frame;

// updateVariables
self.currentMaxX = MAX(self.currentMaxX,CGRectGetMaxX(frame));
self.currentMinX = CGRectGetMinX(frame);
self.currentMinY = CGRectGetMaxY(frame) + [self itemSpacingForSection:indexPath.section];

return attribute;
}

- (CGFloat)availableHeightAtSection:(NSInteger)section
{
if(!self.collectionView || section < 0){
return 0.0f;
}
UIEdgeInsets sectionInset = [self insetForSection:section];
UIEdgeInsets contentInset = self.collectionView.contentInset;
CGFloat totalInset = sectionInset.top + sectionInset.bottom + contentInset.top + contentInset.bottom;
return self.collectionView.bounds.size.height - totalInset;
}

- (CGSize)headerSizeforSection:(NSInteger)section
{
if(section < 0 || !self.collectionView)
{
return CGSizeZero;
}

id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
(id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderSizeAt:)])
{
return [delegate collectionView:self.collectionView horizontalFloatingHeaderSizeAt:section];
}

return CGSizeZero;

}

- (CGFloat)itemSpacingForSection:(NSInteger)section
{
if(!self.collectionView || section < 0)
{
return 0.0f;
}

id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
(id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderItemSpacingForSectionAt:)])
{
return [delegate collectionView:self.collectionView horizontalFloatingHeaderItemSpacingForSectionAt:section];
}

return 0.0f;
}


- (CGFloat)leftMarginForSection:(NSInteger)section
{
if(!self.collectionView || section < 0)
{
return 0.0f;
}

id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
(id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderLeftMarginForSectionAt:)])
{
return [delegate collectionView:self.collectionView horizontalFloatingHeaderLeftMarginForSectionAt:section];
}

return 0.0f;
}

- (CGFloat)columnSpacingForSection:(NSInteger)section
{
if(!self.collectionView || section < 0)
{
return 0.0f;
}

id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
(id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderColumnSpacingForSectionAt:)])
{
return [delegate collectionView:self.collectionView horizontalFloatingHeaderColumnSpacingForSectionAt:section];
}

return 0.0f;
}

- (CGSize)itemSizeForIndexPath:(NSIndexPath*)indexPath
{
if(!self.collectionView)
{
return CGSizeZero;
}

id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
(id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderItemSizeAt:)])
{
return [delegate collectionView:self.collectionView horizontalFloatingHeaderItemSizeAt:indexPath];
}

return CGSizeZero;

}

- (UIEdgeInsets)insetForSection:(NSInteger)section
{
if(section < 0 || !self.collectionView)
{
return UIEdgeInsetsZero;
}

id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
(id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderSectionInsetAt:)])
{
return [delegate collectionView:self.collectionView horizontalFloatingHeaderSectionInsetAt:section];
}

return UIEdgeInsetsZero;
}

@end

C++的容器如何存放OC对象

日常的iOS开发中最常用的容器莫过于NSArrayNSDictionary了,然而在一些对性能要求比较场合下这两个容器明显不给力,很多人选择objective-c++这种混编模式来开发,从而使用强大的C++ STL等类库。但是objc的对象内存管理相对而言比c++对象麻烦很多,比如将objc的对象直接保存在STL容器中时,默认的并不会对该对象进行任何管理,我们需要手动的retain和release

这里直接引用大神duboleon在MRC下的解法

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
class ns_handle
{
private:
      typedef enum
      {
            _retain,
            _assign,
            _copy,
      }_mem_type;

public:
      static const _mem_type retain = _retain;
      static const _mem_type assign = _assign;
      static const _mem_type copy = _copy;

public:
      ns_handle(id nsObj,_mem_type mem_type = retain);
      ns_handle(const ns_handle & handle);
      ns_handle & operator=(const ns_handle & handle);
      id & operator*();
      ~ns_handle();

private:
      id m_nsObj;
};


inline ns_handle::ns_handle(id nsObj,_mem_type mem_type):m_nsObj(nsObj)
{
      switch (mem_type)
      {
            case retain:
                  [m_nsObj retain];
                  break;
            case assign:
                  break;
            case copy:
                  [m_nsObj copy];
                  break;            
            default:
                  break;
      }   
}

inline ns_handle::ns_handle(const ns_handle & handle):m_nsObj(handle.m_nsObj)
{
[m_nsObj retain];
}

inline ns_handle & ns_handle::operator=(const ns_handle & handle)
{
[m_nsObj release];
m_nsObj = handle.m_nsObj;
[m_nsObj retain];
return *this;
}

inline id & ns_handle::operator*()
{
return m_nsObj;
}

inline ns_handle::~ns_handle()
{
[m_nsObj release];
}

  • 对于retain方式,即构建handle的时候原objc对象引用加一,析构的时候引用减一。
  • 对于copy方式,首先该objc对象必须继承NSCopying协议并实现CopyWithZone的方法,才可以正常使用。
  • 对于assign方式,则默认仅做赋值处理。
  • 通过重载解运算符,在调用handle的时候可以直接用handle来获取句柄中保存的objc对象。

使用如下:

1
2
3
4
5
NSString * s = @"Test";
ns_handle handle(s,ns_handle::copy);
std::vector<ns_handle> vec;
vec.push_back(handle);
NSLog(@"%@",*handle);

那么问题来了,如果是在ARC下,又该如何来解决呢?问题的关键在于解决在ARC如何搞定retain和release,这是个棘手的问题

  • ARC如何retain和release呢?
    首先想到了objc_retain和objc_release,然而runtime并没有暴露这两个方法的接口,所以否决了,接下来考虑到的就是修饰符:__bridge_retained__bridge_transfer,以及__bridge

ARC下,retain一个对象obj:

1
2
void *retainedThing = (__bridge_retained void *)obj; 
retainedThing = retainedThing;

ARC下,release一个对象obj:

1
2
3
void *retainedThing = (__bridge void *)obj; 
id unretainedThing = (__bridge_transfer id)retainedThing;
unretainedThing = nil;

这里直接利用宏的形式统一处理ARC和MRC下的情况

1
2
3
4
5
6
7
#if __has_feature(objc_arc)
#define UniversalRetain(...) void *retainedThing = (__bridge_retained void *)__VA_ARGS__; retainedThing = retainedThing
#define UniversalRelease(...) void *retainedThing = (__bridge void *) __VA_ARGS__; id unretainedThing = (__bridge_transfer id)retainedThing; unretainedThing = nil
#else
#define UniversalRetain(...) [__VA_ARGS__ retain];
#define UniversalRelease(...) [__VA_ARGS__ release];
#endif

  • ARC下不能很好地处理C++里面的引用&,所以直接将下面的方法的返回值从id&修改为id
1
2
3
4
5
6
7
8
9
10
11
// 修改前
inline id & ns_handle::operator*()
{
return m_nsObj;
}

// 修改后
inline id ns_handle::operator*()
{
return m_nsObj;
}

解决了上面的两个问题,给出修改后的代码:

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
#ifndef NSHandle_h
#define NSHandle_h

class ns_handle
{
private:
typedef enum
{
_retain,
_assign,
_copy,
}_mem_type;
public:
static const _mem_type retain = _retain;
static const _mem_type assign = _assign;
static const _mem_type copy = _copy;
public:
ns_handle(id nsObj,_mem_type mem_type = retain);
ns_handle(const ns_handle & handle);
ns_handle & operator=(const ns_handle & handle);

inline id operator*();

~ns_handle();
private:
id m_nsObj;
};
inline ns_handle::ns_handle(id nsObj,_mem_type mem_type):m_nsObj(nsObj)
{
switch (mem_type)
{
case retain:
{
UniversalRetain(m_nsObj);
}
break;
case assign:
break;
case copy:
[m_nsObj copy];
break;
default:
break;
}
}
inline ns_handle::ns_handle(const ns_handle & handle):m_nsObj(handle.m_nsObj)
{
UniversalRetain(m_nsObj);
}
inline ns_handle & ns_handle::operator=(const ns_handle & handle)
{
#if __has_feature(objc_arc)
m_nsObj = handle.m_nsObj;
#else
[m_nsObj release];
m_nsObj = handle.m_nsObj;
[m_nsObj retain];
#endif
return *this;
}

inline id ns_handle::operator*()
{
return m_nsObj;
}

inline ns_handle::~ns_handle()
{
UniversalRelease(m_nsObj);
}

#endif /* NSHandle_h */

UIScrollView中的UIButton延迟高亮问题

之前在开发的过程中,就发现UIButton的点击在UITableViewCell中会出现高亮延迟的现象,究其原因是:
当手指触摸到UIScrollView内容的一瞬间,会产生下面的动作:

1、拦截触摸事件,tracking属性变为YES
2、一个内置的计时器开始生效(默认时间间隔是150ms),用来监控在极短的事件间隔内是否发生了手指移动
3、当检测到时间间隔内手指发生了移动,UIScrollView自己触发滚动,tracking属性变为NO,手指触摸下即使有(可以响应触摸事件的)内部控件也不会再响应触摸事件。
4、当检测到时间间隔内手指没有移动,tracking属性保持YES,手指触摸下如果有(可以响应触摸事件的)内部控件,则将触摸事件传递给控件进行处理。

可以通过delaysContentTouches属性来搞定该问题

1
2
3
4
5
6
self.tableView.delaysContentTouches = NO;
for (id obj in self.tableView.subviews) {
if ([obj respondsToSelector:@selector(setDelaysContentTouches:)]) {
[obj setDelaysContentTouches:NO];
}
}

惹不起的UITableViewCell

最近在做个颜色标签,感觉用UILabel很省事,想不到在UITableViewCell却遇到了麻烦,究其原因是UITableViewCell在高亮的时候会改变子视图的背景色:
优化前

但是有时候,并不想要这样的效果。

通过查阅资料,找到一种解决方案

主要思想:通过swizzle方式,分别hook UITableViewCellsetHighlighted:animated:方法,和UILablesetBackgroundColor:方法。然后在UITableViewCell高亮的时候,修改UILabelforbidSetBackgroundColor属性,来进行控制

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
// UITableViewCell+CellClick.m

- (void)qr_setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
if (highlighted)
{
[self setupInnerLabelForbidState:YES superView:self.contentView];
}
else
{
[self setupInnerLabelForbidState:NO superView:self.contentView];
}
[self qr_setHighlighted:highlighted animated:animated];
}

- (void)setupInnerLabelForbidState:(BOOL)forbid superView:(UIView*)superView
{
for (UIView *subview in superView.subviews)
{
if ([subview isKindOfClass:[UILabel class]])
{
// 如果设置了firbidSetBackgroundColorWhenHighlighted属性,则不允许改变
// UILabel的背景色值
UILabel *label = (UILabel *)subview;
if(label.firbidSetBackgroundColorWhenHighlighted)
label.forbidSetBackgroundColor = forbid;
}
else if(subview.class.isCustomClass)
{
// 递归遍历子视图
[self setupInnerLabelForbidState:forbid superView:subview];
}
}
}
1
2
3
4
5
6
7
8
//  UILabel+CellClick.m
- (void)qr_setBackgroundColor:(UIColor *)backgroundColor
{
// 如果没有禁止,就可以调用原来的改变颜色方法
if (!self.forbidSetBackgroundColor){
[self qr_setBackgroundColor:backgroundColor];
}
}

这样的解决方案略显沉重,为了一个小小的背景色,竟然要付出这么高的代价,实在有些得不偿失:要知道每个UITableViewCell初始化的时候,都要调用setBackgroundColor:方法,此外还带着入侵性属性:firbidSetBackgroundColorWhenHighlighted,外部必须传这个属性值来告知是否要执行上面的这一套逻辑。

最关键的是这个方案没有彻底解决问题,虽然躲过了UITableViewCell的高亮逻辑,然而工程项目中有些自定义的控件(也有自己的一套高亮逻辑),却不能幸免,只能说是一个不是很完美的解决方案。

该问题最根本的原因就是根本控制不住视图的setBackgroundColor:方法被调用
如何化解这个大招呢?

如果实现一个自定义控件,设置好setBackgroundColor:方法的调用权限,不就好了吗?

QRCellLabel为例:

1、定义一个内部类:QRCellInnerLabel(继承自UILabel),设置shouldSetBackgroundColor属性,来控制setBackgroundColor:的调用权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface QRCellInnerLabel : UILabel
@property(nonatomic,assign) BOOL shouldSetBackgroundColor;
@end

@implementation QRCellInnerLabel
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
if(_shouldSetBackgroundColor)
{
[super setBackgroundColor:backgroundColor];
}
}

@end

2、定义一个QRCellLabel类,和UILabel的各种属性保持一致,方便调用,编译级别禁止调用setBackgroundColor:方法,并暴露自己的颜色设置方法qr_setBackgroundColor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface QRCellLabel : UIView

@property(nullable, nonatomic,copy) NSString *text;
@property(null_resettable, nonatomic,strong) UIFont *font;
@property(null_resettable, nonatomic,strong) UIColor *textColor;
@property(nonatomic,assign) NSTextAlignment textAlignment;
@property(nonatomic,assign) NSLineBreakMode lineBreakMode;

// 注意NS_UNAVAILABLE,编译级别禁止调用该方法
- (void)setBackgroundColor:(UIColor *_Nullable)backgroundColor NS_UNAVAILABLE;

// 暴露自己的设置背景颜色方法
- (void)qr_setBackgroundColor:(UIColor *_Nullable)backgroundColor;

// 如果有不够用的属性,在下面添加,然后在.m文件中重新set方法
@end

3、QRCellLabel包括两个子控件:UIImageViewQRCellInnerLabel,前者是为了添加背景色,后者是为了文字。
分别实现QRCellLabelsetBackgroundColor:方法和qr_setBackgroundColor:方法:

  • setBackgroundColor:的实现为空方法,这样即使外侧调进来,也无法改变控件的背景色;
  • qr_setBackgroundColor:的实现如下:每次调用的时候,将shouldSetBackgroundColor的属性打开,一旦设置完毕QRCellInnerLabel的背景色,立刻将shouldSetBackgroundColor的属性关闭,所以该控件的背景色只能由自己暴露的qr_setBackgroundColor:方法来改变
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
@implementation QRCellLabel
{
UIImageView* _bgView;
QRCellInnerLabel* _label;
}

- (void)setupBackground
{
_bgView = [[UIImageView alloc]init];
_bgView.backgroundColor = [UIColor clearColor];
[self addSubview:_bgView];
}

- (void)setBackgroundColor:(UIColor *)backgroundColor
{
// 什么都不做,成功躲过大招
}


- (void)qr_setBackgroundColor:(UIColor *)backgroundColor
{
[_bgView setImage:[UIImage qr_imageWithColor:backgroundColor]];

_label.shouldSetBackgroundColor = YES;
_label.backgroundColor = UIColor.clearColor;
_label.shouldSetBackgroundColor = NO;
}


// 各种setter方法的重写

- (void)setFont:(UIFont *)font{
_label.font = font;
}

- (void)setText:(NSString *)text{
_label.text = text;
}

- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode{
_label.lineBreakMode = lineBreakMode;
}

- (void)setTextAlignment:(NSTextAlignment)textAlignment{
_label.textAlignment = textAlignment;
}

- (void)setTextColor:(UIColor *)textColor{
_label.textColor = textColor;
}

...

这样,无论这个控件放到哪个父视图里面,也不会被修改背景色,因为没有权限!

Lottie的坑

前端时间用lottie做动画的时候,碰到了几个问题,这里简单描述下:
(1)缓存问题
在利用lottie做动画的时候,都会把json文件和资源文件打包到一个bundle中,方便处理
有两个lottie文件如下,除了bundle的名字不一样之外,里面的json文件名和资源名称都是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
_leftAnimationBundle = [NSBundle bundleWithURL:[NSBundle.mainBundle URLForResource:@"left_animation"  withExtension:@"bundle"]];
_leftAnimationView = [LOTAnimationView animationNamed:@"data" inBundle:self.leftAnimationBundle];
_leftAnimationView.frame = QRRectMake(24, 0, 80, 90);
_leftAnimationView.loopAnimation = NO;
[self addSubview:_leftAnimationView];


_rightAnimationBundle = [NSBundle bundleWithURL:[NSBundle.mainBundle URLForResource:@"right_animation" withExtension:@"bundle"]];
_rightAnimationView = [LOTAnimationView animationNamed:@"data" inBundle:self.rightAnimationBundle];
_rightAnimationView.frame = QRRectMake(100, 0, 80, 90);
_rightAnimationView.loopAnimation = NO;
[self addSubview:_rightAnimationView];

然后在加载的时候,发现第二个lottie动画总是和第一个lottie展示一样,跟踪了源码,发现了如下端倪:animationName作为key,只用到了json的文件名,所以如果第二个bundle中的json文件名如果已经出现过,就不再加载,而是直接使用缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LOTComposition.m
+ (nullable instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle {

// 这里的animationName作为key,只用到了json的文件名
NSArray *components = [animationName componentsSeparatedByString:@"."];
animationName = components.firstObject;

LOTComposition *comp = [[LOTAnimationCache sharedCache] animationForKey:animationName];
if (comp) {
return comp;
}

...

if (JSONObject && !error) {
LOTComposition *laScene = [[self alloc] initWithJSON:JSONObject withAssetBundle:bundle];
[[LOTAnimationCache sharedCache] addAnimation:laScene forKey:animationName];
laScene.cacheKey = animationName;
return laScene;
}
return nil;
}

这显然是不太合理的,没什么理由让开发者保证不同bundle中的json名字还要不一样。
如果能把bundle的文件名也能作为一个因素考虑进去,去合成一个新的key,应该就能解决问题。
修改如下:新建了animationKey,由json的文件名和bundle的文件名组合而成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LOTComposition.m
+ (nullable instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle
{
NSArray *components = [animationName componentsSeparatedByString:@"."];

// 这里的key需要将bundle也作为一个参考因素,否则,多个bundle如果内部的json名字一样,会出问题
NSString* animationKey = [NSString stringWithFormat:@"%@_%@",components.firstObject,bundle.bundlePath.lastPathComponent];

LOTComposition *comp = [[LOTAnimationCache sharedCache] animationForKey:animationKey];
if (comp) {
return comp;
}
...
if (JSONObject && !error) {
LOTComposition *laScene = [[self alloc] initWithJSON:JSONObject withAssetBundle:bundle];
[[LOTAnimationCache sharedCache] addAnimation:laScene forKey:animationKey];
laScene.cacheKey = animationKey;
return laScene;
}

return nil;
}

(2)回调问题
由于项目需要,lottie动画播放完毕后,还需要做一些回调处理,所以一开始便用了下面的api

1
- (void)playWithCompletion:(nullable LOTAnimationCompletionBlock)completion;

但是却得不到任何回调处理,issue中也的确存在这个问题

后来直接设置回调,设置loopAnimation为NO,然后调用play,回调OK了

1
2
3
_leftAnimationView.loopAnimation = NO;// 如果设置为YES,会一直播放下去,不会回调
_leftAnimationView.completionBlock = [leftCompletionBlock copy];
[_leftAnimationView play];

然而问题接踵而至:
项目需求中需要连续播放2次动画,然后再进行回调,lottie并没有这样的api可以调用,解决办法是在回调中再次调用play方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@weakify(self)
id (^leftCompletionBlock)(BOOL animationFinished) = ^(BOOL animationFinished)
{
@strongify(self)
if(self.leftAnimationView.tag == 0){
self.leftAnimationView.tag = 1;
[self.leftAnimationView play];
}
else{
self.leftAnimationView.tag = 0;
self.leftAnimationView.hidden = YES;

// 最后的回调处理
...
}
};

本以为问题应该得到解决了,然而还是有点天真,第二次回调死活不回来。。。
跟踪源码后发现,回调只要调用1次,就会被设置为空:self.completionBlock = nil

1
2
3
4
5
6
7
8
// LOTAnimationView.m
- (void)_callCompletionIfNecessary:(BOOL)complete {
if (self.completionBlock) {
LOTAnimationCompletionBlock completion = self.completionBlock;
self.completionBlock = nil;
completion(complete);
}
}

这里不是特别明白作者的用意(难道是担心Block内存泄露?)
所以先把这里的self.completionBlock = nil注释掉,问题得以解决。

UIImageView的复用问题

QQReader之前有个问题:就是书架上的书封偶尔会出现错乱的情况,一直以为是cell的复用问题造成的,但是排查了许久,发现每次复用cell之前,都会把里面的UIImageView的image属性设置为nil,感觉不应该出问题,也甚至一直怀疑是刷新机制有些问题,天真的我…

直到最近定位到了问题所在:假设某个cell加载的一个书封urlA,在网络回来之前就被复用了,复用之后也加载了一个书封urlB,但是urlB网络回来比urlA还早(或者设置了一个本地书封),最后urlA的书封网络才回来,但是并没有解绑,所以还是会设置上去,最后本来应该显示urlB的书封,结果还是显示了urlA的书封,这样就导致了书封错乱。这也解释了每次复用之前光设置UIImageViewimage属性设置为nil不够的原因。

  • 那么如果工程中用的是第三方库SDWebImage,那么每次在复用之前需要调用这个方法来解除绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)sd_cancelCurrentImageLoad 
{
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}

如果工程中是自己实现的

  • 如果有解除绑定机制,那么在在复用之前解除绑定;
  • 如果没有解除绑定机制,那么在网络回来之后校验一下UIImageView当前的url和回来的imagedata对应的url是否一致:
1
2
3
4
if([self.imageUrl isEqualToString:imageFetchItem.key])
{
self.image = image;
}

总结

先写到这里,后续会更新添加内容

-------------本文结束 感谢您的阅读-------------

本文标题:iOS问题集锦

文章作者:lingyun

发布时间:2018年11月13日 - 22:11

最后更新:2018年12月31日 - 18:12

原始链接:https://tsuijunxi.github.io/2018/11/13/iOS问题集锦/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。