关于Objc中的类和元类——《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》读后感

我觉得EffectiveOC(暂且简称为EffectiveOC)这本书主要针对iOS开发中的一些常见问题和需要注意的地方供一些建议,但是每个点讲的比较深入,不仅告诉你这样做,并且从底层上讲解why,从讲解的why上我对OC有了更多的理解,这里做一点分享,也算做懒人的一种笔记吧.
最近又翻开关于类和元类这个有点鸡生蛋的问题(~~),到底这个元类和类是什么关系呢?我找到那张有名的 class diagram 图的原文,查看了一下。

一、 EffectiveOC目录

优化Objective-C对象之间的互动与关系.
掌握接口与API的设计原则,写出令开发者用起来得心应手的类.
善用协议与分类,编写便于维护且不易出现bug的代码.
在自动引用计数(ARC)环境下避免内存泄漏.
用”块”与”大中枢派发”编写呈模块化且功能强大的代码.
理解Objective-C中的协议与其他编程语言中的多重继承有何区别,并掌握协议的用法.
通过数组、字典、集合等组合对象来提高代码性能.
揭示Cocoa与Cocoa Touch框架的强大之处.

二、 进一步熟悉内存机制

1
NSString *str = [[NSString alloc] initWithString:@"This is a string"];

常说的实例对象其实是指向对象内存地址的指针。
实例对象.png

三、 命名规范

OC的方法名可能很长,但是是为了避免歧义,在命名方面,先要保证表达清楚,没有歧义,然后再考虑长度优化。

四、 进一步理解消息转发机制

OC是一门极其动态语言,在编译器定义好的方法在运行期系统会查找、调用某方法的实现代码,才能真正确定所调用的方法,如果类无法立即响应某个Selector,就会启动消息转发流程。

  1. objc_msgSend传递消息
    objc_msgSend
1
id returnValue = [someObject messageName: parameter];

消息传递调用的核心函数叫做objc_msgSend,编译器会把刚才的方法转换成:

1
id returnValue = objc_msgSend(someObject, @selecor(messageName:), parameter);

objc_msgSend()方法中,主要通过以下步骤来查找和调用函数:
根据对象obj找到对象类中存储的函数列表methodLists。
再根据[email protected](doSth)在methodLists中查找对应的函数指针method_imp
根据函数指针method_imp调用响应的函数。

old_method_list结构体:

1
2
3
4
5
6
struct old_method_list {
void *obsolete; //废弃的属性
int method_count; //方法的个数
/* variable length structure */
struct old_method method_list[1]; //方法的首地址
};

old_method结构体:

1
2
3
4
5
struct old_method {
SEL method_name; //函数的SEL
char *method_types; //函数的类型
IMP method_imp; //函数指针
};

obj->isa(Class类型) obj对象通过isa属性拿到对应的Class
Class->methodLists(old_method_list类型) Class通过methodLists属性拿到存放所有方法的列表
old_method_list->old_method 在old_method_list中通过SEL查找到对应的old_method
old_method->method_imp(IMP类型) old_method通过method_imp属性拿到函数指针
method_imp->调用函数 通过函数指针调用函数
objc_msgSend函数会根据接受者和Selector的类型来调用适当的方法,如果找到与Selector名称相符的方法名,就跳转到该方法的实现代码,如果没有就沿着继承体系继续向上查找,如果还是找不到,就执行消息转发。

  1. 消息转发
    2.1 “动态方法解析”(dynamic method resolution) 查看所属的类是否能动态添加方法,已处理当前的未知选择子(unknown selector).
    2.2 “完整的消息转发机制”(full forwatding mechanism)请接受者看看有没有其他对象能处理这个消息,如果可以就把消息转发给那个对象,如果没有”备援接受者”(replacement receiver)则启动完整的消息转发机制,运行期系统会把消息有关的全部细节封装到NSInvocation对象中,给receiver最后一次机会,设法解决这条未处理的消息.
    消息转发
    Selector是方法选择器,里面存放的是方法的名字。对应方法的映射列表。
    objc_msgSend函数会一句及守着与Selector的类型来调用适当的方法,他会在方法接受者所属类中搜寻方法列表,如果找到了与Selector名称相符的方法。

  2. Method Swizzing
    使用method_exchangeImplemetations(originalMethod, swappedMethod);实现运行时的Selector交换
    methodSwizzing

五、 类和元类

1. 理解类

比起类,可能对象的概念更熟悉一点,这是对象的定义:
对象的结构体

你会发现有一个定义成Class类型的isa,这是实例对象用以表明其所属类型的,指向Class对象的指针。通过Class搭建了类的继承体系(class hirerarchy)。

其实类也是对象,打开定义的头文件,发现是用一个结构体来存储类的信息。

类的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct objc_class *Class;
struct objc_class {
Class isa; // 指向metaclass
Class superclass; // 指向父类Class
const char *name; // 类名
uint32_t version; // 类的版本信息
uint32_t info; // 一些标识信息,标明是普通的Class还是metaclass
uint32_t instance_size; // 该类的实例变量大小(包括从父类继承下来的实例变量);
struct old_ivar_list *ivars; //类中成员变量的信息
struct old_method_list **methodLists; 类中方法列表
Cache cache; 查找方法的缓存,用于提升效率
struct old_protocol_list *protocols; // 存储该类遵守的协议
}

类的结构体存放着该类的信息:类的方法列表,实例变量,协议,父类等信息。
每个类的isa指针指向该类的所属类型元类(metaClass),用来表述类对象的数据。每个类仅有一个类对象,而每个类对象仅有一个与之相关的”元类”。
比如一个继承NSObjct名叫SomeClass的类,其继承体系如下:
类的继承体系
Objective-C中任何的类定义都是对象。即在程序启动的时候任何类定义都对应于一块内存。在编译的时候,编译器会给每一个类生成一个且只生成一个”描述其定义的对象”,也就是水果公司说的类对象(class object),它是一个单例(singleton).
因此,程序里的所有实例对象(instance object)都是在运行时由Objective-C的运行时库生成的,而这个类对象(class object)就是运行时库用来创建实例对象(instance object)的依据。

Programming with Objective-C的说法就是:Classes Are Blueprints for Objects, 类是对象的抽象设计图。

2. 查询类型信息

有时候会需要查询一个”objct”对象的所属的类,有人会这样写:

1
2
3
4
id objct = /* ... */
if ([objct class] == [SomeClass class]) {
//objct is an instance of SomeClass.
}

其实Objective-C中提供了专门用于查询类型信息的方法,由于runtime在运行时的动态性,对于对象所属类的查询,建议使用isKindOfClassisMemberOfClass,因为某些对象可能实现了消息转发功能,从而判断可能不准确.

3. 理解元类(meta class

为了调用类里的方法,类的isa指针必须指向包含这些类方法的类结构体。
这就引出了元类的定义:元类是类对象的类。
简单说就是:
当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
当你给类发消息时,消息是在寻找这个类的元类的方法列表。
元类是必不可少的,因为它存储了类的类方法。每个类都必须有独一无二的元类,因为每个类都有独一无二的类方法。

4.”元类的类”

元类,就像之前的类一样,它也是一个对象。你也可以调用它的方法。自然的,这就意味着他必须也有一个类。
所有的元类都使用根元类(继承体系中处于顶端的类的元类)作为他们的类。这就意味着所有NSObject的子类(大多数类)的元类都会以NSObject的元类作为他们的类
根据这个规则,所有的元类使用根元类作为他们的类,根元类的元类则就是它自己。也就是说基类的元类的isa指针指向他自己。
关于这两点,原文是这样描述的:

A metaclass is an instance of the root class’s metaclass; the root metaclass is itself an instance of the root metaclass.

所谓的元类就是根类的元类的一个实例。

第二点: And the root metaclass’s superclass is the root class,就说名 根元类 (Root Class meta)的父类是 根类 (Root Class class).可以看到图中的 根元类 (Root Class meta)的superclass是指向 根类 (Root Class class)的。

类的图解.png

5.类和元类的继承

类用super_class指针指向了超类,同样的,元类用super_class指向类的super_class的元类。
说的更拗口一点就是,根元类把它自己的基类设置成了super_class
在这样的继承体系下,所有实例、类以及元类(meta class)都继承自一个基类。
这意味着对于继承于NSObject的所有实例、类和元类,他们可以使用NSObject的所有实例方法,类和元类可以使用NSObject的所有类方法
这些文字看起来莫名其妙难以理解,可以用一份图谱来展示这些关系:
类和元类

如上图,对象是由按照类所定义的各个属性和方法“制造”的,类作为对象的模板,也可看成是对象。正如工厂里面的模子也是要专门制作模子的机器生产,元类 (meta class)就是设计、管理 (class)的角色。所以图上直观的表现出类和元类平行的父类链,表明实例方法和类方法都是并行继承的,每个对象都响应了根类的方法。

注意点

需要弄清的有两点:

1. 所谓的元类就是根类的元类的一个实例,而根元类的实例就是它自己。
2. 根元类的父类是根类。

类方法和实例方法

类方法

+开头的方法是类方法。Objc中的类方法类似Java中的static静态方法,它是属于类本身的方法,不属于类的某一个实例对象,所以不需要实例化类,用类名即可使用,是将消息发送给类:

类方法可以独立于对象而执行,所以在其他的语言里面类方法有的时候被称为静态方法。

  1. 类方法可以调用类方法。
  2. 类方法不可以调用实例方法,但是类方法可以通过创建对象来访问实例方法。
  3. 类方法不可以使用任何实例变量。但是类方法可以使用self,因为self不是实例变量。
  4. 类方法作为消息,可以被发送到类或者对象里面去(实际上,就是可以通过类或者对象调用类方法的意思)。

self到底是什么

self的规则大家需要记住下面的规则:

  1. 实例方法里面的self,是对象的首地址。
  2. 类方法里面的self,是Class.

尽管在同一个类里面的使用self,但是self却有着不同的解读。在类方法里面的self,可以翻译成class self;在实例方法里面的self,应该被翻译成为object self。在类方法里面的self和实例方法里面的self有着本质上的不同,尽管他们的名字都叫self

实例方法

-开头的方法是实例方法。它属于类的某一个或某几个实例对象,即类对象必须实例化后才可以使用的方法,将消息发送给实例对象:

类方法和实例方法认知的误区

  1. 类方法常驻内存,所以比实例方法效率高,类方法效率高但占内存
    答:事实上,方法都是一样的,在加载时机和占用内存上,静态方法和实例方法是一样的,在类型第一次被使用时加载。调用的速度基本上没有差别。

  2. 类方法分配在堆上,实例方法分配在栈上。
    答:事实上,所有的方法都不可能分配在堆栈区,方法作为二进制代码是存储在内存的程序代码区,这个内存区域是不可写的。

其他注意

  • 实例方法需要先创建实例才可以调用,比较麻烦,静态方法不用,比较简单。
  • 静态方法是静态绑定到子类,不是被继承。
  • 一般使用频繁的方法用静态方法,用的少的方法用动态的。静态的速度快,占内存。动态的速度相对慢些,但调用完后,立即释放,可以节省内存,可以根据自己的需要选择是用动态方法还是静态方法。
  • 静态方法修改的是类的状态,而对象修改的是各个对象的状态。
  • 类的实例调用是在类的生命周期中存在,当类没有了以后,对应的实例也就没有了,对应的方法也就没有了。静态类不然,只要你引用了那个静态类的命名空间,它就会一直存在,直到我们退出系统。

isa指针

总结

  1. 任何直接或间接继承了NSObject的类,它的实例对象 (instacne object)中都有一个isa指针,指向它的类对象(class object)。这个类对象(class object)中存储了关于这个实例对象(instace object)所属的类的定义的一切:包括变量,方法,遵守的协议等等。
  2. NSObjectisa指针指向所述的类,而类对象(class object)的isa指针指向元类对象(metaClass object),类对象包含了类的实例变量、实例方法的定义,是用来描述该类的对象的信息;元类对象中包含了类的类方法的定义,是用来描述类的信息(类名,版本,类方法).
  3. 元类(meta class)是Class对象的类。每个类(Class)都有自己独一无二的元类(每个类都有自己第一无二的方法列表)。这意味着所有的类对象都不同。所有的元类使用基类的元类作为自己的基类,对于顶层基类的元类也是一样,只是它指向自己而已。
  4. 理解类与元类的关系有利理解OC面向对象的思想,了解类的继承关系。对类这个概念更加熟悉。

参考阅读

  1. Objective-C特性:Runtime
  2. Effective Objective C 2.0
  3. Objective-C Runtime
  4. Objective-C 中的元类(meta class)是什么?
  5. Classes and metaclasses 这篇文章主要为我们阐述在OC面向对象思想中,对象,类和元类的关系,类作为对象的角度去看OC是如何管理对象、类、元类之间的关系的。
  6. Objective-C中的实例方法、类方法、Category、Protocol | 程序员说
文章作者: MichaelMao
文章链接: http://frizzlefur.com/2016/12/05/iOS_类和元类/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下