【iOS】架構師之路~底層原理三 : (多線程、內存管理)

架構師之路~底層原理四:(性能優化、架構)ios

十四. 多線程

14.1 ios 多線程方案

pthread / NSThread /GCD /NSOperation
複製代碼

45.png

14.2GCD的經常使用函數

GCD中有2個用來執行任務的函數
用同步的方式執行任務
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
queue:隊列
block:任務

用異步的方式執行任務
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

GCD源碼:https://github.com/apple/swift-corelibs-libdispatch
複製代碼

14.3 GCD的隊列

GCD的隊列能夠分爲2大類型
併發隊列(Concurrent Dispatch Queue)
可讓多個任務併發(同時)執行(自動開啓多個線程同時執行任務)
併發功能只有在異步(dispatch_async)函數下才有效

串行隊列(Serial Dispatch Queue)
讓任務一個接着一個地執行(一個任務執行完畢後,再執行下一個任務)
複製代碼

14.4 容易混淆的術語

有4個術語比較容易混淆:同步、異步、併發、串行
同步和異步主要影響:能不能開啓新的線程
同步:在當前線程中執行任務,不具有開啓新線程的能力
異步:在新的線程中執行任務,具有開啓新線程的能力

併發和串行主要影響:任務的執行方式
併發:多個任務併發(同時)執行
串行:一個任務執行完畢後,再執行下一個任務

複製代碼

14.5 各類隊列的執行效果

46.png

14.6 GCD隊列組的使用

47.png

14.7 多線程的安全隱患

資源共享
1塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源
好比多個線程訪問同一個對象、同一個變量、同一個文件

當多個線程訪問同一塊資源時,很容易引起數據錯亂和數據安全問題
複製代碼

48.png

14.8 多線程安全隱患的解決方案

49.png

14.9 iOS中的線程同步方案 線程安全.線程鎖

解決方案: 使用線程同步技術(同步,就是協同步調,按預約的前後次序進行) 常見線程同步技術: 加鎖git

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

複製代碼

14.10 OSSpinLock (自旋鎖) 不安全

50.png

14.11 OSUnfairLock (互斥鎖) 運行效率最高

51.png

14.12 pthread_mutex

52.png

mutex 互斥鎖,等待鎖的線程會處於休眠狀態github

14.13 NSLock、NSRecursiveLock

53.png

14.14 NSCondition

54.png

14.15 dispatch_queue (SerialQueue)

使用GCD串行隊列,實現同步

複製代碼

55.png

14.16 dispatch_semaphore (信號量) 能夠用於控制最大併發數量

semaphore叫作」信號量」
信號量的初始值,能夠用來控制線程併發訪問的最大數量
信號量的初始值爲1,表明同時只容許1條線程訪問資源,保證線程同步

複製代碼

56.png

14.17 @synchronized (互斥鎖)

@synchronized是對mutex遞歸鎖的封裝
源碼查看:objc4中的objc-sync.mm文件
@synchronized(obj)內部會生成obj對應的遞歸鎖,而後進行加鎖、解鎖操做
複製代碼

14.18 iOS線程同步方案性能比較

57.png

os_unfair_lock  ios10 開始
OSSpanLock      ios10 廢棄
dispatch_semaphore  
dispatch_mutex
dispatch_queue   串行
NSLock  對 mutex 封裝
@synchronized 最差
複製代碼

14.19 自旋鎖、互斥鎖比較

什麼狀況使用自旋鎖比較划算?
預計線程等待鎖的時間很短
加鎖的代碼(臨界區)常常被調用,但競爭狀況不多發生
CPU資源不緊張
多核處理器

什麼狀況使用互斥鎖比較划算?
預計線程等待鎖的時間較長
單核處理器
臨界區有IO操做
臨界區代碼複雜或者循環量大
臨界區競爭很是激烈
複製代碼

14.20 Atomic 和 Noatomic

atomic用於保證屬性setter、getter的原子性操做,至關於在getter和setter內部加了線程同步的鎖
能夠參考源碼objc4的objc-accessors.mm
它並不能保證使用屬性的過程是線程安全的
複製代碼

14.21 多線程 讀寫線程安全方案

思考如何實現如下場景
同一時間,只能有1個線程進行寫的操做
同一時間,容許有多個線程進行讀的操做
同一時間,不容許既有寫的操做,又有讀的操做

上面的場景就是典型的「多讀單寫」,常常用於文件等數據的讀寫操做,iOS中的實現方案有
pthread_rwlock:讀寫鎖
dispatch_barrier_async:異步柵欄調用
複製代碼

14.22 pthread_rwlock

58.png

14.23 dispatch_barrier_async

59.png

十五. 內存管理

15.1 CADisplayLink、NSTimer使用注意

CADisplayLink 保證調用頻率和刷幀頻率一直,60FPS, 不用設置時間間隔,每秒鐘60次
    可使用 proxy 代理解決循環引用
    
    CADisplayLink、NSTimer會對target產生強引用,若是target又對它們產生強引用,那麼就會引起循環引用
複製代碼

解決方案1.使用block swift

60.png

解決方案2.使用代理對象(NSProxy) 安全

61.png

15.2 NSProxy 也屬於基類

代理,用於解決循環引用,,用於消息轉發,不會在父類查找方法
NSObject 和 NSProxy 區別
複製代碼

15.3 GCD定時器

NSTimer依賴於RunLoop,若是RunLoop的任務過於繁重,可能會致使NSTimer不許時
而GCD的定時器會更加準時,GCD定時器,不依賴 Runloop ,會很準時,依賴內核
複製代碼

62.png

15.4 iOS 程序的內存佈局

低地址-> 高地址
保留->代碼段->數據段(字符串常量,已初始化全局數據,未初始化數據)>堆->棧內存-> 內核區域
代碼段: 編譯以後的代碼
數據段: 字符串常量,已經初始化的全局變量,或者靜態變量,未初始化的全局變量,靜態變量
堆 (低>高)  經過 alloc malloc calloc 動態分配的內存

棧 (高地址 從 低地址)  函數調用開銷()
複製代碼

63.png

15.5 Tagged Pointer

從64bit開始,iOS引入了Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小對象的存儲

在沒有使用Tagged Pointer以前, NSNumber等對象須要動態分配內存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值

使用Tagged Pointer以後,NSNumber指針裏面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中

當指針不夠存儲數據時,纔會使用動態分配內存的方式來存儲數據

objc_msgSend能識別Tagged Pointer,好比NSNumber的intValue方法,直接從指針提取數據,節省了之前的調用開銷

如何判斷一個指針是否爲Tagged Pointer?
iOS平臺,最高有效位是1(第64bit)
Mac平臺,最低有效位是1
複製代碼

判斷是否爲Tagged Pointer性能優化

64.png

15.6 OC對象的內存管理

在iOS中,使用引用計數來管理OC對象的內存

一個新建立的OC對象引用計數默認是1,當引用計數減爲0,OC對象就會銷燬,釋放其佔用的內存空間

調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1

內存管理的經驗總結
當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不須要這個對象時,要調用release或者autorelease來釋放它
想擁有某個對象,就讓它的引用計數+1;不想再擁有某個對象,就讓它的引用計數-1

能夠經過如下私有函數來查看自動釋放池的狀況
extern void _objc_autoreleasePoolPrint(void);

複製代碼

15.7 copy和mutableCopy

65.png

15.8 引用計數器的存儲 retaincount

66.png

15.9 dealloc

67.png

15.10 autoreleasePool 自動釋放池

自動釋放池的主要底層數據結構是:__AtAutoreleasePool、AutoreleasePoolPage

調用了autorelease的對象最終都是經過AutoreleasePoolPage對象來管理的

源碼分析
-clang重寫@autoreleasepool
-objc4源碼:NSObject.mm
複製代碼

68.png

15.11 AutoreleasePoolPage的結構

69.png

調用push方法會將一個POOL_BOUNDARY入棧,而且返回其存放的內存地址

調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最後一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY

id *next指向了下一個能存放autorelease對象地址的區域
複製代碼

15.12 runloop 和 autoreleasePool

iOS在主線程的Runloop中註冊了2個Observer
-第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
-第2個Observer
    監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
    監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()
複製代碼

參考:iOS底層原理班(下)/OC對象/關聯對象/多線程/內存管理/性能優化bash

相關文章
相關標籤/搜索