Category知识点

1 、Category的简单应用

Category的使用非常频繁,他可以动态的为已经存在的类添加新的行为。当你不想对原类文件进行修改的时候,你就可以通过添加Category去定制自己需要的方法。这样,不需要访问其源代码、也不需要创建子类,Category使用简单的方式,实现了类的相关方法的模块化,把不同的类方法分配到不同的分类文件中,这样可以保证类的原原来的基础上,较小的改动就可以增加需要的功能。比如扩展系统提供的方法,扩展CocoaPod管理的方法时,我们会经常用到Category。

上example(我习惯将原理写在代码注释中,所以认真看哟,当然最后我也会做总结):

创建MJPerson类和两个分类MJPerson+Test、MJPerson+Eat:

//MJPerson.h和MJPerson.m文件

#import <Foundation/Foundation.h>

@interface MJPerson : NSObject

@property(nonatomic,strong)NSString *personName;
@property(nonatomic,assign)int personAge;

-(void)run;

@end

===============================================

#import "MJPerson.h"

@implementation MJPerson

-(void)run{
    NSLog(@"I can run");
}

@end
复制代码
//MJPerson+Test.h和MJPerson+Test.m文件

#import "MJPerson.h"

@interface MJPerson (Test)

-(void)test;

@end
===============================================

#import "MJPerson+Test.h"

@implementation MJPerson (Test)
-(void)test{
    NSLog(@"I can test");
}
@end
复制代码
//MJPerson+Eat.h和MJPerson+Eat.m文件
#import "MJPerson.h"

@interface MJPerson (Eat)<NSCopying,NSCopying>

@property(nonatomic,strong)NSString *name;
@property(nonatomic,assign)int age;

-(void)eat;

-(void)eat1;

+(void)eat2;

+(void)eat3;

@end
===============================================

#import "MJPerson+Eat.h"

@implementation MJPerson (Eat)

- (void)setName:(NSString *)name{
    self.personName = name;
}

- (void)setAge:(int)age{
    self.personAge = age;
}


-(NSString *)name{
    return self.personName;
}

-(int)age{
    return self.personAge;
}

-(void)eat{
    NSLog(@"I can eat");
}

-(void)eat1{
    NSLog(@"实例方法eat1");
}

+(void)eat2{
    NSLog(@"类方法eat2");
}

+(void)eat3{
    NSLog(@"类方法eat3");
}

@end
复制代码
//ViewController.m文件
#import "ViewController.h"
#import "MJPerson.h"
#import "MJPerson+Test.h"
#import "MJPerson+Eat.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
}


-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    MJPerson *person = [[MJPerson alloc] init];
//    这种方法调用的方式都是通过 objc_msgSend(对象名,@selector(方法名)) 来实现的
    [person run];
    [person test];
    [person eat];
    person.age = 10;
    person.name = @"ychen3022";
    NSLog(@"刚刚赋值的age: %d",person.age);
    NSLog(@"刚刚赋值的name: %@",person.name);

//    分类的实现原理
//    给person发送一个消息:通过objc_msgSend(person,@selector(eat))
//    发消息的这种机制又是怎么实现的呢?去哪里找到对应的方法呢?
//    -(void)eat是个实例方法,存储在class(类对象)里面的,所以是给实例对象发送消息,通过isa指针找到这个class(类对象),在class(类对象)的方法里面找到实例方法的实现(IMP),然后进行调用    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end


=========================================================
打印结果:
CategoryTest[2737:706571] I can run
CategoryTest[2737:706571] I can test
CategoryTest[2737:706571] I can eat
CategoryTest[2737:706571] 刚刚赋值的age: 10
CategoryTest[2737:706571] 刚刚赋值的name: ychen3022
复制代码
2 、探究Category本质

使用clang编译命令行,把MJPerson+Eat.m文件转成.cpp文件

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Eat.m
复制代码

从文件中,我们可以找到有个_category_t的结构体,如下:

struct _category_t {
	const char *name; //类名,并不是category小括号里写的名字,而是类的名字
	struct _class_t *cls; //cls要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
	const struct _method_list_t *instance_methods; //存放这个category所有的对象方法列表
	const struct _method_list_t *class_methods; //存放这个category所有的类方法列表
	const struct _protocol_list_t *protocols; //存放这个category所有的协议列表
	const struct _prop_list_t *properties; //存放这个category所有的属性列表
};
复制代码

同时,我们也可以找到MJPerson+Eat这个分类的数据内容 (可以继续找到对应的详细内容,我就不贴代码了)

static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"MJPerson",
	0, // &OBJC_CLASS_$_MJPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat,
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Eat,
};
复制代码

通过上面的分析,我们可以感觉到Category被存储在这个结构体里的,那一直是存储在这里吗?调用的时候也是从这个结构体调用?带着疑问往下看:

苹果源码网站下载下来runtime的objc4-723源码,分析得出: 在运行时初始化的时候,会加载项目中所有的类,并将Category中的内容附加到类对象中。 所以当含有同样的方法名时,会优先调用分类中的方法(因为后添加进来先调用)。同是分类中的同名方法,谁后编译就调用谁的。

obje-runtime-new.mm文件中的部分代码如下:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
// 将方法列表、属性、协议从分类文件中附加到class(类对象)中
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    //定义里三个二维数组(方法数组、属性数组、协议数组),相当于分配了一块内存空间来存储这三类内容
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    // 通过循环,将某个类的所有分类文件中的方法列表、属性列表、协议列表都放到了mlists、proplists、protolists
    while (i--) {
        // 取出某个分类
        auto& entry = cats->list[i];
        // 取出分类中的方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 取出分类中的属性
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        // 取出分类中的协议
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //将所有分类的对象方法附加到class(类对象)的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    //将所有分类的属性附加到class(类对象)的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    //将所有分类的协议附加到class(类对象)的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
复制代码

通过上面的分析,我们可以知道: 在编译阶段,Category的底层结构是struct category_t,里面存储了该分类文件中的数据(方法、属性、协议等)。 在程序运行时候,通过runtime加载某个类的所有Category数据,把该类对应的所有Category数据合并到一个大数组中。 最后,将合并后的Category数据插入到该类原来数据的前面。

3、总结

<1>Category的实现原理 Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

<2>Category和Class Extension的区别是什么?

  • 扩展是在编译的时候,它的数据就已经包含在类信息中, 分类是在运行时,才会将数据合并到类信息中

  • 分类中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已)

  • 扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型(使用范围只能在自身类,而不是子类或其他地方)

  • 扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中

  • 扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,扩展所声明的方法必须依托对应类的实现部分来实现

  • 定义在 .m 文件中的扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。扩展是在 .m 文件中声明私有方法的非常好的方式

<3>Category中能定义属性吗? 是可以的,但是分类可以用@property来添加属性,此种方式会自动生成对应属性的set和get方法的声明,但是没有set和get方法的实现,也不会自动生成带有“_”的属性。 如上example所示,我们必须在MJPerson+Eat.m中对属性age、name的get、set方法进行重写,而且是对其原来的类MJPerson的属性personAge和personName进行赋值的,否则会报错。

转载于:https://juejin.im/post/5bc3535ce51d450e3d2d2eb7

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值