Руководство по actionscript. часть 2, стр. 103
Например, рассмотрим следующую версию класса VirtualZoo из нашей программы «Зоопарк». Обратите внимание, что в этой версии объект VirtualPet, создаваемый в конструкторе класса VirtualZoo, не присваивается переменной.
package { import flash. display. Sprite; import zoo.*;
public class VirtualZoo extends Sprite { public function VirtualZoo ( ) { new VirtualPet(«Stan»);
}
При выполнении конструктора класса VirtualZoo из предыдущего кода выражение new VirtualPet («Stan») создает новый объект класса VirtualPet. Однако обратиться к объекту после его создания невозможно, поскольку он не был присвоен никакой переменной. Как результат, созданный объект считается недостижимым и сразу же становится доступным для сборки мусора.
Теперь рассмотрим следующую версию класса VirtualZoo. Особое внимание уделите телу метода-конструктора, выделенного полужирным шрифтом:
package { import flash. display. Sprite; import zoo.*;
public class VirtualZoo extends Sprite { public function VirtualZoo ( ) { var pet:Virtual Pet * new Virtual Pet(«Stan»);
}
}
}
Как и раньше, при выполнении конструктора класса VirtualZoo из предыдущего кода выражение new VirtualPet («Stan») создает новый объект класса VirtualPet. Однако на этот раз созданный объект класса VirtualPet присваивается локальной переменной pet. В конструкторе класса VirtualZoo к объекту Vi rtual Ре t можно обратиться через эту локальную переменную, поэтому данный объект недоступен для сборки мусора. Тем не менее, как только выполнение конструктора VirtualZoo будет завершено, время жизни переменной pet истечет и обратиться к объекту VirtualPet будет невозможно. Как результат, объект считается недостижимым и становится доступным для сборки мусора.
Далее рассмотрим следующую версию класса VirtualZoo:
package { import flash. display. Sprite; import zoo.*;
public class VirtualZoo extends Sprite { private var pet:Virtual Pet;
public function VirtualZoo ( ) { pet = new Virtual Pet(«Stan»);
}
}
}
При выполнении конструктора класса VirtualZoo из предыдущего кода выражение new VirtualPet («Stan») снова создает объект класса Virtual Pet. Однако на этот раз созданный объект Vi rtual Ре t присваивается переменной экземпляра основного класса программы. По существу, этот объект считается достижимым и поэтому недоступен для сборки мусора.
Руководство по actionscript. часть 2, стр. 104
Теперь рассмотрим следующую версию класса VirtualZoo (снова обратите внимание на фрагменты кода, выделенные полужирным шрифтом):
package { import flash. display. Sprite; import zoo.*;
public class VirtualZoo extends Sprite { private var pet:Virtual Pet;
public function VirtualZoo ( ) { pet = new VirtualPetC’Stan»); pet = new Virtual Pet(«Tim»);
}
}
}
Как и раньше, первая строка конструктора из предыдущего кода создает объект класса VirtualPet и присваивает его переменной экземпляра pet. Однако вторая строка метода-конструктора из предыдущего кода создает еще один объект класса VirtualPet и тоже присваивает его переменной экземпляра pet. Вторая операция присваивания заменяет первое значение переменной pet (то есть животное с именем Stan) новым значением (то есть животным с именем Tim). Как результат, объект VirtualPet с именем Stan считается недостижимым и становится доступным для сборки мусора. Стоит отметить, что, если бы мы присвоили переменной pet значение null (или любое другое допустимое значение), как показано в следующем коде, объект VirtualPet с именем Stan также оказался бы недостижимым: pet = null;
Наконец, рассмотрим следующую версию класса VirtualZoo, в которой определены две переменные экземпляра petl и pet2:
package { import flash. display. Sprite; import zoo.*;
public class VirtualZoo extends Sprite { private var petl:Virtual Pet; private var pet2:Virtual Pet;
public function VirtualZoo ( ) { petl = new VirtualPetC’Stan»); pet2 = petl; petl = nul1; pet2 — nul1;
}
}
}
Как и раньше, первая строка конструктора из предыдущего кода создает объект класса VirtualPet; для удобства назовем его «Stan». Кроме того, первая строка присваивает объект «Stan» переменной petl. Затем вторая строка присваивает тот же объект переменной экземпляра pet2. После выполнения второй строки конструктора и перед выполнением третьей строки программа может обратиться
к объекту «Stan» как через переменную petl, так и через переменную pet2, поэтому данный объект недоступен для сборки мусора. Третья строка заменяет значение переменной petl значением null, так что объект «Stan» оказывается недоступен через эту переменную. Тем не менее к объекту 11 Stan» по-прежнему можно обратиться через переменную pet2, поэтому данный объект все еще недоступен для сборки мусора. Наконец, четвертая строка заменяет значение переменной pet2 значением null. Как результат, к объекту «Stan» больше нельзя обратиться через какую-либо переменную. «Бедный» объект «Stan» оказывается недостижимым и официально становится доступным для сборки мусора.
Руководство по actionscript. часть 2, стр. 105
В языке ActionScript существует два особых случая, когда являющийся достижимым объект становится доступным для сборки мусора. Соответствующую информацию можно найти в разд. «Приемники событий и управление памятью» гл. 12 и в описании класса Dictionary справочника по языку ActionScript корпорации Adobe.
Алгоритм поэтапных пометок и вычищений
В предыдущем разделе рассказывалось, что объект становится доступным для сборки мусора (автоматического удаления из памяти) в тот момент, когда он оказывается недостижимым. Однако мы так и не выяснили, когда недостижимые объекты удаляются из памяти. В идеальном мире объекты должны удаляться из памяти сразу после того, как они оказались недостижимыми. Однако на практике незамедлительное удаление недостижимых объектов отнимает очень много времени и приводит к тому, что большинство нетривиальных программ выполняется медленно или вообще не отвечает на запросы. Соответственно среда выполнения Flash не удаляет недостижимые объекты из памяти незамедлительно. Вместо этого, она ищет и удаляет недостижимые объекты периодически, в процессе выполнения циклов сборки мусора.
Стратегия удаления недостижимых объектов языка ActionScript называется сборкой мусора с использованием алгоритма поэтапных пометок и вычищений. Рассмотрим, как это работает.
После запуска среда Flash просит операционную систему оставить, или выделить, произвольный объем памяти, в которой будут храниться объекты выполняемой в настоящий момент программы. По мере выполнения программы объекты накапливаются в памяти. В любой момент времени некоторые объекть программы являются достижимыми, а другие могут оказаться недостижимыми Если программа создаст достаточное количество объектов, среда выполнение Flash в конечном счете решит выполнить цикл сборки мусора. В процессе выполнения цикла все объекты, находящиеся в памяти, проверяются на «достижимость». В результате все достижимые объекты помечаются и продолжают храниться в памяти, а все недостижимые объекты вычищаются (удаляются^
из памяти. Однако в большой программе проверка объектов на достижимость может оказаться достаточно длительной. Соответственно среда Flash разбивает циклы сборки мусора на небольшие части, или приращения, которые включаются в процесс выполнения программы. Кроме того, среда выполнения Flash использует механизм отложенного подсчета ссылок для повышения производительности процесса сборки мусора. Информацию о механизме отложенного подсчета ссылок можно найти в статье «The Memory Management Reference: Beginner’s Guide: Recycling» по адресу http://www. memorymanagement. org/articles/ recycle. html#reference. deferred.
В языке ActionScript циклы сборки мусора обычно запускаются тогда, когда объем памяти, необходимый для хранения объектов программы, достигает объема памяти, выделенного среде Flash. Однако ActionScript не дает никакой гарантии относительно времени запуска циклов сборки мусора. Более того, программист не может заставить среду Flash выполнить цикл сборки мусора.
Намеренное освобождение объектов
В языке ActionScript не существует прямого способа для удаления объекта из системной памяти. Удаление любых объектов программы осуществляется косвенно, через автоматическую систему сборки мусора.
Тем не менее, хотя программа и не может самостоятельно удалить объект из системной памяти, она по крайней мере может сделать этот объект доступным для удаления, устранив все ссылки на него. Чтобы устранить все ссылки на объект, мы должны вручную удалить этот объект из любых массивов, содержащих его, и присвоить значение null (или другое подходящее значение) всем переменным, ссылающимся на него.
Тот факт, что объект является доступным для сборки мусора, вовсе не означает, что он будет незамедлительно удален из памяти. Среда выполнения Flash просто получает разрешение на удаление этого объекта, когда (и если) запустится цикл сборки мусора.
Стоит отметить, однако, что создание и последующее удаление объектов из памяти зачастую является менее эффективным решением, чем повторное использование существующих объектов.
^ I Когда это возможно, вы должны стараться повторно использовать существующие объ-м?; 4 „ екты, а не удалять их.
Руководство по actionscript. часть 2, стр. 106
Снова вернемся к программе по созданию виртуального зоопарка и рассмотрим следующий код. В нем описывается метод для кормления животного яблоками.
package { import flash. display. Sprite: import zoo.*;
public class VirtualZoo extends Sprite { private var pet:Virtual Pet;
public function VirtualZoo ( ) { pet = new VirtualPetC’Stan»): pet. eat(new Applet )); pet. eat(new Apple( )); pet. eat(new Apple( )); pet. eat(new Apple( )); pet. eat(new Apple( ));
}
}
}
Обратите внимание, что при каждом кормлении животного предыдущий код создает новый экземпляр класса Apple и передает этот экземпляр в метод eat ( ). Всякий раз, когда завершается выполнение метода eat ( ), все ссылки на переданный в этот метод экземпляр класса Apple теряются, поэтому данный экземпляр становится доступным для сборки мусора. Теперь представьте, что произойдет, если мы скормим животному 1000 объектов класса Apple. Затраты нашей программы на обработку данных будут включать не только затраты на создание объектов класса Apple, но и затраты на их удаление из памяти в процессе сборки мусора. Чтобы минимизировать подобные затраты, лучше создать один повторно используемый объект класса Apple и применять его при каждом кормлении животного яблоком. Следующий код демонстрирует процесс пятикратного кормления животного одним и тем же объектом класса Apple:
package { import flash. display. Sprite: import zoo.*:
public class VirtualZoo extends Sprite { private var pet-.Virtual Pet: private var apple:Apple;
public function VirtualZoo ( ) { pet = new VirtualPetC’Stan»): apple = new Apple( );
pet. eat(apple); pet. eat(apple); pet. eat(apple); pet. eat(apple); pet. eat(apple);
}
}
}
К затратам предыдущего кода можно отнести затраты на создание единственного объекта. При этом не будет вообще никаких затрат на сборку мусора. Этот подход гораздо более эффективен по сравнению с предыдущим подходом, заключавшимся в создании нового объекта класса Apple и его последующем удалении для каждого вызова метода eat ( )!
Деактивация объектов
Как уже известно, удаление всех ссылок на объект делает его доступным для сборки мусора. Тем не менее даже после этого объект продолжает существовать в памяти до тех пор, пока среда выполнения Flash не решит «смести» его в одном из циклов сборки мусора. С того момента, как объект становится доступным для сборки мусора, и до того, как он фактически будет удален из системной памяти, данный объект продолжает получать события и, в случае объектов класса Function, может по-прежнему вызываться функцией setlnterval ( ).
Руководство по actionscript. часть 2, стр. 107
Например, представьте приложение для демонстрации изображений в режиме слайд-шоу, которое использует класс ImageLoader для загрузки изображений с сервера через определенные интервалы времени. Код класса ImageLoader выглядит следующим образом:
package { import flash. events.*: import flash. utils.*:
public class ImageLoader { private var loadlnterval:int;
public function ImageLoader (delay:int = 1000) { loadlnterval = setlnterval(loadlmage. delay);
}
public function loadlmage ( ):void { traceC’Now loading image…»); // Код загрузки изображения // здесь не приводится
}
}
}
Теперь представьте, что основной класс приложения SlideShow реализует функциональность для запуска и остановки слайд-шоу. Для запуска слайд-шоу класс SlideShow создает экземпляр класса ImageLoader, управляющего процессом загрузки изображений. Экземпляр класса ImageLoader сохраняется в переменной экземпляра imgLoader, как показано в следующем коде: «
imgLoader = new ImageLoader( );
Для остановки или приостановки слайд-шоу класс SlideShow удаляет ссылку на экземпляр класса ImageLoader, как показано в следующем коде:
imgLoader = null;
Когда переменной imgLoader присваивается значение null, экземпляр класса ImageLoader становится доступным для сборки мусора. Тем не менее, до тех пор пока этот экземпляр не будет фактически удален из системной памяти, операция загрузки в экземпляре ImageLoader, реализованная на базе функции setlnterval ( ), будет регулярно выполняться.
Руководство по actionscript. часть 2, стр. 108
Это демонстрирует следующий очень простой класс. Он создает экземпляр класса ImageLoader и сразу же удаляет ссылку на созданный экземпляр. Но даже после того, как переменной imgLoader присвоено значение null, сообщение «Now loading image…» продолжает появляться в окне для отладки с интервалом один раз в секунду.
package { import flash. display.*;
public class SlideShow extends Sprite { private var imgLoader:ImageLoader; public function SlideShow ( ) {
// Создаем экземпляр класса ImageLoader и сразу же удаляем ссылку
// на созданный экземпляр
imgLoader = new ImageLoader( );
imgLoader = null:
}
}
}
Если объем памяти, который необходим приложению для демонстрации изображений в режиме слайд-шоу, никогда не достигнет уровня, достаточного для запуска цикла сборки мусора, операция загрузки в экземпляре класса ImageLoader, реализованная на базе функции set I nterval ( ), будет выполняться бесконечно долго. Ненужное выполнение кода в «заброшенном» экземпляре класса ImageLoader тратит впустую системные и сетевые ресурсы и может привести к появлению нежелательных побочных эффектов в программе.
Чтобы избежать ненужного выполнения кода в «заброшенных» объектах, программа должна всегда деактивировать объекты перед тем, как избавиться от них. Деактивация объекта означает его перевод в нерабочее состояние, когда программа больше не может воспользоваться этим объектом для выполнения кода. Например, чтобы деактивировать объект, мы могли бы выполнить одно из перечисленных далее действий или же все действия сразу:
? отменить регистрацию методов объекта для событий;
? остановить все таймеры и интервалы;
? остановить воспроизводящую головку временных шкал (для экземпляров клипов, созданных в среде разработки Flash);
? деактивировать любые объекты, которые станут недостижимыми после того, как сам объект станет недостижимым.
Руководство по actionscript. часть 2, стр. 109
Чтобы объекты могли быть деактивированы, любой класс, экземпляры которого регистрируются для получения событий или используют таймеры, должен предоставлять открытый метод для деактивации своих экземпляров.
Например, наш предыдущий класс ImageLoader должен определить метод, останавливающий внутренний интервал. Сейчас добавим подобный метод и назовем его dispose ( ). Имя выбрано произвольно; с таким же успехом мы могли бы присвоить данному методу имя kill ( ), destroy ( ), die ( ), clean ( ), disable ( ), deactivate ( ) или любое другое имя. Рассмотрим этот код:
package { import flash. events.*; import flash. utils.*;
public class ImageLoader { private var loadlnterval:int;
public function ImageLoader (delay;int = 1000) { loadlnterval =-setlnterval (loadlmage. delay);
}
public function loadlmage ( ):void { traceC’Now loading image…»); // Код загрузки изображения не приводится
}
public function dispose ( ):void { clearlnterval(loadlnterval);
}
}
}
Любой код, создающий экземпляр класса ImageLoader, должен в дальнейшем вызвать метод ImageLoader. dispose ( ) перед тем, как избавиться от этого экземпляра, как показано в следующем коде:
package { import flash. display.*;
public class SlideShow extends Sprite { private var imgLoader:ImageLoader; public function SlideShow ( ) {
// Создаем и сразу же избавляемся от экземпляра класса ImageLoader
imgLoader = new ImageLoader( );
imgLoader. dispose( );
imgLoader = null;
}
}
Сборка мусора в действии
В листинге 14.1 показана очень простая программа, демонстрирующая сборку мусора в действии. Программа создает объект класса Sprite, многократно отображающий сообщение в консоли для отладочной информации. Поскольку объект Sprite достижим только через локальную переменную, он становится доступным для сборки мусора сразу после завершения конструктора основного класса программы. При этом в программе также запущен таймер, который многократно создает объекты, занимая системную память. Когда объем потребленной памяти превысит допустимое значение, запустится сборщик мусора. В процессе сборки
мусора исходный объект Sprite будет удален из памяти и его сообщения перестанут появляться в консоли для отладочной информации.
Руководство по actionscript. часть 2, стр. 110
Листинг 14.1. Сборка мусора в действии
package { import flash. display.*; import flash. text.*; import flash. utils.*; import flash. events.*; import flash. system.*;
public class GarbageCollectionDemo extends Sprite { public function GarbageCollectionDemo ( ) { // Данный объект Sprite будет удален в процессе сборки мусора, когда // объем потребленной памяти превысит допустимое значение var s.-Sprite = new Sprite( ):
s. addEventLi stener(Event. ENTER_FRAME. enterFrameLi stener);
// Многократно создает новые объекты, занимая. системную память var timer:Timer = new Timerd. 0); timer. addEventListener(TimerEvent. TIMER, timerListener); timer. start( );
}
private function timerListener (e:TimerEvent):void { // Создаем объект, чтобы захватить часть системной памяти. Это может // быть любой объект, но объекты класса TextField являются довольно // объемными, new TextFi eld( );
}
// Эта функция выполняется до тех пор. пока объект Sprite не будет
// удален из памяти в процессе сборки мусора
private function enterFrameListener (e:Event):void { // Отображаем объем памяти, занимаемой программой traceC’System memory used by this program: » + System. total Memory);
}
}
К задворкам языка ActionScript
Сборка мусора является чрезвычайно важной частью программирования на языке ActionScript. При создании любой программы на языке ActionScript вы должны принимать во внимание вопросы управления памятью. При создании объекта следует решить, нужен ли он на всем протяжении жизни программы. Если это не так, вы должны включить в программу код, который деактивирует этот объект и впоследствии избавляется от него.
Руководство по actionscript. часть 2, стр. 111
Более полную информацию по сборке мусора в языках программирования можно получить в специальной статье на сайте «Википедии» и на сайте The Memory Management Reference по адресу http://www. memorymanagement. org.
Несколько самостоятельно опубликованных статей, написанных Грантом Скинне-ром (Grant Skinner) и посвященных сборке мусора в языке ActionScript 3.0, можно найти на сайте автора по адресу http://gskinner. com/talks/resource-managemenry и http:// www. gskinner. com/blog/archives/2006/06/as3_resource_ma. html.
В следующей главе мы рассмотрим менее распространенные инструменты языка ActionScript, предназначенные для изменения структуры классов и объектов на этапе выполнения программы.
ПИВА 16
Динамические возможности языка ActionScript
Язык ActionScript изначально задумывался как язык, который позволял бы реализовать простейшее программное поведение для содержимого, создававшегося вручную в среде разработки Flash. В ранних версиях ActionScript большая часть кода представляла собой короткие сценарии, которые реализовывали ограниченную функциональность, по сравнению с кодом, необходимым для создания сложного настольного приложения. По существу, первоначальный набор возможностей языка ActionScript отличался гибкостью и простотой.
Изначально язык ActionScript позволял динамически, на этапе выполнения программы, изменять структуры всех классов и даже любых отдельных объектов. Например, на этапе выполнения программа могла:
? добавлять новые методы или переменные экземпляра в любой класс;
? добавлять новые методы или переменные экземпляра в любой отдельно взятый конкретный объект;
? создавать новый класс с нуля;
? изменять суперкласс выбранного класса.
С появлением языка ActionScript 3.0, приложений Flash Player 9, Adobe AIR и Flex, платформа Flash вышла на новый уровень развития, где сложность программы, разработанной на языке ActionScript, может вполне конкурировать со сложностью полнофункционального настольного приложения. Соответственно, ActionScript, как настоящий язык программирования, взял на вооружение многие формальные структуры, необходимые для разработки крупномасштабных приложений, например формальное ключевое слово class и синтаксис наследования, формальные типы данных, систему предопределенных событий, обработку исключений и встроенную поддержку формата XML. Тем не менее динамические возможности ActionScript остаются доступными и по-прежнему составляют важную часть внутренней структуры языка.
В этой главе рассматриваются приемы программирования с использованием динамических возможностей языка ActionScript. Однако стоит отметить, что гибкость, присущая такому программированию, ограничивает или полностью лишает преимущества проверки типов, с которым мы познакомились в гл. 8. Как результат, большинство сложных программ используют описанные в этой главе возможности лишь изредка, если вообще используют. Например, из более чем 700 классов, определенных в среде разработки Flex, только около 10 классов используют возможности динамического программирования. С другой стороны, даже если вы никогда не будете использовать динамическое программирование в своем коде, информа-
ция, представленная в этой главе, поможет получить более полное представление о внутренних процессах языка ActionScript.
Руководство по actionscript. часть 2, стр. 112
В качестве нашего первого приема динамического программирования мы рассмотрим динамические переменные экземпляра — переменные, добавляемые в конкретный объект на этапе выполнения программы.
Динамические переменные экземпляра
В самом начале этой книги написание программы на языке ActionScript сравнивалось с проектированием и разработкой самолета. Чертежи самолета сравнивались с классами ActionScript, а реальные детали конкретного физического самолета—с объектами ActionScript. По этой аналогии структура каждого конкретного самолета будет гарантированно совпадать со структурой всех других самолетов, поскольку все самолеты построены по одному и тому же чертежу. Для сравнения отметим, что все экземпляры конкретного класса будут иметь одинаковую структуру, поскольку в их основе лежит один и тот же класс.
Что же произойдет, если владелец одного из самолетов решит закрепить собственный фонарь на верху своего самолета? Этот самолет будет обладать специфической характеристикой, не присущей всем остальным самолетам. В языке ActionScript «добавление нового фонаря на конкретный самолет» аналогично добавлению новой переменной экземпляра в отдельно взятый конкретный объект, при этом в остальные экземпляры класса данного объекта этот «фонарь» не добавляется. Переменная экземпляра такого рода называется динамической переменной экземпляра. В сравнении с динамическими переменными экземпляра «обычные» переменные экземпляра называются фиксированными.
Динамические переменные экземпляра могут быть добавлены только в экземпляры классов, объявленных с использованием атрибута dynamic (подобные классы называются динамическими). Динамические переменные экземпляра не могут быть добавлены в экземпляры классов, которые не объявлены с использованием атрибута dynamic (подобные классы называются закрытыми). Подкласс динамического класса считается динамическим только в том случае, если его определение тоже включает атрибут dynamic.
Следующий код создает новый класс Person, который может представлять человека в статистической программе, отслеживающей демографическую«ситуацию. Поскольку класс Person объявлен с использованием атрибута dynamic, в любой отдельный объект класса Person на этапе выполнения программы можно добавлять динамические переменные экземпляра.
dynamic public class Person { }
После того как класс будет объявлен динамическим, мы сможем добавлять новые динамические переменные в любой экземпляр этого класса с помощью стандартного оператора присваивания. Например, следующий код добавляет динамическую переменную экземпляра eyeColor в объект класса Person:
var person.-Person = new Person( ); person. eyeColor = «brown»;
Как уже известно из гл. 8, если бы класс Person не был объявлен с использованием атрибута dynamic, приведенный код вызвал бы ошибку обращения, поскольку в классе Person не определена переменная экземпляра с именем eyeColor. Тем не менее в данном случае класс Person объявлен с использованием атрибута dynamic. В результате, когда среда выполнения Flash попытается присвоить значение несуществующей переменной экземпляра eyeColor, вместо того чтобы сообщить об ошибке, она просто создаст динамическую переменную экземпляра с именем eyeColor и присвоит ей указанное значение («brown»). Обратите внимание, что определение динамической переменной экземпляра в предыдущем коде не содержит и не должно содержать объявление типа и модификатор управления доступом.
I Все динамические переменные экземпляра являются нетипизированными и открытыми.
-
Следующий код, в котором происходит попытка использовать объявление типа при создании переменной eyeColor, вызовет ошибку на этапе компиляции:
person. eyeColor:String = «brown»; // Ошибка! Использование :Stri ng здесь
// недопустимо
Для получения значения динамической переменной экземпляра применяется стандартное выражение доступа к переменной, как показано в следующем коде:
trace(person. eyeColor); // Выводит: brown
Если указанная переменная экземпляра (динамическая или нединамическая) не существует, среда выполнения Flash вернет значение undefined, как показано в следующем коде:
trace(person. age); // Выводит undefined, поскольку объект person // не имеет переменной экземпляра с именем age
Наиболее активное использование динамических переменных экземпляра языка ActionScript присуще среде разработки Flash, где каждая анимированная временная шкала представляется подклассом встроенного класса MovieClip. В среде разработки Flash автоматически генерируемые подклассы класса MovieClip являются динамическими, чтобы программисты могли определять новые переменные в создаваемых вручную экземплярах клипов. Подробное описание этой методики, а также описания других приемов работы с временными шкалами можно найти в гл. 29.