在企业中UITableView用处太大了,学会如何去封装和优化真的很重要,给您的工作带来很多便利

2023小满春招iOS笔试复盘:从Runtime到架构设计的核心考点解析 在移动开发领域,iOS工程师不仅需要掌握UIKit、AutoLayout等界面构建技术,更要深入理解Objective-C的Runtime机制、Swift内存管理与多线程并发原理。消息传递、isa指针、循环引用、自动释放池等底层概念,直接影响App的稳定性与性能表现。随着业务复杂度的提升,诸如证书签名、ATS配置、沙盒存储等工程化问题也成为技术面试的高频考察点。同时,良好的架构设计能力,如通过协议与工厂模式构建可扩展的IM模块,是区分初级与高级开发者的关键。本文从这些通用技术基石谈起,结合2023年小满春招 阅读详情

如果你觉得 UITableViewDelegate 和 UITableViewDataSource 这两个协议中有大量方法每次都是复制粘贴,实现起来大同小异;如果你觉得发起网络请求并解析数据需要一大段代码,加上刷新和加载后简直复杂度爆表,如果你想知道为什么下面的代码可以满足上述所有要求:

1

解耦后的VC

MVC

在讨论解耦之前,我们要弄明白 MVC 的核心:控制器(以下简称 C)负责模型(以下简称 M)和视图(以下简称 V)的交互。
这里所说的 M,通常不是一个单独的类,很多情况下它是由多个类构成的一个层。最上层的通常是以 Model 结尾的类,它直接被 C 持有。Model 类还可以持有两个对象:

  1. Item:它是实际存储数据的对象。它可以理解为一个字典,和 V 中的属性一一对应
  2. Cache:它可以缓存自己的 Item(如果有很多)
    常见的误区:
  3. 一般情况下数据的处理会放在 M 而不是 C(C 只做不能复用的事)
  4. 解耦不只是把一段代码拿到外面去。而是关注是否能合并重复代码, 并且有良好的拖展性。

原始版

在 C 中,我们创建 UITableView 对象,然后将它的数据源和代理设置为自己。也就是自己管理着 UI 逻辑和数据存取的逻辑。在这种架构下,主要存在这些问题:

  1. 违背 MVC 模式,现在是 V 持有 C 和 M。
  2. C 管理了全部逻辑,耦合太严重。
  3. 其实绝大多数 UI 相关都是由 Cell 而不是 UITableView 自身完成的。
    为了解决这些问题,我们首先弄明白,数据源和代理分别做了那些事。
    数据源
    它有两个必须实现的代理方法:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

简单来说,只要实现了这个两个方法,一个简单的 UITableView 对象就算是完成了。
除此以外,它还负责管理 section 的数量,标题,某一个 cell 的编辑和移动等。
代理
代理主要涉及以下几个方面的内容:

  1. cell、headerView 等展示前、后的回调。
  2. cell、headerView 等的高度,点击事件。
    最常用的也是两个方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

提醒:绝大多数代理方法都有一个 indexPath 参数

优化数据源

最简单的思路是单独把数据源拿出来作为一个对象。
这种写法有一定的解耦作用,同时可以有效减少 C 中的代码量。然而总代码量会上升。我们的目标是减少不必要的代码。
比如获取每一个 section 的行数,它的实现逻辑总是高度类似。然而由于数据源的具体实现方式不统一,所以每个数据源都要重新实现一遍。

SectionObject

首先我们来思考一个问题,数据源作为 M,它持有的 Item 长什么样?答案是一个二维数组,每个元素保存了一个 section 所需要的全部信息。因此除了有自己的数组(给cell用)外,还有 section 的标题等,我们把这样的元素命名为 SectionObject:

@interface KtTableViewSectionObject : NSObject
@property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 协议中的 titleForHeaderInSection 方法可能会用到
@property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 协议中的 titleForFooterInSection 方法可能会用到
@property (nonatomic, retain) NSMutableArray *items;
- (instancetype)initWithItemArray:(NSMutableArray *)items;
@end

Item

其中的 items 数组,应该存储了每个 cell 所需要的 Item,考虑到 Cell 的特点,基类的 BaseItem 可以设计成这样:

@interface KtTableViewBaseItem : NSObject
@property (nonatomic, retain) NSString *itemIdentifier;
@property (nonatomic, retain) UIImage *itemImage;
@property (nonatomic, retain) NSString *itemTitle;
@property (nonatomic, retain) NSString *itemSubtitle;
@property (nonatomic, retain) UIImage *itemAccessoryImage;
- (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage;
@end

父类实现代码

规定好了统一的数据存储格式以后,我们就可以考虑在基类中完成某些方法了。以 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 方法为例,它可以这样实现:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.sections.count > section) {
        KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section];
        return sectionObject.items.count;
    }
    return 0;
}

比较困难的是创建 cell,因为我们不知道 cell 的类型,自然也就无法调用 alloc 方法。除此以外,cell 除了创建,还需要设置 UI,这些都是数据源不应该做的事。
这两个问题的解决方案如下:

  1. 定义一个协议,父类返回基类 Cell,子类视情况返回合适的类型。
  2. 为 Cell 添加一个 setObject 方法,用于解析 Item 并更新 UI。

优势

经过这一番折腾,好处是相当明显的:

  1. 子类的数据源只需要实现 cellClassForObject 方法即可。原来的数据源方法已经在父类中被统一实现了。
  2. 每一个 Cell 只要写好自己的 setObject 方法,然后坐等自己被创建,被调用这个方法即可。
  3. 子类通过 objectForRowAtIndexPath 方法可以快速获取 item,不用重写。
    对照 demo(SHA-1:6475496),感受一下效果。

优化代理

我们以之前所说的,代理协议中常用的两个方法为例,看看怎么进行优化与解耦。
首先是计算高度,这个逻辑并不一定在 C 完成,由于涉及到 UI,所以由 Cell 负责实现即可。而计算高度的依据就是 Object,所以我们给基类的 Cell 加上一个类方法:

+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;

另外一类问题是以处理点击事件为代表的代理方法, 它们的主要特点是都有 indexPath 参数用来表示位置。然而实际在处理过程中,我们并不关系位置,关心的是这个位置上的数据。
因此,我们对代理方法做一层封装,使得 C 调用的方法中都是带有数据参数的。因为这个数据对象可以从数据源拿到,所以我们需要能够在代理方法中获取到数据源对象。
为了实现这一点, 最好的办法就是继承 UITableView:

@protocol KtTableViewDelegate<UITableViewDelegate>
@optional
- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;
- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section;
// 将来可以有 cell 的编辑,交换,左滑等回调
// 这个协议继承了UITableViewDelegate ,所以自己做一层中转,VC 依然需要实现某
@end
@interface KtBaseTableView : UITableView<UITableViewDelegate>
@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource;
@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate;
@end

cell 高度的实现如下,调用数据源的方法获取到数据:

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
    id<KtTableViewDataSource> dataSource = (id<KtTableViewDataSource>)tableView.dataSource;
    KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
    Class cls = [dataSource tableView:tableView cellClassForObject:object];
    return [cls tableView:tableView rowHeightForObject:object];
}

优势
通过对 UITableViewDelegate 的封装(其实主要是通过 UITableView 完成),我们获得了以下特性:

  1. C 不用关心 Cell 高度了,这个由每个 Cell 类自己负责
  2. 如果数据本身存在数据源中,那么在代理协议中它可以被传给 C,免去了 C 重新访问数据源的操作。
  3. 如果数据不存在于数据源,那么代理协议的方法会被正常转发(因为自定义的代理协议继承自 UITableViewDelegate)
    对照 demo(SHA-1:ca9b261),感受一下效果。

更加 MVC,更加简洁

在上面的两次封装中,其实我们是把 UITableView 持有原生的代理和数据源,改成了 KtTableView 持有自定义的代理和数据源。并且默认实现了很多系统的方法。
到目前为止,看上去一切都已经完成了,然而实际上还是存在一些可以改进的地方:

  1. 目前仍然不是 MVC 模式!
  2. C 的逻辑和实现依然可以进一步简化
    基于以上考虑, 我们实现一个 UIViewController 的子类,并且把数据源和代理封装到 C 中。
@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate>
@property (nonatomic, strong) KtBaseTableView *tableView;
@property (nonatomic, strong) KtTableViewDataSource *dataSource;
@property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用来创建 tableView
- (instancetype)initWithStyle:(UITableViewStyle)style;
@end

为了确保子类创建了数据源,我们把这个方法定义到协议里,并且定义为 required。

成果与目标

现在我们梳理一下经过改造的 TableView 该怎么用:

  1. 首先你需要创建一个继承自 KtTableViewController 的视图控制器,并且调用它的 initWithStyle 方法。
    objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];
  2. 在子类 VC 中实现 createDataSource 方法,实现数据源的绑定。
*   (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 这 一步创建了数据源 } ```

1.在数据源中,需要指定 cell 的类型。

*   (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; } 

1.在 Cell 中,需要通过解析数据,来更新 UI 并返回自己的高度。

*   (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父类的 setObject 方法。 

还有什么要优化的
到目前为止,我们实现了对 UITableView 以及相关协议、方法的封装,使它更容易使用,避免了很多重复、无意义的代码。
在使用时,我们需要创建一个控制器,一个数据源,一个自定义 Cell,它们正好是基于 MVC 模式的。因此,可以说在封装与解耦方面,我们已经做的相当好了,即使再花大力气,也很难有明显的提高。
但关于 UITableView 的讨论远远没有结束,我列出了以下需要解决的问题

  1. 在这种设计下,数据的回传不够方便,比如 cell 的给 C 发消息。
  2. 下拉刷新与上拉加载如何集成
  3. 网络请求的发起,与解析数据如何集成
    关于第一个问题,其实是普通的 MVC 模式中 V 和 C 的交互问题,可以在 Cell(或者其他类) 中添加 weak 属性达到直接持有的目的,也可以定义协议。
    问题二和三是另一大块话题,网络请求大家都会实现,但如何优雅的集成进框架,保证代码的简单和可拓展,就是一个值得深入思考,研究的问题了。接下来我们就重点讨论网络请求。

为何创建网络层

一个 iOS 的网络层框架该如何设计?这是一个非常宽泛,也超出我能力范围之外的问题。业内已有一些优秀的,成熟的思路和解决方案,由于能力,角色所限,我决定从一个普通开发者而不是架构师的角度来说说,一个普通的、简单的网络层该如何设计。我相信再复杂的架构,也是由简单的设计演化而来的。
对于绝大多数小型应用来说,集成 AFNetworking 这样的网络请求框架就足以应付 99% 以上的需求了。但是随着项目的扩大,或者用长远的眼光来考虑,直接在 VC 中调用具体的网络框架(下面以 AFNetworking 为例),至少存在以下问题:

  1. 一旦日后 AFNetworking 停止维护,而且我们需要更换网络框架,这个成本将无法想象。所有的 VC 都要改动代码,而且绝大多数改动都是雷同的。
    这样的例子真实存在,比如我们的项目中就依然使用早已停止维护的 ASIHTTPRequest,可以预见,这个框架迟早要被替换。
  2. 现有的框架可能无法实现我们的需求。以 ASIHTTPRequest 为例,它的底层用 NSOperation 来表示每一个网络请求。众所周知,一个 NSOperation 的取消,并不是简单调用 cancel 方法就可以的。在不修改源码的前提下,一旦它被放入队列,其实是无法取消的。
  3. 有时候我们的需求仅仅是进行网络请求,还会对这个请求进行各种自定义的拓展。比如我们可能要统计请求的发起和结束时间,从而计算网络请求,数据解析的步骤的耗时。有时候,我们希望设计一个通用组件,并且支持由各个业务部门去自定义具体的规则。比如可能不同的部门,会为 HTTP 请求添加不同的头部。
  4. 网络请求还有可能有其他广泛需要添加的需求,比如请求失败时的弹窗,请求时的日志记录等等。
    参考当前代码(SHA-1:a55ef42)感受一下没有任何网络层时的设计。

如何设计网络层

其实解决方案非常简单:

所有的计算机问题,都可以通过添加中间层来解决

读者可以自行思考,为什么添加中间层可以解决上述三个问题。

三大模块

对于一个网络框架来说,我认为主要有三个方面值得去设计:

  1. 如何请求
  2. 如何回调
  3. 数据解析

一个完整的网络请求一般由以上三个模块组成,我们逐一分析每个模块实现时的注意事项:

发起请求

发起请求时,一般有两种思路,第一种是把所有要配置的参数写到同一个方法中,借用 与时俱进,HTTP/2下的iOS网络层架构设计 一文中的代码表示:

+ (void)networkTransferWithURLString:(NSString *)urlString
                       andParameters:(NSDictionary *)parameters
                              isPOST:(BOOL)isPost
                        transferType:(NETWORK_TRANSFER_TYPE)transferType
                   andSuccessHandler:(void (^)(id responseObject))successHandler
                   andFailureHandler:(void (^)(NSError *error))failureHandler {
                           // 封装AFN
                   }

这种写法的好处在于所有参数一目了然,而且简单易用,每次都调用这个方法即可。但是缺点也很明显,随着参数和调用次数的增多,网络请求的代码很快多到爆炸。
另一组方法则是将 API 设置成一个对象,把要传入的参数作为这个对象的属性。在发起请求时,只要设置好对象的相关属性,然后调用一个简单的方法即可。

@interface DRDBaseAPI : NSObject
@property (nonatomic, copy, nullable) NSString *baseUrl;
@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject,  NSError * _Nullable error);
- (void)start;
- (void)cancel;
...
@end

根据前文提到的 Model 和 Item 的概念,那么应该可以想到:这个用于访问网络的 API 对象,其实是作为 Model 的一个属性。

Model 负责对外暴露必要的属性和方法,而具体的网络请求则由 API 对象完成,同时 Model 也应该持有真正用来存储数据的 Item。

如何回调

一次网络请求的返回结果应该是一个 JSON 格式的字符串,通过系统的或者一些开源框架可以将它转换成字典。

接下来我们需要使用 runtime 相关的方法,将字典转换成 Item 对象。

最后,Model 需要将这个 Item 赋值给自己的属性,从而完成整个网络请求。

如果从全局角度来说,我们还需要一个 Model 请求完成的回调,这样 VC 才能有机会做相应的处理。

考虑到 Block 和 Delegate 的优缺点,我们选择用 Block 来完成回调。

数据解析

这一部分主要是利用 runtime 将字典转换成 Item,它的实现并不算难,但是如何隐藏好实现细节,使上层业务不用过多关心,则是我们需要考虑的问题。

我们可以定义一个基类的 Item,并且为它定义一个 parseData 函数:

// KtBaseItem.m
- (void)parseData:(NSDictionary *)data {
    // 解析 data 这个字典,为自己的属性赋值
    // 具体的实现请见后面的文章
}

封装 API 对象

首先,我们封装一个 KtBaseServerAPI 对象,这个对象的主要目的有三个:

  1. 隔离具体的网络库的实现细节,为上层提供一个稳定的的接口
  2. 可以自定义一些属性,比如网络请求的状态,返回的数据等,方便的调用
  3. 处理一些公用的逻辑,比如网络耗时统计
    具体的实现请参考 Git 提交历史:SHA-1:76487f7

Model 与 Item

BaseModel

Model 主要需要负责发起网络请求,并且处理回调,来看一下基类的 Model 如何定义:

@interface KtBaseModel
// 请求回调
@property (nonatomic, copy) KtModelBlock completionBlock;
//网络请求
@property (nonatomic,retain) KtBaseServerAPI *serverApi;
//网络请求参数
@property (nonatomic,retain) NSDictionary *params;
//请求地址 需要在子类init中初始化
@property (nonatomic,copy)   NSString *address;
//model缓存
@property (retain,nonatomic) KtCache *ktCache;

它通过持有 API 对象完成网络请求,可以定制自己的存储逻辑,控制请求方式的选择(长、短链接,JSON或protobuf)。
Model 应该对上层暴露一个非常简单的调用接口,因为假设一个 Model 对应一个 URL,其实每次请求只需要设置好参数,就可以调用合适的方法发起请求了。
由于我们不能预知请求何时结束,所以需要设置请求完成时的回调,这也需要作为 Model 的一个属性。

BaseItem

基类的 Item 主要是负责 property name 到 json path 的映设,以及 json 数据的解析。最核心的字典转模型实现如下:

- (void)parseData:(NSDictionary *)data {
    Class cls = [self class];
    while (cls != [KtBaseItem class]) {
        NSDictionary *propertyList = [[KtClassHelper sharedInstance] propertyList:cls];
        for (NSString *key in [propertyList allKeys]) {
            NSString *typeString = [propertyList objectForKey:key];
            NSString* path = [self.jsonDataMap objectForKey:key];
            id value = [data objectAtPath:path];
            [self setfieldName:key fieldClassName:typeString value:value];
        }
        cls = class_getSuperclass(cls);
    }
}

完整代码参考 Git 提交历史:SHA-1:77c6392

如何使用

在实际使用时,首先要创建子类的 Modle 和 Item。子类的 Model 应该持有 Item 对象,并且在网络请求回调时,将 API 中携带的 JSON 数据赋值给 Item 对象。
这个 JSON 转对象的过程在基类的 Item 中实现,子类的 Item 在创建时,需要指定属性名和 JSON 路径之间的对应关系。
对于上层来说,它需要生成一个 Model 对象,设置好它的路径以及回调,这个回调一般是网络请求返回时 VC 的操作,比如调用 reloadData 方法。这时候的 VC 可以确定,网络请求的数据就存在 Model 持有的 Item 对象中。
具体代码参考 Git 提交历史:SHA-1:8981e28

下拉刷新

很多应用的 UITableview 都具有下拉刷新和上拉加载的功能,在实现这个功能时,我们主要考虑两点:
1
隐藏底层的实现细节,对外暴露稳定易用的接口
2
Model 和 Item 如何实现
第一点已经是老生常谈,参考 SHA-1 61ba974 就可以看到如何实现一个简单的封装。
重点在于对于 Model 和 Item 的改造。

ListItem

这个 Item 没有什么别的作用,就是定义了一个属性 pageNumber,这是需要与服务端协商的。Model 将会根据这个属性这个属性判断有没有全部加载完。

// In .h
@interface KtBaseListItem : KtBaseItem
@property (nonatomic, assign) int pageNumber;
@end
// In .m
- (id)initWithData:(NSDictionary *)data {
    if (self = [super initWithData:data]) {
        self.pageNumber = [[NSString stringWithFormat:@"%@", [data objectForKey:@"page_number"]] intValue];
    }
    return self;
}

对于 Server 来说,如果每次都返回 page_number 无疑是非常低效的,因为每次参数都可能不同,计算总数据量是一项非常耗时的工作。因此在实际使用中,客户端可以和 Server 约定,返回的结果中带有 isHasNext 字段。通过这个字段,我们一样可以判断是否加载到最后一页。

ListModel

它持有一个 ListItem 对象, 对外暴露一组加载方法,并且定义了一个协议 KtBaseListModelProtocol,这个协议中的方法是请求结束后将要执行的方法。

@protocol KtBaseListModelProtocol <NSObject>
@required
- (void)refreshRequestDidSuccess;
- (void)loadRequestDidSuccess;
- (void)didLoadLastPage;
- (void)handleAfterRequestFinish; // 请求结束后的操作,刷新tableview或关闭动画等。
@optional
- (void)didLoadFirstPage;
@end
@interface KtBaseListModel : KtBaseModel
@property (nonatomic, strong) KtBaseListItem *listItem;
@property (nonatomic, weak) id<KtBaseListModelProtocol> delegate;
@property (nonatomic, assign) BOOL isRefresh; // 如果为是,表示刷新,否则为加载。
- (void)loadPage:(int)pageNumber;
- (void)loadNextPage;
- (void)loadPreviousPage;
@end

实际上,当 Server 端发生数据的增删时,只传 nextPage 这个参数是不能满足要求的。两次获取的页面并非完全没有交集,很有可能他们具有重复元素,所以 Model 还应该肩负起去重的任务。为了简化问题,这里就不完整实现了。

RefreshTableViewController

它实现了 ListMode 中定义的协议,提供了一些通用的方法,而具体的业务逻辑则由子类实现。

#pragma -mark KtBaseListModelProtocol
- (void)loadRequestDidSuccess {
    [self requestDidSuccess];
}
- (void)refreshRequestDidSuccess {
    [self.dataSource clearAllItems];
    [self requestDidSuccess];
}
- (void)handleAfterRequestFinish {
    [self.tableView stopRefreshingAnimation];
    [self.tableView reloadData];
}
- (void)didLoadLastPage {
    [self.tableView.mj_footer endRefreshingWithNoMoreData];
}
#pragma -mark KtTableViewDelegate
- (void)pullUpToRefreshAction {
    [self.listModel loadNextPage];
}
- (void)pullDownToRefreshAction {
    [self.listModel refresh];
}

实际使用

在一个 VC 中,它只需要继承 RefreshTableViewController,然后实现 requestDidSuccess 方法即可。下面展示一下 VC 的完整代码,它超乎寻常的简单:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createModel];
    // Do any additional setup after loading the view, typically from a nib.
}
- (void)createModel {
    self.listModel = [[KtMainTableModel alloc] initWithAddress:@"/mooclist.php"];
    self.listModel.delegate = self;
}
- (void)createDataSource {
    self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 这一步创建了数据源
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (void)requestDidSuccess {
    for (KtMainTableBookItem *book in ((KtMainTableModel *)self.listModel).tableViewItem.books) {
        KtTableViewBaseItem *item = [[KtTableViewBaseItem alloc] init];
        item.itemTitle = book.bookTitle;
        [self.dataSource appendItem:item];
    }
}

其他的判断,比如请求结束时关闭动画,最后一页提示没有更多数据,下拉刷新和上拉加载触发的方法等公共逻辑已经被父类实现了。
具体代码见 Git 提交历史:SHA-1:0555db2
写在结尾
网络请求的设计架构到此就全部结束了,它还有很多值的拓展的地方。还是那句老话,没有通用的架构,只有最适合业务的架构。
我的 Demo 为了方便演示和阅读,通常都是先实现底层的类和方法,然后再由上层调用。但实际上这种做法在实际开发中是不现实的。我们总是在发现大量冗余,无意义的代码后,才开始设计架构。
因此在我看来,真正的架构过程是当业务发生变更(通常是变复杂了)时,我们开始应该思考当前哪些操作是可以省略的(由父类或代理实现),最上层应该以何种方式调用底层的服务。一旦设计好了最上层的调用方式,就可以逐步向底层实现了。
由于本人水平也有限,本文的架构并不优秀,希望在深入理解设计模式,积累更多经验后,再与大家分享收获。

 

APP开发时间为什么难估算?七层架构与三维工时拆解 移动应用开发并非简单的编码过程,而是一个涵盖前端渲染、后端服务、数据存储、运维监控、安全合规、发布分发及团队协作的多维系统工程。其时间不确定性源于需求模糊性、技术债不可见性与协作熵增性三大本质矛盾。真实工期取决于技术选型(如Flutter跨平台权衡)、状态管理架构(Riverpod vs BLoC)、离线能力设计(缓存策略与一致性校验)等关键决策,更受制于老旧系统对接、GDPR合规适配、多端灰度发布等外部约束。本文以药店会员APP等真实案例,揭示从需求原子化拆解、三维工时校准(开发/协作/保障)到风险缓冲量 阅读详情

相关推荐

深入探索iOS UITableView及其选择功能

在iOS开发中,是一个高效且灵活的视图组件,用于展示滚动列表。它被广泛应用在各种应用中,以呈现有序的组织数据,如联系人列表、设置选项等。能够有效地管理大量数据,从而提供流畅的用户交互体验。除了标准的表格视图风格,iOS 开发者可以根据实际需求自定义表格视图的外观和行为。自定义可以涉及单元格的布局、交互以及整体的视觉风格。要实现自定义的表格视图,开发者可以继承类,重写其布局方法来自定义单元格的外观。如果需要改变表格的整体布局或动画,可以通过实现和的方法来定制。

weixin_42515392的博客 988

基于数据驱动模式的UITableView解耦方案

总述 传统的TableView是在一个又一个协议的驱动下构建的。一方面,每次我们都要重复的实现逻辑类似的协议,另一方面,业务逻辑不可避免的耦合到了numberOfRows,heightOfRow,cellForRow这些方法中,难以维护。 本文方案的核心思想是抽象封装UITableView的数据源,通过构造特定的数据源来驱动TableView的创建。 问题引入 场景模拟 假如有一个类似网易云音...

weixin_33991418的博客 446

手机如何高效辅助林业调查

手机如何高效辅助林业调查 很多林业工作者将栅格类型的地形图处理后导入平板电脑,在林地使用平板电脑加载小班矢量图,辅助林业调查。但是,如果没有地形图底图,这种方法可能就有难度,而且最重要的是,大部分林地工作者没有能力配置平板电脑。大凡林业基层工作者在林地调查时,除调查记录本等部分纸质材料外,都必然携带手机和水杯,此外其他设备已经成为累赘,尤其是炎热的夏季在杂灌丛生的林地进行调查作业。因此,使用手机...

xu780820的博客 3077

UI学习——UITableView

以上就是本篇文章的全部内容。

m0_73974920的博客 3818

CPRI原理及应用--帧结构

对于CPRI 控制字,协议将1个超帧(256基本帧)的控制数据划分为一个“子通道”,将子通道内超帧内的控制字进行行列划分,分为4列X64行,每64个基本帧组成1列。如下图所示。每个Xs子通道内Ns的定义基本相同,在做控制字设计时,主要已Xs和Ns和X.Y.Z开展控制字映射。

u014035968的博客 2325

动态规划与状态压缩:从TSP问题理解算法优化核心思想

动态规划是一种通过将复杂问题分解为重叠子问题并存储中间结果来避免重复计算的核心算法思想,广泛应用于路径优化、资源分配和序列决策等场景。其核心原理在于最优子结构和无后效性,通过状态设计和状态转移方程,将指数级复杂度问题转化为多项式时间可解。在工程实践中,状态压缩技术常被用于高效表示集合状态,例如使用二进制位运算来操作城市访问集合,从而大幅提升算法效率。本文以经典的旅行商问题(TSP)为例,深入剖析如何运用动态规划与状态压缩技术,将暴力穷举的指数级复杂度优化为可管理的计算量,为解决组合优化问题提供了重要的方法论

weixin_32075843的博客 365

UITableView 使用详解

UITableview

lmyuanhang的专栏 1866

达梦数据库与sharding-jdbc的深度适配实践

本文详细介绍了达梦数据库与sharding-jdbc的深度适配实践,包括驱动与依赖管理、核心适配实现、注册与配置等关键步骤。通过扩展shardingSphere的方式,成功实现了达梦数据库的分库分表功能,并提供了常见问题解决方案和性能优化建议,助力企业级应用高效处理海量数据。

weixin_30300225的博客 623

UITableView学习笔记

当tableView进入编辑模式以后,cell上面显示的delete还是insert除了跟cell的editStyle有关,还与 tableView的delegate的tableView:editingStyleForRowAtIndexPath:方法的返回值有关(在这里唠叨一句,其实delegate提供了很多改变cell属性的机会,如非必要,还是不要去实现这些方法,因为执行这些方法也造成一定的开销)。第二个方法的主要作用是根据用户在indexList中点击的位置,返回相应的section的index值。

精选资讯文章 1487

基于 FastAPI + LangGraph + LLM 大语言模型的通用 Agent 多智能体系统架构设计与开发实战、产业应用

FastAPI是一个现代、快速(高性能)的Python Web框架,专门用于构建API。它由Sebastián Ramírez(@tiangolo)创建,基于Starlette(用于Web部分)和Pydantic(用于数据部分)构建。性能优先:FastAPI是当前最快的Python Web框架之一,性能与NodeJS和Go相当。这得益于其基于Starlette的异步架构,能够充分利用Python 3.6+的async/await特性。开发效率。

AI天才研究院 230

用扩展点为 Fiori Elements 添加自定义动作:从原理到落地的完整实践

本文介绍了如何在基于OData V4的Fiori Elements应用中通过Extension Points添加自定义Action按钮。主要内容包括: 扩展点动作与标准注解动作的区别:扩展点用于非标准前端流程、组合操作等场景,需通过extensionAPI与框架交互。 扩展点动作的插入位置:可放置在List Report表格工具栏、Object Page页眉/页脚/Section工具栏等位置,并说明了相应的manifest.json配置节点。 配置模型与处理函数契约:详细说明了press、visible/e

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 248

iOS 封装tableview,实用例子汇总

文章目录AppSettingTableView.hAppSettingTableView.mAPPSettingTableViewCell使用demo 大家不同的程序的时候可能经常遇到类似的tableview。很简单,但是又很繁琐,这时候封装一个自己常用的tableview也不失为一个良方,充分的节约自己的时间。 AppSettingTableView.h // // AppSettingTab...

煮酒Shae 1050

2ED2410-EM的使用

2ED2410-EM的使用

让你爱上电路设计 971

UITableView的封装

一说到UITableView这个控件,估计大家都十分熟悉,这是IOS开发当中最常用的控件之一。每次用到UITableView这个控件时都要写好多代码。写起来也比较头疼。代码多起来也不方便维护。减少代码量就是减少bug量的原则,所以对UITableView进行了部分封装。并用 try catch进行异常捕获。由于水平有限,封装的不是很好。希望大家能够批评指正。具体代码如下 YWDataSour

伟子的博客 2029

华为OD机试 - 最快到达医院的方法(Python/JS/C/C++ 2024 E卷 100分)

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、发现新题目,随时更新,全天优快云在线答疑。

学Java,找哪吒 2169

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

iOS—简单封装UITableView使用1

如果项目用到很多的UITableView的时候,如果我们每个页面都去新构建UITableView,那么的我们项目的代码量会很大,特别是网络请求下来的数据,重复的创建,解析数据。 下面我谈谈个人在项目使用UITableView的时候,我会简单的封装一个UITableView,使项目中只有一个UITableView,减少代码量,下面我就来说说的想法. 1.首先我先在没有网络请求的情况下创建自定义U

杜文的博客 1470

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

UITableView的懒人封装(几句代码搞定网络加载,上下拉刷新)

代码写的多了有时你会发现,常见的tableview无非是加上下刷新,加载接口,自定义Cell,传入数据模型,千篇一律,但你又不得不去写,干脆就去ctrl+c和ctrl+v,那么能不封装一个tableview,一个初始化就搞定呢,答案是可以的。在写之前你必须了解这个封装tableView的使用限制 你的自定义Cell不能有复杂的交互,比如长按手势,accessoryView带Button等,当然,你可

championf的博客 2508

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

iOS—简单封装UITableView使用2

前几天我说过简单的UITableView 封装,是基于本地数据的情况下,今天我来说说网络请求下,我们封装tableView,其中我用的AFNetWorking 3.0 网络请求 跟新版的MJ 刷新。其中数据的Model 跟tableViewCell  我们代码不变还是用 上次说的文章代码。    首先说说我的思路,首先我们定义一个类MyTableView 继承UIView;给他一个初始化方法(包

杜文的博客 2032

033-UITableViewHeaderFooterView-iOS笔记

学习目标 1.【掌握】静态单元格应用 2.【掌握】动态原型应用 3.【理解】UITableViewHeaderFooterView 一、静态单元格应用 这个案例的目的是了解静态单元格的使用场合及操作方式,在一些情况下使用静态单元格会更加方便和快捷。我们需要了解静态单元格的一些特性及设置方式。案例效果图如下: 静态单元格只支持UITableViewController,所以先将之...

weixin_33906657的博客 431

UITableView的简单封装

初学UITableView,接触了简单的几个项目,也注意到现在大部分项目都在频繁使用这个控件,其重要程度不言而喻。     UITableView中每个数据往往通过plist来获取,并且通过UITableViewDelegate和UITableViewDataSource这两个协议来完成数据的加载和每一行的样式设计。     其中需要实现的两个必要的协议分别是: - tableView:nu

ChyoGenntoo的博客 761

iOS开发 UITableView的创建和使用

1.定义UITableView的实例,并让当前视图遵守UITableView的两个协议。RecordTableViewCell是自定义的单元格cell。2.初始化tableView,并设置tableView的属性。3.实现协议中必须要实现的方法。4.一些其他经常使用的方法。

weixin_45958328的博客 798

iOS UITableView控件

1. UITableView UITableView继承自UIScrollView,可以用来展示一组或多组内容样式相似的数据。UITableView可以说是iOS开发中最重要的控件之一。 2. UITableView样式 UITableView的两种样式UITableViewStylePlain和UITableViewStyleGrouped。 UITableViewStylePlain样式,当一个section的rows有一部分可见时,section的header和footer浮动在内容顶部。 UITab

假装你是大灰狼的专栏 2816

UITableView的使用方法

1. 首先是先创建UITableView /* UITableViewStylePlain UITableViewStyleGrouped 分组 */ self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, [UIScreen mainScreen].bounds.size.width, [UIS

yang_6799的博客 613

【iOS】UITableView性能优化

最近基本将我的第二个项目完结,之后会记录一些源码的学习以及优化项目的一些方法,而在iOS中是最为常用的一种控件,今天我们就从性能优化开始讲起UITableView的性能优化涉及到了许多层面,下到底层的Layer属性,上到第三方库SDWebImage与RunLoop,这些东西的实现都十分巧妙,还有很长一段路需要学习这里笔者写一下Tableview 性能优化方法总览实现Tableview的懒加载以及cell的复用。

weixin_72437555的博客 3064

将UITableView封装到极致

转载自 http://www.jianshu.com/p/bf4ad5ecde18 介绍 “极致”这种情怀问题,手上做不到没关系,嘴上是肯定要做到的。只要不是能力太打脸,坚持一下下倒是也模棱两可。 本文参考了更轻量的 View Controllers ,对table用到的两个个协议,进行了不同思路的封装。这段时间辞职避暑,时间大大的有,整理下这一年的经验,分享给大家。 代码在这gith

大官仁的专栏 961

UITableView 总结 2023

【代码】UITableView 总结 2023。

weixin_42974850的博客 190
上一篇: 其实Servlet很简单,掌握了编程思想,解决问题才是硬道理
下一篇: JavaWeb之JSP零碎整理
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值