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

от автора

Интересные приёмы, взятые из исходников 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 💻


ссылка на оригинал статьи https://habr.com/ru/articles/838330/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *