MQTT 源码移植

目标

在应用层使用mqtt库和平台通信。

移植原因

mqtt java端接口匮乏,不能定制

一. 源码下载

1
git clone https://github.com/eclipse/paho.mqtt.c.git

主要用的就是paho.mqtt.c 是C语言实现的一个库,也有Java的,本次移植主要是基于C端,原因就是更为灵活。

二. 新建一个Android Studio工程

模块化考虑,在新建的工程里新建一个Android Native module,用于封装mqtt的库,提供java接口,目录结构如下图

image-20231115193730943

三. 参考源码提供的Android.mk

image-20231115182011223

Android.mk的主要内容,对于应用层的移植,我们需要根据这个文件转换成CMakeList.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 通用源文件列表,每个库都会依赖
libpaho-mqtt3_local_src_c_files_common := \
$(libpaho-mqtt3_lib_path)/MQTTProtocolClient.c \
$(libpaho-mqtt3_lib_path)/Tree.c \
$(libpaho-mqtt3_lib_path)/Heap.c \
$(libpaho-mqtt3_lib_path)/MQTTPacket.c \
$(libpaho-mqtt3_lib_path)/Clients.c \
$(libpaho-mqtt3_lib_path)/Thread.c \
$(libpaho-mqtt3_lib_path)/utf-8.c \
$(libpaho-mqtt3_lib_path)/StackTrace.c \
$(libpaho-mqtt3_lib_path)/MQTTProtocolOut.c \
$(libpaho-mqtt3_lib_path)/Socket.c \
$(libpaho-mqtt3_lib_path)/Log.c \
$(libpaho-mqtt3_lib_path)/Messages.c \
$(libpaho-mqtt3_lib_path)/LinkedList.c \
$(libpaho-mqtt3_lib_path)/MQTTPersistence.c \
$(libpaho-mqtt3_lib_path)/MQTTPacketOut.c \
$(libpaho-mqtt3_lib_path)/SocketBuffer.c \
$(libpaho-mqtt3_lib_path)/MQTTPersistenceDefault.c \

# 下面是声明的文件列表,分别用于生成不同的库
# libpaho-mqtt3_local_src_c_files_c 文件列表
libpaho-mqtt3_local_src_c_files_c := \
$(libpaho-mqtt3_lib_path)/MQTTClient.c \
# libpaho-mqtt3_local_src_c_files_cs 文件列表
libpaho-mqtt3_local_src_c_files_cs := \
$(libpaho-mqtt3_lib_path)/MQTTClient.c \
$(libpaho-mqtt3_lib_path)/SSLSocket.c \
# libpaho-mqtt3_local_src_c_files_a 文件列表
libpaho-mqtt3_local_src_c_files_a := \
$(libpaho-mqtt3_lib_path)/MQTTAsync.c \
# libpaho-mqtt3_local_src_c_files_as 文件列表
libpaho-mqtt3_local_src_c_files_as := \
$(libpaho-mqtt3_lib_path)/MQTTAsync.c \
$(libpaho-mqtt3_lib_path)/SSLSocket.c \

######################################### 静态库 #####################################################
.......................................................................................................
######################################### 动态库 #####################################################

# libpaho-mqtt3c
include $(CLEAR_VARS)
LOCAL_MODULE := libpaho-mqtt3c
LOCAL_SHARED_LIBRARIES := libdl
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/$(libpaho-mqtt3_lib_path)
LOCAL_C_INCLUDES:= $(libpaho-mqtt3_c_includes)
LOCAL_SRC_FILES := $(libpaho-mqtt3_local_src_c_files_common) $(libpaho-mqtt3_local_src_c_files_c)
include $(BUILD_SHARED_LIBRARY)
# libpaho-mqtt3cs
include $(CLEAR_VARS)
LOCAL_MODULE := libpaho-mqtt3cs
LOCAL_SHARED_LIBRARIES := libcrypto libssl libdl
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/$(libpaho-mqtt3_lib_path)
LOCAL_C_INCLUDES:= $(libpaho-mqtt3_c_includes)
LOCAL_CFLAGS += -DOPENSSL
LOCAL_SRC_FILES := $(libpaho-mqtt3_local_src_c_files_common) $(libpaho-mqtt3_local_src_c_files_cs)
include $(BUILD_SHARED_LIBRARY)
# libpaho-mqtt3a
include $(CLEAR_VARS)
LOCAL_MODULE := libpaho-mqtt3a
LOCAL_SHARED_LIBRARIES := libdl
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/${libpaho-mqtt3_lib_path}
LOCAL_C_INCLUDES:= $(libpaho-mqtt3_c_includes)
LOCAL_SRC_FILES := $(libpaho-mqtt3_local_src_c_files_common) $(libpaho-mqtt3_local_src_c_files_a)
include $(BUILD_SHARED_LIBRARY)
# libpaho-mqtt3as
include $(CLEAR_VARS)
LOCAL_MODULE := libpaho-mqtt3as
LOCAL_SHARED_LIBRARIES := libcrypto libssl libdl
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/${libpaho-mqtt3_lib_path}
LOCAL_CFLAGS += -DOPENSSL
LOCAL_C_INCLUDES:= $(libpaho-mqtt3_c_includes)
LOCAL_SRC_FILES := $(libpaho-mqtt3_local_src_c_files_common) $(libpaho-mqtt3_local_src_c_files_as)
include $(BUILD_SHARED_LIBRARY)

基本上,Android.mk中的重点就是上面的描述了,包含了几个模块:

1
2
3
1. 通用源码列表
2. 不同库的源码列表
3. 生成不同库的依赖

库的作用如下:

1
2
3
4
paho-mqtt3a : 一般实际开发中就是使用这个,a表示的是异步消息推送(asynchronous)。
paho-mqtt3as : as表示的是 异步+加密(asynchronous+OpenSSL)。
paho-mqtt3c : c 表示的应该是同步(Synchronize),一般性能较差,是发送+等待模式。
paho-mqtt3cs : cs表示的是同步+加密(asynchronous+OpenSSL)。

转换成CMakeLists如下:这是最终的CMakeList.txt,具体的过程,请继续往下看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.

# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)

# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("mqtt")
message("openssl/${CMAKE_ANDROID_ARCH_ABI}/include")
# 包含子目录相关头文件
include_directories(mqttsrc)

# 通用源文件
set(libpaho-mqtt3_local_src_c_files_common
mqttsrc/MQTTProtocolClient.c
mqttsrc/Tree.c
mqttsrc/Heap.c
mqttsrc/MQTTPacket.c
mqttsrc/Clients.c
mqttsrc/Thread.c
mqttsrc/utf-8.c
mqttsrc/StackTrace.c
mqttsrc/MQTTProtocolOut.c
mqttsrc/Socket.c
mqttsrc/Log.c
mqttsrc/Messages.c
mqttsrc/LinkedList.c
mqttsrc/MQTTPersistence.c
mqttsrc/SocketBuffer.c
mqttsrc/MQTTPersistenceDefault.c
mqttsrc/MQTTProperties.c
mqttsrc/MQTTTime.c
mqttsrc/MQTTPacketOut.c
mqttsrc/WebSocket.c
mqttsrc/Base64.c
mqttsrc/Proxy.c
mqttsrc/SHA1.c
)

# libpaho-mqtt3_local_src_c_files_c
set(libpaho-mqtt3_local_src_c_files_c
mqttsrc/MQTTClient.c
)

# libpaho-mqtt3_local_src_c_files_cs
set(libpaho-mqtt3_local_src_c_files_cs
mqttsrc/MQTTClient.c
mqttsrc/SSLSocket.c
)

# libpaho-mqtt3_local_src_c_files_a
set(libpaho-mqtt3_local_src_c_files_a
mqttsrc/MQTTAsyncUtils.c
mqttsrc/MQTTAsync.c
)

# libpaho-mqtt3_local_src_c_files_as
set(libpaho-mqtt3_local_src_c_files_as
mqttsrc/MQTTAsyncUtils.c
mqttsrc/MQTTAsync.c
mqttsrc/SSLSocket.c
)
# 导入预编译库
# crypto
add_library(crypto SHARED IMPORTED)
set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${CMAKE_ANDROID_ARCH_ABI}/libcrypto.so)
## openssl
add_library(openssl SHARED IMPORTED)
set_target_properties(openssl PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${CMAKE_ANDROID_ARCH_ABI}/libssl.so)

add_library(libpaho-mqtt3c SHARED
${libpaho-mqtt3_local_src_c_files_common}
${libpaho-mqtt3_local_src_c_files_c}
)
target_link_libraries(libpaho-mqtt3c dl)

add_library(libpaho-mqtt3cs SHARED
${libpaho-mqtt3_local_src_c_files_common}
${libpaho-mqtt3_local_src_c_files_cs}
)
target_link_libraries(libpaho-mqtt3cs openssl dl)


add_library(libpaho-mqtt3a SHARED
${libpaho-mqtt3_local_src_c_files_common}
${libpaho-mqtt3_local_src_c_files_a}
)
target_link_libraries(libpaho-mqtt3a dl)
#
add_library(libpaho-mqtt3as SHARED
${libpaho-mqtt3_local_src_c_files_common}
${libpaho-mqtt3_local_src_c_files_as}
)
target_link_libraries(libpaho-mqtt3as openssl dl)
#
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
${SOURCE_FILES}
mqtt.cpp)
target_include_directories(mqtt PUBLIC openssl/${CMAKE_ANDROID_ARCH_ABI}/include)
#
target_link_libraries(mqtt
libpaho-mqtt3c
libpaho-mqtt3cs
libpaho-mqtt3a
libpaho-mqtt3as
dl
crypto
openssl
android
log)

四. openssl编译

保存下面内容为mkssl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/bin/sh

while getopts n:o:a:t:d:h: option
do
case "${option}"
in
n) ANDROID_NDK=${OPTARG};;
o) OPENSSL_VERSION=${OPTARG};;
a) API_LEVEL=${OPTARG};;
t) BUILD_TARGETS=${OPTARG};;
d) OUT_DIR=${OPTARG};;
h) HOST_TAG=${OPTARG};;
*) twentytwo=${OPTARG};;
esac
done

echo "ANDROID_NDK = $ANDROID_NDK"

echo "OPENSSL_VERSION= $OPENSSL_VERSION"
echo "API_LEVEL = $API_LEVEL"

echo "BUILD_TARGETS = $BUILD_TARGETS"

echo "OUT_DIR= $OUT_DIR"

echo "HOST_TAG = $HOST_TAG"

echo "twentytwo=$twentytwo"



BUILD_DIR=/tmp/openssl_android_build

if [ ! -d openssl-${OPENSSL_VERSION} ]
then
if [ ! -f openssl-${OPENSSL_VERSION}.tar.gz ]
then
wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz || exit 128
fi
tar xzf openssl-${OPENSSL_VERSION}.tar.gz || exit 128
fi

cd openssl-${OPENSSL_VERSION} || exit 128


##### export ndk directory. Required by openssl-build-scripts #####
case ${OPENSSL_VERSION} in
1.1.1*)
export ANDROID_NDK_HOME=$ANDROID_NDK
;;
*)
export ANDROID_NDK_ROOT=$ANDROID_NDK
;;
esac

export PATH=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG/bin:$PATH

##### build-function #####
build_the_thing() {
make clean
./Configure $SSL_TARGET -D__ANDROID_API__=$API_LEVEL && \
make -j128 SHLIB_EXT=.so && \
make install SHLIB_EXT=.so DESTDIR=$DESTDIR || exit 128
}

##### set variables according to build-tagret #####
for build_target in $BUILD_TARGETS
do
case $build_target in
armeabi-v7a)
DESTDIR="$BUILD_DIR/armeabi-v7a"
SSL_TARGET="android-arm"
;;
x86)
DESTDIR="$BUILD_DIR/x86"
SSL_TARGET="android-x86"
;;
x86_64)
DESTDIR="$BUILD_DIR/x86_64"
SSL_TARGET="android-x86_64"
;;
arm64-v8a)
DESTDIR="$BUILD_DIR/arm64-v8a"
SSL_TARGET="android-arm64"
;;
esac

rm -rf $DESTDIR
build_the_thing
#### copy libraries and includes to output-directory #####
mkdir -p $OUT_DIR/$build_target/include
cp -R $DESTDIR/usr/local/include/* $OUT_DIR/$build_target/include
cp -R $DESTDIR/usr/local/ssl/* $OUT_DIR/$build_target/
mkdir -p $OUT_DIR/$build_target/lib
cp -R $DESTDIR/usr/local/lib/*.so $OUT_DIR/$build_target/lib
cp -R $DESTDIR/usr/local/lib/*.a $OUT_DIR/$build_target/lib
done

echo Success

chmod 777 mkssl

执行命令编译: 例如

1
./mkssl -n /data/android/Sdk/ndk/25.2.9519653 -a 21 -t "armeabi-v7a" -o 1.1.1l -d ./ -h linux-x86_64

根据不同的需求更改-t 后面的参数,例如x86

1
./mkssl -n /data/android/Sdk/ndk/25.2.9519653 -a 21 -t "x86" -o 1.1.1l -d ./ -h linux-x86_64

脚本的参数含义如下:

1
2
3
4
5
6
7
8
9
10
11
-n: NDK的根目录

-a:ABI 的级别

-t:ABI

-o:openssl的版本

-d:输出目录

-h:host的架构

五. 拷贝动态库到指定目录

参考第一张图,将openssl生成的动态库拷贝到mqtt目录下的libs中
image-20231115194629348

六. 拷贝头文件

参考第一张图,将openssl生成的头文件拷贝到cpp中
image-20231115194756254

七. 在cmake中包含这些头文件

关键语法如下:这句话的意思就是编译的时候根据不同的ABI去引用不同的头文件,我们这里包含了四个ABI的头文件

1
target_include_directories(mqtt PUBLIC openssl/${CMAKE_ANDROID_ARCH_ABI}/include)

八. 编译就行了

九. 封装待续…….

MMKV简介

1
2
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微
信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。

github地址:https://github.com/Tencent/MMKV

通过github的中文文档查看编译指导:https://github.com/Tencent/MMKV/blob/master/README_CN.md

直接查看POSIX 指南(POSIX是可移植操作系统接口是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称)

POSIX 安装指南原文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
POSIX 安装指南
基本要求
MMKV 支持 Linux(Ubuntu, Arch Linux, CentOS, Gentoo)、Unix(macOS, FreeBSD, OpenBSD) 等 POSIX 平台;
MMKV 需使用 CMake 3.8.0 或以上进行编译;
C++ 编译器需支持 C++ 17 标准。
通过 CMake 安装引入
获取 MMKV 源码:

git clone https://github.com/Tencent/MMKV.git
打开你项目的 CMakeLists.txt, 添加这几行:

add_subdirectory(mmkv/POSIX/src mmkv)
target_link_libraries(MyApp mmkv)
添加头文件 #include "MMKV.h",就可以愉快地开始你的 MMKV 之旅了。

注意:

你也可以编译运行 demo 工程来测试 MMKV:

cd mmkv/POSIX
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make
cd demo && ./demo
MMKV 依赖 zlib 库。如果你的系统没有安装 zlib1g-dev 库也不用担心,MMKV 会使用内置的精简版(zlib v1.2.11)。

如果你确定不需要加密功能,你可以在Core/MMKVPredef.h 文件中打开宏MMKV_DISABLE_CRYPT,以减小一些二进制大小。

简略编译过程(将mmkv源码引入 ndk编译)

1
2
3
4
5
1. git clone https://github.com/Tencent/MMKV.git
2. 创建一个空的Android 工程
3. 创建一个moudle,选择Android Native Livrary
4. 将mmkv中的Core目录直接复制到新建的moudle中
5. 修改CMakeList.txt

关键部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.

# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)

# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("mmkv")

# 添加头文件,方便自己的源码中引用Core下的头文件
include_directories(Core)

# 添加mmkv的子CMakeList,在编译时会首先执行子目录的CMakeList
add_subdirectory(Core)

# 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.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
# 添加源文件,这个源文件可加可不加,随意
libmmkv.cpp
mmkv.cpp)

# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
mmkv
core
# pthread 这里注意,不能链接pthread,在NDK中使用stdc++
stdc++
# List libraries link to the target library
android
log)

然后直接编译项目即可,具体封装可按照mmkv的api文档进行。详细内容可直接给项目导入Android studio查看

libusb简介

1
2
3
4
libusb 是一个 C 库,提供对 USB 设备的通用访问。开发人员旨在使用它来促进与 USB 硬件通信的应用程序的生产。
它是可移植的:使用单个跨平台 API,它提供对 Linux、macOS、Windows 等 USB 设备的访问
它是用户模式:应用程序与设备通信不需要特殊权限或提升权限。
它与版本无关:支持从 1.0 到 3.1(最新)的所有 USB 协议版本。

官网:https://libusb.info/

仓库:https://github.com/libusb/libusb/tree/master

git clone https://github.com/libusb/libusb.git

编译

官方的android目录下的编译指导文档原文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
libusb for Android
==================

Building:
---------

To build libusb for Android do the following:

1. Download the latest NDK from:
http://developer.android.com/tools/sdk/ndk/index.html

2. Extract the NDK.

3. Open a shell and make sure there exist an NDK global variable
set to the directory where you extracted the NDK.

4. Change directory to libusb's "android/jni"

5. Run "$NDK/ndk-build".

The libusb library, examples and tests can then be found in:
"android/libs/$ARCH"

Where $ARCH is one of:
armeabi
armeabi-v7a
mips
mips64
x86
x86_64

Installing:
-----------

If you wish to use libusb from native code in own Android application
then you should add the following line to your Android.mk file:

include $(PATH_TO_LIBUSB_SRC)/android/jni/libusb.mk

You will then need to add the following lines to the build
configuration for each native binary which uses libusb:

LOCAL_C_INCLUDES += $(LIBUSB_ROOT_ABS)
LOCAL_SHARED_LIBRARIES += libusb1.0

The Android build system will then correctly include libusb in the
application package (APK) file, provided ndk-build is invoked before
the package is built.


Runtime Permissions:
--------------------

The Runtime Permissions on Android can be transferred from Java to Native
over the following approach:

JAVA:

--> Obtain USB permissions over the android.hardware.usb.UsbManager class

usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
for (UsbDevice usbDevice : deviceList.values()) {
usbManager.requestPermission(usbDevice, mPermissionIntent);
}

--> Get the native FileDescriptor of the UsbDevice and transfer it to
Native over JNI or JNA

UsbDeviceConnection usbDeviceConnection = usbManager.openDevice(camDevice);
int fileDescriptor = usbDeviceConnection.getFileDescriptor();

--> JNA sample method:

JNA.INSTANCE.set_the_native_Descriptor(fileDescriptor);

NATIVE:

--> Initialize libusb on Android

set_the_native_Descriptor(int fileDescriptor) {
libusb_context *ctx;
libusb_device_handle *devh;
libusb_set_option(&ctx, LIBUSB_OPTION_NO_DEVICE_DISCOVERY, NULL);
libusb_init(&ctx);
libusb_wrap_sys_device(NULL, (intptr_t)fileDescriptor, &devh);
}
/* From this point you can regularly use all libusb functions as usual */

About LIBUSB_OPTION_NO_DEVICE_DISCOVERY:

The method libusb_set_option(&ctx, LIBUSB_OPTION_NO_DEVICE_DISCOVERY, NULL)
does not affect the ctx.
It allows initializing libusb on unrooted Android devices by skipping
the device enumeration.

Rooted Devices:
---------------

For rooted devices the code using libusb could be executed as root
using the "su" command. An alternative would be to use the "su" command
to change the permissions on the appropriate /dev/bus/usb/ files.

Users have reported success in using android.hardware.usb.UsbManager
to request permission to use the UsbDevice and then opening the
device. The difficulties in this method is that there is no guarantee
that it will continue to work in the future Android versions, it
requires invoking Java APIs and running code to match each
android.hardware.usb.UsbDevice to a libusb_device.

For a rooted device it is possible to install libusb into the system
image of a running device:

1. Enable ADB on the device.

2. Connect the device to a machine running ADB.

3. Execute the following commands on the machine
running ADB:

# Make the system partition writable
adb shell su -c "mount -o remount,rw /system"

# Install libusb
adb push obj/local/armeabi/libusb1.0.so /sdcard/
adb shell su -c "cat > /system/lib/libusb1.0.so < /sdcard/libusb1.0.so"
adb shell rm /sdcard/libusb1.0.so

# Install the samples and tests
for B in listdevs fxload xusb sam3u_benchmark hotplugtest stress
do
adb push "obj/local/armeabi/$B" /sdcard/
adb shell su -c "cat > /system/bin/$B < /sdcard/$B"
adb shell su -c "chmod 0755 /system/bin/$B"
adb shell rm "/sdcard/$B"
done

# Make the system partition read only again
adb shell su -c "mount -o remount,ro /system"

# Run listdevs to
adb shell su -c "listdevs"

4. If your device only has a single OTG port then ADB can generally
be switched to using Wifi with the following commands when connected
via USB:

adb shell netcfg
# Note the wifi IP address of the phone
adb tcpip 5555
# Use the IP address from netcfg
adb connect 192.168.1.123:5555

执行编译

1.cd libusb
2.cd android/jni
3.找到自己的ndk目录,例如下面直接使用绝对路径 /home/tyl/Android/Sdk/ndk/26.1.10909125
4./home/tyl/Android/Sdk/ndk/26.1.10909125/ndk-build
5.生成的so文件在libusb/android/libs中

编译好的库可以直接集成到Android中了。

opus官网:https://opus-codec.org

github:https://github.com/xiph/opus

opus简介

1
2
Opus 是一个完全开放、免版税、高度通用的音频编解码器。Opus 在互动方面无与伦比 通过互联网传输语音和音乐,但也用于存储和流式传输
应用。它被互联网工程任务组 (IETF) 标准化为 RFC 6716,它结合了 Skype 的 SILK 编解码器和 Xiph.Org 的 CELT 编解码器的技术;

下载opus

1
2
3
1.官网download页面找到Source code: opus-1.4.tar.gz,点击后自动下载
2.通过github的cmake页面内的指南:git clone https://gitlab.xiph.org/xiph/opus
(https://github.com/xiph/opus/tree/master/cmake)

编译opus

1
2
3
4
5
6
7
8
9
10
11
12
通过查看cmake内的编译介绍
1.cd opus
2.mkdir build
3.cd build
4.cmake .. -DCMAKE_TOOLCHAIN_FILE=/home/tyl/Android/Sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a
//需要修改为本机的ndk路径,执行成功
5.make
//执行完成后在build目录中就会看到libopus.a
//如果需要so文件则需要进行配置 -DOPUS_BUILD_SHARED_LIBRARY=y
6.cmake .. -DCMAKE_TOOLCHAIN_FILE=/home/tyl/Android/Sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DOPUS_BUILD_SHARED_LIBRARY=y
7.make
//执行完毕后build目录下可以看到libopus.so文件

自建编译脚本android.sh (自定义名字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash

# 每次编译删除原来的编译文件
rm build -rf
rm install -rf
# 创建临时编译目录,避免污染源文件
mkdir build
# 定义一个数组,存储架构类型,用来循环编译
ARCH_ARRAY=(armeabi-v7a arm64-v8a x86 x86_64)
mkdir install
cd build

for item in "${ARCH_ARRAY【@】}"; do
mkdir -p install/$item
echo "$item"
echo "$ANDROID_HOME"
rm * -rf
cmake .. \
-DCMAKE_TOOLCHAIN_FILE=${ANDROID_HOME}/ndk/26.1.10909125/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=$item \
-DOPUS_BUILD_SHARED_LIBRARY=y
make -j8
mkdir -p ../install/$item
mv *.so ../install/$item/
done

# 拷贝头文件到安装目录
cp ../include -rf ../install

一. NDK开发之JavaVM

​ JNI定义了两个关键数据结构,一个是JavaVM,一个是JNIEnv,在前面我们看jni.h的时候也看到了,只是没有仔细查看他们的定义,这里先看第一个JavaVM,本质上,JavaVM是一个指向函数表的二级指针(在C++版本中,它们是一些类,这些类具有指向函数表的指针,并具有每个通过该函数表间接调用的JNI函数的成员函数。)JavaVM提供”调用接口”函数,我们可以利用此类的函数创建和销毁JavaVM。理论上,每个进程可以有多个JavaVM,但在Android中只允许有一个。

上面这段话为官方原话,但对于初学者来讲,可能还不是很好理解,所以我们就以官方所描述的内容进行讲解分析。

首先,说JavaVM本质上是一个二级指针,也就是我们说的指针的指针;

首先说指针

1
2
3
4
5
概念,它其实就是内存地址,比如说我们有一个变量,存放在某个位置,那么这个位置的地址就是这个位置的指针,指向这个位置。举个最简单
的例子就是,我们一般会在C语言中申请一块空间来存储东西,申请空间一般用malloc,这个函数返回了一个void *类型的返回值,返回的
这个值就是这块空间的地址。

我们拿着这个地址就可以去操作这块空间了。

指针变量

1
2
指针变量首先是一个变量,它和我们平时定义的int,char,float,double等等变量没有任何区别,本身都是用来存数据的,只是int,
char,float,double等存的是一个值,而指针变量存放的是一个地址,这个地址指向了一块空间。

理解了这两个概念,我们再说几个常见的概念:

1
2
函数指针:指向函数的指针,我们拿着这个指针就可以直接调用这个函数。
结构体指针:指向结构体的指针,我们拿着这个指针就可以操作结构体。

所以,简单理解,我们有指针就可以做相关的操作。

指针和指针的指针(所谓二级指针):

指针,也叫一级指针,它指向的是直接可以操作的内存空间。

指针的指针,也叫二级指针,它指向的是一级指针,一级指针指向的是可以操作的内存空间。如下图,一级指针直接指向可操作的内存空间,指针的指针是指向了指针,然后通过指针再指向可操作的内存空间(图中的102也是一个指针)。

image-20230813102620884

在了解了上面的基本概念之后,补充一下在C中和C++中调用上的一些区别,我们看一下jni.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* JNI invocation interface.
*/
struct JNIInvokeInterface {
void* reserved0;
void* reserved1;
void* reserved2;

jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

/*
* C++ version.
*/
struct _JavaVM {
const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};


// 这里,可以看出来,对于C++来说,JavaVM是一个结构体(C++中结构体和C中的不太一样,C++中允许有函数,和类相似),结构体中有很
//多的成员函数,这个类等于说是一个代理,它帮我们调用了JNIInvokeInterface中的函数。
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
// 而C语言版本中,JavaVM是一个JNIInvokeInterface*类型的指针变量,这里注意一下,正是因为这样,在C和C++中,才会出现使用上的
//差别。
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

知道了他们的调用区别之后,再看一下JavaVM到底有什么用,那就要看他的函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* C++ version.
*/
struct _JavaVM {
const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
jint DestroyJavaVM()
// 释放JavaVM
{ return functions->DestroyJavaVM(this); }
// 将当前线程附着到虚拟机
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
// 讲当前线程和虚拟机分离
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
// 获取ENV
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
// AttachCurrentThreadAsDaemon()函数在JNI中的作用是将当前线程附加到Java虚拟机中作为一个守护线程,以便在非Java线程中调用Java API,并使得当所有非守护线程结束时,Java虚拟机可以退出。
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

通过上面的注释可以看到,JavaVM提供了获取JNIEnv的函数,将当前线程附着到java虚拟机中以及分离等功能。

本质上就是和javaVM取得联系,使得我们可以和java端进行通信,因为虚拟机并不知道我们C/C++层的线程,所以他们不能直接通信,所以,需要将线程附着到虚拟机上,这样我们就可以获得虚拟机的环境,从而和Java端通信。

二. NDK开发之JNIEnv

​ 前面了解了JavaVM之后,我们再看一下JNIEnv,了解一下它是干什么的。同样的,看一下它的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* C++ object wrapper.
*
* This is usually overlaid on a C struct whose first element is a
* JNINativeInterface*. We rely somewhat on compiler behavior.
*/
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;

#if defined(__cplusplus)


// ... 省略若干
jstring NewString(const jchar* unicodeChars, jsize len)
{ return functions->NewString(this, unicodeChars, len); }

jsize GetStringLength(jstring string)
{ return functions->GetStringLength(this, string); }

const jchar* GetStringChars(jstring string, jboolean* isCopy)
{ return functions->GetStringChars(this, string, isCopy); }

void ReleaseStringChars(jstring string, const jchar* chars)
{ functions->ReleaseStringChars(this, string, chars); }

jstring NewStringUTF(const char* bytes)
{ return functions->NewStringUTF(this, bytes); }

jsize GetStringUTFLength(jstring string)
{ return functions->GetStringUTFLength(this, string); }

const char* GetStringUTFChars(jstring string, jboolean* isCopy)
{ return functions->GetStringUTFChars(this, string, isCopy); }

void ReleaseStringUTFChars(jstring string, const char* utf)
{ functions->ReleaseStringUTFChars(this, string, utf); }

jsize GetArrayLength(jarray array)
{ return functions->GetArrayLength(this, array); }

jobjectArray NewObjectArray(jsize length, jclass elementClass,
jobject initialElement)
{ return functions->NewObjectArray(this, length, elementClass,
initialElement); }

jobject GetObjectArrayElement(jobjectArray array, jsize index)
{ return functions->GetObjectArrayElement(this, array, index); }

void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)
{ functions->SetObjectArrayElement(this, array, index, value); }
// ... 省略若干
#endif /*__cplusplus*/
};

看一下上面的函数,时不是很眼熟,就是前面我们讲过的常用函数的使用,所以JNIEnv的作用就是提供了我们和JavaVM通信的一些接口函数。

另外,它和JavaVM一样,也分别定义了C++和C的版本。原理也是一样的,在C++中通过一个结构体封装了一下,C中直接使用的是指针重命名的。所以调用上也和JavaVM一样。

1
2
3
4
// C++中
env->xxxxx();
// C中(这里为什么要带*,是因为它是一个指针的指针)
(*env)->xxxxx();

三. 全局引用和局部引用和弱全局引用

局部引用:

引用在C++和java以及一些面向对象的语言中都存在,对于传递给原生方法的每个参数,以及JNI函数返回的几乎每个对象都属于局部引用,这意味着,局部引用在当前线程中的当前原生方法运行期间有效。在原生方法返回后,即使对象本身继续存在,该引用也无效。

这适用于jobject的所有子类,包括jclass、jstring和jarray。
所有我们在函数中直接获取到的对象都是局部的,如果想让它在全局可用,需要将他变成全局引用。

这个规则对于JNIEnv不适用,它属于线程。

全局引用:局部引用在函数返回后便不能使用了,但如果想让局部引用在函数返回后依旧可以使用,就要将它变成全局的。

将一个局部引用变成全局引用可以通过NewGLobalRef方法,比如我们不想在每个地方都去获取java的class对象,就可以在一次获取后,将他变成全局的。

1
2
3
4
5
6
7
8
jclass localClazz;
jclass globalClazz;

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

// TODO 记住,一定要记住,在不使用它的时候要进行释放,否则,内存泄漏
env->DeleteGlobalRef(globalClass);

以上两种是我们比较常用的,还有一种引用叫弱全局引用

弱全局引用:

它是全局引用的一种类型,与全局引用一样,它也可以在方法返回之后,依旧可以使用,但和全局引用不同的是弱全局引用不会阻止潜在的对象被垃圾回收,所以使用的时候记得检测这个对象是否还有效,避免崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jclass localClazz;
jclass weakGlobalClazz;

jclass localClass = env->FindClass("MyClass");
// 将局部引用变成弱全局引用
jclass weakGlobalClazz = reinterpret_cast<jclass>(env->NewWeakGlobalRef(localClass));

// 使用时检查,IsSameObject判断两个引用是否引用同一对象,必须使用 IsSameObject 函数,不能用 ==
// 这里和NULL比较,如果不同,说明有效
if (JNI_FALSE == env->IsSameObject(weakGlobalClazz, NULL))
{
// 说明对象还存活,可以使用
}else{
// 被回收了,不能再用了
}

四. JNI_OnLoad

最后一个知识点,JNI_OnLoad,这个函数我们还没有使用过,这是一个被系统调用的函数,当库被加载的时候,系统会主动调用它。

那么它有什么用处?

一般情况下,我们在这个函数中获取JavaVM 对象保存到全局。 或者在这里动态注册JNI函数。再或者在这里获取一些类的class缓存起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}

// Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
jclass c = env->FindClass("com/example/app/package/MyClass");
if (c == nullptr) return JNI_ERR;

// Register your class' native methods.
static const JNINativeMethod methods[] = {
{"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
{"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
};
int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
if (rc != JNI_OK) return rc;

return JNI_VERSION_1_6;
}

以上就是今天所讲解的全部内容了。这章内容稍微偏理论,我们将在下节课讲解其他内容时用到上面的所有知识点,所以务必掌握。

在jni中,数据类型和java中的数据类型是不同的,在了解java和jni类型之间的关系之后,我们才能更好的在java和native之间传递数据。

数据类型的分类

基本类型

java jni C/C++ 大小
boolean jboolean uint8_t 无符号8位
byte jbyte int8_t 有符号8位
char jchar uint16_t 无符号16位
short jshort int16_t 有符号16位
int jint int32_t 有符号32位
long jlong int64_t 有符号64位
float jfloat float 32位
double jdouble double 64位

引用类型

java jni C C++
java.lang.Class jclass jobject _jclass*
java.lang.Throwable jthrowable jobject _jthrowable*
java.lang.String jstring jobject _jstring *
Other objects jobject void * _jobject*
java.lang.Object[] jobjectArray jarray _jobjectArray*
boolean[] jbooleanArray jarray _jbooleanArray*
byte[] jbyteArray jarray _jbyteArray*
char[] jcharArray jarray _jcharArray*
short[] jshortArray jarray _jshortArray*
int[] jintArray jarray _jintArray*
long[] jlongArray jarray _jlongArray*
float[] jfloatArray jarray _jfloatArray*
double[] jdoubleArray jarray _jdoubleArray*
Other arrays Jarray jarray jarray*

对于引用类型,我们知道,在java中,所有类的父类都是java.long.Object,由于C语言中没有类的概念,所以在C语言中,使用void *代替,这是一个万能类型,在C++中定义了一个空的类 class _jobject {};来代替java中的类。

基本类型在jni.h中的定义:

1
2
3
4
5
6
7
8
9
10
11
12
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */

/* "cardinal indices and sizes" */
typedef jint jsize;

以下是使用C语言时jni.h中定义的数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* Reference types, in C.
*/
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jobjectArray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jobject jthrowable;
typedef jobject jweak;

以下是使用C++时jni.h定义的数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* Reference types, in C++
*/
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;

typedef 是为类型定义别名,从上面的代码来看,除了基本类型,在引用类型中,C最终是可以看成是void *,

而C++可以看成_jobject*。

全部写成void* 或者_jobject* 也是可以的,这里这么写主要是为了可读性和可扩展性。

字符集

1
2
3
在讲解数据类型的操作函数之前,先看一下字符集,为什么要讲解字符集,回到前面仔细观察一下基本类型的char,这个类型,在java中
和C/C++中占用的空间是不同的。熟悉C的朋友都知道,C语言中的char类型占用的是一个字节,而在java中char是占用了两个字节的,
而jchar也是占用了两个字节。这是因为java和C/C++使用的字符集不同,java使用的是Unicode字符集,而C使用的是ASCII字符集。

Unicode字符集

1
2
3
4
Unicode是一个全球性的字符编码标准,它为世界上几乎所有的字符(包括各种文字、符号、标点符号等)都分配了唯一的代码点。每个代码
点用十六进制表示,例如U+0041代表拉丁字母"A",U+4E2D代表汉字"中"。Unicode字符集的目标是为了包含地球上所有的书写系统。

Unicode有不同的编码方案,最常见的是UTF-8、UTF-16和UTF-32。这些编码方案允许将Unicode代码点转换为字节序列以便存储和传输。

ASCII字符集

1
2
3
4
5
6
7
8
9
10
11
12
13
ASCII字符集仅包含128个字符,包括英文字母、数字和一些特殊符号。这个字符集不足以表示全球范围内的所有字符,因此对于多语言支持
和Unicode字符,C语言需要借助宽字符类型(wchar_t)和相关的函数。

所以,总的来说,Unicode字符集是一个包含全球字符的标准,可以表示各种语言和符号。而C中的char类型在默认情况下使用的是较小的字
符集(通常是ASCII或其扩展),需要通过宽字符类型和函数来支持Unicode字符。要在C中完全支持Unicode字符集,推荐使用wchar_t和
相关的宽字符函数。

由于上面的不同,所以在使用jchar的时候要格外注意。如果java中的字符串仅包含了ASCII的字符,可以直接将jchar转换成char使用,这
是因为ASCII字符在Unicode中的表示和ASCII字符集中的表示是一致的。

但是,如果java中的字符串包含了非ASCII的字符,比如汉字(一个字节无法表示),则需要注意字符编码的转换,一般这种情况可以使用java
中的String类的getBytes方法将字符串转化为字节数组,指定为UTF-8字符集,然后在C中直接使用对应的字符集函数将字节数组转换成char
数组。

所以,接下来我们看一下常用的几类数据类型的操作。

常用数据操作函数

基本数据类型操作

字符转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extern "C"
JNIEXPORT void JNICALL
Java_com_jiangc_example1_MainActivity_charTest(JNIEnv *env, jobject thiz, jchar a, jchar b) {
int codePoint = (int) b;
char utf8Char[4];
if (codePoint < 0x80) { // 128
utf8Char[0] = (char) codePoint;
utf8Char[1] = '\0';
} else if (codePoint < 0x800) { // 对于UTF-8编码来说,对于范围在0x80到0x7FF之间的Unicode字符用两个字节
// 0x7ff
utf8Char[0] = (char)(0xC0 | (codePoint >> 6));
utf8Char[1] = (char)(0x80 | (codePoint & 0x3F));
utf8Char[2] = '\0';
}else{
utf8Char[0] = (char)(0xE0 | (codePoint >> 12));
utf8Char[1] = (char)(0x80 | ((codePoint >> 6) & 0x3F));
utf8Char[2] = (char)(0x80 | (codePoint & 0x3F));
utf8Char[3] = '\0';
}
LOGE("jchar ============================== a = %c b = %s\n", a, utf8Char);
}

字符串创建

1
2
3
4
5
6
7
8
9
10
11
12
// 使用示例
jstring str = env->NewStringUTF("hello world");

// 函数含义
// 这个函数在正常情况下会返回一个jstring,可以直接返回给JAVA端使用。
// 使用时需要做判断,如下
if (NULL == str)
{
// 出错了
}
// 其他深入的用法后面再说,这里先了解简单用法

获取java端的字符串并转化成C/C++端字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* TODO 获取jstring转化成C字符串
* string: 要转换的Java字符串(jstring对象)
* isCopy: 一个可选的指向jboolean变量的指针。当isCopy不为NULL时,
* 函数将通过它返回一个标志,表示返回的C字符串是从Java字符串拷贝而来(JNI_TRUE)
* 还是直接指向Java字符串的内部数据(JNI_FALSE)。
*
* 函数返回值:
* 如果成功,返回一个指向UTF-8编码的C字符串的指针。
* 如果发生错误(如内存不足),返回NULL。
*
* TODO: 最终,这里调用的都是三个参数的 GetStringUTFChars(JNIEnv*, jstring, jboolean*);
* 只是,封装了一次罢了,这里我们先不管env是什么,后面再讲,先使用两个参数的函数
*/
const char *cString = env->GetStringUTFChars(str, NULL);
if (NULL == cString) {
LOGE("获取C字符串失败\n");
return env->NewStringUTF("error");
}
// TODO: 这里注意,一定一定要记得释放,否则会内存泄漏
// TODO: 即便返回的字符串直接指向java字符串的内部数据,也不要修改它,不然会导致一些不确定的行为。
env->ReleaseStringUTFChars(str, cString);

函数的使用看上面的代码注释就可以了,需要注意的就是,在不使用的情况下,要对字符串进行释放,而且不要尝试修改获取到的字符串。

获取字符串的长度

1
2
3
4
5
6
// 这个视频里面没有讲,函数使用也很简单,看一下就知道了
jsize GetStringLength(jstring string);
// 这个函数返回一个jsize,也就是jint,也就是int

// 使用方式:
int length = env->GetStringLength(str);

数组操作

注意:对于数组,我们只讲一种类型,因为其他类型操作方式一样,大家可以自己尝试使用一下其他类型的数组函数

复制数组到native

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 这个函数是将java端的数组内容复制到native端提供的数组中
* @param array java端数组
* @param start 从哪里开始复制,比如从0开始
* @param len 复制几个元素
* @param buf native端的数组地址
*/
void GetIntArrayRegion(jintArray array, jsize start, jsize len,
jint* buf);
// 使用也很简单

// TODO 1. 数组复制
int *pArray = nullptr;
pArray = static_cast<int *>(malloc(length * sizeof(int)));
// 数组复制到了pArray中
// 对应了 Get<Type>ArrayRegion
env->GetIntArrayRegion(array, 0, length, pArray);

这样,就可以对数组进行操作了,但是,操作的修改不会被同步到java中,因为是复制的。

如果想将修改同步到java端,需要进行提交操作

数组设置(将native中的数组数据同步到java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 将native的数组内容设置到java端的数组中
* @param array 被设置的java端的数组
* @param start 从哪里开始设置
* @param len 设置几个元素
* @param buf native的数组
*/
void SetIntArrayRegion(jintArray array, jsize start, jsize len,
const jint* buf);
// 修改
pArray[3] = 9;
LOGE("after pArray[3] = %d\n", pArray[3]);
// 如果想给数组提交到java,可以使用下面的方法
// 对应了Set<Type>ArrayRegion
env->SetIntArrayRegion(array, 0, length, pArray);

注意: 一般来讲,我们不会大量使用复制数组,然后将改变的数据再同步到java,这样效率太低,一般使用指针的方式进行操作。

获取java端数组指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 获取java端数组指针
* @param array
* @param isCopy
* @return
*/
jint* GetIntArrayElements(jintArray array, jboolean* isCopy);
// 函数使用方法如下:
// 对应了 Get<Type>ArrayElements
jint *elements = env->GetIntArrayElements(array, nullptr);
if (nullptr == elements) {
return nullptr;
}
// 通过指针访问数组
for (int i = 0; i < length; ++i) {
elements[i] = i + 1;
}

// TODO 在获取到数组指针之后也要记得释放,否则会内存泄漏
/**
* 这个函数,前面两个参数一看就知道了,主要说明一下最后一个参数
* mode: 释放数组元素时的标志
* 主要有以下取值:
* 0: 对Java的数组进行更新并释放C/C++的数组
* JNI_COMMIT :对Java的数组进行更新但是不释放C/C++的数组,一般用于周期性的修改java数组
* JNI_ABORT:对Java的数组不进行更新,释放C/C++的数组
*/
// 对应了 Release<Type>ArrayElements
env->ReleaseIntArrayElements(javaArray, elements, 0);

注意: 和string一样,凡是获取了指针的,一律记得释放,否则内存泄漏。

前言

做Android平台SDK开发,在Android 8.0版本之后面临不少系统权限问题,经过这几年的开发和适配以及平时的探索,我在系统权限和安全方面学习和掌握了不少这方面的知识。

本文基于Android 11.0, 主要介绍下SELinux和SEAndroid,本文在阅读参考了国内外不少大牛的资料基础上,加上平时的实战总结出来的,希望对大家有所帮助。

自编故事

很久以前,有个公司成立了,公司里有总经理张总一人,秘书小王一人,还有李赵等小兵若干,有一天总经理写了一封信,信中写了公司员工每个人的薪水等级,写完后,放在一个柜子里,

结果总经理为了方便,设置了柜子权限为老铁666,然后下班回家睡觉了

秘书小王有事进到张工办公司发现没人,本来想离开的,但是看到屋里有个柜子,上面钥匙挂在上面(可读写),小王就忍不住打开偷看了一下,看完之后,心生一计,偷偷改了自己的薪水等级,然后关上柜子走了

小李小赵有事请教来找张总,结果也发现了柜子,然后也偷偷改了数据(可读写),

等第二天张总上班后,再拿出来信的时候,倒吸一口凉气,大事不妙。。。。

心想,这种不小心就把文件的权限暴露给其他人的情况,太不安全了,必须要改革!

张总熬了几个通宵,掉了一把头发,终于新的机制2.0诞生了,

2.0模式是这样的,公司里面的每个人都有一个公司派发的身份证,每个文件,也有一个身份证,公司还雇用了一个保安大爷(董事长化身),实时监控每个人对于每个文件的行为。

首先张总写信的时候,还是保持以前的模式,每次都检查下其他人是否有可读写的权限,

然后小李在打开柜子想要读写信的时候,保安是会立即检查小李的身份证和信的身份证,然后在一台只能读的电脑上,搜索小李的权限,看看人有没有读写信的权限

如果没有的话,那么保安会立即传送到小李身边,揪住小李拖出去,然后利用公司大喇叭广播所有人,小李想读信,被我阻止了,大家下次别干了啊,

张总每天最开心的事,就是听保安喋喋不休的说谁谁谁没干成什么什么事。。。。

安全系统的重要性

对于安卓系统,比如一台手机,如果系统不安全,不稳定,那么很可能就会出现如下问题:

  • 系统经常崩溃,影响用户体验
  • 对于各种bug的第三方应用,影响系统使用
  • 对于恶意应用,会入侵入侵用户的私有数据,串改数据等
  • 系统被网络黑客入侵与控制

可以想象,对于生产厂家和用户来说,如上的情况是多么的糟糕,公司倒闭,各种被唾弃都能可能出现。。。

所以构建一个坚固的系统多么的重要

Linux标准安全机制介绍:

众所周知,Android底层是Linux内核,那么关于安全部分,肯定离不开Linux,首先简单介绍下Linux的权限机制。

我们平时登陆 Linux 系统时,虽然输入的是自己的用户名和密码,但其实 Linux 并不认识你的用户名称,它只认识用户名对应的 ID 号(也就是一串数字)。

Linux 系统中,每个用户的 ID 细分为 2 种,分别是用户 ID(User ID,简称 UID)和当前工作组 ID(Group ID,简称 GID),这与文件有拥有者和拥有群组两种属性相对应,另外还有一个用户所有组ID(Groups ID,简称GIDS)

Linux id命令用于显示用户的ID,以及所属群组的ID

1
2
pi@raspberrypi:~ $ id
uid=1000(pi) gid=1000(pi) groups=1000(pi),4(adm),20(dialout),24(cdrom),27(sudo),29(audio),44(video),46(plugdev),60(games),100(users),105(input),109(netdev),997(gpio),998(i2c),999(spi)

如上,可以看到我的树莓派当前uid是1000,当前工作组gid是1000,当前用户所属的所有组的gids是后面那一长串

更多id 命令用法,请用id --help

同样,linux进程也有uid,gid,gids,某个用户启动的进程,那么这个进程就会继承用户的uid,gid,gids

查看命令为ps -aux

1
2
3
4
5
6
7
pi@raspberrypi:~ $ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 2 0.0 0.0 0 0 ? S 09:43 0:00 [kthreadd]
root 510 0.2 0.4 32796 17400 ? S 09:43 0:05 /usr/bin/vncserver-x11-core -service
pi 617 0.0 0.0 9932 3096 ? Ss 09:43 0:00 /usr/bin/vncserver -depth 16 -geometry 1024x768 :1
pi 782 0.0 0.0 9788 2556 pts/0 R+ 10:24 0:00 ps -aux
pi 1638 0.0 0.0 8516 3776 pts/0 Ss 09:43 0:00 bash

如上截取部分输出,可以看到我用当前用户pi执行ps -aux这个执行,那么其进程的uid(USER那一列为pi

最后介绍linux文件系统,只有uid gid 以及相对应的rwx权限

查看命令为ls -l 可以看到文件型态、权限、拥有者、文件大小以及时间等内容

1
2
3
4
5
6
7
8
9
10
pi@raspberrypi:~/Downloads $ touch abc
pi@raspberrypi:~/Downloads $ ls -l
total 0
-rw-r--r-- 1 pi pi 0 Jul 18 10:32 abc
pi@raspberrypi:~/Downloads $ sudo touch bbb
pi@raspberrypi:~/Downloads $ ls -l
total 0
-rw-r--r-- 1 pi pi 0 Jul 18 10:32 abc
-rw-r--r-- 1 root root 0 Jul 18 10:32 bbb
pi@raspberrypi:~/Downloads $

如上,我在当前目录下使用当前用户pi创建了一个文件,那么从结果来看-rw-r--r-- 1 pi pi 0 Jul 18 10:32 abc我们就知道了这个文件的uid为pi ,gid为pi,其rwx权限为-rw-r--r--,其中第一个横杠-代表是一个文件,其余的9位,以3位为一组,分别代表当前用户对着文件的所有权,当前用户组对这个文件的所有权,以及其他用户组对这个文件的所有权,

当我切换到root用户去创建一个文件夹的时候,那么可以看到其uid gid也变成了root

总结一下:

  • 系统上的每个进程(运行中的程序)都作为一个特定的用户来运行
  • 每个文件都归一个特定的用户所有
  • 对资源(文件和目录等)的访问受用户所限制
  • 正在运行的进程所关联的用户可以决定该进程可访问的资源(文件/目录等)

DAC介绍

全称:Discretionary access control (DAC) 自主式权限控制

概念:如果一个用户拥有一个文件,那么用户可以允许自由的控制文件的读写和执行权限,那么就叫DAC

上一节介绍的Linux标准安全机制就是属于DAC,那么这个机制有什么特点(缺点)吗

  1. 资源所有权掌握在特定用户的手里,导致资源可能被乱用
  2. 用户的区分度比较低,系统上只有两个特权等级:普通user和超级用户root
  3. 读权限可以转移,导致信息可以任意流动,比如用户A允许文件被B用户读,那么B读到文件后,可以转手传递给C

可以看到这种DAC模式无论是用户和资源都难以控制,因此大家都想疯狂拿到root权限,然后就可以为所欲为

Android沙箱介绍

Application Sandbox,官网地址:应用沙盒

简单来说主要是

  • 基于Linux保护应用资源
  • 每个app分配唯一的UID/GID,在各自的的进程中运行
  • 互相不能访问(默认)

既然Android是基于Linux的,那么进程和资源也有uid/gid/gids,下面就介绍一下

  • 每个app安装之后都会被分配一个uidaid,源码路径/system/core/include/private/android_filesystem_config.h

  • id查看uid/gid,命令没变,cat /proc/xxx/status查看进程信息,包括uid、gid、gids

  • ps -l查看进程uid/gid 比如 u0_a36那么意思就是10000 add 36 为10036, 其中#define AID_APP 10000 /* first app user */

  • 资源或者说文件的uid/gid定义路径:/system/core/include/private/android_filesystem_config.h

  • 最后说下Android 服务service,其一般会伴随着一个xxx.rc文件,并在其中声明其uid/gid,例如下面中声明user和group,对应的就是uid和gid

    1
    2
    3
    4
    5
    6
    service surfaceflinger /system/bin/surfaceflinger
    class core
    user system
    group graphics drmrpc readproc
    onrestart restart zygote
    writepid /sys/fs/cgroup/stune/foreground/tasks
  • 另外设备节点如/dev/binder 的uid和gid一般定义在各个ueventd.rc文件中 一般放在/system/etc/ueventd.rc

那么Android中的权限Permission除了上面介绍的rwx,对于APK开发,还包括清单文件中需要配置的权限,路径:/frameworks/base/core/res/AndroidManifest.xml

无论是通过Android API,Java API,NDK C API,还是执行shell命令,那么要么在api调用过程中,如framework,要么是在底层,通过uid/gid等进行权限管理和控制。道理是相通的。

SELinux介绍

全称Security-Enhanced Linux (SELinux) ,安全增强型Linux,是增强安全的一个机制,那么就探究下到底相比DAC模式,如何增强的?

首先说下背景知识,简单了解下:

  • 是集成在kernel 2.6.x中一个安全架构
  • 是Linux Security Modules (LSM)的一部分
  • 是美国国家安全局和linux社区开发的一个项目
  • 是MAC (Mandatory Access Control)在Linux内核级的一个实现

其实最重要的就是它是MAC的一个linux实现,那么MAC是什么,跟DAC什么关系呢?

MAC介绍

  • MAC被预置到内核级的系统中,对于linux,是预置在kernel里面的
  • MAC限制主体(英文叫subject,比如用户或进程)对客体(英文名叫object,比如文件等资源)的访问操作的能力
  • MAC机制会给所有主体和客体分配一个安全标签(Security label),包括用户user,进程process,访问的资源等
  • MAC机制会定义一个清晰明确的的访问规则,限制每个用户或者进程只允许访问它已经定义好的资源,所以即使是Root权限也能被限制
  • 是非DAC的,不能被资源的所有者修改

如上可以看出,我们想要了解SELinux以及相关的概念,那么会出现很多名词,当然这些名词其实只是在不同场景下的不同叫法而已,大家习惯就好,我也是经常用,所以有时候会说出好几个名词,但是其实都是表达一个意思。

SELinux 引入**标签(label)**这个东西来进行权限的操作和规则的制定,在SELinux世界里,任何一个对象都有标签,比如进程,文件,目录等等,全都有标签,就像每个人身份证上的名字一样,与生俱来,而且每个对象的标签都是唯一的。SELinux 在做决定时,就会根据这些标签以及系统根据标签来制定的规则进行权检查。

SELinux架构流程

selinux-arc

我这边画了一个图,大致说下流程,首先主体(Subject比如user或Process)想要访问(Access) 一个客体(Object,比如一个文件file吧),那么首先需要经过DAC判断,如果DAC判断失败了(比如rwx权限不足等),那么直接就会拒绝

如果DAC验证通过,那么就会进入MAC的判断,其中MAC,我又把具体内容扩展了一下,见图中虚线方框内,

进入MAC时,首先会通过AVC(访问向量缓存),看名字cache缓存,那么缓存什么呢?缓存的是Security Server(相当于数据库) 对于主体访问客体的策略是否命中,如果命中了,那么为了提高判断效率,直接会把这个策略放到AVC里缓存,下次判断时,直接缓存里查找,速度更快。

最后通过AVC检查后,才能得到访问客体的权限,拿到相应的资源。我这里省略了图中的MLS,这个在后续会单独介绍。

如上可以知道,DAC和MAC是互为补充的,先要经过DAC后才能进行MAC,所以SELinux也叫增强安全型

SEAndroid介绍

了解过SELinux之后,那么SEAndroid就比较好理解了,其实:

SELinux + Android = SEAndroid

也就是说安卓平台对于SELinux的一个实现,或者应用于安卓上,就叫SEAndroid

官网地址:https://source.android.google.cn/security/selinux

简单说下历史演变过程:

  • Android 4.3(宽容模式)
  • Android 4.4(部分强制模式)
  • Android 5.0 及更高版本中,已全面强制执行 SELinux
  • Android 6.0 高度限制对 /proc 的访问
  • Android 8.0 更新了 SELinux 以便与 Treble 配合使用

这里解释下宽容模式(Permissive)和强制模式(Enforcing),SELinux 按照默认拒绝的原则运行:任何未经明确允许的行为都会被拒绝。SELinux 可按两种全局模式运行:

  • 宽容模式:权限拒绝事件会被记录下来,但不会被强制执行。
  • 强制模式:权限拒绝事件会被记录下来强制执行。

Android CDD

提到安卓,不得不提CDD兼容性文档,谷歌认证必备的,

CDD中对于SELinux部分也是有强制要求的,具体链接如下:https://source.android.google.cn/compatibility/11/android-11-cdd#9_7_security_features

这里我直接粘贴过来翻译过的内容

1
2
3
4
5
6
7
如果设备要使用 Linux 内核,则:
•[C-1-1] 必须实现 SELinux。
•[C-1-2] 必须将 SELinux 设置为全局强制模式。
•[C-1-3] 必须将所有域配置为强制模式。不允许使用宽容模式域,包括特定于设备/供应商的域。
•[C-1-4] 对于 AOSP SELinux 域以及特定于设备/供应商的域,不得修改、省略或替换上游 Android 开源项目 (AOSP) 中提供的 system/sepolicy 文件夹中存在的 neverallow 规则,并且政策必须在所有 neverallow 规则都存在的情况下编译。
•[C-1-5] 必须在每个应用的 SELinux 沙盒中运行面向 API 28 级或更高级别的第三方应用,并对每个应用的私有数据目录设定应用级 SELinux 限制。
•应保留上游 Android 开源项目的 system/sepolicy 文件夹中提供的默认 SELinux 政策,并且应仅针对自己的设备特定配置向该政策进一步添加内容。

可以看出,谷歌对于安卓系统的要求还是挺严格的,当然国内厂家如果不需要满足CDD的话, 很多都是为了拿到更多权限而直接宽容模式运行的,但是这样的话,系统安全就得不到保证,所以说安全和权限总是鱼和熊掌不可兼得

SEAndroid 标签Label

selinux-label

下面介绍重要概念标签label,我这里又画了一个图,便于理解这个概念,首先label是给所有对象贴的,包括进程和资源

首先标签label又叫Security Context安全上下文,然后进程的标签,我们也叫scontext(source context)源上下文,而资源的标签也叫tontext(target context)目标上下文名字如意思,比较好理解。

然后它长什么样子呢,就是user:role:domain或者type:mls_level 也就是三个冒号分隔成四个部分

其中user叫用户,在SEAndroid中只定义了一个用户叫u,

然后role角色,在SEAndroid中也定义了两个角色叫r和object_r

什么区别的,r是给进程用的,而object_r是给访问的对象或者叫资源用的,因为object就是对象的意思

然后domain或者type什么区别呢?如果进程的标签,那么就是domain域,如果是资源,那就是type

最后是mls_level在SEAndroid里面只定义了一个级别s0

其实从标签的样子,大家可以知道,四部分中主要变化的是第三部分(domain/type),所以安卓中主要以这个部分定义相关规则

SEAndroid 的标签大致分为五类:

  • 首先是Service服务相关的标签。
  • 其次,对于基于 Binder 的服务,允许向 Service Manager 注册的标签。
  • 第三,系统属性Property的标签。
  • 第四,设备节点相关的标签。
  • 第五,文件相关的标签
  • 最后,app应用相关的标签。

这里先说下SEAndroid中文件相关的标签

  • file_contexts 为文件分配标签,为/system /sys /dev /data 等文件分配标签
  • genfs_contexts 用于为不支持扩展属性的文件系统(例如,proc 或 vfat)分配标签
  • property_contexts 用于为 Android 系统属性分配标签。在启动期间,init 进程会读取此配置。
  • service_contexts 用于为 Android Binder 服务分配标签,以便控制哪些进程可以为相应服务添加(注册)和查找(查询)Binder 引用。在启动期间,servicemanager 进程会读取此配置。
  • seapp_contexts 用于为应用进程和 /data/data 目录分配标签。在每次应用启动时,zygote 进程都会读取此配置;在启动期间,installd 会读取此配置。
  • mac_permissions.xml 用于根据应用签名和应用软件包名称(后者可选)为应用分配 seinfo 标记。随后,分配的 seinfo 标记可在 seapp_contexts 文件中用作密钥,以便为带有该 seinfo 标记的所有应用分配特定标签。在启动期间,system_server 会读取此配置。

最后说下如下的信息含义

1
W com.aa.bb: type=1400 audit(0.0:199): avc: denied { call } for  scontext=u:r:system_app:s0 tcontext=u:object_r:update_engine:s0 tclass=binder permissive=0 app=com.aa.bb

我们平时一般会通过logcat查看相关打印信息,这里介绍两个命令来抓取selinux的拒绝事件打印:logcat |grep avc 或者dmess |grep avc 就会过滤出一大堆类似如上的信息,我们要从这些信息中提取出有用的关键信息,并修改系统的规则来满足我们产品的要求。

下面我会介绍SELinux的规则,并解释如上的信息含义

SEAndroid 规则1 Rule/Policy

每个对象都有标签了,那么如何利用这些标签来干事情呢,重要的规则或者叫策略登场了

基于安卓源码,介绍下SDK中源码相关的内容:

system/sepolicy 是android 关于selinux核心策略配置的所有内容,我们不应该去修改这个路径下任何文件

/device/manufacturer/device-name/sepolicy 这个路径会包含芯片厂家所有平台相关的配置,我们主要修改这里 BOARD_VENDOR_SEPOLICY_DIRS += vendor/oem/sepolicy 我一般会把自定义的规则放在自定义的目录里面,便于跟芯片厂家的分区开

*.te策略配置文件的后缀,其中的语言从global_macros, te_macros attributes 这些配置文件中读取和使用

Android 8.0 之后

  • system/sepolicy/public 平台共有策略的全部定义
  • system/sepolicy/private 平台私有规则,不会向vendor部分暴露。
  • system/sepolicy/vendor 厂商规则,可引用public的规则,不能引用private的规则
  • device/manufacturer/device-name/sepolicy 厂商自定义的规则,包括如上的vendor部分

总之大家没事可以多去上面说的几个目录下点击文件看看内容,其中厂家一般都只修改device里面定义的规则,否则可能会引起谷歌认证失败的情况。大家还是遵守规则比较好

SEAndroid 规则2

image-20210722214227470

下面介绍下*.te中常用的语法规则,目的是让大家能看到,会写

上面图中以adbd.te为例,介绍具体的内容

首先说下domain域定义的规则,如下面两行

1
2
3
type adbd, domain; //解释下type就是定义的意思,定义adbd 为domain,看到domain,意思就是adbd是给进程或者用户贴的标签类型
type adbd_exec, exec_type, file_type; //这行意思是定义一个adbd_exec这个类型,然后属于exec_type和file_type,意思就是adbd这个文件的标签为adbd_exec,那么它是可执行文件(exec_type),也是文件类型(file_type)
//图中标注为红色的,都可以通过箭头找到原始定义的源码路径,大家可以自己查看

域定义完了之后,紧接着就是定义规则了

1
allow domains types:classes permissions; //这行是语法规则,意思就是允许(allow) 某个域(domains) 对于某个资源(types):资源类型(classes) 拥有资源类型相关的权限(permissions)

举个例子

1
2
allow bootanim system_file:dir r_dir_perms;
允许 域类型为bootanim的进程 对于资源为system_file的目录dir 有读r权限

另外介绍下有4种情况:

  • allow - 允许主体对客体执行许可的操作。
  • neverallow - 表示不允许主体对客体执行制定的操作。
  • auditallow - 表示允许操作并记录访问决策信息。
  • dontaudit - 表示不记录违反规则的决策信息,切违反规则不影响运行。

SEAndroid 规格3

说下SDK编译这些规则最终的产出物策略生成位置

  • boot.img(针对非 A/B 设备)或 system.img/vendor.img(针对 A/B 设备)。

  • (/system /vendor /product)/etc/selinux 所有分区中的策略配置文件位置,包括如下内容
  • (plat vendor product)_sepolicy.cil: 所有策略转成cil (CIL(common Intermediate Language))
  • *_file_contexts: 文件的security contexts 其中* 代表plat|vendor|product

  • *_property_contexts: 属性的security contexts

  • *_seapp_contexts: App 的security contexts

  • *_mac_permissions.xml

SEAndroid 命令

平时开发中,我这里总结了常用的命令来查看或者设置SELinux相关的内容

Command Action
setenforce 0/1 或 getenforce 临时关闭或打开安全策略 ,或者获取当前策略 Permissive/Enforcing
Ls -Z 显示文件的Security Context
ps -Z 显示进程的Security Context
id (user) 显示进程/用户的Security Context
chcon 修改文件的Security Context
restorecon 恢复文件的默认Security Context
dmesg |grep avc 或 logcat |grep avc 查看所有denied的信息

如下是一些实际操作的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console:/sdcard # ps -AZ
LABEL USER PID PPID VSZ RSS WCHAN ADDR S NAME
u:r:init:s0 root 1 0 46160 6772 SyS_epoll_wait 0 S init
u:r:kernel:s0 root 2 0 0 0 kthreadd 0 S [kthreadd]
u:r:netd:s0 root 306 281 17776 2632 pipe_wait 0 S iptables-restore
u:r:audioserver:s0 audioserver 332 1 58772 17608 binder_ioctl_write_read 0 S audioserver
u:r:su:s0 root 5414 5149 10864 2932 0 0 R ps
console:/sdcard # id system
uid=1000(system) gid=1000(system) groups=1000(system) context=u:r:su:s0
console:/ $ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1007(log),3009(readproc) context=u:r:shell:s0
console:/ $ id log
uid=1007(log) gid=1007(log) groups=1007(log) context=u:r:shell:s0
console:/sys/fs # ls -Zl
total 0
drwxrwxrwt 2 root root u:object_r:fs_bpf:s0 0 2021-07-20 23:28 bpf
dr-xr-xr-x 2 root root u:object_r:sysfs:s0 0 2021-07-20 23:51 cgroup
drwxr-xr-x 10 root root u:object_r:sysfs:s0 0 2021-07-20 23:51 ext4
drwxr-xr-x 3 root root u:object_r:sysfs_fs_f2fs:s0 0 2021-07-20 23:28 f2fs
drwxr-xr-x 3 root root u:object_r:sysfs:s0 0 2021-07-20 23:28 fuse
drwxr-xr-x 3 root root u:object_r:sysfs:s0 0 2021-07-20 23:51 incremental-fs
dr-xr-x--- 2 system log u:object_r:pstorefs:s0 0 2021-07-20 23:28 pstore
drwxr-xr-x 8 root root u:object_r:selinuxfs:s0 0 1970-01-01 08:00 selinux

这里说一下,系统开机后,selinux会把文件系统挂在到/sys/fs/selinux这个节点下面,里面有所有android所定义的对象以及相应的权限控制,部分列举如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
console:/sys/fs/selinux/class # ls -l
total 0
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 binder
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 blk_file
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 bluetooth_socket
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 capability
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 capability2
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 chr_file
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 dir
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 fd
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 fifo_file
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 file
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 filesystem
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 hwservice_manage
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 ipc
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 keystore_key
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 lnk_file
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 packet_socket
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 process
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 process2
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 property_service
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 service_manager
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 sock_file
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 socket
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 system
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 tcp_socket
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 udp_socket
dr-xr-xr-x 3 root root 0 2021-07-20 23:28 unix_stream_socket
console:/sys/fs/selinux/class/binder/perms # ls -lZ
total 0
-r--r--r-- 1 root root u:object_r:selinuxfs:s0 0 2021-07-20 23:28 call
-r--r--r-- 1 root root u:object_r:selinuxfs:s0 0 2021-07-20 23:28 impersonate
-r--r--r-- 1 root root u:object_r:selinuxfs:s0 0 2021-07-20 23:28 set_context_mgr
-r--r--r-- 1 root root u:object_r:selinuxfs:s0 0 2021-07-20 23:28 transfer
console:/sys/fs/selinux/class/binder/perms #

如上可以看到binder所有的权限,可以与其它进程进行binder ipc通信(call),能够向这些进程传递Binder对象(transfer),以及将自己设置为Binder上下文管理器(set_context_mgr)

具体可以查看每个class里面 perms文件夹内的内容。

SEAndroid 实战1 Service

seandroid-service

对于在 Android 6.x (Marshmallow) 之后添加的 Service,如果缺少相关文件或编写不正确,则 开机时service是无法正常启动和运行的。

图中定义了一个mytest_service的服务,声明了一个mytest.rc文件,这样放入system/etc/init/mytest.rc里面,那么开机后是无法启动的,原因就是缺少安全策略

那么如何定义安全策略呢?

seandroid-service-ok

1
2
3
4
//一共需要如下这些文件
mytest_service
mytest.rc
file_context

想开机运行服务,需要将相关内容添加到与服务相关的安全标签中(图中file_contexts 和mytest.te)即可

但是这种开机起来的服务是不能注册到binder里面的,也就是不能作为binder服务

那么如何作为binder服务并开机注册呢?请看下图

seandroid-service-binder

1
2
3
4
5
6
7
//一共需要如下这些文件
mytest_service
mytest.rc

file_context
service_context.te
service.te

SEAndroid 实战2 Property

Google在Android O以后,为了降低vendor和system之间的耦合度,对property的作用区域也做了明确的区分,分为vendor的property和system里的property.

一般我们自定义property的时候,OEM厂家都应该以vendor.开头,或者persist,ro这种的,而不能是xx.yy.zz,否则谷歌认证会报错

property

1
2
3
4
//一共需要如下这些文件
mytest.te //主要是声明权限控制的
property_contexts //主要是定义你的某个prop的完整标签上下文
property.te //定义一个新标签类型type

这样,你的property,开机后就会能读取或者写入等操作,

也可以使用getprop -Z 来看你定义的prop的标签内容

SEAndroid 实战3 Device设备节点

项目中有可能需要自定义了一个设备节点,比如/dev/test_dev ,然后要求访问特定设备节点,

那么在SEAndroid中也需要设置相关文件

同时如果你发现系统内某个设备节点无法访问,权限不足,那么你也有能需要重新修改下这个设备节点的SELinux标签,满足权限要求

下面介绍下

device

比如一个服务mytest_service想要打开一个设备节点,如上图,那么一般会设计到如下设置

device-context

1
2
3
4
涉及到设备相关的文件
device.te //主要是定义一个设备节点的type
file_contexts //给具体的设备节点路径定义一个完整的标签,标签中加入你新定义的type
mytest.te //最后在你的服务的域中,定义相关的权限即可

当然,如果系统内已经定义好了相关设备节点的标签,那么你直接修改为你自定义的,然后权限自然就有了哦

查看设备节点的标签命令ls -Z 因为设备节点属于资源,不是进程

SEAndroid 实战4 Binder/HIDL

在Android 8.0以后,system和vendor进行了隔离,那么就引入了HIDL来进行通信,其实也是binder通信,

这里总结下Android中的三种binder,以及涉及到的SELinux文件,大家具体可以查看自己家平台里面相关的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//System binder service相关
file_contexts
service.te
service_context
servicemanager.te

//HIDL binder service相关
file_contexts
hwservice.te
hwservice_context
hwservicemanager.te

//vendor中的binder service相关
file_contexts
vndservice
vndservice_contexts
vndservicemanager.te

MLS介绍

当大家查看进程标签时ps -AZ,一般会发现如下的标签中有个c512,c768,这种的

1
u:r:platform_app:s0:c512,c768  u0_a58     747    287 1140388 136964 SyS_epoll_wait      0 S com.android.systemui

SEAndroid里,只定义了s0一个敏感度sensitive,但是定义了0~1023个category。在敏感度只有一个值的情况下,其实MLS已经变成了MCS(Multi-category Security),多组安全。MCS用于隔离,阻断不同组之间的信息流动。颗粒度更细

相关源码定义:

system/sepolicy/private/mls 主要策略控制位置

mls-level

如上 小写l代表level,小写t 代表type,

l1表示subject的MLS level,l2表示object的MLS level

t1表示subject的type,t2表示object的type

上图的mlsconstrain规则,定义了只有在满足下面四个条件其中之一的情况下,才能对系统的任何目录和文件拥有写、追加、重命名等权限:

(1)dir/file的类型为app_data_file(t2 == app_data_file,t2表示object的类型)

(2)主体和客体的MLS相等(l1 eq l2,l1表示subject的MLS level,l2表示object的MLS level)

(3)主体拥有mlstrustedsubject属性(t1 == mlstrustedsubject )

(4)客体拥有mlstrustedobject属性(t2 == mlstrustedsubject )mlstrustedsubject和mlstrustedobject分别是subject和object的属性,相应的类型关联到这两种属性后,可以绕过MLS的限制。

system/sepolicy/private/mls_decl 使用了宏生成sensitive和category

1
system/sepolicy/private/mls_macros

/system/sepolicy/private/seapp_contexts 文件中的levelFrom字段决定应用和目录/文件的level

external/selinux/libselinux/src/android/android_platform.c 文件中,通过对levelFrom的值的判断,赋予应用、应用的数据对应level。

mls-levelfrom

levelcode

Code & Doc

主要代码和文档汇总

谷歌在线文档:https://source.android.google.cn/security/selinux

源码路径 :/system/sepolicy

其他在线文档,排名不分先后

https://jung-max.github.io/2019/09/16/Android-SEAndroid%EC%A0%81%EC%9A%A9/

https://milestone-of-se.nesuke.com/en/sv-advanced/selinux/selinux-summary/

https://blog.csdn.net/innost/article/details/19299937/

https://blog.csdn.net/luoshengyang/article/details/35392905

http://androidxref.com/

https://www.jianshu.com/p/3f6006e74821

感谢!

预编译库和源码编译的库的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
预编译库及非预编译库是什么
- 预编译库是指已经经过编译的二进制文件,可以直接在 Android 应用中使用如静态库(.a)或共享库(.so)。
预编译库可以直接加载到应用程序中,并通过JNI与 Java 层进行交互
- 非预编译库是指将 C/C++ 代码直接放在 Android 项目中,并在构建时进行编译,这种方式会将 C/C++ 代码与 Java 代码一起
编译成最终的应用程序,而不是单独生成预编译库。

什么是动态库:
动态库是一种可重复利用的代码和数据的集合,它可以在程序运行的时候动态的加在和连接到应用程序中。在编译时动态库不会被连接到应
用程序中,而是在程序运行时被加载到内存中。动态库的主要优点是它可以被多个程序共享,并且可以在运行时更新或替换。这意味着一个
库被更新时,无需重新编译整个程序,只需要替换对应的库文件就可以了,这样可以降低程序维护的成本,提供更好的灵活性。

什么是静态库:
静态库也是一种包含了可重复利用的代码和数据的集合,它和动态库最大的不同就是,动态库在程序运行时,需要的时候才会加载链接,而静态
库在编译的时候就会被直接链接到程序中,成为程序的一部分。

相对动态库而言,静态库是独立存在的,动态库是共享的,这是他们最重要的区别之一,也正是因为这个原因,静态库相对于动态库而言,对其
他的依赖更低。运行速度相对静态库会快一些,因为程序在执行过程中不需要再额外加载和链接动作。

什么时候用静态库,什么时候用动态库?
一般情况下,可以从几个方面考虑:
a. 内存: 一些通用的库可以使用动态库,它是共享的,相对节省内存,比如log,utils
b. 存储:动态库只需要一份就可以了,所以依赖越多,相对空间节省越多
c. 环境: 如果是标准环境,比如pc,对于通用库一般依赖动态库(比如系统库),但如果是一些环境情况不清楚,或者为了减少兼容性问题,则
选择静态库
d. 速度: 静态库运行速度比动态库快(理论上是,实际上基本察觉不出);

编译ffmpeg

1
2
3
4
5
6
7
8
1.官网下载:download下面有个more release版本;https://ffmpeg.org/download.html#releases
2.Download gzip tarball (选择压缩包下载)
3.linux下执行解压(tar vxf ffmpeg-6.1.tar.gz)
4.touch build.sh 新建build.sh文件
5.gedit build.sh打开文件,将下面的build.sh文件内的内容复制进去
6.通过./build.sh命令执行开始编译
//报错bash: ./build.sh: Permission denied
//解决:chmod 777 build.sh

build.sh的文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/bin/bash

# 指定ndk路径
NDK=/usr/local/android-ndk-r21d

# 指定平台路径
PLATFORM=$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot

# 指定交叉编译链
TOOLCHHAINS=$NDK/toolchains/llvm/prebuilt/linux-x86_64

#可变参数
API=""
ABI=""
ARCH=""
CPU=""
CC=""
CXX=""
CROSS_PREFIX=""
OPTIMIZE_CFLAGS=""
#关闭ASM:仅在X86架构上使用,实际使用发现--disable-x86asm并没有什么用,在Android API>= 23时还是会出现 has text relocations的问题,其他ABI没有问题,所以X86在编译的时候需要加上 --disable-asm
DISABLE_ASM=""
#输出路径
PREFIX=./android


API=21

function buildFF
{
echo "开始编译ffmpeg $ABI"

./configure \
--prefix=$PREFIX/$ABI \
--target-os=android \
--cross-prefix=$CROSS_PREFIX \
--arch=$ARCH \
--cpu=$CPU \
--sysroot=$PLATFORM \
--extra-cflags="-I$PLATFORM/usr/include -fPIC -DANDROID -mfpu=neon -mfloat-abi=softfp $OPTIMIZE_CFLAGS" \
--cc=$CC \
--ar=$AR \
--cxx=$CXX \
--enable-shared \
--enable-runtime-cpudetect \
--enable-gpl \
--enable-cross-compile \
--enable-jni \
--enable-mediacodec \
--enable-decoder=h264_mediacodec \
--enable-hwaccel=h264_mediacodec \
--disable-x86asm \
--disable-debug \
--disable-static \
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-postproc \
--disable-avdevice \
--disable-symver \
--disable-stripping \
$DISABLE_ASM

make -j4
make install

echo "编译结束"
}

# armv7-a
function build_armv7()
{
API=21
ABI=armeabi-v7a
ARCH=arm
CPU=armv7-a
CC=$TOOLCHHAINS/bin/armv7a-linux-androideabi$API-clang
CXX=$TOOLCHHAINS/bin/armv7a-linux-androideabi$API-clang++
CROSS_PREFIX=$TOOLCHHAINS/bin/arm-linux-androideabi-
DISABLE_ASM=""
# 编译
buildFF
}


# armv8-a aarch64
function build_arm64()
{
API=21
ABI=arm64-v8a
ARCH=arm64
CPU=armv8-a
CC=$TOOLCHHAINS/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHHAINS/bin/aarch64-linux-android$API-clang++
CROSS_PREFIX=$TOOLCHHAINS/bin/aarch64-linux-android-
OPTIMIZE_CFLAGS="-march=$CPU"
DISABLE_ASM=""
# 编译
buildFF

}

# x86 i686

function build_x86()
{
API=21
ABI=x86
ARCH=x86
CPU=x86
CC=$TOOLCHHAINS/bin/i686-linux-android$API-clang
CXX=$TOOLCHHAINS/bin/i686-linux-android$API-clang++
CROSS_PREFIX=$TOOLCHHAINS/bin/i686-linux-android-
OPTIMIZE_CFLAGS="-march=i686 -mno-stackrealign"
DISABLE_ASM="--disable-asm"
# 编译
buildFF

}

# x86_64
function build_x86_64()
{

API=21
ABI=x86_64
ARCH=x86_64
CPU=x86-64
CC=$TOOLCHHAINS/bin/x86_64-linux-android$API-clang
CXX=$TOOLCHHAINS/bin/x86_64-linux-android$API-clang++
CROSS_PREFIX=$TOOLCHHAINS/bin/x86_64-linux-android-
OPTIMIZE_CFLAGS="-march=$CPU"
DISABLE_ASM=""
# 编译
buildFF
}


# all
function build_all()
{
make clean
build_armv7
make clean
build_arm64
make clean
build_x86
make clean
build_x86_64
}


# 编译全部
build_all

cmake基础语法(判断语句)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 声明最小版本
cmake_minimum_required(VERSION 3.18.1)

# 声明项目名称
project(test)
#[[
################################## 首先说最常见的if else ##################################
# 在讲if else之前,先看一下变量的使用
# 一般使用set(SET)命令声明一个变量
# 下面声明了一个变量BL 它的值是1
set(BL 1)
# 移除一个变量,这样,这个变量就不能用了
unset(BL)


# 下面这些都是代表true
# true: 1, ON, YES, TRUE, Y,非0值
# 下面这些代表false
# false: 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND

# 比如下面定义一个变量
set(BT FALSE)
set(TEST NO)
if (${BT})
message("if BT is true")
elseif(${TEST})
message("if TEST is true")
else()
message("else else else")
endif() # 这里是结束,别忘了,其中elsif可以不要,直接结束
]]

####################################### 再说 for循环 ######################################

# 在讲for循环之前还要插入讲一下数组的定义

# 下面定义了一个数组,从1 ~ 10
set(array_list 1 2 3 4 5 6 7 8 9 10)
#[[
# 第一种,常用的
foreach(i ${array_list})
message(" i = ${i}")
endforeach()
]]

#[[
# 第二种
foreach(i IN LISTS array_list)
message(" i = ${i}")
endforeach()
]]

#[[
# 第三种 RANGE
foreach(i RANGE 1 10 2) # 后面三个数字的意思是,从1~10的范围,每次步长为2, 打印1,3,5,7,9
message(" i = ${i}")
endforeach()
]]

#[[
# 第四种 RANGE
foreach(i RANGE 10) # 如果只有一个数字,那就是从0开始,到10,打印0~10
message(" i = ${i}")
endforeach()
]]

#[[
# 第五种 直接列表型
foreach(i 1 2 3 4 5 6) # 直接循环 1~6
message(" i = ${i}")
endforeach()
]]



#################################### 再说 while 循环 ######################################
# 格式:
#while(表达式)
# COMMAND(ARGS...)
#endwhile(表达式)文章来源地址https://www.yii666.com/blog/366023.html

# 示例
while(NOT a STREQUAL "xxx")
set(a "${a}x")
message(">>>>>>a = ${a}")
endwhile()

多文件包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对于普通的三方库,我们可能直接就给包含到一个cmake中了,但是有的三方库相当的庞大,并且会依赖其他库,而这些依赖库又是独立的,
会独立更新,这时候我们为了方便控制,会使用多cmake文件的方式编译。

多文件方式编译关键点如下:
1. 注意依赖顺序,需要先编译被依赖的,然后再编译当前的库

关键语法:

# 包含子目录CMakeLists,这个目录下必须要有CMakeLists.txt
add_subdirectory(test)

# 报行子目录相关头文件,这样才能在主库中使用相关的函数
include_directories(test/include)

target_link_libraries( # Specifies the target library.
secondlesson

# Links the target library to the log library
# included in the NDK.
${log-lib}
# 这里可以直接使用子目录生成的这个库
test
)

示例demo:NDK/Lesson2/example2 at main · jiangchaochao/NDK · GitHub

10.Navigation

Navigation概述

1
2
Navigation是指支持用户导航、进入和退出应用中不同内容片段的交互。用于处理Fragment事务,使fragment之间可以自由切换和跳转,
同时还包括导航界面模式(例如抽屉式导航栏和底部导航),可以降低用户工作量;

Navigation组成(核心三件套)

1
2
3
4
5
1.导航图:在一个集中位置包含所有导航相关信息的 XML 资源。包含用户可以跳转的所有路径,对Navigation来说就像是地图。
2.NavHost:用来显示导航图中目标所要展示的内容。
3.NavController:在 NavHost 中管理应用导航的对象。负责NavHost里内容的改变
如果要在应用中导航,则通过NavController,沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController 就可以
在NavHost里进行跳转。
1
2
3
4
5
6
7
8
9
10
11
 // 指定Navigation的版本
def nav_version = "2.5.3"

// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"

// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

导航图使用方法

1.创建导航图

导航图是一种资源文件,其中包含Navigation所有目的地和操作。会显示应用的所有导航路径。

1.1. 具体操作

1
2
3
4
1.在“Project”窗口中,点击 res 目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。
2.在 File name 字段中输入Navigation的名称,例如“graph”。
3.从 Resource type 下拉列表中选择 Navigation,然后点击 OK。
这样就完成了空白导航图的创建,这时来到res文档下就会看到navigation文件夹还有你创建的导航图

res

2.向Activity添加NavHost

分成两种方法:

1
2
1. 通过 XML 添加
2. 使用布局编辑器添加(暂不介绍)

2.1. 通过 XML 添加

在activity中加入如下代码

1
2
3
4
5
6
7
8
9
10
<!-- 这里navGraph的值要改为自己导航图的名字 -->
<!-- app.defaultNavHost="true"表示回退栈由fragment管理 -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="409dp"
android:layout_height="729dp"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_map"
/>

3.在导航图中创建目的地

目的地相当于是导航图中的一个个地点,展示各种界面内容

3.1. 具体操作

1.双击导航图,点击右上角的Design,来到下图的 Navigation Editor 界面,点击图中标红图标,然后点击 Create new destination。

4

2.在接下来的对话框中,创建 Fragment,Android Studio会按照如下配置创建BlankFragment类和fragment_layout布局(fragment_layout中默认采用FrameLayout的布局,可以改成ConstraintLayout)

5

回到 Navigation Editor 界面就可以看到导航图中已经有了一个目的地

image-20231117101542044

3.连接目的地

操作会将一个目的地连接到另一个目的地,即一个界面是否可以跳转到另一个界面

为了演示,我在上面的基础上再建了一个BFragment

3.1. 具体操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1.在系统生成的AFragment类里的onCreateView方法里进行改动
2.通过 Navigation.findNavController(view) 方法得到对应 NavController
3.使用 NavController 里的 navigate(int) 方法进行导航,该方法的参数为两目的地之间连接的id或者要导向的目的地id

public class AFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View inflate = inflater.inflate(R.layout.fragment_a, container, false);
inflate.findViewById(R.id.tv_text).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// action_AFragment_to_BFragment为两目的地之间连接的id或者要导航向的目的地id,这个id在navigation_map
//的右上角自动生成的
Navigation.findNavController(v).navigate(R.id.action_AFragment_to_BFragment);
}
});
return inflate;
}
}

3.2Navigation的返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Navigation支持多个返回堆栈可让用户在各个页面之间自由切换,同时不会在任何页面中丢失所处的位置,不需要导航图中有对应的连接就可
以进行返回操作

具体操作:
1.在系统生成的BlankFragment类里的onCreateView方法里进行改动
2.通过 Navigation.findNavController(view) 方法得到对应 NavController
3.使用 NavController 里的 popBackStack() 方法即可完成返回

public class BFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View inflate = inflater.inflate(R.layout.fragment_b, container, false);
inflate.findViewById(R.id.tvback).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//返回
Navigation.findNavController(v).popBackStack();
}
});
return inflate;
}
}

4.在目的地间传递数据(不研究导航地图的传参了,麻烦无用)

1
2
3
Bundle bundle=new Bundle();
bundle.putString("name", "tyl");
Navigation.findNavController(v).navigate(R.id.action_AFragment_to_BFragment,bundle);

接收方通过 getArguments() 方法得到 Bundle 对象并可以使用里面的内容

1
String name = getArguments().getString("name");

//BottomNavigationView看了下可扩展性差,不适合正式项目使用,直接不采用这个框架

9.WorkManager

WorkManager概述

1
2
3
WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多
数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。
不依赖于当前用户进程:当前用户进行killed,任务能够继续执行;
1
2
3
4
5
WorkManager 适用于需要可靠运行的工作,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。例如:

向后端服务发送日志或分析数据。
定期将应用数据与服务器同步。
WorkManager 不适用于那些可在应用进程结束时安全终止的进程内后台工作。它也并非对所有需要立即执行的工作都适用的通用解决方案。

3个主要的核心类

image-20231116151435427

基础使用

1
implementation "androidx.work:work-runtime:$work_version"

work类定义 (extends Worker)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DemoWork extends Worker {
public static final String TAG = DemoWork.class.getSimpleName();
private Context context;
private WorkerParameters workerParams;

public DemoWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
this.context = context;
this.workerParams = workerParams;
}
@NonNull
@Override
public Result doWork() {
Log.e(TAG, "doWork----------**-----------: 后台任务执行了");
// 接收Activity传递过来的数据
final String dataString = workerParams.getInputData().getString("data");
Log.e(TAG, "doWork: 接收Activity传递过来的数据:" + dataString);
// 反馈数据 给 Activity
// 把任务中的数据回传到activity中
// Data outputData = new Data.Builder().putString("data", "执行完任务将结果和数据回传给Activity").build();
@SuppressLint("RestrictedApi")
Result.Success success = new Result.Success();
// return new Result.Failure(); // 本地执行 doWork 任务时 失败
// return new Result.Retry(); // 本地执行 doWork 任务时 重试
// return new Result.Success(); // 本地执行 doWork 任务时 成功 执行任务完毕
return success;
}
}

单次执行及延迟调用的示例

1
2
3
4
5
6
7
  //工作通过把WorkRequest传入WorkManager中进行定义。使WorkManager可以调度任何工作;
OneTimeWorkRequest oneTimeWorkRequest =
new OneTimeWorkRequest.Builder(DemoWork.class)
.setInitialDelay(10,TimeUnit.SECONDS)
.build();

WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);

WorkRequest 对象包含 WorkManager 调度和运行工作所需的所有信息

WorkRequest 本身是抽象基类。该类有两个派生实现,可用于创建 OneTimeWorkRequest和 PeriodicWorkRequest 请求。顾名思义,OneTimeWorkRequest 适用于调度非重复性工作,而PeriodicWorkRequest则更适合调度以一定间隔重复执行的工作。

调度一次性工作

1
2
3
4
5
6
WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);
//对于更复杂的工作,可以使用构建器:
WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
//添加自定义规则
.build();

调度定期工作

1
2
3
4
5
6
7
8
有时可能需要定期运行某些工作。例如,您可能要定期备份数据、定期下载应用中的新鲜内容或者定期上传日志到服务器。

使用 PeriodicWorkRequest 创建定期执行的 WorkRequest 对象的方法如下:
PeriodicWorkRequest saveRequest =
new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS)
// Constraints
.build();
//在此示例中,工作的运行时间间隔定为一小时

1
2
3
4
//执行加急工作 (从 WorkManager 2.7 开始支持)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
//延迟工作*
.setInitialDelay(10, TimeUnit.MINUTES)

工作约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
为了让工作在指定的环境下运行,我们可以给WorkRequest添加约束条件,常见的约束条件如下所示。 - **NetworkType**:约束运行工作
所需的网络类型,例如 Wi-Fi (UNMETERED)。 - **BatteryNotLow** :如果设置为 true,那么当设备处于“电量不足模式”时,工作不
会运行。 - **RequiresCharging**:如果设置为 true,那么工作只能在设备充电时运行。 - **DeviceIdle**:如果设置为 true,则
要求用户的设备必须处于空闲状态才能运行工作。 - **StorageNotLow**:如果设置为 true,那么当用户设备上的存储空间不足时,工作
不会运行。

例如,以下代码会构建了一个工作请求,该工作请求仅在用户设备正在充电且连接到 Wi-Fi 网络时才会运行。
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build();

WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
.setConstraints(constraints)
.build();