天天看點

Cocos2d-X3.0 刨根問底(六)----- 排程器Scheduler類源碼分析

上一章,我們分析Node類的源碼,在Node類裡面耦合了一個 Scheduler 類的對象,這章我們就來剖析Cocos2d-x的排程器 Scheduler

類的源碼,從源碼中去了解它的實作與應用方法。

直入正題,我們打開CCScheduler.h檔案看下裡面都藏了些什麼。

打開了CCScheduler.h 檔案,還好,這個檔案沒有ccnode.h那麼大有上午行,不然真的吐血了,

僅僅不到500行代碼。這個檔案裡面一共有五個類的定義,老規矩,從加載的頭檔案開始閱讀。

代碼很簡單,看到加載了ref類,可以推斷Scheduler 可能也繼承了ref類,對象統一由Cocos2d-x記憶體管理器來管理。

這點代碼值得注意的就是下面 定義了一個函數類型 ccSchedulerFunc 接收一個float參數 傳回void類型。

下面我們看這個檔案裡定義的第一個類 Timer

第一點看過這個Timer類定義能了解到的資訊如下:

Timer類也是Ref類的子類,采用了cocos2d-x統一的記憶體管理機制。

這裡一個抽象類。必須被繼承來使用。

Timer主要的函數就是update,這個我們重點分析。

初步了解之後,我們按照老方法,先看看Timer類都有哪些成員變量,了解一下它的資料結構。

第一個變量為

這是一個Scheduler類的對象指針,後面有一個注釋說這個指針是一個

弱引用,弱引用的意思就是,在這個指針被指派的時候并沒有增加對_scheduler的引用 計數。

後面幾個變量也很好了解。

// 時間間隔。

總結一下,通過分析Timer類的成員變量,我們可以知道這是一個用來描述一個計時器的類,

每隔 _interval 來觸發一次,

可以設定定時器觸發時的延遲 _useDelay和延遲時間 _delay.

可以設定定時器觸發的次數_repeat 也可以設定定時器永遠執行 _runforever

下面看Timer類的方法。

getInterval 與 setInterval不用多說了,就是_interval的 讀寫方法。

下面看一下 setupTimerWithInterval方法。

這也是一個設定定時器屬性的方法。

參數 seconds是設定了_interval

第二個參數repeat設定了重複的次數

第三個delay設定了延遲觸發的時間。

通過 這三個參數的設定還計算出了幾個狀态變量 根據 delay是否大于0.0f計算了_useDelay

根據 repeat值是否是  kRepeatForever來設定了 _runforever。

注意一點 第一行代碼

_elapsed = -1;

這說明這個函數 setupTimerWithInterval

是一個初始化的函數,将已經渡過的時間初始化為-1。是以在已經運作的定時器使用這個函數的時候計時器會重新開始。

下面看一下重要的方法 update

這個update 代碼很簡單,就是一個标準的定時器觸發邏輯,沒有接觸過的同學可以試模仿一下。

在這個update方法裡,調用了 trigger與 cancel方法,現在我們可以了解這兩個抽象方法是個什麼作用,

trigger是觸發函數

cancel是取消定時器

具體怎麼觸發與怎麼取消定時器,就要在Timer的子類裡實作了。

Timer類源碼我們分析到這裡,下面看Timer類的第一個子類 TimerTargetSelector 的定義

這個類也很簡單。

我們先看一下成員變量 一共兩個成員變量

這裡關聯了一個 Ref對象,應該是執行定時器的對象。

SEL_SCHEDULE  這裡出現了一個新的類型,我們跟進一下,這個類型是在Ref類下面定義的,我們看一下。

可以看到 SEL_SCHEDULE是一個關聯Ref類的函數指針定義

_selector 是一個函數,那麼應該就是定時器觸發的回調函數。

TimerTargetSelector  也就是一個目标定時器,指定一個Ref對象的定時器

下面我們來看TimerTargetSelector 的幾個主要的函數。

這個數不用多說,就是一個TimerTargetSelector的初始化方法。後面三個參數是用來初始化基類Timer的。

第一個參數 scheduler 因為我們還沒分析到

Scheduler類現在還不能明确它的用處,這裡我們先标紅記下。

getSelector 方法不用多說,就是 _selector的

讀取方法,注意這個類沒有setSelector因為初始化 _selector要在 initWithSelector方法裡進行。

接下來就是兩個重載方法  trigger 和 cancel

下面看看實作過程

實作過程非常簡單。

在trigger函數中,實際上就是調用 了初始化傳進來的回調方法。 _selector 這個回調函數接收一個參數就是度過的時間_elapsed

cancel方法中調用 了 _scheduler的 unschedule方法,這個方法怎麼實作的,後面我們分析到Scheduler類的時候再細看。

小結:

TimerTargetSelector 這個類,是一個針對Ref 對象的定時器,調用的主體是這個Ref

對象。采用了回調函數來執行定時器的觸發過程。

下面我們繼續進行 閱讀  TimerTargetCallback 類的源碼

這個類也是 Timer  類的子類,與TimerTargetSelector類的結構類似

先看成員變量,

_target 一個void類型指針,應該是記錄一個對象的

ccSchedulerFunc 最上在定義的一個回調函數

還有一個_key 應該是一個定時器的别名。

initWithCallback 這個函數就是一些set操作來根據參數對其成員變量指派,不用多說。

getCallback 是 _callback的讀取方法。

getkey是_key值的讀取方法。

下面我們重點看一下 trigger與  cancel的實作。

這兩個方法實作也很簡單,

在trigger中就是調用了callback方法并且把_elapsed作為參數 傳遞。

cancel與上面的cancel實作一樣,後面我們會重點分析 unschedule

方法。

下面一個Timer類的了類是TimerScriptHandler 與腳本調用

有關,這裡大家自行看一下代碼,結構與上面的兩個類大同小異。

接下來我們碰到了本章節的主角了。 Scheduler 類

在Scheduler類之前聲明了四個結構體,我們看一眼

後面分析Scheduler時會碰到這幾個資料類型,這幾個結構體的定義很簡單,後面碰到難點我們在詳細說。

類定義

不用多說了,這樣的定義我們已經碰到好多了, Scheduler也是 Ref的了類。

老方法,先看成員變量。了解Scheduler的資料結構。

看了這些成員變量,大多是一些連結清單,數組,具體幹什麼的也猜不太出來,沒關系,我們從方法入手,看看都幹了些什麼。

構造函數 與 析構函數

構造函數與析構函數都很簡單,注意構造函數裡面有一行注釋,不希望在一幀裡面有超過30個回調函數。我們在編寫自己的程式的時候也要注意這一點。

析構函數中調用 了 unscheduleAll 

這個函數我們先不跟進看。後面再分析,這裡要記住unscheduleAll是一個清理方法。

getTimeScale 與 setTimeScale 是讀寫_timeScale的方法,控制定時器速率的。

下面我們看 Scheduler::schedule 的幾個重載方法。

先看 schedule 方法的幾個參數 很像 TimerTargetSelector  類的init方法的幾個參數。

下面看一下schedule的函數過程,

先調用了 HASH_FIND_PTR(_hashForTimers,

&target, element); 有興趣的同學可以跟一下

HASH_FIND_PTR這個宏,這行代碼的含義是在  _hashForTimers

這個數組中找與&target相等的元素,用element來傳回。

而_hashForTimers不是一個數組,但它是一個線性結構的,它是一個連結清單。

下面的if判斷是判斷element的值,看看是不是已經在_hashForTimers連結清單裡面,如果不在那麼配置設定記憶體建立了一個新的結點并且設定了pause狀态。

再下面的if判斷的含義是,檢查目前這個_target的定時器清單狀态,如果為空那麼給element->timers配置設定了定時器空間

如果這個_target的定時器清單不為空,那麼檢查清單裡是否已經存在了

selector 的回調,如果存在那麼更新它的間隔時間,并退出函數。

這行代碼是給 ccArray配置設定記憶體,确定能再容納一個timer.

函數的最後四行代碼,就是建立了一個新的 TimerTargetSelector  對象,并且對其指派 還加到了 定時器清單裡。

這裡注意一下,調用了 timer->release()

減少了一次引用,會不會造成timer被釋放呢?當然不會了,大家看一下ccArrayAppendObject方法裡面已經對

timer進行了一次retain操作是以 調用了一次release後保證 timer的引用計數為1.

看過這個方法,我們清楚了幾點

tHashTimerEntry  這個結構體是用來記錄一個Ref 對象的所有加載的定時器

_hashForTimers 是用來記錄所有的 tHashTimerEntry 的連結清單頭指針。

下面一個 schedule函數的重載版本與第一個基本是一樣的

唯一 的差別是這個版本的 repeat參數為 kRepeatForever 永遠執行。

下面看第三個 schedule的重載版本

這個版本與第一個版本過程基本一樣,隻不過這裡使用的_target不是Ref類型而是void*類型,可以自定義類型的定時器。是以用到了TimerTargetCallback這個定時器結構。

同樣将所有 void*對象存到了 _hashForTimers

還有一個版本的 schedule 重載,它是第三個版本的擴充,擴充了重複次數為永遠。

這裡小結一下 schedule方法。

Ref類型與非Ref類型對象的定時器處理基本一樣,都是加到了排程控制器的_hashForTimers連結清單裡面,

調用schedule方法會将指定的對象與回調函數做為參數加到schedule的 定時器清單裡面。加入的過程會做一個檢測是否重複添加的操作。

下面我們看一下幾個 unschedule 方法。unschedule方法作用是将定時器從管理清單裡面删除。

我們按函數過程看,怎麼來解除安裝定時器的。

參數為一個回調函數指針和一個Ref 對象指針。

在 對象定時器清單_hashForTimers裡找是否有 target 對象

在找到了target對象的條件下,對target裝載的timers進行逐一周遊

周遊過程 比較目前周遊到的定時器的 selector是等于傳入的 selctor

将找到的定時器從element->timers裡删除。重新設定timers清單裡的 計時器的個數。

最後_currentTarget 與 element的比較值來決定是否從_hashForTimers 将其删除。

這些代碼過程還是很好了解的,不過程小魚在看這幾行代碼的時候有一個問題還沒看明白,就是用到了_currentTarget

與 _currentTargetSalvaged 這兩個變量,它們的作用是什麼呢?下面我們帶着這個問題來找答案。

再看另一個unschedule重載版本,基本都是大同小異,都是執行了這幾個步驟,隻是查找的參數從 selector變成了 std::string

&key 對象從 Ref類型變成了void*類型。

現在我們看一下update方法。當看到update方法時就知道 這個方法是在每一幀中調用的,也是引擎驅動的靈魂。

update方法的詳細分析。

通過上面的代碼分析我們對 schedule的update有了進一步的了解。這裡的currentTartet對象我們已經了解了是什麼意思。

疑問1的解答:

_currentTarget是在

update主循環過程中用來标記目前執行到哪個target的對象。

_currentTargetSalvaged

是标記_currentTarget是否需要進行清除操作的變量。

schedule這個類主要的幾個函數我們都

分析過了,下面還有一些成員方法,我們簡單說明一下,代碼都很簡單大家根據上面的分析可以自行閱讀一下。

到這裡,疑問2 還沒有找到答案。

我們回顧一下,上一章節看Node類的源碼的時候,關于排程任務那塊的代碼我們暫時略過了,這裡我們回去看一眼。

先看Node類構造函數中對排程器的初始化過程有這樣兩行代碼。

通過這兩行代碼我們可以知道在這裡沒有重新建構一個新的Scheduler而是用了Director裡建立的Scheduler。而Director裡面是真正建立了Scheduler對象。

我們再看Node類的一些Schedule方法。

看到了這些方法及實作 ,其實上面都分析過了,隻不過Node 類又內建了一份,其實就是調用

了Director裡的schedulor對象及相應的操作。

我們再看Node類的這兩個函數

這段注釋已經說的很清楚了,Node的這兩個方法 會在每一幀都被調用,而不是按時間間隔來定時的。

看到這段注釋,使我們對定時器的另一個排程機制有了了解,前面分析都是針對 一段間隔時間的排程機制,而這裡又浮現了幀幀排程的機制。

下面我們來梳理一下。

記得 在Node類裡面有一個方法 update

我們回顧一下它的聲明

注釋寫的很清楚, 如果 scheduleUpdate方法被調用 且 node在激活狀态, 那麼 update方法将會在每一幀中都會被調用

再看一下 scheduleUpdate 相關方法。

在Node類定義預設都是 0 級别的結點。

可以看到最終是調用了_scheduler->scheduleUpdate 方法,我們再跟到

Scheduler::scheduleUpdate

看到了吧,Node::update 會在 回調函數中被調用 ,這塊代碼有點不好了解 大家參考一下 c++11的

lambda表達式,這裡的回調函數定義了一個匿名函數。函數的實作過程就是調用

target的update方法。在node類中target那塊傳遞的是node的this指針。

再看一下 schedulePerFrame方法。

哈哈,在這裡将幀排程過程加入到了相應權限的排程清單中,到此疑問2已經得到了解決。

要注意的一點是,這個方法先對target做了檢測,如果已經在幀排程清單裡面會直接傳回的,也就是說一個node結點隻能加入一次幀排程清單裡,也隻能有一個回調過程,這個過程就是Node::update方法,如果想實作自己的幀排程邏輯那麼重載它好了。

好啦,今天羅嗦這麼多,大家看的可能有些亂,小魚這裡總結一下。

Scheduler 類是cocos2d-x裡的排程控制類,它分兩種排程模式 按幀排程與按時間間隔排程

,當然,如果時間間隔設定小于幀的時間間隔那麼就相當于按幀排程了。

按幀排程被內建在Node類裡,排程的回調函數就是Node::update函數。

按時間排程可以分兩種形式對象形式, 一種 是Ref基類的對象,一種是任意對象。

Scheduler實際上是存儲了很多小任務的清單管理器,每一個定時任務都是以Timer類為基類實作的。管理器的清單以對象的指針哈希存放的。

cocos2d-x引擎啟動後Director類會建立一個預設的排程管理器,所有的Node類預設都會引入Director的排程管理器,排程管理器會在Director的

mainLoop裡的 drawscene方法裡被每一幀都排程。

Scheduler類我們就分析到這裡,今天 的内容關聯了好幾個類,如果有什麼問題可以在評論中向我提出,有好建議大家也不要吝啬,多多向我提。

下一章我們來剖析Cocos2d-x的事件機制 Event。