原创
最近更新: 2022/04/22 06:08

objective-C入门 之 基础知识

基础语法

objective-C 是 C语言的严格超集,任何C语言代码都能被OC编译器编译。OC++则是OC加上了C++语言的特性之后的产物。

OC支持C语言的.h头文件,源代码文件的扩展名为.m,OC++源代码扩展名为.mm。

#import 预编译

OC支持C语言的#include引入头文件的方法,但该方法可能造成重复引入的问题。OC的#import方法可以在引入头文件的同时避免重复引入问题。

如果两个头文件有互相包含的情况,此时可以使用@class关键字对其中一个类进行预先声明。

BOOL类型变量

C语言并没有bool类型变量,OC提供了,但条件语句的判断规则并没有改变。

//objc.h
typedef signed char BOOL;
#define YES ((BOOL)1)
#define NO ((BOOL)0)

应当注意的是,由于YES被严格定义为1,条件语句中不能使用 var == YES 来进行判断。

三元运算符

OC中的三元运算符 ?: 和C中相同。值得注意的是,如果判断条件与返回值相同时,有更简洁的写法。

result = object ? object : otherObject;
result = object ? : otherObject;//建议使用这种写法

NSLog 与 格式化输出

NSLog() 是 OC 中重要的日志打印方法,输出内容包括时间、线程号等信息。

%@ %d/%i %ld/%lld %f %x/%X %o %p %s %c %C
description
方法
十进制
整型
长整型 浮点数 十六进制
整型
八进制
整型
指针地址 C字符串 char字符 unichar字符

description方法是 NSObject 类型的实例方法,类似于Java的toString方法。

枚举类型

NS提供了一个可读性较强的枚举类型定义方法。

//NS_ENUM常用于单选的情况
typedef NS_ENUM(NSUInteger, NSEnumType) {
    NSEnumTypeNone = 0,//也可以自定义起始数字
    NSEnumTypeFirst,
    NSEnumTypeSecond,
};
//NS_OPTIONS常用于多选的情况
typedef NS_OPTIONS(NSUInteger, NSOptionType) {
    NSOptionType1 = 1 << 0,
    NSOptionType2 = 1 << 1,
    NSOptionType3 = 1 << 2,
    NSOptionType4 = 1 << 3,
};

#pragma mark

用于在Xcode中画一条分割线,当源文件较长时使用,可以使得代码结构较为清晰。

#pragma mark [-] 代码片段标题

static关键字

OC 中的 static 关键字与C语言相同,与 C++ 有所不同,只能用于修饰局部变量,不能修饰属性和方法。

异常处理

OC中提供了 try-catch 语句,但由于OC语言的动态特性,似乎并不好用,所以并不建议使用。

内存管理

在早期的Xcode版本中,OC需要程序员手动管理对象的创建与释放,称为MRC (Manual Reference Counting, 手动引用计数)。

在 MRC 中,程序员通过 retain 和 release 方法控制对象的引用计数 +1 和 -1,当引用计数为0时,对象调用 dealloc 方法,释放内存。

在复杂情况下,内存管理困难,MRC 容易导致内存泄漏,于是诞生了 ARC(Auto Reference Counting, 自动引用计数)。

在 ARC 中,系统自动维护对象的引用计数,程序员无需,也不允许进行相关操作。

如果要在 ARC 的工程中加入 MRC 的代码,可在 targets 的 build phases 选项下 Compile Sources 中选择使用 MRC 编译的文件,添加 -fno-objc-arc 编译选项。


block 类型

block是OC提供的一种数据类型,功能类似于函数指针,用于存储和传递一段封装好的代码块。

//无参数无返回值
void (^block1)(void) = ^void (void){
	//函数体
};
//void可以省略
void (^block1)() = ^{...};
block1();//block的调用

//有参数有返回值
//定义部分的返回值类型可以省略,编译器会根据return语句自动判断
int (^block2)(int) = ^int (int a){
    return a;
};
int a = block2(1);
//如果不显式定义return语句,则返回一个void类型的nil值

//考虑到可读性,一般会用typedef起个别名
typedef int (^blockType)(int);
blockType block2 = ^(int a){return a;};

block的变量捕获

block代码块内可以访问外界的变量。但由于使用的是值传递,在block内部无法对定义在block外部的局部变量做出的修改。可以修改全局变量的值。

int a = 10;
void (^block1)(void) = ^{
	NSLog(@"%d", a);//允许
	a++;			//报错
	//如果一定要修改a的值,应当将a的声明改为
	//__block int a = 10;
	//此时变量a的传递就是引用传递
};

block可能导致内存泄漏问题

在block中,可能会出现如下的self与block互相持有的情况,将导致循环引用和内存泄漏:

- (void)func {
	self.block = ^{
		NSLog(@"%@", self);
	};
}

//正确的写法
- (void)func {
	//在外部声明一个self的弱引用
	__weak typeof(self) weakSelf = self;
	self.block = ^{
		//让block捕获弱引用,以避免引用计数+1
		//如果要多次使用self指针,则将弱引用转化为强引用,避免释放
		//此时的strongSelf是局部变量,代码块释放时会自动销毁
		__strong typeof(self) strongSelf = weakSelf;
		NSLog(@"%@", strongSelf);
	};
}

block类型的本质

对block代码进行编译后,得到如下的c语言中间代码:

//存储block主要信息的结构体
struct __block_impl {
    void *isa;		//和OC对象的数据类型
    int Flags;		//当block被copy时,应该执行的操作
    int Reserved;	//保留字段
    void *FuncPtr;	//核心字段,函数指针
};

static struct __main_block_desc_0 {
	size_t reserved; 	// 保留字段
	size_t Block_size; 	// block 的大小
}

struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0 *Desc;
	int a;	//从外部捕获的变量

//构造函数,注意此处的变量捕获是值传递
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
	}
};

三种block

根据内存分配状况的不同,block分为三类:

当block未访问局部变量时,为__NSGlobalBlock,存放在数据段

对该类型的block,retain、copy、release操作都无效。

static int age = 10;
void(^block)(void) = ^{
	NSLog(@"%d",age);
};
NSLog(@"%@",[block class]);

在MRC模式下,当block访问局部变量时,为__NSStackBlock,存放在栈区

对该类型的block,retain、release操作无效,在函数返回后将系统回收。

int age = 10;
void(^block)(void) = ^{
	NSLog(@"%d",age);
};
NSLog(@"%@",[block class]);

如果对__NSStackBlock进行了copy操作,为__NSMallocBlock,存放在堆区

  • 在ARC模式下,系统会自动对__NSStackBlock进行copy操作。一般情况下使用的都是这个类型的block。

  • __NSStackBlock被强指针引用,会自动进行copy操作。

int age = 10;
void(^block)(void) = [^{
	NSLog(@"%d",age);
} copy];
NSLog(@"%@",[block class]);

面向对象

类的声明与实现

类的声明

//外部类一般声明在 CLASS_NAME.h 中

//声明类名和父类,没有明确父类的,应设置为NSObject
@interface CLASS_NAME : SUPER_CLASS {
	//大括号内可以声明变量和属性
	//但是一般不声明在此处,而是使用@property关键字
	@private
	//此处声明私有变量
	@public
	//此处声明公共变量
	@protected
	//此处声明的变量可以在子类中访问
}

//此处进行公共属性和方法的声明
@property NSString *name;
@property int *age;

- (instancetype)initWithName:(NSString *)name;

@end//声明部分的结尾

@property关键字

值得一提的是OC提供了一套属性及其getter和setter的命名规范。

以上面的name属性为例,属性名是name,使用@property进行声明时,OC会同时生成:

NSString *_name;					//一个带下划线的私有变量
- (NSString *)name;					//默认 getter 方法,可以重写
- (void)setName:(NSString *)name;	//默认 setter 方法,可以重写

如果同时重写了getter和setter方法,就不会生成对应的私有变量了,此时应当用以下语句手动声明一个私有变量:

@synthesize name = _name;

点语法

在上述命名规范下,OC提供了点语法obj.name来访问属性,此时会调用对应的getter(右值)和setter(左值)方法。

严格按照规范进行命名,即使不使用@property关键字,也能使用点语法

obj.name = @"name";			//等价于 [obj setName:name];
NSString *name = obj.name;	//等价于 NSString *name = [obj name];

由于OC语言方法调用机制的特性,空对象调用方法返回值为空,使用点语法来访问属性可以提高程序的安全性。如果不使用点语法,而使用 obj->name 的形式访问变量,当 obj 对象为空时,程序将会崩溃。

getter 和 setter 方法中应避免使用对应的点语法,以避免无限递归造成崩溃。

@property的参数

@property 关键字可以通过配置参数来生成不同的getter和setter方法。

//线程安全相关
@property(atomic) int a;		//默认,采用自旋锁,对写操作加锁,读操作不加锁
@property(nonatomic) int a;		//常用,线程不加锁,效率高
//引用相关
@property(assign) int a;		//直接赋值,适用于基本类型
@property(retain) id a;			//引用计数+1,在MRC下使用
@property(strong) id a;			//引用计数+1,在ARC下使用
@property(weak) id delegate;	//弱引用,在ARC下使用,不影响引用计数,用于规避循环引用
@property(copy) NSString *a;	//深拷贝,常用于字符串
/*
weak和assign的区别
weak和assign在作用于指针时都不会使引用计数+1。
weak修饰的指针会在对象释放之后指向空对象nil,而assign修饰的指针不会。
此时如果再次访问assign指针,会发生野指针错误。
*/
//空值判断
@property(nullable) id a;		//允许空值赋值
@property(nonnull) id a;		//当该属性赋值为nil时报警告
//getter和setter相关
@property(readwrite) int a;		//默认,同时生成getter和setter
@property(readonly) int a;		//只读,不生成setter
@property(getter=xxx) BOOL a;	//给getter起个别名,当属性是BOOL类型时,可以改为is, can开头的方法名明确语义
@property(setter=yyy:) int a;	//给setter起个别名,注意那个冒号,不建议修改

类的实现

在实例方法中,可以使用self指针来引用对象本身;在类方法中,self指向类本身在代码段中的地址(即isa指针)。

因此,不能在实例方法中通过self调用类方法,也不能在类方法中调用实例方法。

//CLASS_NAME.m
@implementation CLASS_NAME {
	//此处可以声明私有变量,作用域是本实现
	//不过一般不写在此处,而是写在延展中
	int _privateValue;
}

//类方法/静态方法,使用类名调用
+ (instancetype)new {
	//系统默认提供一个new方法,等价于[[CLASS_NAME alloc] init];
	//调用方法为[CLASS_NAME new];
	//此处进行重写,应保证调用方法不变
	return [[[self class] alloc] init];
}

//实例方法,需要具体的对象来调用
- (instancetype)init {
	//系统默认提供一个init方法,生成一个初始化为0的对象
	//调用方法为[[CLASS_NAME alloc] init];
	//此处进行重写,应保证调用方法不变
	if (self = [super init]) {
		//此处书写初始化逻辑
	}
	return self;
}

//个性化的初始化方法,由于系统没有提供默认的声明,所以需要在头文件中声明后,该方法才能被外界调用
//调用方法为[[CLASS_NAME alloc] initWithName:@"name" andAge:10];
- (instancetype)initWithName:(NSString *)name andAge:(int)age {
	if (self = [super init]) {
		_name = name;	//也可以使用self.name = name;
		_age = age;		//也可以使用self.age = age;
		//其他初始化逻辑
	}
	return self;
}

//getter方法的重写,一般用于懒加载
- (NSString *)name {
	if (!_name) {//懒加载逻辑
		_name = @"None";
	}
	return _name;
}

//setter方法的重写
- (void)setName:(NSString *)name {
	_name = name;
}

//一个方法如果没有在头文件中声明,则是私有方法
- (void)func {};

//dealloc方法会在对象释放的时候调用
- (void)dealloc {
	//对象释放时需要处理的逻辑,如资源释放
}

@end//实现部分的结尾

方法可以多次声明,但只能有一次实现。


继承和多态

OC 只支持单继承,子类将拥有父类的所有方法和成员变量,但是只能访问公开的属性和方法

OC 不存在虚方法、抽象类等,子类重写会覆盖父类的方法;调用方法时,会先从子类方法中寻找,然后向父类递归寻找。

在子类中,通过 super 指针来指向父类。类似self,super在类方法中只能调用父类的类方法,在实例方法中只能调用父类的实例方法。

instancetype 是专门为继承的场景设计的关键字,用于方法返回值,会自动返回调用者的指针类型。

这个场合下使用id也可以达成目标,但是id会绕开编译器检查,可能造成不安全的指针赋值。


协议 protocol

OC的协议就是Java的接口,用于声明一系列方法。一个类可以遵守多个协议。

当一个类遵守一个协议时,自动拥有协议中所有方法的声明。

//协议名.h
//协议本身也有继承关系,用尖括号表示,默认应该遵守NSObject协议
@protocol 协议名 <NSObject>
//声明属性
//应当注意的是协议中不会生成变量,此处的@property只会生成getter和setter的声明
@property(nonatomic) a;
//声明方法
@required	//此处的方法应当被实现,如果不实现编译器会报警告
- (void)func;
@optional	//可选方法,如果不实现编译器不会报警告
- (void)func2;
@end

//--------一个类遵守一个协议--------
//类名.h
@interface 类名 : NSObject <协议名1, 协议名2,...>
@end

//--------协议的使用--------
//在声明一个变量时,可以声明该变量所需要遵守的协议
id<MyProtpcol1, MyProtpcol2,...> obj = @"123";//如果右值未遵守协议,编译器会报警告

分类与延展

分类 category

使用分类,OC允许将一个类的方法分散地写在多个头文件和源文件中。

  • 分类不能直接访问本类的私有成员变量,只能通过公开的getter和setter方法访问

  • 分类只能增加方法,不能增加属性

  • 分类使用@property关键字,只会生成对应getter和setter方法的声明,不会生成私有变量和方法的实现;可以通过这个方法修改对应属性在分类中的参数,如readonly改为readwrite

  • 要使用分类中的方法,需要引入分类的头文件

  • 当本类与分类、多个分类中具有同名的方法,优先调用最后编译的分类中的方法(哪怕没有引入对应头文件)

分类常用于

  • 对已有的类进行完善补充;补充系统类时,称为非正式协议

  • 对一个庞大臃肿的类进行拆分,增强其可读性

//本类名+分类名.h
@interface 本类名 (分类名)
//分类中的方法声明
@end
//---------------------------
//本类名+分类名.m
@implementation 本类名 (分类名)
//分类中的方法实现
@end

延展 extension

延展可以看做是特殊的分类,与本类共享一个实现。与分类不同,延展可以声明属性和方法,使用@property关键字时可以生成全套内容。

延展可以单独写在一个.h文件中,但一般写在本类的.m源文件中,用于声明私有的属性和方法。

延展可以继承协议,此时协议中的方法是私有的。

//本类名.m
@interface 本类名 ()
@property(nonatomic) privateProperty;
@end

@implementation
//...
@end

延展一般不会和分类一起使用,因为分类中不允许声明属性,延展起不到效果。


OC runtime

OC的反射

Class对象

OC是动态类型,可以通过反射机制在运行过程中获取对象的类、方法等。

  • 任何实例对象都包含一个 isa 指针,指向自己所属的类。

  • OC 的类本身也是一个对象,存储在代码段中,也有一个 isa 指针,指向自己的 meta-class。

  • meta-class 的 isa 指针指向父类的 meta-class。

其中:

  • 类对象用于存储实例方法、成员变量信息、协议信息等。

  • meta-class 用于存储类方法。

isa指针与superclass指针

Person *p1 = [Person new];
[Person class];	//返回的就是Person本身
[p1 class];		//返回p1的isa指针,即Person

SEL方法以及消息机制

OC 的方法本身是以 SEL 对象的形式存储在代码段中,又称为 selector 选择器,使用 @selector(方法名) 获取。

由于OC的方法调用使用了消息传递机制,所以方法可以看做是一条消息。

[p1 setAge:age];
//手动发送SEL消息
SEL sel = @selector(setAge:);//名为setAge:的方法
[p1 performSelector:sel];
/*
1. 系统获取`@selector(setAge:)`SEL消息对象;
2. 将SEL消息发送给p1对象,如果p1对象是nil,则不发生任何事;
3. p1根据isa指针找到对应的类对象;
4. 在类对象中查找有没有匹配的方法,有则执行,没有则向父类递归查找;
5. 如果最终都没有找到对应的方法,则报错。
*/

动态类型

动态类型与万能指针id

OC的编译器在编译过程中的类型检查并不严格,指针可以随意赋值,但是运行中可能会报错。

OC提供了万能指针id,id类型的指针可以指向任意类型的实例,可以避开编译器检查。

typedef struct objc_object *id;

使用id的注意事项:

  • id类型的指针不能使用点语法,要么进行类型转换,要么直接调用getter或setter。

  • 在必要时候需要做动态类型判断,以避免崩溃。

NSObject *类型的指针也可以指向任意对象,但是编译器会进行语法检查,可能会报警告甚至错误,而id类型不会报任何警告。使用NSObject *类型的指针需要进行强制类型转换。

动态类型检查

在处理id类型的指针时,应当先进行类型检查,避免出错。

- (BOOL)respondsToSelector:(SEL);			//判断对象是否有对应的方法
+ (BOOL)instancesRespondToSelector:(SEL);	//判断类是否有对应的类方法
- (BOOL)isMemberOfClass:(Class);			//判断一个实例是否属于某个类
- (BOOL)isKindOfClass:(Class);				//判断一个实例是否属于某个类或其子类
+ (BOOL)isSubclassOfClass:(Class);			//判断一个类是否是另一个类的子类
- (BOOL)conformsToProtocol:(Protocol *);	//判断一个实例是否遵守某个协议

多线程

OC中提供了四套多线程的解决方案,分别是基于C语言的pthread、GCD和基于OC语言的NSThread、NSOperation。此处仅介绍GCD。

注意:UIKit提供的类是线程不安全的,所以尽量不要在子线程刷新UI。

同步锁

OC提供了@synchronized关键字以实现线程同步和资源的互斥访问。

@synchronized(token) {//token应当是不同线程共有的资源,一般直接写self
	//互斥代码段,应尽量保证该部分尽量精简
}

GCD 中心调度

GCD (Grand Central Dispatch) 是苹果为多核并行运算提出的解决方案,能够自动管理线程的生命周期(线程池),并充分利用多核心。

GCD以队列的形式来组织任务,遵循先进先出原则。

  • 串行队列:串行队列中的任务将被顺序执行,前一个任务完成之后才执行下一个任务

  • 并发队列:允许开辟多线程,并发地执行队列中的任务,仅在异步执行时有效

  • 同步执行:在当前线程中执行任务,不会开启新线程

  • 异步执行:在新线程中执行任务

//创建一个队列,不常用
//第一个参数是队列名
//第二个参数是队列类型:DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_CONCURRENT
dispatch_queue_t mySerialQueue = dispatch_queue_create("newQueue", 0);

//直接获取现有的全局队列,是一种特殊的并行队列
//第一个参数是优先级,默认DISPATCH_QUEUE_PRIORITY_DEFAULT = 0
//第二个参数没有意义,写0
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

//串行队列 + 同步执行:在本线程顺序地执行代码,没啥用
dispatch_sync(mySerialQueue, ^{
	//同步执行的代码
});

//串行队列 + 异步执行:开启一个新线程,顺序地执行代码
dispatch_async(mySerialQueue, ^{});

//并发队列 + 异步执行:随机并发,常用
dispatch_async(queue, ^{
    //异步执行的代码
})

//并发队列 + 同步执行:不开新线程,与串行队列 + 同步执行效果相同,没用
dispatch_sync(queue, ^{});

主队列

是一种特殊的串行队列,运行在主线程上。主线程会在本线程上代码执行完毕后再执行主队列上的任务。

//获取主队列,
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//当要给主队列添加任务时(一般是刷新UI),不能在主线程上串行地添加任务,否则会因为相互等待而发生死锁
//解决方法是开一个新线程,在新线程上给主队列添加任务
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_sync(dispatch_get_main_queue(), ^{
		//给主队列添加的任务
	});
})

延时执行

GCD提供了dispatch_after()函数,在指定的时间将任务追加到指定的队列中。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);	//线程启动的时间
dispatch_after(time, dispatch_get_main_queue(), ^{
    //需要延迟执行的内容,如刷新UI
});

单例模式

GCD提供了dispatch_once()函数,是一个线程安全的单例模式书写方法。

//Singleton.h
@interface Singleton : NSObject
//首先要先禁用一系列公开方法
+(instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));

+ (Singleton *)shareInstance;
@end

//----------------------
//Singleton.m
@implementation Singleton
//获取单例对象
+ (Singleton *)shareInstance {
    static Singleton * singleton = nil ;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (singleton == nil) {
            singleton = [[Singleton alloc] init];
        }
    });
    return (Singleton *)singleton;
}
@end

评论区