一、实例对象的本质
每个对象都是由类创建出来的,每个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;
}
执行结果如下:
+ (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方法。