Android JNI 之 Bitmap 操作

glumes發表於2018-07-25

在 Android 中通過 JNI 去操作 Bitmap。

在 Android 通過 JNI 去呼叫 Bitmap,通過 CMake 去編 so 動態連結庫的話,需要新增 jnigraphics 影像庫。

target_link_libraries( # Specifies the target library.
                       native-operation
                       jnigraphics
                       ${log-lib} )
複製程式碼

在 Android 中關於 JNI Bitmap 的操作,都定義在 bitmap.h 的標頭檔案裡面了,主要就三個函式,明白它們的含義之後就可以去實踐體會了。

檢索 Bitmap 物件資訊

AndroidBitmap_getInfo 函式允許原生程式碼檢索 Bitmap 物件資訊,如它的大小、畫素格式等,函式簽名如下:

/**
 * Given a java bitmap object, fill out the AndroidBitmapInfo struct for it.
 * If the call fails, the info parameter will be ignored.
 */
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
                          AndroidBitmapInfo* info);
複製程式碼

其中,第一個引數就是 JNI 介面指標,第二個引數就是 Bitmap 物件的引用,第三個引數是指向 AndroidBitmapInfo 結構體的指標。

AndroidBitmapInfo 結構體如下:

/** Bitmap info, see AndroidBitmap_getInfo(). */
typedef struct {
    /** The bitmap width in pixels. */
    uint32_t    width;
    /** The bitmap height in pixels. */
    uint32_t    height;
    /** The number of byte per row. */
    uint32_t    stride;
    /** The bitmap pixel format. See {@link AndroidBitmapFormat} */
    int32_t     format;
    /** Unused. */
    uint32_t    flags;      // 0 for now
} AndroidBitmapInfo;
複製程式碼

其中,width 就是 Bitmap 的寬,height 就是高,format 就是影像的格式,而 stride 就是每一行的位元組數。

影像的格式有如下支援:

/** Bitmap pixel format. */
enum AndroidBitmapFormat {
    /** No format. */
    ANDROID_BITMAP_FORMAT_NONE      = 0,
    /** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Alpha: 8 bits. **/
    ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
    /** Red: 5 bits, Green: 6 bits, Blue: 5 bits. **/
    ANDROID_BITMAP_FORMAT_RGB_565   = 4,
    /** Deprecated in API level 13. Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead. **/
    ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
    /** Alpha: 8 bits. */
    ANDROID_BITMAP_FORMAT_A_8       = 8,
};
複製程式碼

如果 AndroidBitmap_getInfo 執行成功的話,會返回 0 ,否則返回一個負數,代表執行的錯誤碼列表如下:

/** AndroidBitmap functions result code. */
enum {
    /** Operation was successful. */
    ANDROID_BITMAP_RESULT_SUCCESS           = 0,
    /** Bad parameter. */
    ANDROID_BITMAP_RESULT_BAD_PARAMETER     = -1,
    /** JNI exception occured. */
    ANDROID_BITMAP_RESULT_JNI_EXCEPTION     = -2,
    /** Allocation failed. */
    ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3,
};
複製程式碼

訪問原生畫素快取

AndroidBitmap_lockPixels 函式鎖定了畫素快取以確保畫素的記憶體不會被移動。

如果 Native 層想要訪問畫素資料並操作它,該方法返回了畫素快取的一個原生指標,

/**
 * Given a java bitmap object, attempt to lock the pixel address.
 * Locking will ensure that the memory for the pixels will not move
 * until the unlockPixels call, and ensure that, if the pixels had been
 * previously purged, they will have been restored.
 *
 * If this call succeeds, it must be balanced by a call to
 * AndroidBitmap_unlockPixels, after which time the address of the pixels should
 * no longer be used.
 *
 * If this succeeds, *addrPtr will be set to the pixel address. If the call
 * fails, addrPtr will be ignored.
 */
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
複製程式碼

其中,第一個引數就是 JNI 介面指標,第二個引數就是 Bitmap 物件的引用,第三個引數是指向畫素快取地址的指標。

AndroidBitmap_lockPixels 執行成功的話返回 0 ,否則返回一個負數,錯誤碼列表就是上面提到的。

釋放原生畫素快取

對 Bitmap 呼叫完 AndroidBitmap_lockPixels 之後都應該對應呼叫一次 AndroidBitmap_unlockPixels 用來釋放原生畫素快取。

當完成對原生畫素快取的讀寫之後,就應該釋放它,一旦釋放後,Bitmap Java 物件又可以在 Java 層使用了,函式簽名如下:

/**
 * Call this to balance a successful call to AndroidBitmap_lockPixels.
 */
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);
複製程式碼

其中,第一個引數就是 JNI 介面指標,第二個引數就是 Bitmap 物件的引用,如果執行成功返回 0,否則返回 1。

對 Bitmap 的操作,最重要的就是 AndroidBitmap_lockPixels 函式拿到所有畫素的快取地址,然後對每個畫素值進行操作,從而更改 Bitmap 。

實踐

通過對 Bitmap 進行旋轉,上下翻轉,左右映象來體驗 JNI 的開發。

效果如下:

Android JNI 之 Bitmap 操作

具體程式碼可以參考我的 Github 專案,歡迎 Star。

github.com/glumes/Andr…

通過 JNI 將 Bitmap 旋轉

首先定義一個這樣的 native 函式:

    // 順時針旋轉 90° 的操作
    public native Bitmap rotateBitmap(Bitmap bitmap);
複製程式碼

傳入一個 Bitmap 物件,然後返回一個 Bitmap 物件。

然後在 C++ 程式碼中,首先檢索 Bitmap 的資訊,看看是否成功。

    AndroidBitmapInfo bitmapInfo;
    int ret;
    if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return NULL;
    }
複製程式碼

接下來就是獲得 Bitmap 的畫素快取指標:

    // 讀取 bitmap 的畫素內容到 native 記憶體
    void *bitmapPixels;
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return NULL;
    }
複製程式碼

這個指標指向的就是 Bitmap 畫素內容,它是一個以一維陣列的形式儲存所有的畫素點的值,但是我們在定義 Bitmap 影像時,都會定義寬和高,這就相對於是一個二維的了,那麼就存在 Bitmap 的畫素內容如何轉成指標指向的一維內容,是按照行排列還是按照列排列呢?

在這裡是按照行進行排列的,而且行的排列是從左往右,列的排列是從上往下,起始點就和螢幕座標原點一樣,位於左上角。

通過 AndroidBitmap_lockPixels 方法,bitmapPixels 指標就指向了 Bitmap 的畫素內容,它的長度就是 Bitmap 的寬和高的乘積。

要將 Bitmap 進行旋轉,可以通過直接更改 bitmapPixels 指標指向的畫素點的值,也可以通過建立一個新的 Bitmap 物件,然後將畫素值填充到 Bitmap 物件中,這裡選擇後者的實現方式。

首先建立一個新的 Bitmap 物件,參考之前文章中提到的方式:Android 通過 JNI 訪問 Java 欄位和方法呼叫

在 Java 程式碼中,通過 createBitmap 方法可以建立一個 Bitmap,如下所示:

 Bitmap.createBitmap(int width, int height, @NonNull Config config)`
複製程式碼

所以在 JNI 中就需要呼叫 Bitmap 的靜態方法來建立一個 Bitmap 物件。

jobject generateBitmap(JNIEnv *env, uint32_t width, uint32_t height) {

    jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
    jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls,
                                                            "createBitmap",
                                                            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
    jstring configName = env->NewStringUTF("ARGB_8888");
    jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
    jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(
            bitmapConfigClass, "valueOf",
            "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");

    jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
                                                       valueOfBitmapConfigFunction, configName);

    jobject newBitmap = env->CallStaticObjectMethod(bitmapCls,
                                                    createBitmapFunction,
                                                    width,
                                                    height, bitmapConfig);
    return newBitmap;
}
複製程式碼

首先通過 FindClass 方法找到 Config 類,得到一個 ARGB_8888 的配置,然後得到 Bitmap 類,呼叫它的靜態方法 createBitmap 建立一個新的 Bitmap 物件,具體可以參考之前的文章。

在這裡要傳入新 Bitmap 的寬高,這個寬高也是通過 AndroidBitmap_getInfo 方法得到原來的寬高之後,根據不同的操作計算後得到的。

	// 旋轉操作,新 Bitmap 的寬等於原來的高,新 Bitmap 的高等於原來的寬
   uint32_t newWidth = bitmapInfo.height;
    uint32_t newHeight = bitmapInfo.width;
複製程式碼

有了新的 Bitmap 物件,又有了原有的 Bitmap 畫素指標,接下來就是建立新的畫素指標,並填充畫素內容,然後把這個畫素內容再填充到 Bitmap 上。

    // 建立一個新的陣列指標,把這個新的陣列指標填充畫素值
	uint32_t *newBitmapPixels = new uint32_t[newWidth * newHeight];
    int whereToGet = 0;
    for (int y = 0; y < newHeight; ++y) {
        for (int x = newWidth - 1; x >= 0; x--) {
            uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
            newBitmapPixels[newWidth * y + x] = pixel;
        }
    }
複製程式碼

在這兩個 for迴圈裡面就是從原來的畫素指標中取出畫素值,然後把它按照特定的排列順序填充到新的畫素指標中對應位置的值,這裡也就是前面強調的畫素指標是按照行進行排列的,起點是 Bitmap 的左上角。

    void *resultBitmapPixels;
    if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return NULL;
    }
    int pixelsCount = newWidth * newHeight;
    memcpy((uint32_t *) resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * pixelsCount);
    AndroidBitmap_unlockPixels(env, newBitmap);
複製程式碼

再次建立一個 resultBitmapPixels 指標,並呼叫 AndroidBitmap_lockPixels 方法獲取新的 Bitmap 的畫素指標快取,然後呼叫 memcpy 方法,將待填充的畫素指標填充到 resultBitmapPixels 上,這樣就完成了畫素的賦值,最後呼叫 AndroidBitmap_unlockPixels 方法釋放畫素指標快取,完成整個賦值過程。

就這樣通過讀取原有 Bitmap 的畫素內容然後進行操作後再賦值給新的 Bitmap 物件就完成了 JNI 操作 Bitmap 。

通過 JNI 將 Bitmap 上下翻轉和左右映象

將 Bitmap 進行上下翻轉以及左右映象和旋轉操作類似了,只是針對畫素指標的操作方式不同。

上下翻轉的操作:

    int whereToGet = 0;
    for (int y = 0; y < newHeight; ++y) {
        for (int x = 0; x < newWidth; x++) {
            uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
            newBitmapPixels[newWidth * (newHeight - 1 - y) + x] = pixel;
        }
    }
複製程式碼

左右映象的操作:

    int whereToGet = 0;
    for (int y = 0; y < newHeight; ++y) {
        for (int x = newWidth - 1; x >= 0; x--) {
            uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
            newBitmapPixels[newWidth * y + x] = pixel;
        }
    }
複製程式碼

其他的操作都相同了,具體還是看專案程式碼吧。

歡迎關注微信公眾號:【紙上淺談】,獲得最新文章推送~~~

掃碼關注

相關文章