在iOS开发中我们常使用Category来给类添加方法或者属性,原理是什么呢.
分类概况
我们先尝试写个分类.
NSObject+Category.h
#import <Foundation/Foundation.h>
#import<UIKit/UIKit.h>
@protocol protocol <NSObject>
-(void)protocolMethod;
@end
@interface NSObject (Category)<protocol>
@property(nonatomic,strong)NSString *property;
+(void)classMethod;
-(void)instanceMethod;
@end
NSObject+Category.m
#import "NSObject+Category.h"
@implementation NSObject (Category)
+(void)classMethod{
NSLog(@"classMethod");
}
-(void)instanceMethod
{
NSLog(@"instanceMethod");
}
@end
我们先通过clang命令将其转化为C++文件查看其中的编译过程
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc NSObject+Category.m
执行后会在当前文件家中生成cpp文件.在cpp文件中可以查看到分类的结构体
static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
//所属类的类名
"NSObject",
//要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象
0, // &OBJC_CLASS_$_NSObject,
//category中所有给类添加的实例方法的列表
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Category,
//category中所有添加的类方法的列表
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_Category,
//协议列表
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_NSObject_$_Category,
//category中所有添加的属性列表
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_Category,
};
分类属性
在结构体中有所属类名/类的对象/实例方法列表/类方法列表/协议列表/属性列表,但是没有成员变量列表.在OC的类中申明一个属性,都会自动生成一个带"_"的成员变量并创建setter/getter方法,分类中没有成员变量列表,所以在分类中创建的属性仅仅是一个申明,并没有对应的成员变量以及setter/getter方法,如果直接使用分类中的属性在运行时会报错.但是OC是一门动态语言,系统虽然没有处理,但是我们可以通过runtime在分类中手动添加setter/getter方法.
#import "NSObject+Category.h"
#import <objc/runtime.h>
@implementation NSObject (Category)
+(void)classMethod{
NSLog(@"classMethod");
}
-(void)instanceMethod
{
NSLog(@"instanceMethod");
}
//定义一个key值
static NSString *propertyKey = @"propertyKey";
//运行时实现setter方法
- (void)setProperty:(NSString *)property{
objc_setAssociatedObject(self, &propertyKey, property, OBJC_ASSOCIATION_COPY);
}
//运行时实现getter方法
- (NSString *)property {
return objc_getAssociatedObject(self, &propertyKey);
}
@end
这样就可以在运行过程中来获取和设置分类的值.需要注意的是,以上代码只是实现了setter/getter方法,并有创建成员变量,所以直接使用_成员变量,依然会报错.
分类方法
我们知道在OC中调用方法时,首先通过obj的isa指针找到obj对应的class(类方法是metaClass)。在class中,有一块最近调用的方法的指针缓存,所以先去cache通过selector查找对应的method,若cache中未找到,再去method list中查找,若method list中未找到,则去superClass中查找。若能找到,则将method加入到cache中,并通过method中的函数指针跳转到对应的函数中去执行。整个过程中并不会到分类的methodList中查找.那分类中的方法是如何被调用到的呢.
这要理解分类的加载栈
_objc_init
└──map_2_images
└──map_images_nolock
└──_read_images
_objc_init 算是整个 objc4 的入口,进行了一些初始化操作,注册了镜像状态改变时的回调函数;
map_2_images 主要是加锁并调用 map_images_nolock;
map_images_nolock 在这个函数中,完成所有 class 的注册、fixup等工作,还有初始化自动释放池、初始化 side table 等工作并在函数后端调用了 ;
_read_images _read_images 方法干了很多苦力活,比如加载类、Protocol、Category,加载分类的代码就写在 _read_images 函数的尾部;
简单的来说,就是初始化时,会先注册镜像,这个镜像会完成class的注册,并通过read_images把category的实例方法、协议以及属性添加到类上,把category的类方法和协议添加到类的metaclass上.
添加方法列表的时候是后添加的在新形成的列表前部,这也是为什么在有多个category中有同名方法时,后编译的在调用时会“覆盖”前面已编译的方法。其实方法本身并没有被覆盖,只是调用的时候是从上而下查找方法列表,当运行时找到对应的方法名后就去忙着调用了,并不会管后面的同名方法。
如果多个分类中有同名方法,可以在Build Phases->Compile Sources中移动文件位置.最终执行的是最后加载的.
总结
分类可以在不改变类名和原类的实现的前提下,进行类的扩展.支持开发人员针对自己构建的类,把相关的方法分组到多个单独的文件中,针对大型而复杂的类,可以提高维护性和可读性,并简化单个源文件的管理,在日常的开发中是非常强大的。
网友评论