« February 2013 | Main | April 2013 »

March 19, 2013

Objective-C 的对象模型

最近稍微学习了一点 Objective-C ,做笔记和做编码练习都是巩固学习的好方法。整理记录脑子里的新知识有助于理清思路,发现知识盲点以及错误的理解。

Objective-C 和 C++ 同样从兼容 C 语言开始,以给 C 语言增加面向对象为初衷,他们的出现的时间都很类似(1983 年左右)。但面向对象编程的源头却不同:C++ 受 Simula 和 Ada 的影响比较多,而 Objective-C 的相关思想源至 Smalltalk ,最终的结果是他们在对象模型上有不小的差异。

以我这些天粗浅的了解,Objective-C 似乎比 C++ 更强调类型的动态性,而牺牲了一些执行性能。不过这些牺牲,由于模型清晰,可以在今天,由更先进的编译技术来弥补了。

我对 C++ 的认知比 Objective-C 要多的多,所以对 C++ 开发中会遇到的问题的了解也多的多。在学习 Objective-C 的过程中,我发现很多地方都可以填上曾经在 C++ 开发中遇到的问题。当然,Objective-C 一定也有它自己的坑,只是我才刚开始,没有踩到过罢了。


ObjC 的类方法调用的形式,更接近于向对象发送消息。语法写作:

[obj message]

如果方法带有参数,那么就写作

[obj param:value]

方法和名称和参数的名称是一体的,参数决定了方法是什么。如果有多个参数,那么写作:

[obj param1:value1 param2:value2]

注意,如果一个类有两个方法,一个有一个参数,一个有两个参数。即使两个参数的版本中有一个参数名称和单个参数版本的相同,它们也是两个不同的方法。ObjC 不支持默认参数的语法。

C++ 调用对象的方法就更接近于 C 的函数调用。两相比较,可以发现 ObjC 的语法让代码可读性更强。你可以很容易的理解参数的用途,也不怕方法参数过多时,一串参数写漏或写错次序了。


和 C++ 一样,ObjC 的类声明和实现是分离的。但做的比 C++ 更彻底。ObjC 不能在声明的代码段中写 inline 函数。这看起来牺牲了一些运行性能,但当实现部分更好的分离。作为补充,ObjC 有 @property ,可以帮助程序员简化实现,也可以让编译器生成更好的代码。

声明一个类写成这样:

@interface class : baseclass {
   type a;
}

- (void) method;

- (void) messge: (type) param;

+ (id) create ;

@end

ObjC 利用了 C 语言中没有使用的符号 @ 来扩展 C 的语法,而不是用 C++ 里增加关键字的方式。这或许是一个对语言扩展更简单的做法,而不用考虑兼容性。C++ 就得精心挑选新增加的关键字,尽量回避那些已有代码中高频出现的单词。

类的数据段和方法是分离的。数据描述放在 {} 中,方法写在其后,在 @end 之前。

"-" 开头的方法是实例方法,也就是 C++ 中的成员方法。成员方法中可以通过 self 取到实例指针,也就是 C++ 中的 this 指针。

同样,ObjC 也支持类方法,也就是 C++ 中的 static 成员方法。通常是用来构造实例。声明方法是在方法名前写一个 + 号。

和 C++ 不同,ObjC 是有类对象的。类对象里有超类指针、类名、类方法列表指针,还有类对象的字节大小等元信息。而 C++ 中是用 RTTI 类实现不完全的类似功能的。

调用类方法和调用实例方法在语法上没有什么不同。类名就是类对象的名字。

ObjC 不支持多继承,没有私有、公开这些修饰符。

ObjC 的类方法实现必须写在同一个源文件里。不像 C++ 有 :: 操作符,ObjC 在实现方法时不写类的名字,而是把所有实现都写在 @implementation class ... @end 之间。访问基类,也可以方便的使用 super 关键字。

那么,如果一个类的方法太多,不适合写在同一个源文件中怎么办?


ObjC 提供了 category 这个概念。

可以通过 category 为一个类添加一些方法。category 和继承是不同的,不能为类添加新的成员变量,所以它不会改变类对象的内存布局。添加了方法的类还是原来那个类。

category 的语法是这样的:

@interface class (category) 

- newmethod;

@end

这样,就给 class 类添加了一个方法 newmethod ,并归类在 category 下。

和 C++ 不同,ObjC 的方法更具动态性。你可以在运行时任意调用一个对象的方法,而不用管它是否存在。ObjC 支持 id 这个类型。 id 其实就是对象指针,任何类型的对象都可以被 id 引用,并可以方便的向其发送消息(方法调用)。如果方法不存在,会抛出运行时错误。

向一个指定类型发送一个不存在的消息,会得到一个编译期警告,而不是编译错误。当然,我们不能随便忽略编译期警告,如果我们清楚的知道运行期这个对象可以处理这个消息,那么可以给类加一个 category 但不必实现它。这样,编译器就能了解新的方法了。

利用 category 可以方便的一个庞大的类拆分成独立的模块。在 C++ 中,比较接近的概念是 friend ,不过 friend 不易被优雅的使用。

既然方法可以被运行期检查,那么方法本身在 ObjC 中也可以被当成一种类型来处理。比较接近的 C++ 中的概念是 成员方法指针。回顾学习 C++ 的经历就能回忆起当年使用 ::* 或是 ->* 的头痛经历。ObjC 中的方法可以运行期绑定, @selector(method:) 的语法也简单的多。

在 NSObject 中就提供了一个叫 respondsToSelector: 的方法,接受一个 selector 用来检查自己是否可以接受这个消息。


ObjC 也提供了类似 Java 的 interface 或是 C++ 的纯虚类的东西,在 ObjC 中被称为 @protocol 。

@protocol 可以看成是一种没有数据成员的虚类。一个实际的类可以声明自己实现了某些协议,语法是

@interface class : base <protocol>
{
   // variables
}

// methods
@end

和继承不同,一个类可以声明多个协议。然后在 @implementation 中必须一一实现它们。

如上所述,ObjC 已经做到了运行期的方法绑定,所以 @protocol 只是做了更严格的编译检查。在新版的 ObjC 2.0 中,追加了 @optional 和 @required 用来描述那些方法的实现是可选的,哪些必须实现。


ObjC 的基础库比 C++ 更完整,标准化要好的多,也和语言结合的更紧密。

比如 NSString 是一个基础类,用于处理字符串。同时,语言也提供 @"string" 的语法方便的生成 NSString 对象。

ObjC 保留了 C 中的 printf 式的字符串操作形式,对比 C++ 重载移位操作符的形式,我想要更清爽一些。

对于 ObjC 对象,使用 %@ 来表示。给对象增加 description 方法就可以让处理函数知道该如何处理这个对象的 %@ 行为。

March 11, 2013

最近一些心得

最近特别忙, 每天写程序的时间都不够。有些东西在做完之前不想公开谈,所以只把一些笔记发在公司内部的周报里了。等这段时间过去,再贴到这里来。

不过还是有一些泛泛的心得可以写写的。

前几天遇到一个优化的问题。我想采用定期计算路图的方式优化寻路的算法。而不用每次每个单位在想查找目标的时候都去做一次运算并记录下路径结果。一切都看起来很顺利,算法的正确性很快就被验证了。可是最后实际跑的时候,发现在生成路图的地方会稍微卡一下影响流畅性。

遇到这类问题,第一反应通常是考虑如何优化。比如是不是应该换用多线程,是不是要分摊每次的计算量,是否可以简化问题减少运算的次数等等。

我在这些方面都尝试做了几个小时的努力。结果发现,如果我简单的把 O3 优化加上的话,可以直接加速 2 倍,相当于我其它方面做的努力的提升(当然其它方面的努力也是有效的)。

这不太符合我的平常遇到的情况。大部分情况下,C 语言编写的程序,编译器优化能做到 25% 的性能提升已经足够好了(通常 C++ 风格的代码可以提升更多)。或许是因为纯算法代码的因素导致这次的结果吧。

但是,采用多线程这样的方式来改进程序,不符合我的直觉。我不认为把程序结构改的复杂来获得需要的性能并不是第一选择。而且,在设计算法之初,我已经做过估算,认为在当前的硬件环境下,并太可能造成容忍不了的延迟的。

最后自己检查了自己的代码实现。发现我不小心把一个 O(n) 复杂度的算法实现成 O(n2) 了。修改正确后,速度一下就提高了 100 倍。

这次是个不多见的教训。程序结果的正确性被验证,但是依然存在 bug 。只是 bug 仅仅导致了代码运行缓慢,把一段需要 1/1000 秒应该完成的任务,减慢到了 1/10 秒的数量级。


另一点心得:

在外部条件不多变的问题上,适当的采用一些常量是很有效的。比如断定某个字符串长度不超过 64 ,数组的元素不超过 16 个等等。

对于 C 风格的代码,我们总能看到一些 MAXxxx 的宏来直接定义数组的大小。在 C++ 教科书上总是把他们当成反面教材,要求换用 std::vector 。

但实际上,如果能对目标问题做精确的评估,并在代码中做出合理的断言。固定长度的数组的好处也很明显。数据结构比较简单,程序会更加健壮。尤其是在多线程环境中更能体现出来。

即使是一些动态分配的部分,如果能估算出大致的总量,那么把内存池定义在结构中也更简洁一些。即,不额外用 malloc 分配一块新内存,再用指针去引用它。而是直接在结构中声明出内存块,让结构内的动态指针引用自身内存池中的内存。

这样,复杂的对象在内存中的数据布局依然是连续的。管理它们(生命期)的代码成本也比较低。对并发支持也更简单。不过,这同时也要求对需求的估算更明确,适合于用在严格定义好的内聚性很高的模块内部。


前段时间读过一本 Objective-C 的书。影响比较深刻的是 cocoa 框架对内存的管理方案。

虽然 cocoa 也使用引用计数。但是却不是在对象引用计数到 0 时就立刻释放内存。而是结合了 autorelease pool 。

这可以减少在当前栈帧访问无效对象的 bug ,而不需要引入 C++ 中的 RAII ,以比较清晰可显的法则定义出内存管理的方案。