文章目录
  1. 1. UITableView 从设计到到优化
    1. 1.1. 设计
    2. 1.2. UITableView优化技巧
      1. 1.2.1. UITableViewCell的重用
      2. 1.2.2. 回调分析
      3. 1.2.3. 简单优化
      4. 1.2.4. 动态按需加载
    3. 1.3. 总结

UITableView 从设计到到优化

  • LastDays,这是我的Blog,我在这里分享我的学习,
  • 我的微博我在这里分享我的生活,欢迎交流

经常使用UITableView,在一些应用,发现他们写的TableView非常的流畅漂亮,因此自己准备系统的学习一下TableView,最后进行一下总结

设计

总结了以下的基本使用

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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
//
// ViewController.m
// TableView
//
// Created by LastDay on 16/3/13.
// Copyright © 2016年 LastDays. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()<UITableViewDelegate,UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic,strong) NSDictionary *myDictonary;

@end



@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

NSArray *oneArray = @[@"LastDays1",@"LastDays2",@"LastDays3"];
NSArray *twoArray = @[@"白菜1",@"白菜2",@"白菜3"];
NSArray *threeArray = @[@"小猪1",@"小猪2",@"小猪3"];

self.myDictonary = @{@"LastDays":oneArray,@"白菜":twoArray,@"小猪":threeArray};

[self configureTbaleView];



}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}


/**
* 配置TableView基本属性
*/
-(void)configureTbaleView{
//改变行线颜色
_tableView.separatorColor = [UIColor blueColor];
//设定Header的高度,
_tableView.sectionHeaderHeight=100;
//设定footer的高度,
_tableView.sectionFooterHeight=100;
//设定行高
_tableView.rowHeight=100;
//设定cell分行线的样式,默认为UITableViewCellSeparatorStyleSingleLine
[_tableView setSeparatorStyle:UITableViewCellSeparatorStyleSingleLine];
//设定cell分行线颜色
[_tableView setSeparatorColor:[UIColor redColor]];
//编辑tableView
_tableView.editing=NO;

}

/**
* 指定多少个分区
*
* @param tableView tableView
*
* @return myDictonary键值个数
*/
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return [[_myDictonary allKeys] count];
}
/**
* 指定每个分区中有多少行
*
* @param tableView tableView
* @param section section
*
* @return 返回当前section所需行数
*/
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return [[self.myDictonary objectForKey:[[self.myDictonary allKeys]objectAtIndex:section]] count];;
}

/**
* section底部标题高度(实现这个代理方法后前面 sectionHeaderHeight 设定的高度无效)
*
* @param tableView tableView
* @param section section
*
* @return 底部标题所需高度
*/
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
return 20;
}

/**
* 每个section头部标题高度(实现这个代理方法后前面 sectionFooterHeight 设定的高度无效)
*
* @param tableView tableView
* @param section section
*
* @return 头部标题所需高度
*/
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
return 100;
}

/**
* 每个section头部标题
*
* @param tableView tableView
* @param section section
*
* @return 头部标题
*/
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
return [[self.myDictonary allKeys] objectAtIndex:section];
}

/**
* 每个人section底部标题
*
* @param tableView tableView
* @param section section
*
* @return 底部标题
*/
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section{
return @"底部标题";
}

/**
* 自定义section头部View,想测试可以去掉注释
*
* @param tableView tableView
* @param section section
*
* @return 自定义UIView
*/
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{

// UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
// imageView.image = [UIImage imageNamed:@"author.png"];
// return imageView;
return nil;
}

/**
* 自定义底部View
*
* @param tableView tableView
* @param section section
*
* @return 自定义UIView
*/
-(UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{
return nil;
}

/**
* 改变行高,需要注意,实现这个代理方法后, rowHeight 设定的高度无效无效
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return 返回行高
*/
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 100;
}

/**
* 绘制Cell
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return Cell
*/
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];

if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
/**
* 设定附加视图
* UITableViewCellAccessoryNone, 没有标示
* UITableViewCellAccessoryDisclosureIndicator, 下一层标示
* UITableViewCellAccessoryDetailDisclosureButton, 详情button
* UITableViewCellAccessoryCheckmark 勾选标记
*/
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];

/**
* 选定cell时改变颜色
* cell.selectionStyle=UITableViewCellSelectionStyleBlue; 选中时蓝色效果
* cell.selectionStyle=UITableViewCellSelectionStyleNone; 选中时没有颜色效果
* cell.selectionStyle=UITableViewCellSelectionStyleGray; 选中时灰色效果
*/
cell.selectionStyle=UITableViewCellSelectionStyleBlue;

}
//[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
cell.contentView.backgroundColor = [UIColor clearColor];
cell.imageView.image=[UIImage imageNamed:@"author.png"];//未选cell时的图片
cell.imageView.highlightedImage=[UIImage imageNamed:@"logo.png"];//选中cell后的图片

cell.textLabel.text = [[self.myDictonary objectForKey:[[self.myDictonary allKeys]objectAtIndex:indexPath.section]]objectAtIndex:indexPath.row];

return cell;
}

/**
* 行缩进,按照阶梯状下移
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return row
*/
-(NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath{
NSUInteger row = [indexPath row];
return row;
}

/**
* 行将显示的时候调用,预加载行
*
* @param tableView tableView
* @param cell cell
* @param indexPath indexPath
*/
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
// NSLog(@"将要显示的行是\n cell=%@ \n indexpath=%@",cell,indexPath);
}


/**
* 选中Cell响应
*
* @param tableView tableView
* @param indexPath indexPath
*/
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[tableView deselectRowAtIndexPath:indexPath animated:YES];//选中后的反显颜色即刻消失

//得到当前选中的cell
UITableViewCell *cell=[tableView cellForRowAtIndexPath:indexPath];
NSLog(@"cell=%@",cell);
}

/**
* didSelectRowAtIndexPath操作之前执行,这里我们设置第二行不可选
*
* @param tableView tableView
* @param cell 选中的cell
* @param indexPath indexPath
*/
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSInteger row = [indexPath row];
if (row == 1) {
return nil;
}
return indexPath;
}

/**
* cell右边按钮格式为UITableViewCellAccessoryDetailDisclosureButton时,点击按扭时调用的方法
*
* @param tableView tableView
* @param indexPath indexPath
*/
-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath{
NSLog(@"当前点击的详情button \n indexpath=%@",indexPath);
}

/**
* 右侧添加索引表
*
* @param tableView tableView
*
* @return 索引NSArray
*/
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
return [self.myDictonary allKeys];
}
/**
* 侧滑Cell是否出现删除
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return BOOl
*/
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath{

return YES;
}

/**
* 设定横向滑动时是否出现删除按扭
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return UITableViewCellEditingStyle
*/
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{

return UITableViewCellEditingStyleDelete;
}

/**
* 自定义delete按钮
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return NSString
*/
- (NSString *)tableView:(UITableView *)tableView
titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath{
return @"删除这行";

}
/**
* 开始移动row时执行
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return NSString
*/
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath
{
NSLog(@"sourceIndexPath=%@",sourceIndexPath);
NSLog(@"sourceIndexPath=%@",destinationIndexPath);
}
/**
* 滑动可以编辑时执行
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return NSString
*/
-(void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"willBeginEditingRowAtIndexPath");
}
/**
* 将取消选中时执行, 也就是上次先中的行
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return NSString
*/
-(NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"上次选中的行是 \n indexpath=%@",indexPath);
return indexPath;
}

/**
* 让行可以移动
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return NSString
*/
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
/**
* 移动row时执行
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return NSString
*/
-(NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath
{
NSLog(@"targetIndexPathForMoveFromRowAtIndexPath");
//用于限制只在当前section下面才可以移动
if(sourceIndexPath.section != proposedDestinationIndexPath.section){
return sourceIndexPath;
}
return proposedDestinationIndexPath;
}

@end

UITableView优化技巧

UITableView的优化是UITableView的难点,流畅的运行,打造出类是朋友圈的功能,优化就是我们需要考虑的东西了。

UITableViewCell的重用

用过UITableView的人都知道,其实它最关键的地方就是UITableViewCell,重用思想我理解的就是,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
/**
* 改变行高,需要注意,实现这个代理方法后, rowHeight 设定的高度无效无效
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return 返回行高
*/
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
return cell.frame.size.height;
}

/**
* 绘制Cell
*
* @param tableView tableView
* @param indexPath indexPath
*
* @return Cell
*/
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];

if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
/**
* 设定附加视图
* UITableViewCellAccessoryNone, 没有标示
* UITableViewCellAccessoryDisclosureIndicator, 下一层标示
* UITableViewCellAccessoryDetailDisclosureButton, 详情button
* UITableViewCellAccessoryCheckmark 勾选标记
*/
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];

/**
* 选定cell时改变颜色
* cell.selectionStyle=UITableViewCellSelectionStyleBlue; 选中时蓝色效果
* cell.selectionStyle=UITableViewCellSelectionStyleNone; 选中时没有颜色效果
* cell.selectionStyle=UITableViewCellSelectionStyleGray; 选中时灰色效果
*/
cell.selectionStyle=UITableViewCellSelectionStyleBlue;

}
//[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
cell.contentView.backgroundColor = [UIColor clearColor];
cell.imageView.image=[UIImage imageNamed:@"author.png"];//未选cell时的图片
cell.imageView.highlightedImage=[UIImage imageNamed:@"logo.png"];//选中cell后的图片

cell.textLabel.text = [[self.myDictonary objectForKey:[[self.myDictonary allKeys]objectAtIndex:indexPath.section]]objectAtIndex:indexPath.row];

return cell;
}

UITableView两个回调方法tableView:cellForRowAtIndexPath:和tableView:heightForRowAtIndexPath:他们的调用顺序我开始的时候认为先调,用的tableView:cellForRowAtIndexPath:在调用tableView:cellForRowAtIndexPath:先绘制控件,然后在设置布局,这是我们写控件的时候基本的思路,然而UITableView并没有那么做,而是先确定Cell的位置,在将Cell绘制在相应的位置上,就是说先调用tableView:heightForRowAtIndexPath:,在调用tableView:cellForRowAtIndexPath:

就像我们上面的例子,每个section要显示3个Cell,那么就是先调用三次tableView:heightForRowAtIndexPath:显示50个Cell时我们就先调用50次,然后在调用3次tableView:cellForRowAtIndexPath:。我们滚动屏幕时,每当Cell滚入屏幕,都会调用一次tableView:heightForRowAtIndexPath:、tableView:cellForRowAtIndexPath:方法。

基本上已经很清晰了,我们的优化主要也就是针对这两个回调展开。

简单优化

具体的优化思路就是让tableView:cellForRowAtIndexPath:方法只负责赋值,tableView:heightForRowAtIndexPath:方法只负责计算高度,不要出现代码重叠现象。根据这样的思路,其实我们可以在得到数据的时候就进行优化,计算出对应的布局,并且缓存起来,这样我们在tableView:heightForRowAtIndexPath:方法中直接返回高度,在这里又节省了计算的开销

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
NSDictionary * frameDictionary = self. frameList[indexPath.row];
CGRect rect = [dict[@"frame"] CGRectValue];
return rect.frame.height;
```


###自定义Cell
以上的简单思路就是应对简单的布局而已,如果要是那种朋友圈图文混排的,还是需要自定义Cell的,重写drawRect,然后进行异步绘制,在这里找到一个简单的示例代码

``` bahs
//异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGRect rect = [_data[@"frame"] CGRectValue];
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
//整个内容的背景
[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
CGContextFillRect(context, rect);
//转发内容的背景
if ([_data valueForKey:@"subData"]) {
[[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];
CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];
CGContextFillRect(context, subFrame);
[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));
}

{
//名字
float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;
float x = leftX;
float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;
[_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
andHeight:rect.size.height];
//时间+设备
y += SIZE_FONT_NAME+5;
float fromX = leftX;
float size = [UIScreen screenWidth]-leftX;
NSString *from = [NSString stringWithFormat:@"%@ %@", _data[@"time"], _data[@"from"]];
[from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)
andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
andHeight:rect.size.height andWidth:size];
}
//将绘制的内容以图片的形式返回,并调主线程显示
UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (flag==drawColorFlag) {
postBGView.frame = rect;
postBGView.image = nil;
postBGView.image = temp;
}
}
//内容如果是图文混排,就添加View,用CoreText绘制
[self drawText];
}}

动态按需加载

按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。节省开销

一段示例代码

1
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 8;
    if (labs(cip.row-ip.row)>skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
        if (velocity.y<0) {
            NSIndexPath *indexPath = [temp lastObject];
            if (indexPath.row+3<datas.count) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
            }
        } else {
            NSIndexPath *indexPath = [temp firstObject];
            if (indexPath.row>3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        [needLoadArr addObjectsFromArray:arr];
    }
}

总结

设计和优化简单的流程就是这些,我们总结下

  • 流畅的优化主要是先计算好高度布局,然后让tableView heightForRowAtIndexPath:直接返回高度节省内存开销。
  • 异步加载,按需加载Cell
  • 在TableView中我们经常会结合网络请求,大量加载图片,SDWebImage异步图片加载非常强大,善于利用。

除了上面最主要的三个方面外,还有很多几乎大伙都很熟知的优化点:

上面的三点是一个核心,还有一些常识

  • Cell内容来自web,尽量使用异步加载,缓存我们的请求结果,减少网络请求
  • 减少图层
  • 使用reuseIdentifier来重用Cells
    如果Cell内现实的内容来自web,使用异步加载,缓存请求结果
    减少subviews的数量
  • 在heightForRowAtIndexPath:中尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果

sunnyxx大神写过的一篇文章优化UITableViewCell高度计算的那些事 非常值得仔细看看

文章目录
  1. 1. UITableView 从设计到到优化
    1. 1.1. 设计
    2. 1.2. UITableView优化技巧
      1. 1.2.1. UITableViewCell的重用
      2. 1.2.2. 回调分析
      3. 1.2.3. 简单优化
      4. 1.2.4. 动态按需加载
    3. 1.3. 总结