在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_915106321 小时前
iOS混淆工具实战 金融支付类 App 的安全防护与合规落地
android·ios·小程序·https·uni-app·iphone·webview
alexhilton3 小时前
运行时着色器实战:实现元球(Metaballs)动效
android·kotlin·android jetpack
從南走到北3 小时前
JAVA国际版东郊到家同城按摩服务美容美发私教到店服务系统源码支持Android+IOS+H5
android·java·开发语言·ios·微信·微信小程序·小程序
观熵5 小时前
Android 相机系统全景架构图解
android·数码相机·架构·camera·影像
雨白8 小时前
Android 自定义 View:彻底搞懂 Xfermode 与官方文档陷阱
android
_小马快跑_9 小时前
从VSync心跳到SurfaceFlinger合成:拆解 Choreographer与Display刷新流程
android
_小马快跑_9 小时前
Android | 视图渲染:从invalidate()到屏幕刷新的链路解析
android
Monkey-旭11 小时前
Android 定位技术全解析:从基础实现到精准优化
android·java·kotlin·地图·定位
树獭非懒12 小时前
Android 媒体篇|吃透 MediaSession 与 MediaController
android·架构