5 个回答
读过《Design and Evolution of C++》的人一定知道 C++ 这个变态的 zero-bookkeeping 原则。任何 C++ 语言的高层概念,其实都已经在编译阶段被剥离掉了。从最后的目标代码中你很难再看出这是一种高级语言。当然,其代价就是程序员必须理解为什么 C++ 不能实现某些功能。而且必须从机器的角度去理解。
动态语言其实就是一个不断添加 bookkeeping 的过程。
比如说,C++ 中为了实现多态还是不得不有一个中间机制,这就是虚表。但是你很难说虚表就是一个 bookkeeping 结构。因为它太简单了。而 Objective-C 就大大增加了对成员函数调用的 bookkeeping 机制。因为如此,所以 Objective-C 对 action-message 的实现就简单多了,因为你可以判断一个成员函数是否存在。而且也可以在不确定对象类型的情况下,指定一个方法作为回调函数。
C++ 的另一个问题,内存管理,根本原因在于其对象引用采用 raw pointer 机制。Objective-C 并没有改善这一点。但是也并非全无改善,在 Objective-C 里,一个 pointer 几乎永远必须指向 NSObject,而这个东西是有引用基数的。当然,它并没有完全解决 over-release 或者 use-after-release 的问题。到了 Java,Python,Lua 这样的语言,raw pointer 就完全消失了。
而 C++ 的内存管理,除了 heap 就只有一个借助 CPU stack 管理的栈。在动态语言里,就要考虑 lexical scope 的表现,这就需要更多的 bookkeeping。这点 Objective-C 也并没有实现。
语法的处理,在 C++ 中是完全在 runtime 之前进行。而在 Python,Lisp,Lua 这样的语言中是有 eval 这样的机制存在的。
所以,Objective-C 是比 C 和 C++ 拥有更多动态特性,而比 Lua,Lisp 缺乏一些动态特性的语言。至于题目中进行比较的 Python,也只能说是个更少缺乏动态特性的语言。Python 缺乏 lexical scope,也缺乏对 continuation 的支持。它的 stack 借助 CPU stack(当然有一个 stackless-Python,不过非官方),相比之下,Lua 为了支持 coroutine,Lisp 为了 full-continuation,都是自行维护 VM stack 的。Python 的 bookkeeping 与 Lua 和 Lisp 比起来也是不够的。
===============
什么是 bookkeeping 信息:Bookkeeping 就是 source code 里的信息用 declarive 的方式来保留在目标码中。
这里举一个所有语言都会舍弃的 bookkeeping 信息,就是 local variable name。Compiler 或者 interpreter 在遇到一个 local variable 的时候,一定要给它分配一个寄存器或者 stack entry(其实寄存器也就是 stack entry 的一种,见我的 blog 《什么是寄存器》)。所以在 runtime 时 variable name 就成了多余的。所以几乎所有语言,不管是 native 还是 byte code,都会舍弃 local name。
再比如说 raw pointer,就是说编译之后的代码里没有一个固定的 field 来说明一个 value 到底是不是 pointer。一个 value 是不是 pointer 完全靠引用它的代码本身来解释。当然你去分析目标码本身可能会得出「这是一个 pointer」的结论,但这是从 imperative code 中分析的来的,而不是从一个 field 中得到的明确的 declartive 信息。
-------------------------------------------
Objective-C 个人认为最为靠近实际使用的标志性特性就是它的消息机制了, 能到比较容易的做到很多静态语言很难做到的事情.
Objective-C简单的运行时知识:
1. 类和对象都是id, 在给你一个id的前提下无法直观的知道这个对象是类对象还是类本身. 简单的可以简化成runtime管理的都是id (id的本质其实是objc_object, objc_class头部其实就是id, 也就是isa).
2. Class在objc中是动态创建的, selector, method, imp, protocol等都是随后绑定上去的(即所谓的运行时绑定).
3. 通过runtime能够查出当前运行时环境中所有的类, 每个类中的方法, 每个类消息的绑定, 每个类的实现的协议, 每个协议的定义, 每个类当前的消息缓存等一切你想知道的东西.
4. 类的方法(消息)调用是间接的.
说一些其他楼貌似没提到的一些点:
1. 根据条件动态屏蔽一些协议方法:
如果协议定义某些可选方法(其实本质是消息)的调用顺序, 比如调用优先级为 S1 > S2 > S3, 实现respondsToSelector:可以来把优先级做调整. 首先我实现S1, S2, S3, 在满足条件T2时, 代理类可以决定不响应S1, 此时S2就具有最高优先级被直接调用, 满足条件T3时, 代理类可以决定不响应S1和S2, 此时S3就具有最高优先级被直接调用, 这样运行的时候根据条件不同, 程序的行为会发生直接改变, 虽然代理的调用者并不知道发生了什么事情. 比如UIPickerViewDelegate中可以直接返回NSString来表示指定区域的内容也可以直接返回一个自定义视图, 但是两个方法有优先级顺序.
2. 需要被封装的实现:
SDK中, 很多类是不直接提供实现的, 只是提供一个接口, 真实的功能实现在框架内部的未公开类中, 此时如果手写类的接口封装如果没有使用消息转发那就是噩梦, 用Objective-C就是实现一个转发消息就可以了(基本是几行代码搞定的事情).
3. 类别的实现:
类别就是runtime和消息组合后带来的特性体现. 在运行时中给指定类绑定自定义的消息以及对应的实现, 如果原来类中有的消息被新绑定覆盖, 体现就是类别的调用优先级比原类高.
4. KVO/KVC的自动willChange, didChange
想要得到KVO功能必须使用KVC来读写的原因是KVO的触发机制其实是willChangeValueForKey:, didChangeValueForKey:等对应的几组通知机制, 如果要求自动实现, runtime会给目标类创建一个包装子类, 重写isa, 重写指定的消息的get, set实现, 在头尾分别包装一层willChange和didChange, KVO/KVC机制就算自动接入了.
个人开发的一个Xcode插件, 里面用到了一些objc动态的特性, 改变了Xcode的代码编辑视图的渲染过程, 效果如图所示: 其中sourceCodeEditorBlurred直接替换了SourceTextView的绘制消息绑定, 一般可以认为是一种简单的黑科技...
https://en.wikipedia.org/wiki/Dynamic_programming_language
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html
但是它并不如Python那么灵活,例如不能在运行时动态创建类,不能替换函数和类,没有eval等。原因就是它骨子里还是C,类和对象是由结构体模拟的,只是将Objective-C的方法翻译成C的函数调用而已;但对于C中的固有限制,它的runtime就无能为力了。
真正意义上的动态语言,运行时包含全部的编译设施(前端+朴素解释器/VM/JIT后端),这些语言能提供eval支持。
近期比较热的基于VM的语言,运行时不需要前端,而是直接运行Bytecode,因此可以有一些简单的运行时自修改。
传统的静态语言,完全不需要运行时环境,当然,一些基础的动态特性还是能做的,比如RTTI、伪动态类型。
Objective-C具有相当多的动态特性,基本的,也是经常被提到和用到的有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)。
这些动态特性都是在Cocoa程序开发时非常常用的语言特性,而在这之后,OC在底层也提供了相当丰富的运行时的特性,比如枚举类属性方法、获取方法实现等等。虽然在平常的Cocoa开发中这些较底层的运行特性基本用不着,但是在某些情况下如果你知道这些特性并合理加以运用的话,往往能事半功倍~
动态特性基础
1、动态类型
即运行时再决定对象的类型。这类动态特性在日常应用中非常常见,简单说就是id类型。id类型即通用的对象类,任何对象都可以被id指针所指,而在实际使用中,往往使用introspection来确定该对象的实际所属类:
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
someClass *classSpecifiedInstance = (someClass *)obj;
// Do Something to classSpecifiedInstance which now is an instance of someClass
//...
}
-isMemberOfClass:
是 NSObject
的方法,用以确定某个 NSObject
对象是否是某个类的成员。与之相似的为-isKindOfClass:
,可以用以确定某个对象是否是某个类或其子类的成员。这两个方法为典型的introspection方法。在确定对象为某类成员后,可以安全地进行强制转换,继续之后的工作。
2、动态绑定
基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。在继续之前,需要明确Objective-C中消息的概念。由于OC的动态特性,在OC中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包到编译后的源码中了,而在OC中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应这条消息的方法。
动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。在Cocoa层,我们一般向一个NSObject对象发送-respondsToSelector:或者-instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类的+resolveClassMethod:和+resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。一个例子:
void dynamicMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
//该方法在OC消息转发生效前被调用
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
//向[self class]中新加入返回为void的实现,SEL名字为aSEL,实现的具体内容为dynamicMethodIMP class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, “v@:”);
return YES;
}
return [super resolveInstanceMethod:aSel];
}
当然也可以在任意需要的地方调用class_addMethod
或者method_setImplementation
(前者添加实现,后者替换实现),来完成动态绑定的需求。
3、动态加载
根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,基本就是根据不同的机型做适配。最经典的例子就是在Retina设备上加载@2x的图片,而在老一些的普通屏设备上加载原图。随着Retina iPad的推出,和之后可能的Retina Mac的出现,这个特性相信会被越来越多地使用。
深入运行时特性
基本的动态特性在常规的Cocoa开发中非常常用,特别是动态类型和动态绑定。由于Cocoa程序大量地使用Protocol-Delegate的设计模式,因此绝大部分的delegate指针类型必须是id,以满足运行时delegate的动态替换(在Java里这个设计模式被叫做Strategy?不是很懂Java,求纠正)。而Objective-C还有一些高级或者说更底层的运行时特性,在一般的Cocoa开发中较为少见,基本被运用与编写OC和其他语言的接口上。但是如果有所了解并使用得当的话,在Cocoa开发中往往可以轻易解决一些棘手问题。
这类运行时特性大多由/usr/lib/libobjc.A.dylib
这个动态库提供,里面包括了对于类、实例成员、成员方法和消息发送的很多API,包括获取类实例变量列表,替换类中的方法,为类成员添加变量,动态改变方法实现等,十分强大。完整的API列表和手册可以在这里找到。虽然文档开头表明是对于Mac OS X Objective-C 2.0适用,但是由于这些是OC的底层方法,因此对于iOS开发来说也是完全相同的。
一个简单的例子,比如在开发Universal应用或者游戏时,如果使用IB构建了大量的自定义的UI,那么在由iPhone版转向iPad版的过程中所面临的一个重要问题就是如何从不同的nib中加载界面。在iOS5之前,所有的UIViewController
在使用默认的界面加载时(init
或者initWithNibName:bundle:
),都会走-loadNibNamed:owner:options:
。而因为我们无法拿到-loadNibNamed:owner:options
的实现,因此对其重载是比较困难而且存在风险的。因此在做iPad版本的nib时,一个简单的办法是将所有的nib的命名方式统一,然后使用自己实现的新的类似-loadNibNamed:owner:options
的方法将原方法替换掉,同时保证非iPad的设备还走原来的loadNibNamed:owner:options
方法。使用OC运行时特性可以较简单地完成这一任务。
代码如下,在程序运行时调用+swizze
,交换自己实现的loadPadNibNamed:owner:options
和系统的loadNibNamed:owner:options
,之后所有的loadNibNamed:owner:options
消息都将会发为loadPadNibNamed:owner:options
,由自己的代码进行处理。
+(BOOL)swizze {
Method oldMethod = class_getInstanceMethod(self, @selector(loadNibNamed:owner:options:));
if (!oldMethod) {
return NO;
}
Method newMethod = class_getInstanceMethod(self, @selector(loadPadNibNamed:owner:options:));
if (!newMethod) {
return NO;
}
method_exchangeImplementations(oldMethod, newMethod);
return YES;
}
loadPadNibNamed:owner:options
的实现如下,注意在其中的loadPadNibNamed:owner:options
由于之前已经进行了交换,因此实际会发送为系统的loadNibNamed:owner:options
。以此完成了对不同资源的加载。
-(NSArray *)loadPadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary *)options {
NSString *newName = [name stringByReplacingOccurrencesOfString:@"@pad" withString:@""];
newName = [newName stringByAppendingFormat:@"@pad"];
//判断是否存在
NSFileManager *fm = [NSFileManager defaultManager];
NSString* filepath = [[NSBundle mainBundle] pathForResource:newName ofType:@”nib”];
//这里调用的loadPadNibNamed:owner:options:实际为为交换后的方法,即loadNibNamed:owner:options:
if ([fm fileExistsAtPath:filepath]) {
return [self loadPadNibNamed:newName owner:owner options:options];
} else {
return [self loadPadNibNamed:name owner:owner options:options];
}
}
当然这只是一个简单的例子,而且这个功能也可以通过别的方法来实现。比如添加UIViewController的类别来重载init,但是这样的重载会比较危险,因为你UIViewController的实现你无法完全知道,因此可能会由于重载导致某些本来应有的init代码没有覆盖,从而出现不可预测的错误。当然在上面这个例子中重载VC的init的话没有什么问题(因为对于VC,init的方法会被自动转发为loadNibNamed:owner:options,因此init方法并没有做其他更复杂的事情,因此只要初始化VC时使用的都是init的话就没有问题)。但是并不能保证这样的重载对于其他类也是可行的。因此对于实现未知的系统方法,使用上面的运行时交换方法会是一个不错的选择~