[劍走偏鋒] Android使用Golang代替C/C++進(jìn)行Native開發(fā)

起因

事情是這樣的: 一次Android項(xiàng)目開發(fā)過程中,要用到數(shù)據(jù)的加密解密,因?yàn)閿?shù)據(jù)運(yùn)算量比較大,所以需要用到native進(jìn)行開發(fā),但是又極不情愿去寫C/C++那種既耽誤時(shí)間又不好調(diào)試的語言,所以想方設(shè)法的尋找替代方案,正好最近在用Golang,尋思著,Golang不是號(hào)稱速度接近C++又能快速開發(fā)的嗎,所以琢磨著能不能用Golang來寫Android的native部分,然后就有了一系列的踩坑過程 (這坑我踩了,剩下的你看著辦吧)

配料表

既然是用Golang開發(fā),當(dāng)然就需要用到Golang的開發(fā)環(huán)境,至于怎么搭建,你自己去找吧 (爛大街的玩意)
大致列一下需要用到的環(huán)境和SDK:

編譯準(zhǔn)備

開搞
首先是寫一份Golang的源碼,打個(gè)比方說 hello.go

package main

import "C"

//export SayHello
func SayHello(name *C.char) *C.char{
    return  C.CString("Hello : " + name)
}

//export Sum
func Sum(a int, n int) int {
    return a + n
}

func main(){
    // 這個(gè)主方法一定要寫,不然不給編譯
}

是不是覺得一臉懵逼,那么有必要解釋一下

  • import "C" 這個(gè)是要告訴CGO我需要調(diào)用C的方法,使用C語言的東西
  • //export SayHello 這個(gè)注釋是必須要有的,就相當(dāng)于C語言中的 extern 值得注意的是雙斜杠后面不能有空格,別問我為什么知道,所以 //export SayHello 就相當(dāng)于C語言的 extern char* SayHello(char*)
  • 為什么這里用 *C.char 而不用 string 呢? 因?yàn)?如果使用Golang中的 string 來定義的話,在Java中就不能直接以String的類型來傳遞,而會(huì)被Golang定義為一種名為 GoString 的神奇類型,就像這樣
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
typedef _GoString_ GoString;

extern GoString SayHello(GoString);

使用string的好處就是,他會(huì)直接導(dǎo)致你的代碼量增加,因?yàn)槟氵€要在Java中寫一個(gè)和GoString差不多的包含 String (字符串)int (字符串長(zhǎng)度) 的類,而C語言中的 char* 直接對(duì)應(yīng)的就是Java中的 String ,所以這里 建議使用 *C.char 來代替 Golang中的 string

  • 最后就是 main 方法,這個(gè)是必須要有的,不使用的話置空就好,沒有的話會(huì)導(dǎo)致編譯 .so 失敗,至于為什么,有待深入研究

參數(shù)配置

然后就開始編譯,在編譯之前,需要設(shè)置一下 go env 的參數(shù),從而達(dá)到我們想要的東西

Windows
set GOOS=android
## GOARCH可選平臺(tái),需要和 CC CXX 對(duì)應(yīng)
## arm (armeabi-v7a) CC=armv7a-linux-androideabi19-clang.cmd CXX=armv7a-linux-androideabi19-clang++.cmd
## arm64 (arm64-v8a) CC=aarch64-linux-android19-clang.cmd CXX=aarch64-linux-android19-clang++.cmd
## 386 (x86) CC=i686-linux-android19-clang.cmd CXX=i686-linux-android19-clang++.cmd
## amd64 (x86_64) CC=x86_64-linux-android21-clang.cmd CXX=x86_64-linux-android21-clang++.cmd
set GOARCH=arm
## 這個(gè)一定要,不然你編譯出來的so各種未定義
set CGO_ENABLED=1
## 設(shè)置NDK的編譯器路徑,需要和 GOARCH 對(duì)應(yīng)
set CC=${你的NDK目錄}\toolchains\llvm\prebuilt\windows-x86_64\bin\armv7a-linux-androideabi19-clang.cmd
set CXX=${你的NDK目錄}\toolchains\llvm\prebuilt\windows-x86_64\bin\armv7a-linux-androideabi19-clang++.cmd
Linux
export GOOS=android
export GOARCH=arm
export CGO_ENABLED=1
export CC=${你的NDK目錄}\toolchains\llvm\prebuilt\linux-x86_64\bin\armv7a-linux-androideabi19-clang
export CXX=${你的NDK目錄}\toolchains\llvm\prebuilt\linux-x86_64\bin\armv7a-linux-androideabi19-clang++
mac
我窮逼用不起蘋果,你們自己百度

之后就是開始編譯 .so 文件

go build -buildmode=c-shared -o libhello.so hello.go

要想優(yōu)化一下編譯后的產(chǎn)物大小可以這樣

go build -ldflags "-s -w" -buildmode=c-shared -o libhello.so hello.go

-s 參數(shù)是去掉編譯后的符號(hào)信息. -w 參數(shù)是去掉DWARF調(diào)試信息,不過需要注意的是,-w參數(shù)得到的產(chǎn)物無法進(jìn)行調(diào)試,當(dāng)然可以在發(fā)布的時(shí)候使用-w參數(shù)編譯.
這里編譯完就可以拿到 armeabi-v7aso 文件了,如果需要其他架構(gòu)的,請(qǐng)修改 GOARCH= arm/arm64/386/amd64 中的任意一個(gè),并修改 CC CXX 為對(duì)應(yīng)架構(gòu)的編譯器,然后重新編譯得到對(duì)應(yīng)架構(gòu)的 so 文件

食用方法

將下載的 JNA 解壓,復(fù)制 dist 目錄下的 jna-platform.jarjna-min.jarlibs目錄下,并將 android-armv7.jar android-aarch64.jar android-x86.jar android-x86-64.jar 解壓,得到里邊的 libjnidispatch.so 放到 jniLibs 的對(duì)應(yīng)目錄下,將編譯Golang源碼得到的 so 復(fù)制到Android項(xiàng)目的 jniLibs 目錄的對(duì)應(yīng)目錄下,如圖:
(假裝有圖)

Project
└─ app
   ├─ libs
   │  ├─ jna-min.jar
   │  └─ jna-platform.jar
   └─ src
      └─ main
         ├─ Androidmanifest.xml
         ├─ java
         ├─ res
         └─ jniLibs
            ├─ armeabi-v7a
            │  ├─ libjnidispatch.so
            │  └─ libhello.so
            ├─ arm64-v8a
            │  ├─ libjnidispatch.so
            │  └─ libhello.so
            ├─ x86
            │  ├─ libjnidispatch.so
            │  └─ libhello.so
            └─ x86_64
               ├─ libjnidispatch.so
               └─ libhello.so

在項(xiàng)目中新建一個(gè)接口,名稱隨意,繼承 com.sun.jna.Library

package com.demo.golang;

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface Hello extends Library {

    // 加載libhello.so
    Hello ins = Native.load("hello", Hello.class);

    /**
     * 對(duì)應(yīng) Golang 中的 SayHello 方法
     */
    String SayHello(String name);

    /**
     * 對(duì)應(yīng) Golang 中的 Sum 方法
     */
    int Sum(int a, int n);
}

然后在你想要調(diào)用的位置調(diào)用 Hello.ins.SayHello("Golang for Android with JNA") 比如我在 MainActivity 里邊調(diào)用

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((TextView) findViewById(R.id.text_view))
            .setText(Hello.ins.SayHello("Golang for Android with JNA ") + Hello.ins.Sum(500, 20));
    }
}

跑起來看看,是不是非常完美?是不是感覺新技能 get 有木有覺得比 C/C++ 簡(jiǎn)單得多?

總結(jié)

總體上來說,相對(duì)直接使用C/C++要簡(jiǎn)單方便,但是,也有一定的缺陷,暫時(shí)我還沒研究出從 Golang 調(diào)用 Java 代碼的方法, 所以簡(jiǎn)單來說就是只能通過 Java 調(diào)用Golang
簡(jiǎn)單的總結(jié)一下相對(duì) C/C++ JNI 來寫的一些缺點(diǎn)

  • 暫時(shí)沒法從 Golang 調(diào)用 Java (當(dāng)然這個(gè)我感覺應(yīng)該不難)
  • Java 調(diào)用 Golang 只能傳基本數(shù)據(jù)類型,沒辦法傳遞對(duì)象 (這個(gè)不知道定義一個(gè)結(jié)構(gòu)體能不能實(shí)現(xiàn))
  • Golang 調(diào)用 C/C++ 的鏈接庫不是很方便

暫時(shí)就這么多,后邊會(huì)花時(shí)間研究一下怎么簡(jiǎn)化流程和調(diào)用更高級(jí)的API,以達(dá)到使用純 Golang 開發(fā) Android 的目的

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

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

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