Objective-C 2.0编程语言(一) 类与对象

Objective-C

Objective-C是对C的扩充,它最初的设计目的是给C语言一个通过简单且直观的方式而实现面向对象编程的能力。本系列用于自我查疑。

面向对象的程序都是围绕对象而构建,一个对象把数据 和一些对这些数据的操作(即Methods)捆绑在一起,打包成一个独立的编程单元。

例如你正在写一个画图程序,这个程序允许用户创建诸如线、弧线、矩形、文本、位图等,那么你就可以创建关于这些基本图形的很多类。例如一个矩形对象,应该有标识它的位置的实例变量和宽与高的信息,可能还会有别的实例变量来定义它的颜色、矩形是否被填充、直线样式等。这个Rectangle类会有一些方法用来设置一个实例的位置、大小、颜色、填充状态、直线样式,还应该提供一个方法用来绘制自己。
在Objective-C中,Object的实例变量属于Object的内部数据,通常要访问这些数据只能通过对象的方法,你还可以通过作用域指示符(scope directives)为一个类的子类或别的对象指定访问实例变量的权限。要获取对象别的信息,必须提供一个相应的方法,例如,Rectangle类应该有专用的方法来获取它的大小和位置。而且,对象只能看到为它自己而设计的方法,它不会把别的对象的方法运用到自己身上。就像一个函数隐藏局部变量一样,对象不但隐藏实例变量,还会隐藏方法的实现。

一、OC中的类

编译器为每个类定义一个类对象(Class_object)。Class_objectClass的编译版本,而它所构建的对象被称为类的实例。在你的程序中真正做工作的是类在运行时对象创建的那些实例。一个类的所有实例有同一套方法,而且有相同一套实例变量。每个对象都有自己的实例变量,但他们却共享这套方法。根据约定,类名以大写字母开头,如Rectangle,而实例变量通常以小写字母开头,如myRect。

  • 继承(Inheritance)
    类的定义通常是添加式的,一个新的类往往都基于另外一个类,而这个新类继承了原来类的方法和实例变量。新类通常简单地添加实例变量或者修改它所继承的方法,它不需要复制继承的代码。继承将这些类连接成一个只有一个根继承关系树。在OC中,写基于功能框架的代码时,这个根类通常是NSObject。每个类(除了根类)都有一个父类,而每个类,包括根类都可以成为任何数量子类的父类。
  • 抽象类
    有些类被设计为不能实例化,他们只能作为super类被其他类所继承。这些类往往自身是不完整的,而只是包含了一些有用的代码,这些代码能够被子类所继承,从而减少子类的代码量。NSObject类就是一个重要的抽象类。程序中经常会定义NSObject的子类并使用这些子类的实例,但从来没有直接使用这个类的实例的。抽象类通常包含一些帮助定义应用程序架构的代码,当你定义这些类的子类时,这些子类的实例能很好地适应这种应用程序架构,并能够自动地和别的对象协作。由于抽象类只有定义了子类才能成为一个有用的类,因此它们常常被称为抽象超类。
  • 类类型
    类实际上是一类对象的详细说明,定义了一个数据类型。这个类型不但基于它所定义的数据结构,还取决于它所定义的一些行为,这些行为就是方法。因此,类名可以出现在任何C语言允许的类型说明符出现的地方,例如作为sizeof操作符的参数:Int i = sizeof(Rectangle);
  • 静态类型匹配
    Rectangle *myRect;
    这种声明对象的方式为编译器提供了对象种类的信息,所以被称为静态类型匹配,静态类型匹配使编译器具备了一些类型检查功能,例如如果一个对象接收一个没有定义的消息时,可以发出警告,而如果把对象定义为id,就会放松这种限制。尽管这样,它还是无法替代动态绑定的位置。一个对象可以被静态地匹配为它自己的类或者它所继承的类。例如因为继承使得Rectangle成为一种Graphic,所以Rectangle实例可以静态匹配为Graphic类:
    Graphic* myRect;
    这样做之所以合法,是因为Rectangle本身就是一种Graphic,但它由于继承了Shape和Rectangle的特性,所以又不止是一个Graphic。为了类型检查,编译器会把myRect当作Graphic,但在运行时环境中,它会被当作Rectangle对待。
  • 动态类型匹配
    id myRect;
    对象通常都是被定义成指针,上面的静态匹配使得指针的含义更加明确,而id却隐藏了这些信息。
    id类型是一种灵活的数据类型, 只表示它是一个对象,不能为编译器提供例如实例变量,可执行操作等信息。所以每个对象在运行时必须提供这些信息。 而之所以能做到这点, 是因为每个对象都有一个isa 实例变量来标示这个对象所属的类每个Rectangle对象都能告诉运行时系统它是一个矩形类,因此,动态类型匹配实际上发生在程序被执行时。
    不论何时,只要需要,运行时系统就能够查明一个对象到底属于哪个类,而这只需要查询对象的isa实例变量就可以了。这个isa指针还为对象提供了一种称为“自省”(introspection)的功能。编译器会在数据机构中记录关于类定义的信息为运行时环境所用。通过这种机制,你可以判断一个对象是否提供了一个特定的方法,也可以获取这个对象的超类(supperclass)的名字。当然也可以通过静态类型匹配为编译器提供有关对象所属类的信息,这也是为什么类名可以作为类型名而使用了。
  • 类型自省
    实例在运行时可以获取自己的类。例如,NSObject类中定义的isMemberOfClass(或isKindOfClass)方法可以检查接收者是不是特定类的实例(或继承特定类的实例)。

  • 消息语法

ObjC中使用[Receiver message]方式来进行方法调用,本质其实就是向Receiver发送message消息.而message告诉这个Receiver要做什么。

  • 给nil发送消息
    在ObjC中,给nil发送消息是合法的,但在运行时什么都不做,而发送给nil的消息带有返回值也是合法的。
    ■如果一个方法返回一个对象、任何类型的指针、任何size小于或等于sizeof(void*)的类型,如float、 double、 long double、或者long long,那么给nil发送消息将返回0。
    ■如果一个方法返回一个数据结构,那么将这个消息传递给nil将为这个数据结构的每个成员都返回0.0。
    ■如果一个方法返回上述类型外的其它类型,那么将这个消息传递给nil,返回值为定义。
  • 多态动态绑定
    函数调用和消息的一个关键区别是,函数和它的参数是在编译时绑定在一起的,而消息和接收者直到程序运行时,消息被发送才实现这种绑定。因此,响应一个消息的具体方法是在运行时才决定的,而不是在代码被编译的时候。Message启动哪个Method取决于Message的Receiver,不同的Receiver可能有同名的不同Method实体,这就是多态。
    而编译器要为一个Message找到正确的Method实体,它必须知道这个Receiver属于什么对象,而这是一个对象只有在运行时接收到Message后才能够真正获取的信息. 因此Method实体的选择是发生在运行时的。

当Message发出之后,运行时环境会查看Receiver以及它与消息同名的Method,它通过名字匹配找到这个Receiver的Method实体,并调用,同时传递一个指向Receiver实例变量的指针。Message中的方法名是用来选择 Receiver的Method实体,因此,Message中的方法名也被称为(selector)选择器.
Method和Message的动态绑定,以及多态之间的协调合作,给了面向对象编程丰富的灵活性和强大的功能,因为每个对象都可以有一个方法的自己的版本,但仅仅是接收相同消息的对象不同,而这些都可以在程序运行时完成,即不同的对象以各自的方式响应相同的消息.
可以简化编程接口,容许在类与类之间重用一些习惯性的命名.

这样你可以编写应用于任何不同种类对象的代码,而不用去考虑到底是应用到什么样的对象上.
这些对象甚至可以是尚未开发的,或者是由别的项目的程序员开发的.(ps.如果你写了一个向id类型变量发送display消息的代码,任何拥有display方法的对象都可能成为这个消息的接收者。)

  • 方法和选取器:
    选取器确定的是方法名,不是方法的实现, 这是多态和动态绑定的基础.它使得向不同对象发送相同的消息成为现实.
  • 方法返回值和参数类型:
    消息机制是通过选取器找到方法的返回值类型和参数类型. 因此: 动态绑定需要同名方法的实现 拥有相同返回值类型和相同的参数类型;否则,运行时可能出现找不到对应方法的错误.(有一个例外,虽然同名静态方法和实例方法拥有相同的选取器,但是它们可以有不同的参数类型和返回值类型。)
  // SEL和@selector区别:选择器的类型是SEL.而 @selector指示符是用来引用选择器的, 它返回类型是SEL.
    SEL response;      
    response = @selector(load:)

    // 1. 通过字符串来得到选取器:
    responseSEL = NSSelectorFromString(@"loadDataForTableView:");
    //  2 . 通过选择器转换来得到方法名: 
    NSString  *methodName = NSStringFromSelector(responseSEL);

1.1 类对象

一个类的定义会包含丰富的信息(详见附录),但大部分都是关于这个类的实例的,如:
■类名及超类
■关于实例变量的完整描述
■关于方法名及其参数的说明
■方法的实现
所有这些信息都被编译被保存在一个运行时系统能够访问的数据结构中。编译器创建一个而且只创建一个对象来表达这些信息,那就是类对象,因此,在OC中,所有的类自身也是一个对象。
虽然类对象保存了类的属性,但它本身并不是一个类的实例。它没有自己的实例变量,而且它也不能执行为类实例设计的方法。不过,类定义了可以包含只为类对象使用的方法,这就是类方法(静态方法),这些方法不同于实例方法(动态方法)。另外,类对象会继承所有超对象的类方法,就像类实例可以继承所有超类实例的方法一样。
在实际代码中,类名就代表类对象,下面的例子中,Rectangle类使用继承自NSObject的方法返回类的版本号:
Int versionNumber = [Rectangle version];
但只有在作为接收者接收一个消息时,类名才能代表类对象,在别的地方,你必须通过给类实例或给类发送class消息来获得一个类id,如:
Id aClass = [anObject class];
Id rectClass = [Rectangle class];
如上例所述,和所有其它的对象一样,类对象可以被转化为id类型,而且类对象还可以更精确地转化为类类型,如:
Class aClass = [anObject class];
Class rectClass = [Rectangle class];
所有的类都属于类对象,使用Class类型和使用类名进行静态类型匹配是等效的。因此,类对象也像类实例那样,可以进行动态类型匹配、接收消息以及从别的类继承方法。不同之处在于它们是由编译器产生的,没有自己的数据结构,它们是用于运行时系统产生类实例的代理。

  • 创建实例
    类对象的主要用途是用于创建新的实例,下面这个例子告诉Rectangle类创建一个Rectangle实例,并将其赋值给myRect变量:
    id myRect;
    myRect = [Rectangle alloc];
    alloc方法会为这个实例的每个实例变量动态分配内存,并将除连接该实例和它的类的 isa变量外的所有实例变量全部清零,一个对象要能真正派上用场,它还必须完整地初始化,这就是init方法的作用,通常init方法都紧挨着alloc方法,如:
    myRect = [[Rectangle alloc] init];
    在一个实例能够接收消息前,对实例进行这样的初始化是必须的。Alloc方法返回一个新的实例,然后init方法对这个事例进行初始化。每个类对象至少拥有一个方法(如alloc)才能让它具备创建新对象的能力。而每个实例至少一个方法(如init)让它能够有用。
    而初始化方法通常带有一些参数用来传递特定的值,而且用标签来标示这些参数,例如initWithPositon: size:,但不管怎样,它都是以init开头。
  • 变量和类对象
    定义一个类,你可以为它定义实例变量,这个类的每个实例都保留一份这些实例变量的拷贝——每个对象管理自己的数据。
    但是你发现没有,OC中并没有类似实例变量那样的所谓的“类变量”。而且类对象无法访问任何实例的实例变量,它不能初始化、读写这些变量。为了让类的所有实例共享数据,你必须定于外部变量(不是在类中定义的变量)。最简单的办法是像下面的代码片断那样在类的实现文件中声明一个变量:
int MCLSGlobalVariable;
@implementation MyClass
...
@end

更巧妙的做法是,你可以把变量声明为静态的,并提供类方法来管理它。静态变量会把它的作用域限定在类范围,而且仅限于当前文件的类的实现部分。并不像实例变量那样,静态变量不能被继承,不能被子类直接操作。这种特性常常用于定于类的共享实例(如单一模式,参考“创建单例”部分)。静态变量能够帮助类对象提供一种比“工厂模式”创建实例更多的功能,而且更接近一个完整和通用的对象。

static MyClass *MCLSSharedInstance;
@implementation MyClass
+ (MyClass *)sharedInstance
{
// check for existence of shared instance
// create if necessary
return MCLSSharedInstance;
}
// implementation continues
  • 初始化类对象
    如果类对象不分配实例,它就可以向类的实例那样初始化,尽管程序并不为类对象分配内存,但Objective-C确实也提供了一个初始化类对象的途径,如果类使用静态或全局变量,那么initialize是一个初始化这些变量的好地方。
    在类接受任何其它消息前,运行时环境会给类对象发送一个initialize消息,当然这发生在它的超类收到initialize消息之后。这就给这个类一个在被使用前建立运行时环境的机会。
    如果没有初始化工作要做,你就没有必要实现一个initialize方法来响应这个消息。因为继承关系的存在,给一个没有实现initialize的类发送initialize消息,这个消息将会被传递到它的超类,即使超类已经受到这个消息也是如此。
    例如,A类实现了initialize方法,而类B继承自类A,但类B没有实现initialize方法,在类B接收它的第一个消息之前,运行时环境会为它发送一个initialize消息。但由于类B没有实现initialize方法,类A的initialize方法会被执行,因此,类A需要确保它的初始化逻辑只被执行一次。为了确保初始化逻辑只执行一次,请使用如下的模板实现initialize方法:
+ (void)initialize
{
  static BOOL initialized = NO;
  if (!initialized) {
  // Perform initialization here.
  ...
  initialized = YES;
  }
}
  • 根类方法
    所有的类和实例, 都需要有一个和runtime环境交互的接口。 类对象和实例都必须对自己有能力完成自省,并能够汇报自己在继承关系中所处的位置,所以要实现两次:一次为实例提供可以和运行时环境交互的接口,另一次在类对象中复制这些接口。 而NSObject会提供这些接口,所以NSObject不需要实现两次。类对象扮演另外一个角色,就是执行根类中定义的实例方法。当类对象接收一个不能响应消息时,运行时环境会判断是否有根实例方法可以响应这个消息。类对象能够执行的唯一的实例方法就是那些在根类中定义的方法,而且还必须是没有类方法能完成这项工作时才会由根类方法来完成。有关类方法执行实例方法的特殊能力,请参考有关NSObject类的说明。
  • 代码中的类名
    在代码中,类名职能在两种不同的上下文环境中出现,这两种情况也反映了类名作为数据类型和对象的双重角色:
    Rectangle * anObject;
    这里anObject被明确地定义为指向Rectangle的一个指针。编译器会认为它具有Rectangle的数据结构和Rectangle的实例方法以及Rectangle所继承的方法。静态类型匹配可以让编译器做到更好的类型检查并使得代码更加清晰。但只有实例才能静态地进行类型匹配,类对象不能,因为它们不属于某个类的一种。
    而在发送消息的表达式中,作为消息的接收者,类名代表的是类对象,这种用法在前面的例子中已经多次展示过。只有作为消息接收者,类名才能代表一个类对象。在任何别的上下文环境中,必须要先通过发送一个class消息,获取类对象的id,例如:
    if ( [anObject isKindOfClass:[Rectangle class]] )
    如果你不知道在编译时类名称,仅仅是用“ Rectangle”这个名称作为参数,NSClassFromString将返回类对象:
    NSString *className;
    if ( [anObject isKindOfClass:NSClassFromString(className)] )
    如果className字符串不是一个有效的类名,返回nil。类名和全局变量存在同一个nameSpace,具有唯一性。

附录1:

类的基础数据结构:

typedef struct objc_class *Class;
struct objc_class { // 类的数据结构
    Class isa  OBJC_ISA_AVAILABILITY; // 
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
    long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif
} OBJC2_UNAVAILABLE;

类实例数据结构:

struct objc_object {

    Class isa  OBJC_ISA_AVAILABILITY;

};
typedef struct objc_object *id;

可以看到,这个结构体只有一个字体,即指向其类的isa指针。因此id其实就是指向Class类型的指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。
当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。
另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

  1. isa:
    isa指针指向其类.
    NSArray *array = [NSArray array];
    显而易见, array的数据结构中isa指针指向NSArray。
    而其实在这个例子中,+array消息发送给了NSArray类,这个NSArray也是一个对象,类对象。它也包含一个指向其类的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?即metaClass元类。 meta-class是一个类对象的类。当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
    meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
    如果你有想法,再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。
  2. super_class:
    指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
  3. cache:
    用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去 methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。结构如下:
struct objc_cache {
/* mask :一个整数,指定分配的缓存bucket的总数。在方法查找过程中,
Objective-C runtime使用这个字段来确定开始线性查找数组的索引位
置。指向方法selector的指针与该字段做一个AND位操作(index = 
(mask & selector))。这可以作为一个简单的hash散列算法。*/
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
  /*occupied:一个整数,指定实际占用的缓存bucket的总数。*/
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
  /*buckets:指向Method数据结构指针的数组。这个数组可能包含不
超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓
存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数
组可能会随着时间而增长。*/
    Method buckets[1]                                        OBJC2_UNAVAILABLE;

};
  1. version:
    我们可以使用这个字段来提供类的版本信息。

附录2

类与对象操作函数

runtime提供了大量的函数来直接操作类与对象数据结构,对照附录1。类的操作方法大部分是以class为前缀的,而对象的操作方法大部分是以objc或object_为前缀。下面我们将根据这些方法的用途来分类讨论这些方法的使用。

1. 类相关操作函数

我们可以回过头去看看objc_class的定义,runtime提供的操作类的方法主要就是针对这个结构体中的各个字段的。

类名(name)

类名操作的函数主要有:
// 获取类的类名
const char * class_getName ( Class cls );

  • 对于class_getName函数,如果传入的cls为Nil,则返回一个char字符串。

父类(super_class)和元类(meta-class)

父类和元类操作的函数主要有:
// 获取类的父类
Class class_getSuperclass ( Class cls );
// 判断给定的Class是否是一个元类
BOOL class_isMetaClass ( Class cls );

  • class_getSuperclass函数,当cls为Nil或者cls为根类时,返回Nil。不过通常我们可以使用NSObject类的superclass方法来达到同样的目的。
  • class_isMetaClass函数,如果是cls是元类,则返回YES;如果否或者传入的cls为Nil,则返回NO。

实例变量大小(instance_size)

实例变量大小操作的函数有:
// 获取实例大小
size_t class_getInstanceSize ( Class cls );

成员变量(ivars)及属性

在objc_class中,所有的成员变量、属性的信息是放在链表ivars中的。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针。runtime提供了丰富的函数来操作这一字段。大体上可以分为以下几类:

  1. 成员变量操作函数,主要包含以下函数:
    // 获取类中指定名称实例成员变量的信息
    Ivar class_getInstanceVariable ( Class cls, const char *name );
    // 获取类成员变量的信息
    Ivar class_getClassVariable ( Class cls, const char *name );
    // 添加成员变量
    BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
    // 获取整个成员变量列表
    Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
  • class_getInstanceVariable函数,它返回一个指向包含name指定的成员变量信息的objc_ivar结构体的指针(Ivar)。
  • class_getClassVariable函数,目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。
  • Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的提供的类,还是我们自定义的类,都无法动态添加成员变量。但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能在objc_allocateClassPair函数与objc_registerClassPair之间调用。另外,这个类也不能是元类。成员变量的按字节最小对齐量是1<<alignment。这取决于ivar的类型和机器的架构。如果变量的类型是指针类型,则传递log2(sizeof(pointer_type))。
  • class_copyIvarList函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。
  1. 属性操作函数,主要包含以下函数:
    // 获取指定的属性
    objc_property_t class_getProperty ( Class cls, const char *name );
    // 获取属性列表
    objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
    // 为类添加属性
    BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
    // 替换类的属性
    void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
    这一种方法也是针对ivars来操作,不过只操作那些是属性的值。我们在后面介绍属性时会再遇到这些函数。

  2. 在MAC OS X系统中,我们可以使用垃圾回收器。runtime提供了几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理strong/weak引用。这几个函数定义如下:
    const uint8_t * class_getIvarLayout ( Class cls );
    void class_setIvarLayout ( Class cls, const uint8_t *layout );
    const uint8_t * class_getWeakIvarLayout ( Class cls );
    void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );
    但通常情况下,我们不需要去主动调用这些方法;在调用objc_registerClassPair时,会生成合理的布局。在此不详细介绍这些函数。

方法(methodLists)

方法操作主要有以下函数:
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );
// 获取所有方法的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 类实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );

  • class_addMethod的实现会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现,可以使用method_setImplementation。一个Objective-C方法是一个简单的C函数,它至少包含两个参数—self和_cmd。所以,我们的实现函数(IMP参数指向的函数)至少需要两个参数,如下所示:
    void myMethodIMP(id self, SEL _cmd)
    {
    // implementation ....
    }
    与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否已存在。

另外,参数types是一个描述传递给方法的参数类型的字符数组,这就涉及到类型编码,我们将在后面介绍。

  • class_getInstanceMethod、class_getClassMethod函数,与class_copyMethodList不同的是,这两个函数都会去搜索父类的实现。
  • class_copyMethodList函数,返回包含所有实例方法的数组,如果需要获取类方法,则可以使用class_copyMethodList(object_getClass(cls), &count)(一个类的实例方法是定义在元类里面)。该列表不包含父类实现的方法。outCount参数返回方法的个数。在获取到列表后,我们需要使用free()方法来释放它。
  • class_replaceMethod函数,该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现。
  • class_getMethodImplementation函数,该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数的指针。这个函数会比method_getImplementation(class_getInstanceMethod(cls, name))更快。返回的函数指针可能是一个指向runtime内部的函数,而不一定是方法的实际实现。例如,如果类实例无法响应selector,则返回的函数指针将是运行时消息转发机制的一部分。
  • class_respondsToSelector函数,我们通常使用NSObject类的respondsToSelector:或instancesRespondToSelector:方法来达到相同目的。

协议(objc_protocol_list)

协议相关的操作包含以下函数:
// 添加协议
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回类是否实现指定的协议
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回类实现的协议列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );

  • class_conformsToProtocol函数可以使用NSObject类的conformsToProtocol:方法来替代。
  • class_copyProtocolList函数返回的是一个数组,在使用后我们需要使用free()手动释放。

版本(version)

版本相关的操作包含以下函数:
// 获取版本号
int class_getVersion ( Class cls );
// 设置版本号
void class_setVersion ( Class cls, int version );

其它

runtime还提供了两个函数来供CoreFoundation的tool-free bridging使用,即:
Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );
通常我们不直接使用这两个函数。

2. 实例相关操作函数

实例操作函数主要是针对我们创建的实例对象的一系列操作函数,我们可以使用这组函数来从实例对象中获取我们想要的一些信息,如实例对象中变量的值。这组函数可以分为三小类:

  1. 针对整个对象进行操作的函数,这类函数包含
    // 返回指定对象的一份拷贝
    id object_copy ( id obj, size_t size );
    // 释放指定对象占用的内存
    id object_dispose ( id obj );
    有这样一种场景,假设我们有类A和类B,且类B是类A的子类。类B通过添加一些额外的属性来扩展类A。现在我们创建了一个A类的实例对象,并希望在运行时将这个对象转换为B类的实例对象,这样可以添加数据到B类的属性中。这种情况下,我们没有办法直接转换,因为B类的实例会比A类的实例更大,没有足够的空间来放置对象。此时,我们就要以使用以上几个函数来处理这种情况,如下代码所示:
    NSObject *a = [[NSObject alloc] init];
    id newB = object_copy(a, class_getInstanceSize(MyClass.class));
    object_setClass(newB, MyClass.class);
    object_dispose(a);

  2. 针对对象实例变量进行操作的函数,这类函数包含:
    // 修改类实例的实例变量的值
    Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
    // 获取对象实例变量的值
    Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
    // 返回指向给定对象分配的任何额外字节的指针
    void * object_getIndexedIvars ( id obj );
    // 返回对象中实例变量的值
    id object_getIvar ( id obj, Ivar ivar );
    // 设置对象中实例变量的值
    void object_setIvar ( id obj, Ivar ivar, id value );
    如果实例变量的Ivar已经知道,那么调用object_getIvar会比object_getInstanceVariable函数快,相同情况下,object_setIvar也比object_setInstanceVariable快。

  3. 针对对象的类进行操作的函数,这类函数包含:
    // 返回给定对象的类名
    const char * object_getClassName ( id obj );
    // 返回对象的类
    Class object_getClass ( id obj );
    // 设置对象的类
    Class object_setClass ( id obj, Class cls );

获取类定义

Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。runtime提供了一系列函数来获取类定义相关的信息,这些函数主要包括:
// 获取已注册的类定义的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList ( unsigned int *outCount );
// 返回指定类的类定义
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
// 返回指定类的元类
Class objc_getMetaClass ( const char *name );

  • objc_getClassList函数:获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自NSObject体系的,所以在这些类上调用方法是,都应该先检测一下这个方法是否在这个类中实现。
    下面代码演示了该函数的用法:
int numClasses;
Class * classes = NULL;
numClasses = objc_getClassList(NULL, 0);
if (numClasses > 0) {
    classes = malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);
    NSLog(@"number of classes: %d", numClasses);
    for (int i = 0; i < numClasses; i++) {
    Class cls = classes[i];
    NSLog(@"class name: %s", class_getName(cls));
  }
  free(classes);
}

输出结果如下:
2014-10-23 16:20:52.589 RuntimeTest[8437:188589] number of classes: 1282
2014-10-23 16:20:52.589 RuntimeTest[8437:188589] class name: DDTokenRegexp
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: _NSMostCommonKoreanCharsKeySet
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: OS_xpc_dictionary
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSFileCoordinator
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSAssertionHandler
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: PFUbiquityTransactionLogMigrator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSNotification
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSKeyValueNilSetEnumerator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: OS_tcp_connection_tls_session
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: _PFRoutines
......还有大量输出

获取类定义的方法有三个:objc_lookUpClass, objc_getClass和objc_getRequiredClass。如果类在运行时未注册,则objc_lookUpClass会返回nil,而objc_getClass会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass函数的操作与objc_getClass相同,只不过如果没有找到类,则会杀死进程。

  • objc_getMetaClass函数:如果指定的类没有注册,则该函数会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容