Skia的初探(Skia的GN腳本編譯與第一個Skia應用)

前言

如今大前端代表之一flutter十分火熱,也是一種大的趨勢。flutter與rn對大前端上的理解不同,rn是自上而下的大前端解決方案,而flutter是自下而上的大前端解決方案。為什么我說flutter是自下而上的解決方案呢?實際上這種解決思路也來源與移動端手機游戲開發(fā),flutter繞開對每一個系統(tǒng)頂層ui api,直接對接系統(tǒng)底層的cpu,gpu。

Google是怎么做到的?實際上,如果翻開Android底層源碼,我們會在external目錄發(fā)現(xiàn)一個名為Skia的第三方庫。Skia實際上是Google出品的Android繪制2D圖形的核心庫,在剛誕生之初就以在低性能的移動端中表現(xiàn)處高性能的水平受人側(cè)目,但是大前端的技術(shù)十分眾多繁雜,就像金子被沙子掩蓋一般,不受大眾的關(guān)注。

關(guān)于Skia如何在Android運作之后我會在Android重學系列中,和大家好好聊聊。

回到原來的話題,我們翻開官網(wǎng),發(fā)現(xiàn)Skia原本支持chrome瀏覽器,Android,iOS,Mac,Web,Windows等平臺上渲染圖像。換句話說,經(jīng)過這些年的發(fā)展,名聲不顯的Skia本身就具備了跨平臺的能力。skia作為核心渲染庫就是極好的選擇。實際上我們能夠看到這段時間Google的新技術(shù),如flutter,新平臺Fuchsia都是以它為核心渲染庫。

學習東西就要學習本質(zhì),才能更好的掌握。因此,我們需要對Skia有一定的理解,本文將作為Skia的系列第一篇,從Skia如何編譯開始。

正文

編譯的準備

我們可以執(zhí)行g(shù)it如下的命令,把Skia源碼從官方下載下來:

git clone https://skia.googlesource.com/skia.git

當然,我們可以從github上搜索skia,下載下來。我就是這樣做的。

第二步十分重要,我就踩了坑,編譯老是缺少了文件,請安裝了python之后,執(zhí)行如下命令:

cd skia
python2 tools/git-sync-deps

把skia編譯,運行需要的第三方庫全部下載下來。

GN與ninja 腳本

我經(jīng)過前段時間的學習,對編譯有一點心得。當我信心滿滿的打開了Skia的目錄的時候,發(fā)現(xiàn)我熟悉的CONFIGURE的文件呢?熟悉的CMake,Makefile呢?都不見了。取而代之的是BUILD.gn腳本。

GN腳本相關(guān)的資料不多,但是官方的資料還是挺詳細的。GN腳本是什么?官方是怎么說的,如果CMake和Makefile是高級語言,那么GN和ninja則是相當于匯編語言的存在,存粹是為了編譯速度而誕生的。我們翻開Android的源碼,發(fā)現(xiàn)Android的編譯工具經(jīng)過了幾次變遷,從mk到cmakelists,現(xiàn)在Android9.0使用的bp+GN腳本。關(guān)于bp文件,不是本文重點就不多贅述。

我在之前的CMake總結(jié)一文有講解過,CMake是基于Makefile之上的很強大的編譯工具。那么GN實際上是基于ninja之上的編譯工具。

ninja的簡單介紹

ninja和Makefile很相似,我們類比學習一下。Makefile需要創(chuàng)建一個make文件,ninja則需要創(chuàng)建一個build.ninja文件。

和學習Makefile,這里我同樣總結(jié)了,ninja需要清楚2個變量,1個規(guī)則,1個命令,就能完成build.ninja的編寫。ninja沒有自動變量,因為ninja是十分追求效率的編譯工具,省略了自動變量的替換和檢索。

一個規(guī)則

rule cc
 command = gcc -g -c ${in} -o ${out}

rule規(guī)則制定的是,一個規(guī)則cc這個命令實際上執(zhí)行的是command賦值的命令。

一個命令

build hello.o: cc hello.c

命令build 實際上告訴ninja我們要編譯的結(jié)果是hello.o,通過上面定義的cc規(guī)則,以hello.c作為輸入?yún)?shù)。

從上文可以知道就是是一個gcc的編譯命令。

2個變量

${out} ${in} 

分別指代的是build命令中輸出對象,以及輸入對象。

完整的一個簡單build.ninja如下:

rule cc
 command = gcc -g -c ${in} -o ${out}
rule link
 command = gcc ${in} -o ${out}

build hello.o: cc hello.c
build hello : link hello.o

當我們編譯好build.ninja之后,別忘了,調(diào)用ninja命令一下,就能自己找到當前目錄下的腳本文件,進行編譯。

GN簡單介紹

GN則稍微復雜點,提供了相當多的語法,以及復雜的流程,這里以GN腳本的開發(fā)流程為線索,鋪開來聊聊。

GN腳本的簡單開發(fā)流程:

  • 1.編寫.gn文件定義GN的配置文件位置,一般名字是BUILDCONFIGURE.gn.
  • 2.編寫B(tài)UILDCONFIGURE.gn,并且定義GN的需要使用的toolchain文件位置,因為GN不提供默認的編譯方法,需要我們?nèi)崿F(xiàn)對應的gcc等命令。
  • 3.在toolchain文件的位置,編寫一個BUILD.gn文件,實現(xiàn)其中各種編譯工具的命令。
  • 4.接著在.gn所在的文件目錄中,編寫B(tài)UILD.gn文件,這個文件才是我們真正的編譯文件。
  • 5.回到.gn的目錄,執(zhí)行如下命令完成編譯:

gn gen out/default //執(zhí)行g(shù)n腳本,并且確定輸出目錄
ninja -C out/default //調(diào)用ninja執(zhí)行由GN生成的ninja腳本完成編譯。

大致分為4個步驟,讓我按照流程一一講解。

步驟一

定義一個名字為.gn文件:

buildconfig = "http://BUILDCONFIG.gn"

//代表當前目錄。因此這里定義的是當前目錄下的BUILDCONFIG.gn文件。

步驟二

創(chuàng)建一個BUILDCONFIG.gn文件:

declare_args(){
} 

 set_default_toolchain("http://toolchain:gcc")
 cflags_cc = [ "-std=c++11" ]

在這個文件中,必須定義set_default_toolchain方法,設(shè)置默認的編譯工具的BUILD.gn配置信息位置。

declare_args,這個是一個方法,一般是GN腳本要編譯動態(tài)庫時候,需要的默認參數(shù)時候會調(diào)用declare_args這個方法,獲取默認參數(shù)。

步驟三

上一個步驟定義的位置為("http://toolchain:gcc"),說明這個編譯工具的配置未見在當前目錄下的toolchain文件下面。
因此,我們需要創(chuàng)建一個toolchain文件夾,并且在toolchain文件下創(chuàng)建一個BUILD.gn文件。定義toolchain文件,實際上和ninja的rule類似,定義不同編譯模式下的工具。這里我就獲取chrome的toolchain,由于我是在mac上編譯的,需要對這個文件做一點修改和調(diào)整。

toolchain("gcc") {
  tool("cc") {
    depfile = "{{output}}.d"
    command = "gcc -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} -c {{source}} -o {{output}}"
    depsformat = "gcc"
    description = "CC {{output}}"
    outputs = [
      "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
    ]
  }
  tool("cxx") {
    depfile = "{{output}}.d"
    command = "g++ -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} -c {{source}} -o {{output}}"
    depsformat = "gcc"
    description = "CXX {{output}}"
    outputs = [
      "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
    ]
  }
  tool("alink") {
    rspfile = "{{output}}.rsp"
    command = "rm -f {{output}} && ar rcs {{output}} @$rspfile"
    description = "AR {{target_output_name}}{{output_extension}}"
    rspfile_content = "{{inputs}}"
    outputs = [
      "{{target_out_dir}}/{{target_output_name}}{{output_extension}}",
    ]
    default_output_extension = ".a"
    output_prefix = "lib"
  }
  tool("solink") {
    soname = "{{target_output_name}}{{output_extension}}"  # e.g. "libfoo.so".
    rspfile = soname + ".rsp"
    command = "g++ -shared {{ldflags}} -o $soname @$rspfile"
    rspfile_content = "-Wl,-all_load {{inputs}} {{solibs}} -Wl,-noall_load {{libs}}"
    description = "SOLINK $soname"
    # Use this for {{output_extension}} expansions unless a target manually
    # overrides it (in which case {{output_extension}} will be what the target
    # specifies).
    default_output_extension = ".so"
    outputs = [
      soname,
    ]
    link_output = soname
    depend_output = soname
    output_prefix = "lib"
  }
  tool("link") {
    outfile = "{{target_output_name}}{{output_extension}}"
    rspfile = "$outfile.rsp"
    command = "g++ {{ldflags}} -o $outfile @$rspfile {{solibs}} {{libs}}"
    description = "LINK $outfile"
    rspfile_content = "{{inputs}}"
    outputs = [
      outfile,
    ]
  }
  tool("stamp") {
    command = "touch {{output}}"
    description = "STAMP {{output}}"
  }
  tool("copy") {
    command = "cp -af {{source}} {{output}}"
    description = "COPY {{source}} {{output}}"
  }
}

能看到在這個BUILD.gn,中我們定義了一個toolchain這個好像方法的東西,toolchain中聲明了一個"gcc"的名字。這種在GN腳本中稱為模版,這是GN內(nèi)置的模版。其作用相當于方法一般,能夠被調(diào)用。toolchain這個模版本身帶有著邏輯,當執(zhí)行完這個作用域內(nèi)的的所有行為之后,竟會執(zhí)行模版內(nèi)部的邏輯。

在toolchain中有許多tool("link"),tool(“solink”)等小的模版。這些內(nèi)置模版,實際上代表著各種工具,如solink,代表著動態(tài)庫的鏈接時候需要的命令,cxx,cc代表著編譯階段時候的命令。當GN腳本執(zhí)行到某個階段,將會調(diào)用tool中對應的命令。

在這里,我們所有的命令都需要做調(diào)整。比如在solink中,我們編寫鏈接的命令,想要設(shè)定soname的時候,mac環(huán)境不支持soname,支持install_name。因此,我們需要成對應平臺的對應的命令。

步驟四

編寫屬于我們的BUILD.gn腳本。
這里需要介紹幾個,我們常用的內(nèi)置模版。

static_library: 編譯靜態(tài)庫
shared_library: 編譯動態(tài)庫
executable: 編譯執(zhí)行文件
source_set: 編譯一種輕量的靜態(tài)庫。
component: 可能是source_set也可能是shared_library,根據(jù)構(gòu)建類型確定

這里舉一個簡單的例子,我們編譯個執(zhí)行hello_gn的執(zhí)行文件,鏈接一個say_hello的動態(tài)鏈接庫。寫法如下:

shared_library("say_hello"){
 sources=["say_hello.c",]
}

executable("hello_gn"){
 sources = ["hello.c",]
 deps=[":say_hello",]
}

能看到,我們使用兩個內(nèi)置模版,自上而下的分別執(zhí)行如下操作:

  • 1.編譯一個名字為say_hello的動態(tài)庫。sources是這個模版里面識別的屬性,代表著這個動態(tài)庫對應的資源文件。
  • 2.編譯一個名字為hello_gn的可執(zhí)行文件,資源文件是hello .c,同時需要依賴say_hello模版生成的產(chǎn)物。換句話說,可執(zhí)行文件要鏈接上這個動態(tài)鏈接庫。

這樣就能完成了一次GN腳本的編寫。最后執(zhí)行第五步的命令,就能開始編譯。

GN的模版(template)

能看到在GN腳本的編寫過程中,模版占了絕大部分的比重。那么我們該怎么自定義模版呢?

自定義模版規(guī)則有四條,如下:

  • 1.定義一個template的作用域,并且在設(shè)置好target_name.如下定義了一個名字為opts的模版
template("opts"){
...
}
  • 2.調(diào)用一個模版方法如下,以上面定義好的模版為例子
opts("target_name"){
}

調(diào)用的同時,要在里面編寫一個target_name。用以識別模版不同的使用。其實可以類比為我們常用的泛型,target_name為泛型中具體的對象。

  • 3.模版能夠獲取調(diào)用模版時候所有的屬性,其方法個js很相似,通過invoker來獲取。
    舉個例子
opts("target_name"){
enabled = true
}

我們能夠在定義template的時候,通過invoker.enabled訪問到調(diào)用時候enabled的參數(shù)。但是請注意,注意獲取方式有兩種:

  1. 必須調(diào)用如下方法判斷了模版調(diào)用者中存在該對象,才能獲取屬性,不然會報錯。
if (defined(invoker.enabled)) {
      values = invoker.enabled
    }
  1. 在獲取參數(shù)之前調(diào)用如下語句:
visibility = [ ":*" ]

這樣默認是所有的參數(shù)可見。但是必須注意了,所有調(diào)用方注入的參數(shù)在對應作用域必須使用,不然會報錯。

  • 4.每一個模版的參數(shù)如同方法一般,只存在在自己的作用域,如果需要把參數(shù)設(shè)置到已經(jīng)在全局存在的參數(shù)。需要通過如下方法,把模版內(nèi)的參數(shù)跨越作用域傳送出去。
forward_variables_from(invoker,"*",[sources,cflags])

這個方法有三個參數(shù):
第一個參數(shù):要傳輸?shù)膶ο骾nvoker中的參數(shù);
第二個參數(shù):要傳輸?shù)饺肿饔糜虻膶傩允鞘裁?"*"代表著所有屬性
第三個參數(shù):哪些屬性不需要傳輸出去。

明白了這些基礎(chǔ),我們就能更好的閱讀Skia的GN腳本,甚至可以按照自己的心意裁剪Skia庫。

更多關(guān)于GN腳本可以看官方文檔:https://gn.googlesource.com/gn/+/master/docs/

理解Skia GN腳本

打開skia源碼根目錄下的BUILD.gn。首先能看到,import幾個gn目錄下的gni文件:

import("gn/flutter_defines.gni")
import("gn/fuchsia_defines.gni")
import("gn/shared_sources.gni")

import("gn/skia.gni")

實際上前兩個我們可以忽略,是把flutter和fuchsia一些參數(shù)需要依賴的數(shù)據(jù)添加進來。主要是后面兩個gni腳本。

shared_sources腳本主要是導入了核心以及相關(guān)的模塊。skia腳本則根據(jù)編譯命令,平臺設(shè)置一些默認值,以及設(shè)置編譯工具鏈toolchain。

Skia 2個重要的模版

opts

template("opts") {
  visibility = [ ":*" ]
  if (invoker.enabled) {
    source_set(target_name) {
      check_includes = false
      forward_variables_from(invoker, "*")
      configs += skia_library_configs 
    } 
  } else {
    # If not enabled, a phony empty target that swallows all otherwise unused variables. 
    source_set(target_name) {
      check_includes = false
      forward_variables_from(invoker,
                             "*",
                             [
                               "sources",
                               "cflags",
                             ])
    }
  }
}

這里就能看到Skia自己定義了一個模版opts的模版,用來控制編譯平臺需要對應需要的配置:

opts("armv7") {
  enabled = current_cpu == "arm"
  sources = skia_opts.armv7_sources + skia_opts.neon_sources
  cflags = []
}

能看到,當我們需要交叉編譯會在命令參數(shù)中設(shè)置當前平臺current_cpu,目標平臺target_cpu。能看到會判斷當前的平臺是什么,并且一些新的資源配置。

optional

控制Skia,各個模塊是否加入到Skia中,各個模塊以什么方式編譯進來。就以jpeg模塊為例子:

optional("jpeg") {
  enabled = skia_use_libjpeg_turbo
  public_defines = [ "SK_HAS_JPEG_LIBRARY" ]

  deps = [
    "http://third_party/libjpeg-turbo:libjpeg",
  ]
  public = [
    "include/encode/SkJpegEncoder.h",
  ]
  sources = [
    "src/codec/SkJpegCodec.cpp",
    "src/codec/SkJpegDecoderMgr.cpp",
    "src/codec/SkJpegUtility.cpp",
    "src/images/SkJPEGWriteUtility.cpp",
    "src/images/SkJpegEncoder.cpp",
  ]
}

能看到在jpeg模塊依賴于third_party的libjpeg-turbo第三方庫。公有開放出來的頭文件是SkJpegEncoder,skia在這個第三方庫之上封裝的源碼就在sources的集合中。每一個模塊是否加入到編譯中,由enabled來決定。

Skia對于每一個模塊都是通過這種模式管理的。

Skia主體模塊

經(jīng)過對每一個模塊的模版都定義之后,我們能看到component內(nèi)置模版。

component("skia") {
...
}

實際上skia的靜態(tài)/動態(tài)庫就是以這里為主體,把各個模塊的都依賴上。

  public_deps = [
    ":gpu",
    ":pdf",
    ":skcms",
  ]

  deps = [
    ":arm64",
    ":armv7",
    ":avx",
    ":compile_processors",
    ":crc32",
    ":fontmgr_android",//android的字體
    ":fontmgr_custom",
    ":fontmgr_custom_empty",
    ":fontmgr_empty",
    ":fontmgr_fontconfig",
    ":fontmgr_fuchsia",
    ":fontmgr_wasm",
    ":fontmgr_win",
    ":fontmgr_win_gdi",
    ":gif",//gif解析
    ":heif",
    ":hsw",
    ":jpeg",//jpeg解析
    ":none",
    ":png",//png解析
    ":raw",
    ":sksl_interpreter",
    ":skvm_jit",//skia 的jit
    ":sse2",
    ":sse41",
    ":sse42",
    ":ssse3",
    ":webp",//webp
    ":wuffs",
    ":xml",//xml解析庫
  ]

就是這里把所有的模塊都依賴上。

如果要裁剪庫的朋友請注意,這些不僅僅是全部,在上面的gni中還有更多的定義,每一個依賴模塊實際上很可能有第二個依賴模塊。就以xml來說,實際上在xml.gni能看到會依賴上zlib這個zip解壓模塊。我們?nèi)绻ㄟ^skia_enabled_zlib為false,但是xml為true,就會出現(xiàn)編譯找不到對應文件的異常。

因此裁剪庫的時候,我們需要理清楚每一個依賴庫之間的依賴關(guān)系。
當依賴全部完成之后,Skia會根據(jù)平臺做進一步資源依賴,最終完成編譯。

Skia編譯命令

當我們理清楚這些之后,我們要交叉編譯出一個arm平臺上的Android skia需要依次執(zhí)行如下命令,就能編譯出一個完整的skia出來。當然我是為了方便,編寫了一個腳本

bin/gn gen out/Shared -args='ndk="/Users/yjy/Library/Android/sdk/ndk/20.0.5594570" ndk_api=21  target_os="android" target_cpu="arm" is_component_build=true skia_use_libjpeg_turbo=true skia_enable_pdf=false skia_use_libpng=true skia_use_libwebp=true is_debug=false is_official_build=false'

ninja -C out/Shared

其中is_component_build,代表著這個時候是需要編譯skia的動態(tài)庫。target_cpu代表目標平臺。skia_enable_pdf代表關(guān)閉pdf的模塊。is_debug關(guān)閉skia的debug模式

Skia編譯總結(jié)和踩坑

實際上,在第一次上手Skia的編譯的時候,踩了不少坑。主要也是對GN腳本不熟悉。網(wǎng)上有不少的問題,我好像都遇到了。

其中有一處,skia_use_system_xxx的標志參數(shù)需要討論一下。這個參數(shù)打開的話就會從當前系統(tǒng)查找又沒有對應的模塊源碼編譯進去。比如說,我們打開skia_use_system_png,則會從系統(tǒng)中查找又沒有l(wèi)ibpng的源碼編譯進來,沒有則報錯。一般我們是關(guān)閉的。

第二處,當我們關(guān)閉了jpeg,設(shè)置了skia_use_libjpeg_turbo =false的時候。編譯通過了,但是卻出現(xiàn)如下的錯誤信息:

dlopen failed: cannot locate symbol '_ZN11SkJpegCodec6IsJpegEPKvj'

這個錯誤信息,是指未定義的符號,可以通過nm -u xxx.so查看未定義符號有什么。一般是需要鏈接的庫沒有鏈接進來。

能從這里面看到實際上是有一個SkJpegCodec的isJpeg方法沒有定義到導致的異常。我翻了下源碼,發(fā)現(xiàn)到skia的so庫中必須編譯如下這個cpp

"src/codec/SkCodec.cpp",

這個cpp中實際上調(diào)用了isJpeg方法。這就是異常的根本。雖然我們并沒有編譯近jpeg,但是耐不住核心庫的解析類調(diào)用了啊。并不是網(wǎng)上所說的is_official_build沒有打開。

當然,也有可能是我當前的代碼是最新版本,導致現(xiàn)象不一致。

因此,我們要裁剪skia,還需要對skia的源碼熟悉才行。這其實應該也是skia劃分模塊沒有劃分很好的坑。

哈哈,也就是我們常說的,組件化沒有徹底。

Skia第一個應用

skia第一個Android應用很簡單:我們就寫hello world。用兩種方式來寫。來驗證我這段時間是否閱讀懂了Canvas的機制。

CMakeLists的代碼在這里就不放出了,很簡單的。我們編寫兩個View,一個是基于Bitmap上修改,另一個是基于Surface的編寫。

首先編寫好兩個native方法:

public class SkiaUtils {
    static {
        System.loadLibrary("skia");
        System.loadLibrary("native-lib");
    }

    public static native void native_renderCanvas(Bitmap bitmap);

    public static native void native_render(Surface surface,int width,int height);
}

之前寫代碼都是靜態(tài)注冊jni,這次來試試動態(tài)注冊jni:

static const char* const className = "com/yjy/skiaapplication/SkiaUtils";
static const JNINativeMethod gMethods[] = {
        {"native_renderCanvas","(Landroid/graphics/Bitmap;)V",(void *)native_renderCanvas},
        {"native_render","(Landroid/view/Surface;II)V",(void *)native_render}
};


jint JNI_OnLoad(JavaVM *vm,void* reserved){
    JNIEnv *env = NULL;
    jint result;

    if(vm->GetEnv((void**)&env,JNI_VERSION_1_4)!=JNI_OK){
        return -1;
    }

    jclass clazz = env->FindClass(className);
    if(!clazz){
        LOGE("can not find class");
        return -1;
    }

    if(env->RegisterNatives(clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0])) < 0){
        LOGE("can not register method");
        return -1;
    }

    return JNI_VERSION_1_4;

}

很簡單,這樣就把方法注冊到JNI中,熟悉源碼的肯定對這種方法十分熟悉。

Skia基于Bitmap的編寫

首先編寫一個簡單View

public class SkiaView extends View {

    // Used to load the 'native-lib' library on application startup.

    static {
        System.loadLibrary("skia");
        System.loadLibrary("native-lib");
    }

    Bitmap bitmap;
    Paint paint = new Paint();

    public SkiaView(Context context) {
        super(context);
    }

    public SkiaView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SkiaView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if(bitmap == null){
            bitmap=
                    Bitmap.createBitmap(canvas.getWidth(),canvas.getHeight(),
                            Bitmap.Config.ARGB_8888);
        }

        SkiaUtils.native_renderCanvas(bitmap);

        canvas.drawBitmap(bitmap,0,0,paint);
    }
}

很簡單就是在onDraw的時候把bitmap傳遞到底層。把上面的bitmap繪制成我們需要的樣子最后把圖像繪制上去。

Skia繪制Bitmap

網(wǎng)上有很多Skia的資料,很遺憾無一例外都過時了,很多api都遷移和變化了,不過只是運用api是十分簡單的事情,看看文檔就能解決了。如果是Android開發(fā)看起來更加輕松,因為Skia幾乎都是在SkCanvas上繪制,實際上對應的是Android的Canvas對象。每一個SkCanvas都對應上了Android中Canvas的Java api。

extern "C"
JNIEXPORT void JNICALL
native_renderCanvas(JNIEnv *env, jobject thiz, jobject bitmap) {
    // TODO: implement native_renderCanvas()
    LOGE("native render");

    AndroidBitmapInfo info;
    int *pixel;
    int ret;

    ret = AndroidBitmap_getInfo(env,bitmap,&info);
    ret = AndroidBitmap_lockPixels(env,bitmap,(void**)&pixel);

    int width = info.width;
    int height = info.height;

    SkBitmap bm = SkBitmap();
    SkImageInfo image_info = SkImageInfo
            ::MakeS32(width,height,SkAlphaType::kOpaque_SkAlphaType);
    bm.setInfo(image_info,image_info.minRowBytes());
    bm.setPixels(pixel);

    SkCanvas background(bm);
    SkPaint paint;//畫筆

    paint.setColor(SK_ColorBLACK);
    SkRect rect;//繪制一個矩形區(qū)域
    rect.set(SkIRect::MakeWH(width,height));

    background.drawRect(rect,paint);

    SkPaint paint2;
    paint2.setColor(SK_ColorBLUE);
    const char *str = "Hello Skia";

    SkFont skfont(SkTypeface::MakeDefault(),100);

    background.drawString(str,100,100,skfont,paint2);

    AndroidBitmap_unlockPixels(env,bitmap);
}

AndroidBitmap_getInfo獲取bitmap中所有的信息。接著通過lock方法,把pixel的像素指針給關(guān)聯(lián)起來,最后通過繪制背景和文字在SKCanvas上,呈現(xiàn)出一個Hello Skia方法。


image.png

Skia基于Surface繪制

好像很多文章都止步于繪制bitmap,沒有其他想法。我倒是看到一個哥們直接獲取Android源碼的skia相關(guān)文件,編譯出so庫。這不失是一種好方法,這樣就能編譯出Android中純凈的Skia。

他的做法是把Canvas對象傳到底層,直接通過GraphicsJNI把Java的Canvas轉(zhuǎn)化為SkCanvas。我不知道這個作者是基于什么版本開發(fā)的,我反正一路看到Android 4.4就沒興趣看下去了。

GraphicsJNI本質(zhì)上是獲取Canvas中存著native對象的地址。但是這個地址對象并非是SkCanvas,而是SkiaCanvas。SkiaCanvas和SkCanvas并非繼承關(guān)系,而是包含關(guān)系。雖然我當然能寫一模一個的獲取Java對象的方法,但是獲取的SkiaCanvas對象,會對整個編程造成錯誤,甚至是誤導。

關(guān)于這一塊的源碼理解,我們將會在Android重學系列,和大家好好講講它們之間的關(guān)系。

還有一種做法,了解音視頻開發(fā)的哥們一定會熟悉,在Android中一種叫做NativeWindow的本地窗口,是用來在Surface上繪制。關(guān)于NativeWindow也會在Android重學系列和大家聊聊。

我們這一次借助NativeWindow來繪制Skia。

編寫一個簡單的SurfaceView
/**
 * <pre>
 *     author : yjy
 *     e-mail : yujunyu12@gmail.com
 *     time   : 2019/09/14
 *     desc   :
 *     version: 1.0
 * </pre>
 */
public class SkiaCanvasView extends SurfaceView implements SurfaceHolder.Callback2 {

    private SurfaceHolder mHolder;
    private HandlerThread mHandlerThread;
    private Handler mHandler;
    private static final int DRAW = 1;

    public SkiaCanvasView(Context context) {
        this(context,null);
    }

    public SkiaCanvasView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SkiaCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mHandlerThread = new HandlerThread("Skia");
        mHolder = getHolder();
        mHolder.addCallback(this);
        mHandlerThread.start();
        mHandler = new SkiaHandler(mHandlerThread.getLooper());

    }

    @Override
    public void surfaceRedrawNeeded(SurfaceHolder holder) {

    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Message message = new Message();
        message.what = DRAW;
        message.obj = holder.getSurface();
        message.arg1 = getWidth();
        message.arg2 = getHeight();
        mHandler.sendMessage(message);
        Log.e("create","width:"+getWidth());
        Log.e("create","height"+getHeight());
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mHandlerThread.quit();
    }

    private  class SkiaHandler extends Handler{

        public SkiaHandler(Looper looper){
            super(looper);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case DRAW:
             
                       SkiaUtils.native_render((Surface) msg.obj,msg.arg1,msg.arg2);
                    

                    break;
            }
        }
    }
}

很簡單,不解釋。

繪制Skia

extern "C"
JNIEXPORT void JNICALL
native_render(JNIEnv *env, jobject thiz, jobject jSurface,jint width,jint height){
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env,jSurface);


    ANativeWindow_setBuffersGeometry(nativeWindow,  width, height, WINDOW_FORMAT_RGBA_8888);

    ANativeWindow_Buffer *buffer = new ANativeWindow_Buffer();

    ANativeWindow_lock(nativeWindow,buffer,0);


    int bpr = buffer->stride * 4;


    SkBitmap bitmap;
    SkImageInfo image_info = SkImageInfo
    ::MakeS32(buffer->width,buffer->height,SkAlphaType::kPremul_SkAlphaType);


    bitmap.setInfo(image_info,bpr);

    bitmap.setPixels(buffer->bits);

    SkCanvas *background = new SkCanvas(bitmap);
    SkPaint paint;

    paint.setColor(SK_ColorBLUE);
    SkRect rect;
    rect.set(SkIRect::MakeWH(width,height));

    background->drawRect(rect,paint);

    SkPaint paint2;
    paint2.setColor(SK_ColorWHITE);
    const char *str = "Hello Surface Skia";

    SkFont skfont(SkTypeface::MakeDefault(),100);

    background->drawString(str,100,100,skfont,paint2);

    SkImageInfo imageInfo = background->imageInfo();


    LOGE("row size:%d,buffer stride:%d",imageInfo.minRowBytes(),bpr);

    LOGE("before native_window stride:%d,width:%d,height:%d,format:%d",
            buffer->stride,buffer->width,buffer->height,buffer->format);

    int rowSize = imageInfo.minRowBytes();




    bool isCopy =  background->readPixels(imageInfo,buffer->bits,bpr,0,0);


    LOGE("after native_window stride:%d,width:%d,height:%d,format:%d",
         buffer->stride,buffer->width,buffer->height,buffer->format);

    ANativeWindow_unlockAndPost(nativeWindow);

}

readPixels 第一個參數(shù)是ImageInfo,圖像信息;第二個是像素集合;第三個參數(shù)是是每一行的像素字節(jié)數(shù);第四個參數(shù)是x方向的偏移量;第五個參數(shù)是y軸方向的偏移量。

能看到,我們同樣是生成一個新的SkCanvas,把圖像和文字繪制到上面,最后讀取所有的像素點,并且把像素點的數(shù)據(jù)傳送出去。

image.png

這是怎么回事?為什么文字都變形了??看起來整個文字被壓縮了。當我放大整個顯示范圍時候,發(fā)現(xiàn)整個文字向右邊倒下去了,并且被壓縮了。

這個問題坑了快一個小時。當時其實已經(jīng)有了初步猜測,如果了解OpenGL就知道,實際上在我們傳輸數(shù)據(jù)的時候,需要定義每一行要讀取多少數(shù)據(jù),要跳轉(zhuǎn)多少位數(shù)。當一行的數(shù)據(jù)設(shè)置異常的時候,就圖像就會出現(xiàn)異常。因為每一行讀的數(shù)據(jù)多了,下一行實際上起點向后挪動了,這就是文字往右邊倒的原因。

解決

我當時翻閱了Android源碼看到了,Android源碼在測定每一行的時候,并非是通過imageInfo去計算一行最小字節(jié)數(shù),而是通過nativeWindow中buffer的一行的像素*像素大小得到的每一行的字節(jié)數(shù)目。

經(jīng)過打印確實nativeWindow中通過buffer得到的stride和minRowSize計算的結(jié)果不一樣。有興趣的可以去看看,minRowSize計算的是最小的行字節(jié)長度,實際上是有可能大于等于這個數(shù)字的。

于是,我把readPixels修改成如下:

background->readPixels(imageInfo,buffer->bits,bpr,0,0);

如下圖:


image.png

總結(jié)

Skia的初探先到這里,到這里也算是入門了。之后Skia相關(guān)的文章將會是關(guān)于Skia的源碼解析篇。這也算是Android開發(fā)的好處,不需要過多熟悉Skia的api,因為我們在做Canvas的時候其實操作的就是SkCanvas的api。

從解決問題上的思路,可以明白閱讀源碼不僅僅是理解原理,更加重要的是,能解決看起來完全沒頭緒的問題,看起來不太可能的實現(xiàn)的功能。這也是我為什么熱衷于閱讀源碼的原因。

最后,附上demo地址的demo:https://github.com/yjy239/SkiaDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容