在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](https://www.deepseek.com/) \[2\] [Android Developer API reference](https://developer.android.google.cn/reference/android/graphics/Bitmap#compress) \[3\] [libpng](https://www.libpng.org/pub/png/libpng.html)

相关推荐
2501_929157682 小时前
Switch 20.5.0系统最新PSP模拟器懒人包
android·游戏·ios·pdf
用户093 小时前
Kotlin Flow的6个必知高阶技巧
android·面试·kotlin
用户093 小时前
Flutter插件与包的本质差异
android·flutter·面试
用户093 小时前
Jetpack Compose静态与动态CompositionLocal深度解析
android·面试·kotlin
聆风吟º6 小时前
【Spring Boot 报错已解决】别让端口配置卡壳!Spring Boot “Binding to target failed” 报错解决思路
android·java·spring boot
非专业程序员Ping14 小时前
HarfBuzz概览
android·ios·swift·font
Jeled15 小时前
「高级 Android 架构师成长路线」的第 1 阶段 —— 强化体系与架构思维(Clean Architecture 实战)
android·kotlin·android studio·1024程序员节
明道源码17 小时前
Kotlin 控制流、函数、Lambda、高阶函数
android·开发语言·kotlin
消失的旧时光-194319 小时前
Kotlin × Gson:为什么遍历 JsonObject 要用 entrySet()
android·kotlin·数据处理·1024程序员节
G果20 小时前
安卓APP页面之间传参(Android studio 开发)
android·java·android studio