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