前言
如今大前端代表之一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ù)。但是請注意,注意獲取方式有兩種:
- 必須調(diào)用如下方法判斷了模版調(diào)用者中存在該對象,才能獲取屬性,不然會報錯。
if (defined(invoker.enabled)) {
values = invoker.enabled
}
- 在獲取參數(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方法。

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ù)傳送出去。

這是怎么回事?為什么文字都變形了??看起來整個文字被壓縮了。當我放大整個顯示范圍時候,發(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);
如下圖:

總結(jié)
Skia的初探先到這里,到這里也算是入門了。之后Skia相關(guān)的文章將會是關(guān)于Skia的源碼解析篇。這也算是Android開發(fā)的好處,不需要過多熟悉Skia的api,因為我們在做Canvas的時候其實操作的就是SkCanvas的api。
從解決問題上的思路,可以明白閱讀源碼不僅僅是理解原理,更加重要的是,能解決看起來完全沒頭緒的問題,看起來不太可能的實現(xiàn)的功能。這也是我為什么熱衷于閱讀源碼的原因。
最后,附上demo地址的demo:https://github.com/yjy239/SkiaDemo