浅谈iOS复杂列表优化

记录一下最进项目改版的一些思考

问题

由于原来首页列表需求多次大变动,加载cell的逻辑也跟着调整了很多次,cell的种类以及布局也变化很大,这次刚好进行重构;而且,原来的加载方式会大量进行子视图修改,有一定的潜在性能问题.

需求分析及目标

  1. cell布局更加灵活,以后增加新类型不需要修改控制器代码逻辑,简言之,视图和控制器解耦;
  2. 实现代码复用
  3. 解决潜在性能问题
    在进行coding之前,可以参考一下几篇文章,里面对列表的论述多有裨益:
    更轻量的 View Controllers
    整洁的 Table View 代码
    iOS 保持界面流畅的技巧

前两篇介绍了如何编写低耦合的UITableView代码的思想,核心就是:

  1. 将其delegatedatasource进行独立,创建单独的类进行管理,这个类是可以复用的,并且通过blocks的方法进行cell的创建于赋值,而控制器不需要了解cell的实现;
  2. 将数据操作集中到单独创建的类中,而数据模型有时候携带的数据还需要重新加工才可以使用,例如返回yyyy-MM-dd HH:mm:ss类型的时间,而我们需要的是yyyy年MM月dd日格式的字符串,类似这种处理可以放在模型的category中;
  3. 面对复杂的布局,比如当前页面中会显示多个控制器视图,类似UITabBarController结构,我们可以采用加载Child Controller的方式,将内聚程度高的代码写到所属控制器中;
    这样,基本上就可以写出比较好,易于测试的UITabelView了,具体的代码可以参考原文,讲述的很详细了.

第三篇博文主要讲解了如何进行性能优化,总结下来就是以下几点:

  1. CPU耗费资源的地方主要在对象创建,对象调整,对象销毁,布局计算,Autolayout,文本计算,文本渲染,图片的解码,图像的绘制等;
  2. GPU资源的耗费主要集中在纹理的渲染,视图的混合,图像的生成
    针对以上几个方面,作者一一给出了解决方案,详细内容请参看原文
    值得注意的是作者提出了过早的优化是万恶之源,当需求不明显或者性能问题不明显的时候尽量不要尝试优化,并给出了评测界面的方法,开源了一个查看FPS的小工具,地址戳一下
    作者开源了一个开发套件,非常不错,里面附了一个微博的Feedlist demo,代码写的非常漂亮,准备借鉴他的做法,demo地址

实践

看一下页面列表,大概是这样的:
home page list
首页返回的简化数据结构:

1
2
3
4
5
6
7
8
9
10
11
@interface DHHomeItemInfo : NSObject
@property (nonatomic, copy) NSString *itemInfoId;
@property (nonatomic, assign) DHHomeItemType type; /**< 类型:1、text 2、list*/
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSData *content; /**< 返回的泛型数据,根据type进行解析*/
@property (nonatomic, strong) User *user;
@property (nonatomic, strong) NSDate *updateTime;
@property (nonatomic, assign) NSUInteger unreadTotal;
@end

现在cell至少有两种布局,一种根据list显示若干item,一种显示为text,根据上图可以将 cell 划分三部分:

  1. 顶部显示标题,包括姓名以及指标的名称,未读数,日期,分割线,实际上还有一个诊断的按钮, UI 上没有显示,诊断和标题有点击事件;
  2. 中部根据返回的数据类型,显示为文本或者若干 item, item 最多显示6个,并且可能有点击事件;
  3. 底部显示箭头,阴影以及一定的留白.

首先,定义一个DHHomeCell类,用于显示所有的数据类型,在主页中进行设置;
接着,定义一个DHHomeLayout类,用于在子线程计算cell的布局等耗时操作,cell通过layout对象进行对象绑定,高度设置;
最后,定义一个DHHomeCellDelegate,用于传递点击事件.
这样,就把视图,数据处理以及交互进行了分离.

cell中大概是这样的:

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
@class DHHomeCell;
@protocol DHHomeCellDelegate;
/// 顶部
@interface DHHomeTitleView : UIView
// ...顶部视图属性
// 持有父视图cell
@property (nonatomic, weak) DHHomeCell *cell;
@end
/// 中部
/// 文本
@interface DHHomeTextLabel : UILabel
// ...视图属性
// 持有父视图cell
@property (nonatomic, weak) DHHomeCell *cell;
@property (nonatomic, strong) DHHomeLayout *layout;
@end
/// 单独指标的view
@interface HDNewDataItemView : UIView
@end
/// 底部视图
@interface HDHomeBottomView : UIView
// ...视图属性
// 持有父视图cell
@property (nonatomic, weak) DHHomeCell *cell;
@property (nonatomic, strong) DHHomeLayout *layout;
@end
// 容器 view
@interface DHHomeContentView: UIView
@property (nonatomic, strong) UIView *contentView; /// 容器
@property (nonatomic, strong) DHHomeTitleView *titleView; /// 标题栏
@property (nonatomic, strong) DHDiagnoseButton *diagnoseButton; /// 诊断按钮
@property (nonatomic, strong) NSArray *items; ///指标 Array<HDNewDataItemView>
@property (nonatomic, strong) DHHomeTextLabel *contentTextLabel; /// 文本
@property (nonatomic, strong) HDHomeBottomView *contentTextLabel; /// 底部
@property (nonatomic, strong) DHHomeLayout *layout; /**< 布局*/
@property (nonatomic, weak) DHHomeCell *cell;
@end
@interface DHHomeCell : UITableViewCell
@property (nonatomic, weak) id<DHHomeCellDelegate> delegate;
@property (nonatomic, strong) DHHomeContentView *dataContentView;
- (void)setHomeLayout:(DHHomeLayout *)layout;
@end
/// 代理方法
@protocol DHHomeCellDelegate <NSObject>
@optional
/// 点击了 Cell
- (void)cellDidClick:(DHHomeCell *)cell;
/// 点击了用户
- (void)cell:(DHHomeCell *)cell didClickUser:(NSString *)userId;
// 点击诊断信息
- (void)cell:(DHHomeCell *)cell didClickDiagnose:(DiagnoseInfo *)info;
/// 点击了item
- (void)cell:(DHHomeCell *)cell didClickNewDataItemAtIndex:(NSUInteger)index;
@end

所有的视图都通过layout的计算结果进行布局,当为list样式,cell中的HDNewDataItemView个数不确定,类似的,根据常见的九宫格布局,可以在初始化的时候一次性添加 6 个这样的子视图,默认全部隐藏,然后根据返回的list进行显示.

DHHomeLayout中,可以定义一些处理好的数据以及计算的frame作为属性,方便进行缓存处理,实现大概是这样子的:

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
// 子线程进行布局
@interface DHHomeLayout : NSObject
- (instancetype)initWithWorkspaceItem:(DHHomeItemInfo *)item style:(DHHomeItemType)type;
// 做一些其他更新
- (void)updateSomething;
@property (nonatomic, strong, readonly) DHHomeItemInfo *item;
@property (nonatomic, assign, readonly) DHHomeLayoutStyle style;
// item中一些深层次访问的对象,或者经过处理的若干属性,比如处理好的时间字符串
@property (nonatomic, copy) NSString *updateTime;
// layout
@property (nonatomic, assign) CGFloat height; /**< 总高度*/
// 顶部视图高度以及子视图位置信息
// ...若干
// 中部视图位置信息
// ...若干
// 底部视图位置信息
// ...若干
@end

需要注意的是,其中一些属性,比如图片相关的设置,是需要在主线程中进行的.一些经常使用的图片或者创建耗时的对象可以使用dispatch_once代码块进行保存,都可以进行一定的性能优化,如果还是出现卡顿,确定问题后,可以借助开源的异步显示框架进行优化.
调用起来大概是这样子的:

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
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
DHHomeLayout *layout = [[DHHomeLayout alloc] initWithWorkspaceItem:item style:style];
[self.layouts addObject:layout];
// 多复制一下列表,测试长度
for (NSInteger i = 0; i < 10; i++) {
[self.layouts addObjectsFromArray:[self.layouts copy]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
});
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _layouts.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString * const cellID = @"cell";
DHHomeCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!cell) {
cell = [[DHHomeCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
cell.delegate = self;
}
[cell setHomeLayout:_layouts[indexPath.row]];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [(DHHomeLayout *)_layouts[indexPath.row] height];
}
// 一堆代理方法...

这样,基本完成了复杂列表的重构.
还可以创建一个工具类或者category,将常用的处理方法进行封装,使用工具类处理,可以方便测试以及代码复用.

如果以后需求出现变动,只需要cell添加新视图,layout计算新的视图的布局,增加新的枚举类型即可完成布局.