查看原文
其他

Target SDK 升级到 29 AnimatorSet 动画不执行了

爱上小懒虫 鸿洋 2023-09-13

本文作者


作者:爱上小懒虫

链接:

https://juejin.cn/post/7258233838370783293

本文由作者授权发布。

1起因
由于项目调整升级 Target Sdk 升级到 30,测试回归没有发现问题正常上线。上线几天后用户反馈,进入直播间看不到红包了,但是点击原有红包的位置还是能弹出红包。这边我们看下伪代码:
fun <T> ValueAnimator.getValueAnimatorAndExecute(call: (T) -> Unit) {
    (animatedValue as? T)?.let {
        call.invoke(it)
    }
}

fun doRedPacketAnimatorSet(view:View){
    AnimatorSet().apply {
        playTogether(
            ValueAnimator.ofFloat(0f, 1.1f, 0.8f, 1f).apply {
                duration = 800L
            },ValueAnimator.ofFloat(0f, 1f).apply {
                duration = 800L
                addUpdateListener { animation: ValueAnimator ->
                    animation.getValueAnimatorAndExecute<Float> {
                       // 监听更新做一个渐现动画
                       view.alpha = it
                    }
                }
            })
        doOnStart {
            view.run {
                // 动画执行开始,透明度降为 1
                isVisible = false
                alpha = 0f
            }
        }
        doOnEnd {

        }
    }
}
按道理只要执行 update 监听 View 肯定会显示出来,为什么会有用户反馈无法显示?看日志发现 update 没有执行,立马就回调了 doOnEnd
为什么 update 不执行?最近也没做改动,只有 Target Sdk 进行升级为啥动画给干没了呢?
至此我人处于懵逼状态,这是什么情况为啥直接就 onEnd 了,奇怪的是为什么只有小部分手机有这个问题,而大部分手机都是好的?要出问题应该全部都得出问题才对啊?
尝试去复现,debug 环境还复现不出来。

分析原因

没得法子了看源码吧,AnimatorSet 部分源码。
public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
    @Override
    public void start() {
        start(falsetrue);
    }

    private void start(boolean inReverse, boolean selfPulse) {
        // 省略部分代码
        boolean isEmptySet = isEmptySet(this);
        if (!isEmptySet) {
            startAnimation();
        }
        // 省略部分代码
    }


    private void startAnimation() {
        addAnimationEndListener();

        // Register animation callback
        addAnimationCallback(0);

        // 省略部分代码
    }

    private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
        AnimationHandler handler = AnimationHandler.getInstance();
        handler.addAnimationFrameCallback(this, delay);
    }
}
这边是简化的调用链,一目了然。最终 AnimatorSet 调用 AnimationHandler 这个类把自己 add 进去了。
public class AnimationHandler {
    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };

    public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
        if (mAnimationCallbacks.size() == 0) {
            getProvider().postFrameCallback(mFrameCallback);
        }
        if (!mAnimationCallbacks.contains(callback)) {
            mAnimationCallbacks.add(callback);
        }
        // 省略部分代码
    }

    private void doAnimationFrame(long frameTime) {
        long currentTime = SystemClock.uptimeMillis();
        final int size = mAnimationCallbacks.size();
        for (int i = 0; i < size; i++) {
            final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
            if (callback == null) {
                continue;
            }
            if (isCallbackDue(callback, currentTime)) {
                // 最终会回调到 AnimatorSet 
                callback.doAnimationFrame(frameTime);
                if (mCommitCallbacks.contains(callback)) {
                    getProvider().postCommitCallback(new Runnable() {
                        @Override
                        public void run() {
                            commitAnimationFrame(callback, getProvider().getFrameTime());
                        }
                    });
                }
            }
        }
        cleanUpList();
    }
}
AnimationHandler 简化后就看起来很清晰了,AnimatorSet 调用 addAnimationFrameCallback 后,AnimationHandler AnimatorSet 放到了 mAnimationCallbacks 进行维护,并且调用了 getProvider().postFrameCallback(mFrameCallback) 监听屏幕刷新,当屏幕刷新时,执行doAnimationFrame 最终就会回调到 AnimatorSetdoAnimationFrame
重点来了。
public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
    @Override
    public boolean doAnimationFrame(long frameTime) {
        float durationScale = ValueAnimator.getDurationScale();
        if (durationScale == 0f) {
            // Duration scale is 0, end the animation right away.
            forceToEnd();
            return true;
        }
        // 省略部分代码
    }
}


public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
    /**
     * 系统范围的动画比例。
     *
     * 要检查是否启用了 areAnimatorsEnabled()动画,请使用 。
     */

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    private static float sDurationScale = 1.0f;

    /**
     * 返回基于动画器的动画的系统范围的比例因子。这会影响所有此类动画的开始延迟和持续时间。设置为 0 
     *
     * 将导致动画立即结束。默认值为 1.0f。
     *
     * 返回:持续时间刻度
     */

    @FloatRange(from = 0)
    public static float getDurationScale() {
        return sDurationScale;
    }

    /**
     * 返回系统范围的动画器当前是否已启用。默认情况下,所有动画器都处于启用状态。如果用户将开发人员选项设置为将动画器持续时间比例设置为 0,或者启用电池保存模式(禁用所有动画),则这可能会更改。
     * 开发人员通常不需要调用此方法,但如果应用希望在禁用动画器时显示不同的体验,则可以将此返回值用作要提供的体验的决策程序。
     * 返回:
     * 布尔值 当前是否启用动画器。默认值为 true。
     */

    public static boolean areAnimatorsEnabled() {
        return !(sDurationScale == 0);
    }
}
到这里我们看到 doAnimationFrame 如果从 ValueAnimator.getDurationScale() 就直接给我掉 onEnd 了,额。。。。
终于是找到原因了,sDurationScale 为 0 就直接给我结束了。
最终和用户确认后确实是有个用户确实是省电模式,但是也有些用户没开过???而且低电量模式有问题,为啥以前老版本没有用户反馈有这个问题???
UnsupportedAppUsage 注意这个,后面会提到。
我又懵逼了。

这个时候我同事发来一篇文章 targetSdkVersion 29,部分海外机型无法显示动效

https://github.com/svga/SVGAPlayer-Android/issues/314#top


刚好项目里面有用到 SVGA 这个库版本是 2.4.7 的,我们看看 SVGA 做了些啥。
open class SVGAImageView : ImageView {
    fun startAnimation() {
        startAnimation(nullfalse)
    }

    fun startAnimation(range: SVGARange?, reverse: Boolean = false) {
        // 省略部分代码
        drawable.videoItem.let {
            // 省略部分代码
            try {
                val animatorClass = Class.forName("android.animation.ValueAnimator")
                animatorClass?.let {
                    it.getDeclaredField("sDurationScale")?.let {
                        it.isAccessible = true
                        it.getFloat(animatorClass).let {
                            durationScale = it.toDouble()
                        }
                        if (durationScale == 0.0) {
                            it.setFloat(animatorClass, 1.0f)
                            durationScale = 1.0
                            Log.e("SVGAPlayer""The animation duration scale has been reset to 1.0x, because you closed it on developer options.")
                        }
                    }
                }
            } catch (e: Exception) {}
            // 省略部分代码
        }
    }
}
看到这里就解释为什么 AnimatorSet 老版本为什么用户开了省电模式也没有出问题,刚好也解释了为什么 Target sdk 升级后才出问题,因为 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 这个注解 sDurationScale 加入到了 受限的灰名单 中,导致反射失效了。
Landroid/animation/ValueAnimator;->sDurationScale:F   # Use ValueAnimator.areAnimatorsEnabled() (introduced in API 26to query whether duration scale = 0Otherwise, it is intended not to expose impl details such as the actual duration scales to devs. 
SVGA 2.5.14 版本修复了这个问题。
open class SVGAImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : ImageView(context, attrs, defStyleAttr) {


    fun startAnimation() {
        startAnimation(nullfalse)
    }

    fun startAnimation(range: SVGARange?, reverse: Boolean = false) {
        stopAnimation(false)
        play(range, reverse)
    }

    private fun play(range: SVGARange?, reverse: Boolean) {
        // 省略部分代码
        animator.duration = ((mEndFrame - mStartFrame + 1) * (1000 / videoItem.FPS) / generateScale()).toLong()
        // 省略部分代码
    }

    @Suppress("UNNECESSARY_SAFE_CALL")
    private fun generateScale()Double {
        var scale = 1.0
        try {
            val animatorClass = Class.forName("android.animation.ValueAnimator") ?: return scale
            val getMethod = animatorClass.getDeclaredMethod("getDurationScale") ?: return scale
            scale = (getMethod.invoke(animatorClass) as Float).toDouble()
            if (scale == 0.0) {
                val setMethod = animatorClass.getDeclaredMethod("setDurationScale",Float::class.java) ?: return scale
                setMethod.isAccessible = true
                setMethod.invoke(animatorClass,1.0f)
                scale = 1.0
                LogUtils.info(TAG,
                        "The animation duration scale has been reset to" +
                                " 1.0x, because you closed it on developer options.")
            }
        } catch (ignore: Exception) {
            ignore.printStackTrace()
        }
        return scale
    }

}
可以看到 svga 都是 startAnimation 的时候才会去反射修改 sDurationScale,但是有些时候是不会使用到 svga 就还是会有问题。

解决问题

由于有些时候我们并不会使用 SVGAImageView 只是想使用 AnimatorSet,那么我们就手动反射修改 sDurationScale
fun setAnimatorsEnabled() {
    // 代码就不贴了,`SVGAImageView` 文章里面都有,自己拷贝下
}
OK,我们在 application 中处理下。
public abstract class BaseApplication extends Application {

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate() {
        setAnimatorsEnabled()
    }
}
心里美滋滋,这次完美了。运行。。。安装。。。测试。。。好,没效果???
我的剧本???怎么回事???那放到 activity 里面执行下吧。
public abstract class BaseActivity extends Application {
    @Override  
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        setAnimatorsEnabled()
    }
}
运行。。。安装。。。测试。。。好了。
到此问题解决了!!!
但是:
  1. 为什么 Application 中设置 sDurationScale 无效?
  2. 为什么 Activity 中设置才有效?
  3. 系统 sDurationScale 又是何时设置这个值的呢?省电模式切换对这个值有些什么影响呢?
  4. 还有没有其他的坑点?

带着这些问题我们看下为什么,找寻下其根本原因。

2分析过程


我们看下 setDurationScale 调用位置。
从 Android 源码搜索网站上我们可以看到 WindowManagerGlobalWindowManagerService.class 有几处调用位置。
WindowManagerGlobal.class
public final class WindowManagerGlobal {
    @UnsupportedAppUsage
    public static void initialize() {
        getWindowManagerService();
    }

    @UnsupportedAppUsage
    public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
                try {
                    if (sWindowManagerService != null) {
                        ValueAnimator.setDurationScale(
                                sWindowManagerService.getCurrentAnimatorScale());
                        sUseBLASTAdapter = sWindowManagerService.useBLAST();
                    }
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowManagerService;
        }
    }


    @UnsupportedAppUsage
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    // Emulate the legacy behavior.  The global instance of InputMethodManager
                    // was instantiated here.
                    // TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
                    InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            });
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }
}
WindowManagerService.class
public class WindowManagerService extends IWindowManager.Stub
        implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs,
                DisplayManagerService.WindowManagerFuncs, DisplayManager.DisplayListener 
{

   private WindowManagerService(Context context, PowerManagerService pm,
            DisplayManagerService displayManager, InputManagerService inputManager,
            Handler uiHandler,
            boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore) 
{
        // 省略部分代码
        if (mPowerManagerInternal != null) {
            mPowerManagerInternal.registerLowPowerModeObserver(
                    new PowerManagerInternal.LowPowerModeListener() {
                @Override
                public int getServiceType() {
                    return ServiceType.ANIMATION;
                }

                @Override
                public void onLowPowerModeChanged(PowerSaveState result) {
                    synchronized (mGlobalLock) {
                        final boolean enabled = result.batterySaverEnabled;
                        if (mAnimationsDisabled != enabled && !mAllowAnimationsInLowPowerMode) {
                            mAnimationsDisabled = enabled;
                            dispatchNewAnimatorScaleLocked(null);
                        }
                    }
                }
            });
            mAnimationsDisabled = mPowerManagerInternal
                    .getLowPowerState(ServiceType.ANIMATION).batterySaverEnabled;
        }
        // 省略部分代码
        setAnimatorDurationScale(getAnimatorDurationScaleSetting());
        // 省略部分代码
    }

    void windowAddedLocked() {
        // 省略部分代码
        if (mSurfaceSession == null) {
            // 省略部分代码
            if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
                mService.dispatchNewAnimatorScaleLocked(this);
            }
        }
        mNumWindow++;
    }

    void dispatchNewAnimatorScaleLocked(Session session) {
        mH.obtainMessage(H.NEW_ANIMATOR_SCALE, session).sendToTarget();
    }

    @Override
    public void handleMessage(Message msg) {
        if (DEBUG_WINDOW_TRACE) {
            Slog.v(TAG_WM, "handleMessage: entry what=" + msg.what);
        }
        case NEW_ANIMATOR_SCALE: {
            float scale = getCurrentAnimatorScale();
            ValueAnimator.setDurationScale(scale);
            Session session = (Session)msg.obj;
            if (session != null) {
                try {
                    session.mCallback.onAnimatorScaleChanged(scale);
                } catch (RemoteException e) {
                }
            } else {
                ArrayList<IWindowSessionCallback> callbacks
                        = new ArrayList<IWindowSessionCallback>();
                synchronized (mGlobalLock) {
                    for (int i=0; i<mSessions.size(); i++) {
                        callbacks.add(mSessions.valueAt(i).mCallback);
                    }
                }
                for (int i=0; i<callbacks.size(); i++) {
                    try {
                        callbacks.get(i).onAnimatorScaleChanged(scale);
                    } catch (RemoteException e) {
                    }
                }
            }
            break;
       }
   }
}
这边能能影响到 sDurationScale 我把代码都贴了下,总结下具体有以下几个地方:
  1. WindowManagerGlobal 类调用 getWindowManagerService() 获取 WindowManagerService 对象时。
  2. openSession 时设置的回调会影响到:
  • windowAddedLocked 由于篇幅有限有兴趣的可以去看下这个 Android 源码 图形系统之 WindowState attach

    https://blog.csdn.net/tyyj90/article/details/107850439


  • onLowPowerModeChanged 这个就很明显了低电量的时候会回调。
这边我们稍微看下 WindowManagerGlobal 可以看到初始化位置在 handleLaunchActivity 中。
public final class ActivityThread extends ClientTransactionHandler
        implements ActivityThreadInternal 
{

    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) 
{
        // 省略部分代码
        WindowManagerGlobal.initialize();
        // 省略部分代码
        return a;
    }

}

这边给大家准备个时序图(网上扒的 Activity 启动时序图)。

https://blog.csdn.net/bobo_zai/article/details/84844178


 看到这里基础好的应该就能明白为什么 Application 中设置 sDurationScale 无效,而 Activity 设置 sDurationScale 有效了。不太明白的可以去搜索 handleLaunchActivity 调用时机,由于篇幅有限不做过多介绍。
这边就解释了完了前面两个疑问的,剩下就看看有没有其他的坑点。虽然我们在 Activity 中设置 sDurationScale 修复了动画不播放的问题,但是不知道有没有人记得 openSession 设置的回调。
假设当前有这么个场景:用户开发者模式中设置了关闭动画,而我们在 Activity 强制打开了动画这个时候是正常的。但是如果用户设置省电模式或者进入了低电量的状态,系统就会回调 WindowManagerService onLowPowerModeChanged 导致 sDurationScale 重新被设置,这个时候用户没有关闭我们的 App 而是边充电边玩,就还是会出现 sDurationScale == 0F 导致动画不执行。
上述这种情况就是一个坑点了!!! 那么怎么去解决这个问题,这个仁者见仁智者见智了,目前个人想到的方案有以下几种:
  1. onLowPowerModeChanged 尝试监听这个,在这个中重新设置。
  2. start() 时候每次都去设置。
  3. 和产品友好交流一波,毕竟这个玩意儿是 google 加入灰名单的玩意儿。保不齐xxxx,这种东西咱不懂咱也不好说。
剩下 windowAddedLocked 是时候的影响,这边大家自己看下吧,不细说了。
OK, 整篇文章到此结束。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

Now in Android #87 带你了解近期 Android 的一些新变化
Gradle Task 系列完结,Task执行大剖析
Android 13 媒体权限适配指南


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存