Разработка под Android, грабли большие и не очень

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


    Грабли большого размера


    Как это происходит: Вы выкладываете свое приложение размером в 10 мегабайт в Android Market, и вам приходит гневное письмо от пользователя — «Как можно делать такие ужасные программы, она даже не хочет устанавливаться на мой телефон, она меня обманывает. У меня было 15 мегабайт свободно на телефоне, я попытался установить ваше приложение в описании которого написано что оно занимает 10 мегабайт — она мне сказала что места не достаточно, я удалил ненужные программы и у меня 25 мегабайт свободно — все равно говорит не достаточно места, я удалил программы которые нужны мне и стало 35 мегабайт свободного места — она все равно не хочет устанавливаться. WTF! Зачем вы пишите неверный размер своей программы!» (мой вольный перевод с английского реального письма).
    Причина: Для установки приложения размером X мегабайт, нужно от 2X до 4X свободного места.
    Почему: Сначала файл приложения скачается на устройство и займет там X мегабайт места, потом он будет распакован для проверки — еще от X до 2X мегабайт в среднем, потом он будет перемещен в приложения в файловой системе еще X мегабайт.
    Что делать: Если вы делаете приложение больше 5 мегабайт размером, то будьте готовы писать функциональность для скачивания и размещения на флэшке части ресурсов, а так же проверки на возможное отсутствие флэшки или удаление част ваших файлов, причем в процессе работы приложения.

    Грабли со встроенной флэш памятью


    Как это происходит: Вы делаете приложение интенсивно работающие с файлами. Замеряете время работы и получаете вполне приемлемые цифры — оверхэд файловой операции порядка 5 миллисекунд. Приложение работает быстро, вы довольны. После установки ряда приложений, вы замечаете что время работы вашей программы выросло в 5 раз.
    Причина: Особенности реализации файловой системы — чем меньше свободного места на флэшке, тем больше времени занимают операции с файлами. Время доступа вырастает с 5 мс до 300мс, в теории время доступа может вырасти до 200 раз.
    Почему: Файловая система пытается сохранить жизнь вашей флэшки, поэтому отслеживает интенсивные операции с файлами и часто востребованные файлы пытается перемещать с места на место. Чем больше файлов, и чем меньше свободного места, тем больше ей на это нужно времени.
    Что делать: Вместо множества маленьких файлов, один большой файл с индексами. И будем надеяться что никто не будет использовать ваше приложение месяцами и не убьет свою флэшку.

    Грабли ограниченные


    Как это происходит: Вам приходит прототип нового планшета на Android c размером экрана 1280 на 800 с целым гигабайтом памяти на борту. Вы в радостном предвкушение начинаете портировать ваш квест — загружаете первый полноэкранный задник, второй, шестой — и получаете OOME (OutOfMemoryError).

    Причина: 16Мб памяти на процесс (24Мб на N1/Desire), это включает себя «нативную кучу», где хранятся Bitmap (и OpenGL текстуры)
    Почему: В лучших традициях демократии диктатуры — лучше пускай всем приложениям будет доступно по немногу памяти, чем дать возможность одному приложения использовать всю память, из-за чего могут быть проблемы у других приложений. Ну и много других нюансов.

    Что делать: 640кб 24 Мб памяти хватит всем

    Чувствительные грабли


    Как это происходит: Вы создали ваше приложение, добавили туда классную музыку, добавили туда управление с использование акселерометра. Пользователи жалуются что управление ужасное.
    Причина: При проигрывания музыкального трека на максимальной громкости, особенно где много басов, при неподвижном телефоне значение поворота снятые со сенсоров меняются от -10 до +10 градусов.
    Почему: Динамик слишком близко расположен к сенсорам
    Что делать: Не давать выставлять музыку на максимальную громкость, не использовать треки в стиле heavy metal, учитывать тремор сенсоров

    Грабли с поддержкой MultiTouch


    Как это происходит: Вы хотите сделать управление, например как в игре iDracula
    Управление

    И не можете.
    Причина: Вы получаете координаты касания парами — две X координаты, две Y координаты. Узнать к какому касанию принадлежит та или иная координата вы не можете. В результате, если вы проводите пальцами навстречу друг другу по какой-то оси, и то после совпадения координат по данной оси вы уже не можете определить к какому нажатию относиться какая координата. Я понимаю что описание достаточно туманное — поэтому можете посмотреть вот это видео —
    Либо скачать на ваш телефон приложение MultiTouch Visualizer.
    Почему: Особенности реализации сенсора на некоторых моделях телефонов — Nexus One, Desire.
    Что делать: Отказаться от подобного управления, либо разносить его по Y координате.

    Магические грабли


    Как это происходит: Вы пишите следующий код

    while(true) {
    Task task = queue.getTaskBlocking();
    task.execute();
    }


    Где Task это достаточно «жирный объект», которые выделяет много памяти. И обнаруживаете, что памяти используется больше чем планировалось. Посмотрев результирующий байт код вы обнаруживаете, что это компилируется совсем не в такой код который вы хотели, и именно в код

    Task task;
    while(true) {
    task = queue.getTaskBlocking();
    task.execute();
    }


    В результате вы имеет два экземпляра Task в памяти. Не беда думаете вы, мы сейчас явно укажем что один экземпляр можно удалить из памяти.

    while(true) {
    Task task = queue.getTaskBlocking();
    task.execute();
    task = null;
    }


    Но посмотрев на результирующий байт код вы видите, что оптимизатор решил что так как переменная out of scope, выкинул ваше явное зануление объекта, и у вас все равно два экземпляра в памяти. И тогда рождается магия.

    class Magic {
    static private int n = 0;
    static public void doMagic(Object obj) {
    if (obj != null) n++;
    }
    static public int getMagic() { return n; }
    }
    while(true) {
    Task task = queue.getTaskBlocking();
    task.execute();
    task = null;
    Magic.doMagic(task);
    }


    Причина: Бага :)
    Почему: // TODO: Найти разработчика отвечающего за это, пытать и узнать почему.
    Что делать: Магия
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 58

      0
      А какой байт-код имеется в виду? java или dex?
        +2
        dex, я правда не копал именно этот баг сам, за объяснения почему так спасибо Максиму из российского офиса Google
      • UFO just landed and posted this here
          +3
          Учитывая что множество моделей никогда не будет обновлено на 2.2, то даже через год, скорее всего, модели с прошивкой < 2.2 будут составлять не менее 30% рынка.

          Т.е. это безусловно приятно, но в ближайшее время не имеет коммерческой ценности. Все равно нужно реализовывать докачку самостоятельно.
          +4
          Спасибо, Андрей. Хорошая подборка. По поводу «магии» не совсем понял

          В результате вы имеет два экземпляра Task в памяти


          В яве — не экземпляры, а ссылки. Видимо, имеешь ввиду, что GC сразу его не убивает? Так это правильно — даже если сделаешь task = null, GC не сразу будет вызван, как и в случае out of scope. Так что это не баг, а скорее особенность явы
            +2
            Ну одно другому не противоречит, ссылки на экземпляры :)

            В первом случае, у нас первый экземпляр Task будет удален GC только после создания второго экземпляра Task. В магическом случае, мы даем GC шанс его удалить в процессе создания нового объекта. Если у нас объекты занимают приличное количество памяти это очень критично.
              0
              queue.getTaskBlocking().execute()?
                +1
                Я думаю будет тоже самое. Ява все равно создаст байт код соответствующий

                Task tmp = queue.getTaskBlocking()
                tmp.execute()


                А dex опять будет переменная вынесена за пределы цикла.

                0
                Не совсем понимаю работу магического класса. Видимо, где-то ошибка. Почему не используется метод getMagic()? В метод Magic.doMagic(task) передается null — это факт. Тогда к чему это условие? if (obj != null) n++;
                  +1
                  На оба вопроса — чтобы обмануть оптимизатор, чтобы тот не повыкидывал код.
                  +1
                  Минутку,
                  Task task;

                  не создаёт экземпляра, а просто объявляет переменную под указатель на этот экземпляр.

                  Тут компилятор всё делает правильно — выделение памяти под указатель внутри цикла множество раз менее эффективно, чем выделение памяти под указатель один раз, до цикла. Кроме того, такая оптимизация упрощает жизнь для GC — он будет срабатывать на объект-без-ссылок, а не на объект-который-больше-не-понадобится
                    +2
                    Не в этом дело.
                    Смотрите, прошли по циклу один раз, создали таск. закончили цикл.
                    Пошли по циклу второй раз: переменная таск указывает на экземпляр таска и поэтому таск не может быть уничтожен. До тех пор, пока не выполнится конструктор второго таска и переменная не будет переписана. То есть во время выполнения второго (и каждого последующего) прохода по циклу есть момент, когда в памяти гарантировано два экземпляра объекта таск.
                    Если же удалось бы обнулить переменную в конце цикла (или как в неоптимизированной версии — выход за границу видимости тоже равен освобождению указателя), то у сборщика был бы шанс утилизировать память первого таска либо до конструкции второго, либо во время.
                      +1
                      Ваша правда.

                      Действительно, момент не очевидный.
                      В том плане, обычному, не андроидному, джависту подобное и в голову не придёт. Потому оно и работает подобным образом, подозреваю.
                    0
                    Собственно, сам объект может создаться только из конструктора.
                    Единственное место, где может быть конструктор — где-то в недрах queue.getTaskBlocking().
                    Или этот вызов возвращает полную копию внутреннего объекта Task?
                  0
                  Я в яве вообще ничего не понимаю, но вот такое решение не должно убрать проблему с out of scope?

                  Task task;
                  while(true) {
                  task = queue.getTaskBlocking();
                  task.execute();
                  task = null;
                  }
                    0
                    Разве в прошивке 2.2 не поправили проблемы с мултитачем? Я сейчас посмотрел на своём N1 с 2.2, кажется, проблемы больше нет или я что-то неправильно сделал.
                      0
                      На моем N1 2.2 проблема осталась.
                        0
                        Не в прошивке проблема — проблема в датчиках
                        0
                        На моем N1 2.2 проблема осталась.
                          0
                          небольшой оффтоп:
                          что означает «оверхэд»? гугль молчит, а слышу термин часто…
                            +2
                            В данном контексте, это некоторое время которое требуется операционной системе чтобы «подготовиться» к считыванию данных.

                            Если на пальцах, допустим физически считать данные с флэшки у нас занимает по времени X мс, то для того чтобы это сделать в программе нам нужно X+Y мс, где Y и есть «оверхэд». Причем, часто данный оверхед не завистит от размера данных.

                              0
                              пасиб
                              +2
                              overhead overheads 1) непроизводительные издержки; накладные расходы
                                0
                                вот дурья башка. не додумался перевод посмотреть )))
                                  –3
                                  Это еще и автору по башке на до дать. ;) Все-таки тянуть англизмы без особой нужды — портить родную речь.
                                    0
                                    Это дело привычки, особенно если часто читаешь английскую документацию :) Тот же mutex например: неужели нужно переводить как «взаимное исключение»?
                                      –6
                                      Блин, а что такое мутекс? Первый раз вижу…
                                0
                                «Издержки». Например, если вы шлёте данные в IP-пакете то с точки зрения вашего приложения оверхэд пересылки равен размеру заголовка IP-пакета. Оверхэд — это обычно объём памяти или трафика. Термин очень относительный и субъективный для приложения :)
                                Также есть «алгоритмический» (вольное название) оверхэд: это издержки на выполнение действия. Здесь оверхедом можно считать все операции которые напрямую не приводят к результату но по каким-то причинам требуются.
                                +2
                                >… добавили туда управление с использование акселерометра.…
                                >… значение поворота снятые со сенсоров меняются от -10 до +10 градусов.…

                                Там (допустим, в Desire и Nexus One) два разных датчика — трёхосевой акселерометр и трёхосевой магнетометр. Магнетометр в системе притворяется двумя разными устройствами — собственно магнетометром и датчиком ориентации. На самом деле это один и тот же датчик, просто данные в разном формате выдаются.
                                Соответственно, вы видимо имеете ввиду магнетометр, т.к. с акселерометра никакие градусы не снимешь.
                                Магнетометр оценивает положение телефона относительно магнитного поля (Земли). Соответственно, если рядом есть металлические предметы, это очень сильно влияет. Динамик телефона играющий, даже на максимальной громкости, никаких заметных искажений не вносит (только что проверил). А вот если, скажем, положить телефон рядом с ноутом — искажения будут очень большие (понятно, почему).
                                Вот данные на акселерометр и магнетометр в Desire/Nexus, если интересно:
                                www.bosch-sensortec.com/content/language1/downloads/BMA150_DataSheet_Rev.1.5_30May2008.pdf
                                www.ic-on-line.cn/iol/datasheet/ak8973_4138699.pdf
                                  –1
                                  А есть разница если ноут включен или выключен?
                                    0
                                    Попробовал в двух местах… Нет — разницы нет. Что неудивительно — есть же довольно жесткие требования относительно электромагнитных помех таких девайсов.
                                    Так что изменения показаний исключительно за счёт металлических частей внутри.
                                      0
                                      Магнитометр срабатывает на постоянное магнитное поле, а требования — на переменное электромагнитное. Помеху будет создавать даже просто кусок металла.
                                  0
                                  Неприятность с датчиками я на себе испытал, благо для меня было не критично если пальцы вдруг меняются местами.
                                    +1
                                    (странно — хабр съел сообщение. вторая попытка..)

                                    "… добавили туда управление с использование акселерометра.…
                                    … значение поворота снятые со сенсоров меняются от -10 до +10 градусов. ..."

                                    Там (допустим, в Desire и Nexus One) два разных датчика — трёхосевой акселерометр и трёхосевой магнетометр. Магнетометр в системе притворяется двумя разными устройствами — собственно магнетометром и датчиком ориентации. На самом деле это один и тот же датчик, просто данные в разном формате выдаются.
                                    Соответственно, вы видимо имеете ввиду магнетометр, т.к. с акселерометра никакие градусы не снимешь.
                                    Магнетометр оценивает положение телефона относительно магнитного поля (Земли). Соответственно, если рядом есть металлические предметы, это очень сильно влияет. Динамик телефона играющий, даже на максимальной громкости, никаких заметных искажений не вносит (только что проверил). А вот если, скажем, положить телефон рядом с ноутом — искажения будут очень большие (понятно, почему).

                                    Вот данные на акселерометр и магнетометр в Desire/Nexus, если интересно:
                                    www.bosch-sensortec.com/content/language1/downloads/BMA150_DataSheet_Rev.1.5_30May2008.pdf
                                    www.ic-on-line.cn/iol/datasheet/ak8973_4138699.pdf
                                      +1
                                      Не понял почему вы думаете, что два task'a создалось.
                                      Task task создает неинициализированную ссылку, никакого объекта в памяти не создается. task = queue.getTaskBlocking() создает (или просто возвращает) объект и теперь ссылка указывает на него. Компилятор просто убирает лишнее создание ссылок, т.е. даже экономит силы процессора.
                                        0
                                        По поводу flash, сколько было флешек которые очень активно использовались, ни одна так и померла от активного использования и на windows и на windows mobile.

                                        На какую лучше сменить файловую систему на flash для android что бы всё быстро работало, а не заботилось о том, как бы флешку за 300р. прожила 10 лет?

                                          +1
                                          Имеется в виду внутренняя память.
                                            0
                                            Как правильно уже отметили речь идет о встроенной флэш памяти.
                                              0
                                              Много раз видел как пользователи убивают флэш (на примере обычных USB).
                                              Всякого рода бухгалтера и прочие любители работать с базами.
                                              За три месяца — пол года угробить новую флэшку — раз плюнуть.
                                              0
                                              16 МБ на процесс и это при том что на устройства в среднем ставят 256МБ памяти…
                                              В WM и то в два раза больше можно…
                                                0
                                                из 256 половину съедает радиомодуль, и ОС
                                                по факту там для приложений около 100мб.
                                                  0
                                                  А говорят винда прожорливая…
                                                0
                                                > Я понимаю что описание достаточно туманное
                                                На самом деле, очень хорошее описание — я хоть теперь понял почему так происходит. Ну, можно и вправду разносить управление по противоположным углам экрана в таком случае.
                                                  –5
                                                  Вы выкладываете свое приложение размером в 10 мегабайт в Android Market, и вам приходит гневное письмо от пользователя

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

                                                  Ну и дела…
                                                    0
                                                    а вы дальше удосужились прочитать?
                                                      0
                                                      «Багофича» специфичная для окружения. Запустив инсталлятор размером 10Мб на реальном девайсе с 40+ Мб свободного места ошибки не получим.
                                                      0
                                                      Кстати пересобирите iDracula под Android 2.X
                                                      ато он даже не находиться сейчас в маркете хотя я даже его честно покупал =)
                                                        –1
                                                        Спасибо за статью. Очередной раз убедился, что если что-то и писать под Андроид — то это должно быть отдельное, оптимизированное под него приложение, а не просто порт с iOS.

                                                        Насчет оптимизации компилером — в яве по идее тоже можно выставлять барьеры различного типа, которые будут влиять на видимость переменных и порядок выполнения кода. Вероятно, при их использовании не нужно будет прибегать к магии :)

                                                        Сам за Android пока браться планирую в очень очень отдаленном будущем, но документацию переодически внимательно изучаю. Хотя у меня и есть что портировать туда с iPhone из популярных вещей (не игры), но пока к сожалению средства NDK не позволяют это сделать без титанического эффорта — на Java это вообще не реализуемо, к сожалению.
                                                          +1
                                                          Если не секрет, что? Интересно было бы знать, что столь сложно реализуется.
                                                          0
                                                          Я думаю, что такие же или схожие проблемы будут и у разработчиков под Windows Phone 7 и других мобильных платформ. Обязательно берём на заметку.
                                                            –1
                                                            >Windows Phone 7 и других мобильных платформ.
                                                            Половины того что описано не будет.
                                                              +1
                                                              Какой именно половины и почемы Вы так считаете?
                                                            +1
                                                            Внимание фокус — простое решение магии:

                                                            Task task=null;
                                                            while(task==null) {
                                                            task = queue.getTaskBlocking();
                                                            task.execute();
                                                            task=null;
                                                            }

                                                            Всё. Ни один оптимизатор ничего не сделает.
                                                            Где выдают призы?
                                                              0
                                                              >Причина: 16Мб памяти на процесс (24Мб на N1/Desire), это включает себя «нативную кучу», где хранятся Bitmap (и OpenGL текстуры)

                                                              насколько я понмю, панять под «честный native»(c/c++) сильно больше, т.е. мегабайт 50 можно занять
                                                                0
                                                                «И будем надеяться что никто не будет использовать ваше приложение месяцами и не убьет свою флэшку.»

                                                                Учитывая, что файловая система оперирует кластерами, вряд ли это убьет флэшку, т.к. кластеры всеравно будут браться из разных мест.
                                                                Но учитывая, опять же, что работа с кластерами, даст ли данный метод решение проблемы? Разве что будет ниже оверхед на открытие файлов.
                                                                  0
                                                                  Вопрос автору — откуда информация про магические грабли?
                                                                  Либо вы что-то не доперевели, либо неправильно поняли.

                                                                  Task task;
                                                                  while(true) {
                                                                  task = queue.getTaskBlocking();
                                                                  task.execute();
                                                                  }

                                                                  В каком именно месте после выполнения этого кода появится второй таск?

                                                                  Единственное, что смущает, так это бесконечный цикл — зачем он тут? Если предполагается, что он выполнится только один раз, то и таск будет всего один, а если несколько раз, то откуда конкретная цифра именно про два таска?

                                                                  Имхо, единственная разница между оптимизированным и изначальным кодом лишь в том, что после выхода из цикла останется один (а не два, как утверждается) сильнодостижимый таск. Решается эта проблема без всякой магии — выносим объявление переменной в место перед циклом, а после цикла её обнуляем.
                                                                    0
                                                                    Данный пункт вызвал много споров. Я сделаю тут одну попытку, объяснить что я имею ввиду. Если не получиться, то наверно продолжать не буду — подожду время для отдельного поста с тестами.

                                                                    Смотрите, для кода который вы процитировали, что происходит.

                                                                    Первая итерация цикла, внутри очереди создается экземпляр класса Task указатель на который записывается в переменную task.

                                                                    Вторая итерация цикла — внутри очереди начинает создаваться второй экземпляр класса Тask, НО по прежнему в переменной task у нас храниться ссылка на экземпляр созданный в первой итерации.

                                                                    Так как переменная task объявлена за пределами текущего блока видимости, то GC не знает о том что данный объект больше никому не нужен и не будет удалять экземпляр объекта созданный на первой итерации.

                                                                    И только после того как создание второго экземпляра объекта будет закончено, и переменная task будет перезаписана GC сможет наконец удалить экземпляр объекта созданного на первой итерации.

                                                                    Для того, чтобы было понятно, попробуйте пройтись данный код поэтапно представляю что таск занимает 10 мегабайт из 16 возможных.
                                                                      +2
                                                                      А, вот теперь понял, что вы имели в виду — в оптимизированном варианте, действительно, будет _момент_, когда в памяти будут одновременно два таска.

                                                                      таск занимает 10 мегабайт из 16 возможных.

                                                                      Абсолютно согласен, что в такой ситуации разница между изначальным кодом и оптимизированным критична.

                                                                      Может, добавите это в пост, а то, боюсь, ещё человек десять зададут схожий вопрос? :)

                                                                  Only users with full accounts can post comments. Log in, please.