iOS開發小記-基礎篇

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

KVC


KVC容許以字符串形式間接操做對象的屬性,全稱爲Key Value Coding,即鍵值編碼。ios

  • 底層實現機制
- (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的可用性,經過以下方法markdown

- (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時通常須要重寫這兩個方法。app

- (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,即鍵值監聽。ide

經常使用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。
相關文章
相關標籤/搜索