此篇博文记录android NDK调用系统库、第三方库实践记录,由于笔者在android上实现内录音,
此apk是运行在云手机上的后台java程序,直接编译到云手机的镜像中、自动运行。由于android系统
的声卡权限问题,故通过C++基于android系统frameworks编写自己的录音库,此库可以通过NDK调用。
实现内录音功能。
第一部分: 在android源码中、增加自己的so库
1.1 在 external 文件夹中创建 audio_record 文件夹,
此文件夹是库的目录,文件基本结构如下。
robot@ubuntu:~/android_build/external/audio_record$ tree -L 2
|-- Android.bp // 共享库编译规则
|-- ImxAudioRecord.cpp // 源文件
|-- ImxAudioRecord.h // 导出的头文件,供NDK工程调用
|-- DemoAudioRecord.cpp //> 调试库源文件
|-- backup-android.mk //> 调试库编译规则文件
|-- jni
`-- libs // NDK工程文件的依赖库
|-- Android.bp // 此文件是在android源码调用三方库规则
`-- arm64-v8a -> /home/robot/Server/NDK/app/build/intermediates/cxx/Debug/6m4c2jr7/obj/arm64-v8a
// 此 libs 文件夹是 NDK工程依赖so库文件,此处做了软连接;
1.2 创建 Android.bp 编译文件,内容如下:
创建自定义库首先需要创建 Android.bp 编译脚本配置文件。内容如下
cc_library_shared {
name: "libimxrecord",
compile_multilib: "both", //编译32与64库和兼容库
//compile_multilib: "32",
//compile_multilib: "64",
include_dirs: ["frameworks/av/include"],
shared_libs: [
"liblog",
"libutils",
"libcutils",
"libbinder",
"libmedia",
"libaudioutils",
"libaudiomanager",
"libaudioclient",
"libmediametrics",
],
export_include_dirs: [
"jni",
],
srcs: ["ImxAudioRecord.cpp"],
cflags: [
"-D_LIB",
"-DMULTICORE",
"-fPIC",
"-DANDROID",
"-Werror",
],
}
1.3 创建头文件与源文件
#ifndef IMX_AUDIO_RECORD_H_
#define IMX_AUDIO_RECORD_H_
#include<thread>
#include<queue>
#include <functional>
using funCB = std::function<void(void*,int)>;
class ImxAudioRecord {
public:
ImxAudioRecord();
~ImxAudioRecord();
private:
ImxAudioRecord * pImxAudioRecord = nullptr;
std::thread mAudioRecordThread;
bool g_bQuitAudioRecordThread = false;
int bufferSizeInBytes = 1024*4;
funCB m_CB;
int Init();
int start();
void stop();
void AudioRecordThread(void);
std::thread CreateAudioRecordThread(){
return std::thread(&ImxAudioRecord::AudioRecordThread,this);
}
public:
ImxAudioRecord* getInstance();
void exitInstance();
//> bind function
void setCB(funCB CB) { m_CB = CB; }
void runCB(void* b, int l){
if(m_CB != nullptr)
m_CB(b,l);
}
};
#endif //IMX_AUDIO_RECORD_H_
源文件在文章尾有gitee的链接地址,有需要的朋友可下载。
1.4 在android源码中的Product.mk文件中
添加对此库的依赖,否则编译整个镜像时,编译工具链不会编译此部分内容。
本例是在 @device/fsl/imx8q/ProductConfigCommon.mk 文件中,增加 libimxrecord.so 库依赖,内容如下
## 添加用户编写库文件至 system/lib 文件中。
PRODUCT_PACKAGES += \
libimxrecord \
## 添加依赖库到 vendor/lib64 和 lib 文件夹
PRODUCT_COPY_FILES += \
external/audio_record/libs/arm64-v8a/libmylib.so:$(TARGET_COPY_OUT_VENDOR)/lib/libmylib.so \
external/audio_record/libs/arm64-v8a/libc++_shared.so:$(TARGET_COPY_OUT_VENDOR)/lib/libc++_shared.so \
external/audio_record/libs/arm64-v8a/libscrcpy.so:$(TARGET_COPY_OUT_VENDOR)/lib/libscrcpy.so \
external/audio_record/libs/arm64-v8a/libmylib.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libmylib.so \
external/audio_record/libs/arm64-v8a/libc++_shared.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libc++_shared.so \
external/audio_record/libs/arm64-v8a/libscrcpy.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libscrcpy.so \
其中 PRODUCT_COPY_FILES 部分内容是 NDK工程依赖的几个so库内容,此配置是让android编译链把此内容拷贝至
系统文件的 vendor/lib 和 lib64 文件夹中,apk运行调用库时、就可以成功加载相关库文件。
1.5 在 android 源码中编译与调试
进入 external/audio_record/ 目录后,使用 mm 对此模块进行编译,生成的so文件内容和路径如下:
robot@ubuntu:~/android_build/out/target/product/mek_8q/obj/SHARED_LIBRARIES/libimxrecord_intermediates$ tree -L 1
.
|-- ImxAudioRecord.d
|-- ImxAudioRecord.o
|-- LINKED
|-- PACKED
|-- export_includes
|-- import_includes
|-- libimxrecord.so //> 此库是兼容库
|-- libimxrecord.so.32
|-- libimxrecord.so.64
|-- libimxrecord.so.debug
|-- libimxrecord.so.dynsyms
|-- libimxrecord.so.funcsyms
|-- libimxrecord.so.keep_symbols
|-- libimxrecord.so.mini_debuginfo.xz
|-- libimxrecord.so.toc
`-- link_type
如何调试so库呢,如果每次都把库push到手机中、在用代码调用库,调试有点麻烦,通过在mk文件使用 include $(BUILD_EXECUTABLE) 方式,
生成可执行文件,把可执行文件push到手机中,直接运行可执行文件,验证库逻辑代码运行正确,在使用 Android.bp 配置文件生成库文件。
至此,我就把在android源码中添加自己编写库的过程介绍清晰了,库做好后、我们怎么用呢?
第二部分: NDK自定义的so库
采用NDK制作库是有限制的,android内核很多功能是无法直接使用,所以上面介绍在android
源码中指针库的过程,此部分简单介绍如何使用NDK制作自己库文件。
2.1 创建库文件夹和文件
创建 user_cpp_lib 文件夹和相关内容,库编译后完整文件夹内容如下
robot@ubuntu:~/user_cpp_lib$ tree -L 2
|-- jni
| |-- Android.mk //> 库编译规则文件
| |-- Application.mk //> 通过变量APP_STL变量指定运libc++_shared
| |-- MyLib.cpp
| |-- MyLib.h
| `-- jniLibs
|-- libs
| |-- arm64-v8a
| |-- armeabi-v7a
| |-- x86
| `-- x86_64
|-- obj
| `-- local
`-- readme.md
2.2 创建头文件和源文件
头文件
user_cpp_lib/jni$ cat MyLib.h
#ifndef TESTNDK_MYLIB_H
#define TESTNDK_MYLIB_H
#include <string>
#include <stdlib.h>
class MyLib {
public:
MyLib();
~MyLib();
int add(int a,int b);
};
源文件
user_cpp_lib/jni$ cat MyLib.cpp
#include "MyLib.h"
MyLib::MyLib()
{}
MyLib::~MyLib()
{}
int MyLib::add(int a,int b)
{
return a+b;
}
2.3 创建编译规则文件mk
Application.mk 文件夹
user_cpp_lib/jni$ cat Application.mk
APP_STL:=c++_shared #指定库版本
APP_PLATFORM := android-16
APP_CPPFLAGS:=-frtti -fexceptions
APP_ABI := all
Android.mk 文件
user_cpp_lib/jni$ cat Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_C_INCLUDES:= \
~/android_build/frameworks/av/include \
~/android_build/development/ndk/platforms/android-21/include
#LOCAL_SHARED_LIBRARIES:= liblog libmedia libutils libcutils
LOCAL_SRC_FILES := \
MyLib.cpp
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := mylib
LOCAL_CFLAGS += -g
NDK_APP_DST_DIR := ./jniLibs/$(TARGET_ARCH_ABI) #生成库路径
include $(BUILD_SHARED_LIBRARY) #生成共享库
#include $(BUILD_EXECUTABLE)
2.4 编译自定义库
user_cpp_lib$ export ANDROID_NDK_ROOT=~/Android/Sdk/ndk/21.4.7075529 # 导出NDK工具版本
user_cpp_lib$ ${ANDROID_NDK_ROOT}/ndk-build #编译库
[arm64-v8a] Install : libmylib.so => jniLibs/arm64-v8a/libmylib.so
[arm64-v8a] Install : libc++_shared.so => libs/arm64-v8a/libc++_shared.so
[x86_64] Install : libmylib.so => jniLibs/x86_64/libmylib.so
[x86_64] Install : libc++_shared.so => libs/x86_64/libc++_shared.so
[armeabi-v7a] Install : libmylib.so => jniLibs/armeabi-v7a/libmylib.so
[armeabi-v7a] Install : libc++_shared.so => libs/armeabi-v7a/libc++_shared.so
[x86] Install : libmylib.so => jniLibs/x86/libmylib.so
[x86] Install : libc++_shared.so => libs/x86/libc++_shared.so
此部分介绍基于NDK编译私有库方法;接下来介绍如何在android studio的NDK工程中,如何使用私有库。
第三部分: 通过 android studio 创建 ndk 工程
使用android studio 创建ndk工程,不是本篇重点,故省略具体过程;假定工程创建完成且可以编译运行,如何在工程中引用私有库呢?
3.1 在CMakeList.txt文件中增加如下内容
cmake_minimum_required(VERSION 3.18.1)
project("scrcpy")
add_library(
scrcpy
SHARED
native-lib.cpp
com_metis_scrcpy_NativeHelper.cpp
com_metis_scrcpy_AAudioRecord.cpp
)
find_library(
log-lib
log)
add_library(
mylib # ndk 编译私有库
SHARED
IMPORTED)
set_target_properties(
mylib
PROPERTIES IMPORTED_LOCATION
${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libmylib.so
)
add_library(
imxrecord # android系统中编译私有库
SHARED
IMPORTED)
set_target_properties(
imxrecord
PROPERTIES IMPORTED_LOCATION
${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libimxrecord.so #把库文件拷贝至jniLibs/相关文件夹中
)
include_directories(src/main/cpp/include) # 把两个库的头文件拷贝至include文件夹中。
target_link_libraries( # Specifies the target library.
scrcpy
mylib # 链接
imxrecord
${log-lib})
android系统生成的32和64位版本库文件,分别把对应so文件拷贝到 android studio ndk工程中的jniLibs文件夹下对应arm64-v8a和armeabi-v7a 的文件
夹中,把头文件拷贝到工程的 cpp\include 文件夹下,ndk工程就能够调用你在android源码中编写的so文件。
笔者在实际使用时,使用的兼容库libimxrecord.so,拷贝至arm64-v8a和armeabi-v7a的文件夹。
3.2 在build.gradle中增加相关内容
defaultConfig {
applicationId "com.metis.scrcpy"
minSdk 24
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
//> 增加共享库
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_STL=c++_shared"
}
}
//> 指定引用库类型 exclude 'armeabi-v7a','armeabi'
ndk{
abiFilters 'arm64-v8a'
}
}
//> 设置库文件
sourceSets{
main{
jniLibs.srcDirs = ['libs']
}
}
笔者android studio 是2021.2版本的,因版本不同配置可能有差异,请读者针对具体版本调整。
在 native-lib.cpp程序中包含库文件的头文件后,就可以直接使用啦。
#include <jni.h>
#include <string>
#include "include/MyLib.h"
#include "include/ImxAudioRecord.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_metis_scrcpy_Server_stringFromJNI(
JNIEnv* env,
jclass clazz) {
//std::string hello = "Hello from C++";
//return env->NewStringUTF(hello.c_str());
MyLib myLib;
int res = myLib.add(10,10);
ImxAudioRecord mAudioRecord;
mAudioRecord.getInstance();
return env->NewStringUTF(std::to_string(res).c_str());
}
extern "C" JNIEXPORT jint JNICALL
Java_com_metis_scrcpy_Server_intFromJNI(JNIEnv *env, jclass clazz) {
// TODO: implement intFromJNI()
jint nativeInt = 20;
return nativeInt;
}
因笔者的apk不能用android界面相关内容,是通过 app_process 方式直接运行的java程序,
需要对android studio生成工程文件做如下修改。
3.3 修改工程的 manifests 文件
<manifest package="com.metis.scrcpy">
</manifest>
仅是一个java包。
3.4 删除 MainActivity.java文件
删除工程自动创建的mainActivity文件,新建 Server.java文件内容如下。
package com.metis.scrcpy;
public final class Server {
static {
try {
System.loadLibrary("scrcpy");
} catch (Exception e){
e.printStackTrace();
}
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
e.printStackTrace();
}
});
System.out.println("main startup ...");
String ss = NativeHelper.getAudioName(); //> 此处是调用 NDK编译私有库
System.out.println("hello world: " + ss);
int id = NativeHelper.getAudioSource(); //> 此处调用是系统环境下的私有库
System.out.println("AudioSource id: "+id);
}
//>
public static native String stringFromJNI();
}
NativeHelper类是把库调用做了封装,如此修改是把android兼容application工程,修改为java通用包,通过app_process直接启动运行。
第四步: 部署并测试库
步骤如下
4.1 编译android镜像并烧写至目标板
在 external/audio_record 文件夹中引用库、并把库打包至android镜像中;
4.2 编译工程并生成apk包
NDK$ ls app/release/
app-release.apk output-metadata.json
4.3 app_process 运行apk
通过adb push 文件到 /data/local/tmp 文件夹下,adb shell 进入console后,如下方法运行apk
hikay960pro:/data/local/tmp #
CLASSPATH=/data/local/tmp/app-release.apk app_process / com.metis.scrcpy.Server
总结与补充说明
各位读者看官朋友们,此工程 Demo 我放到 gitee.com 上,有需求朋友请自行下载参考。
最后麻烦读者朋友们如果本篇对你有帮助,请关注和点赞一下,当然如果有错误和不足的地方也可以拍砖。
补充说明:
把NDK工程中生成的库,软链接至 audio_record/libs/arm64-v8a 中、每次编译android镜像时自动打包库至系统镜像。
robot@ubuntu:~/android_build/external/audio_record/libs$ ls -l
total 8
-rw-rw-r-- 1 robot robot 241 May 24 19:12 Android.bp
lrwxrwxrwx 1 robot robot 92 May 24 18:49 arm64-v8a -> /home/robot/Server/NDK/app/build/intermediates/cxx/Debug/6m4c2jr7/obj/arm64-v8a