编译 go 源码为 android 动态库(so)
文章目录
golang
跨平台, 性能强, 周边也算丰富
一直有一个想法, 把这东西写的代码编译成各个平台的库, 未来在 flutter
里用
开发环境和工具
- MacOS(其他的暂时不考虑)
- Go
- Go 1.14.6, 版本太低可能不能编译成 android 的
- Goland , 咱现在是正版用户, 用开源项目申请的
All products License
, 你也可以根据自己的情况选择别的 IDE 或使用文本编辑器(vscode 也不错)
- Android
- Android Studio
- Cmake
- Android SDK
- Android NDK
Go 部分
go 源码
add_library.go
1package main
2
3import "C"
4
5//export add
6func add(x, y int) int {
7 return x + y
8}
9
10//export remove_int
11func remove_int(x, y int) int {
12 return x - y
13}
14
15func main() {
16}
这里有几点要注意
- package 一定要是 main(强制规定)
- 一定要包含 main 函数(强制规定)
- import "C", 不能少, 因为要编译出 c(c++)的头文件
- 每个方法前要加
//export 方法名
, 这里要注意//
和export
间不能有空格- 方法名和 go 的方法名必须完全一样
- 方法名不能是 c 内置的方法名, 比如
remove
就不行
编译
我在项目里内置了两个脚本, 一个是编译 android 的, 一个是编译 macOS 的, 因为篇幅和主题的原因, macOS 的就不单独拿出来了
看看编译成安卓的脚本吧
1export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/21.0.6113669
2
3export GOARCH=arm
4export GOOS=android
5export CGO_ENABLED=1
6export CC=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi21-clang
7go build -buildmode=c-shared -o output/android/armeabi-v7a/libadd.so add_library.go
8
9echo "Build armeabi-v7a success"
10
11export GOARCH=arm64
12export GOOS=android
13export CGO_ENABLED=1
14export CC=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang
15go build -buildmode=c-shared -o output/android/arm64-v8a/libadd.so add_library.go
16
17echo "Build arm64-v8a success"
简单来说 有几个环境变量要设置一下
- GOARCH=CPU 类型
- GOOS=设备类型
- CGO_ENABLED=1
- CC=ndk 里的 clang,需要是对应 CPU 版本的, API 呢建议是最低
- 这里单独说明一下, 高版本的 ndk 用的是 clang, 低版本用的是 gcc, 这个根据你情况来, 个人建议直接用
21.0.6113669
版本, 这个我测试过, 没有问题, ndk20 我也试过, 也是 ok 的
- 这里单独说明一下, 高版本的 ndk 用的是 clang, 低版本用的是 gcc, 这个根据你情况来, 个人建议直接用
- go build 命令
-buildmode=c-shared
构建类型, 使用go help buildmode
查看, 安卓一般用c-shared
就可以了, 意思就是 c 类型的共享库(动态库)- -o 后面跟输出的位置, 一般建议使用 libxxx.so 的格式, 相对的, 头文件也会被生成在同一目录下, xxx 就是库的名字, 对应到 Java 里加载库的方法就是
System.loadLibrary("xxx")
而不是~~System.loadLibrary("libxxx")
~~ - 最后一个就是需要编译的 go 文件了
CPU 类型和设备类型可以使用go tool dist list
查看
我这里 1.14.6 包含的 android 对应的是:
1$ go tool dist list|grep android
2android/386
3android/amd64
4android/arm
5android/arm64
arm 和 arm64 对应 v7 和 v8
386 amd64 应该对应的是 x86 和 x86_64, 这个没有实测, 如果有需要的朋友可以自己尝试一下
运行脚本./build_android_on_mac.sh
即可完成编译
安卓
创建项目
就是简单的在 Android Studio 中 New Project
CMakeLists.txt
自己创建一个CMakeLists.txt
, 注意大小写敏感, 其实这个文件名可以随便写, 但是约定俗成是这样, 建议不要变
1cmake_minimum_required(VERSION 3.10.2)
2
3project(android_lib)
4
5set(GO_BUILD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../go/output/android)
6set(SRC ${GO_BUILD_PATH}/${CMAKE_ANDROID_ARCH_ABI})
7
8include_directories(${SRC})
9
10find_library(
11 std-lib
12 android
13)
14
15message("CPU ABI: ${CMAKE_ANDROID_ARCH_ABI}")
16
17if (${CMAKE_ANDROID_ARCH_ABI} EQUAL "armeabi") ## 根据ABI的不同, 复制so文件到对应的文件夹
18 file(COPY ${GO_BUILD_PATH}/${CMAKE_ANDROID_ARCH_ABI}/libadd.so DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI}) # 复制库文件
19else ()
20 file(COPY ${SRC}/libadd.so DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI}) # 复制库文件
21endif ()
22file(COPY ${SRC}/libadd.h DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni) # 复制头文件
23
24file(GLOB JNI_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni/*.c ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni/*.h) # 扫描jni目录下的头文件和库文件, 并设置给JNI_SRC
25add_library(my_jni SHARED ${JNI_SRC}) # 添加jni中转库
26
27file(GLOB OUTPUT_LIBRARY libs/${CMAKE_ANDROID_ARCH_ABI}/*.so) # 找到对应cpu的so库文件
28
29target_link_libraries(
30 my_jni
31 ${std-lib} # 连接android标准库给jni, 因为golang用到了一些标准库的定义
32 ${OUTPUT_LIBRARY} # 连接给jni库
33)
我在其中加入了详细的注释, 其中具体的 Cmake 语法没法展开讲
编写代码
C 代码
首先是 c 代码, 我们在 CMake 中定义了一个 my_jni
的库, 这个库包含${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni
下所有的 c 和 h 文件
然后吧, 在src/main/jni
下创建 c 文件, 我这里定义的是 jni.c,别的什么也没关系
1#include <jni.h>
2#include "libadd.h"
3
4//
5// Created by jinglong cai on 2020/8/14.
6//
7
8JNIEXPORT jint JNICALL
9Java_top_kikt_usegolibrary_JniLibrary_add(JNIEnv *env, jclass clazz, jint x, jint y) {
10 return add(x, y);
11// return x + y;
12}
13
14JNIEXPORT jint JNICALL
15Java_top_kikt_usegolibrary_JniLibrary_remove(JNIEnv *env, jclass clazz, jint x, jint y) {
16 return remove_int(x, y);
17}
嗯, 其实就两个方法, 对应了 go 里定义的两个方法, 这里是 jni 的写法
Java 代码
JniLibrary.class
1package top.kikt.usegolibrary;
2
3public class JniLibrary {
4
5 static {
6 System.loadLibrary("my_jni");
7 }
8
9 public static native int add(int x, int y);
10
11 public static native int remove(int x, int y);
12
13}
这里的两个 native 方法就是指向jni.h
里定义的方法的
MainActivity.kt
1package top.kikt.usegolibrary
2
3import androidx.appcompat.app.AppCompatActivity
4import android.os.Bundle
5import kotlinx.android.synthetic.main.activity_main.*
6
7class MainActivity : AppCompatActivity() {
8 override fun onCreate(savedInstanceState: Bundle?) {
9 super.onCreate(savedInstanceState)
10 setContentView(R.layout.activity_main)
11
12 textView.text = "come from jni ${JniLibrary.add(300, 150)}"
13
14 textView2.text = "come from jni ${JniLibrary.remove(300, 150)}"
15 }
16
17
18}
就是简单的调用了这两个方法
运行
结果如下
后记
项目代码:
本篇为系列的第一篇, 不出意外, 后续应该会有对应的 iOS 篇, flutter-dart:ffi , wasm 篇则待定
以上