Android修炼系列(十九),来编译一个自己的 so 库吧

7,451 阅读4分钟

NDK 是 Android 提供的一个开发工具包,是一组使我们能将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具。NDK 能够从 C/C++ 源代码构建原生共享库(.so)或原生静态库(.a),并支持将静态库关联到其他库。JNI 是 Java 和 C/C++ 组件用于相互通信的接口。这样一来,通过 NDK 和 JNI ,我们就能很方便的在 Android 应用中使用 C 和 C++ 代码。

Android Studio 编译原生库的默认构建工具是 CMake,CMake 可适用于跨平台项目。由于很多现有项目都使用 ndk-build 构建工具包,因此 Android Studio 也支持了 ndk-build,相比于 CMake,ndk-build 速度更快,但仅支持 Android。

ndk-build

上面说的都抽象,我们来看个栗子吧:

  • 我定义了一个本地方法 stringFromJNI(),我想通过 c++ 代码来实现它,并通过 NDK 从这些 c++ 源码中构建出 .so 文件(命名随意),来供我的 java 层来调用,怎么做呢?
  /* com.blog.a.jni.HelloJni.java */
  public class HelloJni {
      public native String stringFromJNI();
  }

为了更直观,我先将整体的目录结构贴下:

结构.png

  • 首先我要知道 native 方法所在的 HelloJni.java 所对应的 .h 头文件,通过 javah 命令很容易得到。HelloJni.class 文件可以通过 javac 命令编译,在这里就直接拿 AS 自动编译好的了。
  $ cd /Users/zuomingjie/gitSpace/BlogSample/app/build/intermediates/javac/debug/classes/
  $ javah com.blog.a.jni.HelloJni
  • 这是 HelloJni.class 编译后的 .h 头文件,格式很讲究,Java 打头 + 类的全限定名 + 方法名,更多规范和 JNI 知识可查看 developer
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_blog_a_jni_HelloJni */

#ifndef _Included_com_blog_a_jni_HelloJni
#define _Included_com_blog_a_jni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_blog_a_jni_HelloJni
 * Method:    stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_blog_a_jni_HelloJni_stringFromJNI
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
  • 编写我们的 native-lib.cpp(命名随意), 实现这个头文件,这个方法就是返回一个字符串:
#include <jni.h>
#include <string>

#include "HelloJni.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_blog_a_jni_HelloJni_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
  • 编写 Application.mk 文件,更多其他配置参考 developers:
# 用于此应用的 C++ 标准库。
# 默认情况下使用 system STL。其他选项包括 c++_shared、c++_static 和 none
APP_STL := c++_shared
# 要为项目中的所有 C++ 编译传递的标记。这些标记不会用于 C 代码
APP_CPPFLAGS := -frtti -fexceptions
# 架构,全部:APP_ABI := all
APP_ABI := armeabi-v7a arm64-v8a
# 声明构建此应用所面向的 Android API 级别,并对应于应用的 minSdkVersion
APP_PLATFORM := android-19
  • 编写 Android.mk 文件:
# 此变量表示源文件在开发树中的位置。
# 在上述命令中,构建系统提供的宏函数 my-dir 将返回当前目录(Android.mk 文件本身所在的目录)的路径。
LOCAL_PATH := $(call my-dir)
# 声明 CLEAR_VARS 变量,其值由构建系统提供
# CLEAR_VARS 变量指向一个特殊的 GNU Makefile,后者会为我们清除许多 LOCAL_XXX 变量
include $(CLEAR_VARS)

# 变量存储您要构建的模块的名称
LOCAL_MODULE    := myJniTest
# 列举源文件,以空格分隔多个文件
LOCAL_SRC_FILES := cpp/native-lib.cpp

# 共享库
include $(BUILD_SHARED_LIBRARY)
  • 通过 ndk-build 命令构建 .so 文件(NDK 编译系统默认会在 $(APP_PROJECT_PATH)/jni 目录下寻找名为 Android.mk):
  $ cd /Users/zuomingjie/gitSpace/BlogSample/app/src/main/java/com/blog/a/jni
  $ ndk-build
  • 由于生成的 .so 文件在 jni 同级的 libs 目录下,我们可以直接拷贝至 jniLibs 目录,或者直接就在 gradle 文件中指定:
  sourceSets {
      main() { jniLibs.srcDirs = ['src/main/java/com/blog/a/libs'] }
  }

CMake

Android NDK 支持使用 CMake 编译我们的 C 和 C++ 代码,这也是在日常开发中最常见的方式。通过编写构建脚本 CMakeLists.txt,能非常方便的构建原生库或编译 .so/.a 文件。

这里还是看个栗子吧,为了直观,我先将整体的目录结构贴下:

ccc.png

这是我新创建的 CMakeLists.txt 文件,位置随意。其实当我们通过 AS 创建一个 Navive C++ 工程时,AS 会自动帮我们配置好 CMake 环境和生成 CMakeLists.txt 文件的:

cmake_minimum_required(VERSION 3.4.1)

# 将 native-lib.cpp 构建出 so共享库,并命名为 hello
add_library( # 构建的库的名字
             hello

             # 共享库
             SHARED

             # 库的原文件,这里与 CMakeLists.txt 同目录,直接就写 hello_lib.cpp 了
             hello_lib.cpp )

# 通过 find_library 来找到需要关联的三方库
find_library( # Sets the name of the path variable.
              log-lib

              # 需要关联的 so 名字
              log )

# 通过 link 可将源文件构建的库和三方库都加载进来
target_link_libraries( # 源文件库的名字
                       hello

                       # 三方库
                       ${log-lib} )
  • 在 gradle 文件中配置,CMakeLists 文件位置要一致:
  externalNativeBuild {
      cmake {
          path "src/main/java/com/blog/a/cpp/CMakeLists.txt"
          version "3.10.2"
      }
  }
  • 创建 java 对应的加载类,在这里直接使用静态代码快加载 libhello.so,这个 so 就是我们 CMakeLists 文件中添加的,是的,不需要像上面那样导出 so 文件:
public class HelloCMakeJni {
    static {
        System.loadLibrary("hello");
    }
    public native String stringFromCmakeJNI();
}
  • 创建我们的 native-lib.cpp,并实现 stringFromCmakeJNI 的逻辑,注意这里的命名规范,就不展开说了:
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_blog_a_cpp_HelloCMakeJni_stringFromCmakeJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++ By cMaker";
    return env->NewStringUTF(hello.c_str());
}
  • 最后就能在我们的 Activity 中调用了。并在 app/build/intermediates/cmake/debug/obj 目录下生成了 .so 文件,这里就不贴图了,直接可看 github BlogSample
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.jni_show_layout)
        findViewById<TextView>(R.id.sample_text).text = showJNIStr()
    }

    fun showJNIStr() = StringBuilder().apply {
        append(HelloCMakeJni().stringFromCmakeJNI())
        append("\n")
        append(HelloJni().stringFromJNI())
    }.toString()

调用源 cpp 方法

在刚刚 CMake 的基础上,我想再让 native-lib.cpp 去调用其他的 cpp 方法,这很正常吧,一个 so 库一般不可能仅有一个方法吧,这里写个栗子吧:

为了直观,我先将整体的目录结构贴下:

666.png

  • 编写头文件 method2.h,并定义方法 getHelloWorld:
#ifndef _GET_HELLO_WORLD_H_
#define _GET_HELLO_WORLD_H_

extern const char* getHelloWorld();

#endif
  • 编写 method2.cpp,实现 getHelloWorld 方法,这里仅仅返回一个 “HelloWorld” :
#include "method2.h"

extern const char* getHelloWorld() {
    return "HelloWorld";
}
  • 我们在 native-lib.cpp 中来调用这个方法:
#include <jni.h>
#include <string>

#include "method2.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_blog_a_cpp_HelloCMakeJni_stringFromCmakeJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = getHelloWorld();
    return env->NewStringUTF(hello.c_str());
}
  • 最后还需要在 CMakeLists.txt add_library中添加 method2.cpp:
add_library( # Sets the name of the library.
             hello

             # Sets the library as a shared library.
             SHARED

             method2.cpp
             hello_lib.cpp )

关联第三方so库

在实际场景下,我们的 c++ 源库可能还会引用一些其他的三方库,这该怎么做呢?举个例子吧:

在我们上面 CMake 的基础上,再关联上我们 libs 目录下 ndk-build 出的 so文件,为了直观,我先将整体的目录结构贴下:

888888.png

  • 首先将 so 库的待提供方法所对应的 .h 文件,放在 include 目录下,目的是,在源库 cpp 内需要时调用:
# 这样我们就能在源库内使用 so .h 内提供的方法了
#include "xxx.h"
  • 配置 CMakeLists.txt 文件,引入我们的 libmyJniTest.so, 这里是重点:
# 将我们的 .so 关联到我们的 hello_lib.cpp
include_directories(include)
# 导入三方库
add_library(myJniTest
            SHARED
            IMPORTED)
# 设置关联的 so 库名称、目标位置
# ${CMAKE_SOURCE_DIR} 是 CMakeLists.txt 所在目录
set_target_properties(myJniTest
                      PROPERTIES IMPORTED_LOCATION
                      ${CMAKE_SOURCE_DIR}/../libs/${ANDROID_ABI}/libmyJniTest.so )

# 通过 link 可将源文件构建的库和三方库都加载进来
target_link_libraries( # 源文件库的名字
                       hello
                       # 引用的三方库
                       myJniTest

                       # Links the target library to the log library
                       # included in the NDK.
                       # 三方库
                       ${log-lib} )
  • 通过上面两步,我们的 hello 库内就引入了 libmyJniTest.so 文件了。所以档 HelloJni 注释掉 loadLibrary ,依然能正常使用,因为在加载 libhello.so 时,就算加载了 libmyJniTest.so.
public class HelloJni {
    /* hello 库已经引用了 libmyJniTest.so,所以当 hello.so 加载后,myJniTest 自动就会被关联*/
    // static { System.loadLibrary("myJniTest"); }
    public native String stringFromJNI();
}

好了,本节就先说到这里吧,下节我们接着说带参数的 java c++ 调用,关联 .a 库 和 .a 库怎么才能够生成 .so 库。demo 我已经上传 github 了,有需要的 clone 查看吧。

本文到这里就结束了,希望对你有帮助。