Coroutine(協(xié)程)我想大家都很熟悉了滓侍,由于Unity是單線程的引擎拜姿,我們在做一些異步操作的時候都是靠著協(xié)程來辦到的朽砰。然而,隨著Unity更新到2017版本及以上的版本鹿寨,Runtime可以支持到.NET 4.x Equivalent時,C#中的異步操作就可以使用Thread的升級版Task以及async薪夕、await這些東西了脚草。
我們先來看一個很普通場景,從網(wǎng)上獲取一些json數(shù)據(jù)到本地(這個例子里我都從https://jsonplaceholder.typicode.com/users獲取數(shù)據(jù))原献,以便做進一步處理馏慨,通常我們會開一個協(xié)程,比如這樣
IEnumerator FetchData(){
Users[] users;
// USERS
UnityWebRequest www = UnityWebRequest.Get(USERS_URL);
yield return www.SendWebRequest();
if (www.isHttpError || www.isNetworkError)
{
Debug.Log("A network error occurred");
yield break;
}
string json = www.downloadHandler.text;
try
{
users = JsonHelper.GetJsonArray<Users>(json);
}
catch
{
Debug.Log("An error occurred");
yield break;
}
// OUTPUT
foreach (Users user in users)
{
Debug.Log(user.name);
}
}
然后在需要的時候調(diào)用
void Update()
{
if(Input.GetKeyDown(KeyCode.Space)){
StartCoroutine(FetchData());
}
}
這樣寫代碼大概算是天經(jīng)地義了姑隅。然而写隶,Coroutine也有它自己的不足。首先粤策,它無法返回值樟澜,或者說需要通過一些比較復雜的方法才能使它能夠返回值,這樣我們不得不寫一個很長的單體式協(xié)程(monolithic coroutine叮盘,大概是這么翻譯的吧秩贰,我也不清楚);其次柔吼,yield無法放入try catch中毒费,我們不得不創(chuàng)建一個混合了同步(try catch)與異步(www.isHttpError或www.isNetworkError)的錯誤處理機制。
所以愈魏,在當下的Unity版本中觅玻,有時候我們能用異步編程來代替協(xié)程,以此來規(guī)避一些協(xié)程所帶來的困擾培漏。想要使用異步溪厘,首先要確保Runtime版本,在Unity2017及以上的版本中牌柄,點擊Edit > Project Settings > Player > Configuration > Scripting Runtime Version > .NET 4.x Equivalent畸悬。如果你在使用2017以前的Unity版本,很遺憾珊佣,這個新功能你無法使用了o(╥﹏╥)o
那么現(xiàn)在我們來改寫之前的協(xié)程蹋宦,將它變成異步
async Task<Users[]> FetchUsers(){
UnityWebRequest www = UnityWebRequest.Get(USERS_URL);
www.SendWebRequest();
while (!www.isDone){
await Task.Delay(100);
}
if (www.isHttpError || www.isNetworkError)
{
throw new System.Exception();
}
string json = www.downloadHandler.text;
Users[] res = JsonHelper.GetJsonArray<Users>(json);
return res;
}
然后是調(diào)用
async void LateUpdate() {//這里的方法不標記為async則下面會報錯
if(Input.GetKeyDown(KeyCode.L)){
try
{
//方法不標記為async則報錯:The 'await' operator can only be used within
//an async method. Consider marking this method with the 'async' modifier
//and changing its return type to 'Task'.
Users[] users = await FetchUsers();
for (int i = 0; i < users.Length; i++)
{
Debug.Log(users[i].name);
}
}
catch (System.Exception)
{
Debug.Log("An error occurred");
}
}
}
這里要注意幾點:
- 方法用async標記后,如果方法內(nèi)沒有出現(xiàn)await咒锻,那么這個方法的調(diào)用和普通方法的調(diào)用沒有區(qū)別冷冗。
- 有await時,在await之前的代碼依然在主線程內(nèi)按順序執(zhí)行惑艇,直到遇到await才線程阻塞蒿辙。
- await可以理解為等待方法執(zhí)行完成,除了標記async外,還能標記Task须板,表示等待該線程完成碰镜。所以await并不是針對async方法,而是針對async方法返回給我們的Task习瑰。
- async只能標記返回型為void、Task或者Task<T>的方法秽荤。
由于我也是第一次用C#的異步甜奄,所有的一切對我來說也很新,如果有希望了解更多的小伙伴們窃款,可以去官方看相關文檔
Asynchronous programming with async and await
async (C# Reference)
await operator (C# reference)
最后课兄,我本來以為這個東西可以替代Coroutine的另一個原因是Coroutine有gc,網(wǎng)上一查有關協(xié)程gc的博客一大堆晨继。然而烟阐,我親自試驗了一遍,寫了個最簡單的協(xié)程來測試gc情況紊扬,發(fā)現(xiàn)協(xié)程的gc問題是蜒茄。。餐屎。根本沒有問題L锤稹!腹缩!
private static WaitForSecondsRealtime w = new WaitForSecondsRealtime(1);
IEnumerator Counter(){
for (int i = 0; i < 100000; i++)
{
// Debug.Log(i);
yield return null;
// yield return 0;
// yield return w;
// yield return new WaitForSeconds(1);
}
}
以上是我試驗的好幾種情況屿聋,返回null
、返回一個數(shù)字藏鹊、返回一個new WaitForSeconds(1)
润讥,將new WaitForSecondsRealtime(1)
作為全局變量使用,都試了一遍后盘寡,發(fā)現(xiàn)只有在return 0
的情況下才會發(fā)生gc楚殿,其余情況都不會有gc發(fā)生。
而我為了知道協(xié)程被開啟了所以在協(xié)程里用了Debug.Log()
宴抚,這個東西才是gc大戶勒魔,直接產(chǎn)生了6.1kb的gc,我去9角9诰睢!
而閱讀Unity的日志可以知道常潮,在Unity 5.3.6以前弟胀,協(xié)程的gc問題的確存在,但從Unity 5.3.6開始,這個問題已經(jīng)被修復了孵户!
所以萧朝,Unity原生協(xié)程可以放心使用,沒有gc問題O目蕖<旒怼!真正有問題的是Debug.Log()
竖配,這東西在線上包內(nèi)不要存在何址,嚴重影響性能!=琛用爪!
經(jīng)網(wǎng)友提醒,以上結(jié)論有誤胁镐,頻繁調(diào)用startCoroutine
會產(chǎn)生gc(我的例子是在一個協(xié)程里不斷的yield偎血,沒有gc),原生的協(xié)程真的有點問題盯漂,建議自己寫一套或者unity asset store下一個高效協(xié)程插件使用颇玷,不過呢,頻繁調(diào)用(比如說每幀)startCoroutine
真的合理么宠能?
至于為什么return 0
會有gc,那是因為return 0
發(fā)生了裝箱拆箱操作违崇,不可避免的產(chǎn)生了gc阿弃,StackOverFlow上有人回答了這個問題,地址在https://stackoverflow.com/questions/39268753/what-is-the-difference-between-yield-return-0-and-yield-return-null-in-corou羞延,所以不要認為return 0
和return null
是一樣的渣淳,都是等一幀。其實伴箩,他們不一樣入愧,任何時候都推薦使用return null
來等一幀。
完整示例在項目地址嗤谚,打開Coroutine場景即可棺蛛。
參考
Unity3D里foreach,using和Coroutine的GC問題探究及解決方案
Unity: Leveling up with Async / Await / Tasks
C#基礎系列——異步編程初探:async和await