Интересные приёмы, взятые из исходников Android

В процессе чтения исходников 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();
  
}

Такой механизм использования интерфейсов имеет несколько преимуществ:

  1. легко добавить новую функциональность в независимости от существующей: например, изменение цвета для background'а,
  2. простой и единый интерфейс: не нужно смотреть документацию для каждого компонента, чтобы понять, как у него поменять цвет,
  3. полиморфизм.

Последнее проще продемонстрировать:

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

Обобщим полученные результаты:

  1. MeasureSpec.getMode() берёт только первые два бита целочисленного значения, а остальные зануляет.
  2. MeasureSpec.getSize() зануляет первые два бита целочисленного значения и берёт все остальные.

Вот таким элегантным и эффективным способом MeasureSpec хранит в одном целом числе два значения:

  1. одно из значений: UNSPECIFIED, EXACTLY, AT_MOST,
  2. размер вьюшки, может быть высота или ширина.

Чтобы создать MeasureSpec из отдельных кусочков, нужно сначала пропустить каждое значение через свою битовую маску, а затем сложить получившиеся значения с помощью побитового оператора ИЛИ (bitwise OR):

final int mode = EXACTLY;
final int size = 320;
final int measureSpec = (size & ~MODE_MASK) | (mode & MODE_MASK);

Для более любопытных предлагаю чекнуть исходники android.graphics.Color и глянуть, как извлекаются отдельные компоненты RGB модели.

▍ Заключение


Надеюсь, статья оказалась вам полезной, и вы подчерпнули для себя что-то новое, а самое главное, увидели на примерах, что чтение исходников может быть хорошей книгой, где можно узнать что-то новое.

Полезные ссылки:

  1. Мой телеграм канал.
  2. Мой Github репозиторий с полезными материалами.
  3. Другие статьи.

Пишите в комментах ваше мнение и всем хорошего кода!

© 2024 ООО «МТ ФИНАНС»

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (3)


  1. orefkov
    04.09.2024 15:25
    +21

    Класс, предоставляющий доступ к закрытым данным родительского класса - паттерн "Паблик Морозов"


  1. Beholder
    04.09.2024 15:25
    +4

    Думаю все, кто изучал Java, знают, что можно сделать так (в Kotlin такой возможности нет):

    Не совсем так. Можно

    abstract class Property<T>(value: T) {
        open var value: T = value
            protected set
    }
    class MutableProperty<T>(value: T) : Property<T>(value) {
        override var value: T
            get() = super.value
            set(value) { super.value = value }
    }


  1. Spyman
    04.09.2024 15:25

    Первое и последнее — это то, как делать вообщем-то не стоит)

    Третье, четвертое и пятое - вроде вполне класический код, хотя продумать передачу вызова делегату на стадии проектирования было бы прямо вообще хорошо.

    Про threadLocal действтиельно интересный инструмент