YYCahe源碼分析---總結

花了幾天的時間看完了,ibireme大佬寫的YYCahe、以及其他大佬對YYCahe源碼分析,在此記錄一下,自己看完的收穫。

網上有很多大佬分析了YYCahe 的源碼,如何使用,每個類、函數之間的關係等,每個人的理解不太一樣,就看自己如何理解了,不再做過多的說明。

以下幾點,在很多源碼分析中,只是簡單的指出了一下,或者是未有說明,自己覺得需要去理解,掌握的東西,寫給自己看的 : )

  1. YYMemoryCache,中使用了LRU緩存算法,什麼是LRU?那麼LFU又是什麼?他們的區別是什麼?如何簡單的使用?什麼是緩存文件置換機制?

  2. YYMemoryCache,中使用了雙鏈表處理內存中的,刪除、插入等操作,什麼是雙鏈表?爲何不用單鏈表?

  3. 在移動端使用緩存,包括了內存緩存、磁盤緩存、它們之間的區別是什麼、各有什麼優缺點?

  4. YYMemoryCache中,使用互斥鎖來處理、查找、刪除等操作,什麼是互斥鎖?有什麼作用?dispatch_semaphore、pthread_mutex_lock等區別在哪裏?

  5. 其他的一些以前沒有接觸到的知識點,什麼是內聯函數?

結構圖:
在這裏插入圖片描述

一、YYMemoryCache,中使用了LRU緩存算法,什麼是LRU?那麼LFU又是什麼?他們的區別是什麼?如何簡單的使用?什麼是緩存文件置換機制
YYMemoryCache中,使用緩存淘汰算法(LRU)、緩存清理策略(分別是count(緩存數量),cost(開銷),age(距上一次的訪問時間))。

1.LRU
全稱是Least Recently Used,即最近最久未使用的意思.
如果一個數據在最近一段時間沒有被訪問到,那麼在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最久沒有被訪問到的數據淘汰.
如果使用數組來存儲數據,給每個數據項標記一個時間戳,每次插入、刪除時,需要把數組中存在的數據項的時間戳自增、或自減;插入新數據則需要把時間戳設置爲0.
當空間已滿,則將時間戳最大的數據項淘汰掉;缺點,需要不停的維護數據項的訪問時間戳。

更好的的辦法就是,利用鏈表移動訪問時間的數據順序和hashmap查詢是否是新數據項

1.1 算法流程
新數據插入到鏈表頭部;
每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
當鏈表滿的時候,將鏈表尾部的數據丟棄。
例如:
比如有數據 1,2,1,3,2
此時緩存中已有(1,2)
當3加入的時候,得把後面的2淘汰,變成(3,1)

2.LFU
LFU(Least Frequently Used)最近最少使用算法。它是基於“如果一個數據在最近一段時間內使用次數很少,那麼在將來一段時間內被使用的可能性也很小”的思路。

2.1
新加入數據插入到隊列尾部(因爲引用計數爲1);
隊列中的數據被訪問後,引用計數增加,隊列重新排序;
當需要淘汰數據時,將已經排序的列表最後的數據塊刪除。
例如
比如有數據 1,1,1,2,2,3
緩存中有(1(3次),2(2次))
當3加入的時候,得把後面的2淘汰,變成(1(3次),3(1次))
區別:LRU 是得把 1 淘汰。

3.LRU與LFU 對比:
名詞解釋: 緩存污染指操作系統將不常用的數據,從內存移到緩存,降低了緩存效率的現象

注意LFU和LRU算法的不同之處,LRU的淘汰規則是基於訪問時間,而LFU是基於訪問次數的

命中率
LFU: 一般情況下,LFU效率要優於LRU,且能夠避免週期性或者偶發性的操作導致緩存命中率下降的問題。但LFU需要記錄數據的歷史訪問記錄,一旦數據訪問模式改變,LFU需要更長時間來適用新的訪問模式,即:LFU存在歷史數據影響將來數據的“緩存污染”效用
LRU: 當存在熱點數據時,LRU的效率很好,但偶發性的、週期性的批量操作會導致LRU命中率急劇下降,緩存污染情況比較嚴重

複雜度:
LFU: 需要維護一個隊列記錄所有數據的訪問記錄,每個數據都需要維護引用計數
LRU:實現簡單

代價
LFU: 需要記錄所有數據的訪問記錄,內存消耗較高;需要基於引用計數排序,性能消耗較高
LRU:命中時需要遍歷鏈表,找到命中的數據塊索引,然後需要將數據移到頭部

4.什麼是緩存文件置換機制?
是計算機處理緩存存儲器的一種機制。
計算機存儲器空間的大小固定,無法容納服務器上所有的文件,所以當有新的文件要被置換入緩存時,必須根據一定的原則來取代掉適當的文件。此原則即所謂緩存文件置換機制

緩存文件置換方法有:
先進先出算法(FIFO): 最先進入的內容作爲替換對象
最近最少使用算法(LFU): 最近最少使用的內容作爲替換對象
最久未使用算法(LRU): 最久沒有訪問的內容作爲替換對象
非最近使用算法(NMRU):在最近沒有使用的內容中隨機選擇一個作爲替換對象

二、什麼是雙鏈表、單鏈表?

雙向鏈表也叫雙鏈表,是鏈表的一種,它的每個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點
刪除:在這裏插入圖片描述

單向鏈表(單鏈表)是鏈表的一種,其特點是鏈表的鏈接方向是單向的,對鏈表的訪問要通過順序讀取從頭部開始;鏈表是使用指針進行構造的列表;又稱爲結點列表,因爲鏈表是由一個個結點組裝起來的;其中每個結點都有指針成員變量指向列表中的下一個結點

在這裏插入圖片描述

列表是由結點構成,head指針指向第一個成爲表頭結點,而終止於最後一個指向nuLL的指針

  1. 雙向鏈表與單向鏈表相比有以下優勢:
    插入刪除不需要移動元素外,可以原地插入刪除
    可以雙向遍歷
    存儲效率
    存儲結構來看,每個雙鏈表的節點要比單鏈表的節點多一個指針,而長度爲n就需要 n*length(這個指針的length在32位系統中是4字節,在64位系統中是8個字節) 的空間,這在一些追求時間效率不高應用下並不適應,因爲它佔用空間大於單鏈表所佔用的空間;這時設計者就會採用以時間換空間的做法,這時一種工程總體上的衡量

  2. 鏈表跟數組的區別?
    數組靜態分配內存,鏈表動態分配內存;
    數組在內存中連續,鏈表不連續;
    數組利用下標定位,時間複雜度爲O(1),鏈表定位元素時間複雜度O(n);
    數組插入或刪除元素的時間複雜度O(n),鏈表的時間複雜度O(1)

優點:
數組:
隨機訪問性強(通過下標進行快速定位)
查找速度快

鏈表:
插入刪除速度快(因爲有next指針指向其下一個節點,通過改變指針的指向可以方便的增加刪除元素)
內存利用率高,不會浪費內存(可以使用內存中細小的不連續空間(大於node節點的大小),並且在需要空間的時候才創建空間)
大小沒有固定,拓展很靈活

缺點:
數組
插入和刪除效率低(插入和刪除需要移動數據)
可能浪費內存(因爲是連續的,所以每次申請數組之前必須規定數組的大小,如果大小不合理,則可能會浪費內存)
內存空間要求高,必須有足夠的連續內存空間。
數組大小固定,不能動態拓展

鏈表
不能隨機查找,必須從第一個開始遍歷,查找效率

三、 在移動端使用緩存,包括了內存緩存、磁盤緩存、它們之間的區別是什麼、各有什麼優缺點?

緩存的方式分爲兩種分別爲內存緩存磁盤緩存,內存緩存速度快容量小,磁盤緩存容量大速度慢可持久化。

沙盒緩存: 可以看做磁盤緩存的一部分,是一種安全體系。
每一個APP都有一個存儲空間,就是沙盒,APP之間不能相互通信。

iOS內存分爲5個區:棧區,堆區,全局區,常量區,代碼區

棧區:靜態內存在棧(stack)上分配;靜態內存分配在編譯時完成,不佔用CPU資源

堆區:動態內存在堆(heap)上分配;動態內存分配在運行時,分配與釋放都佔用CPU資源

全局區(靜態區)(static):全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束後由系統釋放。

常量區:常量字符串

代碼區:存儲代碼

沙盒緩存
Documents:用於存儲用戶數據,iTunes備份和恢復的時候會包括此目錄,所以,蘋果建議將程序中建立的或在程序中瀏覽到的文件數據保存在該目錄下。
tmp:存放臨時文件,這個可以放一些當APP退出後不再需要的文件,iTunes不會備份和恢復此目錄,此目錄下文件可能會在應用退出後刪除
Library/Caches:存放緩存文件,iTunes不會備份此目錄,此目錄下文件不會在應用退出刪除

四、YYMemoryCache中,使用互斥鎖來處理、查找、刪除等操作,什麼是互斥鎖?有什麼作用?dispatch_semaphore、pthread_mutex_lock等區別在哪裏?

  1. 什麼是鎖?

鎖是一種同步機制,用於在存在多線程的環境中實施對資源的訪問限制。你可以理解成它用於排除併發的一種策略!你可以理解爲爲了防止多線程訪問下資源的搶奪,保持線程同步的方式,以下就是常用的幾個鎖:

1.@synchronized 關鍵字加鎖
2. NSLock 對象鎖
3. NSCondition
4. NSConditionLock 條件鎖
5. NSRecursiveLock 遞歸鎖
6. pthread_mutex 互斥鎖(C語言)
7. dispatch_semaphore 信號量實現加鎖(GCD)
8. OSSpinLock 自旋鎖
9.pthread_rwlock 讀寫鎖
10.os_unfair_lock iOS10之後替代OSSPinLock的鎖,解決了優先級反轉的問題

簡單的說明:

POSIX 含義
POSIX線程(英語:POSIX Threads,常被縮寫爲Pthreads)是POSIX的線程標準,定義了創建和操縱線程的一套API。實現POSIX 線程標準的庫常被稱作Pthreads,一般用於Unix-like POSIX 系統,如Linux、Solaris。但是Microsoft Windows上的實現也存在,例如直接使用Windows API實現的第三方庫pthreads-w32;而利用Windows的SFU/SUA子系統,則可以使用微軟提供的一部分原生POSIX API

@synchronized
指令使用的obj爲該鎖的唯一標識,只有當標識相同時,才爲滿足互斥,如果線程2中的@synchronized(obj)改爲@synchronized(self),剛線程2就不會被阻塞,@synchronized指令實現鎖的優點就是我們不需要在代碼中顯式的創建鎖對象,便可以實現鎖的機制,但作爲一種預防措施,@synchronized塊會隱式的添加一個異常處理例程來保護代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。所以如果不想讓隱式的異常處理例程帶來額外的開銷,你可以考慮使用鎖對象

NSLock
在這裏插入圖片描述
遵循 NSLocking 協議,lock 方法是加鎖,unlock 是解鎖。

NSCondition
在這裏插入圖片描述
一個鎖和一個線程檢查器
條件對象充當給定線程中的鎖和檢查點。 鎖在測試條件時執行由條件觸發的任務,從而保護您的代碼。 檢查點行爲要求在線程繼續執行任務之前條件爲true。 雖然條件不正確,但線程會阻塞。 它一直被阻塞,直到另一個線程發出條件對象的信號

NSConditionLock
在這裏插入圖片描述
NSConditionLock 和 NSLock 類似,都遵循 NSLocking 協議,方法都類似,只是多了一個 condition 屬性,以及每個操作都多了一個關於 condition 屬性的方法,例如 tryLock,tryLockWhenCondition:,NSConditionLock 可以稱爲條件鎖,只有 condition 參數與初始化時候的 condition 相等,lock 才能正確進行加鎖操作。而 unlockWithCondition: 並不是當 Condition 符合條件時才解鎖,而是解鎖之後,修改 Condition 的值

NSRecursiveLock 遞歸鎖
在這裏插入圖片描述
NSRecursiveLock 可以在一個線程中重複加鎖(反正單線程內任務是按順序執行的,不會出現資源競爭問題),NSRecursiveLock 會記錄上鎖和解鎖的次數,當二者平衡的時候,纔會釋放鎖,其它線程纔可以上鎖成功

pthread_mutex
在這裏插入圖片描述
C 語言下多線程加互斥鎖的方式,實現和 NSRecursiveLock 類似的效果

dispatch_semaphore
dispatch_semaphore_create(long value);
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
GCD 用來同步的一種方式,與他相關的只有三個函數,一個是創建信號量,一個是等待信號,一個是發送信號

OSSpinLock
只有加鎖,解鎖,嘗試加鎖三個方法。和 NSLock 不同的是 NSLock 請求加鎖失敗的話,會先輪詢,但一秒過後便會使線程進入 waiting 狀態,等待喚醒。而 OSSpinLock 會一直輪詢,等待時會消耗大量 CPU 資源,不適用於較長時間的任務

os_unfair_lock
os_unfair_lock iOS 10.0新推出的鎖,用於解決OSSpinLock優先級反轉問題

  1. 自旋鎖(atomic 原子鎖)和互斥鎖 synchronize對比?

自旋鎖:
atomic:原子屬性,它默認會爲setter方法加鎖 (如果不寫,就會默認atomic),線程安全,但需要消耗大量的資源。
nonatomic:非原子屬性,不會爲setter方法加鎖,非線程安全。

如果共享數據已經有其他線程加鎖了,線程會以死循環的方式等待鎖,一旦被訪問的資源被解鎖,則等待資源的線程會立即執行

線程一直是running(加鎖——>解鎖),死循環(忙等 do-while)檢測鎖的標誌位

互斥鎖:
如果共享數據已經有其他線程加鎖了,線程會進入休眠狀態等待鎖。一旦被訪問的資源被解鎖,則等待資源的線程會被喚醒
線程會從sleep(加鎖)——>running(解鎖),過程中有上下文的切換(主動出讓時間片,線程休眠,等待下一次喚醒),cpu的搶佔,信號的發送等開銷。

  1. 關於YYMemoryCache中的,pthread_mutex_lock,與YYDiskCache中dispatch_semaphore區別。

YYMemoryCache中自旋鎖(OSSpinLock)作爲內存緩存的線程鎖,但是後來得知其不夠安全,所以退而求其次,使用了pthread_mutex。

在YYDiskCache在寫入比較大的緩存時,可能會有比較長的等待時間,而dispatch_semaphore在這個時候是不消耗CPU資源的,所以比較適合。

五、其他的一些以前沒有接觸到的知識點,什麼是內聯函數?

  1. 在YYMemoryCache中,使用了inline.內聯函數
    在這裏插入圖片描述

內聯函數的作用:
解決函數調用效率的問題
函數之間調用,是內存地址之間的調用,當函數調用完畢之後還會返回原來函數執行的地址。函數調用有時間開銷,內聯函數就是爲了解決這一問題。
不用inline修飾的函數, 彙編時會出現 call 指令.調用call指令就是就需要:
(1)將下一條指令的所在地址入棧
(2)並將子程序的起始地址送入PC(於是CPU的下一條指令就會轉去執行子程序)

  1. 在YYCahe,大量使用了CF類型的屬性,爲什麼?
    NS是objc的基礎庫
    CG,CF等是比較底層的C語言的庫

在OC中,主要有兩種對象:Objective-C 對象和 Core Foundation 對象。Core Foundation 對象主要是由C語言實現的 Core Foundation Framework 的對象,其中也有對象引用計數的概念,只不過不是 Cocoa Framework中Foundation Framework 的 retain/release,而是自身的 CFRetain/CFRelease 接口。

這兩種對象間可以互相轉換和操作。

例如:
CFDictionary 比 NSDictionary 效率更高.

  1. 宏定義.
    使用__has_include來檢查Frameworks是否引入某個類,無需在重複導入。
    #if __has_include(<sqlite3.h>)
    #import <sqlite3.h>
    #else
    #import “sqlite3.h”
    #endif

非常感謝各位大佬的博客,參考了文章部分中的內容:

ibireme大佬對YYCahe的設計思路:
https://blog.ibireme.com/2015/10/26/yycache/
愛奇藝大佬分析:
https://juejin.im/post/5a657a946fb9a01cb64ee761
https://blog.csdn.net/u012441289/article/category/7274351
其他資料:
關於iOS中,各種鎖的詳細說明:
https://www.jianshu.com/p/a236130bf7a2
https://www.jianshu.com/p/938d68ed832c
https://www.jianshu.com/p/ddbe44064ca4
內聯函數:
https://www.jianshu.com/p/d557b0831c6a

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章