Objective-C入门
Objective-C入门(A First Look at Objective-C)
转自: http://www.fish888.com/Objective-C-t68418
原著:Noah Roberts (jik)
翻译:jeff_yecn
顾名思义,Objective-C就是面向对象的C语言。你可以使用Objective-C编程,但不使用任何非C语言的东西。但那样将是对这语言的浪费。 Objective-C使用所有的C语言的内容,同时添加了一些使得它更具面向对象特征的语法。本指南假定你已经知道什么是面向对象编程,同时熟悉C(你不必是这方面的专家,但是我不打算在这里教你应该已经知道的东西)。
我同时必须建议你不要仅仅读这一篇文章,因为它远远不是在 Objective-C(ObjC)方面的终极资源。在互联网上你可以找到其它更好、更详细的学习这种语言的资料。在我的网站:members.xoom.com/jik_you上有一些链接,也许你可以从那里开始搜索。
ObjC,和其他面向对象语言一样,是围绕着对象的,所以让我展示一下对象的构成。在ObjC中,对象有两部分。它有一个界面,是对象的外在表现。同时它有一个实现,就是它的内部工作机制。换句话说,就是外界看到的部分和看不到的部分。有时,这样更容易理解。
界面
在ObjC中,界面通常有它自己的头文件(通常是.h),但并不是一定要这样。你可以把界面和实现放在一个文件中,或者把几个界面放在一起,但每个界面是不同的,你必须对此很清楚,否则将会感到非常苦恼。我将只使用在头文件中定义界面的办法。
首先,你必须导入这个对象的超类的界面。你可以使用与“#include”相同语法的预处理标识“#import”。事实上,两者之间的区别只是#import会进行检查以确保文件仅被包含一次。有些人选择使用#include,这完全符合语法,但ObjC设计是使用#import,你也许愿意考虑这一点而保护你的界面文件。
如果你使用gcc(或egcs等)来作为你的编译器,也许你会愿意使用开关:-Wno-import,否则,gcc会无休止地提示你使用import来代替include。
因此,以下是以一个对象为超类的新类的第一行代码,也通常是所有类的开头。
#import /* gcc运行时库的对象 */
现在,如果你计划引用其他的类,你可以导入他们的头文件,更好的办法是使用@class来声明这些标识是类。界面并不需要知道这个类是什么和怎么做的全部信息,所以引用它的头文件并没有作用,让我们使用@class。
@class String; //简单吧?
顺便说一下,ObjC同时使用“//”和“/* */”来作为注释,这有时会比较方便。
好了,现在我们准备让世界知道想要什么类型的对象,所以让我们声明它。首先我要说明的是我在声明一个新类,在此之前,你应该定义好这个新类的超类。
@interface NewClass : Object //新类在左边,超类在右边。
我并没有漏掉“;”,就是这样写的。
现在我们声明实例变量,不管他们是不是私有的(我碰巧认为私有的东西是不应该被人看见的,实际上也是这样:P)。我在一对“{}”中间写上,就象我们在C的结构中做的那样。
{
@public
int aValue;
@protected //缺省情况下只能被子类所见
int value2;
@private //不可见
int mrInvisible;
}
做完这些以后,我们要放入method的原型定义。method只能是公共的,虽然如果我愿意的话,我可以演示给你看如果你访问一个私有的method的时候会产生的编译器警告信息。
method 既有“类”的(也有些人叫做“工厂”的),也有“实例”的。类method只能由类对象访问,实例method显然由实例访问。例如,我声明了一个保存实例的变量,但在我使用它之前,我必须使用一个类method来产生一个这个变量指向的实例,这样我才可以调用它的实例method。我这样做了以后,我就不能再访问它的类method了。在我释放对象实例之前,我只能访问它的类method。描述一个method时,你根据它是类method还是实例 method响应在前面放上“+”或“-”的符号。然后,在“( )”中你填入method返回值的类型,除非你返回的是代表任何对象的通用类型id。在然后,如果你的method有参数的话,你先写一个“:”,跟着是在“( )”中间的参数类型,再接着是变量名。你可以一直写下去,直到以“;”结束。
同时,在第一个参数之后和下一个“:”之前,你还可以放入一些单词,但单词与“:”之间不能有空格。
也许你现在已经糊涂了,让我们看一个类和实例method。
+createWith: (int)value; //足够简单
-(void)say: (String *)what to: (String *)name; //第一个参数是what,
// to是method名称的一部分。
在参数表中间使用额外的单词可以使得method念起来更容易理解。就象上面第二个method,念起来象“say 'Hello' to jik”,而不是“say 'Hello', jik”。
可变长度参数表的语法和C语言中是一样的:
-(void)doStuff: (id)format,...;
最后,当我们完成全部的定义以后,我们通过下面的语句告诉编译器我们完成了:
@end
这里谈的都是关于对象的。你可以使用typedef或其他在C中可以放入头文件的语法继续定义对象。
名字的小规则
method 名由除了+/-,类型和参数以外的所有东西组成。例如,method“-(void)say: (String *)what to: (String *)name”的名字在内部是“say:to:”。如果你使用gdb ObjC补丁,这也是用名字引用method的方法。
同时,你应该记住ObjC通常会被处理为C语言并传递到C编译器。过程中名字通常会发生改变,你应该牢记这一点而不要做可能会迷惑编译器的事......就好象:
-roll_d: (int)sides;
-roll: (int)count d: (int)sides;
在这个例子中,两个method都会被gcc编译器转化为以下的C函数:
__i_ClassName_roll_d_()
这个事情发生过在我身上,所以相信我,千万小心。如果我是你的话,我不会在method名的中间使用“_”,在开头使用则没有什么问题。
消息
由于你在实现中将经常使用消息,我们必须首先讨论他们是什么以及在语法上怎么表达。一个消息只是对象method的简单调用。在面向对象编程中,由于它就好象给一个对象发送消息告诉它做某些事,因此称作消息。要表达一个消息,首先是对象的名字,跟着是你要它执行的method以及你要传递的参数,这一切用 “[ ]”包围起来。
例如:
[object jumpAround];
或
[object jump: forward];
“object”可以是一个类也可以是类的一个实例。通常类以大写字母开头,而实例则以小写开头。如果“object”是类的话,你只能要求它执行类method,反之亦然。
例如,要建立类“Object”的一个新实例,我们可以使用...:
[Object alloc];
“alloc”是为新对象分配内存的类method。但这样并不能使得对象能够使用,因为它的实例变量还没有初始化...要那样做的话,我们调用“init”...因此,我们需要做的全部事情是:
id object=[Object alloc];
[object init];
如果消息的返回值的类型合适的话,你也可以使用它作为另一个消息的一部分。由于“alloc”消息返回id类型,我们可以用这个返回值作为消息发送的对象。由于空返回值不能做任何事情,因此通常返回id会比返回void要好。在你的method执行完成后,最好能够返回指向当前对象自身的变量“self”,除非你不想它作为其他消息的参数或发送对象...如果那样的话,返回void吧。一个使用空返回值的好例子是释放内存的method,不好的例子是分配内存的时候。所以,我们可以使用这个特性来在一行中完成分配内存和初始化的工作:
id object=[[Object alloc] init];
当然,你必须确定init method也返回id,否则我们不能继续使用“object”变量。
消息的执行按照先进先出的顺序。如果你把一个消息作为另一个消息的一部分,所有的内层的消息回在外层的消息执行前执行和返回。最终的返回值为最后一个执行的消息的返回值,内层的消息不会在最后返回。
实现
这是对象完成所有工作的部分。它定义所有对象的method的内部工作机制,准确的说,是所有非继承的部分。实现文件的后缀为“.m”。
首先,你需要导入类自己的和这个对象中将要用到的类的头文件。象通常一样,你可以做额外的define,typedef以及其他你可能会在C源程序开头做的事情。
当你完成所有的准备工作,你告诉编译器你开始定义一个新对象的实现部分。你用与界面定义类似的method来进行,但你不在需要指明超类是什么,除非你没有为这个对象创建一个界面(这是可能做到的)。
当然,如果你愿意的话,你也可以在说明一次超类是什么,没有任何规定说明你不能那样做。你可以用创建界面定义一样的method来做。
@implementation NewClass
在这里你不需要添加实例变量,所以在实现中不需要包括那一部分。我们现在要做的是定义新的method,以及我们需要覆盖的继承下来的method。有关的语法是一种界面中的method声明和C函数的交叉。
以下是一个简短的method定义的例子:
-sayHello
{
? printf("Hello world! ");
? ? ? return self;
}
下面一个例子是有参数使用消息的:
-say: (String *)What
{
? ? printf("%s ",[what cString]);
? ? ? return self;
}
你用与界面定义相同的method结束实现定义。
@end
特殊的method
当你在派生一个类的时候,你要知道有些method的情况有些特殊。在不同的API中,它们的称呼有所不同,但其中相似的已经分类在一起。有些method在多数情况下都不应该被覆盖,有些可以被覆盖,在你这样这样做的时候必须满足一些特定的要求。
你不应该覆盖“alloc”method。这个method做了些特别的事情来为新的实例分配存储空间。它已经被在Object中定义成能够为你需要创建实例的任意类分配正确的存储空间。如果你覆盖它,你的类很可能不能正确工作。
如果你使用函数库使用一种引用计数技术,例如libFoundation使用自动释放池来进行对象清除,你不能覆盖release,autorelease 和retain method。这些method使用特别的东西来跟踪对象的引用。改变其中任何一个method会打乱整个体制。
init method可以被覆盖,但你必须保证在做其他事情之前调用超类的init method,否则有些变量可能会没有初始化。总是在覆盖的init method的第一行写上[super init];。对你的库函数中使用的dealloc或free method,同样需要调用超类的method,但是是在结尾。而不是method的开头。简单地把“return [super dealloc];”放在最后一行就足以确保完成超类所需要的所有清除工作。
非标准的问题
ObjC有许多的分支。对各种语言的功能和运行时支持没有一个绝对的标准。各种ObjC编译器之间并不兼容。这很大程度上意味着你要定位于一种编译器,因为它们之间的差异是如此的大,以至于你不能使用#ifdef来编写跨编译器代码。
不但运行时库是完全不同,语言也有不同的扩展。比如,ObjC的有些类型如“类别(Catagory)”和“协议(Protocol)”并没有在全部编译器上实现。有些有,有些没有。我肯定这可以更深入的讨论,而这仅仅是问题的表面。
在选择编译器的时候,你必须要理解这一点。有些编译器,象POC,能够跨平台兼容,但缺少语言中的一些部分。有些更完整些,但仍然缺少一些重要的部分,比如,gcc是ObjC的一个比较完整的实现,但仍然缺少对模块卸载的支持(ObjC建议具有在运行时添加和移除一些部分的能力,gcc可以加载他们,但不能移除)。
有些编译器之间存在一些关系。象Stepstone和POC更多地来自于由Brad Cox编写的该语言的最初实现版本。其它的,象gcc,更多地来源于NeXT。
他们之前都是不同的,如果不重写一大部分源代码,并不能在另一个环境下进行编译(假如存在这种可能的话)。记住这一点,下面我将介绍一些不那么具有可移植性的概念。在此之前,我讲的内容包括在ObjC的所有实现中。现在我们要进入不是那么标准的领域。
新数据类型
ObjC有一些我现在应该提一下的新数据类型。其中的有些类型的名称可能会有不同,或者有些没有实现。你要验证一下你的编译器是否支持。
BOOL是布尔类型,可以是YES或NO,NO为0,YES为非0。
STR为char *。
IOD为FILE *。
id是代表任何ObjC对象的通用类型。
SEL类型是代表method的变量。你可以要求对象执行SEL代表的method。你用@selector(methodName)来创建SEL。“methodName”是method名的ObjC表示形式。例如前面提到的“say:to:”。
IMP用C语言指针代表一个method。这用于你需要节省寻找消息的处理程序的时间的情况下,因为IMP是对method的直接链接。通常用于需要高速度的循环过程中。
这些也许对你来说基本够用了......象我前面说过的,这篇文章不应该是关于ObjC的唯一资源。
类别(Catagory)
类别是扩展对象能力的一种方法。你可以在对象中添加新的method,但你不能添加新的实例变量。新的method被视为扩展的类的一部分,子类将继承这些新的δ堋D阋部梢酝ü?唇ㄏ嘤Φ睦啾鹄锤哺抢嗟method。如果你有两个覆盖一个类的method的类别,究竟哪一个覆盖并不确定。
类别的语法与类的语法相似。他们都有界面和实现。当然,也有一些轻微的差别。类别定义的开头几行象这样:
@interface ClassName (CatagoryName)
@implementation ClassName (CatagoryName)
ClassName是你要扩展的类的名称,CatagoryName是类别的识别的名称。类别可以访问所扩展的类的实例变量。
其余的语法是相同的:在界面中声明method,在实现中定义它们,并以@end结束。
你可以用类别建立私有method。把类别的界面放在类的实现文件的实现代码的前面。然后把你需要作为私有mthod的放在这个类别中。创建对象的实现,然后,在这个文件的末尾,加入类别的实现。这样可以对外隐藏这些method,如果在其他地方试图访问这些method,编译器将会给出警告。它不能够在运行时避免被访问,而只能在编译时警告你。在ObjC中没有办法禁止method被访问。
协议(Protocol)
协议,顾名思义,定义一组类之间相互遵守的行为规范。协议没有界面和实现,但他们与界面更加相似。你不需要添加新的实例变量,也不添加新的method或定义任何method的内部实现。协议所需要做的只是定义一套要求所有类都必须包含的method,以遵循这个协议。
协议在你使用ObjC的动态类型机制的时候特别有用。ObjC拥有和Java类似的运行时的动态类型。这是它优于C++的一点,C++只支持“编译时”的动态类型。
比如你要创建一个可以接受任何对象的method。你必须确定这个对象可以响应你要发送的消息,否则你将会发送这个对象不明白的消息给它。这会导致一些问题。如果你使用了协议,你就可以确保它能响应你将要发给它的许多消息,如果你仅是需要发送一个消息,你还有其它方法来检查......这一点我留给你自己去研究。
同时,在上述的情况中,你还可以使协议要求你所期望的参数。我现在会展示给你看怎么完成这些工作。
定义一个新的协议,首先:
@protocol ProtocolName
接着是任意数量的method的声明,请使用在界面定义的相同的语法。最后以@end结束。
现在,为了遵循这个协议,你首先声明你要创建的新对象的界面的内容。然后,你必须在这个对象的实现中定义协议中的所有method。
@interface NewObject : Object
上述的语句告诉编译器这是一个叫NewObject的对象,它的超类是Object,并要符合ProtocolName的协议。
要查询一个对象是否满足特定的协议,可以使用“conformsTo"的mthod。
[object conformsTo:@protocol(ProtocolName)];
这会根据对象是否符合的情况返回YES或NO。
要查询一个参数是否符合特定的协议,可以象如下地声明method:
-method:(id)argument;
(译注:原文如此,在gcc中似乎并不支持,按照逻辑来说应该为:-method:(id ) argument;)
你也可以返回满足协议的对象:
-(id ) method;
结论
好了,以上就是关于ObjC的快速和琐碎的介绍,要学习它,你需要继续阅读其他的信息资源。我的网站有一些链接,除此以外......做一个网站搜索或询问一下。我会给你一个简短的例子,它可以用gcc编译。
Noah Roberts (jik)
原作者授权声明:
我在此授权,可以免费将本文以任何形式或格式进行复制和分发,但这个版权声明必须出现在正文内或复制品内(在复制品内,它必须明显地可以被读者读到)。你可以编辑本文以修复拼写和语法错误。但你不能以任何形式修改它的内容。如果你不同意这个授权协议,你应该联系我来复制本文档。
“I herby grant, free of charge, the right to copy and redistribute this
work whole in any form or format, as long as this copyright notice
appears intact somewere on or about the copy (it must be obviously
available to the reader of the copy, from the copy). You may edit this
document to fix spelling or gramatical errors, but you must not modify
it's content in any other way. If you object to this licence you must
contact me for permission to copy this document.”
最后修订:1999年3月16日
Objective-C强化解读
一、XCode、Objective-C、Cocoa说的是几样东西?
答案:三样东西。
XCode:你可以把它看成是一个开发环境,就好像Visual Studio或者Netbeans或者SharpDevelop一样的玩意。你可以将Interface Builder认为是Visual Studio中用来画界面的那部分功能单独提出来的程序。
Objective-C:这是一种语言,就好像c++是一种语言,Java是一种语言,c#是一种语言,莺歌历史也是一种语言一样。
Cocoa:是一大堆函数库,就好像MFC、.NET、Swing这类玩意,人家已经写好了一堆现成的东西,你只要知道怎么用就可以了。
有些人会比较容易混淆Objective-C和Cocoa,就好像有些人会混淆c#和.NET一样。这两个东西真的是两个不一样的东西。
//这点对我们初学者来讲还是比较有用的,能从明白框架;
二、Objective-C是什么?
你可以把它认为是语法稍稍有点不一样的c语言。虽然第一眼望上去你可能会认为它是火星语,和你所认知的任何一种语言都不一样。
先简单列出一点差别:
问题一:我在程序中看到大量的减号、中括号和NS****这种东西,他们是什么玩意儿?
1 减号(或者加号)
减号表示一个函数、或者方法、或者消息的开始,怎么说都行。
比如c#中,一个方法的写法可能是:
private void hello(bool ishello)
{
//OOXX
}
用Objective-C写出来就是
-(void) hello:(BOOL)ishello
{
//OOXX
}
挺好懂的吧?
不过在Objective-C里面没有public和private的概念,你可以认为全是public。
而用加号的意思就是其他函数可以直接调用这个类中的这个函数,而不用创建这个类的实例。
//(-)是实例方法;(+)是类方法
2 中括号
中括号可以认为是如何调用你刚才写的这个方法,通常在Objective-C里说“消息”。
比如C#里你可以这么写:
this.hello(true);
在Objective-C里,就要写成:
[self hello:YES];
//[]:消息的调用语法,以下格式都是,只是参数的区别
[receiver message]
[receiver message:argument]
[receiver message:arg1 andArg:arg2]
3 NS****
老乔当年被人挤兑出苹果,自立门户的时候做了个公司叫做NextStep,里面这一整套开发包很是让一些科学家们喜欢,而现在Mac OS用的就是NextStep这一套函数库。
这些开发NextStep的人们比较自恋地把函数库里面所有的类都用NextStep的缩写打头命名,也就是NS****了。比较常见的比如:
NSLog
NSString
NSInteger
NSURL
NSImage
…
你会经常看到一些教学里面会用到:
NSLog (@"%d",myInt);
//很有用的东东
这句话主要是在console里面跟踪使用,你会在console里面看到myInt的值(在XCode里面运行的时候打开dbg窗口即可看到)。而我们在其他开发环境里面可能会比较习惯使用MessageBox这种方式进行调试。
你还可以看到其他名字打头的一些类,比如CF、CA、CG、UI等等,比如
CFStringTokenizer 这是个分词的东东
CALayer 这表示Core Animation的层
CGPoint 这表示一个点
UIImage 这表示iPhone里面的图片
CF说的是Core Foundation,
CA说的是Core Animation,
CG说的是Core Graphics,
UI说的是iPhone的User Interface
……还有很多别的,等你自己去发掘了。
问题二、#import、@interface这类玩意说的是什么?
1、#import
你可以把它认为是#include,一样的。但是最好用#import,记住这个就行了。
// import和 include的区别:import可以过滤掉重复的引用
2、@interface等等
比如你在c#中写一个抓孩子类的定义:
public class Kids : System
{
private string kidName=”mykid”;
private string kidAge=“15”;
private bool isCaughtKid()
{
return true;
}
}
当然,上面的写法不一定对,就是个用于看语法的举例。
// 程序框架
在Objective-C里就得这么写:
先写一个kids.h文件定义这个类:
@interface Kids: NSObject {
NSString *kidName;
NSString *kidAge;
}
-(BOOL) isCaughtKid:;
@end
再写一个kids.m文件实现:
#import “kids.h”
@implementation Kids
-(void) init {
kidName=@”mykid”;
kidAge=@”15”;
}
-(BOOL) isCaughtKid:{
return YES;
}
@end
这个写法也不一定对,主要是看看语法就行了。-_-b
问题三、一个方法如何传递多个参数?
一个方法可以包含多个参数,不过后面的参数都要写名字。
多个参数的写法
(方法的数据类型) 函数名: (参数1数据类型) 参数1的数值的名字 参数2的名字: (参数2数据类型) 参数2值的名字 …. ;
举个例子,一个方法的定义
-(void) setKids: (NSString *)myOldestKidName secondKid: (NSString *) mySecondOldestKidName thirdKid: (NSString *) myThirdOldestKidName;
实现这个函数的时候:
-(void) setKids: (NSString *)myOldestKidName secondKid: (NSString *) mySecondOldestKidName thirdKid: (NSString *) myThirdOldestKidName{
大儿子 = myOldestKidName;
二儿子 = mySecondOldestKidName;
三儿子 = myThirdOldestKidName;
}
调用的时候:
Kids *myKids = [[Kids alloc] init];
[myKids setKids: @”张大力” secondKid: @”张二力” thirdKid: @”张小力”];
而如果你用c#写这个方法,大致的写法可能是
public void setKids( string myOldestKidName, string mySecondOldestKidName, string myThirdOldestKidName)
{
…
}
调用的时候大概的写法可能是:
Kids myKids = new Kids();
myKids.setKids (“张大力”, “张二力”, “张小力”);
明白了吧?其实不怎么难看懂。
基本上,如果你能了解下面这段代码的转换关系,你Objective-C的语法也就懂了八成了:
[ [ [MyClass alloc] init:[foo bar]] autorelease] ;
转换成C#或者Java的语法也就是:
MyClass.alloc().init(foo.bar()).autorelease();
三、其他的一些东西
其实这些本站之前的文章有所提及,这里再详细解释一下。
1、 id:
Objective-C有一种比较特殊的数据类型是id。你可以把它理解为“随便”。
在Objective-C里,一切东西都是指针形式保存 ,你获取到的就是这个对象在内存的位置。那么id就是你知道这个位置,但是不知道里面是啥的时候的写法。
2、 同一个数组可以保存不同的对象:
比如一个数组NSArray,这种数组里面可以保存各种不同的对象,比如这个数组里:
myArray <—-|
0: (float) 234.33f
1: @”我是个好人”
2: (NSImage *) (俺的美图)
3: @”我真的是好人”
这是一个由4个东西组成的数组,这个数组包括一个浮点数,两个字符串和一个图片。
3、BOOL,YES,NO:
你可以认为YES表示C#或者Java里的true,NO表示false。而实际上YES是1,NO是0,BOOL本身就是个char。
4、IBOutlet、IBAction是啥玩意,总能看到。
这两个东西其实在语法中没有太大的作用。如果你希望在Interface Builder中能看到这个控件对象,那么在定义的时候前面加上IBOutlet,在IB里就能看到这个对象的outlet,如果你希望在 Interface Builder里控制某个对象执行某些动作,就在方法前面加上(IBAction)。
而这两个东西实际上和void是一样的。
5、nil。
Objective-C里的NULL(空)就这么写,表示空指针。
6、为什么是@”字符串”而不是”字符串”
前面加上@符号,编译器在编译的时候会在程序中给你留出位置,这样才能保证这个字符串不会丢失。反正记住,如果你要想把某些字符串写死在程序里,就要用@”字符串”,如果忘了用@,程序应该会出错。
superzhou大侠指正:
6、为什么是@”字符串”而不是”字符串”
”字符串”是C的字符串,@”"是把C的字符串转成NSString的一个简写. 在需要NSString的地方才需要这个转化,例如NSLog里面.
在需要C string的地方,还是用”字符串”的.
另外,@”"这个转换是不支持中文的.例如NSLog(@”字符串”); 是一定输出不了中文的.
四、Objective-C 2.0
Objective-C 2.0是Leopard新增加的一门语言,其实和原来的Objective-C是一样的。主要是增加了属性。详细的内容这里不写了,可以参阅Allen Dang的这篇文章,写的很明白。
http://blog.codingmylife.com/?p=81
五、总结
现在来总结一下怎么看Objective-C的代码和怎么开始学Objective-C吧。
1、记住Objective-C就是C,不是火星语,这个很关键。
2、记住你自己看不懂不表示脑子迟钝,大部分人第一次看Objective-C的代码可能比你还要迟钝。
3、把CocoaChina.com加入收藏夹,看不明白代码就来再看一遍这篇开宗明义的好文。
4、文档很关键,当你看不懂某些东西说的是什么的时候,先查Cocoachina,再看英文文档里面的API说明,尤其这个类是以NS开头的时候。再不行就去google搜,直接把你要查的方法贴进google,通常能找到不少人也在问同样的问题,自然也有热心人活雷锋帮助回答。
5、可以看hello world例子,但是不能总看,看多了真的会晕。另外,千万要放弃苹果官方的Currency Converter货币转换的例子,那个例子是毒药,刚学的时候越看越蒙。
6、学习一门语言最好的方法是先用,和学外语一样,当你会说的时候自然会读。给自己设立一个简单的目标,比如做一个简单的程序,然后一点点解决问题。这样学习起来比只看例子快得多。
这是一篇初学者写的文章,希望对同样是初学者的你有一点点帮助:)虽然只是很肤浅的一点点内容,但是应该对你迈入Objective-C的大门有一点帮助。看懂了这篇文章,回过头看Cocoachina的其他文章,你就会觉得很顺眼了。记得天天来哦。
另外,这篇Objective-C的参考资料也比较好,如果你有兴趣可以一读。
中文参考文档:http://www.otierney.net/objective-c.html.zh-tw.big5
Objective-C内存管理方式
原文:http://ocen.javaeye.com/blog/518512
在Objective-C或者说Cocoa里面,有三种内存的管理方式。
第一种,叫做“Garbage Collection”。这种方式和java类似,在你的程序的执行过程中,始终有一个高人在背后准确地帮你收拾垃圾,你不用考虑它什么时候开始工作,怎样工作。你只需要明白,我申请了一段内存空间,当我不再使用从而这段内存成为垃圾的时候,我就彻底的把它忘记掉,反正那个高人会帮我收拾垃圾。遗憾的是,那个高人需要消耗一定的资源,在携带设备里面,资源是紧俏商品所以iPhone不支持这个功能。所以“Garbage Collection”不是本入门指南的范围,对“Garbage Collection”内部机制感兴趣的同学可以参考一些其他的资料,不过说老实话“Garbage Collection”不大适合适初学者研究。
第二种,叫做“Reference Counted”。就是说,从一段内存被申请之后,就存在一个变量用于保存这段内存被使用的次数,我们暂时把它称为计数器,当计数器变为0的时候,那么就是释放这段内存的时候。比如说,当在程序A里面一段内存被成功申请完成之后,那么这个计数器就从0变成1(我们把这个过程叫做alloc),然后程序B也需要使用这个内存,那么计数器就从1变成了2(我们把这个过程叫做retain)。紧接着程序A不再需要这段内存了,那么程序A就把这个计数器减1(我们把这个过程叫做release);程序B也不再需要这段内存的时候,那么也把计数器减1(这个过程还是release)。当系统(也就是 Foundation)发现这个计数器变成了0,那么就会调用内存回收程序把这段内存回收(我们把这个过程叫做dealloc)。顺便提一句,如果没有 Foundation,那么维护计数器,释放内存等等工作需要你手工来完成。
这样做,有一个明显的好处就是,当我们不知道是A先不使用这段内存,还是B先不使用这段内存的时候,我们也可以非常简单的控制内存。否则,当我们在程序A里面释放内存的时候,还需要看看程序B是否还在使用这段内存,否则我们在程序A里面释放了内存之后,可怜的程序B将无法使用这段内存了。这种方式,尤其是在多线程的程序里面很重要,如果多个线程同时使用某一段内存的时候,安全的控制这些内存成为很多天才的程序员的梦魇。
如果有同学搞过COM的话,那么应该对Release/AddRef很熟悉了,其实Obejctive-C和他们的机制是一样的。
接下来,我需要解释一下Autorelease方式。上述的 alloc->retain->release->dealloc过程看起来比较令人满意,但是有的时候不是很方便,我们代码看起来会比较罗嗦,这个时候就需要Autorelease。Autorelease的意思是,不是立即把计数器减1而是把这个过程放在线程里面加以维护。当线程开始的时候,需要通知线程(NSAutoreleasePool),线程结束之后,才把这段内存释放(drain)。Cocoa把这个维护所有申请的内存的计数器的集合叫做pool,当不再需要pool(水池)的时候就要drain(放水)。
笔者想要说的是,虽然iPhone支持Autorelease但是我们最好不要使用。因为Autorelease方式从本质上来说是一种延迟释放内存的机制,手机的空间容量有限,我们必须节约内存,确定不需要的内存应该赶快释放掉,否则当你的程序使用很多内存的情况下也许会发生溢出。这一个习惯最好从刚刚开始学习使用Objective-C的时候就养成,否则长时间使用Autorelease会让你变得“懒散”,万一遇到问题的时候,解决起来会非常耗费时间的。所以,还是关于内存管理,我们还是自己动手,丰衣足食。当然笔者不是说绝对不可以使用,而是当使用Autorelease可以明显减低程序复杂度和易读性的时候,还是考虑使用一下换一下口味。
第三种,就是传统而又原始的C语言的方式,笔者就不在这里叙述了。除非你在Objective-C里面使用C代码,否则不要使用C的方式来申请和释放内存,这样会增加程序的复杂度。
Objective-C 2.0之前需要了解的:关于Obj-C内存管理的规则
Objective-C 2.0增加了一些新的东西,包括属性和垃圾回收。那么,我们在学习Objective-C 2.0之前,最好应该先了解,从前是什么样的,为什么Objective-C 2.0要增加这些支持。
这一切都跟Cocoa内存的管理规则有关系,我们知道,Objective-C中所有变量都定义为指针。指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址,如果使用不当,就会出错或者造成内存的泄露。要了解这些,就需要看看其内存管理的规则到底是什么样的。
这篇文章也应该做为苹果开发工具中提供的性能调试工具Instruments使用前必读知识进行阅读。Cocoa China将在稍后提供Instruments工具的使用方法,以及Objective-C 2.0的详细介绍。
要知道,如果你使用Objective-C 2.0,那么本文描述的大部分工作你都不需要自己去处理了。但是这并不意味着你可以不了解它,相反,只有你对内存管理规则更加了解,你才能更好地使用Objective-C 2.0带来的便利。
本文原文作者是Mmalcolm Crawford,原文地址 这篇文章翻译起来比较晦涩,希望您能看得懂。
当Cocoa新手在进行内存管理时,他们看上去总是把事情变得更为复杂。遵循几个简单的规则就可以把生活变得更简单。而不遵循这些规则,他们几乎一定会造成诸如内存泄露或者将消息发送给释放掉的对象而出现的的运行错误。
Cocoa不使用垃圾回收(当然,Objective-C 2.0之后开始就使用了),你必须通过计算reference的数量进行自己的内存管理,使用-retain , -release 和-autorelease 。
方法描述
-retain
将一个对象的reference数量增加1。
-release
将一个对象的reference数量减少1。
-autorelease
在未来某些时候将reference数量减少1.
-alloc
为一个对象分配内存,并设置保留值数量(retain count)为1。
-copy
复制一个对象,并将其做为返回值。同时设置保留值数量(retain count)为1。
保留值数量规则
1 在一定的代码段中,使用-copy ,-alloc 和-retain 的次数应该和-release ,-autorelease 保持一致。
2 使用便利构造方法创建的对象(比如NSString 的stringWithString )可以被认为会被自动释放。(autoreleased)
3 在使用你自己的参数实例时,需要实现-dealloc 方法来释放。
例子
-alloc / -release
- (void )printHello
{
NSString *string;
string = [[ NSString alloc] initWithString: @"Hello" ];
NSLog (string);
// 我们用 alloc 创建了 NSString,那么需要释放它
[string release ];
}
便利构造方法
- (void )printHello
{
NSString *string;
string = [ NSString stringWithFormat: @"Hello" ];
NSLog (string);
// 我们用便利构造方法创建的 NSString
// 我们可以认为它会被自动释放
}
永远使用存取方法
虽然有时候你可能会认为这很麻烦,但是如果你始终使用了存取方法,造成内存管理问题的麻烦将会降低很多。
如果你在代码实例的参数中频繁使用-retain 和-release ,几乎可以肯定你做了错误的事情。
例子
假设我们希望设置一个Counter对象的数量值。
@interface Counter : NSObject
{
NSNumber *count;
}
为了获取和设置count值,我们定义两个存取方法:
- (NSNumber *)count
{
return count;
// 无需 retain或者 release,
// 仅仅传递数值
}
- (void )setCount:(NSNumber *)newCount
{
// newCount值会被自动释放,那么我们希望保留这个 newCount
// 所以需要在这里 retain。
[newCount retain ];
// 由于我们在这个方法中仅仅改变了计算数量的对象,我们可以在这里先释放它。因为[nil release] 在objective-c 中也是允许的,所以即使count 值没有被指定,也可以这样调用。
// 我们必须在[newCount retain] 之后再释放count ,因为有可能这两个对象的指针是同一个。我们不希望不小心释放它。
[count release ];
// 重新指定
count = newCount;
}
命名约定
注意存取方法的命名约定遵循一个模式: -参数名 和 -set参数名 。
遵循这一约定,会使你的代码可读性更强,而且,更重要地是你可以在后面使用key-value编码。(参阅NSKeyValueCoding协议)。
由于我们有一个对象实例参数,我们必须实现一个释放方法:
- (void )dealloc
{
[ self setCount: nil];
[ super dealloc];
}
假设我们希望实现一个方法重置计数器,我们会有很多选择。在最开始,我们使用了一个 便利构造方法,所以我们假设新的数值是自动释放的。我们不需要发送任何retain 或者release 消息。
- (void )reset
{
NSNumber *zero = [ NSNumber numberWithInt: 0 ];
[ self setCount:zero];
}
然而,如果我们使用-alloc 方法建立的NSNumber 实例,那我们必须同时使用一个-release 。
- (void )reset
{
NSNumber *zero = [[ NSNumber alloc] initWithInt: 0 ];
[ self setCount:zero];
[zero release ];
}
常见错误
在简单的情况下,以下代码几乎一定可以正常运行,但是由于可能没有使用存取方法,下面的代码在某些情况下几乎一定会出问题。
错误-没有使用存取方法
- (void )reset
{
NSNumber *zero = [[ NSNumber alloc] initWithInt: 0 ];
[count release ]
count = zero;
}
错误-实例泄露
- (void )reset
{
NSNumber *zero = [[ NSNumber alloc] initWithInt: 0 ];
[ self setCount:zero];
}
新建的NSNumber 数值数量是1(通过alloc ),而我们在这个方法里没有发出-release 消息。那么这个NSNumber 就永远不会被释放了,这样就会造成内存泄露。
错误-对已经释放的实例发送-release 消息
- (void )reset
{
NSNumber *zero = [ NSNumber numberWithInt: 0 ];
[ self setCount:zero];
[zero release ];
}
你随后在存取count的时候在这里就会出错。这个简便构造方法会返回一个自动释放的对象,你无需发送其他释放消息。
这样写代码意味着,由于对象已经被自动释放,那么当你释放时,retain count将被减至0,对象已经不存在了。当你下次希望获取count值时,你的消息会发到一个不存在的对象(通常这样你会得到一个SIGBUS 10的错误提示)。
经常造成混淆的情况
数组和其他集合类
当对象被加入到数组、字典或者集合中,集合类会将其保留。当集合被释放的同时,对象也会收到一个释放消息。如果你希望写一个建立数字数组的例子,你可能会这么写:
NSMutableArray *array;
int i;
// …
for (i = 0 ; i < 10 ; i++)
{
NSNumber *n = [ NSNumber numberWithInt: i];
[array addObject : n];
}
在这个例子里,你无需保留新建的数值,因为数组会帮你保留。
NSMutableArray *array;
int i;
// …
for (i = 0 ; i < 10 ; i++)
{
NSNumber *n = [[ NSNumber alloc] initWithInt: i];
[array addObject : n];
[n release];
}
本例中,在for 循环里你需要给n发送一个-release 消息,因为你需要始终在-alloc 之后将n的数量保持为1。这么做的原因是当其通过-addObject: 方法被添加至数组中时,数组已经将其保存起来。即使你释放了n,但是这个数字由于已经保存在数组里,所以不会被释放。
为了了解这些,假设你自己就是编写数组类的人。你不希望接收的对象未经你同意就消失,所以你会在对象传递进来时,对其发送一个-retain 消息。如果他们被删除,你同时也要对应地发送一个-release 消息。在你自己-dealloc 时,你也要给你收到的所有对象发送一个-release 。
關於retain,alloc,release,autorelease之間的關系Q&A
原贴:http://www.cocoachina.com/bbs/read.php?tid-7018-page-1.html
其實一直都沒有完全搞懂這些東西。。。。
Q1. 我看文檔說一個對象被retain之後它的count就加1,
那是不是說,比如我在頭文件裡定義一個NSString *a,然後retain這個對象,
那麼在.m裡面我如果執行了alloc的話是不是就要release兩次這個對象a?
Q2. 關於autorelease,文檔說是當離開這個被autorelease的對象的作用域時執行release。
那麼這個作用域是指這個對象被alloc時所在的域還是它定義的作用域?
解答问题:
A1. retain之后count的确加一。alloc之后count就是1,release就会调用dealloc销毁这个对象。
如果retain,的确需要release两次。通常在method中把参数赋给成员变量时需要retain。
例如:
ClassA有setName这个方法:
-(void)setName:(ClassName*) inputName
{
name = inputName;
[name retain]; //此处retian,等同于[inputName retain],count等于2
}
调用时:
ClassName *myName = [[ClassName alloc] init];
[classA setName:myName]; //retain count == 2
[myName release]; //retain count==1,在ClassA的dealloc中release name才能真正释放内存。
A2. autorelease更加tricky,而且很容易被它的名字迷惑。我在这里要强调一下:autorelease不是garbage collection,完全不同于Java或者.Net中的GC。
autorelease和作用域没有任何关系!
autorelease原理:
1.先建立一个autorelease pool
2.对象从这个autorelease pool里面生成。
3.对象生成之后调用autorelease函数,这个函数的作用仅仅是在autorelease pool中做个标记,让pool记得将来release一下这个对象。
4.程序结束时,pool本身也需要rerlease, 此时pool会把每一个标记为autorelease的对象release一次。如果某个对象此时retain count大于1,这个对象还是没有被销毁。
上面这个例子应该这样写:
ClassName *myName = 【[[ClassName alloc] init] autorelease];//标记为autorelease
[classA setName:myName]; //retain count == 2
[myName release]; //retain count==1,注意,在ClassA的dealloc中不能release name,否则release pool时会release这个retain count为0的对象,这是不对的。
记住一点:如果这个对象是你alloc或者new出来的,你就需要调用release。如果使用autorelease,那么仅在发生过retain的时候release一次(让retain count始终为1)。
《线程级的内存管理的疑惑》
在线程里面,比如:
NSString *flieName =[ [NSString alloc]initWithCString:"~/Desktop"];
如果要手动释放的话,应该可以采用:
[fileName release];
理论上可以释放掉,但是实际在调试的时候却没有释放。为什么?难道线程里面和主进程里面的内存管理有所区别吗?
引用计数的逻辑是别的地方如果也有引用的话,你在这里release只是减少一个引用,而不是释放,释放与否要看总的引用数量。
你如何判别这个东西没有被释放? 你只要再做一次 [filename release];出现错误 accessing object already deallocated 的话,就是已经被dealloc了。再不放心,可以做 [NSLog(@"filename count: %d", [filename retainCount]);
无需怀疑这个释放原则吧?
《NSURLConnection对retaiCount的副作用,release前注意NSURLConnection cancel》
alloc/release/retain/autorelease这四个东西对引用计数的作用大家应该都很清楚,
本来按照+1/-1配对的原则来使用应该也不会有什么问题,但是悲剧的我还是遇到了问题…
明明都是按照规则来的,可程序却一直崩溃直到最后连我自己也崩溃了
后来按照《线程级的内存管理的疑惑》提到的方法测试了一下引用计数才发现了问题
(虽然网上也有帖子说看retainCount这个东西不准确,有些是框架自己自动增加或减少引用计数引起的,
但是我觉得也许他反应的绝对数字没什么用,可至少用来监视一下retainCount的变化过程也是很有参考作用的)
我写的这个程序主要以tableViewController为主,每个cell都有一副图片,为了能够流畅滑动,这个图片的下载需要异步执行,
于是给每个cell建立一个Downloader类,Downloader类里的startDownload方法采用NSURLConnection异步下载,
Downloader实现NSURLConnection的delegate方法,tableViewController实现Downloader的delegate方法
问题就出在该死的NSURLConnection上,我在tableViewController中执行了一下[imageDownloader startDownload](imageDownloader是Downloader的实例),
没想到startDownload里面的[[NSURLConnection alloc] initWithRequest:aequest]居然导致了imageDownloader的引用计数增加了1,
只有当这个NSURLConnection被cancel的时候才会把imageDownloader的引用计数再减1,
开始一直没想到执行startDownload居然会增加imageDownloader的引用计数,又加上悲剧的我把NSURLConnection的cancel写在了Downloader的dealloc中了
于是悲剧真的就发生了…
每次tableViewController被dealloc的时候会执行[imageDownloader release],但是由于NSURLConnection给他加的1,导致imageDownloader的引用计数还不为0,
于是就不执行imageDownloader的dealloc,于是就不执行NSURLConnection的cancel,于是imageDownloader和NSURLConnection都存活下来了…
于是imageDownloader实现的NSURLConnection的delegate方法connectionDidFinishLoading就会被执行,
而connectionDidFinishLoading恰恰又会去执行tableViewController实现的Downloader的delegate方法,
而这时候tableViewController已经被dealloc了……于是找不到…
于是悲剧就真的发生了…
写了这么多又这么乱不知道大家看懂没,其实导致悲剧的代码就是下面这些
//ImageDownloader
- (void)dealloc {
[imageConnection cancel];
[imageConnection release];
[super dealloc];
}
- (void)startDownload{
self.activeDownload = [NSMutableData data];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:imageURL]delegate:self];
self.imageConnection = connection;
[connection release];
}
#pragma mark -
#pragma mark NSURLConnectionDelegate
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//tableViewController被dealloc后程序还是会走到这里
if (self.delegate && [self.delegate respondsToSelector:@selector(appImageDidLoad:)]) {
[self.delegate appImageDidLoad:self.indexPathInTableView];
}
}
//tableViewController
- (void)dealloc {
[allImageDownloader release];//这时候会把每个imageDownloader release一下,每个imageDownloader引用计数都变为1
[super dealloc];
}
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
imageDownloader = [[ImageDownloader alloc] init];//imageDownloader引用计数为1
[allImageDownloader addObject:imageDownloader];//imageDownloader引用计数为2
[imageDownloader startDownload];//imageDownloader引用计数为3!!!
[imageDownloader release];//imageDownloader引用计数为2
}
#pragma mark -
#pragma mark ImageDownloader
- (void)appImageDidLoad:(NSIndexPath *)indexPath {
[self.tableView reloadData];
}
也许是由于我经验太少了,不知道NSURLConnection对retaiCount的副作用,
下次一定注意在release采用了的NSURLConnection的类之前先把NSURLConnection cancel了
或者还有别的常用的正确的处理方法?如果有的话也希望大家我告诉我~
我的Twitter:@songwenhai 新浪微博:@Vincent_Soul 希望可以和大家交流