iOS開發小記-基礎篇

進擊的蝸牛君發表於2019-10-23

前幾年學習過程中陸陸續續整理的知識點,今天開始遷移到掘金。由於當年在翻閱國產技術書籍時,發現知識點有不少錯誤,踩了不少坑,當然可能仍然有錯誤和遺漏,歡迎指正~

KVC


KVC允許以字串形式間接操作物件的屬性,全稱為Key Value Coding,即鍵值編碼。

  • 底層實現機制
- (void)setValue:(nullable id)value forKey:(NSString *)key;
複製程式碼
  1. 首先查詢-set<Key>:程式碼通過setter方法賦值。(勘誤1)
  2. 否則,檢查+(BOOL)accessInstanceVariablesDirectly方法,如果你重寫了該方法並使其返回NO,則KVC下一步會執行setValue:forUndefinedKey:,預設丟擲異常。
  3. 否則,KVC會依次搜尋該類中名為_<key>_<isKey><key><isKey>的成員變數。
  4. 如果都沒有,則執行setValue:forUndefinedKey:方法,預設丟擲異常。

勘誤1:經驗證,在查詢`-set<Key>:`後,如果沒有找到,還會去查詢`-_set<Key>:`方法,然後才會進入步驟2,感謝@QXCloud 的指正~

- (nullable id)valueForKey:(NSString *)key;
複製程式碼
  1. 首次依次查詢-get<Key>-<key>-is<Key>程式碼通過getter方法獲取值。
  2. 否則,查詢-countOf<Key>-objectIn<Key>AtIndex:-<key>AtIndexes:方法,如果count方法和另外兩個中的一個被找到,返回一個能響應所有NSArray方法的代理集合,簡單來說就是可以當NSArray用。
  3. 否則,查詢-countOf<Key>-enumeratorOf<Key>-memberOf<Key>:方法,如果三個都能找到,返回一個能響應所有NSSet方法的代理集合,簡單來說就是可以當NSSet使用。
  4. 否則,依次搜尋該類中名為_<key>_<isKey><key><isKey>的成員變數,返回該成員變數的值。
  5. 如果都沒有,則執行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異常,一般也需要重寫。

  • 常見應用場景
  1. 可以靈活的使用字串動態取值和設值,但通過KVC操作物件的效能比getter和setter更差。
  2. 訪問和修改私有屬性,最常見的就是修改UITextField中的placeHolderText。
  3. 通過- (void)setValuesForKeysWithDictionary:字典轉model,如股票欄位。
  4. 當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個物件,而不是容器本身進行操作,由此我們可以有效的提取容器中每個物件的指定屬性值集合。
  5. 使用函式操作容器中的物件,快速對各物件中的基礎型別屬性做運算,如@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指向的都是同一份記憶體地址。 而stringWithFormatinitWithFormat是在執行時建立出來的,儲存在執行時記憶體(即堆記憶體),它們在堆裡面請求對應的值,如果存在,系統便不再分配地址。

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(分類)


分類用於對已有的類新增新方法,並不需要建立新的子類,不需要訪問原有類的原始碼,可以將類定義模組化地分佈到多個分類中。

  • 特點
  1. 分類的方法可以與原來類同名,如果在分類中實現了該方法,分類的方法優先順序大於原有類方法。(不建議同名,分類的作用是新增方法,應使用子類重寫或者統一加字首)
  2. 分類只能新增方法,不能新增成員變數。
  3. 分類不僅影響原有類,還影響其子類。
  4. 一個類支援定義多個分類。
  5. 如果多個分類中有相同方法,執行時到底呼叫哪個方法由編譯器決定,最後一個參與編譯的方法會被呼叫。
  6. 分類是在執行時載入的,不是在編譯時。
  7. 可以新增屬性,但是@property不會生成setter和getter方法,也不會生成對應成員變數。(實際沒有意義)
  • 使用場景
  1. 模組化設計:對於一個超大功能類來說,通過分類將其功能拆分,是個十分有效的方式,有利於管理和協同開發。
  2. 宣告私有方法:我們可以利用分類宣告一個私有方法,這樣可以外部直接使用該方法,不會報錯。
  3. 實現非正式協議:由於分類中的方法可以只宣告不實現,原來協議中不支援可選方法,就可以通過分類宣告可選方法,實現非正式協議。
  • 為什麼不能新增成員變數?

分類的實現是基於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表中,通過記憶體定址,當物件銷燬時,會找到對應儲存的關聯物件做清理工作。

詳見《深入理解Objective-C:Category》

Extension(擴充套件)


擴充套件與分類相似,相當於匿名分類,但分類通常有.h和.m檔案,而擴充套件常用於臨時對某個類的介面進行擴充套件,一般宣告私有屬性、私有方法、私有成員變數。

  • 特點
  1. 可以單獨以檔案定義,命名方式與分類相同。
  2. 通常放在主類的.m中。
  3. 擴充套件是在編譯時載入的。
  4. 擴充套件新新增的方法,類一定要實現。

Block


block是對C語言的擴充套件,用來實現匿名函式的特性。

  • 特性
  1. 對於區域性變數預設是隻讀屬性。
  2. 如果要修改外部變數,宣告__block。
  3. block在OC中是物件,持有block的物件可能也被block持有,從而引發迴圈引用,可以使用weakSelf。
  4. 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 時會被銷燬。

  • 適用場景

事件響應,資料傳遞,鏈式語法。

  • 答疑
  1. 為什麼不能直接修改區域性變數?

這是因為block會重新生成一份變數,所以區域性變數修改不會影響block中的變數,而且編譯器加了限制,block中的變數也不允許修改。

  1. 為什麼能修改全域性變數和靜態變數?

全域性變數所佔用的記憶體只有一份,供所有函式共同呼叫,block可以直接使用,而不需要深拷貝或者使用變數指標。 靜態變數實際與__block修飾類似,block是直接使用的指向該靜態變數的指標,並未重新深拷貝。

  1. 如何修改區域性變數?

將區域性變數使用__block修飾,告訴編譯器這個區域性變數是可以修改的,那麼block不會再生成一份,而是複製使用該區域性變數的指標。

  1. 為什麼要在block中使用strongSelf?

我們為了防止迴圈引用使用了weakSelf,但是某些情況在block的執行過程中,會出現self突然釋放的情況,導致執行不正確,所以我們使用strongSelf來增加強引用,保證後續程式碼都可以正常執行。 那麼豈不是會導致迴圈引用?確實會,但是隻是在block程式碼塊的作用域裡,一旦執行結束,strongSelf就會釋放,這個臨時的迴圈引用就會自動打破。

  1. block用copy還是strong修飾?

MRC下使用copy,ARC下都可以。 MRC下block建立時,如果block中使用了成員變數,其型別是_NSConcreteStackBlock,它的記憶體是放在棧區,作用域僅僅是在初始化的區域內,一旦外部使用,就可能造成崩潰,所以一般使用copy來將block拷貝到堆記憶體,此時型別為_NSConcreteMallocBlock,使得block可以在宣告域外使用。 ARC下只有_NSConcreteGlobalBlock_NSConcreteMallocBlock型別,如果block中使用了成員變數,其型別是_NSConcreteMallocBlock,所以無論是strong還是copy都可以。

  1. 如何不使用__block修改區域性變數?

雖然編譯器做了限制,但是我們仍然可以在block中通過指標修改,如

int a = 1;
void (^test)() = ^ {
    //通過使用指標繞過了編譯器限制,但是由於block中是外面區域性變數的拷貝,所以即使修改了,外面區域性變數也不會變,實際作用不大。
    int *p = &a;
    *p = 2;
    NSLog(@"%d", a);
};
test();
NSLog(@"%d", a);
複製程式碼

詳見《談Objective-C block的實現》

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中的大概實現,包含了父類,成員變數表,方法表等。

  • 常見使用
  1. 動態的修改isa的值,即isa swizzling,例如KVO。
  2. 動態的修改methodLists,即Method Swizzling,例如Category。

ARC與GC


ARC(Automatic Reference Counting)自動引用計數,是蘋果在WWDC 2011大會上提出的記憶體管理技術,常應用於iOS和MacOS。 GC(Garbage Collection)垃圾回收機制,由於Java的流行而廣為人知,簡單說就是系統定期查詢不用的物件,並釋放其記憶體。

  • ARC一定不會記憶體洩漏麼?

不是,雖然大部分使用ARC的記憶體管理都做得很好,但是如果使用不當,仍然會造成記憶體洩漏,例如迴圈引用;OC與Core Foundation類進行橋接的時候,管理不當也會記憶體洩漏;指標未清空,造成野指標等。

  • 兩者的區別
  1. 在效能上,GC需要一套額外的系統來跟蹤處理記憶體,分析哪些記憶體是需要釋放的,相對來說就需要更多的計算;ARC是開發者自己來管理資源的釋放,不需要額外系統,效能比GC高。
  2. 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語言的一部分,在編譯之前,編譯器會對這些預處理命令進行處理,這些預處理的結果與源程式一起編譯。

  • 特徵
  1. 預處理命令都必須以#開頭。
  2. 通常位於程式開頭部分。
  • 常用預處理命令
  1. 巨集定義:#define,#undef。
  2. 條件編譯:#ifdef,#ifndef,#else,#endif。
  3. #include(C),#import(Objective-C)。
  • 巨集
  1. 巨集並不是C語句,既不是變數也不是常量,所以無需使用=號賦值,也無需用;結束。
  2. 編譯器對巨集只進行查詢和替換,將所有出現巨集的地方換成該巨集的字串,因此需要開發者自己保證巨集定義是正確的。
  3. 巨集可以帶引數,最好是將引數用()包住,否則如果引數是個算術式,直接替換會導致結果錯誤。
  4. 佔用程式碼段,大量使用會導致二進位制檔案增大。

@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可以直接使用。

詳見《iOS中.a與.framework庫的區別》

響應鏈


詳見《iOS響應鏈(Responder Chain)》

main()函式之前發生了什麼?


詳見《iOS 程式 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(自動合成)?

  1. 同時重寫了 setter 和 getter 時
  2. 重寫了只讀屬性的 getter 時
  3. 使用了 @dynamic 時
  4. 在 @protocol 中定義的所有屬性
  5. 在 category 中定義的所有屬性
  6. 過載的屬性 當你在子類中過載了父類中的屬性,你必須 使用 @synthesize 來手動合成ivar。 除了後三條,對其他幾個我們可以總結出一個規律:當你想手動管理 @property 的所有內容時,你就會嘗試通過實現 @property 的所有“存取方法”(the accessor methods)或者使用 @dynamic 來達到這個目的,這時編譯器就會認為你打算手動管理 @property,於是編譯器就禁用了 autosynthesis(自動合成)。 因為有了 autosynthesis(自動合成),大部分開發者已經習慣不去手動定義ivar,而是依賴於 autosynthesis(自動合成),但是一旦你需要使用ivar,而 autosynthesis(自動合成)又失效了,如果不去手動定義ivar,那麼你就得藉助 @synthesize 來手動合成 ivar。

BAD_ACCESS


訪問了野指標,比如對一個已經釋放的物件執行了release、訪問已經釋放物件的成員變數或者發訊息。

  • 如何除錯?
  1. 重寫object的respondsToSelector方法,顯示出現EXEC_BAD_ACCESS前訪問的最後一個object。
  2. 通過Edit Scheme-Diagnostics-Zombie Objects。
  3. 通過全域性斷點。
  4. 通過Edit Scheme-Diagnostics -Address Sanitizer。

相關文章