iOS開發基礎知識梳理

我只是一個搬運工,僅僅爲了加深記憶,感謝做者分享,文章大部分來源:進擊的蝸牛君html

alloc/init與new


  • alloc/init

alloc負責爲對象的全部成員變量分配內存空間,而且爲各個成員變量重置爲默認值,如int型爲0,BOOL型爲NO,指針型變量爲nil。僅僅分配空間還不夠,還須要init來對對象執行初始化操做,纔可使用它。若是隻調用alloc不調用init,也能運行,但可能會出現未知結果。數組

  • new

所作的事情和alloc差很少,也是分配內存和初始化。安全

  • 二者區別

new只能使用默認的init初始化,而alloc可使用其餘初始化方法,由於顯示調用總比隱式調用好,因此每每使用alloc/init來初始化。bash

@「hello」和[NSString stringWithFormat:@"hello"]有何區別?


NSString *A = @"hello";
NSString *B = @"hello";
NSString *C = [NSString stringWithFormat:@"hello"];
NSString *D = [NSString stringWithFormat:@"hello"];
複製代碼

@「hello」位於常量池,可重複使用,其中A和B指向的都是同一分內存地址。而[NSString stringWithFormat:@"hello"]是運行時建立出來的,保存在運行時內存(即堆內存),其中C和D指向的內存地址不一樣,與A、B也不相同。數據結構

Propoty修飾符


具體能夠分爲四類:線程安全、讀寫權限、內存管理和指定讀寫方法。app

  • 線程安全(atomic, nonatomic)

若是不寫該類修飾符,默認就是atomic。二者的最大區別決定編譯器生成的getter/setter方法是否屬於原子操做,若是本身寫了getter/setter方法,此時用什麼都同樣。對於atomic來講,getter/setter增長了鎖來確保數據操做的完整性,不受其餘線程影響。例如線程A的getter方法運行到一半,線程B調用setter方法,name線程A仍是能獲得一個完整的Value。而對nonatomic來講,多個線程能同時訪問操做,就沒法保證是不是完成的Value,還會發生髒數據。可是nonatomic更快,開發中每每在可控狀況下安全換效率。ide

注意:atomic並不能徹底保證線程安全,只能保證數據操做的線程安全,例如線程A使用getter方法,同時線程B、C使用setter方法,那最後線程A取到的值有3中可能:原始值、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中如何使用@propety

在protocol中使用property只會生成setter和getter方法聲明,咱們使用屬性的目的,是但願遵照協議的對象能實現該屬性。category使用@propety也是隻會生成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時,查出對應的SideTable,搜索key對應的指針數組,而且遍歷數組將全部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;
}

複製代碼

KVC


KVC容許以字符串形式簡介操做對象的屬性,全稱爲Key Value Coding, 健值編碼。

  • 底層實現機制
-(void) setValue:(nullable id)value forKey:(NSString *)key;
複製代碼
  1. 首先查找-set<Key>:代碼經過setter方法賦值,若是-set<Key>:沒有找到,還會去查找_set<Key>:方法。
  2. 不然,檢查+(BOOL)accessInstanceVariableDirectly方法,若是你重寫了該方法並使其返回NO,則KVC下一步會執行setValue:forUndefineKey:,默認拋出異常。
  3. 不然,KVC會依次搜索該類名爲_<key>_<isKey><key><isKey>的成員變量。
  4. 若是都沒有,則執行setValue:forUnderfinekKey方法,默認拋出異常。
- (nullable id)valueForKey:(NSString *)key;
複製代碼
  1. 首次依次查找-get<key>, -<key>,is<key>代碼經過getter方法獲取值。
  2. 不然,查找countOf<key>objectIn<Key>AtIndex:-<key>AtIndexes方法和另外兩個中的一個被找到,返回一個響應全部NSArray方法的代理集合,簡單來講就是能夠當NSArray用。
  3. 不然,查找-countOf<key>, -enumeratorOf<key>-memberOf<key>:方法,若是三個都能找到,返回一個全部NSSet方法的代理集合,簡單來講就是能夠當NSSet使用。
  4. 不然,依次搜索該類中名爲_<key>, _<isKey>, <key>, <isKey>的成員變量,返回該成員變量的值。
  5. 若是沒有,則執行valueForUndefineKey:方法,默認拋出異常。
  • 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)accessInstanceVariableDirectly來決定是否搜索類似成員變量,所以只須要重寫該方法並返回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時,會拋出NSValidArgumentException異常,通常也須要重寫。

  • 常見應用場景
  1. 能夠靈活的使用字符串動態的取值和設值,但經過KVC操做對象的性能比getter和setter更差。
  2. 訪問和修改私有屬性。
  3. 經過-(void)setValuesForKeysWithDictionary:字典轉model。
  4. 當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每個對象,而不是容器自己進行操做,由此咱們能夠有效的提取容器中每一個對象的指定屬性值集合。
  5. 使用函數操做容器中的對象,快速對各對象中的基礎類型屬性作運算,如@avg@count,@max,@min,@sum

KVO


KVO提供了一種機制,(基於NSKeyValueObservin協議,全部的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方法,在原方法的調用先後通知觀察者值的改變。而後將對象A的isa指針(isa指針告訴Runtime這個對象的類是什麼)指向NSKVONotifying_A這個新類,那麼對象A就變成了新建立類的實例,不只如此,Apple還重寫了-class方法來隱藏該新類,讓人們覺得註冊先後對象A的類並無改變,但實際上若是手動新建一個NSKVONotifying_A類,在觀察者運行到註冊時,便會引發重複類崩潰。

  • 注意事項

從上述實現原理來看,很明顯能夠知道,若是沒有經過setter賦值,直接賦值該成員變量是不會觸發KVO機制的,可是KVC除外,這也側面證實了KVC和KVO是有內在聯繫的。

分類


分類用於對已有的類添加新方法,並不須要建立新的子類,不須要訪問原有類的源代碼,能夠將類定義模塊化地分佈到多個分類中。

  • 特色
  1. 分類的方法能夠與原來類同名,若是在分類中實現了該方法,分類的方法優先級大於原有類方法。(不建議同名,分類的做用是新增方方法,應使用子類重寫或者統一加前綴)。
  2. 分類只能添加方法,不能添加成員變量。
  3. 分類不只影響原有類,還影響子類。
  4. 一個類支持定義多個分類。
  5. 若是多個分類中有相同方法,運行時到底調用哪一個方法由編譯器決定,最後一個參與編譯的方法會被調用。
  6. 分類是在運行時加載的,不是在編譯器。
  7. 能夠添加屬性,可是@propetry不會生成setter和getter方法。也不會生成對應成員變量。(實際沒有意義)
  • 使用場景
  1. 模塊化設計:對於一個超大功能類來講,經過分類將其功能拆分,是個十分有效的方式,有利於管理和協同開發。
  2. 聲明私有方法:咱們能夠利用分類聲明一個私有方法,這樣能夠外部直接使用該方法,不會報錯。
  3. 實現非正式協議:因爲分類中的方法能夠只聲明不實現,原來協議中不支持可選方法,就能夠經過分類聲明可選方法,實現非正式協議。
  • 爲何不能添加成員變量?

分類的實現是基於Runtime動態的將分類中方法添加到類中,Runtime中經過class_addIvar()方法添加成員變量,但蘋果對該方法只能在構造一個類的過程當中用,不容許對一個已有的類動態的添加成員變量。爲何蘋果不容許?這是由於對象在運行期已經給成員變量都分配了內存,若是動態添加屬性,不只須要破壞內部佈局,並且已經建立的類的實例也符合當前類的定義,這簡直是災難性的。可是方法保存在類的可變區域中,修改是不會影響類的內存佈局的因此沒問題。

  • 如何添加有效屬性?

在分類中聲明屬性能夠編譯經過,可是使用該屬性,會報找不到getter/setter方法,這是因爲即便聲明屬性,也不會生成成員變量,天然也沒有必要實現getter/setter方法,那麼咱們就須要經過Runtime的關聯對象來爲屬性實現getter/setter方法。例如對Person的一個分類增長SPeciaName屬性,實現代碼以下

#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語言的擴展,用來實現匿名函數的特性,Block本質也是OC對象,它內部也有isa指針,是封裝了函數調用以及函數調用環境的OC對象。

  • 特性
  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中的變量也不容許修改。

2. 爲何能修改全局變量和靜態變量

全局變量所佔用的內存只有一份,供全部函數共同調用,block能夠直接使用,而不須要深拷貝或者使用變量指針。靜態變量實際與_block修飾相似,block是直接使用的指向靜態變量的指針,並未從新深考貝。

3. 如何修改局部變量

將局部變量使用_block修飾,告訴編譯器這個局部變量是能夠修改的,那麼block不會再生成一份,而是複製使用該局部變量的指針。

4. 爲何要在block中使用strongWeak

咱們爲了防止循環引用使用了weakSelf, 可是某些狀況在block執行過程當中,會出現self忽然釋放的狀況,致使運行不正確,因此咱們使用strongSelf來增長強引用,保證後續代碼能夠正常運行。那麼豈不是會致使循環引用? 確實會,可是隻是在block代碼塊的做用域裏,一旦執行結束,strongSelf就會釋放,這個臨時的循環引用就會自動打破。

5. block用copy仍是strong修飾

MRC下使用copy,ARC下均可以。MRC下block建立時,若是block中使用了成員變量,齊類型是_NSConcreteStackBlock,它的內存是放在棧區,做用域僅僅是在初始化的區域內,一旦外部使用,就可能形成崩潰,因此通常使用copy來將blcok拷貝到堆內存,此時類型爲_NSConcreteMallocBlock,使得block能夠在聲明域外使用。ARC下只有_NSConcreteGlobleBlock_NSConcreteMallocBlock類型,若是block中使用了成員變量,其類型是_NSConcreteMallocBlock,因此不管是strong仍是copy均可以。

6. 如何不使用_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、allco、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")));
複製代碼

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能夠直接使用。詳見《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方法會致使程序崩潰;或者當運行到sameVar = var時,因爲缺乏getter方法一樣會致使崩潰。編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定。

  • 有了自動合成屬性實例變量以後,@synthesize還有哪些使用場景

咱們要搞清楚一個問題,什麼狀況下不會autosynthesis(自行合成)?

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

BAD_ACCESS


訪問了野指針,好比對一個已經釋放的對象執行了release、訪問已經釋放對象的成員變量或者發消息。

  • 如何調試?
  1. 重寫object的respondsToSelector,顯示出現EXEC_BAD_ACCES前訪問的最後一個object。
  2. 經過Edit Scheme-Diagnostics-Zombie Objects。
  3. 經過全局斷點。
  4. 經過Edit Scheme-Diagnostics-Address Sanitizer。
相關文章
相關標籤/搜索