Android NDK开发: 通过C/C++调用第三方so库

上一篇打包so库及jar包的博客我讲了如何将自己的代码打包成so库,并且配合jar包供他人调用。但那种方式仅适合对方从java层调用,如果算法是比较核心的,而又为了效率必须从native来调用,那种方式就不合适了。本篇讲如何打包我们自己的核心代码供他人在native调用,如果对方愿意,也可以自己封装然后从java来调用,灵活性更高。并且在调试的时候更加方便。这种方式是更接近纯C/C++工程的集成方式。

一、编写so库代码

第一步来编写so库的代码,等会儿将这个代码打包成so库供Android工程调用。这个代码比较简单,同样只返回一个字符串。为了步骤清楚,我们新建一个文件夹NDKSo,然后在里面新建一个so文件夹来盛放我们的so代码部分,so文件夹之外,我们存放测试代码。整体目录如下:

NDKSo
├── main.cpp
└── so
    ├── SoStringUtil.cpp
    └── SoStringUtil.h

main.cpp是我们用来调用代码测试so库代码是否正常工作的。
首先是头文件

#ifndef _SO_STRING_UTIL_H_
#define _SO_STRING_UTIL_H_

#include<iostream>
#include<stdlib.h>

using namespace std;


string getStringFromSoLibrary();

#endif

然后是cpp文件:

#include "SoStringUtil.h"

string getStringFromSoLibrary()
{
    return "Hello from shared library!";
}

然后是main.cpp

#include <iostream>
#include <stdlib.h>
#include "so/SoStringUtil.h"

using namespace std;

int main()
{
    cout << getStringFromSoLibrary() << endl;
    getchar();
    return 0;
}

如何测试呢?你可以用各种IDE什么的,这里推荐用VSCode,不过VSCode需要配置一番才可以运行调试C/C++代码。简单起见,我就不演示如何用VSCode调试了,直接用命令行编译输出。
新建一个在NDKSo的终端,我这里是macOS所以使用clang,Windows和Linux都建议使用GCC。
输入编译命令:

clang++ -g *.cpp so/*.cpp -o main.o

如何使用编译器命令不详细展开。
注意的一点是你使用的是C还是C++工程,如果是C++可以使用g++或者clang++,如果是C可以使用gcc或clang。这里推荐你用C++版本的编译命令,因为如果用C的编译命令而你使用了某些C++,编译会出问题。
编译成功后,就会出现main.o文件,它是个可执行文件。
如果你的报错了,那在这个阶段你就需要使用IDE来对代码进行调试了,所以这就是这种方式的优势所在。它的调试不依赖于Android工程,能够让你更专注于算法的实现。
运行这个main.o,就会出现我们期待的输出:

zus-MacBook-Air:NDKSo zu$ ./main.o
Hello from shared library!

OK,至此我们就完成了so库代码。

二、安装Android NDK

虽然完成了代码,但是如果要在手机上运行,就不能使用GCC/clang来编译so,必须使用NDK。如果你已经安装了NDK(开发Android的都会有吧),并且把NDK添加到环境变量里,就可以跳过这步。
首先无论通过什么方式,SDK manager或者人肉也好,把NDK下载下来。如果是用SDK manager,它是放在<AndroidSDK目录>/ndk这里的,可能会多一层文件夹用版本号命名,我们的目的是把ndk-build这个可执行文件添加到环境变量里。
我这边是有版本号的文件夹,完整的目录是~/AndroidSDK/ndk/20.0.5594570/~代表的是用户目录,在这个文件夹里就是ndk内容。
在windows下,把上面那个路径添加到Path下,重新启动cmd即可。
在Mac下,要编辑~/.bash_profile文件,在ubuntu下,要编辑~/.bashrc文件。这里我以Mac举例。
输入sudo vim ~/.bash_profile,输入密码后会使用vim打开~/.bash_profile文件,如果你从未编辑过这个文件,那它应该不存在,会自动新建一个。打开后,按i进入插入模式,输入

export NDK_HOME=/Users/zu/AndroidSDK/ndk/20.0.5594570/
export PATH=$PATH:$NDK_HOME

然后输入:wq!保存,在终端中输入source ~/.bash_profile更新后即可使用ndk-build,这时不会再提示找不到命令了。而是NDK提示你的其他错误,无论如何,ndk-build命令现在可用了。
当然vim还是比较难用,如果是ubuntu一般会有gedit这个编辑器,把vim换成gedit,就能以更自然的方式去编辑了。mac可以先安装VSCode,然后把VSCode添加到环境变量里(这个搜索一下,很简单),把vim换成code就可以使用VSCode打开了。

三、编译so库

到此可以编译so库了。依据NDK官方文档,目前有三种方式可以编译C/C++项目:Android.mk和Application.mk、makefile、gradle。但是如果仅使用NDK手动编译,就必须选择第一种方式,因此这一步我们需要首先编辑Android.mk和Application.mk文件。

3.1 编辑Android.mk

这个文件的详细信息可参阅NDK官方文档-Android.mk。简要地说,这个文件相当于对工程的配置,比如要编译的源码文件、编译的模块名称等。
新建一个Android.mk文件到so目录下,内容如下:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional

# 需要编译出的so的模块名
LOCAL_MODULE:= libndktest

# All of the source files that we will compile.
LOCAL_SRC_FILES:= \
	SoStringUtil.cpp

LOCAL_C_INCLUDES += \
    $(LOCAL_PATH)/SoStringUtil.h \
    

include $(BUILD_SHARED_LIBRARY)

3.2 编辑Application.mk

这个文件的详细信息可参考NDK官方文档-Application.mk。它指定了编译的一些参数以及模块配置。
同样位置在so目录下,内容如下:

APP_ABI := all
APP_OPTIM := release
APP_STL := c++_static
APP_CPPFLAGS := -frtti -fexceptions
APP_MODULES := libndktest
APP_BUILD_SCRIPT := Android.mk

要注意的是APP_BUILD_SCRIPT := Android.mk这一句,它指定了我们Android.mk的位置,Application.mk和Android.mk都在同级目录下,可以直接这样写。如果你的目录有差别,注意改这一句,规则和Linux下文件路径规则是一致的。
最后的目录是这样的

NDKSo
├── main.cpp
├── main.o
└── so
    ├── Android.mk
    ├── Application.mk
    ├── SoStringUtil.cpp
    └── SoStringUtil.h

3.3 编译

接下来,把终端切换到so目录下。由于NDK有一套默认的Application.mk和路径,因此如果要它适应我们自己的目录结构,就要自己设置我们的工程目录并且为它指明Applicatiom.mk,当如果没有设置,直接输ndk-build,它会提示你。
在这里插入图片描述
它提示找不到工程目录,需要定义一个NDK_PROJECT_PATH变量。
输入下面的命令来临时添加这个变量,目录位置就是so目录,由于我现在终端位置就在so目录里,因此直接用./即可。

export NDK_PROJECT_PATH=./

再输入ndk-build,它会提示找不到Android.mk文件,实际上NDK有一个自己的Application.mk文件,但是它并没有指向我们自己的Android.mk文件。
在这里插入图片描述
输入以下命令来为它指明我们的Application.mk文件。

ndk-build NDK_APPLICATION_MK=./Application.mk

注意的是,这个命令同时也会开始进行编译。终端里一堆输出。
在这里插入图片描述
如果没有错的话,会在so目录下生成libsobj这两个文件夹,在libs目录下就有我们需要的so库,由于我在Application.mk文件中ABI指定为all,因此现在最常用的arm和x86的32位、64位库都会被编译出来。

四、集成到Android工程中

首先,我们要新建一个支持C/C++的Android工程。如何建立这样一个工程,可参见我的上一篇博客Android NDK开发:打包so库及jar包供他人使用中关于为Android工程添加C++支持的部分。

我这里的工程名为NaiveSoTest。然后cpp部分仅有一个名为native-lib.cpp的文件。
在这里插入图片描述
接下来,按照国际惯例,把生成的so库放到app下的jniLibs目录里。
在这里插入图片描述

接下来就可以完善cpp部分的代码了。要在cpp中使用动态链接库,有两种方法,一种是dlsym方式来动态寻找并链接so库,灵活性非常高,甚至可以通过替换so库的方式来热修复或热更新核心代码,但是难度更高。第二种就是在编译的时候链接库,这里使用第二种方式。

首先,要想使用一个库,必须先知道它提供了哪些接口。这里有两种方式,第一就是我们把so库的头文件复制到Android工程的cpp文件夹中,这种是最方便的,不过这个方式要求你在CMakeLists文件中设置好包含文件夹include_directories。第二种方式就是我们在任何一个文件中单次声明so里的方法,但是不用实现它,编译的时候编译器会去库中查找它的实现。

我在native-lib.cpp中的代码如下:

#include <jni.h>
#include <stdlib.h>
#include <iostream>
#include "SoStringUtil.h"

using namespace std;

//如果你不想用引入头文件的方法,可以把导入头文件的include语句注释掉,然后将下面这句取消注释。
//string getStringFromSoLibrary();

extern "C"
JNIEXPORT jstring
JNICALL
Java_com_example_nativesotest_MainActivity_nGetStringFromSo(JNIEnv *env, jobject instance)
{
    string result = getStringFromSoLibrary();
    return env->NewStringUTF(result.c_str());
}

在MainActivity中这样调用(Kotlin代码):

class MainActivity : AppCompatActivity() {

    private lateinit var tvContent: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvContent = findViewById(R.id.tv_content)
        tvContent.text = nGetStringFromSo()
    }

    external fun nGetStringFromSo(): String

    companion object{
        init {
            System.loadLibrary("native-lib")
        }
    }
}

然后修改CMakeLists.txt来设置链接。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

include_directories(src/main/cpp/)
file(GLOB CPP_FILES "src/main/cpp/*.cpp")

# 添加so库存放位置
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../jniLibs)

# 添加一个库,它链接我们的so文件
add_library( sotest
        SHARED
        IMPORTED )

# 给sotest这个库设置so文件链接的位置
set_target_properties( sotest
        PROPERTIES IMPORTED_LOCATION
        ../../../../jniLibs/${ANDROID_ABI}/libndktest.so )


# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )


# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib
                       sotest
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib}
                       )

主要的点我都用中文注释写清楚了。需要注意的是,这个CMakeLists和上一篇文章中有些许不一样,首先就是注释掉了导出so库的语句。另外上一篇只是单纯地导出so库,因此并没有寻找log库以及链接等一系列操作。

然后是app的build.gradle文件,这个和上一篇文章中的也是有差别的。上一篇中,在sdk里我们只是编译so库而不涉及链接,因此只设置了NDK的编译选项。在app里我们只是导入so库并链接,但是并没有设置NDK的编译选项。在这篇文章中,我们的工程里既要导入外来的so库,这就需要设置jniLibs的位置,同时我们自己也要编译出so库,所以你也需要设置NDK的编译参数。下面放一个完整的gradle。

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.1"
    defaultConfig {
        applicationId "com.example.nativesotest"
        minSdkVersion 24
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        // 设置ndk编译的cpu架构
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    //设置CMakeLists文件的位置
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
    //我们将外部so库放在jniLibs文件夹下,因此要将它设置为jniLibs使工程在打包的时候能将它包含进去,否则app运行时会报无法找到so库的错误。
    sourceSets {
        main {
            jniLibs.srcDirs = ['jniLibs']
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

其中的原理想一下其实也很简单。除了我们自己编译的外部so库,工程中自己的cpp代码也是会编译成一个so库的,因此需要设置ABI和CMakeLists的位置等,编译的同时将它和外部so库进行链接,这部分是在CMakeLists中设置的。然后在运行时,natibe-lib.so就会去链接外部so库,因此需要设置jniLibs来保证外部so库也被打包进去,否则届时就会报错找不到so库。
然后运行看一下结果:
在这里插入图片描述

运行无误。

然后可以用adb进入应用的安装目录看一下是否有两个so库。不过不知为何我的虚拟机没法root了,在windows上是OK的,所以暂时看不了。

源码可以看我的github

  • 7
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值