关于AndroidQ导致的Bitmap标签带alpha通道的图片变黑问题

最近有用户反馈图片变成黑色,显示有点异常,所以做了追查,同时也是提醒自己后续写代码时候,要注意边界的检查。

问题背景

图片在Android 10系统变黑问题,一开始以为是夜间模式等问题,因为在Android10开始有很多手机开始加多夜间模式,但排查不是这个导致的,经过排查是系统的bug。

<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:alpha="130"
    android:src="@mipmap/alpha_img"> 
</bitmap>
复制代码

这小段代码,在Android 10.0 ( api 29 )以下系统是能正常显示图片,但在Android 10.0 时候图片会变成黑色。效果如下,右边是带alpha通道的正常图片,左边是在Android10系统下的绘制效果

结论

新的Android10系统在外部传入的alpha值时候,没有判断输入的数值是否符合规范,导致的溢出问题,同时也是平时方式不对导致的。

源码解析的alpha值流程

根据标签被解析的源码可以知道,对应的的alpha是赋值到了state.mBaseAlpha这个属性

看这段api29时候的bitmapDrawable的代码段,我们知道这个标签里面的alpha应该是设置浮点,取值是0-1.0f的范围才是真确的,但之前很多人是当0-255的范围来设置也没问题,具体后面来讲。

然后在绘制的时候给到了paint的setAlpha里面去的,其中restoreAlpha是255

看这段代码,我们更确定,我们写在xml里面的应该是按照0-1.0f的范围的,因为这个paint.setAlpha 方法就是要求的0-255的范围啊;那么为什么在Android10以下系统正常,就算在xml里面设置alpha值为【0-255】取值都正常,新版本的就出问题呢?

复现问题的方案

综上,这个bug可以简单的在自定义View来做复现。

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    Bitmap bitmap = getBitmapFromId(R.mipmap.details_slogan);
    Paint p = new Paint();
    p.setAlpha((int) (255 * 130 + 0.5f));
    canvas.drawBitmap(bitmap, 0, 0, p);
}
复制代码

setAlpha流程 差异对比

在明白了发生问题在setAlpha的地方上,我们需要探究,为什么一样的alpha值,在api29上有问题,在29以下的系统没问题。我们看下对应的系统源码

@ColorLong
public static long pack(float red, float green, float blue, float alpha,
        @NonNull ColorSpace colorSpace) {
    if (colorSpace.isSrgb()) {
        int argb =
                ((int) (alpha * 255.0f + 0.5f) << 24) |
                ((int) (red   * 255.0f + 0.5f) << 16) |
                ((int) (green * 255.0f + 0.5f) <<  8) |
                 (int) (blue  * 255.0f + 0.5f);
        return (argb & 0xffffffffL) << 32;
    } 
复制代码

看到这里对比起来两边都是正常的,虽然传入的值不符合范围要求,但因为使用pack函数里面有 <<24和 安位与 的操作,避免了传入数值错误导致的问题。

因此,有可能这个alpha的配置在native层面的代码发生了变更,我们继续看下小于29api的native层的代码,这里以5.0系统代码作为参考

Android 5 的设置alpha流程

  1. skpaint的代码

    void SkPaint::setAlpha(U8CPU a) { 
             this->setColor(SkColorSetARGB(a, SkColorGetR(fColor), 
                                           SkColorGetG(fColor), SkColorGetB(fColor))); 
         }
    
     void SkPaint::setColor(SkColor color) { 
         fColor = color; 
     }
    复制代码
  2. SkColorSetARGB

    这个我们到SkColor.h里面看这个函数
    /** Return a SkColor value from 8 bit component values
    */
    static constexpr inline SkColor SkColorSetARGB(U8CPU a, U8CPU r, U8CPU g, U8CPU b) { 
        return SkASSERT(a <= 255 && r <= 255 && g <= 255 && b <= 255), 
               (a << 24) | (r << 16) | (g << 8) | (b << 0); 
    }
    
    #define SkASSERTF(cond, fmt, ...) static_cast<void>(0)
    复制代码

可以看到,对于外部传入的alpha,会经过 a<<24运算后再赋值,所以不会有显示错误问题。 接下来我们看下10.0 的代码。

Android 10.0 的设置alpha流程

nSetAlpha的代码位置

static const JNINativeMethod methods[] = { 
    ...
    {"nSetColor","(JI)V", (void*) PaintGlue::setColor}, 
    {"nSetColor","(JJJ)V", (void*) PaintGlue::setColorLong}, 
    {"nSetAlpha","(JI)V", (void*) PaintGlue::setAlpha},
    ...
}

struct SkRGBA4f { 
    float fR;  //!< red component
    float fG;  //!< green component
    float fB;  //!< blue component
    ...
}

 static void setAlpha(jlong paintHandle, jint a) { 
        reinterpret_cast<Paint*>(paintHandle)->setAlpha(a); 
 }
 
// Helper that accepts an int between 0 and 255, and divides it by 255.0
 void setAlpha(U8CPU a) { 
     this->setAlphaf(a * (1.0f / 255)); 
 }

void SkPaint::setAlphaf(float a) { 
    SkASSERT(a >= 0 && a <= 1.0f); 
    fColor4f.fA = a; 
}
复制代码

这个结构体定义的fA就是一个浮点型的数据。可以看到没有做数据的判断,对大于1.0f和小于0的数值,没有做转换为默认值1.0f的范围判断,而Android29以下的版本使用了 <<24的运算符,从而规避了这个问题。 最终在绘制的时候,当获取这个alpha时候是个错误的值,导致绘制异常。

验证是否溢出导致

既然我们找到问题根源,那么对于Android 10.0的手机,我们是否以对这个paint.setAlpha()的调用,改为paint.setColor的调用,从而证明就是这个脏数据导致的呢,因为setColor()有做校验。从而不再去看canvas.drawBitmap()的详细内容,判断出就是这个地方的问题呢?所以我们加多这段

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    Bitmap bitmap = getBitmapFromId( R.mipmap.details_slogan );
    Paint p = new Paint();

    // p.setAlpha((int) (255 * 130 + 0.5f));
    // 等价于下面这段代码
    ColorSpace cs = Color.colorSpace(p.getColorLong());
    float r = Color.red(p.getColorLong());
    float g = Color.green(p.getColorLong());
    float b = Color.blue(p.getColorLong());
    p.setColor(Color.pack(r, g, b, (255 * 130) * (1.0f / 255), cs));

    canvas.drawBitmap(bitmap, 0, 0, p);
}
复制代码

这段代码和之前直接调用setAlpha是等价的,只是换成了setColor的方式来修改alpha。运行后发现显示效果正常,所以可以判断就是这个Android10系统在这个地方有bug,给google提个pr去:blush:

最后

由于定位到位置在哪里,就不贴设置了颜色后在native层的绘制流程内容,有兴趣的可以去看下。 因为这个 fA值没在做校验导致的问题,也希望自己借鉴,对阈值边界做判断,然后尽量对于异常情况直接抛异常处理,避免线上引入。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章