Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Обновлено: 24 мая 2023
В Android SDK входит набор инструментов, предназначенных для отладки. Самый важный инструмент при отладке - это LogCat (очень красивое название, которое можно перевести как Логичный Кот). Он отображает сообщения логов (журнал логов), рассылаемые при помощи различных методов.
В студии версии Electric Eel обновили Logcat. Все старые примеры работают, но появились дополнительные возможности.
Рассмотрим на примере стандартное применение Logcat. Очень часто программисту нужно вывести куда-то промежуточные результаты, чтобы понять, почему программа не работает. Особо хитрые временно размещают на экране текстовую метку и выводят туда сообщение при помощи метода textView.setText("Здесь был Васька"). Но есть способ лучше. В Android есть специальный класс android.util.Log для подобных случаев.
Класс android.util.Log позволяет разбивать сообщения по категориям в зависимости от важности. Для разбивки по категориям используются специальные методы, которые легко запомнить по первым буквам, указывающие на категорию:
В первом параметре метода используется строка, называемая тегом. Обычно принято объявлять глобальную статическую строковую переменную TAG в начале кода:
private val TAG = "MyApp"
private static final String TAG = "MyApp";
Некоторые в сложных проектах используют следующий вариант, чтобы понимать, в каком классе происходит вызов:
private val TAG = this.javaClass.simpleName
private static final String TAG = this.getClass().getSimpleName();
Далее уже в любом месте вашей программы вы вызываете нужный метод журналирования с этим тегом:
Log.i(TAG, "Это моё сообщение для записи в журнале");
Пользователи не видят журнал логов. Вы, как разработчик, можете увидеть его через программу LogCat, доступный в Android Studio.
Напишем простой пример с кнопкой.
package ru.alexanderklimov.flamingo
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private val TAG = this.javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button: Button = findViewById(R.id.button)
button.setOnClickListener {
Log.i(TAG, "Кот нажал на кнопку")
}
}
}
Запускаем программу и в студии открываем вкладку Logcat:
Вы увидите несколько строк с технической информацией.
--------- beginning of system
--------- beginning of main
--------------- PROCESS STARTED (25918) for package ru.alexanderklimov.flamingo ---------------
2023-05-24 16:31:08.294 25918-25961 OpenGLRenderer ...
Если нажать на кнопку, то появится новая информация - наш приготовленный текст.
2023-05-24 16:51:57.482 25918-25918 MainActivity ru.alexanderklimov.flamingo I Кот нажал на кнопку
Обратите внимание, что по умолчанию в Logcat установлен фильтр package:mine . Это позволяет отсечь множество системных сообщений, которые вам не нужны. Вы можете удалить фильтр и увидеть все эти сообщения. Убедившись, что они вам действительно не нужны, верните фильтр обратно.
Фильтры можно настраивать по своему вкусу. Например, если у вас много категорий логов, то можете выбрать нужный. Просто начинайте вводить текст, подсказки будут вам помогать при вводе текста. Например, добавим ещё один фильтр.
package:mine level:info
Для нашего примера особо ничего не изменилось. Дело в том, что категория info включает в себя все остальные категории. Фильтр level:warn будет показывать только категории Warning и Error.
Кроме показа сообщений о своём приложении через фильтр package:mine можно установить фильтры для других приложений: package:package-ID (указать идентификатор)
Если вы установили много тэгов, то посмотреть только нужные можно через фильтр tag:. Тогда вы не увидите лишние записи, которые были по умолчанию.
package:mine tag:MainActivity
Можно убрать лишние записи через знак минуса. Например, уберём тэг OpenGLRenderer:
package:mine -tag:OpenGLRenderer
Все логи имеют временную метку. Можно использовать фильтр age с указанием интервала.
Кстати, все ваши фильтры запоминаются и хранятся в истории. Нажмите на значок слева от фильтра, чтобы увидеть полный список. У каждого фильтра есть крестик, позволяющий удалить фильтр из истории.
Слева в списке фильтров есть значок звёзды - вы можете поместить любимые фильтры в Избранное.
Есть и другие фильтры, а также возможность использовать регулярные выражения. Но на первых порах достаточно использовать базовые возможности.
В левой части Logcat есть различные значки. Один из них отвечает за внешний вид журнала логов: можно выбрать вариант Standard View или Compact View. Либо вы можете настроить под себя, выбрав Modify Views, тогда у вас откроется диалоговое окно с настройками.
Можно создавать несколько вкладок Logcat, нажимая на значок +.
Далее идёт текст к старой версии Logcat. Возможны некоторые разночтения с новой версией, но в целом всё работает.
Полный вид сообщения выглядит следующим образом.
03-09 20:44:14.460 3851-3879/ru.alexanderklimov.cat I/OpenGLRenderer: Initialized EGL, version 1.4
Подобные длинные сообщения не всегда удобны для чтения. Вы можете убрать ненужные элементы. Для этого выберите значок LogCat Header в виде шестерёнки и уберите флажки у опций.
В LogCat вы можете отфильтровать сообщение по заданному типу, чтобы видеть на экране только свои сообщения. Для этого выберите нужный тип тега из выпадающего списка Verbose.
Типы сообщений можно раскрасить разными цветами через настройки File | Settings | Editor | Colors Scheme | Android Logcat.
Для отслеживания сообщений с заданным текстом введите в поле поиска нужную строку и нажмите Enter.
Также активно используйте варианты из других выпадающих списков. Например, выбирайте свой пакет из второй колонки, а в последней выбирайте Show only selected application. Для более точной настройки используйте Edit Fiter Configuration.
По умолчанию, окно LogCat выводится в нижней части студии. При желании, можно выбрать другие варианты через значок настроек окна.
LogCat также можно запустить из командной строки:
adb logcat
Параметры командной строки смотрите в документации.
Настоятельно рекомендуется удалять все вызовы LogCat в готовых приложениях. Если проект очень большой и вызовы журналирования разбросаны по всем местам кода, то ручное удаление (или комментирование) становится утомительным занятием. Многие разработчики используют следующую хитрость - создают обёртку вокруг вызова методов LogCat.
public static final boolean isDebug = false;
public final String TAG = "MyLogger";
public void MyLogger(String statement){
if (isDebug) {
Log.v(TAG, statement);
}
}
Теперь остаётся только присвоить нужное значение переменной isDebug перед созданием готового apk-файла для распространения.
Способ устарел. В 17-й версии Android Build Tools появился класс BuildConfig, содержащий статическое поле DEBUG. Можно проверить следующим образом:
if (BuildConfig.DEBUG) {
// Режим отладки, ведём логи
}
Способ для продвинутых - например, требуется релиз с выводом в лог, или наоборот — debug с выключенным выводом. В этом случае можно создать собственный параметр и добавить его в секцию buildType gradle-файла:
buildTypes {
release {
// Здесь настройки релиза
buildConfigField "boolean", "USE_LOG", "false"
}
debug {
buildConfigField "boolean", "USE_LOG", "true"
}
// «Релиз с логами» наследуем от обычного релиза
releaseWithLog.initWith(buildTypes.release)
releaseWithLog {
buildConfigField "boolean", "USE_LOG", "true"
}
}
В этом случае конфигурация releaseWithLog будет являться релизной сборкой с ведением логов. Естественно, в коде слегка поменяется проверка:
if (BuildConfig.USE_LOG) {
// Здесь используем логи
}
Попался в сети пример для просмотра сообщений LogCat на устройстве. С примером не разбирался, оставлю здесь на память.
Разметка
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>
Разметка для элемента списка:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/txtLogString"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
Добавляем разрешение в манифест:
<uses-permission android:name="android.permission.READ_LOGS" />
Код для класса:
package ru.alexanderklimov.test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
public class TestActivity extends ListActivity {
private LogStringadapter adapter = null;
private ArrayList<String> logarray = null;
private LogReaderTask logReaderTask = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
logarray = new ArrayList<String>();
adapter = new LogStringadapter(this, R.id.txtLogString, logarray);
setListAdapter(adapter);
logReaderTask = new LogReaderTask();
logReaderTask.execute();
}
@Override
protected void onDestroy() {
logReaderTask.stopTask();
super.onDestroy();
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
final AlertDialog.Builder builder = new AlertDialog.Builder(
TestActivity.this);
String text = ((String) ((TextView) v).getText());
builder.setMessage(text);
builder.show();
}
private int getLogColor(String type) {
int color = Color.BLUE;
if (type.equals("D")) {
color = Color.rgb(0, 0, 200);
} else if (type.equals("W")) {
color = Color.rgb(128, 0, 0);
} else if (type.equals("E")) {
color = Color.rgb(255, 0, 0);
;
} else if (type.equals("I")) {
color = Color.rgb(0, 128, 0);
;
}
return color;
}
private class LogStringadapter extends ArrayAdapter<String> {
private List<String> objects = null;
public LogStringadapter(Context context, int textviewid,
List<String> objects) {
super(context, textviewid, objects);
this.objects = objects;
}
@Override
public int getCount() {
return ((null != objects) ? objects.size() : 0);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public String getItem(int position) {
return ((null != objects) ? objects.get(position) : null);
}
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (null == view) {
LayoutInflater vi = (LayoutInflater) TestActivity.this
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.logitem, null);
}
String data = objects.get(position);
if (null != data) {
TextView textview = (TextView) view
.findViewById(R.id.txtLogString);
String type = data.substring(0, 1);
String line = data.substring(2);
textview.setText(line);
textview.setTextColor(getLogColor(type));
}
return view;
}
}
private class LogReaderTask extends AsyncTask<Void, String, Void> {
private final String[] LOGCAT_CMD = new String[] { "logcat" };
private final int BUFFER_SIZE = 1024;
private boolean isRunning = true;
private Process logprocess = null;
private BufferedReader reader = null;
private String[] line = null;
@Override
protected Void doInBackground(Void... params) {
try {
logprocess = Runtime.getRuntime().exec(LOGCAT_CMD);
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
}
try {
reader = new BufferedReader(new InputStreamReader(
logprocess.getInputStream()), BUFFER_SIZE);
} catch (IllegalArgumentException e) {
e.printStackTrace();
isRunning = false;
}
line = new String[1];
try {
while (isRunning) {
line[0] = reader.readLine();
publishProgress(line);
}
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
}
return null;
}
@Override
protected void onCancelled() {
super.onCancelled();
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
}
@Override
protected void onProgressUpdate(String... values) {
super.onProgressUpdate(values);
adapter.add(values[0]);
}
public void stopTask() {
isRunning = false;
logprocess.destroy();
}
}
}
Библиотека Timber является надстройкой поверх Log. Автором является Jake Wharton, написавший много полезных утилит. Страница на Гитхабе - https://github.com/JakeWharton/timber.
Меняем цвет для сообщений в LogCat
Android Logcat for Professionals Tutorial - YouTube