Распутываем легаси-код на Android проекте
Введение
Разработчики, присоединяющиеся к проектам с многолетней историей, неизбежно сталкиваются с унаследованным кодом. Когда первоначальная команда уходит, документация исчезает, а на вопросы отвечают "так исторически сложилось". Приложения могут страдать от торможения, утечек памяти и запутанной логики управления состоянием.
В этой статье я поделюсь тремя практическими методами, которые помогут быстро найти узкие места и начать приводить проект в порядок.
1. Логирование изменений в базе данных
Все фреймворки ORM поддерживают отслеживание запросов. В Room это реализуется следующим образом:
Room.databaseBuilder(context, AppDatabase::class.java, "mydb.db")
.setQueryCallback(
{ sqlQuery, bindArgs ->
Log.wtf("my_tag", "query: $sqlQuery args: $bindArgs")
},
Executors.newSingleThreadExecutor()
)
.build()
Что это даёт?
Логирование помогает выявить избыточные операции. На практике я обнаружил, что при запуске приложения десятки таблиц заполнялись ненужными данными, замедляя инициализацию приложения на несколько секунд.
После анализа логов удалось оптимизировать процесс запуска и значительно ускорить загрузку приложения.
2. Логирование изменений в SharedPreferences
SharedPreferences иногда неправильно используется для сохранения больших структур данных или в качестве шины событий, что вызывает фризы UI.
Подписка на изменения
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
Log.wtf("my_tag", "key: $key value: ${prefs.all[key]}")
}
prefs.appPreferences.registerOnSharedPreferenceChangeListener(listener)
Важно: не забывайте отписываться!
prefs.appPreferences.unregisterOnSharedPreferenceChangeListener(listener)
Этот подход помогает отследить, какие ключи меняются слишком часто или содержат избыточные данные.
3. Логирование стектрейса вызова
Вместо медленного отладчика можно использовать функцию логирования со стеком вызовов. Это особенно полезно для отслеживания, откуда вызывается та или иная переменная или функция.
Базовая реализация
object AppLogger {
private const val DEFAULT_TAG = "AppLog"
fun logStack(message: String? = null, tag: String = DEFAULT_TAG) {
val threadName = "Call on Thread: ${Thread.currentThread().name}\n"
message?.run { Log.wtf(tag, this) }
val stack = Thread.currentThread().stackTrace
.filter { it.className.contains(this::class.java.name).not() }
.filter { it.className.contains(LoggingProperty::class.java.name).not() }
.filter { it.className !in listOf("dalvik.system.VMStack", "java.lang.Thread") }
.joinToString(prefix = threadName, separator = "\n") { element ->
"at ${element.className}.${element.methodName} (${element.fileName}:${element.lineNumber})"
}
Log.wtf(tag, stack)
}
}
Интеграция в геттеры и сеттеры
Можно интегрировать логирование в геттеры и сеттеры мутабельных переменных:
var currentLocation: GeoPoint? = null
get() {
AppLogger.logStack("Get currentLocation $field")
return field
}
set(value) {
AppLogger.logStack("Set currentLocation value: $value field: $field")
field = value
}
Элегантный подход с делегатом
Более элегантный подход с использованием делегированных свойств Kotlin:
class LoggingProperty<T>(private var value: T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
AppLogger.logStack()
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
AppLogger.logStack()
value = newValue
}
}
Применение делегата
var currentLocation: GeoPoint? by LoggingProperty(null)
Теперь каждое обращение к переменной currentLocation будет логироваться с полным стеком вызовов, что позволяет быстро понять, откуда происходят изменения.
Заключение
Эти три методики помогают быстро найти узкие места в легаси-коде и начать приводить проект в порядок:
- Логирование БД — выявляет избыточные запросы и проблемы с производительностью
- Логирование SharedPreferences — находит неправильное использование для хранения данных
- Логирование стектрейса — помогает отследить источники изменений переменных
Эти инструменты особенно полезны на начальных этапах работы с незнакомым проектом, когда нужно быстро разобраться в архитектуре и найти проблемные места.
Если у вас есть свои практики для работы с легаси-кодом — буду рад обсудить их!