近段一個新的需求是要做一個雷達圖用來展示數據,下圖為兩種方式繪制的雷達圖以及內存使用情況:
在調研過后 發現現在市面上有兩種做雷達圖的方式一種是通過 drawRect
進行繪制 另一種則是通過 CAShapeLayer
去繪制,那么既然有兩種繪制方式的話, 那么就來對比一下這種兩種繪制方式在內存方面的的優與劣,如上圖,drawRect 方式繪制相同的雷達圖占用的內存比CAShapeLayer 整整大了8M。那么drawRect方法為什么消耗的內存比CAShapeLayer 大呢?
drawRect
如果想要了解drawRect
,那我們就需要擼一擼在iOS程序上圖形顯示的原理了。在iOS系統中所有顯示的視圖都是從基類UIView繼承而來的,同時UIView負責接收用戶交互。但是實際上你所看到的視圖內容,包括圖形等,都是由UIView
的一個實例圖層屬性來繪制和渲染的,那就是CALayer
。
CALayer
類的概念與UIView
非常類似,它也具有樹形的層級關系,并且可以包含圖片文本、背景色等。它與UIView
最大的不同在于它不能響應用戶交互,可以說它根本就不知道響應鏈的存在,它的API雖然提供了“某點是否在圖層范圍內的方法”,但是它并不具有響應的能力。
在每一個UIView
實例當中,都有一個默認的支持圖層,UIView
負責創建并且管理這個圖層。實際上這個CALayer
圖層才是真正用來在屏幕上顯示的,UIView
僅僅是對它的一層封裝,實現了CALayer
的delegate
,提供了處理事件交互的具體功能,還有動畫底層方法的高級API。可以說CALayer
是UIView
的內部實現細節。
腦補了這么多,它與今天的主題drawRect
有何關系呢?別著急,我們既然已經確定CALayer
才是最終顯示到屏幕上的,只要順藤摸瓜,即可分析清楚。CALayer
其實也只是iOS當中一個普通的類,它也并不能直接渲染到屏幕上,因為屏幕上你所看到的東西,其實都是一張張圖片。而為什么我們能看到CALayer
的內容呢,是因為CALayer
內部有一個contents
屬性。contents
默認可以傳一個id類型的對象,但是只有你傳CGImage
的時候,它才能夠正常顯示在屏幕上。所以最終我們的圖形渲染落點落在contents
身上。如圖:
contents
也被稱為寄宿圖,除了給它賦值CGImage
之外,我們也可以直接對它進行繪制,繪制的方法正是這次問題的關鍵,通過繼承UIView
并實現 -drawRect:
方法即可自定義繪制。但是呢drawRect:
方法沒有默認的實現,因為對UIView
來說,寄宿圖并不是必須的,UIView不關心繪制的內容。如果UIView檢測到-drawRect:
方法被調用了,它就會為視圖分配一個寄宿圖,這個寄宿圖的像素尺寸等于視圖大小乘以contentsScale
(這個屬性與屏幕分辨率有關,我們的雷達圖在不同模擬器下呈現的內存用量不同也是因為它)的值。
那么回到我們的雷達圖上,當雷達圖從屏幕上出現的時候,因為重寫了-drawRect:
方法,-drawRect :
就會自動調用。生成一張寄宿圖后,方法里面的代碼利用Core Graphics
去繪制n條線條,然后內容就會緩存起來,等待下次你調用-setNeedsDisplay
時就會在進行更新。
雷達視圖的-drawRect:
方法的背后實際上都是底層的CALayer
進行了重繪和保存中間產生的圖片,CALayer
的delegate
屬性默認實現了CALayerDelegate
協議,當它需要內容信息的時候會調用協議中的方法來拿。
當雷達視圖刷新數據重繪時,因為它的支持圖層CALayer
的代理就是雷達視圖本身,所以支持圖層會請求雷達視圖給它一個寄宿圖來顯示,它此刻會調用:
- (void)displayLayer:(CALayer *)layer;
如果雷達視圖實現了這個方法,就可以拿到layer
來直接設置contents
寄宿圖,如果這個方法沒有實現,支持圖層CALayer
會嘗試調用:
- (void)displayLayer:(CALayer *)layer;
這個方法調用之前,CALayer
創建了一個合適尺寸的空寄宿圖(尺寸由bounds
和contentsScale
決定)和一個Core Graphics
的繪制上下文環境,為繪制寄宿圖做準備,它作為ctx
參數傳入。在這一步生成的空寄宿圖內存是相當巨大的,它就是本次內存問題的關鍵,一旦你實現了CALayerDelegate
協議中的-drawLayer:inContext:
方法或者UIView
中的-drawRect:
方法(其實就是前者的包裝方法),圖層就創建了一個繪制上下文,這個上下文需要的內存可從這個公式得出:圖層寬* 圖層高 * 4字節,寬高的單位均為像素。 而既然我們的雷達圖是封裝的庫 就應該適應各個尺寸下內存的要求,比如我們當前的雷達圖尺寸下需要開辟的內存空間大小為:
radarV = [[JYRadarChart alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, 400)];
那么我們在8P機器 上下文的內存量則是: 1242 * 1200 * 4字節 其實算下來大概有5兆多。在其內部又有通過drawRect
方法去實現繪制文字 title
則加起來有 8M多
CAShapeLayer
CAShapeLayer
是一個通過矢量圖形而不是bitmap
來繪制的圖層子類。用CGPath
來定義想要繪制的圖形,CAShapeLayer
會自動渲染。它可以完美替代我們的直接使用Core Graphics
繪制layer
,對比之下使用CAShapeLayer
有以下優點
- 渲染快速。
CAShapeLayer
使用了硬件加速,繪制同一圖形會比用Core Graphics
快很多。 - 高效使用內存。一個
CAShapeLayer
不需要像普通CALayer
一樣創建一個寄宿圖形,所以無論有多大,都不會占用太多的內存。 - 不會被圖層邊界剪裁掉。一個
CAShapeLayer
可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics
的普通CALayer
一樣被剪裁掉 - 不會出現像素化。當你給
CAShapeLayer
做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化
通過兩個demo的對比下 在當前尺寸下確實內存上會相差8M左右 ,如果其他設計要求雷達圖尺寸大一點的話 那么就會產生更大的內存消耗。
在此總結一下繪制性能優化原則:
- 1.繪制圖形性能的優化最好的辦法就是不去繪制。
- 2.利用專有圖層代替繪圖需求。
- 3.不得不用到繪圖盡量縮小視圖面積,并且盡量降低重繪頻率。
- 4.異步繪制,推測內容,提前在其他線程繪制圖片,在主線程中直接設置圖片。
當然了,這兩種雷達圖的實現方式各有優缺點,也很感謝雷達圖作者開源。
本人修改的兩種實現方式的雷達圖地址Demo,只用于粗略的對比。