美文网首页
iOS 整理-内存篇

iOS 整理-内存篇

作者: 凉秋落尘 | 来源:发表于2019-11-26 09:52 被阅读0次

最近没什么事,开始看一些面试可能会遇到的问题,主要是边看边记,在面试的时候经常会被问到,需要的小伙伴可以参考下:

1、@property 属性声明的关键字
2、weak和assign的区别
3、关键字__block的作用
4、block为什么用copy修饰
5、block之循环引用
6、浅拷贝和深拷贝的区别
7、NSString为什么用copy修饰
8、对象拷贝之copy 与 mutableCopy
9、内存中的5大区都是什么?
10、谈谈iOS内存管理的理解
11、说一下什么是悬垂指针?什么是野指针?
12、ARC下使用@autoReleasePool
13、tableView该怎么做性能优化
14、在 MRC 下如何重写属性的 Setter 和 Getter?
15、访问 __weak 修饰的变量,是否已经被注册在了 @autoreleasePool 中?为什么?
16、__weak 属性修饰的变量,如何实现在变量没有强引用后自动置为 nil

@property 属性声明的关键字

atomic:原子性是指事务的一个完整操作,保证多线程下操作安全,增加同步锁。
nonatomic:非原子操作,不考虑线程安全,效率高。
strong:表示持有特性,为属性设置新值的时候,设置方法会先保留新值(新值的引用计数加一),并释放旧值(旧值的引用计数减一),然后将新值赋值上去,相当于MRC下的retain。
retain 表示持有特性,为属性设置新值的时候,设置方法先保留,再赋值,传入参数的retaincount会+1;
weak表示非持有特性,为属性设置新值的时候,设置方法既不会保留新值,也不会释放旧值。当属性所指的对象释放的时候,属性也会被置为nil。
assign用来修饰基本数据类型和对象。当assign用来修饰对象的时候,和weak类似。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为nil,这就会产生野指针。
copy修饰的属性设置新值的时候,当新值是不可变的,和strong一样。当新值是可变类型,设置方法不会保留新值,而是对新值copy一份,不会影响新值的引用计数。copy常用来修饰NSString,因为当新值是可变的,防止属性在不知不觉中被修改。
readwrite属性同时可使用 set 和 get 方法。
readonly属性只能使用get 方法,不具备set方法,因此不能对其进行赋值操作。
unsafe_unretained用来修饰属性的时候,和assign修饰对象的时候是一模一样的。为属性设置新值的时候,设置方法既不会保留新值,也不会释放旧值。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为nil,这就会产生野指针,所以是不安全的。

weak和assign的区别

释放对象是否产生野指针,适用oc类型还是基本类型。

  1. 修饰类型不同
    weak只能用于修饰OC对象类型;
    assign既可以修饰OC类型也可以修饰基本类型;

  2. 释放是是否产生野指针
    当weak对象被释放是,对象的指针会被设置为nil,因此再次去对该对象发送消息,不会崩溃,因此weak是安全的;
    assign当对象被释放的时候,对象的指针不会置空,该对象会变成野指针,再次对该对象发送消息,会直接崩溃。

总结:
assign:适用于基本数据类型(int),因为基本数据类型存放在栈中,采用先进先出的原则,用系统自动分配释放管理内存。如果使用在对象类型,存放在堆中,需要考虑野指针的问题,则程序员要手动分配释放或者ARC下内存自动管理分配。
weak:适用于oc对象类型,同时也适用于delegate,不会产生野指针,也不会循环引用,非常安全。

block为什么用copy修饰

copy修饰将存放在栈的block,保存到堆,不会产生野指针,再次调用也就不会出现异常。

首先block有三种类型:
NSGlobalBlock:全局的静态block 没有访问外部变量(也就是block没有调用其他外部变量)。

void(^testBlock)() = ^(){
        NSLog(@"我是全局的block");
    };

NSStackBlock:保存在栈中的block,没有用copy去修饰并且访问了外部变量,默认的block类型就是这种,会在函数调用结束被销毁 (需要在MRC)。

int a=0;
void(^testBlock)() = ^(){
        NSLog(@"我是栈的block >> %d", a);
    };

NSMallocBlock 保存在堆中的block 此类型blcok是用copy修饰出来的block 它会随着对象的销毁而销毁,只要对象不销毁,我们就可以调用。

@property (copy) testBlock block;

self.block = ^(){
        NSLog(@"我是栈的block >> %d", a);
    };

栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃。默认情况下,block是存放在栈中即NSStackBlock,因此block在函数调用结束时,对象会变成nil,但是对象的指针变成野指针,因此对象继续调用会产生异常。使用copy修饰之后,会将block对象保存到堆中NSMallocBlock,它的生命周期会随着对象的销毁而结束的。所以函数调用结束之后指针也会被设置为nil,再次调用该对象也不会产生异常。

关键字__block的作用

__block用于解决外部无法修改内部变量的问题

int age = 10;
void (^myblock)(void) =  ^{
  NSLog(@"%d",age);
};
age  = 20;

如果直接定义局部变量,最后结果为10,age赋值无法改变内部的结果,因为传入的是变量的值,并非变量指针,类似深拷贝。

__block  int age = 10;
void (^myblock)(void) =  ^{
  NSLog(@"%d",age);
};
age  = 20;

如果定义的局部变量用__block声明,最后结果为20,block内部引用变量本身,并非对象指针,类似浅拷贝。
而我们一般使用过程中,往往需要内部和外部为同一个变量,因此需要用到__block来声明。

block之循环引用

[深入浅出Block循环引用的底层解析]https://www.jianshu.com/p/42741fe8c932
[Block导致循环引用的问题]https://www.jianshu.com/p/fc2f4d207d25

浅拷贝和深拷贝的区别

浅拷贝是拷贝对象指针地址,指向同一个对象。
深拷贝是拷贝对象本身,源对象和目标对象互不干涉。

当对象创建的时候,系统为对象开辟一块内存空间,并给对象一个指向该内存空间的指针地址。对象的浅拷贝和深拷贝区别就是,新对象是否创建一个新的内存空间,或者直接拷贝地址。

浅拷贝对内存地址的复制,即源对象指针和目标对象指针指向同一个内存空间的,当对象被销毁时,指向这片内存的所有指针都变成野指针。当两个指针都指向同一个对象,使得原来对象的引用计数+1。同时不管是对源对象或者目标对象操作都是相当于对同一个对象操作,以此对两个变量进行操作,会影响两个变量的值。

深拷贝对对象本身的复制,即目标对象将会创建一个新的内存空间,当目标对象与源对象之间互不干涉。虽然存的值是相同的,但是不管指针地址或者对象本身都是不一样的,不会引起引用计数的变化,也不会影响源对象和目标对象的操作,也不会影响各种的值变化。

NSString为什么用copy修饰

可变类型时,使用 copy进行深拷贝,进行数据安全。

这涉及到深拷贝和浅拷贝的问题,当源对象时可变情况,对可变数组进行赋值。只是进行浅拷贝,拷贝对象指针地址,指向同一个内存空间。因此再次改变对象时,使用strong的对象也会被改变,copy的对象则进行深拷贝互不干涉,涉及到安全性问题。

  NSMutableString *str1 = [[NSMutableString alloc]initWithString:@"hello word"];
    self.strongStr = str1;
    self.copStr = str1;
    [str1 appendString:@"_test"];
    NSLog(@"%@,  %@,  %@",str1,self.strongStr,self.copStr);
    NSLog(@"%p,  %p,  %p",str1,self.strongStr,self.copStr);

打印结果:
hello word_test,  hello word_test,  hello word
0x2837b79c0,   0x2837b79c0,  0x2839ee5e0

当使用不可变对象进行赋值时,strong和copy效果是一样的:

 NSString *str1 = @"hello word";
    self.strongStr = str1;
    self.copStr = str1;
str1 = @"test1";
    [str1 appendString:@"_test"];
    NSLog(@"%@,  %@,  %@",str1,self.strongStr,self.copStr);
    NSLog(@"%p,  %p,  %p",str1,self.strongStr,self.copStr);

打印结果:
test1,  hello word,  hello word
0x104504028,  0x104504008,  0x104504008

用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

1.因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。

2.如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。

总结:使用copy的目的是,防止把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发送变化会无意间篡改不可变类型对象原来的值。

对象拷贝之copy 与 mutableCopy
  1. copy 返回的是不可变对象(immutableObject);如果用copy返回值调用mutable对象的方法就会crash。
  2. mutableCopy 返回的是可变对象(mutableObject)。
    不止是OC对象类型会涉及到浅拷贝和深拷贝的问题,自定义类对象也会遇到同样的问题。NSCopingNSMutableCopying协议就是用来解决类对象拷贝问题。继承协议并分别实现方法- (id)copyWithZone:(NSZone *)zone- (id)mutableCopyWithZone:(NSZone *)zone
NSArray *arr1 = @[@"test1", @"test2", @"test3"];
NSArray *copyArr1 = [nameArray copy];
NSMutableArray *mutableCopyArr1 = [nameArray mutableCopy];
[mutableCopyArr1 addObject:@"Sam"];

当调用copymutableCopy时会去调用相应的copyWithZonemutableCopyWithZone方法。
区别在于返回的对象是否是可变的,因此自定义类如果包括可变和不可变的属性是,需要同时实现两个协议,才能实现深拷贝。

内存中的5大区域是什么?

代码区,常量区,全局静态区,堆(heap),栈(stack)

在iOS开发过程中,为了合理的分配有限的内存空间,将内存区域分为五个区,由低地址向高地址分类分别是:代码区、常量区、全局静态区、堆、栈。
代码区:用来存放二进制代码,在运行时要防止非法修改,只允许读取,不允许操作。
常量区:存储常量数据,通常程序结束后由系统自动释放。
全局静态区:全局区又可分为未初始化全局区:.bss段和初始化全局区:data段。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,在程序结束后有系统释放。
堆(heap):由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。堆是向高地址扩展的数据结构,是不连续的内存区域,以链表的方式进行存储。
栈(stack):栈是由编译器自动分配并释放,存放函数的参数值,局部变量的值等。栈是向低地址扩展的数据结构,是不连续的内存区域,采用后进先出(LIFO )。

栈是连续的存储空间,且栈的大小是有限的,采用后进先出有序的创建和释放,因此栈排序不会出现不连续和内存浪费现象。堆是不连续的存储内存区域,是以链表的方式存储。当创建对象时,会寻找大于或等于申请的heap 节点长度的内存空间,频繁的创建和删除内存块势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。由此引入内存管理的概念,以保证高效、快速的分配内存,并且在适当的时候释放和回收内存资源。

谈谈iOS内存管理的理解

内存管理三种方式?

MRC(手动管理引用计数),ARC(自动管理引用计数),自动释放池(autorelease)

在OC中主要分为两种数据类型:基本数据类型(int,float)和对象数据类型(NSString,NSArray)。基本数据类型是存放在栈里面,由系统自动分配释放。对象数据类型则存放在堆中,用链表的方式存储,这就需要程序员手动分配释放。系统内存是有限的,为了能合理的去使用,避免内存不足现象,在运行结束时就需要释放掉多余的内存,也就是需要去管理对象的内存空间。

怎么去管理内存呢?oc使用引用计数的抽象概念,当对象引用计数大于0时,说明该对象依然还被使用,此时不会被回收;当对象的引用计数为0时,说明该对象已经没有再持有或被持有,则对象会被系统回收,释放掉内存空间;

引用计数的数值何时产生变化?

  • 使用alloc、new、copy、mutableCopy这四个关键字创建的对象,自身引用计数为1
  • 当引用到该对象会发送一条retain消息,使引用计数器值+1,说明该对象存在
  • 该对象不再持有或被持有时,发送一条release消息,使引用计数值-1,说明该对象不存在
  • 引用计数值为0时,说明对象不再被使用,自动调用delloc方法,将该对象释放掉。

内存管理中引用计数的原则?

  • 自己生成的对象,自己持有(alloc、new、copy、mutableCopy)。
  • 非自己生成的对象,自己也能持有(retain)。
  • 不再需要自己持有的对象时释放(release,delloc)。
  • 非自己持有的对象无法释放。

Autorelease自动释放池
1、会将对象放到一个自动释放池中
2、当自动释放池被销毁时,会对池子里的所有对象做一次release
3、会返回对象本身
4、调用完autorelease方法后,对象的计数器不受影响(销毁时影响)

说一下什么是悬垂指针?什么是野指针?

悬垂指针:指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 迷途指针
野指针:没有进行初始化的指针,其实都是 野指针

ARC下使用@autoReleasePool

使用ARC的方式去管理,会帮你在合适的地方插入autoReleasePool/release/retain。这将导致内存的延迟释放,短时间内一些操作引起内存峰值。@autoReleasePool就是为了在一定程度上解决。为了防止在某一操作的时候,该操作瞬时占用的内存过大(比如for循环),导致程序瞬时的闪退。在对象的创建者没法销毁对象的时候,去检查并释放掉内存。

以下的一些方法中,可能需要注意使用到:

1、编写不基于UI框架的程序,比如命令行工具。
2、编写的循环创建了很多临时对象。 可以在循环中使用自动释放池block,在下次迭代前处理这些对象。在循环中使用自动释放池block,有助于减少应用程序的内存占用。
3、生成了一个辅助线程。 一旦线程开始执行你必须自己创建自动释放池。否则,应用将泄漏对象。

tableView该怎么做性能优化

1、cell的重用
2、提前计算好cell属性和内容(如cell高度、逻辑计算)
3、减少cell中控件的数量
4、不要使用ClearColor,无背景色,透明度也不要设置为0(渲染比较长)
5、加载网络数据,下载图片,使用异步加载,并缓存
6、少使用addview给cell动态添加view
7、按需加载cell,cell滚动很快时,只加载范围内的cell

在 MRC 下如何重写属性的 Setter 和 Getter?
-(void)setName:(NSString *) name{
//如果实例变量指向的地址和参数指向的地址不同
    if (_name != name)
    {
        //将实例变量的引用计数减1
        [_name release];
       //将参数变量的引用计数加1,并赋值给实例变量
        _name = [name retain];
    }
}
-(NSString *)name{
    //将实例变量的引用计数加1后,添加自动减1
    //作用,保证调用getter方法取值时可以取到值的同时在完全不需要使用后释放
    return [[_brand retain] autorelease];
}
//MRC下 手动释放内存 可重写dealloc但不要调用dealloc  会崩溃
-(void)dealloc{
    [_string release];
    //必须最后调用super dealloc
    [super  dealloc];
}
访问 __weak 修饰的变量,是否已经被注册在了 @autoreleasePool 中?为什么?

__weak修饰的变量属于弱引用,如果没有被注册到 @autoreleasePool 中,创建之后也就会随之销毁,为了延长它的生命周期,必须注册到 @autoreleasePool 中,以延缓释放。

__weak 属性修饰的变量,如何实现在变量没有强引用后自动置为 nil

__weak修饰的变量,会加入到弱引用表。也是一张 哈希表。被 weak 修饰的指针变量所指向的地址是 key ,所有指向这块内存地址的指针会被添加在一个数组里,这个数组是 Value。当内存地址销毁,数组里的所有对象被置为 nil。

相关文章

网友评论

      本文标题:iOS 整理-内存篇

      本文链接:https://www.haomeiwen.com/subject/wkfewctx.html