iOS进阶干货分享iOS精品文章面试题大全

iOS 内存管理相关面试题

2019-04-26  本文已影响23人  _小迷糊_997

内存管理的一些概念

  • 为什么要使用内存管理?
    严格的内存管理,能够是我们的应用程在性能上有很大的提高
    如果忽略内存管理,可能导致应用占用内存过高,导致程序崩溃
  • OC的内存管理主要有三种方式:
    ARC(自动内存计数)
    手动内存计数
    内存池
  • OC中内存管理的基本思想:
    保证任何时候指向对象的指针个数和对象的引用计数相同,多一个指针指向这个对象这个对象的引用计数就加1,少一个指针指向这个对象这个对象的引用计数就减1。没有指针指向这个对象对象就被释放了。

1.每个对象都有一个引用计数器,每个新对象的计数器是1,当对象的计数器减为0时,就会被销毁
2.通过retain可以让对象的计数器+1、release可以让对象的计数器-1
3.还可以通过autorelease pool管理内存
4.如果用ARC,编译器会自动生成管理内存的代码

  • 苹果官方基础内存管理规则:
    你拥有你创建的任何对象
    你可以使用retain获取一个对象的拥有权
    当你不再需要它,你必须放弃你拥有的对象的拥有权
    你一定不能释放不是你拥有的对象的拥有权

自动内存管理

引用计数器

1.给对象发送一条retain消息,可以使引用计数器+1(retain方法返回对象本身)
2.给对象发送一条release消息,可以使引用计数器-1(注意release并不代表销毁/回收对象,仅仅是计数器-1)
3.给对象发送retainCount消息,可以获得当前的引用计数值

自动释放池

@property内存管理策略的选择

atomic
默认属性,访问方法都为原子型事务访问。锁被加到所属对象实例级,性能低。原子性就是说一个操作不可以中途被 cpu 暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的,那么在多线程环境下, 就不会出现变量被修改等奇怪的问题。原子操作就是不可再分的操作,在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是一些常见的多线程 Bug 的源头。当然,原子性的变量在执行效率上要低些。

nonatomic
非原子性访问。不加同步,尽量避免多线程抢夺同一块资源。是直接从内存中取数值,因为它是从内存中取得数据,它并没有一个加锁的保护来用于cpu中的寄存器计算Value,它只是单纯的从内存地址中,当前的内存存储的数据结果来进行使用。 多线程并发访问会提高性能,但无法保证数据同步。尽量避免多线程抢夺同一块资源,否则尽量将加锁资源抢夺的业务逻辑交给服务器处理,减少移动客户端的压力。
当有多个线程需要访问到同一个数据时,OC中,我们可以使用 @synchronized (变量)来对该变量进行加锁(加锁的目的常常是为了同步或保证原子操作)。

strong
strong 系统一般不会自动释放,在 oc 中,对象默认为强指针。作用域销毁时销毁引用。在实际开放中一般属性对象一般 strong 来修饰(NSArray,NSDictionary),在使用懒加载定义控件的时候,一般也用strong。

weak
weak 所引用对象的计数器不会加一,当对象被释放时指针会被自动赋值为 nil,系统会立刻释放对象。

__unsafe_unretained 弱引用 当对象被释放时指针不会被自动赋值为 ni
在ARC时属性的修饰符是可以用 assign 的(相当于 __unsafe_unretained)
在ARC时属性的修饰符是可以用 retain 的 (相当于 __strong)

假定有N个指针指向同一个对象,如果至少有一个是强引用,这个对象只要还在作用域内就不会被释放。相反,如果这N个指针都是弱引用,这个对象马上就被释放

在使用 sb 或者 xib 给控件拖线的时候,为什么拖出来的先属性都是用 weak 修饰呢?
由于在向 xib 或者 sb 里面添加控件的时候,添加的子视图是添加到了跟视图 View 上面,而 控制器 Controller 对其根视图 View 默认是强引用的,当我们的子控件添加到 view 上面的时候,self.view addSubView: 这个方法会对添加的控件进行强引用,如果在用 strong 对添加的子控件进行修饰的话,相当于有两条强指针对子控件进行强引用, 为了避免这种情况,所以用 weak 修饰。

注意:
(1)addSubView 默认对其 subView 进行了强引用
(2)在纯手码实现界面布局时,如果通过懒加载处理界面控件,需要使用strong强指针

内存分析

什么情况下会发生内存泄漏和内存溢出?
内存泄漏:堆里不再使用的对象没有被销毁,依然占据着内存。
内存溢出:一次内存泄露危害可以忽略,但内存泄露多了,内存迟早会被占光,最终会导致内存溢出!当程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如数据长度比较小的数据类型 存储了数据长度比较大的数据。

关于图片占用内存管理

部署版本在>=iOS8的时候,打包的资源包中的图片会被放到Assets.car。图片有被压缩;
部署版本在<iOS8的时候,打包的资源包中的图片会被放在MainBudnle里面。图片没有被压缩

没有放在Images.xcassets里面的所有图片会直接暴露在沙盒的资源包(main Bundle), 不会压缩到Assets.car文件,会被放到MainBudnle里面。图片没有被压缩

结论:
小图片\使用频率比较高的图片放在Images.xcassets里面
大图片\使用频率比较低的图片(一次性的图片, 比如版本新特性的图片)不要放在Images.xcassets里面

内存管理问题

单个对象内存管理的问题

多个对象内存管理的问题

内存相关的一些数据结构的对比

1、管理方式:
堆释放工作由程序员控制,容易产生memory leak;
栈是由编译器自动管理,无需我们手工控制。

2、申请大小:
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

3、碎片问题:
堆:频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
栈:则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出

4、分配方式:
堆都是动态分配的,没有静态分配的堆。
栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

5、分配效率:
栈:是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆:则是C/C++函数库提供的,它的机制是很复杂的。

面试题

** 如何让程序尽量减少内存泄漏**

block的注意

// block的内存默认在栈里面(系统自动管理)
void (^test)() = ^{
    
};
// 如果对block进行了Copy操作, block的内存会迁移到堆里面(需要通过代码管理内存)
Block_copy(test);
// 在不需要使用block的时候, 应该做1次release操作
Block_release(test);
[test release];
B *b = [[B alloc]init];
[self.view addSubview:b.view];

在对象的组合关系中,导致内存泄漏有几种情况?
1.set方法中没有retain对象
2.没有release掉旧的对象
3.没有判断向set方法中传入的是否是同一个对象

该如何正确的重写set方法?
1.先判断是否是同一个对象
2.release一次旧的对象
3.retain新的对象
写一个setter方法用于完成@property (nonatomic,retain)NSString *name,
写一个setter方法用于完成@property(nonatomic,copy)NSString *name。

@property (nonatomic, retain) NSString *name;
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

@property(nonatomic, copy) NSString *name;
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
}

- (void)dealloc {
    self.name = nil;
    // 上边这句相当于下边两句
    [_name release];
    _name = nil;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1
        Person *p = [[Person alloc] init];
        
        p.age = 20;
        
        // 0 (p指向的内存已经是坏内存, 称person对象为僵尸对象)
        // p称为野指针, 野指针: 指向僵尸对象(坏内存)的指针
        [p release];
        
        // p称为空指针
        p = nil;
        
        p.age = 40;
//        [0 setAge:40];
        
        // message sent to deallocated instance 0x100201950
        // 给空指针发消息不会报错
        [p release];
    }
    return 0;
}
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10; // 栈
        
        int b = 20; // 栈
        
        // c : 栈
        // Car对象(计数器==1) : 堆
        Car *c = [[Car alloc] init];
    }
    
    // 当autoreleasepool执行完后后, 栈里面的变量a\b\c都会被回收
    // 但是堆里面的Car对象还会留在内存中, 因为它是计数器依然是1
    
    return 0;
}
NSMutableArray* ary = [[NSMutableArray array] retain];  
NSString *str = [NSString stringWithFormat:@"test"];  // 1 
[str retain];   // 2
[ary addObject:str]; // 3  
NSLog(@"%d", [str retainCount]);  
[str retain];  // 4
[str release];   // 3
[str release];   // 2
NSLog(@"%d", [str retainCount]);  
[ary removeAllObjects]; // 1  
NSLog(@"%d", [str retainCount]);   

1.去掉所有的retain,release,autorelease
2.把NSAutoRelease替换成@autoreleasepool{}块
3.把assign的属性变为weak使用ARC的一些强制规定
4.dealloc方法来管理一些资源,但不能用来释放实例变量,也不能在dealloc方法里面去掉[super dealloc]方法,在ARC下父类的dealloc同样由编译器来自动完成
5.Core Foundation类型的对象任然可以用CFRetain,CFRelease这些方法
6.不能在使用NSAllocateObject和NSDeallocateObject对象
7.不能在c结构体中使用对象指针,如果有类似功能可以创建一个Objective-c类来管理这些对象
8.在id和void *之间没有简便的转换方法,同样在Objective-c和core Foundation类型之间的转换都需要使用编译器制定的转换函数
9.不能使用内存存储区(不能再使用NSZone)
10.不能以new为开头给一个属性命名
11.声明outlet时一般应当使用weak,除了对StoryBoard,这样nib中间的顶层对象要用strong
12.weak 相当于老版本的assign,strong相当于retain

原文地址:

上一篇下一篇

猜你喜欢

热点阅读