Android集成封装libjpeg-turbo解码库,Kotlin语言实现

Android集成封装libjpeg-turbo解码库,Kotlin语言实现

摘要:在Android应用中集成libjpeg-turbo解码库,并通过Kotlin封装对外提供易用的图片解码接口。主要内容包括:1. 在jni目录下配置CMake构建文件,集成libjpeg-turbo源码;2. 实现native层解码逻辑,支持文件路径、文件描述符等方式输入;3. 提供Kotlin工具类LibJpeg,支持EXIF方向自动旋转、缩略图快速解码、Bitmap复用等功能;4. 配置gradle构建参数,确保正确编译native代码。该方案通过直接解码到Bitmap像素内存、DCT缩放等技术优化性能,适用于需要高效JPEG解码的Android应用场景。

一、在应用的工程目录下建jni目录( app\src\main\jni )

从libjpeg-turbo项目主页:https://github.com/libjpeg-turbo/libjpeg-turbo

下载最新的源代码,复制粘贴到jni目录下,另外新建两个新文件CMakeLists.txt 与 native-lib.cpp,这两个文件放到app\src\main\jni下。

CMakeLists.txt

复制代码
cmake_minimum_required(VERSION 3.22.1)

project(nativejpeg LANGUAGES C CXX)

include(ExternalProject)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

set(JNI_DIR ${CMAKE_CURRENT_SOURCE_DIR})

set(LIBJPEG_TURBO_SRC_DIR ${JNI_DIR}/libjpeg-turbo)
set(LIBJPEG_TURBO_BUILD_DIR ${CMAKE_BINARY_DIR}/libjpeg-turbo-build)
set(LIBJPEG_TURBO_INSTALL_DIR ${CMAKE_BINARY_DIR}/libjpeg-turbo-install)

message(STATUS "ANDROID_ABI = ${ANDROID_ABI}")
message(STATUS "ANDROID_PLATFORM = ${ANDROID_PLATFORM}")
message(STATUS "ANDROID_STL = ${ANDROID_STL}")
message(STATUS "CMAKE_TOOLCHAIN_FILE = ${CMAKE_TOOLCHAIN_FILE}")
message(STATUS "CMAKE_MAKE_PROGRAM = ${CMAKE_MAKE_PROGRAM}")

if (NOT EXISTS ${LIBJPEG_TURBO_SRC_DIR}/CMakeLists.txt)
    message(FATAL_ERROR "Cannot find libjpeg-turbo source: ${LIBJPEG_TURBO_SRC_DIR}/CMakeLists.txt")
endif()

if (NOT EXISTS ${LIBJPEG_TURBO_SRC_DIR}/src/turbojpeg.h)
    message(FATAL_ERROR "Cannot find turbojpeg.h: ${LIBJPEG_TURBO_SRC_DIR}/src/turbojpeg.h")
endif()

set(LIBTURBOJPEG_STATIC_LIB
        ${LIBJPEG_TURBO_INSTALL_DIR}/lib/libturbojpeg.a
)

ExternalProject_Add(
        turbojpeg_external

        SOURCE_DIR
        ${LIBJPEG_TURBO_SRC_DIR}

        BINARY_DIR
        ${LIBJPEG_TURBO_BUILD_DIR}

        CMAKE_GENERATOR
        Ninja

        CMAKE_ARGS
        -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
        -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
        -DANDROID_ABI=${ANDROID_ABI}
        -DANDROID_PLATFORM=${ANDROID_PLATFORM}
        -DANDROID_STL=${ANDROID_STL}
        -DCMAKE_BUILD_TYPE=Release
        -DCMAKE_INSTALL_PREFIX=${LIBJPEG_TURBO_INSTALL_DIR}
        -DCMAKE_POSITION_INDEPENDENT_CODE=ON

        -DENABLE_SHARED=OFF
        -DENABLE_STATIC=ON
        -DWITH_JAVA=OFF
        -DWITH_JNA=OFF
        -DWITH_TURBOJPEG=ON
        -DWITH_12BIT=OFF
        -DWITH_TOOLS=OFF
        -DWITH_TESTS=OFF
        -DWITH_SIMD=ON

        BUILD_BYPRODUCTS
        ${LIBTURBOJPEG_STATIC_LIB}

        INSTALL_COMMAND
        ${CMAKE_COMMAND} --build <BINARY_DIR> --target install
)

add_library(
        turbojpeg_prebuilt
        STATIC
        IMPORTED
        GLOBAL
)

set_target_properties(
        turbojpeg_prebuilt
        PROPERTIES
        IMPORTED_LOCATION
        ${LIBTURBOJPEG_STATIC_LIB}
)

add_library(
        nativejpeg
        SHARED
        native-lib.cpp
)

target_include_directories(
        nativejpeg
        PRIVATE
        ${LIBJPEG_TURBO_SRC_DIR}
        ${LIBJPEG_TURBO_SRC_DIR}/src
        ${LIBJPEG_TURBO_INSTALL_DIR}/include
)

add_dependencies(
        nativejpeg
        turbojpeg_external
)

target_link_libraries(
        nativejpeg
        PRIVATE
        turbojpeg_prebuilt
        jnigraphics
        log
        android
        m
)

native-lib.cpp:

cpp 复制代码
#include <jni.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <android/trace.h>

#include <turbojpeg.h>

#include <vector>
#include <fstream>
#include <cmath>
#include <cstring>
#include <climits>
#include <algorithm>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <errno.h>
#include <time.h>

#define LOG_TAG "NativeLibJpeg"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

static int64_t nowMs() {
    timespec ts{};
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return static_cast<int64_t>(ts.tv_sec) * 1000LL + ts.tv_nsec / 1000000LL;
}

static constexpr int THUMBNAIL_POLICY_NEVER = 0;
static constexpr int THUMBNAIL_POLICY_AUTO = 1;
static constexpr int THUMBNAIL_POLICY_ALWAYS = 2;

/**
 * Path 解码时的文件读取策略:
 *
 * 1: 使用 mmap。
 *    优点:避免 readFileToBuffer 把整个 JPEG 主动 copy 到 vector。
 *    注意:mmap 是懒加载,实际 IO/page fault 可能会计入 tjDecompress2 阶段。
 *
 * 0: 使用 open/read。
 *    优点:耗时边界清晰,readFileToBuffer 会显示真实读取成本。
 */
#ifndef LIBJPEG_USE_MMAP_FOR_PATH
#define LIBJPEG_USE_MMAP_FOR_PATH 1
#endif

#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif



struct ExifInfo {
    int orientation = 1;
    bool hasThumbnail = false;
    size_t thumbnailOffset = 0;
    size_t thumbnailSize = 0;
};

static thread_local std::vector<unsigned char> tlsRgbaBuffer;

static uint16_t read16(const unsigned char *p, bool le) {
    if (le) {
        return static_cast<uint16_t>(p[0] | (p[1] << 8));
    } else {
        return static_cast<uint16_t>((p[0] << 8) | p[1]);
    }
}

static uint32_t read32(const unsigned char *p, bool le) {
    if (le) {
        return static_cast<uint32_t>(
                p[0] |
                (p[1] << 8) |
                (p[2] << 16) |
                (p[3] << 24)
        );
    } else {
        return static_cast<uint32_t>(
                (p[0] << 24) |
                (p[1] << 16) |
                (p[2] << 8) |
                p[3]
        );
    }
}


static bool readFileToBuffer(const char *path, std::vector<unsigned char> &out) {
    ATrace_beginSection("LibJpeg.native.readFile.openRead");

    const int64_t start = nowMs();

    out.clear();

    if (!path) {
        LOGE("readFileToBuffer failed: path is null");
        ATrace_endSection();
        return false;
    }

    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0) {
        LOGE("open file failed: %s, errno=%d", path, errno);
        ATrace_endSection();
        return false;
    }

    struct stat st{};
    if (fstat(fd, &st) != 0) {
        LOGE("fstat failed: %s, errno=%d", path, errno);
        close(fd);
        ATrace_endSection();
        return false;
    }

    if (st.st_size <= 0) {
        LOGE("invalid file size: %lld", static_cast<long long>(st.st_size));
        close(fd);
        ATrace_endSection();
        return false;
    }

    const size_t fileSize = static_cast<size_t>(st.st_size);

    out.resize(fileSize);

    size_t total = 0;

    while (total < fileSize) {
        ssize_t n = read(
                fd,
                out.data() + total,
                fileSize - total
        );

        if (n < 0) {
            if (errno == EINTR) {
                continue;
            }

            LOGE("read file failed: %s, errno=%d", path, errno);
            close(fd);
            out.clear();
            ATrace_endSection();
            return false;
        }

        if (n == 0) {
            break;
        }

        total += static_cast<size_t>(n);
    }

    close(fd);

    if (total != fileSize) {
        LOGE(
                "read file incomplete: expect=%zu, actual=%zu",
                fileSize,
                total
        );

        out.resize(total);
    }

    const int64_t end = nowMs();

    LOGD(
            "readFileToBuffer open/read success: size=%zu bytes, cost=%lldms",
            out.size(),
            static_cast<long long>(end - start)
    );

    ATrace_endSection();
    return !out.empty();
}

struct MappedFile {
    int fd = -1;
    const unsigned char *data = nullptr;
    size_t size = 0;
};

static bool mapFileReadOnly(const char *path, MappedFile &mappedFile) {
    ATrace_beginSection("LibJpeg.native.mapFileReadOnly");

    const int64_t start = nowMs();

    mappedFile = MappedFile{};

    if (!path) {
        LOGE("mapFileReadOnly failed: path is null");
        ATrace_endSection();
        return false;
    }

    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0) {
        LOGE("mmap open failed: %s, errno=%d", path, errno);
        ATrace_endSection();
        return false;
    }

    struct stat st{};
    if (fstat(fd, &st) != 0) {
        LOGE("mmap fstat failed: %s, errno=%d", path, errno);
        close(fd);
        ATrace_endSection();
        return false;
    }

    if (st.st_size <= 0) {
        LOGE("mmap invalid file size: %lld", static_cast<long long>(st.st_size));
        close(fd);
        ATrace_endSection();
        return false;
    }

    const size_t fileSize = static_cast<size_t>(st.st_size);

    void *addr = mmap(
            nullptr,
            fileSize,
            PROT_READ,
            MAP_PRIVATE,
            fd,
            0
    );

    if (addr == MAP_FAILED) {
        LOGE("mmap failed: %s, size=%zu, errno=%d", path, fileSize, errno);
        close(fd);
        ATrace_endSection();
        return false;
    }

#ifdef MADV_SEQUENTIAL
    /**
     * 提示内核这是顺序读取场景。
     * 失败也不影响功能。
     */
    madvise(addr, fileSize, MADV_SEQUENTIAL);
#endif

    mappedFile.fd = fd;
    mappedFile.data = reinterpret_cast<const unsigned char *>(addr);
    mappedFile.size = fileSize;

    const int64_t end = nowMs();

    LOGD(
            "mapFileReadOnly success: size=%zu bytes, cost=%lldms",
            mappedFile.size,
            static_cast<long long>(end - start)
    );

    ATrace_endSection();
    return true;
}

static void unmapFile(MappedFile &mappedFile) {
    ATrace_beginSection("LibJpeg.native.unmapFile");

    if (mappedFile.data && mappedFile.size > 0) {
        munmap(
                const_cast<unsigned char *>(mappedFile.data),
                mappedFile.size
        );
    }

    if (mappedFile.fd >= 0) {
        close(mappedFile.fd);
    }

    mappedFile = MappedFile{};

    ATrace_endSection();
}



static bool readFdToBuffer(int inputFd, std::vector<unsigned char> &out) {
    ATrace_beginSection("LibJpeg.native.readFd");

    int fd = dup(inputFd);
    if (fd < 0) {
        LOGE("dup fd failed, errno=%d", errno);
        ATrace_endSection();
        return false;
    }

    struct stat st{};
    if (fstat(fd, &st) != 0) {
        LOGE("fstat failed, errno=%d", errno);
        close(fd);
        ATrace_endSection();
        return false;
    }

    if (st.st_size <= 0) {
        LOGE("invalid fd size");
        close(fd);
        ATrace_endSection();
        return false;
    }

    if (lseek(fd, 0, SEEK_SET) < 0) {
        LOGE("lseek failed, errno=%d", errno);
        close(fd);
        ATrace_endSection();
        return false;
    }

    out.resize(static_cast<size_t>(st.st_size));

    size_t total = 0;
    while (total < out.size()) {
        ssize_t n = read(fd, out.data() + total, out.size() - total);
        if (n < 0) {
            if (errno == EINTR) continue;
            LOGE("read fd failed, errno=%d", errno);
            close(fd);
            ATrace_endSection();
            return false;
        }
        if (n == 0) break;
        total += static_cast<size_t>(n);
    }

    close(fd);

    if (total != out.size()) {
        out.resize(total);
    }

    ATrace_endSection();
    return !out.empty();
}

static void parseIfd0(
        const unsigned char *tiff,
        size_t tiffSize,
        uint32_t ifdOffset,
        bool le,
        ExifInfo &info
) {
    if (ifdOffset + 2 > tiffSize) return;

    const unsigned char *ifd = tiff + ifdOffset;
    uint16_t entryCount = read16(ifd, le);

    size_t entriesStart = ifdOffset + 2;

    for (uint16_t i = 0; i < entryCount; i++) {
        size_t entryOffset = entriesStart + static_cast<size_t>(i) * 12;
        if (entryOffset + 12 > tiffSize) break;

        const unsigned char *entry = tiff + entryOffset;
        uint16_t tag = read16(entry, le);

        if (tag == 0x0112) {
            uint16_t type = read16(entry + 2, le);
            uint32_t count = read32(entry + 4, le);

            if (type == 3 && count >= 1) {
                uint16_t orientation = read16(entry + 8, le);
                if (orientation >= 1 && orientation <= 8) {
                    info.orientation = orientation;
                }
            }
        }
    }
}

static void parseIfd1Thumbnail(
        const unsigned char *tiff,
        size_t tiffSize,
        uint32_t ifdOffset,
        bool le,
        size_t tiffAbsOffset,
        size_t jpegTotalSize,
        ExifInfo &info
) {
    if (ifdOffset == 0) return;
    if (ifdOffset + 2 > tiffSize) return;

    const unsigned char *ifd = tiff + ifdOffset;
    uint16_t entryCount = read16(ifd, le);

    size_t entriesStart = ifdOffset + 2;

    uint32_t jpegOffset = 0;
    uint32_t jpegLength = 0;

    for (uint16_t i = 0; i < entryCount; i++) {
        size_t entryOffset = entriesStart + static_cast<size_t>(i) * 12;
        if (entryOffset + 12 > tiffSize) break;

        const unsigned char *entry = tiff + entryOffset;
        uint16_t tag = read16(entry, le);

        if (tag == 0x0201) {
            jpegOffset = read32(entry + 8, le);
        } else if (tag == 0x0202) {
            jpegLength = read32(entry + 8, le);
        }
    }

    if (jpegOffset > 0 && jpegLength > 0) {
        size_t absOffset = tiffAbsOffset + jpegOffset;
        size_t absEnd = absOffset + jpegLength;

        if (absOffset < jpegTotalSize && absEnd <= jpegTotalSize && absEnd > absOffset) {
            info.hasThumbnail = true;
            info.thumbnailOffset = absOffset;
            info.thumbnailSize = jpegLength;
        }
    }
}

static ExifInfo parseExifInfo(const unsigned char *data, size_t size) {
    ATrace_beginSection("LibJpeg.native.parseExifInfo");

    ExifInfo info;

    if (!data || size < 4) {
        ATrace_endSection();
        return info;
    }

    if (data[0] != 0xFF || data[1] != 0xD8) {
        ATrace_endSection();
        return info;
    }

    size_t pos = 2;

    while (pos + 4 < size) {
        if (data[pos] != 0xFF) break;

        unsigned char marker = data[pos + 1];
        pos += 2;

        if (marker == 0xDA || marker == 0xD9) break;
        if (pos + 2 > size) break;

        uint16_t segmentLen = static_cast<uint16_t>((data[pos] << 8) | data[pos + 1]);
        if (segmentLen < 2) break;

        size_t segmentStart = pos + 2;
        size_t segmentSize = segmentLen - 2;

        if (segmentStart + segmentSize > size) break;

        if (marker == 0xE1 && segmentSize >= 14) {
            const unsigned char *seg = data + segmentStart;

            if (memcmp(seg, "Exif\0\0", 6) == 0) {
                const unsigned char *tiff = seg + 6;
                size_t tiffSize = segmentSize - 6;
                size_t tiffAbsOffset = segmentStart + 6;

                if (tiffSize >= 8) {
                    bool le;
                    if (tiff[0] == 'I' && tiff[1] == 'I') {
                        le = true;
                    } else if (tiff[0] == 'M' && tiff[1] == 'M') {
                        le = false;
                    } else {
                        break;
                    }

                    uint16_t magic = read16(tiff + 2, le);
                    if (magic != 42) break;

                    uint32_t ifd0Offset = read32(tiff + 4, le);
                    parseIfd0(tiff, tiffSize, ifd0Offset, le, info);

                    if (ifd0Offset + 2 <= tiffSize) {
                        uint16_t count = read16(tiff + ifd0Offset, le);
                        size_t nextIfdOffsetPos = ifd0Offset + 2 + static_cast<size_t>(count) * 12;

                        if (nextIfdOffsetPos + 4 <= tiffSize) {
                            uint32_t ifd1Offset = read32(tiff + nextIfdOffsetPos, le);
                            parseIfd1Thumbnail(
                                    tiff,
                                    tiffSize,
                                    ifd1Offset,
                                    le,
                                    tiffAbsOffset,
                                    size,
                                    info
                            );
                        }
                    }
                }

                break;
            }
        }

        pos = segmentStart + segmentSize;
    }

    ATrace_endSection();
    return info;
}

static int orientationToRotation(int orientation) {
    switch (orientation) {
        case 3:
        case 4:
            return 180;
        case 5:
        case 6:
            return 90;
        case 7:
        case 8:
            return 270;
        case 1:
        case 2:
        default:
            return 0;
    }
}

static bool getJpegHeader(
        const unsigned char *jpegData,
        size_t jpegSize,
        int &width,
        int &height
) {
    tjhandle handle = tjInitDecompress();
    if (!handle) return false;

    int subsamp = 0;
    int colorSpace = 0;

    int ret = tjDecompressHeader3(
            handle,
            jpegData,
            static_cast<unsigned long>(jpegSize),
            &width,
            &height,
            &subsamp,
            &colorSpace
    );

    tjDestroy(handle);

    return ret == 0 && width > 0 && height > 0;
}

static bool shouldUseExifThumbnail(
        const unsigned char *thumbData,
        size_t thumbSize,
        int targetW,
        int targetH,
        int rotation,
        int thumbnailPolicy
) {
    if (!thumbData || thumbSize == 0) return false;
    if (thumbnailPolicy == THUMBNAIL_POLICY_NEVER) return false;
    if (thumbnailPolicy == THUMBNAIL_POLICY_ALWAYS) return true;

    int tw = 0;
    int th = 0;
    if (!getJpegHeader(thumbData, thumbSize, tw, th)) return false;

    int logicalW = tw;
    int logicalH = th;
    if (rotation == 90 || rotation == 270) {
        logicalW = th;
        logicalH = tw;
    }

    /**
     * AUTO 策略:
     *
     * 如果 EXIF thumbnail 尺寸已经不小于目标尺寸的 50%,
     * 就认为它适合作为 fast path。
     *
     * 这个阈值适合首帧预览。
     * 如果你们希望更清晰,可以改成 0.75 或 1.0。
     */
    return logicalW >= targetW * 0.5f && logicalH >= targetH * 0.5f;
}

static jobject createBitmap(JNIEnv *env, int width, int height) {
    jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
    if (!bitmapClass) return nullptr;

    jclass configClass = env->FindClass("android/graphics/Bitmap$Config");
    if (!configClass) return nullptr;

    jfieldID argb8888Field = env->GetStaticFieldID(
            configClass,
            "ARGB_8888",
            "Landroid/graphics/Bitmap$Config;"
    );
    if (!argb8888Field) return nullptr;

    jobject argb8888Config = env->GetStaticObjectField(configClass, argb8888Field);
    if (!argb8888Config) return nullptr;

    jmethodID createBitmapMethod = env->GetStaticMethodID(
            bitmapClass,
            "createBitmap",
            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"
    );
    if (!createBitmapMethod) return nullptr;

    return env->CallStaticObjectMethod(
            bitmapClass,
            createBitmapMethod,
            width,
            height,
            argb8888Config
    );
}

static bool isReusableBitmap(JNIEnv *env, jobject bitmap, int width, int height) {
    if (!bitmap) return false;

    AndroidBitmapInfo info{};
    if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
        return false;
    }

    if (info.width != static_cast<uint32_t>(width)) return false;
    if (info.height != static_cast<uint32_t>(height)) return false;
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) return false;

    jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
    if (!bitmapClass) return false;

    jmethodID isMutableMethod = env->GetMethodID(bitmapClass, "isMutable", "()Z");
    if (!isMutableMethod) return false;

    return env->CallBooleanMethod(bitmap, isMutableMethod) == JNI_TRUE;
}

static jobject obtainOutputBitmap(
        JNIEnv *env,
        jobject reuseBitmap,
        int width,
        int height
) {
    ATrace_beginSection("LibJpeg.native.obtainBitmap");

    if (isReusableBitmap(env, reuseBitmap, width, height)) {
        ATrace_endSection();
        return reuseBitmap;
    }

    jobject bitmap = createBitmap(env, width, height);

    ATrace_endSection();
    return bitmap;
}

static tjscalingfactor chooseScalingFactor(
        int srcW,
        int srcH,
        int targetW,
        int targetH,
        int rotation
) {
    int numScalingFactors = 0;
    tjscalingfactor *factors = tjGetScalingFactors(&numScalingFactors);

    tjscalingfactor bestFactor{1, 1};

    if (!factors || numScalingFactors <= 0) {
        return bestFactor;
    }

    bool found = false;
    long long bestPixels = LLONG_MAX;

    for (int i = 0; i < numScalingFactors; i++) {
        int scaledW = TJSCALED(srcW, factors[i]);
        int scaledH = TJSCALED(srcH, factors[i]);

        int logicalW = scaledW;
        int logicalH = scaledH;
        if (rotation == 90 || rotation == 270) {
            logicalW = scaledH;
            logicalH = scaledW;
        }

        if (logicalW >= targetW && logicalH >= targetH) {
            long long pixels = static_cast<long long>(scaledW) * scaledH;
            if (pixels < bestPixels) {
                bestPixels = pixels;
                bestFactor = factors[i];
                found = true;
            }
        }
    }

    if (!found) {
        bestFactor.num = 1;
        bestFactor.denom = 1;
    }

    return bestFactor;
}

static inline void mapOrientedToRaw(
        float ox,
        float oy,
        int rawW,
        int rawH,
        int rotation,
        float &rx,
        float &ry
) {
    switch (rotation) {
        case 90:
            rx = oy;
            ry = static_cast<float>(rawH - 1) - ox;
            break;
        case 180:
            rx = static_cast<float>(rawW - 1) - ox;
            ry = static_cast<float>(rawH - 1) - oy;
            break;
        case 270:
            rx = static_cast<float>(rawW - 1) - oy;
            ry = ox;
            break;
        case 0:
        default:
            rx = ox;
            ry = oy;
            break;
    }
}

static inline void sampleBilinearRGBA(
        const unsigned char *src,
        int srcW,
        int srcH,
        float fx,
        float fy,
        unsigned char *out
) {
    fx = std::clamp(fx, 0.0f, static_cast<float>(srcW - 1));
    fy = std::clamp(fy, 0.0f, static_cast<float>(srcH - 1));

    int x0 = static_cast<int>(floorf(fx));
    int y0 = static_cast<int>(floorf(fy));
    int x1 = std::min(x0 + 1, srcW - 1);
    int y1 = std::min(y0 + 1, srcH - 1);

    float wx = fx - x0;
    float wy = fy - y0;

    const unsigned char *p00 = src + (y0 * srcW + x0) * 4;
    const unsigned char *p10 = src + (y0 * srcW + x1) * 4;
    const unsigned char *p01 = src + (y1 * srcW + x0) * 4;
    const unsigned char *p11 = src + (y1 * srcW + x1) * 4;

    for (int c = 0; c < 4; c++) {
        float v00 = p00[c];
        float v10 = p10[c];
        float v01 = p01[c];
        float v11 = p11[c];

        float v0 = v00 + (v10 - v00) * wx;
        float v1 = v01 + (v11 - v01) * wx;
        float v = v0 + (v1 - v0) * wy;

        out[c] = static_cast<unsigned char>(std::clamp(v, 0.0f, 255.0f));
    }
}

static void rotateResizeRGBA(
        const unsigned char *src,
        int srcW,
        int srcH,
        unsigned char *dst,
        int dstW,
        int dstH,
        int dstStride,
        int rotation
) {
    ATrace_beginSection("LibJpeg.native.rotateResize");

    /**
     * Fast copy path:
     *
     * 如果没有旋转,且源尺寸和目标尺寸完全一致,
     * 不需要做任何双线性采样,直接按行 memcpy 即可。
     *
     * 注意:这个路径仍然是 buffer -> bitmap copy。
     * 更快的是 decodeSelectedJpegToBitmap() 里的 direct decode path:
     * tjDecompress2 直接写 Bitmap pixels。
     */
    if (rotation == 0 && srcW == dstW && srcH == dstH) {
        ATrace_beginSection("LibJpeg.native.rotateResize.memcpyFastPath");

        const int srcStride = srcW * 4;
        const int copyBytesPerRow = dstW * 4;

        for (int y = 0; y < dstH; y++) {
            memcpy(
                    dst + static_cast<size_t>(y) * dstStride,
                    src + static_cast<size_t>(y) * srcStride,
                    static_cast<size_t>(copyBytesPerRow)
            );
        }

        ATrace_endSection();
        ATrace_endSection();
        return;
    }

    int orientedW = srcW;
    int orientedH = srcH;

    if (rotation == 90 || rotation == 270) {
        orientedW = srcH;
        orientedH = srcW;
    }

    float scaleX = static_cast<float>(orientedW) / static_cast<float>(dstW);
    float scaleY = static_cast<float>(orientedH) / static_cast<float>(dstH);

    for (int y = 0; y < dstH; y++) {
        unsigned char *dstRow = dst + static_cast<size_t>(y) * dstStride;

        float oy = (y + 0.5f) * scaleY - 0.5f;

        for (int x = 0; x < dstW; x++) {
            float ox = (x + 0.5f) * scaleX - 0.5f;

            float rx = 0.0f;
            float ry = 0.0f;

            mapOrientedToRaw(ox, oy, srcW, srcH, rotation, rx, ry);

            unsigned char *dp = dstRow + x * 4;
            sampleBilinearRGBA(src, srcW, srcH, rx, ry, dp);
        }
    }

    ATrace_endSection();
}


static jobject decodeSelectedJpegToBitmap(
        JNIEnv *env,
        const unsigned char *jpegData,
        size_t jpegSize,
        int orientation,
        jint targetWidth,
        jint targetHeight,
        jobject reuseBitmap,
        bool isExifThumbnail
) {
    ATrace_beginSection(isExifThumbnail
                        ? "LibJpeg.native.decodeExifThumbnail"
                        : "LibJpeg.native.decodeOriginal");

    const int64_t totalStart = nowMs();

    int rotation = orientationToRotation(orientation);

    tjhandle handle = tjInitDecompress();
    if (!handle) {
        ATrace_endSection();
        return nullptr;
    }

    int srcW = 0;
    int srcH = 0;
    int subsamp = 0;
    int colorSpace = 0;

    ATrace_beginSection("LibJpeg.native.header");
    int headerRet = tjDecompressHeader3(
            handle,
            jpegData,
            static_cast<unsigned long>(jpegSize),
            &srcW,
            &srcH,
            &subsamp,
            &colorSpace
    );
    ATrace_endSection();

    const int64_t afterHeader = nowMs();

    if (headerRet != 0 || srcW <= 0 || srcH <= 0) {
        LOGE("tjDecompressHeader3 failed: %s", tjGetErrorStr2(handle));
        tjDestroy(handle);
        ATrace_endSection();
        return nullptr;
    }

    tjscalingfactor scaleFactor{1, 1};

    /**
     * EXIF thumbnail 本身通常已经较小,不再做 DCT scale。
     * 原图走 DCT scale,减少 decode 成本。
     */
    if (!isExifThumbnail) {
        scaleFactor = chooseScalingFactor(
                srcW,
                srcH,
                targetWidth,
                targetHeight,
                rotation
        );
    }

    int decodeW = TJSCALED(srcW, scaleFactor);
    int decodeH = TJSCALED(srcH, scaleFactor);

    LOGD(
            "decode info: src=%dx%d, target=%dx%d, orientation=%d, rotation=%d, "
            "scale=%d/%d, decode=%dx%d, isThumb=%d",
            srcW,
            srcH,
            targetWidth,
            targetHeight,
            orientation,
            rotation,
            scaleFactor.num,
            scaleFactor.denom,
            decodeW,
            decodeH,
            isExifThumbnail ? 1 : 0
    );

    /**
     * 先创建/复用 Bitmap,并 lock pixels。
     *
     * 这样在 fast path 中可以让 tjDecompress2 直接写入 Bitmap pixels,
     * 避免中间 tlsRgbaBuffer 和 rotateResizeRGBA。
     */
    jobject bitmap = obtainOutputBitmap(
            env,
            reuseBitmap,
            targetWidth,
            targetHeight
    );

    const int64_t afterObtainBitmap = nowMs();

    if (!bitmap) {
        tjDestroy(handle);
        ATrace_endSection();
        return nullptr;
    }

    AndroidBitmapInfo bitmapInfo{};
    if (AndroidBitmap_getInfo(env, bitmap, &bitmapInfo) != ANDROID_BITMAP_RESULT_SUCCESS) {
        tjDestroy(handle);
        ATrace_endSection();
        return nullptr;
    }

    if (bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
        LOGE("bitmap format is not RGBA_8888, format=%d", bitmapInfo.format);
        tjDestroy(handle);
        ATrace_endSection();
        return nullptr;
    }

    void *pixels = nullptr;

    ATrace_beginSection("LibJpeg.native.lockPixels");
    int lockRet = AndroidBitmap_lockPixels(env, bitmap, &pixels);
    ATrace_endSection();

    const int64_t afterLockPixels = nowMs();

    if (lockRet != ANDROID_BITMAP_RESULT_SUCCESS || !pixels) {
        LOGE("AndroidBitmap_lockPixels failed, ret=%d", lockRet);
        tjDestroy(handle);
        ATrace_endSection();
        return nullptr;
    }

    int flags = TJFLAG_FASTDCT | TJFLAG_FASTUPSAMPLE;

    /**
     * Direct decode fast path:
     *
     * 条件:
     * 1. 不需要 EXIF 旋转;
     * 2. DCT scale 后的 decodeW/decodeH 正好等于目标 Bitmap 尺寸。
     *
     * 当前你的公平测试:
     * src=3048x4064
     * target=762x1016
     * scale=1/4
     * decode=762x1016
     * rotation=0
     *
     * 就应该命中这个路径。
     *
     * 命中后链路:
     * JPEG -> tjDecompress2 -> Android Bitmap pixels
     *
     * 避免:
     * JPEG -> tlsRgbaBuffer -> rotateResizeRGBA -> Android Bitmap pixels
     */
    bool canDirectDecodeToBitmap =
            rotation == 0 &&
            decodeW == targetWidth &&
            decodeH == targetHeight;

    if (canDirectDecodeToBitmap) {
        ATrace_beginSection("LibJpeg.native.tjDecompress2.directBitmap");

        int decodeRet = tjDecompress2(
                handle,
                jpegData,
                static_cast<unsigned long>(jpegSize),
                reinterpret_cast<unsigned char *>(pixels),
                decodeW,

                /**
                 * pitch 使用 bitmapInfo.stride。
                 *
                 * 不能传 0,因为 Bitmap 每行可能有 padding。
                 * 传 stride 后,TurboJPEG 会按 Bitmap 的真实行跨度写入。
                 */
                static_cast<int>(bitmapInfo.stride),

                decodeH,
                TJPF_RGBA,
                flags
        );

        ATrace_endSection();

        const int64_t afterDirectDecode = nowMs();

        if (decodeRet != 0) {
            LOGE("tjDecompress2 direct failed: %s", tjGetErrorStr2(handle));

            AndroidBitmap_unlockPixels(env, bitmap);
            tjDestroy(handle);

            ATrace_endSection();
            return nullptr;
        }

        AndroidBitmap_unlockPixels(env, bitmap);
        tjDestroy(handle);

        const int64_t totalEnd = nowMs();

        LOGD(
                "direct decode success: bitmap=%dx%d, stride=%d, "
                "cost header=%lldms, obtainBitmap=%lldms, lock=%lldms, "
                "directDecode=%lldms, totalSelected=%lldms",
                targetWidth,
                targetHeight,
                static_cast<int>(bitmapInfo.stride),
                static_cast<long long>(afterHeader - totalStart),
                static_cast<long long>(afterObtainBitmap - afterHeader),
                static_cast<long long>(afterLockPixels - afterObtainBitmap),
                static_cast<long long>(afterDirectDecode - afterLockPixels),
                static_cast<long long>(totalEnd - totalStart)
        );

        ATrace_endSection();
        return bitmap;
    }

    /**
     * Slow path:
     *
     * 以下情况会走这里:
     * 1. 需要 EXIF 旋转;
     * 2. decodeW/decodeH 和 targetWidth/targetHeight 不一致,需要 resize;
     * 3. EXIF thumbnail 没做 DCT scale,但最终要适配目标尺寸。
     *
     * 链路:
     * JPEG -> tlsRgbaBuffer -> rotateResizeRGBA -> Android Bitmap pixels
     */
    size_t rgbaSize = static_cast<size_t>(decodeW) * static_cast<size_t>(decodeH) * 4;
    if (tlsRgbaBuffer.size() < rgbaSize) {
        tlsRgbaBuffer.resize(rgbaSize);
    }

    const int64_t afterPrepareBuffer = nowMs();

    ATrace_beginSection("LibJpeg.native.tjDecompress2.buffer");
    int decodeRet = tjDecompress2(
            handle,
            jpegData,
            static_cast<unsigned long>(jpegSize),
            tlsRgbaBuffer.data(),
            decodeW,
            0,
            decodeH,
            TJPF_RGBA,
            flags
    );
    ATrace_endSection();

    const int64_t afterBufferDecode = nowMs();

    if (decodeRet != 0) {
        LOGE("tjDecompress2 buffer failed: %s", tjGetErrorStr2(handle));

        AndroidBitmap_unlockPixels(env, bitmap);
        tjDestroy(handle);

        ATrace_endSection();
        return nullptr;
    }

    tjDestroy(handle);

    rotateResizeRGBA(
            tlsRgbaBuffer.data(),
            decodeW,
            decodeH,
            reinterpret_cast<unsigned char *>(pixels),
            targetWidth,
            targetHeight,
            static_cast<int>(bitmapInfo.stride),
            rotation
    );

    const int64_t afterRotateResize = nowMs();

    AndroidBitmap_unlockPixels(env, bitmap);

    const int64_t totalEnd = nowMs();

    LOGD(
            "slow decode success: bitmap=%dx%d, stride=%d, "
            "cost header=%lldms, obtainBitmap=%lldms, lock=%lldms, "
            "prepareBuffer=%lldms, bufferDecode=%lldms, rotateResize=%lldms, "
            "totalSelected=%lldms",
            targetWidth,
            targetHeight,
            static_cast<int>(bitmapInfo.stride),
            static_cast<long long>(afterHeader - totalStart),
            static_cast<long long>(afterObtainBitmap - afterHeader),
            static_cast<long long>(afterLockPixels - afterObtainBitmap),
            static_cast<long long>(afterPrepareBuffer - afterLockPixels),
            static_cast<long long>(afterBufferDecode - afterPrepareBuffer),
            static_cast<long long>(afterRotateResize - afterBufferDecode),
            static_cast<long long>(totalEnd - totalStart)
    );

    ATrace_endSection();
    return bitmap;
}




static jobject decodeJpegBytesToBitmap(
        JNIEnv *env,
        const unsigned char *jpegData,
        size_t jpegSize,
        jint targetWidth,
        jint targetHeight,
        jobject reuseBitmap,
        jint thumbnailPolicy
) {
    ATrace_beginSection("LibJpeg.native.decodeTotal");

    const int64_t totalStart = nowMs();

    if (!jpegData || jpegSize == 0 || targetWidth <= 0 || targetHeight <= 0) {
        LOGE(
                "decodeJpegBytesToBitmap invalid input: data=%p, size=%zu, target=%dx%d",
                jpegData,
                jpegSize,
                targetWidth,
                targetHeight
        );

        ATrace_endSection();
        return nullptr;
    }

    const int64_t exifStart = nowMs();

    ExifInfo exifInfo = parseExifInfo(jpegData, jpegSize);

    const int64_t exifEnd = nowMs();

    int rotation = orientationToRotation(exifInfo.orientation);

    LOGD(
            "decodeJpegBytes start: bufferSize=%zu, target=%dx%d, "
            "orientation=%d, rotation=%d, hasThumb=%d, parseExif=%lldms",
            jpegSize,
            targetWidth,
            targetHeight,
            exifInfo.orientation,
            rotation,
            exifInfo.hasThumbnail ? 1 : 0,
            static_cast<long long>(exifEnd - exifStart)
    );

    if (exifInfo.hasThumbnail && thumbnailPolicy != THUMBNAIL_POLICY_NEVER) {
        const unsigned char *thumbData = jpegData + exifInfo.thumbnailOffset;
        size_t thumbSize = exifInfo.thumbnailSize;

        bool useThumb = shouldUseExifThumbnail(
                thumbData,
                thumbSize,
                targetWidth,
                targetHeight,
                rotation,
                thumbnailPolicy
        );

        LOGD(
                "thumbnail check: policy=%d, useThumb=%d, thumbOffset=%zu, thumbSize=%zu",
                thumbnailPolicy,
                useThumb ? 1 : 0,
                exifInfo.thumbnailOffset,
                thumbSize
        );

        if (useThumb) {
            jobject result = decodeSelectedJpegToBitmap(
                    env,
                    thumbData,
                    thumbSize,
                    exifInfo.orientation,
                    targetWidth,
                    targetHeight,
                    reuseBitmap,
                    true
            );

            const int64_t totalEnd = nowMs();

            LOGD(
                    "decodeJpegBytes finish with thumbnail: result=%p, total=%lldms",
                    result,
                    static_cast<long long>(totalEnd - totalStart)
            );

            if (result) {
                ATrace_endSection();
                return result;
            }

            /**
             * 如果 thumbnail 解码失败,继续 fallback 原图。
             */
            LOGD("decode thumbnail failed, fallback to original jpeg");
        }
    }

    jobject result = decodeSelectedJpegToBitmap(
            env,
            jpegData,
            jpegSize,
            exifInfo.orientation,
            targetWidth,
            targetHeight,
            reuseBitmap,
            false
    );

    const int64_t totalEnd = nowMs();

    LOGD(
            "decodeJpegBytes finish with original: result=%p, total=%lldms",
            result,
            static_cast<long long>(totalEnd - totalStart)
    );

    ATrace_endSection();
    return result;
}

static jobject decodeJpegBufferToBitmap(
        JNIEnv *env,
        const std::vector<unsigned char> &jpegBuffer,
        jint targetWidth,
        jint targetHeight,
        jobject reuseBitmap,
        jint thumbnailPolicy
) {
    if (jpegBuffer.empty()) {
        return nullptr;
    }

    return decodeJpegBytesToBitmap(
            env,
            jpegBuffer.data(),
            jpegBuffer.size(),
            targetWidth,
            targetHeight,
            reuseBitmap,
            thumbnailPolicy
    );
}


extern "C"
JNIEXPORT jobject JNICALL
Java_org_fly_decoder_LibJpeg_nativeDecodePath(
        JNIEnv *env,
        jobject,
        jstring jFilePath,
        jint targetWidth,
        jint targetHeight,
        jobject reuseBitmap,
        jint thumbnailPolicy
) {
    const int64_t totalStart = nowMs();

    if (!jFilePath) {
        LOGE("nativeDecodePath failed: jFilePath is null");
        return nullptr;
    }

    const char *filePath = env->GetStringUTFChars(jFilePath, nullptr);
    if (!filePath) {
        LOGE("nativeDecodePath failed: GetStringUTFChars returned null");
        return nullptr;
    }

    const int64_t afterGetPath = nowMs();

#if LIBJPEG_USE_MMAP_FOR_PATH

    MappedFile mappedFile;

    bool ok = mapFileReadOnly(filePath, mappedFile);

    const int64_t afterMapFile = nowMs();

    env->ReleaseStringUTFChars(jFilePath, filePath);

    if (!ok || !mappedFile.data || mappedFile.size == 0) {
        LOGE("nativeDecodePath mmap failed");
        return nullptr;
    }

    jobject result = decodeJpegBytesToBitmap(
            env,
            mappedFile.data,
            mappedFile.size,
            targetWidth,
            targetHeight,
            reuseBitmap,
            thumbnailPolicy
    );

    const int64_t afterDecode = nowMs();

    LOGD(
            "nativeDecodePath mmap total: target=%dx%d, fileSize=%zu, "
            "getPath=%lldms, mapFile=%lldms, decodeBytes=%lldms, total=%lldms",
            targetWidth,
            targetHeight,
            mappedFile.size,
            static_cast<long long>(afterGetPath - totalStart),
            static_cast<long long>(afterMapFile - afterGetPath),
            static_cast<long long>(afterDecode - afterMapFile),
            static_cast<long long>(afterDecode - totalStart)
    );

    unmapFile(mappedFile);

    return result;

#else

    std::vector<unsigned char> jpegBuffer;

    bool ok = readFileToBuffer(filePath, jpegBuffer);

    const int64_t afterReadFile = nowMs();

    env->ReleaseStringUTFChars(jFilePath, filePath);

    if (!ok || jpegBuffer.empty()) {
        LOGE("nativeDecodePath open/read failed");
        return nullptr;
    }

    jobject result = decodeJpegBufferToBitmap(
            env,
            jpegBuffer,
            targetWidth,
            targetHeight,
            reuseBitmap,
            thumbnailPolicy
    );

    const int64_t afterDecode = nowMs();

    LOGD(
            "nativeDecodePath open/read total: target=%dx%d, fileSize=%zu, "
            "getPath=%lldms, readFile=%lldms, decodeBuffer=%lldms, total=%lldms",
            targetWidth,
            targetHeight,
            jpegBuffer.size(),
            static_cast<long long>(afterGetPath - totalStart),
            static_cast<long long>(afterReadFile - afterGetPath),
            static_cast<long long>(afterDecode - afterReadFile),
            static_cast<long long>(afterDecode - totalStart)
    );

    return result;

#endif
}


extern "C"
JNIEXPORT jobject JNICALL
Java_org_fly_decoder_LibJpeg_nativeDecodeFd(
        JNIEnv *env,
        jobject,
        jint fd,
        jint targetWidth,
        jint targetHeight,
        jobject reuseBitmap,
        jint thumbnailPolicy
) {
    if (fd < 0) return nullptr;

    std::vector<unsigned char> jpegBuffer;
    bool ok = readFdToBuffer(fd, jpegBuffer);

    if (!ok) return nullptr;

    return decodeJpegBufferToBitmap(
            env,
            jpegBuffer,
            targetWidth,
            targetHeight,
            reuseBitmap,
            thumbnailPolicy
    );
}

二、新建LibJpeg.kt,LibJpeg即是对上层应用的调用入口,这个就是对外暴露的Kotlin语言工具类。

Kotlin 复制代码
package org.fly.decoder

import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Trace
import java.io.File

/**
 * 基于 libjpeg-turbo 的 Android JPEG 解码封装。
 *
 * 当前能力:
 *
 * 1. 支持 filePath 解码。
 * 2. 支持 fd 解码。
 * 3. 支持 Uri 解码,适配 MediaStore / ContentProvider。
 * 4. 支持 EXIF Orientation 自动旋转。
 * 5. 支持 EXIF thumbnail fast path。
 * 6. 支持 Bitmap 复用。
 * 7. 支持 Perfetto / Systrace trace 埋点。
 *
 * 注意:
 *
 * - 仅支持 JPEG 图片。
 * - 当前工程 minSdk = 36,仅支持 Android 36+。
 * - width / height 表示最终返回 Bitmap 的目标宽高。
 * - 返回 Bitmap.Config.ARGB_8888。
 */
object LibJpeg {

    private const val THUMBNAIL_POLICY_NEVER = 0
    private const val THUMBNAIL_POLICY_AUTO = 1
    private const val THUMBNAIL_POLICY_ALWAYS = 2

    init {
        System.loadLibrary("nativejpeg")
    }

    /**
     * 解码本地 JPEG 文件。
     *
     * 这是最简单的上层调用 API:
     *
     * ```kotlin
     * val bitmap = LibJpeg.decode("/sdcard/DCIM/Camera/test.jpg", 1080, 1440)
     * ```
     *
     * 解码策略:
     *
     * - 默认会尝试 EXIF thumbnail fast path。
     * - 如果 EXIF thumbnail 足够接近目标尺寸,则优先解 EXIF thumbnail。
     * - 否则使用 libjpeg-turbo 解码原 JPEG,并通过 DCT scale 降低解码成本。
     * - 自动根据 EXIF Orientation 旋转。
     *
     * @param filePath JPEG 文件路径。
     * @param width 目标 Bitmap 宽度,必须大于 0。
     * @param height 目标 Bitmap 高度,必须大于 0。
     * @return 解码后的 ARGB_8888 Bitmap;失败时返回 null。
     */
    fun decode(
        filePath: String,
        width: Int,
        height: Int
    ): Bitmap? {
        return decode(
            filePath = filePath,
            width = width,
            height = height,
            reuseBitmap = null,
            preferExifThumbnail = true
        )
    }

    /**
     * 解码本地 JPEG 文件,支持 Bitmap 复用和 EXIF thumbnail 策略控制。
     *
     * @param filePath JPEG 文件路径。
     * @param width 目标 Bitmap 宽度。
     * @param height 目标 Bitmap 高度。
     * @param reuseBitmap 可复用 Bitmap。要求 mutable、ARGB_8888、宽高完全一致。
     * @param preferExifThumbnail 是否优先尝试 EXIF thumbnail fast path。
     * @return 解码后的 Bitmap;如果 reuseBitmap 可用,会直接写入并返回 reuseBitmap。
     */
    fun decode(
        filePath: String,
        width: Int,
        height: Int,
        reuseBitmap: Bitmap? = null,
        preferExifThumbnail: Boolean = true
    ): Bitmap? {
        if (filePath.isBlank()) return null
        if (width <= 0 || height <= 0) return null

        val file = File(filePath)
        if (!file.exists() || !file.isFile) return null

        val policy = if (preferExifThumbnail) {
            THUMBNAIL_POLICY_AUTO
        } else {
            THUMBNAIL_POLICY_NEVER
        }

        Trace.beginSection("LibJpeg.decode.path")
        return try {
            nativeDecodePath(
                filePath = filePath,
                width = width,
                height = height,
                reuseBitmap = reuseBitmap,
                thumbnailPolicy = policy
            )
        } finally {
            Trace.endSection()
        }
    }

    /**
     * 首帧极速解码。
     *
     * 该接口会强制优先使用 EXIF thumbnail。
     *
     * 适合场景:
     *
     * - 相机左下角缩略图进入图库首帧。
     * - 大图页首帧先快速显示低清图。
     * - 高清图后续再异步替换。
     *
     * 注意:
     *
     * - 如果 EXIF thumbnail 存在,即使它比目标尺寸小,也会优先使用并放大到目标宽高。
     * - 如果 EXIF thumbnail 不存在,则 fallback 到原图 DCT scale 解码。
     *
     * @param filePath JPEG 文件路径。
     * @param width 目标宽度。
     * @param height 目标高度。
     * @param reuseBitmap 可复用 Bitmap。
     */
    fun decodeFirstFrame(
        filePath: String,
        width: Int,
        height: Int,
        reuseBitmap: Bitmap? = null
    ): Bitmap? {
        if (filePath.isBlank()) return null
        if (width <= 0 || height <= 0) return null

        val file = File(filePath)
        if (!file.exists() || !file.isFile) return null

        Trace.beginSection("LibJpeg.decodeFirstFrame.path")
        return try {
            nativeDecodePath(
                filePath = filePath,
                width = width,
                height = height,
                reuseBitmap = reuseBitmap,
                thumbnailPolicy = THUMBNAIL_POLICY_ALWAYS
            )
        } finally {
            Trace.endSection()
        }
    }

    /**
     * 通过 Uri 解码 JPEG,适配 MediaStore。
     *
     * 推荐图库业务优先使用该接口,而不是直接依赖真实文件路径。
     *
     * 示例:
     *
     * ```kotlin
     * val bitmap = LibJpeg.decodeUri(context, uri, 1080, 1440)
     * ```
     *
     * 内部流程:
     *
     * - 通过 ContentResolver.openFileDescriptor(uri, "r") 获取 fd。
     * - native 层 dup(fd),因此 Kotlin 层关闭 ParcelFileDescriptor 不影响 native 读取。
     * - 支持 EXIF Orientation。
     * - 支持 EXIF thumbnail fast path。
     *
     * @param context Android Context。
     * @param uri MediaStore / ContentProvider 图片 Uri。
     * @param width 目标 Bitmap 宽度。
     * @param height 目标 Bitmap 高度。
     * @param reuseBitmap 可复用 Bitmap。
     * @param preferExifThumbnail 是否优先尝试 EXIF thumbnail。
     * @return 解码后的 Bitmap;失败返回 null。
     */
    fun decodeUri(
        context: Context,
        uri: Uri,
        width: Int,
        height: Int,
        reuseBitmap: Bitmap? = null,
        preferExifThumbnail: Boolean = true
    ): Bitmap? {
        if (width <= 0 || height <= 0) return null

        val policy = if (preferExifThumbnail) {
            THUMBNAIL_POLICY_AUTO
        } else {
            THUMBNAIL_POLICY_NEVER
        }

        Trace.beginSection("LibJpeg.decode.uri")
        return try {
            context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
                nativeDecodeFd(
                    fd = pfd.fd,
                    width = width,
                    height = height,
                    reuseBitmap = reuseBitmap,
                    thumbnailPolicy = policy
                )
            }
        } catch (_: Throwable) {
            null
        } finally {
            Trace.endSection()
        }
    }

    /**
     * 通过 Uri 做首帧极速解码。
     *
     * 会强制优先使用 EXIF thumbnail。
     *
     * @param context Android Context。
     * @param uri MediaStore / ContentProvider 图片 Uri。
     * @param width 目标宽度。
     * @param height 目标高度。
     * @param reuseBitmap 可复用 Bitmap。
     */
    fun decodeUriFirstFrame(
        context: Context,
        uri: Uri,
        width: Int,
        height: Int,
        reuseBitmap: Bitmap? = null
    ): Bitmap? {
        if (width <= 0 || height <= 0) return null

        Trace.beginSection("LibJpeg.decodeFirstFrame.uri")
        return try {
            context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
                nativeDecodeFd(
                    fd = pfd.fd,
                    width = width,
                    height = height,
                    reuseBitmap = reuseBitmap,
                    thumbnailPolicy = THUMBNAIL_POLICY_ALWAYS
                )
            }
        } catch (_: Throwable) {
            null
        } finally {
            Trace.endSection()
        }
    }

    /**
     * 通过 fd 解码 JPEG。
     *
     * 适合上层已经通过 ContentResolver 或其他方式拿到 fd 的场景。
     *
     * @param fd 可读 fd。
     * @param width 目标宽度。
     * @param height 目标高度。
     * @param reuseBitmap 可复用 Bitmap。
     * @param preferExifThumbnail 是否优先尝试 EXIF thumbnail。
     */
    fun decodeFd(
        fd: Int,
        width: Int,
        height: Int,
        reuseBitmap: Bitmap? = null,
        preferExifThumbnail: Boolean = true
    ): Bitmap? {
        if (fd < 0) return null
        if (width <= 0 || height <= 0) return null

        val policy = if (preferExifThumbnail) {
            THUMBNAIL_POLICY_AUTO
        } else {
            THUMBNAIL_POLICY_NEVER
        }

        Trace.beginSection("LibJpeg.decode.fd")
        return try {
            nativeDecodeFd(
                fd = fd,
                width = width,
                height = height,
                reuseBitmap = reuseBitmap,
                thumbnailPolicy = policy
            )
        } finally {
            Trace.endSection()
        }
    }

    private external fun nativeDecodePath(
        filePath: String,
        width: Int,
        height: Int,
        reuseBitmap: Bitmap?,
        thumbnailPolicy: Int
    ): Bitmap?

    private external fun nativeDecodeFd(
        fd: Int,
        width: Int,
        height: Int,
        reuseBitmap: Bitmap?,
        thumbnailPolicy: Int
    ): Bitmap?
}

三、配置应用模块的build.gradle文件:

Kotlin 复制代码
android {
    namespace 'org.fly.decoder'

    compileSdk 36

    defaultConfig {
        applicationId "org.fly.decoder"
        minSdk 35
        targetSdk 36
        versionCode 1
        versionName "1.0"

        ndk {
            abiFilters "arm64-v8a"
        }

        externalNativeBuild {
            cmake {
                cppFlags "-std=c++17 -O3 -fexceptions -frtti"
                arguments "-DANDROID_STL=c++_shared"
            }
        }

        
    }

    sourceSets {
        main {
            jni.srcDirs = ['src/main/jni']
        }
    }

    externalNativeBuild {
        cmake {
            path file("src/main/jni/CMakeLists.txt")
            version "3.22.1"
        }
    }

    buildTypes {
        release {
            optimization {
                enable false
            }
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_21
        targetCompatibility JavaVersion.VERSION_21
    }
    buildToolsVersion '36.0.0'
    ndkVersion '28.2.13676358'
}

一个有意思的人工智能网站

相关推荐
恋猫de小郭1 小时前
Flutter 开发怎么做 Agent ?从工程实战详细给你解读下
android·前端·flutter
安卓修改大师2 小时前
安卓修改大师实战:Activity生命周期全解析与Smali插桩教程
android
码上有光2 小时前
c++:map和set的底层实现
android·java·c++·set·map
峥嵘life2 小时前
Android GMS 开机向导横竖屏切换配置总结
android·学习
apihz2 小时前
PING 接口 PLUS 全球版 —— 免费全球多节点 Ping 检测 API教程
android
阿pin2 小时前
Android随笔-epoll机制
android·epoll
plainGeekDev2 小时前
Gradle 写法 → Kotlin DSL
android·java·kotlin
三少爷的鞋2 小时前
协程深度解析:明明只有 8 个异步任务,为何 App 线程数瞬间突破 一倍?
android
GitLqr12 小时前
Flutter 3.44 插件内置 Kotlin (KGP) 双向兼容适配指南
android·flutter·dart