Matrix Code Rain及對Core Graphics繪制的優(yōu)化

前情提要

書接上文,上回書說到我意識(shí)到自己在專業(yè)領(lǐng)域有欠缺,于是正在大量閱讀Blog。在9月25號看到Kevin Chou的這篇介紹他開源的組件庫PNChart受到歡迎的文章時(shí),我突然想到:對啊,這是個(gè)把自己喜好與技術(shù)積累結(jié)合起來的好途徑!之前總覺得往開源社區(qū)貢獻(xiàn)代碼需要超強(qiáng)的底層代碼功力,又不想仿寫已有的組件重復(fù)造輪子,這時(shí)我才剛剛意識(shí)到,上層的UI層面同樣需要優(yōu)秀的貢獻(xiàn)——某種程度上講更加稀缺,畢竟同時(shí)對設(shè)計(jì)和代碼都有研究的程序員比較少。

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


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

架構(gòu)及軌跡生成

這是一個(gè)很簡單的小組件,所以基本架構(gòu)也很簡單:

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

下落及軌跡清理

如何產(chǎn)生動(dòng)畫呢?最開始自然想到用CAAnimation來做。
因?yàn)榇a太簡單,就不在這里寫了。
但是寫完個(gè)大概之后,運(yùn)行起來卻發(fā)現(xiàn)不對勁:總感覺沒有電影里面酷。

問題出在哪里呢?我又從移動(dòng)硬盤里翻出了那三部曲仔細(xì)地研究了一下,經(jīng)過一幀一幀地探究,我找到了原因:
電影里面的代碼并不是在“下落”,如果你盯著一個(gè)字母看,會(huì)發(fā)現(xiàn)它根本就沒移動(dòng)過位置(除去鏡頭本身的移動(dòng))。換句話說,整個(gè)空間是一個(gè)已經(jīng)排列好的字母矩陣,而我們看到的表象是一陣脈沖流過而已。

所以最后改成的方案是由每個(gè)Track實(shí)例自帶的Timer負(fù)責(zé)驅(qū)動(dòng)控制自身的下落(為表述方便我們依然沿用這個(gè)詞),當(dāng)需要刷新時(shí),通知其對應(yīng)的CodeRainLayer實(shí)例(-setNeedsDisplay)進(jìn)行重繪。至于如何重繪,由每個(gè)CodeRainLayer自行負(fù)責(zé)。

而當(dāng)整條軌跡掉出屏幕的時(shí)候,Track會(huì)檢測出邊界條件,然后把對應(yīng)的CALayer執(zhí)行removeFromSuperlayer,最后把自身從DataSource中清除。

階段性成果

OK, so far so good. 我們成功實(shí)現(xiàn)了整個(gè)的動(dòng)畫效果,看起來也確實(shí)蠻酷的:

封裝

在把它傳到GitHub之前,還需要進(jìn)行一些封裝。這里主要有兩方面的工作,一個(gè)是增加控制關(guān)鍵字來限制外界能接觸到的內(nèi)部類和方法,另一個(gè)是將可調(diào)節(jié)的參數(shù)向外界暴露出來。

Access Control

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

順便,Swift 3中的訪問權(quán)限依次是:

open,public,internal,fileprivate,private.

Configurable Parameters

在之前,組件中用到的所有參數(shù)都定義在了一個(gè)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
}

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

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

那么如果用戶不設(shè)置的時(shí)候呢?我們應(yīng)該用回默認(rèn)值。比如這樣:

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
    }
}

而一個(gè)2016年的UI組件應(yīng)當(dāng)是Interface Builder-Friendly的——尤其是,要做到這點(diǎn)只需舉手之勞:將上面的參數(shù)聲明為@IBInspectable。

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


優(yōu)化性能

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

而在我后面想結(jié)合一些CoreMotion的回調(diào)實(shí)現(xiàn)視角縮放效果時(shí),在5s上的畫面終于卡了起來。

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

在我搜索相關(guān)信息的時(shí)候,偶然看到一篇叫《一些提高UI繪制性能的技巧》的文章中寫道:

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

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

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()
}

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

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

帶著這個(gè)目標(biāo)去搜索,在這個(gè)文章里面介紹了創(chuàng)建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()
}

優(yōu)化結(jié)果

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

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

那么接下來的問題是,在為主線程減了這么多負(fù)之后,程序的響應(yīng)性能有提高嗎?因?yàn)橐窃贈(zèng)]什么變化的話,我要為前面這些花出的時(shí)間哭幾秒。

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

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)
        }
    }
}

核心思想是,每隔一定的時(shí)間就在主線程給該線程的信號量發(fā)消息,要是主線程因?yàn)榭D耽擱了,該線程就會(huì)輸出警告信息。

我把時(shí)間設(shè)為1/30秒,因?yàn)檫@是一個(gè)流暢的動(dòng)畫所應(yīng)當(dāng)達(dá)到的幀率。

這下終于有了喜人的對比結(jié)果:

之前:


之后:


直到啟動(dòng)20多秒后收到內(nèi)存警告,都沒有一次卡頓出現(xiàn)!

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

就是下面這個(gè):

最后的話

整個(gè)項(xiàng)目已經(jīng)傳到了GitHub上:
https://github.com/zshowing/JSMatrixCodeRainView

通過這個(gè)項(xiàng)目,我學(xué)到的東西包括:

  • Core Graphic的一些深入內(nèi)容
  • 一些之前用不到的封裝策略
  • 一個(gè)優(yōu)化繪制性能的方法
  • 一個(gè)測量程序響應(yīng)性能的方法

接下來又想到一個(gè)比較有趣的項(xiàng)目,不知道什么時(shí)候能填坑。

感謝觀賞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容