iOS筆記。oc語法、runtime、runloop、多線程、內(nèi)存管理、性能優(yōu)化、設(shè)計(jì)模式、項(xiàng)目管理。

github鏈接


1. OC語法

iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?(KVO的本質(zhì)是什么?)

KVO利用runtime的API生成一個(gè)子類,當(dāng)instance對(duì)象修改屬性時(shí),會(huì)調(diào)用Fondation的_NSSetVauleAndNotfity函數(shù),
willchnagevauleforkey,
父類原來的setter、
didvaulechangeforkey,
內(nèi)部觸發(fā)監(jiān)聽器observe的監(jiān)聽方法(observerVauleForyKeyPath:ofObject:change:context)

簡述一下KVC?

key vaule coding,在iOS開發(fā)中允許直接用key名修改對(duì)象的屬性,或者給對(duì)象的屬性賦值,不需要調(diào)用明確存取方法。
這樣可以在運(yùn)行時(shí)動(dòng)態(tài)的訪問和修改對(duì)象的屬性,而不是在編譯時(shí)就確定,這也是iOS黑魔法之一,像JSON解析model和其它開發(fā)技巧都是通過kvc實(shí)現(xiàn)的。

KVC的賦值和取值過程是怎樣的?原理是什么?

  • setValue:forKey:
graph TB
    A(setValue:forKey:) --> B[按照setKey _setKey順序查找方法]
    B[按照setKey _setKey順序查找方法] --> C{找到了?}
    C{找到了?} -- 找到了 --> e(傳遞參數(shù)/調(diào)用方法直接賦值)
    C{找到了?} -- 沒有找到 --> F[查看accessInstanceVariablesDirectly方法的返回值] --> G{默認(rèn)YES}
    G{默認(rèn)YES} -- NO -->  H(調(diào)用setValue:forUndefinedKey:并拋出異常NSUnknownKeyException沒有找到成員變量)
    G{默認(rèn)YES} -- YES --> K[按照_key _isKey key isKey順序查找成員變量]
    K[按照_key _isKey key isKey順序查找成員變量] --> Z(直接賦值)
  • valueForKey
graph TB
    A(valueForKey:) --> B[按照getKey key isKey _key順序查找方法]
    B[按照getKey key isKey _key順序查找方法] --> C{找到了?}
    C{找到了?} -- 找到了 --> e(調(diào)用方法)
    C{找到了?} -- 沒有找到 --> F[查看accessInstanceVariablesDirectly方法的返回值] --> G{默認(rèn)YES}
    G{默認(rèn)YES} -- NO -->  H(調(diào)用valueForUndefinedKey:并拋出異常<br>NSUnknownKeyException<br>沒有找到成員變量)
    G{默認(rèn)YES} -- YES --> K[按照_key _isKey key isKey順序查找成員變量]
    K[按照_key _isKey key isKey順序查找成員變量] --> Z(直接取值)

Category的使用場合是什么?Category的實(shí)現(xiàn)原理?

  • Category編譯之后的底層結(jié)構(gòu)是struct category_t,里面存放的是分類的對(duì)象方法、類方法、屬性、協(xié)議信息。
  • 在程序運(yùn)行的時(shí)候,runtime會(huì)將分類的數(shù)據(jù)合并到類對(duì)象、元類對(duì)象中。

Category和Class Extension的區(qū)別是什么?

  • Class Extension在編譯的時(shí)候,它的數(shù)據(jù)就已經(jīng)包含在類信息中
  • Category是在運(yùn)行時(shí),才會(huì)將數(shù)據(jù)合并到類信息中

6.Category中有l(wèi)oad方法嗎?load方法是什么時(shí)候調(diào)用的?load 方法能繼承嗎?

  • 有l(wèi)oad方法
  • load方法在runtime加載類、分類的時(shí)候調(diào)用
  • load方法可以繼承,但是一般情況下不會(huì)主動(dòng)去調(diào)用load方法,都是讓系統(tǒng)自動(dòng)調(diào)用

Category能否添加成員變量?如果可以,如何給Category添加成員變量?

不能直接給Category添加成員變量,但是可以間接實(shí)現(xiàn)Category有成員變量的效果。

添加關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)

獲得關(guān)聯(lián)對(duì)象
id objc_getAssociatedObject(id object, const void * key)

移除所有的關(guān)聯(lián)對(duì)象
void objc_removeAssociatedObjects(id object)

load、initialize方法的區(qū)別什么?它們?cè)赾ategory中的調(diào)用的順序?以及出現(xiàn)繼承時(shí)他們之間的調(diào)用過程?

  • load:當(dāng)類被裝載的時(shí)候被調(diào)用,只調(diào)用一次。
  • load 調(diào)用方式并不是runtime的objc_messageSend方式調(diào)用,而是根據(jù)編譯順序,先編譯先調(diào)用。
  • 子類和父類同時(shí)實(shí)現(xiàn)load的方法時(shí),父類的方法先被調(diào)用。
  • 本類與category同時(shí)實(shí)現(xiàn)load,都會(huì)被調(diào)用。

例如:compile sources中的文件順序如下:SubB、SubA、A、B,load的調(diào)用順序是:B、SubB、A、SubA。

分析:SubB是排在compile sources中的第一個(gè),所以應(yīng)當(dāng)?shù)谝粋€(gè)被調(diào)用,但是SubB繼承自B,所以按照優(yōu)先調(diào)用父類的原則,B先被調(diào)用,然后是SubB,A、SubA。

第二種情況:compile sources中的文件順序如下:B、SubA、SubB、A,load調(diào)用順序是:B、A、SubA、SubB

  • initialize:當(dāng)類或子類第一次收到消息時(shí)被調(diào)用,有可能會(huì)調(diào)用多次(如果子類沒有實(shí)現(xiàn)就會(huì)調(diào)用父類的,所以父類的initialize可能被調(diào)用多次),也有可能永遠(yuǎn)不調(diào)用。
  • initialize是通過runtime的objc_msgSend的方式調(diào)用的。
  • 子類和父類同時(shí)實(shí)現(xiàn)initialize,父類的先被調(diào)用。
  • 本類與category同時(shí)實(shí)現(xiàn)initialize,category會(huì)覆蓋本類的方法,只調(diào)用category的。

講一下atomic的實(shí)現(xiàn)機(jī)制;為什么不能保證絕對(duì)的線程安全(最好可以結(jié)合場景來說)?

  • atomic的實(shí)現(xiàn)機(jī)制:

  • atomic是property的修飾詞之一,表示是原子性的,編譯器會(huì)自動(dòng)生成getter/setter方法,最終會(huì)調(diào)用objc_getProperty和objc_setProperty方法來進(jìn)行存取屬性。這兩個(gè)方法內(nèi)部使用os_unfair_lock(os_unfair_lock是在iOS10之后為了替代自旋鎖OSSpinLock而誕生的,主要是通過線程休眠的方式來繼續(xù)加鎖,而不是一個(gè)“忙等”的鎖)來進(jìn)行加鎖,來保證讀寫的原子性。鎖都在PropertyLocks中保存著,在用之前,會(huì)把鎖都初始化好,在需要用到時(shí),用對(duì)象的地址加上成員變量的偏移量為key,去PropertyLocks中去取。因此存取時(shí)用的是同一個(gè)鎖,所以atomic能保證屬性的存取時(shí)是線程安全的。注:由于鎖是有限的,不用對(duì)象,不同屬性的讀取用的也可能是同一個(gè)鎖.

  • atomic在getter/setter方法中加鎖,僅保證了存取時(shí)的線程安全,假設(shè)我們的屬性是@property(atomic)NSMutableArray *array;可變的容器時(shí),無法保證對(duì)容器的修改是線程安全的.

  • 在編譯器自動(dòng)生成的getter/setter方法,最終會(huì)調(diào)用objc_getProperty和objc_setProperty方法存取屬性,在此方法內(nèi)部保證了讀寫時(shí)的線程安全的,當(dāng)我們重寫getter/setter方法時(shí),就只能依靠自己在getter/setter中保證線程安全。

從property看安全隱患

@property(nonatomic,strong) NSString* userName;
@property(nonatomic,assign) int age;

// 對(duì)象類型,userName屬于TagPointer,在棧上,不存在線程安全問題
self.userName = @"xxx";

// 對(duì)象類型, userName屬于指針類型,執(zhí)行堆,多線程下可能有線程安全問題
self.userName = @"123234dfsdfasdfasdfad";

// 棧上,不存在線程安全問題
self.age = 19;
  • Tagged Pointer專門用來存儲(chǔ)小的對(duì)象,例如NSNumber, NSDate, NSString。
    Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已。所以,它的內(nèi)存并不存儲(chǔ)在堆中,也不需要malloc和free。
    在內(nèi)存讀取上有著3倍的效率,創(chuàng)建時(shí)比以前快106倍。
    當(dāng)字符串的長度為10個(gè)以內(nèi)時(shí),字符串的類型都是NSTaggedPointerString類型,當(dāng)超過10個(gè)時(shí),字符串的類型才是__NSCFString

  • property分類:
    · 值類型(棧上) int long bool
    · 對(duì)象類型: 1.TagPointer(棧上) 2.指針類型(堆上)

OC對(duì)象的分類

  • instance對(duì)象(實(shí)例對(duì)象)
    instance對(duì)象在內(nèi)存中存儲(chǔ)的信息包括:

    • isa指針
    • 其他成員變量
  • class對(duì)象(類對(duì)象)
    class對(duì)象在內(nèi)存中存儲(chǔ)的信息主要包括:

    • isa指針
    • superclass指針
    • 類的屬性信息(@property)、類的對(duì)象方法信息(instance method)
    • 類的協(xié)議信息(protocol)、類的成員變量信息(ivar)
    • ......
      class_rw_t里面的methods、properties、protocols是二維數(shù)組,是可讀可寫的,包含了類的初始內(nèi)容、分類的內(nèi)容
      class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一維數(shù)組,是只讀的,包含了類的初始內(nèi)容
  • meta-class對(duì)象(元類對(duì)象)
    每個(gè)類在內(nèi)存中有且只有一個(gè)meta-class對(duì)象,meta-class對(duì)象和class對(duì)象的內(nèi)存結(jié)構(gòu)是一樣的,但是用途不一樣,在內(nèi)存中存儲(chǔ)的信息主要包括:
    • isa指針
    • superclass指針
    • 類的類方法信息(class method)
    • ......

isa、superclass

  • instance的isa指向class

  • class的isa指向meta-class

  • meta-class的isa指向基類的meta-class

  • class的superclass指向父類的class ,如果沒有父類,superclass指針為nil

  • meta-class的superclass指向父類的meta-class,基類的meta-class的superclass指向基類的class

  • instance調(diào)用對(duì)象方法的軌跡:isa找到class,方法不存在,就通過superclass找父類

  • class調(diào)用類方法的軌跡:isa找meta-class,方法不存在,就通過superclass找父類

Block的本質(zhì)

  • block本質(zhì)上也是一個(gè)OC對(duì)象,它內(nèi)部也有個(gè)isa指針
  • block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對(duì)象
  • block的變量捕獲:為了保證block內(nèi)部能夠正常訪問外部的變量,block有個(gè)變量捕獲機(jī)制
變量類型 捕獲到block內(nèi)部 訪問方式
局部變量 auto ? 值傳遞
局部變量 static ? 指針傳遞
全局變量 ? 直接訪問
  • block的copy
    每一種類型的block調(diào)用copy后的結(jié)果如下所示:
block的類型 內(nèi)存區(qū)域 復(fù)制效果
NSGlobalBlock 程序的數(shù)據(jù)區(qū)域 .data區(qū) 什么也不做
NSStackBlock 從棧復(fù)制到堆
NSMallocBlock 引用計(jì)數(shù)增加

在ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的block復(fù)制到堆上,比如以下情況:
- block作為函數(shù)返回值時(shí)
- 將block賦值給__strong指針時(shí)
- block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)
- block作為GCD API的方法參數(shù)時(shí)

MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);

ARC下block屬性的建議寫法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

  • __block修飾符
    • __block可以用于解決block內(nèi)部無法修改auto變量值的問題
    • __block不能修飾全局變量、靜態(tài)變量(static)
    • 編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象
  • __block的內(nèi)存管理

    • 當(dāng)block在棧上時(shí):并不會(huì)對(duì)__block變量產(chǎn)生強(qiáng)引用

    • 當(dāng)block被copy到堆時(shí):會(huì)調(diào)用block內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會(huì)對(duì)__block變量形成強(qiáng)引用(retain)

    • 如果__block變量從堆上移除: 會(huì)調(diào)用__block變量內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放指向的對(duì)象(release)

  • __forwarding指針
    棧上的__forwarding指針指向自己本身的指針
    copy后,指向復(fù)制到堆上__block結(jié)構(gòu)體的指針
    然后堆上的變量的__forwarding再指向自己。

__forwarding指針這里的作用就是針對(duì)堆的Block,把原來__forwarding指針指向自己,換成指向_NSConcreteMallocBlock上復(fù)制之后的__block自己。然后堆上的變量的__forwarding再指向自己。這樣不管__block怎么復(fù)制到堆上,還是在棧上,都可以通過(i->__forwarding->i)來訪問到變量值。

__weak、__strong的實(shí)現(xiàn)原理

在ARC環(huán)境下,id類型和對(duì)象類型和C語言其他類型不同,類型前必須加上所有權(quán)的修飾符。
所有權(quán)修飾符總共有4種:
1.__strong修飾符
2.__weak修飾符
3.__unsafe_unretained修飾符
4.__autoreleasing修飾符

  • _strong的實(shí)現(xiàn)原理
    在ARC中原本對(duì)象生成之后是要注冊(cè)到autoreleasepool中,但是調(diào)用了objc_autoreleasedReturnValue 之后,緊接著調(diào)用了 objc_retainAutoreleasedReturnValue,objc_autoreleasedReturnValue函數(shù)會(huì)去檢查該函數(shù)方法或者函數(shù)調(diào)用方的執(zhí)行命令列表,如果里面有objc_retainAutoreleasedReturnValue()方法,那么該對(duì)象就直接返回給方法或者函數(shù)的調(diào)用方。達(dá)到了即使對(duì)象不注冊(cè)到autoreleasepool中,也可以返回拿到相應(yīng)的對(duì)象。

  • __weak的實(shí)現(xiàn)原理
    聲明一個(gè)__weak對(duì)象 id __weak obj = strongObj; 相應(yīng)的會(huì)調(diào)用:

id obj ;
objc_initWeak(&obj,strongObj);
objc_destoryWeak(&obj);

objc_initWeak的實(shí)現(xiàn)其實(shí)是這樣的: 會(huì)把傳入的object變成0或者nil,然后執(zhí)行objc_storeWeak函數(shù)。

id objc_initWeak(id *object, id value) {   
    *object = nil; 
    return objc_storeWeak(object, value);
}

objc_destoryWeak函數(shù)的實(shí)現(xiàn):也是會(huì)去調(diào)用objc_storeWeak函數(shù)。objc_initWeak和objc_destroyWeak函數(shù)都會(huì)去調(diào)用objc_storeWeak函數(shù),唯一不同的是調(diào)用的入?yún)⒉煌?,一個(gè)是value,一個(gè)是nil。

void objc_destroyWeak(id *object) { 
    objc_storeWeak(object, nil);
}

objc_storeWeak函數(shù)的用途就很明顯了。由于weak表也是用Hash table實(shí)現(xiàn)的,所以objc_storeWeak函數(shù)就把第一個(gè)入?yún)⒌淖兞康刂纷?cè)到weak表中,然后根據(jù)第二個(gè)入?yún)頉Q定是否移除。如果第二個(gè)參數(shù)為0,那么就把__weak變量從weak表中刪除記錄,并從引用計(jì)數(shù)表中刪除對(duì)應(yīng)的鍵值記錄。

所以如果__weak引用的原對(duì)象如果被釋放了,那么對(duì)應(yīng)的__weak對(duì)象就會(huì)被指為nil。原來就是通過objc_storeWeak函數(shù)這些函數(shù)來實(shí)現(xiàn)的。

為什么iOS的Masonry中的self不會(huì)循環(huán)引用?

關(guān)于 Masonry ,它內(nèi)部根本沒有捕獲變量 self,進(jìn)入block的是testButton,所以執(zhí)行完畢后,block會(huì)被銷毀,沒有形成環(huán)。所以,沒有引起循環(huán)依賴。


UIButton *testButton = [[UIButton alloc] init];
[self.view addSubview:testButton];
testButton.backgroundColor = [UIColor redColor];
[testButton mas_makeConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(@100);
    make.height.equalTo(@100);
    make.left.equalTo(self.view.mas_left);
    make.top.equalTo(self.view.mas_top);
}];
[testButton bk_addEventHandler:^(id sender) {
    [self dismissViewControllerAnimated:YES completion:nil];
} forControlEvents:UIControlEventTouchUpInside];

iOS 閉包中的[weak self]在什么情況下需要使用,什么情況下可以不加?

只有當(dāng)block直接或間接的被self持有時(shí),才需要weak self。

為什么 block 里面還需要寫一個(gè) strong self,如果不寫會(huì)怎么樣?

在 block 中先寫一個(gè) strong self,其實(shí)是為了避免在 block 的執(zhí)行過程中,突然出現(xiàn) self 被釋放的尷尬情況。
通常情況下,如果不這么做的話,還是很容易出現(xiàn)一些奇怪的邏輯,甚至閃退 野指針。

iOS block 為什么用copy修飾

首先, block是一個(gè)對(duì)象, 所以block理論上是可以retain/release的.
但是block在創(chuàng)建的時(shí)候它的內(nèi)存是默認(rèn)是分配在棧(stack)上, 而不是堆(heap)上的.
所以它的作用域僅限創(chuàng)建時(shí)候的當(dāng)前上下文(函數(shù), 方法...), 當(dāng)你在該作用域外調(diào)用該block時(shí), 程序就會(huì)崩潰.

  1. 一般情況下你不需要自行調(diào)用copy或者retain一個(gè)block. 只有當(dāng)你需要在block定義域以外的地方使用時(shí)才需要copy. Copy將block從內(nèi)存棧區(qū)移到堆區(qū).
  2. 其實(shí)block使用copy是MRC留下來的, 在MRC下, 如上述, 在方法中的block創(chuàng)建在棧區(qū), 使用copy就能把他放到堆區(qū), 這樣在作用域外調(diào)用該block程序就不會(huì)崩潰.
  3. 但在ARC下, 使用copy與strong其實(shí)都一樣, 因?yàn)閎lock的retain就是用copy來實(shí)現(xiàn)的。

2. Runtime

什么是Runtime?平時(shí)項(xiàng)目中有用過么?

OC是一門動(dòng)態(tài)性比較強(qiáng)的編程語言,允許很多操作推遲到程序運(yùn)行時(shí)再進(jìn)行
OC的動(dòng)態(tài)性就是由Runtime來支撐和實(shí)現(xiàn)的,Runtime是一套C語言的API,封裝了很多動(dòng)態(tài)性相關(guān)的函數(shù)
平時(shí)編寫的OC代碼,底層都是轉(zhuǎn)換成了Runtime API進(jìn)行調(diào)用

具體應(yīng)用:
利用關(guān)聯(lián)對(duì)象(AssociatedObject)給分類添加屬性
遍歷類的所有成員變量(修改textfield的占位文字顏色、字典轉(zhuǎn)模型、自動(dòng)歸檔解檔)
交換方法實(shí)現(xiàn)(交換系統(tǒng)的方法)
利用消息轉(zhuǎn)發(fā)機(jī)制解決方法找不到的異常問題

OC 的消息機(jī)制

OC中的方法調(diào)用其實(shí)都是轉(zhuǎn)成了objc_msgSend函數(shù)的調(diào)用,給receiver(方法調(diào)用者)發(fā)送了一條消息(selector方法名)
objc_msgSend底層有3大階段: 消息發(fā)送、動(dòng)態(tài)方法解析、消息轉(zhuǎn)發(fā)

  1. 消息發(fā)送
graph TB
    A(消息發(fā)送objc_msgSend) --> B[檢查receiver是否為nil]
    B[檢查receiver是否為nil] -- nil --> C(退出)
    B[檢查receiver是否為nil] -- 不為nil --> D[從reveiverClass的cache中查找方法]
    D[從reveiverClass的cache中查找方法] -- 找到了 -->  E(調(diào)用方法<br>結(jié)束查找)
    D[從reveiverClass的cache中查找方法] -- 沒有找到 --> F[從reveiverClass的class_rw_t中查找方法]
    F[從reveiverClass的class_rw_t中查找方法] -- 找到了 --> G(調(diào)用方法/結(jié)束查找/并將方法緩存到reveiverClass的cache中)
    F[從reveiverClass的class_rw_t中查找方法] -- 沒有找到找到了 --> H(從superClass的cache中查找方法)
    H(從superClass的cache中查找方法) -- 找到了 --> G[調(diào)用方法<br>結(jié)束查找<br>并將方法緩存到reveiverClass的cache中]
    H(從superClass的cache中查找方法) -- 沒有找到 --> J(從superClass的class_rw_t中查找方法)
    J(從superClass的class_rw_t中查找方法) -- 找到了 --> G[調(diào)用方法<br>結(jié)束查找<br>并將方法緩存到reveiverClass的cache中]
    J(從superClass的class_rw_t中查找方法) -- 沒有找到 --> K(上層是否還有superClass)
    K(上層是否還有superClass) -- 否 --> L(動(dòng)態(tài)方法解析)
  1. 動(dòng)態(tài)方法解析
graph TB
    A[動(dòng)態(tài)方法解析] --> B{是否曾經(jīng)有動(dòng)態(tài)解析}
    B{是否曾經(jīng)有動(dòng)態(tài)解析} -- 沒有 --> D[調(diào)用+resolveInstanceMethod:<br>或者<br>+resolveClassMethod:<br>方法來動(dòng)態(tài)解析方法] --> F[標(biāo)記為已經(jīng)動(dòng)態(tài)解析] --> G(消息發(fā)送)
    B{是否曾經(jīng)有動(dòng)態(tài)解析} -- 有 --> C(消息轉(zhuǎn)發(fā))
  1. 消息轉(zhuǎn)發(fā)
graph TB
    A[消息轉(zhuǎn)發(fā)] --> B[調(diào)用<br>forwardingTargetForSelector:<br>方法]
    B[調(diào)用<br>forwardingTargetForSelector:方法] -- 返回值不為nil --> C(objc_msgSend) 
    B[調(diào)用<br>forwardingTargetForSelector:方法] -- 返回值為nil --> D[調(diào)用<br>methodSignatureForSelector:方法] 
    D[調(diào)用<br>methodSignatureForSelector:方法] -- 返回值為nil --> F(調(diào)用doesNotRecognizeSelector:方法) 
    D[調(diào)用<br>methodSignatureForSelector:方法] -- 返回值不為nil --> H(調(diào)用forwardInvocation:方法<br>開發(fā)者可以在這個(gè)方法中自定義任何邏輯) 

3. RunLoop

什么是RunLoop?

RunLoop是一個(gè)事件循環(huán)機(jī)制,用于管理線程中的事件和消息。它允許線程在沒有任務(wù)的情況下休眠,并在有任務(wù)需要處理時(shí)喚醒線程。
RunLoop主要負(fù)責(zé)以下幾個(gè)方面:

  • 處理輸入源:RunLoop負(fù)責(zé)處理輸入源,包括用戶界面事件、觸摸事件、定時(shí)器事件、網(wǎng)絡(luò)事件等,通過RunLoop能夠有效地處理這些事件,讓應(yīng)用程序的響應(yīng)更加及時(shí)。

  • 保持線程活動(dòng):RunLoop能夠保持線程活動(dòng),即使在沒有任務(wù)時(shí),RunLoop也會(huì)讓線程休眠而不會(huì)退出,以便隨時(shí)處理來自輸入源的事件。

  • 定時(shí)器功能:RunLoop提供了一些定時(shí)器功能,例如延遲執(zhí)行和重復(fù)執(zhí)行某個(gè)任務(wù)。通過定時(shí)器功能,可以很方便地在指定時(shí)間執(zhí)行任務(wù)。

  • 優(yōu)化性能:RunLoop能夠優(yōu)化應(yīng)用程序的性能,通過RunLoop能夠讓應(yīng)用程序在有任務(wù)需要處理時(shí)及時(shí)喚醒線程,而在沒有任務(wù)時(shí)讓線程休眠,從而避免了線程的空轉(zhuǎn),減少了CPU的占用,提高了應(yīng)用程序的性能。

RunLoop與線程

  • 每條線程都有唯一的一個(gè)與之對(duì)應(yīng)的RunLoop對(duì)象
  • RunLoop保存在一個(gè)全局的Dictionary里,線程作為key,RunLoop作為value
  • 線程剛創(chuàng)建時(shí)并沒有RunLoop對(duì)象,RunLoop會(huì)在第一次獲取它時(shí)創(chuàng)建
  • RunLoop會(huì)在線程結(jié)束時(shí)銷毀
  • 主線程的RunLoop已經(jīng)自動(dòng)獲取(創(chuàng)建),子線程默認(rèn)沒有開啟RunLoop

RunLoop的運(yùn)行邏輯?

01、通知Observers:進(jìn)入Loop

02、通知Observers:即將處理Timers

03、通知Observers:即將處理Sources

04、處理Blocks

05、處理Source0(可能會(huì)再次處理Blocks)

06、如果存在Source1,就跳轉(zhuǎn)到第8步

07、通知Observers:開始休眠(等待消息喚醒)

08、通知Observers:結(jié)束休眠(被某個(gè)消息喚醒):1> 處理Timer,2> 處理GCD Async To Main Queue,3> 處理Source1

09、處理Blocks

10、根據(jù)前面的執(zhí)行結(jié)果,決定如何操作:01> 回到第02步, 02> 退出Loop

11、通知Observers:退出Loop

RunLoop在實(shí)際開中的應(yīng)用?

  • 控制線程生命周期(線程保活)
  • 解決NSTimer在滑動(dòng)時(shí)停止工作的問題
  • 監(jiān)控應(yīng)用卡頓
  • 性能優(yōu)化

4. 多線程

線程安全的本質(zhì)是什么?為什么會(huì)出現(xiàn)多線程不安全?

線程安全不是指線程的安全,而是指內(nèi)存的安全。每個(gè)進(jìn)程的內(nèi)存空間中都有一塊特殊的公共區(qū)域,通常稱為堆。當(dāng)多個(gè)線程同時(shí)訪問該區(qū)域,就會(huì)照成線程不安全的本質(zhì)原因。

針對(duì)一塊內(nèi)存區(qū)域,我們有讀和寫兩種操作,讀和寫同時(shí)發(fā)生在同一塊內(nèi)存區(qū)域時(shí),就有可能發(fā)生多線程不安全。

在 iOS 中,常用的多線程方案有幾種?

  1. NSThread:NSThread 是 Objective-C 對(duì) POSIX 線程(pthread)的封裝,可以直接創(chuàng)建一個(gè)線程,并進(jìn)行啟動(dòng)、停止等操作。但是,由于需要自己管理線程的生命周期,使用起來比較繁瑣。

  2. GCD(Grand Central Dispatch):GCD 是一個(gè)高效的多線程編程方案,通過隊(duì)列和任務(wù)的概念來實(shí)現(xiàn)線程的管理和調(diào)度,使用起來非常方便。

  3. NSOperationQueue:NSOperationQueue 是一個(gè)基于 GCD 的高級(jí)抽象,使用 NSOperation 和 NSOperationQueue 可以實(shí)現(xiàn)更加復(fù)雜的任務(wù)管理。

  4. pthread:pthread 是 POSIX 線程庫,使用起來比較底層,但是可以實(shí)現(xiàn)更加細(xì)粒度的線程控制。

  5. performSelectorInBackground:這是 NSObject 提供的一個(gè)方法,可以在后臺(tái)線程執(zhí)行一個(gè)方法,使用起來非常簡單,但是靈活性不如前面幾種方案。

iOS中的線程鎖有哪些?

自旋鎖、互斥鎖、遞歸鎖、條件鎖

  1. OSSpinLock:OSSpinLock 是一種自旋鎖,等待鎖的線程會(huì)處于忙等(busy-wait)狀態(tài),一直占用著CPU資源;目前已經(jīng)不再安全,可能會(huì)出現(xiàn)優(yōu)先級(jí)反轉(zhuǎn)問題,如果等待鎖的線程優(yōu)先級(jí)較高,它會(huì)一直占用著CPU資源,優(yōu)先級(jí)低的線程就無法釋放鎖。

  2. os_unfair_lock 是蘋果推出的一種線程鎖,也是一種自旋鎖,可以在鎖定期間不斷進(jìn)行忙等待,從而避免線程的切換。與 OSSpinLock 不同的是,os_unfair_lock 采用了更加公平的鎖定策略,避免了優(yōu)先級(jí)反轉(zhuǎn)等問題。在 iOS 10 和 macOS 10.12 中,蘋果已經(jīng)推薦使用 os_unfair_lock 代替 OSSpinLock,并且將其作為一種新的官方推薦的鎖機(jī)制。os_unfair_lock 的使用方式與其他鎖機(jī)制類似,通常需要先進(jìn)行初始化,然后使用 os_unfair_lock_lock、os_unfair_lock_unlock 等方法來進(jìn)行加鎖、解鎖等操作。與其他鎖機(jī)制不同的是,os_unfair_lock 不能被遞歸加鎖,即同一線程多次加鎖會(huì)導(dǎo)致死鎖。因此,在使用 os_unfair_lock 時(shí)需要注意避免這種情況的發(fā)生。

  3. pthread_mutex:”互斥鎖”,等待鎖的線程會(huì)處于休眠狀態(tài)。通過 pthread_mutex_lock 和 pthread_mutex_unlock 來實(shí)現(xiàn)加鎖和解鎖操作。pthread_mutex 還提供了 PTHREAD_MUTEX_RECURSIVE 和 PTHREAD_MUTEX_ERRORCHECK 等不同的類型,用于滿足不同的需求。

  4. @synchronized:@synchronized是對(duì)mutex遞歸鎖的封裝。

  5. NSLock:NSLock是對(duì)mutex普通鎖的封裝,可以通過 lock 和 unlock 方法來控制加鎖和解鎖。NSLock 還提供了 tryLock 和 lockBeforeDate 等方法,可以方便地進(jìn)行加鎖嘗試和超時(shí)控制。

  6. NSRecursiveLock:NSRecursiveLock也是對(duì)mutex遞歸鎖的封裝,API跟NSLock基本一致。不同之處在于 NSRecursiveLock 是一種遞歸鎖,可以被同一線程多次加鎖而不會(huì)造成死鎖。

  7. NSConditionLock:"條件鎖",是對(duì)NSCondition的進(jìn)一步封裝,可以設(shè)置具體的條件值。NSCondition是對(duì)mutex和cond的封裝。用于控制線程的執(zhí)行順序。NSCondition 提供了 wait、signal 和 broadcast 等方法,可以實(shí)現(xiàn)等待、喚醒和廣播等操作。

  8. dispatch_semaphore:dispatch_semaphore 是 GCD 中的一種信號(hào)量,可以通過信號(hào)量的值來控制線程的并發(fā)數(shù)量。當(dāng)信號(hào)量為 1 時(shí),可以實(shí)現(xiàn)互斥鎖的效果。

  9. dispatch_group:dispatch_group 是 GCD 中的一種機(jī)制,可以將一組任務(wù)進(jìn)行分組,以便于統(tǒng)一管理和控制。通過 dispatch_group_notify 方法,可以在任務(wù)組執(zhí)行完畢后執(zhí)行指定的任務(wù)。

iOS線程同步方案性能比較

性能從高到低排序:

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

自旋鎖、互斥鎖比較

在iOS中,自旋鎖(Spin Lock)和互斥鎖(Mutex Lock)是常用的兩種線程鎖。它們的主要區(qū)別在于:

  • 等待方式不同:自旋鎖在鎖定失敗時(shí)會(huì)一直進(jìn)行忙等待,即不斷檢查鎖是否可用,而不會(huì)放棄 CPU 時(shí)間片。而互斥鎖在鎖定失敗時(shí)會(huì)將線程掛起,放棄 CPU 時(shí)間片。

  • 鎖定時(shí)間不同:自旋鎖在鎖定時(shí)間短、競爭輕度的情況下表現(xiàn)更好,而互斥鎖在鎖定時(shí)間長、競爭激烈的情況下表現(xiàn)更好。

  • 死鎖風(fēng)險(xiǎn)不同:自旋鎖沒有鎖定超時(shí)時(shí)間,如果某個(gè)線程持有自旋鎖的時(shí)間過長,其他線程將一直進(jìn)行忙等待,可能導(dǎo)致死鎖。而互斥鎖可以通過設(shè)置鎖定超時(shí)時(shí)間來避免死鎖風(fēng)險(xiǎn)。

另外,自旋鎖和互斥鎖的實(shí)現(xiàn)方式也不同。自旋鎖在鎖定過程中不涉及系統(tǒng)調(diào)用,只需要進(jìn)行原子性操作,因此可以使用 CPU 提供的原子性操作指令實(shí)現(xiàn),比較輕量級(jí)。而互斥鎖需要涉及系統(tǒng)調(diào)用,需要進(jìn)行用戶態(tài)和內(nèi)核態(tài)之間的上下文切換,開銷相對(duì)較大。

綜上所述,自旋鎖適用于鎖定時(shí)間短、競爭輕度的場景,可以避免線程切換的開銷,提高效率。而互斥鎖適用于鎖定時(shí)間長、競爭激烈的場景,可以避免空轉(zhuǎn)的開銷,保證公平性。在實(shí)際使用中,應(yīng)根據(jù)具體場景選擇適合的鎖機(jī)制,以保證程序的正確性和穩(wěn)定性。

NSOperationQueue 和 GCD 的區(qū)別,以及各自的優(yōu)勢(shì)

NSOperationQueue 是基于 GCD 構(gòu)建的高層抽象,本質(zhì)上仍然使用了 GCD 的底層機(jī)制。二者的區(qū)別主要在以下幾個(gè)方面:

  1. 抽象程度:OperationQueue 提供了更高層次的抽象,使用 NSOperation 和 NSOperationQueue 可以將任務(wù)抽象為操作,并進(jìn)行依賴關(guān)系的設(shè)置。而 GCD 只能處理簡單的任務(wù)隊(duì)列,缺少更高層次的抽象。

  2. 線程調(diào)度:OperationQueue 可以將操作分發(fā)到不同的線程中執(zhí)行,而 GCD 只能將任務(wù)分配到全局的線程池中執(zhí)行,無法精確控制線程的數(shù)量。

  3. 優(yōu)先級(jí)設(shè)置:OperationQueue 可以為操作設(shè)置優(yōu)先級(jí),而 GCD 只能使用 dispatch_group_notify 和 dispatch_group_wait 等方法來實(shí)現(xiàn)類似的功能。

  4. 取消操作:OperationQueue 可以隨時(shí)取消某個(gè)操作的執(zhí)行,而 GCD 只能使用 dispatch_suspend 和 dispatch_resume 等方法來控制隊(duì)列的執(zhí)行狀態(tài)。

總的來說,OperationQueue 提供了更高層次的抽象和更精細(xì)的控制,可以實(shí)現(xiàn)更復(fù)雜的任務(wù)調(diào)度和管理。而 GCD 則更加簡單直觀,適合處理簡單的任務(wù)隊(duì)列。在實(shí)際開發(fā)中,應(yīng)根據(jù)具體的場景和需求選擇適合的方案。

如何設(shè)計(jì)一個(gè)線程安全的可變數(shù)組?

線程安全的設(shè)計(jì)原則:

  • 同一時(shí)間 只能有1個(gè)線程進(jìn)行寫的操作
  • 同一時(shí)間 允許多個(gè)線程讀的操作
  • 同一時(shí)間 不能同時(shí)有讀和寫的操作

在 iOS 中,實(shí)現(xiàn)線程安全的數(shù)組有以下幾種方法:

  1. 使用@synchronized關(guān)鍵字,這種方法簡單易懂,但是效率相對(duì)較低。
  2. 使用 GCD 的讀寫鎖,通過 dispatch_barrier_async 和 dispatch_sync 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
  3. 使用 pthread 的讀寫鎖,通過 pthread_rwlock_rdlock 和 pthread_rwlock_wrlock 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
  4. 使用信號(hào)量 dispatch_semaphore,通過dispatch_semaphore_wait 和 dispatch_semaphore_signal 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
  5. 使用 NSLock,通過lock 和 unlock 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
  6. 使用 NSRecursiveLock,這種鎖可以被同一線程多次獲取,因此可以用于遞歸調(diào)用。
  7. 使用 NSConditionLock,這種鎖可以通過設(shè)置不同的條件變量,控制線程的執(zhí)行。

按照性能從高到低排序,可以得到以下結(jié)果:

  1. pthread 的讀寫鎖,是性能最高的實(shí)現(xiàn)方式之一。
  2. GCD 的讀寫鎖,性能略低于 pthread 讀寫鎖。
  3. NSLock 和 NSRecursiveLock,性能比 GCD 稍低。
  4. 信號(hào)量 dispatch_semaphore,性能比 NSLock 和 NSRecursiveLock 稍低。
  5. @synchronized 關(guān)鍵字,性能相對(duì)較低,不建議在大規(guī)模數(shù)據(jù)操作時(shí)使用。

實(shí)現(xiàn)方案:

讀寫鎖pthread_rwlock
@interface ThreadSafeArray : NSObject

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, assign) pthread_rwlock_t rwlock;

@end

@implementation ThreadSafeArray

- (instancetype)init
{
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
        pthread_rwlock_init(&_rwlock, NULL);
    }
    return self;
}

- (void)addObject:(id)object
{
    pthread_rwlock_wrlock(&_rwlock);
    [self.array addObject:object];
    pthread_rwlock_unlock(&_rwlock);
}

- (void)removeObjectAtIndex:(NSUInteger)index
{
    pthread_rwlock_wrlock(&_rwlock);
    [self.array removeObjectAtIndex:index];
    pthread_rwlock_unlock(&_rwlock);
}

- (id)objectAtIndex:(NSUInteger)index
{
    id object;
    pthread_rwlock_rdlock(&_rwlock);
    object = self.array[index];
    pthread_rwlock_unlock(&_rwlock);
    return object;
}

- (void)dealloc
{
    pthread_rwlock_destroy(&_rwlock);
}

@end

在上面的代碼中,addObject: 和 removeObjectAtIndex: 方法都使用了 pthread_rwlock_wrlock 來保證在執(zhí)行這些操作時(shí),其他線程無法對(duì)數(shù)組進(jìn)行讀寫操作。而 objectAtIndex: 方法則使用了 pthread_rwlock_rdlock 來保證在獲取數(shù)組元素時(shí),其他線程可以進(jìn)行讀操作,但不能進(jìn)行寫操作。這樣可以避免讀寫沖突,保證線程安全。注意,需要在對(duì)象銷毀時(shí)調(diào)用 pthread_rwlock_destroy 來釋放讀寫鎖資源。

柵欄+并發(fā)隊(duì)列 (dispatch_barrier)
  • 傳入的并發(fā)隊(duì)列,必須是自己通過dispatch_queue_cretate創(chuàng)建的
  • 如果傳入的是串行隊(duì)列 或 全局并發(fā)隊(duì)列,那這個(gè)函數(shù)等同于dispatch_async/dispatch_sync的效果
  • 讀:可以使用dispatch_async和dispatch_sync,推薦dispatch_sync,dispatch_sync確保讀的時(shí)候,如果有寫的操作時(shí),當(dāng)前線程是阻塞的。
  • 寫:dispatch_barrier_asnyc確保2點(diǎn):一是執(zhí)行此任務(wù)之前的其它任務(wù)已經(jīng)完成,二是此任務(wù)完成之前隊(duì)列中添加的新任務(wù)不會(huì)被執(zhí)行,達(dá)到barrier的效果。
@interface ThreadSafeArray : NSObject

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) dispatch_queue_t queue;

@end

@implementation ThreadSafeArray

- (instancetype)init
{
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
        _queue = dispatch_queue_create("com.example.arrayQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)addObject:(id)object
{
    dispatch_barrier_async(self.queue, ^{
        [self.array addObject:object];
    });
}

- (void)removeObjectAtIndex:(NSUInteger)index
{
    dispatch_barrier_async(self.queue, ^{
        [self.array removeObjectAtIndex:index];
    });
}

- (id)objectAtIndex:(NSUInteger)index
{
    __block id object;
    dispatch_sync(self.queue, ^{
        object = self.array[index];
    });
    return object;
}

@end

在上面的代碼中,addObject: 和 removeObjectAtIndex: 方法都使用了 dispatch_barrier_async 來保證在執(zhí)行這些操作時(shí),其他線程無法對(duì)數(shù)組進(jìn)行讀寫操作。而 objectAtIndex: 方法則使用了 dispatch_sync 來保證在獲取數(shù)組元素時(shí),其他線程無法對(duì)數(shù)組進(jìn)行寫操作,但可以進(jìn)行讀操作。這樣可以避免讀寫沖突,保證線程安全。

dispatch_semaphore_t信號(hào)量
@interface ThreadSafeArray : NSObject

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;

@end

@implementation ThreadSafeArray

- (instancetype)init
{
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
        _semaphore = dispatch_semaphore_create(1);
    }
    return self;
}

- (void)addObject:(id)object
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    [self.array addObject:object];
    dispatch_semaphore_signal(self.semaphore);
}

- (void)removeObjectAtIndex:(NSUInteger)index
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    [self.array removeObjectAtIndex:index];
    dispatch_semaphore_signal(self.semaphore);
}

- (id)objectAtIndex:(NSUInteger)index
{
    id object;
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    object = self.array[index];
    dispatch_semaphore_signal(self.semaphore);
    return object;
}

@end

在上面的代碼中,addObject:、removeObjectAtIndex: 和 objectAtIndex: 方法都使用了 dispatch_semaphore_wait 和 dispatch_semaphore_signal 來保證在執(zhí)行這些操作時(shí),其他線程無法對(duì)數(shù)組進(jìn)行讀寫操作。使用信號(hào)量時(shí),每次讀寫操作都需要獲取信號(hào)量,以阻止其他線程進(jìn)行操作。在操作完成后,需要釋放信號(hào)量,允許其他線程進(jìn)行讀寫操作。這樣可以避免讀寫沖突,保證線程安全。

5. 內(nèi)存管理

使用CADisplayLink、NSTimer有什么注意點(diǎn)?

CADisplayLink、NSTimer會(huì)對(duì)target產(chǎn)生強(qiáng)引用,如果target又對(duì)它們產(chǎn)生強(qiáng)引用,那么就會(huì)引發(fā)循環(huán)引用。
解決方案:

  • 使用block
  • 使用代理對(duì)象(NSProxy)

NSTimer依賴于RunLoop,如果RunLoop的任務(wù)過于繁重,可能會(huì)導(dǎo)致NSTimer不準(zhǔn)時(shí),而GCD的定時(shí)器會(huì)更加準(zhǔn)時(shí)。

 // 獲取一個(gè)全局的線程來運(yùn)行計(jì)時(shí)器
 dispatch_queue_t queue = dispatch_get_global_queu(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 // 創(chuàng)建一個(gè)計(jì)時(shí)器
 dispatch_source_t timer = dispatch_source_creat(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
 // 設(shè)置計(jì)時(shí)器, 這里是每10毫秒執(zhí)行一次
 dispatch_source_set_timer(timer, dispatch_walltime(nil, 0),10*NSEC_PER_MSEC, 0);
 // 設(shè)置計(jì)時(shí)器的里操作事件
 dispatch_source_set_event_handler(timer, ^{
     //do you want....
 });
 // 啟動(dòng)定時(shí)器
 dispatch_resume(timer);

iOS程序的內(nèi)存布局

  • 代碼段:編譯之后的代碼
  • 數(shù)據(jù)段
    • 字符串常量:比如NSString *str = @"123"
    • 已初始化數(shù)據(jù):已初始化的全局變量、靜態(tài)變量等
    • 未初始化數(shù)據(jù):未初始化的全局變量、靜態(tài)變量等
  • 棧:函數(shù)調(diào)用開銷,比如局部變量。分配的內(nèi)存空間地址越來越小
  • 堆:通過alloc、malloc、calloc等動(dòng)態(tài)分配的空間,分配的內(nèi)存空間地址越來越大

Tagged Pointer

從64bit開始,iOS引入了Tagged Pointer技術(shù),用于優(yōu)化NSNumber、NSDate、NSString等小對(duì)象的存儲(chǔ)

在沒有使用Tagged Pointer之前, NSNumber等對(duì)象需要?jiǎng)討B(tài)分配內(nèi)存、維護(hù)引用計(jì)數(shù)等,NSNumber指針存儲(chǔ)的是堆中NSNumber對(duì)象的地址值

使用Tagged Pointer之后,NSNumber指針里面存儲(chǔ)的數(shù)據(jù)變成了:Tag + Data,也就是將數(shù)據(jù)直接存儲(chǔ)在了指針中

當(dāng)指針不夠存儲(chǔ)數(shù)據(jù)時(shí),才會(huì)使用動(dòng)態(tài)分配內(nèi)存的方式來存儲(chǔ)數(shù)據(jù)

objc_msgSend能識(shí)別Tagged Pointer,比如NSNumber的intValue方法,直接從指針提取數(shù)據(jù),節(jié)省了以前的調(diào)用開銷

如何判斷一個(gè)指針是否為Tagged Pointer?
iOS平臺(tái),最高有效位是1(第64bit)
Mac平臺(tái),最低有效位是1

當(dāng)字符串的長度為10個(gè)以內(nèi)時(shí),字符串的類型都是NSTaggedPointerString類型,當(dāng)超過10個(gè)時(shí),字符串的類型才是__NSCFString

// 對(duì)象類型,userName屬于TagPointer,在棧上,不存在線程安全問題
self.userName = @"xxx";

// 對(duì)象類型, userName屬于指針類型,執(zhí)行堆,多線程下可能有線程安全問題
self.userName = @"123234dfsdfasdfasdfad";

OC對(duì)象的內(nèi)存管理

在iOS中,使用引用計(jì)數(shù)來管理OC對(duì)象的內(nèi)存

一個(gè)新創(chuàng)建的OC對(duì)象引用計(jì)數(shù)默認(rèn)是1,當(dāng)引用計(jì)數(shù)減為0,OC對(duì)象就會(huì)銷毀,釋放其占用的內(nèi)存空間

調(diào)用retain會(huì)讓OC對(duì)象的引用計(jì)數(shù)+1,調(diào)用release會(huì)讓OC對(duì)象的引用計(jì)數(shù)-1

內(nèi)存管理的經(jīng)驗(yàn)總結(jié)

  • 當(dāng)調(diào)用alloc、new、copy、mutableCopy方法返回了一個(gè)對(duì)象,在不需要這個(gè)對(duì)象時(shí),要調(diào)用release或者autorelease來釋放它
  • 想擁有某個(gè)對(duì)象,就讓它的引用計(jì)數(shù)+1;不想再擁有某個(gè)對(duì)象,就讓它的引用計(jì)數(shù)-1
  • 可以通過以下私有函數(shù)來查看自動(dòng)釋放池的情況
    • extern void _objc_autoreleasePoolPrint(void);

引用計(jì)數(shù)的存儲(chǔ)

  • 在64bit中,引用計(jì)數(shù)可以直接存儲(chǔ)在優(yōu)化過的isa指針中,也可能存儲(chǔ)在SideTable類中
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}
  • refcnts是一個(gè)存放著對(duì)象引用計(jì)數(shù)的散列表

dealloc

當(dāng)一個(gè)對(duì)象要釋放時(shí),會(huì)自動(dòng)調(diào)用dealloc,接下的調(diào)用軌跡是

  • dealloc
  • _objc_rootDealloc
  • rootDealloc
  • object_dispose
  • objc_destructInstance、free

自動(dòng)釋放池

自動(dòng)釋放池是由 AutoreleasePoolPage 以雙向鏈表的方式實(shí)現(xiàn)的
當(dāng)對(duì)象調(diào)用 autorelease 方法時(shí),會(huì)將對(duì)象加入 AutoreleasePoolPage 的棧中
調(diào)用 AutoreleasePoolPage::pop 方法會(huì)向棧中的對(duì)象發(fā)送 release 消息。

自動(dòng)釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePool、AutoreleasePoolPage
調(diào)用了autorelease的對(duì)象最終都是通過AutoreleasePoolPage對(duì)象來管理的

  • AutoreleasePoolPage的結(jié)構(gòu)
    每個(gè)AutoreleasePoolPage對(duì)象占用4096字節(jié)內(nèi)存,除了用來存放它內(nèi)部的成員變量,剩下的空間用來存放autorelease對(duì)象的地址
    所有的AutoreleasePoolPage對(duì)象通過雙向鏈表的形式連接在一起

POOL_SENTINEL(哨兵對(duì)象)

POOL_SENTINEL 只是 nil 的別名。#define POOL_SENTINEL nil

在每個(gè)自動(dòng)釋放池初始化調(diào)用 objc_autoreleasePoolPush 的時(shí)候,都會(huì)把一個(gè) POOL_SENTINEL push 到自動(dòng)釋放池的棧頂,并且返回這個(gè) POOL_SENTINEL 哨兵對(duì)象。

objc_autoreleasePoolPush 方法
objc_autoreleasePoolPush

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

它調(diào)用 AutoreleasePoolPage 的類方法 push,也非常簡單:

static inline void *push() {
   return autoreleaseFast(POOL_SENTINEL);
}

在這里會(huì)進(jìn)入一個(gè)比較關(guān)鍵的方法 autoreleaseFast,并傳入哨兵對(duì)象 POOL_SENTINEL:

static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
       return page->add(obj);
   } else if (page) {
       return autoreleaseFullPage(obj, page);
   } else {
       return autoreleaseNoPage(obj);
   }
}
  • hotPage 并且當(dāng)前 page 不滿
    調(diào)用 page->add(obj) 方法將對(duì)象添加至 AutoreleasePoolPage 的棧中
  • 有 hotPage 并且當(dāng)前 page 已滿
    調(diào)用 autoreleaseFullPage 初始化一個(gè)新的頁
    調(diào)用 page->add(obj) 方法將對(duì)象添加至 AutoreleasePoolPage 的棧中
  • 無 hotPage
    調(diào)用 autoreleaseNoPage 創(chuàng)建一個(gè) hotPage
    調(diào)用 page->add(obj) 方法將對(duì)象添加至 AutoreleasePoolPage 的棧中
    最后的都會(huì)調(diào)用 page->add(obj) 將對(duì)象添加到自動(dòng)釋放池中。

hotPage 可以理解為當(dāng)前正在使用的 AutoreleasePoolPage。

objc_autoreleasePoolPop 方法

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

該靜態(tài)方法總共做了三件事情:

  1. 使用 pageForPointer 獲取當(dāng)前 token 所在的 AutoreleasePoolPage
  2. 調(diào)用 releaseUntil 方法釋放棧中的對(duì)象,直到 stop
  3. 調(diào)用 child 的 kill 方法

autorelease 方法
我們已經(jīng)對(duì)自動(dòng)釋放池生命周期有一個(gè)比較好的了解,最后需要了解的話題就是 autorelease 方法的實(shí)現(xiàn),先來看一下方法的調(diào)用棧:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

Runloop和Autorelease

iOS在主線程的Runloop中注冊(cè)了2個(gè)Observer:

  • 第1個(gè)Observer 監(jiān)聽了kCFRunLoopEntry事件,會(huì)調(diào)用objc_autoreleasePoolPush()
  • 第2個(gè)Observer
    • 監(jiān)聽了kCFRunLoopBeforeWaiting事件,會(huì)調(diào)用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
    • 監(jiān)聽了kCFRunLoopBeforeExit事件,會(huì)調(diào)用objc_autoreleasePoolPop()

6.性能優(yōu)化

卡頓優(yōu)化 - CPU

  • 盡量用輕量級(jí)的對(duì)象,比如用不到事件處理的地方,可以考慮使用CALayer取代UIView
  • 不要頻繁地調(diào)用UIView的相關(guān)屬性,比如frame、bounds、transform等屬性,盡量減少不必要的修改
  • 盡量提前計(jì)算好布局,在有需要時(shí)一次性調(diào)整對(duì)應(yīng)的屬性,不要多次修改屬性
  • 避免多次修改Autolayout,可以stack+顯示隱藏的方式布局UI
  • 圖片的size最好剛好跟UIImageView的size保持一致
  • 控制一下線程的最大并發(fā)數(shù)量
  • 盡量把耗時(shí)的操作放到子線程:文本處理(尺寸計(jì)算、繪制)、圖片處理(解碼、繪制)

卡頓優(yōu)化 - GPU

  • 盡量避免短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成一張進(jìn)行顯示
  • GPU能處理的最大紋理尺寸是4096x4096,一旦超過這個(gè)尺寸,就會(huì)占用CPU資源進(jìn)行處理,所以紋理盡量不要超過這個(gè)尺寸
  • 盡量減少視圖數(shù)量和層次
  • 減少透明的視圖(alpha<1),不透明的就設(shè)置opaque為YES
  • 盡量避免出現(xiàn)離屏渲染

離屏渲染

在OpenGL中,GPU有2種渲染方式
On-Screen Rendering:當(dāng)前屏幕渲染,在當(dāng)前用于顯示的屏幕緩沖區(qū)進(jìn)行渲染操作
Off-Screen Rendering:離屏渲染,在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作

離屏渲染消耗性能的原因
需要?jiǎng)?chuàng)建新的緩沖區(qū)
離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境,先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上,又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕

哪些操作會(huì)觸發(fā)離屏渲染?

  • 光柵化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圓角,同時(shí)設(shè)置layer.masksToBounds = YES、layer.cornerRadius大于0
    考慮通過CoreGraphics繪制裁剪圓角,或者叫美工提供圓角圖片
  • 陰影,layer.shadowXXX,如果設(shè)置了layer.shadowPath就不會(huì)產(chǎn)生離屏渲染

卡頓檢測(cè)

平時(shí)所說的“卡頓”主要是因?yàn)樵谥骶€程執(zhí)行了比較耗時(shí)的操作
可以添加Observer到主線程RunLoop中,通過監(jiān)聽RunLoop狀態(tài)切換的耗時(shí),以達(dá)到監(jiān)控卡頓的目的

耗電的主要來源

  • CPU處理,Processing
  • 網(wǎng)絡(luò),Networking
  • 定位,Location
  • 圖像,Graphics

耗電優(yōu)化

  • 盡可能降低CPU、GPU功耗
  • 少用定時(shí)器
  • 優(yōu)化I/O操作
    盡量不要頻繁寫入小數(shù)據(jù),最好批量一次性寫入
    讀寫大量重要數(shù)據(jù)時(shí),考慮用dispatch_io,其提供了基于GCD的異步操作文件I/O的API。用dispatch_io系統(tǒng)會(huì)優(yōu)化磁盤訪問
    數(shù)據(jù)量比較大的,建議使用數(shù)據(jù)庫(比如SQLite、CoreData)
  • 網(wǎng)絡(luò)優(yōu)化
    減少、壓縮網(wǎng)絡(luò)數(shù)據(jù)
    如果多次請(qǐng)求的結(jié)果是相同的,盡量使用緩存
    使用斷點(diǎn)續(xù)傳,否則網(wǎng)絡(luò)不穩(wěn)定時(shí)可能多次傳輸相同的內(nèi)容
    網(wǎng)絡(luò)不可用時(shí),不要嘗試執(zhí)行網(wǎng)絡(luò)請(qǐng)求
    讓用戶可以取消長時(shí)間運(yùn)行或者速度很慢的網(wǎng)絡(luò)操作,設(shè)置合適的超時(shí)時(shí)間
    批量傳輸,比如,下載視頻流時(shí),不要傳輸很小的數(shù)據(jù)包,直接下載整個(gè)文件或者一大塊一大塊地下載。如果下載廣告,一次性多下載一些,然后再慢慢展示。如果下載電子郵件,一次下載多封,不要一封一封地下載

APP的啟動(dòng)

  • APP的啟動(dòng)可以分為2種
    冷啟動(dòng)(Cold Launch):從零開始啟動(dòng)APP
    熱啟動(dòng)(Warm Launch):APP已經(jīng)在內(nèi)存中,在后臺(tái)存活著,再次點(diǎn)擊圖標(biāo)啟動(dòng)APP

  • APP啟動(dòng)時(shí)間的優(yōu)化,主要是針對(duì)冷啟動(dòng)進(jìn)行優(yōu)化
    APP的冷啟動(dòng)可以概括為3大階段

    • dyld
      dyld(dynamic link editor),Apple的動(dòng)態(tài)鏈接器,可以用來裝載Mach-O文件(可執(zhí)行文件、動(dòng)態(tài)庫等)
      啟動(dòng)APP時(shí),dyld所做的事情有
      裝載APP的可執(zhí)行文件,同時(shí)會(huì)遞歸加載所有依賴的動(dòng)態(tài)庫
      當(dāng)dyld把可執(zhí)行文件、動(dòng)態(tài)庫都裝載完畢后,會(huì)通知Runtime進(jìn)行下一步的處理

    • runtime
      啟動(dòng)APP時(shí),runtime所做的事情有
      調(diào)用map_images進(jìn)行可執(zhí)行文件內(nèi)容的解析和處理
      在load_images中調(diào)用call_load_methods,調(diào)用所有Class和Category的+load方法
      進(jìn)行各種objc結(jié)構(gòu)的初始化(注冊(cè)O(shè)bjc類 、初始化類對(duì)象等等)
      調(diào)用C++靜態(tài)初始化器和attribute((constructor))修飾的函數(shù)
      到此為止,可執(zhí)行文件和動(dòng)態(tài)庫中所有的符號(hào)(Class,Protocol,Selector,IMP,…)都已經(jīng)按格式成功加載到內(nèi)存中,被runtime 所管理

    • main
      APP的啟動(dòng)由dyld主導(dǎo),將可執(zhí)行文件加載到內(nèi)存,順便加載所有依賴的動(dòng)態(tài)庫
      并由runtime負(fù)責(zé)加載成objc定義的結(jié)構(gòu)
      所有初始化工作結(jié)束后,dyld就會(huì)調(diào)用main函數(shù)
      接下來就是UIApplicationMain函數(shù),AppDelegate的application:didFinishLaunchingWithOptions:方法

  • 通過添加環(huán)境變量可以打印出APP的啟動(dòng)時(shí)間分析(Edit scheme -> Run -> Arguments)
    DYLD_PRINT_STATISTICS設(shè)置為1
    如果需要更詳細(xì)的信息,那就將DYLD_PRINT_STATISTICS_DETAILS設(shè)置為1

APP的啟動(dòng)優(yōu)化

按照不同的階段

  • dyld
    減少動(dòng)態(tài)庫、合并一些動(dòng)態(tài)庫(定期清理不必要的動(dòng)態(tài)庫)
    減少Objc類、分類的數(shù)量、減少Selector數(shù)量(定期清理不必要的類、分類)
    減少C++虛函數(shù)數(shù)量
    Swift盡量使用struct

  • runtime
    用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++靜態(tài)構(gòu)造器、ObjC的+load

  • main
    在不影響用戶體驗(yàn)的前提下,盡可能將一些操作延遲,不要全部都放在finishLaunching方法中
    按需加載

安裝包瘦身

  • 資源(圖片、音頻、視頻等):

  • 可執(zhí)行文件瘦身

    • 編譯器優(yōu)化:
      • Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default設(shè)置為YES
      • 去掉異常支持,Enable C++ Exceptions、Enable Objective-C Exceptions設(shè)置為NO, Other C Flags添加-fno-exceptions
      • 利用AppCode或者fui檢測(cè)未使用的代碼

7. 設(shè)計(jì)模式、架構(gòu)

設(shè)計(jì)模式(Design Pattern)

是一套被反復(fù)使用、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)
使用設(shè)計(jì)模式的好處是:可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性
一般與編程語言無關(guān),是一套比較成熟的編程思想

設(shè)計(jì)模式可以分為三大類

  • 創(chuàng)建型模式:對(duì)象實(shí)例化的模式,用于解耦對(duì)象的實(shí)例化過程
    單例模式、工廠方法模式,等等

  • 結(jié)構(gòu)型模式:把類或?qū)ο蠼Y(jié)合在一起形成一個(gè)更大的結(jié)構(gòu)
    代理模式、適配器模式、組合模式、裝飾模式,等等

  • 行為型模式:類或?qū)ο笾g如何交互,及劃分責(zé)任和算法
    觀察者模式、命令模式、責(zé)任鏈模式,等等

項(xiàng)目管理

  1. 開發(fā)和構(gòu)建環(huán)境
    通過 Ruby ?具鏈為整個(gè)項(xiàng)?搭建?致的開發(fā)和構(gòu)建環(huán)境,我們通過 Xcode、rbenv、RubyGems 和 Bundler 搭建?個(gè)統(tǒng)?的 iOS 開發(fā)和構(gòu)建環(huán)境.
  • macOS
  • Xcode
  • rbenv
  • RubyGems
  • Bundler
  • CocoaPods
  • fastlane
  • git
  1. 項(xiàng)目配置
    xcconfig也叫作 Build configuration file(構(gòu)建配置?件),我們可以使?它來為 Project 或 Target 定義?組 Build Setting。由于它是?個(gè)純?本?件,我們可以使? Xcode 以外的其他?本編輯器來修改,?且可以保 存到 Git 進(jìn)?統(tǒng)?管理。通過 Build Configuration、 Xcode Scheme 以及 xcconfig 配置?件來統(tǒng)?項(xiàng)?的構(gòu)建配置,從?搭建出多個(gè)不同環(huán)境。

  2. 代碼管理

    • 遵循swift-style-guide代碼風(fēng)格。
    • 在提交代碼前,必須使用SwiftFormat對(duì)代碼進(jìn)行格式化。
    • SwiftLint
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容