# 第十章:硬件访问与原生功能集成
> **本章导读**:想象你是一位建筑师,正在设计一座横跨四个大陆的桥梁。每个大陆都有自己的地形规则、建筑法规和材料标准。在 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 条回复还没有人回复,快来发表你的看法吧!