Mac开发-从啥都不懂到好像入门

个人觉得,在OS X上写应用程序其实不怎么好入门(也可能是我太笨),整个学习曲线非常怪异。首先你得掌握Objective-C或者Swift语言,其次你还得熟悉各种API。OS X开发的整个模式与在Windows下开发很不同,而且你还不能一知半解,因为很多东西是互相关联的。

在下面简要记录一下最近学习过程中的一些心得。

Objective-C

首先要讲的就是Objective-C语言。虽然现在(2017年中)Swift语言已经日趋成熟,并且受到苹果官方的大力推崇,但囿于历史原因,大部分项目和库还是基于Objective-C的。因此对Objective-C语言本身的熟悉还是很重要的。

关于Objective-C的基础语法的学习,苹果官方给出了非常详尽的文档,可以在Mac Developer Library找到。这里推荐Programming with Objective-C来学习基本语法。

当然如果你和我一样更喜欢看书的话,推荐《Objective-C程序设计(Programming in Objective-C Sixth Edition)》和《Objective-C编程全解》两本书。

下面记录一些重点内容:

Objective-C是C语言的严格超集,但在类的表示与定义上与C++有很大区别。Objective-C中所有的类的实例变量都是以指针形式存储的,在运行时动态分配内存。

也正是因此,比较两个类的实例时不能用==比较,而应该用isEqual:方法。一个子类的指针可以被赋给一个父类的指针。

Objective-C使用引用计数进行垃圾回收,现在则使用ARC进行自动垃圾回收,不需要手动管理内存。

属性(property)

属性有点像类的实例变量,区别在于属性被用来向类外暴露数据。我们应该严格遵循这样的定义原则,即将需要暴露的数据定义为属性。

对于属性变量,编译器会自动生成属性对应的get方法,对于可修改的属性还会生成set方法,可以通过@synthesize来定义。如果不显式设置@synthesize,则编译器会隐式的生成下划线开头的成员变量。

property常用的属性修饰符有:

  • weak(assgin):不增加引用计数,用于基本数据类型
  • strong(retain):增加引用计数,用于实例变量
  • copy:深拷贝赋值。

属性的默认权限为readwrite(可读写),使用readonly定义只读属性。

常见的使用场景有,例如需要存储一个NSString *类型的字符串,传入了一个NSMutableString *的参数。如果不使用copy赋值,则会直接将该属性指向可变字符串,这样这个属性值就会随着NSMutableString *的变化而变化。

协议(protocol)

协议类似于Java中的接口,其实就是一系列的成员方法。一个类遵从一个协议,则需要实现协议中指定的一些方法(使用@optional定义不必须实现的方法)。

分类与类扩展

分类(Category)就是对一个类的拓展,可以在不修改类的声明的情况下向类增加成员函数。

类扩展则是在对应的.m文件中的匿名分类,但可以增加属性。类扩展主要用于,在.h文件中声明一个readonly属性时,如果需要在.m文件中对其赋值,可以在.m中声明class extension,再次声明一个readwrite的同名属性。

Key-Value Coding(KVC)与Key-Value Observing(KVO)

KVC支持在运行时通过一个字符串来访问或设置属性。假设有一个类对象myObject有一个readwrite的属性name,那么可以通过[myObject valueForKey:@"name"]来访问,通过[myObject setValue:@"xxxxx" forKey:@"name"]来赋值。

KVO则可以通过KVC,在一个属性发生改变时向另一个类对象发出消息。如果有另一个对象observerObject想要观测myObjectname属性的变化,则可以通过addObserver:forKeyPath:options:context:方法设置。而这个对象自己需要定义observeValueForKeyPath:ofObject:change:context:来接受观测值被改变的消息。

KVC和KVO体现了Objective-C语言的动态特性,同时也是后面很多设计方法的基础。

Cocoa架构

这里讲的架构主要是Cocoa。这里有很多概念都互相关联。一个建议是开始时先不要理会Interface Builder,可以考虑直接在代码中创建控件(可能会比较容易上手?)。Interface Builder在一定程度上反而会复杂化问题。

尤其是Xcode 8开始默认使用storyboard,我反正花了很久才把两套机制大概弄懂 :-(

代理(delegate)

代理对象是一个类对象,通常其类必须遵从某一特定的协议。代理对象决定了类对象的一些行为。例如通过App Delegate代理NSApplication的实例;NSUserNotificationCenter(通知中心)的代理可以决定一个通知是否该被发出,并在通知被激活的时候进行处理。

控制器(controller)

可以对一个控件设置其控制器。控制器也是决定控件的行为,比如NSView的控制器可以控制控件的绘制、在控件被显示时进行初始化、处理视图上其他空间触发的事件。

在我的理解中,控制器和代理的不同大概在于,控制器完全就是用来控制控件的,而代理通常只是“兼任”。被控制器所控制的控件应该是控制器的strong属性。

Model-View-Controller(MVC)模型

这是苹果提出的一个程序设计范例。即把内在逻辑(Model层)与外部展示(View层)分开,并使用控制器(Controller)在两者间通信。MVC模型相信大家都很熟悉,就不再赘述了。

事件处理

Objective-C的事件分为两类:eventaction。这一部分内容比较复杂,我也没看太懂,只懂很简单的用法。详细参见文档Cocoa Event Handling Guide

Event

Event事件是由输入设备触发的,如按键或者鼠标点击。一个鼠标event会首先传递给被点击的对象,如果这一对象不对其做出响应,则传递给响应链(responder chain)上的下一个对象。一个键盘event则会首先传给first responder(大概可以理解为当前的焦点)。

默认情况下,一个对象在响应链上的下一个对象是其父对象。如果一个对象要对event做出响应,则这一对象需要定义对应的方法。比如鼠标右击的方法是- (void)rightMouseDown:(NSEvent *)event。举个例子,假设用户右击了一个按钮,如果按钮没有定义右击方法,则事件传递给包含按钮的视图;再下去会传递给窗体,再下去会是窗体的控制器。

个人感觉这种通过定义方法来响应的事件处理方式,有点像遵从协议。

Action

Action通常是程序内部传递的动作。类对象可以设置targetaction。当它们被触发时,会调用对应的target对象的action方法。

这里的action是一个方法的选择器(@selector,其实就是函数指针),这个方法接受一个id类型(即任意类型的指针)的参数。target也可以为nil,这时action会在响应链上传递。例如对一个NSButton设定target-action,则其会在mouse up的时机触发action。

Action的响应链有别于Event的响应链,而且十分复杂,我也不是很懂。请参见文档。

Interface Builder

Interface Builder是Xcode中创建界面的图形化工具。其实光靠这个是满足不了需求的,因为更多的时候我们会用到动态的界面,比如我们会改变文本框的文字,修改窗口大小,在菜单中增加删除项目等。

关于IB两个最蛋疼的概念就是File's OwnerFirst ResponderFile's Owner通常被设定为控件的控制器的类,关于First Responder可以参见上面提到的文档。

一些重要的点

Objective-C中的空值

Objective-C中有几种空值的表示方式,分别为nilNULLNilNSNull。它们的具体用法和区别如下:

nil

nil的定义是null pointer to Objective-C object,指的是一个OC对象指针为空,本质就是(id)0,是Objective-C对象的字面0值。

这里需要注意的是,Objective-C中给空指针发消息并不会崩溃,原因是Objective-C中的函数调用都是通过objc_msgSend进行消息发送来实现的。相对于C和C++来说,对于空指针的操作会引起Crash的问题,而objc_msgSend会通过判断self来决定是否发送消息,如果selfnil,那么selector也会为空,这时消息将直接返回。

这里补充一点,如果一个对象已经被释放了,那么这个时候再去调用方法肯定是会Crash的,因为这个时候这个对象就是一个野指针了。安全的做法是释放后将对象重新置为nil,使它成为一个空指针。

1
2
3
4
5
6
7
8
9
10
11
12
NSString *name = @"Allen";
if(name != nil && [name isEqualToString:@"Allen"]) {
NSLog(@"name: %@", name);
} else {
NSLog(@"name is nil");
}
//or
if([name isEqualToString:@"Allen"]) {
NSLog(@"name: %@", name);
} else {
NSLog(@"name is nil");
}

上面的两种判断都是正确的,我们不必担心当name为nil时调用isEqualToString会出现Crash。但我还是想说,在使用一个对象之前判断它是否为nil是一个很好的习惯,个人觉得有两个原因:

  1. 极大减少向空对象传递消息的开销。如果你增加了nil的判断,那么不需要对空指针发送消息了,发消息其实是件费时的操作。
  2. 允许向空指针传递消息属于Objective-C的语言特性,更准确的说应当是一种fail-safe机制。我们应当养成使用变量前进行判空的好习惯。

NULL

NULL的定义是null pointer to primitive type or absence of data,指的是一般的基础数据类型为空,可以给任意的指针赋值。本质就是(void *)0,是C指针的字面0值。

1
2
3
4
NSInteger *pointerA = NULL;
NSInteger pointerB = 10;
pointerA = &pointerB;
NSLog(@"%ld", *pointerA);

我们应当尽量避免使用NULL初始化Objective-C对象,可能会产生一些异常的错误。对Objective-C对象的初始化应当使用nilNULL主要针对基础数据类型。

Nil

Nil的定义是null pointer to Objective-C class,指的是一个类指针为空。本质就是(class)0,Objective-C类的字面零值。

1
2
3
4
Class class = [NSString class];
if(class != Nil) {
NSLog(@"class name: %@", class);
}

NSNull

NSNull好像没有什么具体的定义(懵),它包含了唯一一个方法+(NSNull*)null[NSNull null]是一个对象,是用来表示零值的对象。

NSNull主要用在不能使用nil的场景下,比如NSMutableArray是以nil作为数组结尾判断的,所以如果想插入一个空的对象就不能使用nil,NSMutableDictionary也是类似,我们不能使用nil作为一个object,而要使用NSNull

1
2
3
4
5
6
7
8
NSMutableArray *array = [[NSMutableArray alloc] init];
NSString *nameOne = @"Allen";
NSString *nameTwo = [NSNull null];
NSString *nameThree = @"Tom";
[array addObject:nameOne];
[array addObject:nameTwo];
[array addObject:nameThree];
NSLog(@"names : %@", array);

id与instancetype

instancetype是clang 3.5以后提供的一个关键字,表示某个方法返回的未知类型的Objective-C对象。我们都知道未知类型的的对象可以用id关键字表示,那么二者到底有什么区别呢?

这就要说到关联返回类型(related result types)的概念。根据Cocoa的命名规则,满足下述规则的方法会返回一个方法所在类类型的对象:

  1. 类方法中,以alloc或new开头
  2. 实例方法中,以autorelease,init,retain或self开头

这些方法被称为是关联返回类型的方法。换句话说,这些方法的返回结果以方法所在的类为类型。看下面的例子:

1
2
3
4
5
6
7
@interface NSObject
+ (id)alloc;
- (id)init;
@end
@interface NSArray : NSObject
@end

当我们使用如下方式初始化NSArray时:

1
NSArray *array = [[NSArray alloc] init];

按照Cocoa的命名规则,语句[NSArray alloc]的类型就是NSArray*,因为alloc的返回类型属于关联返回类型。同样,[[NSArray alloc] init]的返回结果也是NSArray*

而如果一个不是关联返回类型的方法,如:

1
[NSArray array];

其返回类型就是id

instancetype类型只允许用在关联返回类型的方法,能够帮助编译器理解方法的返回类型。如

1
[[[NSArray alloc] init] mediaPlaybackAllowsAirPlay];

由于[[NSArray alloc] init]的结果是NSArray*,这样编译器就能够根据返回的数据类型检测出NSArray是否实现mediaPlaybackAllowsAirPlay方法。

而下列方法:

1
[[NSArray array] mediaPlaybackAllowsAirPlay];

由于array不属于关联返回类型方法,[NSArray array]返回的是id类型,编译器不能对id类型的对象是否实现了mediaPlaybackAllowsAirPlay方法进行检查,只能在运行时进行检查。

除此之外,instancetype只能作为方法的返回值类型,不能像id那样作为参数类型。

self与this

说起selfthis,很多人可能觉得是一样的,但其实两者还是有很一些区别的。

this是 C++语言的关键字,而self只是Objective-C中的一个特殊变量名。this是永远指向当前对象的指针,而self的值是可以被修改的。

在构造器中,当构造失败时,可以使用self = nil标记初始化失败。当子类在调用self = [super init]时,如果发现selfnil时,就不必继续做自己的构造了。另外,可以定义变量名为self,但this是不可以的(this是保留关键字)。

应当明确的是,Objective-C的self未必和this是等价的,即self未必总是指向当前对象,在使用时要格外注意。

那么什么情况下应当使用self,什么情况下不使用self呢?答案是:使用了self.xxx方法,其原理一定会调用[self getXXX][self setXXX]方法(看是lvalue还是rvalue)。例如:

1
self.mainViewController = aController;

等价于

1
[self setMainViewController: aController];

即通过setter函数来改变mainViewController指针的值时,可能会涉及到诸如

releasing old objects, retaining new ones, updating internal variables

的操作。而

1
_mainViewController = aController;

只是简单的直接改变mainViewController指针的值,不经由setter方法。

对应的,

1
2
objc
mainViewController.view.frame = [UIScreen mainScreen].applicationFrame;

会被翻译成

1
2
3
4
5
6
7
8
9
10
11
[[mainViewController view] setFrame:[[UIScreen mainScreen] applicationFrame]];
```
建议的做法是尽量使用显示调用set方法的方式,通过对set方式进行赋值可以实现变量的懒加载。同时如果使用的是拷贝赋值方式,编译器也可以很好的通过set方法进行对应的深拷贝。而如果不使用set方法,就无法自动完成深拷贝赋值。
同时需要注意的是,由于通过self访问会隐式调用set方法,因此在set方法中使用self会造成循环引用:
```objc
- (void)setName: (NSString *)name {
_name = name; // OK
self.name = name; // 循环引用
}

类属性与成员变量

  1. 类的成员变量仅在本类中可以访问,子类无法通过_XXX的形式访问。但是可以通过继承父类的get/set方法访问;

  2. 当声明类的属性后,编译器会自动生成对应的存取方法。我们也可以通过重写的方式自定义get/set方法;

  3. 注意,当同时重写set/get方法时,系统不会自动生成属性实例对象。