【NDK】项目演示-Android串口的封装工具库以及集成的几种思路

Android串口库的封装

前言

在前文中我们介绍过 NDK 的本地使用与 JNI 的语法介绍,那么这一期我们就从一个真实的实战角度出发去集成一个串口相关的第三方库。

老早之前我就分享过串口工具库,对 android-serialport 的 C 库进行调用和封装,我提供了一部分的 ABI,有些同学就问我了,哎呀,为什么没有全版本的 abi 啊?

好吧,这篇文章来了,虽迟但到。我不仅要给你全 ABI 我还要教你如何自己编译你自己的动态库。

还有一些同学可能会问了,你这个不实用啊,我开发App的用不到串口啊?

其实也没关系,大家可以学习/回顾一下思路,为什么用这个库作为开端,纯粹是这个库简单编译方便演示。

那么本文就从如何集成,如何封装,如何编译动态库的几方面介绍一下。

一、getSystemService 的 SerialManager 服务

说起串口我先叠个甲做一个防杠,是的我知道 Android 内部有现成的串口通讯服务。

但是相关的接口类都被隐藏起来了,不能被用户APP直接使用。先假设相关接口没有被隐藏,使用起来其实很简单。

1.得到SerialManager实例

ini 复制代码
SerialManager serialManager = (SerialManager) context.getSystemService("serial");

2.取到可用的端口

ini 复制代码
String[] ports = serialManager.getSerialPorts();

if (ports != null && ports.length > 0) {
String portName = ports[0];
}

3.打开

ini 复制代码
SerialPort serialPort = serialManager.openSerialPort(portName, SPEED);

4.读写

ini 复制代码
ByteBuffer inputBuffer;
int ret = serialPort.read(inputBuffer);
private void writeBuffer(byte[] sendData) throws IOException {
   outputBuffer.clear();
   outputBuffer.put(sendData);
   serialPort.write(outputBuffer, sendData.length);
}

如何使用系统的串口服务呢?理论上有两种方案,拷贝源码和反射。

1.打开系统源码,将SerialManager.java、SerialPort.java SystemProperties及它们引用到的其它隐藏类的源码拷到我们的项目工程中,包括包名。

但是会基于依赖安卓版本的变动可能有变化,

反射的用法

ini 复制代码
//得到系统的SerialManager实例
Object serialManager = context.getSystemService("serial");
try {
   //反射得到SerialManager实例的getSerialPorts方法
   Method methodGetSerialPorts = serialManager.getClass().getMethod("getSerialPorts");
   //执行SerialManager实例的getSerialPorts方法
   String[] ports = (String[])methodGetSerialPorts.invoke(serialManager);

   if (ports != null && ports.length > 0) {
      String portName = ports[0];
      //反射得到SerialManager实例的openSerialPort方法
      Method methodOpenSerialPort = serialManager.getClass().getMethod("openSerialPort",String.class,int.class);

      //执行SerialManager实例的openSerialPort方法,得到SerialPort对象实例
      Object serialPort = methodOpenSerialPort.invoke(serialManager,portName,115200);

      //反射得到SerialPort实例的write方法
      Method methodWrite = serialPort.getClass().getMethod("write",ByteBuffer.class,int.class);

      //发送数据,收数据与此类似
      ByteBuffer outputBuffer = ByteBuffer.allocate(1024);
      byte[] sendData = new byte[]{0x00};
      outputBuffer.put(sendData);
      methodWrite.invoke(serialPort,outputBuffer,sendData.length);
   }


} catch (NoSuchMethodException e) {
   e.printStackTrace();
} catch (InvocationTargetException e) {
   e.printStackTrace();
} catch (IllegalAccessException e) {
   e.printStackTrace();
}

当然如果你不是普通的App开发,如果你是系统应用的开发,例如OEM厂商,你可以添加系统应用权限的方式来使用

xml 复制代码
<manifest ...>
    <uses-permission android:name="android.permission.SERIAL_PORT"/>
    
    <!-- 声明为系统应用 -->
    <application android:sharedUserId="android.uid.system">
        ...
    </application>
</manifest>

但是毕竟都有局限性,设备是否root,能否访问到串口文件?

如果是 androidthings 的设备我们还能使用对应 implementation 'com.google.android.things:androidthings:1.0' 的方式来访问 PeripheralManager 服务去调用。

这又是另一个话题了,这里不做扩展,本文只是 NDK 方向的介绍,使用 Google 第三方的 serialport 如何集成如何封装的示例。

二、android-serialport-api 的源码集成

首先 Google 的串口工具源码在此【android-serialport-api】

源码就一个类,然后就是配置了 android.mk 的配置文件,这个在前文的 NDK 中我们介绍过,使用的是 ndk-build 的方式。

当然它这个项目有一点老,新版的AS估计都很难运行了,这里把最新配置放出来

build.gradle:

ini 复制代码
plugins {
    alias(libs.plugins.androidLibrary)
}

android {
    namespace = "java.android.serialport"
    compileSdk = 34

    defaultConfig {
        minSdk = 21

        ndk {
            abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
        }

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

    //配置外部原生构建
    externalNativeBuild {
        ndkBuild {
            path = file("src/main/jni/Android.mk")
        }
    }
}

dependencies {
    implementation(libs.androidx.appcompat)
}

项目结构:

我们导入了 C 文件,和对应的 Java 类, SerialPort.java 与 SerialPortFinder.java

需要注意 SerialPort.c 中定义了 open 和 close 的 JNI 方法,需要看看你的 SerialPort 类的路径然后在 SerialPort.c 中重新配置下,这样就能运行了。

使用的方式:

ini 复制代码
mSerialPort = new SerialPort(new File(path), baudrate, 0);

InputStream in = serialPort.getInputStream();
OutputStream out = serialPort.getOutputStream();

其实就是打开对应的串口文件,如果 FD 不为空,就可以获取它的输入输出流,通过输出流传输指令,通过输入流来获取结果。

需要注意的是都需要在线程中进行处理,感觉有点麻烦于是我肯就可以把输入输出的内部操作进行封装。

三、加入工具类、监听回调的封装

项目结构如下:

回调:

OnOpenSerialPortListener: 打开串口的监听

java 复制代码
package android.serialport;


import java.io.File;

public abstract interface OnOpenSerialPortListener {
    public abstract void onFail(File paramFile, Status paramStatus);

    public abstract void onSuccess(File paramFile);

    public static enum Status {
        NO_READ_WRITE_PERMISSION, OPEN_FAIL;

        private Status() {
        }
    }
}

OnSerialPortDataListener :串口信息传递的监听

java 复制代码
package android.serialport;

public abstract interface OnSerialPortDataListener {
    public abstract void onDataReceived(byte[] paramArrayOfByte);

    public abstract void onDataSent(byte[] paramArrayOfByte);
}

SerialPortReadThread :读取串口 InputStream 的线程处理

csharp 复制代码
public abstract class SerialPortReadThread extends Thread {
    private static final String TAG = SerialPortReadThread.class.getSimpleName();
    private InputStream mInputStream;
    private byte[] mReadBuffer;

    public SerialPortReadThread(InputStream paramInputStream) {
        this.mInputStream = paramInputStream;
        this.mReadBuffer = new byte['?'];
    }

    public abstract void onDataReceived(byte[] paramArrayOfByte);

    public void release() {
        interrupt();
        InputStream localInputStream = this.mInputStream;
        if (localInputStream != null) {
            try {
                localInputStream.close();
                this.mInputStream = null;
                return;
            } catch (IOException localIOException) {
                localIOException.printStackTrace();
            }
        }
    }

    public void run() {
        super.run();
        while (!isInterrupted()) {
            try {
                if (this.mInputStream == null) {
                    return;
                }
                int i = this.mInputStream.read(this.mReadBuffer);
                if (-1 != i) {
                    if (i <= 0) {
                        return;
                    }
                    byte[] arrayOfByte = new byte[i];
                    System.arraycopy(this.mReadBuffer, 0, arrayOfByte, 0, i);
                    onDataReceived(arrayOfByte);
                } else {
                }
            } catch (IOException localIOException) {
                localIOException.printStackTrace();
                return;
            }
        }
    }

    public void start() {
        try {
            super.start();
            return;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

SerialPortManager: 工具类的入口,继承自SerialPort 内部实现接口的回调,线程的处理,命令的发送与接收等操作:

kotlin 复制代码
public class SerialPortManager extends SerialPort {
    private static final String TAG = SerialPortManager.class.getSimpleName();
    private FileDescriptor mFd;
    private FileInputStream mFileInputStream;
    private FileOutputStream mFileOutputStream;
    private OnOpenSerialPortListener mOnOpenSerialPortListener;
    private OnSerialPortDataListener mOnSerialPortDataListener;
    private Handler mSendingHandler;
    private HandlerThread mSendingHandlerThread;
    private SerialPortReadThread mSerialPortReadThread;
    private Timer timer;

    private void startReadThread() {
        this.mSerialPortReadThread = new SerialPortReadThread(this.mFileInputStream) {
            public void onDataReceived(byte[] paramAnonymousArrayOfByte) {
                if (SerialPortManager.this.mOnSerialPortDataListener != null) {
                    SerialPortManager.this.mOnSerialPortDataListener.onDataReceived(paramAnonymousArrayOfByte);
                }
            }
        };
        this.mSerialPortReadThread.start();
    }

    private void startSendThread() {
        this.mSendingHandlerThread = new HandlerThread("mSendingHandlerThread");
        this.mSendingHandlerThread.start();
        this.mSendingHandler = new Handler(this.mSendingHandlerThread.getLooper()) {
            public void handleMessage(Message paramAnonymousMessage) {
                byte[] byteMessages = (byte[]) paramAnonymousMessage.obj;
                if ((SerialPortManager.this.mFileOutputStream != null) && (byteMessages != null) && (byteMessages.length > 0)) {
                    try {
                        //根据消息写入命令byte
                        SerialPortManager.this.mFileOutputStream.write(byteMessages);
                        if (SerialPortManager.this.mOnSerialPortDataListener != null) {
                            SerialPortManager.this.mOnSerialPortDataListener.onDataSent(byteMessages);
                        }
                        return;
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
    }

    private void stopReadThread() {
        SerialPortReadThread localSerialPortReadThread = this.mSerialPortReadThread;
        if (localSerialPortReadThread != null) {
            localSerialPortReadThread.release();
        }
    }

    private void stopSendThread() {
        this.mSendingHandler = null;
        HandlerThread localHandlerThread = this.mSendingHandlerThread;
        if (localHandlerThread != null) {
            localHandlerThread.interrupt();
            this.mSendingHandlerThread.quit();
            this.mSendingHandlerThread = null;
        }
    }

    /**
     * 暴露方法,每秒发送指令
     */
    public void beginBytes(final byte[] paramArrayOfByte) {
        this.timer = new Timer();
        TimerTask timerTask = new TimerTask() {
            public void run() {
                SerialPortManager.this.sendBytes(paramArrayOfByte);
            }
        };
        this.timer.schedule(timerTask, 0L, 1000L);
    }

    /**
     * 暴露方法,关闭串口
     */
    public void closeSerialPort() {
        if (this.mFd != null) {
            close();
            this.mFd = null;
        }
        stopSendThread();
        stopReadThread();
        FileInputStream localFileInputStream = this.mFileInputStream;
        if (localFileInputStream != null) {
            try {
                localFileInputStream.close();
            } catch (IOException localIOException1) {
                localIOException1.printStackTrace();
            }
            this.mFileInputStream = null;
        }
        FileOutputStream localFileOutputStream = this.mFileOutputStream;
        if (localFileOutputStream != null) {
            try {
                localFileOutputStream.close();
            } catch (IOException localIOException2) {
                localIOException2.printStackTrace();
            }
            this.mFileOutputStream = null;
        }
        this.mOnOpenSerialPortListener = null;
        this.mOnSerialPortDataListener = null;
    }

    /**
     * 暴露的方法,打开指定串口
     */
    public boolean openSerialPort(File paramFile, int paramInt) {
        Object localObject = TAG;
        StringBuilder localStringBuilder = new StringBuilder();
        localStringBuilder.append("openSerialPort: ");
        localStringBuilder.append(String.format("打开串口 %s - 波特率 %s", new Object[]{paramFile.getPath(), Integer.valueOf(paramInt)}));
        Log.i((String) localObject, localStringBuilder.toString());
        if (((!paramFile.canRead()) || (!paramFile.canWrite())) && (!chmod666(paramFile))) {
            Log.i(TAG, "没有读写权限");
            localObject = this.mOnOpenSerialPortListener;
            if (localObject != null) {
                ((OnOpenSerialPortListener) localObject).onFail(paramFile, OnOpenSerialPortListener.Status.NO_READ_WRITE_PERMISSION);
            }
            return false;
        }
        try {
            this.mFd = open(paramFile.getAbsolutePath(), paramInt, 0);
            this.mFileInputStream = new FileInputStream(this.mFd);
            this.mFileOutputStream = new FileOutputStream(this.mFd);
            localObject = TAG;
            localStringBuilder = new StringBuilder();
            localStringBuilder.append("openSerialPort: 串口已经打开");
            localStringBuilder.append(this.mFd);
            Log.i((String) localObject, localStringBuilder.toString());
            if (this.mOnOpenSerialPortListener != null) {
                this.mOnOpenSerialPortListener.onSuccess(paramFile);
            }
            startSendThread();
            startReadThread();
            return true;
        } catch (Exception localException) {
            localException.printStackTrace();
            OnOpenSerialPortListener localOnOpenSerialPortListener = this.mOnOpenSerialPortListener;
            if (localOnOpenSerialPortListener != null) {
                localOnOpenSerialPortListener.onFail(paramFile, OnOpenSerialPortListener.Status.OPEN_FAIL);
            }
        }
        return false;
    }

    /**
     * 暴露的方法,发送指令
     */
    public boolean sendBytes(byte[] paramArrayOfByte) {
        if ((this.mFd != null) && (this.mFileInputStream != null) && (this.mFileOutputStream != null) && (this.mSendingHandler != null)) {
            Message localMessage = Message.obtain();
            localMessage.obj = paramArrayOfByte;
            return this.mSendingHandler.sendMessage(localMessage);
        }
        return false;
    }

    /**
     * 暴露方法设置串口状态监听
     */
    public SerialPortManager setOnOpenSerialPortListener(OnOpenSerialPortListener paramOnOpenSerialPortListener) {
        this.mOnOpenSerialPortListener = paramOnOpenSerialPortListener;
        return this;
    }

    /**
     * 暴露方法设置数据发送监听
     */
    public SerialPortManager setOnSerialPortDataListener(OnSerialPortDataListener paramOnSerialPortDataListener) {
        this.mOnSerialPortDataListener = paramOnSerialPortDataListener;
        return this;
    }

    /**
     * 停止发送指令
     */
    public void stopBytes() {
        Timer localTimer = this.timer;
        if (localTimer != null) {
            localTimer.cancel();
            this.timer = null;
        }
    }
}

使用起来就很方便,例如我的场景是每一秒发送一个指令读取温度传感器的温度值,那么我就用自定义的 beginBytes 定时器发送指令。

如果只是发送一指令则调用 sendBytes 方法即可。

使用起来也很简单:

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private lateinit var mSerialPortManager: SerialPortManager
    var cmd = byteArrayOf(-91, 85, 1, -5)  //开启通信的指令
    private val mHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            when (msg.what) {
                1 -> {
                    binding.tvDesc.text = "当前温度为:${msg.obj}"
                }
            }
        }
    }

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        mSerialPortManager = SerialPortManager()
        mSerialPortManager
            .setOnOpenSerialPortListener(object : OnOpenSerialPortListener {
                override fun onFail(paramFile: File?, paramStatus: OnOpenSerialPortListener.Status) {
                    Toast.makeText(this@MainActivity, "开启串口失败$paramStatus", Toast.LENGTH_SHORT).show()
                }

                override fun onSuccess(paramFile: File) {
                    Toast.makeText(this@MainActivity, "开启串口成功", Toast.LENGTH_SHORT).show()
                }
            })
            //设置串口的数据通信回调
            .setOnSerialPortDataListener(object : OnSerialPortDataListener {
                override fun onDataReceived(paramAnonymousArrayOfByte: ByteArray) {
                    //解析返回的数据转换为摄氏度
                    val i = paramAnonymousArrayOfByte[3]
                    val f = (paramAnonymousArrayOfByte[2] + i * 255 + 20) / 100.0f
                    val message = Message.obtain()
                    message.obj = java.lang.Float.valueOf(f)
                    message.what = 1
                    mHandler.sendMessage(message)
                }

                override fun onDataSent(paramArrayOfByte: ByteArray?) {
                    Log.d("SerialPort", "发送指令:$paramArrayOfByte")
                }
            })
            .openSerialPort(File("dev/ttyS3"), 115200)  //打开指定串口

        binding.btnRead.setOnClickListener {

            startRead()
        }

    }

    private fun startRead() {
        mSerialPortManager.beginBytes(cmd)  //开启读取

    }

    override fun onDestroy() {
        super.onDestroy()
        //关闭串口释放资源
        mSerialPortManager.stopBytes()
        mSerialPortManager.closeSerialPort()
    }

}

效果:

四、使用Linux系统编译对应的动态库

当然可以直接在 AS 中运行,在 build 中也可以拿到对应的动态产物,或者直接打包 apk 文件解压之后就有对于的编译产物了,这样更为简单。

为了这口醋特意包的饺子,我们这里是特意演示 Linux 系统下面的编译场景,毕竟后面复杂的项目还是用 Linux 编译出来更方便。

为什么我们要安装 DNK 来编译而不是直接使用 CLang 编译 ?

不说别的,我们的 SerialPort.c 源码中可是有 JNI 的语法的,CLang 它也识别不了啊。

NDK 本质上是一个 Android 专用的开发工具包 ,它为开发者提供了针对 Android 平台的交叉编译工具链。这些工具链与 Linux 系统上的 gcc 或普通的 clang 编译器有很大的区别:

  • 交叉编译工具链

    • NDK 提供的工具链是专为 Android 构建的 交叉编译工具链 ,可以为目标 Android 平台生成二进制文件(例如 .so 文件)。交叉编译工具链的目标是 Android,而不是运行编译器的宿主操作系统(如 Linux)。
    • 普通的 gccclang 是为当前操作系统(如 Linux、macOS 或 Windows)编译代码的,而无法直接生成适合 Android 的二进制文件。
  • ABI(Application Binary Interface)支持

    • Android 有自己定义的一组 ABI(如 armeabi-v7aarm64-v8ax86x86_64),这些 ABI 会影响编译器如何生成二进制代码。例如,ABI 会决定函数调用约定、寄存器使用方式以及内存对齐方式等。
    • NDK 工具链内置对 Android ABI 的支持,可以确保生成的 .so 文件能够正确运行在 Android 系统中。而 Linux 上的默认 gccclang 并不了解 Android 的 ABI,也不会为其生成正确的代码。
  • Android 平台特性

    • Android 系统有一些特定的 API 和库(如 libandroid.soliblog.so 等),这些库在普通的 Linux 环境中是不存在的。
    • NDK 提供了头文件和预编译库,让您可以在 C/C++ 代码中调用 Android 特定的功能(如日志、文件访问、传感器等)。普通的 gccclang 并不了解这些 Android 特性

NDK 提供了专门的头文件(如 jni.h),这些头文件定义了 JNI 的函数和数据类型。普通的 gccclang 编译器并没有这些头文件,或者即使您手动添加了头文件,也可能缺少必要的依赖库(如 libandroid_runtime.so)。

这一点理解之后,我们确实需要使用 NDK 来编译 Android 环境与平台,那么如何在 Linux 中使用 NDK 呢?

首先如果我们的 Linux 系统没有 Android 相关的环境,我们最好是把 Android SDK 和 NDK 的环境都安装了。

第一步:安装必要的工具

bash 复制代码
# 更新系统并安装必要工具
sudo apt update && sudo apt upgrade -y
sudo apt install -y wget unzip openjdk-17-jdk android-sdk-platform-tools-common

第二步:下载并安装 Android SDK并配置环境

bash 复制代码
# 创建安装目录
mkdir -p ~/Android/android-sdk/cmdline-tools
cd ~/Android/android-sdk/cmdline-tools

# 下载最新命令行工具
wget https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip
unzip commandlinetools-linux-*.zip
rm commandlinetools-linux-*.zip

# 设置环境变量
echo 'export ANDROID_SDK_ROOT="$HOME/Android/android-sdk"' >> ~/.bashrc
echo 'export PATH="$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools"' >> ~/.bashrc
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
source ~/.bashrc

# 接受许可协议
yes | sdkmanager --licenses

# 安装基本组件
sdkmanager "platform-tools" "build-tools;34.0.0" "platforms;android-34"

第三步:下载安装 NDK 这里安装26 或 21都可以

bash 复制代码
cd ~/Android
wget https://dl.google.com/android/repository/android-ndk-r26c-linux.zip
unzip android-ndk-*.zip
rm android-ndk-*.zip

# 设置环境变量
echo 'export ANDROID_NDK_HOME="$HOME/Android/android-ndk-r26c"' >> ~/.bashrc
echo 'export PATH="$PATH:$ANDROID_NDK_HOME"' >> ~/.bashrc
source ~/.bashrc

如果想安装 NDK21 则是这个地址

ruby 复制代码
wget https://dl.google.com/android/repository/android-ndk-r21e-linux-x86_64.zip

第四步:验证安装

bash 复制代码
# 检查 SDK 版本
sdkmanager --version

# 检查 NDK 版本
$ANDROID_NDK_HOME/ndk-build --version

# 检查环境变量
echo "SDK: $ANDROID_SDK_ROOT"
echo "NDK: $ANDROID_NDK_HOME"

让我们把项目中 jni 的文件通过 SSH 或者其他方式传递到 Linux 系统中,因为我们需要指定的是 "armeabi-v7a" "arm64-v8a" "x86" "x86_64" 这四个版本,所以我们需要修改 Application.mk 文件。

makefile 复制代码
# 需要的架构
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64

# 设置最低 Android 版本
APP_PLATFORM := android-21

# 使用最小的 C++ 运行时库
APP_STL := c++_static

# 启用 RTTI 和异常处理
APP_CPPFLAGS := -frtti -fexceptions

手动指定版本与架构,然后我们安装模版创建一个 build.sh 的脚本文件。

bash 复制代码
#!/bin/bash

# 确保使用NDK 21
export ANDROID_NDK_HOME=/home/newki/Android/android-ndk-r21e
export PATH=$PATH:$ANDROID_NDK_HOME

PROJECT_ROOT=$(pwd)
ABIS=("armeabi-v7a" "arm64-v8a" "x86" "x86_64")

# 清理旧编译结果
rm -rf $PROJECT_ROOT/libs $PROJECT_ROOT/obj
mkdir -p $PROJECT_ROOT/libs

echo "开始编译 Android 串口库"

for abi in "${ABIS[@]}"; do
    echo "============================================="
    echo "编译架构: $abi"
    echo "============================================="
    
    # 设置目标平台
    case $abi in
        "armeabi-v7a")
            PLATFORM="android-21"
            ;;
        "arm64-v8a")
            PLATFORM="android-21"
            ;;
        "x86")
            PLATFORM="android-21"
            ;;
        "x86_64")
            PLATFORM="android-21"
            ;;
    esac
    
    # 执行编译
    $ANDROID_NDK_HOME/ndk-build \
        NDK_PROJECT_PATH=$PROJECT_ROOT \
        APP_BUILD_SCRIPT=$PROJECT_ROOT/jni/Android.mk \
        APP_ABI=$abi \
        NDK_APPLICATION_MK=$PROJECT_ROOT/jni/Application.mk \
        APP_PLATFORM=$PLATFORM \
        NDK_LIBS_OUT=$PROJECT_ROOT/libs \
        NDK_OUT=$PROJECT_ROOT/obj
        
    # 检查输出文件
    OUTPUT_PATH="$PROJECT_ROOT/libs/$abi/libserial_port.so"
    
    if [ -f "$OUTPUT_PATH" ]; then
        echo "✅ $abi 架构编译成功!"
    else
        echo "❌ $abi 架构编译失败!"
        exit 1
    fi
done

echo ""
echo "============================================="
echo "所有架构编译完成!"
echo "生成的库文件位于: $PROJECT_ROOT/libs"
echo "============================================="

# 清理中间文件
rm -rf $PROJECT_ROOT/obj

基本上是模板文件,你只需要修改相关的文件名和项目名即可。

授予权限并执行脚本即可完成编译

bash 复制代码
chmod +x build.sh
./build.sh

我们在 Linux 中也可以通过图形化页面查看产物

有了编译好的动态库我们就可以修改对应的安卓项目,架构如下:

此时我们的串口工具库就不需要使用 NDK 编译了,直接使用即可。 如果此时把 Java 文件再打包为 Jar 包,那么这个串口工具类就是我们常用的 SDK 了,分为jar包和so文件。

此时直接运行项目就是和之前一样的效果,无需做任何改变:

到此各种方式就集成就演示完毕了。

总结

在本篇文章中,我们深入探讨了在 Android 应用中集成和封装串口通信的相关技术,如何在 AS 中集成第三方库源码并通过 ndk-build 的方式编译。

然后我们探讨了如何自定义Java类的封装与回调,为什么不卸载 Native 层,其实原则上来讲是可以的,我们前文中就讲过如何使用 C/C++的线程与回调。但是这个工具有点特殊它返回的是 FD 目录还是需要从 Java 层的 IO 流来处理更方便一些。

最后我们演示了如何在 Linux 系统中编译动态库,并且在 AS 中通过动态库的方式引入运行。相信大家通过这个一个示例能层层递进理解 ndk 的编译与集成方式。

在这一期的简单项目筑基之后我们理清楚了思路,那么后期的实战演示中有一些其他的项目演示,有些会用源码的方式集成,有些会编译为动态库来集成,不管哪种方式都不会用 nkd-build 的方式了,无它,只是个人更喜欢 CMake 的方式而已,到时候大家注意区分即可。

那么今天的文章就到此为止了,按照惯例如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。【源码在此】

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下哦,谢谢!

Ok,完结撒花。

相关推荐
Aridvian几秒前
如何使用EventBus?
android
用户2018792831672 分钟前
SystemUI 开发实战故事:手机 "公共设施总管" 的修炼手册
android
火柴就是我7 分钟前
每日见闻之Rust中的引用
android
freyazzr9 分钟前
TCP/IP 网络编程 | Reactor事件处理模式
开发语言·网络·c++·网络协议·tcp/ip
ajassi200010 分钟前
开源 java android app 开发(十一)调试、发布
android·java·linux·开源
小刘同学++1 小时前
用 OpenSSL 库实现 3DES(三重DES)加密
c++·算法·ssl
敲代码的剑缘一心1 小时前
手把手教你学会写 Gradle 插件
android·gradle
青蛙娃娃2 小时前
漫画Android:动画是如何实现的?
android·android studio
LunaGeeking2 小时前
重要的城市(图论 最短路)
c++·算法·编程·图论·最短路·floyd
君鼎2 小时前
C++内存管理与编译链接
c++