《编写高质量代码:改善Objective-C程序的61个建议》——建议1:视Objective-C为一门动态语言-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

《编写高质量代码:改善Objective-C程序的61个建议》——建议1:视Objective-C为一门动态语言

简介:

本节书摘来自华章出版社《编写高质量代码:改善Objective-C程序的61个建议》一 书中的第1章,第1.1节,作者:刘一道,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

建议1:视Objective-C为一门动态语言

Objective-C,是美国人布莱德·确斯(Brad Cox)于1980年年初发明的一种程序设计语言,其与同时代的C++一样,都是在C的基础上加入面向对象特性扩充而成的。C++如日中天,红火已有30年之久,而Objective-C到2010年才被人注意,并逐渐红火起来。造成这种结果的原因跟其版权为苹果独自拥有和苹果自我封闭性、不作为性有很大的关系。这也是Objective-C在语法上跟其他语言(如C++、Pascal、Java和C#)等相比有些不足的原因。可喜的是,苹果公司于2010年起,在每年的苹果年度发布大会上都会针对Objective-C语言的语法发布新的改良版本。
虽然Objective-C和 C++都是在C的基础上加入面向对象特性扩充而成的程序设计语言,但二者实现的机制差异很大。Objective-C基于动态运行时类型,而C++基于静态类型。也就是说,用Objective-C编写的程序不能直接编译成可令机器读懂的机器语言(二进制编码),而是在程序运行时,通过运行时(Runtime)把程序转译成可令机器读懂的机器语言;用C++编写的程序,编译时就直接编译成了可令机器读懂的机器语言。这也就是为什么把Objective-C视为一门动态开发语言的原因。
从原理上来讲,目前常用的Java和C#等程序开发语言都是动态开发语言,只不过Java用的是虚拟机JVM(Java Virtual Machine),如图1-2所示,C#用的是公共语言运行时CLR(Common Language Runtime)。与此相对,C++可视为静态开发语言。

f0ad0ce9fc363964f0de64e48570fb085815d501

Objective-C作为一门动态开发语言,会尽可能地将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C 总是使用动态的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码。
而运行时(Runtime)正是扮演着这样的角色。在一定程度上,运行时系统类似于 Objective-C 语言的操作系统,Objective-C 就基于该系统来工作。因此,运行时系统好比Objective-C的灵魂,很多东西都是在这个基础上出现的。很多开发人员常常对运行时系统充满疑惑,而恰恰这是一个非常重要的概念。可以这么问:“如果让你实现(设计)一个计算机语言,你会如何下手?” 很少程序员这么思考过。但是这么一问,就会强迫你从更高层次来思考以前的问题了。
要想理解“Objective-C是一门动态开发语言”,就不得不理解开发语言的三个不同层次。
(1)传统的面向过程的语言开发。例如C语言,实现C语言编译器很简单,只要按照语法规则实现一个LALR语法分析器就可以了。编译器优化是非常难的,不在本节讨论的范围内。这里实现了编译器中最基础和原始的目标之一,就是把一份代码里的函数名称转化成一个相对内存地址,把调用这个函数的语句转换成一个跳转指令。在程序开始运行时,调用语句可以正确跳转到对应的函数地址。这样很好,也很直白,但是太死板了。
(2)改进的开发面向对象的语言。相对面向过程的语言来说,面向对象的语言更加灵活了。例如C++,C++在C的基础上增加了类的部分。但这到底意味着什么呢?再写它的编译器要如何考虑呢?其实,就是让编译器多绕个弯,在严格的C编译器上增加一层类处理的机制,把一个函数限制在它处在的类环境里,每次请求一个函数调用,先找到它的对象,其类型、返回值、参数等确定后再跳转到需要的函数。这样很多程序增加了灵活性,同样一个函数调用会根据请求参数和类的环境返回完全不同的结果。增加类机制后,就模拟了现实世界的抽象模式,不同的对象有不同的属性和方法。同样的方法,不同的类有不同的行为!这里大家就可以看到作为一个编译器开发者都做了哪些进一步的思考。虽然面相对象的语言有所改进,但还是死板,故此仍称C++是静态语言。
(3)动态开发语言。希望更加灵活,于是完全把上面那个类的实现部分抽象出来,做成一套完整运行阶段的检测环境,形成动态开发语言。这次再写编译器甚至保留部分代码里的sytax名称,名称错误检测,运行时系统环境注册所有全局的类、函数、变量等信息,可以无限地为这个层增加必要的功能。调用函数时,会先从这个运行时系统环境里检测所有可能的参数再做jmp跳转。这就是运行时系统。编译器开发起来比上面更加绕弯,但是这个层极大地增加了程序的灵活性。例如,当调用一个函数时,前两种语言很有可能一个跳到了一个非法地址导致程序崩溃,但是在这个层次里面,运行时系统过滤掉了这些可能性。 这就是动态开发语言更加强壮的原因,因为编译器和运行时系统环境开发人员已经帮你处理了这些问题。
好了,上面说了这么多,再返回来看Objective-C的这些语句:

id obj=self;
if ([obj respondsToSelector:@selector(function1:)) {
}
if ([obj isKindOfClass:[NSArray class]] ) {
}
if ([obj conformsToProtocol:@protocol(myProtocol)]) {
}
if ([[obj class] isSubclassOfClass:[NSArray class]]) {
}
[obj someNonExistFunction];

看似很简单的语句,但是为了让语言实现这个能力,语言开发者要付出很多努力来实现运行时系统环境。这里运行时系统环境处理了弱类型及函数存在检查工作。运行时系统会检测注册列表里是否存在对应的函数,类型是否正确,最后确定正确的函数地址,再进行保存寄存器状态、压栈、函数调用等实际的操作。

id knife=[Knife grateKnife];
NSArray *monsterList=[NSArray array];
[monsterList makeObjectsPerformSelector:@selector(killMonster:)
withObject:knife];

用C和C++完成这个功能还是比较麻烦的,但是动态语言处理却非常简单,并且这些语句让Objective-C语言更加直观。
在Objective-C中,针对对象的函数调用将不再是普通的函数调用:

[obj function1With:var1];

这样的函数调用将被运行时系统环境转换成:

objc_msgSend(target,@selector(function1With:),var1);

Objective-C的运行时系统环境是开源的,这里可以进行简单介绍,可以看到objc_msgSend由汇编语言实现,甚至不必阅读代码,只需查看注释就可以了解。运行时系统环境在函数调用前做了比较全面的安全检查,已确保动态语言函数调用不会导致程序崩溃。对于希望深入学习的朋友可以自行下载Objective-C的运行时系统源代码来阅读,进行更深入的了解。

/********************************************************************
* id       objc_msgSend(id self,
*          SEL op,
*          ...)
*
* a1是消息接收者,a2是选择器
********************************************************************/
     ENTRY objc_msgSend
#检查接收者是否空
    teq     a1, #0
    moveq   a2, #0
    bxeq    lr
 
# 保存寄存器(对象),通过在缓存里查找,来加载接收者(对象)类
    stmfd   sp!, {a4,v1-v3}
    ldr     v1, [a1, #ISA]
 
#寄存器(对象)非空,缓存里可查找到
    CacheLookup a2, LMsgSendCacheMiss
 
#缓存碰撞(imp in ip) —准备转发,恢复寄存器和调用
    teq v1, v1      /* 设置 nonstret (eq) */
    ldmfd   sp!, {a4,v1-v3}
    bx      ip
 
#缓存漏掉:查找方法列表
LMsgSendCacheMiss:
    ldmfd   sp!, {a4,v1-v3}
    b   _objc_msgSend_uncached
 
LMsgSendExit:
    END_ENTRY objc_msgSend
 
 
    .text
    .align 2
_objc_msgSend_uncached:
 
#压入堆栈帧
    stmfd   sp!, {a1-a4,r7,lr}
    add     r7, sp, #16
    SAVE_VFP
 
# 加载类和选择器
    ldr a1, [a1, #ISA]      /* class = receiver->isa  */
    # MOVE  a2, a2          /*选择器已经在a2 */
 
#做查找
    MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache)
    MOVE    ip, a1
 
# 准备转发,挤出堆栈帧并且调用imp
    teq v1, v1      /* 设置nonstret (eq) */
    RESTORE_VFP
    ldmfd   sp!, {a1-a4,r7,lr}
    bx  ip

现在说一下动态开发语言的负面影响,其负面影响可以归纳为两方面。

  1. 执行效率问题
    “静态开发语言执行效率要比动态开发语言高”,这句没错。因为一部分CPU计算损耗在了运行时系统过程中,而从上面的汇编代码也可以看出大概损耗在哪些地方。而静态语言生成的机器指令更简洁。正因为知道这个原因,所以开发语言的人付出很大一部分努力来保持运行时系统的小巧。所以,Objective-C是C的超集加上一个小巧的运行时系统环境。但是,换句话说,从算法角度考虑,这点复杂度是可忽略的。
  2. 安全性问题
    动态语言由于运行时系统环境的需求,会保留一些源码级别的程序结构。这样就给破解带来了方便之门。一个现成的说明就是Java,大家都知道Java运行在jre上面,这就是典型的运行时例子。它的执行文件.class全部可以反编译回近似源代码。所以,这里的额外提示就是,如果需要写和安全有关的代码,离Objective-C远点,直接用C。

简单理解:“Runtime is everything between your each function call.”
但是大家要明白,提到运行时系统并不只是因为它带来了这些简便的语言特性,而是这些简单的语言特性在实际运用中,需要从完全不同的角度考虑。

ade9239bf11596023129880acc17ef027bd292c2

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接