在Android中使用libpng

最近在使用Android的Bitmap.compress方法保存4K png图片时,发现其耗时在1秒钟以上,通过询问deepseek得知相比Bitmap.compress,使用libpng提升png图片的保存速度。接下来本文将阐述在Android中如何集成libpng,以及在使用过程中遇到的问题和最终的对比测试结果。

编译libpng

使用AndroidStudio创建Native C++项目或者Android Native Library模块,然后将下载libpng解压到对应的src/main/cpp目录下,与CMakeLists.txt在同级目录下,如:

shell 复制代码
src/main/cpp
├── CMakeLists.txt
├── libpng

在libpng的libpng16分支中已经提供了CMakeLists.txt文件,因此在Android的CMakeLists.txt中添加子路径:

cmake 复制代码
add_subdirectory(libpng)

同时添加头文件路径:

cmake 复制代码
include_directories(libpng)

build后就可以在build/intermediates/cxx目录下找到编译出来的libpng16.so文件。

接下来在kotlin文件中添加保存png图片的接口:

kotlin 复制代码
class PNG {
    companion object {
        init {
            System.loadLibrary("png-jni")
        }
        external fun save(bitmap: Bitmap, filepath: String): Boolean
    }
}

在c/c++文件中添加native实现:

cpp 复制代码
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_ihuntto_libpng_PNG_00024Companion_save(JNIEnv *env, jobject thiz, jobject bitmap,
                                                jstring file_path) {
    const char *path = env->GetStringUTFChars(file_path, nullptr);
    if (path == nullptr) {
        return JNI_FALSE;
    }

    // 获取 Bitmap 信息
    AndroidBitmapInfo info;
    if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
        env->ReleaseStringUTFChars(file_path, path);
        return JNI_FALSE;
    }

    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
        // 需要 RGBA_8888 格式
        env->ReleaseStringUTFChars(file_path, path);
        return JNI_FALSE;
    }

    // 锁定 Bitmap 像素
    void *pixels;
    if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
        env->ReleaseStringUTFChars(file_path, path);
        return JNI_FALSE;
    }

    FILE *fp = fopen(path, "wb");
    if (!fp) {
        AndroidBitmap_unlockPixels(env, bitmap);
        env->ReleaseStringUTFChars(file_path, path);
        return JNI_FALSE;
    }

    png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    if (!png) {
        fclose(fp);
        AndroidBitmap_unlockPixels(env, bitmap);
        env->ReleaseStringUTFChars(file_path, path);
        return JNI_FALSE;
    }

    png_infop info_ptr = png_create_info_struct(png);
    if (!info_ptr) {
        png_destroy_write_struct(&png, nullptr);
        fclose(fp);
        AndroidBitmap_unlockPixels(env, bitmap);
        env->ReleaseStringUTFChars(file_path, path);
        return JNI_FALSE;
    }

    if (setjmp(png_jmpbuf(png))) {
        png_destroy_write_struct(&png, &info_ptr);
        fclose(fp);
        AndroidBitmap_unlockPixels(env, bitmap);
        env->ReleaseStringUTFChars(file_path, path);
        return JNI_FALSE;
    }

    png_init_io(png, fp);

    // 设置 PNG 头信息
    int color_type = PNG_COLOR_TYPE_RGBA;
    png_set_IHDR(png, info_ptr, info.width, info.height, 8, color_type,
                 PNG_INTERLACE_NONE,
                 PNG_COMPRESSION_TYPE_BASE,
                 PNG_FILTER_TYPE_BASE);

    png_write_info(png, info_ptr);

    // 写入图像数据
    png_bytep *row_pointers = new png_bytep[info.height];
    for (int y = 0; y < info.height; y++) {
        row_pointers[y] = static_cast<png_bytep>(pixels) + y * info.stride;
    }

    png_write_image(png, row_pointers);
    png_write_end(png, nullptr);

    // 清理资源
    delete[] row_pointers;
    png_destroy_write_struct(&png, &info_ptr);
    fclose(fp);
    AndroidBitmap_unlockPixels(env, bitmap);
    env->ReleaseStringUTFChars(file_path, path);

    return JNI_TRUE;
}

上述实现代码由deepseek提供

最后需要在CMakeLists.txt链接libpng库:

cmake 复制代码
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        png_shared
        jnigraphics
        log)

注意libpng的链接目标是png_shared,而不是png或png16,因为libpng的CMakeLists.txt中编译的库目标名称为png_shared,输出库文件名称为libpng16.so,因此不要链接错了,否则会编译报错。

现在就可以通过PNG.save()完成libpng的图片保存目标了。

对比测试

为了对比Bitmap.compress和libpng,增加一段对比测试代码:

kotlin 复制代码
//omit other code
GlobalScope.launch(Dispatchers.IO) {
    var time = System.currentTimeMillis()
    val bitmap = createColorNoiseBitmap(binding.root.width, binding.root.height)
    val sb = StringBuilder()
    sb.append("create bitmap used ${System.currentTimeMillis() - time}ms\n")
    time = System.currentTimeMillis()
    externalCacheDir?.absolutePath?.let { cacheDir ->
        try {
            BufferedOutputStream(FileOutputStream(cacheDir + File.separatorChar + "noise1.png")).use {
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
            }
            sb.append("bitmap compress used ${System.currentTimeMillis() - time}ms\n")
            time = System.currentTimeMillis()
            PNG.save(bitmap, cacheDir + File.separatorChar + "noise0.png")
            sb.append("libpng save used ${System.currentTimeMillis() - time}ms\n")
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    //omit other code
}
序号 图片分辨率 Bitmap.compress libpng
1 1080*2253 8.46MB/323ms 8.46MB/563ms

Build Variants为debug模式时,libpng的速度比Bitmap.compress的速度要慢,上表只列出了一次测试结果,多次测试后也是libpng的速度慢,但两者保存的图片大小是一致的。接下来看看是否能提升一下libpng的保存速度。

libpng优化

  1. Build Variants改为release模式。
序号\耗时(ms) Bitmap.compress libpng
1 318 303
2 310 301
3 318 288
4 299 292
5 317 282

测试的图片分辨率都是1080*2253,不再单独列出。

现在libpng的速度已经快于Bitmap.compress,但相差不大。

  1. 设置libpng速度优先:
cpp 复制代码
    // 1. 设置最快的压缩级别
    png_set_compression_level(png, Z_BEST_SPEED);
    // 2. 禁用所有过滤器(最快)
    png_set_filter(png, PNG_FILTER_TYPE_BASE, PNG_FILTER_NONE);
    // 3. 设置压缩策略为最快
    png_set_compression_strategy(png, Z_DEFAULT_STRATEGY);
序号\耗时(ms) Bitmap.compress libpng
1 324 233
2 327 175
3 314 210
4 303 205
5 306 211

此时libpng已经明显快于Bitmap.compress了,耗时约为Bitmap.compress的三分之二。

  1. 开启png硬件优化

在CMakeLists.txt中添加:

cmake 复制代码
set(PNG_HARDWARE_OPTIMIZATIONS ON)

不过这个是默认开启的,添加后实际无差异。

  1. 其他编译优化

在CMakeLists.txt中添加:

cmake 复制代码
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -ffast-math -fno-rtti -fno-exceptions")

set(PNG_STATIC OFF) # 不编译静态库
set(PNG_TESTS OFF) # 不编译测试程序

第一个是设置Release的编译优化,经过实际测试几乎无优化;后两个主要可以提升编译速度。

目前从测试结果来看,libpng相比于Android自带的Bitmap.compress带来的速度提升有限,并且还会增加apk的大小,是否需要使用需要根据项目实际情况来评估。

参考

1 deepseek

2 Android Developer API reference

3 libpng

相关推荐
松仔log5 小时前
Java中级——组合和继承
android·java·开发语言
我命由我123458 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian2339 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759629 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马10 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone10 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone10 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子13 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker14 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone14 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常