最近在使用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在同级目录下,如:
src/main/cpp ├── CMakeLists.txt ├── libpng
在libpng的libpng16分支中已经提供了CMakeLists.txt文件,因此在Android的CMakeLists.txt中添加子路径:
add_subdirectory(libpng)
同时添加头文件路径:
include_directories(libpng)
build后就可以在build/intermediates/cxx目录下找到编译出来的libpng16.so文件。
接下来在kotlin文件中添加保存png图片的接口:
class PNG { companion object { init { System.loadLibrary("png-jni") } external fun save(bitmap: Bitmap, filepath: String): Boolean } }
在c/c++文件中添加native实现:
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库:
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,增加一段对比测试代码:
//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}msn") 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}msn") time = System.currentTimeMillis() PNG.save(bitmap, cacheDir + File.separatorChar + "noise0.png") sb.append("libpng save used ${System.currentTimeMillis() - time}msn") } 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优化
- 将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,但相差不大。
- 设置libpng速度优先:
// 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的三分之二。
- 开启png硬件优化
在CMakeLists.txt中添加:
set(PNG_HARDWARE_OPTIMIZATIONS ON)
不过这个是默认开启的,添加后实际无差异。
- 其他编译优化
在CMakeLists.txt中添加:
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的大小,是否需要使用需要根据项目实际情况来评估。
完整项目代码已上传Github。
参考
[1] deepseek
[2] Android Developer API reference
[3] libpng