起因
由于项目调整升级 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(false, true);
}
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 最终就会回调到 AnimatorSet 的 doAnimationFrame。
重点来了。
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(null, false)
}
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 26) to query whether duration scale = 0. Otherwise, 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(null, false)
}
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()
}
}
运行。。。安装。。。测试。。。好了。
到此问题解决了!!!
但是:
-
为什么 Application 中设置 sDurationScale 无效?
-
为什么 Activity 中设置才有效?
-
系统 sDurationScale 又是何时设置这个值的呢?省电模式切换对这个值有些什么影响呢?
-
还有没有其他的坑点?
带着这些问题我们看下为什么,找寻下其根本原因。
分析过程
我们看下 setDurationScale 调用位置。
从 Android 源码搜索网站上我们可以看到 WindowManagerGlobal 和 WindowManagerService.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 我把代码都贴了下,总结下具体有以下几个地方:
-
WindowManagerGlobal 类调用 getWindowManagerService() 获取 WindowManagerService 对象时。
-
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 导致动画不执行。
上述这种情况就是一个坑点了!!! 那么怎么去解决这个问题,这个仁者见仁智者见智了,目前个人想到的方案有以下几种:
-
onLowPowerModeChanged 尝试监听这个,在这个中重新设置。
-
start() 时候每次都去设置。
-
和产品友好交流一波,毕竟这个玩意儿是 google 加入灰名单的玩意儿。保不齐xxxx,这种东西咱不懂咱也不好说。
作者:爱上小懒虫
链接:https://juejin.cn/post/7258233838370783293
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。