内存-①基础

①基础

[toc]

知识架构

iOS知识库

Android知识库

目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1、几个本质
>
2、空指针和未初始化的指针
(1)、空指针和未初始化的指针的区别:
(2)、为什么指针变量定义时一定要初始化?
>
3、野指针与悬空指针
(1)、指针与内存的常见使用顺序
(2)、内存泄漏的概念
(3)、野指针概念
(4)、在iOS中野指针的后果
(5)、分析野指针的产生原因及解决办法
>
4、iOS NSerror 用双重指针理解
>
5、指针和引用的区别
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1、堆和栈的区别?
>
2、浅拷贝和深拷贝的区别
>
3、分析NSString、NSMutableString等类的copy、mutableCopy
(1)、分别对NSString、NSMutableString进行copy、mutableCopy生成的类型是什么?
(2)、分别对NSString、NSMutableString进行copy、mutableCopy操作,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
(3)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
(4)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSMutableString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
(5)、自己代码实现copy修饰符,应该怎么写????
>
4、NSCoding和NSCopy
(1)、NSCoding的作用
(2)、NSCopy
>
5、@synthesize和@dynamic区别
>
1
2
3
4
5
6
7
8
9
10
11
12
13
1、内存管理
(1)、在ObjC中,对象什么时候会被释放(或者对象占用的内存什么时候会被回收利用)?
(2)、那怎么知道对象已经没有被引用了呢?
(3)、autorelease、autoreleasepool(自动释放池)
(4)、autoreleasepool(自动释放池) 
(5)、autoreleasepool里面对象的内存什么时候释放?
(6)、runloop、autorelease pool以及线程之间的关系
(7)、自动释放池怎样创建
(8)、自动释放池使用注意
(9)、自动释放池的应用/什么时候要用@autoreleasepool
>
2、如何监测内存泄漏
>
1
2
自动释放池底层怎么实现?
>

常用数据类型占用内存大小

64位编译器

char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 8个字节
long long: 8个字节
unsigned long: 8个字节

一、指针

< 返回目录

1、几个本质

1
2
3
4
5
6
7
8
9
10
11
12
13
数据类型:
数据类型的本质是固定大小内存的别名。
对变量声明数据类型,是为了告诉编译器分配几个字节的内存。

变量:
变量的本质是一段内存空间的别名。
也就是给一段内存空间取一个新的名字,就是变量。

指针:
指针也是一种数据类型,它的值是某一个内存空间的地址。
指针的步长根据它指向的内存空间的数据类型而定。

数组中[]的本质:假如有数组array,则array[i]等价于*(array+i),是因为[]对于程序员来讲是友好的,但是编译器最终还是要将它理解为指针,也就是数组作为函数参数时的退化。array[i] ==> array[0+i] ==>*(array+i)

2、空指针和未初始化的指针/野指针

(1)、空指针和未初始化的指针的区别:

①空指针可以确保不指向任何对象或函数;
②未初始化指针则可能指向任何地方,即它所指向的地址就是随机的,也就说此时它是个野指针。(附:如果一个指针的指向对象后来被删除,却未置为空指针nil,则它也是野指针)
所以空指针在概念上不同于未初始化的指针。
对于malloc在其内存分配的时候,如果内存分配成功,返回的一定不是空指针;但是如果malloc内存分配失败,返回的空指针。而不是一个未初始化的指针。

以下是华为笔试题:

1
2
3
4
5
下面有关空指针和未初始化指针,说法错误的是?
A.对0x0这个地址取值是非法的
B.空指针可以确保不指向任何对象或函数; 而未初始化指针则可能指向任何地方。
C.空指针与任何对象或函数的指针值都不相等
D.malloc在其内存分配失败时返回的是一个未初始化的指针

错误答案是D,因malloc内存分配失败,返回的是空指针。详细请查看原文地址
华为笔试:下面有关空指针和未初始化指针,说法错误的是?

(2)、为什么指针变量定义时一定要初始化?

答:因为你首先要理解一点.内存空间不是你分配了才可以使用,只是你分配了之后使用才安全。

为什么要进行对他初始化呢,因为如果你没对它初始化,那么这个指针所指向的地址就是随机的,即此时它是个野指针。这时候如果你引用这个指针并对它做了修改这个指针所指向的内容的操作的话,如果刚好这个指针所指向的内容恰好是另外一个程序的数据的话,那么你原本随意的一个修改,就造成了对另一个程序的数据的修改了,也就会导致另外一个程序可能不能正常运行了。所以使用前一定要进行初始化。

3、野指针与悬空指针

在C/C++等语言中,

悬空指针(Dangling Pointer)指的是:一个指针的指向对象已被删除,那么就成了悬空指针。

野指针是那些未初始化的指针

有时也把野指针和悬空指针通称悬空指针。
而好像在iOS中是通称为野指针。

以下内容摘自:百度百科:迷途指针

在计算机编程领域中,迷途指针,或称悬空指针、野指针,指的是不指向任何合法的对象的指针。

当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称迷途指针。

若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,则将产生无法预料的后果。因为此时迷途指针所指向的内存现在包含的已经完全是不同的数据。通常来说,若原来的程序继续往迷途指针所指向的内存地址写入数据,这些和原来程序不相关的数据将被损坏,进而导致不可预料的程序错误。

这种类型的程序错误,不容易找到问题的原因,通常会导致存储器区块错误(Linux系统中)和一般保护错误(Windows系统中)。如果操作系统的内存分配器将已经被覆盖的数据区域再分配,就可能会影响系统的稳定性。

某些编程语言允许未初始化的指针的存在,而这类指针即为野指针。野指针所导致的错误和迷途指针非常相似,但野指针的问题更容易被发现。

(1)、指针与内存的常见使用顺序

在堆中申请了一块内存,并用一个指针指向它。
一般我们都会在不用的时候先释放该指针指向的内存,再将该指针置为空指针。
即一般正确的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//指针变量和指针所指向的内存变量是两个不同的概念
//使用动态内存分为三步
//1.定义时,将指针为定义NULL
//2.释放内存时,把指针变量重新赋值或者NULL
//3.释放内存后,把指针变量赋值为NULL

#include <stdio.h>

int main02()
{
int *p = NULL;
p = (int *)malloc(4);

if (p != NULL){
free(p);//释放P所指向的内存空间,但指针变量p仍然留在栈中,成为了野指针
p = NULL;//释放野指针(这是关键,记得释放指针所指向的内存空间后,要释放野指针)
}

return 0;
}

但是
①、如果我们在未来始终没有去手动释放掉我们开辟的内存的话,会导致内存泄漏;
②、如果释放掉了该内存,却忘了同时释放只想该内存的指针,会导致产生悬空指针或者说是迷途指针,或者有人也称是野指针。
③、如果记得释放指针,却忘了释放指针只想的内存(即记得②忘了①),那么由于指针已经消失,而指针指向的东西还在,那么久永远无法控制这块内存,而导致一定内存泄漏了。

(2)、内存泄漏的概念

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

(3)、野指针概念

C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指针 指向1块随机的内存空间。

OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.

野指针:指向内存被释放的内存或者没有访问权限的内存的指针

更详细的概念可查看:百度百科——野指针

(4)、在iOS中野指针的后果

野指针的后果:崩溃EXC_BAD_ADDRESS

(5)、分析野指针的产生原因及解决办法

知识点:任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

“野指针”的成因主要有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1)指针变量没有被初始化。(即随机只想的这个指针很有可能只想一块没人用的内存,)
char *p; //此时p为野指针
//正确的为在声明之后加上 p=new char[10];我们常直接写为一行,即char *p=new char[10];

2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针.
char *p=new char[10]; //指向堆中分配的内存首地址,p存储在栈区
cin>> p;
delete []p; //p重新变为野指针
//正确的应该在指针p被free或者delete之后,加上p = Null;

3)指针操作超越了变量的作用范围。
char *p=new char[10]; //指向堆中分配的内存首地址
cin>> p;
cout<<*(p+10); //可能输出未知数据

iOS 关于僵尸对象和僵尸指针的那些事儿

4、iOS NSerror 用双重指针理解

常见代码如下:

1
2
3
4
5
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtPath:absoluteFilePath error:&error];
if (error) {
NSLog(@"删除单个文件的时候出现错误:%@",error.localizedDescription);
}

可见,如果我们不是传指针的指针&error,而是传error,那么if(error)中的error就肯定是nil了。那就没用了。

所以这里传error的指针的原因可简单概括为:
因为我们要得到一个新的error值。所以如果有方法

1
2
3
4
5
- (NSError *)getNewErrorForremoveItemAtPathremoveItemAtPath:(NSString *)path  error:(NSError **)error {
NSError *newError = [NSError errorWithDomain:...];

return newError;
}

那么这边,我们的error,就可以直接传error本身,甚至不传都是可以的。

但是实际是这些方法本身的返回值,已经被定义为判断能否进行某种操作,而不是操作是否成功给占用了,如这边已经被判断能否进行删除文件给占用了,所以如果我们还想知道这个删除文件操作结果的error,那就把error的指针的指针传进去,最后其出来的就是我们想要的。
所以,猜测其内部结构应该是

1
2
3
4
5
6
7
8
9
10
- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error {
BOOL canRemove = ...;
if (!canRemove) {
* error = [NSError errorWithDomain:...];
return NO;
} else {
//* error = * error; //即不变
return YES;
}
}

以下解释原因摘自:ios中处理错误为什么传递的是&error,而不是error?

因为 需要将error 传入后修改其值,然后再返回来,返回来后还要保证己经修改过了。

&error传入是传的地址引用,传入后处理函数直接访问变量的地址,可以修改其值再返回同一个地址, 调用函数就可以知道值是否有修改,即是否有错。

而error传入是传的值引用,值引用传入到程序栈中后其实是把原来的值复制了一份传过去,处理函数可以修改,但无法将改后的值传出函数体。

5、指针和引用的区别

(1)定义和性质的区别
①指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;
②而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
简单点说:一个是存地址,一个是变量别名
(2)指针和引用作为函数参数进行传递时的区别
①用指针传递参数,可以实现对实参进行改变的目的,是因为传递过来的是实参的地址
②引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

二、内存

< 返回目录

1、堆和栈的区别?

1
2
3
4
5
6
7
一、堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2、堆(操作系统):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

二、堆栈缓存方式区别:
1、栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

八、谈谈内存管理、内存泄露、循环引用

< 返回目录

ARC已经出来很久了,自动释放内存的确很方便,但是并非绝对安全绝对不会产生内存泄露。

1、内存管理

(1)、在ObjC中,对象什么时候会被释放(或者对象占用的内存什么时候会被回收利用)?

答案是:当对象没有被任何变量引用(也可以说是没有指针指向该对象)的时候,就会被释放。

(2)、那怎么知道对象已经没有被引用了呢?

ObjC采用引用计数(reference counting)的技术来进行管理:

1
2
3
4
1)每个对象都有一个关联的整数,称为引用计数器;
2)当代码需要使用该对象时,则将对象的引用计数加1;
3)当代码结束使用该对象时,则将对象的引用计数减1;
4)当引用计数的值变为0时,表示对象没有被任何代码使用,此时对象将被释放。

与之对应的消息发送方法如下:

对象操作 OC中对应的方法 引用计数的变化
当对象被创建时 alloc/new/copy/mutableCopy等 +1
持有对象 retain +1
释放对象 release -1
废弃对象 dealloc -
1
2
3
4
5
6
7
8
1)当对象被创建(通过alloc、new或copy/mutableCopy等方法)时,其引用计数初始值为1;
2)给对象发送retain消息,其引用计数加1;
3)给对象发送release消息,其引用计数减1;
4)当对象引用计数归0时,ObjC給对象发送dealloc消息销毁对象
当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1;
在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1;
需要释放一个对象时,就将该对象的引用计数减1;
直至对象的引用计数为0,对象的内存会被立刻释放。
1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

NSObject *object = [[NSObject alloc] init]; // 执行后,引用计数 = 1
NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x", (unsigned long)[object retainCount], object, &object);
self.property = object; // 执行后,引用计数 = 2
NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x \n property指针内存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
[object release]; // 执行后,引用计数 = 1
NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x \n property指针内存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
return YES;
}

当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

__weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。

2、如何监测内存泄漏

如果内存管理不当,势必会造成内存泄露。那我们如何快速的来找出内存泄露呢?以前我们可能会使用Instruments来监测,但是我们会发现使用Instruments特别繁琐,而且不一定能定位到内存泄露。

所以这里伟大的Facebook工程师们开源了一些自动化工具来解决监测内存泄露问题:FBRetainCycleDetector、FBAllocationTracker、FBMemoryProfiler。详情查看在iOS上自动检测内存泄露

你在开发大型项目时,如何进行内存泄露检测的?

instruments下有个leaks工具,启动此工具后,运行项目,工具里可以显示内存泄露的情况,双击可找到源码位置,可以帮助进行内存泄露的处理。

END

< 返回目录

1
2


内存-②循环引用

内存-②循环引用

[toc]

目录

1
2
3
4
5
6
7
8
9
10
1、Objective-C中block为何得用copy修饰,能否用其他
2、block的循环引用
3、block循环引用的解决
4、判断该block是否会发生循环引用
5、为什么masonry的block里引用self不需要weak?
6、是否所有的Block中,使用self 都会导致循环引用?
7、block修改外部局部变量
(1)、在block中无法直接修改外部变量的原因
(2)、解决如何在 block 中修改外部变量
>

一、循环引用原因

< 返回目录

导致iOS对象无法按预期释放的一个无形杀手是——循环引用。循环引用可以简单理解为A引用了B,而B又引用了A,双方都同时保持对方的一个引用,导致任何时候引用计数都不为0,始终无法释放。若当前对象是一个ViewController,则在dismiss或者pop之后其dealloc无法被调用,在频繁的push或者present之后内存暴增,然后APP就duang地挂了。

循环引用会导致内存泄露,因为循环应用会导致,有些对象没办法在已经不会再使用的时候被释放掉

下面列举我们变成中比较容易碰到的三种循环引用的情形:

1
2
3
4
5
6
(1)计时器NSTimer:✅__weak typeof(self) weakSelf = self;
循环引用:self -> timer -> block -> self
(2)block:✅copy
@property (nonatomic, copy) void (^myBlock)(void);
(3)委托delegate:✅weak
@property (nonatomic, weak) id<YourDelegateProtocol> delegate;

二、NSTimer的循环引用

< 返回目录

NSTimer的循环引用详情,我们放在下面讲解NSTimer(前面已经提过NSTimer 其实就是RunLoop中的CFRunLoopTimerRef,一个基于时间的触发器)的时候介绍。

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@interface HWWeakTimerTarget : NSObject

@property (nonatomic, weak) id target; // 注意是 weak
@property (nonatomic, assign) SEL selector; // assign
@property (nonatomic, weak) NSTimer* timer; // 注意是 weak

@end

@implementation HWWeakTimerTarget

- (void)fire:(NSTimer *)timer {
if(self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
#pragma clang diagnostic pop
} else {
[self.timer invalidate];
}
}

@end

@implementation HWWeakTimer

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
HWWeakTimerTarget *timerTarget = [[HWWeakTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
}

@end

三、委托delegate的循环引用

< 返回目录

在委托问题上出现循环引用问题已经是老生常谈了,声明delegate时请用weak(ARC),如果是MRC则用assign。千万别手贱。

四、block的循环引用

< 返回目录

1、Objective-C中block为何得用copy修饰,能否用其他

答:因为block在创建的时候,它的内存是分配在栈(stack)上,而不是在堆(heap)上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。所以,为了能够在block的声明域外也能够使用block,我们需要将block拷贝到堆上,所以使用copy属性。对于堆中的block,也就是copy修饰的block。他的生命周期是随着对象的销毁而结束的。只要对象不销毁,我们就可以调用的到在堆中的block。

Block 在创建时可能存储在不同的内存区域(栈或堆)中,而在将其赋值给对象或作为函数或方法的参数时,需要确保 Block 存储在堆中,以便在调用时能够正常执行。如果 Block 存储在栈中,则在函数或方法返回后,Block 对象所在的内存区域将被释放,从而导致程序崩溃或者产生其他未定义的行为。所以block应该被持有,即应该使用copy或者strong修改。那为什么我们还是习惯使用copy呢?因为使用 copy 可以提高线程安全性,因为每个线程可以独立地使用 block 的副本,而不必担心其他线程对原始 block 的修改。举例点击一个按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@interface MyClass : NSObject
@property (nonatomic, copy) void (^updateUIBlock)(void); // 使用 copy 属性
@end

@implementation MyClass

- (id)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self; // 使用 __weak 避免循环引用
self.updateUIBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf __updateUI];
});
}
};
}
return;
}
- (void)__updateUI {
// 更新 UI 的代码
NSLog(@"UI updated on main thread");
}

- (void)clickToUpdateUI {
// 执行异步任务,并在完成后调用 updateUIBlock
[self performBackgroundTaskWithCompletion:self.updateUIBlock];
}

// completion这个block如果是原block的副本,那么这里即使多个后台任务尝试同时更新 UI,每个任务都会有 `updateUIBlock` 的独立副本,不会相互干扰。这确保了 UI 更新操作的线程安全性。
- (void)performBackgroundTaskWithCompletion:(void (^)(void))completion {
// 模拟后台线程任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ... 执行一些后台操作 ...

// 任务完成后,调用 completion block
if (completion) {
completion();
}
});
}

@end

扩展:为什么NSString 可以使用strong和copy,却选择使用 copy?(个人理解:因为NSString是不可变的,既然不可变还不如使用copy再加一层线程安全)

使用 copy 属性修饰符通常有以下原因:

  1. 确保线程安全:如果你在多线程环境中操作字符串,使用 copy 可以确保每个线程都有自己的副本,从而避免线程安全问题。
  2. 避免外部修改:如果你不希望外部代码修改原始字符串,使用 copy 可以确保你的属性持有的是原始字符串的一个副本,外部代码对原始字符串的修改不会影响到你的属性。
2、block的循环引用

由于block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。所以,不管是在ARC还是非ARC环境下对block使用不当都会引起循环引用问题

一般表现为,某个类将block作为自己的属性变量(则该类就对block强引用了),然后该类在block的方法体里面又使用了该类本身。即当对象(比如self)拥有一个block属性的时候,在block属性中又引用了对象的其他成员变量或者调用了对象的其他方法。形成你中有我,我中有你,谁都无法将谁释放的困局。形如:

1
2
3
4
5
6
7
8
9
10
11
self.myBlock = ^{
[self doSomething];
};

又或者

ClassA* objA = [[ClassA alloc] init];
objA.myBlock = ^{
[self doSomething];
};
self.objA = objA;

block的这种循环引用会被编译器捕捉到并及时提醒。

以上参考自:Block的循环引用

3、block循环引用的解决
(1)、常规简单解法

解决方法,就一句话的事情:

1
2
3
4
5
6
7
8
__weak typeof (self) weakSelf = self; 
self.myBlock = ^{
[weakSelf doSomething];
};

附1:如果是non-ARC环境下就将__weak替换为__block即可。non-ARC情况下,__block变量的含义是在Block中引入一个新的结构体成员变量指向这个__block变量,那么__block typeof(self) weakSelf = self;就表示Block别再对self对象retain啦,这就打破了循环引用。

附2:__weak 是 iOS 5.0 推出的,_weak 相当于 weak,不会做强引用,如果对象被释放,执行的地址,会指向 nil
(2)、block中使用 weak–strong dance 技术避免循环引用
1
2
3
4
5
6
7
8
__weak typeof(self)weakSelf = self;
[header setTapHandle:^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"strongSelf = %@", strongSelf);
}
[weakSelf headerAction:header];
}];

精髓2:①在 block 之前定义对 self 的一个弱引用weakSelf,因为是弱引用,所以当 self 被释放时weakSelf会变为nil;② 在 block 中引用该弱应用,考虑到多线程情况,通过使用强引用 strongSelf 来引用该弱引用,这时如果 self 不为 nil 就会 retain self,以防止在后面的使用过程中 self 被释放;③在之后的 block 块中使用该强引用 bself,注意在使用前要对 bSelf 进行了 nil 检测,因为多线程环境下在用弱引用weakSelf对强引用strongSelf赋值时,弱引用weakSelf可能已经为nil了。通过这种手法,block 就不会持有 self 的引用,从而打破了循环引用。

iOS开发中在block中为什么要weak和strong配合使用

答:weak是为了解决循环引用。strong是为了防止block持有的对象提前释放。

1
2
3
4
5
6
7
8
9
10
11
 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self dismissViewControllerAnimated:YES completion:nil];

__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf);
});
};
self.block();
}

点击屏幕,当前控制器消失,同时被销毁掉,5秒后打印的weakSelf就是一个(null)。
而我们如果在block内使用__strong后就能保证再打印完strongSelf之后再释放当前控制器。

4、判断该block是否会发生循环引用

判断该block是否会发生循环引用例子

5、为什么masonry的block里引用self不需要weak?

这个就和网络请求里面使用self道理是一样的。因为UIView未强持有block,所以这个block只是个栈block,而且构不成循环引用的条件。栈block有个特性就是它执行完毕之后就出栈,出栈了就会被释放掉。看mas_makexxx的方法实现会发现这个block很快就被调用了,完事儿就出栈销毁,构不成循环引用,所以可以直接放心的使用self。

1
2
3
4
5
6
7
8
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

如果要强引用,block应该是masnory的一个属性,即被masnory对象持有。且是copy修饰符

6、是否所有的Block中,使用self 都会导致循环引用?

7、block修改外部局部变量

如果是全局变量呢?

(1)、在block中无法直接修改外部变量的原因

错误示例:

1
2
3
4
5
6
7
8
9
10
11
//声明一个局部整型变量 
int intValue = 3; //漏掉了__block修饰符

//声明一个返回值为int,一个int参数的block变量
int (^block)(int) = ^(int m){
intValue++;
return m * intValue;
};

//调用block变量,5作为参数之后的结果
NSLog(@"block(5) = %d",block(5));

在上面的例子中,我们编译程序后发现编译器会有红色错误,错误提示为
Variable is not assignable (missing __block type specifier)

为什么会出现不能被赋值的错误提示呢?

block在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,在 block 块内使用该只读拷贝。
那为了避免上述错误,就要精髓1:使用__block修饰符来修饰外部变量,用来通知编译器该外部变量intValue与block中的intValue指的是同一块儿内存地址,而不需要内存拷贝。

(2)、解决如何在 block 中修改外部变量

有两种办法
① 第一种是可以修改 static 全局变量;
② 第二种是可以修改用新关键字 __block 修饰的变量。

1
2
3
4
5
6
7
8
9
10
11
12
__block int blockLocal  = 100;
static int staticLocal = 100;

void (^aBlock)(void) = ^(void){
NSLog(@" >> Sum: %d\n", global + staticLocal);

global++;
blockLocal++;
staticLocal++;
};

aBlock();

附:静态变量 和 全局变量 在加和不加 __block 都会直接引用变量地址。也就意味着静态变量和全局变量的修改可以直接修改,不需要作添加__block的步骤。

在ARC下获取对象的引用计数值

在ARC下获取对象的引用计数值,可以使用CFGetRetainCount。

1
2
3
4
5
6
7
对于Core Foundation对象:
NSLog(@"CFGetRetainCount is %ld", CFGetRetainCount(aCFString));

对于Foundation对象
NSLog(@"CFGetRetainCount is %ld", CFGetRetainCount((__bridge CFTypeRef)aNNstring));

NSLog(@"%@ CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));

以下获取引用计数的方法错了,但是不知道正确的应该怎么用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)dealloc {
NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];

NSLog(@"%@ viewWillDisappear and CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));
}

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];

NSLog(@"%@ viewDidDisappear and CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));
}

几篇文章:

常见笔试/面试题

< 返回目录

block和weak修饰符的区别是什么?
1
2
3
4
1,在MRC时代,__block修饰,可以避免循环引用;ARC时代,__block修饰,同样会引起循环引用问题;
2,__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型;
3,__weak只能在ARC模式下使用,也只能修饰对象,不能修饰基本数据类型;
4,__block对象可以在block中被重新赋值,__weak不可以;

END

< 返回目录

①语言Swift

①语言Swift

[Toc]

一、Copy

  1. 如何让自己的类用copy修饰符

    若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyingNSMutableCopying协议。
    具体步骤:
    1.需声明该类遵从NSCopying协议
    2.实现NSCopying协议的方法,具体区别戳这里

  • NSCopying协议方法为:
1
2
3
4
5
- (id)copyWithZone:(NSZone *)zone {
MyObject *copy = [[[self class] allocWithZone: zone] init];
copy.username = self.username;
return copy;
}

1、浅拷贝和深拷贝的区别

浅层复制:只复制指向对象的指针,而不复制引用对象本身。
深层复制:复制引用对象本身。

意思就是说我有个A对象,复制一份后得到A_copy对象后,

对于浅复制来说,A和A_copy指向的是同一个内存资源,复制的只不过是是一个指针,对象本身资源还是只有一份,那如果我们对A_copy执行了修改操作,那么发现A引用的对象同样被修改,这其实违背了我们复制拷贝的一个思想。
而对于深复制就好理解了,内存中存在了两份独立对象本身。

通俗的话将就是:
浅复制好比你和你的影子,你完蛋,你的影子也完蛋;
深复制好比你和你的克隆人,你完蛋,你的克隆人还活着。

2、 iOS - Copy 与 MutableCopy

iOS - Copy 与 MutableCopy
①copy:因为copy默认返回的是不可变的,所以当我们对一个不可变的字符串进行copy的时候,我们只是拷贝了它的指针(浅拷贝)。当我们对一个可变的字符串进行拷贝的时候,因为类型转变了,我们需对其进行深拷贝。
mutableCopy:默认返回的是一个可变的对象,适用于可变的对象,例如NSMutableString,NSMutableArray,NSMutableDictionary、etc。 无论对于可变的字符串还是不可变的字符串进行mutableCopy,系统都默认进行深拷贝。

其他参考

(1)、copy到底是深拷贝还是浅拷贝?

当我们对一个不可变对象(NSString类型)使用copy关键字的时候,系统是不会产生一个新对象,因为原来的对象是不能修改的,拷贝出来的对象也是不能修改的,那么既然两个都不可以修改,所以这两个对象永远也不会影响到另一个对象(符合我们说的“修改新(旧)对象,不影响旧(新)对象”原则),系统为了节省内存,所以就不会产生一个新的对象了。那么问题来了,copy到底是深拷贝还是浅拷贝?答:是否是深浅拷贝,是否创建新的对象,是由程序运行的环境所造成的,并不是一概而论。

(2)、这个写法会出什么问题@property (nonatomic, copy) NSMutableArray *mutableArray;

添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。原因:

self.mutableArray = xxx;在copy的修饰下执行的是 self.mutableArray = [xxx copy];进行了浅拷贝,得到的是一个xxx的副本,且该副本是一个不可变的数组。导致在运行的时候,其实你的mutableArray已经是NSArray类了。从而在添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。

3、为什么NSArray用copy修饰、NSMutableArray用strong修饰

把NSMutableArray用copy修饰有时就会crash,因为copy后的数组变成了不可变数组NSArray.当你对不可变数组NSArray进行增删改操作的时候就会crash,
举例如下:

1
2
3
4
5
6
7
①NSMutableArray用copy属性造成的crash:
@property (nonatomic, copy) NSMutableArray *mutableArray1; // 会崩溃
@property (nonatomic, strong) NSMutableArray *mutableArray2; // 正确

NSMutableArray *array = [NSMutableArray arrayWithArray:@[[Model1 new], [Model1 new]]];
self.mutableArray1 = array; // copy: mutableArray1是array的副本,且通过打印验证是浅拷贝。且此时aArray的值mArray的一个副本,该副本是通过[mArray copy]进行的浅拷贝得到的一个不可变新对象,即类型在执行时候变为了 NSArray了。所以,如果对归属为NSArray了的aArray执行NSMutableArray才有的如removeAllObjects的方法时,就会崩溃。
self.mutableArray2 = array; // strong:mutableArray1是array自身

当修饰可变类型的属性时,如NSMutableArray、NSMutableDictionary、NSMutableString,用strong。

当修饰不可变类型的属性时,如NSArray、NSDictionary、NSString,用copy。

4、模型数组深拷贝

通常需要实现对模型的拷贝都需要先实现NSCopying、 NSMutableCopying协议。注意:如果是数组使用拷贝操作是不会对数组内实现copy协议的对象进行深拷贝的。

参考文章:iOS 模型数组深拷贝

1、最笨的方法就是通过遍历逐个拷贝元素

1
2
3
4
NSMutableArray *array = [NSMutableArray array];
for (Person *person in dataSourceAry) {
[array addObject:[person copy]];
}

2、也有人使用归档解档实现数组内部元素拷贝

3、这么好用的一个方法现在才发现(推荐)

1
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag
1
2
3
4
5
6
7
8
NSArray <Person *>*deepCopyAry = [[NSArray alloc]initWithArray:dataSourceAry copyItems:YES];
NSLog(@"<dataSourceAry: %@>", dataSourceAry);
NSLog(@"<deepCopyAry: %@>", deepCopyAry);

[deepCopyAry enumerateObjectsUsingBlock:^(Person *obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.name = @"弗兰克";
obj.dog.name = @"弗兰克的dog";
}];

4、分析NSString、NSMutableString等类的copy、mutableCopy

在语言文章中,我们已经说明对于语句NSString *obj = [[NSData alloc] init]; obj在编译时是NSString的类型;运行时是NSData类型的对象。

由此得出的结论是,不要被编译时的类型蒙蔽,还要看实际运行时的类型。

(1)、分别对NSString、NSMutableString进行copy、mutableCopy生成的类型是什么?

你可以片面理解为

copy是[NSString alloc],所以生成的都是不可变的;
mutableCopy是[NSMutableString alloc],所以生成的都是可变的;

所以以下代码的结果,即为代码中的注释一样

1
2
3
4
5
6
7
8
9
10
>NSString *str1 = @"test001";
>
>NSMutableString *str2 = [str1 copy];
>//编译时,str2是NSMutableString类型。因为是把str2声明为可变字符串,所以str2即为声明的可变字符串
>//运行时,str2是NSString类型。因为是copy,所以不管str1是可变不可变,str2都是不可变字符串
>
>NSMutableString *str3 = [str1 mutableCopy];
>//编译时,str3是NSMutableString类型。因为是把str3声明为可变字符串,所以str3即为声明的可变字符串
>//运行时,str3是NSMutableString类型。因为是mutableCopy,所以不管str1是可变不可变,str3都是可变字符串
>
(2)、分别对NSString、NSMutableString进行copy、mutableCopy操作,是否会开辟新地址,即是属于深拷贝还是浅拷贝?

copy:对[string copy]是浅拷贝,即不会开辟新地址,而对[mutableString copy]是深拷贝,会开辟新地址。

mutableCopy:不管是对[string mutableCopy],还是对[mutableString mutableCopy],都是深拷贝,都会开辟新地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)testCopy {
NSString *testStr = [NSString stringWithFormat:@"123"];
id copyedStr = [testStr copy];
id mutableCopyStr = [testStr mutableCopy];
NSLog(@"=== 对NSString变量进行copy、mutableCopy得到的地址和类型分别是 ===");
NSLog(@"testStr = %p, class: %@", testStr, NSStringFromClass([testStr class]));
NSLog(@"copyedStr = %p, class: %@", copyedStr, NSStringFromClass([copyedStr class]));
NSLog(@"mutableCopyStr = %p, class: %@", mutableCopyStr, NSStringFromClass([mutableCopyStr class]));

NSLog(@"\n");

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
id copyedMutableStr = [testMutableStr copy];
id mutableCopyMutableStr = [testMutableStr mutableCopy];
NSLog(@"=== 对NSMutableString变量进行copy、mutableCopy得到的地址和类型分别是 ===");
NSLog(@"testMutableStr = %p, class: %@", testMutableStr, NSStringFromClass([testMutableStr class]));
NSLog(@"copyedMutableStr = %p, class: %@", copyedMutableStr, NSStringFromClass([copyedMutableStr class]));
NSLog(@"mutableCopyMutableStr = %p, class: %@", mutableCopyMutableStr, NSStringFromClass([mutableCopyMutableStr class]));
}

对NSString、NSMutableString变量进行copy、mutableCopy得到的地址和类型分别是

结论:分别对NSString、NSMutableString进行copy、mutableCopy操作,只有NSString copy是浅拷贝

(3)、将NSString、NSMutableString变量赋值给用copy、strong修饰NSString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?(附:NSString用copy修饰是为什么)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@property (nonatomic, copy) NSString *copyedStr;
@property (nonatomic, strong) NSString *strongStr;

@property (nonatomic, copy) NSString *copyedMutableStr;
@property (nonatomic, strong) NSString *strongMutableStr;

///将NSString、NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候
- (void)testCopy {
NSString *testStr = [NSString stringWithFormat:@"123"];
self.copyedStr = testStr;
self.strongStr = testStr;
NSLog(@"=== 将NSString变量赋值给用copy、strong修饰的NSString属性的时候 ===");
NSLog(@"testStr = %p", testStr);
NSLog(@"copyedStr = %p", self.copyedStr);
NSLog(@"strongStr = %p", self.strongStr);

NSLog(@"\n");

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.copyedMutableStr = testMutableStr;
self.strongMutableStr = testMutableStr;
NSLog(@"=== 将NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候 ===");
NSLog(@"testMutableStr = %p", testMutableStr);
NSLog(@"copyedMutableStr = %p", self.copyedMutableStr);
NSLog(@"strongMutableStr = %p", self.strongMutableStr);
}

将NSString、NSMutableString赋值给用copy、strong修饰的NSString变量

可以看出,

①将NSString变量赋值给用copy、strong修饰的NSString属性的时候,不管是strong还是copy属性的对象,其指向的地址都是同一个,即为string指向的地址。

1
2
3
4
5
6
@property (nonatomic, copy) NSString *copyedStr;
@property (nonatomic, strong) NSString *strongStr;

NSString *testStr = [NSString stringWithFormat:@"123"];
self.copyedStr = testStr;
self.strongStr = testStr;

②将NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候,

此时copy属性字符串copyedMutableStr

1
2
3
4
@property (nonatomic, copy) NSString *copyedMutableStr;

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.copyedMutableStr = testMutableStr;

深拷贝了testMutableStr字符串,并让copyedMutableStr对象指向这个字符串(即copyedMutableStr和testMutableStr只是对象值一样,但不是同一个了)。所以此时,我们如果去修改testMutableStr字符串的话,可以看到,我们用@property (nonatomic, copy) NSString *copyedMutableStr;修饰的copyedMutableStr能够不会因为其赋值源testMutableStr的改变而改变,也就保证了安全性。

而strong属性字符串strongMutableStr

1
2
3
4
@property (nonatomic, strong) NSString *strongMutableStr;

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.strongMutableStr = testMutableStr;

因为strongMutableStr与testMutableStr是指向同一对象,所以strongMutableStr的值也会跟随着改变;

综上:所以,在声明NSString属性时,到底是选择strong还是copy,可以根据实际情况来定。不过,一般我们将对象声明为NSString时,都不希望它改变(包括不希望赋值后,其他原来的值的改变会改变到它),所以大多数情况下,我们建议用copy,以免因可变字符串的修改导致的一些非预期问题。使用copy来修饰无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本,这样更安全。

(4)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSMutableString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
1
2
3
4
5
6
7
8
9
10
11
12
13
@property (nonatomic, copy) NSMutableString *copyedStr;
@property (nonatomic, strong) NSMutableString *strongStr;

NSString *testStr = [NSString stringWithFormat:@"123"];
self.copyedStr = testStr;
self.strongStr = testStr;



@property (nonatomic, copy) NSMutableString *copyedMutableStr;

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.copyedMutableStr = testMutableStr;

2.自定义类如何让它具有copy功能?
遵守NScoping协议,实现copywithzone方法即可.

(5)、自己代码实现copy修饰符,应该怎么写????

4、NSCoding和NSCopy

NSCoding和NSCopy

(1)、NSCoding的作用

iOS通过NSCoding保存实体对象

很多时候我们都需要将对象序列化,比如将一个对象存入到NSUserDefault 里面去的时候,由于NSUserDefault支持存入的类型有限制,所以很多时候我们需要将NSObject类型的对象转换成NSData再存入进去。

(2)、NSCopy

当你要进行对象拷贝的时候需要遵循NSCopy协议

1
2
3
4
5
6
7
8
9
- (id)copyWithZone:(NSZone *)zone {
id copy = [[[self class] alloc] init];
if (copy) {
[copy setId:[self.id copyWithZone:zone]];
[copy setNickName:[self.nickName copyWithZone:zone]];
}

return copy;
}

5、@synthesize和@dynamic区别

@synthesize和@dynamic区别

在声明property属性后,有2种实现选择

  • @synthesize

编译器期间,让编译器自动生成getter/setter方法。当有自定义的存或取方法时,自定义会屏蔽自动生成该方法

  • @dynamic

告诉编译器,不自动生成getter/setter方法,避免编译期间产生警告,然后由自己实现存取方法,或存取方法在运行时动态创建绑定:主要使用在CoreData的实现NSManagedObject子类时使用,由Core Data框架在程序运行的时动态生成子类属性

内存-②循环引用Timer

内存-②循环引用Timer

目录

1
2
3
4
5
6
7
8
9
1、NSTimer和NSRunLoop的关系?
2、NSTimer使用细节
3、NSTimer的创建
4、NSTimer的循环引用
5、NSTimer使用的优化
>
6、NSTimer的销毁问题
(1)、子线程中NSTimer的创建和销毁问题
>

九、NSTimer

< 返回目录

1、NSTimer的创建

NSTimer的创建通常有两种方式,尽管都是类方法,一种是timerWithXXX,另一种scheduedTimerWithXXX。

二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是无法正常工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@interface ViewController1 ()
@property (nonatomic, weak) NSTimer *timer1;
@property (nonatomic, weak) NSTimer *timer2;
@end


@implementation ViewController1

- (void)viewDidLoad {
[super viewDidLoad];

self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeInterval1:) userInfo:nil repeats:YES];

/*
错误做法:
self.timer2 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval2:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer2 forMode:NSDefaultRunLoopMode];
*/

//正确做法:
//特别注意:timer2创建时并没直接赋值给timer2。
//原因是timer2是weak属性,如果直接赋值给timer2会被立即释放。
//因为timerWithXXX方法创建的NSTimer默认并没有加入RunLoop,只有后面加入RunLoop以后才可以将引用指向timer2。从而导致执行到addTimer:forMode的时候,访问了野指针而发生EXC_BAD_ACCESS,崩溃。
NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval2:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
。。。。。
}


- (void)timeInterval1:(id)object {
NSLog(@"111");
}

- (void)timeInterval2:(id)object {
NSLog(@"222");
}

@end

2、NSTimer的修饰符

runloop强制持有timer(runloop->timer),timer会强制持有其target,未处理的情况下一般都是self(timer->self),导致self无法释放。
虽然设置timer为weak属性时候,self未强制持有timer,没构成循环应用,但还是导致了self无法释放的问题,dealloc无法执行。

2、NSTimer的循环引用

关于循环引用,我们先看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController1 ()
@property (nonatomic, strong) NSTimer *timer;//注意这里的属性不是为weak,从而很容易引起循环引用
@end


@implementation ViewController1

- (void)viewDidLoad {
// 代码标记1 (产生timer与self之前的强引用,如下图中的L3强引用线)
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
// 代码标记2 (产生RunLoop与timer之间的强引用,如下图中的L4强引用线)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 代码标记3 (产生self与timer之间的强引用,,如下图中的L2强引用线)
self.timer = timer;
}

- (void)timerFire {
NSLog(@"timer fire");
}

@end

假设代码中的视图控制器由UINavigationController管理,由于self.timer是strong类型,则强引用可以表示如下:

NSTimer循环引用例子1

由于,很容易看出来,由于timer本身在创建时候已经与self发生了强引用(不管target使用weak还是strong修饰,timer都会对target强引用。)。而赋值时候又由于timer是被设为strong的,而导致self与timer之间也发生了强引用,最终这两个强引用,就形成了循环引用。

所以,

①、首先,我们先解决循环引用,为了解决timer的循环应用问题,我们上面的timer属性应该使用weak。设置成weak后,L2就消失了。

②、但是即使使用了弱引用,上面的代码中ViewController1在pop退出的时候也无法正常释放,原因是在创建NSTimer时指定了target为self,这样一来造成了timer对ViewController1有一个强引用。从而导致,timer没释放的时候,viewController也是不会被释放的。为了让timer能够释放,我们就需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。所以,假设在viewController pop回去前,我们通过一个按钮来让timer调用invalidate方法,那么viewController在pop回去时候,就能够被释放了。

问题是我们一般不会有这个按钮操作,那么这时候让timer调用invalidate方法的操作,应该写在哪里呢。这时候,你可能会想到那就写在viewDidDisappear中呗。但是一旦在viewWillDisappear中写[timer invalidate]; timer = nil;那么你也得把timer的创建放在viewWillAppear中。因为我们可能执行的是push跳到下一页,再返回来的操作。然而显然将timer的创建放在viewWillAppear中这样的方式,显然会是导致当timer可能需要频繁添加。所以,我们放弃此方法,还是把timer的创建放在viewDidLoad中,然后考虑其他方法。

所以,我们最后为了让ViewController1在pop退出的时候不会因为timer的强引用,而导致无法正常释放。我们选择转移timer中的target。这样就能确保,viewController在pop退出的时候能够正常释放,从而调用viewController的dealloc方法。

附:转移timer中的target的方法通常有两种:

一种是将target分离出来独立成一个对象(在这个对象中创建NSTimer并将对象本身作为NSTimer的target),控制器通过这个对象间接使用NSTimer;

另一种方式的思路仍然是转移target,只是可以直接增加NSTimer扩展(分类),让NSTimer自身做为target,同时可以将操作selector封装到block中。

NSTimer转移target方法二
图中参考NSTimer+Block

后者相对优雅,也是目前使用较多的方案。显然Apple也认识到了这个问题,如果你可以确保代码只在iOS 10下运行就可以使用iOS 10新增的系统级block方案(上面的代码中已经贴出这种方法)。

③、如果不做②中的转移timer的target的话,那么viewController就会无法释放,造成内存泄露。
但是,我们发现通过转移timer的target后,虽然解决了UIViewController1因为被timer强引用而导致的在pop回来的时候无法释放的问题。我们的计时器,却在UIViewController1 pop退出被释放后,两个定时器仍然在运行,也就是它还没被释放。所以,我们还需要解决timer的释放。那怎么让timer释放呢?答:如果要让timer释放掉的话,需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。
invalidate方法有2个功能:一是将timer从runloop中移除,那么图中的L4就消失,二是timer本身也会释放它持有资源,比如它的target、userinfo、block等,因为这里的target是self,所以强引用L3也就消失。
所以,最终当viewController在pop退出的时候,其正常释放并调用了dealloc放。我们只需要在dealloc方法中,添加上timer调用invalidate的方法,即可以解决viewController被释放了,但timer没被释放的问题。

1
2
3
4
- (void)dealloc {
[self.timer invalidate];
NSLog(@"ViewController1 dealloc...");
}

所以,一个完整的timer过程,代码如下:

1

3、NSTimer和NSRunLoop的关系?

只要出现NSTimer必须要有NSRunLoop,NSTimer必须依赖NSRunLoop才能执行 。NSTimer其实也是一种资源,如果看过多线程编程指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会生效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。

NSRunLoop与timer有关方法为:

1
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode; //在run loop上注册timer

注意事项:

我们通常在主线程中使用NSTimer,有个实际遇到的问题需要注意。当滑动界面时,系统为了更好地处理UI事件和滚动显示,主线程runloop会暂时停止处理一些其它事件,这时主线程中运行的NSTimer就会被暂停。解决办法就是改变NSTimer运行的mode(mode可以看成事件类型),不使用缺省的NSDefaultRunLoopMode,而是改用NSRunLoopCommonModes,这样主线程就会继续处理NSTimer事件了。具体代码如下:

1
2
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

4、NSTimer使用细节:

NSTimer上的定时任务是在创建NSTimer的线程上执行的。NSTimer的销毁和创建必须在同一个线程上操作
NSTimer要被添加到当前线程的 Runloop 里面且 Runloop 被启动,定时任务(selector或者invocation)才会触发。

以下内容摘自:NSTimer定时器进阶——详细介绍,循环引用分析与解决

  1. 它需要被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
  2. 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的
  3. runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
  4. timer的执行时间并不准确,系统繁忙的话,还会被跳过去。(具体的两种不准时,请查看原文)
  5. invalidate调用后,timer停止运行后,就一定能从runloop中消除吗,资源????invalidate方法的调用必须在timer添加到的runloop所在的线程,如果不在的话:由于调用invalidate 方法后,timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。

NSTimer的强引用问题举例:

NSTimer的强引用问题举例

5、NSTimer使用的优化

问:为什么要在非主线程创建NSTimer?

  • 将 timer 添加到主线程的Runloop里面本身会增加线程负荷;
  • 如果主线程因为某些原因阻塞卡顿了,timer 定时任务触发的时间精度肯定也会受到影响;
  • 有些定时任务不是UI相关的,本来就没必要在主线程执行,给主线程增加不必要的负担。当然也可以在定时任务执行时,手动将任务指派到非主线程上,但这也是有额外开销的。

6、NSTimer的销毁问题

前面我们已经简单讲过要让NSTimer销毁释放的时候,只能通过调用其invalidate来达到销毁目的。关于invalidate的第一个作用以及它在哪个线程调用的问题,我的理解如下:

invalidate方法的第一个作用将timer从runloop中移除。这里的runLoop指的应该是当前的runLoop,而不是timer被添加到的runLoop,这个纯属个人理解,未验证,因为如果是其添加的runLoop的话,那子线程timer的销毁,就不会有人说还要和timer所在的线程一致了。所以,这里的个人理解有如下:

为了销毁timer和去除runloop与timer之间的强引用,我们调用了timer的invalidate方法。

1、对于invalidate方法的调用是写在必须在timer所添加到的runloop所在的线程(如主线程)的时候,invalidate方法会将timer从runloop中移除,并且释放它持有资源。即上面的L4和L3都消失。

2、对于invalidate方法的调用不是写在必须在timer所添加到的runloop所在的线程的时候(如子线程中添加timer,在主线程中调用该timer的invalidate),虽然timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。

如果invalidate方法的调用的位置不更改的话,这时候要让L4消失的方法,

方法①手动销毁runloop。比如

1
2
3
4
    //[[NSRunLoop currentRunLoop] run]; //将原本的方法注释掉
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5.f];
[[NSRunLoop currentRunLoop] runUntilDate:date]; //让runloop在5s后销毁
>

这种方式,只适用于销毁时间确定的情况。那如果销毁时间不确定怎么办?

方法②:因为当某个线程销毁时,其runloop也随之销毁,所以方法二即为通过销毁timer所在的线程,来达到销毁runloop的目的。如果是在主线程,线程一直存在,我们没法让主线程销毁。

所以,下面我们讨论的是在子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?

6.1子线程中NSTimer的创建和销毁问题

我们按上诉2中②的讨论描述的:子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,写出的对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@property (nonatomic, weak) NSTimer *threadTimer; //子线程timer


- (void)viewDidLoad {
[super viewDidLoad];

// 开辟子线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
}

- (void)newThread {
@autoreleasepool {
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:@"这是子线程"];

self.threadTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(threadTimerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}

- (void)threadTimerAction {
static NSInteger counter = 0;

NSString *isMainThreadDescription = [NSThread isMainThread] ? @"YES" : @"NO";
NSLog(@"当前方法执行的线程:%@, 它是否是主线程:%@, counter = %@", [NSThread currentThread], isMainThreadDescription, @(counter++));
}

这时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?即怎么销毁线程?

乍看当在子线程开启runloop后,timer会一直在子线程中运行,所以子线程不会销毁,runloop也就无法停止,runloop也就没法销毁,runloop与timer之间的强引用则还是被保留着,这似乎又是个死循环。但实际上,由于上述代码runloop的mode item只有Timer,所以只要销毁timer,runloop就会退出。所以,上述的代码是没问题的,不存在内存泄露问题。

附:NSTimer上的定时任务是在创建NSTimer的线程上执行的。

附:以上NSTimer的内容,有空的话还可参考NSTimer,NSRunLoop,autoreleasepool,多线程的爱恨情仇,它那边讲的,和这边自己理解的基本是一样的。只是对于有些点的介绍详细不一定一样而已。

其他有空可看iOS 中的 NSTimer

常见笔试/面试题

< 返回目录

END

< 返回目录

内存-③内存泄漏定位

内存-③内存泄漏定位

未整合的文章:

目录

1
2
3
4
5
6
7
8
1、常见的三种泄露情形
(1)、创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to 'number' is never read。翻译一下:存储在'number'里的值从未被读取过。
(2)、创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to 'str' during its initialization is never read
(3)、调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into 'subImageRef'。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。
>
2、.....
>
>
1
2
3
4
5
6
7
8
1、ARC下dealloc的使用
ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。
>
2、controller 不能释放,不走dealloc方法的几种可能
>

以下内容摘自:iOS性能优化之内存管理:Analyze、Leaks、Allocations的使用和案例代码

前言

内存空间的划分: 我们知道,一个进程占用的内存空间,包含5种不同的数据区:

1
2
3
4
5
(1)BSS段:通常是存放未初始化的全局变量;
(2)数据段:通常是存放已初始化的全局变量。
(3)代码段:通常是存放程序执行代码。
(4)堆:通常是用于存放进程运行中被动态分配的内存段,OC对象(所有继承自NSObject的对象)就存放在堆里。
(5)栈:由编译器自动分配释放,存放函数的参数值,局部变量等值。

栈内存是系统来管理的,因此我们常说的内存管理,指的是堆内存的管理,也就是所有OC对象的创建和销毁的管理。

伴随着iOS5的到来,苹果推出了ARC(自动引用计数)技术,此模式下编译器会自动在合适的地方插入retain、release、autorelease语句,也就是说编译器会自动生成内存管理的代码,解放了广大程序猿的双手,也基本上避免了内存泄露问题,但是呢…

内存泄露的定义是:用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)。

在iOS应用中的内存泄露,原因一般有循环引用、错用Strong/copy等。

重要文章

一、Analyze—静态分析

< 返回目录

顾名思义,静态分析不需要运行程序,就能检查到存在内存泄露的地方。

使用方法:打开Xcode,command + shift + B;或者Xcode - Product - Analyze;

1、常见的三种泄露情形:

(1)创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to ‘number’ is never read。翻译一下:存储在’number’里的值从未被读取过。

(2)创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to ‘str’ during its initialization is never read

(3)调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into ‘subImageRef’。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。

贴上三种常见情形的Demo代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 /**
* 情 形 一:创建了一个对象,但是并没有使用。
* 提示信息:Value Stored to 'number' is never read
* 翻译一下:存储在'number'里的值从未被读取过,
*/
- (void)leakOne {
NSString *str1 = [NSString string];
NSNumber *number;
number = @(str1.length);
/*
说我们没有读取过它,那就读取一下,比如打开下面这句代码,对它发送class消息,就不再会有这个提示了。
当然最好的方法还是将有关number的代码都删掉,因为,你只对number赋值,又不使用,那干嘛创建出来呢。
这是一个比较常见和典型的错误,也很容易检查出来
*/
// [number class];
}

/**
* 情 形 二:创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。
* 提示信息:Value Stored to 'str' during its initialization is never read
*/
- (void)leakTwo {
NSString *str = [NSString string]; // 创建并初始化str,此时已经有一个内存单元保存str初始化的值
// NSString *str; // 这样就内存不泄露,因为str是可变的,只需要先声明就行。
// printf("str前 = %p\n",str);
str = @"ceshi"; // str被改变了,指向了"ceshi"所在的地址,指针改变了,但之前保存初始化值的内存空间还未释放,保存str初始化值的内存单元泄露了。
// printf("str后 = %p\n",str); // 指针改变了
[str class];

// 再举两个例子,同理

NSArray *arr = [NSArray array];
// printf("arr前 = %p\n",arr);
// NSArray *arr; // 这样就内存不泄露
arr = @[@"1",@"2"];
// printf("arr后 = %p\n",arr); // 指针改变了
[arr class];

CGRect rect = self.view.frame;
// CGRect rect = CGRectZero; // 这样就内存不泄露
rect = CGRectMake(0, 0, 0, 0);
NSLog(@"rect = %@",NSStringFromCGRect(rect));
}

/**
* 情 形 三:调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。
* 提示信息:Potential leak of an object stored into 'subImageRef'
* 翻译一下:subImageRef对象的内存单元有潜在的泄露风险
*/
- (void)leakThree {
CGRect rect = CGRectMake(0, 0, 50, 50);
UIImage *image;
CGImageRef subImageRef = CGImageCreateWithImageInRect(image.CGImage, rect); // subImageRef 引用计数 + 1;

UIImage* smallImage = [UIImage imageWithCGImage:subImageRef];

// 应该调用对应的函数,让subImageRef的引用计数减1,就不会泄露了
// CGImageRelease(subImageRef);

[smallImage class];
UIGraphicsEndImageContext();
}

自己遇到的实例:

情形2:静态检测内存泄露Analyze--Value stored to ‘dataArr’ during its initialization is never read
即初始化的时候开辟了一块内存,却始终没用到,导致该块内存泄漏

1
2
3
4
5
6
NSMutableArray *tempMutArr = [NSMutableArray arrayWithCapacity:0];
if ([self.clickedButtonTpye isEqualToString:KClickedButtonTypeLast]) {
tempMutArr = self.lastDataSourceArr;
}else{
tempMutArr = self.hotDataSourceArr;
}

二、Leaks—内存泄露

< 返回目录

  • MLeaksFinder:精准 iOS 内存泄露检测工具

    项目 UIViewController+MemoryLeak.m NSObject+MemoryLeak.m

    原理:MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

    即我们hook viewDidDisappear:dismissViewControllerAnimated: completion: 都去执行NSObject的willDealloc方法。

    在NSObject的willDealloc方法中,隔2秒后再去尝试调用另一个方法assertNotDealloc。如果UIViewController已经被成功释放,则肯定是 nil 执行 assertNotDealloc,即assertNotDealloc不会被执行,也就不会弹出内存泄露的弹窗。反之,若UIViewController没有释放,则会弹出内存泄露的弹窗。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    > // UIViewController+MemoryLeak.m
    > - (void)swizzled_viewDidDisappear:(BOOL)animated {
    > [self swizzled_viewDidDisappear:animated];
    >
    > if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
    > [self willDealloc];
    > }
    > }
    >
    > - (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    > [self swizzled_dismissViewControllerAnimated:flag completion:completion];
    > ......
    >
    > [dismissedViewController willDealloc];
    > }
    >
    > // NSObject+MemoryLeak.m
    > - (BOOL)willDealloc {
    > ......
    >
    > __weak id weakSelf = self;
    > dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    > __strong id strongSelf = weakSelf;
    > [strongSelf assertNotDealloc];
    > });
    >
    > return YES;
    > }
    >
    > - (void)assertNotDealloc {
    > if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
    > return;
    > }
    > [MLeakedObjectProxy addLeakedObject:self];
    >
    > NSString *className = NSStringFromClass([self class]);
    > NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
    > }
    >
  • 自动化内存泄漏检测 项目 https://github.com/liujiakuoyx/leak_detector/blob/main/lib/src/leak_navigator_observer.dart#L121

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    > ///Such as WebSocket is delay close connect.
    > const int _defaultCheckLeakDelay = 500;
    >
    > typedef ShouldAddedRoute = bool Function(Route route);
    >
    > ///NavigatorObserver
    > class LeakNavigatorObserver extends NavigatorObserver {
    > final ShouldAddedRoute? shouldCheck;
    > final int checkLeakDelay;
    >
    > ///[callback] if 'null',the all route can added to LeakDetector.
    > ///if not 'null', returns ‘true’, then this route will be added to the LeakDetector.
    > LeakNavigatorObserver(
    > {this.checkLeakDelay = _defaultCheckLeakDelay, this.shouldCheck});
    >
    > @override
    > void didPop(Route route, Route? previousRoute) {
    > _remove(route);
    > }
    >
    > @override
    > void didPush(Route route, Route? previousRoute) {
    > _add(route);
    > }
    >
    > @override
    > void didRemove(Route route, Route? previousRoute) {
    > _remove(route);
    > }
    >
    > @override
    > void didReplace({Route? newRoute, Route? oldRoute}) {
    > if (newRoute != null) {
    > _add(newRoute);
    > }
    > if (oldRoute != null) {
    > _remove(oldRoute);
    > }
    > }
    > ......
    > }
    >
  • 快手 Flutter 上的内存泄漏监控

Leaks是动态的内存泄露检查工具,需要一边运行程序,一边检测。

内存泄漏Leak1

先不切换到Call Trees,先看看Statistics(统计数据)下的情况

内存泄漏Leak2
Allocation中我们主要关注的是Persistent和Persistent Bytes,分别表示当前时间段,申请了但是还没释放的内存数量和大小。

切换到Call Trees后世这样的

内存泄漏Leak3

项目中遇到过的内存泄漏

这里我们不是去查看Call Tree,而是查看Cycles & Roots

循环引用定位1

点击标记4处为黑色,即代表该处会发生内存泄漏,双击进入代码,如图:
循环引用定位2
果然存在内存泄漏

自己遇到的其他例子:

Leak_mine_1
开始不明白为什么这个变量会内存泄漏后面才能白,其实_priceDetailModel这个本身已经用OrderPriceDetailModel赋值过一次了,而这里你又赋值了一次,导致多了一个。代码情况如下两个图:
Leak_mine_2
又在set方法中生成了一个地址
Leak_mine_3

三、ARC下的dealloc

1、ARC下dealloc的使用

ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:

1
2
3
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。
(1)、通知的观察者,或KVO的观察者;

由于通知中心是系统的一个单例,你在注册通知的观察者时,实际上是在通知中心注册的,

这时,即使ARC下系统帮我们释放了对象,但是在通知中心的观察还是没有移除,那么当有

该通知时,依然会尝试调用该对象的接受通知的方法,这可能会导致一些问题.

(2)、对象强委托/引用的解除;

对于其他的对象来把你当做委托 delegate时,并且是 强引用时,即时你自身被释放,但是引用你的对象依然还在,

这时需要在引用你的对象移除该delegate

(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。

一个对象,如一个ViewController在销毁之前有可能需要和server打交道;

这时我们也可以在dealloc中写

2、controller 不能释放,不走dealloc方法的几种可能

主要原因还是循环引用,引起的内存泄漏。

详情参考:controller 不能释放,不走dealloc方法的4种可能

四、Time Profile

< 返回目录

详情参考:instrument Time Profiler总结

使用Time Profile前有两点需要注意的地方:

1
2
1、一定要使用真机调试
2、应用程序一定要使用发布配置

图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方

TimeProfile1

其他

其他参考材料:

问:Xcode 运行程序,左侧memory 不显示内存。。

答:运行程序后,xcode 不显示当前使用的内存情况,问题是打开了僵尸–enable zoombie Objects,关闭即可。
即打开 product—>SCheme–>EditSCheme –>enable zoombie Objects 取消选中 ok

就可以继续显示了

1、MLeaksFinder 适配新版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+ (void)alertWithTitle:(NSString *)title
message:(NSString *)message
delegate:(id<UIAlertViewDelegate>)delegate
additionalButtonTitle:(NSString *)additionalButtonTitle {
/*
[alertView dismissWithClickedButtonIndex:0 animated:NO];
UIAlertView *alertViewTemp = [[UIAlertView alloc] initWithTitle:title
message:message
delegate:delegate
cancelButtonTitle:@"OK"
otherButtonTitles:additionalButtonTitle, nil];
[alertViewTemp show];
alertView = alertViewTemp;

NSLog(@"%@: %@", title, message);
*/

// 获取当前的根视图控制器
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (rootViewController.presentedViewController) {
rootViewController = rootViewController.presentedViewController;
}

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:okAction];
if (additionalButtonTitle) {
UIAlertAction *additionalAction = [UIAlertAction actionWithTitle:additionalButtonTitle style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:additionalAction];
}
[rootViewController presentViewController:alertController animated:YES completion:nil];

NSLog(@"%@: %@", title, message);
}

常见笔试/面试题

< 返回目录

END

< 返回目录

视图-①本质

视图-①本质

[toc]

目录

1
2
3
4
5
6
7
8
9
10
附:整个响应链及事件链
1、完善响应链查找知识点
2、基础概念等详解
2.1 响应者对象(UIResponder)
2.2、UITouch(点击对象)
2.2.1、UITouch的几个主要属性和方法
2.2.2、UITouch的生成场景
2.3、UIEvent(事件对象)
3、响应链的应用
>

一、在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么

这其实是一个事件传递和响应链的问题。(其实,按钮点击后,这里还包括runloop的唤醒等知识,不过这点我们放在下一大点讲)。

答:在我们点击按钮的时候,会产生了UITouch(点击对象)和UIEvent(事件对象),这两个对象组合成一个点击事件。而发生触摸事件后,

①消息循环(runloop)/系统就会接收到这个触摸事件,并将它放到一个由UIApplication管理的消息队列(先进先出)里。

②UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理。首先UIApplication将事件传递给的是UIWindow对象(即一般为应用程序的主窗口keyWindow)。

③然后,UIWindow(继承自UIView)对象会继续向它的子View对象传递,直到传递到最上层。(或者说UIWindow使用hitTest:withEvent:方法查找touch操作的所在的视图view)

其中的应用程序逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链

二、事件的响应链

事件的响应链大概过程如下图所示:

事件的响应链

  • 1、在传递的过程中,下一响应者的查找是通过UIView里的- pointInside: withEvent:- hitTest: withEvent:两个方法来确定的。当从最初的只有一个响应者通过这样的方式不断的找到下一响应者后,这些响应者就组成了一个响应者链。

  • 2、当通过- hitTest: withEvent:找到第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。这个过程就是事件的传递过程。从这可以看出它的方向是跟响应链方向相反的。这里我们可以用UITableViewCell中点击上面的label来想象。

附:整个响应链及事件链

整个响应链(向下)及事件链(向上),大概如图所示:

响应链(向下)及事件链(向上)及事件链(向上).png)
在上图,当- hitTest: withEvent:方法沿着红色箭头方向寻找第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。
所以响应链为红色部分,事件链的顺序可以理解为图上的灰色箭头部分(个人理解)。

1、完善响应链查找知识点

我们已经知道响应者链是由多个响应者组合起来的链条。那么怎么找到这些相应者呢?

响应者的查找为通过UIView内部的下面两个方法来查找的

1
2
3
4
5
//根据点击坐标返回事件是否发生在本视图以内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds

// 返回响应点击事件的对象(当点击区域在分为内时候,如果有子视图则返回子视图里最终的响应者,如果没有子视图则返回自身)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

对于一个视图

①、若子视图中的- pointInside: withEvent:方法返回为NO,即判断用户点击的区域不在该子视图范围内的话,则停止对这个子视图里的子视图继续查找,- hitTest: withEvent:返回nil。

②、若子视图中的- pointInside: withEvent:方法返回为YES,即判断用户点击的区域在该子视图范围内的话,则继续往该子视图里的子视图查找,直到没有子视图,然后- hitTest: withEvent:返回这个子视图,而后之前的视图的- hitTest: withEvent:也返回这个子视图。

  • 1
     

hitTest-withEvent-查找过程举例,如下图

hitTest-withEvent-查找过程举例
图片中view等级

1
2
3
4
[ViewA addSubview:ViewB];
[ViewA addSubview:ViewC];
[ViewB addSubview:ViewD];
[ViewB addSubview:ViewE];

那么点击viewE后,发生的过程是怎样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.A 是UIWindow的根视图,首先对A进行hitTest:withEvent:
2.判断A的userInteractionEnabled,如果为NO,A的hitTest:withEvent返回nil;
3.pointInside:withEvent:方法判断用户点击是否在A的范围内,显然返回YES
4.遍历A的子视图B和C。由于从后向前遍历,因此先查看C,再查看B。
>
4.1 查看C:
调用C的hitTest:withEvent方法:pointInside:withEvent:方法判断用户点击是否在C的范围内,不在返回NO,C对应的hitTest:withEvent: 方法return nil;
>
4.2 再查看B
调用B的hitTest:withEvent方法:pointInside:withEvent:判断用户点击是否在B的返回内,在返回YES。
>遍历B的子视图D和E,从后向前遍历,所以先查看E,再查看D。
4.2.1先查看E,调用E的hitTest:withEvent方法:pointInside:withEvent:方法 判断用户点击是否在E的范围内,在返回YES,E没有子视图,因此E对应的hitTest:withEvent方法返回E,再往前回溯,就是B的hitTest:withEvent方法返回E,因此A的hitTest:withEvent方法返回E。
4.2.2查看D,略
>
至此,点击事件的第一响应者就找到了。

2、基础概念等详解

iOS中的事件可以分为3大类型:

  1. 触摸事件
  2. 加速计事件
  3. 远程控制事件

在iOS中不是任何对象都能处理事件,能接受并这些处理事件的对象只有直接或间接继承自UIResponder的对象,我们称之为“响应者对象”。

2.1 响应者对象(UIResponder)

①、为什么只有继承自UIResponder的类才能够接收并处理事件呢?因为处理这些事件的方法是卸载UIResponder中的啊。详细的UIResponder中提供的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
4个处理触摸事件的对象方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

和3个处理加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

以及1个处理远程控制事件的方法
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

附:如何实现UIView的拖拽呢?即让UIView随着手指的移动而移动。

答: 重写touchsMoved:withEvent:方法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// 想让控件随着手指移动而移动,监听手指移动
// 获取UITouch对象
UITouch *touch = [touches anyObject];
// 获取当前点的位置
CGPoint curP = [touch locationInView:self];
// 获取上一个点的位置
CGPoint preP = [touch previousLocationInView:self];
// 获取它们x轴的偏移量,每次都是相对上一次
CGFloat offsetX = curP.x - preP.x;
// 获取y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改控件的形变或者frame,center,就可以控制控件的位置
// 形变也是相对上一次形变(平移)
// CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
// make:相对于最原始的位置形变
// CGAffineTransform t:相对这个t的形变的基础上再去形变
// 如果相对哪个形变再次形变,就传入它的形变
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

②、那么iOS中能接收并处理事件或者说继承自UIResponder的类有哪些呢?

1
2
3
> iOS中能接收并处理事件或者说继承自UIResponder的类有:
UIApplication、UIWindow、UIViewController和所有继承UIView的UIKit类都直接或间接的继承自UIResponder。
>

从UIResponder内部提供的方法可以看出,触摸方法接收两个参数,一个UITouch对象的集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象。

2.2、UITouch(点击对象)

UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中。

2.2.1、UITouch的几个主要属性和方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@property(nonatomic,readonly) NSTimeInterval      timestamp;    // 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase; // 当前触摸事件所处的状态
@property(nonatomic,readonly) NSUInteger tapCount; // 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);


@property(nullable,nonatomic,readonly,strong) UIWindow *window; //触摸产生时所处的窗口
@property(nullable,nonatomic,readonly,strong) UIView *view; //触摸产生时所处的视图
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

/**
* 获取当前点击位置的坐标点
*
* @param view 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
*
* @return 返回值表示触摸在view上的位置点(这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0)))
*/
- (CGPoint)locationInView:(nullable UIView *)view;

/// 获取前一个触摸点位置的坐标点
- (CGPoint)previousLocationInView:(nullable UIView *)view;
2.2.2、UITouch的生成场景:

前言:每根手指触摸屏幕时都会创建一个与该手指相关的UITouch对象。一根手指对应一个UITouch对象。每个UITouch对象保存着跟手指相关的信息,比如触摸的位置、时间、阶段。
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象

实际调用现象举例:

①、当用户用一根手指触摸屏幕时,view会调用1次touchesBegan:withEvent:方法。touches参数中装着1个UITouch对象。

②、如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象。

③、如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象

2.3、UIEvent(事件对象)

iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);


typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};

typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,

// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,

// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

介绍了以上响应者对象(UIResponder)及其相关的UITouch(点击对象)和UIEvent(事件对象)相关概念后,我们就知道了用户点击后,会产生了UITouch(点击对象)和UIEvent(事件对象)并打包发送,最后由响应者对象(UIResponder)来处理这些事件。

现在的问题是你知道它是怎么通过用户的点击位置找到处理该点击事件的响应者对象吗?

3、响应链的应用

既然已经知道了系统是怎么获取响应视图的流程了,那么我们可以通过重写查找事件处理者的方法来实现不规则形状点击。

最常见的不规则视图就是圆形视图,在demo中我设置view的宽高为200,那么重写方法事件如下:

1
2
3
4
5
6
7
8
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
const CGFloat halfWidth = 100;
CGFloat xOffset = point.x - 100;
CGFloat yOffset = point.y - 100;
CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
return radius <= halfWidth;
}

最终的效果图如下:
响应链的应用1_点击不规则图形

前面说过按钮点击后,这里还包括一些runloop相关的知识,如唤醒等,所以下面我们就专门开讲一件Runloop。

三、CALayer和UIView

UIView与CALayer是什么关系。

<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents

UIViewCALayerdelegate(CALayerDelegate)

UIView继承自UIResponder类,可以响应事件

CALayer直接继承自NSObject类,不可以响应事件

UIView主要处理事件,CALayer负责绘制

每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,Layer内部有SubLayersView内部有SubViews,但是LayerView多了个AnchorPoint

CALayer的结构图如下:

CALayer的结构图

在 iOS 系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互。 但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer。

CALayer类的概念与UIView非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。它与UIView最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在。

在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。实际上这个CALayer图层才是真正用来在屏幕上显示的,UIView仅仅是对它的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级 API。

以上摘要来自:内存恶鬼drawRect - 谈画图功能的内存优化中的CALayer和UIView介绍部分。

CALayer有三个视觉元素:背景色、内容和边框,其中,内容的本质是一个CGImage

CALayer和UIView

简述CALayer和UIView的关系

答:UIView和CALayer是相互依赖的关系。UIView依赖与calayer提供的内容,CALayer依赖uivew提供的容器来显示绘制的内容。归根到底CALayer是这一切的基础,如果没有CALayer,UIView自身也不会存在,UIView是一个特殊的CALayer实现,添加了响应事件的能力

结论:
UIView来自CALayer,高于CALayer,是CALayer高层实现与封装。UIView的所有特性来源于CALayer支持。

为什么CLLayer可以在子线程,而UIView不行?

CLLayer可以在子线程中运行,而UIView不行,这主要是因为它们在iOS系统中的职责和设计原则不同。

  1. 职责不同
    • UIView:UIView是用户界面的基础类,负责处理用户交互(如触摸事件),并且所有的UI更新和渲染都必须在主线程中进行。UIView通过其内部的CALayer来处理内容的显示,UIView本身作为CALayer的delegate,负责响应CALayer的变化并进行相应的UI更新。
    • CALayer:CALayer是Core Animation框架的一部分,主要负责内容的绘制和显示。CALayer可以独立于UIView存在,并且可以在子线程中进行绘制操作,因为它不直接处理用户交互,只负责图形的渲染。
  2. 线程安全
    • UIView:UIKit框架并不是线程安全的,官方建议所有的UI操作都在主线程进行,以避免出现线程安全问题和不可预测的UI行为。
    • CALayer:CALayer可以在子线程中进行绘制,因为它支持异步绘制。通过实现CALayer的代理方法displayLayer:,可以在子线程中完成绘制工作,然后将绘制好的图像数据传回主线程更新到CALayer中,这种方式被称为异步绘制。
  3. 性能优化
    • UIView:由于UIView的所有UI操作都需要在主线程执行,这限制了其在多核CPU上的性能扩展能力。
    • CALayer:通过在子线程中进行异步绘制,可以减轻主线程的负担,提高应用的性能和响应速度,尤其是在处理复杂的图形渲染时。

综上所述,CALayer可以在子线程中运行,因为它的设计允许异步绘制,而UIView的所有UI更新必须在主线程中进行,以保证线程安全和UI的响应性。

Flutter 的 RenderObject 和 iOS 中的 CALayer 都扮演着渲染树中节点的角色,它们都负责绘制界面的一部分,并且都可以包含子节点。

在Flutter中,三棵树(Widget树、Element树、RenderObject树)与iOS中的类对应关系如下:

  1. Widget树
    • Flutter中的Widget树类似于iOS中的UIView控件树。Widget是Flutter中用于描述用户界面的构件,它们是不可变的,并且当状态变化时,Flutter会构建一个新的Widget树。
  2. Element树
    • Element树在Flutter中扮演着将Widget树的变更映射到RenderObject树的角色。每一个Widget都有一个对应的Element,Element树是Widget树和RenderObject树之间的桥梁。在iOS中,这个概念没有直接对应的类,因为它是Flutter特有的架构。但是,如果非要找一个类比,可以说Element树的作用类似于UIView的实例化和状态管理,因为Element持有Widget和RenderObject的引用,并负责协调它们的状态。
  3. RenderObject树
    • RenderObject树是Flutter中实际负责布局和绘制的树。它与iOS中的CALayer树相似,因为CALayer负责实际的渲染工作。RenderObject树根据Widget的属性进行布局(layout)和绘制(paint),这与CALayer在iOS中的作用类似。

总的来说,Flutter的Widget树类似于iOS的UIView树,Element树是Flutter特有的,没有直接对应的iOS类,而RenderObject树类似于iOS的CALayer树。这种架构使得Flutter能够在不同的平台上以统一的方式处理渲染和布局。

img

Element 同时持有 Widget 和 RenderObject。而无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有 RenderObject。那你可能会问,既然都是发号施令,那为什么需要增加中间的这层 Element 树呢?直接由 Widget 命令 RenderObject 去干活儿不好吗?

答案是,可以,但这样做会极大地增加渲染带来的性能损耗。

因为 Widget 具有不可变性,但 Element 却是可变的。实际上,Element 树这一层将 Widget 树的变化(diff)做了抽象,可以只将真正需要修改的部分同步到 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

这,就是 Element 树存在的意义。

Element 是可复用的,只要 Widget 前后类型一样。比如 Widget 是蓝色的,重建后变红色了,Element 是会复用的。所以多个 Widget(销毁前后)可以对应一个 Element

常见笔试/面试题

< 返回目录

问:UIButton从子类到父类依次继承自什么?

答:UIControl-> UIView-> UIResponder。

哪些视图的设置能禁止其相应事件

1、userInterface = NO;
2、hidden = YES;
3、当UIBUTTON透明度为0就不响应事件了,当UIBUTTON透明度为0就不响应事件了。

更多参考:iOS开发经验:button不能响应的原因

离屏渲染

在使用圆角、阴影和遮罩等视图功能的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所有就需要在屏幕外的上下文中渲染,即离屏渲染。

更多参考:iOS离屏渲染之优化分析该文非常重要。

END

< 返回目录

视图-①Runloop

视图-①Runloop

[toc]

一、RunLoop的理解

让线程永不休眠。

背景:负责持续性的处理各种任务(比如Source,Timer,Observer),让线程能一直运行,且在没有任务的时候能够进入休眠,减少 CPU 的使用率,从而节省电量和资源。

1、正常一个线程一次只能执行一个任务,执行完成后线程就退出了。为了让线程能随时处理事件但并不退出,使用do-while循环实现。

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

2、do-while的检查是一直主动检查?线程使用RunLoop时,不需要一直主动检查输入源是否有事件到来。而是靠事件驱动。即

事件驱动(Event-Driven)

在事件驱动的编程模型中,应用程序的执行流程是由事件(如用户操作、消息、定时器超时等)来驱动的。应用程序不需要不断地检查某个条件,而是注册事件处理函数,并让操作系统在相应的事件发生时通知应用程序。这样,应用程序就可以在没有事件发生时执行其他任务或者进入休眠状态,从而节省资源。

3、do-while性能消耗?

当 RunLoop 检测到没有待处理的事件时,它会将线程置于”休眠等待”状态,而不是忙等。这时,线程不会消耗 CPU 资源。当事件到来时,如用户输入、定时器超时或 I/O 完成等,RunLoop 会被唤醒,处理相应的事件,然后再次进入休眠状态。

Runloop是绑定到线程上的(每个线程可以有一个对应的 RunLoop 对象。这些 RunLoop 对象被保存在一个全局的 Dictionary 中,其中线程作为 Key,RunLoop 作为 Value。)。每个线程可以有自己的 RunLoop,这样每个线程可以独立地处理与自己任务相关的事件,提高了事件处理的效率和局部性。

每个Runloop有多种Model。不同的 Mode 可以包含不同的事件源(Sources)和定时器(Timers)。通过切换 Mode,RunLoop 可以过滤掉一些不想要的事件,只处理当前 Mode 下相关的事件。这样可以避免在处理特定任务时被不相关的事件打扰,提高程序的响应性和效率。例如当用户滚动列表时,iOS 应用程序的 RunLoop 通常会切换到 UITrackingRunLoopMode 模式。这个模式会降低非滚动相关的事件(如未将定时器添加到 NSRunLoopCommonModes 模式则timer会暂停)处理优先级,从而确保滚动操作的流畅性 。

主线程是如何切换runloop?

当系统检测到有scrollerview滑动时,系统就会将当前进程的主线程切换到UITrackingRunLoopMode,直到滑动结束,又会切换到NSDefaultRunLoopMode。

模拟主线程runloop的mode切换。在touchbegan的时候切换到UITrackingRunLoopMode,touchend的时候又切换回NSDefaultRunLoopMode。从模拟中可以看出如果所切到的mode是timer未添加的,则timer会暂停。这也就是为什么NSTimer需要设置在NSRunLoopCommonModes模式下运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.rl = CFRunLoopGetCurrent();

//timer1 运行在 default mode
NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode]; // NSDefaultRunLoopMode

//timer2 运行在 track Mode
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer2 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode]; // UITrackingRunLoopMode

//指定当前运行mode
self.currentMode = NSDefaultRunLoopMode;
while (1) {
[[NSRunLoop currentRunLoop] runMode:self.currentMode beforeDate:[NSDate distantFuture]];
}
});


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch began")
//touchbegan 切换成track mode
self.currentMode = UITrackingRunLoopMode;
CFRunLoopStop(self.rl);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch end");
//touchend 切换成kCFRunLoopDefaultMode
self.currentMode = kCFRunLoopDefaultMode;
CFRunLoopStop(self.rl);
}

runloop与卡顿的关系

在iOS开发中,卡顿通常是由于主线程被长时间占用导致的。CFRunLoop的状态变化可以反映主线程的运行情况,因此通过监听CFRunLoop的状态,我们可以检测到应用的卡顿现象。

正常情况下,CFRunLoop会经历以下几个状态:

  1. kCFRunLoopEntry:即将进入RunLoop

  2. kCFRunLoopBeforeTimers:即将处理定时器。

  3. kCFRunLoopBeforeSources:即将处理输入源。

  4. kCFRunLoopBeforeWaiting:即将进入休眠。

  5. kCFRunLoopAfterWaiting:刚从休眠中唤醒。

  6. kCFRunLoopExit:即将退出RunLoop

为什么通常选择kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting这两个状态来作为卡顿的判断依据,而不使用kCFRunLoopBeforeWaitingkCFRunLoopBeforeTimers呢?

因为①卡顿通常发生在事件处理阶段,而kCFRunLoopBeforeWaiting状态标志着RunLoop即将进入休眠,而不是正在处理事件。当应用处于静止状态,即用户没有进行任何操作时,RunLoop通常处于kCFRunLoopBeforeWaiting状态,等待新的事件到来。在这种状态下,线程会进入休眠模式,以节省CPU资源。

②至于kCFRunLoopBeforeTimers状态,虽然它也是RunLoop状态之一,但它主要表示RunLoop即将处理定时器事件。如果定时器回调执行时间过长,确实可能导致卡顿,但是在实际应用中,定时器回调通常执行时间较短,且定时器回调的执行时间可以通过调整定时器的触发频率来控制,因此kCFRunLoopBeforeTimers状态不是卡顿判断的主要依据。

在没有卡顿的情况下,CFRunLoopkCFRunLoopBeforeSourceskCFRunLoopAfterWaiting这两个状态的停留时间通常是非常短的。kCFRunLoopBeforeSources状态表示RunLoop即将处理输入源,而kCFRunLoopAfterWaiting状态表示RunLoop从休眠中被唤醒。如果主线程在这两个状态之间花费的时间过长,说明线程可能被阻塞,导致应用无法响应用户操作,从而出现卡顿。

这两个状态是RunLoop循环中的关键点,它们分别代表了事件处理前后的状态。如果主线程在这两个状态之间花费的时间过长,说明线程可能被阻塞,导致应用无法响应用户操作,从而出现卡顿。因此,通过监控这两个状态,我们可以有效地检测和优化应用的性能,提高用户体验。

runloop与崩溃的关系

在 iOS 开发中,由于 RunLoop 导致的崩溃通常不是直接由 RunLoop 本身引起的,而是由于 RunLoop 中的事件处理代码存在问题。

《起死回生/回光返照》见《异常与崩溃.md

runtime

运行时(Runtime)是 Objective-C 语言的核心特性之一,它提供了一组丰富的 API,允许程序在运行时查询和修改程序的行为。这种动态性使得 Objective-C 语言具有很高的灵活性。

1、RunLoop概念

Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其内部就是do-while循环,这个循环内部不断地处理各种任务(比如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。

2、RunLoop和线程的关系?

run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程相关的基础框架的一部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop(以下都已Cocoa为例)。

每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。

①、主线程的run loop默认是启动的。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

②、对其它线程来说,run loop默认是没有启动的。

③、在任何一个Cocoa程序的线程中,都可以通过:NSRunLoop *runloop = [NSRunLoop currentRunLoop];来获取到当前线程的run loop。

3、RunLoop相关各类关系

在 CoreFoundation 里面关于 RunLoop 有5个类:

1
2
3
4
5
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系/RunLoop相关各类关系,如下图所示:

RunLoop相关各类关系
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

RunLoop的内部逻辑
RunLoop的内部逻辑

3.1 CFRunLoopSourceRef

Source有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

在iOS 中,除了source1可以自己唤醒run loop之外,其他的事件都需要用户手动唤醒run loop才可以。

3.1.附上题中button点击后,关于RunLoop的过程(此处略过对响应链的描述)

大概为:当一个硬件事件(触摸/锁屏/摇晃等)发生后,
①、首先由 IOKit.framework 生成一个 IOHIDEvent 事件,Source1 接收到系统事件,RunLoop被唤醒
②、RunLoop通知Observer,处理Timer和Source 0
③、RunLoop处理Source 1,Source1 触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
④、Springboard接受touch event,并用source1 的 之后mach port 转发给App进程。
⑤、RunLoop处理完毕进入睡眠,此前会释放旧的autorelease pool并新建一个autorelease pool。

3.2 CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调(NSTimer 其实就是 CFRunLoopTimerRef)。

3.3 CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

4、RunLoop的应用

最常见的为定时器 NSTimer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

讲到RunLoop,我们需明确一点。runloop会对timer有强引用,timer会对目标对象进行强引用

其他详细参考以下文章:

4.1、autoreleasepool 自动释放池

既然说到runloop,简单说下autoreleasepool自动释放池。runloop会默认创建autoreleasepool,在runloop睡眠前或者退出前会执行pop操作。线程池详情查看下面的内存管理中的介绍。

@autoreleasepool是自动释放池,让我们更自由的管理内存;所以我们下面说说内存管理。

4.2、runloop、autorelease pool以及线程之间的关系

END

其他参考文档:runloop

视图-③跳转

视图-③跳转

目录

一、视图的跳转

< 返回目录

1、获取当前显示的视图控制器ViewController

2、如何在多次presentViewController后直接返回到指定层

场景:如果多个控制器都通过 present 的方式跳转呢?比如从A跳转到B,从B跳转到C,从C跳转到D,如何由D直接返回到A呢?

答:可以通过 presentingViewController 一直找到A控制器,然后调用A控制器的 dismissViewControllerAnimated 方法。方法如下:

1
2
3
4
5
UIViewController *controller = self;
while(controller.presentingViewController != nil){
controller = controller.presentingViewController;
}
[controller dismissViewControllerAnimated:YES completion:nil];

PS:如果不是想直接返回到A控制器,比如想回到B控制器,while循环的终止条件可以通过控制器的类来判断。

3、presentedViewController 与 presentingViewController

假设从A控制器通过present的方式跳转到了B控制器,那么 A.presentedViewController 就是B控制器;
B.presentingViewController 就是A控制器。

4、如何通过视图(view)获取该视图所在的控制器(viewController)

1
2
3
4
5
6
7
8
+ (nullable UIViewController *)findBelongViewControllerForView:(UIView *)view {
UIResponder *responder = view;
while ((responder = [responder nextResponder]))
if ([responder isKindOfClass: [UIViewController class]]) {
return (UIViewController *)responder;
}
return nil;
}

常见笔试/面试题

< 返回目录

END

< 返回目录

视图-②生命周期

目录

五、控制器View的生命周期

< 返回目录

更详细的生命周期请查看:iOS程序执行顺序和UIViewController 的生命周期(整理)

题目1:控制器View的生命周期及相关函数是什么?你在开发中是如何用的?
1
2
3
4
1.在视图显示之前调用viewWillAppear;该函数可以调用多次; 
2.视图显示完毕,调用viewDidAppear;
3.在视图消失之前调用viewWillDisAppear;该函数可以调用多次(如需要);
4.在布局变化前后,调用viewWill/DidLayoutSubviews处理相关信息;

viewWillAppear——-》viewWillLayoutSubviews—–》viewDidLayoutSubviews———–》

viewDidAppear

题目2:loadView, viewDidLoad, viewDidUnLoad,分别是在什么时候被调用的.

loadView, viewDidLoad, viewDidUnLoad,分别是在什么时候被调用的.

3、layoutSubviews布局与drawRect重绘

(1)、layoutSubviews布局

layoutSubviews是对subviews重新布局;
比如,我们想更新子视图的位置的时候,可以通过调用layoutSubviews方法,即可以实现对子视图重新布局。但实际上一般我们都是不要直接手动调用layoutSubviews方法。因为有操作时候,系统会自动调用layoutSubviews。

那我们进行哪些操作会触发layoutSubviews方法呢?答如下:

1
2
3
4
5
6
7
8
9
首先注意:
①init初始化不会触发layoutSubviews,
但是使用initWithFrame进行初始化时,当rect的值不为CGRectZero时,会触发layoutSubviews。
②、直接调用setLayoutSubviews。
③、addSubview的时候一般都会触发layoutSubviews。(最常见) 注:但当本View的frame为0时,addSubView也不会调用layoutSubViews。
④、当view的frame发生改变的时候触发layoutSubviews。
⑤、滑动UIScrollView的时候触发layoutSubviews。
⑥、旋转Screen会触发父UIView上的layoutSubviews事件。
⑦、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。

所以我们可以看出当视图约束/frame变化时候,会触发layoutSubviews,进行重新布局。

1
2
3
4
5
6
7
8
9
10
附:
如果你还是想强制更新布局,你可以调用setNeedsLayout方法;
如果你想立即显示你的views,你需要调用layoutIfNeed方法。

①、- (void)layoutSubviews;
这个方法,默认没有做任何事情,需要子类进行重写;
②、- (void)setNeedsLayout;
标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用;
③、- (void)layoutIfNeeded;
如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。

其他参考文章:iOS layoutSubview的方法总结/重绘drawRect

什么时候用layoutSubviews?

答:仅仅在以下情况下:自动布局达不到想要效果时你才有必要重写这个方法.可以直接设置subviews的尺寸.

(2)、drawRect重绘

重绘作用:重写该方法以实现自定义的绘制内容

1
2
3
-drawRect:(CGRect)rect方法:重写此方法,执行重绘任务
-setNeedsDisplay方法:标记为需要重绘,异步调用drawRect
-setNeedsDisplayInRect:(CGRect)invalidRect方法:标记为需要局部重绘
(3)、updateConstraints更新约束、layoutSubviews重新布局与drawRect重绘的调用顺序
1
2
3
layoutSubviews是对subviews重新布局;
drawRect重绘;
layoutSubviews方法调用先于drawRect,也就是先布局子视图,在重绘。

所以,在调用updateConstraintsIfNeeded可能会立即执行updateConstraints,然后调用layoutSubviews。因为按照Autolayout布局的步骤,应该是先更新约束然后更新布局的。

常见笔试/面试题

< 返回目录

END

< 返回目录

视图-②布局

视图-②布局

[toc]

一、Intrinsic Content Size,Content Hugging Priority和Content Compression Resistance Priority

看一下下面的例子,看给出的例子约束是否完整?

1
2
3
4
5
6
7
8
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:15];
label.text = @"Hello";
[self.view addSubview:label];
[label mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view.mas_left).offset(16);
make.top.equalTo(self.view.mas_top).offset(16);
}];

这里只定义了两个约束,left 和 top,只够计算出frame的originX和orginY,没有width和height。那么是不是属于不完整的约束呢?其实在这里给出的约束已经是完整的了。因为对于UILabel这个控件而言 ,只要通过其font和text系统就可以计算出Label该有的长度和宽度。这里的长度和宽度就是UILabel的intrinsic content size(固有属性)。

Intrinsic Content Size, 通俗来讲,就是控件(UIButton,UILabel,UIImageView)能根据它们的内容(content)计算自己的大小(Size)

开发中用到的一些控件或视图,本身就自带大小,比如UIButton控件,设置完title后就能知道这个UIButton是文字的大小再加上两个固定的button margin。
像这种控件或视图本身就带有的高度、宽度,就叫做intrinsic content size(固定内容尺寸)。

2、浅谈 iOS AutoLayout 中 Label 的抗拉伸和抗压缩

在 Autolayout 优先级的范围是 1 ~ 1000,创建一个约束,默认的优先级是最高的 1000。

Content Hugging Priority:
该优先级表示一个控件抗被拉伸的优先级。优先级越高,越不容易被拉伸(即越容易保持原状),默认是251。

Content Compression Resistance Priority:
该优先级表示一个控件抗压缩的优先级。优先级越高,越不容易被压缩(即越容易保持原状),默认是750。

使用场景:

当一个视图上有多个 intrinsic content size 的子控件,并且子控件可能会超出父视图的区域时,此属性可控制哪些视图被内容被优先压缩,使其不超出父视图区域。

场景举例:

1
2
3
4
5
[[yellowLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.left.equalTo(self.view).offset(100);
make.right.equalTo(self.view).offset(-100);
}];

当yellowLable的宽度最多为screenWidth-200。

则我们想让lable对左右两边的约束性没那么高,可以设置

1
2
3
4
5
[yellowLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.left.equalTo(self.view).offset(100).priority(250);
make.right.equalTo(self.view).offset(-100).priority(250);
}];

给出一个比较常见的需求:

在同一行中显示标题和时间,时间必须显示完全,标题如果太长就截取可显示的部分,剩余的用…表示。

intrinsic content size

目标:我们想让绿色的时间显示全,则应该要压缩前面的titleLabel。也就是要降低titleLabel的抗压缩。

1
2
3
4
5
if (b) {
[timeLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// 或
//[titleLabel setContentHuggingPriority:UILayoutPriorityFittingSizeLevel forAxis:UILayoutConstraintAxisHorizontal];
}

UILayoutPriorityRequired:1000

UILayoutPriorityDefaultHigh:750

UILayoutPriorityDefaultLow:250

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//在同一行中显示标题和时间,时间必须显示完全,标题如果太长就截取可显示的部分,剩余的用…表示。
- (UIView *)contentViewWith:(BOOL)b {
UIView *contentView = [[UIView alloc] init];
contentView.backgroundColor = [UIColor lightGrayColor];

UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.backgroundColor = [UIColor redColor];
titleLabel.text = @"Each of these constraints can have its own priority. By default, ";
titleLabel.font = [UIFont systemFontOfSize:17];
[contentView addSubview:titleLabel];
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(contentView.mas_top);
make.left.equalTo(contentView.mas_left).offset(16);
}];

UILabel *timeLabel = [[UILabel alloc] init];
timeLabel.backgroundColor = [UIColor greenColor];
timeLabel.text = @"2017/03/12 18:20:22";
timeLabel.font = [UIFont systemFontOfSize:17];
[contentView addSubview:timeLabel];
[timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(titleLabel.mas_top);
make.left.equalTo(titleLabel.mas_right).offset(8);
make.right.lessThanOrEqualTo(contentView.mas_right).offset(-8);
}];

if (b) {
[timeLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// 或
//[titleLabel setContentHuggingPriority:UILayoutPriorityFittingSizeLevel forAxis:UILayoutConstraintAxisHorizontal];
}

return contentView;
}

二、iOS使用topLayoutGuide和bottomLayoutGuide

参考文章:iOS使用topLayoutGuide和bottomLayoutGuide

在iOS中,可以使用topLayoutGuide和bottomLayoutGuide来适配屏幕内容,它们是属于UIViewController的属性,配合masonry和SnapKit等约束工具,效果更好。

1
2
3
4
5
6
7
8
UIView *bottomPayView = [[UIView alloc] init];
bottomPayView.backgroundColor = [UIColor grayColor];
[self.view addSubview:bottomPayView];
[bottomPayView mas_makeConstraints:^(MASConstraintMaker *x) {
x.height.equalTo(@45);
x.left.right.equalTo(self.view);
x.bottom.equalTo(self.mas_bottomLayoutGuide);
}];

三、UITableView自动计算cell高度并缓存,再也不用管高度啦

UITableView自动计算cell高度并缓存,再也不用管高度啦

用xib加约束和用masonry加代码约束都是可以的。注意约束一定要自上而下加好,让系统知道怎么去计算高度。

加好约束后,然后告诉tableView自己去适应高度就可以了。有两种写法:

1
2
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 100;

或者直接写这个代理方法就可以了

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 100;
}

这个的意思就是告诉tableView,你需要自己适应高度,我不给你算啦哈哈哈。但是我们需要告诉它一个大概高度,例如上面的100,理论上这个是可以随便写的,并不影响显示结果,但是越接近真实高度越好。

可能遇到的问题和解决办法

1.高度不对
有时候有可能运行出来后看到cell的高度显示的不对。这个问题是因为约束没有满足自上而下,从而系统不知道怎么去计算。解决办法就是去修改约束,直到满足为止。一定要好好理解约束啊!

2.点击状态栏无法滚动到顶部
我们知道,如果界面中有UIScrollView的话,点击状态栏会让其滚动到顶部,就像这样:

但是如果我们用了自动计算高度的方法,又调用了tableView的reloadData方法(例如我们的数据有分页的时候,加载完下一页的数据后会去刷新tableView)。这时候就会出现问题,点击状态栏就有几率不能精确滚动到顶部了:

解决这个问题的办法是去缓存cell的高度,代码如下:

1
@property (nonatomic, strong) NSMutableDictionary *heightAtIndexPath;//缓存高度所用字典
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma mark - UITableViewDelegate
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
if(height)
{
return height.floatValue;
}
else
{
return 100;
}
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSNumber *height = @(cell.frame.size.height);
[self.heightAtIndexPath setObject:height forKey:indexPath];
}

四、问题

问题1:使用Masonry的时候进行updateConstraints没有效果

原因:使用updateConstraints更新的时候必须是makeConstraints里面设置过的约束。但如果只是这样还不行,还需要约束对象匹配才能成功。

问题详见:使用Masonry的时候进行updateConstraints没有效果

iOS11适配-Safe Area

iOS11适配-Safe Area

在iOS 11,UIViewController中的UIView的topLayoutGuide和bottomLayoutGuide被替换成了新的安全区属性。

1
2
3
4
5
6
> @available(iOS 11.0, *)
> open var safeAreaInsets: UIEdgeInsets { get }
>
> @available(iOS 11.0, *)
> open var safeAreaLayoutGuide: UILayoutGuide { get }12345
>

safeAreaInsets属性意味着屏幕可以被任何方向遮挡,并不只是上下,当iPhone X出现时,我们就明白了为什么我们需要对左右两边也进行缩进。

Masonry动画