Skip to content

aJIEw/HeadFirstObjectiveC

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

一个安卓开发者的 Objective-C 学习笔记。1

HeadFirstObjectiveC

Objective-C 主要用于开发 macOS/iOS/iPadOS 等平台上的应用,看它的名字就可以猜到,这是一门基于 C 的面向对象的编程语言。

程序入口

与大多数编程语言类似,OC 的程序运行入口也是 main 方法:

// 导入依赖,因为使用了 NSLog
#import <Foundation/Foundation.h>

// main 方法的返回值通常是 int
// main 方法参数分别是参数个数和参数数组,如果不需要也可以不写
// int main() {
int main(int argc, const char * argv[]) {
  // 为了和 C 的字符串区分,声明 NSString 时,需要在字符串前加 @
  // 另外,在创建一些字面量 literals 时,也需要使用 @
  NSLog(@"Hello World!");
}

我们可以使用 clanggcc 编译 OC 源文件:

clang main.m file1.m file2.m -w -framework Foundation -o main

运行编译后生成的二进制文件(-t 表示命令行参数):

./main -t arg1 arg2

数据类型

Primitive types

Objective-C 是一门基于 C 的语言,所以为了兼容 C,它做了很多改进。我们可以像 C 中一样声明原始数据类型,也可以声明 Objective-C 中独有的支持作为对象使用的数据类型。

int primitiveInt  = 1;
long primitiveLong = 1;
float primitiveFloat = 1.0f;
double primitiveDouble = 1.0;
char charA = 'A';

Objective 中布尔类型字面量为数字 0 和 1:

BOOL noBool  = NO;
BOOL yesBool = YES;
NSLog(@"%d %d", noBool, yesBool);

另外,与 C 中一样,所有的整形数字分为 signedunsigned 类型,unsigned 类型数据只能代表非负数。以 unsigned int 为例,同样 16 位的情况下,它的取值范围是 0 ~ 65535,而普通 int 的取值范围则是 -32767 ~ 32767。

Objects

除了基本数据类型之外,其它所有的数据类型都是作为对象被创建的。

MyClass *myObject1 = nil;  // Strong typing
id       myObject2 = nil;  // Weak typing

上面的例子中,MyClass 是我们定义的一个类,创建它时在对象名称前加 * 表示这是一个强类型对象。我们也可以使用 id 声明一个对象,它表示这是一个弱类型的对象,它可以代表任何类的对象。

id var = nil;
if (var) {
  NSLog(@"This is %p", var);
} else {
  NSLog(@"var is empty");
}

上面的例子中,我们声明一个 var 变量,并且初始化值为 nil,表示空值,此时使用 if 判断将会为 false,我们可以尝试将它修改成 1 或者 YES 再运行看看。

NSString

Objective-C 中的字符串用 NSString 表示,由于它是对象,所以创建时也需要使用指针。

NSString *str = @"Objective-C";
NSLog(str);

// 可变字符串
NSMutableString *mutableString = [NSMutableString stringWithString:@"Hello"];
[mutableString appendString:@" World!"];
NSLog(mutableString);
NSNumber

我们可以使用 NSNumber 创建数字类型的对象,与创建字符串类似,同样需要使用 @ 符号。

// 创建一个长整型数字类型的对象
NSNumber *fortyTwoLongNumber = @42L;
// 转换成 long
long fortyTwoLong = [fortyTwoLongNumber longValue];
// 打印
NSLog(@"%li", fortyTwoLong);
NSArray

数组可以包含不同类型的数据,但是必须是对象。

NSArray *anArray = @[@1, @2L, @3.0F, @4U, @"5"];

NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:2];
[mutableArray addObject:@"Hello"];
[mutableArray addObject:@"World"];
[mutableArray removeObjectAtIndex:0];
NSLog(@"%@", [mutableArray objectAtIndex:0]);
NSDictionary

字典,类似于其它编程语言中的 Map 的数据类型。

NSDictionary<NSString *, NSObject *> *dictionary = 
    @{ @"name" : @"Objective-C", @"birth" : @1992 };
NSObject *dName = dictionary[@"name"];
NSLog(@"dictionary = %@", [dictionary description]);
NSSet

集合对象。

// 创建一个可变集合对象
NSMutableSet<NSString *> *set = [NSMutableSet setWithObjects:@"Hi", nil];
[set addObject:@"there"];
// 空值不会被加入
NSLog(@"set = %@", set);

Objective-C 虽然也是面向对象的语言,但是如果你像我一样之前没有接触过 C,那么第一点感到不适应的地方可能会是:为什么所有的类都要分别创建一个 .h 和一个 .m 文件呢?为什么要在 .h 文件中定义所有的属性和方法,然后再在 .m 文件中去实现呢?

其实这主要是因为过去的编程范式遗留下来的习惯,为了最大程度地复用代码,同时也保证了代码的清晰度,而且编译器也能根据 .h 文件来决定哪些属性和方法会被编译到最终生成的可执行文件中。2

如下所示,我们创建了一个 person.hperson.m 文件,其中定义和实现了 Person 类:

// 所有类都继承自 NSObject
@interface Person : NSObject
  // 属性
  @property (assign) NSString *name;
  // 方法,+ 表示类方法(类似静态方法),- 表示实例方法
  - (void)sayHi:(NSString *)greeting;
@end

定义完 .h 文件之后再在 person.m 中实现:

// 首先需要导入头文件
#import "person.h"

@implementation Person {
  // 实例变量
  NSString *_nickName;
  // 私有实例变量
  @private int _age;
}

- (void)sayHi:(NSString *)greeting {
  // 方法体
}

@end

Properties

类的属性,也就是可以被外界直接访问类中的变量,和实例变量最大的区别是,实例变量只能在当前类中的构造器或实例方法中才能被访问。

// 编译器会为属性生成一个 setter 方法,这里的话就是 setName 方法
@property NSString *name;

// 我们也可以自定义 getter 和 setter
@property (getter = ageGet, setter = ageSet: ) int age;
Attributes

我们还可以为类的属性设置其它修饰符 (attribute),上面例子中的自定义 setter 和 getter 其实也是属性的修饰符,其它这样的修饰符还有:

  • readonly: 表示只读,不会生成 setter 方法,默认为 readwrite
  • copy: 复制属性的值,也就是使用强引用,即使引用值发生了修改也不会影响属性本身的值。
  • nonatomic: 非原子性(线程不安全)。默认所有的属性都是原子性的,也就是支持跨进程赋值。
  • unsafe_unretained: 等同于 weak,也就是弱引用,当没有强引用的时候即释放它。主要用于防止多个对象之间的循环引用。

除了 readonly 之外,默认的 attribute 还有:

  • assign: 赋值,告诉编译期生成 setter 方法。
  • retain: 等同于 strong,也就是强引用,保持引用直到所有对象都释放了它,旧值被释放新值被赋值。
  • atomic: 保持原子性,只有单个线程能访问它,线程安全但是效率更低。
synthesize

属性背后其实也依赖于实例属性,它会自动生成为我们生产一个 _perpertyName 的幕后属性 (backing field),假如我们想要在实现类中使用不一样的名字,可以使用 @synthesize 自定义:

@implementation Person

@synthesize name = instanceVariableName;

@end

定义完属性之后,在实现类中有下面几种方式去访问它:

- (void)changeName {
  NSString *var = @"Objective-C";
  // 通过幕后属性访问
  // _name = var;
  
  // 使用 @synthesize 替代幕后属性
  // instanceVariableName = var;
  
  // 通过 setter
  [self setName:var];
  
  // 还可以使用 self. 语法
  self.name = var;
}
实例变量

我们可以把实例变量看作其他语言中类的私有属性,而且通常我们只会在实现类中定义实例变量。

@implementation MyClass {
  NSString *_nickName;
}

之后,我们可以在构造器方法中去初始化它。

Methods

前面提到过,Objective-C 中的方法有两种,一种是类方法,一种是实例方法,分别使用方法前面的 +- 表示。除此之外,实现类中的方法通常由这几个部分组成:方法类型+返回值+方法名+可选的方法参数+方法体。

- (void)myMethod:(int)arg1, name:(NSString *)arg2 {
  // do something
}

可以看到,和其它编程语言不同的地方在于,方法的参数名和参数值是分开表示的,而且参数名也是方法的一部分。因此,整个方法的方法名是:

myMethod:name:

当我们调用一个方法的时候,需要用中括号将对象引用和方法括起来,如果方法有多个参数,还需要将参数名称一一列出,中间使用空格分割,并在冒号后表示实际参数:

[myClass myMethod:42 name:@"arg2"];

需要注意的是,Objective-C 中还有一个 Message 的概念,当我们用以上的形式调用一个方法时,本质上是发送了一个消息给对象实例,编译器会将所有的方法都编译成下面这个形式:

id objc_msgSend(id self, SEL op, ...);

上面的 objc_msgSend 是用于发送消息的方法。也就是说,当我们调用 myMethod 的时候,本质上是调用的是:

objc_msgSend(myClass, @selector(myMethod:name:), 42, @"arg2");

关于 selector 的用法下面会再具体介绍。

Constructors

构造器是一类特殊的方法,返回值通常是 id,我们可以在其中做一些类的初始化工作。

默认的构造器是 init

- (id)init {
  self = [super init];
  // 判断父类是否初始化成功,如果初始化失败将会返回 nil
  if (self) {
    _nickName = @"default";
  }
  return self;
}

自定义构造器:

- (id)initWithNickname:(NSString *)nickName {
  self = [super init];
  if (self) {
    _nickName = nickName;
  }
  return self;
}
Selectors

Selector 是在对象上执行的方法的名称,编译器会将所有的方法都编译成 selector 来执行。当我们使用 selector 的时候,通常是为了实现动态分配执行方法。

Selector 用 SEL 表示,一般有两种方式创建:

SEL sel = @selector(methodName);

当我们不知道方法名时,可以通过运行时方法 NSSelectorFromString 创建 selector:

SEL sel = NSSelectorFromString(dynamicMethodsString);

在使用 selector 之前,通常需要先进行判断,确保方法可以执行:

if ([myClass respondsToSelector:sel]) {
  // 调用方法
  [myClass performSelector:selectorVar];
}

除了以上这种调用方法之外,我们还可以使用 block 动态调用方法:

if ([myClass respondsToSelector:sel]) {
  // 获取实现的方法
  IMP imp = [myClass methodForSelector:sel];
  // 将 IMP 对象转换为 block 对象
  void (*func)(id, SEL, NSString*) = (void *)imp;
  // 调用方法
  func(myClass, sel, @"newArg");
}

上面的例子中,我们使用了 methodForSelector 定位到方法,然后将其转换为 block 并执行。不过,由于 performSelector 方法的限制,方法的参数最多只能有两个。

Lifecycles

Objective-C 中的类也有生命周期,而且还需要我们去管理内存的释放,这点会在内存管理部分详细介绍。

// 任何类在实例化之前都会先调用该方法
+ (void)initialize {  
}

// 对应于 initialize 方法,用于清空对象,当引用计数为 0 时被调用
- (void)dealloc {
  // 如果未启用 ARC,则需要手动释放引用计数
  [nickName release];
  // 调用父类方法
  [super dealloc];
}

Extensions

Extensions 用于扩展类的属性和方法。

// Extension 通常和实现类放在一起
@interface Person () // 声明扩展类的语法是:在类名后 + ()

// 添加一个扩展属性
@property NSString *firstName;
  
// 添加一个扩展方法
+ (instancetype)createWithName:(NSString *)name;

@end

Generic

从 Xcode 7 开始支持声明泛型类,使用关键字 __covariant

@interface Result<__covariant A> : NSObject

// 使用 block 作为方法参数
- (void)handleSuccess:(void (^)(A))success
              failure:(void (^)(NSError *))failure;

@end

由于编译期不支持在实现类中使用泛型,所以实现类中需要使用 id

@implementation Result

- (void)handleSuccess:(void (^)(id))success
              failure:(void (^)(NSError *))failure {
  // 假设调用成功,返回值是 42
  success(@42);
}

使用:

Result<NSNumber *> *r = [[Result alloc] init];
  [r handleSuccess:^void (NSNumber * result) { NSLog(@"result: %i", [result intValue]); } 
           failure:^void (NSError *) { NSLog(@"error"); } ];

当我们将返回值修改为字符串时:

success(@"ok");

对应的处理结果是:

Result<NSString *> *r = [[Result alloc] init];
  [r handleSuccess:^void (NSString * result) { NSLog(@"result: %@", result); } 
           failure:^void (NSError *) { NSLog(@"error"); } ];

Others

Protocols

协议类似于接口 (Interface) 的概念,我们可以在 @protocol 中定义方法和属性,然后交由其它类去实现。

@protocol Worker <NSObject>

  @property BOOL retired;

  - (void)performWork;
@end

通常,我们在声明类时将协议作为泛型类型使用:

@interface Person : NSObject<Worker>

在实现类中实现协议中的方法:

// 下面这行注释会在文件和导航栏中添加一条分割线
#pragma mark -
// pragma mark 是一种特殊的管理代码的注释,方便我们在 XCode 导航栏中跳转到页面的各个区域
#pragma mark Career protocol
// 如果需要在实现类中使用 protocol 中的属性,必须使用 synthesize 暴露出该属性
@synthesize retired = _retired;

// 实现 protocol 中的方法
- (void)performWork {
  NSLog(@"do some work.");
}

Categories

Categories 同样用于扩展一个类,它和 extensions 的最大的区别是,extensions 中扩展的方法一般只能用于某一个特定的实现类,但是 categories 为某个类添加的扩展方法可以用于所有的类,包括子类。

定义 category 和定义类的方式相同,需要先声明头文件,然后再在实现类中实现方法。通常情况下,文件名是实现的基类名+category 名,比如下面的这个例子中,为 Person 类添加了阅读相关的方法,那么,文件名就是 person+read.h

#import "person.h"

// 类后括号内就是这个 category 的名字
@interface Person (Read)

// 添加的方法
- (void)read:(NSString *)material;

@end

实现类:

#import "person+read.h"

@implementation Person (Read)

- (void)read:(NSString *)material {
  NSLog(@"I'm reading %@", material);
}

@end

当需要调用 category 中的方法时,只需要导入 category 即可:

#import "person+read.h"
#import "person.h"

...
- (void)someMethod {
  Person *p = [[Person alloc] init];
  [p read:@"a book"];
}

Blocks

Blocks 是 Objective-C 中一种特殊的对象,它可以直接执行一段代码,类似其它语言中的 lambda 表达式。

Block 字面量

我们可以使用 ^ 符创建一个 block 字面量:

^{
  NSLog(@"This is a block");
}

类似与 C 中的方法指针,我们可以使用下面的语法去引用一个 block:

void (^simpleBlock)(void);

以上可以理解为我们创建了一个 simpleBlock 变量来表示一个参数为 void,返回值也为 void 的方法。

之后,我们可以创建方法体:

simpleBlock = ^ {
  NSLog(@"This is a block");
}

当然,我们也可以将上面两个步骤合二为一:

void (^simpleBlock)(void) = ^ {
  NSLog(@"This is a block");
}

另外,block 也可以包含参数和返回值:

double (^addBlock)(double, double) = 
  ^(double firstValue, double secondValue) {
    return firstValue * secondValue;
  };

更多 block 的语法可以参考这个回答

内存管理

Objective-C 中并没有 Java 中的垃圾回收机制,我们必须自行管理内存。Objective-C 中使用引用计数的方式管理内存,当我们创建一个对象时,对应的内存也会被创建并分配给该对象,我们即拥有了 (own) 该对象,此时引用计数 +1。只要我们还持有该引用,该对象就会持续存在,不会被释放 (deallocate)。而当我们释放 (release) 了该对象,引用计数 -1,若引用计数减到 0 时,该对象才会从内存中被移出。

// 创建对象
MyClass *classVar = [[MyClass alloc] init];

// 使用对象
[classVar doSomething];

// 释放对象
[classVar release];

由于手动管理内存麻烦且容易出错,从 Xcode 4.2 和 iOS 4 开始,引入了 Automatic Reference Counting 来帮助我们管理内存。

// 默认值,变量会被保存在内存中直到离开作用域
__strong NSString *strongString;

// 弱引用,假如引用的对象被释放,那么弱引用会被设为 nil
__weak NSSet *weakSet;

// 与弱引用类似,但是如果引用的对象被释放,引用也不会被设为 nil
__unsafe_unretained NSArray *unsafeArray;

对于类的属性而言,默认会是 strong 的,也就是会在内存中被保存直到对象被释放;而如果属性是 weak 的,则当引用计数减到零后,属性接收到的值会变成 nil。

// 默认值
@property (strong) MyClass *strongVar;
// 改为弱引用
@property (weak) MyClass *weakVar;

Q & A

如何理解指针?

当我们使用指针时,我们其实是在引用一个对象的地址,而不是直接使用堆 (heap) 中创建的对象,这样,当我们传递对象并且对象被改变时,由于使用的是引用,我们能够得到改变后的对象。由于 Objecgive-C 是一门面向对象的语言,当我们创建一个对象时,大多数时候都应该使用指针。

idvoid * 的区别?

id 表示一个指向 Objective-C 对象的指针,而 void * 可以表示为任何指针。

另外,使用 id 声明对象时编辑器不会报错,只有在运行时才会提示错误,所以,推荐使用 NSObject * 而不是直接使用 id 创建一个代表任何类的对象。

Footnotes

  1. https://learnxinyminutes.com/docs/objective-c/

  2. 参考这个回答:https://stackoverflow.com/a/2620632/4837812

About

一个安卓开发者的 Objective-C 学习笔记。

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published