суббота, 5 апреля 2014 г.

Android Studio — два способа изготовить apk-файл

Введение

Комфорт среды программирования во многом зависит от количества второстепенных действий, не относящихся к программированию, которые требуется выполнять программисту, а также глубины изучения самой среды разработки для её правильной настройки под собственные нужды. В этом смысле идеалом, на мой взгляд, является среда Дельфи, заточенная как раз-таки на максимально простую и быструю разработку приложений для Windows (справедливости ради замечу, что с разработкой приложений под Android и другие системы там пока ещё не всё так гладко, как хотелось бы).

В этом смысле в царстве Android нет достаточно комфортных сред разработки, как это, впрочем, и полагается программам в мире бесплатных операционных систем. Ещё до недавнего времени центральным средством разработки считался Eclipse со специализированными плагинами. Однако летом 2013 года Google представил на суд общественности новую IDE — Android Studio, основанную на давнишнем конкуренте Eclipse — системе IntelliJ IDEA. Надо сказать, что несмотря на раннюю версию системы, не вошедшую ещё даже в стадию Beta, она уже превосходит удобством Eclipse.

Тем не менее, программистам всё ещё приходится вникать как в многочисленные нюансы настройки среды, так и в непривычные действия по формированию конечных файлов приложений, готовых к распространению. И это всё на фоне изучения тех специфичных классов, которые приходится использовать, чтобы заставить интерфейс должным образом работать с кодом.

После такого большого вступления, я всего лишь расскажу, как в Android Studio можно генерировать конечные apk-файлы, готовые к распространению, ибо есть нюансы. Надеюсь, моя статья хоть как-то облегчит жизнь начинающим пользователям этой, пожалуй, лучшей на сегодняшний день IDE для программирования под Android.


Ручной способ
  1. В левом нижнем углу Android Studio есть маленькая кнопочка, управляющая отображением специальных ярлыков у краёв экрана. Ярлыки открывают различные вспомогательные окна. Кроме того, при простом наведении на эту кнопочку указателя мыши, появляется список всех этих окон для быстрого открытия нужного. Откроем окно Build Variants и напротив нашего модуля в поле Build Variant переключим режим сборки с debug на release.
  2. В основном меню открываем Build → Generate Signed APK. Возникает сообщение, которое, немного перефразируя, можно перевести примерно так: «Для проектов, собирающихся утилитой Gradle, информация о подписи и порядок подписывания apk-файлов должны быть прописаны в специальном сценарии. Настройте сценарий, как это описано в руководстве пользователя: http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Signing-Configurations. Затем запустите "Gradle assembleRelease", и получите сгенерированный apk-файл в папке build/apk/» Это сообщение как раз настоятельно рекомендует нам использовать второй способ создания apk-файла. Принимаем информацию к сведению, но продолжаем ручной способ, нажав OK.
  3. Открывается окно помощника генерации подписи для apk-файла. Мы уже знаем, что все распространяемые приложения для Android должны иметь цифровую подпись автора, иначе приложение не установится на устройства пользователей. Сгенерированная подпись должна храниться в специальном файле-хранилище, расположенном на вашем компьютере, откуда потом она будет извлекаться для подписания apk-файлов. При этом одно хранилище может хранить несколько подписей для разных apk-файлов. Кроме того, одна и та же подпись может использоваться для подписания разных apk-файлов. Итак, для создания хранилища нажимаем кнопку Create New… и заполняем открывшиеся в окне поля:

    1. Путь для размещения файла хранилища
    2. Пароль и подтверждение для доступа к хранилищу
    3. Имя подписи, по которому она будет вызываться
    4. Пароль и подтверждение для доступа к подписи
    5. Срок действия подписи (по умолчанию 25 лет, оставляем без изменений)
    6. Хотя бы одно из полей сертификата. Обычно заполняют имя и фамилию, город и страну (RU).
       
  4. Нажимаем OK. Окно закрывается, и мы видим, что все поля в предыдущем окне автоматически заполнились введёнными нами данными. Ставим галочку в поле Remember Password, чтобы каждый раз не набирать пароль, и нажимаем OK.
  5. В следующем окне контролируем путь и имя apk-файла. По умолчанию оно равно имени модуля. Включаем галочку Run ProGuard, чтобы наш файл был оптимизирован, и можем даже поменять файл конфигурации proguard-android.txt на proguard-android-optimize.txt для более жёсткой оптимизации (хотя это может быть чревато для некоторых экзотичных смартфонов). Нажимаем Finish и ждём, посматривая в строку состояния.
  6. Когда сборка проекта закончится, отобразится окошко с предложением открыть папку с полученным apk-файлом. Открываем её и видим наш файл.

Скажу сразу, что несмотря на то, что я ставлю галочку в поле Run ProGuard, он у меня почему-то не отрабатывает, и apk-файл остаётся неоптимизированным. Возможно, это пережитки ранней версии Android Studio (у меня версия 0.5.4). Но выйти из положения достаточно просто, включив эту галочку непосредственно в файле сценария утилиты ProGuard. Найдите в окне проекта внутри нашего модуля файл build.gradle. В секции android → buildTypes → release, отвечающей за генерацию релиза, поменяйте параметр runProguard с false на true.

Не забудьте после редактирования файла сценария синхронизировать с ним ваш проект. Для этого в панели инструментов нажмите кнопку Sync Project with Gradle Files (стрелочка вниз из зелёного кружка) или ссылку Sync Now во всплывшей в верхней части исходника жёлтой полосе сообщения. После синхронизации можно снова попробовать собрать apk-файл, начиная со второго пункта нашей инструкции. Только в этот раз вместо генерации нового хранилища используем уже созданное. Все наши пароли и настройки сохранились, поэтому нам только остаётся нажимать во всплывающих окнах OK-Next-Finish. Обратите внимание, что вновь сгенерированный apk-файл стал немного меньше, чем в прошлый раз.


Автоматический способ

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

Для включения автоматического режима надо внести в уже знакомый нам файл сценария build.gradle новую секцию, содержащую в том числе информацию о подписи:

signingConfigs {
    release {
        storeFile file("C:\\Users\\ИмяПользователя\\KEYSTORE.jks")
        storePassword "ПарольХранилища"
        keyAlias "ИмяПодписи"
        keyPassword "ПарольПодписи"
    }

    buildTypes {
        release {
            minifyEnabled true 
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
       }
    }
}

Вместо файла настроек "proguard-android.txt" можно вписать файл ещё более оптимизированных настроек "proguard-android-optimize.txt". Это позволит ещё немного сократить объём apk-файла.

Для внесения этой секции в настройки откроем файл build.gradle, расположенный внутри нашего модуля и закомментируем секцию buildTypes. Вместо неё вставим вышеприведённую секцию. Не забудьте поменять путь к вашему файлу ключей, который вы создали в первой части статьи при ручном создании apk-файла, а также вписать правильное имя подписи и оба пароля.

После внесения изменений в файл нажимаем в панели инструментов кнопку Sync Project with Gradle Files (стрелочка вниз из зелёного кружка) или ссылку Sync Now во всплывшей вверху жёлтой информационной полосе, чтобы синхронизировать изменённые настройки с проектом. Всё, автоматический режим настроен!

Теперь, чтобы иметь возможность генерировать apk-файл без запуска приложения, добавим отдельную команду запуска генерации apk-файла в список конфигураций (комбо-бокс в панели инструментов). Откроем окошко Gradle Tasks, нажав на ярлык Gradle у правого края экрана, или выбрав его во всплывающем списке кнопки, расположенной в левом нижнем углу Android Studio. В секции All tasks раскрываем список задач для нашего модуля и правой кнопкой щёлкаем по задаче assembleRelease. В появившемся контекстном меню выбираем пункт Create… и выбранная задача добавится в список конфигураций.

Теперь мы можем генерировать релиз и без запуска программы на выполнение, просто выбрав в списке конфигураций команду assembleRelease и нажав кнопку Run. Результат автоматического создания apk-файла будет находится в папке build/apk/. Там будут два файла: ИмяМодуля-release-unaligned.apk и ИмяМодуля-release.apk. Первый файл — это полуфабрикат. Он уже подписан но ещё не выровнен. Второй файл — уже выровненный. Это и есть наш конечный apk-файл, который мы уже можем переименовать и свободно распространять.

Замечу, что выравнивание файла гарантирует, что все несжатые данные будут начинаться с позиции, кратной 4 байтам по отношению к началу файла. Это обеспечит оптимизацию производительности программы на устройстве. Когда файл выровнен, Android способен читать информацию из файла по мере необходимости, а не загружать в память весь пакет целиком. В итоге уменьшается объем оперативной памяти, потребляемой запущенным приложением.

четверг, 3 апреля 2014 г.

Округление чисел в Java

Вступление

При выводе результатов числовых расчётов часто бывает ситуация, когда вместо десятичной дроби, например, 3,7, отображается число 3,6999999999999997. Это происходит из-за того, что далеко не все десятичные дроби можно точно представить в двоичном коде. В случае невозможности записать десятичную дробь точно, используется самое близкое к ней значение, которое можно выразить двоичным числом. При обратном переводе в десятичный формат это значение и даёт такие непонятные длинные дроби.


Перевод десятичных дробей в двоичную систему счисления

Невозможность записать некоторые десятичные дроби в двоичной системе (нулями и единицами) можно привести на примере обычного небольшого десятичного числа 20.1. Целая часть десятичных дробей переводится в двоичный вид точно так же, как целые числа, то есть складыванием положительных степеней двойки так, чтобы в итоге получить искомую сумму:



Получаем, что 2010 = 000101002

Как мы видим по таблице, справа налево вес каждого разряда двоичного числа увеличивается вдвое (положительные степени двойки). Набираем число 20, суммируя результаты этих степеней и отмечая единичками те степени, которые мы использовали. Вариант у нас только один – использовать степени 2 и 4, то есть числа 4 и 16, в сумме как раз дающие 20. Отметив их единичками, получаем двоичное число 00010100 или, если отбросить незначащие нули, 10100.

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



Эту таблицу мы можем продолжать вправо до бесконечности, но никогда не наберём полные 0.1. В итоге получаем, что 0,110 ≈ 00011001100112 = 0,099975585937510.

Таким образом, 20,110 ≈ 10100,00011001100112.


Хранение чисел с плавающей точкой в компьютере

В компьютере числа двойной точности (в Java тип Double) хранятся в экспоненциальном (т.н. научном, инженерном) формате. Этот формат, например, записывает десятичное число 20,1 как 2,01*101, или сокращённо, как 2,01E+1. То есть, число после буквы E указывает, на сколько знаков следует сдвинуть запятую вправо (или влево, если число отрицательное) в числе, стоящем перед буквой

Переведённое в двоичную систему число 20,1, выглядящее как 10100,0001100110011 в экспоненциальной форме будет выглядеть как 1,01000001100110011 * 2100, или сокращённо 1,01000001100110011E+100 (двоичное 100 — это десятичное 4, то есть сдвигать запятую в числе 1,01000001100110011 следует на 4 знака вправо).

Для хранения этих чисел в компьютере отводится 64 бита (8 байт). Биты распределяются следующим образом. Для знака числа (плюс или минус) отводится 1 бит, для записи порядка (в нашем случае 100) — 11 бит, а для записи мантиссы (в нашем случае 1,01000001100110011) — 52 бита.

Схематично это выглядит так:



Чтобы иметь возможность записывать отрицательные порядки, за ноль в поле порядка принято число 1023 (в двоичном коде — 01111111111), что делит весь возможный диапазон порядков примерно пополам (от -1023 до 0 и от 0 до 1024). Поэтому наш порядок 100 фактически выглядит в битах, как 10000000011, ведь 1023 + 4 = 1027.

Кроме того, по причинам экономии на аппаратных ресурсах, самого левого бита мантиссы «в железе» не существует, но программно он всегда подразумевается. Этот бит принимается равным 1 во всех случаях, кроме случая нахождения в поле порядка специального числа -1023 (значения битов 00000000000). В нашем случае в поле порядка занесено число 4, а значит, левый бит признаётся единичным, и поэтому у нас в мантиссе фактически лежит лишь дробная часть, то есть число 01000001100110011.

Исходя из вышесказанного, для нашего числа 10100,0001100110011 значения всех 64 реальных битов и 1 подразумеваемого таковы:



Несколько слов про магическое число -1023 (значение битов 00000000000) в поле порядка. Как я уже говорил, это число просто устанавливает подразумеваемый бит целой части мантиссы в ноль. Если все остальные биты мантиссы также будут нулевыми, то хранимое таким образом в компьютере число, очевидно, будет являться нулём.

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

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

Это легко понять, представив, как восстанавливается число из экспоненциального формата. Основание системы счисления (2) возводится в степень порядка и умножается на мантиссу. Самое маленькое число типа Double, которое мы можем так выразить, равно 1 * 2-1022. Ведь подразумеваемый бит равен единице, а значит, мантисса не может быть меньше единицы. Постепенно увеличивая мантиссу с 1 до почти 2 мы можем равномерно заполнить числами весь диапазон до предыдущего порядка 1 * 2-1021. Но у нас всегда остаётся незаполненный диапазон с другой стороны — в сторону нуля. И вот его то мы можем целиком заполнить только делая мантиссу меньше единицы. А для этого надо просто переключить подразумеваемый бит в ноль, что мы и делаем!

Существует ещё одно магическое число в поле порядка — 1024 (значение битов 11111111111). В зависимости от содержимого мантиссы, оно означает либо бесконечность (1 / 0 = ∞) либо неопределённость (неупорядоченное число, не число) (0 / 0 = NaN). Если мантисса нулевая — перед нами бесконечность, иначе — неопределённость.


Правильное округление в Java

Конечно, в Java есть функции, которые умеют округлять числа до нужного знака после запятой. Но иногда может встретится число, которое в десятичном виде округляется в большую сторону, а будучи записано в двоичном виде становится чуть меньше, и уже по правилам должно округляться в меньшую сторону. В качестве примера такого числа можно привести число 13,5465. Очевидно, что оно должно округляться в большую сторону до 13,547. Но при записи в двоичный код число становится равным 13,5464999…, и оно уже должно округляться в меньшую сторону до 13,546.

Но ведь мы мыслим и думаем в десятичной системе счисления, а значит, хотим видеть числа, всегда округлённые в правильную сторону вне зависимости от того, как они там внутри компьютера выглядят. Чтобы провести такое округление и отобразить число правильно, мы должны написать на Java небольшой код:

public BigDecimal roundUp(double value, int digits){
    return new BigDecimal(""+value).setScale(digits, BigDecimal.ROUND_HALF_UP);
} 

Теперь мы можем применить этот метод, например, если программируем под Андроид, при выводе результата result в поле EditText активности:

EditText.setText(String.valueOf(roundUp(result,3)));

Рассмотрим, как же это работает. Метод roundUp принимает подлежащее округлению число value типа double и желаемое количество знаков после запятой digits типа int. Возвращает метод результат в виде числа типа BigDecimal. Тип BigDecimal хранит данные не в виде двоичного представления числа с плавающей запятой, о котором я рассказывал выше, а в виде числа с фиксированной запятой. Число с фиксированной запятой содержит в себе два отдельных целых числа. Это исходное число, но без запятой, и число, содержащее количество знаков после запятой. А целые числа всегда можно записать в двоичный формат абсолютно точно. Так что на выходе мы потенциально можем получить любую точную десятичную дробь.

Внутри нашего метода создаётся объект BigDecimal, который принимает на вход число с плавающей запятой value типа double, и методом setScale округляет его. Но при обычном использовании этого метода число округляется всё по тем же правилам, что приводит к неправильному округлению. В нашем же методе есть одна хитрость. Чтобы округление всегда происходило в нужную сторону, значение на вход метода BigDecimal мы передаём не в виде числа, а в виде строки. На этапе преобразования числа в строку, язык Java сам автоматически округляет число с достаточным количеством знаков, до степени, имеющей смысл (округляя весь хвост, начинающийся с последовательности девяток или нулей, подозревая в этом погрешность). Таким образом, мы подаём на вход BigDecimal строку с уже исправленной погрешностью.

Чтобы перевести число в строку, мы использовали трюк ""+value. При сложении числа со строкой (даже пустой) результат окажется тоже строкой.

Сравним результаты округления без предварительного перевода числа в строку и с переводом.


Вариант 1:

value = 13.5465
digits = 3
return new BigDecimal(value).setScale(digits, BigDecimal.ROUND_HALF_UP);

Результат: 13,546 — неправильно!


Вариант 2:

value = 13.5465
digits = 3
return new BigDecimal(""+value).setScale(digits, BigDecimal.ROUND_HALF_UP);

Результат: 13,547 — правильно.


Вообще, такую удобную особенность преобразования double в строку, как исправление погрешности, можно объяснить тем, что строки обычно используются только для отображения информации, и не участвуют в расчётах, не влияют на них. Поэтому не будет большого криминала даже если погрешность будет исправлена ошибочно. Всё равно такая внезапно округлённая строка при правильной архитектуре приложения не будет участвовать в дальнейших расчётах, но покажет пользователю очень близкий к истинному результат, к тому же более удобный для восприятия.

Если нам нужно вести расчёты вообще без погрешностей, возникающих из-за двоичного представления чисел, то вместо переменных типа Double следует сразу использовать тип BigDecimal, и все расчёты производить с использованием методов этого типа. Обычно такие расчёты используют в бухгалтерии, где при любых суммах для правильной отчётности важна точность до копейки. В других же случаях такой неестественной точности, как правило, не требуется, поскольку погрешность двоичного представления теряется на фоне погрешностей, уже существующих во входных данных из внешнего мира.

вторник, 1 апреля 2014 г.

Контекст - что это?

Введение

Практика показывает, что смысл контекста в Android не всегда прозрачен. Определения контекста слишком абстрактны, разнятся от источника к источнику, и не дают возможности чётко осмыслить, что же это за сущность такая. Я постарался собрать разрозненную информацию о контексте и привести её к единому виду.

Определение контекста

Контекст (Context) – это базовый абстрактный класс, реализация которого обеспечивается системой Android. Этот класс имеет методы для доступа к специфичным для конкретного приложения ресурсам и классам и служит для выполнения операций на уровне приложения, таких, как запуск активностей, отправка широковещательных сообщений, получение намерений и прочее. От класса Context наследуются такие крупные и важные классы, как Application, Activity и Service, поэтому все его методы доступны из этих классов.

Методы получения контекста и их различие

Получить контекст внутри кода можно одним из следующих методов:
  • getBaseContext (получить ссылку на базовый контекст)
  • getApplicationContext (получить ссылку на объект приложения)
  • getContext (внутри активности или сервиса получить ссылку на этот объект)
  • this (то же, что и getContext)
  • MainActivity.this (внутри вложенного класса или метода получить ссылку на объект MainActivity)
  • getActivity (внутри фрагмента получить ссылку на объект родительской активности)
Все эти способы дадут нам возможность получить ссылку на объект, содержащий методы класса Context.

Как было сказано выше, контекст является базовым классом для классов Application, Activity и Service, а значит его методы входят в их состав. Именно поэтому для передачи контекста в качестве параметра можно использовать как ссылку на сам контекст (getBaseContext), так и ссылки на наследуемые классы (getApplicationContext, getContext, this, MainActivity.this, getActivity).

Но тут важно понимать, что время жизни этих ссылок будет разное. Ссылка на переданный объект будет работать, пока будет жить этот объект. Поэтому в качестве контекста важно передать такую ссылку, которая будет рабочей на всём протяжении работы вызываемого метода.

Например, если вызвать сообщение с помощью Toast, используя разные context, то:

Сообщение умрёт вместе с активностью:
Toast.makeText(this, "Text", Toast.LENGTH_SHORT).show();

Сообщение умрёт вместе с приложением:
Toast.makeText(getApplicationContext(), "Text ", Toast.LENGTH_SHORT).show();

Будет видно даже после завершения приложения:
Toast.makeText(getBaseContext(), "Text ", Toast.LENGTH_SHORT).show();

То есть, мы видим, что хотя контекст одинаков у разных объектов, сами эти объекты могут жить разное время. 

Заключение

Итак, контекст можно представить, как набор функций для работы на уровне приложения, вошедший в состав таких крупных классов, как Application, разных видов Activity, Service и пр.

понедельник, 31 марта 2014 г.

Android Studio вместо Eclipse

Наконец то Google разработал нормальную IDE для написания приложений под Android на Java - Android Studio! Эта IDE была анонсирована 16 мая 3013 года на ежегодной конференции "Google I/O" менеджером по продукции Google Элли Пауэрс. Её можно скачать с официальной страницы: http://developer.android.com/sdk/installing/studio.html.

Пока стадия этого проекта не достигла релиза (последняя версия Android Studio на момент написания этого поста была 0.5.3), но работать с ней уже можно. На удивление, эта среда уже намного безглючнее и удобнее, чем Eclipse с его плагинами. Так что рекомендую! :-)

вторник, 12 марта 2013 г.

Eclipse - горячие клавиши в русском регистре

Среда разработки Eclipse, являющаяся классической для программирования под Android на языке Java, имеет маленький но неприятный недостаток - сочетания горячих клавиш перестают работать при переключении на русский язык. В частности, меня достало то, что при нажатии сочетания "Ctrl+/" в русском регистре вместо закомментировывания текущей или всех выделенных строк курсор ускакивал на первое предупреждение в коде, выполняя команду "Ctrl+." Что же, зато в Eclipse можно назначить свои горячие клавиши! Поэтому для решения проблемы надо просто задублировать команду комментария на сочетание "Ctrl+."!

Открываем: "Окно → Параметры... → Общие → Клавиши". В поле "фильтр" набираем "Ctrl+/" и получаем список с этой командой для поддерживаемых языков программирования. Щёлкаем по строчке со значением "Изменение исходного кода Java" в столбце "Когда" и нажимаем кнопку "Добавить". Теперь просто меняем сочетание в поле "Привязка" на  "Ctrl+." и применяем изменения. Всё! Теперь Eclipse будет комментировать строки и в английском и в русском регистрах одинаковыми кнопками!

понедельник, 11 марта 2013 г.

Табуляция или пробелы?


Среди программистов не утихают религиозные войны на тему форматирования кода. Один из острейших вопросов на эту тему — чем делать отступы и выравнивание в исходном коде программ — табуляцией, пробелами или одно — одним, а другое — другим? У каждого варианта, определённо, есть и свои плюсы, и свои минусы.

По моему наблюдению, среди матёрых программистов в большинстве случаев всё же побеждают сторонники пробелов, и решающим аргументом здесь служит визуальная одинаковость выровненных строк вне зависимости от настроек программы у каждого конкретного программиста. А о количестве пробелов в отступе они предпочитают договариваться внутри команды. Что им мешает так же договориться о величине таба — непонятно :)

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


Различия в поведении пробелов и табов

Рассмотрим следующий показательный фрагмент кода:

if (mStatus != Status.PENDING) {
    switch (mStatus) {
        case RUNNING:
            throw new IllegalStateException("Cannot execute task: "
                                          + "the task is already running.");
        case FINISHED:
            throw new IllegalStateException("Cannot execute task: "
                                          + "the task has already been executed "
                                          + "(a task can be executed only once)");
    }
}

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

Если же мы будем выравнивать не пробелами, а табуляцией, то мы сами получим тот же самый вид, а вот у другого программиста, у которого редактор настроен на другую кратность табуляции, например, не 4, как у нас, а 8, строчки сильно сместятся вправо, а при кратности табуляции 2 — влево. При этом у нас неминуемо поползут вторые строчки текстового сообщения. То же самое будет происходить и с выравниванием комментариев в конце строк кода. Они уже не будут красиво выровнены по левому краю на протяжении некоторого блока, имеющего разные уровни вложенности.

Промежуточным вариантом называют создание отступов — табуляцией, а выравнивания — комбинированным методом. Начало выравнивания осуществляется табуляцией, а по достижении уровня предыдущей строки — пробелами. Минусом такого способа является относительная трудоёмкость такого метода и потенциальная путаница — ведь без включения визуализации управляющих символов, сразу непонятно, где табуляция, а где пробелы. Непонятно также, как они поведут себя при копировании в другие места кода.

Более того, вышеописанный способ не спасёт от нарушения выравнивания комментариев в конце строк. Да и не все программисты, которые впоследствии будут редактировать ваш код, поймут или примут вашу задумку.

Так как же правильно оформлять код — пробелами или табуляцией? А может и тем и другим? Давайте попробуем разобраться с этим.


История табуляции — интересные факты

Небольшой экскурс в историю. Табуляция (горизонтальная табуляция) изначально была введена ещё в механических печатных машинках для удобства построения таблиц и применялась также для создания абзацного отступа. Она и не имела жёсткого размера. Перед работой пользователь сам устанавливал нужные ему позиции табуляции с помощью специального механизма. Так же, как это сейчас можно сделать в MS Word. В результате, при последовательных нажатиях на клавишу табуляции (обозначалась как ←) каретка под действием пружины автоматически перемещалась влево по всем установленным ранее позициям, перемещая тем самым область ввода вправо.

Когда пришло время электронной техники, то, чтобы не изобретать велосипед, её стали делать по образу и подобию печатных машинок. Тем более что вначале эта техника мало чем от них отличалась. Например, телетайп был фактически симбиозом телеграфа и пишущей машинки. Поэтому его разработчики просто перевели все её клавиши и некоторые другие элементы (возврат каретки, перевод каретки на новую строку и зачем-то даже сигнал достижения конца строки) в коды ASCII. Клавише табуляции был присвоен код 9, но поскольку эмулировать механизм установки пользовательских позиций табуляции было сложно, то его решили просто сделать фиксированным.

Почему же выбор размера фиксированной табуляции пал именно на 8 знакомест? Как правило, печатные машинки имели длину строки 80 знакомест. Телетайпы, применявшиеся в первых ЭВМ для вывода информации, тоже, поскольку являлись наследниками пишущих машинок. Даже на перфокартах информация стала храниться строками по 80 символов. Таким образом, печатные машинки задали некий стандарт на длину строки.

Поскольку в первую очередь табуляция использовалась для создания колонок таблиц, то наиболее удобный размер должен был быть не меньше разницы в длине наиболее употребительных слов, если их писать в столбик (чтобы для перехода к следующему столбцу не надо было делать разное число табуляций). В то же время, размер этот должен был позволять делать максимально возможное количество столбцов. Третье условие — размер должен укладываться в длину строки 80 символов кратное число раз.

Согласно последнему условию, у разработчиков был выбор из следующих пяти реальных вариантов: 4, 5, 8, 10 и 16. Первые два варианта были не очень удобными, поскольку разница в длинах слов английского языка часто превышала эти значения. Отступы в 16 символов выглядели чрезмерно большими, снижали удобство пользования этим инструментом и сильно ограничивали число столбцов в таблицах. Оставался выбор между 8 и 10 символами.

Значение в 8 символов стало первым значением, которое удовлетворяло условию разницы длин слов. Кроме того, только оно и удовлетворяло второму условию — построения максимального возможного числа столбцов. Вариант с табуляцией в 10 символов выглядел неоправданно расточительно и давал всего 8 столбцов, что могло несколько сузить сферу применения табуляции. Кроме того, число 10 для максимального количества столбцов психологически выглядело более внушительно. Все эти причины, видимо, и склонили разработчиков телетайпа к табуляции именно в 8 символов.

Несколько слов про абзацный отступ, поскольку он тоже проходит где-то рядом с нашей темой. Согласно ОСТ 29.115-88, при печати на пишущих машинках абзацный отступ должен быть равным трём ударам, но допускается использовать отступ и в пять ударов. Кроме того, предписывается размещать на одной странице 29 (+-1) строк (что примерно соответствует полуторному интервалу на печатной машинке). Требование отступа в 3 удара при полуторном интервале довольно странное, и ниже я объясню, почему.

В типографической вёрстке абзацный отступ равен полуторному размеру кегля шрифта, то есть, грубо говоря, вертикальному размеру строки (т.е. расстоянию от низа одной строки до низа другой), умноженному на 1,5. Поскольку в типографической вёрстке полутроный интервал почти не используется, а строчки следуют сразу одна за другой, то правильный абзац в этом случае приблизительно равен трём средним символам кириллицы.

Вероятно, составители ОСТ 29.115-88, не вдаваясь в подробности типографики, приняли это значение за константу и для моноширинных фиксированных шрифтов печатных машинок, и вместе с тем установили стандартом полуторный межстрочный интервал, поскольку при моноширинном шрифте и низком качестве печати одинарный интервал смотрелся реально очень плохо.

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

Кстати, печатные машинки оказали влияние не только на количество столбцов текстового режима монитора, но и на его вертикальный размер в 30 строк. Ведь при печати с полуторным интервалом именно столько строк и вмещалось на один лист!


Стратегический взгляд на решение проблемы

Конечно, наиболее логично делать отступы уровней вложенности кода табуляцией, потому что она изначально для этого и предназначена — начинать печать текста с разных позиций. Кроме того, в эпоху компьютеров символ табуляции очень хорошо подошёл и на роль логического делителя. Количество последовательных символов табуляции чётко указывало уровень логического вложения.

Отсюда становится очевидным, что пробелы — это вынужденный костыль. Ведь, к сожалению, размер табуляции для каждого языка жёстко не зафиксирован. Программисты переходят с языка на язык, зачастую сохраняя традиции прежнего языка, да и свои предпочтения. В этих условиях, пробелы — мера для фиксирования внешнего вида исходника, защита от расползания его форматирования.

Выходом могла бы стать принудительная привязка размера табуляции к языку программирования. Специалистами по эргономике могли бы быть чётко определены наиболее удобные размеры табуляции для синтаксиса каждого из языков, и этот размер мог бы быть зафиксирован в компиляторе, выдавая настойчивые предупреждения при его нарушении.

Поскольку мы не можем повлиять на стратегию решения проблемы, мы вынуждены как-то выкручиваться тактически в рамках имеющейся реальности. Чтобы выработать для себя правильный вариант, давайте рассмотрим проблемы пробелов и табуляции чуть подробнее.


Проблемы пробелов

Когда программист открывает чужой исходный код с отступами и выравниванием, осуществлёнными пробелами, но отступ этот оказывается неудобен ему, он, по сути, ничего не сможет сделать для приведения его к удобному виду без проведения каких-то сложных манипуляций.

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

Кроме того, для удаления лишнего отступа вместо привычного нажатия на Backspace приходится нажимать сочетание клавиш Shift+Tab. Но это уже, конечно, дело привычки.


Проблемы табуляции

У табуляции только одна проблема — при изменении её размера нарушается выравнивание комментариев и других выровненных элементов, хотя их, как правило, не так много, чтобы это затрудняло чтение исходника, да и подправить их уже не так сложно.

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


Выводы

Сначала скажу пару слов о размере отступов, как таковых. Применительно для большинства распространённых языков программирования идеальным размером отступов является ровно 4 символа. Почему ровно? Потому что уже сложилось так, что большинство исходников отформатированы именно таким образом, и тонкая подгонка под субъективный «идеал» в 3 или 5 символов теряет смысл.

Встречающиеся иногда отступы в 8 символов делают код излишне размазанным по горизонтали и совсем не оставляют места для комментариев справа. Кроме того, из-за превышения размера отступов над длиной операторов, в ступеньках лесенки вложенности обнажаются пробелы между операторами и операндами, которые создают визуальные провалы, что никак не идёт на пользу читабельности кода.

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

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

  1. Используйте отступы стандартного де-факто размера. Для Java, Pascal, C, C++  и т.п. стандартом де-факто является отступ в 4 символа, как бы нам ни хотелось использовать другой размер.
  2. Если соблюдён пункт 1, то абсолютно неважно, чем вы будете делать отступы. Если вы сделаете их пробелами, то будет хорошо — ваш код везде будет выглядеть читаемо, и никогда ничего не поползёт. Другие программисты при чтении вашего кода будут привыкать к правильному форматированию. Если же вы сделаете отступы табуляцией, то будет ещё лучше — вы дадите другим программистам выбор — включить в редакторе правильный размер табуляции и читать правильно отформатированный код, или  читать его с привычными им отступами, но поползшими комментариями и отдельными строками, в которых применялось выравнивание.
  3. Выбор в пункте 2 можно осуществлять в зависимости от того, как ваш код будет использоваться в дальнейшем. Если его блоки будут вставляться в чужой код, тогда точно имеет смысл выбрать табуляцию, чтобы вставляющему проще было подогнать форматирование. Если исходник предназначен для публикации в Интернете, где табуляция либо съедается, либо зафиксирована на 8 символов, то тогда можно выбрать пробелы, чтобы исключить дополнительный шаг подготовки исходника к публикации. Если же в вашей организации уже установлены определённые правила форматирования, то выбора у вас уже нет :)

Если несмотря ни на что вы твёрдо намерены использовать своё количество символов в отступах, то в этом случае использование пробелов и табуляции обязательно будет зависеть от того, как впоследствии будет использоваться ваш код другими программистами.

Если для обычного сопровождения в плане исправления ошибок — наверное, лучше использовать пробелы. С вашим кодом будут работать недолго, и если он хорошо отформатирован, то особой нужды в изменении размера отступов ни у кого не будет.

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


Если у вас одно, а нужно другое

Есть такой замечательный редактор — Notepad++. В нём замена табуляции на пробелы и наоборот выполняется одним щелчком мыши, сделанным разделе в меню «Правка →  Операции с Пробелами». Я использую этот редактор для перегона табулированных кусков кода, предназначенных для публикации в блоге, поскольку последний автоматом меняет один знак табуляции на один пробел, что неприемлемо.


Заключение

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

Что касается Notepad из состава Windows, который не имеет настройки размера табуляции, и прочих подобных редакторов, делающих табуляцию принудительно в 8 символов, то я не могу себе представить причину, по которой программист открывал бы мой код именно в них, тогда как уже давно для всех ОС существуют удобные специализированные редакторы для исходного кода.

Работать в блокноте не круто. Круто работать в шестнадцатеричном редакторе :)

суббота, 9 марта 2013 г.

AsyncTask. Максимальное количество одновременных потоков.

Вступление

В настоящее время операционная система Android переживает бурное развитие, что в то же время негативно сказывается на постоянстве результата работы одних и тех же классов и методов в разных версиях этой системы. Одним из примеров такого непостоянства может служить класс асинхронных задач AsyncTask.

Асинхронные задачи - это потоки, выполняемые параллельно с основным потоком, в котором работает пользовательский интерфейс приложения, и никак не синхронизированные с ним. Это значит, что поток пользовательского интерфейса, запустив такую асинхронную задачу, не знает, когда в него вернётся результат её работы. Плюсом такой архитектуры является постоянная доступность пользовательского интерфейса для взаимодействия. Он не подвисает, ни замирает, ожидая окончания работы какой-либо длительной задачи, а продолжает работать.

Минусом такого подхода является сложность в возврате результата работы асинхронной задачи в пользовательский интерфейс. Поскольку интерфейс не знает, когда асинхронный поток закончит работу, поток должен сам производить вывод результата своей работы в интерфейс, как только этот результат у него будет готов. Кроме того, он должен иметь возможность периодически сообщать интерфейсу о ходе своей работы (например, двигать прогрессбар).

Всё это осуществляется соответствующими классами (Thread, ThreadPoolExecutor, Runnable) и приёмами программирования, но выглядит относительно сложно, поскольку во избежание конфликтов потоков просто так нельзя из них манипулировать интерфейсом. Приходится прибегать к дополнительным конструкциям. Вот в этом случае и помогает AsyncTask.

Класс AsyncTask является обёрткой над классами потоков и облегчает работу с ними. Класс содержит несколько переопределяемых методов, из которых обязательным для переопределения является метод doInBackground(), содержимое которого, собственно, и будет выполняться в отдельном потоке. Также достаточно часто используемыми являются методы onPreExecute(), onProgressUpdate() и onPostExecute(), вызываемые перед запуском потока, по команде из потока, и после окончания работы потока соответственно, из которых, собственно, и происходит манипулирование интерфейсом (мы ведь помним, что непосредственно из потока манипулировать интерфейсом нельзя).


Особенности работы AsyncTask в разных версиях Android.

До версии Android 1.5 (т.е. до API 3) AsyncTask вообще назывался UserTask. Вплоть до версии 1.6 (т.е. до API 4) его поведение было следующим. При создании и запуске нескольких объектов класса AsyncTask на выполнение, они все выполняли переопределённое содержимое своего метода onPreExecute(), выполняемого перед запуском потока, а их методы doInBackground() вставали в очередь. В каждый момент времени выполнялся только один метод doInBackground(), а остальные ждали завершения работы предыдущего.

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

Начиная с версии 1.6 (API 4) логика работы AsyncTask была изменена. Теперь первые 5 потоков стали запускаться параллельно, и только начиная с 6-го, они начинали становиться в очередь. Если очередь, ограниченная 10 потоками, заканчивалась (5 запущенных потоков +10 в очереди), 16-й и каждый последующий потоки начинали выполняться параллельно остальным запущенным, а те 10 продолжали ждать своей очереди, но уже могли рассчитывать на места любых из уже работающих параллельно потоков (пяти и более). Максимальное ограничение количества одновременно работающих потоков — 128. Таким образом, можно запустить 138 потоков (128 + 10 в очереди). При превышении этого количества вызывается исключение RejectedExecutionException.

Помимо вышеперечисленных, в AsyncTask появился ещё один внутренний параметр, влияющий на его поведение. Это интервал, в течение которого резервируется место для выполнения новых параллельных потоков после завершения работы дополнительных параллельных потоков. То есть, места 6-го и последующих параллельно выполнявшихся потоков (если таковые были) не сокращаются снова до пяти, а предоставляются потокам из очереди или вновь появившимся потокам, если они успели появиться в течение этого интервала. До версии Android 2.3 (до API 9) этот интервал был равен 10 секундам, а начиная с версии 2.3 (API 9) — 1 секунда.

Практика программирования показала, что выгода от использования параллельности затмевалась валом ошибок, допускаемых программистами при реализации своих многопоточных алгоритмов, и, начиная с версии Android 3.0 (API 11), разработчики вернули прежнее поведение AsyncTask, хотя и добавили помимо стандартного метода execute() новый метод executeOnExecutor(), позволяющий всё же реализовывать прежнее параллельное поведение.

Возможно, важную роль в отказе от параллельного выполнения потоков в Android сыграли какие-то не совсем удобные для использования многопоточности особенности системы, а может быть, дело в низкой квалификации большого процента программистов, ринувшихся на новую платформу. Так или иначе, но при выполнении метода execute() запущенные потоки объектов AsyncTask снова вставали в очередь, которая теперь ограничивалась 128 потоками.

Всё это даёт разное поведение программ, активно использующих множественные вызовы потоков. Если один из потоков застревает, другие просто не выполняются. И это, однозначно, плохо. Конечно, в новых версиях можно использовать метод executeOnExecutor() с параметром AsyncTask.THREAD_POOL_EXECUTOR для параллельного выполнения или с параметром AsyncTask.SERIAL_EXECUTOR для последовательного выполнения потоков аналогично методу execute(). Но как быть, если требуется совместимость программы со старыми версиями, где этого метода ещё не было?


Свой класс AsyncTask

Можно написать свой класс AsyncTask, а точнее, взять его из новых версий Android и, если вы оставляете совместимость с Android 2.2 (API 8) и младше, убрать оттуда возможность последовательного выполнения потоков, поскольку он использует метод ArrayDeque, появившийся только в Android 2.3 (API 9), или же, использовать эту возможность в версиях Android с 2.3 до 3.0, где ранее её не было:

package com.example.testasynctask;

/**
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//import java.util.ArrayDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import android.os.Handler;
import android.os.Message;
import android.os.Process;

/**
 * ### I delete this comments as it make the answer too long to submit ###
 */
public abstract class AsyncTask<Params, Progress, Result> {
    private static final String LOG_TAG = "AsyncTask";

    private static final int CORE_POOL_SIZE = 10;
    private static final int MAXIMUM_POOL_SIZE = 128;
    private static final int KEEP_ALIVE = 1;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(10);

    /**
     * An {@link Executor} that can be used to execute tasks in parallel.
     */
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
            TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

    /**
     * An {@link Executor} that executes tasks one at a time in serial
     * order. This serialization is global to a particular process.
     */
    //    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;

    private static final InternalHandler sHandler = new InternalHandler();

    //    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    private static volatile Executor sDefaultExecutor = THREAD_POOL_EXECUTOR;
    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;

    private volatile Status mStatus = Status.PENDING;

    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

    //    private static class SerialExecutor implements Executor {
    //        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    //        Runnable mActive;
    //
    //        public synchronized void execute(final Runnable r) {
    //            mTasks.offer(new Runnable() {
    //                public void run() {
    //                    try {
    //                        r.run();
    //                    } finally {
    //                        scheduleNext();
    //                    }
    //                }
    //            });
    //            if (mActive == null) {
    //                scheduleNext();
    //            }
    //        }
    //
    //        protected synchronized void scheduleNext() {
    //            if ((mActive = mTasks.poll()) != null) {
    //                THREAD_POOL_EXECUTOR.execute(mActive);
    //            }
    //        }
    //    }

    /**
     * Indicates the current status of the task. Each status will be set only once
     * during the lifetime of a task.
     */
    public enum Status {
        /**
         * Indicates that the task has not been executed yet.
         */
        PENDING,
        /**
         * Indicates that the task is running.
         */
        RUNNING,
        /**
         * Indicates that {@link AsyncTask#onPostExecute} has finished.
         */
        FINISHED,
    }

    /** @hide Used to force static handler to be created. */
    public static void init() {
        sHandler.getLooper();
    }

    /** @hide */
    public static void setDefaultExecutor(Executor exec) {
        sDefaultExecutor = exec;
    }

    /**
     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
     */
    public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);

                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                return postResult(doInBackground(mParams));
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    final Result result = get();

                    postResultIfNotInvoked(result);
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                } catch (Throwable t) {
                    throw new RuntimeException("An error occured while executing "
                            + "doInBackground()", t);
                }
            }
        };
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

    private Result postResult(Result result) {
        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }

    /**
     * Returns the current status of this task.
     * 
     * @return The current status.
     */
    public final Status getStatus() {
        return mStatus;
    }

    /**
     * Override this method to perform a computation on a background thread. The
     * specified parameters are the parameters passed to {@link #execute} by the caller of this task.
     * 
     * This method can call {@link #publishProgress} to publish updates
     * on the UI thread.
     * 
     * @param params The parameters of the task.
     * 
     * @return A result, defined by the subclass of this task.
     * 
     * @see #onPreExecute()
     * @see #onPostExecute
     * @see #publishProgress
     */
    protected abstract Result doInBackground(Params... params);

    /**
     * Runs on the UI thread before {@link #doInBackground}.
     * 
     * @see #onPostExecute
     * @see #doInBackground
     */
    protected void onPreExecute() {
    }

    /**
     * <p>
     * Runs on the UI thread after {@link #doInBackground}. The specified result is the value returned by {@link #doInBackground}.
     * </p>
     * 
     * <p>
     * This method won't be invoked if the task was cancelled.
     * </p>
     * 
     * @param result The result of the operation computed by {@link #doInBackground}.
     * 
     * @see #onPreExecute
     * @see #doInBackground
     * @see #onCancelled(Object)
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    protected void onPostExecute(Result result) {
    }

    /**
     * Runs on the UI thread after {@link #publishProgress} is invoked.
     * The specified values are the values passed to {@link #publishProgress}.
     * 
     * @param values The values indicating progress.
     * 
     * @see #publishProgress
     * @see #doInBackground
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    protected void onProgressUpdate(Progress... values) {
    }

    /**
     * <p>
     * Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link #doInBackground(Object[])} has finished.
     * </p>
     * 
     * <p>
     * The default implementation simply invokes {@link #onCancelled()} and ignores the result. If you write your own implementation, do not call <code>super.onCancelled(result)</code>.
     * </p>
     * 
     * @param result The result, if any, computed in {@link #doInBackground(Object[])}, can be null
     * 
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    @SuppressWarnings({ "UnusedParameters" })
    protected void onCancelled(Result result) {
        onCancelled();
    }

    /**
     * <p>
     * Applications should preferably override {@link #onCancelled(Object)}. This method is invoked by the default implementation of {@link #onCancelled(Object)}.
     * </p>
     * 
     * <p>
     * Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link #doInBackground(Object[])} has finished.
     * </p>
     * 
     * @see #onCancelled(Object)
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    protected void onCancelled() {
    }

    /**
     * Returns <tt>true</tt> if this task was cancelled before it completed
     * normally. If you are calling {@link #cancel(boolean)} on the task,
     * the value returned by this method should be checked periodically from {@link #doInBackground(Object[])} to end the task as soon as possible.
     * 
     * @return <tt>true</tt> if task was cancelled before it completed
     * 
     * @see #cancel(boolean)
     */
    public final boolean isCancelled() {
        return mFuture.isCancelled();
    }

    /**
     * <p>
     * Attempts to cancel execution of this task. This attempt will fail if the task has already completed, already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when <tt>cancel</tt> is
     * called, this task should never run. If the task has already started, then the <tt>mayInterruptIfRunning</tt> parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.
     * </p>
     * 
     * <p>
     * Calling this method will result in {@link #onCancelled(Object)} being invoked on the UI thread after {@link #doInBackground(Object[])} returns. Calling this method guarantees that {@link #onPostExecute(Object)} is never invoked. After invoking
     * this method, you should check the value returned by {@link #isCancelled()} periodically from {@link #doInBackground(Object[])} to finish the task as early as possible.
     * </p>
     * 
     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     *        task should be interrupted; otherwise, in-progress tasks are allowed
     *        to complete.
     * 
     * @return <tt>false</tt> if the task could not be cancelled,
     *         typically because it has already completed normally; <tt>true</tt> otherwise
     * 
     * @see #isCancelled()
     * @see #onCancelled(Object)
     */
    public final boolean cancel(boolean mayInterruptIfRunning) {
        return mFuture.cancel(mayInterruptIfRunning);
    }

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     * 
     * @return The computed result.
     * 
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException If the computation threw an exception.
     * @throws InterruptedException If the current thread was interrupted
     *         while waiting.
     */
    public final Result get() throws InterruptedException, ExecutionException {
        return mFuture.get();
    }

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result.
     * 
     * @param timeout Time to wait before cancelling the operation.
     * @param unit The time unit for the timeout.
     * 
     * @return The computed result.
     * 
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException If the computation threw an exception.
     * @throws InterruptedException If the current thread was interrupted
     *         while waiting.
     * @throws TimeoutException If the wait timed out.
     */
    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
            ExecutionException, TimeoutException {
        return mFuture.get(timeout, unit);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * 
     * <p>
     * Note: this function schedules the task on a queue for a single background thread or pool of threads depending on the platform version. When first introduced, AsyncTasks were executed serially on a single background thread. Starting with
     * {@link android.os.Build.VERSION_CODES#DONUT}, this was changed to a pool of threads allowing multiple tasks to operate in parallel. After {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this back to a single thread to
     * avoid common application errors caused by parallel execution. If you truly want parallel execution, you can use the {@link #executeOnExecutor} version of this method with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for
     * warnings on its use.
     * 
     * <p>
     * This method must be invoked on the UI thread.
     * 
     * @param params The parameters of the task.
     * 
     * @return This instance of AsyncTask.
     * 
     * @throws IllegalStateException If {@link #getStatus()} returns either {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
     */
    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * 
     * <p>
     * This method is typically used with {@link #THREAD_POOL_EXECUTOR} to allow multiple tasks to run in parallel on a pool of threads managed by AsyncTask, however you can also use your own {@link Executor} for custom behavior.
     * 
     * <p>
     * <em>Warning:</em> Allowing multiple tasks to run in parallel from a thread pool is generally <em>not</em> what one wants, because the order of their operation is not defined. For example, if these tasks are used to modify any state in common
     * (such as writing a file due to a button click), there are no guarantees on the order of the modifications. Without careful work it is possible in rare cases for the newer version of the data to be over-written by an older one, leading to
     * obscure data loss and stability issues. Such changes are best executed in serial; to guarantee such work is serialized regardless of platform version you can use this function with {@link #SERIAL_EXECUTOR}.
     * 
     * <p>
     * This method must be invoked on the UI thread.
     * 
     * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a
     *        convenient process-wide thread pool for tasks that are loosely coupled.
     * @param params The parameters of the task.
     * 
     * @return This instance of AsyncTask.
     * 
     * @throws IllegalStateException If {@link #getStatus()} returns either {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
     */
    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }

    /**
     * Convenience version of {@link #execute(Object...)} for use with
     * a simple Runnable object.
     */
    public static void execute(Runnable runnable) {
        sDefaultExecutor.execute(runnable);
    }

    /**
     * This method can be invoked from {@link #doInBackground} to
     * publish updates on the UI thread while the background computation is
     * still running. Each call to this method will trigger the execution of {@link #onProgressUpdate} on the UI thread.
     * 
     * {@link #onProgressUpdate} will note be called if the task has been
     * canceled.
     * 
     * @param values The progress values to update the UI with.
     * 
     * @see #onProgressUpdate
     * @see #doInBackground
     */
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

    private static class InternalHandler extends Handler {
        @SuppressWarnings({ "unchecked", "RawUseOfParameterizedType" })
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult result = (AsyncTaskResult) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

    @SuppressWarnings({ "RawUseOfParameterizedType" })
    private static class AsyncTaskResult<Data> {
        final AsyncTask mTask;
        final Data[] mData;

        AsyncTaskResult(AsyncTask task, Data... data) {
            mTask = task;
            mData = data;
        }
    }
}
Сам код небольшой, основной объём занимают комментарии. После добавления класса в проект просто удалите или закомментируйте в вашем коде импорт: import android.os.AsyncTask; и новый класс будет использоваться вместо штатного.

Кроме всего прочего, теперь мы можем сами менять поведение AsyncTask путём манипулирования его константами прямо в коде класса AsyncTask:
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAXIMUM_POOL_SIZE = 128;
    private static final int KEEP_ALIVE = 1;
Можем увеличить количество изначально одновременно выполняющихся потоков с 5 до, например, 10. Можем увеличить максимально возможное количество параллельно выполняющихся потоков (хотя это вряд ли нам может пригодиться), а также время резервирования места под новые потоки. Кроме того, мы можем изменить размер очереди с 10 на любое другое число в строке:
private static final BlockingQueue<Runnable> sPoolWorkQueue =
   new LinkedBlockingQueue<Runnable>(10);

Простейшее приложение для тестирования работы потоков

Для наглядного изучения поведения потоков можно написать следующее приложение. Создадим приложение Android с активити, содержащей одну кнопку, которой мы будем запускать новые потоки:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/button1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="Добавить новый поток" />

</LinearLayout>
В коде обработаем нажатие на кнопку. По нажатию будем создавать и запускать новый поток, который будет создавать новое TextView, устанавливать его в Layout и в течение заданного времени выдавать в него каждую секунду увеличивающееся на единицу число, после чего TextView будет уничтожаться:
package com.example.testasynctask;

import android.app.Activity;
//import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;

public class MainActivity extends Activity implements OnClickListener { // Наследуем класс Activity и добавим интерфейс OnClickListener  
    LinearLayout linearLayout;                                          // Переменная для хранения ссылки на LinearLayout

    // Создадим активити
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);                     // Вызовем родительский класс создания активити
        setContentView(R.layout.activity_main);                 // Создадим интерфейс согласно разметке в xml-файле
        linearLayout = (LinearLayout) findViewById(R.id.LinearLayout1); // Найдём в интерфейсе LinearLayout  
    }                                                                   // для добавления в него в дальнейшем TextView

    // Переопределим метод интерфейса OnClickListener для обработки нажатия кнопки
    @Override
    public void onClick(View view) {
        (new AsyncTask<Integer, Integer, Void>() {              // Создадим новый объект AsyncTask по классу, описанному прямо здесь 
            TextView textView;                                  // Переменная для хранения ссылки на созданный TextView

            // Выполняется в потоке интерфейса сразу после запуска AsyncTask перед выполнением потока
            @Override
            protected void onPreExecute() {
                super.onPreExecute();                           // Вызовем родительский метод
                textView = new TextView(MainActivity.this);     // Создадим TextView
                // Создадим объект для хранения параметров разметки TextView и зададим их
                LayoutParams lpView = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                textView.setLayoutParams(lpView);               // Присвоим параметры TextView
                linearLayout.addView(textView);                 // Добавим созданное TextView в LinearLayout
                textView.setText("...");                        // Выведем многоточие в TextView
            }

            // Выполняется в отдельном потоке
            @Override
            protected Void doInBackground(Integer... arg) {     
                for (int i = arg[0]; i >= 0; i--) {             // Цикл с количеством итераций, заданным при запуске потока
                    publishProgress(i);                         // Запустить метод onProgressUpdate, выводящий число i в TextView
                    try {
                        Thread.sleep(1000);                     // Задержка в 1000 миллисекунд
                    } catch (InterruptedException e) {
                    }
                }
                return null;
            }

            // Выполняется в потоке интерфейса по команде publishProgress(), дающейся в отдельном потоке
            @Override
            protected void onProgressUpdate(Integer... values) {
                super.onProgressUpdate(values);                 // Вызовем родительский метод
                textView.setText(values[0].toString());         // Вывести число i в TextView
            }

            // Выполняется в потоке интерфейса после окончания работы отдельного потока
            @Override
            protected void onPostExecute(Void arg) {
                super.onPostExecute(arg);                       // Вызовем родительский метод
                linearLayout.removeView(textView);              // Удалим TextView из интерфейса
            }

        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 20);   // Выполнить объект асинхронного потока в параллельном режиме,
    }                                                               // передав ему число 20 в качестве параметра количества циклов
}
Добавим в приложение нашу реализацию класса AsyncTask, запустим его, и будем нажимать на кнопку. Мы увидим, как после каждого нажатия под кнопкой будет появляться новая строчка с бегущим числом. Числа в новых строчках будут бежать, пока количество строчек не достигнет константы количества одновременно запущенных потоков CORE_POOL_SIZE, по умолчанию равняющейся 5. Затем начнут появляться строчки с многоточиями — это потоки, стоящие в очереди. Их будет 10 штук. Затем снова начнут появляться строчки с бегущими цифрами, потому что очередь переполнена. Всё так, как и было описано мной ранее.