Android JNI 之 Bitmap 操做

在 Android 中經過 JNI 去操做 Bitmap。java

在 Android 經過 JNI 去調用 Bitmap,經過 CMake 去編 so 動態連接庫的話,須要添加 jnigraphics 圖像庫。android

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

在 Android 中關於 JNI Bitmap 的操做,都定義在 bitmap.h 的頭文件裏面了,主要就三個函數,明白它們的含義以後就能夠去實踐體會了。git

檢索 Bitmap 對象信息

AndroidBitmap_getInfo 函數容許原生代碼檢索 Bitmap 對象信息,如它的大小、像素格式等,函數簽名以下:github

/** * 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 就是每一行的字節數。微信

圖像的格式有以下支持:ide

/** 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 函數鎖定了像素緩存以確保像素的內存不會被移動。post

若是 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 的開發。

效果以下:

具體代碼能夠參考個人 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;
        }
    }
複製代碼

其餘的操做都相同了,具體仍是看項目代碼吧。

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

掃碼關注
相關文章
相關標籤/搜索