[Unity 3d] 讀源碼幕屹,看 Unity 對帶參數(shù)命令行啟動邏輯的封裝

在本文蓝丙,筆者將帶大家從 EditorUtility.CompileCsharp API 出發(fā),看一看Unity 是怎么使用 帶參數(shù)命令行調(diào)用軟件的望拖。行文倉促內(nèi)容繁雜故而較為佛系渺尘,另筆者技術(shù)有限難免疏漏,見諒~

前言

昨天看到一篇 博客 说敏,講述了如何將Unity 編輯器之外的腳本通過 EditorUtility.CompileCsharp 在編輯器下編譯成 Dll 鸥跟。
這引起了筆者的不少興趣,于是拿起 Dnspy 沿著這個方法名 CompileCsharp 就追了進(jìn)去...


這些與腳本編譯相關(guān)的邏輯居然不是調(diào)用的 Native dll的方法,這很 nice医咨!

發(fā)現(xiàn)

經(jīng)過不停的點擊跟進(jìn)枫匾,筆者發(fā)現(xiàn)了一個 Program 類型,也就是本文的主角拟淮,它將我們常規(guī)的 軟件命令行調(diào)用 進(jìn)行了封裝干茉,感覺到了滿滿的專業(yè)級別的嚴(yán)謹(jǐn):

using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace UnityEditor.Utils
{
    internal class Program : IDisposable
    {
        protected Program()
        {
            this._process = new Process();
        }
        public Program(ProcessStartInfo si) : this()
        {
            this._process.StartInfo = si;
        }
        public void Start()
        {
            this.Start(null);
        }
        public void Start(EventHandler exitCallback) 
        {
            if (exitCallback != null)
            {
                this._process.EnableRaisingEvents = true;
                this._process.Exited += exitCallback; //監(jiān)聽軟件退出事件
            }
            this._process.StartInfo.RedirectStandardInput = true;
            this._process.StartInfo.RedirectStandardError = true;
            this._process.StartInfo.RedirectStandardOutput = true;
            this._process.StartInfo.UseShellExecute = false;
            this._process.Start();
            this._stdout = new ProcessOutputStreamReader(this._process, this._process.StandardOutput); //使用異步讀取軟件常規(guī)輸出
            this._stderr = new ProcessOutputStreamReader(this._process, this._process.StandardError);//使用異步讀取軟件異常
            this._stdin = this._process.StandardInput.BaseStream;
        }
        public ProcessStartInfo GetProcessStartInfo()
        {
            return this._process.StartInfo;
        }
        public void LogProcessStartInfo() 
        {
            if (this._process != null)
            {
                Program.LogProcessStartInfo(this._process.StartInfo);
            }
            else
            {
                Console.WriteLine("Failed to retrieve process startInfo");
            }
        }
        private static void LogProcessStartInfo(ProcessStartInfo si) //實現(xiàn)被啟動的進(jìn)程的 文件名+命令行參數(shù)的輸出
        {
            Console.WriteLine("Filename: " + si.FileName);
            Console.WriteLine("Arguments: " + si.Arguments);
            IEnumerator enumerator = si.EnvironmentVariables.GetEnumerator();
            try
            {
                while (enumerator.MoveNext())
                {
                    object obj = enumerator.Current;
                    DictionaryEntry dictionaryEntry = (DictionaryEntry)obj;
                    if (dictionaryEntry.Key.ToString().StartsWith("MONO"))
                    {
                        Console.WriteLine("{0}: {1}", dictionaryEntry.Key, dictionaryEntry.Value);
                    }
                }
            }
            finally
            {
                IDisposable disposable;
                if ((disposable = (enumerator as IDisposable)) != null)
                {
                    disposable.Dispose();
                }
            }
            int num = si.Arguments.IndexOf("Temp/UnityTempFile");
            Console.WriteLine("index: " + num);
            if (num > 0)
            {
                string text = si.Arguments.Substring(num);
                Console.WriteLine("Responsefile: " + text + " Contents: ");
                Console.WriteLine(File.ReadAllText(text));
            }
        }
        public string GetAllOutput() //實現(xiàn)進(jìn)程的所有標(biāo)準(zhǔn)輸出和異常輸出
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("stdout:");
            foreach (string value in this.GetStandardOutput())
            {
                stringBuilder.AppendLine(value);
            }
            stringBuilder.AppendLine("stderr:");
            foreach (string value2 in this.GetErrorOutput())
            {
                stringBuilder.AppendLine(value2);
            }
            return stringBuilder.ToString();
        }
        public bool HasExited
        {
            get
            {
                if (this._process == null)
                {
                    throw new InvalidOperationException("You cannot call HasExited before calling Start");
                }
                bool result;
                try
                {
                    result = this._process.HasExited;
                }
                catch (InvalidOperationException)
                {
                    result = true;
                }
                return result;
            }
        }
        public int ExitCode
        {
            get
            {
                return this._process.ExitCode;
            }
        }
        public int Id
        {
            get
            {
                return this._process.Id;
            }
        }
        public void Dispose()
        {
            this.Kill();
            this._process.Dispose();
        }
        public void Kill()
        {
            if (!this.HasExited)
            {
                this._process.Kill();
                this._process.WaitForExit();
            }
        }
        public Stream GetStandardInput()
        {
            return this._stdin;
        }
        public string[] GetStandardOutput()
        {
            return this._stdout.GetOutput();
        }
        public string GetStandardOutputAsString()
        {
            string[] standardOutput = this.GetStandardOutput();
            return Program.GetOutputAsString(standardOutput);
        }
        public string[] GetErrorOutput()
        {
            return this._stderr.GetOutput();
        }
        public string GetErrorOutputAsString()
        {
            string[] errorOutput = this.GetErrorOutput();
            return Program.GetOutputAsString(errorOutput);
        }
        private static string GetOutputAsString(string[] output)
        {
            StringBuilder stringBuilder = new StringBuilder();
            foreach (string value in output)
            {
                stringBuilder.AppendLine(value);
            }
            return stringBuilder.ToString();
        }
        public void WaitForExit()
        {
            this._process.WaitForExit();
        }
        public bool WaitForExit(int milliseconds)
        {
            return this._process.WaitForExit(milliseconds);
        }
        private ProcessOutputStreamReader _stdout;
            private ProcessOutputStreamReader _stderr;
        private Stream _stdin;
        public Process _process;
    }
}

其中,它把 ProcessInfo 進(jìn)行了包裝很泊,完善了命令行執(zhí)行軟件運行的流程:Start 角虫、Kill 、Dispose 一個都不少委造。
再次戳鹅,它把 標(biāo)準(zhǔn)輸出(StandardOuput) 和 標(biāo)準(zhǔn)錯誤輸出 (StandardOuput)使用了專門封裝的 流讀寫器中,開啟線程進(jìn)行讀取昏兆,提高了性能枫虏,也避免了卡調(diào)用 Process 的線程。
最后亮垫,為了方便調(diào)試模软,實現(xiàn)了非常完善的 命令行參數(shù)調(diào)試輸出伟骨,Process 常規(guī)數(shù)據(jù)和異常輸出 的方法饮潦。

其他

  1. ProcessOutputStreamReader
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace UnityEditor.Utils
{
    internal class ProcessOutputStreamReader
    {
        internal ProcessOutputStreamReader(Process p, StreamReader stream) : this(() => p.HasExited, stream){}
        internal ProcessOutputStreamReader(Func<bool> hostProcessExited, StreamReader stream)
        {
            this.hostProcessExited = hostProcessExited;
            this.stream = stream;
            this.lines = new List<string>();
            this.thread = new Thread(new ThreadStart(this.ThreadFunc));
            this.thread.Start();
        }
        private void ThreadFunc()
        {
            if (!this.hostProcessExited())
            {
                try
                {
                    while (this.stream.BaseStream != null)
                    {
                        string text = this.stream.ReadLine();
                        if (text == null)
                        {
                            break;
                        }
                        object obj = this.lines;
                        lock (obj)
                        {
                            this.lines.Add(text);
                        }
                    }
                }
                catch (ObjectDisposedException)
                {
                    object obj2 = this.lines;
                    lock (obj2)
                    {
                        this.lines.Add("Could not read output because an ObjectDisposedException was thrown.");
                    }
                }
                catch (IOException)
                {
                }
            }
        }

        internal string[] GetOutput()
        {
            if (this.hostProcessExited())
            {
                this.thread.Join();
            }
            object obj = this.lines;
            string[] result;
            lock (obj)
            {
                result = this.lines.ToArray();
            }
            return result;
        }
        private readonly Func<bool> hostProcessExited;
        private readonly StreamReader stream;
        internal List<string> lines;
        private Thread thread;
    }
}

當(dāng)然了,里面還有很多令人印象深刻的工具類携狭,比如對Unity Mono 庫的查找輔助腳本 MonoInstallationFinder啦继蜡,對命令行參數(shù)進(jìn)行進(jìn)行修剪/格式化的腳本:CommandLineFormatter啦。這些都可以直接拿來用逛腿,當(dāng)然也很值得借鑒一哦稀并。

  1. CommandLineFormatter
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;

namespace UnityEditor.Scripting.Compilers
{
    internal static class CommandLineFormatter
    {
        public static string EscapeCharsQuote(string input)
        {
            string result;
            if (input.IndexOf('\'') == -1)
            {
                result = "'" + input + "'";
            }
            else if (input.IndexOf('"') == -1)
            {
                result = "\"" + input + "\"";
            }
            else
            {
                result = null;
            }
            return result;
        }

        public static string PrepareFileName(string input)
        {
            input = FileUtil.ResolveSymlinks(input);
            string result;
            if (Application.platform == RuntimePlatform.WindowsEditor)
            {
                result = CommandLineFormatter.EscapeCharsWindows(input);
            }
            else
            {
                result = CommandLineFormatter.EscapeCharsQuote(input);
            }
            return result;
        }

        public static string EscapeCharsWindows(string input)
        {
            string result;
            if (input.Length == 0)
            {
                result = "\"\"";
            }
            else if (CommandLineFormatter.UnescapeableChars.IsMatch(input))
            {
                Debug.LogWarning("Cannot escape control characters in string");
                result = "\"\"";
            }
            else if (CommandLineFormatter.UnsafeCharsWindows.IsMatch(input))
            {
                result = "\"" + CommandLineFormatter.Quotes.Replace(input, "\"\"") + "\"";
            }
            else
            {
                result = input;
            }
            return result;
        }

        internal static string GenerateResponseFile(IEnumerable<string> arguments)
        {
            string uniqueTempPathInProject = FileUtil.GetUniqueTempPathInProject();
            using (StreamWriter streamWriter = new StreamWriter(uniqueTempPathInProject))
            {
                foreach (string value in from a in arguments
                where a != null
                select a)
                {
                    streamWriter.WriteLine(value);
                }
            }
            return uniqueTempPathInProject;
        }

        private static readonly Regex UnsafeCharsWindows = new Regex("[^A-Za-z0-9_\\-\\.\\:\\,\\/\\@\\\\]");

        private static readonly Regex UnescapeableChars = new Regex("[\\x00-\\x08\\x10-\\x1a\\x1c-\\x1f\\x7f\\xff]");

        private static readonly Regex Quotes = new Regex("\"");
    }
}
  1. 上述腳本的使用場景:ManagedProgram
using System;
using System.Diagnostics;
using System.IO;
using UnityEditor.Scripting.Compilers;
using UnityEditor.Utils;
using UnityEngine;

namespace UnityEditor.Scripting
{
    internal class ManagedProgram : Program //繼承 Program
    {
        public ManagedProgram(string monodistribution, string profile, string executable, string arguments, Action<ProcessStartInfo> setupStartInfo) : this(monodistribution, profile, executable, arguments, true, setupStartInfo){}
        public ManagedProgram(string monodistribution, string profile, string executable, string arguments, bool setMonoEnvironmentVariables, Action<ProcessStartInfo> setupStartInfo)
        {
            string text = ManagedProgram.PathCombine(new string[]
            {
                monodistribution,
                "bin",
                "mono"
            });
            if (Application.platform == RuntimePlatform.WindowsEditor)
            {
                text = CommandLineFormatter.PrepareFileName(text + ".exe"); //裝載 exe 程序路徑
            }
            ProcessStartInfo processStartInfo = new ProcessStartInfo 
            {
                                //配置帶參數(shù)啟動的命令行參數(shù)
                Arguments = CommandLineFormatter.PrepareFileName(executable) + " " + arguments, 
                CreateNoWindow = true,
                FileName = text, 
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                WorkingDirectory = Application.dataPath + "/..", //設(shè)置工作目錄
                UseShellExecute = false
            };
            if (setMonoEnvironmentVariables)
            {
                string value = ManagedProgram.PathCombine(new string[]
                {
                    monodistribution,
                    "lib",
                    "mono",
                    profile
                });
                processStartInfo.EnvironmentVariables["MONO_PATH"] = value; //按需設(shè)置環(huán)境變量
                processStartInfo.EnvironmentVariables["MONO_CFG_DIR"] = ManagedProgram.PathCombine(new string[]
                {
                    monodistribution,
                    "etc"
                });
            }
            if (setupStartInfo != null)
            {
                setupStartInfo(processStartInfo); // 進(jìn)程信息配置完成的回調(diào)
            }
            this._process.StartInfo = processStartInfo;
        }

        private static string PathCombine(params string[] parts)
        {
            string text = parts[0];
            for (int i = 1; i < parts.Length; i++)
            {
                text = Path.Combine(text, parts[i]);
            }
            return text;
        }
    }
}
Tips : 更上層的調(diào)用見:**UnityEditor.Scripting.Compilers.ScriptCompilerBase**

雜項

  1. 可以發(fā)現(xiàn)在 Windows 下Unity 是調(diào)用了 mcs.exe 進(jìn)行的腳本編譯。


  2. 并不是所有遇到的 API 都能在官方 Manual 找到单默,譬如這個 EditorUtility.CompileCsharp
  3. 博客中提供的 Demo 現(xiàn)在測試時會報錯 【CS0518 】:
  4. 經(jīng)過谷歌碘举,找到解決方案:Compiler Error CS0518 | Microsoft Docs
    亦即是:添加上對 mscorlib.dll 的引用即可。
  5. 經(jīng)過追源碼搁廓,這個 EditorUtility.CompileCsharp 中ApiCompatibilityLevel
    固化為ApiCompatibilityLevel.NET_2_0_Subset,這一點需要留意引颈,因為筆者目前遇到的情況表明子集缺少一些API,包含但可能不限于部分 IO 和 Regex 相關(guān)的API境蜕。


擴展閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市粱年,隨后出現(xiàn)的幾起案子售滤,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件完箩,死亡現(xiàn)場離奇詭異赐俗,居然都是意外死亡,警方通過查閱死者的電腦和手機弊知,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門秃励,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吉捶,你說我怎么就攤上這事夺鲜。” “怎么了呐舔?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵币励,是天一觀的道長。 經(jīng)常有香客問我珊拼,道長食呻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任澎现,我火速辦了婚禮仅胞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘剑辫。我一直安慰自己干旧,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布妹蔽。 她就那樣靜靜地躺著椎眯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胳岂。 梳的紋絲不亂的頭發(fā)上编整,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機與錄音乳丰,去河邊找鬼掌测。 笑死,一個胖子當(dāng)著我的面吹牛产园,可吹牛的內(nèi)容都是我干的汞斧。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼淆两,長吁一口氣:“原來是場噩夢啊……” “哼橄仆!你這毒婦竟也來了砂豌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎魂迄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年赵颅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片暂刘。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡饺谬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谣拣,到底是詐尸還是另有隱情募寨,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布森缠,位于F島的核電站拔鹰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏贵涵。R本人自食惡果不足惜列肢,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宾茂。 院中可真熱鬧瓷马,春花似錦、人聲如沸跨晴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坟奥。三九已至树瞭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間爱谁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工孝偎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留访敌,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓衣盾,卻偏偏與公主長得像寺旺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子势决,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容