1.什麼情況使用weak關鍵字,相比assign有什麼不同?
-
什麼情況使用 weak 關鍵字?
在 ARC 中,在有可能出現循環引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性
自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當然,也可以使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性爲什麼可以被設置成weak?》
-
不同點:
weak 此特質表明該屬性定義了一種“非擁有關係” (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。 而 assign 的“設置方法”只會執行鍼對“純量類型” (scalar type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作。
assign 可以用非 OC 對象,而 weak 必須用於 OC 對象
2.如何讓自己的類用copy修飾符?如何重寫帶copy關鍵字的setter?
-
若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分爲可變版本與不可變版本,那麼就要同時實現 NSCopying 與 NSMutableCopying 協議。
具體步驟:
需聲明該類遵從 NSCopying 協議
實現 NSCopying 協議。該協議只有一個方法:
- (id)copyWithZone:(NSZone *)zone;
注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是 “copyWithZone” 方法。
-
重寫帶 copy 關鍵字的 setter,例如:
- (void)setName:(NSString *)name { //[_name release]; _name = [name copy]; }
3.深拷貝與淺拷貝
淺拷貝只是對指針的拷貝,拷貝後兩個指針指向同一個內存空間,深拷貝不但對指針進行拷貝,而且對指針指向的內容進行拷貝,經深拷貝後的指針是指向兩個不同地址的指針。
當對象中存在指針成員時,除了在複製對象時需要考慮自定義拷貝構造函數,還應該考慮以下兩種情形:
-
當函數的參數爲對象時,實參傳遞給形參的實際上是實參的一個拷貝對象,系統自動通過拷貝構造函數實現;
-
當函數的返回值爲一個對象時,該對象實際上是函數內對象的一個拷貝,用於返回函數調用處。
copy方法:如果是非可擴展類對象,則是淺拷貝。如果是可擴展類對象,則是深拷貝。
mutableCopy方法:無論是可擴展類對象還是不可擴展類對象,都是深拷貝。
4.@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 語法來指定實例變量的名字.
5.@protocol和category中如何使用@property
-
在 protocol 中使用 property 只會生成 setter 和 getter 方法聲明,我們使用屬性的目的,是希望遵守我協議的對象能實現該屬性
-
category 使用 @property 也是隻會生成 setter 和 getter 方法的聲明,如果我們真的需要給 category 增加屬性的實現,需要藉助於運行時的兩個函數:objc_setAssociatedObject和objc_getAssociatedObject
6.簡要說一下@autoreleasePool的數據結構??
簡單說是雙向鏈表,每張鏈表頭尾相接,有 parent、child指針
每創建一個池子,會在首部創建一個 哨兵 對象,作爲標記
最外層池子的頂端會有一個next指針。當鏈表容量滿了,就會在鏈表的頂端,並指向下一張表。
7.BAD_ACCESS在什麼情況下出現?
訪問了懸垂指針,比如對一個已經釋放的對象執行了release、訪問已經釋放對象的成員變量或者發消息。 死循環
8.使用CADisplayLink、NSTimer有什麼注意點?
CADisplayLink、NSTimer會造成循環引用,可以使用YYWeakProxy或者爲CADisplayLink、NSTimer添加block方法解決循環引用
9.iOS內存分區情況
-
棧區(Stack)
由編譯器自動分配釋放,存放函數的參數,局部變量的值等
棧是向低地址擴展的數據結構,是一塊連續的內存區域
-
堆區(Heap)
由程序員分配釋放
是向高地址擴展的數據結構,是不連續的內存區域
-
全局區
全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域
程序結束後由系統釋放
-
常量區
常量字符串就是放在這裏的
程序結束後由系統釋放
-
代碼區
存放函數體的二進制代碼
-
注:
-
在 iOS 中,堆區的內存是應用程序共享的,堆中的內存分配是系統負責的
-
系統使用一個鏈表來維護所有已經分配的內存空間(系統僅僅記錄,並不管理具體的內容)
-
變量使用結束後,需要釋放內存,OC 中是判斷引用計數是否爲 0,如果是就說明沒有任何變量使用該空間,那麼系統將其回收
-
當一個 app 啓動後,代碼區、常量區、全局區大小就已經固定,因此指向這些區的指針不會產生崩潰性的錯誤。而堆區和棧區是時時刻刻變化的(堆的創建銷燬,棧的彈入彈出),所以當使用一個指針指向這個區裏面的內存時,一定要注意內存是否已經被釋放,否則會產生程序崩潰(也即是野指針報錯)
-
10.iOS內存管理方式
-
Tagged Pointer(小對象)
Tagged Pointer 專門用來存儲小的對象,例如 NSNumber 和 NSDate
Tagged Pointer 指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已。所以,它的內存並不存儲在堆中,也不需要 malloc 和 free
在內存讀取上有着 3 倍的效率,創建時比以前快 106 倍
objc_msgSend 能識別 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接從指針提取數據
使用 Tagged Pointer 後,指針內存儲的數據變成了 Tag + Data,也就是將數據直接存儲在了指針中
-
NONPOINTER_ISA (指針中存放與該對象內存相關的信息) 蘋果將 isa 設計成了聯合體,在 isa 中存儲了與該對象相關的一些內存的信息,原因也如上面所說,並不需要 64 個二進制位全部都用來存儲指針。
isa 的結構:
// x86_64 架構 struct { uintptr_t nonpointer : 1; // 0:普通指針,1:優化過,使用位域存儲更多信息 uintptr_t has_assoc : 1; // 對象是否含有或曾經含有關聯引用 uintptr_t has_cxx_dtor : 1; // 表示是否有C++析構函數或OC的dealloc uintptr_t shiftcls : 44; // 存放着 Class、Meta-Class 對象的內存地址信息 uintptr_t magic : 6; // 用於在調試時分辨對象是否未完成初始化 uintptr_t weakly_referenced : 1; // 是否被弱引用指向 uintptr_t deallocating : 1; // 對象是否正在釋放 uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 來存儲引用計數 uintptr_t extra_rc : 8; // 引用計數能夠用 8 個二進制位存儲時,直接存儲在這裏 }; // arm64 架構 struct { uintptr_t nonpointer : 1; // 0:普通指針,1:優化過,使用位域存儲更多信息 uintptr_t has_assoc : 1; // 對象是否含有或曾經含有關聯引用 uintptr_t has_cxx_dtor : 1; // 表示是否有C++析構函數或OC的dealloc uintptr_t shiftcls : 33; // 存放着 Class、Meta-Class 對象的內存地址信息 uintptr_t magic : 6; // 用於在調試時分辨對象是否未完成初始化 uintptr_t weakly_referenced : 1; // 是否被弱引用指向 uintptr_t deallocating : 1; // 對象是否正在釋放 uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 來存儲引用計數 uintptr_t extra_rc : 19; // 引用計數能夠用 19 個二進制位存儲時,直接存儲在這裏 };
這裏的 has_sidetable_rc 和 extra_rc,has_sidetable_rc 表明該指針是否引用了 sidetable 散列表,之所以有這個選項,是因爲少量的引用計數是不會直接存放在 SideTables 表中的,對象的引用計數會先存放在 extra_rc 中,當其被存滿時,纔會存入相應的 SideTables 散列表中,SideTables 中有很多張 SideTable,每個 SideTable 也都是一個散列表,而引用計數表就包含在 SideTable 之中。
-
散列表(引用計數表、弱引用表)
引用計數要麼存放在 isa 的 extra_rc 中,要麼存放在引用計數表中,而引用計數表包含在一個叫 SideTable 的結構中,它是一個散列表,也就是哈希表。而 SideTable 又包含在一個全局的 StripeMap 的哈希映射表中,這個表的名字叫 SideTables。
當一個對象訪問 SideTables 時:
-
首先會取得對象的地址,將地址進行哈希運算,與 SideTables 中 SideTable 的個數取餘,最後得到的結果就是該對象所要訪問的 SideTable
-
在取得的 SideTable 中的 RefcountMap 表中再進行一次哈希查找,找到該對象在引用計數表中對應的位置
-
如果該位置存在對應的引用計數,則對其進行操作,如果沒有對應的引用計數,則創建一個對應的 size_t 對象,其實就是一個 uint 類型的無符號整型
弱引用表也是一張哈希表的結構,其內部包含了每個對象對應的弱引用表 weak_entry_t,而 weak_entry_t 是一個結構體數組,其中包含的則是每一個對象弱引用的對象所對應的弱引用指針。
-
11.循環引用
1. 概述
iOS內存中的分區有:堆、棧、靜態區。其中,棧和靜態區是操作系統自己管理回收,不會造成循環引用。在堆中的相互引用無法回收,有可能造成循環引用。
循環引用的實質:多個對象相互之間有強引用,不能施放讓系統回收。
解決循環引用一般是將 strong 引用改爲 weak 引用。
2. 循環引用場景分析及解決方法
1)父類與子類
如:在使用UITableView 的時候,將 UITableView 給 Cell 使用,cell 中的 strong 引用會造成循環引用。
// controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TestTableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"UITableViewCellId" forIndexPath:indexPath];
cell.tableView = tableView;
return cell;
}
// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, strong) UITableView *tableView; // strong 造成循環引用
@end
解決:strong 改爲 weak
// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, weak) UITableView *tableView; // strong 改爲 weak
@end
2)block
block在copy時都會對block內部用到的對象進行強引用的。
self.testObject.testCircleBlock = ^{
[self doSomething];
};
self將block作爲自己的屬性變量,而在block的方法體裏面又引用了 self 本身,此時就很簡單的形成了一個循環引用。
應該將 self 改爲弱引用
__weak typeof(self) weakSelf = self;
self.testObject.testCircleBlock = ^{
__strong typeof (weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
在 ARC 中,在被拷貝的 block 中無論是直接引用 self 還是通過引用 self 的成員變量間接引用 self,該 block 都會 retain self。
- 快速定義宏
// weak obj
/#define WEAK_OBJ(type) __weak typeof(type) weak##type = type;
// strong obj
/#define STRONG_OBJ(type) __strong typeof(type) str##type = weak##type;
3)Delegate
delegate 屬性的聲明如下:
@property (nonatomic, weak) id <TestDelegate> delegate;
如果將 weak 改爲 strong,則會造成循環引用
// self -> AViewController
BViewController *bVc = [BViewController new];
bVc = self;
[self.navigationController pushViewController: bVc animated:YES];
// 假如是 strong 的情況
// bVc.delegate ===> AViewController (也就是 A 的引用計數 + 1)
// AViewController 本身又是引用了 <BViewControllerDelegate> ===> delegate 引用計數 + 1
// 導致: AViewController <======> Delegate ,也就循環引用啦
4)NSTimer
NSTimer 的 target 對傳入的參數都是強引用(即使是 weak 對象)
解決辦法: 《Effective Objective-C 》中的52條方法
#import <Foundation/Foundation.h>
@interface NSTimer (YPQBlocksSupport)
+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
#import "NSTimer+YPQBlocksSupport.h"
@implementation NSTimer (YPQBlocksSupport)
+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ypq_blockInvoke:) userInfo:[block copy]
repeats:repeats];
}
- (void)ypq_blockInvoke:(NSTimer *)timer
{
void (^block)() = timer.userInfo;
if(block)
{
block();
}
}
@end
使用方式:
__weak ViewController * weakSelf = self;
[NSTimer ypq_scheduledTimeWithTimeInterval:4.0f
block:^{
ViewController * strongSelf = weakSelf;
[strongSelf afterThreeSecondBeginAction];
}
repeats:YES];
計時器保留其目標對象,反覆執行任務導致的循環,確實要注意,另外在dealloc的時候,不要忘了調用計時器中的 invalidate方法。
其他面試題篇章: