【我的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)

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第1张图片

问题所在是使用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).
     * 

Note: for performance reasons, it is * strongly recommended to pass this - the complete set of flags - to any * call to saveLayer() and saveLayerAlpha() * variants. * *

Note: 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"); } }

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第2张图片
这段代码检测的意思就是:当版本大于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)方法
    【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第3张图片
  /**
     * This behaves the same as save(), but in addition it allocates and
     * redirects drawing to an offscreen bitmap.
     * 

Note: 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. *

* 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). *

* 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) 方法
    【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第4张图片
  /**
     * 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) 方法
    【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第5张图片
/**
     * This behaves the same as save(), but in addition it allocates and
     * redirects drawing to an offscreen bitmap.
     * 

Note: 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. *

* 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). *

* 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) 方法
    【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第6张图片
 /**
     * 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() 方法中 有一段代码如下所示
【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第7张图片

 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
【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第8张图片

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

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

  • 查看版本网址: https://github.com/airbnb/lottie-android/releases

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第9张图片

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

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第10张图片

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

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第11张图片
v2.7.0 于 2018年9月24日发布,也是目前项目阶段能用的最高的版本了。就选这个版本试一试修复此bug没。
【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第12张图片
查看Lottie官网上的issue 也有人遇到同样的问题,并且issue被关闭了,应该是修复了。
https://github.com/airbnb/lottie-android/issues/747
【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第13张图片

https://github.com/airbnb/lottie-android/issues/768
【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第14张图片

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

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

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第15张图片
https://github.com/airbnb/lottie-android/pull/824

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第16张图片

3.2 升级版本为v2.7.0

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

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

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

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

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第17张图片

 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); 方法,代码如下所示:

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第18张图片

@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) 源码如下

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第19张图片

/**
     * This behaves the same as save(), but in addition it allocates and
     * redirects drawing to an offscreen rendering target.
     * 

Note: 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. *

* 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). *

* 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

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第20张图片

    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

  • 两个版本对比
    【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第21张图片

四、总结

不管是调用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
[email protected]


【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第22张图片

【我的Android进阶之旅】解决错误:java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAG_第23张图片

你可能感兴趣的:(Android应用开发)