开始学习 Objective-C

随着 Swift 日益成熟,Objective-C 地位在日益下滑,但是并不会完全消失,一是很多已有的大应用都有很多 Objective-C 存量代码,如果要使用 C/C++ 的一些跨平台的库,使用 Objective-C 编写 Objective-C++ 做桥接是唯一的选择。多说一点 Swift 可以直接使用 C 的库。本文并不是一篇完整的教程,更像一篇快速笔记,既会回顾 C 语言,也会讲解 Objective-C 中的扩展部分:声明 declar 和定义 define,基本数据类型、对象封装的基本数据类型、集合类、面向对象和其他杂项。
回顾 C 语言
Objective-C 是对 C 语言的扩展,主要增加了面向对象的功能,这里罗列一下 C 语言的内容,要详细学习 C 语言,推荐 C 语言程序设计:现代方法。
- 语法
- 声明和定义
- 算术运算符、赋值运算符、位运算符
- 选择语句、循环语句
- 基本类型:整数、浮点数、字符,类型转换
- 数组
- 指针
- 字符串
- 函数
- 结构体
- 联合
- 枚举
- 动态存储分配
- 程序的组成
- 头文件和源文件
- 预处理
- 编译和链接
声明 declar 和定义 define
声明说明符 声明符;
声明为编译器提供有关于标识符含义的信息,声明说明符分为 3 大类:
- 存储类型。
- 类型限定符。
- 类型说明符,比如 char, short, int, long 等。
存储类型
存储类型可以用于变量以及较小范围的函数和形式参数的说明,C 和 Objective-C 程序中的每一个变量都具有以下 3 个性质:
- 存储期限 Storage Duration:变量的存储期限决定了为变量预留和内存被释放的时间。
- 作用域 Scope:变量的作用域是指可以引用变量的那部分程序文本。
- 链接 Linkage:变量的链接确定了程序的不同部分可以共享此变量的范围。
auto 存储类型
auto 存储类型的变量具有自动存储期限,块作用域,并且无链接。
int i; // 静态存储期限,文件作用域,外部链接
void f(void) {
int j; // 自动存储期限,块作用域,无链接
}
static 存储类型
static 存储类型用在块外部时,说明变量具有内部链接,本质上使变量只在声明它的文件内可见;当用在块内部时,static 把变量的存储期限从自动的变成了静态的。
static int i; // 静态存储期限,文件作用域,内部链接
void f(void) {
static int j; // 静态存储期限,块作用域,无链接
}
extern 存储类型
extern 存储类型使几个源文件可以共享同一个变量,下面的语句不会导致编译器为变量 i 分配存储单元,它只是提示编译器需要访问定义在别处的变量,可能稍后在同一个文件中,更常见的是在另一个文件中。
extern int i; // 静态存储期限,文件作用域,什么链接?
register 存储类型
register 存储类型就是要求编译器把变量存储在寄存器中。
类型限定符
const
const 用来声明一些变量是只读的。
通过 static 和 const 定义在此文件内部使用的常量:
static NSString * const PWHeaderCRLF = @"\r\n";
通过 extern 和 const 定义在此文件外部可使用的常量:
// .h
extern NSString * const HVBaseURL;
// .m
NSString * const HVBaseURL = @"https://example.com/api/";
基本数据类型
C 定义的基本数据类型
int someInteger = 42;
float someFloatingPointNumber = 3.1415;
double someDoublePrecisionFloatingPointNumber = 6.02214199e23;
char ch = 'a';
Objective-C 定义的基本数据类型
注意 NSInteger、NSUInteger 和 CGFloat 在不同 CPU 架构上精度会有所不同。
BOOL isGood = YES;
NSInteger negative = -123;
NSUInteger positive = 123;
CGFloat width = 100.0;
对象封装的基本数据类型
字符串
可以使用 C 语言中用 指针指向字符数组 的方法来使用字符串:
char *name = "John Smith";
printf("My name is %s", name);
Objective-C 中用 NSString 封装了字符串类型为对象的方式:
NSString *firstString = [[NSString alloc] initWithCString:"Hello World!"
encoding:NSUTF8StringEncoding];
NSString *secondString = [NSString stringWithCString:"Hello World!"
encoding:NSUTF8StringEncoding];
NSString *thirdString = @"Hello World!";
数字对象
NSNumber
NSNumber *magicNumber = @42;
NSNumber *unsignedNumber = @42u;
NSNumber *longNumber = @42l;
NSNumber *boolNumber = @YES;
NSNumber *simpleFloat = @3.14f;
NSNumber *betterDouble = @3.1415926535;
NSNumber *someChar = @'T';
int scalarMagic = [magicNumber intValue];
unsigned int scalarUnsigned = [unsignedNumber unsignedIntValue];
long scalarLong = [longNumber longValue];
BOOL scalarBool = [boolNumber boolValue];
float scalarSimpleFloat = [simpleFloat floatValue];
double scalarBetterDouble = [betterDouble doubleValue];
char scalarChar = [someChar charValue];
值对象
NSNumber 是 NSValue 的子类,NSValue 可以封装 C 基本数据类型,还可以封装指针和结构体。
NSString *mainString = @"This is a long string";
NSRange substringRange = [mainString rangeOfString:@"long"];
NSValue *rangeValue = [NSValue valueWithRange:substringRange];
typedef struct {
int i;
float f;
} MyIntegerFloatStruct;
struct MyIntegerFloatStruct aStruct;
aStruct.i = 42;
aStruct.f = 3.14;
NSValue *structValue = [NSValue value:&aStruct
withObjCType:@encode(MyIntegerFloatStruct)];
集合类
数组
可以使用 C 语言的数组:
int numbers[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
for (int i = 0; i < 3; i++) {
printf("%d\n", numbers[i]);
}
Objective-C 中使用 NSArray 实现数组,NSArray 中只能装对象,所以对于基本数字类型要用 NSNumber 封装为对象,所以要采用下面的形式:
NSArray *numbers = @[@1, @2, @3];
for (NSNumber *number in numbers) {
NSLog(@"%d", number.intValue);
}
id firstObject = @"someString";
id secondObject = nil;
id thirdObject = @"anotherString";
NSArray *someArray = @[firstObject, secondObject, thirdObject];
集合
NSSet
NSSet *simpleSet = [NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil];
字典
NSDictionary
NSDictionary *dictionary = @{
@"anObject" : someObject,
@"helloString" : @"Hello, World!",
@"magicNumber" : @42,
@"aValue" : someValue
};
可变类
上面的 NSArray、NSSet 和 NSDictionary 都是创建的时候给定其内容,之后就不能再修改了,其可变类是 NSMutableArray、NSMutableSet 和 NSMutableDictionary。
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:@"another string" forKey:@"secondString"];
[dictionary removeObjectForKey:@"anObject"];
面向对象
类和对象
定义了一个 DTShape 类,有一个属性是 numberOfSides,还有一个实例方法 simpleDescription:
开头为 + 是类方法
开头为 - 是实例方法
#import <Foundation/Foundation.h>
@interface DTShape : NSObject
@property (nonatomic) int numberOfSides;
- (NSString *)simpleDescription;
@end
#import "DTShape.h"
@implementation DTShape
- (NSString *)simpleDescription {
return [NSString stringWithFormat:@"A shape with %d sides", self.numberOfSides];
}
@end
使用 new 创建一个对象,设置属性 numberOfSides 为 4,再调用 simpleDescription 方法:
DTShape *shape = [DTShape new];
shape.numberOfSides = 4;
NSString *description = [shape simpleDescription];
属性
属性是对实例变量访问的抽象,类似于 Java 中 getter 和 setter,如下的代码会自动添加两个实例变量 _threshold 和 _quantizationLevels 来作为属性的实现:
@interface VideoToonFilter : VideoFilter
@property (nonatomic, assign) float threshold;
@property (nonatomic, assign) float quantizationLevels;
- (instancetype)init;
@end
@dynamic
@dynamic 关键字告诉编译器不要自动添加实例变量作为属性的实现,实现会在运行时找到。
原子性
默认情况下,属性的实现都会加锁来保证原子性,也就自带 atomic,声明成 nonatomic 就不保证原子性。
可读可写
默认情况下,属性的实现都是可读可写的,也就是自带 readwrite,声明成 readonly 就是只读的。
内存管理
- assign:只是赋值。
- strong:retain 新值,release 旧值。
- weak:像 assign 只是赋值,但是指向的对象被销毁时,会赋值为 nil。
- unsafe_unretained:像 assign 只是赋值,但是指向的对象被销毁时,不会赋值为 nil。
- copy:拷贝一次。
方法名
- getter=<name> 指定 getter 的名称。
- setter=<name> 指定 setter 的名称。
继承
通过继承 DTShape 来声明了类 DTNamedShape,其中新增了属性 name,新增了初始化方法 initWithName,覆盖了实例方法 simpleDescription:
#import "DTShape.h"
@interface DTNamedShape : DTShape
@property (nonatomic, copy) NSString *name;
- (instancetype)initWithName:(NSString *)name;
@end
#import "DTNamedShape.h"
@implementation DTNamedShape
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (self) {
self.name = name;
}
return self;
}
- (NSString *)simpleDescription {
return [NSString stringWithFormat:@"A %@ with %d sides", self.name, self.numberOfSides];
}
@end
id
id 数据类型可存储任何类型的对象,它是一般对象类型。
id s = [DTShape new];
@class
@class 指令提高了效率,因为编译器不需要引入和处理整个 DTShape.h 文件,只需要知道 DTShape 是一个类名:
@class DTShape;
其他
typedef
使用 typedef 定义一个新类型名,可按照下面的步骤:
- 像声明所需类型的变量那样写一条语句;
- 在通常应该出现声明的变量名的地方,将其替换为新的类型名;
- 在语句的前面加上关键字 typedef。
typedef NSNumber *NumberObject;
NumberObject myValue1, myValue2, myResult;
枚举
Objective-C 中通过 NS_ENUM 定义枚举,不过只能是整数类型:
typedef NS_ENUM(NSInteger, HVRegisterType) {
HVRegisterTypeRegister = 0,
HVRegisterTypeForgetPassword = 1,
HVRegisterTypeResetPassword = 2
};
C 语言中定义枚举:
typedef enum {CLUBS, DIAMONDS, HEARTS, SPADES} Suit;
Suit s1, s2;
联合
Objective-C 声明和使用的联合就是 C 的语法:
union {
int i;
double d;
} u;
struct {
int i;
double d;
} s;
内存中的分布:
结构体
Objective-C 声明和使用的结构体就是 C 的语法:
struct MyPoint {
int x;
int y;
};
typedef struct MyPoint MyPoint;
MyPoint p = { .x = 10, .y = 20 };
printf("(%d, %d)\n", p.x, p.y);
块
块是 Objective-C 对 C 语言的一种扩展,块定义在函数或者方法内部,并能够访问在函数或者方法范围内块之外的任何变量,一般不能够改变这些变量的值,只有添加 __block 在变量前面才可以:
NSString* (^calculateTriangularNumber) (int) = ^NSString* (int n) {
int i, triangularNumber = 0;
for (i = 1; i <= n; i++) {
triangularNumber += i;
}
return [NSString stringWithFormat:@"Triangular number %i is %i", n, triangularNumber];
};
NSString *result = calculateTriangularNumber(5);
通过 typedef 定义类型:
@interface DTAlertView : UIView
typedef void (^alertViewCompletion) (BOOL finished);
- (void)showInView:(UIView *)view completion:(alertViewCompletion)completion;
@end
函数
Objective-C 声明和使用的函数就是 C 的语法:
int foo(int bar) {
return bar * 2;
}
foo(1);
指针
Objective-C 声明和使用的指针就是 C 的语法,指针就是一个整数变量,里面装着指向某个值的内存地址,& 是地址运算符,* 是间接寻址运算符:
int count = 10;
int *intPtr = &count;
int x = *intPtr;
动态存储分配
C 语言中的动态存储分配:
- malloc 函数 —— 分配内存块,但是不对内存块进行初始化。
- calloc 函数 —— 分配内存块,并且对内存块进行清零。
- realloc 函数 —— 调整先前分配的内存块大小。
预处理
预处理程序语句使用 # 标记,这个符号必须是一行中的第一个非空字符,顾名思义,预处理程序实际上是在分析 Objective-C 或者 C 程序之前处理这些语句。
宏定义
使用 #define 就是给符号名称指定程序常量:
#define PI 3.141592654
#define TWO_PI 2 * 3.141592654
条件编译
#ifdef,#ifndef,#endif 是条件编译:
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#endif
#ifndef __OBJC__
#if USE_IL2CPP_PCH
#include "il2cpp_precompiled_header.h"
#endif
#endif
在 Xcode -> Target -> Build Settings 中设置:
头文件
Objective-C 中使用 #import 导入头文件,"" 在包含源文件的目录中查找,<> 在系统头文件目录中寻找包含文件:
#import "PWAPIController.h"
#import <AFNetworking/AFNetworking.h>
C 中使用 #include 导入头文件,"" 在包含源文件的目录中查找,<> 在系统头文件目录中寻找包含文件,通常把系统头文件保存在目录 /usr/include 中:
#include "decode_audio.h"
#include <stdio.h>