effective-Objective-C 第四章阅读笔记

文章目录

协议和分类

OC不支持多重继承,所以我们将某个类应该实习爱你的一系列方法定义在协议中,最常见的是委托模式。
我们可以利用分类机制,无需继承子类即可直接为当前类添加方法,这个性质建立在OC高度动态的运行期系统。

通过委托与数据源协议进行对象间通信

对象之间相互通信的方式很多,广泛使用的是委托模式实现对象间的通信,这个模式的主旨是:定义一套接口,某对象如果想接受另一个对象的委托,就需要遵守这个接口,设置代理对象。在调用的时候可以回传一些信息。

这个模式可将数据与业务逻辑解耦,我们看下面这个例子:

objc 复制代码
// 数据源:负责"给数据"
@protocol MyListViewDataSource <NSObject>
- (NSInteger)numberOfItems;
- (NSString *)itemAtIndex:(NSInteger)index;
@end

// 委托:负责"处理事件"
@protocol MyListViewDelegate <NSObject>
- (void)didSelectItemAtIndex:(NSInteger)index;
@end

视图对象只负责显示

objc 复制代码
@interface MyListView : NSObject
@property (nonatomic, weak) id<MyListViewDataSource> dataSource;
@property (nonatomic, weak) id<MyListViewDelegate> delegate;

- (void)reloadData;
- (void)simulateUserTap:(NSInteger)index; // 模拟点击
@end
objc 复制代码
@implementation MyListView

- (void)reloadData {
    NSInteger count = [self.dataSource numberOfItems];
    for (NSInteger i = 0; i < count; i++) {
        NSString *text = [self.dataSource itemAtIndex:i];
        NSLog(@"显示数据:%@", text);
    }
}

- (void)simulateUserTap:(NSInteger)index {
    // 用户点击事件 → 回传给 delegate
    [self.delegate didSelectItemAtIndex:index];
}

@end

控制器对象,负责数据流动

objc 复制代码
@interface MyController : NSObject <MyListViewDataSource, MyListViewDelegate>
@property (nonatomic, strong) NSArray *data;
@end
objc 复制代码
@implementation MyController

- (instancetype)init {
    if (self = [super init]) {
        _data = @[@"苹果", @"香蕉", @"橘子"];
    }
    return self;
}

#pragma mark - DataSource

- (NSInteger)numberOfItems {
    return self.data.count;
}

- (NSString *)itemAtIndex:(NSInteger)index {
    return self.data[index];
}

#pragma mark - Delegate

- (void)didSelectItemAtIndex:(NSInteger)index {
    NSLog(@"用户点击了:%@", self.data[index]);
    // 👉 业务逻辑在这里处理(不是在 view 里)
}

@end

上述例子中,视图只负责显示数据所需的逻辑代码,而不决定显示何种数据以及数据之间如何进行交互等问题。视图对象的属性中包含负责数据与事件处理的对象,这两种对象就称之为数据源(dataSource)于委托(delegate)。

整个Cocoa系统框架都是这么做的,我们这么使用协议可以较好的与系统框架融合

我们在看一个例子:

一个网络获取数据的类,也许要从远程服务器的某个资源中获取数据,服务器可能需要长时间的响应,我们可以通过使用委托模式,避免数据获取的过程中阻塞应用程序。在获取完成数据之后回调委托对象。

在定义代理时,属性需要定义成weak,而非strong,因为两者之间不是拥有关系,但是通常情况下,扮演delegate的对象也要持有本对象。用完后才会释放,如果声明为strong,就会引入保留环。

协议中的方法一般都是可选的,所以一般使用@optional关键字来标注其大部分或全部的方法

如果要在委托对象上调用可选对象,那么必须提前使用了行信息查询方法判断委托对象是否能响应相关选择子。如下:

objc 复制代码
if ([_delegate responseToselector:@selector(networkFetcher:didReceiveData:)]) {
  [_delegate networkFetcher:self didReceiveData:data];
}

重定向 = 服务器告诉客户端:这个资源不在这了,去另一个地址拿

我们也可以定义一套接口,令某类经由该接口获取其所需要的数据,称为数据源模式,在此模式中,在此模式中信息从数据源流向类,而在常规的委托模式中,信息从类流向委托者。

在委托模式与数据源模式中,如果协议的方法是可选的,那么就会出现大批判断是否能响应选择子的方法,我们通常将委托对象能否响应某个协议方法的信息缓存起来以优化程序效率。

将方法响应能力缓存起来的最佳途径是使用"位段(bitfield)"数据类型,我们可以将结构体中某个字段所占用的二进制位个数设置为特定的值。如下:

objc 复制代码
struct data {
  unsigned int fieldA : 8;//0-255
  unsigned int fieldB : 4;//0-15
  unsigned int fieldC : 2;//0-3
  unsigned int fieldD : 1;//0-1
};

位段是C语言提供的结构体成员声明方式,允许开发者指定该成员在内存中占用的二进制位数,而不是整个标准类型。

使用位段的原因:

  • 节省内存:如果结构体中有许多的bool值或者小范围数字,使用标准int会浪费空间。位段能将多个布尔值或者小数字打包到同一个字节中
  • 提高缓存效率:数据紧凑,可以减少CPU缓存行占用,提高访问速度。特别适合存储状态标志、协议实现中的开关位等
  • 语义明确:代码中直接声明占用的位数,表示类数据的合法范围

注意事项:

  • 系统区别
  • 位段不是独立变量,不能用&fieldA取地址
  • 在某些CPU上访问位段可能需要额外的掩码和位移操作,太多位段可能会降低性能。

详细示例:

objc 复制代码
@interface EOCNetworkFetcher() {
  struct {
    unsigned int didReceiveData : 1;
    unsigned int didFailWithError : 1;
    unsigned int didUpdateProgressTo : 1;
  } _delegateFlags;
}
@end

上述代码通过分类新增实例变量,其中含有三个位段

objc 复制代码
_delegateFlags.didReceiveData = 1;
if (_delegateFlags.didReceiveData) {
  //
}

上述代码讲述存取过程

objc 复制代码
- (void)setDelegate:(id<EOCNetworkFetcher>)delegate {
    _delegate = delegate;

    _delegateFlags.didReceiveData =
        [_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];

    _delegateFlags.didFailWithError =
        [_delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];

    _delegateFlags.didUpdateProgressTo =
        [_delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}

上述代码实现缓存功能所用的代码,写在delegate的设置方法中就不用检测委托对象是否能响应特定的选择子。

将类的实现代码分散到便于管理的数个分类之中

我们可以按照逻辑将类中的代码按照逻辑划分入几个分区中,有利于开发与调试

在不修改原类、不继承自类的前提下为类横向增加方法

我们了解一下NSURLRequest类的设计思想:

是OC中的一个请求对象,用于从URL获取数据(HTTP请求、FTP请求以及其他协议请求),是一个通用的请求抽象类
HTTP 专属:

  • HTTP Method
  • HTTP Header
  • Body
  • Status Code

FTP 专属:

  • 登录
  • 目录操作
  • 文件传输模式

通用 URL:

  • URL
  • 超时
  • 缓存策略
  • 网络策略

拿HTTP举例,其只是URL协议族的一种实现,其需要额外信息在通用URL层中属于是污染概念。我们不便于从NSURLRequest中继承子类实现HTTP协议的特殊需求,因为该类包裹的一系列操作CFULRequest数据结构所需要的C函数

分类名会出现在调试器的调用栈里(程序内部会给方法起完整的身份证名字)

如下:

objc 复制代码
- [EOCPerson (Friendship) addFriend:]

当程序崩溃或者打断点时,调试器内部会显示调用栈,如下:

objc 复制代码
frame #2:
- [EOCPerson (Friendship) addFriend:]

一眼就可以定位功能模块

我们应该将视为私有的方法归入名叫Private的分类中以隐藏实现细节

总是为第三方类的分类名称加前缀

分类机制通常用于向无源码既有类中新增功能。将分类方法加入类中这一操作是在运行期系统加载分类时完成的,运行期系统会将分类中所实现的每个方法都加入类的方法列表中,注意分类会覆盖本类中同名方法的实现。通过加前缀尽量避免。

勿在分类中声明属性

分类中虽然可以声明属性,但是还是要尽量避免,除了cass-continuation分类之外,其他分类都无法向类中新增实例变量,他们无法合成属性的实例变量

可以声明为@dynamic,使用消息转发机制在运行期拦截方法调用,提供实现,虽然可以但不建议。

关联对象使用:

objc 复制代码
#import <objc/runtime.h>

static const char *kFriendsPropertyKey = "kFriendsPropertyKey";

@implementation EOCPerson (Friendship)

- (NSArray *)friends {
    return objc_getAssociatedObject(self, kFriendsPropertyKey);
}

- (void)setFriends:(NSArray *)friends {
    objc_setAssociatedObject(self,
                             kFriendsPropertyKey,
                             friends,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

这样还需要注意属性的内存管理语义

在分类中,可以定义存起方法,但是尽量不要定义属性

使用"class-continuation分类"隐藏实现细节

objc 复制代码
@interface EOCPerson()
  //
@end

.mm拓展名表示编译器应该将此文件按照Objective- C++来编译,否则无法引入SomeCppClass.h。

我们通常不直接访问实例变量,而是通过设置访问方法来做,因为这样可以触发键值观测,其他对象有可能在监听此事件

通过协议提供匿名对象

协议定义了一系列方法,遵从此协议的对象应该实现它们,我们可以使用协议把自己所写的API 中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。这样的话,想要隐藏的类名就不会出现在API 之中了。若是接又背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法------因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。

objc 复制代码
@property (nonatomic, weak) id<EOCDelegate> delegate;

该类的属性类型是id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性,只需要遵守EOCDelegate协议就行。系统会在运行期查询对象所属的类型。

我们可以看一个例子:

objc 复制代码
@protocol EOCDatabaseConnection <NSObject>
- (void)connect;
- (void)disconnect;
- (BOOL)isConnected;
- (NSArray *)performQuery:(NSString *)query;
@end

上述是一个协议接口,规定了所有数据库连接对象必须遵守的方法

在需要返回数据库连接的类中,我们不返回具体的类,而是返回一个遵循该协议的对象。

objc 复制代码
@interface EOCDatabaseManager : NSObject
+ (instancetype)sharedInstance;
- (id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString *)identifier;
@end

方法中connectionWithIdentifier: 返回的类型是 id<EOCDatabaseConnection>,这表明返回的对象必须遵循 EOCDatabaseConnection 协议,但调用者不需要知道它具体是什么类。

如果一个类实现了协议中的方法:

objc 复制代码
@interface MyDatabaseConnection : NSObject <EOCDatabaseConnection>
@property (nonatomic, assign) BOOL connected;
@end

@implementation MyDatabaseConnection

- (void)connect {
    NSLog(@"MyDatabaseConnection: 正在连接数据库...");
    self.connected = YES;
}

- (void)disconnect {
    NSLog(@"MyDatabaseConnection: 断开数据库连接...");
    self.connected = NO;
}

- (BOOL)isConnected {
    return self.connected;
}

- (NSArray *)performQuery:(NSString *)query {
    NSLog(@"MyDatabaseConnection: 执行查询: %@", query);
    // 这里只返回模拟的数据
    return @[@"结果1", @"结果2", @"结果3"];
}

@end

在数据库方法实现中,我们可以根据情况返回合适的连接对象:

objc 复制代码
@implementation EOCDatabaseManager

+ (instancetype)sharedInstance {
    static EOCDatabaseManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[EOCDatabaseManager alloc] init];
    });
    return instance;
}

- (id<EOCDatabaseConnection>)connectionWithIdentifier:(NSString *)identifier {
    // 根据 identifier 可以决定返回哪种具体实现
    MyDatabaseConnection *connection = [[MyDatabaseConnection alloc] init];
    [connection connect]; // 自动建立连接
    return connection;
}

@end

调用者只需要依赖协议接口就可以实现具体操作:

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        EOCDatabaseManager *dbManager = [EOCDatabaseManager sharedInstance];
        id<EOCDatabaseConnection> connection = [dbManager connectionWithIdentifier:@"MainDB"];
        
        if ([connection isConnected]) {
            NSLog(@"数据库已连接");
            NSArray *results = [connection performQuery:@"SELECT * FROM users"];
            NSLog(@"查询结果: %@", results);
            [connection disconnect];
        } else {
            NSLog(@"数据库连接失败");
        }
    }
    return 0;
}
相关推荐
四谎真好看2 小时前
SSM学习笔记(SpringMVC篇 Day01)
笔记·学习·学习笔记·ssm
嵌入式×边缘AI:打怪升级日志2 小时前
ARM Cortex-M 单片机启动流程与向量表深度解析(保姆级复习笔记)
arm开发·笔记·单片机
cqbzcsq2 小时前
MC Forge1.20.1 mod开发学习笔记(个人向)
笔记·学习·mod·mc·forge
蒸蒸yyyyzwd3 小时前
cpp学习笔记
笔记·学习
浅念-3 小时前
C++ STL vector
java·开发语言·c++·经验分享·笔记·学习·算法
qyhua3 小时前
春节怀旧:翻出 20 年前的 VB6 书籍与老 CPU 记忆
笔记·其他
winfreedoms3 小时前
ROS2主题通讯——黑马程序员ROS2课程上课笔记(2)
笔记
was17214 小时前
你的私有知识库:自托管 Markdown 笔记方案 NoteDiscovery
笔记·云原生·自部署
浅念-15 小时前
C++ string类
开发语言·c++·经验分享·笔记·学习