В процессе чтения исходников Android SDK я замечал интересные механики и приёмы написания кода, какие-то из них до сих пор используются при создании новых библиотек, другие, напротив, заменены более логичными и понятными конструкциями. В этой статье я постараюсь перечислить всё, что смог заметить сам при изучении исходников Android’а. Сразу отмечу: эта статья не претендует на полноту материала и возможно вы нашли даже больше интересных моментов при чтении кода, ладно, погнали, короче!
▍ Переопределение protected метода на public в наследуемом классе
Думаю все, кто изучал Java, знают, что можно сделать так (в Kotlin такой возможности нет):
public abstract class Property<T> { private T value; public Property(T value) { this.value = value; } // метод setValue() недоступен, так как он protected protected void setValue(T value) { this.value = value } public T getValue() { return value; } } public class MutableProperty<T> extends Property<T> { // метод setValue() переопределён как public, в Kotlin так нельзя( @Override public void setValue(T value) { super.setValue(value); } }
Такая механика языка используется для реализации MutableLiveData:
public abstract class LiveData<T> { protected void postValue(T value) { ... } @MainThread protected void setValue(T value) { ... } } public class MutableLiveData<T> extends LiveData<T> { ... @Override public void postValue(T value) { super.postValue(value); } @Override public void setValue(T value) { super.setValue(value); } }
На самом деле такой способ создания изменяемых/неизменяемых классов нарушает концепцию наследования, так как мы не добавляем новую функциональность, а «включаем» её.
Более предпочтительный способ, как это можно сделать, используя наследование:
public class LiveData<T> { protected T value; public LiveData(T value) { this.value = value; } public T getValue() { return value; } } class MutableLiveData<T> extends LiveData<T> { public MutableLiveData(T value) { super(value); } public void setValue(T newValue) { this.value = newValue; } }
В любом случае механика переопределения protected на public имеет место быть.
▍ ThreadLocal переменные
Если вы никогда не слышали, есть такая штука, которая позволяет создать уникальный экземпляр объекта в пределах одного потока, своеобразный Singleton потока.
Посмотрим, для чего это можно использовать:
public final class Looper { static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } }
Looper — один из самых базовых классов Android SDK, на котором построен бесконечный цикл и очередь событий (сообщений).
ThreadLocal гарантирует, что Looper будет единственным экземпляром в пределах текущего потока, так как в одном потоке может быть только один бесконечный цикл для обработки событий.
Если вы создадите новый поток и вызовете Looper.prepare() на нём, то для него будет создан свой уникальный экземпляр Looper и т. д.
Сложно предположить, где ThreadLocal может пригодиться в повседневной Android разработке, но имейте в виду, если вам нужен уникальный экземпляр в пределах потока, используйте ThreadLocal и обязательно посмотрите документацию с примером.
▍ Проксирование/Делегирование методов другому классу
Гораздо проще показать на примере AppCompatActivity из библиотеки appcompat:
public class AppCompatActivity extends ... { @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(getDelegate().attachBaseContext2(newBase)); } @Override public void setTheme(@StyleRes final int resId) { super.setTheme(resId); getDelegate().setTheme(resId); } @Override protected void onPostCreate(@Nullable Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); getDelegate().onPostCreate(savedInstanceState); } @Nullable public ActionBar getSupportActionBar() { return getDelegate().getSupportActionBar(); } public void setSupportActionBar(@Nullable Toolbar toolbar) { getDelegate().setSupportActionBar(toolbar); } }
Метод getDelegate() возвращает объект класса AppCompatDelegate, методы которого реализуют функциональность для методов AppCompatActivity.
Это может пригодиться, когда требуется прозрачно добавить новую функциональность для класса с дальнейшей возможностью на её расширение, «прозрачно» — значит, без влияния на пользователей этого класса.
Приведу простой пример добавления новой функциональности:
class AppCompatActivity extends ... { @NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { // в Android 34 появились специфичные штуки if (Build.VERSION.SDK_INT >= 34) { mDelegate = AppCompatDelegate.create34(this, this); } else { mDelegate = AppCompatDelegate.create(this, this); } } return mDelegate; } }
Пользователю Android SDK не придётся менять свой код + на более свежих версиях Android’а будут работать новые фишки.
▍ Наследование с реализацией интерфейсов для построения единого API
Многие AppCompat*View классы реализованы таким образом для обеспечения единого API:
public class AppCompatImageView extends ImageView implements TintableBackgroundView, ... {} public class AppCompatButton extends Button implements TintableBackgroundView, ... {} public class AppCompatTextView extends TextView implements TintableBackgroundView, ... {}
TintableBackgroundView — это простой интерфейс для изменения цвета background’а:
public interface TintableBackgroundView { void setSupportBackgroundTintList(@Nullable ColorStateList tint); @Nullable ColorStateList getSupportBackgroundTintList(); @Nullable PorterDuff.Mode getSupportBackgroundTintMode(); }
Такой механизм использования интерфейсов имеет несколько преимуществ:
- легко добавить новую функциональность в независимости от существующей: например, изменение цвета для background’а,
- простой и единый интерфейс: не нужно смотреть документацию для каждого компонента, чтобы понять, как у него поменять цвет,
- полиморфизм.
Последнее проще продемонстрировать:
val views: List<TintableBackgroundView> = listOf( AppCompatTextView(this), AppCompatButton(this), AppCompatImageView(this) ) val newColor = ColorStateList.valueOf(0xff333333.toInt()) views.forEach { view -> view.supportBackgroundTintList = newColor }
▍ Создание дополнительного типа в качестве пустого значения
Иногда возникают ситуации, когда null не совсем подходит на роль «нет значения», и в таких случаях приходится выкручиваться дополнительным типом:
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer // дополнительный тип UNINITIALIZED_VALUE указывает, что поле _value ещё не было инициализировано @Volatile private var _value: Any? = UNINITIALIZED_VALUE private val lock = lock ?: this override val value: T get() { val _v1 = _value // проверка состояния поля if (_v1 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return _v1 as T } return synchronized(lock) { val _v2 = _value // вторая проверка состояния поля на случай, если другой поток уже проинициализировал его if (_v2 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") (_v2 as T) } else { val typedValue = initializer!!() _value = typedValue initializer = null typedValue } } } // если поле не равно UNINITIALIZED_VALUE значит оно уже было проинициализировано, // неважно каким значением, им может быть даже null override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE }
Дополнительным типом здесь является UNINITIALIZED_VALUE:
internal object UNINITIALIZED_VALUE
Здесь нельзя обойтись null значением, так как оно входит в диапазон возможных значений:
// временный кэш может быть пустым и тогда значение будет null val temporaryCache by lazy { getTemporaryCache() }
▍ Переиспользуемый пул объектов, реализованный с помощью связанного списка
Возвращаемся к системе обработки событий в Android, а конкретнее нас интересует класс Message:
public final class Message implements Parcelable { public static final Object sPoolSync = new Object(); private static Message sPool; private static int sPoolSize = 0; private static final int MAX_POOL_SIZE = 50; // поле для организации связанного списка Message next; public static Message obtain() { synchronized (sPoolSync) { // если пул сообщений не пустой, берём первое доступное // и возвращаем для переиспользования if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } // в случае, если пул был пустым или закончился, создаём новое сообщение return new Message(); } void recycleUnchecked() { // очистить поля для переиспользования объекта сообщения flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = UID_NONE; workSourceUid = UID_NONE; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { // если лимит сообщений в пуле не превышен, добавляем текущее для переиспользования // в противном случае объект сообщения будет собран сборщиком мусора if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } } }
Пул реализуется с помощью статических полей и методов, конструктор в таком случае желательно делать приватным, чтобы пользователь вашего класса случайно не обошёл логику переиспользования объектов.
▍ Хранение нескольких значений в целочисленном типе с помощью битовых масок
В Android есть так называемый MeasureSpec, кто писал кастомные вьюшки тот в курсе, как извлекаются значения из него:
class CustomView(ctx: Context) : View(ctx) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val width = MeasureSpec.getSize(widthMeasureSpec) val widthMode = MeasureSpec.getMode(widthMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) ... } }
Если глянуть внутрь этих методов, то можно увидеть битовые операции с одним и тем же целочисленным значением:
public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK) } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT;
Чтобы понять, как это работает, распишем значение константы MODE_MASK в двоичной системе (битовые операции работают с отдельными битами):
MODE_MASK = 00000000 00000000 00000000 00000011 << 30 // выполняем побитовый сдвиг влево и получаем значение: MODE_MASK = 11000000 00000000 00000000 00000000
Снова вернёмся к методу MeasureSpec.getMode():
public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); }
Оператор & выполняет побитовую операцию И (bitwise AND), простыми словами, выставляет единичный бит, если оба бита являются таковыми:
01101110 00110001 10001100 01101111 & 11000000 00000000 00000000 00000000 = 01000000 00000000 00000000 00000000
Таким образом метод MeasureSpec.getMode() берёт только первые два бита целочисленного числа, а остальные зануляет.
Два бита нужны для хранения одного из следующих режимов при измерении вьюшек:
// 00000000 00000000 00000000 00000000 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 01000000 00000000 00000000 00000000 public static final int EXACTLY = 1 << MODE_SHIFT; // 10000000 00000000 00000000 00000000 public static final int AT_MOST = 2 << MODE_SHIFT;
Второй метод работает практически аналогично, но только извлекает все биты, кроме первых двух:
public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK) }
Оператор ~ выполняет побитовую инверсию, меняет нулевые биты на единичные и наоборот:
~11000000 00000000 00000000 00000000 = 00111111 11111111 11111111 11111111
После применения инвертированной маски ~MODE_MASK остаются все биты кроме первых двух:
01101110 00110001 10001100 01101111 & 00111111 11111111 11111111 11111111 = 00101110 00110001 10001100 01101111
Обобщим полученные результаты:
- MeasureSpec.getMode() берёт только первые два бита целочисленного значения, а остальные зануляет.
- MeasureSpec.getSize() зануляет первые два бита целочисленного значения и берёт все остальные.
Вот таким элегантным и эффективным способом MeasureSpec хранит в одном целом числе два значения:
- одно из значений: UNSPECIFIED, EXACTLY, AT_MOST,
- размер вьюшки, может быть высота или ширина.
Чтобы создать MeasureSpec из отдельных кусочков, нужно сначала пропустить каждое значение через свою битовую маску, а затем сложить получившиеся значения с помощью побитового оператора ИЛИ (bitwise OR):
final int mode = EXACTLY; final int size = 320; final int measureSpec = (size & ~MODE_MASK) | (mode & MODE_MASK);
Для более любопытных предлагаю чекнуть исходники android.graphics.Color и глянуть, как извлекаются отдельные компоненты RGB модели.
▍ Заключение
Надеюсь, статья оказалась вам полезной, и вы подчерпнули для себя что-то новое, а самое главное, увидели на примерах, что чтение исходников может быть хорошей книгой, где можно узнать что-то новое.
Полезные ссылки:
Пишите в комментах ваше мнение и всем хорошего кода!
© 2024 ООО «МТ ФИНАНС»
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
ссылка на оригинал статьи https://habr.com/ru/articles/838330/
Добавить комментарий