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 只能存储以下基本类型(及其容器):

  • NSStringNSNumberNSDateNSData
  • NSArrayNSDictionary(容器内也必须是上述类型)

自定义对象不能直接写入 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/ 临时文件,系统可能清理

注意事项

  1. 文件命名:不要将自定义 Plist 命名为 Info.plist(精确匹配,大小写不敏感),因为这是系统保留的应用配置文件。包含 "info" 单词的其他名称(如 shopInfo.plist)是完全合法的。
  2. 路径动态获取:永远使用 NSSearchPathForDirectoriesInDomainsFileManager 获取沙盒路径,禁止硬编码绝对路径。
  3. 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 项目中也可以使用 YYModelMJExtension 等第三方库实现自动字典转模型,减少手动映射代码。


四、自定义 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)
    }
}

最佳实践

  1. 重写两个初始化方法

    • initWithFrame: — 代码创建视图时调用。
    • initWithCoder: — Storyboard/XIB 创建时调用。
    • 两个方法都调用公共的 setup 方法,避免代码重复。只重写 init 是不够的,因为 UIView 的指定初始化方法是 initWithFrame:
  2. 子视图引用用 strong:子视图通过 addSubview: 添加后,父视图已经强引用子视图。自定义 View 对自己的子视图用 strongweak 均可,不会造成循环引用。strong 更安全(即使子视图被意外移除,引用仍然有效),weak 更轻量。不存在"必须用 weak 防止内存泄漏"的说法——父视图强引用子视图是正常的持有关系,不会形成循环引用。

  3. 不在 init 中设置 frame:初始化时视图的尺寸尚未确定,子视图的 frame 应在 layoutSubviews 中计算。

  4. 重写 layoutSubviews:在此方法中布局所有子视图。当视图的 bounds 变化时系统会自动调用,也可通过 setNeedsLayout 标记为需要重新布局。

  5. 数据绑定重写 setter:重写模型属性的 setter 方法(Swift 中用 didSet),在数据变化时自动更新子视图内容。

底层逻辑

  • 生命周期initWithFrame: / initWithCoder:didMoveToSuperviewlayoutSubviews(多次)→ drawRect:(如需自定义绘制)。
  • 布局机制layoutSubviews 在视图加入父视图、bounds 变化、调用 setNeedsLayout 时触发,是手动计算子视图 frame 的核心入口。
  • 渲染策略:自定义 View 若不重写 drawRect:,系统不会为其创建绘制上下文,性能更好。仅在需要自定义绘制(如画图、渐变)时才重写 drawRect:

五、总结

模式 核心思想 适用场景
懒加载 首次访问时才创建对象 创建成本高、不一定使用的资源
Plist 轻量级结构化数据存储 少量配置数据、静态列表
字典转模型 将无类型字典转为有属性的对象 服务器数据、Plist 数据的业务化
自定义 View 封装视图创建、布局、数据绑定 可复用的复合 UI 组件
  • 懒加载的标准写法是重写 getter(OC)或使用 lazy(Swift),getter 内部必须访问实例变量而非属性。
  • Plist 写入必须到沙盒目录,Bundle 中的资源只读;可存储类型限于 NSStringNSNumberNSDateNSDataNSArrayNSDictionary
  • 字典转模型提供了编译时检查智能提示,Swift 中推荐使用 Codable 自动解码。
  • 自定义 View 必须同时重写 initWithFrame:initWithCoder:,子视图布局放在 layoutSubviews 中,数据绑定通过重写 setter 实现。
posted @ 2015-07-15 13:10  Mr.陳  阅读(647)  评论(0)    收藏  举报