Matrix Code Rain及對Core Graphics繪制的優化

vbha4425 8年前發布 | 8K 次閱讀 矩陣 iOS開發 移動開發

前情提要

在9月25號看到Kevin Chou的 這篇 介紹他開源的組件庫 PNChart 受到歡迎的文章時,我突然想到:對啊,這是個把自己喜好與技術積累結合起來的好途徑!之前總覺得往開源社區貢獻代碼需要超強的底層代碼功力,又不想仿寫已有的組件重復造輪子,這時我才剛剛意識到,上層的UI層面同樣需要優秀的貢獻——某種程度上講更加稀缺,畢竟同時對設計和代碼都有研究的程序員比較少。

恰好,一個做前端的朋友Fanta發來了一份他業余時間用HTML+JS寫著玩做的《黑客帝國》代碼雨效果的demo:

我覺得這個還挺有意思,搜了一下GitHub上還沒有做過的,于是便開始了編碼工作。

架構及軌跡生成

這是一個很簡單的小組件,所以基本架構也很簡單:

我們約定將每一條下落的軌跡都稱為一個 Track ,由一個 Generator 實例專門來生成,每隔指定的時間(顯然,隨機亦可)就新生成一條,加到 DataSource 中,并創建其對應的 CALayer 子類 CodeRainLayer 加到最底層的 UIView 上。

下落及軌跡清理

如何產生動畫呢?最開始自然想到用 CAAnimation 來做。

因為代碼太簡單,就不在這里寫了。

但是寫完個大概之后,運行起來卻發現不對勁:總感覺沒有電影里面酷。

問題出在哪里呢?我又從移動硬盤里翻出了那三部曲仔細地研究了一下,經過一幀一幀地探究,我找到了原因:

電影里面的代碼并不是在“下落”,如果你盯著一個字母看,會發現它根本就沒移動過位置(除去鏡頭本身的移動)。換句話說,整個空間是一個已經排列好的字母矩陣,而我們看到的表象是 一陣脈沖流過 而已。

所以最后改成的方案是由每個 Track 實例自帶的 Timer 負責驅動控制自身的下落(為表述方便我們依然沿用這個詞),當需要刷新時,通知其對應的 CodeRainLayer 實例( -setNeedsDisplay )進行重繪。至于如何重繪,由每個 CodeRainLayer 自行負責。

而當整條軌跡掉出屏幕的時候, Track 會檢測出邊界條件,然后把對應的 CALayer 執行 removeFromSuperlayer ,最后把自身從 DataSource 中清除。

階段性成果

OK, so far so good. 我們成功實現了整個的動畫效果,看起來也確實蠻酷的:

封裝

在把它傳到GitHub之前,還需要進行一些封裝。這里主要有兩方面的工作,一個是增加控制關鍵字來限制外界能接觸到的內部類和方法,另一個是將可調節的參數向外界暴露出來。

Access Control

在Swift 3中特地新加了 fileprivate 這個訪問權限,正好在這里可以用到。我們把不希望暴露給外界的類都加上這個限定關鍵字。

順便,Swift 3中的訪問權限依次是:

open,public,internal,fileprivate,private.

Configurable Parameters

在之前,組件中用到的所有參數都定義在了一個 struct 里:

fileprivate struct JSMatrixConstants {
    static let maxGlowLength: Int = 3 // Characters
    static let minTrackLength: Int = 8 // Characters
    static let maxTrackLength: Int = 40 // Characters
    static let charactersSpacing: CGFloat = 0.0 // pixel
    static let characterChangeRate = 0.9
    static let firstDropShowTime = 2.0 // Time between the First drop and the later

// Configurable
static let speed: TimeInterval = 0.15 // Seconds that new character pop up
static let newTrackComingLap: TimeInterval = 0.4
static let tracksSpacing: Int = 5

}</code></pre>

為了暴露其中的一些參數,我們在 CodeRainView 那里增加幾個變量:

var trackSpacing: Int
var newTrackComingLap: CGFloat
var speed: CGFloat

那么如果用戶不設置的時候呢?我們應該用回默認值。比如這樣:

var speed: CGFloat = CGFloat(JSMatrixConstants.speed){
    didSet{
        datasource.speed = TimeInterval(speed)
    }
}
var newTrackComingLap: CGFloat = CGFloat(JSMatrixConstants.newTrackComingLap){
    didSet{
        datasource.newTrackComingLap = TimeInterval(newTrackComingLap)
    }
}
var trackSpacing: Int = JSMatrixConstants.tracksSpacing{
    didSet{
        datasource.trackSpacing = trackSpacing
    }
}

而一個2016年的UI組件應當是Interface Builder-Friendly的——尤其是,要做到這點只需舉手之勞:將上面的參數聲明為 @IBInspectable 。

最后在IB中看到的效果是:

優化性能

在我的iPhone6s上測試時,整個組件的表現沒什么大問題;但在比較老的iPhone5s上測試時,就有點吃力了。雖然畫面依然比較流暢,在CPU監測中能明顯看出占用:

而在我后面想結合一些 CoreMotion 的回調實現 視角縮放 效果時,在5s上的畫面終于卡了起來。

之所以會卡很容易理解,整個組件在主線程中進行了大量的繪制工作,擱你你也卡。

繪制UIView最快的方法就是把它當成imageview,我們把需要用Core Graphic繪制的代碼放到另一個線程中去繪制,生成image后直接賦值給view,達到異步繪制的目的。

我試了一下,差不多是這樣:

let track = self.track
DispatchQueue.global().async {
    let size = self.bounds.size
    UIGraphicsBeginImageContext(size)
    context.saveGState()

... // Calculate positions, etc.

context.restoreGState()
self.render(in: context)
let resultImage = UIGraphicsGetImageFromCurrentImageContext();
DispatchQueue.main.async {
    if let image = resultImage{
        self.contents = image.cgImage
    }
}
UIGraphicsEndImageContext()

}</code></pre>

但這樣做有問題:在每一次更新的時候,這個Layer需要在空白的背景下進行繪制,而直接調用 self.render(in: context) 方法,繪制的內容會疊加在當前顯示的內容之上,出來的效果是不可用的。(截圖過于殘暴,從略)

那么怎么解決這個問題呢?一個直接的想法是,如果能在一個新的context上繪制就好了。

帶著這個目標去搜索,在 這個文章 里面介紹了創建context的方法,于是上面的代碼變成了:

let track = self.track
DispatchQueue.global().async {
    let size = self.bounds.size
    UIGraphicsBeginImageContext(size)

/* Create drawing context */
let colorSpace = CGColorSpaceCreateDeviceRGB()
let createdContext = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)

if let context = createdContext{
    context.saveGState()

    ... // calc positions, etc.

    context.restoreGState()
    self.render(in: context)
    let resultImage = UIGraphicsGetImageFromCurrentImageContext();
    DispatchQueue.main.async {
        if let image = resultImage{
            self.contents = image.cgImage
        }
    }
}
UIGraphicsEndImageContext()

}</code></pre>

優化結果

搞定了這些之后興沖沖地在5s上跑了一下,發現除了線程多了一些之外,差別幾乎不可見:

細想一下也可以理解,我們并沒有減少任何繪制的工作量,只不過是把它們移到了后臺線程而已。

那么接下來的問題是,在為主線程減了這么多負之后,程序的響應性能有提高嗎?因為要是再沒什么變化的話,我要為前面這些花出的時間哭幾秒。

接下來我搜到了一篇講述如何測量程序響應性的 文章 ,還附了源碼的截圖,非常良心。

fileprivate class PingThread: Thread{
    var pingTaskIsRunning = false
    var semaphore = DispatchSemaphore(value: 0)
    override func main(){
        while !self.isCancelled{
            pingTaskIsRunning = true
            DispatchQueue.main.async {
                self.pingTaskIsRunning = false
                self.semaphore.signal()
            }
            Thread.sleep(forTimeInterval: 1/30.0)
            if pingTaskIsRunning {
                NSLog("Delayed!")
            }
            _ = semaphore.wait(timeout: DispatchTime.distantFuture)
        }
    }
}

核心思想是,每隔一定的時間就在主線程給該線程的信號量發消息,要是主線程因為卡頓耽擱了,該線程就會輸出警告信息。

我把時間設為1/30秒,因為這是一個流暢的動畫所應當達到的幀率。

這下終于有了喜人的對比結果:

之前:

之后:

直到啟動20多秒后收到內存警告,都沒有一次卡頓出現!

雖然我不是一個使用meme表情控,但看國外的blog看多了之后,總覺得在這種情況下需要出現一個表情……

就是下面這個:

最后的話

通過這個項目,我學到的東西包括:

  • Core Graphic的一些深入內容
  • 一些之前用不到的封裝策略
  • 一個優化繪制性能的方法
  • 一個測量程序響應性能的方法

接下來又想到一個比較有趣的項目,不知道什么時候能填坑。

感謝觀賞。

 

來自:http://www.jianshu.com/p/b38ef781e2c5

 

 本文由用戶 vbha4425 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!