前幾年學習過程中陸陸續續整理的知識點,今天開始遷移到掘金。由於當年在翻閱國產技術書籍時,發現知識點有不少錯誤,踩了不少坑,當然可能仍然有錯誤和遺漏,歡迎指正~
KVC
KVC允許以字串形式間接操作物件的屬性,全稱為Key Value Coding,即鍵值編碼。
- 底層實現機制
- (void)setValue:(nullable id)value forKey:(NSString *)key;
複製程式碼
- 首先查詢
-set<Key>:
程式碼通過setter方法賦值。(勘誤1) - 否則,檢查
+(BOOL)accessInstanceVariablesDirectly
方法,如果你重寫了該方法並使其返回NO,則KVC下一步會執行setValue:forUndefinedKey:
,預設丟擲異常。 - 否則,KVC會依次搜尋該類中名為
_<key>
,_<isKey>
,<key>
,<isKey>
的成員變數。 - 如果都沒有,則執行
setValue:forUndefinedKey:
方法,預設丟擲異常。
勘誤1:經驗證,在查詢`-set<Key>:`後,如果沒有找到,還會去查詢`-_set<Key>:`方法,然後才會進入步驟2,感謝@QXCloud 的指正~
- (nullable id)valueForKey:(NSString *)key;
複製程式碼
- 首次依次查詢
-get<Key>
,-<key>
,-is<Key>
程式碼通過getter方法獲取值。 - 否則,查詢
-countOf<Key>
,-objectIn<Key>AtIndex:
和-<key>AtIndexes:
方法,如果count方法和另外兩個中的一個被找到,返回一個能響應所有NSArray方法的代理集合,簡單來說就是可以當NSArray用。 - 否則,查詢
-countOf<Key>
,-enumeratorOf<Key>
和-memberOf<Key>:
方法,如果三個都能找到,返回一個能響應所有NSSet方法的代理集合,簡單來說就是可以當NSSet使用。 - 否則,依次搜尋該類中名為
_<key>
,_<isKey>
,<key>
,<isKey>
的成員變數,返回該成員變數的值。 - 如果都沒有,則執行
valueForUndefinedKey:
方法,預設丟擲異常。
- Key Path
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
複製程式碼
KVC不僅可以操作物件屬性,還可以操作物件的“屬性鏈”。如Person中有一個型別為Date的birthday屬性,而Date中又有year,month,day等屬性,那麼Person可以直接通過birthday.year這種Key Path來操作birthday的year屬性。
- 如何避免KVC修改readonly屬性?
從上述機制可以看出,在沒有setter方法時,會檢查+(BOOL)accessInstanceVariablesDirectly
來決定是否搜尋相似成員變數,因此只需要重寫該方法並返回NO即可。
- 如何校驗KVC的正確性?
開發中可能有些需要設定物件屬性不可以設定某些值,此時就需要檢驗Value的可用性,通過如下方法
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
複製程式碼
這個方法的預設實現是去探索類裡面是否有一個這樣的方法-(BOOL)validate<Key>:error:
,如果有這個方法,就呼叫這個方法來返回,沒有的話就直接返回YES。
注意:在KVC設值的時候,並不會主動呼叫該方法去校驗,需要開發者手動呼叫校驗,意味著即使實現此方法,也可以賦值成功。
- 常見的異常情況
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
複製程式碼
沒有找到相關key,會丟擲NSUndefinedKeyException
異常,使用KVC時一般需要重寫這兩個方法。
- (void)setNilValueForKey:(NSString *)key;
複製程式碼
當給基礎型別的物件屬性設定nil時,會丟擲NSInvalidArgumentException
異常,一般也需要重寫。
- 常見應用場景
- 可以靈活的使用字串動態取值和設值,但通過KVC操作物件的效能比getter和setter更差。
- 訪問和修改私有屬性,最常見的就是修改UITextField中的placeHolderText。
- 通過
- (void)setValuesForKeysWithDictionary:
字典轉model,如股票欄位。 - 當對容器類使用KVC時,
valueForKey:
將會被傳遞給容器中的每一個物件,而不是容器本身進行操作,由此我們可以有效的提取容器中每個物件的指定屬性值集合。 - 使用函式操作容器中的物件,快速對各物件中的基礎型別屬性做運算,如
@avg
,@count
,@max
,@min
@sum
。
KVO
KVO提供了一種機制(基於NSKeyValueObserving協議,所有Object都實現了此協議)可以供觀察者監聽物件屬性的變化並接收通知,全稱為Key Value Observing,即鍵值監聽。
常用API
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
複製程式碼
觀察者重寫
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
複製程式碼
- 底層實現機制
當觀察者對物件A註冊一個監聽時,系統此時會動態建立一個名為NSKVONotifying_A的新類,該類繼承自物件A原本的類,並重寫被觀察者的setter方法,在原setter方法的呼叫前後通知觀察者值的改變。然後將物件A的isa指標(isa指標告訴Runtime這個物件的類是什麼)指向NSKVONotifying_A這個新類,那麼物件A就變成了新建立新類的例項。
不僅如此,Apple還重寫了-class
方法來隱藏該新類,讓人們以為註冊前後物件A的類並沒有改變,但實際上如果手動新建一個NSKVONotifying_A類,在觀察者執行到註冊時,便會引起重複類崩潰。
- 注意事項
從上述實現原理來看,很明顯可以知道,如果沒有通過setter賦值,直接賦值該成員變數是不會觸發KVO機制的,但是KVC除外,這也側面證明了KVC與KVO是有內在聯絡的。
alloc/init與new
- alloc/init
alloc負責為物件的所有成員變數分配記憶體空間,並且為各成員變數重置為預設值,如int型為0,BOOL型為NO,指標型變數為nil。 僅僅分配空間還不夠,還需要init來對物件執行初始化操作,才可以使用它。如果只呼叫alloc不呼叫init,也能執行,但可能會出現未知結果。
- new
所做的事情與alloc差不多,也是分配記憶體和初始化。
- 兩者區別
new只能使用預設的init初始化,而alloc可以使用其他初始化方法,因為顯示呼叫總比隱式呼叫好,所以往往使用alloc/init來初始化。
@"hello"和[NSString stringWithFormat:@"hello"]有何區別?
NSString *A = @"hello";
NSString *B = @"hello";
NSString *C = [NSString stringWithFormat:@"hello"];
NSString *D = [NSString stringWithFormat:@"hello"];
NSString *E = [[NSString alloc] initWithFormat:@"hello"];
NSString *F = [[NSString alloc] initWithFormat:@"hello"];
NSLog(@"A=%p\n B=%p\n C=%p\n D=%p\n E=%p\n F=%p\n", A, B, C, D, E, F);
// 結果
A=0x104ba0070
B=0x104ba0070
C=0xdbd16c40a2e07e99
D=0xdbd16c40a2e07e99
E=0xdbd16c40a2e07e99
F=0xdbd16c40a2e07e99
複製程式碼
@"hello"
位於常量池中,可重複使用,其中A和B指向的都是同一份記憶體地址。
而stringWithFormat
或initWithFormat
是在執行時建立出來的,儲存在執行時記憶體(即堆記憶體),它們在堆裡面請求對應的值,如果存在,系統便不再分配地址。
Propoty修飾符
具體可分為四類:執行緒安全、讀寫許可權、記憶體管理和指定讀寫方法。
- 執行緒安全(atomic,nonatomic)
如果不寫該類修飾符,預設就是atomic。兩者最大的區別就是決定編譯器生成的getter/setter方法是否屬於原子操作,如果自己寫了getter/setter方法,此時用什麼都一樣。 對於atomic來說,getter/setter方法增加了鎖來確保操作的完整性,不受其他執行緒影響。例如執行緒A的getter方法執行到一半,執行緒B呼叫setter方法,那麼執行緒A還是能得到一個完整的Value。 而對於nonatomic來說,多個執行緒能同時訪問操作,就無法保證是否是完整的Value,還會引發髒資料。但是nonatomic更快,開發中往往在可控情況下安全換效率。
注意:atomic並不能完全保證執行緒安全,只能保證資料操作的執行緒安全,例如執行緒A使用getter方法,同時執行緒B、C使用setter方法,那最後執行緒A獲取到的值有三種可能:原始值、B set的值或者C set的值;又例如執行緒A使用getter方法,執行緒B同時呼叫release方法,由於release方法並沒有加鎖,所以有可能會導致cash。
- 讀寫許可權(readonly,readwrite)
readonly只讀屬性,只會生成getter方法,不會生成setter方法。 readwrite讀寫屬性,會生成getter/setter方法,預設是該修飾符。
- 記憶體管理(strong,weak,assign,copy)
strong強引用,適用於物件,引用計數+1,物件預設是該修飾符。 weak弱引用,為這種屬性設定新值時,設定方法既不釋放舊值,也不保留新值,不會使引用計數加1。當所指物件被銷燬時,指標會自動被置為nil,防止野指標。
assgin適用於基礎資料型別,如NSIntger,CGFloat,int等,只進行簡單賦值,基礎資料型別預設是該修飾符。如果用此修飾符修飾物件,物件被銷燬時,並不會置空,會造成野指標。 copy是為了解決上下文的異常依賴,實際賦值型別不可變物件時,淺拷貝;可變物件時,深拷貝。
- 指定讀寫方法(setter=,getter=)
給getter/setter方法起別名,可以不一致,並且可以與其他屬性的getter/setter重名,例如Person類中定義如下
@property (nonatomic, copy, setter=setNewName:, getter=oldName) NSString *name;
@property (nonatomic, copy) NSString *oldName;
複製程式碼
那麼此時p1.oldName
始終是_name
的值,而如果宣告的順序交換,此時p1.oldName
就是_oldName
的值了,如果想得到_name
的值,使用p1.name
即可,但是此時不能使用-setName:
。所以別名都是有意義且不重複的,避免一些想不到的問題。
- strong和copy的區別
strong是淺拷貝,僅拷貝指標並增加引用計數;而copy在對於實際賦值物件是可變物件時,是深拷貝。不可變物件使用copy修飾,如NSString、NSArray、NSSet等;可變物件使用strong修飾,如NSMutableString、NSMutableArray、NSMutableSet等,這是為什麼呢? 由於父類屬性可以指向子類物件,試想這樣一個例子:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
NSMutableString *mutableName = [NSMutableString stringWithFormat:@"hello"];
p.name = mutableName;
[mutableName appendString:@" world"];
複製程式碼
由於Person.name
使用的strong修飾,它對於賦值物件進行的淺拷貝,那麼Person.name
此時實際指向與mutableName
指向的同一塊的記憶體區,如果將mutableName
的內容修改,此時Person.name
也會修改,這並不是我們想要的,所以我們使用copy來修飾,這樣即使賦值物件是一個可變物件,也會在setter方法中copy一份不可變物件再賦值。
而對於可變物件的屬性來說,如果使用copy修飾,從上面可知會得到一個不可變物件再賦值,那麼如果你想要修改物件內容的時候,就會丟擲異常,所以我們用strong。
- assgin和weak的區別
assgin用於基礎型別,可以修飾物件,但是這個物件在銷燬後,這個指標並不會置空,會造成野指標錯誤。 weak用於物件,無法修飾基礎型別,並且在物件銷燬後,指標會自動置為nil,不會引起野指標崩潰。
- var、getter、setter 是如何生成並新增到這個類中的?
完成屬性定義後,編譯器會自動編寫訪問這些屬性所需的方法,此過程叫做“自動合成”(autosynthesis)。需要強調的是,這個過程由編譯器在編譯期執行,所以編輯器裡看不到這些“合成方法”(synthesized method)的原始碼。除了生成方法程式碼 getter、setter 之外,編譯器還要自動向類中新增適當型別的例項變數,並且在屬性名前面加下劃線,以此作為例項變數的名字。也可以在類的實現程式碼裡通過 @synthesize 語法來指定例項變數的名字。
- @protocol 和 category 中如何使用 @property
在 protocol 中使用 property 只會生成 setter 和 getter 方法宣告,我們使用屬性的目的,是希望遵守我協議的物件能實現該屬性。 category 使用 @property 也是隻會生成 setter 和 getter 方法的宣告,如果我們真的需要給 category 增加屬性的實現,需要藉助於Runtime的關聯物件。
深拷貝與淺拷貝
- 深拷貝
深拷貝是對內容的拷貝,即複製一份原來的內容放在其他記憶體下,新物件指標指向該記憶體區域,與原來的物件沒有關係。
- 淺拷貝
淺拷貝是對指標的拷貝,即建立一個新指標也指向原物件的記憶體空間,相當於給原來物件索引計數+1。
- Copy與MutableCopy
對於可變物件來說,Copy和MutableCopy都是深拷貝; 對於不可變物件來說,Copy是淺拷貝,MutableCopy是深拷貝; Copy返回的都是不可變物件,MutableCopy返回的都是可變物件。
- 測試案例
NSArray *arr1 = @[];
NSArray *arr2 = arr1;
NSArray *arr3 = [arr1 copy];
NSArray *arr4 = [arr1 mutableCopy];
NSMutableArray *arr5 = [NSMutableArray array];
NSMutableArray *arr6 = arr5;
NSMutableArray *arr7 = [arr5 copy];
NSMutableArray *arr8 = [arr5 mutableCopy];
NSLog(@"%p %x", arr1, &arr1);
NSLog(@"%p %x", arr2, &arr2);
NSLog(@"%p %x", arr3, &arr3);
NSLog(@"%p %x", arr4, &arr4);
NSLog(@"%p %x", arr5, &arr5);
NSLog(@"%p %x", arr6, &arr6);
NSLog(@"%p %x", arr7, &arr7);
NSLog(@"%p %x", arr8, &arr8);
複製程式碼
列印結果
0x604000001970 e7ba22d8
0x604000001970 e7ba22c8
0x604000001970 e7ba22c0
0x604000458360 e7ba22b8
0x6040004581e0 e7ba22b0
0x6040004581e0 e7ba22a8
0x604000001970 e7ba22a0
0x604000458270 e7ba2298
複製程式碼
Weak的實現原理
- 前提
在Runtime中,為了管理所有物件的引用計數和weak指標,建立了一個全域性的SideTables,實際是一個hash表,裡面都是SideTable的結構體,並且以物件的記憶體地址作為key,SideTable部分定義如下
struct SideTable {
//保證原子操作的自旋鎖
spinlock_t slock;
//儲存引用計數的hash表
RefcountMap refcnts;
//用於維護weak指標的結構體
weak_table_t weak_table;
....
};
複製程式碼
其中用來維護weak指標的結構體weak_table_t
是一個全域性表,其定義如下
struct weak_table_t {
//儲存所有弱引用表的入口,包含所有物件的弱引用表
weak_entry_t *weak_entries;
//儲存空間
size_t num_entries;
//參與判斷引用計數輔助量
uintptr_t mask;
//hash key 最大偏移值
uintptr_t max_hash_displacement;
};
複製程式碼
其中所有的weak指標正是存在weak_entry_t
中,其部分定義如下
struct weak_entry_t {
//被指物件的地址。前面迴圈遍歷查詢的時候就是判斷目標地址是否和他相等。
DisguisedPtr<objc_object> referent;
union {
struct {
//可變陣列,裡面儲存著所有指向這個物件的弱引用的地址。當這個物件被釋放的時候,referrers裡的所有指標都會被設定成nil。
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
...
};
複製程式碼
- 大致流程
weak由於不增加引用計數,所以不能在SideTable中與引用計數表放在一起,Runtime單獨使用了一個全域性hash表weak_table_t
來管理weak,其中底層結構體weak_entry_t
以weak指向的物件記憶體地址為key,value是一個儲存該物件所有weak指標的陣列。當這個物件dealloc時,假設該物件的記憶體地址為a,查出對應的SideTable,搜尋key為a對應的指標陣列,並且遍歷陣列將所有weak物件置為nil,並清除記錄。
- 程式碼分析(NSObject.mm)
//建立weak物件
id __weak obj1 = obj;
//Runtime會呼叫如下方法初始化
id objc_initWeak(id *location, id newObj)
{
//如果物件例項為nil,當前weak物件直接置空
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
//更新指標指向
static id storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
//查詢當前weak指標原指向的oldSideTable與當前newObj的newSideTable
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
.....
//解除weak指標在舊物件中註冊
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
//新增weak到新物件的註冊
if (haveNew) {
newObj = (objc_object *)
//這個地方仍然需要newObj來核對記憶體地址來找到weak_entry_t,從而刪除
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;
} else {}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj;
}
複製程式碼
Category(分類)
分類用於對已有的類新增新方法,並不需要建立新的子類,不需要訪問原有類的原始碼,可以將類定義模組化地分佈到多個分類中。
- 特點
- 分類的方法可以與原來類同名,如果在分類中實現了該方法,分類的方法優先順序大於原有類方法。(不建議同名,分類的作用是新增方法,應使用子類重寫或者統一加字首)
- 分類只能新增方法,不能新增成員變數。
- 分類不僅影響原有類,還影響其子類。
- 一個類支援定義多個分類。
- 如果多個分類中有相同方法,執行時到底呼叫哪個方法由編譯器決定,最後一個參與編譯的方法會被呼叫。
- 分類是在執行時載入的,不是在編譯時。
- 可以新增屬性,但是@property不會生成setter和getter方法,也不會生成對應成員變數。(實際沒有意義)
- 使用場景
- 模組化設計:對於一個超大功能類來說,通過分類將其功能拆分,是個十分有效的方式,有利於管理和協同開發。
- 宣告私有方法:我們可以利用分類宣告一個私有方法,這樣可以外部直接使用該方法,不會報錯。
- 實現非正式協議:由於分類中的方法可以只宣告不實現,原來協議中不支援可選方法,就可以通過分類宣告可選方法,實現非正式協議。
- 為什麼不能新增成員變數?
分類的實現是基於Runtime動態的將分類中方法新增到類中,Runtime中通過class_addIvar()
方法新增成員變數,但蘋果對該方法只能在構造一個類的過程中呼叫,不允許對一個已存在的類動態新增成員變數。
為什麼蘋果不允許?這是因為物件在執行期已經給成員變數都分配了記憶體,如果動態的新增屬性,不僅需要破壞內部佈局,而且已經建立的類的例項也不符合當前類的定義,這簡直是災難性的。但是方法儲存在類的可變區域中,修改是不會影響類的記憶體佈局的,所以沒問題。
- 如何新增有效屬性?
在分類中宣告屬性可以編譯通過,但是使用該屬性,會報找不到getter/setter方法,這是由於即使宣告屬性,也不回生成_成員變數,自然也沒有必要實現getter/setter方法,那麼我們就需要通過Runtime的關聯物件來為屬性實現getter/setter方法。
例如對Person的一個分類增加SpecialName
屬性,實現程式碼如下
#import "Person+Test.h"
#import <objc/runtime.h>
// 定義關聯的key
static const char* specialNameKey = "specialName";
@implementation Person (Test)
- (void)setSpecialName:(NSString *)specialName {
// 第一個引數:給哪個物件新增關聯
// 第二個引數:關聯的key,通過這個key獲取
// 第三個引數:關聯的value
// 第四個引數:關聯的策略
objc_setAssociatedObject(self, specialNameKey, specialName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)specialName {
// 根據關聯的key,獲取關聯的值。
return objc_getAssociatedObject(self, specialNameKey);
}
@end
複製程式碼
其中關聯物件也是存在一個hash表中,通過記憶體定址,當物件銷燬時,會找到對應儲存的關聯物件做清理工作。
Extension(擴充套件)
擴充套件與分類相似,相當於匿名分類,但分類通常有.h和.m檔案,而擴充套件常用於臨時對某個類的介面進行擴充套件,一般宣告私有屬性、私有方法、私有成員變數。
- 特點
- 可以單獨以檔案定義,命名方式與分類相同。
- 通常放在主類的.m中。
- 擴充套件是在編譯時載入的。
- 擴充套件新新增的方法,類一定要實現。
Block
block是對C語言的擴充套件,用來實現匿名函式的特性。
- 特性
- 對於區域性變數預設是隻讀屬性。
- 如果要修改外部變數,宣告__block。
- block在OC中是物件,持有block的物件可能也被block持有,從而引發迴圈引用,可以使用weakSelf。
- block只是儲存了一份程式碼,只有呼叫時才會執行。
- 底層實現
block對應的結構體如下
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
//所有物件都有該指標,用於實現物件相關的功能
void *isa;
//用於按 bit 位表示一些 block 的附加資訊
int flags;
//保留變數
int reserved;
//函式指標,指向具體的 block 實現的函式呼叫地址
void (*invoke)(void *, ...);
//表示該 block 的附加描述資訊,主要是 size 大小,以及 copy 和 dispose 函式的指標
struct Block_descriptor *descriptor;
/* Imported variables. */
//捕獲的變數,block 能夠訪問它外部的區域性變數,就是因為將這些變數(或變數的地址)複製到了結構體中
};
複製程式碼
在 Objective-C 語言中,一共有 3 種型別的 block:
_NSConcreteGlobalBlock
全域性的靜態 block,不會訪問任何外部變數。
_NSConcreteStackBlock
儲存在棧中的 block,當函式返回時會被銷燬。
_NSConcreteMallocBlock
儲存在堆中的 block,當引用計數為 0 時會被銷燬。
- 適用場景
事件響應,資料傳遞,鏈式語法。
- 答疑
- 為什麼不能直接修改區域性變數?
這是因為block會重新生成一份變數,所以區域性變數修改不會影響block中的變數,而且編譯器加了限制,block中的變數也不允許修改。
- 為什麼能修改全域性變數和靜態變數?
全域性變數所佔用的記憶體只有一份,供所有函式共同呼叫,block可以直接使用,而不需要深拷貝或者使用變數指標。 靜態變數實際與__block修飾類似,block是直接使用的指向該靜態變數的指標,並未重新深拷貝。
- 如何修改區域性變數?
將區域性變數使用__block修飾,告訴編譯器這個區域性變數是可以修改的,那麼block不會再生成一份,而是複製使用該區域性變數的指標。
- 為什麼要在block中使用strongSelf?
我們為了防止迴圈引用使用了weakSelf
,但是某些情況在block的執行過程中,會出現self突然釋放的情況,導致執行不正確,所以我們使用strongSelf
來增加強引用,保證後續程式碼都可以正常執行。
那麼豈不是會導致迴圈引用?確實會,但是隻是在block程式碼塊的作用域裡,一旦執行結束,strongSelf就會釋放,這個臨時的迴圈引用就會自動打破。
- block用copy還是strong修飾?
MRC下使用copy,ARC下都可以。
MRC下block建立時,如果block中使用了成員變數,其型別是_NSConcreteStackBlock
,它的記憶體是放在棧區,作用域僅僅是在初始化的區域內,一旦外部使用,就可能造成崩潰,所以一般使用copy
來將block拷貝到堆記憶體,此時型別為_NSConcreteMallocBlock
,使得block可以在宣告域外使用。
ARC下只有_NSConcreteGlobalBlock
和_NSConcreteMallocBlock
型別,如果block中使用了成員變數,其型別是_NSConcreteMallocBlock
,所以無論是strong
還是copy
都可以。
- 如何不使用__block修改區域性變數?
雖然編譯器做了限制,但是我們仍然可以在block中通過指標修改,如
int a = 1;
void (^test)() = ^ {
//通過使用指標繞過了編譯器限制,但是由於block中是外面區域性變數的拷貝,所以即使修改了,外面區域性變數也不會變,實際作用不大。
int *p = &a;
*p = 2;
NSLog(@"%d", a);
};
test();
NSLog(@"%d", a);
複製程式碼
Objective-C物件模型
所有物件在runtime層都是以struct展示的,NSObject就是一個包含了isa指標的結構體,如下
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
複製程式碼
而Class也是個包含了isa的結構體,如下
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製程式碼
objc_object
中的isa指標告訴了runtime自己指向了什麼類,objc_class
中的isa指向父類,最終根元類的isa會指向自己,形成閉環。 Objective-C 2.0中並未具體暴露實現,但我們可以看到 Objective-C 2.0中的大概實現,包含了父類,成員變數表,方法表等。
- 常見使用
- 動態的修改isa的值,即isa swizzling,例如KVO。
- 動態的修改methodLists,即Method Swizzling,例如Category。
ARC與GC
ARC(Automatic Reference Counting)自動引用計數,是蘋果在WWDC 2011大會上提出的記憶體管理技術,常應用於iOS和MacOS。 GC(Garbage Collection)垃圾回收機制,由於Java的流行而廣為人知,簡單說就是系統定期查詢不用的物件,並釋放其記憶體。
- ARC一定不會記憶體洩漏麼?
不是,雖然大部分使用ARC的記憶體管理都做得很好,但是如果使用不當,仍然會造成記憶體洩漏,例如迴圈引用;OC與Core Foundation類進行橋接的時候,管理不當也會記憶體洩漏;指標未清空,造成野指標等。
- 兩者的區別
- 在效能上,GC需要一套額外的系統來跟蹤處理記憶體,分析哪些記憶體是需要釋放的,相對來說就需要更多的計算;ARC是開發者自己來管理資源的釋放,不需要額外系統,效能比GC高。
- GC回收記憶體時,由於定時跟蹤回收,無用記憶體無法及時釋放,並且需要暫停當前程式,如果資源很多,這個延遲將會很大;ARC只需要引用計數為0便立即釋放,沒有延遲。
記憶體分割槽
分為五個區:棧區,堆區,全域性區,常量區,程式碼區。程式啟動後,全域性區,常量區和程式碼區是已經固定的,不會再更改。
- 棧區(stack)
存一些區域性變數,函式跳轉地址,現場保護等,該區由系統處理,無需我們干預。大量的區域性變數,深遞迴,函式迴圈呼叫都可能耗盡棧記憶體而造成程式崩潰 。
- 堆區(heap)
即執行時記憶體,我們建立物件就是在這裡,需要開發者來管理。
- 全域性區/靜態區
用於存放全域性變數和靜態變數,初始化的放在一塊區域,未初始化的放在相鄰的一塊區域。
- 常量區
存放常量,如字串常量,const常量。
- 程式碼區
存放程式碼。
static、const與extern
static修飾的變數儲存在靜態區,在編譯時就分配好了記憶體,會一直存在app記憶體中直到停止執行。該靜態變數只會初始化一次,在記憶體中只有一份,並且限制了它只能在宣告的作用域中使用,例如單例。 注:static也可以在.h檔案中宣告,但是由於標頭檔案可以被其他檔案任意引入使用,此時限制作用域沒有任何意義,違背了它的初衷,而且重複宣告也會報錯。
const用於宣告常量,只讀不可寫,該常量儲存在常量區,編譯時就分配了相關記憶體,也會一直存在app記憶體直到停止執行,示例程式碼如下:
int const *p // *p只讀 ;p變數
int * const p // *p變數 ; p只讀
const int * const p //p和*p都只讀
int const * const p //p和*p都只讀
複製程式碼
extern用於宣告外部全域性變數/常量,告訴編譯器需要找對應的全域性變數,需要在.m中實現,如下寫法是錯誤的
//Person.h
extern NSString *const Test = @"test";
複製程式碼
正確的使用方法是
//Person.h
extern NSString *const Test;
//Person.m
NSString *const Test = @"test";
複製程式碼
它常用於讓當前類可以使用其他類的全域性變數/常量,也經常用於統一管理全域性變數/常量,更規範整潔,並且在打包時配合const使用,可以避免其他人修改。 extern可以在多處宣告,但是實現只能是一份,否則會報重複定義。
預處理
預處理是C語言的一部分,在編譯之前,編譯器會對這些預處理命令進行處理,這些預處理的結果與源程式一起編譯。
- 特徵
- 預處理命令都必須以#開頭。
- 通常位於程式開頭部分。
- 常用預處理命令
- 巨集定義:#define,#undef。
- 條件編譯:#ifdef,#ifndef,#else,#endif。
- #include(C),#import(Objective-C)。
- 巨集
- 巨集並不是C語句,既不是變數也不是常量,所以無需使用=號賦值,也無需用;結束。
- 編譯器對巨集只進行查詢和替換,將所有出現巨集的地方換成該巨集的字串,因此需要開發者自己保證巨集定義是正確的。
- 巨集可以帶引數,最好是將引數用()包住,否則如果引數是個算術式,直接替換會導致結果錯誤。
- 佔用程式碼段,大量使用會導致二進位制檔案增大。
@class與#import
@class
僅僅是告訴編譯器有這個類,至於類裡有什麼資訊,這裡不需要知道,無法使用該類的例項變數,屬性和方法。其編譯效率較#import
更高,因為#import
需要把引用類的所有標頭檔案都走一遍,而@class
不用。
#import
還會造成遞迴引用,如果A、B兩類只相互引用,不會報錯,但是如果任意一方宣告瞭對方的例項,就會報錯。
如何禁止呼叫已有方法?
由於OC中並不能隱藏系統方法,例如我們在實現單例時,為了避免其他人對單例類new、alloc、copy及mutableCopy,保證整個系統中只有一個單例例項,我們可以在標頭檔案中宣告不可用的方法,如下:
//更簡潔
+(instancetype) alloc NS_UNAVAILABLE;
+(instancetype) new NS_UNAVAILABLE;
-(instancetype) copy NS_UNAVAILABLE;
-(instancetype) mutableCopy NS_UNAVAILABLE;
//能自定義提示語
+(instancetype) alloc __attribute__((unavailable("alloc not available, call sharedInstance instead")));
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));
複製程式碼
CocoaPods
-
如何使用? 安裝CocoaPods環境後,cd到需要的工程根目錄下,通過
pod init
建立Podfile檔案,開啟該檔案,新增需要的三方庫pod 'xxx'
,儲存關閉,輸入pod install
即可安裝成功。 如果成功後import不到三方庫的標頭檔案,可以在User header search paths中新增$(SRCROOT)並且選擇recursive。 -
原理 將所有的依賴庫都放到另一個名為Pods的專案中,然後讓主專案依賴Pods專案,這樣原始碼管理工作就從主專案移到了Pods專案。這個Pods專案最終會變成一個libPods.a的檔案,主專案只需要依賴這個.a檔案即可。
nil Nil NULL及NSNull 之間的區別
NULL是C語言的用法,此時呼叫函式或者訪問成員變數,會報錯。可以用來賦值基本資料型別來表示空。 nil和Nil是OC語法,呼叫函式或者訪問成員變數不會報錯,nil是對object物件置為空,Nil是對Class型別的指標置空。 NSNull是一個類,由於nil比較特殊,在Array和Dictionary中被用於標記結束,所以不能存放nil,我們可以通過NSNull來表示該資料為空。但是向NSNull傳送訊息會報錯。
NSDictionary實現原理
NSDictionary(字典)是使用雜湊表 Hash table(也叫雜湊表)來實現的。雜湊表是根據鍵(Key)而直接訪問在記憶體儲存位置的資料結構。也就是說,它通過計算一個關於鍵(key)值的函式,將所需查詢的資料對映到表中一個位置來訪問記錄,這加快了查詢速度。這個對映函式稱做雜湊函式,存放記錄的陣列稱做雜湊表。也就是說雜湊表的本質是一個陣列,陣列中每一個元素其實就是NSDictionary鍵值對。
.a與.framework
- 什麼是庫?
庫是共享程式程式碼的方式,一般分為靜態庫和動態庫。
- 靜態庫與動態庫的區別?
靜態庫:連結時完整地拷貝至可執行檔案中,被多次使用就有多份冗餘拷貝。檔案字尾一般為.a,開發者自己建立的.framework是靜態庫。 動態庫:連結時不復制,程式執行時由系統動態載入到記憶體,供程式呼叫,系統只載入一次,多個程式共用,節省記憶體。檔案字尾一般為.dylib,系統的. framework就是動態庫。
- .a與.framework的區別
.a是純二進位制檔案,還需要.h檔案以及資原始檔,而.framework可以直接使用。
響應鏈
main()函式之前發生了什麼?
@synthesize和@dynamic?
@synthesize語義是如果你沒有手動實現setter/getter方法,那麼編譯器會自動加上這兩個方法。可以用來改變例項變數的名稱,如@synthesize firstName = _myFirstName;
。
@dynamic是告訴編譯器不需要它自動生成,由使用者自己生成(當然對於 readonly 的屬性只需提供 getter 即可)。假如一個屬性被宣告為 @dynamic var,然後你沒有提供 @setter方法和 @getter 方法,編譯的時候沒問題,但是當程式執行到 instance.var = someVar,由於缺 setter 方法會導致程式崩潰;或者當執行到 someVar = var 時,由於缺 getter 方法同樣會導致崩潰。編譯時沒問題,執行時才執行相應的方法,這就是所謂的動態繫結。
- 有了自動合成屬性例項變數之後,@synthesize還有哪些使用場景
我們要搞清楚一個問題,什麼情況下不會autosynthesis(自動合成)?
- 同時重寫了 setter 和 getter 時
- 重寫了只讀屬性的 getter 時
- 使用了 @dynamic 時
- 在 @protocol 中定義的所有屬性
- 在 category 中定義的所有屬性
- 過載的屬性 當你在子類中過載了父類中的屬性,你必須 使用 @synthesize 來手動合成ivar。 除了後三條,對其他幾個我們可以總結出一個規律:當你想手動管理 @property 的所有內容時,你就會嘗試通過實現 @property 的所有“存取方法”(the accessor methods)或者使用 @dynamic 來達到這個目的,這時編譯器就會認為你打算手動管理 @property,於是編譯器就禁用了 autosynthesis(自動合成)。 因為有了 autosynthesis(自動合成),大部分開發者已經習慣不去手動定義ivar,而是依賴於 autosynthesis(自動合成),但是一旦你需要使用ivar,而 autosynthesis(自動合成)又失效了,如果不去手動定義ivar,那麼你就得藉助 @synthesize 來手動合成 ivar。
BAD_ACCESS
訪問了野指標,比如對一個已經釋放的物件執行了release、訪問已經釋放物件的成員變數或者發訊息。
- 如何除錯?
- 重寫object的respondsToSelector方法,顯示出現EXEC_BAD_ACCESS前訪問的最後一個object。
- 通過Edit Scheme-Diagnostics-Zombie Objects。
- 通過全域性斷點。
- 通過Edit Scheme-Diagnostics -Address Sanitizer。