суббота, 5 января 2019 г.

Как я разбирал Chronometer и нашел “пасхальное яйцо”

Кто чем занимался 31 декабря, а я разбирал класс Chronometer из стандартной библиотеки Андроида. Как раз в 11:30 pm закончил. И вот хочу поделиться, что нового я узнал, плюс порассуждать о своем прогрессе в обучении программированию в целом.

Я уже писал, что в прошлом году, наконец, решил серьезно учить программирование. Вначале цель была – “выпускать собственные приложения”, но постепенно, осознав размах задачи, я поменял ее на более реалистичную – “понимать код, который пишет мой брат”. Искать работу разработчиком я не собираюсь, что сильно ограничивает набор тем и инструментов для изучения.

Моему брату надоели баги в кросс-платформенном движке cocos2d-x, на котором написаны наши приложения, поэтому он переписывает код на нативную для Андроида Java. Я установил Android Studio и принялся тоже учить Java и Android по разнообразным туториалам и официальной документации, чередуя теорию с практикой, то есть с написанием собственных небольших проектов.

Во всех наших приложениях есть “Игра на время”, где надо дать как можно больше правильных ответов за одну минуту. Поэтому я счел необходимым научиться вставлять в свои проекты отсчет времени. Разумеется, такие штуки не придумать самому, а надо искать в интернете готовые решения и подгонять их под свои задачи. Решений оказалось много разных. Я скопировал некий код в свой математический квиз, потратил пару часов, чтобы заставить его правильно работать, и еще пару часов, чтобы разобраться, как именно он работает:


Цифры “9.9” в верхнем правом углу и есть обратный отсчет времени: у вас осталось меньше десяти секунд, чтобы перемножить 27 на 22.

Для другого своего проектика “Отгадай цифры” я попробовал другое решение, которое подсмотрел в одном из чужих проектов на github, – готовый андроидный виджит Chronometer (в toolbar справа):


Брат посмотрел на мои поделки и сказал: “А вот теперь разберись, как этот хронометр работает?”. “Как же я могу это сделать?” – спросил я. Я уже знал, что в Android Studio можно щелкнуть на любой класс, зажав Cmd, и он откроет определение этого класса, но для стандартных андроидных классов он открывал мне только заголовки методов, без их внутренностей.

Брат покачал головой, насколько можно быть тупым, чтобы полгода учить Android, и даже не установить его исходный код, который, как оказалось, полностью открыт, и может быть легко дозагружен из интернета. После чего на меня напало воодушевление: я же могу сейчас почитать код, который настоящие гуглеры пишут, а не какие-то чудики с гитхаба. И 31 декабря я сел разбирать Chronometer.

В моем ученическом представлении разобрать класс – это прочитать его код, понять все слова и что какой метод делает. Код у Chronometer – всего 400 строчек с пробелами и комментариями.

Тут мне пришла аналогия из химии – разбор полного синтеза. Понять все стадии, что на них происходит, какие реагенты используются и почему. И как понимание отдельных стадий не приведет к пониманию стратегии, почему именно таким путем синтезировали молекулу, и тем более не позволит самому придумывать такие синтезы, так и мой дилетантский разбор класса не позволит писать такой же код. Но без знания реакций и базовых химических концепций, тем более не удастся ничего сделать. На то она и учеба.

Первым делом я увидел, что Chronometer наследует от TextView. Год назад я не понял бы, что это значит. Поэтому я постараюсь писать как можно проще, а если вставлять куски кода, то только для иллюстративных целей, понимать его необязательно. В общем, Chronometer – это обыкновенный кусок текста на экране, но еще с несколькими дополнительными методами, которые и определяют, что в этом тексте будет написано.


public class Chronometer extends TextView {


И более того, сам механизм обновления текста через определенные промежутки времени такой же, как и в моем математическом квизе. Те самые Runnable, которые я два часа пытался понять:


    private final Runnable mTickRunnable = new Runnable() {
        @Override
        public void run() {
            if (mRunning) {
                updateText(SystemClock.elapsedRealtime());
                dispatchChronometerTick();
                postDelayed(mTickRunnable, 1000);
            }
        }
    };


А вот время они берут разное. У меня было System.currentTimeMilles() – число миллисекунд между текущим временем и 1 января 1970 года по Гринвичу. А в Chronometer оказалась другая функция SystemClock.elapsedRealTime(), которая возвращает число миллисекунд с последней загрузки устройства, что намного логичнее, так как я не хочу, чтобы автоматическая смена часового пояса или перевод часов самим пользователем сбил мой хронометр на пару часов.

Я стал читать про разные системы времени, встретил незнакомый мне модификатор native у elapsedRealTime(), но брат, с которым я делился своими разысканиями, сказал мне не лезть в такие дебри. Native – обращение к коду, написанному не на Java. Android как операционная система является разновидностью Linux, поэтому такие фундаментальные вещи, как системное время достается через код на C.

То же самое он сказал мне о словечке synchronized, которое используется как модификатор метода updateText(long now) в Chronometer. Что это нечто, связанное с многопоточностью и что мне о таком рано знать. И вообще непонятно, что оно в хронометре делает, потому что вывод текста на экран – это основной поток. Брат решил, что это баг.

А вот следующая функция isTheFinalCountDown() багом не была:


/**
     * @return whether this is the final countdown
     */
    public boolean isTheFinalCountDown() {
        try {
            getContext().startActivity(
                    new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw"))
                            .addCategory(Intent.CATEGORY_BROWSABLE)
                            .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
                                    | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT));
            return true;
        } catch (Exception e) {
            return false;
        }
    }


Из официальной документации класса Chronometer невозможно догадаться, что она делает. Проверят финальный ли это отсчет – что это значит?

Ответ есть в исходном коде. Моих познаний в Android уже было достаточно, чтобы понять, что при вызове этот метод попытается открыть в браузере YouTube, а там песню группы Europe “The Final Countdown” 1986 года. Популярную, судя по 500m+ просмотрам.

Зачем? Этим вопросом задавался не только я. Поэтому нашел обсуждение этого метода на Reddit, где все решили, что это так называемое “пасхальное яйцо” от разработчиков Google, добавленное, кстати, весьма недавно, в API 26 Android Oreo, вышедшей в 2017 году. Именно, чтобы заставить недоумевать, а потом улыбнуться тех, кто будет читать исходный код класса Chronometer.

Пользы от такого знания никакой, но почему-то именно в этот момент я решил, что не зря читал этот код. Намного полезнее мне было узнать, почему комментарии там написаны в таком странном формате с аннотациями типа @return. Официальная документация автоматически генерится из них с помощью программы Javadoc. То есть к исполняемому коду этот @return отношение не имеет, но позволяет писать комментарии к каждому классу и методу один раз в коде, а html для сайта со всеми гиперссылками получится автоматически.

А глобальный вывод в том, что в классе Chronometer нет ничего магического, чего я не смог бы сам воссоздать через TextView или собственный класс. Тем более оказалось, что в Chronometer нельзя задать формат, который показывал бы десятые доли секунды, как мы делаем в своих эппах. А когда знаешь, как он работает, то можешь и такую функциональность добавить и многое другое. У меня есть желание еще какой-нибудь несложный класс разобрать. Это как читать настоящие статьи вместо учебника по химии.

Но если продолжать сравнения, я со своими недоприложениями, которые нельзя в таком виде публиковать, и знанием, что каждое слово значит в Java, сейчас нахожусь на той же стадии обучения, где находится химик, который знает кучу химических реакций, умеет ставить их в лабе, но не умеет делать хроматографию и разбирать ЯМР. Он может побеждать на олимпиадах, сдавать экзамены и пройти практикум по органике на третьем курсе СПбГУ, но в реальном синтезе он не уйдет дальше первой стадии. Ни продукта выделить не сможет, ни понять, какая его структура и насколько он чистый.

Можно заявить, что это уже не органика, а аналитика. Настоящая химия – это смешивать реагенты, а не гонять колонки. Но это не так. Вот и в программировании настоящая работа для меня еще и не начиналась. Я все еще барахтаюсь на учебном уровне, где все работает. А дальше надо заниматься скучными, сложными вещами: архитектурой, дизайном, тестированием.

Научиться переворачивать свое приложение из portrait в landscape, чтобы все сохранилось и не поплыло, кажется такой мелочью по сравнению с логикой игры. Но мой брат эту логику может написать за пять минут, а сидит и думает он как раз над такими вещами, как все лучше сделать, чтобы все работало быстро на всех устройствах.

Помучившись, я переворачивание сделал, но я сам знаю, что у меня есть баг: эпп вылетает, если перевернуть во время “экрана победы”. И мне под силу этот баг исправить, но так не хочется. Я же не собираюсь этот проект публиковать.


Зато сегодня с утра за полчаса добавил в toolbar сердечки-жизни, которые гаснут с заполнением каждого ряда. Вот такое я уже умею. Научусь и с багами воевать. Когда-нибудь. Главное не грузить себя до потери всякой мотивации учиться.

Комментариев нет: