iOS 底层原理

② 内存管理相关

2021-02-24  本文已影响0人  長茳

一、 在 Obj-C 中,如何检测内存泄漏?你知道哪些方式?

目前我知道的方式有以下几种

泄露的内存主要有以下两种:

上面所说的五种方式,其实前四种都比较麻烦,需要不断地调试运行,第五种是腾讯阅读团队出品,效果好一些,感兴趣的可以看一下这两篇文章:

二、 在 MRC 下如何重写属性的 Setter 和 Getter ?

-(void)setBrand:(NSString *)brand{
//如果实例变量指向的地址和参数指向的地址不同
    if (_brand != brand)
    {
        //将实例变量的引用计数减一
        [_brand release];
       //将参数变量的引用计数加一,并赋值给实例变量
        _brand = [brand retain];
    }
}
-(NSString *)brand{
    //将实例变量的引用计数加1后,添加自动减1
    //作用,保证调用getter方法取值时可以取到值的同时在完全不需要使用后释放
    return [[_brand retain] autorelease];
}
//MRC下 手动释放内存 可重写dealloc但不要调用dealloc  会崩溃
-(void)dealloc{
    [_string release];
    //必须最后调用super dealloc
    [super  dealloc];
}

三、循环引用

循环引用的实质:多个对象相互之间有强引用,不能释放让系统回收。

如何解决循环引用?

1、避免产生循环引用,通常是将 strong 引用改为 weak 引用。
比如在修饰属性时用weak
在block内调用对象方法时,使用其弱引用,这里可以使用两个宏

#define WS(weakSelf)            __weak __typeof(&*self)weakSelf = self; // 弱引用

#define ST(strongSelf)          __strong __typeof(&*self)strongSelf = weakSelf; //使用这个要先声明weakSelf

还可以使用__block来修饰变量
在 MRC 下,__block不会增加其引用计数,避免了循环引用
在 ARC 下,__block修饰对象会被强引用,无法避免循环引用,需要手动解除。

2、在合适时机去手动断开循环引用。
通常我们使用第一种。

循环引用场景:

1、代理(delegate)循环引用属于相互循环引用

delegate 是iOS中开发中比较常遇到的循环引用,一般在声明delegate的时候都要使用弱引用 weak,或者assign,当然怎么选择使用assign还是weak,MRC的话只能用assign,在ARC的情况下最好使用weak,因为weak修饰的变量在释放后自动指向nil,防止野指针存在

2、NSTimer循环引用属于相互循环引用

在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,那么该对象和定时器就相互循环引用了。
如何解决呢?
这里我们可以使用手动断开循环引用:
如果是不重复定时器,在回调方法里将定时器invalidate并置为nil即可。
如果是重复定时器,在合适的位置将其invalidate并置为nil即可

3、block循环引用

一个简单的例子:

@property (copy, nonatomic) dispatch_block_t myBlock;
@property (copy, nonatomic) NSString *blockString;

- (void)testBlock {
    self.myBlock = ^() {
        NSLog(@"%@",self.blockString);
    };
}

由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。
解决方案就是使用__weak修饰self即可

__weak typeof(self) weakSelf = self;

self.myBlock = ^() {
        NSLog(@"%@",weakSelf.blockString);
 };
[self testWithBlock:^{
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();
}

还有一种场景,在block执行开始时self对象还未被释放,而执行过程中,self被释放了,由于是用weak修饰的,那么weakSelf也被释放了,此时在block里访问weakSelf时,就可能会发生错误(向nil对象发消息并不会崩溃,但也没任何效果)。
对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有,block执行结束后,解除其持有。

__weak typeof(self) weakSelf = self;

self.myBlock = ^() {

        __strong __typeof(self) strongSelf = weakSelf;

        [strongSelf test];
 };

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

悬垂指针
指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 迷途指针

野指针
没有进行初始化的指针,其实都是 野指针

五、 说一下对 retain,copy,assign,weak,_Unsafe_Unretain 关键字的理解

Strong

Strong 修饰符表示指向并持有该对象,其修饰对象的引用计数会加1。该对象只要引用计数不为0就不会被销毁。当然可以通过将变量强制赋值 nil 来进行销毁。

Weak

weak 修饰符指向但是并不持有该对象,引用计数也不会加1。在 Runtime 中对该属性进行了相关操作,无需处理,可以自动销毁。weak 用来修饰对象,多用于避免循环引用的地方。weak 不可以修饰基本数据类型。

assign

assign 主要用于修饰基本数据类型,
例如 NSInteger,CGFloat,存储在栈中,内存不用程序员管理。assign 是可以修饰对象的,但是会出现问题。

copy

copy 关键字和 strong 类似,copy 多用于修饰有可变类型的不可变对象 NSString, NSArray, NSDictionary上。

__unsafe_unretain

__unsafe_unretain 类似于 weak ,但是当对象被释放后,指针依然保存着之前的地址,被释放后的地址变为 僵尸对象,访问被释放的地址就会出问题,所以说他是不安全的。

__autoreleasing

将对象赋值给附有 __autoreleasing 修饰的变量等同于 ARC 无效时调用对象的 autorelease 方法,实质就是扔进了自动释放池。

六、是否了解 深拷贝 和 浅拷贝 的概念,集合类深拷贝如何实现

简而言之:

1、对不可变的非集合对象,copy是指针拷贝,mutablecopy是内容拷贝

2、对于可变的非集合对象,copy,mutablecopy都是内容拷贝

3、对不可变的数组、字典、集合等集合类对象,copy是指针拷贝,mutablecopy是内容拷贝

4、对于可变的数组、字典、集合等集合类对象,copy,mutablecopy都是内容拷贝

但是,对于集合对象的内容复制仅仅是对 对象本身,但是对 对象的里面的元素还是指针复制。要想复制整个集合对象,就要用集合深复制的方法,有两种:

(1)使用initWithArray:copyItems:方法,将第二个参数设置为YES即可

NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];

(2)将集合对象进行归档(archive)然后解归档(unarchive):

NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

七、 使用自动引用计数应遵循的原则

八、 能不能简述一下 Dealloc 的实现机制

Dealloc 的实现机制是内存管理部分的重点,把这个知识点弄明白,对于全方位的理解内存管理的是很有必要。

1.Dealloc 调用流程

2.object_dispose() 调用流程。

3.objc_destructInstance() 调用流程

4.clearDeallocating() 调用流程。

九、 内存中的5大区分别是什么?

十、 内存管理默认的关键字是什么?

@property (atomic,readWrite,retain) UIView *view;
@property (atomic,readWrite,strong) UIView *view;

如果改为基本数据类型,那就是 assign

十一、内存管理方案

自旋锁:

引用计数表和弱引用表实际是一个哈希表,来提高查找效率。

十二、内存布局

2、64bit和32bit下 longchar*所占字节是不同的

3、static、const 和 sizeof 关键字

static关键字

答:Static的用途主要有两个,一是用于修饰存储类型使之成为静态存储类型,二是用于修饰链接属性使之成为内部链接属性。

在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值不会被销毁,函数下次运行时能仍用到这个值。

在函数外定义的静态变量——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。

静态函数只能在声明它的源文件中使用。

const关键字

const int a = 5;/*a的值一直为5,不能被改变*/

const int b; b = 10;/*b的值被赋值为10后,不能被改变*/

const int *ptr; /*ptr为指向整型常量的指针,ptr的值可以修改,但不能修改其所指向的值*/

int *const ptr;/*ptr为指向整型的常量指针,ptr的值不能修改,但可以修改其所指向的值*/

const int *const ptr;/*ptr为指向整型常量的常量指针,ptr及其指向的值都不能修改*/

int fun(const int a);或int fun(const char *str);
const char *getstr(void);使用:const *str= getstr();

const int getint(void);  使用:const int a =getint();

sizeof关键字

sizeof是在编译阶段处理,且不能被编译为机器码。sizeof的结果等于对象或类型所占的内存字节数。sizeof的返回值类型为size_t。

(1)展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。

(2)结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。

十三、 讲一下 iOS 内存管理的理解

实际上是三种方案的结合

十四、 讲一下 @dynamic 关键字?

@dynamic 意味着编译器不会帮助我们自动合成 setter 和 getter 方法。我们需要手动实现、这里就涉及到 Runtime 的动态添加方法的知识点。

十五、 简要说一下 @autoreleasePool 的数据结构?

简单说是双向链表,每张链表头尾相接,有 parent、child指针

每创建一个池子,会在首部创建一个 哨兵 对象,作为标记

最外层池子的顶端会有一个 next 指针。当链表容量满了,就会在链表的顶端,并指向下一张表。

十六、 访问 __weak 修饰的变量,是否已经被注册在了 @autoreleasePool 中?为什么?

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

十七、 retain、release 的实现机制?

1.Retain 的实现机制。

SideTable& table = SideTables()[This];

size_t& refcntStorage = table.refcnts[This];

refcntStorage += SIZE_TABLE_RC_ONE;

2.Release 的实现机制。

SideTable& table = SideTables()[This];

size_t& refcntStorage = table.refcnts[This];

refcntStorage -= SIZE_TABLE_RC_ONE;

二者的实现机制类似,概括讲就是通过第一层 hash 算法,找到 指针变量 所对应的 sideTable。然后再通过一层 hash 算法,找到存储 引用计数 的 size_t,然后对其进行增减操作。retainCount 不是固定的 1,SIZE_TABLE_RC_ONE 是一个宏定义,实际上是一个值为 4 的偏移量。

十八、MRC(手动引用计数)和ARC(自动引用计数)

1、MRC:alloc,retain,release,retainCount,autorelease,dealloc
2、ARC:

3、引用计数管理:

4、弱引用管理:

5、自动释放池:

在当前runloop将要结束的时候调用objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool

AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成,是和线程一一对应的。
内部属性有parent,child对应前后两个结点,thread对应线程 ,next指针指向栈中下一个可填充的位置。

编译器会将 @autoreleasepool {} 改写为:

void * ctx = objc_autoreleasePoolPush;
    {}
objc_autoreleasePoolPop(ctx);

十九、 BAD_ACCESS 在什么情况下出现?

访问了已经被销毁的内存空间,就会报出这个错误。
根本原因是有 悬垂指针 没有被释放。

二十、 autoReleasePool 什么时候释放?

App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池; Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

二十一、 ARC自动内存管理的原则

二十二、 ARC 在编译时做了哪些工作

根据代码执行的上下文语境,在适当的位置插入 retain,release

二十三、 ARC 在运行时做了哪些工作?

二十四、 ARC 的 retainCount 怎么存储的?

存在64张哈希表中,根据哈希算法去查找所在的位置,无需遍历,十分快捷

散列表(引用计数表、weak表)
- SideTables 表在 非嵌入式的64位系统中,有 64张 SideTable 表
- 每一张 SideTable 主要是由三部分组成。自旋锁、引用计数表、弱引用表。
- 全局的 引用计数 之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。
- 引用计数表 中引入了 分离锁 的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,提升执行效率

引用计数表(哈希表)

通过指针的地址,查找到引用计数的地址,大大提升查找效率

通过 DisguisedPtr(objc_object) 函数存储,同时也通过这个函数查找,这样就避免了循环遍历。

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

用的弱引用 - weak表。也是一张 哈希表。

被 weak 修饰的指针变量所指向的地址是 key ,所有指向这块内存地址的指针会被添加在一个数组里,这个数组是 Value。当内存地址销毁,数组里的所有对象被置为 nil。

二十六、 __weak 和 _Unsafe_Unretain 的区别?

weak 修饰的指针变量,在指向的内存地址销毁后,会在 Runtime 的机制下,自动置为 nil。

_Unsafe_Unretain 不会置为 nil,容易出现 悬垂指针,发生崩溃。但是 _Unsafe_Unretain 比 __weak 效率高。

二十七、 为什么已经有了 ARC ,但还是需要 @AutoreleasePool 的存在?

避免内存峰值,及时释放不需要的内存空间

二十八、 函数返回一个对象时,会对对象 autorelease 么?为什么?

会 ,为了延长返回对象的生命周期,给其他使用者留足调用的时间

二十九、什么情况使用 weak 关键字,相比 assign 有什么不同?

三十、 如何让自己的类用 copy 修饰符?即让自己写的对象具备拷贝功能?如何重写带copy 关键字的 setter ?

    - (void)setName:(NSString *)name {
        //[_name release];
        _name = [name copy];
    }    

三十一、@property的本质是什么?ivar、getter、setter是如何生成并添加到这个类中的?

“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。

完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。
除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加上下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.

三十二、@protocol和category中如何使用@property?

上一篇下一篇

猜你喜欢

热点阅读