前言
最近在围绕iOS编程语言(OC&&Swift)的语法 探索iOS的底层原理实现,在即将探索到 OC 面向对象语法中的:Category
、Extension
、关联对象
之前,先简单对这三者的特点和使用场景有一个简单的回顾。
学过OOP (object-oriented programming)
我们知道,面向对象的三大特性就是:封装、继承、多态,在iOS中也不例外有这三大特性。此外,iOS还提供了其它对OC类进行(功能/‘成员变量’)扩展
的方式:
Category
Extension
Protocol
关联对象
等
一、Category
Category
是 比继承更为简洁 的方法来对Class进行扩展,无需创建子类就可以为现有的类动态添加方法。
- 可以给项目内任何已经存在的类 添加
Category
- 甚至可以是
系统库/闭源库
等只暴露了声明文件的类 添加Category
(看不到.m 文件的类) - 通过
Category
可以添加实例方法
、类方法
、属性
- 注意:通过
Category
可以添加属性,会声明setter、getter方法 ,但需要 开发者 自己 实现 setter、getter方法 - 通过
Class
添加属性,会默认生成并实现 setter、getter方法
- 注意:通过
- 通过
Category
可以 重新实现 在 Class中已存在的 方法 - 通过
Category
可以 重新实现 在 其它 Category中已存在/已实现 的方法 - 在iOS中,实例对象/类对象方法调用顺序严格依赖 源码文件的编译顺序,编译顺序的查看可以通过
Xcode>Build Phases>Compile Sources
查看:-
- 类与 各个分类 各自声明且实现各自的方法:没有方法的实现被
‘覆盖’
,分类 只是扩展了 类的 功能
- 类与 各个分类 各自声明且实现各自的方法:没有方法的实现被
-
- 类与 各个分类 存在 声明 且实现 了相同的 方法: 存在 方法的实现被
‘覆盖’
(实际上不是被覆盖,而是方法地址后挪,系统会找到同名方法在内存地址中位置较前的方法 实现 调用)
- 分类 方法实现 的优先级 > 原来的类
- 各个分类 中 被
‘覆盖’
的情况严格 依赖 源码 文件的编译顺序:- 先编译的 方法 会 先加入 方法列表
「先入栈」
- 后编译的 方法 会 后加入 方法列表
「后入栈」
- 系统在调用 方法 的实现的时候,通过 对象(实例对象、类对象等) 和 方法API 在底层发送消息,拿到方法 实现 的 实现 IMP指针 找到 方法的具体实现(实际上最终拿到的方法实现,是后编译的源码文件中的方法实现)
- 先编译的 方法 会 先加入 方法列表
- 类与 各个分类 存在 声明 且实现 了相同的 方法: 存在 方法的实现被
-
实践验证:
1.添加一个类
//
// Car.h
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Car : NSObject{
@public
int _year;
}
-(void)run;
@end
NS_ASSUME_NONNULL_END
//
// Car.m
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import "Car.h"
@implementation Car
- (void)run{
NSLog(@"%s",__func__);
}
@end
查看 run方法的调用情况:
查看 是否符合 源码文件的编译顺序:
只有一个类的源码文件和main.m,符合预期
2.添加一个Category1
//
// Car+Test.h
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import "Car.h"
@interface Car (Test)
- (void)runFaster;
@end
//
// Car+Test.m
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import "Car+Test.h"
@implementation Car (Test)
- (void)run{
NSLog(@"%s",__func__);
}
- (void)runFaster{
NSLog(@"%s",__func__);
}
@end
此时我们只是编写了一个分类Car+Test
,但未引入到 源码调用中:
(该分类与原类有一个相同的方法)
main.m文件中的源码情况与打印结果:
查看 是否符合 源码文件的编译顺序:
打印结果符合编译文件的先后顺序
我们调换一下分类和原类的编译顺序试试看:
打印结果
不符合编译文件
的先后顺序: 分类 方法实现的优先级 > 原来的类
我们引入一个分类Car+Test
探究一下:\
//
// main.m
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Car+Test.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Car *car = [[Car alloc]init];
[car run];
[car runFaster];
}
return 0;
}
通过分类,原类增加了一个功能方法:
- (void)runFaster;
3.添加一个Category2
//
// Car+Test2.h
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import "Car.h"
NS_ASSUME_NONNULL_BEGIN
@interface Car (Test2)
- (void)runFaster;
@end
NS_ASSUME_NONNULL_END
//
// Car+Test2.m
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import "Car+Test2.h"
@implementation Car (Test2)
- (void)runFaster{
NSLog(@"%s",__func__);
}
@end
Car+Test2.h
与Car+Test.h
有一个相同的方法实现- (void)runFaster
此时尚未引入Car+Test2.h
,只是完成了分类代码的编写!
程序的调用 受 源码文件的 编译顺序的影响:优先级:
后编译源码实现 > 先编译源码实现
3.改变源码文件的编译顺序:
我们,改变两个分类的 源码文件的编译顺序试试看:
4.总结
相同方法(名称相同、类型相同、参数相同)实现的优先级:
- Category 的实现 > 原来的类 的实现
- 后编译的 Category 的实现 > 先编译的 Category 的实现
后编译的 Category > 先编译的 Category >原来的类
二、Extension
延展(Extension)
可以理解成是匿名的Category
- 可以用来给类
添加 属性和方法 的声明
,不作用在Subclass - 可以 通过 在.m文件 给其它类 或者 当前 类 添加
Extension
- 也可以 通过 .h文件 给类 添加
Extension
- 要对
Extension
添加的属性和方法
进行实现
1. 编写一个.h文件声明的Extension
//
// Car+DiyCar.h
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import "Car.h"
NS_ASSUME_NONNULL_BEGIN
@interface Car ()
- (void)diyRun;
@end
NS_ASSUME_NONNULL_END
2. 编写一个Person类,并给其 添加 匿名扩展 且 实现 刚才 .h文件声明的Extension
//
// Person.h
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import <Foundation/Foundation.h>
#import "Car+DiyCar.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
//Desc:
@property (strong, nonatomic) Car *myCar;
- (void)tellMeWhoRU;
@end
NS_ASSUME_NONNULL_END
//
// Person.m
// 分类、扩展
//
// Created by VanZhang on 2022/5/11.
//
#import "Person.h"
@implementation Car (DiyCar)
- (void)diyRun{
NSLog(@"%s",__func__);
}
- (void)run{
NSLog(@"%s",__func__);
}
@end
@interface Person ()
//Desc:
@property (strong, nonatomic) NSString* name;
@end
@implementation Person
- (instancetype)init{
if (self = [super init]) {
_name = @"我是你大哥!!";
}
return self;
}
- (Car *)myCar{
if (!_myCar) {
_myCar = [[Car alloc]init];
}
return _myCar;
}
- (void)tellMeWhoRU{
NSLog(@"name:%@",self.name);
}
@end
3.总结
Extension
的作用:
- 可以用来给类
添加 属性和方法 的声明
,不作用在Subclass - 可以 通过 在.m文件 给其它类 或者 当前 类 添加
Extension
- 给
当前类
添加Extension
时,编译器会默认给 添加的属性 声明且实现 其setter&&getter方法
- 给
- 也可以 通过 .h文件 给类 添加
Extension
- 要对
Extension
添加的属性和方法
进行实现 - 若 对
Extension
的实现中 ,重新实现 原来 类或其它分类中已有的方法,不会对原来的方法执行产生影响(因为没有自身的.m文件,不存在源码实现文件的编译的情况)
Extension
的作用更多在于拆分结构复杂的类,比较清晰的暴露声明
三、关联对象
我们前面的篇幅中提到:通过 Category
添加属性,可以默认生成 setter、getter方法 ,但系统不会默认是实现这两个方法。
所以我们在开发过程中常常自己去实现 setter、getter
我们编写一个Car类和它的分类,添加一个属性实践一下:
//
// Car.h
// 关联对象、Protocol
//
// Created by VanZhang on 2022/5/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Car : NSObject{
@public
int _year;
}
-(void)run;
@end
NS_ASSUME_NONNULL_END
//
// Car.m
// 关联对象、Protocol
//
// Created by VanZhang on 2022/5/11.
//
#import "Car.h"
@implementation Car
- (void)run{
NSLog(@"%s",__func__);
}
@end
//
// Car+Category1.h
// 关联对象、Protocol
//
// Created by VanZhang on 2022/5/11.
//
#import "Car.h"
NS_ASSUME_NONNULL_BEGIN
@interface Car (Category1)
//Desc:
@property (strong, nonatomic) NSString *nameplate;//车子的铭牌
@end
NS_ASSUME_NONNULL_END
//
// Car+Category1.m
// 关联对象、Protocol
//
// Created by VanZhang on 2022/5/11.
//
#import "Car+Category1.h"
@implementation Car (Category1)
@end
与原来的结论相符!!
接下来我们自己展开对 setter、getter
的实现:
如上图,我们不难得知,只需要把添加的属性,在setter方法中存起来,在getter方法中取出来返回即可
1.setter、getter
的方法实现1-通过系统的对象缓存手段存取
- NSUserDefault
- 沙盒缓存
- Dir
- Temp
- Library
- Cache
- Plist
- CoreData
- NSKeyedArchiver .... 诸上方法,可以解决程序开发的需要,但是对于属性的值的存取,基本是随着 类的销毁就销毁 的,没必要如此进行 数据缓存!!!若是这般做,是对硬盘资源的浪费!!
2.setter、getter
的方法实现-通过第三方缓存工具手段存取
- Sqlite(FMDB)
- Realm
- YYCache等 诸上方法,可以解决程序开发的需要,但是对于属性的值的存取,基本是随着 类的销毁就销毁 的,没必要如此进行 数据缓存!!!若是这般做,是对硬盘资源的浪费!!
3.setter、getter
的方法实现-通过静态字典对象进行存取
因为一个分类可以给类添加若干个属性,所以可以通过属性名做键值对的key,对真实的值进行存取。但是添加静态字典,属于全局公用的!
- 若这个类初始化了多个实例,那么将访问通过一个静态字典的内存,那么就存在数据存储隔离的缺点了
- 并且 这样存储 还存在线程安全问题,需要进一步 编写 保障 存取数据 的 可靠性的代码
- 最后,若添加了若干个属性,则诸上代码 需要重复 编写 若干遍,会导致代码文件肿大(若是整个项目都如此编写分类,那么会导致一定的性能降低)
4.setter、getter
的方法实现4-通过系统runtimeAPI提供的关联对象进行存取
修改分类的实现代码如下:
//
// Car+Category1.m
// 关联对象、Protocol
//
// Created by VanZhang on 2022/5/11.
//
#import "Car+Category1.h"
#import <objc/runtime.h>
@implementation Car (Category1)
- (void)setNameplate:(NSString *)nameplate{
objc_setAssociatedObject(self, @selector(nameplate), nameplate, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)nameplate{
return objc_getAssociatedObject(self, _cmd);
}
@end
程序的运行结果如下:
读写成功!!!
- 值的存取都只通过一行代码,十分简介!!
- 线程安全(通过底层分析得知,底层分析代码待下会分晓!)!!
- 在程序结束的时候,随着实例的销毁,也清理了内存
5.总结
- 通过
Category
可以给类添加属性,但需要开发者自己实现属性的setter、getter
方法(只需要把添加的属性,在setter方法中存起来,在getter方法中取出来返回即可) - 实现
setter、getter
方法的最佳方案是 通过系统 的runtimeAPI关联对象
- 代码 简介 可靠
- 线程安全
- 需要引入头文件和API:
#import <objc/runtime.h> void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy) ; id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
- objc_AssociationPolicy其实就是添加属性时候的 描述
/* Associative References */ /** * Policies related to associative references. * These are options to objc_setAssociatedObject() */ typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied. * The association is made atomically. */ };
- OBJC_ASSOCIATION_ASSIGN = @property (assign)
- OBJC_ASSOCIATION_RETAIN_NONATOMIC = @property (strong,nonatomic)
- OBJC_ASSOCIATION_COPY_NONATOMIC = @property (copy,nonatomic)
- OBJC_ASSOCIATION_RETAIN = @property (strong)
- OBJC_ASSOCIATION_COPY = @property (copy)
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy) ;
- 参数介绍:
id
: 为给谁关联对象,一般填self(当前实例对象)const void * _Nonnull key
:关联的keyid _Nullable value
:关联的值objc_AssociationPolicy policy
:关联的策略
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
- 参数介绍:
id
: 为给谁关联对象,一般填self(当前实例对象)const void * _Nonnull key
:关联的key- 返回值就是 前面一个API关联的值
- 对于关联的key有很多种写法:
- 通过属性名 字符串
- 通过宏定义
- 通过...
- 最流行的写法是通过 getter方法的@selector对象
- 能够保证其唯一性
- 能够让 源码文件 不那么肿大(在添加若干属性的时候,需要编写大量的key相关的代码)
- 标识度清晰(一看代码就知道是哪个key),统一编程习惯之后,直接不用看代码都知道内部的实现,进而不引入头文件的情况下可以不耦合获取存储的值
四、Protocol
1.Protocol的作用
- 1、
一个协议对应一个类:
公开方法一般都放在.h
文件中,如果想隐藏实现细节,可以把这些方法放到协议中,再让该类遵守此协议(有点类似与Extension
) - 2、
一个协议对应多个类:
将一些公共方法抽取出来封装成协议,任何类想拥有这些方法,只需遵守此协议即可
问题:公共方法为什么不放在父类中?
答:下图中D
类跟E
类的公共方法可以放在父类B
中,但D
类跟G
类的公共方法放在父类的父类A
中显然是不合适的,这时协议就能很优雅的解决
2.基本使用
2.1 修饰符
@required
:遵守此协议的类必须实现它修饰的方法(默认修饰符)@optional
:遵守此协议的类可以不实现它修饰的方法
@protocol PersonProtocol <NSObject>
@required
- (void)eat;
@optional
- (void)run;
@end
2.2 能声明属性,但必须在遵守此协议的类中调用@syntheszie
才能正常使用
// PersonProtocol
@protocol PersonProtocol <NSObject>
@property (nonatomic, copy) NSString *name;
@end
// Person
@interface Person : NSObject <PersonProtocol>
@end
@implementation Person
@synthesize name; // 生成get/set方法的实现
@end
// 使用
Person *person = [Person new];
person.name = @"111";
NSLog(@"name---%@", person.name);
// 打印
name---111
2.3 不能声明成员变量
3. Protocol特点
- 协议只有方法的声明,没有方法的实现
- 遵守协议只能在类的声明
@interface
上,不能在类的实现@implementation
上 - 一个协议
可以遵守多个其他协议
- 一个协议若遵守了其他协议,就拥有其他协议所有方法的声明
- 一个协议可以被多个类遵守,一个类可以遵守多个协议
- 一个类若遵守了某个协议,就必须实现协议中
@required
修饰的方法(不实现的话,编译器会发出警告,但依然能编译通过) - 若父类遵守了某个协议,子类也就遵守了此协议
4. Protocol应用场景
4.1 不同的类使用统一入口传递数据(一个协议对应多个类)
// CustomViewProtocol
@protocol CustomViewProtocol <NSObject>
- (void)setData:(id)data;
@end
// CustomView
@interface CustomView : UIView <CustomViewProtocol>
@end
@implementation CustomView
- (void)setData:(id)data {
NSLog(@"view---%@", data);
}
@end
// CustomTableView
@interface CustomTableView : UITableView <CustomViewProtocol>
@end
@implementation CustomTableView
- (void)setData:(id)data {
NSLog(@"tableView---%@", data);
}
@end
// 使用
NSArray *views = @[[CustomView new], [CustomTableView new]];
NSArray *datas = @[@"111", @"222"];
[views enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj conformsToProtocol:@protocol(CustomViewProtocol)]) { // 是否遵守此协议
[obj setData:datas[idx]];
}
}];
// 打印
view---111
tableView---222
4.2.面向接口编程AOP:将接口(声明)和实现分离,对外只暴露接口(一个协议对应一个类)
- 图解
面向接口编程
bridge
:关联接口和实现
// .h文件
@interface ServerBridge : NSObject
+ (void)bindServer:(id)server andProtocol:(Protocol *)protocol;
+ (id)serverForProtocol:(Protocol *)protocol;
@end
// .m文件
@interface ServerBridge ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, id> *serverStore;
@end
@implementation ServerBridge
- (NSMutableDictionary<NSString *,id> *)serverStore {
if (!_serverStore) {
_serverStore = [NSMutableDictionary new];
}
return _serverStore;
}
+ (instancetype)shared {
static id _bridge = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_bridge = [[self alloc] init];
});
return _bridge;
}
+ (void)bindServer:(id)server andProtocol:(Protocol *)protocol {
if ([server conformsToProtocol:protocol]) {
[[ServerBridge shared].serverStore setValue:server
forKey:NSStringFromProtocol(protocol)];
}
}
+ (id)serverForProtocol:(Protocol *)protocol {
return [[ServerBridge shared].serverStore valueForKey:NSStringFromProtocol(protocol)];
}
@end
protocol
:对外暴露的接口
@protocol ServerProtocol <NSObject>
@property (nonatomic, copy) NSString *provideData;
- (void)doSomething;
@end
server
:接口的具体实现
@interface Server () <ServerProtocol>
@end
@implementation Server
@synthesize provideData;
+ (void)load {
[ServerBridge bindServer:[self new]
andProtocol:@protocol(ServerProtocol)];
}
- (NSString *)provideData {
return @"server provide data";
}
- (void)doSomething {
NSLog(@"server do something");
}
@end
business
:使用接口的业务
id<ServerProtocol> server = [ServerBridge serverForProtocol:@protocol(ServerProtocol)];
NSLog(@"%@", server.provideData);
[server doSomething];
// 打印
server provide data
server do something
3,控制链式编程的调用顺序
// .h文件
@class SQLTool;
@protocol Fromable;
@protocol Whereable;
typedef SQLTool<Fromable>*(^Select)(NSString *string);
typedef SQLTool<Whereable>*(^From)(NSString *string);
typedef SQLTool*(^Where)(NSString *string);
@protocol Selectable <NSObject>
@property (nonatomic, copy, readonly) Select select;
@end
@protocol Fromable <NSObject>
@property (nonatomic, copy, readonly) From from;
@end
@protocol Whereable <NSObject>
@property (nonatomic, copy, readonly) Where where;
@end
@interface SQLTool : NSObject
+ (NSString *)makeSQL:(void(^)(SQLTool<Selectable> *tool))block;
@end
// .m文件
@interface SQLTool() <Selectable, Fromable, Whereable>
@property (nonatomic, copy) NSString *sql;
@end
@implementation SQLTool
- (NSString *)sql {
if (!_sql) {
_sql = [NSString new];
}
return _sql;
}
+ (NSString *)makeSQL:(void(^)(SQLTool<Selectable> *tool))block {
if (block) {
SQLTool *tool = [SQLTool new];
block(tool);
return tool.sql;
}
return nil;
}
- (Select)select {
return ^(NSString *string) {
self.sql = [self.sql stringByAppendingString:string];
return self;
};
}
- (From)from {
return ^(NSString *string) {
self.sql = [self.sql stringByAppendingString:string];
return self;
};
}
- (Where)where {
return ^(NSString *string) {
self.sql = [self.sql stringByAppendingString:string];
return self;
};
}
@end
// 使用
NSString *sql = [SQLTool makeSQL:^(SQLTool<Selectable> *tool) {
tool.select(@"111").from(@"222").where(@"333"); // 不能改变调用顺序
}];
NSLog(@"%@", sql);
// 打印
111222333
5. 系统协议
5.1 NSObject
:根协议,其他协议都要遵守它,它提供了很多基本的方法,基类NSObject
已经遵守此协议并实现了协议方法,所以我们可以直接使用这些方法
@protocol NSObject
- (id)performSelector:(SEL)aSelector;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)respondsToSelector:(SEL)aSelector;
...
@end
5.2 NSCopying
:与对象拷贝相关的协议,如果想让某个自定义类具备拷贝功能,那么该类必须遵守此协议并实现协议方法
- 声明
@protocol NSCopying
- (id)copyWithZone:(NSZone *)zone;
@end
- 使用
// Person
@interface Person : NSObject <NSCopying>
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
- (id)copyWithZone:(NSZone *)zone {
Person *person = [[Person allocWithZone:zone] init];
person.name = self.name;
return person;
}
@end
// 使用
Person *person1 = [Person new];
person1.name = @"123";
Person *person2 = [person1 copy];
person1.name = @"456";
NSLog(@"%@", person1.name);
NSLog(@"%@", person2.name);
// 打印
456
123
5.3 NSMutableCopying
:让自定义类具备可变拷贝功能
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(NSZone *)zone;
@end
5.4 NSCoding
:如果想用归档存储某个自定义类对象,那么该类必须遵守此协议并实现协议方法
- 声明
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
@end
- 使用
// Person
@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
@end
// 使用
- (void)viewDidLoad {
[super viewDidLoad];
[self save];
[self read];
}
- (NSString *)filePath {
NSString *document = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
return [document stringByAppendingPathComponent:@"Person.data"];
}
- (void)save {
Person *person = [Person new];
person.name = @"111";
[NSKeyedArchiver archiveRootObject:person toFile:self.filePath];
}
- (void)read {
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:self.filePath];
NSLog(@"%@", person.name);
}
// 打印
111
5.5 NSSecureCoding
:在NSCoding
基础上增加了安全性
@protocol NSSecureCoding <NSCoding>
@property (class, readonly) BOOL supportsSecureCoding;
@end
总结
本文整个篇幅通过 理论 + 代码示例 验证的形式 介绍了iOS中除了 封装继承多态三大特性外,其它对OC类进行(功能/‘成员变量’)扩展
的方式:
Category
Extension
Protocol
关联对象
等 为紧接着探索Category、关联对象
的底层原理 进行了一定的准备工作!!