上一章,我們分析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。