iOS全解1-1:基礎/內存管理RAC、Block、GCD與多線程

面試系列:


基本概念

一、OC的三大特性為:封裝、繼承、多態

1、封裝:
  • 變量 的封裝:setter、getter 方法,self調用方便高效
  • 方法 的封裝:簡潔、高效,不用關心內部過程
  • SDK 的封裝:使用方便、私有隱藏,安全性高(不會泄露數據和代碼)
2、繼承
  • 抽取了重復代碼:子類可以擁有父類中的所有成員變量和方法
  • 建立了類與類之間的聯系
  • 每個類中都有一個superclass指針指向父類
  • 所有類的根類都是NSObject,便于isa指針 查詢
    • 調用某個對象的方法時,優先去當前類中找,如果找不到,再去父類中找
    • 子類重新實現父類的某個方法,會覆蓋父類以前的方法。

繼承的缺點:耦合性太高(類與類之間的關系過于緊密),沒有多繼承,OC里面都是單繼承,多繼承可以用protocol委托代理來模擬實現;可以通過實現多個接口完成OC的多重繼承。(實現多個協議)

3、多態

例如: -(void) animal:(id); id 就是多態,傳入時,才識別 具體類的真實形象。runtime 也是運行時,才識別具體類。

  1. 要想使用多態必須使用繼承(繼承是多態的前提)

  2. 多態:父類指針指向子類對象 Animal *aa = [Dog new]; 調用方法時會檢測對象的真實形象

  3. 好處:如果函數或方法參數中使用的是父類類型,可以傳入父類,子類對象。

  4. 局限性:父類類型的變量不能直接調用子類特有的方法,必須強制轉換為子類類型變量后,才能使用。


內存管理RAC:

ARC 相對于 GC 的優點:
ARC 工作在編譯期,在運行時沒有額外開銷。
ARC 的內存回收是平穩進行的,對象不被使用時會立即被回收。而 GC 的內存回收是一陣一陣的,回收時需要暫停程序,會有一定的卡頓。

ARC 相對于 GC 的缺點:
GC 真的是太簡單了,基本上完全不用處理內存管理問題,而 ARC 還是需要處理類似循環引用這種內存管理問題。

property的關鍵字分三類:

1.原子性(也就線程安全)

有atomic和nonatomic, acomic就是線程安全,但是一般使用nonacomic,因為acomic的線程安全開銷太大,影響性能,即使需要保證線程安全,我們也可以通過自已的代碼控制,而不用acomic。

2.引用計數:

assign:

assign用于非指針變量,基本修飾數據類型,統一由系統的棧區進行內存管理。(int、double、float等)

weak:

對對象的弱引用,不增加引用計數,也不持有對象,當對象消失后指針自動變為nil。

copy:分為深拷貝、淺拷貝,可變復制、不可變復制

淺拷貝:對內存地址的復制,讓目標對象指針和原對象指向同一片內存空間會增加引用計數

深拷貝:對對象內容的復制,開辟新的內存空間

單層深復制,也就是我們經常說的深復制,我這里說的單層深復制是對于集合類所說的(即NSArray、NSDictionary、NSSet),單層深復制指的是只復制了該集合類的最外層,里邊的元素沒有復制,(即這兩個集合類的地址不一樣,但是兩個集合里所存儲的元素的地址是一樣的)

完全復制,指的是完全復制整個集合類,也就是說兩個集合地址不一樣,里邊所存儲的元素地址也不一樣

引用:深拷貝和淺拷貝,可變復制,不可變復制(這里解釋更詳細)http://www.lxweimin.com/p/e3da07684bf1

  • 非集合類(NSString,NSNumber)
[noMutableObject copy]        //淺復制  (復制不可變對象)
[noMutableObject mutableCopy] //深復制  (復制不可變對象)
[mutableObject copy]        //深復制  (復制可變對象)
[mutableObject mutableCopy] //深復制  (復制可變對象)
  • 集合類(NSArray,NSDictionary, NSSet)
 [noMutableObject copy]        //淺復制    (復制不可變對象)
 [noMutableObject mutableCopy] //單層深復制 (復制不可變對象)
 [mutableObject copy]        //單層深復制 (復制可變對象)
 [mutableObject mutableCopy] //單層深復制 (復制可變對象)
這個寫法會出什么問題:@property (nonatomic, copy) NSMutableArray *array;

問題:添加/刪除/修改 數組內的元素的時候,程序會因為找不到對應的方法而崩潰。
原因:是因為 copy 就是復制一個不可變 NSArray 的對象,不能對 NSArray 對象進行添加/修改。

然后在需要修改時,創建一個可變的副本:
NSMutableArray *mutableArray = [self.array mutableCopy];
[mutableArray addObject:newObject];
self.array = [mutableArray copy];
  • 那么如何實現多層復制呢?我們以NSArray舉例說明
 // 完全復制
NSArray *copyArray = [[NSArray alloc] initWithArray:array copyItems:YES]; 
需要特別注意的是

以上我們所說的兩種情況默認都實現了NSCopyingNSMutableCopying協議。
對于自定義繼承自NSObject的類

  • copy需要實現NSCopying協議,然后實現以下方法,否則copy會crash
-(id)copyWithZone:(NSZone *)zone {
     CopyObject  *copy = [[[self class] alloc] init];
     copy.name = self.name;
     copy.mobile = self.mobile;
     copy.company = self.company;
     copy.descInfo = self.descInfo;
     return copy;
 } 
  • mutableCopy時,需要實現NSMutableCopying協議,否則mutableCopy會crash
 -(id)mutableCopyWithZone:(NSZone *)zone {
     MutableCopyObject  *mutableCopy = [[[self class] alloc] init];
     mutableCopy.name = self.name;
     mutableCopy.mobile = self.mobile;
     mutableCopy.company = self.company;
     mutableCopy.descInfo = self.descInfo;
     return mutableCopy;
 }
strong:淺拷貝,也就是指針引用

是每對這個屬性引用一次,retainCount 就會+1,只能修飾 NSObject 對象,不能修飾基本數據類型。是 id 和 對象 的默認修飾符。

unsafe_unretained(assgin):和week 非常相似

可以同時修飾基本數據類型和 NSObject 對象 ,其實它本身是 week 的前身 , 在 iOS5 之后,基本都用 week 代替了 unsafe_unretained 。 但它們之間還是稍微有點區別的,并不是完全一樣,對上層代碼來說,能用 unsafe_unretained 的地方,都可以用 week 代替?!巴瑫r要注意一點,這個修飾符修飾的變量不屬于編譯器的內存管理對象”


二、類別Category

重寫一個類的方式用「繼承」還是「分類」,取決于具體情況:

  • 假如目標類有許多的子類,我們需要拓展這個類又不希望影響到原有的代碼,繼承后比較好。
  • 如果僅僅是拓展方法,分類更好.(不需要涉及到原先的代碼)
優點:
  • 分類中方法的優先級比原來類中的方法高,也就是說,在分類中重寫了原來類中的方法,那么分類中的方法會覆蓋原來類中的方法。+(void)load方法是一個特例,它會在當前類執行完之后,category中的再執行。)
  • 可以用runtime進行method swizzling(方法的偷梁換柱)來處理異常調用的方法。
缺點:

通過觀察頭文件我們可以發現,Cocoa框架中的許多類都是通過category來實現功能的,可能不經意間你就覆蓋了這些方法中的其一,有時候就會產生一些無法排查的異常原因。


其他問題

淺拷貝和深拷貝的區別?

淺拷貝:只復制指向對象的指針,指針指向同一個地址,而不復制引用對象本身。
深拷貝:復制引用對象本身。內存中存在了兩份獨立對象本身,當修改A 時,B 不變。(即:B = [A copy]; )



一、內存管理

案例:辦公室開關燈,有人開燈,無人關燈;進人加一,出人減一;第一個開燈的人持有此燈,第一個人能廢棄此燈,其他人只是弱引用。

內存管理原則:

1.自己生成的對象,自己持有
2.非自己生成的對象,自己也能持有
3.不再需要自己持有的對象時,釋放掉
4.非自己持有的對象,無法釋放(釋放會崩潰)

對象操作與Objective-C方法的對應
對象操作 Objective-C方法
生成并持有對象 alloc、new、copy、mutableCopy
持有對象 retain (引用計數加1)
釋放對象 release(引用計數減1)
廢棄對象 dealloc(引用計數為0)

相關釋放函數:autorelease、NSAutoreleasePool、@autoreleasepool

2、非自己生成的對象,自己也能持有

 id obj = [NSMutableArray array];
 [obj retain];

* 使用autorelease方法,可以取得對象的存在,但是自己不持有對象(obj不立刻釋放掉,注冊到autoreleasePool中)

- (id)object {
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    return obj;
}


ARC 環境下的使用功能規則

  • 不能使用retain、release、retainCount、autorelease
  • 不能使用NSAllocateObject、NSDeallocateObject
  • 必須遵循內存管理命名規則(駝峰命名法)
  • 不要顯式調用dealloc
  • 使用 @autorelease 代替 NSAutoreleasePool
  • 不能使用區域 (NSZone)
  • 對象型變量不能作為C語言結構體(struct/union)的成員
  • 顯式轉換 “id” 和 “void *”


會讓對象引用計數增加的操作:

1.  new、alloc、retain、copy、mutableCopy
2.  添加視圖:用 addview 把一個控件添加到視圖上,這個控件的引用計數+1;
3.  添加數組:把一個對象添加到數組中,數組內部會把這個對象的引用計數+1;
4.  屬性賦值:會讓對象的引用計數+1;
5.  屬性關聯:xib中的控件,跟代碼關聯后,會讓對象的引用計數+1;
6.  push這個操作會讓對象的引用計數增加。


ARC、MRC混編

  • ARC環境下 使用MRC代碼,使用 -fno-objc-arc轉換
  • MRC環境下 使用ARC代碼,使用 -fobjc-arc轉換

所有權修飾符

  • _ _strong 修飾符
  • _ _weak 修飾符
  • _ _unsafe_unretained 修飾符(MRC環境下使用)
  • _ _autoreleasing 修飾符 (ARC環境下使用)

1、_ _strong修飾符是id類型和對象類型的默認修飾符,一般是隱式的;

id obj = [[NSObject alloc] init];
//同上
id _ _strong obj = [[NSObject alloc] init];

2、 _ _weak 解決 循環引用的 修飾符,防止內存泄漏。在持有某對象的弱引用時,若該對象被廢棄,則弱引用自動失效,且被賦值nil,不在持有該對象(持有的對象超出作用域,會被廢棄銷毀)。

3、_ _unsafe_unretained 和 _ _weak修飾的變量一樣,因為自己生成并持有的對象不能繼續為自己所有,所以生成的對象會被立即釋放。

4、_ _autoreleasing 隱式修飾符:非自己生成的對象,自己也能持有,會注冊到自動釋放池中。


屬性聲明的屬性 與 所有權修飾符的對應關系

聲明的屬性 所有權修飾符 說明
assign _ _unsafe_unretained 修飾基本數據類型,如NSInteger和CGFloat,這些數值主要存在于棧上。
copy _ _strong 每次復制會在內存中拷貝一份對象,指針指向不同的地址
retain _ _strong 用來持有對象
strong _ _strong 持有對象,引用計數會增加1。該對象只要引用計數不為0則不會被銷毀。當然強行將其設為nil可以銷毀它。
unsafe_unretained _ _unsafe_unretained 不安全持有
weak _ _weak 不擁有該對象。其修飾的對象引用計數不會增加。無需手動設置,該對象會自行在內存中銷毀。


釋放對象時,廢棄對象不被持有的動作:

1、objc_release (釋放)
2、因為引用計數為0,所以執行dealloc(釋放內存)
3、_objc_rootDealloc(根解除)
4、object_dispose(廢棄處理)
5、objc_destructInstance(銷毀)
6、objc_clear_deallocating(清除)


對象被廢棄時,最后調用objc_clear_deallocating的動作(相關表 SideTables):

1、從weak表中獲取廢棄對象的記錄(地址為鍵值)。address= h(key)
2、將包含在記錄中的所有附有_ _weak修飾符變量的地址,賦值nil。
3、從weak表中刪除該記錄。
4、從引用計數表中刪除廢棄對象的記錄(地址為鍵值)。


/* MRC環境
 * NSAutoreleasePool
 * autorelease
 */
- (void)test2 {
    
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool drain];
   
    //同上
    @autoreleasepool {
        id obj = [[NSObject alloc] init];
        [obj autorelease];
    }
    
 //同上 (ARC:ReferenceCountingVC2 )
 @autoreleasepool {
        id __autoreleaseing obj2;
        obj2 =  obj
    }
}



下面了解幾個概念:

內存泄漏:應當廢棄的對象在超出其生存周期后繼續存在。

野指針:指針變量未初始化,其值是隨機的;指針釋放后未置空,指向不存在的內存。

懸垂指針: 指向曾經存在的對象,但是該對象已經不存在了。

標記指針: Tagged Pointer
是一種特殊的“指針”,其特殊在于,其實它存儲的并不是地址,而是真實的數據和一些附加的信息。(后面做專門講解)

isa 指針: NONPOINTER_ISA
對象的isa指針,用來表明對象所屬的類類型和一些附加信息。(nonPointer_isa 后面做專門講解)


標記指針: Tagged Pointer

tagged pointer是一種特殊的“指針”,其特殊在于,其實它存儲的并不是地址,而是真實的數據和一些附加的信息。

image.png

可以看到,利用tagged pointer后,“指針”即存儲了對象本身,也存儲了和對象相關的標記。這時的tagged pointer里面存儲的不是地址,而是一個數據集合。同時,其占用的內存空間也由16字節縮減為8字節。
(即:Tagged Pointer = 對象本身 + 對象相關標記)

我們可以在WWDC2013的《Session 404 Advanced in Objective-C》視頻中,看到蘋果對于Tagged Pointer特點的介紹:
? Tagged Pointer專門用來存儲小的對象,例如NSNumber, NSDate, NSString。

? Tagged Pointer指針的值不再是地址了,而是真正的值。實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內存并不存儲在堆中,也不需要malloc和free。

? 在內存讀取上有著3倍的效率,創建時比以前快10倍

例如:NSString 其輸出的class類型為 NSTaggedPointerString。在字符串長度在9個以內時,iOS其實使用了tagged pointer做了優化的。
直到字符串長度大于9,字符串才真正成為了__NSCFString類型。
首先,iOS需要一個標志位來判斷當前指針是真正的指針還是tagged pointer。

在runtime源碼的objc-internal.h中,有關于標志位的定義如下:

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, // 標志位是2的 tagger pointer表示這是一個NSString對象。
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6, 
    OBJC_TAG_RESERVED_7        = 7, 

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
  • ‘地址’以 0xa開頭,轉換為二進制是1010, 首位1表示這是一個tagged pointer,而010轉換為十進制是2,表示這是一個NSString類型

  • ‘地址’以0xb開頭, 轉換為二進制是1011, 首位1表示這是一個tagged pointer, 而011轉換為十進制是3,表示這是一個NSNumber類型。
    (即:標志位是2的tagger pointer表示這是一個NSString對象。)
    由于一個tagged pointer所指向的并不是一個真正的OC對象,它其實是沒有isa屬性的。


isa 指針(NONPOINTER_ISA)

isa:是一個指向對象所屬Class類型的指針。(nonPointer_isa)
對象的isa指針,用來表明對象所屬的類類型和一些附加信息。

如果isa指針僅表示類型的話,對內存顯然也是一個極大的浪費。于是,就像tagged pointer一樣,對于isa指針,蘋果同樣進行了優化。isa指針表示的內容變得更為豐富,除了表明對象屬于哪個類之外,還附加了 「引用計數」extra_rc,是否有被weak引用標志位weakly_referenced,是否有附加對象標志位has_assoc等信息。(rc:reference counter 引用計數)

//------- NSObject(實質) ------- 
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//------- objc_class(繼承)------- 
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable         以前的緩存指針和虛函數表
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags   class_rw_t *加上自定義rr/alloc標志
    //。。。
}

//------- isa指針(指向)------- 
struct objc_object {
private:
    isa_t isa;

public:
    Class ISA();        // ISA() assumes this is NOT a tagged pointer object    假設這不是一個標記的指針對象
    Class getIsa(); // getIsa() allows this to be a tagged pointer object   允許這是一個標記的指針對象
    //。。。
}

//------- 聯合體(定義)------- 
union isa_t 
{
    isa_t() { }                             //構造函數1
    isa_t(uintptr_t value) : bits(value) { }    //構造函數2
    Class cls;          //成員1(占據64位內存空間)
    uintptr_t bits;     //成員2(占據64位內存空間)

#if SUPPORT_PACKED_ISA
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {                //成員3(占據64位內存空間:從低位到高位依次是nonpointer到extra_rc。成員后面的:表明了該成員占用幾個bit。)
        uintptr_t nonpointer        : 1;  //(低位)注意:標志位,表明isa_t *是否是一個真正的指針?。?!
        uintptr_t has_assoc         : 1;  // 關聯對象
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;  //弱引用
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;  //引用計數 相關成員1
        uintptr_t extra_rc          : 19; //引用計數 相關成員2 (用19位來 記錄對象的引用次數)
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
}



引用于書籍《Objective-C高級編程》
GNUstep 是Cocoa 框架的互換框架。
CF:Core Foundation
CFRutime.c
蘋果的實現大概是采用散列表(引用計數表)來管理引用計數。

在runtime中,有四個數據結構非常重要,分別是SideTablesSideTable、weak_table_tweak_entry_t。它們和對象的引用計數,以及weak引用相關。
(相關記錄,我的電腦文件:101-Runtime基礎)

引用計數表的關系

其實在絕大多數情況下,僅用優化的isa_t 來記錄對象的引用計數就足夠了。只有在19位的extra_rc盛放不了那么大的引用計數時,才會借助SideTable出馬。
SideTable:是一個 全局的引用計數表,它記錄了所有對象的引用計數。

先說一下這四個數據結構的關系。 在runtime內存空間中,SideTables 是一個長度為8個元素 的hash數組,里面存儲了SideTableSideTables的hash鍵值就是一個對象obj的address。即:table1 = h(address1)
因此可以說,一個obj,對應了一個SideTable。但是一個SideTable,會對應多個obj。因為SideTable的數量只有64個,所以會有很多obj共用同一個SideTable。

SideTables

SideTable地址 關鍵字
table1 = h ( address1 ) address1
table2 = h ( address2 ) address1
table3 = h ( address3 ) address1
... ...

SideTable

obj對象地址 關鍵字
address1 = h ( key1 ) 、RefcountMap1、weak_table1 key1
address2 = h ( key2 ) 、RefcountMap2、weak_table2 key2
address3 = h ( key3 )、RefcountMap3、weak_table3 key3
... ...

而在一個SideTable中,又有3個成員,分別是:

  • spinlock_t slock: 自旋鎖,用于上鎖/解鎖 SideTable。
  • RefcountMap refcnts :以DisguisedPtr<objc_object>為key的hash表,用來存儲OC對象的引用計數(僅在未開啟isa優化 或 在isa優化情況下isa_t的引用計數溢出時才會用到)。
  • weak_table_t weak_table : 存儲對象弱引用指針的hash表。是OC weak功能實現的核心數據結構。

struct weak_table_t {
weak_entry_t *weak_entries; // 保存了所有指向指定對象的 weak 指針
size_t num_entries; // 存儲空間
uintptr_t mask; // 參與判斷引用計數輔助量
uintptr_t max_hash_displacement; // hash key 最大偏移值
};

其中,refcents是一個hash map,其key是obj的地址,而value,則是obj對象的引用計數。 即:referenceCount = h ( address )

weak_table則存儲了弱引用obj的指針的地址,其本質是一個以obj地址為key,弱引用obj的指針的地址作為value的hash表。hash表的節點類型是weak_entry_t。 即:objPointer = h ( address )

SideTables關系圖.png

二、Block

iOS全解1-2:Block 詳解

三、GCD與多線程

iOS全解1-3:鎖、GCD與多線程
iOS全解1-4:NSURLSession




參考文章,如有問題請聯系本人QQ1178690076
Object-C高級編程讀書筆記(1)——Block的基本概念
Object-C高級編程讀書筆記(2)——Block的實質
Object-C高級編程讀書筆記(3)——Block的變量截取
Object-C高級編程讀書筆記(4)——__block說明符
Object-C高級編程讀書筆記(5)——Block的對象類型截取


Objective-C runtime機制(1)——基本數據結構:objc_object & objc_class
Objective-C runtime機制(2)——消息機制
Objective-C runtime機制(3)——method swizzling
Objective-C runtime機制(4)——深入理解Category
Objective-C runtime機制(5)——iOS 內存管理
Objective-C runtime機制(6)——weak引用的底層實現原理

Objective-C runtime機制(7)——SideTables, SideTable, weak_table, weak_entry_t
Objective-C runtime機制(8)——OC對象從創建到銷毀
Objective-C runtime機制(9)——main函數前發生了什么
Objective-C runtime機制(10)——KVO的實現機制
Objective-C runtime機制(11)——結業考試

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容