前言
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
这段代码看上去倒是没有什么问题,适配以来在iPhoneX上也并未出过什么问题,然而直到今年苹果爸爸又推出了新的3款iPhone设备,iPhoneXS、iPhoneXR、iPhoneXSMax,问题来了:
这3款新的设备都是刘海屏,刘海高度都是一致的,这样其UI适配和iPhoneX的适配无二异,所以按照搬砖惯性,不自觉地就会往这个宏里面加点东西:1
2
3
4
5
(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
({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 | - (BOOL)isPhoneX |
再也不用担心老年机模式了,打完收工。
躺着的tableview
平常情况下,UITableView
可以通过简单的设置实现SectionHeader悬浮问题,然而想要一个躺着的UITableView
来实现悬浮就没那么容易了,需要借助于UICollectionView
,悬浮要靠自己来实现。
这里主要实现了一个QRHorizontalFloatingHeaderLayout,它继承自UICollectionViewLayout,使用方式如下,之后需要把QRHorizontalFloatingHeaderLayoutDelegate
中的代理方法实现一下就可以了:1
2
3
4QRHorizontalFloatingHeaderLayout *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 |
|
C++的容器如何存放OC对象
日常的iOS开发中最常用的容器莫过于NSArray
和NSDictionary
了,然而在一些对性能要求比较场合下这两个容器明显不给力,很多人选择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
66class 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
5NSString * 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
2void *retainedThing = (__bridge_retained void *)obj;
retainedThing = retainedThing;
ARC下,release一个对象obj:1
2
3void *retainedThing = (__bridge void *)obj;
id unretainedThing = (__bridge_transfer id)retainedThing;
unretainedThing = nil;
这里直接利用宏的形式统一处理ARC和MRC下的情况1
2
3
4
5
6
7
- ARC下不能很好地处理C++里面的引用&,所以直接将下面的方法的返回值从id&修改为id
1 | // 修改前 |
解决了上面的两个问题,给出修改后的代码:
1 |
|
UIScrollView中的UIButton延迟高亮问题
之前在开发的过程中,就发现UIButton
的点击在UITableViewCell
中会出现高亮延迟的现象,究其原因是:
当手指触摸到UIScrollView内容的一瞬间,会产生下面的动作:
1、拦截触摸事件,tracking属性变为YES
2、一个内置的计时器开始生效(默认时间间隔是150ms),用来监控在极短的事件间隔内是否发生了手指移动
3、当检测到时间间隔内手指发生了移动,UIScrollView自己触发滚动,tracking属性变为NO,手指触摸下即使有(可以响应触摸事件的)内部控件也不会再响应触摸事件。
4、当检测到时间间隔内手指没有移动,tracking属性保持YES,手指触摸下如果有(可以响应触摸事件的)内部控件,则将触摸事件传递给控件进行处理。
可以通过delaysContentTouches
属性来搞定该问题1
2
3
4
5
6self.tableView.delaysContentTouches = NO;
for (id obj in self.tableView.subviews) {
if ([obj respondsToSelector:@selector(setDelaysContentTouches:)]) {
[obj setDelaysContentTouches:NO];
}
}
惹不起的UITableViewCell
最近在做个颜色标签,感觉用UILabel很省事,想不到在UITableViewCell
却遇到了麻烦,究其原因是UITableViewCell
在高亮的时候会改变子视图的背景色:
但是有时候,并不想要这样的效果。
通过查阅资料,找到一种解决方案
主要思想:通过swizzle方式,分别hook UITableViewCell
的setHighlighted:animated:
方法,和UILable
的setBackgroundColor:
方法。然后在UITableViewCell
高亮的时候,修改UILabel
的forbidSetBackgroundColor
属性,来进行控制
1 | // UITableViewCell+CellClick.m |
1 | // UILabel+CellClick.m |
这样的解决方案略显沉重,为了一个小小的背景色,竟然要付出这么高的代价,实在有些得不偿失:要知道每个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包括两个子控件:UIImageView
和QRCellInnerLabel
,前者是为了添加背景色,后者是为了文字。
分别实现QRCellLabel
的setBackgroundColor:
方法和qr_setBackgroundColor:
方法:
setBackgroundColor:
的实现为空方法,这样即使外侧调进来,也无法改变控件的背景色;qr_setBackgroundColor:
的实现如下:每次调用的时候,将shouldSetBackgroundColor
的属性打开,一旦设置完毕QRCellInnerLabel
的背景色,立刻将shouldSetBackgroundColor
的属性关闭,所以该控件的背景色只能由自己暴露的qr_setBackgroundColor:
方法来改变
1 | @implementation QRCellLabel |
这样,无论这个控件放到哪个父视图里面,也不会被修改背景色,因为没有权限!
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动画播放完毕后,还需要做一些回调处理,所以一开始便用了下面的api1
- (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的书封,这样就导致了书封错乱。这也解释了每次复用之前光设置UIImageView
的image
属性设置为nil不够的原因。
- 那么如果工程中用的是第三方库
SDWebImage
,那么每次在复用之前需要调用这个方法来解除绑定
1 | - (void)sd_cancelCurrentImageLoad |
如果工程中是自己实现的
- 如果有解除绑定机制,那么在在复用之前解除绑定;
- 如果没有解除绑定机制,那么在网络回来之后校验一下UIImageView当前的url和回来的imagedata对应的url是否一致:
1 | if([self.imageUrl isEqualToString:imageFetchItem.key]) |
总结
先写到这里,后续会更新添加内容