「Skia」1. 使用CMake交叉编译Skia

什么是Skia

  Skia是一个高性能的跨平台2D图形库,由Google开源并维护。Skia能够对字体、坐标转换、点阵图、矢量图以及矢量动画等进行高效的处理,代码结构和接口异常简洁,并且支持OpenGL、Vulkan、甚至OpenCL等硬件加速特性,是一个理想的2D图形库。

skia

  Skia开始是一个初创公司的项目,于2005年被Google收购,往后一直保持低调,直到2007年Google发布了知名的Android系统,Skia才在图形图像领域逐渐被人们所熟知。Android的UI绘制底层采用了Skia图形库,随着Skia的发展壮大,越来越多的平台开始采用Skia作为底层的图形库,比如Flutter、Chrome、Fuchsia等。由于优秀的跨平台特性,Skia也可以被应用于Mac OS、Windows和Linux。
  Skia如此优秀,将其集成到我们的应用当中是一件收益极高的事情,Skia的诸多优势,让我们没有理由拒绝它。
  1. 针对音视频应用,Skia的跨平台特性,使得我们的应用能够在各平台(比如IOS、Android等)使用同一套图形引擎以及图片编解码器。
  2. Skia效率很高,并且支持GPU加速,相比我们自己重写一套图形引擎,Skia的优势不言而喻。
  3. Skia架构简洁,代码成熟,已经经受过了被各大项目的考验,极其稳定。
  4. 使用OpenGL绘制文字是多媒体技术从业者心中永远的痛,Skia可以解决这一问题。
  5. Skia使用BSD协议进行开源,基本意味着我们可以为所欲为

NDK交叉编译Skia

  本文以Android平台的编译为例,其它平台的流程是一致的。
  首先我们从Skia官网下载源码。除了Skia的本体,官方还提供了一个python脚本来下载全部第三方的依赖,比如libjpeg-turbo、libpng等,建议提前安装好python。

#克隆Skia仓库
git clone https://skia.googlesource.com/skia.git
cd skia
#下载所有Skia依赖的第三方源码
python2 tools/git-sync-deps

  整个仓库比较大,包含所有第三方依赖后,其大小达到4GB左右,仓库虽大,但相对于其它大型项目,Skia的代码量是足够小的。实际上交叉编译后的so只有7M左右,并且还有极大的精简空间。
  接着按照官方指引,使用ninja进行编译。注意,最新的Skia源码是基于c++17的,这意味着我们的ndk版本必须大于或等于r17c。

#配置编译环境,类似于Configure
bin/gn gen out/arm   --args='ndk="/tmp/ndk" target_cpu="arm"'
#执行编译
ninja -C out/arm

  如果提示ninja: command not found,你需要自行安装ninja,建议在Linux或者Mac OS下进行编译,避免不必要的坑。

#Mac
sudo brew install ninja-build
#Ubuntu
sudo apt install ninja-build

  经过漫长的等待,结果编译失败,各种报错,比如找不到指定的符号等。可能是我对ninja不够熟悉,摸索了很久依然没能解决编译问题。Terminal上大量的红色字符不断打击着我的自信心,哪怕我成功编译了Skia,也只是拿到了一个可以应用到项目中的共享库而已,我们依然没办法把Skia全部源码通过IDE导入到我们的工程中,体验阅读代码的便利,随心所欲地修改编译,因为各大IDE并不直接支持ninja,要是能够使用我们熟悉的CMake进行编译就好了。
  于是我着手编写CMake编译脚本,Skia本身的代码并不多,但是其依赖的第三方源码却极其庞大,整个CMake脚本的编写过程异常痛苦。即使我成功把数量众多的源码用CMake组织起来,但是面对跨平台编译的脚本处理,也足够我吃一壶。难道还是必须使用ninja进行编译吗?正当我心灰意冷之际,惊喜的发现在官方编译指南的底部角落里,赫然写着Skia支持CMake编译!

CMake交叉编译Skia

  阅读指南发现,Skia并不直接支持CMake编译,而是通过把ninja的gn编译脚本转换成CMake,我们通过下面的命令便可以直接生成CMake脚本。注意,命令里的cmake是CMake脚本和中间文件的保存目录,你也可以改为其它目录。

bin/gn gen cmake --args='is_debug=false ndk="/tmp/ndk"' --ide=json --json-ide-script=../../gn/gn_to_cmake.py

  命令执行成功后,在./skia/cmake目录下生成了两个CMake脚本,其中CMakeLists.txt只是工程CMake的入口,CMakeLists.ext才是本体。通过阅读脚本我发现,Skia并不只是纯粹的使用CMake进行编译,中间还是会使用到ninja,所以cmake目录下的各种gn文件都是必要的,我们并不能简单通过这两个CMake文件就能完成Skia的编译。

./skia/cmake +
        + CMakeLists.txt
        + CMakeLists.ext
        +  ...

  有了CMake之后,我们便可以把Skia源码导入到我们的工程了。Android开发使用的是Android Studio,简单新建一个Demo Project,开启cmake支持,把上面生成的CMakeLists.txt引入到我们的Demo,执行Refresh Linked C++ Projects,接着Build

如果这个过程不知道如何操作,你需要了解一下cmake的使用,也可以参考Skia Demo

  1. Android Studio毫无悬念的报了以下错误。在经历了多次失败之后,这次我的内心显得异常平静,下面开始见招拆招吧。

./skia/third_party/externals/libjpeg-turbo/simd/arm64/jsimd_neon.S:201:20: error: register name expected
...

  查看报错位置,jsimd_neon.S是libjpeg-turbo源码跟neon指令相关的代码,用于使用arm扩展指令集进行加速。这类源码通常和CPU架构强相关,比如在libjpeg-turbo/simd目录下会同时有arm和arm64两个目录,分别对应arm的32位和64位架构。
  这里我编译的目标架构是arm32,错误信息却显示我使用了arm64位的代码。打开CMakeLists.ext脚本,找到jsimd_neon.S被引入的地方,果不其然,写的就是./skia/third_party/externals/libjpeg-turbo/simd/arm64/jsimd_neon.S。实际上这是因为我上面运行的gn转cmake命令没有加target_cpu="arm"造成的,重新运行一下命令,就可以解决这个问题。

bin/gn gen cmake --args='is_debug=false ndk="/tmp/ndk" target_cpu="arm"' --ide=json --json-ide-script=../../gn/gn_to_cmake.py

  但是我并不推荐这么做,因为通常我们同时需要arm的32和64位两个架构,以上也只是解决了arm32的编译问题,如果我们要编译arm64位的应用,依然会碰到这个问题。
  libjpeg-turbo官方是使用CMake编译的,我们可以参考libjpeg-turbo的CMake脚本对CPU架构的处理方法,在CMakeLists.txt前部加入以下代码,同时修改CMakeLists.ext中两处neon源码路径,来彻底解决这个问题。这里需要注意你的源码路径。

# CMakeLists.txt
# Generated by gn_to_cmake.py.
cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR)
cmake_policy(VERSION 2.8.8)
project(Skia)

#//: 从这里开始
# Detect CPU type and whether we're building 64-bit or 32-bit code
math(EXPR BITS "${CMAKE_SIZEOF_VOID_P} * 8")
string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} CMAKE_SYSTEM_PROCESSOR_LC)
if(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "x86_64" OR
        CMAKE_SYSTEM_PROCESSOR_LC MATCHES "amd64" OR
        CMAKE_SYSTEM_PROCESSOR_LC MATCHES "i[0-9]86" OR
        CMAKE_SYSTEM_PROCESSOR_LC MATCHES "x86" OR
        CMAKE_SYSTEM_PROCESSOR_LC MATCHES "ia32")
  if(BITS EQUAL 64)
    set(CPU_TYPE x86_64)
  else()
    set(CPU_TYPE i386)
  endif()
  if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL ${CPU_TYPE})
    set(CMAKE_SYSTEM_PROCESSOR ${CPU_TYPE})
  endif()
elseif(CMAKE_SYSTEM_PROCESSOR_LC STREQUAL "aarch64" OR
        CMAKE_SYSTEM_PROCESSOR_LC MATCHES "arm*64*")
  set(CPU_TYPE arm64)
elseif(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "arm*")
  set(CPU_TYPE arm)
elseif(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "ppc*" OR
        CMAKE_SYSTEM_PROCESSOR_LC MATCHES "powerpc*")
  set(CPU_TYPE powerpc)
else()
  set(CPU_TYPE ${CMAKE_SYSTEM_PROCESSOR_LC})
endif()
message(STATUS "${BITS}-bit build (${CPU_TYPE})")
#//: 到这里结束

file(WRITE "/Users/lmy/.AndroidStudioProjects/AlSkia/src/skia/cmake/empty.cpp")
execute_process(COMMAND
  ninja -C "/Users/lmy/.AndroidStudioProjects/AlSkia/src/skia/cmake/" build.ninja
  RESULT_VARIABLE ninja_result)
if (ninja_result)
  message(WARNING "Regeneration failed running ninja: ${ninja_result}")
endif()
include("/Users/lmy/.AndroidStudioProjects/AlSkia/src/skia/cmake/CMakeLists.ext")
# CMakeLists.ext
"/Users/lmy/.AndroidStudioProjects/AlSkia/src/skia/third_party/externals/libjpeg-turbo/simd/arm64/jsimd.c"
set("${target}__asm_srcs" "/Users/lmy/.AndroidStudioProjects/AlSkia/src/skia/third_party/externals/libjpeg-turbo/simd/arm64/jsimd_neon.S")
#//: 修改为
"/Users/lmy/.AndroidStudioProjects/AlSkia/src/skia/third_party/externals/libjpeg-turbo/simd/${CPU_TYPE}/jsimd.c"
set("${target}__asm_srcs" "/Users/lmy/.AndroidStudioProjects/AlSkia/src/skia/third_party/externals/libjpeg-turbo/simd/${CPU_TYPE}/jsimd_neon.S")

  2. 继续Refresh Linked C++ Projects->Build,接着报错。

./skia/src/core/SkVM.cpp:2816:28: error: use of undeclared identifier 'arg'

  这个错误实际上是由于SkVM.cpp使用了__aarch64__宏判断arm架构,而我这里编译的是arm32架构,是没有__aarch64__这个宏的,所以报错。把整个CPP文件的defined(__aarch64__)改成defined(__arm__) || defined(__aarch64__)即可解决问题。

defined(__aarch64__)
//: 修改为
defined(__arm__) || defined(__aarch64__)

  3. 继续Refresh Linked C++ Projects->Build,接着报错。

./skia/third_party/externals/dng_sdk/source/dng_safe_arithmetic.h:118: error: undefined reference to '__mulodi4'

  这个错误是NDK r17c版本的一个bug,我们让dng_sdk模块依赖compiler_rt-extras静态库就可以了,compiler_rt-extras是NDK的一个静态库,只有4KB,对大小几乎没有影响。如果你用的NDK版本大于r17c,可能不会报错,忽略即可。dng_sdk是Adobe开源的一个RAW图解码器,如果不需要,也可以删除这个依赖,从而避免这个错误。

set("target" "third_party__dng_sdk")
...
# 让dng_sdk依赖compiler_rt-extras
# 查找ndk中的compiler_rt-extras
find_library("library__compiler_rt" "compiler_rt-extras")
# 链接compiler_rt-extras
target_link_libraries("${target}"
  "third_party__zlib"
  "third_party__libjpeg-turbo_libjpeg"
  "${library__compiler_rt}")

  到这里,我成功编译了demo apk,然而事情还远远没有结束。分析apk发现,并没有找到我想要的libskia.so。检查CMakeLists.ext发现,skia被编译成了静态库,最后链接到了liblibeditor.so。实际上liblibeditor.so只是一个包含了native app的demo。

./lib/armeabi-v7a +
    + liblibskqp_app.so
    + liblibviewer.so
    + liblibskottie_android.so
    + liblibeditor.so

  除了liblibeditor.so,还有另外三个liblibskqp_app.so、liblibviewer.so、liblibskottie_android.so,分别是Skia测试模块、功能展示模块、矢量动画模块(JSON动画,类似Facebook的Lottie库)。这些都不是我需要的,全部进行删除。修改CMakeLists.ext脚本,把这四个模块的编译代码全部删除,并且把skia模块的编译目标类型从静态库改为动态库,这样我们就可以成功编译libskia.so了。

add_library("${target}" STATIC ${${target}__cxx_srcs} ${${target}__other_srcs} ${${target}__obj_target_srcs})
# 把STATIC修改为SHARED
add_library("${target}" SHARED ${${target}__cxx_srcs} ${${target}__other_srcs} ${${target}__obj_target_srcs})

  除了以上要修改的部分,CMakeLists.ext还会生成大量的可执行文件,这个对于Android来说也是多余的,我们统统删掉,以提高编译速度。这些模块的开关应该是可以通过ninja控制的,感兴趣的读者可以研究一下。

#//: 以下可执行模块相关脚本全部删除,下面只展示部分代码,方便定位模块代码位置
#//:imgcvt
set("target" "imgcvt")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:sktexttopdf
set("target" "sktexttopdf")
add_executable("${target}" ${${target}__cxx_srcs})
#//:lua_app
set("target" "lua_app")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:list_gms
set("target" "list_gms")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:nanobench
set("target" "nanobench")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:fm
set("target" "fm")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:pathops_unittest
set("target" "pathops_unittest")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__other_srcs} ${${target}__obj_target_srcs})
#//:skpbench
set("target" "skpbench")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:dump_record
set("target" "dump_record")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:get_images_from_skps
set("target" "get_images_from_skps")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:image_diff_metric
set("target" "image_diff_metric")
add_executable("${target}" ${${target}__cxx_srcs})
#//:skdiff
set("target" "skdiff")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:skqp
set("target" "skqp")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:fuzz
set("target" "fuzz")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:list_gpu_unit_tests
set("target" "list_gpu_unit_tests")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:create_test_font_color
set("target" "create_test_font_color")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:remote_demo
set("target" "remote_demo")
add_executable("${target}" ${${target}__cxx_srcs})
#//:jitter_gms
set("target" "jitter_gms")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:skiaserve
set("target" "skiaserve")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:make_skqp_model
set("target" "make_skqp_model")
add_executable("${target}" ${${target}__cxx_srcs})
#//:cpu_modules
set("target" "cpu_modules")
add_executable("${target}" ${${target}__cxx_srcs})
#//:blob_cache_sim
set("target" "blob_cache_sim")
add_executable("${target}" ${${target}__cxx_srcs})
#//:skpinfo
set("target" "skpinfo")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:lua_pictures
set("target" "lua_pictures")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:create_test_font
set("target" "create_test_font")
add_executable("${target}" ${${target}__cxx_srcs})
#//:skp_parser
set("target" "skp_parser")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})
#//:dm
set("target" "dm")
add_executable("${target}" ${${target}__cxx_srcs} ${${target}__obj_target_srcs})

  最后重新编译一下,即可生成带libskia.so的apk,编译成功!

Skia简单demo

  SkCanvas和SkBitmap是Skia比较核心的两类,与Android的Canva和Bitmap基本一致,因为它们的底层实现实际上就是Skia。./skia/samplecode目录下有大量Sample可供参考,这里只展示简单的使用。

// 引入skia头文件,位置在./skia/include,建议通过cmake包含进来
#include "include/codec/SkCodec.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkData.h"
#include "include/core/SkImage.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkPath.h"
#include "include/core/SkFont.h"
// 图片解码
SkBitmap bmp;
sk_sp<SkData> data = SkData::MakeFromFileName("/image/file/path");
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(data);
if (!codec) {
    return;
}
SkImageInfo info = codec->getInfo().makeColorType(colorType);
if (!bmp.tryAllocPixels(info)) {
    return nullptr;
}
if (SkCodec::kSuccess == codec->getPixels(info, bmp.getPixels(), bmp.rowBytes())) {
        // Show image.
}

// 显示文字,如果要显示中文,需要先加载中文字体,否则会乱码
SkBitmap bmp;
bmp.allocN32Pixels(720, 1280);
SkCanvas canvas(bmp);
SkPaint paint;
paint.setAntiAlias(true);
paint.setColor(SK_ColorWHITE);
paint.setStrokeWidth(3);
canvas.drawColor(SK_ColorBLACK);

SkFont font;
font.setSize(60);

SkString str = SkStringPrintf("Test text. 一二三四五六七八九十");
const char *text = str.c_str();
SkRect bounds;
font.measureText(text, strlen(text), SkTextEncoding::kUTF8, &bounds);

canvas.drawSimpleText(text, strlen(text), SkTextEncoding::kUTF8,
                          (bmp.width() - bounds.width()) / 2,
                          (bmp.height() + bounds.height()) / 2, font, paint);

// 绘制二阶贝塞尔曲线
SkBitmap bmp;
bmp.allocN32Pixels(720, 1280);
SkCanvas canvas(bmp);
SkPaint paint;
paint.setAntiAlias(true);
paint.setColor(SK_ColorWHITE);
paint.setStrokeWidth(3);
paint.setStyle(SkPaint::Style::kStroke_Style);
canvas.drawColor(SK_ColorBLACK);

SkPath path;
SkPoint center = SkPoint::Make(360.f, 640.f);
SkPoint end = SkPoint::Make(360.f, 1280.f);
path.moveTo(0, 0);
path.quadTo(center, end);
canvas.drawPath(path, paint);

  至此Skia编译Done!因为通过CMake进行编译,所以可以很方便的使用Android Studio阅读Skia的全部源码,就像浏览自己的项目代码一样,可以愉快的学习了。Skia Demo


Homepage