Конечно, 33 строки — это «маркетинговый» ход для привлечения внимания.
Полное количество строк в основном и единственном классе равно 86, но ядро как раз столько и занимает (33). Внутри код змейки, самого маленького приложения для Android, комментарии и некоторые исследования на тему уменьшения размеров, сборка приложения из командной строки с использованием Proguard.
Быстрые ссылки по статье:
самое маленькое приложение;
змейка;
ссылки на github.
Введение
Думаю, что многие видели HelloWorld в Eclipse для Android.
Выглядит, особенно для новичка, весьма некомпактно. Тут очень много, назовём условно, «лишнего», для того чтобы разобраться, как это всё работает, что из чего следует, и как настраивать приложение.
От версии к версии иерархия файлов постоянно усложняется, что простоты к работе не добавляет.
Что тут есть (для самых маленьких):1. две папки с Java-классами (src, gen). src содержит исходники, gen — сгенерированные классы. В основном, в папке gen бывает R.java.
2. Папка для библиотек libs. В случае HelloWorld создаётся библиотека android-support-v4.jar (или её более новая версия). Эта библиотека используется для обратной совместимости приложений.
То есть, если какие-то новые особенности (например объекты UI), создадут разработчики ОС, то, для того чтобы всё работало и на более ранних версиях, используются именно эти библиотеки. Также при помощи этой библиотеки разработчики исправляют некоторые ошибки. При использовании этой библиотеки, приложение, безусловно, значительно увеличит свой размер.
Тут можно посмотреть список всех изменений по версиям.
3. ресурсная папка (res), в которой обычно располагаются изображения, настройки стилей, различные строки, и много другого прочего. Все файлы из этой папки получают уникальный ID в автоматически создаваемом при компиляции R.java-файле, который помещается в папку gen. ID можно (нужно) использовать внутри приложения, для того чтобы выделить тот или иной объект. Например, иконка для приложения.
4. asserts служит для хранения всех любых других видов файлов, которые нужно использовать в приложении, но им не будет выделен ID. Например, шрифт.
5. Файл-манифест приложения AndroidManifest.xml. Из него ОС узнаёт, что и как запускать, какие разрешения есть у приложения и т.п.
6. ic_launcher-web.png — файл-изображение для Google Play. Честно говоря, не знаю, насколько сейчас это является обязательным пунктом. В крайний раз, когда обновлял приложение на Google Play, этого не требовалось. Минутка субъективизма: не понимаю, что этот файл заслужил расположение в корне.
7. Текстовых файла для настроек проекта project.properties. Самый, пожалуй, частый вариант редактирования — включить/выключить использование proguard.
8. Текстовый файл для настроек proguard: proguard-project.txt
Об этой иерархии всём подробнее можно прочитать тут.
В принципе, для написания приложения достаточно только основной класс для Activity и AndroidManifest.xml
При помощи project.properties и proguard-project.txt можно настраивать работу Proguard. У него самая главная функция — сокращать имена по минимуму, что помогает усложнить реверс инжиниринг, а также сократить размер выходного файла. Имена переменных, методов и классов сокращаются до одно-, двухбуквенных.
Родная сборка приложения от Eclipse достаточно хороша, но лучше воспользоваться более «ручным» способом создания приложения при помощи командной строки. Как минимум исчезнет класс BuildConfig.java, который «ни с чего» появляется в папке gen. Также я заметил такую особенность, что родной компилятор собирает не только java, но и прочие файлы, которые находятся на одном уровне с исходниками. Чтобы не ошибиться(и не слить в релиз то, что не нужно), лучше воспользоваться компиляцией из командной строки.
Как это сделать, подробно написано в этой отличной статье.
Но при работе Тут bat-файл был переработан: добавлена корректная обработка путей в Windows, работа с Proguard, генерация ключа вынесена в отдельный батник.
Как работать с Proguard через командную строку.
Proguard работает с class-файлами (в нашем случае это файл из папки obj). Он всё собирает в выходной jar-файл (в нашем случае ./objPro/classes-processed.jar). Полученный jar-файл уже кормим сборщику apk-файлов. В итоге, всё собирается в dex-файлы, и apk-файлы. Для настройки сборки используется android.pro-файл. Если Proguard не используется, то всё собирается сразу из папки obj с помощью class-файлов.
Самое маленькое приложение на Android
Github
Давайте как предварительный этап перед созданием змейки соберём самое маленькое приложение на Android.
В общем, для этого нужно только A.java (class A extends Activity):
кодpackage a.a;
import android.app.Activity;
public class A extends Activity {
}
и AndroidManifest.xml
код<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="a.a"
android:versionCode="1" >
<uses-sdk
android:minSdkVersion="8" />
<application >
<activity android:name=".A" android:label="A">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
android-support-v4.jar не используется. Ресурсы из папки res все удалены. В этом случае иконка приложения — это автопортрет Android, а строки (название приложения) записываются не ссылками на ресурсы, а в явном виде.
После сборки получается aSmall.apk — 2832 байта, aSmall.unsigned.apk — 945, что показывает, насколько много отъедает сертификат приложения. dex — 404 байта, classes-processed.jar — 248 байт. Это минимум.
Змейка
Github
В начале, конечно, захотелось мини-Excel, но я не нашёл хороший вариант в Java, при котором будет работать аналог функции exec в Javascript. Будет очень интересно посмотреть на результат, если кто-то сможет реализовать подобное.
Характеристики кода: полное количество строк A.java — 86, без import'ов — 69, «физика» поведения — 33 строки (что сравнимо с Javascript).
Приложение — полноэкранное. Управление при помощи жестов — вверх, вниз, влево, вправо.
Исходный код A.java.
A.java добавлен внутренний класс V extends View, который выводится на полный экран, в V есть в свою очередь свой внутренний класс P (Physics), который и описывает поведение змейки. Вот его размеры и равны примерно 30 строкам.
Алгоритм работы
приложение запускается в портретном режиме, основным элементом становится gv = new V();
В классе gv добавляется обработка жестов и запускается физику. В Физике инициализируется таймер (Timer), внутри которого происходит вычисление поведения змейки. Runnable и ASyncTask по размеру бы были гораздо больше. Сама змейки хранится в ArrayList. Были попытки сделать это через bitArray или через обычный массив, но в ArrayList всё как-то компактней получилось.
кодpackage ru.ps.habrsnake;
import java.util.ArrayList;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.View.OnTouchListener;
public class A extends Activity {
static final int RESULT_FINISH = 0, RESULT_OK = 1, RESULT_LOCK = 2,RESULT_SELF = 3, RESULT_INC = 4, RESULT_WIN = 5, RATE = 250, cSnake = 0xAACC33AA, cMeat = 0xEEAACC33, cBG = 0xFF000000, cText = 0xFFFFFFFF, width_cells = 10;
public V gv;
protected void onCreate(Bundle savedInstanceState) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
super.onCreate(savedInstanceState);
gv = new V(this);
setContentView(gv);}
protected void onDestroy() {
super.onDestroy();
gv.ph.timer.cancel();}
class V extends View implements OnTouchListener{
public float diam_cell = 10.f,x,y;
public P ph;
public Paint paintSnake = new Paint(), paintMeat = new Paint();
public V(Activity activity) {
super(activity);
setBackgroundColor(cBG);
paintSnake.setColor(cSnake);
paintMeat.setColor(cMeat);
setOnTouchListener(this);
diam_cell = ((float) A.this.getWindowManager().getDefaultDisplay().getWidth()) / ((float) width_cells);
ph = new P(width_cells, (int) (A.this.getWindowManager().getDefaultDisplay().getHeight() / diam_cell));}
public void invalidateWrapper(){
A.this.runOnUiThread(new Runnable() {public void run() {invalidate();}});}
public void onDraw(Canvas canvas) {
for (int i = 0; i < ph.arSnake.size(); canvas.drawCircle((ph.arSnake.get(i) % width_cells +.5f) * diam_cell, (ph.arSnake.get(i) / width_cells +.5f) * diam_cell, diam_cell*.5f, paintSnake),i++){}
canvas.drawCircle((ph.posMeat % width_cells +.5f) * diam_cell, (ph.posMeat / width_cells +.5f) * diam_cell, diam_cell*.5f, paintMeat);
canvas.drawText(ph.scores + "", canvas.getWidth() * .5f ,paintMeat.getTextSize(), paintMeat);}
public boolean onTouch(View v, MotionEvent event) {
x = (event.getActionMasked() == MotionEvent.ACTION_DOWN)?event.getX():x;
y = (event.getActionMasked() == MotionEvent.ACTION_DOWN)?event.getY():y;
ph.setDir((event.getActionMasked() == MotionEvent.ACTION_UP)?((Math.abs(event.getX() - x) > Math.abs(event.getY() - y) && (event.getX() - x) > 0)?3:(((Math.abs(event.getX() - x) > Math.abs(event.getY() - y)) && (event.getX() - x) <= 0)?2:((event.getY() - y) > 0?1:0))):ph.GlobDir);
return true;}
class P {
public int[] arI = new int[]{5,4,3,2};
public ArrayList<Integer> arSnake = new ArrayList<Integer>();
public Timer timer;
public int posMeat, GlobDir = 1,scores;
public Random r = new Random(System.currentTimeMillis());
public P(final int w, final int h) {
timer = new Timer();
reset(w,h,0);
timer.scheduleAtFixedRate(new TimerTask() {public void run() {
final int res = next(GlobDir,posMeat,w,h);
posMeat = (res==RESULT_INC)?nextPosMeat(w,h,-1,0,0,0):posMeat;
scores += (res==RESULT_INC)?1:0;
V.this.invalidateWrapper();
if (res == RESULT_FINISH || res == RESULT_SELF || res == RESULT_WIN) reset(w,h,0);
}}, 0, RATE);}
public void reset(int width, int height,int i){
for(GlobDir = 1,scores = 0,arSnake.clear(); i < arI.length;arSnake.add(arI[i]),i++){}
posMeat = nextPosMeat(width, height,-1,0,0,0);}
public void setDir(int dir){
GlobDir = ((dir + GlobDir == 1) || (dir + GlobDir == 5))?GlobDir:dir;}
public int nextPosMeat(int width, int height,int resI,int i,int count,int exit) {
for (int r0 = r.nextInt(width*height - arSnake.size()) + 1; i < height*width && exit == 0;count+=(arSnake.contains(Integer.valueOf(i))?0:1),exit = ((count == r0)?1:0),resI = (exit==1?i:resI),i+=(exit == 1)?0:1){}
return resI;}
public int next(int d, int p, int width, int height){//0-up,1-down,2-left,3-right
if (arSnake.size() >= width*height - 1) return RESULT_WIN;
int y = arSnake.get(0) / width, x = arSnake.get(0) % width;
if ((d == 0 && --y < 0) || (d == 1 && ++y >= height) || (d == 2 && --x < 0) || (d == 3 && ++x >= width) ) return RESULT_FINISH;
if (arSnake.contains(Integer.valueOf(x + y * width))) return RESULT_SELF;
arSnake.add(0,x + y * width);
if (x + y * width == p) return RESULT_INC;
arSnake.remove(arSnake.size() - 1);
return RESULT_OK;}}}}
Как было сэкономлено место.
Экономия места в коде
1. Использование for, в параметрах которого записано несколько операций.
Пример:
было
for (int i = 0; i < ph.arSnake.size(); i++){
canvas.drawCircle((ph.arSnake.get(i) % width_cells +.5f) * diam_cell, (ph.arSnake.get(i) / width_cells +.5f) * diam_cell, diam_cell*.5f, paintSnake);}
стало
for (int i = 0; i < ph.arSnake.size(); canvas.drawCircle((ph.arSnake.get(i) % width_cells +.5f) * diam_cell, (ph.arSnake.get(i) / width_cells +.5f) * diam_cell, diam_cell*.5f, paintSnake),i++){}
2. ()?: вместо сложных if
ph.setDir((event.getActionMasked() == MotionEvent.ACTION_UP)?((Math.abs(event.getX() - x) > Math.abs(event.getY() - y) && (event.getX() - x) > 0)?3:(((Math.abs(event.getX() - x) > Math.abs(event.getY() - y)) && (event.getX() - x) <= 0)?2:((event.getY() - y) > 0?1:0))):ph.GlobDir);
3. инримент +±- совместно с if, — заменяет целые блоки switch-case
if ((d == 0 && --y < 0) || (d == 1 && ++y >= height) || (d == 2 && --x < 0) || (d == 3 && ++x >= width) ) return RESULT_FINISH;
4. инициализация переменных в методах.
пример
было (1ая версия функции)
//где-то в коде
int v = summ();
//описание функции
public int summ(){
int s = 0;
for (int i = 0; i < 10; i++){
s += i;
}
return s;
}
стало (2ая версия функции)
int v = summ(0,0);
public int summ(int i,int s){
for(;i < 10; i++){
s += i;
}
return s;
}
Уменьшения строк для пунктов 1-3 не дают уменьшения размера выходного apk, так как варианты «до» и «после» в jar-файле имеют примерно одинаковый код.
С пунктом 4 странно на получается странно. Если взять и протестировать использование функции summ в разных вариантах во всех трёх проектах (helloworld, small, snake), как изменяются размеры файлов при использовании в проекте этих методов, то получится следующая картина (цифры — количество байт):
Jar-файл уменьшается при использовании второй функции, а apk и dex увеличиваются. Скорее всего это связано с архивированием данных. Возможно, в некоторых случаях это даст свои результаты. Правда, они могут не оправдать Ваши ожидания.
На этом, пожалуй, всё.
Итог
Резюмируя, получилось 86 строк и 5696 байт для apk.
Ещё раз ссылки на проекты.
Small
Snake
комментарии (11)