前面与C++简单的进行了类和对象的对比,比较简单的语法,后续类上会有更复杂的语法,现在老讨论一下OC的内存管理。
1、说明
其实网上对OC的内存管理讨论了很多,这里也仅仅是自己的理解与总结,语言精练简单,可能会有错误。
2、引用计数
看到这个标题,如果了解过C++,那么就会想到C++的智能指针,在C++11中引入了share_ptr。OC与该智能指针在实现思路上的相同点如下:
- 采用引用计数方式
- 存在循环引用的问题
- 采用弱引用解决
于是在学习的时候,我通常会以智能指针为参考。(戳我进相关文章)
现在苹果对与内存管理发布了ARC(Automatic Reference Counting),也就是自动引用计数,在这之前一直是MRC(Manual Reference Counting)。
MRC内存管理遵循以下几个原则:
- 只要是自己new、alloc、copy的对象,都要进行一次release,也就是释放。
- retain某对象,就是把该对象的引用计数增加一,也就是持有者加一。
- retain和release的数量是相等的。
当然还有其他原则,但是我认为仍然是围绕着这些。后来有了ARC之后,释放的操作就交给编译器。于是retain、release等操作就没有了。上述的关键字在后续还会遇到。如果熟悉share_ptr,则会很容易理解这里的内存管理,因此不再赘述。对于关键字在后续property会描述。
前面提到了C++的智能指针,顺便提一提前面说为什么OC中的对象总是以指针的形式。在传递数据的时候有两种方式,一个是传值,一个是引用。
- 指针就是以引用的方式在传递数据,直接传递对象的地址,如果修改或者读取直接读取该对象,减少拷贝的过程,且节省空间。
- 另外,我们的对象通常是创建在堆上的,以C++为例,当传值的时候,首先会创建一个临时的对象,这个过程会调用类的拷贝构造,也就是会有拷贝的过程,但是在堆上申请空间的效率是低下的,而且频繁的创建对象容易产生内存碎片。
3、 对象模型
还记得在C++中如果有虚函数,则该类的成员会增加一个序表指针。(在OC中,我们知道每一个类都会有一个超类)在OC中,每个对象都会有一个isa的指针。在前面之所以说OC更为纯粹,其实在其对象模型上就可以提现出来。(可以参考C++对象模型,但是不同)
写下如下类:
@interface Test : NSObject
{
NSString* name_;
}
@end
@implementation Test
@end
int main(int argc, const char * argv[])
{
Test* test = [Test new];
return 0;
}
于是在调试的过程中,对象有如下成员:
可以发现多了一个来自于NSObject的成员isa。现在可以联想C++的虚表指针,C++的虚表指针指向虚表,然后找到虚函数,但是在这里isa指向的不是一个表,而是一个对象。先看一看isa的类型与作用:(后续会出现很多的源代码,层层深入,耐心阅读)
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
现在看一下Class是什么类型。
typedef struct objc_class *Class;
再找objc_class
。下面代码除了前两行其余都是无效的,后面的是OBJC2_UNAVAILABLE
,也就是说在OC2.0中已经不可用。
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class //指向父类 OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在这里可进入查询新的objc_class类【戳我进入】。
目前已经这样定义了。
struct objc_class : objc_object {
// Class ISA;
Class superclass;//指向父类
cache_t cache; //消息缓存 formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data(){……}
// ....
}
下面把重要的成员列举出来
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
……
这是上述指针的类型
/* Types for `void *' pointers. */
#if __WORDSIZE == 64
# ifndef __intptr_t_defined
typedef long int intptr_t;
# define __intptr_t_defined
# endif
typedef unsigned long int uintptr_t;
#else
# ifndef __intptr_t_defined
typedef int intptr_t;
# define __intptr_t_defined
# endif
typedef unsigned int uintptr_t;
#endif
看一下后面的class_rw_t,rw就是读写的意思,也就是动态添加的一些方法,协议等会在这里进行描述和组织。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;//动态添加的方法列表
property_array_t properties;//动态添加的属性列表
protocol_array_t protocols;//动态添加的协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
这是class_ro_t成员,ro就是readonly的意思,也就是一开始就写好,已经确定的方法、协议,在这里进行描述和组织
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;//已经写好的方法列表
protocol_list_t * baseProtocols;//协议
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
有了上面的基础,带来下面的概念:示例对象,类对象,元类对象
实例对象、类对象、元类对象
isa指针 | isa指针 | isa指针 |
---|---|---|
成员变量 | super指针 | super指针 |
消息缓存 | 类方法 | |
方法、属性、协议 | …… |
4、消息传递
有了上面的基础,就可以梳理消息传递了。复制一张图吧。
下面来说明一下消息传递:
- 元类中isa指针指向根元类,而根元类中isa指针则指向自身。
- 根元类中的superClass指针指向根类,因为根元类是通过继承根类产生的。
为了更好的理解,流程图如下:
- 如果是实例方法就会通过对象的isa指针在类对象中的方法列表中寻找,如果没有,则会通过super指针指向父类,然后在父类中寻找。
- 如果是类方法,则会通过类对象的isa指针在元类的方法列表中寻找,没有找到则会通过super指针在父类中寻找。
- 上述方式如果在根类的方法列表中都没有寻找到,就会导致对象接受到一个无法解析的,也就是找不到实现的,没法执行的方法,这个时候会启动消息转发。
如果该方法只有声明没有实现,那么该方法一定不会放在方法列表中,外界一定不能直接调用到方法
5、消息转发
在上述的消息机制中如果最终没有找到方法实现就会启动消息转发。
例如控制台出现如下的代码,说明已经启动了消息转发:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[某类名 noImplementationMuthod]: unrecognized selector sent to instance 某地址'
消息转发有两个阶段,第一阶段,回进行动态方法解析,也就是说是否有动态添加的方法来与这个无法解析的方法符合。第二阶段则是完整的消息转发机制。
5.1 动态绑定
所谓动态就是在运行时才能确定,动态绑定就是在调用函数的时候,需要在执行的时候再回知道执行的是哪个函数,例如下面的例子:
void leftPrint()
{
NSLog(@"我是第一个函数");
}
void rightPrint()
{
NSLog(@"我是第二个函数");
}
int main(int argc, const char * argv[]) {
void (*funcptr)();
int choice = 0;
while(!choice)
{
NSLog(@"输入1或2选择执行的函数:");
scanf("%d",&choice);
if(choice == 1)
{
funcptr = leftPrint;
}
else if(choice == 2)
{
funcptr = rightPrint;
}
else
{
choice = 0;
NSLog(@"输入非法");
}
}
funcptr();//此时执行的函数就是在执行期选择的函数,动态绑定!
return 0;
}
5.2 动态添加方法
首先需要引入<objc/message.h>头文件,然后是下面的方法。
1)+(BOOL)resolveInstanceMethod:(SEL)sel;
2)+(BOOL)resolveClassMethod:(SEL)sel
3)class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types);
然后举一个使用的例子。当启动消息转发后,会调用第一个方法,所以动态添加方法,需要实现上述的第一个方法。(如果是实例方法是第一个,类方法的话是第二个,下面以实例方法举例子)
+(BOOL)resolveInstanceMethod:(SEL)sel
{
//if(sel == NSSelectorFromString(@"noImplementationMethod"))
if([NSStringFromSelector(sel) isEqualToString:@"noImplementationMethod"])//这两种方式都可以进行判断
{
class_addMethod(self, sel,(IMP)replaceFunc,"v@:");//这里就是我们自己动态的方法
//这里的第一个参数是类对象:self == [self class]
//如果是类方法,则需要传元类对象:object_getClass([类名 class])
return YES;
}//这里可以进行else if来进行分支操作,因为可能动态添加的方法不止一个
return [super resolveInstanceMethod:sel];
}
现在来看一看class_addMethod方法
class_addMethod(self, sel,(IMP)replaceFunc,"v@:")
第一个参数:给谁添加方法,一般都是self,给自己
第二个参数:传入进来的输出参数
第三个参数:自己实现的方法名称,自己实现的方法需要用C语言定义函数的方式
第四个参数:方法签名,描述方法的参数个数、参数类型以及返回值类型。
方法签名组成:
第一个字符:返回值类型
第二个字符:接收者(id):@
第三个字符:选择器(SEL)::
后续字符为参数,还有具体的符号,请戳下面的链接。
【戳我进入】
下面就是实现的函数,C函数,注意格式
。
void replaceFunc(id self, SEL _cmd)
{
NSLog(@"动态添加的方法");
}
除了动态添加方法,还有属性,协议等等。
1)BOOL class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size,uint8_t alignment, const char * _Nullable types)
2)BOOL class_addProtocol(Class _Nullable cls, Protocol * _Nonnull protocol)
3)BOOL class_addProperty(Class _Nullable cls, const char * _Nonnull name, const objc_property_attribute_t * _Nullable attributes,unsigned int attributeCount)
——————————————————————————
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
这种带replace分开来说,class_addMethod方法中的实现会覆盖父类的方法实现,但不影响本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。所以要修改已存在的实现,可以使用class_replaceMethod。
上述的方式可以直接调用的。
[对象 动态添加的方法]
还可以借助performSelector的调用,直接添加动态方法:
class_addMethod([类名 class], @selector(动态添加的方法), class_getMethodImplementation([另一个类名 class], @selector(实现的方法名)), "v@:");
例如:
class_addMethod([Student class], @selector(otherMethod), class_getMethodImplementation([MyClass class], @selector(implemationMethod)), "v@:");
因此,调用的时候可以
[stu performSelector:@selector(otherMethod)];
如果直接调用方法,编译是会自动校验,因此回报错。但是performSelector是运行时系统负责去找方法的,在编译时候不做任何校验。
5.3 完整消息转发
完整的消息转发又分为快速转发和慢速转发。
5.3.1 快速转发
找到一个备用的接收者,【或者说是备胎】
其方法如下:
-(id)forwardingTargetForSelector:(SEL)aSelector
{
if([NSStringFromSelector(aSelector) isEqualToString:@"方法名"])
{
return [备用对象的类 new];
}
return [super forwardingTargetForSelector:aSelector];
}
然后在备用对象的类实现这个方法。
5.3.2 慢速转发
在更广的地方进行处理这个无法解析的方法,类似丢漂流瓶,讲方法签名等消息信息封装在NSInvocation的对象中,最终把消息转发到备用接收者。
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
//返回签名信息
if([NSStringFromSelector(aSelector) isEqualToString:@"noImplementationMethod"])
{
//如果知道方法签名,可以直接放回
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
/*
或者还可以这样
return [self methodSignatureForSelector:aSelector];
return [NSObject methodSignatureForSelector:aSelector];
*/
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL sel = anInvocation.selector;
备用接收者类* 某对象 = [备用接收者类 new];
if([某对象 respondsToSelector:sel])
{
[anInvocation invokeWithTarget:某对象];
}
else
{
[super forwardInvocation:anInvocation];
}
}
5.4 总结
通过上面的消息转发机制,这个消息最终就像邮政寄信一样的有保障,最终都会执行,不会导致程序崩溃。
继上次的流程图,现在可以连接新的流程图就是:
5、后记
消息是OC的一个重要概念,因此对于底层的探究是必要的。而内存管理是每个语言的一个重要部分,对象模型应该在内存管理时一并学习。通过上面的实例对象,类,元类可以对OC的运行时有一个了解,或者说是动态语言有一个了解,并且相对C++,OC的面向对象更纯粹有了体现。后面将会接触一些负责的语法,还会对内存管理进行学习。
【戳我进入本章节示例代码】