前言
iOS
的一道经典面试题:分类是什么?
是否可以给分类添加成员变量?如果可以,怎么添加?下面我们就来探究探究分类的前世今生。
分类的探究
分类是一个名称为category_t的结构体。
struct category_t {
const char *name; // 分类关联的类名
classref_t cls; // 分类关联的类
struct method_list_t *instanceMethods; // 实例方法
struct method_list_t *classMethods; // 类方法
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
由代码可知,分类的元素如下:
- 分类关联的类名
- 分类关联的类
- 实例方法
- 类方法
- 协议
- 实例属性
- 类属性
所以,我们可以得出,分类是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些属性、方法、协议。
根据分类的结构,我们还可以得出,分类的可以添加属性,但是需要我们手动实现其set/get
方法,这样才能真正的使用属性。
在我们讨论类的加载的时候,在read_image
的环节中,我们会处理所有的分类,对未绑定的分类进行绑定操作,将分类的的method、protocol、property
添加到类。
for (EACH_HEADER) {
// 外部循环遍历找到当前类,查找类对应的Category数组
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
// 内部循环遍历当前类的所有Category
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 用来判断分类进入
const char *catename = cat->name;
const char *ocatename = "addition"; // addition
if (catename && (strcmp(catename, ocatename) == 0)) {
printf("AAAAAAAAAAAA类名 :%s - %p\n",catename,cls);
}
// 首先,通过其所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表。
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 将Category添加到对应Class的value中,value是Class对应的所有category数组
addUnattachedCategoryForClass(cat, cls, hi);
// 将Category的method、protocol、property添加到Class
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
}
// 这块和上面逻辑一样,区别在于这块是对Meta Class做操作,而上面则是对Class做操作
// 根据下面的逻辑,从代码的角度来说,是可以对原类添加Category的
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
}
}
}
了解到这些相关信息之后,我们就先来创建一些文件来验证一下分类加载的具体情况:
分类的加载
首先我们创建一个TPerson
类:
@interface TPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
+ (void)sayYo;
@end
#import "TPerson.h"
@implementation TPerson
+ (void)load {
NSLog(@"类-load");
}
- (void)sayHello {
NSLog(@"%s",__func__);
}
+ (void)sayYo {
NSLog(@"%s",__func__);
}
@end
并且为其创建一个分类:
#import "TPerson.h"
@interface TPerson (addition)
@property (nonatomic, copy) NSString *cateProp;
- (void)cate_instanceMethod;
+ (void)cate_classMethod;
@end
@implementation TPerson (addition)
+ (void)load {
NSLog(@"分类-load");
}
- (void)setCateProp:(NSString *)cateProp {
}
- (NSString *)cateProp {
return @"cateProp";
}
- (void)cate_instanceMethod {
NSLog(@"%s",__func__);
}
+ (void)cate_classMethod {
NSLog(@"%s",__func__);
}
@end
另外,我们在read_image
方法中加入以下代码,便于调试:
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
const char *cname = cls->nameForLogging();
const char *oname = "TPerson";
if (cname && (strcmp(cname, oname) == 0)) {
printf("_read_images - _getObjc2ClassList 类名 :%s - %p\n",cname,cls);
}
}
}
1. 非懒加载类,非懒加载分类
此时,类和分类都实现了load
方法,类是非懒加载类,分类也是非懒加载分类。非懒加载类在初始化的时候,对调用realizeClassWithoutSwift(Class cls)
,该方法会调用methodizeClass(Class cls)
,而在该方法中会执行下面方法:
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
而此时返回的cats
是null,所以在此处并不会进行分类的加载操作。
我们在read_image
方法中分类的相关处理,添加以下代码:
const char *catename = cat->name;
const char *ocatename = "addition"; // addition
if (catename && (strcmp(catename, ocatename) == 0)) {
printf("AAAAAAAAAAAA类名 :%s - %p\n",catename,cls);
}
并在打印的的地方添加调试断点,运行代码,进行调试,代码会断在此处,使用LLDB
调试:
可以看出此时,ro
里面只有属性的set/get
方法和实例方法setHello
,并没有加载分类中的方法。继续调试,进入以下代码
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties) {
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
}
// 该方法将没有绑定的分类放入表中,并和类进行绑定。
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
runtimeLock.assertLocked();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
// 绑定分类
static void remethodizeClass(Class cls)
{
......
attachCategories(cls, cats, true /*flush caches*/);
......
}
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;
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);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
由代码可以看出,在attachCategories
方法中,首先我们会拿出当前类的所有分类,进行遍历操作,然后将每一个分类的方法、属性、协议全部copy
一份,绑定到对应的类的rw
中的methods、properties、protocols
中,这样完成分类的加载。
通过LLDB
调试,也能得出相应的结果。
结论:当非懒加载类和非懒加载分类配合加载时,类是在realizeClassWithoutSwift
中加载,而分类是在调用addUnattachedCategoryForClass(cat, cls, hi)
之后,在remethodizeClass(cls)
方法中加载。
2. 非懒加载类,懒加载分类
保留类的load
方法,注释掉分类中的load
方法,就可以探究非懒加载类配合懒加载分类是如何加载的。我们在类的加载中添加如下代码打印结果,便于调试:
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
const char *cname = cls->nameForLogging();
const char *oname = "TPerson";//TPerson
if (cname && (strcmp(cname, oname) == 0)) {
printf("_read_images - _getObjc2NonlazyClassList 类名 :%s - %p\n",cname,cls);
}
......
addClassTableEntry(cls);
realizeClassWithoutSwift(cls);
}
}
在打印处添加断点,使用LLDB
调试:
发现此时分类中的方法和数据已经写入ro
中。
结论:当非懒加载类和懒加载分类配合加载时,在realizeClassWithoutSwift
的时候,已经将分类的数据写入ro
中。
3. 懒加载类,非懒加载分类
注释掉类的load
方法,保留分类的load
方法,就可以验证懒加载类配合非懒加载分类是如何加载的。
我们依然在read_images
中,对printf
语句进行LLDB
调试:
得出,类的ro
为空。由于分类实现了load
方法,根据我们的经验实现load
方法会提前加载,可以推断分类的加载也提前了。我们在load_images
方法中下一个断点,然后运行程序,发现程序会进入这里。跟踪流程,会进入prepare_load_methods
中,
void
load_images(const char *path __unused, const struct mach_header *mh)
{
......
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
call_load_methods();
}
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
......
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
const char *cname = cls->nameForLogging();
const char *oname = "TPerson";
if (cname && (strcmp(cname, oname) == 0)) {
printf("prepare_load_methods :非懒加载分类名 :%s \n",cname);
}
realizeClassWithoutSwift(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
非懒加载分类,则会调用realizeClassWithoutSwift
以及methodizeClass(Class cls)
,将数据写入rw
中:
结论:懒加载类配合非懒加载分类加载,在load_images
的方法中,执行prepare_load_methods
对分类和类进行加载。
4. 懒加载类,懒加载分类
我们分别注释掉类和分类中的load
方法,来看看懒加载类配合懒加载分类是如何加载的。
首先在read_images
中,对printf
语句进行LLDB
调试:
显而易见,类的ro
是空的。在类的加载原理中,我们知道懒加载类是在该类第一次调用方法的时候才开始加载,由于调用方法就是消息发送,所以我们直接定位到消息发送流程中。我们在lookUpImpOrForward
添加以下打印方法,用以调试:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
const char *cname = cls->nameForLogging();
const char *oname = "TPerson"; // TPerson
if (cname && (strcmp(cname, oname) == 0)) {
printf("lookUpImpOrForward 类名 :%s - %p\n",cname,cls);
}
// 此时类还没有加载,进行初始化操作
if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
// 绑定数据
if (initialize && !cls->isInitialized()) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
}
我们在printf("lookUpImpOrForward 类名 :%s - %p\n",cname,cls)
进行LLDB
,发现ro
依然是空的,由于当前类并没有初始化,所以会执行realizeClassMaybeSwiftAndLeaveLocked
,然后执行realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
方法,接着调用realizeClassWithoutSwift(Class cls)
方法,初始化类,加载数据。我们也可以在if (initialize && !cls->isInitialized()) {
打断点调试,验证结论:
此时,元类的ro
中存在类方法,且有分类的类方法,说明,分类已经加载过了,数据都已经写入。
结论:懒加载类配合懒加载分类加载时,是在第一次调用方法的时候进行加载,即消息发送的时候,在lookUpImpOrForward
的时候,由于类没有初始化,所以会调用realizeClassMaybeSwiftAndLeaveLocked
进行相关的初始化加载。
总结
-
- 非懒加载类+非懒加载分类:都在
read_images
中加载
- 类在
realizeClassWithoutSwift
中 - 分类在
addUnattachedCategoryForClass
remethodizeClass
attachCategories
写入ro
- 非懒加载类+非懒加载分类:都在
-
- 非懒加载类+懒加载分类:都在
read_images
中加载
- 类在
realizeClassWithoutSwift
中 - 分类在
realizeClassWithoutSwift
写入ro
- 非懒加载类+懒加载分类:都在
-
- 懒加载类+非懒加载分类:在
load_images
中加载
prepare_load_methods
realizeClassWithoutSwift
remethodizeClass
attachCategories
写入ro
- 懒加载类+非懒加载分类:在
-
- 懒加载类+懒加载分类:在第一次调用类的方法的时候加载
lookUpImpOrForward
realizeClassMaybeSwiftAndLeaveLocked
realizeClassMaybeSwiftMaybeRelock
realizeClassWithoutSwift
直接给data
的ro
赋值
分类的懒加载在编译时就确定,非懒加载在运行时确定。
Tips
动态关联对象
我们前言中提到,如何给TPerson
添加在TPerson
的分类中添加了一个cateProp
的字符串属性,但是当我们在main
函数中运行如下代码,就会崩溃:
TPerson *per = [TPerson alloc];
per.cateProp = @"分类的属性";
NSLog(@"---%@----", per.cateProp);
-[TPerson setCateProp:]: unrecognized selector sent to instance
提示我们TPerson
没有为cateProp
实现set
方法,那么我们如何实现它的set/get
方法呢?
使用关联引用,我们就可以实现set/get
方法:
- (void)setCateProp:(NSString *)cateProp {
objc_setAssociatedObject(self, @"cateProp", cateProp, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)cateProp {
return objc_getAssociatedObject(self, @"cateProp");
}
运行代码,该分类的属性可以正常使用。