编写高性能对垃圾收集友好的代码

RodTancred 7年前
   <p>若你想让你的游戏有60桢/秒的体验,你必须要做的就是在16浩渺内完成所有事:子弹运动,创建实体,控制碰撞,轨迹,变换场景,控制输入,播放音效。主流的游戏循环中,你需要做到尽可能高效。即便在30桢/秒的体验中,你也只有32毫秒去完成这一切。特别是当你想要让游戏更加丰满时,速度与效率会显得尤为重要。</p>    <h2>垃圾回收究竟是什么?为何要关注垃圾回收?</h2>    <p>如果你开发的游戏在同一时间内发生了许多事,例如每秒发射5次导弹的武器(一把有着极高射速的非凡武器)。你很快就会发现原型构建及其后的垃圾回收将严重拖累性能。</p>    <p>当你的项目变得越来越复杂,在构建新事物时,你将会感受到明显的延迟,特别是当你设置复杂场景与音效时(即便你已经缓存了静态资源)。创建多个对象同样也会影响垃圾回收。</p>    <p>像其他解释型语言一样JavaScript把你从内存管理中解放出来。你可以随意创建对象而不用去考虑追踪的问题。浏览器(实际上是运行在浏览器环境中的js虚拟器)将会周期性运行并清除你不用的代码。这部分系统就是垃圾回收(garbage collector)简称GC,你可以把它想象为终极女佣。</p>    <p>有赖于浏览器,你使用的大量的对象可以在垃圾回收机制下在10到2000毫秒内被清除。该机制花费的时间取决于,究竟有多少代码需要检查,有多少对象需要清除。如果你在写一个有许多独立运行的对象的游戏,例如作战类游戏。最好的情况是可察觉的卡顿,而最坏的情况则是大量的卡顿毁掉了体验。</p>    <h2>有好的垃圾回收代码</h2>    <p>大多数情况有赖于垃圾回收机制,我们可以很轻易编写代码。唯一需要注意的就是不要应用那些你已经不需要使用的对象。例如下面的案例:</p>    <pre>  <code class="language-javascript">function test()  {     var myString = 'a string';  }    test();</code></pre>    <p>在函数 test 执行之后,变量 myString 将会被标记为闲置的等待释放/删除。因为函数创建了一个可声明变量的作用域。在作用域中,浏览器会为开辟空间存放变量 myString 直到函数不再被使用,作用域中的一切都将被回收。在其后的某个时刻(由浏览器自行计算),GC将会执行,变量 myString 会“真地”被移除内存得到释放。</p>    <p>当然如果还有引用,GC将不会回收变量,如:</p>    <pre>  <code class="language-javascript">var another = null;    function test()  {      var str = 'A string I am';      another = str;  }        test();</code></pre>    <p>在上例中,全局作用域中的另一个变量在函数内引用了 str ,因此垃圾回收器将不会回收 str 。</p>    <p>再看看别的例子,当你不适用 var 关键字时,js会将其理解为一个全局变量:</p>    <pre>  <code class="language-javascript">// var b = null; // 在外部声明    function test()  {      var str = 'A string I am';      b = str; // 没有var 会被理解为全集变量  }        test();</code></pre>    <h2>如何真正地删除变量?</h2>    <p>那么问题来了,如何真实地了解到变量是否被删除,即被垃圾回收器清理了?</p>    <pre>  <code class="language-javascript">var s = { data: 'test' };  delete s.data;</code></pre>    <p>首先,JavaScript提供了 delete 关键字,所以可以用它来实现?不幸的是,不能。 delete 是用来清除对象属性的。显然这是一个间接清除对对象/变量清除引用的方式,但这并没有直接删除变量。当你想要把一个对象的属性变成undefined而非null时,该方法还是挺有用的:</p>    <pre>  <code class="language-javascript">var s = { data: 'test' };  delete s.data;</code></pre>    <p>s.data现在变为了 undefined (同时也被垃圾回收器标记为“去除”)</p>    <p>当你使用同样的方式用 delete 去删除一个变量时将会失败:</p>    <pre>  <code class="language-javascript">var m = 'test';  delete m; // 默认返回 false (不允许操作)  m === 'test'; // true - oops, 依旧是那个值</code></pre>    <p>delete 很好用,但对于一个变量(这里指基本类型)则会失效。因此引用并不会被清除,内存也不会被垃圾回收器回收。使用 delete 将返回false来表示这个变量不会被删除。</p>    <p>删除变量真确的方式是:把变量设为null,之后垃圾回收器就会去做他该做的事。</p>    <pre>  <code class="language-javascript">var m = 'test';  m = null;  m === 'test'; // false</code></pre>    <p>该方法对属性与对象同样适用:</p>    <pre>  <code class="language-javascript">var s = { data: 'test' };  s.data = null;  // not required  s = null;   // 该操作同时也会清除s.data</code></pre>    <p>其后数据将自动清除,因为没有变量引用它了。</p>    <p>(译注:这里清除的是变量中存放的数据)</p>    <h2>避免创建对象</h2>    <p>当然,降低垃圾回收最简单的方式是不要去创建对象。最直接的方式就是使用 new 关键字。</p>    <pre>  <code class="language-javascript">var newObject = new MyObject();</code></pre>    <p>但是还有一些更简洁的方法:</p>    <pre>  <code class="language-javascript">var a = [];   // 创建一个数组  var t = { };  // 创建一个对象</code></pre>    <p>需要记住的是 function 也会被当作对象处理:</p>    <pre>  <code class="language-javascript">function getCompare()  {      return function(a, b) { return a < b; }  }</code></pre>    <p>这段代码在每次调用时都会生成一个新的(函数)对象。在JavaScript中由于函数是一个对象,所以垃圾回收器也能用同样的方式处理。</p>    <p>另一种产生对象的方式是使用函数:</p>    <pre>  <code class="language-javascript">function getResult(}  {      return { result: true, value: 'test' }; //调用时创建新的对象  }</code></pre>    <p>最后,有一个藏得很深的方式:</p>    <pre>  <code class="language-javascript">var b = a.slice(1); // 创建一个新的完全复制的数组</code></pre>    <p>Array.slice 方法在每次调用时会创建一个新的对象/数组。并不存在无痛地有效删除数组中某个对象的方法。诚然现在浏览器在这方面做了很多优化,但频繁进行这样的操作对性能仍是一种挑战。(在 Playcraft 引擎采用双向链表作为替代,详见gamecore.js)</p>    <h2>可控的垃圾回收</h2>    <p>即便你降低了你创建对象的数量,你依旧在消耗内存。你可以使用一个相当简单的工具来了追踪你消耗的内存即因而带来的垃圾回收工作消耗的内存。</p>    <p>Chrome提供了一个观测JavaScript堆(分配给JavaScript对象的内存)状态的方式,你需要在命令行输入一下代码来保证其能够运行:</p>    <pre>  <code class="language-javascript">chrome --enable-memory-info</code></pre>    <p>如果你想要永久的使用这个工具,你可以这样做:</p>    <pre>  <code class="language-javascript">do shell script     "\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"             --enable-memory-info"</code></pre>    <p>保存这段脚本并将其作为你新的Chrome launcher。</p>    <p>一旦你启动了Chrome 的内存分析,你可以访问如下两个属性:</p>    <pre>  <code class="language-javascript">window.performance.memory.totalJSHeapSize;  // 当前使用的堆内存  window.performance.memory.usedJSHeapSize; // 全部的堆内存</code></pre>    <p>这两个值表示当前有多少内存被分配给JavaScript,其中有多少被所有的变量/对象使用。如果你规律地输出被使用的堆,你可以了解到你游戏的内存使用情况。</p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007887891</p>    <p> </p>