前言:大四毕业季,找工作一不小心就从 Android 开发工程师找成了 Mac 开发工程师,所以开发的主力语言也从 Java 转变为 Objective-C(下文统一用 OC 表示),现在已经学习 OC 两个来月了,决定抽出时间,写下这两门语言的对比与理解,这个话题一看就感觉很有趣,但因为 OC 学习时间不长,如有错误,请记得指出哈
历史
1980年,Brad Cox 发明了新的编程语言 Objective-C,最初只是作为 C 语言的简单扩展。
1995年,Sun 公司在 Sun world 会议上正式发布 Java 和 HotJava 浏览器。
从语言的历史来看,Java 语言的诞生相对 OC 晚了十多年。这也导致了 Java 语言吸取了很多前期面向对象语言优点,了解过 Java 的同学大多知道 Java 是从 C++ 上面改变过来的。同时,规避了前期面向对象语言的很多缺点。这也是造就了后面 Java 语言盛况空前的原因之一。OC 的热度完全是由基于 OC 编写的热门 iOS 应用带起来,但现在由于苹果公司推出新语言 Swift,热度有了明显的下降,除了旧项目可能因为替换成本比较大还需要使用 OC 进行开发。
Bjarne 大神曰过,编程语言就两种,一种没人用,一种被人骂。Java 火所以被骂的很多,但不能说 Java 就真的差。而 OC 以前没人用就没那么多人骂,但后来用的人多了,OC 也开始被骂的很多
学习难度
OC 和 Java 对比来看的话,我认为 OC 语言的难度要稍大一些,OC 语言自开发出来以后,基本上一直都是苹果公司在支持和完善。现代OC语言在传统面向对象语言的基础上增加很多特性,例如:Category、Extension、ARC、.语法来访问属性的 getter/setter 方法等等。但网上的资料参差不齐,比如网上不同时间对 @property 这一个属性资料会有所偏差,这些都增加了学习成本。但最好学习的资料当然是苹果的官方文档。
而对于 Java 语言相对而言,学习成本是最低的,网上的资料很多,而且每个版本语法差异基本可以忽略不计,不需要你懂得指针的概念,但懂得当然更好。
故从学习难度的对比来看,Java 语言的学习难度较低,OC 语言的学习难度较高,但这两门语言都有一个特点,就是特别啰嗦,但啰嗦也有明显的好处,对于新手理解起来会更容易点。
语法
先贴下代码对比下。
同样定义一个关于人的类,并实现一个吃的方法,语法如下:
Java
// Person.java
public class Person {
// 构造器
public Person {
super(); // 调用父类构造器
}
public void eat(String food) {
System.out.println("I eat the " + food);
}
}
// mian 方法调用的代码内容
Person p = new Person();
p.eat("apple"); // 此时输出: I eat the apple
OC
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat:(NSString *)food;
@end
//Person.m
@implementation Person
- (instancetype)init {
self = [super init]; // 调用父类构造器
if (self) {
// 自定义初始化属性
}
return self;
}
- (void)eat:(NSString *)food {
NSLog(@"I eat the %@", food); // 此时输出: I eat the apple
}
@end
// mian 方法调用的代码内容
Person *p = [[Person alloc] init];
[p eat:@"apple"];
类文件的组织
Java 为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间,这样有利于屏蔽同类名的冲突。关于包,由两个关键字确定,一个是 package
,定义包的位置,一个是 import
,导入一个包的内容。
而 OC 有 import
导入的概念,但没有 package
这个区分类这种概念,所以它另辟新径通过添加前缀来防止命名冲突,因此你会看到大量的以 NS 为前缀的类名、结构体、枚举等。而我们的类名应该以三个大写字母作为前缀(因为双字母前缀为 Apple 的类预留,避免以后苹果突然要使用刚刚好跟你有冲突的双字母,苹果爸爸就是霸道)。
为什么 OC 中大量类选择使用 NS 作为前缀呢,NeXTSTEP 是由 NeXT.Inc 所开发的操作系统。NeXT 是已故苹果电脑 CEO 乔布斯在 1985 年离开苹果电脑后所创立的公司。所以 "NS" 被作为 Fundation 中所有成员的前缀
创建对象
从上面看,Java 创建对象,主要是通过 new
操作,就会去调用构造方法(其实Person p = new Person();
这句语句并不是个原子操作,实际上在汇编层次分为三个步骤:分配实例分配内存,执行构造函数内容,把该内存分配给变量)。
而 OC 中分为两步:申请分配内存和初始化,即 alloc
和 init
。详情如下:
-
alloc
: 负责创建对象,这个过程包括分配足够的内存来保存对象,写入 isa 指针,初始化引用计数,以及重置所有实例变量。将返回一个有效的未初始化的对象实例。 -
init
: 负责初始化对象,这意味着使对象处于可用状态。这通常意味着为对象的实例变量赋予合理有用的值。
从上面观看 OC 代码可以了解到关于约定俗成的重要部分:
init
这个方法可以(并且应该)通过返回 nil 来告诉调用者,初始化失败了;因为初始化可能会各种原因失败,比如一个输入的格式错误了,或者另一个需要的对象初始化失败了,又或者给空对象发消息不会 Crash 但就初始化失败了。这样我们就能理解为什么总是需要调用self = [super init]
。如果你的父类说初始化自己的时候失败了,那么你必须假定你正处于一个不稳定的状态,因此在你的实现里不要继续你自己的初始化并且也返回 nil。如果不这样做,你可能会操作一个不可用的对象,它的行为是不可预测的,最终可能会导致你的程序崩溃。
方法调用的设计
Java 语言使用点语法调用方法,大多语言也是使用这样的语法,很保守也很实在,更容易被人接受,也更容易理解。
OC 的方法调用在编程语言上属于非常特别的存在,颇为前卫,使用中括号的方式实现方法的调用,这也导致了后来一批程序员不愿接触这门语言,原因仅仅是因为语法太过奇葩。同时,OC 通常不把这个叫做方法调用,而称之为给对象发送消息。为什么叫消息发送,在下一点会着重介绍。
空对象调用方法
Java 中调用 null 对象的方法,会报臭名昭著的 NulllPointerException 空指针异常,但对于老手来说这种问题其实是最容易解决的问题。
而 OC 中向 nil 发消息,程序是不会崩溃的。同时视方法返回值,向 nil 发消息可能会返回 nil(返回值为对象)、0(返回值为一些基础数据类型)或 0X0(返回值为id)等。
为什么会这样呢?因为 OC 的函数调用底层实现都会转换为一个 C 语言实现的 objc_msgSend 函数进行消息发送来实现的,其中实现细节会通过判断 self 是否有值来决定是否发送消息,如果 self 为 nil,那么 selector 也会为空,直接返回,所以不会出现问题。当然,假如对一个野指针发送消息,那么这个时候肯定是会 crash 的,安全的做法是释放后将对象重新置为 nil。这种事在 MRC 的情况下可能发生。
接口特性
Java 和 OC 都有接口的概念,但 Java 的接口关键字是 interface
,接口里面所有的方法都必须实现。
而 OC 中定义类的 @interface
和 Java 中的 interface
不是一回事,@protocol
(OC里面称之为协议)和 Java 中的 interface
才是一回事,OC 协议里面的方法不一定要全部实现,可以通过 @required
和 @optional
去选择设置。
可能从接口的设计对比来看,OC 有可选和必选可能会稍微好一些,但假如真的出现可选,可能也是抽象这个方面做得不够好,其实往这个方向想,其实没毛病,毕竟 OC 并不是完全的面向对象的语言。
认真观察,protocol
其实并没有大量地在 OC 的代码中使用也没有在社区中普及(指的是那种像 Java 程序员使用 interface
那样来使用 protocol
的方式)。一个主要原因是大多数的 Apple 开发的代码没有采用这种的方式,而几乎所有的开发者都是遵从 Apple 的模式以及指南。Apple 几乎只是在委托模式下使用 protocol。
.h
文件设计技巧Tips
- 对于使用协议,像 delegate 相关 protocol 的书写不应该在头文件
@interface
中声明,而应该在类扩展中声明;需要公开由外部调用的协议,如<NSCopying>
则写在.h
文件是正确的。- 内部属性,实例变量和
IBoutlet
不应该在.h
文件定义,这样相当于是把内部实现暴露出去了- 调用者对
IBAction
同样不需要 care,也不应该在.h
文件定义
setter/getter 方法
Java 生成一个实体对象,一般都会把属性设置为私有,对外按需书写提供 setter/getter 方法操作对应属性,IDE 会提供快捷键快速生成这些方法。
而现代 OC 可以直接使用 @property
的变量可以自动生成 setter/getter 方法(早期 OC 不行,后来可以了),同时可以使用通过点语法去调用属性,但点语法的本质是调用类的 getter 方法和 setter 方法,如果类中没有 getter 方法和 setter 方法就不能使用点语法,假如强行使用将编译报错。这极大地简化了代码编写的工作量,减少了很多重复的工作量。
方法访问权限
Java 中方法内容都写在了 .java
文件中,通过 public
、protect
、不带任何修饰符(默认级别)和 private
去控制外部类是否有权限去访问相应的方法。
而 OC 在写 .h
文件中编写的是公开的属性和方法,而在 .m
文件中都会有具体的实现,可以在此文件书写 .h
文件及其继承下来和实现协议的方法中未出现的属性和方法,而这些属性和方法是都属于私有的,这样有利于隐藏细节实现同时不需要改变头文件。
我们应该善于面向接口编程,划清边界,将类的实现隐藏在调用者所见之外,使主调和被调者之间保持最少知识原则。
静态语言 VS 动态语言
首先普及一个知识点,什么是动态语言? 动态语言就是在运行时来执行静态语言的编译链接的工作。这就要求除了编译器之外还要有一种运行时系统来执行编译等功能。
Java 的方法是与 class 静态绑定的,JVM
(Java 虚拟机) 采取了类似 C++ 的虚表机制,在编译的时候会生成 .Class
文件,Class 文件格式采用类似 C 语言结构体的伪结构来存储数据,字段表集合和方法表集合是写死的在文件内的。JVM 会在链接类的过程中,给类分配相应的字段表和方法表固定的内存空间。每个类对应一个方法表,这些都是存在于方法区中的。对于一个 Java 程序员可能会觉得反射很强大(因为我曾经就是),但研究下反射的原理,反射是可以运行动态的调用某个对象的方法/构造函数、获取某个对象的属性等,但无法动态添加字段 or 方法,因为反射的实现是对 JVM
内存的方法区内容进行操作处理,字段表和方法表在类加载后内存区域已经被固定了,无法动态添加相关内容进行扩展,同时通过原理可以了解到反射是对 JVM
进行操作,所以这也是为什么我们所说反射效率并不高的原因,故我们一般只在极端的情况下才会使用。
而由于 OC 使用消息传递,底层大多还是 C 的内容,一个类/对象的数据结构其实就相当是一个结构体,其中具有属性表集合struct objc_ivar_list *ivars
和方法表集合struct objc_method_list **methodLists
,所以我们可以使用 runtime(一套比较底层的纯 C 语言 API,可以在运行时来执行静态语言的编译链接的工作)让类、对象的属性和方法可以在运行时确定和修改,动态添加新的属性和方法,因为添加新的内容不过相当于给集合添加新元素。也就是说,OC 会更加动态一点。
最后再讲讲垃圾回收那点事
OC 垃圾回收使用的是引用计数器算法来搜索和处理不需要的对象内存,但这种垃圾回收是在编译期进行处理,使用变量时相当于多了一点点的计数器指令操作,故性能相比 C 语言仅仅略差一点点。然后 OC 以前是使用 MRC 手动管理内存(Manual Reference Counting),而后来使用 ARC 自动管理内存(Automatic Reference Counting)。这是个十足的进步,因为 OC 开发者不再需要过多的关注内存问题(虽然内存还是必须关注的),而将更多的时间放在我们真正关心的事情上。
引用计数器算法
给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为 0 的时候,就认为对象不再被使用,是“垃圾”了,ARC 下该对象自动设置为 nil,而 MRC 需要手动设置为 nil。实现简单,判断效率高。
而 Java 垃圾回收是 JVM
去处理的,但是垃圾回收是运行时进行处理,一开始版本也是使用引用计数器算法,但此算法很难解决循环引用问题(A 对象引用 B 对象,B 对象又引用 A 对象,但是A, B对象已不被其他对象引用从而这两个对象都不会被回收导致内存泄漏),同时每次计数器的增加和减少都带来了很多额外的开销,所以在 JDK1.1 已废弃,改用GC 搜索算法
根搜索算法
通过一系列的名为“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索所有走过的路径称为引用链,当一个对象没有被 GC Roots 的引用链连接的时候,说明这个对象是不可达的,证明该对象是可以被回收的
那问题来了,为什么 OC 就能解决循环引用的问题呢?
OC 引入两个概念,strong 指针和 weak 指针:默认所有实例变量和局部变量都是 strong 指针,每被 strong 指针指向的对象计数器就自动+1,失去一个 strong 指针 指向则计数器-1,而弱指针指向的对象计数器不变化。两端互相引用时,一端用 strong、一端用 weak,此处使用 Apple 官方的图进行说明。
此时其他对象撤销对下面两个 Object 对象之间的强引用,就再也没有对象对 Delegate Object 具有强引用了。
因此 Delegate Object,即被 weak 的一端,计数器为 0 了,故被回收了。
一但 weak 的对象也被回收,就再也没有对象对 NSTableView 对象进行引用,此时 NSTableView 对象计数器也为 0 了,也要被回收了。这就是 OC 鸡贼,不不不,是机智处理循环引用的方法。
这里普及一点,Java 也是有强引用和弱引用这个概念的,具体如下:
- 强引用:正常使用变量那样。即使内存不足的时候也永远不会被垃圾回收。
- 软引用:通过 SoftReference 类实现,当内存不足时会回收软引用的对象。
- 弱引用:通过 WeakReference 类实现,不管当前内存空间足够与否,只要 GC 后发现了就会回收它的内存。
- 虚引用:跟踪对象被垃圾回收的状态,主要是实现细粒度的内存控制,使得该对象被立即回收,同时可以较快的通知该对象执行 finalize 方法,故可以在这个方法上做一些操作。
总结
其实全面学习第二门主语言,相对难度会较大,而且跳出舒适圈的勇气需要非常大(所以当时我剪了个光头重头开始hhh),因为需要不断去适应一个新的语法糖,当然也可能是 OC 的语法糖并不可爱!!!但学习新的语言的好处是非常好的,因为你会不断你会不断比较和了吐槽,了解到哪些内容是这个语言的瓶颈,就比如 Java 效率较低和语言不动态、OC 不支持跨平台和语法恶心等等。这些经历都有利于以后对其他语言的学习,而且越学越快。
现在天天是接触 Mac 开发,但相对于封闭的开发环境,其实还是比较喜欢开源的感觉,因为苹果爸爸太霸道了,只能长期在一个需要猜测底层实现的平台上开发。但也还好,可能因为以前是 Android 开发者,有研究过一点点源码的内容,对于一个操作系统,其实有些内容和机制大概也就那样而已,而且 IOS 可能还更简单,爸爸终究还是爸爸。而且开源社区中也有许多优秀的开源程序可供学习,不一定要死抠系统应用,保持一颗学习的心就会不断进步的。