PDFIUM.so在国内是编译不了的,我试过,付费购买ke学上网都不行,gclient
总会在某一处卡住、失败。
上一篇介绍了一些陈旧的资源:gyp、https://github.com/PDFium/PDFium
我决定仍然利用 gyp 来生成VS项目,供桌面平台测试用。至于代码,则从谷歌AOSP拉取。
肯定不能拉去最新的代码,最新版已经舍弃 gyp 了,只能生成ninja的脚本。不过 github 上面也有一个较新的搬运:vs_pdfium。
Google's pdfium codebase set up, by hand, as a vanilla Visual Studio solution that compiles to a static library on Windows under Visual Studio 2017.
好家伙,直接手动把偌大的、没有工程的项目装进了Visual Studio。下载下来编译看看,新版编译速度慢了两三倍不止,静态库编译出来300+MB(原先5MB),test.exe 17 MB。
还是编译旧版吧,虽然旧版没有高亮标注的头文件fpdf_annot.h
安卓 barteksc/PdfiumAndroid
项目的代码来自 android-7.1.2,这也是最后一个使用 gyp 的版本,就用它了。
设置proxy,使用git将代码克隆下来:
git clone https://android.googlesource.com/platform/external/pdfium --branch android-7.1.2_r36
利用 gyp 可以生成 vs 工程,编译很快,示例也能运行,这就够了。
接下来就是尝试将这些代码编译到安卓上面运行。PdfiumAndroid 的编译方法是将ASOP项目整个拉取下来,然后更具官方的指示进行编译。国内显然行不通,而且据说编译过程磁盘占用达到了60GB。
即使这样,这个仓库提供的一些.mk文件还是很有用的。写makefile时,可以直接include这些文件,然后改写这些.mk文件,加入recipe rules,一个个模块地把 LibPDFIUM.so 编译出来。
安卓ndk编译路线验证
我们来做一个最基本的路线验证:编译解压缩模块 Pdfium/third_party/zlib_v128 为静态库libpdfiumzlib.a
,然后写一个test.c
调用 zlib 的方法,将 test.c 编译为“第三方库”libhelloworld.so
,最后新建一个安卓jni项目调用libhelloworld.so
中的方法。
一、编译 libpdfiumzlib.a
编译环境:windows10、ndk21(android sutdio下载的windows版ndk)、在bash(win10下的ubuntu子系统)中编译。
以前ndk中自带用来交叉编译的gcc,r19以后的版本已经去除,需换用clang来编译代码。
./build.sh 设置环境变量然后 make
export ANDROID_SDK=/mnt/d/Code/NVPACK/android_sdk
export PATH=$ANDROID_SDK/platform-tools:$PATH
export PATH=$ANDROID_SDK/tools:$PATH
export NDK=/mnt/d/Code/NVPACK/android-ndk-r21b # 这是我自己下载的linux版ndk,事实证明都可以,不过一些路径要改罢了
export NDK=/mnt/d/Code/NVPACK/android_sdk/ndk/21.0.6113669
export PATH=$NDK:$PATH
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64# 自己下载的linux版ndk
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/windows-x86_64
export SYSROOT=$TOOLCHAIN/sysroot
make libpdfiumzlib
总的 third_party/makefile
BASE_PATH := $(call my-dir)
_ARCH_PX_ := aarch64
_TARGETRI_ := aarch64-linux-android
_ARCH_CC_ := $(_TARGETRI_)21-clang
CC := $(TOOLCHAIN)/bin/$(_ARCH_CC_)
AR := $(TOOLCHAIN)/bin/aarch64-linux-android-ar.exe
include pdfiumzlib.mk
include pdfiumjpeg.mk
include pdfiumopenjpeg.mk
include pdfiumbigint.mk
include pdfiumagg23.mk
include pdfiumlcms.mk
all: \
pdfiumzlib.a\
pdfiumjpeg.a\
pdfiumopenjpeg.a\
pdfiumbigint.a\
pdfiumagg23.a\
pdfiumlcms.a\
来自 barteksc/PdfiumAndroid,改写(在后面追加了构建规则)后的 pdfiumzlib.mk :
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libpdfiumzlib
LOCAL_ARM_MODE := arm
LOCAL_NDK_STL_VARIANT := gnustl_static
LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays -fexceptions
LOCAL_CFLAGS += -Wno-non-virtual-dtor -Wall
# Mask some warnings. These are benign, but we probably want to fix them
# upstream at some point.
LOCAL_CFLAGS += -Wno-shift-negative-value -Wno-unused-parameter
LOCAL_SRC_FILES := \
zlib_v128/adler32.c \
zlib_v128/compress.c \
zlib_v128/crc32.c \
zlib_v128/deflate.c \
zlib_v128/gzclose.c \
zlib_v128/gzlib.c \
zlib_v128/gzread.c \
zlib_v128/gzwrite.c \
zlib_v128/infback.c \
zlib_v128/inffast.c \
zlib_v128/inflate.c \
zlib_v128/inftrees.c \
zlib_v128/trees.c \
zlib_v128/uncompr.c \
zlib_v128/zutil.c
LOCAL_C_INCLUDES := \
external/pdfium
include $(BUILD_STATIC_LIBRARY)
# 之后的都是是我写的
OBJS_pdfiumzlib := $(addsuffix .o, $(LOCAL_SRC_FILES))
OBJS_pdfiumzlib := $(addprefix build/$(_ARCH_PX_)/pdfiumzlib/, $(OBJS_pdfiumzlib))
libpdfiumzlib.a: $(OBJS_pdfiumzlib)
$(AR) -rv libpdfiumzlib.a $(OBJS_pdfiumzlib)
# .o 文件的构建规则,改了好久呢!
build/$(_ARCH_PX_)/pdfiumzlib/%.o: %
@echo $<; set -x;\
mkdir -p $(dir $@);\
$(CC) -c -O3 $< -o $(@) -I"../" -I$(LOCAL_C_INCLUDES)
echo
核心就是 :
- 编译.c源代码为.o文件
$(CC) -c -O3 $< -o $(@) -I"../" -I$(LOCAL_C_INCLUDES)
翻译:
aarch64-linux-android21-clang -c -O3 build/aarch64/pdfiumzlib/XXX.c -o build/aarch64/pdfiumzlib/XXX.c.o -I"../" -I$(LOCAL_C_INCLUDES)
- 将许多.o打包为.a文件
$(AR) -rv libpdfiumzlib.a $(OBJS_pdfiumzlib)
翻译:
ar.exe -rv libpdfiumzlib.a build/aarch64/pdfiumzlib/XXX.c.o build/aarch64/pdfiumzlib/YYY.c.o build/aarch64/pdfiumzlib/ZZZ.cpp.o
bash ./build.sh,OK!
二、编译 libhelloworld.so
zlib测试代码来自 c语言使用zlib实现文本字符的gzip压缩与gzip解压缩
test.c :
#include "test.h"
int helloJ() { // 压缩再解压,打印字符串。
printf("helloJ!!!n");
const char *istream = "some foo汉字";
uLong srcLen = strlen(istream)+1; // +1 for the trailing `\0`
uLong destLen = compressBound(srcLen); // this is how you should estimate size
// needed for the buffer
unsigned char* ostream = (unsigned char*)malloc(destLen);
int res = compress(ostream, &destLen, (const unsigned char *)istream, srcLen);
// destLen is now the size of actuall buffer needed for compression
// you don't want to uncompress whole buffer later, just the used part
if(res == Z_BUF_ERROR){
printf("Buffer was too small!\n");
return 1;
}
if(res == Z_MEM_ERROR){
printf("Not enough memory for compression!\n");
return 2;
}
unsigned char *i2stream = ostream;
char* o2stream = (char *)malloc(srcLen);
uLong destLen2 = destLen; //destLen is the actual size of the compressed buffer
int des = uncompress((unsigned char *)o2stream, &srcLen, i2stream, destLen2);
printf("%s\n", o2stream);
return 0;
}
int main()
{
return helloJ();
}
makefile
BASE_PATH := $(call my-dir)
_ARCH_PX_ := aarch64
_TARGETRI_ := aarch64-linux-android
_ARCH_CC_ := $(_TARGETRI_)21-clang
CC := $(TOOLCHAIN)/bin/$(_ARCH_CC_)
#AR := $(TOOLCHAIN)/aarch64-linux-android/bin/ar
AR := $(TOOLCHAIN)/bin/$(_TARGETRI_)-ar.exe
APP_STL:=stlport_static
FLAGSMY=\
-arch aarch64 \
-I../pdfium/third_party/zlib_v128 \
-L../pdfium/third_party
SHARELD = -shared -Wl,-soname,libhelloworld.so
all:
$(CC) test.c \
-lpdfiumzlib \
-lpdfiumfxcrt \
-L../pdfium/third_party \
-L../pdfium/core \
-o libhelloworld.so $(FLAGSMY) $(SHARELD)
( pdfiumzlib 依赖于 pdfiumfxcrt,后者是个内存管理模块。 )
注意安卓编译动态库需要加上-shared -Wl,-soname,libhelloworld.so
这样的链接选项。如果没有-Wl,-soname,…
,则不能正常加载。
三、JNI测试
Android Studio 中,新建一个项目。在 main/java 旁边新建一个 main/jni 文件夹。结构如下:
main
-----java
-----jni
----------source
---------------com_knziha_jni_helloworld.h
---------------jniMain.cpp
----------lib / arm64-v8a / libhelloworld.so
----------Android.mk
----------Application.mk
jni的源文件就两个:
- com_knziha_jni_helloworld.h:
#include <jni.h>
/* Header for class com_knziha_jni_helloworld */
#ifndef _Included_com_knziha_jni_helloworld
#define _Included_com_knziha_jni_helloworld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_knziha_jni_helloworld
* Method: testZlib
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_knziha_jni_helloworld_testZlib
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
这个头文件是用 javah 生成的:
- 先写好Java层
helloworld.java
package com.knziha.jni;
public class helloworld {
public static native String testZlib();
public static boolean iniTestZlib() {
System.loadLibrary("testZlib");
return true;
}
static {
iniTestZlib();
}
}
- 然后 make project,在 terminal ,main/java目录中打入:
javah com.knziha.jni.helloworld
- jniMain.cpp:
#include "com_knziha_jni_helloworld.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "zlib.h"
#include "test.h"
JNIEXPORT jstring JNICALL Java_com_knziha_jni_helloworld_testZlib
(JNIEnv *env, jclass jobj) {
FILE* out = freopen("/storage/emulated/0/Android/data/com.knziha.helloj/log.txt","w",stdout);
helloJ(); //调用"第三方库"中的方法。
fflush(out);
return env->NewStringUTF("testZlib!!!");
}
( 由于native代码中无法打印,调用 freopen 将 stdout 输出到日志文件。 )
写完两个 native 文件后,需要准备两个mk文件,以下是参考了PdfiumAndroid写的:
- android.mk:
LOCAL_PATH := $(call my-dir)
#Prebuilt libraries
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld
ARCH_PATH = $(TARGET_ARCH_ABI)
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/$(ARCH_PATH)/libhelloworld.so
include $(PREBUILT_SHARED_LIBRARY)
#Main JNI library
include $(CLEAR_VARS)
LOCAL_MODULE := testZlib
LOCAL_CFLAGS += -DHAVE_PTHREADS
LOCAL_C_INCLUDES += .
LOCAL_C_INCLUDES += ../../../../../../pdfium/third_party/zlib_v128
LOCAL_C_INCLUDES += ../../../../../../hello_zlib_so
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_SHARED_LIBRARIES += helloworld
LOCAL_LDLIBS += -llog -landroid -ljnigraphics
LOCAL_SRC_FILES := $(LOCAL_PATH)/src/jniMain.cpp
include $(BUILD_SHARED_LIBRARY)
其中定义了 jni 主模块 testZlib,依赖于(先前准备好的)预编译模块 helloworld。
- application.mk:
APP_STL := c++_shared
APP_CPPFLAGS += -fexceptions
#For ANativeWindow support
APP_PLATFORM = android-21
APP_ABI := armeabi-v7a
APP_ABI := arm64-v8a
最后在 jni 目录调用 ndk-build 即可,ndk会将库文件复制到 main / libs,需要在 app 的 gradle 中写明 jniLibs 目录:
android {
defaultConfig {
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
sourceSets{
main {
jni.srcDirs = []
jniLibs.srcDir 'src/main/libs'
}
}
}
成功运行后就可以在手机上看到日志输出了: