【Android 內存優化】Android 工程中使用 libjpeg-turbo 壓縮圖片 ( JNI 傳遞 Bitmap | 獲取位圖信息 | 獲取圖像數據 | 圖像數據過濾 | 釋放資源 )



在上一篇博客 【Android 內存優化】libjpeg-turbo 函數庫交叉編譯與使用 ( 交叉編譯腳本編寫 | 函數庫頭文件拷貝 | 構建腳本配置 | Android Studio 測試函數庫 )對 libjpeg-turbo 函數庫進行了交叉編譯 , 拷貝了相應的頭文件和靜態庫到 Android Studio 項目中 , 並配置了 CMakeList.txt 構建腳本 , 和 build.gradle 構建腳本 , 本篇博客中開始進行代碼編寫 ;





一、Bitmap 圖像數據處理



Bitmap 圖像數據處理 :


① 獲取 Bitmap 圖像對象 : Java 傳遞到 JNI 層的是 jobject 對象 , 需要將其轉爲 JNI 中的 bitmap 對象 ;

② 數據提取 : 從 bitmap 圖像中提取 RGB 像素值 , 也就是剔除 ALPHA 通道 ( 透明度 ) 的數據 ;

③ 使用 libjpeg-turbo 壓縮圖片 : 調用 libjpeg-turbo 函數庫 , 對上述提取的圖片 RGB 像素數據進行壓縮 ;

④ 釋放資源 : 圖片壓縮完畢後 , 釋放相關資源 ;





二、Java 層 Bitmap 對象轉爲 JNI 層 bitmap 對象



1. Bitmap 信息 : 在 AndroidBitmapInfo 結構體中 , 封裝了圖像寬度 , 圖像高度 , 像素格式等信息 ;

/** Bitmap info, see AndroidBitmap_getInfo(). */
typedef struct {
    /** 圖像像素寬度. */
    uint32_t    width;
    /** 圖像像素高度. */
    uint32_t    height;
    /** 每行字節數. */
    uint32_t    stride;
    /** 像素格式. See {@link AndroidBitmapFormat} */
    int32_t     format;
    /** 保留位. */
    uint32_t    flags;      // 0 for now
} AndroidBitmapInfo;

2. 獲取 Bitmap 信息 : 調用 bitmap.h 中的 AndroidBitmap_getInfo 方法 , 可以從 jbitmap 中獲取對應的信息 ;

int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
                          AndroidBitmapInfo* info);

3. 代碼示例 :

    // 聲明 位圖信息, 該變量作爲返回值使用
    // 引用自 bitmap.h
    AndroidBitmapInfo info;
    // 從 bitmap 中獲得信息位圖信息 AndroidBitmapInfo
    AndroidBitmap_getInfo(env, jbitmap, &info);




三、獲取 bitmap 中的圖像數據



調用 AndroidBitmap_lockPixels 方法 , 即可從 Java 的 Bitmap 對象中獲取數據的首地址 ; 向該函數中傳入一個二維指針 , 該二維指針參數作爲返回值使用 , 該二維指針最終指向的內存就是圖像數據內存 ;


1. AndroidBitmap_lockPixels 函數作用 : 從給定 Java Bitmap 對象中 , 獲取其對應的像素數據地址 ; 鎖定可以保證像素數據內存是固定不變的 , 直到調用解除鎖定方法 , 清除相關數據 ; 該方法必須與 AndroidBitmap_unlockPixels 方法成對使用 , 之後 addrPtr 地址不應該再被使用到 ; 如果執行成功 , *addrPtr 會指向圖像像素數據的首地址 , 如果方法失敗 , 那麼該二維指針是無效的指針 ;



2. AndroidBitmap_lockPixels 函數原型 :


① JNIEnv* env 參數 : 注意這裏用到了 JNIEnv* env 參數 , 主線程調用可以直接使用, 子線程調用的話 , 需要使用 JavaVM 調用 AttachCurrentThread 方法 , 傳入 JNIEnv 指針 , 然後該 JNIEnv 就是線程對應的 JNI 環境 , 使用完畢後解除綁定 ;

參考 【Android NDK 開發】JNI 線程 ( JNI 線程創建 | 線程執行函數 | 非 JNI 方法獲取 JNIEnv 與 Java 對象 | 線程獲取 JNIEnv | 全局變量設置 ) 博客 ;

② jobject jbitmap 參數 : Java 中的 Bitmap 對象 ;

③ void** addrPtr 參數 : 二維指針 , 執行成功後指向圖像像素數據的首地址 , 同時用於當做返回值 , 讓用戶可以調用到像素數據 ;

int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);

3. 代碼示例 :

    // 該類型最終類型是 unsigned char, 相當於 Java 中的 byte
    // 這是個 byte 指針, 指向一個數組
    // 此處作爲返回值使用
    uint8_t *addrPtr;
    // 注意該獲取的信息中包含透明度信息, 像素格式是 ARGB
    AndroidBitmap_lockPixels(env, jbitmap, (void **) &addrPtr);




四、過濾 bitmap 中的圖像數據 ( 獲取 RGB 數據 剔除 A 通道數據 )



1. 數據過濾需求 : 之前已經獲取到了圖像數據 , 存儲在了 addrPtr 指針中 , 現在需要將 RGB 數據取出, 剔除 ALPHA 透明度通道數據 , 只保留 RGB 通道數據 ;


2. 兩塊內存 : uint8_t* addrPtr 指針指向的內存是源數據 , uint8_t* data 指針指向的的數據是目標數據 , 最終要壓縮的數據是 data 目標數據 ;


3. 像素格式 : 源數據中存儲的 BGRA 像素格式的數據 , 目標數據中存儲的是 BGR 像素格式的數據 ;


4. 循環像素數據 : 開啓循環 , 直接循環遍歷每個像素點 , 注意像素點存放格式是 BGRA , 然後將數據存儲到另一塊內存中 , 存儲順序是 BGR ; 注意每次循環後 , 都需要移動對應的指針 ;

    // JPEG 格式的圖片, 沒有透明度信息, 像素格式是 RGB
    // 這裏需要去掉透明度信息
    // 獲取圖片的像素寬度
    int width = info.width;
    // 獲取圖片的像素高度
    int height = info.height;

    // 存儲 RGB 數據
    uint8_t* data = (uint8_t *) malloc(width * height * 3);

    // data 指針在後續發生了移動, 這裏保存下初始的指針, 方便在之後釋放該指針
    uint8_t* temp = data;

    // JPEG 像素中的 RGB 三原色, 紅綠藍
    uint8_t red, green, blue;

    // 遍歷從 Bitmap 內存 addrPtr 中讀取 BGRA 數據, 然後向 data 內存存儲 BGR 數據中
    for(int i = 0; i < height; i++){
        for (int j = 0; j < width; ++j) {
        	// 處理 i 行 j 列像素點信息 
            // 在 Bitmap 中內存存儲序列是 BGRA
            blue = *( addrPtr );
            green = *( addrPtr + 1 );
            red = *( addrPtr + 2 );

            // libturbojpeg 中 JPEG 圖像內存排列格式是 BGR
            *data = blue;
            *( data + 1 ) = green;
            *( data + 2 ) = red;

            // 移動 data 指針
            data += 3;
            //移動 addrPtr 指針, 爲下一次讀取數據做準備
            addrPtr +=4;
        }
    }// 截止到此處, 已經讀取出 JPEG 圖片所需的數據, 在 data 指針中




五、釋放資源



之前還有個步驟是壓縮 jpeg 格式圖片 , 這個過程比較複雜單開一個博客講解 , 該章節講解壓縮完畢後的內存釋放操作 ;


1. 鎖定 / 解鎖 像素數據 : AndroidBitmap_unlockPixels 方法與 AndroidBitmap_lockPixels 方法成對使用 , 表示之後不再需要使用 Bitmap 對象的數據了 ;


2. 釋放壓縮數據 : 釋放掉存儲要壓縮的 JPEG 圖片 RGB 數據的內存 , 此時已經壓完畢 , 可以將之前申請的內存都釋放掉了 ; 注意之前申請的 data 指針 , 在拷貝數據過程中 , 將該指針移動過了 , 不能釋放 data 指針 , 只能釋放之前 data 內存申請後的備份指針 , 否則報錯 ;


3. 釋放字符串 : env->GetStringUTFChars 創建的字符串是局部引用 , 這裏需要釋放掉 , 及時回收內存是個好習慣 ;

    // 解鎖
    AndroidBitmap_unlockPixels(env,jbitmap);
    // 注意要釋放 temp 指針 , 不要釋放成 data 指針, 否則會出錯
    free(temp);
    // 釋放局部引用, 不釋放, GC 也會回收, 但是有延遲
    env->ReleaseStringUTFChars(path, filePath);




六、Bitmap 圖像數據處理



GitHub 項目地址 : han1202012/PictureCompression


libjpeg-turbo 壓縮 JPEG 代碼示例 :

#include <jni.h>
#include <string>
#include <jpeglib.h>
#include <android/bitmap.h>
#include <malloc.h>
#include <android/log.h>
#include <bitset>
#include <iosfwd>

// 聲明函數
void compressJpegFile(uint8_t *data, int imageWidth, int imageHeight,
                      jint compressQuality, const char *filename);

extern "C" JNIEXPORT jstring JNICALL
Java_kim_hsl_pc_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";

    // 測試 libturbojpeg.a 函數庫
    jpeg_compress_struct jcs;
    __android_log_print(ANDROID_LOG_INFO, "JPEG", "jpeg_compress_struct jcs = %d", jcs.image_width);
    hello = hello + " , jpeg_compress_struct jcs = " + std::to_string(jcs.image_width);

    return env->NewStringUTF(hello.c_str());
}

/**
 * 圖片壓縮方法
 */
extern "C"
JNIEXPORT void JNICALL
Java_kim_hsl_pc_MainActivity_native_1pictureCompress(JNIEnv *env, jobject thiz,
                                                     jobject jbitmap,
                                                     jint quality, jstring path) {

    // 將 Java 字符串轉爲 C 字符串, 注意這是局部引用
    const char *filePath = env->GetStringUTFChars(path, 0);

    // 聲明 位圖信息, 該變量作爲返回值使用
    // 引用自 bitmap.h
    AndroidBitmapInfo info;
    // 從 bitmap 中獲得信息位圖信息 AndroidBitmapInfo
    AndroidBitmap_getInfo(env, jbitmap, &info);

    // 該類型最終類型是 unsigned char, 相當於 Java 中的 byte
    // 這是個 byte 指針, 指向一個數組
    // 此處作爲返回值使用
    uint8_t *addrPtr;
    // 注意該獲取的信息中包含透明度信息, 像素格式是 ARGB
    AndroidBitmap_lockPixels(env, jbitmap, (void **) &addrPtr);

    // JPEG 格式的圖片, 沒有透明度信息, 像素格式是 RGB
    // 這裏需要去掉透明度信息
    // 獲取圖片的像素寬度
    int width = info.width;
    // 獲取圖片的像素高度
    int height = info.height;

    //rgb
    uint8_t* data = (uint8_t *) malloc(width * height * 3);

    // data 指針在後續發生了移動, 這裏保存下初始的指針, 方便在之後釋放該指針
    uint8_t* temp = data;

    // JPEG 像素中的 RGB 三原色, 紅綠藍
    uint8_t red, green, blue;

    // 遍歷從 Bitmap 內存 addrPtr 中讀取 BGRA 數據, 然後向 data 內存存儲 BGR 數據中
    for(int i = 0; i < height; i++){
        for (int j = 0; j < width; ++j) {
            // 在 Bitmap 中內存存儲序列是 BGRA
            blue = *( addrPtr );
            green = *( addrPtr + 1 );
            red = *( addrPtr + 2 );

            // libturbojpeg 中 JPEG 圖像內存排列格式是 BGR
            *data = blue;
            *( data + 1 ) = green;
            *( data + 2 ) = red;

            // 移動 data 指針
            data += 3;
            //移動 addrPtr 指針, 爲下一次讀取數據做準備
            addrPtr +=4;
        }
    }// 截止到此處, 已經讀取出 JPEG 圖片所需的數據, 在 data 指針中

    // 將 data 指針中的數據壓縮到 JPEG 格式圖片中
    compressJpegFile(temp, width, height, quality, filePath);

    // 解鎖
    AndroidBitmap_unlockPixels(env,jbitmap);
    // 注意要釋放 temp 指針 , 不要釋放成 data 指針, 否則會出錯
    free(temp);
    // 釋放局部引用, 不釋放, GC 也會回收, 但是有延遲
    env->ReleaseStringUTFChars(path, filePath);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章