为初学者编写 Mac OS X Cocoa 程序/一些 Cocoa 核心原则
上一页:Objective C,语言及其优势 | 下一页:构建 GUI
我们已经看到了 Cocoa 的基本操作。在上一章中,我们讨论了从 NSObject 继承,以及如何确保我们调用对象的初始化方法链。在这里,我们将更详细地讨论这一点,因为它非常重要。然后,我们将讨论内存管理。
正确初始化对象是你的责任。与 C++ 不同,没有自动的构造函数方法,也没有自动的析构函数方法。但是,只要我们了解一些简单的规则,我们可以很容易地做到这一点。遵循这些规则对于在 Cocoa 中取得成功至关重要。
当我们实例化一个对象时
id myObject = [NSString alloc];
我们得到的只是一块未初始化的内存。如果我们现在使用这个对象,我们很可能不会得到我们想要的结果。它甚至可能会崩溃。我们必须调用初始化方法
id myObject = [[NSString alloc] init];
然后我们就可以了。你会经常看到这种 alloc/init 组合。那么为什么 alloc 不仅仅在其内部调用 init 并完成它呢?这难道不会为我们节省时间并减少忘记的可能性吗?是的,除了一个问题。有时我们需要一个不同的 init 版本,它需要一个参数。由于 alloc 不知道我们想要哪个,它将调用权留给了我们。
每个对象都有一个 init 版本,它充当“主” init - 当我们创建该对象时必须调用的 init。事物被安排好,以便任何其他 init 变体都会调用它,从而使一切正常运作。主 init 方法被称为指定初始化程序。在大多数情况下,这将是普通的“init”方法,因此使用任何其他变体将在内部调用 init。如果情况并非如此,文档应指示指定初始化程序。
“init”只是一个普通的 方法调用,它没有像 C++ 构造函数那样的特殊状态。因此,你有责任正确编写 init 中的代码,确保你首先调用你的父类的指定初始化程序(或任何最终通过指定初始化程序调用的其初始化程序方法)。完成此操作后,你应该初始化你自己的对象部分。
初始化允许失败。例如,你的对象可能想要分配一大块内存。如果内存不可用,你应该清理已经分配的任何内容,然后从你的 init 方法返回 nil。由于 nil 对象可以安全调用,这意味着内存不足(例如)不会导致程序崩溃。
当一个对象不再需要时,它的“dealloc”方法将被调用。如果你在初始化或对象的操作过程中分配了任何内容,你需要覆盖此方法并释放它。完成后,你应该调用父类的 dealloc 方法。你将经常覆盖 dealloc,所以要熟悉如何做到这一点。但是,dealloc 很少被直接调用。相反,它在对象被释放时被间接调用。
每个继承自 NSObject 的对象都实现了 retain 和 release。这是一种简单的内存管理技术,依赖于引用计数。对象跟踪程序中对其的引用数量。当没有更多引用时,计数为零,它将被释放。在 Cocoa 中,引用计数通常被称为保留计数。它是一样的东西。
retain 方法增加计数,release 方法减少计数。当计数达到零时,dealloc 会自动被调用。
当一个对象第一次被创建时,它的保留计数被设置为 1。因此
id myObject = [[NSString alloc] init];
“myObject”的保留计数为 1,因为只有一个对它的引用。
如果我们然后执行
[myObject release];
它将立即被释放,因为计数从 1 减少到 0,并且 dealloc 被调用。通过调用 release,我们告诉 Cocoa 我们不再对这里这个对象感兴趣,也不会再引用它。
有些人觉得 retain 和 release 很令人困惑,但规则实际上很简单。如果你创建了一个对象,你负责释放它。如果你创建了对现有对象的第二个引用,你负责释放它。你使用 alloc 或 copy 在另一个对象上创建了一个对象,因此在你使用这些时,你需要了解 retain/release 责任。这是你的责任。如果你忘记释放,事情可能会顺利进行,但你会有一个内存泄漏,这可能会在未来造成问题。
让我们看一下第二部分,创建第二个引用。假设你有一个对象,它是另一个对象的一部分。我们在前面的示例中看到了这一点,其中我们的 GCHelloView 具有 NSString 作为数据成员。当我们从外部传入一个 NSString 时,我们不知道是谁创建了它,谁对它负责 - 我们只知道它不是我们!但是,通过将其存储为数据成员,我们正在创建对该同一字符串的另一个引用,因此我们必须保留它。我们之前存在的任何字符串呢?嗯,我们创建了对它的引用,因此我们必须释放它。
- (void) setText:(NSString*) text withSize:(int) size { [text retain]; [_text release]; _text = text; _size = size; [self setNeedsDisplay:YES]; }
所以现在我们可以看到这里发生了什么。我们保留“text”,因为我们正在创建对它的另一个引用,我们的 _text 数据成员,并且我们正在释放上一个值,因为我们不再关心它。我们不在乎字符串最初来自哪里,也不知道是谁创建的,我们已经完成了它,所以我们释放它。如果其他人仍然保留它,它将继续存在,但如果没有,它将被释放。现在,如果“text”和“_text”实际上是同一个字符串呢?这很容易发生。如果我们首先释放旧的,它可能会被释放。接下来的 retain 将作用于一个过时的引用,这很可能会导致崩溃(注意,与访问 nil 不同,访问已释放的对象是不安全的)。因此,通过在释放之前保留,我们避免了这种潜在的障碍。“保留在释放之前”是一种常见且合理的做法。
请注意,对象的保留非常有效,因为只需要增加一个简单的计数。如果你需要对每个对象进行复制,这不仅会慢得多,而且你还可能导致不同的副本彼此不同步,并使用比必要更多的内存。
当我们创建一个对象,但我们想要将其传递给其他人时会发生什么?我们如何确保它在被另一个对象保留之前保持有效,然后我们再释放它?看看这个
id myObject = [[NSString alloc] init]; return myObject;
接收者知道这个对象需要被释放吗?如果没有人释放这个对象,那么我们就会有内存泄漏。为了消除释放的负担,我们希望在接收者完成操作后将其标记为释放。自动释放将对象指定为稍后释放,它表示“保留对象,直到下一个内存池被清空”。如果你没有明确地清空池,那么你可以确保返回值将保留在接收者的直接范围内。
id myObject = [[[NSString alloc] init] autorelease]; return myObject;
这段代码是可以的,因为返回的对象是有效的,但所有权的责任已经传递给了池。
当控制权返回到主事件循环时,池可以确定对对象感兴趣的任何对象都会保留它,因此它会释放它所拥有的所有对象。任何没有引用的对象将被删除,任何有引用的对象将成为保留它的对象的拥有者。一切都保持井井有条。
大多数工厂方法返回自动释放的对象。这是完全合理的,如果你编写一个工厂方法,你应该这样做。例如
NSColor* red = [NSColor redColor];
NSColor 的 redColor 类方法返回一个自动释放的颜色对象。如果要保留此对象,则应保留它。如果只想在几行代码中使用它,则无需执行任何操作 - 它将在稍后自动删除,但仅在您完成后才会删除。
在实践中,保留、释放和自动释放很简单。有些人似乎觉得它很混乱,但也许他们认为它比实际更复杂。要保留给你的对象,请保留它。你创建的对象已经保留了。要丢弃它不再引用它,请释放它。如果你写了一个工厂方法,就自动释放它。如果您只是在短时间内使用工厂方法返回的对象,请什么也不做。
当你的对象的属性是另一个对象,而其他人想读取该属性时会怎样?你应该保留、自动释放,还是什么?不,你什么也不应该做。你已经保留它了,因为它是你自己的属性。如果调用者想保留它,那是他们的事,自动释放与它无关,因为它不是工厂方法。所以直接返回对象。所以
- (NSString*) text { return _text; }
绝对正确。
请注意,如果调用者确实保留了它,你仍然可以自由地在完成它后释放它。错误的做法是调用者在想在较长时间内使用该值(比当前事件更长的时间)时,没有保留它。这不是一个新规则 - 如果调用者创建对对象的引用,它必须保留它。但是,如果它只需要在短时间内使用该对象,例如在一个函数中,它可以简单地这样做,而不必担心它会被其他人释放。
我们已经接触过事件循环,但没有解释它是什么。在 Cocoa 应用程序中,用户活动会导致 *事件*。这些可能是鼠标点击或拖动、键盘输入、选择菜单项等等。其他事件可以自动生成,例如定时器定期触发,或者网络上出现某些内容。对于每个事件,Cocoa 都期望有一个对象或对象组准备好适当地处理该事件。事件循环是检测和路由这些事件到适当位置的地方。每当 Cocoa 没有做其他事情时,它就坐在事件循环中等待事件到来。(事实上,Cocoa 不会像建议的那样轮询事件,而是它的主线程进入睡眠状态。当事件到来时,操作系统会唤醒线程并恢复事件处理。这比轮询效率高得多,并允许其他应用程序更流畅地运行)。
每个事件都被视为一个独立的事物,然后事件循环获得下一个事件,依此类推。如果事件导致需要更新,则会在事件结束时检查此更新,如果需要,则执行窗口刷新。