一、概述 1. 整体流程 1 2 3 4 5 6 7 8 9 10 graph LR Start(开始) --> C[C代码] C --> |编译器|E[编译为.so] E --> |Java|S[JNI调用] S --> |Gradle|R[打包为aar原生插件] R --> |UniApp|H|[引入插件] H --> |HBuilder|J|[准备.so] X --> |JS|D|[JS调用] D --> End(结束)
2. 环境准备
按需安装Android Studio、模拟器、NDK 等环境,确保 adb devices 可以查看到手机设备/模拟器
安装NodeJS、HBuilder,安卓手机一台,打开开发者模式,与开发环境连接(USB或者无线调试)
二、C编译生成so文件 1. 安装 Android NDK 通过 Android Studio 安装: 打开 Android Studio -> Preferences -> Android SDK -> SDK Tools。 勾选 NDK (Side by side) 和 CMake,点击 Apply 安装。
2. 配置 NDK 环境变量 找到 NDK 的安装路径: 默认路径通常为:/Users/your_user/Library/Android/sdk/ndk/{version} 在终端中临时设置环境变量(或永久写入 ~/.bash_profile): export NDK_HOME=/path/to/your/ndk # 有{version} export PATH=$PATH:$NDK_HOME source ~/.zshrc 或者 source ~/.bash_profile
3. 编写编译脚本(NDK Build) 已完成不需要修改
4. 执行编译 打开终端,进入项目根目录(YourProject/) cd /path/to/YourProject 运行 ndk-build 命令: $ANDROID_NDK_HOME/ndk-build 编译成功后,生成的 .so 文件会出现在: YourProject/libs/arm64-v8a/md5.so
三、UniApp调用Java
下载 UniApp原生开发SDK,下载地址见App离线SDK下载。
解压后,打开示例工程UniPlugin-Hello-AS来进行插件开发
打开AndroidStudio,导入上述工程 File->Import Project,选择 UniPlugin-Hello-AS
创建 Module: AndroidStudio -> File -> New Module -> Android Library -> 输入模块名:smandroid
配置 Module Gradle: 打开 build.gradle 文件,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 repositories { flatDir { dirs 'libs' } } dependencies { compileOnly 'androidx.recyclerview:recyclerview:1.0.0' compileOnly 'androidx.legacy:legacy-support-v4:1.0.0' compileOnly 'androidx.appcompat:appcompat:1.0.0' compileOnly 'com.alibaba:fastjson:1.1.46.android' compileOnly fileTree(include: ['uniapp-v8-release.aar' ], dir: '../app/libs' ) }
点击右上角 gradle Sync Now 进行同步, 或者 File -> Sync Project With Gradle Files
打开Module根目录下的proguard-rules.pro文件,在最后一行末尾加入以下代码 -keep public class * extends io.dcloud.feature.uniapp.common.UniModule{*;}
在Module的项目文件夹下创建Module类MySMLib, 使其扩展 UniModule public class MySMLib extends UniModule
完善功能代码
扩展方法必须加上@UniJSMethod (uiThread = false or true) 注解。 UniApp 会根据注解来判断当前方法是否要运行在 UI 线程,和当前方法是否是扩展方法。
UniApp是根据反射来进行调用 Module 扩展方法,所以Module中的扩展方法必须是 public 类型。
C 代码当中的接口函数加 native 修饰符,例如: public static native int GenerateRandom(int length,byte[] randombuff);
简单返回类型可以直接返回,或者参考官方文档使用 UniJSCallback 返回。
使用callback 返回时,注意引入的JSON包用 alibaba.fastjson
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.ss.test;import com.alibaba.fastjson.JSONObject;import io.dcloud.feature.uniapp.annotation.UniJSMethod;import io.dcloud.feature.uniapp.bridge.UniJSCallback;import io.dcloud.feature.uniapp.common.UniModule;public class MySMLib extends UniModule { @UniJSMethod(uiThread = true) public void sayHi (String name, UniJSCallback callback) { if (callback != null ) { JSONObject data = new JSONObject (); data.put("re" , "Hi " + name); callback.invoke(data); } } }
调试插件,注意,是在AndroidStudio中,进行原生插件调试
打开UniApp新建一个项目,打开manifest.json文件,获取appid
在uniapp开发者后台,找到对应的appid,创建证书
查看证书详情,保存证书信息、密码、SHA1、Alias等
下载证书,放在原生插件项目的app目录下( Android Studio -> UniPlugin-Hello-AS -> app )
在 Android Studio 插件项目app目录下的build.gradle文件中,将信息依次填入文件中的signingConfigs.config配置项,然后点击右上角同步完成配置
注册插件 Android Studio 插件项目的app\src\main\assets目录下打开dcloud_uniplugins.json,在nativePlugins配置项中添加一项插件配置,type必须为module
Android Studio -> app -> build.gradle 中,引入插件 implement project(‘MySMLib’)
UniApp 集成调试
使用uni.requireNativePlugin(‘插件名’)方法调用插件
调用插件中的sayHi方法,利用回调函数获取返回值
完成相关代码编写后,需要生成uniapp本地打包资源 HBuilder->发行->生成本地App资源
将生成的打包资源复制到 Android Studio 插件项目的app\src\main\assets\apps目录下
Android Studio 在插件项目app\src\main\assets\data目录下的dcloud_control.xml文件中配置uniapp的appid
在Android Studio中运行项目, 注意选择为 app 点击 绿色三角形运行
四、Java调用.so
使用JNI调用方法,调用.so文件
1 2 3 4 5 6 public class MySMLib extends UniModule { private static final String LIB_NAME = "whatever" ; static { System.loadLibrary(LIB_NAME); } }
注意,如果需要.so 动态替换,详见下文第六部分
五、UniApp调用.so
Android Studio 点击插件项目,点击右上角 Gradle,Tasks->Other,双击 assembleRelease
注意,如果other下没有 assembleRelease,请点击 Android Studio -> Settings -> Experimental -> 选中 Configure all Gradle tasks during xxxx
编译成功后,就可以在插件目录的build/outputs/arr目录下找到编译好的插件
把插件复制到 实际 UniApp 项目中。
HBuilder -> 项目 -> 创建 nativeplugins 目录,把插件复制到 nativeplugins 目录下,注意目录层次: [Project Name] /
在smandroid下创建package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "name" : "MySMLib" , "id" : "MySMLib" , "version" : "0.0.2" , "description" : "xx插件" , "_dp_type" : "nativeplugin" , "_dp_nativeplugin" : { "android" : { "plugins" : [ { "type" : "module" , "name" : "test" , "class" : "com.example.smandroid.MySMLib" } ] , "integrateType" : "aar" , "parameters" : { } , "dependencies" : [ ] } } }
在uniapp项目的manifest.json文件中添加配置好的插件
打包自定义基座,输入打包信息、证书名、证书文件、密码等,提交 传统打包
选择自定义基座,运行 UniApp 项目
六、aar打包与.so动态替换 本方案实现了SO文件的动态更新能力,同时保证了基础功能的可用性。建议在实际业务中结合CDN版本管理和灰度发布策略,确保更新过程的稳定可靠。
1. 动态更新SO操作手册 环境准备 开发工具 - Android Studio (≥ Arctic Fox) - HBuilderX (≥ 3.6.0) - JDK 11
目录结构规划 假设目录结构如下 1 2 3 4 5 6 7 8 9 10 UniAppProject/ ├── nativeplugins/ │ └── smandroid/ │ ├── android/ │ │ ├── smandroid.aar # 主模块 │ │ └── libs/ # 初始SO库 │ │ ├── arm64-v8a/ │ │ └── x86_64/ │ └── package.json └── ... # UniApp主项目
Android模块开发 创建Android Library模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 plugins { id 'com.android.library' } android { compileSdk 34 namespace 'com.example.smandroid' sourceSets { main { jniLibs.srcDirs = [] } } packagingOptions { exclude '**/*.so' } } dependencies { compileOnly fileTree(dir: '../app/libs' , include: ['uniapp-v8-release.aar' ]) }
实现动态加载逻辑 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 public class DynamicLoader extends UniModule { private static final String LIB_DIR_NAME = "dynamic_libs" ; private static final String VERSION_FILE = "lib_versions.json" ; static { loadNativeLibs(null ); } public static void loadNativeLibs (Context context) { try { System.loadLibrary("SMAPI" ); System.loadLibrary("smJNIAPI" ); } catch (UnsatisfiedLinkError e) { if (context != null ) { File libDir = prepareLibDir(context); loadFromDir(libDir); } } } private static File prepareLibDir (Context context) { File libDir = new File (context.getFilesDir(), LIB_DIR_NAME); if (!libDir.exists()) { libDir.mkdirs(); libDir.setExecutable(true , false ); libDir.setReadable(true , false ); } return libDir; } private static void loadFromDir (File libDir) { System.load(new File (libDir, "libSMAPI.so" ).getAbsolutePath()); System.load(new File (libDir, "libsmJNIAPI.so" ).getAbsolutePath()); } }
UniApp集成配置 插件目录结构 1 2 3 4 5 6 7 8 9 10 11 smandroid/ ├── android/ │ ├── smandroid.aar │ └── libs/ # 基础版本SO │ ├── arm64-v8a/ │ │ ├── libSMAPI.so │ │ └── libsmJNIAPI.so │ └── x86_64/ │ ├── libSMAPI.so │ └── libsmJNIAPI.so └── package.json
package.json配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "id" : "smandroid" , "name" : "smandroid" , "version" : "1.0.0" , "_dp_type" : "nativeplugin" , "_dp_nativeplugin" : { "android" : { "plugins" : [ { "type" : "module" , "name" : "smandroid" , "class" : "com.example.smandroid.DynamicLoader" } ] , "integrateType" : "aar" , "libs" : [ "libs/*.so" ] , "permissions" : [ "android.permission.INTERNET" , "android.permission.WRITE_EXTERNAL_STORAGE" ] } } }
动态更新实现 版本检测接口 1 2 3 4 5 6 7 8 9 10 11 const checkUpdate = ( ) => { const plugin = uni.requireNativePlugin ('smandroid' ); plugin.checkVersion ({ currentVersion : '1.0.0' }, res => { if (res.needUpdate ) { downloadLib (res.url ); } }); }
文件下载与校验 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 @UniJSMethod public void downloadLib (String url, UniJSCallback callback) { new Thread (() -> { try { File targetDir = getLibDir(); File tempFile = downloadFile(url, targetDir); if (verifyFile(tempFile)) { replaceLib(tempFile); callback.invoke("SUCCESS" ); } else { tempFile.delete(); callback.invoke("FILE_CORRUPTED" ); } } catch (Exception e) { callback.invoke("DOWNLOAD_FAILED" ); } }).start(); } private boolean verifyFile (File file) { String expectedHash = "9f86d08..." ; return HashUtils.sha256(file).equals(expectedHash); }
安全替换机制 1 2 3 4 5 6 7 8 9 10 11 12 13 private void replaceLib (File newLib) { File libDir = getLibDir(); File destFile = new File (libDir, newLib.getName()); File tempFile = new File (destFile.getAbsolutePath() + ".tmp" ); copyFile(newLib, tempFile); if (verifyFile(tempFile)) { tempFile.renameTo(destFile); System.load(destFile.getAbsolutePath()); } }
调试与验证 查看运行时加载路径 1 adb shell cat /proc/[pid]/maps | grep .so
强制触发更新 1 2 3 4 uni.$emit('forceUpdate' , { url : 'http://test.com/libs/v2/libSMAPI.so' });
日志追踪配置 1 2 3 4 5 public void onCreate () { super .onCreate(); System.setProperty("jni.debug" , "1" ); }
七、注意事项
版本兼容性
保持SO文件与SDK版本ABI严格匹配
使用Build.SUPPORTED_ABIS
检测设备支持架构
安全策略
HTTPS传输动态库
每次加载前进行签名校验
保留旧版本回滚能力
性能优化
差分更新(bsdiff算法)
后台静默下载
安装时机选择(应用切换到后台时)
八、参考资料
https://blog.csdn.net/xiao_qiang666/article/details/136495454
https://www.imspm.com/dev/433773.html
https://blog.csdn.net/qq_43548590/article/details/132333148
https://ask.dcloud.net.cn/article/35414