iOS 面试第六节 内存管理

1. proprety 介绍 实例对象的内存结构、类对象内存结构、元类对象内存结构

proprety解析
iOS底层原理探索—OC对象的本质

在这里插入图片描述
实例对象(instance对象)的isa指针指向class。当调用对象方法时,通过实例对象的isa找到class,最后找到对象方法的实现进行调用
类对象(class对象)的isa指针指向meta-class。当调用类方法时,通过类对象的isa找到meta-class,最后找到类方法的实现进行调用。

非常经典的isa指向图
在这里插入图片描述
进一步说明:
1、instance的isa指向clas
2、class的isa指向meta-class
3、meta-class的isa指向基类的meta-class,基类的isa指向自己
4、class的superClass指向父类的class,如果没有父类,则superClass指针为nil
5、meta-class的superClass指向父类的meta-class,基类的meta-class的superClass指向基类的class
6、instance调用对象方法的轨迹:通过isa找到class,方法不存在,就通过supercla逐层父类里找,有就实现,如果找到基类仍没有找到,就会抛出unrecognized selector sent to instance异常
7、class调用类方法的轨迹:通过isa找到meta-class,方法不存在,就通过superClass逐层父类里找。

总结:
一个NSObject对象占用多少内存?
答:系统会为一个NSObject对象分配最少16个字节的内存空间。一个指针变量所占用的大小(64bit占8个字节,32bit占4个字节)
对象的isa指针指向哪里?
答:instance对象的isa指针指向class对象,class对象的isa指针指向meta-class对象,meta-class对象的isa指针指向基类的meta-class对象,基类自己的isa指针指向自己。
OC的类信息存放在哪里?
答:成员变量的具体值存放在实例对象(instance对象);对象方法,协议,属性,成员变量信息存放在类对象(class对象);类方法信息存放在元类对象(meta-class对象)。

下面是关于copy内容
在这里插入图片描述

  • proprety本质
    @property = 实例变量(iva) + get方法 + set方法

  • 自动合成
    如果没有重写其set、get方法的时候,进行自动合成一个类经过编译后,会生成变量列表ivar_list,方法列表method_list,每添加一个属性,在变量列表ivar_list会添加对应的变量,如_name,方法列表method_list中会添加对应的setter方法和getter方法

  • 动态合成
    对于一个可读写的属性来说,当我们重写了其setter、getter方法时,编译器会认为开发者想手动管理@property,此时会将@property作为@dynamic来处理,因此也就不会自动生成变量。
    如果一个属性是只读的,重写了其getter方法时,编译器也会认为该属性是@dynamic

    非自动合成又称为动态合成。定义一个属性,默认是自动合成的,默认会生成getter方法和setter方法,这也是为何我们可以直接使用self.属性名的原因。实际上,自动合成对应的代码是:

@synthesize name = _name;

这行代码是编译器自动生成的,无需我们来写。相应的,如果我们想要动态合成,需要自己写如下代码:

@dynamic sex;

这样代码就告诉编译器,sex属性的变量名、getter方法、setter方法由开发者自己来添加,编译器无需处理。

那么这样写和自动合成有什么区别呢?来看下面的代码:

Student *stu = [[Student alloc] init];
stu.sex = @"male";

编译,不会有任何问题。运行,也没问题。但是当代码执行到这一行的时候,程序崩溃了,崩溃信息是:

[Student setSex:]: unrecognized selector sent to instance 0x60000217f1a0

即:Student没有setSex方法,没有属性sex的setter方法。**这就是动态合成和自动合成的区别。动态合成,需要开发者自己来写属性的setter方法和getter方法。**添加上setter方法:

- (void)setSex:(NSString *)sex
{
    _sex = sex;
}

由于使用@dynamic,编译器不会自动生成变量,因此除此之外,还需要手动定义_sex变量,如下:

@interface Student : NSObject
{
    NSString *_sex;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@end

现在再编译,运行,执行没有错误和崩溃。

上面的例子中,重写了属性name的getter方法和setter方法,如下:

- (void)setName:(NSString *)name
{
    NSLog(@"rewrite setter");
    _name = name;
}

- (NSString *)name
{
    NSLog(@"rewrite getter");
    return _name;
}

但是编译器会提示错误,错误信息如下:

Use of undeclared identifier '_name'; did you mean 'name'?

提示没有_name变量。为什么呢?我们没有声明@dynamic,那默认就是@autosynthesize,为何没有_name变量呢?奇怪的是,倘若我们把getter方法,或者setter方法注释掉,gettter、setter方法只留下一个,不会有错误,为什么呢?
还是编译器做了些处理。对于一个可读写的属性来说,当我们重写了其setter、getter方法时,编译器会认为开发者想手动管理@property,此时会将@property作为@dynamic来处理,因此也就不会自动生成变量。解决方法,显示的将属性和一个变量绑定

@synthesize name = _name;

这样就没问题了。如果一个属性是只读的,重写了其getter方法时,编译器也会认为该属性是@dynamic,关于可读写、只读,下面会介绍。这里提醒一下,当项目中重写了属性的getter方法和setter方法时,注意下是否有编译的问题。

2.Object-C语言中常用的属性proprety有哪些?有什么区别吗?

  • readwrite
    表示可读可写,set/get方法编译器都会自动合成。

  • readonly
    表示该属性是只读的,编译器只会自动合成get方法, 不会合成set方法。

  • retain
    表示一种“拥有关系”。为属性设置新值的时候,设置方法会先保留新值(新值的引用计数加一),并释放旧值(旧值的引用计数减一),然后将新值赋值上去。相当于MRC下的retain。

  • nonatomic

  • atomic
    atomic修饰的属性的存取方法是线程安全的,但是并不能保证绝对安全。因为如果绕过set方法给属性赋值,就是线程不安全的了。

  • unsafe_unretained
    用来修饰属性的时候,和assing修饰对象的时候是一模一样的。为属性设置新值的时候,设置方法既不会保留新值(新值的引用计数加一),也不会释放旧值(旧值的引用计数减一)。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为nil,这就会产生野指针,所以是不安全的。

  • strong

    表示一种“拥有关系”。为属性设置新值的时候,设置方法会先保留新值(新值的引用计数加一),并释放旧值(旧值的引用计数减一),然后将新值赋值上去。相当于MRC下的retain。

    可变对象使用strong,不可变对象使用copy
    但是如果我们不可变对象使用strong修饰时候会发生什么错误呢?

    首先明确一点,既然类型是NSString,那么则代表我们不希望testStr被改变,否则直接使用可变对象NSMutableString就可以了。另外需要提醒的一点是,NSMutableString是NSString的子类,对继承了解的应该都知道,子类是可以用来初始化父类的。
    我们定义的不可变对象strongStr,在开发者无感知的情况下被篡改了

@property (nonatomic, strong) NSString *strongStr;
- (void)testStrongStr
{
    NSString *tempStr = @"123";
    NSMutableString *mutString = [NSMutableString stringWithString:tempStr];
    self.strongStr = mutString;  // 子类初始化父类
    NSLog(@"self str = %p  mutStr = %p",self.strongStr,mutString);   // 两者指向的地址是一样的
    [mutString insertString:@"456" atIndex:0];
    NSLog(@"self str = %@  mutStr = %@",self.strongStr,mutString);  // 两者的值都会改变,不可变对象的值被改变
}

什么是可变对象、什么是不可变对象

- (void)testNotChange
{
    NSString *str = @"123";
    NSLog(@"str = %p",str);
    str = @"234";
    NSLog(@"after str = %p",str);
}

NSString是不可变对象。虽然在程序中修改了str的值,但是此处的修改实际上是系统重新分配了空间,定义了字符串,然后str重新指向了一个新的地址。这也是为何修改之后地址不一致的原因:

2018-12-06 22:02:41.350812+0800 TestClock[884:17969] str = 0x106ec1290
2018-12-06 22:02:41.350919+0800 TestClock[884:17969] after str = 0x106ec12d0

可变对象的例子

- (void)testChangeAble
{
    NSMutableString *mutStr = [NSMutableString stringWithString:@"abc"];
    NSLog(@"mutStr = %p",mutStr);
    [mutStr appendString:@"def"];
    NSLog(@"after mutStr = %p",mutStr);
}

NSMutableString是可变对象。程序中改变了mutStr的值,且修改前后mutStr的地址一致:

2018-12-06 22:10:08.457179+0800 TestClock[1000:21900] mutStr = 0x600002100540
2018-12-06 22:10:08.457261+0800 TestClock[1000:21900] after mutStr = 0x600002100540
  • copy
    copy常用来修饰NSString,因为当新值是可变的,防止属性在不知不觉中被修改。
    只要是可变对象,无论是集合对象,还是非集合对象,copy和mutableCopy都是深拷贝。
- (void)testMutableCopy
{
    NSMutableString *str1 = [NSMutableString stringWithString:@"abc"];
    NSString *str2 = [str1 copy];
    NSMutableString *str3 = [str1 mutableCopy];
    NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
    
    NSMutableArray *array1 = [NSMutableArray arrayWithObjects:@"a",@"b", nil];
    NSArray *array2 = [array1 copy];
    NSMutableArray *array3 = [array1 mutableCopy];
    NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
}
2018-12-07 13:01:27.525064+0800 TestClock[9357:143436] str1 = 0x60000086d8f0 str2 = 0xc8c1a5736a50d5fe str3 = 0x60000086d9b0
2018-12-07 13:01:27.525198+0800 TestClock[9357:143436] array1 = 0x600000868000 array2 = 0x60000067e5a0 array3 = 0x600000868030

只要是不可变对象,无论是集合对象,还是非集合对象,copy都是浅拷贝,mutableCopy都是深拷贝。

- (void)testCopy
{
    NSString *str1 = @"123";
    NSString *str2 = [str1 copy];
    NSMutableString *str3 = [str1 mutableCopy];
    NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
    
    NSArray *array1 = @[@"1",@"2"];
    NSArray *array2 = [array1 copy];
    NSMutableArray *array3 = [array1 mutableCopy];
    NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
}
2018-12-07 13:06:29.439108+0800 TestClock[9442:147133] str1 = 0x1045612b0 str2 = 0x1045612b0 str3 = 0x6000017e4450
2018-12-07 13:06:29.439236+0800 TestClock[9442:147133] array1 = 0x6000019f5c80 array2 = 0x6000019f5c80 array3 = 0x6000017e1170

copy用来修饰不可变的变量如:NSString,NSArray。
如果修饰可变变量,比如NSMutableString,会导致属性被不知不觉中修改。

@property (nonatomic, copy) NSMutableString *mutString; //测试可变数组用copy产生的影响。可变变量应使用strong才对

- (void)testCopyFixMu {
    NSString *str = @"123";
    self.mutString = [NSMutableString stringWithString:str];
    NSLog(@"str = %p self.mutString = %p",str,self.mutString); // 两者的地址不一样
    [self.mutString appendString:@"456"]; // 会崩溃,因为此时self.mutArray是NSString类型,是不可变对象
    //开始发蒙了,我们明明定义的可变字符,为什么self.mutString 会变成不可变呢?------>其实主要是因为属性copy捣乱的
    
//    self.mutString = [NSMutableString stringWithString:str];
////    这段代码执行后,其属性发生变化。具体其实可以看成如下
//
//    NSMutableString *tempString = [NSMutableString stringWithString:str];
//    // 将该临时变量copy,赋值给self.mutString
//    self.mutString = [tempString copy];
//    通过[tempString copy]得到的self.mutString是一个不可变对象
}
  • assign
    使用:可以修饰对象,但是由于对象存在堆,所以会造成野指针的问题。所以常用于基本数据类型、‘枚举’、‘结构体’ 等非OC对象类型,因为基本数据内存存在栈中,栈是由系统自己管理所以不会造成野指针。不会造成引用计数变化。
    注意:assign修饰变量时 指向的对象销毁时,不会将当前指向对象的指针指向nil,有野指针的生成。造成crash
  • weak
    表示一种“非拥有关系”。用weak修饰属性的时候,为属性设置新值的时候,设置方法既不会保留新值(新值的引用计数加一),也不会释放旧值(旧值的引用计数减一)。当属性所指的对象释放的时候,属性也会被置为nil。用于修饰UI控件,代理(delegate)。
    使用:只可以修饰对象。不会造成引用计数变化。
    注意: weak 指向的对象销毁时,会将当前指向对象的指针指向nil,防止野指针的生成。
    防止野指针原理:Runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

3. 为什么weak修饰的属性,当其实例被释放后,可以置为nil?

简单来说:
a. 从weak表中获取被释放对象的地址为键值的记录
b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为 nil
c. 将weak表中该记录删除
d. 从引用计数表中删除废弃对象的地址为键值的记录

Runtime 实现weak属性具体流程大致分为 3 步:
1、初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。
2、添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数,objc_storeWeak() 的作用是更新指针指向(指针可能原来指向着其他对象,这时候需要将该 weak 指针与旧对象解除绑定,会调用到 weak_unregister_no_lock),如果指针指向的新对象非空,则创建对应的弱引用表,将 weak 指针与新对象进行绑定,会调用到 weak_register_no_lock。在这个过程中,为了防止多线程中竞争冲突,会有一些锁的操作。
3、释放时:调用 clearDeallocating 函数,clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

当weak引用指向的对象被释放时,如何去处理weak指针?
1、调用objc_release
2、因为对象的引用计数为0,所以执行dealloc
3、在dealloc中,调用了_objc_rootDealloc函数
4、在_objc_rootDealloc中,调用了object_dispose函数
5、调用objc_destructInstance
6、最后调用objc_clear_deallocating

4.什么情况使用weak关键字,相比assign有什么不同?

  • 什么情况使用weak关键字
  1. 在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。
  2. 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong
  • 相比assign有什么不同
  1. weak此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而assign的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。
  2. assign 可以用基础类型、OC对象,而 weak 必须用于 OC 对象

5.如何让自己的类用copy修饰符?如何重写带copy关键字的setter?

若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。
该协议只有一个方法:

- (id)copyWithZone:(NSZone *)zone;

重写带 copy 关键字的 setter,例如:

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

6.深拷贝与浅拷贝

浅拷贝:只创建一个新的指针,指向原指针指向的内存。
深拷贝:创建一个新的指针,并开辟新的内存空间,内容拷贝自原指针指向的内存,并指向它。
假设我们要对一个不可变的对象进行不可变copy(原来的对象不可变,新对象也不可变)。就没必要给新对象新建一块内存,反正大家都不可以对这个对象进行改变,那都使用一个就可以。所以iOS系统规定浅拷贝引用计数器加1就行(多了一个指针指向)。而需要给新对象开闭内存空间的,就是深拷贝,深拷贝不会引起原对象引用计数的变化。
那如何对数组里面的元素进行深拷贝呢?需要对copyWithZone进行重写。在copyWithZone: 里对象赋值上不直接赋值而是通过copy方法即可实现,代码如下

- (id)copyWithZone:(NSZone *)zone
{
    Person *cpyPerson = [[Person allocWithZone:zone] init];
    cpyPerson.name = self.name;  //难道此处不是循环吗???
    cpyPerson.age = self.age;
    return cpyPerson;
}

在这里插入图片描述
总结:
copy得到的类型一定是不可变的;mutableCopy得到的类型一定是可变的
使用mutable,都是深拷贝(不管是拷贝类型还是拷贝方法);但是copy也有深拷贝;

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

  • @property 的本质是实例变量(ivar)+存取方法(access method = getter + setter),即 @property = ivar + getter + setter;

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

  • ivar、getter、setter 是自动合成这个类中的

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

8.@protocol和category中如何使用@property

  • protocol
    在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,并不会自动生成实例变量以及存取方法,在协议中添加属性的目的,是希望遵守我协议的类能实现该属性。如果使用协议中属性还需要在对应的实现协议类中合成一下这个变量即@synthesize var = _var;
    synthesize也就是相对于帮我们实现了对应的set get方法。
@protocol testProtory <NSObject>
@property (nonatomic, copy) NSString *name; //通常没有这个属性,这么写是告诉实现协议方去实现这个属性,如果实现协议方想着使用这个属性,则必须将属性同一个变量绑定
- (void) testSetName;
@end
@implementation ProtocolTest
@synthesize name; //一定记得这里合同

 - (void)testSetName {
    NSLog(@"实现SetName协议");
    self.name = @"AAAA";
}
  • category
    category 使用 @property 也是只会生成 setter 和 getter 方法的声明,如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:objc_setAssociatedObject和objc_getAssociatedObject

Category为何不能添加成员变量,而只能添加方法?
这要从Category的原理说起,简单地说就是通过runtime动态地把Category中的方法等添加到类中(苹果在实现的过程中并未将属性添加到类中,所以属性仅仅是声明了setter和getter方法,而并未实现),具体请参考http://tech.meituan.com/DiveIntoCategory.html

因为方法和属性并不“属于”类实例,而成员变量“属于”类实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。

需要注意的有两点:
1)、category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA

2)、category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。

category和+load方法

  1. 在类的+load方法调用的时候,我们可以调用category中声明的方法么
    可以调用,因为附加category到类的工作会先于+load方法的执行
  2. 这么些个+load方法,调用顺序是咋样的呢?
    +load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。

怎么调用到原来类中被category覆盖掉的方法?
我们已经知道category其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,所以我们只要顺着方法列表找到最后一个对应名字的方法

category无法添加实例变量,但是我们想category中添加和对象关联的值,如何实现?
其实就是给类别扩充一个对象,但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?

我们去翻一下runtime的源码,在objc-references.mm文件中有个方法_object_set_associative_reference:
我们可以看到所有的关联对象都由AssociationsManager管理
AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。

而在对象的销毁逻辑里面,见objc-runtime-new.mm:
嗯,runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

9. 连类比事-category和extension

美团技术—category真面目
extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的

extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。(详见2)

但是category则完全不一样,它是在运行期决议的。 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

10.简要说一下@autoreleasePool的数据结构??

简单说是双向链表,每张链表头尾相接,有 parent、child指针
每创建一个池子,会在首部创建一个 哨兵 对象,作为标记
最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。

其实就是一个AutoreleasePoolPage双向链表,在链表的顶端添加一个哨兵,【A autorelease】其实就是将对象A添加到链表的内存中,当next指针知道最高处后,如果还有对象B地址加入,则会新开一个AutoreleasePoolPage双向链表,将B对象地址添加到新链表的栈底

parent、child指针 指向上个链表或下个链表

疑惑:为什么是双向链表,单向链表不可以吗?

  • 什么时候释放
    手动添加的,是在当前作用域大括号结束时释放
    非手动添加的,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

  • 释放时刻原理

    每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,然后被objc_autoreleasePoolPop(哨兵对象)作为入参
    1.根据传入的哨兵对象地址找到哨兵对象所处的page
    2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
    3.补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理)
    知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

总结:
每次push会产生一个新的autoreleasepool,并生成一个POOL_SENTINEL
说完autoreleasepool的创建,接下来说对象是如何加到autoreleasepool中去的,当对象调用【object autorelease】的方法的时候就会加到autoreleasepool中
当执行到这个objc_autoreleasePoolPop方法的时候
autoreleasepool会向POOL_SENTINEL地址后面的对象都发release消息,直到第一个POOL_SENTINEL对象截止。

11.BAD_ACCESS在什么情况下出现?

访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环

12.使用CADisplayLink、NSTimer有什么注意点?

  1. 造成循环引用
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti   target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:  (SEL)sel;
  1. 解决循环引用方法
    1.使用block
    2.使用代理对象 NSProxy
  2. 会造成时间不准确的问题
    1.为什么造成时间不准?
    NStimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时,具体流程如下
具体RunLoop运行流程如下:
1.通知Observers:进入Loop
2.通知Observers:即将处理Timers
3.通知Observers:即将处理Sources
4.处理Blocks
5.处理Source0(可能会再次处理Blocks)
6.如果存在Source1,就跳转到第87.通知Observers:开始休眠(等待消息唤醒)
8.通知Observers:结束休眠(被某个消息唤醒)
01>处理Timer
02>处理GCD Async To Main Queue
03>处理Source1
9.处理Blocks
10.根据前面的执行结果决定如何操作
01>回到第202> 推出Loop
11.通知Observers:退出Loop

RunLoop在执行Timer之前就相当于一直在跑圈,假设Timer要求每隔1s执行一次,此时第一次跑圈执行了0.2s,没到执行Timer的时候,跑第二圈执行了0.5s,没到执行Timer的时候,跑第三圈执行了0.2s,没到执行Timer的时候,跑第四圈执行了0.2s,查看发现1.1s了,此时去执行定时器的内容,这时候就造成了定时器不准确的问题

注:1.RunLoop没有消息就让线程休眠 ,有消息就唤醒线程
2.要求比较准时的时候,还是需要用GCD来实现定时器
3.CFRunLoopModeRef代表RunLoop的运行模式
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
RunLoop启动时只能选择其中一个Mode,作为currentMode
如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

  1. 如何解决时间不准?
    方法一
    1、在子线程中创建timer,在主线程进行定时任务的操作
    2、在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作

    方法二:
    GCD可以解决倒计时不准问题

13.iOS内存分区情况

  • 栈区(Stack)
    由编译器自动分配释放,存放函数的参数,局部变量的值等
    栈是向低地址扩展的数据结构,是一块连续的内存区域

  • 堆区(Heap)
    由程序员分配释放
    是向高地址扩展的数据结构,是不连续的内存区域

  • 全局区
    全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域
    程序结束后由系统释放

  • 常量区
    常量字符串就是放在这里的
    程序结束后由系统释放

  • 代码区
    存放函数体的二进制代码

    注:
    在 iOS 中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的
    系统使用一个链表来维护所有已经分配的内存空间(系统仅仅记录,并不管理具体的内容)
    变量使用结束后,需要释放内存,OC 中是判断引用计数是否为 0,如果是就说明没有任何变量使用该空间,那么系统将其回收
    当一个 app 启动后,代码区、常量区、全局区大小就已经固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)

14.iOS内存管理方式

  1. 自己生成的对象,自己持有。
  2. 非自己生成的对象,自己也能持有。
  3. 不在需要自己持有对象的时候,释放。
  4. 非自己持有的对象无需释放。
  • 散列表(引用计数表、弱引用表)
    引用计数要么存放在 isa 的 extra_rc 中,要么存放在引用计数表中,而引用计数表包含在一个叫 SideTable 的结构中,它是一个散列表,也就是哈希表。而 SideTable 又包含在一个全局的 StripeMap 的哈希映射表中,这个表的名字叫 SideTables。

    当一个对象访问 SideTables 时:
    首先会取得对象的地址,将地址进行哈希运算,与 SideTables 中 SideTable 的个数取余,最后得到的结果就是该对象所要访问的 SideTable。在取得的 SideTable 中的 RefcountMap 表中再进行一次哈希查找,找到该对象在引用计数表中对应的位置。如果该位置存在对应的引用计数,则对其进行操作,如果没有对应的引用计数,则创建一个对应的 size_t 对象,其实就是一个 uint 类型的无符号整型。
    弱引用表也是一张哈希表的结构,其内部包含了每个对象对应的弱引用表 weak_entry_t,而 weak_entry_t 是一个结构体数组,其中包含的则是每一个对象弱引用的对象所对应的弱引用指针。

15.循环引用

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

如何解决循环引用?

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、在合适时机去手动断开循环引用。 通常我们使用第一种。

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

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

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);
 };

并不是所有block都会造成循环引用。 只有被强引用了的block才会产生循环引用 而比如dispatch_async(dispatch_get_main_queue(), ^{}),[UIView animateWithDuration:1 animations:^{}]这些系统方法等 或者block并不是其属性而是临时变量,即栈block

[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];
 };
<<<<<<< HEAD
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章