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

最近有用户反馈图片变成黑色,显示有点异常,因此作了追查,同时也是提醒本身后续写代码时候,要注意边界的检查。android

问题背景

图片在Android 10系统变黑问题,一开始觉得是夜间模式等问题,由于在Android10开始有不少手机开始加多夜间模式,但排查不是这个致使的,通过排查是系统的bug。canvas

<?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系统下的绘制效果api

结论

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

源码解析的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】取值都正常,新版本的就出问题呢? this

复现问题的方案

综上,这个bug能够简单的在自定义View来作复现。google

@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如下的系统没问题。咱们看下对应的系统源码spa

@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和 安位与 的操做,避免了传入数值错误致使的问题。3d

所以,有可能这个alpha的配置在native层面的代码发生了变动,咱们继续看下小于29api的native层的代码,这里以5.0系统代码做为参考rest

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去😊

最后

因为定位到位置在哪里,就不贴设置了颜色后在native层的绘制流程内容,有兴趣的能够去看下。 由于这个 fA值没在作校验致使的问题,也但愿本身借鉴,对阈值边界作判断,而后尽可能对于异常状况直接抛异常处理,避免线上引入。

相关文章
相关标签/搜索