Loading...
正在加载...
请稍候

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

✨步子哥 (steper) 2026年02月17日 05:28
# 第十章:硬件访问与原生功能集成 > **本章导读**:想象你是一位建筑师,正在设计一座横跨四个大陆的桥梁。每个大陆都有自己的地形规则、建筑法规和材料标准。在 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 的实战应用 让我们通过一段完整的代码来理解位置获取的流程: ```csharp 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 <?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 <?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 加速度计:测量重力的方向 加速度计测量的是作用在设备上的加速度,包括重力加速度。这意味着即使设备静止不动,加速度计也能检测到重力方向——这正是屏幕自动旋转的原理。 ```csharp 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 陀螺仪:测量旋转的速度 陀螺仪测量的是设备的角速度——设备旋转得有多快。与加速度计不同,陀螺仪不受重力影响,它只关心旋转本身。 ```csharp 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 磁力计:电子罗盘的实现 磁力计测量地磁场强度,是实现电子罗盘的关键传感器。 ```csharp 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` 是最简单的摄像头调用方式。它会调起系统原生的拍照界面,用户拍照后返回文件引用。 ```csharp 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` 是更好的选择。它提供了对摄像头底层流的直接访问。 ```csharp 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 通过 `StorageFile` 和 `StorageFolder` API 提供了统一的解决方案。 ### 🔓 10.5.1 文件选取器:让用户决定 `FileOpenPicker` 是访问用户文件的最佳方式,因为它能自动处理各平台的文件选择逻辑,同时尊重用户的选择权。 ```csharp 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` 类提供了统一的访问方式。 ```csharp 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 项目**中定义接口和部分声明: ```csharp // 文件位置: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 项目**中实现: ```csharp // 文件位置: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 项目**中实现: ```csharp // 文件位置: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 项目**中实现: ```csharp // 文件位置: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 项目中编写不同平台的代码: ```csharp 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 条回复

还没有人回复,快来发表你的看法吧!