СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Змейка в 33 строки для Android на Java в черновиках


Конечно, 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)

+3
Suvitruf ,  
Начинается новая волна "*** в 33 строки на ***"?
+1
alrusdi ,  
Оно ведь даже на github имеет горизонтальный скролл. Не защитано.
0
Yurevich1 ,  
Поясните, пожалуйста. У Вас view больше размера экрана получается? Вы это имеете в виду?
+1
alrusdi ,  
Нет, я о форматирвании кода — если хвастаться коротким кодом, то желательно чтобы он был не больше 120 символов в ширину. На просмотре в браузере на github уже примерно со 110 символов в ширину появляется горизонтальный скрол и код становится трудно читать
0
+1 –1
Yurevich1 ,  
Понятное дело, что строки будут длинными. Там же много операций в одну определил.
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){}</sourse> -  есть одна строчка. Ну, максимум две.
. Вас какая именно строчка не устродила?
+1
Yurevich1 ,  
Хочу пояснить по чуть выше приведённому коду:
For(;;){} — есть одна строчка. Если записать
for(;;){
}
, то получается две. А какая длина строки в github, не важно. Важно, что логика записывается в языке однострочно. Я не записываю несколько операций в одну строчку. Я пользуюсь особенностями языка, чтобы в команде выполнялось сразу несколько действий. Тут, например, нет тупой однострочной записи нескольких операций:
int i = 0; i++; return i;</sourse>
0
WToll ,   * (был изменён)
Надеюсь критика принимается. Честно говоря — неочень, ни коротко ни красиво. Особо не напрягаясь ужал до 68 в сумме за 15 мин. pastebin.com/7N8kh7Xi
Да и почитать java code conventions Вам бы не помешало, экономия экономией но многие вещи выглядят просто некрасиво без причины.

ps: fullscreen, no_title и orientation перекочевали в xml.
0
Yurevich1 ,  
Спасибо за критику, конечно, она принимается. Fullscreen действительно можно скинуть в AndroidManifest.xml (на две строчки можно сократить), и ещё, пожалуй, List лучше использовать (чтобы убрать ещё 1-2 строки).
Но посмотрите, пожалуйста, ответную критику Вашего кода.
У Вас в основном код уменьшен за счёт переменной класса Timer. Это неправильно. onDestroy обязан быть. В противном случае будет работать фоновый процесс и после закрытия приложения (только что проверил). По той же причине не совсем правильно будет вместо reset(...) писать ph = new P(...). Вы, таким образом просто наплодите не убитых фоновых Timer'ов. А раз нельзя (точней, неудобно) делать ph = new P(...), то и reset(...) (он хранит в себе две функции — пересоздать Змею и пересоздать еду) удалять нельзя.
0
WToll ,  
Так как написано вообще писать нельзя :) Но если стоит задача справиться уложившись в минимальное количество строк, то приходится чем-то жертвовать. Иначе не интересно.
0
raid ,  
Когда открывал статью, хотел увидеть код. Но когда дошёл до раздела «Как было сэкономлено место», желание пропало…
+1
Yurevich1 ,  
А что мешает нажать на спойлер «код» или посмотреть его на Github? Или Вы что-то ещё хотите увидеть?