前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「Skia学习笔记」一、使用CMake交叉编译Skia

「Skia学习笔记」一、使用CMake交叉编译Skia

作者头像
阿利民
发布2022-05-16 12:58:07
4K1
发布2022-05-16 12:58:07
举报
文章被收录于专栏:阿利民阿利民

什么是Skia

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

  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。

代码语言:javascript
复制
#克隆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。

代码语言:javascript
复制
#配置编译环境,类似于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下进行编译,避免不必要的坑。

代码语言:javascript
复制
#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脚本和中间文件的保存目录,你也可以改为其它目录。

代码语言:javascript
复制
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的编译。

代码语言:javascript
复制
./skia/cmake +
        + CMakeLists.txt
        + CMakeLists.ext
        +  ...

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

如果这个过程不知道如何操作,你需要了解一下cmake的使用,也可以参考 https://github.com/imalimin/AlSkia

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

代码语言:javascript
复制
./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被引入的地方,果不其然,写的就是./libjpeg-turbo/simd/arm64/jsimd_neon.S。实际上这是因为我上面运行的gn转cmake命令没有加target_cpu="arm"造成的,重新运行一下命令,就可以解决这个问题。

代码语言:javascript
复制
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源码路径,来彻底解决这个问题。这里需要注意你的源码路径。

代码语言:javascript
复制
# 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")
代码语言:javascript
复制
# 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,接着报错。

代码语言:javascript
复制
./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__)即可解决问题。

代码语言:javascript
复制
defined(__aarch64__)
//: 修改为
defined(__arm__) || defined(__aarch64__)

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

代码语言:javascript
复制
./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图解码器,如果不需要,也可以删除这个依赖,从而避免这个错误。

代码语言:javascript
复制
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。

代码语言:javascript
复制
./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了。

代码语言:javascript
复制
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控制的,感兴趣的读者可以研究一下。

代码语言:javascript
复制
#//: 以下可执行模块相关脚本全部删除,下面只展示部分代码,方便定位模块代码位置
#//: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可供参考,这里只展示简单的使用。

代码语言:javascript
复制
// 引入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: https://github.com/imalimin/AlSkia

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 阿利民 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • NDK交叉编译Skia
  • CMake交叉编译Skia
  • Skia简单demo
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档