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'
}

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

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui