南峰子的技术博客

攀登,一步一个脚印,方能知其乐


  • 首页

  • 知识小集

  • Swift

  • Objective-C

  • Cocoa

  • 翻译

  • 源码分析

  • 杂项

  • 归档

Foundation: NSObject Class

发表于 2015-01-31   |   分类于 Cocoa

Objective-C中有两个NSObject,一个是NSObject类,另一个是NSObject协议。而其中NSObject类采用了NSObject协议。在本文中,我们主要整理一下NSObject类的使用。

说到NSObject类,写Objective-C的人都应该知道它。它是大部分Objective-C类继承体系的根类。这个类提供了一些通用的方法,对象通过继承NSObject,可以从其中继承访问运行时的接口,并让对象具备Objective-C对象的基本能力。以下我们就来看看NSObejct提供给我们的一些基础功能。

+load与+initialize

这两个方法可能平时用得比较少,但很有用。在我们的程序编译后,类相关的数据结构会保留在目标文件中,在程序运行后会被解析和使用,此时类的信息会经历加载和初始化两个过程。在这两个过程中,会分别调用类的load方法和initialize方法,在这两个方法中,我们可以适当地做一些定制处理。不当是类本身,类的分类也会经历这两个过程。对于一个类,我们可以在类的定义中重写这两个方法,也可以在分类中重写它们,或者同时重写。

load方法

对于load方法,当Objective-C运行时加载类或分类时,会调用这个方法;通常如果我们有一些类级别的操作需要在加载类时处理,就可以放在这里面,如为一个类执行Swizzling Method操作。

load消息会被发送到动态加载和静态链接的类和分类里面。不过,只有当我们在类或分类里面实现这个方法时,类/分类才会去调用这个方法。

在类继承体系中,load方法的调用顺序如下:

  1. 一个类的load方法会在其所有父类的load方法之后调用
  2. 分类的load方法会在对应类的load方法之后调用

在load的实现中,如果使用同一库中的另外一个类,则可能是不安全的,因为可能存在的情况是另外一个类的load方法还没有运行,即另一个类可能尚未被加载。另外,在load方法里面,我们不需要显示地去调用[super load],因为父类的load方法会自动被调用,且在子类之前。

在有依赖关系的两个库中,被依赖的库中的类其load方法会优先调用。但在库内部,各个类的load方法的调用顺序是不确定的。

initialize方法

当我们在程序中向类或其任何子类发送第一条消息前,runtime会向该类发送initialize消息。runtime会以线程安全的方式来向类发起initialize消息。父类会在子类之前收到这条消息。父类的initialize实现可能在下面两种情况下被调用:

  1. 子类没有实现initialize方法,runtime将会调用继承而来的实现
  2. 子类的实现中显示的调用了[super initialize]

如果我们不想让某个类中的initialize被调用多次,则可以像如下处理:

1
2
3
4
5
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}

因为initialize是以线程安全的方式调用的,且在不同的类中initialize被调用的顺序是不确定的,所以在initialize方法中,我们应该做少量的必须的工作。特别需要注意是,如果我们initialize方法中的代码使用了锁,则可能会导致死锁。因此,我们不应该在initialize方法中实现复杂的初始化工作,而应该在类的初始化方法(如-init)中来初始化。

另外,每个类的initialize只会被调用一次。所以,如果我们想要为类和类的分类实现单独的初始化操作,则应该实现load方法。

如果想详细地了解这两个方法的使用,可以查看《Effectiveobjc 2.0》的第51条,里面有非常详细的说明。如果想更深入地了解这两个方法的调用,则可以参考objc库的源码,另外,NSObject的load和initialize方法一文从源码层面为我们简单介绍了这两个方法。

对象的生命周期

一说到对象的创建,我们会立即想到[[NSObject alloc] init]这种经典的两段式构造。对于这种两段式构造,唐巧大神在他的”谈ObjC对象的两段构造模式“一文中作了详细描述,大家可以参考一下。

本小节我们主要介绍一下与对象生命周期相关的一些方法。

对象分配

NSObject提供的对象分配的方法有alloc和allocWithZone:,它们都是类方法。这两个方法负责创建对象并为其分配内存空间,返回一个新的对象实例。新的对象的isa实例变量使用一个数据结构来初始化,这个数据结构描述了对象的信息;创建完成后,对象的其它实例变量被初始化为0。

alloc方法的定义如下:

1
+ (instancetype)alloc

而allocWithZone:方法的存在是由历史原因造成的,它的调用基本上和alloc是一样的。既然是历史原因,我们就不说了,官方文档只给了一句话:

1
This method exists for historical reasons; memory zones are no longer used byobjc.

我们只需要知道alloc方法的实现调用了allocWithZone:方法。

对象初始化

我们一般不去自己重写alloc或allocWithZone:方法,不用去关心对象是如何创建、如何为其分配内存空间的;我们更关心的是如何去初始化这个对象。上面提到了,对象创建后,isa以外的实例变量都默认初始化为0。通常,我们希望将这些实例变量初始化为我们期望的值,这就是init方法的工作了。

NSObject类默认提供了一个init方法,其定义如下:

1
- (instancetype)init

正常情况下,它会初始化对象,如果由于某些原因无法完成对象的创建,则会返回nil。注意,对象在使用之前必须被初始化,否则无法使用。不过,NSObject中定义的init方法不做任何初始化操作,只是简单地返回self。

当然,我们定义自己的类时,可以提供自定义的初始化方法,以满足我们自己的初始化需求。需要注意的就是子类的初始化方法需要去调用父类的相应的初始化方法,以保证初始化的正确性。

讲完两段式构造的两个部分,有必要来讲讲NSObject类的new方法了。

new方法实际上是集alloc和init于一身,它创建了对象并初始化了对象。它的实现如下:

1
2
3
+ (instancetype)new {
return [[self alloc] init];
}

new方法更多的是一个历史遗留产物,它源于NeXT时代。如果我们的初始化操作只是调用[[self alloc] init]时,就可以直接用new来代替。不过如果我们需要使用自定义的初始化方法时,通常就使用两段式构造方式。

拷贝

说到拷贝,相信大家都很熟悉。拷贝可以分为“深拷贝”和“浅拷贝”。深拷贝拷贝的是对象的值,两个对象相互不影响,而浅拷贝拷贝的是对象的引用,修改一个对象时会影响到另一个对象。

在Objective-C中,如果一个类想要支持拷贝操作,则需要实现NSCopying协议,并实现copyWithZone:【注意:NSObject类本身并没有实现这个协议】。如果一个类不是直接继承自NSObject,则在实现copyWithZone:方法时需要调用父类的实现。

虽然NSObject自身没有实现拷贝协议,不过它提供了两个拷贝方法,如下:

1
- (id)copy

这个是拷贝操作的便捷方法。它的返回值是NSCopying协议的copyWithZone:方法的返回值。如果我们的类没有实现这个方法,则会抛出一个异常。

与copy对应的还有一个方法,即:

1
- (id)mutableCopy

从字面意义来讲,copy可以理解为不可变拷贝操作,而mutableCopy可以理解为可变操作。这便引出了拷贝的另一个特性,即可变性。

顾名思义,不可变拷贝即拷贝后的对象具有不可变属性,可变拷贝后的对象具有可变属性。这对于数组、字典、字符串、URL这种分可变和不可变的对象来说是很有意义的。我们来看如下示例:

1
2
3
NSMutableArray *mutableArray = [NSMutableArray array];
NSMutableArray *array = [mutableArray copy];
[array addObject:@"test1"];

实际上,这段代码是会崩溃的,我们来看看崩溃日志:

1
2
-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070'

从中可以看出,经过copy操作,我们的array实际上已经变成不可变的了,其底层元类是__NSArrayI。这个类是不支持addObject:方法的。

偶尔在代码中,也会看到类似于下面的情况:

1
@property (copy) NSMutableArray *array;

这种属性的声明方式是有问题的,即上面提到的可变性问题。使用self.array = **赋值后,数组其实是不可变的,所以需要特别注意。

mutableCopy的使用也挺有意思的,具体的还请大家自己去试验一下。

释放

当一个对象的引用计数为0时,系统就会将这个对象释放。此时runtime会自动调用对象的dealloc方法。在ARC环境下,我们不再需要在此方法中去调用[super dealloc]了。我们重写这个方法主要是为了释放对象中用到的一些资源,如我们通过C方法分配的内存空间。dealloc方法的定义如下:

1
- (void)dealloc

需要注意的是,我们不应该直接去调用这个方法。这些事都让runtime去做吧。

消息发送

Objective-C中对方法的调用并不是像C++里面那样直接调用,而是通过消息分发机制来实现的。这个机制核心的方法是objc_msgSend函数。消息机制的具体实现我们在此不做讨论,可以参考Objective-C Runtime 运行时之三:方法与消息。

对于消息的发送,除了使用[obj method]这种机制之外,NSObject类还提供了一系列的performSelector**方法。这些方法可以让我们更加灵活地控制方法的调用。接下来我们就来看看这些方法的使用。

在线程中调用方法

如果我们想在当前线程中调用一个方法,则可以使用以下两个方法:

1
2
3
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes

这两个方法会在当前线程的Run loop中设置一个定时器,以在delay指定的时间之后执行aSelector。如果我们希望定时器运行在默认模式(NSDefaultRunLoopMode)下,可以使用前一个方法;如果想自己指定Run loop模式,则可以使用后一个方法。

当定时器启动时,线程会从Run loop的队列中获取到消息,并执行相应的selector。如果Run loop运行在指定的模式下,则方法会成功调用;否则,定时器会处于等待状态,直到Run loop运行在指定模式下。

需要注意的是,调用这些方法时,Run loop会保留方法接收者及相关的参数的引用(即对这些对象做retain操作),这样在执行时才不至于丢失这些对象。当方法调用完成后,Run loop会调用这些对象的release方法,减少对象的引用计数。

如果我们想在主线程上执行某个对象的方法,则可以使用以下两个方法:

1
2
3
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array

我们都知道,iOS中所有的UI操作都需要在主线程中处理。如果想在某个二级线程的操作完成之后做UI操作,就可以使用这两个方法。

这两个方法会将消息放到主线程Run loop的队列中,前一个方法使用的是NSRunLoopCommonModes运行时模式;如果想自己指定运行模式,则使用后一个方法。方法的执行与之前的两个performSelector方法是类似的。当在一个线程中多次调用这个方法将不同的消息放入队列时,消息的分发顺序与入队顺序是一致的。

方法中的wait参数指定当前线程在指定的selector在主线程执行完成之后,是否被阻塞住。如果设置为YES,则当前线程被阻塞。如果当前线程是主线程,而该参数也被设置为YES,则消息会被立即发送并处理。

另外,这两个方法分发的消息不能被取消。

如果我们想在指定的线程中分发某个消息,则可以使用以下两个方法:

1
2
3
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array

这两个方法基本上与在主线程的方法差不多。在此就不再讨论。

如果想在后台线程中调用接收者的方法,可以使用以下方法:

1
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

这个方法会在程序中创建一个新的线程。由aSelector表示的方法必须像程序中的其它新线程一样去设置它的线程环境。

当然,我们经常看到的performSelector系列方法中还有几个方法,即:

1
2
3
- (id)performSelector:(SEL)aSelector
- (id)performSelector:(SEL)aSelector withObject:(id)anObject
- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

不过这几个方法是在NSObject协议中定义的,NSObject类实现了这个协议,也就定义了相应的实现。这个我们将在NSObject协议中来介绍。

取消方法调用请求

对于使用performSelector:withObject:afterDelay:方法(仅限于此方法)注册的执行请求,在调用发生前,我们可以使用以下两个方法来取消:

1
2
3
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument

前一个方法会取消所以接收者为aTarget的执行请求,不过仅限于当前run loop,而不是所有的。

后一个方法则会取消由aTarget、aSelector和anArgument三个参数指定的执行请求。同样仅限于当前run loop。

消息转发及动态解析方法

当一个对象能接收一个消息时,会走正常的方法调用流程。但如果一个对象无法接收一个消息时,就会走消息转发机制。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发

具体流程可参考Objective-C Runtime 运行时之三:方法与消息,《Effectiveobjc 2.0》一书的第12小节也有详细描述。在此我们只介绍一下NSObject类为实现消息转发提供的方法。

首先,对于动态方法解析,NSObject提供了以下两个方法来处理:

1
2
+ (BOOL)resolveClassMethod:(SEL)name
+ (BOOL)resolveInstanceMethod:(SEL)name

从方法名我们可以看出,resolveClassMethod:是用于动态解析一个类方法;而resolveInstanceMethod:是用于动态解析一个实例方法。

我们知道,一个Objective-C方法是其实是一个C函数,它至少带有两个参数,即self和_cmd。我们使用class_addMethod函数,可以给类添加一个方法。我们以resolveInstanceMethod:为例,如果要给对象动态添加一个实例方法,则可以如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void dynamicMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically))
{
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSel];
}

其次,对于备用接收者,NSObject提供了以下方法来处理:

1
- (id)forwardingTargetForSelector:(SEL)aSelector

该方法返回未被接收消息最先被转发到的对象。如果一个对象实现了这个方法,并返回一个非空的对象(且非对象本身),则这个被返回的对象成为消息的新接收者。另外如果在非根类里面实现这个方法,如果对于给定的selector,我们没有可用的对象可以返回,则应该调用父类的方法实现,并返回其结果。

最后,对于完整转发,NSObject提供了以下方法来处理

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

当前面两步都无法处理消息时,运行时系统便会给接收者最后一个机会,将其转发给其它代理对象来处理。这主要是通过创建一个表示消息的NSInvocation对象并将这个对象当作参数传递给forwardInvocation:方法。我们在forwardInvocation:方法中可以选择将消息转发给其它对象。

在这个方法中,主要是需要做两件事:

  1. 找到一个能处理anInvocation调用的对象。
  2. 将消息以anInvocation的形式发送给对象。anInvocation将维护调用的结果,而运行时则会将这个结果返回给消息的原始发送者。

这一过程如下所示:

1
2
3
4
5
6
7
8
9
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL aSelector = [invocation selector];
if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}

当然,对于一个非根类,如果还是无法处理消息,则应该调用父类的实现。而NSObject类对于这个方法的实现,只是简单地调用了doesNotRecognizeSelector:。它不再转发任何消息,而是抛出一个异常。doesNotRecognizeSelector:的声明如下:

1
- (void)doesNotRecognizeSelector:(SEL)aSelector

运行时系统在对象无法处理或转发一个消息时会调用这个方法。这个方法引发一个NSInvalidArgumentException异常并生成一个错误消息。

任何doesNotRecognizeSelector:消息通常都是由运行时系统来发送的。不过,它们可以用于阻止一个方法被继承。例如,一个NSObject的子类可以按以下方式来重写copy或init方法以阻止继承:

1
2
3
4
- (id)copy
{
[self doesNotRecognizeSelector:_cmd];
}

这段代码阻止子类的实例响应copy消息或阻止父类转发copy消息–虽然respondsToSelector:仍然报告接收者可以访问copy方法。

当然,如果我们要重写doesNotRecognizeSelector:方法,必须调用super的实现,或者在实现的最后引发一个NSInvalidArgumentException异常。它代表对象不能响应消息,所以总是应该引发一个异常。

获取方法信息

在消息转发的最后一步中,forwardInvocation:参数是一个NSInvocation对象,这个对象需要获取方法签名的信息,而这个签名信息就是从methodSignatureForSelector:方法中获取的。

该方法的声明如下:

1
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

这个方法返回包含方法描述信息的NSMethodSignature对象,如果找不到方法,则返回nil。如果我们的对象包含一个代理或者对象能够处理它没有直接实现的消息,则我们需要重写这个方法来返回一个合适的方法签名。

对应于实例方法,当然还有一个处理类方法的相应方法,其声明如下:

1
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector

另外,NSObject类提供了两个方法来获取一个selector对应的方法实现的地址,如下所示:

1
2
- (IMP)methodForSelector:(SEL)aSelector
+ (IMP)instanceMethodForSelector:(SEL)aSelector

获取到了方法实现的地址,我们就可以直接将IMP以函数形式来调用。

对于methodForSelector:方法,如果接收者是一个对象,则aSelector应该是一个实例方法;如果接收者是一个类,则aSelector应该是一个类方法。

对于instanceMethodForSelector:方法,其只是向类对象索取实例方法的实现。如果接收者的实例无法响应aSelector消息,则产生一个错误。

测试类

对于类的测试,在NSObject类中定义了两个方法,其中类方法instancesRespondToSelector:用于测试接收者的实例是否响应指定的消息,其声明如下:

1
+ (BOOL)instancesRespondToSelector:(SEL)aSelector

如果aSelector消息被转发到其它对象,则类的实例可以接收这个消息而不会引发错误,即使该方法返回NO。

为了询问类是否能响应特定消息(注意:不是类的实例),则使用这个方法,而不使用NSObject协议的实例方法respondsToSelector:。

NSObject还提供了一个方法来查看类是否采用了某个协议,其声明如下:

1
+ (BOOL)conformsToProtocol:(Protocol *)aProtocol

如果一个类直接或间接地采用了一个协议,则我们可以说这个类实现了该协议。我们可以看看以下这个例子:

1
2
3
4
5
@protocol AffiliationRequests <Joining>
@interface MyClass : NSObject <AffiliationRequests, Normalization>
BOOL canJoin = [MyClass conformsToProtocol:@protocol(Joining)];

通过继承体系,MyClass类实现了Joining协议。

不过,这个方法并不检查类是否实现了协议的方法,这应该是程序员自己的职责了。

识别类

NSObject类提供了几个类方法来识别一个类,首先是我们常用的class类方法,该方法声明如下:

1
+ (Class)class

该方法返回类对象。当类是消息的接收者时,我们只通过类的名称来引用一个类。在其它情况下,类的对象必须通过这个方法类似的方法(-class实例方法)来获取。如下所示:

1
BOOL test = [self isKindOfClass:[SomeClass class]];

NSObject还提供了superclass类方法来获取接收者的父类,其声明如下:

1
+ (Class)superclass

另外,我们还可以使用isSubclassOfClass:类方法查看一个类是否是另一个类的子类,其声明如下:

1
+ (BOOL)isSubclassOfClass:(Class)aClass

描述类

描述类是使用description方法,它返回一个表示类的内容的字符串。其声明如下:

1
+ (NSString *)description

我们在LLDB调试器中打印类的信息时,使用的就是这个方法。

当然,如果想打印类的实例的描述时,使用的是NSObject协议中的实例方法description,我们在此不多描述。

​

归档操作

一说到归档操作,你会首先想到什么呢?我想到的是NSCoding协议以及它的两个方法:

initWithCoder:和encodeWithCoder:。如果我们的对象需要支持归档操作,则应该采用这个协议并提供两个方法的具体实现。

在编码与解码的过程中,一个编码器会调用一些方法,这些方法允许将对象编码以替代一个更换类或实例本身。这样,就可以使得归档在不同类层次结构或类的不同版本的实现中被共享。例如,类簇能有效地利用这一特性。这一特性也允许每个类在解码时应该只维护单一的实例来执行这一策略。

NSObject类虽然没有采用NSCoding协议,但却提供了一些替代方法,以支持上述策略。这些方法分为两类,即通用和专用的。

通用方法由NSCoder对象调用,主要有如下几个方法和属性:

1
2
3
4
@property(readonly) Class classForCoder
- (id)replacementObjectForCoder:(NSCoder *)aCoder
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder

专用的方法主要是针对NSKeyedArchiver对象的,主要有如下几个方法和属性:

1
2
3
4
5
@property(readonly) Class classForKeyedArchiver
+ (NSArray *)classFallbacksForKeyedArchiver
+ (Class)classForKeyedUnarchiver
- (id)replacementObjectForKeyedArchiver:(NSKeyedArchiver *)archiver

子类在归档的过程中如果有特殊的需求,可以重写这些方法。这些方法的具体描述,可以参考官方文档。

在解码或解档过程中,有一点需要考虑的就是对象所属类的版本号,这样能确保老版本的对象能被正确地解析。NSObject类对此提供了两个方法,如下所示:

1
2
+ (void)setVersion:(NSInteger)aVersion
+ (NSInteger)version

它们都是类方法。默认情况下,如果没有设置版本号,则默认是0.

总结

NSObject类是Objective-C中大部分类层次结构中的根类,并为我们提供了很多功能。了解这些功能更让我们更好地发挥Objective-C的特性。

参考

  1. NSObject Class Reference
  2. Archives and Serializations Programming Guide
  3. NSObject的load和initialize方法
  4. Objective-C Runtime 运行时之三:方法与消息
  5. 《Effectiveobjc 2.0》

LLDB调试器使用简介

发表于 2015-01-25   |   分类于 杂项

随着Xcode 5的发布,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。LLDB为Xcode提供了底层调试环境,其中包括内嵌在Xcode IDE中的位于调试区域的控制面板,在这里我们可以直接调用LLDB命令。如图1所示:

图1:位于Xcode调试区域的控制台

image

在本文中,我们主要整理一下LLDB调试器提供给我们的调试命令,更详细的内容可以查看The LLDB Debugger。

LLDB命令结构

在使用LLDB前,我们需要了解一下LLDB的命令结构及语法,这样可以尽可能地挖掘LLDB的潜能,以帮助我们更充分地利用它。

LLDB命令的语法有其通用结构,通常是以下形式的:

1
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]

其中:

  1. <command>(命令)和<subcommand>(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
  2. <action>:我们想在前面的命令序列的上下文中执行的一些操作。
  3. <options>:行为修改器(action modifiers)。通常带有一些值。
  4. <argument>:根据使用的命令的上下文来表示各种不同的东西。

LLBD命令行的解析操作在执行命令之前完成。上面的这些元素之间通过空格来分割,如果某一元素自身含有空格,则可以使用双引用。而如果元素中又包含双引号,则可以使用反斜杠;或者元素使用单引号。如下所示:

1
2
(lldb) command [subcommand] -option "some \"quoted\" string"
(lldb) command [subcommand] -option 'some "quoted" string'

这种命令解析设计规范了LLDB命令语法,并对所有命令做了个统一。

命令选项

LLDB中的命令选项有规范形式和缩写形式两种格式。以设置断点的命令breakpoint set为例,以下列表了其部分选项的格式,其中括号中的是规范形式:

1
2
3
4
5
6
7
8
breakpoint set
-M <method> ( --method <method> )
-S <selector> ( --selector <selector> )
-b <function-name> ( --basename <function-name> )
-f <filename> ( --file <filename> )
-l <linenum> ( --line <linenum> )
-n <function-name> ( --name <function-name> )
…

各选项的顺序是任意的。如果后面的参数是以”-“开头的,则在选项后面添加”–”作为选项的终止信号,以告诉LLDB我们处理的选项的正确位置。如下命令所示:

1
(lldb) process launch --stop-at-entry -- -program_arg_1 value -program_arg_2 value

如上所示,命令的选项是--stop-at-entry,参数是-program_arg_1和-program_arg_2,我们使用”–”将选项与参数作一下区分。

原始命令

LLDB命令解析器支持”原始(raw)”命令,即没有命令选项,命令字符串的剩余部分未经解析就传递给命令。例如,expression就是一个原始命令。

不过原始命令也可以有选项,如果命令字符串中有虚线,则在命令名与命令字符串之间放置一个选项结束符(–)来表明没有命令标记。

我们可以通过help命令的输出来查看一个命令是否是原始命令。

命令补全(Command Completion)

LLDB支持源文件名,符号名,文件名,等等的命令补全(Commmand Completion)。终端窗口中的补全是通过在命令行中输入一个制表符来初始化的。Xcode控制台中的补全与在源码编辑器中的补全方式是一样的:补全会在第三个字符被键入时自动弹出,或者通过Esc键手动弹出。

一个命令中的私有选项可以有不同的完成者(completers)。如breakpoint中的--file <path>选项作为源文件的完成者,--shlib <path>选项作为当前加载的库的完成者,等等。这些行为是特定的,例如,如果指定--shlib <path>,且以--file <path>结尾,则LLDB只会列出由--shlib <path>指定的共享类库。

Python脚本

对于高级用户来说,LLDB有一个内置的Python解析器,可以通过脚本命令来访问。调试器中的所有特性在Python解析器中都可以作为类来访问。这样,我们就可以使用LLDB-Python库来写Python函数,并通过脚本将其加载到运行会话中,以执行一些更复杂的调试操作。

在命令行中调试程序

通常我们都是在Xcode中直接使用LLDB调试器,Xcode会帮我们完成很多操作。当然,如果我们想让自己看着更Bigger,或者想了解下调试器具体的一些流程,就可以试试直接在终端使用LLDB命令来调试程序。在终端中使用LLDB调试器,我们需要了解以下内容:

  1. 加载程序以备调试
  2. 将一个运行的程序绑定到LLDB
  3. 设置断点和观察点
  4. 控制程序的执行
  5. 在调试的程序中导航
  6. 检查状态和值的变量
  7. 执行替代代码

了解在终端中这些操作是如何进行的,可以帮助我们更深入的了解调试器在Xcode中是如何运作的。下面我们分步来介绍一下。

指定需要调试的程序

首先我们需要设置需要调试的程序。我们可以使用如下命令做到这一点:

1
2
$ lldb /Projects/Sketch/build/Debug/Sketch.app
Current executable set to '/Projects/Sketch/build/Debug/Sketch.app' (x86_64).

或者在运行lldb后,使用file命令来处理,如下所示:

1
2
3
$ lldb
(lldb) file /Projects/Sketch/build/Debug/Sketch.app
Current executable set to '/Projects/Sketch/build/Debug/Sketch.app' (x86_64).

设置断点

在设置完程序后,我们可能想设置一点断点来调试程序。此时我们可以使用breakpoint set命令来设置断点,这个命令简单、直观,且有智能补全,接下来我们看看它的具体操作。

如果想在某个文件中的某行设置一个断点,可使用以下命令:

1
(lldb) breakpoint set --file foo.c --line 12

如果想给某个函数设置断点,可使用以下命令:

1
(lldb) breakpoint set --name foo

如果想给C++中所有命名为foo的方法设置断点,可以使用以下命令:

1
(lldb) breakpoint set --method foo

如果想给Objective-C中所有命名为alignLeftEdges:的选择器设置断点,则可以使用以下命令:

1
(lldb) breakpoint set --selector alignLeftEdges:

我们可以使用--shlib <path>来将断点限定在一个特定的可执行库中:

1
(lldb) breakpoint set --shlib foo.dylib --name foo

看吧,断点设置命令还是很强大的。

如果我们想查看程序中所有的断点,则可以使用breakpoint list命令,如下所示:

1
2
3
4
(lldb) breakpoint list
Current breakpoints:
1: name = 'alignLeftEdges:', locations = 1, resolved = 1
1.1: where = Sketch`-[SKTGraphicView alignLeftEdges:] + 33 at /Projects/Sketch/SKTGraphicView.m:1405, address = 0x0000000100010d5b, resolved, hit count = 0

从上面的输出结果可以看出,一个断点一般有两部分:

  1. 断点的逻辑规范,这一部分是用户提供给breakpoint set命令的。
  2. 与规范匹配的断点的位置。

如上所示,通过"breakpoint set --selector alignLeftEdges:"设置的断点,其信息中会显示出所有alignLeftEdges:方法的位置。

breakpoint list命令输出列表显示每个逻辑断点都有一个整数标识,如上所示断点标识为1。而每个位置也会有一个标识,如上所示的1.1。

输出列表中另一个信息是断点位置是否是已解析的(resolved)。这个标识表示当与之相关的文件地址被加载到程序进行调试时,其位置是已解析的。例如,如果在共享库中设置的断点之后被卸载了,则断点的位置还会保留,但其不能再被解析。

不管是逻辑断点产生的所有位置,还是逻辑断点解析的任何特定位置,我们都可以使用断点触发命令来对其进行删除、禁用、设置条件或忽略计数操作。例如,如果我们想添加一个命令,以在LLDB命中断点1.1时打印跟踪栈,则可以执行以下命令

1
2
3
4
(lldb) breakpoint command add 1.1
Enter your debugger command(s). Type 'DONE' to end.
> bt
> DONE

如果想更详细地了解"breakpoint command add"命令的使用,可以使用help帮助系统来查看。

设置观察点

作为断点的补充,LLDB支持观察点以在不中断程序运行的情况下监测一些变量。例如,我们可以使用以下命令来监测名为global的变量的写操作,并在(global==5)为真时停止监测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(lldb) watch set var global
Watchpoint created: Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
(lldb) watch modify -c '(global==5)'
(lldb) watch list
Current watchpoints:
Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
condition = '(global==5)'
(lldb) c
Process 15562 resuming
(lldb) about to write to 'global'...
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped
* thread #1: tid = 0x1c03, 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16, stop reason = watchpoint 1
frame #0: 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16
13
14 static void modify(int32_t &var) {
15 ++var;
-> 16 }
17
18 int main(int argc, char** argv) {
19 int local = 0;
(lldb) bt
* thread #1: tid = 0x1c03, 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16, stop reason = watchpoint 1
frame #0: 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16
frame #1: 0x0000000100000eac a.out`main + 108 at main.cpp:25
frame #2: 0x00007fff8ac9c7e1 libdyld.dylib`start + 1
(lldb) frame var global
(int32_t) global = 5
(lldb) watch list -v
Current watchpoints:
Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
condition = '(global==5)'
hw_index = 0 hit_count = 5 ignore_count = 0
(lldb)

可以使用help watchpoint来查看该命令的使用。

使用LLDB来启动程序

一旦指定了调试哪个程序,并为其设置了一些断点后,就可以开始运行程序了。我们可以使用以下命令来启动程序:

1
2
3
(lldb) process launch
(lldb) run
(lldb) r

我们同样可以使用进程ID或进程名来连接一个已经运行的程序。当使用名称来连接一个程序时,LLDB支持--waitfor选项。这个选项告诉LLDB等待下一个名称为指定名称的程序出现,然后连接它。例如,下面3个命令都是用于连接Sketch程序(假定其进程ID为123):

1
2
3
(lldb) process attach --pid 123
(lldb) process attach --name Sketch
(lldb) process attach --name Sketch --waitfor

启动或连接程序后,进程可能由于某些原因而停止,如:

1
2
3
4
5
6
(lldb) process attach -p 12345
Process 46915 Attaching
Process 46915 Stopped
1 of 3 threads stopped with reasons:
* thread #1: tid = 0x2c03, 0x00007fff85cac76a, where = libSystem.B.dylib`__getdirentries64 + 10,
stop reason = signal = SIGSTOP, queue = com.apple.main-thread

注意“1 of 3 threads stopped with reasons:”及其下面一行。在多线程环境下,在内核实际返回控制权给调试器前,可能会有多个线程命中同一个断点。在这种情况下,我们可以在停止信息中看到所有因此而停止的线程。

控制程序

启动程序后,LLDB允许程序在到达断点前继续运行。LLDB中流程控制的命令都在thread命令层级中。如下所示:

1
2
3
(lldb) thread continue
Resuming thread 0x2c03 in process 46915
Resuming process 46915

另外,还有以下命令:

1
2
3
4
5
(lldb) thread step-in // The same as "step" or "s" in GDB.
(lldb) thread step-over // The same as "next" or "n" in GDB.
(lldb) thread step-out // The same as "finish" or "f" in GDB.
(lldb) thread step-inst // The same as "stepi" / "si" in GDB.
(lldb) thread step-over-inst // The same as "nexti" / "ni" in GDB.

LLDB还提供了run until line按步调度模式,如:

1
lldb) thread until 100

这条命令会运行线程,直到当前frame到达100行。如果代码在运行的过程中跳过了100行,则当frame被弹出栈后终止执行。

查看线程状态

在进程停止后,LLDB会选择一个当前线程和线程中当前帧(frame)。很多检测状态的命令可以用于这个线程或帧。

为了检测进程的当前状态,可以从以下命令开始:

1
2
3
4
5
(lldb) thread list
Process 46915 state is Stopped
* thread #1: tid = 0x2c03, 0x00007fff85cac76a, where = libSystem.B.dylib`__getdirentries64 + 10, stop reason = signal = SIGSTOP, queue = com.apple.main-thread
thread #2: tid = 0x2e03, 0x00007fff85cbb08a, where = libSystem.B.dylib`kevent + 10, queue = com.apple.libdispatch-manager
thread #3: tid = 0x2f03, 0x00007fff85cbbeaa, where = libSystem.B.dylib`__workq_kernreturn + 10

星号(*)表示thread #1为当前线程。为了获取线程的跟踪栈,可以使用以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) thread backtrace
thread #1: tid = 0x2c03, stop reason = breakpoint 1.1, queue = com.apple.main-thread
frame #0: 0x0000000100010d5b, where = Sketch`-[SKTGraphicView alignLeftEdges:] + 33 at /Projects/Sketch/SKTGraphicView.m:1405
frame #1: 0x00007fff8602d152, where = AppKit`-[NSApplication sendAction:to:from:] + 95
frame #2: 0x00007fff860516be, where = AppKit`-[NSMenuItem _corePerformAction] + 365
frame #3: 0x00007fff86051428, where = AppKit`-[NSCarbonMenuImpl performActionWithHighlightingForItemAtIndex:] + 121
frame #4: 0x00007fff860370c1, where = AppKit`-[NSMenu performKeyEquivalent:] + 272
frame #5: 0x00007fff86035e69, where = AppKit`-[NSApplication _handleKeyEquivalent:] + 559
frame #6: 0x00007fff85f06aa1, where = AppKit`-[NSApplication sendEvent:] + 3630
frame #7: 0x00007fff85e9d922, where = AppKit`-[NSApplication run] + 474
frame #8: 0x00007fff85e965f8, where = AppKit`NSApplicationMain + 364
frame #9: 0x0000000100015ae3, where = Sketch`main + 33 at /Projects/Sketch/SKTMain.m:11
frame #10: 0x0000000100000f20, where = Sketch`start + 52

如果想查看所有线程的调用栈,则可以使用以下命令:

1
(lldb) thread backtrace all

查看调用栈状态

检查帧参数和本地变量的最简便的方式是使用frame variable命令:

1
2
3
4
5
6
7
(lldb) frame variable
self = (SKTGraphicView *) 0x0000000100208b40
_cmd = (struct objc_selector *) 0x000000010001bae1
sender = (id) 0x00000001001264e0
selection = (NSArray *) 0x00000001001264e0
i = (NSUInteger) 0x00000001001264e0
c = (NSUInteger) 0x00000001001253b0

如果没有指定任何变量名,则会显示所有参数和本地变量。如果指定参数名或变量名,则只打印指定的值。如:

1
2
(lldb) frame variable self
(SKTGraphicView *) self = 0x0000000100208b40

frame variable命令不是一个完全的表达式解析器,但它支持一些简单的操作符,如&,*,->,[]。这个数组括号可用于指针,以将指针作为数组处理。如下所示:

1
2
3
4
5
6
7
8
9
10
11
(lldb) frame variable *self
(SKTGraphicView *) self = 0x0000000100208b40
(NSView) NSView = {
(NSResponder) NSResponder = {
...
(lldb) frame variable &self
(SKTGraphicView **) &self = 0x0000000100304ab
(lldb) frame variable argv[0]
(char const *) argv[0] = 0x00007fff5fbffaf8 "/Projects/Sketch/build/Debug/Sketch.app/Contents/MacOS/Sketch"

frame variable命令会在变量上执行”对象打印”操作。目前,LLDB只支持Objective-C打印,使用的是对象的description方法。

如果想查看另外一帧,可以使用frame select命令,如下所示:

1
2
(lldb) frame select 9
frame #9: 0x0000000100015ae3, where = Sketch`function1 + 33 at /Projects/Sketch/SKTFunctions.m:11

小结

以上所介绍的命令可以让我们在终端中直接调试程序。当然,很多命令也可以在Xcode中直接使用。这些命令可以让我们了解程序运行的状态,当然有些状态可以在Xcode中了解到。建议在调试过程中,可以多使用这些命令。

如果想了解这一过程中使用的各种命令,可以查看苹果的官方文档。

在Xcode中调试程序

对于我们日常的开发工作来说,更多的时候是在Xcode中进行调试工作。因此上面所描述的流程,其实Xcode已经帮我们完成了大部分的工作,而且很多东西也可以在Xcode里面看到。因此,我们可以把精力都集中在代码层面上。

在苹果的官方文档中列出了我们在调试中能用到的一些命令,我们在这重点讲一些常用的命令。

打印

打印变量的值可以使用print命令,该命令如果打印的是简单类型,则会列出简单类型的类型和值。如果是对象,还会打印出对象指针地址,如下所示:

1
2
3
4
5
6
7
8
(lldb) print a
(NSInteger) $0 = 0
(lldb) print b
(NSInteger) $1 = 0
(lldb) print str
(NSString *) $2 = 0x0000000100001048 @"abc"
(lldb) print url
(NSURL *) $3 = 0x0000000100206cc0 @"abc"

在输出结果中我们还能看到类似于$0,$1这样的符号,我们可以将其看作是指向对象的一个引用,我们在控制面板中可以直接使用这个符号来操作对应的对象,这些东西存在于LLDB的全名空间中,目的是为了辅助调试。如下所示:

1
2
3
4
(lldb) exp $0 = 100
(NSInteger) $9 = 100
(lldb) p a
(NSInteger) $10 = 100

另外$后面的数值是递增的,每打印一个与对象相关的命令,这个值都会加1。

上面的print命令会打印出对象的很多信息,如果我们只想查看对象的值的信息,则可以使用po(print object的缩写)命令,如下所示:

1
2
(lldb) po str
abc

当然,po命令是"exp -O --"命令的别名,使用"exp -O --"能达到同样的效果。

对于简单类型,我们还可以为其指定不同的打印格式,其命令格式是print/,如下所示:

1
2
(lldb) p/x a
(NSInteger) $13 = 0x0000000000000064

格式的完整清单可以参考Output Formats。

expression

在开发中,我们经常会遇到这样一种情况:我们设置一个视图的背景颜色,运行后发现颜色不好看。嗯,好吧,在代码里面修改一下,再编译运行一下,嗯,还是不好看,然后再修改吧~~这样无形中浪费了我们大把的时间。在这种情况下,expression命令强大的功能就能体现出来了,它不仅会改变调试器中的值,还改变了程序中的实际值。我们先来看看实际效果,如下所示:

1
2
3
4
5
(lldb) exp a = 10
(NSInteger) $0 = 10
(lldb) exp b = 100
(NSInteger) $1 = 100
2015-01-25 14:00:41.313 test[18064:71466] a + b = 110, abc

expression命令的功能不仅于此,正如上面的po命令,其实际也是"expression -O --"命令的别名。更详细使用可以参考Evaluating Expressions。

image

image命令的用法也挺多,首先可以用它来查看工程中使用的库,如下所示:

1
2
3
4
5
6
7
8
9
(lldb) image list
[ 0] 432A6EBF-B9D2-3850-BCB2-821B9E62B1E0 0x0000000100000000 /Users/**/Library/Developer/Xcode/DerivedData/test-byjqwkhxixddxudlnvqhrfughkra/Build/Products/Debug/test
[ 1] 65DCCB06-339C-3E25-9702-600A28291D0E 0x00007fff5fc00000 /usr/lib/dyld
[ 2] E3746EDD-DFB1-3ECB-88ED-A91AC0EF3AAA 0x00007fff8d324000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
[ 3] 759E155D-BC42-3D4E-869B-6F57D477177C 0x00007fff8869f000 /usr/lib/libobjc.A.dylib
[ 4] 5C161F1A-93BA-3221-A31D-F86222005B1B 0x00007fff8c75c000 /usr/lib/libSystem.B.dylib
[ 5] CBD1591C-405E-376E-87E9-B264610EBF49 0x00007fff8df0d000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
[ 6] A260789B-D4D8-316A-9490-254767B8A5F1 0x00007fff8de36000 /usr/lib/libauto.dylib
......

我们还可以用它来查找可执行文件或共享库的原始地址,这一点还是很有用的,当我们的程序崩溃时,我们可以使用这条命令来查找崩溃所在的具体位置,如下所示:

1
2
NSArray *array = @[@1, @2];
NSLog(@"item 3: %@", array[2]);

这段代码在运行后会抛出如下异常:

1
2
3
4
5
6
7
8
9
10
2015-01-25 14:12:01.007 test[18122:76474] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff8e06f66c __exceptionPreprocess + 172
1 libobjc.A.dylib 0x00007fff886ad76e objc_exception_throw + 43
2 CoreFoundation 0x00007fff8df487de -[__NSArrayI objectAtIndex:] + 190
3 test 0x0000000100000de0 main + 384
4 libdyld.dylib 0x00007fff8f1b65c9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

根据以上信息,我们可以判断崩溃位置是在main.m文件中,要想知道具体在哪一行,可以使用以下命令:

1
2
3
(lldb) image lookup --address 0x0000000100000de0
Address: test[0x0000000100000de0] (test.__TEXT.__text + 384)
Summary: test`main + 384 at main.m:23

可以看到,最后定位到了main.m文件的第23行,正是我们代码所在的位置。

我们还可以使用image lookup命令来查看具体的类型,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(lldb) image lookup --type NSURL
Best match found in /Users/**/Library/Developer/Xcode/DerivedData/test-byjqwkhxixddxudlnvqhrfughkra/Build/Products/Debug/test:
id = {0x100000157}, name = "NSURL", byte-size = 40, decl = NSURL.h:17, clang_type = "@interface NSURL : NSObject{
NSString * _urlString;
NSURL * _baseURL;
void * _clients;
void * _reserved;
}
@property ( readonly,getter = absoluteString,setter = <null selector>,nonatomic ) NSString * absoluteString;
@property ( readonly,getter = relativeString,setter = <null selector>,nonatomic ) NSString * relativeString;
@property ( readonly,getter = baseURL,setter = <null selector>,nonatomic ) NSURL * baseURL;
@property ( readonly,getter = absoluteURL,setter = <null selector>,nonatomic ) NSURL * absoluteURL;
@property ( readonly,getter = scheme,setter = <null selector>,nonatomic ) NSString * scheme;
@property ( readonly,getter = resourceSpecifier,setter = <null selector>,nonatomic ) NSString * resourceSpecifier;
@property ( readonly,getter = host,setter = <null selector>,nonatomic ) NSString * host;
@property ( readonly,getter = port,setter = <null selector>,nonatomic ) NSNumber * port;
@property ( readonly,getter = user,setter = <null selector>,nonatomic ) NSString * user;
@property ( readonly,getter = password,setter = <null selector>,nonatomic ) NSString * password;
@property ( readonly,getter = path,setter = <null selector>,nonatomic ) NSString * path;
@property ( readonly,getter = fragment,setter = <null selector>,nonatomic ) NSString * fragment;
@property ( readonly,getter = parameterString,setter = <null selector>,nonatomic ) NSString * parameterString;
@property ( readonly,getter = query,setter = <null selector>,nonatomic ) NSString * query;
@property ( readonly,getter = relativePath,setter = <null selector>,nonatomic ) NSString * relativePath;
@property ( readonly,getter = fileSystemRepresentation,setter = <null selector> ) const char * fileSystemRepresentation;
@property ( readonly,getter = isFileURL,setter = <null selector>,readwrite ) BOOL fileURL;
@property ( readonly,getter = standardizedURL,setter = <null selector>,nonatomic ) NSURL * standardizedURL;
@property ( readonly,getter = filePathURL,setter = <null selector>,nonatomic ) NSURL * filePathURL;
@end"

可以看到,输出结果中列出了NSURL的一些成员变量及属性信息。

image命令还有许多其它功能,具体可以参考Executable and Shared Library Query Commands。

流程控制

流程控制的命令实际上我们在上一小节已经讲过了,在Xcode的控制面板中同样可以使用这些命令,在此不在重复。

命令别名及帮助系统

LLDB有两个非常有用的特性,即命令别名及帮助。

命令别名

我们可以使用LLDB的别名机制来为常用的命令创建一个别名,以方便我们的使用,如下命令:

1
(lldb) breakpoint set --file foo.c --line 12

如果在我们的调试中需要经常用到这条命令,则每次输入这么一长串的字符一定会很让人抓狂。此时,我们就可以为这条命令创建一个别名,如下所示:

1
(lldb) command alias bfl breakpoint set -f %1 -l %2

这样,我们只需要按如下方式来使用它即可:

1
(lldb) bfl foo.c 12

是不是简单多了?

我们可以自由地创建LLDB命令的别名集合。LLDB在启动时会读取~/.lldbinit文件。这个文件中存储了command alias命令创建的别名。LLDB帮助系统会读取这个初始化文件并会列出这些别名,以让我们了解自己所设置的别名。我们可以使用"help -a"命令并在输出的后面来查看这边别名,其以下面这行开始:

1
2
...
The following is a list of your current command abbreviations (see 'help command alias' for more info): ...

如果我们不喜欢已有命令的别名,则可以使用以下命令来取消这个别名:

1
(lldb) command unalias b

帮助系统

LLDB帮助系统让我们可以了解LLDB提供了哪些功能,并可以查看LLDB命令结构的详细信息。熟悉帮助系统可以让我们访问帮助系统中中命令文档。

我们可以简单地调用help命令来列出LLDB所有的顶层命令。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) help
The following is a list of built-in, permanent debugger commands:
_regexp-attach -- Attach to a process id if in decimal, otherwise treat the
argument as a process name to attach to.
_regexp-break -- Set a breakpoint using a regular expression to specify the
location, where <linenum> is in decimal and <address> is
in hex.
_regexp-bt -- Show a backtrace. An optional argument is accepted; if
that argument is a number, it specifies the number of
frames to display. If that argument is 'all', full
backtraces of all threads are displayed.
… and so forth …

如果help后面跟着某个特定的命令,则会列出该命令相关的所有信息,我们以breakpoint set为例,输出信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) help breakpoint set
Sets a breakpoint or set of breakpoints in the executable.
Syntax: breakpoint set <cmd-options>
Command Options Usage:
breakpoint set [-Ho] -l <linenum> [-s <shlib-name>] [-i <count>] [-c <expr>] [-x <thread-index>] [-t <thread-id>] [-T <thread-name>] [-q <queue-name>] [-f <filename>] [-K <boolean>]
breakpoint set [-Ho] -a <address-expression> [-s <shlib-name>] [-i <count>] [-c <expr>] [-x <thread-index>] [-t <thread-id>] [-T <thread-name>] [-q <queue-name>]
breakpoint set [-Ho] -n <function-name> [-s <shlib-name>] [-i <count>] [-c <expr>] [-x <thread-index>] [-t <thread-id>] [-T <thread-name>] [-q <queue-name>] [-f <filename>] [-K <boolean>] [-L <language>]
breakpoint set [-Ho] -F <fullname> [-s <shlib-name>] [-i <count>] [-c <expr>] [-x <thread-index>] [-t <thread-id>] [-T <thread-name>] [-q <queue-name>] [-f <filename>] [-K <boolean>]
… and so forth …

还有一种更直接的方式来查看LLDB有哪些功能,即使用apropos命令:它会根据关键字来搜索LLDB帮助文档,并为每个命令选取一个帮助字符串,我们以apropos file为例,其输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(lldb) apropos file
The following commands may relate to 'file':
…
log enable -- Enable logging for a single log channel.
memory read -- Read from the memory of the process being
debugged.
memory write -- Write to the memory of the process being
debugged.
platform process launch -- Launch a new process on a remote platform.
platform select -- Create a platform if needed and select it as
the current platform.
plugin load -- Import a dylib that implements an LLDB
plugin.
process launch -- Launch the executable in the debugger.
process load -- Load a shared library into the current
process.
source -- A set of commands for accessing source file
information
… and so forth …

我们还可以使用help来了解一个命令别名的构成。如:

1
2
3
(lldb) help b
…
'b' is an abbreviation for '_regexp-break'

help命令的另一个特性是可以查看某个具体参数的使用,我们以"break command add"命令为例:

1
2
3
4
5
(lldb) help break command add
Add a set of commands to a breakpoint, to be executed whenever the breakpoint is hit.
Syntax: breakpoint command add <cmd-options> <breakpt-id>
etc...

如果想了解以上输出的参数<breakpt-id>的作用,我们可以在help后面直接指定这个参数(将其放在尖括号内)来查询它的详细信息,如下所示:

1
2
3
4
(lldb) help <breakpt-id>
<breakpt-id> -- Breakpoint IDs consist major and minor numbers; the major
etc...

帮助系统能让我们快速地了解一个LLDB命令的使用方法。经常使用它,可以让我们更快地熟悉LLDB的各项功能,所以建议多使用它。

总结

LLDB带给我们强大的调试功能,在调试过程中充分地利用它可以帮助我们极大地提高调试效率。我们可以不用写那么多的NSLog来打印一大堆的日志。所以建议在日常工作中多去使用它。当然,上面的命令只是LLDB的冰山一角,更多的使用还需要大家自己去发掘,在此只是抛砖引玉,做了一些整理。

参考

  1. The LLDB Debugger
  2. LLDB Quick Start Guide
  3. 与调试器共舞 - LLDB 的华尔兹
  4. LLDB调试命令初探
  5. NSLog效率低下的原因及尝试lldb断点打印Log

Mantle实现分析

发表于 2015-01-11   |   分类于 源码分析

Mantle是一个用于简化Cocoa或Cocoa Touch程序中model层的第三方库。通常我们的应该中都会定义大量的model来表示各种数据结构,而这些model的初始化和编码解码都需要写大量的代码。而Mantle的优点在于能够大大地简化这些代码。

Mantle源码中最主要的内容包括:

  1. MTLModel类:通常是作为我们的Model的基类,该类提供了一些默认的行为来处理对象的初始化和归档操作,同时可以获取到对象所有属性的键值集合。
  2. MTLJSONAdapter类:用于在MTLModel对象和JSON字典之间进行相互转换,相当于是一个适配器。
  3. MTLJSONSerializing协议:需要与JSON字典进行相互转换的MTLModel的子类都需要实现该协议,以方便MTLJSONApadter对象进行转换。

在此就以这三者作为我们的分析点。

基类MTLModel

MTLModel是一个抽象类,它主要提供了一些默认的行为来处理对象的初始化和归档操作。

初始化

MTLModel默认的初始化方法-init并没有做什么事情,只是调用了下[super init]。而同时,它提供了一个另一个初始化方法:

1
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;

其中参数dictionaryValue是一个字典,它包含了用于初始化对象的key-value对。我们来看下它的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
...
for (NSString *key in dictionary) {
// 1. 将value标记为__autoreleasing,这是因为在MTLValidateAndSetValue函数中,
// 可以会返回一个新的对象存在在该变量中
__autoreleasing id value = [dictionary objectForKey:key];
// 2. value如果为NSNull.null,会在使用前将其转换为nil
if ([value isEqual:NSNull.null]) value = nil;
// 3. MTLValidateAndSetValue函数利用KVC机制来验证value的值对于key是否有效,
// 如果无效,则使用使用默认值来设置key的值。
// 这里同样使用了对象的KVC特性来将value值赋值给model对应于key的属性。
// 有关MTLValidateAndSetValue的实现可参考源码,在此不做详细说明。
BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
if (!success) return nil;
}
...
}

子类可以重写该方法,以在设置完对象的属性后做进一步的处理或初始化工作,不过需要记住的是:应该通过super来调用父类的实现。

获取属性的键(key)、值(value)

MTLModel类提供了一个类方法+propertyKeys,该方法返回所有@property声明的属性所对应的名称字符串的一个集合,但不包括只读属性和MTLModel自身的属性。在这个类方法会去遍历model的所有属性,如果属性是非只读且其ivar值不为NULL,则获取到表示属性名的字符串,并将其放入到集合中,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
+ (NSSet *)propertyKeys {
// 1. 如果对象中已有缓存的属性名的集合,则直接返回缓存。该缓存是放在一个关联对象中。
NSSet *cachedKeys = objc_getAssociatedObject(self, MTLModelCachedPropertyKeysKey);
if (cachedKeys != nil) return cachedKeys;
NSMutableSet *keys = [NSMutableSet set];
// 2. 遍历对象所有的属性
// enumeratePropertiesUsingBlock方法会沿着superclass链一直向上遍历到MTLModel,
// 查找当前model所对应类的继承体系中所有的属性(不包括MTLModel),并对该属性执行block中的操作。
// 有关enumeratePropertiesUsingBlock的实现可参考源码,在此不做详细说明。
[self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
free(attributes);
};
// 3. 过滤只读属性和ivar为NULL的属性
if (attributes->readonly && attributes->ivar == NULL) return;
// 4. 获取属性名字符串,并存储到集合中
NSString *key = @(property_getName(property));
[keys addObject:key];
}];
// 5. 将集合缓存到关联对象中。
objc_setAssociatedObject(self, MTLModelCachedPropertyKeysKey, keys, OBJC_ASSOCIATION_COPY);
return keys;
}

有了上面这个类方法,要想获取到对象中所有属性及其对应的值就方法了。为此MTLModel提供了一个只读属性dictionaryValue来取一个包含当前model所有属性及其值的字典。如果属性值为nil,则会用NSNull来代替。另外该属性不会为nil。

1
2
3
4
5
6
@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;
// 实现
- (NSDictionary *)dictionaryValue {
return [self dictionaryWithValuesForKeys:self.class.propertyKeys.allObjects];
}

合并对象

合并对象是指将两个MTLModel对象按照自定义的方法将其对应的属性值进行合并。为此,在MTLModel定义了以下方法:

1
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model;

该方法将当前对象指定的key属性的值与model参数对应的属性值按照指定的规则来进行合并,这种规则由我们自定义的-merge<Key>FromModel:方法来确定。如果我们的子类中实现了-merge<Key>FromModel:方法,则会调用它;如果没有找到,且model不为nil,则会用model的属性的值来替代当前对象的属性的值。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model {
NSParameterAssert(key != nil);
// 1. 根据传入的key拼接"merge<Key>FromModel:"字符串,并从该字符串中获取到对应的selector
// 如果当前对象没有实现-merge<Key>FromModel:方法,且model不为nil,则用model的属性值
// 替代当前对象的属性值
//
// MTLSelectorWithCapitalizedKeyPattern函数以C语言的方式来拼接方法字符串,具体实现请
// 参数源码,在此不详细说明
SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
if (![self respondsToSelector:selector]) {
if (model != nil) {
[self setValue:[model valueForKey:key] forKey:key];
}
return;
}
// 2. 通过NSInvocation方式来调用对应的-merge<Key>FromModel:方法。
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
invocation.target = self;
invocation.selector = selector;
[invocation setArgument:&model atIndex:2];
[invocation invoke];
}

此外,MTLModel还提供了另一个方法来合并两个对象所有的属性值,即:

1
- (void)mergeValuesForKeysFromModel:(MTLModel *)model;

需要注意的是model必须是当前对象所属类或其子类。

归档对象(Archive)

Mantle将对MTLModel的编码解码处理都放在了MTLModel的NSCoding分类中进行处理了,该分类及相关的定义都放在MTLModel+NSCoding文件中。

对于不同的属性,在编码解码过程中可能需要区别对待,为此Mentle定义了枚举MTLModelEncodingBehavior来确定一个MTLModel属性被编码到一个归档中的行为。其定义如下:

1
2
3
4
5
typedef enum : NSUInteger {
MTLModelEncodingBehaviorExcluded = 0, // 属性绝不应该被编码
MTLModelEncodingBehaviorUnconditional, // 属性总是应该被编码
MTLModelEncodingBehaviorConditional, // 对象只有在其它地方被无条件编码时才应该被编码。这只适用于对象属性
} MTLModelEncodingBehavior;

具体每个属性的归档行为我们可以在+encodingBehaviorsByPropertyKey类方法中设置。MTLModel类为我们提供了一个默认实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (NSDictionary *)encodingBehaviorsByPropertyKey {
// 1. 获取所有属性键值
NSSet *propertyKeys = self.propertyKeys;
NSMutableDictionary *behaviors = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
// 2. 对每一个属性进行处理
for (NSString *key in propertyKeys) {
objc_property_t property = class_getProperty(self, key.UTF8String);
NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
free(attributes);
};
// 3. 当属性为weak时,默认设置为MTLModelEncodingBehaviorConditional,否则默认为MTLModelEncodingBehaviorUnconditional,设置完后,将其封装在NSNumber中并放入字典中。
MTLModelEncodingBehavior behavior = (attributes->weak ? MTLModelEncodingBehaviorConditional : MTLModelEncodingBehaviorUnconditional);
behaviors[key] = @(behavior);
}
return behaviors;
}

任何不在该返回字典中的属性都不会被归档。子类可以根据自己的需要来指定各属性的归档行为。但在实际时应该通过super来调用父类的实现。

而为了从归档中解码指定的属性,Mantle提供了以下方法:

1
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion;

默认情况下,该方法会查找当前对象中类似于-decode<Key>WithCoder:modelVersion:的方法,如果找到便会调用相应方法,并按照自定义的方式来处理属性的解码。如果我们没有实现自定义的方法或者coder不需要安全编码,则会对指定的key调用-[NSCoder decodeObjectForKey:]方法。其具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion {
...
SEL selector = MTLSelectorWithCapitalizedKeyPattern("decode", key, "WithCoder:modelVersion:");
// 1. 如果自定义了-decode<Key>WithCoder:modelVersion:方法,则通过NSInvocation来调用方法
if ([self respondsToSelector:selector]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
invocation.target = self;
invocation.selector = selector;
[invocation setArgument:&coder atIndex:2];
[invocation setArgument:&modelVersion atIndex:3];
[invocation invoke];
__unsafe_unretained id result = nil;
[invocation getReturnValue:&result];
return result;
}
@try {
// 2. 如果没有找到自定义的-decode<Key>WithCoder:modelVersion:方法,
// 则走以下流程。
//
// coderRequiresSecureCoding方法的具体实现请参数源码
if (coderRequiresSecureCoding(coder)) {
// 3. 如果coder要求安全编码,则会从需要安全编码的字典中取出属性所对象的类型,然后根据指定
// 类型来对属性进行解码操作。
// 为此,MTLModel提供了类方法allowedSecureCodingClassesByPropertyKey,来获取
// 类的对象包含的所有需要安全编码的属性及其对应的类的字典。该方法首先会查看是否已有
// 缓存的字典,如果没有则遍历类的所有属性。首先过滤掉那些不需要编码的属性,
// 然后遍历剩下的属性,如果是非对象类型或类类型,则其对应的类型设定为NSValue,
// 如果是这两者,则对应的类型即为相应类型。
// 该方法的具体实现请参考源代码。
NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
} else {
// 4. 不需要安全编码
return [coder decodeObjectForKey:key];
}
} @catch (NSException *exception) {
...
}
}

当然,所有的编码解码工作还得需要我们实现-initWithCoder:和-encodeWithCoder:两个方法来完成。我们在定义MTLModel的子类时,可以根据自己的需要来对特定的属性进行处理,不过最好调用super的实现来执行父类的操作。MTLModel对这两个方法的实现请参考源码,在此不多作说明。

适配器MTLJSONApadter

为了便于在MTLModel对象和JSON字典之间进行相互转换,Mantle提供了类MTLJSONApadter,作为这两者之间的一个适配器。

MTLJSONSerializing协议

Mantle定义了一个协议MTLJSONSerializing,那些需要与JSON字典进行相互转换的MTLModel的子类都需要实现该协议,以方便MTLJSONApadter对象进行转换。这个协议中定义了三个方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
@protocol MTLJSONSerializing
@required
+ (NSDictionary *)JSONKeyPathsByPropertyKey;
@optional
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary;
@end

这三个方法都是类方法。其中+JSONKeyPathsByPropertyKey是必须实现的,它返回的字典指定了如何将对象的属性映射到JSON中不同的key path(字符串值或NSNull)中。任何不在此字典中的属性被认为是与JSON中使用的key值相匹配。而映射到NSNull的属性在JSON序列化过程中将不进行处理。

+JSONTransformerForKey:方法指定了如何将一个JSON值转换为指定的属性值。反过来,转换器也用于将属性值转换成JSON值。如果转换器实现了+<key>JSONTransformer方法,则MTLJSONAdapter会使用这个具体的方法,而不使用+JSONTransformerForKey:方法。另外,如果不需要执行自定义的转换,则返回nil。

重写+classForParsingJSONDictionary:方法可以将当前Model解析为一个不同的类对象。这对象类簇是非常有用的,其中抽象基类将被传递给-[MTLJSONAdapter initWithJSONDictionary:modelClass:]方法,而实例化的则是子类。

如果我们希望MTLModel的一个子类能使用MTLJSONApadter来进行转换,则需要实现这个协议,并实现相应的方法。

初始化

MTLJSONApadter对象有一个只读属性,该属性即为适配器需要处理的MTLModel对象,其声明如下:

1
@property (nonatomic, strong, readonly) MTLModel<MTLJSONSerializing> *model;

可见该对象必须是实现了MTLJSONSerializing协议的MTLModel对象。该属性是只读的,因此它只能通过初始化方法来初始化。

MTLJSONApadter对象不能通过-init来初始化,这个方法会直接断言。而是需要通过类提供的两个初始化方法来初始化,如下:

1
2
3
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error;
- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model;

其中-(id)initWithJSONDictionary:modelClass:error:是使用一个字典和需要转换的类来进行初始化。字典JSONDictionary表示一个JSON数据,这个字典需要符合NSJSONSerialization返回的格式。如果该参数为空,则方法返回nil,且返回带有MTLJSONAdapterErrorInvalidJSONDictionary码的error对象。该方法的具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error {
...
if (JSONDictionary == nil || ![JSONDictionary isKindOfClass:NSDictionary.class]) {
...
return nil;
}
if ([modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
modelClass = [modelClass classForParsingJSONDictionary:JSONDictionary];
if (modelClass == nil) {
...
return nil;
}
...
}
...
_modelClass = modelClass;
_JSONKeyPathsByPropertyKey = [[modelClass JSONKeyPathsByPropertyKey] copy];
NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:JSONDictionary.count];
NSSet *propertyKeys = [self.modelClass propertyKeys];
// 1. 检验model的+JSONKeyPathsByPropertyKey中字典key-value对的有效性
for (NSString *mappedPropertyKey in self.JSONKeyPathsByPropertyKey) {
// 2. 如果model对象的属性不包含+JSONKeyPathsByPropertyKey返回的字典中的某个属性键值
// 则返回nil。即+JSONKeyPathsByPropertyKey中指定的属性键值必须是model对象所包含
// 的属性。
if (![propertyKeys containsObject:mappedPropertyKey]) {
...
return nil;
}
id value = self.JSONKeyPathsByPropertyKey[mappedPropertyKey];
// 3. 如果属性不是映射到一个JSON关键路径或者是NSNull,也返回nil。
if (![value isKindOfClass:NSString.class] && value != NSNull.null) {
...
return nil;
}
}
for (NSString *propertyKey in propertyKeys) {
NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
if (JSONKeyPath == nil) continue;
id value;
@try {
value = [JSONDictionary valueForKeyPath:JSONKeyPath];
} @catch (NSException *ex) {
...
return nil;
}
if (value == nil) continue;
@try {
// 4. 获取一个转换器,
// 如上所述,+JSONTransformerForKey:会先去查看是否有+<key>JSONTransformer方法,
// 如果有则会使用这个具体的方法,如果没有,则调用相应的+JSONTransformerForKey:方法
// 该方法具体实现请参考源码
NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
if (transformer != nil) {
// 5. 获取转换器转换生的值
if ([value isEqual:NSNull.null]) value = nil;
value = [transformer transformedValue:value] ?: NSNull.null;
}
dictionaryValue[propertyKey] = value;
} @catch (NSException *ex) {
...
return nil;
}
}
// 6. 初始化_model
_model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
if (_model == nil) return nil;
return self;
}

另外,MTLJSONApadter还提供了几个类方法来创建一个MTLJSONApadter对象,如下:

1
2
3
4
5
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model;

具体实现可参考源码。

从对象中获取JSON数据

从MTLModel对象中获取JSON数据是上述初始化过程中的一个逆过程。该过程由-JSONDictionary方法来实现,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (NSDictionary *)JSONDictionary {
NSDictionary *dictionaryValue = self.model.dictionaryValue;
NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] initWithCapacity:dictionaryValue.count];
[dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
if (JSONKeyPath == nil) return;
// 1. 获取属性的值
NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
if ([transformer.class allowsReverseTransformation]) {
if ([value isEqual:NSNull.null]) value = nil;
value = [transformer reverseTransformedValue:value] ?: NSNull.null;
}
NSArray *keyPathComponents = [JSONKeyPath componentsSeparatedByString:@"."];
// 2. 对于嵌套属性值的设置,会先从keypath中获取每一层属性,
// 如果当前层级的obj中没有该属性,则为其设置一个空字典;然后再进入下一层级,依此类推
// 最后设置如下形式的字典: @{@"nested": @{@"name": @"foo"}}
id obj = JSONDictionary;
for (NSString *component in keyPathComponents) {
if ([obj valueForKey:component] == nil) {
[obj setValue:[NSMutableDictionary dictionary] forKey:component];
}
obj = [obj valueForKey:component];
}
[JSONDictionary setValue:value forKeyPath:JSONKeyPath];
}];
return JSONDictionary;
}

从上可以看出,该方法实际上最终获得的是一个字典。而获得字典后,再将其序列化为JSON串就容易了。

MTLJSONApadter也提供了一个简便的方法,来从一个model中获取一个JSON字典,其定义如下:

1
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model;

MTLManagedObjectAdapter

为了适应Core Data,Mantle专门定义了MTLManagedObjectAdapter类。该类用作MTLModel对象与NSManagedObject对象之前的转换。具体的我们在此不详细描述。

技术点总结

Mantle的功能主要是进行对象间数据的转换:即如何在一个MTLModel和一个JSON字典中进行数据的转换。因此,所使用的技术大都是Cocoa Foundation提供的功能。除了对于Core Data的处理之外,主要用到的技术的有如下几条:

  1. KVC的应用:这主要体现在对MTLModel子类的属性赋值中,通过KVC机制来验证值的有效性并为属性赋值。
  2. NSValueTransform:这主要用于对JSON值转换为属性值的处理,我们可以自定义转换器来满足我们自己的转换需求。
  3. NSInvocation:这主要用于统一处理针对特定key值的一些方法的调用。比如-merge<Key>FromModel:这一类方法。
  4. Run time函数的使用:这主要用于对从一个字符串中获取到方法对应的字符串,然后通过sel_registerName函数来注册一个selector。

当然在Mantle中还会涉及到其它的一些技术点,在此不多做叙述。

参考

  1. Mantle工程

工具篇:Mantle

发表于 2015-01-11   |   分类于 杂项

来源:https://github.com/Mantle/Mantle

版本:1.5.3

Mantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch application.

由上面这句话可知,Mantle的目的是让我们能简化Cocoa和Cocoa Touch应用的model层。那先来看看通常我们是怎么处理model层的吧。

解决的问题

在我们写代码时,总要面对不同的数据来源。这些数据可能是来自网络服务器、本地数据库或者是内存中。通常我们需要将这些数据存储到一个Model中。一般情况下,我们会怎么去定义一个Model呢?以Mantle官方的例子为例,可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@interface GHIssue : NSObject <NSCoding, NSCopying>
@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end

假定我们从网络服务器上获取了一组GHIssue对应的JSON数据,并已经将其转换为字典后,我们便可以用这个字典对GHIssue对象进行初始化了,-initWithDictionary:的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [self init];
if (self == nil) return nil;
_URL = [NSURL URLWithString:dictionary[@"url"]];
_HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];
if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}
_title = [dictionary[@"title"] copy];
_retrievedAt = [NSDate date];
_body = [dictionary[@"body"] copy];
_reporterLogin = [dictionary[@"user"][@"login"] copy];
_assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];
_updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];
return self;
}

如果GHIssue对象有归档需求,则还需要实现以下两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (id)initWithCoder:(NSCoder *)coder {
self = [self init];
if (self == nil) return nil;
_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_retrievedAt = [NSDate date];
_body = [coder decodeObjectForKey:@"body"];
_reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
_assignee = [coder decodeObjectForKey:@"assignee"];
_updatedAt = [coder decodeObjectForKey:@"updatedAt"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];
[coder encodeUnsignedInteger:self.state forKey:@"state"];
}

额,好多代码。嗯,说实话,以前也经常写这种代码,真可谓又臭又长啊。也许我的工程中还有很多这样的Model,然后,然后……靠,好烦啊。再然后,某天,服务端的同事告诉我有N个接口需要加字段,额~~崩溃中。而且,从上面的Model中,我无法将其还原为对应的JSON串,且如果某些信息变了,那么归档的数据可能就无法使用了。

Mantle就是针对这几个问题而开发的一个开源库。

使用方法

其实Mantle的使用还是很简单的,它最主要的就是二个类和一个协议,即:

  1. MTLModel类:通常是作为我们的Model的基类,该类提供了一些默认的行为来处理对象的初始化和归档操作,同时可以获取到对象所有属性的键值集合。
  2. MTLJSONAdapter类:用于在MTLModel对象和JSON字典之间进行相互转换,相当于是一个适配器。
  3. MTLJSONSerializing协议:需要与JSON字典进行相互转换的MTLModel的子类都需要实现该协议,以方便MTLJSONApadter对象进行转换。

还以GHIssue为例,我们通常会以以下方式来定义我们的Model:

1
2
3
4
5
6
7
8
9
10
@interface GHIssue : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
...
@end

可以看到,我们的Model继承了通常是MTLModel类,同时实现了MTLJSONSerializing协议。这样,我们不再需要像上面那样写一大堆的赋值代码和编码解码方法,而只需要实现MTLJSONSerializing协议的+JSONKeyPathsByPropertyKey类方法,将我们的属性名的键值与JSON字典的键值做一个映射,我们便可以在MTLJSONAdapter对象的帮助下自动进行赋值操作和编码解码操作。我们来看看GHIssue类的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation GHIssue
...
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"URL": @"url",
@"HTMLURL": @"html_url",
@"reporterLogin": @"user.login",
@"assignee": @"assignee",
@"updatedAt": @"updated_at"
};
}
...
@end

可以看到,Model对象的属性与JSON数据之间的映射是通过字典来实现的。通过这种对应关系,Model对象便可以和JSON数据相互转换。需要注意的是返回中字典中的key值在Model对象中必须有对应的属性,否则Model对象将无法初始化成功。

当然这两者的值之间的转换关系可能需要我们自己来定义,这时我们就可以在Model中自定义+(NSValueTransformer *)<key>JSONTransformer方法来完成这一操作,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@implementation GHIssue
...
+ (NSValueTransformer *)URLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
...
@end

​

这样,在转换过程中,会自动调用这些方法来做数据的转换。而如果没有实现相应的方法,则会调用默认的+JSONTransformerForKey:来做处理,具体的实现可以参考《Mantle实现分析》。

有了上面这些准备工作,我们就需要通过MTLJSONAdapter类来适配MTLModel对象和JSON数据了,这个更容易了,代码如下所示:

1
2
3
4
5
NSError *error = nil;
NSDictionary *JSONDictionary = ...;
GHIssue *issue = [MTLJSONAdapter modelOfClass:GHIssue.class fromJSONDictionary:JSONDictionary error:&error];

这样就根据一个JSON字典创建了一个GHIssue对象,而如果要从这个对象中获取到相应的JSON字典,则可以如下操作:

1
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:issue];

以上便是Mantle的简单使用,当然更多的使用方式还需要在实践中多挖掘了。

这里还需要注意的是:

  1. MTLModel的转换只针对我们定义的属性,而无法支持成员变量。
  2. 支持嵌套属性的转换,这对于对象属性来说非常有用。

导入工程

想在我们的工程中使用Mantle,可以通过以下步骤导入:

  1. 将Mantle库作为应用的子模块添加进来。
  2. 运行Mantle文件夹下的script/bootstrap脚本。
  3. 将Mantle.xcodeproj拖进我们的XCode工程或工作空间。
  4. 在程序target的Build Phases选项卡中,在Link Binary With Libraries下添加Mantle的相关信息。在iOS工程中,添加libMantle.a库。
  5. 在"Header Search Paths"设置中添加"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)。
  6. 对于iOS目标,在"Other Linker Flags"设置中添加-ObjC。
  7. 如果我们将Mantle添加到工程(而不是工作空间),则我们需要将Mantle依赖的库添加到程序的"Target Dependencies"中。

不过,我还是喜欢用CocoaPods来处理,只需要在Podfile中添加以下代码:

1
pod 'Mantle', '~> 1.5.3'

然后在对应目录下运行pod install,稍等片刻便可以使用Mantle了。关于CocoaPods的使用,可参考github上的cocoapods工程。

不足之处

Mantle使用简单方便,极大的简化了我们的代码,可以满足我们大部分的需求。不过有时候我们可能会遇到这样的情况,由服务端提供的两个接口A和B,其实际上返回的数据可以转换为程序的同一个Model,只不过由于提供接口的是两个人,而且没有相互约定;抑或是服务端接口返回的数据与本地数据库的数据可以转换化同一个Model,但由于历史原因,这两者的字段也没对应上,如下所示:

1
2
3
4
5
// A接口返回的JSON数据为
{"user": "abc", "password": "abc"}
// B接口返回的JSON数据为
{"user": "123", "pwd": "123"}

这种情况下如何使用Mantle呢?看着实际上都一样,只是字段名不一样。这时似乎就不好处理了。因为+JSONKeyPathsByPropertyKey中,字典的key表示的是MTLModel的属性键值,是通过属性的键值去找相应的JSON数据的key。因此,这种情况下可能就得定义两个Model了。

在我们之前的工程中,也有做过类似Mantle的处理,只不过没有做得这么细致。针对上面的问题,我们的方案是刚好反过来,这个映射字典的key是JSON字典的key值,而映射字典的value是对象属性的key值。这样,我们就可以将不回数据来源的JSON字典的不同key映射到同一个Model对象的同一个属性上了。

另外一方面,由于转换过程涉及到一些映射查找操作,所以性能上也不如直接写赋值语句来得快。不过Mantle已以通过缓存对此做了优化,所以这一点还是可以接受的。

参考与推荐

  1. Mantle工程
  2. 源码篇:Mantle
  3. Mantle 初步使用
  4. 使用Mantle处理Model层对象

Quartz 2D编程指南之十三:PDF文档的创建、显示及转换

发表于 2015-01-08   |   分类于 翻译

PDF文档存储依赖于分辨率的向量图形、文本和位图,并用于程序的一系列指令中。一个PDF文档可以包含多页的图形和文本。PDF可用于创建跨平台、只读的文档,也可用于绘制依赖于分辨率的图形。

Quartz为所有应用程序创建高保真的PDF文档,这些文档保留应用的绘制操作,如图13-1所示。PDF文档的结果将通过系统的其它部分或第三方法的产品来有针对性地进行优化。Quartz创建的PDF文档在Preview和Acrobat中都能正确的显示。

Figure 13-1 Quartz creates high-quality PDF documents

image

Quartz不仅仅只使用PDF作为它的数字页,它同样包含一些API来显示和生成PDF文件,及完成一些其它PDF相关的工作。

打开和查看PDF

Quartz提供了CGPDFDocumentRef数据类型来表示PDF文档。我们可以使用CGPDFDocumentCreateWithProvider或CGPDFDocumentCreateWithURL来创建CGPDFDocument对象。在创建CGPDFDocument对象后,我们可以将其绘制到图形上下文中。图13-2显示了在一个窗体中绘制PDF文档。

Figure 13-2 A PDF document

image

代码清单13-1显示了如何创建一个CGPDFDocument对象及获取文档的页数。

Listing 13-1 Creating a CGPDFDocument object from a PDF file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CGPDFDocumentRef MyGetPDFDocumentRef (const char *filename)
{
CFStringRef path;
CFURLRef url;
CGPDFDocumentRef document;
size_t count;
path = CFStringCreateWithCString (NULL, filename,
kCFStringEncodingUTF8);
url = CFURLCreateWithFileSystemPath (NULL, path,
kCFURLPOSIXPathStyle, 0);
CFRelease (path);
document = CGPDFDocumentCreateWithURL (url);
CFRelease(url);
count = CGPDFDocumentGetNumberOfPages (document);
if (count == 0) {
printf("`%s' needs at least one page!", filename);
return NULL;
}
return document;
}

代码清单显示了如何将一个PDF页绘制到图形上下文中。

Listing 13-2 Drawing a PDF page

1
2
3
4
5
6
7
8
9
10
11
12
void MyDisplayPDFPage (CGContextRef myContext,
size_t pageNumber,
const char *filename)
{
CGPDFDocumentRef document;
CGPDFPageRef page;
document = MyGetPDFDocumentRef (filename);
page = CGPDFDocumentGetPage (document, pageNumber);
CGContextDrawPDFPage (myContext, page);
CGPDFDocumentRelease (document);
}

为PDF页创建一个转换

Quartz提供了函数CGPDFPageGetDrawingTransform来创建一个仿射变换,该变换基于将PDF页的BOX映射到指定的矩形中。函数原型是:

1
2
3
4
5
6
7
CGAffineTransform CGPDFPageGetDrawingTransform (
CGPPageRef page,
CGPDFBox box,
CGRect rect,
int rotate,
bool preserveAspectRatio
);

该函数通过如下算法来返回一个仿射变换:

  1. 将在box参数中指定的PDF box的类型相关的矩形(media, crop, bleed, trim, art)与指定的PDF页的/MediaBox入口求交集。相交的部分即为一个有效的矩形(effectiverectangle)。
  2. 将effective rectangle旋转参数/Rotate入口指定的角度。
  3. 将得到的矩形放到rect参数指定的中间。
  4. 如果rotate参数是一个非零且是90的倍数,函数将effective rectangel旋转该值指定的角度。正值往右旋转;负值往左旋转。需要注意的是我们传入的是角度,而不是弧度。记住PDF页的/Rotate入口也包含一个旋转,我们提供的rotate参数是与/Rotate入口接合在一起的。
  5. 如果需要,可以缩放矩形,从而与我们提供的矩形保持一致。
  6. 如果我们通过传递true值给preserveAspectRadio参数以指定保持长宽比,则最后的矩形将与rect参数的矩形的边一致。

【注:上面这段翻译得不是很好】

例如,我们可以使用这个函数来创建一个与图13-3类似的PDF浏览程序。如果我们提供一个Rotate Left/Rotate Right属性,则可以调用CGPDFPageGetDrawingTransform来根据当前的窗体大小和旋转设置计算出适当的转换。

Figure 13-3 A PDF page rotated 90 degrees to the right

image

程序清单13-3显示了为一个PDF页创建及应用仿射变换,然后绘制PDF。

Listing 13-3 Creating an affine transform for a PDF page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void MyDrawPDFPageInRect (CGContextRef context,
CGPDFPageRef page,
CGPDFBox box,
CGRect rect,
int rotation,
bool preserveAspectRatio)
{
CGAffineTransform m;
m = CGPDFPageGetDrawingTransform (page, box, rect, rotation,
preserveAspectRato);
CGContextSaveGState (context);
CGContextConcatCTM (context, m);
CGContextClipToRect (context,CGPDFPageGetBoxRect (page, box));
CGContextDrawPDFPage (context, page);
CGContextRestoreGState (context);
}

创建PDF文件

使用Quartz创建PDF与绘制其它图形上下文一下简单。我们指定一个PDF文件地址,设置一个PDF图形上下文,并使用与其它图形上下文一样的绘制程序。如代码清单13-4所示的MyCreatePDFFile函数,显示了创建一个PDF的所有工作。

注意,代码在CGPDFContextBeginPage和CGPDFContextEndPage中来绘制PDF。我们可以传递一个CFDictionary对象来指定页属性,包括media, crop, bleed,trim和art boxes。

Listing 13-4 Creating a PDF file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void MyCreatePDFFile (CGRect pageRect, const char *filename)
{
CGContextRef pdfContext;
CFStringRef path;
CFURLRef url;
CFDataRef boxData = NULL;
CFMutableDictionaryRef myDictionary = NULL;
CFMutableDictionaryRef pageDictionary = NULL;
path = CFStringCreateWithCString (NULL, filename,
kCFStringEncodingUTF8);
url = CFURLCreateWithFileSystemPath (NULL, path,
kCFURLPOSIXPathStyle, 0);
CFRelease (path);
myDictionary = CFDictionaryCreateMutable(NULL, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(myDictionary, kCGPDFContextTitle, CFSTR("My PDF File"));
CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR("My Name"));
pdfContext = CGPDFContextCreateWithURL (url, &pageRect, myDictionary);
CFRelease(myDictionary);
CFRelease(url);
pageDictionary = CFDictionaryCreateMutable(NULL, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
boxData = CFDataCreate(NULL,(const UInt8 *)&pageRect, sizeof (CGRect));
CFDictionarySetValue(pageDictionary, kCGPDFContextMediaBox, boxData);
CGPDFContextBeginPage (pdfContext, pageDictionary);
myDrawContent (pdfContext);
CGPDFContextEndPage (pdfContext);
CGContextRelease (pdfContext);
CFRelease(pageDictionary);
CFRelease(boxData);
}

添加链接

我们可以在PDF上下文中添加链接和锚点。Quartz提供了三个函数,每个函数都以PDF图形上下文作为参数,还有链接的信息:

  1. CGPDFContextSetURLForRect可以让我们指定在点击当前PDF页中的矩形时打开一个URL。
  2. CGPDFContextSetDestinationForRect指定在点击当前PDF页中的矩形区域时设置目标以进行跳转。我们需要提供一个目标名。
  3. CGPDFContextAddDestinationAtPoint指定在点击当前PDF页中的一个点时设置目标以进行跳转。我们需要提供一个目标名。

保护PDF内容

为了保护PDF内容,我们可以在辅助字典中指定一些安全选项并传递给CGPDFContextCreate。我们可以通过包含如下关键字来设置所有者密码、用户密码、PDF是否可以被打印或拷贝:

  1. kCGPDFContextOwnerPassword: 定义PDF文档的所有者密码。如果指定该值,则文档使用所有者密码来加密;否则文档不加密。该关键字的值必须是ASCII编码的CFString对象。只有前32位是用于密码的。该值没有默认值。如果该值不能表示成ASCII,则无法创建文档并返回NULL。Quartz使用40-bit加密。
  2. kCGPDFContextUserPassword: 定义PDF文档的用户密码。如果文档加密了,则该值是文档的用户密码。如果没有指定,则用户密码为空。该关键字的值必须是ASCII编码的CFString对象。只有前32位是用于密码的。如果该值不能表示成ASCII,则无法创建文档并返回NULL。
  3. kCGPDFContextAllowsPrinting:指定当使用用户密码锁定时文档是否可以打印。该值必须是CFBoolean对象。默认值是kCGBooleanTrue。
  4. kCGPDFContextAllowsCopying: 指定当使用用户密码锁定时文档是否可以拷贝。该值必须是CFBoolean对象。默认值是kCGBooleanTrue。

代码清单14-4(下一章)显示了确认PDF文档是否被锁定,及用密码打开文档。

Quartz 2D编程指南之十二:Core Graphics层绘制

发表于 2015-01-08   |   分类于 翻译

CGLayer对象(CGLayerRef数据类型)允许程序使用层来进行绘制。

层适合于以下几种情况:

  1. 高质量离屏渲染,以绘制我们想重用的图形。例如,我们可能要建立一个场景并重用相同的背景。将背景场景绘制于一个层上,然后在需要的时候再绘制层。一个额外的好处是我们不需要知道颜色空间或其它设备依赖的信息来绘制层。
  2. 重复绘制。例如,我们可能想创建一个由相同元素反复绘制而组成的模式。将元素绘制到一个层中,然后重复绘制这个层,如图12-1所示。任何我们重复绘制的Quartz对象,包括CGPath, CGShading和CGPDFPage对象,都可以通过将其绘制到CGLayer来优化性能。注意一个层不仅仅是用于离屏绘制;我们也可以将其用于那些不是面向屏幕的图形上下文,如PDF图形上下文。
  3. 缓存。虽然我们可以将层用于此目的,但通常不需要这样做,因为Quartz Compositor已经做了此事。如果我们必须绘制一个缓存,则使用层来代替位图图形上下文。

Figure 12-1 Repeatedly painting the same butterfly image

image

CGLayer对象和透明层是与CGPath对象以及CGContext函数创建的路径并行的。对于一个CGLayer或者CGPath对象,我们可以将其绘制到一个抽象目标,之后可以将其完整地绘制到另一个目标,如显示器或才PDF中。当我们在透明层上绘制或者使用绘制路径的CGContext函数时,可以直接绘制到图形上下文表示的目标上,而不需要负责组装绘制的中间抽象目标。

层如何工作

一个层由CGLayerRef数据类型表示,是为优化性能而设计的。在可能的时候,Quartz使用合适的机制将一个CGLayer对象缓存到与之相关的Quartz图形上下文中。例如,与显卡相关的图形上下文可能将层缓存到显卡中,这样绘制在层中的内容时,就比渲染从一个位图图形上下文中构造的类似图像要快得多。基于这个原因,层比位图图形上下文更适用于离屏绘制。

所有的Quartz绘制函数都是绘制到图形上下文中。图形上下文提供了一个抽象的渲染目标,而将我们从目标的细节中解放出来。我们使用用户空间,Quartz执行必要的转换来将绘图正确地渲染到目标。当我们使用CGLayer对象来绘制时,我们也是绘制到图形上下文中。图12-1演示了层绘制的必要步骤。

Figure 12-2 Layer drawing

image

所有在图形上下文中层的绘制都是以使用函数CGLayerCreateWithContext创建一个CGLayer对象开始的。用于创建CGLayer对象的图形上下文通常是一个window图形上下文。Quartz创建一个层,使得它具有图形上下文的所有特性:包括分辨率,颜色空间和图形状态设置。如果我们不想使用图形上下文的大小,则可以提供一个大小给层。在图12-2中,左侧显示了用于创建层的图形上下文。框右侧的灰色部分,即标记为CGLayer对象的部分表示新创建的层。

在我们可以绘制层之前,我们必须通过调用CGLayerGetContext函数来获取与层相关的图形上下文。这个图形上下文与用于创建层的图形上下文是差不多的。只要用于创建层的图形上下文是一个window图形上下文,则CGLayer图形上下文会尽可能地被缓存到GPU中。图12-2中位于框右侧的白色部分表示新创建的层图形上下文。

在层图形上下文中绘制与在其它图形上下文中绘制一样,将层图形上下文作为参数传给绘制函数。图12-2显示了一片绘制到层图形上下文的叶子。

当我们准备使用层的内容时,我们可以调用函数CGContextDrawLayerInRect或者CGContextDrawLayerAtPoint将层绘制到一个图形上下文。通常情况下,我们会将层绘制到创建层对象的图形上下文中,但这不是必须的。我们可以将层绘制到任意的图形上下文,记住:层带有创建层对象的图形上下文的所有特性,这可能会产生一些限制(如性能或分辨率)。例如,与屏幕关联的层可能会被缓存到显卡中。如果目标上下文是一个打印机或PDF上下文,则可能需要将层对象从显卡中取出并放到内存中,从而导致性能很差。

图12-2显示了层的内容–叶子–被重复地绘制到创建层对象的图形上下文中。我们可以在释放CGLayer对象之前,任意地重复使用层中的绘图。

使用层来绘制

我们需要按照如下几个步骤来使用层对象进行绘制:

  1. 创建一个使用已存在的图形上下文初始化的层对象
  2. 为层获取图形上下文
  3. 绘制到CGLayer图形上下文
  4. 将层绘制到目标图形上下文

我们将在下面详细描述这几个步骤。

创建一个使用已存在的图形上下文初始化的层对象

函数CGLayerCreateWithContext返回一个使用已存在的图形上下文初始化的层对象。这个层对象继承了该图形上下文的所有特性,包括颜色空间、大小、分辨率和像素格式。后期当我们绘制层对象到一个目标时,Quartz会自动对层与目标上下文进行颜色匹配。

函数CGLayerCreateWithContext带有三个参数:

  1. 用于创建层的图形上下文。通常我们传递一个window图形上下文以便后面可以离屏绘制层。
  2. 层相对于图形上下文的大小。层的大小可以和图形上下文一样,或者更小。如果想要获得层的大小,我们可以调用函数CGLayerGetSize。
  3. 一个辅助字典。这个参数现在已经不用了,所以传递NULL即可。

为层获取图形上下文

Quartz总是在一个图形上下文中进行绘制。现在我们有了一个层对象,我们必须创建一个与层相关的图形上下文。所有绘制到层图形上下文的内容都是层的一部分。

函数CGLayerGetContext获取一个层对象作为参数,并返回与之相关的图形上下文。

绘制到CGLayer图形上下文

在获取到与层相关的图形上下文之后,我们可以在层图形上下文中绘制任何东西。我们可以打开一个PDF文件或一个图像文件,并将文件内容绘制到层中。我们可以使用Quartz 2D的任何函数来绘制矩形、直线或其它绘制单元。图12-3显示了在层中绘制一个矩形和直线。

Figure 12-3 A layer that contains two rectangles and a series of lines

image

例如,为了在CGLayer图形上下文中绘制一个填充矩形,我们调用函数CGContextFillRect,并提供从CGLayerGetContext函数中获取到的图形上下文作为参数。假设这个图形上下文命名为myLayerContext,则函数调用如下:

CGContextFillRect (myLayerContext, myRect)

将层绘制到目标图形上下文

当我们已经准备好将层绘制到目标图形上下文时,我们可以使用以下任一一个函数:

  1. CGContextDrawLayerInRect:将层绘制到图形上下文中指定的矩形内。
  2. CGContextDrawLayerAtPoint:将层绘制到图形上下文中指定的点。

通常情况下,我们提供的目标图形上下文是一个window图形上下文,这也是我们用于创建层对象所使用的图形上下文。图12-4显示了重复绘制图12-3所绘制的层。为了达到模式效果,我们可以使用上面两个方法中的任意一个,只是每次改变偏移量而已。例如,我们每次绘制层时,可以调用函数CGContextTranslateCTM来改变坐标系统的原点。

Figure 12-4 Drawing a layer repeatedly

image

注意:我们不必要将层绘制到初始层所使用的图形上下文中。然而,如果我们将层绘制到其它图形上下文中,原始图形上下文的所有限制都会反映到我们的绘图中。

例子:使用多个CGLayer对象来绘制旗子

这一节演示了如何使用CGLayer对象来在屏幕上绘制图12-5中的旗子。首先我们会看到如何将旗子分解成简单的绘制单元,然后会看到要完成这些任务的代码。

Figure 12-5 The result of using layers to draw the United States flag

image

从上面可以看出,旗子主要分三部分:

  1. 红色条纹和白色条纹的模式。我们可以将这个模式分解为一个单一的红色条纹,因为对于屏幕绘制来说,我们可以假设其背景颜色为白色。我们创建一个红色矩形,然后以变化的偏移量来重复绘制这个矩形,以创建美国国旗上的七条红色条纹。我们将红色矩形绘制到一个层,然后将其绘制到屏幕上七次。
  2. 一个蓝色矩形。我们只需要一个蓝色矩形,所以没有必要使用层。当绘制蓝色矩形时,直接将其绘制到屏幕上。
  3. 50个白色星星的模式。与红色条纹一下,可以使用层来绘制星星。我们创建星星边框的一个路径,然后使用白条来填充。将一个星星绘制到层,然后重复50次绘制这个层,每次绘制时适当调整偏移量。

代码清单12-2完成了对图12-5的绘制。myDrawFlag例程在一个Cocoa程序中调用。这个程序传递一个window图形上下文和一个与图形上下文相关的视图的大小。

Listing 12-1 Code that uses layers to draw a flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
void myDrawFlag (CGContextRef context, CGRect* contextRect)
{
int i, j,
num_six_star_rows = 5,
num_five_star_rows = 4;
CGFloat start_x = 5.0,
start_y = 108.0,
red_stripe_spacing = 34.0,
h_spacing = 26.0,
v_spacing = 22.0;
CGContextRef myLayerContext1,
myLayerContext2;
CGLayerRef stripeLayer,
starLayer;
CGRect myBoundingBox,
stripeRect,
starField;
// ***** Setting up the primitives *****
CGPoint point1 = {5, 5}, point2 = {10, 15}, point3 = {10, 15}, point4 = {15, 5};
CGPoint point5 = {15, 5}, point6 = {2.5, 11}, point7 = {2.5, 11}, point8 = {16.5, 11};
CGPoint point9 = {16.5, 11}, point10 = {5, 5};
const CGPoint myStarPoints[] = {point1, point2,
point3, point4,
point5, point6,
point7, point8,
point9, point10};
stripeRect = CGRectMake (0, 0, 400, 17); // stripe
starField = CGRectMake (0, 102, 160, 119); // star field
myBoundingBox = CGRectMake (0, 0, contextRect->size.width,
contextRect->size.height);
// ***** Creating layers and drawing to them *****
stripeLayer = CGLayerCreateWithContext (context,
stripeRect.size, NULL);
myLayerContext1 = CGLayerGetContext (stripeLayer);
CGContextSetRGBFillColor (myLayerContext1, 1, 0 , 0, 1);
CGContextFillRect (myLayerContext1, stripeRect);
starLayer = CGLayerCreateWithContext (context,
starField.size, NULL);
myLayerContext2 = CGLayerGetContext (starLayer);
CGContextSetRGBFillColor (myLayerContext2, 1.0, 1.0, 1.0, 1);
CGContextAddLines (myLayerContext2, myStarPoints, 10);
CGContextFillPath (myLayerContext2);
// ***** Drawing to the window graphics context *****
CGContextSaveGState(context);
for (i=0; i< 7; i++)
{
CGContextDrawLayerAtPoint (context, CGPointZero, stripeLayer);
CGContextTranslateCTM (context, 0.0, red_stripe_spacing);
}
CGContextRestoreGState(context);
CGContextSetRGBFillColor (context, 0, 0, 0.329, 1.0);
CGContextFillRect (context, starField);
CGContextSaveGState (context);
CGContextTranslateCTM (context, start_x, start_y);
for (j=0; j< num_six_star_rows; j++)
{
for (i=0; i< 6; i++)
{
CGContextDrawLayerAtPoint (context,CGPointZero,
starLayer);
CGContextTranslateCTM (context, h_spacing, 0);
}
CGContextTranslateCTM (context, (-i*h_spacing), v_spacing);
}
CGContextRestoreGState(context);
CGContextSaveGState(context);
CGContextTranslateCTM (context, start_x + h_spacing/2,
start_y + v_spacing/2);
for (j=0; j< num_five_star_rows; j++)
{
for (i=0; i< 5; i++)
{
CGContextDrawLayerAtPoint (context, CGPointZero,
starLayer);
CGContextTranslateCTM (context, h_spacing, 0);
}
CGContextTranslateCTM (context, (-i*h_spacing), v_spacing);
}
CGContextRestoreGState(context);
CGLayerRelease(stripeLayer);
CGLayerRelease(starLayer);
}

在此不再翻译对代码的注释,请各位看官查看文档原文Core Graphics Layer Drawing。

Quartz 2D编程指南之十一:位图与图像遮罩

发表于 2015-01-05   |   分类于 翻译

位图与图像遮罩和Quartz中的其它绘制元素一样。这两者在Quartz中都是用CGImageRef数据类型来表示。正如在本章后面看到的一样,我们有一系列的方法来创建一个图像。其中一些需要数据提供者或图像源来提供位图数据。另外一些函数则通过拷贝图像或在图像上应用操作来从已存在的图像中创建图像。不管我们是以何种方式来创建图像,我们都可以将图像绘制到任何类型的图形上下文。记住,位图是在指定分辨率下的一个字节数组。如果我们将位图绘制到一个依赖于分辨率的图形上下文中(如PDF图形上下文),则位图受限于创建它的图形上下文的分辨率。

我们可以通过调用CGImageMaskCreate函数来创建一个Quartz图像遮罩。我们将在“创建图像遮罩”一节中看到如何创建遮罩。使用图像遮罩不是绘制遮罩的唯一方法,具体的我们都会在下面看到。

位图和图像遮罩

一个位图是一个像素数组。每一个像素表示图像中的一个点。JPEG, TIFF和PNG图像文件都是位图。应用程序的icon也是位图。位图被限定在一个矩形内。但是通过使用alpha分量,它们可以呈现不同的形式,也可以旋转或被裁剪,如图11-1所示:

Figure 11-1 Bitmap images

image

位图中的每一个采样包含特定颜色空间下的一个或更多颜色分量,以及一个额外的用于指定alpha值以表示透明度的分量。每一个分量可以是从1-32位。在Mac OS X中,Quartz支持浮点值分量。在Mac OS X和iOS中支持的格式将会在下文中介绍。ColorSync提供了位图支持的颜色空间。

Quartz同样支持图像遮罩(image masks)。一个图像遮罩也是一个位图,它指定了一个绘制区域,而不是颜色。从效果上来说,一个图像遮罩更像一个模块,它指定在page中绘制颜色的位置。Quartz使用当前的填充颜色来绘制一个图像遮罩。一个颜色遮罩可以有1-8位的深度。

位图信息

Quartz提供了很多图像格式并内建了多种常用的格式。在iOS中,这些格式包括JPEG, GIF, PNG, TIF, ICO, GMP, XBM, 和CUR。其它的位图格式或专有格式需要我们指定图像格式的详细信息,以便Quartz能正确地解析图像。我们提供给CGImageCreate函数的图像数据必须是以像素为单位的,而不是基于扫描线的。Quartz不支持平面数据。

这一节描述了与位图相关的信息。当我们创建并使用Quartz图像时(使用CGImageRef数据类型),我们将看到一些Quartz图像创建函数需要我们指定所有的信息,而其它函数只需要部分信息。我们所需要提供的信息依赖于位图数据的编码,以及位图是表示一个图像还是图像遮罩。

注意:当使用原始图像数据时,为了获得更好的性能,我们可以使用vImage框架。我们可以使用vImageBuffer_InitWithCGImage函数从一个CGImageRef引用导入图像数据到vImage中。

创建一个位图(CGImageRef)时,Quartz使用以下信息:

  1. 位图数据源:可以是一个Quartz数据提供者或者是一个Quartz图像源。
  2. 可选的解码数组。(Decode Array)
  3. 插值设置:这是一个布尔值,指定Quartz在重置图像大小时是否使用插值算法。
  4. 渲染意图:指定如何映射位于图形上下文中的目标颜色空间中的颜色。该值在图像遮罩中不需要。
  5. 图像尺寸
  6. 像素格式,包括每个分量中的位数,每个像素的位数和每行中的字节数。
  7. 对于图像来说,颜色空间和位图布局信息描述了alpha的位置和位置是否使用浮点值。图像遮罩不需要这个信息。

解码数组

一个解码数组将图像颜色值映射到其它颜色值,这对于诸如对一个图像做去饱和或者反转颜色值非常有用。数组包含每个颜色分量的一个数值对。当Quartz渲染图像时,它利用一个线性转换将原始分量值映射到一个目标颜色空间中的指定范围内一个相关值。例如,在RGB颜色空间中的一个图像的解码数组包含6个输入,分别用于红、绿、蓝颜色分量。

像素格式

像素格式包含以下信息:

  1. 每个分量的位数,即在一个像素中每个独立颜色分量的位数。对于一个图像遮罩,这个值是源像素中遮罩bit的数目。例如,如果源图片是8-bit的遮罩,则指定每个分量是8位。
  2. 每个像素的位数,即一个源像素所占的总的位数。这个值必须至少是每个分量的位数乘以每个像素中分量的数目。
  3. 每行的字节数,即图像中水平行的字节数。

颜色空间和位图布局

为了确保Quartz能正确的解析每个像素的位,我们必须指定:

  1. 一个位图是否包含alpha通道。Quartz包含RGB,CMYK和灰度颜色空间。它也支持alpha,或者透明度,虽然并不是所有位图图像格式都支持alpha通道。当它可用时,alpha分量可以位于像素最显著的位置,也可以是最不显著的位置。
  2. 对于有alpha分量的位图,指定颜色分量是否已经乘以了alpha值。预乘alpha(Premultiplied alpha)表示一个已将颜色分量乘以了alpha值的源颜色。这种预处理通过消除每个颜色分量的额外的乘法运算来加速图片的渲染。
  3. 采样的数据格式–是整型还是浮点型。

当我们使用CGImageCreate函数来创建一个图像时,我们提供一个类型为CGImageBitmapInfo的bitmapInfo参数,来指定位置布局信息。以下的常量指定了alpha分量的位置及颜色分量是否做预处理:

  1. kCGImageAlphaLast:alpha分量存储在每个像素中最不显著的位置,如RGBA。
  2. kCGImageAlphaFirst:alpha分量存储在每个像素中最显著的位置,如ARGB。
  3. kCGImageAlphaPremultipliedLast:alpha分量存储在每个像素中最不显著的位置,但颜色分量已经乘以了alpha值。
  4. kCGImageAlphaPremultipliedFirst:alpha分量存储在每个像素中最显著的位置,同时颜色分量已经乘以了alpha值。
  5. kCGImageAlphaNoneSkipLast:没有alpha分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间,则最不显著位置的位将被忽略。
  6. kCGImageAlphaNoneSkipFirst:没有alpha分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间,则最显著位置的位将被忽略。
  7. kCGImageAlphaNone:等于kCGImageAlphaNoneSkipLast。

我们使用常量kCGBitmapFloatComponents来标识一个位图格式使用浮点值。对于浮点格式,我们将这个常量与上而描述的合适的常量进行逻辑OR操作。例如,对于每个像素有128位的使用预处理的浮点格式,同时alpha值位于像素中最不显示位置,我们将以下信息提供给Quartz:

1
kCGImageAlphaPremultipliedLast | kCGBitmapFloatComponents

图11-2演示了一个像素在使用16-或32-bit整型像素格式的CMYK和RGB颜色空间中如何表示。32-bit整型像素格式中,每个分量占8位。16-bit整型像素格式中每个分量占5位。Quartz同样支持128-bit浮点像素格式,每个分量占32位。128-bit格式没有显示在下图中。

Figure 11-2 32-bit and 16-bit pixel formats for CMYK and RGB color spaces in Quartz 2D

image

创建图像

表11-1罗列了Quartz提供的用于创建CGImageRef对象的函数。函数的选择依赖于图像的数据源。最常用的函数是CGImageCreate。它可以从任何类型的位图数据来创建一个图像。然而,它是最复杂的函数,因为需要提供所有的位图信息。为了使用这个函数,我们需要熟悉上面讨论的位图图像信息的内容。

如果我们想从一个标准的图像格式,如PNG或JPEG,来创建一个CGImage对象,则最简单的方法是调用函数CGImageSourceCreateWithURL来创建一个图像源,然后调用CGImageSourceCreateImageAtIndex以使用从图像源中索引index指定的图像数据来创建一个图像。如果源图像文件只包含一个图像,则索引为0。如果图像文件格式支持包含多个图像的文件,则需要提供所需要图像的索引值,记住起始值是0。

如果我们已经将内容渲染到一个位图图形上下文,并想要从中获取到CGImage对象,则调用CGBitmapContextCreateImage函数。

有几个函数可以操作已有的图像,如拷贝、创建一个缩略图,或从一个大图像中创建一个图像。不管如何创建一个图像对象,我们都使用函数CGContextDrawImage将图像绘制到一个图形上下文中。记住CGImage是不可变的。当不再需要一个CGImage对象时,调用CGImageRelease函数来释放它。

image

接下来将讨论如何创建:

  1. 从一个已存在图像中创建一个子图像
  2. 从一个图像图形上下文中创建一个图像

从一个大图片中创建一个图像

我们可以使用CGImageCreateWithImageInRect函数从一个大图像中创建一个图像。图11-3演示了这一情形。

Figure 11-3 A subimage created from a larger image

image

函数CGImageCreateWithImageInRect返回的图像保留了源图像的一个引用,这意味着我们在调用完这个函数后可以释放源图像。

图11-4是另外一个例子。在这种情况下,公鸡的头部被从大图中提取出来,然后绘制到一个大于子图像的矩形中。

代码清单11-1显示了创建并绘制子图像的过程。CGContextDrawImage函数绘制公鸡头部的矩形区域是所提取的子图像的四倍大小。清单中的只是一个代码片断。我们需要声明合适的变量,创建公鸡头像,并部署公鸡图像及公鸡头部子图像。因为只是代码片断,所以没有演示如何创建一个图形上下文。我们可以使用任何我们所喜欢的图形上下文。创建图形上下文的例子可以查看“图形上下文”一章。

Figure 11-4 An image, a subimage taken from it and drawn so it’s enlarged

image

Listing 11-1 Code that creates a subimage and draws it enlarged

1
2
3
4
5
myImageArea = CGRectMake (rooster_head_x_origin, rooster_head_y_origin,
myWidth, myHeight);
mySubimage = CGImageCreateWithImageInRect (myRoosterImage, myImageArea);
myRect = CGRectMake(0, 0, myWidth*2, myHeight*2);
CGContextDrawImage(context, myRect, mySubimage);

从一个位图图形上下文创建一个图像

为了从一个已存在的位图图形上下文创建一个图像,我们可以调用函数CGBitmapContextCreateImage,如以下:

1
2
CGImageRef myImage;
myImage = CGBitmapContextCreateImage (myBitmapContext);

这个函数返回的CGImage对象是通过一个拷贝操作创建的。因此我们对位图图形上下文所做的修改都不会影响到已返回的CGImage对象。在一些情况下,这个拷贝操作实际上沿用了copy-on-write语义,即只有当位图图形上下文中的数据被修改时才会去实际拷贝这些数据。我们可能需要在绘制额外数据到位图图形上下文之前使用结果数据或者释放它们,以便我们可以避免实际去拷贝这些数据。

如何创建一个位图图形上下文,可以参考”创建图形上下文”相关的内容。

创建一个图像遮罩

一个Quartz位图图像遮罩如同艺术家使用丝网印刷品(silkscreen)一样。一个位图图像遮罩定义了如何转换颜色,而不是使用哪些颜色。图像遮罩中的每个采样值指定了在特定位置中,当前填充颜色值被遮罩的数量。采样值指定了遮罩的不透明度。值越大,表示越不透明,Quartz在指定位置绘制的颜色越少。我们可以将采样值当成alpha值的反转。1表示透明的,而0表示不透明。

图像遮罩的每个分量可能是1,2,4或者8位。对于1-bit的遮罩,采样值1指定遮罩的区域掩盖了当前的填充颜色。值为0表示当绘制遮罩时,显示当前的填充颜色。我们可以将1-bit遮罩当成黑色和白色;要么完全遮挡,要么完全显示。

每个分量中有2,4,8位的图像遮罩代表灰度值。每个分量使用以下的公式将值映射到[0, 1]之间的值:

image

例如,一个4-bit的遮罩其值位于[0, 1]之间,且增长的步长为1/15。0和1这两个值分别是最小和最大值–分别表示完全遮盖或完全透明。0和1之间的值使用(1-MaskSampleValue)这个公式来处理局部绘制。例如,如果一个8-bit遮罩的采样值设置为0.7,则那些alpha值为(1-0.7),即0.3的颜色将会被绘制。

函数CGImageMaskCreate从我们提供的位图图像信息中创建一个Quartz图像遮罩。我们提供的信息与创建图像所提供的信息是一样的,只是不需要提供颜色空间信息,位图信息常量或渲染意图,我们可以从代码清单11-2中看到这个函数原型:

Listing 11-2 The prototype for the function CGImageMaskCreate

1
2
3
4
5
6
7
8
9
10
CGImageRef CGImageMaskCreate (
size_t width,
size_t height,
size_t bitsPerComponent,
size_t bitsPerPixel,
size_t bytesPerRow,
CGDataProviderRef provider,
const CGFloat decode[],
bool shouldInterpolate
);

遮罩图像

遮罩技术可以让我们通过控制图片的哪一部分被绘制,以生成很多有趣的效果,我们可以:

  1. 在一个图像上使用图像遮罩。我们也可以把一个图像作为遮罩图,以获取同使用图像遮罩相反的效果。
  2. 使用颜色来遮罩图像的一部分,其中包含被称为颜色遮罩的技术
  3. 将图形上下文剪切到一个图像或图像遮罩,当Quartz绘制内容到剪切的图形上下文时来遮罩一个图像。

使用一个图像遮罩来遮罩图像

函数CGImageCreateWithMask通过将图像遮罩使用到一个图像上的方式来创建一个图像。这个函数带有两个参数:

  1. 原始图像,遮罩将用于其上。这个图像不能是图像遮罩,也不能有与之相关的遮罩颜色。
  2. 一个图像遮罩,通过调用CGImageMaskCreate函数创建的。也可以提供一个图像来替代图像遮罩,但这将给出非常不同的结果。这将在下面描述。

一个图像遮罩的采样如同一个反转的alpha值。一个图像遮罩采样值(S):

  1. 为1时,则不会绘制对应的图像样本。
  2. 为0时,则允许完全绘制对应的图像样本。
  3. 0和1之间的值,则让对应的图像样本的alpha的值为(1-S)。

图11-5显示了一个由Quartz图像创建函数创建的图像,而图11-6显示了一个使用CGImageMaskCreate函数创建的图像遮罩。图11-7则显示了一个使用CGImageCreateWithMask函数将图像遮罩应用于一个图像的效果。

Figure 11-5 The original image

image

Figure 11-6 An image mask

image

注意,源图像中与遮罩黑色区域对应的区域绘制出来,而与白色区域对应的部分则没有绘制出来。而与遮罩灰色区域对应的区域则使用一个与(1-图像遮罩采样值)相同的alpha值来绘制。

Figure 11-7 The image that results from applying the image mask to the original image

image

使用一个图像来遮罩一个图像

我们可以使用函数CGImageCreateWithMask来用一个图像遮罩另一个图像,而不是使用一个图像遮罩。我们可以使用这种方式来达到与使用图像遮罩相反的效果。那此时我们传递给CGImageCreateWithMask函数的就不是一个图像遮罩了,而是传递一个通过Quartz图像创建函数创建的图像。

用于遮罩的图像的采样也是操作alpha值。一个图像采样值(S):

  1. 为1时,则允许完全绘制对应的图像样本。
  2. 为0时,则不会绘制对应的图像样本。
  3. 0和1之间的值,则让对应的图像样本的alpha的值为S。

图11-8显示了调用CGImageCreateWithMask函数将图11-6中的图像作为遮罩应用于图11-5中的图像上的效果。在这个例子中,我们假定图11-6中的图像是使用Quartz图像创建函数(如CGImageCreate)创建的。比较图11-8与图11-7,可以看出使用图像采样时,可以获取与使用图像遮罩采样相反的效果。

在图11-8的结果图像中,源图像中与图像的黑色区域对应的区域没有绘制出来。与白色区域对应的区域则绘制出来了。在遮罩中与灰色区域对应的区域则使用与遮罩图像采样值相同的alpha值来绘制。

Figure 11-8 The image that results from masking the original image with an image

image

使用颜色来遮罩图像

函数CGImageCreateWithMaskingColors通过遮罩一种颜色或一个颜色范围内的颜色来创建一个图像。使用这个函数,我们可以执行如图11-9所示的颜色遮罩,当然也可以遮罩一个范围内的颜色,如图11-11、11-12和11-13所示的效果。

函数CGImageCreateWithMaskingColors有两个参数:

  1. 一个图像,它不能是遮罩图像,也不能是使用过图像遮罩或颜色遮罩的图像。
  2. 一个颜色分量数组,指定了一个颜色或一组颜色值,以用于遮罩图像。

Figure 11-9 Chroma key masking

image

颜色分量数组中元素的个数必须等于图像所在颜色空间的颜色分量数目的两倍。对于颜色空间中的每一个颜色分量,提供一个最小值和一个最大值来限定遮罩颜色的范围。如果只使用一个颜色,则设置最大值等于最小值即可。颜色分量数组中的值按以下顺序来提供:

{min[1], max[1], ... min[N], max[N]},其中N是分量的数目

如果图像使用整型像素分量,则颜色分量数组中的每个值必须在[0 .. 2^bitsPerComponent - 1]范围之内。如果图像使用浮点像素分量,则值可以是表示任何有效的颜色分量值的浮点数。

一个图像采样如果其颜色值在以下范围内,则不会被绘制:

{c[1], ... c[N]},其中min[i] <= c[i] <= max[i] for 1 <= i <= N

图11-10中两只老虎的图像使用了每个分量有8位的RGB颜色空间。为了在这个图像上屏蔽一组颜色,我们提供一组在[0, 255]区间内的最小和最大颜色分量值。

Figure 11-10 The original image

image

代码清单11-3演示了如何设置颜色分量数组,并将其提供给CGImageCreateWithMaskingColors函数以达到图11-11的效果。

Listing 11-3 Masking light to mid-range brown colors in an image

1
2
3
4
5
CGImageRef myColorMaskedImage;
const CGFloat myMaskingColors[6] = {124, 255, 68, 222, 0, 165};
myColorMaskedImage = CGImageCreateWithMaskingColors (image,
myMaskingColors);
CGContextDrawImage (context, myContextRect, myColorMaskedImage);

Figure 11-11 An image with light to midrange brown colors masked out

image

代码清单11-14同样操作图11-10并得到图11-12的效果。这个例子遮罩了一组暗色。

Listing 11-4 Masking shades of brown to black

1
2
3
4
5
CGImageRef myMaskedImage;
const CGFloat myMaskingColors[6] = { 0, 124, 0, 68, 0, 0 };
myColorMaskedImage = CGImageCreateWithMaskingColors (image,
myMaskingColors);
CGContextDrawImage (context, myContextRect, myColorMaskedImage);

Figure 11-12 A image after masking colors from dark brown to black

image

我们同样可以设置一个填充颜色来作为图像的遮罩颜色,以达到图11-13的效果,其中被遮罩区域使用了填充颜色。代码清单11-15演示了这一过程

Listing 11-5 Masking a range of colors and setting a fill color and

1
2
3
4
5
6
7
CGImageRef myMaskedImage;
const CGFloat myMaskingColors[6] = { 0, 124, 0, 68, 0, 0 };
myColorMaskedImage = CGImageCreateWithMaskingColors (image,
myMaskingColors);
CGContextSetRGBFillColor (myContext, 0.6373,0.6373, 0, 1);
CGContextFillRect(context, rect);
CGContextDrawImage(context, rect, myColorMaskedImage);

Figure 11-13 An image drawn after masking a range of colors and setting a fill color

image

通过裁减上下文来遮罩一个图片

函数CGContextClipToMask将遮罩映射为一个矩形并将其与图形上下文的当前裁减区域求个交集。我们提供以下参数:

  1. 需要裁减的图形上下文
  2. 要使用遮罩的矩形区域
  3. 一个图像遮罩,其通过CGImageMaskCreate函数创建。我们可以使用图像来替代图像遮罩以达到相反的效果。但图像必须使用Quartz图像创建函数来创建,但不能是使用过图像遮罩或颜色遮罩的图像。

裁减区域的结果依赖于是否提供了一个图像遮罩或图像给CGContextClipToMask函数。

我们看看图11-14.假设它是通过调用CGImageMaskCreate函数创建的一个图像遮罩,然后将其作为CGContextClipToMask函数的参数。结果上下文允许绘制黑色区域,而不绘制白色区域,并使用(1-S)的alpha值来绘制灰色区域,其中S是图像遮罩的采样值。如果使用CGContextDrawImage函数来将一个图像绘制到裁减上下文,则可以获得图11-15所示的结果。

Figure 11-14 A masking image

image

Figure 11-15 An image drawn to a context after clipping the content with an image mask

image

当遮罩图像被当成一个图像时,可以获得相反的结果,如图11-16所示:

Figure 11-16 An image drawn to a context after clipping the content with an image

image

在图像中使用混合模式

此处略,类似于在颜色中使用混合模式。

Quartz 2D编程指南之十:Quartz 2D中的数据管理

发表于 2014-12-11   |   分类于 翻译

管理数据是每一个图形应用程序所需要处理的工作。对于Quartz来说,数据管理涉及为Quartz 2D程序提供数据,及从中获取数据。一些Quartz 2D程序将数据传输到Quartz中,如从文件或程序其它部分获取图片或PDF数据。另一些程序则获取Quartz数据,如将图像或PDF数据写入到文件,或提供给程序其它部分这些数据。

Quartz提供了一系列的函数来管理数据。通过学习本章,我们可以了解到哪些函数是最适合我们的程序的。

注:我们推荐使用图像I/O框架来读取和写入数据,该框架在iOS 4、Mac OS X 10.4或者更高版本中可用。查看《Image I/OProgramming Guide 》可以获取更多关于CGImageSourceRef和CGImageDestinationRef的信息。图像源和目标不仅提供了访问图像数据的方法,不提供了更多访问图像原数据的方法。

Quartz可识别三种类型的数据源和目标:

  1. URL:通过URL指定的数据可以作为数据的提供者和接收者。我们使用Core Foundation数据类型CFURLRef作为参数传递给Quartz函数。
  2. CFData:Core Foundation数据类型CFDataRef和CFMutableDataRef可简化Core Foundation对象的内存分配行为。CFData是一个”toll-freebridged”类,CocoaFoundation中对应的类是NSData;如果在Quartz 2D中使用Cocoa框架,你可以传递一个NSData对象给Quartz方法,以取代CFData对象。
  3. 原始数据:我们可以提供一个指向任何类型数据的指针,连同处理这些数据基本内存管理的回调函数集合。

这些数据,无论是URL、CFData对象,还是数据缓存,都可以是图像数据或PDF数据。图像数据可以是任何格式的数据。Quartz能够解析大部分常用的图像文件格式。一些Quartz数据管理函数专门用于处理图像数据,一些只处理PDF数据,还有一些可同时处理PDF和图像数据。

URL,CFData和原始数据源和目标中的数据都是在Mac OS X 或者iOS图像领域范围之外的,如图10-1所示。Mac OS X或iOS的其它图像技术通常会提供它们自己的方式来和Quartz通信。例如,一个Mac OS X 应用程序可以传输一个Quartz图像给Core Image,并使用Core Image来实现更复杂的效果。

Figure 10-1 Moving data to and from Quartz 2D in Mac OS X

image

传输数据给Quartz 2D

表10-1列出了从数据源获取数据的方法。所有的这些函数,除了CGPDFDocumentCreateWithURL,都返回一个图像源(CGImageSourceRef)或者数据提供者(CGDataProviderRef)。图像源和数据提供者抽象了数据访问工作,并避免了程序去管理原始内存缓存。

图像源是将图像数据传输给Quartz的首先方式。图像源可表示很多种图像数据。一个图像源可表示多于一个图像,也可表示缩略图、图像的属性和图像文件。当我们拥有CGImageSourceRef对象后,我们可以完成如下工作:

  1. 使用函数CGImageSourceCreateImageAtIndex,CGImageSourceCreateThumbnailAtIndex, CGImageSourceCreateIncremental创建图像(CGImageRef). 一个CGImageRef数据类型表示一个单独的Quartz图像。
  2. 通过函数CGImageSourceUpdateData或CGImageSourceUpdateDataProvider来添加内容到图像源中。
  3. 使用函数CGImageSourceGetCount, CGImageSourceCopyProperties和CGImageSourceCopyTypeIdentifiers获取图像源的信息。

CFPDFDocumentCreateWithURL函数可以方便地从URL指定的文件创建PDF文档。

数据提供者是比较老的机制,它有很多限制。它们可用于获取图像或PDF数据。

我们可以将数据提供者用于:

  1. 一个图像创建函数,如CGImageCreate,CGImageCreateWithPNGDataProvider或者CGImageCreateWithJPEGDataProvider。
  2. PDF文档的创建函数CGPDFDocumentCreateWithProvider.
  3. 函数CGImageSourceUpdateDataProvider用于更新已存在的图像源。

关于图像的更多信息,可查看“Bitmap Images andImage Masks”。

Table 10-1 Functions that move data into Quartz 2D

image

获取Quartz 2D的数据

表10-2列出地从Quartz 2D中获取数据的方法。所有这些方法,除了CGPDFContextCreateWithURL,都返回一个图像目标(CGImageDestinationRef)或者是数据消费者(CGDataComsumerRef)。图像目标和数据消费者抽象的数据写入工作,让Quartz来处理细节。

一个图像目标是获取Quartz数据的首先方法。与图像源一样,图像目标也可以表示很多图像数据,如一个单独图片、多个图片、缩略图、图像属性或者图片文件。在获取到CGImageDestinationRef后,我们可以完成以下工作:

  1. 使用函数CGImageDestinationAddImage或者CGImageDestinationAddImageFromSource添加一个图像(CGImageRef)到目标中。一个CGImageRef表示一个图片。
  2. 使用函数CGImageDestinationSetProperties设置属性
  3. 使用函数CGImageDestinationCopyTypeIdentifiers和CGImageDestinationGetTypeID从图像目标中获取信息。

函数CGPDFContextCreateWithURL可以方便地将PDF数据写入URL指定的位置。

数据消费者是一种老的机制,有很多限制。它们用于写图像或PDF数据。我们可以将数据消费者用于:

  1. PDF上下文创建函数CGPDFContextCreate。该函数返回一个图形上下文,用于记录一系列的PDF绘制命令。
  2. 函数CGImageDestinationCreateWithDataConsumer,用于从数据消费者中创建图像目标。

关于图像的更多信息,可查看“Bitmap Images andImage Masks”。

Table 10-2 Functions that move data out of Quartz 2D

image

Quartz 2D编程指南之九:透明层

发表于 2014-12-10   |   分类于 翻译

透明层(TransparencyLayers)通过组合两个或多个对象来生成一个组合图形。组合图形被看成是单一对象。当需要在一组对象上使用特效时,透明层非常有用,如图9-1所示的给三个圆使用阴影的效果。

Figure 9-1 Three circles as a composite in a transparency layer

image

如果没有使用透明层来渲染图9-1中的三个圆,对它们使用阴影的效果将是如图9-2所示:

Figure 9-2 Three circles painted as separate entities

image

透明层的工作方式

Quartz的透明层类似于许多流行的图形应用中的层。层是独立的实体。Quartz维护为每个上下文维护一个透明层栈,并且透明层是可以嵌套的。但由于层通常是栈的一部分,所以我们不能单独操作它们。

我们通过调用函数CGContextBeginTransparencyLayer来开始一个透明层,该函数需要两个参数:图形上下文与CFDictionary对象。字典中包含我们所提供的指定层额外信息的选项,但由于Quartz 2D API中没有使用字典,所以我们传递一个NULL。在调用这个函数后,图形状态参数保持不变,除了alpha值[默认设置为1]、阴影[默认关闭]、混合模式[默认设置为normal]、及其它影响最终组合的参数。

在开始透明层操作后,我们可以绘制任何想显示在层上的对象。指定上下文中的绘制操作将被当成一个组合对象绘制到一个透明背景上。这个背景被当作一个独立于图形上下文的目标缓存。

当绘制完成后,我们调用函数CGContextEndTransparencyLayer。Quartz将结合对象放入上下文,并使用上下文的全局alpha值、阴影状态及裁减区域作用于组合对象。

在透明层中进行绘制

在透明层中绘制需要三步:

  1. 调用函数CGContextBeginTransparencyLayer
  2. 在透明层中绘制需要组合的对象
  3. 调用函数CGContextEndTransparencyLayer

图9-3显示了在透明层中绘制三个矩形,其中将这三个矩形当成一个整体来渲染阴影。

Figure 9-3 Three rectangles painted to a transparency layer

image

代码清单9-1显示了如何利用透明层生成图9-3所示的矩形。

Listing 9-1 Painting to a transparency layer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MyDrawTransparencyLayer (CGContext myContext, float wd,float ht)
{
CGSize myShadowOffset = CGSizeMake (10, -20);
CGContextSetShadow (myContext, myShadowOffset, 10);
CGContextBeginTransparencyLayer (myContext, NULL);
// Your drawing code here
CGContextSetRGBFillColor (myContext, 0, 1, 0, 1);
CGContextFillRect (myContext, CGRectMake (wd/3+ 50,ht/2 ,wd/4,ht/4));
CGContextSetRGBFillColor (myContext, 0, 0, 1, 1);
CGContextFillRect (myContext, CGRectMake (wd/3-50,ht/2-100,wd/4,ht/4));
CGContextSetRGBFillColor (myContext, 1, 0, 0, 1);
CGContextFillRect (myContext, CGRectMake (wd/3,ht/2-50,wd/4,ht/4));
CGContextEndTransparencyLayer (myContext);
}

Quartz 2D编程指南之八:渐变

发表于 2014-12-10   |   分类于 翻译

Quartz提供了两个不透明数据odgago创建渐变:CGShadingRef和CGGradientRef。我们可以使用任何一个来创建轴向(axial)或径向(radial)渐变。一个渐变是从一个颜色到另外一种颜色的填充。

一个轴向渐变(也称为线性渐变)沿着由两个端点连接的轴线渐变。所有位于垂直于轴线的某条线上的点都具有相同的颜色值。

一个径向渐变也是沿着两个端点连接的轴线渐变,不过路径通常由两个圆来定义。

本章提供了一些我们使用Quartz能够创建的轴向和径向渐变的类型的例子,并比较绘制渐变的两种方法,然后演示了如何使用每种不透明数据类型来创建渐变。

轴向和径向渐变实例

Quartz函数提供了一个丰富的功能来创建渐变效果。这一部分显示了一些我们能达到的效果。图8-1中的轴向渐变由橙色向黄色渐变。在这个例子中,渐变轴相对于原点倾斜了45度角。

Figure 8-1 An axial gradient along a 45 degree axis

image

Quartz也允许我们指定一系列的颜色和位置值,以沿着轴来创建更复杂的轴向渐变,如图8-2所示。起始点的颜色值是红色,结束点的颜色是紫罗兰色。同时,在轴上有五个位置,它们的颜色值分别被设置为橙、黄、绿、蓝和靛蓝。我们可以把它看成沿着同一轴线的六段连续的线性渐变。虽然这里的轴线与图8-1是一样的,但这不是必须的。轴线的角度由我们提供的两个端点定义。

Figure 8-2 An axial gradient created with seven locations and colors

image

图8-3显示了一个径向渐变,它从一个小的明亮的红色圆渐变到一个大小黑色的圆。

Figure 8-3 A radial gradient that varies between two circles

image

使用Quartz,我们不局限于创建颜色值改变的渐变;我们可以只修改alpha值,或者创建alpha值与其它颜色组件一起改变的渐变。图8-4显示了一个渐变,其红、绿、蓝组件的值是不变的,但alpha值从1.0渐变到0.1。

注意:如果我们使用alpha值来改变一个渐变,则在绘制一个PDF内容时我们不能捕获这个渐变。因此,这样的渐变无法打印。如果需要绘制一个渐变到PDF,则需要让alpha值为1.0。

Figure 8-4 A radial gradient created by varying only the alpha component

image

我们可以把一个圆放置到一个径向渐变中来创建各种形状。如果一个圆是另一个的一部分或者完全在另一个的外面,则Quartz创建了圆锥和一个圆柱。径向渐变的一个通常用法就是创建一个球体阴影,如图8-5所示。在这种情况下,一个单一的点(半径为0的圆)位于一个大圆以内。

Figure 8-5 A radial gradient that varies between a point and a circle

image

我们可以像图8-6一样通过内嵌几个径向渐变来创建更复杂的效果。它使用同心圆来创建图形中的各环形部分。

Figure 8-6 Nested radial gradients

image

CGShading和CGGradient对象的对比

我们有两个对象类型用于创建渐变,你可能想知道哪一个更好用。本节就来回答这个问题。

CGShadingRef这个不透明数据类型给我们更多的控制权,以确定如何计算每个端点的颜色。在我们创建CGShading对象之前,我们必须创建一个CGFunction对象(CGFunctionRef),这个对象定义了一个用于计算渐变颜色的函数。写一个自定义的函数让我们能够创建平滑的渐变,如图8-3,8-3和8-5及更多非传统的效果,如图8-12所示。

当创建一个CGShading对象时,我们指定其是轴向还是径向。除了计算函数外,我们还需要提供一个颜色空间、起始点和结束点或者是半径,这取决于是绘制轴向还是径向渐变。在绘制时,我们只是简单地传递CGShading对象及绘制上下文给CGContextDrawShading函数。Quartz为渐变上的每个点调用渐变计算函数。

一个CGGradient对象是CGShading对象的子集,其更易于使用。CGGradientRef不透明类型易于作用,因为Quartz在渐变的每一个顶点上计算颜色值。我们不需要提供一个渐变计算函数。当创建一个渐变对象时,我们提供一个位置和颜色的数组。Quartz使用对应的颜色值来计算每个梯度的渐变,。我们可以使用单一的起始与结束点来设置一个渐变对象,如图8-1所示,或者提供一组端点来创建一个类似于图8-2的的效果。使用CGShading对象可以提供多于两个位置的能力。

当我们创建一个CGGradient对象时,我们需要设置一个颜色空间、位置、和每个位置对应的颜色值。当使用一个渐变对象绘制上下文时,我们指定Quartz是绘制一个轴向还是径向渐变。在绘制时,我们指定开始结束点或半径,这取决于我们是绘制轴向还是径向渐变。而CGShading的几何形状是在创建时定义的,而不是绘制时。

表8-1总结了两种不透明数据类型之间的区别。

image

扩展渐变端点外部的颜色

当我们创建一个渐变时,我们可以选择使用纯色来填充渐变端点外部的空间。Quartz使用使用渐变边界上的颜色作为填充颜色。我们可以扩展渐变起点、终点或两端的颜色。我们可以扩展使用CGShading对象或CGGradient对象创建的轴向或径向渐变。

图8-7演示了一个轴向渐变,它扩展了起点和终点两侧的区域。图片中的线段显示了渐变的轴线。我们可以看到,填充颜色与起点和终点的颜色是对应的。

Figure 8-7 Extending an axial gradient

image

图8-8对比了一个未使用扩展的径向渐变和一个在起点和终点两侧使用扩展的径向渐变。Quartz获取了起点和终点的颜色值,并使用这边纯色值来扩展立面。

Figure 8-8 Extending a radial gradient

image

使用CGGradient对象

一个CGGradient对象是一个渐变的抽象定义–它简单地指定了颜色值和位置,但没有指定几何形状。我们可以在轴向和径向几何形状中使用这个对象。作为一个抽象定义,CGGradient对象可能比CGShading对象更容易重用。没有将几何形状存储在CGGradient对象中,这样允许我们使用相同的颜色方案来绘制不同的几何图形,而不需要为多个图形创建多个CGGradient对象。

因为Quartz为我们计算渐变,使用一个CGGradient对象来创建和绘制一个渐变则更直接,只需要以下几步:

  1. 创建一个CGGradient对象,提供一个颜色空间,一个饱含两个或更多颜色组件的数组,一个包含两个或多个位置的数组,和两个数组中元素的个数。
  2. 调用CGContextDrawLinearGradient或CGContextDrawRadialGradient函数并提供一个上下文、一个CGGradient对象、绘制选项和开始结束几何图形来绘制渐变。
  3. 当不再需要时释放CGGradient对象。

一个位置是一个值区间在0.0到1.0之间的CGFloat值,它指定了沿着渐变的轴线的标准化距离。值0.0指定的轴线的起点,1.0指定了轴线的终点。其它的值指定了一个距离的比例。最低限度情况下,Quartz使用两个位置值。如果我们传递NULL值作为位置数组参数,则Quartz使用0作为第一个位置,1作为第二个位置。

每个颜色的颜色组件的数目取决于颜色空间。对于离屏绘制,我们使用一个RGB颜色空间。因为Quartz使用alpha来绘制,每个离屏颜色都有四个组件–红、绿、蓝和alpha。所以,对于离屏绘制,我们提供的颜色组件数组的元素的数目必须是位置数目的4倍。Quartz的RGBA颜色组件可以在0.0到1.0之间改变。

代码清单8-1是创建一个CGGradient对象的代码片断。在声明了必须的变量后,代码设置了位置和颜色组件数组。然后创建了一个通用的RGB颜色空间。(在iOS中,不管RGB颜色空间是否可用,我们都应该调用CGColorSpaceCreateDeviceRGB)。然后,它传递必要的参数到CGGradientCreateWithColorComponents函数。我们同样可以使用CGGradientCreateWithColors,如果我们的程序设置了CGColor对象,这是一种便捷的方法。

Listing 8-1 Creating a CGGradient object

1
2
3
4
5
6
7
8
9
10
CGGradientRef myGradient;
CGColorSpaceRef myColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 0.5, 0.4, 1.0, // Start color
0.8, 0.8, 0.3, 1.0 }; // End color
myColorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
myGradient = CGGradientCreateWithColorComponents (myColorspace, components,
locations, num_locations);

在创建了CGGradient对象后,我们可以使用它来绘制一个轴向或线性渐变。代码清单8-2声明并设置了线性渐变的起始点然后绘制渐变。图8-1显示了结果。代码没有演示如何获取CGContext对象。

Listing 8-2 Painting an axial gradient using a CGGradient object

1
2
3
4
5
6
CGPoint myStartPoint, myEndPoint;
myStartPoint.x = 0.0;
myStartPoint.y = 0.0;
myEndPoint.x = 1.0;
myEndPoint.y = 1.0;
CGContextDrawLinearGradient (myContext, myGradient, myStartPoint, myEndPoint, 0);

代码清单8-3使用代码清单8-1中创建的CGGradient对象来绘制图8-9中径向渐变。这个例子同时也演示了使用纯色来填充渐变的扩展区域。

Listing 8-3 Painting a radial gradient using a CGGradient object

1
2
3
4
5
6
7
8
9
10
11
CGPoint myStartPoint, myEndPoint;
CGFloat myStartRadius, myEndRadius;
myStartPoint.x = 0.15;
myStartPoint.y = 0.15;
myEndPoint.x = 0.5;
myEndPoint.y = 0.5;
myStartRadius = 0.1;
myEndRadius = 0.25;
CGContextDrawRadialGradient (myContext, myGradient, myStartPoint,
myStartRadius, myEndPoint, myEndRadius,
kCGGradientDrawsAfterEndLocation);

Figure 8-9 A radial gradient painted using a CGGradient object

image

图8-4中的径向渐变使用代码清单8-4中的变量来创建。

Listing 8-4 The variables used to create a radial gradient by varying alpha

1
2
3
4
5
6
7
8
9
10
11
12
CGPoint myStartPoint, myEndPoint;
CGFloat myStartRadius, myEndRadius;
myStartPoint.x = 0.2;
myStartPoint.y = 0.5;
myEndPoint.x = 0.65;
myEndPoint.y = 0.5;
myStartRadius = 0.1;
myEndRadius = 0.25;
size_t num_locations = 2;
CGFloat locations[2] = { 0, 1.0 };
CGFloat components[8] = { 0.95, 0.3, 0.4, 1.0,
0.95, 0.3, 0.4, 0.1 };

代码清单8-5显示了用于创建图8-10中的灰色渐变的变量,其中有3个位置。

Listing 8-5 The variables used to create a gray gradient

1
2
3
4
5
size_t num_locations = 3;
CGFloat locations[3] = { 0.0, 0.5, 1.0};
CGFloat components[12] = { 1.0, 1.0, 1.0, 1.0,
0.5, 0.5, 0.5, 1.0,
1.0, 1.0, 1.0, 1.0 };

Figure 8-10 An axial gradient with three locations

image

使用CGShading对象

我们通过调用函数CGShadingCreateAxial或CGShadingCreateRadial创建一个CGShading对象来设置一个渐变,调用这些函数需要提供以下参数:

  1. CGColorSpace对象:颜色空间
  2. 起始点和终点。对于轴向渐变,有轴线的起始点和终点的坐标。对于径向渐变,有起始圆和终点圆中心的坐标。
  3. 用于定义渐变区域的圆的起始半径与终止半径。
  4. 一个CGFunction对象,可以通过CGFunctionCreate函数来获取。这个回调例程必须返回绘制到特定点的颜色值。
  5. 一个布尔值,用于指定是否使用纯色来绘制起始点与终点的扩展区域。

我们提供给CGShading创建函数的CGFunction对象包含一个回调结构体,及Quartz需要实现这个回调的所有信息。也许设置CGShasing对象的最棘手的部分是创建CGFunction对象。当我们调用CGFunctionCreate函数时,我们提供以下参数:

  1. 指向回调所需要的数据的指针
  2. 回调的输入值的个数。Quartz要求回调携带一个输入值。
  3. 一个浮点数的数组。Quartz只会提供数组中的一个元素给回调函数。一个转入值的范围是0(渐变的开始点的颜色)到1(渐变的结束点的颜色)。
  4. 回调提供的输出值的数目。对于每一个输入值,我们的回调必须为每个颜色组件提供一个值,以及一个alpha值来指定透明度。颜色组件值由Quartz提供的颜色空间来解释,并会提供给CGShading创建函数。例如,如果我们使用RGB颜色空间,则我们提供值4作为输出值(R,G,B,A)的数目。
  5. 一个浮点数的数组,用于指定每个颜色组件的值及alpha值。
  6. 一个回调数据结构,包含结构体的版本(设置为0)、生成颜色组件值的回调、一个可选的用于释放回调中info参数表示的数据。该回调类似于以下格式:
1
void myCalculateShadingValues (void *info, const CGFloat *in, CGFloat *out)

在创建CGShading对象后,如果需要我们可以设置额外的裁减操作。然后调用CGContextDrawShading函数来使用渐变来绘制上下文的裁减区域。当调用这个函数时,Quartz调用回调函数来获取从起点到终点这个范围内的颜色值。

当不再需要CGShading对象时,我们调用CGShadingRelease来释放它。

下面我们将一步步地通过代码来看看如何使用CGShading对象来绘制渐变。

使用CGShading对象绘制一个轴向渐变

绘制轴向和径向渐变的步骤是差不多的。这个例子演示了如何使用一个CGShading对象来绘制一个轴向渐变,并在图形上下文中绘制一个半圆形的裁减路径,然后将渐变绘制到裁减区域来达到图8-11的效果。

Figure 8-11 An axial gradient that is clipped and painted

image

为了绘制图中的轴向渐变,需要按以下步骤来处理:

  1. 设置CGFunction对象来计算颜色值
  2. 创建轴向渐变的CGShading对象
  3. 裁减上下文
  4. 使用CGShading对象来绘制轴向渐变
  5. 释放对象

设置CGFunction对象来计算颜色值

我们可以以我们想要的方式来计算颜色值,我们的颜色计算函数包含以下三个参数:

  1. void *info:这个值可以为NULL或者是一个指向传递给CGShading创建函数的数据。
  2. const CGFloat *in:Quartz传递in数组给回调。数组中的值必须在为CGFunction对象定义的输入值范围内。例如,输入范围是0到1;看代码清单8-7
  3. CGFloat *out:我们的回调函数传递out数组给Quartz。它包含用于颜色空间中每个颜色组件的元素及一个alpha值。输出值应该在CGFunction对象中定义的输出值的范围内,例如,输出范围是0到1;看代码清单8-7。

更多关于参数的信息可以查看CGFunctionEvaluateCallback。

代码清单8-6演示了一个函数,它通过将一个常数数组中的值乘以输入值来计算颜色组件值。因为输入值在0到1之间,所以输入值位于黑色(对于RGB来说值为0, 0, 0)和紫色(1, 0, 0.5)之间。注意最后一个组件通常设置为1,表示颜色总是完全不透明的。

Listing 8-6 Computing color component values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void myCalculateShadingValues (void *info,
const CGFloat *in,
CGFloat *out)
{
CGFloat v;
size_t k, components;
static const CGFloat c[] = {1, 0, .5, 0 };
components = (size_t)info;
v = *in;
for (k = 0; k < components -1; k++)
*out++ = c[k] * v;
*out++ = 1;
}

在写完回调计算颜色值后,我们将其打包以作为CGFunction对象的一部分。代码清单显示了一个函数,它创建了一个包含代码清单8-6中的回调函数的CGFunction对象。

Listing 8-7 Creating a CGFunction object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static CGFunctionRef myGetFunction (CGColorSpaceRef colorspace)
{
size_t numComponents;
static const CGFloat input_value_range [2] = { 0, 1 };
static const CGFloat output_value_ranges [8] = { 0, 1, 0, 1, 0, 1, 0, 1 };
static const CGFunctionCallbacks callbacks = { 0,
&myCalculateShadingValues,
NULL };
numComponents = 1 + CGColorSpaceGetNumberOfComponents (colorspace);
return CGFunctionCreate ((void *) numComponents,
1,
input_value_range,
numComponents,
output_value_ranges,
&callbacks);
}

创建一个轴向渐变的CGShading对象

为了创建一个CGShading对象,我们调用CGShadingCreateAxial函数,如代码清单8-8所示。我们传递一个颜色空间,开始点和结束点,一个CGFunction对象,和一个用于指定是否填充渐变的开始点和结束点扩展的布尔值。

Listing 8-8 Creating a CGShading object for an axial gradient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CGPoint startPoint,
endPoint;
CGFunctionRef myFunctionObject;
CGShadingRef myShading;
startPoint = CGPointMake(0,0.5);
endPoint = CGPointMake(1,0.5);
colorspace = CGColorSpaceCreateDeviceRGB();
myFunctionObject = myGetFunction (colorspace);
myShading = CGShadingCreateAxial (colorspace,
startPoint, endPoint,
myFunctionObject,
false, false);

裁减上下文

当绘制一个渐变时,Quartz填充当前上下文。绘制一个渐变与操作颜色和模式不同,后者是用于描边或填充一个路径对象。因此,如果要我们的渐变出现在一个特定形状中,我们需要裁减上下文。代码清单8-9的代码添加了一个半圆形到当前上下文,以便渐变绘制到这个裁减区域,如图8-11。

如果我们仔细看,会发现代码绘制的是一个半圆,而图中显示的是一个半椭圆形。为什么呢?我们会看到,当我们查看后面完整的绘制代码时,上下文被缩放了。稍后会详细说明。虽然我们不需要使用缩放或裁减,这些在Quartz 2D中的选项可以帮助我们达到有趣的效果。

Listing 8-9 Adding a semicircle clip to the graphics context

1
2
3
4
5
CGContextBeginPath (myContext);
CGContextAddArc (myContext, .5, .5, .3, 0,
my_convert_to_radians (180), 0);
CGContextClosePath (myContext);
CGContextClip (myContext);

使用CGShading对象来绘制轴向渐变

调用函数CGContextDrawShading使用CGShading对象为指定的颜色渐变来填充当前上下文:

1
CGContextDrawShading (myContext, myShading);

释放对象

当我们不再需要CGShading对象时,可以调用函数CGShadingRelease来释放它。我们需要同时释放CGColorSpace对象和CGFunction对象,如代码清单8-10所示:

Listing 8-10 Releasing objects

1
2
3
CGShadingRelease (myShading);
CGColorSpaceRelease (colorspace);
CGFunctionRelease (myFunctionObject);

使用CGShading对象绘制轴向渐变的完整例程

代码清单8-11显示了绘制一个轴向渐变的完整例程,使用8-7中的CGFunction对象和8-6中的回调函数。

Listing 8-11 Painting an axial gradient using a CGShading object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void myPaintAxialShading (CGContextRef myContext,
CGRect bounds)
{
CGPoint startPoint,
endPoint;
CGAffineTransform myTransform;
CGFloat width = bounds.size.width;
CGFloat height = bounds.size.height;
startPoint = CGPointMake(0,0.5);
endPoint = CGPointMake(1,0.5);
colorspace = CGColorSpaceCreateDeviceRGB();
myShadingFunction = myGetFunction(colorspace);
shading = CGShadingCreateAxial (colorspace,
startPoint, endPoint,
myShadingFunction,
false, false);
myTransform = CGAffineTransformMakeScale (width, height);
CGContextConcatCTM (myContext, myTransform);
CGContextSaveGState (myContext);
CGContextClipToRect (myContext, CGRectMake(0, 0, 1, 1));
CGContextSetRGBFillColor (myContext, 1, 1, 1, 1);
CGContextFillRect (myContext, CGRectMake(0, 0, 1, 1));
CGContextBeginPath (myContext);
CGContextAddArc (myContext, .5, .5, .3, 0,
my_convert_to_radians (180), 0);
CGContextClosePath (myContext);
CGContextClip (myContext);
CGContextDrawShading (myContext, shading);
CGColorSpaceRelease (colorspace);
CGShadingRelease (shading);
CGFunctionRelease (myShadingFunction);
CGContextRestoreGState (myContext);
}

使用CGShading对象绘制一个径向渐变

这个例子演示了如何使用CGShading对象来生成如图8-12所示的输出

Figure 8-12 A radial gradient created using a CGShading object

image

为了绘制一个径向渐变,我们需要按以下步骤来处理:

  1. 设置CGFunction对象来计算颜色值
  2. 创建径向渐变的CGShading对象
  3. 使用CGShading对象来绘制径向渐变
  4. 释放对象

设置CGFunction对象来计算颜色值

计算径向渐变和轴向渐变颜色值的函数没有什么区别。事实上,我们可以依照上面的轴向的”设置CGFunction对象来计算颜色值”。代码清单8-12用于计算颜色,使用颜色按正弦变化。图8-12与图8-11的结果非常不同。虽然颜色输出值不同,代码清单8-12的代码与8-6中的函数遵循相同的原型。每个函数获取一个输入值并计算N个值,即颜色空间的每个颜色组件加一个alpha值。

Listing 8-12 Computing color component values

1
2
3
4
5
6
7
8
9
10
11
static void myCalculateShadingValues (void *info,
const CGFloat *in,
CGFloat *out)
{
size_t k, components;
double frequency[4] = { 55, 220, 110, 0 };
components = (size_t)info;
for (k = 0; k < components - 1; k++)
*out++ = (1 + sin(*in * frequency[k]))/2;
*out++ = 1; // alpha
}

在写完颜色计算函数后调用它,我们需要创建一个CGFunction对象,如在轴向中”设置CGFunction对象来计算颜色值”所描述的一样。

创建径向渐变的CGShading对象

为了创建一个CGShading对象或者一个径向渐变,我们调用CGShadingCreateRadial函数,如代码清单8-13所求,传递一个颜色空间、开始点和结束点,开始半径和结束半径,一个CGFunction对象,和一个用于指定是否填充渐变的开始点和结束点扩展的布尔值。

Listing 8-13 Creating a CGShading object for a radial gradient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CGPoint startPoint, endPoint;
CGFloat startRadius, endRadius;
startPoint = CGPointMake(0.25,0.3);
startRadius = .1;
endPoint = CGPointMake(.7,0.7);
endRadius = .25;
colorspace = CGColorSpaceCreateDeviceRGB();
myShadingFunction = myGetFunction (colorspace);
CGShadingCreateRadial (colorspace,
startPoint,
startRadius,
endPoint,
endRadius,
myShadingFunction,
false,
false);

使用CGShading对象来绘制径向渐变

调用函数CGContextDrawShading使用CGShading对象为指定的颜色渐变来填充当前上下文:

1
CGContextDrawShading (myContext, myShading);

注意我们使用相同的函数来绘制渐变,而不管它是轴向还是径向。

释放对象

当我们不再需要CGShading对象时,可以调用函数CGShadingRelease来释放它。我们需要同时释放CGColorSpace对象和CGFunction对象,如代码清单8-14所示:

Listing 8-10 Releasing objects

1
2
3
CGShadingRelease (myShading);
CGColorSpaceRelease (colorspace);
CGFunctionRelease (myFunctionObject);

使用CGShading对象绘制径向渐变的完整例程

代码清单8-15显示了绘制一个轴径向渐变的完整例程,使用8-7中的CGFunction对象和8-12中的回调函数。

Listing 8-15 A routine that paints a radial gradient using a CGShading object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void myPaintRadialShading (CGContextRef myContext,
CGRect bounds);
{
CGPoint startPoint,
endPoint;
CGFloat startRadius,
endRadius;
CGAffineTransform myTransform;
CGFloat width = bounds.size.width;
CGFloat height = bounds.size.height;
startPoint = CGPointMake(0.25,0.3);
startRadius = .1;
endPoint = CGPointMake(.7,0.7);
endRadius = .25;
colorspace = CGColorSpaceCreateDeviceRGB();
myShadingFunction = myGetFunction (colorspace);
shading = CGShadingCreateRadial (colorspace,
startPoint, startRadius,
endPoint, endRadius,
myShadingFunction,
false, false);
myTransform = CGAffineTransformMakeScale (width, height);
CGContextConcatCTM (myContext, myTransform);
CGContextSaveGState (myContext);
CGContextClipToRect (myContext, CGRectMake(0, 0, 1, 1));
CGContextSetRGBFillColor (myContext, 1, 1, 1, 1);
CGContextFillRect (myContext, CGRectMake(0, 0, 1, 1));
CGContextDrawShading (myContext, shading);
CGColorSpaceRelease (colorspace);
CGShadingRelease (shading);
CGFunctionRelease (myShadingFunction);
CGContextRestoreGState (myContext);
}
1…345…9
南峰子

南峰子

86 日志
7 分类
© 2017 南峰子
由 Hexo 强力驱动
主题 - NexT.Pisces