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