Рекомендуемый Android-метод безопасной поддержки нового apis имеет ошибку, если класс реализует новый интерфейс. Зачем?

Чтобы поддерживать разные уровни Api, я использую описанную здесь технику: http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html

Вот пример из статьи:

public static VersionedGestureDetector newInstance(Context context,
        OnGestureListener listener) {
    final int sdkVersion = Integer.parseInt(Build.VERSION.SDK);
    VersionedGestureDetector detector = null;
    if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
        detector = new CupcakeDetector();
    } else if (sdkVersion < Build.VERSION_CODES.FROYO) {
        detector = new EclairDetector();
    } else {
        detector = new FroyoDetector(context);
    }

    detector.mListener = listener;

    return detector;
}

Этот подход "использует ленивость ClassLoaders". Для устройств с новым уровнем API (в примере Case, Froyo) он может использовать класс Froyo, который обращается к API в более новой версии. Для более старых устройств они получают класс, который использует только старые API.

Это работает отлично.

Однако, если вы создаете FroyoDetector интерфейс, который существует только на более новом уровне api, когда вызывается newInstance(), даже до того, как он запускает какой-либо код внутри этого метода, он пытается загрузить класс интерфейса, который FroyoDetector реализует и помещает ошибку в журналы, говорящие, что класс FroyoDetector не может быть загружен.

Итак, мой вопрос: зачем это происходит? У меня создалось впечатление, что при таком методе новый класс не будет загружен до тех пор, пока он не будет напрямую указан в первый раз. Однако, если вы добавляете к нему интерфейс, кажется, он пытается загрузить его даже без вызова строки detector = new FroyoDetector(context);.

Вот какой код воспроизводит проблему:

Это таргетинг на приложение sdk 16 с минимальным значением 8. Выполнение этого на устройстве 2.3 воспроизводит проблему.

Вот три класса:

public class VersionedLoader {

    public static VersionedLoader newInstance() {
        if (Build.VERSION.SDK_INT < 12) {
            return new OldVersionLoader();
        } else {
            return new NewVersionLoader();
        }
    }

}

-

public class OldVersionLoader extends VersionedLoader {

}

-

@TargetApi(11)
public class NewVersionLoader extends VersionedLoader implements AnimatorListener {

    @Override
    public void onAnimationStart(Animator animation) {}

    @Override
    public void onAnimationEnd(Animator animation) {}

    @Override
    public void onAnimationCancel(Animator animation) {}

    @Override
    public void onAnimationRepeat(Animator animation) {}

}

AnimatorListener доступен только начиная с версии 3.1.

Теперь, если вы запустите: Object obj = VersionedLoader.newInstance();

Эта ошибка появится в журналах:

10-27 13:51:14.437: I/dalvikvm(7673): Failed resolving Lyour/package/name/NewVersionLoader; interface 7 'Landroid/animation/Animator$AnimatorListener;'
10-27 13:51:14.437: W/dalvikvm(7673): Link of class 'Lyour/package/name/NewVersionLoader;' failed
10-27 13:51:14.445: E/dalvikvm(7673): Could not find class 'your.package.name.NewVersionLoader', referenced from method your.package.name.VersionedLoader.newInstance
10-27 13:51:14.445: W/dalvikvm(7673): VFY: unable to resolve new-instance 1327 (Lyour/package/name/NewVersionLoader;) in Lyour/package/name/VersionedLoader;
10-27 13:51:14.445: D/dalvikvm(7673): VFY: replacing opcode 0x22 at 0x000c
10-27 13:51:14.445: D/dalvikvm(7673): VFY: dead code 0x000e-0011 in Lyour/package/name/VersionedLoader;.newInstance ()Lyour/package/name/VersionedLoader;

Он не сработает, и он будет продолжать работать правильно.

Ответ 1

Да, я могу воспроизвести проблему. Однако удивительно, что, поскольку вы отмечаете, тот факт, что он не сбой, означает, что это больше случай, когда Dalvik, возможно, слишком болван в LogCat, чем что-либо, что может нанести вред приложению.

Один способ - переместить интерфейс во внутренний класс. В вашем примере вместо NewVersionLoader реализации AnimatorListener внутренний класс в NewVersionLoader будет реализовывать AnimationListener:

@TargetApi(11)
public class NewVersionLoader extends VersionedLoader {
    private class Foo implements AnimatorListener {
        @Override
        public void onAnimationStart(Animator animation) {}

        @Override
        public void onAnimationEnd(Animator animation) {}

        @Override
        public void onAnimationCancel(Animator animation) {}

        @Override
        public void onAnimationRepeat(Animator animation) {}

    }
}

По общему признанию, это может быть не идеальным в зависимости от вашего предполагаемого использования VersionedLoader. Однако, поскольку VersionedLoader сам не реализует AnimationListener, пользователи VersionedLoader не будут вызывать методы AnimationListener, поэтому тот факт, что ваша логика находится на внутреннем классе, а не на самом деле, не должна быть огромной проблемой AFAIK.