-
1. OC語法
- iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?(KVO的本質(zhì)是什么?)
- 簡述一下KVC?
- KVC的賦值和取值過程是怎樣的?原理是什么?
- Category的使用場合是什么?Category的實(shí)現(xiàn)原理?
- Category和Class Extension的區(qū)別是什么?
- Category能否添加成員變量?如果可以,如何給Category添加成員變量?
- load、initialize方法的區(qū)別什么?它們?cè)赾ategory中的調(diào)用的順序?以及出現(xiàn)繼承時(shí)他們之間的調(diào)用過程?
- 從property看安全隱患
- OC對(duì)象的分類
- isa、superclass
- Block的本質(zhì)
- __weak、__strong的實(shí)現(xiàn)原理
- 為什么iOS的Masonry中的self不會(huì)循環(huán)引用?
- iOS 閉包中的[weak self]在什么情況下需要使用,什么情況下可以不加?
- 為什么 block 里面還需要寫一個(gè) strong self,如果不寫會(huì)怎么樣?
- iOS block 為什么用copy修飾
- 2. Runtime
- 3. RunLoop
- 4. 多線程
- 5. 內(nèi)存管理
- 6.性能優(yōu)化
- 7. 設(shè)計(jì)模式、架構(gòu)
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í),字符串的類型才是__NSCFStringproperty分類:
· 值類型(棧上) 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ì)崩潰.
- 一般情況下你不需要自行調(diào)用copy或者retain一個(gè)block. 只有當(dāng)你需要在block定義域以外的地方使用時(shí)才需要copy. Copy將block從內(nèi)存棧區(qū)移到堆區(qū).
- 其實(shí)block使用copy是MRC留下來的, 在MRC下, 如上述, 在方法中的block創(chuàng)建在棧區(qū), 使用copy就能把他放到堆區(qū), 這樣在作用域外調(diào)用該block程序就不會(huì)崩潰.
- 但在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ā)
- 消息發(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)方法解析)
- 動(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ā))
- 消息轉(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 中,常用的多線程方案有幾種?
NSThread:NSThread 是 Objective-C 對(duì) POSIX 線程(pthread)的封裝,可以直接創(chuàng)建一個(gè)線程,并進(jìn)行啟動(dòng)、停止等操作。但是,由于需要自己管理線程的生命周期,使用起來比較繁瑣。
GCD(Grand Central Dispatch):GCD 是一個(gè)高效的多線程編程方案,通過隊(duì)列和任務(wù)的概念來實(shí)現(xiàn)線程的管理和調(diào)度,使用起來非常方便。
NSOperationQueue:NSOperationQueue 是一個(gè)基于 GCD 的高級(jí)抽象,使用 NSOperation 和 NSOperationQueue 可以實(shí)現(xiàn)更加復(fù)雜的任務(wù)管理。
pthread:pthread 是 POSIX 線程庫,使用起來比較底層,但是可以實(shí)現(xiàn)更加細(xì)粒度的線程控制。
performSelectorInBackground:這是 NSObject 提供的一個(gè)方法,可以在后臺(tái)線程執(zhí)行一個(gè)方法,使用起來非常簡單,但是靈活性不如前面幾種方案。
iOS中的線程鎖有哪些?
自旋鎖、互斥鎖、遞歸鎖、條件鎖
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í)低的線程就無法釋放鎖。
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ā)生。
pthread_mutex:”互斥鎖”,等待鎖的線程會(huì)處于休眠狀態(tài)。通過 pthread_mutex_lock 和 pthread_mutex_unlock 來實(shí)現(xiàn)加鎖和解鎖操作。pthread_mutex 還提供了 PTHREAD_MUTEX_RECURSIVE 和 PTHREAD_MUTEX_ERRORCHECK 等不同的類型,用于滿足不同的需求。
@synchronized:@synchronized是對(duì)mutex遞歸鎖的封裝。
NSLock:NSLock是對(duì)mutex普通鎖的封裝,可以通過 lock 和 unlock 方法來控制加鎖和解鎖。NSLock 還提供了 tryLock 和 lockBeforeDate 等方法,可以方便地進(jìn)行加鎖嘗試和超時(shí)控制。
NSRecursiveLock:NSRecursiveLock也是對(duì)mutex遞歸鎖的封裝,API跟NSLock基本一致。不同之處在于 NSRecursiveLock 是一種遞歸鎖,可以被同一線程多次加鎖而不會(huì)造成死鎖。
NSConditionLock:"條件鎖",是對(duì)NSCondition的進(jìn)一步封裝,可以設(shè)置具體的條件值。NSCondition是對(duì)mutex和cond的封裝。用于控制線程的執(zhí)行順序。NSCondition 提供了 wait、signal 和 broadcast 等方法,可以實(shí)現(xiàn)等待、喚醒和廣播等操作。
dispatch_semaphore:dispatch_semaphore 是 GCD 中的一種信號(hào)量,可以通過信號(hào)量的值來控制線程的并發(fā)數(shù)量。當(dāng)信號(hào)量為 1 時(shí),可以實(shí)現(xiàn)互斥鎖的效果。
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è)方面:
抽象程度:OperationQueue 提供了更高層次的抽象,使用 NSOperation 和 NSOperationQueue 可以將任務(wù)抽象為操作,并進(jìn)行依賴關(guān)系的設(shè)置。而 GCD 只能處理簡單的任務(wù)隊(duì)列,缺少更高層次的抽象。
線程調(diào)度:OperationQueue 可以將操作分發(fā)到不同的線程中執(zhí)行,而 GCD 只能將任務(wù)分配到全局的線程池中執(zhí)行,無法精確控制線程的數(shù)量。
優(yōu)先級(jí)設(shè)置:OperationQueue 可以為操作設(shè)置優(yōu)先級(jí),而 GCD 只能使用 dispatch_group_notify 和 dispatch_group_wait 等方法來實(shí)現(xiàn)類似的功能。
取消操作: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ù)組有以下幾種方法:
- 使用@synchronized關(guān)鍵字,這種方法簡單易懂,但是效率相對(duì)較低。
- 使用 GCD 的讀寫鎖,通過 dispatch_barrier_async 和 dispatch_sync 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
- 使用 pthread 的讀寫鎖,通過 pthread_rwlock_rdlock 和 pthread_rwlock_wrlock 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
- 使用信號(hào)量 dispatch_semaphore,通過dispatch_semaphore_wait 和 dispatch_semaphore_signal 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
- 使用 NSLock,通過lock 和 unlock 方法可以實(shí)現(xiàn)數(shù)組的讀寫安全。
- 使用 NSRecursiveLock,這種鎖可以被同一線程多次獲取,因此可以用于遞歸調(diào)用。
- 使用 NSConditionLock,這種鎖可以通過設(shè)置不同的條件變量,控制線程的執(zhí)行。
按照性能從高到低排序,可以得到以下結(jié)果:
- pthread 的讀寫鎖,是性能最高的實(shí)現(xiàn)方式之一。
- GCD 的讀寫鎖,性能略低于 pthread 讀寫鎖。
- NSLock 和 NSRecursiveLock,性能比 GCD 稍低。
- 信號(hào)量 dispatch_semaphore,性能比 NSLock 和 NSRecursiveLock 稍低。
- @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)方法總共做了三件事情:
- 使用 pageForPointer 獲取當(dāng)前 token 所在的 AutoreleasePoolPage
- 調(diào)用 releaseUntil 方法釋放棧中的對(duì)象,直到 stop
- 調(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盡量使用structruntime
用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++靜態(tài)構(gòu)造器、ObjC的+loadmain
在不影響用戶體驗(yàn)的前提下,盡可能將一些操作延遲,不要全部都放在finishLaunching方法中
按需加載
安裝包瘦身
-
資源(圖片、音頻、視頻等):
- 采取無損壓縮
- 去除沒有用到的資源: https://github.com/tinymind/LSUnusedResources
- 大資源圖片、表情、特效、音頻、視頻等資源通過下載后使用
- 使用Iconfont替換圖片圖標(biāo)
-
可執(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è)未使用的代碼
- 編譯器優(yōu)化:
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)目管理
- 開發(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
項(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)境。-
代碼管理
- 遵循swift-style-guide代碼風(fēng)格。
- 在提交代碼前,必須使用SwiftFormat對(duì)代碼進(jìn)行格式化。
- SwiftLint