В процессе чтения исходников 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 ООО «МТ ФИНАНС»
Автор: DmitryTsyvtsyn