iOS开发基础6-懒加载、Plist、字典转模型与自定义 View
iOS 开发核心模式:懒加载、Plist、字典转模型与自定义 View
在 iOS 开发中,掌握高效的开发模式能显著提升代码的可读性与可维护性。本文详细介绍懒加载、Plist 文件操作、字典转模型和自定义 View 四个核心知识点,并分析其底层逻辑。
一、懒加载(Lazy Loading)
懒加载是一种延迟初始化策略:对象在第一次被访问时才创建,而非在控制器初始化或 viewDidLoad 中提前创建。这样可以节省内存、加快启动速度,尤其适用于创建成本较高或不一定会用到的资源。
标准实现:重写 getter 方法
懒加载的核心是重写属性的 getter 方法,在方法内部判断实例变量是否为 nil,为 nil 时才创建。
Objective-C
// 注意:getter 内部必须使用实例变量 _shops,不能用 self.shops(否则递归调用)
- (NSArray *)shops {
if (_shops == nil) {
NSLog(@"创建商品数组");
_shops = @[
@{@"name": @"单肩包", @"icon": @"danjianbao"},
@{@"name": @"链条包", @"icon": @"liantiaobao"},
@{@"name": @"钱包", @"icon": @"qianbao"},
@{@"name": @"手提包", @"icon": @"shoutibao"}
];
}
return _shops;
}
Swift
Swift 提供 lazy 关键字,原生支持懒加载,语法更简洁:
lazy var shops: [[String: String]] = {
print("创建商品数组")
return [
["name": "单肩包", "icon": "danjianbao"],
["name": "链条包", "icon": "liantiaobao"],
["name": "钱包", "icon": "qianbao"],
["name": "手提包", "icon": "shoutibao"]
]
}()
关键点:
- OC 中 getter 内部必须访问实例变量
_shops,不能用self.shops,否则会递归调用 getter 导致死循环。- Swift 的
lazy属性是线程不安全的(多线程同时首次访问可能创建多次),如需线程安全应加锁或使用dispatch_once模式。- 懒加载只在第一次访问时执行初始化,后续访问直接返回已创建的对象。
底层逻辑
懒加载的本质是惰性求值:将对象的创建时机从"类初始化时"推迟到"首次使用时"。如果某个属性在整个生命周期中从未被访问,它就永远不会被创建,从而避免了不必要的内存分配和计算开销。
二、Plist 文件操作
Plist(Property List)是 iOS 中常用的轻量级结构化数据存储格式,以 XML 或二进制形式存储键值对,适合存储少量配置数据。
可存储的数据类型
Plist 只能存储以下基本类型(及其容器):
NSString、NSNumber、NSDate、NSDataNSArray、NSDictionary(容器内也必须是上述类型)
自定义对象不能直接写入 Plist,需先归档为
NSData或转换为字典。
从 Plist 读取数据
Bundle 中的 Plist 文件是只读的,作为应用资源随包发布。
Objective-C
NSString *path = [[NSBundle mainBundle] pathForResource:@"shops" ofType:@"plist"];
NSArray *shops = [NSArray arrayWithContentsOfFile:path];
Swift
let path = Bundle.main.path(forResource: "shops", ofType: "plist")!
let shops = NSArray(contentsOfFile: path) as! [[String: String]]
向 Plist 写入数据
Bundle 中的文件不可写入,要持久化数据必须写入应用沙盒(通常是 Documents 目录)。
Objective-C
// 获取沙盒 Documents 目录路径
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [docPath stringByAppendingPathComponent:@"shops.plist"];
// 写入(仅当数组/字典中的对象全为 Plist 支持的类型时才会成功)
BOOL success = [shops writeToFile:filePath atomically:YES];
Swift
let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let filePath = (docPath as NSString).appendingPathComponent("shops.plist")
let success = (shops as NSArray).write(toFile: filePath, atomically: true)
atomically:YES表示先写入临时文件,写入成功后再替换目标文件,避免写入过程中崩溃导致文件损坏。
沙盒目录结构
| 目录 | 用途 | iTunes 备份 |
|---|---|---|
Documents/ |
用户生成的重要数据 | 是 |
Library/Caches/ |
缓存文件,可重新下载 | 否 |
Library/Preferences/ |
NSUserDefaults 存储 |
是 |
tmp/ |
临时文件,系统可能清理 | 否 |
注意事项
- 文件命名:不要将自定义 Plist 命名为
Info.plist(精确匹配,大小写不敏感),因为这是系统保留的应用配置文件。包含 "info" 单词的其他名称(如shopInfo.plist)是完全合法的。 - 路径动态获取:永远使用
NSSearchPathForDirectoriesInDomains或FileManager获取沙盒路径,禁止硬编码绝对路径。 - Bundle 只读:
mainBundle中的资源文件不可写入,如需修改必须先复制到沙盒。
三、字典转模型(Dictionary to Model)
从服务器或 Plist 获取的数据通常是字典(NSDictionary),直接操作字典存在诸多问题:
- 键名拼写错误无编译报错:
dict[@"nme"]不会报错,只会返回nil,bug 难以排查。 - 无智能提示:Xcode 不会提示字典有哪些键。
- 类型不安全:取出的值是
id类型,需要手动强转。 - 维护困难:键名散落在代码各处,修改时容易遗漏。
将字典转换为模型对象(Model)可以解决上述所有问题。
模型类定义
Objective-C
// NJShop.h
@interface NJShop : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
/// 类方法工厂方法:从字典创建模型
+ (instancetype)shopWithDict:(NSDictionary *)dict;
/// 实例初始化方法
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
// NJShop.m
@implementation NJShop
+ (instancetype)shopWithDict:(NSDictionary *)dict {
return [[self alloc] initWithDict:dict];
}
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [super init]) {
self.name = dict[@"name"];
self.icon = dict[@"icon"];
}
return self;
}
@end
Swift
struct Shop {
let name: String
let icon: String
init(dict: [String: String]) {
name = dict["name"] ?? ""
icon = dict["icon"] ?? ""
}
}
Swift 中推荐使用
struct定义模型(值类型,性能更好,无线程安全问题);若需要继承NSObject或引用语义则使用class。
配合懒加载批量转换
Objective-C
- (NSMutableArray *)shops {
if (_shops == nil) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"shops" ofType:@"plist"];
NSArray *tempArr = [NSArray arrayWithContentsOfFile:path];
_shops = [NSMutableArray array];
for (NSDictionary *dict in tempArr) {
NJShop *shop = [NJShop shopWithDict:dict];
[_shops addObject:shop];
}
}
return _shops;
}
Swift
lazy var shops: [Shop] = {
let path = Bundle.main.path(forResource: "shops", ofType: "plist")!
let dictArray = NSArray(contentsOfFile: path) as! [[String: String]]
return dictArray.map { Shop(dict: $0) }
}()
Swift 现代方式:Codable
Swift 4+ 引入的 Codable 协议可以自动完成字典/JSON 与模型的转换,无需手动写映射代码:
struct Shop: Codable {
let name: String
let icon: String
}
// 从 Data 解码
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
let shops = try! PropertyListDecoder().decode([Shop].self, from: data)
Objective-C 项目中也可以使用
YYModel、MJExtension等第三方库实现自动字典转模型,减少手动映射代码。
四、自定义 View
当系统提供的 UI 控件无法满足需求时,通过继承 UIView 封装自定义视图,可以将视图创建、布局和数据绑定逻辑内聚,提高复用性。
完整实现
Objective-C
// NJShopView.h
@class NJShop;
@interface NJShopView : UIView
@property (nonatomic, strong) NJShop *shop;
@end
// NJShopView.m
#import "NJShopView.h"
#import "NJShop.h"
@interface NJShopView ()
@property (nonatomic, strong) UIImageView *iconView;
@property (nonatomic, strong) UILabel *nameLabel;
@end
@implementation NJShopView
#pragma mark - 初始化
// 代码创建时调用
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupSubviews];
}
return self;
}
// Storyboard/XIB 创建时调用
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self setupSubviews];
}
return self;
}
- (void)setupSubviews {
UIImageView *iconView = [[UIImageView alloc] init];
iconView.contentMode = UIViewContentModeScaleAspectFit;
[self addSubview:iconView];
self.iconView = iconView;
UILabel *nameLabel = [[UILabel alloc] init];
nameLabel.textAlignment = NSTextAlignmentCenter;
nameLabel.font = [UIFont systemFontOfSize:14];
[self addSubview:nameLabel];
self.nameLabel = nameLabel;
}
#pragma mark - 布局
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
// 图片在上,占正方形区域
CGFloat iconSize = width;
self.iconView.frame = CGRectMake(0, 0, iconSize, iconSize);
// 文字在下,占剩余高度
self.nameLabel.frame = CGRectMake(0, iconSize, width, height - iconSize);
}
#pragma mark - 数据绑定
- (void)setShop:(NJShop *)shop {
_shop = shop;
self.iconView.image = [UIImage imageNamed:shop.icon];
self.nameLabel.text = shop.name;
}
@end
Swift
class ShopView: UIView {
private lazy var iconView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFit
return iv
}()
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = .systemFont(ofSize: 14)
return label
}()
var shop: Shop? {
didSet {
iconView.image = UIImage(named: shop?.icon ?? "")
nameLabel.text = shop?.name
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupSubviews()
}
private func setupSubviews() {
addSubview(iconView)
addSubview(nameLabel)
}
override func layoutSubviews() {
super.layoutSubviews()
let width = bounds.width
let height = bounds.height
let iconSize = width
iconView.frame = CGRect(x: 0, y: 0, width: iconSize, height: iconSize)
nameLabel.frame = CGRect(x: 0, y: iconSize, width: width, height: height - iconSize)
}
}
最佳实践
-
重写两个初始化方法:
initWithFrame:— 代码创建视图时调用。initWithCoder:— Storyboard/XIB 创建时调用。- 两个方法都调用公共的
setup方法,避免代码重复。只重写init是不够的,因为UIView的指定初始化方法是initWithFrame:。
-
子视图引用用 strong:子视图通过
addSubview:添加后,父视图已经强引用子视图。自定义 View 对自己的子视图用strong或weak均可,不会造成循环引用。strong更安全(即使子视图被意外移除,引用仍然有效),weak更轻量。不存在"必须用 weak 防止内存泄漏"的说法——父视图强引用子视图是正常的持有关系,不会形成循环引用。 -
不在 init 中设置 frame:初始化时视图的尺寸尚未确定,子视图的 frame 应在
layoutSubviews中计算。 -
重写
layoutSubviews:在此方法中布局所有子视图。当视图的bounds变化时系统会自动调用,也可通过setNeedsLayout标记为需要重新布局。 -
数据绑定重写 setter:重写模型属性的 setter 方法(Swift 中用
didSet),在数据变化时自动更新子视图内容。
底层逻辑
- 生命周期:
initWithFrame:/initWithCoder:→didMoveToSuperview→layoutSubviews(多次)→drawRect:(如需自定义绘制)。 - 布局机制:
layoutSubviews在视图加入父视图、bounds变化、调用setNeedsLayout时触发,是手动计算子视图 frame 的核心入口。 - 渲染策略:自定义 View 若不重写
drawRect:,系统不会为其创建绘制上下文,性能更好。仅在需要自定义绘制(如画图、渐变)时才重写drawRect:。
五、总结
| 模式 | 核心思想 | 适用场景 |
|---|---|---|
| 懒加载 | 首次访问时才创建对象 | 创建成本高、不一定使用的资源 |
| Plist | 轻量级结构化数据存储 | 少量配置数据、静态列表 |
| 字典转模型 | 将无类型字典转为有属性的对象 | 服务器数据、Plist 数据的业务化 |
| 自定义 View | 封装视图创建、布局、数据绑定 | 可复用的复合 UI 组件 |
- 懒加载的标准写法是重写 getter(OC)或使用
lazy(Swift),getter 内部必须访问实例变量而非属性。 - Plist 写入必须到沙盒目录,Bundle 中的资源只读;可存储类型限于
NSString、NSNumber、NSDate、NSData、NSArray、NSDictionary。 - 字典转模型提供了编译时检查和智能提示,Swift 中推荐使用
Codable自动解码。 - 自定义 View 必须同时重写
initWithFrame:和initWithCoder:,子视图布局放在layoutSubviews中,数据绑定通过重写 setter 实现。

浙公网安备 33010602011771号