OC学习记录

2022/2/21 OCiOS

记录一些函数和语法。。。

# 一些基础 C 函数

# sleep

用于等待指定的秒数,在unistd.h中声明

# atoi

获取一个字符串后将它转换成整数,如果不能转换,就返回 0。

int num = atoi("23");
1

# modf

返回浮点数的整数部分和小数部分,整数部分通过引用传递。

double pi = 3.14;
double integerPart;
double fractionPart = modf(pi, &integerPart);
printf("%.0f, %.2f\n", integerPart, fractionPart);
1
2
3
4

# time 和 localtime_r

time函数返回英国格林尼治时间从 1970 到当前累计时间。localtime_r函数可以读取当前秒数。localtime_r有两个形式参数,分别是 time_t 类型变量的地址和 struct tm 类型变量的地址。

 long t = time(NULL);
 printf("%ld\n", t);
 struct tm now;
 localtime_r(&t, &now);
 printf("The time is %d:%d:%d\n", now.tm_hour, now.tm_min, now.tm_sec);
1
2
3
4
5

# OC 部分

# import 指令

这是一个预处理指令,是 include 的增强版,将文件的内容在预编译的时候拷贝写指令的地方。同一个文件无论 import 多少次,只会包含一次。

# NSLog

printf 增强版,用于输出一些调试信息。

  • 执行这段代码的时间
  • 程序的名称
  • 进程编号
  • 线程编号
  • 输出的信息

# OC 程序编译

使用编译器将源代码编译成目标文件

cc -cxx.m
1
  • 预处理
  • 检查语法
  • 编译

链接

cc xx.o
1

如果程序中使用了框架中的函数或者类,那么在链接的时候,就必须要告诉编译器,去框架中寻找那个类。

cc xx.o -framework 框架名称
1

链接成功之后,会生成一个 a.out 可执行文件。

# 内存中的五大区域

  • 栈 存储局部变量
  • 堆 手动申请的字节空间 malloc、calloc、realloc
  • BSS 段 存储未被初始化或初始化为 0 的全局变量 静态变量
  • Data 段(常量段) 存储已经被初始化的全局、静态变量 常量数据
  • 代码段 存储代码

# id

当声明指向对象的指针时,通常都会明确地写出相应对象的类:

NSDate *expiration;
1

当声明的指针不知道所指对象的准确类型时,可以使用 id 类型,id 类型的含义是:可以指向任意类型 Objective-C 对象的指针。id 已经隐含了星号的作用。

id delegate;
1

# NSObject 和 id 的区别

相同点:万能指针 都可以指向任意的 oc 对象

不同点: 通过 NSObject 指针去调用对象方法的时候,编译器会做编译检查。通过 id 类型去调用对象方法的时候,编译器会直接通过。

注意:id 指针只能调用对象的方法,不能使用点语法,如果使用点语法就会直接编译错误。

# NSString

# 创建字符串实例

NSString *lament = @"Why me?";
1

@"..."是 OC 中的一个缩写,代表根据给定的字符串创建一个NSString对象。这种缩写为字面量语法。

# 创建动态字符串

NSString *dateStr = [NSString stringWithFormat: @"The date is %@", now];
1

# NSString 方法

NSString *str = @"Hello World";
// 获取长度
NSUInteger count = [str length];
NSLog(@"%lu", count);
NSString *temp = @"123";
// 判断字符串是否相同
if([temp isEqualToString: str]) {
  NSLog(@"equal");
}
// 返回原字符串大写版本
NSString *upper = [str uppercaseString];
NSLog(@"%@", upper);

NSString* str = @"test\n123\n";
NSArray *array = [str componentsSeparatedByString:@"\n"];
for (NSString* str in array) {
  NSLog(@"%@", str);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# NSArray/NSMutableArray

创建 NSArray,使用字面量语法。

NSDate *now = [NSDate date];
NSDate *tomorrow = [now dateByAddingTimeInterval:24.0*60.0*60.0];
NSDate *yesterday = [now dateByAddingTimeInterval:-24.0*60.0*60.0];
NSArray *dateList = @[now, tomorrow, yesterday];

NSLog(@"%@", dateList[0]);
// 获取数组中的元素个数
NSLog(@"%@", [dateList count]);
1
2
3
4
5
6
7
8

NSArray 实例是无法改变的,一旦被创建,就无法添加或者删除。

创建 NSMutableArray

NSMutableArray *dateList = [NSMutableArray array];
[dateList addObject: now];
[dateList addObject: tomorrow];
[dateList insertObject: yesterday atIndex: 0];
[dateList removeObjectAtIndex: 0];
1
2
3
4
5

# 快速枚举

for (NSDate *d in dateList) {
    NSLog(@"%@", d);
}
1
2
3

# 类加载

  • 当某个类第一次被访问时,会被加载到代码段,这个过程叫类加载

  • 执行方法的 4 步:

    1. 将方法加载到代码段
    2. 声明方法的参数到栈
    3. 把实参的值赋值给形参
  • 类一旦被加载到代码段之后,是不会被回收的,除非程序结束。

# 对象在内存中的存储

Person *p1 = [Person new];
1
  1. Person *p1; 会在栈中申请一块空间。在栈内存中声明一个 Person 类型的指针变量 p1。
  2. [Person new]; 真正在内存中创建对象的是这句代码。new 做了下面几件事情。
  1. 在堆内存中申请一块合适大小的空间。
  2. 在这个空间中根据类的模版创建对象。类模版中定义了哪些属性,就把这些属性依次声明在对象之中。对象中还有另外一个属性,叫做isa,是一个指针,它指向对象所属的类在代码段中的地址。
  3. 初始化对象的属性,如果属性的类型是基本数据类型,那么就赋值为 0,如果属性的类型是 C 语言的指针类型,就赋值为 NULL,如果属性的类型是 OC 的类指针类型,那么就赋值为 nil。
  4. 返回对象的地址

注意事项

  • 对象中只有属性,而没有方法,自己类的属性外加 1 个 isa 指针指向代码段中的类。
  • 如何访问对象的属性 指针名 -> 属性名 根据指针,找到指针指向的对象,再找到对象中的属性类访问。
  • 如何调用方法 [指针名 方法名]; 先根据指针名找到对象,对象发现要调用方法,再根据对象的 isa 指针找到类,然后调用类里的方法。

# self

  1. 当类第一次被访问时,会将类的代码存储在代码区,代码区中用来存储类的空间也有 1 个地址
  2. 在类方法中,self 也是一个指针,指向这个类在代码段中的地址,self 在类方法中就相当于这个类
  3. 可以查看 isa 指针的值,或者在类方法中查看 self 的值,调用对象的class方法,就会返回这个对象所属的类在代码段中的地址。调用类的class方法,也会返回这个类在代码段中的地址。
  4. 可以在类中的类方法里通过 self 调用其他的类方法

# 子类的存储

  • 子类对象中有自己的属性和所有父类的属性
  • 代码段中的每一个类都有一个isa指针,这个指针指向它的父类,一直指向NSObject

# 类的本质

在代码段中存储类的步骤:

  1. 先在代码段中创建 1 个 Class 对象,Class 是 Foundation 框架中的 1 个类。这个 Class 对象就是用来存储类信息的。
  2. 将类的信息存储在这个 Class 对象之中。这个 Class 对象至少有 3 个属性,类名:存储的这个类的名称,属性:存储的这个类具有哪些属性,方法:存储的这个类具有哪些方法。

所以。类是以 Class 对象的形式存储在代码段的。存储类的这个 Class 对象我们也叫做类对象。用来存储类的 1 个对象。所以,存储类的类对象也有 1 个叫做 isa 指针的属性这个指针指向存储父类的类对象.

Class c1 = [Person class];
NSLog(@"c1 = %p", c1);

Person *p = [Person new];
Class c2 = [p class];
NSLog(@"c2 = %p", c2);
1
2
3
4
5
6
  • 调用类的类方法class,可以得到存储类的类对象的地址。
  • 调用对象的对象方法 class,就可以得到存储这个对象所属的类的 Class 对象的地址。
  • 对象中的 isa 指针的值其实就是代码段中存储类的类对象的地址。

Class 指针在 typedef 的时候已经加了*,不需要在定义的时候加*。

# 类对象的使用

使用类对象调用类的方法

只能调用类方法,因为类对象就等价于存在其中的类。

// c1就代表Person类
Class c1 = [Person class];
[Person sayHi];
[c1 sayHi];
1
2
3
4

使用类对象来调用new方法

Person *p1 = [Person new];
Class c1 = [Person class];
Person *p2 = [c1 new];
1
2
3

# 方法的本质

SEL全程叫做selector选择器,SEL是一个数据类型,需要在内存中申请空间存储数据。SEL 其实是一个类,SEL 对象用来存储一个方法的。

方法的存储:

  1. 先创建 1 个 SEL 对象
  2. 将方法的信息存储在这个 SEL 对象之中
  3. 再将这个 SEL 对象作为类对象的属性
SEL fun = @selector(sayHi);
NSLog(@"sel = %p", fun);
1
2

方法的调用

[Person sayHi];
1
  1. 先拿到存储 sayHi 方法的 SEL 对象,也就是拿到存储 sayHi 方法的 SEL 数据,SEL 消息。
  2. 将这个 SEL 消息发送给 p1 对象
  3. 这个时候,p1 对象接收到这个 SEL 消息以后就要调用的方法。
  4. 根据对象的 isa 指针找到存储类的类对象
  5. 找到这个类以后,在这个类对象中去搜索是否有和传入的 SEL 数据相匹配的。如果有就执行,如果没有再找父类,直到 NSObject。

OC 最重要的机制,消息机制,调用方法的本质其实是为对象发送 SEL 对象。

# 手动为对象发送消息

  1. 先得到 SEL 数据
  2. 将这个 SEL 消息发送给 p1 对象
  3. 调用对象的performSelector方法,结果和[p sayHi]一致。
Person *p = [Person new];
[p sayHi];
[p performSelector:fun];
1
2
3

带参数函数调用

SEL fun = @selector(setName:);
Person *p = [Person new];
NSString *name = @"jack";
[p performSelector:fun withObject: name];
NSLog(@"name = %@", [p name]);
1
2
3
4
5

# instancetype

+ (instancetype) person {
    return [self new];
}
1
2
3

instancetype 只能作为方法的返回值,不能在别的地方使用。如果写成具体的类型,那么子类集成时,返回的就是父类的指针。

# isKindOfClass / isMemberOfClass

+ (BOOL)respondsToSelector:(SEL)aSelector; // 判断对象中是否有这个方法可以执行
+ (BOOL)instancesRespondToSelector:(SEL)aSelector; // 判断类中是否有指定的类方法
- (BOOL)isKindOfClass:(Class)aClass; // 判断是否是某类的对象或子类对象
- (BOOL)isMemberOfClass:(Class)aClass; // 判断是否是某类的对象,不包括子类对象
1
2
3
4

# 构造方法

new方法的内部,其实是先调用的alloc方法,再调用init方法。

Person *p1 = [Person new];
Person *p2 = [[Person alloc] init];
1
2

init方法:

初始化对象,为对象的属性赋初始值,这个init方法我们叫做构造方法。

# 重写init方法的规范

  1. 必须先调用父类的 init 方法,然后将方法的返回值赋值给 self
  2. 调用 init 方法可能会失败,如果初始化失败,返回的是 nil
  3. 如果初始化成功,基于初始化当前对象的属性
  4. 最后返回 self 的值
- (instancetype) init
{
  self = [super init];
  if (self != nil) {
    self.name = @"jack";
  }
  return self;
}
1
2
3
4
5
6
7
8

# 自定义构造方法

- (instancetype) initWithName: (NSString *)name andAge:(int) age
{
  if (self = [super init]) {
    self.name = name;
    self.age = age;
  }
}
1
2
3
4
5
6
7

# 内存管理

栈:局部变量,当局部变量的作用域被执行完毕之后,这个局部变量就会被系统立即回收。

堆:内存管理

BSS 段:未初始化的全局变量、静态变量,一旦初始化就回收,并转存到数据段中。

数据段:已经初始化得到全局变量、静态变量,直到程序结束才会被回收

代码段:代码、程序结束的时候,系统会自动回收存储在代码段中的数据。

栈、BSS 段、数据段、代码段中的数据,都是由系统自动完成回收。

# 引用计数

每一个对象都有 1 个属性,叫做retainCount,类型是 unsigned long 占 8 个字节。引用计数器的作用,用来记录当前这个对象有多少个人在使用。默认情况下,创建一个对象出来,它的计数器默认为 1。当这个对象的引用计数器变为 0 时,系统会自动回收这个对象。

Person *p = [[Person alloc] init];
NSLog(@"count = %lu", [p retainCount]); // count = 1
1
2

具体操作:

  • 当对象发送一条 retain 消息,对象的引用计数器就会加 1
  • 当对象发送 1 条 release 消息时,对象的引用计数器减 1
  • 当对象的引用计数器变为 0 的时候,对象会被系统立即回收,在被回收的时候,会被调用对象的 dealloc 方法

内存管理分为MRCARC

MRC:Manual Referen Counting 手动引用计数

ARC:Automatic Reference Counting 自动引用计数

# MRC

默认开启 ARC,需要将 Build Setting 中的 ARC 关闭之后,才能使用 MRC。具体操作见下图。

mrc

实现一下类的dealloc方法。

- (void)dealloc {
    NSLog(@"dealloc");
    [super dealloc];
}
1
2
3
4

在主函数中调用相关方法。

Person *p = [[Person alloc] init];
NSLog(@"count = %lu", [p retainCount]); // count = 1
[p retain];
NSLog(@"count = %lu", p.retainCount); // count = 2
[p release];
[p release];
// dealloc
1
2
3
4
5
6
7

release消息并不是回收对象,而是让对象的引用计数器-1,当引用计数器为 0 的时候,才会回收对象。回收只是意味着对象所占用的空间可以分配给别人,但是空间里存储的数据还在。

Person *p = [[Person alloc] init];
[p release]; // retainCount = 0, dealloc
[p sayHi]; // 可能还可以正常运行
1
2
3

# 野指针和僵尸对象

OC 中的野指针:指针指向的对象已经回收了,这样的指针叫做野指针。

僵尸对象:1 个已经释放的对象,但是这个对象所占的空间还没有分配给别人,这样的对象叫做僵尸对象。

当我们通过野指针去访问僵尸对象的时候,有可能有问题,也有可能没有问题。

当僵尸对象占用的空间分配给了别人使用的时候,就不可以。

# 内存泄漏

指的是 1 个对象没有被及时的回收,在该回收的时候而没有被回收,一直驻留在内存中,直到程序结束的时候才回收。

单个对象内存泄漏的情况:

  • 有对象创建,没有对应的 release
  • retain 和 release 的次数不匹配
  • 在不适当的时候,为指针赋值为 nil
  • 在方法当中为传入的对象进行不适当的 retain

如何确保单个对象可以被回收:

  • 有对象的创建,就必须要匹配 1 个 release
  • retain 次数和 release 次数一定要匹配
  • 只有在指针称为野指针的时候才能赋值为 nil
  • 在方法当中不要随意为传入的对象 retain

# OC 属性

@property (nonatomic, retain) NSString *name;
@property (nonatomic, assign) int age;
1
2
  • assign: 默认值,生成的 setter 方法的实现就是直接赋值
  • retain:生成的 setter 方法的实现是标准的 MRC 内存管理代码,也就是,先判断新旧对象是否为同 1 个对象,如果不是,release 旧的,retain 新的。

当属性的类型是 oc 对象类型的时候,使用 retain,否则用 assign

注意:

retain 参数,只是生成标准的 setter 方法为标准的 MRC 内存管理代码,不会自动的在 dealloc 中生成 release 方法,所以我们还要自己手动在 dealloc 中 release。

readwrite 和 readonly

readwrite 是默认值,代表同时生成 getter、setter

readonly:只会生成 getter

@property (nonatomic, assign, getter=xxx, setter=xyz) int age; // 指定getter setter方法名
1
# 自动释放池

存入自动释放池中的对象,在自动释放池被销毁的时候,会自动调用存储在该自动释放池中的所有对象的 release。

# ARC

在 ARC 机制下,当没有任何类型的强指针指向对象的时候,这个对象就会被立即回收。

  • 指向对象的强指针被回收
  • 指向对象的强指针被赋值为 nil

在 ARC 下,property 不可以添加 retain 参数,属性默认是 strong。

# 分类

  • 在分类中可以写@property,但是不会自动生成私有属性,也不会自动生成 getter、setter 的实现,只会生成 getter、setter 的声明,需要自行实现

  • 在分类的方法实现中不可以直接访问本类的真私有属性(定义在 implementation 中),但是可以调用本类的 getter、setter 来访问属性

  • 当分类中有和本类同名的方法的时候,优先调用分类的方法,哪怕没有引入分类的文件,如果多个分类中有同名的方法,那么会调用最后编译的。

@interface Person (grow)

@end
1
2
3

# 延展

@interface Person ()

@end
1
2
3

本质上是一个分类,作为本类的一部分,只有声明,没有单独的实现,和本类共享一个对象。

延展和分类的区别:

  • 分类有名字,延展没有,是一个匿名的分类
  • 每一个分类都有单独的声明和实现,而延展只有声明,没有单独的实现,和本类共享一个实现
  • 分类中只能新增方法,而延展中任意的成员都可以写
  • 分类中可以写@property,但是只会生成 getter、setter 的声明,延展中写@property 会自动生成私有属性,也会生成 getter、setter

使用场景:

  • 要为类写一个私有的@property,生成的 getter、setter 方法只有在类的内部访问,不能在外部访问
  • 延展 100%不会独占 1 个文件,都是将延展写在本类的实现文件中。这个时候,写在延展中的成员,就相当于这个类的私有成员,只能在本类的实现中访问。
  • 将私有方法声明在延展中,在本类中实现,提高代码的阅读性
  • 如果类的成员只希望在类内部访问,定义在延展中,希望在外部访问的话,定义在@interface 中

# block

block 是一个数据类型,block 类型的变量中专门存储 1 段代码,这段代码可以有参数,也可以有返回值

void (^myblock) (void) = ^{
    NSLog(@"myblock");
};
int (^myblock1) (void) = ^int {
    NSLog(@"myblock1");
    return 1;
};
int (^myblock2) (int, int) = ^int(int num1, int num2) {
    return num1 + num2;
};
1
2
3
4
5
6
7
8
9
10

可以使用 typedef 来简化 block 的声明。

typedef void (^VoidBlock) (void);
VoidBlock v = ^void {
  NSLog(@"myblock");
};
1
2
3
4

获取/修改外部变量

  • 在 block 中可以获取外部局部变量和全局变量的值
  • 在 block 代码内部可以修改全局变量的值,但是不能外部局部变量的值
  • 给局部变量添加__block修饰符,就可以在 block 中修改
__block int num = 0;
int (^myblock2) (int num1, int num2) = ^int(int num1, int num2) {
  num++;
  return num1 + num2;
};
1
2
3
4
5

block 作为函数参数

typedef void (^VoidBlock) (void);
void test(VoidBlock testBlock) {
    testBlock();
}

test(^{
  NSLog(@"Hello");
});
1
2
3
4
5
6
7
8

block 作为函数返回值

VoidBlock fun() {
  VoidBlock res = ^{
    NSLog(@"Hello World");
  }
  return res;
}
1
2
3
4
5
6
Last Updated: 2022/7/6 00:42:53