用了这么长时间的 Objective-C 语言, 怎么来”肤浅”描述下什么是 Objective-C 呢?, Objective-C 是一个 C 语言的超集, 整个语言是在 C 语言的基础上添加了一层预处理机制和强大运行时系统. 说到运行时系统, 整个语言精华就在这个这里, 它给整个语言增加了强大的且功能齐全的面向对象的编程接口, 函数式编程环境和神奇的动态类型系统.

今天我们就简单聊下 Objective-C 的动态类型, 废话不多说, 我们就说下体现这个特性其中三个方面.

  • Object Introspection (对象在运行时获取其类型的能力称为Introspection)
  • Packaging static C types with NSValue
  • Bridging with Core Foundation objects

Object Introspection

自省 (Introspection) 是对象揭示自己作为一个运行时对象的详细信息的一种能力, 例如确定一个 Objective-C 的对象类型, 通过 isKindOfClass 方法便可轻松确定.

- (void)testObjectType:(id)obj
{
    if ([obj isKindOfClass: [NSNumber class]]) {
	// do something with number...
    } else if ([obj isKindOfClass: [NSValue class]) {
	// do something with values...
    }
}

为什么需要这个类型自省机制呢? 在一个应用程序中需要通过键值编码来判断一个对象类型, 例如 Core Animation的诸多动画属性…

  • anchorPoint
  • backgroundColor
  • backgroundFilters
  • borderColor
  • borderWidth
  • bounds
  • compositingFilter
  • contents
  • contentsRect
  • cornerRadius
  • doubleSided
  • filters
  • frame
  • hidden
  • mask
  • masksToBounds
  • opacity
  • position
  • shadowColor
  • shadowOffset
  • shadowOpacity
  • shadowRadius
  • sublayers
  • sublayerTransform
  • transform
  • zPosition

看似这么多属性, 其实这些属性类型仅仅包括 CGPoint, CGRect, CGFloat, CGImageRef, CGColorRefBOOL 这几种. 每种类型都有自己的类型值实现. 幸运是的 Objective-C 语言的动态类型系统允许我们传递一个泛型类型 id, 并在运行时再来确定这个泛型类型的具体类型, id仅仅是一个 void 指针. Objective-C 的对象这个结构体中具有一个 isa 指针, 指向定义这个对象的类对象.这个类对象包含了 Objective-C 对象的方法调度表, 属性和协议等信息, 这也是 Objective-C 动态能力的根源.

NSObject 可以简单看作为这样:

@interface NSObject <NSObject> {
     Class    isa;
}

同样用 C 语言结构体可表示为:

struct NSObject{
   Class isa;
 }

Class 的 typedef 定义:

typedef struct objc_class *Class;

这样话 NSObject 可以看作为:

struct NSObject{
  objc_class *isa
}

再进一层看下 objc_class 便可直观的知道 isa 最终指向的信息:

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;
 }

想进一步了解 Objective-C 对象模型的可以出门右转, 看下 @唐巧 写的[Objective-C对象模型及应用][Objective-C对象模型及应用].

Packaging static C types with NSValue

我们之前说了 Objective-C 仅仅是对 C 语言添加了一层运行时皮肤, 所以经常看到一些底层实现大量使用了 int, float, pointerstruct等 C 基础数据类型. 然而这些基础数据类型似乎违反了 Objective-C 的动态类型特性.幸运是, Apple 提供了 NSValue 容器来包装这些基础数据类型, 它可以容纳任何 C 类型, 例如 int, float,char, pointer, struct等. 它不仅包含这些基础数据类型作为一个 Objective-C 对象, 并且还包含了这些类型的原始信息.

创建一个 NSValue 对象, 你需要传递一个数据的指针, 以及由 @encode() 生成的数据类型信息.

CGPoint origin = CGPointMake(0,0);
NSValue * originValue = [NSValue valueWithBytes:&origin objCType:@encode(CGPoint)];

简单的来说 @encode() 就是可以把 Objective-C 的数据类型, 甚至是自定义类型, 函数或方法的元类型表示为 C 字符串(const char*), 关于 Type encodings 可以看看 Apple 的Objective-C runtime type encodings.

举上几个栗子瞧瞧:

@encoding(int ** )
// ==> "{^^i}"
@encoding(CGPoint)
// ==> "{CGPoint=ff}"
@encoding(CGColorRef)
// ==> "^{CGColor=}"
@encoding(NSObject)
// ==> "{NSObject=#}"

确定这种类型编码, 我们仅仅通过几步便可确定:

if ([obj isKindOfClass:[NSValue class]]) {
    NSValue* value = (NSValue*) obj;
    if strcmp([value objCType], @encode(CGPoint)) == 0) {
	CGPoint origin;
	[value getValue:&origin];
    }
}

UIKit addition to NSValue

Apple为我们提供了 UIKit Category, 可以以简便的方法来创建一个 NSValue, Link:Category for NSValue

Bridging with Core Foundation objects

虽然 NSValue 可以帮我们包装大部分的数据类型. 但是Core Foundation中的CGColorRef, CGImageRef 等数据类型 NSValue 是无法对其进行数据包装, 这些数据类型需要通过 toll-free briding 来搞定. 关于 toll-free briding 可以看看我的另一篇 [Objective-C ARC下的陷阱和最佳实践][Objective-C ARC下的陷阱和最佳实践].

一个 Core Foundation 的数据类型其实也可以用类似 id 的形式表示. 通过 CFTypeRef 来表式未知的数据类型, 然后通过提供的C函数 CFGetTypeID 便可返回相应数据的真实类型.

if (CFGetTypeID((__bridge CFTypeRef)obj), == CGImageGetTypeID()) {
    CGImageRef imgRef = CFBridgingRetain(obj);
    // do something
    CFRelease(imgRef);
}

一个 CFTypeRef 类似被标记为我们熟悉的 id 类型, 它能接受像 isKindOfType: 的 Objective-C 消息. 因此想判断出一个 id 类型的对象是相当安全的, 只要是 NSObject, NSValue, CFTypeRef, CGColorRef 等其他 Objective-C 对象或 Core Foundation 对象.

References