關於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值沒在作校驗致使的問題,也但願本身借鑑,對閾值邊界作判斷,而後儘可能對於異常狀況直接拋異常處理,避免線上引入。

相關文章
相關標籤/搜索