携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
一、前言
该文章是我之前发布于其他平台的博文,因为掘友们可能会需要,所以搬运到掘金。
最近项目中有需要压缩 GIF 的需求,最开始时试图使用 FFmpeg 通过降低 GIF 的分辨率和帧率的来减少 GIF 文件体积,但实际测试下来,大多数情况下压缩效果并不理想,甚至会出现降低分辨率后导出的 GIF 甚至比原文件还大的情况。
故选择放弃 FFmpeg ,经过大量的查询资料,发现如果想要压缩 GIF 大致有以下几个途径:
1.由于 GIF 支持全局调色盘和局部调色盘,在没有局部调色盘的时候会用放在文件头中的全局调色盘。所以对于颜色变化不大的 GIF,可以将颜色放入全局调色盘中,去除局部调色盘。
2.对于颜色较少的 GIF,将调色盘大小减少,比如从 256 种减少到 128 种等。
3.对于背景一致,画面中有一部分元素在变化的 GIF,可以将多个元素和背景分开存储,然后加上如何还原的信息
4.对于背景一致,画面中有一部分元素在动的 GIF,可以和前面一帧比较,将不动的部分透明化
5.对于帧数很多的 GIF,可以抽取中间部分的帧,减少帧数
6.对于每帧分辨率很高的 GIF,将每帧的分辨率减小
正如前文所述,抽帧(减小帧率)和减少分辨率有时候效果并不是很好,而且对于图片的质量损耗较大。
对于1,2条途径依然可以使用 FFmpeg 实现,但是效果也不理想,并且处理起来比较复杂。
故剩余的处理方式只剩3和4了。
但是正如上文作者所说:“在移动端,除非将 ImageMagick 或者 gifsicle 移植到 iOS&Android 上,要实现前面 4 个方法是比较困难的。”
作者提到了两个程序: ImageMagick 和 gifsicle ,经过搜索 ImageMagick 已支持安卓, gifsicle 尚未支持。但是 ImageMagick 目前对于安卓的支持较差,限制较多:
Requires API >= 24 (>= Nougat)
Currently, only arm64-v8a is supported
并且其库过于庞大:
对于我来说只需要它的压缩动图功能,却需要添加这么大的库,性价比过低。
不过如果读者有需要的可以试试,项目地址:ImageMagick
也就是说,现在对于我来说只剩下 gifsicle 可用,但是 gifsicle 尚未提供安卓可用版本。
gifsicle 项目地址:gifsicle
如何编译 gifsicle 使其在安卓上可用便是本文想要探讨的问题。
二、gifsicle编译方案
原计划是在 gifsicle 之上使用 jni 封装使其可以在安卓中调用其接口,但是通过对 gifsicle 的源码以及 issus 研究,发现该项目只支持直接编译成可执行文件,且修改较为困难。
这也是为什么至今没有移植到安卓上的原因。
github中关于将gifsicle移植成库的讨论
虽说不能直接移植成安卓的 .so 库,但是即使是编译成可执行文件也可以通过
Runtime.getRuntime().exec(cmd, envp)
使用该库,唯一需要注意的是,在安卓10中可能会禁止执行外部可执行库:
Android 10 includes the following security changes. Removed execute permission for app home directory Untrusted apps that target Android 10 cannot invoke exec() on files within the app's home directory. This execution of files from the writable app home directory is a W^X violation. Apps should load only the binary code that's embedded within an app's APK file. In addition, apps that target Android 10 cannot in-memory modify executable code from files which have been opened with dlopen(). This includes any shared object (.so) files with text relocations.
不过经过实际测试,只要是将其打包进 apk 中且命名形如 libxxx.so 的可执行文件依旧可以使用。
确定好如何使用后下面就开始编译
三、开始编译
1.编译前准备
参考:
1.编译环境
我使用的是 WSL2 Linux + NDK r21b
Linux版本如下(因为电脑上正好装着Kali所以就用Kali了,一般用Ubuntu就行)
2.安装依赖
因为编译前需要 automake 生成 config.h 所以需要安装以下依赖(已安装请忽略)
sudo apt-get install autoconf automake libtool
sudo apt-get install libffi-dev
2.下载代码
编译前首先将 gifsicle 下载下来
这里直接 clone 官方仓库:
git clone https://github.com/kohler/gifsicle.git
切进代码目录
cd gifsicle
3.生成 config.h
依据官方文档,首先生成 config
autoreconf -i
根据需要执行 configure ,因为我只需要压缩gif功能,所以其他模块就不需要编译了:
./configure --disable-gifview --disable-gifdiff
此时目录中应该已经生成了一个 config.h 文件。
切记不要执行 make 和 install
4.编写 Android.mk 文件
在代码根目录中新建一个 Android.mk 文件,内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := gifsicle
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_SRC_FILES := \
src/clp.c \
src/fmalloc.c \
src/giffunc.c \
src/gifread.c \
src/gifsicle.c \
src/gifunopt.c \
src/gifwrite.c \
src/merge.c \
src/optimize.c \
src/quantize.c \
src/support.c \
src/xform.c
LOCAL_CFLAGS := -DHAVE_CONFIG_H
include $(BUILD_EXECUTABLE)
再新建一个 Application.mk 文件,内容如下:
APP_ABI := all
APP_PLATFORM := android-16
NDK_TOOLCHAIN_VERSION := clang
其他东西不需理会,记住 APP_ABI := all 表示需要编译的CPU ABI 版本即可,这里写的是所有版本都编译一份,你也可以指定只编译特定的版本,如 APP_ABI := armeabi-v7a
5.准备编译
将代码目录更名为 jni
否则ndk不会将该项目识别为ndk项目并且编译。
做完上述步骤后的完整目录结构如下:
2.开始编译
1.首先确保你的ndk已经安装并且已配置环境,否则将会
-bash: ndk-build: command not found
安装ndk并配置环境可参考:
VMware安装Ubuntu教程,Linux下搭建Android开发环境 中关于NDK的介绍(AS,SDK等不需要装)
2.切换至 jni 文件夹的上层目录:
cd ../
如图:
3.编译64位的so(arm64-v8a, x86_64)
首先修改 Application.mk 文件的 APP_ABI 为 arm64-v8a, x86_64
vim ./jni/Application.mk
修改后如图:
修改后执行:
ndk-build
如图即为编译成功:
生成的so库在 ./libs 目录下
4.编译32位so(如:armeabi-v7a)
如3中所说,首先修改 Application.mk 文件的 APP_ABI 为 armeabi-v7a
修改 ./jni/config.h 文件
vim ./jni/config.h
找到
#define SIZEOF_UNSIGNED_LONG 8
修改为
#define SIZEOF_UNSIGNED_LONG 4
如图:
切记一定要修改不然会报错如下:
之后执行
ndk-build
即可。
注意: 上述编译32位时如果不修改 ./jni/config.h 会出现报错的情况,根据我猜测应该时由于我使用的是 Linux 的 automake 工具生成的 config.h 文件,也就是说这个config.h 文件是适用于编译成我使用 automake 的系统(kali)的配置文件,而非用于我生成安卓可执行文件的配置文件。
根据报错信息找到源码出错地方如下:
根据源码中注释的描述,此处代码是用于检测 Windos 下编译脚本是否出错的问题(即是否在 64位 系统使用了 32位 脚本进行编译或者反之)
根据源码来说,此处并未有除检测外实际作用,故我直接根据指示将 config.h 中的 UNSIGNED_LONG 值设置为4来规避检测。
虽然样能够编译成功,初步测试使用也没有问题,但是我也不确定是否会影响到 gifsicle 其他功能的使用。
所以读者如有需要使用,还需自行测试是否存在问题,如果有人知道其中的问题也希望能不吝赐教,十分感谢。
四、使用方法
1.复制库
将编译成功的库复制到您的安卓项目文件项目根目录\app\src\main\jniLibs
下,并且改名为 libgifsicle.so:
一定要记得改名,否则安装时不会被系统复制至可执行文件目录下。
2.使用
代码中已用注释说明各个语句的作用。
val gifsicle = File(File(applicationInfo.nativeLibraryDir), "libgifsicle.so") //可执行文件地址安装后形如:/data/app/com.equationl.myapplication-wZxpZo7IgVPNv3jvY0S8QA==/lib/arm/libgifsicle.so
if (!gifsicle.canExecute()) { //无法执行该执行文件
Log.e("el", "startCustomizeCompress: can't excute")
}
val envp = arrayOf("LD_LIBRARY_PATH=" + File(applicationInfo.nativeLibraryDir)) //设置环境
val cmd = String.format(Locale.US, "%s -i %s -k 256 -O3 -o %s",
gifsicle.path, File(externalCacheDir, "test.gif").toString(), File(externalCacheDir, "result.gif").toString()) //设置命令,此处作用为将缓存目录下的 test.gif 更改颜色数为256 按第3级别优化并输出至缓存目录下 result.gif (详细请自己看 gifsicle 的文档)
Log.i("el", "startCustomizeCompress: envp=${envp[0]}\ncmd=$cmd")
val process = Runtime.getRuntime().exec(cmd, envp) //开始执行命令
try {
if (process.waitFor() != 0) { //如果执行成功会返回 0,不成功返回非0
Log.e("el", "startCustomizeCompress: running error process.waitFor() != 0")
}
else {
Log.i("el", "Success!")
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
关于 gifsicle 的使用方法请自行查看官方文档