您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

第十章:硬件访问与原生功能集成

✨步子哥 (steper) 2026年02月17日 05:28 0 次浏览

第十章:硬件访问与原生功能集成

本章导读:想象你是一位建筑师,正在设计一座横跨四个大陆的桥梁。每个大陆都有自己的地形规则、建筑法规和材料标准。在 iOS 大陆,你需要遵守 CoreLocation 的法则;在 Android 平原,LocationManager 和动态权限是你必须应对的挑战;而在 Web 海洋中,JavaScript 的 navigator.geolocation 是唯一的通行证。Uno Platform 就是那位神奇的翻译官,它让你用一种语言——Windows.Devices 命名空间——与四个世界对话。本章将揭开这场跨平台硬件访问的"魔术"背后的秘密。

🎭 10.1 跨平台硬件访问的"魔术"

在正式深入技术细节之前,让我们先理解一个看似矛盾的现象:为什么 Uno Platform 能够让你用 Windows 的 API 在 Android、iOS 和 Web 上运行?这听起来就像是用美元在北京买早餐一样不可思议。

答案藏在编译器的魔法之中。Uno Platform 实现了 WinUI 的 Windows.Devices 命名空间,这意味着你编写的每一行 C# 代码,在编译时都会被自动"翻译"成对应平台的原生调用。当你在 Android 上调用 Geolocator.GetGeopositionAsync() 时,Uno 的内核会在底层调用 Android 的 LocationManager;在 iOS 上,它会调用 CoreLocation;在 WebAssembly 中,它会通过 JavaScript 互操作调用 navigator.geolocation.getCurrentPosition()

第一性原理:跨平台抽象的本质不是消除差异,而是将差异转移到编译时。运行时的统一体验,源于编译时的精确映射。这就像国际会议中的同声传译——听众只需要理解一种语言,而翻译官在后台完成了所有的转换工作。

🔬 10.1.1 这真的是"黑魔法"吗?

当然不是。Uno Platform 的硬件抽象层(Hardware Abstraction Layer,简称 HAL)是一个精心设计的桥梁。它由三个核心组件构成:首先是 接口层,暴露给开发者的 WinUI 风格 API;其次是 映射层,负责将通用 API 转换为平台特定调用;最后是 适配层,直接与各平台的底层框架通信。

这种架构设计有一个美妙的名字——桥梁模式(Bridge Pattern)。想象一座连接两岸的物理桥梁:桥面(接口层)对所有人都是一样的,但桥墩(适配层)必须根据河床的地质条件进行定制。Uno Platform 的 genius 就在于它为每条"河流"(平台)都准备了恰到好处的桥墩。


📍 10.2 地理位置服务:从坐标到世界

获取用户位置是移动应用的基石功能。无论是外卖配送、打车服务,还是社交签到,地理位置都是连接数字世界与物理世界的纽带。让我们以 Geolocator 类为例,深入了解 Uno Platform 如何处理这一核心功能。

🎯 10.2.1 位置服务的核心概念

在开始编码之前,我们需要理解几个关键概念。地理坐标系统是地球表面的点定位标准,最常用的是 WGS-84 坐标系,它使用经度(Longitude)和纬度(Latitude)两个数值来唯一确定地球上的任何一点。经度的范围是 $[-180°, +180°]$,以英国格林威治天文台为本初子午线;纬度的范围是 $[-90°, +90°]$,以赤道为零度。

费曼技巧提问:如果你要向一个十岁的孩子解释 GPS 定位是如何工作的,你会怎么说? 想象地球是一个巨大的橙子,你在橙子表面画了两组线:一组从北极连到南极(经线),另一组平行于赤道环绕橙子(纬线)。GPS 卫星就像天上的灯塔,它们不断发送信号说"我在这里,现在是几点"。你的手机接收到至少四颗卫星的信号后,通过计算信号传播的时间,就能算出你在橙子表面的确切位置——就像通过听到四座教堂的钟声时间差,来推断自己的位置一样。

💻 10.2.2 Geolocator 的实战应用

让我们通过一段完整的代码来理解位置获取的流程:

using Windows.Devices.Geolocation;

public class LocationService
{
    private Geolocator _geolocator;

    public LocationService()
    {
        // 创建地理定位器实例
        // Geolocator 是 Uno 对各平台定位服务的统一抽象
        _geolocator = new Geolocator();

        // 设置期望精度(单位:米)
        // 精度越高,耗电量越大,定位时间越长
        // 50米是一个平衡精度与性能的良好选择
        _geolocator.DesiredAccuracyInMeters = 50;

        // 监听位置状态变化
        // 当设备的定位能力发生变化时(如用户关闭 GPS),会触发此事件
        _geolocator.StatusChanged += OnStatusChanged;
    }

    /// <summary>
    /// 获取当前设备的地理位置
    /// </summary>
    /// <returns>包含经纬度的位置信息</returns>
    public async Task<BasicGeoposition> GetCurrentLocationAsync()
    {
        try
        {
            // GetGeopositionAsync 是一个异步操作
            // 它会触发系统的定位服务,可能需要几秒钟
            // maximumAge 参数表示可接受的缓存位置年龄(这里设为5分钟)
            // timeout 参数表示最大等待时间
            Geoposition position = await _geolocator.GetGeopositionAsync(
                maximumAge: TimeSpan.FromMinutes(5),
                timeout: TimeSpan.FromSeconds(30)
            );

            // 从 Geoposition 中提取坐标信息
            // Point.Position 是一个 BasicGeoposition 结构体
            // 包含 Latitude(纬度)、Longitude(经度)和 Altitude(海拔)
            BasicGeoposition coordinates = position.Coordinate.Point.Position;

            // 获取精度信息(单位:米)
            // 这个值告诉你坐标的误差范围
            double accuracy = position.Coordinate.Accuracy;

            // 如果有海拔信息(并非所有设备都支持)
            double? altitude = position.Coordinate.Point.Position.Altitude;

            return coordinates;
        }
        catch (UnauthorizedAccessException)
        {
            // 用户拒绝了位置权限
            // 这是最常见的异常,需要友好地引导用户去设置中开启权限
            throw new LocationPermissionDeniedException("请在系统设置中允许应用访问位置信息");
        }
        catch (TaskCanceledException)
        {
            // 定位超时
            // 可能是因为 GPS 信号弱(如室内环境)
            throw new LocationTimeoutException("定位超时,请确保处于开阔区域");
        }
        catch (Exception ex)
        {
            // 其他未预期的错误
            throw new LocationException($"定位失败: {ex.Message}");
        }
    }

    /// <summary>
    /// 持续监听位置变化
    /// </summary>
    public void StartTracking()
    {
        // 设置移动阈值(单位:米)
        // 只有当设备移动超过这个距离时,才会触发 PositionChanged 事件
        // 这对于节省电量非常重要
        _geolocator.MovementThreshold = 10; // 10米

        // 订阅位置变化事件
        _geolocator.PositionChanged += OnPositionChanged;
    }

    private void OnPositionChanged(Geolocator sender, PositionChangedEventArgs args)
    {
        // 注意:这个事件可能在非 UI 线程触发
        // 如果需要更新 UI,请使用 Dispatcher
        var newPosition = args.Position.Coordinate.Point.Position;

        // 在实际应用中,你会在这里:
        // 1. 更新地图上的标记点
        // 2. 计算与目标点的距离
        // 3. 触发地理围栏逻辑
    }

    private void OnStatusChanged(Geolocator sender, StatusChangedEventArgs args)
    {
        // 定位状态变化:Ready / Initializing / NoData / Disabled
        // 这个事件对于提供用户反馈非常重要
        var status = args.Status;
    }
}
技术术语Geoposition vs Geocoordinate vs BasicGeoposition 这三个类型经常让开发者困惑。Geoposition 是最外层的容器,包含了位置信息和时间戳;Geocoordinate 包含了更丰富的定位数据,如精度、速度、航向等;而 BasicGeoposition 是最简单的结构体,只包含经度、纬度和海拔三个数值。选择哪个取决于你需要多少信息——如果你只需要在地图上显示一个点,BasicGeoposition 就足够了。

📋 10.2.3 权限声明:不可忽视的平台配置

虽然 C# 代码是统一的,但各个平台的权限声明机制依然保持独立。这就像你持有一张全球通用的会员卡,但每个俱乐部都要求你填写自己的注册表格。

Android 平台的配置位于 Platforms/Android/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 精确定位权限(GPS + 网络定位) -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <!-- 粗略定位权限(仅网络定位) -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- Android 10+ 后台定位权限(如果需要在后台获取位置) -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
</manifest>

iOS 平台的配置位于 Platforms/iOS/Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 使用期间定位权限的描述文字 -->
    <!-- 这段文字会显示在系统的权限请求对话框中 -->
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>我们需要您的位置信息来提供附近的餐厅推荐</string>

    <!-- 始终允许定位权限的描述(如果需要后台定位) -->
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>我们需要持续获取您的位置以提供导航服务</string>

    <!-- 后台定位模式(需要在 UIBackgroundModes 中声明) -->
    <key>UIBackgroundModes</key>
    <array>
        <string>location</string>
    </array>
</dict>
</plist>

WebAssembly 平台的配置则完全不同。浏览器会在运行时自动处理权限请求,但你需要确保网站使用 HTTPS 协议——大多数现代浏览器禁止在 HTTP 下访问地理位置。在 Platforms/WebAssembly/Program.cs 中,你不需要任何额外配置,但应该在 UI 层优雅地处理权限被拒绝的情况。


📱 10.3 传感器集成:感知设备的"触觉"

如果说地理位置是应用的"宏观视觉",那么传感器就是应用的"微观触觉"。通过加速度计、陀螺仪和磁力计,你的应用能够感知设备的每一个微小动作——倾斜、摇晃、旋转,仿佛赋予了软件真实的"身体感"。

🎮 10.3.1 加速度计:测量重力的方向

加速度计测量的是作用在设备上的加速度,包括重力加速度。这意味着即使设备静止不动,加速度计也能检测到重力方向——这正是屏幕自动旋转的原理。

using Windows.Devices.Sensors;

public class AccelerometerService
{
    private Accelerometer _accelerometer;

    public void Initialize()
    {
        // 获取默认的加速度计实例
        // 如果设备不支持(如某些台式机),GetDefault 会返回 null
        _accelerometer = Accelerometer.GetDefault();

        if (_accelerometer == null)
        {
            // 设备不支持加速度计
            // 在桌面应用中,这是常见情况
            return;
        }

        // 设置报告间隔(单位:毫秒)
        // 值越小,数据更新越频繁,但耗电量也越大
        // 最小间隔由设备硬件决定,可通过 MinimumReportInterval 查询
        _accelerometer.ReportInterval = _accelerometer.MinimumReportInterval > 0
            ? _accelerometer.MinimumReportInterval
            : 16; // 约 60Hz

        // 订阅读数变化事件
        _accelerometer.ReadingChanged += OnReadingChanged;
    }

    private void OnReadingChanged(object sender, AccelerometerReadingChangedEventArgs e)
    {
        // 加速度值以 g(重力加速度)为单位
        // 1g ≈ 9.8 m/s²
        var reading = e.Reading;

        double accelerationX = reading.AccelerationX; // 左右方向的加速度
        double accelerationY = reading.AccelerationY; // 前后方向的加速度
        double accelerationZ = reading.AccelerationZ; // 上下方向的加速度(静止时约为 -1g)

        // 计算设备的倾斜角度
        // 这对于制作水平仪、赛车游戏等非常有用
        double pitch = Math.Atan2(accelerationX, Math.Sqrt(accelerationY * accelerationY + accelerationZ * accelerationZ));
        double roll = Math.Atan2(accelerationY, Math.Sqrt(accelerationX * accelerationX + accelerationZ * accelerationZ));

        // 注意:这里的 X, Y 已经在所有平台上进行了坐标系对齐
        // Uno Platform 确保 Android、iOS 和 WASM 使用相同的坐标系
        // 这意味着你不需要为不同平台编写不同的计算逻辑
    }

    /// <summary>
    /// 检测设备是否被摇晃
    /// </summary>
    public bool DetectShake(AccelerometerReading previous, AccelerometerReading current)
    {
        // 计算加速度变化量
        double deltaX = Math.Abs(current.AccelerationX - previous.AccelerationX);
        double deltaY = Math.Abs(current.AccelerationY - previous.AccelerationY);
        double deltaZ = Math.Abs(current.AccelerationZ - previous.AccelerationZ);

        // 阈值需要根据实际测试调整
        // 0.5g 是一个合理的起点
        double threshold = 0.5;

        return (deltaX + deltaY + deltaZ) > threshold;
    }
}
第一性原理:为什么需要坐标系对齐?因为不同操作系统定义的坐标系可能不同——Android 的 Y 轴向上,而 iOS 的 Y 轴向下。如果没有统一的抽象层,开发者就需要为每个平台编写不同的计算逻辑。Uno Platform 在底层处理了这些差异,让你可以用同一套代码面对所有平台。

🌀 10.3.2 陀螺仪:测量旋转的速度

陀螺仪测量的是设备的角速度——设备旋转得有多快。与加速度计不同,陀螺仪不受重力影响,它只关心旋转本身。

using Windows.Devices.Sensors;

public class GyrometerService
{
    private Gyrometer _gyrometer;

    public void Initialize()
    {
        _gyrometer = Gyrometer.GetDefault();

        if (_gyrometer == null)
        {
            // 设备不支持陀螺仪
            return;
        }

        _gyrometer.ReportInterval = 16; // 约 60Hz
        _gyrometer.ReadingChanged += OnReadingChanged;
    }

    private void OnReadingChanged(object sender, GyrometerReadingChangedEventArgs e)
    {
        // 角速度值以 度/秒 为单位
        var reading = e.Reading;

        double angularVelocityX = reading.AngularVelocityX; // 绕 X 轴旋转的速度
        double angularVelocityY = reading.AngularVelocityY; // 绕 Y 轴旋转的速度
        double angularVelocityZ = reading.AngularVelocityZ; // 绕 Z 轴旋转的速度

        // 陀螺仪的典型应用:
        // 1. 增强现实(AR)中的视角控制
        // 2. 飞行/驾驶游戏的转向输入
        // 3. 手势识别(如"翻转"动作)
    }
}

🧭 10.3.3 磁力计:电子罗盘的实现

磁力计测量地磁场强度,是实现电子罗盘的关键传感器。

using Windows.Devices.Sensors;

public class CompassService
{
    private Compass _compass;

    public void Initialize()
    {
        _compass = Compass.GetDefault();

        if (_compass == null)
        {
            return;
        }

        _compass.ReadingChanged += OnReadingChanged;
    }

    private void OnReadingChanged(object sender, CompassReadingChangedEventArgs e)
    {
        // 磁北方向(相对于磁北极,非地理北极)
        double headingMagneticNorth = e.Reading.HeadingMagneticNorth;

        // 如果设备支持,还可以获取真北方向(相对于地理北极)
        // 这需要设备具备 GPS 或网络定位能力来计算磁偏角
        double? headingTrueNorth = e.Reading.HeadingTrueNorth;

        // 注意:指南针读数会受到周围金属物体的影响
        // 在室内或靠近电子设备时,读数可能不准确
    }
}

📷 10.4 摄像头与多媒体:捕获瞬间

摄像头是现代设备最重要的传感器之一。Uno Platform 提供了两套 API 来处理不同复杂度的场景:简单拍照使用 CameraCaptureUI,高级控制使用 MediaCapture

📸 10.4.1 CameraCaptureUI:一键式拍照体验

CameraCaptureUI 是最简单的摄像头调用方式。它会调起系统原生的拍照界面,用户拍照后返回文件引用。

using Windows.Media.Capture;
using Windows.Storage;
using Windows.Storage.Streams;

public class SimpleCameraService
{
    /// <summary>
    /// 拍摄一张照片
    /// </summary>
    /// <returns>拍摄的照片文件,如果用户取消则返回 null</returns>
    public async Task<StorageFile> CapturePhotoAsync()
    {
        // 创建相机捕获 UI
        var captureUI = new CameraCaptureUI();

        // 配置照片格式
        // JPEG 适合大多数场景,PNG 适合需要透明背景的情况
        captureUI.PhotoSettings.Format = CameraCaptureUIPhotoFormat.Jpeg;

        // 设置照片分辨率
        // 设置为 null 表示让用户选择,或指定最大尺寸
        captureUI.PhotoSettings.MaxResolution = CameraCaptureUIMaxPhotoResolution.HighestAvailable;

        // 允许用户裁剪照片
        // 这对于头像上传等场景很有用
        captureUI.PhotoSettings.AllowCropping = true;

        // 调起系统相机界面并等待用户操作
        // 这是一个模态操作,会阻塞当前线程直到用户完成或取消
        StorageFile photo = await captureUI.CaptureFileAsync(CameraCaptureUIMode.Photo);

        if (photo == null)
        {
            // 用户取消了拍照
            return null;
        }

        // photo 是一个临时文件,位于应用的缓存目录中
        // 如果需要永久保存,应该将其复制到 LocalFolder
        return photo;
    }

    /// <summary>
    /// 将拍摄的照片显示在 Image 控件中
    /// </summary>
    public async Task DisplayPhotoAsync(StorageFile photoFile, Image imageControl)
    {
        if (photoFile == null) return;

        // 打开文件流
        using (IRandomAccessStream stream = await photoFile.OpenAsync(FileAccessMode.Read))
        {
            // 创建 BitmapImage 并设置源
            var bitmapImage = new BitmapImage();
            await bitmapImage.SetSourceAsync(stream);

            // 绑定到 Image 控件
            imageControl.Source = bitmapImage;
        }
    }
}

🎥 10.4.2 MediaCapture:专业级的媒体控制

对于需要实时预览、条码扫描或自定义滤镜的应用,MediaCapture 是更好的选择。它提供了对摄像头底层流的直接访问。

using Windows.Media.Capture;
using Windows.Media.MediaProperties;

public class AdvancedCameraService : IDisposable
{
    private MediaCapture _mediaCapture;
    private bool _isInitialized;

    /// <summary>
    /// 初始化摄像头并开始预览
    /// </summary>
    /// <param name="previewElement">用于显示预览的 CaptureElement 控件</param>
    public async Task InitializeAsync(CaptureElement previewElement)
    {
        _mediaCapture = new MediaCapture();

        // 配置捕获设置
        var settings = new MediaCaptureInitializationSettings
        {
            // 指定使用的摄像头(前摄或后摄)
            StreamingCaptureMode = StreamingCaptureMode.Video
        };

        try
        {
            await _mediaCapture.InitializeAsync(settings);
            _isInitialized = true;

            // 将预览绑定到 UI 元素
            previewElement.Source = _mediaCapture;

            // 开始预览
            await _mediaCapture.StartPreviewAsync();
        }
        catch (UnauthorizedAccessException)
        {
            // 摄像头权限被拒绝
            throw new CameraPermissionDeniedException("请在设置中允许应用访问摄像头");
        }
        catch (Exception ex)
        {
            // 其他初始化错误
            throw new CameraInitializationException($"摄像头初始化失败: {ex.Message}");
        }
    }

    /// <summary>
    /// 拍摄照片并保存到指定文件
    /// </summary>
    public async Task CapturePhotoAsync(StorageFile outputFile)
    {
        if (!_isInitialized)
            throw new InvalidOperationException("摄像头未初始化");

        // 创建编码属性
        var imageEncoding = ImageEncodingProperties.CreateJpeg();

        // 拍摄并保存
        await _mediaCapture.CapturePhotoToStorageFileAsync(imageEncoding, outputFile);
    }

    /// <summary>
    /// 切换前后摄像头
    /// </summary>
    public async Task SwitchCameraAsync()
    {
        // 获取所有可用的摄像头
        var allVideoDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);

        // 查找当前使用的摄像头
        var currentDevice = allVideoDevices.FirstOrDefault(d =>
            d.Id == _mediaCapture.MediaCaptureSettings.VideoDeviceId);

        // 选择另一个摄像头(前/后切换)
        var newDevice = allVideoDevices.FirstOrDefault(d =>
            d.Id != currentDevice?.Id);

        if (newDevice != null)
        {
            // 需要重新初始化 MediaCapture
            // 实际应用中,这通常需要重新创建整个 MediaCapture 实例
        }
    }

    public void Dispose()
    {
        _mediaCapture?.Dispose();
    }
}
技术术语CaptureElement 是 Uno Platform 特有的控件,用于显示摄像头的实时预览。在 WASM 平台上,它会被渲染为 HTML5 的 <video> 标签;在 Android 和 iOS 上,它使用各自的原生预览视图。

📁 10.5 文件系统与沙盒:安全的数据访问

跨平台文件访问面临两个核心挑战:路径差异(每个平台有不同的文件系统结构)和沙盒限制(移动应用只能访问受限的目录)。Uno Platform 通过 StorageFileStorageFolder API 提供了统一的解决方案。

🔓 10.5.1 文件选取器:让用户决定

FileOpenPicker 是访问用户文件的最佳方式,因为它能自动处理各平台的文件选择逻辑,同时尊重用户的选择权。

using Windows.Storage;
using Windows.Storage.Pickers;

public class FilePickerService
{
    /// <summary>
    /// 让用户选择一张图片
    /// </summary>
    public async Task<StorageFile> PickImageAsync()
    {
        // 创建文件打开选择器
        var picker = new FileOpenPicker();

        // 设置视图模式(列表或缩略图)
        // 对于图片选择,缩略图视图更友好
        picker.ViewMode = PickerViewMode.Thumbnail;

        // 设置初始位置
        // PicturesLibrary 是一个已知文件夹,会自动映射到各平台的图片目录
        picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;

        // 添加允许的文件类型
        // 用户只能看到并选择这些类型的文件
        picker.FileTypeFilter.Add(".jpg");
        picker.FileTypeFilter.Add(".jpeg");
        picker.FileTypeFilter.Add(".png");
        picker.FileTypeFilter.Add(".gif");
        picker.FileTypeFilter.Add(".bmp");

        // 显示选择器并等待用户选择
        StorageFile file = await picker.PickSingleFileAsync();

        return file; // 如果用户取消,返回 null
    }

    /// <summary>
    /// 让用户选择多个文件
    /// </summary>
    public async Task<IReadOnlyList<StorageFile>> PickMultipleFilesAsync()
    {
        var picker = new FileOpenPicker();
        picker.ViewMode = PickerViewMode.List;
        picker.FileTypeFilter.Add("*"); // 允许所有文件类型

        // 选择多个文件
        IReadOnlyList<StorageFile> files = await picker.PickMultipleFilesAsync();

        return files;
    }

    /// <summary>
    /// 让用户选择保存位置
    /// </summary>
    public async Task<StorageFile> SaveFileAsync(string suggestedName)
    {
        var picker = new FileSavePicker();

        // 建议的文件名
        picker.SuggestedFileName = suggestedName;

        // 添加可用的文件类型选项
        // 第一个类型会是默认选项
        picker.FileTypeChoices.Add("文本文档", new List<string> { ".txt" });
        picker.FileTypeChoices.Add("JSON 文件", new List<string> { ".json" });

        // 显示选择器
        StorageFile file = await picker.PickSaveFileAsync();

        return file;
    }
}

💾 10.5.2 本地应用存储:私有的数据家园

每个应用都有自己私有的存储空间,用于保存配置、缓存和用户数据。Uno Platform 通过 ApplicationData 类提供了统一的访问方式。

using Windows.Storage;

public class LocalStorageService
{
    // LocalFolder:应用私有存储,会随应用卸载而删除
    // 用于存储用户数据、数据库文件等
    private StorageFolder LocalFolder => ApplicationData.Current.LocalFolder;

    // RoamingFolder:漫游存储,会在用户的所有设备间同步
    // 注意:有大小限制(通常为 100KB),仅用于少量配置
    private StorageFolder RoamingFolder => ApplicationData.Current.RoamingFolder;

    // TemporaryFolder:临时存储,系统可能随时清理
    // 用于缓存文件、下载的临时内容等
    private StorageFolder TempFolder => ApplicationData.Current.TemporaryFolder;

    /// <summary>
    /// 保存文本内容到本地文件
    /// </summary>
    public async Task SaveTextAsync(string fileName, string content)
    {
        // 在 LocalFolder 中创建或覆盖文件
        StorageFile file = await LocalFolder.CreateFileAsync(
            fileName,
            CreationCollisionOption.ReplaceExisting
        );

        // 写入文本内容
        await FileIO.WriteTextAsync(file, content);
    }

    /// <summary>
    /// 从本地文件读取文本内容
    /// </summary>
    public async Task<string> ReadTextAsync(string fileName)
    {
        try
        {
            // 获取文件引用
            StorageFile file = await LocalFolder.GetFileAsync(fileName);

            // 读取文本内容
            return await FileIO.ReadTextAsync(file);
        }
        catch (FileNotFoundException)
        {
            return null; // 文件不存在
        }
    }

    /// <summary>
    /// 保存应用设置(键值对)
    /// </summary>
    public void SaveSetting(string key, object value)
    {
        // ApplicationDataContainer 是一个轻量级的键值存储
        // 适合存储用户偏好、应用配置等
        ApplicationDataContainer settings = ApplicationData.Current.LocalSettings;

        // 支持的基本类型:int, string, float, bool, DateTime, Guid 等
        settings.Values[key] = value;
    }

    /// <summary>
    /// 读取应用设置
    /// </summary>
    public T GetSetting<T>(string key, T defaultValue = default)
    {
        ApplicationDataContainer settings = ApplicationData.Current.LocalSettings;

        if (settings.Values.TryGetValue(key, out object value))
        {
            return (T)value;
        }

        return defaultValue;
    }

    /// <summary>
    /// 获取本地存储的路径(用于调试)
    /// </summary>
    public string GetLocalFolderPath()
    {
        // 这个路径在不同平台上不同:
        // Windows: C:\Users\{User}\AppData\Local\Packages\{AppId}\LocalState
        // Android: /data/data/{PackageId}/files
        // iOS: /var/mobile/Containers/Data/Application/{GUID}/Library
        // WASM: 浏览器 IndexedDB(虚拟文件系统)
        return LocalFolder.Path;
    }
}
费曼技巧提问:为什么移动应用不能随意访问文件系统?想象你住在一个安全的公寓里,每个住户都有自己的房间和储物柜。你可以自由地在自己的房间里存放东西,但不能随便进入别人的房间。沙盒机制就是这样一种安全边界——它保护了用户的隐私,防止恶意应用窃取其他应用的数据。

🚪 10.6 原生 API 逃逸舱:突破抽象的边界

Uno Platform 的抽象层覆盖了绝大多数常见场景,但有时候你需要调用某个平台独有的功能——比如 Android 的前台服务、iOS 的 Touch ID、或者 Web 的特定 JavaScript API。这时候,你需要使用条件编译配合部分类(Partial Classes)

🔧 10.6.1 部分类模式:优雅的平台适配

部分类是 C# 的一个强大特性,它允许将一个类的定义分散在多个文件中。在 Uno Platform 中,我们可以利用这个特性将通用接口和平台实现分离。

首先,在 Shared 项目中定义接口和部分声明:

// 文件位置:Shared/Services/DeviceInfo.cs
namespace MyUnoApp.Services
{
    /// <summary>
    /// 设备信息服务 - 跨平台接口
    /// </summary>
    public partial class DeviceInfo
    {
        /// <summary>
        /// 获取设备型号
        /// </summary>
        public partial string GetModel();

        /// <summary>
        /// 获取操作系统版本
        /// </summary>
        public partial string GetOsVersion();

        /// <summary>
        /// 检查是否为低电量模式
        /// </summary>
        public partial bool IsLowPowerMode();
    }
}

然后,在 Android 项目中实现:

// 文件位置:Platforms/Android/Services/DeviceInfo.android.cs
using Android.OS;

namespace MyUnoApp.Services
{
    public partial class DeviceInfo
    {
        public partial string GetModel()
        {
            // 使用 Android API 获取设备型号
            // Build.Model 返回设备制造商定义的型号名称
            // 如 "SM-G991B" (Samsung Galaxy S21)
            return Build.Model;
        }

        public partial string GetOsVersion()
        {
            // 返回 Android 版本,如 "12"
            return Build.VERSION.Release;
        }

        public partial bool IsLowPowerMode()
        {
            // Android 5.0+ 的省电模式检测
            if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
            {
                var powerManager = Android.App.Application.Context
                    .GetSystemService(Android.Content.Context.PowerService)
                    as PowerManager;

                return powerManager?.IsPowerSaveMode ?? false;
            }

            return false;
        }
    }
}

iOS 项目中实现:

// 文件位置:Platforms/iOS/Services/DeviceInfo.ios.cs
using UIKit;

namespace MyUnoApp.Services
{
    public partial class DeviceInfo
    {
        public partial string GetModel()
        {
            // UIDevice.CurrentDevice.Model 返回 "iPhone" 或 "iPad"
            // 如果需要具体型号(如 "iPhone14,2"),需要调用底层 sysctl API
            return UIDevice.CurrentDevice.Model;
        }

        public partial string GetOsVersion()
        {
            // 返回 iOS 版本,如 "15.4"
            return UIDevice.CurrentDevice.SystemVersion;
        }

        public partial bool IsLowPowerMode()
        {
            // iOS 9+ 的低电量模式检测
            // 需要 import UIKit
            return NSProcessInfo.ProcessInfo.LowPowerModeEnabled;
        }
    }
}

WebAssembly 项目中实现:

// 文件位置:Platforms/WebAssembly/Services/DeviceInfo.wasm.cs
using System.Runtime.InteropServices.JavaScript;

namespace MyUnoApp.Services
{
    public partial class DeviceInfo
    {
        public partial string GetModel()
        {
            // Web 平台没有传统意义上的"设备型号"
            // 可以通过 User-Agent 分析,但这不可靠
            return "Web Browser";
        }

        public partial string GetOsVersion()
        {
            // 可以通过 JavaScript 互操作获取一些信息
            // navigator.userAgent 包含浏览器和操作系统信息
            return GetUserAgent();
        }

        public partial bool IsLowPowerMode()
        {
            // Web 平台没有低电量模式的概念
            // 但可以通过 Battery API 检测电量
            return false;
        }

        // JavaScript 互操作示例
        [JSImport("globalThis.navigator.userAgent")]
        private static partial string GetUserAgent();
    }
}

🎯 10.6.2 条件编译:更精细的控制

对于简单的平台差异,你可以使用 条件编译符号 直接在 Shared 项目中编写不同平台的代码:

public string GetPlatformSpecificMessage()
{
    // 注意:这些条件编译符号由 Uno Platform 自动定义
    // 你不需要在 .csproj 中手动配置

#if WINDOWS
    return "运行在 Windows 上";
#elif __ANDROID__
    return $"运行在 Android {Android.OS.Build.VERSION.Release} 上";
#elif __IOS__
    return $"运行在 iOS {UIKit.UIDevice.CurrentDevice.SystemVersion} 上";
#elif HAS_UNO_WASM
    return "运行在 WebAssembly 上";
#else
    return "运行在未知平台";
#endif
}
第一性原理:为什么选择部分类而不是接口+依赖注入?部分类模式的优势在于编译时绑定——编译器会检查每个平台是否完整实现了所有部分方法。相比之下,接口模式需要在运行时通过反射或配置来注入正确的实现。对于平台适配这种"编译时就确定"的场景,部分类更加简洁和安全。

📝 本章小结

通过本章的学习,你已经掌握了跨平台硬件交互的精髓。从地理位置到传感器,从摄像头到文件系统,Uno Platform 不仅提供了高度抽象的 WinUI 风格 API,还通过灵活的逃逸机制确保你不会被限制在"抽象的牢笼"中。

现在,让我们回顾本章的核心要点:

第一,Uno Platform 的硬件抽象层通过编译时映射,让你用统一的 Windows.Devices API 调用各平台的原生功能。这种设计既保持了代码的简洁性,又不失平台的灵活性。

第二,权限管理是跨平台开发的永恒主题。虽然 C# 代码可以统一,但各平台的权限声明机制仍然独立,需要在对应的配置文件中正确设置。

第三,传感器数据的坐标系对齐是 Uno Platform 的一项重要工作。你不需要关心 Android 和 iOS 坐标系的差异,Uno 已经在底层为你处理好了。

第四,对于平台特有的功能,部分类模式提供了一个优雅的解决方案。它保持了共享代码的简洁,同时允许平台特定的实现自由发挥。

在下一章中,我们将进入 Uno Platform 的一个极具魅力的领域——WebAssembly 深度集成。我们将探讨如何在浏览器中发挥 C# 的极致性能,并与现有的 JavaScript 生态系统进行互操作。


动手实验
  1. 位置追踪器:创建一个简单的位置追踪应用,在地图上显示用户当前位置,并记录用户的移动轨迹。尝试处理权限被拒绝的情况,并提供友好的引导界面。
  2. 摇一摇功能:使用加速度计实现"摇一摇"功能——当用户摇晃设备时,随机显示一条名言或执行某个操作。注意设置合适的阈值以避免误触发。
  3. 跨平台文件管理器:实现一个简单的文件管理器,允许用户选择、查看和保存文本文件。在不同平台上测试,体验 Uno Platform 的文件抽象层。

讨论回复

0 条回复

还没有人回复