在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在同级目录下,如:
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}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优化
- 将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的大小,是否需要使用需要根据项目实际情况来评估。
参考
[1] deepseek
[2] Android Developer API reference
[3] libpng
在Android中使用libpng的更多相关文章
- Android 中图片压缩分析(上)
作者: shawnzhao,QQ音乐技术团队一员 一.前言 在 Android 中进行图片压缩是非常常见的开发场景,主要的压缩方法有两种:其一是质量压缩,其二是下采样压缩. 前者是在不改变图片尺寸的情 ...
- Android中的LinearLayout布局
LinearLayout : 线性布局 在一般情况下,当有很多控件需要在一个界面列出来时,我们就可以使用线性布局(LinearLayout)了, 线性布局是按照垂直方向(vertical)或水平方向 ...
- Android中BroadcastReceiver的两种注册方式(静态和动态)详解
今天我们一起来探讨下安卓中BroadcastReceiver组件以及详细分析下它的两种注册方式. BroadcastReceiver也就是"广播接收者"的意思,顾名思义,它就是用来 ...
- Android中使用ExpandableListView实现微信通讯录界面(完善仿微信APP)
之前的博文<Android中使用ExpandableListView实现好友分组>我简单介绍了使用ExpandableListView实现简单的好友分组功能,今天我们针对之前的所做的仿微信 ...
- Android中ListView实现图文并列并且自定义分割线(完善仿微信APP)
昨天的(今天凌晨)的博文<Android中Fragment和ViewPager那点事儿>中,我们通过使用Fragment和ViewPager模仿实现了微信的布局框架.今天我们来通过使用Li ...
- Android中Fragment和ViewPager那点事儿(仿微信APP)
在之前的博文<Android中使用ViewPager实现屏幕页面切换和引导页效果实现>和<Android中Fragment的两种创建方式>以及<Android中Fragm ...
- Android中Fragment与Activity之间的交互(两种实现方式)
(未给Fragment的布局设置BackGound) 之前关于Android中Fragment的概念以及创建方式,我专门写了一篇博文<Android中Fragment的两种创建方式>,就如 ...
- 【月入41万】Mono For Android中使用百度地图SDK
借助于Mono For Android技术,.Net开发者也可以使用自己熟悉的C#语言以及.Net来开发Android应用.由于Mono For Android把Android SDK中绝大部分类库都 ...
- mono for android中使用dapper或petapoco对sqlite进行数据操作
在mono for android中使用dapper或petapoco,很简单,新建android 类库项目,直接把原来的文件复制过来,对Connection连接报错部分进行注释和修改就可以运行了.( ...
- Android开发学习之路-Android中使用RxJava
RxJava的核心内容很简单,就是进行异步操作.类似于Handler和AsyncTask的功能,但是在代码结构上不同. RxJava使用了观察者模式和建造者模式中的链式调用(类似于C#的LINQ). ...
随机推荐
- buck参数工具分享
工具链接:https://cnblogs-img.oss-cn-hangzhou.aliyuncs.com/docs/buck.xls
- 【中文】【吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第二周作业
[吴恩达课后编程作业]Course 1 - 神经网络和深度学习 - 第二周作业 - 具有神经网络思维的Logistic回归 上一篇:[课程1 - 第二周测验]※※※※※ [回到目录]※※※※※下一篇: ...
- SwanLab入门深度学习:Qwen3大模型指令微调
一.概述 Qwen3是通义千问团队的开源大语言模型,由阿里云通义实验室研发.以Qwen2作为基座大模型,通过指令微调的方式实现高准确率的文本分类,是学习大语言模型微调的入门任务. 指令微调是一种通过在 ...
- ArrayList与LinkedList的增删改查
ArrayList: 1 package com.lv.study.am.first; 2 3 //ArrayList 有下标 可重复 有序(添加到集合里面的顺序)不等于排序 4 5 6 import ...
- HarmonyOS运动开发:打造便捷的静态快捷菜单
鸿蒙核心技术##运动开发# 前言 在运动类应用中,用户往往需要快速访问常用功能,如查看成绩.赛事信息或开始运动.为了提升用户体验,鸿蒙(HarmonyOS)提供了静态快捷菜单功能,允许用户从桌面直接跳 ...
- 如何从Docker image提取 Dockerfile
参考链接:https://github.com/cucker0/dockerimage2df 参考链接:https://github.com/cucker0/docker/blob/main/md/由 ...
- linux 配置定时任务
注意:定时任务执行默认路径,我们配置的命令如kubectl要配置绝对路径/usr/local/bin/kubectl,或者在脚本中全局定义PATH 配置说明 linux 配置定时任务的方式比较多,可以 ...
- Ubuntu二进制安装ElasticSearch7.17.x版本集群
概述 本文主要讲解如何二进制安装Linux二进制集群 环境信息 主机名 IP地址 系统 ELK01 10.0.0.40 Ubuntu22.04 ELK02 10.0.0.41 Ubuntu22.04 ...
- 数栈技术分享:详解FlinkX中的断点续传和实时采集
数栈是云原生-站式数据中台PaaS,我们在github和gitee上有一个有趣的开源项目:FlinkX,FlinkX是一个基于Flink的批流统一的数据同步工具,既可以采集静态的数据,也可以采集实时变 ...
- DRF案例
1 反序列化更新,instance 就传要修改的对象,保证修改完成 def update(self, instance, validated_data): publish_id = validated ...