如果你在用uniapp开发App,一定遇到过这种尴尬——官方API能拿到的设备信息少得可怜,想获取个CPU型号或者传感器列表根本无从下手。以前要解决这个问题,要么去插件市场碰运气,要么硬着头皮写原生插件。好在2024年uniapp重点推的UTS插件方案,让这件事变得简单了不少。下面我把整个开发过程完整记录下来,从环境配置到双端代码实现,希望能帮你少走些弯路。
一、为什么需要UTS插件
说实话,uniapp自带的设备信息API在大多数场景下够用,但一旦涉及到一些偏底层的需求,比如获取设备的传感器型号、CPU架构、内存详情等,就力不从心了。之前社区里常见的做法是用原生插件来补这个短板,但传统原生插件开发需要分别写Java和Objective-C,两套语言、两套调试流程,维护成本不低。
UTS(Uni-app TypeScript Solution)的出现算是解决了一个大痛点。它让你用类似TypeScript的语法写插件,编译时自动转成对应平台的原生代码——Android端转成Kotlin,iOS端转成Swift。这意味着你不需要深入学习两门原生语言,只要熟悉TS就能上手。而且UTS插件可以直接调用平台原生API,灵活性比传统的JS插件高出一大截。
下面我以一个实际需求为例——封装一个能读取设备详细硬件信息的插件——来完整走一遍UTS插件的开发流程。
二、环境准备与项目结构
开始之前,确保你的HBuilderX版本在3.7.0以上,这是官方开始比较稳定支持UTS的版本。我当前用的是3.8.7,整个过程下来没遇到什么奇怪的编译问题。
UTS插件的目录结构是固定的,不能随意改动。在项目的uni_modules目录下创建插件文件夹,规范的结构如下:
uni_modules/
└── uts-deviceinfo/ # 插件目录,名称自定义
├── package.json # 插件配置(必填)
├── uts/ # UTS源码目录
│ ├── index.uts # 插件入口文件
│ ├── android/ # Android平台相关UTS源码
│ │ └── device.uts
│ └── ios/ # iOS平台相关UTS源码
│ └── device.uts
└── uts.sdk/ # 原生SDK依赖(如需要)
这里的目录层级很重要。uts文件夹是核心,index.uts作为插件的统一入口,android和ios子目录分别存放平台相关的实现。package.json用来声明插件的基本信息、权限需求等。
先来看package.json怎么写:
{
"id": "uts-deviceinfo",
"name": "原生设备信息采集插件",
"version": "1.0.0",
"description": "通过UTS获取设备详细硬件信息",
"platforms": ["android", "ios"],
"dcloudext": {
"uts": {
"android": {
"minSdkVersion": 21,
"permissions": []
},
"ios": {
"deploymentTarget": "13.0"
}
}
}
}
这里有个细节值得注意:Android端的minSdkVersion我设成了21(Android 5.0),因为再低的版本很多硬件信息API不可用,设太低编译能过但运行会崩。iOS端最低支持13.0,这是考虑到UIDevice相关API的兼容性。
三、插件入口文件的设计
index.uts是整个插件的门面,它负责导出统一的接口给uniapp调用。在这里我们需要定义一个类型结构来描述设备信息,然后声明一个异步函数作为对外的API。代码看起来和写普通的TypeScript没啥区别:
// index.uts - 插件入口
export interface DeviceHardwareInfo {
cpuModel: string; // CPU型号
cpuCores: number; // CPU核心数
cpuArch: string; // CPU架构
totalRAM: number; // 总内存(GB)
availableRAM: number; // 可用内存(GB)
internalStorage: number; // 内部存储总大小(GB)
sensorList: string[]; // 传感器列表
gpuModel: string; // GPU型号
osVersion: string; // 系统版本号
deviceModel: string; // 设备型号
}
// 声明原生实现函数(条件编译引入)
// #ifdef APP-ANDROID
import { getAndroidDeviceInfo } from './android/device';
// #endif
// #ifdef APP-IOS
import { getIOSDeviceInfo } from './ios/device';
// #endif
/**
* 获取设备详细硬件信息
* 返回一个Promise,resolve时返回DeviceHardwareInfo对象
*/
export function getDeviceInfo(): Promise {
return new Promise((resolve, reject) => {
try {
// #ifdef APP-ANDROID
const info = getAndroidDeviceInfo();
resolve(info);
// #endif
// #ifdef APP-IOS
const info = getIOSDeviceInfo();
resolve(info);
// #endif
} catch (e) {
reject(new Error('获取设备信息失败: ' + (e as Error).message));
}
});
}
这里用到了UTS的条件编译指令,`#ifdef APP-ANDROID`和`#ifdef APP-IOS`会在编译时根据目标平台选择性保留对应的代码。这个机制和uniapp页面中的条件编译如出一辙,上手几乎零成本。
DeviceHardwareInfo这个接口定义了我们能拿到的所有设备信息字段。实际开发中你可以根据需求增减,比如有些场景还需要电池容量、屏幕刷新率等,都可以往里加。
四、Android端原生实现
Android端的实现在uts/android/device.uts中。UTS在Android平台上编译为Kotlin,所以我们可以直接调用Android SDK的API。这里需要用到android.os.Build、HardwarePropertiesManager、SensorManager等系统类。
先获取CPU信息。Android上获取CPU型号最靠谱的方式是读取/proc/cpuinfo,或者用Build类提供的字段。我两种方式结合起来用,确保兼容性:
// uts/android/device.uts
import { DeviceHardwareInfo } from '../index';
export function getAndroidDeviceInfo(): DeviceHardwareInfo {
// 获取CPU信息
const cpuModel = getCPUModel();
const cpuCores = getCPUCores();
const cpuArch = getCPUArch();
// 获取内存信息
const ramInfo = getRAMInfo();
// 获取存储信息
const storageInfo = getStorageInfo();
// 获取传感器列表
const sensors = getSensorList();
// 获取GPU信息
const gpuModel = getGPUModel();
return {
cpuModel: cpuModel,
cpuCores: cpuCores,
cpuArch: cpuArch,
totalRAM: ramInfo.total,
availableRAM: ramInfo.available,
internalStorage: storageInfo,
sensorList: sensors,
gpuModel: gpuModel,
osVersion: android.os.Build.VERSION.RELEASE,
deviceModel: android.os.Build.MODEL
} as DeviceHardwareInfo;
}
function getCPUModel(): string {
// 优先使用Build类获取
const hardware = android.os.Build.HARDWARE;
if (hardware != null && hardware.length > 0) {
return hardware;
}
// 降级方案:读取/proc/cpuinfo
try {
const reader = new java.io.BufferedReader(
new java.io.FileReader('/proc/cpuinfo')
);
let line: string | null = reader.readLine();
while (line != null) {
if (line.contains('Hardware')) {
const parts = line.split(':');
if (parts.length >= 2) {
reader.close();
return parts[1].trim();
}
}
line = reader.readLine();
}
reader.close();
} catch (e) {
// 读取失败返回未知
}
return 'Unknown';
}
function getCPUCores(): number {
return java.lang.Runtime.getRuntime().availableProcessors();
}
function getCPUArch(): string {
// 使用系统属性获取CPU架构
const arch = android.os.Build.SUPPORTED_ABIS[0];
if (arch != null) {
return arch;
}
return 'unknown';
}
这里有个踩坑点:读取/proc/cpuinfo在一些高版本Android上可能会因为SELinux策略被拦截,所以一定要用try-catch包裹。我一开始没加异常处理,在Android 13的测试机上直接崩了,查了半天log才发现是权限问题。
接下来是内存和存储信息的获取。Android上获取内存信息主要通过ActivityManager和StorageManager:
function getRAMInfo(): { total: number; available: number } {
const activityManager = uts.android.getUniActivity()!
.getSystemService(android.content.Context.ACTIVITY_SERVICE) as android.app.ActivityManager;
const memInfo = new android.app.ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memInfo);
// 转换为GB
const totalGB = Number((memInfo.totalMem / (1024 * 1024 * 1024)).toFixed(2));
const availGB = Number((memInfo.availMem / (1024 * 1024 * 1024)).toFixed(2));
return { total: totalGB, available: availGB };
}
function getStorageInfo(): number {
const stat = new android.os.StatFs(
android.os.Environment.getDataDirectory().getAbsolutePath()
);
const totalBytes = stat.getTotalBytes();
const totalGB = Number((totalBytes / (1024 * 1024 * 1024)).toFixed(2));
return totalGB;
}
注意这里用到了`uts.android.getUniActivity()`来获取当前Activity的上下文,这是UTS提供的一个便捷方法。如果没有它,获取系统服务会麻烦很多。
传感器列表的获取稍微绕一点。Android的SensorManager可以列出所有传感器,但我们需要的是传感器名称列表:
function getSensorList(): string[] {
const sensorManager = uts.android.getUniActivity()!
.getSystemService(android.content.Context.SENSOR_SERVICE) as android.hardware.SensorManager;
const sensorList = sensorManager.getSensorList(android.hardware.Sensor.TYPE_ALL);
const names: string[] = [];
for (let i = 0; i < sensorList.size(); i++) {
const sensor = sensorList.get(i) as android.hardware.Sensor;
if (sensor != null && sensor.getName() != null) {
names.push(sensor.getName());
}
}
return names;
}
function getGPUModel(): string {
// 通过EGL获取GPU渲染器信息
try {
const egl = javax.microedition.khronos.egl.EGLContext.getEGL() as javax.microedition.khronos.egl.EGL10;
if (egl != null) {
const display = egl.eglGetDisplay(javax.microedition.khronos.egl.EGL10.EGL_DEFAULT_DISPLAY);
if (display != null) {
const version = new Array(1);
egl.eglInitialize(display, version);
const renderer = egl.eglQueryString(
display,
javax.microedition.khronos.egl.EGL10.EGL_RENDERER
);
return renderer != null ? renderer : 'Unknown';
}
}
} catch (e) {
// EGL获取失败
}
return 'Unknown';
}
GPU信息的获取我用了EGL的方式,这个方法在大多数设备上都能拿到比较准确的GPU渲染器名称。不过在一些定制ROM上EGL可能被阉割,所以同样要做好兜底处理。
五、iOS端原生实现
iOS端的实现在uts/ios/device.uts中。UTS在iOS平台编译为Swift,可以直接调用iOS SDK。相比Android,iOS获取硬件信息的API要统一得多,大部分通过UIDevice和sysctl就能搞定。
// uts/ios/device.uts
import { DeviceHardwareInfo } from '../index';
import { Foundation } from 'Foundation';
import { UIKit } from 'UIKit';
export function getIOSDeviceInfo(): DeviceHardwareInfo {
const device = UIKit.UIDevice.currentDevice();
const cpuInfo = getIOSCPUInfo();
const ramInfo = getIOSRAMInfo();
const storageInfo = getIOSStorageInfo();
const sensors = getIOSSensorList();
const gpuModel = getIOSGPUModel();
return {
cpuModel: cpuInfo.model,
cpuCores: cpuInfo.cores,
cpuArch: cpuInfo.arch,
totalRAM: ramInfo.total,
availableRAM: ramInfo.available,
internalStorage: storageInfo,
sensorList: sensors,
gpuModel: gpuModel,
osVersion: device.systemVersion(),
deviceModel: device.model()
} as DeviceHardwareInfo;
}
function getIOSCPUInfo(): { model: string; cores: number; arch: string } {
// 使用sysctl获取CPU信息
let model = 'Unknown';
let cores = 0;
let arch = 'Unknown';
// 获取CPU型号名称(通过sysctl hw.machine)
const machineData = Foundation.NSMutableData.alloc()
.initWithLength(256);
// 此处使用sysctl调用
// 在UTS中可以通过Foundation框架进行底层调用
// 实际项目中建议使用uts.ios.invoke来调用sysctl
// iOS设备的CPU型号可以通过设备型号映射获得
const deviceModel = UIKit.UIDevice.currentDevice().model();
if (deviceModel.contains('iPhone') || deviceModel.contains('iPad')) {
// 根据设备代际推断CPU型号
model = 'Apple Silicon';
}
// 核心数通过NSProcessInfo获取
const processInfo = Foundation.NSProcessInfo.processInfo();
cores = processInfo.processorCount();
// 架构判断
const is64bit = processInfo.operatingSystemVersionString().contains('64');
arch = is64bit ? 'arm64' : 'armv7';
return { model: model, cores: cores, arch: arch };
}
iOS这边获取CPU信息确实比Android麻烦一些,主要是因为苹果对硬件信息的暴露比较保守。CPU型号通过设备型号映射来推断是业内的常见做法,核心数可以通过NSProcessInfo拿到。架构方面,现在新设备基本上都是arm64了,老旧的armv7设备已经很少见。
内存和存储信息在iOS上的获取方式:
function getIOSRAMInfo(): { total: number; available: number } {
// iOS获取物理内存大小
const processInfo = Foundation.NSProcessInfo.processInfo();
const totalBytes = processInfo.physicalMemory();
const totalGB = Number((totalBytes / (1024 * 1024 * 1024)).toFixed(2));
// 可用内存通过host_statistics获取(简化处理)
// 在实际UTS中可以通过调用mach内核函数获取
let availableGB = 0;
// 简化:使用NSProcessInfo的物理内存减去已使用估算
// 生产环境建议通过uts.ios.invoke调用host_statistics
return { total: totalGB, available: availableGB };
}
function getIOSStorageInfo(): number {
// 获取应用沙盒的存储空间(近似设备存储)
const fileManager = Foundation.NSFileManager.defaultManager();
const attributes = fileManager.attributesOfFileSystemForPath(
Foundation.NSHomeDirectory(),
null
);
if (attributes != null) {
const totalSize = attributes.objectForKey(
Foundation.NSFileSystemSize
) as Foundation.NSNumber;
if (totalSize != null) {
const totalGB = Number(
(totalSize.longLongValue() / (1024 * 1024 * 1024)).toFixed(2)
);
return totalGB;
}
}
return 0;
}
iOS上获取可用内存比较麻烦,需要用到mach内核的host_statistics函数。在UTS中可以通过`uts.ios.invoke`来调用这些底层C函数。为了简化演示,上面的代码做了省略处理。实际项目中如果你需要精确的可用内存,建议查一下host_statistics的UTS调用方式,社区里已经有人封装好了相关模块。
传感器列表和GPU信息的获取:
function getIOSSensorList(): string[] {
const sensors: string[] = [];
// iOS的传感器通过CoreMotion框架获取
// 常见的传感器包括:加速度计、陀螺仪、磁力计、气压计等
const motionManager = UIKit.UIDevice.currentDevice();
// 加速度计
sensors.push('Accelerometer');
// 陀螺仪
sensors.push('Gyroscope');
// 磁力计
sensors.push('Magnetometer');
// 气压计(iPhone 6及以上)
if (motionManager.systemVersion().floatValue() >= 8.0) {
sensors.push('Barometer');
}
// 距离传感器
sensors.push('Proximity Sensor');
// 环境光传感器
sensors.push('Ambient Light Sensor');
return sensors;
}
function getIOSGPUModel(): string {
// iOS设备GPU型号通过设备代际映射
const device = UIKit.UIDevice.currentDevice();
if (device.model().contains('iPhone')) {
return 'Apple GPU (Integrated)';
}
if (device.model().contains('iPad')) {
return 'Apple GPU (iPad)';
}
return 'Apple GPU';
}
老实说,iOS端的传感器检测确实不如Android灵活。Android可以动态枚举所有传感器,而iOS只能通过CoreMotion框架逐个检查特定传感器是否可用。上面代码里我硬编码了几种常见传感器,如果你的App需要用到特定传感器,建议在调用前通过CoreMotion的对应API检查可用性。
六、在uniapp中调用插件
插件写好后,在uniapp页面中调用就非常简单了。插件会自动被识别为uni_modules中的一个模块,直接用import引入即可:
// 在Vue页面或JS文件中调用
import { getDeviceInfo } from '@/uni_modules/uts-deviceinfo';
// 在方法中使用
async function fetchDeviceInfo() {
try {
const info = await getDeviceInfo();
console.log('CPU型号:', info.cpuModel);
console.log('CPU核心数:', info.cpuCores);
console.log('CPU架构:', info.cpuArch);
console.log('总内存(GB):', info.totalRAM);
console.log('可用内存(GB):', info.availableRAM);
console.log('存储空间(GB):', info.internalStorage);
console.log('传感器列表:', info.sensorList.join(', '));
console.log('GPU型号:', info.gpuModel);
console.log('系统版本:', info.osVersion);
console.log('设备型号:', info.deviceModel);
} catch (err) {
console.error('获取设备信息失败:', err);
uni.showToast({
title: '获取失败,请重试',
icon: 'none'
});
}
}
调用方式和普通的异步函数完全一致,用async/await处理起来很顺手。返回的数据结构就是我们定义的那个DeviceHardwareInfo,字段齐全,可以直接用来做设备指纹、性能分级等功能。
七、调试中遇到的坑
整个开发过程不算太顺利,踩了几个坑,这里记录下来:
坑一:HBuilderX的UTS编译缓存问题
有时候改了UTS源码后重新编译运行,发现改动的代码根本没生效。折腾了半天才发现是HBuilderX的UTS编译缓存没清掉。解决方法很简单——在HBuilderX菜单里找到”运行-清理UTS编译缓存”,清一下再重新编译就好了。这个在官方文档里提了一嘴但很容易忽略。
坑二:Android端获取Activity上下文失败
在部分Android设备上,`uts.android.getUniActivity()`会返回null,导致后续获取系统服务全部失败。排查后发现是在插件初始化时机太早,Activity还没完全创建好。解决方法是把获取上下文的逻辑放在实际调用时执行,而不是在模块加载时就获取。上面代码里我已经调整过了,每个函数内部才去获取上下文。
坑三:iOS端模拟器与真机表现不一致
在iOS模拟器上测试时,CPU核心数返回的是Mac的CPU核心数而不是模拟设备的。这个其实不是bug,是模拟器的特性——它共享宿主机的硬件资源。所以iOS端的测试一定要上真机,模拟器只能用来验证代码不报错。
坑四:存储空间计算在Android上的差异
用StatFs获取存储空间时,不同Android版本的API有差异。低版本用getBlockCount()乘以getBlockSize(),高版本直接用getTotalBytes()。我上面的代码用了getTotalBytes(),这在API 18以上才支持。如果你的minSdkVersion设得比较低,记得做好版本判断。
八、发布到插件市场与后续维护
插件开发完成并测试通过后,可以打包发布到DCloud插件市场。在HBuilderX中右键uni_modules下的插件目录,选择”发布到插件市场”,按提示填写信息即可。发布前注意检查package.json中的版本号和描述信息是否准确。
关于后续维护,有几个建议:
第一,Android端的/proc/cpuinfo读取方式在未来的Android版本中可能会被进一步限制,建议关注Google的动态,提前准备降级方案。第二,iOS端随着新设备的发布,CPU型号映射表需要持续更新,可以考虑做成远程配置的方式,避免频繁发版。第三,传感器列表在不同设备上差异很大,如果你的业务强依赖某个传感器,务必在插件中增加可用性检测的接口。
UTS插件还有一个好处是热更新友好。传统原生插件修改后必须重新打包,而UTS插件的代码在编译时整合进了App,但插件逻辑的调整只需要更新uni_modules中的源码即可,配合uniapp的wgt热更新方案,可以做到插件级别的热修复。这一点在线上问题修复时非常实用。
九、总结
回过头来看,UTS插件开发的门槛确实比传统原生插件低了不少。一套TypeScript语法搞定双端,编译后直接产出原生代码,省去了大量重复劳动。虽然目前UTS的生态还不如传统的Java/OC插件丰富,但官方明显在大力推这个方向,2024年几个大版本更新都在强化UTS的能力。
这篇文章里封装的设备信息插件虽然功能不算特别复杂,但它完整覆盖了UTS开发的核心流程——目录结构、入口设计、平台代码编写、条件编译、原生API调用、异常处理、以及真机调试。掌握了这些,再去做更复杂的UTS插件,比如调用第三方原生SDK、对接硬件外设等,思路是相通的。
如果你也在用uniapp开发App,不妨试试用UTS把项目中那些依赖原生能力的部分重写一遍。相信我,维护一套UTS代码比同时维护Java和OC两套代码舒服太多了。

