美文网首页音视频开发进阶音视频那点破事OpenGL
「Skia学习笔记」一、使用CMake交叉编译Skia

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

作者: Alimin利民 | 来源:发表于2020-05-11 21:53 被阅读0次

    什么是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


    欢迎关注微信公众,第一时间获取一手多媒体技术资讯

    相关文章

      网友评论

        本文标题:「Skia学习笔记」一、使用CMake交叉编译Skia

        本文链接:https://www.haomeiwen.com/subject/yuvjnhtx.html