iOS 定時器耗電探究

weixin_34120274發表於2018-09-14

iOS開發中的幾種定時器

iOS開發中定時器實現方式大致有三種,一種是Timer實現,一種是通過GCD自己建立,另一種是CADisplayLink建立。

Timer使用簡單,需要注意的是Timerrunloop聯絡緊密,在用scheduled方式建立之後,程式會自動將其新增到runloop中,不再需要Timer時,必須手動將其釋放。

CADisplayLink與螢幕重新整理率同步,也就是每當螢幕重新整理一次就可以執行一次繫結的事件(iPhone的重新整理頻率為60HZ,正常情況下1s需要重新整理60次),也可以通過其屬性設定間隔多少幀執行一次,一般用於即時畫面渲染,啟動CADisplayLink定時器需要呼叫add(to: <#T##RunLoop#>, forMode: <#T##RunLoopMode#>)方法。

gcd定時器主要用於精準定時,它的精度能達到納秒級別。gcd不同於上面兩種定時器,它不會受runloop的影響(runloop也是通過gcd實現),所以精確,例如Timer加入runloop預設模式時,當有滑動控制元件在滑動時會暫停計時,但是gcd不會。gcd可控性強,但是使用稍微複雜

定時器普通模式下不能在後臺執行,需要設定Background Modes,選擇一種可以後臺執行的模式

關於耗電測試

有時候開發需要多個任務定時執行(與硬體結合開發較常見),可能他們的定時時間不一致,這就涉及到多個計時。

我試想了兩種實現方式,一種是通過建立多個定時器實現,另一種是建立一個定時器,每個定時任務定義一個計數器,定時器每秒累加他們的計數器,當滿足時間條件時就去執行相應的定時任務,當需要取消某個任務時就停止累加並將計數器置零。

比較一下這兩種方式,第一種簡單明瞭,便於維護,但是會存在多個定時器,消耗記憶體資源。第二種只有一個定時器,但是程式碼易讀性相對第一種較差,也會建立很多全域性變數判斷是否需要累加計數器以及儲存計數器的值。而且定時器執行間隔是1s,執行內容是將所有任務計數器累加1,判斷每個任務的計數器值是否滿足執行條件,耗費CPU資源。

雖然每個定時器都會消耗資源,但我個人還是認為第一種方式較為科學,耗電也不會比第二種高,因為第二種每秒都會執行任務消耗CPU,比如一個10秒的定時任務,第一種方式執行1次,第二種方式需要執行10次,也不夠靈活,管理多個任務的時候更難操控。接下來我需要驗證一下這個想法,我準備先用Timer實現驗證。最後附帶gcd實現定時器的方式。

本次驗證耗電採用InstrumentEnergy Log工具,測試流程如下:

  • 1.設定app不鎖屏:UIApplication.shared.isIdleTimerDisabled = true

  • 2.安裝APP後斷開iPhone電源線

  • 3.退出所有執行的APP

  • 4.開啟系統設定-開發者-logging開啟Energy後點選Start Recording

  • 5.開啟需要測試的APP執行10分鐘

  • 6.開啟系統設定-開發者-logging點選Stop Recording

  • 7.開啟InstrumentEnergy Log工具,從裝置匯出日誌

8.修改方式後重新執行以上步驟

2670204-c406a4937a966f75.png
2670204-eb1e38b8b347bbc8.png
2670204-83bf5376a9444653.png
2670204-8689f6d0091d5fff.png
2670204-2f7d881d9e88ac76.png

驗證第一種實現方式

定義6個定時器控制6個任務,每個任務時間不一致。
為了有操作消耗CPU資源,這裡任務都是計算0-10000之間的完全數

完全數(Perfect number),又稱完美數或完備數,是一些特殊的自然數。它所有的真因子(即除了自身以外的約數)的和(即因子函式),恰好等於它本身。如果一個數恰好等於它的因子之和,則稱該數為“完全數”。

import UIKit

class ViewController: UIViewController {
    
    fileprivate var timer1: Timer?
    fileprivate var timer2: Timer?
    fileprivate var timer3: Timer?
    fileprivate var timer4: Timer?
    fileprivate var timer5: Timer?
    fileprivate var timer6: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        UIApplication.shared.isIdleTimerDisabled = true
        
        lotsTimer()
    }

    func lotsTimer(){
        
        timer1 = Timer.scheduledTimer(timeInterval: 20, target: self, selector: #selector(timer1Event), userInfo: nil, repeats: true)
        
        timer2 = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(timer2Event), userInfo: nil, repeats: true)
        
        timer3 = Timer.scheduledTimer(timeInterval: 40, target: self, selector: #selector(timer3Event), userInfo: nil, repeats: true)
        
        timer4 = Timer.scheduledTimer(timeInterval: 50, target: self, selector: #selector(timer4Event), userInfo: nil, repeats: true)
        
        timer5 = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(timer5Event), userInfo: nil, repeats: true)
        
        timer6 = Timer.scheduledTimer(timeInterval: 70, target: self, selector: #selector(timer6Event), userInfo: nil, repeats: true)
        
    }
    
    @objc func timer1Event(){
        print("timer1執行")
        envent()
    }
    
    @objc func timer2Event(){
        print("timer2執行")
        envent()
    }
    
    @objc func timer3Event(){
        print("timer3執行")
        envent()
    }
    
    @objc func timer4Event(){
        print("timer4執行")
        envent()
    }
    
    @objc func timer5Event(){
        print("timer5執行")
        envent()
    }
    
    @objc func timer6Event(){
        print("timer6執行")
        envent()
    }
    
    func envent(){
        
        for i in 2...10000{
            var sum = 1
            var j = 2
            while j <= Int(sqrt(Double(i))){
                if i % j == 0{
                    sum += j
                    if i / j != j{
                        sum += i / j
                    }
                }
                j += 1
            }
            if sum == i {
                print(i)
            }
        }
    }
}

按照上面的測試步驟,斷開電源線執行10分鐘。

驗證第二種方式

由於第一種方式所有定時器都是重複計時操作,為保證一致,這裡只定義6個全域性變數儲存每個事件的計數器值,少定義6個全域性變數判斷是否停止累加計數器(實際開發中用這種方式很繁雜,也許會停掉事件,但計時器掌管其他事件,不能停止,所以只能通過變數判斷是否需要繼續計數)。

import UIKit

class ViewController: UIViewController {
    
    fileprivate var timer: Timer?
    
    fileprivate var counter1 = 0
    fileprivate var counter2 = 0
    fileprivate var counter3 = 0
    fileprivate var counter4 = 0
    fileprivate var counter5 = 0
    fileprivate var counter6 = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        
        UIApplication.shared.isIdleTimerDisabled = true
        
        oneTimer()
    }

    func oneTimer(){
        
        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(addCount), userInfo: nil, repeats: true)
    }
    
    @objc func addCount(){
        addCount1()
        addCount2()
        addCount3()
        addCount4()
        addCount5()
        addCount6()
    }
    
    func addCount1(){
        counter1 += 1
        guard counter1 >= 20 else { return }
        timer1Event()
        counter1 = 0
    }
    func addCount2(){
        counter2 += 1
        guard counter2 >= 30 else { return }
        timer2Event()
        counter2 = 0
    }
    func addCount3(){
        counter3 += 1
        guard counter3 >= 40 else { return }
        timer3Event()
        counter3 = 0
    }
    func addCount4(){
        counter4 += 1
        guard counter4 >= 50 else { return }
        timer4Event()
        counter4 = 0
    }
    func addCount5(){
        counter5 += 1
        guard counter5 >= 60 else { return }
        timer5Event()
        counter5 = 0
    }
    func addCount6(){
        counter6 += 1
        guard counter6 >= 70 else { return }
        timer6Event()
        counter6 = 0
    }
    
    func timer1Event(){
        print("timer1執行")
        envent()
    }
    
    func timer2Event(){
        print("timer2執行")
        envent()
    }
    
    func timer3Event(){
        print("timer3執行")
        envent()
    }
    
    func timer4Event(){
        print("timer4執行")
        envent()
    }
    
    func timer5Event(){
        print("timer5執行")
        envent()
    }
    
    func timer6Event(){
        print("timer6執行")
        envent()
    }
    
    func envent(){
        
        for i in 2...10000{
            var sum = 1
            var j = 2
            while j <= Int(sqrt(Double(i))){
                if i % j == 0{
                    sum += j
                    if i / j != j{
                        sum += i / j
                    }
                }
                j += 1
            }
            if sum == i {
                print(i)
            }
        }
    }
}

測試結果

電量

2670204-547f13e82175a8f2.png
方式1耗電.png
2670204-a7957e1a9d11f7cf.png
方式2耗電.png

CPU

2670204-032318aa6fc62215.png
方式1CPU資源消耗.png
2670204-5b911163181dcec3.png
方式2CPU資源消耗.png

測試結果可以看出,耗電量兩個不相上下(20個等級,數字越大越耗電,這裡全部是0),因為操作都過於簡單,幾乎不耗電,但是從CPU佔用情況看。方式1更節約CPU資源,而CPU資源消耗跟耗電量成正比,所以有理由相信當APP執行時間足夠長,方式2會消耗更多電量。

GCD方式實現定時器

import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        createDispatchTimer(timeInterval: 2, handler: { [weak self] (timer) in
            self?.envent()
            
            //需要取消重複時呼叫
            //timer?.cancel()
        }, needRepeat: true)
    }
    
    
    ///   - timeInterval: 間隔時間
    ///   - handler: 事件
    ///   - needRepeat: 是否重複
    func createDispatchTimer(timeInterval: Double, handler:@escaping (DispatchSourceTimer?)->(), needRepeat: Bool)
    {
        let timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
        timer.schedule(deadline: .now(), repeating: timeInterval)
        timer.setEventHandler {
            DispatchQueue.main.async {
                if needRepeat{
                    handler(timer)
                }else{
                    timer.cancel()
                    handler(nil)
                }
            }
        }
        timer.resume()
    }
    
    
    func envent(){
        print("執行")
    }
    
    
    //附贈
    
    /// GCD定時器倒數計時
    ///   - timeInterval: 間隔時間
    ///   - repeatCount: 重複次數
    ///   - handler: 迴圈事件, 閉包引數: 1. timer, 2. 剩餘執行次數
    func createDispatchTimer(timeInterval: Double, repeatCount:Int, handler:@escaping (DispatchSourceTimer?, Int)->())
    {
        if repeatCount <= 0 {
            return
        }
        let timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
        var count = repeatCount
        timer.schedule(wallDeadline: .now(), repeating: timeInterval)
        timer.setEventHandler(handler: {
            count -= 1
            DispatchQueue.main.async {
                handler(timer, count)
            }
            if count == 0 {
                timer.cancel()
            }
        })
        timer.resume()
    }
}

相關文章