Category

参考文章

iOS-分类(Category)
“class-continuation分类”的5大用处
Category底层结构及源码分析
iOS 给分类(Category文件)添加属性

demo

category的测试demo 包括给分类添加属性 以及私有方法的调用

什么是Category

  • Category是Objective-C 2.0之后添加的语言特性,分类、类别其实都是指的Category。
  • Category的主要作用是为已经存在的类添加方法。也可以说是将庞大的类代码按逻辑划入几个分区。
  • Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

Category的优点

  • 声明私有方法
  • 分解体积庞大的类文件
  • 把Framework私有方法公开
  • 模拟多继承(另外可以模拟多继承的还有protocol)

Category的用法示例

在这个demo中,又一个person类,这个类很庞大,想要用Category按代码逻辑划分为几个分区。

分类的创建

选择OC file
2.
file的name:功能(这里我的是person的name分区)
filetype选择Category
class:我的继承于Person
在这里插入图片描述

Category源码分析

Category源码

Category源码

Category 是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *Category;
struct objc_category {
  char *category_name                          OBJC2_UNAVAILABLE; // 分类名
  char *class_name                             OBJC2_UNAVAILABLE; // 分类所属的类名
  struct objc_method_list *instance_methods    OBJC2_UNAVAILABLE; // 实例方法列表
  struct objc_method_list *class_methods       OBJC2_UNAVAILABLE; // 类方法列表
  struct objc_protocol_list *protocols         OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

Category结构体主要包含了分类定义的实例方法与类方法,其中instance_methods 列表是 objc_class 中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。

分类的特点

  1. 分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。

    原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性

  2. 分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量,会编译通过,但是引用变量会报错;

  3. 如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类;

  4. 同名分类方法生效取决于编译顺序: 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。

分类的实现原理

我们不主动引入 Category 的头文件,Category 中的方法都会被添加进主类中。我们可以通过 - performSelector: 等方式对 Category 中的相应方法进行调用,但是调用的地方会有黄色警告,但是方法依然可以实现。(如果这里不想有这个黄色警告,可以导入分类的头文件)
在这里插入图片描述
实现原理:
a)将 Category 和它的主类(或元类)注册到哈希表中;
b)如果主类(或元类)已实现,那么重建它的方法列表。

Category 中的实例方法和属性被整合到主类中;
类方法则被整合到元类中;
对协议的处理比较特殊,Category 中的协议被同时整合到了主类和元类中。

最终都是通过调用 staticvoid remethodizeClass(Class cls) 函数来重新整理类的数据的。

为什么不能添加成员变量

运行时的class消息:

由下面代码可以看出,方法列表,属性列表,协议列表都是可读可写的,但是成员变量列表是只读的。(const常量符号)

class对象结构体

为什么不能调用分类中的属性

  • 虽然不能添加成员变量,但是是可以在分类中添加属性。
  • 添加的属性系统并没有自动生成成员变量,也没有实现set和get方法,只是生成了set和get方法的声明。这就是为什么在分类中扩展了属性,在外部并没有办法调用。
  • 在外部调用点语法设值和取值,本质其实就是调用属性的set和get方法,现在系统并没有实现这两个方法,所以外部就没法调用分类中扩展的属性。
Person *p = [[Person alloc] init];
p.age = 20;
NSLog(@"%d",p.age);

运行报错
-[Person setAge:]: unrecognized selector sent to instance 0x100709220
-[Person age]: unrecognized selector sent to instance 0x1005639e0
  • 首先能调用p.page=20,和p.age两句,说明系统已经生成了set和get方法的声明;
  • 运行时,又会报找不到setAge:和age方法而报错,说明系统没有实现set和get方法。
  • 直接调用_age也会报错,说明没有生成成员变量。这样得以证明以上关于分类中属性的结论。

我的demo
可以正常编译,运行时会出错,说明声明了set和get方法,只是没有实现。
按正常方式写运行时会出错

给分类添加属性(分类添加实例变量-关联对象)

首先介绍一下关联对象

关联对象

  • AssociationsManager 是顶级的对象,维护了一个从 spinlock_t 锁到 AssociationsHashMap 哈希表的单例键值对映射;

  • AssociationsHashMap 是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射;

  • ObjectAssociationMap 是一个 C++ 中的 map ,维护了从 key 到 ObjcAssociation 的映射,即关联记录;

  • ObjcAssociation 是一个 C++ 的类,表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。

把分类中的属性作为关联对象

核心代码

objc_setAssociatedObject(self, &propertyTestKey, testStr, OBJC_ASSOCIATION_COPY);

这句就是把定义的属性testStr作为self的键值为propertyTestKey的关联对象,关联方式为OBJC_ASSOCIATION_COPY。
self的所有关联对象构成了一张AssociationsMap表,其中键值为propertyTestKey的对象就是存贮testStr值和关联方式的。
分类Person+AddPropertyTest的.h文件中
分类Person+AddPropertyTest的.m文件中
17行set 18行get

Category中添加的方法为什么会覆盖原来类中的方法?解释原理?

分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法、属性,协议数据拷贝到类对象的方法列表中。
runtime首先加载某个类的所有Category数据,然后把所有Category的方法、属性、协议数据,合并到一个大数组中(后面参与编译的Category数据会在数组的前面),最后将合并后的分类数据(方法、属性、协议),插入到原来数据的前面。所以调用方法时会优先到调用Category中的方法,当父类中有同样的方法就不会调用。

Category底层结构及源码分析

一个特殊的分类——Class-continuation

“class-continuation 分类” 和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,

而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,“class-continuation 分类”没有名字,其写法如下:

@interface Person ()
	// methods
@end

Class-continuation的作用

1. 隐藏类

我们下载的源码中,基本都是.h文件,.m文件是不公开的,而Class-continuation写在.m文件中,添加到其中的类可以被隐藏。

2. 编写Objctive-C++

编写 Objective-C++ 代码时 “class-continuation 分类” 也尤为有用。Objective-C++ 是 Objective-C 与 C++ 的混合体,其代码可以用这两种语言来编写。

假如某个类打算这样写:

#import <Foundation/Foundation.h>
#include "SomeCppClass.h"

@interface Class : NSObject {
@private
    SomeCppClass _cppClass;
}
@end

Objective-C++是Objective-C与C++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用C++来编码。

该类的实现文件可能叫做 Class.mm,其中 .mm 扩展名表示编译器应该将此文件按 Objective-C++ 来编译,否则,就无法正确引入 SomeCppClass.h 了。然而请注意,名为 SomeCppClass 的这个 C++ 类必须完全引入,因为编译器要完整地解析其定义方能得知 _cppClass 实例变量的大小。

于是,只要是包含 Class.h 的类,都必须编译为 Objective-C++ 才行,因为它们都引入了 SomeCppClass 类的头文件。这很快就会失控,最终导致整个应用程序全部都要编译为 Objective-C++。这么做确实完全可行,不过相当别扭,尤其是将代码发布为程序库供其他应用程序使用时,更不应该如此。要求第三方开发者将其源文件扩展名均改为 .mm 不是很合适。

某些系统库用到了这种模式,比如网页浏览器框架 WebKit,其大部分代码都以 C++ 编写,然而对外展示出来的却是一套整洁的 Objective-C 接口。 CoreAnimation 里面也用到了此模式,它的许多后端代码都用 C++ 写成,但对外公布的却是一套纯 Objective-C 接口。

(OC++代码由两种语言编写和编译,如果导入C++文件,就必须编译为OC++对象,写在.h文件中,则所有导入该文件的文件都需要编译为OC++对象,写在.m文件中,就只有该文件编译为OC++对象)

3. 扩展public接口中声明的属性

将public接口中声明为“只读”的属性扩展为“可读写”,以便在类的内部设置其值。这样,封装在类中的数据就由实例本身来控制,而外部代码则无法修改其值。

// EOCPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName
              lastName:(NSString *)lastName;
@end
NS_ASSUME_NONNULL_END
// EOCPerson.m
@interfaceEOCPerson()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end

@implementation EOCPerson
-(void)someMethod
{
    NSString *test = @"test";
    //在class-continuation分类中不管有没有定义readwrite,这个_firstName都可以赋值
    _firstName = test;
    //只有分类中定义了readwrite,才可以使用点语法和setFirstName:设置方法
    self.firstName = test;
    [self setFirstName:test];
}
@end

4. 声明只会在类实现代码中用到的方法

(语法上来说,这与定义在实现块里没什么区别)
只会在类的实现代码中用到的私有方法也可以声明在“class-continuation分类”中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。这些方法可以这样写:

@interface EOCPerson()
	//私有方法
	-(void)p_privateMethod;
@end

@implementation EOCPerson
	-(void)p_privateMethod {
	
	}
@end

5. 遵守的私有协议

若对象所遵从的协议只应视为私有,则可在“class-continuation分类”中声明。有时由于对象所遵从的某个协议在私有API中,所以我们可能不太想在公共接口中泄露这一信息。

#import "EOCPerson.h"
#import "EOCSecretDelegate.h"
@interfaceEOCPerson() <EOCSecretDelegate>
@end

@implementation EOCPerson
/*… */
@end

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值