These are chat archives for rus-speaking/android-off-topic

9th
Jan 2016
Vladimir Mironov
@nsk-mironov
Jan 09 2016 09:23

Ребят, а есть кто-нибудь, кто хорошо разбирается в байткоде и внутренностях JVM? Допустим у меня есть код:

public open class Foo {
  public fun foo() = Unit
}

public class Bar : Foo() {
  public fun bar() = foo()
}

Метод Bar.bar компилируется в:

// access flags 0x11
public final bar()V
 L0
  LINENUMBER 8 L0
  ALOAD 0
  INVOKEVIRTUAL com/github/vmironov/Bar.foo ()V
  RETURN
 L1
  LOCALVARIABLE this Lcom/github/vmironov/Bar; L0 L1 0
  MAXSTACK = 1
  MAXLOCALS = 1

Вопросы:

  1. Почему при вызове метода foo в INVOKEVIRTUAL передается текущий класс Bar, а не класс, в котором метод был объявлен (т.е. Foo)?
  2. Насколько законно в байткоде выше просто заменить Bar на Foo и надеяться, что все будет работать?
  3. К каким последствиям такая замена может привести? Есть подозрение, что это может повлиять на перформанс, но я слабо представляю как работает JIT и как диспатчатся виртуальные методы, поэтому это только предположение.

(код написан на котлине, но это не принципиально, javac ведет себя точно так же)

moonsweel
@moonsweel
Jan 09 2016 12:06
предположу, что это произошло потому, что Bar наследуется от Foo, наследник же содержит то, что объявлено в родителе.
покажи, как выглядит байткод для Foo.
и в чём смысл bar() = foo() ?
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:15
Предположений у меня самого куча, мне бы точный ответ
что делает bar() = foo() можно понять из байткода (метод, который вызывает другой метод и больше ничего не делает)
moonsweel
@moonsweel
Jan 09 2016 12:28
это то понятно, я спросил "в чём смысл", а не что делает :)
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:29
Это пример, в нем нет смысла
moonsweel
@moonsweel
Jan 09 2016 12:29
у тебя академический интерес или какая-то реальная проблема, которую надо решить?
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:34
я даже боюсь рассказывать, зачем мне это нужно :)
ну ок, постараюсь объяснить
если в двух словах, я делаю немного магии и модифицирую байткод, чтобы уменьшить количество методов в приложении
все же знают про 65к, надеюсь про это никому не нужно рассказывать
moonsweel
@moonsweel
Jan 09 2016 12:35
а proguard?
а multidex?
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:37
я просто вижу кучу способов вырезать много методов, которые прогуард не поддерживает
и у меня просто спортивный интерес эти способы заимплементить
так вот, возвращаясь к исходной проблеме
на самом деле, ограничение в 65к - это ограничение не на суммарное количество методов в дексе, а на количество методов, которые можно вызвать
с точки зрения декса, вызов INVOKEVIRTUAL com/github/vmironov/Foo.foo ()Vи INVOKEVIRTUAL com/github/vmironov/Bar.foo ()V - это два разных метода
и для них создается два референса
хотя на самом деле метод то один и диспатчится это одинаково
так вот теория, если пробежаться по байткоду и заменить все такие INVOKEVIRTUAL вызовы, на вызовы с базовыми классами, то это позволит избавиться от кучи дублирующися референсов
это даже не теория, я уже сделал такой плагин
moonsweel
@moonsweel
Jan 09 2016 12:41
как-то странно немного, что у тебя встала проблема дублирования методов.
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:41
оно работает, на нашем приложении количество методов сократилось на 2к (ДВЕ ТЫСЯЧИ)
moonsweel
@moonsweel
Jan 09 2016 12:41
О_О
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:41
что очень много, учитывая что оптимизация очень простая
так вот, несмотря на то что это работает, я не знаю какие последствия этой оптимизации
moonsweel
@moonsweel
Jan 09 2016 12:42
у вас 2 тысячи методов содержат в себе только вызов метода из суперкласса?
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:42
ну нет же
сейчас пример напишу
public class TestFragment extends Fragment {
    @Override
    public void onCreate(final Bundle bundle) {
        super.onCreate(bundle);
        setHasOptionsMenu(true);
    }
}
например для этого класса будет создан дополнительный референс на метод для setHasOptionsMenu
да и вообще, в любой ситуации когда у тебя есть ссылка на дочерний класс и ты вызываешь у нее метод родительского класса
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:48
и это очень частая ситуация, поэтому и 2к
moonsweel
@moonsweel
Jan 09 2016 12:48
да, теперь понятно.
@nsk-mironov а если метод объявить финальным, такой же результат будет?
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:51
да, final на это никак не влияет
поэтому мне очень интересно, почему компилятор не подставляет класс, в котором метод был объявлен
moonsweel
@moonsweel
Jan 09 2016 12:53
была мысль, что это сделано потому, что комплятор не проверяет дерево наследования. то есть ему неизвестно, переопределён метод или нет.
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:53
причем так делает и javac и kotlinc
и вопрос, то ли ребятам правда было тупо лень пробежаться по иерархии, и найти правильный класс
moonsweel
@moonsweel
Jan 09 2016 12:54
не, это вполне может быть косяк :)
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:54
то ли это какая-то очень хитрая и непонятная для меня оптимизация, которая облегчает процесс диспатчинга и помогает jit'у генерить более оптимальный код
ну я вот тоже искренне надеюсь, что это ни на что не влияет
moonsweel
@moonsweel
Jan 09 2016 12:55
а как быть с методами интерфейса и абстрактными методами?
Vladimir Mironov
@nsk-mironov
Jan 09 2016 12:56
а что с ними не так? :)
moonsweel
@moonsweel
Jan 09 2016 12:57
ну если проверять иерархию на предмет override, то ничего.
Dmitriy Zaitsev
@DmitriyZaitsev
Jan 09 2016 14:42

@nsk-mironov дык а если ты в Bar какой-то из методов предка Foo переопределишь, а потом в байткоде вызов Bar.foo() заменишь на Foo.foo(), то будет беда.
Bar наследуется от Foo, а значит становится послноценным обладателем всех методов предка, поэтому такой вызов INVOKEVIRTUAL видится честным, потому что

fun bar() = foo()

это не то же самое, что

fun bar() = super.foo()
moonsweel
@moonsweel
Jan 09 2016 14:58
@DmitriyZaitsev посмотри сообщение с примером на фрагменте.
речь как раз о том, что можно сэкономить.
Dmitriy Zaitsev
@DmitriyZaitsev
Jan 09 2016 15:07

@moonsweel я и говорю, как сэкономить.
Вот два класса:

class Foo {
  boolean foo() { return true; }
}

и

class Bar extends Foo {
  boolean bar() { return foo(); }
}

Смотрим байткод:

// access flags 0x0
  bar()Z
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKEVIRTUAL Bar.foo ()Z
    IRETURN

а теперь вызов foo() меняем на super.foo() и получаем

  // access flags 0x0
  bar()Z
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL Foo.foo ()Z
    IRETURN
moonsweel
@moonsweel
Jan 09 2016 15:09
ну было бы странно, если бы с super было по-другому. речь о том, что даже в случае, если метод не переопределён, создается новый референс
Vladimir Mironov
@nsk-mironov
Jan 09 2016 15:09

а теперь вызов foo() меняем на super.foo() и получаем

и получаем совсем другое поведение

Dmitriy Zaitsev
@DmitriyZaitsev
Jan 09 2016 15:10
Почему? То же самое, т.к. foo() в наследнике не переопределялся
Vladimir Mironov
@nsk-mironov
Jan 09 2016 15:11
этого никак нельзя знать
потому что может появиться
class Magic extends Bar {
    @Override 
    boolean foo() {
        return true;
    }
}
Dmitriy Zaitsev
@DmitriyZaitsev
Jan 09 2016 15:14
Может. И?
на вызов Bar.bar() он не влияет здесь
Vladimir Mironov
@nsk-mironov
Jan 09 2016 15:17
не влияет, если там super.foo()
но я уже сказал, что в этом случае другое поведение получается
для INVOKESPECIAL плевать какой ты ему класс дашь
базовый или дочерний
ой
Dmitriy Zaitsev
@DmitriyZaitsev
Jan 09 2016 15:18
другое, если foo() переопределялся внутри Bar
Vladimir Mironov
@nsk-mironov
Jan 09 2016 15:18
для INVOKEVIRTUAL конечно же
Dmitriy Zaitsev
@DmitriyZaitsev
Jan 09 2016 15:19
ай, блин. точно. там же INVOKESPECIAL теперь
frolovdaniil
@frolovdaniil
Jan 09 2016 17:26
никак не могу разобраться с интентом, построил кастомный нотификейшон
и от него через интент нужно вызвать метод .mediaPlayer.pause()
не могу понять как это реализовать надо
Intent PauseIntent = new Intent(this, MainActivity.mediaPlayer.pause());
Vladimir Mironov
@nsk-mironov
Jan 09 2016 17:33
Конечно же влияет
Гиттер очень жестко тупит
зашел с телефона, там было набрано старое сообщение, и он конечно же решил его сам отправить
молодцы ребята
Dmitriy Zaitsev
@DmitriyZaitsev
Jan 09 2016 17:39
что на что влияет? ты нашел ответ на свой вопрос?) интересно же
Vladimir Mironov
@nsk-mironov
Jan 09 2016 17:48
это был ответ на твое сообщение
на вызов Bar.bar() он не влияет здесь
я сначала с телефон начал писать, а потом решил с компа ответить
а гиттер умничка все сохранил
и когда я опять страницу с телефона открыл, то еще какого-то черта решил отправить сообщение