Распутываем легаси-код на 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 начинал фризить. А в другом случае SharedPreferences использовался как шина событий.
Логирование изменений поможет выявить подобные проблемы:
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
}
Можно сделать ещё элегантнее с помощью делегата:
class LoggingProperty(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)
Такой подход сильно упрощает работу с кодом, где переменные меняются хаотично, а логика запутана. Эти три совета — мой проверенный арсенал для погружения в легаси-код. Они помогают быстро найти узкие места и начать приводить проект в порядок. Но я уверен, у вас есть свои практики! Делитесь ими в комментариях — будет интересно обсудить и, возможно, дополнить этот список.