我觉得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"]; |
常说的实例对象其实是指向对象内存地址的指针。
三、 命名规范
OC的方法名可能很长,但是是为了避免歧义,在命名方面,先要保证表达清楚,没有歧义,然后再考虑长度优化。
四、 进一步理解消息转发机制
OC是一门极其动态语言,在编译器定义好的方法在运行期系统会查找、调用某方法的实现代码,才能真正确定所调用的方法,如果类无法立即响应某个Selector,就会启动消息转发流程。
- objc_msgSend传递消息
1 | id returnValue = [someObject messageName: parameter]; |
消息传递调用的核心函数叫做objc_msgSend,编译器会把刚才的方法转换成:
1 | id returnValue = objc_msgSend(someObject, @selecor(messageName:), parameter); |
在objc_msgSend()
方法中,主要通过以下步骤来查找和调用函数:
根据对象obj找到对象类中存储的函数列表methodLists。
再根据SEL@selector(doSth)在methodLists
中查找对应的函数指针method_imp
。
根据函数指针method_imp
调用响应的函数。
old_method_list
结构体:
1 | struct old_method_list { |
old_method
结构体:
1 | struct old_method { |
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名称相符的方法名,就跳转到该方法的实现代码,如果没有就沿着继承体系继续向上查找,如果还是找不到,就执行消息转发。
消息转发
2.1 “动态方法解析”(dynamic method resolution
) 查看所属的类是否能动态添加方法,已处理当前的未知选择子(unknown selector).
2.2 “完整的消息转发机制”(full forwatding mechanism
)请接受者看看有没有其他对象能处理这个消息,如果可以就把消息转发给那个对象,如果没有”备援接受者”(replacement receiver
)则启动完整的消息转发机制,运行期系统会把消息有关的全部细节封装到NSInvocation
对象中,给receiver最后一次机会,设法解决这条未处理的消息.Selector
是方法选择器,里面存放的是方法的名字。对应方法的映射列表。objc_msgSend
函数会一句及守着与Selector的类型来调用适当的方法,他会在方法接受者所属类中搜寻方法列表,如果找到了与Selector名称相符的方法。Method Swizzing
使用method_exchangeImplemetations
(originalMethod, swappedMethod
);实现运行时的Selector
交换
五、 类和元类
1. 理解类
比起类,可能对象的概念更熟悉一点,这是对象的定义:
你会发现有一个定义成Class
类型的isa
,这是实例对象用以表明其所属类型的,指向Class
对象的指针。通过Class
搭建了类的继承体系(class hirerarchy
)。
其实类也是对象,打开定义的头文件,发现是用一个结构体来存储类的信息。
1 | typedef struct objc_class *Class; |
类的结构体存放着该类的信息:类的方法列表,实例变量,协议,父类等信息。
每个类的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 | id objct = /* ... */ |
其实Objective-C
中提供了专门用于查询类型信息的方法,由于runtime
在运行时的动态性,对于对象所属类的查询,建议使用isKindOfClass
和isMemberOfClass
,因为某些对象可能实现了消息转发功能,从而判断可能不准确.
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)的。
5.类和元类的继承
类用super_class
指针指向了超类,同样的,元类用super_class
指向类的super_class
的元类。
说的更拗口一点就是,根元类把它自己的基类设置成了super_class
。
在这样的继承体系下,所有实例、类以及元类(meta class
)都继承自一个基类。
这意味着对于继承于NSObject
的所有实例、类和元类,他们可以使用NSObject
的所有实例方法,类和元类可以使用NSObject
的所有类方法
这些文字看起来莫名其妙难以理解,可以用一份图谱来展示这些关系:
如上图,对象是由按照类所定义的各个属性和方法“制造”的,类作为对象的模板,也可看成是对象。正如工厂里面的模子也是要专门制作模子的机器生产,元类
(meta class
)就是设计、管理 类
(class)的角色。所以图上直观的表现出类和元类平行的父类链,表明实例方法和类方法都是并行继承的,每个对象都响应了根类的方法。
注意点
需要弄清的有两点:
1. 所谓的元类就是根类的元类的一个实例,而根元类的实例就是它自己。
2. 根元类的父类是根类。
类方法和实例方法
类方法
以+
开头的方法是类方法。Objc中的类方法类似Java中的static静态方法,它是属于类本身的方法,不属于类的某一个实例对象,所以不需要实例化类,用类名即可使用,是将消息发送给类:
类方法可以独立于对象而执行,所以在其他的语言里面类方法有的时候被称为静态方法。
- 类方法可以调用类方法。
- 类方法不可以调用实例方法,但是类方法可以通过创建对象来访问实例方法。
- 类方法不可以使用任何实例变量。但是类方法可以使用
self
,因为self
不是实例变量。 - 类方法作为消息,可以被发送到类或者对象里面去(实际上,就是可以通过类或者对象调用类方法的意思)。
self
到底是什么
self
的规则大家需要记住下面的规则:
- 实例方法里面的
self
,是对象的首地址。 - 类方法里面的
self
,是Class
.
尽管在同一个类里面的使用self
,但是self
却有着不同的解读。在类方法里面的self
,可以翻译成class self
;在实例方法里面的self
,应该被翻译成为object self
。在类方法里面的self
和实例方法里面的self
有着本质上的不同,尽管他们的名字都叫self
。
实例方法
以-
开头的方法是实例方法。它属于类的某一个或某几个实例对象,即类对象必须实例化后才可以使用的方法,将消息发送给实例对象:
类方法和实例方法认知的误区
类方法常驻内存,所以比实例方法效率高,类方法效率高但占内存
答:事实上,方法都是一样的,在加载时机和占用内存上,静态方法和实例方法是一样的,在类型第一次被使用时加载。调用的速度基本上没有差别。类方法分配在堆上,实例方法分配在栈上。
答:事实上,所有的方法都不可能分配在堆栈区,方法作为二进制代码是存储在内存的程序代码区,这个内存区域是不可写的。
其他注意
- 实例方法需要先创建实例才可以调用,比较麻烦,静态方法不用,比较简单。
- 静态方法是静态绑定到子类,不是被继承。
- 一般使用频繁的方法用静态方法,用的少的方法用动态的。静态的速度快,占内存。动态的速度相对慢些,但调用完后,立即释放,可以节省内存,可以根据自己的需要选择是用动态方法还是静态方法。
- 静态方法修改的是类的状态,而对象修改的是各个对象的状态。
- 类的实例调用是在类的生命周期中存在,当类没有了以后,对应的实例也就没有了,对应的方法也就没有了。静态类不然,只要你引用了那个静态类的命名空间,它就会一直存在,直到我们退出系统。
isa指针
总结
- 任何直接或间接继承了
NSObject
的类,它的实例对象 (instacne object
)中都有一个isa
指针,指向它的类对象(class object
)。这个类对象(class object
)中存储了关于这个实例对象(instace object
)所属的类的定义的一切:包括变量,方法,遵守的协议等等。 NSObject
的isa
指针指向所述的类,而类对象(class object
)的isa
指针指向元类对象(metaClass object
),类对象包含了类的实例变量、实例方法的定义,是用来描述该类的对象的信息;元类对象中包含了类的类方法的定义,是用来描述类的信息(类名,版本,类方法).- 元类(
meta class
)是Class
对象的类。每个类(Class
)都有自己独一无二的元类(每个类都有自己第一无二的方法列表)。这意味着所有的类对象都不同。所有的元类使用基类的元类作为自己的基类,对于顶层基类的元类也是一样,只是它指向自己而已。 - 理解类与元类的关系有利理解OC面向对象的思想,了解类的继承关系。对类这个概念更加熟悉。
参考阅读
- Objective-C特性:Runtime
- Effective Objective C 2.0
- Objective-C Runtime
- Objective-C 中的元类(meta class)是什么?
- Classes and metaclasses 这篇文章主要为我们阐述在OC面向对象思想中,对象,类和元类的关系,类作为对象的角度去看OC是如何管理对象、类、元类之间的关系的。
- Objective-C中的实例方法、类方法、Category、Protocol | 程序员说