深入类的本质



一、实例对象的本质

每个对象都是由类创建出来的,每个Object-C对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后都需要有一个“*”字符。

例如:NSString *someString =@"someObject";

该变量someString中存放的是这个字符串对象在内存中的地址,而NSString自身的数据就存放于该地址中,众所周知,对象所占的内存总是分配在“堆空间”(heap space)中,而指向该实例的变量总会被分配在“栈”(stack)上,他们此时在内存中的布局如下:


如果把对象所需的内存分配在栈上,则编译器会报错。

例如:NSString someString =@"someObject";

会报如下错误:Interface type cannot be statically allocated,翻译过来就是接口类型不能静态分配,可以理解为无法为该变量分配内存。

二、通用对象类型id

对于通用的对象类型id,由于其本身已经是指针来,所以就无需跟上*字符,于是就有:

id someString = @"someObject";

这种写法虽然简洁,但由于没有在对象声明时指定具体类型,所以在其调用本身没有的方法时,编译器并不会报错。  描述Object-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也是定义在这里:

typeof struct obj_object{
 Class isa;
} *id;

三、isa指针与Class类

由类型id的定义可知,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为“isa”指针,例如刚才的例子中所用的对象@"someObject"是一个(isa)NSString,isa指针就指向NSString,借此也可以说明类也是有类型的(Class isa这句代码本身就足以说明了),也就是说类本身也是一个对象,该对象的类型是Class,叫做类对象,而通过类创建的对象就叫类型的对象。例如有一个Person类,类中有一个类方法test(),那么就可以:

[Person test];
Class c =[[[Person alloc] init] class];
[c test];

大致也可以说Class是所有类的类型。Class对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char*name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
};

此结构体中存放类的“元数据”(metadata),例如类的实例实现类几个方法,具备多少个实例变量等信息。结构体的首个变量也是isa指针,这说明Class本身也为Object-C对象。结构体里还有个变量叫super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。前面我们一直所学的“类方法”就定义在此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”也仅有一个与之相关的“元类”。

假设有个名为someClass的子类从NSObject中继承而来(个人觉得NSObject类似于java中的final类),则其继承体系如下:


结构体中的super_class指针确立了类对象与元类之间的继承关系,而isa指针描述了实例所属的类。这就是所谓的“类继承体系”了。

四、load方法

大多数时候,类必须先执行某些初始化操作后才能正常使用。在Object-C中,绝大多数类都继承自NSObject这个根类,而该类的load方法可以用来实现类的加载,其原型如下:

 +(void)load

对于加入运行期系统中的每个类及分类来说,必定会调用此方法,而且近调用一次。当包含类或分类的程序被载入系统时,就会执行该方法。如果分类和其所属的类都定义类load方法,则先调用类,再调用分类。

在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定要先执行,然而根据某个给定的程序库,却无法判断出其中各个类的载入顺序(也就是是否在执行代码load方法之前这些类已经加载好了),因此在load方法中使用其他类是不安全的(NSLog除外,因为Foundation框架肯定在运行咯啊的方法之前就已经载入系统了)。

例如:

#import <Foundation/Foundation.h>
#import “ClassA.h”

@interface ClassB : NSObject
@end

@implementation ClassB
+ (void)load
{
	ClassA *a = [ClassA new];
	NSLog(@”ClassB---loading”);
}
@end



在类B中的load方法里使用了类A,无法确定类A是否已经在类B加载之前就已经加载好了。

load方法并不像普通方法那样,它不遵从那套继承规则,如果某个类本身没有实现load方法,那么不管其各级超类是否实现了该方法,系统都不会调用,此外,分类和其所属的类都有可能出现load方法,此时两个load方法都会被调用,类的实现要比分类的实现先执行

代码示例:

//父类:
#import <Foundation/Foundation.h>

@interface Person : NSObject
@end
	#import "Person.h"

@implementation Person
//程序启动时会加载所有类,加载完毕后会调用load方法,只调用一次
+(void)load
{
    NSLog(@"Person--load");
}
@end
//子类:
#import "Person.h"

@interface Student : Person

@end

#import "Student.h"

@implementation Student
+(void)load
{
    NSLog(@"Student--load");
}
@end

//分类:
#import "Person.h"

@interface Person (wj)

@end

#import "Person+wj.h"

@implementation Person (wj)
+ (void)load
{
    NSLog(@"Person--分类——load");
}
@end

//实例子类:
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Person+wj.h"
#import "Student.h"


int main()
{   
    [[Student alloc] init];
    return 0;
}



执行结果如下


五、initialize方法

想执行与类相关的初始化操作,就的重写以下方法:

+ (voi)initialize

对于每个类来说,该方法会在程序首次使用该类之前调用,且只调用一次,它是由运行期系统来调用的,而绝不应该通过代码直接调用。该方法虽然与load相似,但还是有几点不同:

(1)如果这个类一直没有使用,那么initialize方法就一直不会运行,也就是说应用程序无需先把每个类的initialize都执行一遍,而load方法不同,应用程序必须阻塞并等着所有类的load都执行完才能继续。

(2)只有执行完initialize的那个线程可以操作类或类的实例,其他线程都要先阻塞等着initialize执行完。

(3) initialize方法与其他消息一样,如果某个类未实现他,而其超类实现了,那么就会运行超类的实现代码

  例如:

父类:
#import <Foundation/Foundation.h>

@interface BaseClass : NSObject
@end

#import "BaseClass.h"

@implementation BaseClass
+ (void)initialize
{
    NSLog(@"%@ initialize",self);// 谁调用就打印谁
}
@end
子类:
#import <Foundation/Foundation.h>
#import "BaseClass.h"

@interface SubClass : BaseClass
@property int number;
@end
#import "SubClass.h"

@implementation SubClass
//并未实现initialize方法
@end

实例子类并使用该类:
#import <Foundation/Foundation.h>
#import "SubClass.h"

int main()
{
    SubClass *s = [[SubClass alloc] init];
    s.number = 10;

    return 0;
}



  执行结果如下:


所以可以看出initialize也是遵循通常的继承规则,所以初始化BaseClass时,BaseClass中定义的initialize方法也要运行一遍,而当初始化子类SubClass时,由于该类并未重写此方法,所以还要把父类的实现代码再运行一遍,鉴于此,通常都会这么来实现initialize方法:

+ (void)initialize
{
    if(self == [BaseClass class])
    {
        NSLog(@"%@--initialized",self);
    }
}

加上检测语句后,只有当期望的那个类载入系统时,才会执行相关的初始化操作,执行的结果就只有一条记录:


(这也是MJ老师漏讲的一点)

六、总结

(1)定义指向对象的变量,该变量所需的内存分配在栈上,而对象则分配在堆中

(2)每个对象中都有一个isa指针指向它的父类

(3)类也是一种对象,称为类对象,它的类型称为Class,获取类对象的方式有:

Class c = [Person class];//调用类方法获得

Person *p = [Person new];

Class c1 = [p class];// 调用对象方法获得

用类对象来调用类方法有:

[c test];

或者

[c1 test];

(4)类对象中有个super_class的变量,它定义了类对象的超类,类对象的类型为metaclass(元类),类方法的定义就存在于该类中

(5)在程序启动的时候会加载所有的类和分类,并调用所有类和分类的+load方法

(6)先加载父类,再加载子类;也就是先调用父类的+load,再调用子类的+load

(7)先加载元原始类,再加载分类

(8)不管程序运行过程有没有用到这个类,都会调用+load加载

(9)在第一次使用某个类时(比如创建对象等),就会调用一次+initialize方法

(10)一个类只会调用一次+initialize方法,先调用父类的,再调用子类的

(11)若子类重写initialize方法则会调用父类的initialize方法。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值