【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG

一、问题描述

最近APP大改版,在启动页加入了一个很炫的动画功能介绍。但是当运行在Android Q机子上的时候,出现了奔溃,奔溃信息如下所示:

2019-09-10 15:33:05.825 21734-21734/com.xtc.watch E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.xtc.watch, PID: 21734
    java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed
        at android.graphics.Canvas.checkValidSaveFlags(Canvas.java:442)
        at android.graphics.Canvas.saveLayer(Canvas.java:519)
        at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:222)
        at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
        at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
        at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
        at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
        at com.airbnb.lottie.LottieDrawable.draw(LottieDrawable.java:311)
        at android.widget.ImageView.onDraw(ImageView.java:1434)
        at android.view.View.draw(View.java:21436)
        at android.view.View.updateDisplayListIfDirty(View.java:20313)
        at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
        at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
        at android.view.View.updateDisplayListIfDirty(View.java:20273)
        at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
        at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
        at android.view.View.updateDisplayListIfDirty(View.java:20273)
        at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
        at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
        at android.view.View.updateDisplayListIfDirty(View.java:20273)
        at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
        at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
        at android.view.View.updateDisplayListIfDirty(View.java:20273)
        at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
        at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
        at android.view.View.updateDisplayListIfDirty(View.java:20273)
        at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
        at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
        at android.view.View.updateDisplayListIfDirty(View.java:20273)
        at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
        at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
        at android.view.View.updateDisplayListIfDirty(View.java:20273)
        at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:575)
        at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:581)
        at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:654)
        at android.view.ViewRootImpl.draw(ViewRootImpl.java:3610)
        at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3418)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2755)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1721)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7598)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:966)
        at android.view.Choreographer.doCallbacks(Choreographer.java:790)
        at android.view.Choreographer.doFrame(Choreographer.java:725)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:951)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

在这里插入图片描述

问题所在是使用Lottie播放动画的时候出现的问题。

二、排查问题

Lottie动画的github地址是: https://github.com/airbnb/lottie-android

2.1 排查出应该是Lottie动画库的错误

错误日志部分信息如下,可以判断出应该是Lottie库出现了问题。

java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed
        at android.graphics.Canvas.checkValidSaveFlags(Canvas.java:442)
        at android.graphics.Canvas.saveLayer(Canvas.java:519)
        at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:222)
        at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
        at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
        at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
        at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
        at com.airbnb.lottie.LottieDrawable.draw(LottieDrawable.java:311)
        at android.widget.ImageView.onDraw(ImageView.java:1434)

至于具体错误原因请慢慢往下看。

2.2 我们深入分析下为什么会出现这个错误?

我们进入 android.graphics.Canvas#checkValidSaveFlags方法查看下

    /**
     * Restore everything when restore() is called (standard save flags).
     * <p class="note"><strong>Note:</strong> for performance reasons, it is
     * strongly recommended to pass this - the complete set of flags - to any
     * call to <code>saveLayer()</code> and <code>saveLayerAlpha()</code>
     * variants.
     *
     * <p class="note"><strong>Note:</strong> all methods that accept this flag
     * have flagless versions that are equivalent to passing this flag.
     */
    public static final int ALL_SAVE_FLAG = 0x1F;

    private static void checkValidSaveFlags(int saveFlags) {
        if (sCompatiblityVersion >= Build.VERSION_CODES.P
                && saveFlags != ALL_SAVE_FLAG) {
            throw new IllegalArgumentException(
                    "Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed");
        }
    }

在这里插入图片描述
这段代码检测的意思就是:当版本大于Build.VERSION_CODES.P(即SDK 28版本)的时候,并且saveFlags的值不为ALL_SAVE_FLAG的话,就抛出这个异常。

android.graphics.Canvas#checkValidSaveFlags 这个方法被saveLayer()方法和saveLayerAlpha()方法调用检测的。

  • android.graphics.Canvas#saveLayer(android.graphics.RectF, android.graphics.Paint, int)方法
    在这里插入图片描述
  /**
     * This behaves the same as save(), but in addition it allocates and
     * redirects drawing to an offscreen bitmap.
     * <p class="note"><strong>Note:</strong> this method is very expensive,
     * incurring more than double rendering cost for contained content. Avoid
     * using this method, especially if the bounds provided are large. It is
     * recommended to use a {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
     * to apply an xfermode, color filter, or alpha, as it will perform much
     * better than this method.
     * <p>
     * All drawing calls are directed to a newly allocated offscreen bitmap.
     * Only when the balancing call to restore() is made, is that offscreen
     * buffer drawn back to the current target of the Canvas (either the
     * screen, it's target Bitmap, or the previous layer).
     * <p>
     * Attributes of the Paint - {@link Paint#getAlpha() alpha},
     * {@link Paint#getXfermode() Xfermode}, and
     * {@link Paint#getColorFilter() ColorFilter} are applied when the
     * offscreen bitmap is drawn back when restore() is called.
     *
     * As of API Level API level {@value Build.VERSION_CODES#P} the only valid
     * {@code saveFlags} is {@link #ALL_SAVE_FLAG}.  All other flags are ignored.
     *
     * @deprecated Use {@link #saveLayer(RectF, Paint)} instead.
     * @param bounds May be null. The maximum size the offscreen bitmap
     *               needs to be (in local coordinates)
     * @param paint  This is copied, and is applied to the offscreen when
     *               restore() is called.
     * @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
     *               for performance reasons.
     * @return       value to pass to restoreToCount() to balance this save()
     */
    public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
        if (bounds == null) {
            bounds = new RectF(getClipBounds());
        }
        checkValidSaveFlags(saveFlags);
        return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint,
                ALL_SAVE_FLAG);
    }
  • android.graphics.Canvas#saveLayer(float, float, float, float, android.graphics.Paint, int) 方法
    在这里插入图片描述
  /**
     * Helper version of saveLayer() that takes 4 values rather than a RectF.
     *
     * As of API Level API level {@value Build.VERSION_CODES#P} the only valid
     * {@code saveFlags} is {@link #ALL_SAVE_FLAG}.  All other flags are ignored.
     *
     * @deprecated Use {@link #saveLayer(float, float, float, float, Paint)} instead.
     */
    public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
            @Saveflags int saveFlags) {
        checkValidSaveFlags(saveFlags);
        return nSaveLayer(mNativeCanvasWrapper, left, top, right, bottom,
                paint != null ? paint.getNativeInstance() : 0,
                ALL_SAVE_FLAG);
    }
  • android.graphics.Canvas#saveLayerAlpha(android.graphics.RectF, int, int) 方法
    在这里插入图片描述
/**
     * This behaves the same as save(), but in addition it allocates and
     * redirects drawing to an offscreen bitmap.
     * <p class="note"><strong>Note:</strong> this method is very expensive,
     * incurring more than double rendering cost for contained content. Avoid
     * using this method, especially if the bounds provided are large. It is
     * recommended to use a {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
     * to apply an xfermode, color filter, or alpha, as it will perform much
     * better than this method.
     * <p>
     * All drawing calls are directed to a newly allocated offscreen bitmap.
     * Only when the balancing call to restore() is made, is that offscreen
     * buffer drawn back to the current target of the Canvas (either the
     * screen, it's target Bitmap, or the previous layer).
     * <p>
     * The {@code alpha} parameter is applied when the offscreen bitmap is
     * drawn back when restore() is called.
     *
     * As of API Level API level {@value Build.VERSION_CODES#P} the only valid
     * {@code saveFlags} is {@link #ALL_SAVE_FLAG}.  All other flags are ignored.
     *
     * @deprecated Use {@link #saveLayerAlpha(RectF, int)} instead.
     * @param bounds    The maximum size the offscreen bitmap needs to be
     *                  (in local coordinates)
     * @param alpha     The alpha to apply to the offscreen when it is
                        drawn during restore()
     * @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
     *                  for performance reasons.
     * @return          value to pass to restoreToCount() to balance this call
     */
    public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags) {
        if (bounds == null) {
            bounds = new RectF(getClipBounds());
        }
        checkValidSaveFlags(saveFlags);
        return saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, alpha,
                ALL_SAVE_FLAG);
    }
  • android.graphics.Canvas#saveLayerAlpha(float, float, float, float, int, int) 方法
    在这里插入图片描述
 /**
     * Helper for saveLayerAlpha() that takes 4 values instead of a RectF.
     *
     * As of API Level API level {@value Build.VERSION_CODES#P} the only valid
     * {@code saveFlags} is {@link #ALL_SAVE_FLAG}.  All other flags are ignored.
     *
     * @deprecated Use {@link #saveLayerAlpha(float, float, float, float, int)} instead.
     */
    public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
            @Saveflags int saveFlags) {
        checkValidSaveFlags(saveFlags);
        alpha = Math.min(255, Math.max(0, alpha));
        return nSaveLayerAlpha(mNativeCanvasWrapper, left, top, right, bottom,
                                     alpha, ALL_SAVE_FLAG);
    }

因此,不管是调用saveLayer()方法还是saveLayerAlpha()方法,如果targetSDK 已经达到28的话,saveFlags的值一定要设置为android.graphics.Canvas#ALL_SAVE_FLAG

2.2 继续排查Lottie动画库错在哪里?

在Lottie动画库里面,com.airbnb.lottie.model.layer.BaseLayer#draw() 方法中 有一段代码如下所示
在这里插入图片描述

 if (hasMatteOnThisLayer()) {
      L.beginSection("Layer#drawMatte");
      L.beginSection("Layer#saveLayer");
      canvas.saveLayer(rect, mattePaint, SAVE_FLAGS);
      L.endSection("Layer#saveLayer");
      clearCanvas(canvas);
      //noinspection ConstantConditions
      matteLayer.draw(canvas, parentMatrix, alpha);
      L.beginSection("Layer#restoreLayer");
      canvas.restore();
      L.endSection("Layer#restoreLayer");
      L.endSection("Layer#drawMatte");
    }

因此可以看出来,就是这一行代码出现了问题。

canvas.saveLayer(rect, contentPaint, SAVE_FLAGS);

所以我们得去查一查,Lottie动画库是否有版本更新,是否修复了此问题。

三、修复问题

项目目前使用的Lottie库版本是2.5.4
在这里插入图片描述

3.1 去官网查找合适版本升级

去官网github查看 v2.5.4版本是2018年4月10日发布的。距离现在已经很久很久了。

在这里插入图片描述

最新版本是3.0.1 ,发布与2019年4月26日。

在这里插入图片描述

但是不能直接用最新版本,因为在ReadMe文档中有介绍,2.8.0以上版本只支持迁移到了androidX之后的工程。而我们项目虽然进行迁移了androidX操作,但是还在模块提测阶段,所以暂时不能使用最新版本。 等后面通过模块进入集成之后再升级。

在这里插入图片描述
v2.7.0 于 2018年9月24日发布,也是目前项目阶段能用的最高的版本了。就选这个版本试一试修复此bug没。
在这里插入图片描述
查看Lottie官网上的issue 也有人遇到同样的问题,并且issue被关闭了,应该是修复了。
https://github.com/airbnb/lottie-android/issues/747
在这里插入图片描述

https://github.com/airbnb/lottie-android/issues/768
在这里插入图片描述

官网上的pull合并代码请求查看下

https://github.com/airbnb/lottie-android/pull/748

在这里插入图片描述
https://github.com/airbnb/lottie-android/pull/824

在这里插入图片描述

3.2 升级版本为v2.7.0

所以应该是早就解决了此问题,正好我们项目刚好将targetSDK升级到了28,所以遇到此问题。下面我们升级到v2.7.0版本试一试。

在这里插入图片描述
升级之后,一切正常。

升级之后,saveFlags已经变成了Canvas.ALL_SAVE_FLAG。所以没问题了!

在这里插入图片描述

 if (hasMatteOnThisLayer()) {
      L.beginSection("Layer#drawMatte");
      L.beginSection("Layer#saveLayer");
      saveLayerCompat(canvas, rect, mattePaint, false);
      L.endSection("Layer#saveLayer");
      clearCanvas(canvas);
      //noinspection ConstantConditions
      matteLayer.draw(canvas, parentMatrix, alpha);
      L.beginSection("Layer#restoreLayer");
      canvas.restore();
      L.endSection("Layer#restoreLayer");
      L.endSection("Layer#drawMatte");
    }

这段代码调用了saveLayerCompat(canvas, rect, mattePaint, false); 方法,代码如下所示:

在这里插入图片描述

@SuppressLint("WrongConstant")
  private void saveLayerCompat(Canvas canvas, RectF rect, Paint paint, boolean all) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
      // This method was deprecated in API level 26 and not recommented since 22, but its
      // 2-parameter replacement is only available starting at API level 21.
      canvas.saveLayer(rect, paint, all ? Canvas.ALL_SAVE_FLAG : SAVE_FLAGS);
    } else {
      canvas.saveLayer(rect, paint);
    }
  }
 

当SDK大于M(即23)的时候,走的是canvas.saveLayer(rect, paint);

而 android.graphics.Canvas#saveLayer(android.graphics.RectF, android.graphics.Paint) 源码如下

在这里插入图片描述

/**
     * This behaves the same as save(), but in addition it allocates and
     * redirects drawing to an offscreen rendering target.
     * <p class="note"><strong>Note:</strong> this method is very expensive,
     * incurring more than double rendering cost for contained content. Avoid
     * using this method when possible and instead use a
     * {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
     * to apply an xfermode, color filter, or alpha, as it will perform much
     * better than this method.
     * <p>
     * All drawing calls are directed to a newly allocated offscreen rendering target.
     * Only when the balancing call to restore() is made, is that offscreen
     * buffer drawn back to the current target of the Canvas (which can potentially be a previous
     * layer if these calls are nested).
     * <p>
     * Attributes of the Paint - {@link Paint#getAlpha() alpha},
     * {@link Paint#getXfermode() Xfermode}, and
     * {@link Paint#getColorFilter() ColorFilter} are applied when the
     * offscreen rendering target is drawn back when restore() is called.
     *
     * @param bounds May be null. The maximum size the offscreen render target
     *               needs to be (in local coordinates)
     * @param paint  This is copied, and is applied to the offscreen when
     *               restore() is called.
     * @return       value to pass to restoreToCount() to balance this save()
     */
    public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) {
        return saveLayer(bounds, paint, ALL_SAVE_FLAG);
    }

传递的就是 android.graphics.Canvas#ALL_SAVE_FLAG ,因此该bug不复存在!

  • v2.5.4 代码
    https://github.com/airbnb/lottie-android/blob/v2.5.4/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java

在这里插入图片描述

    if (hasMatteOnThisLayer()) {
      L.beginSection("Layer#drawMatte");
      L.beginSection("Layer#saveLayer");
      canvas.saveLayer(rect, mattePaint, SAVE_FLAGS);
      L.endSection("Layer#saveLayer");
      clearCanvas(canvas);
      //noinspection ConstantConditions
      matteLayer.draw(canvas, parentMatrix, alpha);
      L.beginSection("Layer#restoreLayer");
      canvas.restore();
      L.endSection("Layer#restoreLayer");
      L.endSection("Layer#drawMatte");
    }

传的是 SAVE_FLAGS

  • 两个版本对比
    在这里插入图片描述

四、总结

不管是调用saveLayer()方法还是saveLayerAlpha()方法,如果targetSDK 已经达到28的话,saveFlags的值一定要设置为android.graphics.Canvas#ALL_SAVE_FLAG


作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:https://ouyangpeng.blog.csdn.net/article/details/100700954
☞ 本人QQ: 3024665621
☞ QQ交流群: 123133153
☞ github.com/ouyangpeng
☞ oypcz@foxmail.com


发布了469 篇原创文章 · 获赞 1452 · 访问量 355万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览