这篇文章写起来挺艰难的。因为网上很少有人提到过这种对于android和 iOS的横向比较。因缘际会之下博主同时接触了这两种语言这两个平台。也把我的一些体会分享给大家。

语言的特性

1. 划分

  首先,OC是编译型语言,Java是混合型语言;OC是动态语言,Java是静态语言。它们又都是面向对象的静态类型语言。前者的区别造成了它们在代码形式风格上的诸多不同;后者又使它们在写法上诸多类似。 这里补充一点:动态类型语言和动态语言是两个概念。动态类型语言指的是运行期间才去做数据类型检查的语言,类似一些脚本语言,重点是数据类型。而动态语言指的是运行期间可以改变代码结构的语言,比如Object-C、C#、JavaScript、PHP;反之则如Java、C、C++。所以在这一点上,Java和OC显露出了分歧。

Java有虚拟机JVM的概念。

关于Java 运行时的逻辑图:

image

注:Java先生成字节码,再在Java虚拟机中解释执行。所以是混合型语言。

而编译型的OC则相对简单:
Objective-C → Compiler-gcc → .o文件 → linker装载器 → executable可执行文件
其中,编译是把源代码编译成机器码,链接是把各个模块的机器码和依赖库串连起来生成可执行文件。


2. 继承vs组合

  oc天然用组合来组织类与类之间的关系(比如category)
  java却常用面向对象,继承的方式来衍生出更多的类。

假如你想在全局的activity里做点什么,你会多重继承。拿百姓网主站app举例:
BXMainActivity→ BaixingBaseActivity→BaseActivity
这么一个继承关系。

  假如你想在所有的Fragment中定制UI特有的表现形式,比如全局的底部菜单栏。Java会定义一个BaseFragment来做这件事,以后其他多个Fragment都需要做的事情也会放在Base类里。每当新写一个fragment,都需要先继承base类。这样会导致继承关系复杂,并且最后把很多不同的事全扔到Base类里做。Base类里的代码变得不可看。

并且,一个新来的同学可能不知道它们的继承关系;一个基于敏捷开发的小demo想要合进来也变得狠困难。

  OC有一种天然的组合方式叫category。 如果你想实现上述的功能,只需对controller派生一个UIViewController+BottomMenu,在需要的页面去import它。通过这样的方式可以将不同功能打散到特定的category里。
UIViewController.m, UIViewController+BottomMenu.m 这么个关系。

  这里对仅了解Java的朋友说明一点:OC的category不是只能扩展自定义的类,而是可以改变cocoa底层的框架类,甚至是更底层的OC类(相当于android类和Java类)。 OC之所以能做到这一点,是因为它在编译阶段将include的头文件代码写入了进来,而C系列的语言是动态不定长的,既有类里的方法可以被改变;而Java为了安全、封闭等考虑,不让开发者写完一个类再去修改里面的内容。

Java想要做到动态改原生Activity,只能使用一种Hack的方式,即反射。

3. runtime VS reflection

想要要更深入地理解这种区别,不得不提到runtime和reflection。
iOS中的运行时编程,类似Java的反射。
Java是一门静态语言,类和方法都有严格的public, private之分。而反射机制却可以实现动态性,获取类的私有方法等。

 3.1 相同点

  都可以实现的功能:获取类信息、属性设置获取、类的动态加载、方法的动态调用等。

  ios中相关方法使用:

  类的动态加载:NSClassFromString(@“className”)

  方法的动态调用:NSSelectorFromString(@“doSomethingMethod”)

  ……

  Java反射API的第一个主要作用是获取程序在运行时刻的内部结构。

  通过getConstructor、getField和getMethod这三个方法可以获取到java.lang.Class类 的对象中的构造方法、域和方法。(注:java.lang.Class类是所有Java类的父类)

  具体参见:OC中的runtime与Java反射机制对比

 3.2 不同点

  OC能动态得给class添加类和方法,Java则不行。例如:

import<objc/runtime.h>
Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:")

平台的特性

ARC与GC的异同

  1. GC是什么? GC,garbage collection。 Java由GC来负责回收内存。

  2. ARC是什么? ARC是一种OC对象的自动管理机制,由LLVM编译器来保证(这件事情在预编译的过程中完成最好不过)。编译器在编译期间,动态地帮你添加retain/release/autorelease。同时,还引入了新的运行时特性:弱指针,性能优化等。

  3. difference
    Java 中的 GC 与 OC 中的 ARC 都有计数器的概念。 其实,ARC借鉴了现代语言中的引用计数。当时看的时候在想Java有非常完善的引用计数机制。但ARC又不是GC,它不扫描堆、不暂停进程运行,没有不确定的内存释放。所以其实你可以理解ARC是狠轻的对象管理,帮你添加了一些代码,是编译器提供的机制,而不是GC这种复杂的运行时机制。

  4. 我们可以怎样在android中利用GC机制? 首先,不同厂商的JVM对于GC的调用策略不同,但我们在应用层不需要关心这些。我们只需要知道,GC过程与对象的引用类型是严重相关的,我们来看看Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference。如下表:

级别 回收时机 用途 生存时间
从来不会 对象的一般状态 JVM停止运行时终止
内存不足时 对象的二级高速缓存 内存不足时终止
垃圾回收时 对象的一级高速缓存 gc运行后终止
垃圾回收时 联合ReferenceQueue跟踪垃圾回收器的活动 gc运行后终止

对于这一点,常体现在对于Bitmap的引用上。下面再展开阐述。这就引出了一个非常重要的话题:性能优化。

关于性能优化

android面临的内存问题要比ios的严峻很多。我们来看一下对比:

  iOS8/iPhone6/646MB
  Android 4.1/Mi2/96MB

WOW!
而随着android手机越做越好,努力地去满足消费市场;屏幕越做越大,内存等硬件参数也越来越好看。这个问题有没有好转呢?让我们来看下面这幅图:

image

  这幅图第三列中的 16→24→48 代表的是android框架对每个进程的内存限制。我们可以看到虽然进程的内存限制在逐年增大,然而渲染每一屏所占的图像大小也随着分辨率的增大而不断增大。即使渲染速度变快了,内存却依旧容易溢出。

尽管苹果的消费者对于摄像头、分辨率的呼声很高。苹果却并没有一味地去迎合消费市场。:)

言归正传,回到这样一个场景:假如我们有很多个经常需要用到的bitmap展示,最佳实践应该是怎样的?怎样利用好Java的GC原理?

如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

首先定义一个HashMap,保存软引用对象。

  1. private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

再来定义一个方法,保存Bitmap的软引用到HashMap。

image

[这个例子引用自另一篇文章]

使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉。从而避免内存达到上限,避免Crash的发生。

如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以使用弱引用。

总而言之,堆内存中的长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因。

关于android底下的内存泄露以及如何更好得利用GC的工作原理,让GC运行更加有效率还有太多可说的。这里就不展开了,留到 《下》 再来一一细述,免得iOS程序员看得不耐烦:)


ARC下的内存泄露

在我初接触苹果开发的时候,碰到一个很有趣的现象。大多数iOS程序员在苹果近年推出 ARC 机制以后,认为程序不再会内存泄露,可以高枕无忧了。殊不知Java早有类似的、比它更高级的机制,然而Java程序员却无时无刻不在关心android内存泄露的问题。这种现象一部分来自于人的特殊心理,另一部分来自于并未深刻理解ARC或者内存泄露,最后一部分得益于苹果系统并不像android那么容易发生内存泄露。

事实上,除了前文提到的关于 Java 内存泄露的诸多可能;OC不仅包含了这些,并且只可能更多。
举一个OC中特有的例子:

1. performSelector 系列

performSelector 顾名思义即在运行时执行一个 selector,最简单的方法如下:

- (id)performSelector:(SEL)selector;

这种调用 selector 的方法和直接调用 selector 基本等效,执行效果相同:

[object methodName];
[object performSelector:@selector(methodName)];
但 performSelector 相比直接调用更加灵活

SEL selector;
if (/* some condition */) {
    selector = @selector(newObject);
} else if (/* some other condition */) {
    selector = @selector(copy);
} else {
    selector = @selector(someProperty);
}
id ret = [object performSelector:selector];

这段代码就相当于在动态之上再动态绑定。在 ARC 下编译这段代码,编译器会发出警告

warning: performSelector may cause a leak because its selector is unknow [-Warc-performSelector-leak]

正是由于动态,编译器不知道即将调用的 selector 是什么,不了解方法签名和返回值,甚至是否有返回值都不知道,所以编译器无法用 ARC 的内存管理规则来判断返回值是否应该释放。因此,ARC 采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能持有该对象,从而可能导致内存泄露。

以本段代码为例,前两种情况(newObject, copy)都需要再次释放,而第三种情况不需要。这种泄露隐藏得如此之深,以至于使用 static analyzer 都很难检测到。如果把代码的最后一行改成:

[object performSelector:selector];

不创建一个返回值变量测试分析,简直难以想象这里居然会出现内存问题。所以如果你使用的 selector 有返回值,一定要处理掉。

以上只是一个小小的例子,类似的还不少。

所以,就 语言层面 来说,OC因为其动态的特性,比如一个selector(相当于method)究竟存不存在,在编译阶段我们不得而知;就 内存管理 来说,OC没有像Java一样时刻不停监控内存、回收内存的管理器;就 人员角度 来说,自ARC以后讨论OC内存泄露的技术文章少之又少。 不管如何看,OC都比Java更可能发生内存泄露。


下文预告:

关于性能优化,可讲的实在太多。我们在下一篇文章中再徐徐展开。

  1. 垃圾回收的四个触发时机
  2. Java编程建议
  3. 安卓和iOS不同的优化策略
  4. 工具
  5. 支持的图片格式
  6. 支持的语音格式