Objective-C快速上手
最近在开发iOS程序,这篇博文的内容是刚学习Objective-C时做的笔记,力图达到用最短的时间了解OC并使用OC。Objective-C是OS X 和 iOS平台上面的主要编程语言,它是C语言的超集,在C语言的基础上增加了面向对象的特性。
Note: 文中代码所使用的编辑工具是Xcode5
一些主要概念
-
类(Class) :一个应用是由大量的相互关联的类组成的:包括框架类库(如Cocoa、Cocoa Touch等)中的类,还有程序员自定定义的类。一个类文件包含两部分:
interface
( .h文件)和implementation
(.m文件),前者用来申明公开的属性和方法(严格地说,属性也是方法),后者实现前面声明的属性和方法。 -
类别(Categories):在不更改某个Class的代码情况下可以使用类别对该类的功能进行扩展。扩展的代码可以放在单独的文件或者放在被扩展类的implementation部分(如果可以看到的话,框架中的类如NSString,程序员是看不到其实现部分的)
-
协议(Protocols):协议类似于C#或Java语言的接口,用来声明一组相关的方法,这些方法可以被申明为可选的(
optional
)或是必须的 (required
),如果一个类遵循了一个协议,可选的方法不是必须要实现的。另外,协议可以继承于另一个协议(通常都是继承于NSObject
这个协议的)。 -
**块(Blocks) **:这个东西和匿名函数很类似
定义类
在.h文件中声明类的名称、属性和方法,放在.h中的属性和方法都是公开的,能够在其他的导入(import
)了该.h文件的类里使用。在.m文件中实现具体的代码,下面是一个类的定义:
- Person.h文件:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (strong, nonatomic)NSString *name;
@property NSInteger age;
- (void) work;
- (void) workFor:(NSString*)companyName;
- (void) buy:(NSString*)something from:(NSString*)store at:(NSDate*)time;
+ (NSString*) whoIsYourDad;
@end
- Person.m文件:
#import "Person.h"
@implementation Person
{
int instanceVar;
}
- (void) work
{
NSLog(@"work work");
}
- (void) workFor:(NSString*)companyName
{
NSLog(@"I am working for %@", companyName);
}
- (void) buy:(NSString*)something from:(NSString*)store at:(NSDate*)time
{
NSLog(@"I love shopping");
}
+ (NSString*) whoIsYourDad
{
return @"I like WARIII";
}
@end
其中,person.h文件中声明了类名、属性名称和方法名称。person.m文件中实现了具体的代码。
Note: #import是一个预处理指令,所谓预处理是指编译器编译代码前先要把源文件处理一次,比如把Foundation/Foundation.h的内容包括进来,这样才能做下一步的编译工作。当要包含SDK中的类库时,使用尖括号,如:
#import <Foundation/Foundation.h>
;当要包含项目中自己写的.h文件时用引号,如:#import "Person.h"
。用#include
也可以达到同样的目标,但是不建议使用,据说是后者会把同一个.h包含多次,而前者只包含一次。
类的声明
类的申明部分放在Person.h文件中,以@interface
开头,以@end
结尾
@interface Person : NSObject
//code
@end
Person是类的名称,NSObject
为父类名称,用冒号表示继承关系,Objective-C和C#、Java是一样的,不支持多继承。
实例变量
实例变量写在一对大括号内,如本例中的instanceVar
就是实例变量:
@implementation Person
{
int instanceVar;
}
Note:放置类变量的大括号谓语关键字
@implementation
或@interface
下方,即可以在.h文件中,也可以在.m文件中,具体有什么区别,我也没研究,知道的仁兄请赐教我吧。
类名有讲究
Objective-C没有命名空间或包的概念,可以通过在类名前面加上前缀以达到区分彼此的作用,如NSString
类的NS就是前缀,可能表示NeXTstep系统,熟悉乔布斯的人应该知道这段渊源。
属性
@property (strong, nonatomic)NSString *name;
@property NSInteger age
属性的声明语句写在关键字@interface
下面,@end
之前。关键字@property
用来申明属性,编译器会根据这个关键字自动生成一个实例变量_age以及相关的get和set方法。也可以手动完成这些工作,效果是一样的,需要注意的是:在调用get和set方法时,get方法名称和属性名称一样,不需要get前缀,但set方法需要写set前缀,不过通常也不用调用这个两个方法,直接点号.
后面加属性名称就行了。
如果属性的数据类型是基本类型,如本例中的age属性,属性前不用加上星号(当然,加上也不错,加上的话就表示是一个指针形的变量),否则如果数据类型是对象型的就必须加上星号。
strong
和nonatomic
关键字表明了属性的某些特征,其中 strong表示属性是强引用类型的,于strong
相对应的是weak
,表示弱应用类型。在Objective-C运行过程中,通过被一个被称为自动引用计数器(ARC)的技术手段监控,如果指向一个在堆内存中的对象的强引用数量为0时,系统自动释放这个对象所占据的内存,storng
和weak
关键字用来表示强引用和弱引用。此外还有一些常用修饰属性的关键字,列表如下:
关键字 | 描述 | 是否默认 |
---|---|---|
strong | 强引用 | 是 |
weak | 弱引用 | |
readonly | 只读 | |
readwrite | 可读可写 | 是 |
natomic | 具有原子性、线程安全的 | 是 |
nonatomic | 不具有原子性,非线程安全的,如果属性不会被多个线程访问,建议定义成nonatomic 类型,这样执行效率高 |
|
getter | 强行指定属性对应的getter 方法 |
|
setter | 强行指定属性对应的setter 方法 |
Note:如果指向一个对象的强引用的变量个数减少到0个时,系统自动收回它所占据的内存。如果一个变量不加
strong
或weak
修饰时,默认是strong型的。当出现如下情况时,需要使用weak
关键字,对象A强引用了对象B,这是对象B如果要引用对象A的话只能用弱引用,因为如果A和B互相强引用,那么这两个对象就永运不会被从内存中清除,这是不合理的。另外,局部变量默认是强引用类型,使用关键字__weak
可定义弱引用类型的局部变量。
实例方法
- (void) work;
- (void) workFor:(NSString*)companyName;
- (void) buy:(NSString*)something from:(NSString*)store at:(NSDate*)time;
实例方法的声明以-
开头,声明语句位于关键字@interface
和关键字@end
之间,和属性是一样的(属性本质上也是方法),以第二个方法为例:(void)
表示返回值,workFor
是方法的名称,(NSString*)
表示参数的类型,companyName
是参数,可以在方法体内访问。workFor
方法在.h文件中申明并且在.m文件中实现。
Objective-C的方法命名格式以一种接近自然语言形式定义,如上述最后一个那样,它的方法名称是 buy:from:at
,需要注意的是:冒号也是方法名称的组成部分。Objective-C不存在重载方法的概念,比如如下三个方法是完全独立的方法,而不是重载方法:
实例方法的实现在.m文件中,请参考上面的Person.m文件。
-(void) init;
-(void) init:(NSStrng*);
-(void) init:(NSString*) with(int)parm;
其中比较特殊的是init方法,该方法继承至NSObject
(NSObject
中包含了很多实用的方法,程序员自定义的类最好继承于它)。如果需要在初始化类的时候做一些额外的操作(比如给变量赋默认值),可以重写init
方法,或添加形如initXXX
之类的方法(约定俗成的命名方式)像如下这样:
- (id)init
{
self = [super init];
if (self) {
//code
}
return self;
}
- (id)initWithP:(id)p
{
self = [super init];
if (self) {
//code
}
return self;
}
类方法
+ (NSString*) whoIsYourDad;
类方法和实例方法的区别是前缀是+,而非-。同样声明语句位于关键字@interface
和关键字@end
之间,具体实现在.m文件中,请参考上面的Persion.m文件。
使用类
初始化类
由于所有的类都继承于NSObject
,可以使用NSObject
中的两个方法来初始化类,下面的表达式用来实例化一个Person
对象:
Person *person = [[Person alloc]init];
alloc
方法用来分配内存,init
方法用来做一些初始化操作,类似于于Java和C#的构造函数。
调用方法
很多资料里不叫“调用方法”而叫“发送消息”,这里统一叫“调用方法”。
调用方法的格式如下:
[类名 类方法名]; //方法没有参数
[类名 类方法名:参数...];//方法有参数
[实例名 实例方法名];//方法没有参数
[实例名 实例方法名:参数...];//方法有参数
Note:如果一个实例变量为空(
nil
),调用其方法不会报错,结果是do nothing。
属性是特殊的方法,所以也可以像上面那样调用,但是也可以像Java和C#那样使用点号.
访问,如下面两种写法是等价的:
person.age = 50;
[person setAge:50];
var = person.age;
var = [person age];
Note 推荐使用
.
调用属性,看上去一目了然,不管用那种方式,前后统一还是很重要的
类别
使用类别可以对已经存在的类的行为进行扩展,类似于C#中的扩展类,下面的例子用来给NSString
类增加方法:
- NSString+NSStringAddition.h文件:
#import <Foundation/Foundation.h>
@interface NSString (NSStringAddition)
-(BOOL)islongerThan:(NSString*)str;
@end
- NSString+NSStringAddition.m文件:
#import "NSString+NSStringAddition.h"
@implementation NSString (NSStringAddition)
-(BOOL)islongerThan:(NSString*)str
{
return [self length] > [str length];
}
@end
在main方法中测试:
#import <Foundation/Foundation.h>
#import "NSString+NSStringAddition.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
NSString *strS = @"abc";
NSString *strL = @"abcd";
BOOL result = [strL islongerThan:strS];
if (result)
{
NSLog(@"you are long");
}
}
return 0;
}
Note:大家在做测试的时候,用XCode创建项目,最后选择OS X->Applcation->Command Line Tool,这样运行的时候不会启动模拟器,速度快些。
另一种使用类别的方法是针对自己编写的可以看到源码的类进行扩展,在此情况下,无需新的.h和.m文件,只要在原来的类的 .m文件中开头部分加入扩展的方法,如扩展前面的Person类:
#import "Person.h"
@interface Person()
@property NSString* privatePro;
@end
@implementation Person
{
int instanceVar;
}
- (void) work
{
NSLog(@"work work");
}
- (void) workFor:(NSString*)companyName
{
NSLog(@"I am working for %@", companyName);
}
- (void) buy:(NSString*)something from:(NSString*)store at:(NSDate*)time
{
NSLog(@"I love shopping");
}
+ (NSString*) whoIsYourDad
{
return @"I like WARIII";
}
@end
此例中增加了一个属性:privatePro。也可以增加方法,在“@implementation Person”和“@end”之间实现即可,和其他方法一样。在iOS开发中,ViewController
通常使用这种扩展方法,将不对外公开的控件声明在扩展部分。
协议
协议相当于C#、Java中的接口。协议的声明在.h文件中,现声明一个协议如下:
#import <Foundation/Foundation.h>
@protocol FlyProtocal <NSObject>
@required
- (void)fly;
- (void)flyTo:(NSObject*)space;
@optional
+ (void)canGodFly;
@end
其中,@required
下面的方法是遵循协议的类必须实现的,@optional
下面的方法是可选的。可以看到,canGodFly
方法是类方法,协议里不仅可以申明实例方法,类方法也是可以的。下面编写一个遵循协议FlyProtocal
的Bird
类:
- Bird.h
#import <Foundation/Foundation.h>
#import "FlyProtocal.h"
@interface Bird : NSObject <FlyProtocal>
@end
- Bird.m
#import "Bird.h"
@implementation Bird
- (void)fly
{
NSLog(@"I can fly");
}
- (void)flyTo:(NSObject*)space
{
NSLog(@"fly to %@", space);
}
+ (void)canGodFly
{
NSLog(@"God can fly");
}
@end
观察Bird.h文件,在类名后加<协议名>
即表示类遵循协议。观察Bird.m,三个接口中定义的方法已经被实现,其中fly
和flyTo
是必须实现的,canGodFly
是可选的。
下面编写测试类ProtocalTest:
- ProtocalTest.h
#import <Foundation/Foundation.h>
#import "FlyProtocal.h"
@interface ProtocalTest : NSObject
@property id<FlyProtocal> delegate;
-(void)testFly;
@end
- ProtocalTest.m
#import "ProtocalTest.h"
@implementation ProtocalTest
- (void)testFly
{
[self.delegate fly];
[self.delegate flyTo:@"BeiJing"];
}
@end
在ProtocalTest中定义了一个属性:@property id<FlyProtocal> delegate;
,该属性可以赋值一个遵循了协议FlyProtocal
的类的实例。testFly
方法试图调用属性delegate
所指向的对象的fly
和flyTo
方法。现编辑main方法如下:
#import <Foundation/Foundation.h>
#import "ProtocalTest.h"
#import "Bird.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
ProtocalTest *test = [[ProtocalTest alloc]init];
test.delegate = [[Bird alloc]init];
[test testFly];
}
return 0;
}
和C#、Java的不同是,协议不可以当成一个数据类型,上面的例子中 @property id<FlyProtocal> delegate;
不可以写为:@property FlyProtocal delegate;
。
在实际开发中,很多控件的事件和数据源都是用协议来实现,某个ViewController如欲处理一个控件的某个事件,它可能要实现某个指定的协议里的方法,控件会回调这些方法。
Note: id 类型的变量可以指向任何对象,它本身已经是一个指针,因此无需加上
*
块
块类似于C#、Java中的匿名函数,不用块使用其他旧的方法一样可以达到相同的效果,和C#、Java一样,块的使用可以使代码更紧凑、便于阅读,显得高大上。
定义块
-(void)method
{
NSInteger methodVar = 1;
^{
//code
};
NSInteger (^myBlock)(NSInteger, NSInteger) = ^(NSInteger p1, NSInteger p2)
{
//code
return methodVar + p1 + p2;
};
}
如例子所示,块的定义是这样的格式:
^(参数类型 参数名称, 参数类型 参数名称...){
代码
};
申明一个块类型的变量是这样的格式:
返回值类型(^变量名称)(参数类型, 参数类型...);
块类型的变量即可申明为方法内部的局部变量,也可以声明为类变量。
块代码体内能够使用包含块的方法内部的变量,如本例子中块中使用了局部变量methodVar
。需注意的是块中代码体只能使用methodVar
,而不能改变methodVar
的值,要想改变局部变量的值,需要在声明局部变量的时候加上关键字__block
。
使用块
块可以作为方法的参数传递,以汽车为例,汽车在启动前和启动后要加入一些行为,这个行为由驾驶员决定,如下为代码实例:
- Car.h
#import <Foundation/Foundation.h>
@interface Car : NSObject
-(void)runWithAction:(void(^)(void))beforeRunBlock afterRun:(void(^)(void))afterRunBlock;
@end
- Car.m
#import "Car.h"
@implementation Car
-(void)runWithAction:(void(^)(void))beforeRunBlock afterRun:(void(^)(void))afterRunBlock
{
beforeRunBlock();
NSLog(@"run");
afterRunBlock();
}
@end
块作为方法参数时格式和声明一个块类型的变量类似,只是少了变量名称
Note: 如果参数传来的是空(nil),执行 beforeRunBlock()会导致程序崩溃。
写一个Dirvier类如下:
- Dirver.h
#import <Foundation/Foundation.h>
@interface Dirver : NSObject
- (void)drive;
@end
- Dirver.m
#import "Dirver.h"
#import "Car.h"
@implementation Dirver
- (void)drive
{
Car *car = [[Car alloc]init];
void (^beforeRun)(void) = ^{
NSLog(@"check tire");
};
void (^afterRun)(void) = ^{
NSLog(@"close door");
};
[car runWithAction:beforeRun afterRun:afterRun];
}
@end
main方法:
#import <Foundation/Foundation.h>
#import "Dirver.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
Dirver *dirver = [[Dirver alloc]init];
[dirver drive];
}
return 0;
}
运行结果如下:
*2014-03-21 13:57:48.037 BlockTest[1211:303] check tire *
*2014-03-21 13:57:48.039 BlockTest[1211:303] run *
2014-03-21 13:57:48.040 BlockTest[1211:303] close door
Driver类中可以不定义块变量,以一种类似于匿名方法的书写格式书写:
#import "Dirver.h"
#import "Car.h"
@implementation Dirver
- (void)drive
{
Car *car = [[Car alloc]init];
[car runWithAction:^{
NSLog(@"check tire");
} afterRun:^{
NSLog(@"close door");
}];
}
@end
回调
处理事件离不开回调,下面介绍Objective-C的三种回调模式。
arget-action模式
target指某个对象,action指对象的行为,即某个方法,合起来就是回调某个对象的某个方法,关键字@selector用来得到方法名称的唯一标识,记作SEL。请看例子。
- Car.h
#import <Foundation/Foundation.h>
@interface Car : NSObject
-(void) runWithAciton:(id)target selector:(SEL)oneSelector;
@end
- Car.h
#import "Car.h"
@implementation Car
-(void) runWithAciton:(id)target selector:(SEL)oneSelector;
{
if ([target respondsToSelector:oneSelector])
{
[target performSelector:oneSelector];
}
NSLog(@"run");
}
@end
场景:调用汽车跑起来前,要做一些额外的工作,这个工作将通过回调对象target中的方法oneSelector来完成。下面的代码将指定target和action:
- test.h
#import <Foundation/Foundation.h>
@interface Test : NSObject
- (void)driver;
@end
- test.m
#import "Test.h"
#import "Car.h"
@implementation Test
- (void)driver
{
Car *car = [[Car alloc]init];
[car runWithAciton:self selector:@selector(doSomeThing)];
}
- (void)doSomeThing
{
NSLog(@"check tire before running");
}
@end
在main函数中测试:
#import <Foundation/Foundation.h>
#import "Test.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
Test *test = [[Test alloc]init];
[test driver];
}
return 0;
}
结果是:
2014-03-20 14:28:16.309 TELTest[1531:303] check tire before running
2014-03-20 14:28:16.311 TELTest[1531:303] run
协议模式
场景:在汽车跑起来前已经跑起来后需要做一些额外的工作,并且在某些情况下可以制止汽车跑起来(比如前方睡着一只猫的时候),下面将这三个行为封装在一个协议中:
#import <Foundation/Foundation.h>
@protocol CarDelegate <NSObject>
-(BOOL)shouldCarRun;
-(void)didBeforeCarRun;
-(void)didAfterCarRun;
@end
在Car类中,增加代码如下:
- Car.h
#import <Foundation/Foundation.h>
#import "CarDelegate.h"
@interface Car : NSObject
@property id<CarDelegate> delegate;
-(void) run;
-(void) runWithAciton:(id)target selector:(SEL)oneSelector;
@end
- Car.m
#import "Car.h"
@implementation Car
-(void) runWithAciton:(id)target selector:(SEL)oneSelector;
{
if ([target respondsToSelector:oneSelector])
{
[target performSelector:oneSelector];
}
NSLog(@"run");
}
-(void) run
{
if (![self.delegate shouldCarRun])
{
return;
}
[self.delegate didBeforeCarRun];
NSLog(@"run");
[self.delegate didAfterCarRun];
}
@end
修改Test类,使其遵循协议CarDelegate
:
- Test.h
#import <Foundation/Foundation.h>
#import "CarDelegate.h"
@interface Test : NSObject<CarDelegate>
- (void)driver;
@end
- Test.m
#import "Test.h"
#import "Car.h"
@implementation Test
- (void)driver
{
Car *car = [[Car alloc]init];
car.delegate = self;
[car run];
}
-(BOOL)shouldCarRun
{
//NSLog(@"the cat is sleeping");
//return NO;
return YES;
}
-(void)didBeforeCarRun
{
NSLog(@"check tire before running");
}
-(void)didAfterCarRun
{
NSLog(@"close the door");
}
@end
再次运行main函数:
2014-03-20 15:08:03.517 TELTest[1709:303] check tire before running
2014-03-20 15:08:03.520 TELTest[1709:303] run
2014-03-20 15:08:03.521 TELTest[1709:303] close the door
消息中心模式
注册观察者到消息中心,消息中心发送消息给注册者,并回调注册者的方法。下面是一个观察者的例子:
- Observer.h
#import <Foundation/Foundation.h>
@interface Observer : NSObject
@end
- Observer.m
#import "Observer.h"
@implementation Observer
-(id)init
{
self = [super init];
if (self)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doSomething:) name:@"TestNotification" object:nil];
}
return self;
}
- (void)doSomething:(NSNotification*) aNotification
{
NSString *message = (NSString*)aNotification.object;
NSLog(@"the message is %@", message);
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
在main方法中编写测试代码:
#import <Foundation/Foundation.h>
#import "Observer.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
__unused Observer *observer = [[Observer alloc]init];
[[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotification" object:@"hello world"];
}
return 0;
}
运行结果如下:
2014-03-20 16:12:32.017 ObserverTest[1888:303] the message is hello world
如何选择
- 如果是简单的一个动作,选择Target-action模式,如点击一个按钮所引发的动作
- 如果是多个动作,使用协议模式,如给UIViewTable绑定数据,设置表格样式
- 如果是多个类需要和与之弱耦合的一个类交换信息,考虑使用消息中心模式
全文完
后记
本文章是我学习OC时的笔记,旨在快速上手,并没有深入学习,如有错误之处,还请指教。这是我首次用Markdown语法在博客园撰写文章,总体感觉不错,希望能加入流程图、序列图