雷達圖繪制引發內存的思考

近段一個新的需求是要做一個雷達圖用來展示數據,下圖為兩種方式繪制的雷達圖以及內存使用情況:

drawRect方式繪制的雷達圖.png
CAShapeLayer方式繪制的雷達圖.png

在調研過后 發現現在市面上有兩種做雷達圖的方式一種是通過 drawRect進行繪制 另一種則是通過 CAShapeLayer 去繪制,那么既然有兩種繪制方式的話, 那么就來對比一下這種兩種繪制方式在內存方面的的優與劣,如上圖,drawRect 方式繪制相同的雷達圖占用的內存比CAShapeLayer 整整大了8M。那么drawRect方法為什么消耗的內存比CAShapeLayer 大呢?

drawRect

如果想要了解drawRect,那我們就需要擼一擼在iOS程序上圖形顯示的原理了。在iOS系統中所有顯示的視圖都是從基類UIView繼承而來的,同時UIView負責接收用戶交互。但是實際上你所看到的視圖內容,包括圖形等,都是由UIView的一個實例圖層屬性來繪制和渲染的,那就是CALayer。

CALayer類的概念與UIView非常類似,它也具有樹形的層級關系,并且可以包含圖片文本、背景色等。它與UIView最大的不同在于它不能響應用戶交互,可以說它根本就不知道響應鏈的存在,它的API雖然提供了“某點是否在圖層范圍內的方法”,但是它并不具有響應的能力。

在每一個UIView實例當中,都有一個默認的支持圖層,UIView負責創建并且管理這個圖層。實際上這個CALayer圖層才是真正用來在屏幕上顯示的,UIView僅僅是對它的一層封裝,實現了CALayerdelegate,提供了處理事件交互的具體功能,還有動畫底層方法的高級API。可以說CALayerUIView的內部實現細節。

腦補了這么多,它與今天的主題drawRect有何關系呢?別著急,我們既然已經確定CALayer才是最終顯示到屏幕上的,只要順藤摸瓜,即可分析清楚。CALayer其實也只是iOS當中一個普通的類,它也并不能直接渲染到屏幕上,因為屏幕上你所看到的東西,其實都是一張張圖片。而為什么我們能看到CALayer的內容呢,是因為CALayer內部有一個contents屬性。contents默認可以傳一個id類型的對象,但是只有你傳CGImage的時候,它才能夠正常顯示在屏幕上。所以最終我們的圖形渲染落點落在contents身上。如圖:

image

contents也被稱為寄宿圖,除了給它賦值CGImage之外,我們也可以直接對它進行繪制,繪制的方法正是這次問題的關鍵,通過繼承UIView并實現 -drawRect:方法即可自定義繪制。但是呢drawRect:方法沒有默認的實現,因為對UIView來說,寄宿圖并不是必須的,UIView不關心繪制的內容。如果UIView檢測到-drawRect:方法被調用了,它就會為視圖分配一個寄宿圖,這個寄宿圖的像素尺寸等于視圖大小乘以contentsScale(這個屬性與屏幕分辨率有關,我們的雷達圖在不同模擬器下呈現的內存用量不同也是因為它)的值。

那么回到我們的雷達圖上,當雷達圖從屏幕上出現的時候,因為重寫了-drawRect:方法,-drawRect :就會自動調用。生成一張寄宿圖后,方法里面的代碼利用Core Graphics去繪制n條線條,然后內容就會緩存起來,等待下次你調用-setNeedsDisplay時就會在進行更新。

雷達視圖的-drawRect:方法的背后實際上都是底層的CALayer進行了重繪和保存中間產生的圖片,CALayerdelegate屬性默認實現了CALayerDelegate協議,當它需要內容信息的時候會調用協議中的方法來拿。

當雷達視圖刷新數據重繪時,因為它的支持圖層CALayer的代理就是雷達視圖本身,所以支持圖層會請求雷達視圖給它一個寄宿圖來顯示,它此刻會調用:

- (void)displayLayer:(CALayer *)layer;  

如果雷達視圖實現了這個方法,就可以拿到layer來直接設置contents寄宿圖,如果這個方法沒有實現,支持圖層CALayer會嘗試調用:

- (void)displayLayer:(CALayer *)layer;  

這個方法調用之前,CALayer創建了一個合適尺寸的空寄宿圖(尺寸由boundscontentsScale決定)和一個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,只用于粗略的對比。

參考鏈接:
iOS RadarChart
JYRadarChart
drawRect內存惡鬼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容