uniapp UTS插件开发实战:手写原生设备信息采集插件(附完整源码)

2026-07-24 0 436

如果你在用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两套代码舒服太多了。

uniapp UTS插件开发实战:手写原生设备信息采集插件(附完整源码)
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp UTS插件开发实战:手写原生设备信息采集插件(附完整源码) https://www.taomawang.com/web/uniapp/2395.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务