1.引言
Objective-C是对C的扩展,主要基于面向对象编程,用来开发IOS程序。
2.环境配置
下载最新版的Xcode,首次下载后要先开启一次Xcode进行初始化。
2.1创建控制台项目
打开Xcode,点击创建新Project。
这里我们选择macOS的Command Line Tool
工程名采用大驼峰进行命名,并填好基本信息,选择Objective-C。
此时Xcode会预创建main.m文件,并填入Hello World程序。点击command+R便可以进行编译运行。
2.2创建APP项目
创建APP项目的时候,在选择创建的项目选择ios下的Single View App。
创建过程中,如果希望能够在真机上进行运行调试,我们需要在Team中填入AppleID账号。
接下来我们将SceneDelegate.h、SceneDelegate.m、Main.storyboard删除,并在项目配置中(info)找到下面一行删除。
此外还需要在项目信息中(General),删除预设的Main Interface。
在AppDelegate.m中,需要改成如下代码,多余部分删除:
#import "AppDelegate.h"
#import "ViewController.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
return YES;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
ViewController *vc = [[ViewController alloc] init];
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];
return YES;
}
@end
3.Objective-C中的OOP
3.1类的定义与实现
在Objective-C中,采用“@interface”和“@end”定义类结构。常见类定义如下:
@interface ClassName: FatherClass //类名定义采用大驼峰,Objective-C仅支持单继承
{
//成员变量定义
Type variable1; //变量名采用小驼峰
Type variable2;
...
}
//不同方法之间空一行
//声明方法时,在-/+与返回类型之间留一个空格
//参数之间不要留空格 星号*之前必须有一个空格
//-代表成员方法,需要通过实例化的对象进行访问
- (Return Type)method1:(Parameter Type)parameter1 addParameter2:(Parameter Type)parameter2
//+代表类方法,可以使用类名进行访问,类似于C/C++中的static
+ (Return Type)method2:(Parameter Type)parameter1 addParameter2:(Parameter Type)parameter2
...
@end
定义完类的基本结构后,我们需要在.m文件中定义其类内方法的实现方式,我们按照以下结构来进行实现:
@implementation ClassName
- (Return Type)method1:(Parameter Type)parameter1 addParameter2:(Parameter Type)parameter2
{
//函数具体实现
...
return returnParamter;
}
+ (Return Type)method2:(Parameter Type)parameter1 addParameter2:(Parameter Type)parameter2
{
//函数具体实现
...
return returnParamter;
}
...
@end
在类中,我们还会定义构造函数和析构函数,主要采用以下的形式定义和调用:
@interface ClassName: FatherClass
{
//成员变量定义
...
}
- (id)init; //不带参数的构造函数
- (id)initWith...:(Parameter Type)parameter //带参数的构造函数
- (void)dealloc; //析构函数,如果有继承,需要调用[super dealloc]
...
@end
@implementation ClassName
...
@end
int main(int argc, char *argv[]){
ClassName *myclass1 = [[ClassName alloc] init]; //调用不带参数的构造函数
ClassName *myclass2 = [[ClassName alloc] initWith...:parameter]; //调用带参数的构造函数
...
[myclass1 release];
[myclass2 release]; //release方法使得引用计数减1,当引用计数减至0时调用dealloc方法
}
3.2继承
为了提高代码的复用性,且在需要修改代码时,能够更快的定位到需要修改的代码,我们会采用继承概念来实现类的创建和实现,将多个类之间相同可复用的成员或方法放置在父类中,而在子类中实现其特定的功能,也可以透过重写等操作,修改自定义子类的实现手段。
需要注意的是,在Objective-C中不支持多继承,如果想要使用类似多继承的特性,可以使用协议或分类来实现。
在继承中,有两个会涉及到的关键字:self和super。self的作用类似于C/C++中的this指针,用来返回当前对象的指针,在语法中,有两种访问成员的方法,分别是:“self.”和“self->”,其中“self.”是调用get和set方法,“self->”是直接访问成员变量。一般在子类中访问方法时(隐藏传递了参数self),会优先查找自己是否具有该方法,如果没有会向上搜索超类是否有该方法。如果想要访问超类定义的方法,可以利用super关键字来实现优先查找超类的方法。
在存储有继承链的对象时,采用基地址+偏移量的方式存储,超类会存储在子类前面(结构如下)。通常我们最初始的类都继承自NSObject,NSObject的实例对象是isa,用来保存指向该对象的指针。
3.3复合
复合是指一个类中包含各种其他的类,比如“车”包含“轮子”和“发电机”等其他类,也可以说是多个类组合形成一个新的类。
由于在一个类中,经常会复合其他不同的类,我们需要对这些成员设置set、get方法,以便在使用“self.”调用的时候能够找到对应的方法,具体的set函数和get函数的定义方式如下:
- (void)setParameter:(Parameter Type)parameter; //set函数必须set开头
- (Parameter Type)Parameter; //get函数不要用get前缀
3.4继承和复合的比较:
继承代表的是“is a”的关系,而复合则是代表“has a”的关系。
4.Foundation Kit快速教程
4.1结构体
(1)NSRange:
可以透过字段赋值、聚合结构赋值或是“NSMakeRange(location, length)”函数进行赋值。
typedef struct _NSRange {
unsigned int location;
unsigned int length;
}NSRange;
(2)NSPoint:
NSPoint用来表示笛卡尔平面中的一个点。
typedef struct _NSPoint {`
float x;
float y;
}NSPoint;
(3)NSSize:
typedef struct _NSSize {
float width;
float height;
}NSSize;
(4)NSRect:
typedef struct _NSRect {
NSPoint origin;
NSSize size;
}NSPoint;
4.2字符串
(1)NSString:
创建字符串可以直接赋值也可以透过类方法创建,其中一个按照格式的类方法如下:
+ (id)stringWithFormat:(NSString *)format, ...; //函数后面的...代表可以接受多个以逗号隔开的其他参数
//使用示例
NSString *string = [NSString stringWithFormat:@"Hello World %d!", 1];
//output: Hello World 1!
在NSString中有成员length记录字符串的大小,可以透过其get函数来取得NSString的长度。
//length的get函数
- (unsigned int)length;
//使用示例
unsigned int length = [string length];
unsigned int length = string.length;
字符串的比较采用类内定义的方法来比较两个字符串,有两种方法:
- (BOOL)isEqualToString:(NSString *)aString;
//使用示例
NSString *s1 = @"String1";
NSString *s2 = [NSString stringWithFormat:@"String%d", 1];
[s1 isEqualToString:s2];
//output YES
- (NSComparisonResult)compare:(NSString *)string; //这个方法返回是个枚举类型,可以判断字符串比较的大小(升序或降序)
//NSComparisonResult类型定义
typedef enum _NSComparisonResult {
NSOderedAscending = -1,
NSOrderSame,
NSOrderDescending
}NSComparisonResult;
- (NSComparisonResult)compare:(NSString *)string optiond:(unsigned)mask; //这个方法可以提供掩码选项来进行有条件的比较
//mask可以透过位或操作来添加复合的选项,常用的mask有
//NSCaseInsensitiveSearch 不区分大小写
//NSCLiteralSearch 区分大小写
//NSNumericSearch 比较字符串的字符个数
字符串匹配分为前缀匹配、后缀匹配和中间匹配,透过以下三个函数实现:
- (BOOL)hasPrefix:(NSString *)aString; //前缀匹配
- (BOOL)hasSuffix:(NSString *)aString; //后缀匹配
- (NSRange)rangeOfString:(NSString *)aString; //中间匹配
(2)NSMutableString:
这个类是NSString的子类,与NSString不同的是,NSString一旦被创建后,就不可以被修改,而NSMutableString则可以修改,透过以下类方法创建一个新的NSMutableString:
+ (id)stringWithCapacity:(unsigned)capacity; //透过指定字符串长度来创建NSMutableString,这里的大小不是严格的限制,而是建议值
添加字符可以透过以下几个方法添加:
- (void)appendString:(NSString *)aString;
- (void)appendFormat:(NSString *)format, ...;
- (void)insertString:(NSString *)aString atIndex:(NSUInteger)loc;
删除指定范围的字符串透过类内定义的方法删除;
- (void)deleteCharactersInRange:(NSRange)range;
4.3集合类
(1)NSArray:
NSArray是一个Cocoa类,用来储存有序列表(C/C++中的数组)。NSArray里面只能储存Objective-C的对象,不能储存C/C++中的基本数据类型或NSArray的随机指针(也不能存储nil),我们可以主要透过以下方法进行创建:
+ (instancetype)arrayWithObjects:(ObjectType)firstObj, ...; //后面的Object列表以nil结尾
在NSArray类中,常用的方法如下:
- (unsigned)count; //获得NSArray中对象个数
- (id)objectAtIndex:(unsigned int)index; //获得特定索引的对象
(2)NSMutableArray:
与NSString类似的是,NSArray一旦创建后,里面的对象不能修改,所以也同样的产生了一个可变数组—NSMutableArray。利用和NSMutableString一样的方式创建可变数组,透过给定容量来生成新的可变数组。
可以利用以下函数对NSMutableArray进行函数添加和删除操作:
- (void)addObject:(id)object; //在末尾添加object
- (void)removeObjectAtIndex:(unsigned)index; //删除指定位置的object
(3)NSEnumerator:
NSEnumerator是枚举器,可以用来访问数组的元素,透过数组的objectEnumerator方法来访问object,可以访问直到遇到nil。需要注意的是,当使用枚举器访问可变数组时,不能通过增加或删除方法对数组容器进行操作。
- (NSEnumerator *)objectenumerator; //NSArray类的方法,获得顺序枚举器
- (NSEnumerator *)reverseObjectenumerator; //NSArray类的方法,获得逆序枚举器
- (id)nextObject; //NSEnumerator类方法,获得数组容器的object
(4)NSDictionary:
这是一个字典容器,可以透过给定关键字来访问对应的信息。
//创建字典,先传object再传对应的key
- (id)dictionaryWithObjectsAndKeys:(id)firstObject, ...;
//访问object
- (id)objectForKey:(id)key;
(5)NSMutableDictionary:
与字符串和数组容器类似的,NSDictionary是不可变字典,一样也有可变字典NSMutableDictionary,透过指定容器大小创建,并根据类内方法添加键值对应对象。
//透过容量大小创建可变字典
- (id)dictionaryWithCapacity:(unsigned int)capacity;
//添加新的键值
- (void)setObject:(id)object forKey:(id)key;
//删除键值
- (void)removeObjectForKey:(id)key;
对于上面给出的数据结构,可以使用但不要进行继承。
4.4NS中的数值类
(1)NSNumber:
由于前面提到的集合类,无法存储基本数据类型,因此Cocoa提供了NSNumber来封装这些数据结构,其类内方法定义如下:
//创建NSNumber类
+ (NSNumber *)numberWithChar:(char)value;
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithBool:(BOOL)value;
//取得原来的数据类型
- (char)charValue;
- (int)intValue;
- (float)floatValue;
- (BOOL)boolValue;
- (NSString *)stringValue;
(2)NSValue:
NSValue是NSNumber的超类,NSValue可以包装任意类型,同样也能放进NS容器中。
//创建NSValue,value参数通常传入的是想要存储变量的地址,type参数可以利用编译器指令@encode(className)
- (NSValue *)valueWithBytes:(const void *)value objCType:(const char *)type;
//取得存储的变量数值,value参数传入的是变量地址
- (void)getValue:(void *)value
//Cocoa提供的转换方法
+ (NSValue *)valueWithPoint:(NSPoint)point;
+ (NSValue *)valueWithSize:(NSSize)size;
+ (NSValue *)valueWithRect:(NSRect)rect;
- (NSPoint)pointValue;
- (NSSize)sizeValue;
- (NSRect)rectValue;
(3)NSNull:
这个类用来表示空,由于每次创建的NSNull返回的是一样的数值,可以使用==来进行判断。
+ (NSNull *)null;
5.协议
协议是OC里比较重要的一个概念,透过协议我们可以实现多继承、代理等操作,在iOS的UI编程中,MVC模式下,用户在View中的操作可以透过协议代理的方式通知ViewController,下面是协议的基本定义方法:
@protocol ProtocolName <NSObject>
@required
// Method define
@optional
// Method define
@end
从上面的定义来看,协议中的方法有两种属性:required和optional,在遵守协议的类中,必须实现required属性的方法(虽然不遵守只会报出警告),而optional属性的方法则可以看需求实现。
6.内存管理
一个对象有其生命周期,在C/C++中学习过,如果一个堆上的变量,没有被手动释放的话,会有内存泄漏的风险。在Objective-C中,以前需要程序员手动管理内存(MRC),现在经过Apple的更新,已经采用自动管理内存(ARC),不需要程序员自己管理。
-
引用计数
MRC中的核心,除了new/delete、alloc/dealloc之外,引用计数也是一个重要的观念。Objective-C中,在创建一个对象的时候,Objective-C会自动将该对象的引用计数设置为1;当一个对象的引用计数为0的时候,会自动调用该类的dealloc方法,我们可以重写dealloc方法来实现自定义的操作。
为了让程序员能够控制引用计数。Objective-C中提供了以下几个方法来操作引用计数:
- (id)retain; // 引用计数+1
- (void)release; // 引用计数-1
- (unsigned)retainCount; // 获得该对象的引用计数
当我们在写一个成员的set方法时,需要考虑到对象的销毁时机,以下面的例子举例:
// 假设Car类里面有一个Engine类的成员,考虑以下的set方法
- (void)setEngine:(Engine *)engine
{
_engine = engine;
}
int main()
{
Car *car = [Car new];
Engine *engine = [Engine new]; // engine引用计数=1
[car setEngine:engine]; // engine引用计数=1
[engine release]; // engine引用计数=0,触发dealloc
...
Engine *newEngine = car.engine; // car.engine的引用计数已经归零,被销毁了,访问不到该变量
}
-
所有权
为了避免上面的情况,这里引入一个所有权的概念,所有权代表的是:谁持有一个对象,由谁来管理该对象的引用计数,在持有的时候retain,在不使用的时候release。当所有权的概念被遵守的时候,我们可以保证:如果谁还需要使用一个对象,该对象的引用计数一定不为0,如果没有任何地方要使用该对象时,该对象能够被销毁(引用计数变为0)。
引入所有权的概念后,我们需要对set函数进行修改:
// 假设Car类里面有一个Engine类的成员,考虑以下的set方法
- (void)setEngine:(Engine *)engine
{
[engine retain];
[_engine release]; // 先retain再release可以避免当_engine == engine且该对象引用计数=1时被意外销毁
_engine = engine;
}
int main()
{
Car *car = [Car new];
Engine *engine = [Engine new]; // engine引用计数=1
[car setEngine:engine]; // engine引用计数=2
[engine release]; // engine引用计数=1
...
Engine *newEngine = car.engine; // car.engine=engine引用计数=1
[newEngine retain]; // newEngine引用计数=2
}
// 我们在Car类的dealloc方法也需要将engine的引用次数-1
- (void)dealloc
{
[_engine release];
[super dealloc];
}
-
Autorelease
autorelease透过自动释放池来实现,autorelease的作用是,预先设定在未来某个时刻(autorelease销毁时)向一个对象发送release消息,我们可以透过创建一个autorelease pool并将想要自动释放的对象加入pool中,在pool销毁时,系统会自动向pool中的所有对象发送一次release消息。
// 创建autorelease pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
...
// 将想要自动释放的对象加入pool中
[obj autorelease]; // 此时obj引用计数保持不变,加入自动释放池
...
// 自动释放池销毁
[pool release]; // 此时向池中所有对象发送release消息,如果该对象引用计数变为0,则自动销毁该对象。
7.类别
当我们在开发过程中,时常会需要在现有的类中添加一些新方法,一般来说我们可以透过直接修改类定义和实现或继承的方式处理,但对于三方SDK或系统类,无法直接修改类定义和实现,又觉得继承子类比较麻烦时,我们可以为该类新增一个类别,只要我们保证类别名唯一,并在使用该方法是引入包含了类别定义的头文件,就可以正常使用新方法。
-
类别的定义和实现
类别的定义、实现和一般的类定义类似,只是在类的后面添加"(类别名)"。
// 类别定义,假设为NSString添加一个新的类别
@interface NSString (newCategory)
- (Return Type)newMethod:(Parameter Type)parameter...;
...
@end
// 类别实现
@implementation NSString (newCategory)
- (Return Type)newMethod:(Parameter Type)parameter...
{
...
}
...
@end
-
类别的缺陷
1. 无法在类别中添加实例变量,一般都是透过方法来实现功能的判断。
2. 如果类别中的方法和原有的方法重名,系统默认类别有较高的优先级,会直接覆盖掉原有方法。
8.block
block是一个数据类型,用来表示一段代码,是Objective-C对于闭包的实现,在创建时透过以下的方式创建:
// 按照格式创建
returnType(^blockName)(parameterTypes) = ^(parameters) {
statements
};
// 利用Objective-C中的自动类型创建
__auto_type blockName = ^(parameters) {
statements
};
// 调用block
blockName(parameters);
block具有以下几个特点:
1. 可以嵌套定义,定义方式和函数定义很像。
2. 可以定义在方法内部或外部。
3. 只有调用block的时候才会执行内部代码。
4. 本质是对象。
在block里面,我们还需要了解block如何去捕捉外部的变量。在C/C++/OC中主要有几种变量:
1. 全局变量
2. 静态全局变量
3. 静态变量
4. 局部变量(自动变量)
我们透过以下代码来查看block对四种变量的捕获:
#import <Foundation/Foundation.h>
int global_i = 1;
static int static_global_j = 2;
int main(int argc, const char * argv[])
{
static int static_k = 3;
int val = 4;
void (^myBlock)(void) = ^{
global_i ++;
static_global_j ++;
static_k ++;
NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
};
global_i ++;
static_global_j ++;
static_k ++;
val ++;
NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
myBlock();
return 0;
}
// 输出结果
// Block外 global_i = 2,static_global_j = 3,static_k = 4,val = 5
// Block中 global_i = 3,static_global_j = 4,static_k = 5,val = 4
在上面四种变量之中,对于全局变量、静态全局变量,由于其作用域较广,在block中进行修改后,离开block,其值依然可以保存下去;对于静态变量来说,block里面捕获的是其引用,因此在内部可以对值进行修改;局部变量block捕获是数值,类似于C语言函数中的值传递,因此在block定义的时候已经将局部变量的值捕获进去了,即便外面对局部变量进行修改,也无法影响block内部的数值。
在Objective-C中,为了防止程序员在block中修改局部变量的值导致结果不符合预期,在编译层面就会报错,提示程序员修改变量类型或传递指针/引用。
如果我们希望在block中修改局部变量的值呢?可以透过__block关键字来实现对局部变量的捕获即修改。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
__block int i = 0;
void (^myBlock)(void) = ^{
i ++;
NSLog(@"%d",i);
};
myBlock();
return 0;
}
// 输出:
// 1
__block关键字的原理是将局部变量进行封装,封装成一个结构体,这个结构体中有指向自己的指针__forwarding,可以透过这个指针来实现对局部变量的访问和修改。